@tishlang/tish-lsp 2.38.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 (75) 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/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 +812 -47
  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_lsp/Cargo.toml +3 -1
  53. package/crates/tish_lsp/src/import_goto.rs +41 -14
  54. package/crates/tish_lsp/src/main.rs +18 -0
  55. package/crates/tish_native/src/build.rs +246 -0
  56. package/crates/tish_native/src/config.rs +20 -0
  57. package/crates/tish_native/src/lib.rs +16 -6
  58. package/crates/tish_parser/src/lib.rs +28 -0
  59. package/crates/tish_parser/src/parser.rs +25 -4
  60. package/crates/tish_runtime/src/http_hyper.rs +70 -0
  61. package/crates/tish_runtime/src/lib.rs +55 -10
  62. package/crates/tish_runtime/src/ws.rs +94 -21
  63. package/crates/tish_runtime_gba/Cargo.lock +837 -0
  64. package/crates/tish_runtime_gba/Cargo.toml +16 -0
  65. package/crates/tish_runtime_gba/src/gba.rs +211 -0
  66. package/crates/tish_runtime_gba/src/lib.rs +620 -0
  67. package/crates/tish_vm/src/jit.rs +1446 -164
  68. package/crates/tish_vm/src/vm.rs +535 -173
  69. package/justfile +9 -0
  70. package/package.json +1 -1
  71. package/platform/darwin-arm64/tish-lsp +0 -0
  72. package/platform/darwin-x64/tish-lsp +0 -0
  73. package/platform/linux-arm64/tish-lsp +0 -0
  74. package/platform/linux-x64/tish-lsp +0 -0
  75. package/platform/win32-x64/tish-lsp.exe +0 -0
package/Cargo.toml CHANGED
@@ -36,7 +36,10 @@ members = [
36
36
  ]
37
37
  # Generated native-build projects (`tish build`) land under `target/` when TMPDIR points there;
38
38
  # never enroll anything beneath `target/` as a workspace member.
39
- exclude = ["target"]
39
+ # GBA-only: depends on `agb`, which builds solely for thumbv4t. Excluded so host
40
+ # `cargo build` doesn't try to cross-compile it; built explicitly with the target
41
+ # (and consumed by path from generated GBA projects, not as a workspace member).
42
+ exclude = ["target", "crates/tish_runtime_gba"]
40
43
  resolver = "2"
41
44
 
42
45
  [workspace.package]
