@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.
Files changed (99) hide show
  1. package/Cargo.toml +2 -0
  2. package/README.md +2 -0
  3. package/bin/tish +0 -0
  4. package/crates/js_to_tish/src/transform/expr.rs +28 -8
  5. package/crates/js_to_tish/src/transform/stmt.rs +49 -22
  6. package/crates/tish/Cargo.toml +14 -5
  7. package/crates/tish/src/cargo_native_registry.rs +29 -0
  8. package/crates/tish/src/cli_help.rs +16 -10
  9. package/crates/tish/src/main.rs +87 -32
  10. package/crates/tish/src/repl_completion.rs +3 -3
  11. package/crates/tish/tests/cargo_example_compile.rs +1 -1
  12. package/crates/tish/tests/integration_test.rs +19 -7
  13. package/crates/tish/tests/shortcircuit.rs +1 -1
  14. package/crates/tish_ast/src/ast.rs +80 -9
  15. package/crates/tish_build_utils/Cargo.toml +4 -0
  16. package/crates/tish_build_utils/src/lib.rs +105 -2
  17. package/crates/tish_builtins/Cargo.toml +5 -1
  18. package/crates/tish_builtins/src/array.rs +13 -12
  19. package/crates/tish_builtins/src/construct.rs +34 -33
  20. package/crates/tish_builtins/src/globals.rs +12 -11
  21. package/crates/tish_builtins/src/helpers.rs +2 -1
  22. package/crates/tish_builtins/src/object.rs +3 -2
  23. package/crates/tish_builtins/src/string.rs +73 -3
  24. package/crates/tish_bytecode/src/compiler.rs +12 -14
  25. package/crates/tish_bytecode/src/opcode.rs +12 -3
  26. package/crates/tish_compile/Cargo.toml +1 -0
  27. package/crates/tish_compile/src/codegen.rs +745 -199
  28. package/crates/tish_compile/src/infer.rs +6 -0
  29. package/crates/tish_compile/src/lib.rs +4 -3
  30. package/crates/tish_compile/src/resolve.rs +180 -82
  31. package/crates/tish_compile/src/types.rs +175 -11
  32. package/crates/tish_compile_js/Cargo.toml +1 -0
  33. package/crates/tish_compile_js/src/codegen.rs +152 -29
  34. package/crates/tish_compile_js/src/lib.rs +3 -1
  35. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +31 -12
  36. package/crates/tish_core/Cargo.toml +8 -0
  37. package/crates/tish_core/src/json.rs +102 -53
  38. package/crates/tish_core/src/lib.rs +3 -1
  39. package/crates/tish_core/src/macros.rs +5 -5
  40. package/crates/tish_core/src/value.rs +53 -15
  41. package/crates/tish_core/src/vmref.rs +178 -0
  42. package/crates/tish_eval/Cargo.toml +17 -2
  43. package/crates/tish_eval/src/eval.rs +90 -28
  44. package/crates/tish_eval/src/http.rs +61 -0
  45. package/crates/tish_eval/src/lib.rs +3 -3
  46. package/crates/tish_eval/src/natives.rs +41 -0
  47. package/crates/tish_eval/src/value.rs +7 -3
  48. package/crates/tish_eval/src/value_convert.rs +13 -5
  49. package/crates/tish_fmt/src/lib.rs +120 -30
  50. package/crates/tish_lexer/src/lib.rs +20 -5
  51. package/crates/tish_lexer/src/token.rs +4 -0
  52. package/crates/tish_llvm/src/lib.rs +3 -1
  53. package/crates/tish_lsp/Cargo.toml +4 -1
  54. package/crates/tish_lsp/README.md +1 -1
  55. package/crates/tish_lsp/src/builtin_goto.rs +261 -0
  56. package/crates/tish_lsp/src/import_goto.rs +549 -0
  57. package/crates/tish_lsp/src/main.rs +502 -102
  58. package/crates/tish_native/src/build.rs +3 -2
  59. package/crates/tish_native/src/lib.rs +6 -2
  60. package/crates/tish_opt/src/lib.rs +17 -2
  61. package/crates/tish_parser/src/lib.rs +10 -3
  62. package/crates/tish_parser/src/parser.rs +346 -56
  63. package/crates/tish_pg/Cargo.toml +34 -0
  64. package/crates/tish_pg/README.md +38 -0
  65. package/crates/tish_pg/src/error.rs +52 -0
  66. package/crates/tish_pg/src/lib.rs +967 -0
  67. package/crates/tish_resolve/Cargo.toml +13 -0
  68. package/crates/tish_resolve/src/lib.rs +3436 -0
  69. package/crates/tish_resolve/src/pos.rs +133 -0
  70. package/crates/tish_runtime/Cargo.toml +68 -3
  71. package/crates/tish_runtime/src/http.rs +1123 -141
  72. package/crates/tish_runtime/src/http_fetch.rs +15 -14
  73. package/crates/tish_runtime/src/http_hyper.rs +418 -0
  74. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  75. package/crates/tish_runtime/src/lib.rs +159 -29
  76. package/crates/tish_runtime/src/promise.rs +199 -36
  77. package/crates/tish_runtime/src/promise_io.rs +2 -1
  78. package/crates/tish_runtime/src/timers.rs +37 -1
  79. package/crates/tish_runtime/src/ws.rs +26 -28
  80. package/crates/tish_ui/src/jsx.rs +279 -8
  81. package/crates/tish_ui/src/lib.rs +5 -2
  82. package/crates/tish_ui/src/runtime/hooks.rs +406 -45
  83. package/crates/tish_ui/src/runtime/mod.rs +36 -9
  84. package/crates/tish_vm/Cargo.toml +15 -5
  85. package/crates/tish_vm/src/vm.rs +506 -259
  86. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +3 -1
  87. package/crates/tish_wasm/src/lib.rs +17 -14
  88. package/crates/tish_wasm_runtime/Cargo.toml +2 -1
  89. package/crates/tish_wasm_runtime/src/lib.rs +1 -1
  90. package/crates/tishlang_cargo_bindgen/Cargo.toml +1 -0
  91. package/crates/tishlang_cargo_bindgen/src/discover.rs +68 -0
  92. package/crates/tishlang_cargo_bindgen/src/lib.rs +5 -4
  93. package/justfile +8 -0
  94. package/package.json +1 -1
  95. package/platform/darwin-arm64/tish +0 -0
  96. package/platform/darwin-x64/tish +0 -0
  97. package/platform/linux-arm64/tish +0 -0
  98. package/platform/linux-x64/tish +0 -0
  99. package/platform/win32-x64/tish.exe +0 -0
