@tishlang/tish 1.6.0 β†’ 1.8.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 (113) 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/error.rs +2 -8
  5. package/crates/js_to_tish/src/transform/expr.rs +128 -137
  6. package/crates/js_to_tish/src/transform/stmt.rs +62 -32
  7. package/crates/tish/Cargo.toml +15 -5
  8. package/crates/tish/src/cargo_native_registry.rs +29 -0
  9. package/crates/tish/src/cli_help.rs +92 -39
  10. package/crates/tish/src/main.rs +172 -86
  11. package/crates/tish/src/repl_completion.rs +3 -3
  12. package/crates/tish/tests/cargo_example_compile.rs +4 -2
  13. package/crates/tish/tests/integration_test.rs +216 -54
  14. package/crates/tish/tests/run_optimize_stdout_parity.rs +3 -7
  15. package/crates/tish/tests/shortcircuit.rs +20 -5
  16. package/crates/tish_ast/src/ast.rs +92 -23
  17. package/crates/tish_build_utils/Cargo.toml +4 -0
  18. package/crates/tish_build_utils/src/lib.rs +136 -8
  19. package/crates/tish_builtins/Cargo.toml +5 -1
  20. package/crates/tish_builtins/src/array.rs +65 -33
  21. package/crates/tish_builtins/src/construct.rs +34 -39
  22. package/crates/tish_builtins/src/globals.rs +42 -26
  23. package/crates/tish_builtins/src/helpers.rs +2 -1
  24. package/crates/tish_builtins/src/lib.rs +5 -5
  25. package/crates/tish_builtins/src/math.rs +5 -3
  26. package/crates/tish_builtins/src/object.rs +3 -2
  27. package/crates/tish_builtins/src/string.rs +144 -22
  28. package/crates/tish_bytecode/src/chunk.rs +0 -1
  29. package/crates/tish_bytecode/src/compiler.rs +173 -71
  30. package/crates/tish_bytecode/src/opcode.rs +24 -6
  31. package/crates/tish_bytecode/src/peephole.rs +2 -2
  32. package/crates/tish_compile/Cargo.toml +1 -0
  33. package/crates/tish_compile/src/codegen.rs +1621 -453
  34. package/crates/tish_compile/src/infer.rs +75 -19
  35. package/crates/tish_compile/src/lib.rs +19 -8
  36. package/crates/tish_compile/src/resolve.rs +278 -137
  37. package/crates/tish_compile/src/types.rs +184 -24
  38. package/crates/tish_compile_js/Cargo.toml +1 -0
  39. package/crates/tish_compile_js/src/codegen.rs +181 -37
  40. package/crates/tish_compile_js/src/lib.rs +3 -1
  41. package/crates/tish_compile_js/src/tests_jsx.rs +30 -6
  42. package/crates/tish_compiler_wasm/src/lib.rs +16 -13
  43. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +69 -59
  44. package/crates/tish_core/Cargo.toml +8 -0
  45. package/crates/tish_core/src/json.rs +107 -56
  46. package/crates/tish_core/src/lib.rs +4 -2
  47. package/crates/tish_core/src/macros.rs +5 -5
  48. package/crates/tish_core/src/uri.rs +9 -6
  49. package/crates/tish_core/src/value.rs +145 -43
  50. package/crates/tish_core/src/vmref.rs +178 -0
  51. package/crates/tish_cranelift/src/link.rs +6 -9
  52. package/crates/tish_cranelift/src/lower.rs +14 -8
  53. package/crates/tish_eval/Cargo.toml +17 -2
  54. package/crates/tish_eval/src/eval.rs +474 -165
  55. package/crates/tish_eval/src/http.rs +61 -0
  56. package/crates/tish_eval/src/lib.rs +12 -8
  57. package/crates/tish_eval/src/natives.rs +136 -38
  58. package/crates/tish_eval/src/promise.rs +14 -8
  59. package/crates/tish_eval/src/timers.rs +28 -19
  60. package/crates/tish_eval/src/value.rs +17 -6
  61. package/crates/tish_eval/src/value_convert.rs +13 -5
  62. package/crates/tish_fmt/src/lib.rs +149 -43
  63. package/crates/tish_lexer/src/lib.rs +232 -63
  64. package/crates/tish_lexer/src/token.rs +10 -6
  65. package/crates/tish_llvm/src/lib.rs +17 -8
  66. package/crates/tish_lsp/Cargo.toml +4 -1
  67. package/crates/tish_lsp/README.md +1 -1
  68. package/crates/tish_lsp/src/builtin_goto.rs +261 -0
  69. package/crates/tish_lsp/src/import_goto.rs +549 -0
  70. package/crates/tish_lsp/src/main.rs +504 -106
  71. package/crates/tish_native/src/build.rs +4 -8
  72. package/crates/tish_native/src/lib.rs +54 -21
  73. package/crates/tish_opt/src/lib.rs +84 -52
  74. package/crates/tish_parser/src/lib.rs +45 -13
  75. package/crates/tish_parser/src/parser.rs +505 -130
  76. package/crates/tish_resolve/Cargo.toml +13 -0
  77. package/crates/tish_resolve/src/lib.rs +3436 -0
  78. package/crates/tish_resolve/src/pos.rs +133 -0
  79. package/crates/tish_runtime/Cargo.toml +68 -3
  80. package/crates/tish_runtime/src/http.rs +1136 -145
  81. package/crates/tish_runtime/src/http_fetch.rs +38 -27
  82. package/crates/tish_runtime/src/http_hyper.rs +418 -0
  83. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  84. package/crates/tish_runtime/src/lib.rs +375 -189
  85. package/crates/tish_runtime/src/promise.rs +199 -40
  86. package/crates/tish_runtime/src/promise_io.rs +2 -1
  87. package/crates/tish_runtime/src/timers.rs +37 -1
  88. package/crates/tish_runtime/src/ws.rs +65 -42
  89. package/crates/tish_runtime/tests/fetch_readable_stream.rs +5 -4
  90. package/crates/tish_ui/src/jsx.rs +317 -27
  91. package/crates/tish_ui/src/lib.rs +5 -2
  92. package/crates/tish_ui/src/runtime/hooks.rs +406 -45
  93. package/crates/tish_ui/src/runtime/mod.rs +36 -9
  94. package/crates/tish_vm/Cargo.toml +15 -5
  95. package/crates/tish_vm/src/vm.rs +725 -281
  96. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +11 -4
  97. package/crates/tish_wasm/src/lib.rs +55 -42
  98. package/crates/tish_wasm_runtime/Cargo.toml +2 -1
  99. package/crates/tish_wasm_runtime/src/lib.rs +1 -1
  100. package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
  101. package/crates/tishlang_cargo_bindgen/src/classify.rs +265 -0
  102. package/crates/tishlang_cargo_bindgen/src/discover.rs +120 -0
  103. package/crates/tishlang_cargo_bindgen/src/infer.rs +372 -0
  104. package/crates/tishlang_cargo_bindgen/src/lib.rs +350 -0
  105. package/crates/tishlang_cargo_bindgen/src/main.rs +164 -0
  106. package/crates/tishlang_cargo_bindgen/src/metadata.rs +114 -0
  107. package/justfile +8 -0
  108. package/package.json +1 -1
  109. package/platform/darwin-arm64/tish +0 -0
  110. package/platform/darwin-x64/tish +0 -0
  111. package/platform/linux-arm64/tish +0 -0
  112. package/platform/linux-x64/tish +0 -0
  113. package/platform/win32-x64/tish.exe +0 -0
