@tishlang/tish-lsp 3.10.9 → 3.11.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/src/cargo_native_registry.rs +4 -0
- package/crates/tish/tests/integration_test.rs +3 -0
- package/crates/tish_bytecode/src/compiler.rs +43 -19
- package/crates/tish_bytecode/tests/captured_param_slot_binding.rs +131 -0
- package/crates/tish_bytecode/tests/param_defaults_slot_binding.rs +86 -0
- package/crates/tish_core/src/value.rs +31 -4
- package/crates/tish_lsp/Cargo.toml +1 -1
- package/crates/tish_native/src/build.rs +74 -13
- package/crates/tish_pg/src/lib.rs +59 -8
- package/crates/tish_pg/src/statement_registry.rs +250 -0
- package/crates/tish_runtime/src/http.rs +19 -0
- package/crates/tish_runtime/src/http_fetch.rs +159 -65
- package/crates/tish_runtime/src/promise.rs +521 -90
- package/crates/tish_runtime/src/promise_io.rs +29 -6
- package/crates/tish_vm/src/vm.rs +5 -4
- package/crates/tish_vm/tests/captured_param_slot_mode.rs +210 -0
- package/crates/tish_vm/tests/captured_param_vm_slots_off.rs +64 -0
- package/crates/tish_vm/tests/param_defaults_slot_mode.rs +126 -0
- package/crates/tish_vm/tests/param_defaults_vm_slots_off.rs +44 -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
|
|
@@ -15,6 +15,10 @@ pub(crate) fn register_bytecode_native_modules(vm: &mut tishlang_vm::Vm) {
|
|
|
15
15
|
);
|
|
16
16
|
om.insert(Arc::from("connect"), Value::native(tishlang_pg::connect));
|
|
17
17
|
om.insert(Arc::from("prepare"), Value::native(tishlang_pg::prepare));
|
|
18
|
+
om.insert(
|
|
19
|
+
Arc::from("unprepare"),
|
|
20
|
+
Value::native(tishlang_pg::unprepare),
|
|
21
|
+
);
|
|
18
22
|
om.insert(
|
|
19
23
|
Arc::from("query_prepared"),
|
|
20
24
|
Value::native(tishlang_pg::query_prepared),
|
|
@@ -1068,6 +1068,9 @@ allSettled[1] rejected: boom
|
|
|
1068
1068
|
allSettled[2] ok: 30
|
|
1069
1069
|
race winner: A
|
|
1070
1070
|
any passthrough: 42
|
|
1071
|
+
race pending-loser winner: fast
|
|
1072
|
+
loser still awaitable: late
|
|
1073
|
+
re-await: 7 7
|
|
1071
1074
|
";
|
|
1072
1075
|
for backend_args in [vec!["run"], vec!["run", "--backend", "interp"]] {
|
|
1073
1076
|
let mut args = backend_args.clone();
|
|
@@ -844,8 +844,11 @@ impl SlotScan {
|
|
|
844
844
|
|
|
845
845
|
/// Capture-aware eligibility for general slot-based locals in a FUNCTION. Returns the captured-name set
|
|
846
846
|
/// (names that must stay name-based) when eligible, else `None` (compile name-based). Eligible iff the
|
|
847
|
-
/// flag is on, no rest param, all params simple, the body fully analysable
|
|
848
|
-
///
|
|
847
|
+
/// flag is on, no rest param, all params simple, and the body fully analysable. A CAPTURED param does
|
|
848
|
+
/// NOT disqualify the chunk (#716 follow-up): the VM still binds the arg into its positional slot, and
|
|
849
|
+
/// a copy-in prologue moves it into `local_scope` at entry — from then on the scope is that param's
|
|
850
|
+
/// single source of truth (its name stays out of the slot map, so every body read/write compiles
|
|
851
|
+
/// name-based to the very map closures capture — shared-cell mutation semantics preserved exactly).
|
|
849
852
|
fn slot_analyze(
|
|
850
853
|
params: &[FunParam],
|
|
851
854
|
has_rest: bool,
|
|
@@ -863,13 +866,6 @@ fn slot_analyze(
|
|
|
863
866
|
if !scan.stmt(body, false) {
|
|
864
867
|
return None;
|
|
865
868
|
}
|
|
866
|
-
for p in params {
|
|
867
|
-
if let FunParam::Simple(tp) = p {
|
|
868
|
-
if scan.captured.contains(&tp.name) {
|
|
869
|
-
return None;
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
869
|
Some(scan.captured)
|
|
874
870
|
}
|
|
875
871
|
|
|
@@ -1060,12 +1056,12 @@ impl<'a> Compiler<'a> {
|
|
|
1060
1056
|
self.emit_u16(Opcode::ArgMissing, i as u16);
|
|
1061
1057
|
let skip = self.emit_jump(Opcode::JumpIfFalse);
|
|
1062
1058
|
self.compile_expr(default_expr)?;
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
match
|
|
1059
|
+
// #727: resolve through BOTH slot maps (`resolve_slot`), not `slot_ctx` alone. In
|
|
1060
|
+
// GENERAL slot mode params live in `slot_scopes` (declared before this prologue) and
|
|
1061
|
+
// every read of the param lowers to `LoadLocal <slot>` — a `DeclareVarPlain` here wrote
|
|
1062
|
+
// the default into the name-based scope map no read resolves to, leaving the slot's
|
|
1063
|
+
// missing-arg `Null` visible (`f(a, b = 5); f(1)` → `1 + null` → NaN).
|
|
1064
|
+
match self.resolve_slot(tp.name.as_ref()) {
|
|
1069
1065
|
Some(slot) => self.emit_u16(Opcode::StoreLocal, slot),
|
|
1070
1066
|
None => {
|
|
1071
1067
|
let idx = self.name_idx(&tp.name);
|
|
@@ -2022,14 +2018,42 @@ impl<'a> Compiler<'a> {
|
|
|
2022
2018
|
inner_comp.emit_param_defaults_prologue(params)?;
|
|
2023
2019
|
inner_comp.compile_fn_body(body)?;
|
|
2024
2020
|
} else if let Some(cap) = captured {
|
|
2025
|
-
// Params
|
|
2026
|
-
//
|
|
2027
|
-
//
|
|
2021
|
+
// Params → slots 0..n (matching the VM's positional arg binding); uncaptured
|
|
2022
|
+
// body `let`s get fresh slots via the scope-aware allocator; captured locals
|
|
2023
|
+
// stay name-based in `local_scope` (which closures capture).
|
|
2024
|
+
//
|
|
2025
|
+
// #716 follow-up: a CAPTURED param no longer disqualifies the chunk. It keeps
|
|
2026
|
+
// its slot NUMBER (the VM binds args positionally, so numbering must stay
|
|
2027
|
+
// 0..n) but is NOT slot-resolvable: its name never enters `slot_scopes`, so
|
|
2028
|
+
// every body read/write compiles name-based (`LoadVar`/`StoreVar`) against
|
|
2029
|
+
// `local_scope` — the very map closures capture — keeping the scope the
|
|
2030
|
+
// single source of truth (shared-cell mutation semantics preserved exactly;
|
|
2031
|
+
// a body reassignment is visible to the closure and vice versa). The COPY-IN
|
|
2032
|
+
// prologue below moves each captured arg slot → scope once at entry, BEFORE
|
|
2033
|
+
// the defaults prologue so an applied default (stored name-based for a
|
|
2034
|
+
// captured param) overwrites the copied-in missing-arg null, never the other
|
|
2035
|
+
// way around. This keeps the chunk in slot mode, so the #716 fix (#728) slot-
|
|
2036
|
+
// binds an uncaptured body-level fn NAME and the per-call frame-scope↔closure
|
|
2037
|
+
// cycle never forms for the `handleEvent(payload){ function fmt(){…payload…} }`
|
|
2038
|
+
// shape.
|
|
2028
2039
|
inner_comp.general_slots = true;
|
|
2029
2040
|
inner_comp.slot_captured = cap;
|
|
2030
2041
|
inner_comp.slot_scopes.push(HashMap::new());
|
|
2042
|
+
let mut captured_param_copy_ins: Vec<(u16, Arc<str>)> = Vec::new();
|
|
2031
2043
|
for p in ¶m_names {
|
|
2032
|
-
inner_comp.
|
|
2044
|
+
if inner_comp.slot_captured.contains(p.as_ref()) {
|
|
2045
|
+
// Reserve the positional slot without making it resolvable.
|
|
2046
|
+
let slot = inner_comp.next_slot;
|
|
2047
|
+
inner_comp.next_slot += 1;
|
|
2048
|
+
captured_param_copy_ins.push((slot, Arc::clone(p)));
|
|
2049
|
+
} else {
|
|
2050
|
+
inner_comp.declare_slot(p);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
for (slot, p) in &captured_param_copy_ins {
|
|
2054
|
+
inner_comp.emit_u16(Opcode::LoadLocal, *slot);
|
|
2055
|
+
let idx = inner_comp.name_idx(p);
|
|
2056
|
+
inner_comp.emit_u16(Opcode::DeclareVarPlain, idx);
|
|
2033
2057
|
}
|
|
2034
2058
|
inner_comp.emit_param_defaults_prologue(params)?;
|
|
2035
2059
|
inner_comp.compile_fn_body(body)?;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
//! #716 follow-up: a function whose PARAM is captured by a nested closure must still
|
|
2
|
+
//! compile in general slot mode, with a captured-param COPY-IN PROLOGUE (`LoadLocal
|
|
3
|
+
//! <param slot>` + `DeclareVarPlain <name>`) so the closure reads the param from
|
|
4
|
+
//! `local_scope` while the chunk keeps frame slots for everything uncaptured —
|
|
5
|
+
//! including the body-level helper NAME (#728), which is what breaks the per-call
|
|
6
|
+
//! frame-scope↔closure cycle for the #716 repro shape.
|
|
7
|
+
|
|
8
|
+
use tishlang_bytecode::{compile, Chunk, Opcode};
|
|
9
|
+
|
|
10
|
+
/// Count occurrences of `target` in `chunk.code`, walking real instruction boundaries.
|
|
11
|
+
fn count_opcode(chunk: &Chunk, target: Opcode) -> usize {
|
|
12
|
+
let code = &chunk.code;
|
|
13
|
+
let mut ip = 0usize;
|
|
14
|
+
let mut n = 0usize;
|
|
15
|
+
while ip < code.len() {
|
|
16
|
+
let op = Opcode::from_u8(code[ip]).expect("valid opcode");
|
|
17
|
+
if op == target {
|
|
18
|
+
n += 1;
|
|
19
|
+
}
|
|
20
|
+
ip += op.instruction_size(code, ip).expect("sized instruction");
|
|
21
|
+
}
|
|
22
|
+
n
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn compile_src(src: &str) -> Chunk {
|
|
26
|
+
let program = tishlang_parser::parse(src).expect("parse");
|
|
27
|
+
compile(&program).expect("compile")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// These tests assert slot-mode emission; running the suite with `TISH_VM_SLOTS=0`
|
|
31
|
+
/// deliberately exercises the legacy name-based path, where they do not apply.
|
|
32
|
+
fn slots_disabled() -> bool {
|
|
33
|
+
std::env::var("TISH_VM_SLOTS")
|
|
34
|
+
.map(|v| v == "0")
|
|
35
|
+
.unwrap_or(false)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// The #716 repro shape: the chunk must stay slot-based, copy the captured param in
|
|
39
|
+
/// exactly once, and slot-bind the (uncaptured) helper name — zero `DeclareVar`.
|
|
40
|
+
#[test]
|
|
41
|
+
fn captured_param_chunk_stays_slot_based_with_copy_in() {
|
|
42
|
+
if slots_disabled() {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
let chunk = compile_src(
|
|
46
|
+
r#"
|
|
47
|
+
function handleEvent(payload) {
|
|
48
|
+
function fmt(x) { return x + payload.tag }
|
|
49
|
+
return fmt("v")
|
|
50
|
+
}
|
|
51
|
+
console.log(handleEvent({ tag: "t" }))
|
|
52
|
+
"#,
|
|
53
|
+
);
|
|
54
|
+
let outer = &chunk.nested[0];
|
|
55
|
+
assert!(
|
|
56
|
+
outer.slot_based,
|
|
57
|
+
"a captured-param chunk must no longer be disqualified from general slot mode"
|
|
58
|
+
);
|
|
59
|
+
assert_eq!(
|
|
60
|
+
count_opcode(outer, Opcode::DeclareVarPlain),
|
|
61
|
+
1,
|
|
62
|
+
"exactly one copy-in store for the one captured param"
|
|
63
|
+
);
|
|
64
|
+
assert_eq!(
|
|
65
|
+
count_opcode(outer, Opcode::DeclareVar),
|
|
66
|
+
0,
|
|
67
|
+
"the uncaptured helper name must be slot-bound (#728), not DeclareVar-bound — \
|
|
68
|
+
that is the cycle"
|
|
69
|
+
);
|
|
70
|
+
assert!(
|
|
71
|
+
count_opcode(outer, Opcode::StoreLocal) >= 1,
|
|
72
|
+
"the helper closure should be stored to a frame slot"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Mixed params: only the CAPTURED one is copied in; the uncaptured one stays
|
|
77
|
+
/// slot-resolved (no name-based traffic for it).
|
|
78
|
+
#[test]
|
|
79
|
+
fn only_captured_params_are_copied_in() {
|
|
80
|
+
if slots_disabled() {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
let chunk = compile_src(
|
|
84
|
+
r#"
|
|
85
|
+
function outer(p, q) {
|
|
86
|
+
function get() { return p }
|
|
87
|
+
let r = get() + q
|
|
88
|
+
return r
|
|
89
|
+
}
|
|
90
|
+
console.log(outer(1, 2))
|
|
91
|
+
"#,
|
|
92
|
+
);
|
|
93
|
+
let outer = &chunk.nested[0];
|
|
94
|
+
assert!(outer.slot_based, "mixed-param chunk should be slot-based");
|
|
95
|
+
assert_eq!(
|
|
96
|
+
count_opcode(outer, Opcode::DeclareVarPlain),
|
|
97
|
+
1,
|
|
98
|
+
"only the captured param `p` gets a copy-in store; `q` stays slotted"
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// A self-recursive helper capturing the param: the helper NAME is captured (its own
|
|
103
|
+
/// body references it) and stays on the `DeclareVar` scope path, while the chunk itself
|
|
104
|
+
/// remains slot-based with the param copy-in.
|
|
105
|
+
#[test]
|
|
106
|
+
fn self_recursive_helper_keeps_scope_path_beside_copy_in() {
|
|
107
|
+
if slots_disabled() {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
let chunk = compile_src(
|
|
111
|
+
r#"
|
|
112
|
+
function outer(n) {
|
|
113
|
+
function fact(k) { if (k < 2) { return n } return k * fact(k - 1) }
|
|
114
|
+
return fact(4)
|
|
115
|
+
}
|
|
116
|
+
console.log(outer(1))
|
|
117
|
+
"#,
|
|
118
|
+
);
|
|
119
|
+
let outer = &chunk.nested[0];
|
|
120
|
+
assert!(outer.slot_based, "chunk should stay slot-based");
|
|
121
|
+
assert_eq!(
|
|
122
|
+
count_opcode(outer, Opcode::DeclareVarPlain),
|
|
123
|
+
1,
|
|
124
|
+
"the captured param `n` gets its copy-in"
|
|
125
|
+
);
|
|
126
|
+
assert_eq!(
|
|
127
|
+
count_opcode(outer, Opcode::DeclareVar),
|
|
128
|
+
1,
|
|
129
|
+
"the self-recursive helper name is captured and stays DeclareVar-bound"
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
//! #727: WHERE the default-parameter prologue stores its value in general slot mode.
|
|
2
|
+
//!
|
|
3
|
+
//! In a general-slot-mode chunk every read of a param resolves to `LoadLocal <slot>`, so
|
|
4
|
+
//! the applied default must be stored with `StoreLocal <slot>` — a `DeclareVarPlain`
|
|
5
|
+
//! writes it into the name-based scope map where no read will ever find it (the slot
|
|
6
|
+
//! keeps its missing-arg `Null`; `a + b` → NaN).
|
|
7
|
+
|
|
8
|
+
use tishlang_bytecode::{compile, Chunk, Opcode};
|
|
9
|
+
|
|
10
|
+
/// Count occurrences of `target` in `chunk.code`, walking real instruction boundaries.
|
|
11
|
+
fn count_opcode(chunk: &Chunk, target: Opcode) -> usize {
|
|
12
|
+
let code = &chunk.code;
|
|
13
|
+
let mut ip = 0usize;
|
|
14
|
+
let mut n = 0usize;
|
|
15
|
+
while ip < code.len() {
|
|
16
|
+
let op = Opcode::from_u8(code[ip]).expect("valid opcode");
|
|
17
|
+
if op == target {
|
|
18
|
+
n += 1;
|
|
19
|
+
}
|
|
20
|
+
ip += op.instruction_size(code, ip).expect("sized instruction");
|
|
21
|
+
}
|
|
22
|
+
n
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn compile_src(src: &str) -> Chunk {
|
|
26
|
+
let program = tishlang_parser::parse(src).expect("parse");
|
|
27
|
+
compile(&program).expect("compile")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// These tests assert slot-mode emission; running the suite with `TISH_VM_SLOTS=0`
|
|
31
|
+
/// deliberately exercises the legacy name-based path, where they do not apply.
|
|
32
|
+
fn slots_disabled() -> bool {
|
|
33
|
+
std::env::var("TISH_VM_SLOTS")
|
|
34
|
+
.map(|v| v == "0")
|
|
35
|
+
.unwrap_or(false)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#[test]
|
|
39
|
+
fn general_slot_mode_default_stores_to_slot() {
|
|
40
|
+
if slots_disabled() {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
let chunk = compile_src(
|
|
44
|
+
r#"
|
|
45
|
+
function f(a, b = 5) {
|
|
46
|
+
let c = a + b
|
|
47
|
+
return c
|
|
48
|
+
}
|
|
49
|
+
console.log(f(1))
|
|
50
|
+
"#,
|
|
51
|
+
);
|
|
52
|
+
let f = &chunk.nested[0];
|
|
53
|
+
assert!(f.slot_based, "f should compile in general slot mode");
|
|
54
|
+
assert_eq!(
|
|
55
|
+
count_opcode(f, Opcode::DeclareVarPlain),
|
|
56
|
+
0,
|
|
57
|
+
"the applied default must not be declared into the name-based scope map — \
|
|
58
|
+
reads of the param resolve to its frame slot (#727)"
|
|
59
|
+
);
|
|
60
|
+
assert!(
|
|
61
|
+
count_opcode(f, Opcode::StoreLocal) >= 1,
|
|
62
|
+
"the applied default should be stored to the param's frame slot"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// The slot-DISQUALIFIED shape (captured param → name-based chunk) must keep the
|
|
67
|
+
/// `DeclareVarPlain` store: there is no slot, and closures read the param by name.
|
|
68
|
+
#[test]
|
|
69
|
+
fn name_based_chunk_default_still_declares_by_name() {
|
|
70
|
+
let chunk = compile_src(
|
|
71
|
+
r#"
|
|
72
|
+
function g(a, b = 5) {
|
|
73
|
+
function h() { return b }
|
|
74
|
+
let c = a + h()
|
|
75
|
+
return c
|
|
76
|
+
}
|
|
77
|
+
console.log(g(1))
|
|
78
|
+
"#,
|
|
79
|
+
);
|
|
80
|
+
let g = &chunk.nested[0];
|
|
81
|
+
assert_eq!(
|
|
82
|
+
count_opcode(g, Opcode::DeclareVarPlain),
|
|
83
|
+
1,
|
|
84
|
+
"captured-param chunk is name-based; its default must stay DeclareVarPlain-bound"
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -196,19 +196,46 @@ pub trait TishStruct {
|
|
|
196
196
|
|
|
197
197
|
/// Trait for Promise-like values that can be awaited (block until settled).
|
|
198
198
|
/// Implemented by the runtime for native compile; interpreter uses its own Promise.
|
|
199
|
-
|
|
199
|
+
///
|
|
200
|
+
/// (`'static` is part of the contract: promises are shared as `Arc<dyn TishPromise>`
|
|
201
|
+
/// inside `Value::Promise`, and settlement subscriptions outlive the subscribing frame.)
|
|
202
|
+
pub trait TishPromise: Send + Sync + 'static {
|
|
200
203
|
fn block_until_settled(&self) -> core::result::Result<Value, Value>;
|
|
201
204
|
/// Try to settle WITHOUT blocking. Returns `Some(result)` if the promise was already
|
|
202
205
|
/// settled before this call; returns `None` if it is still pending (a background thread
|
|
203
206
|
/// / I/O task has not completed yet). Default: always pending — implementors of async
|
|
204
|
-
/// promises (fetch
|
|
207
|
+
/// promises (fetch) leave this as `None`; settled/state-backed promises override it.
|
|
208
|
+
/// Must PEEK, not consume: a promise reporting `Some` here stays awaitable.
|
|
205
209
|
///
|
|
206
210
|
/// Used by `race`/`any`/`allSettled` to handle already-settled promises in input-order
|
|
207
|
-
/// (deterministic, JS-compatible) before
|
|
208
|
-
/// genuinely-pending ones.
|
|
211
|
+
/// (deterministic, JS-compatible) before subscribing to genuinely-pending ones.
|
|
209
212
|
fn try_settle(&self) -> Option<core::result::Result<Value, Value>> {
|
|
210
213
|
None
|
|
211
214
|
}
|
|
215
|
+
|
|
216
|
+
/// Register a callback to run exactly once when this promise settles (issue #702).
|
|
217
|
+
///
|
|
218
|
+
/// `Promise.race` / `Promise.any` / `Promise.allSettled` use this to wait on N
|
|
219
|
+
/// promises concurrently WITHOUT parking one OS thread per pending input: an
|
|
220
|
+
/// implementor that can store the callback (state-cell or channel-backed promises)
|
|
221
|
+
/// invokes it from whichever thread settles the promise, so the combinator's own
|
|
222
|
+
/// channel `recv` is the only blocking wait. A subscription that loses a race is
|
|
223
|
+
/// simply dropped after (or instead of) firing — no cancellation path is needed.
|
|
224
|
+
///
|
|
225
|
+
/// The default implementation is the legacy behavior — a detached waiter thread
|
|
226
|
+
/// blocking in [`Self::block_until_settled`] — so implementors that cannot register
|
|
227
|
+
/// callbacks keep working; they only miss the no-thread optimization until they
|
|
228
|
+
/// override this. Overrides MUST NOT block while the promise is pending.
|
|
229
|
+
#[cfg(feature = "send-values")]
|
|
230
|
+
fn subscribe(
|
|
231
|
+
self: Arc<Self>,
|
|
232
|
+
on_settled: Box<dyn FnOnce(core::result::Result<Value, Value>) + Send>,
|
|
233
|
+
) {
|
|
234
|
+
std::thread::spawn(move || {
|
|
235
|
+
let r = self.block_until_settled();
|
|
236
|
+
on_settled(r);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
212
239
|
}
|
|
213
240
|
|
|
214
241
|
/// JavaScript RegExp flags
|
|
@@ -287,7 +287,9 @@ edition = "2021"
|
|
|
287
287
|
.parent()
|
|
288
288
|
.and_then(|p| p.parent())
|
|
289
289
|
.map(|ws| ws.join("target"));
|
|
290
|
-
let target_dir = workspace_target
|
|
290
|
+
let target_dir = workspace_target
|
|
291
|
+
.filter(|p| p.exists())
|
|
292
|
+
.or_else(shared_native_target_dir);
|
|
291
293
|
let cross = build_config.cargo_target.as_deref();
|
|
292
294
|
let release_sub = if let Some(triple) = cross {
|
|
293
295
|
format!("{triple}/release")
|
|
@@ -456,7 +458,10 @@ fn runtime_dep_toml(runtime_path: &str, features_str: &str) -> String {
|
|
|
456
458
|
if let Some(v) = released_compiler_version() {
|
|
457
459
|
format!("tishlang_runtime = {{ version = {:?}{} }}", v, features_str)
|
|
458
460
|
} else {
|
|
459
|
-
format!(
|
|
461
|
+
format!(
|
|
462
|
+
"tishlang_runtime = {{ path = {:?}{} }}",
|
|
463
|
+
runtime_path, features_str
|
|
464
|
+
)
|
|
460
465
|
}
|
|
461
466
|
}
|
|
462
467
|
|
|
@@ -693,8 +698,7 @@ tishlang_runtime = {{ path = {runtime:?}{version}{features} }}
|
|
|
693
698
|
fs::write(src_dir.join("generated_native.rs"), gen)
|
|
694
699
|
.map_err(|e| format!("Cannot write generated_native.rs: {}", e))?;
|
|
695
700
|
}
|
|
696
|
-
fs::write(src_dir.join("lib.rs"), lib_rs)
|
|
697
|
-
.map_err(|e| format!("Cannot write lib.rs: {}", e))?;
|
|
701
|
+
fs::write(src_dir.join("lib.rs"), lib_rs).map_err(|e| format!("Cannot write lib.rs: {}", e))?;
|
|
698
702
|
|
|
699
703
|
println!("Emitted Rust library crate: {}", output_path.display());
|
|
700
704
|
Ok(())
|
|
@@ -739,8 +743,7 @@ fn build_gba_rom(
|
|
|
739
743
|
// the source of truth. The generated ROM crate must depend on the SAME agb: a skew would pull in
|
|
740
744
|
// two agb crates and mismatch types at the sprite-registration boundary. Falls back to a known
|
|
741
745
|
// pin only if the line can't be read (never blocks the build on a parse miss).
|
|
742
|
-
let agb_version =
|
|
743
|
-
read_facade_agb_version(&facade_path).unwrap_or_else(|| "0.25.0".to_string());
|
|
746
|
+
let agb_version = read_facade_agb_version(&facade_path).unwrap_or_else(|| "0.25.0".to_string());
|
|
744
747
|
|
|
745
748
|
let out_stem = output_path
|
|
746
749
|
.file_stem()
|
|
@@ -816,11 +819,13 @@ agb = "{agb_version}"
|
|
|
816
819
|
// Only the channel is inherited. `components` stays `rust-src` because that is what build-std
|
|
817
820
|
// needs — copying a project's `clippy`/`rustfmt` entries in would fail the ROM build whenever
|
|
818
821
|
// the pinned toolchain lacks a component the ROM never uses.
|
|
819
|
-
let channel =
|
|
820
|
-
find_project_toolchain_channel(&base).unwrap_or_else(|| "nightly".to_string());
|
|
822
|
+
let channel = find_project_toolchain_channel(&base).unwrap_or_else(|| "nightly".to_string());
|
|
821
823
|
fs::write(
|
|
822
824
|
build_dir.join("rust-toolchain.toml"),
|
|
823
|
-
format!(
|
|
825
|
+
format!(
|
|
826
|
+
"[toolchain]\nchannel = \"{}\"\ncomponents = [\"rust-src\"]\n",
|
|
827
|
+
channel
|
|
828
|
+
),
|
|
824
829
|
)
|
|
825
830
|
.map_err(|e| format!("Cannot write rust-toolchain.toml: {}", e))?;
|
|
826
831
|
|
|
@@ -971,6 +976,30 @@ fn find_project_toolchain_channel(start: &Path) -> Option<String> {
|
|
|
971
976
|
None
|
|
972
977
|
}
|
|
973
978
|
|
|
979
|
+
/// `TISH_NATIVE_TARGET_DIR` — opt-in shared cargo target dir for native (desktop) builds.
|
|
980
|
+
///
|
|
981
|
+
/// Native builds compile the generated crate under a per-process temp dir that is deleted on
|
|
982
|
+
/// success (`cleanup_build_dir`), so nothing survives between two `tish build --target native`
|
|
983
|
+
/// invocations and CI recompiles every dependency each run. Inside the tish workspace the
|
|
984
|
+
/// workspace `target/` is shared automatically; a released compiler has no workspace, and this
|
|
985
|
+
/// env var is the equivalent for it: point it at a directory that persists (a CI cache) and
|
|
986
|
+
/// cargo reuses the dependency artifacts across builds. Deliberately NOT the ambient
|
|
987
|
+
/// `CARGO_TARGET_DIR` — see the GBA path for why that is scrubbed (`TISH_GBA_TARGET_DIR` is the
|
|
988
|
+
/// same opt-in for ROM builds). The directory is created if missing.
|
|
989
|
+
fn shared_native_target_dir() -> Option<PathBuf> {
|
|
990
|
+
let dir = std::env::var_os("TISH_NATIVE_TARGET_DIR")
|
|
991
|
+
.map(PathBuf::from)
|
|
992
|
+
.filter(|p| !p.as_os_str().is_empty())?;
|
|
993
|
+
if let Err(e) = fs::create_dir_all(&dir) {
|
|
994
|
+
eprintln!(
|
|
995
|
+
"warning: TISH_NATIVE_TARGET_DIR={} is not usable ({e}); building in the temp dir",
|
|
996
|
+
dir.display()
|
|
997
|
+
);
|
|
998
|
+
return None;
|
|
999
|
+
}
|
|
1000
|
+
Some(dir)
|
|
1001
|
+
}
|
|
1002
|
+
|
|
974
1003
|
/// Remove a finished build's per-PID source directory (#384). Called only on SUCCESS, after the
|
|
975
1004
|
/// binary is copied out — the dir is then disposable (it holds only the generated crate source;
|
|
976
1005
|
/// compiled artifacts live in the shared workspace `target/`). Left in place on any earlier error so a
|
|
@@ -1126,7 +1155,9 @@ edition = "2021"
|
|
|
1126
1155
|
.parent()
|
|
1127
1156
|
.and_then(|p| p.parent())
|
|
1128
1157
|
.map(|ws| ws.join("target"));
|
|
1129
|
-
let target_dir = workspace_target
|
|
1158
|
+
let target_dir = workspace_target
|
|
1159
|
+
.filter(|p| p.exists())
|
|
1160
|
+
.or_else(shared_native_target_dir);
|
|
1130
1161
|
let binary_dir = target_dir
|
|
1131
1162
|
.as_ref()
|
|
1132
1163
|
.map(|t| t.join("release"))
|
|
@@ -1188,14 +1219,41 @@ mod tests {
|
|
|
1188
1219
|
let _ = fs::remove_dir_all(&base);
|
|
1189
1220
|
}
|
|
1190
1221
|
|
|
1222
|
+
/// `TISH_NATIVE_TARGET_DIR`: unset/empty → None (temp-dir build); set → that dir, created.
|
|
1223
|
+
#[test]
|
|
1224
|
+
fn shared_native_target_dir_follows_env() {
|
|
1225
|
+
use super::shared_native_target_dir;
|
|
1226
|
+
use std::fs;
|
|
1227
|
+
use std::path::PathBuf;
|
|
1228
|
+
// Only this test touches TISH_NATIVE_TARGET_DIR; the lock keeps it single-threaded anyway.
|
|
1229
|
+
use std::sync::Mutex;
|
|
1230
|
+
static ENV_LOCK: Mutex<()> = Mutex::new(());
|
|
1231
|
+
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
|
1232
|
+
std::env::remove_var("TISH_NATIVE_TARGET_DIR");
|
|
1233
|
+
assert!(shared_native_target_dir().is_none());
|
|
1234
|
+
std::env::set_var("TISH_NATIVE_TARGET_DIR", "");
|
|
1235
|
+
assert!(shared_native_target_dir().is_none());
|
|
1236
|
+
// Scratch under the workspace target/ (never /tmp), unique per case.
|
|
1237
|
+
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
1238
|
+
.join("../../target/tmp_shared_target_test")
|
|
1239
|
+
.join(format!("t_{}", std::process::id()));
|
|
1240
|
+
let _ = fs::remove_dir_all(&dir);
|
|
1241
|
+
std::env::set_var("TISH_NATIVE_TARGET_DIR", &dir);
|
|
1242
|
+
let got = shared_native_target_dir().expect("set → Some");
|
|
1243
|
+
assert_eq!(got, dir);
|
|
1244
|
+
assert!(dir.is_dir(), "the shared target dir is created on demand");
|
|
1245
|
+
std::env::remove_var("TISH_NATIVE_TARGET_DIR");
|
|
1246
|
+
let _ = fs::remove_dir_all(dir.parent().unwrap());
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1191
1249
|
/// #384: `cleanup_build_dir` removes a finished build's dir by default, and preserves it under
|
|
1192
1250
|
/// `TISH_KEEP_BUILD_DIR=1`.
|
|
1193
1251
|
#[test]
|
|
1194
1252
|
fn cleanup_build_dir_removes_by_default_and_keeps_on_flag() {
|
|
1195
1253
|
use std::path::PathBuf;
|
|
1196
1254
|
// Scratch under the workspace target/ (never /tmp), unique per case.
|
|
1197
|
-
let base: PathBuf =
|
|
1198
|
-
.join("../../target/tmp_cleanup_test");
|
|
1255
|
+
let base: PathBuf =
|
|
1256
|
+
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/tmp_cleanup_test");
|
|
1199
1257
|
let removed = base.join("to_remove");
|
|
1200
1258
|
let kept = base.join("to_keep");
|
|
1201
1259
|
for d in [&removed, &kept] {
|
|
@@ -1209,7 +1267,10 @@ mod tests {
|
|
|
1209
1267
|
|
|
1210
1268
|
std::env::set_var("TISH_KEEP_BUILD_DIR", "1");
|
|
1211
1269
|
super::cleanup_build_dir(&kept);
|
|
1212
|
-
assert!(
|
|
1270
|
+
assert!(
|
|
1271
|
+
kept.exists(),
|
|
1272
|
+
"TISH_KEEP_BUILD_DIR=1 should preserve the build dir"
|
|
1273
|
+
);
|
|
1213
1274
|
std::env::remove_var("TISH_KEEP_BUILD_DIR");
|
|
1214
1275
|
|
|
1215
1276
|
let _ = std::fs::remove_dir_all(&base);
|