@tishlang/tish-format 1.0.13 → 2.0.2

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 (108) hide show
  1. package/Cargo.toml +2 -0
  2. package/bin/tish-format +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +1 -0
  4. package/crates/tish/Cargo.toml +10 -2
  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/Cargo.toml +1 -1
  66. package/crates/tish_fmt/src/lib.rs +61 -5
  67. package/crates/tish_lexer/src/lib.rs +397 -9
  68. package/crates/tish_lexer/src/token.rs +7 -0
  69. package/crates/tish_lint/src/lib.rs +2 -10
  70. package/crates/tish_lsp/src/import_goto.rs +2 -0
  71. package/crates/tish_lsp/src/main.rs +439 -26
  72. package/crates/tish_native/src/build.rs +55 -1
  73. package/crates/tish_opt/src/lib.rs +126 -23
  74. package/crates/tish_parser/src/lib.rs +55 -1
  75. package/crates/tish_parser/src/parser.rs +456 -34
  76. package/crates/tish_pg/src/lib.rs +3 -3
  77. package/crates/tish_resolve/src/lib.rs +99 -59
  78. package/crates/tish_runtime/Cargo.toml +4 -0
  79. package/crates/tish_runtime/src/http.rs +66 -17
  80. package/crates/tish_runtime/src/http_fetch.rs +29 -8
  81. package/crates/tish_runtime/src/http_hyper.rs +25 -2
  82. package/crates/tish_runtime/src/lib.rs +299 -44
  83. package/crates/tish_runtime/src/promise.rs +328 -18
  84. package/crates/tish_runtime/src/timers.rs +13 -7
  85. package/crates/tish_runtime/src/tty.rs +226 -0
  86. package/crates/tish_runtime/src/ws.rs +35 -18
  87. package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
  88. package/crates/tish_ui/src/jsx.rs +10 -0
  89. package/crates/tish_ui/src/runtime/hooks.rs +19 -15
  90. package/crates/tish_ui/src/runtime/mod.rs +15 -12
  91. package/crates/tish_vm/Cargo.toml +14 -1
  92. package/crates/tish_vm/src/jit.rs +1050 -0
  93. package/crates/tish_vm/src/lib.rs +2 -0
  94. package/crates/tish_vm/src/vm.rs +1546 -202
  95. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  96. package/crates/tish_wasm/src/lib.rs +6 -2
  97. package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
  98. package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
  99. package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
  100. package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
  101. package/justfile +8 -0
  102. package/package.json +2 -2
  103. package/platform/darwin-arm64/tish-fmt +0 -0
  104. package/platform/darwin-x64/tish-fmt +0 -0
  105. package/platform/linux-arm64/tish-fmt +0 -0
  106. package/platform/linux-x64/tish-fmt +0 -0
  107. package/platform/win32-x64/tish-fmt.exe +0 -0
  108. package/README.md +0 -138