@@ -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,13 +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(
86
- Arc::from("stop"),
87
- Value::Function(Rc::new(|_| Value::Null)),
88
- );
89
- Value::Object(Rc::new(RefCell::new(m)))
86
+ m.insert(Arc::from("stop"), Value::native(|_| Value::Null));
87
+ Value::Object(VmRef::new(m))
90
88
  }
91
89
 
92
90
  fn oscillator_stub() -> Value {
@@ -96,13 +94,10 @@ fn oscillator_stub() -> Value {
96
94
  m.insert(Arc::from("connect"), connect_fn());
97
95
  m.insert(
98
96
  Arc::from("start"),
99
- Value::Function(Rc::new(|_| Value::Null)),
100
- );
101
- m.insert(
102
- Arc::from("stop"),
103
- Value::Function(Rc::new(|_| Value::Null)),
97
+ Value::native(|_| Value::Null),
104
98
  );
105
- Value::Object(Rc::new(RefCell::new(m)))
99
+ m.insert(Arc::from("stop"), Value::native(|_| Value::Null));
100
+ Value::Object(VmRef::new(m))
106
101
  }
107
102
 
108
103
  fn audio_context_instance() -> Value {
@@ -112,66 +107,66 @@ fn audio_context_instance() -> Value {
112
107
 
113
108
  ctx.insert(
114
109
  Arc::from("createGain"),
115
- Value::Function(Rc::new(|_| audio_node_stub())),
110
+ Value::native(|_| audio_node_stub()),
116
111
  );
117
112
  ctx.insert(
118
113
  Arc::from("createBiquadFilter"),
119
- Value::Function(Rc::new(|_| audio_node_stub())),
114
+ Value::native(|_| audio_node_stub()),
120
115
  );
121
116
  ctx.insert(
122
117
  Arc::from("createStereoPanner"),
123
- Value::Function(Rc::new(|_| stereo_panner_stub())),
118
+ Value::native(|_| stereo_panner_stub()),
124
119
  );
125
120
  ctx.insert(
126
121
  Arc::from("createAnalyser"),
127
- Value::Function(Rc::new(|_| analyser_stub())),
122
+ Value::native(|_| analyser_stub()),
128
123
  );
129
124
  ctx.insert(
130
125
  Arc::from("createBuffer"),
131
- Value::Function(Rc::new(|args: &[Value]| {
126
+ Value::native(|args: &[Value]| {
132
127
  let len = args
133
128
  .get(1)
134
129
  .and_then(Value::as_number)
135
130
  .unwrap_or(0.0)
136
131
  .clamp(0.0, 1_000_000_000.0) as usize;
137
132
  audio_buffer_stub(len)
138
- })),
133
+ }),
139
134
  );
140
135
  ctx.insert(
141
136
  Arc::from("createBufferSource"),
142
- Value::Function(Rc::new(|_| buffer_source_stub())),
137
+ Value::native(|_| buffer_source_stub()),
143
138
  );
144
139
  ctx.insert(
145
140
  Arc::from("createOscillator"),
146
- Value::Function(Rc::new(|_| oscillator_stub())),
141
+ Value::native(|_| oscillator_stub()),
147
142
  );
148
143
  ctx.insert(
149
144
  Arc::from("decodeAudioData"),
150
- Value::Function(Rc::new(|_| Value::Null)),
145
+ Value::native(|_| Value::Null),
151
146
  );
152
147
 
153
- Value::Object(Rc::new(RefCell::new(ctx)))
148
+ Value::Object(VmRef::new(ctx))
154
149
  }
155
150
 
156
151
  /// Global `Uint8Array` for native/VM: `new Uint8Array(n)` β†’ numeric array of zeros (not real bytes).
157
152
  pub fn uint8_array_constructor_value() -> Value {
158
- let ctor = Rc::new(|args: &[Value]| {
153
+ let ctor = Value::native(|args: &[Value]| {
159
154
  let len = args
160
155
  .first()
161
156
  .and_then(Value::as_number)
162
157
  .unwrap_or(0.0)
163
158
  .clamp(0.0, 1_000_000_000.0) as usize;
164
- Value::Array(Rc::new(RefCell::new(vec![Value::Number(0.0); len])))
159
+ Value::Array(VmRef::new(vec![Value::Number(0.0); len]))
165
160
  });
166
161
  let mut m = ObjectMap::default();
167
- m.insert(Arc::from(CONSTRUCT), Value::Function(ctor));
168
- Value::Object(Rc::new(RefCell::new(m)))
162
+ m.insert(Arc::from(CONSTRUCT), ctor);
163
+ Value::Object(VmRef::new(m))
169
164
  }
170
165
 
171
166
  /// Global `AudioContext` for native/VM: stub graph (no real audio).
172
167
  pub fn audio_context_constructor_value() -> Value {
173
- let ctor = Rc::new(|_args: &[Value]| audio_context_instance());
168
+ let ctor = Value::native(|_args: &[Value]| audio_context_instance());
174
169
  let mut m = ObjectMap::default();
175
- m.insert(Arc::from(CONSTRUCT), Value::Function(ctor));
176
- Value::Object(Rc::new(RefCell::new(m)))
170
+ m.insert(Arc::from(CONSTRUCT), ctor);
171
+ Value::Object(VmRef::new(m))
177
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};
@@ -16,29 +17,35 @@ pub fn boolean(args: &[Value]) -> Value {
16
17
 
17
18
  /// decodeURI(str)
18
19
  pub fn decode_uri(args: &[Value]) -> Value {
19
- let s = args.first().map(Value::to_display_string).unwrap_or_default();
20
+ let s = args
21
+ .first()
22
+ .map(Value::to_display_string)
23
+ .unwrap_or_default();
20
24
  Value::String(percent_decode(&s).unwrap_or(s).into())
21
25
  }
22
26
 
23
27
  /// encodeURI(str)
24
28
  pub fn encode_uri(args: &[Value]) -> Value {
25
- let s = args.first().map(Value::to_display_string).unwrap_or_default();
29
+ let s = args
30
+ .first()
31
+ .map(Value::to_display_string)
32
+ .unwrap_or_default();
26
33
  Value::String(percent_encode(&s).into())
27
34
  }
28
35
 
29
36
  /// isFinite(value)
30
37
  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())))
38
+ Value::Bool(
39
+ args.first()
40
+ .is_some_and(|v| matches!(v, Value::Number(n) if n.is_finite())),
41
+ )
32
42
  }
33
43
 
34
44
  /// isNaN(value)
35
45
  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
- )
46
+ Value::Bool(args.first().is_none_or(|v| {
47
+ matches!(v, Value::Number(n) if n.is_nan()) || !matches!(v, Value::Number(_))
48
+ }))
42
49
  }
