@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
@@ -344,6 +344,8 @@ impl LanguageServer for Backend {
344
344
  }
345
345
 
346
346
  let mut src_root: Option<PathBuf> = None;
347
+ let mut init_platform: Option<String> = None;
348
+ let mut init_surface: Option<String> = None;
347
349
  if let Some(opts) = &params.initialization_options {
348
350
  if let Some(s) = opts
349
351
  .get("tishlangSourceRoot")
@@ -354,7 +356,23 @@ impl LanguageServer for Backend {
354
356
  src_root = Some(PathBuf::from(s));
355
357
  }
356
358
  }
359
+ // Platform file cascade for imports (same as `tish resolve-id` / Vite).
360
+ // settings.json: "tish.platform" / "tish.surface", or env TISH_PLATFORM / TISH_SURFACE.
361
+ if let Some(p) = opts.get("platform").and_then(|v| v.as_str()) {
362
+ init_platform = Some(p.to_string());
363
+ } else if let Some(p) = opts.get("tishPlatform").and_then(|v| v.as_str()) {
364
+ init_platform = Some(p.to_string());
365
+ }
366
+ if let Some(s) = opts.get("surface").and_then(|v| v.as_str()) {
367
+ init_surface = Some(s.to_string());
368
+ } else if let Some(s) = opts.get("tishSurface").and_then(|v| v.as_str()) {
369
+ init_surface = Some(s.to_string());
370
+ }
357
371
  }
