@tishlang/tish-format 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 (90) hide show
  1. package/Cargo.toml +4 -1
  2. package/bin/tish-format +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_fmt/Cargo.toml +1 -1
  69. package/crates/tish_lsp/Cargo.toml +2 -0
  70. package/crates/tish_lsp/src/import_goto.rs +41 -14
  71. package/crates/tish_lsp/src/main.rs +18 -0
  72. package/crates/tish_native/src/build.rs +246 -0
  73. package/crates/tish_native/src/config.rs +20 -0
  74. package/crates/tish_native/src/lib.rs +16 -6
  75. package/crates/tish_parser/src/lib.rs +28 -0
  76. package/crates/tish_parser/src/parser.rs +25 -4
  77. package/crates/tish_runtime/src/lib.rs +55 -9
  78. package/crates/tish_runtime_gba/Cargo.lock +837 -0
  79. package/crates/tish_runtime_gba/Cargo.toml +16 -0
  80. package/crates/tish_runtime_gba/src/gba.rs +211 -0
  81. package/crates/tish_runtime_gba/src/lib.rs +804 -0
  82. package/crates/tish_vm/src/jit.rs +1446 -164
  83. package/crates/tish_vm/src/vm.rs +583 -173
  84. package/justfile +9 -0
  85. package/package.json +1 -1
  86. package/platform/darwin-arm64/tish-fmt +0 -0
  87. package/platform/darwin-x64/tish-fmt +0 -0
  88. package/platform/linux-arm64/tish-fmt +0 -0
  89. package/platform/linux-x64/tish-fmt +0 -0
  90. package/platform/win32-x64/tish-fmt.exe +0 -0
@@ -1,5 +1,9 @@
1
1
  //! Array builtin methods.
2
2
 
3
+ #[cfg(feature = "portable")]
4
+ #[allow(unused_imports)]
5
+ use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
6
+
3
7
  use crate::helpers::normalize_index;
4
8
  use tishlang_core::Value;
5
9
  use tishlang_core::VmRef;