43
50
 
44
51
  /// Array.isArray(value)
@@ -72,9 +79,9 @@ pub fn object_keys(args: &[Value]) -> Value {
72
79
  .keys()
73
80
  .map(|k| Value::String(Arc::clone(k)))
74
81
  .collect();
75
- Value::Array(Rc::new(RefCell::new(keys)))
82
+ Value::Array(VmRef::new(keys))
76
83
  } else {
77
- Value::Array(Rc::new(RefCell::new(Vec::new())))
84
+ Value::Array(VmRef::new(Vec::new()))
78
85
  }
79
86
  }
80
87
 
@@ -83,9 +90,9 @@ pub fn object_values(args: &[Value]) -> Value {
83
90
  if let Some(Value::Object(obj)) = args.first() {
84
91
  let obj_borrow = obj.borrow();
85
92
  let values: Vec<Value> = obj_borrow.values().cloned().collect();
86
- Value::Array(Rc::new(RefCell::new(values)))
93
+ Value::Array(VmRef::new(values))
87
94
  } else {
88
- Value::Array(Rc::new(RefCell::new(Vec::new())))
95
+ Value::Array(VmRef::new(Vec::new()))
89
96
  }
90
97
  }
91
98
 
@@ -96,15 +103,15 @@ pub fn object_entries(args: &[Value]) -> Value {
96
103
  let entries: Vec<Value> = obj_borrow
97
104
  .iter()
98
105
  .map(|(k, v)| {
99
- Value::Array(Rc::new(RefCell::new(vec![
106
+ Value::Array(VmRef::new(vec![
100
107
  Value::String(Arc::clone(k)),
101
108
  v.clone(),
102
- ])))
109
+ ]))
103
110
  })
104
111
  .collect();
105
- Value::Array(Rc::new(RefCell::new(entries)))
112
+ Value::Array(VmRef::new(entries))
106
113
  } else {
107
- Value::Array(Rc::new(RefCell::new(Vec::new())))
114
+ Value::Array(VmRef::new(Vec::new()))
108
115
  }
