@tishlang/tish 1.10.0 → 1.12.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.
@@ -70,6 +70,14 @@ pub type NativeFn = std::rc::Rc<dyn Fn(&[Value]) -> Value>;
70
70
 
71
71
  /// Trait for opaque Rust types exposed to Tish (e.g. Polars DataFrame).
72
72
  /// Implementors provide method dispatch so Tish can call methods on the value.
73
+ ///
74
+ /// The `Send + Sync` supertrait bound is conditional on the `send-values`
75
+ /// feature. When `send-values` is off (single-threaded VMs: wasm browser /
76
+ /// wasi / interpreter / cranelift), `NativeFn` is already `Rc<dyn Fn>`, so
77
+ /// `Value` is `!Send` anyway — dropping the bound here loses nothing and lets
78
+ /// `!Send` opaques like `JsHandle(wasm_bindgen::JsValue)` be stored in a
79
+ /// `Value::Opaque` on the browser runtime.
80
+ #[cfg(feature = "send-values")]
73
81
  pub trait TishOpaque: Send + Sync {
74
82
  /// Display name for the type (e.g. "DataFrame").
75
83
  fn type_name(&self) -> &'static str;
@@ -81,6 +89,19 @@ pub trait TishOpaque: Send + Sync {
81
89
  fn as_any(&self) -> &dyn std::any::Any;
82
90
  }
83
91
 
92
+ /// Single-threaded variant (no `Send + Sync` bound); see the `send-values` doc above.
93
+ #[cfg(not(feature = "send-values"))]
94
+ pub trait TishOpaque {
95
+ /// Display name for the type (e.g. "DataFrame").
96
+ fn type_name(&self) -> &'static str;
97
+
98
+ /// Get a method by name. Returns a native function if the method exists.
99
+ fn get_method(&self, name: &str) -> Option<NativeFn>;
100
+
101
+ /// For downcasting `Arc<dyn TishOpaque>` in native crates (e.g. Polars → `DataFrame`).
102
+ fn as_any(&self) -> &dyn std::any::Any;
103
+ }
104
+
84
105
  /// Trait for Promise-like values that can be awaited (block until settled).
85
106
  /// Implemented by the runtime for native compile; interpreter uses its own Promise.
86
107
  pub trait TishPromise: Send + Sync {
@@ -103,7 +103,7 @@ fn main() {{
103
103
  message: format!("Cannot write build.rs: {}", e),
104
104
  })?;
105
105
 
106
- tishlang_build_utils::run_cargo_build(&build_dir, None)
106
+ tishlang_build_utils::run_cargo_build(&build_dir, None, None)
107
107
  .map_err(|e| CraneliftError { message: e })?;
108
108
 
109
109
  let binary_dir = build_dir.join("target").join("release");
@@ -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
- let out_name = output_path
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 build_dir = tishlang_build_utils::create_build_dir("tish_build", out_name)?;
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.contains("#[tokio::main]");
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
- [[bin]]
149
- name = "{}"
150
- path = "src/main.rs"
151
-
152
- {}
153
-
202
+ {}{}
154
203
  [dependencies]
155
204
  tishlang_runtime = {{ path = {:?}{} }}
156
205
  {}{}"#,
157
- out_name, profile, runtime_path, features_str, more_deps, ui_dep
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/main.rs"), rust_main)
167
- .map_err(|e| format!("Cannot write main.rs: {}", e))?;
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("release"))
177
- .unwrap_or_else(|| build_dir.join("target").join("release"));
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 binary = tishlang_build_utils::find_release_binary(&binary_dir, out_name)?;
182
- let target = tishlang_build_utils::resolve_output_path(output_path, out_name);
183
- tishlang_build_utils::copy_binary_to_output(&binary, &target)?;
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::compile_project_full(
111
+ tishlang_compile::compile_project_full_emit(
78
112
  entry_path,
79
113
  project_root,
80
- features,
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
- crate::build::build_via_cargo(
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
- &effective_features,
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.contains("#[tokio::main]");
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, substring as string_substring_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
- let due = take_due_timers();
53
- for (id, callback, args, interval_ms) in due {
54
- if let Value::Function(f) = &callback {
55
- let _ = f(&args);
52
+ for _ in 0..64 {
53
+ let due = take_due_timers();
54
+ if due.is_empty() {
55
+ break;
56
56
  }
57
- if interval_ms > 0 {
58
- re_register_interval(id, callback, args, interval_ms);
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, native_use_effect, native_use_memo,
16
- native_use_state, run_with_current_root, ui_h, ui_text, unregister_root,
17
- unregister_root_hooks_and_effects, with_host_for_root, with_thread_local_host, HeadlessHost,
18
- Host, RootId, FRAGMENT_SENTINEL, LEGACY_ROOT_ID,
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
  };