@@ -18,7 +22,7 @@ pub fn from_vec(v: Vec<Value>) -> Value {
18
22
  pub fn snapshot_values(arr: &Value) -> Vec<Value> {
19
23
  match arr {
20
24
  Value::Array(a) => a.borrow().clone(),
21
- Value::NumberArray(a) => a.borrow().iter().map(|&n| Value::Number(n)).collect(),
25
+ Value::NumberArray(a) => a.borrow().to_values(),
22
26
  _ => Vec::new(),
23
27
  }
24
28
  }
@@ -30,19 +34,24 @@ pub fn snapshot_values(arr: &Value) -> Vec<Value> {
30
34
  /// unboxed f64 fast path for it too. The `None` result drives the fused lowering back to its boxed
31
35
  /// loop, so any non-numeric element is handled with identical `Value` semantics.
32
36
  pub fn as_f64_snapshot(arr: &Value) -> Option<Vec<f64>> {
33
- match arr {
34
- Value::NumberArray(a) => Some(a.borrow().clone()),
35
- Value::Array(a) => {
36
- let b = a.borrow();
37
- let mut out: Vec<f64> = Vec::with_capacity(b.len());
38
- for v in b.iter() {
39
- match v {
40
- Value::Number(n) => out.push(*n),
41
- _ => return None,
42
- }
37
+ let extract = |vals: &[Value]| -> Option<Vec<f64>> {
38
+ let mut out: Vec<f64> = Vec::with_capacity(vals.len());
39
+ for v in vals {
40
+ match v {
41
+ Value::Number(n) => out.push(*n),
42
+ _ => return None,
43
43
  }
44
- Some(out)
45
44
  }
45
+ Some(out)
46
+ };
47
+ match arr {
48
+ // Still packed: clone the f64s directly. Deopted: extract from the boxed values (None if
49
+ // any is non-numeric — the caller then uses the boxed path).
50
+ Value::NumberArray(a) => match a.borrow().as_packed() {
51
+ Some(v) => Some(v.clone()),
52
+ None => extract(a.borrow().as_boxed().map(|v| v.as_slice()).unwrap_or(&[])),
53
+ },
54
+ Value::Array(a) => extract(&a.borrow()),
46
55
  _ => None,
47
56
  }
48
57
  }
@@ -60,10 +69,10 @@ pub fn len(arr: &Value) -> Option<usize> {
60
69
  /// can use this deopt helper rather than changing every `if let Value::Array` branch.
61
70
  /// Returns the original value unchanged for anything that isn't a `NumberArray`.
62
71
  #[inline]
63
- fn as_boxed_array(arr: &Value) -> std::borrow::Cow<'_, Value> {
72
+ fn as_boxed_array(arr: &Value) -> alloc::borrow::Cow<'_, Value> {
64
73
  match arr {
65
- Value::NumberArray(na) => std::borrow::Cow::Owned(Value::materialize_number_array(na)),
66
- other => std::borrow::Cow::Borrowed(other),
74
+ Value::NumberArray(na) => alloc::borrow::Cow::Owned(Value::materialize_number_array(na)),
75
+ other => alloc::borrow::Cow::Borrowed(other),
67
76
  }
68
77
  }
69
78
 
@@ -81,7 +90,9 @@ fn packed_snapshot<'c>(
81
90
  callback: &'c Value,
82
91
  ) -> Option<(Vec<f64>, &'c tishlang_core::NativeFn)> {
83
92
  match (arr, callback) {
84
- (Value::NumberArray(na), Value::Function(cb)) => Some((na.borrow().clone(), cb)),
93
+ (Value::NumberArray(na), Value::Function(cb)) => {
94
+ na.borrow().as_packed().map(|v| (v.clone(), cb))
95
+ }
85
96
  _ => None,
86
97
  }
87
98
  }
@@ -282,8 +293,20 @@ pub fn shuffle(arr: &Value) -> Value {
282
293
  let arr = as_boxed_array(arr); let arr = &*arr;
283
294
  if let Value::Array(arr) = arr {
284
295
  let mut v = arr.borrow().clone();
285
- use rand::seq::SliceRandom;
286
- v.shuffle(&mut rand::rng());
296
+ #[cfg(not(feature = "portable"))]
297
+ {
298
+ use rand::seq::SliceRandom;
299
+ v.shuffle(&mut rand::rng());
300
+ }
301
+ // Fisher-Yates with the runtime RNG hook (no `rand` on GBA).
302
+ #[cfg(feature = "portable")]
303
+ {
304
+ let n = v.len();
305
+ for i in (1..n).rev() {
306
+ let j = (tishlang_core::next_u64() % (i as u64 + 1)) as usize;
307
+ v.swap(i, j);
308
+ }
309
+ }
287
310
  Value::Array(VmRef::new(v))
288
311
  } else {
289
312
  Value::Null
@@ -404,10 +427,13 @@ pub fn concat(arr: &Value, args: &[Value]) -> Value {
404
427
  if let Value::Array(arr) = arr {
405
428
  let mut result = arr.borrow().clone();
406
429
  for v in args {
407
- if let Value::Array(other) = v {
408
- result.extend(other.borrow().iter().cloned());
409
- } else {
410
- result.push(v.clone());
430
+ match v {
431
+ Value::Array(other) => result.extend(other.borrow().iter().cloned()),
432
+ // A packed `NumberArray` arg spreads its numbers, same as a boxed array — under
433
+ // TISH_PACKED_ARRAYS=1 a numeric literal `[3,4]` is a NumberArray, so
434
+ // `[1,2].concat([3,4])` must flatten to `[1,2,3,4]`, not push the array whole.
435
+ Value::NumberArray(other) => result.extend(other.borrow().to_values()),
436
+ _ => result.push(v.clone()),
411
437
  }
412
438
  }
413
439
  Value::Array(VmRef::new(result))
@@ -421,9 +447,19 @@ pub fn flat(arr: &Value, depth: &Value) -> Value {
421
447
  fn flatten(arr: &[Value], depth: i32, result: &mut Vec<Value>) {
422
448
  for v in arr {
423
449
  if depth > 0 {
424
- if let Value::Array(inner) = v {
425
- flatten(&inner.borrow(), depth - 1, result);
426
- continue;
450
+ match v {
451
+ Value::Array(inner) => {
452
+ flatten(&inner.borrow(), depth - 1, result);
453
+ continue;
454
+ }
455
+ // A packed `NumberArray` inner flattens like a boxed array (its numbers are the
456
+ // flattened elements) — `[[1,2],[3,4]].flat()` where the inners packed must not
457
+ // leave a `NumberArray` sitting as a scalar element.
458
+ Value::NumberArray(inner) => {
459
+ result.extend(inner.borrow().to_values());
460
+ continue;
461
+ }
462
+ _ => {}
427
463
  }
428
464
  }
429
465
  result.push(v.clone());
@@ -871,7 +907,7 @@ pub fn flat_map(arr: &Value, callback: &Value) -> Value {
871
907
 
872
908
  fn sort_by_impl<F>(arr: &Value, cmp: F) -> Value
873
909
  where
874
- F: FnMut(&Value, &Value) -> std::cmp::Ordering,
910
+ F: FnMut(&Value, &Value) -> core::cmp::Ordering,
875
911
  {
876
912
  if let Value::Array(arr) = arr {
877
913
  arr.borrow_mut().sort_by(cmp);
@@ -895,7 +931,7 @@ pub fn sort_with_comparator(arr: &Value, comparator: &Value) -> Value {
895
931
  // Mutex under send-values and self-deadlock the worker (or panic under RefCell) — the exact
896
932
  // reentrant-lock class fixed for the captured-cell path in #218/#338. `mem::take` leaves the
897
933
  // array empty for the duration of the sort; the final result is written back below.
898
- let mut elements: Vec<Value> = std::mem::take(&mut *arr.borrow_mut());
934
+ let mut elements: Vec<Value> = core::mem::take(&mut *arr.borrow_mut());
899
935
  let len = elements.len();
900
936
  let mut indices: Vec<usize> = (0..len).collect();
901
937
  let mut args_buf: [Value; 2] = [Value::Null, Value::Null];
@@ -904,14 +940,14 @@ pub fn sort_with_comparator(arr: &Value, comparator: &Value) -> Value {
904
940
  // #303: once the comparator has thrown, stop invoking it — return Equal so the sort can
905
941
  // unwind. Avoids extra comparator calls (and their side effects) after the throw.
906
942
  if tishlang_core::has_pending_throw() {
907
- return std::cmp::Ordering::Equal;
943
+ return core::cmp::Ordering::Equal;
908
944
  }
909
945
  args_buf[0] = elements[a].clone();
910
946
  args_buf[1] = elements[b].clone();
911
947
  match cmp_fn.call(&args_buf) {
912
- Value::Number(n) if n < 0.0 => std::cmp::Ordering::Less,
913
- Value::Number(n) if n > 0.0 => std::cmp::Ordering::Greater,
914
- _ => std::cmp::Ordering::Equal,
948
+ Value::Number(n) if n < 0.0 => core::cmp::Ordering::Less,
949
+ Value::Number(n) if n > 0.0 => core::cmp::Ordering::Greater,
950
+ _ => core::cmp::Ordering::Equal,
915
951
  }
916
952
  });
917
953
 
@@ -923,7 +959,7 @@ pub fn sort_with_comparator(arr: &Value, comparator: &Value) -> Value {
923
959
  } else {
924
960
  let sorted: Vec<Value> = indices
925
961
  .into_iter()
926
- .map(|i| std::mem::replace(&mut elements[i], Value::Null))
962
+ .map(|i| core::mem::replace(&mut elements[i], Value::Null))
927
963
  .collect();
928
964
  *arr.borrow_mut() = sorted;
929
965
  }
@@ -933,12 +969,12 @@ pub fn sort_with_comparator(arr: &Value, comparator: &Value) -> Value {
933
969
  }
934
970
  }
935
971
 
936
- fn num_cmp(a: &Value, b: &Value, asc: bool) -> std::cmp::Ordering {
972
+ fn num_cmp(a: &Value, b: &Value, asc: bool) -> core::cmp::Ordering {
937
973
  let (na, nb) = match (a, b) {
938
974
  (Value::Number(a), Value::Number(b)) => (*a, *b),
939
975
  _ => (f64::NAN, f64::NAN),
940
976
  };
941
- let cmp = na.partial_cmp(&nb).unwrap_or(std::cmp::Ordering::Equal);
977
+ let cmp = na.partial_cmp(&nb).unwrap_or(core::cmp::Ordering::Equal);
942
978
  if asc {
943
979
  cmp
944
980
  } else {
@@ -961,13 +997,15 @@ pub fn sort_numeric_desc(arr: &Value) -> Value {
961
997
  fn sort_numeric_impl(arr: &Value, asc: bool) -> Value {
962
998
  // NumberArray fast path: sort the Vec<f64> directly — no unbox pass, no rebox.
963
999
  if let Value::NumberArray(a) = arr {
964
- let mut g = a.borrow_mut();
965
- if asc {
966
- g.sort_unstable_by(|x, y| x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal));
967
- } else {
968
- g.sort_unstable_by(|x, y| y.partial_cmp(x).unwrap_or(std::cmp::Ordering::Equal));
1000
+ // Fast path only while still packed; a deopted numeric array sorts via the boxed path below.
1001
+ if let Some(nums) = a.borrow_mut().as_packed_mut() {
1002
+ if asc {
1003
+ nums.sort_unstable_by(|x, y| x.partial_cmp(y).unwrap_or(core::cmp::Ordering::Equal));
1004
+ } else {
1005
+ nums.sort_unstable_by(|x, y| y.partial_cmp(x).unwrap_or(core::cmp::Ordering::Equal));
1006
+ }
1007
+ return Value::NumberArray(a.clone());
969
1008
  }
970
- return Value::NumberArray(a.clone());
971
1009
  }
972
1010
  if let Value::Array(a) = arr {
973
1011
  {
@@ -982,11 +1020,11 @@ fn sort_numeric_impl(arr: &Value, asc: bool) -> Value {
982
1020
  .collect();
983
1021
  if asc {
984
1022
  nums.sort_unstable_by(|x, y| {
985
- x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)
1023
+ x.partial_cmp(y).unwrap_or(core::cmp::Ordering::Equal)
986
1024
  });
987
1025
  } else {
988
1026
  nums.sort_unstable_by(|x, y| {
989
- y.partial_cmp(x).unwrap_or(std::cmp::Ordering::Equal)
1027
+ y.partial_cmp(x).unwrap_or(core::cmp::Ordering::Equal)
990
1028
  });
991
1029
  }
992
1030
  for (slot, n) in g.iter_mut().zip(nums) {
@@ -1004,11 +1042,11 @@ fn sort_numeric_impl(arr: &Value, asc: bool) -> Value {
1004
1042
 
1005
1043
  /// Sort array of objects by numeric property: arr.sort((a,b)=>a.prop-b.prop)
1006
1044
  pub fn sort_by_property_numeric(arr: &Value, prop: &str, asc: bool) -> Value {
1007
- let prop_arc = std::sync::Arc::from(prop);
1045
+ let prop_arc = tishlang_core::Arc::from(prop);
1008
1046
  sort_by_impl(arr, move |a, b| {
1009
1047
  let na = get_prop_number(a, &prop_arc);
1010
1048
  let nb = get_prop_number(b, &prop_arc);
1011
- let cmp = na.partial_cmp(&nb).unwrap_or(std::cmp::Ordering::Equal);
1049
+ let cmp = na.partial_cmp(&nb).unwrap_or(core::cmp::Ordering::Equal);
1012
1050
  if asc {
1013
1051
  cmp
1014
1052
  } else {
@@ -1017,7 +1055,7 @@ pub fn sort_by_property_numeric(arr: &Value, prop: &str, asc: bool) -> Value {
1017
1055
  })
1018
1056
  }
1019
1057
 
1020
- fn get_prop_number(v: &Value, prop: &std::sync::Arc<str>) -> f64 {
1058
+ fn get_prop_number(v: &Value, prop: &tishlang_core::Arc<str>) -> f64 {
1021
1059
  match v {
1022
1060
  Value::Object(o) => o
1023
1061
  .borrow()
@@ -1097,19 +1135,19 @@ pub fn sort_by_keys(arr: &Value, keys: &[(&[&str], bool)]) -> Value {
1097
1135
  for (col, (_, asc)) in keys.iter().enumerate() {
1098
1136
  let ord = ka[col]
1099
1137
  .partial_cmp(&kb[col])
1100
- .unwrap_or(std::cmp::Ordering::Equal);
1138
+ .unwrap_or(core::cmp::Ordering::Equal);
1101
1139
  let ord = if *asc { ord } else { ord.reverse() };
1102
- if ord != std::cmp::Ordering::Equal {
1140
+ if ord != core::cmp::Ordering::Equal {
1103
1141
  return ord;
1104
1142
  }
1105
1143
  }
1106
- std::cmp::Ordering::Equal
1144
+ core::cmp::Ordering::Equal
1107
1145
  });
1108
1146
  // Undecorate: apply the permutation in place.
1109
- let mut elements: Vec<Value> = std::mem::take(&mut *g);
1147
+ let mut elements: Vec<Value> = core::mem::take(&mut *g);
1110
1148
  *g = decorated
1111
1149
  .into_iter()
1112
- .map(|(i, _)| std::mem::replace(&mut elements[i], Value::Null))
1150
+ .map(|(i, _)| core::mem::replace(&mut elements[i], Value::Null))
1113
1151
  .collect();
1114
1152
  drop(g);
1115
1153
  Value::Array(a.clone())
@@ -1125,15 +1163,15 @@ mod packed_hof_tests {
1125
1163
  //! on it. These run with packing semantics directly (the helpers don't read the env flag; a
1126
1164
  //! `NumberArray` value is enough to take the fast path).
1127
1165
  use super::*;
1128
- use tishlang_core::Value;
1166
+ use tishlang_core::{NumArrayBacking, Value};
1129
1167
 
1130
1168
  fn na(xs: &[f64]) -> Value {
1131
- Value::NumberArray(VmRef::new(xs.to_vec()))
1169
+ Value::NumberArray(VmRef::new(NumArrayBacking::Packed(xs.to_vec())))
1132
1170
  }
1133
1171
  fn nums(v: &Value) -> Vec<f64> {
1134
1172
  match v {
1135
1173
  Value::Array(a) => a.borrow().iter().map(|e| e.as_number().unwrap_or(f64::NAN)).collect(),
1136
- Value::NumberArray(a) => a.borrow().clone(),
1174
+ Value::NumberArray(a) => a.borrow().to_values().iter().map(|e| e.as_number().unwrap_or(f64::NAN)).collect(),
1137
1175
  _ => vec![],
1138
1176
  }
1139
1177
  }
@@ -1317,7 +1355,7 @@ mod sort_by_keys_tests {
1317
1355
  //! EXACT same order as running the equivalent boxed comparator through
1318
1356
  //! `sort_with_comparator`, since cross-backend parity depends on it.
1319
1357
  use super::*;
1320
- use std::sync::Arc;
1358
+ use tishlang_core::Arc;
1321
1359
  use tishlang_core::Value;
1322
1360
 
1323
1361
  fn rec(key: f64, idx: f64) -> Value {
@@ -1533,4 +1571,42 @@ mod reentrancy_tests_382 {
1533
1571
  _ => panic!("sort should return an array"),
1534
1572
  }
1535
1573
  }
1574
+
1575
+ fn nums(v: &Value) -> Vec<f64> {
1576
+ match v {
1577
+ Value::Array(a) => a.borrow().iter().filter_map(|e| e.as_number()).collect(),
1578
+ _ => panic!("expected boxed array result"),
1579
+ }
1580
+ }
1581
+
1582
+ /// #505: a packed `NumberArray` concat ARG must spread its numbers, not push the array whole.
1583
+ #[test]
1584
+ fn concat_spreads_packed_number_array_arg() {
1585
+ let recv = Value::Array(VmRef::new(vec![Value::Number(1.0), Value::Number(2.0)]));
1586
+ let packed = Value::number_array(vec![3.0, 4.0]);
1587
+ let out = concat(&recv, &[packed]);
1588
+ assert_eq!(nums(&out), vec![1.0, 2.0, 3.0, 4.0]);
1589
+ // Mixed with a boxed array + a scalar, order preserved.
1590
+ let boxed = Value::Array(VmRef::new(vec![Value::Number(5.0)]));
1591
+ let out2 = concat(&recv, &[boxed, Value::Number(9.0)]);
1592
+ assert_eq!(nums(&out2), vec![1.0, 2.0, 5.0, 9.0]);
1593
+ }
1594
+
1595
+ /// #507: a packed `NumberArray` INNER element must flatten like a boxed array.
1596
+ #[test]
1597
+ fn flat_flattens_packed_number_array_inner() {
1598
+ let outer = Value::Array(VmRef::new(vec![
1599
+ Value::number_array(vec![1.0, 2.0]),
1600
+ Value::Number(3.0),
1601
+ Value::Array(VmRef::new(vec![Value::Number(4.0)])),
1602
+ ]));
1603
+ let out = flat(&outer, &Value::Number(1.0));
1604
+ assert_eq!(nums(&out), vec![1.0, 2.0, 3.0, 4.0]);
1605
+ // depth 0 keeps the packed inner as-is (not flattened) — matches boxed-array behavior.
1606
+ let out0 = flat(&outer, &Value::Number(0.0));
1607
+ match out0 {
1608
+ Value::Array(a) => assert_eq!(a.borrow().len(), 3, "depth 0 flattens nothing"),
1609
+ _ => panic!("expected array"),
1610
+ }
1611
+ }
1536
1612
  }
@@ -23,9 +23,13 @@
23
23
  //! - Keys/values use **SameValueZero** equality (NaN equals NaN; `+0`/`-0` are the same; objects by
24
24
  //! identity).
25
25
 
26
- use std::any::Any;
27
- use std::hash::{Hash, Hasher};
28
- use std::sync::Arc;
26
+ #[cfg(feature = "portable")]
27
+ #[allow(unused_imports)]
28
+ use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
29
+
30
+ use core::any::Any;
31
+ use core::hash::{Hash, Hasher};
32
+ use tishlang_core::Arc;
29
33
 
30
34
  use indexmap::IndexMap;
31
35
  use tishlang_core::{NativeFn, ObjectMap, TishOpaque, Value, VmRef};
@@ -120,7 +124,7 @@ impl indexmap::Equivalent<Key> for KeyRef<'_> {
120
124
  /// stores `Value::Null` values and iterates its keys; a `Map` stores the mapped value. Uses `ahash`
121
125
  /// (the same fast hasher `tish_core`'s `PropMap` uses) rather than the default SipHash — iteration
122
126
  /// order is insertion order regardless of the hasher, so this is a pure constant-factor speedup.
123
- type Store = VmRef<IndexMap<Key, Value, ahash::RandomState>>;
127
+ type Store = VmRef<IndexMap<Key, Value, tishlang_core::RandomState>>;
124
128
 
125
129
  /// Opaque whose length reports a collection's live element count. Stored under [`SIZE_SLOT`] on every
126
130
  /// `Set`/`Map` instance.
@@ -226,7 +230,7 @@ fn same_value_zero(a: &Value, b: &Value) -> bool {
226
230
  fn iter_elements(v: &Value) -> Vec<Value> {
227
231
  match v {
228
232
  Value::Array(a) => a.borrow().iter().cloned().collect(),
229
- Value::NumberArray(a) => a.borrow().iter().map(|n| Value::Number(*n)).collect(),
233
+ Value::NumberArray(a) => a.borrow().to_values(),
230
234
  _ => Vec::new(),
231
235
  }
232
236
  }
@@ -1,7 +1,13 @@
1
1
  //! `new` lowering for non-JS targets: `construct(callee, args)` approximates JS `[[Construct]]`.
2
2
  //! Browser-exact behavior remains on `tish build --target js`.
3
3
 
4
- use std::sync::Arc;
4
+ #[cfg(feature = "portable")]
5
+ #[allow(unused_imports)]
6
+ use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
7
+ #[cfg(feature = "portable")]
8
+ use tishlang_core::FloatExt;
9
+
10
+ use tishlang_core::Arc;
5
11
  use tishlang_core::{ObjectMap, Value, VmRef};
6
12
 
7
13
  const CONSTRUCT: &str = "__construct";
@@ -11,7 +11,13 @@
11
11
  //! is no timezone database in the core builtins (keeps them lean + wasm-friendly). `getTime`,
12
12
  //! `valueOf`, the `getUTC*` family and `toISOString` are therefore fully deterministic everywhere.
13
13
 
14
- use std::sync::Arc;
14
+ #[cfg(feature = "portable")]
15
+ #[allow(unused_imports)]
16
+ use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
17
+ #[cfg(feature = "portable")]
18
+ use tishlang_core::FloatExt;
19
+
20
+ use tishlang_core::Arc;
15
21
  use tishlang_core::{ObjectMap, Value, VmRef};
16
22
 
17
23
  const CONSTRUCT: &str = "__construct";
@@ -23,6 +29,7 @@ const MONTHS: [&str; 12] = [
23
29
  ];
24
30
 
25
31
  /// Current wall-clock time as epoch milliseconds.
32
+ #[cfg(not(feature = "portable"))]
26
33
  fn now_ms() -> f64 {
27
34
  std::time::SystemTime::now()
28
35
  .duration_since(std::time::UNIX_EPOCH)
@@ -30,6 +37,13 @@ fn now_ms() -> f64 {
30
37
  .unwrap_or(0.0)
31
38
  }
32
39
 
40
+ /// No OS clock on GBA: read the runtime-installed clock hook (defaults to 0 until
41
+ /// the facade wires up an agb timer). See `tishlang_core::install_clock`.
42
+ #[cfg(feature = "portable")]
43
+ fn now_ms() -> f64 {
44
+ tishlang_core::now_ms()
45
+ }
46
+
33
47
  /// A `(method name, field extractor)` pair for the numeric `Date` getters. The explicit
34
48
  /// `fn(&Civil) -> i64` coerces the (non-capturing) extractor closures to a single fn-pointer type so
35
49
  /// they can live in one array.
@@ -3,7 +3,13 @@
3
3
  //! Used by both tishlang_vm (bytecode) and tishlang_runtime (compiled). Keeps tishlang_vm
4
4
  //! independent of tishlang_runtime.
5
5
 
6
- use std::sync::Arc;
6
+ #[cfg(feature = "portable")]
7
+ #[allow(unused_imports)]
8
+ use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
9
+ #[cfg(feature = "portable")]
10
+ use tishlang_core::FloatExt;
11
+
12
+ use tishlang_core::Arc;
7
13
  use tishlang_core::VmRef;
8
14
  use tishlang_core::{percent_decode, percent_decode_component, percent_encode, percent_encode_component, ObjectMap, Value};
9
15
 
@@ -79,7 +85,7 @@ pub fn array_from(args: &[Value]) -> Value {
79
85
  let source = args.first().cloned().unwrap_or(Value::Null);
80
86
  let items: Vec<Value> = match &source {
81
87
  Value::Array(a) => a.borrow().clone(),
82
- Value::NumberArray(a) => a.borrow().iter().map(|n| Value::Number(*n)).collect(),
88
+ Value::NumberArray(a) => a.borrow().to_values(),
83
89
  // `drain_iterator` covers String (chars), Set/Map, and any iterator object.
84
90
  other => {
85
91
  if let Some(d) = tishlang_core::drain_iterator(other) {
@@ -296,6 +302,25 @@ pub fn string_from_char_code(args: &[Value]) -> Value {
296
302
  Value::String(s.into())
297
303
  }
298
304
 
305
+ /// Object.freeze(obj) — mark the object frozen so later property writes throw a catchable
306
+ /// TypeError (#437). Returns the same object; non-objects pass through unchanged (JS semantics).
307
+ pub fn object_freeze(args: &[Value]) -> Value {
308
+ if let Some(Value::Object(obj)) = args.first() {
309
+ obj.borrow_mut().frozen = true;
310
+ }
311
+ args.first().cloned().unwrap_or(Value::Null)
312
+ }
313
+
314
+ /// Object.isFrozen(obj) — true if the object was frozen. Primitives are considered frozen (JS).
315
+ pub fn object_is_frozen(args: &[Value]) -> Value {
316
+ match args.first() {
317
+ Some(Value::Object(obj)) => Value::Bool(obj.borrow().frozen),
318
+ // Arrays are mutable and not freezable in this slice → not frozen; other primitives are.
319
+ Some(Value::Array(_)) => Value::Bool(false),
320
+ _ => Value::Bool(true),
321
+ }
322
+ }
323
+
299
324
  /// Object.keys(obj)
300
325
  pub fn object_keys(args: &[Value]) -> Value {
301
326
  match args.first() {
@@ -319,12 +344,33 @@ pub fn object_keys(args: &[Value]) -> Value {
319
344
  .collect();
320
345
  Value::Array(VmRef::new(keys))
321
346
  }
347
+ // A packed `NumberArray` has the same own enumerable keys (its index strings) — without
348
+ // this, `for (k in arr)` (lowered to `for-of Object.keys`) enumerated NOTHING under
349
+ // TISH_PACKED_ARRAYS=1. Mirrors the Array arm.
350
+ Some(Value::NumberArray(arr)) => {
351
+ let len = arr.borrow().len();
352
+ let keys: Vec<Value> = (0..len)
353
+ .map(|i| Value::String(tishlang_core::ArcStr::from(i.to_string().as_str())))
354
+ .collect();
355
+ Value::Array(VmRef::new(keys))
356
+ }
357
+ // A by-reference boxed typed struct (GBA/portable): materialise it to its ordered object and
358
+ // enumerate that, so `Object.keys(s)` / `for (k in s)` (lowered to `for-of Object.keys`) see
359
+ // the field names instead of nothing. (JSON/display already materialise; this closes the
360
+ // reflection paths so the boxed struct is enumerable exactly like the object it stands for.)
361
+ #[cfg(feature = "portable")]
362
+ Some(Value::Struct(s)) => object_keys(&[s.borrow().tish_to_object()]),
322
363
  _ => Value::Array(VmRef::new(Vec::new())),
323
364
  }
324
365
  }
325
366
 
326
367
  /// Object.values(obj)
327
368
  pub fn object_values(args: &[Value]) -> Value {
369
+ // A boxed typed struct materialises to its ordered object first (see `object_keys`).
370
+ #[cfg(feature = "portable")]
371
+ if let Some(Value::Struct(s)) = args.first() {
372
+ return object_values(&[s.borrow().tish_to_object()]);
373
+ }
328
374
  if let Some(Value::Object(obj)) = args.first() {
329
375
  let obj_borrow = obj.borrow();
330
376
  let values: Vec<Value> = obj_borrow.strings.values().cloned().collect();
@@ -336,6 +382,11 @@ pub fn object_values(args: &[Value]) -> Value {
336
382
 
337
383
  /// Object.entries(obj)
338
384
  pub fn object_entries(args: &[Value]) -> Value {
385
+ // A boxed typed struct materialises to its ordered object first (see `object_keys`).
386
+ #[cfg(feature = "portable")]
387
+ if let Some(Value::Struct(s)) = args.first() {
388
+ return object_entries(&[s.borrow().tish_to_object()]);
389
+ }
339
390
  if let Some(Value::Object(obj)) = args.first() {
340
391
  let obj_borrow = obj.borrow();
341
392
  let entries: Vec<Value> = obj_borrow
@@ -372,6 +423,17 @@ pub fn object_assign(args: &[Value]) -> Value {
372
423
  target_mut.strings.reserve(additional_capacity);
373
424
 
374
425
  for source in args.iter().skip(1) {
426
+ // A boxed typed struct source materialises to its ordered object first (GBA/portable), so
427
+ // `Object.assign(t, s)` copies its fields instead of silently skipping it (see `object_keys`).
428
+ #[cfg(feature = "portable")]
429
+ let struct_obj;
430
+ #[cfg(feature = "portable")]
431
+ let source: &Value = if let Value::Struct(s) = source {
432
+ struct_obj = s.borrow().tish_to_object();
433
+ &struct_obj
434
+ } else {
435
+ source
436
+ };
375
437
  if let Value::Object(src) = source {
376
438
  let src_borrow = src.borrow();
377
439
  for (k, v) in src_borrow.strings.iter() {
@@ -495,7 +557,7 @@ pub fn js_parse_float(s: &str) -> f64 {
495
557
  pub fn object_from_entries(args: &[Value]) -> Value {
496
558
  if let Some(Value::Array(entries)) = args.first() {
497
559
  let entries_borrow = entries.borrow();
498
- let mut obj: ObjectMap = ObjectMap::with_capacity(entries_borrow.len());
560
+ let mut obj: ObjectMap = ObjectMap::with_capacity_and_hasher(entries_borrow.len(), Default::default());
499
561
 
500
562
  for entry in entries_borrow.iter() {
501
563
  if let Value::Array(pair) = entry {
@@ -1,6 +1,10 @@
1
1
  //! Common helper functions used across builtin implementations.
2
2
 
3
- use std::sync::Arc;
3
+ #[cfg(feature = "portable")]
4
+ #[allow(unused_imports)]
5
+ use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
6
+
7
+ use tishlang_core::Arc;
4
8
  use tishlang_core::{ObjectMap, Value};
5
9
 
6
10
  /// Normalize an array index, handling negative indices.
@@ -20,8 +24,8 @@ pub fn normalize_index(idx: &Value, len: i64, default: usize) -> usize {
20
24
  }
21
25
 
22
26
  /// Create an error object with a single "error" field.
23
- pub fn make_error_value(e: impl std::fmt::Display) -> Value {
24
- let mut obj = ObjectMap::with_capacity(1);
27
+ pub fn make_error_value(e: impl core::fmt::Display) -> Value {
28
+ let mut obj = ObjectMap::with_capacity_and_hasher(1, Default::default());
25
29
  obj.insert(Arc::from("error"), Value::String(e.to_string().into()));
26
30
  Value::object(obj)
27
31
  }
@@ -12,7 +12,11 @@
12
12
  //! array-returning version behaved; mid-iteration mutation of the source is not reflected (a
13
13
  //! live iterator is a follow-up).
14
14
 
15
- use std::sync::Arc;
15
+ #[cfg(feature = "portable")]
16
+ #[allow(unused_imports)]
17
+ use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
18
+
19
+ use tishlang_core::Arc;
16
20
 
17
21
  use tishlang_core::{ObjectMap, Value, VmRef};
18
22
 
@@ -3,6 +3,9 @@
3
3
  //! Used by the compiled runtime (tishlang_runtime) and bytecode VM (tishlang_vm). The
4
4
  //! interpreter (tishlang_eval) implements builtins inline due to different Value
5
5
  //! and native signatures.
6
+ #![cfg_attr(feature = "portable", no_std)]
7
+
8
+ extern crate alloc;
6
9
 
7
10
  pub mod array;
8
11
  pub mod collections;
@@ -1,5 +1,11 @@
1
1
  //! Math builtin functions.
2
2
 
3
+ #[cfg(feature = "portable")]
4
+ #[allow(unused_imports)]
5
+ use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
6
+ #[cfg(feature = "portable")]
7
+ use tishlang_core::FloatExt;
8
+
3
9
  use crate::helpers::extract_num;
4
10
  use tishlang_core::{to_int32, to_uint32, Value};
5
11
 
@@ -120,10 +126,18 @@ pub fn pow(args: &[Value]) -> Value {
120
126
  Value::Number(base.powf(exp))
121
127
  }
122
128
 
129
+ #[cfg(not(feature = "portable"))]
123
130
  pub fn random(_args: &[Value]) -> Value {
124
131
  Value::Number(rand::random::<f64>())
125
132
  }
126
133
 
134
+ /// No OS entropy on GBA: read the runtime-installed RNG hook (a seedable xorshift
135
+ /// fallback until the facade wires up `agb::rng`). See `tishlang_core::install_rng`.
136
+ #[cfg(feature = "portable")]
137
+ pub fn random(_args: &[Value]) -> Value {
138
+ Value::Number(tishlang_core::random_f64())
139
+ }
140
+
127
141
  pub fn sign(args: &[Value]) -> Value {
128
142
  let n = extract_num(args.first()).unwrap_or(f64::NAN);
129
143
  Value::Number(if n.is_nan() {