@tishlang/tish-lsp 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tish-lsp +0 -0
- package/crates/tish/tests/scope_merge.rs +31 -0
- package/crates/tish_build_utils/src/lib.rs +131 -6
- package/crates/tish_compile/src/codegen.rs +213 -16
- package/crates/tish_compile/src/resolve.rs +12 -2
- package/crates/tish_core/src/shape.rs +28 -3
- package/crates/tish_lsp/Cargo.toml +1 -1
- package/crates/tish_native/src/build.rs +4 -0
- package/crates/tish_runtime/Cargo.toml +20 -1
- package/crates/tish_runtime/src/crypto.rs +127 -0
- package/crates/tish_runtime/src/encoding.rs +157 -0
- package/crates/tish_runtime/src/fs_ext.rs +27 -0
- package/crates/tish_runtime/src/lib.rs +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/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
package/bin/tish-lsp
CHANGED
|
Binary file
|
|
@@ -77,3 +77,34 @@ fn scope_exports_tish_merges_as_source() {
|
|
|
77
77
|
fn scope_plain_main_merges_as_source() {
|
|
78
78
|
assert_merged_as_source(&setup(r#","main":"index.tish""#));
|
|
79
79
|
}
|
|
80
|
+
|
|
81
|
+
/// The other half of the #38 split: a `@scope/pkg` that opts in via `tish.crate` (the tish-apple
|
|
82
|
+
/// hosts, published as `@tishlang/tish-macos` / `@tishlang/tish-ios`) routes to the NATIVE path —
|
|
83
|
+
/// lowered to `NativeModuleLoad`, not merged as source.
|
|
84
|
+
#[test]
|
|
85
|
+
fn scope_tish_crate_routes_native() {
|
|
86
|
+
let dir = setup(r#","tish":{"module":true,"crate":"test-greet","export":"greet_object"}"#);
|
|
87
|
+
let root = dir.path();
|
|
88
|
+
let modules = resolve_project(&root.join("main.tish"), Some(root)).expect("resolve");
|
|
89
|
+
let merged = merge_modules(modules).expect("merge");
|
|
90
|
+
let native_scope = merged.program.statements.iter().any(|s| {
|
|
91
|
+
matches!(
|
|
92
|
+
s,
|
|
93
|
+
Statement::VarDecl { init: Some(Expr::NativeModuleLoad { spec, .. }), .. }
|
|
94
|
+
if spec.as_ref() == "@test/greet"
|
|
95
|
+
)
|
|
96
|
+
});
|
|
97
|
+
assert!(
|
|
98
|
+
native_scope,
|
|
99
|
+
"@scope package with tish.crate must lower to NativeModuleLoad (native Rust-crate path)"
|
|
100
|
+
);
|
|
101
|
+
let has_greet = merged
|
|
102
|
+
.program
|
|
103
|
+
.statements
|
|
104
|
+
.iter()
|
|
105
|
+
.any(|s| matches!(s, Statement::FunDecl { name, .. } if name.as_ref() == "greet"));
|
|
106
|
+
assert!(
|
|
107
|
+
!has_greet,
|
|
108
|
+
"@scope package with tish.crate must NOT be merged as Tish source"
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
//! Provides workspace discovery, path resolution, and Cargo build orchestration
|
|
4
4
|
//! used by tishlang_wasm, tishlang_cranelift, tishlang_native, and the tish CLI.
|
|
5
5
|
|
|
6
|
+
use std::collections::BTreeSet;
|
|
6
7
|
use std::fs;
|
|
7
8
|
use std::path::{Path, PathBuf};
|
|
8
9
|
use std::process::Command;
|
|
9
|
-
use std::sync::Mutex;
|
|
10
|
+
use std::sync::{Mutex, OnceLock};
|
|
10
11
|
|
|
11
12
|
/// Serialize nested `cargo build` calls that share the workspace `target/` dir (tests + `tish build`).
|
|
12
13
|
static NESTED_CARGO_MUTEX: Mutex<()> = Mutex::new(());
|
|
@@ -19,6 +20,56 @@ fn mold_available() -> bool {
|
|
|
19
20
|
.unwrap_or(false)
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
/// The `target_feature="..."` values in `rustc --print cfg` output.
|
|
24
|
+
fn parse_target_features(print_cfg_stdout: &str) -> BTreeSet<String> {
|
|
25
|
+
print_cfg_stdout
|
|
26
|
+
.lines()
|
|
27
|
+
.filter_map(|l| l.trim().strip_prefix("target_feature=\""))
|
|
28
|
+
.filter_map(|l| l.strip_suffix('"'))
|
|
29
|
+
.map(str::to_owned)
|
|
30
|
+
.collect()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Host target features rustc reports, optionally under one extra `-C` flag.
|
|
34
|
+
fn rustc_target_features(codegen_flag: Option<&str>) -> Option<BTreeSet<String>> {
|
|
35
|
+
let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string());
|
|
36
|
+
let mut cmd = Command::new(rustc);
|
|
37
|
+
cmd.args(["--print", "cfg"]);
|
|
38
|
+
if let Some(flag) = codegen_flag {
|
|
39
|
+
cmd.args(["-C", flag]);
|
|
40
|
+
}
|
|
41
|
+
let out = cmd.output().ok()?;
|
|
42
|
+
if !out.status.success() {
|
|
43
|
+
return None;
|
|
44
|
+
}
|
|
45
|
+
Some(parse_target_features(&String::from_utf8_lossy(&out.stdout)))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// True when `-C target-cpu=native` is worth passing on this host.
|
|
49
|
+
///
|
|
50
|
+
/// `native` asks LLVM to identify the host core. On hosts it cannot identify — notably
|
|
51
|
+
/// the virtualized Apple Silicon behind GitHub Actions' macOS runners — it answers
|
|
52
|
+
/// `generic`, which is *narrower* than the target's own default CPU. On
|
|
53
|
+
/// aarch64-apple-darwin that silently drops `aes` and `sha2`, and crates that assert
|
|
54
|
+
/// those are statically present then fail to compile at all; `ring` 0.17 trips
|
|
55
|
+
/// `assertion failed: CAPS_STATIC == MIN_STATIC_FEATURES` in const-eval.
|
|
56
|
+
///
|
|
57
|
+
/// So this compares feature sets rather than trusting `native` blindly: keep the flag
|
|
58
|
+
/// only when it is a strict win (every default feature survives). If rustc cannot be
|
|
59
|
+
/// queried, skip the flag — the target default is always correct, just less tuned.
|
|
60
|
+
fn native_target_cpu_is_safe() -> bool {
|
|
61
|
+
static SAFE: OnceLock<bool> = OnceLock::new();
|
|
62
|
+
*SAFE.get_or_init(|| {
|
|
63
|
+
match (
|
|
64
|
+
rustc_target_features(None),
|
|
65
|
+
rustc_target_features(Some("target-cpu=native")),
|
|
66
|
+
) {
|
|
67
|
+
(Some(default), Some(native)) => default.is_subset(&native),
|
|
68
|
+
_ => false,
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
22
73
|
/// True if `root` looks like the Tish language repo (has `crates/tish_runtime`).
|
|
23
74
|
///
|
|
24
75
|
/// Used so we do not treat unrelated workspaces (e.g. a parent `zectre-platform` repo) as Tish
|
|
@@ -347,7 +398,13 @@ pub fn cargo_target_name(stem: &str) -> String {
|
|
|
347
398
|
// A leading digit is prefixed with `_`; an empty result falls back to a fixed default.
|
|
348
399
|
let mut name: String = stem
|
|
349
400
|
.chars()
|
|
350
|
-
.map(|c|
|
|
401
|
+
.map(|c| {
|
|
402
|
+
if c.is_ascii_alphanumeric() || c == '_' {
|
|
403
|
+
c
|
|
404
|
+
} else {
|
|
405
|
+
'_'
|
|
406
|
+
}
|
|
407
|
+
})
|
|
351
408
|
.collect();
|
|
352
409
|
if name.is_empty() {
|
|
353
410
|
return "tish_output".to_string();
|
|
@@ -371,6 +428,10 @@ pub fn create_build_dir(prefix: &str, out_name: &str) -> Result<PathBuf, String>
|
|
|
371
428
|
}
|
|
372
429
|
|
|
373
430
|
/// Run cargo build in the given directory.
|
|
431
|
+
///
|
|
432
|
+
/// `-C target-cpu=native` is added only for host builds, only when the caller has not
|
|
433
|
+
/// already pinned a `target-cpu` in `RUSTFLAGS`, and only when `native` does not lose
|
|
434
|
+
/// target features against the target default (see `native_target_cpu_is_safe`).
|
|
374
435
|
/// If `cross_target` is Some, passes `--target` and skips `-C target-cpu=native`.
|
|
375
436
|
pub fn run_cargo_build(
|
|
376
437
|
build_dir: &Path,
|
|
@@ -395,10 +456,14 @@ pub fn run_cargo_build(
|
|
|
395
456
|
merged_rustflags = format!("{} -C link-arg=-fuse-ld=mold", merged_rustflags.trim());
|
|
396
457
|
merged_rustflags = merged_rustflags.trim().to_string();
|
|
397
458
|
}
|
|
398
|
-
} else if merged_rustflags.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
merged_rustflags =
|
|
459
|
+
} else if !merged_rustflags.contains("target-cpu") && native_target_cpu_is_safe() {
|
|
460
|
+
// Caller did not pin a CPU and `native` resolves to something at least as
|
|
461
|
+
// capable as the target default — see `native_target_cpu_is_safe`.
|
|
462
|
+
merged_rustflags = if merged_rustflags.is_empty() {
|
|
463
|
+
"-C target-cpu=native".to_string()
|
|
464
|
+
} else {
|
|
465
|
+
format!("{} -C target-cpu=native", merged_rustflags)
|
|
466
|
+
};
|
|
402
467
|
}
|
|
403
468
|
|
|
404
469
|
let mut cmd = Command::new("cargo");
|
|
@@ -438,6 +503,66 @@ pub fn run_cargo_build(
|
|
|
438
503
|
mod protoc_tests {
|
|
439
504
|
use super::*;
|
|
440
505
|
|
|
506
|
+
/// Trimmed `rustc --print cfg --target aarch64-apple-darwin`.
|
|
507
|
+
const APPLE_DARWIN_DEFAULT_CFG: &str = r#"
|
|
508
|
+
debug_assertions
|
|
509
|
+
panic="unwind"
|
|
510
|
+
target_arch="aarch64"
|
|
511
|
+
target_feature="aes"
|
|
512
|
+
target_feature="neon"
|
|
513
|
+
target_feature="sha2"
|
|
514
|
+
target_feature="sha3"
|
|
515
|
+
target_os="macos"
|
|
516
|
+
"#;
|
|
517
|
+
|
|
518
|
+
#[test]
|
|
519
|
+
fn parses_target_features_from_print_cfg() {
|
|
520
|
+
let features = parse_target_features(APPLE_DARWIN_DEFAULT_CFG);
|
|
521
|
+
assert_eq!(
|
|
522
|
+
features.iter().map(String::as_str).collect::<Vec<_>>(),
|
|
523
|
+
["aes", "neon", "sha2", "sha3"]
|
|
524
|
+
);
|
|
525
|
+
// Non-`target_feature` keys never leak in, and neither do the quotes.
|
|
526
|
+
assert!(!features.contains("aarch64"));
|
|
527
|
+
assert!(!features.contains("\"aes\""));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
#[test]
|
|
531
|
+
fn native_cpu_rejected_when_it_drops_target_features() {
|
|
532
|
+
// What GitHub's virtualized Apple Silicon reports: `native` -> `generic`, which
|
|
533
|
+
// keeps only `neon`. Pinning it would break `ring`'s static feature assertions.
|
|
534
|
+
let default = parse_target_features(APPLE_DARWIN_DEFAULT_CFG);
|
|
535
|
+
let generic = parse_target_features("target_feature=\"neon\"\n");
|
|
536
|
+
assert!(!default.is_subset(&generic));
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
#[test]
|
|
540
|
+
fn native_cpu_accepted_when_it_only_adds() {
|
|
541
|
+
let default = parse_target_features(APPLE_DARWIN_DEFAULT_CFG);
|
|
542
|
+
let apple_m1 = parse_target_features(
|
|
543
|
+
"target_feature=\"aes\"\ntarget_feature=\"neon\"\n\
|
|
544
|
+
target_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"lse\"\n",
|
|
545
|
+
);
|
|
546
|
+
assert!(default.is_subset(&apple_m1));
|
|
547
|
+
// Identical sets count as safe too — `native` matching the default is a no-op.
|
|
548
|
+
assert!(default.is_subset(&default));
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
#[test]
|
|
552
|
+
fn live_rustc_probe_still_parses() {
|
|
553
|
+
// Guards the parse against a `rustc --print cfg` format change: every target Tish
|
|
554
|
+
// builds on has at least one baseline target feature (x86_64 sse2, aarch64 neon).
|
|
555
|
+
// If this ever comes back empty the probe would silently disable target-cpu=native
|
|
556
|
+
// everywhere rather than only on hosts LLVM cannot identify.
|
|
557
|
+
let Some(features) = rustc_target_features(None) else {
|
|
558
|
+
return; // no rustc on PATH — nothing to assert
|
|
559
|
+
};
|
|
560
|
+
assert!(
|
|
561
|
+
!features.is_empty(),
|
|
562
|
+
"no target_feature in rustc --print cfg"
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
441
566
|
#[test]
|
|
442
567
|
fn cargo_target_name_replaces_hyphens() {
|
|
443
568
|
assert_eq!(cargo_target_name("hello-ios"), "hello_ios");
|
|
@@ -1018,6 +1018,9 @@ pub(crate) struct Codegen {
|
|
|
1018
1018
|
features: std::collections::HashSet<String>,
|
|
1019
1019
|
/// spec -> native init strategy (legacy adapter object vs generated `generated_native` wrapper)
|
|
1020
1020
|
native_module_init: std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
|
|
1021
|
+
/// spec -> the `run()`-local holding that module's namespace object, bound ONCE in the preamble.
|
|
1022
|
+
/// Building it is O(exports) — see `emit_native_namespace_preamble`.
|
|
1023
|
+
native_ns_locals: std::collections::HashMap<String, String>,
|
|
1021
1024
|
/// Typed native imports (`declare fun` + a native import): tish name -> its typed signature.
|
|
1022
1025
|
/// A matching call emits a DIRECT `<crate>::name_typed(..)` instead of a boxed `value_call`.
|
|
1023
1026
|
extern_fns: std::collections::HashMap<String, ExternSig>,
|
|
@@ -1292,6 +1295,7 @@ impl Codegen {
|
|
|
1292
1295
|
string_charset: String::new(),
|
|
1293
1296
|
features,
|
|
1294
1297
|
native_module_init,
|
|
1298
|
+
native_ns_locals: std::collections::HashMap::new(),
|
|
1295
1299
|
extern_fns: std::collections::HashMap::new(),
|
|
1296
1300
|
user_fn_returns: std::collections::HashMap::new(),
|
|
1297
1301
|
async_context_stack: Vec::new(),
|
|
@@ -2044,6 +2048,48 @@ impl Codegen {
|
|
|
2044
2048
|
}
|
|
2045
2049
|
}
|
|
2046
2050
|
|
|
2051
|
+
/// The Rust expression that CONSTRUCTS a native module's namespace object. O(exports): it inserts
|
|
2052
|
+
/// every function the crate exports into a fresh map. Call it once per module, not once per import.
|
|
2053
|
+
fn native_module_ns_ctor(init: &crate::resolve::NativeModuleInit) -> String {
|
|
2054
|
+
match init {
|
|
2055
|
+
crate::resolve::NativeModuleInit::Legacy {
|
|
2056
|
+
crate_name,
|
|
2057
|
+
export_fn,
|
|
2058
|
+
} => format!("{}::{}()", crate_name, export_fn),
|
|
2059
|
+
crate::resolve::NativeModuleInit::Generated { export_fn, .. } => {
|
|
2060
|
+
format!("crate::generated_native::{}()", export_fn)
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
/// Bind every native module's namespace object to a `run()`-local, ONCE, before any import reads
|
|
2066
|
+
/// from one.
|
|
2067
|
+
///
|
|
2068
|
+
/// Each named import lowers to "get this key out of the module's namespace". Building that
|
|
2069
|
+
/// namespace is O(exports) — a fresh `ObjectMap` with an `Arc<str>` key and a native closure per
|
|
2070
|
+
/// exported function — so doing it at every import site is O(imports x exports): importing 68
|
|
2071
|
+
/// names from a crate that exports 68 cost 4,624 allocations to bind 68 functions. On a GBA that
|
|
2072
|
+
/// was measured at 3 seconds of black screen (`tish-gba/examples/bench-boot`), paid before the
|
|
2073
|
+
/// program's first statement; `packages/engine.tish` there imports 117 symbols across two crates
|
|
2074
|
+
/// and cost 4.3s. Hoisting makes it O(imports + exports).
|
|
2075
|
+
///
|
|
2076
|
+
/// Emitted in sorted order so the generated Rust is byte-identical across builds (the map is a
|
|
2077
|
+
/// `HashMap`, whose iteration order is not).
|
|
2078
|
+
fn emit_native_namespace_preamble(&mut self) {
|
|
2079
|
+
let mut specs: Vec<String> = self.native_module_init.keys().cloned().collect();
|
|
2080
|
+
specs.sort();
|
|
2081
|
+
for (i, spec) in specs.iter().enumerate() {
|
|
2082
|
+
// Index rather than a sanitized spec: `cargo:a-b` and `cargo:a_b` sanitize to the same
|
|
2083
|
+
// identifier, and silently binding one module's namespace under another's name would be a
|
|
2084
|
+
// very quiet bug.
|
|
2085
|
+
let local = format!("__tish_native_ns_{}", i);
|
|
2086
|
+
let ctor = Self::native_module_ns_ctor(&self.native_module_init[spec]);
|
|
2087
|
+
// A module can be resolved but have every one of its imports shadowed or unused.
|
|
2088
|
+
self.writeln(&format!("#[allow(unused_variables)] let {} = {};", local, ctor));
|
|
2089
|
+
self.native_ns_locals.insert(spec.clone(), local);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2047
2093
|
/// Map native module spec to Rust init expression using resolved package.json modules.
|
|
2048
2094
|
/// For built-in modules (tish:fs, tish:http, tish:process), use builtin_native_module_rust_init.
|
|
2049
2095
|
fn native_module_rust_init(&self, spec: &str, export_name: &str) -> Option<String> {
|
|
@@ -2053,19 +2099,18 @@ impl Codegen {
|
|
|
2053
2099
|
self.native_module_init.get(spec).map(|init| {
|
|
2054
2100
|
// Native modules return a namespace object (like an ES module).
|
|
2055
2101
|
// 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
|
-
)
|
|
2102
|
+
// Read it from the hoisted local when there is one; the inline constructor is the fallback
|
|
2103
|
+
// for anything emitted outside `run()`'s preamble.
|
|
2104
|
+
match self.native_ns_locals.get(spec) {
|
|
2105
|
+
Some(local) => format!(
|
|
2106
|
+
"{{ match &{} {{ Value::Object(ref _o) => _o.borrow().strings.get({:?}).cloned().unwrap_or(Value::Null), _ => Value::Null }} }}",
|
|
2107
|
+
local, export_name
|
|
2108
|
+
),
|
|
2109
|
+
None => format!(
|
|
2110
|
+
"{{ let _ns = {}; match _ns {{ Value::Object(ref _o) => _o.borrow().strings.get({:?}).cloned().unwrap_or(Value::Null), _ => Value::Null }} }}",
|
|
2111
|
+
Self::native_module_ns_ctor(init), export_name
|
|
2112
|
+
),
|
|
2113
|
+
}
|
|
2069
2114
|
})
|
|
2070
2115
|
}
|
|
2071
2116
|
|
|
@@ -2098,6 +2143,7 @@ impl Codegen {
|
|
|
2098
2143
|
"realpath" | "realpathSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::realpath(args))"),
|
|
2099
2144
|
"readlink" | "readlinkSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::readlink(args))"),
|
|
2100
2145
|
"truncate" | "truncateSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::truncate(args))"),
|
|
2146
|
+
"chmod" | "chmodSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::chmod(args))"),
|
|
2101
2147
|
"mkdtemp" | "mkdtempSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::mkdtemp(args))"),
|
|
2102
2148
|
"cp" | "cpSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::cp(args))"),
|
|
2103
2149
|
"access" | "accessSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::access(args))"),
|
|
@@ -2121,6 +2167,7 @@ impl Codegen {
|
|
|
2121
2167
|
"realpath" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::realpath_promise(args))"),
|
|
2122
2168
|
"readlink" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::readlink_promise(args))"),
|
|
2123
2169
|
"truncate" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::truncate_promise(args))"),
|
|
2170
|
+
"chmod" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::chmod_promise(args))"),
|
|
2124
2171
|
"mkdtemp" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::mkdtemp_promise(args))"),
|
|
2125
2172
|
"cp" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::cp_promise(args))"),
|
|
2126
2173
|
"access" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::access_promise(args))"),
|
|
@@ -2155,9 +2202,20 @@ impl Codegen {
|
|
|
2155
2202
|
"execFile" => Some("Value::native(|args: &[Value]| tish_process_exec_file(args))"),
|
|
2156
2203
|
"execCapture" => Some("Value::native(|args: &[Value]| tish_process_exec_capture(args))"),
|
|
2157
2204
|
"execFileCapture" => Some("Value::native(|args: &[Value]| tish_process_exec_file_capture(args))"),
|
|
2205
|
+
"spawn" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_spawn(args))"),
|
|
2206
|
+
"readStdout" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_read_stdout(args))"),
|
|
2207
|
+
"readStderr" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_read_stderr(args))"),
|
|
2208
|
+
"writeStdin" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_write_stdin(args))"),
|
|
2209
|
+
"closeStdin" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_close_stdin(args))"),
|
|
2210
|
+
"wait" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_wait(args))"),
|
|
2211
|
+
"kill" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_kill(args))"),
|
|
2212
|
+
"pid" => Some("Value::native(|args: &[Value]| tishlang_runtime::process_pid(args))"),
|
|
2158
2213
|
"argv" => Some("Value::Array(VmRef::new(std::env::args().map(|s| Value::String(s.into())).collect()))"),
|
|
2159
2214
|
"env" => Some("Value::object(std::env::vars().map(|(k,v)| (Arc::from(k.as_str()), Value::String(v.into()))).collect())"),
|
|
2160
|
-
|
|
2215
|
+
// Node-style platform string so `process.platform` reads identically on the JS
|
|
2216
|
+
// target (Node's real global) and native (this): macos->darwin, windows->win32.
|
|
2217
|
+
"platform" => Some("Value::String(match std::env::consts::OS { \"macos\" => \"darwin\", \"windows\" => \"win32\", o => o }.into())"),
|
|
2218
|
+
"process" => Some("{ let mut m = ObjectMap::default(); m.insert(Arc::from(\"exit\"), Value::native(|args: &[Value]| tish_process_exit(args))); m.insert(Arc::from(\"cwd\"), Value::native(|args: &[Value]| tish_process_cwd(args))); m.insert(Arc::from(\"exec\"), Value::native(|args: &[Value]| tish_process_exec(args))); m.insert(Arc::from(\"execFile\"), Value::native(|args: &[Value]| tish_process_exec_file(args))); m.insert(Arc::from(\"execCapture\"), Value::native(|args: &[Value]| tish_process_exec_capture(args))); m.insert(Arc::from(\"execFileCapture\"), Value::native(|args: &[Value]| tish_process_exec_file_capture(args))); m.insert(Arc::from(\"argv\"), Value::Array(VmRef::new(std::env::args().map(|s| Value::String(s.into())).collect()))); m.insert(Arc::from(\"env\"), Value::object(std::env::vars().map(|(k,v)| (Arc::from(k.as_str()), Value::String(v.into()))).collect::<ObjectMap>())); m.insert(Arc::from(\"platform\"), Value::String(match std::env::consts::OS { \"macos\" => \"darwin\", \"windows\" => \"win32\", o => o }.into())); Value::object(m) }"),
|
|
2161
2219
|
_ => None,
|
|
2162
2220
|
},
|
|
2163
2221
|
"tish:ws" if self.has_feature("ws") => match export_name {
|
|
@@ -2187,6 +2245,37 @@ impl Codegen {
|
|
|
2187
2245
|
"pid" => Some("Value::native(|args: &[Value]| tishlang_runtime::pty_pid(args))"),
|
|
2188
2246
|
_ => None,
|
|
2189
2247
|
},
|
|
2248
|
+
"tish:net" if self.has_feature("net") => match export_name {
|
|
2249
|
+
"connect" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_connect(args))"),
|
|
2250
|
+
"read" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_read(args))"),
|
|
2251
|
+
"write" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_write(args))"),
|
|
2252
|
+
"close" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_close(args))"),
|
|
2253
|
+
"listen" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_listen(args))"),
|
|
2254
|
+
"accept" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_accept(args))"),
|
|
2255
|
+
"probe" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_probe(args))"),
|
|
2256
|
+
"sleep" => Some("Value::native(|args: &[Value]| tishlang_runtime::net_sleep(args))"),
|
|
2257
|
+
_ => None,
|
|
2258
|
+
},
|
|
2259
|
+
"tish:encoding" if self.has_feature("encoding") => match export_name {
|
|
2260
|
+
"base64Encode" => Some("Value::native(|args: &[Value]| tishlang_runtime::base64_encode(args))"),
|
|
2261
|
+
"base64Decode" => Some("Value::native(|args: &[Value]| tishlang_runtime::base64_decode(args))"),
|
|
2262
|
+
"base64UrlEncode" => Some("Value::native(|args: &[Value]| tishlang_runtime::base64_url_encode(args))"),
|
|
2263
|
+
"base64UrlDecode" => Some("Value::native(|args: &[Value]| tishlang_runtime::base64_url_decode(args))"),
|
|
2264
|
+
"utf8Decode" => Some("Value::native(|args: &[Value]| tishlang_runtime::utf8_decode(args))"),
|
|
2265
|
+
"utf8Encode" => Some("Value::native(|args: &[Value]| tishlang_runtime::utf8_encode(args))"),
|
|
2266
|
+
_ => None,
|
|
2267
|
+
},
|
|
2268
|
+
"tish:crypto" if self.has_feature("crypto") => match export_name {
|
|
2269
|
+
"sha256" => Some("Value::native(|args: &[Value]| tishlang_runtime::sha256(args))"),
|
|
2270
|
+
"sha256Hex" => Some("Value::native(|args: &[Value]| tishlang_runtime::sha256_hex(args))"),
|
|
2271
|
+
"randomBytes" => Some("Value::native(|args: &[Value]| tishlang_runtime::random_bytes(args))"),
|
|
2272
|
+
_ => None,
|
|
2273
|
+
},
|
|
2274
|
+
"tish:zip" if self.has_feature("zip") => match export_name {
|
|
2275
|
+
"extract" => Some("Value::native(|args: &[Value]| tishlang_runtime::zip_extract(args))"),
|
|
2276
|
+
"entries" => Some("Value::native(|args: &[Value]| tishlang_runtime::zip_entries(args))"),
|
|
2277
|
+
_ => None,
|
|
2278
|
+
},
|
|
2190
2279
|
_ => return None,
|
|
2191
2280
|
};
|
|
2192
2281
|
init.map(String::from)
|
|
@@ -2196,7 +2285,8 @@ impl Codegen {
|
|
|
2196
2285
|
if self.features.contains("full") {
|
|
2197
2286
|
matches!(
|
|
2198
2287
|
name,
|
|
2199
|
-
"http" | "timers" | "fs" | "process" | "regex" | "ws" | "tty" | "pty"
|
|
2288
|
+
"http" | "timers" | "fs" | "process" | "regex" | "ws" | "tty" | "pty" | "net"
|
|
2289
|
+
| "encoding" | "crypto" | "zip"
|
|
2200
2290
|
)
|
|
2201
2291
|
} else {
|
|
2202
2292
|
self.features.contains(name)
|
|
@@ -3088,6 +3178,9 @@ impl Codegen {
|
|
|
3088
3178
|
}
|
|
3089
3179
|
self.indent += 1;
|
|
3090
3180
|
|
|
3181
|
+
// Before anything else: one namespace object per native module, shared by all of its imports.
|
|
3182
|
+
self.emit_native_namespace_preamble();
|
|
3183
|
+
|
|
3091
3184
|
// Initialize builtins
|
|
3092
3185
|
self.writeln("let mut console = Value::object(ObjectMap::from([");
|
|
3093
3186
|
self.indent += 1;
|
|
@@ -3331,6 +3424,9 @@ impl Codegen {
|
|
|
3331
3424
|
self.indent -= 1;
|
|
3332
3425
|
self.writeln("}");
|
|
3333
3426
|
self.writeln("p.insert(Arc::from(\"env\"), Value::object(env_obj));");
|
|
3427
|
+
// Node-style platform string (macos->darwin, windows->win32) so bare `process.platform`
|
|
3428
|
+
// matches the JS target's Node global and the named `tish:process` export.
|
|
3429
|
+
self.writeln("p.insert(Arc::from(\"platform\"), Value::String(match std::env::consts::OS { \"macos\" => \"darwin\", \"windows\" => \"win32\", o => o }.into()));");
|
|
3334
3430
|
self.writeln("p");
|
|
3335
3431
|
self.indent -= 1;
|
|
3336
3432
|
self.writeln("});");
|
|
@@ -3612,9 +3708,17 @@ impl Codegen {
|
|
|
3612
3708
|
let escaped = Self::escape_ident(name.as_ref());
|
|
3613
3709
|
return Ok(format!("{} = ({}) as i32", escaped, int_code));
|
|
3614
3710
|
}
|
|
3711
|
+
let (val_code, val_ty) = self.emit_typed_expr(value)?;
|
|
3712
|
+
// An RHS that is ALREADY an integer stores straight into the register. `d = d + 1`
|
|
3713
|
+
// reaches here — it is arithmetic, not a bitwise chain, so `emit_int32_operand`
|
|
3714
|
+
// declines it — and boxing that i32 into a `Value::Number` only to run ToInt32 back
|
|
3715
|
+
// out of it was the whole cost of incrementing an annotated counter.
|
|
3716
|
+
let escaped_int = Self::escape_ident(name.as_ref());
|
|
3717
|
+
if val_ty.is_integer_scalar() {
|
|
3718
|
+
return Ok(format!("{} = ({}) as i32", escaped_int, val_code));
|
|
3719
|
+
}
|
|
3615
3720
|
// Defensive: gate guarantees `Some`, but if a future RHS shape slips through,
|
|
3616
3721
|
// fall back to a sound f64-narrowed store rather than miscompiling.
|
|
3617
|
-
let (val_code, val_ty) = self.emit_typed_expr(value)?;
|
|
3618
3722
|
let v = if val_ty.is_native() {
|
|
3619
3723
|
val_ty.to_value_expr(&val_code)
|
|
3620
3724
|
} else {
|
|
@@ -14083,6 +14187,22 @@ impl Codegen {
|
|
|
14083
14187
|
/// native `Num<i32,8>` arithmetic instead of boxing the literal side through `f64`. Uses the
|
|
14084
14188
|
/// same truncating `(n * 256) as i32` conversion as the dynamic `fixed`↔`Value` boundary, so
|
|
14085
14189
|
/// the constant is bit-identical to the boxed path.
|
|
14190
|
+
/// A numeric literal that is EXACTLY a whole number inside `i32`'s range, as an `i32`. Used to keep
|
|
14191
|
+
/// `x > 0` / `i < 100` in integer registers when the other side is already an integer — a fraction
|
|
14192
|
+
/// or an out-of-range magnitude returns `None` and stays on the `f64` path, where it is correct.
|
|
14193
|
+
fn int_literal_of(e: &Expr) -> Option<i32> {
|
|
14194
|
+
if let Expr::Literal {
|
|
14195
|
+
value: Literal::Number(n),
|
|
14196
|
+
..
|
|
14197
|
+
} = e
|
|
14198
|
+
{
|
|
14199
|
+
if n.fract() == 0.0 && *n >= i32::MIN as f64 && *n <= i32::MAX as f64 {
|
|
14200
|
+
return Some(*n as i32);
|
|
14201
|
+
}
|
|
14202
|
+
}
|
|
14203
|
+
None
|
|
14204
|
+
}
|
|
14205
|
+
|
|
14086
14206
|
fn fixed_literal_of(e: &Expr) -> Option<String> {
|
|
14087
14207
|
if let Expr::Literal {
|
|
14088
14208
|
value: Literal::Number(n),
|
|
@@ -23161,6 +23281,83 @@ impl Codegen {
|
|
|
23161
23281
|
// A narrow int storage read (`u8` HP, `i16` tile x) promotes the same way — read
|
|
23162
23282
|
// as its exact `f64` value so arithmetic keeps JS Number semantics; the truncating
|
|
23163
23283
|
// store back to the field happens at the assignment site.
|
|
23284
|
+
// …but when BOTH sides are already integers, that detour is pure loss on a CPU with no
|
|
23285
|
+
// FPU. On ARM7TDMI every f64 op is a soft-float library CALL, so `i + 1` on a `: i32`
|
|
23286
|
+
// counter costs two int→float conversions, a call, and a float→int store. It is why
|
|
23287
|
+
// annotating a loop counter `: i32` measured SLOWER than leaving it a plain number
|
|
23288
|
+
// (examples/bench-ai) — the opposite of what an annotation is for.
|
|
23289
|
+
//
|
|
23290
|
+
// Keeping the pair in integer registers is not an approximation: every i32 is exact in
|
|
23291
|
+
// f64, so `+`, `-` and the relational operators give bit-identical answers in either
|
|
23292
|
+
// domain. Only the operators where the two domains genuinely DISAGREE stay on floats:
|
|
23293
|
+
// `/` JS division is real division — `5 / 2` is 2.5, not 2
|
|
23294
|
+
// `%` integer `%` panics on a zero divisor where JS yields NaN
|
|
23295
|
+
// `*` a product can leave i32's range while staying exact in f64, and the bitwise
|
|
23296
|
+
// lowering below deliberately relies on that headroom (`h * 16777619`)
|
|
23297
|
+
// Operands widen to `i32` first so mixed widths (a `u8` HP against an `i16` tile
|
|
23298
|
+
// coordinate) still agree with JS Number semantics. `u32` is excluded because it is the
|
|
23299
|
+
// one width that does not fit — reading 3 billion `as i32` would flip its sign.
|
|
23300
|
+
// The adds wrap rather than panic, which matches both JS (`to_int32` is modulo 2³²) and
|
|
23301
|
+
// the truncating `as` store every integer target already performs.
|
|
23302
|
+
let fits_i32 = |t: &RustType| {
|
|
23303
|
+
matches!(
|
|
23304
|
+
t,
|
|
23305
|
+
RustType::I32
|
|
23306
|
+
| RustType::I8
|
|
23307
|
+
| RustType::U8
|
|
23308
|
+
| RustType::I16
|
|
23309
|
+
| RustType::U16
|
|
23310
|
+
)
|
|
23311
|
+
};
|
|
23312
|
+
// An integer against a whole-number LITERAL is the shape most of this actually is —
|
|
23313
|
+
// `x > 0`, `i < 100`, `0 - dir`. The literal types as `f64`, which would drag the
|
|
23314
|
+
// integer side out of its register for a soft-float compare over two values that are
|
|
23315
|
+
// both plainly integers. Fold it into the integer domain instead. Only exact whole
|
|
23316
|
+
// numbers inside i32's range qualify, so `x < 2.5` and `x < 3e9` still go to floats,
|
|
23317
|
+
// where they belong.
|
|
23318
|
+
let (l, lt, r, rt) = if fits_i32(<) && rt == RustType::F64 {
|
|
23319
|
+
match Self::int_literal_of(right) {
|
|
23320
|
+
Some(v) => (l, lt, format!("{}i32", v), RustType::I32),
|
|
23321
|
+
None => (l, lt, r, rt),
|
|
23322
|
+
}
|
|
23323
|
+
} else if fits_i32(&rt) && lt == RustType::F64 {
|
|
23324
|
+
match Self::int_literal_of(left) {
|
|
23325
|
+
Some(v) => (format!("{}i32", v), RustType::I32, r, rt),
|
|
23326
|
+
None => (l, lt, r, rt),
|
|
23327
|
+
}
|
|
23328
|
+
} else {
|
|
23329
|
+
(l, lt, r, rt)
|
|
23330
|
+
};
|
|
23331
|
+
if fits_i32(<) && fits_i32(&rt) {
|
|
23332
|
+
let li = format!("(({}) as i32)", l);
|
|
23333
|
+
let ri = format!("(({}) as i32)", r);
|
|
23334
|
+
match op {
|
|
23335
|
+
BinOp::Add => {
|
|
23336
|
+
return Ok((
|
|
23337
|
+
format!("({}).wrapping_add({})", li, ri),
|
|
23338
|
+
RustType::I32,
|
|
23339
|
+
))
|
|
23340
|
+
}
|
|
23341
|
+
BinOp::Sub => {
|
|
23342
|
+
return Ok((
|
|
23343
|
+
format!("({}).wrapping_sub({})", li, ri),
|
|
23344
|
+
RustType::I32,
|
|
23345
|
+
))
|
|
23346
|
+
}
|
|
23347
|
+
BinOp::Lt => return Ok((format!("({} < {})", li, ri), RustType::Bool)),
|
|
23348
|
+
BinOp::Le => return Ok((format!("({} <= {})", li, ri), RustType::Bool)),
|
|
23349
|
+
BinOp::Gt => return Ok((format!("({} > {})", li, ri), RustType::Bool)),
|
|
23350
|
+
BinOp::Ge => return Ok((format!("({} >= {})", li, ri), RustType::Bool)),
|
|
23351
|
+
BinOp::StrictEq => {
|
|
23352
|
+
return Ok((format!("({} == {})", li, ri), RustType::Bool))
|
|
23353
|
+
}
|
|
23354
|
+
BinOp::StrictNe => {
|
|
23355
|
+
return Ok((format!("({} != {})", li, ri), RustType::Bool))
|
|
23356
|
+
}
|
|
23357
|
+
_ => {}
|
|
23358
|
+
}
|
|
23359
|
+
}
|
|
23360
|
+
|
|
23164
23361
|
let (l, lt) = if lt.is_integer_scalar() {
|
|
23165
23362
|
(format!("(({}) as f64)", l), RustType::F64)
|
|
23166
23363
|
} else {
|
|
@@ -77,6 +77,10 @@ const BUILTIN_ALIASES: &[(&str, &str)] = &[
|
|
|
77
77
|
("ws", "tish:ws"),
|
|
78
78
|
("tty", "tish:tty"),
|
|
79
79
|
("pty", "tish:pty"),
|
|
80
|
+
("net", "tish:net"),
|
|
81
|
+
("encoding", "tish:encoding"),
|
|
82
|
+
("crypto", "tish:crypto"),
|
|
83
|
+
("zip", "tish:zip"),
|
|
80
84
|
];
|
|
81
85
|
|
|
82
86
|
/// Normalize built-in spec to canonical form. Handles the Node `node:` prefix
|
|
@@ -106,9 +110,14 @@ pub fn is_builtin_native_spec(spec: &str) -> bool {
|
|
|
106
110
|
| "tish:ws"
|
|
107
111
|
| "tish:tty"
|
|
108
112
|
| "tish:pty"
|
|
113
|
+
| "tish:net"
|
|
114
|
+
| "tish:encoding"
|
|
115
|
+
| "tish:crypto"
|
|
116
|
+
| "tish:zip"
|
|
109
117
|
) || matches!(
|
|
110
118
|
spec,
|
|
111
|
-
"fs" | "fs/promises" | "http" | "timers" | "process" | "ws" | "tty" | "pty"
|
|
119
|
+
"fs" | "fs/promises" | "http" | "timers" | "process" | "ws" | "tty" | "pty" | "net"
|
|
120
|
+
| "encoding" | "crypto" | "zip"
|
|
112
121
|
)
|
|
113
122
|
}
|
|
114
123
|
|
|
@@ -1029,7 +1038,8 @@ pub fn is_native_import(spec: &str) -> bool {
|
|
|
1029
1038
|
|| spec.starts_with("ffi:")
|
|
1030
1039
|
|| matches!(
|
|
1031
1040
|
spec,
|
|
1032
|
-
"fs" | "fs/promises" | "http" | "timers" | "process" | "ws" | "tty"
|
|
1041
|
+
"fs" | "fs/promises" | "http" | "timers" | "process" | "ws" | "tty" | "net" | "encoding"
|
|
1042
|
+
| "crypto" | "zip"
|
|
1033
1043
|
)
|
|
1034
1044
|
}
|
|
1035
1045
|
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
//! `PropMap` on a cache miss). Phase 1b will attach the ordered key list to each shape so objects can
|
|
13
13
|
//! drop per-object key storage entirely (the butterfly representation).
|
|
14
14
|
|
|
15
|
-
use crate::compat::
|
|
16
|
-
#[cfg(feature = "portable")]
|
|
17
|
-
use
|
|
15
|
+
use crate::compat::Arc;
|
|
16
|
+
#[cfg(not(feature = "portable"))]
|
|
17
|
+
use crate::compat::{AHashMap, OnceLock, RwLock};
|
|
18
18
|
|
|
19
19
|
/// Identity of an object's ordered key-set.
|
|
20
20
|
pub type ShapeId = u32;
|
|
@@ -31,15 +31,18 @@ pub const DICT_SHAPE: ShapeId = u32::MAX;
|
|
|
31
31
|
/// Edges are keyed by an `ahash` map, not the default SipHash one: `transition` is on the hot path of
|
|
32
32
|
/// every object construction (and `JSON.parse` — profiled as ~16% of parse, almost all SipHash), and
|
|
33
33
|
/// ahash is ~2–3× faster on short string keys. Random-seeded per process, so no HashDoS regression.
|
|
34
|
+
#[cfg(not(feature = "portable"))]
|
|
34
35
|
#[derive(Default)]
|
|
35
36
|
struct ShapeNode {
|
|
36
37
|
transitions: AHashMap<Arc<str>, ShapeId>,
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
#[cfg(not(feature = "portable"))]
|
|
39
41
|
struct Registry {
|
|
40
42
|
nodes: Vec<ShapeNode>,
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
#[cfg(not(feature = "portable"))]
|
|
43
46
|
fn registry() -> &'static RwLock<Registry> {
|
|
44
47
|
static REG: OnceLock<RwLock<Registry>> = OnceLock::new();
|
|
45
48
|
REG.get_or_init(|| {
|
|
@@ -56,6 +59,28 @@ fn registry() -> &'static RwLock<Registry> {
|
|
|
56
59
|
/// object with the same construction path reuses it. Cheap on the hot path (a read-lock + one hashmap
|
|
57
60
|
/// lookup once the edge exists). A `DICT_SHAPE` input (or shape-space exhaustion) stays `DICT_SHAPE`.
|
|
58
61
|
pub fn transition(from: ShapeId, key: &Arc<str>) -> ShapeId {
|
|
62
|
+
// On `portable` (the embedded/GBA link) nothing ever READS a shape id: the only consumers are
|
|
63
|
+
// the bytecode VM (`tish_vm`) and the std runtime's inline caches (`tish_runtime`), and neither
|
|
64
|
+
// is linked there — the GBA facade's `get_prop` goes straight to `PropMap`. Maintaining the
|
|
65
|
+
// registry anyway cost a lock plus an ahash lookup on every new-key insert AND — since this
|
|
66
|
+
// file has no eviction of any kind — a permanent `ShapeNode` plus an `Arc::clone` of the key
|
|
67
|
+
// for every distinct construction path, for the life of the program, on a 136 KB heap.
|
|
68
|
+
// Opt out via the sentinel the file already defines for exactly this case: `DICT_SHAPE` never
|
|
69
|
+
// matches an inline cache, so any future reader degrades to the slow path rather than trusting
|
|
70
|
+
// a slot index that was never recorded.
|
|
71
|
+
#[cfg(feature = "portable")]
|
|
72
|
+
{
|
|
73
|
+
let _ = (from, key);
|
|
74
|
+
DICT_SHAPE
|
|
75
|
+
}
|
|
76
|
+
#[cfg(not(feature = "portable"))]
|
|
77
|
+
{
|
|
78
|
+
transition_tracked(from, key)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[cfg(not(feature = "portable"))]
|
|
83
|
+
fn transition_tracked(from: ShapeId, key: &Arc<str>) -> ShapeId {
|
|
59
84
|
if from == DICT_SHAPE {
|
|
60
85
|
return DICT_SHAPE;
|
|
61
86
|
}
|