@tishlang/tish-format 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.
- package/Cargo.toml +4 -1
- package/bin/tish-format +0 -0
- package/crates/tish/src/cli_help.rs +28 -0
- package/crates/tish/src/main.rs +41 -5
- package/crates/tish/tests/scope_merge.rs +79 -0
- package/crates/tish_build_utils/src/lib.rs +27 -1
- package/crates/tish_builtins/Cargo.toml +14 -6
- package/crates/tish_builtins/src/array.rs +130 -54
- package/crates/tish_builtins/src/collections.rs +9 -5
- package/crates/tish_builtins/src/construct.rs +7 -1
- package/crates/tish_builtins/src/date.rs +15 -1
- package/crates/tish_builtins/src/globals.rs +38 -3
- package/crates/tish_builtins/src/helpers.rs +7 -3
- package/crates/tish_builtins/src/iterator.rs +5 -1
- package/crates/tish_builtins/src/lib.rs +3 -0
- package/crates/tish_builtins/src/math.rs +14 -0
- package/crates/tish_builtins/src/number.rs +72 -6
- package/crates/tish_builtins/src/object.rs +6 -1
- package/crates/tish_builtins/src/string.rs +13 -2
- package/crates/tish_builtins/src/symbol.rs +17 -17
- package/crates/tish_builtins/src/typedarrays.rs +9 -2
- package/crates/tish_bytecode/src/compiler.rs +76 -2
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +77 -2
- package/crates/tish_compile/src/codegen.rs +806 -46
- package/crates/tish_compile/src/infer.rs +37 -41
- package/crates/tish_compile/src/lib.rs +13 -1
- package/crates/tish_compile/src/platform_resolve.rs +361 -0
- package/crates/tish_compile/src/resolve.rs +148 -10
- package/crates/tish_compile/src/schemes.rs +278 -0
- package/crates/tish_compile/src/types.rs +141 -2
- package/crates/tish_compile/tests/perf_codegen_320.rs +2 -2
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +3 -1
- package/crates/tish_compile/tests/platform_resolve_cli.rs +164 -0
- package/crates/tish_compile/tests/regr_gba_numerics_gated.rs +58 -0
- package/crates/tish_compiler_wasm/Cargo.toml +1 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +56 -6
- package/crates/tish_core/Cargo.toml +39 -4
- package/crates/tish_core/src/compat.rs +506 -0
- package/crates/tish_core/src/console_style.rs +19 -1
- package/crates/tish_core/src/json.rs +34 -10
- package/crates/tish_core/src/lib.rs +121 -25
- package/crates/tish_core/src/macros.rs +1 -1
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/uri.rs +8 -1
- package/crates/tish_core/src/value.rs +182 -54
- package/crates/tish_core/src/vmref.rs +17 -5
- package/crates/tish_eval/Cargo.toml +2 -0
- package/crates/tish_eval/src/eval.rs +90 -3
- package/crates/tish_eval/src/value.rs +8 -0
- package/crates/tish_eval/src/value_convert.rs +316 -21
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_lsp/Cargo.toml +2 -0
- package/crates/tish_lsp/src/import_goto.rs +41 -14
- package/crates/tish_lsp/src/main.rs +18 -0
- package/crates/tish_native/src/build.rs +246 -0
- package/crates/tish_native/src/config.rs +20 -0
- package/crates/tish_native/src/lib.rs +16 -6
- package/crates/tish_parser/src/lib.rs +28 -0
- package/crates/tish_parser/src/parser.rs +25 -4
- package/crates/tish_runtime/src/lib.rs +54 -9
- package/crates/tish_runtime_gba/Cargo.lock +837 -0
- package/crates/tish_runtime_gba/Cargo.toml +16 -0
- package/crates/tish_runtime_gba/src/gba.rs +211 -0
- package/crates/tish_runtime_gba/src/lib.rs +620 -0
- package/crates/tish_vm/src/jit.rs +1446 -164
- package/crates/tish_vm/src/vm.rs +535 -173
- package/justfile +9 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- 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.
|
|
20
|
-
/// codegen's i32-loop-var lowering produces this type — never
|
|
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("
|
|
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(
|
|
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.
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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"] }
|