@tishlang/tish-lsp 2.43.8 → 3.1.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/tests/scope_merge.rs +31 -0
- package/crates/tish_build_utils/src/lib.rs +131 -6
- package/crates/tish_builtins/src/globals.rs +27 -0
- package/crates/tish_compile/src/codegen.rs +1498 -132
- package/crates/tish_compile/src/resolve.rs +65 -9
- 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/shape.rs +28 -3
- package/crates/tish_core/src/value.rs +123 -0
- package/crates/tish_lsp/Cargo.toml +1 -1
- package/crates/tish_native/src/build.rs +4 -0
- package/crates/tish_runtime/Cargo.toml +20 -1
- package/crates/tish_runtime/src/crypto.rs +127 -0
- package/crates/tish_runtime/src/encoding.rs +157 -0
- package/crates/tish_runtime/src/fs_ext.rs +27 -0
- package/crates/tish_runtime/src/lib.rs +33 -0
- package/crates/tish_runtime/src/net.rs +482 -0
- package/crates/tish_runtime/src/process_spawn.rs +508 -0
- package/crates/tish_runtime/src/ws.rs +206 -23
- package/crates/tish_runtime/src/zip.rs +196 -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,16 @@ 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
|
+
/// spec -> the `run()`-local holding that module's namespace object, bound ONCE in the preamble.
|
|
1022
|
+
/// Building it is O(exports) — see `emit_native_namespace_preamble`.
|
|
1023
|
+
native_ns_locals: std::collections::HashMap<String, String>,
|
|
1024
|
+
/// Typed native imports (`declare fun` + a native import): tish name -> its typed signature.
|
|
1025
|
+
/// A matching call emits a DIRECT `<crate>::name_typed(..)` instead of a boxed `value_call`.
|
|
1026
|
+
extern_fns: std::collections::HashMap<String, ExternSig>,
|
|
1027
|
+
/// User tish functions whose declared return type is a native STRUCT (`function f(): Iface`).
|
|
1028
|
+
/// A call in a typed context coerces the boxed result to the native struct (a `Value::Struct`
|
|
1029
|
+
/// downcast on GBA), so `f().field` reads are native instead of `get_prop`. Name -> `RustType::Named`.
|
|
1030
|
+
user_fn_returns: std::collections::HashMap<String, crate::types::RustType>,
|
|
987
1031
|
/// Stack: true = async Rust context (run body), false = sync closure (Tish fn body)
|
|
988
1032
|
async_context_stack: Vec<bool>,
|
|
989
1033
|
loop_stack: Vec<(String, Option<String>)>, // (break_label, continue_update) for innermost loop
|
|
@@ -1220,7 +1264,7 @@ pub(crate) struct Codegen {
|
|
|
1220
1264
|
cse_temp_counter: usize,
|
|
1221
1265
|
/// The program's used NON-ASCII characters (collected from every string literal), as a sorted
|
|
1222
1266
|
/// string. Fed to import schemes via `{charset}` so a scheme can bake only the glyphs a program
|
|
1223
|
-
/// actually uses — e.g. `font
|
|
1267
|
+
/// actually uses — e.g. `font:` bakes ASCII + these, not all ~24k glyphs of a CJK font.
|
|
1224
1268
|
string_charset: String,
|
|
1225
1269
|
}
|
|
1226
1270
|
|
|
@@ -1251,6 +1295,9 @@ impl Codegen {
|
|
|
1251
1295
|
string_charset: String::new(),
|
|
1252
1296
|
features,
|
|
1253
1297
|
native_module_init,
|
|
1298
|
+
native_ns_locals: std::collections::HashMap::new(),
|
|
1299
|
+
extern_fns: std::collections::HashMap::new(),
|
|
1300
|
+
user_fn_returns: std::collections::HashMap::new(),
|
|
1254
1301
|
async_context_stack: Vec::new(),
|
|
1255
1302
|
loop_stack: Vec::new(),
|
|
1256
1303
|
break_stack: Vec::new(),
|
|
@@ -1523,7 +1570,7 @@ impl Codegen {
|
|
|
1523
1570
|
/// only way a tagless image's sprites escape). Registration into the runtime arena happens in
|
|
1524
1571
|
/// the entry below.
|
|
1525
1572
|
/// 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
|
|
1573
|
+
/// Fed to schemes via `{charset}` (see `render_template`) so a `font:` import can bake only the
|
|
1527
1574
|
/// glyphs actually used. ASCII is always baked by the scheme, so it's excluded here. Runtime-built
|
|
1528
1575
|
/// strings (concatenation, numbers) resolve to ASCII, which is covered; a non-ASCII char reachable
|
|
1529
1576
|
/// only through a computed string is the rare gap (documented — add it to a literal if needed).
|
|
@@ -1634,6 +1681,11 @@ impl Codegen {
|
|
|
1634
1681
|
));
|
|
1635
1682
|
}
|
|
1636
1683
|
self.write("}\n\n");
|
|
1684
|
+
// A `TishStruct` impl so this struct can be boxed BY REFERENCE into a `Value`
|
|
1685
|
+
// (`Value::from_struct` → `Value::Struct` on GBA) instead of serialised to a hashmap
|
|
1686
|
+
// object — property access on the boxed value then dispatches here (a small match,
|
|
1687
|
+
// no hash / alloc). See `types::to_value_expr` `Named` arm.
|
|
1688
|
+
self.emit_struct_tish_impl(&struct_name, fields);
|
|
1637
1689
|
// #315: emit a direct JSON serializer for this struct when every field is
|
|
1638
1690
|
// JSON-simple. Emitted for each eligible alias (unused ones are covered by
|
|
1639
1691
|
// the generated crate's `#![allow(unused)]`); the JSON.stringify intercept
|
|
@@ -1657,6 +1709,7 @@ impl Codegen {
|
|
|
1657
1709
|
));
|
|
1658
1710
|
}
|
|
1659
1711
|
self.write("}\n\n");
|
|
1712
|
+
self.emit_struct_tish_impl(&vstruct, vfields);
|
|
1660
1713
|
}
|
|
1661
1714
|
let enum_ident = crate::types::shape_union_enum_ident(uname);
|
|
1662
1715
|
self.write("#[derive(Clone, Debug)]\n");
|
|
@@ -1683,6 +1736,68 @@ impl Codegen {
|
|
|
1683
1736
|
}
|
|
1684
1737
|
}
|
|
1685
1738
|
|
|
1739
|
+
/// Emit `impl tishlang_runtime::TishStruct for TishStruct_<name>` so a typed struct can be boxed
|
|
1740
|
+
/// BY REFERENCE into a `Value` (`Value::from_struct`) rather than eagerly serialised to a hashmap
|
|
1741
|
+
/// object. `tish_get`/`tish_set` are the dynamic property path (a `match` on the field name — no
|
|
1742
|
+
/// hash, no alloc); `tish_to_object` is the materialisation used for JSON / `Object.keys`-family
|
|
1743
|
+
/// and as the non-portable `from_struct` fallback (identical to the historical boundary).
|
|
1744
|
+
fn emit_struct_tish_impl(
|
|
1745
|
+
&mut self,
|
|
1746
|
+
struct_name: &str,
|
|
1747
|
+
fields: &[(std::sync::Arc<str>, crate::types::RustType)],
|
|
1748
|
+
) {
|
|
1749
|
+
self.write("#[allow(non_snake_case, unused)]\n");
|
|
1750
|
+
self.write(&format!(
|
|
1751
|
+
"impl tishlang_runtime::TishStruct for {} {{\n",
|
|
1752
|
+
struct_name
|
|
1753
|
+
));
|
|
1754
|
+
|
|
1755
|
+
// tish_get: field name → boxed Value (Null for an unknown key).
|
|
1756
|
+
self.write(" fn tish_get(&self, __k: &str) -> tishlang_runtime::Value {\n");
|
|
1757
|
+
self.write(" match __k {\n");
|
|
1758
|
+
for (k, ty) in fields {
|
|
1759
|
+
let access = format!("self.{}", crate::types::field_ident(k));
|
|
1760
|
+
let v_expr = if matches!(ty, crate::types::RustType::Value) {
|
|
1761
|
+
format!("{}.clone()", access)
|
|
1762
|
+
} else {
|
|
1763
|
+
ty.to_value_expr(&access)
|
|
1764
|
+
};
|
|
1765
|
+
self.write(&format!(" {:?} => {},\n", k.as_ref(), v_expr));
|
|
1766
|
+
}
|
|
1767
|
+
self.write(" _ => tishlang_runtime::Value::Null,\n");
|
|
1768
|
+
self.write(" }\n }\n");
|
|
1769
|
+
|
|
1770
|
+
// tish_set: field name + boxed Value → coerce into the native field (unknown key = no-op).
|
|
1771
|
+
self.write(" fn tish_set(&mut self, __k: &str, __v: tishlang_runtime::Value) {\n");
|
|
1772
|
+
self.write(" match __k {\n");
|
|
1773
|
+
for (k, ty) in fields {
|
|
1774
|
+
let coerced = if matches!(ty, crate::types::RustType::Value) {
|
|
1775
|
+
"__v".to_string()
|
|
1776
|
+
} else {
|
|
1777
|
+
ty.from_value_expr("__v")
|
|
1778
|
+
};
|
|
1779
|
+
self.write(&format!(
|
|
1780
|
+
" {:?} => {{ self.{} = {}; }},\n",
|
|
1781
|
+
k.as_ref(),
|
|
1782
|
+
crate::types::field_ident(k),
|
|
1783
|
+
coerced
|
|
1784
|
+
));
|
|
1785
|
+
}
|
|
1786
|
+
self.write(" _ => {},\n");
|
|
1787
|
+
self.write(" }\n }\n");
|
|
1788
|
+
|
|
1789
|
+
// tish_to_object: ordered `object_from_pairs` (JS insertion order) — the materialisation path.
|
|
1790
|
+
self.write(" fn tish_to_object(&self) -> tishlang_runtime::Value {\n");
|
|
1791
|
+
self.write(&format!(
|
|
1792
|
+
" {}\n",
|
|
1793
|
+
crate::types::named_struct_to_object_expr(fields, "self")
|
|
1794
|
+
));
|
|
1795
|
+
self.write(" }\n");
|
|
1796
|
+
|
|
1797
|
+
self.write(" fn as_any(&self) -> &dyn core::any::Any { self }\n");
|
|
1798
|
+
self.write("}\n\n");
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1686
1801
|
/// #315: JSON-simple = serializable by a direct per-struct writer that is byte-identical
|
|
1687
1802
|
/// to node's `JSON.stringify` (reuses `write_json_number` / `escape_into`). Whitelist, so
|
|
1688
1803
|
/// any future RustType variant defaults to the sound Value-path fallback.
|
|
@@ -1794,6 +1909,100 @@ impl Codegen {
|
|
|
1794
1909
|
|
|
1795
1910
|
/// #315: direct JSON serializer for `JSON.stringify(arg)` when `arg`'s static type is a
|
|
1796
1911
|
/// simple struct (or Vec of one). Returns None → fall through to the generic Value path.
|
|
1912
|
+
/// A boxed `Value` of a struct field: a `Value`-typed field is cloned out of the borrow; a native
|
|
1913
|
+
/// field boxes through its own `to_value_expr` (which copies/clones internally). `place` is the
|
|
1914
|
+
/// borrowed struct expression (e.g. `_s`), so the access is `place.field`.
|
|
1915
|
+
fn struct_field_to_value(place: &str, k: &std::sync::Arc<str>, fty: &crate::types::RustType) -> String {
|
|
1916
|
+
let access = format!("{}.{}", place, crate::types::field_ident(k));
|
|
1917
|
+
if matches!(fty, crate::types::RustType::Value) {
|
|
1918
|
+
format!("{}.clone()", access)
|
|
1919
|
+
} else {
|
|
1920
|
+
fty.to_value_expr(&access)
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
/// Static-type-aware reflection: `Object.keys(s)` / `Object.values(s)` / `Object.entries(s)` where
|
|
1925
|
+
/// `s`'s static type is a known struct (`RustType::Named`). Reads the struct's fields NATIVELY, so
|
|
1926
|
+
/// the struct is never boxed to a `Value::Struct` (GBA) / hashmap object (elsewhere) and no
|
|
1927
|
+
/// intermediate object is built — only the RESULT array is a `Value` (that is the op's output, and
|
|
1928
|
+
/// unavoidable). Keys come from the compile-time field list; values are native field reads. Returns
|
|
1929
|
+
/// `None` (→ the generic boxed `Object.*` path) for a non-struct arg, a spread/multi arg, or a
|
|
1930
|
+
/// dynamic key. This is target-agnostic: the `TishStruct_*` Rust struct exists on every native
|
|
1931
|
+
/// backend, so the same native reads apply on GBA and desktop alike.
|
|
1932
|
+
fn try_emit_native_object_reflection(
|
|
1933
|
+
&mut self,
|
|
1934
|
+
callee: &Expr,
|
|
1935
|
+
args: &[CallArg],
|
|
1936
|
+
) -> Result<Option<String>, CompileError> {
|
|
1937
|
+
use crate::types::RustType;
|
|
1938
|
+
// GBA only. Desktop keeps reflection on the existing boxed path — it's already correct there
|
|
1939
|
+
// (a struct materialises to a real `Value::Object`), and gating avoids any churn to desktop
|
|
1940
|
+
// benches / codegen-string tests. GBA is where avoiding the `Value::Struct` box + hashmap
|
|
1941
|
+
// actually matters. The native reads are target-agnostic, so this path was validated by
|
|
1942
|
+
// running it on desktop before the gate went in.
|
|
1943
|
+
if self.emit_mode != crate::NativeEmitMode::Gba {
|
|
1944
|
+
return Ok(None);
|
|
1945
|
+
}
|
|
1946
|
+
let Expr::Member {
|
|
1947
|
+
object,
|
|
1948
|
+
prop: MemberProp::Name { name: m, .. },
|
|
1949
|
+
optional: false,
|
|
1950
|
+
..
|
|
1951
|
+
} = callee
|
|
1952
|
+
else {
|
|
1953
|
+
return Ok(None);
|
|
1954
|
+
};
|
|
1955
|
+
if !matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Object") {
|
|
1956
|
+
return Ok(None);
|
|
1957
|
+
}
|
|
1958
|
+
let kind = m.as_ref();
|
|
1959
|
+
if !matches!(kind, "keys" | "values" | "entries") || args.len() != 1 {
|
|
1960
|
+
return Ok(None);
|
|
1961
|
+
}
|
|
1962
|
+
let CallArg::Expr(arg0) = &args[0] else {
|
|
1963
|
+
return Ok(None);
|
|
1964
|
+
};
|
|
1965
|
+
let (code, ty) = self.emit_typed_expr(arg0)?;
|
|
1966
|
+
let RustType::Named { fields, .. } = &ty else {
|
|
1967
|
+
return Ok(None);
|
|
1968
|
+
};
|
|
1969
|
+
let out = match kind {
|
|
1970
|
+
// Keys are the compile-time field names; the struct is evaluated only for side effects.
|
|
1971
|
+
"keys" => {
|
|
1972
|
+
let keys = fields
|
|
1973
|
+
.iter()
|
|
1974
|
+
.map(|(k, _)| format!("Value::String({:?}.into())", k.as_ref()))
|
|
1975
|
+
.collect::<Vec<_>>()
|
|
1976
|
+
.join(", ");
|
|
1977
|
+
format!("{{ let _ = &({}); Value::Array(VmRef::new(vec![{}])) }}", code, keys)
|
|
1978
|
+
}
|
|
1979
|
+
"values" => {
|
|
1980
|
+
let vals = fields
|
|
1981
|
+
.iter()
|
|
1982
|
+
.map(|(k, fty)| Self::struct_field_to_value("_s", k, fty))
|
|
1983
|
+
.collect::<Vec<_>>()
|
|
1984
|
+
.join(", ");
|
|
1985
|
+
format!("{{ let _s = &({}); Value::Array(VmRef::new(vec![{}])) }}", code, vals)
|
|
1986
|
+
}
|
|
1987
|
+
"entries" => {
|
|
1988
|
+
let pairs = fields
|
|
1989
|
+
.iter()
|
|
1990
|
+
.map(|(k, fty)| {
|
|
1991
|
+
format!(
|
|
1992
|
+
"Value::Array(VmRef::new(vec![Value::String({:?}.into()), {}]))",
|
|
1993
|
+
k.as_ref(),
|
|
1994
|
+
Self::struct_field_to_value("_s", k, fty)
|
|
1995
|
+
)
|
|
1996
|
+
})
|
|
1997
|
+
.collect::<Vec<_>>()
|
|
1998
|
+
.join(", ");
|
|
1999
|
+
format!("{{ let _s = &({}); Value::Array(VmRef::new(vec![{}])) }}", code, pairs)
|
|
2000
|
+
}
|
|
2001
|
+
_ => unreachable!("kind checked above"),
|
|
2002
|
+
};
|
|
2003
|
+
Ok(Some(out))
|
|
2004
|
+
}
|
|
2005
|
+
|
|
1797
2006
|
fn try_emit_direct_json_stringify(
|
|
1798
2007
|
&mut self,
|
|
1799
2008
|
arg: &Expr,
|
|
@@ -1839,6 +2048,48 @@ impl Codegen {
|
|
|
1839
2048
|
}
|
|
1840
2049
|
}
|
|
1841
2050
|
|
|
2051
|
+
/// The Rust expression that CONSTRUCTS a native module's namespace object. O(exports): it inserts
|
|
2052
|
+
/// every function the crate exports into a fresh map. Call it once per module, not once per import.
|
|
2053
|
+
fn native_module_ns_ctor(init: &crate::resolve::NativeModuleInit) -> String {
|
|
2054
|
+
match init {
|
|
2055
|
+
crate::resolve::NativeModuleInit::Legacy {
|
|
2056
|
+
crate_name,
|
|
2057
|
+
export_fn,
|
|
2058
|
+
} => format!("{}::{}()", crate_name, export_fn),
|
|
2059
|
+
crate::resolve::NativeModuleInit::Generated { export_fn, .. } => {
|
|
2060
|
+
format!("crate::generated_native::{}()", export_fn)
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
/// Bind every native module's namespace object to a `run()`-local, ONCE, before any import reads
|
|
2066
|
+
/// from one.
|
|
2067
|
+
///
|
|
2068
|
+
/// Each named import lowers to "get this key out of the module's namespace". Building that
|
|
2069
|
+
/// namespace is O(exports) — a fresh `ObjectMap` with an `Arc<str>` key and a native closure per
|
|
2070
|
+
/// exported function — so doing it at every import site is O(imports x exports): importing 68
|
|
2071
|
+
/// names from a crate that exports 68 cost 4,624 allocations to bind 68 functions. On a GBA that
|
|
2072
|
+
/// was measured at 3 seconds of black screen (`tish-gba/examples/bench-boot`), paid before the
|
|
2073
|
+
/// program's first statement; `packages/engine.tish` there imports 117 symbols across two crates
|
|
2074
|
+
/// and cost 4.3s. Hoisting makes it O(imports + exports).
|
|
2075
|
+
///
|
|
2076
|
+
/// Emitted in sorted order so the generated Rust is byte-identical across builds (the map is a
|
|
2077
|
+
/// `HashMap`, whose iteration order is not).
|
|
2078
|
+
fn emit_native_namespace_preamble(&mut self) {
|
|
2079
|
+
let mut specs: Vec<String> = self.native_module_init.keys().cloned().collect();
|
|
2080
|
+
specs.sort();
|
|
2081
|
+
for (i, spec) in specs.iter().enumerate() {
|
|
2082
|
+
// Index rather than a sanitized spec: `cargo:a-b` and `cargo:a_b` sanitize to the same
|
|
2083
|
+
// identifier, and silently binding one module's namespace under another's name would be a
|
|
2084
|
+
// very quiet bug.
|
|
2085
|
+
let local = format!("__tish_native_ns_{}", i);
|
|
2086
|
+
let ctor = Self::native_module_ns_ctor(&self.native_module_init[spec]);
|
|
2087
|
+
// A module can be resolved but have every one of its imports shadowed or unused.
|
|
2088
|
+
self.writeln(&format!("#[allow(unused_variables)] let {} = {};", local, ctor));
|
|
2089
|
+
self.native_ns_locals.insert(spec.clone(), local);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
|
|
1842
2093
|
/// Map native module spec to Rust init expression using resolved package.json modules.
|
|
1843
2094
|
/// For built-in modules (tish:fs, tish:http, tish:process), use builtin_native_module_rust_init.
|
|
1844
2095
|
fn native_module_rust_init(&self, spec: &str, export_name: &str) -> Option<String> {
|
|
@@ -1848,19 +2099,18 @@ impl Codegen {
|
|
|
1848
2099
|
self.native_module_init.get(spec).map(|init| {
|
|
1849
2100
|
// Native modules return a namespace object (like an ES module).
|
|
1850
2101
|
// Named imports extract the field from that namespace: `import { foo } from "pkg"` → `ns.foo`.
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
)
|
|
2102
|
+
// Read it from the hoisted local when there is one; the inline constructor is the fallback
|
|
2103
|
+
// for anything emitted outside `run()`'s preamble.
|
|
2104
|
+
match self.native_ns_locals.get(spec) {
|
|
2105
|
+
Some(local) => format!(
|
|
2106
|
+
"{{ match &{} {{ Value::Object(ref _o) => _o.borrow().strings.get({:?}).cloned().unwrap_or(Value::Null), _ => Value::Null }} }}",
|
|
2107
|
+
local, export_name
|
|
2108
|
+
),
|
|
2109
|
+
None => format!(
|
|
2110
|
+
"{{ let _ns = {}; match _ns {{ Value::Object(ref _o) => _o.borrow().strings.get({:?}).cloned().unwrap_or(Value::Null), _ => Value::Null }} }}",
|
|
2111
|
+
Self::native_module_ns_ctor(init), export_name
|
|
2112
|
+
),
|
|
2113
|
+
}
|
|
1864
2114
|
})
|
|
1865
2115
|
}
|
|
1866
2116
|
|
|
@@ -1893,6 +2143,7 @@ impl Codegen {
|
|
|
1893
2143
|
"realpath" | "realpathSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::realpath(args))"),
|
|
1894
2144
|
"readlink" | "readlinkSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::readlink(args))"),
|
|
1895
2145
|
"truncate" | "truncateSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::truncate(args))"),
|
|
2146
|
+
"chmod" | "chmodSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::chmod(args))"),
|
|
1896
2147
|
"mkdtemp" | "mkdtempSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::mkdtemp(args))"),
|
|
1897
2148
|
"cp" | "cpSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::cp(args))"),
|
|
1898
2149
|
"access" | "accessSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::access(args))"),
|
|
@@ -1916,6 +2167,7 @@ impl Codegen {
|
|
|
1916
2167
|
"realpath" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::realpath_promise(args))"),
|
|
1917
2168
|
"readlink" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::readlink_promise(args))"),
|
|
1918
2169
|
"truncate" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::truncate_promise(args))"),
|
|
2170
|
+
"chmod" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::chmod_promise(args))"),
|
|
1919
2171
|
"mkdtemp" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::mkdtemp_promise(args))"),
|
|
1920
2172
|
"cp" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::cp_promise(args))"),
|
|
1921
2173
|
"access" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::access_promise(args))"),
|
|
@@ -1950,9 +2202,20 @@ impl Codegen {
|
|
|
1950
2202
|
"execFile" => Some("Value::native(|args: &[Value]| tish_process_exec_file(args))"),
|
|
1951
2203
|
"execCapture" => Some("Value::native(|args: &[Value]| tish_process_exec_capture(args))"),
|
|
1952
2204
|
"execFileCapture" => Some("Value::native(|args: &[Value]| tish_process_exec_file_capture(args))"),
|
|
2205
|
+
"spawn" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_spawn(args))"),
|
|
2206
|
+
"readStdout" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_read_stdout(args))"),
|
|
2207
|
+
"readStderr" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_read_stderr(args))"),
|
|
2208
|
+
"writeStdin" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_write_stdin(args))"),
|
|
2209
|
+
"closeStdin" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_close_stdin(args))"),
|
|
2210
|
+
"wait" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_wait(args))"),
|
|
2211
|
+
"kill" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_kill(args))"),
|
|
2212
|
+
"pid" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_pid(args))"),
|
|
1953
2213
|
"argv" => Some("Value::Array(VmRef::new(std::env::args().map(|s| Value::String(s.into())).collect()))"),
|
|
1954
2214
|
"env" => Some("Value::object(std::env::vars().map(|(k,v)| (Arc::from(k.as_str()), Value::String(v.into()))).collect())"),
|
|
1955
|
-
|
|
2215
|
+
// Node-style platform string so `process.platform` reads identically on the JS
|
|
2216
|
+
// target (Node's real global) and native (this): macos->darwin, windows->win32.
|
|
2217
|
+
"platform" => Some("Value::String(match std::env::consts::OS { \"macos\" => \"darwin\", \"windows\" => \"win32\", o => o }.into())"),
|
|
2218
|
+
"process" => Some("{ let mut m = ObjectMap::default(); m.insert(Arc::from(\"exit\"), Value::native(|args: &[Value]| tish_process_exit(args))); m.insert(Arc::from(\"cwd\"), Value::native(|args: &[Value]| tish_process_cwd(args))); m.insert(Arc::from(\"exec\"), Value::native(|args: &[Value]| tish_process_exec(args))); m.insert(Arc::from(\"execFile\"), Value::native(|args: &[Value]| tish_process_exec_file(args))); m.insert(Arc::from(\"execCapture\"), Value::native(|args: &[Value]| tish_process_exec_capture(args))); m.insert(Arc::from(\"execFileCapture\"), Value::native(|args: &[Value]| tish_process_exec_file_capture(args))); m.insert(Arc::from(\"argv\"), Value::Array(VmRef::new(std::env::args().map(|s| Value::String(s.into())).collect()))); m.insert(Arc::from(\"env\"), Value::object(std::env::vars().map(|(k,v)| (Arc::from(k.as_str()), Value::String(v.into()))).collect::<ObjectMap>())); m.insert(Arc::from(\"platform\"), Value::String(match std::env::consts::OS { \"macos\" => \"darwin\", \"windows\" => \"win32\", o => o }.into())); Value::object(m) }"),
|
|
1956
2219
|
_ => None,
|
|
1957
2220
|
},
|
|
1958
2221
|
"tish:ws" if self.has_feature("ws") => match export_name {
|
|
@@ -1982,6 +2245,37 @@ impl Codegen {
|
|
|
1982
2245
|
"pid" => Some("Value::native(|args: &[Value]| tishlang_runtime::pty_pid(args))"),
|
|
1983
2246
|
_ => None,
|
|
1984
2247
|
},
|
|
2248
|
+
"tish:net" if self.has_feature("net") => match export_name {
|
|
2249
|
+
"connect" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_connect(args))"),
|
|
2250
|
+
"read" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_read(args))"),
|
|
2251
|
+
"write" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_write(args))"),
|
|
2252
|
+
"close" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_close(args))"),
|
|
2253
|
+
"listen" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_listen(args))"),
|
|
2254
|
+
"accept" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_accept(args))"),
|
|
2255
|
+
"probe" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_probe(args))"),
|
|
2256
|
+
"sleep" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_sleep(args))"),
|
|
2257
|
+
_ => None,
|
|
2258
|
+
},
|
|
2259
|
+
"tish:encoding" if self.has_feature("encoding") => match export_name {
|
|
2260
|
+
"base64Encode" => Some("Value::native(|args: &[Value]| tishlang_runtime::base64_encode(args))"),
|
|
2261
|
+
"base64Decode" => Some("Value::native(|args: &[Value]| tishlang_runtime::base64_decode(args))"),
|
|
2262
|
+
"base64UrlEncode" => Some("Value::native(|args: &[Value]| tishlang_runtime::base64_url_encode(args))"),
|
|
2263
|
+
"base64UrlDecode" => Some("Value::native(|args: &[Value]| tishlang_runtime::base64_url_decode(args))"),
|
|
2264
|
+
"utf8Decode" => Some("Value::native(|args: &[Value]| tishlang_runtime::utf8_decode(args))"),
|
|
2265
|
+
"utf8Encode" => Some("Value::native(|args: &[Value]| tishlang_runtime::utf8_encode(args))"),
|
|
2266
|
+
_ => None,
|
|
2267
|
+
},
|
|
2268
|
+
"tish:crypto" if self.has_feature("crypto") => match export_name {
|
|
2269
|
+
"sha256" => Some("Value::native(|args: &[Value]| tishlang_runtime::sha256(args))"),
|
|
2270
|
+
"sha256Hex" => Some("Value::native(|args: &[Value]| tishlang_runtime::sha256_hex(args))"),
|
|
2271
|
+
"randomBytes" => Some("Value::native(|args: &[Value]| tishlang_runtime::random_bytes(args))"),
|
|
2272
|
+
_ => None,
|
|
2273
|
+
},
|
|
2274
|
+
"tish:zip" if self.has_feature("zip") => match export_name {
|
|
2275
|
+
"extract" => Some("Value::native(|args: &[Value]| tishlang_runtime::zip_extract(args))"),
|
|
2276
|
+
"entries" => Some("Value::native(|args: &[Value]| tishlang_runtime::zip_entries(args))"),
|
|
2277
|
+
_ => None,
|
|
2278
|
+
},
|
|
1985
2279
|
_ => return None,
|
|
1986
2280
|
};
|
|
1987
2281
|
init.map(String::from)
|
|
@@ -1991,7 +2285,8 @@ impl Codegen {
|
|
|
1991
2285
|
if self.features.contains("full") {
|
|
1992
2286
|
matches!(
|
|
1993
2287
|
name,
|
|
1994
|
-
"http" | "timers" | "fs" | "process" | "regex" | "ws" | "tty" | "pty"
|
|
2288
|
+
"http" | "timers" | "fs" | "process" | "regex" | "ws" | "tty" | "pty" | "net"
|
|
2289
|
+
| "encoding" | "crypto" | "zip"
|
|
1995
2290
|
)
|
|
1996
2291
|
} else {
|
|
1997
2292
|
self.features.contains(name)
|
|
@@ -2502,8 +2797,117 @@ impl Codegen {
|
|
|
2502
2797
|
out
|
|
2503
2798
|
}
|
|
2504
2799
|
|
|
2800
|
+
/// Typed externs (#perf): pair each top-level `declare fun name(a: i32, …): T` with a native
|
|
2801
|
+
/// import of the same name (which supplies the crate) → an [`ExternSig`] in `extern_fns`. A later
|
|
2802
|
+
/// call to `name` with lowerable args emits a DIRECT `<crate>::name_typed(..)` instead of a boxed
|
|
2803
|
+
/// `value_call`. Only fully-typed simple-param signatures qualify; anything else stays boxed.
|
|
2804
|
+
fn collect_extern_fns(&mut self, program: &Program) {
|
|
2805
|
+
use std::collections::HashMap;
|
|
2806
|
+
// Resolve lowers each `import { x } from 'cargo:…'` to `let x = NativeModuleLoad{spec, export}`,
|
|
2807
|
+
// so pair off THOSE bindings (the `Import` statement itself is gone by codegen): local name ->
|
|
2808
|
+
// (module spec, exported Rust fn name). Only a native module with a known init can back a call.
|
|
2809
|
+
let mut name_info: HashMap<String, (String, String)> = HashMap::new();
|
|
2810
|
+
for stmt in &program.statements {
|
|
2811
|
+
if let Statement::VarDecl {
|
|
2812
|
+
name,
|
|
2813
|
+
init: Some(Expr::NativeModuleLoad { spec, export_name, .. }),
|
|
2814
|
+
..
|
|
2815
|
+
} = stmt
|
|
2816
|
+
{
|
|
2817
|
+
if self.native_module_init.contains_key(spec.as_ref()) {
|
|
2818
|
+
name_info.insert(
|
|
2819
|
+
name.to_string(),
|
|
2820
|
+
(spec.to_string(), export_name.to_string()),
|
|
2821
|
+
);
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
if name_info.is_empty() {
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2828
|
+
for stmt in &program.statements {
|
|
2829
|
+
let Statement::DeclareFun {
|
|
2830
|
+
name,
|
|
2831
|
+
params,
|
|
2832
|
+
rest_param: None,
|
|
2833
|
+
return_type,
|
|
2834
|
+
async_: false,
|
|
2835
|
+
..
|
|
2836
|
+
} = stmt
|
|
2837
|
+
else {
|
|
2838
|
+
continue;
|
|
2839
|
+
};
|
|
2840
|
+
let Some((spec, export_name)) = name_info.get(name.as_ref()) else {
|
|
2841
|
+
continue;
|
|
2842
|
+
};
|
|
2843
|
+
let crate_name = match self.native_module_init.get(spec) {
|
|
2844
|
+
Some(init) => init.crate_name().to_string(),
|
|
2845
|
+
None => continue,
|
|
2846
|
+
};
|
|
2847
|
+
// Every parameter must be a simple, explicitly-typed native scalar; a `Value` param (an
|
|
2848
|
+
// untyped one) means the direct call would gain nothing, so keep the whole fn boxed.
|
|
2849
|
+
let mut ptys = Vec::with_capacity(params.len());
|
|
2850
|
+
let mut ok = true;
|
|
2851
|
+
for p in params {
|
|
2852
|
+
match p {
|
|
2853
|
+
FunParam::Simple(tp) if tp.default.is_none() => match &tp.type_ann {
|
|
2854
|
+
Some(ann) => {
|
|
2855
|
+
let ty = RustType::from_annotation(ann);
|
|
2856
|
+
if ty.is_native() {
|
|
2857
|
+
ptys.push(ty);
|
|
2858
|
+
} else {
|
|
2859
|
+
ok = false;
|
|
2860
|
+
break;
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
None => {
|
|
2864
|
+
ok = false;
|
|
2865
|
+
break;
|
|
2866
|
+
}
|
|
2867
|
+
},
|
|
2868
|
+
_ => {
|
|
2869
|
+
ok = false;
|
|
2870
|
+
break;
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
if !ok {
|
|
2875
|
+
continue;
|
|
2876
|
+
}
|
|
2877
|
+
let ret = return_type
|
|
2878
|
+
.as_ref()
|
|
2879
|
+
.map(RustType::from_annotation)
|
|
2880
|
+
.unwrap_or(RustType::Unit);
|
|
2881
|
+
self.extern_fns.insert(
|
|
2882
|
+
name.to_string(),
|
|
2883
|
+
ExternSig { crate_name, symbol: export_name.clone(), params: ptys, ret },
|
|
2884
|
+
);
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
/// Record every top-level `function f(...): Iface` whose return annotation resolves — via the type
|
|
2889
|
+
/// aliases — to a native STRUCT (`RustType::Named`, or a `ShapeUnion`). A later call to `f` in a
|
|
2890
|
+
/// typed position then coerces the boxed result to the native struct (a `Value::Struct` downcast on
|
|
2891
|
+
/// GBA, the `get_prop` rebuild elsewhere), so `f().field` reads are native, not boxed. Runs AFTER
|
|
2892
|
+
/// `collect_type_aliases` so an interface name in the annotation resolves to its `Named` type.
|
|
2893
|
+
fn collect_user_fn_returns(&mut self, program: &Program) {
|
|
2894
|
+
for stmt in &program.statements {
|
|
2895
|
+
if let Statement::FunDecl { name, return_type: Some(ann), .. } = stmt {
|
|
2896
|
+
let ty = RustType::from_annotation_with_aliases(ann, &self.type_aliases);
|
|
2897
|
+
// Only a concrete `Named` struct return is coerced. `ShapeUnion` is deliberately
|
|
2898
|
+
// excluded: its `from_value_expr` is a forbidden boundary (`unreachable!()`), so
|
|
2899
|
+
// admitting it here would compile a runtime panic into the coercion site. (A shape
|
|
2900
|
+
// union only ever arises as an array element type, never a bare fn return, so this
|
|
2901
|
+
// is a defensive guard against a violated invariant, not a live path.)
|
|
2902
|
+
if matches!(ty, RustType::Named { .. }) {
|
|
2903
|
+
self.user_fn_returns.insert(name.to_string(), ty);
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2505
2909
|
fn emit_program(&mut self, program: &Program) -> Result<(), CompileError> {
|
|
2506
|
-
// Only GBA `font
|
|
2910
|
+
// Only GBA `font:`/`emoji:` schemes consume `{charset}` (and both `render_template`
|
|
2507
2911
|
// callers that read `string_charset` are Gba-gated), so skip the whole-program string walk
|
|
2508
2912
|
// for every other target — its result is never read off-GBA.
|
|
2509
2913
|
if self.emit_mode == crate::NativeEmitMode::Gba {
|
|
@@ -2513,6 +2917,7 @@ impl Codegen {
|
|
|
2513
2917
|
self.program_has_jsx = tishlang_ui::jsx::program_contains_jsx(program);
|
|
2514
2918
|
self.program_fun_decl_names = tishlang_ui::jsx::collect_fun_decl_names(program);
|
|
2515
2919
|
self.program_uses_document = crate::resolve::program_uses_document(program);
|
|
2920
|
+
self.collect_extern_fns(program);
|
|
2516
2921
|
if self.emit_mode == crate::NativeEmitMode::Gba {
|
|
2517
2922
|
// no_std GBA ROM: alloc-based, Arc aliases to Rc in the facade. The
|
|
2518
2923
|
// prelude `use tishlang_runtime::{…}` below is unchanged — those names
|
|
@@ -2584,6 +2989,9 @@ impl Codegen {
|
|
|
2584
2989
|
// are stored too, so later annotations like `let x: N = 0` still
|
|
2585
2990
|
// pick up the right native type.
|
|
2586
2991
|
self.collect_type_aliases(&program.statements);
|
|
2992
|
+
// Now that the interface/type-alias table is built, record user functions that return a native
|
|
2993
|
+
// struct, so a typed call to one keeps the result native (`f().field` = a Rust field read).
|
|
2994
|
+
self.collect_user_fn_returns(program);
|
|
2587
2995
|
// Emit a Rust `struct` for every alias whose RHS is an object
|
|
2588
2996
|
// shape. Subsequent `let x: Foo = ...` literals lower to plain
|
|
2589
2997
|
// struct moves (no `VmRef::new(ObjectMap::from(..))` allocation),
|
|
@@ -2770,6 +3178,9 @@ impl Codegen {
|
|
|
2770
3178
|
}
|
|
2771
3179
|
self.indent += 1;
|
|
2772
3180
|
|
|
3181
|
+
// Before anything else: one namespace object per native module, shared by all of its imports.
|
|
3182
|
+
self.emit_native_namespace_preamble();
|
|
3183
|
+
|
|
2773
3184
|
// Initialize builtins
|
|
2774
3185
|
self.writeln("let mut console = Value::object(ObjectMap::from([");
|
|
2775
3186
|
self.indent += 1;
|
|
@@ -3013,6 +3424,9 @@ impl Codegen {
|
|
|
3013
3424
|
self.indent -= 1;
|
|
3014
3425
|
self.writeln("}");
|
|
3015
3426
|
self.writeln("p.insert(Arc::from(\"env\"), Value::object(env_obj));");
|
|
3427
|
+
// Node-style platform string (macos->darwin, windows->win32) so bare `process.platform`
|
|
3428
|
+
// matches the JS target's Node global and the named `tish:process` export.
|
|
3429
|
+
self.writeln("p.insert(Arc::from(\"platform\"), Value::String(match std::env::consts::OS { \"macos\" => \"darwin\", \"windows\" => \"win32\", o => o }.into()));");
|
|
3016
3430
|
self.writeln("p");
|
|
3017
3431
|
self.indent -= 1;
|
|
3018
3432
|
self.writeln("});");
|
|
@@ -3262,6 +3676,15 @@ impl Codegen {
|
|
|
3262
3676
|
return Ok(code);
|
|
3263
3677
|
}
|
|
3264
3678
|
}
|
|
3679
|
+
// #558: `arr[i].field = v` / `s.field = v` on a typed struct → a native element store, not a
|
|
3680
|
+
// discarded set_prop on a copy. Side-effect position (statement), so no value is produced.
|
|
3681
|
+
if let Expr::MemberAssign { object, prop, value, .. } = expr {
|
|
3682
|
+
if let Some(code) =
|
|
3683
|
+
self.try_emit_native_struct_field_assign(object, prop.as_ref(), value, true)?
|
|
3684
|
+
{
|
|
3685
|
+
return Ok(code);
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3265
3688
|
match expr {
|
|
3266
3689
|
Expr::Assign { name, value, .. } => {
|
|
3267
3690
|
if self.native_numeric_globals.contains_key(name.as_ref()) {
|
|
@@ -3285,9 +3708,17 @@ impl Codegen {
|
|
|
3285
3708
|
let escaped = Self::escape_ident(name.as_ref());
|
|
3286
3709
|
return Ok(format!("{} = ({}) as i32", escaped, int_code));
|
|
3287
3710
|
}
|
|
3711
|
+
let (val_code, val_ty) = self.emit_typed_expr(value)?;
|
|
3712
|
+
// An RHS that is ALREADY an integer stores straight into the register. `d = d + 1`
|
|
3713
|
+
// reaches here — it is arithmetic, not a bitwise chain, so `emit_int32_operand`
|
|
3714
|
+
// declines it — and boxing that i32 into a `Value::Number` only to run ToInt32 back
|
|
3715
|
+
// out of it was the whole cost of incrementing an annotated counter.
|
|
3716
|
+
let escaped_int = Self::escape_ident(name.as_ref());
|
|
3717
|
+
if val_ty.is_integer_scalar() {
|
|
3718
|
+
return Ok(format!("{} = ({}) as i32", escaped_int, val_code));
|
|
3719
|
+
}
|
|
3288
3720
|
// Defensive: gate guarantees `Some`, but if a future RHS shape slips through,
|
|
3289
3721
|
// fall back to a sound f64-narrowed store rather than miscompiling.
|
|
3290
|
-
let (val_code, val_ty) = self.emit_typed_expr(value)?;
|
|
3291
3722
|
let v = if val_ty.is_native() {
|
|
3292
3723
|
val_ty.to_value_expr(&val_code)
|
|
3293
3724
|
} else {
|
|
@@ -3360,6 +3791,11 @@ impl Codegen {
|
|
|
3360
3791
|
let (val_code, val_ty) = self.emit_typed_expr(value)?;
|
|
3361
3792
|
let native_val = if val_ty == RustType::Value {
|
|
3362
3793
|
rust_type.from_value_expr(&val_code)
|
|
3794
|
+
} else if rust_type == RustType::Fixed && val_ty == RustType::F64 {
|
|
3795
|
+
// an f64 (an int/float literal like `nx = 0`, or a promoted int) assigned to a
|
|
3796
|
+
// `fixed` local must be lifted to `Fixed` — the RHS emitter reports numeric
|
|
3797
|
+
// literals as F64, so without this a `fixed` reassignment mistypes (E0308).
|
|
3798
|
+
format!("tishlang_runtime::Fixed::from_raw((({}) * 256.0) as i32)", val_code)
|
|
3363
3799
|
} else {
|
|
3364
3800
|
val_code
|
|
3365
3801
|
};
|
|
@@ -6322,7 +6758,8 @@ impl Codegen {
|
|
|
6322
6758
|
matches!(
|
|
6323
6759
|
t,
|
|
6324
6760
|
RustType::F64 | RustType::Bool | RustType::String
|
|
6325
|
-
|
|
6761
|
+
| RustType::Fixed | RustType::I32
|
|
6762
|
+
) || t.is_narrow_int()
|
|
6326
6763
|
})
|
|
6327
6764
|
} else {
|
|
6328
6765
|
None
|
|
@@ -6430,6 +6867,21 @@ impl Codegen {
|
|
|
6430
6867
|
for v in &read_only_outer_vars {
|
|
6431
6868
|
self.refcell_wrapped_vars.remove(v);
|
|
6432
6869
|
}
|
|
6870
|
+
// tishlang/tish#556: a PARAMETER (or the rest param) SHADOWS any outer var of the
|
|
6871
|
+
// same name — inside the body it is a plain `let`-bound `Value`/native, never the
|
|
6872
|
+
// outer `VmRef` cell. Drop such names from the wrapped set for the body, so a
|
|
6873
|
+
// reference to the param isn't emitted as `vm_read(¶m)` (which won't compile —
|
|
6874
|
+
// the param is a `Value`, not a `VmRef`). `saved_refcell` restores the outer view
|
|
6875
|
+
// after the body; a body-local `let` that a nested closure captures is re-added by
|
|
6876
|
+
// the `body_cell_vars` prepass below, so its cell handling is unaffected.
|
|
6877
|
+
for p in params {
|
|
6878
|
+
for bn in p.bound_names() {
|
|
6879
|
+
self.refcell_wrapped_vars.remove(bn.as_ref());
|
|
6880
|
+
}
|
|
6881
|
+
}
|
|
6882
|
+
if let Some(rest) = rest_param {
|
|
6883
|
+
self.refcell_wrapped_vars.remove(rest.name.as_ref());
|
|
6884
|
+
}
|
|
6433
6885
|
|
|
6434
6886
|
// Pre-scan body for nested functions (handles function body as Block)
|
|
6435
6887
|
if let Statement::Block { statements, .. } = body.as_ref() {
|
|
@@ -7153,6 +7605,12 @@ impl Codegen {
|
|
|
7153
7605
|
}
|
|
7154
7606
|
}
|
|
7155
7607
|
}
|
|
7608
|
+
// Static-type-aware reflection: `Object.keys/values/entries(s)` on a statically-known
|
|
7609
|
+
// struct → native field reads, never boxing the struct or building an intermediate
|
|
7610
|
+
// object. Falls through to the generic boxed `Object.*` path for a non-struct arg.
|
|
7611
|
+
if let Some(code) = self.try_emit_native_object_reflection(callee, args)? {
|
|
7612
|
+
return Ok(code);
|
|
7613
|
+
}
|
|
7156
7614
|
// #178: route a boxed-context call to a recursive-struct consumer
|
|
7157
7615
|
// (`sum(build(n))` / `sum(node)`) → `Value::Number(sum_rec(&..))`.
|
|
7158
7616
|
if self.rec_struct_plan.is_some() {
|
|
@@ -7184,29 +7642,273 @@ impl Codegen {
|
|
|
7184
7642
|
// ── native Vec<T> push fast path ──────────────────────────────
|
|
7185
7643
|
if method_name.as_ref() == "push" {
|
|
7186
7644
|
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
|
-
|
|
7645
|
+
let obj_type = self.type_context.get_type(name.as_ref());
|
|
7646
|
+
if let RustType::Vec(elem_type) = obj_type {
|
|
7647
|
+
let esc_obj = Self::escape_ident(name.as_ref()).into_owned();
|
|
7648
|
+
// A captured (refcell-wrapped) native Vec pushes through
|
|
7649
|
+
// `(*a.borrow_mut())` — WITHOUT this it fell to the boxed `array_push`,
|
|
7650
|
+
// which boxed the WHOLE Vec into a throwaway `Value::Array` (discarding
|
|
7651
|
+
// the push, O(n), and — for a `Vec<struct>` — E0282 on the `from_struct`
|
|
7652
|
+
// boxing closure since the native push never ran to pin the element type).
|
|
7653
|
+
let base = if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
7654
|
+
format!("(*{}.borrow_mut())", esc_obj)
|
|
7655
|
+
} else {
|
|
7656
|
+
esc_obj
|
|
7657
|
+
};
|
|
7658
|
+
// Collect push arguments as native values.
|
|
7659
|
+
let mut push_stmts: Vec<String> = Vec::new();
|
|
7660
|
+
for a in args {
|
|
7661
|
+
if let CallArg::Expr(e) = a {
|
|
7662
|
+
// Emit the element AGAINST the Vec's element type so an object
|
|
7663
|
+
// literal hits the struct-literal fast path (direct `Struct { … }`
|
|
7664
|
+
// with each field value emitted natively — zero allocations)
|
|
7665
|
+
// instead of building a boxed `object_from_pairs` and then
|
|
7666
|
+
// `get_prop`-ing every field back out. emit_native_expr falls
|
|
7667
|
+
// back to the typed/value path for non-literal args.
|
|
7668
|
+
let native_val = self.emit_native_expr(e, elem_type.as_ref())?;
|
|
7669
|
+
push_stmts.push(format!("{}.push({});", base, native_val));
|
|
7204
7670
|
}
|
|
7205
|
-
return Ok(format!(
|
|
7206
|
-
"{{ {} Value::Null }}",
|
|
7207
|
-
push_stmts.join(" ")
|
|
7208
|
-
));
|
|
7209
7671
|
}
|
|
7672
|
+
// `push` returns the new length (JS / tish interp) — a plain read, no
|
|
7673
|
+
// O(n) boxing. A captured Vec reads its length through `(*a.borrow())`.
|
|
7674
|
+
let base_read = if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
7675
|
+
format!("(*{}.borrow())", Self::escape_ident(name.as_ref()))
|
|
7676
|
+
} else {
|
|
7677
|
+
Self::escape_ident(name.as_ref()).into_owned()
|
|
7678
|
+
};
|
|
7679
|
+
return Ok(format!(
|
|
7680
|
+
"{{ {} Value::Number({}.len() as f64) }}",
|
|
7681
|
+
push_stmts.join(" "),
|
|
7682
|
+
base_read
|
|
7683
|
+
));
|
|
7684
|
+
}
|
|
7685
|
+
}
|
|
7686
|
+
}
|
|
7687
|
+
|
|
7688
|
+
// Native in-place mutators on a native `Vec` (`pop`/`shift`/`unshift`/`reverse`).
|
|
7689
|
+
// The boxed `array_*` fallback operates on a THROWAWAY boxed copy of the Vec, so the
|
|
7690
|
+
// mutation is DISCARDED (O(n), and — for a `Vec<struct>` — an E0282 on the boxing
|
|
7691
|
+
// closure); these mutate the real Vec. A captured Vec goes through `(*a.borrow_mut())`.
|
|
7692
|
+
// Return values match JS / tish interp: pop/shift yield the removed element (boxed) or
|
|
7693
|
+
// Null; unshift yields the new length; reverse yields THE array (boxed copy — a native
|
|
7694
|
+
// `Vec` has no O(1) by-ref boxing like interp's Rc-array, so the contents match but it
|
|
7695
|
+
// is not aliased; reverse is O(n) anyway so the copy is asymptotically free).
|
|
7696
|
+
if matches!(method_name.as_ref(), "pop" | "shift" | "unshift" | "reverse") {
|
|
7697
|
+
if let Expr::Ident { name, .. } = object.as_ref() {
|
|
7698
|
+
if let RustType::Vec(elem_type) =
|
|
7699
|
+
self.type_context.get_type(name.as_ref())
|
|
7700
|
+
{
|
|
7701
|
+
let esc = Self::escape_ident(name.as_ref()).into_owned();
|
|
7702
|
+
let wrapped_v = self.refcell_wrapped_vars.contains(name.as_ref());
|
|
7703
|
+
let base = if wrapped_v {
|
|
7704
|
+
format!("(*{}.borrow_mut())", esc)
|
|
7705
|
+
} else {
|
|
7706
|
+
esc.clone()
|
|
7707
|
+
};
|
|
7708
|
+
// Shared-borrow read (for `reverse`'s result boxing / length reads).
|
|
7709
|
+
let base_read = if wrapped_v {
|
|
7710
|
+
format!("(*{}.borrow())", esc)
|
|
7711
|
+
} else {
|
|
7712
|
+
esc.clone()
|
|
7713
|
+
};
|
|
7714
|
+
// Box a removed element back to a `Value` (`Value` elements pass through).
|
|
7715
|
+
let box_elem = |v: &str| -> String {
|
|
7716
|
+
if matches!(elem_type.as_ref(), RustType::Value) {
|
|
7717
|
+
v.to_string()
|
|
7718
|
+
} else {
|
|
7719
|
+
elem_type.to_value_expr(v)
|
|
7720
|
+
}
|
|
7721
|
+
};
|
|
7722
|
+
let code = match method_name.as_ref() {
|
|
7723
|
+
"pop" => format!(
|
|
7724
|
+
"{{ match {b}.pop() {{ Some(__e) => {v}, None => Value::Null }} }}",
|
|
7725
|
+
b = base,
|
|
7726
|
+
v = box_elem("__e")
|
|
7727
|
+
),
|
|
7728
|
+
"shift" => format!(
|
|
7729
|
+
"{{ if {b}.is_empty() {{ Value::Null }} else {{ let __e = {b}.remove(0); {v} }} }}",
|
|
7730
|
+
b = base,
|
|
7731
|
+
v = box_elem("__e")
|
|
7732
|
+
),
|
|
7733
|
+
"unshift" => {
|
|
7734
|
+
// Bind items to temps BEFORE any `borrow_mut` insert (an item may
|
|
7735
|
+
// read the same array), then insert at the front in REVERSE so the
|
|
7736
|
+
// argument order is preserved. Returns the new length.
|
|
7737
|
+
let mut binds = String::new();
|
|
7738
|
+
let mut names: Vec<String> = Vec::new();
|
|
7739
|
+
for (i, a) in args.iter().enumerate() {
|
|
7740
|
+
if let CallArg::Expr(e) = a {
|
|
7741
|
+
let it = self.emit_native_expr(e, elem_type.as_ref())?;
|
|
7742
|
+
binds.push_str(&format!("let __u{} = {}; ", i, it));
|
|
7743
|
+
names.push(format!("__u{}", i));
|
|
7744
|
+
}
|
|
7745
|
+
}
|
|
7746
|
+
let mut ins = String::new();
|
|
7747
|
+
for n in names.iter().rev() {
|
|
7748
|
+
ins.push_str(&format!("{}.insert(0, {}); ", base, n));
|
|
7749
|
+
}
|
|
7750
|
+
format!(
|
|
7751
|
+
"{{ {}{}Value::Number({}.len() as f64) }}",
|
|
7752
|
+
binds, ins, base
|
|
7753
|
+
)
|
|
7754
|
+
}
|
|
7755
|
+
// reverse in place, then return a boxed copy of the array (contents
|
|
7756
|
+
// match interp; see the note above).
|
|
7757
|
+
"reverse" => format!(
|
|
7758
|
+
"{{ {}.reverse(); Value::Array(VmRef::new({}.iter().cloned().map(|__e| {}).collect())) }}",
|
|
7759
|
+
base,
|
|
7760
|
+
base_read,
|
|
7761
|
+
box_elem("__e")
|
|
7762
|
+
),
|
|
7763
|
+
_ => unreachable!("method checked above"),
|
|
7764
|
+
};
|
|
7765
|
+
return Ok(code);
|
|
7766
|
+
}
|
|
7767
|
+
}
|
|
7768
|
+
}
|
|
7769
|
+
|
|
7770
|
+
// Native `splice`/`fill`/`copyWithin` on a native `Vec`. Like the mutators above,
|
|
7771
|
+
// the boxed fallback would run on a THROWAWAY copy (mutation discarded). These need
|
|
7772
|
+
// several accesses (len + index math + a range op), so a captured Vec binds ONE
|
|
7773
|
+
// `borrow_mut` guard `__g` for the whole body; every argument is evaluated BEFORE the
|
|
7774
|
+
// guard (an arg may read the same array, which would double-borrow the cell). JS index
|
|
7775
|
+
// clamping (negative → from end, out-of-range → clamped) is emitted inline so the
|
|
7776
|
+
// result matches the interpreter.
|
|
7777
|
+
if matches!(method_name.as_ref(), "splice" | "fill" | "copyWithin") {
|
|
7778
|
+
if let Expr::Ident { name, .. } = object.as_ref() {
|
|
7779
|
+
if let RustType::Vec(elem_type) =
|
|
7780
|
+
self.type_context.get_type(name.as_ref())
|
|
7781
|
+
{
|
|
7782
|
+
let esc = Self::escape_ident(name.as_ref()).into_owned();
|
|
7783
|
+
let wrapped = self.refcell_wrapped_vars.contains(name.as_ref());
|
|
7784
|
+
// `p` is the Vec place inside the body: a `borrow_mut` guard when wrapped.
|
|
7785
|
+
let (guard, p) = if wrapped {
|
|
7786
|
+
(format!("let mut __g = {}.borrow_mut(); ", esc), "__g".to_string())
|
|
7787
|
+
} else {
|
|
7788
|
+
(String::new(), esc.clone())
|
|
7789
|
+
};
|
|
7790
|
+
let elem_rust = elem_type.to_rust_type_str();
|
|
7791
|
+
let box_elem = |v: &str| -> String {
|
|
7792
|
+
if matches!(elem_type.as_ref(), RustType::Value) {
|
|
7793
|
+
v.to_string()
|
|
7794
|
+
} else {
|
|
7795
|
+
elem_type.to_value_expr(v)
|
|
7796
|
+
}
|
|
7797
|
+
};
|
|
7798
|
+
// `fill`/`copyWithin` return THE array (JS / tish interp) — a boxed copy
|
|
7799
|
+
// built from the (still-borrowed) place `p` after the mutation. Contents
|
|
7800
|
+
// match interp; not aliased (a native `Vec` has no O(1) by-ref boxing).
|
|
7801
|
+
let arr_box = format!(
|
|
7802
|
+
"Value::Array(VmRef::new({}.iter().cloned().map(|__be| {}).collect()))",
|
|
7803
|
+
p,
|
|
7804
|
+
box_elem("__be")
|
|
7805
|
+
);
|
|
7806
|
+
// f64 arg (bound before the guard) → a clamped `usize` index.
|
|
7807
|
+
let clamp = |v: &str| -> String {
|
|
7808
|
+
format!(
|
|
7809
|
+
"(if {v} < 0.0 {{ (__lenf + {v}).max(0.0) }} else {{ {v}.min(__lenf) }}) as usize"
|
|
7810
|
+
)
|
|
7811
|
+
};
|
|
7812
|
+
let argf = |cg: &mut Self, i: usize| -> Result<Option<String>, CompileError> {
|
|
7813
|
+
match args.get(i) {
|
|
7814
|
+
Some(CallArg::Expr(e)) => {
|
|
7815
|
+
Ok(Some(cg.emit_native_expr(e, &RustType::F64)?))
|
|
7816
|
+
}
|
|
7817
|
+
_ => Ok(None),
|
|
7818
|
+
}
|
|
7819
|
+
};
|
|
7820
|
+
let code = match method_name.as_ref() {
|
|
7821
|
+
"splice" => {
|
|
7822
|
+
let start = argf(self, 0)?.unwrap_or_else(|| "0.0".into());
|
|
7823
|
+
let dc = argf(self, 1)?;
|
|
7824
|
+
// Items to insert (args[2..]) — bound before the guard.
|
|
7825
|
+
let mut item_binds = String::new();
|
|
7826
|
+
let mut item_names: Vec<String> = Vec::new();
|
|
7827
|
+
for (i, a) in args.iter().enumerate().skip(2) {
|
|
7828
|
+
if let CallArg::Expr(e) = a {
|
|
7829
|
+
let it = self.emit_native_expr(e, elem_type.as_ref())?;
|
|
7830
|
+
item_binds.push_str(&format!("let __it{} = {}; ", i, it));
|
|
7831
|
+
item_names.push(format!("__it{}", i));
|
|
7832
|
+
}
|
|
7833
|
+
}
|
|
7834
|
+
let dc_bind = dc
|
|
7835
|
+
.as_ref()
|
|
7836
|
+
.map(|d| format!("let __dc = {}; ", d))
|
|
7837
|
+
.unwrap_or_default();
|
|
7838
|
+
let del_calc = if dc.is_some() {
|
|
7839
|
+
"let __del = if __dc < 0.0 { 0usize } else { (__dc as usize).min(__len - __start) };"
|
|
7840
|
+
} else {
|
|
7841
|
+
"let __del = __len - __start;"
|
|
7842
|
+
};
|
|
7843
|
+
format!(
|
|
7844
|
+
"{{ let __s0 = {start}; {dc_bind}{item_binds}{guard}\
|
|
7845
|
+
let __len = {p}.len(); let __lenf = __len as f64; \
|
|
7846
|
+
let __start = {start_clamp}; {del_calc} \
|
|
7847
|
+
let __items: Vec<{elem_rust}> = vec![{items}]; \
|
|
7848
|
+
let __removed: Vec<{elem_rust}> = {p}.splice(__start..__start + __del, __items).collect(); \
|
|
7849
|
+
Value::Array(VmRef::new(__removed.into_iter().map(|__e| {boxed}).collect())) }}",
|
|
7850
|
+
start_clamp = clamp("__s0"),
|
|
7851
|
+
items = item_names.join(", "),
|
|
7852
|
+
boxed = box_elem("__e"),
|
|
7853
|
+
)
|
|
7854
|
+
}
|
|
7855
|
+
"fill" => {
|
|
7856
|
+
// fill(value, start=0, end=len)
|
|
7857
|
+
let val = match args.first() {
|
|
7858
|
+
Some(CallArg::Expr(e)) => {
|
|
7859
|
+
self.emit_native_expr(e, elem_type.as_ref())?
|
|
7860
|
+
}
|
|
7861
|
+
_ => elem_type.default_value(),
|
|
7862
|
+
};
|
|
7863
|
+
let start = argf(self, 1)?.unwrap_or_else(|| "0.0".into());
|
|
7864
|
+
let end = argf(self, 2)?;
|
|
7865
|
+
format!(
|
|
7866
|
+
"{{ let __v = {val}; let __s0 = {start}; {end}{guard}\
|
|
7867
|
+
let __len = {p}.len(); let __lenf = __len as f64; \
|
|
7868
|
+
let __start = {start_clamp}; {end_clamp}\
|
|
7869
|
+
let mut __i = __start; while __i < __end {{ {p}[__i] = __v.clone(); __i += 1; }} \
|
|
7870
|
+
{arr_box} }}",
|
|
7871
|
+
end = end
|
|
7872
|
+
.as_ref()
|
|
7873
|
+
.map(|e| format!("let __e0 = {}; ", e))
|
|
7874
|
+
.unwrap_or_default(),
|
|
7875
|
+
start_clamp = clamp("__s0"),
|
|
7876
|
+
end_clamp = end
|
|
7877
|
+
.as_ref()
|
|
7878
|
+
.map(|_| format!("let __end = {}; ", clamp("__e0")))
|
|
7879
|
+
.unwrap_or_else(|| "let __end = __len; ".into()),
|
|
7880
|
+
arr_box = arr_box,
|
|
7881
|
+
)
|
|
7882
|
+
}
|
|
7883
|
+
"copyWithin" => {
|
|
7884
|
+
// copyWithin(target, start=0, end=len) — overlap-safe via a temp.
|
|
7885
|
+
let target = argf(self, 0)?.unwrap_or_else(|| "0.0".into());
|
|
7886
|
+
let start = argf(self, 1)?.unwrap_or_else(|| "0.0".into());
|
|
7887
|
+
let end = argf(self, 2)?;
|
|
7888
|
+
format!(
|
|
7889
|
+
"{{ let __t0 = {target}; let __s0 = {start}; {end_bind}{guard}\
|
|
7890
|
+
let __len = {p}.len(); let __lenf = __len as f64; \
|
|
7891
|
+
let __target = {target_clamp}; let __start = {start_clamp}; {end_clamp}\
|
|
7892
|
+
let __count = if __end > __start {{ (__end - __start).min(__len - __target) }} else {{ 0 }}; \
|
|
7893
|
+
let __tmp: Vec<{elem_rust}> = {p}[__start..__start + __count].to_vec(); \
|
|
7894
|
+
let mut __k = 0usize; for __val in __tmp {{ {p}[__target + __k] = __val; __k += 1; }} \
|
|
7895
|
+
{arr_box} }}",
|
|
7896
|
+
end_bind = end
|
|
7897
|
+
.as_ref()
|
|
7898
|
+
.map(|e| format!("let __e0 = {}; ", e))
|
|
7899
|
+
.unwrap_or_default(),
|
|
7900
|
+
target_clamp = clamp("__t0"),
|
|
7901
|
+
start_clamp = clamp("__s0"),
|
|
7902
|
+
end_clamp = end
|
|
7903
|
+
.as_ref()
|
|
7904
|
+
.map(|_| format!("let __end = {}; ", clamp("__e0")))
|
|
7905
|
+
.unwrap_or_else(|| "let __end = __len; ".into()),
|
|
7906
|
+
arr_box = arr_box,
|
|
7907
|
+
)
|
|
7908
|
+
}
|
|
7909
|
+
_ => unreachable!("method checked above"),
|
|
7910
|
+
};
|
|
7911
|
+
return Ok(code);
|
|
7210
7912
|
}
|
|
7211
7913
|
}
|
|
7212
7914
|
}
|
|
@@ -7219,35 +7921,122 @@ impl Codegen {
|
|
|
7219
7921
|
// sorts via the runtime, and writes the result back into the Vec (correct).
|
|
7220
7922
|
if method_name.as_ref() == "sort" {
|
|
7221
7923
|
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
|
-
|
|
7924
|
+
if let RustType::Vec(elem_type) =
|
|
7925
|
+
self.type_context.get_type(name.as_ref())
|
|
7926
|
+
{
|
|
7927
|
+
if matches!(elem_type.as_ref(), RustType::Named { .. }) {
|
|
7928
|
+
let esc = Self::escape_ident(name.as_ref()).into_owned();
|
|
7929
|
+
// A captured (refcell-wrapped) Vec sorts through the shared cell:
|
|
7930
|
+
// `base_mut` for the in-place `sort_by`, `base_read` for the boxing
|
|
7931
|
+
// scan, `assign` for the general-comparator write-back.
|
|
7932
|
+
let wrapped = self.refcell_wrapped_vars.contains(name.as_ref());
|
|
7933
|
+
let base_mut = if wrapped {
|
|
7934
|
+
format!("(*{}.borrow_mut())", esc)
|
|
7935
|
+
} else {
|
|
7936
|
+
esc.clone()
|
|
7937
|
+
};
|
|
7938
|
+
let base_read = if wrapped {
|
|
7939
|
+
format!("(*{}.borrow())", esc)
|
|
7940
|
+
} else {
|
|
7941
|
+
esc.clone()
|
|
7942
|
+
};
|
|
7943
|
+
let assign = if wrapped {
|
|
7944
|
+
format!("*{}.borrow_mut()", esc)
|
|
7945
|
+
} else {
|
|
7946
|
+
esc.clone()
|
|
7947
|
+
};
|
|
7948
|
+
// `sort` returns THE array (JS / tish interp) — a boxed copy of the
|
|
7949
|
+
// now-sorted Vec (contents match interp; not aliased). `sort` is
|
|
7950
|
+
// O(n log n), so the O(n) box is asymptotically free.
|
|
7951
|
+
let sort_box = format!(
|
|
7952
|
+
"Value::Array(VmRef::new({}.iter().cloned().map(|__se| {}).collect()))",
|
|
7953
|
+
base_read,
|
|
7954
|
+
elem_type.to_value_expr("__se")
|
|
7955
|
+
);
|
|
7956
|
+
if let Some(CallArg::Expr(cmp)) = args.first() {
|
|
7957
|
+
if let Some(keys) =
|
|
7958
|
+
Self::detect_lexicographic_key_comparator(cmp)
|
|
7959
|
+
{
|
|
7960
|
+
if keys.iter().all(|(p, _)| p.len() == 1) {
|
|
7961
|
+
let chain = Self::render_struct_sort_chain(&keys);
|
|
7962
|
+
return Ok(format!(
|
|
7963
|
+
"{{ {base_mut}.sort_by(|a, b| {chain}); {sort_box} }}"
|
|
7964
|
+
));
|
|
7238
7965
|
}
|
|
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
7966
|
}
|
|
7967
|
+
// General comparator: box → sort → write back (correct).
|
|
7968
|
+
let cmp_code = self.emit_expr(cmp)?;
|
|
7969
|
+
let to_v = elem_type.to_value_expr("__e");
|
|
7970
|
+
let from_v = elem_type.from_value_expr("__v");
|
|
7971
|
+
return Ok(format!(
|
|
7972
|
+
"{{ let __sv: Vec<Value> = {base_read}.iter().map(|__e| {to_v}).collect(); \
|
|
7973
|
+
let __sorted = tishlang_runtime::array_sort(&Value::Array(VmRef::new(__sv)), Some(&({cmp_code}))); \
|
|
7974
|
+
if let Value::Array(__a) = __sorted {{ {assign} = __a.borrow().iter().map(|__v| {from_v}).collect(); }} \
|
|
7975
|
+
{sort_box} }}"
|
|
7976
|
+
));
|
|
7250
7977
|
}
|
|
7978
|
+
} else {
|
|
7979
|
+
// #568: a scalar / `Value` native `Vec` (`number[]`, `string[]`,
|
|
7980
|
+
// …) sorts IN PLACE like interp/vm/JS. Box a copy, sort it via the
|
|
7981
|
+
// SAME runtime fn the boxed path uses (so order is byte-identical),
|
|
7982
|
+
// then WRITE the ordered elements BACK into the native Vec — the
|
|
7983
|
+
// generic boxed dispatch below sorted a throwaway copy and dropped
|
|
7984
|
+
// the write (a silent no-op; only `arr = arr.sort()` worked).
|
|
7985
|
+
// Returns the sorted array (JS / interp). `sort` marks the receiver
|
|
7986
|
+
// assigned (see `collect_assigned_idents_in_expr`), so the Vec is
|
|
7987
|
+
// `mut` and the write-back compiles.
|
|
7988
|
+
let esc = Self::escape_ident(name.as_ref()).into_owned();
|
|
7989
|
+
let wrapped =
|
|
7990
|
+
self.refcell_wrapped_vars.contains(name.as_ref());
|
|
7991
|
+
let base_read = if wrapped {
|
|
7992
|
+
format!("(*{}.borrow())", esc)
|
|
7993
|
+
} else {
|
|
7994
|
+
esc.clone()
|
|
7995
|
+
};
|
|
7996
|
+
let assign = if wrapped {
|
|
7997
|
+
format!("*{}.borrow_mut()", esc)
|
|
7998
|
+
} else {
|
|
7999
|
+
esc.clone()
|
|
8000
|
+
};
|
|
8001
|
+
let to_v = elem_type.to_value_expr("__e");
|
|
8002
|
+
let from_v = elem_type.from_value_expr("__v");
|
|
8003
|
+
// Mirror the boxed path's comparator detection EXACTLY (numeric
|
|
8004
|
+
// fast path → lexicographic keys → general callback → default),
|
|
8005
|
+
// so the sorted order matches the generic path and interp/vm.
|
|
8006
|
+
let sort_call = if let Some(CallArg::Expr(cmp)) = args.first() {
|
|
8007
|
+
if let Some(ascending) =
|
|
8008
|
+
Self::detect_numeric_sort_comparator(cmp)
|
|
8009
|
+
{
|
|
8010
|
+
if ascending {
|
|
8011
|
+
"tishlang_runtime::array_sort_numeric_asc(&__boxed)"
|
|
8012
|
+
.to_string()
|
|
8013
|
+
} else {
|
|
8014
|
+
"tishlang_runtime::array_sort_numeric_desc(&__boxed)"
|
|
8015
|
+
.to_string()
|
|
8016
|
+
}
|
|
8017
|
+
} else if let Some(keys) =
|
|
8018
|
+
Self::detect_lexicographic_key_comparator(cmp)
|
|
8019
|
+
{
|
|
8020
|
+
format!(
|
|
8021
|
+
"tishlang_runtime::array_sort_by_keys(&__boxed, {})",
|
|
8022
|
+
Self::render_sort_keys(&keys)
|
|
8023
|
+
)
|
|
8024
|
+
} else {
|
|
8025
|
+
let cmp_code = self.emit_expr(cmp)?;
|
|
8026
|
+
format!(
|
|
8027
|
+
"tishlang_runtime::array_sort(&__boxed, Some(&({})))",
|
|
8028
|
+
cmp_code
|
|
8029
|
+
)
|
|
8030
|
+
}
|
|
8031
|
+
} else {
|
|
8032
|
+
"tishlang_runtime::array_sort(&__boxed, None)".to_string()
|
|
8033
|
+
};
|
|
8034
|
+
return Ok(format!(
|
|
8035
|
+
"{{ let __boxed = Value::Array(VmRef::new({base_read}.iter().cloned().map(|__e| {to_v}).collect())); \
|
|
8036
|
+
let __sorted = {sort_call}; \
|
|
8037
|
+
if let Value::Array(__a) = &__sorted {{ {assign} = __a.borrow().iter().map(|__v| {from_v}).collect(); }} \
|
|
8038
|
+
__sorted }}"
|
|
8039
|
+
));
|
|
7251
8040
|
}
|
|
7252
8041
|
}
|
|
7253
8042
|
}
|
|
@@ -7872,6 +8661,41 @@ impl Codegen {
|
|
|
7872
8661
|
}
|
|
7873
8662
|
}
|
|
7874
8663
|
|
|
8664
|
+
// Typed externs (#perf): a call to a native import with a declared typed signature →
|
|
8665
|
+
// a DIRECT `<crate>::name_typed(coerced args)` call. No `Value` boxing of the args, no
|
|
8666
|
+
// `value_call` dispatch through the namespace object. Falls through to the boxed path
|
|
8667
|
+
// when the arg count/kind doesn't match (a dynamic/spread call stays correct).
|
|
8668
|
+
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
8669
|
+
if let Some(sig) = self.extern_fns.get(fname.as_ref()).cloned() {
|
|
8670
|
+
if args.len() == sig.params.len()
|
|
8671
|
+
&& args.iter().all(|a| matches!(a, CallArg::Expr(_)))
|
|
8672
|
+
{
|
|
8673
|
+
let mut argc: Vec<String> = Vec::with_capacity(args.len());
|
|
8674
|
+
for (a, ty) in args.iter().zip(sig.params.iter()) {
|
|
8675
|
+
if let CallArg::Expr(e) = a {
|
|
8676
|
+
argc.push(self.emit_coerced_native(e, ty)?);
|
|
8677
|
+
}
|
|
8678
|
+
}
|
|
8679
|
+
let call = format!(
|
|
8680
|
+
"{}::{}_typed({})",
|
|
8681
|
+
sig.crate_name,
|
|
8682
|
+
sig.symbol,
|
|
8683
|
+
argc.join(", ")
|
|
8684
|
+
);
|
|
8685
|
+
// This is a Value-producing context: a `void` extern yields `Null`; a
|
|
8686
|
+
// native return is boxed once at the boundary (still far cheaper than the
|
|
8687
|
+
// boxed-args `value_call`); a `Value`-returning extern passes through.
|
|
8688
|
+
return Ok(if sig.ret == RustType::Unit {
|
|
8689
|
+
format!("{{ {}; Value::Null }}", call)
|
|
8690
|
+
} else if sig.ret.is_native() {
|
|
8691
|
+
sig.ret.to_value_expr(&call)
|
|
8692
|
+
} else {
|
|
8693
|
+
call
|
|
8694
|
+
});
|
|
8695
|
+
}
|
|
8696
|
+
}
|
|
8697
|
+
}
|
|
8698
|
+
|
|
7875
8699
|
// M5: boxed-context call to an eligible native fn → `Value::Number(name_native(..))`.
|
|
7876
8700
|
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
7877
8701
|
if self.native_fns.contains(fname.as_ref()) {
|
|
@@ -7950,17 +8774,25 @@ impl Codegen {
|
|
|
7950
8774
|
fields.iter().find(|(k, _)| k.as_ref() == prop_name.as_ref())
|
|
7951
8775
|
{
|
|
7952
8776
|
let var_esc = Self::escape_ident(var_name.as_ref()).into_owned();
|
|
8777
|
+
// Clone a non-Copy field so reading it doesn't move out of the local
|
|
8778
|
+
// (E0382 when the struct is used again); a Copy scalar reads by value.
|
|
8779
|
+
let clone = if field_ty.is_copy() { "" } else { ".clone()" };
|
|
7953
8780
|
let access = if self.refcell_wrapped_vars.contains(var_name.as_ref()) {
|
|
8781
|
+
// #567: scope the borrow to its own block so the lock drops
|
|
8782
|
+
// immediately — a `send-values` `Arc<Mutex>` VmRef is NOT
|
|
8783
|
+
// reentrant, so two `.borrow()`s of the same cell alive at once
|
|
8784
|
+
// (e.g. `s.a + s.b` in one expression) would deadlock.
|
|
7954
8785
|
format!(
|
|
7955
|
-
"
|
|
8786
|
+
"{{ let __bg = {}.borrow(); (*__bg).{}.clone() }}",
|
|
7956
8787
|
var_esc,
|
|
7957
8788
|
crate::types::field_ident(prop_name.as_ref())
|
|
7958
8789
|
)
|
|
7959
8790
|
} else {
|
|
7960
8791
|
format!(
|
|
7961
|
-
"{}.{}",
|
|
8792
|
+
"{}.{}{}",
|
|
7962
8793
|
var_esc,
|
|
7963
|
-
crate::types::field_ident(prop_name.as_ref())
|
|
8794
|
+
crate::types::field_ident(prop_name.as_ref()),
|
|
8795
|
+
clone
|
|
7964
8796
|
)
|
|
7965
8797
|
};
|
|
7966
8798
|
// Caller expects a `Value`; wrap.
|
|
@@ -7972,24 +8804,90 @@ impl Codegen {
|
|
|
7972
8804
|
// Generalize the typed struct-field fast path to `xs[i].field` (array-of-structs):
|
|
7973
8805
|
// when `object` indexes a `Vec<Named>`, do native struct field access.
|
|
7974
8806
|
if !optional {
|
|
7975
|
-
if let (
|
|
7976
|
-
|
|
8807
|
+
if let (
|
|
8808
|
+
Expr::Index { object: io, index: ii, .. },
|
|
8809
|
+
MemberProp::Name { name: prop_name, .. },
|
|
8810
|
+
) = (object.as_ref(), prop)
|
|
7977
8811
|
{
|
|
8812
|
+
// A captured (refcell-wrapped) `Vec<Named>` element field read: emit the field
|
|
8813
|
+
// access DIRECTLY on the borrowed element (`(*arr.borrow())[i].field`) — no whole
|
|
8814
|
+
// struct clone, and no whole-Vec boxing. This is the hot-loop `states[id].timer`
|
|
8815
|
+
// case (tish#558 read side). A non-Copy field is still cloned out of the borrow.
|
|
8816
|
+
if let Expr::Ident { name: arr_name, .. } = io.as_ref() {
|
|
8817
|
+
if self.refcell_wrapped_vars.contains(arr_name.as_ref()) {
|
|
8818
|
+
if let RustType::Vec(elem) =
|
|
8819
|
+
self.type_context.get_type(arr_name.as_ref())
|
|
8820
|
+
{
|
|
8821
|
+
if let RustType::Named { fields, .. } = elem.as_ref() {
|
|
8822
|
+
if let Some((_, field_ty)) = fields
|
|
8823
|
+
.iter()
|
|
8824
|
+
.find(|(k, _)| k.as_ref() == prop_name.as_ref())
|
|
8825
|
+
{
|
|
8826
|
+
let field_ty = field_ty.clone();
|
|
8827
|
+
let idx = self.emit_index_usize(ii)?;
|
|
8828
|
+
let esc = Self::escape_ident(arr_name.as_ref());
|
|
8829
|
+
let clone = if field_ty.is_copy() { "" } else { ".clone()" };
|
|
8830
|
+
// #567: bind the index (it may read the same cell, e.g.
|
|
8831
|
+
// `arr[arr.length-1]`) BEFORE taking the borrow, and scope
|
|
8832
|
+
// the borrow to its own block so the non-reentrant
|
|
8833
|
+
// `Arc<Mutex>` lock drops before any sibling read of `arr`
|
|
8834
|
+
// (`arr[i].a + arr[j].b`) — otherwise overlapping borrows
|
|
8835
|
+
// deadlock.
|
|
8836
|
+
let access = format!(
|
|
8837
|
+
"{{ let __bi = {}; let __bg = {}.borrow(); (*__bg)[__bi].{}{} }}",
|
|
8838
|
+
idx,
|
|
8839
|
+
esc,
|
|
8840
|
+
crate::types::field_ident(prop_name.as_ref()),
|
|
8841
|
+
clone
|
|
8842
|
+
);
|
|
8843
|
+
return Ok(field_ty.to_value_expr(&access));
|
|
8844
|
+
}
|
|
8845
|
+
}
|
|
8846
|
+
}
|
|
8847
|
+
}
|
|
8848
|
+
}
|
|
7978
8849
|
let (obj_code, obj_ty) = self.emit_typed_expr(object)?;
|
|
7979
8850
|
if let RustType::Named { fields, .. } = &obj_ty {
|
|
7980
8851
|
if let Some((_, field_ty)) =
|
|
7981
8852
|
fields.iter().find(|(k, _)| k.as_ref() == prop_name.as_ref())
|
|
7982
8853
|
{
|
|
8854
|
+
// The element `xs[i]` is BORROWED, so a non-Copy field (a `Value`/
|
|
8855
|
+
// `String`) must be cloned out — otherwise reading it in a value/return
|
|
8856
|
+
// position moves out of the index (E0507). A Copy scalar reads by value.
|
|
8857
|
+
let clone = if field_ty.is_copy() { "" } else { ".clone()" };
|
|
7983
8858
|
let access = format!(
|
|
7984
|
-
"({}).{}",
|
|
8859
|
+
"({}).{}{}",
|
|
7985
8860
|
obj_code,
|
|
7986
|
-
crate::types::field_ident(prop_name.as_ref())
|
|
8861
|
+
crate::types::field_ident(prop_name.as_ref()),
|
|
8862
|
+
clone
|
|
7987
8863
|
);
|
|
7988
8864
|
return Ok(field_ty.to_value_expr(&access));
|
|
7989
8865
|
}
|
|
7990
8866
|
}
|
|
7991
8867
|
}
|
|
7992
8868
|
}
|
|
8869
|
+
// `arr.length` on a native `Vec` (wrapped or not) → a cheap native `len()` boxed to a
|
|
8870
|
+
// `Number`, instead of boxing the WHOLE Vec to a `Value::Array` just to `get_prop`
|
|
8871
|
+
// its length (O(n) per read — the hot `states.length`-per-frame footgun, tish#562).
|
|
8872
|
+
if !*optional {
|
|
8873
|
+
if let MemberProp::Name { name: p, .. } = prop {
|
|
8874
|
+
if p.as_ref() == "length" {
|
|
8875
|
+
if let Expr::Ident { name, .. } = object.as_ref() {
|
|
8876
|
+
if let RustType::Vec(_) = self.type_context.get_type(name.as_ref()) {
|
|
8877
|
+
let esc = Self::escape_ident(name.as_ref());
|
|
8878
|
+
// #567: scope the borrow (non-reentrant `Arc<Mutex>` VmRef) so it
|
|
8879
|
+
// can't overlap a sibling read of the same cell in one expression.
|
|
8880
|
+
let len = if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
8881
|
+
format!("{{ let __bg = {}.borrow(); __bg.len() }}", esc)
|
|
8882
|
+
} else {
|
|
8883
|
+
format!("{}.len()", esc)
|
|
8884
|
+
};
|
|
8885
|
+
return Ok(format!("Value::Number({} as f64)", len));
|
|
8886
|
+
}
|
|
8887
|
+
}
|
|
8888
|
+
}
|
|
8889
|
+
}
|
|
8890
|
+
}
|
|
7993
8891
|
let obj = self.emit_expr(object)?;
|
|
7994
8892
|
// A literal (non-computed) key other than `size` is eligible for a per-site
|
|
7995
8893
|
// polymorphic inline cache (#179). `size` is excluded because `get_prop` special-cases
|
|
@@ -8148,9 +9046,58 @@ impl Codegen {
|
|
|
8148
9046
|
}
|
|
8149
9047
|
}
|
|
8150
9048
|
ObjectProp::Spread(e) => {
|
|
8151
|
-
|
|
9049
|
+
// Peek the source's static type ONCE. On GBA a statically-known struct
|
|
9050
|
+
// spreads NATIVELY (fields read straight off the Rust struct → the source
|
|
9051
|
+
// is never boxed to a `Value::Struct` / hashmap). `to_value_expr` re-boxes
|
|
9052
|
+
// a native scalar source so the boxed arms still receive a `Value`
|
|
9053
|
+
// (`{ ...5 }` stays sound). Desktop is deliberately untouched — a struct
|
|
9054
|
+
// source there boxes exactly as before (byte-identical emit, so the hot
|
|
9055
|
+
// object_spread bench keeps its clone-shape path).
|
|
9056
|
+
let gba = self.emit_mode == crate::NativeEmitMode::Gba;
|
|
9057
|
+
let (typed_code, typed_ty) = self.emit_typed_expr(e)?;
|
|
9058
|
+
if gba {
|
|
9059
|
+
if let crate::types::RustType::Named { fields, .. } = &typed_ty {
|
|
9060
|
+
let mut ins = String::new();
|
|
9061
|
+
for (k, fty) in fields {
|
|
9062
|
+
let key = self.cached_object_key(k.as_ref());
|
|
9063
|
+
ins.push_str(&format!(
|
|
9064
|
+
"_obj.insert({}, {}); ",
|
|
9065
|
+
key,
|
|
9066
|
+
Self::struct_field_to_value("_s", k, fty)
|
|
9067
|
+
));
|
|
9068
|
+
}
|
|
9069
|
+
let block = format!("{{ let _s = &({}); {}}}", typed_code, ins);
|
|
9070
|
+
if i == 0 {
|
|
9071
|
+
init = format!(
|
|
9072
|
+
"let mut _obj: PropMap = PropMap::with_capacity({}); {}",
|
|
9073
|
+
literal_keys + fields.len(),
|
|
9074
|
+
block
|
|
9075
|
+
);
|
|
9076
|
+
} else {
|
|
9077
|
+
// Later struct fields override earlier keys (JS order).
|
|
9078
|
+
parts.push(block);
|
|
9079
|
+
}
|
|
9080
|
+
continue;
|
|
9081
|
+
}
|
|
9082
|
+
}
|
|
9083
|
+
let val = if typed_ty.is_native() {
|
|
9084
|
+
typed_ty.to_value_expr(&typed_code)
|
|
9085
|
+
} else {
|
|
9086
|
+
typed_code
|
|
9087
|
+
};
|
|
9088
|
+
// GBA (non-struct source, e.g. a struct behind a `Value` param): route
|
|
9089
|
+
// through the facade's `object_spread_props`, which materialises a
|
|
9090
|
+
// `Value::Struct` to its object first. Desktop keeps the inline `if let
|
|
9091
|
+
// Value::Object` fast path untouched (no struct variant exists there, and
|
|
9092
|
+
// it's the hot object_spread bench).
|
|
8152
9093
|
if i == 0 {
|
|
8153
|
-
init =
|
|
9094
|
+
init = if gba {
|
|
9095
|
+
format!("let mut _obj: PropMap = tishlang_runtime::object_spread_props(&({})).unwrap_or_else(|| PropMap::with_capacity({}));", val, literal_keys)
|
|
9096
|
+
} else {
|
|
9097
|
+
format!("let mut _obj: PropMap = if let Value::Object(ref _spread) = {} {{ _spread.borrow().strings.clone() }} else {{ PropMap::with_capacity({}) }};", val, literal_keys)
|
|
9098
|
+
};
|
|
9099
|
+
} else if gba {
|
|
9100
|
+
parts.push(format!("if let Some(_spread) = tishlang_runtime::object_spread_props(&({})) {{ _obj.merge_from(&_spread); }}", val));
|
|
8154
9101
|
} else {
|
|
8155
9102
|
// `merge_from` reserves for the source's len, then inserts in one
|
|
8156
9103
|
// pass (later props still override earlier keys, matching JS order).
|
|
@@ -8561,6 +9508,13 @@ impl Codegen {
|
|
|
8561
9508
|
format!("{{ if {} {{ {} }} else {{ {} }} }}", cond, assign_and_return, else_expr)
|
|
8562
9509
|
}
|
|
8563
9510
|
Expr::MemberAssign { object, prop, value, .. } => {
|
|
9511
|
+
// #558: a native struct-element / struct-local field store (yielding the assigned
|
|
9512
|
+
// value) instead of set_prop on a throwaway copy that discards the write.
|
|
9513
|
+
if let Some(code) =
|
|
9514
|
+
self.try_emit_native_struct_field_assign(object, prop.as_ref(), value, false)?
|
|
9515
|
+
{
|
|
9516
|
+
return Ok(code);
|
|
9517
|
+
}
|
|
8564
9518
|
let obj = self.emit_expr(object)?;
|
|
8565
9519
|
let val = self.emit_expr(value)?;
|
|
8566
9520
|
format!(
|
|
@@ -9249,6 +10203,17 @@ impl Codegen {
|
|
|
9249
10203
|
None
|
|
9250
10204
|
}
|
|
9251
10205
|
|
|
10206
|
+
/// The root variable of a member/index chain (`a.b.c` → `a`, `a[i].b` → `a`, `a` → `a`), or
|
|
10207
|
+
/// `None` when the base isn't a plain identifier (e.g. a call result `f().x`).
|
|
10208
|
+
fn root_ident_of(expr: &Expr) -> Option<&str> {
|
|
10209
|
+
match expr {
|
|
10210
|
+
Expr::Ident { name, .. } => Some(name.as_ref()),
|
|
10211
|
+
Expr::Member { object, .. } => Self::root_ident_of(object),
|
|
10212
|
+
Expr::Index { object, .. } => Self::root_ident_of(object),
|
|
10213
|
+
_ => None,
|
|
10214
|
+
}
|
|
10215
|
+
}
|
|
10216
|
+
|
|
9252
10217
|
fn collect_assigned_idents_in_expr(expr: &Expr, names: &mut HashSet<String>) {
|
|
9253
10218
|
match expr {
|
|
9254
10219
|
Expr::Assign { name, value, .. } => {
|
|
@@ -9270,6 +10235,12 @@ impl Codegen {
|
|
|
9270
10235
|
names.insert(name.to_string());
|
|
9271
10236
|
}
|
|
9272
10237
|
Expr::MemberAssign { object, value, .. } => {
|
|
10238
|
+
// Mutating a field (`x.f = v`, `a.b.c = v`) mutates the ROOT variable — record it so a
|
|
10239
|
+
// captured struct/object is wrapped mutably; otherwise the native field store lands on
|
|
10240
|
+
// an immutable `let` binding (E0594).
|
|
10241
|
+
if let Some(root) = Self::root_ident_of(object) {
|
|
10242
|
+
names.insert(root.to_string());
|
|
10243
|
+
}
|
|
9273
10244
|
Self::collect_assigned_idents_in_expr(object, names);
|
|
9274
10245
|
Self::collect_assigned_idents_in_expr(value, names);
|
|
9275
10246
|
}
|
|
@@ -9279,6 +10250,9 @@ impl Codegen {
|
|
|
9279
10250
|
value,
|
|
9280
10251
|
..
|
|
9281
10252
|
} => {
|
|
10253
|
+
if let Some(root) = Self::root_ident_of(object) {
|
|
10254
|
+
names.insert(root.to_string());
|
|
10255
|
+
}
|
|
9282
10256
|
Self::collect_assigned_idents_in_expr(object, names);
|
|
9283
10257
|
Self::collect_assigned_idents_in_expr(index, names);
|
|
9284
10258
|
Self::collect_assigned_idents_in_expr(value, names);
|
|
@@ -9290,6 +10264,29 @@ impl Codegen {
|
|
|
9290
10264
|
Expr::Unary { operand, .. } => Self::collect_assigned_idents_in_expr(operand, names),
|
|
9291
10265
|
Expr::Delete { target, .. } => Self::collect_assigned_idents_in_expr(target, names),
|
|
9292
10266
|
Expr::Call { callee, args, .. } => {
|
|
10267
|
+
// An in-place mutating array method (`a.push(x)`, `a.pop()`, `a.reverse()`, …) mutates
|
|
10268
|
+
// the ROOT variable — record it so a captured native Vec is wrapped for shared mutation.
|
|
10269
|
+
// WITHOUT this, the mutation inside a closure lands on a by-value / immutable-`let`
|
|
10270
|
+
// captured Vec (E0596), and the boxed fallback would DISCARD it (and E0282 on a
|
|
10271
|
+
// `Vec<struct>`, whose element type the skipped native op never pinned). Every listed
|
|
10272
|
+
// method has a native fast path that handles the wrapped receiver (a `(*a.borrow_mut())`
|
|
10273
|
+
// access, or a `borrow_mut` guard for the multi-access `splice`/`fill`/`copyWithin`).
|
|
10274
|
+
if let Expr::Member {
|
|
10275
|
+
object,
|
|
10276
|
+
prop: MemberProp::Name { name: m, .. },
|
|
10277
|
+
..
|
|
10278
|
+
} = callee.as_ref()
|
|
10279
|
+
{
|
|
10280
|
+
if matches!(
|
|
10281
|
+
m.as_ref(),
|
|
10282
|
+
"push" | "pop" | "shift" | "unshift" | "reverse" | "sort" | "splice"
|
|
10283
|
+
| "fill" | "copyWithin"
|
|
10284
|
+
) {
|
|
10285
|
+
if let Some(root) = Self::root_ident_of(object) {
|
|
10286
|
+
names.insert(root.to_string());
|
|
10287
|
+
}
|
|
10288
|
+
}
|
|
10289
|
+
}
|
|
9293
10290
|
Self::collect_assigned_idents_in_expr(callee, names);
|
|
9294
10291
|
for arg in args {
|
|
9295
10292
|
match arg {
|
|
@@ -10619,6 +11616,15 @@ impl Codegen {
|
|
|
10619
11616
|
for v in &implicit_env_captures {
|
|
10620
11617
|
self.refcell_wrapped_vars.remove(v);
|
|
10621
11618
|
}
|
|
11619
|
+
// tishlang/tish#556: an arrow PARAMETER shadows any outer var of the same name — inside the
|
|
11620
|
+
// body it is a plain `Value`/native binding, not the outer `VmRef` cell, so drop such names
|
|
11621
|
+
// from the wrapped set for the body (restored via `saved_refcell_vars`). Without this, a
|
|
11622
|
+
// reference to the param compiles as `vm_read(¶m)` — a `&Value` where a `&VmRef` is wanted.
|
|
11623
|
+
for p in params {
|
|
11624
|
+
for bn in p.bound_names() {
|
|
11625
|
+
self.refcell_wrapped_vars.remove(bn.as_ref());
|
|
11626
|
+
}
|
|
11627
|
+
}
|
|
10622
11628
|
|
|
10623
11629
|
self.type_context.push_fun_param_scope(params, None);
|
|
10624
11630
|
|
|
@@ -10709,11 +11715,15 @@ impl Codegen {
|
|
|
10709
11715
|
{
|
|
10710
11716
|
if p.as_ref() == "length" {
|
|
10711
11717
|
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
|
-
{
|
|
11718
|
+
if matches!(self.type_context.get_type(arr.as_ref()), RustType::Vec(_)) {
|
|
10715
11719
|
let esc = Self::escape_ident(arr.as_ref());
|
|
10716
|
-
|
|
11720
|
+
// A captured Vec reads its length through the cell (`(*a.borrow()).len()`).
|
|
11721
|
+
let code = if self.refcell_wrapped_vars.contains(arr.as_ref()) {
|
|
11722
|
+
format!("{{ let __bg = {}.borrow(); __bg.len() as f64 }}", esc)
|
|
11723
|
+
} else {
|
|
11724
|
+
format!("{}.len() as f64", esc)
|
|
11725
|
+
};
|
|
11726
|
+
return Ok(format!("({})", code));
|
|
10717
11727
|
}
|
|
10718
11728
|
}
|
|
10719
11729
|
}
|
|
@@ -10938,6 +11948,17 @@ impl Codegen {
|
|
|
10938
11948
|
if *target_type == RustType::F64 && typed_ty.is_integer_scalar() {
|
|
10939
11949
|
return Ok(format!("({}) as f64", typed_code));
|
|
10940
11950
|
}
|
|
11951
|
+
// `fixed` target from a native integer / f64 source: lift to `Fixed` directly, no
|
|
11952
|
+
// Value round-trip. Lets `let dx: fixed = input_x()` (i32→fixed) or `let v: fixed = n`
|
|
11953
|
+
// stay native so downstream `dx * spd` is `Fixed*Fixed` instead of boxing.
|
|
11954
|
+
if *target_type == RustType::Fixed {
|
|
11955
|
+
if typed_ty.is_integer_scalar() {
|
|
11956
|
+
return Ok(format!("tishlang_runtime::Fixed::from_raw((({}) as i32) * 256)", typed_code));
|
|
11957
|
+
}
|
|
11958
|
+
if typed_ty == RustType::F64 {
|
|
11959
|
+
return Ok(format!("tishlang_runtime::Fixed::from_raw((({}) * 256.0) as i32)", typed_code));
|
|
11960
|
+
}
|
|
11961
|
+
}
|
|
10941
11962
|
}
|
|
10942
11963
|
|
|
10943
11964
|
// Fall back to emit_expr + conversion
|
|
@@ -12250,50 +13271,19 @@ impl Codegen {
|
|
|
12250
13271
|
let Expr::Ident { name, .. } = object else {
|
|
12251
13272
|
return Ok(None);
|
|
12252
13273
|
};
|
|
12253
|
-
if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
12254
|
-
return Ok(None);
|
|
12255
|
-
}
|
|
12256
13274
|
let obj_type = self.type_context.get_type(name.as_ref());
|
|
12257
13275
|
let RustType::Vec(elem_type) = obj_type else {
|
|
12258
13276
|
return Ok(None);
|
|
12259
13277
|
};
|
|
13278
|
+
// #564: a closure-captured (refcell-wrapped) native Vec is handled below via a `borrow_mut`
|
|
13279
|
+
// store. WITHOUT this it fell to the generic boxed `set_index` fallback, which mutates a
|
|
13280
|
+
// THROWAWAY boxed copy of the Vec (write silently DISCARDED) — and for a compound `a[i] += x`
|
|
13281
|
+
// (desugared to `a[i] = a[i] + x`) the fallback's `get_index(&a) … set_index(&a …)` takes two
|
|
13282
|
+
// overlapping locks on the non-reentrant `Arc<Mutex>` VmRef and DEADLOCKS the binary.
|
|
13283
|
+
let wrapped = self.refcell_wrapped_vars.contains(name.as_ref());
|
|
12260
13284
|
let esc_obj = Self::escape_ident(name.as_ref()).into_owned();
|
|
12261
13285
|
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
|
-
};
|
|
13286
|
+
let idx_usize = self.emit_index_usize(index)?;
|
|
12297
13287
|
let native_val = if *elem_type == RustType::I32 {
|
|
12298
13288
|
if let Expr::Binary {
|
|
12299
13289
|
left,
|
|
@@ -12342,6 +13332,34 @@ impl Codegen {
|
|
|
12342
13332
|
val_code
|
|
12343
13333
|
}
|
|
12344
13334
|
};
|
|
13335
|
+
// #564: wrapped receiver — bind the index and RHS to temps BEFORE taking the `borrow_mut`,
|
|
13336
|
+
// so a RHS that reads the same cell (`a[i] += x`, `a[i] = a[j]`) releases its shared borrow
|
|
13337
|
+
// first (no overlapping lock → no deadlock), then store into the real element through the cell.
|
|
13338
|
+
if wrapped {
|
|
13339
|
+
let needs_resize =
|
|
13340
|
+
matches!(elem_type.as_ref(), RustType::F64 | RustType::Bool) && !in_bounds;
|
|
13341
|
+
let store = if needs_resize {
|
|
13342
|
+
let pad = if matches!(elem_type.as_ref(), RustType::F64) {
|
|
13343
|
+
"f64::NAN"
|
|
13344
|
+
} else {
|
|
13345
|
+
"false"
|
|
13346
|
+
};
|
|
13347
|
+
format!(
|
|
13348
|
+
"let __i = {idx}; let __v = {val}; let mut __g = {esc}.borrow_mut(); if __i >= __g.len() {{ __g.resize(__i + 1, {pad}); }} __g[__i] = __v;",
|
|
13349
|
+
idx = idx_usize, val = native_val, esc = esc_obj, pad = pad
|
|
13350
|
+
)
|
|
13351
|
+
} else {
|
|
13352
|
+
format!(
|
|
13353
|
+
"let __i = {idx}; let __v = {val}; (*{esc}.borrow_mut())[__i] = __v;",
|
|
13354
|
+
idx = idx_usize, val = native_val, esc = esc_obj
|
|
13355
|
+
)
|
|
13356
|
+
};
|
|
13357
|
+
return Ok(Some(if side_effect_only {
|
|
13358
|
+
format!("{{ {} }}", store)
|
|
13359
|
+
} else {
|
|
13360
|
+
format!("{{ {} Value::Null }}", store)
|
|
13361
|
+
}));
|
|
13362
|
+
}
|
|
12345
13363
|
let assign = match elem_type.as_ref() {
|
|
12346
13364
|
RustType::F64 | RustType::Bool | RustType::I32 if in_bounds => {
|
|
12347
13365
|
if side_effect_only {
|
|
@@ -12369,6 +13387,130 @@ impl Codegen {
|
|
|
12369
13387
|
Ok(Some(assign))
|
|
12370
13388
|
}
|
|
12371
13389
|
|
|
13390
|
+
/// Emit an array/element index expression as a Rust `usize`. A substituted loop counter reuses
|
|
13391
|
+
/// its `usize` binding; a numeric index casts (`(idx) as usize`); anything else unwraps a
|
|
13392
|
+
/// `Value::Number` at runtime. Shared by the native `Vec` index-assign and struct-field-assign
|
|
13393
|
+
/// lowerings so they agree on index handling.
|
|
13394
|
+
fn emit_index_usize(&mut self, index: &Expr) -> Result<String, CompileError> {
|
|
13395
|
+
if let Expr::Ident { name: idx_name, .. } = index {
|
|
13396
|
+
if let Some(uv) = self.usize_var_subst.get(idx_name.as_ref()) {
|
|
13397
|
+
return Ok(uv.clone());
|
|
13398
|
+
}
|
|
13399
|
+
}
|
|
13400
|
+
let (idx_code, idx_ty) = self.emit_typed_expr(index)?;
|
|
13401
|
+
if idx_ty == RustType::F64 || idx_ty == RustType::I32 {
|
|
13402
|
+
Ok(format!("({}) as usize", idx_code))
|
|
13403
|
+
} else {
|
|
13404
|
+
let iv = if idx_ty.is_native() {
|
|
13405
|
+
idx_ty.to_value_expr(&idx_code)
|
|
13406
|
+
} else {
|
|
13407
|
+
idx_code
|
|
13408
|
+
};
|
|
13409
|
+
Ok(format!(
|
|
13410
|
+
"{{ let _i = &{}; if let Value::Number(n) = _i {{ *n as usize }} else {{ panic!(\"array index must be a number\") }} }}",
|
|
13411
|
+
iv
|
|
13412
|
+
))
|
|
13413
|
+
}
|
|
13414
|
+
}
|
|
13415
|
+
|
|
13416
|
+
/// tishlang/tish#558 — `place.field = value` where `place` is an addressable STRUCT: an element
|
|
13417
|
+
/// of a typed struct `Vec` (`arr[i].field`) or a struct-typed local (`s.field`). Lowers to a
|
|
13418
|
+
/// NATIVE field store into the real element/local, coercing `value` to the field's type. Without
|
|
13419
|
+
/// this, the generic member-assign path emits `set_prop` on a throwaway `Value::object_from_pairs`
|
|
13420
|
+
/// COPY of the element — mutating the copy and silently discarding the write. Returns `None` for
|
|
13421
|
+
/// anything that isn't a known struct place (the caller falls back to the dynamic `set_prop`).
|
|
13422
|
+
fn try_emit_native_struct_field_assign(
|
|
13423
|
+
&mut self,
|
|
13424
|
+
object: &Expr,
|
|
13425
|
+
prop: &str,
|
|
13426
|
+
value: &Expr,
|
|
13427
|
+
side_effect_only: bool,
|
|
13428
|
+
) -> Result<Option<String>, CompileError> {
|
|
13429
|
+
// `prelude` binds any index temp BEFORE a `borrow_mut()` place is taken (so the index may
|
|
13430
|
+
// itself read the array — `arr[arr.length-1]` — without a RefCell double-borrow); it is empty
|
|
13431
|
+
// for the non-wrapped cases. `place` is the addressable struct expression the field stores into.
|
|
13432
|
+
let (prelude, place, fields) = match object {
|
|
13433
|
+
// `arr[i].field = v` — a typed struct Vec element. A captured (refcell-wrapped) array
|
|
13434
|
+
// writes through `(*arr.borrow_mut())[i]` so the store lands natively on the shared cell;
|
|
13435
|
+
// WITHOUT this it fell to the generic member-assign, which boxed the WHOLE Vec into a
|
|
13436
|
+
// throwaway `Value::Array` (cloning + `from_struct`-boxing every element) and `set_prop`'d
|
|
13437
|
+
// the discarded copy — O(n) per write, the write lost, and an E0282 on the boxing closure.
|
|
13438
|
+
Expr::Index { object: io, index, .. } => {
|
|
13439
|
+
let Expr::Ident { name, .. } = io.as_ref() else {
|
|
13440
|
+
return Ok(None);
|
|
13441
|
+
};
|
|
13442
|
+
let RustType::Vec(elem) = self.type_context.get_type(name.as_ref()) else {
|
|
13443
|
+
return Ok(None);
|
|
13444
|
+
};
|
|
13445
|
+
let RustType::Named { fields, .. } = elem.as_ref() else {
|
|
13446
|
+
return Ok(None);
|
|
13447
|
+
};
|
|
13448
|
+
let fields = fields.clone();
|
|
13449
|
+
let idx_usize = self.emit_index_usize(index)?;
|
|
13450
|
+
let esc = Self::escape_ident(name.as_ref());
|
|
13451
|
+
if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
13452
|
+
(
|
|
13453
|
+
format!("let _i = {}; ", idx_usize),
|
|
13454
|
+
format!("(*{}.borrow_mut())[_i]", esc),
|
|
13455
|
+
fields,
|
|
13456
|
+
)
|
|
13457
|
+
} else {
|
|
13458
|
+
(String::new(), format!("{}[{}]", esc, idx_usize), fields)
|
|
13459
|
+
}
|
|
13460
|
+
}
|
|
13461
|
+
// `s.field = v` — a struct-typed local/var. A captured (refcell-wrapped) struct writes
|
|
13462
|
+
// through `(*s.borrow_mut())` so the field store lands natively on the shared cell instead
|
|
13463
|
+
// of falling to the boxed `set_prop(object_from_pairs(..))` path (which also wouldn't
|
|
13464
|
+
// persist — it built a throwaway object).
|
|
13465
|
+
Expr::Ident { name, .. } => {
|
|
13466
|
+
let RustType::Named { fields, .. } = self.type_context.get_type(name.as_ref()) else {
|
|
13467
|
+
return Ok(None);
|
|
13468
|
+
};
|
|
13469
|
+
let place = if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
13470
|
+
format!("(*{}.borrow_mut())", Self::escape_ident(name.as_ref()))
|
|
13471
|
+
} else {
|
|
13472
|
+
Self::escape_ident(name.as_ref()).into_owned()
|
|
13473
|
+
};
|
|
13474
|
+
(String::new(), place, fields)
|
|
13475
|
+
}
|
|
13476
|
+
_ => return Ok(None),
|
|
13477
|
+
};
|
|
13478
|
+
// The property must be a real field of the struct; its type drives the RHS coercion.
|
|
13479
|
+
let Some((_, field_ty)) = fields.iter().find(|(k, _)| k.as_ref() == prop) else {
|
|
13480
|
+
return Ok(None);
|
|
13481
|
+
};
|
|
13482
|
+
let field_ty = field_ty.clone();
|
|
13483
|
+
let field = crate::types::field_ident(prop);
|
|
13484
|
+
let coerced = self.emit_coerced_native(value, &field_ty)?;
|
|
13485
|
+
// Bind the RHS to a temp first so a `borrow_mut()` place can't alias a `borrow()` in the RHS
|
|
13486
|
+
// (`s.a = s.b + 1`) — evaluate the read before taking the mutable borrow. Any index prelude
|
|
13487
|
+
// runs first, so its evaluation also predates the borrow.
|
|
13488
|
+
let via_temp = !prelude.is_empty() || place.contains(".borrow_mut()");
|
|
13489
|
+
if side_effect_only {
|
|
13490
|
+
if via_temp {
|
|
13491
|
+
Ok(Some(format!(
|
|
13492
|
+
"{{ {}let _v = {}; {}.{} = _v; }}",
|
|
13493
|
+
prelude, coerced, place, field
|
|
13494
|
+
)))
|
|
13495
|
+
} else {
|
|
13496
|
+
Ok(Some(format!("{}.{} = {}", place, field, coerced)))
|
|
13497
|
+
}
|
|
13498
|
+
} else {
|
|
13499
|
+
// Assignment-as-expression yields the assigned value (JS `a.b = c` ⇒ c). Bind once (the
|
|
13500
|
+
// RHS may have effects and the place may re-evaluate an index), store, then hand back a
|
|
13501
|
+
// `Value` of it — box a native field, pass a `Value` field through.
|
|
13502
|
+
let ret = if field_ty.is_native() {
|
|
13503
|
+
field_ty.to_value_expr("_v")
|
|
13504
|
+
} else {
|
|
13505
|
+
"_v".to_string()
|
|
13506
|
+
};
|
|
13507
|
+
Ok(Some(format!(
|
|
13508
|
+
"{{ {}let _v = {}; {}.{} = _v.clone(); {} }}",
|
|
13509
|
+
prelude, coerced, place, field, ret
|
|
13510
|
+
)))
|
|
13511
|
+
}
|
|
13512
|
+
}
|
|
13513
|
+
|
|
12372
13514
|
/// Emit `value` as a native expression of type `target_ty`, coercing without a
|
|
12373
13515
|
/// `Value` round-trip where the conversion is a cheap numeric cast. Same-typed RHS
|
|
12374
13516
|
/// passes through; f64↔i32 casts directly; everything else boxes to `Value` then
|
|
@@ -13045,6 +14187,22 @@ impl Codegen {
|
|
|
13045
14187
|
/// native `Num<i32,8>` arithmetic instead of boxing the literal side through `f64`. Uses the
|
|
13046
14188
|
/// same truncating `(n * 256) as i32` conversion as the dynamic `fixed`↔`Value` boundary, so
|
|
13047
14189
|
/// the constant is bit-identical to the boxed path.
|
|
14190
|
+
/// A numeric literal that is EXACTLY a whole number inside `i32`'s range, as an `i32`. Used to keep
|
|
14191
|
+
/// `x > 0` / `i < 100` in integer registers when the other side is already an integer — a fraction
|
|
14192
|
+
/// or an out-of-range magnitude returns `None` and stays on the `f64` path, where it is correct.
|
|
14193
|
+
fn int_literal_of(e: &Expr) -> Option<i32> {
|
|
14194
|
+
if let Expr::Literal {
|
|
14195
|
+
value: Literal::Number(n),
|
|
14196
|
+
..
|
|
14197
|
+
} = e
|
|
14198
|
+
{
|
|
14199
|
+
if n.fract() == 0.0 && *n >= i32::MIN as f64 && *n <= i32::MAX as f64 {
|
|
14200
|
+
return Some(*n as i32);
|
|
14201
|
+
}
|
|
14202
|
+
}
|
|
14203
|
+
None
|
|
14204
|
+
}
|
|
14205
|
+
|
|
13048
14206
|
fn fixed_literal_of(e: &Expr) -> Option<String> {
|
|
13049
14207
|
if let Expr::Literal {
|
|
13050
14208
|
value: Literal::Number(n),
|
|
@@ -22097,6 +23255,23 @@ impl Codegen {
|
|
|
22097
23255
|
let (l, lt) = self.emit_typed_expr(left)?;
|
|
22098
23256
|
let (r, rt) = self.emit_typed_expr(right)?;
|
|
22099
23257
|
|
|
23258
|
+
// `fixed ⊕ <runtime integer>` (e.g. `t * speed` with `t: i32`, `speed: fixed`): LIFT the
|
|
23259
|
+
// integer operand to `Fixed` via `Num::new` so the whole op stays native `Num<i32,8>`
|
|
23260
|
+
// arithmetic. This runs BEFORE the generic integer→f64 coercion below — otherwise the
|
|
23261
|
+
// integer becomes f64 and `f64 ⊕ fixed` drops to the boxed `ops::*` path (that path is
|
|
23262
|
+
// the deliberate escape hatch for a GENUINE runtime f64, which is lossy against fixed;
|
|
23263
|
+
// a plain integer is exact, so lifting is sound). Integer *literals* are already folded
|
|
23264
|
+
// to a Fixed constant by `fixed_literal_of` further down; this covers runtime integers.
|
|
23265
|
+
// `Fixed<i32,8>` raw = value << 8 (× 256), so an integer `v` lifts to fixed `v.0` as
|
|
23266
|
+
// `Fixed::from_raw(v * 256)` — the same constructor tish emits everywhere for fixed.
|
|
23267
|
+
let (l, lt, r, rt) = if lt == RustType::Fixed && rt.is_integer_scalar() {
|
|
23268
|
+
(l, lt, format!("tishlang_runtime::Fixed::from_raw((({}) as i32) * 256)", r), RustType::Fixed)
|
|
23269
|
+
} else if rt == RustType::Fixed && lt.is_integer_scalar() {
|
|
23270
|
+
(format!("tishlang_runtime::Fixed::from_raw((({}) as i32) * 256)", l), RustType::Fixed, r, rt)
|
|
23271
|
+
} else {
|
|
23272
|
+
(l, lt, r, rt)
|
|
23273
|
+
};
|
|
23274
|
+
|
|
22100
23275
|
// An `I32` loop-accumulator (the i32-loop-var lowering) used in a NON-bitwise
|
|
22101
23276
|
// expression reads as its signed int32 value coerced to `f64` — every i32 is exact
|
|
22102
23277
|
// in f64. Bitwise/shift parents never see this: they recurse into the raw AST via
|
|
@@ -22106,6 +23281,83 @@ impl Codegen {
|
|
|
22106
23281
|
// A narrow int storage read (`u8` HP, `i16` tile x) promotes the same way — read
|
|
22107
23282
|
// as its exact `f64` value so arithmetic keeps JS Number semantics; the truncating
|
|
22108
23283
|
// store back to the field happens at the assignment site.
|
|
23284
|
+
// …but when BOTH sides are already integers, that detour is pure loss on a CPU with no
|
|
23285
|
+
// FPU. On ARM7TDMI every f64 op is a soft-float library CALL, so `i + 1` on a `: i32`
|
|
23286
|
+
// counter costs two int→float conversions, a call, and a float→int store. It is why
|
|
23287
|
+
// annotating a loop counter `: i32` measured SLOWER than leaving it a plain number
|
|
23288
|
+
// (examples/bench-ai) — the opposite of what an annotation is for.
|
|
23289
|
+
//
|
|
23290
|
+
// Keeping the pair in integer registers is not an approximation: every i32 is exact in
|
|
23291
|
+
// f64, so `+`, `-` and the relational operators give bit-identical answers in either
|
|
23292
|
+
// domain. Only the operators where the two domains genuinely DISAGREE stay on floats:
|
|
23293
|
+
// `/` JS division is real division — `5 / 2` is 2.5, not 2
|
|
23294
|
+
// `%` integer `%` panics on a zero divisor where JS yields NaN
|
|
23295
|
+
// `*` a product can leave i32's range while staying exact in f64, and the bitwise
|
|
23296
|
+
// lowering below deliberately relies on that headroom (`h * 16777619`)
|
|
23297
|
+
// Operands widen to `i32` first so mixed widths (a `u8` HP against an `i16` tile
|
|
23298
|
+
// coordinate) still agree with JS Number semantics. `u32` is excluded because it is the
|
|
23299
|
+
// one width that does not fit — reading 3 billion `as i32` would flip its sign.
|
|
23300
|
+
// The adds wrap rather than panic, which matches both JS (`to_int32` is modulo 2³²) and
|
|
23301
|
+
// the truncating `as` store every integer target already performs.
|
|
23302
|
+
let fits_i32 = |t: &RustType| {
|
|
23303
|
+
matches!(
|
|
23304
|
+
t,
|
|
23305
|
+
RustType::I32
|
|
23306
|
+
| RustType::I8
|
|
23307
|
+
| RustType::U8
|
|
23308
|
+
| RustType::I16
|
|
23309
|
+
| RustType::U16
|
|
23310
|
+
)
|
|
23311
|
+
};
|
|
23312
|
+
// An integer against a whole-number LITERAL is the shape most of this actually is —
|
|
23313
|
+
// `x > 0`, `i < 100`, `0 - dir`. The literal types as `f64`, which would drag the
|
|
23314
|
+
// integer side out of its register for a soft-float compare over two values that are
|
|
23315
|
+
// both plainly integers. Fold it into the integer domain instead. Only exact whole
|
|
23316
|
+
// numbers inside i32's range qualify, so `x < 2.5` and `x < 3e9` still go to floats,
|
|
23317
|
+
// where they belong.
|
|
23318
|
+
let (l, lt, r, rt) = if fits_i32(<) && rt == RustType::F64 {
|
|
23319
|
+
match Self::int_literal_of(right) {
|
|
23320
|
+
Some(v) => (l, lt, format!("{}i32", v), RustType::I32),
|
|
23321
|
+
None => (l, lt, r, rt),
|
|
23322
|
+
}
|
|
23323
|
+
} else if fits_i32(&rt) && lt == RustType::F64 {
|
|
23324
|
+
match Self::int_literal_of(left) {
|
|
23325
|
+
Some(v) => (format!("{}i32", v), RustType::I32, r, rt),
|
|
23326
|
+
None => (l, lt, r, rt),
|
|
23327
|
+
}
|
|
23328
|
+
} else {
|
|
23329
|
+
(l, lt, r, rt)
|
|
23330
|
+
};
|
|
23331
|
+
if fits_i32(<) && fits_i32(&rt) {
|
|
23332
|
+
let li = format!("(({}) as i32)", l);
|
|
23333
|
+
let ri = format!("(({}) as i32)", r);
|
|
23334
|
+
match op {
|
|
23335
|
+
BinOp::Add => {
|
|
23336
|
+
return Ok((
|
|
23337
|
+
format!("({}).wrapping_add({})", li, ri),
|
|
23338
|
+
RustType::I32,
|
|
23339
|
+
))
|
|
23340
|
+
}
|
|
23341
|
+
BinOp::Sub => {
|
|
23342
|
+
return Ok((
|
|
23343
|
+
format!("({}).wrapping_sub({})", li, ri),
|
|
23344
|
+
RustType::I32,
|
|
23345
|
+
))
|
|
23346
|
+
}
|
|
23347
|
+
BinOp::Lt => return Ok((format!("({} < {})", li, ri), RustType::Bool)),
|
|
23348
|
+
BinOp::Le => return Ok((format!("({} <= {})", li, ri), RustType::Bool)),
|
|
23349
|
+
BinOp::Gt => return Ok((format!("({} > {})", li, ri), RustType::Bool)),
|
|
23350
|
+
BinOp::Ge => return Ok((format!("({} >= {})", li, ri), RustType::Bool)),
|
|
23351
|
+
BinOp::StrictEq => {
|
|
23352
|
+
return Ok((format!("({} == {})", li, ri), RustType::Bool))
|
|
23353
|
+
}
|
|
23354
|
+
BinOp::StrictNe => {
|
|
23355
|
+
return Ok((format!("({} != {})", li, ri), RustType::Bool))
|
|
23356
|
+
}
|
|
23357
|
+
_ => {}
|
|
23358
|
+
}
|
|
23359
|
+
}
|
|
23360
|
+
|
|
22109
23361
|
let (l, lt) = if lt.is_integer_scalar() {
|
|
22110
23362
|
(format!("(({}) as f64)", l), RustType::F64)
|
|
22111
23363
|
} else {
|
|
@@ -22263,6 +23515,41 @@ impl Codegen {
|
|
|
22263
23515
|
}
|
|
22264
23516
|
}
|
|
22265
23517
|
|
|
23518
|
+
// GBA: `key in s` on a statically-known struct → answer NATIVELY against the field
|
|
23519
|
+
// names (no boxing of `s`, no hashmap). A string-literal key folds to a compile-time
|
|
23520
|
+
// bool; a string-typed key becomes a `matches!` over the field names (zero alloc). The
|
|
23521
|
+
// struct is still evaluated (bound to `_`) for its side effects. Any other key kind
|
|
23522
|
+
// falls through to the boxed runtime `in` below. Desktop is untouched.
|
|
23523
|
+
if *op == BinOp::In
|
|
23524
|
+
&& self.emit_mode == crate::NativeEmitMode::Gba
|
|
23525
|
+
{
|
|
23526
|
+
if let RustType::Named { fields, .. } = &rt {
|
|
23527
|
+
if let Expr::Literal { value: Literal::String(s), .. } = left.as_ref() {
|
|
23528
|
+
let present = fields.iter().any(|(k, _)| k.as_ref() == s.as_ref());
|
|
23529
|
+
return Ok((
|
|
23530
|
+
format!("{{ let _ = &({}); Value::Bool({}) }}", r, present),
|
|
23531
|
+
RustType::Value,
|
|
23532
|
+
));
|
|
23533
|
+
}
|
|
23534
|
+
if lt == RustType::String {
|
|
23535
|
+
let arms = fields
|
|
23536
|
+
.iter()
|
|
23537
|
+
.map(|(k, _)| format!("{:?}", k.as_ref()))
|
|
23538
|
+
.collect::<Vec<_>>()
|
|
23539
|
+
.join(" | ");
|
|
23540
|
+
// Bind the key BEFORE evaluating the struct — `key in obj` evaluates
|
|
23541
|
+
// left-to-right, so the key's side effects must run first.
|
|
23542
|
+
return Ok((
|
|
23543
|
+
format!(
|
|
23544
|
+
"{{ let _k = ({}); let _ = &({}); Value::Bool(matches!(_k.as_str(), {})) }}",
|
|
23545
|
+
l, r, arms
|
|
23546
|
+
),
|
|
23547
|
+
RustType::Value,
|
|
23548
|
+
));
|
|
23549
|
+
}
|
|
23550
|
+
}
|
|
23551
|
+
}
|
|
23552
|
+
|
|
22266
23553
|
// Fall back: convert both sides to Value and use the runtime.
|
|
22267
23554
|
let lv = Self::box_operand_for_runtime(left, &l, <);
|
|
22268
23555
|
let rv = Self::box_operand_for_runtime(right, &r, &rt);
|
|
@@ -22349,10 +23636,24 @@ impl Codegen {
|
|
|
22349
23636
|
// Native fast path: `vec[i]` where vec is Vec<T> and i is numeric.
|
|
22350
23637
|
if !optional {
|
|
22351
23638
|
if let Expr::Ident { name, .. } = object.as_ref() {
|
|
22352
|
-
|
|
23639
|
+
{
|
|
23640
|
+
let idx_wrapped = self.refcell_wrapped_vars.contains(name.as_ref());
|
|
22353
23641
|
let obj_type = self.type_context.get_type(name.as_ref());
|
|
22354
23642
|
if let RustType::Vec(elem_type) = &obj_type {
|
|
22355
|
-
|
|
23643
|
+
// A captured (refcell-wrapped) Vec reads through `(*a.borrow())` — WITHOUT
|
|
23644
|
+
// this it fell to the boxed `get_index`, which cloned + `from_struct`-boxed
|
|
23645
|
+
// the WHOLE Vec into a `Value::Array` just to read one element (O(n) per
|
|
23646
|
+
// read — the hot-loop `states[id].timer` footgun, tish#558 read side).
|
|
23647
|
+
let esc_raw = Self::escape_ident(name.as_ref()).into_owned();
|
|
23648
|
+
// #567: for a wrapped Vec the place is the block-local guard `__bg`
|
|
23649
|
+
// (bound below), so the non-reentrant `Arc<Mutex>` borrow is scoped to
|
|
23650
|
+
// this single read and can't overlap a sibling read of the same cell
|
|
23651
|
+
// (`a[i] + a[j]` in one expression), which would deadlock.
|
|
23652
|
+
let esc_obj = if idx_wrapped {
|
|
23653
|
+
"(*__bg)".to_string()
|
|
23654
|
+
} else {
|
|
23655
|
+
esc_raw.clone()
|
|
23656
|
+
};
|
|
22356
23657
|
// #173 part 3: prove the index in-bounds BEFORE emitting it.
|
|
22357
23658
|
let in_bounds = self.index_in_bounds(index, name.as_ref());
|
|
22358
23659
|
let idx_usize = if let Expr::Ident { name: idx_name, .. } =
|
|
@@ -22394,6 +23695,14 @@ impl Codegen {
|
|
|
22394
23695
|
)
|
|
22395
23696
|
}
|
|
22396
23697
|
};
|
|
23698
|
+
// #567: inside the wrapped block the index is pre-bound to `__bi`
|
|
23699
|
+
// (evaluated BEFORE the borrow, so an index that itself reads this cell
|
|
23700
|
+
// — `arr[arr.length-1]` — doesn't double-lock the non-reentrant cell).
|
|
23701
|
+
let idx_ref = if idx_wrapped {
|
|
23702
|
+
"__bi".to_string()
|
|
23703
|
+
} else {
|
|
23704
|
+
idx_usize.clone()
|
|
23705
|
+
};
|
|
22397
23706
|
let elem_ty = *elem_type.clone();
|
|
22398
23707
|
// OOB-safe read for numeric/bool Vecs: JS `arr[oob]` is `undefined`
|
|
22399
23708
|
// (→ NaN / false in those contexts), NOT a panic. In-bounds is the
|
|
@@ -22403,27 +23712,42 @@ impl Codegen {
|
|
|
22403
23712
|
// #173 part 3: a proven in-bounds read skips the `.get().unwrap_or`
|
|
22404
23713
|
// branch — a direct `a[i]` (the `idx < len` proof guarantees it).
|
|
22405
23714
|
RustType::F64 | RustType::Bool | RustType::I32 if in_bounds => {
|
|
22406
|
-
format!("{}[{}]", esc_obj,
|
|
23715
|
+
format!("{}[{}]", esc_obj, idx_ref)
|
|
22407
23716
|
}
|
|
22408
23717
|
RustType::F64 => format!(
|
|
22409
23718
|
"{}.get({}).copied().unwrap_or(f64::NAN)",
|
|
22410
|
-
esc_obj,
|
|
23719
|
+
esc_obj, idx_ref
|
|
22411
23720
|
),
|
|
22412
23721
|
RustType::I32 => format!(
|
|
22413
23722
|
"{}.get({}).copied().unwrap_or(0)",
|
|
22414
|
-
esc_obj,
|
|
23723
|
+
esc_obj, idx_ref
|
|
22415
23724
|
),
|
|
22416
23725
|
RustType::Bool => {
|
|
22417
|
-
format!("{}.get({}).copied().unwrap_or(false)", esc_obj,
|
|
23726
|
+
format!("{}.get({}).copied().unwrap_or(false)", esc_obj, idx_ref)
|
|
22418
23727
|
}
|
|
22419
|
-
// Other element types
|
|
22420
|
-
|
|
23728
|
+
// Other element types (struct/Value/String). From behind a
|
|
23729
|
+
// `borrow()` guard a non-Copy element must be cloned OUT (it can't be
|
|
23730
|
+
// moved from the borrow); a plain local keeps the direct place.
|
|
23731
|
+
_ if idx_wrapped => format!("{}[{}].clone()", esc_obj, idx_ref),
|
|
23732
|
+
_ => format!("{}[{}]", esc_obj, idx_ref),
|
|
23733
|
+
};
|
|
23734
|
+
// #567: wrap the wrapped-Vec read in a block that pre-binds the index
|
|
23735
|
+
// and scopes the borrow guard `__bg`, so the lock is released before
|
|
23736
|
+
// any sibling read of the same cell in the enclosing expression.
|
|
23737
|
+
let access = if idx_wrapped {
|
|
23738
|
+
format!(
|
|
23739
|
+
"{{ let __bi = {}; let __bg = {}.borrow(); {} }}",
|
|
23740
|
+
idx_usize, esc_raw, access
|
|
23741
|
+
)
|
|
23742
|
+
} else {
|
|
23743
|
+
access
|
|
22421
23744
|
};
|
|
22422
23745
|
return Ok((access, elem_ty));
|
|
22423
23746
|
}
|
|
22424
23747
|
// Native tuple access: `tuple[const]` -> `tuple.const` (Rust tuples
|
|
22425
23748
|
// require a literal index; a variable index falls through to boxed).
|
|
22426
|
-
|
|
23749
|
+
// (Plain local only — a wrapped tuple keeps the boxed path.)
|
|
23750
|
+
if let (false, RustType::Tuple(elems)) = (idx_wrapped, &obj_type) {
|
|
22427
23751
|
if let Expr::Literal {
|
|
22428
23752
|
value: Literal::Number(n),
|
|
22429
23753
|
..
|
|
@@ -22501,6 +23825,35 @@ impl Codegen {
|
|
|
22501
23825
|
}
|
|
22502
23826
|
}
|
|
22503
23827
|
}
|
|
23828
|
+
// Typed extern (a `cargo:` import with a declared native signature) used INSIDE a
|
|
23829
|
+
// native/typed expression: `entity_x(e) + 16`, `cvar(e, k) + 1`. Emit the direct
|
|
23830
|
+
// `crate::name_typed(<coerced args>)` and report its NATIVE return type, so the
|
|
23831
|
+
// surrounding arithmetic/comparison stays native instead of boxing the result and
|
|
23832
|
+
// dropping to `ops::*`. Only a native-returning extern is taken here; a `void`/`Value`
|
|
23833
|
+
// one falls through to the boxed path (this is a value-position expression).
|
|
23834
|
+
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
23835
|
+
if let Some(sig) = self.extern_fns.get(fname.as_ref()).cloned() {
|
|
23836
|
+
if sig.ret.is_native()
|
|
23837
|
+
&& args.len() == sig.params.len()
|
|
23838
|
+
&& args.iter().all(|a| matches!(a, CallArg::Expr(_)))
|
|
23839
|
+
{
|
|
23840
|
+
let mut argc: Vec<String> = Vec::with_capacity(args.len());
|
|
23841
|
+
for (a, ty) in args.iter().zip(sig.params.iter()) {
|
|
23842
|
+
if let CallArg::Expr(e) = a {
|
|
23843
|
+
argc.push(self.emit_coerced_native(e, ty)?);
|
|
23844
|
+
}
|
|
23845
|
+
}
|
|
23846
|
+
let call = format!(
|
|
23847
|
+
"{}::{}_typed({})",
|
|
23848
|
+
sig.crate_name,
|
|
23849
|
+
sig.symbol,
|
|
23850
|
+
argc.join(", ")
|
|
23851
|
+
);
|
|
23852
|
+
return Ok((call, sig.ret));
|
|
23853
|
+
}
|
|
23854
|
+
}
|
|
23855
|
+
}
|
|
23856
|
+
|
|
22504
23857
|
// M5: direct call to an eligible native fn -> `name_native(<native args>)`.
|
|
22505
23858
|
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
22506
23859
|
if self.native_fns.contains(fname.as_ref()) {
|
|
@@ -22620,6 +23973,19 @@ impl Codegen {
|
|
|
22620
23973
|
if let Some(res) = self.native_vec_hof_for_call(callee, args)? {
|
|
22621
23974
|
return Ok(res);
|
|
22622
23975
|
}
|
|
23976
|
+
// A call to a user tish function whose declared return type is a native struct
|
|
23977
|
+
// (`function gameState(): GameState`). The function is still a boxed closure, so the call
|
|
23978
|
+
// yields a `Value` — a `Value::Struct` on GBA. Coerce it to the native struct via
|
|
23979
|
+
// `from_value_expr` (a cheap `downcast_struct` on GBA; the `get_prop` rebuild off-GBA), so
|
|
23980
|
+
// the surrounding `gs.score` / `gs.bossHp` reads lower to plain Rust field loads instead of
|
|
23981
|
+
// boxed `get_prop`. A false positive (name shadowed by a non-struct local) is harmless:
|
|
23982
|
+
// `from_value_expr` downcasts if it can and otherwise rebuilds via `get_prop`.
|
|
23983
|
+
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
23984
|
+
if let Some(ret) = self.user_fn_returns.get(fname.as_ref()).cloned() {
|
|
23985
|
+
let boxed = self.emit_expr(expr)?;
|
|
23986
|
+
return Ok((ret.from_value_expr(&boxed), ret));
|
|
23987
|
+
}
|
|
23988
|
+
}
|
|
22623
23989
|
let result = self.emit_expr(expr)?;
|
|
22624
23990
|
Ok((result, RustType::Value))
|
|
22625
23991
|
}
|
|
@@ -22644,7 +24010,7 @@ impl Codegen {
|
|
|
22644
24010
|
if prop_name.as_ref() == "length" {
|
|
22645
24011
|
let var_esc = Self::escape_ident(var_name.as_ref()).into_owned();
|
|
22646
24012
|
let code = if self.refcell_wrapped_vars.contains(var_name.as_ref()) {
|
|
22647
|
-
format!("({}.borrow().len() as f64)", var_esc)
|
|
24013
|
+
format!("({{ let __bg = {}.borrow(); __bg.len() as f64 }})", var_esc)
|
|
22648
24014
|
} else {
|
|
22649
24015
|
format!("({}.len() as f64)", var_esc)
|
|
22650
24016
|
};
|
|
@@ -22659,7 +24025,7 @@ impl Codegen {
|
|
|
22659
24025
|
let field = crate::types::field_ident(prop_name.as_ref());
|
|
22660
24026
|
let access = if self.refcell_wrapped_vars.contains(var_name.as_ref())
|
|
22661
24027
|
{
|
|
22662
|
-
format!("
|
|
24028
|
+
format!("{{ let __bg = {}.borrow(); (*__bg).{}.clone() }}", var_esc, field)
|
|
22663
24029
|
} else {
|
|
22664
24030
|
format!("{}.{}", var_esc, field)
|
|
22665
24031
|
};
|