@tishlang/tish-format 2.38.0 → 2.43.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +4 -1
- package/bin/tish-format +0 -0
- package/crates/tish/src/cli_help.rs +28 -0
- package/crates/tish/src/main.rs +41 -5
- package/crates/tish/tests/scope_merge.rs +79 -0
- package/crates/tish_build_utils/src/lib.rs +27 -1
- package/crates/tish_builtins/Cargo.toml +14 -6
- package/crates/tish_builtins/src/array.rs +130 -54
- package/crates/tish_builtins/src/collections.rs +9 -5
- package/crates/tish_builtins/src/construct.rs +7 -1
- package/crates/tish_builtins/src/date.rs +15 -1
- package/crates/tish_builtins/src/globals.rs +38 -3
- package/crates/tish_builtins/src/helpers.rs +7 -3
- package/crates/tish_builtins/src/iterator.rs +5 -1
- package/crates/tish_builtins/src/lib.rs +3 -0
- package/crates/tish_builtins/src/math.rs +14 -0
- package/crates/tish_builtins/src/number.rs +72 -6
- package/crates/tish_builtins/src/object.rs +6 -1
- package/crates/tish_builtins/src/string.rs +13 -2
- package/crates/tish_builtins/src/symbol.rs +17 -17
- package/crates/tish_builtins/src/typedarrays.rs +9 -2
- package/crates/tish_bytecode/src/compiler.rs +76 -2
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +77 -2
- package/crates/tish_compile/src/codegen.rs +812 -47
- package/crates/tish_compile/src/infer.rs +37 -41
- package/crates/tish_compile/src/lib.rs +13 -1
- package/crates/tish_compile/src/platform_resolve.rs +361 -0
- package/crates/tish_compile/src/resolve.rs +148 -10
- package/crates/tish_compile/src/schemes.rs +278 -0
- package/crates/tish_compile/src/types.rs +141 -2
- package/crates/tish_compile/tests/perf_codegen_320.rs +2 -2
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +3 -1
- package/crates/tish_compile/tests/platform_resolve_cli.rs +164 -0
- package/crates/tish_compile/tests/regr_gba_numerics_gated.rs +58 -0
- package/crates/tish_compiler_wasm/Cargo.toml +1 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +56 -6
- package/crates/tish_core/Cargo.toml +39 -4
- package/crates/tish_core/src/compat.rs +506 -0
- package/crates/tish_core/src/console_style.rs +19 -1
- package/crates/tish_core/src/json.rs +34 -10
- package/crates/tish_core/src/lib.rs +121 -25
- package/crates/tish_core/src/macros.rs +1 -1
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/uri.rs +8 -1
- package/crates/tish_core/src/value.rs +182 -54
- package/crates/tish_core/src/vmref.rs +17 -5
- package/crates/tish_eval/Cargo.toml +2 -0
- package/crates/tish_eval/src/eval.rs +90 -3
- package/crates/tish_eval/src/value.rs +8 -0
- package/crates/tish_eval/src/value_convert.rs +316 -21
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_lsp/Cargo.toml +2 -0
- package/crates/tish_lsp/src/import_goto.rs +41 -14
- package/crates/tish_lsp/src/main.rs +18 -0
- package/crates/tish_native/src/build.rs +246 -0
- package/crates/tish_native/src/config.rs +20 -0
- package/crates/tish_native/src/lib.rs +16 -6
- package/crates/tish_parser/src/lib.rs +28 -0
- package/crates/tish_parser/src/parser.rs +25 -4
- package/crates/tish_runtime/src/http_hyper.rs +70 -0
- package/crates/tish_runtime/src/lib.rs +55 -10
- package/crates/tish_runtime/src/ws.rs +94 -21
- package/crates/tish_runtime_gba/Cargo.lock +837 -0
- package/crates/tish_runtime_gba/Cargo.toml +16 -0
- package/crates/tish_runtime_gba/src/gba.rs +211 -0
- package/crates/tish_runtime_gba/src/lib.rs +620 -0
- package/crates/tish_vm/src/jit.rs +1446 -164
- package/crates/tish_vm/src/vm.rs +535 -173
- package/justfile +9 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-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
|
@@ -122,6 +122,11 @@ pub fn resolve_native_modules(
|
|
|
122
122
|
if is_builtin_native_spec(&s) {
|
|
123
123
|
continue;
|
|
124
124
|
}
|
|
125
|
+
// `asset:` loads are baked into the ROM by the GBA codegen, not linked as native
|
|
126
|
+
// modules — they have no crate to resolve and no Value wrapper to generate.
|
|
127
|
+
if crate::schemes::is_scheme_import(&s) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
125
130
|
if !seen.insert(s.clone()) {
|
|
126
131
|
continue;
|
|
127
132
|
}
|
|
@@ -561,7 +566,7 @@ pub fn infer_native_module_exports(program: &Program) -> HashMap<String, HashSet
|
|
|
561
566
|
..
|
|
562
567
|
} => {
|
|
563
568
|
let s = spec.as_ref();
|
|
564
|
-
if is_builtin_native_spec(s) {
|
|
569
|
+
if is_builtin_native_spec(s) || crate::schemes::is_scheme_import(s) {
|
|
565
570
|
continue;
|
|
566
571
|
}
|
|
567
572
|
map.entry(s.to_string())
|
|
@@ -709,6 +714,13 @@ fn find_package_dir(package_name: &str, project_root: &Path) -> Result<PathBuf,
|
|
|
709
714
|
{
|
|
710
715
|
return Ok(node_mod);
|
|
711
716
|
}
|
|
717
|
+
let apple_crate = search.join("tish-apple").join("crates").join(package_name);
|
|
718
|
+
if apple_crate.join("package.json").exists()
|
|
719
|
+
&& read_package_name(&apple_crate.join("package.json"))
|
|
720
|
+
== Some(package_name.to_string())
|
|
721
|
+
{
|
|
722
|
+
return Ok(apple_crate);
|
|
723
|
+
}
|
|
712
724
|
let sibling = search.join(package_name);
|
|
713
725
|
if sibling.join("package.json").exists()
|
|
714
726
|
&& read_package_name(&sibling.join("package.json")) == Some(package_name.to_string())
|
|
@@ -876,7 +888,7 @@ pub fn resolve_project_from_stdin(
|
|
|
876
888
|
|
|
877
889
|
for stmt in &program.statements {
|
|
878
890
|
if let Some(from) = stmt_module_dep(stmt) {
|
|
879
|
-
if is_native_import(from.as_ref()) {
|
|
891
|
+
if is_native_import(from.as_ref()) || is_scoped_native_import(from.as_ref(), from_dir) || crate::schemes::is_scheme_import(from.as_ref()) {
|
|
880
892
|
continue;
|
|
881
893
|
}
|
|
882
894
|
let dep_path = resolve_import_path(from.as_ref(), from_dir, &root_canon)?;
|
|
@@ -944,7 +956,7 @@ fn load_module_recursive(
|
|
|
944
956
|
let dir = canonical.parent().unwrap_or(Path::new("."));
|
|
945
957
|
for stmt in &program.statements {
|
|
946
958
|
if let Some(from) = stmt_module_dep(stmt) {
|
|
947
|
-
if is_native_import(from.as_ref()) {
|
|
959
|
+
if is_native_import(from.as_ref()) || is_scoped_native_import(from.as_ref(), dir) || crate::schemes::is_scheme_import(from.as_ref()) {
|
|
948
960
|
continue; // Native imports don't load files
|
|
949
961
|
}
|
|
950
962
|
let dep_path = resolve_import_path(from.as_ref(), dir, project_root)?;
|
|
@@ -971,7 +983,13 @@ fn load_module_recursive(
|
|
|
971
983
|
/// - cargo:… (Cargo `rustDependencies` + generated wrapper; Rust native backend)
|
|
972
984
|
/// - ffi:… (a C-ABI cdylib loaded via `tish_ffi::load_module` — portable across backends)
|
|
973
985
|
///
|
|
974
|
-
/// Scoped npm packages (`@scope/pkg`) are merged as Tish source
|
|
986
|
+
/// Scoped npm packages (`@scope/pkg`) are **merged as Tish source** by default — they are NOT
|
|
987
|
+
/// native imports and fall through to the module-merge resolver (`exports.tish` / `tish.module`
|
|
988
|
+
/// path / `tish.source` / `index.tish`). A scoped package is only treated as a native Rust crate
|
|
989
|
+
/// when it opts in via `tish.crate` / `tish.rustDependencies` in its package.json (the tish-apple
|
|
990
|
+
/// crates). This split is why `@scope` is intentionally absent below (#38): matching the ecosystem's
|
|
991
|
+
/// resolution for `import … from "@scope/pkg"` is interop, and the native-vs-merged axis is the one
|
|
992
|
+
/// piece tish adds on top, keyed off the native markers rather than mere presence of `tish.module`.
|
|
975
993
|
pub fn is_native_import(spec: &str) -> bool {
|
|
976
994
|
// A leading `node:` (node:fs, node:fs/promises) resolves the same as the bare form.
|
|
977
995
|
let spec = spec.strip_prefix("node:").unwrap_or(spec);
|
|
@@ -984,6 +1002,39 @@ pub fn is_native_import(spec: &str) -> bool {
|
|
|
984
1002
|
)
|
|
985
1003
|
}
|
|
986
1004
|
|
|
1005
|
+
// Import schemes (`asset:` and any project-declared prefix) are recognized via the active
|
|
1006
|
+
// [`crate::schemes`] registry — `crate::schemes::is_scheme_import(spec)`. Like native imports, a
|
|
1007
|
+
// scheme spec is NOT resolved as a `.tish` module or npm package; unlike them it links no crate and
|
|
1008
|
+
// generates no Value wrapper — the codegen injects the scheme's include macro and binds each name to
|
|
1009
|
+
// an i32 handle. See [`resolve_scheme_spec`] for the file-resolution half.
|
|
1010
|
+
|
|
1011
|
+
/// The `@scope/pkg` native opt-in described above (#38): true only when the package resolves and
|
|
1012
|
+
/// its package.json carries the native markers — `tish.module == true` (the native-module flag, a
|
|
1013
|
+
/// bool; a string is a merged-source entry path) AND `tish.crate` / `tish.rustDependencies` (the
|
|
1014
|
+
/// Rust-crate opt-in). Anything else — unresolvable, plain `tish.module: true` with no crate
|
|
1015
|
+
/// (#38), or a `.tish` source package like `@tishlang/pg` — falls through to the module-merge
|
|
1016
|
+
/// resolver. Package resolution walks up from `from_dir` (the importing module's directory), the
|
|
1017
|
+
/// same walk [`resolve_native_module`] later repeats from the project root.
|
|
1018
|
+
pub fn is_scoped_native_import(spec: &str, from_dir: &Path) -> bool {
|
|
1019
|
+
if !spec.starts_with('@') {
|
|
1020
|
+
return false;
|
|
1021
|
+
}
|
|
1022
|
+
let Ok(pkg_dir) = find_package_dir(spec, from_dir) else {
|
|
1023
|
+
return false;
|
|
1024
|
+
};
|
|
1025
|
+
let Ok(content) = std::fs::read_to_string(pkg_dir.join("package.json")) else {
|
|
1026
|
+
return false;
|
|
1027
|
+
};
|
|
1028
|
+
let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
|
|
1029
|
+
return false;
|
|
1030
|
+
};
|
|
1031
|
+
let Some(tish) = json.get("tish") else {
|
|
1032
|
+
return false;
|
|
1033
|
+
};
|
|
1034
|
+
tish.get("module").and_then(|v| v.as_bool()).unwrap_or(false)
|
|
1035
|
+
&& (tish.get("crate").is_some() || tish.get("rustDependencies").is_some())
|
|
1036
|
+
}
|
|
1037
|
+
|
|
987
1038
|
/// True for `ffi:…` specs (portable C-ABI cdylib extensions, loadable on every backend). The
|
|
988
1039
|
/// path after `ffi:` is resolved relative to the importing program and loaded with
|
|
989
1040
|
/// `tish_ffi::load_module`. Unlike `cargo:` (rust-AOT only), `ffi:` is allowed everywhere.
|
|
@@ -1070,6 +1121,48 @@ pub fn resolve_bare_spec(spec: &str, from_dir: &Path, _project_root: &Path) -> O
|
|
|
1070
1121
|
None
|
|
1071
1122
|
}
|
|
1072
1123
|
|
|
1124
|
+
/// Resolve a scheme import (`asset:relpath`, or any project-declared scheme) to a canonical spec.
|
|
1125
|
+
/// For a `resolve_file` scheme, the part after the colon is resolved against the importing module's
|
|
1126
|
+
/// directory (`from_dir`) to an absolute path that must exist — a missing file is a compile-time
|
|
1127
|
+
/// error, not a device panic — and the spec becomes `prefix:/abs/path` (the codegen feeds that
|
|
1128
|
+
/// absolute path to the scheme's include macro; agb's `resolve_path` takes absolute paths as-is, so
|
|
1129
|
+
/// no asset-copy step is needed). A non-`resolve_file` scheme passes through unchanged.
|
|
1130
|
+
fn resolve_scheme_spec(spec: &str, from_dir: &Path) -> Result<String, String> {
|
|
1131
|
+
let (prefix, resolve_file) = crate::schemes::with_active(|reg| {
|
|
1132
|
+
reg.matches(spec)
|
|
1133
|
+
.map(|s| (s.prefix(), s.resolve_file))
|
|
1134
|
+
})
|
|
1135
|
+
.ok_or_else(|| format!("import '{}' is not a registered scheme", spec))?;
|
|
1136
|
+
let rest = spec.strip_prefix(&prefix).unwrap_or(spec);
|
|
1137
|
+
if !resolve_file {
|
|
1138
|
+
return Ok(spec.to_string());
|
|
1139
|
+
}
|
|
1140
|
+
if rest.is_empty() {
|
|
1141
|
+
return Err(format!(
|
|
1142
|
+
"{} import needs a path, e.g. import {{ player }} from '{}gfx/player.png'",
|
|
1143
|
+
prefix.trim_end_matches(':'),
|
|
1144
|
+
prefix
|
|
1145
|
+
));
|
|
1146
|
+
}
|
|
1147
|
+
let path = Path::new(rest);
|
|
1148
|
+
let joined = if path.is_absolute() {
|
|
1149
|
+
path.to_path_buf()
|
|
1150
|
+
} else {
|
|
1151
|
+
from_dir.join(path)
|
|
1152
|
+
};
|
|
1153
|
+
let abs = joined.canonicalize().map_err(|e| {
|
|
1154
|
+
format!(
|
|
1155
|
+
"{} import '{}': cannot find '{}' (resolved from {}): {}",
|
|
1156
|
+
prefix.trim_end_matches(':'),
|
|
1157
|
+
spec,
|
|
1158
|
+
rest,
|
|
1159
|
+
from_dir.display(),
|
|
1160
|
+
e
|
|
1161
|
+
)
|
|
1162
|
+
})?;
|
|
1163
|
+
Ok(format!("{}{}", prefix, abs.to_string_lossy()))
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1073
1166
|
/// Resolve an import specifier (e.g. "./foo.tish", "../lib/utils", "lattish") to an absolute path.
|
|
1074
1167
|
fn resolve_import_path(
|
|
1075
1168
|
spec: &str,
|
|
@@ -1091,8 +1184,12 @@ fn resolve_import_path(
|
|
|
1091
1184
|
spec, spec
|
|
1092
1185
|
));
|
|
1093
1186
|
}
|
|
1187
|
+
let ctx = crate::platform_resolve::resolve_context();
|
|
1188
|
+
if let Some(path) = crate::platform_resolve::resolve_with_platform(spec, from_dir, ctx) {
|
|
1189
|
+
return Ok(path);
|
|
1190
|
+
}
|
|
1094
1191
|
let base = from_dir.join(spec);
|
|
1095
|
-
//
|
|
1192
|
+
// Fallback: legacy single .tish probe when platform context is empty / file missing
|
|
1096
1193
|
let path = if base.extension().is_none() {
|
|
1097
1194
|
let with_ext = base.with_extension("tish");
|
|
1098
1195
|
if with_ext.exists() {
|
|
@@ -1155,7 +1252,7 @@ fn has_cycle_from(
|
|
|
1155
1252
|
) -> Result<bool, String> {
|
|
1156
1253
|
for stmt in &program.statements {
|
|
1157
1254
|
if let Some(from) = stmt_module_dep(stmt) {
|
|
1158
|
-
if is_native_import(from.as_ref()) {
|
|
1255
|
+
if is_native_import(from.as_ref()) || is_scoped_native_import(from.as_ref(), from_dir) || crate::schemes::is_scheme_import(from.as_ref()) {
|
|
1159
1256
|
continue;
|
|
1160
1257
|
}
|
|
1161
1258
|
let dep_path = resolve_import_path(from.as_ref(), from_dir, Path::new("."))?;
|
|
@@ -1837,10 +1934,20 @@ pub fn merge_modules(mut modules: Vec<ResolvedModule>) -> Result<MergedProgram,
|
|
|
1837
1934
|
from,
|
|
1838
1935
|
span,
|
|
1839
1936
|
} => {
|
|
1840
|
-
if is_native_import(from.as_ref())
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1937
|
+
if is_native_import(from.as_ref())
|
|
1938
|
+
|| is_scoped_native_import(from.as_ref(), dir)
|
|
1939
|
+
|| crate::schemes::is_scheme_import(from.as_ref())
|
|
1940
|
+
{
|
|
1941
|
+
// Normalize fs/http/process -> tish:fs etc. for Node compatibility.
|
|
1942
|
+
// For `asset:` imports, resolve the (relative) file path to an absolute
|
|
1943
|
+
// one now, while we still know the importing module's directory — the GBA
|
|
1944
|
+
// codegen emits `include_aseprite_inner!("<abspath>")` from that spec.
|
|
1945
|
+
let canonical_spec = if crate::schemes::is_scheme_import(from.as_ref()) {
|
|
1946
|
+
resolve_scheme_spec(from.as_ref(), dir)?
|
|
1947
|
+
} else {
|
|
1948
|
+
normalize_builtin_spec(from.as_ref())
|
|
1949
|
+
.unwrap_or_else(|| from.to_string())
|
|
1950
|
+
};
|
|
1844
1951
|
// Emit VarDecl with NativeModuleLoad for each specifier
|
|
1845
1952
|
for spec in specifiers {
|
|
1846
1953
|
match spec {
|
|
@@ -2090,3 +2197,34 @@ mod cargo_spec_tests {
|
|
|
2090
2197
|
);
|
|
2091
2198
|
}
|
|
2092
2199
|
}
|
|
2200
|
+
|
|
2201
|
+
#[cfg(test)]
|
|
2202
|
+
mod find_package_dir_tests {
|
|
2203
|
+
use super::find_package_dir;
|
|
2204
|
+
|
|
2205
|
+
#[test]
|
|
2206
|
+
fn prefers_tish_apple_crates_over_sibling() {
|
|
2207
|
+
let dir = tempfile::tempdir().expect("tempdir");
|
|
2208
|
+
let root = dir.path();
|
|
2209
|
+
// sibling decoy
|
|
2210
|
+
let sibling = root.join("tish-macos");
|
|
2211
|
+
std::fs::create_dir_all(&sibling).unwrap();
|
|
2212
|
+
std::fs::write(
|
|
2213
|
+
sibling.join("package.json"),
|
|
2214
|
+
r#"{"name":"tish-macos","tish":{"module":true,"crate":"tishlang_macos","export":"macos_object"}}"#,
|
|
2215
|
+
)
|
|
2216
|
+
.unwrap();
|
|
2217
|
+
// preferred apple path
|
|
2218
|
+
let apple = root.join("tish-apple").join("crates").join("tish-macos");
|
|
2219
|
+
std::fs::create_dir_all(&apple).unwrap();
|
|
2220
|
+
std::fs::write(
|
|
2221
|
+
apple.join("package.json"),
|
|
2222
|
+
r#"{"name":"tish-macos","tish":{"module":true,"crate":"tish-macos","export":"macos_object"}}"#,
|
|
2223
|
+
)
|
|
2224
|
+
.unwrap();
|
|
2225
|
+
let project = root.join("app");
|
|
2226
|
+
std::fs::create_dir_all(&project).unwrap();
|
|
2227
|
+
let hit = find_package_dir("tish-macos", &project).expect("resolve");
|
|
2228
|
+
assert_eq!(hit.canonicalize().unwrap(), apple.canonicalize().unwrap());
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
//! Import-scheme registry — the extension seam for custom import prefixes.
|
|
2
|
+
//!
|
|
3
|
+
//! An *import scheme* is a prefix like `asset:` that means "don't resolve this as a `.tish` module
|
|
4
|
+
//! or npm package; instead resolve it my way and inject my code." tish core ships **no** built-in
|
|
5
|
+
//! schemes — it is target-agnostic (see [`SchemeRegistry::builtin`]). Schemes are contributed by
|
|
6
|
+
//! cargo path-dependencies that ship a `tish.schemes.json` (this is how `tish-agb` provides
|
|
7
|
+
//! `asset:`/`sheet:`/`background:`/`wav:`/`map:` for free to any game that depends on it) and by a
|
|
8
|
+
//! project's own `package.json` → `tish.schemes`, with no changes to the compiler. This is the same
|
|
9
|
+
//! philosophy as `cargo:` imports (config-declared, zero core edits per crate), generalized.
|
|
10
|
+
//!
|
|
11
|
+
//! A scheme is described declaratively:
|
|
12
|
+
//! * `prefix` — the scheme name (`asset` → matches `asset:...`).
|
|
13
|
+
//! * `resolve_file` — resolve the rest as a project-relative file and validate it exists.
|
|
14
|
+
//! * `targets` — per emit-target ("gba", …) code templates: the `use`s + body of a generated
|
|
15
|
+
//! `mod`, an accessor, and a registration call. Templates substitute `{path}` (the resolved
|
|
16
|
+
//! absolute path, as a Rust string literal) and `{mod}` (the generated module's identifier).
|
|
17
|
+
//!
|
|
18
|
+
//! Only *file-baking* schemes are expressible this way today — the family `asset:` belongs to
|
|
19
|
+
//! (resolve a file → emit a macro + a registration + bind an i32 handle). Schemes needing
|
|
20
|
+
//! compile-time logic (e.g. parsing aseprite tags) will want a code-plugin surface later.
|
|
21
|
+
//!
|
|
22
|
+
//! The active registry is compile-scoped: [`set_active`] installs it at the start of a build and
|
|
23
|
+
//! the resolve/codegen predicates read it via [`with_active`]. It defaults to [`builtin`] so any
|
|
24
|
+
//! path that doesn't install one still sees the built-in schemes (matching prior behaviour).
|
|
25
|
+
|
|
26
|
+
use std::cell::RefCell;
|
|
27
|
+
use std::collections::HashMap;
|
|
28
|
+
use std::path::Path;
|
|
29
|
+
|
|
30
|
+
/// What an imported name binds to. Only `Handle` (an `i32` index into a runtime arena) exists
|
|
31
|
+
/// today; kept as an enum so richer bindings (typed structs, const data) can be added later.
|
|
32
|
+
#[derive(Clone, Debug, PartialEq)]
|
|
33
|
+
pub enum BindKind {
|
|
34
|
+
/// The imported name is an `i32` handle — its index among the scheme's registered files.
|
|
35
|
+
Handle,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// One emit target's code templates for a scheme (e.g. the `gba` recipe for `asset:`).
|
|
39
|
+
#[derive(Clone, Debug)]
|
|
40
|
+
pub struct TargetEmit {
|
|
41
|
+
/// `use` lines placed at the top of the generated `mod`.
|
|
42
|
+
pub uses: Vec<String>,
|
|
43
|
+
/// The module body — typically the include macro. `{path}` → the file as a Rust string literal.
|
|
44
|
+
pub module_body: String,
|
|
45
|
+
/// A public accessor emitted inside the same `mod` (so it can see the macro's private statics).
|
|
46
|
+
pub accessor: String,
|
|
47
|
+
/// Registration call emitted in the entry before `run()`. `{mod}` → the generated module ident.
|
|
48
|
+
pub register: String,
|
|
49
|
+
/// What each imported name binds to.
|
|
50
|
+
pub bind: BindKind,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// A declaratively-described import scheme.
|
|
54
|
+
#[derive(Clone, Debug)]
|
|
55
|
+
pub struct SchemeDef {
|
|
56
|
+
/// The prefix WITHOUT the trailing colon (`asset`); matches specs starting `asset:`.
|
|
57
|
+
pub name: String,
|
|
58
|
+
/// Resolve the part after the colon as a project-relative file (canonicalize + validate exists).
|
|
59
|
+
pub resolve_file: bool,
|
|
60
|
+
/// Per emit-target code templates.
|
|
61
|
+
pub targets: HashMap<String, TargetEmit>,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
impl SchemeDef {
|
|
65
|
+
/// The spec prefix including the colon (`asset:`).
|
|
66
|
+
pub fn prefix(&self) -> String {
|
|
67
|
+
format!("{}:", self.name)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// The set of active import schemes for a build.
|
|
72
|
+
#[derive(Clone, Debug, Default)]
|
|
73
|
+
pub struct SchemeRegistry {
|
|
74
|
+
pub schemes: Vec<SchemeDef>,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
impl SchemeRegistry {
|
|
78
|
+
/// The schemes tish core ships with — **none**. tish core is target-agnostic and knows nothing
|
|
79
|
+
/// about agb/GBA; concrete schemes (like `asset:`) are contributed by the crates that implement
|
|
80
|
+
/// their runtime (see [`from_project`](Self::from_project)). Kept as the seam for any future
|
|
81
|
+
/// genuinely-generic built-in scheme.
|
|
82
|
+
pub fn builtin() -> Self {
|
|
83
|
+
SchemeRegistry {
|
|
84
|
+
schemes: Vec::new(),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// The active registry for a build: schemes contributed by cargo dependencies, then the
|
|
89
|
+
/// project's own, merged in that order (project overrides a dependency of the same name).
|
|
90
|
+
///
|
|
91
|
+
/// * **Dependency-contributed** — each `tish.rustDependencies` entry with a `path` may ship a
|
|
92
|
+
/// `tish.schemes.json` (`{ "schemes": { … } }`) in its crate dir. This is how tish-agb
|
|
93
|
+
/// provides `asset:` without any tish-core edits: a game that depends on tish-agb (as every
|
|
94
|
+
/// sprite game already does) gets the scheme for free, no per-game config.
|
|
95
|
+
/// * **Project-local** — `package.json` → `tish.schemes`, for one-off or overriding schemes.
|
|
96
|
+
///
|
|
97
|
+
/// A malformed entry is skipped, not fatal.
|
|
98
|
+
pub fn from_project(project_root: &Path) -> Self {
|
|
99
|
+
let mut reg = Self::builtin();
|
|
100
|
+
let tish = crate::resolve::read_project_tish_config(project_root);
|
|
101
|
+
// 1. Schemes contributed by cargo path-dependencies.
|
|
102
|
+
if let Some(deps) = tish.get("rustDependencies").and_then(|v| v.as_object()) {
|
|
103
|
+
for val in deps.values() {
|
|
104
|
+
let Some(path) = val
|
|
105
|
+
.as_object()
|
|
106
|
+
.and_then(|o| o.get("path"))
|
|
107
|
+
.and_then(|p| p.as_str())
|
|
108
|
+
else {
|
|
109
|
+
continue;
|
|
110
|
+
};
|
|
111
|
+
let p = Path::new(path);
|
|
112
|
+
let dep_dir = if p.is_absolute() {
|
|
113
|
+
p.to_path_buf()
|
|
114
|
+
} else {
|
|
115
|
+
project_root.join(p)
|
|
116
|
+
};
|
|
117
|
+
let manifest = dep_dir.join("tish.schemes.json");
|
|
118
|
+
if let Ok(content) = std::fs::read_to_string(&manifest) {
|
|
119
|
+
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
|
|
120
|
+
reg.merge_schemes(json.get("schemes"));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// 2. Project-local schemes (override dependency-contributed ones of the same name).
|
|
126
|
+
reg.merge_schemes(tish.get("schemes"));
|
|
127
|
+
reg
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Merge a JSON `{ name: def, … }` schemes object into the registry; a same-named scheme
|
|
131
|
+
/// replaces the existing one.
|
|
132
|
+
fn merge_schemes(&mut self, schemes: Option<&serde_json::Value>) {
|
|
133
|
+
let Some(obj) = schemes.and_then(|v| v.as_object()) else {
|
|
134
|
+
return;
|
|
135
|
+
};
|
|
136
|
+
for (name, def) in obj {
|
|
137
|
+
if let Some(parsed) = parse_scheme_def(name, def) {
|
|
138
|
+
self.schemes.retain(|s| s.name != parsed.name);
|
|
139
|
+
self.schemes.push(parsed);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// The scheme whose prefix `spec` starts with, if any.
|
|
145
|
+
pub fn matches(&self, spec: &str) -> Option<&SchemeDef> {
|
|
146
|
+
self.schemes.iter().find(|s| spec.starts_with(&s.prefix()))
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Parse one `tish.schemes["name"]` JSON entry into a [`SchemeDef`]. Returns `None` (skip) on a
|
|
151
|
+
/// shape that doesn't describe a valid scheme.
|
|
152
|
+
fn parse_scheme_def(name: &str, def: &serde_json::Value) -> Option<SchemeDef> {
|
|
153
|
+
// The name is interpolated into a generated module identifier (`mod __scheme_<name>_<j>`) and
|
|
154
|
+
// used as the `<name>:` import prefix, so it must be a valid Rust identifier. A name like
|
|
155
|
+
// `sprite-sheet` would emit un-compilable Rust (a raw rustc error far from its cause); skip it
|
|
156
|
+
// instead, consistent with "a malformed entry is skipped, not fatal".
|
|
157
|
+
if !is_valid_scheme_name(name) {
|
|
158
|
+
return None;
|
|
159
|
+
}
|
|
160
|
+
let obj = def.as_object()?;
|
|
161
|
+
let resolve_file = obj.get("file").and_then(|v| v.as_bool()).unwrap_or(true);
|
|
162
|
+
let mut targets = HashMap::new();
|
|
163
|
+
if let Some(target_map) = obj.get("targets").and_then(|v| v.as_object()) {
|
|
164
|
+
for (target, spec) in target_map {
|
|
165
|
+
let s = spec.as_object()?;
|
|
166
|
+
let uses = s
|
|
167
|
+
.get("uses")
|
|
168
|
+
.and_then(|v| v.as_array())
|
|
169
|
+
.map(|a| {
|
|
170
|
+
a.iter()
|
|
171
|
+
.filter_map(|x| x.as_str().map(|s| s.to_string()))
|
|
172
|
+
.collect()
|
|
173
|
+
})
|
|
174
|
+
.unwrap_or_default();
|
|
175
|
+
let module_body = s.get("module").and_then(|v| v.as_str())?.to_string();
|
|
176
|
+
let accessor = s
|
|
177
|
+
.get("accessor")
|
|
178
|
+
.and_then(|v| v.as_str())
|
|
179
|
+
.unwrap_or("")
|
|
180
|
+
.to_string();
|
|
181
|
+
let register = s
|
|
182
|
+
.get("register")
|
|
183
|
+
.and_then(|v| v.as_str())
|
|
184
|
+
.unwrap_or("")
|
|
185
|
+
.to_string();
|
|
186
|
+
targets.insert(
|
|
187
|
+
target.clone(),
|
|
188
|
+
TargetEmit {
|
|
189
|
+
uses,
|
|
190
|
+
module_body,
|
|
191
|
+
accessor,
|
|
192
|
+
register,
|
|
193
|
+
bind: BindKind::Handle,
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
Some(SchemeDef {
|
|
199
|
+
name: name.to_string(),
|
|
200
|
+
resolve_file,
|
|
201
|
+
targets,
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// A scheme name must be a valid Rust identifier: it becomes part of a generated module ident
|
|
206
|
+
/// (`__scheme_<name>_<j>`) and the `<name>:` import prefix. ASCII letter/underscore, then
|
|
207
|
+
/// letters/digits/underscores. (The same shape `types::is_struct_field_safe` enforces for native
|
|
208
|
+
/// struct field keys, kept local so `schemes` stays self-contained.)
|
|
209
|
+
fn is_valid_scheme_name(name: &str) -> bool {
|
|
210
|
+
let mut chars = name.chars();
|
|
211
|
+
match chars.next() {
|
|
212
|
+
Some(c) if c == '_' || c.is_ascii_alphabetic() => {}
|
|
213
|
+
_ => return false,
|
|
214
|
+
}
|
|
215
|
+
chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
thread_local! {
|
|
219
|
+
static ACTIVE: RefCell<SchemeRegistry> = RefCell::new(SchemeRegistry::builtin());
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/// Install the registry for the current build. Call once at the start of a compile, before
|
|
223
|
+
/// resolution — every scheme predicate reads whatever is installed here.
|
|
224
|
+
pub fn set_active(registry: SchemeRegistry) {
|
|
225
|
+
ACTIVE.with(|c| *c.borrow_mut() = registry);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/// Read from the active registry.
|
|
229
|
+
pub fn with_active<R>(f: impl FnOnce(&SchemeRegistry) -> R) -> R {
|
|
230
|
+
ACTIVE.with(|c| f(&c.borrow()))
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// True if `spec` is a registered import scheme (built-in or project-declared).
|
|
234
|
+
pub fn is_scheme_import(spec: &str) -> bool {
|
|
235
|
+
with_active(|r| r.matches(spec).is_some())
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// Substitute `{path}` (the file as a Rust string literal), `{mod}` (the generated module ident), and
|
|
239
|
+
/// `{charset}` (the program's used non-ASCII characters as a Rust string literal — for schemes that
|
|
240
|
+
/// bake only the glyphs a program actually uses, e.g. `font<N>:`) in a template.
|
|
241
|
+
pub fn render_template(template: &str, abspath: &str, module_ident: &str, charset: &str) -> String {
|
|
242
|
+
template
|
|
243
|
+
.replace("{path}", &format!("{:?}", abspath))
|
|
244
|
+
.replace("{mod}", module_ident)
|
|
245
|
+
.replace("{charset}", &format!("{:?}", charset))
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#[cfg(test)]
|
|
249
|
+
mod tests {
|
|
250
|
+
use super::*;
|
|
251
|
+
|
|
252
|
+
#[test]
|
|
253
|
+
fn scheme_name_must_be_a_rust_identifier() {
|
|
254
|
+
// Valid identifiers pass.
|
|
255
|
+
assert!(is_valid_scheme_name("asset"));
|
|
256
|
+
assert!(is_valid_scheme_name("sprite_sheet"));
|
|
257
|
+
assert!(is_valid_scheme_name("_x"));
|
|
258
|
+
assert!(is_valid_scheme_name("bg2"));
|
|
259
|
+
// Anything that would break the generated `mod __scheme_<name>_<j>` is rejected.
|
|
260
|
+
assert!(!is_valid_scheme_name("sprite-sheet")); // hyphen
|
|
261
|
+
assert!(!is_valid_scheme_name("")); // empty
|
|
262
|
+
assert!(!is_valid_scheme_name("2d")); // leading digit
|
|
263
|
+
assert!(!is_valid_scheme_name("a:b")); // colon
|
|
264
|
+
assert!(!is_valid_scheme_name("a b")); // space
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#[test]
|
|
268
|
+
fn parse_scheme_def_skips_invalid_name() {
|
|
269
|
+
let def = serde_json::json!({
|
|
270
|
+
"file": true,
|
|
271
|
+
"targets": { "gba": { "module": "// x", "register": "" } }
|
|
272
|
+
});
|
|
273
|
+
// A hyphenated name is skipped (returns None) rather than emitting un-compilable Rust.
|
|
274
|
+
assert!(parse_scheme_def("sprite-sheet", &def).is_none());
|
|
275
|
+
// The same def under a valid name parses.
|
|
276
|
+
assert!(parse_scheme_def("sheet", &def).is_some());
|
|
277
|
+
}
|
|
278
|
+
}
|