@tishlang/tish 1.13.2 → 2.0.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 (106) hide show
  1. package/Cargo.toml +2 -0
  2. package/bin/tish +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +1 -0
  4. package/crates/tish/Cargo.toml +11 -3
  5. package/crates/tish/build.rs +21 -0
  6. package/crates/tish/src/cli_help.rs +15 -4
  7. package/crates/tish/src/main.rs +93 -21
  8. package/crates/tish/src/repl_completion.rs +0 -1
  9. package/crates/tish/tests/error_source_location.rs +36 -0
  10. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  11. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  12. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  13. package/crates/tish/tests/integration_test.rs +402 -91
  14. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  15. package/crates/tish/tests/tty_capability.rs +43 -0
  16. package/crates/tish_ast/src/ast.rs +37 -8
  17. package/crates/tish_builtins/Cargo.toml +2 -0
  18. package/crates/tish_builtins/src/array.rs +375 -13
  19. package/crates/tish_builtins/src/collections.rs +481 -0
  20. package/crates/tish_builtins/src/construct.rs +59 -19
  21. package/crates/tish_builtins/src/date.rs +538 -0
  22. package/crates/tish_builtins/src/globals.rs +86 -6
  23. package/crates/tish_builtins/src/iterator.rs +129 -0
  24. package/crates/tish_builtins/src/lib.rs +5 -0
  25. package/crates/tish_builtins/src/number.rs +96 -0
  26. package/crates/tish_builtins/src/object.rs +2 -2
  27. package/crates/tish_builtins/src/string.rs +19 -20
  28. package/crates/tish_builtins/src/symbol.rs +1 -1
  29. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  30. package/crates/tish_bytecode/src/chunk.rs +69 -1
  31. package/crates/tish_bytecode/src/compiler.rs +933 -89
  32. package/crates/tish_bytecode/src/encoding.rs +2 -0
  33. package/crates/tish_bytecode/src/lib.rs +2 -1
  34. package/crates/tish_bytecode/src/opcode.rs +47 -4
  35. package/crates/tish_bytecode/src/serialize.rs +31 -1
  36. package/crates/tish_compile/Cargo.toml +1 -0
  37. package/crates/tish_compile/src/check.rs +774 -0
  38. package/crates/tish_compile/src/codegen.rs +2334 -349
  39. package/crates/tish_compile/src/infer.rs +1395 -6
  40. package/crates/tish_compile/src/lib.rs +50 -8
  41. package/crates/tish_compile/src/resolve.rs +584 -21
  42. package/crates/tish_compile/src/types.rs +106 -2
  43. package/crates/tish_compile_js/src/codegen.rs +67 -0
  44. package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
  45. package/crates/tish_core/Cargo.toml +7 -1
  46. package/crates/tish_core/src/console_style.rs +11 -1
  47. package/crates/tish_core/src/json.rs +81 -38
  48. package/crates/tish_core/src/lib.rs +3 -0
  49. package/crates/tish_core/src/shape.rs +85 -0
  50. package/crates/tish_core/src/value.rs +679 -25
  51. package/crates/tish_core/src/vmref.rs +13 -8
  52. package/crates/tish_cranelift/src/link.rs +17 -4
  53. package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
  54. package/crates/tish_eval/Cargo.toml +6 -0
  55. package/crates/tish_eval/src/eval.rs +665 -117
  56. package/crates/tish_eval/src/http.rs +4 -1
  57. package/crates/tish_eval/src/natives.rs +165 -13
  58. package/crates/tish_eval/src/value.rs +31 -13
  59. package/crates/tish_eval/src/value_convert.rs +10 -4
  60. package/crates/tish_ffi/Cargo.toml +26 -0
  61. package/crates/tish_ffi/src/lib.rs +518 -0
  62. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  63. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  64. package/crates/tish_ffi/tests/loader.rs +65 -0
  65. package/crates/tish_fmt/src/lib.rs +43 -5
  66. package/crates/tish_lexer/src/lib.rs +397 -9
  67. package/crates/tish_lexer/src/token.rs +7 -0
  68. package/crates/tish_lint/src/lib.rs +2 -10
  69. package/crates/tish_lsp/src/import_goto.rs +2 -0
  70. package/crates/tish_lsp/src/main.rs +439 -26
  71. package/crates/tish_native/src/build.rs +55 -1
  72. package/crates/tish_opt/src/lib.rs +126 -23
  73. package/crates/tish_parser/src/lib.rs +55 -1
  74. package/crates/tish_parser/src/parser.rs +456 -34
  75. package/crates/tish_pg/src/lib.rs +3 -3
  76. package/crates/tish_resolve/src/lib.rs +99 -59
  77. package/crates/tish_runtime/Cargo.toml +4 -0
  78. package/crates/tish_runtime/src/http.rs +66 -17
  79. package/crates/tish_runtime/src/http_fetch.rs +29 -8
  80. package/crates/tish_runtime/src/http_hyper.rs +25 -2
  81. package/crates/tish_runtime/src/lib.rs +299 -44
  82. package/crates/tish_runtime/src/promise.rs +328 -18
  83. package/crates/tish_runtime/src/timers.rs +13 -7
  84. package/crates/tish_runtime/src/tty.rs +226 -0
  85. package/crates/tish_runtime/src/ws.rs +35 -18
  86. package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
  87. package/crates/tish_ui/src/jsx.rs +10 -0
  88. package/crates/tish_ui/src/runtime/hooks.rs +19 -15
  89. package/crates/tish_ui/src/runtime/mod.rs +15 -12
  90. package/crates/tish_vm/Cargo.toml +14 -1
  91. package/crates/tish_vm/src/jit.rs +1050 -0
  92. package/crates/tish_vm/src/lib.rs +2 -0
  93. package/crates/tish_vm/src/vm.rs +1546 -202
  94. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  95. package/crates/tish_wasm/src/lib.rs +6 -2
  96. package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
  97. package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
  98. package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
  99. package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
  100. package/justfile +8 -0
  101. package/package.json +1 -1
  102. package/platform/darwin-arm64/tish +0 -0
  103. package/platform/darwin-x64/tish +0 -0
  104. package/platform/linux-arm64/tish +0 -0
  105. package/platform/linux-x64/tish +0 -0
  106. package/platform/win32-x64/tish.exe +0 -0
