@tishlang/tish 1.6.0 → 1.8.0
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 +2 -0
- package/README.md +2 -0
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/error.rs +2 -8
- package/crates/js_to_tish/src/transform/expr.rs +128 -137
- package/crates/js_to_tish/src/transform/stmt.rs +62 -32
- package/crates/tish/Cargo.toml +15 -5
- package/crates/tish/src/cargo_native_registry.rs +29 -0
- package/crates/tish/src/cli_help.rs +92 -39
- package/crates/tish/src/main.rs +172 -86
- package/crates/tish/src/repl_completion.rs +3 -3
- package/crates/tish/tests/cargo_example_compile.rs +4 -2
- package/crates/tish/tests/integration_test.rs +216 -54
- package/crates/tish/tests/run_optimize_stdout_parity.rs +3 -7
- package/crates/tish/tests/shortcircuit.rs +20 -5
- package/crates/tish_ast/src/ast.rs +92 -23
- package/crates/tish_build_utils/Cargo.toml +4 -0
- package/crates/tish_build_utils/src/lib.rs +136 -8
- package/crates/tish_builtins/Cargo.toml +5 -1
- package/crates/tish_builtins/src/array.rs +65 -33
- package/crates/tish_builtins/src/construct.rs +34 -39
- package/crates/tish_builtins/src/globals.rs +42 -26
- package/crates/tish_builtins/src/helpers.rs +2 -1
- package/crates/tish_builtins/src/lib.rs +5 -5
- package/crates/tish_builtins/src/math.rs +5 -3
- package/crates/tish_builtins/src/object.rs +3 -2
- package/crates/tish_builtins/src/string.rs +144 -22
- package/crates/tish_bytecode/src/chunk.rs +0 -1
- package/crates/tish_bytecode/src/compiler.rs +173 -71
- package/crates/tish_bytecode/src/opcode.rs +24 -6
- package/crates/tish_bytecode/src/peephole.rs +2 -2
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/codegen.rs +1621 -453
- package/crates/tish_compile/src/infer.rs +75 -19
- package/crates/tish_compile/src/lib.rs +19 -8
- package/crates/tish_compile/src/resolve.rs +278 -137
- package/crates/tish_compile/src/types.rs +184 -24
- package/crates/tish_compile_js/Cargo.toml +1 -0
- package/crates/tish_compile_js/src/codegen.rs +181 -37
- package/crates/tish_compile_js/src/lib.rs +3 -1
- package/crates/tish_compile_js/src/tests_jsx.rs +30 -6
- package/crates/tish_compiler_wasm/src/lib.rs +16 -13
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +69 -59
- package/crates/tish_core/Cargo.toml +8 -0
- package/crates/tish_core/src/json.rs +107 -56
- package/crates/tish_core/src/lib.rs +4 -2
- package/crates/tish_core/src/macros.rs +5 -5
- package/crates/tish_core/src/uri.rs +9 -6
- package/crates/tish_core/src/value.rs +145 -43
- package/crates/tish_core/src/vmref.rs +178 -0
- package/crates/tish_cranelift/src/link.rs +6 -9
- package/crates/tish_cranelift/src/lower.rs +14 -8
- package/crates/tish_eval/Cargo.toml +17 -2
- package/crates/tish_eval/src/eval.rs +474 -165
- package/crates/tish_eval/src/http.rs +61 -0
- package/crates/tish_eval/src/lib.rs +12 -8
- package/crates/tish_eval/src/natives.rs +136 -38
- package/crates/tish_eval/src/promise.rs +14 -8
- package/crates/tish_eval/src/timers.rs +28 -19
- package/crates/tish_eval/src/value.rs +17 -6
- package/crates/tish_eval/src/value_convert.rs +13 -5
- package/crates/tish_fmt/src/lib.rs +149 -43
- package/crates/tish_lexer/src/lib.rs +232 -63
- package/crates/tish_lexer/src/token.rs +10 -6
- package/crates/tish_llvm/src/lib.rs +17 -8
- package/crates/tish_lsp/Cargo.toml +4 -1
- package/crates/tish_lsp/README.md +1 -1
- package/crates/tish_lsp/src/builtin_goto.rs +261 -0
- package/crates/tish_lsp/src/import_goto.rs +549 -0
- package/crates/tish_lsp/src/main.rs +504 -106
- package/crates/tish_native/src/build.rs +4 -8
- package/crates/tish_native/src/lib.rs +54 -21
- package/crates/tish_opt/src/lib.rs +84 -52
- package/crates/tish_parser/src/lib.rs +45 -13
- package/crates/tish_parser/src/parser.rs +505 -130
- package/crates/tish_resolve/Cargo.toml +13 -0
- package/crates/tish_resolve/src/lib.rs +3436 -0
- package/crates/tish_resolve/src/pos.rs +133 -0
- package/crates/tish_runtime/Cargo.toml +68 -3
- package/crates/tish_runtime/src/http.rs +1136 -145
- package/crates/tish_runtime/src/http_fetch.rs +38 -27
- package/crates/tish_runtime/src/http_hyper.rs +418 -0
- package/crates/tish_runtime/src/http_prefork.rs +189 -0
- package/crates/tish_runtime/src/lib.rs +375 -189
- package/crates/tish_runtime/src/promise.rs +199 -40
- package/crates/tish_runtime/src/promise_io.rs +2 -1
- package/crates/tish_runtime/src/timers.rs +37 -1
- package/crates/tish_runtime/src/ws.rs +65 -42
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +5 -4
- package/crates/tish_ui/src/jsx.rs +317 -27
- package/crates/tish_ui/src/lib.rs +5 -2
- package/crates/tish_ui/src/runtime/hooks.rs +406 -45
- package/crates/tish_ui/src/runtime/mod.rs +36 -9
- package/crates/tish_vm/Cargo.toml +15 -5
- package/crates/tish_vm/src/vm.rs +725 -281
- package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +11 -4
- package/crates/tish_wasm/src/lib.rs +55 -42
- package/crates/tish_wasm_runtime/Cargo.toml +2 -1
- package/crates/tish_wasm_runtime/src/lib.rs +1 -1
- package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
- package/crates/tishlang_cargo_bindgen/src/classify.rs +265 -0
- package/crates/tishlang_cargo_bindgen/src/discover.rs +120 -0
- package/crates/tishlang_cargo_bindgen/src/infer.rs +372 -0
- package/crates/tishlang_cargo_bindgen/src/lib.rs +350 -0
- package/crates/tishlang_cargo_bindgen/src/main.rs +164 -0
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +114 -0
- package/justfile +8 -0
- package/package.json +1 -1
- 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
|
@@ -7,7 +7,9 @@ mod error;
|
|
|
7
7
|
#[cfg(test)]
|
|
8
8
|
mod tests_jsx;
|
|
9
9
|
|
|
10
|
-
pub use codegen::{
|
|
10
|
+
pub use codegen::{
|
|
11
|
+
compile_project_with_jsx, compile_project_with_jsx_and_source_map, compile_with_jsx, JsBundle,
|
|
12
|
+
};
|
|
11
13
|
pub use error::CompileError;
|
|
12
14
|
|
|
13
15
|
/// JSX lowers to `h` / `Fragment`; merge the `lattish` runtime for hooks and DOM.
|
|
@@ -11,7 +11,11 @@ mod tests {
|
|
|
11
11
|
let src = r#"fn X() { return <div class="a">{"hi"}</div> }"#;
|
|
12
12
|
let program = parse(src).unwrap();
|
|
13
13
|
let js = compile_with_jsx(&program, false).unwrap();
|
|
14
|
-
assert!(
|
|
14
|
+
assert!(
|
|
15
|
+
js.contains("h(\"div\", { class: \"a\" }, [\"hi\"])"),
|
|
16
|
+
"{}",
|
|
17
|
+
js
|
|
18
|
+
);
|
|
15
19
|
assert!(!js.contains("function __h("));
|
|
16
20
|
}
|
|
17
21
|
|
|
@@ -57,7 +61,11 @@ mod tests {
|
|
|
57
61
|
let src = r#"fn X() { return <p>work!</p> }"#;
|
|
58
62
|
let program = parse(src).unwrap();
|
|
59
63
|
let js = compile_with_jsx(&program, false).unwrap();
|
|
60
|
-
assert!(
|
|
64
|
+
assert!(
|
|
65
|
+
js.contains(r#""work!""#),
|
|
66
|
+
"expected 'work!', got: {}",
|
|
67
|
+
&js[..400.min(js.len())]
|
|
68
|
+
);
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
#[test]
|
|
@@ -65,7 +73,11 @@ mod tests {
|
|
|
65
73
|
let src = r#"fn X() { return <p>hello 😔</p> }"#;
|
|
66
74
|
let program = parse(src).unwrap();
|
|
67
75
|
let js = compile_with_jsx(&program, false).unwrap();
|
|
68
|
-
assert!(
|
|
76
|
+
assert!(
|
|
77
|
+
js.contains("😔"),
|
|
78
|
+
"expected emoji, got: {}",
|
|
79
|
+
&js[..400.min(js.len())]
|
|
80
|
+
);
|
|
69
81
|
}
|
|
70
82
|
|
|
71
83
|
#[test]
|
|
@@ -93,10 +105,22 @@ mod tests {
|
|
|
93
105
|
let src = r#"fn X() { return <div class="x">{"a"}</div> }"#;
|
|
94
106
|
let program = parse(src).unwrap();
|
|
95
107
|
let js = compile_with_jsx(&program, false).unwrap();
|
|
96
|
-
assert!(
|
|
108
|
+
assert!(
|
|
109
|
+
js.contains("h(\"div\", { class: \"x\" }"),
|
|
110
|
+
"{}",
|
|
111
|
+
&js[..500.min(js.len())]
|
|
112
|
+
);
|
|
97
113
|
assert!(!js.contains("__vdom_h"), "{}", &js[..600.min(js.len())]);
|
|
98
|
-
assert!(
|
|
99
|
-
|
|
114
|
+
assert!(
|
|
115
|
+
!js.contains("window.__LATTISH_JSX_VDOM"),
|
|
116
|
+
"{}",
|
|
117
|
+
&js[..600.min(js.len())]
|
|
118
|
+
);
|
|
119
|
+
assert!(
|
|
120
|
+
!js.contains("__lattishVdomPatch"),
|
|
121
|
+
"{}",
|
|
122
|
+
&js[..600.min(js.len())]
|
|
123
|
+
);
|
|
100
124
|
}
|
|
101
125
|
|
|
102
126
|
/// Component calls like {Panel()} return DOM elements. Wrapping in String() produces [object HTMLDivElement].
|
|
@@ -14,29 +14,34 @@ use wasm_bindgen::prelude::*;
|
|
|
14
14
|
|
|
15
15
|
#[wasm_bindgen]
|
|
16
16
|
pub fn compile_to_bytecode(source: &str) -> Result<String, JsValue> {
|
|
17
|
-
let program =
|
|
17
|
+
let program =
|
|
18
|
+
tishlang_parser::parse(source.trim()).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
|
18
19
|
let program = tishlang_opt::optimize(&program);
|
|
19
|
-
let chunk =
|
|
20
|
+
let chunk =
|
|
21
|
+
tishlang_bytecode::compile(&program).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
|
20
22
|
Ok(base64::engine::general_purpose::STANDARD.encode(tishlang_bytecode::serialize(&chunk)))
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
#[wasm_bindgen]
|
|
24
26
|
pub fn compile_to_js(source: &str) -> Result<String, JsValue> {
|
|
25
|
-
let program =
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
let program =
|
|
28
|
+
tishlang_parser::parse(source.trim()).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
|
29
|
+
tishlang_compile_js::compile_with_jsx(&program, true).map_err(|e| JsValue::from_str(&e.message))
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
#[wasm_bindgen]
|
|
31
|
-
pub fn compile_to_bytecode_with_imports(
|
|
33
|
+
pub fn compile_to_bytecode_with_imports(
|
|
34
|
+
entry_path: &str,
|
|
35
|
+
files_json: &str,
|
|
36
|
+
) -> Result<String, JsValue> {
|
|
32
37
|
let files: HashMap<String, String> = serde_json::from_str(files_json)
|
|
33
38
|
.map_err(|e| JsValue::from_str(&format!("Invalid files JSON: {}", e)))?;
|
|
34
|
-
let modules = resolve_virtual(entry_path, &files)
|
|
35
|
-
.map_err(|e| JsValue::from_str(&e))?;
|
|
39
|
+
let modules = resolve_virtual(entry_path, &files).map_err(|e| JsValue::from_str(&e))?;
|
|
36
40
|
detect_cycles_virtual(&modules).map_err(|e| JsValue::from_str(&e))?;
|
|
37
41
|
let program = merge_modules_virtual(modules).map_err(|e| JsValue::from_str(&e))?;
|
|
38
42
|
let program = tishlang_opt::optimize(&program);
|
|
39
|
-
let chunk =
|
|
43
|
+
let chunk =
|
|
44
|
+
tishlang_bytecode::compile(&program).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
|
40
45
|
Ok(base64::engine::general_purpose::STANDARD.encode(tishlang_bytecode::serialize(&chunk)))
|
|
41
46
|
}
|
|
42
47
|
|
|
@@ -44,11 +49,9 @@ pub fn compile_to_bytecode_with_imports(entry_path: &str, files_json: &str) -> R
|
|
|
44
49
|
pub fn compile_to_js_with_imports(entry_path: &str, files_json: &str) -> Result<String, JsValue> {
|
|
45
50
|
let files: HashMap<String, String> = serde_json::from_str(files_json)
|
|
46
51
|
.map_err(|e| JsValue::from_str(&format!("Invalid files JSON: {}", e)))?;
|
|
47
|
-
let modules = resolve_virtual(entry_path, &files)
|
|
48
|
-
.map_err(|e| JsValue::from_str(&e))?;
|
|
52
|
+
let modules = resolve_virtual(entry_path, &files).map_err(|e| JsValue::from_str(&e))?;
|
|
49
53
|
detect_cycles_virtual(&modules).map_err(|e| JsValue::from_str(&e))?;
|
|
50
54
|
let program = merge_modules_virtual(modules).map_err(|e| JsValue::from_str(&e))?;
|
|
51
55
|
let program = tishlang_opt::optimize(&program);
|
|
52
|
-
tishlang_compile_js::compile_with_jsx(&program, true)
|
|
53
|
-
.map_err(|e| JsValue::from_str(&e.message))
|
|
56
|
+
tishlang_compile_js::compile_with_jsx(&program, true).map_err(|e| JsValue::from_str(&e.message))
|
|
54
57
|
}
|
|
@@ -17,6 +17,7 @@ pub struct VirtualModule {
|
|
|
17
17
|
const BUILTIN_ALIASES: &[(&str, &str)] = &[
|
|
18
18
|
("fs", "tish:fs"),
|
|
19
19
|
("http", "tish:http"),
|
|
20
|
+
("timers", "tish:timers"),
|
|
20
21
|
("process", "tish:process"),
|
|
21
22
|
("ws", "tish:ws"),
|
|
22
23
|
];
|
|
@@ -35,7 +36,7 @@ fn is_native_import(spec: &str) -> bool {
|
|
|
35
36
|
spec.starts_with("tish:")
|
|
36
37
|
|| spec.starts_with("cargo:")
|
|
37
38
|
|| spec.starts_with('@')
|
|
38
|
-
|| matches!(spec, "fs" | "http" | "process" | "ws")
|
|
39
|
+
|| matches!(spec, "fs" | "http" | "timers" | "process" | "ws")
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/// Normalize a virtual path: resolve . and .. components.
|
|
@@ -109,10 +110,16 @@ pub fn resolve_virtual(
|
|
|
109
110
|
if files.contains_key(&with_ext) {
|
|
110
111
|
with_ext
|
|
111
112
|
} else {
|
|
112
|
-
return Err(format!(
|
|
113
|
+
return Err(format!(
|
|
114
|
+
"Entry file '{}' not in virtual file map",
|
|
115
|
+
entry_path
|
|
116
|
+
));
|
|
113
117
|
}
|
|
114
118
|
} else {
|
|
115
|
-
return Err(format!(
|
|
119
|
+
return Err(format!(
|
|
120
|
+
"Entry file '{}' not in virtual file map",
|
|
121
|
+
entry_path
|
|
122
|
+
));
|
|
116
123
|
};
|
|
117
124
|
|
|
118
125
|
let mut visited = HashSet::new();
|
|
@@ -148,9 +155,9 @@ fn load_module_recursive(
|
|
|
148
155
|
}
|
|
149
156
|
visited.insert(module_path.to_string());
|
|
150
157
|
|
|
151
|
-
let source = files
|
|
152
|
-
|
|
153
|
-
|
|
158
|
+
let source = files
|
|
159
|
+
.get(module_path)
|
|
160
|
+
.ok_or_else(|| format!("Module '{}' not in virtual file map", module_path))?;
|
|
154
161
|
let program = tishlang_parser::parse(source.trim())
|
|
155
162
|
.map_err(|e| format!("Parse error in {}: {}", module_path, e))?;
|
|
156
163
|
|
|
@@ -162,13 +169,7 @@ fn load_module_recursive(
|
|
|
162
169
|
}
|
|
163
170
|
let dep_key = resolve_import_to_key(from, from_dir, files)?;
|
|
164
171
|
if !path_to_module.contains_key(&dep_key) {
|
|
165
|
-
load_module_recursive(
|
|
166
|
-
&dep_key,
|
|
167
|
-
files,
|
|
168
|
-
visited,
|
|
169
|
-
path_to_module,
|
|
170
|
-
load_order,
|
|
171
|
-
)?;
|
|
172
|
+
load_module_recursive(&dep_key, files, visited, path_to_module, load_order)?;
|
|
172
173
|
}
|
|
173
174
|
}
|
|
174
175
|
}
|
|
@@ -197,11 +198,11 @@ pub fn detect_cycles_virtual(modules: &[VirtualModule]) -> Result<(), String> {
|
|
|
197
198
|
&mut stack,
|
|
198
199
|
&mut HashSet::new(),
|
|
199
200
|
)? {
|
|
200
|
-
let path_names: Vec<_> = stack
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
.
|
|
204
|
-
|
|
201
|
+
let path_names: Vec<_> = stack.iter().map(|&i| modules[i].path.clone()).collect();
|
|
202
|
+
return Err(format!(
|
|
203
|
+
"Circular import detected: {}",
|
|
204
|
+
path_names.join(" -> ")
|
|
205
|
+
));
|
|
205
206
|
}
|
|
206
207
|
}
|
|
207
208
|
Ok(())
|
|
@@ -231,14 +232,8 @@ fn has_cycle_from(
|
|
|
231
232
|
stack.push(dep_idx);
|
|
232
233
|
let dep = &modules[dep_idx];
|
|
233
234
|
let dep_dir = parent_dir(&dep.path);
|
|
234
|
-
if has_cycle_from(
|
|
235
|
-
|
|
236
|
-
&dep.program,
|
|
237
|
-
path_to_idx,
|
|
238
|
-
modules,
|
|
239
|
-
stack,
|
|
240
|
-
visiting,
|
|
241
|
-
)? {
|
|
235
|
+
if has_cycle_from(dep_dir, &dep.program, path_to_idx, modules, stack, visiting)?
|
|
236
|
+
{
|
|
242
237
|
return Ok(true);
|
|
243
238
|
}
|
|
244
239
|
stack.pop();
|
|
@@ -307,14 +302,24 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
|
|
|
307
302
|
let dir = parent_dir(&module.path);
|
|
308
303
|
for stmt in &module.program.statements {
|
|
309
304
|
match stmt {
|
|
310
|
-
Statement::Import {
|
|
305
|
+
Statement::Import {
|
|
306
|
+
specifiers,
|
|
307
|
+
from,
|
|
308
|
+
span,
|
|
309
|
+
} => {
|
|
311
310
|
if is_native_import(from) {
|
|
312
311
|
let canonical_spec =
|
|
313
312
|
normalize_builtin_spec(from).unwrap_or_else(|| from.to_string());
|
|
314
313
|
for spec in specifiers {
|
|
315
314
|
match spec {
|
|
316
|
-
ImportSpecifier::Named {
|
|
315
|
+
ImportSpecifier::Named {
|
|
316
|
+
name,
|
|
317
|
+
name_span,
|
|
318
|
+
alias,
|
|
319
|
+
alias_span,
|
|
320
|
+
} => {
|
|
317
321
|
let bind = alias.as_deref().unwrap_or(name.as_ref());
|
|
322
|
+
let decl_name_span = alias_span.as_ref().unwrap_or(name_span);
|
|
318
323
|
let init = Expr::NativeModuleLoad {
|
|
319
324
|
spec: Arc::from(canonical_spec.clone()),
|
|
320
325
|
export_name: name.clone(),
|
|
@@ -322,23 +327,24 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
|
|
|
322
327
|
};
|
|
323
328
|
statements.push(Statement::VarDecl {
|
|
324
329
|
name: Arc::from(bind),
|
|
330
|
+
name_span: *decl_name_span,
|
|
325
331
|
mutable: false,
|
|
326
332
|
type_ann: None,
|
|
327
333
|
init: Some(init),
|
|
328
334
|
span: *span,
|
|
329
335
|
});
|
|
330
336
|
}
|
|
331
|
-
ImportSpecifier::Namespace
|
|
337
|
+
ImportSpecifier::Namespace { name, .. } => {
|
|
332
338
|
return Err(format!(
|
|
333
339
|
"Namespace import (* as {}) not supported for native module '{}'",
|
|
334
|
-
|
|
340
|
+
name.as_ref(),
|
|
335
341
|
from.as_ref()
|
|
336
342
|
));
|
|
337
343
|
}
|
|
338
|
-
ImportSpecifier::Default
|
|
344
|
+
ImportSpecifier::Default { name, .. } => {
|
|
339
345
|
return Err(format!(
|
|
340
346
|
"Default import '{}' not supported for native module '{}'. Use named import.",
|
|
341
|
-
|
|
347
|
+
name.as_ref(),
|
|
342
348
|
from.as_ref()
|
|
343
349
|
));
|
|
344
350
|
}
|
|
@@ -353,15 +359,22 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
|
|
|
353
359
|
let dep_exports = &module_exports[dep_idx];
|
|
354
360
|
for spec in specifiers {
|
|
355
361
|
match spec {
|
|
356
|
-
ImportSpecifier::Named {
|
|
362
|
+
ImportSpecifier::Named {
|
|
363
|
+
name,
|
|
364
|
+
name_span,
|
|
365
|
+
alias,
|
|
366
|
+
alias_span,
|
|
367
|
+
} => {
|
|
357
368
|
let source = dep_exports
|
|
358
369
|
.get(name.as_ref())
|
|
359
370
|
.cloned()
|
|
360
371
|
.unwrap_or_else(|| name.to_string());
|
|
361
372
|
let bind = alias.as_deref().unwrap_or(name.as_ref());
|
|
362
373
|
if bind != source {
|
|
374
|
+
let decl_name_span = alias_span.as_ref().unwrap_or(name_span);
|
|
363
375
|
statements.push(Statement::VarDecl {
|
|
364
376
|
name: Arc::from(bind),
|
|
377
|
+
name_span: *decl_name_span,
|
|
365
378
|
mutable: false,
|
|
366
379
|
type_ann: None,
|
|
367
380
|
init: Some(Expr::Ident {
|
|
@@ -372,7 +385,7 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
|
|
|
372
385
|
});
|
|
373
386
|
}
|
|
374
387
|
}
|
|
375
|
-
ImportSpecifier::Namespace
|
|
388
|
+
ImportSpecifier::Namespace { name, name_span } => {
|
|
376
389
|
let mut props = Vec::new();
|
|
377
390
|
for (k, v) in dep_exports {
|
|
378
391
|
props.push(tishlang_ast::ObjectProp::KeyValue(
|
|
@@ -384,25 +397,22 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
|
|
|
384
397
|
));
|
|
385
398
|
}
|
|
386
399
|
statements.push(Statement::VarDecl {
|
|
387
|
-
name:
|
|
400
|
+
name: name.clone(),
|
|
401
|
+
name_span: *name_span,
|
|
388
402
|
mutable: false,
|
|
389
403
|
type_ann: None,
|
|
390
|
-
init: Some(Expr::Object {
|
|
391
|
-
props,
|
|
392
|
-
span: *span,
|
|
393
|
-
}),
|
|
404
|
+
init: Some(Expr::Object { props, span: *span }),
|
|
394
405
|
span: *span,
|
|
395
406
|
});
|
|
396
407
|
}
|
|
397
|
-
ImportSpecifier::Default
|
|
398
|
-
let source =
|
|
399
|
-
.get("default")
|
|
400
|
-
.cloned()
|
|
401
|
-
.ok_or_else(|| {
|
|
408
|
+
ImportSpecifier::Default { name, name_span } => {
|
|
409
|
+
let source =
|
|
410
|
+
dep_exports.get("default").cloned().ok_or_else(|| {
|
|
402
411
|
format!("Module '{}' has no default export", from)
|
|
403
412
|
})?;
|
|
404
413
|
statements.push(Statement::VarDecl {
|
|
405
|
-
name:
|
|
414
|
+
name: name.clone(),
|
|
415
|
+
name_span: *name_span,
|
|
406
416
|
mutable: false,
|
|
407
417
|
type_ann: None,
|
|
408
418
|
init: Some(Expr::Ident {
|
|
@@ -415,21 +425,21 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
|
|
|
415
425
|
}
|
|
416
426
|
}
|
|
417
427
|
}
|
|
418
|
-
Statement::Export { declaration, .. } => {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
428
|
+
Statement::Export { declaration, .. } => match declaration.as_ref() {
|
|
429
|
+
ExportDeclaration::Named(s) => statements.push(*s.clone()),
|
|
430
|
+
ExportDeclaration::Default(e) => {
|
|
431
|
+
let default_name = format!("__default_{}", idx);
|
|
432
|
+
let espan = e.span();
|
|
433
|
+
statements.push(Statement::VarDecl {
|
|
434
|
+
name: Arc::from(default_name),
|
|
435
|
+
name_span: espan,
|
|
436
|
+
mutable: false,
|
|
437
|
+
type_ann: None,
|
|
438
|
+
init: Some((*e).clone()),
|
|
439
|
+
span: espan,
|
|
440
|
+
});
|
|
431
441
|
}
|
|
432
|
-
}
|
|
442
|
+
},
|
|
433
443
|
_ => statements.push(stmt.clone()),
|
|
434
444
|
}
|
|
435
445
|
}
|
|
@@ -9,6 +9,14 @@ repository = { workspace = true }
|
|
|
9
9
|
[features]
|
|
10
10
|
default = []
|
|
11
11
|
regex = ["dep:fancy-regex"]
|
|
12
|
+
# Make `Value` (and its array / object / regex payloads) `Send + Sync` by
|
|
13
|
+
# switching the interior `Rc<RefCell<T>>` to `Arc<Mutex<T>>` and the native
|
|
14
|
+
# function type from `Rc<dyn Fn>` to `Arc<dyn Fn + Send + Sync>`. Enabled
|
|
15
|
+
# transitively by any crate that needs to pass `Value`s across threads —
|
|
16
|
+
# most notably `tishlang_runtime/http`, which dispatches HTTP handlers
|
|
17
|
+
# across a worker pool. Off by default so wasm / wasi / cranelift / llvm /
|
|
18
|
+
# interpreter builds pay no atomic-ref-count or mutex overhead.
|
|
19
|
+
send-values = []
|
|
12
20
|
|
|
13
21
|
[dependencies]
|
|
14
22
|
ahash = "0.8.11"
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
//! JSON parsing and stringification for Tish values.
|
|
2
2
|
|
|
3
|
-
use crate::Value;
|
|
4
|
-
use std::cell::RefCell;
|
|
5
|
-
use std::rc::Rc;
|
|
3
|
+
use crate::{Value, VmRef};
|
|
6
4
|
use std::sync::Arc;
|
|
7
5
|
|
|
8
6
|
/// Parse JSON string into a Value.
|
|
@@ -19,75 +17,126 @@ pub fn json_parse(json: &str) -> Result<Value, String> {
|
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
/// Stringify a Value to JSON.
|
|
20
|
+
///
|
|
21
|
+
/// Single-buffer write strategy: all nested values append into one
|
|
22
|
+
/// `String` via [`json_stringify_into`], so we never allocate a transient
|
|
23
|
+
/// per-node `String` only to copy + drop it on the way back up. For a
|
|
24
|
+
/// 20-row TFB `/queries` response (~40 numbers, 2 keys × 20 = ~80 string
|
|
25
|
+
/// ops) that saves dozens of small allocations per request.
|
|
22
26
|
pub fn json_stringify(value: &Value) -> String {
|
|
27
|
+
// 256 B is enough for typical TFB responses (`/db` is 31 B,
|
|
28
|
+
// `/queries=20` is ~700 B). Larger payloads reallocate normally.
|
|
29
|
+
let mut buf = String::with_capacity(256);
|
|
30
|
+
json_stringify_into(&mut buf, value);
|
|
31
|
+
buf
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Append a JSON-stringified `value` to `buf`. Used by JSON.stringify for
|
|
35
|
+
/// the recursive case so we don't pay for an intermediate `String` per
|
|
36
|
+
/// node.
|
|
37
|
+
pub fn json_stringify_into(buf: &mut String, value: &Value) {
|
|
23
38
|
match value {
|
|
24
|
-
Value::Null => "null"
|
|
25
|
-
Value::Bool(
|
|
39
|
+
Value::Null => buf.push_str("null"),
|
|
40
|
+
Value::Bool(true) => buf.push_str("true"),
|
|
41
|
+
Value::Bool(false) => buf.push_str("false"),
|
|
26
42
|
Value::Number(n) => {
|
|
27
43
|
if n.is_nan() || n.is_infinite() {
|
|
28
|
-
"null"
|
|
44
|
+
buf.push_str("null");
|
|
29
45
|
} else {
|
|
30
|
-
|
|
46
|
+
// `write!` avoids the heap allocation that `to_string`
|
|
47
|
+
// produces. The f64 → decimal formatter is the same
|
|
48
|
+
// either way (`std::fmt::Display`).
|
|
49
|
+
use std::fmt::Write;
|
|
50
|
+
let _ = write!(buf, "{}", n);
|
|
31
51
|
}
|
|
32
52
|
}
|
|
33
|
-
Value::String(s) =>
|
|
53
|
+
Value::String(s) => {
|
|
54
|
+
buf.push('"');
|
|
55
|
+
escape_json_string_into(buf, s);
|
|
56
|
+
buf.push('"');
|
|
57
|
+
}
|
|
34
58
|
Value::Array(arr) => {
|
|
35
59
|
let borrowed = arr.borrow();
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if !first {
|
|
41
|
-
result.push(',');
|
|
60
|
+
buf.push('[');
|
|
61
|
+
for (i, item) in borrowed.iter().enumerate() {
|
|
62
|
+
if i > 0 {
|
|
63
|
+
buf.push(',');
|
|
42
64
|
}
|
|
43
|
-
|
|
44
|
-
result.push_str(&json_stringify(item));
|
|
65
|
+
json_stringify_into(buf, item);
|
|
45
66
|
}
|
|
46
|
-
|
|
47
|
-
result
|
|
67
|
+
buf.push(']');
|
|
48
68
|
}
|
|
49
69
|
Value::Object(obj) => {
|
|
50
70
|
let borrowed = obj.borrow();
|
|
51
|
-
|
|
52
|
-
keys.
|
|
53
|
-
let mut
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
// Sort keys for deterministic output. Pre-allocate to avoid
|
|
72
|
+
// a fresh `Vec` realloc inside `keys().collect()`.
|
|
73
|
+
let mut keys: Vec<&Arc<str>> = Vec::with_capacity(borrowed.len());
|
|
74
|
+
keys.extend(borrowed.keys());
|
|
75
|
+
keys.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
|
|
76
|
+
buf.push('{');
|
|
77
|
+
for (i, key) in keys.into_iter().enumerate() {
|
|
78
|
+
if i > 0 {
|
|
79
|
+
buf.push(',');
|
|
59
80
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
result.push_str(&json_stringify(borrowed.get(key).unwrap()));
|
|
81
|
+
buf.push('"');
|
|
82
|
+
escape_json_string_into(buf, key);
|
|
83
|
+
buf.push_str("\":");
|
|
84
|
+
json_stringify_into(buf, borrowed.get(key).unwrap());
|
|
65
85
|
}
|
|
66
|
-
|
|
67
|
-
result
|
|
86
|
+
buf.push('}');
|
|
68
87
|
}
|
|
69
|
-
Value::Function(_) => "null"
|
|
70
|
-
Value::Promise(_) => "null".to_string(),
|
|
71
|
-
Value::Opaque(_) => "null".to_string(),
|
|
88
|
+
Value::Function(_) | Value::Promise(_) | Value::Opaque(_) => buf.push_str("null"),
|
|
72
89
|
#[cfg(feature = "regex")]
|
|
73
|
-
Value::RegExp(_) => "null"
|
|
90
|
+
Value::RegExp(_) => buf.push_str("null"),
|
|
74
91
|
}
|
|
75
92
|
}
|
|
76
93
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
94
|
+
/// Append an escaped JSON string body (without the surrounding quotes)
|
|
95
|
+
/// to `buf`. Optimised for the common case where the input is ASCII and
|
|
96
|
+
/// contains no characters that need escaping — we fast-pass the bytes
|
|
97
|
+
/// straight through, only falling into the per-char path on a hit.
|
|
98
|
+
fn escape_json_string_into(buf: &mut String, s: &str) {
|
|
99
|
+
let bytes = s.as_bytes();
|
|
100
|
+
let mut start = 0usize;
|
|
101
|
+
for (i, &b) in bytes.iter().enumerate() {
|
|
102
|
+
// Anything < 0x20 is a JSON control char that must be escaped;
|
|
103
|
+
// 0x22 (`"`) and 0x5C (`\`) also need an explicit escape; bytes
|
|
104
|
+
// ≥ 0x80 are the start of a multi-byte UTF-8 sequence, which is
|
|
105
|
+
// valid JSON as-is.
|
|
106
|
+
if b < 0x20 || b == b'"' || b == b'\\' {
|
|
107
|
+
// Flush the run of clean bytes before this one in one push.
|
|
108
|
+
if start < i {
|
|
109
|
+
// SAFETY: `s` is `&str`, every byte in `start..i` was a
|
|
110
|
+
// single-byte ASCII char (we only stop on ASCII triggers
|
|
111
|
+
// below 0x80), so the slice is a valid `&str`.
|
|
112
|
+
buf.push_str(&s[start..i]);
|
|
113
|
+
}
|
|
114
|
+
match b {
|
|
115
|
+
b'"' => buf.push_str("\\\""),
|
|
116
|
+
b'\\' => buf.push_str("\\\\"),
|
|
117
|
+
b'\n' => buf.push_str("\\n"),
|
|
118
|
+
b'\r' => buf.push_str("\\r"),
|
|
119
|
+
b'\t' => buf.push_str("\\t"),
|
|
120
|
+
b'\x08' => buf.push_str("\\b"),
|
|
121
|
+
b'\x0c' => buf.push_str("\\f"),
|
|
122
|
+
_ => {
|
|
123
|
+
use std::fmt::Write;
|
|
124
|
+
let _ = write!(buf, "\\u{:04x}", b as u32);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
start = i + 1;
|
|
88
128
|
}
|
|
89
129
|
}
|
|
90
|
-
|
|
130
|
+
if start < bytes.len() {
|
|
131
|
+
buf.push_str(&s[start..]);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#[allow(dead_code)]
|
|
136
|
+
fn escape_json_string(s: &str) -> String {
|
|
137
|
+
let mut buf = String::with_capacity(s.len());
|
|
138
|
+
escape_json_string_into(&mut buf, s);
|
|
139
|
+
buf
|
|
91
140
|
}
|
|
92
141
|
|
|
93
142
|
fn parse_value(input: &str) -> Result<(Value, &str), String> {
|
|
@@ -228,7 +277,7 @@ fn parse_number(input: &str) -> Result<(Value, &str), String> {
|
|
|
228
277
|
|
|
229
278
|
let num_str: String = chars[..end].iter().collect();
|
|
230
279
|
let byte_len: usize = chars[..end].iter().map(|c| c.len_utf8()).sum();
|
|
231
|
-
|
|
280
|
+
|
|
232
281
|
num_str
|
|
233
282
|
.parse::<f64>()
|
|
234
283
|
.map(|n| (Value::Number(n), &input[byte_len..]))
|
|
@@ -241,7 +290,7 @@ fn parse_array(input: &str) -> Result<(Value, &str), String> {
|
|
|
241
290
|
|
|
242
291
|
input = input.trim_start();
|
|
243
292
|
if let Some(rest) = input.strip_prefix(']') {
|
|
244
|
-
return Ok((Value::Array(
|
|
293
|
+
return Ok((Value::Array(VmRef::new(items)), rest));
|
|
245
294
|
}
|
|
246
295
|
|
|
247
296
|
loop {
|
|
@@ -251,7 +300,7 @@ fn parse_array(input: &str) -> Result<(Value, &str), String> {
|
|
|
251
300
|
|
|
252
301
|
match input.chars().next() {
|
|
253
302
|
Some(',') => input = &input[1..],
|
|
254
|
-
Some(']') => return Ok((Value::Array(
|
|
303
|
+
Some(']') => return Ok((Value::Array(VmRef::new(items)), &input[1..])),
|
|
255
304
|
_ => return Err("Expected ',' or ']' in array".to_string()),
|
|
256
305
|
}
|
|
257
306
|
}
|
|
@@ -263,7 +312,7 @@ fn parse_object(input: &str) -> Result<(Value, &str), String> {
|
|
|
263
312
|
|
|
264
313
|
input = input.trim_start();
|
|
265
314
|
if let Some(rest) = input.strip_prefix('}') {
|
|
266
|
-
return Ok((Value::Object(
|
|
315
|
+
return Ok((Value::Object(VmRef::new(map)), rest));
|
|
267
316
|
}
|
|
268
317
|
|
|
269
318
|
loop {
|
|
@@ -290,7 +339,7 @@ fn parse_object(input: &str) -> Result<(Value, &str), String> {
|
|
|
290
339
|
|
|
291
340
|
match input.chars().next() {
|
|
292
341
|
Some(',') => input = &input[1..],
|
|
293
|
-
Some('}') => return Ok((Value::Object(
|
|
342
|
+
Some('}') => return Ok((Value::Object(VmRef::new(map)), &input[1..])),
|
|
294
343
|
_ => return Err("Expected ',' or '}' in object".to_string()),
|
|
295
344
|
}
|
|
296
345
|
}
|
|
@@ -306,7 +355,9 @@ mod tests {
|
|
|
306
355
|
assert!(matches!(json_parse("true").unwrap(), Value::Bool(true)));
|
|
307
356
|
assert!(matches!(json_parse("false").unwrap(), Value::Bool(false)));
|
|
308
357
|
assert!(matches!(json_parse("42").unwrap(), Value::Number(n) if n == 42.0));
|
|
309
|
-
assert!(
|
|
358
|
+
assert!(
|
|
359
|
+
matches!(json_parse("\"hello\"").unwrap(), Value::String(s) if s.as_ref() == "hello")
|
|
360
|
+
);
|
|
310
361
|
}
|
|
311
362
|
|
|
312
363
|
#[test]
|
|
@@ -315,7 +366,7 @@ mod tests {
|
|
|
315
366
|
let value = json_parse(original).unwrap();
|
|
316
367
|
let stringified = json_stringify(&value);
|
|
317
368
|
let reparsed = json_parse(&stringified).unwrap();
|
|
318
|
-
|
|
369
|
+
|
|
319
370
|
match (&value, &reparsed) {
|
|
320
371
|
(Value::Object(a), Value::Object(b)) => {
|
|
321
372
|
assert_eq!(a.borrow().len(), b.borrow().len());
|