109
116
  }
110
117
 
@@ -139,17 +146,23 @@ pub fn object_assign(args: &[Value]) -> Value {
139
146
  }
140
147
  }
141
148
  drop(target_mut);
142
- Value::Object(Rc::clone(target))
149
+ Value::Object(target.clone())
143
150
  }
144
151
 
145
152
  /// parseInt(string, radix?)
146
153
  pub fn parse_int(args: &[Value]) -> Value {
147
- let s = args.first().map(Value::to_display_string).unwrap_or_default();
154
+ let s = args
155
+ .first()
156
+ .map(Value::to_display_string)
157
+ .unwrap_or_default();
148
158
  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);
159
+ let radix = args
160
+ .get(1)
161
+ .and_then(|v| match v {
162
+ Value::Number(n) => Some(*n as i32),
163
+ _ => None,
164
+ })
165
+ .unwrap_or(10);
153
166
 
154
167
  if (2..=36).contains(&radix) {
155
168
  let prefix: String = s
@@ -165,7 +178,10 @@ pub fn parse_int(args: &[Value]) -> Value {
165
178
 
166
179
  /// parseFloat(string)
167
180
  pub fn parse_float(args: &[Value]) -> Value {
168
- let s = args.first().map(Value::to_display_string).unwrap_or_default();
181
+ let s = args
182
+ .first()
183
+ .map(Value::to_display_string)
184
+ .unwrap_or_default();
169
185
  Value::Number(s.trim().parse().unwrap_or(f64::NAN))
170
186
  }
171
187
 
@@ -188,8 +204,8 @@ pub fn object_from_entries(args: &[Value]) -> Value {
188
204
  }
189
205
  }
190
206
 
191
- Value::Object(Rc::new(RefCell::new(obj)))
207
+ Value::Object(VmRef::new(obj))
192
208
  } else {
193
- Value::Object(Rc::new(RefCell::new(ObjectMap::default())))
209
+ Value::Object(VmRef::new(ObjectMap::default()))
194
210
  }
