@tishlang/tish-lsp 2.39.0 → 3.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 (89) hide show
  1. package/Cargo.toml +4 -1
  2. package/bin/tish-lsp +0 -0
  3. package/crates/tish/src/cli_help.rs +28 -0
  4. package/crates/tish/src/main.rs +41 -5
  5. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +21 -0
  6. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/tish.d.tish +4 -0
  7. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +5 -1
  8. package/crates/tish/tests/regr_typed_externs.rs +59 -0
  9. package/crates/tish/tests/scope_merge.rs +79 -0
  10. package/crates/tish_build_utils/src/lib.rs +27 -1
  11. package/crates/tish_builtins/Cargo.toml +14 -6
  12. package/crates/tish_builtins/src/array.rs +130 -54
  13. package/crates/tish_builtins/src/collections.rs +9 -5
  14. package/crates/tish_builtins/src/construct.rs +7 -1
  15. package/crates/tish_builtins/src/date.rs +15 -1
  16. package/crates/tish_builtins/src/globals.rs +65 -3
  17. package/crates/tish_builtins/src/helpers.rs +7 -3
  18. package/crates/tish_builtins/src/iterator.rs +5 -1
  19. package/crates/tish_builtins/src/lib.rs +3 -0
  20. package/crates/tish_builtins/src/math.rs +14 -0
  21. package/crates/tish_builtins/src/number.rs +72 -6
  22. package/crates/tish_builtins/src/object.rs +6 -1
  23. package/crates/tish_builtins/src/string.rs +13 -2
  24. package/crates/tish_builtins/src/symbol.rs +17 -17
  25. package/crates/tish_builtins/src/typedarrays.rs +9 -2
  26. package/crates/tish_bytecode/src/compiler.rs +76 -2
  27. package/crates/tish_bytecode/src/lib.rs +1 -1
  28. package/crates/tish_bytecode/src/opcode.rs +77 -2
  29. package/crates/tish_compile/src/codegen.rs +2154 -225
  30. package/crates/tish_compile/src/infer.rs +37 -41
  31. package/crates/tish_compile/src/lib.rs +13 -1
  32. package/crates/tish_compile/src/platform_resolve.rs +361 -0
  33. package/crates/tish_compile/src/resolve.rs +195 -11
  34. package/crates/tish_compile/src/schemes.rs +333 -0
  35. package/crates/tish_compile/src/types.rs +205 -34
  36. package/crates/tish_compile/tests/perf_codegen_320.rs +2 -2
  37. package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +3 -1
  38. package/crates/tish_compile/tests/platform_resolve_cli.rs +164 -0
  39. package/crates/tish_compile/tests/regr_556_param_name_collision.rs +40 -0
  40. package/crates/tish_compile/tests/regr_558_struct_array_write.rs +65 -0
  41. package/crates/tish_compile/tests/regr_558b_struct_field_read.rs +21 -0
  42. package/crates/tish_compile/tests/regr_558c_wrapped_struct_array.rs +64 -0
  43. package/crates/tish_compile/tests/regr_558d_native_vec_mutators.rs +156 -0
  44. package/crates/tish_compile/tests/regr_562_wrapped_struct_array_push.rs +50 -0
  45. package/crates/tish_compile/tests/regr_fixed_native_coercions.rs +69 -0
  46. package/crates/tish_compile/tests/regr_gba_numerics_gated.rs +58 -0
  47. package/crates/tish_compile/tests/regr_gba_struct_reflection.rs +106 -0
  48. package/crates/tish_compile/tests/regression/name_collision_lib.tish +6 -0
  49. package/crates/tish_compile/tests/regression/name_collision_main.tish +13 -0
  50. package/crates/tish_compile/tests/regression/struct_array_write.tish +22 -0
  51. package/crates/tish_compile/tests/regression/struct_field_return_read.tish +10 -0
  52. package/crates/tish_compiler_wasm/Cargo.toml +1 -0
  53. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +56 -6
  54. package/crates/tish_core/Cargo.toml +39 -4
  55. package/crates/tish_core/src/compat.rs +506 -0
  56. package/crates/tish_core/src/console_style.rs +23 -1
  57. package/crates/tish_core/src/json.rs +42 -10
  58. package/crates/tish_core/src/lib.rs +121 -25
  59. package/crates/tish_core/src/macros.rs +1 -1
  60. package/crates/tish_core/src/shape.rs +4 -2
  61. package/crates/tish_core/src/uri.rs +8 -1
  62. package/crates/tish_core/src/value.rs +305 -54
  63. package/crates/tish_core/src/vmref.rs +17 -5
  64. package/crates/tish_eval/Cargo.toml +2 -0
  65. package/crates/tish_eval/src/eval.rs +90 -3
  66. package/crates/tish_eval/src/value.rs +8 -0
  67. package/crates/tish_eval/src/value_convert.rs +316 -21
  68. package/crates/tish_lsp/Cargo.toml +3 -1
  69. package/crates/tish_lsp/src/import_goto.rs +41 -14
  70. package/crates/tish_lsp/src/main.rs +18 -0
  71. package/crates/tish_native/src/build.rs +246 -0
  72. package/crates/tish_native/src/config.rs +20 -0
  73. package/crates/tish_native/src/lib.rs +16 -6
  74. package/crates/tish_parser/src/lib.rs +28 -0
  75. package/crates/tish_parser/src/parser.rs +25 -4
  76. package/crates/tish_runtime/src/lib.rs +55 -9
  77. package/crates/tish_runtime_gba/Cargo.lock +837 -0
  78. package/crates/tish_runtime_gba/Cargo.toml +16 -0
  79. package/crates/tish_runtime_gba/src/gba.rs +211 -0
  80. package/crates/tish_runtime_gba/src/lib.rs +804 -0
  81. package/crates/tish_vm/src/jit.rs +1446 -164
  82. package/crates/tish_vm/src/vm.rs +583 -173
  83. package/justfile +9 -0
  84. package/package.json +1 -1
  85. package/platform/darwin-arm64/tish-lsp +0 -0
  86. package/platform/darwin-x64/tish-lsp +0 -0
  87. package/platform/linux-arm64/tish-lsp +0 -0
  88. package/platform/linux-x64/tish-lsp +0 -0
  89. package/platform/win32-x64/tish-lsp.exe +0 -0
