@tishlang/tish-lsp 3.1.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-lsp +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 +304 -3
- package/crates/tish_compile/src/lib.rs +14 -1
- 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_fmt/src/lib.rs +54 -1
- package/crates/tish_lsp/Cargo.toml +1 -1
- package/crates/tish_native/src/build.rs +127 -0
- package/crates/tish_native/src/config.rs +14 -0
- package/crates/tish_runtime_gba/src/lib.rs +17 -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
package/bin/tish-lsp
CHANGED
|
Binary file
|
|
@@ -404,6 +404,10 @@ pub fn build_after_help() -> String {
|
|
|
404
404
|
Native executable (see --native-backend)
|
|
405
405
|
{t}js{r}
|
|
406
406
|
JavaScript bundle
|
|
407
|
+
{t}rust-lib{r}
|
|
408
|
+
Rust library source crate (crate-type = \"rlib\") at `-o <dir>` — one `pub fn` per
|
|
409
|
+
`export fn` of the entry module, over `tishlang_runtime::Value`. Cargo is NOT run; the
|
|
410
|
+
crate is the output, for the consumer to depend on. The Rust counterpart of `--target js`
|
|
407
411
|
{t}wasm{r}
|
|
408
412
|
WebAssembly (.tish project; .js source supported on some paths)
|
|
409
413
|
{t}wasm-gpu{r}
|
package/crates/tish/src/main.rs
CHANGED
|
@@ -914,9 +914,9 @@ fn build_file(
|
|
|
914
914
|
.map_err(|e| e.to_string());
|
|
915
915
|
}
|
|
916
916
|
|
|
917
|
-
if target != "native" && target != "gba" {
|
|
917
|
+
if target != "native" && target != "gba" && target != "rust-lib" {
|
|
918
918
|
return Err(format!(
|
|
919
|
-
"Unknown target: {}. Use 'native', 'gba', 'js', 'wasm', 'wasm-gpu', 'wasi', or 'bytecode'.",
|
|
919
|
+
"Unknown target: {}. Use 'native', 'gba', 'rust-lib', 'js', 'wasm', 'wasm-gpu', 'wasi', or 'bytecode'.",
|
|
920
920
|
target
|
|
921
921
|
));
|
|
922
922
|
}
|
|
@@ -935,6 +935,11 @@ fn build_file(
|
|
|
935
935
|
return Err("tish build --target gba requires a .tish entry file.".to_string());
|
|
936
936
|
}
|
|
937
937
|
tishlang_native::NativeBuildConfig::gba()
|
|
938
|
+
} else if target == "rust-lib" {
|
|
939
|
+
if is_js {
|
|
940
|
+
return Err("tish build --target rust-lib requires a .tish entry file.".to_string());
|
|
941
|
+
}
|
|
942
|
+
tishlang_native::NativeBuildConfig::rust_lib()
|
|
938
943
|
} else if let Some(triple) = ios_triple {
|
|
939
944
|
tishlang_native::NativeBuildConfig::ios_staticlib(triple)
|
|
940
945
|
} else if crate_type == "staticlib" {
|
|
@@ -982,6 +987,12 @@ fn build_file(
|
|
|
982
987
|
.map_err(|e| e.to_string())?;
|
|
983
988
|
}
|
|
984
989
|
|
|
990
|
+
// `rust-lib` writes a crate DIRECTORY at `-o`, and already reported it — the artifact-path
|
|
991
|
+
// rewriting below is all about locating a single built file, which does not apply.
|
|
992
|
+
if build_config.artifact == tishlang_native::NativeArtifact::RustLib {
|
|
993
|
+
return Ok(());
|
|
994
|
+
}
|
|
995
|
+
|
|
985
996
|
let out_name = Path::new(output_path)
|
|
986
997
|
.file_stem()
|
|
987
998
|
.and_then(|s| s.to_str())
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
//! `tish build --target rust-lib` end to end: the emitted artifact is a Cargo **source crate**, and
|
|
2
|
+
//! cargo is deliberately not run — the consumer decides what to build it for.
|
|
3
|
+
//!
|
|
4
|
+
//! The fast test pins the crate layout. The behavioural test — module state surviving between
|
|
5
|
+
//! `pub fn` calls, a `clear*()` reassignment being visible to other exports, and the module body
|
|
6
|
+
//! running exactly once — actually builds the crate and a consumer, so it is gated behind
|
|
7
|
+
//! `TISH_SLOW_TESTS=1` to keep the default `cargo test` fast.
|
|
8
|
+
use std::path::{Path, PathBuf};
|
|
9
|
+
|
|
10
|
+
fn fixture() -> PathBuf {
|
|
11
|
+
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
12
|
+
.join("../../tests/regression/rust_lib_emit.tish")
|
|
13
|
+
.canonicalize()
|
|
14
|
+
.unwrap()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fn emit_crate(out: &Path) {
|
|
18
|
+
let entry = fixture();
|
|
19
|
+
tishlang_native::compile_to_native_with_config(
|
|
20
|
+
&entry,
|
|
21
|
+
entry.parent(),
|
|
22
|
+
out,
|
|
23
|
+
&[],
|
|
24
|
+
"rust",
|
|
25
|
+
true,
|
|
26
|
+
&tishlang_native::NativeBuildConfig::rust_lib(),
|
|
27
|
+
)
|
|
28
|
+
.expect("rust-lib emit");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[test]
|
|
32
|
+
fn emits_an_rlib_source_crate_and_does_not_run_cargo() {
|
|
33
|
+
let out = std::env::temp_dir().join("tish_rustlib_layout");
|
|
34
|
+
let _ = std::fs::remove_dir_all(&out);
|
|
35
|
+
emit_crate(&out);
|
|
36
|
+
|
|
37
|
+
let manifest = std::fs::read_to_string(out.join("Cargo.toml")).expect("Cargo.toml");
|
|
38
|
+
assert!(
|
|
39
|
+
manifest.contains(r#"crate-type = ["rlib"]"#),
|
|
40
|
+
"expected an rlib library crate:\n{manifest}"
|
|
41
|
+
);
|
|
42
|
+
assert!(
|
|
43
|
+
manifest.contains("path = \"src/lib.rs\""),
|
|
44
|
+
"expected a lib entry point:\n{manifest}"
|
|
45
|
+
);
|
|
46
|
+
// Both `path` and `version` on the runtime dep: cargo prefers the path locally and falls back to
|
|
47
|
+
// the registry when the emitted crate is published, so it is publishable as-is.
|
|
48
|
+
assert!(
|
|
49
|
+
manifest.contains("tishlang_runtime = { path =") && manifest.contains("version = \""),
|
|
50
|
+
"the runtime dep must carry both path and version:\n{manifest}"
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
assert!(out.join("src/lib.rs").is_file(), "expected src/lib.rs");
|
|
54
|
+
assert!(
|
|
55
|
+
!out.join("src/main.rs").exists(),
|
|
56
|
+
"a library crate must not emit src/main.rs"
|
|
57
|
+
);
|
|
58
|
+
// Cargo was not invoked: no build output next to the emitted crate.
|
|
59
|
+
assert!(
|
|
60
|
+
!out.join("target").exists(),
|
|
61
|
+
"--target rust-lib must emit the crate and stop, not build it"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// `TISH_SLOW_TESTS=1 cargo test -p tishlang --test rust_lib_crate` — compiles the emitted crate
|
|
66
|
+
/// plus a consumer that exercises the module-state guarantees.
|
|
67
|
+
#[test]
|
|
68
|
+
fn consumer_observes_persistent_module_state() {
|
|
69
|
+
if std::env::var("TISH_SLOW_TESTS").as_deref() != Ok("1") {
|
|
70
|
+
eprintln!("skipped (set TISH_SLOW_TESTS=1 to run the full cargo build)");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
let root = std::env::temp_dir().join("tish_rustlib_e2e");
|
|
74
|
+
let _ = std::fs::remove_dir_all(&root);
|
|
75
|
+
let lib = root.join("lib");
|
|
76
|
+
emit_crate(&lib);
|
|
77
|
+
|
|
78
|
+
let consumer = root.join("consumer");
|
|
79
|
+
std::fs::create_dir_all(consumer.join("src")).unwrap();
|
|
80
|
+
std::fs::write(
|
|
81
|
+
consumer.join("Cargo.toml"),
|
|
82
|
+
format!(
|
|
83
|
+
"[package]\nname = \"consumer\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n\
|
|
84
|
+
[workspace]\n\n[dependencies]\nlib = {{ package = \"lib\", path = {:?} }}\n",
|
|
85
|
+
lib.display().to_string()
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
.unwrap();
|
|
89
|
+
std::fs::write(
|
|
90
|
+
consumer.join("src/main.rs"),
|
|
91
|
+
r#"
|
|
92
|
+
use lib::Value;
|
|
93
|
+
fn num(v: &Value) -> f64 { match v { Value::Number(x) => *x, _ => f64::NAN } }
|
|
94
|
+
fn text(v: &Value) -> String { match v { Value::String(x) => x.to_string(), _ => String::new() } }
|
|
95
|
+
fn main() {
|
|
96
|
+
// First call is not the first declared export: init must already have run.
|
|
97
|
+
assert_eq!(num(&lib::add(Value::Number(2.0), Value::Number(40.0))), 42.0, "add");
|
|
98
|
+
assert_eq!(num(&lib::initRuns()), 1.0, "module top level runs once");
|
|
99
|
+
|
|
100
|
+
lib::register(Value::String("kick".into()), Value::String("noiseBurst".into()));
|
|
101
|
+
assert_eq!(text(&lib::lookup(Value::String("kick".into()))), "noiseBurst", "state persists");
|
|
102
|
+
|
|
103
|
+
// `clearAll` REASSIGNS the module-level binding; other exports must see that.
|
|
104
|
+
assert_eq!(num(&lib::clearAll()), 0.0, "clearAll empties");
|
|
105
|
+
assert_eq!(text(&lib::lookup(Value::String("kick".into()))), "miss", "reassignment visible");
|
|
106
|
+
|
|
107
|
+
assert_eq!(num(&lib::initRuns()), 1.0, "still initialised once");
|
|
108
|
+
|
|
109
|
+
// A throw is parked for the next checkpoint, not unwound. Across the `pub fn` boundary there is
|
|
110
|
+
// no Tish frame to surface it in, so a parked throw used to sit latched and make every LATER
|
|
111
|
+
// call bail and return null. Reading a property off a null is all it takes.
|
|
112
|
+
let _ = lib::runtime::get_index(&Value::Null, &Value::String("nope".into()));
|
|
113
|
+
assert_eq!(
|
|
114
|
+
num(&lib::add(Value::Number(1.0), Value::Number(2.0))),
|
|
115
|
+
3.0,
|
|
116
|
+
"a parked throw must not brick the module"
|
|
117
|
+
);
|
|
118
|
+
for _ in 0..20 {
|
|
119
|
+
let _ = lib::runtime::get_index(&Value::Null, &Value::String("nope".into()));
|
|
120
|
+
}
|
|
121
|
+
assert_eq!(text(&lib::lookup(Value::String("kick".into()))), "miss", "still working after 20");
|
|
122
|
+
|
|
123
|
+
println!("ALL_OK");
|
|
124
|
+
}
|
|
125
|
+
"#,
|
|
126
|
+
)
|
|
127
|
+
.unwrap();
|
|
128
|
+
|
|
129
|
+
let out = std::process::Command::new("cargo")
|
|
130
|
+
.args(["run", "-q"])
|
|
131
|
+
.current_dir(&consumer)
|
|
132
|
+
.output()
|
|
133
|
+
.expect("cargo run");
|
|
134
|
+
assert!(
|
|
135
|
+
String::from_utf8_lossy(&out.stdout).contains("ALL_OK"),
|
|
136
|
+
"consumer failed:\nstdout: {}\nstderr: {}",
|
|
137
|
+
String::from_utf8_lossy(&out.stdout),
|
|
138
|
+
String::from_utf8_lossy(&out.stderr)
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -652,7 +652,7 @@ pub fn compile_project_full_emit(
|
|
|
652
652
|
if let Some(cap) = feature_cap {
|
|
653
653
|
all_features.retain(|f| cap.contains(f));
|
|
654
654
|
}
|
|
655
|
-
let rust =
|
|
655
|
+
let rust = compile_with_native_modules_emit_exports(
|
|
656
656
|
&merged.program,
|
|
657
657
|
project_root,
|
|
658
658
|
&all_features,
|
|
@@ -660,6 +660,7 @@ pub fn compile_project_full_emit(
|
|
|
660
660
|
&native_build.native_init,
|
|
661
661
|
optimize,
|
|
662
662
|
emit_mode,
|
|
663
|
+
&merged.entry_exports,
|
|
663
664
|
)?;
|
|
664
665
|
Ok((rust, native_modules, all_features, native_build))
|
|
665
666
|
}
|
|
@@ -728,6 +729,16 @@ fn run_type_check(program: &Program) -> Result<(), CompileError> {
|
|
|
728
729
|
Ok(())
|
|
729
730
|
}
|
|
730
731
|
|
|
732
|
+
/// One entry-module export, as the `RustLib` surface sees it.
|
|
733
|
+
struct RustLibExport {
|
|
734
|
+
/// The Rust local `run()` binds — for a re-export, the dependency's binding.
|
|
735
|
+
local_name: String,
|
|
736
|
+
/// The name the crate exposes (and the key in the export table).
|
|
737
|
+
exported_name: String,
|
|
738
|
+
/// Parameter count when the export names a `fn`; `None` for a value export.
|
|
739
|
+
arity: Option<usize>,
|
|
740
|
+
}
|
|
741
|
+
|
|
731
742
|
pub fn compile_with_native_modules_emit(
|
|
732
743
|
program: &Program,
|
|
733
744
|
project_root: Option<&Path>,
|
|
@@ -736,6 +747,33 @@ pub fn compile_with_native_modules_emit(
|
|
|
736
747
|
native_init: &std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
|
|
737
748
|
optimize: bool,
|
|
738
749
|
emit_mode: crate::NativeEmitMode,
|
|
750
|
+
) -> Result<String, CompileError> {
|
|
751
|
+
compile_with_native_modules_emit_exports(
|
|
752
|
+
program,
|
|
753
|
+
project_root,
|
|
754
|
+
features,
|
|
755
|
+
native_modules,
|
|
756
|
+
native_init,
|
|
757
|
+
optimize,
|
|
758
|
+
emit_mode,
|
|
759
|
+
&[],
|
|
760
|
+
)
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/// Like [`compile_with_native_modules_emit`], plus the entry module's exported
|
|
764
|
+
/// `(local_name, exported_name)` pairs. Only [`crate::NativeEmitMode::RustLib`] reads them — they
|
|
765
|
+
/// become the emitted crate's `pub fn` surface. Every other mode ignores the argument, which is
|
|
766
|
+
/// why the plain entry point above can pass `&[]`.
|
|
767
|
+
#[allow(clippy::too_many_arguments)]
|
|
768
|
+
pub fn compile_with_native_modules_emit_exports(
|
|
769
|
+
program: &Program,
|
|
770
|
+
project_root: Option<&Path>,
|
|
771
|
+
features: &[String],
|
|
772
|
+
native_modules: &[crate::resolve::ResolvedNativeModule],
|
|
773
|
+
native_init: &std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
|
|
774
|
+
optimize: bool,
|
|
775
|
+
emit_mode: crate::NativeEmitMode,
|
|
776
|
+
entry_exports: &[(String, String)],
|
|
739
777
|
) -> Result<String, CompileError> {
|
|
740
778
|
// Per-compile thread-local state — the narrow-int/`fixed`/`i32` numeric profile and the import-
|
|
741
779
|
// scheme registry — must not outlive THIS compile. Both persist on the thread, so a later compile
|
|
@@ -786,6 +824,7 @@ pub fn compile_with_native_modules_emit(
|
|
|
786
824
|
};
|
|
787
825
|
let mut g = Codegen::new_with_native_modules(project_root, features, map);
|
|
788
826
|
g.emit_mode = emit_mode;
|
|
827
|
+
g.entry_exports = entry_exports.to_vec();
|
|
789
828
|
g.has_native_ui_host = native_modules.iter().any(|m| {
|
|
790
829
|
m.package_name == "tish-macos"
|
|
791
830
|
|| m.package_name == "tish-ios"
|
|
@@ -1245,6 +1284,10 @@ pub(crate) struct Codegen {
|
|
|
1245
1284
|
/// closures it must not return a `Result` from a `Value`-returning closure.
|
|
1246
1285
|
value_fn_depth: u32,
|
|
1247
1286
|
emit_mode: crate::NativeEmitMode,
|
|
1287
|
+
/// `RustLib` only: the entry module's `(local_name, exported_name)` pairs, as computed by
|
|
1288
|
+
/// `merge_modules` for the JS bundle's trailing `export { … }`. Drives the `pub fn` wrappers.
|
|
1289
|
+
/// Empty for every other emit mode.
|
|
1290
|
+
entry_exports: Vec<(String, String)>,
|
|
1248
1291
|
/// Scheme imports as `(scheme_name, absolute_path)`, in first-seen order. Populated (Gba mode)
|
|
1249
1292
|
/// before the entry is emitted from the active [`crate::schemes`] registry; drives the
|
|
1250
1293
|
/// `mod __scheme_<name>_<j> { … }` blocks and their per-scheme registration. The handle a tish
|
|
@@ -1363,6 +1406,7 @@ impl Codegen {
|
|
|
1363
1406
|
program_fun_decl_names: std::collections::HashSet::new(),
|
|
1364
1407
|
value_fn_depth: 0,
|
|
1365
1408
|
emit_mode: crate::NativeEmitMode::DesktopBin,
|
|
1409
|
+
entry_exports: Vec::new(),
|
|
1366
1410
|
scheme_files: Vec::new(),
|
|
1367
1411
|
has_native_ui_host: false,
|
|
1368
1412
|
program_uses_document: false,
|
|
@@ -2939,6 +2983,14 @@ impl Codegen {
|
|
|
2939
2983
|
self.write("use std::rc::Rc;\n");
|
|
2940
2984
|
self.write("use std::sync::Arc;\n");
|
|
2941
2985
|
}
|
|
2986
|
+
// RustLib: re-export the runtime prelude rather than importing it privately, so the crate's
|
|
2987
|
+
// public API is self-contained — a consumer gets `Value` (and `ObjectMap` / `PropMap` /
|
|
2988
|
+
// `VmRef`) from THIS crate and cannot version-skew them against a separate `tishlang_runtime`
|
|
2989
|
+
// dependency of their own. Also avoids a duplicate-import error, since `Value` is already in
|
|
2990
|
+
// this list and could not be re-exported a second time at the crate root.
|
|
2991
|
+
if self.emit_mode == crate::NativeEmitMode::RustLib {
|
|
2992
|
+
self.write("pub ");
|
|
2993
|
+
}
|
|
2942
2994
|
self.write("use tishlang_runtime::{console_debug as tish_console_debug, console_info as tish_console_info, console_log as tish_console_log, console_warn as tish_console_warn, console_error as tish_console_error, boolean as tish_boolean, decode_uri as tish_decode_uri, encode_uri as tish_encode_uri, encode_uri_component as tish_encode_uri_component, decode_uri_component as tish_decode_uri_component, string_escape_html_impl as tish_escape_html, in_operator as tish_in_operator, is_finite as tish_is_finite, is_nan as tish_is_nan, json_parse as tish_json_parse, json_stringify as tish_json_stringify, math_abs as tish_math_abs, math_ceil as tish_math_ceil, math_floor as tish_math_floor, math_max as tish_math_max, math_min as tish_math_min, math_round as tish_math_round, math_sqrt as tish_math_sqrt, parse_float as tish_parse_float, parse_int as tish_parse_int, math_random as tish_math_random, math_pow as tish_math_pow, math_sin as tish_math_sin, math_cos as tish_math_cos, math_tan as tish_math_tan, math_log as tish_math_log, math_exp as tish_math_exp, math_sign as tish_math_sign, math_trunc as tish_math_trunc, math_imul as tish_math_imul, math_sinh as tish_math_sinh, math_cosh as tish_math_cosh, math_tanh as tish_math_tanh, math_asinh as tish_math_asinh, math_acosh as tish_math_acosh, math_atanh as tish_math_atanh, math_cbrt as tish_math_cbrt, math_log2 as tish_math_log2, math_log10 as tish_math_log10, math_expm1 as tish_math_expm1, math_log1p as tish_math_log1p, math_clz32 as tish_math_clz32, math_fround as tish_math_fround, math_hypot as tish_math_hypot, math_atan2 as tish_math_atan2, math_asin as tish_math_asin, math_acos as tish_math_acos, math_atan as tish_math_atan, array_is_array as tish_array_is_array, array_of as tish_array_of, array_from as tish_array_from, object_is as tish_object_is, object_has_own as tish_object_has_own, structured_clone as tish_structured_clone, array_construct as tish_array_construct, string_from_char_code as tish_string_from_char_code, string_convert as tish_string_convert, number_convert as tish_number_convert, number_is_integer as tish_number_is_integer, number_is_safe_integer as tish_number_is_safe_integer, number_is_nan as tish_number_is_nan, number_is_finite as tish_number_is_finite, object_assign as tish_object_assign, object_freeze as tish_object_freeze, object_is_frozen as tish_object_is_frozen, object_keys as tish_object_keys, object_values as tish_object_values, object_entries as tish_object_entries, object_from_entries as tish_object_from_entries, symbol_object as tish_symbol_object, tish_construct, tish_error_constructor, tish_date_constructor, tish_set_constructor, tish_map_constructor, map_get as tish_map_get, map_has as tish_map_has, map_set as tish_map_set, map_values as tish_map_values, tish_float64_array_constructor, tish_float32_array_constructor, tish_int8_array_constructor, tish_uint8_array_constructor, tish_uint8_clamped_array_constructor, tish_int16_array_constructor, tish_uint16_array_constructor, tish_int32_array_constructor, tish_uint32_array_constructor, tish_audio_context_constructor, ObjectMap, ObjectData, PropMap, TishError, Value, VmRef};\n");
|
|
2943
2995
|
if self.program_has_jsx {
|
|
2944
2996
|
self.write("use tishlang_ui::{fragment_value, install_thread_local_host, native_create_root, native_use_state, ui_h, ui_text, HeadlessHost};\n");
|
|
@@ -3054,6 +3106,9 @@ impl Codegen {
|
|
|
3054
3106
|
self.writeln("}");
|
|
3055
3107
|
self.writeln("");
|
|
3056
3108
|
}
|
|
3109
|
+
if self.emit_mode == crate::NativeEmitMode::RustLib {
|
|
3110
|
+
self.emit_rust_lib_surface(program)?;
|
|
3111
|
+
}
|
|
3057
3112
|
// M5 (default-on via native_opts_enabled; TISH_NATIVE_OPT=0 opts out): emit a parallel
|
|
3058
3113
|
// native `fn f_native` for each eligible top-level numeric fn at top level; direct calls
|
|
3059
3114
|
// route to it in emit_typed_expr.
|
|
@@ -3552,6 +3607,29 @@ impl Codegen {
|
|
|
3552
3607
|
self.writeln("tishlang_runtime::drain_timers();");
|
|
3553
3608
|
}
|
|
3554
3609
|
|
|
3610
|
+
// RustLib: hand the entry module's exported bindings to the thread-local table before
|
|
3611
|
+
// `run()`'s frame drops. The bindings are ordinary `Value` locals; cloning one clones an
|
|
3612
|
+
// `Rc`, so the closure — and every module-level cell it captured — stays alive and shared.
|
|
3613
|
+
// That is what makes module state survive between `pub fn` calls (and stay mutable through
|
|
3614
|
+
// `clear*`-style reassignment, since captures are `VmRef` cells, not copies).
|
|
3615
|
+
if self.emit_mode == crate::NativeEmitMode::RustLib {
|
|
3616
|
+
let pairs: Vec<String> = self
|
|
3617
|
+
.rust_lib_exports(program)
|
|
3618
|
+
.iter()
|
|
3619
|
+
.map(|e| {
|
|
3620
|
+
format!(
|
|
3621
|
+
"({:?}, {}.clone())",
|
|
3622
|
+
e.exported_name,
|
|
3623
|
+
Self::escape_ident(&e.local_name)
|
|
3624
|
+
)
|
|
3625
|
+
})
|
|
3626
|
+
.collect();
|
|
3627
|
+
self.writeln(&format!(
|
|
3628
|
+
"__tish_publish_exports(::std::vec![{}]);",
|
|
3629
|
+
pairs.join(", ")
|
|
3630
|
+
));
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3555
3633
|
self.writeln("Ok(())");
|
|
3556
3634
|
self.indent -= 1;
|
|
3557
3635
|
self.writeln("}");
|
|
@@ -3580,6 +3658,191 @@ impl Codegen {
|
|
|
3580
3658
|
Ok(())
|
|
3581
3659
|
}
|
|
3582
3660
|
|
|
3661
|
+
/// The entry module's exports, paired with the arity of the `fn` each names (if it names one).
|
|
3662
|
+
/// `merge_modules` unwraps `export` into plain top-level declarations and records the
|
|
3663
|
+
/// `(local, exported)` pairs — including through `export { … } from './x.tish'` re-exports,
|
|
3664
|
+
/// which resolve to the dependency's local binding — so a barrel module still yields the full
|
|
3665
|
+
/// surface. Sorted for deterministic output.
|
|
3666
|
+
fn rust_lib_exports(&self, program: &Program) -> Vec<RustLibExport> {
|
|
3667
|
+
let mut arities: std::collections::HashMap<&str, usize> =
|
|
3668
|
+
std::collections::HashMap::new();
|
|
3669
|
+
for stmt in &program.statements {
|
|
3670
|
+
if let Statement::FunDecl { name, params, .. } = stmt {
|
|
3671
|
+
arities.insert(name.as_ref(), params.len());
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
let mut out: Vec<RustLibExport> = self
|
|
3675
|
+
.entry_exports
|
|
3676
|
+
.iter()
|
|
3677
|
+
.map(|(local, exported)| RustLibExport {
|
|
3678
|
+
local_name: local.clone(),
|
|
3679
|
+
exported_name: exported.clone(),
|
|
3680
|
+
arity: arities.get(local.as_str()).copied(),
|
|
3681
|
+
})
|
|
3682
|
+
.collect();
|
|
3683
|
+
out.sort_by(|a, b| a.exported_name.cmp(&b.exported_name));
|
|
3684
|
+
out
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
/// Emit the `RustLib` public surface: the thread-local module instance, the lazy `ensure_init`,
|
|
3688
|
+
/// and one `pub fn` per entry export.
|
|
3689
|
+
///
|
|
3690
|
+
/// The module body itself is the ordinary `run()` — this only changes *when* it runs and what
|
|
3691
|
+
/// outlives it. Storage is `thread_local!` rather than a global because `Value` is `!Send`
|
|
3692
|
+
/// unless the `send-values` feature is on (it follows `http`), so a `static` would not compile
|
|
3693
|
+
/// for the common configuration. Per-thread instances also match how a consumer uses this:
|
|
3694
|
+
/// register into the module's registries, then call it, on one thread.
|
|
3695
|
+
fn emit_rust_lib_surface(&mut self, program: &Program) -> Result<(), CompileError> {
|
|
3696
|
+
if self.is_async {
|
|
3697
|
+
return Err(CompileError::new(
|
|
3698
|
+
"--target rust --emit lib: the entry module has top-level `await`. A library has \
|
|
3699
|
+
no runtime to drive it — move the async work into an exported function."
|
|
3700
|
+
.to_string(),
|
|
3701
|
+
None,
|
|
3702
|
+
));
|
|
3703
|
+
}
|
|
3704
|
+
let exports = self.rust_lib_exports(program);
|
|
3705
|
+
if exports.is_empty() {
|
|
3706
|
+
return Err(CompileError::new(
|
|
3707
|
+
"--target rust --emit lib: the entry module exports nothing, so the crate would \
|
|
3708
|
+
have no public API. Add `export fn …` to the entry module."
|
|
3709
|
+
.to_string(),
|
|
3710
|
+
None,
|
|
3711
|
+
));
|
|
3712
|
+
}
|
|
3713
|
+
// Only `export fn` is supported. A value export has no arity, and its Rust local is not
|
|
3714
|
+
// necessarily a `Value` to publish (a `const` string lowers to `&str`), so it cannot be
|
|
3715
|
+
// handed to the export table the way a closure can. Say so instead of emitting a getter
|
|
3716
|
+
// that would always read back null.
|
|
3717
|
+
let non_fn: Vec<&str> = exports
|
|
3718
|
+
.iter()
|
|
3719
|
+
.filter(|e| e.arity.is_none())
|
|
3720
|
+
.map(|e| e.exported_name.as_str())
|
|
3721
|
+
.collect();
|
|
3722
|
+
if !non_fn.is_empty() {
|
|
3723
|
+
return Err(CompileError::new(
|
|
3724
|
+
format!(
|
|
3725
|
+
"--target rust --emit lib: only `export fn` can cross the crate boundary, but \
|
|
3726
|
+
the entry module also exports the value(s) `{}`. Wrap each in an exported \
|
|
3727
|
+
function (`export fn {}() {{ … }}`).",
|
|
3728
|
+
non_fn.join("`, `"),
|
|
3729
|
+
non_fn[0]
|
|
3730
|
+
),
|
|
3731
|
+
None,
|
|
3732
|
+
));
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
self.writeln("// ── Tish library surface ──────────────────────────────────────────────");
|
|
3736
|
+
// `Value` and friends are already re-exported by the (pub) runtime prelude above; this adds
|
|
3737
|
+
// the whole runtime for consumers that need more than the prelude carries.
|
|
3738
|
+
self.writeln("pub use tishlang_runtime as runtime;");
|
|
3739
|
+
self.writeln("");
|
|
3740
|
+
self.writeln("thread_local! {");
|
|
3741
|
+
self.indent += 1;
|
|
3742
|
+
self.writeln(
|
|
3743
|
+
"static __TISH_MODULE: ::std::cell::RefCell<::std::option::Option<::std::collections::HashMap<&'static str, Value>>> =",
|
|
3744
|
+
);
|
|
3745
|
+
self.writeln(" ::std::cell::RefCell::new(::std::option::Option::None);");
|
|
3746
|
+
self.indent -= 1;
|
|
3747
|
+
self.writeln("}");
|
|
3748
|
+
self.writeln("");
|
|
3749
|
+
self.writeln("/// Called once from the tail of `run()` with the entry module's exports.");
|
|
3750
|
+
self.writeln("fn __tish_publish_exports(pairs: ::std::vec::Vec<(&'static str, Value)>) {");
|
|
3751
|
+
self.indent += 1;
|
|
3752
|
+
self.writeln("__TISH_MODULE.with(|m| {");
|
|
3753
|
+
self.writeln(" *m.borrow_mut() = ::std::option::Option::Some(pairs.into_iter().collect());");
|
|
3754
|
+
self.writeln("});");
|
|
3755
|
+
self.indent -= 1;
|
|
3756
|
+
self.writeln("}");
|
|
3757
|
+
self.writeln("");
|
|
3758
|
+
self.writeln("/// Run the module body exactly once per thread, before the first export call.");
|
|
3759
|
+
self.writeln("fn __tish_ensure_init() {");
|
|
3760
|
+
self.indent += 1;
|
|
3761
|
+
self.writeln("// Park an empty table BEFORE running the body: that makes init non-reentrant");
|
|
3762
|
+
self.writeln("// (an export reached while the module is still initialising sees an empty");
|
|
3763
|
+
self.writeln("// table and returns null, rather than recursing forever).");
|
|
3764
|
+
self.writeln("let fresh = __TISH_MODULE.with(|m| {");
|
|
3765
|
+
self.writeln(" let mut b = m.borrow_mut();");
|
|
3766
|
+
self.writeln(" if b.is_some() { return false; }");
|
|
3767
|
+
self.writeln(" *b = ::std::option::Option::Some(::std::collections::HashMap::new());");
|
|
3768
|
+
self.writeln(" true");
|
|
3769
|
+
self.writeln("});");
|
|
3770
|
+
self.writeln("if fresh {");
|
|
3771
|
+
self.writeln(" // A module whose top level fails cannot be recovered from here: every");
|
|
3772
|
+
self.writeln(" // export would observe half-initialised state.");
|
|
3773
|
+
self.writeln(" run().unwrap_or_else(|e| panic!(\"tish module initialisation failed: {}\", e));");
|
|
3774
|
+
self.writeln("}");
|
|
3775
|
+
self.indent -= 1;
|
|
3776
|
+
self.writeln("}");
|
|
3777
|
+
self.writeln("");
|
|
3778
|
+
self.writeln("/// Clone an export out of the table. The borrow is released before the value is");
|
|
3779
|
+
self.writeln("/// used, because an export is free to call back into another export.");
|
|
3780
|
+
self.writeln("fn __tish_export(name: &str) -> Value {");
|
|
3781
|
+
self.indent += 1;
|
|
3782
|
+
self.writeln("__tish_ensure_init();");
|
|
3783
|
+
self.writeln("__TISH_MODULE.with(|m| {");
|
|
3784
|
+
self.writeln(" m.borrow().as_ref().and_then(|t| t.get(name).cloned()).unwrap_or(Value::Null)");
|
|
3785
|
+
self.writeln("})");
|
|
3786
|
+
self.indent -= 1;
|
|
3787
|
+
self.writeln("}");
|
|
3788
|
+
self.writeln("");
|
|
3789
|
+
self.writeln("thread_local! {");
|
|
3790
|
+
self.writeln(" static __TISH_LAST_THROW: ::std::cell::RefCell<Value> = ::std::cell::RefCell::new(Value::Null);");
|
|
3791
|
+
self.writeln("}");
|
|
3792
|
+
self.writeln("");
|
|
3793
|
+
self.writeln("/// The value thrown by the most recent export call, or null if it completed normally.");
|
|
3794
|
+
self.writeln("///");
|
|
3795
|
+
self.writeln("/// A `pub fn … -> Value` has no error channel, so a throw that escapes an export is drained");
|
|
3796
|
+
self.writeln("/// and recorded here rather than left parked. Reading it is the way to tell \"returned null\"");
|
|
3797
|
+
self.writeln("/// from \"threw\".");
|
|
3798
|
+
self.writeln("pub fn tish_last_throw() -> Value {");
|
|
3799
|
+
self.indent += 1;
|
|
3800
|
+
self.writeln("__TISH_LAST_THROW.with(|t| t.borrow().clone())");
|
|
3801
|
+
self.indent -= 1;
|
|
3802
|
+
self.writeln("}");
|
|
3803
|
+
self.writeln("");
|
|
3804
|
+
self.writeln("fn __tish_call_export(name: &str, args: &[Value]) -> Value {");
|
|
3805
|
+
self.indent += 1;
|
|
3806
|
+
self.writeln("// A throw is PARKED for the next checkpoint rather than unwound (#303). Across a `pub fn`");
|
|
3807
|
+
self.writeln("// boundary there is no Tish frame to surface it in, so a parked throw would sit latched and");
|
|
3808
|
+
self.writeln("// every LATER call would bail at its own checkpoint and silently return null — one bad call");
|
|
3809
|
+
self.writeln("// bricking the module for the rest of the process. A host hits this simply by reading a");
|
|
3810
|
+
self.writeln("// property off a null the runtime handed it. So: enter clean, leave clean.");
|
|
3811
|
+
self.writeln("let _ = tishlang_runtime::take_pending_throw();");
|
|
3812
|
+
self.writeln("let out = match __tish_export(name) {");
|
|
3813
|
+
self.writeln(" Value::Function(f) => f.call(args),");
|
|
3814
|
+
self.writeln(" other => other,");
|
|
3815
|
+
self.writeln("};");
|
|
3816
|
+
self.writeln("let thrown = tishlang_runtime::take_pending_throw().unwrap_or(Value::Null);");
|
|
3817
|
+
self.writeln("__TISH_LAST_THROW.with(|t| *t.borrow_mut() = thrown);");
|
|
3818
|
+
self.writeln("out");
|
|
3819
|
+
self.indent -= 1;
|
|
3820
|
+
self.writeln("}");
|
|
3821
|
+
self.writeln("");
|
|
3822
|
+
|
|
3823
|
+
for e in &exports {
|
|
3824
|
+
// Checked above: every remaining export is a `fn`, so the arity is present.
|
|
3825
|
+
let n = e.arity.unwrap_or(0);
|
|
3826
|
+
let params: Vec<String> = (0..n).map(|i| format!("a{}: Value", i)).collect();
|
|
3827
|
+
let args: Vec<String> = (0..n).map(|i| format!("a{}", i)).collect();
|
|
3828
|
+
self.writeln(&format!(
|
|
3829
|
+
"pub fn {}({}) -> Value {{",
|
|
3830
|
+
Self::escape_ident(&e.exported_name),
|
|
3831
|
+
params.join(", ")
|
|
3832
|
+
));
|
|
3833
|
+
self.indent += 1;
|
|
3834
|
+
self.writeln(&format!(
|
|
3835
|
+
"__tish_call_export({:?}, &[{}])",
|
|
3836
|
+
e.exported_name,
|
|
3837
|
+
args.join(", ")
|
|
3838
|
+
));
|
|
3839
|
+
self.indent -= 1;
|
|
3840
|
+
self.writeln("}");
|
|
3841
|
+
}
|
|
3842
|
+
self.writeln("");
|
|
3843
|
+
Ok(())
|
|
3844
|
+
}
|
|
3845
|
+
|
|
3583
3846
|
/// Emit an expression in **statement position** (its value is discarded). For a native
|
|
3584
3847
|
/// assignment this emits only the side-effect — NOT the boxed `Value::Number(..)` that the
|
|
3585
3848
|
/// expression form returns (JS "assignment yields its value"). In a hot loop that boxed
|
|
@@ -6963,6 +7226,23 @@ impl Codegen {
|
|
|
6963
7226
|
Ok(())
|
|
6964
7227
|
}
|
|
6965
7228
|
|
|
7229
|
+
/// `.clone()` an emitted operand that is a bare Rust identifier, leave anything else alone.
|
|
7230
|
+
///
|
|
7231
|
+
/// Used where an operand is yielded as the value of an expression that does not consume it
|
|
7232
|
+
/// exclusively (`&&` / `||`): moving a non-`Copy` `Value` local out of such a position makes
|
|
7233
|
+
/// every later read a use-after-move. A parenthesised expression, call, field access, or an
|
|
7234
|
+
/// already-`.clone()`d ident is a temporary, so it needs nothing.
|
|
7235
|
+
fn clone_if_bare_ident(s: &str) -> String {
|
|
7236
|
+
let bare = !s.is_empty()
|
|
7237
|
+
&& !s.starts_with(|c: char| c.is_ascii_digit())
|
|
7238
|
+
&& s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
|
|
7239
|
+
if bare {
|
|
7240
|
+
format!("{}.clone()", s)
|
|
7241
|
+
} else {
|
|
7242
|
+
s.to_string()
|
|
7243
|
+
}
|
|
7244
|
+
}
|
|
7245
|
+
|
|
6966
7246
|
fn emit_call_arg(&mut self, arg: &CallArg) -> Result<String, CompileError> {
|
|
6967
7247
|
let e = match arg {
|
|
6968
7248
|
CallArg::Expr(e) | CallArg::Spread(e) => e,
|
|
@@ -21944,6 +22224,14 @@ impl Codegen {
|
|
|
21944
22224
|
/// the routing state so call sites de-virtualize. On any failure the state is left empty (path
|
|
21945
22225
|
/// disabled) and nothing is appended to the output.
|
|
21946
22226
|
fn setup_aggregate_fns(&mut self, program: &Program) {
|
|
22227
|
+
// #177 de-virtualization deletes the boxed `let <name> = Value::native(..)` local for every
|
|
22228
|
+
// routed fn. `RustLib` publishes exactly those locals as the crate's public API, so an
|
|
22229
|
+
// exported fn that got de-virtualized would leave the emitted `__tish_publish_exports` call
|
|
22230
|
+
// referencing a binding that no longer exists. The unboxed path is a numeric fast path;
|
|
22231
|
+
// a correct public surface outranks it.
|
|
22232
|
+
if self.emit_mode == crate::NativeEmitMode::RustLib {
|
|
22233
|
+
return;
|
|
22234
|
+
}
|
|
21947
22235
|
let dbg = std::env::var("TISH_AGG_DEBUG").is_ok();
|
|
21948
22236
|
// The unboxed struct alias: the (unique) name `A` used as an `A[]` array param of some fn,
|
|
21949
22237
|
// and registered as an all-`Copy`-field struct alias.
|
|
@@ -25014,8 +25302,21 @@ impl Codegen {
|
|
|
25014
25302
|
// the same local (`aid && out.indexOf(aid)`), and a bare `let __l = aid;` moves the non-`Copy`
|
|
25015
25303
|
// `Value` so the reuse is a borrow-after-move (E0382). The left operand is still evaluated
|
|
25016
25304
|
// exactly once (its side effects don't double-run); only its value is cloned for the test.
|
|
25017
|
-
|
|
25018
|
-
|
|
25305
|
+
//
|
|
25306
|
+
// The RESULT operand has the same hazard: when it lowers to a bare local (`inGenBlock &&
|
|
25307
|
+
// currentTrack`), yielding it moves that local out, so a later read — the next iteration
|
|
25308
|
+
// of the enclosing loop, or any subsequent use — is E0382. Clone it in that case only;
|
|
25309
|
+
// anything else is already a temporary (or already cloned), so the fast path is unchanged.
|
|
25310
|
+
BinOp::And => format!(
|
|
25311
|
+
"{{ let __l = ({}).clone(); if __l.is_truthy() {{ {} }} else {{ __l }} }}",
|
|
25312
|
+
l,
|
|
25313
|
+
Self::clone_if_bare_ident(r)
|
|
25314
|
+
),
|
|
25315
|
+
BinOp::Or => format!(
|
|
25316
|
+
"{{ let __l = ({}).clone(); if __l.is_truthy() {{ __l }} else {{ {} }} }}",
|
|
25317
|
+
l,
|
|
25318
|
+
Self::clone_if_bare_ident(r)
|
|
25319
|
+
),
|
|
25019
25320
|
BinOp::BitAnd => Self::emit_bitwise_binop(l, r, "&"),
|
|
25020
25321
|
BinOp::BitOr => Self::emit_bitwise_binop(l, r, "|"),
|
|
25021
25322
|
BinOp::BitXor => Self::emit_bitwise_binop(l, r, "^"),
|
|
@@ -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,
|
|
@@ -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
|
+
}
|
|
@@ -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 {
|
|
@@ -149,6 +149,17 @@ pub fn build_via_cargo_with_config(
|
|
|
149
149
|
project_root,
|
|
150
150
|
);
|
|
151
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
|
+
}
|
|
152
163
|
|
|
153
164
|
let out_stem = output_path
|
|
154
165
|
.file_stem()
|
|
@@ -348,6 +359,122 @@ fn read_facade_agb_version(facade_path: &Path) -> Option<String> {
|
|
|
348
359
|
/// Runs its own `cargo build` (NOT `run_cargo_build`) so the `.cargo/config.toml`
|
|
349
360
|
/// rustflags (`-Tgba.ld`, `-Ctarget-cpu=arm7tdmi`) apply — `run_cargo_build` sets a
|
|
350
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
|
+
|
|
351
478
|
fn build_gba_rom(
|
|
352
479
|
rust_code: &str,
|
|
353
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
|
}
|
|
@@ -266,6 +266,23 @@ pub fn get_index(obj: &Value, index: &Value) -> Value {
|
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
/// `&str` `s[i]` — char at a non-negative integer `idx`; non-int / negative / OOB → null.
|
|
270
|
+
///
|
|
271
|
+
/// Mirrors `tish_runtime::str_index`, and exists here because codegen emits a call to it whenever
|
|
272
|
+
/// the indexed expression is statically a `&str` rather than a `Value` — which is what a module
|
|
273
|
+
/// `const` string lowers to. Without it, `const D = "0123456789"` followed by `D[i]` is a hard
|
|
274
|
+
/// E0425 on the GBA target only, while the identical code compiles and runs everywhere else.
|
|
275
|
+
#[inline]
|
|
276
|
+
pub fn str_index(s: &str, idx: &Value) -> Value {
|
|
277
|
+
match idx {
|
|
278
|
+
Value::Number(n) if *n >= 0.0 && n.fract() == 0.0 => match s.chars().nth(*n as usize) {
|
|
279
|
+
Some(c) => Value::String(c.to_string().into()),
|
|
280
|
+
None => Value::Null,
|
|
281
|
+
},
|
|
282
|
+
_ => Value::Null,
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
269
286
|
pub fn delete_property(obj: &Value, key: &Value) -> Value {
|
|
270
287
|
match obj {
|
|
271
288
|
Value::Object(m) => {
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|