@tishlang/tish 1.4.2 → 1.6.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/bin/tish +0 -0
- package/crates/tish/Cargo.toml +2 -2
- package/crates/tish/src/cli_help.rs +504 -0
- package/crates/tish/src/main.rs +76 -90
- package/crates/tish/src/repl_completion.rs +1 -1
- package/crates/tish/tests/cargo_example_compile.rs +65 -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 +48 -0
- package/crates/tish_build_utils/src/lib.rs +204 -1
- package/crates/tish_builtins/src/string.rs +248 -0
- package/crates/tish_bytecode/Cargo.toml +1 -0
- package/crates/tish_bytecode/src/compiler.rs +289 -66
- package/crates/tish_bytecode/src/opcode.rs +13 -3
- package/crates/tish_bytecode/src/peephole.rs +21 -16
- package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/codegen.rs +277 -93
- package/crates/tish_compile/src/lib.rs +7 -4
- package/crates/tish_compile/src/resolve.rs +418 -40
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +1 -0
- package/crates/tish_core/src/value.rs +1 -0
- package/crates/tish_eval/src/eval.rs +49 -1
- package/crates/tish_eval/src/lib.rs +1 -1
- package/crates/tish_native/src/build.rs +86 -17
- package/crates/tish_native/src/lib.rs +36 -16
- package/crates/tish_runtime/src/lib.rs +4 -0
- package/crates/tish_vm/src/lib.rs +1 -1
- package/crates/tish_vm/src/vm.rs +165 -19
- package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -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
|
@@ -13,9 +13,12 @@ pub use codegen::{
|
|
|
13
13
|
};
|
|
14
14
|
pub use codegen::CompileError;
|
|
15
15
|
pub use resolve::{
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
cargo_export_fn_name, compute_native_build_artifacts, detect_cycles, export_name_to_rust_ident,
|
|
17
|
+
extract_native_import_features, format_rust_dependencies_toml, generate_native_wrapper_rs,
|
|
18
|
+
has_external_native_imports, has_native_imports, infer_native_module_exports,
|
|
19
|
+
is_builtin_native_spec, is_cargo_native_spec, merge_modules, read_project_tish_config,
|
|
20
|
+
resolve_native_modules, resolve_project, resolve_project_from_stdin, NativeBuildArtifacts,
|
|
21
|
+
NativeModuleInit, ResolvedNativeModule,
|
|
19
22
|
};
|
|
20
23
|
pub use types::{RustType, TypeContext};
|
|
21
24
|
|
|
@@ -108,7 +111,7 @@ fn factory() {
|
|
|
108
111
|
.into_iter()
|
|
109
112
|
.map(String::from)
|
|
110
113
|
.collect::<Vec<_>>();
|
|
111
|
-
let (rust, _) = compile_project_full(&bench, bench.parent(), &features, true).unwrap();
|
|
114
|
+
let (rust, _, _, _) = compile_project_full(&bench, bench.parent(), &features, true).unwrap();
|
|
112
115
|
// outerVar = 42 is inferred as f64; f64 is Copy so no .clone() is emitted.
|
|
113
116
|
assert!(
|
|
114
117
|
rust.contains("let mut outerVar: f64"),
|
|
@@ -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,6 +72,7 @@ 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.
|
|
75
|
+
/// Handles both lowered `NativeModuleLoad` (merged modules) and raw `import { … } from 'tish:…'`.
|
|
48
76
|
pub fn resolve_native_modules(program: &Program, project_root: &Path) -> Result<Vec<ResolvedNativeModule>, String> {
|
|
49
77
|
let root_canon = project_root
|
|
50
78
|
.canonicalize()
|
|
@@ -52,25 +80,93 @@ pub fn resolve_native_modules(program: &Program, project_root: &Path) -> Result<
|
|
|
52
80
|
let mut seen = HashSet::new();
|
|
53
81
|
let mut modules = Vec::new();
|
|
54
82
|
for stmt in &program.statements {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
83
|
+
let specs: Vec<String> = match stmt {
|
|
84
|
+
Statement::VarDecl {
|
|
85
|
+
init: Some(Expr::NativeModuleLoad { spec, .. }),
|
|
86
|
+
..
|
|
87
|
+
} => vec![spec.as_ref().to_string()],
|
|
88
|
+
Statement::Import { from, .. } if is_native_import(from.as_ref()) => {
|
|
89
|
+
vec![normalize_builtin_spec(from.as_ref()).unwrap_or_else(|| from.to_string())]
|
|
90
|
+
}
|
|
91
|
+
_ => continue,
|
|
92
|
+
};
|
|
93
|
+
for s in specs {
|
|
94
|
+
if is_builtin_native_spec(&s) {
|
|
95
|
+
continue;
|
|
63
96
|
}
|
|
64
|
-
if !seen.insert(s.
|
|
97
|
+
if !seen.insert(s.clone()) {
|
|
65
98
|
continue;
|
|
66
99
|
}
|
|
67
|
-
let m =
|
|
100
|
+
let m = if s.starts_with("cargo:") {
|
|
101
|
+
resolve_cargo_native_module(&s, &root_canon)?
|
|
102
|
+
} else {
|
|
103
|
+
resolve_native_module(&s, &root_canon)?
|
|
104
|
+
};
|
|
68
105
|
modules.push(m);
|
|
69
106
|
}
|
|
70
107
|
}
|
|
71
108
|
Ok(modules)
|
|
72
109
|
}
|
|
73
110
|
|
|
111
|
+
/// True for `cargo:…` specs (Cargo-backed imports; Rust native backend only).
|
|
112
|
+
pub fn is_cargo_native_spec(spec: &str) -> bool {
|
|
113
|
+
spec.starts_with("cargo:")
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Stable Rust symbol for the generated namespace function, e.g. `cargo:my-crate` → `cargo_native_my_crate_object`.
|
|
117
|
+
pub fn cargo_export_fn_name(spec: &str) -> String {
|
|
118
|
+
let tail = spec.strip_prefix("cargo:").unwrap_or(spec);
|
|
119
|
+
let mut out = String::from("cargo_native_");
|
|
120
|
+
for c in tail.chars() {
|
|
121
|
+
if c.is_ascii_alphanumeric() {
|
|
122
|
+
out.push(c);
|
|
123
|
+
} else {
|
|
124
|
+
out.push('_');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if out == "cargo_native_" {
|
|
128
|
+
out.push_str("unnamed");
|
|
129
|
+
}
|
|
130
|
+
out.push_str("_object");
|
|
131
|
+
out
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fn resolve_cargo_native_module(spec: &str, project_root: &Path) -> Result<ResolvedNativeModule, String> {
|
|
135
|
+
let tail = spec
|
|
136
|
+
.strip_prefix("cargo:")
|
|
137
|
+
.ok_or_else(|| format!("Invalid cargo native spec: {}", spec))?;
|
|
138
|
+
if tail.is_empty() {
|
|
139
|
+
return Err("cargo: import needs a dependency name, e.g. import { x } from 'cargo:serde_json'".into());
|
|
140
|
+
}
|
|
141
|
+
let dep_key = tail.to_string();
|
|
142
|
+
let tish = read_project_tish_config(project_root);
|
|
143
|
+
let rust_deps = tish.get("rustDependencies").and_then(|v| v.as_object()).ok_or_else(|| {
|
|
144
|
+
format!(
|
|
145
|
+
"cargo:{} requires package.json \"tish\": {{ \"rustDependencies\": {{ \"{}\": \"…\" }} }}",
|
|
146
|
+
tail, dep_key
|
|
147
|
+
)
|
|
148
|
+
})?;
|
|
149
|
+
if !rust_deps.contains_key(&dep_key) {
|
|
150
|
+
return Err(format!(
|
|
151
|
+
"cargo:{}: add \"{}\" to tish.rustDependencies in package.json (version string or inline table)",
|
|
152
|
+
tail, dep_key
|
|
153
|
+
));
|
|
154
|
+
}
|
|
155
|
+
let crate_name = dep_key.replace('-', "_");
|
|
156
|
+
let export_fn = cargo_export_fn_name(spec);
|
|
157
|
+
let crate_path = project_root
|
|
158
|
+
.canonicalize()
|
|
159
|
+
.unwrap_or_else(|_| project_root.to_path_buf());
|
|
160
|
+
Ok(ResolvedNativeModule {
|
|
161
|
+
spec: spec.to_string(),
|
|
162
|
+
package_name: dep_key.clone(),
|
|
163
|
+
crate_name,
|
|
164
|
+
crate_path,
|
|
165
|
+
export_fn,
|
|
166
|
+
use_path_dependency: false,
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
74
170
|
fn resolve_native_module(spec: &str, project_root: &Path) -> Result<ResolvedNativeModule, String> {
|
|
75
171
|
let package_name = if spec.starts_with("tish:") {
|
|
76
172
|
format!("tish-{}", spec.strip_prefix("tish:").unwrap_or(spec))
|
|
@@ -110,6 +206,236 @@ fn resolve_native_module(spec: &str, project_root: &Path) -> Result<ResolvedNati
|
|
|
110
206
|
crate_name: raw_crate.replace('-', "_"),
|
|
111
207
|
crate_path,
|
|
112
208
|
export_fn,
|
|
209
|
+
use_path_dependency: true,
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// Read the `tish` object from the project root `package.json` (empty JSON object if missing).
|
|
214
|
+
pub fn read_project_tish_config(project_root: &Path) -> serde_json::Value {
|
|
215
|
+
let path = project_root.join("package.json");
|
|
216
|
+
let Ok(content) = std::fs::read_to_string(&path) else {
|
|
217
|
+
return serde_json::json!({});
|
|
218
|
+
};
|
|
219
|
+
let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
|
|
220
|
+
return serde_json::json!({});
|
|
221
|
+
};
|
|
222
|
+
json.get("tish").cloned().unwrap_or_else(|| serde_json::json!({}))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
fn resolve_cargo_path_for_toml(project_root: &Path, raw: &str) -> String {
|
|
226
|
+
let p = Path::new(raw);
|
|
227
|
+
let resolved = if p.is_absolute() {
|
|
228
|
+
p.to_path_buf()
|
|
229
|
+
} else {
|
|
230
|
+
project_root.join(p)
|
|
231
|
+
};
|
|
232
|
+
let resolved = resolved.canonicalize().unwrap_or(resolved);
|
|
233
|
+
resolved.display().to_string().replace('\\', "/")
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
fn json_to_cargo_inline_value(v: &serde_json::Value, project_root: &Path) -> Result<String, String> {
|
|
237
|
+
match v {
|
|
238
|
+
serde_json::Value::String(s) => Ok(format!("{:?}", s.as_str())),
|
|
239
|
+
serde_json::Value::Bool(b) => Ok(b.to_string()),
|
|
240
|
+
serde_json::Value::Number(n) => Ok(n.to_string()),
|
|
241
|
+
serde_json::Value::Array(arr) => {
|
|
242
|
+
let mut inner = Vec::new();
|
|
243
|
+
for item in arr {
|
|
244
|
+
inner.push(json_to_cargo_inline_value(item, project_root)?);
|
|
245
|
+
}
|
|
246
|
+
Ok(format!("[{}]", inner.join(", ")))
|
|
247
|
+
}
|
|
248
|
+
serde_json::Value::Object(map) => {
|
|
249
|
+
let mut parts = Vec::new();
|
|
250
|
+
for (k, v) in map {
|
|
251
|
+
let rhs = if k == "path" && v.as_str().is_some() {
|
|
252
|
+
let s = v.as_str().unwrap();
|
|
253
|
+
format!("{:?}", resolve_cargo_path_for_toml(project_root, s))
|
|
254
|
+
} else {
|
|
255
|
+
json_to_cargo_inline_value(v, project_root)?
|
|
256
|
+
};
|
|
257
|
+
parts.push(format!("{} = {}", k, rhs));
|
|
258
|
+
}
|
|
259
|
+
Ok(format!("{{ {} }}", parts.join(", ")))
|
|
260
|
+
}
|
|
261
|
+
serde_json::Value::Null => Err("null is not valid in a Cargo dependency value".to_string()),
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// Serialize `tish.rustDependencies` from project `package.json` into Cargo.toml `[dependencies]` lines.
|
|
266
|
+
/// Relative `path = "…"` entries in inline tables are resolved against `project_root` so the temp build crate can find them.
|
|
267
|
+
pub fn format_rust_dependencies_toml(tish: &serde_json::Value, project_root: &Path) -> Result<String, String> {
|
|
268
|
+
let Some(obj) = tish.get("rustDependencies").and_then(|v| v.as_object()) else {
|
|
269
|
+
return Ok(String::new());
|
|
270
|
+
};
|
|
271
|
+
let mut out = String::new();
|
|
272
|
+
for (name, val) in obj {
|
|
273
|
+
match val {
|
|
274
|
+
serde_json::Value::String(_) | serde_json::Value::Object(_) => {
|
|
275
|
+
out.push_str(&format!(
|
|
276
|
+
"{} = {}\n",
|
|
277
|
+
name,
|
|
278
|
+
json_to_cargo_inline_value(val, project_root)?
|
|
279
|
+
));
|
|
280
|
+
}
|
|
281
|
+
_ => {
|
|
282
|
+
return Err(format!(
|
|
283
|
+
"tish.rustDependencies.{} must be a string (version) or object (inline table)",
|
|
284
|
+
name
|
|
285
|
+
));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
Ok(out)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/// Map a Tish export name to a Rust identifier (e.g. `readFile` → `read_file`) for shim crate symbols.
|
|
293
|
+
pub fn export_name_to_rust_ident(export_name: &str) -> String {
|
|
294
|
+
let mut out = String::new();
|
|
295
|
+
for (i, c) in export_name.chars().enumerate() {
|
|
296
|
+
if c.is_uppercase() && i > 0 {
|
|
297
|
+
out.push('_');
|
|
298
|
+
}
|
|
299
|
+
for lower in c.to_lowercase() {
|
|
300
|
+
out.push(lower);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if out.is_empty() {
|
|
304
|
+
"native_export".to_string()
|
|
305
|
+
} else {
|
|
306
|
+
out
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/// Collect `(spec, export_name)` for every non-builtin native import in the program.
|
|
311
|
+
pub fn infer_native_module_exports(program: &Program) -> HashMap<String, HashSet<String>> {
|
|
312
|
+
let mut map: HashMap<String, HashSet<String>> = HashMap::new();
|
|
313
|
+
for stmt in &program.statements {
|
|
314
|
+
match stmt {
|
|
315
|
+
Statement::VarDecl {
|
|
316
|
+
init: Some(Expr::NativeModuleLoad { spec, export_name, .. }),
|
|
317
|
+
..
|
|
318
|
+
} => {
|
|
319
|
+
let s = spec.as_ref();
|
|
320
|
+
if is_builtin_native_spec(s) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
map.entry(s.to_string())
|
|
324
|
+
.or_default()
|
|
325
|
+
.insert(export_name.to_string());
|
|
326
|
+
}
|
|
327
|
+
Statement::Import { specifiers, from, .. } if is_native_import(from.as_ref()) => {
|
|
328
|
+
let spec = normalize_builtin_spec(from.as_ref()).unwrap_or_else(|| from.to_string());
|
|
329
|
+
if is_builtin_native_spec(&spec) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
for sp in specifiers {
|
|
333
|
+
if let ImportSpecifier::Named { name, .. } = sp {
|
|
334
|
+
map.entry(spec.clone())
|
|
335
|
+
.or_default()
|
|
336
|
+
.insert(name.to_string());
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
_ => {}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
map
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/// Emit `generated_native.rs` for [`NativeModuleInit::Generated`] modules.
|
|
347
|
+
pub fn generate_native_wrapper_rs(
|
|
348
|
+
modules: &[ResolvedNativeModule],
|
|
349
|
+
inferred: &HashMap<String, HashSet<String>>,
|
|
350
|
+
init_by_spec: &HashMap<String, NativeModuleInit>,
|
|
351
|
+
) -> String {
|
|
352
|
+
let mut file = String::from(
|
|
353
|
+
"//! Generated by `tish build` — do not edit.\n\
|
|
354
|
+
use std::cell::RefCell;\n\
|
|
355
|
+
use std::rc::Rc;\n\
|
|
356
|
+
use std::sync::Arc;\n\
|
|
357
|
+
use tishlang_runtime::{ObjectMap, Value};\n\n",
|
|
358
|
+
);
|
|
359
|
+
let mut any = false;
|
|
360
|
+
for m in modules {
|
|
361
|
+
let Some(NativeModuleInit::Generated { shim_crate, export_fn }) = init_by_spec.get(&m.spec) else {
|
|
362
|
+
continue;
|
|
363
|
+
};
|
|
364
|
+
let Some(names) = inferred.get(&m.spec) else {
|
|
365
|
+
continue;
|
|
366
|
+
};
|
|
367
|
+
if names.is_empty() {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
any = true;
|
|
371
|
+
let mut keys: Vec<_> = names.iter().cloned().collect();
|
|
372
|
+
keys.sort();
|
|
373
|
+
file.push_str(&format!("pub fn {}() -> Value {{\n", export_fn));
|
|
374
|
+
file.push_str(" let mut m = ObjectMap::default();\n");
|
|
375
|
+
for export_name in keys {
|
|
376
|
+
let rust_fn = export_name_to_rust_ident(&export_name);
|
|
377
|
+
let key_lit = format!("{:?}", export_name);
|
|
378
|
+
file.push_str(&format!(
|
|
379
|
+
" m.insert(Arc::from({}), Value::Function(Rc::new(|args: &[Value]| {{\n {}::{}(args)\n }})));\n",
|
|
380
|
+
key_lit, shim_crate, rust_fn
|
|
381
|
+
));
|
|
382
|
+
}
|
|
383
|
+
file.push_str(" Value::Object(Rc::new(RefCell::new(m)))\n}\n\n");
|
|
384
|
+
}
|
|
385
|
+
if !any {
|
|
386
|
+
return String::new();
|
|
387
|
+
}
|
|
388
|
+
file
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/// Combine project `package.json`, inferred exports, and resolved native modules into build artifacts.
|
|
392
|
+
pub fn compute_native_build_artifacts(
|
|
393
|
+
program: &Program,
|
|
394
|
+
project_root: &Path,
|
|
395
|
+
native_modules: &[ResolvedNativeModule],
|
|
396
|
+
) -> Result<NativeBuildArtifacts, String> {
|
|
397
|
+
let tish = read_project_tish_config(project_root);
|
|
398
|
+
let rust_dependencies_toml = format_rust_dependencies_toml(&tish, project_root)?;
|
|
399
|
+
let inferred = infer_native_module_exports(program);
|
|
400
|
+
let gen_tish = tish
|
|
401
|
+
.get("generateNativeWrapper")
|
|
402
|
+
.and_then(|v| v.as_bool())
|
|
403
|
+
.unwrap_or(false);
|
|
404
|
+
|
|
405
|
+
let mut native_init: HashMap<String, NativeModuleInit> = HashMap::new();
|
|
406
|
+
for m in native_modules {
|
|
407
|
+
let use_gen = if is_cargo_native_spec(&m.spec) {
|
|
408
|
+
inferred.get(&m.spec).map(|s| !s.is_empty()).unwrap_or(false)
|
|
409
|
+
} else {
|
|
410
|
+
gen_tish && inferred.get(&m.spec).map(|s| !s.is_empty()).unwrap_or(false)
|
|
411
|
+
};
|
|
412
|
+
let init = if use_gen {
|
|
413
|
+
NativeModuleInit::Generated {
|
|
414
|
+
shim_crate: m.crate_name.clone(),
|
|
415
|
+
export_fn: m.export_fn.clone(),
|
|
416
|
+
}
|
|
417
|
+
} else {
|
|
418
|
+
NativeModuleInit::Legacy {
|
|
419
|
+
crate_name: m.crate_name.clone(),
|
|
420
|
+
export_fn: m.export_fn.clone(),
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
native_init.insert(m.spec.clone(), init);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
let generated_native_rs = {
|
|
427
|
+
let s = generate_native_wrapper_rs(native_modules, &inferred, &native_init);
|
|
428
|
+
if s.trim().is_empty() {
|
|
429
|
+
None
|
|
430
|
+
} else {
|
|
431
|
+
Some(s)
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
Ok(NativeBuildArtifacts {
|
|
436
|
+
rust_dependencies_toml,
|
|
437
|
+
generated_native_rs,
|
|
438
|
+
native_init,
|
|
113
439
|
})
|
|
114
440
|
}
|
|
115
441
|
|
|
@@ -151,16 +477,25 @@ fn read_package_name(pkg_path: &Path) -> Option<String> {
|
|
|
151
477
|
json.get("name").and_then(|v| v.as_str()).map(String::from)
|
|
152
478
|
}
|
|
153
479
|
|
|
480
|
+
fn stmt_native_specs(stmt: &Statement) -> Vec<String> {
|
|
481
|
+
match stmt {
|
|
482
|
+
Statement::VarDecl {
|
|
483
|
+
init: Some(Expr::NativeModuleLoad { spec, .. }),
|
|
484
|
+
..
|
|
485
|
+
} => vec![spec.to_string()],
|
|
486
|
+
Statement::Import { from, .. } if is_native_import(from.as_ref()) => {
|
|
487
|
+
vec![normalize_builtin_spec(from.as_ref()).unwrap_or_else(|| from.to_string())]
|
|
488
|
+
}
|
|
489
|
+
_ => vec![],
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
154
493
|
/// Extract Cargo feature names from native imports in a merged program.
|
|
155
494
|
/// Used to enable tishlang_runtime features based on `import { x } from 'tish:egui'` etc.
|
|
156
495
|
pub fn extract_native_import_features(program: &Program) -> Vec<String> {
|
|
157
496
|
let mut features = std::collections::HashSet::new();
|
|
158
497
|
for stmt in &program.statements {
|
|
159
|
-
|
|
160
|
-
init: Some(Expr::NativeModuleLoad { spec, .. }),
|
|
161
|
-
..
|
|
162
|
-
} = stmt
|
|
163
|
-
{
|
|
498
|
+
for spec in stmt_native_specs(stmt) {
|
|
164
499
|
if let Some(f) = native_spec_to_feature(spec.as_ref()) {
|
|
165
500
|
features.insert(f);
|
|
166
501
|
}
|
|
@@ -171,27 +506,17 @@ pub fn extract_native_import_features(program: &Program) -> Vec<String> {
|
|
|
171
506
|
|
|
172
507
|
/// Returns true if the merged program contains native imports (tish:*, @scope/pkg).
|
|
173
508
|
pub fn has_native_imports(program: &Program) -> bool {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
} = stmt
|
|
179
|
-
{
|
|
180
|
-
return true;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
false
|
|
509
|
+
program
|
|
510
|
+
.statements
|
|
511
|
+
.iter()
|
|
512
|
+
.any(|stmt| !stmt_native_specs(stmt).is_empty())
|
|
184
513
|
}
|
|
185
514
|
|
|
186
515
|
/// Returns true if the merged program contains external native imports (not built-in tish:fs/http/process).
|
|
187
516
|
/// Cranelift/LLVM reject these; bytecode VM supports built-ins only.
|
|
188
517
|
pub fn has_external_native_imports(program: &Program) -> bool {
|
|
189
518
|
for stmt in &program.statements {
|
|
190
|
-
|
|
191
|
-
init: Some(Expr::NativeModuleLoad { spec, .. }),
|
|
192
|
-
..
|
|
193
|
-
} = stmt
|
|
194
|
-
{
|
|
519
|
+
for spec in stmt_native_specs(stmt) {
|
|
195
520
|
if !is_builtin_native_spec(spec.as_ref()) {
|
|
196
521
|
return true;
|
|
197
522
|
}
|
|
@@ -267,10 +592,10 @@ pub fn resolve_project_from_stdin(
|
|
|
267
592
|
|
|
268
593
|
for stmt in &program.statements {
|
|
269
594
|
if let Statement::Import { from, .. } = stmt {
|
|
270
|
-
if is_native_import(from) {
|
|
595
|
+
if is_native_import(from.as_ref()) {
|
|
271
596
|
continue;
|
|
272
597
|
}
|
|
273
|
-
let dep_path = resolve_import_path(from, from_dir, &root_canon)?;
|
|
598
|
+
let dep_path = resolve_import_path(from.as_ref(), from_dir, &root_canon)?;
|
|
274
599
|
if !path_to_module.contains_key(&dep_path) {
|
|
275
600
|
load_module_recursive(
|
|
276
601
|
&dep_path,
|
|
@@ -320,10 +645,10 @@ fn load_module_recursive(
|
|
|
320
645
|
let dir = canonical.parent().unwrap_or(Path::new("."));
|
|
321
646
|
for stmt in &program.statements {
|
|
322
647
|
if let Statement::Import { from, .. } = stmt {
|
|
323
|
-
if is_native_import(from) {
|
|
648
|
+
if is_native_import(from.as_ref()) {
|
|
324
649
|
continue; // Native imports don't load files
|
|
325
650
|
}
|
|
326
|
-
let dep_path = resolve_import_path(from, dir, project_root)?;
|
|
651
|
+
let dep_path = resolve_import_path(from.as_ref(), dir, project_root)?;
|
|
327
652
|
if !path_to_module.contains_key(&dep_path) {
|
|
328
653
|
load_module_recursive(
|
|
329
654
|
&dep_path,
|
|
@@ -344,9 +669,11 @@ fn load_module_recursive(
|
|
|
344
669
|
/// Returns true for native module imports that don't resolve to files.
|
|
345
670
|
/// - fs, http, process, ws (Node-compatible aliases for tish:fs, tish:http, tish:process, tish:ws)
|
|
346
671
|
/// - tish:egui, tish:polars, etc.
|
|
672
|
+
/// - cargo:… (Cargo `rustDependencies` + generated wrapper; Rust native backend)
|
|
347
673
|
/// - @scope/package (npm-style)
|
|
348
674
|
pub fn is_native_import(spec: &str) -> bool {
|
|
349
675
|
spec.starts_with("tish:")
|
|
676
|
+
|| spec.starts_with("cargo:")
|
|
350
677
|
|| spec.starts_with('@')
|
|
351
678
|
|| matches!(spec, "fs" | "http" | "process" | "ws")
|
|
352
679
|
}
|
|
@@ -476,10 +803,10 @@ fn has_cycle_from(
|
|
|
476
803
|
) -> Result<bool, String> {
|
|
477
804
|
for stmt in &program.statements {
|
|
478
805
|
if let Statement::Import { from, .. } = stmt {
|
|
479
|
-
if is_native_import(from) {
|
|
806
|
+
if is_native_import(from.as_ref()) {
|
|
480
807
|
continue;
|
|
481
808
|
}
|
|
482
|
-
let dep_path = resolve_import_path(from, from_dir, Path::new("."))?;
|
|
809
|
+
let dep_path = resolve_import_path(from.as_ref(), from_dir, Path::new("."))?;
|
|
483
810
|
if let Some(&dep_idx) = path_to_idx.get(&dep_path) {
|
|
484
811
|
if stack.contains(&dep_idx) {
|
|
485
812
|
stack.push(dep_idx);
|
|
@@ -548,10 +875,11 @@ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
|
|
|
548
875
|
for stmt in &module.program.statements {
|
|
549
876
|
match stmt {
|
|
550
877
|
Statement::Import { specifiers, from, span } => {
|
|
551
|
-
if is_native_import(from) {
|
|
878
|
+
if is_native_import(from.as_ref()) {
|
|
552
879
|
// Normalize fs/http/process -> tish:fs etc. for Node compatibility
|
|
553
880
|
let canonical_spec =
|
|
554
|
-
normalize_builtin_spec(from
|
|
881
|
+
normalize_builtin_spec(from.as_ref())
|
|
882
|
+
.unwrap_or_else(|| from.to_string());
|
|
555
883
|
// Emit VarDecl with NativeModuleLoad for each specifier
|
|
556
884
|
for spec in specifiers {
|
|
557
885
|
match spec {
|
|
@@ -589,7 +917,7 @@ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
|
|
|
589
917
|
}
|
|
590
918
|
continue;
|
|
591
919
|
}
|
|
592
|
-
let dep_path = resolve_import_path(from, dir, Path::new("."))?;
|
|
920
|
+
let dep_path = resolve_import_path(from.as_ref(), dir, Path::new("."))?;
|
|
593
921
|
let dep_path = dep_path
|
|
594
922
|
.canonicalize()
|
|
595
923
|
.unwrap_or(dep_path);
|
|
@@ -682,3 +1010,53 @@ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
|
|
|
682
1010
|
}
|
|
683
1011
|
Ok(Program { statements })
|
|
684
1012
|
}
|
|
1013
|
+
|
|
1014
|
+
#[cfg(test)]
|
|
1015
|
+
mod cargo_spec_tests {
|
|
1016
|
+
use std::sync::Arc;
|
|
1017
|
+
|
|
1018
|
+
use super::cargo_export_fn_name;
|
|
1019
|
+
use super::is_native_import;
|
|
1020
|
+
|
|
1021
|
+
#[test]
|
|
1022
|
+
fn is_native_import_accepts_arc_str_ref() {
|
|
1023
|
+
let from: &Arc<str> = &Arc::from("cargo:demo_shim");
|
|
1024
|
+
assert!(is_native_import(from));
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
#[test]
|
|
1028
|
+
fn detect_cycles_skips_cargo_import() {
|
|
1029
|
+
use super::{detect_cycles, resolve_project};
|
|
1030
|
+
let dir = tempfile::tempdir().expect("tempdir");
|
|
1031
|
+
let p = dir.path().join("main.tish");
|
|
1032
|
+
let src = "import { greet } from 'cargo:demo_shim'\nconsole.log(1)\n";
|
|
1033
|
+
std::fs::write(&p, src).unwrap();
|
|
1034
|
+
let root = dir.path();
|
|
1035
|
+
let modules = resolve_project(&p, Some(root)).unwrap();
|
|
1036
|
+
detect_cycles(&modules).unwrap();
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
#[test]
|
|
1040
|
+
fn merge_modules_skips_cargo_import() {
|
|
1041
|
+
use super::{merge_modules, resolve_project};
|
|
1042
|
+
let dir = tempfile::tempdir().expect("tempdir");
|
|
1043
|
+
let p = dir.path().join("main.tish");
|
|
1044
|
+
let src = "import { greet } from 'cargo:demo_shim'\nconsole.log(1)\n";
|
|
1045
|
+
std::fs::write(&p, src).unwrap();
|
|
1046
|
+
let root = dir.path();
|
|
1047
|
+
let modules = resolve_project(&p, Some(root)).unwrap();
|
|
1048
|
+
merge_modules(modules).unwrap();
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
#[test]
|
|
1052
|
+
fn cargo_export_fn_name_sanitizes() {
|
|
1053
|
+
assert_eq!(
|
|
1054
|
+
cargo_export_fn_name("cargo:serde_json"),
|
|
1055
|
+
"cargo_native_serde_json_object"
|
|
1056
|
+
);
|
|
1057
|
+
assert_eq!(
|
|
1058
|
+
cargo_export_fn_name("cargo:my-crate"),
|
|
1059
|
+
"cargo_native_my_crate_object"
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
}
|