@tishlang/tish 1.10.0 → 1.12.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.
@@ -540,6 +540,33 @@ pub fn compile_project_full(
540
540
  crate::resolve::NativeBuildArtifacts,
541
541
  ),
542
542
  CompileError,
543
+ > {
544
+ compile_project_full_emit(
545
+ entry_path,
546
+ project_root,
547
+ features,
548
+ optimize,
549
+ crate::NativeEmitMode::DesktopBin,
550
+ None,
551
+ )
552
+ }
553
+
554
+ /// Like [`compile_project_full`], with emit mode and optional feature cap (e.g. iOS sandbox).
555
+ pub fn compile_project_full_emit(
556
+ entry_path: &Path,
557
+ project_root: Option<&Path>,
558
+ features: &[String],
559
+ optimize: bool,
560
+ emit_mode: crate::NativeEmitMode,
561
+ feature_cap: Option<&std::collections::HashSet<String>>,
562
+ ) -> Result<
563
+ (
564
+ String,
565
+ Vec<crate::resolve::ResolvedNativeModule>,
566
+ Vec<String>,
567
+ crate::resolve::NativeBuildArtifacts,
568
+ ),
569
+ CompileError,
543
570
  > {
544
571
  use crate::resolve;
545
572
  let root = project_root.unwrap_or_else(|| entry_path.parent().unwrap_or(Path::new(".")));
@@ -555,11 +582,17 @@ pub fn compile_project_full(
555
582
  message: e,
556
583
  span: None,
557
584
  })?;
558
- let native_modules =
585
+ let mut native_modules =
559
586
  resolve::resolve_native_modules(&merged.program, root).map_err(|e| CompileError {
560
587
  message: e,
561
588
  span: None,
562
589
  })?;
590
+ if resolve::program_uses_document(&merged.program) {
591
+ resolve::ensure_tish_canvas_module(&mut native_modules, root).map_err(|e| CompileError {
592
+ message: e,
593
+ span: None,
594
+ })?;
595
+ }
563
596
  let native_build =
