@tishlang/tish 1.10.0 → 1.13.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 +0 -0
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/src/cli_help.rs +9 -1
- package/crates/tish/src/main.rs +55 -3
- package/crates/tish_build_utils/src/lib.rs +33 -10
- package/crates/tish_builtins/src/math.rs +7 -0
- package/crates/tish_builtins/src/string.rs +26 -0
- package/crates/tish_compile/src/codegen.rs +207 -26
- package/crates/tish_compile/src/lib.rs +19 -9
- package/crates/tish_compile/src/resolve.rs +185 -3
- package/crates/tish_core/src/value.rs +21 -0
- package/crates/tish_cranelift/src/link.rs +1 -1
- package/crates/tish_fmt/src/lib.rs +1201 -132
- package/crates/tish_native/src/build.rs +88 -19
- package/crates/tish_native/src/config.rs +48 -0
- package/crates/tish_native/src/lib.rs +48 -5
- package/crates/tish_runtime/src/lib.rs +9 -1
- package/crates/tish_runtime/src/timers.rs +12 -7
- package/crates/tish_ui/src/lib.rs +5 -4
- package/crates/tish_ui/src/runtime/hooks.rs +119 -23
- package/crates/tish_ui/src/runtime/mod.rs +7 -26
- package/crates/tish_vm/src/vm.rs +40 -0
- package/crates/tish_wasm/src/lib.rs +27 -0
- package/crates/tish_wasm_runtime/Cargo.toml +6 -0
- package/crates/tish_wasm_runtime/src/gpu.rs +413 -0
- package/crates/tish_wasm_runtime/src/lib.rs +5 -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
|
@@ -5,6 +5,8 @@ use std::path::Path;
|
|
|
5
5
|
|
|
6
6
|
use tishlang_compile::ResolvedNativeModule;
|
|
7
7
|
|
|
8
|
+
use crate::config::{NativeArtifact, NativeBuildConfig};
|
|
9
|
+
|
|
8
10
|
/// `tishlang_runtime` Cargo feature names (subset of CLI / compile feature names).
|
|
9
11
|
const RUNTIME_CARGO_FEATURES: &[&str] = &[
|
|
10
12
|
"http",
|
|
@@ -69,6 +71,10 @@ fn inject_generated_native_mod(rust_code: &str) -> String {
|
|
|
69
71
|
}
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
pub(crate) fn rust_code_needs_tokio(rust_code: &str) -> bool {
|
|
75
|
+
rust_code.contains("#[tokio::main]") || rust_code.contains("tokio::runtime::Runtime")
|
|
76
|
+
}
|
|
77
|
+
|
|
72
78
|
pub fn build_via_cargo(
|
|
73
79
|
rust_code: &str,
|
|
74
80
|
native_modules: Vec<ResolvedNativeModule>,
|
|
@@ -78,11 +84,34 @@ pub fn build_via_cargo(
|
|
|
78
84
|
generated_native_rs: Option<&str>,
|
|
79
85
|
project_root: Option<&Path>,
|
|
80
86
|
) -> Result<(), String> {
|
|
81
|
-
|
|
87
|
+
build_via_cargo_with_config(
|
|
88
|
+
rust_code,
|
|
89
|
+
native_modules,
|
|
90
|
+
output_path,
|
|
91
|
+
features,
|
|
92
|
+
extra_dependencies_toml,
|
|
93
|
+
generated_native_rs,
|
|
94
|
+
project_root,
|
|
95
|
+
&NativeBuildConfig::desktop(),
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
pub fn build_via_cargo_with_config(
|
|
100
|
+
rust_code: &str,
|
|
101
|
+
native_modules: Vec<ResolvedNativeModule>,
|
|
102
|
+
output_path: &Path,
|
|
103
|
+
features: &[String],
|
|
104
|
+
extra_dependencies_toml: &str,
|
|
105
|
+
generated_native_rs: Option<&str>,
|
|
106
|
+
project_root: Option<&Path>,
|
|
107
|
+
build_config: &NativeBuildConfig,
|
|
108
|
+
) -> Result<(), String> {
|
|
109
|
+
let out_stem = output_path
|
|
82
110
|
.file_stem()
|
|
83
111
|
.and_then(|s| s.to_str())
|
|
84
112
|
.unwrap_or("tish_out");
|
|
85
|
-
let
|
|
113
|
+
let cargo_name = tishlang_build_utils::cargo_target_name(out_stem);
|
|
114
|
+
let build_dir = tishlang_build_utils::create_build_dir("tish_build", out_stem)?;
|
|
86
115
|
|
|
87
116
|
let runtime_path = tishlang_build_utils::find_runtime_path_for_project(project_root)?;
|
|
88
117
|
|
|
@@ -94,7 +123,7 @@ pub fn build_via_cargo(
|
|
|
94
123
|
format!(", features = {:?}", runtime_refs)
|
|
95
124
|
};
|
|
96
125
|
|
|
97
|
-
let needs_tokio = rust_code
|
|
126
|
+
let needs_tokio = rust_code_needs_tokio(rust_code);
|
|
98
127
|
let tokio_dep = if needs_tokio {
|
|
99
128
|
"\ntokio = { version = \"1\", features = [\"rt-multi-thread\", \"macros\"] }\n"
|
|
100
129
|
} else {
|
|
@@ -139,22 +168,42 @@ pub fn build_via_cargo(
|
|
|
139
168
|
};
|
|
140
169
|
|
|
141
170
|
let profile = nested_release_profile_toml();
|
|
171
|
+
let src_file = if build_config.artifact == NativeArtifact::StaticLib {
|
|
172
|
+
"lib.rs"
|
|
173
|
+
} else {
|
|
174
|
+
"main.rs"
|
|
175
|
+
};
|
|
176
|
+
let crate_section = if build_config.artifact == NativeArtifact::StaticLib {
|
|
177
|
+
format!(
|
|
178
|
+
r#"[lib]
|
|
179
|
+
name = "{}"
|
|
180
|
+
crate-type = ["staticlib"]
|
|
181
|
+
path = "src/lib.rs"
|
|
182
|
+
|
|
183
|
+
"#,
|
|
184
|
+
cargo_name
|
|
185
|
+
)
|
|
186
|
+
} else {
|
|
187
|
+
format!(
|
|
188
|
+
r#"[[bin]]
|
|
189
|
+
name = "{}"
|
|
190
|
+
path = "src/main.rs"
|
|
191
|
+
|
|
192
|
+
"#,
|
|
193
|
+
cargo_name
|
|
194
|
+
)
|
|
195
|
+
};
|
|
142
196
|
let cargo_toml = format!(
|
|
143
197
|
r#"[package]
|
|
144
198
|
name = "tish_output"
|
|
145
199
|
version = "0.1.0"
|
|
146
200
|
edition = "2021"
|
|
147
201
|
|
|
148
|
-
|
|
149
|
-
name = "{}"
|
|
150
|
-
path = "src/main.rs"
|
|
151
|
-
|
|
152
|
-
{}
|
|
153
|
-
|
|
202
|
+
{}{}
|
|
154
203
|
[dependencies]
|
|
155
204
|
tishlang_runtime = {{ path = {:?}{} }}
|
|
156
205
|
{}{}"#,
|
|
157
|
-
|
|
206
|
+
crate_section, profile, runtime_path, features_str, more_deps, ui_dep
|
|
158
207
|
);
|
|
159
208
|
|
|
160
209
|
fs::write(build_dir.join("Cargo.toml"), cargo_toml)
|
|
@@ -163,24 +212,44 @@ tishlang_runtime = {{ path = {:?}{} }}
|
|
|
163
212
|
fs::write(build_dir.join("src/generated_native.rs"), gen)
|
|
164
213
|
.map_err(|e| format!("Cannot write generated_native.rs: {}", e))?;
|
|
165
214
|
}
|
|
166
|
-
fs::write(build_dir.join("src
|
|
167
|
-
.map_err(|e| format!("Cannot write
|
|
215
|
+
fs::write(build_dir.join("src").join(src_file), rust_main)
|
|
216
|
+
.map_err(|e| format!("Cannot write {}: {}", src_file, e))?;
|
|
168
217
|
|
|
169
218
|
let workspace_target = Path::new(&runtime_path)
|
|
170
219
|
.parent()
|
|
171
220
|
.and_then(|p| p.parent())
|
|
172
221
|
.map(|ws| ws.join("target"));
|
|
173
222
|
let target_dir = workspace_target.filter(|p| p.exists());
|
|
223
|
+
let cross = build_config.cargo_target.as_deref();
|
|
224
|
+
let release_sub = if let Some(triple) = cross {
|
|
225
|
+
format!("{triple}/release")
|
|
226
|
+
} else {
|
|
227
|
+
"release".to_string()
|
|
228
|
+
};
|
|
174
229
|
let binary_dir = target_dir
|
|
175
230
|
.as_ref()
|
|
176
|
-
.map(|t| t.join(
|
|
177
|
-
.unwrap_or_else(|| build_dir.join("target").join(
|
|
231
|
+
.map(|t| t.join(&release_sub))
|
|
232
|
+
.unwrap_or_else(|| build_dir.join("target").join(&release_sub));
|
|
178
233
|
|
|
179
|
-
tishlang_build_utils::run_cargo_build(&build_dir, target_dir.as_deref())?;
|
|
234
|
+
tishlang_build_utils::run_cargo_build(&build_dir, target_dir.as_deref(), cross)?;
|
|
180
235
|
|
|
181
|
-
let
|
|
182
|
-
|
|
183
|
-
|
|
236
|
+
let artifact = if build_config.artifact == NativeArtifact::StaticLib {
|
|
237
|
+
tishlang_build_utils::find_release_staticlib(&binary_dir, &cargo_name)?
|
|
238
|
+
} else {
|
|
239
|
+
tishlang_build_utils::find_release_binary(&binary_dir, &cargo_name)?
|
|
240
|
+
};
|
|
241
|
+
let target = if build_config.artifact == NativeArtifact::StaticLib {
|
|
242
|
+
if output_path.extension().is_some_and(|e| e == "a") {
|
|
243
|
+
output_path.to_path_buf()
|
|
244
|
+
} else if output_path.to_string_lossy().ends_with('/') || output_path.is_dir() {
|
|
245
|
+
output_path.join(format!("lib{out_stem}.a"))
|
|
246
|
+
} else {
|
|
247
|
+
output_path.with_extension("a")
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
tishlang_build_utils::resolve_output_path(output_path, out_stem)
|
|
251
|
+
};
|
|
252
|
+
tishlang_build_utils::copy_binary_to_output(&artifact, &target)?;
|
|
184
253
|
|
|
185
254
|
Ok(())
|
|
186
255
|
}
|
|
@@ -321,7 +390,7 @@ tishlang_runtime = {{ path = {:?}{} }}
|
|
|
321
390
|
.map(|t| t.join("release"))
|
|
322
391
|
.unwrap_or_else(|| build_dir.join("target").join("release"));
|
|
323
392
|
|
|
324
|
-
tishlang_build_utils::run_cargo_build(&build_dir, target_dir.as_deref())?;
|
|
393
|
+
tishlang_build_utils::run_cargo_build(&build_dir, target_dir.as_deref(), None)?;
|
|
325
394
|
|
|
326
395
|
for i in 0..bins.len() {
|
|
327
396
|
let stem = bins[i].0.as_str();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//! Native build configuration (desktop binary vs iOS staticlib, cross-target).
|
|
2
|
+
|
|
3
|
+
use tishlang_compile::NativeEmitMode;
|
|
4
|
+
|
|
5
|
+
/// Output artifact kind for `tish build --target native`.
|
|
6
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
7
|
+
pub enum NativeArtifact {
|
|
8
|
+
#[default]
|
|
9
|
+
Bin,
|
|
10
|
+
StaticLib,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/// Options passed from the CLI into nested `cargo build`.
|
|
14
|
+
#[derive(Debug, Clone, Default)]
|
|
15
|
+
pub struct NativeBuildConfig {
|
|
16
|
+
pub artifact: NativeArtifact,
|
|
17
|
+
/// When set, run `cargo build --target <triple>` and skip `-C target-cpu=native`.
|
|
18
|
+
pub cargo_target: Option<String>,
|
|
19
|
+
pub emit_mode: NativeEmitMode,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
impl NativeBuildConfig {
|
|
23
|
+
pub fn desktop() -> Self {
|
|
24
|
+
Self::default()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
pub fn ios_staticlib(triple: &str) -> Self {
|
|
28
|
+
Self {
|
|
29
|
+
artifact: NativeArtifact::StaticLib,
|
|
30
|
+
cargo_target: Some(triple.to_string()),
|
|
31
|
+
emit_mode: NativeEmitMode::EmbeddedLib,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
pub fn is_cross_compile(&self) -> bool {
|
|
36
|
+
self.cargo_target.is_some()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Filter runtime features for iOS sandbox builds.
|
|
41
|
+
pub fn ios_runtime_features(features: &[String]) -> Vec<String> {
|
|
42
|
+
const ALLOW: &[&str] = &["http", "http-hyper", "regex", "timers"];
|
|
43
|
+
features
|
|
44
|
+
.iter()
|
|
45
|
+
.filter(|f| ALLOW.contains(&f.as_str()))
|
|
46
|
+
.cloned()
|
|
47
|
+
.collect()
|
|
48
|
+
}
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
//! emit Rust using `Vec<f64>` / fixed primitives instead of `Value` on hot paths.
|
|
11
11
|
|
|
12
12
|
mod build;
|
|
13
|
+
mod config;
|
|
14
|
+
|
|
15
|
+
pub use config::{ios_runtime_features, NativeArtifact, NativeBuildConfig};
|
|
13
16
|
|
|
14
17
|
use std::path::Path;
|
|
15
18
|
use tishlang_ast::Program;
|
|
@@ -56,6 +59,27 @@ pub fn compile_to_native(
|
|
|
56
59
|
features: &[String],
|
|
57
60
|
native_backend: &str,
|
|
58
61
|
optimize: bool,
|
|
62
|
+
) -> Result<(), NativeError> {
|
|
63
|
+
compile_to_native_with_config(
|
|
64
|
+
entry_path,
|
|
65
|
+
project_root,
|
|
66
|
+
output_path,
|
|
67
|
+
features,
|
|
68
|
+
native_backend,
|
|
69
|
+
optimize,
|
|
70
|
+
&NativeBuildConfig::desktop(),
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Compile a Tish project to a native binary or static library.
|
|
75
|
+
pub fn compile_to_native_with_config(
|
|
76
|
+
entry_path: &Path,
|
|
77
|
+
project_root: Option<&Path>,
|
|
78
|
+
output_path: &Path,
|
|
79
|
+
features: &[String],
|
|
80
|
+
native_backend: &str,
|
|
81
|
+
optimize: bool,
|
|
82
|
+
build_config: &NativeBuildConfig,
|
|
59
83
|
) -> Result<(), NativeError> {
|
|
60
84
|
let backend = match native_backend {
|
|
61
85
|
"rust" => Backend::Rust,
|
|
@@ -73,25 +97,44 @@ pub fn compile_to_native(
|
|
|
73
97
|
|
|
74
98
|
match backend {
|
|
75
99
|
Backend::Rust => {
|
|
100
|
+
let ios_cap = build_config.cargo_target.as_ref().map(|_| {
|
|
101
|
+
ios_runtime_features(features)
|
|
102
|
+
.into_iter()
|
|
103
|
+
.collect::<std::collections::HashSet<_>>()
|
|
104
|
+
});
|
|
105
|
+
let compile_features = if ios_cap.is_some() {
|
|
106
|
+
ios_runtime_features(features)
|
|
107
|
+
} else {
|
|
108
|
+
features.to_vec()
|
|
109
|
+
};
|
|
76
110
|
let (rust_code, native_modules, effective_features, native_build) =
|
|
77
|
-
tishlang_compile::
|
|
111
|
+
tishlang_compile::compile_project_full_emit(
|
|
78
112
|
entry_path,
|
|
79
113
|
project_root,
|
|
80
|
-
|
|
114
|
+
&compile_features,
|
|
81
115
|
optimize,
|
|
116
|
+
build_config.emit_mode,
|
|
117
|
+
ios_cap.as_ref(),
|
|
82
118
|
)
|
|
83
119
|
.map_err(|e| NativeError {
|
|
84
120
|
message: e.to_string(),
|
|
85
121
|
})?;
|
|
86
122
|
|
|
87
|
-
|
|
123
|
+
let features_for_cargo = if build_config.cargo_target.is_some() {
|
|
124
|
+
ios_runtime_features(&effective_features)
|
|
125
|
+
} else {
|
|
126
|
+
effective_features
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
crate::build::build_via_cargo_with_config(
|
|
88
130
|
&rust_code,
|
|
89
131
|
native_modules,
|
|
90
132
|
output_path,
|
|
91
|
-
&
|
|
133
|
+
&features_for_cargo,
|
|
92
134
|
&native_build.rust_dependencies_toml,
|
|
93
135
|
native_build.generated_native_rs.as_deref(),
|
|
94
136
|
project_root,
|
|
137
|
+
build_config,
|
|
95
138
|
)
|
|
96
139
|
.map_err(|e| NativeError { message: e })
|
|
97
140
|
}
|
|
@@ -347,7 +390,7 @@ pub fn compile_many_to_native(
|
|
|
347
390
|
merged_extra_deps.push_str(extra);
|
|
348
391
|
merged_extra_deps.push('\n');
|
|
349
392
|
}
|
|
350
|
-
needs_tokio |= rust_code
|
|
393
|
+
needs_tokio |= crate::build::rust_code_needs_tokio(&rust_code);
|
|
351
394
|
needs_ui |= rust_code.contains("tishlang_ui");
|
|
352
395
|
bins.push((stem, rust_code, native_build.generated_native_rs));
|
|
353
396
|
}
|
|
@@ -47,7 +47,8 @@ pub use tishlang_builtins::string::{
|
|
|
47
47
|
pad_start as string_pad_start_impl, repeat as string_repeat_impl,
|
|
48
48
|
replace as string_replace_impl, replace_all as string_replace_all_impl,
|
|
49
49
|
slice as string_slice_impl, split as string_split_impl,
|
|
50
|
-
starts_with as string_starts_with_impl,
|
|
50
|
+
starts_with as string_starts_with_impl, substr as string_substr_impl,
|
|
51
|
+
substring as string_substring_impl,
|
|
51
52
|
to_lower_case as string_to_lower_case, to_upper_case as string_to_upper_case,
|
|
52
53
|
trim as string_trim,
|
|
53
54
|
};
|
|
@@ -105,6 +106,9 @@ pub fn string_slice(s: &Value, start: &Value, end: &Value) -> Value {
|
|
|
105
106
|
pub fn string_substring(s: &Value, start: &Value, end: &Value) -> Value {
|
|
106
107
|
string_substring_impl(s, start, end)
|
|
107
108
|
}
|
|
109
|
+
pub fn string_substr(s: &Value, start: &Value, length: &Value) -> Value {
|
|
110
|
+
string_substr_impl(s, start, length)
|
|
111
|
+
}
|
|
108
112
|
pub fn string_split(s: &Value, sep: &Value) -> Value {
|
|
109
113
|
string_split_impl(s, sep)
|
|
110
114
|
}
|
|
@@ -420,6 +424,7 @@ pub use tishlang_builtins::math::{
|
|
|
420
424
|
exp as tish_math_exp_impl, floor as tish_math_floor_impl, max as tish_math_max_impl,
|
|
421
425
|
min as tish_math_min_impl, pow as tish_math_pow_impl, random as tish_math_random_impl,
|
|
422
426
|
round as tish_math_round_impl, sign as tish_math_sign_impl, sin as tish_math_sin_impl,
|
|
427
|
+
imul as tish_math_imul_impl,
|
|
423
428
|
sqrt as tish_math_sqrt_impl, tan as tish_math_tan_impl, trunc as tish_math_trunc_impl,
|
|
424
429
|
};
|
|
425
430
|
|
|
@@ -460,6 +465,9 @@ pub fn math_exp(args: &[Value]) -> Value {
|
|
|
460
465
|
pub fn math_trunc(args: &[Value]) -> Value {
|
|
461
466
|
tish_math_trunc_impl(args)
|
|
462
467
|
}
|
|
468
|
+
pub fn math_imul(args: &[Value]) -> Value {
|
|
469
|
+
tish_math_imul_impl(args)
|
|
470
|
+
}
|
|
463
471
|
pub fn math_pow(args: &[Value]) -> Value {
|
|
464
472
|
tish_math_pow_impl(args)
|
|
465
473
|
}
|
|
@@ -47,15 +47,20 @@ pub fn drain_timers() {
|
|
|
47
47
|
run_due_timers();
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/// Run all due timer callbacks.
|
|
50
|
+
/// Run all due timer callbacks (including timers scheduled by other timers).
|
|
51
51
|
fn run_due_timers() {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if
|
|
55
|
-
|
|
52
|
+
for _ in 0..64 {
|
|
53
|
+
let due = take_due_timers();
|
|
54
|
+
if due.is_empty() {
|
|
55
|
+
break;
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
for (id, callback, args, interval_ms) in due {
|
|
58
|
+
if let Value::Function(f) = &callback {
|
|
59
|
+
let _ = f(&args);
|
|
60
|
+
}
|
|
61
|
+
if interval_ms > 0 {
|
|
62
|
+
re_register_interval(id, callback, args, interval_ms);
|
|
63
|
+
}
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
66
|
}
|
|
@@ -12,8 +12,9 @@ pub mod runtime;
|
|
|
12
12
|
#[cfg(feature = "runtime")]
|
|
13
13
|
pub use runtime::{
|
|
14
14
|
alloc_root_id, current_root_id, drop_host_for_root, fragment_value, install_host_for_root,
|
|
15
|
-
install_thread_local_host, native_create_root,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
install_thread_local_host, native_create_root,
|
|
16
|
+
native_use_effect, native_use_layout_effect, native_use_memo, native_use_ref,
|
|
17
|
+
native_use_state, run_with_current_root, ui_h, ui_text,
|
|
18
|
+
unregister_root, unregister_root_hooks_and_effects, with_host_for_root,
|
|
19
|
+
with_thread_local_host, HeadlessHost, Host, RootId, FRAGMENT_SENTINEL, LEGACY_ROOT_ID,
|
|
19
20
|
};
|
|
@@ -5,6 +5,8 @@ use std::cell::{Cell, RefCell};
|
|
|
5
5
|
use std::collections::HashMap;
|
|
6
6
|
use std::rc::Rc;
|
|
7
7
|
|
|
8
|
+
use std::sync::Arc;
|
|
9
|
+
|
|
8
10
|
use tishlang_core::{ObjectMap, Value, VmRef};
|
|
9
11
|
|
|
10
12
|
use super::Host;
|
|
@@ -18,9 +20,10 @@ pub const LEGACY_ROOT_ID: RootId = 1;
|
|
|
18
20
|
thread_local! {
|
|
19
21
|
static HOOKS: RefCell<HashMap<RootId, HookState>> = RefCell::new(HashMap::new());
|
|
20
22
|
static CURRENT_ROOT: Cell<Option<RootId>> = Cell::new(None);
|
|
21
|
-
static HOSTS: RefCell<HashMap<RootId, Box<dyn Host
|
|
23
|
+
static HOSTS: RefCell<HashMap<RootId, Rc<RefCell<Box<dyn Host>>>>> = RefCell::new(HashMap::new());
|
|
22
24
|
static NEXT_DYNAMIC_ROOT_ID: Cell<RootId> = Cell::new(2);
|
|
23
25
|
static IN_FLUSH: Cell<bool> = Cell::new(false);
|
|
26
|
+
static IN_EFFECT_FLUSH: Cell<bool> = Cell::new(false);
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
/// Allocate an id for an additional in-process window (starts at 2; 1 is legacy primary).
|
|
@@ -42,7 +45,8 @@ fn ensure_hook_entry(root_id: RootId) {
|
|
|
42
45
|
pub fn install_host_for_root(root_id: RootId, host: Box<dyn Host>) {
|
|
43
46
|
ensure_hook_entry(root_id);
|
|
44
47
|
HOSTS.with(|h| {
|
|
45
|
-
h.borrow_mut()
|
|
48
|
+
h.borrow_mut()
|
|
49
|
+
.insert(root_id, Rc::new(RefCell::new(host)));
|
|
46
50
|
});
|
|
47
51
|
}
|
|
48
52
|
|
|
@@ -77,8 +81,10 @@ pub fn unregister_root(root_id: RootId) {
|
|
|
77
81
|
|
|
78
82
|
pub fn with_host_for_root<R>(root_id: RootId, f: impl FnOnce(&mut dyn Host) -> R) -> Option<R> {
|
|
79
83
|
HOSTS.with(|c| {
|
|
80
|
-
let
|
|
81
|
-
m.
|
|
84
|
+
let m = c.borrow();
|
|
85
|
+
let host = m.get(&root_id)?;
|
|
86
|
+
let mut host = host.try_borrow_mut().ok()?;
|
|
87
|
+
Some(f(host.as_mut()))
|
|
82
88
|
})
|
|
83
89
|
}
|
|
84
90
|
|
|
@@ -128,6 +134,11 @@ pub struct HookState {
|
|
|
128
134
|
effect_cells: Rc<RefCell<Vec<EffectCell>>>,
|
|
129
135
|
effect_cursor: usize,
|
|
130
136
|
pending_effects: Rc<RefCell<Vec<PendingEffect>>>,
|
|
137
|
+
layout_effect_cells: Rc<RefCell<Vec<EffectCell>>>,
|
|
138
|
+
layout_effect_cursor: usize,
|
|
139
|
+
pending_layout_effects: Rc<RefCell<Vec<PendingEffect>>>,
|
|
140
|
+
ref_slots: Rc<RefCell<Vec<Value>>>,
|
|
141
|
+
ref_cursor: usize,
|
|
131
142
|
}
|
|
132
143
|
|
|
133
144
|
impl Default for HookState {
|
|
@@ -143,6 +154,11 @@ impl Default for HookState {
|
|
|
143
154
|
effect_cells: Rc::new(RefCell::new(Vec::new())),
|
|
144
155
|
effect_cursor: 0,
|
|
145
156
|
pending_effects: Rc::new(RefCell::new(Vec::new())),
|
|
157
|
+
layout_effect_cells: Rc::new(RefCell::new(Vec::new())),
|
|
158
|
+
layout_effect_cursor: 0,
|
|
159
|
+
pending_layout_effects: Rc::new(RefCell::new(Vec::new())),
|
|
160
|
+
ref_slots: Rc::new(RefCell::new(Vec::new())),
|
|
161
|
+
ref_cursor: 0,
|
|
146
162
|
}
|
|
147
163
|
}
|
|
148
164
|
}
|
|
@@ -160,11 +176,17 @@ fn run_all_effect_cleanups(cells: &RefCell<Vec<EffectCell>>) {
|
|
|
160
176
|
impl HookState {
|
|
161
177
|
pub fn reset_for_new_root(&mut self) {
|
|
162
178
|
run_all_effect_cleanups(self.effect_cells.as_ref());
|
|
179
|
+
run_all_effect_cleanups(self.layout_effect_cells.as_ref());
|
|
163
180
|
self.effect_cells.borrow_mut().clear();
|
|
181
|
+
self.layout_effect_cells.borrow_mut().clear();
|
|
164
182
|
self.effect_cursor = 0;
|
|
183
|
+
self.layout_effect_cursor = 0;
|
|
165
184
|
self.pending_effects.borrow_mut().clear();
|
|
185
|
+
self.pending_layout_effects.borrow_mut().clear();
|
|
166
186
|
self.state_slots.borrow_mut().clear();
|
|
167
187
|
self.cursor = 0;
|
|
188
|
+
self.ref_slots.borrow_mut().clear();
|
|
189
|
+
self.ref_cursor = 0;
|
|
168
190
|
self.root_vnode = None;
|
|
169
191
|
self.flush_scheduled = false;
|
|
170
192
|
self.memo_cache.borrow_mut().clear();
|
|
@@ -191,6 +213,7 @@ fn memo_dep_eq(a: &Value, b: &Value) -> bool {
|
|
|
191
213
|
}
|
|
192
214
|
ab.iter().zip(bb.iter()).all(|(x, y)| memo_dep_eq(x, y))
|
|
193
215
|
}
|
|
216
|
+
(Value::Object(a), Value::Object(b)) => tishlang_core::VmRef::ptr_eq(a, b),
|
|
194
217
|
_ => false,
|
|
195
218
|
}
|
|
196
219
|
}
|
|
@@ -239,7 +262,7 @@ pub fn native_use_state(args: &[Value]) -> Value {
|
|
|
239
262
|
st.flush_scheduled = true;
|
|
240
263
|
}
|
|
241
264
|
});
|
|
242
|
-
if !IN_FLUSH.get() {
|
|
265
|
+
if !IN_FLUSH.get() && !IN_EFFECT_FLUSH.get() {
|
|
243
266
|
drain_flush_queue();
|
|
244
267
|
}
|
|
245
268
|
Value::Null
|
|
@@ -284,9 +307,29 @@ pub fn native_use_memo(args: &[Value]) -> Value {
|
|
|
284
307
|
})
|
|
285
308
|
}
|
|
286
309
|
|
|
287
|
-
/// `
|
|
288
|
-
|
|
289
|
-
|
|
310
|
+
/// `useRef(initial)` → `{ current: initial }` (stable object identity per slot).
|
|
311
|
+
pub fn native_use_ref(args: &[Value]) -> Value {
|
|
312
|
+
let initial = args.first().cloned().unwrap_or(Value::Null);
|
|
313
|
+
let root_id = root_id_for_hooks();
|
|
314
|
+
HOOKS.with(|h| {
|
|
315
|
+
let mut map = h.borrow_mut();
|
|
316
|
+
let st = map.entry(root_id).or_default();
|
|
317
|
+
let i = st.ref_cursor;
|
|
318
|
+
st.ref_cursor += 1;
|
|
319
|
+
while i >= st.ref_slots.borrow().len() {
|
|
320
|
+
let mut m = ObjectMap::default();
|
|
321
|
+
m.insert(Arc::from("current"), initial.clone());
|
|
322
|
+
st.ref_slots.borrow_mut().push(Value::object(m));
|
|
323
|
+
}
|
|
324
|
+
let out = st.ref_slots.borrow()[i].clone();
|
|
325
|
+
out
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
fn queue_effect(
|
|
330
|
+
args: &[Value],
|
|
331
|
+
layout: bool,
|
|
332
|
+
) -> Value {
|
|
290
333
|
let Some(Value::Function(effect_fn)) = args.first() else {
|
|
291
334
|
return Value::Null;
|
|
292
335
|
};
|
|
@@ -301,9 +344,22 @@ pub fn native_use_effect(args: &[Value]) -> Value {
|
|
|
301
344
|
HOOKS.with(|h| {
|
|
302
345
|
let mut map = h.borrow_mut();
|
|
303
346
|
let st = map.entry(root_id).or_default();
|
|
304
|
-
let
|
|
305
|
-
|
|
306
|
-
|
|
347
|
+
let (cursor, cells, pending) = if layout {
|
|
348
|
+
(
|
|
349
|
+
&mut st.layout_effect_cursor,
|
|
350
|
+
&st.layout_effect_cells,
|
|
351
|
+
&st.pending_layout_effects,
|
|
352
|
+
)
|
|
353
|
+
} else {
|
|
354
|
+
(
|
|
355
|
+
&mut st.effect_cursor,
|
|
356
|
+
&st.effect_cells,
|
|
357
|
+
&st.pending_effects,
|
|
358
|
+
)
|
|
359
|
+
};
|
|
360
|
+
let i = *cursor;
|
|
361
|
+
*cursor += 1;
|
|
362
|
+
let cells = cells.clone();
|
|
307
363
|
let mut cells_b = cells.borrow_mut();
|
|
308
364
|
while cells_b.len() <= i {
|
|
309
365
|
cells_b.push(EffectCell::default());
|
|
@@ -315,7 +371,7 @@ pub fn native_use_effect(args: &[Value]) -> Value {
|
|
|
315
371
|
drop(cells_b);
|
|
316
372
|
|
|
317
373
|
if should_run {
|
|
318
|
-
|
|
374
|
+
pending.borrow_mut().push(PendingEffect {
|
|
319
375
|
slot: i,
|
|
320
376
|
effect_fn: Value::Function(effect_fn),
|
|
321
377
|
new_deps: deps,
|
|
@@ -325,14 +381,29 @@ pub fn native_use_effect(args: &[Value]) -> Value {
|
|
|
325
381
|
})
|
|
326
382
|
}
|
|
327
383
|
|
|
328
|
-
|
|
384
|
+
/// `useLayoutEffect(effect, deps?)` — runs synchronously after commit, before `useEffect`.
|
|
385
|
+
pub fn native_use_layout_effect(args: &[Value]) -> Value {
|
|
386
|
+
queue_effect(args, true)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/// `useEffect(effect, deps?)` — runs `effect` after the host commits the tree; compares `deps` like `useMemo`.
|
|
390
|
+
/// If `effect` returns a function, it is called before the next run or on root teardown (`render` replacement / [`unregister_root`]).
|
|
391
|
+
pub fn native_use_effect(args: &[Value]) -> Value {
|
|
392
|
+
queue_effect(args, false)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
fn flush_pending_effects_for(
|
|
396
|
+
root_id: RootId,
|
|
397
|
+
pending_key: impl FnOnce(&mut HookState) -> &Rc<RefCell<Vec<PendingEffect>>>,
|
|
398
|
+
cells_key: impl FnOnce(&HookState) -> Rc<RefCell<Vec<EffectCell>>>,
|
|
399
|
+
) {
|
|
329
400
|
let pending: Vec<PendingEffect> = HOOKS.with(|h| {
|
|
330
401
|
h.borrow_mut()
|
|
331
402
|
.get_mut(&root_id)
|
|
332
|
-
.map(|st| std::mem::take(&mut *st.
|
|
403
|
+
.map(|st| std::mem::take(&mut *pending_key(st).borrow_mut()))
|
|
333
404
|
.unwrap_or_default()
|
|
334
405
|
});
|
|
335
|
-
let cells_rc = HOOKS.with(|h| h.borrow().get(&root_id).map(
|
|
406
|
+
let cells_rc = HOOKS.with(|h| h.borrow().get(&root_id).map(cells_key));
|
|
336
407
|
let Some(cells_rc) = cells_rc else {
|
|
337
408
|
return;
|
|
338
409
|
};
|
|
@@ -370,6 +441,26 @@ fn flush_pending_effects(root_id: RootId) {
|
|
|
370
441
|
}
|
|
371
442
|
}
|
|
372
443
|
|
|
444
|
+
fn flush_pending_layout_effects(root_id: RootId) {
|
|
445
|
+
IN_EFFECT_FLUSH.set(true);
|
|
446
|
+
flush_pending_effects_for(
|
|
447
|
+
root_id,
|
|
448
|
+
|st| &st.pending_layout_effects,
|
|
449
|
+
|st| st.layout_effect_cells.clone(),
|
|
450
|
+
);
|
|
451
|
+
IN_EFFECT_FLUSH.set(false);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
fn flush_pending_effects(root_id: RootId) {
|
|
455
|
+
IN_EFFECT_FLUSH.set(true);
|
|
456
|
+
flush_pending_effects_for(
|
|
457
|
+
root_id,
|
|
458
|
+
|st| &st.pending_effects,
|
|
459
|
+
|st| st.effect_cells.clone(),
|
|
460
|
+
);
|
|
461
|
+
IN_EFFECT_FLUSH.set(false);
|
|
462
|
+
}
|
|
463
|
+
|
|
373
464
|
fn parse_root_id_arg(args: &[Value]) -> RootId {
|
|
374
465
|
match args.first() {
|
|
375
466
|
Some(Value::Number(n)) if n.is_finite() && *n >= 1.0 && n.fract() == 0.0 => *n as u64,
|
|
@@ -416,6 +507,10 @@ pub fn schedule_flush() {
|
|
|
416
507
|
}
|
|
417
508
|
|
|
418
509
|
fn drain_flush_queue() {
|
|
510
|
+
drain_flush_queue_on_current_thread();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
fn drain_flush_queue_on_current_thread() {
|
|
419
514
|
loop {
|
|
420
515
|
let root_id = HOOKS.with(|h| {
|
|
421
516
|
h.borrow()
|
|
@@ -440,8 +535,11 @@ fn drain_flush_queue() {
|
|
|
440
535
|
let st = map.get_mut(&root_id)?;
|
|
441
536
|
st.cursor = 0;
|
|
442
537
|
st.memo_cursor = 0;
|
|
538
|
+
st.ref_cursor = 0;
|
|
443
539
|
st.effect_cursor = 0;
|
|
540
|
+
st.layout_effect_cursor = 0;
|
|
444
541
|
st.pending_effects.borrow_mut().clear();
|
|
542
|
+
st.pending_layout_effects.borrow_mut().clear();
|
|
445
543
|
let app = st.root_app.clone()?;
|
|
446
544
|
let Value::Function(f) = app else {
|
|
447
545
|
return None;
|
|
@@ -452,18 +550,16 @@ fn drain_flush_queue() {
|
|
|
452
550
|
if let Some(f) = app_fn {
|
|
453
551
|
let tree = f(&[]);
|
|
454
552
|
HOOKS.with(|h| {
|
|
455
|
-
let
|
|
456
|
-
if let Some(st) = map.get_mut(&root_id) {
|
|
553
|
+
if let Some(st) = h.borrow_mut().get_mut(&root_id) {
|
|
457
554
|
st.root_vnode = Some(tree.clone());
|
|
458
|
-
HOSTS.with(|hosts| {
|
|
459
|
-
let mut hm = hosts.borrow_mut();
|
|
460
|
-
if let Some(host) = hm.get_mut(&root_id) {
|
|
461
|
-
host.commit_root(&tree);
|
|
462
|
-
}
|
|
463
|
-
});
|
|
464
555
|
}
|
|
465
556
|
});
|
|
557
|
+
let host = HOSTS.with(|hosts| hosts.borrow().get(&root_id).cloned());
|
|
558
|
+
if let Some(host) = host {
|
|
559
|
+
host.borrow_mut().commit_root(&tree);
|
|
560
|
+
}
|
|
466
561
|
IN_FLUSH.set(false);
|
|
562
|
+
flush_pending_layout_effects(root_id);
|
|
467
563
|
flush_pending_effects(root_id);
|
|
468
564
|
}
|
|
469
565
|
|