@@ -0,0 +1,518 @@
1
+ //! # tish FFI — the stable C ABI for native extensions (Workstream B / B1)
2
+ //!
3
+ //! `tish_core::Value` is a non-`#[repr(C)]` Rust enum, so it cannot cross an `extern "C"`
4
+ //! boundary by value, and a native extension compiled separately can't name its type
5
+ //! ("conflicting Value types"). This crate defines the **opaque-handle + accessor** ABI that
6
+ //! decouples extensions from `tish_core`:
7
+ //!
8
+ //! - A [`TishValueRef`] is an opaque handle (a boxed `Value` behind a `*mut c_void`). An
9
+ //! extension only ever sees the pointer, never the layout.
10
+ //! - The host exposes the `extern "C"` accessors below (`tish_value_*`); an extension *imports*
11
+ //! them. A native extension is then a **cdylib** whose exports match [`TishNativeFn`], and the
12
+ //! exact same contract is satisfied on wasm by **host imports** — one ABI, two bindings
13
+ //! (B4). This is what lets cranelift/llvm/wasi load native extensions without sharing a Rust
14
+ //! compilation; only `cargo:` (Rust-crate compile-time linking) stays rust-AOT-only.
15
+ //!
16
+ //! Ownership rule (C-style): every handle a caller *receives* from a `_new_*` / `_get` / `_clone`
17
+ //! accessor is owned by the caller and must be released with [`tish_value_drop`]; `_push`/`_set`
18
+ //! **clone** their argument (the caller keeps owning it). Strings returned by
19
+ //! [`tish_value_as_string`] are owned by the caller and freed with [`tish_string_free`].
20
+ //!
21
+ //! **Status:** B1 (this crate) is the ABI + host accessors, unit-tested below. The loader
22
+ //! (`libloading` cdylib / wasm host imports) and per-backend `ffi:` wiring are B2–B4.
23
+
24
+ use std::ffi::{c_char, c_void, CStr, CString};
25
+
26
+ use tishlang_core::{ObjectData, Value, VmRef};
27
+
28
+ /// Opaque handle to a tish value. Internally `*mut Value` (a leaked `Box`); never inspect or
29
+ /// free it except through the accessors. `null` is a valid "no value" sentinel for fallible
30
+ /// accessors (e.g. `tish_value_object_get` on a missing key returns a fresh null handle, not
31
+ /// a null pointer — but defensive code treats a null pointer as `Value::Null`).
32
+ pub type TishValueRef = *mut c_void;
33
+
34
+ /// Value kind tags returned by [`tish_value_tag`]. Stable across versions.
35
+ pub const TISH_TAG_NULL: i32 = 0;
36
+ pub const TISH_TAG_NUMBER: i32 = 1;
37
+ pub const TISH_TAG_STRING: i32 = 2;
38
+ pub const TISH_TAG_BOOL: i32 = 3;
39
+ pub const TISH_TAG_ARRAY: i32 = 4;
40
+ pub const TISH_TAG_OBJECT: i32 = 5;
41
+ /// Anything the C ABI doesn't model (Function/Promise/RegExp/Symbol/…). Opaque to extensions.
42
+ pub const TISH_TAG_OTHER: i32 = 6;
43
+
44
+ /// An `extern "C"` native function: receives a borrowed array of argument handles (owned by the
45
+ /// host for the call) and returns a freshly-owned result handle (the host drops it).
46
+ pub type TishNativeFn =
47
+ extern "C" fn(args: *const TishValueRef, argc: usize) -> TishValueRef;
48
+
49
+ /// One named export in a module's table.
50
+ #[repr(C)]
51
+ pub struct TishExport {
52
+ /// NUL-terminated function name.
53
+ pub name: *const c_char,
54
+ pub func: TishNativeFn,
55
+ }
56
+
57
+ /// What a module's `#[no_mangle] extern "C" fn tish_module_register() -> *const TishExportTable`
58
+ /// returns: a static name→fn table the host registers as a native module.
59
+ #[repr(C)]
60
+ pub struct TishExportTable {
61
+ pub exports: *const TishExport,
62
+ pub count: usize,
63
+ }
64
+
65
+ // ── handle <-> Value plumbing ────────────────────────────────────────────────
66
+ #[inline]
67
+ fn box_value(v: Value) -> TishValueRef {
68
+ Box::into_raw(Box::new(v)) as TishValueRef
69
+ }
70
+
71
+ /// Borrow the `Value` behind a handle; `None` for a null pointer (treated as `Value::Null`).
72
+ ///
73
+ /// # Safety
74
+ /// `r` must be null or a handle returned by an accessor and not yet dropped.
75
+ #[inline]
76
+ unsafe fn as_value<'a>(r: TishValueRef) -> Option<&'a Value> {
77
+ (r as *const Value).as_ref()
78
+ }
79
+
80
+ // ── constructors ─────────────────────────────────────────────────────────────
81
+ #[no_mangle]
82
+ pub extern "C" fn tish_value_new_number(n: f64) -> TishValueRef {
83
+ box_value(Value::Number(n))
84
+ }
85
+
86
+ #[no_mangle]
87
+ pub extern "C" fn tish_value_new_bool(b: bool) -> TishValueRef {
88
+ box_value(Value::Bool(b))
89
+ }
90
+
91
+ #[no_mangle]
92
+ pub extern "C" fn tish_value_new_null() -> TishValueRef {
93
+ box_value(Value::Null)
94
+ }
95
+
96
+ /// Build a string value from a NUL-terminated UTF-8 C string. Returns a null value handle if
97
+ /// `s` is null or not valid UTF-8.
98
+ ///
99
+ /// # Safety
100
+ /// `s` must be null or point to a valid NUL-terminated C string.
101
+ #[no_mangle]
102
+ pub unsafe extern "C" fn tish_value_new_string(s: *const c_char) -> TishValueRef {
103
+ if s.is_null() {
104
+ return box_value(Value::Null);
105
+ }
106
+ match CStr::from_ptr(s).to_str() {
107
+ Ok(st) => box_value(Value::String(st.into())),
108
+ Err(_) => box_value(Value::Null),
109
+ }
110
+ }
111
+
112
+ // ── tag + scalar readers ─────────────────────────────────────────────────────
113
+ /// # Safety
114
+ /// `r` must be null or a live handle.
115
+ #[no_mangle]
116
+ pub unsafe extern "C" fn tish_value_tag(r: TishValueRef) -> i32 {
117
+ match as_value(r) {
118
+ None | Some(Value::Null) => TISH_TAG_NULL,
119
+ Some(Value::Number(_)) => TISH_TAG_NUMBER,
120
+ Some(Value::String(_)) => TISH_TAG_STRING,
121
+ Some(Value::Bool(_)) => TISH_TAG_BOOL,
122
+ Some(Value::Array(_)) => TISH_TAG_ARRAY,
123
+ Some(Value::Object(_)) => TISH_TAG_OBJECT,
124
+ Some(_) => TISH_TAG_OTHER,
125
+ }
126
+ }
127
+
128
+ /// Number value, or `NaN` if the handle isn't a number.
129
+ /// # Safety
130
+ /// `r` must be null or a live handle.
131
+ #[no_mangle]
132
+ pub unsafe extern "C" fn tish_value_as_number(r: TishValueRef) -> f64 {
133
+ match as_value(r) {
134
+ Some(Value::Number(n)) => *n,
135
+ _ => f64::NAN,
136
+ }
137
+ }
138
+
139
+ /// Bool value, or `false` if the handle isn't a bool.
140
+ /// # Safety
141
+ /// `r` must be null or a live handle.
142
+ #[no_mangle]
143
+ pub unsafe extern "C" fn tish_value_as_bool(r: TishValueRef) -> bool {
144
+ matches!(as_value(r), Some(Value::Bool(true)))
145
+ }
146
+
147
+ /// Newly-allocated NUL-terminated copy of a string value (caller frees with
148
+ /// [`tish_string_free`]); null pointer if the handle isn't a string (or contains an interior NUL).
149
+ /// # Safety
150
+ /// `r` must be null or a live handle.
151
+ #[no_mangle]
152
+ pub unsafe extern "C" fn tish_value_as_string(r: TishValueRef) -> *mut c_char {
153
+ match as_value(r) {
154
+ Some(Value::String(s)) => match CString::new(s.as_bytes()) {
155
+ Ok(c) => c.into_raw(),
156
+ Err(_) => std::ptr::null_mut(),
157
+ },
158
+ _ => std::ptr::null_mut(),
159
+ }
160
+ }
161
+
162
+ /// Free a string returned by [`tish_value_as_string`].
163
+ /// # Safety
164
+ /// `s` must be null or a pointer from `tish_value_as_string`, freed once.
165
+ #[no_mangle]
166
+ pub unsafe extern "C" fn tish_string_free(s: *mut c_char) {
167
+ if !s.is_null() {
168
+ drop(CString::from_raw(s));
169
+ }
170
+ }
171
+
172
+ // ── arrays ───────────────────────────────────────────────────────────────────
173
+ #[no_mangle]
174
+ pub extern "C" fn tish_value_array_new() -> TishValueRef {
175
+ box_value(Value::Array(VmRef::new(Vec::new())))
176
+ }
177
+
178
+ /// Append a **clone** of `elem` to array `arr` (the caller keeps owning `elem`). No-op if `arr`
179
+ /// isn't an array.
180
+ /// # Safety
181
+ /// both must be null or live handles.
182
+ #[no_mangle]
183
+ pub unsafe extern "C" fn tish_value_array_push(arr: TishValueRef, elem: TishValueRef) {
184
+ if let Some(Value::Array(a)) = as_value(arr) {
185
+ let v = as_value(elem).cloned().unwrap_or(Value::Null);
186
+ a.borrow_mut().push(v);
187
+ }
188
+ }
189
+
190
+ /// Length of `arr`, or 0 if it isn't an array.
191
+ /// # Safety
192
+ /// `arr` must be null or a live handle.
193
+ #[no_mangle]
194
+ pub unsafe extern "C" fn tish_value_array_len(arr: TishValueRef) -> usize {
195
+ match as_value(arr) {
196
+ Some(Value::Array(a)) => a.borrow().len(),
197
+ _ => 0,
198
+ }
199
+ }
200
+
201
+ /// A newly-owned handle to a **clone** of element `i` (caller drops it); a null value handle if
202
+ /// out of range or not an array.
203
+ /// # Safety
204
+ /// `arr` must be null or a live handle.
205
+ #[no_mangle]
206
+ pub unsafe extern "C" fn tish_value_array_get(arr: TishValueRef, i: usize) -> TishValueRef {
207
+ match as_value(arr) {
208
+ Some(Value::Array(a)) => box_value(a.borrow().get(i).cloned().unwrap_or(Value::Null)),
209
+ _ => box_value(Value::Null),
210
+ }
211
+ }
212
+
213
+ // ── objects ──────────────────────────────────────────────────────────────────
214
+ #[no_mangle]
215
+ pub extern "C" fn tish_value_object_new() -> TishValueRef {
216
+ box_value(Value::Object(VmRef::new(ObjectData::default())))
217
+ }
218
+
219
+ /// Set `obj[key]` to a **clone** of `val`. No-op if `obj` isn't an object or `key` is invalid.
220
+ /// # Safety
221
+ /// handles live; `key` a valid C string.
222
+ #[no_mangle]
223
+ pub unsafe extern "C" fn tish_value_object_set(
224
+ obj: TishValueRef,
225
+ key: *const c_char,
226
+ val: TishValueRef,
227
+ ) {
228
+ if key.is_null() {
229
+ return;
230
+ }
231
+ if let (Some(Value::Object(o)), Ok(k)) = (as_value(obj), CStr::from_ptr(key).to_str()) {
232
+ let v = as_value(val).cloned().unwrap_or(Value::Null);
233
+ o.borrow_mut().strings.insert(k.into(), v);
234
+ }
235
+ }
236
+
237
+ /// Newly-owned handle to a **clone** of `obj[key]`; a null value handle if missing / not an object.
238
+ /// # Safety
239
+ /// `obj` live; `key` a valid C string.
240
+ #[no_mangle]
241
+ pub unsafe extern "C" fn tish_value_object_get(
242
+ obj: TishValueRef,
243
+ key: *const c_char,
244
+ ) -> TishValueRef {
245
+ if !key.is_null() {
246
+ if let (Some(Value::Object(o)), Ok(k)) = (as_value(obj), CStr::from_ptr(key).to_str()) {
247
+ return box_value(o.borrow().strings.get(k).cloned().unwrap_or(Value::Null));
248
+ }
249
+ }
250
+ box_value(Value::Null)
251
+ }
252
+
253
+ // ── lifetime ─────────────────────────────────────────────────────────────────
254
+ /// Deep-share clone (`Value::clone` shares `Arc`/`Rc` containers, like the interpreter).
255
+ /// # Safety
256
+ /// `r` null or live.
257
+ #[no_mangle]
258
+ pub unsafe extern "C" fn tish_value_clone(r: TishValueRef) -> TishValueRef {
259
+ box_value(as_value(r).cloned().unwrap_or(Value::Null))
260
+ }
261
+
262
+ /// Release a handle obtained from any `_new_*` / `_get` / `_clone` accessor.
263
+ /// # Safety
264
+ /// `r` null or a live handle, dropped exactly once.
265
+ #[no_mangle]
266
+ pub unsafe extern "C" fn tish_value_drop(r: TishValueRef) {
267
+ if !r.is_null() {
268
+ drop(Box::from_raw(r as *mut Value));
269
+ }
270
+ }
271
+
272
+ // ── B2: loader + dispatch shim ───────────────────────────────────────────────
273
+
274
+ /// Wrap an `extern "C"` native function as a tish `Value::native`, marshaling each call's
275
+ /// `&[Value]` into owned handles, invoking the C-ABI function, and unwrapping the returned handle.
276
+ ///
277
+ /// This is the bridge the loader and every backend's `register_native_module` use: the only thing
278
+ /// that changes vs a Rust built-in is that the call crosses the C ABI instead of a direct closure.
279
+ /// Args are passed as **clones** (arrays/objects share their `Arc`/`Rc` container, matching tish's
280
+ /// reference semantics, so an extension can mutate a passed array); the per-call handles and the
281
+ /// result handle are dropped here.
282
+ pub fn wrap_native_fn(func: TishNativeFn) -> Value {
283
+ Value::native(move |args: &[Value]| -> Value {
284
+ let handles: Vec<TishValueRef> = args.iter().map(|v| box_value(v.clone())).collect();
285
+ // Calling an `extern "C"` fn pointer is a safe operation; the unsafety is in the accessors.
286
+ let result = func(handles.as_ptr(), handles.len());
287
+ // SAFETY: every `handles[i]` came from `box_value`; `result` is a freshly-owned handle
288
+ // from a `_new_*`/`_clone` accessor (the documented return contract).
289
+ unsafe {
290
+ let out = as_value(result).cloned().unwrap_or(Value::Null);
291
+ for h in handles {
292
+ tish_value_drop(h);
293
+ }
294
+ tish_value_drop(result);
295
+ out
296
+ }
297
+ })
298
+ }
299
+
300
+ /// Load a native C-ABI extension (`cdylib`) and return its exports as a name→`Value::native` map,
301
+ /// ready for the interpreter's `with_modules` or the VM's `register_native_module`. The module
302
+ /// must export `extern "C" fn tish_module_register() -> *const TishExportTable`.
303
+ ///
304
+ /// The extension imports the host's `tish_value_*` accessors, so the host must export them at link
305
+ /// time (`-rdynamic` / `-Wl,-export_dynamic`). The loaded library is intentionally leaked so the
306
+ /// function pointers stay valid for the process — the load-once model (matches the JIT module).
307
+ #[cfg(not(target_arch = "wasm32"))]
308
+ pub fn load_module(path: &str) -> Result<tishlang_core::ObjectMap, String> {
309
+ use tishlang_core::ObjectMap;
310
+ // SAFETY: dlopen of a caller-supplied path; the export table is validated (null checks) and the
311
+ // function pointers are wrapped behind the marshaling shim.
312
+ unsafe {
313
+ let lib =
314
+ libloading::Library::new(path).map_err(|e| format!("ffi: load {}: {}", path, e))?;
315
+ let register: libloading::Symbol<unsafe extern "C" fn() -> *const TishExportTable> = lib
316
+ .get(b"tish_module_register")
317
+ .map_err(|e| format!("ffi: {}: no tish_module_register: {}", path, e))?;
318
+ let table = register();
319
+ if table.is_null() {
320
+ return Err(format!("ffi: {}: tish_module_register returned null", path));
321
+ }
322
+ let table = &*table;
323
+ if table.count > 0 && table.exports.is_null() {
324
+ return Err(format!("ffi: {}: null export table", path));
325
+ }
326
+ let exports = std::slice::from_raw_parts(table.exports, table.count);
327
+ let mut map = ObjectMap::default();
328
+ for exp in exports {
329
+ if exp.name.is_null() {
330
+ continue;
331
+ }
332
+ let name = CStr::from_ptr(exp.name)
333
+ .to_str()
334
+ .map_err(|_| format!("ffi: {}: non-UTF-8 export name", path))?;
335
+ map.insert(name.into(), wrap_native_fn(exp.func));
336
+ }
337
+ std::mem::forget(lib); // keep symbols live for the process lifetime
338
+ Ok(map)
339
+ }
340
+ }
341
+
342
+ #[cfg(test)]
343
+ mod tests {
344
+ use super::*;
345
+
346
+ // Helper: a value's tag + scalar round-trip through the C ABI, then drop.
347
+ unsafe fn roundtrip_scalars() {
348
+ let n = tish_value_new_number(42.5);
349
+ assert_eq!(tish_value_tag(n), TISH_TAG_NUMBER);
350
+ assert_eq!(tish_value_as_number(n), 42.5);
351
+ tish_value_drop(n);
352
+
353
+ let b = tish_value_new_bool(true);
354
+ assert_eq!(tish_value_tag(b), TISH_TAG_BOOL);
355
+ assert!(tish_value_as_bool(b));
356
+ tish_value_drop(b);
357
+
358
+ let z = tish_value_new_null();
359
+ assert_eq!(tish_value_tag(z), TISH_TAG_NULL);
360
+ tish_value_drop(z);
361
+
362
+ // null pointer behaves as Null, never UB.
363
+ assert_eq!(tish_value_tag(std::ptr::null_mut()), TISH_TAG_NULL);
364
+ assert!(tish_value_as_number(std::ptr::null_mut()).is_nan());
365
+ }
366
+
367
+ #[test]
368
+ fn scalars_roundtrip() {
369
+ unsafe { roundtrip_scalars() }
370
+ }
371
+
372
+ #[test]
373
+ fn string_roundtrip() {
374
+ unsafe {
375
+ let cs = CString::new("héllo").unwrap();
376
+ let s = tish_value_new_string(cs.as_ptr());
377
+ assert_eq!(tish_value_tag(s), TISH_TAG_STRING);
378
+ let out = tish_value_as_string(s);
379
+ assert!(!out.is_null());
380
+ assert_eq!(CStr::from_ptr(out).to_str().unwrap(), "héllo");
381
+ tish_string_free(out);
382
+ // non-string → null pointer
383
+ let n = tish_value_new_number(1.0);
384
+ assert!(tish_value_as_string(n).is_null());
385
+ tish_value_drop(n);
386
+ tish_value_drop(s);
387
+ }
388
+ }
389
+
390
+ #[test]
391
+ fn array_roundtrip() {
392
+ unsafe {
393
+ let arr = tish_value_array_new();
394
+ assert_eq!(tish_value_tag(arr), TISH_TAG_ARRAY);
395
+ for i in 0..3 {
396
+ let e = tish_value_new_number(i as f64 * 10.0);
397
+ tish_value_array_push(arr, e);
398
+ tish_value_drop(e); // push cloned; caller still owns e
399
+ }
400
+ assert_eq!(tish_value_array_len(arr), 3);
401
+ let g = tish_value_array_get(arr, 1);
402
+ assert_eq!(tish_value_as_number(g), 10.0);
403
+ tish_value_drop(g);
404
+ // out of range → null
405
+ let oob = tish_value_array_get(arr, 9);
406
+ assert_eq!(tish_value_tag(oob), TISH_TAG_NULL);
407
+ tish_value_drop(oob);
408
+ tish_value_drop(arr);
409
+ }
410
+ }
411
+
412
+ #[test]
413
+ fn object_roundtrip() {
414
+ unsafe {
415
+ let obj = tish_value_object_new();
416
+ assert_eq!(tish_value_tag(obj), TISH_TAG_OBJECT);
417
+ let key = CString::new("x").unwrap();
418
+ let v = tish_value_new_number(7.0);
419
+ tish_value_object_set(obj, key.as_ptr(), v);
420
+ tish_value_drop(v);
421
+ let got = tish_value_object_get(obj, key.as_ptr());
422
+ assert_eq!(tish_value_as_number(got), 7.0);
423
+ tish_value_drop(got);
424
+ // missing key → null
425
+ let miss = CString::new("nope").unwrap();
426
+ let m = tish_value_object_get(obj, miss.as_ptr());
427
+ assert_eq!(tish_value_tag(m), TISH_TAG_NULL);
428
+ tish_value_drop(m);
429
+ tish_value_drop(obj);
430
+ }
431
+ }
432
+
433
+ // Simulates an extension fn `(a, b) => a + b` using ONLY the C ABI — the marshaling the B2
434
+ // loader's shim will drive.
435
+ extern "C" fn add_fn(args: *const TishValueRef, argc: usize) -> TishValueRef {
436
+ unsafe {
437
+ if argc < 2 {
438
+ return tish_value_new_null();
439
+ }
440
+ let a = tish_value_as_number(*args);
441
+ let b = tish_value_as_number(*args.add(1));
442
+ tish_value_new_number(a + b)
443
+ }
444
+ }
445
+
446
+ #[test]
447
+ fn native_fn_call_shape() {
448
+ unsafe {
449
+ let a = tish_value_new_number(2.0);
450
+ let b = tish_value_new_number(3.0);
451
+ let argv: [TishValueRef; 2] = [a, b];
452
+ let r = add_fn(argv.as_ptr(), 2);
453
+ assert_eq!(tish_value_as_number(r), 5.0);
454
+ tish_value_drop(r);
455
+ tish_value_drop(a);
456
+ tish_value_drop(b);
457
+ // A module table referencing it type-checks (the `tish_module_register` shape).
458
+ let name = CString::new("add").unwrap();
459
+ let exports = [TishExport { name: name.as_ptr(), func: add_fn }];
460
+ let table = TishExportTable { exports: exports.as_ptr(), count: 1 };
461
+ assert_eq!(table.count, 1);
462
+ }
463
+ }
464
+
465
+ // B2: the marshaling shim turns a C-ABI fn into a tish `Value::native` end-to-end.
466
+ #[test]
467
+ fn wrap_native_fn_marshals() {
468
+ let wrapped = wrap_native_fn(add_fn);
469
+ match wrapped {
470
+ Value::Function(f) => {
471
+ let r = f.call(&[Value::Number(2.0), Value::Number(40.0)]);
472
+ match r {
473
+ Value::Number(n) => assert_eq!(n, 42.0),
474
+ other => panic!("expected Number(42), got {:?}", other),
475
+ }
476
+ // argc < 2 → the extension returns null; the shim unwraps it.
477
+ assert!(matches!(f.call(&[]), Value::Null));
478
+ }
479
+ other => panic!("expected Value::Function, got {:?}", other),
480
+ }
481
+ }
482
+
483
+ // The shim shares array containers (reference semantics): an extension can read passed arrays.
484
+ extern "C" fn sum_array(args: *const TishValueRef, argc: usize) -> TishValueRef {
485
+ unsafe {
486
+ if argc < 1 {
487
+ return tish_value_new_number(0.0);
488
+ }
489
+ let arr = *args;
490
+ let n = tish_value_array_len(arr);
491
+ let mut total = 0.0;
492
+ for i in 0..n {
493
+ let e = tish_value_array_get(arr, i);
494
+ total += tish_value_as_number(e);
495
+ tish_value_drop(e);
496
+ }
497
+ tish_value_new_number(total)
498
+ }
499
+ }
500
+
501
+ #[test]
502
+ fn wrap_native_fn_array_arg() {
503
+ let wrapped = wrap_native_fn(sum_array);
504
+ if let Value::Function(f) = wrapped {
505
+ let arr = Value::Array(VmRef::new(vec![
506
+ Value::Number(1.0),
507
+ Value::Number(2.0),
508
+ Value::Number(3.0),
509
+ ]));
510
+ match f.call(&[arr]) {
511
+ Value::Number(n) => assert_eq!(n, 6.0),
512
+ other => panic!("expected Number(6), got {:?}", other),
513
+ }
514
+ } else {
515
+ panic!("expected function");
516
+ }
517
+ }
518
+ }
@@ -0,0 +1,18 @@
1
+ # A fixture native extension built as a cdylib + loaded by the `load_module` integration test.
2
+ # Standalone workspace ([workspace] below) so the main workspace doesn't treat it as a member.
3
+ # It links `tishlang_ffi` for the ABI types + accessors; a real third-party extension would instead
4
+ # only declare the `tish_value_*` accessors `extern "C"` and rely on the host exporting them
5
+ # (-rdynamic), but linking here keeps the value layout identical so the mechanics are exercised.
6
+ [package]
7
+ name = "tish_ffi_testmod"
8
+ version = "0.0.0"
9
+ edition = "2021"
10
+ publish = false
11
+
12
+ [workspace]
13
+
14
+ [lib]
15
+ crate-type = ["cdylib"]
16
+
17
+ [dependencies]
18
+ tishlang_ffi = { path = "../../.." }
@@ -0,0 +1,46 @@
1
+ //! Fixture native extension (cdylib). Exposes `triple(x) = x*3` and `make_pair(a,b) = [a,b]`
2
+ //! through the C ABI, registered via `tish_module_register`. Loaded by the `load_module` test.
3
+
4
+ use std::ffi::{c_char, CString};
5
+
6
+ use tishlang_ffi::{
7
+ tish_value_array_new, tish_value_array_push, tish_value_as_number, tish_value_new_null,
8
+ tish_value_new_number, TishExport, TishExportTable, TishValueRef,
9
+ };
10
+
11
+ extern "C" fn triple(args: *const TishValueRef, argc: usize) -> TishValueRef {
12
+ unsafe {
13
+ if argc < 1 {
14
+ return tish_value_new_null();
15
+ }
16
+ let x = tish_value_as_number(*args);
17
+ tish_value_new_number(x * 3.0)
18
+ }
19
+ }
20
+
21
+ extern "C" fn make_pair(args: *const TishValueRef, argc: usize) -> TishValueRef {
22
+ unsafe {
23
+ let arr = tish_value_array_new();
24
+ for i in 0..argc.min(2) {
25
+ tish_value_array_push(arr, *args.add(i));
26
+ }
27
+ arr
28
+ }
29
+ }
30
+
31
+ /// Module entry point. Returns a leaked static table (process-lifetime, called once).
32
+ #[no_mangle]
33
+ pub extern "C" fn tish_module_register() -> *const TishExportTable {
34
+ let mk = |name: &str, func: extern "C" fn(*const TishValueRef, usize) -> TishValueRef| {
35
+ TishExport {
36
+ name: CString::new(name).unwrap().into_raw() as *const c_char,
37
+ func,
38
+ }
39
+ };
40
+ let exports = Box::leak(Box::new([mk("triple", triple), mk("make_pair", make_pair)]));
41
+ let table = Box::leak(Box::new(TishExportTable {
42
+ exports: exports.as_ptr(),
43
+ count: exports.len(),
44
+ }));
45
+ table as *const TishExportTable
46
+ }
@@ -0,0 +1,65 @@
1
+ //! End-to-end B2 validation: build the fixture cdylib, `load_module` it, and call its exports
2
+ //! through the wrapped `Value::native` shims — exercising the whole load → register → marshal path
3
+ //! with a real `dlopen`'d artifact (not a same-binary function pointer).
4
+
5
+ #![cfg(not(target_arch = "wasm32"))]
6
+
7
+ use std::path::PathBuf;
8
+ use std::process::Command;
9
+
10
+ use tishlang_core::Value;
11
+
12
+ fn build_and_locate_fixture() -> PathBuf {
13
+ let manifest = format!(
14
+ "{}/tests/fixtures/testmod/Cargo.toml",
15
+ env!("CARGO_MANIFEST_DIR")
16
+ );
17
+ let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
18
+ let status = Command::new(cargo)
19
+ .args(["build", "--release", "--manifest-path", &manifest])
20
+ .status()
21
+ .expect("spawn cargo to build fixture cdylib");
22
+ assert!(status.success(), "fixture cdylib build failed");
23
+
24
+ let dir = format!(
25
+ "{}/tests/fixtures/testmod/target/release",
26
+ env!("CARGO_MANIFEST_DIR")
27
+ );
28
+ std::fs::read_dir(&dir)
29
+ .unwrap_or_else(|e| panic!("read {dir}: {e}"))
30
+ .filter_map(|e| e.ok().map(|e| e.path()))
31
+ .find(|p| {
32
+ let name = p.file_name().unwrap_or_default().to_string_lossy();
33
+ name.contains("tish_ffi_testmod")
34
+ && matches!(
35
+ p.extension().and_then(|x| x.to_str()),
36
+ Some("dylib") | Some("so") | Some("dll")
37
+ )
38
+ })
39
+ .expect("built cdylib artifact (lib*.{dylib,so} / *.dll)")
40
+ }
41
+
42
+ #[test]
43
+ fn load_and_call_real_cdylib() {
44
+ let lib = build_and_locate_fixture();
45
+ let module = tishlang_ffi::load_module(lib.to_str().unwrap())
46
+ .unwrap_or_else(|e| panic!("load_module: {e}"));
47
+
48
+ // `triple(7) === 21`
49
+ match module.get("triple") {
50
+ Some(Value::Function(f)) => match f.call(&[Value::Number(7.0)]) {
51
+ Value::Number(n) => assert_eq!(n, 21.0),
52
+ other => panic!("triple(7) = {other:?}"),
53
+ },
54
+ other => panic!("triple export = {other:?}"),
55
+ }
56
+
57
+ // `make_pair(1, 2)` builds an array of length 2 inside the extension, via the C ABI.
58
+ match module.get("make_pair") {
59
+ Some(Value::Function(f)) => match f.call(&[Value::Number(1.0), Value::Number(2.0)]) {
60
+ Value::Array(a) => assert_eq!(a.borrow().len(), 2),
61
+ other => panic!("make_pair = {other:?}"),
62
+ },
63
+ other => panic!("make_pair export = {other:?}"),
64
+ }
65
+ }
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tishlang_fmt"
3
- version = "0.1.0"
3
+ version = "2.0.2"
4
4
  edition = "2021"
5
5
  description = "Opinionated formatter for Tish source (parse → pretty-print)"
6
6
  license-file = { workspace = true }