@tishlang/tish-lsp 3.7.1 → 3.8.6
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/bin/tish-lsp +0 -0
- package/crates/tish_compile/src/codegen.rs +555 -23
- package/crates/tish_compile/src/lib.rs +8 -2
- package/crates/tish_compile/tests/regr_655_gba_stack_guard.rs +41 -0
- package/crates/tish_lsp/Cargo.toml +1 -1
- package/crates/tish_native/src/build.rs +287 -22
- package/crates/tish_runtime_gba/src/lib.rs +51 -2
- 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
|
@@ -117,6 +117,7 @@ fn count(): number {
|
|
|
117
117
|
for (let i: number = 0; i < 10; i = i + 1) { total = total + i }
|
|
118
118
|
return total
|
|
119
119
|
}
|
|
120
|
+
console.log(count())
|
|
120
121
|
"#;
|
|
121
122
|
let program = parse(src_native).unwrap();
|
|
122
123
|
let rust = compile(&program).unwrap();
|
|
@@ -124,9 +125,14 @@ fn count(): number {
|
|
|
124
125
|
rust.contains("let mut total: f64"),
|
|
125
126
|
"numeric-only reassignment keeps `total` native f64"
|
|
126
127
|
);
|
|
128
|
+
// The boxing moved, it did not disappear. With the boxed wrapper elided for a fn every
|
|
129
|
+
// caller reaches directly, the f64 is wrapped at the CALL SITE
|
|
130
|
+
// (`Value::Number(count_native())`) instead of inside a wrapper's return. Either spelling
|
|
131
|
+
// satisfies what this is checking — that a native f64 crosses back to Value at the boundary
|
|
132
|
+
// — so accept both rather than pinning the assertion to one codegen shape.
|
|
127
133
|
assert!(
|
|
128
|
-
rust.contains("Value::Number(total)"),
|
|
129
|
-
"f64 total is wrapped back to Value at
|
|
134
|
+
rust.contains("Value::Number(total)") || rust.contains("Value::Number(count_native())"),
|
|
135
|
+
"f64 total is wrapped back to Value at a boundary (wrapper return or call site)"
|
|
130
136
|
);
|
|
131
137
|
}
|
|
132
138
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//! #655 — the stack-overflow guard must actually be emitted on the GBA target.
|
|
2
|
+
//!
|
|
3
|
+
//! The recursion guard was gated OFF for `NativeEmitMode::Gba` (the no_std runtime had no
|
|
4
|
+
//! stack-pressure probe), and the boxed guard's floor fell back to "never trips". GBA is the one
|
|
5
|
+
//! target where that is unrecoverable: 32 KB of IWRAM, no MMU, no guard page — an unguarded
|
|
6
|
+
//! overflow grows down into agb's live data (the audio mixer's buffers among them) and surfaces
|
|
7
|
+
//! frames later as an illegal opcode at an address that names nothing.
|
|
8
|
+
//!
|
|
9
|
+
//! `tishlang_runtime_gba` now derives a real floor from the link map (`__iwram_end`), so both the
|
|
10
|
+
//! typed and boxed guards are emitted here exactly as they are on the host.
|
|
11
|
+
|
|
12
|
+
use std::path::PathBuf;
|
|
13
|
+
|
|
14
|
+
use tishlang_compile::{compile_project_full_emit, NativeEmitMode};
|
|
15
|
+
|
|
16
|
+
fn gba_rust(fixture: &str) -> String {
|
|
17
|
+
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(fixture);
|
|
18
|
+
compile_project_full_emit(&path, path.parent(), &[], true, NativeEmitMode::Gba, None)
|
|
19
|
+
.expect("compile for gba")
|
|
20
|
+
.0
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// A self-recursive typed fn opens with the stack check and bails through the NaN sentinel.
|
|
24
|
+
#[test]
|
|
25
|
+
fn gba_typed_recursion_emits_stack_guard() {
|
|
26
|
+
let rust = gba_rust("../../tests/perf/recursion_fib.tish");
|
|
27
|
+
let copy0 = rust
|
|
28
|
+
.split("fn fib_native(")
|
|
29
|
+
.nth(1)
|
|
30
|
+
.expect("fib_native must be lowered on gba")
|
|
31
|
+
.split("\nfn ")
|
|
32
|
+
.next()
|
|
33
|
+
.unwrap()
|
|
34
|
+
.to_string();
|
|
35
|
+
assert!(
|
|
36
|
+
copy0.contains("tishlang_runtime::stack_low()")
|
|
37
|
+
&& copy0.contains("tishlang_runtime::recursion_tripped_f64()"),
|
|
38
|
+
"gba typed recursion must open with the stack check — without it an overflow silently \
|
|
39
|
+
overwrites IWRAM instead of raising a catchable RangeError:\n{copy0}"
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -350,15 +350,107 @@ fn read_facade_agb_version(facade_path: &Path) -> Option<String> {
|
|
|
350
350
|
None
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
-
///
|
|
353
|
+
/// Cargo `[profile.*]` blocks for the nested GBA crate.
|
|
354
354
|
///
|
|
355
|
-
///
|
|
356
|
-
/// `
|
|
357
|
-
///
|
|
355
|
+
/// #581 — the default used to be `lto = "fat"` + `codegen-units = 1`, which dominates wall-clock on
|
|
356
|
+
/// the ~20k-line `run()` a real game generates, and every `tish build --target gba` paid it. Thin
|
|
357
|
+
/// LTO across 8 CGUs keeps most of the size/speed win and lets rustc parallelize; fat LTO stays one
|
|
358
|
+
/// env var away for a ship build.
|
|
358
359
|
///
|
|
359
|
-
///
|
|
360
|
-
///
|
|
361
|
-
///
|
|
360
|
+
/// Env knobs (checked here, not by cargo):
|
|
361
|
+
/// - `TISH_FAST_NATIVE_BUILD=1` — iteration profile: no LTO, opt-level 1, many codegen-units.
|
|
362
|
+
/// Same flag as desktop native; use this for day-to-day GBA work.
|
|
363
|
+
/// - `TISH_GBA_FAT_LTO=1` — old ship profile: fat LTO + single CGU (slow; smallest/fastest ROM).
|
|
364
|
+
/// - default — thin LTO, opt-level 3, 8 CGUs, no debuginfo. Much faster than fat LTO while still
|
|
365
|
+
/// producing a solid ROM.
|
|
366
|
+
/// - `TISH_GBA_DEBUG=1` — keep debuginfo in the release profile (slower link; for mgba backtraces).
|
|
367
|
+
fn gba_cargo_profiles_toml() -> String {
|
|
368
|
+
let debug = if std::env::var("TISH_GBA_DEBUG").as_deref() == Ok("1") {
|
|
369
|
+
"true"
|
|
370
|
+
} else {
|
|
371
|
+
"false"
|
|
372
|
+
};
|
|
373
|
+
if std::env::var("TISH_FAST_NATIVE_BUILD").as_deref() == Ok("1") {
|
|
374
|
+
return format!(
|
|
375
|
+
r#"[profile.dev]
|
|
376
|
+
opt-level = 1
|
|
377
|
+
debug = {debug}
|
|
378
|
+
|
|
379
|
+
[profile.dev.build-override]
|
|
380
|
+
opt-level = 1
|
|
381
|
+
|
|
382
|
+
[profile.release]
|
|
383
|
+
opt-level = 1
|
|
384
|
+
lto = false
|
|
385
|
+
codegen-units = 16
|
|
386
|
+
incremental = true
|
|
387
|
+
debug = {debug}
|
|
388
|
+
panic = "abort"
|
|
389
|
+
|
|
390
|
+
[profile.release.build-override]
|
|
391
|
+
opt-level = 1
|
|
392
|
+
"#
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
// ⚠️ THIN LTO IS OPT-IN, AND THE REASON IS STACK, NOT SPEED. Less inlining means bigger frames,
|
|
396
|
+
// and a GBA has 32 KB of IWRAM for the whole stack. Measured on tish-gba's examples/ffta, same
|
|
397
|
+
// source, cold build dir:
|
|
398
|
+
//
|
|
399
|
+
// profile run() factory combined vs 32,512 B usable stack
|
|
400
|
+
// fat 23,604 + 7,452 = 31,056 fits, 1,456 B spare
|
|
401
|
+
// thin 25,436 + 9,068 = 34,504 OVER by 1,992 B
|
|
402
|
+
//
|
|
403
|
+
// And it does not trap. The deepest SP under thin is 0x02FFF838; the GBA mirrors EWRAM every
|
|
404
|
+
// 256 KB, so that aliases 0x0203F838 — the top of the heap. The stack writes into allocated
|
|
405
|
+
// memory and the ROM keeps running, which is far worse to diagnose than a crash.
|
|
406
|
+
//
|
|
407
|
+
// The build-time win thin was introduced for is real but example-dependent: on a small ROM
|
|
408
|
+
// (examples/ffta-hud) it was 70s vs 86s with a 37 KB smaller output and byte-identical frames;
|
|
409
|
+
// on ffta itself it was SLOWER, 148s vs 137s. So it is offered, not defaulted.
|
|
410
|
+
if std::env::var("TISH_GBA_THIN_LTO").as_deref() == Ok("1") {
|
|
411
|
+
return format!(
|
|
412
|
+
r#"[profile.dev]
|
|
413
|
+
opt-level = 3
|
|
414
|
+
debug = {debug}
|
|
415
|
+
|
|
416
|
+
[profile.dev.build-override]
|
|
417
|
+
opt-level = 3
|
|
418
|
+
|
|
419
|
+
[profile.release]
|
|
420
|
+
opt-level = 3
|
|
421
|
+
lto = "thin"
|
|
422
|
+
codegen-units = 8
|
|
423
|
+
debug = {debug}
|
|
424
|
+
panic = "abort"
|
|
425
|
+
|
|
426
|
+
[profile.release.build-override]
|
|
427
|
+
opt-level = 3
|
|
428
|
+
"#
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
// Default: fat LTO, one codegen unit — the smallest frames, which is what fits in IWRAM.
|
|
432
|
+
// `TISH_GBA_FAT_LTO=1` is still accepted and is now a no-op, so existing scripts keep working.
|
|
433
|
+
format!(
|
|
434
|
+
r#"[profile.dev]
|
|
435
|
+
opt-level = 3
|
|
436
|
+
debug = {debug}
|
|
437
|
+
|
|
438
|
+
[profile.dev.build-override]
|
|
439
|
+
opt-level = 3
|
|
440
|
+
|
|
441
|
+
[profile.release]
|
|
442
|
+
opt-level = 3
|
|
443
|
+
lto = "fat"
|
|
444
|
+
codegen-units = 1
|
|
445
|
+
debug = {debug}
|
|
446
|
+
panic = "abort"
|
|
447
|
+
|
|
448
|
+
[profile.release.build-override]
|
|
449
|
+
opt-level = 3
|
|
450
|
+
"#
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
|
|
362
454
|
/// Read `version = "…"` from the `[package]` section of a crate's `Cargo.toml`.
|
|
363
455
|
fn read_crate_version(crate_dir: &Path) -> Option<String> {
|
|
364
456
|
let toml = fs::read_to_string(crate_dir.join("Cargo.toml")).ok()?;
|
|
@@ -475,6 +567,17 @@ tishlang_runtime = {{ path = {runtime:?}{version}{features} }}
|
|
|
475
567
|
Ok(())
|
|
476
568
|
}
|
|
477
569
|
|
|
570
|
+
/// Build a Game Boy Advance ROM from generated `#![no_std]` Rust.
|
|
571
|
+
///
|
|
572
|
+
/// Emits an agb-style cargo project (nightly + build-std + `thumbv4t-none-eabi` +
|
|
573
|
+
/// `gba.ld`) that links the `tishlang_runtime_gba` facade under the `tishlang_runtime`
|
|
574
|
+
/// name (the `package =` rename), builds it, then runs `agb-gbafix` on the ELF.
|
|
575
|
+
///
|
|
576
|
+
/// Runs its own `cargo build` (NOT `run_cargo_build`) so the `.cargo/config.toml`
|
|
577
|
+
/// rustflags (`-Tgba.ld`, `-Ctarget-cpu=arm7tdmi`) apply — `run_cargo_build` sets a
|
|
578
|
+
/// `RUSTFLAGS` env that would shadow them.
|
|
579
|
+
///
|
|
580
|
+
/// Profiles come from [`gba_cargo_profiles_toml`].
|
|
478
581
|
fn build_gba_rom(
|
|
479
582
|
rust_code: &str,
|
|
480
583
|
native_modules: Vec<ResolvedNativeModule>,
|
|
@@ -543,6 +646,7 @@ fn build_gba_rom(
|
|
|
543
646
|
}
|
|
544
647
|
|
|
545
648
|
let facade = facade_path.display().to_string().replace('\\', "/");
|
|
649
|
+
let profiles = gba_cargo_profiles_toml();
|
|
546
650
|
let cargo_toml = format!(
|
|
547
651
|
r#"[package]
|
|
548
652
|
name = "tish_output"
|
|
@@ -561,28 +665,29 @@ path = "src/main.rs"
|
|
|
561
665
|
tishlang_runtime = {{ package = "tishlang_runtime_gba", path = {facade:?} }}
|
|
562
666
|
agb = "{agb_version}"
|
|
563
667
|
{more_deps}
|
|
564
|
-
|
|
565
|
-
opt-level = 3
|
|
566
|
-
debug = true
|
|
567
|
-
|
|
568
|
-
[profile.dev.build-override]
|
|
569
|
-
opt-level = 3
|
|
570
|
-
|
|
571
|
-
[profile.release]
|
|
572
|
-
opt-level = 3
|
|
573
|
-
lto = "fat"
|
|
574
|
-
debug = true
|
|
575
|
-
|
|
576
|
-
[profile.release.build-override]
|
|
577
|
-
opt-level = 3
|
|
668
|
+
{profiles}
|
|
578
669
|
"#,
|
|
579
670
|
);
|
|
580
671
|
fs::write(build_dir.join("Cargo.toml"), cargo_toml)
|
|
581
672
|
.map_err(|e| format!("Cannot write Cargo.toml: {}", e))?;
|
|
582
673
|
|
|
674
|
+
// Inherit the project's toolchain pin when it has one.
|
|
675
|
+
//
|
|
676
|
+
// rustup honours the `rust-toolchain.toml` NEAREST the directory cargo runs in, and this
|
|
677
|
+
// generated file sits in `.tish/gba/<name>/` — so it wins over the pin in the project's own
|
|
678
|
+
// checkout. Writing a bare `channel = "nightly"` here therefore made every GBA build float: the
|
|
679
|
+
// compiler could change under an unchanged checkout, and differ from CI or another machine, with
|
|
680
|
+
// nothing in the source to show it. Inheriting the project's channel makes the checkout the
|
|
681
|
+
// single source of truth; projects without a pin still get `nightly` exactly as before.
|
|
682
|
+
//
|
|
683
|
+
// Only the channel is inherited. `components` stays `rust-src` because that is what build-std
|
|
684
|
+
// needs — copying a project's `clippy`/`rustfmt` entries in would fail the ROM build whenever
|
|
685
|
+
// the pinned toolchain lacks a component the ROM never uses.
|
|
686
|
+
let channel =
|
|
687
|
+
find_project_toolchain_channel(&base).unwrap_or_else(|| "nightly".to_string());
|
|
583
688
|
fs::write(
|
|
584
689
|
build_dir.join("rust-toolchain.toml"),
|
|
585
|
-
"[toolchain]\nchannel = \"
|
|
690
|
+
format!("[toolchain]\nchannel = \"{}\"\ncomponents = [\"rust-src\"]\n", channel),
|
|
586
691
|
)
|
|
587
692
|
.map_err(|e| format!("Cannot write rust-toolchain.toml: {}", e))?;
|
|
588
693
|
|
|
@@ -694,6 +799,45 @@ runner = ["mgba-qt", "-C", "logToStdout=1", "-C", "logLevel.gba.debug=127"]
|
|
|
694
799
|
Ok(())
|
|
695
800
|
}
|
|
696
801
|
|
|
802
|
+
/// Find the toolchain channel the project pinned, searching `start` and its ancestors.
|
|
803
|
+
///
|
|
804
|
+
/// Mirrors rustup's own lookup: nearest ancestor wins, and both the TOML form and the legacy
|
|
805
|
+
/// one-line `rust-toolchain` file are accepted. Returns `None` when no pin exists anywhere above the
|
|
806
|
+
/// project, which is what keeps unpinned projects on plain `nightly`.
|
|
807
|
+
///
|
|
808
|
+
/// Deliberately a line scan rather than a TOML parse: this runs on every build, needs no new
|
|
809
|
+
/// dependency, and a pin is always a single `channel = "…"` line in practice. Comment lines are
|
|
810
|
+
/// skipped so prose mentioning a channel cannot be mistaken for the pin.
|
|
811
|
+
fn find_project_toolchain_channel(start: &Path) -> Option<String> {
|
|
812
|
+
for dir in start.ancestors() {
|
|
813
|
+
for name in ["rust-toolchain.toml", "rust-toolchain"] {
|
|
814
|
+
let Ok(text) = fs::read_to_string(dir.join(name)) else {
|
|
815
|
+
continue;
|
|
816
|
+
};
|
|
817
|
+
for line in text.lines() {
|
|
818
|
+
let line = line.trim();
|
|
819
|
+
if line.starts_with('#') {
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
if let Some(rest) = line.strip_prefix("channel") {
|
|
823
|
+
if let Some(value) = rest.trim_start().strip_prefix('=') {
|
|
824
|
+
let value = value.trim().trim_matches('"').trim_matches('\'');
|
|
825
|
+
if !value.is_empty() {
|
|
826
|
+
return Some(value.to_string());
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
// Legacy form: the entire file is the toolchain name (no table, no assignment).
|
|
832
|
+
let bare = text.trim();
|
|
833
|
+
if !bare.is_empty() && !bare.contains('[') && !bare.contains('=') {
|
|
834
|
+
return Some(bare.to_string());
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
None
|
|
839
|
+
}
|
|
840
|
+
|
|
697
841
|
/// Remove a finished build's per-PID source directory (#384). Called only on SUCCESS, after the
|
|
698
842
|
/// binary is copied out — the dir is then disposable (it holds only the generated crate source;
|
|
699
843
|
/// compiled artifacts live in the shared workspace `target/`). Left in place on any earlier error so a
|
|
@@ -871,6 +1015,44 @@ tishlang_runtime = {{ path = {:?}{} }}
|
|
|
871
1015
|
mod tests {
|
|
872
1016
|
use super::runtime_features_for_cargo;
|
|
873
1017
|
|
|
1018
|
+
/// The generated ROM crate must inherit the project's pin, or GBA builds float against whatever
|
|
1019
|
+
/// nightly the machine last downloaded (see `find_project_toolchain_channel`).
|
|
1020
|
+
#[test]
|
|
1021
|
+
fn project_toolchain_channel_is_inherited_from_nearest_pin() {
|
|
1022
|
+
use super::find_project_toolchain_channel;
|
|
1023
|
+
use std::fs;
|
|
1024
|
+
use std::path::PathBuf;
|
|
1025
|
+
// Scratch under the workspace target/ (never /tmp), unique per case.
|
|
1026
|
+
let base: PathBuf =
|
|
1027
|
+
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/tmp_toolchain_test");
|
|
1028
|
+
let nested = base.join("examples").join("game");
|
|
1029
|
+
let _ = fs::remove_dir_all(&base);
|
|
1030
|
+
fs::create_dir_all(&nested).unwrap();
|
|
1031
|
+
|
|
1032
|
+
// No pin anywhere below the workspace: caller falls back to plain `nightly`.
|
|
1033
|
+
assert_eq!(find_project_toolchain_channel(&nested), None);
|
|
1034
|
+
|
|
1035
|
+
// A pin several levels up is found, and prose in comments is not mistaken for it.
|
|
1036
|
+
fs::write(
|
|
1037
|
+
base.join("rust-toolchain.toml"),
|
|
1038
|
+
"# do not use a bare channel = \"nightly\"\n[toolchain]\nchannel = \"nightly-2026-07-22\"\ncomponents = [\"rust-src\"]\n",
|
|
1039
|
+
)
|
|
1040
|
+
.unwrap();
|
|
1041
|
+
assert_eq!(
|
|
1042
|
+
find_project_toolchain_channel(&nested).as_deref(),
|
|
1043
|
+
Some("nightly-2026-07-22")
|
|
1044
|
+
);
|
|
1045
|
+
|
|
1046
|
+
// Nearest ancestor wins, matching rustup's own lookup.
|
|
1047
|
+
fs::write(nested.join("rust-toolchain"), "nightly-2026-01-01\n").unwrap();
|
|
1048
|
+
assert_eq!(
|
|
1049
|
+
find_project_toolchain_channel(&nested).as_deref(),
|
|
1050
|
+
Some("nightly-2026-01-01")
|
|
1051
|
+
);
|
|
1052
|
+
|
|
1053
|
+
let _ = fs::remove_dir_all(&base);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
874
1056
|
/// #384: `cleanup_build_dir` removes a finished build's dir by default, and preserves it under
|
|
875
1057
|
/// `TISH_KEEP_BUILD_DIR=1`.
|
|
876
1058
|
#[test]
|
|
@@ -915,4 +1097,87 @@ mod tests {
|
|
|
915
1097
|
assert_eq!(f.len(), super::RUNTIME_CARGO_FEATURES.len());
|
|
916
1098
|
assert_eq!(f.iter().filter(|x| *x == "http").count(), 1);
|
|
917
1099
|
}
|
|
1100
|
+
|
|
1101
|
+
// #581 — the GBA ROM profile policy. The default used to be fat LTO + 1 CGU, so every
|
|
1102
|
+
// `tish build --target gba` paid a full fat-LTO link over the ~20k-line `run()` a real game
|
|
1103
|
+
// generates. These assert the policy rather than the wall-clock (which is machine-dependent):
|
|
1104
|
+
// thin by default, fat still reachable, and a genuinely fast iteration profile.
|
|
1105
|
+
//
|
|
1106
|
+
// Serialized because they mutate process-wide env; `gba_cargo_profiles_toml` reads it directly.
|
|
1107
|
+
#[test]
|
|
1108
|
+
fn gba_profiles_follow_581_policy() {
|
|
1109
|
+
use std::sync::Mutex;
|
|
1110
|
+
static ENV_LOCK: Mutex<()> = Mutex::new(());
|
|
1111
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
|
1112
|
+
|
|
1113
|
+
let restore = |k: &str, v: Option<String>| match v {
|
|
1114
|
+
Some(v) => std::env::set_var(k, v),
|
|
1115
|
+
None => std::env::remove_var(k),
|
|
1116
|
+
};
|
|
1117
|
+
let keys = [
|
|
1118
|
+
"TISH_GBA_FAT_LTO",
|
|
1119
|
+
"TISH_GBA_THIN_LTO",
|
|
1120
|
+
"TISH_FAST_NATIVE_BUILD",
|
|
1121
|
+
"TISH_GBA_DEBUG",
|
|
1122
|
+
];
|
|
1123
|
+
let saved: Vec<_> = keys.iter().map(|k| (*k, std::env::var(k).ok())).collect();
|
|
1124
|
+
for k in keys {
|
|
1125
|
+
std::env::remove_var(k);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// ⚠️ FAT IS THE DEFAULT, AND THE CONSTRAINT IS STACK, NOT LINK TIME. #581 briefly made thin
|
|
1129
|
+
// the default; it costs ~3.4 KB of stack frame on a real ROM (measured on tish-gba's
|
|
1130
|
+
// examples/ffta: 31,056 B combined under fat vs 34,504 under thin, against 32,512 usable),
|
|
1131
|
+
// and the overflow does not trap — it aliases into EWRAM through the GBA's 256 KB mirror and
|
|
1132
|
+
// corrupts the heap silently. See docs/gba-target.md.
|
|
1133
|
+
let default = super::gba_cargo_profiles_toml();
|
|
1134
|
+
assert!(
|
|
1135
|
+
default.contains(r#"lto = "fat""#) && default.contains("codegen-units = 1"),
|
|
1136
|
+
"default ROM profile must be fat LTO + 1 CGU — smallest stack frames:\n{default}"
|
|
1137
|
+
);
|
|
1138
|
+
assert!(
|
|
1139
|
+
!default.contains(r#"lto = "thin""#),
|
|
1140
|
+
"thin LTO must be opt-in via TISH_GBA_THIN_LTO, not the default:\n{default}"
|
|
1141
|
+
);
|
|
1142
|
+
assert!(
|
|
1143
|
+
default.contains("debug = false"),
|
|
1144
|
+
"release debuginfo must be opt-in via TISH_GBA_DEBUG (#581)"
|
|
1145
|
+
);
|
|
1146
|
+
|
|
1147
|
+
// Legacy: scripts that set TISH_GBA_FAT_LTO=1 still get fat (now a no-op, not an error).
|
|
1148
|
+
std::env::set_var("TISH_GBA_FAT_LTO", "1");
|
|
1149
|
+
let ship = super::gba_cargo_profiles_toml();
|
|
1150
|
+
assert!(
|
|
1151
|
+
ship.contains(r#"lto = "fat""#) && ship.contains("codegen-units = 1"),
|
|
1152
|
+
"TISH_GBA_FAT_LTO=1 must keep working as a no-op:\n{ship}"
|
|
1153
|
+
);
|
|
1154
|
+
std::env::remove_var("TISH_GBA_FAT_LTO");
|
|
1155
|
+
|
|
1156
|
+
// The opt-in that #581 was really about: faster iteration where stack headroom allows.
|
|
1157
|
+
std::env::set_var("TISH_GBA_THIN_LTO", "1");
|
|
1158
|
+
let thin = super::gba_cargo_profiles_toml();
|
|
1159
|
+
assert!(
|
|
1160
|
+
thin.contains(r#"lto = "thin""#) && thin.contains("codegen-units = 8"),
|
|
1161
|
+
"TISH_GBA_THIN_LTO=1 must select thin LTO across 8 CGUs:\n{thin}"
|
|
1162
|
+
);
|
|
1163
|
+
std::env::remove_var("TISH_GBA_THIN_LTO");
|
|
1164
|
+
|
|
1165
|
+
std::env::set_var("TISH_FAST_NATIVE_BUILD", "1");
|
|
1166
|
+
let fast = super::gba_cargo_profiles_toml();
|
|
1167
|
+
assert!(
|
|
1168
|
+
fast.contains("lto = false") && fast.contains("opt-level = 1"),
|
|
1169
|
+
"TISH_FAST_NATIVE_BUILD=1 is the iteration profile — no LTO, opt-level 1 (#581):\n{fast}"
|
|
1170
|
+
);
|
|
1171
|
+
std::env::remove_var("TISH_FAST_NATIVE_BUILD");
|
|
1172
|
+
|
|
1173
|
+
std::env::set_var("TISH_GBA_DEBUG", "1");
|
|
1174
|
+
assert!(
|
|
1175
|
+
super::gba_cargo_profiles_toml().contains("debug = true"),
|
|
1176
|
+
"TISH_GBA_DEBUG=1 must keep release debuginfo (#581)"
|
|
1177
|
+
);
|
|
1178
|
+
|
|
1179
|
+
for (k, v) in saved {
|
|
1180
|
+
restore(k, v);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
918
1183
|
}
|
|
@@ -30,13 +30,62 @@ pub use tishlang_core::{
|
|
|
30
30
|
has_pending_throw, set_pending_throw, stack_overflow_error, take_pending_throw, CallDepthGuard,
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
///
|
|
33
|
+
unsafe extern "C" {
|
|
34
|
+
/// End of the `.iwram` output section — provided by agb's `gba.ld`. IWRAM runs
|
|
35
|
+
/// 0x0300_0000..0x0300_8000; agb's code/data occupies the bottom and the user
|
|
36
|
+
/// stack grows DOWN from 0x0300_7F00 toward this address. Anything below it is
|
|
37
|
+
/// live agb data (the audio mixer's buffers among them), so this is the real
|
|
38
|
+
/// bottom of the stack.
|
|
39
|
+
static __iwram_end: u8;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Headroom kept above [`__iwram_end`] (#655): must cover the frames emitted between
|
|
43
|
+
/// two guard checks plus the bail path that parks the `RangeError`. Boxed `Value`
|
|
44
|
+
/// frames on GBA run a few hundred bytes each, and the total stack is under 32 KB,
|
|
45
|
+
/// so this is deliberately small relative to the host's 256 KB margin.
|
|
46
|
+
const GBA_STACK_MARGIN: usize = 2 * 1024;
|
|
47
|
+
|
|
48
|
+
/// #655 — is the GBA stack nearly exhausted? The host probe (`stacker::remaining_stack`)
|
|
49
|
+
/// needs `std` and reports no bounds here, so its `None → floor 1` fallback made the
|
|
50
|
+
/// guard dead code on the one target where overflow is unrecoverable: there is no MMU
|
|
51
|
+
/// and no guard page, so the stack silently grows down into agb's IWRAM data and the
|
|
52
|
+
/// corruption surfaces frames later as an illegal opcode somewhere unrelated.
|
|
53
|
+
///
|
|
54
|
+
/// The bounds ARE known statically here — the linker hands us the IWRAM extent — so
|
|
55
|
+
/// this is a link-time constant plus a pointer compare, about the cost of the
|
|
56
|
+
/// thread-local read it replaces on the host.
|
|
57
|
+
#[inline]
|
|
58
|
+
pub fn stack_low() -> bool {
|
|
59
|
+
let anchor = 0u8;
|
|
60
|
+
let sp = &anchor as *const u8 as usize;
|
|
61
|
+
let floor = (&raw const __iwram_end) as usize + GBA_STACK_MARGIN;
|
|
62
|
+
sp < floor
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Enter a boxed user-fn call frame, or trip the recursion guard. Trips on EITHER the
|
|
66
|
+
/// counted ceiling (`tishlang_core`) or real stack pressure ([`stack_low`]) — on a
|
|
67
|
+
/// 32 KB IWRAM stack the counted default is far out of reach, so pressure is the
|
|
68
|
+
/// trigger that actually fires. On trip: parks the catchable `RangeError` and returns
|
|
69
|
+
/// `None`, exactly as on the host.
|
|
35
70
|
#[inline]
|
|
36
71
|
pub fn enter_call_guarded() -> Option<CallDepthGuard> {
|
|
72
|
+
if stack_low() {
|
|
73
|
+
set_pending_throw(stack_overflow_error());
|
|
74
|
+
return None;
|
|
75
|
+
}
|
|
37
76
|
tishlang_core::enter_call_guarded()
|
|
38
77
|
}
|
|
39
78
|
|
|
79
|
+
/// #655 — bail path for a tripped typed-fn recursion guard (GBA mirror of the host's
|
|
80
|
+
/// `recursion_tripped_f64`): park the catchable `RangeError` and unwind the numeric
|
|
81
|
+
/// frame with NaN until the first `Value` frame's pending-throw checkpoint raises it.
|
|
82
|
+
#[cold]
|
|
83
|
+
#[inline(never)]
|
|
84
|
+
pub fn recursion_tripped_f64() -> f64 {
|
|
85
|
+
set_pending_throw(stack_overflow_error());
|
|
86
|
+
f64::NAN
|
|
87
|
+
}
|
|
88
|
+
|
|
40
89
|
// ── Error type for throw/return non-local control flow ───────────────────────
|
|
41
90
|
/// Mirrors `tishlang_runtime::TishError` (the host runtime defines its own; kept
|
|
42
91
|
/// in lock-step). `run()` in generated code returns `Result<(), Box<dyn Error>>`.
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|