@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
@@ -8,9 +8,13 @@ description = "Shared builtin implementations for Tish (array, string, object, m
8
8
  license-file = { workspace = true }
9
9
  repository = { workspace = true }
10
10
  [dependencies]
11
- rand = "0.10.0"
11
+ rand = "0.10.1"
12
12
  tishlang_core = { path = "../tish_core", version = ">=0.1" }
13
13
 
14
14
  [features]
15
15
  default = []
16
16
  regex = ["tishlang_core/regex"]
17
+ # Propagate the `send-values` feature from `tishlang_core` so that closures
18
+ # handed to us via `NativeFn` pick up the `Send + Sync` bound automatically
19
+ # when multi-threaded `Value`s are enabled upstream.
20
+ send-values = ["tishlang_core/send-values"]
@@ -1,13 +1,14 @@
1
1
  //! Array builtin methods.
2
2
 
3
3
  use crate::helpers::normalize_index;
4
+ use tishlang_core::VmRef;
4
5
  use std::cell::RefCell;
5
6
  use std::rc::Rc;
6
7
  use tishlang_core::Value;
7
8
 
8
9
  /// Create a new array Value from a Vec of Values.
9
10
  pub fn from_vec(v: Vec<Value>) -> Value {
10
- Value::Array(Rc::new(RefCell::new(v)))
11
+ Value::Array(VmRef::new(v))
11
12
  }
12
13
 
13
14
  /// Get the length of an array.
@@ -110,7 +111,7 @@ pub fn join(arr: &Value, sep: &Value) -> Value {
110
111
  pub fn reverse(arr: &Value) -> Value {
111
112
  if let Value::Array(arr) = arr {
112
113
  arr.borrow_mut().reverse();
113
- Value::Array(Rc::clone(arr))
114
+ Value::Array(arr.clone())
114
115
  } else {
115
116
  Value::Null
116
117
  }
@@ -122,7 +123,7 @@ pub fn shuffle(arr: &Value) -> Value {
122
123
  let mut v = arr.borrow().clone();
123
124
  use rand::seq::SliceRandom;
124
125
  v.shuffle(&mut rand::rng());
125
- Value::Array(Rc::new(RefCell::new(v)))
126
+ Value::Array(VmRef::new(v))
126
127
  } else {
127
128
  Value::Null
128
129
  }
@@ -141,7 +142,7 @@ pub fn splice(arr: &Value, start: &Value, delete_count: Option<&Value>, items: &
141
142
  let removed: Vec<Value> = arr_mut
142
143
  .splice(start_idx..start_idx + actual_delete, items.iter().cloned())
143
144
  .collect();
144
- Value::Array(Rc::new(RefCell::new(removed)))
145
+ Value::Array(VmRef::new(removed))
145
146
  } else {
146
147
  Value::Null
147
148
  }
@@ -158,7 +159,7 @@ pub fn slice(arr: &Value, start: &Value, end: &Value) -> Value {
158
159
  } else {
159
160
  vec![]
160
161
  };
161
- Value::Array(Rc::new(RefCell::new(sliced)))
162
+ Value::Array(VmRef::new(sliced))
162
163
  } else {
163
164
  Value::Null
164
165
  }
@@ -174,7 +175,7 @@ pub fn concat(arr: &Value, args: &[Value]) -> Value {
174
175
  result.push(v.clone());
175
176
  }
176
177
  }
177
- Value::Array(Rc::new(RefCell::new(result)))
178
+ Value::Array(VmRef::new(result))
178
179
  } else {
179
180
  Value::Null
180
181
  }
@@ -200,7 +201,7 @@ pub fn flat(arr: &Value, depth: &Value) -> Value {
200
201
  };
201
202
  let mut result = Vec::new();
202
203
  flatten(&arr.borrow(), d, &mut result);
203
- Value::Array(Rc::new(RefCell::new(result)))
204
+ Value::Array(VmRef::new(result))
204
205
  } else {
205
206
  Value::Null
206
207
  }
