@tishlang/tish-lsp 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-lsp +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_lsp/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-lsp +0 -0
- package/platform/darwin-x64/tish-lsp +0 -0
- package/platform/linux-arm64/tish-lsp +0 -0
- package/platform/linux-x64/tish-lsp +0 -0
- package/platform/win32-x64/tish-lsp.exe +0 -0
|
@@ -22,6 +22,11 @@ pub struct ResolvedNativeModule {
|
|
|
22
22
|
pub export_fn: String,
|
|
23
23
|
/// When false, omit `path = …` in the generated Cargo.toml (crate comes from `tish.rustDependencies` only).
|
|
24
24
|
pub use_path_dependency: bool,
|
|
25
|
+
/// The crate's own SOURCE directory (where its `Cargo.toml`/`src` live), when known — distinct from
|
|
26
|
+
/// `crate_path` (the importing project root). Used to find a `tish.d.tish` the crate ships (typed
|
|
27
|
+
/// externs). For a `cargo:` dep it's the resolved `rustDependencies` path; for a `tish.module`
|
|
28
|
+
/// package it's the package dir; `None` for registry deps with no local path.
|
|
29
|
+
pub source_dir: Option<PathBuf>,
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
/// How codegen links a native import to Rust (`generateNativeWrapper` for `tish:*`; `cargo:*` always generated).
|
|
@@ -39,6 +44,18 @@ pub enum NativeModuleInit {
|
|
|
39
44
|
},
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
impl NativeModuleInit {
|
|
48
|
+
/// The Rust crate name whose functions back this module — the path for a direct typed call
|
|
49
|
+
/// (`<crate_name>::name_typed(..)`). Both variants carry it (Legacy directly, Generated as the
|
|
50
|
+
/// shim crate the generated namespace forwards to).
|
|
51
|
+
pub fn crate_name(&self) -> &str {
|
|
52
|
+
match self {
|
|
53
|
+
NativeModuleInit::Legacy { crate_name, .. } => crate_name,
|
|
54
|
+
NativeModuleInit::Generated { shim_crate, .. } => shim_crate,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
42
59
|
/// Extra native build inputs produced alongside Rust source (Cargo merge + optional wrapper).
|
|
43
60
|
#[derive(Debug, Clone)]
|
|
44
61
|
pub struct NativeBuildArtifacts {
|
|
@@ -383,6 +400,17 @@ fn resolve_cargo_native_module(
|
|
|
383
400
|
let crate_path = project_root
|
|
384
401
|
.canonicalize()
|
|
385
402
|
.unwrap_or_else(|_| project_root.to_path_buf());
|
|
403
|
+
// The crate's own source dir, from a `rustDependencies` `path` (so we can find its `tish.d.tish`).
|
|
404
|
+
let source_dir = rust_deps
|
|
405
|
+
.get(&dep_key)
|
|
406
|
+
.and_then(|v| v.as_object())
|
|
407
|
+
.and_then(|o| o.get("path"))
|
|
408
|
+
.and_then(|p| p.as_str())
|
|
409
|
+
.map(|raw| {
|
|
410
|
+
let p = Path::new(raw);
|
|
411
|
+
let resolved = if p.is_absolute() { p.to_path_buf() } else { project_root.join(p) };
|
|
412
|
+
resolved.canonicalize().unwrap_or(resolved)
|
|
413
|
+
});
|
|
386
414
|
Ok(ResolvedNativeModule {
|
|
387
415
|
spec: spec.to_string(),
|
|
388
416
|
package_name: dep_key.clone(),
|
|
@@ -390,6 +418,7 @@ fn resolve_cargo_native_module(
|
|
|
390
418
|
crate_path,
|
|
391
419
|
export_fn,
|
|
392
420
|
use_path_dependency: false,
|
|
421
|
+
source_dir,
|
|
393
422
|
})
|
|
394
423
|
}
|
|
395
424
|
|
|
@@ -442,9 +471,11 @@ fn resolve_native_module(spec: &str, project_root: &Path) -> Result<ResolvedNati
|
|
|
442
471
|
spec: spec.to_string(),
|
|
443
472
|
package_name: raw_crate.clone(),
|
|
444
473
|
crate_name: raw_crate.replace('-', "_"),
|
|
445
|
-
crate_path,
|
|
474
|
+
crate_path: crate_path.clone(),
|
|
446
475
|
export_fn,
|
|
447
476
|
use_path_dependency: true,
|
|
477
|
+
// A tish.module package IS its own source dir — that's where any `tish.d.tish` lives.
|
|
478
|
+
source_dir: Some(crate_path),
|
|
448
479
|
})
|
|
449
480
|
}
|
|
450
481
|
|
|
@@ -1126,11 +1157,14 @@ pub fn resolve_bare_spec(spec: &str, from_dir: &Path, _project_root: &Path) -> O
|
|
|
1126
1157
|
/// directory (`from_dir`) to an absolute path that must exist — a missing file is a compile-time
|
|
1127
1158
|
/// error, not a device panic — and the spec becomes `prefix:/abs/path` (the codegen feeds that
|
|
1128
1159
|
/// absolute path to the scheme's include macro; agb's `resolve_path` takes absolute paths as-is, so
|
|
1129
|
-
/// no asset-copy step is needed). A
|
|
1160
|
+
/// no asset-copy step is needed). A trailing `@N` on the path (e.g. `font:face.ttf@7`) is stripped
|
|
1161
|
+
/// before canonicalize and reattached on the canonical spec so `{size}` is available at emit. A
|
|
1162
|
+
/// bare `font:path` (no `@N`) defaults to [`crate::schemes::FONT_DEFAULT_SIZE`]. A non-`resolve_file`
|
|
1163
|
+
/// scheme passes through unchanged.
|
|
1130
1164
|
fn resolve_scheme_spec(spec: &str, from_dir: &Path) -> Result<String, String> {
|
|
1131
|
-
let (prefix, resolve_file) = crate::schemes::with_active(|reg| {
|
|
1165
|
+
let (scheme_name, prefix, resolve_file) = crate::schemes::with_active(|reg| {
|
|
1132
1166
|
reg.matches(spec)
|
|
1133
|
-
.map(|s| (s.prefix(), s.resolve_file))
|
|
1167
|
+
.map(|s| (s.name.clone(), s.prefix(), s.resolve_file))
|
|
1134
1168
|
})
|
|
1135
1169
|
.ok_or_else(|| format!("import '{}' is not a registered scheme", spec))?;
|
|
1136
1170
|
let rest = spec.strip_prefix(&prefix).unwrap_or(spec);
|
|
@@ -1144,7 +1178,8 @@ fn resolve_scheme_spec(spec: &str, from_dir: &Path) -> Result<String, String> {
|
|
|
1144
1178
|
prefix
|
|
1145
1179
|
));
|
|
1146
1180
|
}
|
|
1147
|
-
let
|
|
1181
|
+
let (file_rest, size_opt) = crate::schemes::split_path_size(rest);
|
|
1182
|
+
let path = Path::new(file_rest);
|
|
1148
1183
|
let joined = if path.is_absolute() {
|
|
1149
1184
|
path.to_path_buf()
|
|
1150
1185
|
} else {
|
|
@@ -1155,12 +1190,23 @@ fn resolve_scheme_spec(spec: &str, from_dir: &Path) -> Result<String, String> {
|
|
|
1155
1190
|
"{} import '{}': cannot find '{}' (resolved from {}): {}",
|
|
1156
1191
|
prefix.trim_end_matches(':'),
|
|
1157
1192
|
spec,
|
|
1158
|
-
|
|
1193
|
+
file_rest,
|
|
1159
1194
|
from_dir.display(),
|
|
1160
1195
|
e
|
|
1161
1196
|
)
|
|
1162
1197
|
})?;
|
|
1163
|
-
|
|
1198
|
+
let mut out = format!("{}{}", prefix, abs.to_string_lossy());
|
|
1199
|
+
// `font:` always carries an explicit size in the canonical spec (explicit @N, or default 16).
|
|
1200
|
+
if scheme_name == "font" {
|
|
1201
|
+
let size = size_opt.unwrap_or(crate::schemes::FONT_DEFAULT_SIZE);
|
|
1202
|
+
out.push('@');
|
|
1203
|
+
out.push_str(&size.to_string());
|
|
1204
|
+
} else if let Some(size) = size_opt {
|
|
1205
|
+
// Other schemes may use `@N` + `{size}` later; preserve when present.
|
|
1206
|
+
out.push('@');
|
|
1207
|
+
out.push_str(&size.to_string());
|
|
1208
|
+
}
|
|
1209
|
+
Ok(out)
|
|
1164
1210
|
}
|
|
1165
1211
|
|
|
1166
1212
|
/// Resolve an import specifier (e.g. "./foo.tish", "../lib/utils", "lattish") to an absolute path.
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
//! * `resolve_file` — resolve the rest as a project-relative file and validate it exists.
|
|
14
14
|
//! * `targets` — per emit-target ("gba", …) code templates: the `use`s + body of a generated
|
|
15
15
|
//! `mod`, an accessor, and a registration call. Templates substitute `{path}` (the resolved
|
|
16
|
-
//! absolute path, as a Rust string literal)
|
|
16
|
+
//! absolute path, as a Rust string literal), `{mod}` (the generated module's identifier),
|
|
17
|
+
//! `{charset}` (used non-ASCII chars), and optionally `{size}` (from a trailing `@N` on the path).
|
|
17
18
|
//!
|
|
18
19
|
//! Only *file-baking* schemes are expressible this way today — the family `asset:` belongs to
|
|
19
20
|
//! (resolve a file → emit a macro + a registration + bind an i32 handle). Schemes needing
|
|
@@ -235,14 +236,38 @@ pub fn is_scheme_import(spec: &str) -> bool {
|
|
|
235
236
|
with_active(|r| r.matches(spec).is_some())
|
|
236
237
|
}
|
|
237
238
|
|
|
238
|
-
///
|
|
239
|
-
|
|
240
|
-
|
|
239
|
+
/// Default bake size when `font:path` omits `@N` (most common native grid: monogram/alagard/m3x6).
|
|
240
|
+
pub const FONT_DEFAULT_SIZE: u32 = 16;
|
|
241
|
+
|
|
242
|
+
/// Split a scheme path rest into `(file_path, size)` when it ends with `@` + digits
|
|
243
|
+
/// (`face.ttf@7` → `("face.ttf", Some(7))`). Non-digit suffixes after `@` are left intact
|
|
244
|
+
/// (`foo@2x.png` is not a size).
|
|
245
|
+
pub fn split_path_size(rest: &str) -> (&str, Option<u32>) {
|
|
246
|
+
let Some((path, size_str)) = rest.rsplit_once('@') else {
|
|
247
|
+
return (rest, None);
|
|
248
|
+
};
|
|
249
|
+
if path.is_empty() || size_str.is_empty() || !size_str.bytes().all(|b| b.is_ascii_digit()) {
|
|
250
|
+
return (rest, None);
|
|
251
|
+
}
|
|
252
|
+
match size_str.parse::<u32>() {
|
|
253
|
+
Ok(n) if n > 0 => (path, Some(n)),
|
|
254
|
+
_ => (rest, None),
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/// Substitute `{path}` (the file as a Rust string literal), `{mod}` (the generated module ident),
|
|
259
|
+
/// `{charset}` (used non-ASCII chars as a Rust string literal), and `{size}` (plain integer from a
|
|
260
|
+
/// trailing `@N` on the path, e.g. `font:face.ttf@7`) in a template. `{path}` never includes `@N`.
|
|
241
261
|
pub fn render_template(template: &str, abspath: &str, module_ident: &str, charset: &str) -> String {
|
|
262
|
+
let (path, size) = split_path_size(abspath);
|
|
263
|
+
let size_str = size
|
|
264
|
+
.map(|n| n.to_string())
|
|
265
|
+
.unwrap_or_else(|| FONT_DEFAULT_SIZE.to_string());
|
|
242
266
|
template
|
|
243
|
-
.replace("{path}", &format!("{:?}",
|
|
267
|
+
.replace("{path}", &format!("{:?}", path))
|
|
244
268
|
.replace("{mod}", module_ident)
|
|
245
269
|
.replace("{charset}", &format!("{:?}", charset))
|
|
270
|
+
.replace("{size}", &size_str)
|
|
246
271
|
}
|
|
247
272
|
|
|
248
273
|
#[cfg(test)]
|
|
@@ -275,4 +300,34 @@ mod tests {
|
|
|
275
300
|
// The same def under a valid name parses.
|
|
276
301
|
assert!(parse_scheme_def("sheet", &def).is_some());
|
|
277
302
|
}
|
|
303
|
+
|
|
304
|
+
#[test]
|
|
305
|
+
fn split_path_size_parses_trailing_at_digits() {
|
|
306
|
+
assert_eq!(split_path_size("face.ttf@7"), ("face.ttf", Some(7)));
|
|
307
|
+
assert_eq!(split_path_size("/abs/face.ttf@16"), ("/abs/face.ttf", Some(16)));
|
|
308
|
+
assert_eq!(split_path_size("face.ttf"), ("face.ttf", None));
|
|
309
|
+
// Not a size — non-digit suffix after @.
|
|
310
|
+
assert_eq!(split_path_size("foo@2x.png"), ("foo@2x.png", None));
|
|
311
|
+
assert_eq!(split_path_size("@7"), ("@7", None));
|
|
312
|
+
assert_eq!(split_path_size("face.ttf@0"), ("face.ttf@0", None));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
#[test]
|
|
316
|
+
fn render_template_substitutes_size_and_strips_at_from_path() {
|
|
317
|
+
let out = render_template(
|
|
318
|
+
"pack!({path}, {size}, {charset}); //{mod}",
|
|
319
|
+
"/abs/face.ttf@7",
|
|
320
|
+
"__scheme_font_0",
|
|
321
|
+
"你好",
|
|
322
|
+
);
|
|
323
|
+
assert!(out.contains("pack!(\"/abs/face.ttf\", 7, \"你好\")"), "got: {out}");
|
|
324
|
+
assert!(out.contains("//__scheme_font_0"));
|
|
325
|
+
assert!(!out.contains("face.ttf@7"));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#[test]
|
|
329
|
+
fn render_template_defaults_size_when_missing() {
|
|
330
|
+
let out = render_template("x({size})", "/abs/face.ttf", "m", "");
|
|
331
|
+
assert_eq!(out, format!("x({})", FONT_DEFAULT_SIZE));
|
|
332
|
+
}
|
|
278
333
|
}
|
|
@@ -255,6 +255,17 @@ impl RustType {
|
|
|
255
255
|
!matches!(self, RustType::Value)
|
|
256
256
|
}
|
|
257
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
|
+
|
|
258
269
|
/// Check if this type is numeric (f64).
|
|
259
270
|
pub fn is_numeric(&self) -> bool {
|
|
260
271
|
matches!(self, RustType::F64)
|
|
@@ -533,11 +544,19 @@ impl RustType {
|
|
|
533
544
|
// e.g. an assignment-as-expression `{ let _v = obj; lhs = <coerce _v>; _v }` reuses
|
|
534
545
|
// `_v` for its value (tishlang/tish#486). `&(expr)` evaluates the source once and
|
|
535
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);
|
|
536
554
|
format!(
|
|
537
|
-
"{{ let _src = &({}); {} {{
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
555
|
+
"{{ let _src = &({}); match _src.downcast_struct::<{}>() {{ \
|
|
556
|
+
Some(_s) => _s, \
|
|
557
|
+
None => {} {{ {} }} \
|
|
558
|
+
}} }}",
|
|
559
|
+
value_expr, struct_ident, struct_ident, field_assigns
|
|
541
560
|
)
|
|
542
561
|
}
|
|
543
562
|
RustType::ShapeUnion { .. } => {
|
|
@@ -602,34 +621,22 @@ impl RustType {
|
|
|
602
621
|
native_expr, inner_to_value
|
|
603
622
|
)
|
|
604
623
|
}
|
|
605
|
-
RustType::Named {
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
// `
|
|
611
|
-
//
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
let v_expr = if matches!(ty, RustType::Value) {
|
|
622
|
-
format!("{}.clone()", access)
|
|
623
|
-
} else {
|
|
624
|
-
ty.to_value_expr(&access)
|
|
625
|
-
};
|
|
626
|
-
format!("(::std::sync::Arc::from({:?}), {})", k.as_ref(), v_expr)
|
|
627
|
-
})
|
|
628
|
-
.collect::<Vec<_>>()
|
|
629
|
-
.join(", ");
|
|
630
|
-
// `object_from_pairs::<N>` builds the `PropMap` in one ordered pass (N = field
|
|
631
|
-
// count, a codegen-time constant) — no AHashMap, so key order is preserved.
|
|
632
|
-
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
|
+
)
|
|
633
640
|
}
|
|
634
641
|
RustType::ShapeUnion { name, variants } => {
|
|
635
642
|
// union → boxed Value::Object: one match arm per variant, delegating to that
|
|
@@ -673,6 +680,31 @@ pub fn named_struct_ident(tish_name: &str) -> String {
|
|
|
673
680
|
format!("TishStruct_{}", tish_name)
|
|
674
681
|
}
|
|
675
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
|
+
|
|
676
708
|
/// #179 Stage B: map a shape-union alias to the generated Rust enum identifier.
|
|
677
709
|
pub fn shape_union_enum_ident(tish_name: &str) -> String {
|
|
678
710
|
format!("TishUnion_{}", tish_name)
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//! tishlang/tish#558 — a captured (refcell-wrapped) native struct `Vec` must mutate NATIVELY, not fall
|
|
2
|
+
//! to the boxed path. When a struct array is closure-captured-and-mutated it is wrapped in a `VmRef`;
|
|
3
|
+
//! the native fast paths used to bail on that (`!refcell_wrapped_vars`), so `arr[i].field = v` and
|
|
4
|
+
//! `arr.push(x)` fell to the generic boxed path, which cloned+`from_struct`-boxed the WHOLE Vec into a
|
|
5
|
+
//! throwaway `Value::Array` and ran `set_prop`/`array_push` on the discarded copy — O(n) per op, the
|
|
6
|
+
//! write lost, and (for a `Vec<struct>`) an E0282 on the boxing closure since the skipped native op
|
|
7
|
+
//! never pinned the element type. These assert the native `borrow_mut` forms are emitted instead.
|
|
8
|
+
|
|
9
|
+
use std::path::PathBuf;
|
|
10
|
+
|
|
11
|
+
use tishlang_compile::compile_project_full;
|
|
12
|
+
|
|
13
|
+
fn emit(src: &str, name: &str) -> String {
|
|
14
|
+
let path = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(name);
|
|
15
|
+
std::fs::write(&path, src).unwrap();
|
|
16
|
+
compile_project_full(&path, path.parent(), &[], true).unwrap().0
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// `arr` is captured AND mutated (field-assign) by `update`, so it is refcell-wrapped.
|
|
20
|
+
const WRAPPED_ASSIGN: &str = "\
|
|
21
|
+
interface E { x: number; y: number }
|
|
22
|
+
let arr: E[] = []
|
|
23
|
+
arr.push({ x: 0, y: 0 })
|
|
24
|
+
function update() { arr[0].x = 200 }
|
|
25
|
+
update()
|
|
26
|
+
console.log(arr[0].x)
|
|
27
|
+
";
|
|
28
|
+
|
|
29
|
+
#[test]
|
|
30
|
+
fn wrapped_struct_element_field_assign_is_a_native_store() {
|
|
31
|
+
let rust = emit(WRAPPED_ASSIGN, "wrapped_assign.tish");
|
|
32
|
+
// Native element field store through the shared cell — not a `set_prop` on a boxed copy.
|
|
33
|
+
assert!(
|
|
34
|
+
rust.contains("borrow_mut())[_i]") || rust.contains(".borrow_mut())[") ,
|
|
35
|
+
"#558: wrapped `arr[i].field = v` must store natively through `(*arr.borrow_mut())[i]`.\n{rust}"
|
|
36
|
+
);
|
|
37
|
+
assert!(
|
|
38
|
+
!rust.contains("set_prop(&(tishlang_runtime::get_index(&Value::Array(VmRef::new"),
|
|
39
|
+
"#558: wrapped struct assign must NOT box the whole Vec and set_prop a throwaway copy.\n{rust}"
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// `a` is captured AND mutated (push) by `f`, so it is refcell-wrapped.
|
|
44
|
+
const WRAPPED_PUSH: &str = "\
|
|
45
|
+
interface E { x: number; y: number }
|
|
46
|
+
let a: E[] = []
|
|
47
|
+
function f() { a.push({ x: 1, y: 2 }) }
|
|
48
|
+
f()
|
|
49
|
+
console.log(a[0].x)
|
|
50
|
+
";
|
|
51
|
+
|
|
52
|
+
#[test]
|
|
53
|
+
fn wrapped_struct_push_is_a_native_push() {
|
|
54
|
+
let rust = emit(WRAPPED_PUSH, "wrapped_push.tish");
|
|
55
|
+
// Native push through the shared cell — not a boxed `array_push` on a whole-Vec copy.
|
|
56
|
+
assert!(
|
|
57
|
+
rust.contains("borrow_mut()).push("),
|
|
58
|
+
"#558: wrapped `a.push(struct)` must push natively through `(*a.borrow_mut()).push(..)`.\n{rust}"
|
|
59
|
+
);
|
|
60
|
+
assert!(
|
|
61
|
+
!rust.contains("array_push(&Value::Array(VmRef::new"),
|
|
62
|
+
"#558: wrapped struct push must NOT box the whole Vec into a throwaway array_push.\n{rust}"
|
|
63
|
+
);
|
|
64
|
+
}
|