@tishlang/tish-lsp 3.0.0 → 3.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tish-lsp +0 -0
- package/crates/tish/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/tests/scope_merge.rs +31 -0
- package/crates/tish_build_utils/src/lib.rs +131 -6
- package/crates/tish_compile/src/codegen.rs +517 -19
- package/crates/tish_compile/src/lib.rs +14 -1
- package/crates/tish_compile/src/resolve.rs +12 -2
- package/crates/tish_compile/tests/regr_andor_rhs_move.rs +85 -0
- package/crates/tish_compile/tests/regr_rust_lib_emit.rs +150 -0
- package/crates/tish_core/src/shape.rs +28 -3
- package/crates/tish_fmt/src/lib.rs +54 -1
- package/crates/tish_lsp/Cargo.toml +1 -1
- package/crates/tish_native/src/build.rs +131 -0
- package/crates/tish_native/src/config.rs +14 -0
- package/crates/tish_runtime/Cargo.toml +20 -1
- package/crates/tish_runtime/src/crypto.rs +127 -0
- package/crates/tish_runtime/src/encoding.rs +157 -0
- package/crates/tish_runtime/src/fs_ext.rs +27 -0
- package/crates/tish_runtime/src/lib.rs +32 -0
- package/crates/tish_runtime/src/net.rs +482 -0
- package/crates/tish_runtime/src/process_spawn.rs +508 -0
- package/crates/tish_runtime/src/ws.rs +206 -23
- package/crates/tish_runtime/src/zip.rs +196 -0
- package/crates/tish_runtime_gba/src/lib.rs +17 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lsp +0 -0
- package/platform/darwin-x64/tish-lsp +0 -0
- package/platform/linux-arm64/tish-lsp +0 -0
- package/platform/linux-x64/tish-lsp +0 -0
- package/platform/win32-x64/tish-lsp.exe +0 -0
|
@@ -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"
|
|
@@ -1018,6 +1057,9 @@ pub(crate) struct Codegen {
|
|
|
1018
1057
|
features: std::collections::HashSet<String>,
|
|
1019
1058
|
/// spec -> native init strategy (legacy adapter object vs generated `generated_native` wrapper)
|
|
1020
1059
|
native_module_init: std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
|
|
1060
|
+
/// spec -> the `run()`-local holding that module's namespace object, bound ONCE in the preamble.
|
|
1061
|
+
/// Building it is O(exports) — see `emit_native_namespace_preamble`.
|
|
1062
|
+
native_ns_locals: std::collections::HashMap<String, String>,
|
|
1021
1063
|
/// Typed native imports (`declare fun` + a native import): tish name -> its typed signature.
|
|
1022
1064
|
/// A matching call emits a DIRECT `<crate>::name_typed(..)` instead of a boxed `value_call`.
|
|
1023
1065
|
extern_fns: std::collections::HashMap<String, ExternSig>,
|
|
@@ -1242,6 +1284,10 @@ pub(crate) struct Codegen {
|
|
|
1242
1284
|
/// closures it must not return a `Result` from a `Value`-returning closure.
|
|
1243
1285
|
value_fn_depth: u32,
|
|
1244
1286
|
emit_mode: crate::NativeEmitMode,
|
|
1287
|
+
/// `RustLib` only: the entry module's `(local_name, exported_name)` pairs, as computed by
|
|
1288
|
+
/// `merge_modules` for the JS bundle's trailing `export { … }`. Drives the `pub fn` wrappers.
|
|
1289
|
+
/// Empty for every other emit mode.
|
|
1290
|
+
entry_exports: Vec<(String, String)>,
|
|
1245
1291
|
/// Scheme imports as `(scheme_name, absolute_path)`, in first-seen order. Populated (Gba mode)
|
|
1246
1292
|
/// before the entry is emitted from the active [`crate::schemes`] registry; drives the
|
|
1247
1293
|
/// `mod __scheme_<name>_<j> { … }` blocks and their per-scheme registration. The handle a tish
|
|
@@ -1292,6 +1338,7 @@ impl Codegen {
|
|
|
1292
1338
|
string_charset: String::new(),
|
|
1293
1339
|
features,
|
|
1294
1340
|
native_module_init,
|
|
1341
|
+
native_ns_locals: std::collections::HashMap::new(),
|
|
1295
1342
|
extern_fns: std::collections::HashMap::new(),
|
|
1296
1343
|
user_fn_returns: std::collections::HashMap::new(),
|
|
1297
1344
|
async_context_stack: Vec::new(),
|
|
@@ -1359,6 +1406,7 @@ impl Codegen {
|
|
|
1359
1406
|
program_fun_decl_names: std::collections::HashSet::new(),
|
|
1360
1407
|
value_fn_depth: 0,
|
|
1361
1408
|
emit_mode: crate::NativeEmitMode::DesktopBin,
|
|
1409
|
+
entry_exports: Vec::new(),
|
|
1362
1410
|
scheme_files: Vec::new(),
|
|
1363
1411
|
has_native_ui_host: false,
|
|
1364
1412
|
program_uses_document: false,
|
|
@@ -2044,6 +2092,48 @@ impl Codegen {
|
|
|
2044
2092
|
}
|
|
2045
2093
|
}
|
|
2046
2094
|
|
|
2095
|
+
/// The Rust expression that CONSTRUCTS a native module's namespace object. O(exports): it inserts
|
|
2096
|
+
/// every function the crate exports into a fresh map. Call it once per module, not once per import.
|
|
2097
|
+
fn native_module_ns_ctor(init: &crate::resolve::NativeModuleInit) -> String {
|
|
2098
|
+
match init {
|
|
2099
|
+
crate::resolve::NativeModuleInit::Legacy {
|
|
2100
|
+
crate_name,
|
|
2101
|
+
export_fn,
|
|
2102
|
+
} => format!("{}::{}()", crate_name, export_fn),
|
|
2103
|
+
crate::resolve::NativeModuleInit::Generated { export_fn, .. } => {
|
|
2104
|
+
format!("crate::generated_native::{}()", export_fn)
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
/// Bind every native module's namespace object to a `run()`-local, ONCE, before any import reads
|
|
2110
|
+
/// from one.
|
|
2111
|
+
///
|
|
2112
|
+
/// Each named import lowers to "get this key out of the module's namespace". Building that
|
|
2113
|
+
/// namespace is O(exports) — a fresh `ObjectMap` with an `Arc<str>` key and a native closure per
|
|
2114
|
+
/// exported function — so doing it at every import site is O(imports x exports): importing 68
|
|
2115
|
+
/// names from a crate that exports 68 cost 4,624 allocations to bind 68 functions. On a GBA that
|
|
2116
|
+
/// was measured at 3 seconds of black screen (`tish-gba/examples/bench-boot`), paid before the
|
|
2117
|
+
/// program's first statement; `packages/engine.tish` there imports 117 symbols across two crates
|
|
2118
|
+
/// and cost 4.3s. Hoisting makes it O(imports + exports).
|
|
2119
|
+
///
|
|
2120
|
+
/// Emitted in sorted order so the generated Rust is byte-identical across builds (the map is a
|
|
2121
|
+
/// `HashMap`, whose iteration order is not).
|
|
2122
|
+
fn emit_native_namespace_preamble(&mut self) {
|
|
2123
|
+
let mut specs: Vec<String> = self.native_module_init.keys().cloned().collect();
|
|
2124
|
+
specs.sort();
|
|
2125
|
+
for (i, spec) in specs.iter().enumerate() {
|
|
2126
|
+
// Index rather than a sanitized spec: `cargo:a-b` and `cargo:a_b` sanitize to the same
|
|
2127
|
+
// identifier, and silently binding one module's namespace under another's name would be a
|
|
2128
|
+
// very quiet bug.
|
|
2129
|
+
let local = format!("__tish_native_ns_{}", i);
|
|
2130
|
+
let ctor = Self::native_module_ns_ctor(&self.native_module_init[spec]);
|
|
2131
|
+
// A module can be resolved but have every one of its imports shadowed or unused.
|
|
2132
|
+
self.writeln(&format!("#[allow(unused_variables)] let {} = {};", local, ctor));
|
|
2133
|
+
self.native_ns_locals.insert(spec.clone(), local);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2047
2137
|
/// Map native module spec to Rust init expression using resolved package.json modules.
|
|
2048
2138
|
/// For built-in modules (tish:fs, tish:http, tish:process), use builtin_native_module_rust_init.
|
|
2049
2139
|
fn native_module_rust_init(&self, spec: &str, export_name: &str) -> Option<String> {
|
|
@@ -2053,19 +2143,18 @@ impl Codegen {
|
|
|
2053
2143
|
self.native_module_init.get(spec).map(|init| {
|
|
2054
2144
|
// Native modules return a namespace object (like an ES module).
|
|
2055
2145
|
// Named imports extract the field from that namespace: `import { foo } from "pkg"` → `ns.foo`.
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
)
|
|
2146
|
+
// Read it from the hoisted local when there is one; the inline constructor is the fallback
|
|
2147
|
+
// for anything emitted outside `run()`'s preamble.
|
|
2148
|
+
match self.native_ns_locals.get(spec) {
|
|
2149
|
+
Some(local) => format!(
|
|
2150
|
+
"{{ match &{} {{ Value::Object(ref _o) => _o.borrow().strings.get({:?}).cloned().unwrap_or(Value::Null), _ => Value::Null }} }}",
|
|
2151
|
+
local, export_name
|
|
2152
|
+
),
|
|
2153
|
+
None => format!(
|
|
2154
|
+
"{{ let _ns = {}; match _ns {{ Value::Object(ref _o) => _o.borrow().strings.get({:?}).cloned().unwrap_or(Value::Null), _ => Value::Null }} }}",
|
|
2155
|
+
Self::native_module_ns_ctor(init), export_name
|
|
2156
|
+
),
|
|
2157
|
+
}
|
|
2069
2158
|
})
|
|
2070
2159
|
}
|
|
2071
2160
|
|
|
@@ -2098,6 +2187,7 @@ impl Codegen {
|
|
|
2098
2187
|
"realpath" | "realpathSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::realpath(args))"),
|
|
2099
2188
|
"readlink" | "readlinkSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::readlink(args))"),
|
|
2100
2189
|
"truncate" | "truncateSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::truncate(args))"),
|
|
2190
|
+
"chmod" | "chmodSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::chmod(args))"),
|
|
2101
2191
|
"mkdtemp" | "mkdtempSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::mkdtemp(args))"),
|
|
2102
2192
|
"cp" | "cpSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::cp(args))"),
|
|
2103
2193
|
"access" | "accessSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::access(args))"),
|
|
@@ -2121,6 +2211,7 @@ impl Codegen {
|
|
|
2121
2211
|
"realpath" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::realpath_promise(args))"),
|
|
2122
2212
|
"readlink" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::readlink_promise(args))"),
|
|
2123
2213
|
"truncate" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::truncate_promise(args))"),
|
|
2214
|
+
"chmod" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::chmod_promise(args))"),
|
|
2124
2215
|
"mkdtemp" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::mkdtemp_promise(args))"),
|
|
2125
2216
|
"cp" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::cp_promise(args))"),
|
|
2126
2217
|
"access" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::access_promise(args))"),
|
|
@@ -2155,9 +2246,20 @@ impl Codegen {
|
|
|
2155
2246
|
"execFile" => Some("Value::native(|args: &[Value]| tish_process_exec_file(args))"),
|
|
2156
2247
|
"execCapture" => Some("Value::native(|args: &[Value]| tish_process_exec_capture(args))"),
|
|
2157
2248
|
"execFileCapture" => Some("Value::native(|args: &[Value]| tish_process_exec_file_capture(args))"),
|
|
2249
|
+
"spawn" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_spawn(args))"),
|
|
2250
|
+
"readStdout" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_read_stdout(args))"),
|
|
2251
|
+
"readStderr" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_read_stderr(args))"),
|
|
2252
|
+
"writeStdin" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_write_stdin(args))"),
|
|
2253
|
+
"closeStdin" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_close_stdin(args))"),
|
|
2254
|
+
"wait" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_wait(args))"),
|
|
2255
|
+
"kill" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_kill(args))"),
|
|
2256
|
+
"pid" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_pid(args))"),
|
|
2158
2257
|
"argv" => Some("Value::Array(VmRef::new(std::env::args().map(|s| Value::String(s.into())).collect()))"),
|
|
2159
2258
|
"env" => Some("Value::object(std::env::vars().map(|(k,v)| (Arc::from(k.as_str()), Value::String(v.into()))).collect())"),
|
|
2160
|
-
|
|
2259
|
+
// Node-style platform string so `process.platform` reads identically on the JS
|
|
2260
|
+
// target (Node's real global) and native (this): macos->darwin, windows->win32.
|
|
2261
|
+
"platform" => Some("Value::String(match std::env::consts::OS { \"macos\" => \"darwin\", \"windows\" => \"win32\", o => o }.into())"),
|
|
2262
|
+
"process" => Some("{ let mut m = ObjectMap::default(); m.insert(Arc::from(\"exit\"), Value::native(|args: &[Value]| tish_process_exit(args))); m.insert(Arc::from(\"cwd\"), Value::native(|args: &[Value]| tish_process_cwd(args))); m.insert(Arc::from(\"exec\"), Value::native(|args: &[Value]| tish_process_exec(args))); m.insert(Arc::from(\"execFile\"), Value::native(|args: &[Value]| tish_process_exec_file(args))); m.insert(Arc::from(\"execCapture\"), Value::native(|args: &[Value]| tish_process_exec_capture(args))); m.insert(Arc::from(\"execFileCapture\"), Value::native(|args: &[Value]| tish_process_exec_file_capture(args))); m.insert(Arc::from(\"argv\"), Value::Array(VmRef::new(std::env::args().map(|s| Value::String(s.into())).collect()))); m.insert(Arc::from(\"env\"), Value::object(std::env::vars().map(|(k,v)| (Arc::from(k.as_str()), Value::String(v.into()))).collect::<ObjectMap>())); m.insert(Arc::from(\"platform\"), Value::String(match std::env::consts::OS { \"macos\" => \"darwin\", \"windows\" => \"win32\", o => o }.into())); Value::object(m) }"),
|
|
2161
2263
|
_ => None,
|
|
2162
2264
|
},
|
|
2163
2265
|
"tish:ws" if self.has_feature("ws") => match export_name {
|
|
@@ -2187,6 +2289,37 @@ impl Codegen {
|
|
|
2187
2289
|
"pid" => Some("Value::native(|args: &[Value]| tishlang_runtime::pty_pid(args))"),
|
|
2188
2290
|
_ => None,
|
|
2189
2291
|
},
|
|
2292
|
+
"tish:net" if self.has_feature("net") => match export_name {
|
|
2293
|
+
"connect" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_connect(args))"),
|
|
2294
|
+
"read" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_read(args))"),
|
|
2295
|
+
"write" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_write(args))"),
|
|
2296
|
+
"close" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_close(args))"),
|
|
2297
|
+
"listen" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_listen(args))"),
|
|
2298
|
+
"accept" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_accept(args))"),
|
|
2299
|
+
"probe" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_probe(args))"),
|
|
2300
|
+
"sleep" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_sleep(args))"),
|
|
2301
|
+
_ => None,
|
|
2302
|
+
},
|
|
2303
|
+
"tish:encoding" if self.has_feature("encoding") => match export_name {
|
|
2304
|
+
"base64Encode" => Some("Value::native(|args: &[Value]| tishlang_runtime::base64_encode(args))"),
|
|
2305
|
+
"base64Decode" => Some("Value::native(|args: &[Value]| tishlang_runtime::base64_decode(args))"),
|
|
2306
|
+
"base64UrlEncode" => Some("Value::native(|args: &[Value]| tishlang_runtime::base64_url_encode(args))"),
|
|
2307
|
+
"base64UrlDecode" => Some("Value::native(|args: &[Value]| tishlang_runtime::base64_url_decode(args))"),
|
|
2308
|
+
"utf8Decode" => Some("Value::native(|args: &[Value]| tishlang_runtime::utf8_decode(args))"),
|
|
2309
|
+
"utf8Encode" => Some("Value::native(|args: &[Value]| tishlang_runtime::utf8_encode(args))"),
|
|
2310
|
+
_ => None,
|
|
2311
|
+
},
|
|
2312
|
+
"tish:crypto" if self.has_feature("crypto") => match export_name {
|
|
2313
|
+
"sha256" => Some("Value::native(|args: &[Value]| tishlang_runtime::sha256(args))"),
|
|
2314
|
+
"sha256Hex" => Some("Value::native(|args: &[Value]| tishlang_runtime::sha256_hex(args))"),
|
|
2315
|
+
"randomBytes" => Some("Value::native(|args: &[Value]| tishlang_runtime::random_bytes(args))"),
|
|
2316
|
+
_ => None,
|
|
2317
|
+
},
|
|
2318
|
+
"tish:zip" if self.has_feature("zip") => match export_name {
|
|
2319
|
+
"extract" => Some("Value::native(|args: &[Value]| tishlang_runtime::zip_extract(args))"),
|
|
2320
|
+
"entries" => Some("Value::native(|args: &[Value]| tishlang_runtime::zip_entries(args))"),
|
|
2321
|
+
_ => None,
|
|
2322
|
+
},
|
|
2190
2323
|
_ => return None,
|
|
2191
2324
|
};
|
|
2192
2325
|
init.map(String::from)
|
|
@@ -2196,7 +2329,8 @@ impl Codegen {
|
|
|
2196
2329
|
if self.features.contains("full") {
|
|
2197
2330
|
matches!(
|
|
2198
2331
|
name,
|
|
2199
|
-
"http" | "timers" | "fs" | "process" | "regex" | "ws" | "tty" | "pty"
|
|
2332
|
+
"http" | "timers" | "fs" | "process" | "regex" | "ws" | "tty" | "pty" | "net"
|
|
2333
|
+
| "encoding" | "crypto" | "zip"
|
|
2200
2334
|
)
|
|
2201
2335
|
} else {
|
|
2202
2336
|
self.features.contains(name)
|
|
@@ -2849,6 +2983,14 @@ impl Codegen {
|
|
|
2849
2983
|
self.write("use std::rc::Rc;\n");
|
|
2850
2984
|
self.write("use std::sync::Arc;\n");
|
|
2851
2985
|
}
|
|
2986
|
+
// RustLib: re-export the runtime prelude rather than importing it privately, so the crate's
|
|
2987
|
+
// public API is self-contained — a consumer gets `Value` (and `ObjectMap` / `PropMap` /
|
|
2988
|
+
// `VmRef`) from THIS crate and cannot version-skew them against a separate `tishlang_runtime`
|
|
2989
|
+
// dependency of their own. Also avoids a duplicate-import error, since `Value` is already in
|
|
2990
|
+
// this list and could not be re-exported a second time at the crate root.
|
|
2991
|
+
if self.emit_mode == crate::NativeEmitMode::RustLib {
|
|
2992
|
+
self.write("pub ");
|
|
2993
|
+
}
|
|
2852
2994
|
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");
|
|
2853
2995
|
if self.program_has_jsx {
|
|
2854
2996
|
self.write("use tishlang_ui::{fragment_value, install_thread_local_host, native_create_root, native_use_state, ui_h, ui_text, HeadlessHost};\n");
|
|
@@ -2964,6 +3106,9 @@ impl Codegen {
|
|
|
2964
3106
|
self.writeln("}");
|
|
2965
3107
|
self.writeln("");
|
|
2966
3108
|
}
|
|
3109
|
+
if self.emit_mode == crate::NativeEmitMode::RustLib {
|
|
3110
|
+
self.emit_rust_lib_surface(program)?;
|
|
3111
|
+
}
|
|
2967
3112
|
// M5 (default-on via native_opts_enabled; TISH_NATIVE_OPT=0 opts out): emit a parallel
|
|
2968
3113
|
// native `fn f_native` for each eligible top-level numeric fn at top level; direct calls
|
|
2969
3114
|
// route to it in emit_typed_expr.
|
|
@@ -3088,6 +3233,9 @@ impl Codegen {
|
|
|
3088
3233
|
}
|
|
3089
3234
|
self.indent += 1;
|
|
3090
3235
|
|
|
3236
|
+
// Before anything else: one namespace object per native module, shared by all of its imports.
|
|
3237
|
+
self.emit_native_namespace_preamble();
|
|
3238
|
+
|
|
3091
3239
|
// Initialize builtins
|
|
3092
3240
|
self.writeln("let mut console = Value::object(ObjectMap::from([");
|
|
3093
3241
|
self.indent += 1;
|
|
@@ -3331,6 +3479,9 @@ impl Codegen {
|
|
|
3331
3479
|
self.indent -= 1;
|
|
3332
3480
|
self.writeln("}");
|
|
3333
3481
|
self.writeln("p.insert(Arc::from(\"env\"), Value::object(env_obj));");
|
|
3482
|
+
// Node-style platform string (macos->darwin, windows->win32) so bare `process.platform`
|
|
3483
|
+
// matches the JS target's Node global and the named `tish:process` export.
|
|
3484
|
+
self.writeln("p.insert(Arc::from(\"platform\"), Value::String(match std::env::consts::OS { \"macos\" => \"darwin\", \"windows\" => \"win32\", o => o }.into()));");
|
|
3334
3485
|
self.writeln("p");
|
|
3335
3486
|
self.indent -= 1;
|
|
3336
3487
|
self.writeln("});");
|
|
@@ -3456,6 +3607,29 @@ impl Codegen {
|
|
|
3456
3607
|
self.writeln("tishlang_runtime::drain_timers();");
|
|
3457
3608
|
}
|
|
3458
3609
|
|
|
3610
|
+
// RustLib: hand the entry module's exported bindings to the thread-local table before
|
|
3611
|
+
// `run()`'s frame drops. The bindings are ordinary `Value` locals; cloning one clones an
|
|
3612
|
+
// `Rc`, so the closure — and every module-level cell it captured — stays alive and shared.
|
|
3613
|
+
// That is what makes module state survive between `pub fn` calls (and stay mutable through
|
|
3614
|
+
// `clear*`-style reassignment, since captures are `VmRef` cells, not copies).
|
|
3615
|
+
if self.emit_mode == crate::NativeEmitMode::RustLib {
|
|
3616
|
+
let pairs: Vec<String> = self
|
|
3617
|
+
.rust_lib_exports(program)
|
|
3618
|
+
.iter()
|
|
3619
|
+
.map(|e| {
|
|
3620
|
+
format!(
|
|
3621
|
+
"({:?}, {}.clone())",
|
|
3622
|
+
e.exported_name,
|
|
3623
|
+
Self::escape_ident(&e.local_name)
|
|
3624
|
+
)
|
|
3625
|
+
})
|
|
3626
|
+
.collect();
|
|
3627
|
+
self.writeln(&format!(
|
|
3628
|
+
"__tish_publish_exports(::std::vec![{}]);",
|
|
3629
|
+
pairs.join(", ")
|
|
3630
|
+
));
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3459
3633
|
self.writeln("Ok(())");
|
|
3460
3634
|
self.indent -= 1;
|
|
3461
3635
|
self.writeln("}");
|
|
@@ -3484,6 +3658,191 @@ impl Codegen {
|
|
|
3484
3658
|
Ok(())
|
|
3485
3659
|
}
|
|
3486
3660
|
|
|
3661
|
+
/// The entry module's exports, paired with the arity of the `fn` each names (if it names one).
|
|
3662
|
+
/// `merge_modules` unwraps `export` into plain top-level declarations and records the
|
|
3663
|
+
/// `(local, exported)` pairs — including through `export { … } from './x.tish'` re-exports,
|
|
3664
|
+
/// which resolve to the dependency's local binding — so a barrel module still yields the full
|
|
3665
|
+
/// surface. Sorted for deterministic output.
|
|
3666
|
+
fn rust_lib_exports(&self, program: &Program) -> Vec<RustLibExport> {
|
|
3667
|
+
let mut arities: std::collections::HashMap<&str, usize> =
|
|
3668
|
+
std::collections::HashMap::new();
|
|
3669
|
+
for stmt in &program.statements {
|
|
3670
|
+
if let Statement::FunDecl { name, params, .. } = stmt {
|
|
3671
|
+
arities.insert(name.as_ref(), params.len());
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
let mut out: Vec<RustLibExport> = self
|
|
3675
|
+
.entry_exports
|
|
3676
|
+
.iter()
|
|
3677
|
+
.map(|(local, exported)| RustLibExport {
|
|
3678
|
+
local_name: local.clone(),
|
|
3679
|
+
exported_name: exported.clone(),
|
|
3680
|
+
arity: arities.get(local.as_str()).copied(),
|
|
3681
|
+
})
|
|
3682
|
+
.collect();
|
|
3683
|
+
out.sort_by(|a, b| a.exported_name.cmp(&b.exported_name));
|
|
3684
|
+
out
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
/// Emit the `RustLib` public surface: the thread-local module instance, the lazy `ensure_init`,
|
|
3688
|
+
/// and one `pub fn` per entry export.
|
|
3689
|
+
///
|
|
3690
|
+
/// The module body itself is the ordinary `run()` — this only changes *when* it runs and what
|
|
3691
|
+
/// outlives it. Storage is `thread_local!` rather than a global because `Value` is `!Send`
|
|
3692
|
+
/// unless the `send-values` feature is on (it follows `http`), so a `static` would not compile
|
|
3693
|
+
/// for the common configuration. Per-thread instances also match how a consumer uses this:
|
|
3694
|
+
/// register into the module's registries, then call it, on one thread.
|
|
3695
|
+
fn emit_rust_lib_surface(&mut self, program: &Program) -> Result<(), CompileError> {
|
|
3696
|
+
if self.is_async {
|
|
3697
|
+
return Err(CompileError::new(
|
|
3698
|
+
"--target rust --emit lib: the entry module has top-level `await`. A library has \
|
|
3699
|
+
no runtime to drive it — move the async work into an exported function."
|
|
3700
|
+
.to_string(),
|
|
3701
|
+
None,
|
|
3702
|
+
));
|
|
3703
|
+
}
|
|
3704
|
+
let exports = self.rust_lib_exports(program);
|
|
3705
|
+
if exports.is_empty() {
|
|
3706
|
+
return Err(CompileError::new(
|
|
3707
|
+
"--target rust --emit lib: the entry module exports nothing, so the crate would \
|
|
3708
|
+
have no public API. Add `export fn …` to the entry module."
|
|
3709
|
+
.to_string(),
|
|
3710
|
+
None,
|
|
3711
|
+
));
|
|
3712
|
+
}
|
|
3713
|
+
// Only `export fn` is supported. A value export has no arity, and its Rust local is not
|
|
3714
|
+
// necessarily a `Value` to publish (a `const` string lowers to `&str`), so it cannot be
|
|
3715
|
+
// handed to the export table the way a closure can. Say so instead of emitting a getter
|
|
3716
|
+
// that would always read back null.
|
|
3717
|
+
let non_fn: Vec<&str> = exports
|
|
3718
|
+
.iter()
|
|
3719
|
+
.filter(|e| e.arity.is_none())
|
|
3720
|
+
.map(|e| e.exported_name.as_str())
|
|
3721
|
+
.collect();
|
|
3722
|
+
if !non_fn.is_empty() {
|
|
3723
|
+
return Err(CompileError::new(
|
|
3724
|
+
format!(
|
|
3725
|
+
"--target rust --emit lib: only `export fn` can cross the crate boundary, but \
|
|
3726
|
+
the entry module also exports the value(s) `{}`. Wrap each in an exported \
|
|
3727
|
+
function (`export fn {}() {{ … }}`).",
|
|
3728
|
+
non_fn.join("`, `"),
|
|
3729
|
+
non_fn[0]
|
|
3730
|
+
),
|
|
3731
|
+
None,
|
|
3732
|
+
));
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
self.writeln("// ── Tish library surface ──────────────────────────────────────────────");
|
|
3736
|
+
// `Value` and friends are already re-exported by the (pub) runtime prelude above; this adds
|
|
3737
|
+
// the whole runtime for consumers that need more than the prelude carries.
|
|
3738
|
+
self.writeln("pub use tishlang_runtime as runtime;");
|
|
3739
|
+
self.writeln("");
|
|
3740
|
+
self.writeln("thread_local! {");
|
|
3741
|
+
self.indent += 1;
|
|
3742
|
+
self.writeln(
|
|
3743
|
+
"static __TISH_MODULE: ::std::cell::RefCell<::std::option::Option<::std::collections::HashMap<&'static str, Value>>> =",
|
|
3744
|
+
);
|
|
3745
|
+
self.writeln(" ::std::cell::RefCell::new(::std::option::Option::None);");
|
|
3746
|
+
self.indent -= 1;
|
|
3747
|
+
self.writeln("}");
|
|
3748
|
+
self.writeln("");
|
|
3749
|
+
self.writeln("/// Called once from the tail of `run()` with the entry module's exports.");
|
|
3750
|
+
self.writeln("fn __tish_publish_exports(pairs: ::std::vec::Vec<(&'static str, Value)>) {");
|
|
3751
|
+
self.indent += 1;
|
|
3752
|
+
self.writeln("__TISH_MODULE.with(|m| {");
|
|
3753
|
+
self.writeln(" *m.borrow_mut() = ::std::option::Option::Some(pairs.into_iter().collect());");
|
|
3754
|
+
self.writeln("});");
|
|
3755
|
+
self.indent -= 1;
|
|
3756
|
+
self.writeln("}");
|
|
3757
|
+
self.writeln("");
|
|
3758
|
+
self.writeln("/// Run the module body exactly once per thread, before the first export call.");
|
|
3759
|
+
self.writeln("fn __tish_ensure_init() {");
|
|
3760
|
+
self.indent += 1;
|
|
3761
|
+
self.writeln("// Park an empty table BEFORE running the body: that makes init non-reentrant");
|
|
3762
|
+
self.writeln("// (an export reached while the module is still initialising sees an empty");
|
|
3763
|
+
self.writeln("// table and returns null, rather than recursing forever).");
|
|
3764
|
+
self.writeln("let fresh = __TISH_MODULE.with(|m| {");
|
|
3765
|
+
self.writeln(" let mut b = m.borrow_mut();");
|
|
3766
|
+
self.writeln(" if b.is_some() { return false; }");
|
|
3767
|
+
self.writeln(" *b = ::std::option::Option::Some(::std::collections::HashMap::new());");
|
|
3768
|
+
self.writeln(" true");
|
|
3769
|
+
self.writeln("});");
|
|
3770
|
+
self.writeln("if fresh {");
|
|
3771
|
+
self.writeln(" // A module whose top level fails cannot be recovered from here: every");
|
|
3772
|
+
self.writeln(" // export would observe half-initialised state.");
|
|
3773
|
+
self.writeln(" run().unwrap_or_else(|e| panic!(\"tish module initialisation failed: {}\", e));");
|
|
3774
|
+
self.writeln("}");
|
|
3775
|
+
self.indent -= 1;
|
|
3776
|
+
self.writeln("}");
|
|
3777
|
+
self.writeln("");
|
|
3778
|
+
self.writeln("/// Clone an export out of the table. The borrow is released before the value is");
|
|
3779
|
+
self.writeln("/// used, because an export is free to call back into another export.");
|
|
3780
|
+
self.writeln("fn __tish_export(name: &str) -> Value {");
|
|
3781
|
+
self.indent += 1;
|
|
3782
|
+
self.writeln("__tish_ensure_init();");
|
|
3783
|
+
self.writeln("__TISH_MODULE.with(|m| {");
|
|
3784
|
+
self.writeln(" m.borrow().as_ref().and_then(|t| t.get(name).cloned()).unwrap_or(Value::Null)");
|
|
3785
|
+
self.writeln("})");
|
|
3786
|
+
self.indent -= 1;
|
|
3787
|
+
self.writeln("}");
|
|
3788
|
+
self.writeln("");
|
|
3789
|
+
self.writeln("thread_local! {");
|
|
3790
|
+
self.writeln(" static __TISH_LAST_THROW: ::std::cell::RefCell<Value> = ::std::cell::RefCell::new(Value::Null);");
|
|
3791
|
+
self.writeln("}");
|
|
3792
|
+
self.writeln("");
|
|
3793
|
+
self.writeln("/// The value thrown by the most recent export call, or null if it completed normally.");
|
|
3794
|
+
self.writeln("///");
|
|
3795
|
+
self.writeln("/// A `pub fn … -> Value` has no error channel, so a throw that escapes an export is drained");
|
|
3796
|
+
self.writeln("/// and recorded here rather than left parked. Reading it is the way to tell \"returned null\"");
|
|
3797
|
+
self.writeln("/// from \"threw\".");
|
|
3798
|
+
self.writeln("pub fn tish_last_throw() -> Value {");
|
|
3799
|
+
self.indent += 1;
|
|
3800
|
+
self.writeln("__TISH_LAST_THROW.with(|t| t.borrow().clone())");
|
|
3801
|
+
self.indent -= 1;
|
|
3802
|
+
self.writeln("}");
|
|
3803
|
+
self.writeln("");
|
|
3804
|
+
self.writeln("fn __tish_call_export(name: &str, args: &[Value]) -> Value {");
|
|
3805
|
+
self.indent += 1;
|
|
3806
|
+
self.writeln("// A throw is PARKED for the next checkpoint rather than unwound (#303). Across a `pub fn`");
|
|
3807
|
+
self.writeln("// boundary there is no Tish frame to surface it in, so a parked throw would sit latched and");
|
|
3808
|
+
self.writeln("// every LATER call would bail at its own checkpoint and silently return null — one bad call");
|
|
3809
|
+
self.writeln("// bricking the module for the rest of the process. A host hits this simply by reading a");
|
|
3810
|
+
self.writeln("// property off a null the runtime handed it. So: enter clean, leave clean.");
|
|
3811
|
+
self.writeln("let _ = tishlang_runtime::take_pending_throw();");
|
|
3812
|
+
self.writeln("let out = match __tish_export(name) {");
|
|
3813
|
+
self.writeln(" Value::Function(f) => f.call(args),");
|
|
3814
|
+
self.writeln(" other => other,");
|
|
3815
|
+
self.writeln("};");
|
|
3816
|
+
self.writeln("let thrown = tishlang_runtime::take_pending_throw().unwrap_or(Value::Null);");
|
|
3817
|
+
self.writeln("__TISH_LAST_THROW.with(|t| *t.borrow_mut() = thrown);");
|
|
3818
|
+
self.writeln("out");
|
|
3819
|
+
self.indent -= 1;
|
|
3820
|
+
self.writeln("}");
|
|
3821
|
+
self.writeln("");
|
|
3822
|
+
|
|
3823
|
+
for e in &exports {
|
|
3824
|
+
// Checked above: every remaining export is a `fn`, so the arity is present.
|
|
3825
|
+
let n = e.arity.unwrap_or(0);
|
|
3826
|
+
let params: Vec<String> = (0..n).map(|i| format!("a{}: Value", i)).collect();
|
|
3827
|
+
let args: Vec<String> = (0..n).map(|i| format!("a{}", i)).collect();
|
|
3828
|
+
self.writeln(&format!(
|
|
3829
|
+
"pub fn {}({}) -> Value {{",
|
|
3830
|
+
Self::escape_ident(&e.exported_name),
|
|
3831
|
+
params.join(", ")
|
|
3832
|
+
));
|
|
3833
|
+
self.indent += 1;
|
|
3834
|
+
self.writeln(&format!(
|
|
3835
|
+
"__tish_call_export({:?}, &[{}])",
|
|
3836
|
+
e.exported_name,
|
|
3837
|
+
args.join(", ")
|
|
3838
|
+
));
|
|
3839
|
+
self.indent -= 1;
|
|
3840
|
+
self.writeln("}");
|
|
3841
|
+
}
|
|
3842
|
+
self.writeln("");
|
|
3843
|
+
Ok(())
|
|
3844
|
+
}
|
|
3845
|
+
|
|
3487
3846
|
/// Emit an expression in **statement position** (its value is discarded). For a native
|
|
3488
3847
|
/// assignment this emits only the side-effect — NOT the boxed `Value::Number(..)` that the
|
|
3489
3848
|
/// expression form returns (JS "assignment yields its value"). In a hot loop that boxed
|
|
@@ -3612,9 +3971,17 @@ impl Codegen {
|
|
|
3612
3971
|
let escaped = Self::escape_ident(name.as_ref());
|
|
3613
3972
|
return Ok(format!("{} = ({}) as i32", escaped, int_code));
|
|
3614
3973
|
}
|
|
3974
|
+
let (val_code, val_ty) = self.emit_typed_expr(value)?;
|
|
3975
|
+
// An RHS that is ALREADY an integer stores straight into the register. `d = d + 1`
|
|
3976
|
+
// reaches here — it is arithmetic, not a bitwise chain, so `emit_int32_operand`
|
|
3977
|
+
// declines it — and boxing that i32 into a `Value::Number` only to run ToInt32 back
|
|
3978
|
+
// out of it was the whole cost of incrementing an annotated counter.
|
|
3979
|
+
let escaped_int = Self::escape_ident(name.as_ref());
|
|
3980
|
+
if val_ty.is_integer_scalar() {
|
|
3981
|
+
return Ok(format!("{} = ({}) as i32", escaped_int, val_code));
|
|
3982
|
+
}
|
|
3615
3983
|
// Defensive: gate guarantees `Some`, but if a future RHS shape slips through,
|
|
3616
3984
|
// fall back to a sound f64-narrowed store rather than miscompiling.
|
|
3617
|
-
let (val_code, val_ty) = self.emit_typed_expr(value)?;
|
|
3618
3985
|
let v = if val_ty.is_native() {
|
|
3619
3986
|
val_ty.to_value_expr(&val_code)
|
|
3620
3987
|
} else {
|
|
@@ -6859,6 +7226,23 @@ impl Codegen {
|
|
|
6859
7226
|
Ok(())
|
|
6860
7227
|
}
|
|
6861
7228
|
|
|
7229
|
+
/// `.clone()` an emitted operand that is a bare Rust identifier, leave anything else alone.
|
|
7230
|
+
///
|
|
7231
|
+
/// Used where an operand is yielded as the value of an expression that does not consume it
|
|
7232
|
+
/// exclusively (`&&` / `||`): moving a non-`Copy` `Value` local out of such a position makes
|
|
7233
|
+
/// every later read a use-after-move. A parenthesised expression, call, field access, or an
|
|
7234
|
+
/// already-`.clone()`d ident is a temporary, so it needs nothing.
|
|
7235
|
+
fn clone_if_bare_ident(s: &str) -> String {
|
|
7236
|
+
let bare = !s.is_empty()
|
|
7237
|
+
&& !s.starts_with(|c: char| c.is_ascii_digit())
|
|
7238
|
+
&& s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
|
|
7239
|
+
if bare {
|
|
7240
|
+
format!("{}.clone()", s)
|
|
7241
|
+
} else {
|
|
7242
|
+
s.to_string()
|
|
7243
|
+
}
|
|
7244
|
+
}
|
|
7245
|
+
|
|
6862
7246
|
fn emit_call_arg(&mut self, arg: &CallArg) -> Result<String, CompileError> {
|
|
6863
7247
|
let e = match arg {
|
|
6864
7248
|
CallArg::Expr(e) | CallArg::Spread(e) => e,
|
|
@@ -14083,6 +14467,22 @@ impl Codegen {
|
|
|
14083
14467
|
/// native `Num<i32,8>` arithmetic instead of boxing the literal side through `f64`. Uses the
|
|
14084
14468
|
/// same truncating `(n * 256) as i32` conversion as the dynamic `fixed`↔`Value` boundary, so
|
|
14085
14469
|
/// the constant is bit-identical to the boxed path.
|
|
14470
|
+
/// A numeric literal that is EXACTLY a whole number inside `i32`'s range, as an `i32`. Used to keep
|
|
14471
|
+
/// `x > 0` / `i < 100` in integer registers when the other side is already an integer — a fraction
|
|
14472
|
+
/// or an out-of-range magnitude returns `None` and stays on the `f64` path, where it is correct.
|
|
14473
|
+
fn int_literal_of(e: &Expr) -> Option<i32> {
|
|
14474
|
+
if let Expr::Literal {
|
|
14475
|
+
value: Literal::Number(n),
|
|
14476
|
+
..
|
|
14477
|
+
} = e
|
|
14478
|
+
{
|
|
14479
|
+
if n.fract() == 0.0 && *n >= i32::MIN as f64 && *n <= i32::MAX as f64 {
|
|
14480
|
+
return Some(*n as i32);
|
|
14481
|
+
}
|
|
14482
|
+
}
|
|
14483
|
+
None
|
|
14484
|
+
}
|
|
14485
|
+
|
|
14086
14486
|
fn fixed_literal_of(e: &Expr) -> Option<String> {
|
|
14087
14487
|
if let Expr::Literal {
|
|
14088
14488
|
value: Literal::Number(n),
|
|
@@ -21824,6 +22224,14 @@ impl Codegen {
|
|
|
21824
22224
|
/// the routing state so call sites de-virtualize. On any failure the state is left empty (path
|
|
21825
22225
|
/// disabled) and nothing is appended to the output.
|
|
21826
22226
|
fn setup_aggregate_fns(&mut self, program: &Program) {
|
|
22227
|
+
// #177 de-virtualization deletes the boxed `let <name> = Value::native(..)` local for every
|
|
22228
|
+
// routed fn. `RustLib` publishes exactly those locals as the crate's public API, so an
|
|
22229
|
+
// exported fn that got de-virtualized would leave the emitted `__tish_publish_exports` call
|
|
22230
|
+
// referencing a binding that no longer exists. The unboxed path is a numeric fast path;
|
|
22231
|
+
// a correct public surface outranks it.
|
|
22232
|
+
if self.emit_mode == crate::NativeEmitMode::RustLib {
|
|
22233
|
+
return;
|
|
22234
|
+
}
|
|
21827
22235
|
let dbg = std::env::var("TISH_AGG_DEBUG").is_ok();
|
|
21828
22236
|
// The unboxed struct alias: the (unique) name `A` used as an `A[]` array param of some fn,
|
|
21829
22237
|
// and registered as an all-`Copy`-field struct alias.
|
|
@@ -23161,6 +23569,83 @@ impl Codegen {
|
|
|
23161
23569
|
// A narrow int storage read (`u8` HP, `i16` tile x) promotes the same way — read
|
|
23162
23570
|
// as its exact `f64` value so arithmetic keeps JS Number semantics; the truncating
|
|
23163
23571
|
// store back to the field happens at the assignment site.
|
|
23572
|
+
// …but when BOTH sides are already integers, that detour is pure loss on a CPU with no
|
|
23573
|
+
// FPU. On ARM7TDMI every f64 op is a soft-float library CALL, so `i + 1` on a `: i32`
|
|
23574
|
+
// counter costs two int→float conversions, a call, and a float→int store. It is why
|
|
23575
|
+
// annotating a loop counter `: i32` measured SLOWER than leaving it a plain number
|
|
23576
|
+
// (examples/bench-ai) — the opposite of what an annotation is for.
|
|
23577
|
+
//
|
|
23578
|
+
// Keeping the pair in integer registers is not an approximation: every i32 is exact in
|
|
23579
|
+
// f64, so `+`, `-` and the relational operators give bit-identical answers in either
|
|
23580
|
+
// domain. Only the operators where the two domains genuinely DISAGREE stay on floats:
|
|
23581
|
+
// `/` JS division is real division — `5 / 2` is 2.5, not 2
|
|
23582
|
+
// `%` integer `%` panics on a zero divisor where JS yields NaN
|
|
23583
|
+
// `*` a product can leave i32's range while staying exact in f64, and the bitwise
|
|
23584
|
+
// lowering below deliberately relies on that headroom (`h * 16777619`)
|
|
23585
|
+
// Operands widen to `i32` first so mixed widths (a `u8` HP against an `i16` tile
|
|
23586
|
+
// coordinate) still agree with JS Number semantics. `u32` is excluded because it is the
|
|
23587
|
+
// one width that does not fit — reading 3 billion `as i32` would flip its sign.
|
|
23588
|
+
// The adds wrap rather than panic, which matches both JS (`to_int32` is modulo 2³²) and
|
|
23589
|
+
// the truncating `as` store every integer target already performs.
|
|
23590
|
+
let fits_i32 = |t: &RustType| {
|
|
23591
|
+
matches!(
|
|
23592
|
+
t,
|
|
23593
|
+
RustType::I32
|
|
23594
|
+
| RustType::I8
|
|
23595
|
+
| RustType::U8
|
|
23596
|
+
| RustType::I16
|
|
23597
|
+
| RustType::U16
|
|
23598
|
+
)
|
|
23599
|
+
};
|
|
23600
|
+
// An integer against a whole-number LITERAL is the shape most of this actually is —
|
|
23601
|
+
// `x > 0`, `i < 100`, `0 - dir`. The literal types as `f64`, which would drag the
|
|
23602
|
+
// integer side out of its register for a soft-float compare over two values that are
|
|
23603
|
+
// both plainly integers. Fold it into the integer domain instead. Only exact whole
|
|
23604
|
+
// numbers inside i32's range qualify, so `x < 2.5` and `x < 3e9` still go to floats,
|
|
23605
|
+
// where they belong.
|
|
23606
|
+
let (l, lt, r, rt) = if fits_i32(<) && rt == RustType::F64 {
|
|
23607
|
+
match Self::int_literal_of(right) {
|
|
23608
|
+
Some(v) => (l, lt, format!("{}i32", v), RustType::I32),
|
|
23609
|
+
None => (l, lt, r, rt),
|
|
23610
|
+
}
|
|
23611
|
+
} else if fits_i32(&rt) && lt == RustType::F64 {
|
|
23612
|
+
match Self::int_literal_of(left) {
|
|
23613
|
+
Some(v) => (format!("{}i32", v), RustType::I32, r, rt),
|
|
23614
|
+
None => (l, lt, r, rt),
|
|
23615
|
+
}
|
|
23616
|
+
} else {
|
|
23617
|
+
(l, lt, r, rt)
|
|
23618
|
+
};
|
|
23619
|
+
if fits_i32(<) && fits_i32(&rt) {
|
|
23620
|
+
let li = format!("(({}) as i32)", l);
|
|
23621
|
+
let ri = format!("(({}) as i32)", r);
|
|
23622
|
+
match op {
|
|
23623
|
+
BinOp::Add => {
|
|
23624
|
+
return Ok((
|
|
23625
|
+
format!("({}).wrapping_add({})", li, ri),
|
|
23626
|
+
RustType::I32,
|
|
23627
|
+
))
|
|
23628
|
+
}
|
|
23629
|
+
BinOp::Sub => {
|
|
23630
|
+
return Ok((
|
|
23631
|
+
format!("({}).wrapping_sub({})", li, ri),
|
|
23632
|
+
RustType::I32,
|
|
23633
|
+
))
|
|
23634
|
+
}
|
|
23635
|
+
BinOp::Lt => return Ok((format!("({} < {})", li, ri), RustType::Bool)),
|
|
23636
|
+
BinOp::Le => return Ok((format!("({} <= {})", li, ri), RustType::Bool)),
|
|
23637
|
+
BinOp::Gt => return Ok((format!("({} > {})", li, ri), RustType::Bool)),
|
|
23638
|
+
BinOp::Ge => return Ok((format!("({} >= {})", li, ri), RustType::Bool)),
|
|
23639
|
+
BinOp::StrictEq => {
|
|
23640
|
+
return Ok((format!("({} == {})", li, ri), RustType::Bool))
|
|
23641
|
+
}
|
|
23642
|
+
BinOp::StrictNe => {
|
|
23643
|
+
return Ok((format!("({} != {})", li, ri), RustType::Bool))
|
|
23644
|
+
}
|
|
23645
|
+
_ => {}
|
|
23646
|
+
}
|
|
23647
|
+
}
|
|
23648
|
+
|
|
23164
23649
|
let (l, lt) = if lt.is_integer_scalar() {
|
|
23165
23650
|
(format!("(({}) as f64)", l), RustType::F64)
|
|
23166
23651
|
} else {
|
|
@@ -24817,8 +25302,21 @@ impl Codegen {
|
|
|
24817
25302
|
// the same local (`aid && out.indexOf(aid)`), and a bare `let __l = aid;` moves the non-`Copy`
|
|
24818
25303
|
// `Value` so the reuse is a borrow-after-move (E0382). The left operand is still evaluated
|
|
24819
25304
|
// exactly once (its side effects don't double-run); only its value is cloned for the test.
|
|
24820
|
-
|
|
24821
|
-
|
|
25305
|
+
//
|
|
25306
|
+
// The RESULT operand has the same hazard: when it lowers to a bare local (`inGenBlock &&
|
|
25307
|
+
// currentTrack`), yielding it moves that local out, so a later read — the next iteration
|
|
25308
|
+
// of the enclosing loop, or any subsequent use — is E0382. Clone it in that case only;
|
|
25309
|
+
// anything else is already a temporary (or already cloned), so the fast path is unchanged.
|
|
25310
|
+
BinOp::And => format!(
|
|
25311
|
+
"{{ let __l = ({}).clone(); if __l.is_truthy() {{ {} }} else {{ __l }} }}",
|
|
25312
|
+
l,
|
|
25313
|
+
Self::clone_if_bare_ident(r)
|
|
25314
|
+
),
|
|
25315
|
+
BinOp::Or => format!(
|
|
25316
|
+
"{{ let __l = ({}).clone(); if __l.is_truthy() {{ __l }} else {{ {} }} }}",
|
|
25317
|
+
l,
|
|
25318
|
+
Self::clone_if_bare_ident(r)
|
|
25319
|
+
),
|
|
24822
25320
|
BinOp::BitAnd => Self::emit_bitwise_binop(l, r, "&"),
|
|
24823
25321
|
BinOp::BitOr => Self::emit_bitwise_binop(l, r, "|"),
|
|
24824
25322
|
BinOp::BitXor => Self::emit_bitwise_binop(l, r, "^"),
|