564
597
  resolve::compute_native_build_artifacts(&merged.program, root, &native_modules).map_err(
565
598
  |e| CompileError {
@@ -573,13 +606,17 @@ pub fn compile_project_full(
573
606
  all_features.push(f);
574
607
  }
575
608
  }
576
- let rust = compile_with_native_modules(
609
+ if let Some(cap) = feature_cap {
610
+ all_features.retain(|f| cap.contains(f));
611
+ }
612
+ let rust = compile_with_native_modules_emit(
577
613
  &merged.program,
578
614
  project_root,
579
615
  &all_features,
580
616
  &native_modules,
581
617
  &native_build.native_init,
582
618
  optimize,
619
+ emit_mode,
583
620
  )?;
584
621
  Ok((rust, native_modules, all_features, native_build))
585
622
  }
@@ -603,6 +640,26 @@ pub fn compile_with_native_modules(
603
640
  native_modules: &[crate::resolve::ResolvedNativeModule],
604
641
  native_init: &std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
605
642
  optimize: bool,
643
+ ) -> Result<String, CompileError> {
644
+ compile_with_native_modules_emit(
645
+ program,
646
+ project_root,
647
+ features,
648
+ native_modules,
649
+ native_init,
650
+ optimize,
651
+ crate::NativeEmitMode::DesktopBin,
652
+ )
653
+ }
654
+
655
+ pub fn compile_with_native_modules_emit(
656
+ program: &Program,
657
+ project_root: Option<&Path>,
658
+ features: &[String],
659
+ native_modules: &[crate::resolve::ResolvedNativeModule],
660
+ native_init: &std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
661
+ optimize: bool,
662
+ emit_mode: crate::NativeEmitMode,
606
663
  ) -> Result<String, CompileError> {
607
664
  let program = if optimize {
608
665
  tishlang_opt::optimize(program)
@@ -630,6 +687,13 @@ pub fn compile_with_native_modules(
630
687
  native_init.clone()
631
688
  };
632
689
  let mut g = Codegen::new_with_native_modules(project_root, features, map);
690
+ g.emit_mode = emit_mode;
691
+ g.has_native_ui_host = native_modules.iter().any(|m| {
692
+ m.package_name == "tish-macos"
693
+ || m.package_name == "tish-ios"
694
+ || m.crate_name == "tishlang_macos"
695
+ || m.crate_name == "tishlang_ios"
696
+ });
633
697
  g.emit_program(&program)?;
634
698
  Ok(g.output)
635
699
  }
@@ -680,6 +744,11 @@ struct Codegen {
680
744
  /// `try`/`throw` lowering uses `return Err` only at depth 0 (e.g. `run()`); inside native
681
745
  /// closures it must not return a `Result` from a `Value`-returning closure.
682
746
  value_fn_depth: u32,
747
+ emit_mode: crate::NativeEmitMode,
748
+ /// Program links `tish:macos` / `tish:ios` — skip HeadlessHost install.
749
+ has_native_ui_host: bool,
750
+ /// Program references browser global `document` — inject tish-canvas.
751
+ program_uses_document: bool,
683
752
  }
684
753
 
685
754
  impl Codegen {
@@ -710,6 +779,19 @@ impl Codegen {
710
779
  program_has_jsx: false,
711
780
  program_fun_decl_names: std::collections::HashSet::new(),
712
781
  value_fn_depth: 0,
782
+ emit_mode: crate::NativeEmitMode::DesktopBin,
783
+ has_native_ui_host: false,
784
+ program_uses_document: false,
785
+ }
786
+ }
787
+
788
+ /// In async `run()` bodies, propagate runtime op errors with `?`; in sync
789
+ /// `Value::native` closures use `.unwrap_or(Value::Null)`.
790
+ fn ops_result_suffix(&self) -> &'static str {
791
+ if self.is_async && self.async_context_stack.last().copied().unwrap_or(false) {
792
+ "?"
793
+ } else {
794
+ ".unwrap_or(Value::Null)"
713
795
  }
714
796
  }
715
797
 
@@ -1221,11 +1303,12 @@ impl Codegen {
1221
1303
  self.is_async = program_uses_async(program);
1222
1304
  self.program_has_jsx = tishlang_ui::jsx::program_contains_jsx(program);
1223
1305
  self.program_fun_decl_names = tishlang_ui::jsx::collect_fun_decl_names(program);
1306
+ self.program_uses_document = crate::resolve::program_uses_document(program);
1224
1307
  self.write("#![allow(unused, non_snake_case)]\n\n");
1225
1308
  self.write("use std::cell::RefCell;\n");
1226
1309
  self.write("use std::rc::Rc;\n");
1227
1310
  self.write("use std::sync::Arc;\n");
1228
- self.write("use tishlang_runtime::{console_debug as tish_console_debug, console_info as tish_console_info, console_log as tish_console_log, console_warn as tish_console_warn, console_error as tish_console_error, boolean as tish_boolean, decode_uri as tish_decode_uri, encode_uri as tish_encode_uri, string_escape_html_impl as tish_escape_html, in_operator as tish_in_operator, is_finite as tish_is_finite, is_nan as tish_is_nan, json_parse as tish_json_parse, json_stringify as tish_json_stringify, math_abs as tish_math_abs, math_ceil as tish_math_ceil, math_floor as tish_math_floor, math_max as tish_math_max, math_min as tish_math_min, math_round as tish_math_round, math_sqrt as tish_math_sqrt, parse_float as tish_parse_float, parse_int as tish_parse_int, math_random as tish_math_random, math_pow as tish_math_pow, math_sin as tish_math_sin, math_cos as tish_math_cos, math_tan as tish_math_tan, math_log as tish_math_log, math_exp as tish_math_exp, math_sign as tish_math_sign, math_trunc as tish_math_trunc, date_now as tish_date_now, array_is_array as tish_array_is_array, string_from_char_code as tish_string_from_char_code, object_assign as tish_object_assign, object_keys as tish_object_keys, object_values as tish_object_values, object_entries as tish_object_entries, object_from_entries as tish_object_from_entries, symbol_object as tish_symbol_object, tish_construct, tish_uint8_array_constructor, tish_audio_context_constructor, register_static_route as tish_register_static_route, ObjectMap, TishError, Value, VmRef};\n");
1311
+ self.write("use tishlang_runtime::{console_debug as tish_console_debug, console_info as tish_console_info, console_log as tish_console_log, console_warn as tish_console_warn, console_error as tish_console_error, boolean as tish_boolean, decode_uri as tish_decode_uri, encode_uri as tish_encode_uri, string_escape_html_impl as tish_escape_html, in_operator as tish_in_operator, is_finite as tish_is_finite, is_nan as tish_is_nan, json_parse as tish_json_parse, json_stringify as tish_json_stringify, math_abs as tish_math_abs, math_ceil as tish_math_ceil, math_floor as tish_math_floor, math_max as tish_math_max, math_min as tish_math_min, math_round as tish_math_round, math_sqrt as tish_math_sqrt, parse_float as tish_parse_float, parse_int as tish_parse_int, math_random as tish_math_random, math_pow as tish_math_pow, math_sin as tish_math_sin, math_cos as tish_math_cos, math_tan as tish_math_tan, math_log as tish_math_log, math_exp as tish_math_exp, math_sign as tish_math_sign, math_trunc as tish_math_trunc, math_imul as tish_math_imul, date_now as tish_date_now, array_is_array as tish_array_is_array, string_from_char_code as tish_string_from_char_code, object_assign as tish_object_assign, object_keys as tish_object_keys, object_values as tish_object_values, object_entries as tish_object_entries, object_from_entries as tish_object_from_entries, symbol_object as tish_symbol_object, tish_construct, tish_uint8_array_constructor, tish_audio_context_constructor, register_static_route as tish_register_static_route, ObjectMap, TishError, Value, VmRef};\n");
1229
1312
  if self.program_has_jsx {
1230
1313
  self.write("use tishlang_ui::{fragment_value, install_thread_local_host, native_create_root, native_use_state, ui_h, ui_text, HeadlessHost};\n");
1231
1314
  }
@@ -1251,6 +1334,9 @@ impl Codegen {
1251
1334
  if self.has_feature("regex") {
1252
1335
  self.write("use tishlang_runtime::regexp_new;\n");
1253
1336
  }
1337
+ if self.program_uses_document {
1338
+ self.write("use tish_canvas::document_value as tish_canvas_document;\n");
1339
+ }
1254
1340
  self.write("\n");
1255
1341
 
1256
1342
  // Collect every `type Foo = { ... }` declaration in the program
@@ -1266,28 +1352,32 @@ impl Codegen {
1266
1352
  // and `x.field` becomes a direct field access.
1267
1353
  self.emit_named_struct_decls();
1268
1354
 
1269
- if self.is_async {
1355
+ if self.is_async && self.emit_mode == crate::NativeEmitMode::DesktopBin {
1270
1356
  self.writeln("#[tokio::main]");
1271
1357
  self.writeln("async fn main() {");
1272
- } else {
1358
+ } else if self.emit_mode == crate::NativeEmitMode::DesktopBin {
1273
1359
  self.writeln("fn main() {");
1274
1360
  }
1275
- self.indent += 1;
1276
- if self.is_async {
1277
- self.writeln("if let Err(e) = run().await {");
1278
- } else {
1279
- self.writeln("if let Err(e) = run() {");
1361
+ if self.emit_mode == crate::NativeEmitMode::DesktopBin {
1362
+ self.indent += 1;
1363
+ if self.is_async {
1364
+ self.writeln("if let Err(e) = run().await {");
1365
+ } else {
1366
+ self.writeln("if let Err(e) = run() {");
1367
+ }
1368
+ self.indent += 1;
1369
+ self.writeln("eprintln!(\"Error: {}\", e);");
1370
+ self.writeln("std::process::exit(1);");
1371
+ self.indent -= 1;
1372
+ self.writeln("}");
1373
+ self.indent -= 1;
1374
+ self.writeln("}");
1375
+ self.writeln("");
1280
1376
  }
1281
- self.indent += 1;
1282
- self.writeln("eprintln!(\"Error: {}\", e);");
1283
- self.writeln("std::process::exit(1);");
1284
- self.indent -= 1;
1285
- self.writeln("}");
1286
- self.indent -= 1;
1287
- self.writeln("}");
1288
- self.writeln("");
1289
1377
  if self.is_async {
1290
1378
  self.writeln("async fn run() -> Result<(), Box<dyn std::error::Error>> {");
1379
+ } else if self.emit_mode == crate::NativeEmitMode::EmbeddedLib {
1380
+ self.writeln("pub fn run() -> Result<(), Box<dyn std::error::Error>> {");
1291
1381
  } else {
1292
1382
  self.writeln("fn run() -> Result<(), Box<dyn std::error::Error>> {");
1293
1383
  }
@@ -1350,6 +1440,9 @@ impl Codegen {
1350
1440
  self.writeln(
1351
1441
  "(Arc::from(\"trunc\"), Value::native(|args: &[Value]| tish_math_trunc(args))),",
1352
1442
  );
1443
+ self.writeln(
1444
+ "(Arc::from(\"imul\"), Value::native(|args: &[Value]| tish_math_imul(args))),",
1445
+ );
1353
1446
  self.writeln("(Arc::from(\"PI\"), Value::Number(std::f64::consts::PI)),");
1354
1447
  self.writeln("(Arc::from(\"E\"), Value::Number(std::f64::consts::E)),");
1355
1448
  self.indent -= 1;
@@ -1405,6 +1498,14 @@ impl Codegen {
1405
1498
 
1406
1499
  self.writeln("let Uint8Array = tish_uint8_array_constructor();");
1407
1500
  self.writeln("let AudioContext = tish_audio_context_constructor();");
1501
+ if self.program_uses_document {
1502
+ self.writeln("let document = VmRef::new(tish_canvas_document());");
1503
+ self.refcell_wrapped_vars.insert("document".to_string());
1504
+ self.rc_cell_storage_define("document");
1505
+ if let Some(scope) = self.outer_vars_stack.last_mut() {
1506
+ scope.push("document".to_string());
1507
+ }
1508
+ }
1408
1509
 
1409
1510
  if self.has_feature("process") {
1410
1511
  self.writeln("let process = Value::object({");
@@ -1489,7 +1590,7 @@ impl Codegen {
1489
1590
  self.writeln("let RegExp = Value::native(|args: &[Value]| regexp_new(args));");
1490
1591
  }
1491
1592
 
1492
- if self.program_has_jsx {
1593
+ if self.program_has_jsx && !self.has_native_ui_host {
1493
1594
  self.writeln("install_thread_local_host(Box::new(HeadlessHost::default()));");
1494
1595
  self.writeln("let Fragment = fragment_value();");
1495
1596
  self.writeln("let h = Value::native(|args: &[Value]| ui_h(args));");
@@ -1537,6 +1638,20 @@ impl Codegen {
1537
1638
  self.writeln("Ok(())");
1538
1639
  self.indent -= 1;
1539
1640
  self.writeln("}");
1641
+ if self.emit_mode == crate::NativeEmitMode::EmbeddedLib {
1642
+ self.writeln("");
1643
+ self.writeln("#[no_mangle]");
1644
+ self.writeln("pub extern \"C\" fn tish_ios_launch() {");
1645
+ self.indent += 1;
1646
+ if self.is_async {
1647
+ self.writeln("let rt = tokio::runtime::Runtime::new().expect(\"tokio runtime\");");
1648
+ self.writeln("let _ = rt.block_on(run());");
1649
+ } else {
1650
+ self.writeln("let _ = run();");
1651
+ }
1652
+ self.indent -= 1;
1653
+ self.writeln("}");
1654
+ }
1540
1655
  Ok(())
1541
1656
  }
1542
1657
 
@@ -2227,6 +2342,10 @@ impl Codegen {
2227
2342
  for v in &mutable_outer_vars {
2228
2343
  self.refcell_wrapped_vars.insert(v.clone());
2229
2344
  }
2345
+ // Read-only captures are plain Value bindings inside the closure.
2346
+ for v in &read_only_outer_vars {
2347
+ self.refcell_wrapped_vars.remove(v);
2348
+ }
2230
2349
 
2231
2350
  // Pre-scan body for nested functions (handles function body as Block)
2232
2351
  if let Statement::Block { statements, .. } = body.as_ref() {
@@ -2487,7 +2606,12 @@ impl Codegen {
2487
2606
  // Convert native type to Value for compatibility with existing code
2488
2607
  var_type.to_value_expr(&escaped)
2489
2608
  } else {
2490
- escaped.into_owned()
2609
+ let s = escaped.into_owned();
2610
+ if self.value_fn_depth > 0 || !self.loop_stack.is_empty() {
2611
+ format!("({}).clone()", s)
2612
+ } else {
2613
+ s
2614
+ }
2491
2615
  }
2492
2616
  }
2493
2617
  }
@@ -2745,6 +2869,14 @@ impl Codegen {
2745
2869
  obj_expr, start, end
2746
2870
  ));
2747
2871
  }
2872
+ "substr" => {
2873
+ let start = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Number(0.0)".to_string());
2874
+ let length = arg_exprs.get(1).cloned().unwrap_or_else(|| "Value::Null".to_string());
2875
+ return Ok(format!(
2876
+ "tishlang_runtime::string_substr(&{}, &{}, &{})",
2877
+ obj_expr, start, length
2878
+ ));
2879
+ }
2748
2880
  "split" => {
2749
2881
  let sep = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
2750
2882
  return Ok(format!(
@@ -3386,10 +3518,11 @@ impl Codegen {
3386
3518
  CompoundOp::Div => "div",
3387
3519
  CompoundOp::Mod => "modulo",
3388
3520
  };
3521
+ let op_suffix = self.ops_result_suffix();
3389
3522
  if is_refcell {
3390
3523
  format!(
3391
- "{{ let _lhs_v = (*{}.borrow()).clone(); let _rhs = ({}).clone(); let _new = tishlang_runtime::ops::{}(&_lhs_v, &_rhs)?; *{}.borrow_mut() = _new; (*{}.borrow()).clone() }}",
3392
- n, val, op_fn, n, n
3524
+ "{{ let _lhs_v = (*{}.borrow()).clone(); let _rhs = ({}).clone(); let _new = tishlang_runtime::ops::{}(&_lhs_v, &_rhs){}; *{}.borrow_mut() = _new; (*{}.borrow()).clone() }}",
3525
+ n, val, op_fn, op_suffix, n, n
3393
3526
  )
3394
3527
  } else if var_type.is_native() {
3395
3528
  // Wrap native lhs as Value, run ops::, unbox result back to native
@@ -3397,13 +3530,13 @@ impl Codegen {
3397
3530
  let result_native = var_type.from_value_expr("_result");
3398
3531
  let n_as_value2 = var_type.to_value_expr(&n);
3399
3532
  format!(
3400
- "{{ let _lhs = {}; let _rhs = ({}).clone(); let _result = tishlang_runtime::ops::{}(&_lhs, &_rhs)?; {} = {}; {} }}",
3401
- n_as_value, val, op_fn, n, result_native, n_as_value2
3533
+ "{{ let _lhs = {}; let _rhs = ({}).clone(); let _result = tishlang_runtime::ops::{}(&_lhs, &_rhs){}; {} = {}; {} }}",
3534
+ n_as_value, val, op_fn, op_suffix, n, result_native, n_as_value2
3402
3535
  )
3403
3536
  } else {
3404
3537
  format!(
3405
- "{{ let _rhs = ({}).clone(); {} = tishlang_runtime::ops::{}(&{}, &_rhs)?; {}.clone() }}",
3406
- val, n, op_fn, n, n
3538
+ "{{ let _rhs = ({}).clone(); {} = tishlang_runtime::ops::{}(&{}, &_rhs){}; {}.clone() }}",
3539
+ val, n, op_fn, n, op_suffix, n
3407
3540
  )
3408
3541
  }
3409
3542
  }
@@ -4695,6 +4828,41 @@ impl Codegen {
4695
4828
  ));
4696
4829
  }
4697
4830
 
4831
+ // Locals from an enclosing Value::native (e.g. captured helper fns) are not on
4832
+ // outer_vars_stack but must not move into multiple sibling closures.
4833
+ const BUILTINS: &[&str] = &[
4834
+ "Boolean", "console", "Math", "JSON", "Date", "Object", "process",
4835
+ "setTimeout", "clearTimeout", "setInterval", "clearInterval", "Promise",
4836
+ "Symbol", "RegExp", "Polars", "Infinity", "NaN", "serve",
4837
+ ];
4838
+ let mut already_captured: HashSet<String> = outer_vars
4839
+ .iter()
4840
+ .chain(outer_params.iter())
4841
+ .chain(referenced_funcs.iter())
4842
+ .cloned()
4843
+ .collect();
4844
+ already_captured.extend(BUILTINS.iter().map(|s| s.to_string()));
4845
+ let implicit_env_captures: Vec<String> = if self.value_fn_depth > 0 {
4846
+ referenced
4847
+ .iter()
4848
+ .filter(|name| {
4849
+ !param_names.contains(name.as_str())
4850
+ && !local_var_names.contains(name.as_str())
4851
+ && !already_captured.contains(name.as_str())
4852
+ })
4853
+ .cloned()
4854
+ .collect()
4855
+ } else {
4856
+ Vec::new()
4857
+ };
4858
+ for name in &implicit_env_captures {
4859
+ let escaped = Self::escape_ident(name);
4860
+ code.push_str(&format!(
4861
+ " let {}_ref = VmRef::new({}.clone());\n",
4862
+ escaped, escaped
4863
+ ));
4864
+ }
4865
+
4698
4866
  code.push_str(" Value::native(move |args: &[Value]| {\n");
4699
4867
  self.value_fn_depth += 1;
4700
4868
 
@@ -4722,6 +4890,13 @@ impl Codegen {
4722
4890
  var_escaped, var_escaped
4723
4891
  ));
4724
4892
  }
4893
+ for name in &implicit_env_captures {
4894
+ let escaped = Self::escape_ident(name);
4895
+ code.push_str(&format!(
4896
+ " let {} = (*{}_ref.borrow()).clone();\n",
4897
+ escaped, escaped
4898
+ ));
4899
+ }
4725
4900
 
4726
4901
  // Make captured functions available
4727
4902
  for func_name in &referenced_funcs {
@@ -4774,6 +4949,12 @@ impl Codegen {
4774
4949
  for v in &cell_capture_outer_vars {
4775
4950
  self.refcell_wrapped_vars.insert(v.clone());
4776
4951
  }
4952
+ for v in &read_only_outer_vars {
4953
+ self.refcell_wrapped_vars.remove(v);
4954
+ }
4955
+ for v in &implicit_env_captures {
4956
+ self.refcell_wrapped_vars.remove(v);
4957
+ }
4777
4958
 
4778
4959
  self.type_context.push_fun_param_scope(params, None);
4779
4960
 
@@ -7,19 +7,29 @@ mod infer;
7
7
  mod resolve;
8
8
  mod types;
9
9
 
10
+ /// How generated Rust is linked (desktop binary vs embedded iOS staticlib).
11
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12
+ pub enum NativeEmitMode {
13
+ #[default]
14
+ DesktopBin,
15
+ /// `[lib] crate-type = ["staticlib"]` — no `fn main()`, host calls `tish_ios_launch`.
16
+ EmbeddedLib,
17
+ }
18
+
10
19
  pub use codegen::CompileError;
11
20
  pub use codegen::{
12
- compile, compile_project, compile_project_full, compile_with_features,
13
- compile_with_native_modules, compile_with_project_root,
21
+ compile, compile_project, compile_project_full, compile_project_full_emit,
22
+ compile_with_features, compile_with_native_modules, compile_with_native_modules_emit,
23
+ compile_with_project_root,
14
24
  };
15
25
  pub use resolve::{
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, is_native_import, merge_modules,
20
- normalize_builtin_spec, read_project_tish_config, resolve_bare_spec, resolve_native_modules,
21
- resolve_project, resolve_project_from_stdin, MergedProgram, NativeBuildArtifacts,
22
- NativeModuleInit, ResolvedNativeModule,
26
+ cargo_export_fn_name, compute_native_build_artifacts, detect_cycles, ensure_tish_canvas_module,
27
+ export_name_to_rust_ident, extract_native_import_features, format_rust_dependencies_toml,
28
+ generate_native_wrapper_rs, has_external_native_imports, has_native_imports,
29
+ infer_native_module_exports, is_builtin_native_spec, is_cargo_native_spec, is_native_import,
30
+ merge_modules, normalize_builtin_spec, program_uses_document, read_project_tish_config,
31
+ resolve_bare_spec, resolve_native_modules, resolve_project, resolve_project_from_stdin,
32
+ MergedProgram, NativeBuildArtifacts, NativeModuleInit, ResolvedNativeModule,
23
33
  };
24
34
  pub use types::{RustType, TypeContext};
25
35
 
@@ -4,7 +4,7 @@
4
4
  use std::collections::{HashMap, HashSet};
5
5
  use std::path::{Path, PathBuf};
6
6
  use std::sync::Arc;
7
- use tishlang_ast::{ExportDeclaration, Expr, ImportSpecifier, Program, Statement};
7
+ use tishlang_ast::{ExportDeclaration, Expr, ImportSpecifier, MemberProp, Program, Statement, CallArg};
8
8
 
9
9
  /// Resolved native module: crate path and init expression.
10
10
  #[derive(Debug, Clone)]
@@ -114,6 +114,188 @@ pub fn resolve_native_modules(
114
114
  Ok(modules)
115
115
  }
116
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
+
117
299
  /// True for `cargo:…` specs (Cargo-backed imports; Rust native backend only).
118
300
  pub fn is_cargo_native_spec(spec: &str) -> bool {
119
301
  spec.starts_with("cargo:")
@@ -725,11 +907,11 @@ fn load_module_recursive(
725
907
  /// - fs, http, timers, process, ws (Node-compatible aliases for tish:*)
726
908
  /// - tish:egui, tish:polars, etc.
727
909
  /// - cargo:… (Cargo `rustDependencies` + generated wrapper; Rust native backend)
728
- /// - @scope/package (npm-style)
910
+ ///
911
+ /// Scoped npm packages (`@scope/pkg`) are merged as Tish source unless imported via `tish:…`.
729
912
  pub fn is_native_import(spec: &str) -> bool {
730
913
  spec.starts_with("tish:")
731
914
  || spec.starts_with("cargo:")
732
- || spec.starts_with('@')
733
915
  || matches!(spec, "fs" | "http" | "timers" | "process" | "ws")
734
916
  }
735
917