@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
|
@@ -8,8 +8,8 @@ use std::path::Path;
|
|
|
8
8
|
use std::sync::Arc;
|
|
9
9
|
use tishlang_ast::{
|
|
10
10
|
ArrayElement, ArrowBody, BinOp, CallArg, CompoundOp, DestructElement, DestructPattern, Expr,
|
|
11
|
-
FunParam, Literal, LogicalAssignOp, MemberProp, ObjectProp, Program, Span,
|
|
12
|
-
TypeAnnotation, UnaryOp,
|
|
11
|
+
FunParam, ImportSpecifier, Literal, LogicalAssignOp, MemberProp, ObjectProp, Program, Span,
|
|
12
|
+
Statement, TypeAnnotation, UnaryOp,
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
/// Tracks variable usage for move/clone optimization.
|
|
@@ -603,7 +603,7 @@ pub fn compile_project_full_emit(
|
|
|
603
603
|
message: e,
|
|
604
604
|
span: None,
|
|
605
605
|
})?;
|
|
606
|
-
let merged = resolve::merge_modules(modules).map_err(|e| CompileError {
|
|
606
|
+
let mut merged = resolve::merge_modules(modules).map_err(|e| CompileError {
|
|
607
607
|
message: e,
|
|
608
608
|
span: None,
|
|
609
609
|
})?;
|
|
@@ -618,6 +618,24 @@ pub fn compile_project_full_emit(
|
|
|
618
618
|
span: None,
|
|
619
619
|
})?;
|
|
620
620
|
}
|
|
621
|
+
// Typed externs: a native module may ship a `tish.d.tish` beside its crate declaring the typed
|
|
622
|
+
// signatures of its exports (`declare fn set_body(e: i32, …): void`). Inject those ambient
|
|
623
|
+
// declarations so codegen can emit direct `crate::name_typed(..)` calls — the game never writes
|
|
624
|
+
// them. Only `declare fn` statements are taken; anything else in the file is ignored. Missing or
|
|
625
|
+
// unparseable files are silently skipped (the module simply keeps the boxed call path).
|
|
626
|
+
for m in &native_modules {
|
|
627
|
+
let Some(dir) = &m.source_dir else { continue };
|
|
628
|
+
let dts = dir.join("tish.d.tish");
|
|
629
|
+
if let Ok(src) = std::fs::read_to_string(&dts) {
|
|
630
|
+
if let Ok(decls) = tishlang_parser::parse(&src) {
|
|
631
|
+
for st in decls.statements {
|
|
632
|
+
if matches!(st, Statement::DeclareFun { .. }) {
|
|
633
|
+
merged.program.statements.push(st);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
621
639
|
let mut native_build =
|
|
622
640
|
resolve::compute_native_build_artifacts(&merged.program, root, &native_modules).map_err(
|
|
623
641
|
|e| CompileError {
|
|
@@ -949,6 +967,22 @@ pub(crate) struct NativeVecFnSig {
|
|
|
949
967
|
ret: VecRetKind,
|
|
950
968
|
}
|
|
951
969
|
|
|
970
|
+
/// A TYPED native import — the "typed externs" perf lever. Formed by pairing a `declare fun name(a:
|
|
971
|
+
/// i32, …): T` signature with a `cargo:`/native import of the same name (which supplies the crate).
|
|
972
|
+
/// A call `name(x, y)` whose args lower to `params` is then emitted as a DIRECT Rust call
|
|
973
|
+
/// `<crate>::name_typed(x, y)` — no `Value` boxing of the args, no `value_call` dispatch — instead of
|
|
974
|
+
/// the boxed namespace-object path. The crate provides `name_typed(<native params>)` and keeps its
|
|
975
|
+
/// boxed `fn(&[Value])` shim (which now just delegates) for dynamic call sites.
|
|
976
|
+
#[derive(Debug, Clone)]
|
|
977
|
+
pub(crate) struct ExternSig {
|
|
978
|
+
crate_name: String,
|
|
979
|
+
/// The exported Rust fn name (the import's original name, not a local alias); the direct call
|
|
980
|
+
/// targets `<crate_name>::<symbol>_typed(..)`.
|
|
981
|
+
symbol: String,
|
|
982
|
+
params: Vec<RustType>,
|
|
983
|
+
ret: RustType,
|
|
984
|
+
}
|
|
985
|
+
|
|
952
986
|
/// #173 part 3 — a symbolic upper bound used by the in-bounds index proof. Two forms are matched by
|
|
953
987
|
/// structural equality: an integer constant (`vec![K; 100]`, guard `i < 100`) and a single variable
|
|
954
988
|
/// (`a` filled to length `n`, guard `i < n`). Anything more complex (`2 * n`, `a.length` member,
|
|
@@ -984,6 +1018,13 @@ pub(crate) struct Codegen {
|
|
|
984
1018
|
features: std::collections::HashSet<String>,
|
|
985
1019
|
/// spec -> native init strategy (legacy adapter object vs generated `generated_native` wrapper)
|
|
986
1020
|
native_module_init: std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
|
|
1021
|
+
/// Typed native imports (`declare fun` + a native import): tish name -> its typed signature.
|
|
1022
|
+
/// A matching call emits a DIRECT `<crate>::name_typed(..)` instead of a boxed `value_call`.
|
|
1023
|
+
extern_fns: std::collections::HashMap<String, ExternSig>,
|
|
1024
|
+
/// User tish functions whose declared return type is a native STRUCT (`function f(): Iface`).
|
|
1025
|
+
/// A call in a typed context coerces the boxed result to the native struct (a `Value::Struct`
|
|
1026
|
+
/// downcast on GBA), so `f().field` reads are native instead of `get_prop`. Name -> `RustType::Named`.
|
|
1027
|
+
user_fn_returns: std::collections::HashMap<String, crate::types::RustType>,
|
|
987
1028
|
/// Stack: true = async Rust context (run body), false = sync closure (Tish fn body)
|
|
988
1029
|
async_context_stack: Vec<bool>,
|
|
989
1030
|
loop_stack: Vec<(String, Option<String>)>, // (break_label, continue_update) for innermost loop
|
|
@@ -1220,7 +1261,7 @@ pub(crate) struct Codegen {
|
|
|
1220
1261
|
cse_temp_counter: usize,
|
|
1221
1262
|
/// The program's used NON-ASCII characters (collected from every string literal), as a sorted
|
|
1222
1263
|
/// string. Fed to import schemes via `{charset}` so a scheme can bake only the glyphs a program
|
|
1223
|
-
/// actually uses — e.g. `font
|
|
1264
|
+
/// actually uses — e.g. `font:` bakes ASCII + these, not all ~24k glyphs of a CJK font.
|
|
1224
1265
|
string_charset: String,
|
|
1225
1266
|
}
|
|
1226
1267
|
|
|
@@ -1251,6 +1292,8 @@ impl Codegen {
|
|
|
1251
1292
|
string_charset: String::new(),
|
|
1252
1293
|
features,
|
|
1253
1294
|
native_module_init,
|
|
1295
|
+
extern_fns: std::collections::HashMap::new(),
|
|
1296
|
+
user_fn_returns: std::collections::HashMap::new(),
|
|
1254
1297
|
async_context_stack: Vec::new(),
|
|
1255
1298
|
loop_stack: Vec::new(),
|
|
1256
1299
|
break_stack: Vec::new(),
|
|
@@ -1523,7 +1566,7 @@ impl Codegen {
|
|
|
1523
1566
|
/// only way a tagless image's sprites escape). Registration into the runtime arena happens in
|
|
1524
1567
|
/// the entry below.
|
|
1525
1568
|
/// Collect every NON-ASCII character appearing in the program's string literals, sorted+unique.
|
|
1526
|
-
/// Fed to schemes via `{charset}` (see `render_template`) so a `font
|
|
1569
|
+
/// Fed to schemes via `{charset}` (see `render_template`) so a `font:` import can bake only the
|
|
1527
1570
|
/// glyphs actually used. ASCII is always baked by the scheme, so it's excluded here. Runtime-built
|
|
1528
1571
|
/// strings (concatenation, numbers) resolve to ASCII, which is covered; a non-ASCII char reachable
|
|
1529
1572
|
/// only through a computed string is the rare gap (documented — add it to a literal if needed).
|
|
@@ -1634,6 +1677,11 @@ impl Codegen {
|
|
|
1634
1677
|
));
|
|
1635
1678
|
}
|
|
1636
1679
|
self.write("}\n\n");
|
|
1680
|
+
// A `TishStruct` impl so this struct can be boxed BY REFERENCE into a `Value`
|
|
1681
|
+
// (`Value::from_struct` → `Value::Struct` on GBA) instead of serialised to a hashmap
|
|
1682
|
+
// object — property access on the boxed value then dispatches here (a small match,
|
|
1683
|
+
// no hash / alloc). See `types::to_value_expr` `Named` arm.
|
|
1684
|
+
self.emit_struct_tish_impl(&struct_name, fields);
|
|
1637
1685
|
// #315: emit a direct JSON serializer for this struct when every field is
|
|
1638
1686
|
// JSON-simple. Emitted for each eligible alias (unused ones are covered by
|
|
1639
1687
|
// the generated crate's `#![allow(unused)]`); the JSON.stringify intercept
|
|
@@ -1657,6 +1705,7 @@ impl Codegen {
|
|
|
1657
1705
|
));
|
|
1658
1706
|
}
|
|
1659
1707
|
self.write("}\n\n");
|
|
1708
|
+
self.emit_struct_tish_impl(&vstruct, vfields);
|
|
1660
1709
|
}
|
|
1661
1710
|
let enum_ident = crate::types::shape_union_enum_ident(uname);
|
|
1662
1711
|
self.write("#[derive(Clone, Debug)]\n");
|
|
@@ -1683,6 +1732,68 @@ impl Codegen {
|
|
|
1683
1732
|
}
|
|
1684
1733
|
}
|
|
1685
1734
|
|
|
1735
|
+
/// Emit `impl tishlang_runtime::TishStruct for TishStruct_<name>` so a typed struct can be boxed
|
|
1736
|
+
/// BY REFERENCE into a `Value` (`Value::from_struct`) rather than eagerly serialised to a hashmap
|
|
1737
|
+
/// object. `tish_get`/`tish_set` are the dynamic property path (a `match` on the field name — no
|
|
1738
|
+
/// hash, no alloc); `tish_to_object` is the materialisation used for JSON / `Object.keys`-family
|
|
1739
|
+
/// and as the non-portable `from_struct` fallback (identical to the historical boundary).
|
|
1740
|
+
fn emit_struct_tish_impl(
|
|
1741
|
+
&mut self,
|
|
1742
|
+
struct_name: &str,
|
|
1743
|
+
fields: &[(std::sync::Arc<str>, crate::types::RustType)],
|
|
1744
|
+
) {
|
|
1745
|
+
self.write("#[allow(non_snake_case, unused)]\n");
|
|
1746
|
+
self.write(&format!(
|
|
1747
|
+
"impl tishlang_runtime::TishStruct for {} {{\n",
|
|
1748
|
+
struct_name
|
|
1749
|
+
));
|
|
1750
|
+
|
|
1751
|
+
// tish_get: field name → boxed Value (Null for an unknown key).
|
|
1752
|
+
self.write(" fn tish_get(&self, __k: &str) -> tishlang_runtime::Value {\n");
|
|
1753
|
+
self.write(" match __k {\n");
|
|
1754
|
+
for (k, ty) in fields {
|
|
1755
|
+
let access = format!("self.{}", crate::types::field_ident(k));
|
|
1756
|
+
let v_expr = if matches!(ty, crate::types::RustType::Value) {
|
|
1757
|
+
format!("{}.clone()", access)
|
|
1758
|
+
} else {
|
|
1759
|
+
ty.to_value_expr(&access)
|
|
1760
|
+
};
|
|
1761
|
+
self.write(&format!(" {:?} => {},\n", k.as_ref(), v_expr));
|
|
1762
|
+
}
|
|
1763
|
+
self.write(" _ => tishlang_runtime::Value::Null,\n");
|
|
1764
|
+
self.write(" }\n }\n");
|
|
1765
|
+
|
|
1766
|
+
// tish_set: field name + boxed Value → coerce into the native field (unknown key = no-op).
|
|
1767
|
+
self.write(" fn tish_set(&mut self, __k: &str, __v: tishlang_runtime::Value) {\n");
|
|
1768
|
+
self.write(" match __k {\n");
|
|
1769
|
+
for (k, ty) in fields {
|
|
1770
|
+
let coerced = if matches!(ty, crate::types::RustType::Value) {
|
|
1771
|
+
"__v".to_string()
|
|
1772
|
+
} else {
|
|
1773
|
+
ty.from_value_expr("__v")
|
|
1774
|
+
};
|
|
1775
|
+
self.write(&format!(
|
|
1776
|
+
" {:?} => {{ self.{} = {}; }},\n",
|
|
1777
|
+
k.as_ref(),
|
|
1778
|
+
crate::types::field_ident(k),
|
|
1779
|
+
coerced
|
|
1780
|
+
));
|
|
1781
|
+
}
|
|
1782
|
+
self.write(" _ => {},\n");
|
|
1783
|
+
self.write(" }\n }\n");
|
|
1784
|
+
|
|
1785
|
+
// tish_to_object: ordered `object_from_pairs` (JS insertion order) — the materialisation path.
|
|
1786
|
+
self.write(" fn tish_to_object(&self) -> tishlang_runtime::Value {\n");
|
|
1787
|
+
self.write(&format!(
|
|
1788
|
+
" {}\n",
|
|
1789
|
+
crate::types::named_struct_to_object_expr(fields, "self")
|
|
1790
|
+
));
|
|
1791
|
+
self.write(" }\n");
|
|
1792
|
+
|
|
1793
|
+
self.write(" fn as_any(&self) -> &dyn core::any::Any { self }\n");
|
|
1794
|
+
self.write("}\n\n");
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1686
1797
|
/// #315: JSON-simple = serializable by a direct per-struct writer that is byte-identical
|
|
1687
1798
|
/// to node's `JSON.stringify` (reuses `write_json_number` / `escape_into`). Whitelist, so
|
|
1688
1799
|
/// any future RustType variant defaults to the sound Value-path fallback.
|
|
@@ -1794,6 +1905,100 @@ impl Codegen {
|
|
|
1794
1905
|
|
|
1795
1906
|
/// #315: direct JSON serializer for `JSON.stringify(arg)` when `arg`'s static type is a
|
|
1796
1907
|
/// simple struct (or Vec of one). Returns None → fall through to the generic Value path.
|
|
1908
|
+
/// A boxed `Value` of a struct field: a `Value`-typed field is cloned out of the borrow; a native
|
|
1909
|
+
/// field boxes through its own `to_value_expr` (which copies/clones internally). `place` is the
|
|
1910
|
+
/// borrowed struct expression (e.g. `_s`), so the access is `place.field`.
|
|
1911
|
+
fn struct_field_to_value(place: &str, k: &std::sync::Arc<str>, fty: &crate::types::RustType) -> String {
|
|
1912
|
+
let access = format!("{}.{}", place, crate::types::field_ident(k));
|
|
1913
|
+
if matches!(fty, crate::types::RustType::Value) {
|
|
1914
|
+
format!("{}.clone()", access)
|
|
1915
|
+
} else {
|
|
1916
|
+
fty.to_value_expr(&access)
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
/// Static-type-aware reflection: `Object.keys(s)` / `Object.values(s)` / `Object.entries(s)` where
|
|
1921
|
+
/// `s`'s static type is a known struct (`RustType::Named`). Reads the struct's fields NATIVELY, so
|
|
1922
|
+
/// the struct is never boxed to a `Value::Struct` (GBA) / hashmap object (elsewhere) and no
|
|
1923
|
+
/// intermediate object is built — only the RESULT array is a `Value` (that is the op's output, and
|
|
1924
|
+
/// unavoidable). Keys come from the compile-time field list; values are native field reads. Returns
|
|
1925
|
+
/// `None` (→ the generic boxed `Object.*` path) for a non-struct arg, a spread/multi arg, or a
|
|
1926
|
+
/// dynamic key. This is target-agnostic: the `TishStruct_*` Rust struct exists on every native
|
|
1927
|
+
/// backend, so the same native reads apply on GBA and desktop alike.
|
|
1928
|
+
fn try_emit_native_object_reflection(
|
|
1929
|
+
&mut self,
|
|
1930
|
+
callee: &Expr,
|
|
1931
|
+
args: &[CallArg],
|
|
1932
|
+
) -> Result<Option<String>, CompileError> {
|
|
1933
|
+
use crate::types::RustType;
|
|
1934
|
+
// GBA only. Desktop keeps reflection on the existing boxed path — it's already correct there
|
|
1935
|
+
// (a struct materialises to a real `Value::Object`), and gating avoids any churn to desktop
|
|
1936
|
+
// benches / codegen-string tests. GBA is where avoiding the `Value::Struct` box + hashmap
|
|
1937
|
+
// actually matters. The native reads are target-agnostic, so this path was validated by
|
|
1938
|
+
// running it on desktop before the gate went in.
|
|
1939
|
+
if self.emit_mode != crate::NativeEmitMode::Gba {
|
|
1940
|
+
return Ok(None);
|
|
1941
|
+
}
|
|
1942
|
+
let Expr::Member {
|
|
1943
|
+
object,
|
|
1944
|
+
prop: MemberProp::Name { name: m, .. },
|
|
1945
|
+
optional: false,
|
|
1946
|
+
..
|
|
1947
|
+
} = callee
|
|
1948
|
+
else {
|
|
1949
|
+
return Ok(None);
|
|
1950
|
+
};
|
|
1951
|
+
if !matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Object") {
|
|
1952
|
+
return Ok(None);
|
|
1953
|
+
}
|
|
1954
|
+
let kind = m.as_ref();
|
|
1955
|
+
if !matches!(kind, "keys" | "values" | "entries") || args.len() != 1 {
|
|
1956
|
+
return Ok(None);
|
|
1957
|
+
}
|
|
1958
|
+
let CallArg::Expr(arg0) = &args[0] else {
|
|
1959
|
+
return Ok(None);
|
|
1960
|
+
};
|
|
1961
|
+
let (code, ty) = self.emit_typed_expr(arg0)?;
|
|
1962
|
+
let RustType::Named { fields, .. } = &ty else {
|
|
1963
|
+
return Ok(None);
|
|
1964
|
+
};
|
|
1965
|
+
let out = match kind {
|
|
1966
|
+
// Keys are the compile-time field names; the struct is evaluated only for side effects.
|
|
1967
|
+
"keys" => {
|
|
1968
|
+
let keys = fields
|
|
1969
|
+
.iter()
|
|
1970
|
+
.map(|(k, _)| format!("Value::String({:?}.into())", k.as_ref()))
|
|
1971
|
+
.collect::<Vec<_>>()
|
|
1972
|
+
.join(", ");
|
|
1973
|
+
format!("{{ let _ = &({}); Value::Array(VmRef::new(vec![{}])) }}", code, keys)
|
|
1974
|
+
}
|
|
1975
|
+
"values" => {
|
|
1976
|
+
let vals = fields
|
|
1977
|
+
.iter()
|
|
1978
|
+
.map(|(k, fty)| Self::struct_field_to_value("_s", k, fty))
|
|
1979
|
+
.collect::<Vec<_>>()
|
|
1980
|
+
.join(", ");
|
|
1981
|
+
format!("{{ let _s = &({}); Value::Array(VmRef::new(vec![{}])) }}", code, vals)
|
|
1982
|
+
}
|
|
1983
|
+
"entries" => {
|
|
1984
|
+
let pairs = fields
|
|
1985
|
+
.iter()
|
|
1986
|
+
.map(|(k, fty)| {
|
|
1987
|
+
format!(
|
|
1988
|
+
"Value::Array(VmRef::new(vec![Value::String({:?}.into()), {}]))",
|
|
1989
|
+
k.as_ref(),
|
|
1990
|
+
Self::struct_field_to_value("_s", k, fty)
|
|
1991
|
+
)
|
|
1992
|
+
})
|
|
1993
|
+
.collect::<Vec<_>>()
|
|
1994
|
+
.join(", ");
|
|
1995
|
+
format!("{{ let _s = &({}); Value::Array(VmRef::new(vec![{}])) }}", code, pairs)
|
|
1996
|
+
}
|
|
1997
|
+
_ => unreachable!("kind checked above"),
|
|
1998
|
+
};
|
|
1999
|
+
Ok(Some(out))
|
|
2000
|
+
}
|
|
2001
|
+
|
|
1797
2002
|
fn try_emit_direct_json_stringify(
|
|
1798
2003
|
&mut self,
|
|
1799
2004
|
arg: &Expr,
|
|
@@ -2502,8 +2707,117 @@ impl Codegen {
|
|
|
2502
2707
|
out
|
|
2503
2708
|
}
|
|
2504
2709
|
|
|
2710
|
+
/// Typed externs (#perf): pair each top-level `declare fun name(a: i32, …): T` with a native
|
|
2711
|
+
/// import of the same name (which supplies the crate) → an [`ExternSig`] in `extern_fns`. A later
|
|
2712
|
+
/// call to `name` with lowerable args emits a DIRECT `<crate>::name_typed(..)` instead of a boxed
|
|
2713
|
+
/// `value_call`. Only fully-typed simple-param signatures qualify; anything else stays boxed.
|
|
2714
|
+
fn collect_extern_fns(&mut self, program: &Program) {
|
|
2715
|
+
use std::collections::HashMap;
|
|
2716
|
+
// Resolve lowers each `import { x } from 'cargo:…'` to `let x = NativeModuleLoad{spec, export}`,
|
|
2717
|
+
// so pair off THOSE bindings (the `Import` statement itself is gone by codegen): local name ->
|
|
2718
|
+
// (module spec, exported Rust fn name). Only a native module with a known init can back a call.
|
|
2719
|
+
let mut name_info: HashMap<String, (String, String)> = HashMap::new();
|
|
2720
|
+
for stmt in &program.statements {
|
|
2721
|
+
if let Statement::VarDecl {
|
|
2722
|
+
name,
|
|
2723
|
+
init: Some(Expr::NativeModuleLoad { spec, export_name, .. }),
|
|
2724
|
+
..
|
|
2725
|
+
} = stmt
|
|
2726
|
+
{
|
|
2727
|
+
if self.native_module_init.contains_key(spec.as_ref()) {
|
|
2728
|
+
name_info.insert(
|
|
2729
|
+
name.to_string(),
|
|
2730
|
+
(spec.to_string(), export_name.to_string()),
|
|
2731
|
+
);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
if name_info.is_empty() {
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
for stmt in &program.statements {
|
|
2739
|
+
let Statement::DeclareFun {
|
|
2740
|
+
name,
|
|
2741
|
+
params,
|
|
2742
|
+
rest_param: None,
|
|
2743
|
+
return_type,
|
|
2744
|
+
async_: false,
|
|
2745
|
+
..
|
|
2746
|
+
} = stmt
|
|
2747
|
+
else {
|
|
2748
|
+
continue;
|
|
2749
|
+
};
|
|
2750
|
+
let Some((spec, export_name)) = name_info.get(name.as_ref()) else {
|
|
2751
|
+
continue;
|
|
2752
|
+
};
|
|
2753
|
+
let crate_name = match self.native_module_init.get(spec) {
|
|
2754
|
+
Some(init) => init.crate_name().to_string(),
|
|
2755
|
+
None => continue,
|
|
2756
|
+
};
|
|
2757
|
+
// Every parameter must be a simple, explicitly-typed native scalar; a `Value` param (an
|
|
2758
|
+
// untyped one) means the direct call would gain nothing, so keep the whole fn boxed.
|
|
2759
|
+
let mut ptys = Vec::with_capacity(params.len());
|
|
2760
|
+
let mut ok = true;
|
|
2761
|
+
for p in params {
|
|
2762
|
+
match p {
|
|
2763
|
+
FunParam::Simple(tp) if tp.default.is_none() => match &tp.type_ann {
|
|
2764
|
+
Some(ann) => {
|
|
2765
|
+
let ty = RustType::from_annotation(ann);
|
|
2766
|
+
if ty.is_native() {
|
|
2767
|
+
ptys.push(ty);
|
|
2768
|
+
} else {
|
|
2769
|
+
ok = false;
|
|
2770
|
+
break;
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
None => {
|
|
2774
|
+
ok = false;
|
|
2775
|
+
break;
|
|
2776
|
+
}
|
|
2777
|
+
},
|
|
2778
|
+
_ => {
|
|
2779
|
+
ok = false;
|
|
2780
|
+
break;
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
if !ok {
|
|
2785
|
+
continue;
|
|
2786
|
+
}
|
|
2787
|
+
let ret = return_type
|
|
2788
|
+
.as_ref()
|
|
2789
|
+
.map(RustType::from_annotation)
|
|
2790
|
+
.unwrap_or(RustType::Unit);
|
|
2791
|
+
self.extern_fns.insert(
|
|
2792
|
+
name.to_string(),
|
|
2793
|
+
ExternSig { crate_name, symbol: export_name.clone(), params: ptys, ret },
|
|
2794
|
+
);
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
/// Record every top-level `function f(...): Iface` whose return annotation resolves — via the type
|
|
2799
|
+
/// aliases — to a native STRUCT (`RustType::Named`, or a `ShapeUnion`). A later call to `f` in a
|
|
2800
|
+
/// typed position then coerces the boxed result to the native struct (a `Value::Struct` downcast on
|
|
2801
|
+
/// GBA, the `get_prop` rebuild elsewhere), so `f().field` reads are native, not boxed. Runs AFTER
|
|
2802
|
+
/// `collect_type_aliases` so an interface name in the annotation resolves to its `Named` type.
|
|
2803
|
+
fn collect_user_fn_returns(&mut self, program: &Program) {
|
|
2804
|
+
for stmt in &program.statements {
|
|
2805
|
+
if let Statement::FunDecl { name, return_type: Some(ann), .. } = stmt {
|
|
2806
|
+
let ty = RustType::from_annotation_with_aliases(ann, &self.type_aliases);
|
|
2807
|
+
// Only a concrete `Named` struct return is coerced. `ShapeUnion` is deliberately
|
|
2808
|
+
// excluded: its `from_value_expr` is a forbidden boundary (`unreachable!()`), so
|
|
2809
|
+
// admitting it here would compile a runtime panic into the coercion site. (A shape
|
|
2810
|
+
// union only ever arises as an array element type, never a bare fn return, so this
|
|
2811
|
+
// is a defensive guard against a violated invariant, not a live path.)
|
|
2812
|
+
if matches!(ty, RustType::Named { .. }) {
|
|
2813
|
+
self.user_fn_returns.insert(name.to_string(), ty);
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2505
2819
|
fn emit_program(&mut self, program: &Program) -> Result<(), CompileError> {
|
|
2506
|
-
// Only GBA `font
|
|
2820
|
+
// Only GBA `font:`/`emoji:` schemes consume `{charset}` (and both `render_template`
|
|
2507
2821
|
// callers that read `string_charset` are Gba-gated), so skip the whole-program string walk
|
|
2508
2822
|
// for every other target — its result is never read off-GBA.
|
|
2509
2823
|
if self.emit_mode == crate::NativeEmitMode::Gba {
|
|
@@ -2513,6 +2827,7 @@ impl Codegen {
|
|
|
2513
2827
|
self.program_has_jsx = tishlang_ui::jsx::program_contains_jsx(program);
|
|
2514
2828
|
self.program_fun_decl_names = tishlang_ui::jsx::collect_fun_decl_names(program);
|
|
2515
2829
|
self.program_uses_document = crate::resolve::program_uses_document(program);
|
|
2830
|
+
self.collect_extern_fns(program);
|
|
2516
2831
|
if self.emit_mode == crate::NativeEmitMode::Gba {
|
|
2517
2832
|
// no_std GBA ROM: alloc-based, Arc aliases to Rc in the facade. The
|
|
2518
2833
|
// prelude `use tishlang_runtime::{…}` below is unchanged — those names
|
|
@@ -2584,6 +2899,9 @@ impl Codegen {
|
|
|
2584
2899
|
// are stored too, so later annotations like `let x: N = 0` still
|
|
2585
2900
|
// pick up the right native type.
|
|
2586
2901
|
self.collect_type_aliases(&program.statements);
|
|
2902
|
+
// Now that the interface/type-alias table is built, record user functions that return a native
|
|
2903
|
+
// struct, so a typed call to one keeps the result native (`f().field` = a Rust field read).
|
|
2904
|
+
self.collect_user_fn_returns(program);
|
|
2587
2905
|
// Emit a Rust `struct` for every alias whose RHS is an object
|
|
2588
2906
|
// shape. Subsequent `let x: Foo = ...` literals lower to plain
|
|
2589
2907
|
// struct moves (no `VmRef::new(ObjectMap::from(..))` allocation),
|
|
@@ -3262,6 +3580,15 @@ impl Codegen {
|
|
|
3262
3580
|
return Ok(code);
|
|
3263
3581
|
}
|
|
3264
3582
|
}
|
|
3583
|
+
// #558: `arr[i].field = v` / `s.field = v` on a typed struct → a native element store, not a
|
|
3584
|
+
// discarded set_prop on a copy. Side-effect position (statement), so no value is produced.
|
|
3585
|
+
if let Expr::MemberAssign { object, prop, value, .. } = expr {
|
|
3586
|
+
if let Some(code) =
|
|
3587
|
+
self.try_emit_native_struct_field_assign(object, prop.as_ref(), value, true)?
|
|
3588
|
+
{
|
|
3589
|
+
return Ok(code);
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3265
3592
|
match expr {
|
|
3266
3593
|
Expr::Assign { name, value, .. } => {
|
|
3267
3594
|
if self.native_numeric_globals.contains_key(name.as_ref()) {
|
|
@@ -3360,6 +3687,11 @@ impl Codegen {
|
|
|
3360
3687
|
let (val_code, val_ty) = self.emit_typed_expr(value)?;
|
|
3361
3688
|
let native_val = if val_ty == RustType::Value {
|
|
3362
3689
|
rust_type.from_value_expr(&val_code)
|
|
3690
|
+
} else if rust_type == RustType::Fixed && val_ty == RustType::F64 {
|
|
3691
|
+
// an f64 (an int/float literal like `nx = 0`, or a promoted int) assigned to a
|
|
3692
|
+
// `fixed` local must be lifted to `Fixed` — the RHS emitter reports numeric
|
|
3693
|
+
// literals as F64, so without this a `fixed` reassignment mistypes (E0308).
|
|
3694
|
+
format!("tishlang_runtime::Fixed::from_raw((({}) * 256.0) as i32)", val_code)
|
|
3363
3695
|
} else {
|
|
3364
3696
|
val_code
|
|
3365
3697
|
};
|
|
@@ -6322,7 +6654,8 @@ impl Codegen {
|
|
|
6322
6654
|
matches!(
|
|
6323
6655
|
t,
|
|
6324
6656
|
RustType::F64 | RustType::Bool | RustType::String
|
|
6325
|
-
|
|
6657
|
+
| RustType::Fixed | RustType::I32
|
|
6658
|
+
) || t.is_narrow_int()
|
|
6326
6659
|
})
|
|
6327
6660
|
} else {
|
|
6328
6661
|
None
|
|
@@ -6430,6 +6763,21 @@ impl Codegen {
|
|
|
6430
6763
|
for v in &read_only_outer_vars {
|
|
6431
6764
|
self.refcell_wrapped_vars.remove(v);
|
|
6432
6765
|
}
|
|
6766
|
+
// tishlang/tish#556: a PARAMETER (or the rest param) SHADOWS any outer var of the
|
|
6767
|
+
// same name — inside the body it is a plain `let`-bound `Value`/native, never the
|
|
6768
|
+
// outer `VmRef` cell. Drop such names from the wrapped set for the body, so a
|
|
6769
|
+
// reference to the param isn't emitted as `vm_read(¶m)` (which won't compile —
|
|
6770
|
+
// the param is a `Value`, not a `VmRef`). `saved_refcell` restores the outer view
|
|
6771
|
+
// after the body; a body-local `let` that a nested closure captures is re-added by
|
|
6772
|
+
// the `body_cell_vars` prepass below, so its cell handling is unaffected.
|
|
6773
|
+
for p in params {
|
|
6774
|
+
for bn in p.bound_names() {
|
|
6775
|
+
self.refcell_wrapped_vars.remove(bn.as_ref());
|
|
6776
|
+
}
|
|
6777
|
+
}
|
|
6778
|
+
if let Some(rest) = rest_param {
|
|
6779
|
+
self.refcell_wrapped_vars.remove(rest.name.as_ref());
|
|
6780
|
+
}
|
|
6433
6781
|
|
|
6434
6782
|
// Pre-scan body for nested functions (handles function body as Block)
|
|
6435
6783
|
if let Statement::Block { statements, .. } = body.as_ref() {
|
|
@@ -7153,6 +7501,12 @@ impl Codegen {
|
|
|
7153
7501
|
}
|
|
7154
7502
|
}
|
|
7155
7503
|
}
|
|
7504
|
+
// Static-type-aware reflection: `Object.keys/values/entries(s)` on a statically-known
|
|
7505
|
+
// struct → native field reads, never boxing the struct or building an intermediate
|
|
7506
|
+
// object. Falls through to the generic boxed `Object.*` path for a non-struct arg.
|
|
7507
|
+
if let Some(code) = self.try_emit_native_object_reflection(callee, args)? {
|
|
7508
|
+
return Ok(code);
|
|
7509
|
+
}
|
|
7156
7510
|
// #178: route a boxed-context call to a recursive-struct consumer
|
|
7157
7511
|
// (`sum(build(n))` / `sum(node)`) → `Value::Number(sum_rec(&..))`.
|
|
7158
7512
|
if self.rec_struct_plan.is_some() {
|
|
@@ -7184,29 +7538,273 @@ impl Codegen {
|
|
|
7184
7538
|
// ── native Vec<T> push fast path ──────────────────────────────
|
|
7185
7539
|
if method_name.as_ref() == "push" {
|
|
7186
7540
|
if let Expr::Ident { name, .. } = object.as_ref() {
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7541
|
+
let obj_type = self.type_context.get_type(name.as_ref());
|
|
7542
|
+
if let RustType::Vec(elem_type) = obj_type {
|
|
7543
|
+
let esc_obj = Self::escape_ident(name.as_ref()).into_owned();
|
|
7544
|
+
// A captured (refcell-wrapped) native Vec pushes through
|
|
7545
|
+
// `(*a.borrow_mut())` — WITHOUT this it fell to the boxed `array_push`,
|
|
7546
|
+
// which boxed the WHOLE Vec into a throwaway `Value::Array` (discarding
|
|
7547
|
+
// the push, O(n), and — for a `Vec<struct>` — E0282 on the `from_struct`
|
|
7548
|
+
// boxing closure since the native push never ran to pin the element type).
|
|
7549
|
+
let base = if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
7550
|
+
format!("(*{}.borrow_mut())", esc_obj)
|
|
7551
|
+
} else {
|
|
7552
|
+
esc_obj
|
|
7553
|
+
};
|
|
7554
|
+
// Collect push arguments as native values.
|
|
7555
|
+
let mut push_stmts: Vec<String> = Vec::new();
|
|
7556
|
+
for a in args {
|
|
7557
|
+
if let CallArg::Expr(e) = a {
|
|
7558
|
+
// Emit the element AGAINST the Vec's element type so an object
|
|
7559
|
+
// literal hits the struct-literal fast path (direct `Struct { … }`
|
|
7560
|
+
// with each field value emitted natively — zero allocations)
|
|
7561
|
+
// instead of building a boxed `object_from_pairs` and then
|
|
7562
|
+
// `get_prop`-ing every field back out. emit_native_expr falls
|
|
7563
|
+
// back to the typed/value path for non-literal args.
|
|
7564
|
+
let native_val = self.emit_native_expr(e, elem_type.as_ref())?;
|
|
7565
|
+
push_stmts.push(format!("{}.push({});", base, native_val));
|
|
7204
7566
|
}
|
|
7205
|
-
return Ok(format!(
|
|
7206
|
-
"{{ {} Value::Null }}",
|
|
7207
|
-
push_stmts.join(" ")
|
|
7208
|
-
));
|
|
7209
7567
|
}
|
|
7568
|
+
// `push` returns the new length (JS / tish interp) — a plain read, no
|
|
7569
|
+
// O(n) boxing. A captured Vec reads its length through `(*a.borrow())`.
|
|
7570
|
+
let base_read = if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
7571
|
+
format!("(*{}.borrow())", Self::escape_ident(name.as_ref()))
|
|
7572
|
+
} else {
|
|
7573
|
+
Self::escape_ident(name.as_ref()).into_owned()
|
|
7574
|
+
};
|
|
7575
|
+
return Ok(format!(
|
|
7576
|
+
"{{ {} Value::Number({}.len() as f64) }}",
|
|
7577
|
+
push_stmts.join(" "),
|
|
7578
|
+
base_read
|
|
7579
|
+
));
|
|
7580
|
+
}
|
|
7581
|
+
}
|
|
7582
|
+
}
|
|
7583
|
+
|
|
7584
|
+
// Native in-place mutators on a native `Vec` (`pop`/`shift`/`unshift`/`reverse`).
|
|
7585
|
+
// The boxed `array_*` fallback operates on a THROWAWAY boxed copy of the Vec, so the
|
|
7586
|
+
// mutation is DISCARDED (O(n), and — for a `Vec<struct>` — an E0282 on the boxing
|
|
7587
|
+
// closure); these mutate the real Vec. A captured Vec goes through `(*a.borrow_mut())`.
|
|
7588
|
+
// Return values match JS / tish interp: pop/shift yield the removed element (boxed) or
|
|
7589
|
+
// Null; unshift yields the new length; reverse yields THE array (boxed copy — a native
|
|
7590
|
+
// `Vec` has no O(1) by-ref boxing like interp's Rc-array, so the contents match but it
|
|
7591
|
+
// is not aliased; reverse is O(n) anyway so the copy is asymptotically free).
|
|
7592
|
+
if matches!(method_name.as_ref(), "pop" | "shift" | "unshift" | "reverse") {
|
|
7593
|
+
if let Expr::Ident { name, .. } = object.as_ref() {
|
|
7594
|
+
if let RustType::Vec(elem_type) =
|
|
7595
|
+
self.type_context.get_type(name.as_ref())
|
|
7596
|
+
{
|
|
7597
|
+
let esc = Self::escape_ident(name.as_ref()).into_owned();
|
|
7598
|
+
let wrapped_v = self.refcell_wrapped_vars.contains(name.as_ref());
|
|
7599
|
+
let base = if wrapped_v {
|
|
7600
|
+
format!("(*{}.borrow_mut())", esc)
|
|
7601
|
+
} else {
|
|
7602
|
+
esc.clone()
|
|
7603
|
+
};
|
|
7604
|
+
// Shared-borrow read (for `reverse`'s result boxing / length reads).
|
|
7605
|
+
let base_read = if wrapped_v {
|
|
7606
|
+
format!("(*{}.borrow())", esc)
|
|
7607
|
+
} else {
|
|
7608
|
+
esc.clone()
|
|
7609
|
+
};
|
|
7610
|
+
// Box a removed element back to a `Value` (`Value` elements pass through).
|
|
7611
|
+
let box_elem = |v: &str| -> String {
|
|
7612
|
+
if matches!(elem_type.as_ref(), RustType::Value) {
|
|
7613
|
+
v.to_string()
|
|
7614
|
+
} else {
|
|
7615
|
+
elem_type.to_value_expr(v)
|
|
7616
|
+
}
|
|
7617
|
+
};
|
|
7618
|
+
let code = match method_name.as_ref() {
|
|
7619
|
+
"pop" => format!(
|
|
7620
|
+
"{{ match {b}.pop() {{ Some(__e) => {v}, None => Value::Null }} }}",
|
|
7621
|
+
b = base,
|
|
7622
|
+
v = box_elem("__e")
|
|
7623
|
+
),
|
|
7624
|
+
"shift" => format!(
|
|
7625
|
+
"{{ if {b}.is_empty() {{ Value::Null }} else {{ let __e = {b}.remove(0); {v} }} }}",
|
|
7626
|
+
b = base,
|
|
7627
|
+
v = box_elem("__e")
|
|
7628
|
+
),
|
|
7629
|
+
"unshift" => {
|
|
7630
|
+
// Bind items to temps BEFORE any `borrow_mut` insert (an item may
|
|
7631
|
+
// read the same array), then insert at the front in REVERSE so the
|
|
7632
|
+
// argument order is preserved. Returns the new length.
|
|
7633
|
+
let mut binds = String::new();
|
|
7634
|
+
let mut names: Vec<String> = Vec::new();
|
|
7635
|
+
for (i, a) in args.iter().enumerate() {
|
|
7636
|
+
if let CallArg::Expr(e) = a {
|
|
7637
|
+
let it = self.emit_native_expr(e, elem_type.as_ref())?;
|
|
7638
|
+
binds.push_str(&format!("let __u{} = {}; ", i, it));
|
|
7639
|
+
names.push(format!("__u{}", i));
|
|
7640
|
+
}
|
|
7641
|
+
}
|
|
7642
|
+
let mut ins = String::new();
|
|
7643
|
+
for n in names.iter().rev() {
|
|
7644
|
+
ins.push_str(&format!("{}.insert(0, {}); ", base, n));
|
|
7645
|
+
}
|
|
7646
|
+
format!(
|
|
7647
|
+
"{{ {}{}Value::Number({}.len() as f64) }}",
|
|
7648
|
+
binds, ins, base
|
|
7649
|
+
)
|
|
7650
|
+
}
|
|
7651
|
+
// reverse in place, then return a boxed copy of the array (contents
|
|
7652
|
+
// match interp; see the note above).
|
|
7653
|
+
"reverse" => format!(
|
|
7654
|
+
"{{ {}.reverse(); Value::Array(VmRef::new({}.iter().cloned().map(|__e| {}).collect())) }}",
|
|
7655
|
+
base,
|
|
7656
|
+
base_read,
|
|
7657
|
+
box_elem("__e")
|
|
7658
|
+
),
|
|
7659
|
+
_ => unreachable!("method checked above"),
|
|
7660
|
+
};
|
|
7661
|
+
return Ok(code);
|
|
7662
|
+
}
|
|
7663
|
+
}
|
|
7664
|
+
}
|
|
7665
|
+
|
|
7666
|
+
// Native `splice`/`fill`/`copyWithin` on a native `Vec`. Like the mutators above,
|
|
7667
|
+
// the boxed fallback would run on a THROWAWAY copy (mutation discarded). These need
|
|
7668
|
+
// several accesses (len + index math + a range op), so a captured Vec binds ONE
|
|
7669
|
+
// `borrow_mut` guard `__g` for the whole body; every argument is evaluated BEFORE the
|
|
7670
|
+
// guard (an arg may read the same array, which would double-borrow the cell). JS index
|
|
7671
|
+
// clamping (negative → from end, out-of-range → clamped) is emitted inline so the
|
|
7672
|
+
// result matches the interpreter.
|
|
7673
|
+
if matches!(method_name.as_ref(), "splice" | "fill" | "copyWithin") {
|
|
7674
|
+
if let Expr::Ident { name, .. } = object.as_ref() {
|
|
7675
|
+
if let RustType::Vec(elem_type) =
|
|
7676
|
+
self.type_context.get_type(name.as_ref())
|
|
7677
|
+
{
|
|
7678
|
+
let esc = Self::escape_ident(name.as_ref()).into_owned();
|
|
7679
|
+
let wrapped = self.refcell_wrapped_vars.contains(name.as_ref());
|
|
7680
|
+
// `p` is the Vec place inside the body: a `borrow_mut` guard when wrapped.
|
|
7681
|
+
let (guard, p) = if wrapped {
|
|
7682
|
+
(format!("let mut __g = {}.borrow_mut(); ", esc), "__g".to_string())
|
|
7683
|
+
} else {
|
|
7684
|
+
(String::new(), esc.clone())
|
|
7685
|
+
};
|
|
7686
|
+
let elem_rust = elem_type.to_rust_type_str();
|
|
7687
|
+
let box_elem = |v: &str| -> String {
|
|
7688
|
+
if matches!(elem_type.as_ref(), RustType::Value) {
|
|
7689
|
+
v.to_string()
|
|
7690
|
+
} else {
|
|
7691
|
+
elem_type.to_value_expr(v)
|
|
7692
|
+
}
|
|
7693
|
+
};
|
|
7694
|
+
// `fill`/`copyWithin` return THE array (JS / tish interp) — a boxed copy
|
|
7695
|
+
// built from the (still-borrowed) place `p` after the mutation. Contents
|
|
7696
|
+
// match interp; not aliased (a native `Vec` has no O(1) by-ref boxing).
|
|
7697
|
+
let arr_box = format!(
|
|
7698
|
+
"Value::Array(VmRef::new({}.iter().cloned().map(|__be| {}).collect()))",
|
|
7699
|
+
p,
|
|
7700
|
+
box_elem("__be")
|
|
7701
|
+
);
|
|
7702
|
+
// f64 arg (bound before the guard) → a clamped `usize` index.
|
|
7703
|
+
let clamp = |v: &str| -> String {
|
|
7704
|
+
format!(
|
|
7705
|
+
"(if {v} < 0.0 {{ (__lenf + {v}).max(0.0) }} else {{ {v}.min(__lenf) }}) as usize"
|
|
7706
|
+
)
|
|
7707
|
+
};
|
|
7708
|
+
let argf = |cg: &mut Self, i: usize| -> Result<Option<String>, CompileError> {
|
|
7709
|
+
match args.get(i) {
|
|
7710
|
+
Some(CallArg::Expr(e)) => {
|
|
7711
|
+
Ok(Some(cg.emit_native_expr(e, &RustType::F64)?))
|
|
7712
|
+
}
|
|
7713
|
+
_ => Ok(None),
|
|
7714
|
+
}
|
|
7715
|
+
};
|
|
7716
|
+
let code = match method_name.as_ref() {
|
|
7717
|
+
"splice" => {
|
|
7718
|
+
let start = argf(self, 0)?.unwrap_or_else(|| "0.0".into());
|
|
7719
|
+
let dc = argf(self, 1)?;
|
|
7720
|
+
// Items to insert (args[2..]) — bound before the guard.
|
|
7721
|
+
let mut item_binds = String::new();
|
|
7722
|
+
let mut item_names: Vec<String> = Vec::new();
|
|
7723
|
+
for (i, a) in args.iter().enumerate().skip(2) {
|
|
7724
|
+
if let CallArg::Expr(e) = a {
|
|
7725
|
+
let it = self.emit_native_expr(e, elem_type.as_ref())?;
|
|
7726
|
+
item_binds.push_str(&format!("let __it{} = {}; ", i, it));
|
|
7727
|
+
item_names.push(format!("__it{}", i));
|
|
7728
|
+
}
|
|
7729
|
+
}
|
|
7730
|
+
let dc_bind = dc
|
|
7731
|
+
.as_ref()
|
|
7732
|
+
.map(|d| format!("let __dc = {}; ", d))
|
|
7733
|
+
.unwrap_or_default();
|
|
7734
|
+
let del_calc = if dc.is_some() {
|
|
7735
|
+
"let __del = if __dc < 0.0 { 0usize } else { (__dc as usize).min(__len - __start) };"
|
|
7736
|
+
} else {
|
|
7737
|
+
"let __del = __len - __start;"
|
|
7738
|
+
};
|
|
7739
|
+
format!(
|
|
7740
|
+
"{{ let __s0 = {start}; {dc_bind}{item_binds}{guard}\
|
|
7741
|
+
let __len = {p}.len(); let __lenf = __len as f64; \
|
|
7742
|
+
let __start = {start_clamp}; {del_calc} \
|
|
7743
|
+
let __items: Vec<{elem_rust}> = vec![{items}]; \
|
|
7744
|
+
let __removed: Vec<{elem_rust}> = {p}.splice(__start..__start + __del, __items).collect(); \
|
|
7745
|
+
Value::Array(VmRef::new(__removed.into_iter().map(|__e| {boxed}).collect())) }}",
|
|
7746
|
+
start_clamp = clamp("__s0"),
|
|
7747
|
+
items = item_names.join(", "),
|
|
7748
|
+
boxed = box_elem("__e"),
|
|
7749
|
+
)
|
|
7750
|
+
}
|
|
7751
|
+
"fill" => {
|
|
7752
|
+
// fill(value, start=0, end=len)
|
|
7753
|
+
let val = match args.first() {
|
|
7754
|
+
Some(CallArg::Expr(e)) => {
|
|
7755
|
+
self.emit_native_expr(e, elem_type.as_ref())?
|
|
7756
|
+
}
|
|
7757
|
+
_ => elem_type.default_value(),
|
|
7758
|
+
};
|
|
7759
|
+
let start = argf(self, 1)?.unwrap_or_else(|| "0.0".into());
|
|
7760
|
+
let end = argf(self, 2)?;
|
|
7761
|
+
format!(
|
|
7762
|
+
"{{ let __v = {val}; let __s0 = {start}; {end}{guard}\
|
|
7763
|
+
let __len = {p}.len(); let __lenf = __len as f64; \
|
|
7764
|
+
let __start = {start_clamp}; {end_clamp}\
|
|
7765
|
+
let mut __i = __start; while __i < __end {{ {p}[__i] = __v.clone(); __i += 1; }} \
|
|
7766
|
+
{arr_box} }}",
|
|
7767
|
+
end = end
|
|
7768
|
+
.as_ref()
|
|
7769
|
+
.map(|e| format!("let __e0 = {}; ", e))
|
|
7770
|
+
.unwrap_or_default(),
|
|
7771
|
+
start_clamp = clamp("__s0"),
|
|
7772
|
+
end_clamp = end
|
|
7773
|
+
.as_ref()
|
|
7774
|
+
.map(|_| format!("let __end = {}; ", clamp("__e0")))
|
|
7775
|
+
.unwrap_or_else(|| "let __end = __len; ".into()),
|
|
7776
|
+
arr_box = arr_box,
|
|
7777
|
+
)
|
|
7778
|
+
}
|
|
7779
|
+
"copyWithin" => {
|
|
7780
|
+
// copyWithin(target, start=0, end=len) — overlap-safe via a temp.
|
|
7781
|
+
let target = argf(self, 0)?.unwrap_or_else(|| "0.0".into());
|
|
7782
|
+
let start = argf(self, 1)?.unwrap_or_else(|| "0.0".into());
|
|
7783
|
+
let end = argf(self, 2)?;
|
|
7784
|
+
format!(
|
|
7785
|
+
"{{ let __t0 = {target}; let __s0 = {start}; {end_bind}{guard}\
|
|
7786
|
+
let __len = {p}.len(); let __lenf = __len as f64; \
|
|
7787
|
+
let __target = {target_clamp}; let __start = {start_clamp}; {end_clamp}\
|
|
7788
|
+
let __count = if __end > __start {{ (__end - __start).min(__len - __target) }} else {{ 0 }}; \
|
|
7789
|
+
let __tmp: Vec<{elem_rust}> = {p}[__start..__start + __count].to_vec(); \
|
|
7790
|
+
let mut __k = 0usize; for __val in __tmp {{ {p}[__target + __k] = __val; __k += 1; }} \
|
|
7791
|
+
{arr_box} }}",
|
|
7792
|
+
end_bind = end
|
|
7793
|
+
.as_ref()
|
|
7794
|
+
.map(|e| format!("let __e0 = {}; ", e))
|
|
7795
|
+
.unwrap_or_default(),
|
|
7796
|
+
target_clamp = clamp("__t0"),
|
|
7797
|
+
start_clamp = clamp("__s0"),
|
|
7798
|
+
end_clamp = end
|
|
7799
|
+
.as_ref()
|
|
7800
|
+
.map(|_| format!("let __end = {}; ", clamp("__e0")))
|
|
7801
|
+
.unwrap_or_else(|| "let __end = __len; ".into()),
|
|
7802
|
+
arr_box = arr_box,
|
|
7803
|
+
)
|
|
7804
|
+
}
|
|
7805
|
+
_ => unreachable!("method checked above"),
|
|
7806
|
+
};
|
|
7807
|
+
return Ok(code);
|
|
7210
7808
|
}
|
|
7211
7809
|
}
|
|
7212
7810
|
}
|
|
@@ -7219,35 +7817,122 @@ impl Codegen {
|
|
|
7219
7817
|
// sorts via the runtime, and writes the result back into the Vec (correct).
|
|
7220
7818
|
if method_name.as_ref() == "sort" {
|
|
7221
7819
|
if let Expr::Ident { name, .. } = object.as_ref() {
|
|
7222
|
-
if
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
{
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7820
|
+
if let RustType::Vec(elem_type) =
|
|
7821
|
+
self.type_context.get_type(name.as_ref())
|
|
7822
|
+
{
|
|
7823
|
+
if matches!(elem_type.as_ref(), RustType::Named { .. }) {
|
|
7824
|
+
let esc = Self::escape_ident(name.as_ref()).into_owned();
|
|
7825
|
+
// A captured (refcell-wrapped) Vec sorts through the shared cell:
|
|
7826
|
+
// `base_mut` for the in-place `sort_by`, `base_read` for the boxing
|
|
7827
|
+
// scan, `assign` for the general-comparator write-back.
|
|
7828
|
+
let wrapped = self.refcell_wrapped_vars.contains(name.as_ref());
|
|
7829
|
+
let base_mut = if wrapped {
|
|
7830
|
+
format!("(*{}.borrow_mut())", esc)
|
|
7831
|
+
} else {
|
|
7832
|
+
esc.clone()
|
|
7833
|
+
};
|
|
7834
|
+
let base_read = if wrapped {
|
|
7835
|
+
format!("(*{}.borrow())", esc)
|
|
7836
|
+
} else {
|
|
7837
|
+
esc.clone()
|
|
7838
|
+
};
|
|
7839
|
+
let assign = if wrapped {
|
|
7840
|
+
format!("*{}.borrow_mut()", esc)
|
|
7841
|
+
} else {
|
|
7842
|
+
esc.clone()
|
|
7843
|
+
};
|
|
7844
|
+
// `sort` returns THE array (JS / tish interp) — a boxed copy of the
|
|
7845
|
+
// now-sorted Vec (contents match interp; not aliased). `sort` is
|
|
7846
|
+
// O(n log n), so the O(n) box is asymptotically free.
|
|
7847
|
+
let sort_box = format!(
|
|
7848
|
+
"Value::Array(VmRef::new({}.iter().cloned().map(|__se| {}).collect()))",
|
|
7849
|
+
base_read,
|
|
7850
|
+
elem_type.to_value_expr("__se")
|
|
7851
|
+
);
|
|
7852
|
+
if let Some(CallArg::Expr(cmp)) = args.first() {
|
|
7853
|
+
if let Some(keys) =
|
|
7854
|
+
Self::detect_lexicographic_key_comparator(cmp)
|
|
7855
|
+
{
|
|
7856
|
+
if keys.iter().all(|(p, _)| p.len() == 1) {
|
|
7857
|
+
let chain = Self::render_struct_sort_chain(&keys);
|
|
7858
|
+
return Ok(format!(
|
|
7859
|
+
"{{ {base_mut}.sort_by(|a, b| {chain}); {sort_box} }}"
|
|
7860
|
+
));
|
|
7238
7861
|
}
|
|
7239
|
-
// General comparator: box → sort → write back (correct).
|
|
7240
|
-
let cmp_code = self.emit_expr(cmp)?;
|
|
7241
|
-
let to_v = elem_type.to_value_expr("__e");
|
|
7242
|
-
let from_v = elem_type.from_value_expr("__v");
|
|
7243
|
-
return Ok(format!(
|
|
7244
|
-
"{{ let __sv: Vec<Value> = {esc}.iter().map(|__e| {to_v}).collect(); \
|
|
7245
|
-
let __sorted = tishlang_runtime::array_sort(&Value::Array(VmRef::new(__sv)), Some(&({cmp_code}))); \
|
|
7246
|
-
if let Value::Array(__a) = __sorted {{ {esc} = __a.borrow().iter().map(|__v| {from_v}).collect(); }} \
|
|
7247
|
-
Value::Null }}"
|
|
7248
|
-
));
|
|
7249
7862
|
}
|
|
7863
|
+
// General comparator: box → sort → write back (correct).
|
|
7864
|
+
let cmp_code = self.emit_expr(cmp)?;
|
|
7865
|
+
let to_v = elem_type.to_value_expr("__e");
|
|
7866
|
+
let from_v = elem_type.from_value_expr("__v");
|
|
7867
|
+
return Ok(format!(
|
|
7868
|
+
"{{ let __sv: Vec<Value> = {base_read}.iter().map(|__e| {to_v}).collect(); \
|
|
7869
|
+
let __sorted = tishlang_runtime::array_sort(&Value::Array(VmRef::new(__sv)), Some(&({cmp_code}))); \
|
|
7870
|
+
if let Value::Array(__a) = __sorted {{ {assign} = __a.borrow().iter().map(|__v| {from_v}).collect(); }} \
|
|
7871
|
+
{sort_box} }}"
|
|
7872
|
+
));
|
|
7250
7873
|
}
|
|
7874
|
+
} else {
|
|
7875
|
+
// #568: a scalar / `Value` native `Vec` (`number[]`, `string[]`,
|
|
7876
|
+
// …) sorts IN PLACE like interp/vm/JS. Box a copy, sort it via the
|
|
7877
|
+
// SAME runtime fn the boxed path uses (so order is byte-identical),
|
|
7878
|
+
// then WRITE the ordered elements BACK into the native Vec — the
|
|
7879
|
+
// generic boxed dispatch below sorted a throwaway copy and dropped
|
|
7880
|
+
// the write (a silent no-op; only `arr = arr.sort()` worked).
|
|
7881
|
+
// Returns the sorted array (JS / interp). `sort` marks the receiver
|
|
7882
|
+
// assigned (see `collect_assigned_idents_in_expr`), so the Vec is
|
|
7883
|
+
// `mut` and the write-back compiles.
|
|
7884
|
+
let esc = Self::escape_ident(name.as_ref()).into_owned();
|
|
7885
|
+
let wrapped =
|
|
7886
|
+
self.refcell_wrapped_vars.contains(name.as_ref());
|
|
7887
|
+
let base_read = if wrapped {
|
|
7888
|
+
format!("(*{}.borrow())", esc)
|
|
7889
|
+
} else {
|
|
7890
|
+
esc.clone()
|
|
7891
|
+
};
|
|
7892
|
+
let assign = if wrapped {
|
|
7893
|
+
format!("*{}.borrow_mut()", esc)
|
|
7894
|
+
} else {
|
|
7895
|
+
esc.clone()
|
|
7896
|
+
};
|
|
7897
|
+
let to_v = elem_type.to_value_expr("__e");
|
|
7898
|
+
let from_v = elem_type.from_value_expr("__v");
|
|
7899
|
+
// Mirror the boxed path's comparator detection EXACTLY (numeric
|
|
7900
|
+
// fast path → lexicographic keys → general callback → default),
|
|
7901
|
+
// so the sorted order matches the generic path and interp/vm.
|
|
7902
|
+
let sort_call = if let Some(CallArg::Expr(cmp)) = args.first() {
|
|
7903
|
+
if let Some(ascending) =
|
|
7904
|
+
Self::detect_numeric_sort_comparator(cmp)
|
|
7905
|
+
{
|
|
7906
|
+
if ascending {
|
|
7907
|
+
"tishlang_runtime::array_sort_numeric_asc(&__boxed)"
|
|
7908
|
+
.to_string()
|
|
7909
|
+
} else {
|
|
7910
|
+
"tishlang_runtime::array_sort_numeric_desc(&__boxed)"
|
|
7911
|
+
.to_string()
|
|
7912
|
+
}
|
|
7913
|
+
} else if let Some(keys) =
|
|
7914
|
+
Self::detect_lexicographic_key_comparator(cmp)
|
|
7915
|
+
{
|
|
7916
|
+
format!(
|
|
7917
|
+
"tishlang_runtime::array_sort_by_keys(&__boxed, {})",
|
|
7918
|
+
Self::render_sort_keys(&keys)
|
|
7919
|
+
)
|
|
7920
|
+
} else {
|
|
7921
|
+
let cmp_code = self.emit_expr(cmp)?;
|
|
7922
|
+
format!(
|
|
7923
|
+
"tishlang_runtime::array_sort(&__boxed, Some(&({})))",
|
|
7924
|
+
cmp_code
|
|
7925
|
+
)
|
|
7926
|
+
}
|
|
7927
|
+
} else {
|
|
7928
|
+
"tishlang_runtime::array_sort(&__boxed, None)".to_string()
|
|
7929
|
+
};
|
|
7930
|
+
return Ok(format!(
|
|
7931
|
+
"{{ let __boxed = Value::Array(VmRef::new({base_read}.iter().cloned().map(|__e| {to_v}).collect())); \
|
|
7932
|
+
let __sorted = {sort_call}; \
|
|
7933
|
+
if let Value::Array(__a) = &__sorted {{ {assign} = __a.borrow().iter().map(|__v| {from_v}).collect(); }} \
|
|
7934
|
+
__sorted }}"
|
|
7935
|
+
));
|
|
7251
7936
|
}
|
|
7252
7937
|
}
|
|
7253
7938
|
}
|
|
@@ -7872,6 +8557,41 @@ impl Codegen {
|
|
|
7872
8557
|
}
|
|
7873
8558
|
}
|
|
7874
8559
|
|
|
8560
|
+
// Typed externs (#perf): a call to a native import with a declared typed signature →
|
|
8561
|
+
// a DIRECT `<crate>::name_typed(coerced args)` call. No `Value` boxing of the args, no
|
|
8562
|
+
// `value_call` dispatch through the namespace object. Falls through to the boxed path
|
|
8563
|
+
// when the arg count/kind doesn't match (a dynamic/spread call stays correct).
|
|
8564
|
+
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
8565
|
+
if let Some(sig) = self.extern_fns.get(fname.as_ref()).cloned() {
|
|
8566
|
+
if args.len() == sig.params.len()
|
|
8567
|
+
&& args.iter().all(|a| matches!(a, CallArg::Expr(_)))
|
|
8568
|
+
{
|
|
8569
|
+
let mut argc: Vec<String> = Vec::with_capacity(args.len());
|
|
8570
|
+
for (a, ty) in args.iter().zip(sig.params.iter()) {
|
|
8571
|
+
if let CallArg::Expr(e) = a {
|
|
8572
|
+
argc.push(self.emit_coerced_native(e, ty)?);
|
|
8573
|
+
}
|
|
8574
|
+
}
|
|
8575
|
+
let call = format!(
|
|
8576
|
+
"{}::{}_typed({})",
|
|
8577
|
+
sig.crate_name,
|
|
8578
|
+
sig.symbol,
|
|
8579
|
+
argc.join(", ")
|
|
8580
|
+
);
|
|
8581
|
+
// This is a Value-producing context: a `void` extern yields `Null`; a
|
|
8582
|
+
// native return is boxed once at the boundary (still far cheaper than the
|
|
8583
|
+
// boxed-args `value_call`); a `Value`-returning extern passes through.
|
|
8584
|
+
return Ok(if sig.ret == RustType::Unit {
|
|
8585
|
+
format!("{{ {}; Value::Null }}", call)
|
|
8586
|
+
} else if sig.ret.is_native() {
|
|
8587
|
+
sig.ret.to_value_expr(&call)
|
|
8588
|
+
} else {
|
|
8589
|
+
call
|
|
8590
|
+
});
|
|
8591
|
+
}
|
|
8592
|
+
}
|
|
8593
|
+
}
|
|
8594
|
+
|
|
7875
8595
|
// M5: boxed-context call to an eligible native fn → `Value::Number(name_native(..))`.
|
|
7876
8596
|
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
7877
8597
|
if self.native_fns.contains(fname.as_ref()) {
|
|
@@ -7950,17 +8670,25 @@ impl Codegen {
|
|
|
7950
8670
|
fields.iter().find(|(k, _)| k.as_ref() == prop_name.as_ref())
|
|
7951
8671
|
{
|
|
7952
8672
|
let var_esc = Self::escape_ident(var_name.as_ref()).into_owned();
|
|
8673
|
+
// Clone a non-Copy field so reading it doesn't move out of the local
|
|
8674
|
+
// (E0382 when the struct is used again); a Copy scalar reads by value.
|
|
8675
|
+
let clone = if field_ty.is_copy() { "" } else { ".clone()" };
|
|
7953
8676
|
let access = if self.refcell_wrapped_vars.contains(var_name.as_ref()) {
|
|
8677
|
+
// #567: scope the borrow to its own block so the lock drops
|
|
8678
|
+
// immediately — a `send-values` `Arc<Mutex>` VmRef is NOT
|
|
8679
|
+
// reentrant, so two `.borrow()`s of the same cell alive at once
|
|
8680
|
+
// (e.g. `s.a + s.b` in one expression) would deadlock.
|
|
7954
8681
|
format!(
|
|
7955
|
-
"
|
|
8682
|
+
"{{ let __bg = {}.borrow(); (*__bg).{}.clone() }}",
|
|
7956
8683
|
var_esc,
|
|
7957
8684
|
crate::types::field_ident(prop_name.as_ref())
|
|
7958
8685
|
)
|
|
7959
8686
|
} else {
|
|
7960
8687
|
format!(
|
|
7961
|
-
"{}.{}",
|
|
8688
|
+
"{}.{}{}",
|
|
7962
8689
|
var_esc,
|
|
7963
|
-
crate::types::field_ident(prop_name.as_ref())
|
|
8690
|
+
crate::types::field_ident(prop_name.as_ref()),
|
|
8691
|
+
clone
|
|
7964
8692
|
)
|
|
7965
8693
|
};
|
|
7966
8694
|
// Caller expects a `Value`; wrap.
|
|
@@ -7972,24 +8700,90 @@ impl Codegen {
|
|
|
7972
8700
|
// Generalize the typed struct-field fast path to `xs[i].field` (array-of-structs):
|
|
7973
8701
|
// when `object` indexes a `Vec<Named>`, do native struct field access.
|
|
7974
8702
|
if !optional {
|
|
7975
|
-
if let (
|
|
7976
|
-
|
|
8703
|
+
if let (
|
|
8704
|
+
Expr::Index { object: io, index: ii, .. },
|
|
8705
|
+
MemberProp::Name { name: prop_name, .. },
|
|
8706
|
+
) = (object.as_ref(), prop)
|
|
7977
8707
|
{
|
|
8708
|
+
// A captured (refcell-wrapped) `Vec<Named>` element field read: emit the field
|
|
8709
|
+
// access DIRECTLY on the borrowed element (`(*arr.borrow())[i].field`) — no whole
|
|
8710
|
+
// struct clone, and no whole-Vec boxing. This is the hot-loop `states[id].timer`
|
|
8711
|
+
// case (tish#558 read side). A non-Copy field is still cloned out of the borrow.
|
|
8712
|
+
if let Expr::Ident { name: arr_name, .. } = io.as_ref() {
|
|
8713
|
+
if self.refcell_wrapped_vars.contains(arr_name.as_ref()) {
|
|
8714
|
+
if let RustType::Vec(elem) =
|
|
8715
|
+
self.type_context.get_type(arr_name.as_ref())
|
|
8716
|
+
{
|
|
8717
|
+
if let RustType::Named { fields, .. } = elem.as_ref() {
|
|
8718
|
+
if let Some((_, field_ty)) = fields
|
|
8719
|
+
.iter()
|
|
8720
|
+
.find(|(k, _)| k.as_ref() == prop_name.as_ref())
|
|
8721
|
+
{
|
|
8722
|
+
let field_ty = field_ty.clone();
|
|
8723
|
+
let idx = self.emit_index_usize(ii)?;
|
|
8724
|
+
let esc = Self::escape_ident(arr_name.as_ref());
|
|
8725
|
+
let clone = if field_ty.is_copy() { "" } else { ".clone()" };
|
|
8726
|
+
// #567: bind the index (it may read the same cell, e.g.
|
|
8727
|
+
// `arr[arr.length-1]`) BEFORE taking the borrow, and scope
|
|
8728
|
+
// the borrow to its own block so the non-reentrant
|
|
8729
|
+
// `Arc<Mutex>` lock drops before any sibling read of `arr`
|
|
8730
|
+
// (`arr[i].a + arr[j].b`) — otherwise overlapping borrows
|
|
8731
|
+
// deadlock.
|
|
8732
|
+
let access = format!(
|
|
8733
|
+
"{{ let __bi = {}; let __bg = {}.borrow(); (*__bg)[__bi].{}{} }}",
|
|
8734
|
+
idx,
|
|
8735
|
+
esc,
|
|
8736
|
+
crate::types::field_ident(prop_name.as_ref()),
|
|
8737
|
+
clone
|
|
8738
|
+
);
|
|
8739
|
+
return Ok(field_ty.to_value_expr(&access));
|
|
8740
|
+
}
|
|
8741
|
+
}
|
|
8742
|
+
}
|
|
8743
|
+
}
|
|
8744
|
+
}
|
|
7978
8745
|
let (obj_code, obj_ty) = self.emit_typed_expr(object)?;
|
|
7979
8746
|
if let RustType::Named { fields, .. } = &obj_ty {
|
|
7980
8747
|
if let Some((_, field_ty)) =
|
|
7981
8748
|
fields.iter().find(|(k, _)| k.as_ref() == prop_name.as_ref())
|
|
7982
8749
|
{
|
|
8750
|
+
// The element `xs[i]` is BORROWED, so a non-Copy field (a `Value`/
|
|
8751
|
+
// `String`) must be cloned out — otherwise reading it in a value/return
|
|
8752
|
+
// position moves out of the index (E0507). A Copy scalar reads by value.
|
|
8753
|
+
let clone = if field_ty.is_copy() { "" } else { ".clone()" };
|
|
7983
8754
|
let access = format!(
|
|
7984
|
-
"({}).{}",
|
|
8755
|
+
"({}).{}{}",
|
|
7985
8756
|
obj_code,
|
|
7986
|
-
crate::types::field_ident(prop_name.as_ref())
|
|
8757
|
+
crate::types::field_ident(prop_name.as_ref()),
|
|
8758
|
+
clone
|
|
7987
8759
|
);
|
|
7988
8760
|
return Ok(field_ty.to_value_expr(&access));
|
|
7989
8761
|
}
|
|
7990
8762
|
}
|
|
7991
8763
|
}
|
|
7992
8764
|
}
|
|
8765
|
+
// `arr.length` on a native `Vec` (wrapped or not) → a cheap native `len()` boxed to a
|
|
8766
|
+
// `Number`, instead of boxing the WHOLE Vec to a `Value::Array` just to `get_prop`
|
|
8767
|
+
// its length (O(n) per read — the hot `states.length`-per-frame footgun, tish#562).
|
|
8768
|
+
if !*optional {
|
|
8769
|
+
if let MemberProp::Name { name: p, .. } = prop {
|
|
8770
|
+
if p.as_ref() == "length" {
|
|
8771
|
+
if let Expr::Ident { name, .. } = object.as_ref() {
|
|
8772
|
+
if let RustType::Vec(_) = self.type_context.get_type(name.as_ref()) {
|
|
8773
|
+
let esc = Self::escape_ident(name.as_ref());
|
|
8774
|
+
// #567: scope the borrow (non-reentrant `Arc<Mutex>` VmRef) so it
|
|
8775
|
+
// can't overlap a sibling read of the same cell in one expression.
|
|
8776
|
+
let len = if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
8777
|
+
format!("{{ let __bg = {}.borrow(); __bg.len() }}", esc)
|
|
8778
|
+
} else {
|
|
8779
|
+
format!("{}.len()", esc)
|
|
8780
|
+
};
|
|
8781
|
+
return Ok(format!("Value::Number({} as f64)", len));
|
|
8782
|
+
}
|
|
8783
|
+
}
|
|
8784
|
+
}
|
|
8785
|
+
}
|
|
8786
|
+
}
|
|
7993
8787
|
let obj = self.emit_expr(object)?;
|
|
7994
8788
|
// A literal (non-computed) key other than `size` is eligible for a per-site
|
|
7995
8789
|
// polymorphic inline cache (#179). `size` is excluded because `get_prop` special-cases
|
|
@@ -8148,9 +8942,58 @@ impl Codegen {
|
|
|
8148
8942
|
}
|
|
8149
8943
|
}
|
|
8150
8944
|
ObjectProp::Spread(e) => {
|
|
8151
|
-
|
|
8945
|
+
// Peek the source's static type ONCE. On GBA a statically-known struct
|
|
8946
|
+
// spreads NATIVELY (fields read straight off the Rust struct → the source
|
|
8947
|
+
// is never boxed to a `Value::Struct` / hashmap). `to_value_expr` re-boxes
|
|
8948
|
+
// a native scalar source so the boxed arms still receive a `Value`
|
|
8949
|
+
// (`{ ...5 }` stays sound). Desktop is deliberately untouched — a struct
|
|
8950
|
+
// source there boxes exactly as before (byte-identical emit, so the hot
|
|
8951
|
+
// object_spread bench keeps its clone-shape path).
|
|
8952
|
+
let gba = self.emit_mode == crate::NativeEmitMode::Gba;
|
|
8953
|
+
let (typed_code, typed_ty) = self.emit_typed_expr(e)?;
|
|
8954
|
+
if gba {
|
|
8955
|
+
if let crate::types::RustType::Named { fields, .. } = &typed_ty {
|
|
8956
|
+
let mut ins = String::new();
|
|
8957
|
+
for (k, fty) in fields {
|
|
8958
|
+
let key = self.cached_object_key(k.as_ref());
|
|
8959
|
+
ins.push_str(&format!(
|
|
8960
|
+
"_obj.insert({}, {}); ",
|
|
8961
|
+
key,
|
|
8962
|
+
Self::struct_field_to_value("_s", k, fty)
|
|
8963
|
+
));
|
|
8964
|
+
}
|
|
8965
|
+
let block = format!("{{ let _s = &({}); {}}}", typed_code, ins);
|
|
8966
|
+
if i == 0 {
|
|
8967
|
+
init = format!(
|
|
8968
|
+
"let mut _obj: PropMap = PropMap::with_capacity({}); {}",
|
|
8969
|
+
literal_keys + fields.len(),
|
|
8970
|
+
block
|
|
8971
|
+
);
|
|
8972
|
+
} else {
|
|
8973
|
+
// Later struct fields override earlier keys (JS order).
|
|
8974
|
+
parts.push(block);
|
|
8975
|
+
}
|
|
8976
|
+
continue;
|
|
8977
|
+
}
|
|
8978
|
+
}
|
|
8979
|
+
let val = if typed_ty.is_native() {
|
|
8980
|
+
typed_ty.to_value_expr(&typed_code)
|
|
8981
|
+
} else {
|
|
8982
|
+
typed_code
|
|
8983
|
+
};
|
|
8984
|
+
// GBA (non-struct source, e.g. a struct behind a `Value` param): route
|
|
8985
|
+
// through the facade's `object_spread_props`, which materialises a
|
|
8986
|
+
// `Value::Struct` to its object first. Desktop keeps the inline `if let
|
|
8987
|
+
// Value::Object` fast path untouched (no struct variant exists there, and
|
|
8988
|
+
// it's the hot object_spread bench).
|
|
8152
8989
|
if i == 0 {
|
|
8153
|
-
init =
|
|
8990
|
+
init = if gba {
|
|
8991
|
+
format!("let mut _obj: PropMap = tishlang_runtime::object_spread_props(&({})).unwrap_or_else(|| PropMap::with_capacity({}));", val, literal_keys)
|
|
8992
|
+
} else {
|
|
8993
|
+
format!("let mut _obj: PropMap = if let Value::Object(ref _spread) = {} {{ _spread.borrow().strings.clone() }} else {{ PropMap::with_capacity({}) }};", val, literal_keys)
|
|
8994
|
+
};
|
|
8995
|
+
} else if gba {
|
|
8996
|
+
parts.push(format!("if let Some(_spread) = tishlang_runtime::object_spread_props(&({})) {{ _obj.merge_from(&_spread); }}", val));
|
|
8154
8997
|
} else {
|
|
8155
8998
|
// `merge_from` reserves for the source's len, then inserts in one
|
|
8156
8999
|
// pass (later props still override earlier keys, matching JS order).
|
|
@@ -8561,6 +9404,13 @@ impl Codegen {
|
|
|
8561
9404
|
format!("{{ if {} {{ {} }} else {{ {} }} }}", cond, assign_and_return, else_expr)
|
|
8562
9405
|
}
|
|
8563
9406
|
Expr::MemberAssign { object, prop, value, .. } => {
|
|
9407
|
+
// #558: a native struct-element / struct-local field store (yielding the assigned
|
|
9408
|
+
// value) instead of set_prop on a throwaway copy that discards the write.
|
|
9409
|
+
if let Some(code) =
|
|
9410
|
+
self.try_emit_native_struct_field_assign(object, prop.as_ref(), value, false)?
|
|
9411
|
+
{
|
|
9412
|
+
return Ok(code);
|
|
9413
|
+
}
|
|
8564
9414
|
let obj = self.emit_expr(object)?;
|
|
8565
9415
|
let val = self.emit_expr(value)?;
|
|
8566
9416
|
format!(
|
|
@@ -9249,6 +10099,17 @@ impl Codegen {
|
|
|
9249
10099
|
None
|
|
9250
10100
|
}
|
|
9251
10101
|
|
|
10102
|
+
/// The root variable of a member/index chain (`a.b.c` → `a`, `a[i].b` → `a`, `a` → `a`), or
|
|
10103
|
+
/// `None` when the base isn't a plain identifier (e.g. a call result `f().x`).
|
|
10104
|
+
fn root_ident_of(expr: &Expr) -> Option<&str> {
|
|
10105
|
+
match expr {
|
|
10106
|
+
Expr::Ident { name, .. } => Some(name.as_ref()),
|
|
10107
|
+
Expr::Member { object, .. } => Self::root_ident_of(object),
|
|
10108
|
+
Expr::Index { object, .. } => Self::root_ident_of(object),
|
|
10109
|
+
_ => None,
|
|
10110
|
+
}
|
|
10111
|
+
}
|
|
10112
|
+
|
|
9252
10113
|
fn collect_assigned_idents_in_expr(expr: &Expr, names: &mut HashSet<String>) {
|
|
9253
10114
|
match expr {
|
|
9254
10115
|
Expr::Assign { name, value, .. } => {
|
|
@@ -9270,6 +10131,12 @@ impl Codegen {
|
|
|
9270
10131
|
names.insert(name.to_string());
|
|
9271
10132
|
}
|
|
9272
10133
|
Expr::MemberAssign { object, value, .. } => {
|
|
10134
|
+
// Mutating a field (`x.f = v`, `a.b.c = v`) mutates the ROOT variable — record it so a
|
|
10135
|
+
// captured struct/object is wrapped mutably; otherwise the native field store lands on
|
|
10136
|
+
// an immutable `let` binding (E0594).
|
|
10137
|
+
if let Some(root) = Self::root_ident_of(object) {
|
|
10138
|
+
names.insert(root.to_string());
|
|
10139
|
+
}
|
|
9273
10140
|
Self::collect_assigned_idents_in_expr(object, names);
|
|
9274
10141
|
Self::collect_assigned_idents_in_expr(value, names);
|
|
9275
10142
|
}
|
|
@@ -9279,6 +10146,9 @@ impl Codegen {
|
|
|
9279
10146
|
value,
|
|
9280
10147
|
..
|
|
9281
10148
|
} => {
|
|
10149
|
+
if let Some(root) = Self::root_ident_of(object) {
|
|
10150
|
+
names.insert(root.to_string());
|
|
10151
|
+
}
|
|
9282
10152
|
Self::collect_assigned_idents_in_expr(object, names);
|
|
9283
10153
|
Self::collect_assigned_idents_in_expr(index, names);
|
|
9284
10154
|
Self::collect_assigned_idents_in_expr(value, names);
|
|
@@ -9290,6 +10160,29 @@ impl Codegen {
|
|
|
9290
10160
|
Expr::Unary { operand, .. } => Self::collect_assigned_idents_in_expr(operand, names),
|
|
9291
10161
|
Expr::Delete { target, .. } => Self::collect_assigned_idents_in_expr(target, names),
|
|
9292
10162
|
Expr::Call { callee, args, .. } => {
|
|
10163
|
+
// An in-place mutating array method (`a.push(x)`, `a.pop()`, `a.reverse()`, …) mutates
|
|
10164
|
+
// the ROOT variable — record it so a captured native Vec is wrapped for shared mutation.
|
|
10165
|
+
// WITHOUT this, the mutation inside a closure lands on a by-value / immutable-`let`
|
|
10166
|
+
// captured Vec (E0596), and the boxed fallback would DISCARD it (and E0282 on a
|
|
10167
|
+
// `Vec<struct>`, whose element type the skipped native op never pinned). Every listed
|
|
10168
|
+
// method has a native fast path that handles the wrapped receiver (a `(*a.borrow_mut())`
|
|
10169
|
+
// access, or a `borrow_mut` guard for the multi-access `splice`/`fill`/`copyWithin`).
|
|
10170
|
+
if let Expr::Member {
|
|
10171
|
+
object,
|
|
10172
|
+
prop: MemberProp::Name { name: m, .. },
|
|
10173
|
+
..
|
|
10174
|
+
} = callee.as_ref()
|
|
10175
|
+
{
|
|
10176
|
+
if matches!(
|
|
10177
|
+
m.as_ref(),
|
|
10178
|
+
"push" | "pop" | "shift" | "unshift" | "reverse" | "sort" | "splice"
|
|
10179
|
+
| "fill" | "copyWithin"
|
|
10180
|
+
) {
|
|
10181
|
+
if let Some(root) = Self::root_ident_of(object) {
|
|
10182
|
+
names.insert(root.to_string());
|
|
10183
|
+
}
|
|
10184
|
+
}
|
|
10185
|
+
}
|
|
9293
10186
|
Self::collect_assigned_idents_in_expr(callee, names);
|
|
9294
10187
|
for arg in args {
|
|
9295
10188
|
match arg {
|
|
@@ -10619,6 +11512,15 @@ impl Codegen {
|
|
|
10619
11512
|
for v in &implicit_env_captures {
|
|
10620
11513
|
self.refcell_wrapped_vars.remove(v);
|
|
10621
11514
|
}
|
|
11515
|
+
// tishlang/tish#556: an arrow PARAMETER shadows any outer var of the same name — inside the
|
|
11516
|
+
// body it is a plain `Value`/native binding, not the outer `VmRef` cell, so drop such names
|
|
11517
|
+
// from the wrapped set for the body (restored via `saved_refcell_vars`). Without this, a
|
|
11518
|
+
// reference to the param compiles as `vm_read(¶m)` — a `&Value` where a `&VmRef` is wanted.
|
|
11519
|
+
for p in params {
|
|
11520
|
+
for bn in p.bound_names() {
|
|
11521
|
+
self.refcell_wrapped_vars.remove(bn.as_ref());
|
|
11522
|
+
}
|
|
11523
|
+
}
|
|
10622
11524
|
|
|
10623
11525
|
self.type_context.push_fun_param_scope(params, None);
|
|
10624
11526
|
|
|
@@ -10709,11 +11611,15 @@ impl Codegen {
|
|
|
10709
11611
|
{
|
|
10710
11612
|
if p.as_ref() == "length" {
|
|
10711
11613
|
if let Expr::Ident { name: arr, .. } = object.as_ref() {
|
|
10712
|
-
if matches!(self.type_context.get_type(arr.as_ref()), RustType::Vec(_))
|
|
10713
|
-
&& !self.refcell_wrapped_vars.contains(arr.as_ref())
|
|
10714
|
-
{
|
|
11614
|
+
if matches!(self.type_context.get_type(arr.as_ref()), RustType::Vec(_)) {
|
|
10715
11615
|
let esc = Self::escape_ident(arr.as_ref());
|
|
10716
|
-
|
|
11616
|
+
// A captured Vec reads its length through the cell (`(*a.borrow()).len()`).
|
|
11617
|
+
let code = if self.refcell_wrapped_vars.contains(arr.as_ref()) {
|
|
11618
|
+
format!("{{ let __bg = {}.borrow(); __bg.len() as f64 }}", esc)
|
|
11619
|
+
} else {
|
|
11620
|
+
format!("{}.len() as f64", esc)
|
|
11621
|
+
};
|
|
11622
|
+
return Ok(format!("({})", code));
|
|
10717
11623
|
}
|
|
10718
11624
|
}
|
|
10719
11625
|
}
|
|
@@ -10938,6 +11844,17 @@ impl Codegen {
|
|
|
10938
11844
|
if *target_type == RustType::F64 && typed_ty.is_integer_scalar() {
|
|
10939
11845
|
return Ok(format!("({}) as f64", typed_code));
|
|
10940
11846
|
}
|
|
11847
|
+
// `fixed` target from a native integer / f64 source: lift to `Fixed` directly, no
|
|
11848
|
+
// Value round-trip. Lets `let dx: fixed = input_x()` (i32→fixed) or `let v: fixed = n`
|
|
11849
|
+
// stay native so downstream `dx * spd` is `Fixed*Fixed` instead of boxing.
|
|
11850
|
+
if *target_type == RustType::Fixed {
|
|
11851
|
+
if typed_ty.is_integer_scalar() {
|
|
11852
|
+
return Ok(format!("tishlang_runtime::Fixed::from_raw((({}) as i32) * 256)", typed_code));
|
|
11853
|
+
}
|
|
11854
|
+
if typed_ty == RustType::F64 {
|
|
11855
|
+
return Ok(format!("tishlang_runtime::Fixed::from_raw((({}) * 256.0) as i32)", typed_code));
|
|
11856
|
+
}
|
|
11857
|
+
}
|
|
10941
11858
|
}
|
|
10942
11859
|
|
|
10943
11860
|
// Fall back to emit_expr + conversion
|
|
@@ -12250,50 +13167,19 @@ impl Codegen {
|
|
|
12250
13167
|
let Expr::Ident { name, .. } = object else {
|
|
12251
13168
|
return Ok(None);
|
|
12252
13169
|
};
|
|
12253
|
-
if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
12254
|
-
return Ok(None);
|
|
12255
|
-
}
|
|
12256
13170
|
let obj_type = self.type_context.get_type(name.as_ref());
|
|
12257
13171
|
let RustType::Vec(elem_type) = obj_type else {
|
|
12258
13172
|
return Ok(None);
|
|
12259
13173
|
};
|
|
13174
|
+
// #564: a closure-captured (refcell-wrapped) native Vec is handled below via a `borrow_mut`
|
|
13175
|
+
// store. WITHOUT this it fell to the generic boxed `set_index` fallback, which mutates a
|
|
13176
|
+
// THROWAWAY boxed copy of the Vec (write silently DISCARDED) — and for a compound `a[i] += x`
|
|
13177
|
+
// (desugared to `a[i] = a[i] + x`) the fallback's `get_index(&a) … set_index(&a …)` takes two
|
|
13178
|
+
// overlapping locks on the non-reentrant `Arc<Mutex>` VmRef and DEADLOCKS the binary.
|
|
13179
|
+
let wrapped = self.refcell_wrapped_vars.contains(name.as_ref());
|
|
12260
13180
|
let esc_obj = Self::escape_ident(name.as_ref()).into_owned();
|
|
12261
13181
|
let in_bounds = self.index_in_bounds(index, name.as_ref());
|
|
12262
|
-
let idx_usize =
|
|
12263
|
-
if let Some(uv) = self.usize_var_subst.get(idx_name.as_ref()) {
|
|
12264
|
-
uv.clone()
|
|
12265
|
-
} else {
|
|
12266
|
-
let (idx_code, idx_ty) = self.emit_typed_expr(index)?;
|
|
12267
|
-
if idx_ty == RustType::F64 || idx_ty == RustType::I32 {
|
|
12268
|
-
format!("({}) as usize", idx_code)
|
|
12269
|
-
} else {
|
|
12270
|
-
let iv = if idx_ty.is_native() {
|
|
12271
|
-
idx_ty.to_value_expr(&idx_code)
|
|
12272
|
-
} else {
|
|
12273
|
-
idx_code
|
|
12274
|
-
};
|
|
12275
|
-
format!(
|
|
12276
|
-
"{{ let _i = &{}; if let Value::Number(n) = _i {{ *n as usize }} else {{ panic!(\"array index must be a number\") }} }}",
|
|
12277
|
-
iv
|
|
12278
|
-
)
|
|
12279
|
-
}
|
|
12280
|
-
}
|
|
12281
|
-
} else {
|
|
12282
|
-
let (idx_code, idx_ty) = self.emit_typed_expr(index)?;
|
|
12283
|
-
if idx_ty == RustType::F64 || idx_ty == RustType::I32 {
|
|
12284
|
-
format!("({}) as usize", idx_code)
|
|
12285
|
-
} else {
|
|
12286
|
-
let iv = if idx_ty.is_native() {
|
|
12287
|
-
idx_ty.to_value_expr(&idx_code)
|
|
12288
|
-
} else {
|
|
12289
|
-
idx_code
|
|
12290
|
-
};
|
|
12291
|
-
format!(
|
|
12292
|
-
"{{ let _i = &{}; if let Value::Number(n) = _i {{ *n as usize }} else {{ panic!(\"array index must be a number\") }} }}",
|
|
12293
|
-
iv
|
|
12294
|
-
)
|
|
12295
|
-
}
|
|
12296
|
-
};
|
|
13182
|
+
let idx_usize = self.emit_index_usize(index)?;
|
|
12297
13183
|
let native_val = if *elem_type == RustType::I32 {
|
|
12298
13184
|
if let Expr::Binary {
|
|
12299
13185
|
left,
|
|
@@ -12342,6 +13228,34 @@ impl Codegen {
|
|
|
12342
13228
|
val_code
|
|
12343
13229
|
}
|
|
12344
13230
|
};
|
|
13231
|
+
// #564: wrapped receiver — bind the index and RHS to temps BEFORE taking the `borrow_mut`,
|
|
13232
|
+
// so a RHS that reads the same cell (`a[i] += x`, `a[i] = a[j]`) releases its shared borrow
|
|
13233
|
+
// first (no overlapping lock → no deadlock), then store into the real element through the cell.
|
|
13234
|
+
if wrapped {
|
|
13235
|
+
let needs_resize =
|
|
13236
|
+
matches!(elem_type.as_ref(), RustType::F64 | RustType::Bool) && !in_bounds;
|
|
13237
|
+
let store = if needs_resize {
|
|
13238
|
+
let pad = if matches!(elem_type.as_ref(), RustType::F64) {
|
|
13239
|
+
"f64::NAN"
|
|
13240
|
+
} else {
|
|
13241
|
+
"false"
|
|
13242
|
+
};
|
|
13243
|
+
format!(
|
|
13244
|
+
"let __i = {idx}; let __v = {val}; let mut __g = {esc}.borrow_mut(); if __i >= __g.len() {{ __g.resize(__i + 1, {pad}); }} __g[__i] = __v;",
|
|
13245
|
+
idx = idx_usize, val = native_val, esc = esc_obj, pad = pad
|
|
13246
|
+
)
|
|
13247
|
+
} else {
|
|
13248
|
+
format!(
|
|
13249
|
+
"let __i = {idx}; let __v = {val}; (*{esc}.borrow_mut())[__i] = __v;",
|
|
13250
|
+
idx = idx_usize, val = native_val, esc = esc_obj
|
|
13251
|
+
)
|
|
13252
|
+
};
|
|
13253
|
+
return Ok(Some(if side_effect_only {
|
|
13254
|
+
format!("{{ {} }}", store)
|
|
13255
|
+
} else {
|
|
13256
|
+
format!("{{ {} Value::Null }}", store)
|
|
13257
|
+
}));
|
|
13258
|
+
}
|
|
12345
13259
|
let assign = match elem_type.as_ref() {
|
|
12346
13260
|
RustType::F64 | RustType::Bool | RustType::I32 if in_bounds => {
|
|
12347
13261
|
if side_effect_only {
|
|
@@ -12369,6 +13283,130 @@ impl Codegen {
|
|
|
12369
13283
|
Ok(Some(assign))
|
|
12370
13284
|
}
|
|
12371
13285
|
|
|
13286
|
+
/// Emit an array/element index expression as a Rust `usize`. A substituted loop counter reuses
|
|
13287
|
+
/// its `usize` binding; a numeric index casts (`(idx) as usize`); anything else unwraps a
|
|
13288
|
+
/// `Value::Number` at runtime. Shared by the native `Vec` index-assign and struct-field-assign
|
|
13289
|
+
/// lowerings so they agree on index handling.
|
|
13290
|
+
fn emit_index_usize(&mut self, index: &Expr) -> Result<String, CompileError> {
|
|
13291
|
+
if let Expr::Ident { name: idx_name, .. } = index {
|
|
13292
|
+
if let Some(uv) = self.usize_var_subst.get(idx_name.as_ref()) {
|
|
13293
|
+
return Ok(uv.clone());
|
|
13294
|
+
}
|
|
13295
|
+
}
|
|
13296
|
+
let (idx_code, idx_ty) = self.emit_typed_expr(index)?;
|
|
13297
|
+
if idx_ty == RustType::F64 || idx_ty == RustType::I32 {
|
|
13298
|
+
Ok(format!("({}) as usize", idx_code))
|
|
13299
|
+
} else {
|
|
13300
|
+
let iv = if idx_ty.is_native() {
|
|
13301
|
+
idx_ty.to_value_expr(&idx_code)
|
|
13302
|
+
} else {
|
|
13303
|
+
idx_code
|
|
13304
|
+
};
|
|
13305
|
+
Ok(format!(
|
|
13306
|
+
"{{ let _i = &{}; if let Value::Number(n) = _i {{ *n as usize }} else {{ panic!(\"array index must be a number\") }} }}",
|
|
13307
|
+
iv
|
|
13308
|
+
))
|
|
13309
|
+
}
|
|
13310
|
+
}
|
|
13311
|
+
|
|
13312
|
+
/// tishlang/tish#558 — `place.field = value` where `place` is an addressable STRUCT: an element
|
|
13313
|
+
/// of a typed struct `Vec` (`arr[i].field`) or a struct-typed local (`s.field`). Lowers to a
|
|
13314
|
+
/// NATIVE field store into the real element/local, coercing `value` to the field's type. Without
|
|
13315
|
+
/// this, the generic member-assign path emits `set_prop` on a throwaway `Value::object_from_pairs`
|
|
13316
|
+
/// COPY of the element — mutating the copy and silently discarding the write. Returns `None` for
|
|
13317
|
+
/// anything that isn't a known struct place (the caller falls back to the dynamic `set_prop`).
|
|
13318
|
+
fn try_emit_native_struct_field_assign(
|
|
13319
|
+
&mut self,
|
|
13320
|
+
object: &Expr,
|
|
13321
|
+
prop: &str,
|
|
13322
|
+
value: &Expr,
|
|
13323
|
+
side_effect_only: bool,
|
|
13324
|
+
) -> Result<Option<String>, CompileError> {
|
|
13325
|
+
// `prelude` binds any index temp BEFORE a `borrow_mut()` place is taken (so the index may
|
|
13326
|
+
// itself read the array — `arr[arr.length-1]` — without a RefCell double-borrow); it is empty
|
|
13327
|
+
// for the non-wrapped cases. `place` is the addressable struct expression the field stores into.
|
|
13328
|
+
let (prelude, place, fields) = match object {
|
|
13329
|
+
// `arr[i].field = v` — a typed struct Vec element. A captured (refcell-wrapped) array
|
|
13330
|
+
// writes through `(*arr.borrow_mut())[i]` so the store lands natively on the shared cell;
|
|
13331
|
+
// WITHOUT this it fell to the generic member-assign, which boxed the WHOLE Vec into a
|
|
13332
|
+
// throwaway `Value::Array` (cloning + `from_struct`-boxing every element) and `set_prop`'d
|
|
13333
|
+
// the discarded copy — O(n) per write, the write lost, and an E0282 on the boxing closure.
|
|
13334
|
+
Expr::Index { object: io, index, .. } => {
|
|
13335
|
+
let Expr::Ident { name, .. } = io.as_ref() else {
|
|
13336
|
+
return Ok(None);
|
|
13337
|
+
};
|
|
13338
|
+
let RustType::Vec(elem) = self.type_context.get_type(name.as_ref()) else {
|
|
13339
|
+
return Ok(None);
|
|
13340
|
+
};
|
|
13341
|
+
let RustType::Named { fields, .. } = elem.as_ref() else {
|
|
13342
|
+
return Ok(None);
|
|
13343
|
+
};
|
|
13344
|
+
let fields = fields.clone();
|
|
13345
|
+
let idx_usize = self.emit_index_usize(index)?;
|
|
13346
|
+
let esc = Self::escape_ident(name.as_ref());
|
|
13347
|
+
if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
13348
|
+
(
|
|
13349
|
+
format!("let _i = {}; ", idx_usize),
|
|
13350
|
+
format!("(*{}.borrow_mut())[_i]", esc),
|
|
13351
|
+
fields,
|
|
13352
|
+
)
|
|
13353
|
+
} else {
|
|
13354
|
+
(String::new(), format!("{}[{}]", esc, idx_usize), fields)
|
|
13355
|
+
}
|
|
13356
|
+
}
|
|
13357
|
+
// `s.field = v` — a struct-typed local/var. A captured (refcell-wrapped) struct writes
|
|
13358
|
+
// through `(*s.borrow_mut())` so the field store lands natively on the shared cell instead
|
|
13359
|
+
// of falling to the boxed `set_prop(object_from_pairs(..))` path (which also wouldn't
|
|
13360
|
+
// persist — it built a throwaway object).
|
|
13361
|
+
Expr::Ident { name, .. } => {
|
|
13362
|
+
let RustType::Named { fields, .. } = self.type_context.get_type(name.as_ref()) else {
|
|
13363
|
+
return Ok(None);
|
|
13364
|
+
};
|
|
13365
|
+
let place = if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
13366
|
+
format!("(*{}.borrow_mut())", Self::escape_ident(name.as_ref()))
|
|
13367
|
+
} else {
|
|
13368
|
+
Self::escape_ident(name.as_ref()).into_owned()
|
|
13369
|
+
};
|
|
13370
|
+
(String::new(), place, fields)
|
|
13371
|
+
}
|
|
13372
|
+
_ => return Ok(None),
|
|
13373
|
+
};
|
|
13374
|
+
// The property must be a real field of the struct; its type drives the RHS coercion.
|
|
13375
|
+
let Some((_, field_ty)) = fields.iter().find(|(k, _)| k.as_ref() == prop) else {
|
|
13376
|
+
return Ok(None);
|
|
13377
|
+
};
|
|
13378
|
+
let field_ty = field_ty.clone();
|
|
13379
|
+
let field = crate::types::field_ident(prop);
|
|
13380
|
+
let coerced = self.emit_coerced_native(value, &field_ty)?;
|
|
13381
|
+
// Bind the RHS to a temp first so a `borrow_mut()` place can't alias a `borrow()` in the RHS
|
|
13382
|
+
// (`s.a = s.b + 1`) — evaluate the read before taking the mutable borrow. Any index prelude
|
|
13383
|
+
// runs first, so its evaluation also predates the borrow.
|
|
13384
|
+
let via_temp = !prelude.is_empty() || place.contains(".borrow_mut()");
|
|
13385
|
+
if side_effect_only {
|
|
13386
|
+
if via_temp {
|
|
13387
|
+
Ok(Some(format!(
|
|
13388
|
+
"{{ {}let _v = {}; {}.{} = _v; }}",
|
|
13389
|
+
prelude, coerced, place, field
|
|
13390
|
+
)))
|
|
13391
|
+
} else {
|
|
13392
|
+
Ok(Some(format!("{}.{} = {}", place, field, coerced)))
|
|
13393
|
+
}
|
|
13394
|
+
} else {
|
|
13395
|
+
// Assignment-as-expression yields the assigned value (JS `a.b = c` ⇒ c). Bind once (the
|
|
13396
|
+
// RHS may have effects and the place may re-evaluate an index), store, then hand back a
|
|
13397
|
+
// `Value` of it — box a native field, pass a `Value` field through.
|
|
13398
|
+
let ret = if field_ty.is_native() {
|
|
13399
|
+
field_ty.to_value_expr("_v")
|
|
13400
|
+
} else {
|
|
13401
|
+
"_v".to_string()
|
|
13402
|
+
};
|
|
13403
|
+
Ok(Some(format!(
|
|
13404
|
+
"{{ {}let _v = {}; {}.{} = _v.clone(); {} }}",
|
|
13405
|
+
prelude, coerced, place, field, ret
|
|
13406
|
+
)))
|
|
13407
|
+
}
|
|
13408
|
+
}
|
|
13409
|
+
|
|
12372
13410
|
/// Emit `value` as a native expression of type `target_ty`, coercing without a
|
|
12373
13411
|
/// `Value` round-trip where the conversion is a cheap numeric cast. Same-typed RHS
|
|
12374
13412
|
/// passes through; f64↔i32 casts directly; everything else boxes to `Value` then
|
|
@@ -22097,6 +23135,23 @@ impl Codegen {
|
|
|
22097
23135
|
let (l, lt) = self.emit_typed_expr(left)?;
|
|
22098
23136
|
let (r, rt) = self.emit_typed_expr(right)?;
|
|
22099
23137
|
|
|
23138
|
+
// `fixed ⊕ <runtime integer>` (e.g. `t * speed` with `t: i32`, `speed: fixed`): LIFT the
|
|
23139
|
+
// integer operand to `Fixed` via `Num::new` so the whole op stays native `Num<i32,8>`
|
|
23140
|
+
// arithmetic. This runs BEFORE the generic integer→f64 coercion below — otherwise the
|
|
23141
|
+
// integer becomes f64 and `f64 ⊕ fixed` drops to the boxed `ops::*` path (that path is
|
|
23142
|
+
// the deliberate escape hatch for a GENUINE runtime f64, which is lossy against fixed;
|
|
23143
|
+
// a plain integer is exact, so lifting is sound). Integer *literals* are already folded
|
|
23144
|
+
// to a Fixed constant by `fixed_literal_of` further down; this covers runtime integers.
|
|
23145
|
+
// `Fixed<i32,8>` raw = value << 8 (× 256), so an integer `v` lifts to fixed `v.0` as
|
|
23146
|
+
// `Fixed::from_raw(v * 256)` — the same constructor tish emits everywhere for fixed.
|
|
23147
|
+
let (l, lt, r, rt) = if lt == RustType::Fixed && rt.is_integer_scalar() {
|
|
23148
|
+
(l, lt, format!("tishlang_runtime::Fixed::from_raw((({}) as i32) * 256)", r), RustType::Fixed)
|
|
23149
|
+
} else if rt == RustType::Fixed && lt.is_integer_scalar() {
|
|
23150
|
+
(format!("tishlang_runtime::Fixed::from_raw((({}) as i32) * 256)", l), RustType::Fixed, r, rt)
|
|
23151
|
+
} else {
|
|
23152
|
+
(l, lt, r, rt)
|
|
23153
|
+
};
|
|
23154
|
+
|
|
22100
23155
|
// An `I32` loop-accumulator (the i32-loop-var lowering) used in a NON-bitwise
|
|
22101
23156
|
// expression reads as its signed int32 value coerced to `f64` — every i32 is exact
|
|
22102
23157
|
// in f64. Bitwise/shift parents never see this: they recurse into the raw AST via
|
|
@@ -22263,6 +23318,41 @@ impl Codegen {
|
|
|
22263
23318
|
}
|
|
22264
23319
|
}
|
|
22265
23320
|
|
|
23321
|
+
// GBA: `key in s` on a statically-known struct → answer NATIVELY against the field
|
|
23322
|
+
// names (no boxing of `s`, no hashmap). A string-literal key folds to a compile-time
|
|
23323
|
+
// bool; a string-typed key becomes a `matches!` over the field names (zero alloc). The
|
|
23324
|
+
// struct is still evaluated (bound to `_`) for its side effects. Any other key kind
|
|
23325
|
+
// falls through to the boxed runtime `in` below. Desktop is untouched.
|
|
23326
|
+
if *op == BinOp::In
|
|
23327
|
+
&& self.emit_mode == crate::NativeEmitMode::Gba
|
|
23328
|
+
{
|
|
23329
|
+
if let RustType::Named { fields, .. } = &rt {
|
|
23330
|
+
if let Expr::Literal { value: Literal::String(s), .. } = left.as_ref() {
|
|
23331
|
+
let present = fields.iter().any(|(k, _)| k.as_ref() == s.as_ref());
|
|
23332
|
+
return Ok((
|
|
23333
|
+
format!("{{ let _ = &({}); Value::Bool({}) }}", r, present),
|
|
23334
|
+
RustType::Value,
|
|
23335
|
+
));
|
|
23336
|
+
}
|
|
23337
|
+
if lt == RustType::String {
|
|
23338
|
+
let arms = fields
|
|
23339
|
+
.iter()
|
|
23340
|
+
.map(|(k, _)| format!("{:?}", k.as_ref()))
|
|
23341
|
+
.collect::<Vec<_>>()
|
|
23342
|
+
.join(" | ");
|
|
23343
|
+
// Bind the key BEFORE evaluating the struct — `key in obj` evaluates
|
|
23344
|
+
// left-to-right, so the key's side effects must run first.
|
|
23345
|
+
return Ok((
|
|
23346
|
+
format!(
|
|
23347
|
+
"{{ let _k = ({}); let _ = &({}); Value::Bool(matches!(_k.as_str(), {})) }}",
|
|
23348
|
+
l, r, arms
|
|
23349
|
+
),
|
|
23350
|
+
RustType::Value,
|
|
23351
|
+
));
|
|
23352
|
+
}
|
|
23353
|
+
}
|
|
23354
|
+
}
|
|
23355
|
+
|
|
22266
23356
|
// Fall back: convert both sides to Value and use the runtime.
|
|
22267
23357
|
let lv = Self::box_operand_for_runtime(left, &l, <);
|
|
22268
23358
|
let rv = Self::box_operand_for_runtime(right, &r, &rt);
|
|
@@ -22349,10 +23439,24 @@ impl Codegen {
|
|
|
22349
23439
|
// Native fast path: `vec[i]` where vec is Vec<T> and i is numeric.
|
|
22350
23440
|
if !optional {
|
|
22351
23441
|
if let Expr::Ident { name, .. } = object.as_ref() {
|
|
22352
|
-
|
|
23442
|
+
{
|
|
23443
|
+
let idx_wrapped = self.refcell_wrapped_vars.contains(name.as_ref());
|
|
22353
23444
|
let obj_type = self.type_context.get_type(name.as_ref());
|
|
22354
23445
|
if let RustType::Vec(elem_type) = &obj_type {
|
|
22355
|
-
|
|
23446
|
+
// A captured (refcell-wrapped) Vec reads through `(*a.borrow())` — WITHOUT
|
|
23447
|
+
// this it fell to the boxed `get_index`, which cloned + `from_struct`-boxed
|
|
23448
|
+
// the WHOLE Vec into a `Value::Array` just to read one element (O(n) per
|
|
23449
|
+
// read — the hot-loop `states[id].timer` footgun, tish#558 read side).
|
|
23450
|
+
let esc_raw = Self::escape_ident(name.as_ref()).into_owned();
|
|
23451
|
+
// #567: for a wrapped Vec the place is the block-local guard `__bg`
|
|
23452
|
+
// (bound below), so the non-reentrant `Arc<Mutex>` borrow is scoped to
|
|
23453
|
+
// this single read and can't overlap a sibling read of the same cell
|
|
23454
|
+
// (`a[i] + a[j]` in one expression), which would deadlock.
|
|
23455
|
+
let esc_obj = if idx_wrapped {
|
|
23456
|
+
"(*__bg)".to_string()
|
|
23457
|
+
} else {
|
|
23458
|
+
esc_raw.clone()
|
|
23459
|
+
};
|
|
22356
23460
|
// #173 part 3: prove the index in-bounds BEFORE emitting it.
|
|
22357
23461
|
let in_bounds = self.index_in_bounds(index, name.as_ref());
|
|
22358
23462
|
let idx_usize = if let Expr::Ident { name: idx_name, .. } =
|
|
@@ -22394,6 +23498,14 @@ impl Codegen {
|
|
|
22394
23498
|
)
|
|
22395
23499
|
}
|
|
22396
23500
|
};
|
|
23501
|
+
// #567: inside the wrapped block the index is pre-bound to `__bi`
|
|
23502
|
+
// (evaluated BEFORE the borrow, so an index that itself reads this cell
|
|
23503
|
+
// — `arr[arr.length-1]` — doesn't double-lock the non-reentrant cell).
|
|
23504
|
+
let idx_ref = if idx_wrapped {
|
|
23505
|
+
"__bi".to_string()
|
|
23506
|
+
} else {
|
|
23507
|
+
idx_usize.clone()
|
|
23508
|
+
};
|
|
22397
23509
|
let elem_ty = *elem_type.clone();
|
|
22398
23510
|
// OOB-safe read for numeric/bool Vecs: JS `arr[oob]` is `undefined`
|
|
22399
23511
|
// (→ NaN / false in those contexts), NOT a panic. In-bounds is the
|
|
@@ -22403,27 +23515,42 @@ impl Codegen {
|
|
|
22403
23515
|
// #173 part 3: a proven in-bounds read skips the `.get().unwrap_or`
|
|
22404
23516
|
// branch — a direct `a[i]` (the `idx < len` proof guarantees it).
|
|
22405
23517
|
RustType::F64 | RustType::Bool | RustType::I32 if in_bounds => {
|
|
22406
|
-
format!("{}[{}]", esc_obj,
|
|
23518
|
+
format!("{}[{}]", esc_obj, idx_ref)
|
|
22407
23519
|
}
|
|
22408
23520
|
RustType::F64 => format!(
|
|
22409
23521
|
"{}.get({}).copied().unwrap_or(f64::NAN)",
|
|
22410
|
-
esc_obj,
|
|
23522
|
+
esc_obj, idx_ref
|
|
22411
23523
|
),
|
|
22412
23524
|
RustType::I32 => format!(
|
|
22413
23525
|
"{}.get({}).copied().unwrap_or(0)",
|
|
22414
|
-
esc_obj,
|
|
23526
|
+
esc_obj, idx_ref
|
|
22415
23527
|
),
|
|
22416
23528
|
RustType::Bool => {
|
|
22417
|
-
format!("{}.get({}).copied().unwrap_or(false)", esc_obj,
|
|
23529
|
+
format!("{}.get({}).copied().unwrap_or(false)", esc_obj, idx_ref)
|
|
22418
23530
|
}
|
|
22419
|
-
// Other element types
|
|
22420
|
-
|
|
23531
|
+
// Other element types (struct/Value/String). From behind a
|
|
23532
|
+
// `borrow()` guard a non-Copy element must be cloned OUT (it can't be
|
|
23533
|
+
// moved from the borrow); a plain local keeps the direct place.
|
|
23534
|
+
_ if idx_wrapped => format!("{}[{}].clone()", esc_obj, idx_ref),
|
|
23535
|
+
_ => format!("{}[{}]", esc_obj, idx_ref),
|
|
23536
|
+
};
|
|
23537
|
+
// #567: wrap the wrapped-Vec read in a block that pre-binds the index
|
|
23538
|
+
// and scopes the borrow guard `__bg`, so the lock is released before
|
|
23539
|
+
// any sibling read of the same cell in the enclosing expression.
|
|
23540
|
+
let access = if idx_wrapped {
|
|
23541
|
+
format!(
|
|
23542
|
+
"{{ let __bi = {}; let __bg = {}.borrow(); {} }}",
|
|
23543
|
+
idx_usize, esc_raw, access
|
|
23544
|
+
)
|
|
23545
|
+
} else {
|
|
23546
|
+
access
|
|
22421
23547
|
};
|
|
22422
23548
|
return Ok((access, elem_ty));
|
|
22423
23549
|
}
|
|
22424
23550
|
// Native tuple access: `tuple[const]` -> `tuple.const` (Rust tuples
|
|
22425
23551
|
// require a literal index; a variable index falls through to boxed).
|
|
22426
|
-
|
|
23552
|
+
// (Plain local only — a wrapped tuple keeps the boxed path.)
|
|
23553
|
+
if let (false, RustType::Tuple(elems)) = (idx_wrapped, &obj_type) {
|
|
22427
23554
|
if let Expr::Literal {
|
|
22428
23555
|
value: Literal::Number(n),
|
|
22429
23556
|
..
|
|
@@ -22501,6 +23628,35 @@ impl Codegen {
|
|
|
22501
23628
|
}
|
|
22502
23629
|
}
|
|
22503
23630
|
}
|
|
23631
|
+
// Typed extern (a `cargo:` import with a declared native signature) used INSIDE a
|
|
23632
|
+
// native/typed expression: `entity_x(e) + 16`, `cvar(e, k) + 1`. Emit the direct
|
|
23633
|
+
// `crate::name_typed(<coerced args>)` and report its NATIVE return type, so the
|
|
23634
|
+
// surrounding arithmetic/comparison stays native instead of boxing the result and
|
|
23635
|
+
// dropping to `ops::*`. Only a native-returning extern is taken here; a `void`/`Value`
|
|
23636
|
+
// one falls through to the boxed path (this is a value-position expression).
|
|
23637
|
+
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
23638
|
+
if let Some(sig) = self.extern_fns.get(fname.as_ref()).cloned() {
|
|
23639
|
+
if sig.ret.is_native()
|
|
23640
|
+
&& args.len() == sig.params.len()
|
|
23641
|
+
&& args.iter().all(|a| matches!(a, CallArg::Expr(_)))
|
|
23642
|
+
{
|
|
23643
|
+
let mut argc: Vec<String> = Vec::with_capacity(args.len());
|
|
23644
|
+
for (a, ty) in args.iter().zip(sig.params.iter()) {
|
|
23645
|
+
if let CallArg::Expr(e) = a {
|
|
23646
|
+
argc.push(self.emit_coerced_native(e, ty)?);
|
|
23647
|
+
}
|
|
23648
|
+
}
|
|
23649
|
+
let call = format!(
|
|
23650
|
+
"{}::{}_typed({})",
|
|
23651
|
+
sig.crate_name,
|
|
23652
|
+
sig.symbol,
|
|
23653
|
+
argc.join(", ")
|
|
23654
|
+
);
|
|
23655
|
+
return Ok((call, sig.ret));
|
|
23656
|
+
}
|
|
23657
|
+
}
|
|
23658
|
+
}
|
|
23659
|
+
|
|
22504
23660
|
// M5: direct call to an eligible native fn -> `name_native(<native args>)`.
|
|
22505
23661
|
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
22506
23662
|
if self.native_fns.contains(fname.as_ref()) {
|
|
@@ -22620,6 +23776,19 @@ impl Codegen {
|
|
|
22620
23776
|
if let Some(res) = self.native_vec_hof_for_call(callee, args)? {
|
|
22621
23777
|
return Ok(res);
|
|
22622
23778
|
}
|
|
23779
|
+
// A call to a user tish function whose declared return type is a native struct
|
|
23780
|
+
// (`function gameState(): GameState`). The function is still a boxed closure, so the call
|
|
23781
|
+
// yields a `Value` — a `Value::Struct` on GBA. Coerce it to the native struct via
|
|
23782
|
+
// `from_value_expr` (a cheap `downcast_struct` on GBA; the `get_prop` rebuild off-GBA), so
|
|
23783
|
+
// the surrounding `gs.score` / `gs.bossHp` reads lower to plain Rust field loads instead of
|
|
23784
|
+
// boxed `get_prop`. A false positive (name shadowed by a non-struct local) is harmless:
|
|
23785
|
+
// `from_value_expr` downcasts if it can and otherwise rebuilds via `get_prop`.
|
|
23786
|
+
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
23787
|
+
if let Some(ret) = self.user_fn_returns.get(fname.as_ref()).cloned() {
|
|
23788
|
+
let boxed = self.emit_expr(expr)?;
|
|
23789
|
+
return Ok((ret.from_value_expr(&boxed), ret));
|
|
23790
|
+
}
|
|
23791
|
+
}
|
|
22623
23792
|
let result = self.emit_expr(expr)?;
|
|
22624
23793
|
Ok((result, RustType::Value))
|
|
22625
23794
|
}
|
|
@@ -22644,7 +23813,7 @@ impl Codegen {
|
|
|
22644
23813
|
if prop_name.as_ref() == "length" {
|
|
22645
23814
|
let var_esc = Self::escape_ident(var_name.as_ref()).into_owned();
|
|
22646
23815
|
let code = if self.refcell_wrapped_vars.contains(var_name.as_ref()) {
|
|
22647
|
-
format!("({}.borrow().len() as f64)", var_esc)
|
|
23816
|
+
format!("({{ let __bg = {}.borrow(); __bg.len() as f64 }})", var_esc)
|
|
22648
23817
|
} else {
|
|
22649
23818
|
format!("({}.len() as f64)", var_esc)
|
|
22650
23819
|
};
|
|
@@ -22659,7 +23828,7 @@ impl Codegen {
|
|
|
22659
23828
|
let field = crate::types::field_ident(prop_name.as_ref());
|
|
22660
23829
|
let access = if self.refcell_wrapped_vars.contains(var_name.as_ref())
|
|
22661
23830
|
{
|
|
22662
|
-
format!("
|
|
23831
|
+
format!("{{ let __bg = {}.borrow(); (*__bg).{}.clone() }}", var_esc, field)
|
|
22663
23832
|
} else {
|
|
22664
23833
|
format!("{}.{}", var_esc, field)
|
|
22665
23834
|
};
|