@tishlang/tish-format 1.0.12 → 1.0.13
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 +49 -0
- package/LICENSE +13 -0
- package/README.md +138 -0
- package/bin/tish-format +0 -0
- package/crates/js_to_tish/Cargo.toml +11 -0
- package/crates/js_to_tish/README.md +18 -0
- package/crates/js_to_tish/src/error.rs +55 -0
- package/crates/js_to_tish/src/lib.rs +11 -0
- package/crates/js_to_tish/src/span_util.rs +35 -0
- package/crates/js_to_tish/src/transform/expr.rs +610 -0
- package/crates/js_to_tish/src/transform/stmt.rs +503 -0
- package/crates/js_to_tish/src/transform.rs +60 -0
- package/crates/tish/Cargo.toml +54 -0
- package/crates/tish/src/cargo_native_registry.rs +32 -0
- package/crates/tish/src/cli_help.rs +565 -0
- package/crates/tish/src/main.rs +781 -0
- package/crates/tish/src/repl_completion.rs +200 -0
- 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 +1095 -0
- package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
- package/crates/tish/tests/shortcircuit.rs +65 -0
- package/crates/tish_ast/Cargo.toml +9 -0
- package/crates/tish_ast/src/ast.rs +620 -0
- package/crates/tish_ast/src/lib.rs +5 -0
- package/crates/tish_build_utils/Cargo.toml +11 -0
- package/crates/tish_build_utils/src/lib.rs +577 -0
- package/crates/tish_builtins/Cargo.toml +20 -0
- package/crates/tish_builtins/src/array.rs +441 -0
- package/crates/tish_builtins/src/construct.rs +159 -0
- package/crates/tish_builtins/src/globals.rs +213 -0
- package/crates/tish_builtins/src/helpers.rs +35 -0
- package/crates/tish_builtins/src/lib.rs +16 -0
- package/crates/tish_builtins/src/math.rs +89 -0
- package/crates/tish_builtins/src/object.rs +36 -0
- package/crates/tish_builtins/src/string.rs +647 -0
- package/crates/tish_builtins/src/symbol.rs +83 -0
- package/crates/tish_bytecode/Cargo.toml +17 -0
- package/crates/tish_bytecode/src/chunk.rs +96 -0
- package/crates/tish_bytecode/src/compiler.rs +1760 -0
- package/crates/tish_bytecode/src/encoding.rs +100 -0
- package/crates/tish_bytecode/src/lib.rs +19 -0
- package/crates/tish_bytecode/src/opcode.rs +142 -0
- package/crates/tish_bytecode/src/peephole.rs +189 -0
- package/crates/tish_bytecode/src/serialize.rs +163 -0
- package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
- package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
- package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
- package/crates/tish_compile/Cargo.toml +26 -0
- package/crates/tish_compile/src/codegen.rs +5332 -0
- package/crates/tish_compile/src/infer.rs +292 -0
- package/crates/tish_compile/src/lib.rs +164 -0
- package/crates/tish_compile/src/resolve.rs +1388 -0
- package/crates/tish_compile/src/types.rs +501 -0
- package/crates/tish_compile_js/Cargo.toml +18 -0
- package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
- package/crates/tish_compile_js/src/codegen.rs +871 -0
- package/crates/tish_compile_js/src/error.rs +20 -0
- package/crates/tish_compile_js/src/lib.rs +26 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +350 -0
- package/crates/tish_compiler_wasm/Cargo.toml +21 -0
- package/crates/tish_compiler_wasm/src/lib.rs +57 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
- package/crates/tish_core/Cargo.toml +26 -0
- package/crates/tish_core/src/console_style.rs +160 -0
- package/crates/tish_core/src/json.rs +387 -0
- package/crates/tish_core/src/lib.rs +17 -0
- package/crates/tish_core/src/macros.rs +36 -0
- package/crates/tish_core/src/uri.rs +118 -0
- package/crates/tish_core/src/value.rs +696 -0
- package/crates/tish_core/src/vmref.rs +178 -0
- package/crates/tish_cranelift/Cargo.toml +19 -0
- package/crates/tish_cranelift/src/lib.rs +43 -0
- package/crates/tish_cranelift/src/link.rs +117 -0
- package/crates/tish_cranelift/src/lower.rs +85 -0
- package/crates/tish_cranelift_runtime/Cargo.toml +25 -0
- package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
- package/crates/tish_eval/Cargo.toml +45 -0
- package/crates/tish_eval/src/eval.rs +3717 -0
- package/crates/tish_eval/src/http.rs +188 -0
- package/crates/tish_eval/src/lib.rs +99 -0
- package/crates/tish_eval/src/natives.rs +399 -0
- package/crates/tish_eval/src/promise.rs +179 -0
- package/crates/tish_eval/src/regex.rs +299 -0
- package/crates/tish_eval/src/timers.rs +120 -0
- package/crates/tish_eval/src/value.rs +318 -0
- package/crates/tish_eval/src/value_convert.rs +111 -0
- package/crates/tish_fmt/Cargo.toml +16 -0
- package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
- package/crates/tish_fmt/src/lib.rs +2101 -0
- package/crates/tish_jsx_web/Cargo.toml +9 -0
- package/crates/tish_jsx_web/README.md +5 -0
- package/crates/tish_jsx_web/src/lib.rs +2 -0
- package/crates/tish_lexer/Cargo.toml +9 -0
- package/crates/tish_lexer/src/lib.rs +716 -0
- package/crates/tish_lexer/src/token.rs +163 -0
- package/crates/tish_lint/Cargo.toml +18 -0
- package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
- package/crates/tish_lint/src/lib.rs +289 -0
- package/crates/tish_llvm/Cargo.toml +13 -0
- package/crates/tish_llvm/src/lib.rs +115 -0
- package/crates/tish_lsp/Cargo.toml +25 -0
- package/crates/tish_lsp/README.md +26 -0
- package/crates/tish_lsp/src/builtin_goto.rs +362 -0
- package/crates/tish_lsp/src/import_goto.rs +562 -0
- package/crates/tish_lsp/src/main.rs +1046 -0
- package/crates/tish_native/Cargo.toml +16 -0
- package/crates/tish_native/src/build.rs +427 -0
- package/crates/tish_native/src/config.rs +48 -0
- package/crates/tish_native/src/lib.rs +416 -0
- package/crates/tish_opt/Cargo.toml +13 -0
- package/crates/tish_opt/src/lib.rs +943 -0
- package/crates/tish_parser/Cargo.toml +11 -0
- package/crates/tish_parser/src/lib.rs +332 -0
- package/crates/tish_parser/src/parser.rs +2304 -0
- package/crates/tish_pg/Cargo.toml +34 -0
- package/crates/tish_pg/README.md +38 -0
- package/crates/tish_pg/src/error.rs +52 -0
- package/crates/tish_pg/src/lib.rs +955 -0
- package/crates/tish_resolve/Cargo.toml +13 -0
- package/crates/tish_resolve/src/lib.rs +3561 -0
- package/crates/tish_resolve/src/pos.rs +141 -0
- package/crates/tish_runtime/Cargo.toml +96 -0
- package/crates/tish_runtime/src/http.rs +1298 -0
- package/crates/tish_runtime/src/http_fetch.rs +471 -0
- package/crates/tish_runtime/src/http_hyper.rs +418 -0
- package/crates/tish_runtime/src/http_prefork.rs +189 -0
- package/crates/tish_runtime/src/lib.rs +1192 -0
- package/crates/tish_runtime/src/native_promise.rs +15 -0
- package/crates/tish_runtime/src/promise.rs +248 -0
- package/crates/tish_runtime/src/promise_io.rs +38 -0
- package/crates/tish_runtime/src/timers.rs +166 -0
- package/crates/tish_runtime/src/ws.rs +761 -0
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
- package/crates/tish_ui/Cargo.toml +17 -0
- package/crates/tish_ui/src/jsx.rs +682 -0
- package/crates/tish_ui/src/lib.rs +20 -0
- package/crates/tish_ui/src/runtime/hooks.rs +569 -0
- package/crates/tish_ui/src/runtime/mod.rs +180 -0
- package/crates/tish_vm/Cargo.toml +47 -0
- package/crates/tish_vm/src/lib.rs +39 -0
- package/crates/tish_vm/src/vm.rs +2192 -0
- package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
- package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
- package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
- package/crates/tish_wasm/Cargo.toml +15 -0
- package/crates/tish_wasm/src/lib.rs +424 -0
- package/crates/tish_wasm_runtime/Cargo.toml +37 -0
- package/crates/tish_wasm_runtime/src/gpu.rs +413 -0
- package/crates/tish_wasm_runtime/src/lib.rs +42 -0
- package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
- package/crates/tishlang_cargo_bindgen/src/classify.rs +263 -0
- package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
- package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
- package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
- package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
- package/justfile +268 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-fmt +0 -0
|
@@ -0,0 +1,1388 @@
|
|
|
1
|
+
//! Module resolver: resolves relative imports, builds dependency graph, detects cycles.
|
|
2
|
+
//! Supports native imports: `tish:…`, `cargo:…`, `@scope/pkg` (via package.json).
|
|
3
|
+
|
|
4
|
+
use std::collections::{HashMap, HashSet};
|
|
5
|
+
use std::path::{Path, PathBuf};
|
|
6
|
+
use std::sync::Arc;
|
|
7
|
+
use tishlang_ast::{ExportDeclaration, Expr, ImportSpecifier, MemberProp, Program, Statement, CallArg};
|
|
8
|
+
|
|
9
|
+
/// Resolved native module: crate path and init expression.
|
|
10
|
+
#[derive(Debug, Clone)]
|
|
11
|
+
pub struct ResolvedNativeModule {
|
|
12
|
+
pub spec: String,
|
|
13
|
+
/// Cargo package name (e.g. tish-egui) for [dependencies]
|
|
14
|
+
pub package_name: String,
|
|
15
|
+
/// Rust crate name with underscores (e.g. tish_egui) for use in generated code
|
|
16
|
+
pub crate_name: String,
|
|
17
|
+
pub crate_path: PathBuf,
|
|
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>,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Node-compatible aliases for built-in modules (fs -> tish:fs, etc.).
|
|
49
|
+
const BUILTIN_ALIASES: &[(&str, &str)] = &[
|
|
50
|
+
("fs", "tish:fs"),
|
|
51
|
+
("http", "tish:http"),
|
|
52
|
+
("timers", "tish:timers"),
|
|
53
|
+
("process", "tish:process"),
|
|
54
|
+
("ws", "tish:ws"),
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
/// Normalize built-in spec to canonical form. E.g. "fs" -> "tish:fs".
|
|
58
|
+
pub fn normalize_builtin_spec(spec: &str) -> Option<String> {
|
|
59
|
+
if spec.starts_with("tish:") {
|
|
60
|
+
return Some(spec.to_string());
|
|
61
|
+
}
|
|
62
|
+
BUILTIN_ALIASES
|
|
63
|
+
.iter()
|
|
64
|
+
.find(|(alias, _)| *alias == spec)
|
|
65
|
+
.map(|(_, canonical)| (*canonical).to_string())
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Built-in modules that come from tishlang_runtime, not from package.json.
|
|
69
|
+
pub fn is_builtin_native_spec(spec: &str) -> bool {
|
|
70
|
+
matches!(
|
|
71
|
+
spec,
|
|
72
|
+
"tish:fs" | "tish:http" | "tish:timers" | "tish:process" | "tish:ws"
|
|
73
|
+
) || matches!(spec, "fs" | "http" | "timers" | "process" | "ws")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Resolve all native imports in a merged program via package.json lookup.
|
|
77
|
+
/// Built-in modules (tish:fs, tish:http, tish:process) are skipped - they use tishlang_runtime directly.
|
|
78
|
+
/// Handles both lowered `NativeModuleLoad` (merged modules) and raw `import { … } from 'tish:…'`.
|
|
79
|
+
pub fn resolve_native_modules(
|
|
80
|
+
program: &Program,
|
|
81
|
+
project_root: &Path,
|
|
82
|
+
) -> Result<Vec<ResolvedNativeModule>, String> {
|
|
83
|
+
let root_canon = project_root
|
|
84
|
+
.canonicalize()
|
|
85
|
+
.map_err(|e| format!("Cannot canonicalize project root: {}", e))?;
|
|
86
|
+
let mut seen = HashSet::new();
|
|
87
|
+
let mut modules = Vec::new();
|
|
88
|
+
for stmt in &program.statements {
|
|
89
|
+
let specs: Vec<String> = match stmt {
|
|
90
|
+
Statement::VarDecl {
|
|
91
|
+
init: Some(Expr::NativeModuleLoad { spec, .. }),
|
|
92
|
+
..
|
|
93
|
+
} => vec![spec.as_ref().to_string()],
|
|
94
|
+
Statement::Import { from, .. } if is_native_import(from.as_ref()) => {
|
|
95
|
+
vec![normalize_builtin_spec(from.as_ref()).unwrap_or_else(|| from.to_string())]
|
|
96
|
+
}
|
|
97
|
+
_ => continue,
|
|
98
|
+
};
|
|
99
|
+
for s in specs {
|
|
100
|
+
if is_builtin_native_spec(&s) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if !seen.insert(s.clone()) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
let m = if s.starts_with("cargo:") {
|
|
107
|
+
resolve_cargo_native_module(&s, &root_canon)?
|
|
108
|
+
} else {
|
|
109
|
+
resolve_native_module(&s, &root_canon)?
|
|
110
|
+
};
|
|
111
|
+
modules.push(m);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
Ok(modules)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// True when merged Tish source references the browser global `document` (e.g. juke-cards).
|
|
118
|
+
pub fn program_uses_document(program: &Program) -> bool {
|
|
119
|
+
use tishlang_ast::{ArrayElement, ArrowBody, JsxAttrValue, JsxChild, JsxProp, ObjectProp};
|
|
120
|
+
|
|
121
|
+
fn expr_uses_document(e: &Expr) -> bool {
|
|
122
|
+
match e {
|
|
123
|
+
Expr::Ident { name, .. } => name.as_ref() == "document",
|
|
124
|
+
Expr::Literal { .. } | Expr::NativeModuleLoad { .. } => false,
|
|
125
|
+
Expr::Binary { left, right, .. } => {
|
|
126
|
+
expr_uses_document(left) || expr_uses_document(right)
|
|
127
|
+
}
|
|
128
|
+
Expr::Unary { operand, .. } | Expr::TypeOf { operand, .. } => {
|
|
129
|
+
expr_uses_document(operand)
|
|
130
|
+
}
|
|
131
|
+
Expr::Call { callee, args, .. } => {
|
|
132
|
+
expr_uses_document(callee)
|
|
133
|
+
|| args.iter().any(|a| match a {
|
|
134
|
+
CallArg::Expr(e) | CallArg::Spread(e) => expr_uses_document(e),
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
Expr::New { callee, args, .. } => {
|
|
138
|
+
expr_uses_document(callee)
|
|
139
|
+
|| args.iter().any(|a| match a {
|
|
140
|
+
CallArg::Expr(e) | CallArg::Spread(e) => expr_uses_document(e),
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
Expr::Member { object, prop, .. } => {
|
|
144
|
+
expr_uses_document(object)
|
|
145
|
+
|| if let MemberProp::Expr(e) = prop {
|
|
146
|
+
expr_uses_document(e)
|
|
147
|
+
} else {
|
|
148
|
+
false
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
Expr::Index { object, index, .. } => {
|
|
152
|
+
expr_uses_document(object) || expr_uses_document(index)
|
|
153
|
+
}
|
|
154
|
+
Expr::Conditional {
|
|
155
|
+
cond,
|
|
156
|
+
then_branch,
|
|
157
|
+
else_branch,
|
|
158
|
+
..
|
|
159
|
+
} => {
|
|
160
|
+
expr_uses_document(cond)
|
|
161
|
+
|| expr_uses_document(then_branch)
|
|
162
|
+
|| expr_uses_document(else_branch)
|
|
163
|
+
}
|
|
164
|
+
Expr::NullishCoalesce { left, right, .. } => {
|
|
165
|
+
expr_uses_document(left) || expr_uses_document(right)
|
|
166
|
+
}
|
|
167
|
+
Expr::Array { elements, .. } => elements.iter().any(|el| match el {
|
|
168
|
+
ArrayElement::Expr(e) | ArrayElement::Spread(e) => expr_uses_document(e),
|
|
169
|
+
}),
|
|
170
|
+
Expr::Object { props, .. } => props.iter().any(|p| match p {
|
|
171
|
+
ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => expr_uses_document(e),
|
|
172
|
+
}),
|
|
173
|
+
Expr::Assign { value, .. }
|
|
174
|
+
| Expr::CompoundAssign { value, .. }
|
|
175
|
+
| Expr::LogicalAssign { value, .. }
|
|
176
|
+
| Expr::MemberAssign { value, .. }
|
|
177
|
+
| Expr::IndexAssign { value, .. } => expr_uses_document(value),
|
|
178
|
+
Expr::PostfixInc { .. }
|
|
179
|
+
| Expr::PostfixDec { .. }
|
|
180
|
+
| Expr::PrefixInc { .. }
|
|
181
|
+
| Expr::PrefixDec { .. } => false,
|
|
182
|
+
Expr::ArrowFunction { body, .. } => match body {
|
|
183
|
+
ArrowBody::Expr(e) => expr_uses_document(e),
|
|
184
|
+
ArrowBody::Block(s) => stmt_uses_document(s),
|
|
185
|
+
},
|
|
186
|
+
Expr::TemplateLiteral { exprs, .. } => exprs.iter().any(expr_uses_document),
|
|
187
|
+
Expr::Await { operand, .. } => expr_uses_document(operand),
|
|
188
|
+
Expr::JsxElement { props, children, .. } => {
|
|
189
|
+
props.iter().any(|p| match p {
|
|
190
|
+
JsxProp::Attr { value, .. } => match value {
|
|
191
|
+
JsxAttrValue::Expr(e) => expr_uses_document(e),
|
|
192
|
+
JsxAttrValue::String(_) | JsxAttrValue::ImplicitTrue => false,
|
|
193
|
+
},
|
|
194
|
+
JsxProp::Spread(e) => expr_uses_document(e),
|
|
195
|
+
}) || children.iter().any(|c| match c {
|
|
196
|
+
JsxChild::Expr(e) => expr_uses_document(e),
|
|
197
|
+
JsxChild::Text(_) => false,
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
Expr::JsxFragment { children, .. } => children.iter().any(|c| match c {
|
|
201
|
+
JsxChild::Expr(e) => expr_uses_document(e),
|
|
202
|
+
JsxChild::Text(_) => false,
|
|
203
|
+
}),
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fn stmt_uses_document(s: &Statement) -> bool {
|
|
208
|
+
match s {
|
|
209
|
+
Statement::VarDecl { init, .. } => init.as_ref().is_some_and(|e| expr_uses_document(e)),
|
|
210
|
+
Statement::VarDeclDestructure { init, .. } => expr_uses_document(init),
|
|
211
|
+
Statement::ExprStmt { expr, .. } => expr_uses_document(expr),
|
|
212
|
+
Statement::Return { value, .. } => value.as_ref().is_some_and(|e| expr_uses_document(e)),
|
|
213
|
+
Statement::Throw { value, .. } => expr_uses_document(value),
|
|
214
|
+
Statement::If {
|
|
215
|
+
cond,
|
|
216
|
+
then_branch,
|
|
217
|
+
else_branch,
|
|
218
|
+
..
|
|
219
|
+
} => {
|
|
220
|
+
expr_uses_document(cond)
|
|
221
|
+
|| stmt_uses_document(then_branch)
|
|
222
|
+
|| else_branch
|
|
223
|
+
.as_ref()
|
|
224
|
+
.is_some_and(|b| stmt_uses_document(b.as_ref()))
|
|
225
|
+
}
|
|
226
|
+
Statement::While { cond, body, .. }
|
|
227
|
+
| Statement::DoWhile { cond, body, .. } => {
|
|
228
|
+
expr_uses_document(cond) || stmt_uses_document(body)
|
|
229
|
+
}
|
|
230
|
+
Statement::For { init, cond, update, body, .. } => {
|
|
231
|
+
init.as_ref().is_some_and(|s| stmt_uses_document(s.as_ref()))
|
|
232
|
+
|| cond.as_ref().is_some_and(|e| expr_uses_document(e))
|
|
233
|
+
|| update.as_ref().is_some_and(|e| expr_uses_document(e))
|
|
234
|
+
|| stmt_uses_document(body)
|
|
235
|
+
}
|
|
236
|
+
Statement::ForOf { iterable, body, .. } => {
|
|
237
|
+
expr_uses_document(iterable) || stmt_uses_document(body)
|
|
238
|
+
}
|
|
239
|
+
Statement::Switch {
|
|
240
|
+
expr,
|
|
241
|
+
cases,
|
|
242
|
+
default_body,
|
|
243
|
+
..
|
|
244
|
+
} => {
|
|
245
|
+
expr_uses_document(expr)
|
|
246
|
+
|| cases.iter().any(|(e, stmts)| {
|
|
247
|
+
e.as_ref().is_some_and(|e| expr_uses_document(e))
|
|
248
|
+
|| stmts.iter().any(stmt_uses_document)
|
|
249
|
+
})
|
|
250
|
+
|| default_body
|
|
251
|
+
.as_ref()
|
|
252
|
+
.is_some_and(|stmts| stmts.iter().any(stmt_uses_document))
|
|
253
|
+
}
|
|
254
|
+
Statement::Block { statements, .. } => statements.iter().any(stmt_uses_document),
|
|
255
|
+
Statement::FunDecl { body, .. } => stmt_uses_document(body),
|
|
256
|
+
Statement::Try {
|
|
257
|
+
body,
|
|
258
|
+
catch_body,
|
|
259
|
+
finally_body,
|
|
260
|
+
..
|
|
261
|
+
} => {
|
|
262
|
+
stmt_uses_document(body)
|
|
263
|
+
|| catch_body
|
|
264
|
+
.as_ref()
|
|
265
|
+
.is_some_and(|b| stmt_uses_document(b.as_ref()))
|
|
266
|
+
|| finally_body
|
|
267
|
+
.as_ref()
|
|
268
|
+
.is_some_and(|b| stmt_uses_document(b.as_ref()))
|
|
269
|
+
}
|
|
270
|
+
Statement::Import { .. }
|
|
271
|
+
| Statement::Export { .. }
|
|
272
|
+
| Statement::Break { .. }
|
|
273
|
+
| Statement::Continue { .. }
|
|
274
|
+
| Statement::TypeAlias { .. }
|
|
275
|
+
| Statement::DeclareVar { .. }
|
|
276
|
+
| Statement::DeclareFun { .. } => false,
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
program.statements.iter().any(stmt_uses_document)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/// When Tish uses bare `document`, link `tish-canvas` even without `import from 'tish:canvas'`.
|
|
284
|
+
pub fn ensure_tish_canvas_module(
|
|
285
|
+
native_modules: &mut Vec<ResolvedNativeModule>,
|
|
286
|
+
project_root: &Path,
|
|
287
|
+
) -> Result<(), String> {
|
|
288
|
+
if native_modules
|
|
289
|
+
.iter()
|
|
290
|
+
.any(|m| m.crate_name == "tish_canvas" || m.package_name == "tish-canvas")
|
|
291
|
+
{
|
|
292
|
+
return Ok(());
|
|
293
|
+
}
|
|
294
|
+
let m = resolve_native_module("tish:canvas", project_root)?;
|
|
295
|
+
native_modules.push(m);
|
|
296
|
+
Ok(())
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/// True for `cargo:…` specs (Cargo-backed imports; Rust native backend only).
|
|
300
|
+
pub fn is_cargo_native_spec(spec: &str) -> bool {
|
|
301
|
+
spec.starts_with("cargo:")
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/// Stable Rust symbol for the generated namespace function, e.g. `cargo:my-crate` → `cargo_native_my_crate_object`.
|
|
305
|
+
pub fn cargo_export_fn_name(spec: &str) -> String {
|
|
306
|
+
let tail = spec.strip_prefix("cargo:").unwrap_or(spec);
|
|
307
|
+
let mut out = String::from("cargo_native_");
|
|
308
|
+
for c in tail.chars() {
|
|
309
|
+
if c.is_ascii_alphanumeric() {
|
|
310
|
+
out.push(c);
|
|
311
|
+
} else {
|
|
312
|
+
out.push('_');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if out == "cargo_native_" {
|
|
316
|
+
out.push_str("unnamed");
|
|
317
|
+
}
|
|
318
|
+
out.push_str("_object");
|
|
319
|
+
out
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
fn resolve_cargo_native_module(
|
|
323
|
+
spec: &str,
|
|
324
|
+
project_root: &Path,
|
|
325
|
+
) -> Result<ResolvedNativeModule, String> {
|
|
326
|
+
let tail = spec
|
|
327
|
+
.strip_prefix("cargo:")
|
|
328
|
+
.ok_or_else(|| format!("Invalid cargo native spec: {}", spec))?;
|
|
329
|
+
if tail.is_empty() {
|
|
330
|
+
return Err(
|
|
331
|
+
"cargo: import needs a dependency name, e.g. import { x } from 'cargo:my_crate'".into(),
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
let dep_key = tail.to_string();
|
|
335
|
+
let tish = read_project_tish_config(project_root);
|
|
336
|
+
let rust_deps = tish.get("rustDependencies").and_then(|v| v.as_object()).ok_or_else(|| {
|
|
337
|
+
format!(
|
|
338
|
+
"cargo:{} requires package.json \"tish\": {{ \"rustDependencies\": {{ \"{}\": \"…\" }} }}",
|
|
339
|
+
tail, dep_key
|
|
340
|
+
)
|
|
341
|
+
})?;
|
|
342
|
+
if !rust_deps.contains_key(&dep_key) {
|
|
343
|
+
return Err(format!(
|
|
344
|
+
"cargo:{}: add \"{}\" to tish.rustDependencies in package.json (version string or inline table)",
|
|
345
|
+
tail, dep_key
|
|
346
|
+
));
|
|
347
|
+
}
|
|
348
|
+
let crate_name = dep_key.replace('-', "_");
|
|
349
|
+
let export_fn = cargo_export_fn_name(spec);
|
|
350
|
+
let crate_path = project_root
|
|
351
|
+
.canonicalize()
|
|
352
|
+
.unwrap_or_else(|_| project_root.to_path_buf());
|
|
353
|
+
Ok(ResolvedNativeModule {
|
|
354
|
+
spec: spec.to_string(),
|
|
355
|
+
package_name: dep_key.clone(),
|
|
356
|
+
crate_name,
|
|
357
|
+
crate_path,
|
|
358
|
+
export_fn,
|
|
359
|
+
use_path_dependency: false,
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
fn resolve_native_module(spec: &str, project_root: &Path) -> Result<ResolvedNativeModule, String> {
|
|
364
|
+
let package_name = if spec.starts_with("tish:") {
|
|
365
|
+
format!("tish-{}", spec.strip_prefix("tish:").unwrap_or(spec))
|
|
366
|
+
} else if spec.starts_with('@') {
|
|
367
|
+
spec.to_string()
|
|
368
|
+
} else {
|
|
369
|
+
return Err(format!("Unsupported native import spec: {}", spec));
|
|
370
|
+
};
|
|
371
|
+
let pkg_dir = find_package_dir(&package_name, project_root)?;
|
|
372
|
+
let pkg_json = pkg_dir.join("package.json");
|
|
373
|
+
let content = std::fs::read_to_string(&pkg_json)
|
|
374
|
+
.map_err(|e| format!("Cannot read {}: {}", pkg_json.display(), e))?;
|
|
375
|
+
let json: serde_json::Value = serde_json::from_str(&content)
|
|
376
|
+
.map_err(|e| format!("Invalid JSON in {}: {}", pkg_json.display(), e))?;
|
|
377
|
+
let tish = json
|
|
378
|
+
.get("tish")
|
|
379
|
+
.and_then(|v| v.as_object())
|
|
380
|
+
.ok_or_else(|| {
|
|
381
|
+
format!(
|
|
382
|
+
"Package {} has no \"tish\" config in package.json",
|
|
383
|
+
package_name
|
|
384
|
+
)
|
|
385
|
+
})?;
|
|
386
|
+
if !tish
|
|
387
|
+
.get("module")
|
|
388
|
+
.and_then(|v| v.as_bool())
|
|
389
|
+
.unwrap_or(false)
|
|
390
|
+
{
|
|
391
|
+
return Err(format!(
|
|
392
|
+
"Package {} is not a Tish native module (tish.module must be true)",
|
|
393
|
+
package_name
|
|
394
|
+
));
|
|
395
|
+
}
|
|
396
|
+
let raw_crate = tish
|
|
397
|
+
.get("crate")
|
|
398
|
+
.and_then(|v| v.as_str())
|
|
399
|
+
.unwrap_or(&package_name)
|
|
400
|
+
.to_string();
|
|
401
|
+
let module_part = spec.strip_prefix("tish:").unwrap_or(spec);
|
|
402
|
+
let export_fn = tish
|
|
403
|
+
.get("export")
|
|
404
|
+
.and_then(|v| v.as_str())
|
|
405
|
+
.map(String::from)
|
|
406
|
+
.unwrap_or_else(|| format!("{}_object", str::replace(module_part, "-", "_")));
|
|
407
|
+
let crate_path = pkg_dir.canonicalize().unwrap_or(pkg_dir);
|
|
408
|
+
Ok(ResolvedNativeModule {
|
|
409
|
+
spec: spec.to_string(),
|
|
410
|
+
package_name: raw_crate.clone(),
|
|
411
|
+
crate_name: raw_crate.replace('-', "_"),
|
|
412
|
+
crate_path,
|
|
413
|
+
export_fn,
|
|
414
|
+
use_path_dependency: true,
|
|
415
|
+
})
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/// Read the `tish` object from the project root `package.json` (empty JSON object if missing).
|
|
419
|
+
pub fn read_project_tish_config(project_root: &Path) -> serde_json::Value {
|
|
420
|
+
let path = project_root.join("package.json");
|
|
421
|
+
let Ok(content) = std::fs::read_to_string(&path) else {
|
|
422
|
+
return serde_json::json!({});
|
|
423
|
+
};
|
|
424
|
+
let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
|
|
425
|
+
return serde_json::json!({});
|
|
426
|
+
};
|
|
427
|
+
json.get("tish")
|
|
428
|
+
.cloned()
|
|
429
|
+
.unwrap_or_else(|| serde_json::json!({}))
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
fn resolve_cargo_path_for_toml(project_root: &Path, raw: &str) -> String {
|
|
433
|
+
let p = Path::new(raw);
|
|
434
|
+
let resolved = if p.is_absolute() {
|
|
435
|
+
p.to_path_buf()
|
|
436
|
+
} else {
|
|
437
|
+
project_root.join(p)
|
|
438
|
+
};
|
|
439
|
+
let resolved = resolved.canonicalize().unwrap_or(resolved);
|
|
440
|
+
resolved.display().to_string().replace('\\', "/")
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
fn json_to_cargo_inline_value(
|
|
444
|
+
v: &serde_json::Value,
|
|
445
|
+
project_root: &Path,
|
|
446
|
+
) -> Result<String, String> {
|
|
447
|
+
match v {
|
|
448
|
+
serde_json::Value::String(s) => Ok(format!("{:?}", s.as_str())),
|
|
449
|
+
serde_json::Value::Bool(b) => Ok(b.to_string()),
|
|
450
|
+
serde_json::Value::Number(n) => Ok(n.to_string()),
|
|
451
|
+
serde_json::Value::Array(arr) => {
|
|
452
|
+
let mut inner = Vec::new();
|
|
453
|
+
for item in arr {
|
|
454
|
+
inner.push(json_to_cargo_inline_value(item, project_root)?);
|
|
455
|
+
}
|
|
456
|
+
Ok(format!("[{}]", inner.join(", ")))
|
|
457
|
+
}
|
|
458
|
+
serde_json::Value::Object(map) => {
|
|
459
|
+
let mut parts = Vec::new();
|
|
460
|
+
for (k, v) in map {
|
|
461
|
+
let rhs = if k == "path" && v.as_str().is_some() {
|
|
462
|
+
let s = v.as_str().unwrap();
|
|
463
|
+
format!("{:?}", resolve_cargo_path_for_toml(project_root, s))
|
|
464
|
+
} else {
|
|
465
|
+
json_to_cargo_inline_value(v, project_root)?
|
|
466
|
+
};
|
|
467
|
+
parts.push(format!("{} = {}", k, rhs));
|
|
468
|
+
}
|
|
469
|
+
Ok(format!("{{ {} }}", parts.join(", ")))
|
|
470
|
+
}
|
|
471
|
+
serde_json::Value::Null => Err("null is not valid in a Cargo dependency value".to_string()),
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/// Serialize `tish.rustDependencies` from project `package.json` into Cargo.toml `[dependencies]` lines.
|
|
476
|
+
/// Relative `path = "…"` entries in inline tables are resolved against `project_root` so the temp build crate can find them.
|
|
477
|
+
pub fn format_rust_dependencies_toml(
|
|
478
|
+
tish: &serde_json::Value,
|
|
479
|
+
project_root: &Path,
|
|
480
|
+
) -> Result<String, String> {
|
|
481
|
+
let Some(obj) = tish.get("rustDependencies").and_then(|v| v.as_object()) else {
|
|
482
|
+
return Ok(String::new());
|
|
483
|
+
};
|
|
484
|
+
let mut out = String::new();
|
|
485
|
+
for (name, val) in obj {
|
|
486
|
+
match val {
|
|
487
|
+
serde_json::Value::String(_) | serde_json::Value::Object(_) => {
|
|
488
|
+
out.push_str(&format!(
|
|
489
|
+
"{} = {}\n",
|
|
490
|
+
name,
|
|
491
|
+
json_to_cargo_inline_value(val, project_root)?
|
|
492
|
+
));
|
|
493
|
+
}
|
|
494
|
+
_ => {
|
|
495
|
+
return Err(format!(
|
|
496
|
+
"tish.rustDependencies.{} must be a string (version) or object (inline table)",
|
|
497
|
+
name
|
|
498
|
+
));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
Ok(out)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/// Map a Tish export name to a Rust identifier (e.g. `readFile` → `read_file`) for shim crate symbols.
|
|
506
|
+
pub fn export_name_to_rust_ident(export_name: &str) -> String {
|
|
507
|
+
let mut out = String::new();
|
|
508
|
+
for (i, c) in export_name.chars().enumerate() {
|
|
509
|
+
if c.is_uppercase() && i > 0 {
|
|
510
|
+
out.push('_');
|
|
511
|
+
}
|
|
512
|
+
for lower in c.to_lowercase() {
|
|
513
|
+
out.push(lower);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if out.is_empty() {
|
|
517
|
+
"native_export".to_string()
|
|
518
|
+
} else {
|
|
519
|
+
out
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/// Collect `(spec, export_name)` for every non-builtin native import in the program.
|
|
524
|
+
pub fn infer_native_module_exports(program: &Program) -> HashMap<String, HashSet<String>> {
|
|
525
|
+
let mut map: HashMap<String, HashSet<String>> = HashMap::new();
|
|
526
|
+
for stmt in &program.statements {
|
|
527
|
+
match stmt {
|
|
528
|
+
Statement::VarDecl {
|
|
529
|
+
init:
|
|
530
|
+
Some(Expr::NativeModuleLoad {
|
|
531
|
+
spec, export_name, ..
|
|
532
|
+
}),
|
|
533
|
+
..
|
|
534
|
+
} => {
|
|
535
|
+
let s = spec.as_ref();
|
|
536
|
+
if is_builtin_native_spec(s) {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
map.entry(s.to_string())
|
|
540
|
+
.or_default()
|
|
541
|
+
.insert(export_name.to_string());
|
|
542
|
+
}
|
|
543
|
+
Statement::Import {
|
|
544
|
+
specifiers, from, ..
|
|
545
|
+
} if is_native_import(from.as_ref()) => {
|
|
546
|
+
let spec =
|
|
547
|
+
normalize_builtin_spec(from.as_ref()).unwrap_or_else(|| from.to_string());
|
|
548
|
+
if is_builtin_native_spec(&spec) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
for sp in specifiers {
|
|
552
|
+
if let ImportSpecifier::Named { name, .. } = sp {
|
|
553
|
+
map.entry(spec.clone())
|
|
554
|
+
.or_default()
|
|
555
|
+
.insert(name.to_string());
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
_ => {}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
map
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/// Emit `generated_native.rs` for [`NativeModuleInit::Generated`] modules.
|
|
566
|
+
pub fn generate_native_wrapper_rs(
|
|
567
|
+
modules: &[ResolvedNativeModule],
|
|
568
|
+
inferred: &HashMap<String, HashSet<String>>,
|
|
569
|
+
init_by_spec: &HashMap<String, NativeModuleInit>,
|
|
570
|
+
) -> String {
|
|
571
|
+
let mut file = String::from(
|
|
572
|
+
"//! Generated by `tish build` — do not edit.\n\
|
|
573
|
+
use std::cell::RefCell;\n\
|
|
574
|
+
use std::rc::Rc;\n\
|
|
575
|
+
use std::sync::Arc;\n\
|
|
576
|
+
use tishlang_runtime::{ObjectMap, Value, VmRef};\n\n",
|
|
577
|
+
);
|
|
578
|
+
let mut any = false;
|
|
579
|
+
for m in modules {
|
|
580
|
+
let Some(NativeModuleInit::Generated {
|
|
581
|
+
shim_crate,
|
|
582
|
+
export_fn,
|
|
583
|
+
}) = init_by_spec.get(&m.spec)
|
|
584
|
+
else {
|
|
585
|
+
continue;
|
|
586
|
+
};
|
|
587
|
+
let Some(names) = inferred.get(&m.spec) else {
|
|
588
|
+
continue;
|
|
589
|
+
};
|
|
590
|
+
if names.is_empty() {
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
any = true;
|
|
594
|
+
let mut keys: Vec<_> = names.iter().cloned().collect();
|
|
595
|
+
keys.sort();
|
|
596
|
+
file.push_str(&format!("pub fn {}() -> Value {{\n", export_fn));
|
|
597
|
+
file.push_str(" let mut m = ObjectMap::default();\n");
|
|
598
|
+
for export_name in keys {
|
|
599
|
+
let rust_fn = export_name_to_rust_ident(&export_name);
|
|
600
|
+
let key_lit = format!("{:?}", export_name);
|
|
601
|
+
file.push_str(&format!(
|
|
602
|
+
" m.insert(Arc::from({}), Value::native(|args: &[Value]| {{\n {}::{}(args)\n }}));\n",
|
|
603
|
+
key_lit, shim_crate, rust_fn
|
|
604
|
+
));
|
|
605
|
+
}
|
|
606
|
+
file.push_str(" Value::Object(VmRef::new(m))\n}\n\n");
|
|
607
|
+
}
|
|
608
|
+
if !any {
|
|
609
|
+
return String::new();
|
|
610
|
+
}
|
|
611
|
+
file
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/// Combine project `package.json`, inferred exports, and resolved native modules into build artifacts.
|
|
615
|
+
pub fn compute_native_build_artifacts(
|
|
616
|
+
program: &Program,
|
|
617
|
+
project_root: &Path,
|
|
618
|
+
native_modules: &[ResolvedNativeModule],
|
|
619
|
+
) -> Result<NativeBuildArtifacts, String> {
|
|
620
|
+
let tish = read_project_tish_config(project_root);
|
|
621
|
+
let rust_dependencies_toml = format_rust_dependencies_toml(&tish, project_root)?;
|
|
622
|
+
let inferred = infer_native_module_exports(program);
|
|
623
|
+
let gen_tish = tish
|
|
624
|
+
.get("generateNativeWrapper")
|
|
625
|
+
.and_then(|v| v.as_bool())
|
|
626
|
+
.unwrap_or(false);
|
|
627
|
+
|
|
628
|
+
let mut native_init: HashMap<String, NativeModuleInit> = HashMap::new();
|
|
629
|
+
for m in native_modules {
|
|
630
|
+
let use_gen = if is_cargo_native_spec(&m.spec) {
|
|
631
|
+
inferred
|
|
632
|
+
.get(&m.spec)
|
|
633
|
+
.map(|s| !s.is_empty())
|
|
634
|
+
.unwrap_or(false)
|
|
635
|
+
} else {
|
|
636
|
+
gen_tish
|
|
637
|
+
&& inferred
|
|
638
|
+
.get(&m.spec)
|
|
639
|
+
.map(|s| !s.is_empty())
|
|
640
|
+
.unwrap_or(false)
|
|
641
|
+
};
|
|
642
|
+
let init = if use_gen {
|
|
643
|
+
NativeModuleInit::Generated {
|
|
644
|
+
shim_crate: m.crate_name.clone(),
|
|
645
|
+
export_fn: m.export_fn.clone(),
|
|
646
|
+
}
|
|
647
|
+
} else {
|
|
648
|
+
NativeModuleInit::Legacy {
|
|
649
|
+
crate_name: m.crate_name.clone(),
|
|
650
|
+
export_fn: m.export_fn.clone(),
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
native_init.insert(m.spec.clone(), init);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
let generated_native_rs = {
|
|
657
|
+
let s = generate_native_wrapper_rs(native_modules, &inferred, &native_init);
|
|
658
|
+
if s.trim().is_empty() {
|
|
659
|
+
None
|
|
660
|
+
} else {
|
|
661
|
+
Some(s)
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
Ok(NativeBuildArtifacts {
|
|
666
|
+
rust_dependencies_toml,
|
|
667
|
+
generated_native_rs,
|
|
668
|
+
native_init,
|
|
669
|
+
})
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
fn find_package_dir(package_name: &str, project_root: &Path) -> Result<PathBuf, String> {
|
|
673
|
+
let mut search = project_root.to_path_buf();
|
|
674
|
+
loop {
|
|
675
|
+
let node_mod = search.join("node_modules").join(package_name);
|
|
676
|
+
if node_mod.join("package.json").exists()
|
|
677
|
+
&& read_package_name(&node_mod.join("package.json")) == Some(package_name.to_string())
|
|
678
|
+
{
|
|
679
|
+
return Ok(node_mod);
|
|
680
|
+
}
|
|
681
|
+
let sibling = search.join(package_name);
|
|
682
|
+
if sibling.join("package.json").exists()
|
|
683
|
+
&& read_package_name(&sibling.join("package.json")) == Some(package_name.to_string())
|
|
684
|
+
{
|
|
685
|
+
return Ok(sibling);
|
|
686
|
+
}
|
|
687
|
+
if search.join("package.json").exists()
|
|
688
|
+
&& read_package_name(&search.join("package.json")) == Some(package_name.to_string())
|
|
689
|
+
{
|
|
690
|
+
return Ok(search);
|
|
691
|
+
}
|
|
692
|
+
if let Some(parent) = search.parent() {
|
|
693
|
+
search = parent.to_path_buf();
|
|
694
|
+
} else {
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
Err(format!(
|
|
699
|
+
"Native module {} not found. Add it as a dependency or place it in node_modules/ or as a sibling directory.",
|
|
700
|
+
package_name
|
|
701
|
+
))
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
fn read_package_name(pkg_path: &Path) -> Option<String> {
|
|
705
|
+
let content = std::fs::read_to_string(pkg_path).ok()?;
|
|
706
|
+
let json: serde_json::Value = serde_json::from_str(&content).ok()?;
|
|
707
|
+
json.get("name").and_then(|v| v.as_str()).map(String::from)
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
fn stmt_native_specs(stmt: &Statement) -> Vec<String> {
|
|
711
|
+
match stmt {
|
|
712
|
+
Statement::VarDecl {
|
|
713
|
+
init: Some(Expr::NativeModuleLoad { spec, .. }),
|
|
714
|
+
..
|
|
715
|
+
} => vec![spec.to_string()],
|
|
716
|
+
Statement::Import { from, .. } if is_native_import(from.as_ref()) => {
|
|
717
|
+
vec![normalize_builtin_spec(from.as_ref()).unwrap_or_else(|| from.to_string())]
|
|
718
|
+
}
|
|
719
|
+
_ => vec![],
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/// Extract Cargo feature names from native imports in a merged program.
|
|
724
|
+
/// Used to enable tishlang_runtime features based on `import { x } from 'tish:egui'` etc.
|
|
725
|
+
pub fn extract_native_import_features(program: &Program) -> Vec<String> {
|
|
726
|
+
let mut features = std::collections::HashSet::new();
|
|
727
|
+
for stmt in &program.statements {
|
|
728
|
+
for spec in stmt_native_specs(stmt) {
|
|
729
|
+
if let Some(f) = native_spec_to_feature(spec.as_ref()) {
|
|
730
|
+
features.insert(f);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
features.into_iter().collect()
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/// Returns true if the merged program contains native imports (tish:*, @scope/pkg).
|
|
738
|
+
pub fn has_native_imports(program: &Program) -> bool {
|
|
739
|
+
program
|
|
740
|
+
.statements
|
|
741
|
+
.iter()
|
|
742
|
+
.any(|stmt| !stmt_native_specs(stmt).is_empty())
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/// Returns true if the merged program contains external native imports (not built-in tish:fs/http/process).
|
|
746
|
+
/// Cranelift/LLVM reject these; bytecode VM supports built-ins only.
|
|
747
|
+
pub fn has_external_native_imports(program: &Program) -> bool {
|
|
748
|
+
for stmt in &program.statements {
|
|
749
|
+
for spec in stmt_native_specs(stmt) {
|
|
750
|
+
if !is_builtin_native_spec(spec.as_ref()) {
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
false
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/// A resolved module: path and its parsed program.
|
|
759
|
+
#[derive(Debug, Clone)]
|
|
760
|
+
pub struct ResolvedModule {
|
|
761
|
+
pub path: PathBuf,
|
|
762
|
+
pub program: Program,
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/// Resolve all modules starting from the entry file. Returns modules in dependency order
|
|
766
|
+
/// (dependencies first, then dependents). Entry module is last.
|
|
767
|
+
pub fn resolve_project(
|
|
768
|
+
entry_path: &Path,
|
|
769
|
+
project_root: Option<&Path>,
|
|
770
|
+
) -> Result<Vec<ResolvedModule>, String> {
|
|
771
|
+
let project_root =
|
|
772
|
+
project_root.unwrap_or_else(|| entry_path.parent().unwrap_or(Path::new(".")));
|
|
773
|
+
let entry_canon = entry_path
|
|
774
|
+
.canonicalize()
|
|
775
|
+
.map_err(|e| format!("Cannot canonicalize entry {}: {}", entry_path.display(), e))?;
|
|
776
|
+
let root_canon = project_root.canonicalize().map_err(|e| {
|
|
777
|
+
format!(
|
|
778
|
+
"Cannot canonicalize project root {}: {}",
|
|
779
|
+
project_root.display(),
|
|
780
|
+
e
|
|
781
|
+
)
|
|
782
|
+
})?;
|
|
783
|
+
|
|
784
|
+
let mut visited = HashSet::new();
|
|
785
|
+
let mut path_to_module: HashMap<PathBuf, Program> = HashMap::new();
|
|
786
|
+
let mut load_order: Vec<PathBuf> = Vec::new();
|
|
787
|
+
|
|
788
|
+
load_module_recursive(
|
|
789
|
+
&entry_canon,
|
|
790
|
+
&root_canon,
|
|
791
|
+
&mut visited,
|
|
792
|
+
&mut path_to_module,
|
|
793
|
+
&mut load_order,
|
|
794
|
+
)?;
|
|
795
|
+
|
|
796
|
+
Ok(load_order
|
|
797
|
+
.into_iter()
|
|
798
|
+
.map(|p| {
|
|
799
|
+
let program = path_to_module.remove(&p).unwrap();
|
|
800
|
+
ResolvedModule { path: p, program }
|
|
801
|
+
})
|
|
802
|
+
.collect())
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/// Resolve modules when the entry program is read from stdin (`tish run -`).
|
|
806
|
+
/// Relative file imports resolve from `project_root` (typically [`std::env::current_dir()`]).
|
|
807
|
+
/// The synthetic entry path `<stdin>` is not a real file; dependencies load from disk as usual.
|
|
808
|
+
pub fn resolve_project_from_stdin(
|
|
809
|
+
source: &str,
|
|
810
|
+
project_root: &Path,
|
|
811
|
+
) -> Result<Vec<ResolvedModule>, String> {
|
|
812
|
+
let root_canon = project_root.canonicalize().map_err(|e| {
|
|
813
|
+
format!(
|
|
814
|
+
"Cannot canonicalize project root {}: {}",
|
|
815
|
+
project_root.display(),
|
|
816
|
+
e
|
|
817
|
+
)
|
|
818
|
+
})?;
|
|
819
|
+
|
|
820
|
+
let stdin_path = root_canon.join("<stdin>");
|
|
821
|
+
let program =
|
|
822
|
+
tishlang_parser::parse(source).map_err(|e| format!("Parse error (stdin): {}", e))?;
|
|
823
|
+
|
|
824
|
+
let mut visited = HashSet::new();
|
|
825
|
+
let mut path_to_module: HashMap<PathBuf, Program> = HashMap::new();
|
|
826
|
+
let mut load_order: Vec<PathBuf> = Vec::new();
|
|
827
|
+
|
|
828
|
+
let from_dir = stdin_path.parent().unwrap_or_else(|| Path::new("."));
|
|
829
|
+
|
|
830
|
+
for stmt in &program.statements {
|
|
831
|
+
if let Statement::Import { from, .. } = stmt {
|
|
832
|
+
if is_native_import(from.as_ref()) {
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
let dep_path = resolve_import_path(from.as_ref(), from_dir, &root_canon)?;
|
|
836
|
+
if !path_to_module.contains_key(&dep_path) {
|
|
837
|
+
load_module_recursive(
|
|
838
|
+
&dep_path,
|
|
839
|
+
&root_canon,
|
|
840
|
+
&mut visited,
|
|
841
|
+
&mut path_to_module,
|
|
842
|
+
&mut load_order,
|
|
843
|
+
)?;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
path_to_module.insert(stdin_path.clone(), program);
|
|
849
|
+
load_order.push(stdin_path);
|
|
850
|
+
|
|
851
|
+
Ok(load_order
|
|
852
|
+
.into_iter()
|
|
853
|
+
.map(|p| {
|
|
854
|
+
let program = path_to_module.remove(&p).unwrap();
|
|
855
|
+
ResolvedModule { path: p, program }
|
|
856
|
+
})
|
|
857
|
+
.collect())
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
fn load_module_recursive(
|
|
861
|
+
module_path: &Path,
|
|
862
|
+
project_root: &Path,
|
|
863
|
+
visited: &mut HashSet<PathBuf>,
|
|
864
|
+
path_to_module: &mut HashMap<PathBuf, Program>,
|
|
865
|
+
load_order: &mut Vec<PathBuf>,
|
|
866
|
+
) -> Result<(), String> {
|
|
867
|
+
let canonical = module_path
|
|
868
|
+
.canonicalize()
|
|
869
|
+
.map_err(|e| format!("Cannot read {}: {}", module_path.display(), e))?;
|
|
870
|
+
|
|
871
|
+
if visited.contains(&canonical) {
|
|
872
|
+
return Ok(());
|
|
873
|
+
}
|
|
874
|
+
visited.insert(canonical.clone());
|
|
875
|
+
|
|
876
|
+
let source = std::fs::read_to_string(&canonical)
|
|
877
|
+
.map_err(|e| format!("Cannot read {}: {}", canonical.display(), e))?;
|
|
878
|
+
let program = tishlang_parser::parse(&source)
|
|
879
|
+
.map_err(|e| format!("Parse error in {}: {}", canonical.display(), e))?;
|
|
880
|
+
|
|
881
|
+
// Collect imports and load dependencies first (skip native imports)
|
|
882
|
+
let dir = canonical.parent().unwrap_or(Path::new("."));
|
|
883
|
+
for stmt in &program.statements {
|
|
884
|
+
if let Statement::Import { from, .. } = stmt {
|
|
885
|
+
if is_native_import(from.as_ref()) {
|
|
886
|
+
continue; // Native imports don't load files
|
|
887
|
+
}
|
|
888
|
+
let dep_path = resolve_import_path(from.as_ref(), dir, project_root)?;
|
|
889
|
+
if !path_to_module.contains_key(&dep_path) {
|
|
890
|
+
load_module_recursive(
|
|
891
|
+
&dep_path,
|
|
892
|
+
project_root,
|
|
893
|
+
visited,
|
|
894
|
+
path_to_module,
|
|
895
|
+
load_order,
|
|
896
|
+
)?;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
path_to_module.insert(canonical.clone(), program);
|
|
902
|
+
load_order.push(canonical);
|
|
903
|
+
Ok(())
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/// Returns true for native module imports that don't resolve to files.
|
|
907
|
+
/// - fs, http, timers, process, ws (Node-compatible aliases for tish:*)
|
|
908
|
+
/// - tish:egui, tish:polars, etc.
|
|
909
|
+
/// - cargo:… (Cargo `rustDependencies` + generated wrapper; Rust native backend)
|
|
910
|
+
///
|
|
911
|
+
/// Scoped npm packages (`@scope/pkg`) are merged as Tish source unless imported via `tish:…`.
|
|
912
|
+
pub fn is_native_import(spec: &str) -> bool {
|
|
913
|
+
spec.starts_with("tish:")
|
|
914
|
+
|| spec.starts_with("cargo:")
|
|
915
|
+
|| matches!(spec, "fs" | "http" | "timers" | "process" | "ws")
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/// Map native spec to Cargo feature name for built-in tish:* modules.
|
|
919
|
+
pub fn native_spec_to_feature(spec: &str) -> Option<String> {
|
|
920
|
+
let canonical = normalize_builtin_spec(spec)?;
|
|
921
|
+
canonical.strip_prefix("tish:").map(|s| s.to_string())
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/// Resolve `package.json` at `pkg_root` to the package's main `.tish` entry, if `name` matches `spec`.
|
|
925
|
+
fn resolve_package_root_to_entry(pkg_root: &Path, spec: &str) -> Option<PathBuf> {
|
|
926
|
+
let pkg_json = pkg_root.join("package.json");
|
|
927
|
+
if !pkg_json.exists() {
|
|
928
|
+
return None;
|
|
929
|
+
}
|
|
930
|
+
if read_package_name(&pkg_json).as_deref() != Some(spec) {
|
|
931
|
+
return None;
|
|
932
|
+
}
|
|
933
|
+
let content = std::fs::read_to_string(&pkg_json).ok()?;
|
|
934
|
+
let json: serde_json::Value = serde_json::from_str(&content).ok()?;
|
|
935
|
+
let entry = json
|
|
936
|
+
.get("tish")
|
|
937
|
+
.and_then(|t| t.get("module"))
|
|
938
|
+
.and_then(|m| m.as_str())
|
|
939
|
+
.or_else(|| json.get("main").and_then(|m| m.as_str()))
|
|
940
|
+
.unwrap_or("index.tish");
|
|
941
|
+
let entry_clean = entry.trim_start_matches("./");
|
|
942
|
+
let resolved = pkg_root.join(entry_clean);
|
|
943
|
+
if !resolved.exists() {
|
|
944
|
+
return None;
|
|
945
|
+
}
|
|
946
|
+
match resolved.canonicalize() {
|
|
947
|
+
Ok(p) => Some(p),
|
|
948
|
+
Err(_) => Some(resolved),
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/// Resolve a bare specifier (e.g. "lattish") to the package entry `.tish` file.
|
|
953
|
+
///
|
|
954
|
+
/// Walks upward from `from_dir` and, at each level, checks (same order as native [`find_package_dir`]):
|
|
955
|
+
/// - `node_modules/<spec>/`
|
|
956
|
+
/// - `<spec>/` as a sibling directory (monorepo: `…/tish/tish-candle` next to `…/tish/tish-hub`)
|
|
957
|
+
/// - the search directory itself if its `package.json` name matches `spec`
|
|
958
|
+
pub fn resolve_bare_spec(spec: &str, from_dir: &Path, _project_root: &Path) -> Option<PathBuf> {
|
|
959
|
+
let mut search = from_dir.to_path_buf();
|
|
960
|
+
loop {
|
|
961
|
+
if let Some(p) =
|
|
962
|
+
resolve_package_root_to_entry(&search.join("node_modules").join(spec), spec)
|
|
963
|
+
{
|
|
964
|
+
return Some(p);
|
|
965
|
+
}
|
|
966
|
+
if let Some(p) = resolve_package_root_to_entry(&search.join(spec), spec) {
|
|
967
|
+
return Some(p);
|
|
968
|
+
}
|
|
969
|
+
if let Some(p) = resolve_package_root_to_entry(&search, spec) {
|
|
970
|
+
return Some(p);
|
|
971
|
+
}
|
|
972
|
+
if let Some(parent) = search.parent() {
|
|
973
|
+
if parent == search {
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
search = parent.to_path_buf();
|
|
977
|
+
} else {
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
None
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/// Resolve an import specifier (e.g. "./foo.tish", "../lib/utils", "lattish") to an absolute path.
|
|
985
|
+
fn resolve_import_path(
|
|
986
|
+
spec: &str,
|
|
987
|
+
from_dir: &Path,
|
|
988
|
+
project_root: &Path,
|
|
989
|
+
) -> Result<PathBuf, String> {
|
|
990
|
+
if is_native_import(spec) {
|
|
991
|
+
return Err(format!(
|
|
992
|
+
"resolve_import_path called for native import (use merge_modules native branch): {}",
|
|
993
|
+
spec
|
|
994
|
+
));
|
|
995
|
+
}
|
|
996
|
+
if !spec.starts_with("./") && !spec.starts_with("../") {
|
|
997
|
+
if let Some(path) = resolve_bare_spec(spec, from_dir, project_root) {
|
|
998
|
+
return Ok(path);
|
|
999
|
+
}
|
|
1000
|
+
return Err(format!(
|
|
1001
|
+
"Package '{}' not found. Install with `npm install {}`, or place the package under node_modules/ or as a sibling directory (same layout as native `find_package_dir`).",
|
|
1002
|
+
spec, spec
|
|
1003
|
+
));
|
|
1004
|
+
}
|
|
1005
|
+
let base = from_dir.join(spec);
|
|
1006
|
+
// Try with .tish extension if the path has no extension
|
|
1007
|
+
let path = if base.extension().is_none() {
|
|
1008
|
+
let with_ext = base.with_extension("tish");
|
|
1009
|
+
if with_ext.exists() {
|
|
1010
|
+
with_ext
|
|
1011
|
+
} else {
|
|
1012
|
+
base
|
|
1013
|
+
}
|
|
1014
|
+
} else {
|
|
1015
|
+
base
|
|
1016
|
+
};
|
|
1017
|
+
path.canonicalize().map_err(|e| {
|
|
1018
|
+
format!(
|
|
1019
|
+
"Cannot resolve import '{}' from {}: {}",
|
|
1020
|
+
spec,
|
|
1021
|
+
from_dir.display(),
|
|
1022
|
+
e
|
|
1023
|
+
)
|
|
1024
|
+
})
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/// Check for cyclic imports. Returns Err if a cycle is detected.
|
|
1028
|
+
pub fn detect_cycles(modules: &[ResolvedModule]) -> Result<(), String> {
|
|
1029
|
+
let path_to_idx: HashMap<_, _> = modules
|
|
1030
|
+
.iter()
|
|
1031
|
+
.enumerate()
|
|
1032
|
+
.map(|(i, m)| (m.path.clone(), i))
|
|
1033
|
+
.collect();
|
|
1034
|
+
|
|
1035
|
+
for (idx, module) in modules.iter().enumerate() {
|
|
1036
|
+
let dir = module.path.parent().unwrap_or(Path::new("."));
|
|
1037
|
+
let mut stack = vec![idx];
|
|
1038
|
+
if has_cycle_from(
|
|
1039
|
+
dir,
|
|
1040
|
+
&module.program,
|
|
1041
|
+
&path_to_idx,
|
|
1042
|
+
modules,
|
|
1043
|
+
&mut stack,
|
|
1044
|
+
&mut HashSet::new(),
|
|
1045
|
+
)? {
|
|
1046
|
+
let path_names: Vec<_> = stack
|
|
1047
|
+
.iter()
|
|
1048
|
+
.map(|&i| modules[i].path.display().to_string())
|
|
1049
|
+
.collect();
|
|
1050
|
+
return Err(format!(
|
|
1051
|
+
"Circular import detected: {}",
|
|
1052
|
+
path_names.join(" -> ")
|
|
1053
|
+
));
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
Ok(())
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
fn has_cycle_from(
|
|
1060
|
+
from_dir: &Path,
|
|
1061
|
+
program: &Program,
|
|
1062
|
+
path_to_idx: &HashMap<PathBuf, usize>,
|
|
1063
|
+
modules: &[ResolvedModule],
|
|
1064
|
+
stack: &mut Vec<usize>,
|
|
1065
|
+
visiting: &mut HashSet<usize>,
|
|
1066
|
+
) -> Result<bool, String> {
|
|
1067
|
+
for stmt in &program.statements {
|
|
1068
|
+
if let Statement::Import { from, .. } = stmt {
|
|
1069
|
+
if is_native_import(from.as_ref()) {
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
let dep_path = resolve_import_path(from.as_ref(), from_dir, Path::new("."))?;
|
|
1073
|
+
if let Some(&dep_idx) = path_to_idx.get(&dep_path) {
|
|
1074
|
+
if stack.contains(&dep_idx) {
|
|
1075
|
+
stack.push(dep_idx);
|
|
1076
|
+
return Ok(true);
|
|
1077
|
+
}
|
|
1078
|
+
if !visiting.contains(&dep_idx) {
|
|
1079
|
+
visiting.insert(dep_idx);
|
|
1080
|
+
stack.push(dep_idx);
|
|
1081
|
+
let dep = &modules[dep_idx];
|
|
1082
|
+
let dep_dir = dep.path.parent().unwrap_or(Path::new("."));
|
|
1083
|
+
if has_cycle_from(dep_dir, &dep.program, path_to_idx, modules, stack, visiting)?
|
|
1084
|
+
{
|
|
1085
|
+
return Ok(true);
|
|
1086
|
+
}
|
|
1087
|
+
stack.pop();
|
|
1088
|
+
visiting.remove(&dep_idx);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
Ok(false)
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/// Result of [`merge_modules`]: merged AST plus, per top-level statement, the originating `.tish` file.
|
|
1097
|
+
#[derive(Debug)]
|
|
1098
|
+
pub struct MergedProgram {
|
|
1099
|
+
pub program: Program,
|
|
1100
|
+
pub statement_sources: Vec<PathBuf>,
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
fn merge_push(
|
|
1104
|
+
statements: &mut Vec<Statement>,
|
|
1105
|
+
statement_sources: &mut Vec<PathBuf>,
|
|
1106
|
+
stmt: Statement,
|
|
1107
|
+
source: PathBuf,
|
|
1108
|
+
) {
|
|
1109
|
+
statements.push(stmt);
|
|
1110
|
+
statement_sources.push(source);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/// Merge all resolved modules into a single program. Dependencies are emitted first.
|
|
1114
|
+
/// Import statements are rewritten as bindings from already-emitted dep exports.
|
|
1115
|
+
/// Export statements are unwrapped (the inner declaration is emitted).
|
|
1116
|
+
pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<MergedProgram, String> {
|
|
1117
|
+
let path_to_idx: HashMap<PathBuf, usize> = modules
|
|
1118
|
+
.iter()
|
|
1119
|
+
.enumerate()
|
|
1120
|
+
.map(|(i, m)| (m.path.canonicalize().unwrap_or(m.path.clone()), i))
|
|
1121
|
+
.collect();
|
|
1122
|
+
|
|
1123
|
+
let mut module_exports: Vec<HashMap<String, String>> = vec![HashMap::new(); modules.len()];
|
|
1124
|
+
for (idx, module) in modules.iter().enumerate() {
|
|
1125
|
+
for stmt in &module.program.statements {
|
|
1126
|
+
if let Statement::Export { declaration, .. } = stmt {
|
|
1127
|
+
match declaration.as_ref() {
|
|
1128
|
+
ExportDeclaration::Named(s) => {
|
|
1129
|
+
let name = match s.as_ref() {
|
|
1130
|
+
Statement::VarDecl { name, .. } | Statement::FunDecl { name, .. } => {
|
|
1131
|
+
name.to_string()
|
|
1132
|
+
}
|
|
1133
|
+
_ => continue,
|
|
1134
|
+
};
|
|
1135
|
+
module_exports[idx].insert(name.clone(), name);
|
|
1136
|
+
}
|
|
1137
|
+
ExportDeclaration::Default(_) => {
|
|
1138
|
+
let default_name = format!("__default_{}", idx);
|
|
1139
|
+
module_exports[idx].insert("default".to_string(), default_name);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
let mut statements = Vec::new();
|
|
1147
|
+
let mut statement_sources = Vec::new();
|
|
1148
|
+
for (idx, module) in modules.iter().enumerate() {
|
|
1149
|
+
let src_path = module.path.clone();
|
|
1150
|
+
let dir = module.path.parent().unwrap_or(Path::new("."));
|
|
1151
|
+
for stmt in &module.program.statements {
|
|
1152
|
+
match stmt {
|
|
1153
|
+
Statement::Import {
|
|
1154
|
+
specifiers,
|
|
1155
|
+
from,
|
|
1156
|
+
span,
|
|
1157
|
+
} => {
|
|
1158
|
+
if is_native_import(from.as_ref()) {
|
|
1159
|
+
// Normalize fs/http/process -> tish:fs etc. for Node compatibility
|
|
1160
|
+
let canonical_spec = normalize_builtin_spec(from.as_ref())
|
|
1161
|
+
.unwrap_or_else(|| from.to_string());
|
|
1162
|
+
// Emit VarDecl with NativeModuleLoad for each specifier
|
|
1163
|
+
for spec in specifiers {
|
|
1164
|
+
match spec {
|
|
1165
|
+
ImportSpecifier::Named {
|
|
1166
|
+
name,
|
|
1167
|
+
name_span,
|
|
1168
|
+
alias,
|
|
1169
|
+
alias_span,
|
|
1170
|
+
} => {
|
|
1171
|
+
let bind = alias.as_deref().unwrap_or(name.as_ref());
|
|
1172
|
+
let decl_name_span = alias_span.as_ref().unwrap_or(name_span);
|
|
1173
|
+
let init = Expr::NativeModuleLoad {
|
|
1174
|
+
spec: Arc::from(canonical_spec.clone()),
|
|
1175
|
+
export_name: name.clone(),
|
|
1176
|
+
span: *span,
|
|
1177
|
+
};
|
|
1178
|
+
merge_push(
|
|
1179
|
+
&mut statements,
|
|
1180
|
+
&mut statement_sources,
|
|
1181
|
+
Statement::VarDecl {
|
|
1182
|
+
name: Arc::from(bind),
|
|
1183
|
+
name_span: *decl_name_span,
|
|
1184
|
+
mutable: false,
|
|
1185
|
+
type_ann: None,
|
|
1186
|
+
init: Some(init),
|
|
1187
|
+
span: *span,
|
|
1188
|
+
},
|
|
1189
|
+
src_path.clone(),
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
ImportSpecifier::Namespace { name, .. } => {
|
|
1193
|
+
return Err(format!(
|
|
1194
|
+
"Namespace import (* as {}) not supported for native module '{}'",
|
|
1195
|
+
name.as_ref(),
|
|
1196
|
+
from.as_ref()
|
|
1197
|
+
));
|
|
1198
|
+
}
|
|
1199
|
+
ImportSpecifier::Default { name, .. } => {
|
|
1200
|
+
return Err(format!(
|
|
1201
|
+
"Default import '{}' not supported for native module '{}'. Use named import, e.g. import {{ egui }} from '{}'",
|
|
1202
|
+
name.as_ref(),
|
|
1203
|
+
from.as_ref(),
|
|
1204
|
+
from.as_ref()
|
|
1205
|
+
));
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
let dep_path = resolve_import_path(from.as_ref(), dir, Path::new("."))?;
|
|
1212
|
+
let dep_path = dep_path.canonicalize().unwrap_or(dep_path);
|
|
1213
|
+
let dep_idx = *path_to_idx
|
|
1214
|
+
.get(&dep_path)
|
|
1215
|
+
.ok_or_else(|| format!("Resolved import '{}' not in module list", from))?;
|
|
1216
|
+
let dep_exports = &module_exports[dep_idx];
|
|
1217
|
+
for spec in specifiers {
|
|
1218
|
+
match spec {
|
|
1219
|
+
ImportSpecifier::Named {
|
|
1220
|
+
name,
|
|
1221
|
+
name_span,
|
|
1222
|
+
alias,
|
|
1223
|
+
alias_span,
|
|
1224
|
+
} => {
|
|
1225
|
+
let source = dep_exports
|
|
1226
|
+
.get(name.as_ref())
|
|
1227
|
+
.cloned()
|
|
1228
|
+
.unwrap_or_else(|| name.to_string());
|
|
1229
|
+
let bind = alias.as_deref().unwrap_or(name.as_ref());
|
|
1230
|
+
if bind != source {
|
|
1231
|
+
let decl_name_span = alias_span.as_ref().unwrap_or(name_span);
|
|
1232
|
+
merge_push(
|
|
1233
|
+
&mut statements,
|
|
1234
|
+
&mut statement_sources,
|
|
1235
|
+
Statement::VarDecl {
|
|
1236
|
+
name: Arc::from(bind),
|
|
1237
|
+
name_span: *decl_name_span,
|
|
1238
|
+
mutable: false,
|
|
1239
|
+
type_ann: None,
|
|
1240
|
+
init: Some(Expr::Ident {
|
|
1241
|
+
name: Arc::from(source),
|
|
1242
|
+
span: *span,
|
|
1243
|
+
}),
|
|
1244
|
+
span: *span,
|
|
1245
|
+
},
|
|
1246
|
+
src_path.clone(),
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
ImportSpecifier::Namespace { name, name_span } => {
|
|
1251
|
+
let mut props = Vec::new();
|
|
1252
|
+
for (k, v) in dep_exports {
|
|
1253
|
+
props.push(tishlang_ast::ObjectProp::KeyValue(
|
|
1254
|
+
Arc::from(k.clone()),
|
|
1255
|
+
Expr::Ident {
|
|
1256
|
+
name: Arc::from(v.clone()),
|
|
1257
|
+
span: *span,
|
|
1258
|
+
},
|
|
1259
|
+
));
|
|
1260
|
+
}
|
|
1261
|
+
merge_push(
|
|
1262
|
+
&mut statements,
|
|
1263
|
+
&mut statement_sources,
|
|
1264
|
+
Statement::VarDecl {
|
|
1265
|
+
name: name.clone(),
|
|
1266
|
+
name_span: *name_span,
|
|
1267
|
+
mutable: false,
|
|
1268
|
+
type_ann: None,
|
|
1269
|
+
init: Some(Expr::Object { props, span: *span }),
|
|
1270
|
+
span: *span,
|
|
1271
|
+
},
|
|
1272
|
+
src_path.clone(),
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
ImportSpecifier::Default { name, name_span } => {
|
|
1276
|
+
let source =
|
|
1277
|
+
dep_exports.get("default").cloned().ok_or_else(|| {
|
|
1278
|
+
format!("Module '{}' has no default export", from)
|
|
1279
|
+
})?;
|
|
1280
|
+
merge_push(
|
|
1281
|
+
&mut statements,
|
|
1282
|
+
&mut statement_sources,
|
|
1283
|
+
Statement::VarDecl {
|
|
1284
|
+
name: name.clone(),
|
|
1285
|
+
name_span: *name_span,
|
|
1286
|
+
mutable: false,
|
|
1287
|
+
type_ann: None,
|
|
1288
|
+
init: Some(Expr::Ident {
|
|
1289
|
+
name: Arc::from(source),
|
|
1290
|
+
span: *span,
|
|
1291
|
+
}),
|
|
1292
|
+
span: *span,
|
|
1293
|
+
},
|
|
1294
|
+
src_path.clone(),
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
Statement::Export { declaration, .. } => match declaration.as_ref() {
|
|
1301
|
+
ExportDeclaration::Named(s) => merge_push(
|
|
1302
|
+
&mut statements,
|
|
1303
|
+
&mut statement_sources,
|
|
1304
|
+
*s.clone(),
|
|
1305
|
+
src_path.clone(),
|
|
1306
|
+
),
|
|
1307
|
+
ExportDeclaration::Default(e) => {
|
|
1308
|
+
let default_name = format!("__default_{}", idx);
|
|
1309
|
+
let espan = e.span();
|
|
1310
|
+
merge_push(
|
|
1311
|
+
&mut statements,
|
|
1312
|
+
&mut statement_sources,
|
|
1313
|
+
Statement::VarDecl {
|
|
1314
|
+
name: Arc::from(default_name),
|
|
1315
|
+
name_span: espan,
|
|
1316
|
+
mutable: false,
|
|
1317
|
+
type_ann: None,
|
|
1318
|
+
init: Some((*e).clone()),
|
|
1319
|
+
span: espan,
|
|
1320
|
+
},
|
|
1321
|
+
src_path.clone(),
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
},
|
|
1325
|
+
_ => merge_push(
|
|
1326
|
+
&mut statements,
|
|
1327
|
+
&mut statement_sources,
|
|
1328
|
+
stmt.clone(),
|
|
1329
|
+
src_path.clone(),
|
|
1330
|
+
),
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
Ok(MergedProgram {
|
|
1335
|
+
program: Program { statements },
|
|
1336
|
+
statement_sources,
|
|
1337
|
+
})
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
#[cfg(test)]
|
|
1341
|
+
mod cargo_spec_tests {
|
|
1342
|
+
use std::sync::Arc;
|
|
1343
|
+
|
|
1344
|
+
use super::cargo_export_fn_name;
|
|
1345
|
+
use super::is_native_import;
|
|
1346
|
+
|
|
1347
|
+
#[test]
|
|
1348
|
+
fn is_native_import_accepts_arc_str_ref() {
|
|
1349
|
+
let from: &Arc<str> = &Arc::from("cargo:demo_shim");
|
|
1350
|
+
assert!(is_native_import(from));
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
#[test]
|
|
1354
|
+
fn detect_cycles_skips_cargo_import() {
|
|
1355
|
+
use super::{detect_cycles, resolve_project};
|
|
1356
|
+
let dir = tempfile::tempdir().expect("tempdir");
|
|
1357
|
+
let p = dir.path().join("main.tish");
|
|
1358
|
+
let src = "import { greet } from 'cargo:demo_shim'\nconsole.log(1)\n";
|
|
1359
|
+
std::fs::write(&p, src).unwrap();
|
|
1360
|
+
let root = dir.path();
|
|
1361
|
+
let modules = resolve_project(&p, Some(root)).unwrap();
|
|
1362
|
+
detect_cycles(&modules).unwrap();
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
#[test]
|
|
1366
|
+
fn merge_modules_skips_cargo_import() {
|
|
1367
|
+
use super::{merge_modules, resolve_project};
|
|
1368
|
+
let dir = tempfile::tempdir().expect("tempdir");
|
|
1369
|
+
let p = dir.path().join("main.tish");
|
|
1370
|
+
let src = "import { greet } from 'cargo:demo_shim'\nconsole.log(1)\n";
|
|
1371
|
+
std::fs::write(&p, src).unwrap();
|
|
1372
|
+
let root = dir.path();
|
|
1373
|
+
let modules = resolve_project(&p, Some(root)).unwrap();
|
|
1374
|
+
let _ = merge_modules(modules).unwrap();
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
#[test]
|
|
1378
|
+
fn cargo_export_fn_name_sanitizes() {
|
|
1379
|
+
assert_eq!(
|
|
1380
|
+
cargo_export_fn_name("cargo:tish_serde_json"),
|
|
1381
|
+
"cargo_native_tish_serde_json_object"
|
|
1382
|
+
);
|
|
1383
|
+
assert_eq!(
|
|
1384
|
+
cargo_export_fn_name("cargo:my-crate"),
|
|
1385
|
+
"cargo_native_my_crate_object"
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
}
|