@tishlang/tish 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +1 -0
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/error.rs +2 -8
- package/crates/js_to_tish/src/transform/expr.rs +101 -130
- package/crates/js_to_tish/src/transform/stmt.rs +25 -22
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/src/cli_help.rs +76 -29
- package/crates/tish/src/main.rs +85 -54
- package/crates/tish/tests/cargo_example_compile.rs +67 -0
- package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
- package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
- package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
- package/crates/tish/tests/integration_test.rs +197 -47
- package/crates/tish/tests/run_optimize_stdout_parity.rs +3 -7
- package/crates/tish/tests/shortcircuit.rs +19 -4
- package/crates/tish_ast/src/ast.rs +12 -14
- package/crates/tish_build_utils/src/lib.rs +64 -6
- package/crates/tish_builtins/src/array.rs +52 -21
- package/crates/tish_builtins/src/construct.rs +2 -8
- package/crates/tish_builtins/src/globals.rs +30 -15
- package/crates/tish_builtins/src/lib.rs +5 -5
- package/crates/tish_builtins/src/math.rs +5 -3
- package/crates/tish_builtins/src/string.rs +71 -19
- package/crates/tish_bytecode/src/chunk.rs +0 -1
- package/crates/tish_bytecode/src/compiler.rs +164 -60
- package/crates/tish_bytecode/src/opcode.rs +13 -4
- package/crates/tish_bytecode/src/peephole.rs +2 -2
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/codegen.rs +989 -318
- package/crates/tish_compile/src/infer.rs +69 -19
- package/crates/tish_compile/src/lib.rs +21 -8
- package/crates/tish_compile/src/resolve.rs +515 -94
- package/crates/tish_compile/src/types.rs +10 -14
- package/crates/tish_compile_js/src/codegen.rs +34 -13
- package/crates/tish_compile_js/src/tests_jsx.rs +30 -6
- package/crates/tish_compiler_wasm/src/lib.rs +16 -13
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +40 -48
- package/crates/tish_core/src/json.rs +5 -3
- package/crates/tish_core/src/lib.rs +1 -1
- package/crates/tish_core/src/uri.rs +9 -6
- package/crates/tish_core/src/value.rs +92 -28
- package/crates/tish_cranelift/src/link.rs +6 -9
- package/crates/tish_cranelift/src/lower.rs +14 -8
- package/crates/tish_eval/src/eval.rs +398 -141
- package/crates/tish_eval/src/lib.rs +10 -6
- package/crates/tish_eval/src/natives.rs +95 -38
- package/crates/tish_eval/src/promise.rs +14 -8
- package/crates/tish_eval/src/timers.rs +28 -19
- package/crates/tish_eval/src/value.rs +10 -3
- package/crates/tish_fmt/src/lib.rs +29 -13
- package/crates/tish_lexer/src/lib.rs +217 -63
- package/crates/tish_lexer/src/token.rs +6 -6
- package/crates/tish_llvm/src/lib.rs +15 -8
- package/crates/tish_lsp/src/main.rs +41 -43
- package/crates/tish_native/src/build.rs +38 -15
- package/crates/tish_native/src/lib.rs +76 -32
- package/crates/tish_opt/src/lib.rs +67 -50
- package/crates/tish_parser/src/lib.rs +36 -11
- package/crates/tish_parser/src/parser.rs +172 -87
- package/crates/tish_runtime/src/http.rs +15 -6
- package/crates/tish_runtime/src/http_fetch.rs +24 -14
- package/crates/tish_runtime/src/lib.rs +224 -168
- package/crates/tish_runtime/src/promise.rs +1 -5
- package/crates/tish_runtime/src/ws.rs +45 -20
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +5 -4
- package/crates/tish_ui/src/jsx.rs +41 -22
- package/crates/tish_ui/src/lib.rs +2 -2
- package/crates/tish_vm/src/vm.rs +320 -116
- package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +8 -3
- package/crates/tish_wasm/src/lib.rs +38 -28
- package/crates/tishlang_cargo_bindgen/Cargo.toml +25 -0
- package/crates/tishlang_cargo_bindgen/src/classify.rs +265 -0
- package/crates/tishlang_cargo_bindgen/src/discover.rs +52 -0
- package/crates/tishlang_cargo_bindgen/src/infer.rs +372 -0
- package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
- package/crates/tishlang_cargo_bindgen/src/main.rs +164 -0
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +114 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//! Module resolver: resolves relative imports, builds dependency graph, detects cycles.
|
|
2
|
-
//! Supports native imports: tish
|
|
2
|
+
//! Supports native imports: `tish:…`, `cargo:…`, `@scope/pkg` (via package.json).
|
|
3
3
|
|
|
4
4
|
use std::collections::{HashMap, HashSet};
|
|
5
5
|
use std::path::{Path, PathBuf};
|
|
@@ -16,6 +16,33 @@ pub struct ResolvedNativeModule {
|
|
|
16
16
|
pub crate_name: String,
|
|
17
17
|
pub crate_path: PathBuf,
|
|
18
18
|
pub export_fn: String,
|
|
19
|
+
/// When false, omit `path = …` in the generated Cargo.toml (crate comes from `tish.rustDependencies` only).
|
|
20
|
+
pub use_path_dependency: bool,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// How codegen links a native import to Rust (`generateNativeWrapper` for `tish:*`; `cargo:*` always generated).
|
|
24
|
+
#[derive(Debug, Clone)]
|
|
25
|
+
pub enum NativeModuleInit {
|
|
26
|
+
/// Call `external_crate::export_fn()` and read named exports from the returned object.
|
|
27
|
+
Legacy {
|
|
28
|
+
crate_name: String,
|
|
29
|
+
export_fn: String,
|
|
30
|
+
},
|
|
31
|
+
/// Call `crate::generated_native::export_fn()` — object built from per-export fns on `shim_crate`.
|
|
32
|
+
Generated {
|
|
33
|
+
shim_crate: String,
|
|
34
|
+
export_fn: String,
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Extra native build inputs produced alongside Rust source (Cargo merge + optional wrapper).
|
|
39
|
+
#[derive(Debug, Clone)]
|
|
40
|
+
pub struct NativeBuildArtifacts {
|
|
41
|
+
/// Extra `[dependencies]` lines from `tish.rustDependencies`.
|
|
42
|
+
pub rust_dependencies_toml: String,
|
|
43
|
+
/// Generated `generated_native.rs` when using [`NativeModuleInit::Generated`].
|
|
44
|
+
pub generated_native_rs: Option<String>,
|
|
45
|
+
pub native_init: std::collections::HashMap<String, NativeModuleInit>,
|
|
19
46
|
}
|
|
20
47
|
|
|
21
48
|
/// Node-compatible aliases for built-in modules (fs -> tish:fs, etc.).
|
|
@@ -45,32 +72,109 @@ pub fn is_builtin_native_spec(spec: &str) -> bool {
|
|
|
45
72
|
|
|
46
73
|
/// Resolve all native imports in a merged program via package.json lookup.
|
|
47
74
|
/// Built-in modules (tish:fs, tish:http, tish:process) are skipped - they use tishlang_runtime directly.
|
|
48
|
-
|
|
75
|
+
/// Handles both lowered `NativeModuleLoad` (merged modules) and raw `import { … } from 'tish:…'`.
|
|
76
|
+
pub fn resolve_native_modules(
|
|
77
|
+
program: &Program,
|
|
78
|
+
project_root: &Path,
|
|
79
|
+
) -> Result<Vec<ResolvedNativeModule>, String> {
|
|
49
80
|
let root_canon = project_root
|
|
50
81
|
.canonicalize()
|
|
51
82
|
.map_err(|e| format!("Cannot canonicalize project root: {}", e))?;
|
|
52
83
|
let mut seen = HashSet::new();
|
|
53
84
|
let mut modules = Vec::new();
|
|
54
85
|
for stmt in &program.statements {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
86
|
+
let specs: Vec<String> = match stmt {
|
|
87
|
+
Statement::VarDecl {
|
|
88
|
+
init: Some(Expr::NativeModuleLoad { spec, .. }),
|
|
89
|
+
..
|
|
90
|
+
} => vec![spec.as_ref().to_string()],
|
|
91
|
+
Statement::Import { from, .. } if is_native_import(from.as_ref()) => {
|
|
92
|
+
vec![normalize_builtin_spec(from.as_ref()).unwrap_or_else(|| from.to_string())]
|
|
93
|
+
}
|
|
94
|
+
_ => continue,
|
|
95
|
+
};
|
|
96
|
+
for s in specs {
|
|
97
|
+
if is_builtin_native_spec(&s) {
|
|
98
|
+
continue;
|
|
63
99
|
}
|
|
64
|
-
if !seen.insert(s.
|
|
100
|
+
if !seen.insert(s.clone()) {
|
|
65
101
|
continue;
|
|
66
102
|
}
|
|
67
|
-
let m =
|
|
103
|
+
let m = if s.starts_with("cargo:") {
|
|
104
|
+
resolve_cargo_native_module(&s, &root_canon)?
|
|
105
|
+
} else {
|
|
106
|
+
resolve_native_module(&s, &root_canon)?
|
|
107
|
+
};
|
|
68
108
|
modules.push(m);
|
|
69
109
|
}
|
|
70
110
|
}
|
|
71
111
|
Ok(modules)
|
|
72
112
|
}
|
|
73
113
|
|
|
114
|
+
/// True for `cargo:…` specs (Cargo-backed imports; Rust native backend only).
|
|
115
|
+
pub fn is_cargo_native_spec(spec: &str) -> bool {
|
|
116
|
+
spec.starts_with("cargo:")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// Stable Rust symbol for the generated namespace function, e.g. `cargo:my-crate` → `cargo_native_my_crate_object`.
|
|
120
|
+
pub fn cargo_export_fn_name(spec: &str) -> String {
|
|
121
|
+
let tail = spec.strip_prefix("cargo:").unwrap_or(spec);
|
|
122
|
+
let mut out = String::from("cargo_native_");
|
|
123
|
+
for c in tail.chars() {
|
|
124
|
+
if c.is_ascii_alphanumeric() {
|
|
125
|
+
out.push(c);
|
|
126
|
+
} else {
|
|
127
|
+
out.push('_');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if out == "cargo_native_" {
|
|
131
|
+
out.push_str("unnamed");
|
|
132
|
+
}
|
|
133
|
+
out.push_str("_object");
|
|
134
|
+
out
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fn resolve_cargo_native_module(
|
|
138
|
+
spec: &str,
|
|
139
|
+
project_root: &Path,
|
|
140
|
+
) -> Result<ResolvedNativeModule, String> {
|
|
141
|
+
let tail = spec
|
|
142
|
+
.strip_prefix("cargo:")
|
|
143
|
+
.ok_or_else(|| format!("Invalid cargo native spec: {}", spec))?;
|
|
144
|
+
if tail.is_empty() {
|
|
145
|
+
return Err(
|
|
146
|
+
"cargo: import needs a dependency name, e.g. import { x } from 'cargo:my_crate'".into(),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
let dep_key = tail.to_string();
|
|
150
|
+
let tish = read_project_tish_config(project_root);
|
|
151
|
+
let rust_deps = tish.get("rustDependencies").and_then(|v| v.as_object()).ok_or_else(|| {
|
|
152
|
+
format!(
|
|
153
|
+
"cargo:{} requires package.json \"tish\": {{ \"rustDependencies\": {{ \"{}\": \"…\" }} }}",
|
|
154
|
+
tail, dep_key
|
|
155
|
+
)
|
|
156
|
+
})?;
|
|
157
|
+
if !rust_deps.contains_key(&dep_key) {
|
|
158
|
+
return Err(format!(
|
|
159
|
+
"cargo:{}: add \"{}\" to tish.rustDependencies in package.json (version string or inline table)",
|
|
160
|
+
tail, dep_key
|
|
161
|
+
));
|
|
162
|
+
}
|
|
163
|
+
let crate_name = dep_key.replace('-', "_");
|
|
164
|
+
let export_fn = cargo_export_fn_name(spec);
|
|
165
|
+
let crate_path = project_root
|
|
166
|
+
.canonicalize()
|
|
167
|
+
.unwrap_or_else(|_| project_root.to_path_buf());
|
|
168
|
+
Ok(ResolvedNativeModule {
|
|
169
|
+
spec: spec.to_string(),
|
|
170
|
+
package_name: dep_key.clone(),
|
|
171
|
+
crate_name,
|
|
172
|
+
crate_path,
|
|
173
|
+
export_fn,
|
|
174
|
+
use_path_dependency: false,
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
74
178
|
fn resolve_native_module(spec: &str, project_root: &Path) -> Result<ResolvedNativeModule, String> {
|
|
75
179
|
let package_name = if spec.starts_with("tish:") {
|
|
76
180
|
format!("tish-{}", spec.strip_prefix("tish:").unwrap_or(spec))
|
|
@@ -83,14 +187,26 @@ fn resolve_native_module(spec: &str, project_root: &Path) -> Result<ResolvedNati
|
|
|
83
187
|
let pkg_json = pkg_dir.join("package.json");
|
|
84
188
|
let content = std::fs::read_to_string(&pkg_json)
|
|
85
189
|
.map_err(|e| format!("Cannot read {}: {}", pkg_json.display(), e))?;
|
|
86
|
-
let json: serde_json::Value =
|
|
87
|
-
|
|
190
|
+
let json: serde_json::Value = serde_json::from_str(&content)
|
|
191
|
+
.map_err(|e| format!("Invalid JSON in {}: {}", pkg_json.display(), e))?;
|
|
88
192
|
let tish = json
|
|
89
193
|
.get("tish")
|
|
90
194
|
.and_then(|v| v.as_object())
|
|
91
|
-
.ok_or_else(||
|
|
92
|
-
|
|
93
|
-
|
|
195
|
+
.ok_or_else(|| {
|
|
196
|
+
format!(
|
|
197
|
+
"Package {} has no \"tish\" config in package.json",
|
|
198
|
+
package_name
|
|
199
|
+
)
|
|
200
|
+
})?;
|
|
201
|
+
if !tish
|
|
202
|
+
.get("module")
|
|
203
|
+
.and_then(|v| v.as_bool())
|
|
204
|
+
.unwrap_or(false)
|
|
205
|
+
{
|
|
206
|
+
return Err(format!(
|
|
207
|
+
"Package {} is not a Tish native module (tish.module must be true)",
|
|
208
|
+
package_name
|
|
209
|
+
));
|
|
94
210
|
}
|
|
95
211
|
let raw_crate = tish
|
|
96
212
|
.get("crate")
|
|
@@ -110,6 +226,261 @@ fn resolve_native_module(spec: &str, project_root: &Path) -> Result<ResolvedNati
|
|
|
110
226
|
crate_name: raw_crate.replace('-', "_"),
|
|
111
227
|
crate_path,
|
|
112
228
|
export_fn,
|
|
229
|
+
use_path_dependency: true,
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// Read the `tish` object from the project root `package.json` (empty JSON object if missing).
|
|
234
|
+
pub fn read_project_tish_config(project_root: &Path) -> serde_json::Value {
|
|
235
|
+
let path = project_root.join("package.json");
|
|
236
|
+
let Ok(content) = std::fs::read_to_string(&path) else {
|
|
237
|
+
return serde_json::json!({});
|
|
238
|
+
};
|
|
239
|
+
let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
|
|
240
|
+
return serde_json::json!({});
|
|
241
|
+
};
|
|
242
|
+
json.get("tish")
|
|
243
|
+
.cloned()
|
|
244
|
+
.unwrap_or_else(|| serde_json::json!({}))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
fn resolve_cargo_path_for_toml(project_root: &Path, raw: &str) -> String {
|
|
248
|
+
let p = Path::new(raw);
|
|
249
|
+
let resolved = if p.is_absolute() {
|
|
250
|
+
p.to_path_buf()
|
|
251
|
+
} else {
|
|
252
|
+
project_root.join(p)
|
|
253
|
+
};
|
|
254
|
+
let resolved = resolved.canonicalize().unwrap_or(resolved);
|
|
255
|
+
resolved.display().to_string().replace('\\', "/")
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fn json_to_cargo_inline_value(
|
|
259
|
+
v: &serde_json::Value,
|
|
260
|
+
project_root: &Path,
|
|
261
|
+
) -> Result<String, String> {
|
|
262
|
+
match v {
|
|
263
|
+
serde_json::Value::String(s) => Ok(format!("{:?}", s.as_str())),
|
|
264
|
+
serde_json::Value::Bool(b) => Ok(b.to_string()),
|
|
265
|
+
serde_json::Value::Number(n) => Ok(n.to_string()),
|
|
266
|
+
serde_json::Value::Array(arr) => {
|
|
267
|
+
let mut inner = Vec::new();
|
|
268
|
+
for item in arr {
|
|
269
|
+
inner.push(json_to_cargo_inline_value(item, project_root)?);
|
|
270
|
+
}
|
|
271
|
+
Ok(format!("[{}]", inner.join(", ")))
|
|
272
|
+
}
|
|
273
|
+
serde_json::Value::Object(map) => {
|
|
274
|
+
let mut parts = Vec::new();
|
|
275
|
+
for (k, v) in map {
|
|
276
|
+
let rhs = if k == "path" && v.as_str().is_some() {
|
|
277
|
+
let s = v.as_str().unwrap();
|
|
278
|
+
format!("{:?}", resolve_cargo_path_for_toml(project_root, s))
|
|
279
|
+
} else {
|
|
280
|
+
json_to_cargo_inline_value(v, project_root)?
|
|
281
|
+
};
|
|
282
|
+
parts.push(format!("{} = {}", k, rhs));
|
|
283
|
+
}
|
|
284
|
+
Ok(format!("{{ {} }}", parts.join(", ")))
|
|
285
|
+
}
|
|
286
|
+
serde_json::Value::Null => Err("null is not valid in a Cargo dependency value".to_string()),
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/// Serialize `tish.rustDependencies` from project `package.json` into Cargo.toml `[dependencies]` lines.
|
|
291
|
+
/// Relative `path = "…"` entries in inline tables are resolved against `project_root` so the temp build crate can find them.
|
|
292
|
+
pub fn format_rust_dependencies_toml(
|
|
293
|
+
tish: &serde_json::Value,
|
|
294
|
+
project_root: &Path,
|
|
295
|
+
) -> Result<String, String> {
|
|
296
|
+
let Some(obj) = tish.get("rustDependencies").and_then(|v| v.as_object()) else {
|
|
297
|
+
return Ok(String::new());
|
|
298
|
+
};
|
|
299
|
+
let mut out = String::new();
|
|
300
|
+
for (name, val) in obj {
|
|
301
|
+
match val {
|
|
302
|
+
serde_json::Value::String(_) | serde_json::Value::Object(_) => {
|
|
303
|
+
out.push_str(&format!(
|
|
304
|
+
"{} = {}\n",
|
|
305
|
+
name,
|
|
306
|
+
json_to_cargo_inline_value(val, project_root)?
|
|
307
|
+
));
|
|
308
|
+
}
|
|
309
|
+
_ => {
|
|
310
|
+
return Err(format!(
|
|
311
|
+
"tish.rustDependencies.{} must be a string (version) or object (inline table)",
|
|
312
|
+
name
|
|
313
|
+
));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
Ok(out)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/// Map a Tish export name to a Rust identifier (e.g. `readFile` → `read_file`) for shim crate symbols.
|
|
321
|
+
pub fn export_name_to_rust_ident(export_name: &str) -> String {
|
|
322
|
+
let mut out = String::new();
|
|
323
|
+
for (i, c) in export_name.chars().enumerate() {
|
|
324
|
+
if c.is_uppercase() && i > 0 {
|
|
325
|
+
out.push('_');
|
|
326
|
+
}
|
|
327
|
+
for lower in c.to_lowercase() {
|
|
328
|
+
out.push(lower);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if out.is_empty() {
|
|
332
|
+
"native_export".to_string()
|
|
333
|
+
} else {
|
|
334
|
+
out
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/// Collect `(spec, export_name)` for every non-builtin native import in the program.
|
|
339
|
+
pub fn infer_native_module_exports(program: &Program) -> HashMap<String, HashSet<String>> {
|
|
340
|
+
let mut map: HashMap<String, HashSet<String>> = HashMap::new();
|
|
341
|
+
for stmt in &program.statements {
|
|
342
|
+
match stmt {
|
|
343
|
+
Statement::VarDecl {
|
|
344
|
+
init:
|
|
345
|
+
Some(Expr::NativeModuleLoad {
|
|
346
|
+
spec, export_name, ..
|
|
347
|
+
}),
|
|
348
|
+
..
|
|
349
|
+
} => {
|
|
350
|
+
let s = spec.as_ref();
|
|
351
|
+
if is_builtin_native_spec(s) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
map.entry(s.to_string())
|
|
355
|
+
.or_default()
|
|
356
|
+
.insert(export_name.to_string());
|
|
357
|
+
}
|
|
358
|
+
Statement::Import {
|
|
359
|
+
specifiers, from, ..
|
|
360
|
+
} if is_native_import(from.as_ref()) => {
|
|
361
|
+
let spec =
|
|
362
|
+
normalize_builtin_spec(from.as_ref()).unwrap_or_else(|| from.to_string());
|
|
363
|
+
if is_builtin_native_spec(&spec) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
for sp in specifiers {
|
|
367
|
+
if let ImportSpecifier::Named { name, .. } = sp {
|
|
368
|
+
map.entry(spec.clone())
|
|
369
|
+
.or_default()
|
|
370
|
+
.insert(name.to_string());
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
_ => {}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
map
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/// Emit `generated_native.rs` for [`NativeModuleInit::Generated`] modules.
|
|
381
|
+
pub fn generate_native_wrapper_rs(
|
|
382
|
+
modules: &[ResolvedNativeModule],
|
|
383
|
+
inferred: &HashMap<String, HashSet<String>>,
|
|
384
|
+
init_by_spec: &HashMap<String, NativeModuleInit>,
|
|
385
|
+
) -> String {
|
|
386
|
+
let mut file = String::from(
|
|
387
|
+
"//! Generated by `tish build` — do not edit.\n\
|
|
388
|
+
use std::cell::RefCell;\n\
|
|
389
|
+
use std::rc::Rc;\n\
|
|
390
|
+
use std::sync::Arc;\n\
|
|
391
|
+
use tishlang_runtime::{ObjectMap, Value};\n\n",
|
|
392
|
+
);
|
|
393
|
+
let mut any = false;
|
|
394
|
+
for m in modules {
|
|
395
|
+
let Some(NativeModuleInit::Generated {
|
|
396
|
+
shim_crate,
|
|
397
|
+
export_fn,
|
|
398
|
+
}) = init_by_spec.get(&m.spec)
|
|
399
|
+
else {
|
|
400
|
+
continue;
|
|
401
|
+
};
|
|
402
|
+
let Some(names) = inferred.get(&m.spec) else {
|
|
403
|
+
continue;
|
|
404
|
+
};
|
|
405
|
+
if names.is_empty() {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
any = true;
|
|
409
|
+
let mut keys: Vec<_> = names.iter().cloned().collect();
|
|
410
|
+
keys.sort();
|
|
411
|
+
file.push_str(&format!("pub fn {}() -> Value {{\n", export_fn));
|
|
412
|
+
file.push_str(" let mut m = ObjectMap::default();\n");
|
|
413
|
+
for export_name in keys {
|
|
414
|
+
let rust_fn = export_name_to_rust_ident(&export_name);
|
|
415
|
+
let key_lit = format!("{:?}", export_name);
|
|
416
|
+
file.push_str(&format!(
|
|
417
|
+
" m.insert(Arc::from({}), Value::Function(Rc::new(|args: &[Value]| {{\n {}::{}(args)\n }})));\n",
|
|
418
|
+
key_lit, shim_crate, rust_fn
|
|
419
|
+
));
|
|
420
|
+
}
|
|
421
|
+
file.push_str(" Value::Object(Rc::new(RefCell::new(m)))\n}\n\n");
|
|
422
|
+
}
|
|
423
|
+
if !any {
|
|
424
|
+
return String::new();
|
|
425
|
+
}
|
|
426
|
+
file
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/// Combine project `package.json`, inferred exports, and resolved native modules into build artifacts.
|
|
430
|
+
pub fn compute_native_build_artifacts(
|
|
431
|
+
program: &Program,
|
|
432
|
+
project_root: &Path,
|
|
433
|
+
native_modules: &[ResolvedNativeModule],
|
|
434
|
+
) -> Result<NativeBuildArtifacts, String> {
|
|
435
|
+
let tish = read_project_tish_config(project_root);
|
|
436
|
+
let rust_dependencies_toml = format_rust_dependencies_toml(&tish, project_root)?;
|
|
437
|
+
let inferred = infer_native_module_exports(program);
|
|
438
|
+
let gen_tish = tish
|
|
439
|
+
.get("generateNativeWrapper")
|
|
440
|
+
.and_then(|v| v.as_bool())
|
|
441
|
+
.unwrap_or(false);
|
|
442
|
+
|
|
443
|
+
let mut native_init: HashMap<String, NativeModuleInit> = HashMap::new();
|
|
444
|
+
for m in native_modules {
|
|
445
|
+
let use_gen = if is_cargo_native_spec(&m.spec) {
|
|
446
|
+
inferred
|
|
447
|
+
.get(&m.spec)
|
|
448
|
+
.map(|s| !s.is_empty())
|
|
449
|
+
.unwrap_or(false)
|
|
450
|
+
} else {
|
|
451
|
+
gen_tish
|
|
452
|
+
&& inferred
|
|
453
|
+
.get(&m.spec)
|
|
454
|
+
.map(|s| !s.is_empty())
|
|
455
|
+
.unwrap_or(false)
|
|
456
|
+
};
|
|
457
|
+
let init = if use_gen {
|
|
458
|
+
NativeModuleInit::Generated {
|
|
459
|
+
shim_crate: m.crate_name.clone(),
|
|
460
|
+
export_fn: m.export_fn.clone(),
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
NativeModuleInit::Legacy {
|
|
464
|
+
crate_name: m.crate_name.clone(),
|
|
465
|
+
export_fn: m.export_fn.clone(),
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
native_init.insert(m.spec.clone(), init);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
let generated_native_rs = {
|
|
472
|
+
let s = generate_native_wrapper_rs(native_modules, &inferred, &native_init);
|
|
473
|
+
if s.trim().is_empty() {
|
|
474
|
+
None
|
|
475
|
+
} else {
|
|
476
|
+
Some(s)
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
Ok(NativeBuildArtifacts {
|
|
481
|
+
rust_dependencies_toml,
|
|
482
|
+
generated_native_rs,
|
|
483
|
+
native_init,
|
|
113
484
|
})
|
|
114
485
|
}
|
|
115
486
|
|
|
@@ -151,16 +522,25 @@ fn read_package_name(pkg_path: &Path) -> Option<String> {
|
|
|
151
522
|
json.get("name").and_then(|v| v.as_str()).map(String::from)
|
|
152
523
|
}
|
|
153
524
|
|
|
525
|
+
fn stmt_native_specs(stmt: &Statement) -> Vec<String> {
|
|
526
|
+
match stmt {
|
|
527
|
+
Statement::VarDecl {
|
|
528
|
+
init: Some(Expr::NativeModuleLoad { spec, .. }),
|
|
529
|
+
..
|
|
530
|
+
} => vec![spec.to_string()],
|
|
531
|
+
Statement::Import { from, .. } if is_native_import(from.as_ref()) => {
|
|
532
|
+
vec![normalize_builtin_spec(from.as_ref()).unwrap_or_else(|| from.to_string())]
|
|
533
|
+
}
|
|
534
|
+
_ => vec![],
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
154
538
|
/// Extract Cargo feature names from native imports in a merged program.
|
|
155
539
|
/// Used to enable tishlang_runtime features based on `import { x } from 'tish:egui'` etc.
|
|
156
540
|
pub fn extract_native_import_features(program: &Program) -> Vec<String> {
|
|
157
541
|
let mut features = std::collections::HashSet::new();
|
|
158
542
|
for stmt in &program.statements {
|
|
159
|
-
|
|
160
|
-
init: Some(Expr::NativeModuleLoad { spec, .. }),
|
|
161
|
-
..
|
|
162
|
-
} = stmt
|
|
163
|
-
{
|
|
543
|
+
for spec in stmt_native_specs(stmt) {
|
|
164
544
|
if let Some(f) = native_spec_to_feature(spec.as_ref()) {
|
|
165
545
|
features.insert(f);
|
|
166
546
|
}
|
|
@@ -171,27 +551,17 @@ pub fn extract_native_import_features(program: &Program) -> Vec<String> {
|
|
|
171
551
|
|
|
172
552
|
/// Returns true if the merged program contains native imports (tish:*, @scope/pkg).
|
|
173
553
|
pub fn has_native_imports(program: &Program) -> bool {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
} = stmt
|
|
179
|
-
{
|
|
180
|
-
return true;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
false
|
|
554
|
+
program
|
|
555
|
+
.statements
|
|
556
|
+
.iter()
|
|
557
|
+
.any(|stmt| !stmt_native_specs(stmt).is_empty())
|
|
184
558
|
}
|
|
185
559
|
|
|
186
560
|
/// Returns true if the merged program contains external native imports (not built-in tish:fs/http/process).
|
|
187
561
|
/// Cranelift/LLVM reject these; bytecode VM supports built-ins only.
|
|
188
562
|
pub fn has_external_native_imports(program: &Program) -> bool {
|
|
189
563
|
for stmt in &program.statements {
|
|
190
|
-
|
|
191
|
-
init: Some(Expr::NativeModuleLoad { spec, .. }),
|
|
192
|
-
..
|
|
193
|
-
} = stmt
|
|
194
|
-
{
|
|
564
|
+
for spec in stmt_native_specs(stmt) {
|
|
195
565
|
if !is_builtin_native_spec(spec.as_ref()) {
|
|
196
566
|
return true;
|
|
197
567
|
}
|
|
@@ -213,13 +583,18 @@ pub fn resolve_project(
|
|
|
213
583
|
entry_path: &Path,
|
|
214
584
|
project_root: Option<&Path>,
|
|
215
585
|
) -> Result<Vec<ResolvedModule>, String> {
|
|
216
|
-
let project_root =
|
|
586
|
+
let project_root =
|
|
587
|
+
project_root.unwrap_or_else(|| entry_path.parent().unwrap_or(Path::new(".")));
|
|
217
588
|
let entry_canon = entry_path
|
|
218
589
|
.canonicalize()
|
|
219
590
|
.map_err(|e| format!("Cannot canonicalize entry {}: {}", entry_path.display(), e))?;
|
|
220
|
-
let root_canon = project_root
|
|
221
|
-
|
|
222
|
-
|
|
591
|
+
let root_canon = project_root.canonicalize().map_err(|e| {
|
|
592
|
+
format!(
|
|
593
|
+
"Cannot canonicalize project root {}: {}",
|
|
594
|
+
project_root.display(),
|
|
595
|
+
e
|
|
596
|
+
)
|
|
597
|
+
})?;
|
|
223
598
|
|
|
224
599
|
let mut visited = HashSet::new();
|
|
225
600
|
let mut path_to_module: HashMap<PathBuf, Program> = HashMap::new();
|
|
@@ -249,28 +624,30 @@ pub fn resolve_project_from_stdin(
|
|
|
249
624
|
source: &str,
|
|
250
625
|
project_root: &Path,
|
|
251
626
|
) -> Result<Vec<ResolvedModule>, String> {
|
|
252
|
-
let root_canon = project_root
|
|
253
|
-
|
|
254
|
-
|
|
627
|
+
let root_canon = project_root.canonicalize().map_err(|e| {
|
|
628
|
+
format!(
|
|
629
|
+
"Cannot canonicalize project root {}: {}",
|
|
630
|
+
project_root.display(),
|
|
631
|
+
e
|
|
632
|
+
)
|
|
633
|
+
})?;
|
|
255
634
|
|
|
256
635
|
let stdin_path = root_canon.join("<stdin>");
|
|
257
|
-
let program =
|
|
258
|
-
.map_err(|e| format!("Parse error (stdin): {}", e))?;
|
|
636
|
+
let program =
|
|
637
|
+
tishlang_parser::parse(source).map_err(|e| format!("Parse error (stdin): {}", e))?;
|
|
259
638
|
|
|
260
639
|
let mut visited = HashSet::new();
|
|
261
640
|
let mut path_to_module: HashMap<PathBuf, Program> = HashMap::new();
|
|
262
641
|
let mut load_order: Vec<PathBuf> = Vec::new();
|
|
263
642
|
|
|
264
|
-
let from_dir = stdin_path
|
|
265
|
-
.parent()
|
|
266
|
-
.unwrap_or_else(|| Path::new("."));
|
|
643
|
+
let from_dir = stdin_path.parent().unwrap_or_else(|| Path::new("."));
|
|
267
644
|
|
|
268
645
|
for stmt in &program.statements {
|
|
269
646
|
if let Statement::Import { from, .. } = stmt {
|
|
270
|
-
if is_native_import(from) {
|
|
647
|
+
if is_native_import(from.as_ref()) {
|
|
271
648
|
continue;
|
|
272
649
|
}
|
|
273
|
-
let dep_path = resolve_import_path(from, from_dir, &root_canon)?;
|
|
650
|
+
let dep_path = resolve_import_path(from.as_ref(), from_dir, &root_canon)?;
|
|
274
651
|
if !path_to_module.contains_key(&dep_path) {
|
|
275
652
|
load_module_recursive(
|
|
276
653
|
&dep_path,
|
|
@@ -320,10 +697,10 @@ fn load_module_recursive(
|
|
|
320
697
|
let dir = canonical.parent().unwrap_or(Path::new("."));
|
|
321
698
|
for stmt in &program.statements {
|
|
322
699
|
if let Statement::Import { from, .. } = stmt {
|
|
323
|
-
if is_native_import(from) {
|
|
700
|
+
if is_native_import(from.as_ref()) {
|
|
324
701
|
continue; // Native imports don't load files
|
|
325
702
|
}
|
|
326
|
-
let dep_path = resolve_import_path(from, dir, project_root)?;
|
|
703
|
+
let dep_path = resolve_import_path(from.as_ref(), dir, project_root)?;
|
|
327
704
|
if !path_to_module.contains_key(&dep_path) {
|
|
328
705
|
load_module_recursive(
|
|
329
706
|
&dep_path,
|
|
@@ -344,9 +721,11 @@ fn load_module_recursive(
|
|
|
344
721
|
/// Returns true for native module imports that don't resolve to files.
|
|
345
722
|
/// - fs, http, process, ws (Node-compatible aliases for tish:fs, tish:http, tish:process, tish:ws)
|
|
346
723
|
/// - tish:egui, tish:polars, etc.
|
|
724
|
+
/// - cargo:… (Cargo `rustDependencies` + generated wrapper; Rust native backend)
|
|
347
725
|
/// - @scope/package (npm-style)
|
|
348
726
|
pub fn is_native_import(spec: &str) -> bool {
|
|
349
727
|
spec.starts_with("tish:")
|
|
728
|
+
|| spec.starts_with("cargo:")
|
|
350
729
|
|| spec.starts_with('@')
|
|
351
730
|
|| matches!(spec, "fs" | "http" | "process" | "ws")
|
|
352
731
|
}
|
|
@@ -460,7 +839,10 @@ pub fn detect_cycles(modules: &[ResolvedModule]) -> Result<(), String> {
|
|
|
460
839
|
.iter()
|
|
461
840
|
.map(|&i| modules[i].path.display().to_string())
|
|
462
841
|
.collect();
|
|
463
|
-
return Err(format!(
|
|
842
|
+
return Err(format!(
|
|
843
|
+
"Circular import detected: {}",
|
|
844
|
+
path_names.join(" -> ")
|
|
845
|
+
));
|
|
464
846
|
}
|
|
465
847
|
}
|
|
466
848
|
Ok(())
|
|
@@ -476,10 +858,10 @@ fn has_cycle_from(
|
|
|
476
858
|
) -> Result<bool, String> {
|
|
477
859
|
for stmt in &program.statements {
|
|
478
860
|
if let Statement::Import { from, .. } = stmt {
|
|
479
|
-
if is_native_import(from) {
|
|
861
|
+
if is_native_import(from.as_ref()) {
|
|
480
862
|
continue;
|
|
481
863
|
}
|
|
482
|
-
let dep_path = resolve_import_path(from, from_dir, Path::new("."))?;
|
|
864
|
+
let dep_path = resolve_import_path(from.as_ref(), from_dir, Path::new("."))?;
|
|
483
865
|
if let Some(&dep_idx) = path_to_idx.get(&dep_path) {
|
|
484
866
|
if stack.contains(&dep_idx) {
|
|
485
867
|
stack.push(dep_idx);
|
|
@@ -490,14 +872,8 @@ fn has_cycle_from(
|
|
|
490
872
|
stack.push(dep_idx);
|
|
491
873
|
let dep = &modules[dep_idx];
|
|
492
874
|
let dep_dir = dep.path.parent().unwrap_or(Path::new("."));
|
|
493
|
-
if has_cycle_from(
|
|
494
|
-
|
|
495
|
-
&dep.program,
|
|
496
|
-
path_to_idx,
|
|
497
|
-
modules,
|
|
498
|
-
stack,
|
|
499
|
-
visiting,
|
|
500
|
-
)? {
|
|
875
|
+
if has_cycle_from(dep_dir, &dep.program, path_to_idx, modules, stack, visiting)?
|
|
876
|
+
{
|
|
501
877
|
return Ok(true);
|
|
502
878
|
}
|
|
503
879
|
stack.pop();
|
|
@@ -547,11 +923,15 @@ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
|
|
|
547
923
|
let dir = module.path.parent().unwrap_or(Path::new("."));
|
|
548
924
|
for stmt in &module.program.statements {
|
|
549
925
|
match stmt {
|
|
550
|
-
Statement::Import {
|
|
551
|
-
|
|
926
|
+
Statement::Import {
|
|
927
|
+
specifiers,
|
|
928
|
+
from,
|
|
929
|
+
span,
|
|
930
|
+
} => {
|
|
931
|
+
if is_native_import(from.as_ref()) {
|
|
552
932
|
// Normalize fs/http/process -> tish:fs etc. for Node compatibility
|
|
553
|
-
let canonical_spec =
|
|
554
|
-
|
|
933
|
+
let canonical_spec = normalize_builtin_spec(from.as_ref())
|
|
934
|
+
.unwrap_or_else(|| from.to_string());
|
|
555
935
|
// Emit VarDecl with NativeModuleLoad for each specifier
|
|
556
936
|
for spec in specifiers {
|
|
557
937
|
match spec {
|
|
@@ -589,10 +969,8 @@ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
|
|
|
589
969
|
}
|
|
590
970
|
continue;
|
|
591
971
|
}
|
|
592
|
-
let dep_path = resolve_import_path(from, dir, Path::new("."))?;
|
|
593
|
-
let dep_path = dep_path
|
|
594
|
-
.canonicalize()
|
|
595
|
-
.unwrap_or(dep_path);
|
|
972
|
+
let dep_path = resolve_import_path(from.as_ref(), dir, Path::new("."))?;
|
|
973
|
+
let dep_path = dep_path.canonicalize().unwrap_or(dep_path);
|
|
596
974
|
let dep_idx = *path_to_idx
|
|
597
975
|
.get(&dep_path)
|
|
598
976
|
.ok_or_else(|| format!("Resolved import '{}' not in module list", from))?;
|
|
@@ -633,18 +1011,13 @@ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
|
|
|
633
1011
|
name: ns.clone(),
|
|
634
1012
|
mutable: false,
|
|
635
1013
|
type_ann: None,
|
|
636
|
-
init: Some(Expr::Object {
|
|
637
|
-
props,
|
|
638
|
-
span: *span,
|
|
639
|
-
}),
|
|
1014
|
+
init: Some(Expr::Object { props, span: *span }),
|
|
640
1015
|
span: *span,
|
|
641
1016
|
});
|
|
642
1017
|
}
|
|
643
1018
|
ImportSpecifier::Default(bind) => {
|
|
644
|
-
let source =
|
|
645
|
-
.get("default")
|
|
646
|
-
.cloned()
|
|
647
|
-
.ok_or_else(|| {
|
|
1019
|
+
let source =
|
|
1020
|
+
dep_exports.get("default").cloned().ok_or_else(|| {
|
|
648
1021
|
format!("Module '{}' has no default export", from)
|
|
649
1022
|
})?;
|
|
650
1023
|
statements.push(Statement::VarDecl {
|
|
@@ -661,24 +1034,72 @@ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
|
|
|
661
1034
|
}
|
|
662
1035
|
}
|
|
663
1036
|
}
|
|
664
|
-
Statement::Export { declaration, .. } => {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
});
|
|
676
|
-
}
|
|
1037
|
+
Statement::Export { declaration, .. } => match declaration.as_ref() {
|
|
1038
|
+
ExportDeclaration::Named(s) => statements.push(*s.clone()),
|
|
1039
|
+
ExportDeclaration::Default(e) => {
|
|
1040
|
+
let default_name = format!("__default_{}", idx);
|
|
1041
|
+
statements.push(Statement::VarDecl {
|
|
1042
|
+
name: Arc::from(default_name),
|
|
1043
|
+
mutable: false,
|
|
1044
|
+
type_ann: None,
|
|
1045
|
+
init: Some((*e).clone()),
|
|
1046
|
+
span: e.span(),
|
|
1047
|
+
});
|
|
677
1048
|
}
|
|
678
|
-
}
|
|
1049
|
+
},
|
|
679
1050
|
_ => statements.push(stmt.clone()),
|
|
680
1051
|
}
|
|
681
1052
|
}
|
|
682
1053
|
}
|
|
683
1054
|
Ok(Program { statements })
|
|
684
1055
|
}
|
|
1056
|
+
|
|
1057
|
+
#[cfg(test)]
|
|
1058
|
+
mod cargo_spec_tests {
|
|
1059
|
+
use std::sync::Arc;
|
|
1060
|
+
|
|
1061
|
+
use super::cargo_export_fn_name;
|
|
1062
|
+
use super::is_native_import;
|
|
1063
|
+
|
|
1064
|
+
#[test]
|
|
1065
|
+
fn is_native_import_accepts_arc_str_ref() {
|
|
1066
|
+
let from: &Arc<str> = &Arc::from("cargo:demo_shim");
|
|
1067
|
+
assert!(is_native_import(from));
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
#[test]
|
|
1071
|
+
fn detect_cycles_skips_cargo_import() {
|
|
1072
|
+
use super::{detect_cycles, resolve_project};
|
|
1073
|
+
let dir = tempfile::tempdir().expect("tempdir");
|
|
1074
|
+
let p = dir.path().join("main.tish");
|
|
1075
|
+
let src = "import { greet } from 'cargo:demo_shim'\nconsole.log(1)\n";
|
|
1076
|
+
std::fs::write(&p, src).unwrap();
|
|
1077
|
+
let root = dir.path();
|
|
1078
|
+
let modules = resolve_project(&p, Some(root)).unwrap();
|
|
1079
|
+
detect_cycles(&modules).unwrap();
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
#[test]
|
|
1083
|
+
fn merge_modules_skips_cargo_import() {
|
|
1084
|
+
use super::{merge_modules, resolve_project};
|
|
1085
|
+
let dir = tempfile::tempdir().expect("tempdir");
|
|
1086
|
+
let p = dir.path().join("main.tish");
|
|
1087
|
+
let src = "import { greet } from 'cargo:demo_shim'\nconsole.log(1)\n";
|
|
1088
|
+
std::fs::write(&p, src).unwrap();
|
|
1089
|
+
let root = dir.path();
|
|
1090
|
+
let modules = resolve_project(&p, Some(root)).unwrap();
|
|
1091
|
+
merge_modules(modules).unwrap();
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
#[test]
|
|
1095
|
+
fn cargo_export_fn_name_sanitizes() {
|
|
1096
|
+
assert_eq!(
|
|
1097
|
+
cargo_export_fn_name("cargo:tish_serde_json"),
|
|
1098
|
+
"cargo_native_tish_serde_json_object"
|
|
1099
|
+
);
|
|
1100
|
+
assert_eq!(
|
|
1101
|
+
cargo_export_fn_name("cargo:my-crate"),
|
|
1102
|
+
"cargo_native_my_crate_object"
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
}
|