@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.
- package/Cargo.toml +4 -1
- package/bin/tish-lsp +0 -0
- package/crates/tish/src/cli_help.rs +28 -0
- package/crates/tish/src/main.rs +41 -5
- package/crates/tish/tests/scope_merge.rs +79 -0
- package/crates/tish_build_utils/src/lib.rs +27 -1
- package/crates/tish_builtins/Cargo.toml +14 -6
- package/crates/tish_builtins/src/array.rs +130 -54
- package/crates/tish_builtins/src/collections.rs +9 -5
- package/crates/tish_builtins/src/construct.rs +7 -1
- package/crates/tish_builtins/src/date.rs +15 -1
- package/crates/tish_builtins/src/globals.rs +38 -3
- package/crates/tish_builtins/src/helpers.rs +7 -3
- package/crates/tish_builtins/src/iterator.rs +5 -1
- package/crates/tish_builtins/src/lib.rs +3 -0
- package/crates/tish_builtins/src/math.rs +14 -0
- package/crates/tish_builtins/src/number.rs +72 -6
- package/crates/tish_builtins/src/object.rs +6 -1
- package/crates/tish_builtins/src/string.rs +13 -2
- package/crates/tish_builtins/src/symbol.rs +17 -17
- package/crates/tish_builtins/src/typedarrays.rs +9 -2
- package/crates/tish_bytecode/src/compiler.rs +76 -2
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +77 -2
- package/crates/tish_compile/src/codegen.rs +812 -47
- package/crates/tish_compile/src/infer.rs +37 -41
- package/crates/tish_compile/src/lib.rs +13 -1
- package/crates/tish_compile/src/platform_resolve.rs +361 -0
- package/crates/tish_compile/src/resolve.rs +148 -10
- package/crates/tish_compile/src/schemes.rs +278 -0
- package/crates/tish_compile/src/types.rs +141 -2
- package/crates/tish_compile/tests/perf_codegen_320.rs +2 -2
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +3 -1
- package/crates/tish_compile/tests/platform_resolve_cli.rs +164 -0
- package/crates/tish_compile/tests/regr_gba_numerics_gated.rs +58 -0
- package/crates/tish_compiler_wasm/Cargo.toml +1 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +56 -6
- package/crates/tish_core/Cargo.toml +39 -4
- package/crates/tish_core/src/compat.rs +506 -0
- package/crates/tish_core/src/console_style.rs +19 -1
- package/crates/tish_core/src/json.rs +34 -10
- package/crates/tish_core/src/lib.rs +121 -25
- package/crates/tish_core/src/macros.rs +1 -1
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/uri.rs +8 -1
- package/crates/tish_core/src/value.rs +182 -54
- package/crates/tish_core/src/vmref.rs +17 -5
- package/crates/tish_eval/Cargo.toml +2 -0
- package/crates/tish_eval/src/eval.rs +90 -3
- package/crates/tish_eval/src/value.rs +8 -0
- package/crates/tish_eval/src/value_convert.rs +316 -21
- package/crates/tish_lsp/Cargo.toml +3 -1
- package/crates/tish_lsp/src/import_goto.rs +41 -14
- package/crates/tish_lsp/src/main.rs +18 -0
- package/crates/tish_native/src/build.rs +246 -0
- package/crates/tish_native/src/config.rs +20 -0
- package/crates/tish_native/src/lib.rs +16 -6
- package/crates/tish_parser/src/lib.rs +28 -0
- package/crates/tish_parser/src/parser.rs +25 -4
- package/crates/tish_runtime/src/http_hyper.rs +70 -0
- package/crates/tish_runtime/src/lib.rs +55 -10
- package/crates/tish_runtime/src/ws.rs +94 -21
- package/crates/tish_runtime_gba/Cargo.lock +837 -0
- package/crates/tish_runtime_gba/Cargo.toml +16 -0
- package/crates/tish_runtime_gba/src/gba.rs +211 -0
- package/crates/tish_runtime_gba/src/lib.rs +620 -0
- package/crates/tish_vm/src/jit.rs +1446 -164
- package/crates/tish_vm/src/vm.rs +535 -173
- package/justfile +9 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lsp +0 -0
- package/platform/darwin-x64/tish-lsp +0 -0
- package/platform/linux-arm64/tish-lsp +0 -0
- package/platform/linux-x64/tish-lsp +0 -0
- package/platform/win32-x64/tish-lsp.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) = ¶ms.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
|
-
|
|
101
|
-
|
|
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
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 () => …`
|
|
2468
|
-
// `async x =>
|
|
2469
|
-
//
|
|
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)
|
|
@@ -289,6 +289,9 @@ fn worker_thread(
|
|
|
289
289
|
// headers slower than this instead of pinning the worker task.
|
|
290
290
|
.header_read_timeout(std::time::Duration::from_secs(30))
|
|
291
291
|
.serve_connection(io, svc)
|
|
292
|
+
// Required for `hyper::upgrade::on` to hand off the socket after a 101 — without
|
|
293
|
+
// this, a WebSocket upgrade sends the 101 but the upgrade future never resolves.
|
|
294
|
+
.with_upgrades()
|
|
292
295
|
.await
|
|
293
296
|
{
|
|
294
297
|
// Chatty connection errors (resets, client timeouts) are
|
|
@@ -314,6 +317,13 @@ async fn handle_request(
|
|
|
314
317
|
return Ok(static_route_response(route));
|
|
315
318
|
}
|
|
316
319
|
|
|
320
|
+
// WebSocket upgrade: complete the handshake here and hand the socket to `ws.rs` — the VM handler
|
|
321
|
+
// never sees it. The program picks the connection up via `wsAccept`. (Single-port WS, issue #495.)
|
|
322
|
+
#[cfg(feature = "ws")]
|
|
323
|
+
if is_ws_upgrade(&req) {
|
|
324
|
+
return Ok(handle_ws_upgrade(req));
|
|
325
|
+
}
|
|
326
|
+
|
|
317
327
|
// Slow path: cross the mpsc to the VM thread.
|
|
318
328
|
let (method, url, path, query, headers, body_bytes) = extract_request(req).await;
|
|
319
329
|
// Lossy UTF-8 decode to match the tiny_http path (`http.rs`): a body with invalid UTF-8 becomes
|
|
@@ -455,6 +465,66 @@ fn simple_error_response(status: StatusCode, msg: &str) -> Response<Full<Bytes>>
|
|
|
455
465
|
.unwrap()
|
|
456
466
|
}
|
|
457
467
|
|
|
468
|
+
/// A well-formed WebSocket upgrade handshake request (RFC 6455): `Upgrade: websocket`,
|
|
469
|
+
/// `Connection: Upgrade`, and a `Sec-WebSocket-Key`.
|
|
470
|
+
#[cfg(feature = "ws")]
|
|
471
|
+
fn is_ws_upgrade(req: &Request<hyper::body::Incoming>) -> bool {
|
|
472
|
+
let h = req.headers();
|
|
473
|
+
let upgrade_ws = h
|
|
474
|
+
.get(hyper::header::UPGRADE)
|
|
475
|
+
.and_then(|v| v.to_str().ok())
|
|
476
|
+
.map(|s| s.eq_ignore_ascii_case("websocket"))
|
|
477
|
+
.unwrap_or(false);
|
|
478
|
+
let conn_upgrade = h
|
|
479
|
+
.get(hyper::header::CONNECTION)
|
|
480
|
+
.and_then(|v| v.to_str().ok())
|
|
481
|
+
.map(|s| s.to_ascii_lowercase().split(',').any(|p| p.trim() == "upgrade"))
|
|
482
|
+
.unwrap_or(false);
|
|
483
|
+
upgrade_ws && conn_upgrade && h.contains_key(hyper::header::SEC_WEBSOCKET_KEY)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/// Complete a WebSocket handshake on the HTTP port: reply `101` with the derived accept key, then
|
|
487
|
+
/// (after the response flushes) take over the connection, wrap it as a server-role WebSocket, and
|
|
488
|
+
/// register it with `ws.rs` so the program can pick it up via `wsAccept`.
|
|
489
|
+
#[cfg(feature = "ws")]
|
|
490
|
+
fn handle_ws_upgrade(mut req: Request<hyper::body::Incoming>) -> Response<Full<Bytes>> {
|
|
491
|
+
use tokio_tungstenite::tungstenite::handshake::derive_accept_key;
|
|
492
|
+
use tokio_tungstenite::tungstenite::protocol::Role;
|
|
493
|
+
|
|
494
|
+
let accept = match req.headers().get(hyper::header::SEC_WEBSOCKET_KEY) {
|
|
495
|
+
Some(k) => derive_accept_key(k.as_bytes()),
|
|
496
|
+
None => return simple_error_response(StatusCode::BAD_REQUEST, "missing Sec-WebSocket-Key"),
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
let on_upgrade = hyper::upgrade::on(&mut req);
|
|
500
|
+
tokio::spawn(async move {
|
|
501
|
+
match on_upgrade.await {
|
|
502
|
+
Ok(upgraded) => {
|
|
503
|
+
let io = TokioIo::new(upgraded);
|
|
504
|
+
let ws = tokio_tungstenite::WebSocketStream::from_raw_socket(io, Role::Server, None)
|
|
505
|
+
.await;
|
|
506
|
+
let id = crate::ws::register_ws_stream(ws);
|
|
507
|
+
crate::ws::enqueue_upgraded_conn(id);
|
|
508
|
+
}
|
|
509
|
+
Err(e) => {
|
|
510
|
+
if std::env::var_os("TISH_HTTP_DEBUG").is_some() {
|
|
511
|
+
eprintln!("[tish http/hyper] ws upgrade failed: {}", e);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
Response::builder()
|
|
518
|
+
.status(StatusCode::SWITCHING_PROTOCOLS)
|
|
519
|
+
.header(hyper::header::UPGRADE, "websocket")
|
|
520
|
+
.header(hyper::header::CONNECTION, "Upgrade")
|
|
521
|
+
.header(hyper::header::SEC_WEBSOCKET_ACCEPT, accept)
|
|
522
|
+
.body(Full::new(Bytes::new()))
|
|
523
|
+
.unwrap_or_else(|_| {
|
|
524
|
+
simple_error_response(StatusCode::INTERNAL_SERVER_ERROR, "ws upgrade build failed")
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
|
|
458
528
|
/// Marker used by external callers (like the bench) to gate the backend
|
|
459
529
|
/// choice. Returns true when `TISH_HTTP_BACKEND=hyper` is in the env.
|
|
460
530
|
pub fn is_enabled_via_env() -> bool {
|