@tishlang/tish-lsp 2.39.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +4 -1
- package/bin/tish-lsp +0 -0
- package/crates/tish/src/cli_help.rs +28 -0
- package/crates/tish/src/main.rs +41 -5
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +21 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/tish.d.tish +4 -0
- package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +5 -1
- package/crates/tish/tests/regr_typed_externs.rs +59 -0
- package/crates/tish/tests/scope_merge.rs +79 -0
- package/crates/tish_build_utils/src/lib.rs +27 -1
- package/crates/tish_builtins/Cargo.toml +14 -6
- package/crates/tish_builtins/src/array.rs +130 -54
- package/crates/tish_builtins/src/collections.rs +9 -5
- package/crates/tish_builtins/src/construct.rs +7 -1
- package/crates/tish_builtins/src/date.rs +15 -1
- package/crates/tish_builtins/src/globals.rs +65 -3
- package/crates/tish_builtins/src/helpers.rs +7 -3
- package/crates/tish_builtins/src/iterator.rs +5 -1
- package/crates/tish_builtins/src/lib.rs +3 -0
- package/crates/tish_builtins/src/math.rs +14 -0
- package/crates/tish_builtins/src/number.rs +72 -6
- package/crates/tish_builtins/src/object.rs +6 -1
- package/crates/tish_builtins/src/string.rs +13 -2
- package/crates/tish_builtins/src/symbol.rs +17 -17
- package/crates/tish_builtins/src/typedarrays.rs +9 -2
- package/crates/tish_bytecode/src/compiler.rs +76 -2
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +77 -2
- package/crates/tish_compile/src/codegen.rs +2154 -225
- package/crates/tish_compile/src/infer.rs +37 -41
- package/crates/tish_compile/src/lib.rs +13 -1
- package/crates/tish_compile/src/platform_resolve.rs +361 -0
- package/crates/tish_compile/src/resolve.rs +195 -11
- package/crates/tish_compile/src/schemes.rs +333 -0
- package/crates/tish_compile/src/types.rs +205 -34
- package/crates/tish_compile/tests/perf_codegen_320.rs +2 -2
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +3 -1
- package/crates/tish_compile/tests/platform_resolve_cli.rs +164 -0
- package/crates/tish_compile/tests/regr_556_param_name_collision.rs +40 -0
- package/crates/tish_compile/tests/regr_558_struct_array_write.rs +65 -0
- package/crates/tish_compile/tests/regr_558b_struct_field_read.rs +21 -0
- package/crates/tish_compile/tests/regr_558c_wrapped_struct_array.rs +64 -0
- package/crates/tish_compile/tests/regr_558d_native_vec_mutators.rs +156 -0
- package/crates/tish_compile/tests/regr_562_wrapped_struct_array_push.rs +50 -0
- package/crates/tish_compile/tests/regr_fixed_native_coercions.rs +69 -0
- package/crates/tish_compile/tests/regr_gba_numerics_gated.rs +58 -0
- package/crates/tish_compile/tests/regr_gba_struct_reflection.rs +106 -0
- package/crates/tish_compile/tests/regression/name_collision_lib.tish +6 -0
- package/crates/tish_compile/tests/regression/name_collision_main.tish +13 -0
- package/crates/tish_compile/tests/regression/struct_array_write.tish +22 -0
- package/crates/tish_compile/tests/regression/struct_field_return_read.tish +10 -0
- package/crates/tish_compiler_wasm/Cargo.toml +1 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +56 -6
- package/crates/tish_core/Cargo.toml +39 -4
- package/crates/tish_core/src/compat.rs +506 -0
- package/crates/tish_core/src/console_style.rs +23 -1
- package/crates/tish_core/src/json.rs +42 -10
- package/crates/tish_core/src/lib.rs +121 -25
- package/crates/tish_core/src/macros.rs +1 -1
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/uri.rs +8 -1
- package/crates/tish_core/src/value.rs +305 -54
- package/crates/tish_core/src/vmref.rs +17 -5
- package/crates/tish_eval/Cargo.toml +2 -0
- package/crates/tish_eval/src/eval.rs +90 -3
- package/crates/tish_eval/src/value.rs +8 -0
- package/crates/tish_eval/src/value_convert.rs +316 -21
- package/crates/tish_lsp/Cargo.toml +3 -1
- package/crates/tish_lsp/src/import_goto.rs +41 -14
- package/crates/tish_lsp/src/main.rs +18 -0
- package/crates/tish_native/src/build.rs +246 -0
- package/crates/tish_native/src/config.rs +20 -0
- package/crates/tish_native/src/lib.rs +16 -6
- package/crates/tish_parser/src/lib.rs +28 -0
- package/crates/tish_parser/src/parser.rs +25 -4
- package/crates/tish_runtime/src/lib.rs +55 -9
- package/crates/tish_runtime_gba/Cargo.lock +837 -0
- package/crates/tish_runtime_gba/Cargo.toml +16 -0
- package/crates/tish_runtime_gba/src/gba.rs +211 -0
- package/crates/tish_runtime_gba/src/lib.rs +804 -0
- package/crates/tish_vm/src/jit.rs +1446 -164
- package/crates/tish_vm/src/vm.rs +583 -173
- package/justfile +9 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-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.
|
|
@@ -589,6 +589,12 @@ pub fn compile_project_full_emit(
|
|
|
589
589
|
> {
|
|
590
590
|
use crate::resolve;
|
|
591
591
|
let root = project_root.unwrap_or_else(|| entry_path.parent().unwrap_or(Path::new(".")));
|
|
592
|
+
// Install the import-scheme registry (built-ins + any `tish.schemes` from package.json) for
|
|
593
|
+
// this build, so resolution and codegen recognize `asset:` and any project-declared scheme.
|
|
594
|
+
crate::schemes::set_active(crate::schemes::SchemeRegistry::from_project(root));
|
|
595
|
+
// (The narrow-int/`fixed`/`i32` numeric profile is set from `emit_mode` in
|
|
596
|
+
// `compile_with_native_modules_emit`, the funnel every entry point reaches — not here — so it
|
|
597
|
+
// can't leak across compiles on a shared thread. Resolution/inference don't lower annotations.)
|
|
592
598
|
let modules = resolve::resolve_project(entry_path, project_root).map_err(|e| CompileError {
|
|
593
599
|
message: e,
|
|
594
600
|
span: None,
|
|
@@ -597,7 +603,7 @@ pub fn compile_project_full_emit(
|
|
|
597
603
|
message: e,
|
|
598
604
|
span: None,
|
|
599
605
|
})?;
|
|
600
|
-
let merged = resolve::merge_modules(modules).map_err(|e| CompileError {
|
|
606
|
+
let mut merged = resolve::merge_modules(modules).map_err(|e| CompileError {
|
|
601
607
|
message: e,
|
|
602
608
|
span: None,
|
|
603
609
|
})?;
|
|
@@ -612,6 +618,24 @@ pub fn compile_project_full_emit(
|
|
|
612
618
|
span: None,
|
|
613
619
|
})?;
|
|
614
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
|
+
}
|
|
615
639
|
let mut native_build =
|
|
616
640
|
resolve::compute_native_build_artifacts(&merged.program, root, &native_modules).map_err(
|
|
617
641
|
|e| CompileError {
|
|
@@ -660,6 +684,11 @@ pub fn compile_with_native_modules(
|
|
|
660
684
|
native_init: &std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
|
|
661
685
|
optimize: bool,
|
|
662
686
|
) -> Result<String, CompileError> {
|
|
687
|
+
// This non-project entry (reached by `compile`/`compile_with_features`, never the GBA project
|
|
688
|
+
// path) compiles an already-resolved program, so no scheme registry was installed for it. Reset
|
|
689
|
+
// to the empty built-in set, in case a prior GBA compile on this thread left one active — a
|
|
690
|
+
// normal program has no scheme imports, but this keeps per-compile thread-local state hermetic.
|
|
691
|
+
crate::schemes::set_active(crate::schemes::SchemeRegistry::builtin());
|
|
663
692
|
compile_with_native_modules_emit(
|
|
664
693
|
program,
|
|
665
694
|
project_root,
|
|
@@ -708,6 +737,24 @@ pub fn compile_with_native_modules_emit(
|
|
|
708
737
|
optimize: bool,
|
|
709
738
|
emit_mode: crate::NativeEmitMode,
|
|
710
739
|
) -> Result<String, CompileError> {
|
|
740
|
+
// Per-compile thread-local state — the narrow-int/`fixed`/`i32` numeric profile and the import-
|
|
741
|
+
// scheme registry — must not outlive THIS compile. Both persist on the thread, so a later compile
|
|
742
|
+
// on the same thread (LSP, `cargo test` workers share threads) could inherit a prior GBA compile's
|
|
743
|
+
// state and lower those annotations natively off-GBA / see stale schemes — diverging from the
|
|
744
|
+
// interpreter. This funnel is the one point every entry reaches, so reset both to their defaults
|
|
745
|
+
// on the way OUT, even on panic (mirrors tish_resolve's guard). Then set the numeric profile for
|
|
746
|
+
// THIS compile from the always-correct `emit_mode`. The scheme registry is installed before
|
|
747
|
+
// resolution by `compile_project_full_emit` (it must exist during resolve); the guard only clears
|
|
748
|
+
// it afterward so it can't leak forward.
|
|
749
|
+
struct StateGuard;
|
|
750
|
+
impl Drop for StateGuard {
|
|
751
|
+
fn drop(&mut self) {
|
|
752
|
+
crate::types::set_gba_numerics(false);
|
|
753
|
+
crate::schemes::set_active(crate::schemes::SchemeRegistry::builtin());
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
let _state_guard = StateGuard;
|
|
757
|
+
crate::types::set_gba_numerics(emit_mode == crate::NativeEmitMode::Gba);
|
|
711
758
|
let program = if optimize {
|
|
712
759
|
tishlang_opt::optimize(program)
|
|
713
760
|
} else {
|
|
@@ -742,13 +789,77 @@ pub fn compile_with_native_modules_emit(
|
|
|
742
789
|
g.has_native_ui_host = native_modules.iter().any(|m| {
|
|
743
790
|
m.package_name == "tish-macos"
|
|
744
791
|
|| m.package_name == "tish-ios"
|
|
792
|
+
|| m.crate_name == "tish_macos"
|
|
745
793
|
|| m.crate_name == "tishlang_macos"
|
|
794
|
+
|| m.crate_name == "tish_ios"
|
|
746
795
|
|| m.crate_name == "tishlang_ios"
|
|
747
796
|
});
|
|
748
797
|
g.emit_program(&program)?;
|
|
749
798
|
Ok(g.output)
|
|
750
799
|
}
|
|
751
800
|
|
|
801
|
+
/// Rewrite the `std::` paths that survive in `Gba`-mode generated code into their
|
|
802
|
+
/// `core::` equivalents (the emitted crate is `#![no_std]`). Applied once to the
|
|
803
|
+
/// whole output. Each pattern is a specific fully-qualified Rust path — none occur
|
|
804
|
+
/// inside escaped user string literals — so the textual pass is safe. Callers add
|
|
805
|
+
/// to this table as the thumbv4t corpus build surfaces new sites.
|
|
806
|
+
fn gba_no_std_rewrite(src: String) -> String {
|
|
807
|
+
// Apply the fixups only to CODE, never to the contents of string literals: a user
|
|
808
|
+
// string that happens to contain one of these patterns (e.g. `let s = "std::mem::x"`)
|
|
809
|
+
// must be left byte-for-byte intact. We scan the generated source, copy each `"…"`
|
|
810
|
+
// literal through verbatim (respecting `\` escapes), and rewrite only the code between
|
|
811
|
+
// them. (`"` and `\` are ASCII, so a byte scan is UTF-8 safe; char literals can't hold
|
|
812
|
+
// these multi-char patterns, and any stray `'"'` at worst *misses* a fixup — a loud
|
|
813
|
+
// no_std compile error — never a silent string corruption.)
|
|
814
|
+
let bytes = src.as_bytes();
|
|
815
|
+
let mut out = String::with_capacity(src.len());
|
|
816
|
+
let mut seg_start = 0;
|
|
817
|
+
let mut i = 0;
|
|
818
|
+
while i < bytes.len() {
|
|
819
|
+
if bytes[i] == b'"' {
|
|
820
|
+
out.push_str(&gba_rewrite_code(&src[seg_start..i]));
|
|
821
|
+
let str_start = i;
|
|
822
|
+
i += 1;
|
|
823
|
+
while i < bytes.len() {
|
|
824
|
+
match bytes[i] {
|
|
825
|
+
b'\\' => i += 2, // skip the escape and the (ASCII) escaped byte
|
|
826
|
+
b'"' => {
|
|
827
|
+
i += 1;
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
830
|
+
_ => i += 1,
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
out.push_str(&src[str_start..i.min(bytes.len())]);
|
|
834
|
+
seg_start = i;
|
|
835
|
+
} else {
|
|
836
|
+
i += 1;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
out.push_str(&gba_rewrite_code(&src[seg_start..]));
|
|
840
|
+
out
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/// The std::→core::/facade path fixups, applied only to non-string-literal code by
|
|
844
|
+
/// [`gba_no_std_rewrite`]. Callers extend this table as the thumbv4t corpus build surfaces
|
|
845
|
+
/// new sites (a surviving `std::` path is a loud `#![no_std]` compile error, not silent).
|
|
846
|
+
fn gba_rewrite_code(src: &str) -> String {
|
|
847
|
+
src.replace("std::error::Error", "core::error::Error")
|
|
848
|
+
.replace("std::f64::consts", "core::f64::consts")
|
|
849
|
+
.replace("std::cmp::Ordering", "core::cmp::Ordering")
|
|
850
|
+
.replace("std::mem::", "core::mem::")
|
|
851
|
+
.replace("std::slice::", "core::slice::")
|
|
852
|
+
// Struct→Value boundary conversions (`object_from_pairs`) and a few other
|
|
853
|
+
// sites emit fully-qualified `::std::sync::Arc` / `std::sync::Arc`. The Gba
|
|
854
|
+
// prelude imports `Arc` (= facade Rc alias), so map both to it. The `::`
|
|
855
|
+
// form must be replaced first (it contains the non-`::` form).
|
|
856
|
+
.replace("::std::sync::Arc", "Arc")
|
|
857
|
+
.replace("std::sync::Arc", "Arc")
|
|
858
|
+
// hashbrown lacks `From<[_; N]>` with a custom hasher; route object-literal
|
|
859
|
+
// construction through the facade helper.
|
|
860
|
+
.replace("ObjectMap::from(", "tishlang_runtime::object_map_from(")
|
|
861
|
+
}
|
|
862
|
+
|
|
752
863
|
/// #177 (S-E/S-F): the return shape of a de-virtualized aggregate (struct/array) free fn.
|
|
753
864
|
#[derive(Debug, Clone, PartialEq)]
|
|
754
865
|
enum AggRet {
|
|
@@ -784,7 +895,7 @@ struct AggFnSig {
|
|
|
784
895
|
ret: AggRet,
|
|
785
896
|
}
|
|
786
897
|
|
|
787
|
-
/// #178 (
|
|
898
|
+
/// #178 (default-on via `native_opts_enabled`): a recursive struct shape inferred *structurally* (no name
|
|
788
899
|
/// matching) from object literals whose fields are either scalars or recursive self-references.
|
|
789
900
|
/// Drives native lowering of tree/list builders and consumers to `Option<Box<T>>` + native fns —
|
|
790
901
|
/// the real, name-independent binary_trees lowering. See docs/recursive-struct-native.md.
|
|
@@ -856,6 +967,22 @@ pub(crate) struct NativeVecFnSig {
|
|
|
856
967
|
ret: VecRetKind,
|
|
857
968
|
}
|
|
858
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
|
+
|
|
859
986
|
/// #173 part 3 — a symbolic upper bound used by the in-bounds index proof. Two forms are matched by
|
|
860
987
|
/// structural equality: an integer constant (`vec![K; 100]`, guard `i < 100`) and a single variable
|
|
861
988
|
/// (`a` filled to length `n`, guard `i < n`). Anything more complex (`2 * n`, `a.length` member,
|
|
@@ -891,6 +1018,13 @@ pub(crate) struct Codegen {
|
|
|
891
1018
|
features: std::collections::HashSet<String>,
|
|
892
1019
|
/// spec -> native init strategy (legacy adapter object vs generated `generated_native` wrapper)
|
|
893
1020
|
native_module_init: std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
|
|
1021
|
+
/// Typed native imports (`declare fun` + a native import): tish name -> its typed signature.
|
|
1022
|
+
/// A matching call emits a DIRECT `<crate>::name_typed(..)` instead of a boxed `value_call`.
|
|
1023
|
+
extern_fns: std::collections::HashMap<String, ExternSig>,
|
|
1024
|
+
/// User tish functions whose declared return type is a native STRUCT (`function f(): Iface`).
|
|
1025
|
+
/// A call in a typed context coerces the boxed result to the native struct (a `Value::Struct`
|
|
1026
|
+
/// downcast on GBA), so `f().field` reads are native instead of `get_prop`. Name -> `RustType::Named`.
|
|
1027
|
+
user_fn_returns: std::collections::HashMap<String, crate::types::RustType>,
|
|
894
1028
|
/// Stack: true = async Rust context (run body), false = sync closure (Tish fn body)
|
|
895
1029
|
async_context_stack: Vec<bool>,
|
|
896
1030
|
loop_stack: Vec<(String, Option<String>)>, // (break_label, continue_update) for innermost loop
|
|
@@ -915,7 +1049,7 @@ pub(crate) struct Codegen {
|
|
|
915
1049
|
/// Variables currently wrapped in Rc<RefCell<Value>> for mutable capture in closures
|
|
916
1050
|
/// These need special handling: reads via .borrow().clone(), writes via *var.borrow_mut()
|
|
917
1051
|
refcell_wrapped_vars: std::collections::HashSet<String>,
|
|
918
|
-
/// #176 (
|
|
1052
|
+
/// #176 (default-on via `native_opts_enabled`): top-level `let` bindings lowered to `thread_local Cell<f64>`
|
|
919
1053
|
/// (`G_NAME`) when every use is a numeric read or a whole-binding numeric assign (fasta `seed`).
|
|
920
1054
|
native_numeric_globals: std::collections::HashMap<String, f64>,
|
|
921
1055
|
/// Top-level `let name = [f64 literals…]` never reassigned — emitted as `const G_name: [f64; N]`
|
|
@@ -940,7 +1074,7 @@ pub(crate) struct Codegen {
|
|
|
940
1074
|
/// of `str_char_code_at`'s `chars().nth(i)` = O(i). Maps the source ident → the hoisted `_scN`
|
|
941
1075
|
/// `Vec<char>` binding; populated for the loop body's extent only.
|
|
942
1076
|
hoisted_string_chars: std::collections::HashMap<String, String>,
|
|
943
|
-
/// M5 (
|
|
1077
|
+
/// M5 (default-on via `native_opts_enabled`): top-level functions eligible for a parallel
|
|
944
1078
|
/// free `fn f_native(f64,..)->f64` (all params `: number`, returns `number`, native-safe
|
|
945
1079
|
/// body). Direct calls to these route to the native fn, bypassing the boxed `value_call`.
|
|
946
1080
|
native_fns: std::collections::HashSet<String>,
|
|
@@ -1001,7 +1135,7 @@ pub(crate) struct Codegen {
|
|
|
1001
1135
|
/// nested call site these are already `&/&mut Vec<T>` references, so they pass through by reborrow
|
|
1002
1136
|
/// (`&mut *p`) rather than being address-of'd like a local `Vec` (`&mut local`).
|
|
1003
1137
|
vec_ref_params: std::collections::HashMap<String, bool>,
|
|
1004
|
-
/// #320 (
|
|
1138
|
+
/// #320 (default-on via `native_opts_enabled`): normal boxed-closure fns that take one or more READ-ONLY
|
|
1005
1139
|
/// `number[]` params (e.g. k_nucleotide's `kNucleotide(seq, k)` — indexes `seq` but returns a
|
|
1006
1140
|
/// boxed Map). Per param: `true` = unbox the boxed arg into an owned native `Vec<f64>` so the
|
|
1007
1141
|
/// body indexes it natively (`seq[i+j]` → f64), `false` = leave it on its normal path. The fn
|
|
@@ -1009,10 +1143,10 @@ pub(crate) struct Codegen {
|
|
|
1009
1143
|
/// native. Computed by `Self::native_arr_param_fns`, the SAME pure fn infer reads (agreement
|
|
1010
1144
|
/// contract — see `infer::native_vec_array_params`), disjoint from `native_vec_fns`.
|
|
1011
1145
|
native_arr_param_fns: std::collections::HashMap<String, Vec<bool>>,
|
|
1012
|
-
/// #177 S-E/S-F (
|
|
1146
|
+
/// #177 S-E/S-F (default-on via `native_opts_enabled`): the unboxed struct alias name
|
|
1013
1147
|
/// (e.g. `TishAnon_0`) when the interprocedural aggregate path is active for this program,
|
|
1014
1148
|
/// else `None`. Set in `emit_program` only after the de-virtualized fns emit successfully.
|
|
1015
|
-
/// #178: inferred recursive-struct lowering plan (
|
|
1149
|
+
/// #178: inferred recursive-struct lowering plan (default-on via `native_opts_enabled`).
|
|
1016
1150
|
rec_struct_plan: Option<RecStructPlan>,
|
|
1017
1151
|
aggregate_alias: Option<String>,
|
|
1018
1152
|
/// #177: fn name → its de-virtualized native signature. Calls to these route directly to the
|
|
@@ -1108,6 +1242,11 @@ pub(crate) struct Codegen {
|
|
|
1108
1242
|
/// closures it must not return a `Result` from a `Value`-returning closure.
|
|
1109
1243
|
value_fn_depth: u32,
|
|
1110
1244
|
emit_mode: crate::NativeEmitMode,
|
|
1245
|
+
/// Scheme imports as `(scheme_name, absolute_path)`, in first-seen order. Populated (Gba mode)
|
|
1246
|
+
/// before the entry is emitted from the active [`crate::schemes`] registry; drives the
|
|
1247
|
+
/// `mod __scheme_<name>_<j> { … }` blocks and their per-scheme registration. The handle a tish
|
|
1248
|
+
/// name binds to is its index AMONG same-scheme entries (= its slot in that scheme's arena).
|
|
1249
|
+
scheme_files: Vec<(String, String)>,
|
|
1111
1250
|
/// Program links `tish:macos` / `tish:ios` — skip HeadlessHost install.
|
|
1112
1251
|
has_native_ui_host: bool,
|
|
1113
1252
|
/// Program references browser global `document` — inject tish-canvas.
|
|
@@ -1120,6 +1259,10 @@ pub(crate) struct Codegen {
|
|
|
1120
1259
|
cse_subst: Vec<(Expr, String)>,
|
|
1121
1260
|
/// Monotonic counter for unique CSE temp names (`_cse0`, `_cse1`, …).
|
|
1122
1261
|
cse_temp_counter: usize,
|
|
1262
|
+
/// The program's used NON-ASCII characters (collected from every string literal), as a sorted
|
|
1263
|
+
/// string. Fed to import schemes via `{charset}` so a scheme can bake only the glyphs a program
|
|
1264
|
+
/// actually uses — e.g. `font:` bakes ASCII + these, not all ~24k glyphs of a CJK font.
|
|
1265
|
+
string_charset: String,
|
|
1123
1266
|
}
|
|
1124
1267
|
|
|
1125
1268
|
/// One `map`/`filter` stage of a fused HOF chain (#317): the single-element-param closure's
|
|
@@ -1146,8 +1289,11 @@ impl Codegen {
|
|
|
1146
1289
|
indent: 0,
|
|
1147
1290
|
loop_label_index: 0,
|
|
1148
1291
|
is_async: false,
|
|
1292
|
+
string_charset: String::new(),
|
|
1149
1293
|
features,
|
|
1150
1294
|
native_module_init,
|
|
1295
|
+
extern_fns: std::collections::HashMap::new(),
|
|
1296
|
+
user_fn_returns: std::collections::HashMap::new(),
|
|
1151
1297
|
async_context_stack: Vec::new(),
|
|
1152
1298
|
loop_stack: Vec::new(),
|
|
1153
1299
|
break_stack: Vec::new(),
|
|
@@ -1213,6 +1359,7 @@ impl Codegen {
|
|
|
1213
1359
|
program_fun_decl_names: std::collections::HashSet::new(),
|
|
1214
1360
|
value_fn_depth: 0,
|
|
1215
1361
|
emit_mode: crate::NativeEmitMode::DesktopBin,
|
|
1362
|
+
scheme_files: Vec::new(),
|
|
1216
1363
|
has_native_ui_host: false,
|
|
1217
1364
|
program_uses_document: false,
|
|
1218
1365
|
cse_subst: Vec::new(),
|
|
@@ -1346,6 +1493,160 @@ impl Codegen {
|
|
|
1346
1493
|
/// an object shape. Each generated struct derives `Clone` + `Debug`
|
|
1347
1494
|
/// (cheap; field types are all `Copy`-or-cheap-clone in practice) and
|
|
1348
1495
|
/// is named `TishStruct_<TishAlias>`.
|
|
1496
|
+
/// Split a lowered scheme spec (`<prefix><abspath>`) into `(scheme_name, abspath)` using the
|
|
1497
|
+
/// active registry. `None` if the spec isn't a registered scheme.
|
|
1498
|
+
fn split_scheme_spec(spec: &str) -> Option<(String, String)> {
|
|
1499
|
+
crate::schemes::with_active(|reg| {
|
|
1500
|
+
reg.matches(spec).map(|s| {
|
|
1501
|
+
let prefix = s.prefix();
|
|
1502
|
+
let rest = spec.strip_prefix(&prefix).unwrap_or(spec).to_string();
|
|
1503
|
+
(s.name.clone(), rest)
|
|
1504
|
+
})
|
|
1505
|
+
})
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/// Populate [`Self::scheme_files`] from the program's scheme imports. Each was lowered (resolve
|
|
1509
|
+
/// phase) to a top-level `let name = NativeModuleLoad { spec: "<prefix><abspath>" }`, so a flat
|
|
1510
|
+
/// scan of the top-level statements finds them all. De-duplicated on `(scheme, path)`.
|
|
1511
|
+
fn collect_scheme_files(&mut self, stmts: &[Statement]) {
|
|
1512
|
+
for s in stmts {
|
|
1513
|
+
if let Statement::VarDecl {
|
|
1514
|
+
init: Some(Expr::NativeModuleLoad { spec, .. }),
|
|
1515
|
+
..
|
|
1516
|
+
} = s
|
|
1517
|
+
{
|
|
1518
|
+
if let Some(entry) = Self::split_scheme_spec(spec.as_ref()) {
|
|
1519
|
+
if !self.scheme_files.contains(&entry) {
|
|
1520
|
+
self.scheme_files.push(entry);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
/// The runtime arena a scheme registers into, keyed by its (pre-render) `register` template.
|
|
1528
|
+
/// Schemes that share a template share an arena: `asset:` and `sheet:` both call
|
|
1529
|
+
/// `__asset_register_sheet`, so both index into the same runtime `Vec`. `None` for a scheme with
|
|
1530
|
+
/// no registration call (nothing to index into).
|
|
1531
|
+
fn scheme_arena_key(&self, scheme: &str) -> Option<String> {
|
|
1532
|
+
let emit = self.scheme_target_emit(scheme).ok()?;
|
|
1533
|
+
(!emit.register.is_empty()).then_some(emit.register)
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/// The handle a scheme spec binds to — its slot in the runtime ARENA it registers into. The
|
|
1537
|
+
/// arena is keyed by the scheme's `register` target, NOT its name: `asset:` and `sheet:` both
|
|
1538
|
+
/// push to `__asset_register_sheet`, so a program mixing them must number handles across BOTH
|
|
1539
|
+
/// (the entry registers every scheme file in one first-seen pass, so same-arena entries land in
|
|
1540
|
+
/// exactly this order). Numbering per-scheme-name collided their handles — an `asset:` and a
|
|
1541
|
+
/// `sheet:` import each got handle 0 while the runtime gave them slots 0 and 1 (#552).
|
|
1542
|
+
fn scheme_handle_of(&self, spec: &str) -> Option<usize> {
|
|
1543
|
+
let (scheme, path) = Self::split_scheme_spec(spec)?;
|
|
1544
|
+
let arena = self.scheme_arena_key(&scheme)?;
|
|
1545
|
+
let mut k = 0usize;
|
|
1546
|
+
for (s, p) in &self.scheme_files {
|
|
1547
|
+
if self.scheme_arena_key(s).as_deref() != Some(arena.as_str()) {
|
|
1548
|
+
continue;
|
|
1549
|
+
}
|
|
1550
|
+
if *s == scheme && *p == path {
|
|
1551
|
+
return Some(k);
|
|
1552
|
+
}
|
|
1553
|
+
k += 1;
|
|
1554
|
+
}
|
|
1555
|
+
None
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
/// The generated `mod` identifier for the `j`-th collected scheme file.
|
|
1559
|
+
fn scheme_mod_ident(scheme: &str, j: usize) -> String {
|
|
1560
|
+
format!("__scheme_{}_{}", scheme, j)
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
/// Emit one `mod __scheme_<name>_<j> { … }` per collected scheme file (Gba only), from the
|
|
1564
|
+
/// scheme's `gba` emit template: its `use`s, module body (the include macro, `{path}` → the
|
|
1565
|
+
/// absolute file), and a same-module accessor (which can see the macro's private statics — the
|
|
1566
|
+
/// only way a tagless image's sprites escape). Registration into the runtime arena happens in
|
|
1567
|
+
/// the entry below.
|
|
1568
|
+
/// Collect every NON-ASCII character appearing in the program's string literals, sorted+unique.
|
|
1569
|
+
/// Fed to schemes via `{charset}` (see `render_template`) so a `font:` import can bake only the
|
|
1570
|
+
/// glyphs actually used. ASCII is always baked by the scheme, so it's excluded here. Runtime-built
|
|
1571
|
+
/// strings (concatenation, numbers) resolve to ASCII, which is covered; a non-ASCII char reachable
|
|
1572
|
+
/// only through a computed string is the rare gap (documented — add it to a literal if needed).
|
|
1573
|
+
fn collect_string_charset(stmts: &[Statement]) -> String {
|
|
1574
|
+
let mut set = std::collections::BTreeSet::new();
|
|
1575
|
+
fn add(s: &str, set: &mut std::collections::BTreeSet<char>) {
|
|
1576
|
+
for c in s.chars() {
|
|
1577
|
+
if (c as u32) >= 0x7F {
|
|
1578
|
+
set.insert(c);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
fn walk(stmts: &[Statement], set: &mut std::collections::BTreeSet<char>) {
|
|
1583
|
+
for s in stmts {
|
|
1584
|
+
Codegen::for_each_stmt_expr(s, &mut |e| {
|
|
1585
|
+
Codegen::nv_for_each_subexpr(e, &mut |x| {
|
|
1586
|
+
if let Expr::Literal { value: Literal::String(str), .. } = x {
|
|
1587
|
+
add(str, set);
|
|
1588
|
+
}
|
|
1589
|
+
if let Expr::TemplateLiteral { quasis, .. } = x {
|
|
1590
|
+
for q in quasis {
|
|
1591
|
+
add(q, set);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
});
|
|
1595
|
+
});
|
|
1596
|
+
Codegen::for_each_child_stmt_list(s, &mut |list| walk(list, set));
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
walk(stmts, &mut set);
|
|
1600
|
+
set.into_iter().collect()
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
fn emit_scheme_modules(&mut self) -> Result<(), CompileError> {
|
|
1604
|
+
let files = self.scheme_files.clone();
|
|
1605
|
+
for (j, (scheme, path)) in files.iter().enumerate() {
|
|
1606
|
+
let emit = self.scheme_target_emit(scheme)?;
|
|
1607
|
+
let module_ident = Self::scheme_mod_ident(scheme, j);
|
|
1608
|
+
self.writeln(&format!("mod {} {{", module_ident));
|
|
1609
|
+
self.indent += 1;
|
|
1610
|
+
for u in &emit.uses {
|
|
1611
|
+
self.writeln(u);
|
|
1612
|
+
}
|
|
1613
|
+
self.writeln(&crate::schemes::render_template(
|
|
1614
|
+
&emit.module_body,
|
|
1615
|
+
path,
|
|
1616
|
+
&module_ident,
|
|
1617
|
+
&self.string_charset,
|
|
1618
|
+
));
|
|
1619
|
+
if !emit.accessor.is_empty() {
|
|
1620
|
+
self.writeln(&emit.accessor);
|
|
1621
|
+
}
|
|
1622
|
+
self.indent -= 1;
|
|
1623
|
+
self.writeln("}");
|
|
1624
|
+
}
|
|
1625
|
+
if !files.is_empty() {
|
|
1626
|
+
self.writeln("");
|
|
1627
|
+
}
|
|
1628
|
+
Ok(())
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
/// The `gba` [`crate::schemes::TargetEmit`] for `scheme`, or a compile error if the scheme has
|
|
1632
|
+
/// no template for this target.
|
|
1633
|
+
fn scheme_target_emit(
|
|
1634
|
+
&self,
|
|
1635
|
+
scheme: &str,
|
|
1636
|
+
) -> Result<crate::schemes::TargetEmit, CompileError> {
|
|
1637
|
+
crate::schemes::with_active(|reg| {
|
|
1638
|
+
reg.matches(&format!("{}:", scheme))
|
|
1639
|
+
.and_then(|s| s.targets.get("gba").cloned())
|
|
1640
|
+
})
|
|
1641
|
+
.ok_or_else(|| CompileError {
|
|
1642
|
+
message: format!(
|
|
1643
|
+
"import scheme '{}:' has no emit template for target 'gba'",
|
|
1644
|
+
scheme
|
|
1645
|
+
),
|
|
1646
|
+
span: None,
|
|
1647
|
+
})
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1349
1650
|
fn emit_named_struct_decls(&mut self) {
|
|
1350
1651
|
// Snapshot keys + values so we can mutate `self` (writing the
|
|
1351
1652
|
// emitted source) inside the loop.
|
|
@@ -1376,6 +1677,11 @@ impl Codegen {
|
|
|
1376
1677
|
));
|
|
1377
1678
|
}
|
|
1378
1679
|
self.write("}\n\n");
|
|
1680
|
+
// A `TishStruct` impl so this struct can be boxed BY REFERENCE into a `Value`
|
|
1681
|
+
// (`Value::from_struct` → `Value::Struct` on GBA) instead of serialised to a hashmap
|
|
1682
|
+
// object — property access on the boxed value then dispatches here (a small match,
|
|
1683
|
+
// no hash / alloc). See `types::to_value_expr` `Named` arm.
|
|
1684
|
+
self.emit_struct_tish_impl(&struct_name, fields);
|
|
1379
1685
|
// #315: emit a direct JSON serializer for this struct when every field is
|
|
1380
1686
|
// JSON-simple. Emitted for each eligible alias (unused ones are covered by
|
|
1381
1687
|
// the generated crate's `#![allow(unused)]`); the JSON.stringify intercept
|
|
@@ -1399,6 +1705,7 @@ impl Codegen {
|
|
|
1399
1705
|
));
|
|
1400
1706
|
}
|
|
1401
1707
|
self.write("}\n\n");
|
|
1708
|
+
self.emit_struct_tish_impl(&vstruct, vfields);
|
|
1402
1709
|
}
|
|
1403
1710
|
let enum_ident = crate::types::shape_union_enum_ident(uname);
|
|
1404
1711
|
self.write("#[derive(Clone, Debug)]\n");
|
|
@@ -1425,6 +1732,68 @@ impl Codegen {
|
|
|
1425
1732
|
}
|
|
1426
1733
|
}
|
|
1427
1734
|
|
|
1735
|
+
/// Emit `impl tishlang_runtime::TishStruct for TishStruct_<name>` so a typed struct can be boxed
|
|
1736
|
+
/// BY REFERENCE into a `Value` (`Value::from_struct`) rather than eagerly serialised to a hashmap
|
|
1737
|
+
/// object. `tish_get`/`tish_set` are the dynamic property path (a `match` on the field name — no
|
|
1738
|
+
/// hash, no alloc); `tish_to_object` is the materialisation used for JSON / `Object.keys`-family
|
|
1739
|
+
/// and as the non-portable `from_struct` fallback (identical to the historical boundary).
|
|
1740
|
+
fn emit_struct_tish_impl(
|
|
1741
|
+
&mut self,
|
|
1742
|
+
struct_name: &str,
|
|
1743
|
+
fields: &[(std::sync::Arc<str>, crate::types::RustType)],
|
|
1744
|
+
) {
|
|
1745
|
+
self.write("#[allow(non_snake_case, unused)]\n");
|
|
1746
|
+
self.write(&format!(
|
|
1747
|
+
"impl tishlang_runtime::TishStruct for {} {{\n",
|
|
1748
|
+
struct_name
|
|
1749
|
+
));
|
|
1750
|
+
|
|
1751
|
+
// tish_get: field name → boxed Value (Null for an unknown key).
|
|
1752
|
+
self.write(" fn tish_get(&self, __k: &str) -> tishlang_runtime::Value {\n");
|
|
1753
|
+
self.write(" match __k {\n");
|
|
1754
|
+
for (k, ty) in fields {
|
|
1755
|
+
let access = format!("self.{}", crate::types::field_ident(k));
|
|
1756
|
+
let v_expr = if matches!(ty, crate::types::RustType::Value) {
|
|
1757
|
+
format!("{}.clone()", access)
|
|
1758
|
+
} else {
|
|
1759
|
+
ty.to_value_expr(&access)
|
|
1760
|
+
};
|
|
1761
|
+
self.write(&format!(" {:?} => {},\n", k.as_ref(), v_expr));
|
|
1762
|
+
}
|
|
1763
|
+
self.write(" _ => tishlang_runtime::Value::Null,\n");
|
|
1764
|
+
self.write(" }\n }\n");
|
|
1765
|
+
|
|
1766
|
+
// tish_set: field name + boxed Value → coerce into the native field (unknown key = no-op).
|
|
1767
|
+
self.write(" fn tish_set(&mut self, __k: &str, __v: tishlang_runtime::Value) {\n");
|
|
1768
|
+
self.write(" match __k {\n");
|
|
1769
|
+
for (k, ty) in fields {
|
|
1770
|
+
let coerced = if matches!(ty, crate::types::RustType::Value) {
|
|
1771
|
+
"__v".to_string()
|
|
1772
|
+
} else {
|
|
1773
|
+
ty.from_value_expr("__v")
|
|
1774
|
+
};
|
|
1775
|
+
self.write(&format!(
|
|
1776
|
+
" {:?} => {{ self.{} = {}; }},\n",
|
|
1777
|
+
k.as_ref(),
|
|
1778
|
+
crate::types::field_ident(k),
|
|
1779
|
+
coerced
|
|
1780
|
+
));
|
|
1781
|
+
}
|
|
1782
|
+
self.write(" _ => {},\n");
|
|
1783
|
+
self.write(" }\n }\n");
|
|
1784
|
+
|
|
1785
|
+
// tish_to_object: ordered `object_from_pairs` (JS insertion order) — the materialisation path.
|
|
1786
|
+
self.write(" fn tish_to_object(&self) -> tishlang_runtime::Value {\n");
|
|
1787
|
+
self.write(&format!(
|
|
1788
|
+
" {}\n",
|
|
1789
|
+
crate::types::named_struct_to_object_expr(fields, "self")
|
|
1790
|
+
));
|
|
1791
|
+
self.write(" }\n");
|
|
1792
|
+
|
|
1793
|
+
self.write(" fn as_any(&self) -> &dyn core::any::Any { self }\n");
|
|
1794
|
+
self.write("}\n\n");
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1428
1797
|
/// #315: JSON-simple = serializable by a direct per-struct writer that is byte-identical
|
|
1429
1798
|
/// to node's `JSON.stringify` (reuses `write_json_number` / `escape_into`). Whitelist, so
|
|
1430
1799
|
/// any future RustType variant defaults to the sound Value-path fallback.
|
|
@@ -1536,6 +1905,100 @@ impl Codegen {
|
|
|
1536
1905
|
|
|
1537
1906
|
/// #315: direct JSON serializer for `JSON.stringify(arg)` when `arg`'s static type is a
|
|
1538
1907
|
/// simple struct (or Vec of one). Returns None → fall through to the generic Value path.
|
|
1908
|
+
/// A boxed `Value` of a struct field: a `Value`-typed field is cloned out of the borrow; a native
|
|
1909
|
+
/// field boxes through its own `to_value_expr` (which copies/clones internally). `place` is the
|
|
1910
|
+
/// borrowed struct expression (e.g. `_s`), so the access is `place.field`.
|
|
1911
|
+
fn struct_field_to_value(place: &str, k: &std::sync::Arc<str>, fty: &crate::types::RustType) -> String {
|
|
1912
|
+
let access = format!("{}.{}", place, crate::types::field_ident(k));
|
|
1913
|
+
if matches!(fty, crate::types::RustType::Value) {
|
|
1914
|
+
format!("{}.clone()", access)
|
|
1915
|
+
} else {
|
|
1916
|
+
fty.to_value_expr(&access)
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
/// Static-type-aware reflection: `Object.keys(s)` / `Object.values(s)` / `Object.entries(s)` where
|
|
1921
|
+
/// `s`'s static type is a known struct (`RustType::Named`). Reads the struct's fields NATIVELY, so
|
|
1922
|
+
/// the struct is never boxed to a `Value::Struct` (GBA) / hashmap object (elsewhere) and no
|
|
1923
|
+
/// intermediate object is built — only the RESULT array is a `Value` (that is the op's output, and
|
|
1924
|
+
/// unavoidable). Keys come from the compile-time field list; values are native field reads. Returns
|
|
1925
|
+
/// `None` (→ the generic boxed `Object.*` path) for a non-struct arg, a spread/multi arg, or a
|
|
1926
|
+
/// dynamic key. This is target-agnostic: the `TishStruct_*` Rust struct exists on every native
|
|
1927
|
+
/// backend, so the same native reads apply on GBA and desktop alike.
|
|
1928
|
+
fn try_emit_native_object_reflection(
|
|
1929
|
+
&mut self,
|
|
1930
|
+
callee: &Expr,
|
|
1931
|
+
args: &[CallArg],
|
|
1932
|
+
) -> Result<Option<String>, CompileError> {
|
|
1933
|
+
use crate::types::RustType;
|
|
1934
|
+
// GBA only. Desktop keeps reflection on the existing boxed path — it's already correct there
|
|
1935
|
+
// (a struct materialises to a real `Value::Object`), and gating avoids any churn to desktop
|
|
1936
|
+
// benches / codegen-string tests. GBA is where avoiding the `Value::Struct` box + hashmap
|
|
1937
|
+
// actually matters. The native reads are target-agnostic, so this path was validated by
|
|
1938
|
+
// running it on desktop before the gate went in.
|
|
1939
|
+
if self.emit_mode != crate::NativeEmitMode::Gba {
|
|
1940
|
+
return Ok(None);
|
|
1941
|
+
}
|
|
1942
|
+
let Expr::Member {
|
|
1943
|
+
object,
|
|
1944
|
+
prop: MemberProp::Name { name: m, .. },
|
|
1945
|
+
optional: false,
|
|
1946
|
+
..
|
|
1947
|
+
} = callee
|
|
1948
|
+
else {
|
|
1949
|
+
return Ok(None);
|
|
1950
|
+
};
|
|
1951
|
+
if !matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Object") {
|
|
1952
|
+
return Ok(None);
|
|
1953
|
+
}
|
|
1954
|
+
let kind = m.as_ref();
|
|
1955
|
+
if !matches!(kind, "keys" | "values" | "entries") || args.len() != 1 {
|
|
1956
|
+
return Ok(None);
|
|
1957
|
+
}
|
|
1958
|
+
let CallArg::Expr(arg0) = &args[0] else {
|
|
1959
|
+
return Ok(None);
|
|
1960
|
+
};
|
|
1961
|
+
let (code, ty) = self.emit_typed_expr(arg0)?;
|
|
1962
|
+
let RustType::Named { fields, .. } = &ty else {
|
|
1963
|
+
return Ok(None);
|
|
1964
|
+
};
|
|
1965
|
+
let out = match kind {
|
|
1966
|
+
// Keys are the compile-time field names; the struct is evaluated only for side effects.
|
|
1967
|
+
"keys" => {
|
|
1968
|
+
let keys = fields
|
|
1969
|
+
.iter()
|
|
1970
|
+
.map(|(k, _)| format!("Value::String({:?}.into())", k.as_ref()))
|
|
1971
|
+
.collect::<Vec<_>>()
|
|
1972
|
+
.join(", ");
|
|
1973
|
+
format!("{{ let _ = &({}); Value::Array(VmRef::new(vec![{}])) }}", code, keys)
|
|
1974
|
+
}
|
|
1975
|
+
"values" => {
|
|
1976
|
+
let vals = fields
|
|
1977
|
+
.iter()
|
|
1978
|
+
.map(|(k, fty)| Self::struct_field_to_value("_s", k, fty))
|
|
1979
|
+
.collect::<Vec<_>>()
|
|
1980
|
+
.join(", ");
|
|
1981
|
+
format!("{{ let _s = &({}); Value::Array(VmRef::new(vec![{}])) }}", code, vals)
|
|
1982
|
+
}
|
|
1983
|
+
"entries" => {
|
|
1984
|
+
let pairs = fields
|
|
1985
|
+
.iter()
|
|
1986
|
+
.map(|(k, fty)| {
|
|
1987
|
+
format!(
|
|
1988
|
+
"Value::Array(VmRef::new(vec![Value::String({:?}.into()), {}]))",
|
|
1989
|
+
k.as_ref(),
|
|
1990
|
+
Self::struct_field_to_value("_s", k, fty)
|
|
1991
|
+
)
|
|
1992
|
+
})
|
|
1993
|
+
.collect::<Vec<_>>()
|
|
1994
|
+
.join(", ");
|
|
1995
|
+
format!("{{ let _s = &({}); Value::Array(VmRef::new(vec![{}])) }}", code, pairs)
|
|
1996
|
+
}
|
|
1997
|
+
_ => unreachable!("kind checked above"),
|
|
1998
|
+
};
|
|
1999
|
+
Ok(Some(out))
|
|
2000
|
+
}
|
|
2001
|
+
|
|
1539
2002
|
fn try_emit_direct_json_stringify(
|
|
1540
2003
|
&mut self,
|
|
1541
2004
|
arg: &Expr,
|
|
@@ -2244,16 +2707,149 @@ impl Codegen {
|
|
|
2244
2707
|
out
|
|
2245
2708
|
}
|
|
2246
2709
|
|
|
2710
|
+
/// Typed externs (#perf): pair each top-level `declare fun name(a: i32, …): T` with a native
|
|
2711
|
+
/// import of the same name (which supplies the crate) → an [`ExternSig`] in `extern_fns`. A later
|
|
2712
|
+
/// call to `name` with lowerable args emits a DIRECT `<crate>::name_typed(..)` instead of a boxed
|
|
2713
|
+
/// `value_call`. Only fully-typed simple-param signatures qualify; anything else stays boxed.
|
|
2714
|
+
fn collect_extern_fns(&mut self, program: &Program) {
|
|
2715
|
+
use std::collections::HashMap;
|
|
2716
|
+
// Resolve lowers each `import { x } from 'cargo:…'` to `let x = NativeModuleLoad{spec, export}`,
|
|
2717
|
+
// so pair off THOSE bindings (the `Import` statement itself is gone by codegen): local name ->
|
|
2718
|
+
// (module spec, exported Rust fn name). Only a native module with a known init can back a call.
|
|
2719
|
+
let mut name_info: HashMap<String, (String, String)> = HashMap::new();
|
|
2720
|
+
for stmt in &program.statements {
|
|
2721
|
+
if let Statement::VarDecl {
|
|
2722
|
+
name,
|
|
2723
|
+
init: Some(Expr::NativeModuleLoad { spec, export_name, .. }),
|
|
2724
|
+
..
|
|
2725
|
+
} = stmt
|
|
2726
|
+
{
|
|
2727
|
+
if self.native_module_init.contains_key(spec.as_ref()) {
|
|
2728
|
+
name_info.insert(
|
|
2729
|
+
name.to_string(),
|
|
2730
|
+
(spec.to_string(), export_name.to_string()),
|
|
2731
|
+
);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
if name_info.is_empty() {
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
for stmt in &program.statements {
|
|
2739
|
+
let Statement::DeclareFun {
|
|
2740
|
+
name,
|
|
2741
|
+
params,
|
|
2742
|
+
rest_param: None,
|
|
2743
|
+
return_type,
|
|
2744
|
+
async_: false,
|
|
2745
|
+
..
|
|
2746
|
+
} = stmt
|
|
2747
|
+
else {
|
|
2748
|
+
continue;
|
|
2749
|
+
};
|
|
2750
|
+
let Some((spec, export_name)) = name_info.get(name.as_ref()) else {
|
|
2751
|
+
continue;
|
|
2752
|
+
};
|
|
2753
|
+
let crate_name = match self.native_module_init.get(spec) {
|
|
2754
|
+
Some(init) => init.crate_name().to_string(),
|
|
2755
|
+
None => continue,
|
|
2756
|
+
};
|
|
2757
|
+
// Every parameter must be a simple, explicitly-typed native scalar; a `Value` param (an
|
|
2758
|
+
// untyped one) means the direct call would gain nothing, so keep the whole fn boxed.
|
|
2759
|
+
let mut ptys = Vec::with_capacity(params.len());
|
|
2760
|
+
let mut ok = true;
|
|
2761
|
+
for p in params {
|
|
2762
|
+
match p {
|
|
2763
|
+
FunParam::Simple(tp) if tp.default.is_none() => match &tp.type_ann {
|
|
2764
|
+
Some(ann) => {
|
|
2765
|
+
let ty = RustType::from_annotation(ann);
|
|
2766
|
+
if ty.is_native() {
|
|
2767
|
+
ptys.push(ty);
|
|
2768
|
+
} else {
|
|
2769
|
+
ok = false;
|
|
2770
|
+
break;
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
None => {
|
|
2774
|
+
ok = false;
|
|
2775
|
+
break;
|
|
2776
|
+
}
|
|
2777
|
+
},
|
|
2778
|
+
_ => {
|
|
2779
|
+
ok = false;
|
|
2780
|
+
break;
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
if !ok {
|
|
2785
|
+
continue;
|
|
2786
|
+
}
|
|
2787
|
+
let ret = return_type
|
|
2788
|
+
.as_ref()
|
|
2789
|
+
.map(RustType::from_annotation)
|
|
2790
|
+
.unwrap_or(RustType::Unit);
|
|
2791
|
+
self.extern_fns.insert(
|
|
2792
|
+
name.to_string(),
|
|
2793
|
+
ExternSig { crate_name, symbol: export_name.clone(), params: ptys, ret },
|
|
2794
|
+
);
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
/// Record every top-level `function f(...): Iface` whose return annotation resolves — via the type
|
|
2799
|
+
/// aliases — to a native STRUCT (`RustType::Named`, or a `ShapeUnion`). A later call to `f` in a
|
|
2800
|
+
/// typed position then coerces the boxed result to the native struct (a `Value::Struct` downcast on
|
|
2801
|
+
/// GBA, the `get_prop` rebuild elsewhere), so `f().field` reads are native, not boxed. Runs AFTER
|
|
2802
|
+
/// `collect_type_aliases` so an interface name in the annotation resolves to its `Named` type.
|
|
2803
|
+
fn collect_user_fn_returns(&mut self, program: &Program) {
|
|
2804
|
+
for stmt in &program.statements {
|
|
2805
|
+
if let Statement::FunDecl { name, return_type: Some(ann), .. } = stmt {
|
|
2806
|
+
let ty = RustType::from_annotation_with_aliases(ann, &self.type_aliases);
|
|
2807
|
+
// Only a concrete `Named` struct return is coerced. `ShapeUnion` is deliberately
|
|
2808
|
+
// excluded: its `from_value_expr` is a forbidden boundary (`unreachable!()`), so
|
|
2809
|
+
// admitting it here would compile a runtime panic into the coercion site. (A shape
|
|
2810
|
+
// union only ever arises as an array element type, never a bare fn return, so this
|
|
2811
|
+
// is a defensive guard against a violated invariant, not a live path.)
|
|
2812
|
+
if matches!(ty, RustType::Named { .. }) {
|
|
2813
|
+
self.user_fn_returns.insert(name.to_string(), ty);
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2247
2819
|
fn emit_program(&mut self, program: &Program) -> Result<(), CompileError> {
|
|
2820
|
+
// Only GBA `font:`/`emoji:` schemes consume `{charset}` (and both `render_template`
|
|
2821
|
+
// callers that read `string_charset` are Gba-gated), so skip the whole-program string walk
|
|
2822
|
+
// for every other target — its result is never read off-GBA.
|
|
2823
|
+
if self.emit_mode == crate::NativeEmitMode::Gba {
|
|
2824
|
+
self.string_charset = Self::collect_string_charset(&program.statements);
|
|
2825
|
+
}
|
|
2248
2826
|
self.is_async = program_uses_async(program);
|
|
2249
2827
|
self.program_has_jsx = tishlang_ui::jsx::program_contains_jsx(program);
|
|
2250
2828
|
self.program_fun_decl_names = tishlang_ui::jsx::collect_fun_decl_names(program);
|
|
2251
2829
|
self.program_uses_document = crate::resolve::program_uses_document(program);
|
|
2252
|
-
self.
|
|
2253
|
-
self.
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2830
|
+
self.collect_extern_fns(program);
|
|
2831
|
+
if self.emit_mode == crate::NativeEmitMode::Gba {
|
|
2832
|
+
// no_std GBA ROM: alloc-based, Arc aliases to Rc in the facade. The
|
|
2833
|
+
// prelude `use tishlang_runtime::{…}` below is unchanged — those names
|
|
2834
|
+
// are re-exported by the tishlang_runtime_gba facade.
|
|
2835
|
+
self.write("#![no_std]\n#![no_main]\n#![allow(unused, non_snake_case)]\n\n");
|
|
2836
|
+
self.write("extern crate alloc;\n");
|
|
2837
|
+
self.write("use core::cell::RefCell;\n");
|
|
2838
|
+
self.write("use alloc::rc::Rc;\n");
|
|
2839
|
+
self.write("use alloc::boxed::Box;\n");
|
|
2840
|
+
self.write("use alloc::vec::Vec;\n");
|
|
2841
|
+
self.write("use alloc::string::{String, ToString};\n");
|
|
2842
|
+
self.write("use alloc::{vec, format};\n");
|
|
2843
|
+
self.write("use tishlang_runtime::Arc;\n");
|
|
2844
|
+
// f64 transcendentals for inline math (`Math.sqrt(x)` → `x.sqrt()`).
|
|
2845
|
+
self.write("use tishlang_runtime::FloatExt;\n");
|
|
2846
|
+
} else {
|
|
2847
|
+
self.write("#![allow(unused, non_snake_case)]\n\n");
|
|
2848
|
+
self.write("use std::cell::RefCell;\n");
|
|
2849
|
+
self.write("use std::rc::Rc;\n");
|
|
2850
|
+
self.write("use std::sync::Arc;\n");
|
|
2851
|
+
}
|
|
2852
|
+
self.write("use tishlang_runtime::{console_debug as tish_console_debug, console_info as tish_console_info, console_log as tish_console_log, console_warn as tish_console_warn, console_error as tish_console_error, boolean as tish_boolean, decode_uri as tish_decode_uri, encode_uri as tish_encode_uri, encode_uri_component as tish_encode_uri_component, decode_uri_component as tish_decode_uri_component, string_escape_html_impl as tish_escape_html, in_operator as tish_in_operator, is_finite as tish_is_finite, is_nan as tish_is_nan, json_parse as tish_json_parse, json_stringify as tish_json_stringify, math_abs as tish_math_abs, math_ceil as tish_math_ceil, math_floor as tish_math_floor, math_max as tish_math_max, math_min as tish_math_min, math_round as tish_math_round, math_sqrt as tish_math_sqrt, parse_float as tish_parse_float, parse_int as tish_parse_int, math_random as tish_math_random, math_pow as tish_math_pow, math_sin as tish_math_sin, math_cos as tish_math_cos, math_tan as tish_math_tan, math_log as tish_math_log, math_exp as tish_math_exp, math_sign as tish_math_sign, math_trunc as tish_math_trunc, math_imul as tish_math_imul, math_sinh as tish_math_sinh, math_cosh as tish_math_cosh, math_tanh as tish_math_tanh, math_asinh as tish_math_asinh, math_acosh as tish_math_acosh, math_atanh as tish_math_atanh, math_cbrt as tish_math_cbrt, math_log2 as tish_math_log2, math_log10 as tish_math_log10, math_expm1 as tish_math_expm1, math_log1p as tish_math_log1p, math_clz32 as tish_math_clz32, math_fround as tish_math_fround, math_hypot as tish_math_hypot, math_atan2 as tish_math_atan2, math_asin as tish_math_asin, math_acos as tish_math_acos, math_atan as tish_math_atan, array_is_array as tish_array_is_array, array_of as tish_array_of, array_from as tish_array_from, object_is as tish_object_is, object_has_own as tish_object_has_own, structured_clone as tish_structured_clone, array_construct as tish_array_construct, string_from_char_code as tish_string_from_char_code, string_convert as tish_string_convert, number_convert as tish_number_convert, number_is_integer as tish_number_is_integer, number_is_safe_integer as tish_number_is_safe_integer, number_is_nan as tish_number_is_nan, number_is_finite as tish_number_is_finite, object_assign as tish_object_assign, object_freeze as tish_object_freeze, object_is_frozen as tish_object_is_frozen, object_keys as tish_object_keys, object_values as tish_object_values, object_entries as tish_object_entries, object_from_entries as tish_object_from_entries, symbol_object as tish_symbol_object, tish_construct, tish_error_constructor, tish_date_constructor, tish_set_constructor, tish_map_constructor, map_get as tish_map_get, map_has as tish_map_has, map_set as tish_map_set, map_values as tish_map_values, tish_float64_array_constructor, tish_float32_array_constructor, tish_int8_array_constructor, tish_uint8_array_constructor, tish_uint8_clamped_array_constructor, tish_int16_array_constructor, tish_uint16_array_constructor, tish_int32_array_constructor, tish_uint32_array_constructor, tish_audio_context_constructor, ObjectMap, ObjectData, PropMap, TishError, Value, VmRef};\n");
|
|
2257
2853
|
if self.program_has_jsx {
|
|
2258
2854
|
self.write("use tishlang_ui::{fragment_value, install_thread_local_host, native_create_root, native_use_state, ui_h, ui_text, HeadlessHost};\n");
|
|
2259
2855
|
}
|
|
@@ -2303,12 +2899,25 @@ impl Codegen {
|
|
|
2303
2899
|
// are stored too, so later annotations like `let x: N = 0` still
|
|
2304
2900
|
// pick up the right native type.
|
|
2305
2901
|
self.collect_type_aliases(&program.statements);
|
|
2902
|
+
// Now that the interface/type-alias table is built, record user functions that return a native
|
|
2903
|
+
// struct, so a typed call to one keeps the result native (`f().field` = a Rust field read).
|
|
2904
|
+
self.collect_user_fn_returns(program);
|
|
2306
2905
|
// Emit a Rust `struct` for every alias whose RHS is an object
|
|
2307
2906
|
// shape. Subsequent `let x: Foo = ...` literals lower to plain
|
|
2308
2907
|
// struct moves (no `VmRef::new(ObjectMap::from(..))` allocation),
|
|
2309
2908
|
// and `x.field` becomes a direct field access.
|
|
2310
2909
|
self.emit_named_struct_decls();
|
|
2311
2910
|
|
|
2911
|
+
// `asset:` imports (Gba only): collect the distinct files (each an i32 sheet handle) and
|
|
2912
|
+
// emit one `mod __asset_F { include_aseprite_inner!("<abspath>"); pub fn sheet() }` block.
|
|
2913
|
+
// The inner macro's `SPRITES` static is private, so the same-module `sheet()` accessor is
|
|
2914
|
+
// how the frames escape the module (a tagless image exposes no public tag). Registration
|
|
2915
|
+
// into the facade arena happens in the entry below, before `run()`.
|
|
2916
|
+
if self.emit_mode == crate::NativeEmitMode::Gba {
|
|
2917
|
+
self.collect_scheme_files(&program.statements);
|
|
2918
|
+
self.emit_scheme_modules()?;
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2312
2921
|
if self.is_async && self.emit_mode == crate::NativeEmitMode::DesktopBin {
|
|
2313
2922
|
self.writeln("#[tokio::main]");
|
|
2314
2923
|
self.writeln("async fn main() {");
|
|
@@ -2331,9 +2940,36 @@ impl Codegen {
|
|
|
2331
2940
|
self.writeln("}");
|
|
2332
2941
|
self.writeln("");
|
|
2333
2942
|
}
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2943
|
+
if self.emit_mode == crate::NativeEmitMode::Gba {
|
|
2944
|
+
// agb runtime entry: hand the peripherals to the facade, run the
|
|
2945
|
+
// program body, then halt. (Async lands in P5 — `run()` is sync here.)
|
|
2946
|
+
self.writeln("#[agb::entry]");
|
|
2947
|
+
self.writeln("fn agb_main(gba: agb::Gba) -> ! {");
|
|
2948
|
+
self.indent += 1;
|
|
2949
|
+
self.writeln("tishlang_runtime::gba::init(gba);");
|
|
2950
|
+
// Register each scheme file into its runtime arena, in first-seen order, so the handle a
|
|
2951
|
+
// tish name binds to (its per-scheme index) matches the arena slot it gets here.
|
|
2952
|
+
let scheme_files = self.scheme_files.clone();
|
|
2953
|
+
for (j, (scheme, _path)) in scheme_files.iter().enumerate() {
|
|
2954
|
+
let emit = self.scheme_target_emit(scheme)?;
|
|
2955
|
+
if !emit.register.is_empty() {
|
|
2956
|
+
let module_ident = Self::scheme_mod_ident(scheme, j);
|
|
2957
|
+
let call = crate::schemes::render_template(&emit.register, "", &module_ident, &self.string_charset);
|
|
2958
|
+
self.writeln(&format!("{};", call));
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
self.writeln("let _ = run();");
|
|
2962
|
+
self.writeln("tishlang_runtime::gba::halt()");
|
|
2963
|
+
self.indent -= 1;
|
|
2964
|
+
self.writeln("}");
|
|
2965
|
+
self.writeln("");
|
|
2966
|
+
}
|
|
2967
|
+
// M5 (default-on via native_opts_enabled; TISH_NATIVE_OPT=0 opts out): emit a parallel
|
|
2968
|
+
// native `fn f_native` for each eligible top-level numeric fn at top level; direct calls
|
|
2969
|
+
// route to it in emit_typed_expr.
|
|
2970
|
+
// Gba mode: these passes emit `thread_local!` and other std-isms; gate them off initially
|
|
2971
|
+
// (they are pure perf — correctness is unaffected). Re-audit for no_std later.
|
|
2972
|
+
if crate::native_opts_enabled() && self.emit_mode != crate::NativeEmitMode::Gba {
|
|
2337
2973
|
self.native_numeric_globals =
|
|
2338
2974
|
Self::collect_native_numeric_globals(&program.statements);
|
|
2339
2975
|
if !self.native_numeric_globals.is_empty() {
|
|
@@ -2378,21 +3014,21 @@ impl Codegen {
|
|
|
2378
3014
|
self.writeln("");
|
|
2379
3015
|
}
|
|
2380
3016
|
}
|
|
2381
|
-
// #177 (S-E/S-F,
|
|
3017
|
+
// #177 (S-E/S-F, default-on via native_opts_enabled): de-virtualize the nbody-shape
|
|
2382
3018
|
// aggregate fns into native Rust free fns operating on an unboxed `Vec<TishStruct_alias>`
|
|
2383
3019
|
// threaded by reference. Computed + emitted here (before `run()`); if any fn can't be
|
|
2384
3020
|
// lowered the whole path is disabled and we fall back to the boxed closures unchanged.
|
|
2385
3021
|
if crate::native_opts_enabled() {
|
|
2386
3022
|
self.setup_aggregate_fns(program);
|
|
2387
3023
|
}
|
|
2388
|
-
// #178 (
|
|
3024
|
+
// #178 (default-on via native_opts_enabled): de-virtualize recursive-struct builders/consumers into
|
|
2389
3025
|
// native free fns over `Option<Box<T>>` structs (the real binary_trees fix). Structural,
|
|
2390
3026
|
// name-independent; emits before `run()` with scratch-buffer rollback so any unsupported
|
|
2391
3027
|
// construct cleanly disables the path and falls back to the boxed closures unchanged.
|
|
2392
3028
|
if crate::native_opts_enabled() {
|
|
2393
3029
|
self.setup_rec_struct_plan(program);
|
|
2394
3030
|
}
|
|
2395
|
-
// #175 (
|
|
3031
|
+
// #175 (default-on via native_opts_enabled): de-virtualize fns over plain `number[]`/`boolean[]` params
|
|
2396
3032
|
// into native free fns threaded by `&/&mut Vec<T>` (spectral_norm / queens). Runs after the
|
|
2397
3033
|
// aggregate path so it can skip any fn that path already claimed; emits before `run()` with
|
|
2398
3034
|
// its own scratch-buffer rollback so a failure leaves the boxed closures untouched.
|
|
@@ -2403,7 +3039,7 @@ impl Codegen {
|
|
|
2403
3039
|
self.inline_fns = Self::collect_inline_fns(program);
|
|
2404
3040
|
self.setup_native_vec_fns(program);
|
|
2405
3041
|
}
|
|
2406
|
-
// #320 (
|
|
3042
|
+
// #320 (default-on via native_opts_enabled): normal fns taking read-only `number[]` params get
|
|
2407
3043
|
// those params unboxed to native owned `Vec<f64>` (boxed body + boxed return otherwise), so
|
|
2408
3044
|
// `seq[i+j]` indexes natively. The SAME pure detection infer reads to keep the caller's array
|
|
2409
3045
|
// `number[]`, so the two never disagree about which args pass as native arrays.
|
|
@@ -2441,7 +3077,9 @@ impl Codegen {
|
|
|
2441
3077
|
}
|
|
2442
3078
|
self.nonneg_locals = self.collect_nonneg_locals(&program.statements);
|
|
2443
3079
|
self.shift_half_of = Self::collect_shift_half_of(&program.statements);
|
|
2444
|
-
|
|
3080
|
+
// Gba mode is sync in P2 (real async lowering is P5); `std::error::Error` in
|
|
3081
|
+
// the signature is rewritten to `core::error::Error` by the no_std post-pass.
|
|
3082
|
+
if self.is_async && self.emit_mode != crate::NativeEmitMode::Gba {
|
|
2445
3083
|
self.writeln("async fn run() -> Result<(), Box<dyn std::error::Error>> {");
|
|
2446
3084
|
} else if self.emit_mode == crate::NativeEmitMode::EmbeddedLib {
|
|
2447
3085
|
self.writeln("pub fn run() -> Result<(), Box<dyn std::error::Error>> {");
|
|
@@ -2545,6 +3183,15 @@ impl Codegen {
|
|
|
2545
3183
|
}
|
|
2546
3184
|
self.writeln("(Arc::from(\"PI\"), Value::Number(std::f64::consts::PI)),");
|
|
2547
3185
|
self.writeln("(Arc::from(\"E\"), Value::Number(std::f64::consts::E)),");
|
|
3186
|
+
// #539: the remaining ECMAScript Math value constants (were missing → `null`).
|
|
3187
|
+
self.writeln("(Arc::from(\"LN2\"), Value::Number(std::f64::consts::LN_2)),");
|
|
3188
|
+
self.writeln("(Arc::from(\"LN10\"), Value::Number(std::f64::consts::LN_10)),");
|
|
3189
|
+
self.writeln("(Arc::from(\"LOG2E\"), Value::Number(std::f64::consts::LOG2_E)),");
|
|
3190
|
+
self.writeln("(Arc::from(\"LOG10E\"), Value::Number(std::f64::consts::LOG10_E)),");
|
|
3191
|
+
self.writeln("(Arc::from(\"SQRT2\"), Value::Number(std::f64::consts::SQRT_2)),");
|
|
3192
|
+
self.writeln(
|
|
3193
|
+
"(Arc::from(\"SQRT1_2\"), Value::Number(std::f64::consts::FRAC_1_SQRT_2)),",
|
|
3194
|
+
);
|
|
2548
3195
|
self.indent -= 1;
|
|
2549
3196
|
self.writeln("]));");
|
|
2550
3197
|
self.writeln("let JSON = Value::object(ObjectMap::from([");
|
|
@@ -2623,6 +3270,12 @@ impl Codegen {
|
|
|
2623
3270
|
self.writeln(
|
|
2624
3271
|
"(Arc::from(\"getOwnPropertyNames\"), Value::native(|args: &[Value]| tish_object_keys(args))),",
|
|
2625
3272
|
);
|
|
3273
|
+
self.writeln(
|
|
3274
|
+
"(Arc::from(\"freeze\"), Value::native(|args: &[Value]| tish_object_freeze(args))),",
|
|
3275
|
+
);
|
|
3276
|
+
self.writeln(
|
|
3277
|
+
"(Arc::from(\"isFrozen\"), Value::native(|args: &[Value]| tish_object_is_frozen(args))),",
|
|
3278
|
+
);
|
|
2626
3279
|
self.writeln(
|
|
2627
3280
|
"(Arc::from(\"keys\"), Value::native(|args: &[Value]| tish_object_keys(args))),",
|
|
2628
3281
|
);
|
|
@@ -2820,6 +3473,14 @@ impl Codegen {
|
|
|
2820
3473
|
self.indent -= 1;
|
|
2821
3474
|
self.writeln("}");
|
|
2822
3475
|
}
|
|
3476
|
+
// Gba mode: rewrite the handful of `std::` paths that survive in emitted
|
|
3477
|
+
// code into their `core::` equivalents (the crate is `#![no_std]`). These
|
|
3478
|
+
// are specific Rust path patterns, not substrings that occur in escaped
|
|
3479
|
+
// user string literals, so a targeted textual pass is safe. Any new site
|
|
3480
|
+
// is caught by the thumbv4t corpus build.
|
|
3481
|
+
if self.emit_mode == crate::NativeEmitMode::Gba {
|
|
3482
|
+
self.output = gba_no_std_rewrite(core::mem::take(&mut self.output));
|
|
3483
|
+
}
|
|
2823
3484
|
Ok(())
|
|
2824
3485
|
}
|
|
2825
3486
|
|
|
@@ -2842,7 +3503,12 @@ impl Codegen {
|
|
|
2842
3503
|
/// unchanged, and `PropMap` dedup + hidden-class shapes key by text, not pointer — so shapes,
|
|
2843
3504
|
/// insertion order, and `Object.keys` are all unaffected. Same inline-`static` idiom as the
|
|
2844
3505
|
/// per-site `PropIC` caches.
|
|
2845
|
-
fn cached_object_key(k: &str) -> String {
|
|
3506
|
+
fn cached_object_key(&self, k: &str) -> String {
|
|
3507
|
+
if self.emit_mode == crate::NativeEmitMode::Gba {
|
|
3508
|
+
// No `OnceLock` on no_std GBA; emit a direct interned key (`Arc` = `Rc`).
|
|
3509
|
+
// Object-literal keys in a hot loop are a later perf item on GBA.
|
|
3510
|
+
return format!("Arc::from({:?})", k);
|
|
3511
|
+
}
|
|
2846
3512
|
format!(
|
|
2847
3513
|
"{{ static K: ::std::sync::OnceLock<::std::sync::Arc<str>> = \
|
|
2848
3514
|
::std::sync::OnceLock::new(); K.get_or_init(|| ::std::sync::Arc::from({:?})).clone() }}",
|
|
@@ -2914,6 +3580,15 @@ impl Codegen {
|
|
|
2914
3580
|
return Ok(code);
|
|
2915
3581
|
}
|
|
2916
3582
|
}
|
|
3583
|
+
// #558: `arr[i].field = v` / `s.field = v` on a typed struct → a native element store, not a
|
|
3584
|
+
// discarded set_prop on a copy. Side-effect position (statement), so no value is produced.
|
|
3585
|
+
if let Expr::MemberAssign { object, prop, value, .. } = expr {
|
|
3586
|
+
if let Some(code) =
|
|
3587
|
+
self.try_emit_native_struct_field_assign(object, prop.as_ref(), value, true)?
|
|
3588
|
+
{
|
|
3589
|
+
return Ok(code);
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
2917
3592
|
match expr {
|
|
2918
3593
|
Expr::Assign { name, value, .. } => {
|
|
2919
3594
|
if self.native_numeric_globals.contains_key(name.as_ref()) {
|
|
@@ -2979,7 +3654,24 @@ impl Codegen {
|
|
|
2979
3654
|
}
|
|
2980
3655
|
}
|
|
2981
3656
|
}
|
|
2982
|
-
|
|
3657
|
+
// Narrow int local (`let hp: u8 = …; hp = hp - 1`): compute the RHS (promoted to
|
|
3658
|
+
// `f64` by the binary emitter) and truncate-cast to the width on store. No
|
|
3659
|
+
// int32-register trick — these are plain storage locals.
|
|
3660
|
+
if rust_type.is_narrow_int() {
|
|
3661
|
+
let escaped = Self::escape_ident(name.as_ref());
|
|
3662
|
+
let native_val = self.emit_coerced_native(value, &rust_type)?;
|
|
3663
|
+
if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
3664
|
+
return Ok(format!(
|
|
3665
|
+
"{{ let _t = {}; *{}.borrow_mut() = _t; }}",
|
|
3666
|
+
native_val, escaped
|
|
3667
|
+
));
|
|
3668
|
+
}
|
|
3669
|
+
return Ok(format!("{} = {}", escaped, native_val));
|
|
3670
|
+
}
|
|
3671
|
+
if matches!(
|
|
3672
|
+
rust_type,
|
|
3673
|
+
RustType::F64 | RustType::Fixed | RustType::Bool | RustType::String
|
|
3674
|
+
) {
|
|
2983
3675
|
if let Expr::Index { object, index, .. } = value.as_ref() {
|
|
2984
3676
|
if let Expr::Ident { name: arr_name, .. } = object.as_ref() {
|
|
2985
3677
|
if self.index_in_bounds(index, arr_name.as_ref()) {
|
|
@@ -2995,6 +3687,11 @@ impl Codegen {
|
|
|
2995
3687
|
let (val_code, val_ty) = self.emit_typed_expr(value)?;
|
|
2996
3688
|
let native_val = if val_ty == RustType::Value {
|
|
2997
3689
|
rust_type.from_value_expr(&val_code)
|
|
3690
|
+
} else if rust_type == RustType::Fixed && val_ty == RustType::F64 {
|
|
3691
|
+
// an f64 (an int/float literal like `nx = 0`, or a promoted int) assigned to a
|
|
3692
|
+
// `fixed` local must be lifted to `Fixed` — the RHS emitter reports numeric
|
|
3693
|
+
// literals as F64, so without this a `fixed` reassignment mistypes (E0308).
|
|
3694
|
+
format!("tishlang_runtime::Fixed::from_raw((({}) * 256.0) as i32)", val_code)
|
|
2998
3695
|
} else {
|
|
2999
3696
|
val_code
|
|
3000
3697
|
};
|
|
@@ -3029,6 +3726,47 @@ impl Codegen {
|
|
|
3029
3726
|
}
|
|
3030
3727
|
// `s += x` etc. in statement position: native f64 compound op, no boxed return.
|
|
3031
3728
|
Expr::CompoundAssign { name, op, value, .. } => {
|
|
3729
|
+
// Narrow int compound-assign (`hp -= 3`): compute in `f64` (JS Number semantics),
|
|
3730
|
+
// truncate-cast back to the width — same model as a narrow-int plain assign, no box.
|
|
3731
|
+
if self.type_context.get_type(name.as_ref()).is_narrow_int() {
|
|
3732
|
+
let width = self.type_context.get_type(name.as_ref()).to_rust_type_str();
|
|
3733
|
+
let n = Self::escape_ident(name.as_ref()).into_owned();
|
|
3734
|
+
let is_refcell = self.refcell_wrapped_vars.contains(name.as_ref());
|
|
3735
|
+
let (rhs_code, rhs_ty) = self.emit_typed_expr(value)?;
|
|
3736
|
+
let rhs_f64 = if rhs_ty == RustType::F64 {
|
|
3737
|
+
rhs_code
|
|
3738
|
+
} else if rhs_ty.is_integer_scalar() {
|
|
3739
|
+
format!("(({}) as f64)", rhs_code)
|
|
3740
|
+
} else {
|
|
3741
|
+
let rhs_val = if rhs_ty.is_native() {
|
|
3742
|
+
rhs_ty.to_value_expr(&rhs_code)
|
|
3743
|
+
} else {
|
|
3744
|
+
rhs_code
|
|
3745
|
+
};
|
|
3746
|
+
format!("(match &({}) {{ Value::Number(n) => *n, v => panic!(\"compound assign: expected number, got {{:?}}\", v) }})", rhs_val)
|
|
3747
|
+
};
|
|
3748
|
+
let op_str = match op {
|
|
3749
|
+
CompoundOp::Add => "+",
|
|
3750
|
+
CompoundOp::Sub => "-",
|
|
3751
|
+
CompoundOp::Mul => "*",
|
|
3752
|
+
CompoundOp::Div => "/",
|
|
3753
|
+
CompoundOp::Mod => "%",
|
|
3754
|
+
};
|
|
3755
|
+
let read = if is_refcell {
|
|
3756
|
+
format!("(*{}.borrow())", n)
|
|
3757
|
+
} else {
|
|
3758
|
+
n.clone()
|
|
3759
|
+
};
|
|
3760
|
+
let new_val =
|
|
3761
|
+
format!("((({}) as f64) {} {}) as {}", read, op_str, rhs_f64, width);
|
|
3762
|
+
if is_refcell {
|
|
3763
|
+
return Ok(format!(
|
|
3764
|
+
"{{ let _t = {}; *{}.borrow_mut() = _t; }}",
|
|
3765
|
+
new_val, n
|
|
3766
|
+
));
|
|
3767
|
+
}
|
|
3768
|
+
return Ok(format!("{} = {}", n, new_val));
|
|
3769
|
+
}
|
|
3032
3770
|
if self.type_context.get_type(name.as_ref()) == RustType::F64 {
|
|
3033
3771
|
let n = Self::escape_ident(name.as_ref());
|
|
3034
3772
|
let is_refcell = self.refcell_wrapped_vars.contains(name.as_ref());
|
|
@@ -5057,10 +5795,10 @@ impl Codegen {
|
|
|
5057
5795
|
// re-boxing each element to `Value::Number` for the loop body.
|
|
5058
5796
|
self.writeln("Value::NumberArray(ref _arr) => {");
|
|
5059
5797
|
self.indent += 1;
|
|
5060
|
-
self.writeln("for _v in _arr.borrow().
|
|
5798
|
+
self.writeln("for _v in _arr.borrow().to_values() {");
|
|
5061
5799
|
self.indent += 1;
|
|
5062
5800
|
self.writeln(&format!(
|
|
5063
|
-
"let {} =
|
|
5801
|
+
"let {} = _v;",
|
|
5064
5802
|
Self::escape_ident(name.as_ref())
|
|
5065
5803
|
));
|
|
5066
5804
|
self.emit_statement(body)?;
|
|
@@ -5843,14 +6581,15 @@ impl Codegen {
|
|
|
5843
6581
|
sibling_escaped, sibling_escaped
|
|
5844
6582
|
));
|
|
5845
6583
|
}
|
|
5846
|
-
// Extract just the parameter names
|
|
6584
|
+
// Extract just the parameter names for capture bookkeeping (typed params get
|
|
6585
|
+
// native shadows below — the M1 block)
|
|
5847
6586
|
let current_param_names: Vec<String> = params
|
|
5848
6587
|
.iter()
|
|
5849
6588
|
.flat_map(|p| p.bound_names())
|
|
5850
6589
|
.map(|n| n.to_string())
|
|
5851
6590
|
.collect();
|
|
5852
6591
|
let formal_span = *span;
|
|
5853
|
-
// M1 (keystone
|
|
6592
|
+
// M1 (keystone; default-on via native_opts_enabled): a typed scalar param
|
|
5854
6593
|
// normally arrives boxed (`args.get(i).cloned()`), which poisons native math in
|
|
5855
6594
|
// the body (e.g. `i*N+k` boxes). Bind a *native shadow* — coerce once to f64/
|
|
5856
6595
|
// bool/String — so the body lowers it like a native local. Conservative: only
|
|
@@ -5893,7 +6632,9 @@ impl Codegen {
|
|
|
5893
6632
|
// `arr[oob]`→NaN) is unreachable for sound inputs but never panics.
|
|
5894
6633
|
self.writeln(&format!(
|
|
5895
6634
|
"let mut {}: Vec<f64> = match args.get({}) {{ \
|
|
5896
|
-
Some(Value::NumberArray(a)) => a.borrow()
|
|
6635
|
+
Some(Value::NumberArray(a)) => match &*a.borrow() {{ \
|
|
6636
|
+
tishlang_runtime::NumArrayBacking::Packed(p) => p.clone(), \
|
|
6637
|
+
tishlang_runtime::NumArrayBacking::Boxed(b) => b.iter().map(|v| match v {{ Value::Number(n) => *n, _ => f64::NAN }}).collect() }}, \
|
|
5897
6638
|
Some(Value::Array(arr)) => arr.borrow().iter().map(|v| match v {{ Value::Number(n) => *n, _ => f64::NAN }}).collect(), \
|
|
5898
6639
|
_ => Vec::new() }};",
|
|
5899
6640
|
Self::escape_ident(tp.name.as_ref()),
|
|
@@ -5913,7 +6654,8 @@ impl Codegen {
|
|
|
5913
6654
|
matches!(
|
|
5914
6655
|
t,
|
|
5915
6656
|
RustType::F64 | RustType::Bool | RustType::String
|
|
5916
|
-
|
|
6657
|
+
| RustType::Fixed | RustType::I32
|
|
6658
|
+
) || t.is_narrow_int()
|
|
5917
6659
|
})
|
|
5918
6660
|
} else {
|
|
5919
6661
|
None
|
|
@@ -6021,6 +6763,21 @@ impl Codegen {
|
|
|
6021
6763
|
for v in &read_only_outer_vars {
|
|
6022
6764
|
self.refcell_wrapped_vars.remove(v);
|
|
6023
6765
|
}
|
|
6766
|
+
// tishlang/tish#556: a PARAMETER (or the rest param) SHADOWS any outer var of the
|
|
6767
|
+
// same name — inside the body it is a plain `let`-bound `Value`/native, never the
|
|
6768
|
+
// outer `VmRef` cell. Drop such names from the wrapped set for the body, so a
|
|
6769
|
+
// reference to the param isn't emitted as `vm_read(¶m)` (which won't compile —
|
|
6770
|
+
// the param is a `Value`, not a `VmRef`). `saved_refcell` restores the outer view
|
|
6771
|
+
// after the body; a body-local `let` that a nested closure captures is re-added by
|
|
6772
|
+
// the `body_cell_vars` prepass below, so its cell handling is unaffected.
|
|
6773
|
+
for p in params {
|
|
6774
|
+
for bn in p.bound_names() {
|
|
6775
|
+
self.refcell_wrapped_vars.remove(bn.as_ref());
|
|
6776
|
+
}
|
|
6777
|
+
}
|
|
6778
|
+
if let Some(rest) = rest_param {
|
|
6779
|
+
self.refcell_wrapped_vars.remove(rest.name.as_ref());
|
|
6780
|
+
}
|
|
6024
6781
|
|
|
6025
6782
|
// Pre-scan body for nested functions (handles function body as Block)
|
|
6026
6783
|
if let Statement::Block { statements, .. } = body.as_ref() {
|
|
@@ -6744,6 +7501,12 @@ impl Codegen {
|
|
|
6744
7501
|
}
|
|
6745
7502
|
}
|
|
6746
7503
|
}
|
|
7504
|
+
// Static-type-aware reflection: `Object.keys/values/entries(s)` on a statically-known
|
|
7505
|
+
// struct → native field reads, never boxing the struct or building an intermediate
|
|
7506
|
+
// object. Falls through to the generic boxed `Object.*` path for a non-struct arg.
|
|
7507
|
+
if let Some(code) = self.try_emit_native_object_reflection(callee, args)? {
|
|
7508
|
+
return Ok(code);
|
|
7509
|
+
}
|
|
6747
7510
|
// #178: route a boxed-context call to a recursive-struct consumer
|
|
6748
7511
|
// (`sum(build(n))` / `sum(node)`) → `Value::Number(sum_rec(&..))`.
|
|
6749
7512
|
if self.rec_struct_plan.is_some() {
|
|
@@ -6775,29 +7538,273 @@ impl Codegen {
|
|
|
6775
7538
|
// ── native Vec<T> push fast path ──────────────────────────────
|
|
6776
7539
|
if method_name.as_ref() == "push" {
|
|
6777
7540
|
if let Expr::Ident { name, .. } = object.as_ref() {
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
7541
|
+
let obj_type = self.type_context.get_type(name.as_ref());
|
|
7542
|
+
if let RustType::Vec(elem_type) = obj_type {
|
|
7543
|
+
let esc_obj = Self::escape_ident(name.as_ref()).into_owned();
|
|
7544
|
+
// A captured (refcell-wrapped) native Vec pushes through
|
|
7545
|
+
// `(*a.borrow_mut())` — WITHOUT this it fell to the boxed `array_push`,
|
|
7546
|
+
// which boxed the WHOLE Vec into a throwaway `Value::Array` (discarding
|
|
7547
|
+
// the push, O(n), and — for a `Vec<struct>` — E0282 on the `from_struct`
|
|
7548
|
+
// boxing closure since the native push never ran to pin the element type).
|
|
7549
|
+
let base = if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
7550
|
+
format!("(*{}.borrow_mut())", esc_obj)
|
|
7551
|
+
} else {
|
|
7552
|
+
esc_obj
|
|
7553
|
+
};
|
|
7554
|
+
// Collect push arguments as native values.
|
|
7555
|
+
let mut push_stmts: Vec<String> = Vec::new();
|
|
7556
|
+
for a in args {
|
|
7557
|
+
if let CallArg::Expr(e) = a {
|
|
7558
|
+
// Emit the element AGAINST the Vec's element type so an object
|
|
7559
|
+
// literal hits the struct-literal fast path (direct `Struct { … }`
|
|
7560
|
+
// with each field value emitted natively — zero allocations)
|
|
7561
|
+
// instead of building a boxed `object_from_pairs` and then
|
|
7562
|
+
// `get_prop`-ing every field back out. emit_native_expr falls
|
|
7563
|
+
// back to the typed/value path for non-literal args.
|
|
7564
|
+
let native_val = self.emit_native_expr(e, elem_type.as_ref())?;
|
|
7565
|
+
push_stmts.push(format!("{}.push({});", base, native_val));
|
|
6795
7566
|
}
|
|
6796
|
-
return Ok(format!(
|
|
6797
|
-
"{{ {} Value::Null }}",
|
|
6798
|
-
push_stmts.join(" ")
|
|
6799
|
-
));
|
|
6800
7567
|
}
|
|
7568
|
+
// `push` returns the new length (JS / tish interp) — a plain read, no
|
|
7569
|
+
// O(n) boxing. A captured Vec reads its length through `(*a.borrow())`.
|
|
7570
|
+
let base_read = if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
7571
|
+
format!("(*{}.borrow())", Self::escape_ident(name.as_ref()))
|
|
7572
|
+
} else {
|
|
7573
|
+
Self::escape_ident(name.as_ref()).into_owned()
|
|
7574
|
+
};
|
|
7575
|
+
return Ok(format!(
|
|
7576
|
+
"{{ {} Value::Number({}.len() as f64) }}",
|
|
7577
|
+
push_stmts.join(" "),
|
|
7578
|
+
base_read
|
|
7579
|
+
));
|
|
7580
|
+
}
|
|
7581
|
+
}
|
|
7582
|
+
}
|
|
7583
|
+
|
|
7584
|
+
// Native in-place mutators on a native `Vec` (`pop`/`shift`/`unshift`/`reverse`).
|
|
7585
|
+
// The boxed `array_*` fallback operates on a THROWAWAY boxed copy of the Vec, so the
|
|
7586
|
+
// mutation is DISCARDED (O(n), and — for a `Vec<struct>` — an E0282 on the boxing
|
|
7587
|
+
// closure); these mutate the real Vec. A captured Vec goes through `(*a.borrow_mut())`.
|
|
7588
|
+
// Return values match JS / tish interp: pop/shift yield the removed element (boxed) or
|
|
7589
|
+
// Null; unshift yields the new length; reverse yields THE array (boxed copy — a native
|
|
7590
|
+
// `Vec` has no O(1) by-ref boxing like interp's Rc-array, so the contents match but it
|
|
7591
|
+
// is not aliased; reverse is O(n) anyway so the copy is asymptotically free).
|
|
7592
|
+
if matches!(method_name.as_ref(), "pop" | "shift" | "unshift" | "reverse") {
|
|
7593
|
+
if let Expr::Ident { name, .. } = object.as_ref() {
|
|
7594
|
+
if let RustType::Vec(elem_type) =
|
|
7595
|
+
self.type_context.get_type(name.as_ref())
|
|
7596
|
+
{
|
|
7597
|
+
let esc = Self::escape_ident(name.as_ref()).into_owned();
|
|
7598
|
+
let wrapped_v = self.refcell_wrapped_vars.contains(name.as_ref());
|
|
7599
|
+
let base = if wrapped_v {
|
|
7600
|
+
format!("(*{}.borrow_mut())", esc)
|
|
7601
|
+
} else {
|
|
7602
|
+
esc.clone()
|
|
7603
|
+
};
|
|
7604
|
+
// Shared-borrow read (for `reverse`'s result boxing / length reads).
|
|
7605
|
+
let base_read = if wrapped_v {
|
|
7606
|
+
format!("(*{}.borrow())", esc)
|
|
7607
|
+
} else {
|
|
7608
|
+
esc.clone()
|
|
7609
|
+
};
|
|
7610
|
+
// Box a removed element back to a `Value` (`Value` elements pass through).
|
|
7611
|
+
let box_elem = |v: &str| -> String {
|
|
7612
|
+
if matches!(elem_type.as_ref(), RustType::Value) {
|
|
7613
|
+
v.to_string()
|
|
7614
|
+
} else {
|
|
7615
|
+
elem_type.to_value_expr(v)
|
|
7616
|
+
}
|
|
7617
|
+
};
|
|
7618
|
+
let code = match method_name.as_ref() {
|
|
7619
|
+
"pop" => format!(
|
|
7620
|
+
"{{ match {b}.pop() {{ Some(__e) => {v}, None => Value::Null }} }}",
|
|
7621
|
+
b = base,
|
|
7622
|
+
v = box_elem("__e")
|
|
7623
|
+
),
|
|
7624
|
+
"shift" => format!(
|
|
7625
|
+
"{{ if {b}.is_empty() {{ Value::Null }} else {{ let __e = {b}.remove(0); {v} }} }}",
|
|
7626
|
+
b = base,
|
|
7627
|
+
v = box_elem("__e")
|
|
7628
|
+
),
|
|
7629
|
+
"unshift" => {
|
|
7630
|
+
// Bind items to temps BEFORE any `borrow_mut` insert (an item may
|
|
7631
|
+
// read the same array), then insert at the front in REVERSE so the
|
|
7632
|
+
// argument order is preserved. Returns the new length.
|
|
7633
|
+
let mut binds = String::new();
|
|
7634
|
+
let mut names: Vec<String> = Vec::new();
|
|
7635
|
+
for (i, a) in args.iter().enumerate() {
|
|
7636
|
+
if let CallArg::Expr(e) = a {
|
|
7637
|
+
let it = self.emit_native_expr(e, elem_type.as_ref())?;
|
|
7638
|
+
binds.push_str(&format!("let __u{} = {}; ", i, it));
|
|
7639
|
+
names.push(format!("__u{}", i));
|
|
7640
|
+
}
|
|
7641
|
+
}
|
|
7642
|
+
let mut ins = String::new();
|
|
7643
|
+
for n in names.iter().rev() {
|
|
7644
|
+
ins.push_str(&format!("{}.insert(0, {}); ", base, n));
|
|
7645
|
+
}
|
|
7646
|
+
format!(
|
|
7647
|
+
"{{ {}{}Value::Number({}.len() as f64) }}",
|
|
7648
|
+
binds, ins, base
|
|
7649
|
+
)
|
|
7650
|
+
}
|
|
7651
|
+
// reverse in place, then return a boxed copy of the array (contents
|
|
7652
|
+
// match interp; see the note above).
|
|
7653
|
+
"reverse" => format!(
|
|
7654
|
+
"{{ {}.reverse(); Value::Array(VmRef::new({}.iter().cloned().map(|__e| {}).collect())) }}",
|
|
7655
|
+
base,
|
|
7656
|
+
base_read,
|
|
7657
|
+
box_elem("__e")
|
|
7658
|
+
),
|
|
7659
|
+
_ => unreachable!("method checked above"),
|
|
7660
|
+
};
|
|
7661
|
+
return Ok(code);
|
|
7662
|
+
}
|
|
7663
|
+
}
|
|
7664
|
+
}
|
|
7665
|
+
|
|
7666
|
+
// Native `splice`/`fill`/`copyWithin` on a native `Vec`. Like the mutators above,
|
|
7667
|
+
// the boxed fallback would run on a THROWAWAY copy (mutation discarded). These need
|
|
7668
|
+
// several accesses (len + index math + a range op), so a captured Vec binds ONE
|
|
7669
|
+
// `borrow_mut` guard `__g` for the whole body; every argument is evaluated BEFORE the
|
|
7670
|
+
// guard (an arg may read the same array, which would double-borrow the cell). JS index
|
|
7671
|
+
// clamping (negative → from end, out-of-range → clamped) is emitted inline so the
|
|
7672
|
+
// result matches the interpreter.
|
|
7673
|
+
if matches!(method_name.as_ref(), "splice" | "fill" | "copyWithin") {
|
|
7674
|
+
if let Expr::Ident { name, .. } = object.as_ref() {
|
|
7675
|
+
if let RustType::Vec(elem_type) =
|
|
7676
|
+
self.type_context.get_type(name.as_ref())
|
|
7677
|
+
{
|
|
7678
|
+
let esc = Self::escape_ident(name.as_ref()).into_owned();
|
|
7679
|
+
let wrapped = self.refcell_wrapped_vars.contains(name.as_ref());
|
|
7680
|
+
// `p` is the Vec place inside the body: a `borrow_mut` guard when wrapped.
|
|
7681
|
+
let (guard, p) = if wrapped {
|
|
7682
|
+
(format!("let mut __g = {}.borrow_mut(); ", esc), "__g".to_string())
|
|
7683
|
+
} else {
|
|
7684
|
+
(String::new(), esc.clone())
|
|
7685
|
+
};
|
|
7686
|
+
let elem_rust = elem_type.to_rust_type_str();
|
|
7687
|
+
let box_elem = |v: &str| -> String {
|
|
7688
|
+
if matches!(elem_type.as_ref(), RustType::Value) {
|
|
7689
|
+
v.to_string()
|
|
7690
|
+
} else {
|
|
7691
|
+
elem_type.to_value_expr(v)
|
|
7692
|
+
}
|
|
7693
|
+
};
|
|
7694
|
+
// `fill`/`copyWithin` return THE array (JS / tish interp) — a boxed copy
|
|
7695
|
+
// built from the (still-borrowed) place `p` after the mutation. Contents
|
|
7696
|
+
// match interp; not aliased (a native `Vec` has no O(1) by-ref boxing).
|
|
7697
|
+
let arr_box = format!(
|
|
7698
|
+
"Value::Array(VmRef::new({}.iter().cloned().map(|__be| {}).collect()))",
|
|
7699
|
+
p,
|
|
7700
|
+
box_elem("__be")
|
|
7701
|
+
);
|
|
7702
|
+
// f64 arg (bound before the guard) → a clamped `usize` index.
|
|
7703
|
+
let clamp = |v: &str| -> String {
|
|
7704
|
+
format!(
|
|
7705
|
+
"(if {v} < 0.0 {{ (__lenf + {v}).max(0.0) }} else {{ {v}.min(__lenf) }}) as usize"
|
|
7706
|
+
)
|
|
7707
|
+
};
|
|
7708
|
+
let argf = |cg: &mut Self, i: usize| -> Result<Option<String>, CompileError> {
|
|
7709
|
+
match args.get(i) {
|
|
7710
|
+
Some(CallArg::Expr(e)) => {
|
|
7711
|
+
Ok(Some(cg.emit_native_expr(e, &RustType::F64)?))
|
|
7712
|
+
}
|
|
7713
|
+
_ => Ok(None),
|
|
7714
|
+
}
|
|
7715
|
+
};
|
|
7716
|
+
let code = match method_name.as_ref() {
|
|
7717
|
+
"splice" => {
|
|
7718
|
+
let start = argf(self, 0)?.unwrap_or_else(|| "0.0".into());
|
|
7719
|
+
let dc = argf(self, 1)?;
|
|
7720
|
+
// Items to insert (args[2..]) — bound before the guard.
|
|
7721
|
+
let mut item_binds = String::new();
|
|
7722
|
+
let mut item_names: Vec<String> = Vec::new();
|
|
7723
|
+
for (i, a) in args.iter().enumerate().skip(2) {
|
|
7724
|
+
if let CallArg::Expr(e) = a {
|
|
7725
|
+
let it = self.emit_native_expr(e, elem_type.as_ref())?;
|
|
7726
|
+
item_binds.push_str(&format!("let __it{} = {}; ", i, it));
|
|
7727
|
+
item_names.push(format!("__it{}", i));
|
|
7728
|
+
}
|
|
7729
|
+
}
|
|
7730
|
+
let dc_bind = dc
|
|
7731
|
+
.as_ref()
|
|
7732
|
+
.map(|d| format!("let __dc = {}; ", d))
|
|
7733
|
+
.unwrap_or_default();
|
|
7734
|
+
let del_calc = if dc.is_some() {
|
|
7735
|
+
"let __del = if __dc < 0.0 { 0usize } else { (__dc as usize).min(__len - __start) };"
|
|
7736
|
+
} else {
|
|
7737
|
+
"let __del = __len - __start;"
|
|
7738
|
+
};
|
|
7739
|
+
format!(
|
|
7740
|
+
"{{ let __s0 = {start}; {dc_bind}{item_binds}{guard}\
|
|
7741
|
+
let __len = {p}.len(); let __lenf = __len as f64; \
|
|
7742
|
+
let __start = {start_clamp}; {del_calc} \
|
|
7743
|
+
let __items: Vec<{elem_rust}> = vec![{items}]; \
|
|
7744
|
+
let __removed: Vec<{elem_rust}> = {p}.splice(__start..__start + __del, __items).collect(); \
|
|
7745
|
+
Value::Array(VmRef::new(__removed.into_iter().map(|__e| {boxed}).collect())) }}",
|
|
7746
|
+
start_clamp = clamp("__s0"),
|
|
7747
|
+
items = item_names.join(", "),
|
|
7748
|
+
boxed = box_elem("__e"),
|
|
7749
|
+
)
|
|
7750
|
+
}
|
|
7751
|
+
"fill" => {
|
|
7752
|
+
// fill(value, start=0, end=len)
|
|
7753
|
+
let val = match args.first() {
|
|
7754
|
+
Some(CallArg::Expr(e)) => {
|
|
7755
|
+
self.emit_native_expr(e, elem_type.as_ref())?
|
|
7756
|
+
}
|
|
7757
|
+
_ => elem_type.default_value(),
|
|
7758
|
+
};
|
|
7759
|
+
let start = argf(self, 1)?.unwrap_or_else(|| "0.0".into());
|
|
7760
|
+
let end = argf(self, 2)?;
|
|
7761
|
+
format!(
|
|
7762
|
+
"{{ let __v = {val}; let __s0 = {start}; {end}{guard}\
|
|
7763
|
+
let __len = {p}.len(); let __lenf = __len as f64; \
|
|
7764
|
+
let __start = {start_clamp}; {end_clamp}\
|
|
7765
|
+
let mut __i = __start; while __i < __end {{ {p}[__i] = __v.clone(); __i += 1; }} \
|
|
7766
|
+
{arr_box} }}",
|
|
7767
|
+
end = end
|
|
7768
|
+
.as_ref()
|
|
7769
|
+
.map(|e| format!("let __e0 = {}; ", e))
|
|
7770
|
+
.unwrap_or_default(),
|
|
7771
|
+
start_clamp = clamp("__s0"),
|
|
7772
|
+
end_clamp = end
|
|
7773
|
+
.as_ref()
|
|
7774
|
+
.map(|_| format!("let __end = {}; ", clamp("__e0")))
|
|
7775
|
+
.unwrap_or_else(|| "let __end = __len; ".into()),
|
|
7776
|
+
arr_box = arr_box,
|
|
7777
|
+
)
|
|
7778
|
+
}
|
|
7779
|
+
"copyWithin" => {
|
|
7780
|
+
// copyWithin(target, start=0, end=len) — overlap-safe via a temp.
|
|
7781
|
+
let target = argf(self, 0)?.unwrap_or_else(|| "0.0".into());
|
|
7782
|
+
let start = argf(self, 1)?.unwrap_or_else(|| "0.0".into());
|
|
7783
|
+
let end = argf(self, 2)?;
|
|
7784
|
+
format!(
|
|
7785
|
+
"{{ let __t0 = {target}; let __s0 = {start}; {end_bind}{guard}\
|
|
7786
|
+
let __len = {p}.len(); let __lenf = __len as f64; \
|
|
7787
|
+
let __target = {target_clamp}; let __start = {start_clamp}; {end_clamp}\
|
|
7788
|
+
let __count = if __end > __start {{ (__end - __start).min(__len - __target) }} else {{ 0 }}; \
|
|
7789
|
+
let __tmp: Vec<{elem_rust}> = {p}[__start..__start + __count].to_vec(); \
|
|
7790
|
+
let mut __k = 0usize; for __val in __tmp {{ {p}[__target + __k] = __val; __k += 1; }} \
|
|
7791
|
+
{arr_box} }}",
|
|
7792
|
+
end_bind = end
|
|
7793
|
+
.as_ref()
|
|
7794
|
+
.map(|e| format!("let __e0 = {}; ", e))
|
|
7795
|
+
.unwrap_or_default(),
|
|
7796
|
+
target_clamp = clamp("__t0"),
|
|
7797
|
+
start_clamp = clamp("__s0"),
|
|
7798
|
+
end_clamp = end
|
|
7799
|
+
.as_ref()
|
|
7800
|
+
.map(|_| format!("let __end = {}; ", clamp("__e0")))
|
|
7801
|
+
.unwrap_or_else(|| "let __end = __len; ".into()),
|
|
7802
|
+
arr_box = arr_box,
|
|
7803
|
+
)
|
|
7804
|
+
}
|
|
7805
|
+
_ => unreachable!("method checked above"),
|
|
7806
|
+
};
|
|
7807
|
+
return Ok(code);
|
|
6801
7808
|
}
|
|
6802
7809
|
}
|
|
6803
7810
|
}
|
|
@@ -6810,35 +7817,122 @@ impl Codegen {
|
|
|
6810
7817
|
// sorts via the runtime, and writes the result back into the Vec (correct).
|
|
6811
7818
|
if method_name.as_ref() == "sort" {
|
|
6812
7819
|
if let Expr::Ident { name, .. } = object.as_ref() {
|
|
6813
|
-
if
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
{
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
7820
|
+
if let RustType::Vec(elem_type) =
|
|
7821
|
+
self.type_context.get_type(name.as_ref())
|
|
7822
|
+
{
|
|
7823
|
+
if matches!(elem_type.as_ref(), RustType::Named { .. }) {
|
|
7824
|
+
let esc = Self::escape_ident(name.as_ref()).into_owned();
|
|
7825
|
+
// A captured (refcell-wrapped) Vec sorts through the shared cell:
|
|
7826
|
+
// `base_mut` for the in-place `sort_by`, `base_read` for the boxing
|
|
7827
|
+
// scan, `assign` for the general-comparator write-back.
|
|
7828
|
+
let wrapped = self.refcell_wrapped_vars.contains(name.as_ref());
|
|
7829
|
+
let base_mut = if wrapped {
|
|
7830
|
+
format!("(*{}.borrow_mut())", esc)
|
|
7831
|
+
} else {
|
|
7832
|
+
esc.clone()
|
|
7833
|
+
};
|
|
7834
|
+
let base_read = if wrapped {
|
|
7835
|
+
format!("(*{}.borrow())", esc)
|
|
7836
|
+
} else {
|
|
7837
|
+
esc.clone()
|
|
7838
|
+
};
|
|
7839
|
+
let assign = if wrapped {
|
|
7840
|
+
format!("*{}.borrow_mut()", esc)
|
|
7841
|
+
} else {
|
|
7842
|
+
esc.clone()
|
|
7843
|
+
};
|
|
7844
|
+
// `sort` returns THE array (JS / tish interp) — a boxed copy of the
|
|
7845
|
+
// now-sorted Vec (contents match interp; not aliased). `sort` is
|
|
7846
|
+
// O(n log n), so the O(n) box is asymptotically free.
|
|
7847
|
+
let sort_box = format!(
|
|
7848
|
+
"Value::Array(VmRef::new({}.iter().cloned().map(|__se| {}).collect()))",
|
|
7849
|
+
base_read,
|
|
7850
|
+
elem_type.to_value_expr("__se")
|
|
7851
|
+
);
|
|
7852
|
+
if let Some(CallArg::Expr(cmp)) = args.first() {
|
|
7853
|
+
if let Some(keys) =
|
|
7854
|
+
Self::detect_lexicographic_key_comparator(cmp)
|
|
7855
|
+
{
|
|
7856
|
+
if keys.iter().all(|(p, _)| p.len() == 1) {
|
|
7857
|
+
let chain = Self::render_struct_sort_chain(&keys);
|
|
7858
|
+
return Ok(format!(
|
|
7859
|
+
"{{ {base_mut}.sort_by(|a, b| {chain}); {sort_box} }}"
|
|
7860
|
+
));
|
|
6829
7861
|
}
|
|
6830
|
-
// General comparator: box → sort → write back (correct).
|
|
6831
|
-
let cmp_code = self.emit_expr(cmp)?;
|
|
6832
|
-
let to_v = elem_type.to_value_expr("__e");
|
|
6833
|
-
let from_v = elem_type.from_value_expr("__v");
|
|
6834
|
-
return Ok(format!(
|
|
6835
|
-
"{{ let __sv: Vec<Value> = {esc}.iter().map(|__e| {to_v}).collect(); \
|
|
6836
|
-
let __sorted = tishlang_runtime::array_sort(&Value::Array(VmRef::new(__sv)), Some(&({cmp_code}))); \
|
|
6837
|
-
if let Value::Array(__a) = __sorted {{ {esc} = __a.borrow().iter().map(|__v| {from_v}).collect(); }} \
|
|
6838
|
-
Value::Null }}"
|
|
6839
|
-
));
|
|
6840
7862
|
}
|
|
7863
|
+
// General comparator: box → sort → write back (correct).
|
|
7864
|
+
let cmp_code = self.emit_expr(cmp)?;
|
|
7865
|
+
let to_v = elem_type.to_value_expr("__e");
|
|
7866
|
+
let from_v = elem_type.from_value_expr("__v");
|
|
7867
|
+
return Ok(format!(
|
|
7868
|
+
"{{ let __sv: Vec<Value> = {base_read}.iter().map(|__e| {to_v}).collect(); \
|
|
7869
|
+
let __sorted = tishlang_runtime::array_sort(&Value::Array(VmRef::new(__sv)), Some(&({cmp_code}))); \
|
|
7870
|
+
if let Value::Array(__a) = __sorted {{ {assign} = __a.borrow().iter().map(|__v| {from_v}).collect(); }} \
|
|
7871
|
+
{sort_box} }}"
|
|
7872
|
+
));
|
|
6841
7873
|
}
|
|
7874
|
+
} else {
|
|
7875
|
+
// #568: a scalar / `Value` native `Vec` (`number[]`, `string[]`,
|
|
7876
|
+
// …) sorts IN PLACE like interp/vm/JS. Box a copy, sort it via the
|
|
7877
|
+
// SAME runtime fn the boxed path uses (so order is byte-identical),
|
|
7878
|
+
// then WRITE the ordered elements BACK into the native Vec — the
|
|
7879
|
+
// generic boxed dispatch below sorted a throwaway copy and dropped
|
|
7880
|
+
// the write (a silent no-op; only `arr = arr.sort()` worked).
|
|
7881
|
+
// Returns the sorted array (JS / interp). `sort` marks the receiver
|
|
7882
|
+
// assigned (see `collect_assigned_idents_in_expr`), so the Vec is
|
|
7883
|
+
// `mut` and the write-back compiles.
|
|
7884
|
+
let esc = Self::escape_ident(name.as_ref()).into_owned();
|
|
7885
|
+
let wrapped =
|
|
7886
|
+
self.refcell_wrapped_vars.contains(name.as_ref());
|
|
7887
|
+
let base_read = if wrapped {
|
|
7888
|
+
format!("(*{}.borrow())", esc)
|
|
7889
|
+
} else {
|
|
7890
|
+
esc.clone()
|
|
7891
|
+
};
|
|
7892
|
+
let assign = if wrapped {
|
|
7893
|
+
format!("*{}.borrow_mut()", esc)
|
|
7894
|
+
} else {
|
|
7895
|
+
esc.clone()
|
|
7896
|
+
};
|
|
7897
|
+
let to_v = elem_type.to_value_expr("__e");
|
|
7898
|
+
let from_v = elem_type.from_value_expr("__v");
|
|
7899
|
+
// Mirror the boxed path's comparator detection EXACTLY (numeric
|
|
7900
|
+
// fast path → lexicographic keys → general callback → default),
|
|
7901
|
+
// so the sorted order matches the generic path and interp/vm.
|
|
7902
|
+
let sort_call = if let Some(CallArg::Expr(cmp)) = args.first() {
|
|
7903
|
+
if let Some(ascending) =
|
|
7904
|
+
Self::detect_numeric_sort_comparator(cmp)
|
|
7905
|
+
{
|
|
7906
|
+
if ascending {
|
|
7907
|
+
"tishlang_runtime::array_sort_numeric_asc(&__boxed)"
|
|
7908
|
+
.to_string()
|
|
7909
|
+
} else {
|
|
7910
|
+
"tishlang_runtime::array_sort_numeric_desc(&__boxed)"
|
|
7911
|
+
.to_string()
|
|
7912
|
+
}
|
|
7913
|
+
} else if let Some(keys) =
|
|
7914
|
+
Self::detect_lexicographic_key_comparator(cmp)
|
|
7915
|
+
{
|
|
7916
|
+
format!(
|
|
7917
|
+
"tishlang_runtime::array_sort_by_keys(&__boxed, {})",
|
|
7918
|
+
Self::render_sort_keys(&keys)
|
|
7919
|
+
)
|
|
7920
|
+
} else {
|
|
7921
|
+
let cmp_code = self.emit_expr(cmp)?;
|
|
7922
|
+
format!(
|
|
7923
|
+
"tishlang_runtime::array_sort(&__boxed, Some(&({})))",
|
|
7924
|
+
cmp_code
|
|
7925
|
+
)
|
|
7926
|
+
}
|
|
7927
|
+
} else {
|
|
7928
|
+
"tishlang_runtime::array_sort(&__boxed, None)".to_string()
|
|
7929
|
+
};
|
|
7930
|
+
return Ok(format!(
|
|
7931
|
+
"{{ let __boxed = Value::Array(VmRef::new({base_read}.iter().cloned().map(|__e| {to_v}).collect())); \
|
|
7932
|
+
let __sorted = {sort_call}; \
|
|
7933
|
+
if let Value::Array(__a) = &__sorted {{ {assign} = __a.borrow().iter().map(|__v| {from_v}).collect(); }} \
|
|
7934
|
+
__sorted }}"
|
|
7935
|
+
));
|
|
6842
7936
|
}
|
|
6843
7937
|
}
|
|
6844
7938
|
}
|
|
@@ -7261,7 +8355,7 @@ impl Codegen {
|
|
|
7261
8355
|
}
|
|
7262
8356
|
"reduce" => {
|
|
7263
8357
|
let initial = arg_exprs.get(1).cloned().unwrap_or_else(|| "Value::Null".to_string());
|
|
7264
|
-
// Fused reduce (
|
|
8358
|
+
// Fused reduce (default-on via native_opts_enabled): `arr.reduce((acc, x) => acc OP x, init)`
|
|
7265
8359
|
// with a plain binop of the two params → a native fold using the SAME
|
|
7266
8360
|
// runtime Value op the closure body would, eliminating the per-element
|
|
7267
8361
|
// `value_call`. Sound (identical Value semantics, incl. string `+`).
|
|
@@ -7463,6 +8557,41 @@ impl Codegen {
|
|
|
7463
8557
|
}
|
|
7464
8558
|
}
|
|
7465
8559
|
|
|
8560
|
+
// Typed externs (#perf): a call to a native import with a declared typed signature →
|
|
8561
|
+
// a DIRECT `<crate>::name_typed(coerced args)` call. No `Value` boxing of the args, no
|
|
8562
|
+
// `value_call` dispatch through the namespace object. Falls through to the boxed path
|
|
8563
|
+
// when the arg count/kind doesn't match (a dynamic/spread call stays correct).
|
|
8564
|
+
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
8565
|
+
if let Some(sig) = self.extern_fns.get(fname.as_ref()).cloned() {
|
|
8566
|
+
if args.len() == sig.params.len()
|
|
8567
|
+
&& args.iter().all(|a| matches!(a, CallArg::Expr(_)))
|
|
8568
|
+
{
|
|
8569
|
+
let mut argc: Vec<String> = Vec::with_capacity(args.len());
|
|
8570
|
+
for (a, ty) in args.iter().zip(sig.params.iter()) {
|
|
8571
|
+
if let CallArg::Expr(e) = a {
|
|
8572
|
+
argc.push(self.emit_coerced_native(e, ty)?);
|
|
8573
|
+
}
|
|
8574
|
+
}
|
|
8575
|
+
let call = format!(
|
|
8576
|
+
"{}::{}_typed({})",
|
|
8577
|
+
sig.crate_name,
|
|
8578
|
+
sig.symbol,
|
|
8579
|
+
argc.join(", ")
|
|
8580
|
+
);
|
|
8581
|
+
// This is a Value-producing context: a `void` extern yields `Null`; a
|
|
8582
|
+
// native return is boxed once at the boundary (still far cheaper than the
|
|
8583
|
+
// boxed-args `value_call`); a `Value`-returning extern passes through.
|
|
8584
|
+
return Ok(if sig.ret == RustType::Unit {
|
|
8585
|
+
format!("{{ {}; Value::Null }}", call)
|
|
8586
|
+
} else if sig.ret.is_native() {
|
|
8587
|
+
sig.ret.to_value_expr(&call)
|
|
8588
|
+
} else {
|
|
8589
|
+
call
|
|
8590
|
+
});
|
|
8591
|
+
}
|
|
8592
|
+
}
|
|
8593
|
+
}
|
|
8594
|
+
|
|
7466
8595
|
// M5: boxed-context call to an eligible native fn → `Value::Number(name_native(..))`.
|
|
7467
8596
|
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
7468
8597
|
if self.native_fns.contains(fname.as_ref()) {
|
|
@@ -7541,17 +8670,25 @@ impl Codegen {
|
|
|
7541
8670
|
fields.iter().find(|(k, _)| k.as_ref() == prop_name.as_ref())
|
|
7542
8671
|
{
|
|
7543
8672
|
let var_esc = Self::escape_ident(var_name.as_ref()).into_owned();
|
|
8673
|
+
// Clone a non-Copy field so reading it doesn't move out of the local
|
|
8674
|
+
// (E0382 when the struct is used again); a Copy scalar reads by value.
|
|
8675
|
+
let clone = if field_ty.is_copy() { "" } else { ".clone()" };
|
|
7544
8676
|
let access = if self.refcell_wrapped_vars.contains(var_name.as_ref()) {
|
|
8677
|
+
// #567: scope the borrow to its own block so the lock drops
|
|
8678
|
+
// immediately — a `send-values` `Arc<Mutex>` VmRef is NOT
|
|
8679
|
+
// reentrant, so two `.borrow()`s of the same cell alive at once
|
|
8680
|
+
// (e.g. `s.a + s.b` in one expression) would deadlock.
|
|
7545
8681
|
format!(
|
|
7546
|
-
"
|
|
8682
|
+
"{{ let __bg = {}.borrow(); (*__bg).{}.clone() }}",
|
|
7547
8683
|
var_esc,
|
|
7548
8684
|
crate::types::field_ident(prop_name.as_ref())
|
|
7549
8685
|
)
|
|
7550
8686
|
} else {
|
|
7551
8687
|
format!(
|
|
7552
|
-
"{}.{}",
|
|
8688
|
+
"{}.{}{}",
|
|
7553
8689
|
var_esc,
|
|
7554
|
-
crate::types::field_ident(prop_name.as_ref())
|
|
8690
|
+
crate::types::field_ident(prop_name.as_ref()),
|
|
8691
|
+
clone
|
|
7555
8692
|
)
|
|
7556
8693
|
};
|
|
7557
8694
|
// Caller expects a `Value`; wrap.
|
|
@@ -7563,24 +8700,90 @@ impl Codegen {
|
|
|
7563
8700
|
// Generalize the typed struct-field fast path to `xs[i].field` (array-of-structs):
|
|
7564
8701
|
// when `object` indexes a `Vec<Named>`, do native struct field access.
|
|
7565
8702
|
if !optional {
|
|
7566
|
-
if let (
|
|
7567
|
-
|
|
8703
|
+
if let (
|
|
8704
|
+
Expr::Index { object: io, index: ii, .. },
|
|
8705
|
+
MemberProp::Name { name: prop_name, .. },
|
|
8706
|
+
) = (object.as_ref(), prop)
|
|
7568
8707
|
{
|
|
8708
|
+
// A captured (refcell-wrapped) `Vec<Named>` element field read: emit the field
|
|
8709
|
+
// access DIRECTLY on the borrowed element (`(*arr.borrow())[i].field`) — no whole
|
|
8710
|
+
// struct clone, and no whole-Vec boxing. This is the hot-loop `states[id].timer`
|
|
8711
|
+
// case (tish#558 read side). A non-Copy field is still cloned out of the borrow.
|
|
8712
|
+
if let Expr::Ident { name: arr_name, .. } = io.as_ref() {
|
|
8713
|
+
if self.refcell_wrapped_vars.contains(arr_name.as_ref()) {
|
|
8714
|
+
if let RustType::Vec(elem) =
|
|
8715
|
+
self.type_context.get_type(arr_name.as_ref())
|
|
8716
|
+
{
|
|
8717
|
+
if let RustType::Named { fields, .. } = elem.as_ref() {
|
|
8718
|
+
if let Some((_, field_ty)) = fields
|
|
8719
|
+
.iter()
|
|
8720
|
+
.find(|(k, _)| k.as_ref() == prop_name.as_ref())
|
|
8721
|
+
{
|
|
8722
|
+
let field_ty = field_ty.clone();
|
|
8723
|
+
let idx = self.emit_index_usize(ii)?;
|
|
8724
|
+
let esc = Self::escape_ident(arr_name.as_ref());
|
|
8725
|
+
let clone = if field_ty.is_copy() { "" } else { ".clone()" };
|
|
8726
|
+
// #567: bind the index (it may read the same cell, e.g.
|
|
8727
|
+
// `arr[arr.length-1]`) BEFORE taking the borrow, and scope
|
|
8728
|
+
// the borrow to its own block so the non-reentrant
|
|
8729
|
+
// `Arc<Mutex>` lock drops before any sibling read of `arr`
|
|
8730
|
+
// (`arr[i].a + arr[j].b`) — otherwise overlapping borrows
|
|
8731
|
+
// deadlock.
|
|
8732
|
+
let access = format!(
|
|
8733
|
+
"{{ let __bi = {}; let __bg = {}.borrow(); (*__bg)[__bi].{}{} }}",
|
|
8734
|
+
idx,
|
|
8735
|
+
esc,
|
|
8736
|
+
crate::types::field_ident(prop_name.as_ref()),
|
|
8737
|
+
clone
|
|
8738
|
+
);
|
|
8739
|
+
return Ok(field_ty.to_value_expr(&access));
|
|
8740
|
+
}
|
|
8741
|
+
}
|
|
8742
|
+
}
|
|
8743
|
+
}
|
|
8744
|
+
}
|
|
7569
8745
|
let (obj_code, obj_ty) = self.emit_typed_expr(object)?;
|
|
7570
8746
|
if let RustType::Named { fields, .. } = &obj_ty {
|
|
7571
8747
|
if let Some((_, field_ty)) =
|
|
7572
8748
|
fields.iter().find(|(k, _)| k.as_ref() == prop_name.as_ref())
|
|
7573
8749
|
{
|
|
8750
|
+
// The element `xs[i]` is BORROWED, so a non-Copy field (a `Value`/
|
|
8751
|
+
// `String`) must be cloned out — otherwise reading it in a value/return
|
|
8752
|
+
// position moves out of the index (E0507). A Copy scalar reads by value.
|
|
8753
|
+
let clone = if field_ty.is_copy() { "" } else { ".clone()" };
|
|
7574
8754
|
let access = format!(
|
|
7575
|
-
"({}).{}",
|
|
8755
|
+
"({}).{}{}",
|
|
7576
8756
|
obj_code,
|
|
7577
|
-
crate::types::field_ident(prop_name.as_ref())
|
|
8757
|
+
crate::types::field_ident(prop_name.as_ref()),
|
|
8758
|
+
clone
|
|
7578
8759
|
);
|
|
7579
8760
|
return Ok(field_ty.to_value_expr(&access));
|
|
7580
8761
|
}
|
|
7581
8762
|
}
|
|
7582
8763
|
}
|
|
7583
8764
|
}
|
|
8765
|
+
// `arr.length` on a native `Vec` (wrapped or not) → a cheap native `len()` boxed to a
|
|
8766
|
+
// `Number`, instead of boxing the WHOLE Vec to a `Value::Array` just to `get_prop`
|
|
8767
|
+
// its length (O(n) per read — the hot `states.length`-per-frame footgun, tish#562).
|
|
8768
|
+
if !*optional {
|
|
8769
|
+
if let MemberProp::Name { name: p, .. } = prop {
|
|
8770
|
+
if p.as_ref() == "length" {
|
|
8771
|
+
if let Expr::Ident { name, .. } = object.as_ref() {
|
|
8772
|
+
if let RustType::Vec(_) = self.type_context.get_type(name.as_ref()) {
|
|
8773
|
+
let esc = Self::escape_ident(name.as_ref());
|
|
8774
|
+
// #567: scope the borrow (non-reentrant `Arc<Mutex>` VmRef) so it
|
|
8775
|
+
// can't overlap a sibling read of the same cell in one expression.
|
|
8776
|
+
let len = if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
8777
|
+
format!("{{ let __bg = {}.borrow(); __bg.len() }}", esc)
|
|
8778
|
+
} else {
|
|
8779
|
+
format!("{}.len()", esc)
|
|
8780
|
+
};
|
|
8781
|
+
return Ok(format!("Value::Number({} as f64)", len));
|
|
8782
|
+
}
|
|
8783
|
+
}
|
|
8784
|
+
}
|
|
8785
|
+
}
|
|
8786
|
+
}
|
|
7584
8787
|
let obj = self.emit_expr(object)?;
|
|
7585
8788
|
// A literal (non-computed) key other than `size` is eligible for a per-site
|
|
7586
8789
|
// polymorphic inline cache (#179). `size` is excluded because `get_prop` special-cases
|
|
@@ -7600,10 +8803,11 @@ impl Codegen {
|
|
|
7600
8803
|
tishlang_runtime::get_prop(&o, {}) }} }}",
|
|
7601
8804
|
obj, key
|
|
7602
8805
|
)
|
|
7603
|
-
} else if ic_eligible {
|
|
8806
|
+
} else if ic_eligible && self.emit_mode != crate::NativeEmitMode::Gba {
|
|
7604
8807
|
// Per-site inline cache: a block-scoped `static` cell unique to this member-read
|
|
7605
8808
|
// site (#179). Caches (shape, slot) so repeat reads skip the key scan. Behaviour is
|
|
7606
8809
|
// identical to `get_prop` — non-cacheable objects fall through inside `get_prop_ic`.
|
|
8810
|
+
// Gba: `PropIC` uses atomics (unavailable on thumbv4t) — use the plain path.
|
|
7607
8811
|
format!(
|
|
7608
8812
|
"{{ static IC: tishlang_runtime::PropIC = tishlang_runtime::PropIC::new(); \
|
|
7609
8813
|
tishlang_runtime::get_prop_ic(&{}, {}, &IC) }}",
|
|
@@ -7730,7 +8934,7 @@ impl Codegen {
|
|
|
7730
8934
|
match prop {
|
|
7731
8935
|
ObjectProp::KeyValue(k, v, _) => {
|
|
7732
8936
|
let val = self.emit_expr(v)?;
|
|
7733
|
-
let key =
|
|
8937
|
+
let key = self.cached_object_key(k.as_ref());
|
|
7734
8938
|
if self.should_clone(v) {
|
|
7735
8939
|
parts.push(format!("_obj.insert({}, ({}).clone());", key, val));
|
|
7736
8940
|
} else {
|
|
@@ -7738,9 +8942,58 @@ impl Codegen {
|
|
|
7738
8942
|
}
|
|
7739
8943
|
}
|
|
7740
8944
|
ObjectProp::Spread(e) => {
|
|
7741
|
-
|
|
8945
|
+
// Peek the source's static type ONCE. On GBA a statically-known struct
|
|
8946
|
+
// spreads NATIVELY (fields read straight off the Rust struct → the source
|
|
8947
|
+
// is never boxed to a `Value::Struct` / hashmap). `to_value_expr` re-boxes
|
|
8948
|
+
// a native scalar source so the boxed arms still receive a `Value`
|
|
8949
|
+
// (`{ ...5 }` stays sound). Desktop is deliberately untouched — a struct
|
|
8950
|
+
// source there boxes exactly as before (byte-identical emit, so the hot
|
|
8951
|
+
// object_spread bench keeps its clone-shape path).
|
|
8952
|
+
let gba = self.emit_mode == crate::NativeEmitMode::Gba;
|
|
8953
|
+
let (typed_code, typed_ty) = self.emit_typed_expr(e)?;
|
|
8954
|
+
if gba {
|
|
8955
|
+
if let crate::types::RustType::Named { fields, .. } = &typed_ty {
|
|
8956
|
+
let mut ins = String::new();
|
|
8957
|
+
for (k, fty) in fields {
|
|
8958
|
+
let key = self.cached_object_key(k.as_ref());
|
|
8959
|
+
ins.push_str(&format!(
|
|
8960
|
+
"_obj.insert({}, {}); ",
|
|
8961
|
+
key,
|
|
8962
|
+
Self::struct_field_to_value("_s", k, fty)
|
|
8963
|
+
));
|
|
8964
|
+
}
|
|
8965
|
+
let block = format!("{{ let _s = &({}); {}}}", typed_code, ins);
|
|
8966
|
+
if i == 0 {
|
|
8967
|
+
init = format!(
|
|
8968
|
+
"let mut _obj: PropMap = PropMap::with_capacity({}); {}",
|
|
8969
|
+
literal_keys + fields.len(),
|
|
8970
|
+
block
|
|
8971
|
+
);
|
|
8972
|
+
} else {
|
|
8973
|
+
// Later struct fields override earlier keys (JS order).
|
|
8974
|
+
parts.push(block);
|
|
8975
|
+
}
|
|
8976
|
+
continue;
|
|
8977
|
+
}
|
|
8978
|
+
}
|
|
8979
|
+
let val = if typed_ty.is_native() {
|
|
8980
|
+
typed_ty.to_value_expr(&typed_code)
|
|
8981
|
+
} else {
|
|
8982
|
+
typed_code
|
|
8983
|
+
};
|
|
8984
|
+
// GBA (non-struct source, e.g. a struct behind a `Value` param): route
|
|
8985
|
+
// through the facade's `object_spread_props`, which materialises a
|
|
8986
|
+
// `Value::Struct` to its object first. Desktop keeps the inline `if let
|
|
8987
|
+
// Value::Object` fast path untouched (no struct variant exists there, and
|
|
8988
|
+
// it's the hot object_spread bench).
|
|
7742
8989
|
if i == 0 {
|
|
7743
|
-
init =
|
|
8990
|
+
init = if gba {
|
|
8991
|
+
format!("let mut _obj: PropMap = tishlang_runtime::object_spread_props(&({})).unwrap_or_else(|| PropMap::with_capacity({}));", val, literal_keys)
|
|
8992
|
+
} else {
|
|
8993
|
+
format!("let mut _obj: PropMap = if let Value::Object(ref _spread) = {} {{ _spread.borrow().strings.clone() }} else {{ PropMap::with_capacity({}) }};", val, literal_keys)
|
|
8994
|
+
};
|
|
8995
|
+
} else if gba {
|
|
8996
|
+
parts.push(format!("if let Some(_spread) = tishlang_runtime::object_spread_props(&({})) {{ _obj.merge_from(&_spread); }}", val));
|
|
7744
8997
|
} else {
|
|
7745
8998
|
// `merge_from` reserves for the source's len, then inserts in one
|
|
7746
8999
|
// pass (later props still override earlier keys, matching JS order).
|
|
@@ -7753,7 +9006,7 @@ impl Codegen {
|
|
|
7753
9006
|
init = format!("let mut _obj: PropMap = PropMap::with_capacity({});", literal_keys);
|
|
7754
9007
|
}
|
|
7755
9008
|
format!(
|
|
7756
|
-
"{{ {} {} Value::Object(VmRef::new(ObjectData {{ strings: _obj, symbols: None }})) }}",
|
|
9009
|
+
"{{ {} {} Value::Object(VmRef::new(ObjectData {{ strings: _obj, symbols: None, frozen: false }})) }}",
|
|
7757
9010
|
init,
|
|
7758
9011
|
parts.join(" ")
|
|
7759
9012
|
)
|
|
@@ -7762,7 +9015,7 @@ impl Codegen {
|
|
|
7762
9015
|
for prop in props {
|
|
7763
9016
|
if let ObjectProp::KeyValue(k, v, _) = prop {
|
|
7764
9017
|
let val = self.emit_expr(v)?;
|
|
7765
|
-
let key =
|
|
9018
|
+
let key = self.cached_object_key(k.as_ref());
|
|
7766
9019
|
if self.should_clone(v) {
|
|
7767
9020
|
parts.push(format!("({}, ({}).clone())", key, val));
|
|
7768
9021
|
} else {
|
|
@@ -8151,6 +9404,13 @@ impl Codegen {
|
|
|
8151
9404
|
format!("{{ if {} {{ {} }} else {{ {} }} }}", cond, assign_and_return, else_expr)
|
|
8152
9405
|
}
|
|
8153
9406
|
Expr::MemberAssign { object, prop, value, .. } => {
|
|
9407
|
+
// #558: a native struct-element / struct-local field store (yielding the assigned
|
|
9408
|
+
// value) instead of set_prop on a throwaway copy that discards the write.
|
|
9409
|
+
if let Some(code) =
|
|
9410
|
+
self.try_emit_native_struct_field_assign(object, prop.as_ref(), value, false)?
|
|
9411
|
+
{
|
|
9412
|
+
return Ok(code);
|
|
9413
|
+
}
|
|
8154
9414
|
let obj = self.emit_expr(object)?;
|
|
8155
9415
|
let val = self.emit_expr(value)?;
|
|
8156
9416
|
format!(
|
|
@@ -8258,6 +9518,27 @@ impl Codegen {
|
|
|
8258
9518
|
)
|
|
8259
9519
|
}
|
|
8260
9520
|
Expr::NativeModuleLoad { spec, export_name, .. } => {
|
|
9521
|
+
// A scheme import (`asset:`, or any project-declared scheme) binds to an i32 handle
|
|
9522
|
+
// — its per-scheme index, registered into a runtime arena by the generated
|
|
9523
|
+
// `agb_main`. Only valid in Gba mode; no other target bakes scheme files into a ROM.
|
|
9524
|
+
if crate::schemes::is_scheme_import(spec.as_ref()) {
|
|
9525
|
+
if self.emit_mode != crate::NativeEmitMode::Gba {
|
|
9526
|
+
return Err(CompileError {
|
|
9527
|
+
message: format!(
|
|
9528
|
+
"scheme import '{}' requires --target gba",
|
|
9529
|
+
spec.as_ref()
|
|
9530
|
+
),
|
|
9531
|
+
span: None,
|
|
9532
|
+
});
|
|
9533
|
+
}
|
|
9534
|
+
let handle = self.scheme_handle_of(spec.as_ref()).ok_or_else(|| {
|
|
9535
|
+
CompileError {
|
|
9536
|
+
message: format!("internal: scheme import '{}' not collected", spec.as_ref()),
|
|
9537
|
+
span: None,
|
|
9538
|
+
}
|
|
9539
|
+
})?;
|
|
9540
|
+
return Ok(format!("Value::Number({} as f64)", handle));
|
|
9541
|
+
}
|
|
8261
9542
|
self.native_module_rust_init(spec.as_ref(), export_name.as_ref())
|
|
8262
9543
|
.ok_or_else(|| CompileError {
|
|
8263
9544
|
message: if crate::resolve::is_builtin_native_spec(spec.as_ref()) {
|
|
@@ -8818,6 +10099,17 @@ impl Codegen {
|
|
|
8818
10099
|
None
|
|
8819
10100
|
}
|
|
8820
10101
|
|
|
10102
|
+
/// The root variable of a member/index chain (`a.b.c` → `a`, `a[i].b` → `a`, `a` → `a`), or
|
|
10103
|
+
/// `None` when the base isn't a plain identifier (e.g. a call result `f().x`).
|
|
10104
|
+
fn root_ident_of(expr: &Expr) -> Option<&str> {
|
|
10105
|
+
match expr {
|
|
10106
|
+
Expr::Ident { name, .. } => Some(name.as_ref()),
|
|
10107
|
+
Expr::Member { object, .. } => Self::root_ident_of(object),
|
|
10108
|
+
Expr::Index { object, .. } => Self::root_ident_of(object),
|
|
10109
|
+
_ => None,
|
|
10110
|
+
}
|
|
10111
|
+
}
|
|
10112
|
+
|
|
8821
10113
|
fn collect_assigned_idents_in_expr(expr: &Expr, names: &mut HashSet<String>) {
|
|
8822
10114
|
match expr {
|
|
8823
10115
|
Expr::Assign { name, value, .. } => {
|
|
@@ -8839,6 +10131,12 @@ impl Codegen {
|
|
|
8839
10131
|
names.insert(name.to_string());
|
|
8840
10132
|
}
|
|
8841
10133
|
Expr::MemberAssign { object, value, .. } => {
|
|
10134
|
+
// Mutating a field (`x.f = v`, `a.b.c = v`) mutates the ROOT variable — record it so a
|
|
10135
|
+
// captured struct/object is wrapped mutably; otherwise the native field store lands on
|
|
10136
|
+
// an immutable `let` binding (E0594).
|
|
10137
|
+
if let Some(root) = Self::root_ident_of(object) {
|
|
10138
|
+
names.insert(root.to_string());
|
|
10139
|
+
}
|
|
8842
10140
|
Self::collect_assigned_idents_in_expr(object, names);
|
|
8843
10141
|
Self::collect_assigned_idents_in_expr(value, names);
|
|
8844
10142
|
}
|
|
@@ -8848,6 +10146,9 @@ impl Codegen {
|
|
|
8848
10146
|
value,
|
|
8849
10147
|
..
|
|
8850
10148
|
} => {
|
|
10149
|
+
if let Some(root) = Self::root_ident_of(object) {
|
|
10150
|
+
names.insert(root.to_string());
|
|
10151
|
+
}
|
|
8851
10152
|
Self::collect_assigned_idents_in_expr(object, names);
|
|
8852
10153
|
Self::collect_assigned_idents_in_expr(index, names);
|
|
8853
10154
|
Self::collect_assigned_idents_in_expr(value, names);
|
|
@@ -8859,6 +10160,29 @@ impl Codegen {
|
|
|
8859
10160
|
Expr::Unary { operand, .. } => Self::collect_assigned_idents_in_expr(operand, names),
|
|
8860
10161
|
Expr::Delete { target, .. } => Self::collect_assigned_idents_in_expr(target, names),
|
|
8861
10162
|
Expr::Call { callee, args, .. } => {
|
|
10163
|
+
// An in-place mutating array method (`a.push(x)`, `a.pop()`, `a.reverse()`, …) mutates
|
|
10164
|
+
// the ROOT variable — record it so a captured native Vec is wrapped for shared mutation.
|
|
10165
|
+
// WITHOUT this, the mutation inside a closure lands on a by-value / immutable-`let`
|
|
10166
|
+
// captured Vec (E0596), and the boxed fallback would DISCARD it (and E0282 on a
|
|
10167
|
+
// `Vec<struct>`, whose element type the skipped native op never pinned). Every listed
|
|
10168
|
+
// method has a native fast path that handles the wrapped receiver (a `(*a.borrow_mut())`
|
|
10169
|
+
// access, or a `borrow_mut` guard for the multi-access `splice`/`fill`/`copyWithin`).
|
|
10170
|
+
if let Expr::Member {
|
|
10171
|
+
object,
|
|
10172
|
+
prop: MemberProp::Name { name: m, .. },
|
|
10173
|
+
..
|
|
10174
|
+
} = callee.as_ref()
|
|
10175
|
+
{
|
|
10176
|
+
if matches!(
|
|
10177
|
+
m.as_ref(),
|
|
10178
|
+
"push" | "pop" | "shift" | "unshift" | "reverse" | "sort" | "splice"
|
|
10179
|
+
| "fill" | "copyWithin"
|
|
10180
|
+
) {
|
|
10181
|
+
if let Some(root) = Self::root_ident_of(object) {
|
|
10182
|
+
names.insert(root.to_string());
|
|
10183
|
+
}
|
|
10184
|
+
}
|
|
10185
|
+
}
|
|
8862
10186
|
Self::collect_assigned_idents_in_expr(callee, names);
|
|
8863
10187
|
for arg in args {
|
|
8864
10188
|
match arg {
|
|
@@ -9343,81 +10667,252 @@ impl Codegen {
|
|
|
9343
10667
|
Self::collect_local_var_names(s, names);
|
|
9344
10668
|
}
|
|
9345
10669
|
}
|
|
9346
|
-
if let Some(stmts) = default_body {
|
|
9347
|
-
for s in stmts {
|
|
9348
|
-
Self::collect_local_var_names(s, names);
|
|
10670
|
+
if let Some(stmts) = default_body {
|
|
10671
|
+
for s in stmts {
|
|
10672
|
+
Self::collect_local_var_names(s, names);
|
|
10673
|
+
}
|
|
10674
|
+
}
|
|
10675
|
+
}
|
|
10676
|
+
Statement::Try {
|
|
10677
|
+
body,
|
|
10678
|
+
catch_body,
|
|
10679
|
+
finally_body,
|
|
10680
|
+
..
|
|
10681
|
+
} => {
|
|
10682
|
+
Self::collect_local_var_names(body, names);
|
|
10683
|
+
if let Some(c) = catch_body {
|
|
10684
|
+
Self::collect_local_var_names(c, names);
|
|
10685
|
+
}
|
|
10686
|
+
if let Some(f) = finally_body {
|
|
10687
|
+
Self::collect_local_var_names(f, names);
|
|
10688
|
+
}
|
|
10689
|
+
}
|
|
10690
|
+
Statement::FunDecl { body, .. } => Self::collect_local_var_names(body, names),
|
|
10691
|
+
_ => {}
|
|
10692
|
+
}
|
|
10693
|
+
}
|
|
10694
|
+
|
|
10695
|
+
/// Names bound by a fn/arrow PARAMETER, a `for…of` loop var, or a `catch` param anywhere in `s`
|
|
10696
|
+
/// (recursively, incl. nested fns, control flow, and arrow exprs). Complements
|
|
10697
|
+
/// `collect_local_var_names` (`let`/`const`); together they are every name that can shadow a
|
|
10698
|
+
/// top-level numeric global.
|
|
10699
|
+
fn collect_binding_names(s: &Statement, out: &mut HashSet<String>) {
|
|
10700
|
+
use Statement::*;
|
|
10701
|
+
let mut ce = |x: &Expr, out: &mut HashSet<String>| Self::collect_arrow_params_expr(x, out);
|
|
10702
|
+
match s {
|
|
10703
|
+
FunDecl { params, body, .. } => {
|
|
10704
|
+
for p in params {
|
|
10705
|
+
for n in p.bound_names() {
|
|
10706
|
+
out.insert(n.to_string());
|
|
10707
|
+
}
|
|
10708
|
+
}
|
|
10709
|
+
Self::collect_binding_names(body, out);
|
|
10710
|
+
}
|
|
10711
|
+
ForOf { name, iterable, body, .. } => {
|
|
10712
|
+
out.insert(name.to_string());
|
|
10713
|
+
ce(iterable, out);
|
|
10714
|
+
Self::collect_binding_names(body, out);
|
|
10715
|
+
}
|
|
10716
|
+
Try { body, catch_param, catch_body, finally_body, .. } => {
|
|
10717
|
+
if let Some(cp) = catch_param {
|
|
10718
|
+
out.insert(cp.to_string());
|
|
10719
|
+
}
|
|
10720
|
+
Self::collect_binding_names(body, out);
|
|
10721
|
+
if let Some(c) = catch_body {
|
|
10722
|
+
Self::collect_binding_names(c, out);
|
|
10723
|
+
}
|
|
10724
|
+
if let Some(f) = finally_body {
|
|
10725
|
+
Self::collect_binding_names(f, out);
|
|
10726
|
+
}
|
|
10727
|
+
}
|
|
10728
|
+
Block { statements, .. } | Multi { statements, .. } => {
|
|
10729
|
+
statements.iter().for_each(|x| Self::collect_binding_names(x, out))
|
|
10730
|
+
}
|
|
10731
|
+
If { cond, then_branch, else_branch, .. } => {
|
|
10732
|
+
ce(cond, out);
|
|
10733
|
+
Self::collect_binding_names(then_branch, out);
|
|
10734
|
+
if let Some(eb) = else_branch {
|
|
10735
|
+
Self::collect_binding_names(eb, out);
|
|
10736
|
+
}
|
|
10737
|
+
}
|
|
10738
|
+
While { cond, body, .. } | DoWhile { body, cond, .. } => {
|
|
10739
|
+
ce(cond, out);
|
|
10740
|
+
Self::collect_binding_names(body, out);
|
|
10741
|
+
}
|
|
10742
|
+
For { init, cond, update, body, .. } => {
|
|
10743
|
+
if let Some(i) = init {
|
|
10744
|
+
Self::collect_binding_names(i, out);
|
|
10745
|
+
}
|
|
10746
|
+
if let Some(c) = cond {
|
|
10747
|
+
ce(c, out);
|
|
10748
|
+
}
|
|
10749
|
+
if let Some(u) = update {
|
|
10750
|
+
ce(u, out);
|
|
10751
|
+
}
|
|
10752
|
+
Self::collect_binding_names(body, out);
|
|
10753
|
+
}
|
|
10754
|
+
VarDecl { init: Some(i), .. } => ce(i, out),
|
|
10755
|
+
VarDeclDestructure { init, .. } => ce(init, out),
|
|
10756
|
+
ExprStmt { expr, .. } => ce(expr, out),
|
|
10757
|
+
Return { value: Some(v), .. } => ce(v, out),
|
|
10758
|
+
Throw { value, .. } => ce(value, out),
|
|
10759
|
+
Switch { expr, cases, default_body, .. } => {
|
|
10760
|
+
ce(expr, out);
|
|
10761
|
+
for (g, b) in cases {
|
|
10762
|
+
if let Some(ge) = g {
|
|
10763
|
+
ce(ge, out);
|
|
10764
|
+
}
|
|
10765
|
+
b.iter().for_each(|x| Self::collect_binding_names(x, out));
|
|
10766
|
+
}
|
|
10767
|
+
if let Some(d) = default_body {
|
|
10768
|
+
d.iter().for_each(|x| Self::collect_binding_names(x, out));
|
|
10769
|
+
}
|
|
10770
|
+
}
|
|
10771
|
+
_ => {}
|
|
10772
|
+
}
|
|
10773
|
+
}
|
|
10774
|
+
|
|
10775
|
+
/// Collect arrow-function parameter names anywhere in an expression (recursively), so an arrow
|
|
10776
|
+
/// param that shadows a top-level numeric global also disqualifies the global.
|
|
10777
|
+
fn collect_arrow_params_expr(e: &Expr, out: &mut HashSet<String>) {
|
|
10778
|
+
match e {
|
|
10779
|
+
Expr::ArrowFunction { params, body, .. } => {
|
|
10780
|
+
for p in params {
|
|
10781
|
+
for n in p.bound_names() {
|
|
10782
|
+
out.insert(n.to_string());
|
|
10783
|
+
}
|
|
10784
|
+
}
|
|
10785
|
+
match body {
|
|
10786
|
+
ArrowBody::Expr(x) => Self::collect_arrow_params_expr(x, out),
|
|
10787
|
+
ArrowBody::Block(b) => Self::collect_binding_names(b, out),
|
|
10788
|
+
}
|
|
10789
|
+
}
|
|
10790
|
+
Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
|
|
10791
|
+
Self::collect_arrow_params_expr(callee, out);
|
|
10792
|
+
for a in args {
|
|
10793
|
+
if let CallArg::Expr(x) | CallArg::Spread(x) = a {
|
|
10794
|
+
Self::collect_arrow_params_expr(x, out);
|
|
10795
|
+
}
|
|
10796
|
+
}
|
|
10797
|
+
}
|
|
10798
|
+
Expr::Binary { left, right, .. } | Expr::NullishCoalesce { left, right, .. } => {
|
|
10799
|
+
Self::collect_arrow_params_expr(left, out);
|
|
10800
|
+
Self::collect_arrow_params_expr(right, out);
|
|
10801
|
+
}
|
|
10802
|
+
Expr::Unary { operand, .. } => Self::collect_arrow_params_expr(operand, out),
|
|
10803
|
+
Expr::Conditional { cond, then_branch, else_branch, .. } => {
|
|
10804
|
+
Self::collect_arrow_params_expr(cond, out);
|
|
10805
|
+
Self::collect_arrow_params_expr(then_branch, out);
|
|
10806
|
+
Self::collect_arrow_params_expr(else_branch, out);
|
|
10807
|
+
}
|
|
10808
|
+
Expr::Index { object, index, .. } => {
|
|
10809
|
+
Self::collect_arrow_params_expr(object, out);
|
|
10810
|
+
Self::collect_arrow_params_expr(index, out);
|
|
10811
|
+
}
|
|
10812
|
+
Expr::Member { object, prop, .. } => {
|
|
10813
|
+
Self::collect_arrow_params_expr(object, out);
|
|
10814
|
+
if let MemberProp::Expr(pe) = prop {
|
|
10815
|
+
Self::collect_arrow_params_expr(pe, out);
|
|
10816
|
+
}
|
|
10817
|
+
}
|
|
10818
|
+
Expr::Assign { value, .. }
|
|
10819
|
+
| Expr::CompoundAssign { value, .. }
|
|
10820
|
+
| Expr::LogicalAssign { value, .. } => Self::collect_arrow_params_expr(value, out),
|
|
10821
|
+
Expr::Array { elements, .. } => {
|
|
10822
|
+
for el in elements {
|
|
10823
|
+
match el {
|
|
10824
|
+
ArrayElement::Expr(x) | ArrayElement::Spread(x) => {
|
|
10825
|
+
Self::collect_arrow_params_expr(x, out)
|
|
10826
|
+
}
|
|
10827
|
+
}
|
|
10828
|
+
}
|
|
10829
|
+
}
|
|
10830
|
+
Expr::Object { props, .. } => {
|
|
10831
|
+
for pr in props {
|
|
10832
|
+
match pr {
|
|
10833
|
+
ObjectProp::KeyValue(_, x, _) | ObjectProp::Spread(x) => {
|
|
10834
|
+
Self::collect_arrow_params_expr(x, out)
|
|
10835
|
+
}
|
|
9349
10836
|
}
|
|
9350
10837
|
}
|
|
9351
10838
|
}
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
catch_body,
|
|
9355
|
-
finally_body,
|
|
9356
|
-
..
|
|
9357
|
-
} => {
|
|
9358
|
-
Self::collect_local_var_names(body, names);
|
|
9359
|
-
if let Some(c) = catch_body {
|
|
9360
|
-
Self::collect_local_var_names(c, names);
|
|
9361
|
-
}
|
|
9362
|
-
if let Some(f) = finally_body {
|
|
9363
|
-
Self::collect_local_var_names(f, names);
|
|
9364
|
-
}
|
|
10839
|
+
Expr::TemplateLiteral { exprs, .. } => {
|
|
10840
|
+
exprs.iter().for_each(|x| Self::collect_arrow_params_expr(x, out))
|
|
9365
10841
|
}
|
|
9366
|
-
Statement::FunDecl { body, .. } => Self::collect_local_var_names(body, names),
|
|
9367
10842
|
_ => {}
|
|
9368
10843
|
}
|
|
9369
10844
|
}
|
|
9370
10845
|
|
|
9371
|
-
///
|
|
9372
|
-
///
|
|
9373
|
-
///
|
|
9374
|
-
///
|
|
9375
|
-
|
|
10846
|
+
/// Every name BOUND anywhere inside `s` — `let`/`const` (incl. destructuring), fn/arrow
|
|
10847
|
+
/// params, `for…of`/`for…in` loop vars, `catch` params, and nested `FunDecl` names —
|
|
10848
|
+
/// descending into nested arrow and function bodies. Unlike `collect_local_var_names`
|
|
10849
|
+
/// (which skips arrow bodies) or `collect_binding_names` (which skips `let`/`const`),
|
|
10850
|
+
/// this is the complete binder set of the subtree. Used by the arrow emitter's implicit
|
|
10851
|
+
/// env-capture fallback so a NESTED closure's own bindings are never mistaken for capture
|
|
10852
|
+
/// requirements of the closure being emitted (#467: `(a)=>(b)=>(c)=> …` E0425'd because
|
|
10853
|
+
/// the middle arrow tried to capture the innermost arrow's param `c`).
|
|
10854
|
+
fn collect_bound_names_deep(s: &Statement, out: &mut HashSet<String>) {
|
|
9376
10855
|
use Statement::*;
|
|
9377
|
-
let
|
|
10856
|
+
let ce = Self::collect_bound_names_deep_expr;
|
|
9378
10857
|
match s {
|
|
9379
|
-
|
|
10858
|
+
VarDecl { name, init, .. } => {
|
|
10859
|
+
out.insert(name.to_string());
|
|
10860
|
+
if let Some(i) = init {
|
|
10861
|
+
ce(i, out);
|
|
10862
|
+
}
|
|
10863
|
+
}
|
|
10864
|
+
VarDeclDestructure { pattern, init, .. } => {
|
|
10865
|
+
Self::collect_destruct_names(pattern, out);
|
|
10866
|
+
ce(init, out);
|
|
10867
|
+
}
|
|
10868
|
+
FunDecl { name, params, body, .. } => {
|
|
10869
|
+
out.insert(name.to_string());
|
|
9380
10870
|
for p in params {
|
|
9381
10871
|
for n in p.bound_names() {
|
|
9382
10872
|
out.insert(n.to_string());
|
|
9383
10873
|
}
|
|
9384
10874
|
}
|
|
9385
|
-
Self::
|
|
10875
|
+
Self::collect_bound_names_deep(body, out);
|
|
9386
10876
|
}
|
|
9387
10877
|
ForOf { name, iterable, body, .. } => {
|
|
9388
10878
|
out.insert(name.to_string());
|
|
9389
10879
|
ce(iterable, out);
|
|
9390
|
-
Self::
|
|
10880
|
+
Self::collect_bound_names_deep(body, out);
|
|
10881
|
+
}
|
|
10882
|
+
ForIn { name, object, body, .. } => {
|
|
10883
|
+
out.insert(name.to_string());
|
|
10884
|
+
ce(object, out);
|
|
10885
|
+
Self::collect_bound_names_deep(body, out);
|
|
9391
10886
|
}
|
|
9392
10887
|
Try { body, catch_param, catch_body, finally_body, .. } => {
|
|
9393
10888
|
if let Some(cp) = catch_param {
|
|
9394
10889
|
out.insert(cp.to_string());
|
|
9395
10890
|
}
|
|
9396
|
-
Self::
|
|
10891
|
+
Self::collect_bound_names_deep(body, out);
|
|
9397
10892
|
if let Some(c) = catch_body {
|
|
9398
|
-
Self::
|
|
10893
|
+
Self::collect_bound_names_deep(c, out);
|
|
9399
10894
|
}
|
|
9400
10895
|
if let Some(f) = finally_body {
|
|
9401
|
-
Self::
|
|
10896
|
+
Self::collect_bound_names_deep(f, out);
|
|
9402
10897
|
}
|
|
9403
10898
|
}
|
|
9404
10899
|
Block { statements, .. } | Multi { statements, .. } => {
|
|
9405
|
-
statements.iter().for_each(|x| Self::
|
|
10900
|
+
statements.iter().for_each(|x| Self::collect_bound_names_deep(x, out))
|
|
9406
10901
|
}
|
|
9407
10902
|
If { cond, then_branch, else_branch, .. } => {
|
|
9408
10903
|
ce(cond, out);
|
|
9409
|
-
Self::
|
|
10904
|
+
Self::collect_bound_names_deep(then_branch, out);
|
|
9410
10905
|
if let Some(eb) = else_branch {
|
|
9411
|
-
Self::
|
|
10906
|
+
Self::collect_bound_names_deep(eb, out);
|
|
9412
10907
|
}
|
|
9413
10908
|
}
|
|
9414
10909
|
While { cond, body, .. } | DoWhile { body, cond, .. } => {
|
|
9415
10910
|
ce(cond, out);
|
|
9416
|
-
Self::
|
|
10911
|
+
Self::collect_bound_names_deep(body, out);
|
|
9417
10912
|
}
|
|
9418
10913
|
For { init, cond, update, body, .. } => {
|
|
9419
10914
|
if let Some(i) = init {
|
|
9420
|
-
Self::
|
|
10915
|
+
Self::collect_bound_names_deep(i, out);
|
|
9421
10916
|
}
|
|
9422
10917
|
if let Some(c) = cond {
|
|
9423
10918
|
ce(c, out);
|
|
@@ -9425,10 +10920,8 @@ impl Codegen {
|
|
|
9425
10920
|
if let Some(u) = update {
|
|
9426
10921
|
ce(u, out);
|
|
9427
10922
|
}
|
|
9428
|
-
Self::
|
|
10923
|
+
Self::collect_bound_names_deep(body, out);
|
|
9429
10924
|
}
|
|
9430
|
-
VarDecl { init: Some(i), .. } => ce(i, out),
|
|
9431
|
-
VarDeclDestructure { init, .. } => ce(init, out),
|
|
9432
10925
|
ExprStmt { expr, .. } => ce(expr, out),
|
|
9433
10926
|
Return { value: Some(v), .. } => ce(v, out),
|
|
9434
10927
|
Throw { value, .. } => ce(value, out),
|
|
@@ -9438,19 +10931,25 @@ impl Codegen {
|
|
|
9438
10931
|
if let Some(ge) = g {
|
|
9439
10932
|
ce(ge, out);
|
|
9440
10933
|
}
|
|
9441
|
-
b.iter().for_each(|x| Self::
|
|
10934
|
+
b.iter().for_each(|x| Self::collect_bound_names_deep(x, out));
|
|
9442
10935
|
}
|
|
9443
10936
|
if let Some(d) = default_body {
|
|
9444
|
-
d.iter().for_each(|x| Self::
|
|
10937
|
+
d.iter().for_each(|x| Self::collect_bound_names_deep(x, out));
|
|
10938
|
+
}
|
|
10939
|
+
}
|
|
10940
|
+
Export { declaration, .. } => {
|
|
10941
|
+
if let tishlang_ast::ExportDeclaration::Named(inner) = declaration.as_ref() {
|
|
10942
|
+
Self::collect_bound_names_deep(inner, out);
|
|
9445
10943
|
}
|
|
9446
10944
|
}
|
|
9447
10945
|
_ => {}
|
|
9448
10946
|
}
|
|
9449
10947
|
}
|
|
9450
10948
|
|
|
9451
|
-
///
|
|
9452
|
-
///
|
|
9453
|
-
fn
|
|
10949
|
+
/// Expression side of [`Self::collect_bound_names_deep`]: recurse every child; an
|
|
10950
|
+
/// `ArrowFunction` contributes its params and descends into its body.
|
|
10951
|
+
fn collect_bound_names_deep_expr(e: &Expr, out: &mut HashSet<String>) {
|
|
10952
|
+
let ce = Self::collect_bound_names_deep_expr;
|
|
9454
10953
|
match e {
|
|
9455
10954
|
Expr::ArrowFunction { params, body, .. } => {
|
|
9456
10955
|
for p in params {
|
|
@@ -9459,62 +10958,68 @@ impl Codegen {
|
|
|
9459
10958
|
}
|
|
9460
10959
|
}
|
|
9461
10960
|
match body {
|
|
9462
|
-
ArrowBody::Expr(x) =>
|
|
9463
|
-
ArrowBody::Block(b) => Self::
|
|
10961
|
+
ArrowBody::Expr(x) => ce(x, out),
|
|
10962
|
+
ArrowBody::Block(b) => Self::collect_bound_names_deep(b, out),
|
|
9464
10963
|
}
|
|
9465
10964
|
}
|
|
9466
10965
|
Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
|
|
9467
|
-
|
|
10966
|
+
ce(callee, out);
|
|
9468
10967
|
for a in args {
|
|
9469
10968
|
if let CallArg::Expr(x) | CallArg::Spread(x) = a {
|
|
9470
|
-
|
|
10969
|
+
ce(x, out);
|
|
9471
10970
|
}
|
|
9472
10971
|
}
|
|
9473
10972
|
}
|
|
9474
10973
|
Expr::Binary { left, right, .. } | Expr::NullishCoalesce { left, right, .. } => {
|
|
9475
|
-
|
|
9476
|
-
|
|
10974
|
+
ce(left, out);
|
|
10975
|
+
ce(right, out);
|
|
9477
10976
|
}
|
|
9478
|
-
Expr::Unary { operand, .. } =>
|
|
10977
|
+
Expr::Unary { operand, .. } => ce(operand, out),
|
|
10978
|
+
Expr::TypeOf { operand, .. } => ce(operand, out),
|
|
10979
|
+
Expr::Delete { target, .. } => ce(target, out),
|
|
10980
|
+
Expr::Await { operand, .. } => ce(operand, out),
|
|
9479
10981
|
Expr::Conditional { cond, then_branch, else_branch, .. } => {
|
|
9480
|
-
|
|
9481
|
-
|
|
9482
|
-
|
|
10982
|
+
ce(cond, out);
|
|
10983
|
+
ce(then_branch, out);
|
|
10984
|
+
ce(else_branch, out);
|
|
9483
10985
|
}
|
|
9484
10986
|
Expr::Index { object, index, .. } => {
|
|
9485
|
-
|
|
9486
|
-
|
|
10987
|
+
ce(object, out);
|
|
10988
|
+
ce(index, out);
|
|
10989
|
+
}
|
|
10990
|
+
Expr::IndexAssign { object, index, value, .. } => {
|
|
10991
|
+
ce(object, out);
|
|
10992
|
+
ce(index, out);
|
|
10993
|
+
ce(value, out);
|
|
9487
10994
|
}
|
|
9488
10995
|
Expr::Member { object, prop, .. } => {
|
|
9489
|
-
|
|
10996
|
+
ce(object, out);
|
|
9490
10997
|
if let MemberProp::Expr(pe) = prop {
|
|
9491
|
-
|
|
10998
|
+
ce(pe, out);
|
|
9492
10999
|
}
|
|
9493
11000
|
}
|
|
11001
|
+
Expr::MemberAssign { object, value, .. } => {
|
|
11002
|
+
ce(object, out);
|
|
11003
|
+
ce(value, out);
|
|
11004
|
+
}
|
|
9494
11005
|
Expr::Assign { value, .. }
|
|
9495
11006
|
| Expr::CompoundAssign { value, .. }
|
|
9496
|
-
| Expr::LogicalAssign { value, .. } =>
|
|
11007
|
+
| Expr::LogicalAssign { value, .. } => ce(value, out),
|
|
9497
11008
|
Expr::Array { elements, .. } => {
|
|
9498
11009
|
for el in elements {
|
|
9499
11010
|
match el {
|
|
9500
|
-
ArrayElement::Expr(x) | ArrayElement::Spread(x) =>
|
|
9501
|
-
Self::collect_arrow_params_expr(x, out)
|
|
9502
|
-
}
|
|
11011
|
+
ArrayElement::Expr(x) | ArrayElement::Spread(x) => ce(x, out),
|
|
9503
11012
|
}
|
|
9504
11013
|
}
|
|
9505
11014
|
}
|
|
9506
11015
|
Expr::Object { props, .. } => {
|
|
9507
11016
|
for pr in props {
|
|
9508
11017
|
match pr {
|
|
9509
|
-
ObjectProp::KeyValue(_, x, _) | ObjectProp::Spread(x) =>
|
|
9510
|
-
Self::collect_arrow_params_expr(x, out)
|
|
9511
|
-
}
|
|
11018
|
+
ObjectProp::KeyValue(_, x, _) | ObjectProp::Spread(x) => ce(x, out),
|
|
9512
11019
|
}
|
|
9513
11020
|
}
|
|
9514
11021
|
}
|
|
9515
|
-
Expr::TemplateLiteral { exprs, .. } =>
|
|
9516
|
-
exprs.iter().for_each(|x| Self::collect_arrow_params_expr(x, out))
|
|
9517
|
-
}
|
|
11022
|
+
Expr::TemplateLiteral { exprs, .. } => exprs.iter().for_each(|x| ce(x, out)),
|
|
9518
11023
|
_ => {}
|
|
9519
11024
|
}
|
|
9520
11025
|
}
|
|
@@ -9777,7 +11282,14 @@ impl Codegen {
|
|
|
9777
11282
|
}
|
|
9778
11283
|
}
|
|
9779
11284
|
// Only clone builtins that are actually referenced (clone so outer scope can still use, e.g. process for PORT)
|
|
11285
|
+
// #513: every name on the BUILTINS skip-list below MUST also be clone-hoisted here (or
|
|
11286
|
+
// lower as a constant, like Infinity/NaN). A name skipped by the implicit env-capture
|
|
11287
|
+
// fallback but NOT hoisted is referenced raw inside the `move` closure — which MOVES it
|
|
11288
|
+
// out of the enclosing `Fn` closure's capture (E0507: `serve`/`Boolean` in any nested
|
|
11289
|
+
// value-closure that also captures a local, e.g. a `Promise.spawn(fn() { serve(port, …) })`).
|
|
9780
11290
|
for builtin in &[
|
|
11291
|
+
"Boolean",
|
|
11292
|
+
"serve",
|
|
9781
11293
|
"console",
|
|
9782
11294
|
"Math",
|
|
9783
11295
|
"JSON",
|
|
@@ -9844,6 +11356,20 @@ impl Codegen {
|
|
|
9844
11356
|
.cloned()
|
|
9845
11357
|
.collect();
|
|
9846
11358
|
already_captured.extend(BUILTINS.iter().map(|s| s.to_string()));
|
|
11359
|
+
// #467: names bound INSIDE this body's nested closures (their params and locals) show up
|
|
11360
|
+
// in `referenced` — `collect_referenced_idents` has no binder awareness — but they are not
|
|
11361
|
+
// free variables of THIS closure, and capturing them emits a clone of an ident that does
|
|
11362
|
+
// not exist in the enclosing scope (E0425: `(a)=>(b)=>(c)=> …`'s middle arrow tried to
|
|
11363
|
+
// capture the innermost arrow's own param `c`). Subtract the subtree's full binder set.
|
|
11364
|
+
// Corner accepted: a name that is BOTH bound in a nested closure AND genuinely free at
|
|
11365
|
+
// this level via the implicit fallback (an enclosing-closure local, not an outer param/
|
|
11366
|
+
// var) is skipped too — that fails loudly at compile time (E0425), never silently, and
|
|
11367
|
+
// the primary outer_params/outer_vars capture paths are unaffected by this subtraction.
|
|
11368
|
+
let mut nested_bound = HashSet::new();
|
|
11369
|
+
match body {
|
|
11370
|
+
ArrowBody::Expr(e) => Self::collect_bound_names_deep_expr(e, &mut nested_bound),
|
|
11371
|
+
ArrowBody::Block(s) => Self::collect_bound_names_deep(s, &mut nested_bound),
|
|
11372
|
+
}
|
|
9847
11373
|
let implicit_env_captures: Vec<String> = if self.value_fn_depth > 0 {
|
|
9848
11374
|
referenced
|
|
9849
11375
|
.iter()
|
|
@@ -9851,6 +11377,13 @@ impl Codegen {
|
|
|
9851
11377
|
!param_names.contains(name.as_str())
|
|
9852
11378
|
&& !local_var_names.contains(name.as_str())
|
|
9853
11379
|
&& !already_captured.contains(name.as_str())
|
|
11380
|
+
&& !nested_bound.contains(name.as_str())
|
|
11381
|
+
// #467: a native numeric GLOBAL (#176 TLS-lowered, e.g. `G_BASE`) has no
|
|
11382
|
+
// boxed binding anywhere — the body reads it via the thread-local — so a
|
|
11383
|
+
// capture emit here references a nonexistent ident (E0425 for any nested
|
|
11384
|
+
// arrow whose grandparent-scope var was TLS-lowered). Same exclusion the
|
|
11385
|
+
// FunDecl closure path and this fn's own outer_vars filter already apply.
|
|
11386
|
+
&& !self.native_numeric_globals.contains_key(name.as_str())
|
|
9854
11387
|
})
|
|
9855
11388
|
.cloned()
|
|
9856
11389
|
.collect()
|
|
@@ -9979,6 +11512,15 @@ impl Codegen {
|
|
|
9979
11512
|
for v in &implicit_env_captures {
|
|
9980
11513
|
self.refcell_wrapped_vars.remove(v);
|
|
9981
11514
|
}
|
|
11515
|
+
// tishlang/tish#556: an arrow PARAMETER shadows any outer var of the same name — inside the
|
|
11516
|
+
// body it is a plain `Value`/native binding, not the outer `VmRef` cell, so drop such names
|
|
11517
|
+
// from the wrapped set for the body (restored via `saved_refcell_vars`). Without this, a
|
|
11518
|
+
// reference to the param compiles as `vm_read(¶m)` — a `&Value` where a `&VmRef` is wanted.
|
|
11519
|
+
for p in params {
|
|
11520
|
+
for bn in p.bound_names() {
|
|
11521
|
+
self.refcell_wrapped_vars.remove(bn.as_ref());
|
|
11522
|
+
}
|
|
11523
|
+
}
|
|
9982
11524
|
|
|
9983
11525
|
self.type_context.push_fun_param_scope(params, None);
|
|
9984
11526
|
|
|
@@ -10069,11 +11611,15 @@ impl Codegen {
|
|
|
10069
11611
|
{
|
|
10070
11612
|
if p.as_ref() == "length" {
|
|
10071
11613
|
if let Expr::Ident { name: arr, .. } = object.as_ref() {
|
|
10072
|
-
if matches!(self.type_context.get_type(arr.as_ref()), RustType::Vec(_))
|
|
10073
|
-
&& !self.refcell_wrapped_vars.contains(arr.as_ref())
|
|
10074
|
-
{
|
|
11614
|
+
if matches!(self.type_context.get_type(arr.as_ref()), RustType::Vec(_)) {
|
|
10075
11615
|
let esc = Self::escape_ident(arr.as_ref());
|
|
10076
|
-
|
|
11616
|
+
// A captured Vec reads its length through the cell (`(*a.borrow()).len()`).
|
|
11617
|
+
let code = if self.refcell_wrapped_vars.contains(arr.as_ref()) {
|
|
11618
|
+
format!("{{ let __bg = {}.borrow(); __bg.len() as f64 }}", esc)
|
|
11619
|
+
} else {
|
|
11620
|
+
format!("{}.len() as f64", esc)
|
|
11621
|
+
};
|
|
11622
|
+
return Ok(format!("({})", code));
|
|
10077
11623
|
}
|
|
10078
11624
|
}
|
|
10079
11625
|
}
|
|
@@ -10264,7 +11810,7 @@ impl Codegen {
|
|
|
10264
11810
|
}
|
|
10265
11811
|
}
|
|
10266
11812
|
|
|
10267
|
-
// Native typed-array HOFs (
|
|
11813
|
+
// Native typed-array HOFs (default-on via native_opts_enabled): `xs.reduce/map/filter/some/every(<arrow>)`
|
|
10268
11814
|
// whose native result type matches this binding's target → emit the iterator chain
|
|
10269
11815
|
// directly, with NO box/unbox round-trip (the per-element `value_call` is gone too).
|
|
10270
11816
|
if let Expr::Call { callee, args, .. } = expr {
|
|
@@ -10286,6 +11832,29 @@ impl Codegen {
|
|
|
10286
11832
|
if &typed_ty == target_type {
|
|
10287
11833
|
return Ok(typed_code);
|
|
10288
11834
|
}
|
|
11835
|
+
// Integer-scalar target from a native numeric source (`let hp: u8 = 100`,
|
|
11836
|
+
// `hp: dmg` where dmg is f64): truncate-cast directly instead of round-tripping
|
|
11837
|
+
// through `Value::Number`. Covers narrow widths and `i32` uniformly.
|
|
11838
|
+
if target_type.is_integer_scalar()
|
|
11839
|
+
&& (typed_ty == RustType::F64 || typed_ty.is_integer_scalar())
|
|
11840
|
+
{
|
|
11841
|
+
return Ok(format!("({}) as {}", typed_code, target_type.to_rust_type_str()));
|
|
11842
|
+
}
|
|
11843
|
+
// `f64` target from an integer-scalar source: widen directly.
|
|
11844
|
+
if *target_type == RustType::F64 && typed_ty.is_integer_scalar() {
|
|
11845
|
+
return Ok(format!("({}) as f64", typed_code));
|
|
11846
|
+
}
|
|
11847
|
+
// `fixed` target from a native integer / f64 source: lift to `Fixed` directly, no
|
|
11848
|
+
// Value round-trip. Lets `let dx: fixed = input_x()` (i32→fixed) or `let v: fixed = n`
|
|
11849
|
+
// stay native so downstream `dx * spd` is `Fixed*Fixed` instead of boxing.
|
|
11850
|
+
if *target_type == RustType::Fixed {
|
|
11851
|
+
if typed_ty.is_integer_scalar() {
|
|
11852
|
+
return Ok(format!("tishlang_runtime::Fixed::from_raw((({}) as i32) * 256)", typed_code));
|
|
11853
|
+
}
|
|
11854
|
+
if typed_ty == RustType::F64 {
|
|
11855
|
+
return Ok(format!("tishlang_runtime::Fixed::from_raw((({}) * 256.0) as i32)", typed_code));
|
|
11856
|
+
}
|
|
11857
|
+
}
|
|
10289
11858
|
}
|
|
10290
11859
|
|
|
10291
11860
|
// Fall back to emit_expr + conversion
|
|
@@ -11598,50 +13167,19 @@ impl Codegen {
|
|
|
11598
13167
|
let Expr::Ident { name, .. } = object else {
|
|
11599
13168
|
return Ok(None);
|
|
11600
13169
|
};
|
|
11601
|
-
if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
11602
|
-
return Ok(None);
|
|
11603
|
-
}
|
|
11604
13170
|
let obj_type = self.type_context.get_type(name.as_ref());
|
|
11605
13171
|
let RustType::Vec(elem_type) = obj_type else {
|
|
11606
13172
|
return Ok(None);
|
|
11607
13173
|
};
|
|
13174
|
+
// #564: a closure-captured (refcell-wrapped) native Vec is handled below via a `borrow_mut`
|
|
13175
|
+
// store. WITHOUT this it fell to the generic boxed `set_index` fallback, which mutates a
|
|
13176
|
+
// THROWAWAY boxed copy of the Vec (write silently DISCARDED) — and for a compound `a[i] += x`
|
|
13177
|
+
// (desugared to `a[i] = a[i] + x`) the fallback's `get_index(&a) … set_index(&a …)` takes two
|
|
13178
|
+
// overlapping locks on the non-reentrant `Arc<Mutex>` VmRef and DEADLOCKS the binary.
|
|
13179
|
+
let wrapped = self.refcell_wrapped_vars.contains(name.as_ref());
|
|
11608
13180
|
let esc_obj = Self::escape_ident(name.as_ref()).into_owned();
|
|
11609
13181
|
let in_bounds = self.index_in_bounds(index, name.as_ref());
|
|
11610
|
-
let idx_usize =
|
|
11611
|
-
if let Some(uv) = self.usize_var_subst.get(idx_name.as_ref()) {
|
|
11612
|
-
uv.clone()
|
|
11613
|
-
} else {
|
|
11614
|
-
let (idx_code, idx_ty) = self.emit_typed_expr(index)?;
|
|
11615
|
-
if idx_ty == RustType::F64 || idx_ty == RustType::I32 {
|
|
11616
|
-
format!("({}) as usize", idx_code)
|
|
11617
|
-
} else {
|
|
11618
|
-
let iv = if idx_ty.is_native() {
|
|
11619
|
-
idx_ty.to_value_expr(&idx_code)
|
|
11620
|
-
} else {
|
|
11621
|
-
idx_code
|
|
11622
|
-
};
|
|
11623
|
-
format!(
|
|
11624
|
-
"{{ let _i = &{}; if let Value::Number(n) = _i {{ *n as usize }} else {{ panic!(\"array index must be a number\") }} }}",
|
|
11625
|
-
iv
|
|
11626
|
-
)
|
|
11627
|
-
}
|
|
11628
|
-
}
|
|
11629
|
-
} else {
|
|
11630
|
-
let (idx_code, idx_ty) = self.emit_typed_expr(index)?;
|
|
11631
|
-
if idx_ty == RustType::F64 || idx_ty == RustType::I32 {
|
|
11632
|
-
format!("({}) as usize", idx_code)
|
|
11633
|
-
} else {
|
|
11634
|
-
let iv = if idx_ty.is_native() {
|
|
11635
|
-
idx_ty.to_value_expr(&idx_code)
|
|
11636
|
-
} else {
|
|
11637
|
-
idx_code
|
|
11638
|
-
};
|
|
11639
|
-
format!(
|
|
11640
|
-
"{{ let _i = &{}; if let Value::Number(n) = _i {{ *n as usize }} else {{ panic!(\"array index must be a number\") }} }}",
|
|
11641
|
-
iv
|
|
11642
|
-
)
|
|
11643
|
-
}
|
|
11644
|
-
};
|
|
13182
|
+
let idx_usize = self.emit_index_usize(index)?;
|
|
11645
13183
|
let native_val = if *elem_type == RustType::I32 {
|
|
11646
13184
|
if let Expr::Binary {
|
|
11647
13185
|
left,
|
|
@@ -11690,6 +13228,34 @@ impl Codegen {
|
|
|
11690
13228
|
val_code
|
|
11691
13229
|
}
|
|
11692
13230
|
};
|
|
13231
|
+
// #564: wrapped receiver — bind the index and RHS to temps BEFORE taking the `borrow_mut`,
|
|
13232
|
+
// so a RHS that reads the same cell (`a[i] += x`, `a[i] = a[j]`) releases its shared borrow
|
|
13233
|
+
// first (no overlapping lock → no deadlock), then store into the real element through the cell.
|
|
13234
|
+
if wrapped {
|
|
13235
|
+
let needs_resize =
|
|
13236
|
+
matches!(elem_type.as_ref(), RustType::F64 | RustType::Bool) && !in_bounds;
|
|
13237
|
+
let store = if needs_resize {
|
|
13238
|
+
let pad = if matches!(elem_type.as_ref(), RustType::F64) {
|
|
13239
|
+
"f64::NAN"
|
|
13240
|
+
} else {
|
|
13241
|
+
"false"
|
|
13242
|
+
};
|
|
13243
|
+
format!(
|
|
13244
|
+
"let __i = {idx}; let __v = {val}; let mut __g = {esc}.borrow_mut(); if __i >= __g.len() {{ __g.resize(__i + 1, {pad}); }} __g[__i] = __v;",
|
|
13245
|
+
idx = idx_usize, val = native_val, esc = esc_obj, pad = pad
|
|
13246
|
+
)
|
|
13247
|
+
} else {
|
|
13248
|
+
format!(
|
|
13249
|
+
"let __i = {idx}; let __v = {val}; (*{esc}.borrow_mut())[__i] = __v;",
|
|
13250
|
+
idx = idx_usize, val = native_val, esc = esc_obj
|
|
13251
|
+
)
|
|
13252
|
+
};
|
|
13253
|
+
return Ok(Some(if side_effect_only {
|
|
13254
|
+
format!("{{ {} }}", store)
|
|
13255
|
+
} else {
|
|
13256
|
+
format!("{{ {} Value::Null }}", store)
|
|
13257
|
+
}));
|
|
13258
|
+
}
|
|
11693
13259
|
let assign = match elem_type.as_ref() {
|
|
11694
13260
|
RustType::F64 | RustType::Bool | RustType::I32 if in_bounds => {
|
|
11695
13261
|
if side_effect_only {
|
|
@@ -11717,6 +13283,161 @@ impl Codegen {
|
|
|
11717
13283
|
Ok(Some(assign))
|
|
11718
13284
|
}
|
|
11719
13285
|
|
|
13286
|
+
/// Emit an array/element index expression as a Rust `usize`. A substituted loop counter reuses
|
|
13287
|
+
/// its `usize` binding; a numeric index casts (`(idx) as usize`); anything else unwraps a
|
|
13288
|
+
/// `Value::Number` at runtime. Shared by the native `Vec` index-assign and struct-field-assign
|
|
13289
|
+
/// lowerings so they agree on index handling.
|
|
13290
|
+
fn emit_index_usize(&mut self, index: &Expr) -> Result<String, CompileError> {
|
|
13291
|
+
if let Expr::Ident { name: idx_name, .. } = index {
|
|
13292
|
+
if let Some(uv) = self.usize_var_subst.get(idx_name.as_ref()) {
|
|
13293
|
+
return Ok(uv.clone());
|
|
13294
|
+
}
|
|
13295
|
+
}
|
|
13296
|
+
let (idx_code, idx_ty) = self.emit_typed_expr(index)?;
|
|
13297
|
+
if idx_ty == RustType::F64 || idx_ty == RustType::I32 {
|
|
13298
|
+
Ok(format!("({}) as usize", idx_code))
|
|
13299
|
+
} else {
|
|
13300
|
+
let iv = if idx_ty.is_native() {
|
|
13301
|
+
idx_ty.to_value_expr(&idx_code)
|
|
13302
|
+
} else {
|
|
13303
|
+
idx_code
|
|
13304
|
+
};
|
|
13305
|
+
Ok(format!(
|
|
13306
|
+
"{{ let _i = &{}; if let Value::Number(n) = _i {{ *n as usize }} else {{ panic!(\"array index must be a number\") }} }}",
|
|
13307
|
+
iv
|
|
13308
|
+
))
|
|
13309
|
+
}
|
|
13310
|
+
}
|
|
13311
|
+
|
|
13312
|
+
/// tishlang/tish#558 — `place.field = value` where `place` is an addressable STRUCT: an element
|
|
13313
|
+
/// of a typed struct `Vec` (`arr[i].field`) or a struct-typed local (`s.field`). Lowers to a
|
|
13314
|
+
/// NATIVE field store into the real element/local, coercing `value` to the field's type. Without
|
|
13315
|
+
/// this, the generic member-assign path emits `set_prop` on a throwaway `Value::object_from_pairs`
|
|
13316
|
+
/// COPY of the element — mutating the copy and silently discarding the write. Returns `None` for
|
|
13317
|
+
/// anything that isn't a known struct place (the caller falls back to the dynamic `set_prop`).
|
|
13318
|
+
fn try_emit_native_struct_field_assign(
|
|
13319
|
+
&mut self,
|
|
13320
|
+
object: &Expr,
|
|
13321
|
+
prop: &str,
|
|
13322
|
+
value: &Expr,
|
|
13323
|
+
side_effect_only: bool,
|
|
13324
|
+
) -> Result<Option<String>, CompileError> {
|
|
13325
|
+
// `prelude` binds any index temp BEFORE a `borrow_mut()` place is taken (so the index may
|
|
13326
|
+
// itself read the array — `arr[arr.length-1]` — without a RefCell double-borrow); it is empty
|
|
13327
|
+
// for the non-wrapped cases. `place` is the addressable struct expression the field stores into.
|
|
13328
|
+
let (prelude, place, fields) = match object {
|
|
13329
|
+
// `arr[i].field = v` — a typed struct Vec element. A captured (refcell-wrapped) array
|
|
13330
|
+
// writes through `(*arr.borrow_mut())[i]` so the store lands natively on the shared cell;
|
|
13331
|
+
// WITHOUT this it fell to the generic member-assign, which boxed the WHOLE Vec into a
|
|
13332
|
+
// throwaway `Value::Array` (cloning + `from_struct`-boxing every element) and `set_prop`'d
|
|
13333
|
+
// the discarded copy — O(n) per write, the write lost, and an E0282 on the boxing closure.
|
|
13334
|
+
Expr::Index { object: io, index, .. } => {
|
|
13335
|
+
let Expr::Ident { name, .. } = io.as_ref() else {
|
|
13336
|
+
return Ok(None);
|
|
13337
|
+
};
|
|
13338
|
+
let RustType::Vec(elem) = self.type_context.get_type(name.as_ref()) else {
|
|
13339
|
+
return Ok(None);
|
|
13340
|
+
};
|
|
13341
|
+
let RustType::Named { fields, .. } = elem.as_ref() else {
|
|
13342
|
+
return Ok(None);
|
|
13343
|
+
};
|
|
13344
|
+
let fields = fields.clone();
|
|
13345
|
+
let idx_usize = self.emit_index_usize(index)?;
|
|
13346
|
+
let esc = Self::escape_ident(name.as_ref());
|
|
13347
|
+
if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
13348
|
+
(
|
|
13349
|
+
format!("let _i = {}; ", idx_usize),
|
|
13350
|
+
format!("(*{}.borrow_mut())[_i]", esc),
|
|
13351
|
+
fields,
|
|
13352
|
+
)
|
|
13353
|
+
} else {
|
|
13354
|
+
(String::new(), format!("{}[{}]", esc, idx_usize), fields)
|
|
13355
|
+
}
|
|
13356
|
+
}
|
|
13357
|
+
// `s.field = v` — a struct-typed local/var. A captured (refcell-wrapped) struct writes
|
|
13358
|
+
// through `(*s.borrow_mut())` so the field store lands natively on the shared cell instead
|
|
13359
|
+
// of falling to the boxed `set_prop(object_from_pairs(..))` path (which also wouldn't
|
|
13360
|
+
// persist — it built a throwaway object).
|
|
13361
|
+
Expr::Ident { name, .. } => {
|
|
13362
|
+
let RustType::Named { fields, .. } = self.type_context.get_type(name.as_ref()) else {
|
|
13363
|
+
return Ok(None);
|
|
13364
|
+
};
|
|
13365
|
+
let place = if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
13366
|
+
format!("(*{}.borrow_mut())", Self::escape_ident(name.as_ref()))
|
|
13367
|
+
} else {
|
|
13368
|
+
Self::escape_ident(name.as_ref()).into_owned()
|
|
13369
|
+
};
|
|
13370
|
+
(String::new(), place, fields)
|
|
13371
|
+
}
|
|
13372
|
+
_ => return Ok(None),
|
|
13373
|
+
};
|
|
13374
|
+
// The property must be a real field of the struct; its type drives the RHS coercion.
|
|
13375
|
+
let Some((_, field_ty)) = fields.iter().find(|(k, _)| k.as_ref() == prop) else {
|
|
13376
|
+
return Ok(None);
|
|
13377
|
+
};
|
|
13378
|
+
let field_ty = field_ty.clone();
|
|
13379
|
+
let field = crate::types::field_ident(prop);
|
|
13380
|
+
let coerced = self.emit_coerced_native(value, &field_ty)?;
|
|
13381
|
+
// Bind the RHS to a temp first so a `borrow_mut()` place can't alias a `borrow()` in the RHS
|
|
13382
|
+
// (`s.a = s.b + 1`) — evaluate the read before taking the mutable borrow. Any index prelude
|
|
13383
|
+
// runs first, so its evaluation also predates the borrow.
|
|
13384
|
+
let via_temp = !prelude.is_empty() || place.contains(".borrow_mut()");
|
|
13385
|
+
if side_effect_only {
|
|
13386
|
+
if via_temp {
|
|
13387
|
+
Ok(Some(format!(
|
|
13388
|
+
"{{ {}let _v = {}; {}.{} = _v; }}",
|
|
13389
|
+
prelude, coerced, place, field
|
|
13390
|
+
)))
|
|
13391
|
+
} else {
|
|
13392
|
+
Ok(Some(format!("{}.{} = {}", place, field, coerced)))
|
|
13393
|
+
}
|
|
13394
|
+
} else {
|
|
13395
|
+
// Assignment-as-expression yields the assigned value (JS `a.b = c` ⇒ c). Bind once (the
|
|
13396
|
+
// RHS may have effects and the place may re-evaluate an index), store, then hand back a
|
|
13397
|
+
// `Value` of it — box a native field, pass a `Value` field through.
|
|
13398
|
+
let ret = if field_ty.is_native() {
|
|
13399
|
+
field_ty.to_value_expr("_v")
|
|
13400
|
+
} else {
|
|
13401
|
+
"_v".to_string()
|
|
13402
|
+
};
|
|
13403
|
+
Ok(Some(format!(
|
|
13404
|
+
"{{ {}let _v = {}; {}.{} = _v.clone(); {} }}",
|
|
13405
|
+
prelude, coerced, place, field, ret
|
|
13406
|
+
)))
|
|
13407
|
+
}
|
|
13408
|
+
}
|
|
13409
|
+
|
|
13410
|
+
/// Emit `value` as a native expression of type `target_ty`, coercing without a
|
|
13411
|
+
/// `Value` round-trip where the conversion is a cheap numeric cast. Same-typed RHS
|
|
13412
|
+
/// passes through; f64↔i32 casts directly; everything else boxes to `Value` then
|
|
13413
|
+
/// converts (correct for Fixed/String/Bool/Value fields, at one box per store).
|
|
13414
|
+
fn emit_coerced_native(
|
|
13415
|
+
&mut self,
|
|
13416
|
+
value: &Expr,
|
|
13417
|
+
target_ty: &RustType,
|
|
13418
|
+
) -> Result<String, CompileError> {
|
|
13419
|
+
let (val_code, val_ty) = self.emit_typed_expr(value)?;
|
|
13420
|
+
if val_ty == *target_ty {
|
|
13421
|
+
return Ok(val_code);
|
|
13422
|
+
}
|
|
13423
|
+
// Cheap numeric coercions that avoid a `Value` round-trip: an integer scalar target
|
|
13424
|
+
// truncate-casts a numeric RHS (`(v) as u8`), and an `f64` target widens an integer
|
|
13425
|
+
// scalar RHS (`(v) as f64`). Covers i32↔f64 and every narrow width in one shape.
|
|
13426
|
+
if target_ty.is_integer_scalar() && (val_ty == RustType::F64 || val_ty.is_integer_scalar())
|
|
13427
|
+
{
|
|
13428
|
+
return Ok(format!("({}) as {}", val_code, target_ty.to_rust_type_str()));
|
|
13429
|
+
}
|
|
13430
|
+
if *target_ty == RustType::F64 && val_ty.is_integer_scalar() {
|
|
13431
|
+
return Ok(format!("({}) as f64", val_code));
|
|
13432
|
+
}
|
|
13433
|
+
let boxed = if val_ty.is_native() {
|
|
13434
|
+
val_ty.to_value_expr(&val_code)
|
|
13435
|
+
} else {
|
|
13436
|
+
val_code
|
|
13437
|
+
};
|
|
13438
|
+
Ok(target_ty.from_value_expr(&boxed))
|
|
13439
|
+
}
|
|
13440
|
+
|
|
11720
13441
|
/// `for (i=0; i<r; i++) { arr[i] = arr[i+1] }` → `arr.copy_within(1..(r+1), 0)` (fannkuch perm1 rotation).
|
|
11721
13442
|
fn try_emit_vec_copy_within_shift_for(
|
|
11722
13443
|
&mut self,
|
|
@@ -12357,6 +14078,24 @@ impl Codegen {
|
|
|
12357
14078
|
}
|
|
12358
14079
|
}
|
|
12359
14080
|
|
|
14081
|
+
/// A numeric literal folded to a compile-time `Fixed` (Q24.8) constant, or `None` if `e`
|
|
14082
|
+
/// isn't a plain number literal. Lets a `fixed ⊕ <literal>` op (`px + 1.5`, `hp * 2`) stay
|
|
14083
|
+
/// native `Num<i32,8>` arithmetic instead of boxing the literal side through `f64`. Uses the
|
|
14084
|
+
/// same truncating `(n * 256) as i32` conversion as the dynamic `fixed`↔`Value` boundary, so
|
|
14085
|
+
/// the constant is bit-identical to the boxed path.
|
|
14086
|
+
fn fixed_literal_of(e: &Expr) -> Option<String> {
|
|
14087
|
+
if let Expr::Literal {
|
|
14088
|
+
value: Literal::Number(n),
|
|
14089
|
+
..
|
|
14090
|
+
} = e
|
|
14091
|
+
{
|
|
14092
|
+
let raw = (n * 256.0) as i32;
|
|
14093
|
+
Some(format!("tishlang_runtime::Fixed::from_raw({}i32)", raw))
|
|
14094
|
+
} else {
|
|
14095
|
+
None
|
|
14096
|
+
}
|
|
14097
|
+
}
|
|
14098
|
+
|
|
12360
14099
|
/// Names of `f64` locals provably integer-bounded within `(-2^53, 2^53)` across the whole
|
|
12361
14100
|
/// program. Seeds from integer-literal initializers and literal-bounded `for` counters, then
|
|
12362
14101
|
/// runs a join fixpoint over reassignments: a local keeps a bound only if its init and EVERY
|
|
@@ -14277,6 +16016,9 @@ impl Codegen {
|
|
|
14277
16016
|
Expr::Member { optional: false, .. } | Expr::Index { optional: false, .. } => {
|
|
14278
16017
|
found = true
|
|
14279
16018
|
}
|
|
16019
|
+
// #437: a write to a frozen object throws a catchable TypeError (set_prop parks
|
|
16020
|
+
// it), so an assignment statement must checkpoint like a read/call does.
|
|
16021
|
+
Expr::MemberAssign { .. } | Expr::IndexAssign { .. } => found = true,
|
|
14280
16022
|
_ => {}
|
|
14281
16023
|
}
|
|
14282
16024
|
});
|
|
@@ -14853,7 +16595,7 @@ impl Codegen {
|
|
|
14853
16595
|
|
|
14854
16596
|
fn module_const_f64_array_as_value(static_name: &str) -> String {
|
|
14855
16597
|
format!(
|
|
14856
|
-
"Value::NumberArray(VmRef::new({}.iter().copied().collect::<Vec<f64>>()))",
|
|
16598
|
+
"Value::NumberArray(VmRef::new(tishlang_runtime::NumArrayBacking::Packed({}.iter().copied().collect::<Vec<f64>>())))",
|
|
14857
16599
|
static_name
|
|
14858
16600
|
)
|
|
14859
16601
|
}
|
|
@@ -16635,6 +18377,14 @@ impl Codegen {
|
|
|
16635
18377
|
code
|
|
16636
18378
|
} else if ty == RustType::Value {
|
|
16637
18379
|
RustType::F64.from_value_expr(&code)
|
|
18380
|
+
} else if ty.is_integer_scalar() {
|
|
18381
|
+
// An integer scalar (`i32` or a narrow width) is exact in `f64` — widen it, rather
|
|
18382
|
+
// than emitting the raw integer where an `f64` is required (e.g. `return total;`
|
|
18383
|
+
// from a native-vec `-> f64` fn whose body yields an `i32` local).
|
|
18384
|
+
format!("(({}) as f64)", code)
|
|
18385
|
+
} else if ty == RustType::Fixed {
|
|
18386
|
+
// Q24.8 → f64 (exact): raw / 256.
|
|
18387
|
+
format!("(({}).to_raw() as f64 / 256.0)", code)
|
|
16638
18388
|
} else {
|
|
16639
18389
|
code
|
|
16640
18390
|
})
|
|
@@ -17192,6 +18942,7 @@ impl Codegen {
|
|
|
17192
18942
|
name,
|
|
17193
18943
|
params,
|
|
17194
18944
|
rest_param: None,
|
|
18945
|
+
return_type,
|
|
17195
18946
|
body,
|
|
17196
18947
|
..
|
|
17197
18948
|
} = s
|
|
@@ -17199,6 +18950,13 @@ impl Codegen {
|
|
|
17199
18950
|
if nonnumeric.contains(name.as_ref()) {
|
|
17200
18951
|
continue;
|
|
17201
18952
|
}
|
|
18953
|
+
// The native-vec `_nv` lowering only expresses `f64`/`Vec<f64>`/unit returns
|
|
18954
|
+
// (`VecRetKind`). A `fixed`-returning fn would be forced to `-> f64` while its
|
|
18955
|
+
// body yields `Num<i32,8>` — a hard type error. Keep such fns on the (correct,
|
|
18956
|
+
// boxed) dynamic path until `VecRetKind` learns `Fixed`.
|
|
18957
|
+
if Self::ann_is_simple(return_type.as_ref(), "fixed") {
|
|
18958
|
+
continue;
|
|
18959
|
+
}
|
|
17202
18960
|
if let Some(sig) = Self::infer_vec_fn_sig(params, body) {
|
|
17203
18961
|
let has_array_param = sig
|
|
17204
18962
|
.params
|
|
@@ -17237,6 +18995,7 @@ impl Codegen {
|
|
|
17237
18995
|
name,
|
|
17238
18996
|
params,
|
|
17239
18997
|
rest_param: None,
|
|
18998
|
+
return_type,
|
|
17240
18999
|
body,
|
|
17241
19000
|
..
|
|
17242
19001
|
} = s
|
|
@@ -17250,6 +19009,11 @@ impl Codegen {
|
|
|
17250
19009
|
if nonnumeric.contains(name.as_ref()) {
|
|
17251
19010
|
continue;
|
|
17252
19011
|
}
|
|
19012
|
+
// A `fixed` return can't be expressed by `VecRetKind` (see the initial pass) — the
|
|
19013
|
+
// forwarding fixpoint must skip it too, or it re-adds the fn via `body_uses_local_vec_ops`.
|
|
19014
|
+
if Self::ann_is_simple(return_type.as_ref(), "fixed") {
|
|
19015
|
+
continue;
|
|
19016
|
+
}
|
|
17253
19017
|
if let Some(sig) = Self::infer_vec_fn_sig_forwarding(params, body, &sigs) {
|
|
17254
19018
|
let has_array_param = sig
|
|
17255
19019
|
.params
|
|
@@ -18698,7 +20462,7 @@ impl Codegen {
|
|
|
18698
20462
|
// ====================================================================================
|
|
18699
20463
|
// #177 (S-E / S-F): interprocedural aggregate (unboxed struct + Vec<Struct>) free fns.
|
|
18700
20464
|
//
|
|
18701
|
-
// The infer front-end (S-0..S-D,
|
|
20465
|
+
// The infer front-end (S-0..S-D, default-on via native_opts_enabled) stamps a struct alias and
|
|
18702
20466
|
// `: alias` / `: alias[]` annotations onto the nbody-shape factory/array/operator fns iff a
|
|
18703
20467
|
// whole-program candidacy predicate holds (monomorphic all-f64 struct, no `===`/escape/
|
|
18704
20468
|
// reshape, write-permitting field ops). Here we consume that: emit each such fn as a native
|
|
@@ -18712,7 +20476,7 @@ impl Codegen {
|
|
|
18712
20476
|
// identical to flag-off). So we never half-wire the feature or miscompile.
|
|
18713
20477
|
// ====================================================================================
|
|
18714
20478
|
|
|
18715
|
-
// ── #178: recursive-struct native lowering (
|
|
20479
|
+
// ── #178: recursive-struct native lowering (default-on via native_opts_enabled) ─────────
|
|
18716
20480
|
// Infers a recursive struct shape STRUCTURALLY (no identifier matching) from object literals
|
|
18717
20481
|
// whose fields are scalars or recursive self-references, and emits native builders
|
|
18718
20482
|
// (`name_rec(..) -> TishStruct_X`) + consumers (`name_rec(&TishStruct_X) -> f64`) over
|
|
@@ -21371,23 +23135,62 @@ impl Codegen {
|
|
|
21371
23135
|
let (l, lt) = self.emit_typed_expr(left)?;
|
|
21372
23136
|
let (r, rt) = self.emit_typed_expr(right)?;
|
|
21373
23137
|
|
|
23138
|
+
// `fixed ⊕ <runtime integer>` (e.g. `t * speed` with `t: i32`, `speed: fixed`): LIFT the
|
|
23139
|
+
// integer operand to `Fixed` via `Num::new` so the whole op stays native `Num<i32,8>`
|
|
23140
|
+
// arithmetic. This runs BEFORE the generic integer→f64 coercion below — otherwise the
|
|
23141
|
+
// integer becomes f64 and `f64 ⊕ fixed` drops to the boxed `ops::*` path (that path is
|
|
23142
|
+
// the deliberate escape hatch for a GENUINE runtime f64, which is lossy against fixed;
|
|
23143
|
+
// a plain integer is exact, so lifting is sound). Integer *literals* are already folded
|
|
23144
|
+
// to a Fixed constant by `fixed_literal_of` further down; this covers runtime integers.
|
|
23145
|
+
// `Fixed<i32,8>` raw = value << 8 (× 256), so an integer `v` lifts to fixed `v.0` as
|
|
23146
|
+
// `Fixed::from_raw(v * 256)` — the same constructor tish emits everywhere for fixed.
|
|
23147
|
+
let (l, lt, r, rt) = if lt == RustType::Fixed && rt.is_integer_scalar() {
|
|
23148
|
+
(l, lt, format!("tishlang_runtime::Fixed::from_raw((({}) as i32) * 256)", r), RustType::Fixed)
|
|
23149
|
+
} else if rt == RustType::Fixed && lt.is_integer_scalar() {
|
|
23150
|
+
(format!("tishlang_runtime::Fixed::from_raw((({}) as i32) * 256)", l), RustType::Fixed, r, rt)
|
|
23151
|
+
} else {
|
|
23152
|
+
(l, lt, r, rt)
|
|
23153
|
+
};
|
|
23154
|
+
|
|
21374
23155
|
// An `I32` loop-accumulator (the i32-loop-var lowering) used in a NON-bitwise
|
|
21375
23156
|
// expression reads as its signed int32 value coerced to `f64` — every i32 is exact
|
|
21376
23157
|
// in f64. Bitwise/shift parents never see this: they recurse into the raw AST via
|
|
21377
23158
|
// `emit_int32_operand`, which reads the i32 register directly. So coercing here only
|
|
21378
23159
|
// governs arithmetic/relational reads (e.g. the `h * 16777619` excursion), where the
|
|
21379
23160
|
// operand must be f64 to keep JS Number semantics.
|
|
21380
|
-
|
|
23161
|
+
// A narrow int storage read (`u8` HP, `i16` tile x) promotes the same way — read
|
|
23162
|
+
// as its exact `f64` value so arithmetic keeps JS Number semantics; the truncating
|
|
23163
|
+
// store back to the field happens at the assignment site.
|
|
23164
|
+
let (l, lt) = if lt.is_integer_scalar() {
|
|
21381
23165
|
(format!("(({}) as f64)", l), RustType::F64)
|
|
21382
23166
|
} else {
|
|
21383
23167
|
(l, lt)
|
|
21384
23168
|
};
|
|
21385
|
-
let (r, rt) = if rt
|
|
23169
|
+
let (r, rt) = if rt.is_integer_scalar() {
|
|
21386
23170
|
(format!("(({}) as f64)", r), RustType::F64)
|
|
21387
23171
|
} else {
|
|
21388
23172
|
(r, rt)
|
|
21389
23173
|
};
|
|
21390
23174
|
|
|
23175
|
+
// `fixed ⊕ <numeric-literal>` (`px + 1.5`, `hp * 2`, `y < 100.0`): fold the literal
|
|
23176
|
+
// operand to a compile-time `Fixed` constant so the op lowers to native `Num<i32,8>`
|
|
23177
|
+
// math via the `Fixed⊕Fixed` case below, instead of falling through to the boxed
|
|
23178
|
+
// `ops::*` path. Only a plain literal is folded — a runtime `f64` still boxes (mixing
|
|
23179
|
+
// a runtime f64 with fixed is genuinely lossy and stays on the safe dynamic path).
|
|
23180
|
+
let (l, lt, r, rt) = if lt == RustType::Fixed && rt != RustType::Fixed {
|
|
23181
|
+
match Self::fixed_literal_of(right) {
|
|
23182
|
+
Some(fx) => (l, lt, fx, RustType::Fixed),
|
|
23183
|
+
None => (l, lt, r, rt),
|
|
23184
|
+
}
|
|
23185
|
+
} else if rt == RustType::Fixed && lt != RustType::Fixed {
|
|
23186
|
+
match Self::fixed_literal_of(left) {
|
|
23187
|
+
Some(fx) => (fx, RustType::Fixed, r, rt),
|
|
23188
|
+
None => (l, lt, r, rt),
|
|
23189
|
+
}
|
|
23190
|
+
} else {
|
|
23191
|
+
(l, lt, r, rt)
|
|
23192
|
+
};
|
|
23193
|
+
|
|
21391
23194
|
if let Some(result_ty) = RustType::result_type_of_binop(*op, <, &rt) {
|
|
21392
23195
|
// Bitwise/shift over numbers: lower the *whole* chain in the int32 domain so
|
|
21393
23196
|
// intermediate `to_int32`/`to_uint32`↔`f64` round-trips collapse (the win `>>>`
|
|
@@ -21515,6 +23318,41 @@ impl Codegen {
|
|
|
21515
23318
|
}
|
|
21516
23319
|
}
|
|
21517
23320
|
|
|
23321
|
+
// GBA: `key in s` on a statically-known struct → answer NATIVELY against the field
|
|
23322
|
+
// names (no boxing of `s`, no hashmap). A string-literal key folds to a compile-time
|
|
23323
|
+
// bool; a string-typed key becomes a `matches!` over the field names (zero alloc). The
|
|
23324
|
+
// struct is still evaluated (bound to `_`) for its side effects. Any other key kind
|
|
23325
|
+
// falls through to the boxed runtime `in` below. Desktop is untouched.
|
|
23326
|
+
if *op == BinOp::In
|
|
23327
|
+
&& self.emit_mode == crate::NativeEmitMode::Gba
|
|
23328
|
+
{
|
|
23329
|
+
if let RustType::Named { fields, .. } = &rt {
|
|
23330
|
+
if let Expr::Literal { value: Literal::String(s), .. } = left.as_ref() {
|
|
23331
|
+
let present = fields.iter().any(|(k, _)| k.as_ref() == s.as_ref());
|
|
23332
|
+
return Ok((
|
|
23333
|
+
format!("{{ let _ = &({}); Value::Bool({}) }}", r, present),
|
|
23334
|
+
RustType::Value,
|
|
23335
|
+
));
|
|
23336
|
+
}
|
|
23337
|
+
if lt == RustType::String {
|
|
23338
|
+
let arms = fields
|
|
23339
|
+
.iter()
|
|
23340
|
+
.map(|(k, _)| format!("{:?}", k.as_ref()))
|
|
23341
|
+
.collect::<Vec<_>>()
|
|
23342
|
+
.join(" | ");
|
|
23343
|
+
// Bind the key BEFORE evaluating the struct — `key in obj` evaluates
|
|
23344
|
+
// left-to-right, so the key's side effects must run first.
|
|
23345
|
+
return Ok((
|
|
23346
|
+
format!(
|
|
23347
|
+
"{{ let _k = ({}); let _ = &({}); Value::Bool(matches!(_k.as_str(), {})) }}",
|
|
23348
|
+
l, r, arms
|
|
23349
|
+
),
|
|
23350
|
+
RustType::Value,
|
|
23351
|
+
));
|
|
23352
|
+
}
|
|
23353
|
+
}
|
|
23354
|
+
}
|
|
23355
|
+
|
|
21518
23356
|
// Fall back: convert both sides to Value and use the runtime.
|
|
21519
23357
|
let lv = Self::box_operand_for_runtime(left, &l, <);
|
|
21520
23358
|
let rv = Self::box_operand_for_runtime(right, &r, &rt);
|
|
@@ -21601,10 +23439,24 @@ impl Codegen {
|
|
|
21601
23439
|
// Native fast path: `vec[i]` where vec is Vec<T> and i is numeric.
|
|
21602
23440
|
if !optional {
|
|
21603
23441
|
if let Expr::Ident { name, .. } = object.as_ref() {
|
|
21604
|
-
|
|
23442
|
+
{
|
|
23443
|
+
let idx_wrapped = self.refcell_wrapped_vars.contains(name.as_ref());
|
|
21605
23444
|
let obj_type = self.type_context.get_type(name.as_ref());
|
|
21606
23445
|
if let RustType::Vec(elem_type) = &obj_type {
|
|
21607
|
-
|
|
23446
|
+
// A captured (refcell-wrapped) Vec reads through `(*a.borrow())` — WITHOUT
|
|
23447
|
+
// this it fell to the boxed `get_index`, which cloned + `from_struct`-boxed
|
|
23448
|
+
// the WHOLE Vec into a `Value::Array` just to read one element (O(n) per
|
|
23449
|
+
// read — the hot-loop `states[id].timer` footgun, tish#558 read side).
|
|
23450
|
+
let esc_raw = Self::escape_ident(name.as_ref()).into_owned();
|
|
23451
|
+
// #567: for a wrapped Vec the place is the block-local guard `__bg`
|
|
23452
|
+
// (bound below), so the non-reentrant `Arc<Mutex>` borrow is scoped to
|
|
23453
|
+
// this single read and can't overlap a sibling read of the same cell
|
|
23454
|
+
// (`a[i] + a[j]` in one expression), which would deadlock.
|
|
23455
|
+
let esc_obj = if idx_wrapped {
|
|
23456
|
+
"(*__bg)".to_string()
|
|
23457
|
+
} else {
|
|
23458
|
+
esc_raw.clone()
|
|
23459
|
+
};
|
|
21608
23460
|
// #173 part 3: prove the index in-bounds BEFORE emitting it.
|
|
21609
23461
|
let in_bounds = self.index_in_bounds(index, name.as_ref());
|
|
21610
23462
|
let idx_usize = if let Expr::Ident { name: idx_name, .. } =
|
|
@@ -21646,6 +23498,14 @@ impl Codegen {
|
|
|
21646
23498
|
)
|
|
21647
23499
|
}
|
|
21648
23500
|
};
|
|
23501
|
+
// #567: inside the wrapped block the index is pre-bound to `__bi`
|
|
23502
|
+
// (evaluated BEFORE the borrow, so an index that itself reads this cell
|
|
23503
|
+
// — `arr[arr.length-1]` — doesn't double-lock the non-reentrant cell).
|
|
23504
|
+
let idx_ref = if idx_wrapped {
|
|
23505
|
+
"__bi".to_string()
|
|
23506
|
+
} else {
|
|
23507
|
+
idx_usize.clone()
|
|
23508
|
+
};
|
|
21649
23509
|
let elem_ty = *elem_type.clone();
|
|
21650
23510
|
// OOB-safe read for numeric/bool Vecs: JS `arr[oob]` is `undefined`
|
|
21651
23511
|
// (→ NaN / false in those contexts), NOT a panic. In-bounds is the
|
|
@@ -21655,27 +23515,42 @@ impl Codegen {
|
|
|
21655
23515
|
// #173 part 3: a proven in-bounds read skips the `.get().unwrap_or`
|
|
21656
23516
|
// branch — a direct `a[i]` (the `idx < len` proof guarantees it).
|
|
21657
23517
|
RustType::F64 | RustType::Bool | RustType::I32 if in_bounds => {
|
|
21658
|
-
format!("{}[{}]", esc_obj,
|
|
23518
|
+
format!("{}[{}]", esc_obj, idx_ref)
|
|
21659
23519
|
}
|
|
21660
23520
|
RustType::F64 => format!(
|
|
21661
23521
|
"{}.get({}).copied().unwrap_or(f64::NAN)",
|
|
21662
|
-
esc_obj,
|
|
23522
|
+
esc_obj, idx_ref
|
|
21663
23523
|
),
|
|
21664
23524
|
RustType::I32 => format!(
|
|
21665
23525
|
"{}.get({}).copied().unwrap_or(0)",
|
|
21666
|
-
esc_obj,
|
|
23526
|
+
esc_obj, idx_ref
|
|
21667
23527
|
),
|
|
21668
23528
|
RustType::Bool => {
|
|
21669
|
-
format!("{}.get({}).copied().unwrap_or(false)", esc_obj,
|
|
23529
|
+
format!("{}.get({}).copied().unwrap_or(false)", esc_obj, idx_ref)
|
|
21670
23530
|
}
|
|
21671
|
-
// Other element types
|
|
21672
|
-
|
|
23531
|
+
// Other element types (struct/Value/String). From behind a
|
|
23532
|
+
// `borrow()` guard a non-Copy element must be cloned OUT (it can't be
|
|
23533
|
+
// moved from the borrow); a plain local keeps the direct place.
|
|
23534
|
+
_ if idx_wrapped => format!("{}[{}].clone()", esc_obj, idx_ref),
|
|
23535
|
+
_ => format!("{}[{}]", esc_obj, idx_ref),
|
|
23536
|
+
};
|
|
23537
|
+
// #567: wrap the wrapped-Vec read in a block that pre-binds the index
|
|
23538
|
+
// and scopes the borrow guard `__bg`, so the lock is released before
|
|
23539
|
+
// any sibling read of the same cell in the enclosing expression.
|
|
23540
|
+
let access = if idx_wrapped {
|
|
23541
|
+
format!(
|
|
23542
|
+
"{{ let __bi = {}; let __bg = {}.borrow(); {} }}",
|
|
23543
|
+
idx_usize, esc_raw, access
|
|
23544
|
+
)
|
|
23545
|
+
} else {
|
|
23546
|
+
access
|
|
21673
23547
|
};
|
|
21674
23548
|
return Ok((access, elem_ty));
|
|
21675
23549
|
}
|
|
21676
23550
|
// Native tuple access: `tuple[const]` -> `tuple.const` (Rust tuples
|
|
21677
23551
|
// require a literal index; a variable index falls through to boxed).
|
|
21678
|
-
|
|
23552
|
+
// (Plain local only — a wrapped tuple keeps the boxed path.)
|
|
23553
|
+
if let (false, RustType::Tuple(elems)) = (idx_wrapped, &obj_type) {
|
|
21679
23554
|
if let Expr::Literal {
|
|
21680
23555
|
value: Literal::Number(n),
|
|
21681
23556
|
..
|
|
@@ -21753,6 +23628,35 @@ impl Codegen {
|
|
|
21753
23628
|
}
|
|
21754
23629
|
}
|
|
21755
23630
|
}
|
|
23631
|
+
// Typed extern (a `cargo:` import with a declared native signature) used INSIDE a
|
|
23632
|
+
// native/typed expression: `entity_x(e) + 16`, `cvar(e, k) + 1`. Emit the direct
|
|
23633
|
+
// `crate::name_typed(<coerced args>)` and report its NATIVE return type, so the
|
|
23634
|
+
// surrounding arithmetic/comparison stays native instead of boxing the result and
|
|
23635
|
+
// dropping to `ops::*`. Only a native-returning extern is taken here; a `void`/`Value`
|
|
23636
|
+
// one falls through to the boxed path (this is a value-position expression).
|
|
23637
|
+
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
23638
|
+
if let Some(sig) = self.extern_fns.get(fname.as_ref()).cloned() {
|
|
23639
|
+
if sig.ret.is_native()
|
|
23640
|
+
&& args.len() == sig.params.len()
|
|
23641
|
+
&& args.iter().all(|a| matches!(a, CallArg::Expr(_)))
|
|
23642
|
+
{
|
|
23643
|
+
let mut argc: Vec<String> = Vec::with_capacity(args.len());
|
|
23644
|
+
for (a, ty) in args.iter().zip(sig.params.iter()) {
|
|
23645
|
+
if let CallArg::Expr(e) = a {
|
|
23646
|
+
argc.push(self.emit_coerced_native(e, ty)?);
|
|
23647
|
+
}
|
|
23648
|
+
}
|
|
23649
|
+
let call = format!(
|
|
23650
|
+
"{}::{}_typed({})",
|
|
23651
|
+
sig.crate_name,
|
|
23652
|
+
sig.symbol,
|
|
23653
|
+
argc.join(", ")
|
|
23654
|
+
);
|
|
23655
|
+
return Ok((call, sig.ret));
|
|
23656
|
+
}
|
|
23657
|
+
}
|
|
23658
|
+
}
|
|
23659
|
+
|
|
21756
23660
|
// M5: direct call to an eligible native fn -> `name_native(<native args>)`.
|
|
21757
23661
|
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
21758
23662
|
if self.native_fns.contains(fname.as_ref()) {
|
|
@@ -21866,12 +23770,25 @@ impl Codegen {
|
|
|
21866
23770
|
}
|
|
21867
23771
|
}
|
|
21868
23772
|
}
|
|
21869
|
-
// Native typed-array HOFs over a `Vec<f64>` receiver (
|
|
23773
|
+
// Native typed-array HOFs over a `Vec<f64>` receiver (default-on via native_opts_enabled):
|
|
21870
23774
|
// `xs.reduce/map/filter/some/every(<arrow>)` → a direct Rust iterator chain,
|
|
21871
23775
|
// eliminating the per-element `value_call` and all `Value` boxing.
|
|
21872
23776
|
if let Some(res) = self.native_vec_hof_for_call(callee, args)? {
|
|
21873
23777
|
return Ok(res);
|
|
21874
23778
|
}
|
|
23779
|
+
// A call to a user tish function whose declared return type is a native struct
|
|
23780
|
+
// (`function gameState(): GameState`). The function is still a boxed closure, so the call
|
|
23781
|
+
// yields a `Value` — a `Value::Struct` on GBA. Coerce it to the native struct via
|
|
23782
|
+
// `from_value_expr` (a cheap `downcast_struct` on GBA; the `get_prop` rebuild off-GBA), so
|
|
23783
|
+
// the surrounding `gs.score` / `gs.bossHp` reads lower to plain Rust field loads instead of
|
|
23784
|
+
// boxed `get_prop`. A false positive (name shadowed by a non-struct local) is harmless:
|
|
23785
|
+
// `from_value_expr` downcasts if it can and otherwise rebuilds via `get_prop`.
|
|
23786
|
+
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
23787
|
+
if let Some(ret) = self.user_fn_returns.get(fname.as_ref()).cloned() {
|
|
23788
|
+
let boxed = self.emit_expr(expr)?;
|
|
23789
|
+
return Ok((ret.from_value_expr(&boxed), ret));
|
|
23790
|
+
}
|
|
23791
|
+
}
|
|
21875
23792
|
let result = self.emit_expr(expr)?;
|
|
21876
23793
|
Ok((result, RustType::Value))
|
|
21877
23794
|
}
|
|
@@ -21896,7 +23813,7 @@ impl Codegen {
|
|
|
21896
23813
|
if prop_name.as_ref() == "length" {
|
|
21897
23814
|
let var_esc = Self::escape_ident(var_name.as_ref()).into_owned();
|
|
21898
23815
|
let code = if self.refcell_wrapped_vars.contains(var_name.as_ref()) {
|
|
21899
|
-
format!("({}.borrow().len() as f64)", var_esc)
|
|
23816
|
+
format!("({{ let __bg = {}.borrow(); __bg.len() as f64 }})", var_esc)
|
|
21900
23817
|
} else {
|
|
21901
23818
|
format!("({}.len() as f64)", var_esc)
|
|
21902
23819
|
};
|
|
@@ -21911,7 +23828,7 @@ impl Codegen {
|
|
|
21911
23828
|
let field = crate::types::field_ident(prop_name.as_ref());
|
|
21912
23829
|
let access = if self.refcell_wrapped_vars.contains(var_name.as_ref())
|
|
21913
23830
|
{
|
|
21914
|
-
format!("
|
|
23831
|
+
format!("{{ let __bg = {}.borrow(); (*__bg).{}.clone() }}", var_esc, field)
|
|
21915
23832
|
} else {
|
|
21916
23833
|
format!("{}.{}", var_esc, field)
|
|
21917
23834
|
};
|
|
@@ -22082,16 +23999,25 @@ impl Codegen {
|
|
|
22082
23999
|
None
|
|
22083
24000
|
};
|
|
22084
24001
|
if let Some(nat_op) = nat_op {
|
|
24002
|
+
// #508: a packed `Value::NumberArray` receiver (e.g. `new Float64Array([…])` under
|
|
24003
|
+
// TISH_PACKED_ARRAYS=1) must fold too — the old `if let Value::Array … else init`
|
|
24004
|
+
// silently returned the INIT for it. Its elements are raw f64 by construction, so the
|
|
24005
|
+
// packed arm is the pure-f64 fold with zero boxing; a non-Number init falls back to
|
|
24006
|
+
// the boxed fold over `Value::Number` elements (identical semantics).
|
|
22085
24007
|
return Ok(Some(format!(
|
|
22086
24008
|
"{{ let _init0 = {init}; let _arr = ({obj}).clone(); \
|
|
22087
|
-
|
|
24009
|
+
match _arr {{ Value::Array(ref _a) => {{ let _b = _a.borrow(); \
|
|
22088
24010
|
let mut _accn: f64 = 0.0; let mut _ok = false; \
|
|
22089
24011
|
if let Value::Number(_i0) = &_init0 {{ _accn = *_i0; _ok = true; }} \
|
|
22090
24012
|
if _ok {{ for _el in _b.iter() {{ \
|
|
22091
24013
|
if let Value::Number(_n) = _el {{ _accn {nat_op} *_n; }} else {{ _ok = false; break; }} }} }} \
|
|
22092
24014
|
if _ok {{ Value::Number(_accn) }} \
|
|
22093
24015
|
else {{ let mut _acc = _init0; for _el in _b.iter() {{ let _x = _el.clone(); _acc = {body}; }} _acc }} \
|
|
22094
|
-
}}
|
|
24016
|
+
}}, Value::NumberArray(ref _a) => {{ let _vals = _a.borrow().to_values(); \
|
|
24017
|
+
if let (true, Value::Number(_i0)) = (_a.borrow().as_packed().is_some(), &_init0) {{ let mut _accn: f64 = *_i0; \
|
|
24018
|
+
for _v in &_vals {{ if let Value::Number(_n) = _v {{ _accn {nat_op} *_n; }} }} Value::Number(_accn) }} \
|
|
24019
|
+
else {{ let mut _acc = _init0; for _x in _vals {{ _acc = {body}; }} _acc }} \
|
|
24020
|
+
}}, _ => _init0 }} }}",
|
|
22095
24021
|
init = initial,
|
|
22096
24022
|
obj = obj_expr,
|
|
22097
24023
|
nat_op = nat_op,
|
|
@@ -22099,10 +24025,13 @@ impl Codegen {
|
|
|
22099
24025
|
)));
|
|
22100
24026
|
}
|
|
22101
24027
|
|
|
24028
|
+
// #508: same NumberArray arm for the general (non-arithmetic-op) fold.
|
|
22102
24029
|
Ok(Some(format!(
|
|
22103
24030
|
"{{ let mut _acc = {init}; let _arr = ({obj}).clone(); \
|
|
22104
|
-
|
|
22105
|
-
let _x = _el.clone(); _acc = {body}; }} }}
|
|
24031
|
+
match _arr {{ Value::Array(ref _a) => {{ for _el in _a.borrow().iter() {{ \
|
|
24032
|
+
let _x = _el.clone(); _acc = {body}; }} }} \
|
|
24033
|
+
Value::NumberArray(ref _a) => {{ for _x in _a.borrow().to_values() {{ \
|
|
24034
|
+
_acc = {body}; }} }} _ => {{}} }} _acc }}",
|
|
22106
24035
|
init = initial,
|
|
22107
24036
|
obj = obj_expr,
|
|
22108
24037
|
body = body_code
|
|
@@ -22595,7 +24524,7 @@ impl Codegen {
|
|
|
22595
24524
|
"let mut __out: Vec<f64> = Vec::new(); \
|
|
22596
24525
|
for &__f0 in __na.iter() {{ let mut __f = __f0; {inner} \
|
|
22597
24526
|
let {p}: f64 = __f; __out.push({term}); }} \
|
|
22598
|
-
Value::NumberArray(VmRef::new(__out))",
|
|
24527
|
+
Value::NumberArray(VmRef::new(tishlang_runtime::NumArrayBacking::Packed(__out)))",
|
|
22599
24528
|
inner = inner, p = pesc, term = term
|
|
22600
24529
|
)
|
|
22601
24530
|
}
|
|
@@ -22607,7 +24536,7 @@ impl Codegen {
|
|
|
22607
24536
|
"let mut __out: Vec<f64> = Vec::new(); \
|
|
22608
24537
|
for &__f0 in __na.iter() {{ let mut __f = __f0; {inner} \
|
|
22609
24538
|
let {p}: f64 = __f; if {pred} {{ __out.push({p}); }} }} \
|
|
22610
|
-
Value::NumberArray(VmRef::new(__out))",
|
|
24539
|
+
Value::NumberArray(VmRef::new(tishlang_runtime::NumArrayBacking::Packed(__out)))",
|
|
22611
24540
|
inner = inner, p = pesc, pred = pred
|
|
22612
24541
|
)
|
|
22613
24542
|
}
|
|
@@ -22617,7 +24546,7 @@ impl Codegen {
|
|
|
22617
24546
|
}
|
|
22618
24547
|
|
|
22619
24548
|
/// If `callee(args)` is `<Vec<f64>-ident>.reduce/map/filter/some/every(<arrow>)` and the
|
|
22620
|
-
///
|
|
24549
|
+
/// native opts are enabled (default-on), lower it to a native iterator chain. Shared by
|
|
22621
24550
|
/// `emit_typed_expr` (native sub-expressions) and `emit_native_expr` (typed `let` RHS), so a
|
|
22622
24551
|
/// typed-array HOF lowers natively whether its result flows into arithmetic or a binding.
|
|
22623
24552
|
fn native_vec_hof_for_call(
|
|
@@ -22654,7 +24583,7 @@ impl Codegen {
|
|
|
22654
24583
|
self.try_native_vec_hof(&recv_code, &inner, recv_name.as_ref(), method.as_ref(), args)
|
|
22655
24584
|
}
|
|
22656
24585
|
|
|
22657
|
-
/// Native typed-array HOFs (`
|
|
24586
|
+
/// Native typed-array HOFs (default-on via `native_opts_enabled`): when the receiver is a native `Vec<f64>`
|
|
22658
24587
|
/// (a typed `number[]`), lower `reduce`/`map`/`filter`/`some`/`every` to a direct Rust
|
|
22659
24588
|
/// iterator chain — no per-element `value_call`, no `Value` boxing.
|
|
22660
24589
|
///
|