@tishlang/tish 1.0.7 → 1.0.10
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 +43 -0
- package/LICENSE +13 -0
- package/README.md +66 -0
- package/crates/js_to_tish/Cargo.toml +9 -0
- package/crates/js_to_tish/README.md +18 -0
- package/crates/js_to_tish/src/error.rs +61 -0
- package/crates/js_to_tish/src/lib.rs +11 -0
- package/crates/js_to_tish/src/span_util.rs +35 -0
- package/crates/js_to_tish/src/transform/expr.rs +608 -0
- package/crates/js_to_tish/src/transform/stmt.rs +474 -0
- package/crates/js_to_tish/src/transform.rs +60 -0
- package/crates/tish/Cargo.toml +44 -0
- package/crates/tish/src/main.rs +585 -0
- package/crates/tish/src/repl_completion.rs +200 -0
- package/crates/tish/tests/integration_test.rs +726 -0
- package/crates/tish_ast/Cargo.toml +7 -0
- package/crates/tish_ast/src/ast.rs +494 -0
- package/crates/tish_ast/src/lib.rs +5 -0
- package/crates/tish_build_utils/Cargo.toml +5 -0
- package/crates/tish_build_utils/src/lib.rs +175 -0
- package/crates/tish_builtins/Cargo.toml +12 -0
- package/crates/tish_builtins/src/array.rs +410 -0
- package/crates/tish_builtins/src/globals.rs +197 -0
- package/crates/tish_builtins/src/helpers.rs +38 -0
- package/crates/tish_builtins/src/lib.rs +14 -0
- package/crates/tish_builtins/src/math.rs +80 -0
- package/crates/tish_builtins/src/object.rs +36 -0
- package/crates/tish_builtins/src/string.rs +253 -0
- package/crates/tish_bytecode/Cargo.toml +15 -0
- package/crates/tish_bytecode/src/chunk.rs +97 -0
- package/crates/tish_bytecode/src/compiler.rs +1361 -0
- package/crates/tish_bytecode/src/encoding.rs +100 -0
- package/crates/tish_bytecode/src/lib.rs +19 -0
- package/crates/tish_bytecode/src/opcode.rs +110 -0
- package/crates/tish_bytecode/src/peephole.rs +159 -0
- package/crates/tish_bytecode/src/serialize.rs +163 -0
- package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
- package/crates/tish_bytecode/tests/shortcircuit.rs +49 -0
- package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
- package/crates/tish_compile/Cargo.toml +21 -0
- package/crates/tish_compile/src/codegen.rs +3316 -0
- package/crates/tish_compile/src/lib.rs +71 -0
- package/crates/tish_compile/src/resolve.rs +631 -0
- package/crates/tish_compile/src/types.rs +304 -0
- package/crates/tish_compile_js/Cargo.toml +16 -0
- package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
- package/crates/tish_compile_js/src/codegen.rs +794 -0
- package/crates/tish_compile_js/src/error.rs +20 -0
- package/crates/tish_compile_js/src/js_intrinsics.rs +82 -0
- package/crates/tish_compile_js/src/lib.rs +27 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +32 -0
- package/crates/tish_compiler_wasm/Cargo.toml +19 -0
- package/crates/tish_compiler_wasm/src/lib.rs +55 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +462 -0
- package/crates/tish_core/Cargo.toml +11 -0
- package/crates/tish_core/src/console_style.rs +128 -0
- package/crates/tish_core/src/json.rs +327 -0
- package/crates/tish_core/src/lib.rs +15 -0
- package/crates/tish_core/src/macros.rs +37 -0
- package/crates/tish_core/src/uri.rs +115 -0
- package/crates/tish_core/src/value.rs +376 -0
- package/crates/tish_cranelift/Cargo.toml +17 -0
- package/crates/tish_cranelift/src/lib.rs +41 -0
- package/crates/tish_cranelift/src/link.rs +120 -0
- package/crates/tish_cranelift/src/lower.rs +77 -0
- package/crates/tish_cranelift_runtime/Cargo.toml +19 -0
- package/crates/tish_cranelift_runtime/src/lib.rs +43 -0
- package/crates/tish_eval/Cargo.toml +26 -0
- package/crates/tish_eval/src/eval.rs +3205 -0
- package/crates/tish_eval/src/http.rs +122 -0
- package/crates/tish_eval/src/lib.rs +59 -0
- package/crates/tish_eval/src/natives.rs +301 -0
- package/crates/tish_eval/src/promise.rs +173 -0
- package/crates/tish_eval/src/regex.rs +298 -0
- package/crates/tish_eval/src/timers.rs +111 -0
- package/crates/tish_eval/src/value.rs +224 -0
- package/crates/tish_eval/src/value_convert.rs +85 -0
- package/crates/tish_fmt/Cargo.toml +16 -0
- package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
- package/crates/tish_fmt/src/lib.rs +884 -0
- package/crates/tish_jsx_web/Cargo.toml +7 -0
- package/crates/tish_jsx_web/README.md +18 -0
- package/crates/tish_jsx_web/src/lib.rs +157 -0
- package/crates/tish_jsx_web/vendor/Lattish.tish +347 -0
- package/crates/tish_lexer/Cargo.toml +7 -0
- package/crates/tish_lexer/src/lib.rs +430 -0
- package/crates/tish_lexer/src/token.rs +155 -0
- package/crates/tish_lint/Cargo.toml +17 -0
- package/crates/tish_lint/src/bin/tish-lint.rs +77 -0
- package/crates/tish_lint/src/lib.rs +278 -0
- package/crates/tish_llvm/Cargo.toml +11 -0
- package/crates/tish_llvm/src/lib.rs +106 -0
- package/crates/tish_lsp/Cargo.toml +22 -0
- package/crates/tish_lsp/README.md +26 -0
- package/crates/tish_lsp/src/main.rs +615 -0
- package/crates/tish_native/Cargo.toml +14 -0
- package/crates/tish_native/src/build.rs +102 -0
- package/crates/tish_native/src/lib.rs +237 -0
- package/crates/tish_opt/Cargo.toml +11 -0
- package/crates/tish_opt/src/lib.rs +896 -0
- package/crates/tish_parser/Cargo.toml +9 -0
- package/crates/tish_parser/src/lib.rs +123 -0
- package/crates/tish_parser/src/parser.rs +1714 -0
- package/crates/tish_runtime/Cargo.toml +26 -0
- package/crates/tish_runtime/src/http.rs +308 -0
- package/crates/tish_runtime/src/http_fetch.rs +453 -0
- package/crates/tish_runtime/src/lib.rs +1004 -0
- package/crates/tish_runtime/src/native_promise.rs +26 -0
- package/crates/tish_runtime/src/promise.rs +77 -0
- package/crates/tish_runtime/src/promise_io.rs +41 -0
- package/crates/tish_runtime/src/timers.rs +125 -0
- package/crates/tish_runtime/src/ws.rs +725 -0
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +99 -0
- package/crates/tish_vm/Cargo.toml +31 -0
- package/crates/tish_vm/src/lib.rs +39 -0
- package/crates/tish_vm/src/vm.rs +1399 -0
- package/crates/tish_wasm/Cargo.toml +13 -0
- package/crates/tish_wasm/src/lib.rs +358 -0
- package/crates/tish_wasm_runtime/Cargo.toml +25 -0
- package/crates/tish_wasm_runtime/src/lib.rs +36 -0
- package/justfile +260 -0
- package/package.json +8 -3
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
//! Virtual module resolution for browser/playground. No filesystem.
|
|
2
|
+
//! Resolves imports from an in-memory file map, merges modules into a single Program.
|
|
3
|
+
|
|
4
|
+
use std::collections::{HashMap, HashSet};
|
|
5
|
+
use std::sync::Arc;
|
|
6
|
+
|
|
7
|
+
use tish_ast::{ExportDeclaration, Expr, ImportSpecifier, Program, Statement};
|
|
8
|
+
|
|
9
|
+
/// A resolved module: virtual path and its parsed program.
|
|
10
|
+
#[derive(Debug, Clone)]
|
|
11
|
+
pub struct VirtualModule {
|
|
12
|
+
pub path: String,
|
|
13
|
+
pub program: Program,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// Node-compatible aliases for built-in modules (fs -> tish:fs, etc.).
|
|
17
|
+
const BUILTIN_ALIASES: &[(&str, &str)] = &[
|
|
18
|
+
("fs", "tish:fs"),
|
|
19
|
+
("http", "tish:http"),
|
|
20
|
+
("process", "tish:process"),
|
|
21
|
+
("ws", "tish:ws"),
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
fn normalize_builtin_spec(spec: &str) -> Option<String> {
|
|
25
|
+
if spec.starts_with("tish:") {
|
|
26
|
+
return Some(spec.to_string());
|
|
27
|
+
}
|
|
28
|
+
BUILTIN_ALIASES
|
|
29
|
+
.iter()
|
|
30
|
+
.find(|(alias, _)| *alias == spec)
|
|
31
|
+
.map(|(_, canonical)| (*canonical).to_string())
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fn is_native_import(spec: &str) -> bool {
|
|
35
|
+
spec.starts_with("tish:")
|
|
36
|
+
|| spec.starts_with('@')
|
|
37
|
+
|| matches!(spec, "fs" | "http" | "process" | "ws")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Normalize a virtual path: resolve . and .. components.
|
|
41
|
+
/// e.g. "sub/../lib.tish" -> "lib.tish", "./foo.tish" -> "foo.tish"
|
|
42
|
+
fn normalize_virtual_path(from_dir: &str, spec: &str) -> Result<String, String> {
|
|
43
|
+
if !spec.starts_with("./") && !spec.starts_with("../") {
|
|
44
|
+
return Err(format!(
|
|
45
|
+
"Only relative imports (./, ../) or native imports (tish:*, @scope/pkg) are supported. Got: {}",
|
|
46
|
+
spec
|
|
47
|
+
));
|
|
48
|
+
}
|
|
49
|
+
let combined = if from_dir.is_empty() {
|
|
50
|
+
spec.to_string()
|
|
51
|
+
} else {
|
|
52
|
+
format!("{}/{}", from_dir, spec)
|
|
53
|
+
};
|
|
54
|
+
let parts: Vec<&str> = combined.split('/').collect();
|
|
55
|
+
let mut stack: Vec<&str> = Vec::new();
|
|
56
|
+
for p in parts {
|
|
57
|
+
match p {
|
|
58
|
+
"" | "." => {}
|
|
59
|
+
".." => {
|
|
60
|
+
stack.pop();
|
|
61
|
+
}
|
|
62
|
+
_ => stack.push(p),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
Ok(stack.join("/"))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Parent directory of a virtual path. "main.tish" -> "", "sub/foo.tish" -> "sub"
|
|
69
|
+
fn parent_dir(path: &str) -> &str {
|
|
70
|
+
if let Some(slash) = path.rfind('/') {
|
|
71
|
+
&path[..slash]
|
|
72
|
+
} else {
|
|
73
|
+
""
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Resolve import spec to a key for the files map. Tries .tish extension if missing.
|
|
78
|
+
fn resolve_import_to_key(
|
|
79
|
+
spec: &str,
|
|
80
|
+
from_dir: &str,
|
|
81
|
+
files: &HashMap<String, String>,
|
|
82
|
+
) -> Result<String, String> {
|
|
83
|
+
let normalized = normalize_virtual_path(from_dir, spec)?;
|
|
84
|
+
if files.contains_key(&normalized) {
|
|
85
|
+
return Ok(normalized);
|
|
86
|
+
}
|
|
87
|
+
if !normalized.ends_with(".tish") && !normalized.contains('.') {
|
|
88
|
+
let with_ext = format!("{}.tish", normalized);
|
|
89
|
+
if files.contains_key(&with_ext) {
|
|
90
|
+
return Ok(with_ext);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
Err(format!(
|
|
94
|
+
"Cannot resolve import '{}' from {}: file not in virtual file map",
|
|
95
|
+
spec, from_dir
|
|
96
|
+
))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Resolve all modules starting from the entry file. Returns modules in dependency order.
|
|
100
|
+
pub fn resolve_virtual(
|
|
101
|
+
entry_path: &str,
|
|
102
|
+
files: &HashMap<String, String>,
|
|
103
|
+
) -> Result<Vec<VirtualModule>, String> {
|
|
104
|
+
let entry_key = if files.contains_key(entry_path) {
|
|
105
|
+
entry_path.to_string()
|
|
106
|
+
} else if !entry_path.ends_with(".tish") {
|
|
107
|
+
let with_ext = format!("{}.tish", entry_path);
|
|
108
|
+
if files.contains_key(&with_ext) {
|
|
109
|
+
with_ext
|
|
110
|
+
} else {
|
|
111
|
+
return Err(format!("Entry file '{}' not in virtual file map", entry_path));
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
return Err(format!("Entry file '{}' not in virtual file map", entry_path));
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
let mut visited = HashSet::new();
|
|
118
|
+
let mut path_to_module: HashMap<String, Program> = HashMap::new();
|
|
119
|
+
let mut load_order: Vec<String> = Vec::new();
|
|
120
|
+
|
|
121
|
+
load_module_recursive(
|
|
122
|
+
&entry_key,
|
|
123
|
+
files,
|
|
124
|
+
&mut visited,
|
|
125
|
+
&mut path_to_module,
|
|
126
|
+
&mut load_order,
|
|
127
|
+
)?;
|
|
128
|
+
|
|
129
|
+
Ok(load_order
|
|
130
|
+
.into_iter()
|
|
131
|
+
.map(|p| {
|
|
132
|
+
let program = path_to_module.remove(&p).unwrap();
|
|
133
|
+
VirtualModule { path: p, program }
|
|
134
|
+
})
|
|
135
|
+
.collect())
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fn load_module_recursive(
|
|
139
|
+
module_path: &str,
|
|
140
|
+
files: &HashMap<String, String>,
|
|
141
|
+
visited: &mut HashSet<String>,
|
|
142
|
+
path_to_module: &mut HashMap<String, Program>,
|
|
143
|
+
load_order: &mut Vec<String>,
|
|
144
|
+
) -> Result<(), String> {
|
|
145
|
+
if visited.contains(module_path) {
|
|
146
|
+
return Ok(());
|
|
147
|
+
}
|
|
148
|
+
visited.insert(module_path.to_string());
|
|
149
|
+
|
|
150
|
+
let source = files.get(module_path).ok_or_else(|| {
|
|
151
|
+
format!("Module '{}' not in virtual file map", module_path)
|
|
152
|
+
})?;
|
|
153
|
+
let program = tish_parser::parse(source.trim())
|
|
154
|
+
.map_err(|e| format!("Parse error in {}: {}", module_path, e))?;
|
|
155
|
+
|
|
156
|
+
let from_dir = parent_dir(module_path);
|
|
157
|
+
for stmt in &program.statements {
|
|
158
|
+
if let Statement::Import { from, .. } = stmt {
|
|
159
|
+
if is_native_import(from) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
let dep_key = resolve_import_to_key(from, from_dir, files)?;
|
|
163
|
+
if !path_to_module.contains_key(&dep_key) {
|
|
164
|
+
load_module_recursive(
|
|
165
|
+
&dep_key,
|
|
166
|
+
files,
|
|
167
|
+
visited,
|
|
168
|
+
path_to_module,
|
|
169
|
+
load_order,
|
|
170
|
+
)?;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
path_to_module.insert(module_path.to_string(), program);
|
|
176
|
+
load_order.push(module_path.to_string());
|
|
177
|
+
Ok(())
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// Check for cyclic imports.
|
|
181
|
+
pub fn detect_cycles_virtual(modules: &[VirtualModule]) -> Result<(), String> {
|
|
182
|
+
let path_to_idx: HashMap<_, _> = modules
|
|
183
|
+
.iter()
|
|
184
|
+
.enumerate()
|
|
185
|
+
.map(|(i, m)| (m.path.clone(), i))
|
|
186
|
+
.collect();
|
|
187
|
+
|
|
188
|
+
for (idx, module) in modules.iter().enumerate() {
|
|
189
|
+
let dir = parent_dir(&module.path);
|
|
190
|
+
let mut stack = vec![idx];
|
|
191
|
+
if has_cycle_from(
|
|
192
|
+
dir,
|
|
193
|
+
&module.program,
|
|
194
|
+
&path_to_idx,
|
|
195
|
+
modules,
|
|
196
|
+
&mut stack,
|
|
197
|
+
&mut HashSet::new(),
|
|
198
|
+
)? {
|
|
199
|
+
let path_names: Vec<_> = stack
|
|
200
|
+
.iter()
|
|
201
|
+
.map(|&i| modules[i].path.clone())
|
|
202
|
+
.collect();
|
|
203
|
+
return Err(format!("Circular import detected: {}", path_names.join(" -> ")));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
Ok(())
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fn has_cycle_from(
|
|
210
|
+
from_dir: &str,
|
|
211
|
+
program: &Program,
|
|
212
|
+
path_to_idx: &HashMap<String, usize>,
|
|
213
|
+
modules: &[VirtualModule],
|
|
214
|
+
stack: &mut Vec<usize>,
|
|
215
|
+
visiting: &mut HashSet<usize>,
|
|
216
|
+
) -> Result<bool, String> {
|
|
217
|
+
for stmt in &program.statements {
|
|
218
|
+
if let Statement::Import { from, .. } = stmt {
|
|
219
|
+
if is_native_import(from) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
let dep_key = resolve_import_to_key_for_cycle(from, from_dir, path_to_idx)?;
|
|
223
|
+
if let Some(&dep_idx) = path_to_idx.get(&dep_key) {
|
|
224
|
+
if stack.contains(&dep_idx) {
|
|
225
|
+
stack.push(dep_idx);
|
|
226
|
+
return Ok(true);
|
|
227
|
+
}
|
|
228
|
+
if !visiting.contains(&dep_idx) {
|
|
229
|
+
visiting.insert(dep_idx);
|
|
230
|
+
stack.push(dep_idx);
|
|
231
|
+
let dep = &modules[dep_idx];
|
|
232
|
+
let dep_dir = parent_dir(&dep.path);
|
|
233
|
+
if has_cycle_from(
|
|
234
|
+
dep_dir,
|
|
235
|
+
&dep.program,
|
|
236
|
+
path_to_idx,
|
|
237
|
+
modules,
|
|
238
|
+
stack,
|
|
239
|
+
visiting,
|
|
240
|
+
)? {
|
|
241
|
+
return Ok(true);
|
|
242
|
+
}
|
|
243
|
+
stack.pop();
|
|
244
|
+
visiting.remove(&dep_idx);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
Ok(false)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fn resolve_import_to_key_for_cycle(
|
|
253
|
+
spec: &str,
|
|
254
|
+
from_dir: &str,
|
|
255
|
+
path_to_idx: &HashMap<String, usize>,
|
|
256
|
+
) -> Result<String, String> {
|
|
257
|
+
let normalized = normalize_virtual_path(from_dir, spec)?;
|
|
258
|
+
if path_to_idx.contains_key(&normalized) {
|
|
259
|
+
return Ok(normalized);
|
|
260
|
+
}
|
|
261
|
+
if !normalized.ends_with(".tish") && !normalized.contains('.') {
|
|
262
|
+
let with_ext = format!("{}.tish", normalized);
|
|
263
|
+
if path_to_idx.contains_key(&with_ext) {
|
|
264
|
+
return Ok(with_ext);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
Err(format!(
|
|
268
|
+
"Cannot resolve import '{}' from {}: module not in resolved set",
|
|
269
|
+
spec, from_dir
|
|
270
|
+
))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/// Merge all resolved modules into a single program. Dependencies are emitted first.
|
|
274
|
+
pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, String> {
|
|
275
|
+
let path_to_idx: HashMap<String, usize> = modules
|
|
276
|
+
.iter()
|
|
277
|
+
.enumerate()
|
|
278
|
+
.map(|(i, m)| (m.path.clone(), i))
|
|
279
|
+
.collect();
|
|
280
|
+
|
|
281
|
+
let mut module_exports: Vec<HashMap<String, String>> = vec![HashMap::new(); modules.len()];
|
|
282
|
+
for (idx, module) in modules.iter().enumerate() {
|
|
283
|
+
for stmt in &module.program.statements {
|
|
284
|
+
if let Statement::Export { declaration, .. } = stmt {
|
|
285
|
+
match declaration.as_ref() {
|
|
286
|
+
ExportDeclaration::Named(s) => {
|
|
287
|
+
let name = match s.as_ref() {
|
|
288
|
+
Statement::VarDecl { name, .. } | Statement::FunDecl { name, .. } => {
|
|
289
|
+
name.to_string()
|
|
290
|
+
}
|
|
291
|
+
_ => continue,
|
|
292
|
+
};
|
|
293
|
+
module_exports[idx].insert(name.clone(), name);
|
|
294
|
+
}
|
|
295
|
+
ExportDeclaration::Default(_) => {
|
|
296
|
+
let default_name = format!("__default_{}", idx);
|
|
297
|
+
module_exports[idx].insert("default".to_string(), default_name);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
let mut statements = Vec::new();
|
|
305
|
+
for (idx, module) in modules.iter().enumerate() {
|
|
306
|
+
let dir = parent_dir(&module.path);
|
|
307
|
+
for stmt in &module.program.statements {
|
|
308
|
+
match stmt {
|
|
309
|
+
Statement::Import { specifiers, from, span } => {
|
|
310
|
+
if is_native_import(from) {
|
|
311
|
+
let canonical_spec =
|
|
312
|
+
normalize_builtin_spec(from).unwrap_or_else(|| from.to_string());
|
|
313
|
+
for spec in specifiers {
|
|
314
|
+
match spec {
|
|
315
|
+
ImportSpecifier::Named { name, alias } => {
|
|
316
|
+
let bind = alias.as_deref().unwrap_or(name.as_ref());
|
|
317
|
+
let init = Expr::NativeModuleLoad {
|
|
318
|
+
spec: Arc::from(canonical_spec.clone()),
|
|
319
|
+
export_name: name.clone(),
|
|
320
|
+
span: *span,
|
|
321
|
+
};
|
|
322
|
+
statements.push(Statement::VarDecl {
|
|
323
|
+
name: Arc::from(bind),
|
|
324
|
+
mutable: false,
|
|
325
|
+
type_ann: None,
|
|
326
|
+
init: Some(init),
|
|
327
|
+
span: *span,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
ImportSpecifier::Namespace(ns) => {
|
|
331
|
+
return Err(format!(
|
|
332
|
+
"Namespace import (* as {}) not supported for native module '{}'",
|
|
333
|
+
ns.as_ref(),
|
|
334
|
+
from.as_ref()
|
|
335
|
+
));
|
|
336
|
+
}
|
|
337
|
+
ImportSpecifier::Default(bind) => {
|
|
338
|
+
return Err(format!(
|
|
339
|
+
"Default import '{}' not supported for native module '{}'. Use named import.",
|
|
340
|
+
bind.as_ref(),
|
|
341
|
+
from.as_ref()
|
|
342
|
+
));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
let dep_key = resolve_import_to_key_for_cycle(from, dir, &path_to_idx)?;
|
|
349
|
+
let dep_idx = *path_to_idx
|
|
350
|
+
.get(&dep_key)
|
|
351
|
+
.ok_or_else(|| format!("Resolved import '{}' not in module list", from))?;
|
|
352
|
+
let dep_exports = &module_exports[dep_idx];
|
|
353
|
+
for spec in specifiers {
|
|
354
|
+
match spec {
|
|
355
|
+
ImportSpecifier::Named { name, alias } => {
|
|
356
|
+
let source = dep_exports
|
|
357
|
+
.get(name.as_ref())
|
|
358
|
+
.cloned()
|
|
359
|
+
.unwrap_or_else(|| name.to_string());
|
|
360
|
+
let bind = alias.as_deref().unwrap_or(name.as_ref());
|
|
361
|
+
if bind != source {
|
|
362
|
+
statements.push(Statement::VarDecl {
|
|
363
|
+
name: Arc::from(bind),
|
|
364
|
+
mutable: false,
|
|
365
|
+
type_ann: None,
|
|
366
|
+
init: Some(Expr::Ident {
|
|
367
|
+
name: Arc::from(source),
|
|
368
|
+
span: *span,
|
|
369
|
+
}),
|
|
370
|
+
span: *span,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
ImportSpecifier::Namespace(ns) => {
|
|
375
|
+
let mut props = Vec::new();
|
|
376
|
+
for (k, v) in dep_exports {
|
|
377
|
+
props.push(tish_ast::ObjectProp::KeyValue(
|
|
378
|
+
Arc::from(k.clone()),
|
|
379
|
+
Expr::Ident {
|
|
380
|
+
name: Arc::from(v.clone()),
|
|
381
|
+
span: *span,
|
|
382
|
+
},
|
|
383
|
+
));
|
|
384
|
+
}
|
|
385
|
+
statements.push(Statement::VarDecl {
|
|
386
|
+
name: ns.clone(),
|
|
387
|
+
mutable: false,
|
|
388
|
+
type_ann: None,
|
|
389
|
+
init: Some(Expr::Object {
|
|
390
|
+
props,
|
|
391
|
+
span: *span,
|
|
392
|
+
}),
|
|
393
|
+
span: *span,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
ImportSpecifier::Default(bind) => {
|
|
397
|
+
let source = dep_exports
|
|
398
|
+
.get("default")
|
|
399
|
+
.cloned()
|
|
400
|
+
.ok_or_else(|| {
|
|
401
|
+
format!("Module '{}' has no default export", from)
|
|
402
|
+
})?;
|
|
403
|
+
statements.push(Statement::VarDecl {
|
|
404
|
+
name: bind.clone(),
|
|
405
|
+
mutable: false,
|
|
406
|
+
type_ann: None,
|
|
407
|
+
init: Some(Expr::Ident {
|
|
408
|
+
name: Arc::from(source),
|
|
409
|
+
span: *span,
|
|
410
|
+
}),
|
|
411
|
+
span: *span,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
Statement::Export { declaration, .. } => {
|
|
418
|
+
match declaration.as_ref() {
|
|
419
|
+
ExportDeclaration::Named(s) => statements.push(*s.clone()),
|
|
420
|
+
ExportDeclaration::Default(e) => {
|
|
421
|
+
let default_name = format!("__default_{}", idx);
|
|
422
|
+
statements.push(Statement::VarDecl {
|
|
423
|
+
name: Arc::from(default_name),
|
|
424
|
+
mutable: false,
|
|
425
|
+
type_ann: None,
|
|
426
|
+
init: Some((*e).clone()),
|
|
427
|
+
span: e.span(),
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
_ => statements.push(stmt.clone()),
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
Ok(Program { statements })
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
#[cfg(test)]
|
|
440
|
+
mod tests {
|
|
441
|
+
use super::*;
|
|
442
|
+
|
|
443
|
+
#[test]
|
|
444
|
+
fn test_resolve_virtual_simple_import() {
|
|
445
|
+
let mut files = HashMap::new();
|
|
446
|
+
files.insert(
|
|
447
|
+
"lib.tish".to_string(),
|
|
448
|
+
"export fn add(a, b) { return a + b }".to_string(),
|
|
449
|
+
);
|
|
450
|
+
files.insert(
|
|
451
|
+
"main.tish".to_string(),
|
|
452
|
+
"import { add } from \"./lib.tish\"\nconsole.log(add(1, 2))".to_string(),
|
|
453
|
+
);
|
|
454
|
+
let modules = resolve_virtual("main.tish", &files).unwrap();
|
|
455
|
+
assert_eq!(modules.len(), 2);
|
|
456
|
+
assert_eq!(modules[0].path, "lib.tish");
|
|
457
|
+
assert_eq!(modules[1].path, "main.tish");
|
|
458
|
+
detect_cycles_virtual(&modules).unwrap();
|
|
459
|
+
let program = merge_modules_virtual(modules).unwrap();
|
|
460
|
+
assert!(!program.statements.is_empty());
|
|
461
|
+
}
|
|
462
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
//! Console styling for values (Node/Bun-style colors).
|
|
2
|
+
//!
|
|
3
|
+
//! Use for REPL, console.log, and any terminal output so numbers, strings,
|
|
4
|
+
//! booleans, null, and object structure are easier to scan.
|
|
5
|
+
|
|
6
|
+
use std::io::IsTerminal;
|
|
7
|
+
|
|
8
|
+
use crate::Value;
|
|
9
|
+
|
|
10
|
+
/// ANSI escape codes (standard 4-bit + bright black for dim).
|
|
11
|
+
const RESET: &str = "\x1b[0m";
|
|
12
|
+
/// Number: yellow (Node-style)
|
|
13
|
+
const NUMBER: &str = "\x1b[33m";
|
|
14
|
+
/// String: green
|
|
15
|
+
const STRING: &str = "\x1b[32m";
|
|
16
|
+
/// Boolean: blue
|
|
17
|
+
const BOOLEAN: &str = "\x1b[34m";
|
|
18
|
+
/// Null: dim grey
|
|
19
|
+
const NULL: &str = "\x1b[90m";
|
|
20
|
+
/// Object keys: cyan
|
|
21
|
+
const KEY: &str = "\x1b[36m";
|
|
22
|
+
/// Punctuation (brackets, commas): dim
|
|
23
|
+
const PUNCT: &str = "\x1b[90m";
|
|
24
|
+
/// Function / special (e.g. [Function]): dim
|
|
25
|
+
const SPECIAL: &str = "\x1b[90m";
|
|
26
|
+
|
|
27
|
+
/// Returns whether console output should use colors (stdout is a TTY).
|
|
28
|
+
pub fn use_console_colors() -> bool {
|
|
29
|
+
std::io::stdout().is_terminal()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Format a single value for console with optional ANSI colors (Node/Bun-style).
|
|
33
|
+
pub fn format_value_styled(value: &Value, colors: bool) -> String {
|
|
34
|
+
if !colors {
|
|
35
|
+
return value.to_display_string();
|
|
36
|
+
}
|
|
37
|
+
format_value_styled_inner(value, colors)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fn format_value_styled_inner(value: &Value, colors: bool) -> String {
|
|
41
|
+
match value {
|
|
42
|
+
Value::Number(n) => {
|
|
43
|
+
let s = if n.is_nan() {
|
|
44
|
+
"NaN".to_string()
|
|
45
|
+
} else if *n == f64::INFINITY {
|
|
46
|
+
"Infinity".to_string()
|
|
47
|
+
} else if *n == f64::NEG_INFINITY {
|
|
48
|
+
"-Infinity".to_string()
|
|
49
|
+
} else {
|
|
50
|
+
n.to_string()
|
|
51
|
+
};
|
|
52
|
+
format!("{NUMBER}{s}{RESET}")
|
|
53
|
+
}
|
|
54
|
+
Value::String(s) => {
|
|
55
|
+
let escaped = escape_string_for_display(s);
|
|
56
|
+
format!("{STRING}\"{escaped}\"{RESET}")
|
|
57
|
+
}
|
|
58
|
+
Value::Bool(b) => format!("{BOOLEAN}{b}{RESET}"),
|
|
59
|
+
Value::Null => format!("{NULL}null{RESET}"),
|
|
60
|
+
Value::Array(arr) => {
|
|
61
|
+
let inner: Vec<String> = arr
|
|
62
|
+
.borrow()
|
|
63
|
+
.iter()
|
|
64
|
+
.map(|v| format_value_styled_inner(v, colors))
|
|
65
|
+
.collect();
|
|
66
|
+
let sep = format!("{PUNCT}, {RESET}");
|
|
67
|
+
format!("{PUNCT}[{RESET}{}{PUNCT}]{RESET}", inner.join(&sep))
|
|
68
|
+
}
|
|
69
|
+
Value::Object(obj) => {
|
|
70
|
+
let inner: Vec<String> = obj
|
|
71
|
+
.borrow()
|
|
72
|
+
.iter()
|
|
73
|
+
.map(|(k, v)| {
|
|
74
|
+
format!(
|
|
75
|
+
"{KEY}{}{RESET}{PUNCT}: {RESET}{}",
|
|
76
|
+
k.as_ref(),
|
|
77
|
+
format_value_styled_inner(v, colors)
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
.collect();
|
|
81
|
+
let sep = format!("{PUNCT}, {RESET}");
|
|
82
|
+
format!("{PUNCT}{{{RESET} {} {PUNCT}}}{RESET}", inner.join(&sep))
|
|
83
|
+
}
|
|
84
|
+
Value::Function(_) => format!("{SPECIAL}[Function]{RESET}"),
|
|
85
|
+
Value::Promise(_) => format!("{SPECIAL}[object Promise]{RESET}"),
|
|
86
|
+
Value::Opaque(o) => format!("{SPECIAL}[object {}]{RESET}", o.type_name()),
|
|
87
|
+
#[cfg(feature = "regex")]
|
|
88
|
+
Value::RegExp(re) => {
|
|
89
|
+
let re = re.borrow();
|
|
90
|
+
format!(
|
|
91
|
+
"{PUNCT}/{KEY}{}{RESET}{PUNCT}/{}{RESET}",
|
|
92
|
+
re.source,
|
|
93
|
+
re.flags_string()
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fn escape_string_for_display(s: &str) -> String {
|
|
100
|
+
let mut out = String::with_capacity(s.len());
|
|
101
|
+
for c in s.chars() {
|
|
102
|
+
match c {
|
|
103
|
+
'\\' => out.push_str("\\\\"),
|
|
104
|
+
'\n' => out.push_str("\\n"),
|
|
105
|
+
'\r' => out.push_str("\\r"),
|
|
106
|
+
'\t' => out.push_str("\\t"),
|
|
107
|
+
'"' => out.push_str("\\\""),
|
|
108
|
+
c => out.push(c),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
out
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Format multiple values for console (e.g. console.log(a, b, c)) with optional colors.
|
|
115
|
+
pub fn format_values_for_console(values: &[Value], colors: bool) -> String {
|
|
116
|
+
let mut iter = values.iter();
|
|
117
|
+
match iter.next() {
|
|
118
|
+
None => String::new(),
|
|
119
|
+
Some(first) => {
|
|
120
|
+
let mut result = format_value_styled(first, colors);
|
|
121
|
+
for v in iter {
|
|
122
|
+
result.push(' ');
|
|
123
|
+
result.push_str(&format_value_styled(v, colors));
|
|
124
|
+
}
|
|
125
|
+
result
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|