@tishlang/tish-lint 2.39.0 → 2.43.8

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 (74) hide show
  1. package/Cargo.toml +4 -1
  2. package/bin/tish-lint +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/scope_merge.rs +79 -0
  6. package/crates/tish_build_utils/src/lib.rs +27 -1
  7. package/crates/tish_builtins/Cargo.toml +14 -6
  8. package/crates/tish_builtins/src/array.rs +130 -54
  9. package/crates/tish_builtins/src/collections.rs +9 -5
  10. package/crates/tish_builtins/src/construct.rs +7 -1
  11. package/crates/tish_builtins/src/date.rs +15 -1
  12. package/crates/tish_builtins/src/globals.rs +38 -3
  13. package/crates/tish_builtins/src/helpers.rs +7 -3
  14. package/crates/tish_builtins/src/iterator.rs +5 -1
  15. package/crates/tish_builtins/src/lib.rs +3 -0
  16. package/crates/tish_builtins/src/math.rs +14 -0
  17. package/crates/tish_builtins/src/number.rs +72 -6
  18. package/crates/tish_builtins/src/object.rs +6 -1
  19. package/crates/tish_builtins/src/string.rs +13 -2
  20. package/crates/tish_builtins/src/symbol.rs +17 -17
  21. package/crates/tish_builtins/src/typedarrays.rs +9 -2
  22. package/crates/tish_bytecode/src/compiler.rs +76 -2
  23. package/crates/tish_bytecode/src/lib.rs +1 -1
  24. package/crates/tish_bytecode/src/opcode.rs +77 -2
  25. package/crates/tish_compile/src/codegen.rs +806 -46
  26. package/crates/tish_compile/src/infer.rs +37 -41
  27. package/crates/tish_compile/src/lib.rs +13 -1
  28. package/crates/tish_compile/src/platform_resolve.rs +361 -0
  29. package/crates/tish_compile/src/resolve.rs +148 -10
  30. package/crates/tish_compile/src/schemes.rs +278 -0
  31. package/crates/tish_compile/src/types.rs +141 -2
  32. package/crates/tish_compile/tests/perf_codegen_320.rs +2 -2
  33. package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +3 -1
  34. package/crates/tish_compile/tests/platform_resolve_cli.rs +164 -0
  35. package/crates/tish_compile/tests/regr_gba_numerics_gated.rs +58 -0
  36. package/crates/tish_compiler_wasm/Cargo.toml +1 -0
  37. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +56 -6
  38. package/crates/tish_core/Cargo.toml +39 -4
  39. package/crates/tish_core/src/compat.rs +506 -0
  40. package/crates/tish_core/src/console_style.rs +19 -1
  41. package/crates/tish_core/src/json.rs +34 -10
  42. package/crates/tish_core/src/lib.rs +121 -25
  43. package/crates/tish_core/src/macros.rs +1 -1
  44. package/crates/tish_core/src/shape.rs +4 -2
  45. package/crates/tish_core/src/uri.rs +8 -1
  46. package/crates/tish_core/src/value.rs +182 -54
  47. package/crates/tish_core/src/vmref.rs +17 -5
  48. package/crates/tish_eval/Cargo.toml +2 -0
  49. package/crates/tish_eval/src/eval.rs +90 -3
  50. package/crates/tish_eval/src/value.rs +8 -0
  51. package/crates/tish_eval/src/value_convert.rs +316 -21
  52. package/crates/tish_lint/Cargo.toml +1 -1
  53. package/crates/tish_lsp/Cargo.toml +2 -0
  54. package/crates/tish_lsp/src/import_goto.rs +41 -14
  55. package/crates/tish_lsp/src/main.rs +18 -0
  56. package/crates/tish_native/src/build.rs +246 -0
  57. package/crates/tish_native/src/config.rs +20 -0
  58. package/crates/tish_native/src/lib.rs +16 -6
  59. package/crates/tish_parser/src/lib.rs +28 -0
  60. package/crates/tish_parser/src/parser.rs +25 -4
  61. package/crates/tish_runtime/src/lib.rs +54 -9
  62. package/crates/tish_runtime_gba/Cargo.lock +837 -0
  63. package/crates/tish_runtime_gba/Cargo.toml +16 -0
  64. package/crates/tish_runtime_gba/src/gba.rs +211 -0
  65. package/crates/tish_runtime_gba/src/lib.rs +620 -0
  66. package/crates/tish_vm/src/jit.rs +1446 -164
  67. package/crates/tish_vm/src/vm.rs +535 -173
  68. package/justfile +9 -0
  69. package/package.json +1 -1
  70. package/platform/darwin-arm64/tish-lint +0 -0
  71. package/platform/darwin-x64/tish-lint +0 -0
  72. package/platform/linux-arm64/tish-lint +0 -0
  73. package/platform/linux-x64/tish-lint +0 -0
  74. package/platform/win32-x64/tish-lint.exe +0 -0
