@tishlang/tish-format 2.39.0 → 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/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/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/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 +65 -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 +2154 -225
- 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 +195 -11
- package/crates/tish_compile/src/schemes.rs +333 -0
- package/crates/tish_compile/src/types.rs +205 -34
- 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_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_numerics_gated.rs +58 -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_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 +23 -1
- package/crates/tish_core/src/json.rs +42 -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 +305 -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 +55 -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 +804 -0
- package/crates/tish_vm/src/jit.rs +1446 -164
- package/crates/tish_vm/src/vm.rs +583 -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,
|
|
@@ -191,11 +255,47 @@ impl RustType {
|
|
|
191
255
|
!matches!(self, RustType::Value)
|
|
192
256
|
}
|
|
193
257
|
|
|
258
|
+
/// Whether the generated Rust type is `Copy` — the native numeric scalars, `Fixed`, and `Unit`.
|
|
259
|
+
/// A `Copy` field can be read out of a borrowed place (`arr[i].field`, `(*cell.borrow()).field`)
|
|
260
|
+
/// by value; a non-`Copy` one (`Value`/`String`/an aggregate) must be `.clone()`d there or it
|
|
261
|
+
/// moves out of the borrow — a use-after-move / cannot-move-out-of-index error (E0382 / E0507).
|
|
262
|
+
pub fn is_copy(&self) -> bool {
|
|
263
|
+
matches!(
|
|
264
|
+
self,
|
|
265
|
+
RustType::F64 | RustType::I32 | RustType::Bool | RustType::Fixed | RustType::Unit
|
|
266
|
+
) || self.is_narrow_int()
|
|
267
|
+
}
|
|
268
|
+
|
|
194
269
|
/// Check if this type is numeric (f64).
|
|
195
270
|
pub fn is_numeric(&self) -> bool {
|
|
196
271
|
matches!(self, RustType::F64)
|
|
197
272
|
}
|
|
198
273
|
|
|
274
|
+
/// A native integer scalar (`i32` or one of the narrow storage widths). These share one
|
|
275
|
+
/// arithmetic model: read as `f64` (JS Number semantics), store with a truncating `as`
|
|
276
|
+
/// cast. `Fixed` is deliberately excluded — it is fixed-point, not an integer.
|
|
277
|
+
pub fn is_integer_scalar(&self) -> bool {
|
|
278
|
+
matches!(
|
|
279
|
+
self,
|
|
280
|
+
RustType::I32
|
|
281
|
+
| RustType::I8
|
|
282
|
+
| RustType::U8
|
|
283
|
+
| RustType::I16
|
|
284
|
+
| RustType::U16
|
|
285
|
+
| RustType::U32
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/// A NARROW integer storage width (`i8`/`u8`/`i16`/`u16`/`u32`) — an integer scalar other
|
|
290
|
+
/// than the `I32` register type, which has its own JS-ToInt32 lowering. These promote to
|
|
291
|
+
/// `f64` for arithmetic and truncate-cast on store.
|
|
292
|
+
pub fn is_narrow_int(&self) -> bool {
|
|
293
|
+
matches!(
|
|
294
|
+
self,
|
|
295
|
+
RustType::I8 | RustType::U8 | RustType::I16 | RustType::U16 | RustType::U32
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
199
299
|
/// Infer the result type of a binary operation given the operand types.
|
|
200
300
|
/// Returns `None` if native code cannot be emitted (fall back to Value path).
|
|
201
301
|
pub fn result_type_of_binop(op: BinOp, lhs: &RustType, rhs: &RustType) -> Option<RustType> {
|
|
@@ -221,6 +321,21 @@ impl RustType {
|
|
|
221
321
|
| BinOp::StrictNe => Some(RustType::Bool),
|
|
222
322
|
_ => None,
|
|
223
323
|
}
|
|
324
|
+
} else if lhs == &RustType::Fixed && rhs == &RustType::Fixed {
|
|
325
|
+
// agb `Num<i32,8>` overloads `+ - * /` (Mul/Div apply the Q24.8 shift
|
|
326
|
+
// correction) and `PartialOrd`/`PartialEq`, so these lower to native
|
|
327
|
+
// fixed-point ops — no f64 round-trip, no FPU. `%`/`**`/bitwise fall to
|
|
328
|
+
// the boxed path (rare on positions/velocities).
|
|
329
|
+
match op {
|
|
330
|
+
BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div => Some(RustType::Fixed),
|
|
331
|
+
BinOp::Lt
|
|
332
|
+
| BinOp::Le
|
|
333
|
+
| BinOp::Gt
|
|
334
|
+
| BinOp::Ge
|
|
335
|
+
| BinOp::StrictEq
|
|
336
|
+
| BinOp::StrictNe => Some(RustType::Bool),
|
|
337
|
+
_ => None,
|
|
338
|
+
}
|
|
224
339
|
} else if lhs == &RustType::Bool && rhs == &RustType::Bool {
|
|
225
340
|
match op {
|
|
226
341
|
BinOp::And | BinOp::Or => Some(RustType::Bool),
|
|
@@ -248,6 +363,12 @@ impl RustType {
|
|
|
248
363
|
RustType::Value => "Value".to_string(),
|
|
249
364
|
RustType::F64 => "f64".to_string(),
|
|
250
365
|
RustType::I32 => "i32".to_string(),
|
|
366
|
+
RustType::I8 => "i8".to_string(),
|
|
367
|
+
RustType::U8 => "u8".to_string(),
|
|
368
|
+
RustType::I16 => "i16".to_string(),
|
|
369
|
+
RustType::U16 => "u16".to_string(),
|
|
370
|
+
RustType::U32 => "u32".to_string(),
|
|
371
|
+
RustType::Fixed => "tishlang_runtime::Fixed".to_string(),
|
|
251
372
|
RustType::String => "String".to_string(),
|
|
252
373
|
RustType::Bool => "bool".to_string(),
|
|
253
374
|
RustType::Unit => "()".to_string(),
|
|
@@ -279,6 +400,12 @@ impl RustType {
|
|
|
279
400
|
RustType::Value => "Value::Null".to_string(),
|
|
280
401
|
RustType::F64 => "0.0".to_string(),
|
|
281
402
|
RustType::I32 => "0i32".to_string(),
|
|
403
|
+
RustType::I8 => "0i8".to_string(),
|
|
404
|
+
RustType::U8 => "0u8".to_string(),
|
|
405
|
+
RustType::I16 => "0i16".to_string(),
|
|
406
|
+
RustType::U16 => "0u16".to_string(),
|
|
407
|
+
RustType::U32 => "0u32".to_string(),
|
|
408
|
+
RustType::Fixed => "tishlang_runtime::Fixed::from_raw(0)".to_string(),
|
|
282
409
|
RustType::String => "String::new()".to_string(),
|
|
283
410
|
RustType::Bool => "false".to_string(),
|
|
284
411
|
RustType::Unit => "()".to_string(),
|
|
@@ -358,6 +485,23 @@ impl RustType {
|
|
|
358
485
|
"match &{} {{ Value::Number(n) => tishlang_runtime::to_int32(*n), _ => panic!(\"expected number\") }}",
|
|
359
486
|
value_expr
|
|
360
487
|
),
|
|
488
|
+
// Narrow int storage: extract the JS Number and truncate-cast to the field width
|
|
489
|
+
// (Rust `f64 as iN` saturates out-of-range and maps NaN→0 — safe for a storage slot).
|
|
490
|
+
RustType::I8 | RustType::U8 | RustType::I16 | RustType::U16 | RustType::U32 => {
|
|
491
|
+
format!(
|
|
492
|
+
"match &{} {{ Value::Number(n) => (*n as {}), _ => panic!(\"expected number\") }}",
|
|
493
|
+
value_expr,
|
|
494
|
+
self.to_rust_type_str()
|
|
495
|
+
)
|
|
496
|
+
}
|
|
497
|
+
// f64 → Q24.8: scale by 256 and TRUNCATE toward zero (`as i32`), keeping only 8
|
|
498
|
+
// fractional bits (`0.1` → raw 25 ≈ 0.0977, not exact). Must stay bit-identical with
|
|
499
|
+
// the compile-time literal fold in codegen (`fixed_literal_of`) so a `fixed` value is
|
|
500
|
+
// the same whether it came from a folded literal or this runtime conversion.
|
|
501
|
+
RustType::Fixed => format!(
|
|
502
|
+
"match &{} {{ Value::Number(n) => tishlang_runtime::Fixed::from_raw((*n * 256.0) as i32), _ => panic!(\"expected number\") }}",
|
|
503
|
+
value_expr
|
|
504
|
+
),
|
|
361
505
|
RustType::String => format!(
|
|
362
506
|
"match &{} {{ Value::String(s) => s.to_string(), _ => panic!(\"expected string\") }}",
|
|
363
507
|
value_expr
|
|
@@ -400,11 +544,19 @@ impl RustType {
|
|
|
400
544
|
// e.g. an assignment-as-expression `{ let _v = obj; lhs = <coerce _v>; _v }` reuses
|
|
401
545
|
// `_v` for its value (tishlang/tish#486). `&(expr)` evaluates the source once and
|
|
402
546
|
// (for a temporary) lifetime-extends it to this block, so it's still single-eval.
|
|
547
|
+
//
|
|
548
|
+
// FAST PATH (GBA/portable): if the source is a BY-REFERENCE boxed struct of exactly this
|
|
549
|
+
// type (`Value::Struct`, e.g. from a `Value::from_struct` return), `downcast_struct`
|
|
550
|
+
// clones it out directly — no per-field `get_prop`. Off-portable there is no struct
|
|
551
|
+
// variant, so `downcast_struct` is a `None` constant the optimiser removes, leaving the
|
|
552
|
+
// rebuild path byte-identical to before.
|
|
553
|
+
let struct_ident = named_struct_ident(name);
|
|
403
554
|
format!(
|
|
404
|
-
"{{ let _src = &({}); {} {{
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
555
|
+
"{{ let _src = &({}); match _src.downcast_struct::<{}>() {{ \
|
|
556
|
+
Some(_s) => _s, \
|
|
557
|
+
None => {} {{ {} }} \
|
|
558
|
+
}} }}",
|
|
559
|
+
value_expr, struct_ident, struct_ident, field_assigns
|
|
408
560
|
)
|
|
409
561
|
}
|
|
410
562
|
RustType::ShapeUnion { .. } => {
|
|
@@ -437,6 +589,12 @@ impl RustType {
|
|
|
437
589
|
// The signed int32 view boxes as a JS Number (every i32 is exactly representable in
|
|
438
590
|
// f64). The uint32 `>>> 0` reinterpretation is applied at the boxing site, not here.
|
|
439
591
|
RustType::I32 => format!("Value::Number(({}) as f64)", native_expr),
|
|
592
|
+
// Narrow int → JS Number: every in-range narrow int is exact in f64.
|
|
593
|
+
RustType::I8 | RustType::U8 | RustType::I16 | RustType::U16 | RustType::U32 => {
|
|
594
|
+
format!("Value::Number(({}) as f64)", native_expr)
|
|
595
|
+
}
|
|
596
|
+
// Q24.8 → f64 is exact (32 significant bits into a 52-bit mantissa).
|
|
597
|
+
RustType::Fixed => format!("Value::Number(({}).to_raw() as f64 / 256.0)", native_expr),
|
|
440
598
|
RustType::String => format!("Value::String({}.clone().into())", native_expr),
|
|
441
599
|
RustType::Bool => format!("Value::Bool({})", native_expr),
|
|
442
600
|
RustType::Unit => "Value::Null".to_string(),
|
|
@@ -463,34 +621,22 @@ impl RustType {
|
|
|
463
621
|
native_expr, inner_to_value
|
|
464
622
|
)
|
|
465
623
|
}
|
|
466
|
-
RustType::Named {
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
// `
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
let v_expr = if matches!(ty, RustType::Value) {
|
|
483
|
-
format!("{}.clone()", access)
|
|
484
|
-
} else {
|
|
485
|
-
ty.to_value_expr(&access)
|
|
486
|
-
};
|
|
487
|
-
format!("(::std::sync::Arc::from({:?}), {})", k.as_ref(), v_expr)
|
|
488
|
-
})
|
|
489
|
-
.collect::<Vec<_>>()
|
|
490
|
-
.join(", ");
|
|
491
|
-
// `object_from_pairs::<N>` builds the `PropMap` in one ordered pass (N = field
|
|
492
|
-
// count, a codegen-time constant) — no AHashMap, so key order is preserved.
|
|
493
|
-
format!("Value::object_from_pairs([{}])", pairs)
|
|
624
|
+
RustType::Named { .. } => {
|
|
625
|
+
// Box the struct BY SHARED REFERENCE via `Value::from_struct_ref(&(..))`. Portable/GBA
|
|
626
|
+
// keeps it as a cheap `Value::Struct` (one `Rc` — the struct must be OWNED there, so it
|
|
627
|
+
// clones internally). Every other native backend serialises the borrow directly through
|
|
628
|
+
// `TishStruct::tish_to_object` (the ordered `object_from_pairs` boundary — see
|
|
629
|
+
// [`named_struct_to_object_expr`]) with NO whole-struct clone, byte-identical to the
|
|
630
|
+
// historical behaviour that read fields straight off the place (#565: the previous
|
|
631
|
+
// `from_struct((..).clone())` cloned the entire struct just to serialise it off-GBA).
|
|
632
|
+
// Property access on a boxed struct dispatches through the generated `impl TishStruct`
|
|
633
|
+
// (small match, no hash); materialisation is reached only on JSON.stringify / display /
|
|
634
|
+
// `Object.keys`-family / `for..in` / `in` / `Object.assign` / `{...s}` spread. `&(..)`
|
|
635
|
+
// borrows the place codegen already emits here (a value or a deref like `(*gs.borrow())`).
|
|
636
|
+
format!(
|
|
637
|
+
"tishlang_runtime::Value::from_struct_ref(&({}))",
|
|
638
|
+
native_expr
|
|
639
|
+
)
|
|
494
640
|
}
|
|
495
641
|
RustType::ShapeUnion { name, variants } => {
|
|
496
642
|
// union → boxed Value::Object: one match arm per variant, delegating to that
|
|
@@ -534,6 +680,31 @@ pub fn named_struct_ident(tish_name: &str) -> String {
|
|
|
534
680
|
format!("TishStruct_{}", tish_name)
|
|
535
681
|
}
|
|
536
682
|
|
|
683
|
+
/// Build the `Value::object_from_pairs([...])` that materialises a Named struct at `native_expr` into
|
|
684
|
+
/// an ORDERED boxed object (field-declaration = JS insertion order — deterministic, unlike an AHashMap
|
|
685
|
+
/// insert). This was the body of `to_value_expr`'s `Named` arm; it now backs the generated
|
|
686
|
+
/// `TishStruct::tish_to_object` (the JSON / display / `Object.keys`-family / `for..in` / `in` /
|
|
687
|
+
/// `Object.assign` / `{...s}` spread materialisation, and the non-portable
|
|
688
|
+
/// `Value::from_struct` fallback), while the live boxing path emits the cheap by-ref `Value::from_struct`.
|
|
689
|
+
pub fn named_struct_to_object_expr(fields: &[(Arc<str>, RustType)], native_expr: &str) -> String {
|
|
690
|
+
let pairs = fields
|
|
691
|
+
.iter()
|
|
692
|
+
.map(|(k, ty)| {
|
|
693
|
+
let access = format!("{}.{}", native_expr, field_ident(k));
|
|
694
|
+
// A `Value`-typed field must be cloned (not `Copy`, and `to_value_expr(Value)` is identity);
|
|
695
|
+
// native field types clone/copy inside their own `to_value_expr`.
|
|
696
|
+
let v_expr = if matches!(ty, RustType::Value) {
|
|
697
|
+
format!("{}.clone()", access)
|
|
698
|
+
} else {
|
|
699
|
+
ty.to_value_expr(&access)
|
|
700
|
+
};
|
|
701
|
+
format!("(::std::sync::Arc::from({:?}), {})", k.as_ref(), v_expr)
|
|
702
|
+
})
|
|
703
|
+
.collect::<Vec<_>>()
|
|
704
|
+
.join(", ");
|
|
705
|
+
format!("Value::object_from_pairs([{}])", pairs)
|
|
706
|
+
}
|
|
707
|
+
|
|
537
708
|
/// #179 Stage B: map a shape-union alias to the generated Rust enum identifier.
|
|
538
709
|
pub fn shape_union_enum_ident(tish_name: &str) -> String {
|
|
539
710
|
format!("TishUnion_{}", tish_name)
|
|
@@ -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,40 @@
|
|
|
1
|
+
//! tishlang/tish#556 — a top-level MUTABLE var captured by a closure becomes a `VmRef` cell; if its
|
|
2
|
+
//! name matches an imported/other function's PARAMETER, a reference to the parameter inside that
|
|
3
|
+
//! function must resolve to the parameter, NOT a `vm_read` of the outer cell (which emits
|
|
4
|
+
//! `vm_read(¶m)` — a `&Value` where a `&VmRef` is wanted, so the generated Rust won't compile).
|
|
5
|
+
//! The bug bit the GBA backend; guard both emit modes.
|
|
6
|
+
use std::collections::HashSet;
|
|
7
|
+
use std::path::PathBuf;
|
|
8
|
+
use tishlang_compile::{compile_project_full_emit, NativeEmitMode};
|
|
9
|
+
|
|
10
|
+
fn compile(rel: &str, mode: NativeEmitMode) -> String {
|
|
11
|
+
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
12
|
+
let path = manifest.join("../..").join(rel).canonicalize().unwrap();
|
|
13
|
+
let caps: HashSet<String> = HashSet::new();
|
|
14
|
+
compile_project_full_emit(&path, path.parent(), &[], true, mode, Some(&caps))
|
|
15
|
+
.unwrap()
|
|
16
|
+
.0
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[test]
|
|
20
|
+
fn param_shadows_outer_cell_not_read_via_vm_read() {
|
|
21
|
+
let rel = "crates/tish_compile/tests/regression/name_collision_main.tish";
|
|
22
|
+
for mode in [NativeEmitMode::DesktopBin, NativeEmitMode::Gba] {
|
|
23
|
+
let rust = compile(rel, mode);
|
|
24
|
+
// The bug: inside `loadScene`, calling the `scene` PARAMETER (`scene(1)`) compiled the callee
|
|
25
|
+
// as `vm_read(&scene)` — a read of the outer `VmRef` cell, i.e. `&Value` where a `&VmRef` is
|
|
26
|
+
// wanted (won't compile). The param must be used directly instead. (A plain `vm_read(&scene)`
|
|
27
|
+
// still legitimately appears where `bump` mutates the real outer cell, so we target this
|
|
28
|
+
// exact param-as-callee shape.)
|
|
29
|
+
assert!(
|
|
30
|
+
!rust.contains("_callee = (tishlang_runtime::vm_read(&scene))"),
|
|
31
|
+
"#556 ({mode:?}): `loadScene`'s `scene` parameter must shadow the outer cell — a call to \
|
|
32
|
+
it must not read `vm_read(&scene)`\n{rust}"
|
|
33
|
+
);
|
|
34
|
+
// The outer cell itself is still legitimately a real `VmRef` where `bump` mutates it.
|
|
35
|
+
assert!(
|
|
36
|
+
rust.contains("scene.borrow_mut()"),
|
|
37
|
+
"#556 ({mode:?}): expected the outer captured `scene` to remain a real `VmRef` cell\n{rust}"
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
//! tishlang/tish#558 — a typed `Vec<struct>` element field write (`arr[i].field = v`) must lower to
|
|
2
|
+
//! a NATIVE store into the Vec element, not a `set_prop` on a throwaway `Value::object_from_pairs`
|
|
3
|
+
//! copy of the element (which silently discards the write). Regressed the module-level array and the
|
|
4
|
+
//! local array; both are covered here.
|
|
5
|
+
use std::collections::HashSet;
|
|
6
|
+
use std::path::PathBuf;
|
|
7
|
+
use tishlang_compile::{compile_project_full, compile_project_full_emit, NativeEmitMode};
|
|
8
|
+
|
|
9
|
+
fn compile(rel: &str) -> String {
|
|
10
|
+
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
11
|
+
let path = manifest.join("../..").join(rel).canonicalize().unwrap();
|
|
12
|
+
compile_project_full(&path, path.parent(), &[], true).unwrap().0
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fn compile_gba(rel: &str) -> String {
|
|
16
|
+
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
17
|
+
let path = manifest.join("../..").join(rel).canonicalize().unwrap();
|
|
18
|
+
let caps: HashSet<String> = HashSet::new();
|
|
19
|
+
compile_project_full_emit(&path, path.parent(), &[], true, NativeEmitMode::Gba, Some(&caps))
|
|
20
|
+
.unwrap()
|
|
21
|
+
.0
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#[test]
|
|
25
|
+
fn gba_struct_write_is_a_fully_native_i32_store() {
|
|
26
|
+
// On the GBA target a `: i32` struct field is a native `i32`, so the write is a single-word store
|
|
27
|
+
// into the Vec element — the whole point of typed struct arrays for hot AI loops. Guard that the
|
|
28
|
+
// perf win is maximal there: no `Value`, no `set_prop`, no per-write allocation.
|
|
29
|
+
let rust = compile_gba("crates/tish_compile/tests/regression/struct_array_write.tish");
|
|
30
|
+
assert!(
|
|
31
|
+
rust.contains("pub x: i32"),
|
|
32
|
+
"expected the `: i32` field to be a native i32 on the GBA target\n{rust}"
|
|
33
|
+
);
|
|
34
|
+
assert!(
|
|
35
|
+
rust.contains("arr[(0_f64) as usize].x = (200_f64) as i32"),
|
|
36
|
+
"#558: expected a fully-native i32 element store on GBA\n{rust}"
|
|
37
|
+
);
|
|
38
|
+
assert!(
|
|
39
|
+
!rust.contains("set_prop(&(Value::object_from_pairs"),
|
|
40
|
+
"#558: the GBA struct write must not box + set_prop on a throwaway copy\n{rust}"
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[test]
|
|
45
|
+
fn struct_array_write_is_a_native_store_not_a_discarded_copy() {
|
|
46
|
+
let rust = compile("crates/tish_compile/tests/regression/struct_array_write.tish");
|
|
47
|
+
|
|
48
|
+
// The write must NOT build a throwaway object and set_prop on it — that mutates a copy and
|
|
49
|
+
// discards the store. This exact shape is what the bug emitted.
|
|
50
|
+
assert!(
|
|
51
|
+
!rust.contains("set_prop(&(Value::object_from_pairs"),
|
|
52
|
+
"#558: `arr[i].field = v` must not set_prop on a throwaway object_from_pairs copy\n{rust}"
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// It must instead be a direct field store into the Vec element (module-level `arr` here).
|
|
56
|
+
assert!(
|
|
57
|
+
rust.contains("arr[(0_f64) as usize].x ="),
|
|
58
|
+
"#558: expected a native `arr[(0_f64) as usize].x = …` store into the Vec element\n{rust}"
|
|
59
|
+
);
|
|
60
|
+
// …and the local array `es` inside the function.
|
|
61
|
+
assert!(
|
|
62
|
+
rust.contains("es[(0_f64) as usize].x ="),
|
|
63
|
+
"#558: expected a native `es[(0_f64) as usize].x = …` store into the local Vec element\n{rust}"
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//! tishlang/tish#558 residual — a struct-field read from a Vec element in a value/return position must
|
|
2
|
+
//! clone a non-Copy field instead of moving it out of the borrowed element (E0507).
|
|
3
|
+
use std::path::PathBuf;
|
|
4
|
+
use tishlang_compile::compile_project_full;
|
|
5
|
+
|
|
6
|
+
fn compile(rel: &str) -> String {
|
|
7
|
+
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
8
|
+
let path = manifest.join("../..").join(rel).canonicalize().unwrap();
|
|
9
|
+
compile_project_full(&path, path.parent(), &[], true).unwrap().0
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
#[test]
|
|
13
|
+
fn struct_field_read_from_vec_element_clones_non_copy() {
|
|
14
|
+
let rust = compile("crates/tish_compile/tests/regression/struct_field_return_read.tish");
|
|
15
|
+
// Off-GBA the `: i32` field is a boxed `Value` (non-Copy); the read must clone it out of the
|
|
16
|
+
// borrowed `es[0]`, not move it.
|
|
17
|
+
assert!(
|
|
18
|
+
rust.contains("es[(0_f64) as usize]).x.clone()"),
|
|
19
|
+
"#558 residual: `es[0].x` read must clone the non-Copy field out of the borrowed element\n{rust}"
|
|
20
|
+
);
|
|
21
|
+
}
|