package/bin/tish-lsp CHANGED
Binary file
@@ -557,6 +557,12 @@ pub(crate) struct BuildArgs {
557
557
  pub crate_type: String,
558
558
  #[arg(long, help_heading = "Options")]
559
559
  pub no_optimize: bool,
560
+ /// Platform for RN-style module resolution (`macos`, `ios`, `windows`, `linux`, `web`).
561
+ #[arg(long, value_name = "NAME", help_heading = "Options")]
562
+ pub platform: Option<String>,
563
+ /// Surface for module resolution: `native`, `webview`, or `web` (also `TISH_SURFACE`).
564
+ #[arg(long, value_name = "NAME", help_heading = "Options")]
565
+ pub surface: Option<String>,
560
566
  /// For `--target js` project builds: emit `OUTPUT.js.map` and `//# sourceMappingURL=…` so JS/TS tools can jump to original `.tish` (implies `--no-optimize` for that build).
561
567
  #[arg(long, help_heading = "Options")]
562
568
  pub source_map: bool,
@@ -599,6 +605,12 @@ pub(crate) struct CompileModuleArgs {
599
605
  /// Project root for resolving bare specifiers / `node_modules` (defaults to the file's parent).
600
606
  #[arg(long, value_name = "DIR", help_heading = "Options")]
601
607
  pub project_root: Option<String>,
608
+ /// Platform for RN-style module resolution (`macos`, `ios`, `windows`, `linux`, `web`).
609
+ #[arg(long, value_name = "NAME", help_heading = "Options")]
610
+ pub platform: Option<String>,
611
+ /// Surface for module resolution: `native`, `webview`, or `web`.
612
+ #[arg(long, value_name = "NAME", help_heading = "Options")]
613
+ pub surface: Option<String>,
602
614
  /// Disable AST optimizations (forced on when a source map is emitted).
603
615
  #[arg(long, help_heading = "Options")]
604
616
  pub no_optimize: bool,
@@ -626,6 +638,22 @@ pub(crate) enum Commands {
626
638
  /// Compile a single `.tish` module to one ES module (for Vite dev / HMR); prints to stdout
627
639
  #[command(name = "compile-module")]
628
640
  CompileModule(CompileModuleArgs),
641
+ /// Resolve a relative `.tish` import with platform/surface cascade (for Vite / tooling)
642
+ #[command(name = "resolve-id")]
643
+ ResolveId {
644
+ /// Import specifier (must be relative, e.g. `./Button` or `./Button.tish`).
645
+ #[arg(required = true, value_name = "SOURCE", help_heading = "Arguments")]
646
+ source: String,
647
+ /// Absolute path of the importing file (directory used as resolve base).
648
+ #[arg(long, value_name = "FILE", help_heading = "Options")]
649
+ importer: Option<String>,
650
+ /// Platform token (`macos`, `ios`, `windows`, `linux`, `web`).
651
+ #[arg(long, value_name = "NAME", help_heading = "Options")]
652
+ platform: Option<String>,
653
+ /// Surface token (`native`, `webview`, `web`).
654
+ #[arg(long, value_name = "NAME", help_heading = "Options")]
655
+ surface: Option<String>,
656
+ },
629
657
  /// Parse and dump AST
630
658
  #[command(name = "dump-ast")]
631
659
  DumpAst {
@@ -73,7 +73,8 @@ fn native_build_features_from_cli(cli_features: &[String]) -> Vec<String> {
73
73
  fn argv_with_implicit_run(mut argv: Vec<String>) -> Vec<String> {
74
74
  if argv.len() >= 2 {
75
75
  let first = argv[1].as_str();
76
- const SUBCOMMANDS: &[&str] = &["run", "repl", "build", "compile-module", "dump-ast"];
76
+ const SUBCOMMANDS: &[&str] =
77
+ &["run", "repl", "build", "compile-module", "resolve-id", "dump-ast"];
77
78
  let looks_like_file = !first.starts_with('-') && !SUBCOMMANDS.contains(&first);
78
79
  if looks_like_file {
79
80
  argv.insert(1, "run".to_string());
@@ -126,6 +127,13 @@ fn main() {
126
127
  if let Some(mode) = &a.check {
127
128
  std::env::set_var("TISH_CHECK", mode);
128
129
  }
130
+ if let Err(e) = tishlang_compile::apply_resolve_env(
131
+ a.platform.as_deref(),
132
+ a.surface.as_deref(),
133
+ ) {
134
+ eprintln!("Error: {}", e);
135
+ std::process::exit(1);
136
+ }
129
137
  build_file(
130
138
  &a.file,
131
139
  &a.output,
@@ -140,7 +148,30 @@ fn main() {
140
148
  &a.jsx_import_source,
141
149
  )
142
150
  }
143
- Some(Commands::CompileModule(a)) => compile_module(&a),
151
+ Some(Commands::CompileModule(a)) => {
152
+ if let Err(e) = tishlang_compile::apply_resolve_env(
153
+ a.platform.as_deref(),
154
+ a.surface.as_deref(),
155
+ ) {
156
+ eprintln!("Error: {}", e);
157
+ std::process::exit(1);
158
+ }
159
+ compile_module(&a)
160
+ }
161
+ Some(Commands::ResolveId {
162
+ source,
163
+ importer,
164
+ platform,
165
+ surface,
166
+ }) => tishlang_compile::resolve_id_public(
167
+ &source,
168
+ importer.as_deref(),
169
+ platform.as_deref(),
170
+ surface.as_deref(),
171
+ )
172
+ .map(|path| {
173
+ println!("{path}");
174
+ }),
144
175
  Some(Commands::DumpAst {
145
176
  file,
146
177
  ignore_indent,
@@ -883,9 +914,9 @@ fn build_file(
883
914
  .map_err(|e| e.to_string());
884
915
  }
885
916
 
886
- if target != "native" {
917
+ if target != "native" && target != "gba" {
887
918
  return Err(format!(
888
- "Unknown target: {}. Use 'native', 'js', 'wasm', 'wasm-gpu', 'wasi', or 'bytecode'.",
919
+ "Unknown target: {}. Use 'native', 'gba', 'js', 'wasm', 'wasm-gpu', 'wasi', or 'bytecode'.",
889
920
  target
890
921
  ));
891
922
  }
@@ -899,7 +930,12 @@ fn build_file(
899
930
  });
900
931
  let features: Vec<String> = native_build_features_from_cli(cli_features);
901
932
 
902
- let build_config = if let Some(triple) = ios_triple {
933
+ let build_config = if target == "gba" {
934
+ if is_js {
935
+ return Err("tish build --target gba requires a .tish entry file.".to_string());
936
+ }
937
+ tishlang_native::NativeBuildConfig::gba()
938
+ } else if let Some(triple) = ios_triple {
903
939
  tishlang_native::NativeBuildConfig::ios_staticlib(triple)
904
940
  } else if crate_type == "staticlib" {
905
941
  tishlang_native::NativeBuildConfig {
@@ -0,0 +1,79 @@
1
+ //! Regression for #38: a merged `@scope/pkg` package (declared via `tish.module`, `exports.tish`,
2
+ //! etc.) must resolve as merged **Tish source**, not be routed to the native Rust-crate path (which
3
+ //! would reject it with "not a Tish native module" or mis-treat it as a crate). Native `@scope`
4
+ //! crates (marked with `tish.crate` / `tish.rustDependencies`) remain native and are exercised by
5
+ //! the downstream regression (tish-apple/tish-macos).
6
+
7
+ use std::fs;
8
+
9
+ use tishlang_ast::{Expr, Statement};
10
+ use tishlang_compile::{merge_modules, resolve_project};
11
+
12
+ /// Build a tempdir project whose `main.tish` imports a merged `@test/greet` package. `tish_field`
13
+ /// is the extra package.json snippet declaring how the package presents itself (`,"tish":{…}` etc.).
14
+ fn setup(tish_field: &str) -> tempfile::TempDir {
15
+ let dir = tempfile::tempdir().unwrap();
16
+ let root = dir.path();
17
+ let pkg = root.join("node_modules/@test/greet");
18
+ fs::create_dir_all(&pkg).unwrap();
19
+ fs::write(
20
+ pkg.join("package.json"),
21
+ format!(r#"{{"name":"@test/greet","version":"1.0.0"{tish_field}}}"#),
22
+ )
23
+ .unwrap();
24
+ fs::write(pkg.join("index.tish"), "export fn greet() { return \"hi\" }\n").unwrap();
25
+ fs::write(
26
+ root.join("main.tish"),
27
+ "import { greet } from \"@test/greet\"\nconsole.log(greet())\n",
28
+ )
29
+ .unwrap();
30
+ dir
31
+ }
32
+
33
+ fn assert_merged_as_source(dir: &tempfile::TempDir) {
34
+ let root = dir.path();
35
+ let modules = resolve_project(&root.join("main.tish"), Some(root)).expect("resolve");
36
+ let merged = merge_modules(modules).expect("merge");
37
+ // Merged as source ⇒ the package's `greet` fn is folded into the top-level program.
38
+ let has_greet = merged
39
+ .program
40
+ .statements
41
+ .iter()
42
+ .any(|s| matches!(s, Statement::FunDecl { name, .. } if name.as_ref() == "greet"));
43
+ assert!(
44
+ has_greet,
45
+ "merged @scope package's fn must be folded into the program (merged as Tish source)"
46
+ );
47
+ // ...and NOT lowered to a native module load (the #38 mis-routing).
48
+ let native_scope = merged.program.statements.iter().any(|s| {
49
+ matches!(
50
+ s,
51
+ Statement::VarDecl { init: Some(Expr::NativeModuleLoad { spec, .. }), .. }
52
+ if spec.contains("@test")
53
+ )
54
+ });
55
+ assert!(
56
+ !native_scope,
57
+ "a merged @scope package must NOT be routed to the native Rust-crate path (#38)"
58
+ );
59
+ }
60
+
61
+ #[test]
62
+ fn scope_tish_module_true_merges_as_source() {
63
+ assert_merged_as_source(&setup(r#","tish":{"module":true}"#));
64
+ }
65
+
66
+ #[test]
67
+ fn scope_tish_module_string_path_merges_as_source() {
68
+ assert_merged_as_source(&setup(r#","tish":{"module":"index.tish"}"#));
69
+ }
70
+
71
+ #[test]
72
+ fn scope_exports_tish_merges_as_source() {
73
+ assert_merged_as_source(&setup(r#","exports":{"tish":"./index.tish"}"#));
74
+ }
75
+
76
+ #[test]
77
+ fn scope_plain_main_merges_as_source() {
78
+ assert_merged_as_source(&setup(r#","main":"index.tish""#));
79
+ }
@@ -340,7 +340,22 @@ pub fn find_crate_path(crate_name: &str) -> Result<PathBuf, String> {
340
340
 
341
341
  /// Sanitize a user-chosen output stem for Cargo `[lib]` / `[[bin]]` target names.
342
342
  pub fn cargo_target_name(stem: &str) -> String {
343
- stem.replace('-', "_")
343
+ // The result is emitted verbatim into a generated `Cargo.toml` (`name = "<stem>"`) and used as
344
+ // the crate/target name, so it must be a valid Rust identifier. Map every char that isn't
345
+ // ASCII-alphanumeric or `_` to `_` — a `-` (cargo normalizes it to `_` anyway), but also a `.`,
346
+ // space, or `"` from an odd `-o` path, which would otherwise break or inject the manifest.
347
+ // A leading digit is prefixed with `_`; an empty result falls back to a fixed default.
348
+ let mut name: String = stem
349
+ .chars()
350
+ .map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' })
351
+ .collect();
352
+ if name.is_empty() {
353
+ return "tish_output".to_string();
354
+ }
355
+ if name.starts_with(|c: char| c.is_ascii_digit()) {
356
+ name.insert(0, '_');
357
+ }
358
+ name
344
359
  }
345
360
 
346
361
  /// Create a temp build directory with src subdir.
@@ -428,6 +443,17 @@ mod protoc_tests {
428
443
  assert_eq!(cargo_target_name("hello-ios"), "hello_ios");
429
444
  assert_eq!(cargo_target_name("tish_out"), "tish_out");
430
445
  }
446
+
447
+ #[test]
448
+ fn cargo_target_name_sanitizes_invalid_chars() {
449
+ // Quotes / dots / spaces from an odd `-o` stem can't break or inject the manifest.
450
+ assert_eq!(cargo_target_name("a\"b"), "a_b");
451
+ assert_eq!(cargo_target_name("my.app"), "my_app");
452
+ assert_eq!(cargo_target_name("my game"), "my_game");
453
+ // A leading digit is made identifier-safe; empty falls back.
454
+ assert_eq!(cargo_target_name("3d"), "_3d");
455
+ assert_eq!(cargo_target_name(""), "tish_output");
456
+ }
431
457
  }
432
458
 
433
459
  /// Find the built static library in target/release (or target/$TRIPLE/release).
@@ -8,15 +8,23 @@ description = "Shared builtin implementations for Tish (array, string, object, m
8
8
  license-file = { workspace = true }
9
9
  repository = { workspace = true }
10
10
  [dependencies]
11
- ahash = "0.8.11"
12
- indexmap = "2"
13
- rand = "0.10.1"
14
- unicode-normalization = "0.1.25"
15
- tishlang_core = { path = "../tish_core", version = ">=0.1" }
11
+ # std-only deps, optional so the `portable` build drops them (rand needs OS
12
+ # entropy; ahash needs atomics — both replaced by tishlang_core hooks/FxHasher).
13
+ ahash = { version = "0.8.11", optional = true }
14
+ indexmap = { version = "2", default-features = false }
15
+ rand = { version = "0.10.1", optional = true }
16
+ # default-features=false keeps it no_std (it works on core+alloc via tinyvec);
17
+ # tish only uses NFC/NFD/NFKC/NFKD, none of which need std.
18
+ unicode-normalization = { version = "0.1.25", default-features = false }
19
+ tishlang_core = { path = "../tish_core", version = ">=0.1", default-features = false }
16
20
 
17
21
  [features]
18
- default = []
22
+ default = ["std"]
23
+ std = ["tishlang_core/std", "dep:ahash", "dep:rand"]
19
24
  regex = ["tishlang_core/regex"]
25
+ # no_std + alloc build for GBA. Inherits tishlang_core's portable vocabulary and
26
+ # its clock/RNG hooks; float math routes through tishlang_core::FloatExt (libm).
27
+ portable = ["tishlang_core/portable"]
20
28
  # Propagate the `send-values` feature from `tishlang_core` so that closures
21
29
  # handed to us via `NativeFn` pick up the `Send + Sync` bound automatically
22
30
  # when multi-threaded `Value`s are enabled upstream.
@@ -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
  }