@@ -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_lint"
3
- version = "2.39.0"
3
+ version = "2.43.8"
4
4
  edition = "2021"
5
5
  description = "AST-based linter 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"
@@ -451,19 +451,29 @@ fn module_source(can: &Path, u: &Url, open_docs: &HashMap<Url, String>) -> Optio
451
451
  }
452
452
  }
453
453
 
454
- fn resolve_relative_tish(
455
- from_dir: &Path,
456
- from_s: &str,
457
- imported: &str,
458
- open_docs: &HashMap<Url, String>,
459
- ) -> Option<Location> {
454
+ fn resolve_relative_module_path(from_dir: &Path, from_s: &str) -> Option<std::path::PathBuf> {
455
+ let _ = tishlang_compile::apply_resolve_env(None, None);
456
+ let ctx = tishlang_compile::resolve_context();
457
+ if let Some(p) = tishlang_compile::resolve_with_platform(from_s, from_dir, ctx) {
458
+ return Some(p);
459
+ }
460
+ // Fallback: legacy exact `.tish` join (no cascade).
460
461
  let target = from_dir.join(from_s.trim_start_matches("./"));
461
462
  let target = if target.extension().is_none() {
462
463
  target.with_extension("tish")
463
464
  } else {
464
465
  target
465
466
  };
466
- let can = target.canonicalize().ok()?;
467
+ target.canonicalize().ok()
468
+ }
469
+
470
+ fn resolve_relative_tish(
471
+ from_dir: &Path,
472
+ from_s: &str,
473
+ imported: &str,
474
+ open_docs: &HashMap<Url, String>,
475
+ ) -> Option<Location> {
476
+ let can = resolve_relative_module_path(from_dir, from_s)?;
467
477
  let u = Url::from_file_path(&can).ok()?;
468
478
  let src = module_source(&can, &u, open_docs)?;
469
479
  let prog = tishlang_parser::parse(&src).ok()?;
@@ -476,13 +486,7 @@ fn resolve_relative_tish_default(
476
486
  from_s: &str,
477
487
  open_docs: &HashMap<Url, String>,
478
488
  ) -> Option<Location> {
479
- let target = from_dir.join(from_s.trim_start_matches("./"));
480
- let target = if target.extension().is_none() {
481
- target.with_extension("tish")
482
- } else {
483
- target
484
- };
485
- let can = target.canonicalize().ok()?;
489
+ let can = resolve_relative_module_path(from_dir, from_s)?;
486
490
  let u = Url::from_file_path(&can).ok()?;
487
491
  let src = module_source(&can, &u, open_docs)?;
488
492
  let prog = tishlang_parser::parse(&src).ok()?;
@@ -628,4 +632,27 @@ mod receiver_member_tests {
628
632
  let k = super::pragma_key_for_native_member(Some("window"), &[Arc::from("innerHeight")]);
629
633
  assert_eq!(k.as_deref(), Some("window.innerHeight"));
630
634
  }
635
+
636
+ // Goto-def resolves relative imports through the platform/surface cascade (same rules as
637
+ // `tish resolve-id` / disk resolve), falling back to the bare `.tish` when no context is set.
638
+ #[test]
639
+ fn resolve_relative_module_path_honors_platform_cascade() {
640
+ use tishlang_compile::{set_resolve_context, Platform, ResolveContext, Surface};
641
+ let dir = tempfile::tempdir().unwrap();
642
+ let root = dir.path();
643
+ std::fs::write(root.join("Button.tish"), "export fn Button() {}").unwrap();
644
+ std::fs::write(root.join("Button.macos.tish"), "export fn Button() {}").unwrap();
645
+
646
+ set_resolve_context(ResolveContext {
647
+ platform: Platform::Macos,
648
+ surface: Surface::Native,
649
+ });
650
+ let hit = super::resolve_relative_module_path(root, "./Button").expect("macos resolve");
651
+ assert!(hit.ends_with("Button.macos.tish"));
652
+
653
+ // No platform context → legacy bare `.tish`.
654
+ set_resolve_context(ResolveContext::default());
655
+ let base = super::resolve_relative_module_path(root, "./Button").expect("base resolve");
656
+ assert!(base.ends_with("Button.tish"));
657
+ }
631
658
  }