@@ -217,7 +218,7 @@ pub fn map(arr: &Value, callback: &Value) -> Value {
217
218
  .enumerate()
218
219
  .map(|(i, v)| cb(&[v.clone(), Value::Number(i as f64)]))
219
220
  .collect();
220
- Value::Array(Rc::new(RefCell::new(result)))
221
+ Value::Array(VmRef::new(result))
221
222
  } else {
222
223
  Value::Null
223
224
  }
@@ -238,7 +239,7 @@ pub fn filter(arr: &Value, callback: &Value) -> Value {
238
239
  }
239
240
  })
240
241
  .collect();
241
- Value::Array(Rc::new(RefCell::new(result)))
242
+ Value::Array(VmRef::new(result))
242
243
  } else {
243
244
  Value::Null
244
245
  }
@@ -340,7 +341,7 @@ pub fn flat_map(arr: &Value, callback: &Value) -> Value {
340
341
  result.push(mapped);
341
342
  }
342
343
  }
343
- Value::Array(Rc::new(RefCell::new(result)))
344
+ Value::Array(VmRef::new(result))
344
345
  } else {
345
346
  Value::Null
346
347
  }
@@ -352,7 +353,7 @@ where
352
353
  {
353
354
  if let Value::Array(arr) = arr {
354
355
  arr.borrow_mut().sort_by(cmp);
355
- Value::Array(Rc::clone(arr))
356
+ Value::Array(arr.clone())
356
357
  } else {
357
358
  Value::Null
358
359
  }
@@ -387,7 +388,7 @@ pub fn sort_with_comparator(arr: &Value, comparator: &Value) -> Value {
387
388
  .map(|i| std::mem::replace(&mut elements[i], Value::Null))
388
389
  .collect();
389
390
  drop(arr_mut);
390
- Value::Array(Rc::clone(arr))
391
+ Value::Array(arr.clone())
391
392
  } else {
392
393
  Value::Null
393
394
  }
@@ -2,6 +2,7 @@
2
2
  //! Browser-exact behavior remains on `tish build --target js`.
3
3
 
4
4
  use std::cell::RefCell;
5
+ use tishlang_core::VmRef;
5
6
  use std::rc::Rc;
6
7
  use std::sync::Arc;
7
8
 
@@ -16,7 +17,7 @@ pub fn construct(callee: &Value, args: &[Value]) -> Value {
16
17
  Value::Object(o) => {
17
18
  let b = o.borrow();
18
19
  if let Some(Value::Function(ctor)) = b.get(&Arc::from(CONSTRUCT)) {
19
- let c = Rc::clone(ctor);
20
+ let c = ctor.clone();
20
21
  drop(b);
21
22
  return c(args);
22
23
  }
@@ -29,11 +30,11 @@ pub fn construct(callee: &Value, args: &[Value]) -> Value {
29
30
  fn param(initial: f64) -> Value {
30
31
  let mut m = ObjectMap::default();
31
32
  m.insert(Arc::from("value"), Value::Number(initial));
32
- Value::Object(Rc::new(RefCell::new(m)))
33
+ Value::Object(VmRef::new(m))
33
34
  }
34
35
 
35
36
  fn connect_fn() -> Value {
36
- Value::Function(Rc::new(|_| Value::Null))
37
+ Value::native(|_| Value::Null)
37
38
  }
38
39
 
39
40
  /// Shared audio-node shape: connect, gain, optional filter fields.
@@ -44,33 +45,33 @@ fn audio_node_stub() -> Value {
44
45
  m.insert(Arc::from("frequency"), param(440.0));
45
46
  m.insert(Arc::from("Q"), param(1.0));
46
47
  m.insert(Arc::from("type"), Value::String("peaking".into()));
47
- Value::Object(Rc::new(RefCell::new(m)))
48
+ Value::Object(VmRef::new(m))
48
49
  }
49
50
 
50
51
  fn analyser_stub() -> Value {
51
52
  let mut m = ObjectMap::default();
52
53
  m.insert(Arc::from("connect"), connect_fn());
53
54
  m.insert(Arc::from("fftSize"), Value::Number(2048.0));
54
- Value::Object(Rc::new(RefCell::new(m)))
55
+ Value::Object(VmRef::new(m))
55
56
  }
56
57
 
57
58
  fn stereo_panner_stub() -> Value {
58
59
  let mut m = ObjectMap::default();
59
60
  m.insert(Arc::from("connect"), connect_fn());
60
61
  m.insert(Arc::from("pan"), param(0.0));
61
- Value::Object(Rc::new(RefCell::new(m)))
62
+ Value::Object(VmRef::new(m))
62
63
  }
63
64
 
64
65
  fn audio_buffer_stub(len: usize) -> Value {
65
66
  let n = len.max(1);
66
- let data = Rc::new(RefCell::new(vec![Value::Number(0.0); n]));
67
- let data2 = Rc::clone(&data);
67
+ let data = VmRef::new(vec![Value::Number(0.0); n]);
68
+ let data2 = data.clone();
68
69
  let mut m = ObjectMap::default();
69
70
  m.insert(
70
71
  Arc::from("getChannelData"),
71
- Value::Function(Rc::new(move |_args| Value::Array(Rc::clone(&data2)))),
72
+ Value::native(move |_args| Value::Array(data2.clone())),
72
73
  );
73
- Value::Object(Rc::new(RefCell::new(m)))
74
+ Value::Object(VmRef::new(m))
74
75
  }
75
76
 
76
77
  fn buffer_source_stub() -> Value {
@@ -80,10 +81,10 @@ fn buffer_source_stub() -> Value {
80
81
  m.insert(Arc::from("connect"), connect_fn());
81
82
  m.insert(
82
83
  Arc::from("start"),
83
- Value::Function(Rc::new(|_| Value::Null)),
84
+ Value::native(|_| Value::Null),
84
85
  );
85
- m.insert(Arc::from("stop"), Value::Function(Rc::new(|_| Value::Null)));
86
- Value::Object(Rc::new(RefCell::new(m)))
86
+ m.insert(Arc::from("stop"), Value::native(|_| Value::Null));
87
+ Value::Object(VmRef::new(m))
87
88
  }
88
89
 
89
90
  fn oscillator_stub() -> Value {
@@ -93,10 +94,10 @@ fn oscillator_stub() -> Value {
93
94
  m.insert(Arc::from("connect"), connect_fn());
94
95
  m.insert(
95
96
  Arc::from("start"),
96
- Value::Function(Rc::new(|_| Value::Null)),
97
+ Value::native(|_| Value::Null),
97
98
  );
98
- m.insert(Arc::from("stop"), Value::Function(Rc::new(|_| Value::Null)));
99
- Value::Object(Rc::new(RefCell::new(m)))
99
+ m.insert(Arc::from("stop"), Value::native(|_| Value::Null));
100
+ Value::Object(VmRef::new(m))
100
101
  }
101
102
 
102
103
  fn audio_context_instance() -> Value {
@@ -106,66 +107,66 @@ fn audio_context_instance() -> Value {
106
107
 
107
108
  ctx.insert(
108
109
  Arc::from("createGain"),
109
- Value::Function(Rc::new(|_| audio_node_stub())),
110
+ Value::native(|_| audio_node_stub()),
110
111
  );
111
112
  ctx.insert(
112
113
  Arc::from("createBiquadFilter"),
113
- Value::Function(Rc::new(|_| audio_node_stub())),
114
+ Value::native(|_| audio_node_stub()),
114
115
  );
115
116
  ctx.insert(
116
117
  Arc::from("createStereoPanner"),
117
- Value::Function(Rc::new(|_| stereo_panner_stub())),
118
+ Value::native(|_| stereo_panner_stub()),
118
119
  );
119
120
  ctx.insert(
120
121
  Arc::from("createAnalyser"),
121
- Value::Function(Rc::new(|_| analyser_stub())),
122
+ Value::native(|_| analyser_stub()),
122
123
  );
123
124
  ctx.insert(
124
125
  Arc::from("createBuffer"),
125
- Value::Function(Rc::new(|args: &[Value]| {
126
+ Value::native(|args: &[Value]| {
126
127
  let len = args
127
128
  .get(1)
128
129
  .and_then(Value::as_number)
129
130
  .unwrap_or(0.0)
130
131
  .clamp(0.0, 1_000_000_000.0) as usize;
131
132
  audio_buffer_stub(len)
132
- })),
133
+ }),
133
134
  );
134
135
  ctx.insert(
135
136
  Arc::from("createBufferSource"),
136
- Value::Function(Rc::new(|_| buffer_source_stub())),
137
+ Value::native(|_| buffer_source_stub()),
137
138
  );
138
139
  ctx.insert(
139
140
  Arc::from("createOscillator"),
140
- Value::Function(Rc::new(|_| oscillator_stub())),
141
+ Value::native(|_| oscillator_stub()),
141
142
  );
142
143
  ctx.insert(
143
144
  Arc::from("decodeAudioData"),
144
- Value::Function(Rc::new(|_| Value::Null)),
145
+ Value::native(|_| Value::Null),
145
146
  );
146
147
 
147
- Value::Object(Rc::new(RefCell::new(ctx)))
148
+ Value::Object(VmRef::new(ctx))
148
149
  }
149
150
 
150
151
  /// Global `Uint8Array` for native/VM: `new Uint8Array(n)` → numeric array of zeros (not real bytes).
151
152
  pub fn uint8_array_constructor_value() -> Value {
152
- let ctor = Rc::new(|args: &[Value]| {
153
+ let ctor = Value::native(|args: &[Value]| {
153
154
  let len = args
154
155
  .first()
155
156
  .and_then(Value::as_number)
156
157
  .unwrap_or(0.0)
157
158
  .clamp(0.0, 1_000_000_000.0) as usize;
158
- Value::Array(Rc::new(RefCell::new(vec![Value::Number(0.0); len])))
159
+ Value::Array(VmRef::new(vec![Value::Number(0.0); len]))
159
160
  });
160
161
  let mut m = ObjectMap::default();
161
- m.insert(Arc::from(CONSTRUCT), Value::Function(ctor));
162
- Value::Object(Rc::new(RefCell::new(m)))
162
+ m.insert(Arc::from(CONSTRUCT), ctor);
163
+ Value::Object(VmRef::new(m))
163
164
  }
164
165
 
165
166
  /// Global `AudioContext` for native/VM: stub graph (no real audio).
166
167
  pub fn audio_context_constructor_value() -> Value {
167
- let ctor = Rc::new(|_args: &[Value]| audio_context_instance());
168
+ let ctor = Value::native(|_args: &[Value]| audio_context_instance());
168
169
  let mut m = ObjectMap::default();
169
- m.insert(Arc::from(CONSTRUCT), Value::Function(ctor));
170
- Value::Object(Rc::new(RefCell::new(m)))
170
+ m.insert(Arc::from(CONSTRUCT), ctor);
171
+ Value::Object(VmRef::new(m))
171
172
  }
@@ -4,6 +4,7 @@
4
4
  //! independent of tishlang_runtime.
5
5
 
6
6
  use std::cell::RefCell;
7
+ use tishlang_core::VmRef;
7
8
  use std::rc::Rc;
8
9
  use std::sync::Arc;
9
10
  use tishlang_core::{percent_decode, percent_encode, ObjectMap, Value};
@@ -78,9 +79,9 @@ pub fn object_keys(args: &[Value]) -> Value {
78
79
  .keys()
79
80
  .map(|k| Value::String(Arc::clone(k)))
80
81
  .collect();
81
- Value::Array(Rc::new(RefCell::new(keys)))
82
+ Value::Array(VmRef::new(keys))
82
83
  } else {
83
- Value::Array(Rc::new(RefCell::new(Vec::new())))
84
+ Value::Array(VmRef::new(Vec::new()))
84
85
  }
85
86
  }
86
87
 
@@ -89,9 +90,9 @@ pub fn object_values(args: &[Value]) -> Value {
89
90
  if let Some(Value::Object(obj)) = args.first() {
90
91
  let obj_borrow = obj.borrow();
91
92
  let values: Vec<Value> = obj_borrow.values().cloned().collect();
92
- Value::Array(Rc::new(RefCell::new(values)))
93
+ Value::Array(VmRef::new(values))
93
94
  } else {
94
- Value::Array(Rc::new(RefCell::new(Vec::new())))
95
+ Value::Array(VmRef::new(Vec::new()))
95
96
  }
96
97
  }
97
98
 
@@ -102,15 +103,15 @@ pub fn object_entries(args: &[Value]) -> Value {
102
103
  let entries: Vec<Value> = obj_borrow
103
104
  .iter()
104
105
  .map(|(k, v)| {
105
- Value::Array(Rc::new(RefCell::new(vec![
106
+ Value::Array(VmRef::new(vec![
106
107
  Value::String(Arc::clone(k)),
107
108
  v.clone(),
108
- ])))
109
+ ]))
109
110
  })
110
111
  .collect();
111
- Value::Array(Rc::new(RefCell::new(entries)))
112
+ Value::Array(VmRef::new(entries))
112
113
  } else {
113
- Value::Array(Rc::new(RefCell::new(Vec::new())))
114
+ Value::Array(VmRef::new(Vec::new()))
114
115
  }
115
116
  }
116
117
 
@@ -145,7 +146,7 @@ pub fn object_assign(args: &[Value]) -> Value {
145
146
  }
146
147
  }
147
148
  drop(target_mut);
148
- Value::Object(Rc::clone(target))
149
+ Value::Object(target.clone())
149
150
  }
150
151
 
151
152
  /// parseInt(string, radix?)
@@ -203,8 +204,8 @@ pub fn object_from_entries(args: &[Value]) -> Value {
203
204
  }
204
205
  }
205
206
 
206
- Value::Object(Rc::new(RefCell::new(obj)))
207
+ Value::Object(VmRef::new(obj))
207
208
  } else {
208
- Value::Object(Rc::new(RefCell::new(ObjectMap::default())))
209
+ Value::Object(VmRef::new(ObjectMap::default()))
209
210
  }
210
211
  }
@@ -1,6 +1,7 @@
1
1
  //! Common helper functions used across builtin implementations.
2
2
 
3
3
  use std::cell::RefCell;
4
+ use tishlang_core::VmRef;
4
5
  use std::rc::Rc;
5
6
  use std::sync::Arc;
6
7
  use tishlang_core::{ObjectMap, Value};
@@ -25,7 +26,7 @@ pub fn normalize_index(idx: &Value, len: i64, default: usize) -> usize {
25
26
  pub fn make_error_value(e: impl std::fmt::Display) -> Value {
26
27
  let mut obj = ObjectMap::with_capacity(1);
27
28
  obj.insert(Arc::from("error"), Value::String(e.to_string().into()));
28
- Value::Object(Rc::new(RefCell::new(obj)))
29
+ Value::Object(VmRef::new(obj))
29
30
  }
30
31
 
31
32
  /// Extract a number from a Value, returning None for non-numbers.
@@ -4,18 +4,19 @@
4
4
  //! Functions will be migrated here from tishlang_runtime and tishlang_eval.
5
5
 
6
6
  use std::cell::RefCell;
7
+ use tishlang_core::VmRef;
7
8
  use std::rc::Rc;
8
9
  use std::sync::Arc;
9
10
  use tishlang_core::{ObjectMap, Value};
10
11
 
11
12
  /// Create a new empty object Value.
12
13
  pub fn new() -> Value {
13
- Value::Object(Rc::new(RefCell::new(ObjectMap::default())))
14
+ Value::Object(VmRef::new(ObjectMap::default()))
14
15
  }
15
16
 
16
17
  /// Create a new object Value with a given capacity.
17
18
  pub fn with_capacity(capacity: usize) -> Value {
18
- Value::Object(Rc::new(RefCell::new(ObjectMap::with_capacity(capacity))))
19
+ Value::Object(VmRef::new(ObjectMap::with_capacity(capacity)))
19
20
  }
20
21
 
21
22
  /// Get the keys of an object.
@@ -4,6 +4,7 @@
4
4
  //! JavaScript, matching .length and .charAt(). Byte offsets are never exposed.
5
5
 
6
6
  use crate::helpers::normalize_index;
7
+ use tishlang_core::VmRef;
7
8
  use std::cell::RefCell;
8
9
  use std::rc::Rc;
9
10
  use std::sync::Arc;
@@ -194,13 +195,13 @@ pub fn split(s: &Value, sep: &Value) -> Value {
194
195
  if let Value::String(s) = s {
195
196
  let separator = match sep {
196
197
  Value::String(ss) => ss.as_ref(),
197
- _ => return Value::Array(Rc::new(RefCell::new(vec![Value::String(Arc::clone(s))]))),
198
+ _ => return Value::Array(VmRef::new(vec![Value::String(Arc::clone(s))])),
198
199
  };
199
200
  let parts: Vec<Value> = s
200
201
  .split(separator)
201
202
  .map(|p| Value::String(p.into()))
202
203
  .collect();
203
- Value::Array(Rc::new(RefCell::new(parts)))
204
+ Value::Array(VmRef::new(parts))
204
205
  } else {
205
206
  Value::Null
206
207
  }
@@ -275,6 +276,53 @@ pub fn replace_all(s: &Value, search: &Value, replacement: &Value) -> Value {
275
276
  replace_impl(s, search, replacement, true)
276
277
  }
277
278
 
279
+ /// HTML entity escape for the five canonical characters (`& < > " '`).
280
+ /// Single linear pass over the input; takes a zero-copy fast path when no
281
+ /// character needs escaping. Matches TFB's fortunes verifier byte-for-byte.
282
+ pub fn escape_html(s: &Value) -> Value {
283
+ let input = match s {
284
+ Value::String(s) => s.as_ref(),
285
+ Value::Null => return Value::String(Arc::from("")),
286
+ _ => return Value::Null,
287
+ };
288
+ let bytes = input.as_bytes();
289
+ let mut extra = 0usize;
290
+ for b in bytes {
291
+ match b {
292
+ b'&' => extra += 4,
293
+ b'<' | b'>' => extra += 3,
294
+ b'"' => extra += 5,
295
+ b'\'' => extra += 4,
296
+ _ => {}
297
+ }
298
+ }
299
+ if extra == 0 {
300
+ return Value::String(Arc::clone(match s {
301
+ Value::String(s) => s,
302
+ _ => unreachable!(),
303
+ }));
304
+ }
305
+ let mut out = String::with_capacity(input.len() + extra);
306
+ let mut last = 0usize;
307
+ for (i, b) in bytes.iter().enumerate() {
308
+ let repl: Option<&'static str> = match b {
309
+ b'&' => Some("&amp;"),
310
+ b'<' => Some("&lt;"),
311
+ b'>' => Some("&gt;"),
312
+ b'"' => Some("&quot;"),
313
+ b'\'' => Some("&#39;"),
314
+ _ => None,
315
+ };
316
+ if let Some(r) = repl {
317
+ out.push_str(&input[last..i]);
318
+ out.push_str(r);
319
+ last = i + 1;
320
+ }
321
+ }
322
+ out.push_str(&input[last..]);
323
+ Value::String(Arc::from(out))
324
+ }
325
+
278
326
  fn char_at_idx(s: &str, idx: usize) -> Option<char> {
279
327
  s.chars().nth(idx)
280
328
  }
@@ -463,7 +511,7 @@ mod tests {
463
511
  assert_eq!(a.borrow().len(), 2);
464
512
  assert_same!(
465
513
  split(&s("x"), &n(1.0)),
466
- Value::Array(Rc::new(RefCell::new(vec![s("x")])))
514
+ Value::Array(VmRef::new(vec![s("x")]))
467
515
  );
468
516
  assert_same!(split(&n(1.0), &s(",")), Value::Null);
469
517
  assert_same!(trim(&s(" x ")), s("x"));
@@ -550,4 +598,26 @@ mod tests {
550
598
  fn last_index_of_non_string() {
551
599
  assert_same!(last_index_of(&n(1.0), &s("a"), &n(0.0)), n(-1.0));
552
600
  }
601
+
602
+ #[test]
603
+ fn escape_html_basic() {
604
+ assert_same!(escape_html(&s("plain text")), s("plain text"));
605
+ assert_same!(
606
+ escape_html(&s("<script>alert(\"xss\")</script>")),
607
+ s("&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;")
608
+ );
609
+ assert_same!(escape_html(&s("tom & jerry")), s("tom &amp; jerry"));
610
+ assert_same!(escape_html(&s("it's")), s("it&#39;s"));
611
+ assert_same!(
612
+ escape_html(&s("<script>alert('x' & \"y\");</script>")),
613
+ s("&lt;script&gt;alert(&#39;x&#39; &amp; &quot;y&quot;);&lt;/script&gt;")
614
+ );
615
+ }
616
+
617
+ #[test]
618
+ fn escape_html_unicode_preserved() {
619
+ // Astral symbols / non-ASCII must round-trip unchanged.
620
+ assert_same!(escape_html(&s("フレーム")), s("フレーム"));
621
+ assert_same!(escape_html(&s("🎉 & 💥")), s("🎉 &amp; 💥"));
622
+ }
553
623
  }
@@ -275,12 +275,12 @@ impl<'a> Compiler<'a> {
275
275
  if let (
276
276
  Expr::Member {
277
277
  object: lo,
278
- prop: MemberProp::Name(p),
278
+ prop: MemberProp::Name { name: p, .. },
279
279
  ..
280
280
  },
281
281
  Expr::Member {
282
282
  object: ro,
283
- prop: MemberProp::Name(pr),
283
+ prop: MemberProp::Name { name: pr, .. },
284
284
  ..
285
285
  },
286
286
  ) = (left.as_ref(), right.as_ref())
@@ -982,6 +982,9 @@ impl<'a> Compiler<'a> {
982
982
  });
983
983
  }
984
984
  },
985
+ Statement::TypeAlias { .. }
986
+ | Statement::DeclareVar { .. }
987
+ | Statement::DeclareFun { .. } => {}
985
988
  }
986
989
  Ok(())
987
990
  }
