@tishlang/tish-format 2.38.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 (76) hide show
  1. package/Cargo.toml +4 -1
  2. package/bin/tish-format +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 +812 -47
  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_fmt/Cargo.toml +1 -1
  53. package/crates/tish_lsp/Cargo.toml +2 -0
  54. package/crates/tish_lsp/src/import_goto.rs +41 -14
  55. package/crates/tish_lsp/src/main.rs +18 -0
  56. package/crates/tish_native/src/build.rs +246 -0
  57. package/crates/tish_native/src/config.rs +20 -0
  58. package/crates/tish_native/src/lib.rs +16 -6
  59. package/crates/tish_parser/src/lib.rs +28 -0
  60. package/crates/tish_parser/src/parser.rs +25 -4
  61. package/crates/tish_runtime/src/http_hyper.rs +70 -0
  62. package/crates/tish_runtime/src/lib.rs +55 -10
  63. package/crates/tish_runtime/src/ws.rs +94 -21
  64. package/crates/tish_runtime_gba/Cargo.lock +837 -0
  65. package/crates/tish_runtime_gba/Cargo.toml +16 -0
  66. package/crates/tish_runtime_gba/src/gba.rs +211 -0
  67. package/crates/tish_runtime_gba/src/lib.rs +620 -0
  68. package/crates/tish_vm/src/jit.rs +1446 -164
  69. package/crates/tish_vm/src/vm.rs +535 -173
  70. package/justfile +9 -0
  71. package/package.json +1 -1
  72. package/platform/darwin-arm64/tish-fmt +0 -0
  73. package/platform/darwin-x64/tish-fmt +0 -0
  74. package/platform/linux-arm64/tish-fmt +0 -0
  75. package/platform/linux-x64/tish-fmt +0 -0
  76. package/platform/win32-x64/tish-fmt.exe +0 -0
@@ -2,10 +2,34 @@
2
2
  //!
3
3
  //! Maps TypeAnnotation from the AST to concrete Rust types for code generation.
4
4
 
5
+ use std::cell::Cell;
5
6
  use std::collections::HashMap;
6
7
  use std::sync::Arc;
7
8
  use tishlang_ast::{BinOp, FunParam, TypeAnnotation, TypedParam};
8
9
 
10
+ thread_local! {
11
+ /// Whether the extended GBA numeric vocabulary — the narrow integer widths (`i8/u8/i16/u16/u32`)
12
+ /// and `fixed` — lowers to native scalar types. **Off by default** (standard targets): those
13
+ /// annotations must fall back to the boxed `Value` path off-GBA, because `fixed` has no host
14
+ /// runtime type (a hard compile error) and the narrow widths truncate on store (`u8 = 300` → 255),
15
+ /// diverging from the interpreter and breaking the typed == boxed == interpreter guarantee that
16
+ /// non-GBA targets rely on. A GBA build turns this on (see `set_gba_numerics`), where those
17
+ /// types ARE the point. `i32`/`f64` are NOT gated — `f64` is just `number`, and `i32` is the
18
+ /// pre-existing JS-ToInt32 register lowering.
19
+ static GBA_NUMERICS: Cell<bool> = const { Cell::new(false) };
20
+ }
21
+
22
+ /// Enable (GBA) or disable (every other target) the extended narrow-int/`fixed` numeric vocabulary
23
+ /// for the current compile. Set once at compile start from the emit mode; see [`GBA_NUMERICS`].
24
+ pub fn set_gba_numerics(on: bool) {
25
+ GBA_NUMERICS.with(|c| c.set(on));
26
+ }
27
+
28
+ /// Whether `from_annotation` should lower `i8/u8/i16/u16/u32/fixed` to native scalars.
29
+ fn gba_numerics() -> bool {
30
+ GBA_NUMERICS.with(|c| c.get())
31
+ }
32
+
9
33
  /// Concrete Rust type representation for code generation.
10
34
  #[derive(Debug, Clone, PartialEq)]