@@ -103,10 +103,13 @@ pub fn value_to_response(value: &Value) -> (u16, Vec<(String, String)>, String)
103
103
  (status, headers, body)
104
104
  }
105
105
 
106
+ /// `(status, headers, file path)` extracted from a response object's `file` key.
107
+ type FileResponse = (u16, Vec<(String, String)>, String);
108
+
106
109
  /// If the response value has a `file` key, stream that path (binary-safe). Matches `tishlang_runtime` HTTP behavior.
107
110
  pub(crate) fn extract_file_from_response(
108
111
  value: &Value,
109
- ) -> Option<(u16, Vec<(String, String)>, String)> {
112
+ ) -> Option<FileResponse> {
110
113
  let Value::Object(obj) = value else {
111
114
  return None;
112
115
  };
@@ -80,8 +80,8 @@ pub fn parse_int(args: &[Value]) -> Result<Value, String> {
80
80
 
81
81
  pub fn parse_float(args: &[Value]) -> Result<Value, String> {
82
82
  let s = args.first().map(|v| v.to_string()).unwrap_or_default();
83
- let n: f64 = s.trim().parse().unwrap_or(f64::NAN);
84
- Ok(Value::Number(n))
83
+ // JS parseFloat parses the longest leading numeric prefix (issue #36); shared with VM/native.
84
+ Ok(Value::Number(tishlang_builtins::globals::js_parse_float(&s)))
85
85
  }
86
86
 
87
87
  pub fn is_finite(args: &[Value]) -> Result<Value, String> {
@@ -215,10 +215,9 @@ pub fn math_round(args: &[Value]) -> Result<Value, String> {
215
215
  }
216
216
 
217
217
  pub fn math_random(_args: &[Value]) -> Result<Value, String> {
218
- use std::collections::hash_map::RandomState;
219
- use std::hash::{BuildHasher, Hasher};
220
- let random = RandomState::new().build_hasher().finish() as f64 / u64::MAX as f64;
221
- Ok(Value::Number(random))
218
+ // Match the VM / builtins (`rand::random::<f64>()`) — uniform [0,1). The old
219
+ // RandomState-hash was non-uniform and diverged from every other backend.
220
+ Ok(Value::Number(rand::random::<f64>()))
222
221
  }
223
222
 
224
223
  pub fn math_pow(args: &[Value]) -> Result<Value, String> {
@@ -277,19 +276,94 @@ pub fn math_trunc(args: &[Value]) -> Result<Value, String> {
277
276
  ))
278
277
  }
279
278
 
280
- pub fn date_now(_args: &[Value]) -> Result<Value, String> {
281
- use std::time::{SystemTime, UNIX_EPOCH};
282
- let ms = SystemTime::now()
283
- .duration_since(UNIX_EPOCH)
284
- .map(|d| d.as_millis() as f64)
285
- .unwrap_or(0.0);
286
- Ok(Value::Number(ms))
279
+ // Hyperbolic / inverse-hyperbolic / cbrt / base-2/10 logs — these were missing from the
280
+ // interpreter's `Math`, so they returned "Not a function" (issue #61, and an interp↔VM
281
+ // divergence under #67). One macro per unary `f64` method keeps them in lockstep with the VM.
282
+ macro_rules! math_unary {
283
+ ($name:ident, $method:ident) => {
284
+ pub fn $name(args: &[Value]) -> Result<Value, String> {
285
+ Ok(Value::Number(
286
+ get_num(args.first().unwrap_or(&Value::Null)).$method(),
287
+ ))
288
+ }
289
+ };
287
290
  }
291
+ math_unary!(math_sinh, sinh);
292
+ math_unary!(math_cosh, cosh);
293
+ math_unary!(math_tanh, tanh);
294
+ math_unary!(math_asinh, asinh);
295
+ math_unary!(math_acosh, acosh);
296
+ math_unary!(math_atanh, atanh);
297
+ math_unary!(math_cbrt, cbrt);
298
+ math_unary!(math_log2, log2);
299
+ math_unary!(math_log10, log10);
288
300
 
289
301
  pub fn array_is_array(args: &[Value]) -> Result<Value, String> {
290
302
  Ok(Value::Bool(matches!(args.first(), Some(Value::Array(_)))))
291
303
  }
292
304
 
305
+ /// Build a JS-style error object `{ name, message }` for the interpreter (issue #60).
306
+ fn make_error_obj(name: &str, args: &[Value]) -> Value {
307
+ let message = args.first().map(|v| v.to_string()).unwrap_or_default();
308
+ let mut m = crate::value::PropMap::with_capacity(2);
309
+ m.insert("name".into(), Value::String(name.into()));
310
+ m.insert("message".into(), Value::String(message.into()));
311
+ Value::object(m)
312
+ }
313
+
314
+ pub fn error_construct(args: &[Value]) -> Result<Value, String> {
315
+ Ok(make_error_obj("Error", args))
316
+ }
317
+ pub fn type_error_construct(args: &[Value]) -> Result<Value, String> {
318
+ Ok(make_error_obj("TypeError", args))
319
+ }
320
+ pub fn range_error_construct(args: &[Value]) -> Result<Value, String> {
321
+ Ok(make_error_obj("RangeError", args))
322
+ }
323
+ pub fn syntax_error_construct(args: &[Value]) -> Result<Value, String> {
324
+ Ok(make_error_obj("SyntaxError", args))
325
+ }
326
+
327
+ /// `Array(...)` / `new Array(...)` (issue #72) — mirrors `tishlang_builtins::construct::
328
+ /// array_construct` for the interpreter's `Value`. A single non-negative integer is a length
329
+ /// (null holes); other args become the array's elements.
330
+ pub fn array_construct(args: &[Value]) -> Result<Value, String> {
331
+ if let [Value::Number(n)] = args {
332
+ let n = *n;
333
+ if n >= 0.0 && n.fract() == 0.0 && n <= 4_294_967_295.0 {
334
+ return Ok(Value::array(vec![Value::Null; n as usize]));
335
+ }
336
+ }
337
+ Ok(Value::array(args.to_vec()))
338
+ }
339
+
340
+ /// `String(value)` as a function: JS `ToString` coercion (arrays comma-join recursively,
341
+ /// objects → "[object Object]", null → "null"), matching the VM/native `string_convert`.
342
+ pub fn string_convert(args: &[Value]) -> Result<Value, String> {
343
+ let v = args.first().unwrap_or(&Value::Null);
344
+ Ok(Value::String(v.to_js_string().into()))
345
+ }
346
+
347
+ /// `Number(value)` coercion (issue #36) — shares the string parser with the VM/native
348
+ /// backend so the result is byte-identical.
349
+ pub fn number_convert(args: &[Value]) -> Result<Value, String> {
350
+ let v = args.first().unwrap_or(&Value::Null);
351
+ let n = match v {
352
+ Value::Number(n) => *n,
353
+ Value::Bool(b) => {
354
+ if *b {
355
+ 1.0
356
+ } else {
357
+ 0.0
358
+ }
359
+ }
360
+ Value::Null => 0.0,
361
+ Value::String(s) => tishlang_builtins::globals::parse_numeric_string(s),
362
+ other => tishlang_builtins::globals::parse_numeric_string(&other.to_js_string()),
363
+ };
364
+ Ok(Value::Number(n))
365
+ }
366
+
293
367
  pub fn string_from_char_code(args: &[Value]) -> Result<Value, String> {
294
368
  let s: String = args
295
369
  .iter()
@@ -397,3 +471,81 @@ pub fn mkdir(args: &[Value]) -> Result<Value, String> {
397
471
  };
398
472
  Ok(Value::Bool(result.is_ok()))
399
473
  }
474
+
475
+ // ── Interactive terminal I/O (issue #101), behind the `tty` feature ──────────────────────
476
+ // Build the interpreter's `Value` from the shared, Value-agnostic core in
477
+ // `tishlang_runtime::tty`, so the interpreter, VM, and native backends behave identically.
478
+
479
+ #[cfg(feature = "tty")]
480
+ fn tty_obj(pairs: Vec<(&str, Value)>) -> Value {
481
+ let mut m = crate::value::PropMap::with_capacity(pairs.len());
482
+ for (k, v) in pairs {
483
+ m.insert(k.into(), v);
484
+ }
485
+ Value::object(m)
486
+ }
487
+
488
+ #[cfg(feature = "tty")]
489
+ pub fn tty_size(_args: &[Value]) -> Result<Value, String> {
490
+ Ok(match tishlang_runtime::tty::size() {
491
+ Some((cols, rows)) => tty_obj(vec![
492
+ ("cols", Value::Number(cols as f64)),
493
+ ("rows", Value::Number(rows as f64)),
494
+ ]),
495
+ None => Value::Null,
496
+ })
497
+ }
498
+
499
+ #[cfg(feature = "tty")]
500
+ pub fn tty_is_tty(_args: &[Value]) -> Result<Value, String> {
501
+ Ok(Value::Bool(tishlang_runtime::tty::is_tty()))
502
+ }
503
+
504
+ #[cfg(feature = "tty")]
505
+ pub fn tty_set_raw_mode(args: &[Value]) -> Result<Value, String> {
506
+ let on = args.first().map(|v| v.is_truthy()).unwrap_or(false);
507
+ Ok(Value::Bool(tishlang_runtime::tty::set_raw_mode(on)))
508
+ }
509
+
510
+ #[cfg(feature = "tty")]
511
+ pub fn tty_enter_alt_screen(_args: &[Value]) -> Result<Value, String> {
512
+ Ok(Value::Bool(tishlang_runtime::tty::enter_alt_screen()))
513
+ }
514
+
515
+ #[cfg(feature = "tty")]
516
+ pub fn tty_leave_alt_screen(_args: &[Value]) -> Result<Value, String> {
517
+ Ok(Value::Bool(tishlang_runtime::tty::leave_alt_screen()))
518
+ }
519
+
520
+ #[cfg(feature = "tty")]
521
+ pub fn tty_read_line(_args: &[Value]) -> Result<Value, String> {
522
+ Ok(match tishlang_runtime::tty::read_line() {
523
+ Some(s) => Value::String(s.into()),
524
+ None => Value::Null,
525
+ })
526
+ }
527
+
528
+ #[cfg(feature = "tty")]
529
+ pub fn tty_read(args: &[Value]) -> Result<Value, String> {
530
+ use tishlang_runtime::tty::TtyEvent;
531
+ let timeout = match args.first() {
532
+ Some(Value::Number(ms)) => Some(ms.max(0.0) as u64),
533
+ _ => None,
534
+ };
535
+ Ok(match tishlang_runtime::tty::read_event(timeout) {
536
+ Some(TtyEvent::Key { key, ctrl, alt, shift }) => tty_obj(vec![
537
+ ("type", Value::String("key".into())),
538
+ ("key", Value::String(key.into())),
539
+ ("ctrl", Value::Bool(ctrl)),
540
+ ("alt", Value::Bool(alt)),
541
+ ("shift", Value::Bool(shift)),
542
+ ]),
543
+ Some(TtyEvent::Resize { cols, rows }) => tty_obj(vec![
544
+ ("type", Value::String("resize".into())),
545
+ ("cols", Value::Number(cols as f64)),
546
+ ("rows", Value::Number(rows as f64)),
547
+ ]),
548
+ Some(TtyEvent::Other) => tty_obj(vec![("type", Value::String("other".into()))]),
549
+ None => Value::Null,
550
+ })
551
+ }
@@ -15,7 +15,9 @@ use tishlang_core::NativeFn as CoreNativeFn;
15
15
  use tishlang_core::TishSymbol;
16
16
 
17
17
  /// Property map for interpreter object string keys (uses `eval::Value`, not `tishlang_core::Value`).
18
- pub type PropMap = AHashMap<Arc<str>, Value>;
18
+ /// `IndexMap` preserves insertion order so `Object.keys` / `JSON.stringify` match JS/Node
19
+ /// (and the VM/rust backends, which use `tishlang_core`'s insertion-ordered `PropMap`).
20
+ pub type PropMap = indexmap::IndexMap<Arc<str>, Value>;
19
21
 
20
22
  /// Interpreter object: string keys plus optional symbol-keyed properties.
21
23
  #[derive(Clone, Debug, Default)]
@@ -110,11 +112,14 @@ pub enum Value {
110
112
  Array(Rc<RefCell<Vec<Value>>>),
111
113
  Object(Rc<RefCell<EvalObjectData>>),
112
114
  Symbol(Arc<TishSymbol>),
113
- /// User-defined function with AST body
115
+ /// User-defined function with AST body. `env` is the lexical scope captured at definition
116
+ /// time, so the body resolves free variables against where it was DEFINED (a real closure),
117
+ /// not where it is called.
114
118
  Function {
115
119
  formals: Arc<[FunParam]>,
116
120
  rest_param: Option<Arc<str>>,
117
121
  body: Arc<Statement>,
122
+ env: crate::eval::ScopeRef,
118
123
  },
119
124
  /// Native/builtin function
120
125
  Native(NativeFn),
@@ -192,17 +197,9 @@ impl std::fmt::Debug for Value {
192
197
  impl std::fmt::Display for Value {
193
198
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194
199
  match self {
195
- Value::Number(n) => {
196
- if n.is_nan() {
197
- write!(f, "NaN")
198
- } else if *n == f64::INFINITY {
199
- write!(f, "Infinity")
200
- } else if *n == f64::NEG_INFINITY {
201
- write!(f, "-Infinity")
202
- } else {
203
- write!(f, "{}", n)
204
- }
205
- }
200
+ // Match JS `Number.prototype.toString` (exponential past digit 21 / before −6),
201
+ // shared with the VM/native path via `tishlang_core`.
202
+ Value::Number(n) => write!(f, "{}", tishlang_core::js_number_to_string(*n)),
206
203
  Value::String(s) => write!(f, "{}", s),
207
204
  Value::Bool(b) => write!(f, "{}", b),
208
205
  Value::Null => write!(f, "null"),
@@ -265,6 +262,27 @@ impl Value {
265
262
  }
266
263
  }
267
264
 
265
+ /// JavaScript `ToString` coercion (as used by `Array.prototype.join`), distinct from the
266
+ /// `Display`/inspect form: a nested **array** stringifies to its own comma-joined `toString`
267
+ /// (recursively, always `,`), an **object** becomes `"[object Object]"`, and `null`/`undefined`
268
+ /// elements elide to `""`. Mirrors `tishlang_core::Value::to_js_string` so interp output matches
269
+ /// the VM/rust/cranelift/wasi backends (and Node) for join/coercion.
270
+ pub fn to_js_string(&self) -> String {
271
+ match self {
272
+ Value::Array(arr) => arr
273
+ .borrow()
274
+ .iter()
275
+ .map(|v| match v {
276
+ Value::Null => String::new(),
277
+ other => other.to_js_string(),
278
+ })
279
+ .collect::<Vec<_>>()
280
+ .join(","),
281
+ Value::Object(_) => "[object Object]".to_string(),
282
+ _ => self.to_string(),
283
+ }
284
+ }
285
+
268
286
  pub fn strict_eq(&self, other: &Value) -> bool {
269
287
  match (self, other) {
270
288
  (Value::Number(a), Value::Number(b)) => {
@@ -7,7 +7,7 @@ use std::sync::Arc;
7
7
  #[cfg(feature = "regex")]
8
8
  use tishlang_core::TishRegExp;
9
9
  use ahash::AHashMap;
10
- use tishlang_core::{ObjectData, ObjectMap, Value as CoreValue, VmRef};
10
+ use tishlang_core::{ObjectData, Value as CoreValue, VmRef};
11
11
 
12
12
  use crate::value::{EvalObjectData, PropMap, Value};
13
13
 
@@ -15,7 +15,7 @@ use crate::value::{EvalObjectData, PropMap, Value};
15
15
  pub fn eval_to_core(v: &Value) -> Result<CoreValue, String> {
16
16
  match v {
17
17
  Value::Number(n) => Ok(CoreValue::Number(*n)),
18
- Value::String(s) => Ok(CoreValue::String(Arc::clone(s))),
18
+ Value::String(s) => Ok(CoreValue::String(tishlang_core::ArcStr::from(s.as_ref()))),
19
19
  Value::Bool(b) => Ok(CoreValue::Bool(*b)),
20
20
  Value::Null => Ok(CoreValue::Null),
21
21
  Value::Array(arr) => {
@@ -27,7 +27,7 @@ pub fn eval_to_core(v: &Value) -> Result<CoreValue, String> {
27
27
  }
28
28
  Value::Object(map) => {
29
29
  let b = map.borrow();
30
- let mut strings = ObjectMap::default();
30
+ let mut strings = tishlang_core::PropMap::default();
31
31
  for (k, v) in b.strings.iter() {
32
32
  strings.insert(Arc::clone(k), eval_to_core(v)?);
33
33
  }
@@ -55,7 +55,7 @@ pub fn eval_to_core(v: &Value) -> Result<CoreValue, String> {
55
55
  pub fn core_to_eval(v: CoreValue) -> Value {
56
56
  match v {
57
57
  CoreValue::Number(n) => Value::Number(n),
58
- CoreValue::String(s) => Value::String(s),
58
+ CoreValue::String(s) => Value::String(Arc::from(s.as_str())),
59
59
  CoreValue::Bool(b) => Value::Bool(b),
60
60
  CoreValue::Null => Value::Null,
61
61
  CoreValue::Array(arr) => {
@@ -87,6 +87,12 @@ pub fn core_to_eval(v: CoreValue) -> Value {
87
87
  CoreValue::Promise(p) => Value::CorePromise(Arc::clone(&p)),
88
88
  #[cfg(not(feature = "http"))]
89
89
  CoreValue::Promise(_) => Value::Null,
90
+ // NumberArray: materialize to boxed Array for the interpreter (it has no packed path).
91
+ CoreValue::NumberArray(arr) => {
92
+ let nums = arr.borrow();
93
+ let out: Vec<Value> = nums.iter().map(|&n| Value::Number(n)).collect();
94
+ Value::Array(Rc::new(RefCell::new(out)))
95
+ }
90
96
  // `CoreNativeFn` is feature-gated (Rc vs Arc), so use Clone::clone
91
97
  // which works for either.
92
98
  CoreValue::Function(f) => Value::CoreFn(f.clone()),
@@ -0,0 +1,26 @@
1
+ [package]
2
+ name = "tishlang_ffi"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "Stable C ABI for tish native extensions — opaque value handles + extern \"C\" accessors so a cdylib (native) or wasm host-import module can interoperate with any tish backend without sharing tish_core's Rust types. Workstream B (full-backend-parity-plan.md)."
6
+
7
+ license-file = { workspace = true }
8
+ repository = { workspace = true }
9
+
10
+ [features]
11
+ default = []
12
+ # An extension that LINKS tishlang_core must match the host's value representation/layout exactly.
13
+ # `send-values` flips arrays/objects to Arc<Mutex<…>>; `regex` adds the `Value::RegExp` variant
14
+ # (which changes the enum's niche/discriminant layout). The shipped `tish` (default="full") enables
15
+ # both, so a linked extension targeting it must too. (The decoupled, host-exported-accessor model
16
+ # avoids all of this — there is then a single tish_core.)
17
+ send-values = ["tishlang_core/send-values"]
18
+ regex = ["tishlang_core/regex"]
19
+
20
+ [dependencies]
21
+ tishlang_core = { path = "../tish_core", version = ">=0.1" }
22
+
23
+ # Native cdylib loading (B2). Not for wasm — there is no dlopen in wasm; wasi resolves the SAME
24
+ # handle+accessor contract via host imports (B4), so the loader is gated to non-wasm targets.
25
+ [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
26
+ libloading = "0.8"