@tishlang/tish-lsp 2.39.0 → 2.43.8

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.
Files changed (73) hide show
  1. package/Cargo.toml +4 -1
  2. package/bin/tish-lsp +0 -0
  3. package/crates/tish/src/cli_help.rs +28 -0
  4. package/crates/tish/src/main.rs +41 -5
  5. package/crates/tish/tests/scope_merge.rs +79 -0
  6. package/crates/tish_build_utils/src/lib.rs +27 -1
  7. package/crates/tish_builtins/Cargo.toml +14 -6
  8. package/crates/tish_builtins/src/array.rs +130 -54
  9. package/crates/tish_builtins/src/collections.rs +9 -5
  10. package/crates/tish_builtins/src/construct.rs +7 -1
  11. package/crates/tish_builtins/src/date.rs +15 -1
  12. package/crates/tish_builtins/src/globals.rs +38 -3
  13. package/crates/tish_builtins/src/helpers.rs +7 -3
  14. package/crates/tish_builtins/src/iterator.rs +5 -1
  15. package/crates/tish_builtins/src/lib.rs +3 -0
  16. package/crates/tish_builtins/src/math.rs +14 -0
  17. package/crates/tish_builtins/src/number.rs +72 -6
  18. package/crates/tish_builtins/src/object.rs +6 -1
  19. package/crates/tish_builtins/src/string.rs +13 -2
  20. package/crates/tish_builtins/src/symbol.rs +17 -17
  21. package/crates/tish_builtins/src/typedarrays.rs +9 -2
  22. package/crates/tish_bytecode/src/compiler.rs +76 -2
  23. package/crates/tish_bytecode/src/lib.rs +1 -1
  24. package/crates/tish_bytecode/src/opcode.rs +77 -2
  25. package/crates/tish_compile/src/codegen.rs +806 -46
  26. package/crates/tish_compile/src/infer.rs +37 -41
  27. package/crates/tish_compile/src/lib.rs +13 -1
  28. package/crates/tish_compile/src/platform_resolve.rs +361 -0
  29. package/crates/tish_compile/src/resolve.rs +148 -10
  30. package/crates/tish_compile/src/schemes.rs +278 -0
  31. package/crates/tish_compile/src/types.rs +141 -2
  32. package/crates/tish_compile/tests/perf_codegen_320.rs +2 -2
  33. package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +3 -1
  34. package/crates/tish_compile/tests/platform_resolve_cli.rs +164 -0
  35. package/crates/tish_compile/tests/regr_gba_numerics_gated.rs +58 -0
  36. package/crates/tish_compiler_wasm/Cargo.toml +1 -0
  37. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +56 -6
  38. package/crates/tish_core/Cargo.toml +39 -4
  39. package/crates/tish_core/src/compat.rs +506 -0
  40. package/crates/tish_core/src/console_style.rs +19 -1
  41. package/crates/tish_core/src/json.rs +34 -10
  42. package/crates/tish_core/src/lib.rs +121 -25
  43. package/crates/tish_core/src/macros.rs +1 -1
  44. package/crates/tish_core/src/shape.rs +4 -2
  45. package/crates/tish_core/src/uri.rs +8 -1
  46. package/crates/tish_core/src/value.rs +182 -54
  47. package/crates/tish_core/src/vmref.rs +17 -5
  48. package/crates/tish_eval/Cargo.toml +2 -0
  49. package/crates/tish_eval/src/eval.rs +90 -3
  50. package/crates/tish_eval/src/value.rs +8 -0
  51. package/crates/tish_eval/src/value_convert.rs +316 -21
  52. package/crates/tish_lsp/Cargo.toml +3 -1
  53. package/crates/tish_lsp/src/import_goto.rs +41 -14
  54. package/crates/tish_lsp/src/main.rs +18 -0
  55. package/crates/tish_native/src/build.rs +246 -0
  56. package/crates/tish_native/src/config.rs +20 -0
  57. package/crates/tish_native/src/lib.rs +16 -6
  58. package/crates/tish_parser/src/lib.rs +28 -0
  59. package/crates/tish_parser/src/parser.rs +25 -4
  60. package/crates/tish_runtime/src/lib.rs +54 -9
  61. package/crates/tish_runtime_gba/Cargo.lock +837 -0
  62. package/crates/tish_runtime_gba/Cargo.toml +16 -0
  63. package/crates/tish_runtime_gba/src/gba.rs +211 -0
  64. package/crates/tish_runtime_gba/src/lib.rs +620 -0
  65. package/crates/tish_vm/src/jit.rs +1446 -164
  66. package/crates/tish_vm/src/vm.rs +535 -173
  67. package/justfile +9 -0
  68. package/package.json +1 -1
  69. package/platform/darwin-arm64/tish-lsp +0 -0
  70. package/platform/darwin-x64/tish-lsp +0 -0
  71. package/platform/linux-arm64/tish-lsp +0 -0
  72. package/platform/linux-x64/tish-lsp +0 -0
  73. package/platform/win32-x64/tish-lsp.exe +0 -0