11
35
  pub enum RustType {
@@ -16,9 +40,27 @@ pub enum RustType {
16
40
  /// i32 — a `number` local PROVEN to always hold an integer reinterpretable as a JS ToInt32
17
41
  /// bit-pattern, kept in an integer register across a bitwise/hash hot loop (bun/JSC-style)
18
42
  /// instead of round-tripping `f64`↔`i32` on every op. The value is the signed int32
19
- /// (= JS `ToInt32`) view; `>>> 0` results are uint32 reinterpreted into this i32. Only the
20
- /// codegen's i32-loop-var lowering produces this type — never `from_annotation`.
43
+ /// (= JS `ToInt32`) view; `>>> 0` results are uint32 reinterpreted into this i32. Off-GBA only
44
+ /// the codegen's i32-loop-var lowering produces this type (a proven-in-range value) — never
45
+ /// `from_annotation`; a `--target gba` build additionally lowers a `: i32` annotation to it
46
+ /// (part of the GBA typed-scalar vocabulary, gated by `GBA_NUMERICS`).
21
47
  I32,
48
+ /// Fixed-point `tishlang_runtime::Fixed` (= agb `Num<i32, 8>`), for the `fixed`
49
+ /// annotation — fast, FPU-free math for positions/velocities on the GBA. Only
50
+ /// `from_annotation` (and fixed-typed inference) produces this. Boxes to/from
51
+ /// `Value::Number` losslessly at boundaries.
52
+ Fixed,
53
+ /// Narrow integer STORAGE types (`i8`/`u8`/`i16`/`u16`/`u32` annotations). Their whole
54
+ /// point is compact struct fields — a `u8` HP or an `i16` tile coordinate costs 1–2
55
+ /// bytes in scarce EWRAM instead of 8. Arithmetic PROMOTES to `f64` (JS Number
56
+ /// semantics, exactly like `I32` at [`Self::result_type_of_binop`]); a store back into
57
+ /// the narrow field truncates with a saturating `as` cast. Only `from_annotation`
58
+ /// produces these. Box to/from `Value::Number` (every value in-range is exact in f64).
59
+ I8,
60
+ U8,
61
+ I16,
62
+ U16,
63
+ U32,
22
64
  /// String (for string)
23
65
  String,
24
66
  /// bool (for boolean)
@@ -83,6 +125,28 @@ impl RustType {
83
125
  match ann {
84
126
  TypeAnnotation::Simple(name, _) => match name.as_ref() {
85
127
  "number" => RustType::F64,
128
+ // Concrete Rust scalar names in typed tish (user requirement): they
129
+ // lower to the corresponding native Rust type instead of a boxed
130
+ // `Value`. `f64` is an alias for `number`; `i32` reuses the existing
131
+ // integer-register lowering (its value is the JS ToInt32 view, which
132
+ // is exactly an annotated `i32`). The narrow widths are compact struct
133
+ // storage; `fixed` is agb `Num<i32,8>`.
134
+ // GBA-only numeric vocabulary (see `GBA_NUMERICS`): off-GBA these fall through to
135
+ // the `other` arm → boxed `Value` (matching main, where these annotations were
136
+ // unrecognized), so `fixed` compiles (no host `Fixed` type), narrow widths keep
137
+ // interpreter number semantics instead of truncating, and a `: i32` annotation
138
+ // doesn't saturate/ToInt32 an out-of-range value — restoring the invariant that
139
+ // `I32` comes ONLY from the proven-in-range i32-loop-var lowering, never a raw
140
+ // annotation. `f64` is the redundant Rust spelling of `number`; gated alongside the
141
+ // rest so it too is inert off-GBA (`number` itself stays `F64`, always).
142
+ "f64" if gba_numerics() => RustType::F64,
143
+ "i32" if gba_numerics() => RustType::I32,
144
+ "i8" if gba_numerics() => RustType::I8,
145
+ "u8" if gba_numerics() => RustType::U8,
146
+ "i16" if gba_numerics() => RustType::I16,
147
+ "u16" if gba_numerics() => RustType::U16,
148
+ "u32" if gba_numerics() => RustType::U32,
149
+ "fixed" if gba_numerics() => RustType::Fixed,
86
150
  "string" => RustType::String,
87
151
  "boolean" | "bool" => RustType::Bool,
88
152
  "void" | "undefined" => RustType::Unit,
@@ -196,6 +260,31 @@ impl RustType {
196
260
  matches!(self, RustType::F64)
197
261
  }
198
262
 
263
+ /// A native integer scalar (`i32` or one of the narrow storage widths). These share one
264
+ /// arithmetic model: read as `f64` (JS Number semantics), store with a truncating `as`
265
+ /// cast. `Fixed` is deliberately excluded — it is fixed-point, not an integer.
266
+ pub fn is_integer_scalar(&self) -> bool {
267
+ matches!(
268
+ self,
269
+ RustType::I32
270
+ | RustType::I8
271
+ | RustType::U8
272
+ | RustType::I16
273
+ | RustType::U16
274
+ | RustType::U32
275
+ )
276
+ }
277
+
278
+ /// A NARROW integer storage width (`i8`/`u8`/`i16`/`u16`/`u32`) — an integer scalar other
279
+ /// than the `I32` register type, which has its own JS-ToInt32 lowering. These promote to
280
+ /// `f64` for arithmetic and truncate-cast on store.
281
+ pub fn is_narrow_int(&self) -> bool {
282
+ matches!(
283
+ self,
284
+ RustType::I8 | RustType::U8 | RustType::I16 | RustType::U16 | RustType::U32
285
+ )
286
+ }
287
+
199
288
  /// Infer the result type of a binary operation given the operand types.
200
289
  /// Returns `None` if native code cannot be emitted (fall back to Value path).
201
290
  pub fn result_type_of_binop(op: BinOp, lhs: &RustType, rhs: &RustType) -> Option<RustType> {
@@ -221,6 +310,21 @@ impl RustType {
221
310
  | BinOp::StrictNe => Some(RustType::Bool),
222
311
  _ => None,
223
312
  }
313
+ } else if lhs == &RustType::Fixed && rhs == &RustType::Fixed {
314
+ // agb `Num<i32,8>` overloads `+ - * /` (Mul/Div apply the Q24.8 shift
315
+ // correction) and `PartialOrd`/`PartialEq`, so these lower to native
316
+ // fixed-point ops — no f64 round-trip, no FPU. `%`/`**`/bitwise fall to
317
+ // the boxed path (rare on positions/velocities).
318
+ match op {
319
+ BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div => Some(RustType::Fixed),
320
+ BinOp::Lt
321
+ | BinOp::Le
322
+ | BinOp::Gt
323
+ | BinOp::Ge
324
+ | BinOp::StrictEq
325
+ | BinOp::StrictNe => Some(RustType::Bool),
326
+ _ => None,
327
+ }
224
328
  } else if lhs == &RustType::Bool && rhs == &RustType::Bool {
225
329
  match op {
226
330
  BinOp::And | BinOp::Or => Some(RustType::Bool),
@@ -248,6 +352,12 @@ impl RustType {
248
352
  RustType::Value => "Value".to_string(),
249
353
  RustType::F64 => "f64".to_string(),
250
354
  RustType::I32 => "i32".to_string(),
355
+ RustType::I8 => "i8".to_string(),
356
+ RustType::U8 => "u8".to_string(),
357
+ RustType::I16 => "i16".to_string(),
358
+ RustType::U16 => "u16".to_string(),
359
+ RustType::U32 => "u32".to_string(),
360
+ RustType::Fixed => "tishlang_runtime::Fixed".to_string(),
251
361
  RustType::String => "String".to_string(),
252
362
  RustType::Bool => "bool".to_string(),
253
363
  RustType::Unit => "()".to_string(),
@@ -279,6 +389,12 @@ impl RustType {
279
389
  RustType::Value => "Value::Null".to_string(),
280
390
  RustType::F64 => "0.0".to_string(),
281
391
  RustType::I32 => "0i32".to_string(),
392
+ RustType::I8 => "0i8".to_string(),
393
+ RustType::U8 => "0u8".to_string(),
394
+ RustType::I16 => "0i16".to_string(),
395
+ RustType::U16 => "0u16".to_string(),
396
+ RustType::U32 => "0u32".to_string(),
397
+ RustType::Fixed => "tishlang_runtime::Fixed::from_raw(0)".to_string(),
282
398
  RustType::String => "String::new()".to_string(),
283
399
  RustType::Bool => "false".to_string(),
284
400
  RustType::Unit => "()".to_string(),
@@ -358,6 +474,23 @@ impl RustType {
358
474
  "match &{} {{ Value::Number(n) => tishlang_runtime::to_int32(*n), _ => panic!(\"expected number\") }}",
359
475
  value_expr
360
476
  ),
477
+ // Narrow int storage: extract the JS Number and truncate-cast to the field width
478
+ // (Rust `f64 as iN` saturates out-of-range and maps NaN→0 — safe for a storage slot).
479
+ RustType::I8 | RustType::U8 | RustType::I16 | RustType::U16 | RustType::U32 => {
480
+ format!(
481
+ "match &{} {{ Value::Number(n) => (*n as {}), _ => panic!(\"expected number\") }}",
482
+ value_expr,
483
+ self.to_rust_type_str()
484
+ )
485
+ }
486
+ // f64 → Q24.8: scale by 256 and TRUNCATE toward zero (`as i32`), keeping only 8
487
+ // fractional bits (`0.1` → raw 25 ≈ 0.0977, not exact). Must stay bit-identical with
488
+ // the compile-time literal fold in codegen (`fixed_literal_of`) so a `fixed` value is
489
+ // the same whether it came from a folded literal or this runtime conversion.
490
+ RustType::Fixed => format!(
491
+ "match &{} {{ Value::Number(n) => tishlang_runtime::Fixed::from_raw((*n * 256.0) as i32), _ => panic!(\"expected number\") }}",
492
+ value_expr
493
+ ),
361
494
  RustType::String => format!(
362
495
  "match &{} {{ Value::String(s) => s.to_string(), _ => panic!(\"expected string\") }}",
363
496
  value_expr
@@ -437,6 +570,12 @@ impl RustType {
437
570
  // The signed int32 view boxes as a JS Number (every i32 is exactly representable in
438
571
  // f64). The uint32 `>>> 0` reinterpretation is applied at the boxing site, not here.
439
572
  RustType::I32 => format!("Value::Number(({}) as f64)", native_expr),
573
+ // Narrow int → JS Number: every in-range narrow int is exact in f64.
574
+ RustType::I8 | RustType::U8 | RustType::I16 | RustType::U16 | RustType::U32 => {
575
+ format!("Value::Number(({}) as f64)", native_expr)
576
+ }
577
+ // Q24.8 → f64 is exact (32 significant bits into a 52-bit mantissa).
578
+ RustType::Fixed => format!("Value::Number(({}).to_raw() as f64 / 256.0)", native_expr),
440
579
  RustType::String => format!("Value::String({}.clone().into())", native_expr),
441
580
  RustType::Bool => format!("Value::Bool({})", native_expr),
442
581
  RustType::Unit => "Value::Null".to_string(),
@@ -134,8 +134,8 @@ fn readonly_arr_param_unboxed_to_native_vec() {
134
134
  // NumberArray clones its backing; a boxed Array is mapped element-wise).
135
135
  assert!(
136
136
  rust.contains("let mut seq: Vec<f64> = match args.get(")
137
- && rust.contains("Some(Value::NumberArray(a)) => a.borrow().clone()"),
138
- "read-only number[] param should be unboxed to a native Vec<f64>"
137
+ && rust.contains("NumArrayBacking::Packed(p) => p.clone()"),
138
+ "read-only number[] param should be unboxed to a native Vec<f64> (packed backing cloned directly)"
139
139
  );
140
140
  // And the hot inner loop indexes it natively (f64 mul + native vec read), no boxed get_index.
141
141
  assert!(
@@ -33,7 +33,9 @@ fn template_literals_hoisted_nums_compiles() {
33
33
  let (rust, _, _, _) = compile_project_full(&path, path.parent(), &[], true).unwrap();
34
34
  assert!(rust.contains("const G_NUMS:"));
35
35
  assert!(
36
- rust.contains("array_join(&Value::NumberArray(VmRef::new(G_NUMS"),
36
+ rust.contains(
37
+ "array_join(&Value::NumberArray(VmRef::new(tishlang_runtime::NumArrayBacking::Packed(G_NUMS"
38
+ ),
37
39
  "nums.join should box hoisted G_NUMS, not reference missing local nums"
38
40
  );
39
41
  assert!(!rust.contains("&nums,"));
@@ -0,0 +1,164 @@
1
+ //! Golden: `resolve_with_platform` cascade matches the documented order.
2
+ //! Vite consumes the same rules via `tish resolve-id`.
3
+ //!
4
+ //! The CLI parity test **fails** (does not skip) when no usable `tish` binary is found,
5
+ //! so CI cannot silently ship without `resolve-id`.
6
+
7
+ use std::fs;
8
+ use std::path::{Path, PathBuf};
9
+ use std::process::Command;
10
+
11
+ use tempfile::tempdir;
12
+ use tishlang_compile::{resolve_with_platform, Platform, ResolveContext, Surface};
13
+
14
+ #[test]
15
+ fn cascade_orders_match_language_md() {
16
+ let dir = tempdir().unwrap();
17
+ let root = dir.path();
18
+ fs::write(root.join("Button.tish"), "export fn Button() {}").unwrap();
19
+ fs::write(root.join("Button.web.tish"), "export fn Button() {}").unwrap();
20
+ fs::write(root.join("Button.webview.tish"), "export fn Button() {}").unwrap();
21
+ fs::write(root.join("Button.macos.tish"), "export fn Button() {}").unwrap();
22
+ fs::write(root.join("Button.desktop.tish"), "export fn Button() {}").unwrap();
23
+
24
+ let macos_native = resolve_with_platform(
25
+ "./Button",
26
+ root,
27
+ ResolveContext {
28
+ platform: Platform::Macos,
29
+ surface: Surface::Native,
30
+ },
31
+ )
32
+ .unwrap();
33
+ assert!(macos_native.ends_with("Button.macos.tish"));
34
+
35
+ let webview = resolve_with_platform(
36
+ "./Button",
37
+ root,
38
+ ResolveContext {
39
+ platform: Platform::Macos,
40
+ surface: Surface::Webview,
41
+ },
42
+ )
43
+ .unwrap();
44
+ assert!(webview.ends_with("Button.webview.tish"));
45
+
46
+ let web = resolve_with_platform(
47
+ "./Button",
48
+ root,
49
+ ResolveContext {
50
+ platform: Platform::Web,
51
+ surface: Surface::Web,
52
+ },
53
+ )
54
+ .unwrap();
55
+ assert!(web.ends_with("Button.web.tish"));
56
+
57
+ // Remap explicit .tish to platform file when present
58
+ let remapped = resolve_with_platform(
59
+ "./Button.tish",
60
+ root,
61
+ ResolveContext {
62
+ platform: Platform::Macos,
63
+ surface: Surface::Native,
64
+ },
65
+ )
66
+ .unwrap();
67
+ assert!(remapped.ends_with("Button.macos.tish"));
68
+ }
69
+
70
+ /// Locate a `tish` binary that supports `resolve-id`.
71
+ ///
72
+ /// Does **not** shell out to `cargo build --bin tish` (under `cargo llvm-cov` that can shadow the
73
+ /// instrumented CLI). Searches the same places as `integration_test::tish_bin()`, including
74
+ /// `llvm-cov-target`. Callers must fail hard if this returns `None` — CI must not soft-skip.
75
+ fn find_tish_binary() -> Option<PathBuf> {
76
+ if let Ok(p) = std::env::var("TISH_PATH") {
77
+ let pb = PathBuf::from(&p);
78
+ if pb.is_file() {
79
+ return Some(pb);
80
+ }
81
+ }
82
+ let bin = if cfg!(windows) { "tish.exe" } else { "tish" };
83
+ let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
84
+ let mut roots: Vec<PathBuf> = Vec::new();
85
+ if let Ok(td) = std::env::var("CARGO_TARGET_DIR") {
86
+ roots.push(PathBuf::from(td));
87
+ }
88
+ // crates/tish_compile → workspace `target/` (and the coverage-instrumented `llvm-cov-target`).
89
+ for rel in ["../../target", "../../../target"] {
90
+ roots.push(manifest.join(rel));
91
+ roots.push(manifest.join(rel).join("llvm-cov-target"));
92
+ }
93
+ for root in roots {
94
+ for profile in ["debug", "release"] {
95
+ let cand = root.join(profile).join(bin);
96
+ if cand.is_file() {
97
+ return Some(cand);
98
+ }
99
+ }
100
+ }
101
+ if let Ok(out) = Command::new("which").arg("tish").output() {
102
+ if out.status.success() {
103
+ let p = String::from_utf8_lossy(&out.stdout).trim().to_string();
104
+ if !p.is_empty() && Path::new(&p).is_file() {
105
+ return Some(PathBuf::from(p));
106
+ }
107
+ }
108
+ }
109
+ None
110
+ }
111
+
112
+ #[test]
113
+ fn resolve_id_cli_matches_library() {
114
+ let dir = tempdir().unwrap();
115
+ let root = dir.path();
116
+ fs::write(root.join("X.web.tish"), "").unwrap();
117
+ fs::write(root.join("X.tish"), "").unwrap();
118
+ let importer = root.join("App.tish");
119
+ fs::write(&importer, "").unwrap();
120
+
121
+ let lib = resolve_with_platform(
122
+ "./X",
123
+ root,
124
+ ResolveContext {
125
+ platform: Platform::Web,
126
+ surface: Surface::Web,
127
+ },
128
+ )
129
+ .unwrap();
130
+
131
+ let tish = find_tish_binary().unwrap_or_else(|| {
132
+ panic!(
133
+ "tish binary with resolve-id required (set TISH_PATH or cargo build -p tishlang --bin tish). \
134
+ Soft-skip is forbidden — Vite and tish build must share resolve rules."
135
+ )
136
+ });
137
+ let out = Command::new(&tish)
138
+ .args([
139
+ "resolve-id",
140
+ "./X",
141
+ "--importer",
142
+ importer.to_str().unwrap(),
143
+ "--platform",
144
+ "web",
145
+ "--surface",
146
+ "web",
147
+ ])
148
+ .output()
149
+ .unwrap_or_else(|e| panic!("failed to run {}: {e}", tish.display()));
150
+
151
+ assert!(
152
+ out.status.success(),
153
+ "tish resolve-id failed ({}):\nstdout: {}\nstderr: {}",
154
+ tish.display(),
155
+ String::from_utf8_lossy(&out.stdout),
156
+ String::from_utf8_lossy(&out.stderr)
157
+ );
158
+ let cli_path = String::from_utf8_lossy(&out.stdout).trim().to_string();
159
+ assert_eq!(
160
+ std::fs::canonicalize(&cli_path).unwrap(),
161
+ lib,
162
+ "Vite/CLI resolve-id must match library cascade"
163
+ );
164
+ }
@@ -0,0 +1,58 @@
1
+ //! Regression: `fixed` and the narrow integer widths (`i8/u8/i16/u16/u32`) are the GBA typed-scalar
2
+ //! vocabulary and must lower to native scalars ONLY for `--target gba`. On the default native build
3
+ //! they must fall back to boxed `Value` — otherwise `fixed` references `tishlang_runtime::Fixed`,
4
+ //! which does not exist off-GBA (a hard compile error), and narrow widths truncate on store,
5
+ //! diverging from the interpreter and breaking the typed == boxed == interpreter guarantee.
6
+ use std::path::PathBuf;
7
+ use tishlang_compile::{compile_project_full, compile_project_full_emit, NativeEmitMode};
8
+
9
+ fn fixture(rel: &str) -> PathBuf {
10
+ let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
11
+ manifest.join("../..").join(rel).canonicalize().unwrap()
12
+ }
13
+
14
+ fn compile(rel: &str) -> String {
15
+ let path = fixture(rel);
16
+ // compile_project_full uses the default DesktopBin emit mode (i.e. NOT --target gba).
17
+ compile_project_full(&path, path.parent(), &[], true).unwrap().0
18
+ }
19
+
20
+ #[test]
21
+ fn narrow_int_and_fixed_do_not_lower_to_native_off_gba() {
22
+ let rust = compile("tests/regression/gba_numerics_gated.tish");
23
+ // `fixed` must not emit the GBA-only native type (absent off-GBA → would not compile).
24
+ assert!(
25
+ !rust.contains("tishlang_runtime::Fixed"),
26
+ "`: fixed` must fall back to boxed Value on a non-GBA build (found the native Fixed type):\n{}",
27
+ rust
28
+ );
29
+ // The narrow-width fields must not become native storage types (which would truncate).
30
+ assert!(
31
+ !rust.contains(": u8") && !rust.contains(": i16") && !rust.contains("as u8"),
32
+ "narrow-int annotations must stay boxed Value off-GBA (found native narrow storage):\n{}",
33
+ rust
34
+ );
35
+ }
36
+
37
+ #[test]
38
+ fn gba_numeric_profile_does_not_leak_to_a_later_desktop_compile() {
39
+ // The numeric profile is a thread-local. A GBA compile turns it on; a subsequent non-GBA
40
+ // compile on the SAME thread (as happens in an LSP / a `cargo test` worker) must NOT inherit it
41
+ // — otherwise `: fixed`/narrow/`i32` would lower natively off-GBA and diverge. This orders both
42
+ // compiles on one thread and checks the desktop one is clean.
43
+ let path = fixture("tests/regression/gba_numerics_gated.tish");
44
+ let gba = compile_project_full_emit(&path, path.parent(), &[], true, NativeEmitMode::Gba, None)
45
+ .unwrap()
46
+ .0;
47
+ assert!(
48
+ gba.contains("tishlang_runtime::Fixed"),
49
+ "sanity: GBA mode DOES lower `: fixed` to the native Fixed type"
50
+ );
51
+ // Same thread, right after the GBA compile: a default (DesktopBin) compile must reset the profile.
52
+ let desktop = compile_project_full(&path, path.parent(), &[], true).unwrap().0;
53
+ assert!(
54
+ !desktop.contains("tishlang_runtime::Fixed"),
55
+ "a desktop compile after a GBA compile leaked the GBA numeric profile (found native Fixed):\n{}",
56
+ desktop
57
+ );
58
+ }
@@ -16,6 +16,7 @@ wasm-bindgen = "0.2"
16
16
 
17
17
  tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
18
18
  tishlang_bytecode = { path = "../tish_bytecode", version = ">=0.1" }
19
+ tishlang_compile = { path = "../tish_compile", version = ">=0.1" }
19
20
  tishlang_compile_js = { path = "../tish_compile_js", version = ">=0.1" }
20
21
  tishlang_opt = { path = "../tish_opt", version = ">=0.1" }
21
22
  tishlang_parser = { path = "../tish_parser", version = ">=0.1" }
@@ -76,7 +76,8 @@ fn parent_dir(path: &str) -> &str {
76
76
  }
77
77
  }
78
78
 
79
- /// Resolve import spec to a key for the files map. Tries .tish extension if missing.
79
+ /// Resolve import spec to a key for the files map.
80
+ /// Uses the same platform/surface cascade as `tish resolve-id` / disk resolve (`TISH_PLATFORM` / `TISH_SURFACE`).
80
81
  fn resolve_import_to_key(
81
82
  spec: &str,
82
83
  from_dir: &str,
@@ -86,14 +87,29 @@ fn resolve_import_to_key(
86
87
  if files.contains_key(&normalized) {
87
88
  return Ok(normalized);
88
89
  }
89
- if !normalized.ends_with(".tish") && !normalized.contains('.') {
90
- let with_ext = format!("{}.tish", normalized);
91
- if files.contains_key(&with_ext) {
92
- return Ok(with_ext);
90
+
91
+ let stem = if normalized.ends_with(".tish") {
92
+ &normalized[..normalized.len() - 5]
93
+ } else if !normalized.contains('.') {
94
+ normalized.as_str()
95
+ } else {
96
+ return Err(format!(
97
+ "Cannot resolve import '{}' from {}: file not in virtual file map",
98
+ spec, from_dir
99
+ ));
100
+ };
101
+
102
+ // Match CLI: honor env when set (playground can set TISH_PLATFORM / TISH_SURFACE).
103
+ let _ = tishlang_compile::apply_resolve_env(None, None);
104
+ let ctx = tishlang_compile::resolve_context();
105
+ for key in tishlang_compile::platform_virtual_keys(stem, ctx) {
106
+ if files.contains_key(&key) {
107
+ return Ok(key);
93
108
  }
94
109
  }
110
+
95
111
  Err(format!(
96
- "Cannot resolve import '{}' from {}: file not in virtual file map",
112
+ "Cannot resolve import '{}' from {}: file not in virtual file map (platform cascade)",
97
113
  spec, from_dir
98
114
  ))
99
115
  }
@@ -584,4 +600,38 @@ mod tests {
584
600
  "import of re-exported `plus` should bind to dep's `add`"
585
601
  );
586
602
  }
603
+
604
+ #[test]
605
+ fn resolve_virtual_honors_platform_cascade() {
606
+ use tishlang_compile::{set_resolve_context, Platform, ResolveContext, Surface};
607
+ set_resolve_context(ResolveContext {
608
+ platform: Platform::Macos,
609
+ surface: Surface::Native,
610
+ });
611
+ let mut files = HashMap::new();
612
+ files.insert(
613
+ "Button.tish".to_string(),
614
+ "export fn Button() {}".to_string(),
615
+ );
616
+ files.insert(
617
+ "Button.macos.tish".to_string(),
618
+ "export fn Button() {}".to_string(),
619
+ );
620
+ files.insert(
621
+ "main.tish".to_string(),
622
+ "import { Button } from \"./Button\"\nButton()".to_string(),
623
+ );
624
+ let modules = resolve_virtual("main.tish", &files).unwrap();
625
+ // macos/native → the .macos variant wins; the base is never loaded.
626
+ assert!(
627
+ modules.iter().any(|m| m.path == "Button.macos.tish"),
628
+ "platform cascade should resolve Button.macos.tish"
629
+ );
630
+ assert!(
631
+ !modules.iter().any(|m| m.path == "Button.tish"),
632
+ "base Button.tish must not resolve when the macos variant exists"
633
+ );
634
+ // Reset the process-wide context so it doesn't leak into sibling tests.
635
+ set_resolve_context(ResolveContext::default());
636
+ }
587
637
  }
@@ -7,8 +7,25 @@ description = "Shared Value type and utilities for Tish (used by interpreter and
7
7
  license-file = { workspace = true }
8
8
  repository = { workspace = true }
9
9
  [features]
10
- default = []
10
+ # `std` is on by default, so every existing consumer builds exactly as before
11
+ # (ahash + arcstr + indexmap/std). No workspace crate disables default-features on
12
+ # tishlang_core, so gating the std-only deps here is safe.
13
+ default = ["std"]
14
+ std = ["dep:ahash", "dep:arcstr", "indexmap/std"]
11
15
  regex = ["dep:fancy-regex"]
16
+ # no_std + alloc build for embedded targets (Game Boy Advance, thumbv4t-none-eabi).
17
+ # Depend on it as `default-features = false, features = ["portable"]`. It swaps the
18
+ # std-only deps (ahash, arcstr) and primitives (std::sync::Arc/atomic, thread_local!)
19
+ # for atomics-free, alloc-based equivalents via the `compat` module. Default (std)
20
+ # builds are unaffected — `compat` is a transparent re-export when this is off.
21
+ # Mutually exclusive with `send-values` (single-threaded target; no Arc/Mutex).
22
+ # See crates/tish_core/src/compat.rs.
23
+ portable = [
24
+ "dep:hashbrown",
25
+ "dep:rustc-hash",
26
+ "dep:portable-atomic",
27
+ "dep:libm",
28
+ ]
12
29
  # Make `Value` (and its array / object / regex payloads) `Send + Sync` by
13
30
  # switching the interior `Rc<RefCell<T>>` to `Arc<Mutex<T>>` and the native
14
31
  # function type from `Rc<dyn Fn>` to `Arc<dyn Fn + Send + Sync>`. Enabled
@@ -19,10 +36,13 @@ regex = ["dep:fancy-regex"]
19
36
  send-values = ["dep:parking_lot"]
20
37
 
21
38
  [dependencies]
22
- ahash = "0.8.11"
23
- arcstr = "1"
39
+ # std-only (need atomics); optional so the `portable` build can drop them.
40
+ ahash = { version = "0.8.11", optional = true }
41
+ arcstr = { version = "1", optional = true }
24
42
  smallvec = "1"
25
- indexmap = "2"
43
+ # default-features=false keeps indexmap no_std-capable; the `std` feature re-adds
44
+ # its std integration for hosted builds (indexmap always works on alloc alone).
45
+ indexmap = { version = "2", default-features = false }
26
46
  # Fast integer→decimal for the JSON.stringify number fast-path (already in the workspace lock).
27
47
  itoa = "1"
28
48
  # Fast shortest-float→decimal for the number→string general path (JSON/template/log). ryu writes the
@@ -34,5 +54,20 @@ fancy-regex = { version = "0.17.0", optional = true }
34
54
  # single atomic with no pthread syscall — profiled as a top cost on object/array access.
35
55
  parking_lot = { version = "0.12", optional = true }
36
56
 
57
+ # --- `portable` (no_std) deps. All optional; only pulled in for embedded builds. ---
58
+ # Atomics-free hash map (ahash needs std/atomics). FxHasher is what agb_hashmap uses on GBA.
59
+ hashbrown = { version = "0.15", default-features = false, optional = true }
60
+ rustc-hash = { version = "2", default-features = false, optional = true }
61
+ # AtomicU64 for the single symbol-id counter — portable-atomic provides it on no-atomics
62
+ # targets via critical-section (impl supplied at final link by agb).
63
+ # Default features include `fallback`, which provides AtomicU64 on targets lacking
64
+ # native 64-bit atomics. The single-core backend is selected by the
65
+ # `portable_atomic_unsafe_assume_single_core` cfg — supplied by agb in the real GBA
66
+ # build (do NOT also enable the `critical-section` feature; agb forbids both). The
67
+ # standalone core/builtins CI build passes that cfg via RUSTFLAGS.
68
+ portable-atomic = { version = "1", optional = true }
69
+ # f64 transcendentals in no_std (std's f64 methods are unavailable without libm).
70
+ libm = { version = "0.2", optional = true }
71
+
37
72
  [target.wasm32-unknown-unknown.dependencies]
38
73
  getrandom = { version = "0.3", features = ["wasm_js"] }