195
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.
@@ -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 })
@@ -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.
@@ -3,11 +3,12 @@
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;
7
+ use tishlang_core::VmRef;
6
8
  use std::cell::RefCell;
7
9
  use std::rc::Rc;
8
10
  use std::sync::Arc;
9
11
  use tishlang_core::Value;
10
- use crate::helpers::normalize_index;
11
12
 
12
13
  /// Byte offset -> character index.
13
14
  fn byte_to_char_index(s: &str, byte_offset: usize) -> usize {
@@ -152,7 +153,10 @@ pub fn slice(s: &Value, start: &Value, end: &Value) -> Value {
152
153
  if let Value::String(s) = s {
153
154
  let chars: Vec<char> = s.chars().collect();
154
155
  let len = chars.len() as i64;
155
- let (si, ei) = (normalize_index(start, len, 0), normalize_index(end, len, len as usize));
156
+ let (si, ei) = (
157
+ normalize_index(start, len, 0),
158
+ normalize_index(end, len, len as usize),
159
+ );
156
160
  let result: String = if si < ei {
157
161
  chars[si..ei].iter().collect()
158
162
  } else {
@@ -191,12 +195,13 @@ pub fn split(s: &Value, sep: &Value) -> Value {
191
195
  if let Value::String(s) = s {
192
196
  let separator = match sep {
193
197
  Value::String(ss) => ss.as_ref(),
194
- _ => 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))])),
195
199
  };
196
- let parts: Vec<Value> = s.split(separator)
200
+ let parts: Vec<Value> = s
201
+ .split(separator)
197
202
  .map(|p| Value::String(p.into()))
198
203
  .collect();
199
- Value::Array(Rc::new(RefCell::new(parts)))
204
+ Value::Array(VmRef::new(parts))
200
205
  } else {
201
206
  Value::Null
202
207
  }
@@ -244,9 +249,19 @@ pub fn ends_with(s: &Value, search: &Value) -> Value {
244
249
 
245
250
  fn replace_impl(s: &Value, search: &Value, replacement: &Value, all: bool) -> Value {
246
251
  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) };
252
+ let search_str = match search {
253
+ Value::String(ss) => ss.as_ref(),
254
+ _ => return Value::String(Arc::clone(s)),
255
+ };
256
+ let repl_str = match replacement {
257
+ Value::String(ss) => ss.as_ref(),
258
+ _ => "",
259
+ };
260
+ let result = if all {
261
+ s.replace(search_str, repl_str)
262
+ } else {
263
+ s.replacen(search_str, repl_str, 1)
264
+ };
250
265
  Value::String(result.into())
251
266
  } else {
252
267
  Value::Null
@@ -261,14 +276,66 @@ pub fn replace_all(s: &Value, search: &Value, replacement: &Value) -> Value {
261
276
  replace_impl(s, search, replacement, true)
262
277
  }
263
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
+
264
326
  fn char_at_idx(s: &str, idx: usize) -> Option<char> {
265
327
  s.chars().nth(idx)
266
328
  }
267
329
 
268
330
  pub fn char_at(s: &Value, idx: &Value) -> Value {
269
331
  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()))
332
+ let idx = match idx {
333
+ Value::Number(n) => *n as usize,
334
+ _ => 0,
335
+ };
336
+ char_at_idx(s, idx)
337
+ .map(|c| Value::String(c.to_string().into()))
338
+ .unwrap_or(Value::String("".into()))
272
339
  } else {
273
340
  Value::Null
274
341
  }
