@tishlang/tish-format 3.0.0 → 3.2.2
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/tests/scope_merge.rs +31 -0
- package/crates/tish_build_utils/src/lib.rs +131 -6
- package/crates/tish_compile/src/codegen.rs +517 -19
- package/crates/tish_compile/src/lib.rs +14 -1
- package/crates/tish_compile/src/resolve.rs +12 -2
- package/crates/tish_compile/tests/regr_andor_rhs_move.rs +85 -0
- package/crates/tish_compile/tests/regr_rust_lib_emit.rs +150 -0
- package/crates/tish_core/src/shape.rs +28 -3
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_fmt/src/lib.rs +54 -1
- package/crates/tish_native/src/build.rs +131 -0
- package/crates/tish_native/src/config.rs +14 -0
- package/crates/tish_runtime/Cargo.toml +20 -1
- package/crates/tish_runtime/src/crypto.rs +127 -0
- package/crates/tish_runtime/src/encoding.rs +157 -0
- package/crates/tish_runtime/src/fs_ext.rs +27 -0
- package/crates/tish_runtime/src/lib.rs +32 -0
- package/crates/tish_runtime/src/net.rs +482 -0
- package/crates/tish_runtime/src/process_spawn.rs +508 -0
- package/crates/tish_runtime/src/ws.rs +206 -23
- package/crates/tish_runtime/src/zip.rs +196 -0
- package/crates/tish_runtime_gba/src/lib.rs +17 -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,
|
|
@@ -77,6 +77,10 @@ const BUILTIN_ALIASES: &[(&str, &str)] = &[
|
|
|
77
77
|
("ws", "tish:ws"),
|
|
78
78
|
("tty", "tish:tty"),
|
|
79
79
|
("pty", "tish:pty"),
|
|
80
|
+
("net", "tish:net"),
|
|
81
|
+
("encoding", "tish:encoding"),
|
|
82
|
+
("crypto", "tish:crypto"),
|
|
83
|
+
("zip", "tish:zip"),
|
|
80
84
|
];
|
|
81
85
|
|
|
82
86
|
/// Normalize built-in spec to canonical form. Handles the Node `node:` prefix
|
|
@@ -106,9 +110,14 @@ pub fn is_builtin_native_spec(spec: &str) -> bool {
|
|
|
106
110
|
| "tish:ws"
|
|
107
111
|
| "tish:tty"
|
|
108
112
|
| "tish:pty"
|
|
113
|
+
| "tish:net"
|
|
114
|
+
| "tish:encoding"
|
|
115
|
+
| "tish:crypto"
|
|
116
|
+
| "tish:zip"
|
|
109
117
|
) || matches!(
|
|
110
118
|
spec,
|
|
111
|
-
"fs" | "fs/promises" | "http" | "timers" | "process" | "ws" | "tty" | "pty"
|
|
119
|
+
"fs" | "fs/promises" | "http" | "timers" | "process" | "ws" | "tty" | "pty" | "net"
|
|
120
|
+
| "encoding" | "crypto" | "zip"
|
|
112
121
|
)
|
|
113
122
|
}
|
|
114
123
|
|
|
@@ -1029,7 +1038,8 @@ pub fn is_native_import(spec: &str) -> bool {
|
|
|
1029
1038
|
|| spec.starts_with("ffi:")
|
|
1030
1039
|
|| matches!(
|
|
1031
1040
|
spec,
|
|
1032
|
-
"fs" | "fs/promises" | "http" | "timers" | "process" | "ws" | "tty"
|
|
1041
|
+
"fs" | "fs/promises" | "http" | "timers" | "process" | "ws" | "tty" | "net" | "encoding"
|
|
1042
|
+
| "crypto" | "zip"
|
|
1033
1043
|
)
|
|
1034
1044
|
}
|
|
1035
1045
|
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
//! `--target rust-lib` (`NativeEmitMode::RustLib`): a Tish library compiled to a Rust library crate
|
|
2
|
+
//! whose public API is the entry module's `export fn`s — the Rust counterpart of `--target js`.
|
|
3
|
+
//!
|
|
4
|
+
//! The module body still lowers to the ordinary whole-program `run()`. What the library emit adds is
|
|
5
|
+
//! (a) one `pub fn` per export and (b) keeping the exported bindings alive past `run()` in a
|
|
6
|
+
//! thread-local, so module-level state survives between calls. These tests pin that emitted shape;
|
|
7
|
+
//! the crate layout and the behavioural end (state persists, reassignment is observable, init runs
|
|
8
|
+
//! once) are covered in `crates/tish/tests/rust_lib_crate.rs`.
|
|
9
|
+
use std::path::PathBuf;
|
|
10
|
+
use tishlang_compile::{compile_project_full, compile_project_full_emit, NativeEmitMode};
|
|
11
|
+
|
|
12
|
+
fn fixture(rel: &str) -> PathBuf {
|
|
13
|
+
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
14
|
+
manifest.join("../..").join(rel).canonicalize().unwrap()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fn compile_lib(rel: &str) -> String {
|
|
18
|
+
let path = fixture(rel);
|
|
19
|
+
compile_project_full_emit(&path, path.parent(), &[], true, NativeEmitMode::RustLib, None)
|
|
20
|
+
.expect("rust-lib emit")
|
|
21
|
+
.0
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#[test]
|
|
25
|
+
fn every_export_becomes_an_arity_matched_pub_fn() {
|
|
26
|
+
let rust = compile_lib("tests/regression/rust_lib_emit.tish");
|
|
27
|
+
for sig in [
|
|
28
|
+
"pub fn register(a0: Value, a1: Value) -> Value",
|
|
29
|
+
"pub fn lookup(a0: Value) -> Value",
|
|
30
|
+
"pub fn clearAll() -> Value",
|
|
31
|
+
"pub fn initRuns() -> Value",
|
|
32
|
+
"pub fn add(a0: Value, a1: Value) -> Value",
|
|
33
|
+
] {
|
|
34
|
+
assert!(
|
|
35
|
+
rust.contains(sig),
|
|
36
|
+
"missing `{sig}` in the emitted library surface:\n{rust}"
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#[test]
|
|
42
|
+
fn library_crate_has_no_binary_entry_point() {
|
|
43
|
+
let rust = compile_lib("tests/regression/rust_lib_emit.tish");
|
|
44
|
+
// A `fn main` (or the iOS C entry) would make this an executable/staticlib, not an rlib.
|
|
45
|
+
assert!(
|
|
46
|
+
!rust.contains("fn main()"),
|
|
47
|
+
"a library crate must not emit a binary entry point:\n{rust}"
|
|
48
|
+
);
|
|
49
|
+
assert!(
|
|
50
|
+
!rust.contains("tish_ios_launch"),
|
|
51
|
+
"the iOS staticlib entry belongs to EmbeddedLib, not RustLib:\n{rust}"
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#[test]
|
|
56
|
+
fn exports_are_published_out_of_run_so_module_state_outlives_it() {
|
|
57
|
+
let rust = compile_lib("tests/regression/rust_lib_emit.tish");
|
|
58
|
+
// Without this call the closures — and the `VmRef` cells they captured — drop with `run()`'s
|
|
59
|
+
// frame, and every `pub fn` would see a fresh module on each call.
|
|
60
|
+
assert!(
|
|
61
|
+
rust.contains("__tish_publish_exports(::std::vec!["),
|
|
62
|
+
"run() must hand its exported bindings to the module table:\n{rust}"
|
|
63
|
+
);
|
|
64
|
+
assert!(
|
|
65
|
+
rust.contains("(\"clearAll\", clearAll.clone())"),
|
|
66
|
+
"each export must be published under its exported name:\n{rust}"
|
|
67
|
+
);
|
|
68
|
+
// `Value` is `!Send` unless `send-values` (which follows `http`) is on, so a `static` would not
|
|
69
|
+
// compile for the common configuration.
|
|
70
|
+
assert!(
|
|
71
|
+
rust.contains("thread_local! {") && rust.contains("static __TISH_MODULE"),
|
|
72
|
+
"module storage must be thread-local:\n{rust}"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#[test]
|
|
77
|
+
fn runtime_prelude_is_re_exported_so_consumers_cannot_version_skew_value() {
|
|
78
|
+
let rust = compile_lib("tests/regression/rust_lib_emit.tish");
|
|
79
|
+
assert!(
|
|
80
|
+
rust.contains("pub use tishlang_runtime::{console_debug"),
|
|
81
|
+
"RustLib must re-export the runtime prelude (so `Value` comes from THIS crate):\n{rust}"
|
|
82
|
+
);
|
|
83
|
+
assert!(
|
|
84
|
+
rust.contains("pub use tishlang_runtime as runtime;"),
|
|
85
|
+
"RustLib must re-export the runtime module:\n{rust}"
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#[test]
|
|
90
|
+
fn other_emit_modes_keep_the_prelude_private_and_gain_no_surface() {
|
|
91
|
+
let path = fixture("tests/regression/rust_lib_emit.tish");
|
|
92
|
+
let rust = compile_project_full(&path, path.parent(), &[], true)
|
|
93
|
+
.expect("desktop emit")
|
|
94
|
+
.0;
|
|
95
|
+
assert!(
|
|
96
|
+
!rust.contains("__TISH_MODULE") && !rust.contains("pub fn clearAll"),
|
|
97
|
+
"the library surface must be RustLib-only:\n{rust}"
|
|
98
|
+
);
|
|
99
|
+
assert!(
|
|
100
|
+
rust.contains("use tishlang_runtime::{console_debug")
|
|
101
|
+
&& !rust.contains("pub use tishlang_runtime::{console_debug"),
|
|
102
|
+
"a binary must keep the runtime prelude private:\n{rust}"
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#[test]
|
|
107
|
+
fn a_value_export_is_rejected_rather_than_silently_returning_null() {
|
|
108
|
+
// A value export has no arity and its Rust local need not even be a `Value` (a `const` string
|
|
109
|
+
// lowers to `&str`), so it cannot be published like a closure. Fail loudly instead.
|
|
110
|
+
let dir = std::env::temp_dir().join("tish_rustlib_value_export");
|
|
111
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
112
|
+
let path = dir.join("index.tish");
|
|
113
|
+
std::fs::write(&path, "export let answer = 42\nexport fn get() { return answer }\n").unwrap();
|
|
114
|
+
let err = compile_project_full_emit(
|
|
115
|
+
&path,
|
|
116
|
+
Some(dir.as_path()),
|
|
117
|
+
&[],
|
|
118
|
+
true,
|
|
119
|
+
NativeEmitMode::RustLib,
|
|
120
|
+
None,
|
|
121
|
+
)
|
|
122
|
+
.expect_err("a value export must be rejected");
|
|
123
|
+
assert!(
|
|
124
|
+
err.message.contains("only `export fn`") && err.message.contains("answer"),
|
|
125
|
+
"the error should name the offending export: {}",
|
|
126
|
+
err.message
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#[test]
|
|
131
|
+
fn an_entry_module_with_no_exports_is_rejected() {
|
|
132
|
+
let dir = std::env::temp_dir().join("tish_rustlib_no_exports");
|
|
133
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
134
|
+
let path = dir.join("index.tish");
|
|
135
|
+
std::fs::write(&path, "fn unused() { return 1 }\n").unwrap();
|
|
136
|
+
let err = compile_project_full_emit(
|
|
137
|
+
&path,
|
|
138
|
+
Some(dir.as_path()),
|
|
139
|
+
&[],
|
|
140
|
+
true,
|
|
141
|
+
NativeEmitMode::RustLib,
|
|
142
|
+
None,
|
|
143
|
+
)
|
|
144
|
+
.expect_err("a crate with no public API must be rejected");
|
|
145
|
+
assert!(
|
|
146
|
+
err.message.contains("exports nothing"),
|
|
147
|
+
"unexpected error: {}",
|
|
148
|
+
err.message
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
//! `PropMap` on a cache miss). Phase 1b will attach the ordered key list to each shape so objects can
|
|
13
13
|
//! drop per-object key storage entirely (the butterfly representation).
|
|
14
14
|
|
|
15
|
-
use crate::compat::
|
|
16
|
-
#[cfg(feature = "portable")]
|
|
17
|
-
use
|
|
15
|
+
use crate::compat::Arc;
|
|
16
|
+
#[cfg(not(feature = "portable"))]
|
|
17
|
+
use crate::compat::{AHashMap, OnceLock, RwLock};
|
|
18
18
|
|
|
19
19
|
/// Identity of an object's ordered key-set.
|
|
20
20
|
pub type ShapeId = u32;
|
|
@@ -31,15 +31,18 @@ pub const DICT_SHAPE: ShapeId = u32::MAX;
|
|
|
31
31
|
/// Edges are keyed by an `ahash` map, not the default SipHash one: `transition` is on the hot path of
|
|
32
32
|
/// every object construction (and `JSON.parse` — profiled as ~16% of parse, almost all SipHash), and
|
|
33
33
|
/// ahash is ~2–3× faster on short string keys. Random-seeded per process, so no HashDoS regression.
|
|
34
|
+
#[cfg(not(feature = "portable"))]
|
|
34
35
|
#[derive(Default)]
|
|
35
36
|
struct ShapeNode {
|
|
36
37
|
transitions: AHashMap<Arc<str>, ShapeId>,
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
#[cfg(not(feature = "portable"))]
|
|
39
41
|
struct Registry {
|
|
40
42
|
nodes: Vec<ShapeNode>,
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
#[cfg(not(feature = "portable"))]
|
|
43
46
|
fn registry() -> &'static RwLock<Registry> {
|
|
44
47
|
static REG: OnceLock<RwLock<Registry>> = OnceLock::new();
|
|
45
48
|
REG.get_or_init(|| {
|
|
@@ -56,6 +59,28 @@ fn registry() -> &'static RwLock<Registry> {
|
|
|
56
59
|
/// object with the same construction path reuses it. Cheap on the hot path (a read-lock + one hashmap
|
|
57
60
|
/// lookup once the edge exists). A `DICT_SHAPE` input (or shape-space exhaustion) stays `DICT_SHAPE`.
|
|
58
61
|
pub fn transition(from: ShapeId, key: &Arc<str>) -> ShapeId {
|
|
62
|
+
// On `portable` (the embedded/GBA link) nothing ever READS a shape id: the only consumers are
|
|
63
|
+
// the bytecode VM (`tish_vm`) and the std runtime's inline caches (`tish_runtime`), and neither
|
|
64
|
+
// is linked there — the GBA facade's `get_prop` goes straight to `PropMap`. Maintaining the
|
|
65
|
+
// registry anyway cost a lock plus an ahash lookup on every new-key insert AND — since this
|
|
66
|
+
// file has no eviction of any kind — a permanent `ShapeNode` plus an `Arc::clone` of the key
|
|
67
|
+
// for every distinct construction path, for the life of the program, on a 136 KB heap.
|
|
68
|
+
// Opt out via the sentinel the file already defines for exactly this case: `DICT_SHAPE` never
|
|
69
|
+
// matches an inline cache, so any future reader degrades to the slow path rather than trusting
|
|
70
|
+
// a slot index that was never recorded.
|
|
71
|
+
#[cfg(feature = "portable")]
|
|
72
|
+
{
|
|
73
|
+
let _ = (from, key);
|
|
74
|
+
DICT_SHAPE
|
|
75
|
+
}
|
|
76
|
+
#[cfg(not(feature = "portable"))]
|
|
77
|
+
{
|
|
78
|
+
transition_tracked(from, key)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[cfg(not(feature = "portable"))]
|
|
83
|
+
fn transition_tracked(from: ShapeId, key: &Arc<str>) -> ShapeId {
|
|
59
84
|
if from == DICT_SHAPE {
|
|
60
85
|
return DICT_SHAPE;
|
|
61
86
|
}
|
|
@@ -1706,6 +1706,12 @@ impl Scanner {
|
|
|
1706
1706
|
self.chars.get(self.i + 1).copied()
|
|
1707
1707
|
}
|
|
1708
1708
|
|
|
1709
|
+
/// True when the char immediately before the cursor is `:`, i.e. the `//` about to
|
|
1710
|
+
/// be scanned is the scheme separator of a URL (`http://`) rather than a comment.
|
|
1711
|
+
fn prev_is_colon(&self) -> bool {
|
|
1712
|
+
self.i > 0 && self.chars.get(self.i - 1) == Some(&':')
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1709
1715
|
/// Consume one char, tracking line/col like the lexer (col resets after `\n`).
|
|
1710
1716
|
fn bump(&mut self) -> Option<char> {
|
|
1711
1717
|
let c = *self.chars.get(self.i)?;
|
|
@@ -1765,7 +1771,23 @@ impl Scanner {
|
|
|
1765
1771
|
self.seen_nonws = true;
|
|
1766
1772
|
self.scan_template();
|
|
1767
1773
|
}
|
|
1768
|
-
|
|
1774
|
+
// `://` is a URL scheme separator, never a comment. This scanner has no JSX
|
|
1775
|
+
// awareness, so JSX TEXT — which, unlike a string or an attribute value, is bare
|
|
1776
|
+
// source — used to have its URLs read as comments:
|
|
1777
|
+
//
|
|
1778
|
+
// <div>Redirect URI: http://127.0.0.1/callback</div>
|
|
1779
|
+
//
|
|
1780
|
+
// scanned `//127.0.0.1/callback</div>` as a line comment, and the printer then
|
|
1781
|
+
// re-emitted it after the element. Worse, the re-emitted text is a REAL comment, so
|
|
1782
|
+
// every subsequent run found one more and the file grew without converging — and
|
|
1783
|
+
// `--check` could never pass on such a file. See tishlang/tish#578.
|
|
1784
|
+
//
|
|
1785
|
+
// Guarding on the preceding `:` fixes the whole class (http://, file://, dune://)
|
|
1786
|
+
// without teaching this scanner to parse JSX, which would mean distinguishing
|
|
1787
|
+
// `<div>` from `a < b` and risks breaking comment scanning in ordinary code.
|
|
1788
|
+
'/' if self.peek2() == Some('/') && !self.prev_is_colon() => {
|
|
1789
|
+
self.scan_line_comment()
|
|
1790
|
+
}
|
|
1769
1791
|
'/' if self.peek2() == Some('*') => self.scan_block_comment(),
|
|
1770
1792
|
' ' | '\t' | '\r' | '\n' => {
|
|
1771
1793
|
self.bump();
|
|
@@ -1884,6 +1906,37 @@ mod tests {
|
|
|
1884
1906
|
let _ = tishlang_parser::parse(&out).unwrap();
|
|
1885
1907
|
}
|
|
1886
1908
|
|
|
1909
|
+
/// tishlang/tish#578 — a URL in JSX TEXT had its `//` scanned as a line comment, so the
|
|
1910
|
+
/// printer re-emitted everything after it as a trailing comment. The re-emitted text was
|
|
1911
|
+
/// itself a real comment, so each run appended another copy and the file never converged.
|
|
1912
|
+
#[test]
|
|
1913
|
+
fn url_in_jsx_text_is_not_a_comment() {
|
|
1914
|
+
let src = "/** @jsxImportSource lattish */\n\nexport fn Demo() {\n return <div>Redirect URI: http://127.0.0.1/callback for details</div>\n}\n";
|
|
1915
|
+
let once = format_source(src).unwrap();
|
|
1916
|
+
// Not `!contains("//127.0.0.1")` — that substring is part of the legitimate URL. The
|
|
1917
|
+
// corruption signature is the text appearing TWICE: once in the JSX, once hoisted out.
|
|
1918
|
+
assert_eq!(
|
|
1919
|
+
once.matches("127.0.0.1").count(),
|
|
1920
|
+
1,
|
|
1921
|
+
"URL text was duplicated into a comment:\n{once}"
|
|
1922
|
+
);
|
|
1923
|
+
// Idempotence is the property that actually broke: formatting must reach a fixed point,
|
|
1924
|
+
// otherwise `--check` can never pass no matter how many times the file is formatted.
|
|
1925
|
+
let twice = format_source(&once).unwrap();
|
|
1926
|
+
assert_eq!(once, twice, "formatting is not idempotent");
|
|
1927
|
+
let thrice = format_source(&twice).unwrap();
|
|
1928
|
+
assert_eq!(twice, thrice, "formatting is not idempotent");
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
/// The guard keys off the `:` immediately before `//`, so ordinary trailing comments —
|
|
1932
|
+
/// including ones right after a colon-bearing construct — must still be recognised.
|
|
1933
|
+
#[test]
|
|
1934
|
+
fn ordinary_comments_still_scanned() {
|
|
1935
|
+
let src = "fn f() {\n let o = { a: 1 } // trailing\n return o\n}\n";
|
|
1936
|
+
let out = format_source(src).unwrap();
|
|
1937
|
+
assert!(out.contains("// trailing"), "comment was dropped:\n{out}");
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1887
1940
|
/// Debug-render the parsed AST with all `Span { … }` contents removed, so two programs compare
|
|
1888
1941
|
/// equal iff they are structurally identical (reformatting legitimately changes spans).
|
|
1889
1942
|
fn structure(src: &str) -> String {
|
|
@@ -18,6 +18,10 @@ const RUNTIME_CARGO_FEATURES: &[&str] = &[
|
|
|
18
18
|
"ws",
|
|
19
19
|
"tty",
|
|
20
20
|
"pty",
|
|
21
|
+
"net",
|
|
22
|
+
"encoding",
|
|
23
|
+
"crypto",
|
|
24
|
+
"zip",
|
|
21
25
|
];
|
|
22
26
|
|
|
23
27
|
/// Map CLI/compile features to flags passed to `tishlang_runtime` in the temp crate's Cargo.toml.
|
|
@@ -145,6 +149,17 @@ pub fn build_via_cargo_with_config(
|
|
|
145
149
|
project_root,
|
|
146
150
|
);
|
|
147
151
|
}
|
|
152
|
+
if build_config.artifact == NativeArtifact::RustLib {
|
|
153
|
+
return emit_rust_lib_crate(
|
|
154
|
+
rust_code,
|
|
155
|
+
native_modules,
|
|
156
|
+
output_path,
|
|
157
|
+
features,
|
|
158
|
+
extra_dependencies_toml,
|
|
159
|
+
generated_native_rs,
|
|
160
|
+
project_root,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
148
163
|
|
|
149
164
|
let out_stem = output_path
|
|
150
165
|
.file_stem()
|
|
@@ -344,6 +359,122 @@ fn read_facade_agb_version(facade_path: &Path) -> Option<String> {
|
|
|
344
359
|
/// Runs its own `cargo build` (NOT `run_cargo_build`) so the `.cargo/config.toml`
|
|
345
360
|
/// rustflags (`-Tgba.ld`, `-Ctarget-cpu=arm7tdmi`) apply — `run_cargo_build` sets a
|
|
346
361
|
/// `RUSTFLAGS` env that would shadow them.
|
|
362
|
+
/// Read `version = "…"` from the `[package]` section of a crate's `Cargo.toml`.
|
|
363
|
+
fn read_crate_version(crate_dir: &Path) -> Option<String> {
|
|
364
|
+
let toml = fs::read_to_string(crate_dir.join("Cargo.toml")).ok()?;
|
|
365
|
+
for line in toml.lines() {
|
|
366
|
+
let line = line.trim();
|
|
367
|
+
if let Some(rest) = line.strip_prefix("version") {
|
|
368
|
+
let rest = rest.trim_start();
|
|
369
|
+
if let Some(rest) = rest.strip_prefix('=') {
|
|
370
|
+
return Some(rest.trim().trim_matches('"').to_string());
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// Only the first (package) table — stop before dependency versions.
|
|
374
|
+
if line.starts_with('[') && !line.starts_with("[package]") {
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
None
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/// `--target rust --emit lib`: write a Cargo **library source crate** to `output_path` and stop.
|
|
382
|
+
///
|
|
383
|
+
/// This is the one artifact that does not run cargo. The consumer decides what to build it for and
|
|
384
|
+
/// how to depend on it, exactly as they would with any other crate — which is the point: it lets a
|
|
385
|
+
/// Tish library be consumed from Rust the way `--target js` lets it be consumed from JS.
|
|
386
|
+
///
|
|
387
|
+
/// The `tishlang_runtime` dependency carries **both** `path` and `version`. Cargo prefers the path
|
|
388
|
+
/// locally and falls back to the registry version when the crate is published, so the emitted crate
|
|
389
|
+
/// is publishable without a post-processing step.
|
|
390
|
+
fn emit_rust_lib_crate(
|
|
391
|
+
rust_code: &str,
|
|
392
|
+
native_modules: Vec<ResolvedNativeModule>,
|
|
393
|
+
output_path: &Path,
|
|
394
|
+
features: &[String],
|
|
395
|
+
extra_dependencies_toml: &str,
|
|
396
|
+
generated_native_rs: Option<&str>,
|
|
397
|
+
project_root: Option<&Path>,
|
|
398
|
+
) -> Result<(), String> {
|
|
399
|
+
let crate_name = tishlang_build_utils::cargo_target_name(
|
|
400
|
+
output_path
|
|
401
|
+
.file_name()
|
|
402
|
+
.and_then(|s| s.to_str())
|
|
403
|
+
.unwrap_or("tish_lib"),
|
|
404
|
+
);
|
|
405
|
+
let runtime_path = tishlang_build_utils::find_runtime_path_for_project(project_root)?;
|
|
406
|
+
let runtime_version = read_crate_version(Path::new(&runtime_path))
|
|
407
|
+
.map(|v| format!(", version = {:?}", v))
|
|
408
|
+
.unwrap_or_default();
|
|
409
|
+
|
|
410
|
+
let runtime_features = runtime_features_for_cargo(features);
|
|
411
|
+
let runtime_refs: Vec<&str> = runtime_features.iter().map(String::as_str).collect();
|
|
412
|
+
let features_str = if runtime_refs.is_empty() {
|
|
413
|
+
String::new()
|
|
414
|
+
} else {
|
|
415
|
+
format!(", features = {:?}", runtime_refs)
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
let native_deps: String = native_modules
|
|
419
|
+
.iter()
|
|
420
|
+
.filter(|m| m.use_path_dependency)
|
|
421
|
+
.map(|m| {
|
|
422
|
+
let path = m.crate_path.display().to_string().replace('\\', "/");
|
|
423
|
+
format!("{} = {{ path = {:?} }}\n", m.package_name, path)
|
|
424
|
+
})
|
|
425
|
+
.collect();
|
|
426
|
+
|
|
427
|
+
let mut more_deps = String::new();
|
|
428
|
+
if !native_deps.is_empty() {
|
|
429
|
+
more_deps.push_str(&format!("\n{}", native_deps));
|
|
430
|
+
}
|
|
431
|
+
if !extra_dependencies_toml.trim().is_empty() {
|
|
432
|
+
more_deps.push_str(&format!("\n{}", extra_dependencies_toml));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
let lib_rs = if generated_native_rs.is_some() {
|
|
436
|
+
inject_generated_native_mod(rust_code)
|
|
437
|
+
} else {
|
|
438
|
+
rust_code.to_string()
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
let cargo_toml = format!(
|
|
442
|
+
r#"[package]
|
|
443
|
+
name = "{name}"
|
|
444
|
+
version = "0.1.0"
|
|
445
|
+
edition = "2021"
|
|
446
|
+
|
|
447
|
+
[lib]
|
|
448
|
+
name = "{name}"
|
|
449
|
+
crate-type = ["rlib"]
|
|
450
|
+
path = "src/lib.rs"
|
|
451
|
+
|
|
452
|
+
[dependencies]
|
|
453
|
+
tishlang_runtime = {{ path = {runtime:?}{version}{features} }}
|
|
454
|
+
{more}"#,
|
|
455
|
+
name = crate_name,
|
|
456
|
+
runtime = runtime_path,
|
|
457
|
+
version = runtime_version,
|
|
458
|
+
features = features_str,
|
|
459
|
+
more = more_deps,
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
let src_dir = output_path.join("src");
|
|
463
|
+
fs::create_dir_all(&src_dir)
|
|
464
|
+
.map_err(|e| format!("Cannot create {}: {}", src_dir.display(), e))?;
|
|
465
|
+
fs::write(output_path.join("Cargo.toml"), cargo_toml)
|
|
466
|
+
.map_err(|e| format!("Cannot write Cargo.toml: {}", e))?;
|
|
467
|
+
if let Some(gen) = generated_native_rs {
|
|
468
|
+
fs::write(src_dir.join("generated_native.rs"), gen)
|
|
469
|
+
.map_err(|e| format!("Cannot write generated_native.rs: {}", e))?;
|
|
470
|
+
}
|
|
471
|
+
fs::write(src_dir.join("lib.rs"), lib_rs)
|
|
472
|
+
.map_err(|e| format!("Cannot write lib.rs: {}", e))?;
|
|
473
|
+
|
|
474
|
+
println!("Emitted Rust library crate: {}", output_path.display());
|
|
475
|
+
Ok(())
|
|
476
|
+
}
|
|
477
|
+
|
|
347
478
|
fn build_gba_rom(
|
|
348
479
|
rust_code: &str,
|
|
349
480
|
native_modules: Vec<ResolvedNativeModule>,
|
|
@@ -11,6 +11,10 @@ pub enum NativeArtifact {
|
|
|
11
11
|
/// A GBA ROM: cargo builds an ELF for `thumbv4t-none-eabi`, then `agb-gbafix`
|
|
12
12
|
/// converts it to a `.gba`.
|
|
13
13
|
GbaRom,
|
|
14
|
+
/// A Rust *library source crate* (`crate-type = ["rlib"]`) whose public API is the entry
|
|
15
|
+
/// module's `export fn`s. Unlike every other artifact, cargo is **not** run — the emitted
|
|
16
|
+
/// crate directory is itself the output, for the consumer to add as a path/registry dependency.
|
|
17
|
+
RustLib,
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
/// Options passed from the CLI into nested `cargo build`.
|
|
@@ -45,6 +49,16 @@ impl NativeBuildConfig {
|
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
/// `tish build --target rust --emit lib`: emit a Cargo library crate and stop. Host target —
|
|
53
|
+
/// the consumer decides what to build it for.
|
|
54
|
+
pub fn rust_lib() -> Self {
|
|
55
|
+
Self {
|
|
56
|
+
artifact: NativeArtifact::RustLib,
|
|
57
|
+
cargo_target: None,
|
|
58
|
+
emit_mode: NativeEmitMode::RustLib,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
pub fn is_cross_compile(&self) -> bool {
|
|
49
63
|
self.cargo_target.is_some()
|
|
50
64
|
}
|
|
@@ -56,6 +56,15 @@ http-hyper = [
|
|
|
56
56
|
# `mkdtemp` needs a random suffix (predictable/timestamp names are a temp-dir security smell).
|
|
57
57
|
fs = ["promise", "dep:rand"]
|
|
58
58
|
process = []
|
|
59
|
+
# Raw TCP sockets: DAP TCP transports, OAuth loopback listener, connect probes.
|
|
60
|
+
net = []
|
|
61
|
+
# Base64 encode/decode (tish:encoding) — bytes <-> string, for file-bytes-over-JSON (extension host
|
|
62
|
+
# vscode.workspace.fs, broker_read_file_base64) and PKCE (URL-safe, no-pad).
|
|
63
|
+
encoding = ["dep:base64"]
|
|
64
|
+
# SHA-256 + secure random (tish:crypto) — PKCE code_challenge/verifier, checkpoint dir hashing.
|
|
65
|
+
crypto = ["dep:sha2", "dep:rand"]
|
|
66
|
+
# Zip extraction (tish:zip) — VSIX / archive extract with `..`-traversal guard + unix-mode preserved.
|
|
67
|
+
zip = ["dep:zip"]
|
|
59
68
|
# Interactive terminal I/O (issue #101): raw mode, key/resize events, size, alt screen.
|
|
60
69
|
tty = ["dep:crossterm"]
|
|
61
70
|
# Pseudoterminal hosting (issue #493): spawn a shell/program on a real PTY, read/write/resize/kill —
|
|
@@ -71,6 +80,7 @@ ws = [
|
|
|
71
80
|
"tokio/net",
|
|
72
81
|
"tokio/io-util",
|
|
73
82
|
"dep:tokio-tungstenite",
|
|
83
|
+
"dep:rustls",
|
|
74
84
|
"dep:futures-util",
|
|
75
85
|
"dep:lazy_static",
|
|
76
86
|
"send-values",
|
|
@@ -84,6 +94,9 @@ tishlang_builtins = { path = "../tish_builtins", version = ">=0.1" }
|
|
|
84
94
|
tokio = { version = "1.50.0", features = ["rt-multi-thread", "macros", "time", "sync"], optional = true }
|
|
85
95
|
reqwest = { version = "0.13.2", default-features = false, features = ["rustls", "json", "stream", "multipart"], optional = true }
|
|
86
96
|
futures = { version = "0.3.32", optional = true }
|
|
97
|
+
base64 = { version = "0.22", optional = true }
|
|
98
|
+
sha2 = { version = "0.10", optional = true }
|
|
99
|
+
zip = { version = "2", default-features = false, features = ["deflate"], optional = true }
|
|
87
100
|
bytes = { version = "1", optional = true }
|
|
88
101
|
tiny_http = { version = "0.12.0", optional = true }
|
|
89
102
|
# fs feature only: random suffix for `mkdtemp`.
|
|
@@ -100,7 +113,13 @@ hyper = { version = "1", default-features = false, features = ["server", "http1"
|
|
|
100
113
|
hyper-util = { version = "0.1", features = ["server", "tokio"], optional = true }
|
|
101
114
|
http-body-util = { version = "0.1", optional = true }
|
|
102
115
|
core_affinity = { version = "0.8", optional = true }
|
|
103
|
-
|
|
116
|
+
# rustls-tls-native-roots gives `wss://` support (system cert store, no OpenSSL — matches reqwest's
|
|
117
|
+
# rustls backend). Without a TLS feature, connect_async can only do plain `ws://` and rejects wss.
|
|
118
|
+
tokio-tungstenite = { version = "0.29", optional = true, features = ["rustls-tls-native-roots"] }
|
|
119
|
+
# Direct rustls handle so ws.rs can install a process-default CryptoProvider: both aws-lc-rs and ring
|
|
120
|
+
# are in the tree (reqwest + transitive), so rustls 0.23 can't auto-select one, and connect_async's
|
|
121
|
+
# wss path needs the process default. aws_lc_rs matches reqwest's provider.
|
|
122
|
+
rustls = { version = "0.23", default-features = false, features = ["aws_lc_rs"], optional = true }
|
|
104
123
|
futures-util = { version = "0.3", optional = true }
|
|
105
124
|
lazy_static = { version = "1", optional = true }
|
|
106
125
|
|