@@ -17,6 +17,7 @@ pub struct VirtualModule {
17
17
  const BUILTIN_ALIASES: &[(&str, &str)] = &[
18
18
  ("fs", "tish:fs"),
19
19
  ("http", "tish:http"),
20
+ ("timers", "tish:timers"),
20
21
  ("process", "tish:process"),
21
22
  ("ws", "tish:ws"),
22
23
  ];
@@ -35,7 +36,7 @@ fn is_native_import(spec: &str) -> bool {
35
36
  spec.starts_with("tish:")
36
37
  || spec.starts_with("cargo:")
37
38
  || spec.starts_with('@')
38
- || matches!(spec, "fs" | "http" | "process" | "ws")
39
+ || matches!(spec, "fs" | "http" | "timers" | "process" | "ws")
39
40
  }
40
41
 
41
42
  /// Normalize a virtual path: resolve . and .. components.
@@ -311,8 +312,14 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
311
312
  normalize_builtin_spec(from).unwrap_or_else(|| from.to_string());
312
313
  for spec in specifiers {
313
314
  match spec {
314
- ImportSpecifier::Named { name, alias } => {
315
+ ImportSpecifier::Named {
316
+ name,
317
+ name_span,
318
+ alias,
319
+ alias_span,
320
+ } => {
315
321
  let bind = alias.as_deref().unwrap_or(name.as_ref());
322
+ let decl_name_span = alias_span.as_ref().unwrap_or(name_span);
316
323
  let init = Expr::NativeModuleLoad {
317
324
  spec: Arc::from(canonical_spec.clone()),
318
325
  export_name: name.clone(),
@@ -320,23 +327,24 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
320
327
  };
321
328
  statements.push(Statement::VarDecl {
322
329
  name: Arc::from(bind),
330
+ name_span: *decl_name_span,
323
331
  mutable: false,
324
332
  type_ann: None,
325
333
  init: Some(init),
326
334
  span: *span,
327
335
  });
328
336
  }
329
- ImportSpecifier::Namespace(ns) => {
337
+ ImportSpecifier::Namespace { name, .. } => {
330
338
  return Err(format!(
331
339
  "Namespace import (* as {}) not supported for native module '{}'",
332
- ns.as_ref(),
340
+ name.as_ref(),
333
341
  from.as_ref()
334
342
  ));
335
343
  }
336
- ImportSpecifier::Default(bind) => {
344
+ ImportSpecifier::Default { name, .. } => {
337
345
  return Err(format!(
338
346
  "Default import '{}' not supported for native module '{}'. Use named import.",
339
- bind.as_ref(),
347
+ name.as_ref(),
340
348
  from.as_ref()
341
349
  ));
342
350
  }
@@ -351,15 +359,22 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
351
359
  let dep_exports = &module_exports[dep_idx];
352
360
  for spec in specifiers {
353
361
  match spec {
354
- ImportSpecifier::Named { name, alias } => {
362
+ ImportSpecifier::Named {
363
+ name,
364
+ name_span,
365
+ alias,
366
+ alias_span,
367
+ } => {
355
368
  let source = dep_exports
356
369
  .get(name.as_ref())
357
370
  .cloned()
358
371
  .unwrap_or_else(|| name.to_string());
359
372
  let bind = alias.as_deref().unwrap_or(name.as_ref());
360
373
  if bind != source {
374
+ let decl_name_span = alias_span.as_ref().unwrap_or(name_span);
361
375
  statements.push(Statement::VarDecl {
362
376
  name: Arc::from(bind),
377
+ name_span: *decl_name_span,
363
378
  mutable: false,
364
379
  type_ann: None,
365
380
  init: Some(Expr::Ident {
@@ -370,7 +385,7 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
370
385
  });
371
386
  }
372
387
  }
373
- ImportSpecifier::Namespace(ns) => {
388
+ ImportSpecifier::Namespace { name, name_span } => {
374
389
  let mut props = Vec::new();
375
390
  for (k, v) in dep_exports {
376
391
  props.push(tishlang_ast::ObjectProp::KeyValue(
@@ -382,20 +397,22 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
382
397
  ));
383
398
  }
384
399
  statements.push(Statement::VarDecl {
385
- name: ns.clone(),
400
+ name: name.clone(),
401
+ name_span: *name_span,
386
402
  mutable: false,
387
403
  type_ann: None,
388
404
  init: Some(Expr::Object { props, span: *span }),
389
405
  span: *span,
390
406
  });
391
407
  }
392
- ImportSpecifier::Default(bind) => {
408
+ ImportSpecifier::Default { name, name_span } => {
393
409
  let source =
394
410
  dep_exports.get("default").cloned().ok_or_else(|| {
395
411
  format!("Module '{}' has no default export", from)
396
412
  })?;
397
413
  statements.push(Statement::VarDecl {
398
- name: bind.clone(),
414
+ name: name.clone(),
415
+ name_span: *name_span,
399
416
  mutable: false,
400
417
  type_ann: None,
401
418
  init: Some(Expr::Ident {
@@ -412,12 +429,14 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
412
429
  ExportDeclaration::Named(s) => statements.push(*s.clone()),
413
430
  ExportDeclaration::Default(e) => {
414
431
  let default_name = format!("__default_{}", idx);
432
+ let espan = e.span();
415
433
  statements.push(Statement::VarDecl {
416
434
  name: Arc::from(default_name),
435
+ name_span: espan,
417
436
  mutable: false,
418
437
  type_ann: None,
419
438
  init: Some((*e).clone()),
420
- span: e.span(),
439
+ span: espan,
421
440
  });
422
441
  }
423
442
  },
@@ -9,6 +9,14 @@ repository = { workspace = true }
9
9
  [features]
10
10
  default = []
11
11
  regex = ["dep:fancy-regex"]
12
+ # Make `Value` (and its array / object / regex payloads) `Send + Sync` by
13
+ # switching the interior `Rc<RefCell<T>>` to `Arc<Mutex<T>>` and the native
14
+ # function type from `Rc<dyn Fn>` to `Arc<dyn Fn + Send + Sync>`. Enabled
15
+ # transitively by any crate that needs to pass `Value`s across threads —
16
+ # most notably `tishlang_runtime/http`, which dispatches HTTP handlers
17
+ # across a worker pool. Off by default so wasm / wasi / cranelift / llvm /
18
+ # interpreter builds pay no atomic-ref-count or mutex overhead.
19
+ send-values = []
12
20
 
13
21
  [dependencies]
14
22
  ahash = "0.8.11"
@@ -1,8 +1,6 @@
1
1
  //! JSON parsing and stringification for Tish values.
2
2
 
3
- use crate::Value;
4
- use std::cell::RefCell;
5
- use std::rc::Rc;
3
+ use crate::{Value, VmRef};
6
4
  use std::sync::Arc;
7
5
 
8
6
  /// Parse JSON string into a Value.
@@ -19,75 +17,126 @@ pub fn json_parse(json: &str) -> Result<Value, String> {
19
17
  }
20
18
 
21
19
  /// Stringify a Value to JSON.
20
+ ///
21
+ /// Single-buffer write strategy: all nested values append into one
22
+ /// `String` via [`json_stringify_into`], so we never allocate a transient
23
+ /// per-node `String` only to copy + drop it on the way back up. For a
24
+ /// 20-row TFB `/queries` response (~40 numbers, 2 keys × 20 = ~80 string
25
+ /// ops) that saves dozens of small allocations per request.
22
26
  pub fn json_stringify(value: &Value) -> String {
27
+ // 256 B is enough for typical TFB responses (`/db` is 31 B,
28
+ // `/queries=20` is ~700 B). Larger payloads reallocate normally.
29
+ let mut buf = String::with_capacity(256);
30
+ json_stringify_into(&mut buf, value);
31
+ buf
32
+ }
33
+
34
+ /// Append a JSON-stringified `value` to `buf`. Used by JSON.stringify for
35
+ /// the recursive case so we don't pay for an intermediate `String` per
36
+ /// node.
37
+ pub fn json_stringify_into(buf: &mut String, value: &Value) {
23
38
  match value {
24
- Value::Null => "null".to_string(),
25
- Value::Bool(b) => b.to_string(),
39
+ Value::Null => buf.push_str("null"),
40
+ Value::Bool(true) => buf.push_str("true"),
41
+ Value::Bool(false) => buf.push_str("false"),
26
42
  Value::Number(n) => {
27
43
  if n.is_nan() || n.is_infinite() {
28
- "null".to_string()
44
+ buf.push_str("null");
29
45
  } else {
30
- n.to_string()
46
+ // `write!` avoids the heap allocation that `to_string`
47
+ // produces. The f64 → decimal formatter is the same
48
+ // either way (`std::fmt::Display`).
49
+ use std::fmt::Write;
50
+ let _ = write!(buf, "{}", n);
31
51
  }
32
52
  }
33
- Value::String(s) => format!("\"{}\"", escape_json_string(s)),
53
+ Value::String(s) => {
54
+ buf.push('"');
55
+ escape_json_string_into(buf, s);
56
+ buf.push('"');
57
+ }
34
58
  Value::Array(arr) => {
35
59
  let borrowed = arr.borrow();
36
- let mut result = String::with_capacity(borrowed.len() * 8 + 2);
37
- result.push('[');
38
- let mut first = true;
39
- for item in borrowed.iter() {
40
- if !first {
41
- result.push(',');
60
+ buf.push('[');
61
+ for (i, item) in borrowed.iter().enumerate() {
62
+ if i > 0 {
63
+ buf.push(',');
42
64
  }
43
- first = false;
44
- result.push_str(&json_stringify(item));
65
+ json_stringify_into(buf, item);
45
66
  }
46
- result.push(']');
47
- result
67
+ buf.push(']');
48
68
  }
49
69
  Value::Object(obj) => {
50
70
  let borrowed = obj.borrow();
51
- let mut keys: Vec<_> = borrowed.keys().collect();
52
- keys.sort();
53
- let mut result = String::with_capacity(borrowed.len() * 16 + 2);
54
- result.push('{');
55
- let mut first = true;
56
- for key in keys {
57
- if !first {
58
- result.push(',');
71
+ // Sort keys for deterministic output. Pre-allocate to avoid
72
+ // a fresh `Vec` realloc inside `keys().collect()`.
73
+ let mut keys: Vec<&Arc<str>> = Vec::with_capacity(borrowed.len());
74
+ keys.extend(borrowed.keys());
75
+ keys.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
76
+ buf.push('{');
77
+ for (i, key) in keys.into_iter().enumerate() {
78
+ if i > 0 {
79
+ buf.push(',');
59
80
  }
60
- first = false;
61
- result.push('"');
62
- result.push_str(&escape_json_string(key));
63
- result.push_str("\":");
64
- result.push_str(&json_stringify(borrowed.get(key).unwrap()));
81
+ buf.push('"');
82
+ escape_json_string_into(buf, key);
83
+ buf.push_str("\":");
84
+ json_stringify_into(buf, borrowed.get(key).unwrap());
65
85
  }
66
- result.push('}');
67
- result
86
+ buf.push('}');
68
87
  }
69
- Value::Function(_) => "null".to_string(),
70
- Value::Promise(_) => "null".to_string(),
71
- Value::Opaque(_) => "null".to_string(),
88
+ Value::Function(_) | Value::Promise(_) | Value::Opaque(_) => buf.push_str("null"),
72
89
  #[cfg(feature = "regex")]
73
- Value::RegExp(_) => "null".to_string(),
90
+ Value::RegExp(_) => buf.push_str("null"),
74
91
  }
75
92
  }
76
93
 
77
- fn escape_json_string(s: &str) -> String {
78
- let mut result = String::with_capacity(s.len());
79
- for c in s.chars() {
80
- match c {
81
- '"' => result.push_str("\\\""),
82
- '\\' => result.push_str("\\\\"),
83
- '\n' => result.push_str("\\n"),
84
- '\r' => result.push_str("\\r"),
85
- '\t' => result.push_str("\\t"),
86
- c if c.is_control() => result.push_str(&format!("\\u{:04x}", c as u32)),
87
- c => result.push(c),
94
+ /// Append an escaped JSON string body (without the surrounding quotes)
95
+ /// to `buf`. Optimised for the common case where the input is ASCII and
96
+ /// contains no characters that need escaping — we fast-pass the bytes
97
+ /// straight through, only falling into the per-char path on a hit.
98
+ fn escape_json_string_into(buf: &mut String, s: &str) {
99
+ let bytes = s.as_bytes();
100
+ let mut start = 0usize;
101
+ for (i, &b) in bytes.iter().enumerate() {
102
+ // Anything < 0x20 is a JSON control char that must be escaped;
103
+ // 0x22 (`"`) and 0x5C (`\`) also need an explicit escape; bytes
104
+ // 0x80 are the start of a multi-byte UTF-8 sequence, which is
105
+ // valid JSON as-is.
106
+ if b < 0x20 || b == b'"' || b == b'\\' {
107
+ // Flush the run of clean bytes before this one in one push.
108
+ if start < i {
109
+ // SAFETY: `s` is `&str`, every byte in `start..i` was a
110
+ // single-byte ASCII char (we only stop on ASCII triggers
111
+ // below 0x80), so the slice is a valid `&str`.
112
+ buf.push_str(&s[start..i]);
113
+ }
114
+ match b {
115
+ b'"' => buf.push_str("\\\""),
116
+ b'\\' => buf.push_str("\\\\"),
117
+ b'\n' => buf.push_str("\\n"),
118
+ b'\r' => buf.push_str("\\r"),
119
+ b'\t' => buf.push_str("\\t"),
120
+ b'\x08' => buf.push_str("\\b"),
121
+ b'\x0c' => buf.push_str("\\f"),
122
+ _ => {
123
+ use std::fmt::Write;
124
+ let _ = write!(buf, "\\u{:04x}", b as u32);
125
+ }
126
+ }
127
+ start = i + 1;
88
128
  }
89
129
  }
90
- result
130
+ if start < bytes.len() {
131
+ buf.push_str(&s[start..]);
132
+ }
133
+ }
134
+
135
+ #[allow(dead_code)]
136
+ fn escape_json_string(s: &str) -> String {
137
+ let mut buf = String::with_capacity(s.len());
138
+ escape_json_string_into(&mut buf, s);
139
+ buf
91
140
  }
92
141
 
93
142
  fn parse_value(input: &str) -> Result<(Value, &str), String> {
@@ -241,7 +290,7 @@ fn parse_array(input: &str) -> Result<(Value, &str), String> {
241
290
 
242
291
  input = input.trim_start();
243
292
  if let Some(rest) = input.strip_prefix(']') {
244
- return Ok((Value::Array(Rc::new(RefCell::new(items))), rest));
293
+ return Ok((Value::Array(VmRef::new(items)), rest));
245
294
  }
246
295
 
247
296
  loop {
@@ -251,7 +300,7 @@ fn parse_array(input: &str) -> Result<(Value, &str), String> {
251
300
 
252
301
  match input.chars().next() {
253
302
  Some(',') => input = &input[1..],
254
- Some(']') => return Ok((Value::Array(Rc::new(RefCell::new(items))), &input[1..])),
303
+ Some(']') => return Ok((Value::Array(VmRef::new(items)), &input[1..])),
255
304
  _ => return Err("Expected ',' or ']' in array".to_string()),
256
305
  }
257
306
  }
@@ -263,7 +312,7 @@ fn parse_object(input: &str) -> Result<(Value, &str), String> {
263
312
 
264
313
  input = input.trim_start();
265
314
  if let Some(rest) = input.strip_prefix('}') {
266
- return Ok((Value::Object(Rc::new(RefCell::new(map))), rest));
315
+ return Ok((Value::Object(VmRef::new(map)), rest));
267
316
  }
268
317
 
269
318
  loop {
@@ -290,7 +339,7 @@ fn parse_object(input: &str) -> Result<(Value, &str), String> {
290
339
 
291
340
  match input.chars().next() {
292
341
  Some(',') => input = &input[1..],
293
- Some('}') => return Ok((Value::Object(Rc::new(RefCell::new(map))), &input[1..])),
342
+ Some('}') => return Ok((Value::Object(VmRef::new(map)), &input[1..])),
294
343
  _ => return Err("Expected ',' or '}' in object".to_string()),
295
344
  }
296
345
  }
@@ -8,8 +8,10 @@ mod json;
8
8
  mod macros;
9
9
  mod uri;
10
10
  mod value;
11
+ mod vmref;
11
12
 
12
13
  pub use console_style::{format_value_styled, format_values_for_console, use_console_colors};
13
- pub use json::{json_parse, json_stringify};
14
+ pub use json::{json_parse, json_stringify, json_stringify_into};
14
15
  pub use uri::{percent_decode, percent_encode};
15
16
  pub use value::*;
17
+ pub use vmref::{VmReadGuard, VmRef, VmWriteGuard};
@@ -23,14 +23,14 @@
23
23
  #[macro_export]
24
24
  macro_rules! tish_module {
25
25
  ($($name:expr => $fn:expr),* $(,)?) => {{
26
- use std::cell::RefCell;
27
- use std::rc::Rc;
28
26
  use std::sync::Arc;
29
- use $crate::{ObjectMap, Value};
27
+ use $crate::{ObjectMap, Value, VmRef};
30
28
  let mut map = ObjectMap::default();
31
29
  $(
32
- map.insert(Arc::from($name), Value::Function(Rc::new($fn)));
30
+ // `Value::native` picks the right Rc / Arc wrapper depending on
31
+ // whether the `send-values` feature is enabled upstream.
32
+ map.insert(Arc::from($name), Value::native($fn));
33
33
  )*
34
- Value::Object(Rc::new(RefCell::new(map)))
34
+ Value::Object(VmRef::new(map))
35
35
  }};
36
36
  }
@@ -1,11 +1,11 @@
1
1
  //! Unified Value type for Tish runtime values.
2
2
 
3
- use std::cell::RefCell;
4
- use std::rc::Rc;
5
3
  use std::sync::Arc;
6
4
 
7
5
  use ahash::AHashMap;
8
6
 
7
+ use crate::vmref::VmRef;
8
+
9
9
  /// Property map for objects and other `Arc<str>` → `Value` tables (VM globals, scopes).
10
10
  /// Uses a faster hasher than `std::collections::HashMap` for string-heavy workloads.
11
11
  pub type ObjectMap = AHashMap<Arc<str>, Value>;
@@ -14,8 +14,17 @@ pub type ObjectMap = AHashMap<Arc<str>, Value>;
14
14
  use fancy_regex::Regex;
15
15
 
16
16
  /// Native function signature.
17
- /// Returns Value directly (not Result) for simplicity and backward compatibility.
18
- pub type NativeFn = Rc<dyn Fn(&[Value]) -> Value>;
17
+ ///
18
+ /// When the `send-values` feature is enabled this is
19
+ /// `Arc<dyn Fn + Send + Sync>`, so handler closures can be dispatched across
20
+ /// HTTP worker threads (`tishlang_runtime::http::serve`). Otherwise it stays
21
+ /// `Rc<dyn Fn>` for zero-overhead single-threaded execution (wasm / wasi /
22
+ /// interpreter / cranelift / llvm VMs and any Rust native build without
23
+ /// `http`).
24
+ #[cfg(feature = "send-values")]
25
+ pub type NativeFn = Arc<dyn Fn(&[Value]) -> Value + Send + Sync>;
26
+ #[cfg(not(feature = "send-values"))]
27
+ pub type NativeFn = std::rc::Rc<dyn Fn(&[Value]) -> Value>;
19
28
 
20
29
  /// Trait for opaque Rust types exposed to Tish (e.g. Polars DataFrame).
21
30
  /// Implementors provide method dispatch so Tish can call methods on the value.
@@ -202,17 +211,21 @@ impl TishRegExp {
202
211
 
203
212
  /// Runtime value for Tish programs.
204
213
  /// Used by both interpreter and compiled code.
214
+ ///
215
+ /// **Thread safety**: `Value: Send + Sync`. Mutable payloads live inside
216
+ /// [`VmRef`], a `Send + Sync` `Arc<Mutex<T>>` wrapper that preserves the
217
+ /// `RefCell`-style borrow API. Functions are `Arc<dyn Fn + Send + Sync>`.
205
218
  #[derive(Clone)]
206
219
  pub enum Value {
207
220
  Number(f64),
208
221
  String(Arc<str>),
209
222
  Bool(bool),
210
223
  Null,
211
- Array(Rc<RefCell<Vec<Value>>>),
212
- Object(Rc<RefCell<ObjectMap>>),
224
+ Array(VmRef<Vec<Value>>),
225
+ Object(VmRef<ObjectMap>),
213
226
  Function(NativeFn),
214
227
  #[cfg(feature = "regex")]
215
- RegExp(Rc<RefCell<TishRegExp>>),
228
+ RegExp(VmRef<TishRegExp>),
216
229
  /// Promise (for native compile). Interpreter uses tishlang_eval::Value::Promise.
217
230
  Promise(Arc<dyn TishPromise>),
218
231
  /// Opaque handle to a native Rust type (e.g. Polars DataFrame).
@@ -308,35 +321,60 @@ impl Value {
308
321
  (Value::String(a), Value::String(b)) => a == b,
309
322
  (Value::Bool(a), Value::Bool(b)) => a == b,
310
323
  (Value::Null, Value::Null) => true,
311
- (Value::Array(a), Value::Array(b)) => Rc::ptr_eq(a, b),
312
- (Value::Object(a), Value::Object(b)) => Rc::ptr_eq(a, b),
313
- (Value::Function(a), Value::Function(b)) => Rc::ptr_eq(a, b),
324
+ (Value::Array(a), Value::Array(b)) => VmRef::ptr_eq(a, b),
325
+ (Value::Object(a), Value::Object(b)) => VmRef::ptr_eq(a, b),
326
+ #[cfg(feature = "send-values")]
327
+ (Value::Function(a), Value::Function(b)) => Arc::ptr_eq(a, b),
328
+ #[cfg(not(feature = "send-values"))]
329
+ (Value::Function(a), Value::Function(b)) => std::rc::Rc::ptr_eq(a, b),
314
330
  #[cfg(feature = "regex")]
315
- (Value::RegExp(a), Value::RegExp(b)) => Rc::ptr_eq(a, b),
331
+ (Value::RegExp(a), Value::RegExp(b)) => VmRef::ptr_eq(a, b),
316
332
  (Value::Promise(a), Value::Promise(b)) => Arc::ptr_eq(a, b),
317
333
  (Value::Opaque(a), Value::Opaque(b)) => Arc::ptr_eq(a, b),
318
334
  _ => false,
319
335
  }
320
336
  }
321
337
 
338
+ /// Wrap a Rust closure in a `Value::Function`. Automatically picks
339
+ /// `Rc<dyn Fn>` or `Arc<dyn Fn + Send + Sync>` based on the
340
+ /// `send-values` feature, so callers don't have to `cfg`-gate their
341
+ /// code. The input bound tracks the feature too: when `send-values`
342
+ /// is enabled the closure must be `Send + Sync`, otherwise any `Fn`
343
+ /// is accepted.
344
+ #[cfg(feature = "send-values")]
345
+ pub fn native<F>(f: F) -> Self
346
+ where
347
+ F: Fn(&[Value]) -> Value + Send + Sync + 'static,
348
+ {
349
+ Value::Function(Arc::new(f))
350
+ }
351
+
352
+ #[cfg(not(feature = "send-values"))]
353
+ pub fn native<F>(f: F) -> Self
354
+ where
355
+ F: Fn(&[Value]) -> Value + 'static,
356
+ {
357
+ Value::Function(std::rc::Rc::new(f))
358
+ }
359
+
322
360
  /// Create a new array Value from a Vec.
323
361
  pub fn array(items: Vec<Value>) -> Self {
324
- Value::Array(Rc::new(RefCell::new(items)))
362
+ Value::Array(VmRef::new(items))
325
363
  }
326
364
 
327
365
  /// Create a new object Value from a property map.
328
366
  pub fn object(map: ObjectMap) -> Self {
329
- Value::Object(Rc::new(RefCell::new(map)))
367
+ Value::Object(VmRef::new(map))
330
368
  }
331
369
 
332
370
  /// Create an empty array Value.
333
371
  pub fn empty_array() -> Self {
334
- Value::Array(Rc::new(RefCell::new(Vec::new())))
372
+ Value::Array(VmRef::new(Vec::new()))
335
373
  }
336
374
 
337
375
  /// Create an empty object Value.
338
376
  pub fn empty_object() -> Self {
339
- Value::Object(Rc::new(RefCell::new(ObjectMap::default())))
377
+ Value::Object(VmRef::new(ObjectMap::default()))
340
378
  }
341
379
 
342
380
  /// Extract the number value, if this is a Number.