@tishlang/tish 1.5.0 → 1.7.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.
Files changed (85) hide show
  1. package/Cargo.toml +1 -0
  2. package/bin/tish +0 -0
  3. package/crates/js_to_tish/src/error.rs +2 -8
  4. package/crates/js_to_tish/src/transform/expr.rs +101 -130
  5. package/crates/js_to_tish/src/transform/stmt.rs +25 -22
  6. package/crates/tish/Cargo.toml +1 -1
  7. package/crates/tish/src/cli_help.rs +76 -29
  8. package/crates/tish/src/main.rs +85 -54
  9. package/crates/tish/tests/cargo_example_compile.rs +67 -0
  10. package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
  11. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
  12. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
  13. package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
  14. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
  15. package/crates/tish/tests/integration_test.rs +197 -47
  16. package/crates/tish/tests/run_optimize_stdout_parity.rs +3 -7
  17. package/crates/tish/tests/shortcircuit.rs +19 -4
  18. package/crates/tish_ast/src/ast.rs +12 -14
  19. package/crates/tish_build_utils/src/lib.rs +64 -6
  20. package/crates/tish_builtins/src/array.rs +52 -21
  21. package/crates/tish_builtins/src/construct.rs +2 -8
  22. package/crates/tish_builtins/src/globals.rs +30 -15
  23. package/crates/tish_builtins/src/lib.rs +5 -5
  24. package/crates/tish_builtins/src/math.rs +5 -3
  25. package/crates/tish_builtins/src/string.rs +71 -19
  26. package/crates/tish_bytecode/src/chunk.rs +0 -1
  27. package/crates/tish_bytecode/src/compiler.rs +164 -60
  28. package/crates/tish_bytecode/src/opcode.rs +13 -4
  29. package/crates/tish_bytecode/src/peephole.rs +2 -2
  30. package/crates/tish_compile/Cargo.toml +1 -0
  31. package/crates/tish_compile/src/codegen.rs +989 -318
  32. package/crates/tish_compile/src/infer.rs +69 -19
  33. package/crates/tish_compile/src/lib.rs +21 -8
  34. package/crates/tish_compile/src/resolve.rs +515 -94
  35. package/crates/tish_compile/src/types.rs +10 -14
  36. package/crates/tish_compile_js/src/codegen.rs +34 -13
  37. package/crates/tish_compile_js/src/tests_jsx.rs +30 -6
  38. package/crates/tish_compiler_wasm/src/lib.rs +16 -13
  39. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +40 -48
  40. package/crates/tish_core/src/json.rs +5 -3
  41. package/crates/tish_core/src/lib.rs +1 -1
  42. package/crates/tish_core/src/uri.rs +9 -6
  43. package/crates/tish_core/src/value.rs +92 -28
  44. package/crates/tish_cranelift/src/link.rs +6 -9
  45. package/crates/tish_cranelift/src/lower.rs +14 -8
  46. package/crates/tish_eval/src/eval.rs +398 -141
  47. package/crates/tish_eval/src/lib.rs +10 -6
  48. package/crates/tish_eval/src/natives.rs +95 -38
  49. package/crates/tish_eval/src/promise.rs +14 -8
  50. package/crates/tish_eval/src/timers.rs +28 -19
  51. package/crates/tish_eval/src/value.rs +10 -3
  52. package/crates/tish_fmt/src/lib.rs +29 -13
  53. package/crates/tish_lexer/src/lib.rs +217 -63
  54. package/crates/tish_lexer/src/token.rs +6 -6
  55. package/crates/tish_llvm/src/lib.rs +15 -8
  56. package/crates/tish_lsp/src/main.rs +41 -43
  57. package/crates/tish_native/src/build.rs +38 -15
  58. package/crates/tish_native/src/lib.rs +76 -32
  59. package/crates/tish_opt/src/lib.rs +67 -50
  60. package/crates/tish_parser/src/lib.rs +36 -11
  61. package/crates/tish_parser/src/parser.rs +172 -87
  62. package/crates/tish_runtime/src/http.rs +15 -6
  63. package/crates/tish_runtime/src/http_fetch.rs +24 -14
  64. package/crates/tish_runtime/src/lib.rs +224 -168
  65. package/crates/tish_runtime/src/promise.rs +1 -5
  66. package/crates/tish_runtime/src/ws.rs +45 -20
  67. package/crates/tish_runtime/tests/fetch_readable_stream.rs +5 -4
  68. package/crates/tish_ui/src/jsx.rs +41 -22
  69. package/crates/tish_ui/src/lib.rs +2 -2
  70. package/crates/tish_vm/src/vm.rs +320 -116
  71. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +8 -3
  72. package/crates/tish_wasm/src/lib.rs +38 -28
  73. package/crates/tishlang_cargo_bindgen/Cargo.toml +25 -0
  74. package/crates/tishlang_cargo_bindgen/src/classify.rs +265 -0
  75. package/crates/tishlang_cargo_bindgen/src/discover.rs +52 -0
  76. package/crates/tishlang_cargo_bindgen/src/infer.rs +372 -0
  77. package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
  78. package/crates/tishlang_cargo_bindgen/src/main.rs +164 -0
  79. package/crates/tishlang_cargo_bindgen/src/metadata.rs +114 -0
  80. package/package.json +1 -1
  81. package/platform/darwin-arm64/tish +0 -0
  82. package/platform/darwin-x64/tish +0 -0
  83. package/platform/linux-arm64/tish +0 -0
  84. package/platform/linux-x64/tish +0 -0
  85. package/platform/win32-x64/tish.exe +0 -0
