@tishlang/tish-format 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tish-format +0 -0
- package/crates/tish/src/cli_help.rs +4 -0
- package/crates/tish/src/main.rs +13 -2
- package/crates/tish/tests/rust_lib_crate.rs +140 -0
- package/crates/tish_compile/src/codegen.rs +1395 -107
- package/crates/tish_compile/src/lib.rs +14 -1
- package/crates/tish_compile/src/types.rs +6 -0
- package/crates/tish_compile/tests/perf_codegen_i32_native_abi.rs +113 -0
- package/crates/tish_compile/tests/regr_andor_rhs_move.rs +85 -0
- package/crates/tish_compile/tests/regr_gba_lowered_call_sites.rs +487 -0
- package/crates/tish_compile/tests/regr_rust_lib_emit.rs +150 -0
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_fmt/src/lib.rs +54 -1
- package/crates/tish_native/src/build.rs +151 -6
- package/crates/tish_native/src/config.rs +14 -0
- package/crates/tish_runtime_gba/src/gba.rs +26 -6
- package/crates/tish_runtime_gba/src/lib.rs +46 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
|
@@ -652,7 +652,7 @@ pub fn compile_project_full_emit(
|
|
|
652
652
|
if let Some(cap) = feature_cap {
|
|
653
653
|
all_features.retain(|f| cap.contains(f));
|
|
654
654
|
}
|
|
655
|
-
let rust =
|
|
655
|
+
let rust = compile_with_native_modules_emit_exports(
|
|
656
656
|
&merged.program,
|
|
657
657
|
project_root,
|
|
658
658
|
&all_features,
|
|
@@ -660,6 +660,7 @@ pub fn compile_project_full_emit(
|
|
|
660
660
|
&native_build.native_init,
|
|
661
661
|
optimize,
|
|
662
662
|
emit_mode,
|
|
663
|
+
&merged.entry_exports,
|
|
663
664
|
)?;
|
|
664
665
|
Ok((rust, native_modules, all_features, native_build))
|
|
665
666
|
}
|
|
@@ -728,6 +729,16 @@ fn run_type_check(program: &Program) -> Result<(), CompileError> {
|
|
|
728
729
|
Ok(())
|
|
729
730
|
}
|
|
730
731
|
|
|
732
|
+
/// One entry-module export, as the `RustLib` surface sees it.
|
|
733
|
+
struct RustLibExport {
|
|
734
|
+
/// The Rust local `run()` binds — for a re-export, the dependency's binding.
|
|
735
|
+
local_name: String,
|
|
736
|
+
/// The name the crate exposes (and the key in the export table).
|
|
737
|
+
exported_name: String,
|
|
738
|
+
/// Parameter count when the export names a `fn`; `None` for a value export.
|
|
739
|
+
arity: Option<usize>,
|
|
740
|
+
}
|
|
741
|
+
|
|
731
742
|
pub fn compile_with_native_modules_emit(
|
|
732
743
|
program: &Program,
|
|
733
744
|
project_root: Option<&Path>,
|
|
@@ -736,6 +747,33 @@ pub fn compile_with_native_modules_emit(
|
|
|
736
747
|
native_init: &std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
|
|
737
748
|
optimize: bool,
|
|
738
749
|
emit_mode: crate::NativeEmitMode,
|
|
750
|
+
) -> Result<String, CompileError> {
|
|
751
|
+
compile_with_native_modules_emit_exports(
|
|
752
|
+
program,
|
|
753
|
+
project_root,
|
|
754
|
+
features,
|
|
755
|
+
native_modules,
|
|
756
|
+
native_init,
|
|
757
|
+
optimize,
|
|
758
|
+
emit_mode,
|
|
759
|
+
&[],
|
|
760
|
+
)
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/// Like [`compile_with_native_modules_emit`], plus the entry module's exported
|
|
764
|
+
/// `(local_name, exported_name)` pairs. Only [`crate::NativeEmitMode::RustLib`] reads them — they
|
|
765
|
+
/// become the emitted crate's `pub fn` surface. Every other mode ignores the argument, which is
|
|
766
|
+
/// why the plain entry point above can pass `&[]`.
|
|
767
|
+
#[allow(clippy::too_many_arguments)]
|
|
768
|
+
pub fn compile_with_native_modules_emit_exports(
|
|
769
|
+
program: &Program,
|
|
770
|
+
project_root: Option<&Path>,
|
|
771
|
+
features: &[String],
|
|
772
|
+
native_modules: &[crate::resolve::ResolvedNativeModule],
|
|
773
|
+
native_init: &std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
|
|
774
|
+
optimize: bool,
|
|
775
|
+
emit_mode: crate::NativeEmitMode,
|
|
776
|
+
entry_exports: &[(String, String)],
|
|
739
777
|
) -> Result<String, CompileError> {
|
|
740
778
|
// Per-compile thread-local state — the narrow-int/`fixed`/`i32` numeric profile and the import-
|
|
741
779
|
// scheme registry — must not outlive THIS compile. Both persist on the thread, so a later compile
|
|
@@ -786,6 +824,7 @@ pub fn compile_with_native_modules_emit(
|
|
|
786
824
|
};
|
|
787
825
|
let mut g = Codegen::new_with_native_modules(project_root, features, map);
|
|
788
826
|
g.emit_mode = emit_mode;
|
|
827
|
+
g.entry_exports = entry_exports.to_vec();
|
|
789
828
|
g.has_native_ui_host = native_modules.iter().any(|m| {
|
|
790
829
|
m.package_name == "tish-macos"
|
|
791
830
|
|| m.package_name == "tish-ios"
|
|
@@ -795,6 +834,7 @@ pub fn compile_with_native_modules_emit(
|
|
|
795
834
|
|| m.crate_name == "tishlang_ios"
|
|
796
835
|
});
|
|
797
836
|
g.emit_program(&program)?;
|
|
837
|
+
g.emit_gba_key_table();
|
|
798
838
|
Ok(g.output)
|
|
799
839
|
}
|
|
800
840
|
|
|
@@ -959,6 +999,10 @@ pub(crate) enum VecRetKind {
|
|
|
959
999
|
F64,
|
|
960
1000
|
Unit,
|
|
961
1001
|
VecF64,
|
|
1002
|
+
/// #605. Every `return` yields a string, so the fn can be a native `-> String` instead of a
|
|
1003
|
+
/// boxed closure. OWNED, not `&str`: a returned string outlives the frame that built it, and a
|
|
1004
|
+
/// borrow would need a lifetime the generated free fn has nowhere to name.
|
|
1005
|
+
Str,
|
|
962
1006
|
}
|
|
963
1007
|
|
|
964
1008
|
#[derive(Debug, Clone)]
|
|
@@ -1081,6 +1125,12 @@ pub(crate) struct Codegen {
|
|
|
1081
1125
|
/// free `fn f_native(f64,..)->f64` (all params `: number`, returns `number`, native-safe
|
|
1082
1126
|
/// body). Direct calls to these route to the native fn, bypassing the boxed `value_call`.
|
|
1083
1127
|
native_fns: std::collections::HashSet<String>,
|
|
1128
|
+
/// Set while emitting the body of an i32-ABI native fn, so `return` coerces to `i32`.
|
|
1129
|
+
native_fn_abi_i32: bool,
|
|
1130
|
+
/// The subset of `native_fns` emitted with an `fn(i32, ..) -> i32` signature instead of the
|
|
1131
|
+
/// f64 one — every parameter and the return annotated `: i32`. On ARM7TDMI this is the whole
|
|
1132
|
+
/// point: an f64 ABI makes every argument and every return a soft-float conversion.
|
|
1133
|
+
native_fns_i32: std::collections::HashSet<String>,
|
|
1084
1134
|
/// True while emitting an M5 `fn name_native` body — keeps VarDecl inits on the native path.
|
|
1085
1135
|
native_fn_body_emit: bool,
|
|
1086
1136
|
/// M5 fn currently being emitted (`mandel_native`, `fastaRandom_native`, …).
|
|
@@ -1170,6 +1220,17 @@ pub(crate) struct Codegen {
|
|
|
1170
1220
|
/// `RustType::Value`. This is the rust-AOT analogue of the VM array-JIT bailing to the
|
|
1171
1221
|
/// interpreter on a non-numeric element. See `collect_demoted_numeric_locals`.
|
|
1172
1222
|
demoted_numeric_locals: std::collections::HashSet<String>,
|
|
1223
|
+
/// Top-level fns whose EVERY return is provably numeric → the native type a call to one yields.
|
|
1224
|
+
///
|
|
1225
|
+
/// Without it a call is opaque, so a local assigned from one demotes to a boxed `Value` and
|
|
1226
|
+
/// every arithmetic consumer falls back to the generic `ops::add` plus two soft-float
|
|
1227
|
+
/// conversions — most of the ~92 ticks a user-fn call costs on GBA
|
|
1228
|
+
/// (`docs/issues/user-fn-call-cost-on-gba.md`).
|
|
1229
|
+
///
|
|
1230
|
+
/// KEYED ON A PROOF, NOT THE ANNOTATION. `function f(): i32 { return "no" }` compiles and
|
|
1231
|
+
/// returns the string — return types are erased — so trusting the declaration would reinterpret
|
|
1232
|
+
/// a `String` as a number.
|
|
1233
|
+
user_fn_ret: std::collections::HashMap<String, RustType>,
|
|
1173
1234
|
/// Integer-range lattice (#174): names of `f64` locals the analysis proves always hold an
|
|
1174
1235
|
/// integer within `[min, max]`, both strictly inside `(-2^53, 2^53)` so `as i64` is exact and
|
|
1175
1236
|
/// `i64` arithmetic is bit-identical to the `f64` the interpreter/VM use. Lets the codegen
|
|
@@ -1245,11 +1306,25 @@ pub(crate) struct Codegen {
|
|
|
1245
1306
|
/// closures it must not return a `Result` from a `Value`-returning closure.
|
|
1246
1307
|
value_fn_depth: u32,
|
|
1247
1308
|
emit_mode: crate::NativeEmitMode,
|
|
1309
|
+
/// `RustLib` only: the entry module's `(local_name, exported_name)` pairs, as computed by
|
|
1310
|
+
/// `merge_modules` for the JS bundle's trailing `export { … }`. Drives the `pub fn` wrappers.
|
|
1311
|
+
/// Empty for every other emit mode.
|
|
1312
|
+
entry_exports: Vec<(String, String)>,
|
|
1248
1313
|
/// Scheme imports as `(scheme_name, absolute_path)`, in first-seen order. Populated (Gba mode)
|
|
1249
1314
|
/// before the entry is emitted from the active [`crate::schemes`] registry; drives the
|
|
1250
1315
|
/// `mod __scheme_<name>_<j> { … }` blocks and their per-scheme registration. The handle a tish
|
|
1251
1316
|
/// name binds to is its index AMONG same-scheme entries (= its slot in that scheme's arena).
|
|
1252
1317
|
scheme_files: Vec<(String, String)>,
|
|
1318
|
+
/// GBA only: the program-wide intern table for OBJECT-LITERAL KEYS, in first-seen order,
|
|
1319
|
+
/// plus the name→index map. `RefCell` so `cached_object_key` can stay `&self`.
|
|
1320
|
+
///
|
|
1321
|
+
/// ⚠️ This is a HEAP fix, not a tidiness one. Off-GBA every key site gets its own `OnceLock`,
|
|
1322
|
+
/// which dedupes repeated evaluation of ONE site but never across sites — and a data table is
|
|
1323
|
+
/// N sibling object literals, so N separate sites each allocate their own copy of every key
|
|
1324
|
+
/// name. A real ROM measured 1,890 key sites over 474 distinct names: 1,416 duplicate `Arc<str>`
|
|
1325
|
+
/// allocations that exist only to spell "name" and "id" again. One table, indexed, kills all of
|
|
1326
|
+
/// them. See `cached_object_key`.
|
|
1327
|
+
gba_keys: std::cell::RefCell<(Vec<String>, std::collections::HashMap<String, usize>)>,
|
|
1253
1328
|
/// Program links `tish:macos` / `tish:ios` — skip HeadlessHost install.
|
|
1254
1329
|
has_native_ui_host: bool,
|
|
1255
1330
|
/// Program references browser global `document` — inject tish-canvas.
|
|
@@ -1314,6 +1389,8 @@ impl Codegen {
|
|
|
1314
1389
|
collection_instance_locals: std::collections::HashSet::new(),
|
|
1315
1390
|
hoisted_string_chars: std::collections::HashMap::new(),
|
|
1316
1391
|
native_fns: std::collections::HashSet::new(),
|
|
1392
|
+
native_fns_i32: std::collections::HashSet::new(),
|
|
1393
|
+
native_fn_abi_i32: false,
|
|
1317
1394
|
native_fn_body_emit: false,
|
|
1318
1395
|
native_fn_emit_name: None,
|
|
1319
1396
|
native_fn_literal_args: std::collections::HashMap::new(),
|
|
@@ -1340,6 +1417,7 @@ impl Codegen {
|
|
|
1340
1417
|
aggregate_array_locals: std::collections::HashSet::new(),
|
|
1341
1418
|
agg_cur_ret: None,
|
|
1342
1419
|
demoted_numeric_locals: std::collections::HashSet::new(),
|
|
1420
|
+
user_fn_ret: std::collections::HashMap::new(),
|
|
1343
1421
|
int_range_locals: std::collections::HashMap::new(),
|
|
1344
1422
|
diag_coord_indices: std::collections::HashSet::new(),
|
|
1345
1423
|
int_i32_vec_locals: std::collections::HashSet::new(),
|
|
@@ -1363,7 +1441,9 @@ impl Codegen {
|
|
|
1363
1441
|
program_fun_decl_names: std::collections::HashSet::new(),
|
|
1364
1442
|
value_fn_depth: 0,
|
|
1365
1443
|
emit_mode: crate::NativeEmitMode::DesktopBin,
|
|
1444
|
+
entry_exports: Vec::new(),
|
|
1366
1445
|
scheme_files: Vec::new(),
|
|
1446
|
+
gba_keys: std::cell::RefCell::new((Vec::new(), std::collections::HashMap::new())),
|
|
1367
1447
|
has_native_ui_host: false,
|
|
1368
1448
|
program_uses_document: false,
|
|
1369
1449
|
cse_subst: Vec::new(),
|
|
@@ -2939,6 +3019,14 @@ impl Codegen {
|
|
|
2939
3019
|
self.write("use std::rc::Rc;\n");
|
|
2940
3020
|
self.write("use std::sync::Arc;\n");
|
|
2941
3021
|
}
|
|
3022
|
+
// RustLib: re-export the runtime prelude rather than importing it privately, so the crate's
|
|
3023
|
+
// public API is self-contained — a consumer gets `Value` (and `ObjectMap` / `PropMap` /
|
|
3024
|
+
// `VmRef`) from THIS crate and cannot version-skew them against a separate `tishlang_runtime`
|
|
3025
|
+
// dependency of their own. Also avoids a duplicate-import error, since `Value` is already in
|
|
3026
|
+
// this list and could not be re-exported a second time at the crate root.
|
|
3027
|
+
if self.emit_mode == crate::NativeEmitMode::RustLib {
|
|
3028
|
+
self.write("pub ");
|
|
3029
|
+
}
|
|
2942
3030
|
self.write("use tishlang_runtime::{console_debug as tish_console_debug, console_info as tish_console_info, console_log as tish_console_log, console_warn as tish_console_warn, console_error as tish_console_error, boolean as tish_boolean, decode_uri as tish_decode_uri, encode_uri as tish_encode_uri, encode_uri_component as tish_encode_uri_component, decode_uri_component as tish_decode_uri_component, string_escape_html_impl as tish_escape_html, in_operator as tish_in_operator, is_finite as tish_is_finite, is_nan as tish_is_nan, json_parse as tish_json_parse, json_stringify as tish_json_stringify, math_abs as tish_math_abs, math_ceil as tish_math_ceil, math_floor as tish_math_floor, math_max as tish_math_max, math_min as tish_math_min, math_round as tish_math_round, math_sqrt as tish_math_sqrt, parse_float as tish_parse_float, parse_int as tish_parse_int, math_random as tish_math_random, math_pow as tish_math_pow, math_sin as tish_math_sin, math_cos as tish_math_cos, math_tan as tish_math_tan, math_log as tish_math_log, math_exp as tish_math_exp, math_sign as tish_math_sign, math_trunc as tish_math_trunc, math_imul as tish_math_imul, math_sinh as tish_math_sinh, math_cosh as tish_math_cosh, math_tanh as tish_math_tanh, math_asinh as tish_math_asinh, math_acosh as tish_math_acosh, math_atanh as tish_math_atanh, math_cbrt as tish_math_cbrt, math_log2 as tish_math_log2, math_log10 as tish_math_log10, math_expm1 as tish_math_expm1, math_log1p as tish_math_log1p, math_clz32 as tish_math_clz32, math_fround as tish_math_fround, math_hypot as tish_math_hypot, math_atan2 as tish_math_atan2, math_asin as tish_math_asin, math_acos as tish_math_acos, math_atan as tish_math_atan, array_is_array as tish_array_is_array, array_of as tish_array_of, array_from as tish_array_from, object_is as tish_object_is, object_has_own as tish_object_has_own, structured_clone as tish_structured_clone, array_construct as tish_array_construct, string_from_char_code as tish_string_from_char_code, string_convert as tish_string_convert, number_convert as tish_number_convert, number_is_integer as tish_number_is_integer, number_is_safe_integer as tish_number_is_safe_integer, number_is_nan as tish_number_is_nan, number_is_finite as tish_number_is_finite, object_assign as tish_object_assign, object_freeze as tish_object_freeze, object_is_frozen as tish_object_is_frozen, object_keys as tish_object_keys, object_values as tish_object_values, object_entries as tish_object_entries, object_from_entries as tish_object_from_entries, symbol_object as tish_symbol_object, tish_construct, tish_error_constructor, tish_date_constructor, tish_set_constructor, tish_map_constructor, map_get as tish_map_get, map_has as tish_map_has, map_set as tish_map_set, map_values as tish_map_values, tish_float64_array_constructor, tish_float32_array_constructor, tish_int8_array_constructor, tish_uint8_array_constructor, tish_uint8_clamped_array_constructor, tish_int16_array_constructor, tish_uint16_array_constructor, tish_int32_array_constructor, tish_uint32_array_constructor, tish_audio_context_constructor, ObjectMap, ObjectData, PropMap, TishError, Value, VmRef};\n");
|
|
2943
3031
|
if self.program_has_jsx {
|
|
2944
3032
|
self.write("use tishlang_ui::{fragment_value, install_thread_local_host, native_create_root, native_use_state, ui_h, ui_text, HeadlessHost};\n");
|
|
@@ -3054,30 +3142,54 @@ impl Codegen {
|
|
|
3054
3142
|
self.writeln("}");
|
|
3055
3143
|
self.writeln("");
|
|
3056
3144
|
}
|
|
3145
|
+
if self.emit_mode == crate::NativeEmitMode::RustLib {
|
|
3146
|
+
self.emit_rust_lib_surface(program)?;
|
|
3147
|
+
}
|
|
3057
3148
|
// M5 (default-on via native_opts_enabled; TISH_NATIVE_OPT=0 opts out): emit a parallel
|
|
3058
3149
|
// native `fn f_native` for each eligible top-level numeric fn at top level; direct calls
|
|
3059
3150
|
// route to it in emit_typed_expr.
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3151
|
+
if crate::native_opts_enabled() {
|
|
3152
|
+
// MODULE STATE GETS A TOP-LEVEL HOME ON GBA TOO (#594). These passes used to be skipped
|
|
3153
|
+
// there because the numeric-global one emits `thread_local!`, which no_std has not got —
|
|
3154
|
+
// and the const-array pass was collateral, gated only because it shared the block. The
|
|
3155
|
+
// consequence was the bug this issue reports: a lowered free `fn` is emitted at top
|
|
3156
|
+
// level, so every module binding it reads is a local of `run()` that does not exist at
|
|
3157
|
+
// that scope, and the emitted crate fails with E0425.
|
|
3158
|
+
//
|
|
3159
|
+
// A const numeric array is `const G_X: [f64; N] = [..]` — no std in it at all. A mutable
|
|
3160
|
+
// numeric global becomes `SingleCore<Cell<f64>>`, the facade's single-core static whose
|
|
3161
|
+
// `with()` deliberately mirrors `LocalKey::with` (compat.rs), so every read and write the
|
|
3162
|
+
// emitter already produces (`G.with(|c| c.get())`) is unchanged.
|
|
3163
|
+
let gba = self.emit_mode == crate::NativeEmitMode::Gba;
|
|
3063
3164
|
self.native_numeric_globals =
|
|
3064
3165
|
Self::collect_native_numeric_globals(&program.statements);
|
|
3065
3166
|
if !self.native_numeric_globals.is_empty() {
|
|
3066
|
-
|
|
3167
|
+
if gba {
|
|
3168
|
+
self.emit_native_numeric_global_singlecore()?;
|
|
3169
|
+
} else {
|
|
3170
|
+
self.emit_native_numeric_global_tls()?;
|
|
3171
|
+
}
|
|
3067
3172
|
self.writeln("");
|
|
3068
3173
|
}
|
|
3069
3174
|
self.module_const_f64_arrays =
|
|
3070
3175
|
Self::collect_module_const_f64_arrays(&program.statements);
|
|
3071
|
-
self.module_const_f64_cum =
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3176
|
+
self.module_const_f64_cum = Self::collect_module_const_cum(
|
|
3177
|
+
&program.statements,
|
|
3178
|
+
&self.module_const_f64_arrays,
|
|
3179
|
+
);
|
|
3180
|
+
self.module_const_f64_aliases = Self::collect_module_const_aliases(
|
|
3181
|
+
&program.statements,
|
|
3182
|
+
&self.module_const_f64_cum,
|
|
3183
|
+
);
|
|
3075
3184
|
if !self.module_const_f64_arrays.is_empty() {
|
|
3076
3185
|
self.emit_module_const_f64_arrays()?;
|
|
3077
3186
|
self.writeln("");
|
|
3078
3187
|
}
|
|
3079
|
-
let native_vec_names: std::collections::HashSet<String> =
|
|
3080
|
-
|
|
3188
|
+
let native_vec_names: std::collections::HashSet<String> = if gba {
|
|
3189
|
+
std::collections::HashSet::new()
|
|
3190
|
+
} else {
|
|
3191
|
+
Self::detect_native_vec_fns(program).keys().cloned().collect()
|
|
3192
|
+
};
|
|
3081
3193
|
let mut global_names: std::collections::HashSet<String> =
|
|
3082
3194
|
self.native_numeric_globals.keys().cloned().collect();
|
|
3083
3195
|
global_names.extend(self.module_const_f64_arrays.keys().cloned());
|
|
@@ -3086,6 +3198,21 @@ impl Codegen {
|
|
|
3086
3198
|
&global_names,
|
|
3087
3199
|
&native_vec_names,
|
|
3088
3200
|
);
|
|
3201
|
+
// Which of the survivors carry the all-`i32` signature. Computed AFTER the fixpoint,
|
|
3202
|
+
// so a function the proof rejected never reaches the i32 ABI either.
|
|
3203
|
+
self.native_fns_i32 = program
|
|
3204
|
+
.statements
|
|
3205
|
+
.iter()
|
|
3206
|
+
.filter_map(|s| match s {
|
|
3207
|
+
Statement::FunDecl { name, params, return_type, .. }
|
|
3208
|
+
if self.native_fns.contains(name.as_ref())
|
|
3209
|
+
&& Self::fn_sig_all_i32(params, return_type) =>
|
|
3210
|
+
{
|
|
3211
|
+
Some(name.to_string())
|
|
3212
|
+
}
|
|
3213
|
+
_ => None,
|
|
3214
|
+
})
|
|
3215
|
+
.collect();
|
|
3089
3216
|
self.native_fn_literal_args =
|
|
3090
3217
|
Self::collect_native_fn_literal_calls(&program.statements, &self.native_fns);
|
|
3091
3218
|
self.native_fn_param_names =
|
|
@@ -3094,11 +3221,17 @@ impl Codegen {
|
|
|
3094
3221
|
Self::collect_native_lcg_fns(&program.statements, &self.native_fns);
|
|
3095
3222
|
// #381 — fns on a call-graph cycle get guarded rotation copies (default ON;
|
|
3096
3223
|
// TISH_NATIVE_RECUR_GUARD=0 restores the plain unguarded emission).
|
|
3097
|
-
self.native_recur_fns = if crate::native_recur_guard_enabled()
|
|
3224
|
+
self.native_recur_fns = if crate::native_recur_guard_enabled()
|
|
3225
|
+
&& self.emit_mode != crate::NativeEmitMode::Gba
|
|
3226
|
+
{
|
|
3098
3227
|
Self::compute_native_recur_fns(&program.statements, &self.native_fns)
|
|
3099
3228
|
} else {
|
|
3100
3229
|
std::collections::HashMap::new()
|
|
3101
3230
|
};
|
|
3231
|
+
// A fn on a call-graph cycle keeps the f64 ABI: its stack-guard bail unwinds through a
|
|
3232
|
+
// NaN sentinel (#381), and an `i32` return has no value to spare for that. Recursion is
|
|
3233
|
+
// not what the i32 shape exists for.
|
|
3234
|
+
self.native_fns_i32.retain(|n| !self.native_recur_fns.contains_key(n));
|
|
3102
3235
|
if !self.native_fns.is_empty() {
|
|
3103
3236
|
self.emit_native_fns(&program.statements)?;
|
|
3104
3237
|
self.writeln("");
|
|
@@ -3141,6 +3274,9 @@ impl Codegen {
|
|
|
3141
3274
|
// `VarDecl` lowers them as boxed `Value` rather than native `f64` (else the store coerces
|
|
3142
3275
|
// and panics on a JS string-concat result like `s = s + arr[i]`). See
|
|
3143
3276
|
// `collect_demoted_numeric_locals` / `demoted_numeric_locals`.
|
|
3277
|
+
// BEFORE the demotion pass, which consults it: a local assigned from a provably-numeric
|
|
3278
|
+
// call must stop demoting, which is half of where the win comes from.
|
|
3279
|
+
self.user_fn_ret = self.collect_user_fn_rets(&program.statements);
|
|
3144
3280
|
self.demoted_numeric_locals = self.collect_demoted_numeric_locals(&program.statements);
|
|
3145
3281
|
self.int_valued_locals = Self::collect_int_valued_locals(&program.statements);
|
|
3146
3282
|
self.int_range_locals = self.collect_int_range_locals(&program.statements);
|
|
@@ -3552,6 +3688,29 @@ impl Codegen {
|
|
|
3552
3688
|
self.writeln("tishlang_runtime::drain_timers();");
|
|
3553
3689
|
}
|
|
3554
3690
|
|
|
3691
|
+
// RustLib: hand the entry module's exported bindings to the thread-local table before
|
|
3692
|
+
// `run()`'s frame drops. The bindings are ordinary `Value` locals; cloning one clones an
|
|
3693
|
+
// `Rc`, so the closure — and every module-level cell it captured — stays alive and shared.
|
|
3694
|
+
// That is what makes module state survive between `pub fn` calls (and stay mutable through
|
|
3695
|
+
// `clear*`-style reassignment, since captures are `VmRef` cells, not copies).
|
|
3696
|
+
if self.emit_mode == crate::NativeEmitMode::RustLib {
|
|
3697
|
+
let pairs: Vec<String> = self
|
|
3698
|
+
.rust_lib_exports(program)
|
|
3699
|
+
.iter()
|
|
3700
|
+
.map(|e| {
|
|
3701
|
+
format!(
|
|
3702
|
+
"({:?}, {}.clone())",
|
|
3703
|
+
e.exported_name,
|
|
3704
|
+
Self::escape_ident(&e.local_name)
|
|
3705
|
+
)
|
|
3706
|
+
})
|
|
3707
|
+
.collect();
|
|
3708
|
+
self.writeln(&format!(
|
|
3709
|
+
"__tish_publish_exports(::std::vec![{}]);",
|
|
3710
|
+
pairs.join(", ")
|
|
3711
|
+
));
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3555
3714
|
self.writeln("Ok(())");
|
|
3556
3715
|
self.indent -= 1;
|
|
3557
3716
|
self.writeln("}");
|
|
@@ -3580,6 +3739,191 @@ impl Codegen {
|
|
|
3580
3739
|
Ok(())
|
|
3581
3740
|
}
|
|
3582
3741
|
|
|
3742
|
+
/// The entry module's exports, paired with the arity of the `fn` each names (if it names one).
|
|
3743
|
+
/// `merge_modules` unwraps `export` into plain top-level declarations and records the
|
|
3744
|
+
/// `(local, exported)` pairs — including through `export { … } from './x.tish'` re-exports,
|
|
3745
|
+
/// which resolve to the dependency's local binding — so a barrel module still yields the full
|
|
3746
|
+
/// surface. Sorted for deterministic output.
|
|
3747
|
+
fn rust_lib_exports(&self, program: &Program) -> Vec<RustLibExport> {
|
|
3748
|
+
let mut arities: std::collections::HashMap<&str, usize> =
|
|
3749
|
+
std::collections::HashMap::new();
|
|
3750
|
+
for stmt in &program.statements {
|
|
3751
|
+
if let Statement::FunDecl { name, params, .. } = stmt {
|
|
3752
|
+
arities.insert(name.as_ref(), params.len());
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
let mut out: Vec<RustLibExport> = self
|
|
3756
|
+
.entry_exports
|
|
3757
|
+
.iter()
|
|
3758
|
+
.map(|(local, exported)| RustLibExport {
|
|
3759
|
+
local_name: local.clone(),
|
|
3760
|
+
exported_name: exported.clone(),
|
|
3761
|
+
arity: arities.get(local.as_str()).copied(),
|
|
3762
|
+
})
|
|
3763
|
+
.collect();
|
|
3764
|
+
out.sort_by(|a, b| a.exported_name.cmp(&b.exported_name));
|
|
3765
|
+
out
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3768
|
+
/// Emit the `RustLib` public surface: the thread-local module instance, the lazy `ensure_init`,
|
|
3769
|
+
/// and one `pub fn` per entry export.
|
|
3770
|
+
///
|
|
3771
|
+
/// The module body itself is the ordinary `run()` — this only changes *when* it runs and what
|
|
3772
|
+
/// outlives it. Storage is `thread_local!` rather than a global because `Value` is `!Send`
|
|
3773
|
+
/// unless the `send-values` feature is on (it follows `http`), so a `static` would not compile
|
|
3774
|
+
/// for the common configuration. Per-thread instances also match how a consumer uses this:
|
|
3775
|
+
/// register into the module's registries, then call it, on one thread.
|
|
3776
|
+
fn emit_rust_lib_surface(&mut self, program: &Program) -> Result<(), CompileError> {
|
|
3777
|
+
if self.is_async {
|
|
3778
|
+
return Err(CompileError::new(
|
|
3779
|
+
"--target rust --emit lib: the entry module has top-level `await`. A library has \
|
|
3780
|
+
no runtime to drive it — move the async work into an exported function."
|
|
3781
|
+
.to_string(),
|
|
3782
|
+
None,
|
|
3783
|
+
));
|
|
3784
|
+
}
|
|
3785
|
+
let exports = self.rust_lib_exports(program);
|
|
3786
|
+
if exports.is_empty() {
|
|
3787
|
+
return Err(CompileError::new(
|
|
3788
|
+
"--target rust --emit lib: the entry module exports nothing, so the crate would \
|
|
3789
|
+
have no public API. Add `export fn …` to the entry module."
|
|
3790
|
+
.to_string(),
|
|
3791
|
+
None,
|
|
3792
|
+
));
|
|
3793
|
+
}
|
|
3794
|
+
// Only `export fn` is supported. A value export has no arity, and its Rust local is not
|
|
3795
|
+
// necessarily a `Value` to publish (a `const` string lowers to `&str`), so it cannot be
|
|
3796
|
+
// handed to the export table the way a closure can. Say so instead of emitting a getter
|
|
3797
|
+
// that would always read back null.
|
|
3798
|
+
let non_fn: Vec<&str> = exports
|
|
3799
|
+
.iter()
|
|
3800
|
+
.filter(|e| e.arity.is_none())
|
|
3801
|
+
.map(|e| e.exported_name.as_str())
|
|
3802
|
+
.collect();
|
|
3803
|
+
if !non_fn.is_empty() {
|
|
3804
|
+
return Err(CompileError::new(
|
|
3805
|
+
format!(
|
|
3806
|
+
"--target rust --emit lib: only `export fn` can cross the crate boundary, but \
|
|
3807
|
+
the entry module also exports the value(s) `{}`. Wrap each in an exported \
|
|
3808
|
+
function (`export fn {}() {{ … }}`).",
|
|
3809
|
+
non_fn.join("`, `"),
|
|
3810
|
+
non_fn[0]
|
|
3811
|
+
),
|
|
3812
|
+
None,
|
|
3813
|
+
));
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
self.writeln("// ── Tish library surface ──────────────────────────────────────────────");
|
|
3817
|
+
// `Value` and friends are already re-exported by the (pub) runtime prelude above; this adds
|
|
3818
|
+
// the whole runtime for consumers that need more than the prelude carries.
|
|
3819
|
+
self.writeln("pub use tishlang_runtime as runtime;");
|
|
3820
|
+
self.writeln("");
|
|
3821
|
+
self.writeln("thread_local! {");
|
|
3822
|
+
self.indent += 1;
|
|
3823
|
+
self.writeln(
|
|
3824
|
+
"static __TISH_MODULE: ::std::cell::RefCell<::std::option::Option<::std::collections::HashMap<&'static str, Value>>> =",
|
|
3825
|
+
);
|
|
3826
|
+
self.writeln(" ::std::cell::RefCell::new(::std::option::Option::None);");
|
|
3827
|
+
self.indent -= 1;
|
|
3828
|
+
self.writeln("}");
|
|
3829
|
+
self.writeln("");
|
|
3830
|
+
self.writeln("/// Called once from the tail of `run()` with the entry module's exports.");
|
|
3831
|
+
self.writeln("fn __tish_publish_exports(pairs: ::std::vec::Vec<(&'static str, Value)>) {");
|
|
3832
|
+
self.indent += 1;
|
|
3833
|
+
self.writeln("__TISH_MODULE.with(|m| {");
|
|
3834
|
+
self.writeln(" *m.borrow_mut() = ::std::option::Option::Some(pairs.into_iter().collect());");
|
|
3835
|
+
self.writeln("});");
|
|
3836
|
+
self.indent -= 1;
|
|
3837
|
+
self.writeln("}");
|
|
3838
|
+
self.writeln("");
|
|
3839
|
+
self.writeln("/// Run the module body exactly once per thread, before the first export call.");
|
|
3840
|
+
self.writeln("fn __tish_ensure_init() {");
|
|
3841
|
+
self.indent += 1;
|
|
3842
|
+
self.writeln("// Park an empty table BEFORE running the body: that makes init non-reentrant");
|
|
3843
|
+
self.writeln("// (an export reached while the module is still initialising sees an empty");
|
|
3844
|
+
self.writeln("// table and returns null, rather than recursing forever).");
|
|
3845
|
+
self.writeln("let fresh = __TISH_MODULE.with(|m| {");
|
|
3846
|
+
self.writeln(" let mut b = m.borrow_mut();");
|
|
3847
|
+
self.writeln(" if b.is_some() { return false; }");
|
|
3848
|
+
self.writeln(" *b = ::std::option::Option::Some(::std::collections::HashMap::new());");
|
|
3849
|
+
self.writeln(" true");
|
|
3850
|
+
self.writeln("});");
|
|
3851
|
+
self.writeln("if fresh {");
|
|
3852
|
+
self.writeln(" // A module whose top level fails cannot be recovered from here: every");
|
|
3853
|
+
self.writeln(" // export would observe half-initialised state.");
|
|
3854
|
+
self.writeln(" run().unwrap_or_else(|e| panic!(\"tish module initialisation failed: {}\", e));");
|
|
3855
|
+
self.writeln("}");
|
|
3856
|
+
self.indent -= 1;
|
|
3857
|
+
self.writeln("}");
|
|
3858
|
+
self.writeln("");
|
|
3859
|
+
self.writeln("/// Clone an export out of the table. The borrow is released before the value is");
|
|
3860
|
+
self.writeln("/// used, because an export is free to call back into another export.");
|
|
3861
|
+
self.writeln("fn __tish_export(name: &str) -> Value {");
|
|
3862
|
+
self.indent += 1;
|
|
3863
|
+
self.writeln("__tish_ensure_init();");
|
|
3864
|
+
self.writeln("__TISH_MODULE.with(|m| {");
|
|
3865
|
+
self.writeln(" m.borrow().as_ref().and_then(|t| t.get(name).cloned()).unwrap_or(Value::Null)");
|
|
3866
|
+
self.writeln("})");
|
|
3867
|
+
self.indent -= 1;
|
|
3868
|
+
self.writeln("}");
|
|
3869
|
+
self.writeln("");
|
|
3870
|
+
self.writeln("thread_local! {");
|
|
3871
|
+
self.writeln(" static __TISH_LAST_THROW: ::std::cell::RefCell<Value> = ::std::cell::RefCell::new(Value::Null);");
|
|
3872
|
+
self.writeln("}");
|
|
3873
|
+
self.writeln("");
|
|
3874
|
+
self.writeln("/// The value thrown by the most recent export call, or null if it completed normally.");
|
|
3875
|
+
self.writeln("///");
|
|
3876
|
+
self.writeln("/// A `pub fn … -> Value` has no error channel, so a throw that escapes an export is drained");
|
|
3877
|
+
self.writeln("/// and recorded here rather than left parked. Reading it is the way to tell \"returned null\"");
|
|
3878
|
+
self.writeln("/// from \"threw\".");
|
|
3879
|
+
self.writeln("pub fn tish_last_throw() -> Value {");
|
|
3880
|
+
self.indent += 1;
|
|
3881
|
+
self.writeln("__TISH_LAST_THROW.with(|t| t.borrow().clone())");
|
|
3882
|
+
self.indent -= 1;
|
|
3883
|
+
self.writeln("}");
|
|
3884
|
+
self.writeln("");
|
|
3885
|
+
self.writeln("fn __tish_call_export(name: &str, args: &[Value]) -> Value {");
|
|
3886
|
+
self.indent += 1;
|
|
3887
|
+
self.writeln("// A throw is PARKED for the next checkpoint rather than unwound (#303). Across a `pub fn`");
|
|
3888
|
+
self.writeln("// boundary there is no Tish frame to surface it in, so a parked throw would sit latched and");
|
|
3889
|
+
self.writeln("// every LATER call would bail at its own checkpoint and silently return null — one bad call");
|
|
3890
|
+
self.writeln("// bricking the module for the rest of the process. A host hits this simply by reading a");
|
|
3891
|
+
self.writeln("// property off a null the runtime handed it. So: enter clean, leave clean.");
|
|
3892
|
+
self.writeln("let _ = tishlang_runtime::take_pending_throw();");
|
|
3893
|
+
self.writeln("let out = match __tish_export(name) {");
|
|
3894
|
+
self.writeln(" Value::Function(f) => f.call(args),");
|
|
3895
|
+
self.writeln(" other => other,");
|
|
3896
|
+
self.writeln("};");
|
|
3897
|
+
self.writeln("let thrown = tishlang_runtime::take_pending_throw().unwrap_or(Value::Null);");
|
|
3898
|
+
self.writeln("__TISH_LAST_THROW.with(|t| *t.borrow_mut() = thrown);");
|
|
3899
|
+
self.writeln("out");
|
|
3900
|
+
self.indent -= 1;
|
|
3901
|
+
self.writeln("}");
|
|
3902
|
+
self.writeln("");
|
|
3903
|
+
|
|
3904
|
+
for e in &exports {
|
|
3905
|
+
// Checked above: every remaining export is a `fn`, so the arity is present.
|
|
3906
|
+
let n = e.arity.unwrap_or(0);
|
|
3907
|
+
let params: Vec<String> = (0..n).map(|i| format!("a{}: Value", i)).collect();
|
|
3908
|
+
let args: Vec<String> = (0..n).map(|i| format!("a{}", i)).collect();
|
|
3909
|
+
self.writeln(&format!(
|
|
3910
|
+
"pub fn {}({}) -> Value {{",
|
|
3911
|
+
Self::escape_ident(&e.exported_name),
|
|
3912
|
+
params.join(", ")
|
|
3913
|
+
));
|
|
3914
|
+
self.indent += 1;
|
|
3915
|
+
self.writeln(&format!(
|
|
3916
|
+
"__tish_call_export({:?}, &[{}])",
|
|
3917
|
+
e.exported_name,
|
|
3918
|
+
args.join(", ")
|
|
3919
|
+
));
|
|
3920
|
+
self.indent -= 1;
|
|
3921
|
+
self.writeln("}");
|
|
3922
|
+
}
|
|
3923
|
+
self.writeln("");
|
|
3924
|
+
Ok(())
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3583
3927
|
/// Emit an expression in **statement position** (its value is discarded). For a native
|
|
3584
3928
|
/// assignment this emits only the side-effect — NOT the boxed `Value::Number(..)` that the
|
|
3585
3929
|
/// expression form returns (JS "assignment yields its value"). In a hot loop that boxed
|
|
@@ -3599,11 +3943,64 @@ impl Codegen {
|
|
|
3599
3943
|
/// unchanged, and `PropMap` dedup + hidden-class shapes key by text, not pointer — so shapes,
|
|
3600
3944
|
/// insertion order, and `Object.keys` are all unaffected. Same inline-`static` idiom as the
|
|
3601
3945
|
/// per-site `PropIC` caches.
|
|
3946
|
+
/// Append the GBA object-key intern table. Called ONCE, after the whole program is emitted,
|
|
3947
|
+
/// because the key set is only complete then — Rust does not care that a `fn` is defined below
|
|
3948
|
+
/// its callers.
|
|
3949
|
+
///
|
|
3950
|
+
/// ⚠️ `static mut` + `&raw mut` rather than a `OnceLock`: the target is `#![no_std]` and the GBA
|
|
3951
|
+
/// is single-core, so there is no thread to race with. Taking a raw pointer avoids the
|
|
3952
|
+
/// `static_mut_refs` lint that a plain `&mut __TISH_KEYS` would trip.
|
|
3953
|
+
fn emit_gba_key_table(&mut self) {
|
|
3954
|
+
if self.emit_mode != crate::NativeEmitMode::Gba {
|
|
3955
|
+
return;
|
|
3956
|
+
}
|
|
3957
|
+
let names: Vec<String> = self.gba_keys.borrow().0.clone();
|
|
3958
|
+
if names.is_empty() {
|
|
3959
|
+
return;
|
|
3960
|
+
}
|
|
3961
|
+
let list = names
|
|
3962
|
+
.iter()
|
|
3963
|
+
.map(|k| format!("{:?}", k))
|
|
3964
|
+
.collect::<Vec<_>>()
|
|
3965
|
+
.join(", ");
|
|
3966
|
+
self.write(&format!(
|
|
3967
|
+
"\n// Object-literal keys, interned program-wide: {} distinct names.\n\
|
|
3968
|
+
static __TISH_KEY_NAMES: [&str; {}] = [{}];\n\
|
|
3969
|
+
static mut __TISH_KEYS: Option<Vec<Arc<str>>> = None;\n\
|
|
3970
|
+
fn __tish_key(i: usize) -> Arc<str> {{\n\
|
|
3971
|
+
\x20 unsafe {{\n\
|
|
3972
|
+
\x20 let t = &raw mut __TISH_KEYS;\n\
|
|
3973
|
+
\x20 if (*t).is_none() {{\n\
|
|
3974
|
+
\x20 *t = Some(__TISH_KEY_NAMES.iter().map(|s| Arc::from(*s)).collect());\n\
|
|
3975
|
+
\x20 }}\n\
|
|
3976
|
+
\x20 match (*t).as_ref() {{\n\
|
|
3977
|
+
\x20 Some(v) => v[i].clone(),\n\
|
|
3978
|
+
\x20 None => Arc::from(__TISH_KEY_NAMES[i]),\n\
|
|
3979
|
+
\x20 }}\n\
|
|
3980
|
+
\x20 }}\n\
|
|
3981
|
+
}}\n",
|
|
3982
|
+
names.len(),
|
|
3983
|
+
names.len(),
|
|
3984
|
+
list
|
|
3985
|
+
));
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3602
3988
|
fn cached_object_key(&self, k: &str) -> String {
|
|
3603
3989
|
if self.emit_mode == crate::NativeEmitMode::Gba {
|
|
3604
|
-
// No `OnceLock` on no_std GBA
|
|
3605
|
-
//
|
|
3606
|
-
|
|
3990
|
+
// No `OnceLock` on no_std GBA — and a per-site cache would be the wrong tool anyway.
|
|
3991
|
+
// Intern PROGRAM-WIDE instead: every distinct key name is allocated once, at first use,
|
|
3992
|
+
// and each site clones an `Arc` (= `Rc`) out of one table. See the `gba_keys` field.
|
|
3993
|
+
let mut t = self.gba_keys.borrow_mut();
|
|
3994
|
+
let idx = match t.1.get(k) {
|
|
3995
|
+
Some(&i) => i,
|
|
3996
|
+
None => {
|
|
3997
|
+
let i = t.0.len();
|
|
3998
|
+
t.0.push(k.to_string());
|
|
3999
|
+
t.1.insert(k.to_string(), i);
|
|
4000
|
+
i
|
|
4001
|
+
}
|
|
4002
|
+
};
|
|
4003
|
+
return format!("__tish_key({})", idx);
|
|
3607
4004
|
}
|
|
3608
4005
|
format!(
|
|
3609
4006
|
"{{ static K: ::std::sync::OnceLock<::std::sync::Arc<str>> = \
|
|
@@ -6158,6 +6555,24 @@ impl Codegen {
|
|
|
6158
6555
|
}
|
|
6159
6556
|
}
|
|
6160
6557
|
}
|
|
6558
|
+
VecRetKind::Str => {
|
|
6559
|
+
// #605. The body's own `return "…"`, emitted as an OWNED String so it
|
|
6560
|
+
// can leave the fn. `emit_typed_expr` already yields a `String` for a
|
|
6561
|
+
// string literal; anything else is converted rather than assumed,
|
|
6562
|
+
// because a wrong guess here is a type error in the emitted crate.
|
|
6563
|
+
let e = value.as_ref().ok_or_else(|| {
|
|
6564
|
+
CompileError::new("native-vec string fn: empty return", None)
|
|
6565
|
+
})?;
|
|
6566
|
+
let (c, ty) = self.emit_typed_expr(e)?;
|
|
6567
|
+
if ty == RustType::String {
|
|
6568
|
+
self.writeln(&format!("return {};", c));
|
|
6569
|
+
} else {
|
|
6570
|
+
self.writeln(&format!(
|
|
6571
|
+
"return {};",
|
|
6572
|
+
RustType::String.from_value_expr(&c)
|
|
6573
|
+
));
|
|
6574
|
+
}
|
|
6575
|
+
}
|
|
6161
6576
|
VecRetKind::Unit => {
|
|
6162
6577
|
self.writeln("return;");
|
|
6163
6578
|
}
|
|
@@ -6963,6 +7378,23 @@ impl Codegen {
|
|
|
6963
7378
|
Ok(())
|
|
6964
7379
|
}
|
|
6965
7380
|
|
|
7381
|
+
/// `.clone()` an emitted operand that is a bare Rust identifier, leave anything else alone.
|
|
7382
|
+
///
|
|
7383
|
+
/// Used where an operand is yielded as the value of an expression that does not consume it
|
|
7384
|
+
/// exclusively (`&&` / `||`): moving a non-`Copy` `Value` local out of such a position makes
|
|
7385
|
+
/// every later read a use-after-move. A parenthesised expression, call, field access, or an
|
|
7386
|
+
/// already-`.clone()`d ident is a temporary, so it needs nothing.
|
|
7387
|
+
fn clone_if_bare_ident(s: &str) -> String {
|
|
7388
|
+
let bare = !s.is_empty()
|
|
7389
|
+
&& !s.starts_with(|c: char| c.is_ascii_digit())
|
|
7390
|
+
&& s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
|
|
7391
|
+
if bare {
|
|
7392
|
+
format!("{}.clone()", s)
|
|
7393
|
+
} else {
|
|
7394
|
+
s.to_string()
|
|
7395
|
+
}
|
|
7396
|
+
}
|
|
7397
|
+
|
|
6966
7398
|
fn emit_call_arg(&mut self, arg: &CallArg) -> Result<String, CompileError> {
|
|
6967
7399
|
let e = match arg {
|
|
6968
7400
|
CallArg::Expr(e) | CallArg::Spread(e) => e,
|
|
@@ -8699,27 +9131,31 @@ impl Codegen {
|
|
|
8699
9131
|
// M5: boxed-context call to an eligible native fn → `Value::Number(name_native(..))`.
|
|
8700
9132
|
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
8701
9133
|
if self.native_fns.contains(fname.as_ref()) {
|
|
9134
|
+
let i32_abi = self.native_fns_i32.contains(fname.as_ref());
|
|
9135
|
+
let want =
|
|
9136
|
+
if i32_abi { RustType::I32 } else { RustType::F64 };
|
|
8702
9137
|
let mut argc: Vec<String> = Vec::with_capacity(args.len());
|
|
8703
9138
|
let mut ok = true;
|
|
8704
9139
|
for a in args {
|
|
8705
9140
|
if let CallArg::Expr(e) = a {
|
|
8706
9141
|
let (ac, at) = self.emit_typed_expr(e)?;
|
|
8707
|
-
argc.push(
|
|
8708
|
-
RustType::F64.from_value_expr(&ac)
|
|
8709
|
-
} else {
|
|
8710
|
-
ac
|
|
8711
|
-
});
|
|
9142
|
+
argc.push(self.coerce_native_arg(&ac, at, want.clone()));
|
|
8712
9143
|
} else {
|
|
8713
9144
|
ok = false;
|
|
8714
9145
|
break;
|
|
8715
9146
|
}
|
|
8716
9147
|
}
|
|
8717
9148
|
if ok {
|
|
8718
|
-
|
|
8719
|
-
"
|
|
9149
|
+
let call = format!(
|
|
9150
|
+
"{}({})",
|
|
8720
9151
|
self.native_call_target(fname.as_ref()),
|
|
8721
9152
|
argc.join(", ")
|
|
8722
|
-
)
|
|
9153
|
+
);
|
|
9154
|
+
return Ok(if i32_abi {
|
|
9155
|
+
format!("Value::Number(({}) as f64)", call)
|
|
9156
|
+
} else {
|
|
9157
|
+
format!("Value::Number({})", call)
|
|
9158
|
+
});
|
|
8723
9159
|
}
|
|
8724
9160
|
}
|
|
8725
9161
|
}
|
|
@@ -11701,6 +12137,21 @@ impl Codegen {
|
|
|
11701
12137
|
}
|
|
11702
12138
|
}
|
|
11703
12139
|
|
|
12140
|
+
// Int-domain shortcut for an integer-typed local: `let nw: i32 = w | P_MARK` binds the
|
|
12141
|
+
// integer form rather than `(int as f64) as i32`, so `nw` is i32-backed and every later use
|
|
12142
|
+
// — including storing it into an `i32[]` — stays integral. Without this the annotation
|
|
12143
|
+
// actively costs: measured 23,4xx ticks/1000 for `let v: i32 = j & 255; arr[j] = v`
|
|
12144
|
+
// against 1,5xx for `arr[j] = j & 255` written directly.
|
|
12145
|
+
if target_type.is_integer_scalar() {
|
|
12146
|
+
if let Some(int_code) = self.emit_int32_for_int_consumer(expr)? {
|
|
12147
|
+
return Ok(if *target_type == RustType::I32 {
|
|
12148
|
+
int_code
|
|
12149
|
+
} else {
|
|
12150
|
+
format!("({}) as {}", int_code, target_type.to_rust_type_str())
|
|
12151
|
+
});
|
|
12152
|
+
}
|
|
12153
|
+
}
|
|
12154
|
+
|
|
11704
12155
|
// #179 Stage C: `arr.length` on a native `Vec<_>` against an F64 target → native
|
|
11705
12156
|
// `(arr.len() as f64)` (no boxing). Lets `let n = arr.length` stay native f64.
|
|
11706
12157
|
if let (
|
|
@@ -11981,23 +12432,285 @@ impl Codegen {
|
|
|
11981
12432
|
RustType::from_annotation(ann) == RustType::F64
|
|
11982
12433
|
}
|
|
11983
12434
|
|
|
11984
|
-
|
|
11985
|
-
|
|
11986
|
-
|
|
11987
|
-
|
|
11988
|
-
|
|
11989
|
-
|
|
11990
|
-
|
|
11991
|
-
|
|
11992
|
-
|
|
11993
|
-
|
|
11994
|
-
|
|
11995
|
-
|
|
11996
|
-
|
|
11997
|
-
|
|
11998
|
-
|
|
11999
|
-
|
|
12000
|
-
|
|
12435
|
+
fn ann_is_i32(ann: &TypeAnnotation) -> bool {
|
|
12436
|
+
RustType::from_annotation(ann) == RustType::I32
|
|
12437
|
+
}
|
|
12438
|
+
|
|
12439
|
+
/// Is every parameter AND the return annotated `: i32`?
|
|
12440
|
+
///
|
|
12441
|
+
/// ALL OR NOTHING, deliberately. An `: i32` parameter reached through the boxed path is
|
|
12442
|
+
/// ToInt32-coerced on entry; promoted to a native `f64` it would not be, so 3.7 would arrive as
|
|
12443
|
+
/// 3 one way and 3.7 the other. Admitting `: i32` per-annotation would do exactly that to a
|
|
12444
|
+
/// function that mixes the two. A function whose whole signature is `i32` has one convention
|
|
12445
|
+
/// and keeps the coercion at the boundary, where the caller does it once on an argument that is
|
|
12446
|
+
/// usually already an integer.
|
|
12447
|
+
fn fn_sig_all_i32(params: &[FunParam], return_type: &Option<TypeAnnotation>) -> bool {
|
|
12448
|
+
return_type.as_ref().is_some_and(Self::ann_is_i32)
|
|
12449
|
+
&& !params.is_empty()
|
|
12450
|
+
&& params.iter().all(|p| {
|
|
12451
|
+
matches!(p, FunParam::Simple(tp)
|
|
12452
|
+
if tp.default.is_none()
|
|
12453
|
+
&& tp.type_ann.as_ref().is_some_and(Self::ann_is_i32))
|
|
12454
|
+
})
|
|
12455
|
+
}
|
|
12456
|
+
|
|
12457
|
+
// ── Soundness: demote `number` locals that a reassignment can turn non-numeric ──────────────
|
|
12458
|
+
//
|
|
12459
|
+
// `let s = 0` is inferred `number` → lowered to a native `f64`, and a reassignment stores into
|
|
12460
|
+
// it via `s = match &<rhs> { Value::Number(n) => *n, _ => panic!("expected number") }`. That
|
|
12461
|
+
// coercion PANICS when `<rhs>` is not a number — which `s = s + arr[i]` produces whenever
|
|
12462
|
+
// `arr[i]` is a String (JS `+` is string concat). Node, the interpreter, and the VM all yield
|
|
12463
|
+
// a string there (the VM array-JIT bails to the interpreter on a non-numeric element). The
|
|
12464
|
+
// fix: keep such a local a boxed `Value`, so the boxed `ops::add` — which concatenates —
|
|
12465
|
+
// flows through unchanged.
|
|
12466
|
+
//
|
|
12467
|
+
// A reassignment is SAFE iff its RHS lowers to a native `f64`, which is exactly what
|
|
12468
|
+
// `emit_typed_expr` decides. `expr_native_type` is a read-only mirror of that decision and is
|
|
12469
|
+
// deliberately conservative: any form it does not model → `Value` → demote (sound; at worst an
|
|
12470
|
+
// unnecessary box). A fixpoint propagates demotions through chains (`y = y + s` once `s` is
|
|
12471
|
+
// demoted). The map is name-flat across the whole program (a name demoted in one function is
|
|
12472
|
+
// demoted in all) — still sound, and harmless to the perf gauntlet, where each kernel is its
|
|
12473
|
+
// own program with unique accumulator names.
|
|
12474
|
+
/// Which top-level fns provably return a number, and as what native type. A fixpoint, because
|
|
12475
|
+
/// one qualifying fn makes calls to it numeric inside another; growing only, so it terminates.
|
|
12476
|
+
fn collect_user_fn_rets(
|
|
12477
|
+
&self,
|
|
12478
|
+
stmts: &[Statement],
|
|
12479
|
+
) -> std::collections::HashMap<String, RustType> {
|
|
12480
|
+
// MODULE SCOPE ONLY. `collect_annotated_types` over the whole program builds a NAME-FLAT
|
|
12481
|
+
// map, which `collect_demoted_numeric_locals` can use because its error direction is safe —
|
|
12482
|
+
// over-demoting only costs a box. Proving is the opposite: a numeric `s` in one function
|
|
12483
|
+
// would make a STRING `s` in another look numeric, and the return gets coerced.
|
|
12484
|
+
//
|
|
12485
|
+
// That is not hypothetical. It shipped: `function ruleTag(mask: i32) { let s = ""; …;
|
|
12486
|
+
// return s }` was proven numeric off an unrelated numeric `s`, and every ROM using it
|
|
12487
|
+
// panicked with "expected number". Each fn gets its own scope now.
|
|
12488
|
+
let mut module_env: HashMap<String, RustType> = HashMap::new();
|
|
12489
|
+
let top_only: Vec<Statement> = stmts
|
|
12490
|
+
.iter()
|
|
12491
|
+
.filter(|s| matches!(s, Statement::VarDecl { .. }))
|
|
12492
|
+
.cloned()
|
|
12493
|
+
.collect();
|
|
12494
|
+
Self::collect_annotated_types(&top_only, &self.type_aliases, &mut module_env);
|
|
12495
|
+
|
|
12496
|
+
let mut decls: Vec<(String, &Statement, HashMap<String, RustType>)> = Vec::new();
|
|
12497
|
+
for s in stmts {
|
|
12498
|
+
if let Statement::FunDecl { async_: false, name, params, body, .. } = s {
|
|
12499
|
+
let mut env = module_env.clone();
|
|
12500
|
+
// This fn's own params and locals — not a nested closure's, whose names are its own.
|
|
12501
|
+
for prm in params {
|
|
12502
|
+
if let FunParam::Simple(tp) = prm {
|
|
12503
|
+
if let Some(ann) = &tp.type_ann {
|
|
12504
|
+
let ty = RustType::from_annotation(ann);
|
|
12505
|
+
if ty.is_native() {
|
|
12506
|
+
env.insert(tp.name.to_string(), ty);
|
|
12507
|
+
}
|
|
12508
|
+
}
|
|
12509
|
+
}
|
|
12510
|
+
}
|
|
12511
|
+
Self::collect_own_locals(body.as_ref(), &self.type_aliases, &mut env);
|
|
12512
|
+
decls.push((name.to_string(), body.as_ref(), env));
|
|
12513
|
+
}
|
|
12514
|
+
}
|
|
12515
|
+
|
|
12516
|
+
// A REASSIGNED name is not this fn at the call site, and a SHADOWED one is not either —
|
|
12517
|
+
// `emit_typed_expr` sees a bare callee name with no scope information. Excluding every name
|
|
12518
|
+
// ever bound as a variable or parameter is blunt, but the alternative is a miscompile.
|
|
12519
|
+
let mut reassigns: Vec<(String, &Expr)> = Vec::new();
|
|
12520
|
+
Self::collect_reassignments_stmts(stmts, &mut reassigns);
|
|
12521
|
+
let mut excluded: std::collections::HashSet<String> =
|
|
12522
|
+
reassigns.iter().map(|(n, _)| n.clone()).collect();
|
|
12523
|
+
Self::collect_bound_names(stmts, &mut excluded);
|
|
12524
|
+
|
|
12525
|
+
let mut out: std::collections::HashMap<String, RustType> = std::collections::HashMap::new();
|
|
12526
|
+
loop {
|
|
12527
|
+
let mut changed = false;
|
|
12528
|
+
for (name, body, env) in &decls {
|
|
12529
|
+
if out.contains_key(name) || excluded.contains(name) {
|
|
12530
|
+
continue;
|
|
12531
|
+
}
|
|
12532
|
+
let mut rets: Vec<&Expr> = Vec::new();
|
|
12533
|
+
Self::collect_return_exprs(body, &mut rets);
|
|
12534
|
+
if rets.is_empty() {
|
|
12535
|
+
continue; // no return yields `undefined`, which is not a number
|
|
12536
|
+
}
|
|
12537
|
+
let mut ty: Option<RustType> = None;
|
|
12538
|
+
let mut ok = true;
|
|
12539
|
+
for e in &rets {
|
|
12540
|
+
let Some(et) = self.proof_numeric_type(e, env, &out) else {
|
|
12541
|
+
ok = false;
|
|
12542
|
+
break;
|
|
12543
|
+
};
|
|
12544
|
+
ty = match ty {
|
|
12545
|
+
None => Some(et),
|
|
12546
|
+
Some(prev) if prev == et => Some(et),
|
|
12547
|
+
Some(_) => Some(RustType::F64), // mixed I32/F64 settles on the wider
|
|
12548
|
+
};
|
|
12549
|
+
}
|
|
12550
|
+
if ok {
|
|
12551
|
+
if let Some(final_ty) = ty {
|
|
12552
|
+
out.insert(name.clone(), final_ty);
|
|
12553
|
+
changed = true;
|
|
12554
|
+
}
|
|
12555
|
+
}
|
|
12556
|
+
}
|
|
12557
|
+
if !changed {
|
|
12558
|
+
break;
|
|
12559
|
+
}
|
|
12560
|
+
}
|
|
12561
|
+
out
|
|
12562
|
+
}
|
|
12563
|
+
|
|
12564
|
+
/// Is `e` provably a NUMBER, and which native type describes it?
|
|
12565
|
+
///
|
|
12566
|
+
/// Separate from `expr_native_type`, which answers the stricter "can this be LOWERED natively"
|
|
12567
|
+
/// and is bound by `result_type_of_binop`, whose arms require BOTH operands `F64`. That rejects
|
|
12568
|
+
/// `arr[i] & 255` on a `Vec<i32>` — element `I32`, literal `F64` — which on GBA is nearly every
|
|
12569
|
+
/// masked read of a typed array. Widening that function would change lowering everywhere it is
|
|
12570
|
+
/// consulted, to serve a question it is not being asked.
|
|
12571
|
+
///
|
|
12572
|
+
/// Two unconditional JS facts carry it: `- * / % **` coerce with ToNumber, and `& | ^ << >>`
|
|
12573
|
+
/// with ToInt32 — a number, and an int32 one, even if an operand was a string. `>>>` is excluded
|
|
12574
|
+
/// (its uint32 can exceed `i32::MAX`) and `+` needs both sides proven, because it concatenates.
|
|
12575
|
+
fn proof_numeric_type(
|
|
12576
|
+
&self,
|
|
12577
|
+
e: &Expr,
|
|
12578
|
+
env: &HashMap<String, RustType>,
|
|
12579
|
+
fn_rets: &std::collections::HashMap<String, RustType>,
|
|
12580
|
+
) -> Option<RustType> {
|
|
12581
|
+
let numeric = |t: &RustType| matches!(t, RustType::F64 | RustType::I32);
|
|
12582
|
+
match e {
|
|
12583
|
+
Expr::Literal { value: Literal::Number(_), .. } => Some(RustType::F64),
|
|
12584
|
+
Expr::Ident { name, .. } => env.get(name.as_ref()).filter(|t| numeric(t)).cloned(),
|
|
12585
|
+
Expr::Index { object, optional: false, .. } => {
|
|
12586
|
+
if let Expr::Ident { name, .. } = object.as_ref() {
|
|
12587
|
+
if let Some(RustType::Vec(inner)) = env.get(name.as_ref()) {
|
|
12588
|
+
if numeric(inner) {
|
|
12589
|
+
return Some((**inner).clone());
|
|
12590
|
+
}
|
|
12591
|
+
}
|
|
12592
|
+
}
|
|
12593
|
+
None
|
|
12594
|
+
}
|
|
12595
|
+
Expr::Unary { op, operand, .. } => match op {
|
|
12596
|
+
UnaryOp::Neg | UnaryOp::Pos => {
|
|
12597
|
+
self.proof_numeric_type(operand, env, fn_rets).map(|_| RustType::F64)
|
|
12598
|
+
}
|
|
12599
|
+
_ => None,
|
|
12600
|
+
},
|
|
12601
|
+
Expr::Binary { left, op, right, .. } => match op {
|
|
12602
|
+
BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor | BinOp::Shl | BinOp::Shr => {
|
|
12603
|
+
Some(RustType::I32)
|
|
12604
|
+
}
|
|
12605
|
+
BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow => Some(RustType::F64),
|
|
12606
|
+
BinOp::Add => {
|
|
12607
|
+
let l = self.proof_numeric_type(left, env, fn_rets)?;
|
|
12608
|
+
let r = self.proof_numeric_type(right, env, fn_rets)?;
|
|
12609
|
+
Some(if l == RustType::I32 && r == RustType::I32 {
|
|
12610
|
+
RustType::I32
|
|
12611
|
+
} else {
|
|
12612
|
+
RustType::F64
|
|
12613
|
+
})
|
|
12614
|
+
}
|
|
12615
|
+
_ => None,
|
|
12616
|
+
},
|
|
12617
|
+
Expr::Conditional { then_branch, else_branch, .. } => {
|
|
12618
|
+
let a = self.proof_numeric_type(then_branch, env, fn_rets)?;
|
|
12619
|
+
let b = self.proof_numeric_type(else_branch, env, fn_rets)?;
|
|
12620
|
+
Some(if a == b { a } else { RustType::F64 })
|
|
12621
|
+
}
|
|
12622
|
+
Expr::Call { callee, .. } => match callee.as_ref() {
|
|
12623
|
+
Expr::Ident { name, .. } => fn_rets.get(name.as_ref()).cloned(),
|
|
12624
|
+
_ => None,
|
|
12625
|
+
},
|
|
12626
|
+
_ => None,
|
|
12627
|
+
}
|
|
12628
|
+
}
|
|
12629
|
+
|
|
12630
|
+
/// Annotated locals declared directly in this body — through blocks, ifs and loops, but NOT
|
|
12631
|
+
/// into a nested function, whose locals are its own scope and would re-create the name
|
|
12632
|
+
/// collision this exists to avoid.
|
|
12633
|
+
fn collect_own_locals(
|
|
12634
|
+
s: &Statement,
|
|
12635
|
+
aliases: &HashMap<String, RustType>,
|
|
12636
|
+
env: &mut HashMap<String, RustType>,
|
|
12637
|
+
) {
|
|
12638
|
+
match s {
|
|
12639
|
+
Statement::VarDecl { .. } => {
|
|
12640
|
+
Self::collect_annotated_types(core::slice::from_ref(s), aliases, env)
|
|
12641
|
+
}
|
|
12642
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
12643
|
+
for x in statements {
|
|
12644
|
+
Self::collect_own_locals(x, aliases, env);
|
|
12645
|
+
}
|
|
12646
|
+
}
|
|
12647
|
+
Statement::If { then_branch, else_branch, .. } => {
|
|
12648
|
+
Self::collect_own_locals(then_branch, aliases, env);
|
|
12649
|
+
if let Some(e) = else_branch {
|
|
12650
|
+
Self::collect_own_locals(e, aliases, env);
|
|
12651
|
+
}
|
|
12652
|
+
}
|
|
12653
|
+
Statement::While { body, .. } | Statement::For { body, .. } => {
|
|
12654
|
+
Self::collect_own_locals(body, aliases, env)
|
|
12655
|
+
}
|
|
12656
|
+
_ => {}
|
|
12657
|
+
}
|
|
12658
|
+
}
|
|
12659
|
+
|
|
12660
|
+
/// Every name bound as a variable or a function parameter, anywhere, at any depth.
|
|
12661
|
+
fn collect_bound_names(stmts: &[Statement], out: &mut std::collections::HashSet<String>) {
|
|
12662
|
+
for s in stmts {
|
|
12663
|
+
match s {
|
|
12664
|
+
Statement::VarDecl { name, .. } => {
|
|
12665
|
+
out.insert(name.to_string());
|
|
12666
|
+
}
|
|
12667
|
+
Statement::FunDecl { params, body, .. } => {
|
|
12668
|
+
for p in params {
|
|
12669
|
+
if let FunParam::Simple(tp) = p {
|
|
12670
|
+
out.insert(tp.name.to_string());
|
|
12671
|
+
}
|
|
12672
|
+
}
|
|
12673
|
+
Self::collect_bound_names(core::slice::from_ref(body.as_ref()), out);
|
|
12674
|
+
}
|
|
12675
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
12676
|
+
Self::collect_bound_names(statements, out)
|
|
12677
|
+
}
|
|
12678
|
+
Statement::If { then_branch, else_branch, .. } => {
|
|
12679
|
+
Self::collect_bound_names(core::slice::from_ref(then_branch.as_ref()), out);
|
|
12680
|
+
if let Some(e) = else_branch {
|
|
12681
|
+
Self::collect_bound_names(core::slice::from_ref(e.as_ref()), out);
|
|
12682
|
+
}
|
|
12683
|
+
}
|
|
12684
|
+
Statement::While { body, .. } | Statement::For { body, .. } => {
|
|
12685
|
+
Self::collect_bound_names(core::slice::from_ref(body.as_ref()), out)
|
|
12686
|
+
}
|
|
12687
|
+
_ => {}
|
|
12688
|
+
}
|
|
12689
|
+
}
|
|
12690
|
+
}
|
|
12691
|
+
|
|
12692
|
+
/// Every `return <expr>` in a body, through blocks, ifs and loops. NOT into a nested function.
|
|
12693
|
+
fn collect_return_exprs<'a>(s: &'a Statement, out: &mut Vec<&'a Expr>) {
|
|
12694
|
+
match s {
|
|
12695
|
+
Statement::Return { value: Some(e), .. } => out.push(e),
|
|
12696
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
12697
|
+
for x in statements {
|
|
12698
|
+
Self::collect_return_exprs(x, out);
|
|
12699
|
+
}
|
|
12700
|
+
}
|
|
12701
|
+
Statement::If { then_branch, else_branch, .. } => {
|
|
12702
|
+
Self::collect_return_exprs(then_branch, out);
|
|
12703
|
+
if let Some(e) = else_branch {
|
|
12704
|
+
Self::collect_return_exprs(e, out);
|
|
12705
|
+
}
|
|
12706
|
+
}
|
|
12707
|
+
Statement::While { body, .. } | Statement::For { body, .. } => {
|
|
12708
|
+
Self::collect_return_exprs(body, out)
|
|
12709
|
+
}
|
|
12710
|
+
_ => {}
|
|
12711
|
+
}
|
|
12712
|
+
}
|
|
12713
|
+
|
|
12001
12714
|
fn collect_demoted_numeric_locals(&self, stmts: &[Statement]) -> HashSet<String> {
|
|
12002
12715
|
// 1. Flat env: every annotated local/param name → its native `RustType`.
|
|
12003
12716
|
let mut env: HashMap<String, RustType> = HashMap::new();
|
|
@@ -13285,7 +13998,11 @@ impl Codegen {
|
|
|
13285
13998
|
let in_bounds = self.index_in_bounds(index, name.as_ref());
|
|
13286
13999
|
let idx_usize = self.emit_index_usize(index)?;
|
|
13287
14000
|
let native_val = if *elem_type == RustType::I32 {
|
|
13288
|
-
|
|
14001
|
+
// A bitwise RHS stays in the integer domain instead of `(int as f64) as i32`. Exact —
|
|
14002
|
+
// see `emit_int32_for_int_consumer`.
|
|
14003
|
+
if let Some(int_code) = self.emit_int32_for_int_consumer(value)? {
|
|
14004
|
+
int_code
|
|
14005
|
+
} else if let Expr::Binary {
|
|
13289
14006
|
left,
|
|
13290
14007
|
op: BinOp::Sub,
|
|
13291
14008
|
right,
|
|
@@ -13397,6 +14114,12 @@ impl Codegen {
|
|
|
13397
14114
|
return Ok(uv.clone());
|
|
13398
14115
|
}
|
|
13399
14116
|
}
|
|
14117
|
+
// Same int-domain shortcut as the element store, with one correction: `(neg as f64) as
|
|
14118
|
+
// usize` SATURATES to 0, while `(neg_i32) as usize` wraps to a huge value. `.max(0)` makes
|
|
14119
|
+
// the two identical for every input, and costs a compare against a soft-float call.
|
|
14120
|
+
if let Some(int_code) = self.emit_int32_for_int_consumer(index)? {
|
|
14121
|
+
return Ok(format!("({}).max(0) as usize", int_code));
|
|
14122
|
+
}
|
|
13400
14123
|
let (idx_code, idx_ty) = self.emit_typed_expr(index)?;
|
|
13401
14124
|
if idx_ty == RustType::F64 || idx_ty == RustType::I32 {
|
|
13402
14125
|
Ok(format!("({}) as usize", idx_code))
|
|
@@ -13520,6 +14243,19 @@ impl Codegen {
|
|
|
13520
14243
|
value: &Expr,
|
|
13521
14244
|
target_ty: &RustType,
|
|
13522
14245
|
) -> Result<String, CompileError> {
|
|
14246
|
+
// Same int-domain shortcut as the `Vec<i32>` element store: a bitwise RHS bound to an
|
|
14247
|
+
// integer target keeps its integer form instead of `(int as f64) as i32`. Without this,
|
|
14248
|
+
// `let v: i32 = w & 255` is f64-backed and every later use of `v` pays the conversion —
|
|
14249
|
+
// measured at 23,4xx ticks/1000 against 1,5xx for the direct form.
|
|
14250
|
+
if target_ty.is_integer_scalar() {
|
|
14251
|
+
if let Some(int_code) = self.emit_int32_for_int_consumer(value)? {
|
|
14252
|
+
return Ok(if *target_ty == RustType::I32 {
|
|
14253
|
+
int_code
|
|
14254
|
+
} else {
|
|
14255
|
+
format!("({}) as {}", int_code, target_ty.to_rust_type_str())
|
|
14256
|
+
});
|
|
14257
|
+
}
|
|
14258
|
+
}
|
|
13523
14259
|
let (val_code, val_ty) = self.emit_typed_expr(value)?;
|
|
13524
14260
|
if val_ty == *target_ty {
|
|
13525
14261
|
return Ok(val_code);
|
|
@@ -13534,6 +14270,26 @@ impl Codegen {
|
|
|
13534
14270
|
if *target_ty == RustType::F64 && val_ty.is_integer_scalar() {
|
|
13535
14271
|
return Ok(format!("({}) as f64", val_code));
|
|
13536
14272
|
}
|
|
14273
|
+
// FIXED CROSSES DIRECTLY, not through a `Value`. Without these arms a `fixed` reaching an
|
|
14274
|
+
// `f64` extern parameter (or the reverse) fell to the boxed round trip below — allocate a
|
|
14275
|
+
// Value::Number, then immediately unwrap it — which is the whole cost the typed-extern path
|
|
14276
|
+
// exists to remove, reintroduced at the boundary it was removed from (#607). Same scaling as
|
|
14277
|
+
// `RustType::Fixed`'s Value conversions in types.rs, so the values are identical either way.
|
|
14278
|
+
if *target_ty == RustType::F64 && val_ty == RustType::Fixed {
|
|
14279
|
+
return Ok(format!("(({}).to_raw() as f64 / 256.0)", val_code));
|
|
14280
|
+
}
|
|
14281
|
+
if *target_ty == RustType::Fixed && val_ty == RustType::F64 {
|
|
14282
|
+
return Ok(format!(
|
|
14283
|
+
"tishlang_runtime::Fixed::from_raw((({}) * 256.0) as i32)",
|
|
14284
|
+
val_code
|
|
14285
|
+
));
|
|
14286
|
+
}
|
|
14287
|
+
if *target_ty == RustType::Fixed && val_ty.is_integer_scalar() {
|
|
14288
|
+
return Ok(format!(
|
|
14289
|
+
"tishlang_runtime::Fixed::from_raw((({}) as i32).wrapping_mul(256))",
|
|
14290
|
+
val_code
|
|
14291
|
+
));
|
|
14292
|
+
}
|
|
13537
14293
|
let boxed = if val_ty.is_native() {
|
|
13538
14294
|
val_ty.to_value_expr(&val_code)
|
|
13539
14295
|
} else {
|
|
@@ -15419,6 +16175,9 @@ impl Codegen {
|
|
|
15419
16175
|
let rt = self.expr_native_type(right, env);
|
|
15420
16176
|
RustType::result_type_of_binop(*op, <, &rt).unwrap_or(RustType::Value)
|
|
15421
16177
|
}
|
|
16178
|
+
// A call to a fn whose every return is PROVEN numeric. Without this a call is opaque and
|
|
16179
|
+
// any local assigned from one demotes to a boxed `Value`. `env` shadowing wins: a local
|
|
16180
|
+
// of that name is not the fn.
|
|
15422
16181
|
// `vec[i]` where `vec` is a `number[]` (Vec<f64>) → the element type. A `Vec<f64>`
|
|
15423
16182
|
// can only hold numbers, so this never feeds a string into the accumulator.
|
|
15424
16183
|
Expr::Index {
|
|
@@ -15497,13 +16256,30 @@ impl Codegen {
|
|
|
15497
16256
|
}
|
|
15498
16257
|
RustType::Value
|
|
15499
16258
|
}
|
|
16259
|
+
// ONE Call arm, and the singularity is load-bearing: the numeric-return proof briefly
|
|
16260
|
+
// lived in its own `Expr::Call` arm ABOVE this one, which shadowed everything below —
|
|
16261
|
+
// native-fn results, the Math intrinsics and the #169 reduce modelling all became
|
|
16262
|
+
// unreachable (rustc said so, as a warning nobody read) and every accumulator they
|
|
16263
|
+
// feed demoted to a boxed `Value`. CI caught it as issue_169 + the #320 push test.
|
|
15500
16264
|
Expr::Call { callee, args, .. } => {
|
|
15501
|
-
// M5 native fn (`fn f_native(..) -> f64`); requires all-positional args.
|
|
15502
16265
|
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
16266
|
+
// M5 native fn (`fn f_native(..) -> f64`); requires all-positional args.
|
|
15503
16267
|
if self.native_fns.contains(fname.as_ref())
|
|
15504
16268
|
&& args.iter().all(|a| matches!(a, CallArg::Expr(_)))
|
|
15505
16269
|
{
|
|
15506
|
-
return
|
|
16270
|
+
return if self.native_fns_i32.contains(fname.as_ref()) {
|
|
16271
|
+
RustType::I32
|
|
16272
|
+
} else {
|
|
16273
|
+
RustType::F64
|
|
16274
|
+
};
|
|
16275
|
+
}
|
|
16276
|
+
// A call to a fn whose every return is PROVEN numeric (the boxed call's RESULT
|
|
16277
|
+
// stops being opaque — the dispatch is unchanged). `env` shadowing wins: a
|
|
16278
|
+
// local of that name is not the fn.
|
|
16279
|
+
if !env.contains_key(fname.as_ref()) {
|
|
16280
|
+
if let Some(ft) = self.user_fn_ret.get(fname.as_ref()) {
|
|
16281
|
+
return ft.clone();
|
|
16282
|
+
}
|
|
15507
16283
|
}
|
|
15508
16284
|
}
|
|
15509
16285
|
// Single-arg `Math.<intrinsic>(x)` lowered to a direct `f64` method → number.
|
|
@@ -16254,6 +17030,22 @@ impl Codegen {
|
|
|
16254
17030
|
}
|
|
16255
17031
|
|
|
16256
17032
|
/// Emit `thread_local! { static G_NAME: Cell<f64> = ... }` for each eligible global.
|
|
17033
|
+
/// #594, GBA: the no_std twin of `emit_native_numeric_global_tls`. `SingleCore` is the facade's
|
|
17034
|
+
/// single-core static wrapper — sound because the GBA runs one cooperative thread and no
|
|
17035
|
+
/// interrupt handler touches interpreter state — and its `with()` mirrors `LocalKey::with`, so
|
|
17036
|
+
/// the read/write emission is shared with the TLS path rather than duplicated.
|
|
17037
|
+
fn emit_native_numeric_global_singlecore(&mut self) -> Result<(), CompileError> {
|
|
17038
|
+
for (name, init) in self.native_numeric_globals.clone() {
|
|
17039
|
+
self.writeln(&format!(
|
|
17040
|
+
"static {}: tishlang_runtime::SingleCore<core::cell::Cell<f64>> = \
|
|
17041
|
+
tishlang_runtime::SingleCore::new(core::cell::Cell::new({}));",
|
|
17042
|
+
Self::native_global_static(&name),
|
|
17043
|
+
Self::f64_lit(init),
|
|
17044
|
+
));
|
|
17045
|
+
}
|
|
17046
|
+
Ok(())
|
|
17047
|
+
}
|
|
17048
|
+
|
|
16257
17049
|
fn emit_native_numeric_global_tls(&mut self) -> Result<(), CompileError> {
|
|
16258
17050
|
self.writeln("thread_local! {");
|
|
16259
17051
|
self.indent += 1;
|
|
@@ -16272,6 +17064,9 @@ impl Codegen {
|
|
|
16272
17064
|
/// Top-level `let name = [n0, n1, …]` with numeric literals, never reassigned — emitted as
|
|
16273
17065
|
/// `const G_name: [f64; N]` for direct indexing from native fns (fasta `codes`/`probs`).
|
|
16274
17066
|
fn collect_module_const_f64_arrays(stmts: &[Statement]) -> HashMap<String, Vec<f64>> {
|
|
17067
|
+
// Every name bound anywhere in the program by anything other than the candidate decls
|
|
17068
|
+
// themselves. Collected alongside the existing per-fn local scan below.
|
|
17069
|
+
let mut shadow_bindings: HashSet<String> = HashSet::new();
|
|
16275
17070
|
use std::collections::{HashMap, HashSet};
|
|
16276
17071
|
let mut candidates: HashMap<String, Vec<f64>> = HashMap::new();
|
|
16277
17072
|
for s in stmts {
|
|
@@ -16312,6 +17107,16 @@ impl Codegen {
|
|
|
16312
17107
|
candidates.remove(&n);
|
|
16313
17108
|
}
|
|
16314
17109
|
}
|
|
17110
|
+
// …AND ANY OTHER BINDING OF THE NAME, not just `let`/`const`. `collect_local_var_names`
|
|
17111
|
+
// sees VarDecl only, so a PARAMETER, a `for-of` variable or a `catch` binding named like
|
|
17112
|
+
// a module const did not disqualify it — and the const fast path in `emit_typed_expr`'s
|
|
17113
|
+
// Index arm is static, with no scope check, so `function f(T: number[], i) { return T[i] }`
|
|
17114
|
+
// read the MODULE `T` and returned the wrong number with no diagnostic. The sibling
|
|
17115
|
+
// numeric-global pass has always used `collect_binding_names` for exactly this reason.
|
|
17116
|
+
Self::collect_binding_names(s, &mut shadow_bindings);
|
|
17117
|
+
}
|
|
17118
|
+
for n in &shadow_bindings {
|
|
17119
|
+
candidates.remove(n);
|
|
16315
17120
|
}
|
|
16316
17121
|
let globals: HashSet<String> = candidates.keys().cloned().collect();
|
|
16317
17122
|
let mut reassigns: Vec<(String, &Expr)> = Vec::new();
|
|
@@ -17546,6 +18351,12 @@ impl Codegen {
|
|
|
17546
18351
|
Some(rt) => Self::ann_is_number(rt),
|
|
17547
18352
|
None => true,
|
|
17548
18353
|
};
|
|
18354
|
+
// THE SECOND SHAPE: an all-`i32` signature, which is what a device with no FPU
|
|
18355
|
+
// wants and what the existing gate excluded. The fixpoint below is unchanged and
|
|
18356
|
+
// still has to prove the body native-safe and every return numeric — the annotation
|
|
18357
|
+
// never decides that on its own (see `liar` in the issue).
|
|
18358
|
+
let i32_ok = Self::fn_sig_all_i32(params, return_type);
|
|
18359
|
+
let (params_ok, ret_ok) = (params_ok || i32_ok, ret_ok || i32_ok);
|
|
17549
18360
|
// #320: 0-param numeric fns (e.g. k_nucleotide's `nextBase()` — mutates a numeric
|
|
17550
18361
|
// global, returns number) are eligible too; the fixpoint below still proves the body
|
|
17551
18362
|
// native-safe + all-numeric-returns, so `fn name_native() -> f64` is sound.
|
|
@@ -17939,6 +18750,27 @@ impl Codegen {
|
|
|
17939
18750
|
}
|
|
17940
18751
|
}
|
|
17941
18752
|
|
|
18753
|
+
/// Can a top-level native `fn` name this indexed object? A parameter, a global with a static,
|
|
18754
|
+
/// a native-vec, or a local it declared itself — anything else is a binding that only exists
|
|
18755
|
+
/// inside `run()`.
|
|
18756
|
+
fn native_safe_indexable(
|
|
18757
|
+
object: &Expr,
|
|
18758
|
+
params: &std::collections::HashSet<String>,
|
|
18759
|
+
globals: &std::collections::HashSet<String>,
|
|
18760
|
+
nums: &HashSet<String>,
|
|
18761
|
+
native_vec_names: &std::collections::HashSet<String>,
|
|
18762
|
+
) -> bool {
|
|
18763
|
+
match object {
|
|
18764
|
+
Expr::Ident { name, .. } => {
|
|
18765
|
+
params.contains(name.as_ref())
|
|
18766
|
+
|| globals.contains(name.as_ref())
|
|
18767
|
+
|| nums.contains(name.as_ref())
|
|
18768
|
+
|| native_vec_names.contains(name.as_ref())
|
|
18769
|
+
}
|
|
18770
|
+
_ => false,
|
|
18771
|
+
}
|
|
18772
|
+
}
|
|
18773
|
+
|
|
17942
18774
|
fn native_safe_expr(
|
|
17943
18775
|
expr: &Expr,
|
|
17944
18776
|
params: &std::collections::HashSet<String>,
|
|
@@ -17961,24 +18793,45 @@ impl Codegen {
|
|
|
17961
18793
|
&& Self::native_safe_expr(value, params, cand, globals, nums, native_vec_names)
|
|
17962
18794
|
}
|
|
17963
18795
|
Expr::Binary { left, op, right, .. } => {
|
|
17964
|
-
matches!(
|
|
18796
|
+
let arith = matches!(
|
|
17965
18797
|
op,
|
|
17966
18798
|
BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow
|
|
17967
18799
|
| BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge
|
|
17968
18800
|
| BinOp::StrictEq | BinOp::StrictNe | BinOp::And | BinOp::Or
|
|
17969
|
-
)
|
|
18801
|
+
);
|
|
18802
|
+
// Bitwise, unlike in `numeric_shaped`, still recurses into its operands: there the
|
|
18803
|
+
// question is what the RESULT is (always a number), here it is whether the emitter
|
|
18804
|
+
// can lower the whole expression natively. Gated with its sibling so that off the
|
|
18805
|
+
// GBA numeric vocabulary eligibility is byte-identical to before.
|
|
18806
|
+
let bitwise = crate::types::gba_numerics_enabled()
|
|
18807
|
+
&& matches!(
|
|
18808
|
+
op,
|
|
18809
|
+
BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor
|
|
18810
|
+
| BinOp::Shl | BinOp::Shr | BinOp::UShr
|
|
18811
|
+
);
|
|
18812
|
+
(arith || bitwise)
|
|
18813
|
+
&& Self::native_safe_expr(left, params, cand, globals, nums, native_vec_names)
|
|
17970
18814
|
&& Self::native_safe_expr(right, params, cand, globals, nums, native_vec_names)
|
|
17971
18815
|
}
|
|
17972
18816
|
Expr::Unary { op, operand, .. } => {
|
|
17973
|
-
matches!(op, UnaryOp::Neg | UnaryOp::Pos | UnaryOp::Not)
|
|
18817
|
+
(matches!(op, UnaryOp::Neg | UnaryOp::Pos | UnaryOp::Not)
|
|
18818
|
+
|| (crate::types::gba_numerics_enabled()
|
|
18819
|
+
&& matches!(op, UnaryOp::BitNot)))
|
|
17974
18820
|
&& Self::native_safe_expr(operand, params, cand, globals, nums, native_vec_names)
|
|
17975
18821
|
}
|
|
17976
18822
|
Expr::PostfixInc { name, .. }
|
|
17977
18823
|
| Expr::PrefixInc { name, .. }
|
|
17978
18824
|
| Expr::PostfixDec { name, .. }
|
|
17979
18825
|
| Expr::PrefixDec { name, .. } => nums.contains(name.as_ref()),
|
|
18826
|
+
// THE INDEXED THING MUST BE IN SCOPE FOR A FREE `fn`, not merely be an identifier.
|
|
18827
|
+
// This accepted any name at all, and a native fn is emitted at top level where a
|
|
18828
|
+
// module-scope binding — a `let` inside `run()`, captured by the boxed closures — does
|
|
18829
|
+
// not exist: `function reads(i: i32): i32 { return ARR[i] & 255 }` compiled to a
|
|
18830
|
+
// `reads_native` whose body named `ARR`, and rustc rejected the generated program with
|
|
18831
|
+
// E0425. Masked until now because the globals set was only ever populated with things
|
|
18832
|
+
// that DO have a top-level static.
|
|
17980
18833
|
Expr::Index { object, index, .. } => {
|
|
17981
|
-
|
|
18834
|
+
Self::native_safe_indexable(object, params, globals, nums, native_vec_names)
|
|
17982
18835
|
&& Self::native_safe_expr(index, params, cand, globals, nums, native_vec_names)
|
|
17983
18836
|
}
|
|
17984
18837
|
Expr::IndexAssign {
|
|
@@ -17987,7 +18840,7 @@ impl Codegen {
|
|
|
17987
18840
|
value,
|
|
17988
18841
|
..
|
|
17989
18842
|
} => {
|
|
17990
|
-
|
|
18843
|
+
Self::native_safe_indexable(object, params, globals, nums, native_vec_names)
|
|
17991
18844
|
&& Self::native_safe_expr(index, params, cand, globals, nums, native_vec_names)
|
|
17992
18845
|
&& Self::native_safe_expr(value, params, cand, globals, nums, native_vec_names)
|
|
17993
18846
|
}
|
|
@@ -18067,6 +18920,26 @@ impl Codegen {
|
|
|
18067
18920
|
|| globals.contains(name.as_ref())
|
|
18068
18921
|
|| nums.contains(name.as_ref())
|
|
18069
18922
|
}
|
|
18923
|
+
// A BITWISE RESULT IS A NUMBER WHATEVER WENT IN. `& | ^ << >> >>>` are ToInt32/ToUint32
|
|
18924
|
+
// contexts in the language, so `"7" & 255` is 7 and `{} | 0` is 0 — the operands need
|
|
18925
|
+
// no proof of their own, which is why this arm does not recurse into them. Their
|
|
18926
|
+
// absence is not a small omission on a device with no FPU: `(a + b) | 0`, `a & 255` and
|
|
18927
|
+
// `w >> 16` are what integer code on such a target is made of, and every function
|
|
18928
|
+
// containing one was ineligible for a native `fn` no matter how it was annotated.
|
|
18929
|
+
Expr::Binary { op, .. }
|
|
18930
|
+
if crate::types::gba_numerics_enabled()
|
|
18931
|
+
&& matches!(
|
|
18932
|
+
op,
|
|
18933
|
+
BinOp::BitAnd
|
|
18934
|
+
| BinOp::BitOr
|
|
18935
|
+
| BinOp::BitXor
|
|
18936
|
+
| BinOp::Shl
|
|
18937
|
+
| BinOp::Shr
|
|
18938
|
+
| BinOp::UShr
|
|
18939
|
+
) =>
|
|
18940
|
+
{
|
|
18941
|
+
true
|
|
18942
|
+
}
|
|
18070
18943
|
Expr::Binary { left, op, right, .. } => {
|
|
18071
18944
|
matches!(
|
|
18072
18945
|
op,
|
|
@@ -18074,6 +18947,8 @@ impl Codegen {
|
|
|
18074
18947
|
) && Self::numeric_shaped(left, params, cand, globals, nums)
|
|
18075
18948
|
&& Self::numeric_shaped(right, params, cand, globals, nums)
|
|
18076
18949
|
}
|
|
18950
|
+
// Unary `~` is ToInt32 for the same reason.
|
|
18951
|
+
Expr::Unary { op: UnaryOp::BitNot, .. } if crate::types::gba_numerics_enabled() => true,
|
|
18077
18952
|
Expr::Unary { op, operand, .. } => {
|
|
18078
18953
|
matches!(op, UnaryOp::Neg | UnaryOp::Pos)
|
|
18079
18954
|
&& Self::numeric_shaped(operand, params, cand, globals, nums)
|
|
@@ -18220,6 +19095,46 @@ impl Codegen {
|
|
|
18220
19095
|
/// guarded fn is copy 0, the one with the stack check, so every entry from outside the cycle is
|
|
18221
19096
|
/// checked. Inside rotation copy `i`, an intra-SCC call targets the callee's copy `i+1` (the
|
|
18222
19097
|
/// checked copy 0 again after K levels).
|
|
19098
|
+
/// Bring one argument to a native fn's parameter type.
|
|
19099
|
+
///
|
|
19100
|
+
/// The f64 ABI accepts an `i32` expression as-is (Rust widens on the `as` the emitter already
|
|
19101
|
+
/// writes); the i32 ABI has to ToInt32 an f64 one, which is exactly the coercion the boxed
|
|
19102
|
+
/// path applied on entry — moved to the caller, where the value is usually already an integer
|
|
19103
|
+
/// and the conversion folds away.
|
|
19104
|
+
fn coerce_native_arg(&self, code: &str, from: RustType, want: RustType) -> String {
|
|
19105
|
+
if from == want {
|
|
19106
|
+
return code.to_string();
|
|
19107
|
+
}
|
|
19108
|
+
match (from, want) {
|
|
19109
|
+
(RustType::Value, w) => w.from_value_expr(code),
|
|
19110
|
+
(RustType::I32, RustType::F64) => format!("(({}) as f64)", code),
|
|
19111
|
+
// FIXED IS NOT AN f64 AND THE FALLTHROUGH BELOW USED TO PRETEND IT WAS. `fixed` is agb's
|
|
19112
|
+
// `Num<i32,8>`; handed to a native `f64` parameter it is a hard E0308 in the generated
|
|
19113
|
+
// program, not a slow path — which is how four call sites in one file of hyrule stopped
|
|
19114
|
+
// that game building the moment its helper became M5-eligible. The scaling is the same
|
|
19115
|
+
// one `RustType::Fixed`'s Value boundary uses (`types.rs:603`), so a value crossing here
|
|
19116
|
+
// reads identically to one crossing there.
|
|
19117
|
+
(RustType::Fixed, RustType::F64) => format!("(({}).to_raw() as f64 / 256.0)", code),
|
|
19118
|
+
// And the other direction, for the same reason: a native f64 reaching a `fixed`
|
|
19119
|
+
// parameter. Truncating, matching `Fixed::from_value_expr` (`types.rs:507`).
|
|
19120
|
+
(RustType::F64, RustType::Fixed) => format!(
|
|
19121
|
+
"tishlang_runtime::Fixed::from_raw((({}) * 256.0) as i32)",
|
|
19122
|
+
code
|
|
19123
|
+
),
|
|
19124
|
+
(RustType::I32, RustType::Fixed) => format!(
|
|
19125
|
+
"tishlang_runtime::Fixed::from_raw((({}) as i32).wrapping_mul(256))",
|
|
19126
|
+
code
|
|
19127
|
+
),
|
|
19128
|
+
// DIVIDE, DO NOT SHIFT. `>> 8` is an arithmetic shift and FLOORS: fixed -1.5 would
|
|
19129
|
+
// become -2 and -0.0039 would become -1. Rust integer division truncates toward zero,
|
|
19130
|
+
// which is what ToInt32 and `as i32` do, so -1.5 -> -1 and -0.0039 -> 0. The two agree
|
|
19131
|
+
// on every non-negative value, which is exactly why this would have shipped.
|
|
19132
|
+
(RustType::Fixed, RustType::I32) => format!("(({}).to_raw() / 256)", code),
|
|
19133
|
+
(_, RustType::I32) => format!("tishlang_runtime::to_int32({})", code),
|
|
19134
|
+
_ => code.to_string(),
|
|
19135
|
+
}
|
|
19136
|
+
}
|
|
19137
|
+
|
|
18223
19138
|
fn native_call_target(&self, fname: &str) -> String {
|
|
18224
19139
|
let base = format!("{}_native", Self::escape_ident(fname));
|
|
18225
19140
|
if let (Some((cur_scc, i, k)), Some(&(callee_scc, _))) =
|
|
@@ -18241,11 +19156,13 @@ impl Codegen {
|
|
|
18241
19156
|
if !self.native_fns.contains(name.as_ref()) {
|
|
18242
19157
|
continue;
|
|
18243
19158
|
}
|
|
19159
|
+
let i32_abi = self.native_fns_i32.contains(name.as_ref());
|
|
19160
|
+
let abi = if i32_abi { "i32" } else { "f64" };
|
|
18244
19161
|
let plist: Vec<String> = params
|
|
18245
19162
|
.iter()
|
|
18246
19163
|
.filter_map(|p| match p {
|
|
18247
19164
|
FunParam::Simple(tp) => {
|
|
18248
|
-
Some(format!("mut {}:
|
|
19165
|
+
Some(format!("mut {}: {}", Self::escape_ident(tp.name.as_ref()), abi))
|
|
18249
19166
|
}
|
|
18250
19167
|
_ => None,
|
|
18251
19168
|
})
|
|
@@ -18265,18 +19182,30 @@ impl Codegen {
|
|
|
18265
19182
|
self.type_context.push_scope();
|
|
18266
19183
|
for p in params {
|
|
18267
19184
|
if let FunParam::Simple(tp) = p {
|
|
18268
|
-
self.type_context.define(
|
|
19185
|
+
self.type_context.define(
|
|
19186
|
+
tp.name.as_ref(),
|
|
19187
|
+
if i32_abi { RustType::I32 } else { RustType::F64 },
|
|
19188
|
+
);
|
|
18269
19189
|
}
|
|
18270
19190
|
}
|
|
18271
19191
|
let copy_suffix = if copy_idx == 0 { String::new() } else { format!("_r{}", copy_idx) };
|
|
18272
|
-
self.writeln(&format!("fn {}_native{}({}) ->
|
|
19192
|
+
self.writeln(&format!("fn {}_native{}({}) -> {} {{", Self::escape_ident(name.as_ref()), copy_suffix, plist.join(", "), abi));
|
|
19193
|
+
let saved_i32_abi = self.native_fn_abi_i32;
|
|
19194
|
+
self.native_fn_abi_i32 = i32_abi;
|
|
18273
19195
|
self.indent += 1;
|
|
18274
19196
|
if copies > 1 {
|
|
18275
19197
|
self.native_recur_rotation = Some((scc_id, copy_idx, copies));
|
|
18276
19198
|
if copy_idx == 0 {
|
|
18277
19199
|
// The guard: one thread-local read + pointer compare; the cold bail parks
|
|
18278
19200
|
// the catchable RangeError and unwinds via the NaN sentinel (#381).
|
|
18279
|
-
|
|
19201
|
+
// The f64 bail unwinds through a NaN sentinel, which an `i32` return has no
|
|
19202
|
+
// room for. `compute_native_recur_fns` therefore never gives an i32-ABI fn
|
|
19203
|
+
// rotation copies — see the guard where it is called.
|
|
19204
|
+
self.writeln(if i32_abi {
|
|
19205
|
+
"if tishlang_runtime::stack_low() { return 0; }"
|
|
19206
|
+
} else {
|
|
19207
|
+
"if tishlang_runtime::stack_low() { return tishlang_runtime::recursion_tripped_f64(); }"
|
|
19208
|
+
});
|
|
18280
19209
|
}
|
|
18281
19210
|
}
|
|
18282
19211
|
if let Some((global, mul, add, modulus)) = self.native_lcg_fns.get(name.as_ref()) {
|
|
@@ -18348,10 +19277,11 @@ impl Codegen {
|
|
|
18348
19277
|
self.native_lcg_hoist.take();
|
|
18349
19278
|
self.native_lcg_hoist_int = false;
|
|
18350
19279
|
if !self.native_fn_body_returned {
|
|
18351
|
-
self.writeln("0.0");
|
|
19280
|
+
self.writeln(if i32_abi { "0" } else { "0.0" });
|
|
18352
19281
|
}
|
|
18353
19282
|
self.native_fn_emit_name = None;
|
|
18354
19283
|
}
|
|
19284
|
+
self.native_fn_abi_i32 = saved_i32_abi;
|
|
18355
19285
|
self.indent -= 1;
|
|
18356
19286
|
self.writeln("}");
|
|
18357
19287
|
self.type_context.pop_scope();
|
|
@@ -18408,7 +19338,20 @@ impl Codegen {
|
|
|
18408
19338
|
self.skip_iter_local = Some(iter);
|
|
18409
19339
|
}
|
|
18410
19340
|
}
|
|
18411
|
-
|
|
19341
|
+
// A NATIVE FN BODY IS A SCOPE, and it has to say so. `VarDecl` registers every
|
|
19342
|
+
// name it emits into `outer_vars_stack.last_mut()`; without a frame of its own,
|
|
19343
|
+
// a native fn's locals land in the MODULE frame — where they do not exist, because
|
|
19344
|
+
// the body was emitted inside `fn name_native(..)` at top level. Any later closure
|
|
19345
|
+
// referencing a same-named local of its own then sees the phantom in `outer_vars`
|
|
19346
|
+
// and emits `let i_cell = VmRef::new(i.clone());` against nothing: E0425, 49 of the
|
|
19347
|
+
// 114 tish-gba examples, every one of them with an `i` or `b` loop counter.
|
|
19348
|
+
//
|
|
19349
|
+
// The boxed FunDecl path has always pushed here (both of its arms do); this path
|
|
19350
|
+
// was written without one and only became reachable when M5 was enabled for GBA.
|
|
19351
|
+
self.outer_vars_stack.push(Vec::new());
|
|
19352
|
+
let r = self.emit_statements_with_folds(statements);
|
|
19353
|
+
self.outer_vars_stack.pop();
|
|
19354
|
+
r?;
|
|
18412
19355
|
if self.pending_stayed_var.is_none() {
|
|
18413
19356
|
self.skip_iter_local = None;
|
|
18414
19357
|
}
|
|
@@ -18416,10 +19359,31 @@ impl Codegen {
|
|
|
18416
19359
|
Statement::Return { value, .. } => {
|
|
18417
19360
|
let e = value.as_ref().expect("eligible return has a value");
|
|
18418
19361
|
let (code, ty) = self.emit_typed_expr(e)?;
|
|
18419
|
-
let
|
|
19362
|
+
let want = if self.native_fn_abi_i32 { RustType::I32 } else { RustType::F64 };
|
|
19363
|
+
// The int-domain shortcut FIRST for an i32 return. A bitwise expression types as
|
|
19364
|
+
// `F64` by design (`types.rs`), so taking the generic path below would emit
|
|
19365
|
+
// `to_int32(<i32 work> as f64)` — the exact round trip this ABI exists to remove,
|
|
19366
|
+
// reintroduced at the return. `emit_int32_operand` is the same helper the operator
|
|
19367
|
+
// fixes use.
|
|
19368
|
+
if want == RustType::I32 {
|
|
19369
|
+
if let Some(code) = self.emit_int32_operand(e)? {
|
|
19370
|
+
if let Some((global, _, _, _)) = self.native_lcg_hoist.as_ref() {
|
|
19371
|
+
let _ = global;
|
|
19372
|
+
}
|
|
19373
|
+
self.writeln(&format!("return {};", code));
|
|
19374
|
+
self.native_fn_body_returned = true;
|
|
19375
|
+
return Ok(());
|
|
19376
|
+
}
|
|
19377
|
+
}
|
|
19378
|
+
let (code, ty) = (code, ty);
|
|
19379
|
+
let f = if ty == want {
|
|
18420
19380
|
code
|
|
18421
19381
|
} else if ty == RustType::Value {
|
|
18422
|
-
|
|
19382
|
+
want.from_value_expr(&code)
|
|
19383
|
+
} else if want == RustType::I32 {
|
|
19384
|
+
// An f64-typed expression returned from an `i32` fn takes the same ToInt32 the
|
|
19385
|
+
// boxed path would have applied at the consumer.
|
|
19386
|
+
format!("tishlang_runtime::to_int32({})", code)
|
|
18423
19387
|
} else {
|
|
18424
19388
|
code
|
|
18425
19389
|
};
|
|
@@ -19056,6 +20020,17 @@ impl Codegen {
|
|
|
19056
20020
|
// fn either — its scalar params would be typed `f64` and mistype the arg (e.g. `fn rec(m){
|
|
19057
20021
|
// log.push(m) }` called `rec("ran")`). Same gate the M4/M5/S-0 paths use.
|
|
19058
20022
|
let nonnumeric = Self::fns_called_with_nonnumeric_arg(&program.statements);
|
|
20023
|
+
// Every top-level fn name. A CALL to one is validated separately (`vec_body_calls_native`);
|
|
20024
|
+
// what matters here is that the identifier itself is not a `run()` local the way a module
|
|
20025
|
+
// `let` or a builtin like `Math` is.
|
|
20026
|
+
let fn_names: std::collections::HashSet<String> = program
|
|
20027
|
+
.statements
|
|
20028
|
+
.iter()
|
|
20029
|
+
.filter_map(|s| match s {
|
|
20030
|
+
Statement::FunDecl { name, .. } => Some(name.to_string()),
|
|
20031
|
+
_ => None,
|
|
20032
|
+
})
|
|
20033
|
+
.collect();
|
|
19059
20034
|
for s in &program.statements {
|
|
19060
20035
|
if let Statement::FunDecl {
|
|
19061
20036
|
async_: false,
|
|
@@ -19082,7 +20057,28 @@ impl Codegen {
|
|
|
19082
20057
|
.params
|
|
19083
20058
|
.iter()
|
|
19084
20059
|
.any(|(_, k)| matches!(k, VecParamKind::Array { .. }));
|
|
19085
|
-
|
|
20060
|
+
// #605: a scalars-in/STRING-out fn has neither an array parameter nor a local
|
|
20061
|
+
// vec op, so it never reached this group and stayed a boxed closure — which is
|
|
20062
|
+
// what "VecRetKind has no String" amounts to in practice. The return shape earns
|
|
20063
|
+
// admission on its own, but ONLY under three extra conditions, each of which a
|
|
20064
|
+
// review found by breaking the version without it:
|
|
20065
|
+
//
|
|
20066
|
+
// 1. TOTAL. `vec_fn_return_kind` rejects an explicit bare `return;` but cannot
|
|
20067
|
+
// see an IMPLICIT fall-through, and the lowered fn's tail yields
|
|
20068
|
+
// `String::new()` where the boxed one yields `Value::Null` — so
|
|
20069
|
+
// `function label(n){ if(n>0){return "pos"} }` returned "" instead of null.
|
|
20070
|
+
// A wrong value, silently, from ordinary code.
|
|
20071
|
+
// 2. NO STRING-COMPARED PARAM. `scan_param_use` counts `===` as arithmetic, so
|
|
20072
|
+
// `tag === "fire"` "proves" `tag` numeric and the param takes the f64 ABI —
|
|
20073
|
+
// a `panic!("expected number")` on a no_std cart, or E0308. (infer.rs treats
|
|
20074
|
+
// the same operator as OVERLOADED for exactly this reason.)
|
|
20075
|
+
// 3. NO MODULE READS, on EVERY target. A string or object module binding has a
|
|
20076
|
+
// top-level home nowhere, and this is the first rule that admits a fn shaped
|
|
20077
|
+
// to read one, so the GBA-only gate below is not enough.
|
|
20078
|
+
let string_out = sig.ret == VecRetKind::Str
|
|
20079
|
+
&& Self::body_always_returns(body)
|
|
20080
|
+
&& Self::body_names_only_its_own(params, body, &fn_names);
|
|
20081
|
+
if has_array_param || Self::body_uses_local_vec_ops(body) || string_out {
|
|
19086
20082
|
sigs.insert(name.to_string(), sig);
|
|
19087
20083
|
}
|
|
19088
20084
|
}
|
|
@@ -19594,6 +20590,100 @@ impl Codegen {
|
|
|
19594
20590
|
None
|
|
19595
20591
|
}
|
|
19596
20592
|
|
|
20593
|
+
/// Does `body` name ONLY things a free `fn` can see — its own parameters, its own locals, and
|
|
20594
|
+
/// other top-level fns? Anything else (a module `let`, or a builtin like `Math`/`console`) is a
|
|
20595
|
+
/// local of `run()` in the emitted program, and a free fn naming one is E0425.
|
|
20596
|
+
///
|
|
20597
|
+
/// Stricter than the module-binding check it replaced for this door, and deliberately so:
|
|
20598
|
+
/// `regex_redux`'s `nextBase()` is scalars-in/string-out, reaches for `Math.floor`, and the
|
|
20599
|
+
/// native-vec emitter lowers that to a `get_prop` against the `Math` binding — which is not a
|
|
20600
|
+
/// module `let`, so a module-only guard waved it through and the fixture stopped building.
|
|
20601
|
+
fn body_names_only_its_own(
|
|
20602
|
+
params: &[FunParam],
|
|
20603
|
+
body: &Statement,
|
|
20604
|
+
fn_names: &std::collections::HashSet<String>,
|
|
20605
|
+
) -> bool {
|
|
20606
|
+
let mut referenced = HashSet::new();
|
|
20607
|
+
Self::collect_stmt_idents(body, &mut referenced);
|
|
20608
|
+
let mut bound: HashSet<String> = params
|
|
20609
|
+
.iter()
|
|
20610
|
+
.flat_map(|p| p.bound_names())
|
|
20611
|
+
.map(|n| n.to_string())
|
|
20612
|
+
.collect();
|
|
20613
|
+
Self::collect_local_var_names(body, &mut bound);
|
|
20614
|
+
referenced
|
|
20615
|
+
.iter()
|
|
20616
|
+
.all(|n| bound.contains(n) || fn_names.contains(n))
|
|
20617
|
+
}
|
|
20618
|
+
|
|
20619
|
+
/// Does every path through `body` end in an explicit `return <expr>`? Conservative: only a
|
|
20620
|
+
/// trailing `return`, or an if/else where BOTH arms are total, counts. Anything else may fall
|
|
20621
|
+
/// off the end, and a lowered fn's tail default (`String::new()` / `0.0`) is not what the boxed
|
|
20622
|
+
/// path yields there (`Value::Null`).
|
|
20623
|
+
fn body_always_returns(body: &Statement) -> bool {
|
|
20624
|
+
match body {
|
|
20625
|
+
Statement::Return { value: Some(_), .. } => true,
|
|
20626
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
20627
|
+
statements.last().is_some_and(Self::body_always_returns)
|
|
20628
|
+
}
|
|
20629
|
+
Statement::If { then_branch, else_branch, .. } => {
|
|
20630
|
+
else_branch.as_ref().is_some_and(|e| {
|
|
20631
|
+
Self::body_always_returns(then_branch) && Self::body_always_returns(e)
|
|
20632
|
+
})
|
|
20633
|
+
}
|
|
20634
|
+
_ => false,
|
|
20635
|
+
}
|
|
20636
|
+
}
|
|
20637
|
+
|
|
20638
|
+
/// Every module-level `let`/`const` name. On GBA these are locals of `run()` — nothing gives
|
|
20639
|
+
/// them a top-level home — so a free `fn` naming one does not compile.
|
|
20640
|
+
fn collect_module_level_binding_names(
|
|
20641
|
+
statements: &[Statement],
|
|
20642
|
+
) -> std::collections::HashSet<String> {
|
|
20643
|
+
let mut out = std::collections::HashSet::new();
|
|
20644
|
+
for s in statements {
|
|
20645
|
+
Self::collect_module_binding_names_of(s, &mut out);
|
|
20646
|
+
}
|
|
20647
|
+
out
|
|
20648
|
+
}
|
|
20649
|
+
|
|
20650
|
+
/// One statement's module-level bindings. `Multi` matters: `let a = 1, b = 2` parses to it, and
|
|
20651
|
+
/// codegen emits each declarator into the CURRENT scope — so at module level those are `run()`
|
|
20652
|
+
/// locals exactly like a plain `let`, and missing them let a free fn name one (E0425).
|
|
20653
|
+
fn collect_module_binding_names_of(s: &Statement, out: &mut std::collections::HashSet<String>) {
|
|
20654
|
+
match s {
|
|
20655
|
+
Statement::VarDecl { name, .. } => {
|
|
20656
|
+
out.insert(name.to_string());
|
|
20657
|
+
}
|
|
20658
|
+
Statement::VarDeclDestructure { pattern, .. } => {
|
|
20659
|
+
Self::collect_destruct_names(pattern, out);
|
|
20660
|
+
}
|
|
20661
|
+
Statement::Multi { statements, .. } => {
|
|
20662
|
+
for inner in statements {
|
|
20663
|
+
Self::collect_module_binding_names_of(inner, out);
|
|
20664
|
+
}
|
|
20665
|
+
}
|
|
20666
|
+
_ => {}
|
|
20667
|
+
}
|
|
20668
|
+
}
|
|
20669
|
+
|
|
20670
|
+
/// Does `body` read a module binding it would not be able to see as a free `fn`? Names the body
|
|
20671
|
+
/// binds itself (params are checked by the caller; locals here) shadow the module one, so they
|
|
20672
|
+
/// do not count — that is the same shadow rule #556 established for parameters.
|
|
20673
|
+
fn body_reads_module_binding(
|
|
20674
|
+
body: &Statement,
|
|
20675
|
+
module_bindings: &std::collections::HashSet<String>,
|
|
20676
|
+
) -> bool {
|
|
20677
|
+
// NO SHADOW SUBTRACTION, deliberately. `collect_local_var_names` flattens every nested
|
|
20678
|
+
// block, branch and loop into one set with no scope structure, so an inner
|
|
20679
|
+
// `if (..) { let CFG = 0 }` would mask a module `CFG` read elsewhere in the body and let the
|
|
20680
|
+
// fn lower with a free name (E0425). Over-rejecting costs a fn its lowering; under-rejecting
|
|
20681
|
+
// costs the whole program its build.
|
|
20682
|
+
let mut referenced = HashSet::new();
|
|
20683
|
+
Self::collect_stmt_idents(body, &mut referenced);
|
|
20684
|
+
referenced.iter().any(|n| module_bindings.contains(n))
|
|
20685
|
+
}
|
|
20686
|
+
|
|
19597
20687
|
/// Detect, validate, and emit the native-vec fn group. On any gap the group is left empty.
|
|
19598
20688
|
fn setup_native_vec_fns(&mut self, program: &Program) {
|
|
19599
20689
|
use std::collections::HashMap;
|
|
@@ -19601,6 +20691,55 @@ impl Codegen {
|
|
|
19601
20691
|
// #177 aggregate fns (`advance`/`offsetMomentum` on `Vec<Struct>`) take precedence — do not
|
|
19602
20692
|
// also emit a broken `name_nv(&Vec<f64>)` wrapper for them.
|
|
19603
20693
|
sigs.retain(|name, _| !self.aggregate_fns.contains_key(name));
|
|
20694
|
+
// ON GBA, A MODULE BINDING IS NOT REACHABLE FROM A FREE `fn`. The passes that give module
|
|
20695
|
+
// state a top-level home — `native_numeric_globals`' TLS cells and `module_const_f64_arrays`'
|
|
20696
|
+
// statics — are the std-emitting ones, and they are gated off for GBA. So every module
|
|
20697
|
+
// `let`/`const` is a local of `run()` there, while `name_nv` is emitted at top level: a body
|
|
20698
|
+
// that reads one compiles to `error[E0425]: cannot find value CFG in this scope` and the
|
|
20699
|
+
// game does not build (#594).
|
|
20700
|
+
//
|
|
20701
|
+
// Reject those fns here rather than emitting a broken program. They stay on the boxed path,
|
|
20702
|
+
// which is correct and merely slower. Scoped to GBA deliberately: off-target the statics DO
|
|
20703
|
+
// exist, the reference resolves, and eligibility must stay byte-identical to before.
|
|
20704
|
+
//
|
|
20705
|
+
// This does not close #594's other half — a lowered fn that can actually SEE module state,
|
|
20706
|
+
// which needs those bindings emitted as statics on GBA too. It stops the miscompile.
|
|
20707
|
+
if self.emit_mode == crate::NativeEmitMode::Gba {
|
|
20708
|
+
let mut module_bindings = Self::collect_module_level_binding_names(&program.statements);
|
|
20709
|
+
// …EXCEPT the ones that now have a top-level home. Since #594 a module const numeric
|
|
20710
|
+
// array is a `const` and a mutable numeric global is a `SingleCore` static, both
|
|
20711
|
+
// reachable from a free fn — rejecting a fn for reading those would give up exactly the
|
|
20712
|
+
// lowering this issue exists to enable. What remains unreachable is everything else:
|
|
20713
|
+
// boxed objects, strings, arrays of non-numbers.
|
|
20714
|
+
for k in self.module_const_f64_arrays.keys() {
|
|
20715
|
+
module_bindings.remove(k);
|
|
20716
|
+
}
|
|
20717
|
+
for k in self.native_numeric_globals.keys() {
|
|
20718
|
+
module_bindings.remove(k);
|
|
20719
|
+
}
|
|
20720
|
+
for k in self.module_const_f64_cum.keys() {
|
|
20721
|
+
module_bindings.remove(k);
|
|
20722
|
+
}
|
|
20723
|
+
for k in self.module_const_f64_aliases.keys() {
|
|
20724
|
+
module_bindings.remove(k);
|
|
20725
|
+
}
|
|
20726
|
+
if !module_bindings.is_empty() {
|
|
20727
|
+
let bodies_by_name: std::collections::HashMap<&str, &Statement> = program
|
|
20728
|
+
.statements
|
|
20729
|
+
.iter()
|
|
20730
|
+
.filter_map(|s| match s {
|
|
20731
|
+
Statement::FunDecl { name, body, .. } => Some((name.as_ref(), body.as_ref())),
|
|
20732
|
+
_ => None,
|
|
20733
|
+
})
|
|
20734
|
+
.collect();
|
|
20735
|
+
sigs.retain(|name, _| {
|
|
20736
|
+
let Some(body) = bodies_by_name.get(name.as_str()) else {
|
|
20737
|
+
return true;
|
|
20738
|
+
};
|
|
20739
|
+
!Self::body_reads_module_binding(body, &module_bindings)
|
|
20740
|
+
});
|
|
20741
|
+
}
|
|
20742
|
+
}
|
|
19604
20743
|
if sigs.is_empty() {
|
|
19605
20744
|
return;
|
|
19606
20745
|
}
|
|
@@ -19929,7 +21068,7 @@ impl Codegen {
|
|
|
19929
21068
|
}
|
|
19930
21069
|
}
|
|
19931
21070
|
Expr::Binary { left, op, right, .. } => {
|
|
19932
|
-
let
|
|
21071
|
+
let arith = matches!(
|
|
19933
21072
|
op,
|
|
19934
21073
|
BinOp::Add
|
|
19935
21074
|
| BinOp::Sub
|
|
@@ -19941,9 +21080,17 @@ impl Codegen {
|
|
|
19941
21080
|
| BinOp::Le
|
|
19942
21081
|
| BinOp::Gt
|
|
19943
21082
|
| BinOp::Ge
|
|
19944
|
-
| BinOp::StrictEq
|
|
19945
|
-
| BinOp::StrictNe
|
|
19946
21083
|
);
|
|
21084
|
+
// `===`/`!==` ARE OVERLOADED — they compare strings as readily as numbers, so one is
|
|
21085
|
+
// proof of a numeric operand ONLY when the other side is a numeric literal.
|
|
21086
|
+
// `tag === "fire"` used to mark `tag` numeric, which handed a string parameter the
|
|
21087
|
+
// f64 ABI: `panic!("expected number")` on a no_std cart, or E0308. `infer.rs`
|
|
21088
|
+
// classifies the same operator as overloaded for exactly this reason, and the two
|
|
21089
|
+
// disagreeing is what let it through here.
|
|
21090
|
+
let num_lit = |x: &Expr| matches!(x, Expr::Literal { value: Literal::Number(_), .. });
|
|
21091
|
+
let eq_numeric = matches!(op, BinOp::StrictEq | BinOp::StrictNe)
|
|
21092
|
+
&& (num_lit(left) || num_lit(right));
|
|
21093
|
+
let a = arith || eq_numeric;
|
|
19947
21094
|
Self::scan_param_use(left, p, a, f);
|
|
19948
21095
|
Self::scan_param_use(right, p, a, f);
|
|
19949
21096
|
}
|
|
@@ -20056,6 +21203,17 @@ impl Codegen {
|
|
|
20056
21203
|
{
|
|
20057
21204
|
return Some(VecRetKind::F64);
|
|
20058
21205
|
}
|
|
21206
|
+
// #605: EVERY return a string literal. Held to the same standard as the numeric arm above —
|
|
21207
|
+
// agreement across all returns, proven from the expressions, never from the annotation,
|
|
21208
|
+
// because a return type is erased and unchecked (the `liar()` case). A mixed
|
|
21209
|
+
// string-and-number fn falls through to `None` and stays boxed, which is the only shape a
|
|
21210
|
+
// single native signature cannot express.
|
|
21211
|
+
if ret_exprs
|
|
21212
|
+
.iter()
|
|
21213
|
+
.all(|e| matches!(e, Expr::Literal { value: Literal::String(_), .. }))
|
|
21214
|
+
{
|
|
21215
|
+
return Some(VecRetKind::Str);
|
|
21216
|
+
}
|
|
20059
21217
|
None
|
|
20060
21218
|
}
|
|
20061
21219
|
|
|
@@ -20378,6 +21536,7 @@ impl Codegen {
|
|
|
20378
21536
|
match sig.ret {
|
|
20379
21537
|
VecRetKind::F64 => Some(RustType::F64),
|
|
20380
21538
|
VecRetKind::VecF64 => Some(RustType::Vec(Box::new(RustType::F64))),
|
|
21539
|
+
VecRetKind::Str => Some(RustType::String),
|
|
20381
21540
|
VecRetKind::Unit => None,
|
|
20382
21541
|
}
|
|
20383
21542
|
}
|
|
@@ -20416,6 +21575,7 @@ impl Codegen {
|
|
|
20416
21575
|
let ret_str = match sig.ret {
|
|
20417
21576
|
VecRetKind::F64 => " -> f64",
|
|
20418
21577
|
VecRetKind::VecF64 => " -> Vec<f64>",
|
|
21578
|
+
VecRetKind::Str => " -> String",
|
|
20419
21579
|
VecRetKind::Unit => "",
|
|
20420
21580
|
};
|
|
20421
21581
|
self.writeln("#[allow(non_snake_case, unused)]");
|
|
@@ -20479,6 +21639,11 @@ impl Codegen {
|
|
|
20479
21639
|
if sig.ret == VecRetKind::F64 {
|
|
20480
21640
|
self.writeln("0.0");
|
|
20481
21641
|
}
|
|
21642
|
+
// Same total-function guarantee for the string arm: falling off the end yields "" rather
|
|
21643
|
+
// than leaving the emitted fn without a value on that path.
|
|
21644
|
+
if sig.ret == VecRetKind::Str {
|
|
21645
|
+
self.writeln("String::new()");
|
|
21646
|
+
}
|
|
20482
21647
|
self.native_vec_ret = saved_ret;
|
|
20483
21648
|
self.vec_ref_params = saved_refs;
|
|
20484
21649
|
self.indent -= 1;
|
|
@@ -20569,6 +21734,13 @@ impl Codegen {
|
|
|
20569
21734
|
call
|
|
20570
21735
|
}
|
|
20571
21736
|
}
|
|
21737
|
+
VecRetKind::Str => {
|
|
21738
|
+
if as_value {
|
|
21739
|
+
format!("Value::String(({}).into())", call)
|
|
21740
|
+
} else {
|
|
21741
|
+
call
|
|
21742
|
+
}
|
|
21743
|
+
}
|
|
20572
21744
|
VecRetKind::Unit => {
|
|
20573
21745
|
if as_value {
|
|
20574
21746
|
format!("{{ {}; Value::Null }}", call)
|
|
@@ -21944,6 +23116,14 @@ impl Codegen {
|
|
|
21944
23116
|
/// the routing state so call sites de-virtualize. On any failure the state is left empty (path
|
|
21945
23117
|
/// disabled) and nothing is appended to the output.
|
|
21946
23118
|
fn setup_aggregate_fns(&mut self, program: &Program) {
|
|
23119
|
+
// #177 de-virtualization deletes the boxed `let <name> = Value::native(..)` local for every
|
|
23120
|
+
// routed fn. `RustLib` publishes exactly those locals as the crate's public API, so an
|
|
23121
|
+
// exported fn that got de-virtualized would leave the emitted `__tish_publish_exports` call
|
|
23122
|
+
// referencing a binding that no longer exists. The unboxed path is a numeric fast path;
|
|
23123
|
+
// a correct public surface outranks it.
|
|
23124
|
+
if self.emit_mode == crate::NativeEmitMode::RustLib {
|
|
23125
|
+
return;
|
|
23126
|
+
}
|
|
21947
23127
|
let dbg = std::env::var("TISH_AGG_DEBUG").is_ok();
|
|
21948
23128
|
// The unboxed struct alias: the (unique) name `A` used as an `A[]` array param of some fn,
|
|
21949
23129
|
// and registered as an all-`Copy`-field struct alias.
|
|
@@ -23041,6 +24221,60 @@ impl Codegen {
|
|
|
23041
24221
|
/// the round-trips. Crucially, only bitwise/shift nodes recurse — an `f64` `*`/`+`/`-` node is
|
|
23042
24222
|
/// a *leaf* here, so e.g. `(h * 16777619) >>> 0` keeps its `f64` multiply (the 2^53 rule: the
|
|
23043
24223
|
/// product exceeds 2^53 and must round in `f64` *before* `ToUint32`, exactly as V8 does).
|
|
24224
|
+
/// Int-domain code for a bitwise/shift expression that is about to be consumed by something
|
|
24225
|
+
/// wanting an INTEGER — a `Vec<i32>` element store, an array index.
|
|
24226
|
+
///
|
|
24227
|
+
/// `result_type_of_binop` (types.rs) types every bitwise result `F64`, which is semantically
|
|
24228
|
+
/// right (JS bitwise returns a Number) and a real win against boxing. But the int-domain chain
|
|
24229
|
+
/// is then wrapped `(… ) as f64` and an integer consumer immediately casts it back, so on a
|
|
24230
|
+
/// target with no FPU every `arr[i] = w | BIT` pays a soft-float round trip in each direction.
|
|
24231
|
+
/// Measured on GBA (tish-gba `examples/bench-grid`): `arr[j] = 7` and `arr[j] = j` cost ~1.4
|
|
24232
|
+
/// ticks per 1000, `arr[j] = j & 255` cost 23.2, and a bitwise INDEX cost 19.9 — ~15x, twice per
|
|
24233
|
+
/// cell access for anything built on a packed word.
|
|
24234
|
+
///
|
|
24235
|
+
/// SOUNDNESS. Only the SIGNED-result ops qualify. Their value is by construction inside i32, so
|
|
24236
|
+
/// `(x as f64) as i32` is the identity and dropping both casts cannot change it. `>>>` is
|
|
24237
|
+
/// excluded deliberately: it yields a uint32 Number that can exceed `i32::MAX`, and Rust's
|
|
24238
|
+
/// float→int `as` SATURATES, so the f64 route and the int route genuinely disagree there.
|
|
24239
|
+
fn emit_int32_for_int_consumer(
|
|
24240
|
+
&mut self,
|
|
24241
|
+
e: &Expr,
|
|
24242
|
+
) -> Result<Option<String>, CompileError> {
|
|
24243
|
+
let Expr::Binary { op, .. } = e else {
|
|
24244
|
+
return Ok(None);
|
|
24245
|
+
};
|
|
24246
|
+
if !matches!(
|
|
24247
|
+
op,
|
|
24248
|
+
BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor | BinOp::Shl | BinOp::Shr
|
|
24249
|
+
) {
|
|
24250
|
+
return Ok(None);
|
|
24251
|
+
}
|
|
24252
|
+
self.emit_int32_operand(e)
|
|
24253
|
+
}
|
|
24254
|
+
|
|
24255
|
+
/// An operand that is provably an int32: either one `emit_int32_operand` lowers, or one the
|
|
24256
|
+
/// type system already calls `i32`.
|
|
24257
|
+
///
|
|
24258
|
+
/// The second case is what `emit_int32_operand` alone misses. An `: i32` local and a `Vec<i32>`
|
|
24259
|
+
/// element read need no ToInt32 lowering — they ARE integers — but that function's leaf fold
|
|
24260
|
+
/// only recognises `F64` leaves, so it declines them and falls back to a `to_int32` wrapper.
|
|
24261
|
+
/// Every accumulator has one on its left-hand side.
|
|
24262
|
+
fn int32_or_native_i32(&mut self, e: &Expr) -> Result<Option<String>, CompileError> {
|
|
24263
|
+
if let Some(c) = self.emit_int32_operand(e)? {
|
|
24264
|
+
// Reject the generic fold's own output: if it handed back a `to_int32(..)` wrapper it
|
|
24265
|
+
// has not actually reached the integer domain, and using it would keep the round trip
|
|
24266
|
+
// while claiming to have removed it.
|
|
24267
|
+
if !c.contains("to_int32") {
|
|
24268
|
+
return Ok(Some(c));
|
|
24269
|
+
}
|
|
24270
|
+
}
|
|
24271
|
+
let (code, ty) = self.emit_typed_expr(e)?;
|
|
24272
|
+
if ty == RustType::I32 {
|
|
24273
|
+
return Ok(Some(code));
|
|
24274
|
+
}
|
|
24275
|
+
Ok(None)
|
|
24276
|
+
}
|
|
24277
|
+
|
|
23044
24278
|
fn emit_int32_operand(&mut self, e: &Expr) -> Result<Option<String>, CompileError> {
|
|
23045
24279
|
if let Expr::Binary {
|
|
23046
24280
|
left, op, right, ..
|
|
@@ -23081,6 +24315,33 @@ impl Codegen {
|
|
|
23081
24315
|
};
|
|
23082
24316
|
return Ok(Some(code));
|
|
23083
24317
|
}
|
|
24318
|
+
// ARITHMETIC in a ToInt32 context: `acc + (w & MASK)`, `i - 1`.
|
|
24319
|
+
//
|
|
24320
|
+
// `+` is not itself a ToInt32 context, so this cannot be done in general — but THIS
|
|
24321
|
+
// FUNCTION'S CONTRACT is that its result will be ToInt32'd by the caller, and for int32
|
|
24322
|
+
// operands `to_int32(a + b) == a.wrapping_add(b)` exactly, since |a + b| < 2^32 is far
|
|
24323
|
+
// inside f64's exact-integer range. So inside this context, and only here, the integer
|
|
24324
|
+
// form is equivalent.
|
|
24325
|
+
//
|
|
24326
|
+
// It has to sit ABOVE the generic leaf fold below, which would otherwise swallow the
|
|
24327
|
+
// whole expression and hand back `to_int32((a as f64) + (b as f64))` — technically a
|
|
24328
|
+
// success, and exactly the round trip this exists to remove.
|
|
24329
|
+
//
|
|
24330
|
+
// Measured on GBA (tish-gba `examples/bench-grid`): `acc = acc + (arr[i] & 255)` ran at
|
|
24331
|
+
// 44.4 ticks an iteration against 1.64 for the same read unmasked — a 27x penalty on
|
|
24332
|
+
// every packed-word structure, since `w & FIELD` is how you read a field.
|
|
24333
|
+
if matches!(op, BinOp::Add | BinOp::Sub) {
|
|
24334
|
+
let li = self.int32_or_native_i32(left)?;
|
|
24335
|
+
let ri = self.int32_or_native_i32(right)?;
|
|
24336
|
+
if let (Some(li), Some(ri)) = (li, ri) {
|
|
24337
|
+
let f = if matches!(op, BinOp::Add) {
|
|
24338
|
+
"wrapping_add"
|
|
24339
|
+
} else {
|
|
24340
|
+
"wrapping_sub"
|
|
24341
|
+
};
|
|
24342
|
+
return Ok(Some(format!("({}).{}({})", li, f, ri)));
|
|
24343
|
+
}
|
|
24344
|
+
}
|
|
23084
24345
|
}
|
|
23085
24346
|
// `Math.imul(a, b)` in an int32 context → the raw i32 `wrapping_mul` (no f64 excursion, no
|
|
23086
24347
|
// `to_int32` round-trip on the product), when both operands are themselves int32-lowerable.
|
|
@@ -23656,45 +24917,15 @@ impl Codegen {
|
|
|
23656
24917
|
};
|
|
23657
24918
|
// #173 part 3: prove the index in-bounds BEFORE emitting it.
|
|
23658
24919
|
let in_bounds = self.index_in_bounds(index, name.as_ref());
|
|
23659
|
-
|
|
23660
|
-
|
|
23661
|
-
|
|
23662
|
-
|
|
23663
|
-
|
|
23664
|
-
|
|
23665
|
-
|
|
23666
|
-
|
|
23667
|
-
|
|
23668
|
-
if idx_ty == RustType::F64 || idx_ty == RustType::I32 {
|
|
23669
|
-
format!("({}) as usize", idx_code)
|
|
23670
|
-
} else {
|
|
23671
|
-
let iv = if idx_ty.is_native() {
|
|
23672
|
-
idx_ty.to_value_expr(&idx_code)
|
|
23673
|
-
} else {
|
|
23674
|
-
idx_code
|
|
23675
|
-
};
|
|
23676
|
-
format!(
|
|
23677
|
-
"{{ let _i = &{}; if let Value::Number(n) = _i {{ *n as usize }} else {{ panic!(\"array index must be a number\") }} }}",
|
|
23678
|
-
iv
|
|
23679
|
-
)
|
|
23680
|
-
}
|
|
23681
|
-
}
|
|
23682
|
-
} else {
|
|
23683
|
-
let (idx_code, idx_ty) = self.emit_typed_expr(index)?;
|
|
23684
|
-
if idx_ty == RustType::F64 || idx_ty == RustType::I32 {
|
|
23685
|
-
format!("({}) as usize", idx_code)
|
|
23686
|
-
} else {
|
|
23687
|
-
let iv = if idx_ty.is_native() {
|
|
23688
|
-
idx_ty.to_value_expr(&idx_code)
|
|
23689
|
-
} else {
|
|
23690
|
-
idx_code
|
|
23691
|
-
};
|
|
23692
|
-
format!(
|
|
23693
|
-
"{{ let _i = &{}; if let Value::Number(n) = _i {{ *n as usize }} else {{ panic!(\"array index must be a number\") }} }}",
|
|
23694
|
-
iv
|
|
23695
|
-
)
|
|
23696
|
-
}
|
|
23697
|
-
};
|
|
24920
|
+
// The READ index goes through the same helper as the element STORE.
|
|
24921
|
+
// These two arms used to be a hand-inlined copy of it that predated
|
|
24922
|
+
// the int-domain shortcut, so `arr[a | b]` lowered its store index
|
|
24923
|
+
// integrally and its read index as `((a|b) as f64) as usize` — two
|
|
24924
|
+
// soft-float calls on ARM7TDMI, on the read half of every packed-grid
|
|
24925
|
+
// access. Measured in tish-gba: a 63-cell board copy whose body is
|
|
24926
|
+
// `P[dc|r] = P[sc|r] & keep` cost 2,103 ticks, 33 per cell, against
|
|
24927
|
+
// ~1.6 for a module `i32[]` read plus write.
|
|
24928
|
+
let idx_usize = self.emit_index_usize(index)?;
|
|
23698
24929
|
// #567: inside the wrapped block the index is pre-bound to `__bi`
|
|
23699
24930
|
// (evaluated BEFORE the borrow, so an index that itself reads this cell
|
|
23700
24931
|
// — `arr[arr.length-1]` — doesn't double-lock the non-reentrant cell).
|
|
@@ -23818,6 +25049,7 @@ impl Codegen {
|
|
|
23818
25049
|
let ty = match sig.ret {
|
|
23819
25050
|
VecRetKind::F64 => RustType::F64,
|
|
23820
25051
|
VecRetKind::VecF64 => RustType::Vec(Box::new(RustType::F64)),
|
|
25052
|
+
VecRetKind::Str => RustType::String,
|
|
23821
25053
|
VecRetKind::Unit => RustType::Value,
|
|
23822
25054
|
};
|
|
23823
25055
|
return Ok((code, ty));
|
|
@@ -23825,6 +25057,39 @@ impl Codegen {
|
|
|
23825
25057
|
}
|
|
23826
25058
|
}
|
|
23827
25059
|
}
|
|
25060
|
+
// A user fn whose every return is PROVEN numeric (`collect_user_fn_rets`). The call
|
|
25061
|
+
// stays boxed — this is not a calling convention — but its RESULT stops being
|
|
25062
|
+
// opaque, so the surrounding arithmetic stays integral instead of dropping to the
|
|
25063
|
+
// generic `ops::add` and its two soft-float conversions.
|
|
25064
|
+
//
|
|
25065
|
+
// The `_ =>` arm is unreachable by construction: membership required every return
|
|
25066
|
+
// to be numeric. Written out rather than `unreachable!()` so a proof bug surfaces as
|
|
25067
|
+
// the same diagnostic every other coercion in this file produces.
|
|
25068
|
+
// NOT for fns that also have an M5 native form: the direct-call path below emits
|
|
25069
|
+
// `name_native(..)` bare, and this block reaching them first wrapped that same call
|
|
25070
|
+
// in `Value::Number(..)` and match-unwrapped it straight back — the boxed round
|
|
25071
|
+
// trip the #320 push test pins against. A proof is the fallback, never the winner.
|
|
25072
|
+
if !self.user_fn_ret.is_empty() {
|
|
25073
|
+
if let Expr::Ident { name, .. } = callee.as_ref() {
|
|
25074
|
+
if self.native_fns.contains(name.as_ref()) {
|
|
25075
|
+
// fall through to the native direct-call path
|
|
25076
|
+
} else if let Some(ty) = self.user_fn_ret.get(name.as_ref()).cloned() {
|
|
25077
|
+
let boxed = self.emit_expr(expr)?;
|
|
25078
|
+
let conv = if ty == RustType::I32 {
|
|
25079
|
+
"tishlang_runtime::to_int32(*n)"
|
|
25080
|
+
} else {
|
|
25081
|
+
"*n"
|
|
25082
|
+
};
|
|
25083
|
+
return Ok((
|
|
25084
|
+
format!(
|
|
25085
|
+
"match &{} {{ Value::Number(n) => {}, _ => panic!(\"expected number\") }}",
|
|
25086
|
+
boxed, conv
|
|
25087
|
+
),
|
|
25088
|
+
ty,
|
|
25089
|
+
));
|
|
25090
|
+
}
|
|
25091
|
+
}
|
|
25092
|
+
}
|
|
23828
25093
|
// Typed extern (a `cargo:` import with a declared native signature) used INSIDE a
|
|
23829
25094
|
// native/typed expression: `entity_x(e) + 16`, `cvar(e, k) + 1`. Emit the direct
|
|
23830
25095
|
// `crate::name_typed(<coerced args>)` and report its NATIVE return type, so the
|
|
@@ -23857,16 +25122,24 @@ impl Codegen {
|
|
|
23857
25122
|
// M5: direct call to an eligible native fn -> `name_native(<native args>)`.
|
|
23858
25123
|
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
23859
25124
|
if self.native_fns.contains(fname.as_ref()) {
|
|
25125
|
+
// The ABI this callee actually has. Passing every argument through
|
|
25126
|
+
// `coerce_native_arg` — as the boxed-context site already does — is what
|
|
25127
|
+
// keeps a NATIVE argument of the other width from being handed over raw:
|
|
25128
|
+
// `manhattan(c, r, tac_unit_col(id), tac_unit_row(id))` types its extern
|
|
25129
|
+
// args I32 and the callee takes f64, which emitted `manhattan_native(.., i32)`
|
|
25130
|
+
// and rustc rejected it with E0308. Value args keep their existing
|
|
25131
|
+
// from_value_expr path (coerce_native_arg's first arm IS that path).
|
|
25132
|
+
let want = if self.native_fns_i32.contains(fname.as_ref()) {
|
|
25133
|
+
RustType::I32
|
|
25134
|
+
} else {
|
|
25135
|
+
RustType::F64
|
|
25136
|
+
};
|
|
23860
25137
|
let mut argc: Vec<String> = Vec::with_capacity(args.len());
|
|
23861
25138
|
let mut ok = true;
|
|
23862
25139
|
for a in args {
|
|
23863
25140
|
if let CallArg::Expr(e) = a {
|
|
23864
25141
|
let (ac, at) = self.emit_typed_expr(e)?;
|
|
23865
|
-
argc.push(
|
|
23866
|
-
RustType::F64.from_value_expr(&ac)
|
|
23867
|
-
} else {
|
|
23868
|
-
ac
|
|
23869
|
-
});
|
|
25142
|
+
argc.push(self.coerce_native_arg(&ac, at, want.clone()));
|
|
23870
25143
|
} else {
|
|
23871
25144
|
ok = false;
|
|
23872
25145
|
break;
|
|
@@ -23882,13 +25155,15 @@ impl Codegen {
|
|
|
23882
25155
|
RustType::F64,
|
|
23883
25156
|
));
|
|
23884
25157
|
}
|
|
25158
|
+
// …and report the ABI's OWN return type, for the same reason: an
|
|
25159
|
+
// i32-ABI callee reported as F64 mistypes every consumer downstream.
|
|
23885
25160
|
return Ok((
|
|
23886
25161
|
format!(
|
|
23887
25162
|
"{}({})",
|
|
23888
25163
|
self.native_call_target(fname.as_ref()),
|
|
23889
25164
|
argc.join(", ")
|
|
23890
25165
|
),
|
|
23891
|
-
|
|
25166
|
+
want,
|
|
23892
25167
|
));
|
|
23893
25168
|
}
|
|
23894
25169
|
}
|
|
@@ -25014,8 +26289,21 @@ impl Codegen {
|
|
|
25014
26289
|
// the same local (`aid && out.indexOf(aid)`), and a bare `let __l = aid;` moves the non-`Copy`
|
|
25015
26290
|
// `Value` so the reuse is a borrow-after-move (E0382). The left operand is still evaluated
|
|
25016
26291
|
// exactly once (its side effects don't double-run); only its value is cloned for the test.
|
|
25017
|
-
|
|
25018
|
-
|
|
26292
|
+
//
|
|
26293
|
+
// The RESULT operand has the same hazard: when it lowers to a bare local (`inGenBlock &&
|
|
26294
|
+
// currentTrack`), yielding it moves that local out, so a later read — the next iteration
|
|
26295
|
+
// of the enclosing loop, or any subsequent use — is E0382. Clone it in that case only;
|
|
26296
|
+
// anything else is already a temporary (or already cloned), so the fast path is unchanged.
|
|
26297
|
+
BinOp::And => format!(
|
|
26298
|
+
"{{ let __l = ({}).clone(); if __l.is_truthy() {{ {} }} else {{ __l }} }}",
|
|
26299
|
+
l,
|
|
26300
|
+
Self::clone_if_bare_ident(r)
|
|
26301
|
+
),
|
|
26302
|
+
BinOp::Or => format!(
|
|
26303
|
+
"{{ let __l = ({}).clone(); if __l.is_truthy() {{ __l }} else {{ {} }} }}",
|
|
26304
|
+
l,
|
|
26305
|
+
Self::clone_if_bare_ident(r)
|
|
26306
|
+
),
|
|
25019
26307
|
BinOp::BitAnd => Self::emit_bitwise_binop(l, r, "&"),
|
|
25020
26308
|
BinOp::BitOr => Self::emit_bitwise_binop(l, r, "|"),
|
|
25021
26309
|
BinOp::BitXor => Self::emit_bitwise_binop(l, r, "^"),
|