@tishlang/tish 1.7.0 → 1.9.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/Cargo.toml +2 -0
- package/README.md +2 -0
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +28 -8
- package/crates/js_to_tish/src/transform/stmt.rs +49 -22
- package/crates/tish/Cargo.toml +14 -5
- package/crates/tish/src/cargo_native_registry.rs +29 -0
- package/crates/tish/src/cli_help.rs +16 -10
- package/crates/tish/src/main.rs +87 -32
- package/crates/tish/src/repl_completion.rs +3 -3
- package/crates/tish/tests/cargo_example_compile.rs +1 -1
- package/crates/tish/tests/integration_test.rs +19 -7
- package/crates/tish/tests/shortcircuit.rs +1 -1
- package/crates/tish_ast/src/ast.rs +80 -9
- package/crates/tish_build_utils/Cargo.toml +4 -0
- package/crates/tish_build_utils/src/lib.rs +105 -2
- package/crates/tish_builtins/Cargo.toml +5 -1
- package/crates/tish_builtins/src/array.rs +13 -12
- package/crates/tish_builtins/src/construct.rs +34 -33
- package/crates/tish_builtins/src/globals.rs +12 -11
- package/crates/tish_builtins/src/helpers.rs +2 -1
- package/crates/tish_builtins/src/object.rs +3 -2
- package/crates/tish_builtins/src/string.rs +73 -3
- package/crates/tish_bytecode/src/compiler.rs +12 -14
- package/crates/tish_bytecode/src/opcode.rs +12 -3
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/codegen.rs +745 -199
- package/crates/tish_compile/src/infer.rs +6 -0
- package/crates/tish_compile/src/lib.rs +4 -3
- package/crates/tish_compile/src/resolve.rs +180 -82
- package/crates/tish_compile/src/types.rs +175 -11
- package/crates/tish_compile_js/Cargo.toml +1 -0
- package/crates/tish_compile_js/src/codegen.rs +152 -29
- package/crates/tish_compile_js/src/lib.rs +3 -1
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +31 -12
- package/crates/tish_core/Cargo.toml +8 -0
- package/crates/tish_core/src/json.rs +102 -53
- package/crates/tish_core/src/lib.rs +3 -1
- package/crates/tish_core/src/macros.rs +5 -5
- package/crates/tish_core/src/value.rs +53 -15
- package/crates/tish_core/src/vmref.rs +178 -0
- package/crates/tish_eval/Cargo.toml +17 -2
- package/crates/tish_eval/src/eval.rs +90 -28
- package/crates/tish_eval/src/http.rs +61 -0
- package/crates/tish_eval/src/lib.rs +3 -3
- package/crates/tish_eval/src/natives.rs +41 -0
- package/crates/tish_eval/src/value.rs +7 -3
- package/crates/tish_eval/src/value_convert.rs +13 -5
- package/crates/tish_fmt/src/lib.rs +120 -30
- package/crates/tish_lexer/src/lib.rs +20 -5
- package/crates/tish_lexer/src/token.rs +4 -0
- package/crates/tish_llvm/src/lib.rs +3 -1
- package/crates/tish_lsp/Cargo.toml +4 -1
- package/crates/tish_lsp/README.md +1 -1
- package/crates/tish_lsp/src/builtin_goto.rs +261 -0
- package/crates/tish_lsp/src/import_goto.rs +549 -0
- package/crates/tish_lsp/src/main.rs +502 -102
- package/crates/tish_native/src/build.rs +3 -2
- package/crates/tish_native/src/lib.rs +6 -2
- package/crates/tish_opt/src/lib.rs +17 -2
- package/crates/tish_parser/src/lib.rs +10 -3
- package/crates/tish_parser/src/parser.rs +346 -56
- package/crates/tish_pg/Cargo.toml +34 -0
- package/crates/tish_pg/README.md +38 -0
- package/crates/tish_pg/src/error.rs +52 -0
- package/crates/tish_pg/src/lib.rs +967 -0
- package/crates/tish_resolve/Cargo.toml +13 -0
- package/crates/tish_resolve/src/lib.rs +3436 -0
- package/crates/tish_resolve/src/pos.rs +133 -0
- package/crates/tish_runtime/Cargo.toml +68 -3
- package/crates/tish_runtime/src/http.rs +1123 -141
- package/crates/tish_runtime/src/http_fetch.rs +15 -14
- package/crates/tish_runtime/src/http_hyper.rs +418 -0
- package/crates/tish_runtime/src/http_prefork.rs +189 -0
- package/crates/tish_runtime/src/lib.rs +159 -29
- package/crates/tish_runtime/src/promise.rs +199 -36
- package/crates/tish_runtime/src/promise_io.rs +2 -1
- package/crates/tish_runtime/src/timers.rs +37 -1
- package/crates/tish_runtime/src/ws.rs +26 -28
- package/crates/tish_ui/src/jsx.rs +279 -8
- package/crates/tish_ui/src/lib.rs +5 -2
- package/crates/tish_ui/src/runtime/hooks.rs +406 -45
- package/crates/tish_ui/src/runtime/mod.rs +36 -9
- package/crates/tish_vm/Cargo.toml +15 -5
- package/crates/tish_vm/src/vm.rs +506 -259
- package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +3 -1
- package/crates/tish_wasm/src/lib.rs +17 -14
- package/crates/tish_wasm_runtime/Cargo.toml +2 -1
- package/crates/tish_wasm_runtime/src/lib.rs +1 -1
- package/crates/tishlang_cargo_bindgen/Cargo.toml +1 -0
- package/crates/tishlang_cargo_bindgen/src/discover.rs +68 -0
- package/crates/tishlang_cargo_bindgen/src/lib.rs +5 -4
- package/justfile +8 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
|
@@ -1,82 +1,423 @@
|
|
|
1
|
-
//! Minimal hook state: `useState`
|
|
1
|
+
//! Minimal hook state: `useState`, `useMemo`, and render flush (Lattish-style cursor reset).
|
|
2
|
+
//! Supports multiple independent roots (`RootId`) in one thread.
|
|
2
3
|
|
|
3
4
|
use std::cell::{Cell, RefCell};
|
|
5
|
+
use std::collections::HashMap;
|
|
4
6
|
use std::rc::Rc;
|
|
5
7
|
|
|
6
|
-
use tishlang_core::{ObjectMap, Value};
|
|
8
|
+
use tishlang_core::{ObjectMap, Value, VmRef};
|
|
7
9
|
|
|
8
|
-
use super::
|
|
10
|
+
use super::Host;
|
|
11
|
+
|
|
12
|
+
/// Opaque id for one `createRoot().render(App)` tree in this thread.
|
|
13
|
+
pub type RootId = u64;
|
|
14
|
+
|
|
15
|
+
/// First root: `install_thread_local_host` and `native_create_root` without an id argument.
|
|
16
|
+
pub const LEGACY_ROOT_ID: RootId = 1;
|
|
9
17
|
|
|
10
18
|
thread_local! {
|
|
11
|
-
|
|
19
|
+
static HOOKS: RefCell<HashMap<RootId, HookState>> = RefCell::new(HashMap::new());
|
|
20
|
+
static CURRENT_ROOT: Cell<Option<RootId>> = Cell::new(None);
|
|
21
|
+
static HOSTS: RefCell<HashMap<RootId, Box<dyn Host>>> = RefCell::new(HashMap::new());
|
|
22
|
+
static NEXT_DYNAMIC_ROOT_ID: Cell<RootId> = Cell::new(2);
|
|
12
23
|
static IN_FLUSH: Cell<bool> = Cell::new(false);
|
|
13
24
|
}
|
|
14
25
|
|
|
26
|
+
/// Allocate an id for an additional in-process window (starts at 2; 1 is legacy primary).
|
|
27
|
+
pub fn alloc_root_id() -> RootId {
|
|
28
|
+
NEXT_DYNAMIC_ROOT_ID.with(|n| {
|
|
29
|
+
let id = n.get();
|
|
30
|
+
n.set(id.saturating_add(1).max(2));
|
|
31
|
+
id
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fn ensure_hook_entry(root_id: RootId) {
|
|
36
|
+
HOOKS.with(|h| {
|
|
37
|
+
h.borrow_mut().entry(root_id).or_default();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Install the host for a specific root. Replaces any previous host for that id.
|
|
42
|
+
pub fn install_host_for_root(root_id: RootId, host: Box<dyn Host>) {
|
|
43
|
+
ensure_hook_entry(root_id);
|
|
44
|
+
HOSTS.with(|h| {
|
|
45
|
+
h.borrow_mut().insert(root_id, host);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Legacy: install host for [`LEGACY_ROOT_ID`] (same as `macos.run` / single-window tools).
|
|
50
|
+
#[allow(dead_code)] // Emitted Rust / hosts call via `tishlang_ui` re-exports; unused inside this crate.
|
|
51
|
+
pub fn install_thread_local_host(host: Box<dyn Host>) {
|
|
52
|
+
install_host_for_root(LEGACY_ROOT_ID, host);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Remove hook state and run effect cleanups. Safe to call while the host still exists (e.g. before
|
|
56
|
+
/// dropping AppKit objects that might receive async callbacks).
|
|
57
|
+
pub fn unregister_root_hooks_and_effects(root_id: RootId) {
|
|
58
|
+
HOOKS.with(|h| {
|
|
59
|
+
if let Some(st) = h.borrow_mut().remove(&root_id) {
|
|
60
|
+
run_all_effect_cleanups(st.effect_cells.as_ref());
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Drop the [`Host`] for `root_id`. Defer after `windowWillClose:` returns when the host retains the
|
|
66
|
+
/// window delegate object that is still executing that callback.
|
|
67
|
+
pub fn drop_host_for_root(root_id: RootId) {
|
|
68
|
+
HOSTS.with(|h| {
|
|
69
|
+
h.borrow_mut().remove(&root_id);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
pub fn unregister_root(root_id: RootId) {
|
|
74
|
+
unregister_root_hooks_and_effects(root_id);
|
|
75
|
+
drop_host_for_root(root_id);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
pub fn with_host_for_root<R>(root_id: RootId, f: impl FnOnce(&mut dyn Host) -> R) -> Option<R> {
|
|
79
|
+
HOSTS.with(|c| {
|
|
80
|
+
let mut m = c.borrow_mut();
|
|
81
|
+
m.get_mut(&root_id).map(|host| f(host.as_mut()))
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Prefer [`with_host_for_root`]; kept for call sites that assume a single primary root.
|
|
86
|
+
#[allow(dead_code)]
|
|
87
|
+
pub fn with_thread_local_host<R>(f: impl FnOnce(&mut dyn Host) -> R) -> Option<R> {
|
|
88
|
+
with_host_for_root(LEGACY_ROOT_ID, f)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Root currently rendering or running hook flush (`None` outside that scope).
|
|
92
|
+
pub fn current_root_id() -> Option<RootId> {
|
|
93
|
+
CURRENT_ROOT.get()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Sets [`CURRENT_ROOT`] for the duration of `f` so `window.*` and similar APIs target this tree.
|
|
97
|
+
pub fn run_with_current_root<R>(root_id: RootId, f: impl FnOnce() -> R) -> R {
|
|
98
|
+
let prev = CURRENT_ROOT.get();
|
|
99
|
+
CURRENT_ROOT.set(Some(root_id));
|
|
100
|
+
let out = f();
|
|
101
|
+
CURRENT_ROOT.set(prev);
|
|
102
|
+
out
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// One `useEffect` slot: committed dependency snapshot and optional cleanup from the last run.
|
|
15
106
|
#[derive(Default)]
|
|
107
|
+
struct EffectCell {
|
|
108
|
+
committed_deps: Option<Vec<Value>>,
|
|
109
|
+
cleanup: Option<Value>,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
struct PendingEffect {
|
|
113
|
+
slot: usize,
|
|
114
|
+
effect_fn: Value,
|
|
115
|
+
new_deps: Vec<Value>,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Hook storage for one `createRoot().render(App)` tree.
|
|
16
119
|
pub struct HookState {
|
|
17
120
|
pub state_slots: Rc<RefCell<Vec<Value>>>,
|
|
18
121
|
pub cursor: usize,
|
|
19
122
|
pub root_app: Option<Value>,
|
|
20
123
|
pub root_vnode: Option<Value>,
|
|
21
124
|
pub flush_scheduled: bool,
|
|
125
|
+
/// Per-slot: last dependency tuple snapshot and cached value from `useMemo`.
|
|
126
|
+
pub memo_cache: Rc<RefCell<Vec<Option<(Vec<Value>, Value)>>>>,
|
|
127
|
+
pub memo_cursor: usize,
|
|
128
|
+
effect_cells: Rc<RefCell<Vec<EffectCell>>>,
|
|
129
|
+
effect_cursor: usize,
|
|
130
|
+
pending_effects: Rc<RefCell<Vec<PendingEffect>>>,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
impl Default for HookState {
|
|
134
|
+
fn default() -> Self {
|
|
135
|
+
Self {
|
|
136
|
+
state_slots: Rc::new(RefCell::new(Vec::new())),
|
|
137
|
+
cursor: 0,
|
|
138
|
+
root_app: None,
|
|
139
|
+
root_vnode: None,
|
|
140
|
+
flush_scheduled: false,
|
|
141
|
+
memo_cache: Rc::new(RefCell::new(Vec::new())),
|
|
142
|
+
memo_cursor: 0,
|
|
143
|
+
effect_cells: Rc::new(RefCell::new(Vec::new())),
|
|
144
|
+
effect_cursor: 0,
|
|
145
|
+
pending_effects: Rc::new(RefCell::new(Vec::new())),
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fn run_all_effect_cleanups(cells: &RefCell<Vec<EffectCell>>) {
|
|
151
|
+
for cell in cells.borrow_mut().iter_mut() {
|
|
152
|
+
if let Some(c) = cell.cleanup.take() {
|
|
153
|
+
if let Value::Function(f) = c {
|
|
154
|
+
let _ = f(&[]);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
22
158
|
}
|
|
23
159
|
|
|
24
160
|
impl HookState {
|
|
25
161
|
pub fn reset_for_new_root(&mut self) {
|
|
162
|
+
run_all_effect_cleanups(self.effect_cells.as_ref());
|
|
163
|
+
self.effect_cells.borrow_mut().clear();
|
|
164
|
+
self.effect_cursor = 0;
|
|
165
|
+
self.pending_effects.borrow_mut().clear();
|
|
26
166
|
self.state_slots.borrow_mut().clear();
|
|
27
167
|
self.cursor = 0;
|
|
28
168
|
self.root_vnode = None;
|
|
29
169
|
self.flush_scheduled = false;
|
|
170
|
+
self.memo_cache.borrow_mut().clear();
|
|
171
|
+
self.memo_cursor = 0;
|
|
30
172
|
}
|
|
31
173
|
}
|
|
32
174
|
|
|
175
|
+
fn memo_dep_eq(a: &Value, b: &Value) -> bool {
|
|
176
|
+
match (a, b) {
|
|
177
|
+
(Value::Number(x), Value::Number(y)) => {
|
|
178
|
+
if x.is_nan() && y.is_nan() {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
x == y
|
|
182
|
+
}
|
|
183
|
+
(Value::String(x), Value::String(y)) => x == y,
|
|
184
|
+
(Value::Bool(x), Value::Bool(y)) => x == y,
|
|
185
|
+
(Value::Null, Value::Null) => true,
|
|
186
|
+
(Value::Array(ax), Value::Array(bx)) => {
|
|
187
|
+
let ab = ax.borrow();
|
|
188
|
+
let bb = bx.borrow();
|
|
189
|
+
if ab.len() != bb.len() {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
ab.iter()
|
|
193
|
+
.zip(bb.iter())
|
|
194
|
+
.all(|(x, y)| memo_dep_eq(x, y))
|
|
195
|
+
}
|
|
196
|
+
_ => false,
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
fn memo_deps_unchanged(prev: &[Value], next: &[Value]) -> bool {
|
|
201
|
+
prev.len() == next.len()
|
|
202
|
+
&& prev
|
|
203
|
+
.iter()
|
|
204
|
+
.zip(next.iter())
|
|
205
|
+
.all(|(a, b)| memo_dep_eq(a, b))
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
fn root_id_for_hooks() -> RootId {
|
|
209
|
+
CURRENT_ROOT.get().unwrap_or(LEGACY_ROOT_ID)
|
|
210
|
+
}
|
|
211
|
+
|
|
33
212
|
/// `useState(initial)` → `[state, setState]` as a Tish array.
|
|
34
213
|
pub fn native_use_state(args: &[Value]) -> Value {
|
|
35
214
|
let initial = args.first().cloned().unwrap_or(Value::Null);
|
|
36
|
-
|
|
37
|
-
|
|
215
|
+
let root_id = root_id_for_hooks();
|
|
216
|
+
HOOKS.with(|h| {
|
|
217
|
+
let mut map = h.borrow_mut();
|
|
218
|
+
let st = map.entry(root_id).or_default();
|
|
38
219
|
let i = st.cursor;
|
|
39
220
|
st.cursor += 1;
|
|
40
|
-
let slots =
|
|
221
|
+
let slots = &st.state_slots.clone();
|
|
41
222
|
while i >= slots.borrow().len() {
|
|
42
223
|
slots.borrow_mut().push(initial.clone());
|
|
43
224
|
}
|
|
44
225
|
let current = slots.borrow()[i].clone();
|
|
45
226
|
let idx = i;
|
|
46
|
-
let setter = Value::
|
|
47
|
-
let
|
|
48
|
-
|
|
49
|
-
|
|
227
|
+
let setter = Value::native(move |a: &[Value]| {
|
|
228
|
+
let arg = a.first().cloned().unwrap_or(Value::Null);
|
|
229
|
+
HOOKS.with(|hooks| {
|
|
230
|
+
if let Some(st) = hooks.borrow_mut().get_mut(&root_id) {
|
|
231
|
+
let slot_len = st.state_slots.borrow().len();
|
|
232
|
+
if idx >= slot_len {
|
|
233
|
+
st.flush_scheduled = true;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
let prev = st.state_slots.borrow()[idx].clone();
|
|
237
|
+
let new_v = match &arg {
|
|
238
|
+
Value::Function(f) => f(&[prev.clone()]),
|
|
239
|
+
_ => arg.clone(),
|
|
240
|
+
};
|
|
241
|
+
if memo_dep_eq(&prev, &new_v) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
st.state_slots.borrow_mut()[idx] = new_v;
|
|
245
|
+
st.flush_scheduled = true;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
if !IN_FLUSH.get() {
|
|
249
|
+
drain_flush_queue();
|
|
250
|
+
}
|
|
50
251
|
Value::Null
|
|
51
|
-
})
|
|
52
|
-
Value::Array(
|
|
252
|
+
});
|
|
253
|
+
Value::Array(VmRef::new(vec![current, setter]))
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/// `useMemo(factory, deps?)` — caches `factory()` until `deps` changes (shallow compare per slot).
|
|
258
|
+
pub fn native_use_memo(args: &[Value]) -> Value {
|
|
259
|
+
let Some(Value::Function(factory)) = args.first() else {
|
|
260
|
+
return Value::Null;
|
|
261
|
+
};
|
|
262
|
+
let factory = factory.clone();
|
|
263
|
+
let deps: Vec<Value> = match args.get(1) {
|
|
264
|
+
Some(Value::Array(a)) => a.borrow().clone(),
|
|
265
|
+
Some(other) => vec![other.clone()],
|
|
266
|
+
None => vec![],
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
let root_id = root_id_for_hooks();
|
|
270
|
+
HOOKS.with(|h| {
|
|
271
|
+
let mut map = h.borrow_mut();
|
|
272
|
+
let st = map.entry(root_id).or_default();
|
|
273
|
+
let i = st.memo_cursor;
|
|
274
|
+
st.memo_cursor += 1;
|
|
275
|
+
let cache = &st.memo_cache.clone();
|
|
276
|
+
let mut c = cache.borrow_mut();
|
|
277
|
+
while c.len() <= i {
|
|
278
|
+
c.push(None);
|
|
279
|
+
}
|
|
280
|
+
let reuse = match &c[i] {
|
|
281
|
+
Some((old_deps, _)) => memo_deps_unchanged(old_deps, &deps),
|
|
282
|
+
None => false,
|
|
283
|
+
};
|
|
284
|
+
if reuse {
|
|
285
|
+
return c[i].as_ref().unwrap().1.clone();
|
|
286
|
+
}
|
|
287
|
+
let produced = factory(&[]);
|
|
288
|
+
c[i] = Some((deps, produced.clone()));
|
|
289
|
+
produced
|
|
53
290
|
})
|
|
54
291
|
}
|
|
55
292
|
|
|
56
|
-
/// `
|
|
293
|
+
/// `useEffect(effect, deps?)` — runs `effect` after the host commits the tree; compares `deps` like `useMemo`.
|
|
294
|
+
/// If `effect` returns a function, it is called before the next run or on root teardown (`render` replacement / [`unregister_root`]).
|
|
295
|
+
pub fn native_use_effect(args: &[Value]) -> Value {
|
|
296
|
+
let Some(Value::Function(effect_fn)) = args.first() else {
|
|
297
|
+
return Value::Null;
|
|
298
|
+
};
|
|
299
|
+
let effect_fn = effect_fn.clone();
|
|
300
|
+
let deps: Vec<Value> = match args.get(1) {
|
|
301
|
+
Some(Value::Array(a)) => a.borrow().clone(),
|
|
302
|
+
Some(other) => vec![other.clone()],
|
|
303
|
+
None => vec![],
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
let root_id = root_id_for_hooks();
|
|
307
|
+
HOOKS.with(|h| {
|
|
308
|
+
let mut map = h.borrow_mut();
|
|
309
|
+
let st = map.entry(root_id).or_default();
|
|
310
|
+
let i = st.effect_cursor;
|
|
311
|
+
st.effect_cursor += 1;
|
|
312
|
+
let cells = &st.effect_cells.clone();
|
|
313
|
+
let mut cells_b = cells.borrow_mut();
|
|
314
|
+
while cells_b.len() <= i {
|
|
315
|
+
cells_b.push(EffectCell::default());
|
|
316
|
+
}
|
|
317
|
+
let should_run = match &cells_b[i].committed_deps {
|
|
318
|
+
None => true,
|
|
319
|
+
Some(old) => !memo_deps_unchanged(old, &deps),
|
|
320
|
+
};
|
|
321
|
+
drop(cells_b);
|
|
322
|
+
|
|
323
|
+
if should_run {
|
|
324
|
+
st.pending_effects.borrow_mut().push(PendingEffect {
|
|
325
|
+
slot: i,
|
|
326
|
+
effect_fn: Value::Function(effect_fn),
|
|
327
|
+
new_deps: deps,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
Value::Null
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
fn flush_pending_effects(root_id: RootId) {
|
|
335
|
+
let pending: Vec<PendingEffect> = HOOKS.with(|h| {
|
|
336
|
+
h.borrow_mut()
|
|
337
|
+
.get_mut(&root_id)
|
|
338
|
+
.map(|st| std::mem::take(&mut *st.pending_effects.borrow_mut()))
|
|
339
|
+
.unwrap_or_default()
|
|
340
|
+
});
|
|
341
|
+
let cells_rc = HOOKS.with(|h| {
|
|
342
|
+
h.borrow()
|
|
343
|
+
.get(&root_id)
|
|
344
|
+
.map(|st| st.effect_cells.clone())
|
|
345
|
+
});
|
|
346
|
+
let Some(cells_rc) = cells_rc else {
|
|
347
|
+
return;
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
for p in pending {
|
|
351
|
+
// Never hold `effect_cells` across user cleanup/effect: they may call `setState` →
|
|
352
|
+
// `drain_flush_queue` → `native_use_effect`, which must `borrow_mut` the same RefCell.
|
|
353
|
+
let cleanup_fn = {
|
|
354
|
+
let mut cells = cells_rc.borrow_mut();
|
|
355
|
+
while cells.len() <= p.slot {
|
|
356
|
+
cells.push(EffectCell::default());
|
|
357
|
+
}
|
|
358
|
+
cells[p.slot].cleanup.take()
|
|
359
|
+
};
|
|
360
|
+
if let Some(Value::Function(f)) = cleanup_fn {
|
|
361
|
+
let _ = f(&[]);
|
|
362
|
+
}
|
|
363
|
+
let run_result = if let Value::Function(f) = &p.effect_fn {
|
|
364
|
+
f(&[])
|
|
365
|
+
} else {
|
|
366
|
+
Value::Null
|
|
367
|
+
};
|
|
368
|
+
{
|
|
369
|
+
let mut cells = cells_rc.borrow_mut();
|
|
370
|
+
while cells.len() <= p.slot {
|
|
371
|
+
cells.push(EffectCell::default());
|
|
372
|
+
}
|
|
373
|
+
let cell = &mut cells[p.slot];
|
|
374
|
+
cell.cleanup = match run_result {
|
|
375
|
+
Value::Function(f) => Some(Value::Function(f)),
|
|
376
|
+
_ => None,
|
|
377
|
+
};
|
|
378
|
+
cell.committed_deps = Some(p.new_deps);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
fn parse_root_id_arg(args: &[Value]) -> RootId {
|
|
384
|
+
match args.first() {
|
|
385
|
+
Some(Value::Number(n)) if n.is_finite() && *n >= 1.0 && n.fract() == 0.0 => *n as u64,
|
|
386
|
+
_ => LEGACY_ROOT_ID,
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/// `createRoot(container?)` or `createRoot(rootId)` → `{ render: (App) => { ... } }`.
|
|
391
|
+
/// Pass a positive integer as the first argument to bind this root to a host installed via
|
|
392
|
+
/// [`install_host_for_root`].
|
|
57
393
|
pub fn native_create_root(args: &[Value]) -> Value {
|
|
58
|
-
let
|
|
59
|
-
|
|
394
|
+
let root_id = parse_root_id_arg(args);
|
|
395
|
+
ensure_hook_entry(root_id);
|
|
396
|
+
let render_fn = Value::native(move |app_args: &[Value]| {
|
|
60
397
|
let app = app_args.first().cloned().unwrap_or(Value::Null);
|
|
61
|
-
|
|
62
|
-
let mut
|
|
398
|
+
HOOKS.with(|h| {
|
|
399
|
+
let mut map = h.borrow_mut();
|
|
400
|
+
let st = map.entry(root_id).or_default();
|
|
63
401
|
st.reset_for_new_root();
|
|
64
402
|
st.root_app = Some(app);
|
|
65
403
|
st.flush_scheduled = true;
|
|
66
404
|
});
|
|
67
405
|
drain_flush_queue();
|
|
68
406
|
Value::Null
|
|
69
|
-
})
|
|
70
|
-
Value::Object(
|
|
407
|
+
});
|
|
408
|
+
Value::Object(VmRef::new(ObjectMap::from([(
|
|
71
409
|
std::sync::Arc::from("render"),
|
|
72
410
|
render_fn,
|
|
73
|
-
)])))
|
|
411
|
+
)])))
|
|
74
412
|
}
|
|
75
413
|
|
|
76
414
|
/// Request a re-render (coalesced; safe if called during flush).
|
|
77
415
|
pub fn schedule_flush() {
|
|
78
|
-
|
|
79
|
-
|
|
416
|
+
let root_id = root_id_for_hooks();
|
|
417
|
+
HOOKS.with(|h| {
|
|
418
|
+
if let Some(st) = h.borrow_mut().get_mut(&root_id) {
|
|
419
|
+
st.flush_scheduled = true;
|
|
420
|
+
}
|
|
80
421
|
});
|
|
81
422
|
if IN_FLUSH.get() {
|
|
82
423
|
return;
|
|
@@ -86,37 +427,57 @@ pub fn schedule_flush() {
|
|
|
86
427
|
|
|
87
428
|
fn drain_flush_queue() {
|
|
88
429
|
loop {
|
|
89
|
-
let
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
st.flush_scheduled
|
|
93
|
-
|
|
94
|
-
} else {
|
|
95
|
-
false
|
|
96
|
-
}
|
|
430
|
+
let root_id = HOOKS.with(|h| {
|
|
431
|
+
h.borrow()
|
|
432
|
+
.iter()
|
|
433
|
+
.find(|(_, st)| st.flush_scheduled)
|
|
434
|
+
.map(|(id, _)| *id)
|
|
97
435
|
});
|
|
98
|
-
|
|
436
|
+
let Some(root_id) = root_id else {
|
|
99
437
|
break;
|
|
100
|
-
}
|
|
438
|
+
};
|
|
439
|
+
|
|
101
440
|
IN_FLUSH.set(true);
|
|
102
|
-
|
|
103
|
-
|
|
441
|
+
CURRENT_ROOT.set(Some(root_id));
|
|
442
|
+
HOOKS.with(|h| {
|
|
443
|
+
if let Some(st) = h.borrow_mut().get_mut(&root_id) {
|
|
444
|
+
st.flush_scheduled = false;
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
let app_fn = HOOKS.with(|h| {
|
|
449
|
+
let mut map = h.borrow_mut();
|
|
450
|
+
let st = map.get_mut(&root_id)?;
|
|
104
451
|
st.cursor = 0;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
452
|
+
st.memo_cursor = 0;
|
|
453
|
+
st.effect_cursor = 0;
|
|
454
|
+
st.pending_effects.borrow_mut().clear();
|
|
455
|
+
let app = st.root_app.clone()?;
|
|
108
456
|
let Value::Function(f) = app else {
|
|
109
|
-
return;
|
|
457
|
+
return None;
|
|
110
458
|
};
|
|
459
|
+
Some(f)
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
if let Some(f) = app_fn {
|
|
111
463
|
let tree = f(&[]);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
let
|
|
115
|
-
|
|
116
|
-
|
|
464
|
+
HOOKS.with(|h| {
|
|
465
|
+
let mut map = h.borrow_mut();
|
|
466
|
+
if let Some(st) = map.get_mut(&root_id) {
|
|
467
|
+
st.root_vnode = Some(tree.clone());
|
|
468
|
+
HOSTS.with(|hosts| {
|
|
469
|
+
let mut hm = hosts.borrow_mut();
|
|
470
|
+
if let Some(host) = hm.get_mut(&root_id) {
|
|
471
|
+
host.commit_root(&tree);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
117
474
|
}
|
|
118
475
|
});
|
|
119
|
-
|
|
476
|
+
IN_FLUSH.set(false);
|
|
477
|
+
flush_pending_effects(root_id);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
CURRENT_ROOT.set(None);
|
|
120
481
|
IN_FLUSH.set(false);
|
|
121
482
|
}
|
|
122
483
|
}
|
|
@@ -6,9 +6,14 @@ use std::cell::RefCell;
|
|
|
6
6
|
use std::rc::Rc;
|
|
7
7
|
use std::sync::Arc;
|
|
8
8
|
|
|
9
|
-
pub use hooks::{
|
|
9
|
+
pub use hooks::{
|
|
10
|
+
alloc_root_id, current_root_id, drop_host_for_root, install_host_for_root, native_create_root,
|
|
11
|
+
native_use_effect, native_use_memo, native_use_state, run_with_current_root, schedule_flush,
|
|
12
|
+
unregister_root, unregister_root_hooks_and_effects, with_host_for_root, HookState,
|
|
13
|
+
LEGACY_ROOT_ID, RootId,
|
|
14
|
+
};
|
|
10
15
|
|
|
11
|
-
use tishlang_core::{ObjectMap, Value};
|
|
16
|
+
use tishlang_core::{ObjectMap, Value, VmRef};
|
|
12
17
|
|
|
13
18
|
/// Sentinel string for `Fragment` (native). JS/Lattish uses `Symbol`; hosts compare via equality.
|
|
14
19
|
pub const FRAGMENT_SENTINEL: &str = "__tish_ui_Fragment__";
|
|
@@ -51,10 +56,10 @@ pub fn ui_h(args: &[Value]) -> Value {
|
|
|
51
56
|
if !children_vec.is_empty() {
|
|
52
57
|
merged.insert(
|
|
53
58
|
Arc::from("children"),
|
|
54
|
-
Value::Array(
|
|
59
|
+
Value::Array(VmRef::new(children_vec.clone())),
|
|
55
60
|
);
|
|
56
61
|
}
|
|
57
|
-
return f(&[Value::Object(
|
|
62
|
+
return f(&[Value::Object(VmRef::new(merged))]);
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
if is_fragment_tag(&tag) {
|
|
@@ -72,11 +77,25 @@ pub fn ui_h(args: &[Value]) -> Value {
|
|
|
72
77
|
fn normalize_children_list(children_arg: Value) -> Vec<Value> {
|
|
73
78
|
match children_arg {
|
|
74
79
|
Value::Null => vec![],
|
|
75
|
-
Value::Array(a) => a.borrow()
|
|
80
|
+
Value::Array(a) => flatten_vnode_children(&a.borrow()),
|
|
76
81
|
other => vec![other],
|
|
77
82
|
}
|
|
78
83
|
}
|
|
79
84
|
|
|
85
|
+
/// JSX often passes `{items.map(...)}` as one slot in the children array. Treat nested `Value::Array`
|
|
86
|
+
/// like React: splice them into the parent list so hosts never see a raw array vnode.
|
|
87
|
+
fn flatten_vnode_children(items: &[Value]) -> Vec<Value> {
|
|
88
|
+
let mut out = Vec::new();
|
|
89
|
+
for c in items {
|
|
90
|
+
match c {
|
|
91
|
+
Value::Array(inner) => out.extend(flatten_vnode_children(&inner.borrow())),
|
|
92
|
+
Value::Null => {}
|
|
93
|
+
_ => out.push(c.clone()),
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
out
|
|
97
|
+
}
|
|
98
|
+
|
|
80
99
|
fn vnode_element(tag: Arc<str>, props: Value, children: Vec<Value>) -> Value {
|
|
81
100
|
let mut m = ObjectMap::default();
|
|
82
101
|
m.insert(Arc::from("tag"), Value::String(tag));
|
|
@@ -90,10 +109,10 @@ fn vnode_element(tag: Arc<str>, props: Value, children: Vec<Value>) -> Value {
|
|
|
90
109
|
);
|
|
91
110
|
m.insert(
|
|
92
111
|
Arc::from("children"),
|
|
93
|
-
Value::Array(
|
|
112
|
+
Value::Array(VmRef::new(children)),
|
|
94
113
|
);
|
|
95
114
|
m.insert(Arc::from("_el"), Value::Null);
|
|
96
|
-
Value::Object(
|
|
115
|
+
Value::Object(VmRef::new(m))
|
|
97
116
|
}
|
|
98
117
|
|
|
99
118
|
fn vnode_fragment(children: Vec<Value>) -> Value {
|
|
@@ -102,16 +121,24 @@ fn vnode_fragment(children: Vec<Value>) -> Value {
|
|
|
102
121
|
m.insert(Arc::from("props"), Value::Null);
|
|
103
122
|
m.insert(
|
|
104
123
|
Arc::from("children"),
|
|
105
|
-
Value::Array(
|
|
124
|
+
Value::Array(VmRef::new(children)),
|
|
106
125
|
);
|
|
107
126
|
m.insert(Arc::from("_el"), Value::Null);
|
|
108
|
-
Value::Object(
|
|
127
|
+
Value::Object(VmRef::new(m))
|
|
109
128
|
}
|
|
110
129
|
|
|
111
130
|
/// Pluggable UI backend (Floem, DOM, SwiftUI, …). Main-thread / single-threaded by default.
|
|
112
131
|
pub trait Host {
|
|
113
132
|
/// Apply a new root vnode (after each render flush).
|
|
114
133
|
fn commit_root(&mut self, vnode: &Value);
|
|
134
|
+
/// Content area width changed (e.g. window resize); default no-op.
|
|
135
|
+
fn content_width_changed(&mut self, _width: f64) {}
|
|
136
|
+
/// Called once from the main queue shortly after the window is ordered on-screen. Split /
|
|
137
|
+
/// sidebar hosts can use this to re-layout when pane bounds were still provisional during the
|
|
138
|
+
/// first commit.
|
|
139
|
+
fn after_window_shown(&mut self) {}
|
|
140
|
+
/// Clear native control target/action (and similar) before the host is dropped — e.g. window close.
|
|
141
|
+
fn detach_native_actions(&mut self) {}
|
|
115
142
|
}
|
|
116
143
|
|
|
117
144
|
/// No-op / test host that only stores the last committed tree.
|
|
@@ -9,13 +9,23 @@ repository = { workspace = true }
|
|
|
9
9
|
[features]
|
|
10
10
|
default = []
|
|
11
11
|
regex = ["tishlang_core/regex"]
|
|
12
|
+
# Propagate `send-values` so that every native-function closure we build
|
|
13
|
+
# in the VM (array / string / object methods) picks up the right
|
|
14
|
+
# `Rc<dyn Fn>` vs `Arc<dyn Fn + Send + Sync>` wrapper at compile time.
|
|
15
|
+
send-values = ["tishlang_core/send-values", "tishlang_builtins/send-values"]
|
|
12
16
|
# For wasm32 target: use web_sys console for output
|
|
13
17
|
wasm = ["dep:wasm-bindgen"]
|
|
14
|
-
# Built-in modules: fs, http, process (for bytecode LoadNativeExport)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
# Built-in modules: fs, http, process (for bytecode LoadNativeExport).
|
|
19
|
+
# `dep:tishlang_runtime` is required so enabling e.g. `http` always activates the optional runtime crate
|
|
20
|
+
# (some Cargo/cache combinations only saw `tishlang_runtime/http` and built VM stubs without http).
|
|
21
|
+
fs = ["dep:tishlang_runtime", "tishlang_runtime/fs"]
|
|
22
|
+
process = ["dep:tishlang_runtime", "tishlang_runtime/process"]
|
|
23
|
+
# Timer globals + `tish:timers` LoadNativeExport (uses tishlang_runtime TLS timer registry).
|
|
24
|
+
timers = ["dep:tishlang_runtime"]
|
|
25
|
+
# Any HTTP build needs Send-safe values so handlers can be dispatched
|
|
26
|
+
# across worker threads or processes. HTTP implies timers (fetch/Promise often pair with setTimeout).
|
|
27
|
+
http = ["dep:tishlang_runtime", "tishlang_runtime/http", "send-values", "timers"]
|
|
28
|
+
ws = ["dep:tishlang_runtime", "tishlang_runtime/ws"]
|
|
19
29
|
|
|
20
30
|
[dependencies]
|
|
21
31
|
tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
|