@tishlang/tish-format 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tish-format +0 -0
- package/crates/tish/src/cli_help.rs +4 -0
- package/crates/tish/src/main.rs +13 -2
- package/crates/tish/tests/rust_lib_crate.rs +140 -0
- package/crates/tish_compile/src/codegen.rs +1395 -107
- package/crates/tish_compile/src/lib.rs +14 -1
- package/crates/tish_compile/src/types.rs +6 -0
- package/crates/tish_compile/tests/perf_codegen_i32_native_abi.rs +113 -0
- package/crates/tish_compile/tests/regr_andor_rhs_move.rs +85 -0
- package/crates/tish_compile/tests/regr_gba_lowered_call_sites.rs +487 -0
- package/crates/tish_compile/tests/regr_rust_lib_emit.rs +150 -0
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_fmt/src/lib.rs +54 -1
- package/crates/tish_native/src/build.rs +151 -6
- package/crates/tish_native/src/config.rs +14 -0
- package/crates/tish_runtime_gba/src/gba.rs +26 -6
- package/crates/tish_runtime_gba/src/lib.rs +46 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
|
@@ -40,6 +40,19 @@ pub enum NativeEmitMode {
|
|
|
40
40
|
DesktopBin,
|
|
41
41
|
/// `[lib] crate-type = ["staticlib"]` — no `fn main()`, host calls `tish_ios_launch`.
|
|
42
42
|
EmbeddedLib,
|
|
43
|
+
/// `[lib] crate-type = ["rlib"]` — a Rust *library* crate whose public API is the entry
|
|
44
|
+
/// module's `export fn`s, so a Tish library can be consumed from Rust the way `--target js`
|
|
45
|
+
/// lets it be consumed from JS. No `fn main()`, no `tish_ios_launch`.
|
|
46
|
+
///
|
|
47
|
+
/// The module body still lowers to the ordinary whole-program `run()`; what differs is that
|
|
48
|
+
/// `run()` is invoked lazily behind an `ensure_init()` and its exported bindings are parked in
|
|
49
|
+
/// a `thread_local!` so they outlive the call. That keeps module-level state alive between
|
|
50
|
+
/// `pub fn` calls with the same semantics an ESM module has — including reassignment, since
|
|
51
|
+
/// top-level `let`s are captured by the emitted closures as `VmRef` cells.
|
|
52
|
+
///
|
|
53
|
+
/// `thread_local!` (rather than a global) is forced by `Value`: `send-values` is off unless
|
|
54
|
+
/// `http` is enabled, so `VmRef` is `Rc<RefCell<_>>` and `Value` is `!Send`.
|
|
55
|
+
RustLib,
|
|
43
56
|
/// Game Boy Advance ROM: `#![no_std]`, `#[agb::entry] fn agb_main(gba)`, links the
|
|
44
57
|
/// `tishlang_runtime_gba` facade. Numbers/async lowering diverge; see codegen `emit_program`.
|
|
45
58
|
Gba,
|
|
@@ -49,7 +62,7 @@ pub use codegen::CompileError;
|
|
|
49
62
|
pub use codegen::{
|
|
50
63
|
compile, compile_project, compile_project_full, compile_project_full_emit,
|
|
51
64
|
compile_with_features, compile_with_native_modules, compile_with_native_modules_emit,
|
|
52
|
-
compile_with_project_root,
|
|
65
|
+
compile_with_native_modules_emit_exports, compile_with_project_root,
|
|
53
66
|
};
|
|
54
67
|
pub use platform_resolve::{
|
|
55
68
|
apply_resolve_env, parse_platform, parse_surface, platform_suffixes, platform_virtual_keys,
|
|
@@ -30,6 +30,12 @@ fn gba_numerics() -> bool {
|
|
|
30
30
|
GBA_NUMERICS.with(|c| c.get())
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/// Public view of the same switch, for passes that must behave differently only where the extended
|
|
34
|
+
/// numeric vocabulary exists — see the native-fn bitwise widening in `codegen.rs`.
|
|
35
|
+
pub fn gba_numerics_enabled() -> bool {
|
|
36
|
+
gba_numerics()
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
/// Concrete Rust type representation for code generation.
|
|
34
40
|
#[derive(Debug, Clone, PartialEq)]
|
|
35
41
|
pub enum RustType {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
//! M5 native fns, with an `i32` calling convention.
|
|
2
|
+
//!
|
|
3
|
+
//! `collect_native_fns` gated parameters and returns on `RustType::F64`, so `: number` qualified for
|
|
4
|
+
//! a native free `fn` and `: i32` did not — and `: i32` is the annotation a device with no FPU
|
|
5
|
+
//! wants. Every argument and every return of such a call was therefore an i32→f64→i32 round trip,
|
|
6
|
+
//! which on ARM7TDMI is a soft-float call apiece.
|
|
7
|
+
//!
|
|
8
|
+
//! The shape is ALL-OR-NOTHING: every parameter and the return annotated `: i32`. Mixing is what
|
|
9
|
+
//! makes it unsound — an `: i32` parameter reached through the boxed path is ToInt32-coerced on
|
|
10
|
+
//! entry, and promoted to a native `f64` parameter it would not be, so 3.7 would arrive as 3 one
|
|
11
|
+
//! way and 3.7 the other.
|
|
12
|
+
|
|
13
|
+
use std::path::PathBuf;
|
|
14
|
+
|
|
15
|
+
use tishlang_compile::{compile_project_full_emit, NativeEmitMode};
|
|
16
|
+
|
|
17
|
+
/// Compile for GBA — `: i32` is only a native scalar under the GBA numeric vocabulary
|
|
18
|
+
/// (`types.rs`: `"i32" if gba_numerics()`), so on a desktop target this ABI cannot exist and the
|
|
19
|
+
/// assertions below would be vacuous.
|
|
20
|
+
fn compile_src(stem: &str, src: &str) -> String {
|
|
21
|
+
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/perf_codegen_i32_abi");
|
|
22
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
23
|
+
let path = dir.join(format!("{stem}.tish"));
|
|
24
|
+
std::fs::write(&path, src).unwrap();
|
|
25
|
+
let path = path.canonicalize().unwrap();
|
|
26
|
+
let (rust, _, _, _) =
|
|
27
|
+
compile_project_full_emit(&path, path.parent(), &[], true, NativeEmitMode::Gba, None)
|
|
28
|
+
.unwrap();
|
|
29
|
+
rust
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const ALL_I32: &str = r#"
|
|
33
|
+
function mix(a: i32, b: i32): i32 { return ((a * 3) ^ (b << 2)) & 65535 }
|
|
34
|
+
let acc: i32 = 0
|
|
35
|
+
let j: i32 = 0
|
|
36
|
+
while (j < 10) { acc = (acc + mix(j, acc)) & 65535; j = j + 1 }
|
|
37
|
+
console.log(acc)
|
|
38
|
+
"#;
|
|
39
|
+
|
|
40
|
+
#[test]
|
|
41
|
+
fn an_all_i32_signature_gets_an_i32_native_fn() {
|
|
42
|
+
let rust = compile_src("all_i32", ALL_I32);
|
|
43
|
+
assert!(
|
|
44
|
+
rust.contains("fn mix_native(mut a: i32, mut b: i32) -> i32"),
|
|
45
|
+
"expected an i32-signature native fn, got:\n{}",
|
|
46
|
+
rust.lines().filter(|l| l.contains("mix_native")).collect::<Vec<_>>().join("\n")
|
|
47
|
+
);
|
|
48
|
+
// And the call must reach it directly rather than through `value_call`.
|
|
49
|
+
assert!(rust.contains("mix_native("), "the call site should target the native fn");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const MIXED: &str = r#"
|
|
53
|
+
function half(a: i32, b: number): i32 { return (a + b) | 0 }
|
|
54
|
+
console.log(half(7, 2.5))
|
|
55
|
+
"#;
|
|
56
|
+
|
|
57
|
+
#[test]
|
|
58
|
+
fn a_mixed_signature_does_not_get_the_i32_abi() {
|
|
59
|
+
let rust = compile_src("mixed", MIXED);
|
|
60
|
+
assert!(
|
|
61
|
+
!rust.contains("fn half_native(mut a: i32"),
|
|
62
|
+
"a mixed i32/number signature must not take the i32 ABI — an `: i32` param is \
|
|
63
|
+
ToInt32-coerced on entry and an f64 native param is not"
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const LIAR: &str = r#"
|
|
68
|
+
function liar(x: i32): i32 {
|
|
69
|
+
if (x > 0) { return "not a number" }
|
|
70
|
+
return 7
|
|
71
|
+
}
|
|
72
|
+
console.log(liar(1))
|
|
73
|
+
"#;
|
|
74
|
+
|
|
75
|
+
#[test]
|
|
76
|
+
fn the_annotation_alone_does_not_qualify_a_fn() {
|
|
77
|
+
// Return annotations are erased and unchecked: this program prints the STRING today. The i32
|
|
78
|
+
// ABI must not be handed out on the strength of `: i32`, only on the fixpoint's proof that
|
|
79
|
+
// every return is numeric — otherwise this would unbox a string as a number.
|
|
80
|
+
let rust = compile_src("liar", LIAR);
|
|
81
|
+
assert!(
|
|
82
|
+
!rust.contains("fn liar_native"),
|
|
83
|
+
"a fn with a non-numeric return path must stay boxed regardless of its annotation"
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const READS_MODULE_STATE: &str = r#"
|
|
88
|
+
let ARR: i32[] = []
|
|
89
|
+
let k: i32 = 0
|
|
90
|
+
while (k < 8) { ARR.push(k); k = k + 1 }
|
|
91
|
+
function pure(a: i32, b: i32): i32 { return (a ^ b) & 255 }
|
|
92
|
+
function reads(i: i32): i32 { return ARR[i] & 255 }
|
|
93
|
+
let x: i32 = pure(1, 2) + reads(3)
|
|
94
|
+
console.log(x)
|
|
95
|
+
"#;
|
|
96
|
+
|
|
97
|
+
#[test]
|
|
98
|
+
fn a_fn_reading_module_state_stays_boxed() {
|
|
99
|
+
// A native fn is emitted at TOP LEVEL, where a module-scope `let` — a local of `run()`, closed
|
|
100
|
+
// over by the boxed closures — does not exist. `native_safe_expr`'s index arm accepted any
|
|
101
|
+
// identifier as the indexed object, so this compiled to a `reads_native` whose body named
|
|
102
|
+
// `ARR` and rustc rejected the generated program with E0425. The sibling `pure` must still be
|
|
103
|
+
// promoted: the check has to reject the out-of-scope read, not indexing as such.
|
|
104
|
+
let rust = compile_src("reads_module_state", READS_MODULE_STATE);
|
|
105
|
+
assert!(
|
|
106
|
+
!rust.contains("fn reads_native"),
|
|
107
|
+
"a fn indexing a module-scope binding cannot become a top-level free fn"
|
|
108
|
+
);
|
|
109
|
+
assert!(
|
|
110
|
+
rust.contains("fn pure_native(mut a: i32, mut b: i32) -> i32"),
|
|
111
|
+
"its parameter-only sibling must still get the native i32 fn"
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
//! Regression: `&&` / `||` yield the deciding OPERAND (JS semantics, #240). #328 fixed the left
|
|
2
|
+
//! operand to bind by `.clone()` rather than by move; the right operand had the identical hazard.
|
|
3
|
+
//!
|
|
4
|
+
//! When the right operand lowers to a bare local — `inGenBlock && currentTrack` — yielding it moved
|
|
5
|
+
//! that local out of scope, so any later read was `E0382: use of moved value`. It only bites when
|
|
6
|
+
//! the operand is a plain non-`Copy` `Value` local that is read again (typically the next iteration
|
|
7
|
+
//! of the enclosing loop), which is why it survived: the common right operand is a call or an
|
|
8
|
+
//! already-cloned expression, which are temporaries.
|
|
9
|
+
//!
|
|
10
|
+
//! Found compiling `@spacedevin/deck`'s `Parser.tish`, whose statement loop does exactly this. The
|
|
11
|
+
//! source runs fine under `--target js` and the interpreter, so the divergence was Rust-backend-only.
|
|
12
|
+
use tishlang_compile::compile;
|
|
13
|
+
use tishlang_parser::parse;
|
|
14
|
+
|
|
15
|
+
fn rust_for(src: &str) -> String {
|
|
16
|
+
compile(&parse(src).unwrap()).unwrap()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[test]
|
|
20
|
+
fn bare_ident_right_operand_is_cloned_not_moved() {
|
|
21
|
+
let rust = rust_for(
|
|
22
|
+
r#"
|
|
23
|
+
fn f() {
|
|
24
|
+
let cur = null
|
|
25
|
+
let flag = true
|
|
26
|
+
let i = 0
|
|
27
|
+
while (i < 3) {
|
|
28
|
+
if (flag && cur) { return 1 }
|
|
29
|
+
if (flag && cur) { return 2 }
|
|
30
|
+
i = i + 1
|
|
31
|
+
}
|
|
32
|
+
return 0
|
|
33
|
+
}
|
|
34
|
+
"#,
|
|
35
|
+
);
|
|
36
|
+
assert!(
|
|
37
|
+
rust.contains("if __l.is_truthy() { cur.clone() }"),
|
|
38
|
+
"a bare-ident right operand must be cloned out, not moved:\n{rust}"
|
|
39
|
+
);
|
|
40
|
+
assert!(
|
|
41
|
+
!rust.contains("if __l.is_truthy() { cur }"),
|
|
42
|
+
"found the moving form, which is E0382 on the second read:\n{rust}"
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#[test]
|
|
47
|
+
fn or_right_operand_gets_the_same_treatment() {
|
|
48
|
+
let rust = rust_for(
|
|
49
|
+
r#"
|
|
50
|
+
fn f() {
|
|
51
|
+
let a = null
|
|
52
|
+
let b = null
|
|
53
|
+
let i = 0
|
|
54
|
+
while (i < 3) {
|
|
55
|
+
let x = a || b
|
|
56
|
+
let y = a || b
|
|
57
|
+
i = i + 1
|
|
58
|
+
}
|
|
59
|
+
return 0
|
|
60
|
+
}
|
|
61
|
+
"#,
|
|
62
|
+
);
|
|
63
|
+
assert!(
|
|
64
|
+
rust.contains("else { b.clone() }"),
|
|
65
|
+
"`||` must clone a bare-ident right operand too:\n{rust}"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[test]
|
|
70
|
+
fn non_ident_right_operands_are_not_given_a_redundant_clone() {
|
|
71
|
+
// A call result is already a temporary — cloning it would be pure overhead on a hot path.
|
|
72
|
+
let rust = rust_for(
|
|
73
|
+
r#"
|
|
74
|
+
fn g() { return 1 }
|
|
75
|
+
fn f() {
|
|
76
|
+
let flag = true
|
|
77
|
+
return flag && g()
|
|
78
|
+
}
|
|
79
|
+
"#,
|
|
80
|
+
);
|
|
81
|
+
assert!(
|
|
82
|
+
!rust.contains("))).clone().clone()"),
|
|
83
|
+
"an already-temporary right operand must not gain a second clone:\n{rust}"
|
|
84
|
+
);
|
|
85
|
+
}
|