@tishlang/tish 1.13.2 → 2.0.1
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 +2 -0
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +1 -0
- package/crates/tish/Cargo.toml +11 -3
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cli_help.rs +15 -4
- package/crates/tish/src/main.rs +93 -21
- package/crates/tish/src/repl_completion.rs +0 -1
- package/crates/tish/tests/error_source_location.rs +36 -0
- package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
- package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
- package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
- package/crates/tish/tests/integration_test.rs +402 -91
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/src/ast.rs +37 -8
- package/crates/tish_builtins/Cargo.toml +2 -0
- package/crates/tish_builtins/src/array.rs +375 -13
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +59 -19
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +86 -6
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +5 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +2 -2
- package/crates/tish_builtins/src/string.rs +19 -20
- package/crates/tish_builtins/src/symbol.rs +1 -1
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/src/chunk.rs +69 -1
- package/crates/tish_bytecode/src/compiler.rs +933 -89
- package/crates/tish_bytecode/src/encoding.rs +2 -0
- package/crates/tish_bytecode/src/lib.rs +2 -1
- package/crates/tish_bytecode/src/opcode.rs +47 -4
- package/crates/tish_bytecode/src/serialize.rs +31 -1
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +2334 -349
- package/crates/tish_compile/src/infer.rs +1395 -6
- package/crates/tish_compile/src/lib.rs +50 -8
- package/crates/tish_compile/src/resolve.rs +584 -21
- package/crates/tish_compile/src/types.rs +106 -2
- package/crates/tish_compile_js/src/codegen.rs +67 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
- package/crates/tish_core/Cargo.toml +7 -1
- package/crates/tish_core/src/console_style.rs +11 -1
- package/crates/tish_core/src/json.rs +81 -38
- package/crates/tish_core/src/lib.rs +3 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/value.rs +679 -25
- package/crates/tish_core/src/vmref.rs +13 -8
- package/crates/tish_cranelift/src/link.rs +17 -4
- package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
- package/crates/tish_eval/Cargo.toml +6 -0
- package/crates/tish_eval/src/eval.rs +665 -117
- package/crates/tish_eval/src/http.rs +4 -1
- package/crates/tish_eval/src/natives.rs +165 -13
- package/crates/tish_eval/src/value.rs +31 -13
- package/crates/tish_eval/src/value_convert.rs +10 -4
- package/crates/tish_ffi/Cargo.toml +26 -0
- package/crates/tish_ffi/src/lib.rs +518 -0
- package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
- package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
- package/crates/tish_ffi/tests/loader.rs +65 -0
- package/crates/tish_fmt/src/lib.rs +61 -5
- package/crates/tish_lexer/src/lib.rs +397 -9
- package/crates/tish_lexer/src/token.rs +7 -0
- package/crates/tish_lint/src/lib.rs +2 -10
- package/crates/tish_lsp/src/import_goto.rs +2 -0
- package/crates/tish_lsp/src/main.rs +439 -26
- package/crates/tish_native/src/build.rs +55 -1
- package/crates/tish_opt/src/lib.rs +126 -23
- package/crates/tish_parser/src/lib.rs +55 -1
- package/crates/tish_parser/src/parser.rs +456 -34
- package/crates/tish_pg/src/lib.rs +3 -3
- package/crates/tish_resolve/src/lib.rs +99 -59
- package/crates/tish_runtime/Cargo.toml +4 -0
- package/crates/tish_runtime/src/http.rs +66 -17
- package/crates/tish_runtime/src/http_fetch.rs +29 -8
- package/crates/tish_runtime/src/http_hyper.rs +25 -2
- package/crates/tish_runtime/src/lib.rs +299 -44
- package/crates/tish_runtime/src/promise.rs +328 -18
- package/crates/tish_runtime/src/timers.rs +13 -7
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +35 -18
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
- package/crates/tish_ui/src/jsx.rs +10 -0
- package/crates/tish_ui/src/runtime/hooks.rs +19 -15
- package/crates/tish_ui/src/runtime/mod.rs +15 -12
- package/crates/tish_vm/Cargo.toml +14 -1
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +2 -0
- package/crates/tish_vm/src/vm.rs +1546 -202
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_wasm/src/lib.rs +6 -2
- package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
- package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
- package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
- package/justfile +8 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
|
@@ -57,7 +57,9 @@ impl UsageAnalyzer {
|
|
|
57
57
|
self.analyze_statement(e);
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
Statement::Block { statements, .. }
|
|
60
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
61
|
+
self.analyze_statements(statements)
|
|
62
|
+
}
|
|
61
63
|
Statement::For {
|
|
62
64
|
init,
|
|
63
65
|
cond,
|
|
@@ -202,6 +204,7 @@ impl UsageAnalyzer {
|
|
|
202
204
|
self.analyze_expr(right);
|
|
203
205
|
}
|
|
204
206
|
Expr::TypeOf { operand, .. } => self.analyze_expr(operand),
|
|
207
|
+
Expr::Delete { target, .. } => self.analyze_expr(target),
|
|
205
208
|
Expr::TemplateLiteral { exprs, .. } => {
|
|
206
209
|
for e in exprs {
|
|
207
210
|
self.analyze_expr(e);
|
|
@@ -310,7 +313,9 @@ fn program_uses_async(program: &Program) -> bool {
|
|
|
310
313
|
fn stmt_has_async(s: &Statement) -> bool {
|
|
311
314
|
match s {
|
|
312
315
|
Statement::FunDecl { async_, .. } if *async_ => true,
|
|
313
|
-
Statement::Block { statements, .. }
|
|
316
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
317
|
+
statements.iter().any(stmt_has_async)
|
|
318
|
+
}
|
|
314
319
|
Statement::If {
|
|
315
320
|
then_branch,
|
|
316
321
|
else_branch,
|
|
@@ -427,7 +432,9 @@ fn program_uses_async(program: &Program) -> bool {
|
|
|
427
432
|
}
|
|
428
433
|
fn stmt_has_await(s: &Statement) -> bool {
|
|
429
434
|
match s {
|
|
430
|
-
Statement::Block { statements, .. }
|
|
435
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
436
|
+
statements.iter().any(stmt_has_await)
|
|
437
|
+
}
|
|
431
438
|
Statement::VarDecl { init, .. } => init.as_ref().is_some_and(expr_has_await),
|
|
432
439
|
Statement::VarDeclDestructure { init, .. } => expr_has_await(init),
|
|
433
440
|
Statement::ExprStmt { expr, .. } => expr_has_await(expr),
|
|
@@ -652,6 +659,34 @@ pub fn compile_with_native_modules(
|
|
|
652
659
|
)
|
|
653
660
|
}
|
|
654
661
|
|
|
662
|
+
/// Opt-in gradual type check. `TISH_CHECK=1`/`warn` prints provable annotation violations to stderr
|
|
663
|
+
/// as warnings; `TISH_CHECK=error` also fails the build. Unset/`0` → no-op (default builds are
|
|
664
|
+
/// unaffected). The checker is gradual (see `check.rs`): it never flags code it can't prove wrong.
|
|
665
|
+
fn run_type_check(program: &Program) -> Result<(), CompileError> {
|
|
666
|
+
let mode = std::env::var("TISH_CHECK").unwrap_or_default();
|
|
667
|
+
if mode.is_empty() || mode == "0" {
|
|
668
|
+
return Ok(());
|
|
669
|
+
}
|
|
670
|
+
let diags = crate::check::check_program(program);
|
|
671
|
+
if diags.is_empty() {
|
|
672
|
+
return Ok(());
|
|
673
|
+
}
|
|
674
|
+
let kind = if mode == "error" { "error" } else { "warning" };
|
|
675
|
+
for d in &diags {
|
|
676
|
+
eprintln!(
|
|
677
|
+
"tish type {}: {}:{}: {}",
|
|
678
|
+
kind, d.span.start.0, d.span.start.1, d.message
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
if mode == "error" {
|
|
682
|
+
return Err(CompileError::new(
|
|
683
|
+
format!("type checking failed: {} error(s)", diags.len()),
|
|
684
|
+
Some(diags[0].span),
|
|
685
|
+
));
|
|
686
|
+
}
|
|
687
|
+
Ok(())
|
|
688
|
+
}
|
|
689
|
+
|
|
655
690
|
pub fn compile_with_native_modules_emit(
|
|
656
691
|
program: &Program,
|
|
657
692
|
project_root: Option<&Path>,
|
|
@@ -666,6 +701,10 @@ pub fn compile_with_native_modules_emit(
|
|
|
666
701
|
} else {
|
|
667
702
|
program.clone()
|
|
668
703
|
};
|
|
704
|
+
// Gradual type check (opt-in via `TISH_CHECK`): `=1`/`=warn` prints provable annotation
|
|
705
|
+
// violations as warnings; `=error` blocks the build. Off by default — never affects the
|
|
706
|
+
// standard build. Run on the optimized, pre-inference program (real user annotations only).
|
|
707
|
+
run_type_check(&program)?;
|
|
669
708
|
// Type-inference pass: fills in `type_ann` on unannotated VarDecl nodes where
|
|
670
709
|
// the type is unambiguous (literals, arithmetic of typed vars, etc.).
|
|
671
710
|
let program = crate::infer::infer_program(&program);
|
|
@@ -703,7 +742,6 @@ struct Codegen {
|
|
|
703
742
|
indent: usize,
|
|
704
743
|
loop_label_index: usize,
|
|
705
744
|
is_async: bool,
|
|
706
|
-
project_root: Option<std::path::PathBuf>,
|
|
707
745
|
/// Requested features (http, process, fs, regex, polars). When non-empty, used instead of #[cfg].
|
|
708
746
|
features: std::collections::HashSet<String>,
|
|
709
747
|
/// spec -> native init strategy (legacy adapter object vs generated `generated_native` wrapper)
|
|
@@ -711,6 +749,15 @@ struct Codegen {
|
|
|
711
749
|
/// Stack: true = async Rust context (run body), false = sync closure (Tish fn body)
|
|
712
750
|
async_context_stack: Vec<bool>,
|
|
713
751
|
loop_stack: Vec<(String, Option<String>)>, // (break_label, continue_update) for innermost loop
|
|
752
|
+
/// Break targets for innermost breakable construct — loops AND switches (JS `break` exits the
|
|
753
|
+
/// nearest loop OR switch; `continue` uses loop_stack). Loops push to both; switches push here only.
|
|
754
|
+
break_stack: Vec<String>,
|
|
755
|
+
/// How many enclosing `try`-body closures we're currently emitting inside (within the current
|
|
756
|
+
/// function). A try body compiles to `(|| -> Result<Option<Value>, _> { … })()` — a *completion*
|
|
757
|
+
/// closure: `Ok(None)`=normal, `Ok(Some(v))`=pending `return v`, `Err(Throw)`=pending throw. When
|
|
758
|
+
/// depth>0, `return`/`throw` emit the closure-escaping completion form so they unwind through
|
|
759
|
+
/// `finally`; at depth 0 they're a plain `return`/panic (the fast path is untouched).
|
|
760
|
+
try_closure_depth: u32,
|
|
714
761
|
/// Stack of scopes, each containing function names declared in that scope
|
|
715
762
|
/// Used to capture sibling functions for mutual recursion
|
|
716
763
|
function_scope_stack: Vec<Vec<String>>,
|
|
@@ -723,6 +770,18 @@ struct Codegen {
|
|
|
723
770
|
/// Variables currently wrapped in Rc<RefCell<Value>> for mutable capture in closures
|
|
724
771
|
/// These need special handling: reads via .borrow().clone(), writes via *var.borrow_mut()
|
|
725
772
|
refcell_wrapped_vars: std::collections::HashSet<String>,
|
|
773
|
+
/// M5 (dark-shipped behind `TISH_NATIVE_FN`): top-level functions eligible for a parallel
|
|
774
|
+
/// free `fn f_native(f64,..)->f64` (all params `: number`, returns `number`, native-safe
|
|
775
|
+
/// body). Direct calls to these route to the native fn, bypassing the boxed `value_call`.
|
|
776
|
+
native_fns: std::collections::HashSet<String>,
|
|
777
|
+
/// Names of `number`-typed locals demoted to a boxed `Value` because some reassignment can
|
|
778
|
+
/// store a non-number — e.g. `let s = 0; s = s + arr[i]` where `arr` is a boxed Value: `+` is
|
|
779
|
+
/// JS string concat, so `s` may become a `String`. Lowering `s` to a native `f64` would panic
|
|
780
|
+
/// at the store's `from_value_expr(F64)` coercion (`_ => panic!("expected number")`). Computed
|
|
781
|
+
/// once in `emit_program` (after type aliases + `native_fns`), consulted at `VarDecl` to force
|
|
782
|
+
/// `RustType::Value`. This is the rust-AOT analogue of the VM array-JIT bailing to the
|
|
783
|
+
/// interpreter on a non-numeric element. See `collect_demoted_numeric_locals`.
|
|
784
|
+
demoted_numeric_locals: std::collections::HashSet<String>,
|
|
726
785
|
/// Scopes of names whose Rust binding is actually `Rc<RefCell<_>>` (emitted at VarDecl).
|
|
727
786
|
/// `refcell_wrapped_vars` alone is insufficient: it is set by prepasses before decl may run.
|
|
728
787
|
rc_cell_storage_scopes: Vec<std::collections::HashSet<String>>,
|
|
@@ -753,7 +812,9 @@ struct Codegen {
|
|
|
753
812
|
|
|
754
813
|
impl Codegen {
|
|
755
814
|
fn new_with_native_modules(
|
|
756
|
-
project_root
|
|
815
|
+
// `project_root` is no longer needed by codegen (the only consumer, a Polars-specific
|
|
816
|
+
// `read_csv` compile-time embed, was removed — crate-specific codegen belongs in that crate).
|
|
817
|
+
_project_root: Option<&Path>,
|
|
757
818
|
features: &[String],
|
|
758
819
|
native_module_init: std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
|
|
759
820
|
) -> Self {
|
|
@@ -763,15 +824,18 @@ impl Codegen {
|
|
|
763
824
|
indent: 0,
|
|
764
825
|
loop_label_index: 0,
|
|
765
826
|
is_async: false,
|
|
766
|
-
project_root: project_root.map(|p| p.to_path_buf()),
|
|
767
827
|
features,
|
|
768
828
|
native_module_init,
|
|
769
829
|
async_context_stack: Vec::new(),
|
|
770
830
|
loop_stack: Vec::new(),
|
|
831
|
+
break_stack: Vec::new(),
|
|
832
|
+
try_closure_depth: 0,
|
|
771
833
|
function_scope_stack: vec![Vec::new()], // Start with global scope
|
|
772
834
|
outer_params_stack: Vec::new(),
|
|
773
835
|
outer_vars_stack: vec![Vec::new()], // Start with module-level scope
|
|
774
836
|
refcell_wrapped_vars: std::collections::HashSet::new(),
|
|
837
|
+
native_fns: std::collections::HashSet::new(),
|
|
838
|
+
demoted_numeric_locals: std::collections::HashSet::new(),
|
|
775
839
|
rc_cell_storage_scopes: vec![std::collections::HashSet::new()],
|
|
776
840
|
usage_analyzer: None,
|
|
777
841
|
type_context: TypeContext::new(),
|
|
@@ -834,7 +898,9 @@ impl Codegen {
|
|
|
834
898
|
Statement::TypeAlias { name, ty, .. } => {
|
|
835
899
|
out.push((name.to_string(), ty));
|
|
836
900
|
}
|
|
837
|
-
Statement::Block { statements, .. }
|
|
901
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
902
|
+
Self::walk_type_aliases(statements, out)
|
|
903
|
+
}
|
|
838
904
|
Statement::If {
|
|
839
905
|
then_branch,
|
|
840
906
|
else_branch,
|
|
@@ -884,7 +950,7 @@ impl Codegen {
|
|
|
884
950
|
= &ty
|
|
885
951
|
{
|
|
886
952
|
let struct_name = crate::types::named_struct_ident(&name);
|
|
887
|
-
self.write(
|
|
953
|
+
self.write("#[derive(Clone, Debug, Default)]\n");
|
|
888
954
|
self.write("#[allow(non_snake_case, non_camel_case_types)]\n");
|
|
889
955
|
self.write(&format!("pub struct {} {{\n", struct_name));
|
|
890
956
|
for (k, t) in fields {
|
|
@@ -955,9 +1021,14 @@ impl Codegen {
|
|
|
955
1021
|
self.write(" buf.push(']');\n");
|
|
956
1022
|
}
|
|
957
1023
|
_ => {
|
|
958
|
-
// Fallback: convert the field to a Value and
|
|
959
|
-
//
|
|
960
|
-
|
|
1024
|
+
// Fallback: convert the field to a Value and delegate to the dynamic
|
|
1025
|
+
// stringifier. A `Value` field (e.g. a generic struct's `Box<T>` field)
|
|
1026
|
+
// is behind `&self` and not `Copy`, so clone it.
|
|
1027
|
+
let v_expr = if matches!(t, crate::types::RustType::Value) {
|
|
1028
|
+
format!("{}.clone()", access)
|
|
1029
|
+
} else {
|
|
1030
|
+
t.to_value_expr(&access)
|
|
1031
|
+
};
|
|
961
1032
|
self.write(&format!(
|
|
962
1033
|
" let _v: Value = {}; tishlang_runtime::json::stringify_into(buf, &_v);\n",
|
|
963
1034
|
v_expr
|
|
@@ -1034,7 +1105,7 @@ impl Codegen {
|
|
|
1034
1105
|
// latter dispatches into `http_serve_per_worker`, which
|
|
1035
1106
|
// calls onWorker once per accept thread to build that
|
|
1036
1107
|
// thread's handler.
|
|
1037
|
-
"serve" => Some("Value::native(|args: &[Value]| { let handler = args.get(1).cloned().unwrap_or(Value::Null); match handler { Value::Function(f) => tish_http_serve(args, move |req_args| f(req_args)), Value::Object(ref opts) => { let factory = opts.borrow().strings.get(\"onWorker\").cloned().unwrap_or(Value::Null); tishlang_runtime::http_serve_per_worker(args, factory) }, _ => Value::Null } })"),
|
|
1108
|
+
"serve" => Some("Value::native(|args: &[Value]| { let handler = args.get(1).cloned().unwrap_or(Value::Null); match handler { Value::Function(f) => tish_http_serve(args, move |req_args| f.call(req_args)), Value::Object(ref opts) => { let factory = opts.borrow().strings.get(\"onWorker\").cloned().unwrap_or(Value::Null); tishlang_runtime::http_serve_per_worker(args, factory) }, _ => Value::Null } })"),
|
|
1038
1109
|
"Promise" => Some("tish_promise_object()"),
|
|
1039
1110
|
"Symbol" => Some("tish_symbol_object()"),
|
|
1040
1111
|
_ => None,
|
|
@@ -1062,6 +1133,16 @@ impl Codegen {
|
|
|
1062
1133
|
"wsBroadcast" => Some("Value::native(|args: &[Value]| tishlang_runtime::ws_broadcast_native(args))"),
|
|
1063
1134
|
_ => None,
|
|
1064
1135
|
},
|
|
1136
|
+
"tish:tty" if self.has_feature("tty") => match export_name {
|
|
1137
|
+
"size" => Some("Value::native(|args: &[Value]| tishlang_runtime::tty_size(args))"),
|
|
1138
|
+
"isTTY" => Some("Value::native(|args: &[Value]| tishlang_runtime::tty_is_tty(args))"),
|
|
1139
|
+
"setRawMode" => Some("Value::native(|args: &[Value]| tishlang_runtime::tty_set_raw_mode(args))"),
|
|
1140
|
+
"enterAltScreen" => Some("Value::native(|args: &[Value]| tishlang_runtime::tty_enter_alt_screen(args))"),
|
|
1141
|
+
"leaveAltScreen" => Some("Value::native(|args: &[Value]| tishlang_runtime::tty_leave_alt_screen(args))"),
|
|
1142
|
+
"read" => Some("Value::native(|args: &[Value]| tishlang_runtime::tty_read(args))"),
|
|
1143
|
+
"readLine" => Some("Value::native(|args: &[Value]| tishlang_runtime::tty_read_line(args))"),
|
|
1144
|
+
_ => None,
|
|
1145
|
+
},
|
|
1065
1146
|
_ => return None,
|
|
1066
1147
|
};
|
|
1067
1148
|
init.map(String::from)
|
|
@@ -1069,7 +1150,10 @@ impl Codegen {
|
|
|
1069
1150
|
|
|
1070
1151
|
fn has_feature(&self, name: &str) -> bool {
|
|
1071
1152
|
if self.features.contains("full") {
|
|
1072
|
-
matches!(
|
|
1153
|
+
matches!(
|
|
1154
|
+
name,
|
|
1155
|
+
"http" | "timers" | "fs" | "process" | "regex" | "ws" | "tty"
|
|
1156
|
+
)
|
|
1073
1157
|
} else {
|
|
1074
1158
|
self.features.contains(name)
|
|
1075
1159
|
}
|
|
@@ -1098,6 +1182,17 @@ impl Codegen {
|
|
|
1098
1182
|
}
|
|
1099
1183
|
|
|
1100
1184
|
/// Escape Rust reserved keywords by prefixing with r#
|
|
1185
|
+
/// Binding keyword that stays valid for the wildcard `_`. A `_` binding cannot be `mut`
|
|
1186
|
+
/// (`error: mut must be followed by a named binding`) and is never reassigned, so it always
|
|
1187
|
+
/// takes a plain `let`. `base` is the keyword for a normal binding here (e.g. `"let mut"`).
|
|
1188
|
+
fn mut_kw_for<'a>(name: &str, base: &'a str) -> &'a str {
|
|
1189
|
+
if name == "_" {
|
|
1190
|
+
"let"
|
|
1191
|
+
} else {
|
|
1192
|
+
base
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1101
1196
|
fn escape_ident(name: &str) -> Cow<'_, str> {
|
|
1102
1197
|
// Rust standard library macros that conflict with variable names
|
|
1103
1198
|
const RUST_MACROS: &[&str] = &[
|
|
@@ -1227,15 +1322,46 @@ impl Codegen {
|
|
|
1227
1322
|
}
|
|
1228
1323
|
}
|
|
1229
1324
|
|
|
1230
|
-
///
|
|
1325
|
+
/// Emit a valid Rust `f64` expression for `n`, handling non-finite values. Constant-folding can
|
|
1326
|
+
/// produce Infinity/NaN (e.g. `5/0` → `f64::INFINITY`, `0/0` → `f64::NAN`), which the plain
|
|
1327
|
+
/// `format!("{}_f64", n)` would render as the INVALID Rust `inf_f64` / `NaN_f64`. Finite values
|
|
1328
|
+
/// keep the literal `{n}_f64` form.
|
|
1329
|
+
fn f64_lit(n: f64) -> String {
|
|
1330
|
+
if n.is_nan() {
|
|
1331
|
+
"f64::NAN".to_string()
|
|
1332
|
+
} else if n.is_infinite() {
|
|
1333
|
+
if n > 0.0 {
|
|
1334
|
+
"f64::INFINITY".to_string()
|
|
1335
|
+
} else {
|
|
1336
|
+
"f64::NEG_INFINITY".to_string()
|
|
1337
|
+
}
|
|
1338
|
+
} else {
|
|
1339
|
+
format!("{}_f64", n)
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/// Generate code for a bitwise binary operation (`& | ^`). `to_int32` is JS ToInt32
|
|
1344
|
+
/// (modulo 2³², NaN/±Infinity → 0) — out-of-range operands wrap, not saturate.
|
|
1231
1345
|
fn emit_bitwise_binop(l: &str, r: &str, op: &str) -> String {
|
|
1232
1346
|
format!(
|
|
1233
1347
|
"Value::Number({{ let Value::Number(a) = &({}) else {{ panic!() }}; \
|
|
1234
|
-
let Value::Number(b) = &({}) else {{ panic!() }}; ((*a
|
|
1348
|
+
let Value::Number(b) = &({}) else {{ panic!() }}; (tishlang_runtime::to_int32(*a) {} tishlang_runtime::to_int32(*b)) as f64 }})",
|
|
1235
1349
|
l, r, op
|
|
1236
1350
|
)
|
|
1237
1351
|
}
|
|
1238
1352
|
|
|
1353
|
+
/// Generate code for a shift (`<< >> >>>`). `a_to` is the left-operand coercion
|
|
1354
|
+
/// (`to_int32` signed, `to_uint32` for the logical `>>>`); `method` is the `wrapping_sh*`
|
|
1355
|
+
/// call. Counts go through `to_uint32` then mask to 5 bits — exact JS semantics, panic-free.
|
|
1356
|
+
fn emit_shift_binop(l: &str, r: &str, a_to: &str, method: &str) -> String {
|
|
1357
|
+
format!(
|
|
1358
|
+
"Value::Number({{ let Value::Number(a) = &({}) else {{ panic!() }}; \
|
|
1359
|
+
let Value::Number(b) = &({}) else {{ panic!() }}; \
|
|
1360
|
+
tishlang_runtime::{}(*a).{}(tishlang_runtime::to_uint32(*b)) as f64 }})",
|
|
1361
|
+
l, r, a_to, method
|
|
1362
|
+
)
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1239
1365
|
fn write(&mut self, s: &str) {
|
|
1240
1366
|
self.output.push_str(s);
|
|
1241
1367
|
}
|
|
@@ -1308,7 +1434,7 @@ impl Codegen {
|
|
|
1308
1434
|
self.write("use std::cell::RefCell;\n");
|
|
1309
1435
|
self.write("use std::rc::Rc;\n");
|
|
1310
1436
|
self.write("use std::sync::Arc;\n");
|
|
1311
|
-
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, 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,
|
|
1437
|
+
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, 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, array_is_array as tish_array_is_array, 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, object_assign as tish_object_assign, 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, 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, TishError, Value, VmRef};\n");
|
|
1312
1438
|
if self.program_has_jsx {
|
|
1313
1439
|
self.write("use tishlang_ui::{fragment_value, install_thread_local_host, native_create_root, native_use_state, ui_h, ui_text, HeadlessHost};\n");
|
|
1314
1440
|
}
|
|
@@ -1319,8 +1445,11 @@ impl Codegen {
|
|
|
1319
1445
|
self.write("use tishlang_runtime::{timer_set_timeout as tish_timer_set_timeout, timer_clear_timeout as tish_timer_clear_timeout, timer_set_interval as tish_timer_set_interval, timer_clear_interval as tish_timer_clear_interval};\n");
|
|
1320
1446
|
}
|
|
1321
1447
|
if self.has_feature("http") {
|
|
1448
|
+
// `register_static_route` is http-gated in the runtime; emit its import only when http is
|
|
1449
|
+
// linked, else a non-http `tish build --feature …` fails with an unresolved import.
|
|
1450
|
+
self.write("use tishlang_runtime::register_static_route as tish_register_static_route;\n");
|
|
1322
1451
|
if self.is_async {
|
|
1323
|
-
self.write("use tishlang_runtime::{fetch_promise as tish_fetch_promise, fetch_all_promise as tish_fetch_all_promise, http_serve as tish_http_serve, promise_object as tish_promise_object, await_promise as tish_await_promise};\n");
|
|
1452
|
+
self.write("use tishlang_runtime::{fetch_promise as tish_fetch_promise, fetch_all_promise as tish_fetch_all_promise, http_serve as tish_http_serve, promise_object as tish_promise_object, await_promise as tish_await_promise, await_promise_throw as tish_await_promise_throw};\n");
|
|
1324
1453
|
} else {
|
|
1325
1454
|
self.write("use tishlang_runtime::{fetch_promise as tish_fetch_promise, fetch_all_promise as tish_fetch_all_promise, http_serve as tish_http_serve};\n");
|
|
1326
1455
|
}
|
|
@@ -1374,6 +1503,21 @@ impl Codegen {
|
|
|
1374
1503
|
self.writeln("}");
|
|
1375
1504
|
self.writeln("");
|
|
1376
1505
|
}
|
|
1506
|
+
// M5 (dark-shipped behind TISH_NATIVE_FN): emit a parallel native `fn f_native` for each
|
|
1507
|
+
// eligible top-level numeric fn at top level; direct calls route to it in emit_typed_expr.
|
|
1508
|
+
if std::env::var("TISH_NATIVE_FN").map(|v| v != "0").unwrap_or(false) {
|
|
1509
|
+
self.native_fns = Self::collect_native_fns(&program.statements);
|
|
1510
|
+
if !self.native_fns.is_empty() {
|
|
1511
|
+
self.emit_native_fns(&program.statements)?;
|
|
1512
|
+
self.writeln("");
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
// Soundness pass — must run after type aliases + `native_fns` are known (both feed the
|
|
1516
|
+
// native-type oracle): find `number`-typed locals a reassignment can turn non-numeric so
|
|
1517
|
+
// `VarDecl` lowers them as boxed `Value` rather than native `f64` (else the store coerces
|
|
1518
|
+
// and panics on a JS string-concat result like `s = s + arr[i]`). See
|
|
1519
|
+
// `collect_demoted_numeric_locals` / `demoted_numeric_locals`.
|
|
1520
|
+
self.demoted_numeric_locals = self.collect_demoted_numeric_locals(&program.statements);
|
|
1377
1521
|
if self.is_async {
|
|
1378
1522
|
self.writeln("async fn run() -> Result<(), Box<dyn std::error::Error>> {");
|
|
1379
1523
|
} else if self.emit_mode == crate::NativeEmitMode::EmbeddedLib {
|
|
@@ -1398,9 +1542,13 @@ impl Codegen {
|
|
|
1398
1542
|
self.writeln("let parseFloat = Value::native(|args: &[Value]| tish_parse_float(args));");
|
|
1399
1543
|
self.writeln("let decodeURI = Value::native(|args: &[Value]| tish_decode_uri(args));");
|
|
1400
1544
|
self.writeln("let encodeURI = Value::native(|args: &[Value]| tish_encode_uri(args));");
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
)
|
|
1545
|
+
// `registerStaticRoute` calls the http-gated runtime fn, so only bind it when http is linked
|
|
1546
|
+
// (matches the conditional `use` above; otherwise non-http builds fail to resolve it).
|
|
1547
|
+
if self.has_feature("http") {
|
|
1548
|
+
self.writeln(
|
|
1549
|
+
r#"let registerStaticRoute = Value::native(|args: &[Value]| { let path = match args.get(0) { Some(Value::String(s)) => s.to_string(), _ => return Value::Null }; let body = match args.get(1) { Some(Value::String(s)) => s.as_bytes().to_vec(), _ => return Value::Null }; let ct = match args.get(2) { Some(Value::String(s)) => s.to_string(), _ => "application/octet-stream".to_string() }; tish_register_static_route(&path, &body, &ct); Value::Null });"#,
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1404
1552
|
self.writeln(
|
|
1405
1553
|
"let htmlEscape = Value::native(|args: &[Value]| tish_escape_html(args.first().unwrap_or(&Value::Null)));",
|
|
1406
1554
|
);
|
|
@@ -1443,6 +1591,22 @@ impl Codegen {
|
|
|
1443
1591
|
self.writeln(
|
|
1444
1592
|
"(Arc::from(\"imul\"), Value::native(|args: &[Value]| tish_math_imul(args))),",
|
|
1445
1593
|
);
|
|
1594
|
+
// Hyperbolic / inverse-hyperbolic / cbrt / base-2/10 logs (issue #61).
|
|
1595
|
+
for (name, func) in [
|
|
1596
|
+
("sinh", "tish_math_sinh"),
|
|
1597
|
+
("cosh", "tish_math_cosh"),
|
|
1598
|
+
("tanh", "tish_math_tanh"),
|
|
1599
|
+
("asinh", "tish_math_asinh"),
|
|
1600
|
+
("acosh", "tish_math_acosh"),
|
|
1601
|
+
("atanh", "tish_math_atanh"),
|
|
1602
|
+
("cbrt", "tish_math_cbrt"),
|
|
1603
|
+
("log2", "tish_math_log2"),
|
|
1604
|
+
("log10", "tish_math_log10"),
|
|
1605
|
+
] {
|
|
1606
|
+
self.writeln(&format!(
|
|
1607
|
+
"(Arc::from(\"{name}\"), Value::native(|args: &[Value]| {func}(args))),"
|
|
1608
|
+
));
|
|
1609
|
+
}
|
|
1446
1610
|
self.writeln("(Arc::from(\"PI\"), Value::Number(std::f64::consts::PI)),");
|
|
1447
1611
|
self.writeln("(Arc::from(\"E\"), Value::Number(std::f64::consts::E)),");
|
|
1448
1612
|
self.indent -= 1;
|
|
@@ -1461,21 +1625,32 @@ impl Codegen {
|
|
|
1461
1625
|
self.writeln(
|
|
1462
1626
|
"(Arc::from(\"isArray\"), Value::native(|args: &[Value]| tish_array_is_array(args))),",
|
|
1463
1627
|
);
|
|
1628
|
+
// `Array(n)` / `new Array(n)` constructor (issue #72); `__call` covers both forms.
|
|
1629
|
+
self.writeln(
|
|
1630
|
+
"(Arc::from(\"__call\"), Value::native(|args: &[Value]| tish_array_construct(args))),",
|
|
1631
|
+
);
|
|
1464
1632
|
self.indent -= 1;
|
|
1465
1633
|
self.writeln("]));");
|
|
1466
1634
|
|
|
1467
1635
|
self.writeln("let String = Value::object(ObjectMap::from([");
|
|
1468
1636
|
self.indent += 1;
|
|
1469
1637
|
self.writeln("(Arc::from(\"fromCharCode\"), Value::native(|args: &[Value]| tish_string_from_char_code(args))),");
|
|
1638
|
+
// `String(value)` callable: `value_call` dispatches objects via `__call`, like `Symbol`.
|
|
1639
|
+
self.writeln("(Arc::from(\"__call\"), Value::native(|args: &[Value]| tish_string_convert(args))),");
|
|
1470
1640
|
self.indent -= 1;
|
|
1471
1641
|
self.writeln("]));");
|
|
1472
1642
|
|
|
1473
|
-
|
|
1643
|
+
// `Number(value)` coercion callable (issue #36).
|
|
1644
|
+
self.writeln("let Number = Value::object(ObjectMap::from([");
|
|
1474
1645
|
self.indent += 1;
|
|
1475
|
-
self.writeln("(Arc::from(\"
|
|
1646
|
+
self.writeln("(Arc::from(\"__call\"), Value::native(|args: &[Value]| tish_number_convert(args))),");
|
|
1476
1647
|
self.indent -= 1;
|
|
1477
1648
|
self.writeln("]));");
|
|
1478
1649
|
|
|
1650
|
+
self.writeln("let Date = tish_date_constructor();");
|
|
1651
|
+
self.writeln("let Set = tish_set_constructor();");
|
|
1652
|
+
self.writeln("let Map = tish_map_constructor();");
|
|
1653
|
+
|
|
1479
1654
|
self.writeln("let Symbol = tish_symbol_object();");
|
|
1480
1655
|
|
|
1481
1656
|
self.writeln("let Object = Value::object(ObjectMap::from([");
|
|
@@ -1496,8 +1671,20 @@ impl Codegen {
|
|
|
1496
1671
|
self.indent -= 1;
|
|
1497
1672
|
self.writeln("]));");
|
|
1498
1673
|
|
|
1674
|
+
self.writeln("let Float64Array = tish_float64_array_constructor();");
|
|
1675
|
+
self.writeln("let Float32Array = tish_float32_array_constructor();");
|
|
1676
|
+
self.writeln("let Int8Array = tish_int8_array_constructor();");
|
|
1499
1677
|
self.writeln("let Uint8Array = tish_uint8_array_constructor();");
|
|
1678
|
+
self.writeln("let Uint8ClampedArray = tish_uint8_clamped_array_constructor();");
|
|
1679
|
+
self.writeln("let Int16Array = tish_int16_array_constructor();");
|
|
1680
|
+
self.writeln("let Uint16Array = tish_uint16_array_constructor();");
|
|
1681
|
+
self.writeln("let Int32Array = tish_int32_array_constructor();");
|
|
1682
|
+
self.writeln("let Uint32Array = tish_uint32_array_constructor();");
|
|
1500
1683
|
self.writeln("let AudioContext = tish_audio_context_constructor();");
|
|
1684
|
+
// Error constructors (issue #60): `new Error(msg)` / `Error(msg)` → `{ name, message }`.
|
|
1685
|
+
for name in ["Error", "TypeError", "RangeError", "SyntaxError"] {
|
|
1686
|
+
self.writeln(&format!("let {name} = tish_error_constructor({name:?});"));
|
|
1687
|
+
}
|
|
1501
1688
|
if self.program_uses_document {
|
|
1502
1689
|
self.writeln("let document = VmRef::new(tish_canvas_document());");
|
|
1503
1690
|
self.refcell_wrapped_vars.insert("document".to_string());
|
|
@@ -1560,7 +1747,7 @@ impl Codegen {
|
|
|
1560
1747
|
self.writeln("match handler {");
|
|
1561
1748
|
self.indent += 1;
|
|
1562
1749
|
self.writeln(
|
|
1563
|
-
"Value::Function(f) => tish_http_serve(args, move |req_args| f(req_args)),",
|
|
1750
|
+
"Value::Function(f) => tish_http_serve(args, move |req_args| f.call(req_args)),",
|
|
1564
1751
|
);
|
|
1565
1752
|
self.writeln("Value::Object(ref opts) => {");
|
|
1566
1753
|
self.indent += 1;
|
|
@@ -1620,7 +1807,7 @@ impl Codegen {
|
|
|
1620
1807
|
self.usage_analyzer = Some(analyzer);
|
|
1621
1808
|
|
|
1622
1809
|
// Prepass: vars mutated by nested closures must be RefCell from the start (top-level)
|
|
1623
|
-
let top_level_mutated = Self::
|
|
1810
|
+
let top_level_mutated = Self::collect_vars_needing_capture_cell(&program.statements);
|
|
1624
1811
|
for v in &top_level_mutated {
|
|
1625
1812
|
self.refcell_wrapped_vars.insert(v.clone());
|
|
1626
1813
|
}
|
|
@@ -1635,6 +1822,14 @@ impl Codegen {
|
|
|
1635
1822
|
self.async_context_stack.pop();
|
|
1636
1823
|
}
|
|
1637
1824
|
|
|
1825
|
+
// Run pending timers to completion before exiting — the JS event loop drains the
|
|
1826
|
+
// timer queue after top-level code finishes. Without this the rust backend drops
|
|
1827
|
+
// `setTimeout(cb, 0)` callbacks that never coincided with a blocking-op drain,
|
|
1828
|
+
// diverging from interp/vm/cranelift/wasi (which drain at end-of-program).
|
|
1829
|
+
if self.has_feature("timers") {
|
|
1830
|
+
self.writeln("tishlang_runtime::drain_timers();");
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1638
1833
|
self.writeln("Ok(())");
|
|
1639
1834
|
self.indent -= 1;
|
|
1640
1835
|
self.writeln("}");
|
|
@@ -1655,6 +1850,122 @@ impl Codegen {
|
|
|
1655
1850
|
Ok(())
|
|
1656
1851
|
}
|
|
1657
1852
|
|
|
1853
|
+
/// Emit an expression in **statement position** (its value is discarded). For a native
|
|
1854
|
+
/// assignment this emits only the side-effect — NOT the boxed `Value::Number(..)` that the
|
|
1855
|
+
/// expression form returns (JS "assignment yields its value"). In a hot loop that boxed
|
|
1856
|
+
/// value was constructed + dropped every iteration, and because `Value` has a non-trivial
|
|
1857
|
+
/// `Drop` (other variants hold `Rc`/`Arc`) LLVM couldn't prove it dead — so it could not
|
|
1858
|
+
/// vectorize/fold the loop. Falls back to `emit_expr` for everything else (whose trailing
|
|
1859
|
+
/// value is simply dropped by the `;`).
|
|
1860
|
+
fn emit_expr_discard(&mut self, expr: &Expr) -> Result<String, CompileError> {
|
|
1861
|
+
match expr {
|
|
1862
|
+
Expr::Assign { name, value, .. } => {
|
|
1863
|
+
let rust_type = self.type_context.get_type(name.as_ref());
|
|
1864
|
+
// String self-append `s = s + rhs` -> in-place push_str (amortized O(1)). The
|
|
1865
|
+
// general path boxes via `ops::add(Value::String(s.clone()), ...)` which clones
|
|
1866
|
+
// the whole string per concat -> O(n^2) string building. rhs must be String-typed.
|
|
1867
|
+
if rust_type == RustType::String {
|
|
1868
|
+
if let Expr::Binary {
|
|
1869
|
+
left,
|
|
1870
|
+
op: BinOp::Add,
|
|
1871
|
+
right,
|
|
1872
|
+
..
|
|
1873
|
+
} = value.as_ref()
|
|
1874
|
+
{
|
|
1875
|
+
if matches!(left.as_ref(), Expr::Ident { name: ln, .. } if ln.as_ref() == name.as_ref())
|
|
1876
|
+
{
|
|
1877
|
+
let (rhs_code, rhs_ty) = self.emit_typed_expr(right.as_ref())?;
|
|
1878
|
+
if rhs_ty == RustType::String {
|
|
1879
|
+
let escaped = Self::escape_ident(name.as_ref());
|
|
1880
|
+
if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
1881
|
+
return Ok(format!(
|
|
1882
|
+
"{{ let _r = {}; {}.borrow_mut().push_str(&_r); }}",
|
|
1883
|
+
rhs_code, escaped
|
|
1884
|
+
));
|
|
1885
|
+
}
|
|
1886
|
+
return Ok(format!(
|
|
1887
|
+
"{{ let _r = {}; {}.push_str(&_r); }}",
|
|
1888
|
+
rhs_code, escaped
|
|
1889
|
+
));
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
if matches!(rust_type, RustType::F64 | RustType::Bool | RustType::String) {
|
|
1895
|
+
let escaped = Self::escape_ident(name.as_ref());
|
|
1896
|
+
let is_ref = self.refcell_wrapped_vars.contains(name.as_ref());
|
|
1897
|
+
let (val_code, val_ty) = self.emit_typed_expr(value)?;
|
|
1898
|
+
let native_val = if val_ty == RustType::Value {
|
|
1899
|
+
rust_type.from_value_expr(&val_code)
|
|
1900
|
+
} else {
|
|
1901
|
+
val_code
|
|
1902
|
+
};
|
|
1903
|
+
if is_ref {
|
|
1904
|
+
return Ok(format!(
|
|
1905
|
+
"{{ let _assign_tmp = {}; *{}.borrow_mut() = _assign_tmp; }}",
|
|
1906
|
+
native_val, escaped
|
|
1907
|
+
));
|
|
1908
|
+
}
|
|
1909
|
+
return Ok(format!("{} = {}", escaped, native_val));
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
// `i++` / `++i` / `i--` / `--i` in statement position (incl. for-loop update):
|
|
1913
|
+
// emit just the native increment, no boxed `Value::Number(_prev)`.
|
|
1914
|
+
Expr::PostfixInc { name, .. } | Expr::PrefixInc { name, .. } => {
|
|
1915
|
+
if self.type_context.get_type(name.as_ref()) == RustType::F64 {
|
|
1916
|
+
let n = Self::escape_ident(name.as_ref());
|
|
1917
|
+
if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
1918
|
+
return Ok(format!("*{}.borrow_mut() += 1.0_f64", n));
|
|
1919
|
+
}
|
|
1920
|
+
return Ok(format!("{} += 1.0_f64", n));
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
Expr::PostfixDec { name, .. } | Expr::PrefixDec { name, .. } => {
|
|
1924
|
+
if self.type_context.get_type(name.as_ref()) == RustType::F64 {
|
|
1925
|
+
let n = Self::escape_ident(name.as_ref());
|
|
1926
|
+
if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
1927
|
+
return Ok(format!("*{}.borrow_mut() -= 1.0_f64", n));
|
|
1928
|
+
}
|
|
1929
|
+
return Ok(format!("{} -= 1.0_f64", n));
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
// `s += x` etc. in statement position: native f64 compound op, no boxed return.
|
|
1933
|
+
Expr::CompoundAssign { name, op, value, .. } => {
|
|
1934
|
+
if self.type_context.get_type(name.as_ref()) == RustType::F64 {
|
|
1935
|
+
let n = Self::escape_ident(name.as_ref());
|
|
1936
|
+
let is_refcell = self.refcell_wrapped_vars.contains(name.as_ref());
|
|
1937
|
+
let (rhs_code, rhs_ty) = self.emit_typed_expr(value)?;
|
|
1938
|
+
let rhs_f64 = if rhs_ty == RustType::F64 {
|
|
1939
|
+
rhs_code
|
|
1940
|
+
} else {
|
|
1941
|
+
let rhs_val = if rhs_ty.is_native() {
|
|
1942
|
+
rhs_ty.to_value_expr(&rhs_code)
|
|
1943
|
+
} else {
|
|
1944
|
+
rhs_code
|
|
1945
|
+
};
|
|
1946
|
+
format!("(match &({}) {{ Value::Number(n) => *n, v => panic!(\"compound assign: expected number, got {{:?}}\", v) }})", rhs_val)
|
|
1947
|
+
};
|
|
1948
|
+
let op_str = match op {
|
|
1949
|
+
CompoundOp::Add => "+=",
|
|
1950
|
+
CompoundOp::Sub => "-=",
|
|
1951
|
+
CompoundOp::Mul => "*=",
|
|
1952
|
+
CompoundOp::Div => "/=",
|
|
1953
|
+
CompoundOp::Mod => "%=",
|
|
1954
|
+
};
|
|
1955
|
+
if is_refcell {
|
|
1956
|
+
return Ok(format!(
|
|
1957
|
+
"{{ let _op_rhs = {}; *{}.borrow_mut() {} _op_rhs; }}",
|
|
1958
|
+
rhs_f64, n, op_str
|
|
1959
|
+
));
|
|
1960
|
+
}
|
|
1961
|
+
return Ok(format!("{} {} {}", n, op_str, rhs_f64));
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
_ => {}
|
|
1965
|
+
}
|
|
1966
|
+
self.emit_expr(expr)
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1658
1969
|
fn emit_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
|
|
1659
1970
|
match stmt {
|
|
1660
1971
|
Statement::Block { statements, .. } => {
|
|
@@ -1666,7 +1977,7 @@ impl Codegen {
|
|
|
1666
1977
|
.push(std::collections::HashSet::new());
|
|
1667
1978
|
// Prepass: vars that must be RefCell because nested closures capture and mutate them
|
|
1668
1979
|
let vars_mutated_by_nested =
|
|
1669
|
-
Self::
|
|
1980
|
+
Self::collect_vars_needing_capture_cell(statements);
|
|
1670
1981
|
for v in &vars_mutated_by_nested {
|
|
1671
1982
|
self.refcell_wrapped_vars.insert(v.clone());
|
|
1672
1983
|
}
|
|
@@ -1694,6 +2005,13 @@ impl Codegen {
|
|
|
1694
2005
|
self.indent -= 1;
|
|
1695
2006
|
self.writeln("}");
|
|
1696
2007
|
}
|
|
2008
|
+
// Comma-declarators: emit each declarator into the *current* Rust scope
|
|
2009
|
+
// (no wrapping `{}`), so the bindings stay visible to later statements.
|
|
2010
|
+
Statement::Multi { statements, .. } => {
|
|
2011
|
+
for s in statements {
|
|
2012
|
+
self.emit_statement(s)?;
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
1697
2015
|
Statement::VarDecl {
|
|
1698
2016
|
name,
|
|
1699
2017
|
mutable,
|
|
@@ -1705,13 +2023,22 @@ impl Codegen {
|
|
|
1705
2023
|
// user-declared `type` aliases so a `let x: World = ...`
|
|
1706
2024
|
// resolves to `RustType::Named { name: "World", fields }`
|
|
1707
2025
|
// and we can emit a struct move instead of a Value box.
|
|
1708
|
-
let rust_type = type_ann
|
|
2026
|
+
let mut rust_type = type_ann
|
|
1709
2027
|
.as_ref()
|
|
1710
2028
|
.map(|t| {
|
|
1711
2029
|
crate::types::RustType::from_annotation_with_aliases(t, &self.type_aliases)
|
|
1712
2030
|
})
|
|
1713
2031
|
.unwrap_or(RustType::Value);
|
|
1714
2032
|
|
|
2033
|
+
// Soundness: a `number` local that a reassignment can turn non-numeric (e.g.
|
|
2034
|
+
// `s = s + arr[i]`, JS string concat) must stay a boxed `Value` — a native-f64
|
|
2035
|
+
// store would panic at the `from_value_expr(F64)` coercion. See
|
|
2036
|
+
// `demoted_numeric_locals`.
|
|
2037
|
+
if rust_type == RustType::F64 && self.demoted_numeric_locals.contains(name.as_ref())
|
|
2038
|
+
{
|
|
2039
|
+
rust_type = RustType::Value;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
1715
2042
|
// Track the variable type
|
|
1716
2043
|
self.type_context.define(name.as_ref(), rust_type.clone());
|
|
1717
2044
|
|
|
@@ -1789,7 +2116,7 @@ impl Codegen {
|
|
|
1789
2116
|
self.register_destruct_pattern_outer_vars(pattern);
|
|
1790
2117
|
}
|
|
1791
2118
|
Statement::ExprStmt { expr, .. } => {
|
|
1792
|
-
let e = self.
|
|
2119
|
+
let e = self.emit_expr_discard(expr)?;
|
|
1793
2120
|
self.writeln(&format!("{};", e));
|
|
1794
2121
|
}
|
|
1795
2122
|
Statement::If {
|
|
@@ -1816,9 +2143,11 @@ impl Codegen {
|
|
|
1816
2143
|
let label = format!("'while_loop_{}", self.loop_label_index);
|
|
1817
2144
|
self.loop_label_index += 1;
|
|
1818
2145
|
self.loop_stack.push((label.clone(), None));
|
|
2146
|
+
self.break_stack.push(label.clone());
|
|
1819
2147
|
self.write(&format!("{}: while {} {{\n", label, c));
|
|
1820
2148
|
self.indent += 1;
|
|
1821
2149
|
self.emit_statement(body)?;
|
|
2150
|
+
self.break_stack.pop();
|
|
1822
2151
|
self.loop_stack.pop();
|
|
1823
2152
|
self.indent -= 1;
|
|
1824
2153
|
self.writeln("}");
|
|
@@ -1829,42 +2158,102 @@ impl Codegen {
|
|
|
1829
2158
|
body,
|
|
1830
2159
|
..
|
|
1831
2160
|
} => {
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
2161
|
+
// M3 native fast path: the iterable is a `Vec<elem>` local with a native element
|
|
2162
|
+
// type (e.g. `let xs: number[]` -> `Vec<f64>`) and the body never mentions it (so
|
|
2163
|
+
// iterating by reference can't alias a mutation). Bind the loop var as `elem` so the
|
|
2164
|
+
// body lowers natively — no per-element `Value::clone`, and accumulators stay f64.
|
|
2165
|
+
let mut emitted_native = false;
|
|
2166
|
+
if let Expr::Ident { name: it_name, .. } = iterable {
|
|
2167
|
+
if let RustType::Vec(elem) = self.type_context.get_type(it_name.as_ref()) {
|
|
2168
|
+
if elem.is_native() {
|
|
2169
|
+
let mut body_idents = std::collections::HashSet::new();
|
|
2170
|
+
Self::collect_stmt_idents(body, &mut body_idents);
|
|
2171
|
+
if !body_idents.contains(it_name.as_ref()) {
|
|
2172
|
+
let esc_it = Self::escape_ident(it_name.as_ref()).into_owned();
|
|
2173
|
+
let esc_name = Self::escape_ident(name.as_ref()).into_owned();
|
|
2174
|
+
// Index-based iteration (not `.iter().cloned()`, which rustc fails to
|
|
2175
|
+
// tighten here): `0..len` indexing of a `Vec<f64>` matches a hand-
|
|
2176
|
+
// written C-style loop. Unique counter names keep nested ForOf sound.
|
|
2177
|
+
let idx = self.loop_label_index;
|
|
2178
|
+
self.loop_label_index += 1;
|
|
2179
|
+
let copy_elem = matches!(*elem, RustType::F64 | RustType::Bool);
|
|
2180
|
+
let bind = if copy_elem {
|
|
2181
|
+
format!("let {} = {}[_fof_i{}];", esc_name, esc_it, idx)
|
|
2182
|
+
} else {
|
|
2183
|
+
format!("let {} = {}[_fof_i{}].clone();", esc_name, esc_it, idx)
|
|
2184
|
+
};
|
|
2185
|
+
self.writeln(&format!("for _fof_i{} in 0..{}.len() {{", idx, esc_it));
|
|
2186
|
+
self.indent += 1;
|
|
2187
|
+
self.writeln(&bind);
|
|
2188
|
+
self.type_context.push_scope();
|
|
2189
|
+
self.type_context.define(name.as_ref(), *elem);
|
|
2190
|
+
self.emit_statement(body)?;
|
|
2191
|
+
self.type_context.pop_scope();
|
|
2192
|
+
self.indent -= 1;
|
|
2193
|
+
self.writeln("}");
|
|
2194
|
+
emitted_native = true;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
if !emitted_native {
|
|
2200
|
+
let iter_expr = self.emit_expr(iterable)?;
|
|
2201
|
+
// `normalize_for_of` drains a JS iterator object (Map/Set `.values()` etc.)
|
|
2202
|
+
// into an array; arrays/strings/everything else pass through unchanged.
|
|
2203
|
+
self.writeln(&format!(
|
|
2204
|
+
"{{ let _fof = tishlang_runtime::normalize_for_of(({}).clone());",
|
|
2205
|
+
iter_expr
|
|
2206
|
+
));
|
|
2207
|
+
self.indent += 1;
|
|
2208
|
+
self.writeln("match &_fof {");
|
|
2209
|
+
self.indent += 1;
|
|
2210
|
+
self.writeln("Value::Array(ref _arr) => {");
|
|
2211
|
+
self.indent += 1;
|
|
2212
|
+
self.writeln("for _v in _arr.borrow().iter() {");
|
|
2213
|
+
self.indent += 1;
|
|
2214
|
+
self.writeln(&format!(
|
|
2215
|
+
"let {} = _v.clone();",
|
|
2216
|
+
Self::escape_ident(name.as_ref())
|
|
2217
|
+
));
|
|
2218
|
+
self.emit_statement(body)?;
|
|
2219
|
+
self.indent -= 1;
|
|
2220
|
+
self.writeln("}");
|
|
2221
|
+
self.indent -= 1;
|
|
2222
|
+
self.writeln("}");
|
|
2223
|
+
// Packed `Float64Array` (`TISH_PACKED_ARRAYS`): iterate the `Vec<f64>` directly,
|
|
2224
|
+
// re-boxing each element to `Value::Number` for the loop body.
|
|
2225
|
+
self.writeln("Value::NumberArray(ref _arr) => {");
|
|
2226
|
+
self.indent += 1;
|
|
2227
|
+
self.writeln("for _v in _arr.borrow().iter() {");
|
|
2228
|
+
self.indent += 1;
|
|
2229
|
+
self.writeln(&format!(
|
|
2230
|
+
"let {} = Value::Number(*_v);",
|
|
2231
|
+
Self::escape_ident(name.as_ref())
|
|
2232
|
+
));
|
|
2233
|
+
self.emit_statement(body)?;
|
|
2234
|
+
self.indent -= 1;
|
|
2235
|
+
self.writeln("}");
|
|
2236
|
+
self.indent -= 1;
|
|
2237
|
+
self.writeln("}");
|
|
2238
|
+
self.writeln("Value::String(ref _s) => {");
|
|
2239
|
+
self.indent += 1;
|
|
2240
|
+
self.writeln("for _ch in _s.chars() {");
|
|
2241
|
+
self.indent += 1;
|
|
2242
|
+
self.writeln(&format!(
|
|
2243
|
+
"let {} = Value::String(tishlang_runtime::ArcStr::from(_ch.to_string()));",
|
|
2244
|
+
Self::escape_ident(name.as_ref())
|
|
2245
|
+
));
|
|
2246
|
+
self.emit_statement(body)?;
|
|
2247
|
+
self.indent -= 1;
|
|
2248
|
+
self.writeln("}");
|
|
2249
|
+
self.indent -= 1;
|
|
2250
|
+
self.writeln("}");
|
|
2251
|
+
self.writeln("_ => panic!(\"for-of requires array or string\"),");
|
|
2252
|
+
self.indent -= 1;
|
|
2253
|
+
self.writeln("}");
|
|
2254
|
+
self.indent -= 1;
|
|
2255
|
+
self.writeln("}");
|
|
2256
|
+
}
|
|
1868
2257
|
}
|
|
1869
2258
|
Statement::For {
|
|
1870
2259
|
init,
|
|
@@ -1885,18 +2274,20 @@ impl Codegen {
|
|
|
1885
2274
|
.map(|c| self.emit_cond_expr(c).unwrap())
|
|
1886
2275
|
.unwrap_or_else(|| "true".to_string());
|
|
1887
2276
|
let update_code = update.as_ref().map(|u| {
|
|
1888
|
-
let ue = self.
|
|
2277
|
+
let ue = self.emit_expr_discard(u).unwrap();
|
|
1889
2278
|
format!("{};", ue)
|
|
1890
2279
|
});
|
|
1891
2280
|
self.loop_stack.push((label.clone(), update_code));
|
|
2281
|
+
self.break_stack.push(label.clone());
|
|
1892
2282
|
self.write(&format!("{}: loop {{\n", label));
|
|
1893
2283
|
self.indent += 1;
|
|
1894
2284
|
self.writeln(&format!("if !{} {{ break; }}", cond_expr));
|
|
1895
2285
|
self.emit_statement(body)?;
|
|
1896
2286
|
if let Some(u) = update {
|
|
1897
|
-
let ue = self.
|
|
2287
|
+
let ue = self.emit_expr_discard(u)?;
|
|
1898
2288
|
self.writeln(&format!("{};", ue));
|
|
1899
2289
|
}
|
|
2290
|
+
self.break_stack.pop();
|
|
1900
2291
|
self.loop_stack.pop();
|
|
1901
2292
|
self.indent -= 1;
|
|
1902
2293
|
self.writeln("}");
|
|
@@ -1909,10 +2300,18 @@ impl Codegen {
|
|
|
1909
2300
|
.map(|e| self.emit_expr(e))
|
|
1910
2301
|
.transpose()?
|
|
1911
2302
|
.unwrap_or_else(|| "Value::Null".to_string());
|
|
1912
|
-
self.
|
|
2303
|
+
if self.try_closure_depth > 0 {
|
|
2304
|
+
// Inside a try-body closure: escape it as a pending-return completion so any
|
|
2305
|
+
// enclosing `finally` runs on the way out to the function boundary.
|
|
2306
|
+
self.writeln(&format!("return Ok(Some({}));", v));
|
|
2307
|
+
} else {
|
|
2308
|
+
self.writeln(&format!("return {};", v));
|
|
2309
|
+
}
|
|
1913
2310
|
}
|
|
1914
2311
|
Statement::Break { .. } => {
|
|
1915
|
-
|
|
2312
|
+
// `break` exits the innermost loop OR switch (break_stack), not necessarily the
|
|
2313
|
+
// innermost loop. A switch pushes a label here so its `break` stays switch-local.
|
|
2314
|
+
if let Some(label) = self.break_stack.last() {
|
|
1916
2315
|
self.writeln(&format!("break {};", label));
|
|
1917
2316
|
} else {
|
|
1918
2317
|
self.writeln("break;");
|
|
@@ -1949,6 +2348,13 @@ impl Codegen {
|
|
|
1949
2348
|
} => {
|
|
1950
2349
|
let e = self.emit_expr(expr)?;
|
|
1951
2350
|
self.writeln(&format!("let _sv = {};", e));
|
|
2351
|
+
// Wrap in a labeled block so `break` inside a case exits the SWITCH, not an
|
|
2352
|
+
// enclosing loop. tish switch has no fall-through (match's first-arm semantics).
|
|
2353
|
+
let sw_label = format!("'switch_{}", self.loop_label_index);
|
|
2354
|
+
self.loop_label_index += 1;
|
|
2355
|
+
self.break_stack.push(sw_label.clone());
|
|
2356
|
+
self.write(&format!("{}: {{\n", sw_label));
|
|
2357
|
+
self.indent += 1;
|
|
1952
2358
|
self.writeln("match () {");
|
|
1953
2359
|
self.indent += 1;
|
|
1954
2360
|
for (case_expr, body) in cases {
|
|
@@ -1978,30 +2384,39 @@ impl Codegen {
|
|
|
1978
2384
|
}
|
|
1979
2385
|
self.indent -= 1;
|
|
1980
2386
|
self.writeln("}");
|
|
2387
|
+
self.indent -= 1;
|
|
2388
|
+
self.writeln("}");
|
|
2389
|
+
self.break_stack.pop();
|
|
1981
2390
|
}
|
|
1982
2391
|
Statement::DoWhile { body, cond, .. } => {
|
|
1983
2392
|
let c = self.emit_cond_expr(cond)?;
|
|
1984
2393
|
let label = format!("'dowhile_loop_{}", self.loop_label_index);
|
|
1985
2394
|
self.loop_label_index += 1;
|
|
1986
2395
|
self.loop_stack.push((label.clone(), None));
|
|
2396
|
+
self.break_stack.push(label.clone());
|
|
1987
2397
|
self.write(&format!("{}: loop {{\n", label));
|
|
1988
2398
|
self.indent += 1;
|
|
1989
2399
|
self.emit_statement(body)?;
|
|
1990
2400
|
self.write(&format!("if !{} {{ break; }}\n", c));
|
|
2401
|
+
self.break_stack.pop();
|
|
1991
2402
|
self.loop_stack.pop();
|
|
1992
2403
|
self.indent -= 1;
|
|
1993
2404
|
self.writeln("}");
|
|
1994
2405
|
}
|
|
1995
2406
|
Statement::Throw { value, .. } => {
|
|
1996
2407
|
let v = self.emit_expr(value)?;
|
|
1997
|
-
if self.
|
|
2408
|
+
if self.try_closure_depth > 0 || self.value_fn_depth == 0 {
|
|
2409
|
+
// Inside a try-body closure (so `catch`/`finally` can see it) or at top level
|
|
2410
|
+
// (run() returns a Result): a catchable error completion.
|
|
1998
2411
|
self.writeln(&format!(
|
|
1999
|
-
"
|
|
2412
|
+
"return Err(Box::new(tishlang_runtime::TishError::Throw({})) as Box<dyn std::error::Error>);",
|
|
2000
2413
|
v
|
|
2001
2414
|
));
|
|
2002
2415
|
} else {
|
|
2416
|
+
// Top of a value-fn body with no enclosing try: there is no error channel
|
|
2417
|
+
// across the native-fn ABI, so an uncaught throw aborts (matches prior behavior).
|
|
2003
2418
|
self.writeln(&format!(
|
|
2004
|
-
"
|
|
2419
|
+
"{{ let _th = {}; panic!(\"uncaught throw: {{}}\", _th.to_display_string()); }}",
|
|
2005
2420
|
v
|
|
2006
2421
|
));
|
|
2007
2422
|
}
|
|
@@ -2013,58 +2428,81 @@ impl Codegen {
|
|
|
2013
2428
|
finally_body,
|
|
2014
2429
|
..
|
|
2015
2430
|
} => {
|
|
2016
|
-
|
|
2431
|
+
// The try body runs in a completion closure:
|
|
2432
|
+
// Ok(None) = ran to the end normally
|
|
2433
|
+
// Ok(Some(v)) = a `return v` is pending (must run finally, then return)
|
|
2434
|
+
// Err(Throw) = a `throw` is pending (catchable; else runs finally then re-raises)
|
|
2435
|
+
// `return`/`throw` inside the body emit the closure-escaping form (try_closure_depth).
|
|
2436
|
+
self.writeln(
|
|
2437
|
+
"let mut _flow: Result<Option<Value>, Box<dyn std::error::Error>> = (|| {",
|
|
2438
|
+
);
|
|
2017
2439
|
self.indent += 1;
|
|
2440
|
+
self.try_closure_depth += 1;
|
|
2018
2441
|
self.emit_statement(body)?;
|
|
2019
|
-
self.
|
|
2442
|
+
self.try_closure_depth -= 1;
|
|
2443
|
+
self.writeln("Ok(None)");
|
|
2020
2444
|
self.indent -= 1;
|
|
2021
2445
|
self.writeln("})();");
|
|
2022
2446
|
|
|
2023
2447
|
if let Some(catch_stmt) = catch_body {
|
|
2448
|
+
// Only a `throw` is catchable; a pending `return` (Ok(Some)) bypasses catch.
|
|
2449
|
+
self.writeln("_flow = match _flow {");
|
|
2450
|
+
self.indent += 1;
|
|
2451
|
+
self.writeln("Err(_e) => match _e.downcast::<tishlang_runtime::TishError>() {");
|
|
2452
|
+
self.indent += 1;
|
|
2453
|
+
self.writeln("Ok(_te) => match *_te {");
|
|
2454
|
+
self.indent += 1;
|
|
2455
|
+
self.writeln("tishlang_runtime::TishError::Throw(_tv) => {");
|
|
2456
|
+
self.indent += 1;
|
|
2024
2457
|
if let Some(param) = catch_param {
|
|
2025
|
-
self.writeln("
|
|
2026
|
-
self.indent += 1;
|
|
2027
|
-
self.writeln("match e.downcast::<tishlang_runtime::TishError>() {");
|
|
2028
|
-
self.indent += 1;
|
|
2029
|
-
self.writeln("Ok(tish_err) => {");
|
|
2030
|
-
self.indent += 1;
|
|
2031
|
-
self.writeln("if let tishlang_runtime::TishError::Throw(v) = *tish_err {");
|
|
2032
|
-
self.writeln(&format!(
|
|
2033
|
-
"let {} = v.clone();",
|
|
2034
|
-
Self::escape_ident(param.as_ref())
|
|
2035
|
-
));
|
|
2036
|
-
self.emit_statement(catch_stmt)?;
|
|
2037
|
-
if self.value_fn_depth > 0 {
|
|
2038
|
-
self.writeln(
|
|
2039
|
-
"} else { panic!(\"unhandled error in native Tish: {:?}\", *tish_err); }",
|
|
2040
|
-
);
|
|
2041
|
-
} else {
|
|
2042
|
-
self.writeln("} else { return Err(Box::new(tish_err)); }");
|
|
2043
|
-
}
|
|
2044
|
-
self.indent -= 1;
|
|
2045
|
-
self.writeln("}");
|
|
2046
|
-
if self.value_fn_depth > 0 {
|
|
2047
|
-
self.writeln(
|
|
2048
|
-
"Err(orig) => panic!(\"non-Tish error in native Tish: {:?}\", orig),",
|
|
2049
|
-
);
|
|
2050
|
-
} else {
|
|
2051
|
-
self.writeln("Err(orig) => return Err(orig),");
|
|
2052
|
-
}
|
|
2053
|
-
self.indent -= 1;
|
|
2054
|
-
self.writeln("}");
|
|
2055
|
-
self.indent -= 1;
|
|
2056
|
-
} else {
|
|
2057
|
-
self.writeln("if let Err(_e) = _try_result {");
|
|
2058
|
-
self.indent += 1;
|
|
2059
|
-
self.emit_statement(catch_stmt)?;
|
|
2060
|
-
self.indent -= 1;
|
|
2458
|
+
self.writeln(&format!("let {} = _tv;", Self::escape_ident(param.as_ref())));
|
|
2061
2459
|
}
|
|
2460
|
+
self.writeln(
|
|
2461
|
+
"(|| -> Result<Option<Value>, Box<dyn std::error::Error>> {",
|
|
2462
|
+
);
|
|
2463
|
+
self.indent += 1;
|
|
2464
|
+
self.try_closure_depth += 1;
|
|
2465
|
+
self.emit_statement(catch_stmt)?;
|
|
2466
|
+
self.try_closure_depth -= 1;
|
|
2467
|
+
self.writeln("Ok(None)");
|
|
2468
|
+
self.indent -= 1;
|
|
2469
|
+
self.writeln("})()");
|
|
2470
|
+
self.indent -= 1;
|
|
2062
2471
|
self.writeln("}");
|
|
2472
|
+
self.writeln("_other => Err(Box::new(_other)),");
|
|
2473
|
+
self.indent -= 1;
|
|
2474
|
+
self.writeln("},");
|
|
2475
|
+
self.writeln("Err(_orig) => Err(_orig),");
|
|
2476
|
+
self.indent -= 1;
|
|
2477
|
+
self.writeln("},");
|
|
2478
|
+
self.writeln("_ok => _ok,");
|
|
2479
|
+
self.indent -= 1;
|
|
2480
|
+
self.writeln("};");
|
|
2063
2481
|
}
|
|
2064
2482
|
|
|
2065
2483
|
if let Some(finally_stmt) = finally_body {
|
|
2066
2484
|
self.emit_statement(finally_stmt)?;
|
|
2067
2485
|
}
|
|
2486
|
+
|
|
2487
|
+
// After finally, propagate any pending completion in the form the enclosing context
|
|
2488
|
+
// expects (an outer try-closure / a value-fn body / top-level run()).
|
|
2489
|
+
self.writeln("match _flow {");
|
|
2490
|
+
self.indent += 1;
|
|
2491
|
+
if self.try_closure_depth > 0 {
|
|
2492
|
+
self.writeln("Ok(Some(_rv)) => return Ok(Some(_rv)),");
|
|
2493
|
+
self.writeln("Err(_e) => return Err(_e),");
|
|
2494
|
+
} else if self.value_fn_depth > 0 {
|
|
2495
|
+
self.writeln("Ok(Some(_rv)) => return _rv,");
|
|
2496
|
+
self.writeln("Err(_e) => return tishlang_runtime::fn_unwind(_e),");
|
|
2497
|
+
} else {
|
|
2498
|
+
// Top level (run() -> Result<(), _>): a top-level `return value` just ends the
|
|
2499
|
+
// script (the value is unobservable); an uncaught throw propagates out of run().
|
|
2500
|
+
self.writeln("Ok(Some(_)) => return Ok(()),");
|
|
2501
|
+
self.writeln("Err(_e) => return Err(_e),");
|
|
2502
|
+
}
|
|
2503
|
+
self.writeln("Ok(None) => {}");
|
|
2504
|
+
self.indent -= 1;
|
|
2505
|
+
self.writeln("}");
|
|
2068
2506
|
}
|
|
2069
2507
|
Statement::FunDecl {
|
|
2070
2508
|
name,
|
|
@@ -2127,6 +2565,8 @@ impl Codegen {
|
|
|
2127
2565
|
"Math",
|
|
2128
2566
|
"JSON",
|
|
2129
2567
|
"Date",
|
|
2568
|
+
"Set",
|
|
2569
|
+
"Map",
|
|
2130
2570
|
"Object",
|
|
2131
2571
|
"process",
|
|
2132
2572
|
"setTimeout",
|
|
@@ -2142,18 +2582,22 @@ impl Codegen {
|
|
|
2142
2582
|
})
|
|
2143
2583
|
.collect();
|
|
2144
2584
|
|
|
2145
|
-
//
|
|
2146
|
-
//
|
|
2585
|
+
// Live cell capture: assigned in this body, or already a shared
|
|
2586
|
+
// `VmRef` cell in a parent scope (so a closure that only READS the
|
|
2587
|
+
// var still sees later mutations through the shared cell, instead
|
|
2588
|
+
// of snapshotting it by value at creation time). Truly read-only,
|
|
2589
|
+
// non-cell vars get a Value snapshot (avoids param-shadow issues).
|
|
2590
|
+
// Mirrors `emit_arrow_function`.
|
|
2147
2591
|
let mut assigned_in_body = HashSet::new();
|
|
2148
2592
|
Self::collect_assigned_idents_in_stmt(body, &mut assigned_in_body);
|
|
2149
2593
|
let mutable_outer_vars: Vec<String> = outer_vars
|
|
2150
2594
|
.iter()
|
|
2151
|
-
.filter(|v| assigned_in_body.contains(*v))
|
|
2595
|
+
.filter(|v| assigned_in_body.contains(*v) || self.rc_cell_storage_contains(v))
|
|
2152
2596
|
.cloned()
|
|
2153
2597
|
.collect();
|
|
2154
2598
|
let read_only_outer_vars: Vec<String> = outer_vars
|
|
2155
2599
|
.iter()
|
|
2156
|
-
.filter(|v| !assigned_in_body.contains(*v))
|
|
2600
|
+
.filter(|v| !assigned_in_body.contains(*v) && !self.rc_cell_storage_contains(v))
|
|
2157
2601
|
.cloned()
|
|
2158
2602
|
.collect();
|
|
2159
2603
|
|
|
@@ -2226,8 +2670,18 @@ impl Codegen {
|
|
|
2226
2670
|
"Math",
|
|
2227
2671
|
"JSON",
|
|
2228
2672
|
"Date",
|
|
2673
|
+
"Set",
|
|
2674
|
+
"Map",
|
|
2229
2675
|
"Object",
|
|
2676
|
+
"Float64Array",
|
|
2677
|
+
"Float32Array",
|
|
2678
|
+
"Int8Array",
|
|
2230
2679
|
"Uint8Array",
|
|
2680
|
+
"Uint8ClampedArray",
|
|
2681
|
+
"Int16Array",
|
|
2682
|
+
"Uint16Array",
|
|
2683
|
+
"Int32Array",
|
|
2684
|
+
"Uint32Array",
|
|
2231
2685
|
"AudioContext",
|
|
2232
2686
|
"process",
|
|
2233
2687
|
"setTimeout",
|
|
@@ -2300,14 +2754,82 @@ impl Codegen {
|
|
|
2300
2754
|
.map(|n| n.to_string())
|
|
2301
2755
|
.collect();
|
|
2302
2756
|
let formal_span = *span;
|
|
2757
|
+
// M1 (keystone, dark-shipped behind TISH_PARAM_NATIVE): a typed scalar param
|
|
2758
|
+
// normally arrives boxed (`args.get(i).cloned()`), which poisons native math in
|
|
2759
|
+
// the body (e.g. `i*N+k` boxes). Bind a *native shadow* — coerce once to f64/
|
|
2760
|
+
// bool/String — so the body lowers it like a native local. Conservative: only
|
|
2761
|
+
// simple params, native-scalar annotation, no default value.
|
|
2762
|
+
let param_native =
|
|
2763
|
+
std::env::var("TISH_PARAM_NATIVE").map(|v| v != "0").unwrap_or(false);
|
|
2764
|
+
// A param referenced by ANY sibling default expr (e.g. `(a, b = a + 1)`) must NOT
|
|
2765
|
+
// get a native f64 shadow: the default binding is emitted on the boxed Value path
|
|
2766
|
+
// (`ops::add(&a, …)` expects `&Value`), so a native `a: f64` would mistype the
|
|
2767
|
+
// generated Rust. Keep such params boxed — correctness over the M1 optimization;
|
|
2768
|
+
// defaults referencing params are rare in hot code. Also covers the M4 case where
|
|
2769
|
+
// an unannotated param (e.g. `dependent(a, b = a + 1)`) is inferred numeric.
|
|
2770
|
+
let mut default_referenced: std::collections::HashSet<String> =
|
|
2771
|
+
std::collections::HashSet::new();
|
|
2772
|
+
for p in params {
|
|
2773
|
+
if let FunParam::Simple(tp) = p {
|
|
2774
|
+
if let Some(d) = &tp.default {
|
|
2775
|
+
Self::collect_expr_idents(d, &mut default_referenced);
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
let mut native_params: Vec<(String, RustType)> = Vec::new();
|
|
2303
2780
|
for (i, p) in params.iter().enumerate() {
|
|
2304
2781
|
match p {
|
|
2305
2782
|
FunParam::Simple(tp) => {
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2783
|
+
let native_ty = if param_native
|
|
2784
|
+
&& tp.default.is_none()
|
|
2785
|
+
&& !default_referenced.contains(tp.name.as_ref())
|
|
2786
|
+
{
|
|
2787
|
+
tp.type_ann
|
|
2788
|
+
.as_ref()
|
|
2789
|
+
.map(RustType::from_annotation)
|
|
2790
|
+
.filter(|t| {
|
|
2791
|
+
matches!(
|
|
2792
|
+
t,
|
|
2793
|
+
RustType::F64 | RustType::Bool | RustType::String
|
|
2794
|
+
)
|
|
2795
|
+
})
|
|
2796
|
+
} else {
|
|
2797
|
+
None
|
|
2798
|
+
};
|
|
2799
|
+
if let Some(nt) = native_ty {
|
|
2800
|
+
let coercion = nt.from_value_expr(&format!(
|
|
2801
|
+
"args.get({}).cloned().unwrap_or(Value::Null)",
|
|
2802
|
+
i
|
|
2803
|
+
));
|
|
2804
|
+
self.writeln(&format!(
|
|
2805
|
+
"{} {} = {};",
|
|
2806
|
+
Self::mut_kw_for(tp.name.as_ref(), "let mut"),
|
|
2807
|
+
Self::escape_ident(tp.name.as_ref()),
|
|
2808
|
+
coercion
|
|
2809
|
+
));
|
|
2810
|
+
native_params.push((tp.name.to_string(), nt));
|
|
2811
|
+
} else if let Some(default_expr) = &tp.default {
|
|
2812
|
+
// Default applies only when the positional arg is MISSING
|
|
2813
|
+
// (`args.get(i) == None`), matching the interpreter + bytecode VM.
|
|
2814
|
+
// An explicit `null` argument is "supplied" and keeps the null.
|
|
2815
|
+
// Earlier params are already bound above, so a default may
|
|
2816
|
+
// reference them, e.g. `(a, b = a + 1)`.
|
|
2817
|
+
let default_str = self.emit_expr(default_expr)?;
|
|
2818
|
+
self.writeln(&format!(
|
|
2819
|
+
"{} {} = match args.get({}) {{ Some(v) => v.clone(), None => {} }};",
|
|
2820
|
+
Self::mut_kw_for(tp.name.as_ref(), "let mut"),
|
|
2821
|
+
Self::escape_ident(tp.name.as_ref()),
|
|
2822
|
+
i,
|
|
2823
|
+
default_str
|
|
2824
|
+
));
|
|
2825
|
+
} else {
|
|
2826
|
+
self.writeln(&format!(
|
|
2827
|
+
"{} {} = args.get({}).cloned().unwrap_or(Value::Null);",
|
|
2828
|
+
Self::mut_kw_for(tp.name.as_ref(), "let mut"),
|
|
2829
|
+
Self::escape_ident(tp.name.as_ref()),
|
|
2830
|
+
i
|
|
2831
|
+
));
|
|
2832
|
+
}
|
|
2311
2833
|
}
|
|
2312
2834
|
FunParam::Destructure { pattern, .. } => {
|
|
2313
2835
|
let tmp = format!("_formal_{}", i);
|
|
@@ -2319,16 +2841,47 @@ impl Codegen {
|
|
|
2319
2841
|
}
|
|
2320
2842
|
}
|
|
2321
2843
|
}
|
|
2844
|
+
// A typed rest-param `...args: number[]` lowers to a native `Vec<elem>` (unbox each
|
|
2845
|
+
// trailing arg) instead of a boxed `Value::Array`, so the body iterates/indexes it
|
|
2846
|
+
// natively (and `for (let x of args)` keeps accumulators `f64`). Non-native element
|
|
2847
|
+
// types fall back to the boxed array.
|
|
2848
|
+
let rest_native: Option<RustType> = rest_param.as_ref().and_then(|rp| {
|
|
2849
|
+
rp.type_ann.as_ref().and_then(|ann| {
|
|
2850
|
+
match RustType::from_annotation_with_aliases(ann, &self.type_aliases) {
|
|
2851
|
+
RustType::Vec(elem) if elem.is_native() => Some(RustType::Vec(elem)),
|
|
2852
|
+
_ => None,
|
|
2853
|
+
}
|
|
2854
|
+
})
|
|
2855
|
+
});
|
|
2322
2856
|
if let Some(rest) = rest_param {
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2857
|
+
if let Some(RustType::Vec(elem)) = &rest_native {
|
|
2858
|
+
self.writeln(&format!(
|
|
2859
|
+
"let {}: Vec<{}> = args[{}..].iter().map(|v| {}).collect();",
|
|
2860
|
+
Self::escape_ident(rest.name.as_ref()),
|
|
2861
|
+
elem.to_rust_type_str(),
|
|
2862
|
+
params.len(),
|
|
2863
|
+
elem.from_value_expr("v")
|
|
2864
|
+
));
|
|
2865
|
+
} else {
|
|
2866
|
+
self.writeln(&format!(
|
|
2867
|
+
"let {} = Value::Array(VmRef::new(args[{}..].to_vec()));",
|
|
2868
|
+
Self::escape_ident(rest.name.as_ref()),
|
|
2869
|
+
params.len()
|
|
2870
|
+
));
|
|
2871
|
+
}
|
|
2328
2872
|
}
|
|
2329
2873
|
|
|
2330
2874
|
self.type_context
|
|
2331
2875
|
.push_fun_param_scope(params, rest_param.as_ref());
|
|
2876
|
+
// Register native-shadowed params (bound above) with their native type so the
|
|
2877
|
+
// body lowers them exactly like native locals (binops, indices, etc.).
|
|
2878
|
+
for (pname, pty) in &native_params {
|
|
2879
|
+
self.type_context.define(pname, pty.clone());
|
|
2880
|
+
}
|
|
2881
|
+
// A native `Vec` rest-param: register so the body iterates/indexes it natively.
|
|
2882
|
+
if let (Some(rest), Some(rt)) = (rest_param.as_ref(), rest_native.as_ref()) {
|
|
2883
|
+
self.type_context.define(rest.name.as_ref(), rt.clone());
|
|
2884
|
+
}
|
|
2332
2885
|
|
|
2333
2886
|
let fun_body_res: Result<(), CompileError> = (|| -> Result<(), CompileError> {
|
|
2334
2887
|
// Push current params to stack for nested functions
|
|
@@ -2362,9 +2915,22 @@ impl Codegen {
|
|
|
2362
2915
|
escaped
|
|
2363
2916
|
));
|
|
2364
2917
|
}
|
|
2918
|
+
// Vars declared in this body that a nested closure captures
|
|
2919
|
+
// and that are assigned somewhere in the body must be shared
|
|
2920
|
+
// `VmRef` cells (e.g. `let t=0; let f=()=>t; t=100`). Block
|
|
2921
|
+
// scopes get this via emit_statement(Block); a function body
|
|
2922
|
+
// is iterated directly, so run the same prepass here.
|
|
2923
|
+
let body_cell_vars =
|
|
2924
|
+
Self::collect_vars_needing_capture_cell(statements);
|
|
2925
|
+
for v in &body_cell_vars {
|
|
2926
|
+
self.refcell_wrapped_vars.insert(v.clone());
|
|
2927
|
+
}
|
|
2365
2928
|
for s in statements {
|
|
2366
2929
|
self.emit_statement(s)?;
|
|
2367
2930
|
}
|
|
2931
|
+
for v in &body_cell_vars {
|
|
2932
|
+
self.refcell_wrapped_vars.remove(v);
|
|
2933
|
+
}
|
|
2368
2934
|
self.function_scope_stack.pop();
|
|
2369
2935
|
self.outer_vars_stack.pop();
|
|
2370
2936
|
self.rc_cell_storage_scopes.pop();
|
|
@@ -2435,7 +3001,7 @@ impl Codegen {
|
|
|
2435
3001
|
}
|
|
2436
3002
|
CallArg::Spread(e) => {
|
|
2437
3003
|
let val = self.emit_expr(e)?;
|
|
2438
|
-
parts.push(format!("if let Value::Array(ref _spread) = {} {{ _args.extend(_spread.borrow().iter().cloned()); }}", val));
|
|
3004
|
+
parts.push(format!("if let Value::Array(ref _spread) = tishlang_runtime::normalize_for_of(({}).clone()) {{ _args.extend(_spread.borrow().iter().cloned()); }}", val));
|
|
2439
3005
|
}
|
|
2440
3006
|
}
|
|
2441
3007
|
}
|
|
@@ -2480,7 +3046,7 @@ impl Codegen {
|
|
|
2480
3046
|
DestructElement::Ident(name, _) => {
|
|
2481
3047
|
self.writeln(&format!(
|
|
2482
3048
|
"{} {} = match &({}) {{ Value::Array(ref _a) => _a.borrow().get({}).cloned().unwrap_or(Value::Null), _ => Value::Null }};",
|
|
2483
|
-
mutability,
|
|
3049
|
+
Self::mut_kw_for(name.as_ref(), mutability),
|
|
2484
3050
|
Self::escape_ident(name.as_ref()),
|
|
2485
3051
|
value_expr,
|
|
2486
3052
|
i
|
|
@@ -2497,7 +3063,7 @@ impl Codegen {
|
|
|
2497
3063
|
DestructElement::Rest(name, _) => {
|
|
2498
3064
|
self.writeln(&format!(
|
|
2499
3065
|
"{} {} = match &({}) {{ Value::Array(ref _a) => {{ let _b = _a.borrow(); Value::Array(VmRef::new(_b.iter().skip({}).cloned().collect())) }}, _ => Value::Array(VmRef::new(Vec::new())) }};",
|
|
2500
|
-
mutability,
|
|
3066
|
+
Self::mut_kw_for(name.as_ref(), mutability),
|
|
2501
3067
|
Self::escape_ident(name.as_ref()),
|
|
2502
3068
|
value_expr,
|
|
2503
3069
|
i
|
|
@@ -2514,7 +3080,7 @@ impl Codegen {
|
|
|
2514
3080
|
DestructElement::Ident(name, _) => {
|
|
2515
3081
|
self.writeln(&format!(
|
|
2516
3082
|
"{} {} = match &({}) {{ Value::Object(ref _o) => _o.borrow().strings.get({:?}).cloned().unwrap_or(Value::Null), _ => Value::Null }};",
|
|
2517
|
-
mutability,
|
|
3083
|
+
Self::mut_kw_for(name.as_ref(), mutability),
|
|
2518
3084
|
Self::escape_ident(name.as_ref()),
|
|
2519
3085
|
value_expr,
|
|
2520
3086
|
key
|
|
@@ -2585,7 +3151,7 @@ impl Codegen {
|
|
|
2585
3151
|
fn emit_expr(&mut self, expr: &Expr) -> Result<String, CompileError> {
|
|
2586
3152
|
Ok(match expr {
|
|
2587
3153
|
Expr::Literal { value, .. } => match value {
|
|
2588
|
-
Literal::Number(n) => format!("Value::Number({}
|
|
3154
|
+
Literal::Number(n) => format!("Value::Number({})", Self::f64_lit(*n)),
|
|
2589
3155
|
Literal::String(s) => format!("Value::String({:?}.into())", s.as_ref()),
|
|
2590
3156
|
Literal::Bool(b) => format!("Value::Bool({})", b),
|
|
2591
3157
|
Literal::Null => "Value::Null".to_string(),
|
|
@@ -2633,7 +3199,7 @@ impl Codegen {
|
|
|
2633
3199
|
o
|
|
2634
3200
|
),
|
|
2635
3201
|
UnaryOp::BitNot => format!(
|
|
2636
|
-
"Value::Number({{ let Value::Number(n) = &({}) else {{ panic!(\"Expected number\") }}; (!(*n
|
|
3202
|
+
"Value::Number({{ let Value::Number(n) = &({}) else {{ panic!(\"Expected number\") }}; (!tishlang_runtime::to_int32(*n)) as f64 }})",
|
|
2637
3203
|
o
|
|
2638
3204
|
),
|
|
2639
3205
|
UnaryOp::Void => format!("{{ {}; Value::Null }}", o),
|
|
@@ -2656,8 +3222,7 @@ impl Codegen {
|
|
|
2656
3222
|
{
|
|
2657
3223
|
if method_name.as_ref() == "stringify"
|
|
2658
3224
|
&& matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "JSON")
|
|
2659
|
-
|
|
2660
|
-
if args.len() == 1 {
|
|
3225
|
+
&& args.len() == 1 {
|
|
2661
3226
|
if let CallArg::Expr(arg) = &args[0] {
|
|
2662
3227
|
let (arg_code, arg_ty) = self.emit_typed_expr(arg)?;
|
|
2663
3228
|
match &arg_ty {
|
|
@@ -2682,52 +3247,6 @@ impl Codegen {
|
|
|
2682
3247
|
}
|
|
2683
3248
|
}
|
|
2684
3249
|
}
|
|
2685
|
-
}
|
|
2686
|
-
}
|
|
2687
|
-
|
|
2688
|
-
// Compile-time embed: Polars.read_csv("<literal path>") when file exists
|
|
2689
|
-
if let Some(init) = self.native_module_init.get("tish:polars") {
|
|
2690
|
-
let crate_name = match init {
|
|
2691
|
-
crate::resolve::NativeModuleInit::Legacy { crate_name, .. } => {
|
|
2692
|
-
crate_name.as_str()
|
|
2693
|
-
}
|
|
2694
|
-
crate::resolve::NativeModuleInit::Generated { shim_crate, .. } => {
|
|
2695
|
-
shim_crate.as_str()
|
|
2696
|
-
}
|
|
2697
|
-
};
|
|
2698
|
-
if let (Some(root), Some(CallArg::Expr(first_arg))) =
|
|
2699
|
-
(self.project_root.as_ref(), args.first())
|
|
2700
|
-
{
|
|
2701
|
-
if let Expr::Member {
|
|
2702
|
-
object,
|
|
2703
|
-
prop: MemberProp::Name { name: ref method_name, .. },
|
|
2704
|
-
..
|
|
2705
|
-
} = callee.as_ref()
|
|
2706
|
-
{
|
|
2707
|
-
if method_name.as_ref() == "read_csv"
|
|
2708
|
-
&& matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Polars")
|
|
2709
|
-
{
|
|
2710
|
-
if let Expr::Literal {
|
|
2711
|
-
value: Literal::String(ref path),
|
|
2712
|
-
..
|
|
2713
|
-
} = first_arg
|
|
2714
|
-
{
|
|
2715
|
-
let path_str = path.as_ref();
|
|
2716
|
-
let normalized = path_str.trim_start_matches("./");
|
|
2717
|
-
let full_path = root.join(normalized);
|
|
2718
|
-
if full_path.exists() {
|
|
2719
|
-
if let Ok(content) = std::fs::read_to_string(&full_path) {
|
|
2720
|
-
let escaped = format!("{:?}", content);
|
|
2721
|
-
return Ok(format!(
|
|
2722
|
-
"{}::polars_read_csv_from_string_runtime({})",
|
|
2723
|
-
crate_name, escaped
|
|
2724
|
-
));
|
|
2725
|
-
}
|
|
2726
|
-
}
|
|
2727
|
-
}
|
|
2728
|
-
}
|
|
2729
|
-
}
|
|
2730
|
-
}
|
|
2731
3250
|
}
|
|
2732
3251
|
|
|
2733
3252
|
// Check for built-in method calls on arrays/strings
|
|
@@ -2839,6 +3358,15 @@ impl Codegen {
|
|
|
2839
3358
|
"reverse" => {
|
|
2840
3359
|
return Ok(format!("tishlang_runtime::array_reverse(&{})", obj_expr));
|
|
2841
3360
|
}
|
|
3361
|
+
"fill" => {
|
|
3362
|
+
let value = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
|
|
3363
|
+
let start = arg_exprs.get(1).cloned().unwrap_or_else(|| "Value::Null".to_string());
|
|
3364
|
+
let end = arg_exprs.get(2).cloned().unwrap_or_else(|| "Value::Null".to_string());
|
|
3365
|
+
return Ok(format!(
|
|
3366
|
+
"tishlang_runtime::array_fill(&{}, &{}, &{}, &{})",
|
|
3367
|
+
obj_expr, value, start, end
|
|
3368
|
+
));
|
|
3369
|
+
}
|
|
2842
3370
|
"shuffle" => {
|
|
2843
3371
|
return Ok(format!("tishlang_runtime::array_shuffle(&{})", obj_expr));
|
|
2844
3372
|
}
|
|
@@ -2923,13 +3451,24 @@ impl Codegen {
|
|
|
2923
3451
|
obj_expr, search, replacement
|
|
2924
3452
|
));
|
|
2925
3453
|
}
|
|
2926
|
-
|
|
3454
|
+
// Gate on the *requested* feature (has_feature), not tish_compile's own
|
|
3455
|
+
// cfg!(feature="regex") — the generated binary links the runtime's regex
|
|
3456
|
+
// impls when the build requests regex, regardless of how tish_compile was
|
|
3457
|
+
// compiled. Falls through to a generic call (no-regex builds) otherwise.
|
|
3458
|
+
"match" if self.has_feature("regex") => {
|
|
2927
3459
|
let regexp = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
|
|
2928
3460
|
return Ok(format!(
|
|
2929
3461
|
"tishlang_runtime::string_match_regex(&{}, &{})",
|
|
2930
3462
|
obj_expr, regexp
|
|
2931
3463
|
));
|
|
2932
3464
|
}
|
|
3465
|
+
"search" if self.has_feature("regex") => {
|
|
3466
|
+
let regexp = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
|
|
3467
|
+
return Ok(format!(
|
|
3468
|
+
"tishlang_runtime::string_search_regex(&{}, &{})",
|
|
3469
|
+
obj_expr, regexp
|
|
3470
|
+
));
|
|
3471
|
+
}
|
|
2933
3472
|
"charAt" => {
|
|
2934
3473
|
let idx = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Number(0.0)".to_string());
|
|
2935
3474
|
return Ok(format!(
|
|
@@ -2975,6 +3514,13 @@ impl Codegen {
|
|
|
2975
3514
|
obj_expr, digits
|
|
2976
3515
|
));
|
|
2977
3516
|
}
|
|
3517
|
+
"toString" => {
|
|
3518
|
+
let radix = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
|
|
3519
|
+
return Ok(format!(
|
|
3520
|
+
"tishlang_runtime::number_to_string(&{}, &{})",
|
|
3521
|
+
obj_expr, radix
|
|
3522
|
+
));
|
|
3523
|
+
}
|
|
2978
3524
|
// Higher-order array methods
|
|
2979
3525
|
"map" => {
|
|
2980
3526
|
let callback = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
|
|
@@ -2991,8 +3537,20 @@ impl Codegen {
|
|
|
2991
3537
|
));
|
|
2992
3538
|
}
|
|
2993
3539
|
"reduce" => {
|
|
2994
|
-
let callback = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
|
|
2995
3540
|
let initial = arg_exprs.get(1).cloned().unwrap_or_else(|| "Value::Null".to_string());
|
|
3541
|
+
// Fused reduce (TISH_FUSED_HOF): `arr.reduce((acc, x) => acc OP x, init)`
|
|
3542
|
+
// with a plain binop of the two params → a native fold using the SAME
|
|
3543
|
+
// runtime Value op the closure body would, eliminating the per-element
|
|
3544
|
+
// `value_call`. Sound (identical Value semantics, incl. string `+`).
|
|
3545
|
+
// Requires an explicit init; anything else falls back to array_reduce.
|
|
3546
|
+
if std::env::var("TISH_FUSED_HOF").is_ok() && args.len() == 2 {
|
|
3547
|
+
if let Some(fold) =
|
|
3548
|
+
self.try_fused_reduce(args, &obj_expr, &initial)?
|
|
3549
|
+
{
|
|
3550
|
+
return Ok(fold);
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
let callback = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
|
|
2996
3554
|
return Ok(format!(
|
|
2997
3555
|
"tishlang_runtime::array_reduce(&{}, &{}, &{})",
|
|
2998
3556
|
obj_expr, callback, initial
|
|
@@ -3161,6 +3719,27 @@ impl Codegen {
|
|
|
3161
3719
|
}
|
|
3162
3720
|
}
|
|
3163
3721
|
}
|
|
3722
|
+
// Generalize the typed struct-field fast path to `xs[i].field` (array-of-structs):
|
|
3723
|
+
// when `object` indexes a `Vec<Named>`, do native struct field access.
|
|
3724
|
+
if !optional {
|
|
3725
|
+
if let (Expr::Index { .. }, MemberProp::Name { name: prop_name, .. }) =
|
|
3726
|
+
(object.as_ref(), prop)
|
|
3727
|
+
{
|
|
3728
|
+
let (obj_code, obj_ty) = self.emit_typed_expr(object)?;
|
|
3729
|
+
if let RustType::Named { fields, .. } = &obj_ty {
|
|
3730
|
+
if let Some((_, field_ty)) =
|
|
3731
|
+
fields.iter().find(|(k, _)| k.as_ref() == prop_name.as_ref())
|
|
3732
|
+
{
|
|
3733
|
+
let access = format!(
|
|
3734
|
+
"({}).{}",
|
|
3735
|
+
obj_code,
|
|
3736
|
+
crate::types::field_ident(prop_name.as_ref())
|
|
3737
|
+
);
|
|
3738
|
+
return Ok(field_ty.to_value_expr(&access));
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3164
3743
|
let obj = self.emit_expr(object)?;
|
|
3165
3744
|
let key = match prop {
|
|
3166
3745
|
MemberProp::Name { name, .. } => format!("{:?}", name.as_ref()),
|
|
@@ -3233,7 +3812,7 @@ impl Codegen {
|
|
|
3233
3812
|
}
|
|
3234
3813
|
ArrayElement::Spread(e) => {
|
|
3235
3814
|
let val = self.emit_expr(e)?;
|
|
3236
|
-
parts.push(format!("if let Value::Array(ref _spread) = {} {{ _arr.extend(_spread.borrow().iter().cloned()); }}", val));
|
|
3815
|
+
parts.push(format!("if let Value::Array(ref _spread) = tishlang_runtime::normalize_for_of(({}).clone()) {{ _arr.extend(_spread.borrow().iter().cloned()); }}", val));
|
|
3237
3816
|
}
|
|
3238
3817
|
}
|
|
3239
3818
|
}
|
|
@@ -3294,10 +3873,9 @@ impl Codegen {
|
|
|
3294
3873
|
}
|
|
3295
3874
|
}
|
|
3296
3875
|
}
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
)
|
|
3876
|
+
// Build the PropMap directly (no intermediate AHashMap) — one
|
|
3877
|
+
// inline allocation for small objects (the common case).
|
|
3878
|
+
format!("Value::object_from_pairs([{}])", parts.join(", "))
|
|
3301
3879
|
}
|
|
3302
3880
|
}
|
|
3303
3881
|
Expr::Assign { name, value, .. } => {
|
|
@@ -3359,26 +3937,36 @@ impl Codegen {
|
|
|
3359
3937
|
#[cfg(feature = "http")]
|
|
3360
3938
|
if self.is_async {
|
|
3361
3939
|
let _in_async = self.async_context_stack.last().copied().unwrap_or(false);
|
|
3940
|
+
// A rejected awaited promise must THROW (so a surrounding try/catch fires).
|
|
3941
|
+
// Use the throwing `?`-variant wherever an error channel exists — the SAME
|
|
3942
|
+
// condition `throw` uses to emit `return Err(..)` (inside a try body, or the
|
|
3943
|
+
// top-level run()). Elsewhere there is no channel, so fall back to the
|
|
3944
|
+
// value-returning variant (matches the existing uncaught-throw limitation).
|
|
3945
|
+
let (awaiter, q) = if self.try_closure_depth > 0 || self.value_fn_depth == 0 {
|
|
3946
|
+
("tish_await_promise_throw", "?")
|
|
3947
|
+
} else {
|
|
3948
|
+
("tish_await_promise", "")
|
|
3949
|
+
};
|
|
3362
3950
|
if let Expr::Call { callee, args, .. } = operand.as_ref() {
|
|
3363
3951
|
if let Expr::Ident { name, .. } = callee.as_ref() {
|
|
3364
3952
|
let args_code = self.emit_call_args(args)?;
|
|
3365
3953
|
return Ok(match name.as_ref() {
|
|
3366
3954
|
"fetch" => {
|
|
3367
|
-
format!("
|
|
3955
|
+
format!("{}(tish_fetch_promise({})){}", awaiter, args_code, q)
|
|
3368
3956
|
}
|
|
3369
3957
|
"fetchAll" => {
|
|
3370
|
-
format!("
|
|
3958
|
+
format!("{}(tish_fetch_all_promise({})){}", awaiter, args_code, q)
|
|
3371
3959
|
}
|
|
3372
3960
|
_ => {
|
|
3373
3961
|
let o = self.emit_expr(operand)?;
|
|
3374
|
-
return Ok(format!("
|
|
3962
|
+
return Ok(format!("{}({}){}", awaiter, o, q));
|
|
3375
3963
|
}
|
|
3376
3964
|
});
|
|
3377
3965
|
}
|
|
3378
3966
|
}
|
|
3379
3967
|
// await Call with non-Ident callee, or await Promise value: wrap in await_promise
|
|
3380
3968
|
let o = self.emit_expr(operand)?;
|
|
3381
|
-
return Ok(format!("
|
|
3969
|
+
return Ok(format!("{}({}){}", awaiter, o, q));
|
|
3382
3970
|
}
|
|
3383
3971
|
// Fallback: emit operand as sync call (no real .await in our model)
|
|
3384
3972
|
let o = self.emit_expr(operand)?;
|
|
@@ -3396,6 +3984,27 @@ impl Codegen {
|
|
|
3396
3984
|
o
|
|
3397
3985
|
)
|
|
3398
3986
|
}
|
|
3987
|
+
Expr::Delete { target, .. } => match target.as_ref() {
|
|
3988
|
+
Expr::Member { object, prop: MemberProp::Name { name, .. }, .. } => {
|
|
3989
|
+
let obj = self.emit_expr(object)?;
|
|
3990
|
+
format!(
|
|
3991
|
+
"tishlang_runtime::delete_property(&{}, &Value::String({:?}.into()))",
|
|
3992
|
+
obj,
|
|
3993
|
+
name.as_ref()
|
|
3994
|
+
)
|
|
3995
|
+
}
|
|
3996
|
+
Expr::Member { object, prop: MemberProp::Expr(key), .. } => {
|
|
3997
|
+
let obj = self.emit_expr(object)?;
|
|
3998
|
+
let k = self.emit_expr(key)?;
|
|
3999
|
+
format!("tishlang_runtime::delete_property(&{}, &{})", obj, k)
|
|
4000
|
+
}
|
|
4001
|
+
Expr::Index { object, index, .. } => {
|
|
4002
|
+
let obj = self.emit_expr(object)?;
|
|
4003
|
+
let idx = self.emit_expr(index)?;
|
|
4004
|
+
format!("tishlang_runtime::delete_property(&{}, &{})", obj, idx)
|
|
4005
|
+
}
|
|
4006
|
+
_ => "Value::Bool(true)".to_string(),
|
|
4007
|
+
},
|
|
3399
4008
|
Expr::PostfixInc { name, .. } => self.emit_inc_dec(name.as_ref(), false, "+ 1.0", "++"),
|
|
3400
4009
|
Expr::PostfixDec { name, .. } => self.emit_inc_dec(name.as_ref(), false, "- 1.0", "--"),
|
|
3401
4010
|
Expr::PrefixInc { name, .. } => self.emit_inc_dec(name.as_ref(), true, "+ 1.0", "++"),
|
|
@@ -3473,7 +4082,7 @@ impl Codegen {
|
|
|
3473
4082
|
Value::Number(n) => {{ let i = *n as i64; if (*n - i as f64).abs() < f64::EPSILON {{ i.to_string() }} else {{ n.to_string() }} }}, \
|
|
3474
4083
|
Value::Bool(b) => b.to_string(), \
|
|
3475
4084
|
Value::Null => \"null\".to_string(), \
|
|
3476
|
-
other =>
|
|
4085
|
+
other => other.to_js_string() }}",
|
|
3477
4086
|
rhs_val
|
|
3478
4087
|
)
|
|
3479
4088
|
};
|
|
@@ -3500,7 +4109,7 @@ impl Codegen {
|
|
|
3500
4109
|
Value::Number(n) => {{ let i = *n as i64; if (*n - i as f64).abs() < f64::EPSILON {{ i.to_string() }} else {{ n.to_string() }} }}, \
|
|
3501
4110
|
Value::Bool(b) => b.to_string(), \
|
|
3502
4111
|
Value::Null => \"null\".to_string(), \
|
|
3503
|
-
other =>
|
|
4112
|
+
other => other.to_js_string() }}",
|
|
3504
4113
|
rhs_val
|
|
3505
4114
|
)
|
|
3506
4115
|
};
|
|
@@ -3682,10 +4291,25 @@ impl Codegen {
|
|
|
3682
4291
|
// both native but different type — best effort
|
|
3683
4292
|
val_code
|
|
3684
4293
|
};
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
)
|
|
4294
|
+
// OOB-safe write for numeric/bool Vecs: JS `a[i] = x` past the end
|
|
4295
|
+
// grows the array (holes read back as `undefined` → NaN/false), it does
|
|
4296
|
+
// not panic. In-bounds is the same direct store. Other element types keep
|
|
4297
|
+
// the direct store (their OOB semantics aren't a native-inference target).
|
|
4298
|
+
let assign = match elem_type.as_ref() {
|
|
4299
|
+
RustType::F64 | RustType::Bool => {
|
|
4300
|
+
let pad = if matches!(elem_type.as_ref(), RustType::F64) {
|
|
4301
|
+
"f64::NAN"
|
|
4302
|
+
} else {
|
|
4303
|
+
"false"
|
|
4304
|
+
};
|
|
4305
|
+
format!(
|
|
4306
|
+
"{{ let _idx = {}; if _idx >= {}.len() {{ {}.resize(_idx + 1, {}); }} {}[_idx] = {}; Value::Null }}",
|
|
4307
|
+
idx_usize, esc_obj, esc_obj, pad, esc_obj, native_val
|
|
4308
|
+
)
|
|
4309
|
+
}
|
|
4310
|
+
_ => format!("{{ {}[{}] = {}; Value::Null }}", esc_obj, idx_usize, native_val),
|
|
4311
|
+
};
|
|
4312
|
+
return Ok(assign);
|
|
3689
4313
|
}
|
|
3690
4314
|
}
|
|
3691
4315
|
}
|
|
@@ -3712,7 +4336,7 @@ impl Codegen {
|
|
|
3712
4336
|
parts.push(format!("\"{}\"", escaped));
|
|
3713
4337
|
if i < exprs.len() {
|
|
3714
4338
|
let expr_code = self.emit_expr(&exprs[i])?;
|
|
3715
|
-
parts.push(format!("&({}).
|
|
4339
|
+
parts.push(format!("&({}).to_js_string()", expr_code));
|
|
3716
4340
|
}
|
|
3717
4341
|
}
|
|
3718
4342
|
format!("Value::String([{}].concat().into())", parts.join(", "))
|
|
@@ -3727,6 +4351,37 @@ impl Codegen {
|
|
|
3727
4351
|
.map_err(|m| CompileError::new(m, None))?
|
|
3728
4352
|
}
|
|
3729
4353
|
Expr::New { callee, args, .. } => {
|
|
4354
|
+
// Packed-native fast path: `new Float64Array(...)` lowers to a packed
|
|
4355
|
+
// `Value::NumberArray` (`Vec<f64>`) instead of the boxed `Value::Array` the generic
|
|
4356
|
+
// `tish_construct` builds — `Float64Array` is the one view whose element type *is*
|
|
4357
|
+
// f64, so it needs no coercion and avoids the per-element `Value` boxing. The helper
|
|
4358
|
+
// falls back to the identical boxed value when `TISH_PACKED_ARRAYS` is off, so default
|
|
4359
|
+
// builds stay byte-for-byte unchanged. The other typed-array views have no packed
|
|
4360
|
+
// `Value` variant (would need `Vec<f32>`/`Vec<i32>`/… + the 24-byte size assertion and
|
|
4361
|
+
// every exhaustive match), so they keep the generic path. Native-only: interp/VM value
|
|
4362
|
+
// bridges carry no `NumberArray`, so only the native runtime grew the support. Keyed on
|
|
4363
|
+
// the callee ident like the existing `JSON.`/`Polars.` special-cases.
|
|
4364
|
+
if matches!(callee.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Float64Array")
|
|
4365
|
+
{
|
|
4366
|
+
if args.iter().any(|a| matches!(a, CallArg::Spread(_))) {
|
|
4367
|
+
let args_code = self.emit_call_args(args)?;
|
|
4368
|
+
return Ok(format!(
|
|
4369
|
+
"{{ let _spread_args = {}; tishlang_runtime::float64_array_packed(&_spread_args[..]) }}",
|
|
4370
|
+
args_code
|
|
4371
|
+
));
|
|
4372
|
+
}
|
|
4373
|
+
let arg_exprs: Result<Vec<_>, _> =
|
|
4374
|
+
args.iter().map(|a| self.emit_call_arg(a)).collect();
|
|
4375
|
+
let args_vec = arg_exprs?
|
|
4376
|
+
.iter()
|
|
4377
|
+
.map(|a| format!("{}.clone()", a))
|
|
4378
|
+
.collect::<Vec<_>>()
|
|
4379
|
+
.join(", ");
|
|
4380
|
+
return Ok(format!(
|
|
4381
|
+
"tishlang_runtime::float64_array_packed(&[{}])",
|
|
4382
|
+
args_vec
|
|
4383
|
+
));
|
|
4384
|
+
}
|
|
3730
4385
|
let callee_expr = self.emit_expr(callee)?;
|
|
3731
4386
|
let has_spread = args.iter().any(|a| matches!(a, CallArg::Spread(_)));
|
|
3732
4387
|
if has_spread {
|
|
@@ -3795,6 +4450,7 @@ impl Codegen {
|
|
|
3795
4450
|
Self::collect_expr_idents(right, idents);
|
|
3796
4451
|
}
|
|
3797
4452
|
Expr::Unary { operand, .. } => Self::collect_expr_idents(operand, idents),
|
|
4453
|
+
Expr::Delete { target, .. } => Self::collect_expr_idents(target, idents),
|
|
3798
4454
|
Expr::Call { callee, args, .. } => {
|
|
3799
4455
|
Self::collect_expr_idents(callee, idents);
|
|
3800
4456
|
for arg in args {
|
|
@@ -3931,8 +4587,19 @@ impl Codegen {
|
|
|
3931
4587
|
fn collect_assigned_idents_in_stmt(stmt: &Statement, names: &mut HashSet<String>) {
|
|
3932
4588
|
match stmt {
|
|
3933
4589
|
Statement::ExprStmt { expr, .. } => Self::collect_assigned_idents_in_expr(expr, names),
|
|
3934
|
-
|
|
3935
|
-
|
|
4590
|
+
// Descend into initializers: an assignment may live inside a closure
|
|
4591
|
+
// stored in a `let`/`const` (e.g. `let inc = () => { count = count + 1 }`).
|
|
4592
|
+
// The declared name itself is a binding, not an assignment, so it is
|
|
4593
|
+
// not added here. Closing this gap also closes it for arrow-block
|
|
4594
|
+
// bodies, which are scanned via collect_assigned_idents_in_expr.
|
|
4595
|
+
Statement::VarDecl { init: Some(e), .. } => {
|
|
4596
|
+
Self::collect_assigned_idents_in_expr(e, names)
|
|
4597
|
+
}
|
|
4598
|
+
Statement::VarDecl { init: None, .. } => {}
|
|
4599
|
+
Statement::VarDeclDestructure { init, .. } => {
|
|
4600
|
+
Self::collect_assigned_idents_in_expr(init, names)
|
|
4601
|
+
}
|
|
4602
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
3936
4603
|
for s in statements {
|
|
3937
4604
|
Self::collect_assigned_idents_in_stmt(s, names);
|
|
3938
4605
|
}
|
|
@@ -4066,6 +4733,7 @@ impl Codegen {
|
|
|
4066
4733
|
Self::collect_assigned_idents_in_expr(right, names);
|
|
4067
4734
|
}
|
|
4068
4735
|
Expr::Unary { operand, .. } => Self::collect_assigned_idents_in_expr(operand, names),
|
|
4736
|
+
Expr::Delete { target, .. } => Self::collect_assigned_idents_in_expr(target, names),
|
|
4069
4737
|
Expr::Call { callee, args, .. } => {
|
|
4070
4738
|
Self::collect_assigned_idents_in_expr(callee, names);
|
|
4071
4739
|
for arg in args {
|
|
@@ -4197,9 +4865,11 @@ impl Codegen {
|
|
|
4197
4865
|
}
|
|
4198
4866
|
}
|
|
4199
4867
|
|
|
4200
|
-
/// Collect
|
|
4201
|
-
/// block_vars: vars declared in the enclosing block
|
|
4202
|
-
|
|
4868
|
+
/// Collect block vars captured (referenced) by this closure and any nested
|
|
4869
|
+
/// closures. block_vars: vars declared in the enclosing block. The caller
|
|
4870
|
+
/// (`collect_vars_needing_capture_cell`) further restricts to vars that are
|
|
4871
|
+
/// also assigned somewhere in the defining scope.
|
|
4872
|
+
fn collect_captured_block_vars_from_closure(
|
|
4203
4873
|
params: &[FunParam],
|
|
4204
4874
|
body: &Statement,
|
|
4205
4875
|
block_vars: &HashSet<String>,
|
|
@@ -4214,8 +4884,6 @@ impl Codegen {
|
|
|
4214
4884
|
Self::collect_local_var_names(body, &mut local_var_names);
|
|
4215
4885
|
let mut referenced = HashSet::new();
|
|
4216
4886
|
Self::collect_stmt_idents(body, &mut referenced);
|
|
4217
|
-
let mut assigned = HashSet::new();
|
|
4218
|
-
Self::collect_assigned_idents_in_stmt(body, &mut assigned);
|
|
4219
4887
|
let outer_captured: HashSet<String> = referenced
|
|
4220
4888
|
.difference(¶m_names)
|
|
4221
4889
|
.cloned()
|
|
@@ -4223,16 +4891,18 @@ impl Codegen {
|
|
|
4223
4891
|
.difference(&local_var_names)
|
|
4224
4892
|
.cloned()
|
|
4225
4893
|
.collect();
|
|
4226
|
-
|
|
4894
|
+
// Every block var this closure captures is a candidate; the caller keeps
|
|
4895
|
+
// only those also assigned somewhere in the defining scope.
|
|
4896
|
+
for v in &outer_captured {
|
|
4227
4897
|
if block_vars.contains(v) {
|
|
4228
4898
|
result.insert(v.clone());
|
|
4229
4899
|
}
|
|
4230
4900
|
}
|
|
4231
4901
|
// Recurse into nested fns
|
|
4232
|
-
Self::
|
|
4902
|
+
Self::collect_captured_block_vars_from_statements(body, block_vars, result);
|
|
4233
4903
|
}
|
|
4234
4904
|
|
|
4235
|
-
fn
|
|
4905
|
+
fn collect_captured_block_vars_from_arrow(
|
|
4236
4906
|
params: &[FunParam],
|
|
4237
4907
|
body: &ArrowBody,
|
|
4238
4908
|
block_vars: &HashSet<String>,
|
|
@@ -4253,11 +4923,6 @@ impl Codegen {
|
|
|
4253
4923
|
ArrowBody::Expr(e) => Self::collect_expr_idents(e, &mut referenced),
|
|
4254
4924
|
ArrowBody::Block(s) => Self::collect_stmt_idents(s, &mut referenced),
|
|
4255
4925
|
}
|
|
4256
|
-
let mut assigned = HashSet::new();
|
|
4257
|
-
match body {
|
|
4258
|
-
ArrowBody::Expr(e) => Self::collect_assigned_idents_in_expr(e, &mut assigned),
|
|
4259
|
-
ArrowBody::Block(s) => Self::collect_assigned_idents_in_stmt(s, &mut assigned),
|
|
4260
|
-
}
|
|
4261
4926
|
let outer_captured: HashSet<String> = referenced
|
|
4262
4927
|
.difference(¶m_names)
|
|
4263
4928
|
.cloned()
|
|
@@ -4265,52 +4930,52 @@ impl Codegen {
|
|
|
4265
4930
|
.difference(&local_var_names)
|
|
4266
4931
|
.cloned()
|
|
4267
4932
|
.collect();
|
|
4268
|
-
for v in outer_captured
|
|
4933
|
+
for v in &outer_captured {
|
|
4269
4934
|
if block_vars.contains(v) {
|
|
4270
4935
|
result.insert(v.clone());
|
|
4271
4936
|
}
|
|
4272
4937
|
}
|
|
4273
4938
|
match body {
|
|
4274
|
-
ArrowBody::Expr(e) => Self::
|
|
4939
|
+
ArrowBody::Expr(e) => Self::collect_captured_block_vars_from_expr(e, block_vars, result),
|
|
4275
4940
|
ArrowBody::Block(s) => {
|
|
4276
|
-
Self::
|
|
4941
|
+
Self::collect_captured_block_vars_from_statements(s, block_vars, result)
|
|
4277
4942
|
}
|
|
4278
4943
|
}
|
|
4279
4944
|
}
|
|
4280
4945
|
|
|
4281
|
-
fn
|
|
4946
|
+
fn collect_captured_block_vars_from_expr(
|
|
4282
4947
|
expr: &Expr,
|
|
4283
4948
|
block_vars: &HashSet<String>,
|
|
4284
4949
|
result: &mut HashSet<String>,
|
|
4285
4950
|
) {
|
|
4286
4951
|
match expr {
|
|
4287
4952
|
Expr::ArrowFunction { params, body, .. } => {
|
|
4288
|
-
Self::
|
|
4953
|
+
Self::collect_captured_block_vars_from_arrow(params, body, block_vars, result);
|
|
4289
4954
|
}
|
|
4290
4955
|
Expr::Call { callee, args, .. } => {
|
|
4291
|
-
Self::
|
|
4956
|
+
Self::collect_captured_block_vars_from_expr(callee, block_vars, result);
|
|
4292
4957
|
for arg in args {
|
|
4293
4958
|
match arg {
|
|
4294
4959
|
CallArg::Expr(e) | CallArg::Spread(e) => {
|
|
4295
|
-
Self::
|
|
4960
|
+
Self::collect_captured_block_vars_from_expr(e, block_vars, result);
|
|
4296
4961
|
}
|
|
4297
4962
|
}
|
|
4298
4963
|
}
|
|
4299
4964
|
}
|
|
4300
4965
|
Expr::New { callee, args, .. } => {
|
|
4301
|
-
Self::
|
|
4966
|
+
Self::collect_captured_block_vars_from_expr(callee, block_vars, result);
|
|
4302
4967
|
for arg in args {
|
|
4303
4968
|
match arg {
|
|
4304
4969
|
CallArg::Expr(e) | CallArg::Spread(e) => {
|
|
4305
|
-
Self::
|
|
4970
|
+
Self::collect_captured_block_vars_from_expr(e, block_vars, result);
|
|
4306
4971
|
}
|
|
4307
4972
|
}
|
|
4308
4973
|
}
|
|
4309
4974
|
}
|
|
4310
4975
|
Expr::Member { object, prop, .. } => {
|
|
4311
|
-
Self::
|
|
4976
|
+
Self::collect_captured_block_vars_from_expr(object, block_vars, result);
|
|
4312
4977
|
if let MemberProp::Expr(e) = prop {
|
|
4313
|
-
Self::
|
|
4978
|
+
Self::collect_captured_block_vars_from_expr(e, block_vars, result);
|
|
4314
4979
|
}
|
|
4315
4980
|
}
|
|
4316
4981
|
Expr::Conditional {
|
|
@@ -4319,19 +4984,19 @@ impl Codegen {
|
|
|
4319
4984
|
else_branch,
|
|
4320
4985
|
..
|
|
4321
4986
|
} => {
|
|
4322
|
-
Self::
|
|
4323
|
-
Self::
|
|
4324
|
-
Self::
|
|
4987
|
+
Self::collect_captured_block_vars_from_expr(cond, block_vars, result);
|
|
4988
|
+
Self::collect_captured_block_vars_from_expr(then_branch, block_vars, result);
|
|
4989
|
+
Self::collect_captured_block_vars_from_expr(else_branch, block_vars, result);
|
|
4325
4990
|
}
|
|
4326
4991
|
Expr::Binary { left, right, .. } | Expr::NullishCoalesce { left, right, .. } => {
|
|
4327
|
-
Self::
|
|
4328
|
-
Self::
|
|
4992
|
+
Self::collect_captured_block_vars_from_expr(left, block_vars, result);
|
|
4993
|
+
Self::collect_captured_block_vars_from_expr(right, block_vars, result);
|
|
4329
4994
|
}
|
|
4330
4995
|
Expr::Array { elements, .. } => {
|
|
4331
4996
|
for el in elements {
|
|
4332
4997
|
match el {
|
|
4333
4998
|
ArrayElement::Expr(e) | ArrayElement::Spread(e) => {
|
|
4334
|
-
Self::
|
|
4999
|
+
Self::collect_captured_block_vars_from_expr(e, block_vars, result);
|
|
4335
5000
|
}
|
|
4336
5001
|
}
|
|
4337
5002
|
}
|
|
@@ -4340,7 +5005,7 @@ impl Codegen {
|
|
|
4340
5005
|
for prop in props {
|
|
4341
5006
|
match prop {
|
|
4342
5007
|
ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => {
|
|
4343
|
-
Self::
|
|
5008
|
+
Self::collect_captured_block_vars_from_expr(e, block_vars, result);
|
|
4344
5009
|
}
|
|
4345
5010
|
}
|
|
4346
5011
|
}
|
|
@@ -4349,21 +5014,21 @@ impl Codegen {
|
|
|
4349
5014
|
}
|
|
4350
5015
|
}
|
|
4351
5016
|
|
|
4352
|
-
fn
|
|
5017
|
+
fn collect_captured_block_vars_from_statements(
|
|
4353
5018
|
stmt: &Statement,
|
|
4354
5019
|
block_vars: &HashSet<String>,
|
|
4355
5020
|
result: &mut HashSet<String>,
|
|
4356
5021
|
) {
|
|
4357
5022
|
match stmt {
|
|
4358
5023
|
Statement::FunDecl { params, body, .. } => {
|
|
4359
|
-
Self::
|
|
5024
|
+
Self::collect_captured_block_vars_from_closure(params, body, block_vars, result);
|
|
4360
5025
|
}
|
|
4361
5026
|
Statement::ExprStmt { expr, .. } => {
|
|
4362
|
-
Self::
|
|
5027
|
+
Self::collect_captured_block_vars_from_expr(expr, block_vars, result);
|
|
4363
5028
|
}
|
|
4364
|
-
Statement::Block { statements, .. } => {
|
|
5029
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
4365
5030
|
for s in statements {
|
|
4366
|
-
Self::
|
|
5031
|
+
Self::collect_captured_block_vars_from_statements(s, block_vars, result);
|
|
4367
5032
|
}
|
|
4368
5033
|
}
|
|
4369
5034
|
Statement::If {
|
|
@@ -4372,10 +5037,10 @@ impl Codegen {
|
|
|
4372
5037
|
else_branch,
|
|
4373
5038
|
..
|
|
4374
5039
|
} => {
|
|
4375
|
-
Self::
|
|
4376
|
-
Self::
|
|
5040
|
+
Self::collect_captured_block_vars_from_expr(cond, block_vars, result);
|
|
5041
|
+
Self::collect_captured_block_vars_from_statements(then_branch, block_vars, result);
|
|
4377
5042
|
if let Some(eb) = else_branch {
|
|
4378
|
-
Self::
|
|
5043
|
+
Self::collect_captured_block_vars_from_statements(eb, block_vars, result);
|
|
4379
5044
|
}
|
|
4380
5045
|
}
|
|
4381
5046
|
Statement::For {
|
|
@@ -4386,23 +5051,23 @@ impl Codegen {
|
|
|
4386
5051
|
..
|
|
4387
5052
|
} => {
|
|
4388
5053
|
if let Some(i) = init {
|
|
4389
|
-
Self::
|
|
5054
|
+
Self::collect_captured_block_vars_from_statements(i, block_vars, result);
|
|
4390
5055
|
}
|
|
4391
5056
|
if let Some(c) = cond {
|
|
4392
|
-
Self::
|
|
5057
|
+
Self::collect_captured_block_vars_from_expr(c, block_vars, result);
|
|
4393
5058
|
}
|
|
4394
5059
|
if let Some(u) = update {
|
|
4395
|
-
Self::
|
|
5060
|
+
Self::collect_captured_block_vars_from_expr(u, block_vars, result);
|
|
4396
5061
|
}
|
|
4397
|
-
Self::
|
|
5062
|
+
Self::collect_captured_block_vars_from_statements(body, block_vars, result);
|
|
4398
5063
|
}
|
|
4399
5064
|
Statement::ForOf { iterable, body, .. } => {
|
|
4400
|
-
Self::
|
|
4401
|
-
Self::
|
|
5065
|
+
Self::collect_captured_block_vars_from_expr(iterable, block_vars, result);
|
|
5066
|
+
Self::collect_captured_block_vars_from_statements(body, block_vars, result);
|
|
4402
5067
|
}
|
|
4403
5068
|
Statement::While { cond, body, .. } | Statement::DoWhile { body, cond, .. } => {
|
|
4404
|
-
Self::
|
|
4405
|
-
Self::
|
|
5069
|
+
Self::collect_captured_block_vars_from_expr(cond, block_vars, result);
|
|
5070
|
+
Self::collect_captured_block_vars_from_statements(body, block_vars, result);
|
|
4406
5071
|
}
|
|
4407
5072
|
Statement::Switch {
|
|
4408
5073
|
expr,
|
|
@@ -4410,18 +5075,18 @@ impl Codegen {
|
|
|
4410
5075
|
default_body,
|
|
4411
5076
|
..
|
|
4412
5077
|
} => {
|
|
4413
|
-
Self::
|
|
5078
|
+
Self::collect_captured_block_vars_from_expr(expr, block_vars, result);
|
|
4414
5079
|
for (ce, stmts) in cases {
|
|
4415
5080
|
if let Some(e) = ce {
|
|
4416
|
-
Self::
|
|
5081
|
+
Self::collect_captured_block_vars_from_expr(e, block_vars, result);
|
|
4417
5082
|
}
|
|
4418
5083
|
for s in stmts {
|
|
4419
|
-
Self::
|
|
5084
|
+
Self::collect_captured_block_vars_from_statements(s, block_vars, result);
|
|
4420
5085
|
}
|
|
4421
5086
|
}
|
|
4422
5087
|
if let Some(stmts) = default_body {
|
|
4423
5088
|
for s in stmts {
|
|
4424
|
-
Self::
|
|
5089
|
+
Self::collect_captured_block_vars_from_statements(s, block_vars, result);
|
|
4425
5090
|
}
|
|
4426
5091
|
}
|
|
4427
5092
|
}
|
|
@@ -4431,39 +5096,77 @@ impl Codegen {
|
|
|
4431
5096
|
finally_body,
|
|
4432
5097
|
..
|
|
4433
5098
|
} => {
|
|
4434
|
-
Self::
|
|
5099
|
+
Self::collect_captured_block_vars_from_statements(body, block_vars, result);
|
|
4435
5100
|
if let Some(c) = catch_body {
|
|
4436
|
-
Self::
|
|
5101
|
+
Self::collect_captured_block_vars_from_statements(c, block_vars, result);
|
|
4437
5102
|
}
|
|
4438
5103
|
if let Some(f) = finally_body {
|
|
4439
|
-
Self::
|
|
5104
|
+
Self::collect_captured_block_vars_from_statements(f, block_vars, result);
|
|
4440
5105
|
}
|
|
4441
5106
|
}
|
|
4442
5107
|
Statement::VarDecl { init: Some(e), .. } => {
|
|
4443
|
-
Self::
|
|
5108
|
+
Self::collect_captured_block_vars_from_expr(e, block_vars, result);
|
|
4444
5109
|
}
|
|
4445
5110
|
Statement::VarDeclDestructure { init, .. } => {
|
|
4446
|
-
Self::
|
|
5111
|
+
Self::collect_captured_block_vars_from_expr(init, block_vars, result);
|
|
4447
5112
|
}
|
|
4448
5113
|
Statement::Return { value: Some(e), .. } => {
|
|
4449
|
-
Self::
|
|
5114
|
+
Self::collect_captured_block_vars_from_expr(e, block_vars, result);
|
|
4450
5115
|
}
|
|
4451
5116
|
Statement::Throw { value, .. } => {
|
|
4452
|
-
Self::
|
|
5117
|
+
Self::collect_captured_block_vars_from_expr(value, block_vars, result)
|
|
4453
5118
|
}
|
|
4454
5119
|
_ => {}
|
|
4455
5120
|
}
|
|
4456
5121
|
}
|
|
4457
5122
|
|
|
4458
|
-
/// For a block, return
|
|
4459
|
-
|
|
5123
|
+
/// For a block, return the names of block-scoped vars that must live in a
|
|
5124
|
+
/// shared `VmRef` cell because a nested closure captures them by reference.
|
|
5125
|
+
///
|
|
5126
|
+
/// A var needs a cell when it is BOTH (a) captured (referenced) by some nested
|
|
5127
|
+
/// closure AND (b) assigned somewhere in the defining scope. The assignment may
|
|
5128
|
+
/// be inside a closure (`counter()`, sibling `inc`/`get`) or in the enclosing
|
|
5129
|
+
/// scope — including AFTER the closure is created (`let t = 0; let f = () => t;
|
|
5130
|
+
/// t = 100`). Capture alone is not enough: a never-mutated var can be snapshot
|
|
5131
|
+
/// by value. The previous rule (captured AND mutated *inside* a closure) was too
|
|
5132
|
+
/// narrow — it snapshotted capture-then-mutate vars by value, so the rust backend
|
|
5133
|
+
/// returned the stale value and diverged from node/vm/interp/cranelift.
|
|
5134
|
+
fn collect_vars_needing_capture_cell(statements: &[Statement]) -> HashSet<String> {
|
|
4460
5135
|
let mut block_vars = HashSet::new();
|
|
4461
5136
|
Self::collect_block_var_names(statements, &mut block_vars);
|
|
4462
|
-
|
|
5137
|
+
// (a) Block vars captured by any nested closure.
|
|
5138
|
+
let mut captured = HashSet::new();
|
|
5139
|
+
for s in statements {
|
|
5140
|
+
Self::collect_captured_block_vars_from_statements(s, &block_vars, &mut captured);
|
|
5141
|
+
}
|
|
5142
|
+
// (b) Idents assigned anywhere in this scope (incl. inside closures).
|
|
5143
|
+
let mut assigned_in_scope = HashSet::new();
|
|
5144
|
+
for s in statements {
|
|
5145
|
+
Self::collect_assigned_idents_in_stmt(s, &mut assigned_in_scope);
|
|
5146
|
+
}
|
|
5147
|
+
captured.retain(|v| assigned_in_scope.contains(v));
|
|
5148
|
+
// A `for (let i = 0; …; i++)` counter is declared ONCE in the header but is a
|
|
5149
|
+
// per-iteration `let` in JS: a closure in the body must snapshot THIS iteration's
|
|
5150
|
+
// value, not share one cell across all iterations. The loop's own `i++` would
|
|
5151
|
+
// otherwise pull it in here. (for-of vars are not block vars, and body-`let`s are
|
|
5152
|
+
// re-declared each iteration so they get a fresh cell regardless — only header
|
|
5153
|
+
// counters, declared once, must be excluded.) See loop_let_capture.tish.
|
|
5154
|
+
let mut for_counters = HashSet::new();
|
|
4463
5155
|
for s in statements {
|
|
4464
|
-
|
|
5156
|
+
if let Statement::For { init: Some(i), .. } = s {
|
|
5157
|
+
match i.as_ref() {
|
|
5158
|
+
Statement::VarDecl { name, .. } => {
|
|
5159
|
+
for_counters.insert(name.to_string());
|
|
5160
|
+
}
|
|
5161
|
+
Statement::VarDeclDestructure { pattern, .. } => {
|
|
5162
|
+
Self::collect_destruct_names(pattern, &mut for_counters);
|
|
5163
|
+
}
|
|
5164
|
+
_ => {}
|
|
5165
|
+
}
|
|
5166
|
+
}
|
|
4465
5167
|
}
|
|
4466
|
-
|
|
5168
|
+
captured.retain(|v| !for_counters.contains(v));
|
|
5169
|
+
captured
|
|
4467
5170
|
}
|
|
4468
5171
|
|
|
4469
5172
|
/// Collect variable names declared in a statement (VarDecl, Destructure, For init).
|
|
@@ -4475,7 +5178,7 @@ impl Codegen {
|
|
|
4475
5178
|
Statement::VarDeclDestructure { pattern, .. } => {
|
|
4476
5179
|
Self::collect_destruct_names(pattern, names);
|
|
4477
5180
|
}
|
|
4478
|
-
Statement::Block { statements, .. } => {
|
|
5181
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
4479
5182
|
for s in statements {
|
|
4480
5183
|
Self::collect_local_var_names(s, names);
|
|
4481
5184
|
}
|
|
@@ -4572,7 +5275,7 @@ impl Codegen {
|
|
|
4572
5275
|
}
|
|
4573
5276
|
}
|
|
4574
5277
|
Statement::VarDeclDestructure { init, .. } => Self::collect_expr_idents(init, idents),
|
|
4575
|
-
Statement::Block { statements, .. } => {
|
|
5278
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
4576
5279
|
for s in statements {
|
|
4577
5280
|
Self::collect_stmt_idents(s, idents);
|
|
4578
5281
|
}
|
|
@@ -4718,6 +5421,8 @@ impl Codegen {
|
|
|
4718
5421
|
"Math",
|
|
4719
5422
|
"JSON",
|
|
4720
5423
|
"Date",
|
|
5424
|
+
"Set",
|
|
5425
|
+
"Map",
|
|
4721
5426
|
"Object",
|
|
4722
5427
|
"process",
|
|
4723
5428
|
"setTimeout",
|
|
@@ -4743,12 +5448,12 @@ impl Codegen {
|
|
|
4743
5448
|
// (cleanups may only read `timer2` but must see updates from nested callbacks).
|
|
4744
5449
|
let cell_capture_outer_vars: Vec<String> = outer_vars
|
|
4745
5450
|
.iter()
|
|
4746
|
-
.filter(|v| assigned_in_body.contains(*v) || self.rc_cell_storage_contains(
|
|
5451
|
+
.filter(|v| assigned_in_body.contains(*v) || self.rc_cell_storage_contains(v))
|
|
4747
5452
|
.cloned()
|
|
4748
5453
|
.collect();
|
|
4749
5454
|
let read_only_outer_vars: Vec<String> = outer_vars
|
|
4750
5455
|
.iter()
|
|
4751
|
-
.filter(|v| !assigned_in_body.contains(*v) && !self.rc_cell_storage_contains(
|
|
5456
|
+
.filter(|v| !assigned_in_body.contains(*v) && !self.rc_cell_storage_contains(v))
|
|
4752
5457
|
.cloned()
|
|
4753
5458
|
.collect();
|
|
4754
5459
|
|
|
@@ -4790,8 +5495,18 @@ impl Codegen {
|
|
|
4790
5495
|
"Math",
|
|
4791
5496
|
"JSON",
|
|
4792
5497
|
"Date",
|
|
5498
|
+
"Set",
|
|
5499
|
+
"Map",
|
|
4793
5500
|
"Object",
|
|
5501
|
+
"Float64Array",
|
|
5502
|
+
"Float32Array",
|
|
5503
|
+
"Int8Array",
|
|
4794
5504
|
"Uint8Array",
|
|
5505
|
+
"Uint8ClampedArray",
|
|
5506
|
+
"Int16Array",
|
|
5507
|
+
"Uint16Array",
|
|
5508
|
+
"Int32Array",
|
|
5509
|
+
"Uint32Array",
|
|
4795
5510
|
"AudioContext",
|
|
4796
5511
|
"process",
|
|
4797
5512
|
"setTimeout",
|
|
@@ -4916,11 +5631,29 @@ impl Codegen {
|
|
|
4916
5631
|
for (i, p) in params.iter().enumerate() {
|
|
4917
5632
|
match p {
|
|
4918
5633
|
FunParam::Simple(tp) => {
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
5634
|
+
if let Some(default_expr) = &tp.default {
|
|
5635
|
+
// Default applies only for a MISSING positional arg (matches interp + VM);
|
|
5636
|
+
// an explicit `null` keeps the null. emit_expr captured like the destructure
|
|
5637
|
+
// path below so any prelude lands in `code`, not the outer output buffer.
|
|
5638
|
+
let saved = std::mem::take(&mut self.output);
|
|
5639
|
+
let default_str = self.emit_expr(default_expr)?;
|
|
5640
|
+
let prelude = std::mem::replace(&mut self.output, saved);
|
|
5641
|
+
code.push_str(&prelude);
|
|
5642
|
+
code.push_str(&format!(
|
|
5643
|
+
" {} {} = match args.get({}) {{ Some(v) => v.clone(), None => {} }};\n",
|
|
5644
|
+
Self::mut_kw_for(tp.name.as_ref(), "let mut"),
|
|
5645
|
+
Self::escape_ident(tp.name.as_ref()),
|
|
5646
|
+
i,
|
|
5647
|
+
default_str
|
|
5648
|
+
));
|
|
5649
|
+
} else {
|
|
5650
|
+
code.push_str(&format!(
|
|
5651
|
+
" {} {} = args.get({}).cloned().unwrap_or(Value::Null);\n",
|
|
5652
|
+
Self::mut_kw_for(tp.name.as_ref(), "let mut"),
|
|
5653
|
+
Self::escape_ident(tp.name.as_ref()),
|
|
5654
|
+
i
|
|
5655
|
+
));
|
|
5656
|
+
}
|
|
4924
5657
|
}
|
|
4925
5658
|
FunParam::Destructure { pattern, .. } => {
|
|
4926
5659
|
let tmp = format!("_formal_{}", i);
|
|
@@ -4961,7 +5694,14 @@ impl Codegen {
|
|
|
4961
5694
|
let arrow_body_res: Result<(), CompileError> = match body {
|
|
4962
5695
|
tishlang_ast::ArrowBody::Expr(expr) => {
|
|
4963
5696
|
let expr_code = self.emit_expr(expr)?;
|
|
4964
|
-
|
|
5697
|
+
// Bind to a temp before the closure returns: if `expr_code` reads a
|
|
5698
|
+
// cell-captured var its `RefCell` borrow guard is a temporary, and a
|
|
5699
|
+
// borrow left in tail position outlives the local cell binding —
|
|
5700
|
+
// which fails to compile (E0597). The `let` releases it at the `;`.
|
|
5701
|
+
code.push_str(&format!(
|
|
5702
|
+
" let __arrow_ret = {};\n __arrow_ret\n",
|
|
5703
|
+
expr_code
|
|
5704
|
+
));
|
|
4965
5705
|
Ok(())
|
|
4966
5706
|
}
|
|
4967
5707
|
tishlang_ast::ArrowBody::Block(block_stmt) => {
|
|
@@ -5016,7 +5756,7 @@ impl Codegen {
|
|
|
5016
5756
|
if let Expr::Literal { value, .. } = expr {
|
|
5017
5757
|
match (target_type, value) {
|
|
5018
5758
|
(RustType::F64, Literal::Number(n)) => {
|
|
5019
|
-
return Ok(
|
|
5759
|
+
return Ok(Self::f64_lit(*n));
|
|
5020
5760
|
}
|
|
5021
5761
|
(RustType::String, Literal::String(s)) => {
|
|
5022
5762
|
return Ok(format!("{:?}.to_string()", s.as_ref()));
|
|
@@ -5050,6 +5790,28 @@ impl Codegen {
|
|
|
5050
5790
|
return Ok(format!("vec![{}]", items.join(", ")));
|
|
5051
5791
|
}
|
|
5052
5792
|
|
|
5793
|
+
// Tuple literal: `[a, b]` against a `[T0, T1]` tuple type -> native Rust tuple `(a, b)`.
|
|
5794
|
+
if let (RustType::Tuple(elem_types), Expr::Array { elements, .. }) = (target_type, expr) {
|
|
5795
|
+
if elements.len() == elem_types.len()
|
|
5796
|
+
&& elements.iter().all(|e| matches!(e, ArrayElement::Expr(_)))
|
|
5797
|
+
{
|
|
5798
|
+
let mut items = Vec::new();
|
|
5799
|
+
for (elem, ty) in elements.iter().zip(elem_types) {
|
|
5800
|
+
if let ArrayElement::Expr(e) = elem {
|
|
5801
|
+
items.push(self.emit_native_expr(e, ty)?);
|
|
5802
|
+
}
|
|
5803
|
+
}
|
|
5804
|
+
return Ok(if items.len() == 1 {
|
|
5805
|
+
format!("({},)", items[0])
|
|
5806
|
+
} else {
|
|
5807
|
+
format!("({})", items.join(", "))
|
|
5808
|
+
});
|
|
5809
|
+
}
|
|
5810
|
+
// arity/shape mismatch -> boxed fallback
|
|
5811
|
+
let value_expr = self.emit_expr(expr)?;
|
|
5812
|
+
return Ok(target_type.from_value_expr(&value_expr));
|
|
5813
|
+
}
|
|
5814
|
+
|
|
5053
5815
|
// Try to emit object literals directly as a Rust struct literal
|
|
5054
5816
|
// when the target is a `RustType::Named` (a user `type Foo = {...}`
|
|
5055
5817
|
// alias). Each property in source order is matched to a struct
|
|
@@ -5110,6 +5872,17 @@ impl Codegen {
|
|
|
5110
5872
|
}
|
|
5111
5873
|
}
|
|
5112
5874
|
|
|
5875
|
+
// Native typed-array HOFs (TISH_NATIVE_HOF): `xs.reduce/map/filter/some/every(<arrow>)`
|
|
5876
|
+
// whose native result type matches this binding's target → emit the iterator chain
|
|
5877
|
+
// directly, with NO box/unbox round-trip (the per-element `value_call` is gone too).
|
|
5878
|
+
if let Expr::Call { callee, args, .. } = expr {
|
|
5879
|
+
if let Some((code, ty)) = self.native_vec_hof_for_call(callee, args)? {
|
|
5880
|
+
if &ty == target_type {
|
|
5881
|
+
return Ok(code);
|
|
5882
|
+
}
|
|
5883
|
+
}
|
|
5884
|
+
}
|
|
5885
|
+
|
|
5113
5886
|
// Fall back to emit_expr + conversion
|
|
5114
5887
|
let value_expr = self.emit_expr(expr)?;
|
|
5115
5888
|
Ok(target_type.from_value_expr(&value_expr))
|
|
@@ -5125,95 +5898,863 @@ impl Codegen {
|
|
|
5125
5898
|
/// through arithmetic, indexing, and assignments. For any expression this
|
|
5126
5899
|
/// function cannot handle natively, it falls back to `emit_expr` and returns
|
|
5127
5900
|
/// `RustType::Value`.
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5901
|
+
// ───────────────────────── M5: native monomorphic functions ─────────────────────────
|
|
5902
|
+
fn ann_is_number(ann: &TypeAnnotation) -> bool {
|
|
5903
|
+
RustType::from_annotation(ann) == RustType::F64
|
|
5904
|
+
}
|
|
5905
|
+
|
|
5906
|
+
// ── Soundness: demote `number` locals that a reassignment can turn non-numeric ──────────────
|
|
5907
|
+
//
|
|
5908
|
+
// `let s = 0` is inferred `number` → lowered to a native `f64`, and a reassignment stores into
|
|
5909
|
+
// it via `s = match &<rhs> { Value::Number(n) => *n, _ => panic!("expected number") }`. That
|
|
5910
|
+
// coercion PANICS when `<rhs>` is not a number — which `s = s + arr[i]` produces whenever
|
|
5911
|
+
// `arr[i]` is a String (JS `+` is string concat). Node, the interpreter, and the VM all yield
|
|
5912
|
+
// a string there (the VM array-JIT bails to the interpreter on a non-numeric element). The
|
|
5913
|
+
// fix: keep such a local a boxed `Value`, so the boxed `ops::add` — which concatenates —
|
|
5914
|
+
// flows through unchanged.
|
|
5915
|
+
//
|
|
5916
|
+
// A reassignment is SAFE iff its RHS lowers to a native `f64`, which is exactly what
|
|
5917
|
+
// `emit_typed_expr` decides. `expr_native_type` is a read-only mirror of that decision and is
|
|
5918
|
+
// deliberately conservative: any form it does not model → `Value` → demote (sound; at worst an
|
|
5919
|
+
// unnecessary box). A fixpoint propagates demotions through chains (`y = y + s` once `s` is
|
|
5920
|
+
// demoted). The map is name-flat across the whole program (a name demoted in one function is
|
|
5921
|
+
// demoted in all) — still sound, and harmless to the perf gauntlet, where each kernel is its
|
|
5922
|
+
// own program with unique accumulator names.
|
|
5923
|
+
fn collect_demoted_numeric_locals(&self, stmts: &[Statement]) -> HashSet<String> {
|
|
5924
|
+
// 1. Flat env: every annotated local/param name → its native `RustType`.
|
|
5925
|
+
let mut env: HashMap<String, RustType> = HashMap::new();
|
|
5926
|
+
Self::collect_annotated_types(stmts, &self.type_aliases, &mut env);
|
|
5927
|
+
// 2. Every reassignment `(name, rhs)` anywhere in the program (incl. nested exprs/closures).
|
|
5928
|
+
let mut reassigns: Vec<(String, &Expr)> = Vec::new();
|
|
5929
|
+
Self::collect_reassignments_stmts(stmts, &mut reassigns);
|
|
5930
|
+
// 3. Fixpoint: demote a `number` local whose any reassignment RHS isn't native `f64`.
|
|
5931
|
+
let mut demoted: HashSet<String> = HashSet::new();
|
|
5932
|
+
loop {
|
|
5933
|
+
let mut changed = false;
|
|
5934
|
+
for (name, rhs) in &reassigns {
|
|
5935
|
+
if demoted.contains(name) {
|
|
5936
|
+
continue;
|
|
5135
5937
|
}
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5938
|
+
if env.get(name) == Some(&RustType::F64)
|
|
5939
|
+
&& self.expr_native_type(rhs, &env) != RustType::F64
|
|
5940
|
+
{
|
|
5941
|
+
demoted.insert(name.clone());
|
|
5942
|
+
env.insert(name.clone(), RustType::Value);
|
|
5943
|
+
changed = true;
|
|
5944
|
+
}
|
|
5945
|
+
}
|
|
5946
|
+
if !changed {
|
|
5947
|
+
break;
|
|
5948
|
+
}
|
|
5949
|
+
}
|
|
5950
|
+
demoted
|
|
5951
|
+
}
|
|
5139
5952
|
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5953
|
+
/// Record every annotated `VarDecl`/param name → its native `RustType`, recursing through all
|
|
5954
|
+
/// nested statements (loops, ifs, blocks, switch/try, function bodies). Flat; last write wins.
|
|
5955
|
+
fn collect_annotated_types(
|
|
5956
|
+
stmts: &[Statement],
|
|
5957
|
+
aliases: &HashMap<String, RustType>,
|
|
5958
|
+
env: &mut HashMap<String, RustType>,
|
|
5959
|
+
) {
|
|
5960
|
+
for s in stmts {
|
|
5961
|
+
match s {
|
|
5962
|
+
Statement::VarDecl {
|
|
5963
|
+
name,
|
|
5964
|
+
type_ann: Some(ann),
|
|
5965
|
+
..
|
|
5966
|
+
} => {
|
|
5967
|
+
env.insert(
|
|
5968
|
+
name.to_string(),
|
|
5969
|
+
RustType::from_annotation_with_aliases(ann, aliases),
|
|
5970
|
+
);
|
|
5971
|
+
}
|
|
5972
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
5973
|
+
Self::collect_annotated_types(statements, aliases, env)
|
|
5974
|
+
}
|
|
5975
|
+
Statement::If {
|
|
5976
|
+
then_branch,
|
|
5977
|
+
else_branch,
|
|
5978
|
+
..
|
|
5979
|
+
} => {
|
|
5980
|
+
Self::collect_annotated_types(std::slice::from_ref(then_branch), aliases, env);
|
|
5981
|
+
if let Some(e) = else_branch {
|
|
5982
|
+
Self::collect_annotated_types(std::slice::from_ref(e), aliases, env);
|
|
5149
5983
|
}
|
|
5150
|
-
}
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5984
|
+
}
|
|
5985
|
+
Statement::While { body, .. } | Statement::DoWhile { body, .. } => {
|
|
5986
|
+
Self::collect_annotated_types(std::slice::from_ref(body), aliases, env)
|
|
5987
|
+
}
|
|
5988
|
+
Statement::ForOf {
|
|
5989
|
+
name,
|
|
5990
|
+
iterable,
|
|
5991
|
+
body,
|
|
5992
|
+
..
|
|
5993
|
+
} => {
|
|
5994
|
+
// A loop var iterating a `Vec<elem>` local binds `elem` — so `total += n` (n the
|
|
5995
|
+
// loop var over a `number[]`) is seen as native f64 and `total` is NOT demoted.
|
|
5996
|
+
// Sound: the Vec's elements are genuinely that native type at runtime.
|
|
5997
|
+
if let Expr::Ident { name: it_name, .. } = iterable {
|
|
5998
|
+
let elem_ty = match env.get(it_name.as_ref()) {
|
|
5999
|
+
Some(RustType::Vec(elem)) => Some((**elem).clone()),
|
|
6000
|
+
_ => None,
|
|
6001
|
+
};
|
|
6002
|
+
if let Some(t) = elem_ty {
|
|
6003
|
+
env.insert(name.to_string(), t);
|
|
6004
|
+
}
|
|
5156
6005
|
}
|
|
6006
|
+
Self::collect_annotated_types(std::slice::from_ref(body), aliases, env)
|
|
5157
6007
|
}
|
|
6008
|
+
Statement::For { init, body, .. } => {
|
|
6009
|
+
if let Some(i) = init {
|
|
6010
|
+
Self::collect_annotated_types(std::slice::from_ref(i), aliases, env);
|
|
6011
|
+
}
|
|
6012
|
+
Self::collect_annotated_types(std::slice::from_ref(body), aliases, env);
|
|
6013
|
+
}
|
|
6014
|
+
Statement::FunDecl {
|
|
6015
|
+
params,
|
|
6016
|
+
rest_param,
|
|
6017
|
+
body,
|
|
6018
|
+
..
|
|
6019
|
+
} => {
|
|
6020
|
+
for p in params {
|
|
6021
|
+
if let FunParam::Simple(tp) = p {
|
|
6022
|
+
if let Some(ann) = &tp.type_ann {
|
|
6023
|
+
env.insert(
|
|
6024
|
+
tp.name.to_string(),
|
|
6025
|
+
RustType::from_annotation_with_aliases(ann, aliases),
|
|
6026
|
+
);
|
|
6027
|
+
}
|
|
6028
|
+
}
|
|
6029
|
+
}
|
|
6030
|
+
// Typed rest-param `...args: number[]` -> `Vec<f64>`, so a ForOf loop var over it
|
|
6031
|
+
// binds the element type and accumulators stay native.
|
|
6032
|
+
if let Some(rp) = rest_param {
|
|
6033
|
+
if let Some(ann) = &rp.type_ann {
|
|
6034
|
+
env.insert(
|
|
6035
|
+
rp.name.to_string(),
|
|
6036
|
+
RustType::from_annotation_with_aliases(ann, aliases),
|
|
6037
|
+
);
|
|
6038
|
+
}
|
|
6039
|
+
}
|
|
6040
|
+
Self::collect_annotated_types(std::slice::from_ref(body), aliases, env);
|
|
6041
|
+
}
|
|
6042
|
+
Statement::Switch {
|
|
6043
|
+
cases,
|
|
6044
|
+
default_body,
|
|
6045
|
+
..
|
|
6046
|
+
} => {
|
|
6047
|
+
for (_, body) in cases {
|
|
6048
|
+
Self::collect_annotated_types(body, aliases, env);
|
|
6049
|
+
}
|
|
6050
|
+
if let Some(b) = default_body {
|
|
6051
|
+
Self::collect_annotated_types(b, aliases, env);
|
|
6052
|
+
}
|
|
6053
|
+
}
|
|
6054
|
+
Statement::Try {
|
|
6055
|
+
body,
|
|
6056
|
+
catch_body,
|
|
6057
|
+
finally_body,
|
|
6058
|
+
..
|
|
6059
|
+
} => {
|
|
6060
|
+
Self::collect_annotated_types(std::slice::from_ref(body), aliases, env);
|
|
6061
|
+
if let Some(b) = catch_body {
|
|
6062
|
+
Self::collect_annotated_types(std::slice::from_ref(b), aliases, env);
|
|
6063
|
+
}
|
|
6064
|
+
if let Some(b) = finally_body {
|
|
6065
|
+
Self::collect_annotated_types(std::slice::from_ref(b), aliases, env);
|
|
6066
|
+
}
|
|
6067
|
+
}
|
|
6068
|
+
_ => {}
|
|
5158
6069
|
}
|
|
6070
|
+
}
|
|
6071
|
+
}
|
|
5159
6072
|
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
6073
|
+
fn collect_reassignments_stmts<'a>(stmts: &'a [Statement], out: &mut Vec<(String, &'a Expr)>) {
|
|
6074
|
+
for s in stmts {
|
|
6075
|
+
Self::collect_reassignments_stmt(s, out);
|
|
6076
|
+
}
|
|
6077
|
+
}
|
|
6078
|
+
|
|
6079
|
+
/// Collect every `(name, rhs)` reassignment (`=`, compound `+=`, logical `||=`) reachable from
|
|
6080
|
+
/// `s` — descending through nested statements and expressions (including closures).
|
|
6081
|
+
fn collect_reassignments_stmt<'a>(s: &'a Statement, out: &mut Vec<(String, &'a Expr)>) {
|
|
6082
|
+
match s {
|
|
6083
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
6084
|
+
Self::collect_reassignments_stmts(statements, out)
|
|
6085
|
+
}
|
|
6086
|
+
Statement::VarDecl { init: Some(e), .. } => Self::collect_reassignments_expr(e, out),
|
|
6087
|
+
Statement::VarDeclDestructure { init, .. } => {
|
|
6088
|
+
Self::collect_reassignments_expr(init, out)
|
|
6089
|
+
}
|
|
6090
|
+
Statement::ExprStmt { expr, .. } => Self::collect_reassignments_expr(expr, out),
|
|
6091
|
+
Statement::If {
|
|
6092
|
+
cond,
|
|
6093
|
+
then_branch,
|
|
6094
|
+
else_branch,
|
|
5166
6095
|
..
|
|
5167
6096
|
} => {
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
// Both sides are compatible native types → emit native op.
|
|
5173
|
-
let code = match op {
|
|
5174
|
-
BinOp::Add => format!("({} + {})", l, r),
|
|
5175
|
-
BinOp::Sub => format!("({} - {})", l, r),
|
|
5176
|
-
BinOp::Mul => format!("({} * {})", l, r),
|
|
5177
|
-
BinOp::Div => format!("({} / {})", l, r),
|
|
5178
|
-
BinOp::Mod => format!("({} % {})", l, r),
|
|
5179
|
-
BinOp::Pow => format!("({}).powf({})", l, r),
|
|
5180
|
-
BinOp::Lt => format!("({} < {})", l, r),
|
|
5181
|
-
BinOp::Le => format!("({} <= {})", l, r),
|
|
5182
|
-
BinOp::Gt => format!("({} > {})", l, r),
|
|
5183
|
-
BinOp::Ge => format!("({} >= {})", l, r),
|
|
5184
|
-
BinOp::StrictEq => format!("({} == {})", l, r),
|
|
5185
|
-
BinOp::StrictNe => format!("({} != {})", l, r),
|
|
5186
|
-
BinOp::And => format!("({} && {})", l, r),
|
|
5187
|
-
BinOp::Or => format!("({} || {})", l, r),
|
|
5188
|
-
_ => unreachable!("result_type_of_binop covers all handled ops"),
|
|
5189
|
-
};
|
|
5190
|
-
return Ok((code, result_ty));
|
|
6097
|
+
Self::collect_reassignments_expr(cond, out);
|
|
6098
|
+
Self::collect_reassignments_stmt(then_branch, out);
|
|
6099
|
+
if let Some(e) = else_branch {
|
|
6100
|
+
Self::collect_reassignments_stmt(e, out);
|
|
5191
6101
|
}
|
|
5192
|
-
|
|
5193
|
-
// Fall back: convert both sides to Value and use the runtime.
|
|
5194
|
-
let lv = if lt.is_native() {
|
|
5195
|
-
lt.to_value_expr(&l)
|
|
5196
|
-
} else {
|
|
5197
|
-
l
|
|
5198
|
-
};
|
|
5199
|
-
let rv = if rt.is_native() {
|
|
5200
|
-
rt.to_value_expr(&r)
|
|
5201
|
-
} else {
|
|
5202
|
-
r
|
|
5203
|
-
};
|
|
5204
|
-
let result = self.emit_binop(&lv, *op, &rv, *span)?;
|
|
5205
|
-
Ok((result, RustType::Value))
|
|
5206
6102
|
}
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
6103
|
+
Statement::While { cond, body, .. } => {
|
|
6104
|
+
Self::collect_reassignments_expr(cond, out);
|
|
6105
|
+
Self::collect_reassignments_stmt(body, out);
|
|
6106
|
+
}
|
|
6107
|
+
Statement::DoWhile { body, cond, .. } => {
|
|
6108
|
+
Self::collect_reassignments_stmt(body, out);
|
|
6109
|
+
Self::collect_reassignments_expr(cond, out);
|
|
6110
|
+
}
|
|
6111
|
+
Statement::For {
|
|
6112
|
+
init,
|
|
6113
|
+
cond,
|
|
6114
|
+
update,
|
|
6115
|
+
body,
|
|
5213
6116
|
..
|
|
5214
6117
|
} => {
|
|
5215
|
-
|
|
5216
|
-
|
|
6118
|
+
if let Some(i) = init {
|
|
6119
|
+
Self::collect_reassignments_stmt(i, out);
|
|
6120
|
+
}
|
|
6121
|
+
if let Some(c) = cond {
|
|
6122
|
+
Self::collect_reassignments_expr(c, out);
|
|
6123
|
+
}
|
|
6124
|
+
if let Some(u) = update {
|
|
6125
|
+
Self::collect_reassignments_expr(u, out);
|
|
6126
|
+
}
|
|
6127
|
+
Self::collect_reassignments_stmt(body, out);
|
|
6128
|
+
}
|
|
6129
|
+
Statement::ForOf { iterable, body, .. } => {
|
|
6130
|
+
Self::collect_reassignments_expr(iterable, out);
|
|
6131
|
+
Self::collect_reassignments_stmt(body, out);
|
|
6132
|
+
}
|
|
6133
|
+
Statement::Return { value: Some(e), .. } => Self::collect_reassignments_expr(e, out),
|
|
6134
|
+
Statement::Throw { value, .. } => Self::collect_reassignments_expr(value, out),
|
|
6135
|
+
Statement::FunDecl { body, .. } => Self::collect_reassignments_stmt(body, out),
|
|
6136
|
+
Statement::Switch {
|
|
6137
|
+
expr,
|
|
6138
|
+
cases,
|
|
6139
|
+
default_body,
|
|
6140
|
+
..
|
|
6141
|
+
} => {
|
|
6142
|
+
Self::collect_reassignments_expr(expr, out);
|
|
6143
|
+
for (g, body) in cases {
|
|
6144
|
+
if let Some(g) = g {
|
|
6145
|
+
Self::collect_reassignments_expr(g, out);
|
|
6146
|
+
}
|
|
6147
|
+
Self::collect_reassignments_stmts(body, out);
|
|
6148
|
+
}
|
|
6149
|
+
if let Some(b) = default_body {
|
|
6150
|
+
Self::collect_reassignments_stmts(b, out);
|
|
6151
|
+
}
|
|
6152
|
+
}
|
|
6153
|
+
Statement::Try {
|
|
6154
|
+
body,
|
|
6155
|
+
catch_body,
|
|
6156
|
+
finally_body,
|
|
6157
|
+
..
|
|
6158
|
+
} => {
|
|
6159
|
+
Self::collect_reassignments_stmt(body, out);
|
|
6160
|
+
if let Some(b) = catch_body {
|
|
6161
|
+
Self::collect_reassignments_stmt(b, out);
|
|
6162
|
+
}
|
|
6163
|
+
if let Some(b) = finally_body {
|
|
6164
|
+
Self::collect_reassignments_stmt(b, out);
|
|
6165
|
+
}
|
|
6166
|
+
}
|
|
6167
|
+
_ => {}
|
|
6168
|
+
}
|
|
6169
|
+
}
|
|
6170
|
+
|
|
6171
|
+
fn collect_reassignments_expr<'a>(e: &'a Expr, out: &mut Vec<(String, &'a Expr)>) {
|
|
6172
|
+
match e {
|
|
6173
|
+
Expr::Assign { name, value, .. }
|
|
6174
|
+
| Expr::CompoundAssign { name, value, .. }
|
|
6175
|
+
| Expr::LogicalAssign { name, value, .. } => {
|
|
6176
|
+
out.push((name.to_string(), value.as_ref()));
|
|
6177
|
+
Self::collect_reassignments_expr(value, out);
|
|
6178
|
+
}
|
|
6179
|
+
Expr::Binary { left, right, .. } | Expr::NullishCoalesce { left, right, .. } => {
|
|
6180
|
+
Self::collect_reassignments_expr(left, out);
|
|
6181
|
+
Self::collect_reassignments_expr(right, out);
|
|
6182
|
+
}
|
|
6183
|
+
Expr::Unary { operand, .. }
|
|
6184
|
+
| Expr::TypeOf { operand, .. }
|
|
6185
|
+
| Expr::Await { operand, .. } => Self::collect_reassignments_expr(operand, out),
|
|
6186
|
+
Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
|
|
6187
|
+
Self::collect_reassignments_expr(callee, out);
|
|
6188
|
+
for a in args {
|
|
6189
|
+
match a {
|
|
6190
|
+
CallArg::Expr(x) | CallArg::Spread(x) => {
|
|
6191
|
+
Self::collect_reassignments_expr(x, out)
|
|
6192
|
+
}
|
|
6193
|
+
}
|
|
6194
|
+
}
|
|
6195
|
+
}
|
|
6196
|
+
Expr::Member { object, prop, .. } => {
|
|
6197
|
+
Self::collect_reassignments_expr(object, out);
|
|
6198
|
+
if let MemberProp::Expr(p) = prop {
|
|
6199
|
+
Self::collect_reassignments_expr(p, out);
|
|
6200
|
+
}
|
|
6201
|
+
}
|
|
6202
|
+
Expr::Index { object, index, .. } => {
|
|
6203
|
+
Self::collect_reassignments_expr(object, out);
|
|
6204
|
+
Self::collect_reassignments_expr(index, out);
|
|
6205
|
+
}
|
|
6206
|
+
Expr::Conditional {
|
|
6207
|
+
cond,
|
|
6208
|
+
then_branch,
|
|
6209
|
+
else_branch,
|
|
6210
|
+
..
|
|
6211
|
+
} => {
|
|
6212
|
+
Self::collect_reassignments_expr(cond, out);
|
|
6213
|
+
Self::collect_reassignments_expr(then_branch, out);
|
|
6214
|
+
Self::collect_reassignments_expr(else_branch, out);
|
|
6215
|
+
}
|
|
6216
|
+
Expr::Array { elements, .. } => {
|
|
6217
|
+
for el in elements {
|
|
6218
|
+
match el {
|
|
6219
|
+
ArrayElement::Expr(x) | ArrayElement::Spread(x) => {
|
|
6220
|
+
Self::collect_reassignments_expr(x, out)
|
|
6221
|
+
}
|
|
6222
|
+
}
|
|
6223
|
+
}
|
|
6224
|
+
}
|
|
6225
|
+
Expr::Object { props, .. } => {
|
|
6226
|
+
for p in props {
|
|
6227
|
+
match p {
|
|
6228
|
+
ObjectProp::KeyValue(_, v) => Self::collect_reassignments_expr(v, out),
|
|
6229
|
+
ObjectProp::Spread(x) => Self::collect_reassignments_expr(x, out),
|
|
6230
|
+
}
|
|
6231
|
+
}
|
|
6232
|
+
}
|
|
6233
|
+
Expr::MemberAssign { object, value, .. } => {
|
|
6234
|
+
Self::collect_reassignments_expr(object, out);
|
|
6235
|
+
Self::collect_reassignments_expr(value, out);
|
|
6236
|
+
}
|
|
6237
|
+
Expr::IndexAssign {
|
|
6238
|
+
object,
|
|
6239
|
+
index,
|
|
6240
|
+
value,
|
|
6241
|
+
..
|
|
6242
|
+
} => {
|
|
6243
|
+
Self::collect_reassignments_expr(object, out);
|
|
6244
|
+
Self::collect_reassignments_expr(index, out);
|
|
6245
|
+
Self::collect_reassignments_expr(value, out);
|
|
6246
|
+
}
|
|
6247
|
+
Expr::TemplateLiteral { exprs, .. } => {
|
|
6248
|
+
for x in exprs {
|
|
6249
|
+
Self::collect_reassignments_expr(x, out);
|
|
6250
|
+
}
|
|
6251
|
+
}
|
|
6252
|
+
Expr::ArrowFunction { body, .. } => match body {
|
|
6253
|
+
ArrowBody::Expr(x) => Self::collect_reassignments_expr(x, out),
|
|
6254
|
+
ArrowBody::Block(b) => Self::collect_reassignments_stmt(b, out),
|
|
6255
|
+
},
|
|
6256
|
+
_ => {}
|
|
6257
|
+
}
|
|
6258
|
+
}
|
|
6259
|
+
|
|
6260
|
+
/// Read-only mirror of `emit_typed_expr`'s native-type decision (no code generated), over a
|
|
6261
|
+
/// flat `name → RustType` env. Returns `RustType::F64` only for forms that provably lower to a
|
|
6262
|
+
/// native `f64`; everything else → `RustType::Value`. Conservative by construction: it never
|
|
6263
|
+
/// claims `F64` where `emit_typed_expr` would box, so a numeric local is never wrongly kept
|
|
6264
|
+
/// native (which would reintroduce the coercion panic).
|
|
6265
|
+
fn expr_native_type(&self, e: &Expr, env: &HashMap<String, RustType>) -> RustType {
|
|
6266
|
+
match e {
|
|
6267
|
+
Expr::Literal { value, .. } => match value {
|
|
6268
|
+
Literal::Number(_) => RustType::F64,
|
|
6269
|
+
Literal::String(_) => RustType::String,
|
|
6270
|
+
Literal::Bool(_) => RustType::Bool,
|
|
6271
|
+
Literal::Null => RustType::Value,
|
|
6272
|
+
},
|
|
6273
|
+
Expr::Ident { name, .. } => env
|
|
6274
|
+
.get(name.as_ref())
|
|
6275
|
+
.filter(|t| t.is_native())
|
|
6276
|
+
.cloned()
|
|
6277
|
+
.unwrap_or(RustType::Value),
|
|
6278
|
+
Expr::Binary {
|
|
6279
|
+
left, op, right, ..
|
|
6280
|
+
} => {
|
|
6281
|
+
let lt = self.expr_native_type(left, env);
|
|
6282
|
+
let rt = self.expr_native_type(right, env);
|
|
6283
|
+
RustType::result_type_of_binop(*op, <, &rt).unwrap_or(RustType::Value)
|
|
6284
|
+
}
|
|
6285
|
+
// `vec[i]` where `vec` is a `number[]` (Vec<f64>) → the element type. A `Vec<f64>`
|
|
6286
|
+
// can only hold numbers, so this never feeds a string into the accumulator.
|
|
6287
|
+
Expr::Index {
|
|
6288
|
+
object,
|
|
6289
|
+
optional: false,
|
|
6290
|
+
..
|
|
6291
|
+
} => {
|
|
6292
|
+
if let Expr::Ident { name, .. } = object.as_ref() {
|
|
6293
|
+
if let Some(RustType::Vec(inner)) = env.get(name.as_ref()) {
|
|
6294
|
+
return (**inner).clone();
|
|
6295
|
+
}
|
|
6296
|
+
}
|
|
6297
|
+
RustType::Value
|
|
6298
|
+
}
|
|
6299
|
+
// `o.field` where `o` is a native struct local and `field` is a native field.
|
|
6300
|
+
Expr::Member {
|
|
6301
|
+
object,
|
|
6302
|
+
prop: MemberProp::Name { name: prop_name, .. },
|
|
6303
|
+
optional: false,
|
|
6304
|
+
..
|
|
6305
|
+
} => {
|
|
6306
|
+
if let Expr::Ident { name: var_name, .. } = object.as_ref() {
|
|
6307
|
+
if let Some(RustType::Named { fields, .. }) = env.get(var_name.as_ref()) {
|
|
6308
|
+
if let Some((_, field_ty)) =
|
|
6309
|
+
fields.iter().find(|(k, _)| k.as_ref() == prop_name.as_ref())
|
|
6310
|
+
{
|
|
6311
|
+
if field_ty.is_native() {
|
|
6312
|
+
return field_ty.clone();
|
|
6313
|
+
}
|
|
6314
|
+
}
|
|
6315
|
+
}
|
|
6316
|
+
}
|
|
6317
|
+
RustType::Value
|
|
6318
|
+
}
|
|
6319
|
+
Expr::Call { callee, args, .. } => {
|
|
6320
|
+
// M5 native fn (`fn f_native(..) -> f64`); requires all-positional args.
|
|
6321
|
+
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
6322
|
+
if self.native_fns.contains(fname.as_ref())
|
|
6323
|
+
&& args.iter().all(|a| matches!(a, CallArg::Expr(_)))
|
|
6324
|
+
{
|
|
6325
|
+
return RustType::F64;
|
|
6326
|
+
}
|
|
6327
|
+
}
|
|
6328
|
+
// Single-arg `Math.<intrinsic>(x)` lowered to a direct `f64` method → number.
|
|
6329
|
+
if let [CallArg::Expr(_)] = args.as_slice() {
|
|
6330
|
+
if let Expr::Member {
|
|
6331
|
+
object,
|
|
6332
|
+
prop: MemberProp::Name { name: method, .. },
|
|
6333
|
+
..
|
|
6334
|
+
} = callee.as_ref()
|
|
6335
|
+
{
|
|
6336
|
+
if matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
|
|
6337
|
+
&& matches!(
|
|
6338
|
+
method.as_ref(),
|
|
6339
|
+
"sqrt" | "sin" | "cos" | "tan" | "abs" | "floor" | "ceil" | "exp"
|
|
6340
|
+
| "trunc" | "log"
|
|
6341
|
+
)
|
|
6342
|
+
{
|
|
6343
|
+
return RustType::F64;
|
|
6344
|
+
}
|
|
6345
|
+
}
|
|
6346
|
+
}
|
|
6347
|
+
RustType::Value
|
|
6348
|
+
}
|
|
6349
|
+
// Unary, Conditional, etc. are not modelled by `emit_typed_expr` (it boxes them), so a
|
|
6350
|
+
// store from one already coerces; treat as `Value` to match (→ demote if it feeds an
|
|
6351
|
+
// accumulator). Sound and consistent.
|
|
6352
|
+
_ => RustType::Value,
|
|
6353
|
+
}
|
|
6354
|
+
}
|
|
6355
|
+
|
|
6356
|
+
/// Names of top-level fns eligible for a parallel native `fn f_native(f64,..)->f64`:
|
|
6357
|
+
/// non-async, every param `: number` (no default), `: number` return, and a native-safe
|
|
6358
|
+
/// body (only block/if/return/expr-stmt over native exprs + calls to other eligible fns or
|
|
6359
|
+
/// 1-arg Math intrinsics). Conservative fixpoint — bails on anything else.
|
|
6360
|
+
fn collect_native_fns(statements: &[Statement]) -> std::collections::HashSet<String> {
|
|
6361
|
+
use std::collections::HashSet;
|
|
6362
|
+
let mut cand: HashSet<String> = HashSet::new();
|
|
6363
|
+
let mut decls: Vec<(&str, &Vec<FunParam>, &Statement)> = Vec::new();
|
|
6364
|
+
for s in statements {
|
|
6365
|
+
if let Statement::FunDecl {
|
|
6366
|
+
async_: false,
|
|
6367
|
+
name,
|
|
6368
|
+
params,
|
|
6369
|
+
rest_param: None,
|
|
6370
|
+
return_type,
|
|
6371
|
+
body,
|
|
6372
|
+
..
|
|
6373
|
+
} = s
|
|
6374
|
+
{
|
|
6375
|
+
let params_ok = params.iter().all(|p| {
|
|
6376
|
+
matches!(p, FunParam::Simple(tp)
|
|
6377
|
+
if tp.default.is_none()
|
|
6378
|
+
&& tp.type_ann.as_ref().map(Self::ann_is_number).unwrap_or(false))
|
|
6379
|
+
});
|
|
6380
|
+
// Return: an annotated `: number`, OR unannotated with all-numeric returns
|
|
6381
|
+
// (verified in the fixpoint via `returns_numeric`), so the native `-> f64` holds.
|
|
6382
|
+
let ret_ok = match return_type {
|
|
6383
|
+
Some(rt) => Self::ann_is_number(rt),
|
|
6384
|
+
None => true,
|
|
6385
|
+
};
|
|
6386
|
+
if ret_ok && params_ok && !params.is_empty() {
|
|
6387
|
+
cand.insert(name.to_string());
|
|
6388
|
+
decls.push((name.as_ref(), params, body));
|
|
6389
|
+
}
|
|
6390
|
+
}
|
|
6391
|
+
}
|
|
6392
|
+
loop {
|
|
6393
|
+
let mut remove: Vec<String> = Vec::new();
|
|
6394
|
+
for &(name, params, body) in &decls {
|
|
6395
|
+
if !cand.contains(name) {
|
|
6396
|
+
continue;
|
|
6397
|
+
}
|
|
6398
|
+
let pnames: HashSet<String> =
|
|
6399
|
+
params.iter().flat_map(|p| p.bound_names()).map(|n| n.to_string()).collect();
|
|
6400
|
+
if !Self::native_safe_stmt(body, &pnames, &cand)
|
|
6401
|
+
|| !Self::returns_numeric(body, &pnames, &cand)
|
|
6402
|
+
{
|
|
6403
|
+
remove.push(name.to_string());
|
|
6404
|
+
}
|
|
6405
|
+
}
|
|
6406
|
+
if remove.is_empty() {
|
|
6407
|
+
break;
|
|
6408
|
+
}
|
|
6409
|
+
for n in remove {
|
|
6410
|
+
cand.remove(&n);
|
|
6411
|
+
}
|
|
6412
|
+
}
|
|
6413
|
+
cand
|
|
6414
|
+
}
|
|
6415
|
+
|
|
6416
|
+
fn native_safe_stmt(
|
|
6417
|
+
stmt: &Statement,
|
|
6418
|
+
params: &std::collections::HashSet<String>,
|
|
6419
|
+
cand: &std::collections::HashSet<String>,
|
|
6420
|
+
) -> bool {
|
|
6421
|
+
match stmt {
|
|
6422
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
6423
|
+
statements.iter().all(|s| Self::native_safe_stmt(s, params, cand))
|
|
6424
|
+
}
|
|
6425
|
+
Statement::Return { value, .. } => {
|
|
6426
|
+
value.as_ref().is_some_and(|e| Self::native_safe_expr(e, params, cand))
|
|
6427
|
+
}
|
|
6428
|
+
Statement::If { cond, then_branch, else_branch, .. } => {
|
|
6429
|
+
Self::native_safe_expr(cond, params, cand)
|
|
6430
|
+
&& Self::native_safe_stmt(then_branch, params, cand)
|
|
6431
|
+
&& else_branch.as_ref().is_none_or(|e| Self::native_safe_stmt(e, params, cand))
|
|
6432
|
+
}
|
|
6433
|
+
Statement::ExprStmt { expr, .. } => Self::native_safe_expr(expr, params, cand),
|
|
6434
|
+
_ => false,
|
|
6435
|
+
}
|
|
6436
|
+
}
|
|
6437
|
+
|
|
6438
|
+
fn native_safe_expr(
|
|
6439
|
+
expr: &Expr,
|
|
6440
|
+
params: &std::collections::HashSet<String>,
|
|
6441
|
+
cand: &std::collections::HashSet<String>,
|
|
6442
|
+
) -> bool {
|
|
6443
|
+
match expr {
|
|
6444
|
+
Expr::Literal { value, .. } => matches!(value, Literal::Number(_) | Literal::Bool(_)),
|
|
6445
|
+
Expr::Ident { name, .. } => params.contains(name.as_ref()),
|
|
6446
|
+
Expr::Binary { left, op, right, .. } => {
|
|
6447
|
+
matches!(
|
|
6448
|
+
op,
|
|
6449
|
+
BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow
|
|
6450
|
+
| BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge
|
|
6451
|
+
| BinOp::StrictEq | BinOp::StrictNe | BinOp::And | BinOp::Or
|
|
6452
|
+
) && Self::native_safe_expr(left, params, cand)
|
|
6453
|
+
&& Self::native_safe_expr(right, params, cand)
|
|
6454
|
+
}
|
|
6455
|
+
Expr::Unary { op, operand, .. } => {
|
|
6456
|
+
matches!(op, UnaryOp::Neg | UnaryOp::Pos | UnaryOp::Not)
|
|
6457
|
+
&& Self::native_safe_expr(operand, params, cand)
|
|
6458
|
+
}
|
|
6459
|
+
Expr::Call { callee, args, .. } => {
|
|
6460
|
+
let args_ok = args
|
|
6461
|
+
.iter()
|
|
6462
|
+
.all(|a| matches!(a, CallArg::Expr(e) if Self::native_safe_expr(e, params, cand)));
|
|
6463
|
+
if !args_ok {
|
|
6464
|
+
return false;
|
|
6465
|
+
}
|
|
6466
|
+
match callee.as_ref() {
|
|
6467
|
+
Expr::Ident { name, .. } => cand.contains(name.as_ref()),
|
|
6468
|
+
Expr::Member { object, prop: MemberProp::Name { name: m, .. }, .. } => {
|
|
6469
|
+
matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
|
|
6470
|
+
&& args.len() == 1
|
|
6471
|
+
&& matches!(
|
|
6472
|
+
m.as_ref(),
|
|
6473
|
+
"sqrt" | "sin" | "cos" | "tan" | "abs" | "floor" | "ceil" | "exp"
|
|
6474
|
+
| "trunc" | "log"
|
|
6475
|
+
)
|
|
6476
|
+
}
|
|
6477
|
+
_ => false,
|
|
6478
|
+
}
|
|
6479
|
+
}
|
|
6480
|
+
_ => false,
|
|
6481
|
+
}
|
|
6482
|
+
}
|
|
6483
|
+
|
|
6484
|
+
/// Every `return` in `s` yields a numeric-shaped value, so a native `-> f64` body is sound.
|
|
6485
|
+
/// Lets an unannotated-but-numeric-returning fn (e.g. `function fib(n) {...}` after M4 typed
|
|
6486
|
+
/// the param) become M5-eligible.
|
|
6487
|
+
fn returns_numeric(
|
|
6488
|
+
s: &Statement,
|
|
6489
|
+
params: &std::collections::HashSet<String>,
|
|
6490
|
+
cand: &std::collections::HashSet<String>,
|
|
6491
|
+
) -> bool {
|
|
6492
|
+
match s {
|
|
6493
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
6494
|
+
statements.iter().all(|x| Self::returns_numeric(x, params, cand))
|
|
6495
|
+
}
|
|
6496
|
+
Statement::Return { value, .. } => {
|
|
6497
|
+
value.as_ref().is_some_and(|e| Self::numeric_shaped(e, params, cand))
|
|
6498
|
+
}
|
|
6499
|
+
Statement::If { then_branch, else_branch, .. } => {
|
|
6500
|
+
Self::returns_numeric(then_branch, params, cand)
|
|
6501
|
+
&& else_branch.as_ref().is_none_or(|e| Self::returns_numeric(e, params, cand))
|
|
6502
|
+
}
|
|
6503
|
+
Statement::While { body, .. } | Statement::For { body, .. } => {
|
|
6504
|
+
Self::returns_numeric(body, params, cand)
|
|
6505
|
+
}
|
|
6506
|
+
_ => true, // no return in this statement form
|
|
6507
|
+
}
|
|
6508
|
+
}
|
|
6509
|
+
|
|
6510
|
+
/// `e` evaluates to a number: built from numeric params, number literals, ARITHMETIC binops
|
|
6511
|
+
/// (comparisons/logical yield bool → excluded), numeric unary, conditionals, and calls to
|
|
6512
|
+
/// eligible native fns / 1-arg Math.
|
|
6513
|
+
fn numeric_shaped(
|
|
6514
|
+
e: &Expr,
|
|
6515
|
+
params: &std::collections::HashSet<String>,
|
|
6516
|
+
cand: &std::collections::HashSet<String>,
|
|
6517
|
+
) -> bool {
|
|
6518
|
+
match e {
|
|
6519
|
+
Expr::Literal { value: Literal::Number(_), .. } => true,
|
|
6520
|
+
Expr::Ident { name, .. } => params.contains(name.as_ref()),
|
|
6521
|
+
Expr::Binary { left, op, right, .. } => {
|
|
6522
|
+
matches!(
|
|
6523
|
+
op,
|
|
6524
|
+
BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow
|
|
6525
|
+
) && Self::numeric_shaped(left, params, cand)
|
|
6526
|
+
&& Self::numeric_shaped(right, params, cand)
|
|
6527
|
+
}
|
|
6528
|
+
Expr::Unary { op, operand, .. } => {
|
|
6529
|
+
matches!(op, UnaryOp::Neg | UnaryOp::Pos)
|
|
6530
|
+
&& Self::numeric_shaped(operand, params, cand)
|
|
6531
|
+
}
|
|
6532
|
+
Expr::Conditional { then_branch, else_branch, .. } => {
|
|
6533
|
+
Self::numeric_shaped(then_branch, params, cand)
|
|
6534
|
+
&& Self::numeric_shaped(else_branch, params, cand)
|
|
6535
|
+
}
|
|
6536
|
+
Expr::Call { callee, .. } => match callee.as_ref() {
|
|
6537
|
+
Expr::Ident { name, .. } => cand.contains(name.as_ref()),
|
|
6538
|
+
Expr::Member { object, prop: MemberProp::Name { name: m, .. }, .. } => {
|
|
6539
|
+
matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
|
|
6540
|
+
&& matches!(
|
|
6541
|
+
m.as_ref(),
|
|
6542
|
+
"sqrt" | "sin" | "cos" | "tan" | "abs" | "floor" | "ceil" | "exp"
|
|
6543
|
+
| "trunc" | "log"
|
|
6544
|
+
)
|
|
6545
|
+
}
|
|
6546
|
+
_ => false,
|
|
6547
|
+
},
|
|
6548
|
+
_ => false,
|
|
6549
|
+
}
|
|
6550
|
+
}
|
|
6551
|
+
|
|
6552
|
+
/// Emit `fn name_native(p: f64, ...) -> f64 { ... }` (top level) for each eligible fn.
|
|
6553
|
+
fn emit_native_fns(&mut self, statements: &[Statement]) -> Result<(), CompileError> {
|
|
6554
|
+
for s in statements {
|
|
6555
|
+
if let Statement::FunDecl { name, params, body, .. } = s {
|
|
6556
|
+
if !self.native_fns.contains(name.as_ref()) {
|
|
6557
|
+
continue;
|
|
6558
|
+
}
|
|
6559
|
+
let plist: Vec<String> = params
|
|
6560
|
+
.iter()
|
|
6561
|
+
.filter_map(|p| match p {
|
|
6562
|
+
FunParam::Simple(tp) => {
|
|
6563
|
+
Some(format!("mut {}: f64", Self::escape_ident(tp.name.as_ref())))
|
|
6564
|
+
}
|
|
6565
|
+
_ => None,
|
|
6566
|
+
})
|
|
6567
|
+
.collect();
|
|
6568
|
+
self.type_context.push_scope();
|
|
6569
|
+
for p in params {
|
|
6570
|
+
if let FunParam::Simple(tp) = p {
|
|
6571
|
+
self.type_context.define(tp.name.as_ref(), RustType::F64);
|
|
6572
|
+
}
|
|
6573
|
+
}
|
|
6574
|
+
self.writeln(&format!("fn {}_native({}) -> f64 {{", Self::escape_ident(name.as_ref()), plist.join(", ")));
|
|
6575
|
+
self.indent += 1;
|
|
6576
|
+
self.emit_native_fn_body(body)?;
|
|
6577
|
+
// Functions that fall off the end without returning: JS yields undefined; an
|
|
6578
|
+
// eligible numeric fn shouldn't, but emit a default to keep `-> f64` total.
|
|
6579
|
+
self.writeln("0.0");
|
|
6580
|
+
self.indent -= 1;
|
|
6581
|
+
self.writeln("}");
|
|
6582
|
+
self.type_context.pop_scope();
|
|
6583
|
+
}
|
|
6584
|
+
}
|
|
6585
|
+
Ok(())
|
|
6586
|
+
}
|
|
6587
|
+
|
|
6588
|
+
fn emit_native_fn_body(&mut self, stmt: &Statement) -> Result<(), CompileError> {
|
|
6589
|
+
match stmt {
|
|
6590
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
6591
|
+
for s in statements {
|
|
6592
|
+
self.emit_native_fn_body(s)?;
|
|
6593
|
+
}
|
|
6594
|
+
}
|
|
6595
|
+
Statement::Return { value, .. } => {
|
|
6596
|
+
let e = value.as_ref().expect("eligible return has a value");
|
|
6597
|
+
let (code, ty) = self.emit_typed_expr(e)?;
|
|
6598
|
+
let f = if ty == RustType::F64 {
|
|
6599
|
+
code
|
|
6600
|
+
} else if ty == RustType::Value {
|
|
6601
|
+
RustType::F64.from_value_expr(&code)
|
|
6602
|
+
} else {
|
|
6603
|
+
code
|
|
6604
|
+
};
|
|
6605
|
+
self.writeln(&format!("return {};", f));
|
|
6606
|
+
}
|
|
6607
|
+
Statement::If { cond, then_branch, else_branch, .. } => {
|
|
6608
|
+
let (c, ct) = self.emit_typed_expr(cond)?;
|
|
6609
|
+
let c_bool = match ct {
|
|
6610
|
+
RustType::Bool => c,
|
|
6611
|
+
RustType::F64 => format!("({} != 0.0)", c),
|
|
6612
|
+
_ => format!("{}.is_truthy()", c),
|
|
6613
|
+
};
|
|
6614
|
+
self.writeln(&format!("if {} {{", c_bool));
|
|
6615
|
+
self.indent += 1;
|
|
6616
|
+
self.emit_native_fn_body(then_branch)?;
|
|
6617
|
+
self.indent -= 1;
|
|
6618
|
+
if let Some(eb) = else_branch {
|
|
6619
|
+
self.writeln("} else {");
|
|
6620
|
+
self.indent += 1;
|
|
6621
|
+
self.emit_native_fn_body(eb)?;
|
|
6622
|
+
self.indent -= 1;
|
|
6623
|
+
}
|
|
6624
|
+
self.writeln("}");
|
|
6625
|
+
}
|
|
6626
|
+
Statement::ExprStmt { expr, .. } => {
|
|
6627
|
+
let (code, _) = self.emit_typed_expr(expr)?;
|
|
6628
|
+
self.writeln(&format!("{};", code));
|
|
6629
|
+
}
|
|
6630
|
+
_ => unreachable!("emit_native_fn_body: eligibility guarantees only handled statements"),
|
|
6631
|
+
}
|
|
6632
|
+
Ok(())
|
|
6633
|
+
}
|
|
6634
|
+
|
|
6635
|
+
fn emit_typed_expr(&mut self, expr: &Expr) -> Result<(String, RustType), CompileError> {
|
|
6636
|
+
match expr {
|
|
6637
|
+
// ── literals ─────────────────────────────────────────────────────────
|
|
6638
|
+
Expr::Literal { value, .. } => match value {
|
|
6639
|
+
Literal::Number(n) => Ok((Self::f64_lit(*n), RustType::F64)),
|
|
6640
|
+
Literal::String(s) => {
|
|
6641
|
+
Ok((format!("{:?}.to_string()", s.as_ref()), RustType::String))
|
|
6642
|
+
}
|
|
6643
|
+
Literal::Bool(b) => Ok((format!("{}", b), RustType::Bool)),
|
|
6644
|
+
Literal::Null => Ok(("Value::Null".to_string(), RustType::Value)),
|
|
6645
|
+
},
|
|
6646
|
+
|
|
6647
|
+
// ── identifiers ──────────────────────────────────────────────────────
|
|
6648
|
+
Expr::Ident { name, .. } => {
|
|
6649
|
+
let escaped = Self::escape_ident(name.as_ref());
|
|
6650
|
+
if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
6651
|
+
let var_type = self.type_context.get_type(name.as_ref());
|
|
6652
|
+
if var_type.is_native() {
|
|
6653
|
+
Ok((format!("(*{}.borrow()).clone()", escaped), var_type))
|
|
6654
|
+
} else {
|
|
6655
|
+
Ok((format!("(*{}.borrow()).clone()", escaped), RustType::Value))
|
|
6656
|
+
}
|
|
6657
|
+
} else {
|
|
6658
|
+
let var_type = self.type_context.get_type(name.as_ref());
|
|
6659
|
+
if var_type.is_native() {
|
|
6660
|
+
Ok((escaped.into_owned(), var_type))
|
|
6661
|
+
} else {
|
|
6662
|
+
Ok((escaped.into_owned(), RustType::Value))
|
|
6663
|
+
}
|
|
6664
|
+
}
|
|
6665
|
+
}
|
|
6666
|
+
|
|
6667
|
+
// ── binary expressions ───────────────────────────────────────────────
|
|
6668
|
+
Expr::Binary {
|
|
6669
|
+
left,
|
|
6670
|
+
op,
|
|
6671
|
+
right,
|
|
6672
|
+
span,
|
|
6673
|
+
..
|
|
6674
|
+
} => {
|
|
6675
|
+
let (l, lt) = self.emit_typed_expr(left)?;
|
|
6676
|
+
let (r, rt) = self.emit_typed_expr(right)?;
|
|
6677
|
+
|
|
6678
|
+
if let Some(result_ty) = RustType::result_type_of_binop(*op, <, &rt) {
|
|
6679
|
+
// Both sides are compatible native types → emit native op.
|
|
6680
|
+
let code = match op {
|
|
6681
|
+
BinOp::Add if result_ty == RustType::String => {
|
|
6682
|
+
// M2: Rust `String + String` is illegal; build a fresh String.
|
|
6683
|
+
// `format!` borrows both operands, so chained concats (`a + b + c`)
|
|
6684
|
+
// nest cleanly with no move/clone hazards.
|
|
6685
|
+
format!("format!(\"{{}}{{}}\", {}, {})", l, r)
|
|
6686
|
+
}
|
|
6687
|
+
BinOp::Add => format!("({} + {})", l, r),
|
|
6688
|
+
BinOp::Sub => format!("({} - {})", l, r),
|
|
6689
|
+
BinOp::Mul => format!("({} * {})", l, r),
|
|
6690
|
+
BinOp::Div => format!("({} / {})", l, r),
|
|
6691
|
+
BinOp::Mod => format!("({} % {})", l, r),
|
|
6692
|
+
BinOp::Pow => format!("({}).powf({})", l, r),
|
|
6693
|
+
BinOp::Lt => format!("({} < {})", l, r),
|
|
6694
|
+
BinOp::Le => format!("({} <= {})", l, r),
|
|
6695
|
+
BinOp::Gt => format!("({} > {})", l, r),
|
|
6696
|
+
BinOp::Ge => format!("({} >= {})", l, r),
|
|
6697
|
+
BinOp::StrictEq => format!("({} == {})", l, r),
|
|
6698
|
+
BinOp::StrictNe => format!("({} != {})", l, r),
|
|
6699
|
+
BinOp::And => format!("({} && {})", l, r),
|
|
6700
|
+
BinOp::Or => format!("({} || {})", l, r),
|
|
6701
|
+
// Native int32 bitwise/shift (operands are f64 here). `to_int32`/`to_uint32`
|
|
6702
|
+
// is JS ToInt32/ToUint32 (modulo 2³², NaN/±Infinity → 0; `#[inline]` so the
|
|
6703
|
+
// `is_finite` guard folds away on the hot finite path); shift counts mask to
|
|
6704
|
+
// 5 bits via `wrapping_sh*` (JS semantics, no panic).
|
|
6705
|
+
BinOp::BitAnd => format!(
|
|
6706
|
+
"((tishlang_runtime::to_int32({}) & tishlang_runtime::to_int32({})) as f64)",
|
|
6707
|
+
l, r
|
|
6708
|
+
),
|
|
6709
|
+
BinOp::BitOr => format!(
|
|
6710
|
+
"((tishlang_runtime::to_int32({}) | tishlang_runtime::to_int32({})) as f64)",
|
|
6711
|
+
l, r
|
|
6712
|
+
),
|
|
6713
|
+
BinOp::BitXor => format!(
|
|
6714
|
+
"((tishlang_runtime::to_int32({}) ^ tishlang_runtime::to_int32({})) as f64)",
|
|
6715
|
+
l, r
|
|
6716
|
+
),
|
|
6717
|
+
BinOp::Shl => format!(
|
|
6718
|
+
"(tishlang_runtime::to_int32({}).wrapping_shl(tishlang_runtime::to_uint32({})) as f64)",
|
|
6719
|
+
l, r
|
|
6720
|
+
),
|
|
6721
|
+
BinOp::Shr => format!(
|
|
6722
|
+
"(tishlang_runtime::to_int32({}).wrapping_shr(tishlang_runtime::to_uint32({})) as f64)",
|
|
6723
|
+
l, r
|
|
6724
|
+
),
|
|
6725
|
+
BinOp::UShr => format!(
|
|
6726
|
+
"(tishlang_runtime::to_uint32({}).wrapping_shr(tishlang_runtime::to_uint32({})) as f64)",
|
|
6727
|
+
l, r
|
|
6728
|
+
),
|
|
6729
|
+
_ => unreachable!("result_type_of_binop covers all handled ops"),
|
|
6730
|
+
};
|
|
6731
|
+
return Ok((code, result_ty));
|
|
6732
|
+
}
|
|
6733
|
+
|
|
6734
|
+
// Fall back: convert both sides to Value and use the runtime.
|
|
6735
|
+
let lv = if lt.is_native() {
|
|
6736
|
+
lt.to_value_expr(&l)
|
|
6737
|
+
} else {
|
|
6738
|
+
l
|
|
6739
|
+
};
|
|
6740
|
+
let rv = if rt.is_native() {
|
|
6741
|
+
rt.to_value_expr(&r)
|
|
6742
|
+
} else {
|
|
6743
|
+
r
|
|
6744
|
+
};
|
|
6745
|
+
let result = self.emit_binop(&lv, *op, &rv, *span)?;
|
|
6746
|
+
Ok((result, RustType::Value))
|
|
6747
|
+
}
|
|
6748
|
+
|
|
6749
|
+
// ── array indexing ───────────────────────────────────────────────────
|
|
6750
|
+
Expr::Index {
|
|
6751
|
+
object,
|
|
6752
|
+
index,
|
|
6753
|
+
optional,
|
|
6754
|
+
..
|
|
6755
|
+
} => {
|
|
6756
|
+
// Native fast path: `vec[i]` where vec is Vec<T> and i is numeric.
|
|
6757
|
+
if !optional {
|
|
5217
6758
|
if let Expr::Ident { name, .. } = object.as_ref() {
|
|
5218
6759
|
if !self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
5219
6760
|
let obj_type = self.type_context.get_type(name.as_ref());
|
|
@@ -5234,7 +6775,37 @@ impl Codegen {
|
|
|
5234
6775
|
)
|
|
5235
6776
|
};
|
|
5236
6777
|
let elem_ty = *elem_type.clone();
|
|
5237
|
-
|
|
6778
|
+
// OOB-safe read for numeric/bool Vecs: JS `arr[oob]` is `undefined`
|
|
6779
|
+
// (→ NaN / false in those contexts), NOT a panic. In-bounds is the
|
|
6780
|
+
// same bounds-checked access, so this is purely a correctness gain
|
|
6781
|
+
// (and what lets index reads be *inferred* as native — phase 2).
|
|
6782
|
+
let access = match &elem_ty {
|
|
6783
|
+
RustType::F64 => format!(
|
|
6784
|
+
"{}.get({}).copied().unwrap_or(f64::NAN)",
|
|
6785
|
+
esc_obj, idx_usize
|
|
6786
|
+
),
|
|
6787
|
+
RustType::Bool => {
|
|
6788
|
+
format!("{}.get({}).copied().unwrap_or(false)", esc_obj, idx_usize)
|
|
6789
|
+
}
|
|
6790
|
+
// Other element types keep the direct index (unchanged).
|
|
6791
|
+
_ => format!("{}[{}]", esc_obj, idx_usize),
|
|
6792
|
+
};
|
|
6793
|
+
return Ok((access, elem_ty));
|
|
6794
|
+
}
|
|
6795
|
+
// Native tuple access: `tuple[const]` -> `tuple.const` (Rust tuples
|
|
6796
|
+
// require a literal index; a variable index falls through to boxed).
|
|
6797
|
+
if let RustType::Tuple(elems) = &obj_type {
|
|
6798
|
+
if let Expr::Literal {
|
|
6799
|
+
value: Literal::Number(n),
|
|
6800
|
+
..
|
|
6801
|
+
} = index.as_ref()
|
|
6802
|
+
{
|
|
6803
|
+
let i = *n as usize;
|
|
6804
|
+
if n.fract() == 0.0 && i < elems.len() {
|
|
6805
|
+
let esc_obj = Self::escape_ident(name.as_ref()).into_owned();
|
|
6806
|
+
return Ok((format!("{}.{}", esc_obj, i), elems[i].clone()));
|
|
6807
|
+
}
|
|
6808
|
+
}
|
|
5238
6809
|
}
|
|
5239
6810
|
}
|
|
5240
6811
|
}
|
|
@@ -5255,6 +6826,132 @@ impl Codegen {
|
|
|
5255
6826
|
Ok((result, RustType::Value))
|
|
5256
6827
|
}
|
|
5257
6828
|
|
|
6829
|
+
// ── native Math intrinsics ───────────────────────────────────────────
|
|
6830
|
+
// `Math.sqrt(x)` etc. with a native-f64 arg lowers to a direct f64 method,
|
|
6831
|
+
// skipping the boxed value_call per element. Only methods whose Rust f64 op
|
|
6832
|
+
// matches JS semantics (round half-up & sign(0) differ → left to the runtime).
|
|
6833
|
+
Expr::Call { callee, args, .. } => {
|
|
6834
|
+
// M5: direct call to an eligible native fn -> `name_native(<native args>)`.
|
|
6835
|
+
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
6836
|
+
if self.native_fns.contains(fname.as_ref()) {
|
|
6837
|
+
let mut argc: Vec<String> = Vec::with_capacity(args.len());
|
|
6838
|
+
let mut ok = true;
|
|
6839
|
+
for a in args {
|
|
6840
|
+
if let CallArg::Expr(e) = a {
|
|
6841
|
+
let (ac, at) = self.emit_typed_expr(e)?;
|
|
6842
|
+
argc.push(if at == RustType::Value {
|
|
6843
|
+
RustType::F64.from_value_expr(&ac)
|
|
6844
|
+
} else {
|
|
6845
|
+
ac
|
|
6846
|
+
});
|
|
6847
|
+
} else {
|
|
6848
|
+
ok = false;
|
|
6849
|
+
break;
|
|
6850
|
+
}
|
|
6851
|
+
}
|
|
6852
|
+
if ok {
|
|
6853
|
+
return Ok((
|
|
6854
|
+
format!(
|
|
6855
|
+
"{}_native({})",
|
|
6856
|
+
Self::escape_ident(fname.as_ref()),
|
|
6857
|
+
argc.join(", ")
|
|
6858
|
+
),
|
|
6859
|
+
RustType::F64,
|
|
6860
|
+
));
|
|
6861
|
+
}
|
|
6862
|
+
}
|
|
6863
|
+
}
|
|
6864
|
+
if let [CallArg::Expr(arg_expr)] = args.as_slice() {
|
|
6865
|
+
if let Expr::Member {
|
|
6866
|
+
object,
|
|
6867
|
+
prop: MemberProp::Name { name: method, .. },
|
|
6868
|
+
..
|
|
6869
|
+
} = callee.as_ref()
|
|
6870
|
+
{
|
|
6871
|
+
if matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
|
|
6872
|
+
{
|
|
6873
|
+
let rust_m = match method.as_ref() {
|
|
6874
|
+
"sqrt" => Some("sqrt"),
|
|
6875
|
+
"sin" => Some("sin"),
|
|
6876
|
+
"cos" => Some("cos"),
|
|
6877
|
+
"tan" => Some("tan"),
|
|
6878
|
+
"abs" => Some("abs"),
|
|
6879
|
+
"floor" => Some("floor"),
|
|
6880
|
+
"ceil" => Some("ceil"),
|
|
6881
|
+
"exp" => Some("exp"),
|
|
6882
|
+
"trunc" => Some("trunc"),
|
|
6883
|
+
"log" => Some("ln"),
|
|
6884
|
+
"sinh" => Some("sinh"),
|
|
6885
|
+
"cosh" => Some("cosh"),
|
|
6886
|
+
"tanh" => Some("tanh"),
|
|
6887
|
+
"asinh" => Some("asinh"),
|
|
6888
|
+
"acosh" => Some("acosh"),
|
|
6889
|
+
"atanh" => Some("atanh"),
|
|
6890
|
+
"cbrt" => Some("cbrt"),
|
|
6891
|
+
"log2" => Some("log2"),
|
|
6892
|
+
"log10" => Some("log10"),
|
|
6893
|
+
_ => None,
|
|
6894
|
+
};
|
|
6895
|
+
if let Some(m) = rust_m {
|
|
6896
|
+
let (arg_code, arg_ty) = self.emit_typed_expr(arg_expr)?;
|
|
6897
|
+
let arg_f64 = if arg_ty == RustType::F64 {
|
|
6898
|
+
arg_code
|
|
6899
|
+
} else if arg_ty == RustType::Value {
|
|
6900
|
+
RustType::F64.from_value_expr(&arg_code)
|
|
6901
|
+
} else {
|
|
6902
|
+
arg_code
|
|
6903
|
+
};
|
|
6904
|
+
return Ok((format!("({}).{}()", arg_f64, m), RustType::F64));
|
|
6905
|
+
}
|
|
6906
|
+
}
|
|
6907
|
+
}
|
|
6908
|
+
}
|
|
6909
|
+
// Native typed-array HOFs over a `Vec<f64>` receiver (TISH_NATIVE_HOF):
|
|
6910
|
+
// `xs.reduce/map/filter/some/every(<arrow>)` → a direct Rust iterator chain,
|
|
6911
|
+
// eliminating the per-element `value_call` and all `Value` boxing.
|
|
6912
|
+
if let Some(res) = self.native_vec_hof_for_call(callee, args)? {
|
|
6913
|
+
return Ok(res);
|
|
6914
|
+
}
|
|
6915
|
+
let result = self.emit_expr(expr)?;
|
|
6916
|
+
Ok((result, RustType::Value))
|
|
6917
|
+
}
|
|
6918
|
+
|
|
6919
|
+
// ── native struct field access ───────────────────────────────────────
|
|
6920
|
+
// `o.x` where `o` is a `RustType::Named` struct local and `x` is a native
|
|
6921
|
+
// (f64/bool/string) field → a direct Rust field read with that native type,
|
|
6922
|
+
// instead of boxing it through `Value::Number(o.x)`. This keeps `sum + o.x + o.y`
|
|
6923
|
+
// entirely native (the object_sum hot loop) — see emit_expr's struct fast path,
|
|
6924
|
+
// which returns the SAME access but wrapped in `Value::*` for the dynamic callers.
|
|
6925
|
+
Expr::Member {
|
|
6926
|
+
object,
|
|
6927
|
+
prop: MemberProp::Name { name: prop_name, .. },
|
|
6928
|
+
optional: false,
|
|
6929
|
+
..
|
|
6930
|
+
} => {
|
|
6931
|
+
if let Expr::Ident { name: var_name, .. } = object.as_ref() {
|
|
6932
|
+
let var_type = self.type_context.get_type(var_name.as_ref());
|
|
6933
|
+
if let RustType::Named { fields, .. } = &var_type {
|
|
6934
|
+
if let Some((_, field_ty)) =
|
|
6935
|
+
fields.iter().find(|(k, _)| k.as_ref() == prop_name.as_ref())
|
|
6936
|
+
{
|
|
6937
|
+
if field_ty.is_native() {
|
|
6938
|
+
let var_esc = Self::escape_ident(var_name.as_ref()).into_owned();
|
|
6939
|
+
let field = crate::types::field_ident(prop_name.as_ref());
|
|
6940
|
+
let access = if self.refcell_wrapped_vars.contains(var_name.as_ref())
|
|
6941
|
+
{
|
|
6942
|
+
format!("(*{}.borrow()).{}.clone()", var_esc, field)
|
|
6943
|
+
} else {
|
|
6944
|
+
format!("{}.{}", var_esc, field)
|
|
6945
|
+
};
|
|
6946
|
+
return Ok((access, field_ty.clone()));
|
|
6947
|
+
}
|
|
6948
|
+
}
|
|
6949
|
+
}
|
|
6950
|
+
}
|
|
6951
|
+
let result = self.emit_expr(expr)?;
|
|
6952
|
+
Ok((result, RustType::Value))
|
|
6953
|
+
}
|
|
6954
|
+
|
|
5258
6955
|
// ── everything else: delegate to emit_expr ───────────────────────────
|
|
5259
6956
|
_ => {
|
|
5260
6957
|
let result = self.emit_expr(expr)?;
|
|
@@ -5280,6 +6977,293 @@ impl Codegen {
|
|
|
5280
6977
|
}
|
|
5281
6978
|
}
|
|
5282
6979
|
|
|
6980
|
+
/// Fused `reduce`: if the callback is exactly `(acc, x) => acc OP x` (or `x OP acc`) with a
|
|
6981
|
+
/// plain binop of the two params, emit a native fold over the array using the SAME runtime
|
|
6982
|
+
/// Value op the closure body would — eliminating the per-element `value_call`. Sound (identical
|
|
6983
|
+
/// Value semantics, including string `+`). Returns `None` to fall back to `array_reduce`.
|
|
6984
|
+
fn try_fused_reduce(
|
|
6985
|
+
&self,
|
|
6986
|
+
args: &[CallArg],
|
|
6987
|
+
obj_expr: &str,
|
|
6988
|
+
initial: &str,
|
|
6989
|
+
) -> Result<Option<String>, CompileError> {
|
|
6990
|
+
let Some(CallArg::Expr(Expr::ArrowFunction { params, body, .. })) = args.first() else {
|
|
6991
|
+
return Ok(None);
|
|
6992
|
+
};
|
|
6993
|
+
let tishlang_ast::ArrowBody::Expr(be) = body else {
|
|
6994
|
+
return Ok(None);
|
|
6995
|
+
};
|
|
6996
|
+
if params.len() != 2 {
|
|
6997
|
+
return Ok(None);
|
|
6998
|
+
}
|
|
6999
|
+
let pname = |p: &FunParam| -> Option<std::sync::Arc<str>> {
|
|
7000
|
+
match p {
|
|
7001
|
+
FunParam::Simple(tp) if tp.default.is_none() => Some(std::sync::Arc::clone(&tp.name)),
|
|
7002
|
+
_ => None,
|
|
7003
|
+
}
|
|
7004
|
+
};
|
|
7005
|
+
let (Some(acc), Some(cur)) = (pname(¶ms[0]), pname(¶ms[1])) else {
|
|
7006
|
+
return Ok(None);
|
|
7007
|
+
};
|
|
7008
|
+
let Expr::Binary {
|
|
7009
|
+
left, op, right, span,
|
|
7010
|
+
} = be.as_ref()
|
|
7011
|
+
else {
|
|
7012
|
+
return Ok(None);
|
|
7013
|
+
};
|
|
7014
|
+
let ident = |e: &Expr| -> Option<std::sync::Arc<str>> {
|
|
7015
|
+
match e {
|
|
7016
|
+
Expr::Ident { name, .. } => Some(std::sync::Arc::clone(name)),
|
|
7017
|
+
_ => None,
|
|
7018
|
+
}
|
|
7019
|
+
};
|
|
7020
|
+
let (Some(ln), Some(rn)) = (ident(left), ident(right)) else {
|
|
7021
|
+
return Ok(None);
|
|
7022
|
+
};
|
|
7023
|
+
// Map each operand to `_acc` / `_x` in the body's actual order.
|
|
7024
|
+
let (ls, rs) = if ln.as_ref() == acc.as_ref() && rn.as_ref() == cur.as_ref() {
|
|
7025
|
+
("_acc", "_x")
|
|
7026
|
+
} else if ln.as_ref() == cur.as_ref() && rn.as_ref() == acc.as_ref() {
|
|
7027
|
+
("_x", "_acc")
|
|
7028
|
+
} else {
|
|
7029
|
+
return Ok(None);
|
|
7030
|
+
};
|
|
7031
|
+
let body_code = self.emit_binop(ls, *op, rs, *span)?;
|
|
7032
|
+
|
|
7033
|
+
// Native-f64 fast path for arithmetic reducers in the standard `acc OP x` order. We can't
|
|
7034
|
+
// assume the array is numeric at compile time (`+` concatenates strings in JS), so emit a
|
|
7035
|
+
// runtime all-numeric guard: if the init and every element are `Value::Number`, fold in raw
|
|
7036
|
+
// f64 (no per-element `ops::add` call, no Result, no re-boxing); otherwise fall back to the
|
|
7037
|
+
// boxed fold from the original init — identical semantics either way. This is the array_hof
|
|
7038
|
+
// hot loop; a fully-unboxed `Vec<f64>` (packed arrays / task #13) would go further.
|
|
7039
|
+
let nat_op = if (ls, rs) == ("_acc", "_x") {
|
|
7040
|
+
match op {
|
|
7041
|
+
BinOp::Add => Some("+="),
|
|
7042
|
+
BinOp::Sub => Some("-="),
|
|
7043
|
+
BinOp::Mul => Some("*="),
|
|
7044
|
+
BinOp::Div => Some("/="),
|
|
7045
|
+
_ => None,
|
|
7046
|
+
}
|
|
7047
|
+
} else {
|
|
7048
|
+
None
|
|
7049
|
+
};
|
|
7050
|
+
if let Some(nat_op) = nat_op {
|
|
7051
|
+
return Ok(Some(format!(
|
|
7052
|
+
"{{ let _init0 = {init}; let _arr = ({obj}).clone(); \
|
|
7053
|
+
if let Value::Array(ref _a) = _arr {{ let _b = _a.borrow(); \
|
|
7054
|
+
let mut _accn: f64 = 0.0; let mut _ok = false; \
|
|
7055
|
+
if let Value::Number(_i0) = &_init0 {{ _accn = *_i0; _ok = true; }} \
|
|
7056
|
+
if _ok {{ for _el in _b.iter() {{ \
|
|
7057
|
+
if let Value::Number(_n) = _el {{ _accn {nat_op} *_n; }} else {{ _ok = false; break; }} }} }} \
|
|
7058
|
+
if _ok {{ Value::Number(_accn) }} \
|
|
7059
|
+
else {{ let mut _acc = _init0; for _el in _b.iter() {{ let _x = _el.clone(); _acc = {body}; }} _acc }} \
|
|
7060
|
+
}} else {{ _init0 }} }}",
|
|
7061
|
+
init = initial,
|
|
7062
|
+
obj = obj_expr,
|
|
7063
|
+
nat_op = nat_op,
|
|
7064
|
+
body = body_code
|
|
7065
|
+
)));
|
|
7066
|
+
}
|
|
7067
|
+
|
|
7068
|
+
Ok(Some(format!(
|
|
7069
|
+
"{{ let mut _acc = {init}; let _arr = ({obj}).clone(); \
|
|
7070
|
+
if let Value::Array(ref _a) = _arr {{ for _el in _a.borrow().iter() {{ \
|
|
7071
|
+
let _x = _el.clone(); _acc = {body}; }} }} _acc }}",
|
|
7072
|
+
init = initial,
|
|
7073
|
+
obj = obj_expr,
|
|
7074
|
+
body = body_code
|
|
7075
|
+
)))
|
|
7076
|
+
}
|
|
7077
|
+
|
|
7078
|
+
/// If `callee(args)` is `<Vec<f64>-ident>.reduce/map/filter/some/every(<arrow>)` and the
|
|
7079
|
+
/// `TISH_NATIVE_HOF` flag is set, lower it to a native iterator chain. Shared by
|
|
7080
|
+
/// `emit_typed_expr` (native sub-expressions) and `emit_native_expr` (typed `let` RHS), so a
|
|
7081
|
+
/// typed-array HOF lowers natively whether its result flows into arithmetic or a binding.
|
|
7082
|
+
fn native_vec_hof_for_call(
|
|
7083
|
+
&mut self,
|
|
7084
|
+
callee: &Expr,
|
|
7085
|
+
args: &[CallArg],
|
|
7086
|
+
) -> Result<Option<(String, RustType)>, CompileError> {
|
|
7087
|
+
if std::env::var("TISH_NATIVE_HOF").is_err() {
|
|
7088
|
+
return Ok(None);
|
|
7089
|
+
}
|
|
7090
|
+
let Expr::Member {
|
|
7091
|
+
object,
|
|
7092
|
+
prop: MemberProp::Name { name: method, .. },
|
|
7093
|
+
optional: false,
|
|
7094
|
+
..
|
|
7095
|
+
} = callee
|
|
7096
|
+
else {
|
|
7097
|
+
return Ok(None);
|
|
7098
|
+
};
|
|
7099
|
+
let Expr::Ident { name: recv_name, .. } = object.as_ref() else {
|
|
7100
|
+
return Ok(None);
|
|
7101
|
+
};
|
|
7102
|
+
// A RefCell-wrapped receiver would need a borrow to iterate — bail to the boxed path.
|
|
7103
|
+
if self.refcell_wrapped_vars.contains(recv_name.as_ref()) {
|
|
7104
|
+
return Ok(None);
|
|
7105
|
+
}
|
|
7106
|
+
let RustType::Vec(inner) = self.type_context.get_type(recv_name.as_ref()) else {
|
|
7107
|
+
return Ok(None);
|
|
7108
|
+
};
|
|
7109
|
+
if *inner != RustType::F64 {
|
|
7110
|
+
return Ok(None);
|
|
7111
|
+
}
|
|
7112
|
+
let recv_code = Self::escape_ident(recv_name.as_ref()).into_owned();
|
|
7113
|
+
self.try_native_vec_hof(&recv_code, &inner, recv_name.as_ref(), method.as_ref(), args)
|
|
7114
|
+
}
|
|
7115
|
+
|
|
7116
|
+
/// Native typed-array HOFs (`TISH_NATIVE_HOF`): when the receiver is a native `Vec<f64>`
|
|
7117
|
+
/// (a typed `number[]`), lower `reduce`/`map`/`filter`/`some`/`every` to a direct Rust
|
|
7118
|
+
/// iterator chain — no per-element `value_call`, no `Value` boxing.
|
|
7119
|
+
///
|
|
7120
|
+
/// Preconditions (any failure → `Ok(None)` → boxed `array_*`, correctness over coverage):
|
|
7121
|
+
/// - element type is `f64` (Copy → `.copied()`),
|
|
7122
|
+
/// - the callback is an arrow with simple, no-default params and an **expression** body,
|
|
7123
|
+
/// - the body does **not** mention the receiver — `pi_mentions` is conservative (unknown
|
|
7124
|
+
/// AST nodes count as "mentions"), so a `&mut`/alias of the array inside the closure can't
|
|
7125
|
+
/// slip through and break the `.iter()` borrow,
|
|
7126
|
+
/// - the body's emitted native type matches what the method needs (`f64` for `reduce`/`map`
|
|
7127
|
+
/// element, `bool` for `filter`/`some`/`every`).
|
|
7128
|
+
///
|
|
7129
|
+
/// The closure params are bound natively (`f64`) only while the body is emitted, then popped.
|
|
7130
|
+
fn try_native_vec_hof(
|
|
7131
|
+
&mut self,
|
|
7132
|
+
recv: &str,
|
|
7133
|
+
elem: &RustType,
|
|
7134
|
+
recv_name: &str,
|
|
7135
|
+
method: &str,
|
|
7136
|
+
args: &[CallArg],
|
|
7137
|
+
) -> Result<Option<(String, RustType)>, CompileError> {
|
|
7138
|
+
// Only numeric arrays for now: `.copied()` needs a `Copy` element.
|
|
7139
|
+
if *elem != RustType::F64 {
|
|
7140
|
+
return Ok(None);
|
|
7141
|
+
}
|
|
7142
|
+
let Some(CallArg::Expr(Expr::ArrowFunction { params, body, .. })) = args.first() else {
|
|
7143
|
+
return Ok(None);
|
|
7144
|
+
};
|
|
7145
|
+
let tishlang_ast::ArrowBody::Expr(be) = body else {
|
|
7146
|
+
return Ok(None);
|
|
7147
|
+
};
|
|
7148
|
+
// The body must not touch the receiver (aliasing would break the `.iter()` borrow).
|
|
7149
|
+
if crate::infer::pi_mentions(be, recv_name) {
|
|
7150
|
+
return Ok(None);
|
|
7151
|
+
}
|
|
7152
|
+
let simple_name = |p: &FunParam| -> Option<std::sync::Arc<str>> {
|
|
7153
|
+
match p {
|
|
7154
|
+
FunParam::Simple(tp) if tp.default.is_none() => Some(std::sync::Arc::clone(&tp.name)),
|
|
7155
|
+
_ => None,
|
|
7156
|
+
}
|
|
7157
|
+
};
|
|
7158
|
+
// Emit `be` with `binds` (name, type) installed as native locals; restore on the way out.
|
|
7159
|
+
let emit_with = |this: &mut Self,
|
|
7160
|
+
binds: &[(&std::sync::Arc<str>, RustType)]|
|
|
7161
|
+
-> Result<(String, RustType), CompileError> {
|
|
7162
|
+
this.type_context.push_scope();
|
|
7163
|
+
for (n, t) in binds {
|
|
7164
|
+
this.type_context.define(n.as_ref(), t.clone());
|
|
7165
|
+
}
|
|
7166
|
+
let res = this.emit_typed_expr(be);
|
|
7167
|
+
this.type_context.pop_scope();
|
|
7168
|
+
res
|
|
7169
|
+
};
|
|
7170
|
+
match method {
|
|
7171
|
+
"reduce" => {
|
|
7172
|
+
if args.len() != 2 || params.len() != 2 {
|
|
7173
|
+
return Ok(None);
|
|
7174
|
+
}
|
|
7175
|
+
let (Some(acc), Some(x)) = (simple_name(¶ms[0]), simple_name(¶ms[1])) else {
|
|
7176
|
+
return Ok(None);
|
|
7177
|
+
};
|
|
7178
|
+
let CallArg::Expr(init_e) = &args[1] else {
|
|
7179
|
+
return Ok(None);
|
|
7180
|
+
};
|
|
7181
|
+
let (init_code, init_ty) = self.emit_typed_expr(init_e)?;
|
|
7182
|
+
let init_f64 = match init_ty {
|
|
7183
|
+
RustType::F64 => init_code,
|
|
7184
|
+
RustType::Value => RustType::F64.from_value_expr(&init_code),
|
|
7185
|
+
_ => return Ok(None),
|
|
7186
|
+
};
|
|
7187
|
+
let (body_code, body_ty) =
|
|
7188
|
+
emit_with(self, &[(&acc, RustType::F64), (&x, RustType::F64)])?;
|
|
7189
|
+
if body_ty != RustType::F64 {
|
|
7190
|
+
return Ok(None);
|
|
7191
|
+
}
|
|
7192
|
+
let acc_esc = Self::escape_ident(acc.as_ref()).into_owned();
|
|
7193
|
+
let x_esc = Self::escape_ident(x.as_ref()).into_owned();
|
|
7194
|
+
Ok(Some((
|
|
7195
|
+
format!(
|
|
7196
|
+
"{{ let mut {acc}: f64 = {init}; for {x} in {recv}.iter().copied() {{ {acc} = {body}; }} {acc} }}",
|
|
7197
|
+
acc = acc_esc, init = init_f64, x = x_esc, recv = recv, body = body_code
|
|
7198
|
+
),
|
|
7199
|
+
RustType::F64,
|
|
7200
|
+
)))
|
|
7201
|
+
}
|
|
7202
|
+
"map" => {
|
|
7203
|
+
if args.len() != 1 || params.len() != 1 {
|
|
7204
|
+
return Ok(None);
|
|
7205
|
+
}
|
|
7206
|
+
let Some(x) = simple_name(¶ms[0]) else {
|
|
7207
|
+
return Ok(None);
|
|
7208
|
+
};
|
|
7209
|
+
let (body_code, body_ty) = emit_with(self, &[(&x, RustType::F64)])?;
|
|
7210
|
+
if !body_ty.is_native() {
|
|
7211
|
+
return Ok(None);
|
|
7212
|
+
}
|
|
7213
|
+
let x_esc = Self::escape_ident(x.as_ref()).into_owned();
|
|
7214
|
+
Ok(Some((
|
|
7215
|
+
format!(
|
|
7216
|
+
"{recv}.iter().copied().map(|{x}| {body}).collect::<Vec<{ety}>>()",
|
|
7217
|
+
recv = recv, x = x_esc, body = body_code, ety = body_ty.to_rust_type_str()
|
|
7218
|
+
),
|
|
7219
|
+
RustType::Vec(Box::new(body_ty)),
|
|
7220
|
+
)))
|
|
7221
|
+
}
|
|
7222
|
+
"filter" => {
|
|
7223
|
+
if args.len() != 1 || params.len() != 1 {
|
|
7224
|
+
return Ok(None);
|
|
7225
|
+
}
|
|
7226
|
+
let Some(x) = simple_name(¶ms[0]) else {
|
|
7227
|
+
return Ok(None);
|
|
7228
|
+
};
|
|
7229
|
+
let (body_code, body_ty) = emit_with(self, &[(&x, RustType::F64)])?;
|
|
7230
|
+
if body_ty != RustType::Bool {
|
|
7231
|
+
return Ok(None);
|
|
7232
|
+
}
|
|
7233
|
+
let x_esc = Self::escape_ident(x.as_ref()).into_owned();
|
|
7234
|
+
Ok(Some((
|
|
7235
|
+
format!(
|
|
7236
|
+
"{recv}.iter().copied().filter(|&{x}| {body}).collect::<Vec<f64>>()",
|
|
7237
|
+
recv = recv, x = x_esc, body = body_code
|
|
7238
|
+
),
|
|
7239
|
+
RustType::Vec(Box::new(RustType::F64)),
|
|
7240
|
+
)))
|
|
7241
|
+
}
|
|
7242
|
+
"some" | "every" => {
|
|
7243
|
+
if args.len() != 1 || params.len() != 1 {
|
|
7244
|
+
return Ok(None);
|
|
7245
|
+
}
|
|
7246
|
+
let Some(x) = simple_name(¶ms[0]) else {
|
|
7247
|
+
return Ok(None);
|
|
7248
|
+
};
|
|
7249
|
+
let (body_code, body_ty) = emit_with(self, &[(&x, RustType::F64)])?;
|
|
7250
|
+
if body_ty != RustType::Bool {
|
|
7251
|
+
return Ok(None);
|
|
7252
|
+
}
|
|
7253
|
+
let x_esc = Self::escape_ident(x.as_ref()).into_owned();
|
|
7254
|
+
let adapter = if method == "some" { "any" } else { "all" };
|
|
7255
|
+
Ok(Some((
|
|
7256
|
+
format!(
|
|
7257
|
+
"{recv}.iter().copied().{adapter}(|{x}| {body})",
|
|
7258
|
+
recv = recv, adapter = adapter, x = x_esc, body = body_code
|
|
7259
|
+
),
|
|
7260
|
+
RustType::Bool,
|
|
7261
|
+
)))
|
|
7262
|
+
}
|
|
7263
|
+
_ => Ok(None),
|
|
7264
|
+
}
|
|
7265
|
+
}
|
|
7266
|
+
|
|
5283
7267
|
fn emit_binop(&self, l: &str, op: BinOp, r: &str, span: Span) -> Result<String, CompileError> {
|
|
5284
7268
|
Ok(match op {
|
|
5285
7269
|
BinOp::Add => format!(
|
|
@@ -5318,8 +7302,9 @@ impl Codegen {
|
|
|
5318
7302
|
BinOp::BitAnd => Self::emit_bitwise_binop(l, r, "&"),
|
|
5319
7303
|
BinOp::BitOr => Self::emit_bitwise_binop(l, r, "|"),
|
|
5320
7304
|
BinOp::BitXor => Self::emit_bitwise_binop(l, r, "^"),
|
|
5321
|
-
BinOp::Shl => Self::
|
|
5322
|
-
BinOp::Shr => Self::
|
|
7305
|
+
BinOp::Shl => Self::emit_shift_binop(l, r, "to_int32", "wrapping_shl"),
|
|
7306
|
+
BinOp::Shr => Self::emit_shift_binop(l, r, "to_int32", "wrapping_shr"),
|
|
7307
|
+
BinOp::UShr => Self::emit_shift_binop(l, r, "to_uint32", "wrapping_shr"),
|
|
5323
7308
|
BinOp::In => format!("tish_in_operator(&{}, &{})", l, r),
|
|
5324
7309
|
BinOp::Eq | BinOp::Ne => {
|
|
5325
7310
|
return Err(CompileError::new(
|