@tishlang/tish-lsp 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-lsp +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_lsp/Cargo.toml +3 -1
- 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-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
|
@@ -229,7 +229,7 @@ pub fn infer_expr_type(expr: &Expr, ctx: &InferCtx) -> Option<TypeAnnotation> {
|
|
|
229
229
|
/// Run inference over a program, returning a modified Program with additional
|
|
230
230
|
/// type annotations filled in on `VarDecl` nodes.
|
|
231
231
|
pub fn infer_program(program: &Program) -> Program {
|
|
232
|
-
// M4 (
|
|
232
|
+
// M4 (default-on via native_opts_enabled; TISH_NATIVE_OPT=0 opts out) runs FIRST: give unannotated params used PURELY
|
|
233
233
|
// numerically a synthetic `: number`. Doing this *before* local/struct inference is what
|
|
234
234
|
// lets derived numeric locals (`let x0 = (px / w) * 3`) be proven numeric off the now-known
|
|
235
235
|
// param types — otherwise they fall back to boxed `Value` and the whole hot loop boxes with
|
|
@@ -245,7 +245,7 @@ pub fn infer_program(program: &Program) -> Program {
|
|
|
245
245
|
let p = Program {
|
|
246
246
|
statements: infer_statements(&p.statements, &mut ctx),
|
|
247
247
|
};
|
|
248
|
-
// Automatic struct inference (
|
|
248
|
+
// Automatic struct inference (default-on via native_opts_enabled):
|
|
249
249
|
// give unannotated object literals a concrete struct type so the Rust
|
|
250
250
|
// backend emits unboxed structs with direct field access. Conservative —
|
|
251
251
|
// only applies when every use of the binding is a literal-key field read,
|
|
@@ -255,17 +255,18 @@ pub fn infer_program(program: &Program) -> Program {
|
|
|
255
255
|
} else {
|
|
256
256
|
p
|
|
257
257
|
};
|
|
258
|
-
// S-0..S-
|
|
259
|
-
//
|
|
258
|
+
// S-0..S-F aggregate (interprocedural monomorphic struct) inference — issue #177, default-on
|
|
259
|
+
// via native_opts_enabled. Front-end of the nbody unboxing lever: it
|
|
260
260
|
// S-0: types params used ONLY as object-literal field values (`body(x,…)` → `: number`),
|
|
261
261
|
// S-A: registers the return-shape struct alias for an all-f64 object-literal-returning fn,
|
|
262
262
|
// S-B: propagates a call's return type to `let p = body(…)` / `let bs = makeBodies()`,
|
|
263
263
|
// S-C: types `[ident,…]` array literals from those struct-typed locals.
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
// (
|
|
268
|
-
//
|
|
264
|
+
// S-0 scalar params are always stamped. The S-A/S-B/S-C struct/array shapes are stamped only
|
|
265
|
+
// when the S-D whole-program candidacy predicate holds (`unbox_alias` — monomorphic all-f64
|
|
266
|
+
// shape, no `===`/escape/reshape), in which case codegen's S-E/S-F typed-fn ABI tier
|
|
267
|
+
// (`setup_aggregate_fns`) consumes them, de-virtualizing the group into native free fns over
|
|
268
|
+
// `Vec<TishStruct_alias>`; any unlowerable use disables the whole path (boxed fallback,
|
|
269
|
+
// byte-identical). See `aggregate_infer_program` / `stamp_aggregate`.
|
|
269
270
|
if crate::native_opts_enabled() {
|
|
270
271
|
aggregate_infer_program(p)
|
|
271
272
|
} else {
|
|
@@ -274,7 +275,7 @@ pub fn infer_program(program: &Program) -> Program {
|
|
|
274
275
|
}
|
|
275
276
|
|
|
276
277
|
// ---------------------------------------------------------------------------
|
|
277
|
-
// M4: parameter type inference (conservative, sound,
|
|
278
|
+
// M4: parameter type inference (conservative, sound, default-on)
|
|
278
279
|
// ---------------------------------------------------------------------------
|
|
279
280
|
|
|
280
281
|
fn param_infer_program(program: Program) -> Program {
|
|
@@ -1469,7 +1470,7 @@ pub(crate) fn pi_mentions(e: &Expr, name: &str) -> bool {
|
|
|
1469
1470
|
}
|
|
1470
1471
|
|
|
1471
1472
|
// ---------------------------------------------------------------------------
|
|
1472
|
-
// Automatic struct inference (conservative, sound,
|
|
1473
|
+
// Automatic struct inference (conservative, sound, default-on)
|
|
1473
1474
|
// ---------------------------------------------------------------------------
|
|
1474
1475
|
|
|
1475
1476
|
/// One emitted `type` decl: alias name plus its field list.
|
|
@@ -2319,7 +2320,7 @@ fn inline_single_call_factories(stmts: Vec<Statement>) -> Vec<Statement> {
|
|
|
2319
2320
|
// (`return [a, b]`), a call return, or a discarded `f()` is left intact — those are the #177
|
|
2320
2321
|
// aggregate machinery's territory (nbody's `makeBodies` returns `[sun, jupiter, …]`, and
|
|
2321
2322
|
// inlining it removes the function the aggregate ABI expects → nbody BUILD-FAIL under
|
|
2322
|
-
//
|
|
2323
|
+
// the aggregate path). This keeps the inliner to exactly the Stage A/B bridge target.
|
|
2323
2324
|
let return_ident = match &ret_expr {
|
|
2324
2325
|
Expr::Ident { name, .. } if leaked.iter().any(|l| l.as_ref() == name.as_ref()) => {
|
|
2325
2326
|
name.clone()
|
|
@@ -3987,8 +3988,8 @@ fn _uses_arrow_body(_: &ArrowBody) {}
|
|
|
3987
3988
|
// S-0..S-C: aggregate (interprocedural monomorphic struct) inference — issue #177
|
|
3988
3989
|
// ===========================================================================
|
|
3989
3990
|
//
|
|
3990
|
-
// This is the front-end of the nbody-unboxing lever. It runs
|
|
3991
|
-
// `
|
|
3991
|
+
// This is the front-end of the nbody-unboxing lever. It runs by default under
|
|
3992
|
+
// `native_opts_enabled()` (`TISH_NATIVE_OPT=0` opts out). The four sub-passes are pure analysis
|
|
3992
3993
|
// over the (already base/param/struct-inferred) `Program`:
|
|
3993
3994
|
//
|
|
3994
3995
|
// S-0 param-numeric-as-field-value: a param used ONLY as an object-literal
|
|
@@ -4004,27 +4005,22 @@ fn _uses_arrow_body(_: &ArrowBody) {}
|
|
|
4004
4005
|
//
|
|
4005
4006
|
// SOUNDNESS / WHAT IS ACTUALLY WRITTEN BACK
|
|
4006
4007
|
// -----------------------------------------
|
|
4007
|
-
//
|
|
4008
|
-
//
|
|
4009
|
-
//
|
|
4010
|
-
//
|
|
4011
|
-
//
|
|
4012
|
-
//
|
|
4013
|
-
//
|
|
4014
|
-
//
|
|
4015
|
-
//
|
|
4016
|
-
//
|
|
4017
|
-
//
|
|
4018
|
-
//
|
|
4019
|
-
//
|
|
4020
|
-
//
|
|
4021
|
-
//
|
|
4022
|
-
//
|
|
4023
|
-
// codegen — they change nothing on their own).
|
|
4024
|
-
// The S-A/S-B/S-C struct *shapes* are computed and exposed via
|
|
4025
|
-
// `analyze_aggregate` for unit tests and for the future S-D..S-F consumers, but
|
|
4026
|
-
// are NOT yet stamped onto params / call-locals. This keeps the ON path free of
|
|
4027
|
-
// any checksum divergence while the inference logic is validated independently.
|
|
4008
|
+
// Stamping a struct/array annotation is only sound when codegen can back it end
|
|
4009
|
+
// to end — a `Named`/`Array(Named)` annotation on a param or call-initialised
|
|
4010
|
+
// local whose actual binding stays a boxed `Value` would MISCOMPILE (`p.x` /
|
|
4011
|
+
// `bodies[i].vx = …` lowered against a boxed value). So write-back is two-tier:
|
|
4012
|
+
// * S-0's scalar `: number` params are ALWAYS stamped (identical to the M4
|
|
4013
|
+
// param-infer mechanism, already consumed soundly), plus the struct `type`
|
|
4014
|
+
// alias decls (inert on their own — unreferenced aliases are dropped).
|
|
4015
|
+
// * The S-A/S-B/S-C struct/array shapes are stamped ONLY when the S-D
|
|
4016
|
+
// whole-program candidacy predicate holds (`compute_unbox_candidacy` →
|
|
4017
|
+
// `unbox_alias`: one monomorphic all-f64 shape, no `===`/escape/reshape).
|
|
4018
|
+
// Codegen's S-E/S-F typed-fn ABI tier (`setup_aggregate_fns`) then consumes
|
|
4019
|
+
// them, de-virtualizing the factory/array/operator fns into native free fns
|
|
4020
|
+
// over `Vec<TishStruct_alias>` threaded by reference. All-or-nothing with
|
|
4021
|
+
// scratch-buffer rollback: any unlowerable use disables the whole path and
|
|
4022
|
+
// the boxed closures are emitted unchanged (byte-identical fallback).
|
|
4023
|
+
// `analyze_aggregate` stays exposed for unit tests over the raw analysis.
|
|
4028
4024
|
|
|
4029
4025
|
/// The result of the aggregate analysis: the registered struct shapes plus, per
|
|
4030
4026
|
/// function, the inferred return shape and the set of params S-0 typed numeric.
|
|
@@ -5003,12 +4999,12 @@ fn infer_aggregate_expr(
|
|
|
5003
4999
|
}
|
|
5004
5000
|
}
|
|
5005
5001
|
|
|
5006
|
-
/// Aggregate inference writeback
|
|
5007
|
-
///
|
|
5008
|
-
///
|
|
5009
|
-
///
|
|
5010
|
-
///
|
|
5011
|
-
///
|
|
5002
|
+
/// Aggregate inference writeback (two-tier — see the module note above). Always
|
|
5003
|
+
/// stamps the S-0 numeric `: number` params plus the struct alias `type` decls;
|
|
5004
|
+
/// when the S-D candidacy predicate holds (`unbox_alias` is `Some`) it ALSO stamps
|
|
5005
|
+
/// the S-A/S-B/S-C struct/array shapes (factory returns, group-fn array params,
|
|
5006
|
+
/// top-level array locals), which codegen's S-E/S-F typed-fn ABI tier
|
|
5007
|
+
/// (`setup_aggregate_fns`) consumes with scratch-buffer rollback.
|
|
5012
5008
|
fn aggregate_infer_program(program: Program) -> Program {
|
|
5013
5009
|
let analysis = analyze_aggregate(&program);
|
|
5014
5010
|
|
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
mod check;
|
|
6
6
|
mod codegen;
|
|
7
7
|
mod infer;
|
|
8
|
+
mod platform_resolve;
|
|
8
9
|
mod resolve;
|
|
10
|
+
mod schemes;
|
|
9
11
|
mod types;
|
|
10
12
|
|
|
13
|
+
pub use schemes::{set_active as set_scheme_registry, SchemeRegistry};
|
|
14
|
+
|
|
11
15
|
pub use check::{check_program, TypeDiagnostic};
|
|
12
16
|
|
|
13
17
|
/// The native typed-codegen optimizations — numeric param inference, struct/aggregate inference,
|
|
@@ -29,13 +33,16 @@ pub(crate) fn native_recur_guard_enabled() -> bool {
|
|
|
29
33
|
std::env::var("TISH_NATIVE_RECUR_GUARD").map(|v| v != "0").unwrap_or(true)
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
/// How generated Rust is linked (desktop binary vs embedded iOS staticlib).
|
|
36
|
+
/// How generated Rust is linked (desktop binary vs embedded iOS staticlib vs GBA ROM).
|
|
33
37
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
34
38
|
pub enum NativeEmitMode {
|
|
35
39
|
#[default]
|
|
36
40
|
DesktopBin,
|
|
37
41
|
/// `[lib] crate-type = ["staticlib"]` — no `fn main()`, host calls `tish_ios_launch`.
|
|
38
42
|
EmbeddedLib,
|
|
43
|
+
/// Game Boy Advance ROM: `#![no_std]`, `#[agb::entry] fn agb_main(gba)`, links the
|
|
44
|
+
/// `tishlang_runtime_gba` facade. Numbers/async lowering diverge; see codegen `emit_program`.
|
|
45
|
+
Gba,
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
pub use codegen::CompileError;
|
|
@@ -44,6 +51,11 @@ pub use codegen::{
|
|
|
44
51
|
compile_with_features, compile_with_native_modules, compile_with_native_modules_emit,
|
|
45
52
|
compile_with_project_root,
|
|
46
53
|
};
|
|
54
|
+
pub use platform_resolve::{
|
|
55
|
+
apply_resolve_env, parse_platform, parse_surface, platform_suffixes, platform_virtual_keys,
|
|
56
|
+
resolve_context, resolve_id_public, resolve_with_platform, set_resolve_context, Platform,
|
|
57
|
+
ResolveContext, Surface,
|
|
58
|
+
};
|
|
47
59
|
pub use resolve::{
|
|
48
60
|
cargo_export_fn_name, compute_native_build_artifacts, detect_cycles, ensure_tish_canvas_module,
|
|
49
61
|
export_name_to_rust_ident, extract_native_import_features, format_rust_dependencies_toml,
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
//! React Native–style platform/surface file resolution for `.tish` imports.
|
|
2
|
+
//!
|
|
3
|
+
//! See docs/LANGUAGE.md (platform extensions). Context is process-wide so existing
|
|
4
|
+
//! `resolve_project` callers pick up `--platform` / `--surface` / env without signature churn.
|
|
5
|
+
|
|
6
|
+
use std::path::{Path, PathBuf};
|
|
7
|
+
use std::sync::{Mutex, OnceLock};
|
|
8
|
+
|
|
9
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
10
|
+
pub enum Platform {
|
|
11
|
+
#[default]
|
|
12
|
+
Unknown,
|
|
13
|
+
Macos,
|
|
14
|
+
Ios,
|
|
15
|
+
Android,
|
|
16
|
+
Windows,
|
|
17
|
+
Linux,
|
|
18
|
+
Web,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
22
|
+
pub enum Surface {
|
|
23
|
+
#[default]
|
|
24
|
+
Unknown,
|
|
25
|
+
Native,
|
|
26
|
+
Webview,
|
|
27
|
+
Web,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
31
|
+
pub struct ResolveContext {
|
|
32
|
+
pub platform: Platform,
|
|
33
|
+
pub surface: Surface,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn resolve_ctx_lock() -> &'static Mutex<ResolveContext> {
|
|
37
|
+
static RESOLVE_CTX: OnceLock<Mutex<ResolveContext>> = OnceLock::new();
|
|
38
|
+
RESOLVE_CTX.get_or_init(|| Mutex::new(ResolveContext::default()))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
pub fn set_resolve_context(ctx: ResolveContext) {
|
|
42
|
+
*resolve_ctx_lock().lock().unwrap_or_else(|e| e.into_inner()) = ctx;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
pub fn resolve_context() -> ResolveContext {
|
|
46
|
+
*resolve_ctx_lock().lock().unwrap_or_else(|e| e.into_inner())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
pub fn parse_platform(s: &str) -> Option<Platform> {
|
|
50
|
+
match s.trim().to_ascii_lowercase().as_str() {
|
|
51
|
+
"macos" | "darwin" | "osx" => Some(Platform::Macos),
|
|
52
|
+
"ios" => Some(Platform::Ios),
|
|
53
|
+
"android" => Some(Platform::Android),
|
|
54
|
+
"windows" | "win32" | "win" => Some(Platform::Windows),
|
|
55
|
+
"linux" => Some(Platform::Linux),
|
|
56
|
+
"web" => Some(Platform::Web),
|
|
57
|
+
"" | "unknown" | "default" => Some(Platform::Unknown),
|
|
58
|
+
_ => None,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pub fn parse_surface(s: &str) -> Option<Surface> {
|
|
63
|
+
match s.trim().to_ascii_lowercase().as_str() {
|
|
64
|
+
"native" => Some(Surface::Native),
|
|
65
|
+
"webview" => Some(Surface::Webview),
|
|
66
|
+
"web" => Some(Surface::Web),
|
|
67
|
+
"" | "unknown" | "default" => Some(Surface::Unknown),
|
|
68
|
+
_ => None,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Apply CLI/env into the process resolve context.
|
|
73
|
+
/// `TISH_PLATFORM` / `TISH_SURFACE` fill gaps when args are omitted.
|
|
74
|
+
pub fn apply_resolve_env(platform: Option<&str>, surface: Option<&str>) -> Result<(), String> {
|
|
75
|
+
let mut ctx = resolve_context();
|
|
76
|
+
if let Some(p) = platform {
|
|
77
|
+
ctx.platform = parse_platform(p).ok_or_else(|| format!("unknown --platform '{p}'"))?;
|
|
78
|
+
} else if let Ok(p) = std::env::var("TISH_PLATFORM") {
|
|
79
|
+
if let Some(parsed) = parse_platform(&p) {
|
|
80
|
+
ctx.platform = parsed;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if let Some(s) = surface {
|
|
84
|
+
ctx.surface = parse_surface(s).ok_or_else(|| format!("unknown --surface '{s}'"))?;
|
|
85
|
+
} else if let Ok(s) = std::env::var("TISH_SURFACE") {
|
|
86
|
+
if let Some(parsed) = parse_surface(&s) {
|
|
87
|
+
ctx.surface = parsed;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// `--platform web` implies surface web when surface unset
|
|
91
|
+
if ctx.platform == Platform::Web && ctx.surface == Surface::Unknown {
|
|
92
|
+
ctx.surface = Surface::Web;
|
|
93
|
+
}
|
|
94
|
+
set_resolve_context(ctx);
|
|
95
|
+
Ok(())
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Suffix tokens to try (before bare `.tish`), most specific first.
|
|
99
|
+
pub fn platform_suffixes(ctx: ResolveContext) -> Vec<&'static str> {
|
|
100
|
+
let mut out = Vec::new();
|
|
101
|
+
match (ctx.platform, ctx.surface) {
|
|
102
|
+
(_, Surface::Web) | (Platform::Web, _) => {
|
|
103
|
+
out.push("web");
|
|
104
|
+
}
|
|
105
|
+
(Platform::Macos, Surface::Native) | (Platform::Macos, Surface::Unknown) => {
|
|
106
|
+
out.extend(["macos", "desktop", "native"]);
|
|
107
|
+
}
|
|
108
|
+
(Platform::Ios, Surface::Native) | (Platform::Ios, Surface::Unknown) => {
|
|
109
|
+
out.extend(["ios", "mobile", "native"]);
|
|
110
|
+
}
|
|
111
|
+
(Platform::Android, Surface::Native) | (Platform::Android, Surface::Unknown) => {
|
|
112
|
+
out.extend(["android", "mobile", "native"]);
|
|
113
|
+
}
|
|
114
|
+
(Platform::Windows, Surface::Native) | (Platform::Windows, Surface::Unknown) => {
|
|
115
|
+
out.extend(["windows", "desktop", "native"]);
|
|
116
|
+
}
|
|
117
|
+
(Platform::Linux, Surface::Native) | (Platform::Linux, Surface::Unknown) => {
|
|
118
|
+
out.extend(["linux", "desktop", "native"]);
|
|
119
|
+
}
|
|
120
|
+
(Platform::Macos, Surface::Webview)
|
|
121
|
+
| (Platform::Windows, Surface::Webview)
|
|
122
|
+
| (Platform::Linux, Surface::Webview)
|
|
123
|
+
| (Platform::Unknown, Surface::Webview) => {
|
|
124
|
+
out.extend(["webview", "web", "desktop"]);
|
|
125
|
+
}
|
|
126
|
+
(Platform::Ios, Surface::Webview) | (Platform::Android, Surface::Webview) => {
|
|
127
|
+
out.extend(["webview", "web", "mobile"]);
|
|
128
|
+
}
|
|
129
|
+
_ => {}
|
|
130
|
+
}
|
|
131
|
+
out
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// After normalizing a virtual path to a stem (no `.tish`), list cascade keys to probe.
|
|
135
|
+
/// `normalized_stem` is e.g. `app/Button` (no extension).
|
|
136
|
+
pub fn platform_virtual_keys(normalized_stem: &str, ctx: ResolveContext) -> Vec<String> {
|
|
137
|
+
let parent = Path::new(normalized_stem)
|
|
138
|
+
.parent()
|
|
139
|
+
.map(|p| p.to_string_lossy().to_string())
|
|
140
|
+
.filter(|p| !p.is_empty() && p != ".");
|
|
141
|
+
let file_stem = Path::new(normalized_stem)
|
|
142
|
+
.file_name()
|
|
143
|
+
.and_then(|s| s.to_str())
|
|
144
|
+
.unwrap_or("index");
|
|
145
|
+
let mut keys = Vec::new();
|
|
146
|
+
for suffix in platform_suffixes(ctx) {
|
|
147
|
+
keys.push(match &parent {
|
|
148
|
+
Some(p) => format!("{p}/{file_stem}.{suffix}.tish"),
|
|
149
|
+
None => format!("{file_stem}.{suffix}.tish"),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
keys.push(match &parent {
|
|
153
|
+
Some(p) => format!("{p}/{file_stem}.tish"),
|
|
154
|
+
None => format!("{file_stem}.tish"),
|
|
155
|
+
});
|
|
156
|
+
keys
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// Resolve a relative import specifier to an on-disk path, trying platform variants.
|
|
160
|
+
///
|
|
161
|
+
/// `spec` is e.g. `./Button` or `./Button.tish`. Never picks native-only suffixes for web surface.
|
|
162
|
+
pub fn resolve_with_platform(
|
|
163
|
+
spec: &str,
|
|
164
|
+
from_dir: &Path,
|
|
165
|
+
ctx: ResolveContext,
|
|
166
|
+
) -> Option<PathBuf> {
|
|
167
|
+
let joined = from_dir.join(spec);
|
|
168
|
+
let (stem_path, had_tish_ext) = if joined.extension().and_then(|e| e.to_str()) == Some("tish")
|
|
169
|
+
{
|
|
170
|
+
(joined.with_extension(""), true)
|
|
171
|
+
} else if joined.extension().is_none() {
|
|
172
|
+
(joined, false)
|
|
173
|
+
} else {
|
|
174
|
+
// Other extensions (.js, .css): leave to caller / no platform remap
|
|
175
|
+
return if joined.exists() {
|
|
176
|
+
joined.canonicalize().ok()
|
|
177
|
+
} else {
|
|
178
|
+
None
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
let parent = stem_path.parent().unwrap_or(from_dir);
|
|
183
|
+
let file_stem = stem_path
|
|
184
|
+
.file_name()
|
|
185
|
+
.and_then(|s| s.to_str())
|
|
186
|
+
.unwrap_or("index");
|
|
187
|
+
|
|
188
|
+
for suffix in platform_suffixes(ctx) {
|
|
189
|
+
let candidate = parent.join(format!("{file_stem}.{suffix}.tish"));
|
|
190
|
+
if candidate.exists() {
|
|
191
|
+
return candidate.canonicalize().ok();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let base = parent.join(format!("{file_stem}.tish"));
|
|
196
|
+
if base.exists() {
|
|
197
|
+
return base.canonicalize().ok();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Explicit path without remap that already exists (edge)
|
|
201
|
+
if had_tish_ext {
|
|
202
|
+
let explicit = from_dir.join(spec);
|
|
203
|
+
if explicit.exists() {
|
|
204
|
+
return explicit.canonicalize().ok();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
None
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/// Public one-shot for CLI / Vite: resolve `importer` + `source` → absolute path string.
|
|
212
|
+
pub fn resolve_id_public(
|
|
213
|
+
source: &str,
|
|
214
|
+
importer: Option<&str>,
|
|
215
|
+
platform: Option<&str>,
|
|
216
|
+
surface: Option<&str>,
|
|
217
|
+
) -> Result<String, String> {
|
|
218
|
+
apply_resolve_env(platform, surface)?;
|
|
219
|
+
let ctx = resolve_context();
|
|
220
|
+
let from_dir = if let Some(imp) = importer {
|
|
221
|
+
Path::new(imp)
|
|
222
|
+
.parent()
|
|
223
|
+
.map(Path::to_path_buf)
|
|
224
|
+
.unwrap_or_else(|| PathBuf::from("."))
|
|
225
|
+
} else {
|
|
226
|
+
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
if !source.starts_with("./") && !source.starts_with("../") {
|
|
230
|
+
return Err(format!(
|
|
231
|
+
"resolve-id only handles relative imports (got '{source}')"
|
|
232
|
+
));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
resolve_with_platform(source, &from_dir, ctx)
|
|
236
|
+
.map(|p| p.display().to_string())
|
|
237
|
+
.ok_or_else(|| {
|
|
238
|
+
let tried: Vec<_> = platform_suffixes(ctx)
|
|
239
|
+
.into_iter()
|
|
240
|
+
.map(|s| format!("*.{s}.tish"))
|
|
241
|
+
.collect();
|
|
242
|
+
format!(
|
|
243
|
+
"Cannot resolve '{source}' from {} (tried {} then *.tish)",
|
|
244
|
+
from_dir.display(),
|
|
245
|
+
if tried.is_empty() {
|
|
246
|
+
"(no platform suffixes)".into()
|
|
247
|
+
} else {
|
|
248
|
+
tried.join(", ")
|
|
249
|
+
}
|
|
250
|
+
)
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#[cfg(test)]
|
|
255
|
+
mod tests {
|
|
256
|
+
use super::*;
|
|
257
|
+
use std::fs;
|
|
258
|
+
use tempfile::tempdir;
|
|
259
|
+
|
|
260
|
+
#[test]
|
|
261
|
+
fn macos_native_prefers_macos_then_desktop() {
|
|
262
|
+
let dir = tempdir().unwrap();
|
|
263
|
+
let root = dir.path();
|
|
264
|
+
fs::write(root.join("Button.desktop.tish"), "").unwrap();
|
|
265
|
+
fs::write(root.join("Button.macos.tish"), "").unwrap();
|
|
266
|
+
fs::write(root.join("Button.tish"), "").unwrap();
|
|
267
|
+
let ctx = ResolveContext {
|
|
268
|
+
platform: Platform::Macos,
|
|
269
|
+
surface: Surface::Native,
|
|
270
|
+
};
|
|
271
|
+
let p = resolve_with_platform("./Button", root, ctx).unwrap();
|
|
272
|
+
assert!(p.ends_with("Button.macos.tish"));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#[test]
|
|
276
|
+
fn webview_reuses_web_not_macos() {
|
|
277
|
+
let dir = tempdir().unwrap();
|
|
278
|
+
let root = dir.path();
|
|
279
|
+
fs::write(root.join("Button.macos.tish"), "").unwrap();
|
|
280
|
+
fs::write(root.join("Button.web.tish"), "").unwrap();
|
|
281
|
+
let ctx = ResolveContext {
|
|
282
|
+
platform: Platform::Macos,
|
|
283
|
+
surface: Surface::Webview,
|
|
284
|
+
};
|
|
285
|
+
let p = resolve_with_platform("./Button", root, ctx).unwrap();
|
|
286
|
+
assert!(p.ends_with("Button.web.tish"));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
#[test]
|
|
290
|
+
fn web_surface_only_web_and_base() {
|
|
291
|
+
let dir = tempdir().unwrap();
|
|
292
|
+
let root = dir.path();
|
|
293
|
+
fs::write(root.join("Button.macos.tish"), "").unwrap();
|
|
294
|
+
fs::write(root.join("Button.web.tish"), "").unwrap();
|
|
295
|
+
let ctx = ResolveContext {
|
|
296
|
+
platform: Platform::Web,
|
|
297
|
+
surface: Surface::Web,
|
|
298
|
+
};
|
|
299
|
+
let p = resolve_with_platform("./Button.tish", root, ctx).unwrap();
|
|
300
|
+
assert!(p.ends_with("Button.web.tish"));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
#[test]
|
|
304
|
+
fn android_native_prefers_android_then_mobile() {
|
|
305
|
+
let dir = tempdir().unwrap();
|
|
306
|
+
let root = dir.path();
|
|
307
|
+
fs::write(root.join("Button.mobile.tish"), "").unwrap();
|
|
308
|
+
fs::write(root.join("Button.android.tish"), "").unwrap();
|
|
309
|
+
fs::write(root.join("Button.tish"), "").unwrap();
|
|
310
|
+
let ctx = ResolveContext {
|
|
311
|
+
platform: Platform::Android,
|
|
312
|
+
surface: Surface::Native,
|
|
313
|
+
};
|
|
314
|
+
let p = resolve_with_platform("./Button", root, ctx).unwrap();
|
|
315
|
+
assert!(p.ends_with("Button.android.tish"));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
#[test]
|
|
319
|
+
fn ios_native_falls_back_to_mobile() {
|
|
320
|
+
let dir = tempdir().unwrap();
|
|
321
|
+
let root = dir.path();
|
|
322
|
+
fs::write(root.join("Button.mobile.tish"), "").unwrap();
|
|
323
|
+
fs::write(root.join("Button.tish"), "").unwrap();
|
|
324
|
+
let ctx = ResolveContext {
|
|
325
|
+
platform: Platform::Ios,
|
|
326
|
+
surface: Surface::Native,
|
|
327
|
+
};
|
|
328
|
+
let p = resolve_with_platform("./Button", root, ctx).unwrap();
|
|
329
|
+
assert!(p.ends_with("Button.mobile.tish"));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
#[test]
|
|
333
|
+
fn virtual_keys_cascade_with_and_without_parent() {
|
|
334
|
+
// nested path keeps the parent dir; macos/native → macos, desktop, native, then base
|
|
335
|
+
let macos = platform_virtual_keys(
|
|
336
|
+
"app/Button",
|
|
337
|
+
ResolveContext {
|
|
338
|
+
platform: Platform::Macos,
|
|
339
|
+
surface: Surface::Native,
|
|
340
|
+
},
|
|
341
|
+
);
|
|
342
|
+
assert_eq!(
|
|
343
|
+
macos,
|
|
344
|
+
vec![
|
|
345
|
+
"app/Button.macos.tish",
|
|
346
|
+
"app/Button.desktop.tish",
|
|
347
|
+
"app/Button.native.tish",
|
|
348
|
+
"app/Button.tish",
|
|
349
|
+
]
|
|
350
|
+
);
|
|
351
|
+
// bare stem (no parent) + web surface → web, then base
|
|
352
|
+
let web = platform_virtual_keys(
|
|
353
|
+
"Button",
|
|
354
|
+
ResolveContext {
|
|
355
|
+
platform: Platform::Web,
|
|
356
|
+
surface: Surface::Web,
|
|
357
|
+
},
|
|
358
|
+
);
|
|
359
|
+
assert_eq!(web, vec!["Button.web.tish", "Button.tish"]);
|
|
360
|
+
}
|
|
361
|
+
}
|