@@ -589,6 +589,12 @@ pub fn compile_project_full_emit(
589
589
  > {
590
590
  use crate::resolve;
591
591
  let root = project_root.unwrap_or_else(|| entry_path.parent().unwrap_or(Path::new(".")));
592
+ // Install the import-scheme registry (built-ins + any `tish.schemes` from package.json) for
593
+ // this build, so resolution and codegen recognize `asset:` and any project-declared scheme.
594
+ crate::schemes::set_active(crate::schemes::SchemeRegistry::from_project(root));
595
+ // (The narrow-int/`fixed`/`i32` numeric profile is set from `emit_mode` in
596
+ // `compile_with_native_modules_emit`, the funnel every entry point reaches — not here — so it
597
+ // can't leak across compiles on a shared thread. Resolution/inference don't lower annotations.)
592
598
  let modules = resolve::resolve_project(entry_path, project_root).map_err(|e| CompileError {
593
599
  message: e,
594
600
  span: None,
@@ -660,6 +666,11 @@ pub fn compile_with_native_modules(
660
666
  native_init: &std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
661
667
  optimize: bool,
662
668
  ) -> Result<String, CompileError> {
669
+ // This non-project entry (reached by `compile`/`compile_with_features`, never the GBA project
670
+ // path) compiles an already-resolved program, so no scheme registry was installed for it. Reset
671
+ // to the empty built-in set, in case a prior GBA compile on this thread left one active — a
672
+ // normal program has no scheme imports, but this keeps per-compile thread-local state hermetic.
673
+ crate::schemes::set_active(crate::schemes::SchemeRegistry::builtin());
663
674
  compile_with_native_modules_emit(
664
675
  program,
665
676
  project_root,
@@ -708,6 +719,24 @@ pub fn compile_with_native_modules_emit(
708
719
  optimize: bool,
709
720
  emit_mode: crate::NativeEmitMode,
710
721
  ) -> Result<String, CompileError> {
722
+ // Per-compile thread-local state — the narrow-int/`fixed`/`i32` numeric profile and the import-
723
+ // scheme registry — must not outlive THIS compile. Both persist on the thread, so a later compile
724
+ // on the same thread (LSP, `cargo test` workers share threads) could inherit a prior GBA compile's
725
+ // state and lower those annotations natively off-GBA / see stale schemes — diverging from the
726
+ // interpreter. This funnel is the one point every entry reaches, so reset both to their defaults
727
+ // on the way OUT, even on panic (mirrors tish_resolve's guard). Then set the numeric profile for
728
+ // THIS compile from the always-correct `emit_mode`. The scheme registry is installed before
729
+ // resolution by `compile_project_full_emit` (it must exist during resolve); the guard only clears
730
+ // it afterward so it can't leak forward.
731
+ struct StateGuard;
732
+ impl Drop for StateGuard {
733
+ fn drop(&mut self) {
734
+ crate::types::set_gba_numerics(false);
735
+ crate::schemes::set_active(crate::schemes::SchemeRegistry::builtin());
736
+ }
737
+ }
738
+ let _state_guard = StateGuard;
739
+ crate::types::set_gba_numerics(emit_mode == crate::NativeEmitMode::Gba);
711
740
  let program = if optimize {
712
741
  tishlang_opt::optimize(program)
713
742
  } else {
@@ -742,13 +771,77 @@ pub fn compile_with_native_modules_emit(
742
771
  g.has_native_ui_host = native_modules.iter().any(|m| {
743
772
  m.package_name == "tish-macos"
744
773
  || m.package_name == "tish-ios"
774
+ || m.crate_name == "tish_macos"
745
775
  || m.crate_name == "tishlang_macos"
776
+ || m.crate_name == "tish_ios"
746
777
  || m.crate_name == "tishlang_ios"
747
778
  });
748
779
  g.emit_program(&program)?;
749
780
  Ok(g.output)
750
781
  }
751
782
 
783
+ /// Rewrite the `std::` paths that survive in `Gba`-mode generated code into their
784
+ /// `core::` equivalents (the emitted crate is `#![no_std]`). Applied once to the
785
+ /// whole output. Each pattern is a specific fully-qualified Rust path — none occur
786
+ /// inside escaped user string literals — so the textual pass is safe. Callers add
787
+ /// to this table as the thumbv4t corpus build surfaces new sites.
788
+ fn gba_no_std_rewrite(src: String) -> String {
789
+ // Apply the fixups only to CODE, never to the contents of string literals: a user
790
+ // string that happens to contain one of these patterns (e.g. `let s = "std::mem::x"`)
791
+ // must be left byte-for-byte intact. We scan the generated source, copy each `"…"`
792
+ // literal through verbatim (respecting `\` escapes), and rewrite only the code between
793
+ // them. (`"` and `\` are ASCII, so a byte scan is UTF-8 safe; char literals can't hold
794
+ // these multi-char patterns, and any stray `'"'` at worst *misses* a fixup — a loud
795
+ // no_std compile error — never a silent string corruption.)
796
+ let bytes = src.as_bytes();
797
+ let mut out = String::with_capacity(src.len());
798
+ let mut seg_start = 0;
799
+ let mut i = 0;
800
+ while i < bytes.len() {
801
+ if bytes[i] == b'"' {
802
+ out.push_str(&gba_rewrite_code(&src[seg_start..i]));
803
+ let str_start = i;
804
+ i += 1;
805
+ while i < bytes.len() {
806
+ match bytes[i] {
807
+ b'\\' => i += 2, // skip the escape and the (ASCII) escaped byte
808
+ b'"' => {
809
+ i += 1;
810
+ break;
811
+ }
812
+ _ => i += 1,
813
+ }
814
+ }
815
+ out.push_str(&src[str_start..i.min(bytes.len())]);
816
+ seg_start = i;
817
+ } else {
818
+ i += 1;
819
+ }
820
+ }
821
+ out.push_str(&gba_rewrite_code(&src[seg_start..]));
822
+ out
823
+ }
824
+
825
+ /// The std::→core::/facade path fixups, applied only to non-string-literal code by
826
+ /// [`gba_no_std_rewrite`]. Callers extend this table as the thumbv4t corpus build surfaces
827
+ /// new sites (a surviving `std::` path is a loud `#![no_std]` compile error, not silent).
828
+ fn gba_rewrite_code(src: &str) -> String {
829
+ src.replace("std::error::Error", "core::error::Error")
830
+ .replace("std::f64::consts", "core::f64::consts")
831
+ .replace("std::cmp::Ordering", "core::cmp::Ordering")
832
+ .replace("std::mem::", "core::mem::")
833
+ .replace("std::slice::", "core::slice::")
834
+ // Struct→Value boundary conversions (`object_from_pairs`) and a few other
835
+ // sites emit fully-qualified `::std::sync::Arc` / `std::sync::Arc`. The Gba
836
+ // prelude imports `Arc` (= facade Rc alias), so map both to it. The `::`
837
+ // form must be replaced first (it contains the non-`::` form).
838
+ .replace("::std::sync::Arc", "Arc")
839
+ .replace("std::sync::Arc", "Arc")
840
+ // hashbrown lacks `From<[_; N]>` with a custom hasher; route object-literal
841
+ // construction through the facade helper.
842
+ .replace("ObjectMap::from(", "tishlang_runtime::object_map_from(")
843
+ }
844
+
752
845
  /// #177 (S-E/S-F): the return shape of a de-virtualized aggregate (struct/array) free fn.
753
846
  #[derive(Debug, Clone, PartialEq)]
754
847
  enum AggRet {
@@ -784,7 +877,7 @@ struct AggFnSig {
784
877
  ret: AggRet,
785
878
  }
786
879
 
787
- /// #178 (behind `TISH_REC_STRUCT`): a recursive struct shape inferred *structurally* (no name
880
+ /// #178 (default-on via `native_opts_enabled`): a recursive struct shape inferred *structurally* (no name
788
881
  /// matching) from object literals whose fields are either scalars or recursive self-references.
789
882
  /// Drives native lowering of tree/list builders and consumers to `Option<Box<T>>` + native fns —
790
883
  /// the real, name-independent binary_trees lowering. See docs/recursive-struct-native.md.
@@ -915,7 +1008,7 @@ pub(crate) struct Codegen {
915
1008
  /// Variables currently wrapped in Rc<RefCell<Value>> for mutable capture in closures
916
1009
  /// These need special handling: reads via .borrow().clone(), writes via *var.borrow_mut()
917
1010
  refcell_wrapped_vars: std::collections::HashSet<String>,
918
- /// #176 (behind `TISH_NATIVE_FN`): top-level `let` bindings lowered to `thread_local Cell<f64>`
1011
+ /// #176 (default-on via `native_opts_enabled`): top-level `let` bindings lowered to `thread_local Cell<f64>`
919
1012
  /// (`G_NAME`) when every use is a numeric read or a whole-binding numeric assign (fasta `seed`).
920
1013
  native_numeric_globals: std::collections::HashMap<String, f64>,
921
1014
  /// Top-level `let name = [f64 literals…]` never reassigned — emitted as `const G_name: [f64; N]`
@@ -940,7 +1033,7 @@ pub(crate) struct Codegen {
940
1033
  /// of `str_char_code_at`'s `chars().nth(i)` = O(i). Maps the source ident → the hoisted `_scN`
941
1034
  /// `Vec<char>` binding; populated for the loop body's extent only.
942
1035
  hoisted_string_chars: std::collections::HashMap<String, String>,
943
- /// M5 (dark-shipped behind `TISH_NATIVE_FN`): top-level functions eligible for a parallel
1036
+ /// M5 (default-on via `native_opts_enabled`): top-level functions eligible for a parallel
944
1037
  /// free `fn f_native(f64,..)->f64` (all params `: number`, returns `number`, native-safe
945
1038
  /// body). Direct calls to these route to the native fn, bypassing the boxed `value_call`.
946
1039
  native_fns: std::collections::HashSet<String>,
@@ -1001,7 +1094,7 @@ pub(crate) struct Codegen {
1001
1094
  /// nested call site these are already `&/&mut Vec<T>` references, so they pass through by reborrow
1002
1095
  /// (`&mut *p`) rather than being address-of'd like a local `Vec` (`&mut local`).
1003
1096
  vec_ref_params: std::collections::HashMap<String, bool>,
1004
- /// #320 (behind TISH_NATIVE_ARR_PARAM): normal boxed-closure fns that take one or more READ-ONLY
1097
+ /// #320 (default-on via `native_opts_enabled`): normal boxed-closure fns that take one or more READ-ONLY
1005
1098
  /// `number[]` params (e.g. k_nucleotide's `kNucleotide(seq, k)` — indexes `seq` but returns a
1006
1099
  /// boxed Map). Per param: `true` = unbox the boxed arg into an owned native `Vec<f64>` so the
1007
1100
  /// body indexes it natively (`seq[i+j]` → f64), `false` = leave it on its normal path. The fn
@@ -1009,10 +1102,10 @@ pub(crate) struct Codegen {
1009
1102
  /// native. Computed by `Self::native_arr_param_fns`, the SAME pure fn infer reads (agreement
1010
1103
  /// contract — see `infer::native_vec_array_params`), disjoint from `native_vec_fns`.
1011
1104
  native_arr_param_fns: std::collections::HashMap<String, Vec<bool>>,
1012
- /// #177 S-E/S-F (dark-shipped behind `TISH_AGGREGATE_INFER`): the unboxed struct alias name
1105
+ /// #177 S-E/S-F (default-on via `native_opts_enabled`): the unboxed struct alias name
1013
1106
  /// (e.g. `TishAnon_0`) when the interprocedural aggregate path is active for this program,
1014
1107
  /// else `None`. Set in `emit_program` only after the de-virtualized fns emit successfully.
1015
- /// #178: inferred recursive-struct lowering plan (behind `TISH_REC_STRUCT`).
1108
+ /// #178: inferred recursive-struct lowering plan (default-on via `native_opts_enabled`).
1016
1109
  rec_struct_plan: Option<RecStructPlan>,
1017
1110
  aggregate_alias: Option<String>,
1018
1111
  /// #177: fn name → its de-virtualized native signature. Calls to these route directly to the
@@ -1108,6 +1201,11 @@ pub(crate) struct Codegen {
1108
1201
  /// closures it must not return a `Result` from a `Value`-returning closure.
1109
1202
  value_fn_depth: u32,
1110
1203
  emit_mode: crate::NativeEmitMode,
1204
+ /// Scheme imports as `(scheme_name, absolute_path)`, in first-seen order. Populated (Gba mode)
1205
+ /// before the entry is emitted from the active [`crate::schemes`] registry; drives the
1206
+ /// `mod __scheme_<name>_<j> { … }` blocks and their per-scheme registration. The handle a tish
1207
+ /// name binds to is its index AMONG same-scheme entries (= its slot in that scheme's arena).
1208
+ scheme_files: Vec<(String, String)>,
1111
1209
  /// Program links `tish:macos` / `tish:ios` — skip HeadlessHost install.
1112
1210
  has_native_ui_host: bool,
1113
1211
  /// Program references browser global `document` — inject tish-canvas.
@@ -1120,6 +1218,10 @@ pub(crate) struct Codegen {
1120
1218
  cse_subst: Vec<(Expr, String)>,
1121
1219
  /// Monotonic counter for unique CSE temp names (`_cse0`, `_cse1`, …).
1122
1220
  cse_temp_counter: usize,
1221
+ /// The program's used NON-ASCII characters (collected from every string literal), as a sorted
1222
+ /// string. Fed to import schemes via `{charset}` so a scheme can bake only the glyphs a program
1223
+ /// actually uses — e.g. `font<N>:` bakes ASCII + these, not all ~24k glyphs of a CJK font.
1224
+ string_charset: String,
1123
1225
  }
1124
1226
 
1125
1227
  /// One `map`/`filter` stage of a fused HOF chain (#317): the single-element-param closure's
@@ -1146,6 +1248,7 @@ impl Codegen {
1146
1248
  indent: 0,
1147
1249
  loop_label_index: 0,
1148
1250
  is_async: false,
1251
+ string_charset: String::new(),
1149
1252
  features,
1150
1253
  native_module_init,
1151
1254
  async_context_stack: Vec::new(),
@@ -1213,6 +1316,7 @@ impl Codegen {
1213
1316
  program_fun_decl_names: std::collections::HashSet::new(),
1214
1317
  value_fn_depth: 0,
1215
1318
  emit_mode: crate::NativeEmitMode::DesktopBin,
1319
+ scheme_files: Vec::new(),
1216
1320
  has_native_ui_host: false,
1217
1321
  program_uses_document: false,
1218
1322
  cse_subst: Vec::new(),
@@ -1346,6 +1450,160 @@ impl Codegen {
1346
1450
  /// an object shape. Each generated struct derives `Clone` + `Debug`
1347
1451
  /// (cheap; field types are all `Copy`-or-cheap-clone in practice) and
1348
1452
  /// is named `TishStruct_<TishAlias>`.
1453
+ /// Split a lowered scheme spec (`<prefix><abspath>`) into `(scheme_name, abspath)` using the
1454
+ /// active registry. `None` if the spec isn't a registered scheme.
1455
+ fn split_scheme_spec(spec: &str) -> Option<(String, String)> {
1456
+ crate::schemes::with_active(|reg| {
1457
+ reg.matches(spec).map(|s| {
1458
+ let prefix = s.prefix();
1459
+ let rest = spec.strip_prefix(&prefix).unwrap_or(spec).to_string();
1460
+ (s.name.clone(), rest)
1461
+ })
1462
+ })
1463
+ }
1464
+
1465
+ /// Populate [`Self::scheme_files`] from the program's scheme imports. Each was lowered (resolve
1466
+ /// phase) to a top-level `let name = NativeModuleLoad { spec: "<prefix><abspath>" }`, so a flat
1467
+ /// scan of the top-level statements finds them all. De-duplicated on `(scheme, path)`.
1468
+ fn collect_scheme_files(&mut self, stmts: &[Statement]) {
1469
+ for s in stmts {
1470
+ if let Statement::VarDecl {
1471
+ init: Some(Expr::NativeModuleLoad { spec, .. }),
1472
+ ..
1473
+ } = s
1474
+ {
1475
+ if let Some(entry) = Self::split_scheme_spec(spec.as_ref()) {
1476
+ if !self.scheme_files.contains(&entry) {
1477
+ self.scheme_files.push(entry);
1478
+ }
1479
+ }
1480
+ }
1481
+ }
1482
+ }
1483
+
1484
+ /// The runtime arena a scheme registers into, keyed by its (pre-render) `register` template.
1485
+ /// Schemes that share a template share an arena: `asset:` and `sheet:` both call
1486
+ /// `__asset_register_sheet`, so both index into the same runtime `Vec`. `None` for a scheme with
1487
+ /// no registration call (nothing to index into).
1488
+ fn scheme_arena_key(&self, scheme: &str) -> Option<String> {
1489
+ let emit = self.scheme_target_emit(scheme).ok()?;
1490
+ (!emit.register.is_empty()).then_some(emit.register)
1491
+ }
1492
+
1493
+ /// The handle a scheme spec binds to — its slot in the runtime ARENA it registers into. The
1494
+ /// arena is keyed by the scheme's `register` target, NOT its name: `asset:` and `sheet:` both
1495
+ /// push to `__asset_register_sheet`, so a program mixing them must number handles across BOTH
1496
+ /// (the entry registers every scheme file in one first-seen pass, so same-arena entries land in
1497
+ /// exactly this order). Numbering per-scheme-name collided their handles — an `asset:` and a
1498
+ /// `sheet:` import each got handle 0 while the runtime gave them slots 0 and 1 (#552).
1499
+ fn scheme_handle_of(&self, spec: &str) -> Option<usize> {
1500
+ let (scheme, path) = Self::split_scheme_spec(spec)?;
1501
+ let arena = self.scheme_arena_key(&scheme)?;
1502
+ let mut k = 0usize;
1503
+ for (s, p) in &self.scheme_files {
1504
+ if self.scheme_arena_key(s).as_deref() != Some(arena.as_str()) {
1505
+ continue;
1506
+ }
1507
+ if *s == scheme && *p == path {
1508
+ return Some(k);
1509
+ }
1510
+ k += 1;
1511
+ }
1512
+ None
1513
+ }
1514
+
1515
+ /// The generated `mod` identifier for the `j`-th collected scheme file.
1516
+ fn scheme_mod_ident(scheme: &str, j: usize) -> String {
1517
+ format!("__scheme_{}_{}", scheme, j)
1518
+ }
1519
+
1520
+ /// Emit one `mod __scheme_<name>_<j> { … }` per collected scheme file (Gba only), from the
1521
+ /// scheme's `gba` emit template: its `use`s, module body (the include macro, `{path}` → the
1522
+ /// absolute file), and a same-module accessor (which can see the macro's private statics — the
1523
+ /// only way a tagless image's sprites escape). Registration into the runtime arena happens in
1524
+ /// the entry below.
1525
+ /// Collect every NON-ASCII character appearing in the program's string literals, sorted+unique.
1526
+ /// Fed to schemes via `{charset}` (see `render_template`) so a `font<N>:` import can bake only the
1527
+ /// glyphs actually used. ASCII is always baked by the scheme, so it's excluded here. Runtime-built
1528
+ /// strings (concatenation, numbers) resolve to ASCII, which is covered; a non-ASCII char reachable
1529
+ /// only through a computed string is the rare gap (documented — add it to a literal if needed).
1530
+ fn collect_string_charset(stmts: &[Statement]) -> String {
1531
+ let mut set = std::collections::BTreeSet::new();
1532
+ fn add(s: &str, set: &mut std::collections::BTreeSet<char>) {
1533
+ for c in s.chars() {
1534
+ if (c as u32) >= 0x7F {
1535
+ set.insert(c);
1536
+ }
1537
+ }
1538
+ }
1539
+ fn walk(stmts: &[Statement], set: &mut std::collections::BTreeSet<char>) {
1540
+ for s in stmts {
1541
+ Codegen::for_each_stmt_expr(s, &mut |e| {
1542
+ Codegen::nv_for_each_subexpr(e, &mut |x| {
1543
+ if let Expr::Literal { value: Literal::String(str), .. } = x {
1544
+ add(str, set);
1545
+ }
1546
+ if let Expr::TemplateLiteral { quasis, .. } = x {
1547
+ for q in quasis {
1548
+ add(q, set);
1549
+ }
1550
+ }
1551
+ });
1552
+ });
1553
+ Codegen::for_each_child_stmt_list(s, &mut |list| walk(list, set));
1554
+ }
1555
+ }
1556
+ walk(stmts, &mut set);
1557
+ set.into_iter().collect()
1558
+ }
1559
+
1560
+ fn emit_scheme_modules(&mut self) -> Result<(), CompileError> {
1561
+ let files = self.scheme_files.clone();
1562
+ for (j, (scheme, path)) in files.iter().enumerate() {
1563
+ let emit = self.scheme_target_emit(scheme)?;
1564
+ let module_ident = Self::scheme_mod_ident(scheme, j);
1565
+ self.writeln(&format!("mod {} {{", module_ident));
1566
+ self.indent += 1;
1567
+ for u in &emit.uses {
1568
+ self.writeln(u);
1569
+ }
1570
+ self.writeln(&crate::schemes::render_template(
1571
+ &emit.module_body,
1572
+ path,
1573
+ &module_ident,
1574
+ &self.string_charset,
1575
+ ));
1576
+ if !emit.accessor.is_empty() {
1577
+ self.writeln(&emit.accessor);
1578
+ }
1579
+ self.indent -= 1;
1580
+ self.writeln("}");
1581
+ }
1582
+ if !files.is_empty() {
1583
+ self.writeln("");
1584
+ }
1585
+ Ok(())
1586
+ }
1587
+
1588
+ /// The `gba` [`crate::schemes::TargetEmit`] for `scheme`, or a compile error if the scheme has
1589
+ /// no template for this target.
1590
+ fn scheme_target_emit(
1591
+ &self,
1592
+ scheme: &str,
1593
+ ) -> Result<crate::schemes::TargetEmit, CompileError> {
1594
+ crate::schemes::with_active(|reg| {
1595
+ reg.matches(&format!("{}:", scheme))
1596
+ .and_then(|s| s.targets.get("gba").cloned())
1597
+ })
1598
+ .ok_or_else(|| CompileError {
1599
+ message: format!(
1600
+ "import scheme '{}:' has no emit template for target 'gba'",
1601
+ scheme
1602
+ ),
1603
+ span: None,
1604
+ })
1605
+ }
1606
+
1349
1607
  fn emit_named_struct_decls(&mut self) {
1350
1608
  // Snapshot keys + values so we can mutate `self` (writing the
1351
1609
  // emitted source) inside the loop.
@@ -2245,15 +2503,38 @@ impl Codegen {
2245
2503
  }
2246
2504
 
2247
2505
  fn emit_program(&mut self, program: &Program) -> Result<(), CompileError> {
2506
+ // Only GBA `font<N>:`/`emoji:` schemes consume `{charset}` (and both `render_template`
2507
+ // callers that read `string_charset` are Gba-gated), so skip the whole-program string walk
2508
+ // for every other target — its result is never read off-GBA.
2509
+ if self.emit_mode == crate::NativeEmitMode::Gba {
2510
+ self.string_charset = Self::collect_string_charset(&program.statements);
2511
+ }
2248
2512
  self.is_async = program_uses_async(program);
2249
2513
  self.program_has_jsx = tishlang_ui::jsx::program_contains_jsx(program);
2250
2514
  self.program_fun_decl_names = tishlang_ui::jsx::collect_fun_decl_names(program);
2251
2515
  self.program_uses_document = crate::resolve::program_uses_document(program);
2252
- self.write("#![allow(unused, non_snake_case)]\n\n");
2253
- self.write("use std::cell::RefCell;\n");
2254
- self.write("use std::rc::Rc;\n");
2255
- self.write("use std::sync::Arc;\n");
2256
- 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, encode_uri_component as tish_encode_uri_component, decode_uri_component as tish_decode_uri_component, 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, math_sinh as tish_math_sinh, math_cosh as tish_math_cosh, math_tanh as tish_math_tanh, math_asinh as tish_math_asinh, math_acosh as tish_math_acosh, math_atanh as tish_math_atanh, math_cbrt as tish_math_cbrt, math_log2 as tish_math_log2, math_log10 as tish_math_log10, math_expm1 as tish_math_expm1, math_log1p as tish_math_log1p, math_clz32 as tish_math_clz32, math_fround as tish_math_fround, math_hypot as tish_math_hypot, math_atan2 as tish_math_atan2, math_asin as tish_math_asin, math_acos as tish_math_acos, math_atan as tish_math_atan, array_is_array as tish_array_is_array, array_of as tish_array_of, array_from as tish_array_from, object_is as tish_object_is, object_has_own as tish_object_has_own, structured_clone as tish_structured_clone, array_construct as tish_array_construct, string_from_char_code as tish_string_from_char_code, string_convert as tish_string_convert, number_convert as tish_number_convert, number_is_integer as tish_number_is_integer, number_is_safe_integer as tish_number_is_safe_integer, number_is_nan as tish_number_is_nan, number_is_finite as tish_number_is_finite, 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_error_constructor, tish_date_constructor, tish_set_constructor, tish_map_constructor, map_get as tish_map_get, map_has as tish_map_has, map_set as tish_map_set, map_values as tish_map_values, tish_float64_array_constructor, tish_float32_array_constructor, tish_int8_array_constructor, tish_uint8_array_constructor, tish_uint8_clamped_array_constructor, tish_int16_array_constructor, tish_uint16_array_constructor, tish_int32_array_constructor, tish_uint32_array_constructor, tish_audio_context_constructor, ObjectMap, ObjectData, PropMap, TishError, Value, VmRef};\n");
2516
+ if self.emit_mode == crate::NativeEmitMode::Gba {
2517
+ // no_std GBA ROM: alloc-based, Arc aliases to Rc in the facade. The
2518
+ // prelude `use tishlang_runtime::{…}` below is unchanged — those names
2519
+ // are re-exported by the tishlang_runtime_gba facade.
2520
+ self.write("#![no_std]\n#![no_main]\n#![allow(unused, non_snake_case)]\n\n");
2521
+ self.write("extern crate alloc;\n");
2522
+ self.write("use core::cell::RefCell;\n");
2523
+ self.write("use alloc::rc::Rc;\n");
2524
+ self.write("use alloc::boxed::Box;\n");
2525
+ self.write("use alloc::vec::Vec;\n");
2526
+ self.write("use alloc::string::{String, ToString};\n");
2527
+ self.write("use alloc::{vec, format};\n");
2528
+ self.write("use tishlang_runtime::Arc;\n");
2529
+ // f64 transcendentals for inline math (`Math.sqrt(x)` → `x.sqrt()`).
2530
+ self.write("use tishlang_runtime::FloatExt;\n");
2531
+ } else {
2532
+ self.write("#![allow(unused, non_snake_case)]\n\n");
2533
+ self.write("use std::cell::RefCell;\n");
2534
+ self.write("use std::rc::Rc;\n");
2535
+ self.write("use std::sync::Arc;\n");
2536
+ }
2537
+ 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, encode_uri_component as tish_encode_uri_component, decode_uri_component as tish_decode_uri_component, 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, math_sinh as tish_math_sinh, math_cosh as tish_math_cosh, math_tanh as tish_math_tanh, math_asinh as tish_math_asinh, math_acosh as tish_math_acosh, math_atanh as tish_math_atanh, math_cbrt as tish_math_cbrt, math_log2 as tish_math_log2, math_log10 as tish_math_log10, math_expm1 as tish_math_expm1, math_log1p as tish_math_log1p, math_clz32 as tish_math_clz32, math_fround as tish_math_fround, math_hypot as tish_math_hypot, math_atan2 as tish_math_atan2, math_asin as tish_math_asin, math_acos as tish_math_acos, math_atan as tish_math_atan, array_is_array as tish_array_is_array, array_of as tish_array_of, array_from as tish_array_from, object_is as tish_object_is, object_has_own as tish_object_has_own, structured_clone as tish_structured_clone, array_construct as tish_array_construct, string_from_char_code as tish_string_from_char_code, string_convert as tish_string_convert, number_convert as tish_number_convert, number_is_integer as tish_number_is_integer, number_is_safe_integer as tish_number_is_safe_integer, number_is_nan as tish_number_is_nan, number_is_finite as tish_number_is_finite, object_assign as tish_object_assign, object_freeze as tish_object_freeze, object_is_frozen as tish_object_is_frozen, 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_error_constructor, tish_date_constructor, tish_set_constructor, tish_map_constructor, map_get as tish_map_get, map_has as tish_map_has, map_set as tish_map_set, map_values as tish_map_values, tish_float64_array_constructor, tish_float32_array_constructor, tish_int8_array_constructor, tish_uint8_array_constructor, tish_uint8_clamped_array_constructor, tish_int16_array_constructor, tish_uint16_array_constructor, tish_int32_array_constructor, tish_uint32_array_constructor, tish_audio_context_constructor, ObjectMap, ObjectData, PropMap, TishError, Value, VmRef};\n");
2257
2538
  if self.program_has_jsx {
2258
2539
  self.write("use tishlang_ui::{fragment_value, install_thread_local_host, native_create_root, native_use_state, ui_h, ui_text, HeadlessHost};\n");
2259
2540
  }
@@ -2309,6 +2590,16 @@ impl Codegen {
2309
2590
  // and `x.field` becomes a direct field access.
2310
2591
  self.emit_named_struct_decls();
2311
2592
 
2593
+ // `asset:` imports (Gba only): collect the distinct files (each an i32 sheet handle) and
2594
+ // emit one `mod __asset_F { include_aseprite_inner!("<abspath>"); pub fn sheet() }` block.
2595
+ // The inner macro's `SPRITES` static is private, so the same-module `sheet()` accessor is
2596
+ // how the frames escape the module (a tagless image exposes no public tag). Registration
2597
+ // into the facade arena happens in the entry below, before `run()`.
2598
+ if self.emit_mode == crate::NativeEmitMode::Gba {
2599
+ self.collect_scheme_files(&program.statements);
2600
+ self.emit_scheme_modules()?;
2601
+ }
2602
+
2312
2603
  if self.is_async && self.emit_mode == crate::NativeEmitMode::DesktopBin {
2313
2604
  self.writeln("#[tokio::main]");
2314
2605
  self.writeln("async fn main() {");
@@ -2331,9 +2622,36 @@ impl Codegen {
2331
2622
  self.writeln("}");
2332
2623
  self.writeln("");
2333
2624
  }
2334
- // M5 (dark-shipped behind TISH_NATIVE_FN): emit a parallel native `fn f_native` for each
2335
- // eligible top-level numeric fn at top level; direct calls route to it in emit_typed_expr.
2336
- if crate::native_opts_enabled() {
2625
+ if self.emit_mode == crate::NativeEmitMode::Gba {
2626
+ // agb runtime entry: hand the peripherals to the facade, run the
2627
+ // program body, then halt. (Async lands in P5 — `run()` is sync here.)
2628
+ self.writeln("#[agb::entry]");
2629
+ self.writeln("fn agb_main(gba: agb::Gba) -> ! {");
2630
+ self.indent += 1;
2631
+ self.writeln("tishlang_runtime::gba::init(gba);");
2632
+ // Register each scheme file into its runtime arena, in first-seen order, so the handle a
2633
+ // tish name binds to (its per-scheme index) matches the arena slot it gets here.
2634
+ let scheme_files = self.scheme_files.clone();
2635
+ for (j, (scheme, _path)) in scheme_files.iter().enumerate() {
2636
+ let emit = self.scheme_target_emit(scheme)?;
2637
+ if !emit.register.is_empty() {
2638
+ let module_ident = Self::scheme_mod_ident(scheme, j);
2639
+ let call = crate::schemes::render_template(&emit.register, "", &module_ident, &self.string_charset);
2640
+ self.writeln(&format!("{};", call));
2641
+ }
2642
+ }
2643
+ self.writeln("let _ = run();");
2644
+ self.writeln("tishlang_runtime::gba::halt()");
2645
+ self.indent -= 1;
2646
+ self.writeln("}");
2647
+ self.writeln("");
2648
+ }
2649
+ // M5 (default-on via native_opts_enabled; TISH_NATIVE_OPT=0 opts out): emit a parallel
2650
+ // native `fn f_native` for each eligible top-level numeric fn at top level; direct calls
2651
+ // route to it in emit_typed_expr.
2652
+ // Gba mode: these passes emit `thread_local!` and other std-isms; gate them off initially
2653
+ // (they are pure perf — correctness is unaffected). Re-audit for no_std later.
2654
+ if crate::native_opts_enabled() && self.emit_mode != crate::NativeEmitMode::Gba {
2337
2655
  self.native_numeric_globals =
2338
2656
  Self::collect_native_numeric_globals(&program.statements);
2339
2657
  if !self.native_numeric_globals.is_empty() {
@@ -2378,21 +2696,21 @@ impl Codegen {
2378
2696
  self.writeln("");
2379
2697
  }
2380
2698
  }
2381
- // #177 (S-E/S-F, dark-shipped behind TISH_AGGREGATE_INFER): de-virtualize the nbody-shape
2699
+ // #177 (S-E/S-F, default-on via native_opts_enabled): de-virtualize the nbody-shape
2382
2700
  // aggregate fns into native Rust free fns operating on an unboxed `Vec<TishStruct_alias>`
2383
2701
  // threaded by reference. Computed + emitted here (before `run()`); if any fn can't be
2384
2702
  // lowered the whole path is disabled and we fall back to the boxed closures unchanged.
2385
2703
  if crate::native_opts_enabled() {
2386
2704
  self.setup_aggregate_fns(program);
2387
2705
  }
2388
- // #178 (behind TISH_REC_STRUCT): de-virtualize recursive-struct builders/consumers into
2706
+ // #178 (default-on via native_opts_enabled): de-virtualize recursive-struct builders/consumers into
2389
2707
  // native free fns over `Option<Box<T>>` structs (the real binary_trees fix). Structural,
2390
2708
  // name-independent; emits before `run()` with scratch-buffer rollback so any unsupported
2391
2709
  // construct cleanly disables the path and falls back to the boxed closures unchanged.
2392
2710
  if crate::native_opts_enabled() {
2393
2711
  self.setup_rec_struct_plan(program);
2394
2712
  }
2395
- // #175 (behind TISH_NATIVE_FN): de-virtualize fns over plain `number[]`/`boolean[]` params
2713
+ // #175 (default-on via native_opts_enabled): de-virtualize fns over plain `number[]`/`boolean[]` params
2396
2714
  // into native free fns threaded by `&/&mut Vec<T>` (spectral_norm / queens). Runs after the
2397
2715
  // aggregate path so it can skip any fn that path already claimed; emits before `run()` with
2398
2716
  // its own scratch-buffer rollback so a failure leaves the boxed closures untouched.
@@ -2403,7 +2721,7 @@ impl Codegen {
2403
2721
  self.inline_fns = Self::collect_inline_fns(program);
2404
2722
  self.setup_native_vec_fns(program);
2405
2723
  }
2406
- // #320 (behind TISH_NATIVE_ARR_PARAM): normal fns taking read-only `number[]` params get
2724
+ // #320 (default-on via native_opts_enabled): normal fns taking read-only `number[]` params get
2407
2725
  // those params unboxed to native owned `Vec<f64>` (boxed body + boxed return otherwise), so
2408
2726
  // `seq[i+j]` indexes natively. The SAME pure detection infer reads to keep the caller's array
2409
2727
  // `number[]`, so the two never disagree about which args pass as native arrays.
@@ -2441,7 +2759,9 @@ impl Codegen {
2441
2759
  }
2442
2760
  self.nonneg_locals = self.collect_nonneg_locals(&program.statements);
2443
2761
  self.shift_half_of = Self::collect_shift_half_of(&program.statements);
2444
- if self.is_async {
2762
+ // Gba mode is sync in P2 (real async lowering is P5); `std::error::Error` in
2763
+ // the signature is rewritten to `core::error::Error` by the no_std post-pass.
2764
+ if self.is_async && self.emit_mode != crate::NativeEmitMode::Gba {
2445
2765
  self.writeln("async fn run() -> Result<(), Box<dyn std::error::Error>> {");
2446
2766
  } else if self.emit_mode == crate::NativeEmitMode::EmbeddedLib {
2447
2767
  self.writeln("pub fn run() -> Result<(), Box<dyn std::error::Error>> {");
@@ -2545,6 +2865,15 @@ impl Codegen {
2545
2865
  }
2546
2866
  self.writeln("(Arc::from(\"PI\"), Value::Number(std::f64::consts::PI)),");
2547
2867
  self.writeln("(Arc::from(\"E\"), Value::Number(std::f64::consts::E)),");
2868
+ // #539: the remaining ECMAScript Math value constants (were missing → `null`).
2869
+ self.writeln("(Arc::from(\"LN2\"), Value::Number(std::f64::consts::LN_2)),");
2870
+ self.writeln("(Arc::from(\"LN10\"), Value::Number(std::f64::consts::LN_10)),");
2871
+ self.writeln("(Arc::from(\"LOG2E\"), Value::Number(std::f64::consts::LOG2_E)),");
2872
+ self.writeln("(Arc::from(\"LOG10E\"), Value::Number(std::f64::consts::LOG10_E)),");
2873
+ self.writeln("(Arc::from(\"SQRT2\"), Value::Number(std::f64::consts::SQRT_2)),");
2874
+ self.writeln(
2875
+ "(Arc::from(\"SQRT1_2\"), Value::Number(std::f64::consts::FRAC_1_SQRT_2)),",
2876
+ );
2548
2877
  self.indent -= 1;
2549
2878
  self.writeln("]));");
2550
2879
  self.writeln("let JSON = Value::object(ObjectMap::from([");
@@ -2623,6 +2952,12 @@ impl Codegen {
2623
2952
  self.writeln(
2624
2953
  "(Arc::from(\"getOwnPropertyNames\"), Value::native(|args: &[Value]| tish_object_keys(args))),",
2625
2954
  );
2955
+ self.writeln(
2956
+ "(Arc::from(\"freeze\"), Value::native(|args: &[Value]| tish_object_freeze(args))),",
2957
+ );
2958
+ self.writeln(
2959
+ "(Arc::from(\"isFrozen\"), Value::native(|args: &[Value]| tish_object_is_frozen(args))),",
2960
+ );
2626
2961
  self.writeln(
2627
2962
  "(Arc::from(\"keys\"), Value::native(|args: &[Value]| tish_object_keys(args))),",
2628
2963
  );
@@ -2820,6 +3155,14 @@ impl Codegen {
2820
3155
  self.indent -= 1;
2821
3156
  self.writeln("}");
2822
3157
  }
3158
+ // Gba mode: rewrite the handful of `std::` paths that survive in emitted
3159
+ // code into their `core::` equivalents (the crate is `#![no_std]`). These
3160
+ // are specific Rust path patterns, not substrings that occur in escaped
3161
+ // user string literals, so a targeted textual pass is safe. Any new site
3162
+ // is caught by the thumbv4t corpus build.
3163
+ if self.emit_mode == crate::NativeEmitMode::Gba {
3164
+ self.output = gba_no_std_rewrite(core::mem::take(&mut self.output));
3165
+ }
2823
3166
  Ok(())
2824
3167
  }
2825
3168
 
@@ -2842,7 +3185,12 @@ impl Codegen {
2842
3185
  /// unchanged, and `PropMap` dedup + hidden-class shapes key by text, not pointer — so shapes,
2843
3186
  /// insertion order, and `Object.keys` are all unaffected. Same inline-`static` idiom as the
2844
3187
  /// per-site `PropIC` caches.
2845
- fn cached_object_key(k: &str) -> String {
3188
+ fn cached_object_key(&self, k: &str) -> String {
3189
+ if self.emit_mode == crate::NativeEmitMode::Gba {
3190
+ // No `OnceLock` on no_std GBA; emit a direct interned key (`Arc` = `Rc`).
3191
+ // Object-literal keys in a hot loop are a later perf item on GBA.
3192
+ return format!("Arc::from({:?})", k);
3193
+ }
2846
3194
  format!(
2847
3195
  "{{ static K: ::std::sync::OnceLock<::std::sync::Arc<str>> = \
2848
3196
  ::std::sync::OnceLock::new(); K.get_or_init(|| ::std::sync::Arc::from({:?})).clone() }}",
@@ -2979,7 +3327,24 @@ impl Codegen {
2979
3327
  }
2980
3328
  }
2981
3329
  }
2982
- if matches!(rust_type, RustType::F64 | RustType::Bool | RustType::String) {
3330
+ // Narrow int local (`let hp: u8 = …; hp = hp - 1`): compute the RHS (promoted to
3331
+ // `f64` by the binary emitter) and truncate-cast to the width on store. No
3332
+ // int32-register trick — these are plain storage locals.
3333
+ if rust_type.is_narrow_int() {
3334
+ let escaped = Self::escape_ident(name.as_ref());
3335
+ let native_val = self.emit_coerced_native(value, &rust_type)?;
3336
+ if self.refcell_wrapped_vars.contains(name.as_ref()) {
3337
+ return Ok(format!(
3338
+ "{{ let _t = {}; *{}.borrow_mut() = _t; }}",
3339
+ native_val, escaped
3340
+ ));
3341
+ }
3342
+ return Ok(format!("{} = {}", escaped, native_val));
3343
+ }
3344
+ if matches!(
3345
+ rust_type,
3346
+ RustType::F64 | RustType::Fixed | RustType::Bool | RustType::String
3347
+ ) {
2983
3348
  if let Expr::Index { object, index, .. } = value.as_ref() {
2984
3349
  if let Expr::Ident { name: arr_name, .. } = object.as_ref() {
2985
3350
  if self.index_in_bounds(index, arr_name.as_ref()) {
@@ -3029,6 +3394,47 @@ impl Codegen {
3029
3394
  }
3030
3395
  // `s += x` etc. in statement position: native f64 compound op, no boxed return.
3031
3396
  Expr::CompoundAssign { name, op, value, .. } => {
3397
+ // Narrow int compound-assign (`hp -= 3`): compute in `f64` (JS Number semantics),
3398
+ // truncate-cast back to the width — same model as a narrow-int plain assign, no box.
3399
+ if self.type_context.get_type(name.as_ref()).is_narrow_int() {
3400
+ let width = self.type_context.get_type(name.as_ref()).to_rust_type_str();
3401
+ let n = Self::escape_ident(name.as_ref()).into_owned();
3402
+ let is_refcell = self.refcell_wrapped_vars.contains(name.as_ref());
3403
+ let (rhs_code, rhs_ty) = self.emit_typed_expr(value)?;
3404
+ let rhs_f64 = if rhs_ty == RustType::F64 {
3405
+ rhs_code
3406
+ } else if rhs_ty.is_integer_scalar() {
3407
+ format!("(({}) as f64)", rhs_code)
3408
+ } else {
3409
+ let rhs_val = if rhs_ty.is_native() {
3410
+ rhs_ty.to_value_expr(&rhs_code)
3411
+ } else {
3412
+ rhs_code
3413
+ };
3414
+ format!("(match &({}) {{ Value::Number(n) => *n, v => panic!(\"compound assign: expected number, got {{:?}}\", v) }})", rhs_val)
3415
+ };
3416
+ let op_str = match op {
3417
+ CompoundOp::Add => "+",
3418
+ CompoundOp::Sub => "-",
3419
+ CompoundOp::Mul => "*",
3420
+ CompoundOp::Div => "/",
3421
+ CompoundOp::Mod => "%",
3422
+ };
3423
+ let read = if is_refcell {
3424
+ format!("(*{}.borrow())", n)
3425
+ } else {
3426
+ n.clone()
3427
+ };
3428
+ let new_val =
3429
+ format!("((({}) as f64) {} {}) as {}", read, op_str, rhs_f64, width);
3430
+ if is_refcell {
3431
+ return Ok(format!(
3432
+ "{{ let _t = {}; *{}.borrow_mut() = _t; }}",
3433
+ new_val, n
3434
+ ));
3435
+ }
3436
+ return Ok(format!("{} = {}", n, new_val));
3437
+ }
3032
3438
  if self.type_context.get_type(name.as_ref()) == RustType::F64 {
3033
3439
  let n = Self::escape_ident(name.as_ref());
3034
3440
  let is_refcell = self.refcell_wrapped_vars.contains(name.as_ref());
@@ -5057,10 +5463,10 @@ impl Codegen {
5057
5463
  // re-boxing each element to `Value::Number` for the loop body.
5058
5464
  self.writeln("Value::NumberArray(ref _arr) => {");
5059
5465
  self.indent += 1;
5060
- self.writeln("for _v in _arr.borrow().iter() {");
5466
+ self.writeln("for _v in _arr.borrow().to_values() {");
5061
5467
  self.indent += 1;
5062
5468
  self.writeln(&format!(
5063
- "let {} = Value::Number(*_v);",
5469
+ "let {} = _v;",
5064
5470
  Self::escape_ident(name.as_ref())
5065
5471
  ));
5066
5472
  self.emit_statement(body)?;
@@ -5843,14 +6249,15 @@ impl Codegen {
5843
6249
  sibling_escaped, sibling_escaped
5844
6250
  ));
5845
6251
  }
5846
- // Extract just the parameter names (type annotations are parsed but not used in codegen yet)
6252
+ // Extract just the parameter names for capture bookkeeping (typed params get
6253
+ // native shadows below — the M1 block)
5847
6254
  let current_param_names: Vec<String> = params
5848
6255
  .iter()
5849
6256
  .flat_map(|p| p.bound_names())
5850
6257
  .map(|n| n.to_string())
5851
6258
  .collect();
5852
6259
  let formal_span = *span;
5853
- // M1 (keystone, dark-shipped behind TISH_PARAM_NATIVE): a typed scalar param
6260
+ // M1 (keystone; default-on via native_opts_enabled): a typed scalar param
5854
6261
  // normally arrives boxed (`args.get(i).cloned()`), which poisons native math in
5855
6262
  // the body (e.g. `i*N+k` boxes). Bind a *native shadow* — coerce once to f64/
5856
6263
  // bool/String — so the body lowers it like a native local. Conservative: only
@@ -5893,7 +6300,9 @@ impl Codegen {
5893
6300
  // `arr[oob]`→NaN) is unreachable for sound inputs but never panics.
5894
6301
  self.writeln(&format!(
5895
6302
  "let mut {}: Vec<f64> = match args.get({}) {{ \
5896
- Some(Value::NumberArray(a)) => a.borrow().clone(), \
6303
+ Some(Value::NumberArray(a)) => match &*a.borrow() {{ \
6304
+ tishlang_runtime::NumArrayBacking::Packed(p) => p.clone(), \
6305
+ tishlang_runtime::NumArrayBacking::Boxed(b) => b.iter().map(|v| match v {{ Value::Number(n) => *n, _ => f64::NAN }}).collect() }}, \
5897
6306
  Some(Value::Array(arr)) => arr.borrow().iter().map(|v| match v {{ Value::Number(n) => *n, _ => f64::NAN }}).collect(), \
5898
6307
  _ => Vec::new() }};",
5899
6308
  Self::escape_ident(tp.name.as_ref()),
@@ -7261,7 +7670,7 @@ impl Codegen {
7261
7670
  }
7262
7671
  "reduce" => {
7263
7672
  let initial = arg_exprs.get(1).cloned().unwrap_or_else(|| "Value::Null".to_string());
7264
- // Fused reduce (TISH_FUSED_HOF): `arr.reduce((acc, x) => acc OP x, init)`
7673
+ // Fused reduce (default-on via native_opts_enabled): `arr.reduce((acc, x) => acc OP x, init)`
7265
7674
  // with a plain binop of the two params → a native fold using the SAME
7266
7675
  // runtime Value op the closure body would, eliminating the per-element
7267
7676
  // `value_call`. Sound (identical Value semantics, incl. string `+`).
@@ -7600,10 +8009,11 @@ impl Codegen {
7600
8009
  tishlang_runtime::get_prop(&o, {}) }} }}",
7601
8010
  obj, key
7602
8011
  )
7603
- } else if ic_eligible {
8012
+ } else if ic_eligible && self.emit_mode != crate::NativeEmitMode::Gba {
7604
8013
  // Per-site inline cache: a block-scoped `static` cell unique to this member-read
7605
8014
  // site (#179). Caches (shape, slot) so repeat reads skip the key scan. Behaviour is
7606
8015
  // identical to `get_prop` — non-cacheable objects fall through inside `get_prop_ic`.
8016
+ // Gba: `PropIC` uses atomics (unavailable on thumbv4t) — use the plain path.
7607
8017
  format!(
7608
8018
  "{{ static IC: tishlang_runtime::PropIC = tishlang_runtime::PropIC::new(); \
7609
8019
  tishlang_runtime::get_prop_ic(&{}, {}, &IC) }}",
@@ -7730,7 +8140,7 @@ impl Codegen {
7730
8140
  match prop {
7731
8141
  ObjectProp::KeyValue(k, v, _) => {
7732
8142
  let val = self.emit_expr(v)?;
7733
- let key = Self::cached_object_key(k.as_ref());
8143
+ let key = self.cached_object_key(k.as_ref());
7734
8144
  if self.should_clone(v) {
7735
8145
  parts.push(format!("_obj.insert({}, ({}).clone());", key, val));
7736
8146
  } else {
@@ -7753,7 +8163,7 @@ impl Codegen {
7753
8163
  init = format!("let mut _obj: PropMap = PropMap::with_capacity({});", literal_keys);
7754
8164
  }
7755
8165
  format!(
7756
- "{{ {} {} Value::Object(VmRef::new(ObjectData {{ strings: _obj, symbols: None }})) }}",
8166
+ "{{ {} {} Value::Object(VmRef::new(ObjectData {{ strings: _obj, symbols: None, frozen: false }})) }}",
7757
8167
  init,
7758
8168
  parts.join(" ")
7759
8169
  )
@@ -7762,7 +8172,7 @@ impl Codegen {
7762
8172
  for prop in props {
7763
8173
  if let ObjectProp::KeyValue(k, v, _) = prop {
7764
8174
  let val = self.emit_expr(v)?;
7765
- let key = Self::cached_object_key(k.as_ref());
8175
+ let key = self.cached_object_key(k.as_ref());
7766
8176
  if self.should_clone(v) {
7767
8177
  parts.push(format!("({}, ({}).clone())", key, val));
7768
8178
  } else {
@@ -8258,6 +8668,27 @@ impl Codegen {
8258
8668
  )
8259
8669
  }
8260
8670
  Expr::NativeModuleLoad { spec, export_name, .. } => {
8671
+ // A scheme import (`asset:`, or any project-declared scheme) binds to an i32 handle
8672
+ // — its per-scheme index, registered into a runtime arena by the generated
8673
+ // `agb_main`. Only valid in Gba mode; no other target bakes scheme files into a ROM.
8674
+ if crate::schemes::is_scheme_import(spec.as_ref()) {
8675
+ if self.emit_mode != crate::NativeEmitMode::Gba {
8676
+ return Err(CompileError {
8677
+ message: format!(
8678
+ "scheme import '{}' requires --target gba",
8679
+ spec.as_ref()
8680
+ ),
8681
+ span: None,
8682
+ });
8683
+ }
8684
+ let handle = self.scheme_handle_of(spec.as_ref()).ok_or_else(|| {
8685
+ CompileError {
8686
+ message: format!("internal: scheme import '{}' not collected", spec.as_ref()),
8687
+ span: None,
8688
+ }
8689
+ })?;
8690
+ return Ok(format!("Value::Number({} as f64)", handle));
8691
+ }
8261
8692
  self.native_module_rust_init(spec.as_ref(), export_name.as_ref())
8262
8693
  .ok_or_else(|| CompileError {
8263
8694
  message: if crate::resolve::is_builtin_native_spec(spec.as_ref()) {
@@ -9519,6 +9950,187 @@ impl Codegen {
9519
9950
  }
9520
9951
  }
9521
9952
 
9953
+ /// Every name BOUND anywhere inside `s` — `let`/`const` (incl. destructuring), fn/arrow
9954
+ /// params, `for…of`/`for…in` loop vars, `catch` params, and nested `FunDecl` names —
9955
+ /// descending into nested arrow and function bodies. Unlike `collect_local_var_names`
9956
+ /// (which skips arrow bodies) or `collect_binding_names` (which skips `let`/`const`),
9957
+ /// this is the complete binder set of the subtree. Used by the arrow emitter's implicit
9958
+ /// env-capture fallback so a NESTED closure's own bindings are never mistaken for capture
9959
+ /// requirements of the closure being emitted (#467: `(a)=>(b)=>(c)=> …` E0425'd because
9960
+ /// the middle arrow tried to capture the innermost arrow's param `c`).
9961
+ fn collect_bound_names_deep(s: &Statement, out: &mut HashSet<String>) {
9962
+ use Statement::*;
9963
+ let ce = Self::collect_bound_names_deep_expr;
9964
+ match s {
9965
+ VarDecl { name, init, .. } => {
9966
+ out.insert(name.to_string());
9967
+ if let Some(i) = init {
9968
+ ce(i, out);
9969
+ }
9970
+ }
9971
+ VarDeclDestructure { pattern, init, .. } => {
9972
+ Self::collect_destruct_names(pattern, out);
9973
+ ce(init, out);
9974
+ }
9975
+ FunDecl { name, params, body, .. } => {
9976
+ out.insert(name.to_string());
9977
+ for p in params {
9978
+ for n in p.bound_names() {
9979
+ out.insert(n.to_string());
9980
+ }
9981
+ }
9982
+ Self::collect_bound_names_deep(body, out);
9983
+ }
9984
+ ForOf { name, iterable, body, .. } => {
9985
+ out.insert(name.to_string());
9986
+ ce(iterable, out);
9987
+ Self::collect_bound_names_deep(body, out);
9988
+ }
9989
+ ForIn { name, object, body, .. } => {
9990
+ out.insert(name.to_string());
9991
+ ce(object, out);
9992
+ Self::collect_bound_names_deep(body, out);
9993
+ }
9994
+ Try { body, catch_param, catch_body, finally_body, .. } => {
9995
+ if let Some(cp) = catch_param {
9996
+ out.insert(cp.to_string());
9997
+ }
9998
+ Self::collect_bound_names_deep(body, out);
9999
+ if let Some(c) = catch_body {
10000
+ Self::collect_bound_names_deep(c, out);
10001
+ }
10002
+ if let Some(f) = finally_body {
10003
+ Self::collect_bound_names_deep(f, out);
10004
+ }
10005
+ }
10006
+ Block { statements, .. } | Multi { statements, .. } => {
10007
+ statements.iter().for_each(|x| Self::collect_bound_names_deep(x, out))
10008
+ }
10009
+ If { cond, then_branch, else_branch, .. } => {
10010
+ ce(cond, out);
10011
+ Self::collect_bound_names_deep(then_branch, out);
10012
+ if let Some(eb) = else_branch {
10013
+ Self::collect_bound_names_deep(eb, out);
10014
+ }
10015
+ }
10016
+ While { cond, body, .. } | DoWhile { body, cond, .. } => {
10017
+ ce(cond, out);
10018
+ Self::collect_bound_names_deep(body, out);
10019
+ }
10020
+ For { init, cond, update, body, .. } => {
10021
+ if let Some(i) = init {
10022
+ Self::collect_bound_names_deep(i, out);
10023
+ }
10024
+ if let Some(c) = cond {
10025
+ ce(c, out);
10026
+ }
10027
+ if let Some(u) = update {
10028
+ ce(u, out);
10029
+ }
10030
+ Self::collect_bound_names_deep(body, out);
10031
+ }
10032
+ ExprStmt { expr, .. } => ce(expr, out),
10033
+ Return { value: Some(v), .. } => ce(v, out),
10034
+ Throw { value, .. } => ce(value, out),
10035
+ Switch { expr, cases, default_body, .. } => {
10036
+ ce(expr, out);
10037
+ for (g, b) in cases {
10038
+ if let Some(ge) = g {
10039
+ ce(ge, out);
10040
+ }
10041
+ b.iter().for_each(|x| Self::collect_bound_names_deep(x, out));
10042
+ }
10043
+ if let Some(d) = default_body {
10044
+ d.iter().for_each(|x| Self::collect_bound_names_deep(x, out));
10045
+ }
10046
+ }
10047
+ Export { declaration, .. } => {
10048
+ if let tishlang_ast::ExportDeclaration::Named(inner) = declaration.as_ref() {
10049
+ Self::collect_bound_names_deep(inner, out);
10050
+ }
10051
+ }
10052
+ _ => {}
10053
+ }
10054
+ }
10055
+
10056
+ /// Expression side of [`Self::collect_bound_names_deep`]: recurse every child; an
10057
+ /// `ArrowFunction` contributes its params and descends into its body.
10058
+ fn collect_bound_names_deep_expr(e: &Expr, out: &mut HashSet<String>) {
10059
+ let ce = Self::collect_bound_names_deep_expr;
10060
+ match e {
10061
+ Expr::ArrowFunction { params, body, .. } => {
10062
+ for p in params {
10063
+ for n in p.bound_names() {
10064
+ out.insert(n.to_string());
10065
+ }
10066
+ }
10067
+ match body {
10068
+ ArrowBody::Expr(x) => ce(x, out),
10069
+ ArrowBody::Block(b) => Self::collect_bound_names_deep(b, out),
10070
+ }
10071
+ }
10072
+ Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
10073
+ ce(callee, out);
10074
+ for a in args {
10075
+ if let CallArg::Expr(x) | CallArg::Spread(x) = a {
10076
+ ce(x, out);
10077
+ }
10078
+ }
10079
+ }
10080
+ Expr::Binary { left, right, .. } | Expr::NullishCoalesce { left, right, .. } => {
10081
+ ce(left, out);
10082
+ ce(right, out);
10083
+ }
10084
+ Expr::Unary { operand, .. } => ce(operand, out),
10085
+ Expr::TypeOf { operand, .. } => ce(operand, out),
10086
+ Expr::Delete { target, .. } => ce(target, out),
10087
+ Expr::Await { operand, .. } => ce(operand, out),
10088
+ Expr::Conditional { cond, then_branch, else_branch, .. } => {
10089
+ ce(cond, out);
10090
+ ce(then_branch, out);
10091
+ ce(else_branch, out);
10092
+ }
10093
+ Expr::Index { object, index, .. } => {
10094
+ ce(object, out);
10095
+ ce(index, out);
10096
+ }
10097
+ Expr::IndexAssign { object, index, value, .. } => {
10098
+ ce(object, out);
10099
+ ce(index, out);
10100
+ ce(value, out);
10101
+ }
10102
+ Expr::Member { object, prop, .. } => {
10103
+ ce(object, out);
10104
+ if let MemberProp::Expr(pe) = prop {
10105
+ ce(pe, out);
10106
+ }
10107
+ }
10108
+ Expr::MemberAssign { object, value, .. } => {
10109
+ ce(object, out);
10110
+ ce(value, out);
10111
+ }
10112
+ Expr::Assign { value, .. }
10113
+ | Expr::CompoundAssign { value, .. }
10114
+ | Expr::LogicalAssign { value, .. } => ce(value, out),
10115
+ Expr::Array { elements, .. } => {
10116
+ for el in elements {
10117
+ match el {
10118
+ ArrayElement::Expr(x) | ArrayElement::Spread(x) => ce(x, out),
10119
+ }
10120
+ }
10121
+ }
10122
+ Expr::Object { props, .. } => {
10123
+ for pr in props {
10124
+ match pr {
10125
+ ObjectProp::KeyValue(_, x, _) | ObjectProp::Spread(x) => ce(x, out),
10126
+ }
10127
+ }
10128
+ }
10129
+ Expr::TemplateLiteral { exprs, .. } => exprs.iter().for_each(|x| ce(x, out)),
10130
+ _ => {}
10131
+ }
10132
+ }
10133
+
9522
10134
  fn collect_destruct_names(pattern: &DestructPattern, names: &mut HashSet<String>) {
9523
10135
  match pattern {
9524
10136
  DestructPattern::Array(elements) => {
@@ -9777,7 +10389,14 @@ impl Codegen {
9777
10389
  }
9778
10390
  }
9779
10391
  // Only clone builtins that are actually referenced (clone so outer scope can still use, e.g. process for PORT)
10392
+ // #513: every name on the BUILTINS skip-list below MUST also be clone-hoisted here (or
10393
+ // lower as a constant, like Infinity/NaN). A name skipped by the implicit env-capture
10394
+ // fallback but NOT hoisted is referenced raw inside the `move` closure — which MOVES it
10395
+ // out of the enclosing `Fn` closure's capture (E0507: `serve`/`Boolean` in any nested
10396
+ // value-closure that also captures a local, e.g. a `Promise.spawn(fn() { serve(port, …) })`).
9780
10397
  for builtin in &[
10398
+ "Boolean",
10399
+ "serve",
9781
10400
  "console",
9782
10401
  "Math",
9783
10402
  "JSON",
@@ -9844,6 +10463,20 @@ impl Codegen {
9844
10463
  .cloned()
9845
10464
  .collect();
9846
10465
  already_captured.extend(BUILTINS.iter().map(|s| s.to_string()));
10466
+ // #467: names bound INSIDE this body's nested closures (their params and locals) show up
10467
+ // in `referenced` — `collect_referenced_idents` has no binder awareness — but they are not
10468
+ // free variables of THIS closure, and capturing them emits a clone of an ident that does
10469
+ // not exist in the enclosing scope (E0425: `(a)=>(b)=>(c)=> …`'s middle arrow tried to
10470
+ // capture the innermost arrow's own param `c`). Subtract the subtree's full binder set.
10471
+ // Corner accepted: a name that is BOTH bound in a nested closure AND genuinely free at
10472
+ // this level via the implicit fallback (an enclosing-closure local, not an outer param/
10473
+ // var) is skipped too — that fails loudly at compile time (E0425), never silently, and
10474
+ // the primary outer_params/outer_vars capture paths are unaffected by this subtraction.
10475
+ let mut nested_bound = HashSet::new();
10476
+ match body {
10477
+ ArrowBody::Expr(e) => Self::collect_bound_names_deep_expr(e, &mut nested_bound),
10478
+ ArrowBody::Block(s) => Self::collect_bound_names_deep(s, &mut nested_bound),
10479
+ }
9847
10480
  let implicit_env_captures: Vec<String> = if self.value_fn_depth > 0 {
9848
10481
  referenced
9849
10482
  .iter()
@@ -9851,6 +10484,13 @@ impl Codegen {
9851
10484
  !param_names.contains(name.as_str())
9852
10485
  && !local_var_names.contains(name.as_str())
9853
10486
  && !already_captured.contains(name.as_str())
10487
+ && !nested_bound.contains(name.as_str())
10488
+ // #467: a native numeric GLOBAL (#176 TLS-lowered, e.g. `G_BASE`) has no
10489
+ // boxed binding anywhere — the body reads it via the thread-local — so a
10490
+ // capture emit here references a nonexistent ident (E0425 for any nested
10491
+ // arrow whose grandparent-scope var was TLS-lowered). Same exclusion the
10492
+ // FunDecl closure path and this fn's own outer_vars filter already apply.
10493
+ && !self.native_numeric_globals.contains_key(name.as_str())
9854
10494
  })
9855
10495
  .cloned()
9856
10496
  .collect()
@@ -10264,7 +10904,7 @@ impl Codegen {
10264
10904
  }
10265
10905
  }
10266
10906
 
10267
- // Native typed-array HOFs (TISH_NATIVE_HOF): `xs.reduce/map/filter/some/every(<arrow>)`
10907
+ // Native typed-array HOFs (default-on via native_opts_enabled): `xs.reduce/map/filter/some/every(<arrow>)`
10268
10908
  // whose native result type matches this binding's target → emit the iterator chain
10269
10909
  // directly, with NO box/unbox round-trip (the per-element `value_call` is gone too).
10270
10910
  if let Expr::Call { callee, args, .. } = expr {
@@ -10286,6 +10926,18 @@ impl Codegen {
10286
10926
  if &typed_ty == target_type {
10287
10927
  return Ok(typed_code);
10288
10928
  }
10929
+ // Integer-scalar target from a native numeric source (`let hp: u8 = 100`,
10930
+ // `hp: dmg` where dmg is f64): truncate-cast directly instead of round-tripping
10931
+ // through `Value::Number`. Covers narrow widths and `i32` uniformly.
10932
+ if target_type.is_integer_scalar()
10933
+ && (typed_ty == RustType::F64 || typed_ty.is_integer_scalar())
10934
+ {
10935
+ return Ok(format!("({}) as {}", typed_code, target_type.to_rust_type_str()));
10936
+ }
10937
+ // `f64` target from an integer-scalar source: widen directly.
10938
+ if *target_type == RustType::F64 && typed_ty.is_integer_scalar() {
10939
+ return Ok(format!("({}) as f64", typed_code));
10940
+ }
10289
10941
  }
10290
10942
 
10291
10943
  // Fall back to emit_expr + conversion
@@ -11717,6 +12369,37 @@ impl Codegen {
11717
12369
  Ok(Some(assign))
11718
12370
  }
11719
12371
 
12372
+ /// Emit `value` as a native expression of type `target_ty`, coercing without a
12373
+ /// `Value` round-trip where the conversion is a cheap numeric cast. Same-typed RHS
12374
+ /// passes through; f64↔i32 casts directly; everything else boxes to `Value` then
12375
+ /// converts (correct for Fixed/String/Bool/Value fields, at one box per store).
12376
+ fn emit_coerced_native(
12377
+ &mut self,
12378
+ value: &Expr,
12379
+ target_ty: &RustType,
12380
+ ) -> Result<String, CompileError> {
12381
+ let (val_code, val_ty) = self.emit_typed_expr(value)?;
12382
+ if val_ty == *target_ty {
12383
+ return Ok(val_code);
12384
+ }
12385
+ // Cheap numeric coercions that avoid a `Value` round-trip: an integer scalar target
12386
+ // truncate-casts a numeric RHS (`(v) as u8`), and an `f64` target widens an integer
12387
+ // scalar RHS (`(v) as f64`). Covers i32↔f64 and every narrow width in one shape.
12388
+ if target_ty.is_integer_scalar() && (val_ty == RustType::F64 || val_ty.is_integer_scalar())
12389
+ {
12390
+ return Ok(format!("({}) as {}", val_code, target_ty.to_rust_type_str()));
12391
+ }
12392
+ if *target_ty == RustType::F64 && val_ty.is_integer_scalar() {
12393
+ return Ok(format!("({}) as f64", val_code));
12394
+ }
12395
+ let boxed = if val_ty.is_native() {
12396
+ val_ty.to_value_expr(&val_code)
12397
+ } else {
12398
+ val_code
12399
+ };
12400
+ Ok(target_ty.from_value_expr(&boxed))
12401
+ }
12402
+
11720
12403
  /// `for (i=0; i<r; i++) { arr[i] = arr[i+1] }` → `arr.copy_within(1..(r+1), 0)` (fannkuch perm1 rotation).
11721
12404
  fn try_emit_vec_copy_within_shift_for(
11722
12405
  &mut self,
@@ -12357,6 +13040,24 @@ impl Codegen {
12357
13040
  }
12358
13041
  }
12359
13042
 
13043
+ /// A numeric literal folded to a compile-time `Fixed` (Q24.8) constant, or `None` if `e`
13044
+ /// isn't a plain number literal. Lets a `fixed ⊕ <literal>` op (`px + 1.5`, `hp * 2`) stay
13045
+ /// native `Num<i32,8>` arithmetic instead of boxing the literal side through `f64`. Uses the
13046
+ /// same truncating `(n * 256) as i32` conversion as the dynamic `fixed`↔`Value` boundary, so
13047
+ /// the constant is bit-identical to the boxed path.
13048
+ fn fixed_literal_of(e: &Expr) -> Option<String> {
13049
+ if let Expr::Literal {
13050
+ value: Literal::Number(n),
13051
+ ..
13052
+ } = e
13053
+ {
13054
+ let raw = (n * 256.0) as i32;
13055
+ Some(format!("tishlang_runtime::Fixed::from_raw({}i32)", raw))
13056
+ } else {
13057
+ None
13058
+ }
13059
+ }
13060
+
12360
13061
  /// Names of `f64` locals provably integer-bounded within `(-2^53, 2^53)` across the whole
12361
13062
  /// program. Seeds from integer-literal initializers and literal-bounded `for` counters, then
12362
13063
  /// runs a join fixpoint over reassignments: a local keeps a bound only if its init and EVERY
@@ -14277,6 +14978,9 @@ impl Codegen {
14277
14978
  Expr::Member { optional: false, .. } | Expr::Index { optional: false, .. } => {
14278
14979
  found = true
14279
14980
  }
14981
+ // #437: a write to a frozen object throws a catchable TypeError (set_prop parks
14982
+ // it), so an assignment statement must checkpoint like a read/call does.
14983
+ Expr::MemberAssign { .. } | Expr::IndexAssign { .. } => found = true,
14280
14984
  _ => {}
14281
14985
  }
14282
14986
  });
@@ -14853,7 +15557,7 @@ impl Codegen {
14853
15557
 
14854
15558
  fn module_const_f64_array_as_value(static_name: &str) -> String {
14855
15559
  format!(
14856
- "Value::NumberArray(VmRef::new({}.iter().copied().collect::<Vec<f64>>()))",
15560
+ "Value::NumberArray(VmRef::new(tishlang_runtime::NumArrayBacking::Packed({}.iter().copied().collect::<Vec<f64>>())))",
14857
15561
  static_name
14858
15562
  )
14859
15563
  }
@@ -16635,6 +17339,14 @@ impl Codegen {
16635
17339
  code
16636
17340
  } else if ty == RustType::Value {
16637
17341
  RustType::F64.from_value_expr(&code)
17342
+ } else if ty.is_integer_scalar() {
17343
+ // An integer scalar (`i32` or a narrow width) is exact in `f64` — widen it, rather
17344
+ // than emitting the raw integer where an `f64` is required (e.g. `return total;`
17345
+ // from a native-vec `-> f64` fn whose body yields an `i32` local).
17346
+ format!("(({}) as f64)", code)
17347
+ } else if ty == RustType::Fixed {
17348
+ // Q24.8 → f64 (exact): raw / 256.
17349
+ format!("(({}).to_raw() as f64 / 256.0)", code)
16638
17350
  } else {
16639
17351
  code
16640
17352
  })
@@ -17192,6 +17904,7 @@ impl Codegen {
17192
17904
  name,
17193
17905
  params,
17194
17906
  rest_param: None,
17907
+ return_type,
17195
17908
  body,
17196
17909
  ..
17197
17910
  } = s
@@ -17199,6 +17912,13 @@ impl Codegen {
17199
17912
  if nonnumeric.contains(name.as_ref()) {
17200
17913
  continue;
17201
17914
  }
17915
+ // The native-vec `_nv` lowering only expresses `f64`/`Vec<f64>`/unit returns
17916
+ // (`VecRetKind`). A `fixed`-returning fn would be forced to `-> f64` while its
17917
+ // body yields `Num<i32,8>` — a hard type error. Keep such fns on the (correct,
17918
+ // boxed) dynamic path until `VecRetKind` learns `Fixed`.
17919
+ if Self::ann_is_simple(return_type.as_ref(), "fixed") {
17920
+ continue;
17921
+ }
17202
17922
  if let Some(sig) = Self::infer_vec_fn_sig(params, body) {
17203
17923
  let has_array_param = sig
17204
17924
  .params
@@ -17237,6 +17957,7 @@ impl Codegen {
17237
17957
  name,
17238
17958
  params,
17239
17959
  rest_param: None,
17960
+ return_type,
17240
17961
  body,
17241
17962
  ..
17242
17963
  } = s
@@ -17250,6 +17971,11 @@ impl Codegen {
17250
17971
  if nonnumeric.contains(name.as_ref()) {
17251
17972
  continue;
17252
17973
  }
17974
+ // A `fixed` return can't be expressed by `VecRetKind` (see the initial pass) — the
17975
+ // forwarding fixpoint must skip it too, or it re-adds the fn via `body_uses_local_vec_ops`.
17976
+ if Self::ann_is_simple(return_type.as_ref(), "fixed") {
17977
+ continue;
17978
+ }
17253
17979
  if let Some(sig) = Self::infer_vec_fn_sig_forwarding(params, body, &sigs) {
17254
17980
  let has_array_param = sig
17255
17981
  .params
@@ -18698,7 +19424,7 @@ impl Codegen {
18698
19424
  // ====================================================================================
18699
19425
  // #177 (S-E / S-F): interprocedural aggregate (unboxed struct + Vec<Struct>) free fns.
18700
19426
  //
18701
- // The infer front-end (S-0..S-D, behind TISH_AGGREGATE_INFER) stamps a struct alias and
19427
+ // The infer front-end (S-0..S-D, default-on via native_opts_enabled) stamps a struct alias and
18702
19428
  // `: alias` / `: alias[]` annotations onto the nbody-shape factory/array/operator fns iff a
18703
19429
  // whole-program candidacy predicate holds (monomorphic all-f64 struct, no `===`/escape/
18704
19430
  // reshape, write-permitting field ops). Here we consume that: emit each such fn as a native
@@ -18712,7 +19438,7 @@ impl Codegen {
18712
19438
  // identical to flag-off). So we never half-wire the feature or miscompile.
18713
19439
  // ====================================================================================
18714
19440
 
18715
- // ── #178: recursive-struct native lowering (behind TISH_REC_STRUCT) ─────────────────────
19441
+ // ── #178: recursive-struct native lowering (default-on via native_opts_enabled) ─────────
18716
19442
  // Infers a recursive struct shape STRUCTURALLY (no identifier matching) from object literals
18717
19443
  // whose fields are scalars or recursive self-references, and emits native builders
18718
19444
  // (`name_rec(..) -> TishStruct_X`) + consumers (`name_rec(&TishStruct_X) -> f64`) over
@@ -21377,17 +22103,39 @@ impl Codegen {
21377
22103
  // `emit_int32_operand`, which reads the i32 register directly. So coercing here only
21378
22104
  // governs arithmetic/relational reads (e.g. the `h * 16777619` excursion), where the
21379
22105
  // operand must be f64 to keep JS Number semantics.
21380
- let (l, lt) = if lt == RustType::I32 {
22106
+ // A narrow int storage read (`u8` HP, `i16` tile x) promotes the same way — read
22107
+ // as its exact `f64` value so arithmetic keeps JS Number semantics; the truncating
22108
+ // store back to the field happens at the assignment site.
22109
+ let (l, lt) = if lt.is_integer_scalar() {
21381
22110
  (format!("(({}) as f64)", l), RustType::F64)
21382
22111
  } else {
21383
22112
  (l, lt)
21384
22113
  };
21385
- let (r, rt) = if rt == RustType::I32 {
22114
+ let (r, rt) = if rt.is_integer_scalar() {
21386
22115
  (format!("(({}) as f64)", r), RustType::F64)
21387
22116
  } else {
21388
22117
  (r, rt)
21389
22118
  };
21390
22119
 
22120
+ // `fixed ⊕ <numeric-literal>` (`px + 1.5`, `hp * 2`, `y < 100.0`): fold the literal
22121
+ // operand to a compile-time `Fixed` constant so the op lowers to native `Num<i32,8>`
22122
+ // math via the `Fixed⊕Fixed` case below, instead of falling through to the boxed
22123
+ // `ops::*` path. Only a plain literal is folded — a runtime `f64` still boxes (mixing
22124
+ // a runtime f64 with fixed is genuinely lossy and stays on the safe dynamic path).
22125
+ let (l, lt, r, rt) = if lt == RustType::Fixed && rt != RustType::Fixed {
22126
+ match Self::fixed_literal_of(right) {
22127
+ Some(fx) => (l, lt, fx, RustType::Fixed),
22128
+ None => (l, lt, r, rt),
22129
+ }
22130
+ } else if rt == RustType::Fixed && lt != RustType::Fixed {
22131
+ match Self::fixed_literal_of(left) {
22132
+ Some(fx) => (fx, RustType::Fixed, r, rt),
22133
+ None => (l, lt, r, rt),
22134
+ }
22135
+ } else {
22136
+ (l, lt, r, rt)
22137
+ };
22138
+
21391
22139
  if let Some(result_ty) = RustType::result_type_of_binop(*op, &lt, &rt) {
21392
22140
  // Bitwise/shift over numbers: lower the *whole* chain in the int32 domain so
21393
22141
  // intermediate `to_int32`/`to_uint32`↔`f64` round-trips collapse (the win `>>>`
@@ -21866,7 +22614,7 @@ impl Codegen {
21866
22614
  }
21867
22615
  }
21868
22616
  }
21869
- // Native typed-array HOFs over a `Vec<f64>` receiver (TISH_NATIVE_HOF):
22617
+ // Native typed-array HOFs over a `Vec<f64>` receiver (default-on via native_opts_enabled):
21870
22618
  // `xs.reduce/map/filter/some/every(<arrow>)` → a direct Rust iterator chain,
21871
22619
  // eliminating the per-element `value_call` and all `Value` boxing.
21872
22620
  if let Some(res) = self.native_vec_hof_for_call(callee, args)? {
@@ -22082,16 +22830,25 @@ impl Codegen {
22082
22830
  None
22083
22831
  };
22084
22832
  if let Some(nat_op) = nat_op {
22833
+ // #508: a packed `Value::NumberArray` receiver (e.g. `new Float64Array([…])` under
22834
+ // TISH_PACKED_ARRAYS=1) must fold too — the old `if let Value::Array … else init`
22835
+ // silently returned the INIT for it. Its elements are raw f64 by construction, so the
22836
+ // packed arm is the pure-f64 fold with zero boxing; a non-Number init falls back to
22837
+ // the boxed fold over `Value::Number` elements (identical semantics).
22085
22838
  return Ok(Some(format!(
22086
22839
  "{{ let _init0 = {init}; let _arr = ({obj}).clone(); \
22087
- if let Value::Array(ref _a) = _arr {{ let _b = _a.borrow(); \
22840
+ match _arr {{ Value::Array(ref _a) => {{ let _b = _a.borrow(); \
22088
22841
  let mut _accn: f64 = 0.0; let mut _ok = false; \
22089
22842
  if let Value::Number(_i0) = &_init0 {{ _accn = *_i0; _ok = true; }} \
22090
22843
  if _ok {{ for _el in _b.iter() {{ \
22091
22844
  if let Value::Number(_n) = _el {{ _accn {nat_op} *_n; }} else {{ _ok = false; break; }} }} }} \
22092
22845
  if _ok {{ Value::Number(_accn) }} \
22093
22846
  else {{ let mut _acc = _init0; for _el in _b.iter() {{ let _x = _el.clone(); _acc = {body}; }} _acc }} \
22094
- }} else {{ _init0 }} }}",
22847
+ }}, Value::NumberArray(ref _a) => {{ let _vals = _a.borrow().to_values(); \
22848
+ if let (true, Value::Number(_i0)) = (_a.borrow().as_packed().is_some(), &_init0) {{ let mut _accn: f64 = *_i0; \
22849
+ for _v in &_vals {{ if let Value::Number(_n) = _v {{ _accn {nat_op} *_n; }} }} Value::Number(_accn) }} \
22850
+ else {{ let mut _acc = _init0; for _x in _vals {{ _acc = {body}; }} _acc }} \
22851
+ }}, _ => _init0 }} }}",
22095
22852
  init = initial,
22096
22853
  obj = obj_expr,
22097
22854
  nat_op = nat_op,
@@ -22099,10 +22856,13 @@ impl Codegen {
22099
22856
  )));
22100
22857
  }
22101
22858
 
22859
+ // #508: same NumberArray arm for the general (non-arithmetic-op) fold.
22102
22860
  Ok(Some(format!(
22103
22861
  "{{ let mut _acc = {init}; let _arr = ({obj}).clone(); \
22104
- if let Value::Array(ref _a) = _arr {{ for _el in _a.borrow().iter() {{ \
22105
- let _x = _el.clone(); _acc = {body}; }} }} _acc }}",
22862
+ match _arr {{ Value::Array(ref _a) => {{ for _el in _a.borrow().iter() {{ \
22863
+ let _x = _el.clone(); _acc = {body}; }} }} \
22864
+ Value::NumberArray(ref _a) => {{ for _x in _a.borrow().to_values() {{ \
22865
+ _acc = {body}; }} }} _ => {{}} }} _acc }}",
22106
22866
  init = initial,
22107
22867
  obj = obj_expr,
22108
22868
  body = body_code
@@ -22595,7 +23355,7 @@ impl Codegen {
22595
23355
  "let mut __out: Vec<f64> = Vec::new(); \
22596
23356
  for &__f0 in __na.iter() {{ let mut __f = __f0; {inner} \
22597
23357
  let {p}: f64 = __f; __out.push({term}); }} \
22598
- Value::NumberArray(VmRef::new(__out))",
23358
+ Value::NumberArray(VmRef::new(tishlang_runtime::NumArrayBacking::Packed(__out)))",
22599
23359
  inner = inner, p = pesc, term = term
22600
23360
  )
22601
23361
  }
@@ -22607,7 +23367,7 @@ impl Codegen {
22607
23367
  "let mut __out: Vec<f64> = Vec::new(); \
22608
23368
  for &__f0 in __na.iter() {{ let mut __f = __f0; {inner} \
22609
23369
  let {p}: f64 = __f; if {pred} {{ __out.push({p}); }} }} \
22610
- Value::NumberArray(VmRef::new(__out))",
23370
+ Value::NumberArray(VmRef::new(tishlang_runtime::NumArrayBacking::Packed(__out)))",
22611
23371
  inner = inner, p = pesc, pred = pred
22612
23372
  )
22613
23373
  }
@@ -22617,7 +23377,7 @@ impl Codegen {
22617
23377
  }
22618
23378
 
22619
23379
  /// If `callee(args)` is `<Vec<f64>-ident>.reduce/map/filter/some/every(<arrow>)` and the
22620
- /// `TISH_NATIVE_HOF` flag is set, lower it to a native iterator chain. Shared by
23380
+ /// native opts are enabled (default-on), lower it to a native iterator chain. Shared by
22621
23381
  /// `emit_typed_expr` (native sub-expressions) and `emit_native_expr` (typed `let` RHS), so a
22622
23382
  /// typed-array HOF lowers natively whether its result flows into arithmetic or a binding.
22623
23383
  fn native_vec_hof_for_call(
@@ -22654,7 +23414,7 @@ impl Codegen {
22654
23414
  self.try_native_vec_hof(&recv_code, &inner, recv_name.as_ref(), method.as_ref(), args)
22655
23415
  }
22656
23416
 
22657
- /// Native typed-array HOFs (`TISH_NATIVE_HOF`): when the receiver is a native `Vec<f64>`
23417
+ /// Native typed-array HOFs (default-on via `native_opts_enabled`): when the receiver is a native `Vec<f64>`
22658
23418
  /// (a typed `number[]`), lower `reduce`/`map`/`filter`/`some`/`every` to a direct Rust
22659
23419
  /// iterator chain — no per-element `value_call`, no `Value` boxing.
22660
23420
  ///