@@ -1001,7 +1004,7 @@ impl<'a> Compiler<'a> {
1001
1004
  DestructPattern::Array(elements) => {
1002
1005
  for (i, elem) in elements.iter().enumerate() {
1003
1006
  match elem {
1004
- Some(DestructElement::Ident(name)) => {
1007
+ Some(DestructElement::Ident(name, _)) => {
1005
1008
  self.emit(Opcode::Dup);
1006
1009
  let idx = self.constant_idx(Constant::Number(i as f64));
1007
1010
  self.emit(Opcode::LoadConst);
@@ -1031,7 +1034,7 @@ impl<'a> Compiler<'a> {
1031
1034
  self.chunk.write_u16(key_idx);
1032
1035
  self.emit(Opcode::GetIndex); // GetIndex pops obj, index and uses get_member
1033
1036
  match &prop.value {
1034
- DestructElement::Ident(name) => {
1037
+ DestructElement::Ident(name, _) => {
1035
1038
  let idx = self.name_idx(name);
1036
1039
  self.emit_u16(decl_op, idx);
1037
1040
  if mutable {
@@ -1118,7 +1121,7 @@ impl<'a> Compiler<'a> {
1118
1121
  if let (
1119
1122
  Expr::Member {
1120
1123
  object,
1121
- prop: MemberProp::Name(key),
1124
+ prop: MemberProp::Name { name: key, .. },
1122
1125
  optional: false,
1123
1126
  ..
1124
1127
  },
@@ -1231,7 +1234,7 @@ impl<'a> Compiler<'a> {
1231
1234
  let jump_end = self.emit_jump(Opcode::Jump);
1232
1235
  self.patch_jump(jump_to_get, self.chunk.code.len());
1233
1236
  match prop {
1234
- MemberProp::Name(key) => {
1237
+ MemberProp::Name { name: key, .. } => {
1235
1238
  let idx = self.name_idx(key);
1236
1239
  self.emit_u16(Opcode::GetMemberOptional, idx);
1237
1240
  }
@@ -1243,7 +1246,7 @@ impl<'a> Compiler<'a> {
1243
1246
  self.patch_jump(jump_end, self.chunk.code.len());
1244
1247
  } else {
1245
1248
  match prop {
1246
- MemberProp::Name(key) => {
1249
+ MemberProp::Name { name: key, .. } => {
1247
1250
  let idx = self.name_idx(key);
1248
1251
  self.emit_u16(Opcode::GetMember, idx);
1249
1252
  }
@@ -1511,14 +1514,9 @@ impl<'a> Compiler<'a> {
1511
1514
  self.compile_jsx_fragment(children)?;
1512
1515
  }
1513
1516
  Expr::Await { operand, .. } => {
1514
- // await expr => LoadNativeExport("tish:http","await"), compile(operand), Call(1)
1515
- let spec_idx = self.constant_idx(Constant::String(Arc::from("tish:http")));
1516
- let await_idx = self.constant_idx(Constant::String(Arc::from("await")));
1517
- self.emit(Opcode::LoadNativeExport);
1518
- self.chunk.write_u16(spec_idx);
1519
- self.chunk.write_u16(await_idx);
1517
+ // await expr => evaluate operand, then VM Opcode::AwaitPromise (throw on reject).
1520
1518
  self.compile_expr(operand)?;
1521
- self.emit_u16(Opcode::Call, 1);
1519
+ self.emit(Opcode::AwaitPromise);
1522
1520
  }
1523
1521
  Expr::LogicalAssign {
1524
1522
  name, op, value, ..
@@ -93,13 +93,16 @@ pub enum Opcode {
93
93
  ExitBlock = 41,
94
94
  /// Like [`DeclareVar`] but does not record block-scope undo (for `for`/`for-of` header bindings).
95
95
  DeclareVarPlain = 42,
96
+ /// Pop the `await` operand value; if it is a `Promise`, block until settled, push the result,
97
+ /// or unwind to `catch` like `Throw` on rejection.
98
+ AwaitPromise = 43,
96
99
  }
97
100
 
98
101
  impl Opcode {
99
- /// Decode byte to opcode. Safe for b in 0..=42 (matches #[repr(u8)] discriminants).
102
+ /// Decode byte to opcode. Safe for b in 0..=43 (matches #[repr(u8)] discriminants).
100
103
  #[inline]
101
104
  pub fn from_u8(b: u8) -> Option<Opcode> {
102
- if b <= 42 {
105
+ if b <= 43 {
103
106
  Some(unsafe { std::mem::transmute(b) })
104
107
  } else {
105
108
  None
@@ -114,11 +117,17 @@ impl Opcode {
114
117
  | Opcode::Dup
115
118
  | Opcode::Return
116
119
  | Opcode::ExitTry
120
+ | Opcode::ConcatArray
121
+ | Opcode::MergeObject
122
+ | Opcode::GetIndex
123
+ | Opcode::SetIndex
124
+ | Opcode::Throw
117
125
  | Opcode::ArrayMapIdentity
118
126
  | Opcode::CallSpread
119
127
  | Opcode::ConstructSpread
120
128
  | Opcode::EnterBlock
121
- | Opcode::ExitBlock => 1,
129
+ | Opcode::ExitBlock
130
+ | Opcode::AwaitPromise => 1,
122
131
  Opcode::ArraySortByProperty
123
132
  | Opcode::ArrayMapBinOp
124
133
  | Opcode::ArrayFilterBinOp
@@ -9,6 +9,7 @@ repository = { workspace = true }
9
9
  [features]
10
10
  default = []
11
11
  http = []
12
+ timers = []
12
13
  fs = []
13
14
  process = []
14
15
  regex = []