@@ -49,24 +49,24 @@
49
49
  //! feature. They both `Deref` (and, for write guards, `DerefMut`) to `T`
50
50
  //! just like the underlying types.
51
51
 
52
- use std::fmt;
52
+ use core::fmt;
53
53
 
54
54
  // --------------------------------------------------------------------------
55
55
  // Single-threaded backing store (default): Rc<RefCell<T>>
56
56
  // --------------------------------------------------------------------------
57
57
  #[cfg(not(feature = "send-values"))]
58
58
  mod imp {
59
- use std::cell::RefCell;
60
- use std::rc::Rc;
59
+ use alloc::rc::Rc;
60
+ use core::cell::RefCell;
61
61
 
62
62
  #[derive(Default)]
63
63
  pub struct VmRef<T: ?Sized>(pub(super) Rc<RefCell<T>>);
64
64
 
65
65
  /// Read guard alias. On the single-threaded path this is a true
66
66
  /// `Ref<'_, T>`, so multiple readers can coexist.
67
- pub type ReadGuard<'a, T> = std::cell::Ref<'a, T>;
67
+ pub type ReadGuard<'a, T> = core::cell::Ref<'a, T>;
68
68
  /// Write guard alias. Exclusive, `DerefMut`.
69
- pub type WriteGuard<'a, T> = std::cell::RefMut<'a, T>;
69
+ pub type WriteGuard<'a, T> = core::cell::RefMut<'a, T>;
70
70
 
71
71
  impl<T> VmRef<T> {
72
72
  #[inline]
@@ -182,6 +182,7 @@ mod imp {
182
182
 
183
183
  pub use imp::{ReadGuard as VmReadGuard, VmRef, WriteGuard as VmWriteGuard};
184
184
 
185
+ #[cfg(not(feature = "portable"))]
185
186
  impl<T: fmt::Debug> fmt::Debug for VmRef<T> {
186
187
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187
188
  // Match `RefCell`'s debug format so snapshot-test output stays
@@ -195,3 +196,14 @@ impl<T: fmt::Debug> fmt::Debug for VmRef<T> {
195
196
  }
196
197
  }
197
198
  }
199
+
200
+ // No unwinding on GBA (`panic = abort`), so `catch_unwind` is unavailable.
201
+ // Debug is used for host snapshot tests, not the runtime hot path, so a direct
202
+ // borrow is fine (a live borrow during Debug would abort, which never happens in
203
+ // practice on the single-threaded target).
204
+ #[cfg(feature = "portable")]
205
+ impl<T: fmt::Debug> fmt::Debug for VmRef<T> {
206
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207
+ write!(f, "RefCell {{ value: {:?} }}", &*self.borrow())
208
+ }
209
+ }
@@ -42,6 +42,8 @@ tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
42
42
  tishlang_builtins = { path = "../tish_builtins", version = ">=0.1" }
43
43
  tishlang_parser = { path = "../tish_parser", version = ">=0.1" }
44
44
  tishlang_core = { path = "../tish_core", version = ">=0.1" }
45
+ # Shared platform/surface file cascade (same as `tish build` / `resolve-id`).
46
+ tishlang_compile = { path = "../tish_compile", version = ">=0.1" }
45
47
  tiny_http = { version = "0.12.0", optional = true }
46
48
  tokio = { version = "1.50.0", features = ["rt-multi-thread", "time", "sync"], optional = true }
47
49
  tishlang_runtime = { path = "../tish_runtime", version = ">=0.1", optional = true }
@@ -293,6 +293,16 @@ impl Evaluator {
293
293
  math.insert("log10".into(), Value::Native(natives::math_log10));
294
294
  math.insert("PI".into(), Value::Number(std::f64::consts::PI));
295
295
  math.insert("E".into(), Value::Number(std::f64::consts::E));
296
+ // #539: the remaining ECMAScript Math value constants (were missing → `null`).
297
+ math.insert("LN2".into(), Value::Number(std::f64::consts::LN_2));
298
+ math.insert("LN10".into(), Value::Number(std::f64::consts::LN_10));
299
+ math.insert("LOG2E".into(), Value::Number(std::f64::consts::LOG2_E));
300
+ math.insert("LOG10E".into(), Value::Number(std::f64::consts::LOG10_E));
301
+ math.insert("SQRT2".into(), Value::Number(std::f64::consts::SQRT_2));
302
+ math.insert(
303
+ "SQRT1_2".into(),
304
+ Value::Number(std::f64::consts::FRAC_1_SQRT_2),
305
+ );
296
306
  s.set(
297
307
  "Math".into(),
298
308
  Value::object(math),
@@ -336,6 +346,8 @@ impl Evaluator {
336
346
 
337
347
  let mut object = PropMap::with_capacity(5);
338
348
  object.insert("keys".into(), Value::Native(Self::object_keys));
349
+ object.insert("freeze".into(), Value::Native(Self::object_freeze));
350
+ object.insert("isFrozen".into(), Value::Native(Self::object_is_frozen));
339
351
  object.insert("values".into(), Value::Native(Self::object_values));
340
352
  object.insert("entries".into(), Value::Native(Self::object_entries));
341
353
  object.insert("assign".into(), Value::Native(Self::object_assign));
@@ -1190,6 +1202,10 @@ impl Evaluator {
1190
1202
  from
1191
1203
  )));
1192
1204
  }
1205
+ let ctx = tishlang_compile::resolve_context();
1206
+ if let Some(path) = tishlang_compile::resolve_with_platform(from, dir, ctx) {
1207
+ return Ok(path);
1208
+ }
1193
1209
  let base = dir.join(from);
1194
1210
  let path = if base.extension().is_none() {
1195
1211
  let with_ext = base.with_extension("tish");
@@ -3366,6 +3382,17 @@ impl Evaluator {
3366
3382
  let val = self.eval_expr(value)?;
3367
3383
  match obj_val {
3368
3384
  Value::Object(map) => {
3385
+ if map.borrow().frozen {
3386
+ return Err(EvalError::Throw(
3387
+ crate::natives::type_error_construct(&[Value::String(
3388
+ format!(
3389
+ "Cannot assign to read only property '{prop}' of a frozen object"
3390
+ )
3391
+ .into(),
3392
+ )])
3393
+ .unwrap_or(Value::Null),
3394
+ ));
3395
+ }
3369
3396
  map.borrow_mut()
3370
3397
  .strings
3371
3398
  .insert(Arc::clone(prop), val.clone());
@@ -3417,7 +3444,18 @@ impl Evaluator {
3417
3444
  arr_mut[idx] = val.clone();
3418
3445
  Ok(val)
3419
3446
  }
3420
- Value::Object(_) => {
3447
+ Value::Object(ref map) => {
3448
+ if map.borrow().frozen {
3449
+ return Err(EvalError::Throw(
3450
+ crate::natives::type_error_construct(&[Value::String(
3451
+ format!(
3452
+ "Cannot assign to read only property '{idx_val}' of a frozen object"
3453
+ )
3454
+ .into(),
3455
+ )])
3456
+ .unwrap_or(Value::Null),
3457
+ ));
3458
+ }
3421
3459
  eval_object_set(&obj_val, &idx_val, val.clone())
3422
3460
  .map_err(EvalError::Error)?;
3423
3461
  Ok(val)
@@ -3912,7 +3950,23 @@ impl Evaluator {
3912
3950
  if let Some(call) = o.borrow().strings.get("__call").cloned() {
3913
3951
  return self.call_func(&call, args);
3914
3952
  }
3915
- Err(EvalError::Error("Not a function".to_string()))
3953
+ Err(EvalError::Throw(
3954
+ crate::natives::type_error_construct(&[Value::String(
3955
+ format!(
3956
+ "Call of non-function: {}",
3957
+ match f {
3958
+ Value::Number(_) => "number",
3959
+ Value::String(_) => "string",
3960
+ Value::Bool(_) => "boolean",
3961
+ Value::Null => "null",
3962
+ Value::Symbol(_) => "symbol",
3963
+ _ => "object",
3964
+ }
3965
+ )
3966
+ .into(),
3967
+ )])
3968
+ .unwrap_or(Value::Null),
3969
+ ))
3916
3970
  }
3917
3971
  Value::Native(native_fn) => {
3918
3972
  // #122: Node fs callback form — `readFile(path, (err, data) => …)`. The self-less
@@ -4154,7 +4208,23 @@ impl Evaluator {
4154
4208
  }
4155
4209
  }
4156
4210
  }
4157
- _ => Err(EvalError::Error("Not a function".to_string())),
4211
+ _ => Err(EvalError::Throw(
4212
+ crate::natives::type_error_construct(&[Value::String(
4213
+ format!(
4214
+ "Call of non-function: {}",
4215
+ match f {
4216
+ Value::Number(_) => "number",
4217
+ Value::String(_) => "string",
4218
+ Value::Bool(_) => "boolean",
4219
+ Value::Null => "null",
4220
+ Value::Symbol(_) => "symbol",
4221
+ _ => "object",
4222
+ }
4223
+ )
4224
+ .into(),
4225
+ )])
4226
+ .unwrap_or(Value::Null),
4227
+ )),
4158
4228
  }
4159
4229
  }
4160
4230
 
@@ -4852,6 +4922,23 @@ impl Evaluator {
4852
4922
  }
4853
4923
  }
4854
4924
 
4925
+ /// Object.freeze(obj) — mark frozen so writes throw a catchable TypeError (#437); returns obj.
4926
+ fn object_freeze(args: &[Value]) -> Result<Value, String> {
4927
+ if let Some(Value::Object(obj)) = args.first() {
4928
+ obj.borrow_mut().frozen = true;
4929
+ }
4930
+ Ok(args.first().cloned().unwrap_or(Value::Null))
4931
+ }
4932
+
4933
+ /// Object.isFrozen(obj) — true if frozen; primitives are frozen, arrays are not (this slice).
4934
+ fn object_is_frozen(args: &[Value]) -> Result<Value, String> {
4935
+ Ok(match args.first() {
4936
+ Some(Value::Object(obj)) => Value::Bool(obj.borrow().frozen),
4937
+ Some(Value::Array(_)) => Value::Bool(false),
4938
+ _ => Value::Bool(true),
4939
+ })
4940
+ }
4941
+
4855
4942
  fn object_keys(args: &[Value]) -> Result<Value, String> {
4856
4943
  match args.first() {
4857
4944
  Some(Value::Object(obj)) => {
@@ -24,6 +24,8 @@ pub type PropMap = indexmap::IndexMap<Arc<str>, Value>;
24
24
  pub struct EvalObjectData {
25
25
  pub strings: PropMap,
26
26
  pub symbols: Option<AHashMap<u64, Value>>,
27
+ /// `Object.freeze` marker (#437) — writes to a frozen object throw a catchable TypeError.
28
+ pub frozen: bool,
27
29
  }
28
30
 
29
31
  impl EvalObjectData {
@@ -31,8 +33,14 @@ impl EvalObjectData {
31
33
  Self {
32
34
  strings,
33
35
  symbols: None,
36
+ frozen: false,
34
37
  }
35
38
  }
39
+
40
+ #[inline]
41
+ pub fn is_frozen(&self) -> bool {
42
+ self.frozen
43
+ }
36
44
  }
37
45
 
38
46
  pub fn eval_object_get(obj: &Value, key: &Value) -> Option<Value> {
@@ -1,16 +1,77 @@
1
1
  //! Conversion between tishlang_eval::Value and tishlang_core::Value for opaque method calls.
2
2
 
3
3
  use std::cell::RefCell;
4
- use std::rc::Rc;
4
+ use std::rc::{Rc, Weak};
5
5
  use std::sync::Arc;
6
6
 
7
7
  #[cfg(feature = "regex")]
8
8
  use tishlang_core::TishRegExp;
9
- use ahash::AHashMap;
9
+ use ahash::{AHashMap, AHashSet};
10
10
  use tishlang_core::{ObjectData, Value as CoreValue, VmRef};
11
11
 
12
12
  use crate::value::{EvalObjectData, PropMap, Value};
13
13
 
14
+ /// #466 — identity-stable bridging for reference values (objects, arrays).
15
+ ///
16
+ /// Core `Map`/`Set` key equality is SameValueZero (`VmRef::ptr_eq` for objects/arrays), so a fresh
17
+ /// `VmRef` per `eval_to_core` call made `m.get(k)`/`s.has(a)` fail for ANY object/array key in the
18
+ /// interpreter: the stored key and the lookup probe were different core allocations. This cache
19
+ /// gives every eval `Rc` a single stable core **twin**: repeat bridges return the same `VmRef`
20
+ /// (identity holds), with contents refreshed from the eval side on each bridge (preserving the
21
+ /// existing copy-at-the-boundary semantics — and matching vm/node more closely on
22
+ /// mutate-after-insert, since the stored key's view updates with its object).
23
+ ///
24
+ /// The reverse maps let `core_to_eval` return the ORIGINAL eval object when a forward-bridged twin
25
+ /// flows back out (`m.get(k) === v`, `[...m.keys()][0] === k` — identity across the round trip,
26
+ /// as on vm/native/node).
27
+ ///
28
+ /// Soundness against address reuse (the #250 lesson): forward entries pair the twin with a `Weak`
29
+ /// to the eval `Rc` and every hit verifies `upgrade() + Rc::ptr_eq`; a dead/differing weak means
30
+ /// the address was reused and the entry is replaced. Reverse hits re-verify through the forward
31
+ /// entry (`VmRef::ptr_eq`), and a live forward entry holds the twin strongly, so a live reverse
32
+ /// key's address cannot have been recycled. `in_flight` makes conversion cycle-safe: a
33
+ /// self/mutually-referential object hit mid-refresh returns its twin as-is instead of recursing
34
+ /// forever (the enclosing refresh completes the contents).
35
+ struct BridgeCache {
36
+ obj_fwd: AHashMap<usize, (Weak<RefCell<EvalObjectData>>, VmRef<ObjectData>)>,
37
+ arr_fwd: AHashMap<usize, (Weak<RefCell<Vec<Value>>>, VmRef<Vec<CoreValue>>)>,
38
+ obj_rev: AHashMap<usize, Weak<RefCell<EvalObjectData>>>,
39
+ arr_rev: AHashMap<usize, Weak<RefCell<Vec<Value>>>>,
40
+ /// Eval addresses currently being refreshed (cycle guard).
41
+ in_flight: AHashSet<usize>,
42
+ /// Amortized cleanup threshold: sweep dead entries when the cache outgrows it.
43
+ high_water: usize,
44
+ }
45
+
46
+ impl BridgeCache {
47
+ fn new() -> Self {
48
+ Self {
49
+ obj_fwd: AHashMap::default(),
50
+ arr_fwd: AHashMap::default(),
51
+ obj_rev: AHashMap::default(),
52
+ arr_rev: AHashMap::default(),
53
+ in_flight: AHashSet::default(),
54
+ high_water: 256,
55
+ }
56
+ }
57
+
58
+ /// Drop entries whose eval side has died. Called on the insert path only; amortized O(1).
59
+ fn maybe_sweep(&mut self) {
60
+ if self.obj_fwd.len() + self.arr_fwd.len() <= self.high_water {
61
+ return;
62
+ }
63
+ self.obj_fwd.retain(|_, (w, _)| w.upgrade().is_some());
64
+ self.arr_fwd.retain(|_, (w, _)| w.upgrade().is_some());
65
+ self.obj_rev.retain(|_, w| w.upgrade().is_some());
66
+ self.arr_rev.retain(|_, w| w.upgrade().is_some());
67
+ self.high_water = ((self.obj_fwd.len() + self.arr_fwd.len()) * 2).max(256);
68
+ }
69
+ }
70
+
71
+ thread_local! {
72
+ static BRIDGE: RefCell<BridgeCache> = RefCell::new(BridgeCache::new());
73
+ }
74
+
14
75
  /// Convert interpreter Value to core Value. Fails for interpreter-only variants.
15
76
  pub fn eval_to_core(v: &Value) -> Result<CoreValue, String> {
16
77
  match v {
@@ -19,28 +80,110 @@ pub fn eval_to_core(v: &Value) -> Result<CoreValue, String> {
19
80
  Value::Bool(b) => Ok(CoreValue::Bool(*b)),
20
81
  Value::Null => Ok(CoreValue::Null),
21
82
  Value::Array(arr) => {
22
- let mut out = Vec::new();
23
- for item in arr.borrow().iter() {
24
- out.push(eval_to_core(item)?);
83
+ let addr = Rc::as_ptr(arr) as usize;
84
+ // Cycle guard: this array is already being refreshed higher up this same
85
+ // conversion — hand back its twin as-is; the enclosing refresh fills it.
86
+ if let Some(twin) = BRIDGE.with(|c| {
87
+ let c = c.borrow();
88
+ if c.in_flight.contains(&addr) {
89
+ c.arr_fwd
90
+ .get(&addr)
91
+ .filter(|(w, _)| w.upgrade().is_some_and(|up| Rc::ptr_eq(&up, arr)))
92
+ .map(|(_, t)| t.clone())
93
+ } else {
94
+ None
95
+ }
96
+ }) {
97
+ return Ok(CoreValue::Array(twin));
25
98
  }
26
- Ok(CoreValue::Array(VmRef::new(out)))
99
+ // Resolve (or mint) the stable core twin for this eval Rc, then refresh contents.
100
+ let twin = BRIDGE.with(|c| {
101
+ let mut c = c.borrow_mut();
102
+ match c.arr_fwd.get(&addr) {
103
+ Some((w, t)) if w.upgrade().is_some_and(|up| Rc::ptr_eq(&up, arr)) => {
104
+ t.clone()
105
+ }
106
+ _ => {
107
+ c.maybe_sweep();
108
+ let t: VmRef<Vec<CoreValue>> = VmRef::new(Vec::new());
109
+ c.arr_fwd.insert(addr, (Rc::downgrade(arr), t.clone()));
110
+ c.arr_rev.insert(t.as_ptr() as usize, Rc::downgrade(arr));
111
+ t
112
+ }
113
+ }
114
+ });
115
+ BRIDGE.with(|c| c.borrow_mut().in_flight.insert(addr));
116
+ let contents = (|| -> Result<Vec<CoreValue>, String> {
117
+ let mut out = Vec::new();
118
+ for item in arr.borrow().iter() {
119
+ out.push(eval_to_core(item)?);
120
+ }
121
+ Ok(out)
122
+ })();
123
+ BRIDGE.with(|c| {
124
+ c.borrow_mut().in_flight.remove(&addr);
125
+ });
126
+ *twin.borrow_mut() = contents?;
127
+ Ok(CoreValue::Array(twin))
27
128
  }
28
129
  Value::Object(map) => {
29
- let b = map.borrow();
30
- let mut strings = tishlang_core::PropMap::default();
31
- for (k, v) in b.strings.iter() {
32
- strings.insert(Arc::clone(k), eval_to_core(v)?);
130
+ let addr = Rc::as_ptr(map) as usize;
131
+ // Cycle guard — see the Array arm.
132
+ if let Some(twin) = BRIDGE.with(|c| {
133
+ let c = c.borrow();
134
+ if c.in_flight.contains(&addr) {
135
+ c.obj_fwd
136
+ .get(&addr)
137
+ .filter(|(w, _)| w.upgrade().is_some_and(|up| Rc::ptr_eq(&up, map)))
138
+ .map(|(_, t)| t.clone())
139
+ } else {
140
+ None
141
+ }
142
+ }) {
143
+ return Ok(CoreValue::Object(twin));
33
144
  }
34
- let symbols = if let Some(ss) = &b.symbols {
35
- let mut sm = AHashMap::default();
36
- for (id, v) in ss.iter() {
37
- sm.insert(*id, eval_to_core(v)?);
145
+ let twin = BRIDGE.with(|c| {
146
+ let mut c = c.borrow_mut();
147
+ match c.obj_fwd.get(&addr) {
148
+ Some((w, t)) if w.upgrade().is_some_and(|up| Rc::ptr_eq(&up, map)) => {
149
+ t.clone()
150
+ }
151
+ _ => {
152
+ c.maybe_sweep();
153
+ let t: VmRef<ObjectData> = VmRef::new(ObjectData {
154
+ strings: tishlang_core::PropMap::default(),
155
+ symbols: None,
156
+ frozen: false,
157
+ });
158
+ c.obj_fwd.insert(addr, (Rc::downgrade(map), t.clone()));
159
+ c.obj_rev.insert(t.as_ptr() as usize, Rc::downgrade(map));
160
+ t
161
+ }
162
+ }
163
+ });
164
+ BRIDGE.with(|c| c.borrow_mut().in_flight.insert(addr));
165
+ let contents = (|| -> Result<ObjectData, String> {
166
+ let b = map.borrow();
167
+ let mut strings = tishlang_core::PropMap::default();
168
+ for (k, v) in b.strings.iter() {
169
+ strings.insert(Arc::clone(k), eval_to_core(v)?);
38
170
  }
39
- Some(sm)
40
- } else {
41
- None
42
- };
43
- Ok(CoreValue::Object(VmRef::new(ObjectData { strings, symbols })))
171
+ let symbols = if let Some(ss) = &b.symbols {
172
+ let mut sm = AHashMap::default();
173
+ for (id, v) in ss.iter() {
174
+ sm.insert(*id, eval_to_core(v)?);
175
+ }
176
+ Some(sm)
177
+ } else {
178
+ None
179
+ };
180
+ Ok(ObjectData { strings, symbols, frozen: false })
181
+ })();
182
+ BRIDGE.with(|c| {
183
+ c.borrow_mut().in_flight.remove(&addr);
184
+ });
185
+ *twin.borrow_mut() = contents?;
186
+ Ok(CoreValue::Object(twin))
44
187
  }
45
188
  Value::Symbol(s) => Ok(CoreValue::Symbol(Arc::clone(s))),
46
189
  Value::Opaque(o) => Ok(CoreValue::Opaque(Arc::clone(o))),
@@ -59,6 +202,21 @@ pub fn core_to_eval(v: CoreValue) -> Value {
59
202
  CoreValue::Bool(b) => Value::Bool(b),
60
203
  CoreValue::Null => Value::Null,
61
204
  CoreValue::Array(arr) => {
205
+ // #466: a forward-bridged twin flowing back out returns the ORIGINAL eval array, so
206
+ // identity survives the round trip (`s.has(a)` after `[...s.keys()]`, `x === a`).
207
+ if let Some(orig) = BRIDGE.with(|c| {
208
+ let c = c.borrow();
209
+ c.arr_rev
210
+ .get(&(arr.as_ptr() as usize))
211
+ .and_then(|w| w.upgrade())
212
+ .filter(|orig| {
213
+ c.arr_fwd
214
+ .get(&(Rc::as_ptr(orig) as usize))
215
+ .is_some_and(|(_, t)| VmRef::ptr_eq(t, &arr))
216
+ })
217
+ }) {
218
+ return Value::Array(orig);
219
+ }
62
220
  let mut out = Vec::new();
63
221
  for item in arr.borrow().iter() {
64
222
  out.push(core_to_eval(item.clone()));
@@ -66,6 +224,20 @@ pub fn core_to_eval(v: CoreValue) -> Value {
66
224
  Value::Array(Rc::new(RefCell::new(out)))
67
225
  }
68
226
  CoreValue::Object(map) => {
227
+ // #466: see the Array arm.
228
+ if let Some(orig) = BRIDGE.with(|c| {
229
+ let c = c.borrow();
230
+ c.obj_rev
231
+ .get(&(map.as_ptr() as usize))
232
+ .and_then(|w| w.upgrade())
233
+ .filter(|orig| {
234
+ c.obj_fwd
235
+ .get(&(Rc::as_ptr(orig) as usize))
236
+ .is_some_and(|(_, t)| VmRef::ptr_eq(t, &map))
237
+ })
238
+ }) {
239
+ return Value::Object(orig);
240
+ }
69
241
  let b = map.borrow();
70
242
  let mut out = PropMap::default();
71
243
  for (k, v) in b.strings.iter() {
@@ -89,8 +261,17 @@ pub fn core_to_eval(v: CoreValue) -> Value {
89
261
  CoreValue::Promise(_) => Value::Null,
90
262
  // NumberArray: materialize to boxed Array for the interpreter (it has no packed path).
91
263
  CoreValue::NumberArray(arr) => {
92
- let nums = arr.borrow();
93
- let out: Vec<Value> = nums.iter().map(|&n| Value::Number(n)).collect();
264
+ // Materialize to a boxed Array for the interpreter (it has no packed path). `to_values`
265
+ // handles a packed or a deopted (#199) backing.
266
+ let out: Vec<Value> = arr
267
+ .borrow()
268
+ .to_values()
269
+ .into_iter()
270
+ .map(|v| match v {
271
+ tishlang_core::Value::Number(n) => Value::Number(n),
272
+ _ => core_to_eval(v),
273
+ })
274
+ .collect();
94
275
  Value::Array(Rc::new(RefCell::new(out)))
95
276
  }
96
277
  // `CoreNativeFn` is feature-gated (Rc vs Arc), so use Clone::clone
@@ -115,3 +296,117 @@ pub fn core_to_eval(v: CoreValue) -> Value {
115
296
  }
116
297
  }
117
298
  }
299
+
300
+ #[cfg(test)]
301
+ mod bridge_identity_tests {
302
+ use super::*;
303
+
304
+ fn eval_obj(pairs: &[(&str, Value)]) -> Value {
305
+ let mut strings = PropMap::default();
306
+ for (k, v) in pairs {
307
+ strings.insert(Arc::from(*k), v.clone());
308
+ }
309
+ Value::Object(Rc::new(RefCell::new(EvalObjectData::from_strings(strings))))
310
+ }
311
+
312
+ /// #466: the same eval object must bridge to the SAME core allocation every time —
313
+ /// that's what makes it usable as a Map/Set key (SameValueZero is ptr_eq for objects).
314
+ #[test]
315
+ fn same_eval_object_bridges_to_same_core_twin() {
316
+ let k = eval_obj(&[("id", Value::Number(1.0))]);
317
+ let (CoreValue::Object(t1), CoreValue::Object(t2)) =
318
+ (eval_to_core(&k).unwrap(), eval_to_core(&k).unwrap())
319
+ else {
320
+ panic!("expected core objects");
321
+ };
322
+ assert!(VmRef::ptr_eq(&t1, &t2), "twin identity must be stable across bridges");
323
+
324
+ // Distinct eval objects get distinct twins.
325
+ let k2 = eval_obj(&[("id", Value::Number(1.0))]);
326
+ let CoreValue::Object(t3) = eval_to_core(&k2).unwrap() else { panic!() };
327
+ assert!(!VmRef::ptr_eq(&t1, &t3), "distinct objects must not share a twin");
328
+ }
329
+
330
+ /// Re-bridging refreshes the twin's contents (copy-at-the-boundary semantics kept).
331
+ #[test]
332
+ fn rebridge_refreshes_twin_contents() {
333
+ let k = eval_obj(&[("id", Value::Number(1.0))]);
334
+ let CoreValue::Object(t1) = eval_to_core(&k).unwrap() else { panic!() };
335
+ assert!(matches!(t1.borrow().strings.get("id"), Some(CoreValue::Number(n)) if *n == 1.0));
336
+
337
+ if let Value::Object(o) = &k {
338
+ o.borrow_mut().strings.insert(Arc::from("id"), Value::Number(42.0));
339
+ }
340
+ let CoreValue::Object(t2) = eval_to_core(&k).unwrap() else { panic!() };
341
+ assert!(VmRef::ptr_eq(&t1, &t2));
342
+ assert!(matches!(t2.borrow().strings.get("id"), Some(CoreValue::Number(n)) if *n == 42.0));
343
+ }
344
+
345
+ /// A forward-bridged twin converts BACK to the original eval object (round-trip identity:
346
+ /// `m.get(k) === v` / `[...m.keys()][0] === k`).
347
+ #[test]
348
+ fn core_to_eval_returns_original_for_bridged_twin() {
349
+ let k = eval_obj(&[("id", Value::Number(1.0))]);
350
+ let core = eval_to_core(&k).unwrap();
351
+ let back = core_to_eval(core);
352
+ let (Value::Object(orig), Value::Object(round)) = (&k, &back) else { panic!() };
353
+ assert!(Rc::ptr_eq(orig, round), "round trip must return the original eval object");
354
+
355
+ // An unrelated core object (never forward-bridged) still gets a fresh conversion.
356
+ let alien = CoreValue::Object(VmRef::new(ObjectData {
357
+ strings: tishlang_core::PropMap::default(),
358
+ symbols: None,
359
+ frozen: false,
360
+ }));
361
+ assert!(matches!(core_to_eval(alien), Value::Object(_)));
362
+ }
363
+
364
+ /// Arrays get the same treatment (Set members / Map values).
365
+ #[test]
366
+ fn array_identity_and_roundtrip() {
367
+ let a = Value::Array(Rc::new(RefCell::new(vec![Value::Number(1.0), Value::Number(2.0)])));
368
+ let (CoreValue::Array(t1), CoreValue::Array(t2)) =
369
+ (eval_to_core(&a).unwrap(), eval_to_core(&a).unwrap())
370
+ else {
371
+ panic!();
372
+ };
373
+ assert!(VmRef::ptr_eq(&t1, &t2));
374
+ let back = core_to_eval(CoreValue::Array(t1));
375
+ let (Value::Array(orig), Value::Array(round)) = (&a, &back) else { panic!() };
376
+ assert!(Rc::ptr_eq(orig, round));
377
+ }
378
+
379
+ /// A self-referential object must terminate (insert-before-recurse) and its twin's
380
+ /// self-property must be the twin itself — mirroring the eval-side cycle.
381
+ #[test]
382
+ fn cyclic_object_converts_without_recursing_forever() {
383
+ let k = eval_obj(&[]);
384
+ if let Value::Object(o) = &k {
385
+ let self_ref = k.clone();
386
+ o.borrow_mut().strings.insert(Arc::from("me"), self_ref);
387
+ }
388
+ let CoreValue::Object(twin) = eval_to_core(&k).unwrap() else { panic!() };
389
+ let inner = twin.borrow().strings.get("me").cloned();
390
+ let Some(CoreValue::Object(inner)) = inner else { panic!("self prop missing") };
391
+ assert!(VmRef::ptr_eq(&twin, &inner), "cycle must map to the twin itself");
392
+ }
393
+
394
+ /// Dropping the eval object invalidates its cache entry: a NEW object (even if the allocator
395
+ /// reuses the address) must get a fresh twin, never the stale one (the #250 lesson).
396
+ #[test]
397
+ fn dead_eval_object_never_resurrects_stale_twin() {
398
+ let mut stale_twin = None;
399
+ for _ in 0..64 {
400
+ let k = eval_obj(&[("x", Value::Number(7.0))]);
401
+ let CoreValue::Object(t) = eval_to_core(&k).unwrap() else { panic!() };
402
+ if let Some(prev) = &stale_twin {
403
+ assert!(
404
+ !VmRef::ptr_eq(prev, &t),
405
+ "a fresh eval object must never receive a dead object's twin"
406
+ );
407
+ }
408
+ stale_twin = Some(t);
409
+ // k drops here — its address is free for reuse by the next iteration.
410
+ }
411
+ }
412
+ }
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tishlang_lsp"
3
- version = "2.39.0"
3
+ version = "3.0.0"
4
4
  edition = "2021"
5
5
  description = "Language Server Protocol implementation for Tish"
6
6
  license-file = { workspace = true }
@@ -29,3 +29,5 @@ walkdir = "2"
29
29
  # implements `Service` against (0.4), or the trait bound on `.call()` won't be satisfied. `util`
30
30
  # brings in `ServiceExt::ready`.
31
31
  tower = { version = "0.4", features = ["util"] }
32
+ # Temp dirs for the platform-cascade import-resolution test.
33
+ tempfile = "3"