@tishlang/tish-format 2.43.8 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tish-format +0 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +21 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/tish.d.tish +4 -0
- package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +5 -1
- package/crates/tish/tests/regr_typed_externs.rs +59 -0
- package/crates/tish_builtins/src/globals.rs +27 -0
- package/crates/tish_compile/src/codegen.rs +1285 -116
- package/crates/tish_compile/src/resolve.rs +53 -7
- package/crates/tish_compile/src/schemes.rs +60 -5
- package/crates/tish_compile/src/types.rs +64 -32
- package/crates/tish_compile/tests/regr_556_param_name_collision.rs +40 -0
- package/crates/tish_compile/tests/regr_558_struct_array_write.rs +65 -0
- package/crates/tish_compile/tests/regr_558b_struct_field_read.rs +21 -0
- package/crates/tish_compile/tests/regr_558c_wrapped_struct_array.rs +64 -0
- package/crates/tish_compile/tests/regr_558d_native_vec_mutators.rs +156 -0
- package/crates/tish_compile/tests/regr_562_wrapped_struct_array_push.rs +50 -0
- package/crates/tish_compile/tests/regr_fixed_native_coercions.rs +69 -0
- package/crates/tish_compile/tests/regr_gba_struct_reflection.rs +106 -0
- package/crates/tish_compile/tests/regression/name_collision_lib.tish +6 -0
- package/crates/tish_compile/tests/regression/name_collision_main.tish +13 -0
- package/crates/tish_compile/tests/regression/struct_array_write.tish +22 -0
- package/crates/tish_compile/tests/regression/struct_field_return_read.tish +10 -0
- package/crates/tish_core/src/console_style.rs +4 -0
- package/crates/tish_core/src/json.rs +8 -0
- package/crates/tish_core/src/value.rs +123 -0
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_runtime/src/lib.rs +1 -0
- package/crates/tish_runtime_gba/src/lib.rs +188 -4
- package/crates/tish_vm/src/vm.rs +48 -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
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
//! tishlang/tish#558 — in-place native `Vec` mutators (`pop`/`shift`/`unshift`/`reverse`/`sort`) must
|
|
2
|
+
//! mutate the REAL Vec, both for a plain local and for a closure-captured (refcell-wrapped) one. The
|
|
3
|
+
//! boxed `array_*` fallback operates on a THROWAWAY boxed copy of the Vec (cloning + `from_struct`-boxing
|
|
4
|
+
//! every element into a `Value::Array`), so the mutation is silently discarded (O(n), and an E0282 on
|
|
5
|
+
//! the boxing closure for a `Vec<struct>`). These assert the native forms are emitted — plain (`a.pop()`,
|
|
6
|
+
//! `a.remove(0)`, `a.insert(0, …)`, `a.reverse()`, `a.sort_by(…)`) and captured (`(*a.borrow_mut())…`) —
|
|
7
|
+
//! and that the throwaway-boxing fallback does NOT appear.
|
|
8
|
+
//!
|
|
9
|
+
//! End-to-end runtime parity (interp/vm == native) is exercised by the fixtures under tests/core via
|
|
10
|
+
//! `test_mvp_programs_native`; these codegen-string checks pin the lowering itself.
|
|
11
|
+
|
|
12
|
+
use std::path::PathBuf;
|
|
13
|
+
|
|
14
|
+
use tishlang_compile::compile_project_full;
|
|
15
|
+
|
|
16
|
+
fn emit(body: &str, name: &str) -> String {
|
|
17
|
+
let src = format!("interface E {{ x: number; y: number }}\n{}", body);
|
|
18
|
+
let path = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(name);
|
|
19
|
+
std::fs::write(&path, src).unwrap();
|
|
20
|
+
compile_project_full(&path, path.parent(), &[], true).unwrap().0
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// A plain local `a` mutated at top level (not captured) — native, not wrapped.
|
|
24
|
+
const PLAIN: &str = "\
|
|
25
|
+
let a: E[] = []
|
|
26
|
+
a.push({ x: 1, y: 2 })
|
|
27
|
+
a.push({ x: 3, y: 4 })
|
|
28
|
+
let p = a.pop()
|
|
29
|
+
let s = a.shift()
|
|
30
|
+
a.unshift({ x: 9, y: 9 })
|
|
31
|
+
a.reverse()
|
|
32
|
+
a.sort((u, v) => u.x - v.x)
|
|
33
|
+
console.log(a.length)
|
|
34
|
+
";
|
|
35
|
+
|
|
36
|
+
#[test]
|
|
37
|
+
fn plain_native_vec_mutators_are_native() {
|
|
38
|
+
let r = emit(PLAIN, "plain_mut.tish");
|
|
39
|
+
assert!(r.contains("a.pop()"), "pop must be native `a.pop()`\n{r}");
|
|
40
|
+
assert!(r.contains("a.remove(0)"), "shift must be native `a.remove(0)`\n{r}");
|
|
41
|
+
assert!(r.contains("a.insert(0,"), "unshift must be native `a.insert(0, …)`\n{r}");
|
|
42
|
+
assert!(r.contains("a.reverse()"), "reverse must be native `a.reverse()`\n{r}");
|
|
43
|
+
assert!(r.contains("a.sort_by("), "sort must be native `a.sort_by(…)`\n{r}");
|
|
44
|
+
// None of the mutators may fall to the throwaway whole-Vec boxing fallback.
|
|
45
|
+
for boxed in ["array_pop(&Value::Array(VmRef::new", "array_shift", "array_unshift", "array_reverse"] {
|
|
46
|
+
assert!(!r.contains(boxed), "mutator must not box the whole Vec (`{boxed}`)\n{r}");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// `a` is captured AND mutated inside a closure → refcell-wrapped. Each mutator must go through the cell.
|
|
51
|
+
fn wrapped(mutator: &str, name: &str) -> String {
|
|
52
|
+
emit(
|
|
53
|
+
&format!(
|
|
54
|
+
"let a: E[] = []\na.push({{ x: 1, y: 2 }})\na.push({{ x: 3, y: 4 }})\nfunction f() {{ {mutator} }}\nf()\nconsole.log(a.length)\n"
|
|
55
|
+
),
|
|
56
|
+
name,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#[test]
|
|
61
|
+
fn wrapped_pop_is_native_through_cell() {
|
|
62
|
+
let r = wrapped("a.pop()", "w_pop.tish");
|
|
63
|
+
assert!(r.contains(".borrow_mut()).pop()"), "wrapped pop must be `(*a.borrow_mut()).pop()`\n{r}");
|
|
64
|
+
assert!(!r.contains("array_pop(&Value::Array(VmRef::new"), "wrapped pop must not box the whole Vec\n{r}");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#[test]
|
|
68
|
+
fn wrapped_shift_is_native_through_cell() {
|
|
69
|
+
let r = wrapped("a.shift()", "w_shift.tish");
|
|
70
|
+
assert!(r.contains(".borrow_mut()).remove(0)"), "wrapped shift must be `(*a.borrow_mut()).remove(0)`\n{r}");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#[test]
|
|
74
|
+
fn wrapped_unshift_is_native_through_cell() {
|
|
75
|
+
let r = wrapped("a.unshift({ x: 9, y: 9 })", "w_unshift.tish");
|
|
76
|
+
assert!(r.contains(".borrow_mut()).insert(0,"), "wrapped unshift must be `(*a.borrow_mut()).insert(0, …)`\n{r}");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#[test]
|
|
80
|
+
fn wrapped_reverse_is_native_through_cell() {
|
|
81
|
+
let r = wrapped("a.reverse()", "w_reverse.tish");
|
|
82
|
+
assert!(r.contains(".borrow_mut()).reverse()"), "wrapped reverse must be `(*a.borrow_mut()).reverse()`\n{r}");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#[test]
|
|
86
|
+
fn wrapped_sort_is_native_through_cell() {
|
|
87
|
+
let r = wrapped("a.sort((u, v) => u.x - v.x)", "w_sort.tish");
|
|
88
|
+
assert!(
|
|
89
|
+
r.contains(".borrow_mut()).sort_by(") || r.contains(".borrow_mut() = "),
|
|
90
|
+
"wrapped sort must mutate through the cell (`(*a.borrow_mut()).sort_by` or a write-back)\n{r}"
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── splice / fill / copyWithin (native emitters, plain + captured) ──
|
|
95
|
+
|
|
96
|
+
const SFC_PLAIN: &str = "\
|
|
97
|
+
let a: E[] = []
|
|
98
|
+
a.push({ x: 1, y: 2 })
|
|
99
|
+
a.push({ x: 3, y: 4 })
|
|
100
|
+
let removed = a.splice(0, 1, { x: 9, y: 9 })
|
|
101
|
+
a.fill({ x: 0, y: 0 }, 0, 1)
|
|
102
|
+
a.copyWithin(0, 1)
|
|
103
|
+
console.log(a.length + \"\" + removed.length)
|
|
104
|
+
";
|
|
105
|
+
|
|
106
|
+
#[test]
|
|
107
|
+
fn plain_splice_fill_copywithin_are_native() {
|
|
108
|
+
let r = emit(SFC_PLAIN, "sfc_plain.tish");
|
|
109
|
+
assert!(r.contains(".splice(__start"), "splice must be native `Vec::splice`\n{r}");
|
|
110
|
+
assert!(r.contains("__v.clone()"), "fill must write elements natively\n{r}");
|
|
111
|
+
assert!(r.contains(".to_vec()"), "copyWithin must copy via a native temp\n{r}");
|
|
112
|
+
for boxed in ["array_splice", "array_fill", "array_copy_within", "copy_within as"] {
|
|
113
|
+
assert!(!r.contains(boxed), "must not fall to the boxed `{boxed}` on a throwaway copy\n{r}");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#[test]
|
|
118
|
+
fn wrapped_splice_is_native_through_guard() {
|
|
119
|
+
// `a` captured + spliced in a closure → wrapped; splice uses a `borrow_mut` guard.
|
|
120
|
+
let r = wrapped("a.splice(0, 1)", "w_splice.tish");
|
|
121
|
+
assert!(
|
|
122
|
+
r.contains(".borrow_mut()") && r.contains("__g.splice("),
|
|
123
|
+
"wrapped splice must run on a `borrow_mut` guard (`__g.splice(..)`)\n{r}"
|
|
124
|
+
);
|
|
125
|
+
assert!(!r.contains("array_splice"), "wrapped splice must not box the whole Vec\n{r}");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── captured (wrapped) array READS must NOT box the whole Vec (tish#558 read side) ──
|
|
129
|
+
|
|
130
|
+
#[test]
|
|
131
|
+
fn wrapped_element_field_read_is_native_not_whole_array_box() {
|
|
132
|
+
// `arr` captured + mutated in a closure → wrapped; reading `arr[0].x` must index the borrowed
|
|
133
|
+
// Vec directly, NOT clone+box the entire Vec into a `Value::Array` just to read one field.
|
|
134
|
+
let src = "let arr: E[] = []\narr.push({ x: 1, y: 2 })\nfunction f() { arr[0].x = 9; return arr[0].x }\nconsole.log(f())\n";
|
|
135
|
+
let path = std::path::PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("w_read.tish");
|
|
136
|
+
std::fs::write(&path, format!("interface E {{ x: number; y: number }}\n{src}")).unwrap();
|
|
137
|
+
let r = compile_project_full(&path, path.parent(), &[], true).unwrap().0;
|
|
138
|
+
assert!(
|
|
139
|
+
r.contains("let __bg = arr.borrow();") && r.contains("(*__bg)["),
|
|
140
|
+
"wrapped `arr[i].field` read must index the borrowed Vec natively via a scoped guard (#567)\n{r}"
|
|
141
|
+
);
|
|
142
|
+
assert!(
|
|
143
|
+
!r.contains("iter().cloned().map(|v| tishlang_runtime::Value::from_struct"),
|
|
144
|
+
"wrapped read must NOT clone+box the whole Vec into a Value::Array (O(n)/read)\n{r}"
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#[test]
|
|
149
|
+
fn push_returns_new_length_not_null() {
|
|
150
|
+
// `push` returns the new length (JS / tish interp), a plain `.len()` read — not `Value::Null`.
|
|
151
|
+
let r = emit("let a: E[] = []\nlet n = a.push({ x: 1, y: 2 })\nconsole.log(n)\n", "push_ret.tish");
|
|
152
|
+
assert!(
|
|
153
|
+
r.contains(".len() as f64"),
|
|
154
|
+
"push must return the new length (`.len() as f64`), not Null\n{r}"
|
|
155
|
+
);
|
|
156
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//! tishlang/tish#562 — `.push()` (and `.length`) on a captured (refcell-wrapped) typed struct array
|
|
2
|
+
//! must lower to NATIVE ops, not box the whole `Vec` into a `Value::Array` via an UNTYPED `map(|v| …)`
|
|
3
|
+
//! closure. The boxed path both wasted O(n) per op AND failed to compile with `error[E0282]: type
|
|
4
|
+
//! annotations needed` (the untyped closure param, since the skipped native push never pinned the Vec's
|
|
5
|
+
//! element type). Reproduced on the GBA emit path (the `i32` struct field is native only there).
|
|
6
|
+
|
|
7
|
+
use std::path::PathBuf;
|
|
8
|
+
|
|
9
|
+
use tishlang_compile::{compile_project_full_emit, NativeEmitMode};
|
|
10
|
+
|
|
11
|
+
fn gba_emit(src: &str, name: &str) -> String {
|
|
12
|
+
let path = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(name);
|
|
13
|
+
std::fs::write(&path, src).unwrap();
|
|
14
|
+
compile_project_full_emit(&path, path.parent(), &[], true, NativeEmitMode::Gba, None)
|
|
15
|
+
.unwrap()
|
|
16
|
+
.0
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// The #562 repro shape: a typed struct array with an i32 field, captured + pushed in a closure (which
|
|
20
|
+
// is what refcell-wraps it, matching the issue's `(*enemyStates.borrow())` generated code).
|
|
21
|
+
const SRC: &str = "\
|
|
22
|
+
type EnemyState = { timer: i32 }
|
|
23
|
+
let enemyStates: EnemyState[] = []
|
|
24
|
+
function update() { enemyStates.push({ timer: 0 }) }
|
|
25
|
+
update()
|
|
26
|
+
console.log(enemyStates.length)
|
|
27
|
+
";
|
|
28
|
+
|
|
29
|
+
#[test]
|
|
30
|
+
fn wrapped_struct_push_and_length_do_not_box_the_whole_vec() {
|
|
31
|
+
let rust = gba_emit(SRC, "regr562.tish");
|
|
32
|
+
// No whole-Vec boxing (the E0282-prone untyped `map(|v| …)` closure) for either push or length.
|
|
33
|
+
assert!(
|
|
34
|
+
!rust.contains("iter().cloned().map(|v|"),
|
|
35
|
+
"#562: wrapped struct push/length must NOT box the whole Vec via an untyped `map(|v| …)`\n{rust}"
|
|
36
|
+
);
|
|
37
|
+
assert!(
|
|
38
|
+
!rust.contains("array_push(&Value::Array(VmRef::new"),
|
|
39
|
+
"#562: push must not fall to the boxed `array_push` on a throwaway Value::Array copy\n{rust}"
|
|
40
|
+
);
|
|
41
|
+
// Native push through the shared cell, and a native `len()` for `.length`.
|
|
42
|
+
assert!(
|
|
43
|
+
rust.contains(".borrow_mut()).push("),
|
|
44
|
+
"#562: push must be native `(*enemyStates.borrow_mut()).push(..)`\n{rust}"
|
|
45
|
+
);
|
|
46
|
+
assert!(
|
|
47
|
+
rust.contains("enemyStates.borrow(); __bg.len()"),
|
|
48
|
+
"#562: `.length` must be a native scoped `{{ let __bg = enemyStates.borrow(); __bg.len() }}` (#567), not a whole-Vec box\n{rust}"
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//! GBA `fixed` native coercions (the "gba perf" numeric lowering). These paths lift a native
|
|
2
|
+
//! integer/f64 into `tishlang_runtime::Fixed` (agb `Num<i32,8>`, raw = value << 8) WITHOUT a boxed
|
|
3
|
+
//! `Value` round-trip, so `fixed` arithmetic stays native:
|
|
4
|
+
//! * `let v: fixed = <int/float literal>` — literal → Fixed
|
|
5
|
+
//! * `let d: fixed = t * speed` (t is a runtime i32) — runtime integer LIFTED to Fixed before `Fixed * Fixed`
|
|
6
|
+
//! * `nx = <int literal>` (reassign a fixed local) — RHS lifted to Fixed (the E0308 reassignment fix)
|
|
7
|
+
//!
|
|
8
|
+
//! All three are GBA-only (`fixed`/`i32` are native scalars only under `--target gba`). Off-GBA the
|
|
9
|
+
//! `fixed` annotation falls back to a boxed `Value`, so none of the native `Fixed` code appears —
|
|
10
|
+
//! asserted below so a leak into the desktop build (where `tishlang_runtime::Fixed` does not exist)
|
|
11
|
+
//! would fail the test rather than the downstream Rust compile.
|
|
12
|
+
|
|
13
|
+
use std::path::PathBuf;
|
|
14
|
+
|
|
15
|
+
use tishlang_compile::{compile_project_full, compile_project_full_emit, NativeEmitMode};
|
|
16
|
+
|
|
17
|
+
const SRC: &str = "\
|
|
18
|
+
let speed: fixed = 2
|
|
19
|
+
let t: i32 = 5
|
|
20
|
+
let d: fixed = t * speed
|
|
21
|
+
let nx: fixed = 0
|
|
22
|
+
nx = 3
|
|
23
|
+
console.log(d)
|
|
24
|
+
console.log(nx)
|
|
25
|
+
";
|
|
26
|
+
|
|
27
|
+
fn write_src() -> PathBuf {
|
|
28
|
+
let dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR"));
|
|
29
|
+
let path = dir.join("fixed_native_coercions.tish");
|
|
30
|
+
std::fs::write(&path, SRC).unwrap();
|
|
31
|
+
path
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#[test]
|
|
35
|
+
fn gba_lifts_int_and_float_into_native_fixed() {
|
|
36
|
+
let path = write_src();
|
|
37
|
+
let rust = compile_project_full_emit(&path, path.parent(), &[], true, NativeEmitMode::Gba, None)
|
|
38
|
+
.unwrap()
|
|
39
|
+
.0;
|
|
40
|
+
|
|
41
|
+
// `let speed: fixed = 2` — the int literal lifts to Fixed (× 256 via from_raw), not a boxed number.
|
|
42
|
+
assert!(
|
|
43
|
+
rust.contains("let mut speed: tishlang_runtime::Fixed = tishlang_runtime::Fixed::from_raw("),
|
|
44
|
+
"`let speed: fixed = 2` must lower to a native Fixed::from_raw literal.\n{rust}"
|
|
45
|
+
);
|
|
46
|
+
// `t * speed` (runtime i32 × fixed) — the integer operand `t` is LIFTED to Fixed (raw = value * 256)
|
|
47
|
+
// so the whole op stays `Fixed * Fixed`, instead of dropping `t` to f64 and boxing to `ops::*`.
|
|
48
|
+
assert!(
|
|
49
|
+
rust.contains("as i32) * 256) * speed"),
|
|
50
|
+
"`t * speed` must lift the runtime integer `t` to Fixed and stay native Fixed*Fixed.\n{rust}"
|
|
51
|
+
);
|
|
52
|
+
// `nx = 3` — reassigning a fixed local lifts the RHS to Fixed (the E0308 reassignment fix), not f64.
|
|
53
|
+
assert!(
|
|
54
|
+
rust.contains("nx = tishlang_runtime::Fixed::from_raw("),
|
|
55
|
+
"reassigning a `fixed` local must lift the RHS to Fixed (not leave it f64).\n{rust}"
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#[test]
|
|
60
|
+
fn off_gba_fixed_stays_boxed_no_native_fixed_type() {
|
|
61
|
+
let path = write_src();
|
|
62
|
+
// Default (desktop) emit: `fixed` has no host type, so every one of the above must stay boxed —
|
|
63
|
+
// NO `tishlang_runtime::Fixed` anywhere (it would not compile off-GBA).
|
|
64
|
+
let rust = compile_project_full(&path, path.parent(), &[], true).unwrap().0;
|
|
65
|
+
assert!(
|
|
66
|
+
!rust.contains("tishlang_runtime::Fixed"),
|
|
67
|
+
"`fixed` (incl. the `t * speed` lift and `nx =` reassign) must stay boxed off-GBA.\n{rust}"
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
//! GBA static-type-aware struct reflection. On `--target gba`, `Object.keys/values/entries(s)` and
|
|
2
|
+
//! object spread `{ ...s }` over a statically-known struct (`RustType::Named`) lower to NATIVE reads of
|
|
3
|
+
//! the `TishStruct_*` Rust struct — the struct is never boxed to a `Value::Struct` and no intermediate
|
|
4
|
+
//! hashmap `Value::Object` is built. Only the RESULT (an array / the spread object) is a `Value`.
|
|
5
|
+
//!
|
|
6
|
+
//! DESKTOP is deliberately untouched: reflection there keeps the boxed path (a struct materialises to a
|
|
7
|
+
//! real `Value::Object`, already correct), and struct spread boxes exactly as before — so the hot
|
|
8
|
+
//! `object_spread` bench keeps its clone-shape path. Asserted below so a future change that leaks the
|
|
9
|
+
//! GBA native lowering onto desktop (bench regression) fails here.
|
|
10
|
+
|
|
11
|
+
use std::path::PathBuf;
|
|
12
|
+
|
|
13
|
+
use tishlang_compile::{compile_project_full, compile_project_full_emit, NativeEmitMode};
|
|
14
|
+
|
|
15
|
+
fn emit(src: &str, name: &str, gba: bool) -> String {
|
|
16
|
+
let path = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(name);
|
|
17
|
+
std::fs::write(&path, src).unwrap();
|
|
18
|
+
if gba {
|
|
19
|
+
compile_project_full_emit(&path, path.parent(), &[], true, NativeEmitMode::Gba, None)
|
|
20
|
+
.unwrap()
|
|
21
|
+
.0
|
|
22
|
+
} else {
|
|
23
|
+
compile_project_full(&path, path.parent(), &[], true).unwrap().0
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const KEYS_SRC: &str = "\
|
|
28
|
+
interface P { x: number; y: number }
|
|
29
|
+
let p: P = { x: 1, y: 2 }
|
|
30
|
+
console.log(Object.keys(p).join(\",\"))
|
|
31
|
+
";
|
|
32
|
+
|
|
33
|
+
#[test]
|
|
34
|
+
fn gba_object_keys_on_struct_is_native_field_names() {
|
|
35
|
+
let rust = emit(KEYS_SRC, "gba_keys.tish", true);
|
|
36
|
+
// Field names emitted as a native array literal — no boxed `tish_object_keys` call on the struct.
|
|
37
|
+
assert!(
|
|
38
|
+
rust.contains("Value::String(\"x\".into()), Value::String(\"y\".into())"),
|
|
39
|
+
"GBA `Object.keys(struct)` must emit the field names natively (no struct box / hashmap).\n{rust}"
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[test]
|
|
44
|
+
fn desktop_object_keys_stays_boxed_untouched() {
|
|
45
|
+
let rust = emit(KEYS_SRC, "desk_keys.tish", false);
|
|
46
|
+
assert!(
|
|
47
|
+
!rust.contains("Value::String(\"x\".into()), Value::String(\"y\".into())"),
|
|
48
|
+
"desktop reflection must stay on the boxed path (GBA-gated native lowering leaked to desktop).\n{rust}"
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const SPREAD_SRC: &str = "\
|
|
53
|
+
interface P { x: number; y: number }
|
|
54
|
+
let base: P = { x: 1, y: 2 }
|
|
55
|
+
let merged = { ...base, z: 3 }
|
|
56
|
+
console.log(JSON.stringify(merged))
|
|
57
|
+
";
|
|
58
|
+
|
|
59
|
+
#[test]
|
|
60
|
+
fn gba_struct_spread_inserts_fields_natively() {
|
|
61
|
+
let rust = emit(SPREAD_SRC, "gba_spread.tish", true);
|
|
62
|
+
// Native per-field inserts from a borrowed struct — not the boxed `object_spread_props` helper
|
|
63
|
+
// (that stays the fallback for a struct hidden behind a `Value`) and not a hashmap materialisation.
|
|
64
|
+
assert!(
|
|
65
|
+
rust.contains("let _s = &(") && rust.contains("_obj.insert("),
|
|
66
|
+
"GBA `{{ ...struct }}` must insert the struct's fields natively.\n{rust}"
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[test]
|
|
71
|
+
fn desktop_struct_spread_stays_boxed_untouched() {
|
|
72
|
+
let rust = emit(SPREAD_SRC, "desk_spread.tish", false);
|
|
73
|
+
// Desktop must keep the inline `if let Value::Object` clone-shape path — the native `let _s = &(`
|
|
74
|
+
// insert form must NOT appear (that would mean the GBA lowering leaked onto the object_spread bench).
|
|
75
|
+
assert!(
|
|
76
|
+
!rust.contains("let _s = &("),
|
|
77
|
+
"desktop struct spread must stay boxed/inline (bench-safe); native inserts leaked.\n{rust}"
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const IN_SRC: &str = "\
|
|
82
|
+
interface P { x: number; y: number }
|
|
83
|
+
let p: P = { x: 1, y: 2 }
|
|
84
|
+
console.log(\"x\" in p)
|
|
85
|
+
console.log(\"z\" in p)
|
|
86
|
+
";
|
|
87
|
+
|
|
88
|
+
#[test]
|
|
89
|
+
fn gba_in_on_struct_folds_to_native_bool() {
|
|
90
|
+
let rust = emit(IN_SRC, "gba_in.tish", true);
|
|
91
|
+
// A string-literal key `in` a known struct folds to a compile-time bool — no boxed `tish_in_operator`
|
|
92
|
+
// on the struct, no `Value::Struct` box.
|
|
93
|
+
assert!(
|
|
94
|
+
rust.contains("Value::Bool(true)") && rust.contains("Value::Bool(false)"),
|
|
95
|
+
"GBA `\"x\" in struct` must fold to a compile-time bool.\n{rust}"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#[test]
|
|
100
|
+
fn desktop_in_on_struct_stays_boxed_untouched() {
|
|
101
|
+
let rust = emit(IN_SRC, "desk_in.tish", false);
|
|
102
|
+
assert!(
|
|
103
|
+
rust.contains("tish_in_operator"),
|
|
104
|
+
"desktop `in` must stay on the boxed runtime operator (GBA native lowering leaked).\n{rust}"
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// An exported helper whose PARAMETER name (`scene`) collides with a top-level mutable var in the
|
|
2
|
+
// importer. The reference to the parameter inside the body must resolve to the parameter, not the
|
|
3
|
+
// importer's VmRef cell of the same name (tishlang/tish#556).
|
|
4
|
+
export function loadScene(scene) {
|
|
5
|
+
return scene(1)
|
|
6
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { loadScene } from './name_collision_lib'
|
|
2
|
+
|
|
3
|
+
// A top-level var named `scene`, CAPTURED + mutated by a closure ⇒ it becomes a shared `VmRef` cell.
|
|
4
|
+
// This is the shape the real game hit: mutable module state whose name matches an imported helper's
|
|
5
|
+
// parameter (tishlang/tish#556).
|
|
6
|
+
let scene = 0
|
|
7
|
+
function bump() { scene = scene + 1 }
|
|
8
|
+
bump()
|
|
9
|
+
|
|
10
|
+
function build(n) { return n + 100 }
|
|
11
|
+
|
|
12
|
+
console.log(loadScene(build)) // expect 101 — the param `scene` inside loadScene, not the cell
|
|
13
|
+
console.log(scene) // expect 1
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// tishlang/tish#558 — typed `Vec<struct>` element field writes must be NATIVE stores into the Vec,
|
|
2
|
+
// not a `set_prop` on a throwaway `Value::object_from_pairs([field.clone()...])` copy (which discards
|
|
3
|
+
// the write). Covers a module-level typed struct array and a local one; both read the field back so a
|
|
4
|
+
// discarded write is observable.
|
|
5
|
+
|
|
6
|
+
type P = { x: i32, y: i32 }
|
|
7
|
+
|
|
8
|
+
// (1) module-level typed struct array
|
|
9
|
+
let arr: P[] = []
|
|
10
|
+
arr.push({ x: 10, y: 0 })
|
|
11
|
+
arr[0].x = 200
|
|
12
|
+
|
|
13
|
+
// (2) local typed struct array inside a function
|
|
14
|
+
function localWrite() {
|
|
15
|
+
let es: P[] = []
|
|
16
|
+
es.push({ x: 1, y: 2 })
|
|
17
|
+
es[0].x = 99
|
|
18
|
+
console.log(es[0].x) // 99 if the write took effect, 1 if discarded
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log(arr[0].x) // 200 if the write took effect, 10 if discarded
|
|
22
|
+
localWrite()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// tishlang/tish#558 residual — reading a struct field from a Vec element in a value/return position
|
|
2
|
+
// must CLONE a non-Copy field, not move it out of the borrowed element (E0507). (A Copy field like an
|
|
3
|
+
// on-GBA `i32` reads by value fine; off-GBA a `: i32` field is a boxed `Value`, which is non-Copy.)
|
|
4
|
+
type P = { x: i32 }
|
|
5
|
+
function firstX(): i32 {
|
|
6
|
+
let es: P[] = []
|
|
7
|
+
es.push({ x: 7 })
|
|
8
|
+
return es[0].x
|
|
9
|
+
}
|
|
10
|
+
console.log(firstX())
|
|
@@ -90,6 +90,10 @@ fn format_value_styled_inner(value: &Value, colors: bool, quote_strings: bool) -
|
|
|
90
90
|
}
|
|
91
91
|
Value::Bool(b) => format!("{BOOLEAN}{b}{RESET}"),
|
|
92
92
|
Value::Null => format!("{NULL}null{RESET}"),
|
|
93
|
+
#[cfg(feature = "portable")]
|
|
94
|
+
Value::Struct(s) => {
|
|
95
|
+
format_value_styled_inner(&s.borrow().tish_to_object(), colors, quote_strings)
|
|
96
|
+
}
|
|
93
97
|
Value::Array(arr) => {
|
|
94
98
|
let inner: Vec<String> = arr
|
|
95
99
|
.borrow()
|
|
@@ -239,6 +239,14 @@ fn json_stringify_into_guarded(buf: &mut String, value: &Value, ancestors: &mut
|
|
|
239
239
|
}
|
|
240
240
|
#[cfg(feature = "regex")]
|
|
241
241
|
Value::RegExp(_) => buf.push_str("null"),
|
|
242
|
+
// A boxed typed struct serialises like the object it is: materialise once and recurse. (This is
|
|
243
|
+
// the generic path; codegen also has a direct `__tish_json_TishStruct_*` fast path that never
|
|
244
|
+
// boxes to a struct-Value in the first place, used when the static type is known.)
|
|
245
|
+
#[cfg(feature = "portable")]
|
|
246
|
+
Value::Struct(s) => {
|
|
247
|
+
let obj = s.borrow().tish_to_object();
|
|
248
|
+
json_stringify_into_guarded(buf, &obj, ancestors);
|
|
249
|
+
}
|
|
242
250
|
}
|
|
243
251
|
}
|
|
244
252
|
|