@tishlang/tish 1.5.0 → 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 +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_build_utils/src/lib.rs +33 -0
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/codegen.rs +69 -20
- 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_eval/src/eval.rs +10 -0
- package/crates/tish_native/src/build.rs +38 -10
- package/crates/tish_native/src/lib.rs +30 -15
- package/crates/tish_vm/src/vm.rs +11 -4
- 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
package/bin/tish
CHANGED
|
Binary file
|
package/crates/tish/Cargo.toml
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
//! `cargo:` + `tish.rustDependencies` using the in-repo fixture at `tests/fixtures/cargo_example_project/`
|
|
2
|
+
//! (same layout as the standalone `tish-cargo-example` template).
|
|
3
|
+
|
|
4
|
+
use std::path::PathBuf;
|
|
5
|
+
|
|
6
|
+
use tishlang_ast::Statement;
|
|
7
|
+
use tishlang_compile::{compile_project_full, merge_modules, resolve_project};
|
|
8
|
+
|
|
9
|
+
fn native_build_features_from_cli(cli_features: &[String]) -> Vec<String> {
|
|
10
|
+
if cli_features.is_empty() {
|
|
11
|
+
let mut v: Vec<String> = tishlang_vm::all_compiled_capabilities().into_iter().collect();
|
|
12
|
+
v.sort();
|
|
13
|
+
v
|
|
14
|
+
} else {
|
|
15
|
+
cli_features.to_vec()
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[test]
|
|
20
|
+
fn resolve_and_merge_cargo_example_fixture() {
|
|
21
|
+
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
22
|
+
let input_path = manifest_dir
|
|
23
|
+
.join("tests/fixtures/cargo_example_project/src/main.tish")
|
|
24
|
+
.canonicalize()
|
|
25
|
+
.expect("cargo_example_project test fixture");
|
|
26
|
+
let project_root = input_path.parent().map(|p| {
|
|
27
|
+
if p.file_name().and_then(|n| n.to_str()) == Some("src") {
|
|
28
|
+
p.parent().unwrap_or(p)
|
|
29
|
+
} else {
|
|
30
|
+
p
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
let modules = resolve_project(&input_path, project_root).unwrap();
|
|
34
|
+
assert_eq!(modules.len(), 1, "expected single entry module");
|
|
35
|
+
let first = &modules[0].program.statements[0];
|
|
36
|
+
let Statement::Import { from, .. } = first else {
|
|
37
|
+
panic!("expected import, got {:?}", first);
|
|
38
|
+
};
|
|
39
|
+
assert_eq!(from.as_ref(), "cargo:demo_shim");
|
|
40
|
+
merge_modules(modules).unwrap();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[test]
|
|
44
|
+
fn compile_project_full_cargo_example_fixture() {
|
|
45
|
+
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
46
|
+
let example_main = manifest_dir
|
|
47
|
+
.join("tests/fixtures/cargo_example_project/src/main.tish")
|
|
48
|
+
.canonicalize()
|
|
49
|
+
.expect("cargo_example_project test fixture");
|
|
50
|
+
let input_path = example_main;
|
|
51
|
+
let project_root = input_path.parent().map(|p| {
|
|
52
|
+
if p.file_name().and_then(|n| n.to_str()) == Some("src") {
|
|
53
|
+
p.parent().unwrap_or(p)
|
|
54
|
+
} else {
|
|
55
|
+
p
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
let features = native_build_features_from_cli(&[]);
|
|
59
|
+
let r = compile_project_full(&input_path, project_root, &features, true);
|
|
60
|
+
assert!(
|
|
61
|
+
r.is_ok(),
|
|
62
|
+
"compile_project_full failed: {:?}",
|
|
63
|
+
r.map_err(|e| e.message)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "demo_shim"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
|
|
6
|
+
[lib]
|
|
7
|
+
crate-type = ["rlib"]
|
|
8
|
+
|
|
9
|
+
[dependencies]
|
|
10
|
+
# Fixture lives at crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim → repo crates/tish_core
|
|
11
|
+
tishlang_core = { path = "../../../../../../../crates/tish_core" }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//! Rust exports for `import { … } from 'cargo:demo_shim'` (test fixture).
|
|
2
|
+
|
|
3
|
+
use std::sync::Arc;
|
|
4
|
+
use tishlang_core::Value;
|
|
5
|
+
|
|
6
|
+
pub fn greet(args: &[Value]) -> Value {
|
|
7
|
+
let name = match args.first() {
|
|
8
|
+
Some(Value::String(s)) => s.as_ref(),
|
|
9
|
+
_ => "world",
|
|
10
|
+
};
|
|
11
|
+
Value::String(Arc::from(format!("Hello, {}!", name)))
|
|
12
|
+
}
|
|
@@ -202,6 +202,21 @@ pub fn find_workspace_root() -> Result<PathBuf, String> {
|
|
|
202
202
|
Err("Cannot find Tish workspace root. Run from workspace root or use cargo run.".to_string())
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
/// Path to `crates/tish_runtime` inside a locally installed `@tishlang/tish` npm package.
|
|
206
|
+
pub fn npm_package_runtime_path(project_root: &Path) -> Option<PathBuf> {
|
|
207
|
+
let p = project_root
|
|
208
|
+
.join("node_modules")
|
|
209
|
+
.join("@tishlang")
|
|
210
|
+
.join("tish")
|
|
211
|
+
.join("crates")
|
|
212
|
+
.join("tish_runtime");
|
|
213
|
+
if p.is_dir() {
|
|
214
|
+
Some(p)
|
|
215
|
+
} else {
|
|
216
|
+
None
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
205
220
|
/// Find the path to the tishlang_runtime crate.
|
|
206
221
|
///
|
|
207
222
|
/// Returns a canonical path string suitable for Cargo.toml path dependencies.
|
|
@@ -217,6 +232,24 @@ pub fn find_runtime_path() -> Result<String, String> {
|
|
|
217
232
|
.map(|p| p.display().to_string().replace('\\', "/"))
|
|
218
233
|
}
|
|
219
234
|
|
|
235
|
+
/// Resolve `tishlang_runtime` for a Cargo build, preferring the npm install under `project_root`.
|
|
236
|
+
///
|
|
237
|
+
/// When a Tish app lives next to a checkout of the language repo (e.g. `…/tish/tish-cargo-example`),
|
|
238
|
+
/// [`find_workspace_root`] can return the checkout while `rustDependencies` point at
|
|
239
|
+
/// `node_modules/@tishlang/tish/crates/tish_core`. Using the npm tree for **both** runtime and shim
|
|
240
|
+
/// avoids Cargo lockfile "package collision" for the same crate name/version at two paths.
|
|
241
|
+
pub fn find_runtime_path_for_project(project_root: Option<&Path>) -> Result<String, String> {
|
|
242
|
+
if let Some(root) = project_root {
|
|
243
|
+
if let Some(rt) = npm_package_runtime_path(root) {
|
|
244
|
+
return rt
|
|
245
|
+
.canonicalize()
|
|
246
|
+
.map_err(|e| format!("Cannot canonicalize tishlang_runtime (npm): {}", e))
|
|
247
|
+
.map(|p| p.display().to_string().replace('\\', "/"));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
find_runtime_path()
|
|
251
|
+
}
|
|
252
|
+
|
|
220
253
|
/// Crate package name -> directory name (directories kept as tish_* for historical reasons).
|
|
221
254
|
const CRATE_NAME_TO_DIR: &[(&str, &str)] = &[
|
|
222
255
|
("tishlang_runtime", "tish_runtime"),
|
|
@@ -393,19 +393,27 @@ pub fn compile_project(
|
|
|
393
393
|
project_root: Option<&Path>,
|
|
394
394
|
features: &[String],
|
|
395
395
|
) -> Result<String, CompileError> {
|
|
396
|
-
let (rust, _, _) = compile_project_full(entry_path, project_root, features, true)?;
|
|
396
|
+
let (rust, _, _, _) = compile_project_full(entry_path, project_root, features, true)?;
|
|
397
397
|
Ok(rust)
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
-
/// Compile a project and return Rust code, resolved native modules,
|
|
401
|
-
/// (CLI features plus any inferred from `tish:fs` / `tish:http` / … imports)
|
|
402
|
-
///
|
|
400
|
+
/// Compile a project and return Rust code, resolved native modules, the **effective** feature list
|
|
401
|
+
/// (CLI features plus any inferred from `tish:fs` / `tish:http` / … imports), and native build
|
|
402
|
+
/// artifacts (Cargo dep lines, optional `generated_native.rs` source, init strategy per spec).
|
|
403
403
|
pub fn compile_project_full(
|
|
404
404
|
entry_path: &Path,
|
|
405
405
|
project_root: Option<&Path>,
|
|
406
406
|
features: &[String],
|
|
407
407
|
optimize: bool,
|
|
408
|
-
) -> Result<
|
|
408
|
+
) -> Result<
|
|
409
|
+
(
|
|
410
|
+
String,
|
|
411
|
+
Vec<crate::resolve::ResolvedNativeModule>,
|
|
412
|
+
Vec<String>,
|
|
413
|
+
crate::resolve::NativeBuildArtifacts,
|
|
414
|
+
),
|
|
415
|
+
CompileError,
|
|
416
|
+
> {
|
|
409
417
|
use crate::resolve;
|
|
410
418
|
let root = project_root.unwrap_or_else(|| entry_path.parent().unwrap_or(Path::new(".")));
|
|
411
419
|
let modules = resolve::resolve_project(entry_path, project_root)
|
|
@@ -416,14 +424,23 @@ pub fn compile_project_full(
|
|
|
416
424
|
.map_err(|e| CompileError { message: e, span: None })?;
|
|
417
425
|
let native_modules = resolve::resolve_native_modules(&merged, root)
|
|
418
426
|
.map_err(|e| CompileError { message: e, span: None })?;
|
|
427
|
+
let native_build = resolve::compute_native_build_artifacts(&merged, root, &native_modules)
|
|
428
|
+
.map_err(|e| CompileError { message: e, span: None })?;
|
|
419
429
|
let mut all_features: Vec<String> = features.to_vec();
|
|
420
430
|
for f in resolve::extract_native_import_features(&merged) {
|
|
421
431
|
if !all_features.contains(&f) {
|
|
422
432
|
all_features.push(f);
|
|
423
433
|
}
|
|
424
434
|
}
|
|
425
|
-
let rust = compile_with_native_modules(
|
|
426
|
-
|
|
435
|
+
let rust = compile_with_native_modules(
|
|
436
|
+
&merged,
|
|
437
|
+
project_root,
|
|
438
|
+
&all_features,
|
|
439
|
+
&native_modules,
|
|
440
|
+
&native_build.native_init,
|
|
441
|
+
optimize,
|
|
442
|
+
)?;
|
|
443
|
+
Ok((rust, native_modules, all_features, native_build))
|
|
427
444
|
}
|
|
428
445
|
|
|
429
446
|
/// Compile with explicit feature flags. When features are provided, codegen uses them
|
|
@@ -433,7 +450,8 @@ pub fn compile_with_features(
|
|
|
433
450
|
project_root: Option<&Path>,
|
|
434
451
|
features: &[String],
|
|
435
452
|
) -> Result<String, CompileError> {
|
|
436
|
-
|
|
453
|
+
let empty = std::collections::HashMap::new();
|
|
454
|
+
compile_with_native_modules(program, project_root, features, &[], &empty, true)
|
|
437
455
|
}
|
|
438
456
|
|
|
439
457
|
/// Compile with resolved native modules. Native imports emit calls to the module crates directly.
|
|
@@ -442,16 +460,30 @@ pub fn compile_with_native_modules(
|
|
|
442
460
|
project_root: Option<&Path>,
|
|
443
461
|
features: &[String],
|
|
444
462
|
native_modules: &[crate::resolve::ResolvedNativeModule],
|
|
463
|
+
native_init: &std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
|
|
445
464
|
optimize: bool,
|
|
446
465
|
) -> Result<String, CompileError> {
|
|
447
466
|
let program = if optimize { tishlang_opt::optimize(program) } else { program.clone() };
|
|
448
467
|
// Type-inference pass: fills in `type_ann` on unannotated VarDecl nodes where
|
|
449
468
|
// the type is unambiguous (literals, arithmetic of typed vars, etc.).
|
|
450
469
|
let program = crate::infer::infer_program(&program);
|
|
451
|
-
let map: std::collections::HashMap<String,
|
|
452
|
-
.
|
|
453
|
-
|
|
454
|
-
|
|
470
|
+
let map: std::collections::HashMap<String, crate::resolve::NativeModuleInit> =
|
|
471
|
+
if native_init.is_empty() {
|
|
472
|
+
native_modules
|
|
473
|
+
.iter()
|
|
474
|
+
.map(|m| {
|
|
475
|
+
(
|
|
476
|
+
m.spec.clone(),
|
|
477
|
+
crate::resolve::NativeModuleInit::Legacy {
|
|
478
|
+
crate_name: m.crate_name.clone(),
|
|
479
|
+
export_fn: m.export_fn.clone(),
|
|
480
|
+
},
|
|
481
|
+
)
|
|
482
|
+
})
|
|
483
|
+
.collect()
|
|
484
|
+
} else {
|
|
485
|
+
native_init.clone()
|
|
486
|
+
};
|
|
455
487
|
let mut g = Codegen::new_with_native_modules(project_root, features, map);
|
|
456
488
|
g.emit_program(&program)?;
|
|
457
489
|
Ok(g.output)
|
|
@@ -465,8 +497,8 @@ struct Codegen {
|
|
|
465
497
|
project_root: Option<std::path::PathBuf>,
|
|
466
498
|
/// Requested features (http, process, fs, regex, polars). When non-empty, used instead of #[cfg].
|
|
467
499
|
features: std::collections::HashSet<String>,
|
|
468
|
-
/// spec -> (
|
|
469
|
-
|
|
500
|
+
/// spec -> native init strategy (legacy adapter object vs generated `generated_native` wrapper)
|
|
501
|
+
native_module_init: std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
|
|
470
502
|
/// Stack: true = async Rust context (run body), false = sync closure (Tish fn body)
|
|
471
503
|
async_context_stack: Vec<bool>,
|
|
472
504
|
loop_stack: Vec<(String, Option<String>)>, // (break_label, continue_update) for innermost loop
|
|
@@ -497,7 +529,7 @@ impl Codegen {
|
|
|
497
529
|
fn new_with_native_modules(
|
|
498
530
|
project_root: Option<&Path>,
|
|
499
531
|
features: &[String],
|
|
500
|
-
|
|
532
|
+
native_module_init: std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
|
|
501
533
|
) -> Self {
|
|
502
534
|
let features: std::collections::HashSet<String> = features.iter().cloned().collect();
|
|
503
535
|
Self {
|
|
@@ -507,7 +539,7 @@ impl Codegen {
|
|
|
507
539
|
is_async: false,
|
|
508
540
|
project_root: project_root.map(|p| p.to_path_buf()),
|
|
509
541
|
features,
|
|
510
|
-
|
|
542
|
+
native_module_init,
|
|
511
543
|
async_context_stack: Vec::new(),
|
|
512
544
|
loop_stack: Vec::new(),
|
|
513
545
|
function_scope_stack: vec![Vec::new()], // Start with global scope
|
|
@@ -540,12 +572,21 @@ impl Codegen {
|
|
|
540
572
|
if is_builtin_native_spec(spec) {
|
|
541
573
|
return self.builtin_native_module_rust_init(spec, export_name);
|
|
542
574
|
}
|
|
543
|
-
self.
|
|
575
|
+
self.native_module_init.get(spec).map(|init| {
|
|
544
576
|
// Native modules return a namespace object (like an ES module).
|
|
545
577
|
// Named imports extract the field from that namespace: `import { foo } from "pkg"` → `ns.foo`.
|
|
578
|
+
let init_expr = match init {
|
|
579
|
+
crate::resolve::NativeModuleInit::Legacy {
|
|
580
|
+
crate_name,
|
|
581
|
+
export_fn,
|
|
582
|
+
} => format!("{}::{}()", crate_name, export_fn),
|
|
583
|
+
crate::resolve::NativeModuleInit::Generated { export_fn, .. } => {
|
|
584
|
+
format!("crate::generated_native::{}()", export_fn)
|
|
585
|
+
}
|
|
586
|
+
};
|
|
546
587
|
format!(
|
|
547
|
-
"{{ let _ns = {}
|
|
548
|
-
|
|
588
|
+
"{{ let _ns = {}; match _ns {{ Value::Object(ref _o) => _o.borrow().get({:?}).cloned().unwrap_or(Value::Null), _ => Value::Null }} }}",
|
|
589
|
+
init_expr, export_name
|
|
549
590
|
)
|
|
550
591
|
})
|
|
551
592
|
}
|
|
@@ -1779,7 +1820,15 @@ impl Codegen {
|
|
|
1779
1820
|
}
|
|
1780
1821
|
Expr::Call { callee, args, .. } => {
|
|
1781
1822
|
// Compile-time embed: Polars.read_csv("<literal path>") when file exists
|
|
1782
|
-
if let Some(
|
|
1823
|
+
if let Some(init) = self.native_module_init.get("tish:polars") {
|
|
1824
|
+
let crate_name = match init {
|
|
1825
|
+
crate::resolve::NativeModuleInit::Legacy { crate_name, .. } => {
|
|
1826
|
+
crate_name.as_str()
|
|
1827
|
+
}
|
|
1828
|
+
crate::resolve::NativeModuleInit::Generated { shim_crate, .. } => {
|
|
1829
|
+
shim_crate.as_str()
|
|
1830
|
+
}
|
|
1831
|
+
};
|
|
1783
1832
|
if let (Some(root), Some(CallArg::Expr(first_arg))) =
|
|
1784
1833
|
(self.project_root.as_ref(), args.first())
|
|
1785
1834
|
{
|
|
@@ -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
|
+
}
|
|
@@ -512,6 +512,11 @@ impl Evaluator {
|
|
|
512
512
|
|
|
513
513
|
/// Load and evaluate a module, returning its exports object. Uses cache.
|
|
514
514
|
fn load_module(&mut self, from: &str) -> Result<Value, EvalError> {
|
|
515
|
+
if from.starts_with("cargo:") {
|
|
516
|
+
return Err(EvalError::Error(
|
|
517
|
+
"cargo:… imports are only supported by `tish build` with the Rust native backend.".into(),
|
|
518
|
+
));
|
|
519
|
+
}
|
|
515
520
|
if from.starts_with("tish:") {
|
|
516
521
|
return self.load_builtin_module(from);
|
|
517
522
|
}
|
|
@@ -599,6 +604,11 @@ impl Evaluator {
|
|
|
599
604
|
|
|
600
605
|
/// Load built-in module (tish:fs, tish:http, tish:process, …) or a virtual module from native crates.
|
|
601
606
|
fn load_builtin_module(&self, spec: &str) -> Result<Value, EvalError> {
|
|
607
|
+
if spec.starts_with("cargo:") {
|
|
608
|
+
return Err(EvalError::Error(
|
|
609
|
+
"cargo:… imports are only supported when compiling with `tish build` and the Rust native backend. They link Cargo crates via package.json tish.rustDependencies and a generated native wrapper — not the interpreter or VM.".into(),
|
|
610
|
+
));
|
|
611
|
+
}
|
|
602
612
|
if let Some(v) = self.virtual_builtins.borrow().get(spec) {
|
|
603
613
|
return Ok(v.clone());
|
|
604
614
|
}
|
|
@@ -28,11 +28,24 @@ fn runtime_features_for_cargo(features: &[String]) -> Vec<String> {
|
|
|
28
28
|
out
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/// Inject `mod generated_native;` after the crate attribute so the binary crate can call `crate::generated_native::…`.
|
|
32
|
+
fn inject_generated_native_mod(rust_code: &str) -> String {
|
|
33
|
+
if let Some(pos) = rust_code.find("\n\n") {
|
|
34
|
+
let (a, b) = rust_code.split_at(pos + 2);
|
|
35
|
+
format!("{}mod generated_native;\n{}", a, b)
|
|
36
|
+
} else {
|
|
37
|
+
format!("{}\n\nmod generated_native;\n", rust_code)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
pub fn build_via_cargo(
|
|
32
42
|
rust_code: &str,
|
|
33
43
|
native_modules: Vec<ResolvedNativeModule>,
|
|
34
44
|
output_path: &Path,
|
|
35
45
|
features: &[String],
|
|
46
|
+
extra_dependencies_toml: &str,
|
|
47
|
+
generated_native_rs: Option<&str>,
|
|
48
|
+
project_root: Option<&Path>,
|
|
36
49
|
) -> Result<(), String> {
|
|
37
50
|
let out_name = output_path
|
|
38
51
|
.file_stem()
|
|
@@ -40,7 +53,7 @@ pub fn build_via_cargo(
|
|
|
40
53
|
.unwrap_or("tish_out");
|
|
41
54
|
let build_dir = tishlang_build_utils::create_build_dir("tish_build", out_name)?;
|
|
42
55
|
|
|
43
|
-
let runtime_path = tishlang_build_utils::
|
|
56
|
+
let runtime_path = tishlang_build_utils::find_runtime_path_for_project(project_root)?;
|
|
44
57
|
|
|
45
58
|
let runtime_features = runtime_features_for_cargo(features);
|
|
46
59
|
let runtime_refs: Vec<&str> = runtime_features.iter().map(String::as_str).collect();
|
|
@@ -59,12 +72,28 @@ pub fn build_via_cargo(
|
|
|
59
72
|
|
|
60
73
|
let native_deps: String = native_modules
|
|
61
74
|
.iter()
|
|
75
|
+
.filter(|m| m.use_path_dependency)
|
|
62
76
|
.map(|m| {
|
|
63
77
|
let path = m.crate_path.display().to_string().replace('\\', "/");
|
|
64
78
|
format!("{} = {{ path = {:?} }}\n", m.package_name, path)
|
|
65
79
|
})
|
|
66
80
|
.collect();
|
|
67
81
|
|
|
82
|
+
let mut more_deps = String::new();
|
|
83
|
+
more_deps.push_str(&tokio_dep);
|
|
84
|
+
if !native_deps.is_empty() {
|
|
85
|
+
more_deps.push_str(&format!("\n{}", native_deps));
|
|
86
|
+
}
|
|
87
|
+
if !extra_dependencies_toml.trim().is_empty() {
|
|
88
|
+
more_deps.push_str(&format!("\n{}", extra_dependencies_toml));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let rust_main = if generated_native_rs.is_some() {
|
|
92
|
+
inject_generated_native_mod(rust_code)
|
|
93
|
+
} else {
|
|
94
|
+
rust_code.to_string()
|
|
95
|
+
};
|
|
96
|
+
|
|
68
97
|
let tish_ui_path = std::path::Path::new(&runtime_path)
|
|
69
98
|
.parent()
|
|
70
99
|
.ok_or_else(|| "invalid tishlang_runtime path (no parent)".to_string())?
|
|
@@ -96,23 +125,22 @@ codegen-units = 1
|
|
|
96
125
|
lto = "thin"
|
|
97
126
|
|
|
98
127
|
[dependencies]
|
|
99
|
-
tishlang_runtime = {{ path = {:?}{} }}
|
|
100
|
-
"#,
|
|
128
|
+
tishlang_runtime = {{ path = {:?}{} }}
|
|
129
|
+
{}{}"#,
|
|
101
130
|
out_name,
|
|
102
131
|
runtime_path,
|
|
103
132
|
features_str,
|
|
104
|
-
|
|
105
|
-
if native_deps.is_empty() {
|
|
106
|
-
String::new()
|
|
107
|
-
} else {
|
|
108
|
-
format!("\n{}", native_deps)
|
|
109
|
-
},
|
|
133
|
+
more_deps,
|
|
110
134
|
ui_dep
|
|
111
135
|
);
|
|
112
136
|
|
|
113
137
|
fs::write(build_dir.join("Cargo.toml"), cargo_toml)
|
|
114
138
|
.map_err(|e| format!("Cannot write Cargo.toml: {}", e))?;
|
|
115
|
-
|
|
139
|
+
if let Some(gen) = generated_native_rs {
|
|
140
|
+
fs::write(build_dir.join("src/generated_native.rs"), gen)
|
|
141
|
+
.map_err(|e| format!("Cannot write generated_native.rs: {}", e))?;
|
|
142
|
+
}
|
|
143
|
+
fs::write(build_dir.join("src/main.rs"), rust_main)
|
|
116
144
|
.map_err(|e| format!("Cannot write main.rs: {}", e))?;
|
|
117
145
|
|
|
118
146
|
let workspace_target = Path::new(&runtime_path)
|
|
@@ -57,21 +57,25 @@ pub fn compile_to_native(
|
|
|
57
57
|
|
|
58
58
|
match backend {
|
|
59
59
|
Backend::Rust => {
|
|
60
|
-
let (rust_code, native_modules, effective_features) =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
let (rust_code, native_modules, effective_features, native_build) =
|
|
61
|
+
tishlang_compile::compile_project_full(
|
|
62
|
+
entry_path,
|
|
63
|
+
project_root,
|
|
64
|
+
features,
|
|
65
|
+
optimize,
|
|
66
|
+
)
|
|
67
|
+
.map_err(|e| NativeError {
|
|
68
|
+
message: e.to_string(),
|
|
69
|
+
})?;
|
|
69
70
|
|
|
70
71
|
crate::build::build_via_cargo(
|
|
71
72
|
&rust_code,
|
|
72
73
|
native_modules,
|
|
73
74
|
output_path,
|
|
74
75
|
&effective_features,
|
|
76
|
+
&native_build.rust_dependencies_toml,
|
|
77
|
+
native_build.generated_native_rs.as_deref(),
|
|
78
|
+
project_root,
|
|
75
79
|
)
|
|
76
80
|
.map_err(|e| NativeError { message: e })
|
|
77
81
|
}
|
|
@@ -96,7 +100,7 @@ pub fn compile_to_native(
|
|
|
96
100
|
|
|
97
101
|
if tishlang_compile::has_external_native_imports(&program) {
|
|
98
102
|
return Err(NativeError {
|
|
99
|
-
message: "Cranelift backend does not support external native imports (tish
|
|
103
|
+
message: "Cranelift backend does not support external native imports (tish:…, cargo:…, @scope/pkg). Built-in tish:fs, tish:http, tish:process are supported. Use --native-backend rust for external modules.".to_string(),
|
|
100
104
|
});
|
|
101
105
|
}
|
|
102
106
|
|
|
@@ -132,7 +136,7 @@ pub fn compile_to_native(
|
|
|
132
136
|
};
|
|
133
137
|
if tishlang_compile::has_external_native_imports(&program) {
|
|
134
138
|
return Err(NativeError {
|
|
135
|
-
message: "LLVM backend does not support external native imports. Built-in tish:fs, tish:http, tish:process are supported.".to_string(),
|
|
139
|
+
message: "LLVM backend does not support external native imports (tish:…, cargo:…, @scope/pkg). Built-in tish:fs, tish:http, tish:process are supported.".to_string(),
|
|
136
140
|
});
|
|
137
141
|
}
|
|
138
142
|
let chunk = if optimize {
|
|
@@ -180,6 +184,8 @@ pub fn compile_program_to_native(
|
|
|
180
184
|
let root = project_root.unwrap_or_else(|| Path::new("."));
|
|
181
185
|
let native_modules = tishlang_compile::resolve_native_modules(&program, root)
|
|
182
186
|
.map_err(|e| NativeError { message: e })?;
|
|
187
|
+
let native_build = tishlang_compile::compute_native_build_artifacts(&program, root, &native_modules)
|
|
188
|
+
.map_err(|e| NativeError { message: e })?;
|
|
183
189
|
let mut all_features = features.to_vec();
|
|
184
190
|
for f in tishlang_compile::extract_native_import_features(&program) {
|
|
185
191
|
if !all_features.contains(&f) {
|
|
@@ -191,18 +197,27 @@ pub fn compile_program_to_native(
|
|
|
191
197
|
project_root,
|
|
192
198
|
&all_features,
|
|
193
199
|
&native_modules,
|
|
200
|
+
&native_build.native_init,
|
|
194
201
|
optimize,
|
|
195
202
|
)
|
|
196
203
|
.map_err(|e| NativeError {
|
|
197
204
|
message: e.message,
|
|
198
205
|
})?;
|
|
199
|
-
crate::build::build_via_cargo(
|
|
200
|
-
|
|
206
|
+
crate::build::build_via_cargo(
|
|
207
|
+
&rust_code,
|
|
208
|
+
native_modules,
|
|
209
|
+
output_path,
|
|
210
|
+
&all_features,
|
|
211
|
+
&native_build.rust_dependencies_toml,
|
|
212
|
+
native_build.generated_native_rs.as_deref(),
|
|
213
|
+
Some(root),
|
|
214
|
+
)
|
|
215
|
+
.map_err(|e| NativeError { message: e })
|
|
201
216
|
}
|
|
202
217
|
Backend::Cranelift => {
|
|
203
218
|
if tishlang_compile::has_external_native_imports(program) {
|
|
204
219
|
return Err(NativeError {
|
|
205
|
-
message: "Cranelift backend does not support external native imports. Built-in tish:fs, tish:http, tish:process are supported.".to_string(),
|
|
220
|
+
message: "Cranelift backend does not support external native imports (tish:…, cargo:…, @scope/pkg). Built-in tish:fs, tish:http, tish:process are supported.".to_string(),
|
|
206
221
|
});
|
|
207
222
|
}
|
|
208
223
|
let program = if optimize { tishlang_opt::optimize(program) } else { program.clone() };
|
|
@@ -218,7 +233,7 @@ pub fn compile_program_to_native(
|
|
|
218
233
|
Backend::Llvm => {
|
|
219
234
|
if tishlang_compile::has_external_native_imports(program) {
|
|
220
235
|
return Err(NativeError {
|
|
221
|
-
message: "LLVM backend does not support external native imports.".to_string(),
|
|
236
|
+
message: "LLVM backend does not support external native imports (tish:…, cargo:…, @scope/pkg).".to_string(),
|
|
222
237
|
});
|
|
223
238
|
}
|
|
224
239
|
let program = if optimize { tishlang_opt::optimize(program) } else { program.clone() };
|
package/crates/tish_vm/src/vm.rs
CHANGED
|
@@ -1260,10 +1260,17 @@ impl Vm {
|
|
|
1260
1260
|
}
|
|
1261
1261
|
};
|
|
1262
1262
|
let v = get_builtin_export(self.capabilities.as_ref(), spec, export_name).ok_or_else(|| {
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1263
|
+
if spec.starts_with("cargo:") {
|
|
1264
|
+
format!(
|
|
1265
|
+
"cargo:… imports are only supported by `tish build` with the Rust native backend (not the bytecode VM). Spec: {}",
|
|
1266
|
+
spec
|
|
1267
|
+
)
|
|
1268
|
+
} else {
|
|
1269
|
+
format!(
|
|
1270
|
+
"Built-in module '{}' does not export '{}' or capability not enabled for this run. Use e.g. tish run --feature fs (or full). The tish binary must also be built with that capability linked in.",
|
|
1271
|
+
spec, export_name
|
|
1272
|
+
)
|
|
1273
|
+
}
|
|
1267
1274
|
})?;
|
|
1268
1275
|
self.stack.push(v);
|
|
1269
1276
|
}
|
package/package.json
CHANGED
|
Binary file
|
package/platform/darwin-x64/tish
CHANGED
|
Binary file
|
|
Binary file
|
package/platform/linux-x64/tish
CHANGED
|
Binary file
|
|
Binary file
|