372
+ let _ = tishlang_compile::apply_resolve_env(
373
+ init_platform.as_deref(),
374
+ init_surface.as_deref(),
375
+ );
358
376
  if src_root.is_none() {
359
377
  if let Ok(s) = std::env::var("TISHLANG_SOURCE_ROOT") {
360
378
  let t = s.trim();
@@ -135,6 +135,17 @@ pub fn build_via_cargo_with_config(
135
135
  project_root: Option<&Path>,
136
136
  build_config: &NativeBuildConfig,
137
137
  ) -> Result<(), String> {
138
+ if build_config.artifact == NativeArtifact::GbaRom {
139
+ return build_gba_rom(
140
+ rust_code,
141
+ native_modules,
142
+ output_path,
143
+ extra_dependencies_toml,
144
+ generated_native_rs,
145
+ project_root,
146
+ );
147
+ }
148
+
138
149
  let out_stem = output_path
139
150
  .file_stem()
140
151
  .and_then(|s| s.to_str())
@@ -299,6 +310,241 @@ tishlang_runtime = {{ path = {:?}{} }}
299
310
  Ok(())
300
311
  }
301
312
 
313
+ /// Read the agb version the facade pins, from `<facade>/Cargo.toml`. This keeps the generated ROM
314
+ /// crate's agb in lockstep with the facade's (the single source of truth) — see [`build_gba_rom`].
315
+ /// Matches an `agb = "X"` or `agb = { version = "X", … }` line; `None` if absent (caller falls back).
316
+ fn read_facade_agb_version(facade_path: &Path) -> Option<String> {
317
+ let toml = fs::read_to_string(facade_path.join("Cargo.toml")).ok()?;
318
+ for line in toml.lines() {
319
+ // Require the key to be exactly `agb` (not `agb_foo`): after `agb`, the next non-space
320
+ // token must be `=`.
321
+ let Some(after_key) = line.trim_start().strip_prefix("agb") else {
322
+ continue;
323
+ };
324
+ let Some(after_eq) = after_key.trim_start().strip_prefix('=') else {
325
+ continue;
326
+ };
327
+ // Take the first double-quoted string (the version, in either the bare or table form).
328
+ let rest = after_eq.trim_start();
329
+ if let Some(start) = rest.find('"') {
330
+ if let Some(len) = rest[start + 1..].find('"') {
331
+ return Some(rest[start + 1..start + 1 + len].to_string());
332
+ }
333
+ }
334
+ }
335
+ None
336
+ }
337
+
338
+ /// Build a Game Boy Advance ROM from generated `#![no_std]` Rust.
339
+ ///
340
+ /// Emits an agb-style cargo project (nightly + build-std + `thumbv4t-none-eabi` +
341
+ /// `gba.ld`) that links the `tishlang_runtime_gba` facade under the `tishlang_runtime`
342
+ /// name (the `package =` rename), builds it, then runs `agb-gbafix` on the ELF.
343
+ ///
344
+ /// Runs its own `cargo build` (NOT `run_cargo_build`) so the `.cargo/config.toml`
345
+ /// rustflags (`-Tgba.ld`, `-Ctarget-cpu=arm7tdmi`) apply — `run_cargo_build` sets a
346
+ /// `RUSTFLAGS` env that would shadow them.
347
+ fn build_gba_rom(
348
+ rust_code: &str,
349
+ native_modules: Vec<ResolvedNativeModule>,
350
+ output_path: &Path,
351
+ extra_dependencies_toml: &str,
352
+ generated_native_rs: Option<&str>,
353
+ project_root: Option<&Path>,
354
+ ) -> Result<(), String> {
355
+ use std::process::Command;
356
+
357
+ // Locate the facade crate (sibling of tishlang_runtime in the tish workspace).
358
+ let runtime_path = tishlang_build_utils::find_runtime_path_for_project(project_root)?;
359
+ let facade_path = Path::new(&runtime_path)
360
+ .parent()
361
+ .ok_or_else(|| "invalid tishlang_runtime path (no parent)".to_string())?
362
+ .join("tish_runtime_gba");
363
+ if !facade_path.exists() {
364
+ return Err(format!(
365
+ "GBA runtime facade not found at {} (expected the tishlang_runtime_gba crate)",
366
+ facade_path.display()
367
+ ));
368
+ }
369
+
370
+ // Single-source the agb version from the facade's own Cargo.toml — the facade is the one crate
371
+ // that actually links agb (its `Fixed`, entry-point, and asset arenas are agb types), so it is
372
+ // the source of truth. The generated ROM crate must depend on the SAME agb: a skew would pull in
373
+ // two agb crates and mismatch types at the sprite-registration boundary. Falls back to a known
374
+ // pin only if the line can't be read (never blocks the build on a parse miss).
375
+ let agb_version =
376
+ read_facade_agb_version(&facade_path).unwrap_or_else(|| "0.25.0".to_string());
377
+
378
+ let out_stem = output_path
379
+ .file_stem()
380
+ .and_then(|s| s.to_str())
381
+ .unwrap_or("tish_out");
382
+ let cargo_name = tishlang_build_utils::cargo_target_name(out_stem);
383
+
384
+ // Persistent build dir under the project so the (expensive) build-std recompile
385
+ // is cached across runs; the generated Rust stays inspectable.
386
+ let base = project_root
387
+ .map(|p| p.to_path_buf())
388
+ .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
389
+ let build_dir = base.join(".tish").join("gba").join(out_stem);
390
+ fs::create_dir_all(build_dir.join("src"))
391
+ .map_err(|e| format!("Cannot create GBA build dir: {}", e))?;
392
+ fs::create_dir_all(build_dir.join(".cargo"))
393
+ .map_err(|e| format!("Cannot create .cargo dir: {}", e))?;
394
+
395
+ // Path dependencies for any `cargo:` native modules the program imported.
396
+ let native_deps: String = native_modules
397
+ .iter()
398
+ .filter(|m| m.use_path_dependency)
399
+ .map(|m| {
400
+ let path = m.crate_path.display().to_string().replace('\\', "/");
401
+ format!("{} = {{ path = {:?} }}\n", m.package_name, path)
402
+ })
403
+ .collect();
404
+
405
+ // `cargo:` module path deps arrive via `extra_dependencies_toml`
406
+ // (`tish.rustDependencies`); `native_modules` covers any that use a path dep
407
+ // resolved another way. Both are appended to `[dependencies]`.
408
+ let mut more_deps = native_deps;
409
+ if !extra_dependencies_toml.trim().is_empty() {
410
+ more_deps.push('\n');
411
+ more_deps.push_str(extra_dependencies_toml);
412
+ }
413
+
414
+ let facade = facade_path.display().to_string().replace('\\', "/");
415
+ let cargo_toml = format!(
416
+ r#"[package]
417
+ name = "tish_output"
418
+ version = "0.1.0"
419
+ edition = "2021"
420
+
421
+ # Standalone workspace so a parent Cargo workspace (e.g. the tish-gba repo, when
422
+ # the build dir lives under it) doesn't try to absorb this generated crate.
423
+ [workspace]
424
+
425
+ [[bin]]
426
+ name = "{cargo_name}"
427
+ path = "src/main.rs"
428
+
429
+ [dependencies]
430
+ tishlang_runtime = {{ package = "tishlang_runtime_gba", path = {facade:?} }}
431
+ agb = "{agb_version}"
432
+ {more_deps}
433
+ [profile.dev]
434
+ opt-level = 3
435
+ debug = true
436
+
437
+ [profile.dev.build-override]
438
+ opt-level = 3
439
+
440
+ [profile.release]
441
+ opt-level = 3
442
+ lto = "fat"
443
+ debug = true
444
+
445
+ [profile.release.build-override]
446
+ opt-level = 3
447
+ "#,
448
+ );
449
+ fs::write(build_dir.join("Cargo.toml"), cargo_toml)
450
+ .map_err(|e| format!("Cannot write Cargo.toml: {}", e))?;
451
+
452
+ fs::write(
453
+ build_dir.join("rust-toolchain.toml"),
454
+ "[toolchain]\nchannel = \"nightly\"\ncomponents = [\"rust-src\"]\n",
455
+ )
456
+ .map_err(|e| format!("Cannot write rust-toolchain.toml: {}", e))?;
457
+
458
+ // build-std + target + gba.ld rustflags (agb ships gba.ld via its build.rs).
459
+ fs::write(
460
+ build_dir.join(".cargo").join("config.toml"),
461
+ r#"[unstable]
462
+ build-std = ["core", "alloc"]
463
+ build-std-features = ["compiler-builtins-mem"]
464
+
465
+ [build]
466
+ target = "thumbv4t-none-eabi"
467
+
468
+ [target.thumbv4t-none-eabi]
469
+ runner = ["mgba-qt", "-C", "logToStdout=1", "-C", "logLevel.gba.debug=127"]
470
+ "#,
471
+ )
472
+ .map_err(|e| format!("Cannot write .cargo/config.toml: {}", e))?;
473
+
474
+ let rust_main = if generated_native_rs.is_some() {
475
+ inject_generated_native_mod(rust_code)
476
+ } else {
477
+ rust_code.to_string()
478
+ };
479
+ if let Some(gen) = generated_native_rs {
480
+ // The `cargo:` wrapper emits the std header (`std::cell/rc/sync`); map it
481
+ // to the no_std facade equivalents (`alloc` is crate-wide via main.rs's
482
+ // `extern crate alloc`). The module body is otherwise Value-only.
483
+ let gen = gen
484
+ .replace("use std::cell::RefCell;", "use core::cell::RefCell;")
485
+ .replace("use std::rc::Rc;", "use alloc::rc::Rc;")
486
+ .replace("use std::sync::Arc;", "use tishlang_runtime::Arc;");
487
+ fs::write(build_dir.join("src").join("generated_native.rs"), gen)
488
+ .map_err(|e| format!("Cannot write generated_native.rs: {}", e))?;
489
+ }
490
+ fs::write(build_dir.join("src").join("main.rs"), rust_main)
491
+ .map_err(|e| format!("Cannot write main.rs: {}", e))?;
492
+
493
+ // Pass the GBA linker/target rustflags via the RUSTFLAGS env (which REPLACES,
494
+ // not joins, any `[target.*].rustflags` from config files). This matters when
495
+ // the build dir sits inside a project that already has a `.cargo/config.toml`
496
+ // with `-Tgba.ld` — cargo *joins* rustflags arrays across config files, so a
497
+ // config-file approach would pass `-Tgba.ld` twice ("region already defined").
498
+ // build-std still comes from the generated `[unstable]` config (not rustflags).
499
+ let status = Command::new("cargo")
500
+ .arg("build")
501
+ .arg("--release")
502
+ .current_dir(&build_dir)
503
+ .env(
504
+ "RUSTFLAGS",
505
+ "-Clink-arg=-Tgba.ld -Ctarget-cpu=arm7tdmi -Cforce-frame-pointers=yes",
506
+ )
507
+ .env("CARGO_TERM_COLOR", "always")
508
+ .status()
509
+ .map_err(|e| format!("Failed to run cargo for GBA build: {}", e))?;
510
+ if !status.success() {
511
+ return Err(format!(
512
+ "GBA cargo build failed (see output above). Build dir: {}",
513
+ build_dir.display()
514
+ ));
515
+ }
516
+
517
+ // ELF → .gba via agb-gbafix.
518
+ let elf = build_dir
519
+ .join("target")
520
+ .join("thumbv4t-none-eabi")
521
+ .join("release")
522
+ .join(&cargo_name);
523
+ if !elf.exists() {
524
+ return Err(format!("GBA ELF not found at {}", elf.display()));
525
+ }
526
+ let rom = if output_path.extension().is_some_and(|e| e == "gba") {
527
+ output_path.to_path_buf()
528
+ } else {
529
+ output_path.with_extension("gba")
530
+ };
531
+ let gbafix = Command::new("agb-gbafix")
532
+ .arg(&elf)
533
+ .arg("-o")
534
+ .arg(&rom)
535
+ .status()
536
+ .map_err(|e| {
537
+ format!(
538
+ "Failed to run agb-gbafix ({}). Install it with: cargo install agb-gbafix",
539
+ e
540
+ )
541
+ })?;
542
+ if !gbafix.success() {
543
+ return Err("agb-gbafix failed to produce the ROM".to_string());
544
+ }
545
+ Ok(())
546
+ }
547
+
302
548
  /// Remove a finished build's per-PID source directory (#384). Called only on SUCCESS, after the
303
549
  /// binary is copied out — the dir is then disposable (it holds only the generated crate source;
304
550
  /// compiled artifacts live in the shared workspace `target/`). Left in place on any earlier error so a
@@ -8,6 +8,9 @@ pub enum NativeArtifact {
8
8
  #[default]
9
9
  Bin,
10
10
  StaticLib,
11
+ /// A GBA ROM: cargo builds an ELF for `thumbv4t-none-eabi`, then `agb-gbafix`
12
+ /// converts it to a `.gba`.
13
+ GbaRom,
11
14
  }
12
15
 
13
16
  /// Options passed from the CLI into nested `cargo build`.
@@ -32,11 +35,28 @@ impl NativeBuildConfig {
32
35
  }
33
36
  }
34
37
 
38
+ /// Game Boy Advance ROM build (`tish build --target gba`): cross-compile to
39
+ /// `thumbv4t-none-eabi` with the `Gba` emit mode.
40
+ pub fn gba() -> Self {
41
+ Self {
42
+ artifact: NativeArtifact::GbaRom,
43
+ cargo_target: Some("thumbv4t-none-eabi".to_string()),
44
+ emit_mode: NativeEmitMode::Gba,
45
+ }
46
+ }
47
+
35
48
  pub fn is_cross_compile(&self) -> bool {
36
49
  self.cargo_target.is_some()
37
50
  }
38
51
  }