@@ -158,7 +158,9 @@ pub fn find_workspace_root() -> Result<PathBuf, String> {
158
158
  if let Some(mut current) = exe.parent() {
159
159
  for _ in 0..15 {
160
160
  let crates_dir = current.join("crates");
161
- if crates_dir.join("tish_runtime").exists() || crates_dir.join("tish_cranelift_runtime").exists() {
161
+ if crates_dir.join("tish_runtime").exists()
162
+ || crates_dir.join("tish_cranelift_runtime").exists()
163
+ {
162
164
  return Ok(current.to_path_buf());
163
165
  }
164
166
  if let Some(p) = current.parent() {
@@ -177,6 +179,19 @@ pub fn find_workspace_root() -> Result<PathBuf, String> {
177
179
  }
178
180
  }
179
181
 
182
+ // Strategy 3b: `node_modules/@tishlang/tish` from cwd or any ancestor (package.json-only apps)
183
+ if let Ok(mut dir) = std::env::current_dir() {
184
+ for _ in 0..32 {
185
+ let npm_pkg = dir.join("node_modules").join("@tishlang").join("tish");
186
+ if is_tish_workspace_root(&npm_pkg) {
187
+ return Ok(npm_pkg);
188
+ }
189
+ if !dir.pop() {
190
+ break;
191
+ }
192
+ }
193
+ }
194
+
180
195
  // Strategy 4: Walk from current working directory
181
196
  if let Ok(mut current) = std::env::current_dir() {
182
197
  for _ in 0..15 {
@@ -202,6 +217,21 @@ pub fn find_workspace_root() -> Result<PathBuf, String> {
202
217
  Err("Cannot find Tish workspace root. Run from workspace root or use cargo run.".to_string())
203
218
  }
204
219
 
220
+ /// Path to `crates/tish_runtime` inside a locally installed `@tishlang/tish` npm package.
221
+ pub fn npm_package_runtime_path(project_root: &Path) -> Option<PathBuf> {
222
+ let p = project_root
223
+ .join("node_modules")
224
+ .join("@tishlang")
225
+ .join("tish")
226
+ .join("crates")
227
+ .join("tish_runtime");
228
+ if p.is_dir() {
229
+ Some(p)
230
+ } else {
231
+ None
232
+ }
233
+ }
234
+
205
235
  /// Find the path to the tishlang_runtime crate.
206
236
  ///
207
237
  /// Returns a canonical path string suitable for Cargo.toml path dependencies.
@@ -217,6 +247,24 @@ pub fn find_runtime_path() -> Result<String, String> {
217
247
  .map(|p| p.display().to_string().replace('\\', "/"))
218
248
  }
219
249
 
250
+ /// Resolve `tishlang_runtime` for a Cargo build, preferring the npm install under `project_root`.
251
+ ///
252
+ /// When a Tish app lives next to a checkout of the language repo (e.g. `…/tish/tish-cargo-example`),
253
+ /// [`find_workspace_root`] can return the checkout while `rustDependencies` point at
254
+ /// `node_modules/@tishlang/tish/crates/tish_core`. Using the npm tree for **both** runtime and shim
255
+ /// avoids Cargo lockfile "package collision" for the same crate name/version at two paths.
256
+ pub fn find_runtime_path_for_project(project_root: Option<&Path>) -> Result<String, String> {
257
+ if let Some(root) = project_root {
258
+ if let Some(rt) = npm_package_runtime_path(root) {
259
+ return rt
260
+ .canonicalize()
261
+ .map_err(|e| format!("Cannot canonicalize tishlang_runtime (npm): {}", e))
262
+ .map(|p| p.display().to_string().replace('\\', "/"));
263
+ }
264
+ }
265
+ find_runtime_path()
266
+ }
267
+
220
268
  /// Crate package name -> directory name (directories kept as tish_* for historical reasons).
221
269
  const CRATE_NAME_TO_DIR: &[(&str, &str)] = &[
222
270
  ("tishlang_runtime", "tish_runtime"),
@@ -243,16 +291,22 @@ pub fn find_crate_path(crate_name: &str) -> Result<PathBuf, String> {
243
291
 
244
292
  /// Create a temp build directory with src subdir.
245
293
  pub fn create_build_dir(prefix: &str, out_name: &str) -> Result<PathBuf, String> {
246
- let build_dir = std::env::temp_dir().join(prefix).join(format!("{}_{}", out_name, std::process::id()));
294
+ let build_dir =
295
+ std::env::temp_dir()
296
+ .join(prefix)
297
+ .join(format!("{}_{}", out_name, std::process::id()));
247
298
  fs::create_dir_all(&build_dir).map_err(|e| format!("Cannot create build dir: {}", e))?;
248
- fs::create_dir_all(build_dir.join("src")).map_err(|e| format!("Cannot create src dir: {}", e))?;
299
+ fs::create_dir_all(build_dir.join("src"))
300
+ .map_err(|e| format!("Cannot create src dir: {}", e))?;
249
301
  Ok(build_dir)
250
302
  }
251
303
 
252
304
  /// Run cargo build in the given directory.
253
305
  /// If target_dir is Some, use that for --target-dir (e.g. workspace target for caching).
254
306
  pub fn run_cargo_build(build_dir: &Path, target_dir: Option<&Path>) -> Result<(), String> {
255
- let target_dir = target_dir.map(|p| p.to_path_buf()).unwrap_or_else(|| build_dir.join("target"));
307
+ let target_dir = target_dir
308
+ .map(|p| p.to_path_buf())
309
+ .unwrap_or_else(|| build_dir.join("target"));
256
310
  let output = Command::new("cargo")
257
311
  .args(["build", "--release", "--target-dir"])
258
312
  .arg(&target_dir)
@@ -265,7 +319,10 @@ pub fn run_cargo_build(build_dir: &Path, target_dir: Option<&Path>) -> Result<()
265
319
  if !output.status.success() {
266
320
  let stderr = String::from_utf8_lossy(&output.stderr);
267
321
  let stdout = String::from_utf8_lossy(&output.stdout);
268
- return Err(format!("Compilation failed.\nstdout:\n{}\nstderr:\n{}", stdout, stderr));
322
+ return Err(format!(
323
+ "Compilation failed.\nstdout:\n{}\nstderr:\n{}",
324
+ stdout, stderr
325
+ ));
269
326
  }
270
327
  Ok(())
271
328
  }
@@ -318,7 +375,8 @@ pub fn copy_binary_to_output(binary: &Path, output_path: &Path) -> Result<(), St
318
375
  if let Some(parent) = output_path.parent() {
319
376
  fs::create_dir_all(parent).map_err(|e| format!("Cannot create output dir: {}", e))?;
320
377
  }
321
- fs::copy(binary, output_path).map_err(|e| format!("Cannot copy to {}: {}", output_path.display(), e))?;
378
+ fs::copy(binary, output_path)
379
+ .map_err(|e| format!("Cannot copy to {}: {}", output_path.display(), e))?;
322
380
  Ok(())
323
381
  }
324
382
 
@@ -1,9 +1,9 @@
1
1
  //! Array builtin methods.
2
2
 
3
+ use crate::helpers::normalize_index;
3
4
  use std::cell::RefCell;
4
5
  use std::rc::Rc;
5
6
  use tishlang_core::Value;
6
- use crate::helpers::normalize_index;
7
7
 
8
8
  /// Create a new array Value from a Vec of Values.
9
9
  pub fn from_vec(v: Vec<Value>) -> Value {
@@ -153,7 +153,11 @@ pub fn slice(arr: &Value, start: &Value, end: &Value) -> Value {
153
153
  let len = arr_borrow.len() as i64;
154
154
  let start_idx = normalize_index(start, len, 0);
155
155
  let end_idx = normalize_index(end, len, len as usize);
156
- let sliced = if start_idx < end_idx { arr_borrow[start_idx..end_idx].to_vec() } else { vec![] };
156
+ let sliced = if start_idx < end_idx {
157
+ arr_borrow[start_idx..end_idx].to_vec()
158
+ } else {
159
+ vec![]
160
+ };
157
161
  Value::Array(Rc::new(RefCell::new(sliced)))
158
162
  } else {
159
163
  Value::Null
@@ -188,7 +192,7 @@ pub fn flat(arr: &Value, depth: &Value) -> Value {
188
192
  result.push(v.clone());
189
193
  }
190
194
  }
191
-
195
+
192
196
  if let Value::Array(arr) = arr {
193
197
  let d = match depth {
194
198
  Value::Number(n) => *n as i32,
@@ -208,9 +212,11 @@ pub fn flat(arr: &Value, depth: &Value) -> Value {
208
212
  pub fn map(arr: &Value, callback: &Value) -> Value {
209
213
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
210
214
  let arr_borrow = arr.borrow();
211
- let result: Vec<Value> = arr_borrow.iter().enumerate().map(|(i, v)| {
212
- cb(&[v.clone(), Value::Number(i as f64)])
213
- }).collect();
215
+ let result: Vec<Value> = arr_borrow
216
+ .iter()
217
+ .enumerate()
218
+ .map(|(i, v)| cb(&[v.clone(), Value::Number(i as f64)]))
219
+ .collect();
214
220
  Value::Array(Rc::new(RefCell::new(result)))
215
221
  } else {
216
222
  Value::Null
@@ -220,10 +226,18 @@ pub fn map(arr: &Value, callback: &Value) -> Value {
220
226
  pub fn filter(arr: &Value, callback: &Value) -> Value {
221
227
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
222
228
  let arr_borrow = arr.borrow();
223
- let result: Vec<Value> = arr_borrow.iter().enumerate().filter_map(|(i, v)| {
224
- let keep = cb(&[v.clone(), Value::Number(i as f64)]);
225
- if keep.is_truthy() { Some(v.clone()) } else { None }
226
- }).collect();
229
+ let result: Vec<Value> = arr_borrow
230
+ .iter()
231
+ .enumerate()
232
+ .filter_map(|(i, v)| {
233
+ let keep = cb(&[v.clone(), Value::Number(i as f64)]);
234
+ if keep.is_truthy() {
235
+ Some(v.clone())
236
+ } else {
237
+ None
238
+ }
239
+ })
240
+ .collect();
227
241
  Value::Array(Rc::new(RefCell::new(result)))
228
242
  } else {
229
243
  Value::Null
@@ -234,9 +248,7 @@ pub fn reduce(arr: &Value, callback: &Value, initial: &Value) -> Value {
234
248
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
235
249
  let arr_borrow = arr.borrow();
236
250
  let len = arr_borrow.len();
237
- let (start_idx, mut acc) = if matches!(initial, Value::Null)
238
- && !arr_borrow.is_empty()
239
- {
251
+ let (start_idx, mut acc) = if matches!(initial, Value::Null) && !arr_borrow.is_empty() {
240
252
  // No initial value: use first element as acc, start from index 1
241
253
  (1, arr_borrow[0].clone())
242
254
  } else {
@@ -335,7 +347,9 @@ pub fn flat_map(arr: &Value, callback: &Value) -> Value {
335
347
  }
336
348
 
337
349
  fn sort_by_impl<F>(arr: &Value, cmp: F) -> Value
338
- where F: FnMut(&Value, &Value) -> std::cmp::Ordering {
350
+ where
351
+ F: FnMut(&Value, &Value) -> std::cmp::Ordering,
352
+ {
339
353
  if let Value::Array(arr) = arr {
340
354
  arr.borrow_mut().sort_by(cmp);
341
355
  Value::Array(Rc::clone(arr))
@@ -345,7 +359,9 @@ where F: FnMut(&Value, &Value) -> std::cmp::Ordering {
345
359
  }
346
360
 
347
361
  pub fn sort_default(arr: &Value) -> Value {
348
- sort_by_impl(arr, |a, b| a.to_display_string().cmp(&b.to_display_string()))
362
+ sort_by_impl(arr, |a, b| {
363
+ a.to_display_string().cmp(&b.to_display_string())
364
+ })
349
365
  }
350
366
 
351
367
  pub fn sort_with_comparator(arr: &Value, comparator: &Value) -> Value {
@@ -355,7 +371,7 @@ pub fn sort_with_comparator(arr: &Value, comparator: &Value) -> Value {
355
371
  let mut indices: Vec<usize> = (0..len).collect();
356
372
  let mut elements: Vec<Value> = std::mem::take(&mut *arr_mut);
357
373
  let mut args_buf: [Value; 2] = [Value::Null, Value::Null];
358
-
374
+
359
375
  indices.sort_by(|&a, &b| {
360
376
  args_buf[0] = elements[a].clone();
361
377
  args_buf[1] = elements[b].clone();
@@ -365,8 +381,11 @@ pub fn sort_with_comparator(arr: &Value, comparator: &Value) -> Value {
365
381
  _ => std::cmp::Ordering::Equal,
366
382
  }
367
383
  });
368
-
369
- *arr_mut = indices.into_iter().map(|i| std::mem::replace(&mut elements[i], Value::Null)).collect();
384
+
385
+ *arr_mut = indices
386
+ .into_iter()
387
+ .map(|i| std::mem::replace(&mut elements[i], Value::Null))
388
+ .collect();
370
389
  drop(arr_mut);
371
390
  Value::Array(Rc::clone(arr))
372
391
  } else {
@@ -380,7 +399,11 @@ fn num_cmp(a: &Value, b: &Value, asc: bool) -> std::cmp::Ordering {
380
399
  _ => (f64::NAN, f64::NAN),
381
400
  };
382
401
  let cmp = na.partial_cmp(&nb).unwrap_or(std::cmp::Ordering::Equal);
383
- if asc { cmp } else { cmp.reverse() }
402
+ if asc {
403
+ cmp
404
+ } else {
405
+ cmp.reverse()
406
+ }
384
407
  }
385
408
 
386
409
  pub fn sort_numeric_asc(arr: &Value) -> Value {
@@ -398,13 +421,21 @@ pub fn sort_by_property_numeric(arr: &Value, prop: &str, asc: bool) -> Value {
398
421
  let na = get_prop_number(a, &prop_arc);
399
422
  let nb = get_prop_number(b, &prop_arc);
400
423
  let cmp = na.partial_cmp(&nb).unwrap_or(std::cmp::Ordering::Equal);
401
- if asc { cmp } else { cmp.reverse() }
424
+ if asc {
425
+ cmp
426
+ } else {
427
+ cmp.reverse()
428
+ }
402
429
  })
403
430
  }
404
431
 
405
432
  fn get_prop_number(v: &Value, prop: &std::sync::Arc<str>) -> f64 {
406
433
  match v {
407
- Value::Object(o) => o.borrow().get(prop.as_ref()).map(|v| v.as_number().unwrap_or(f64::NAN)).unwrap_or(f64::NAN),
434
+ Value::Object(o) => o
435
+ .borrow()
436
+ .get(prop.as_ref())
437
+ .map(|v| v.as_number().unwrap_or(f64::NAN))
438
+ .unwrap_or(f64::NAN),
408
439
  _ => f64::NAN,
409
440
  }
410
441
  }
@@ -82,10 +82,7 @@ fn buffer_source_stub() -> Value {
82
82
  Arc::from("start"),
83
83
  Value::Function(Rc::new(|_| Value::Null)),
84
84
  );
85
- m.insert(
86
- Arc::from("stop"),
87
- Value::Function(Rc::new(|_| Value::Null)),
88
- );
85
+ m.insert(Arc::from("stop"), Value::Function(Rc::new(|_| Value::Null)));
89
86
  Value::Object(Rc::new(RefCell::new(m)))
90
87
  }
91
88
 
@@ -98,10 +95,7 @@ fn oscillator_stub() -> Value {
98
95
  Arc::from("start"),
99
96
  Value::Function(Rc::new(|_| Value::Null)),
100
97
  );
101
- m.insert(
102
- Arc::from("stop"),
103
- Value::Function(Rc::new(|_| Value::Null)),
104
- );
98
+ m.insert(Arc::from("stop"), Value::Function(Rc::new(|_| Value::Null)));
105
99
  Value::Object(Rc::new(RefCell::new(m)))
106
100
  }
107
101
 
@@ -16,29 +16,35 @@ pub fn boolean(args: &[Value]) -> Value {
16
16
 
17
17
  /// decodeURI(str)
18
18
  pub fn decode_uri(args: &[Value]) -> Value {
19
- let s = args.first().map(Value::to_display_string).unwrap_or_default();
19
+ let s = args
20
+ .first()
21
+ .map(Value::to_display_string)
22
+ .unwrap_or_default();
20
23
  Value::String(percent_decode(&s).unwrap_or(s).into())
21
24
  }
22
25
 
23
26
  /// encodeURI(str)
24
27
  pub fn encode_uri(args: &[Value]) -> Value {
25
- let s = args.first().map(Value::to_display_string).unwrap_or_default();
28
+ let s = args
29
+ .first()
30
+ .map(Value::to_display_string)
31
+ .unwrap_or_default();
26
32
  Value::String(percent_encode(&s).into())
27
33
  }
28
34
 
29
35
  /// isFinite(value)
30
36
  pub fn is_finite(args: &[Value]) -> Value {
31
- Value::Bool(args.first().is_some_and(|v| matches!(v, Value::Number(n) if n.is_finite())))
37
+ Value::Bool(
38
+ args.first()
39
+ .is_some_and(|v| matches!(v, Value::Number(n) if n.is_finite())),
40
+ )
32
41
  }
33
42
 
34
43
  /// isNaN(value)
35
44
  pub fn is_nan(args: &[Value]) -> Value {
36
- Value::Bool(
37
- args.first().is_none_or(|v| {
38
- matches!(v, Value::Number(n) if n.is_nan())
39
- || !matches!(v, Value::Number(_))
40
- }),
41
- )
45
+ Value::Bool(args.first().is_none_or(|v| {
46
+ matches!(v, Value::Number(n) if n.is_nan()) || !matches!(v, Value::Number(_))
47
+ }))
42
48
  }
43
49
 
44
50
  /// Array.isArray(value)
@@ -144,12 +150,18 @@ pub fn object_assign(args: &[Value]) -> Value {
144
150
 
145
151
  /// parseInt(string, radix?)
146
152
  pub fn parse_int(args: &[Value]) -> Value {
147
- let s = args.first().map(Value::to_display_string).unwrap_or_default();
153
+ let s = args
154
+ .first()
155
+ .map(Value::to_display_string)
156
+ .unwrap_or_default();
148
157
  let s = s.trim();
149
- let radix = args.get(1).and_then(|v| match v {
150
- Value::Number(n) => Some(*n as i32),
151
- _ => None,
152
- }).unwrap_or(10);
158
+ let radix = args
159
+ .get(1)
160
+ .and_then(|v| match v {
161
+ Value::Number(n) => Some(*n as i32),
162
+ _ => None,
163
+ })
164
+ .unwrap_or(10);
153
165
 
154
166
  if (2..=36).contains(&radix) {
155
167
  let prefix: String = s
@@ -165,7 +177,10 @@ pub fn parse_int(args: &[Value]) -> Value {
165
177
 
166
178
  /// parseFloat(string)
167
179
  pub fn parse_float(args: &[Value]) -> Value {
168
- let s = args.first().map(Value::to_display_string).unwrap_or_default();
180
+ let s = args
181
+ .first()
182
+ .map(Value::to_display_string)
183
+ .unwrap_or_default();
169
184
  Value::Number(s.trim().parse().unwrap_or(f64::NAN))
170
185
  }
171
186
 
@@ -5,11 +5,11 @@
5
5
  //! and native signatures.
6
6
 
7
7
  pub mod array;
8
- pub mod string;
9
- pub mod object;
10
- pub mod math;
11
- pub mod helpers;
12
- pub mod globals;
13
8
  pub mod construct;
9
+ pub mod globals;
10
+ pub mod helpers;
11
+ pub mod math;
12
+ pub mod object;
13
+ pub mod string;
14
14
 
15
15
  pub use tishlang_core::Value;
@@ -1,7 +1,7 @@
1
1
  //! Math builtin functions.
2
2
 
3
- use tishlang_core::Value;
4
3
  use crate::helpers::extract_num;
4
+ use tishlang_core::Value;
5
5
 
6
6
  macro_rules! math_unary {
7
7
  ($name:ident, $op:ident) => {
@@ -31,14 +31,16 @@ math_unary!(trunc, trunc);
31
31
  math_unary!(cbrt, cbrt);
32
32
 
33
33
  pub fn min(args: &[Value]) -> Value {
34
- let n = args.iter()
34
+ let n = args
35
+ .iter()
35
36
  .filter_map(|v| extract_num(Some(v)))
36
37
  .fold(f64::INFINITY, f64::min);
37
38
  Value::Number(if n == f64::INFINITY { f64::NAN } else { n })
38
39
  }
39
40
 
40
41
  pub fn max(args: &[Value]) -> Value {
41
- let n = args.iter()
42
+ let n = args
43
+ .iter()
42
44
  .filter_map(|v| extract_num(Some(v)))
43
45
  .fold(f64::NEG_INFINITY, f64::max);
44
46
  Value::Number(if n == f64::NEG_INFINITY { f64::NAN } else { n })
@@ -3,11 +3,11 @@
3
3
  //! All indices use character (Unicode scalar) positions for consistency with
4
4
  //! JavaScript, matching .length and .charAt(). Byte offsets are never exposed.
5
5
 
6
+ use crate::helpers::normalize_index;
6
7
  use std::cell::RefCell;
7
8
  use std::rc::Rc;
8
9
  use std::sync::Arc;
9
10
  use tishlang_core::Value;
10
- use crate::helpers::normalize_index;
11
11
 
12
12
  /// Byte offset -> character index.
13
13
  fn byte_to_char_index(s: &str, byte_offset: usize) -> usize {
@@ -152,7 +152,10 @@ pub fn slice(s: &Value, start: &Value, end: &Value) -> Value {
152
152
  if let Value::String(s) = s {
153
153
  let chars: Vec<char> = s.chars().collect();
154
154
  let len = chars.len() as i64;
155
- let (si, ei) = (normalize_index(start, len, 0), normalize_index(end, len, len as usize));
155
+ let (si, ei) = (
156
+ normalize_index(start, len, 0),
157
+ normalize_index(end, len, len as usize),
158
+ );
156
159
  let result: String = if si < ei {
157
160
  chars[si..ei].iter().collect()
158
161
  } else {
@@ -193,7 +196,8 @@ pub fn split(s: &Value, sep: &Value) -> Value {
193
196
  Value::String(ss) => ss.as_ref(),
194
197
  _ => return Value::Array(Rc::new(RefCell::new(vec![Value::String(Arc::clone(s))]))),
195
198
  };
196
- let parts: Vec<Value> = s.split(separator)
199
+ let parts: Vec<Value> = s
200
+ .split(separator)
197
201
  .map(|p| Value::String(p.into()))
198
202
  .collect();
199
203
  Value::Array(Rc::new(RefCell::new(parts)))
@@ -244,9 +248,19 @@ pub fn ends_with(s: &Value, search: &Value) -> Value {
244
248
 
245
249
  fn replace_impl(s: &Value, search: &Value, replacement: &Value, all: bool) -> Value {
246
250
  if let Value::String(s) = s {
247
- let search_str = match search { Value::String(ss) => ss.as_ref(), _ => return Value::String(Arc::clone(s)) };
248
- let repl_str = match replacement { Value::String(ss) => ss.as_ref(), _ => "" };
249
- let result = if all { s.replace(search_str, repl_str) } else { s.replacen(search_str, repl_str, 1) };
251
+ let search_str = match search {
252
+ Value::String(ss) => ss.as_ref(),
253
+ _ => return Value::String(Arc::clone(s)),
254
+ };
255
+ let repl_str = match replacement {
256
+ Value::String(ss) => ss.as_ref(),
257
+ _ => "",
258
+ };
259
+ let result = if all {
260
+ s.replace(search_str, repl_str)
261
+ } else {
262
+ s.replacen(search_str, repl_str, 1)
263
+ };
250
264
  Value::String(result.into())
251
265
  } else {
252
266
  Value::Null
@@ -267,8 +281,13 @@ fn char_at_idx(s: &str, idx: usize) -> Option<char> {
267
281
 
268
282
  pub fn char_at(s: &Value, idx: &Value) -> Value {
269
283
  if let Value::String(s) = s {
270
- let idx = match idx { Value::Number(n) => *n as usize, _ => 0 };
271
- char_at_idx(s, idx).map(|c| Value::String(c.to_string().into())).unwrap_or(Value::String("".into()))
284
+ let idx = match idx {
285
+ Value::Number(n) => *n as usize,
286
+ _ => 0,
287
+ };
288
+ char_at_idx(s, idx)
289
+ .map(|c| Value::String(c.to_string().into()))
290
+ .unwrap_or(Value::String("".into()))
272
291
  } else {
273
292
  Value::Null
274
293
  }
@@ -276,8 +295,13 @@ pub fn char_at(s: &Value, idx: &Value) -> Value {
276
295
 
277
296
  pub fn char_code_at(s: &Value, idx: &Value) -> Value {
278
297
  if let Value::String(s) = s {
279
- let idx = match idx { Value::Number(n) => *n as usize, _ => 0 };
280
- char_at_idx(s, idx).map(|c| Value::Number(c as u32 as f64)).unwrap_or(Value::Number(f64::NAN))
298
+ let idx = match idx {
299
+ Value::Number(n) => *n as usize,
300
+ _ => 0,
301
+ };
302
+ char_at_idx(s, idx)
303
+ .map(|c| Value::Number(c as u32 as f64))
304
+ .unwrap_or(Value::Number(f64::NAN))
281
305
  } else {
282
306
  Value::Null
283
307
  }
@@ -311,7 +335,11 @@ fn pad_impl(s: &Value, target_len: &Value, pad: &Value, at_start: bool) -> Value
311
335
  }
312
336
  let needed = target_len - char_count;
313
337
  let padding: String = pad_str.chars().cycle().take(needed).collect();
314
- let result = if at_start { format!("{}{}", padding, s) } else { format!("{}{}", s, padding) };
338
+ let result = if at_start {
339
+ format!("{}{}", padding, s)
340
+ } else {
341
+ format!("{}{}", s, padding)
342
+ };
315
343
  Value::String(result.into())
316
344
  } else {
317
345
  Value::Null
@@ -382,16 +410,31 @@ mod tests {
382
410
  fn includes_basic() {
383
411
  assert_same!(includes(&s("hello"), &s("ll"), None), Value::Bool(true));
384
412
  assert_same!(includes(&s("hello"), &s("x"), None), Value::Bool(false));
385
- assert_same!(includes(&s("hello"), &s("l"), Some(&n(3.0))), Value::Bool(true));
386
- assert_same!(includes(&s("hello"), &s("l"), Some(&n(4.0))), Value::Bool(false));
413
+ assert_same!(
414
+ includes(&s("hello"), &s("l"), Some(&n(3.0))),
415
+ Value::Bool(true)
416
+ );
417
+ assert_same!(
418
+ includes(&s("hello"), &s("l"), Some(&n(4.0))),
419
+ Value::Bool(false)
420
+ );
387
421
  }
388
422
 
389
423
  #[test]
390
424
  fn includes_negative_from() {
391
- assert_same!(includes(&s("hello"), &s("o"), Some(&n(-1.0))), Value::Bool(true));
392
- assert_same!(includes(&s("hello"), &s("h"), Some(&n(-5.0))), Value::Bool(true));
425
+ assert_same!(
426
+ includes(&s("hello"), &s("o"), Some(&n(-1.0))),
427
+ Value::Bool(true)
428
+ );
429
+ assert_same!(
430
+ includes(&s("hello"), &s("h"), Some(&n(-5.0))),
431
+ Value::Bool(true)
432
+ );
393
433
  // fromIndex -1 → start at len-1 = 1 ("i" only), "h" not found
394
- assert_same!(includes(&s("hi"), &s("h"), Some(&n(-1.0))), Value::Bool(false));
434
+ assert_same!(
435
+ includes(&s("hi"), &s("h"), Some(&n(-1.0))),
436
+ Value::Bool(false)
437
+ );
395
438
  }
396
439
 
397
440
  #[test]
@@ -466,7 +509,10 @@ mod tests {
466
509
 
467
510
  #[test]
468
511
  fn last_index_of_basic() {
469
- assert_same!(last_index_of(&s("abcabc"), &s("a"), &n(f64::INFINITY)), n(3.0));
512
+ assert_same!(
513
+ last_index_of(&s("abcabc"), &s("a"), &n(f64::INFINITY)),
514
+ n(3.0)
515
+ );
470
516
  assert_same!(last_index_of(&s("abcabc"), &s("a"), &n(2.0)), n(0.0));
471
517
  assert_same!(last_index_of(&s("hello"), &s("l"), &n(3.0)), n(3.0));
472
518
  assert_same!(last_index_of(&s("hello"), &s("l"), &n(1.0)), n(-1.0));
@@ -490,8 +536,14 @@ mod tests {
490
536
 
491
537
  #[test]
492
538
  fn last_index_of_unicode() {
493
- assert_same!(last_index_of(&s("😀a😀"), &s("a"), &n(f64::INFINITY)), n(1.0));
494
- assert_same!(last_index_of(&s("😀a😀"), &s("😀"), &n(f64::INFINITY)), n(2.0));
539
+ assert_same!(
540
+ last_index_of(&s("😀a😀"), &s("a"), &n(f64::INFINITY)),
541
+ n(1.0)
542
+ );
543
+ assert_same!(
544
+ last_index_of(&s("😀a😀"), &s("😀"), &n(f64::INFINITY)),
545
+ n(2.0)
546
+ );
495
547
  }
496
548
 
497
549
  #[test]
@@ -30,7 +30,6 @@ impl Constant {
30
30
  }
31
31
  }
32
32
 
33
-
34
33
  /// A bytecode chunk: instructions and associated data.
35
34
  #[derive(Debug, Clone)]
36
35
  pub struct Chunk {