@@ -276,8 +343,13 @@ pub fn char_at(s: &Value, idx: &Value) -> Value {
276
343
 
277
344
  pub fn char_code_at(s: &Value, idx: &Value) -> Value {
278
345
  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))
346
+ let idx = match idx {
347
+ Value::Number(n) => *n as usize,
348
+ _ => 0,
349
+ };
350
+ char_at_idx(s, idx)
351
+ .map(|c| Value::Number(c as u32 as f64))
352
+ .unwrap_or(Value::Number(f64::NAN))
281
353
  } else {
282
354
  Value::Null
283
355
  }
@@ -311,7 +383,11 @@ fn pad_impl(s: &Value, target_len: &Value, pad: &Value, at_start: bool) -> Value
311
383
  }
312
384
  let needed = target_len - char_count;
313
385
  let padding: String = pad_str.chars().cycle().take(needed).collect();
314
- let result = if at_start { format!("{}{}", padding, s) } else { format!("{}{}", s, padding) };
386
+ let result = if at_start {
387
+ format!("{}{}", padding, s)
388
+ } else {
389
+ format!("{}{}", s, padding)
390
+ };
315
391
  Value::String(result.into())
316
392
  } else {
317
393
  Value::Null
@@ -382,16 +458,31 @@ mod tests {
382
458
  fn includes_basic() {
383
459
  assert_same!(includes(&s("hello"), &s("ll"), None), Value::Bool(true));
384
460
  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));
461
+ assert_same!(
462
+ includes(&s("hello"), &s("l"), Some(&n(3.0))),
463
+ Value::Bool(true)
464
+ );
465
+ assert_same!(
466
+ includes(&s("hello"), &s("l"), Some(&n(4.0))),
467
+ Value::Bool(false)
468
+ );
387
469
  }
388
470
 
389
471
  #[test]
390
472
  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));
473
+ assert_same!(
474
+ includes(&s("hello"), &s("o"), Some(&n(-1.0))),
475
+ Value::Bool(true)
476
+ );
477
+ assert_same!(
478
+ includes(&s("hello"), &s("h"), Some(&n(-5.0))),
479
+ Value::Bool(true)
480
+ );
393
481
  // 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));
482
+ assert_same!(
483
+ includes(&s("hi"), &s("h"), Some(&n(-1.0))),
484
+ Value::Bool(false)
485
+ );
395
486
  }
396
487
 
397
488
  #[test]
@@ -420,7 +511,7 @@ mod tests {
420
511
  assert_eq!(a.borrow().len(), 2);
421
512
  assert_same!(
422
513
  split(&s("x"), &n(1.0)),
423
- Value::Array(Rc::new(RefCell::new(vec![s("x")])))
514
+ Value::Array(VmRef::new(vec![s("x")]))
424
515
  );
425
516
  assert_same!(split(&n(1.0), &s(",")), Value::Null);
426
517
  assert_same!(trim(&s(" x ")), s("x"));
@@ -466,7 +557,10 @@ mod tests {
466
557
 
467
558
  #[test]
468
559
  fn last_index_of_basic() {
469
- assert_same!(last_index_of(&s("abcabc"), &s("a"), &n(f64::INFINITY)), n(3.0));
560
+ assert_same!(
561
+ last_index_of(&s("abcabc"), &s("a"), &n(f64::INFINITY)),
562
+ n(3.0)
563
+ );
470
564
  assert_same!(last_index_of(&s("abcabc"), &s("a"), &n(2.0)), n(0.0));
471
565
  assert_same!(last_index_of(&s("hello"), &s("l"), &n(3.0)), n(3.0));
472
566
  assert_same!(last_index_of(&s("hello"), &s("l"), &n(1.0)), n(-1.0));
@@ -490,12 +584,40 @@ mod tests {
490
584
 
491
585
  #[test]
492
586
  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));
587
+ assert_same!(
588
+ last_index_of(&s("πŸ˜€aπŸ˜€"), &s("a"), &n(f64::INFINITY)),
589
+ n(1.0)
590
+ );
591
+ assert_same!(
592
+ last_index_of(&s("πŸ˜€aπŸ˜€"), &s("πŸ˜€"), &n(f64::INFINITY)),
593
+ n(2.0)
594
+ );
495
595
  }
496
596
 
497
597
  #[test]
498
598
  fn last_index_of_non_string() {
499
599
  assert_same!(last_index_of(&n(1.0), &s("a"), &n(0.0)), n(-1.0));
500
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
+ }
501
623
  }