39
52
 
53
+ /// Feature cap for GBA builds: no host-only runtime capabilities (http/fs/process/
54
+ /// timers/ws/tty/pty). The facade `compile_error!`-stubs them, but capping keeps
55
+ /// the emitted prelude from importing names that don't exist on GBA.
56
+ pub fn gba_runtime_features(_features: &[String]) -> Vec<String> {
57
+ Vec::new()
58
+ }
59
+
40
60
  /// Filter runtime features for iOS sandbox builds.
41
61
  pub fn ios_runtime_features(features: &[String]) -> Vec<String> {
42
62
  const ALLOW: &[&str] = &["http", "http-hyper", "regex", "timers"];
@@ -97,13 +97,23 @@ pub fn compile_to_native_with_config(
97
97
 
98
98
  match backend {
99
99
  Backend::Rust => {
100
- let ios_cap = build_config.cargo_target.as_ref().map(|_| {
101
- ios_runtime_features(features)
100
+ // Cross-target builds cap runtime features to what the target supports:
101
+ // GBA gets none (no OS), iOS gets its sandbox allow-list.
102
+ let is_gba = build_config.emit_mode == tishlang_compile::NativeEmitMode::Gba;
103
+ let cap = |f: &[String]| -> Vec<String> {
104
+ if is_gba {
105
+ crate::config::gba_runtime_features(f)
106
+ } else {
107
+ ios_runtime_features(f)
108
+ }
109
+ };
110
+ let cross_cap = build_config.cargo_target.as_ref().map(|_| {
111
+ cap(features)
102
112
  .into_iter()
103
113
  .collect::<std::collections::HashSet<_>>()
104
114
  });
105
- let compile_features = if ios_cap.is_some() {
106
- ios_runtime_features(features)
115
+ let compile_features = if cross_cap.is_some() {
116
+ cap(features)
107
117
  } else {
108
118
  features.to_vec()
109
119
  };
@@ -114,14 +124,14 @@ pub fn compile_to_native_with_config(
114
124
  &compile_features,
115
125
  optimize,
116
126
  build_config.emit_mode,
117
- ios_cap.as_ref(),
127
+ cross_cap.as_ref(),
118
128
  )
119
129
  .map_err(|e| NativeError {
120
130
  message: e.to_string(),
121
131
  })?;
122
132
 
123
133
  let features_for_cargo = if build_config.cargo_target.is_some() {
124
- ios_runtime_features(&effective_features)
134
+ cap(&effective_features)
125
135
  } else {
126
136
  effective_features
127
137
  };
@@ -101,6 +101,34 @@ mod tests {
101
101
  assert!(parse("let x = async 5").is_err());
102
102
  }
103
103
 
104
+ /// #428 (function-expression half): `async function (…) {…}` / named `async function f(…) {…}`
105
+ /// in expression position parse as async ArrowFunctions — anonymous, named, object-property,
106
+ /// and IIFE-operand forms. Non-async function expressions keep `async_: false` (#464).
107
+ #[test]
108
+ fn async_function_expressions_parse() {
109
+ use tishlang_ast::Expr;
110
+ let fn_expr_async = |src: &str| -> bool {
111
+ let p = parse(src).expect("parse");
112
+ let Some(Statement::VarDecl { init: Some(e), .. }) = p.statements.first() else {
113
+ panic!("expected `let x = <fn expr>`");
114
+ };
115
+ match e {
116
+ Expr::ArrowFunction { async_, .. } => *async_,
117
+ other => panic!("expected ArrowFunction, got {other:?}"),
118
+ }
119
+ };
120
+ assert!(fn_expr_async("let f = async function() { return 1 }"), "anonymous");
121
+ assert!(fn_expr_async("let f = async function named(n) { return n }"), "named");
122
+ assert!(!fn_expr_async("let f = function() { return 1 }"), "plain fn expr is not async");
123
+ // Object-property and IIFE-operand positions.
124
+ assert!(parse("let o = { m: async function() { return 1 } }").is_ok());
125
+ assert!(parse("let p = (async function() { return 1 })()").is_ok());
126
+ assert!(parse("let q = (async () => { return 1 })()").is_ok());
127
+ // `async fn => …` stays a single-param arrow with param `fn` (#55 ident-compat) —
128
+ // the function-expression branch requires `(` or an identifier after `function`.
129
+ assert!(parse("let r = async fn => fn").is_ok());
130
+ }
131
+
104
132
  #[test]
105
133
  fn test_async_fn_parse() {
106
134
  let program = parse("async fn foo() { }").expect("parse async fn");
@@ -197,6 +197,11 @@ impl<'a> Parser<'a> {
197
197
  self.peek().map(|t| t.kind)
198
198
  }
199
199
 
200
+ /// Kind of the token `n` positions past the current one (`peek_ahead_kind(0)` == `peek_kind`).
201
+ fn peek_ahead_kind(&self, n: usize) -> Option<TokenKind> {
202
+ self.tokens.get(self.pos + n).map(|t| t.kind)
203
+ }
204
+
200
205
  fn advance(&mut self) -> Option<&Token> {
201
206
  let t = self.tokens.get(self.pos);
202
207
  if t.is_some() {
@@ -1060,7 +1065,8 @@ impl<'a> Parser<'a> {
1060
1065
 
1061
1066
  /// `interface Name { k: T, ... }` — desugared to `type Name = { ... }` so the checker
1062
1067
  /// (structural matching) and codegen (native `TishStruct_*`) treat it exactly like an
1063
- /// object-type alias. (`extends` is not yet supported.)
1068
+ /// object-type alias. `interface Name extends P1, P2 { ... }` desugars to an intersection
1069
+ /// of the parents with the body (see below), so inherited fields participate too.
1064
1070
  fn parse_interface(&mut self) -> Result<Statement, String> {
1065
1071
  let span_start = self.expect(TokenKind::Interface)?.span.start;
1066
1072
  let name_tok = self.expect(TokenKind::Ident)?;
@@ -2464,9 +2470,11 @@ impl<'a> Parser<'a> {
2464
2470
  value: Literal::Null,
2465
2471
  span,
2466
2472
  }),
2467
- // `async` in expression position (#428): an async arrow — `async () => …` or
2468
- // `async x => …`. The body runs in async context (`await` allowed). Async function
2469
- // expressions / method shorthand need function-expression support, which tish lacks.
2473
+ // `async` in expression position (#428): an async arrow — `async () => …` /
2474
+ // `async x => …` — or an async function EXPRESSION — `async function (…) {…}` /
2475
+ // named `async function f(…) {…}` (desugars to an async ArrowFunction exactly like
2476
+ // the non-async `function` expression form, #464). The body runs in async context
2477
+ // (`await` allowed).
2470
2478
  TokenKind::Async => {
2471
2479
  if matches!(self.peek_kind(), Some(TokenKind::LParen)) {
2472
2480
  self.advance(); // consume `(`
@@ -2475,6 +2483,19 @@ impl<'a> Parser<'a> {
2475
2483
  }
2476
2484
  return Err("Expected an arrow function after `async (`".to_string());
2477
2485
  }
2486
+ // `async function (…) {…}` / `async function f(…) {…}`: `function` (`Fn`)
2487
+ // followed by `(` or an identifier is a function expression, mirroring the
2488
+ // non-async arm below. Checked BEFORE the single-param-arrow branch, which
2489
+ // would otherwise consume `function` as a parameter name and demand `=>`.
2490
+ if matches!(self.peek_kind(), Some(TokenKind::Fn))
2491
+ && matches!(
2492
+ self.peek_ahead_kind(1),
2493
+ Some(TokenKind::LParen | TokenKind::Ident)
2494
+ )
2495
+ {
2496
+ self.advance(); // consume `function`
2497
+ return self.parse_function_expr_tail(&span, true);
2498
+ }
2478
2499
  if matches!(
2479
2500
  self.peek_kind(),
2480
2501
  Some(TokenKind::Ident | TokenKind::Fn | TokenKind::Type)
@@ -16,6 +16,7 @@ pub use tishlang_core::ObjectMap;
16
16
  // pass) instead of materializing an intermediate `AHashMap` and rebuilding from it.
17
17
  pub use tishlang_core::{ObjectData, PropMap};
18
18
  pub use tishlang_core::Value;
19
+ pub use tishlang_core::NumArrayBacking;
19
20
  pub use tishlang_core::ArcStr;
20
21
  /// Used by native codegen for `f()` / `obj()` dispatch (`Value::Function` or `__call` on objects).
21
22
  /// Wraps `tishlang_core::value_call` with a pending-throw suppression check (#381): while a thrown
@@ -79,7 +80,7 @@ pub fn normalize_for_of(v: Value) -> Value {
79
80
  // single-variant `if let Value::Array` in the spread / for-of codegen binds — otherwise it spreads
80
81
  // to zero elements.
81
82
  if let Value::NumberArray(arr) = &v {
82
- let items: Vec<Value> = arr.borrow().iter().map(|n| Value::Number(*n)).collect();
83
+ let items: Vec<Value> = arr.borrow().to_values();
83
84
  return Value::Array(VmRef::new(items));
84
85
  }
85
86
  match tishlang_core::drain_iterator(&v) {
@@ -547,7 +548,8 @@ use tishlang_builtins::globals::{
547
548
  decode_uri as builtins_decode_uri, encode_uri as builtins_encode_uri,
548
549
  is_finite as builtins_is_finite, is_nan as builtins_is_nan,
549
550
  object_assign as builtins_object_assign, object_entries as builtins_object_entries,
550
- object_from_entries as builtins_object_from_entries, object_keys as builtins_object_keys,
551
+ object_freeze as builtins_object_freeze, object_from_entries as builtins_object_from_entries,
552
+ object_is_frozen as builtins_object_is_frozen, object_keys as builtins_object_keys,
551
553
  object_values as builtins_object_values,
552
554
  number_convert as builtins_number_convert,
553
555
  string_convert as builtins_string_convert,
@@ -1389,7 +1391,10 @@ pub fn get_prop(obj: &Value, key: impl AsRef<str>) -> Value {
1389
1391
  if key == "length" {
1390
1392
  Value::Number(arr.borrow().len() as f64)
1391
1393
  } else if let Ok(idx) = key.parse::<usize>() {
1392
- arr.borrow().get(idx).copied().map(Value::Number).unwrap_or(Value::Null)
1394
+ arr.borrow().get(idx).map(|v| match v {
1395
+ Value::Number(n) if n.is_nan() => Value::Null,
1396
+ other => other,
1397
+ }).unwrap_or(Value::Null)
1393
1398
  } else {
1394
1399
  Value::Null
1395
1400
  }
@@ -1487,7 +1492,10 @@ pub fn get_index(obj: &Value, index: &Value) -> Value {
1487
1492
  },
1488
1493
  _ => return Value::Null,
1489
1494
  };
1490
- arr.borrow().get(idx).copied().map(Value::Number).unwrap_or(Value::Null)
1495
+ arr.borrow().get(idx).map(|v| match v {
1496
+ Value::Number(n) if n.is_nan() => Value::Null,
1497
+ other => other,
1498
+ }).unwrap_or(Value::Null)
1491
1499
  }
1492
1500
  // `str[i]` returns the character at index `i` (issue #17) — matches the VM /
1493
1501
  // interpreter; out-of-bounds / negative / non-integer indices yield null.
@@ -1558,6 +1566,14 @@ pub fn set_prop(obj: &Value, key: &str, val: Value) -> Value {
1558
1566
  // exists we re-use the existing `Arc<str>` and skip the
1559
1567
  // alloc. Only newly-inserted keys pay for `Arc::from(key)`.
1560
1568
  let mut m = map.borrow_mut();
1569
+ if m.frozen {
1570
+ // #437: a write to a frozen object throws a catchable TypeError (parked in the
1571
+ // pending-throw slot; the generated code surfaces it at the next checkpoint).
1572
+ tishlang_core::set_pending_throw(tishlang_core::type_error(format!(
1573
+ "Cannot assign to read only property '{key}' of a frozen object"
1574
+ )));
1575
+ return val;
1576
+ }
1561
1577
  if let Some(slot) = m.strings.get_mut(key) {
1562
1578
  *slot = val.clone();
1563
1579
  } else {
@@ -1613,14 +1629,35 @@ pub fn set_index(obj: &Value, idx: &Value, val: Value) -> Value {
1613
1629
  },
1614
1630
  _ => panic!("Array index must be number"),
1615
1631
  };
1616
- let mut arr_mut = arr.borrow_mut();
1617
- while arr_mut.len() <= index {
1618
- arr_mut.push(0.0);
1632
+ let mut b = arr.borrow_mut();
1633
+ match (b.as_packed_mut(), val.as_number()) {
1634
+ (Some(nums), Some(n)) => {
1635
+ while nums.len() <= index {
1636
+ nums.push(f64::NAN);
1637
+ }
1638
+ nums[index] = n;
1639
+ }
1640
+ // Non-numeric (or already deopted): upgrade in place, store the real Value.
1641
+ _ => {
1642
+ let boxed = b.deopt();
1643
+ while boxed.len() <= index {
1644
+ boxed.push(Value::Null);
1645
+ }
1646
+ boxed[index] = val.clone();
1647
+ }
1619
1648
  }
1620
- arr_mut[index] = val.as_number().unwrap_or(f64::NAN);
1621
1649
  val
1622
1650
  }
1623
- Value::Object(_) => {
1651
+ Value::Object(map) => {
1652
+ if map.borrow().frozen {
1653
+ // #437: a write to a frozen object throws a catchable TypeError (parked; surfaced at
1654
+ // the next assignment checkpoint, same as `set_prop`).
1655
+ tishlang_core::set_pending_throw(tishlang_core::type_error(format!(
1656
+ "Cannot assign to read only property '{}' of a frozen object",
1657
+ idx.to_display_string()
1658
+ )));
1659
+ return val;
1660
+ }
1624
1661
  tishlang_core::object_set(obj, idx, val.clone()).expect("object set");
1625
1662
  val
1626
1663
  }
@@ -1673,6 +1710,14 @@ pub fn object_keys(args: &[Value]) -> Value {
1673
1710
  builtins_object_keys(args)
1674
1711
  }
1675
1712
 
1713
+ pub fn object_freeze(args: &[Value]) -> Value {
1714
+ builtins_object_freeze(args)
1715
+ }
1716
+
1717
+ pub fn object_is_frozen(args: &[Value]) -> Value {
1718
+ builtins_object_is_frozen(args)
1719
+ }
1720
+
1676
1721
  pub fn object_values(args: &[Value]) -> Value {
1677
1722
  builtins_object_values(args)
1678
1723
  }