@tishlang/tish 1.0.28 → 1.0.33
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 +1 -0
- package/crates/js_to_tish/src/transform/expr.rs +15 -6
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/src/main.rs +8 -55
- package/crates/tish/tests/integration_test.rs +4 -3
- package/crates/tish_ast/src/ast.rs +65 -2
- package/crates/tish_build_utils/src/lib.rs +10 -2
- package/crates/tish_builtins/src/construct.rs +177 -0
- package/crates/tish_builtins/src/globals.rs +3 -5
- package/crates/tish_builtins/src/helpers.rs +2 -3
- package/crates/tish_builtins/src/lib.rs +1 -0
- package/crates/tish_builtins/src/object.rs +3 -4
- package/crates/tish_bytecode/src/compiler.rs +85 -11
- package/crates/tish_bytecode/src/opcode.rs +7 -3
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/codegen.rs +233 -71
- package/crates/tish_compile/src/lib.rs +35 -0
- package/crates/tish_compile_js/Cargo.toml +1 -1
- package/crates/tish_compile_js/src/codegen.rs +43 -147
- package/crates/tish_compile_js/src/lib.rs +4 -7
- package/crates/tish_compile_js/src/tests_jsx.rs +89 -19
- package/crates/tish_compiler_wasm/src/lib.rs +2 -3
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/console_style.rs +7 -1
- package/crates/tish_core/src/json.rs +1 -2
- package/crates/tish_core/src/macros.rs +2 -3
- package/crates/tish_core/src/value.rs +10 -5
- package/crates/tish_eval/Cargo.toml +2 -0
- package/crates/tish_eval/src/eval.rs +149 -72
- package/crates/tish_eval/src/http.rs +3 -4
- package/crates/tish_eval/src/regex.rs +3 -2
- package/crates/tish_eval/src/value.rs +11 -13
- package/crates/tish_eval/src/value_convert.rs +4 -8
- package/crates/tish_fmt/src/lib.rs +49 -10
- package/crates/tish_jsx_web/Cargo.toml +1 -1
- package/crates/tish_jsx_web/README.md +3 -16
- package/crates/tish_jsx_web/src/lib.rs +2 -157
- package/crates/tish_lexer/src/token.rs +2 -0
- package/crates/tish_lint/src/lib.rs +9 -0
- package/crates/tish_lsp/README.md +1 -1
- package/crates/tish_native/src/build.rs +16 -2
- package/crates/tish_opt/src/lib.rs +15 -0
- package/crates/tish_parser/src/lib.rs +101 -1
- package/crates/tish_parser/src/parser.rs +161 -50
- package/crates/tish_runtime/src/http.rs +4 -5
- package/crates/tish_runtime/src/http_fetch.rs +9 -10
- package/crates/tish_runtime/src/lib.rs +9 -2
- package/crates/tish_runtime/src/promise.rs +2 -3
- package/crates/tish_runtime/src/promise_io.rs +2 -3
- package/crates/tish_runtime/src/ws.rs +7 -7
- package/crates/tish_ui/Cargo.toml +17 -0
- package/crates/tish_ui/src/jsx.rs +390 -0
- package/crates/tish_ui/src/lib.rs +16 -0
- package/crates/tish_ui/src/runtime/hooks.rs +122 -0
- package/crates/tish_ui/src/runtime/mod.rs +173 -0
- package/crates/tish_vm/src/vm.rs +121 -27
- package/justfile +3 -3
- 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
- package/crates/tish_compile_js/src/js_intrinsics.rs +0 -82
- package/crates/tish_jsx_web/vendor/Lattish.tish +0 -362
|
@@ -10,8 +10,8 @@ repository = { workspace = true }
|
|
|
10
10
|
default = []
|
|
11
11
|
|
|
12
12
|
[dependencies]
|
|
13
|
-
tishlang_jsx_web = { path = "../tish_jsx_web", version = ">=0.1" }
|
|
14
13
|
tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
|
|
15
14
|
tishlang_compile = { path = "../tish_compile", version = ">=0.1" }
|
|
16
15
|
tishlang_opt = { path = "../tish_opt", version = ">=0.1" }
|
|
17
16
|
tishlang_parser = { path = "../tish_parser", version = ">=0.1" }
|
|
17
|
+
tishlang_ui = { path = "../tish_ui", default-features = false, features = ["compiler"] }
|
|
@@ -2,30 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
use tishlang_ast::{
|
|
4
4
|
ArrayElement, ArrowBody, BinOp, CallArg, CompoundOp, DestructElement, DestructPattern, Expr,
|
|
5
|
-
|
|
6
|
-
Statement, UnaryOp,
|
|
5
|
+
FunParam, Literal, LogicalAssignOp, MemberProp, ObjectProp, Program, Statement, UnaryOp,
|
|
7
6
|
};
|
|
8
7
|
|
|
9
8
|
use crate::error::CompileError;
|
|
10
|
-
use crate::js_intrinsics::{JsIntrinsic, JsIntrinsics};
|
|
11
|
-
|
|
12
|
-
/// How JSX lowers for `--target js`. Native targets never use this.
|
|
13
|
-
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
14
|
-
pub enum JsxMode {
|
|
15
|
-
/// `h(tag, props, [children])` + `Fragment` (Lattish.tish). Import `h` / `Fragment` from lattish.
|
|
16
|
-
#[default]
|
|
17
|
-
LattishH,
|
|
18
|
-
/// Vnode `__vdom_h` + `window.__lattishVdomPatch` (Lattish createRoot must use patch).
|
|
19
|
-
Vdom,
|
|
20
|
-
}
|
|
21
9
|
|
|
22
10
|
struct Codegen {
|
|
23
11
|
output: String,
|
|
24
12
|
indent: usize,
|
|
25
13
|
in_async: bool,
|
|
26
|
-
uses_jsx: bool,
|
|
27
|
-
intrinsics: JsIntrinsics,
|
|
28
|
-
jsx_mode: JsxMode,
|
|
29
14
|
}
|
|
30
15
|
|
|
31
16
|
fn stmt_terminates_switch(stmt: Option<&Statement>) -> bool {
|
|
@@ -36,14 +21,11 @@ fn stmt_terminates_switch(stmt: Option<&Statement>) -> bool {
|
|
|
36
21
|
}
|
|
37
22
|
|
|
38
23
|
impl Codegen {
|
|
39
|
-
fn new(
|
|
24
|
+
fn new() -> Self {
|
|
40
25
|
Self {
|
|
41
26
|
output: String::new(),
|
|
42
27
|
indent: 0,
|
|
43
28
|
in_async: false,
|
|
44
|
-
uses_jsx: false,
|
|
45
|
-
intrinsics: JsIntrinsics::new(),
|
|
46
|
-
jsx_mode,
|
|
47
29
|
}
|
|
48
30
|
}
|
|
49
31
|
|
|
@@ -72,25 +54,9 @@ impl Codegen {
|
|
|
72
54
|
|
|
73
55
|
fn emit_program(&mut self, program: &Program) -> Result<(), CompileError> {
|
|
74
56
|
self.write("// Generated by tishlang_compile_js\n");
|
|
75
|
-
// First pass: check if JSX is used (we'll set uses_jsx during emit)
|
|
76
57
|
for stmt in &program.statements {
|
|
77
58
|
self.emit_statement(stmt)?;
|
|
78
59
|
}
|
|
79
|
-
self.output = self
|
|
80
|
-
.intrinsics
|
|
81
|
-
.prepend_runtime_preamble(std::mem::take(&mut self.output));
|
|
82
|
-
if self.uses_jsx {
|
|
83
|
-
match self.jsx_mode {
|
|
84
|
-
JsxMode::Vdom => {
|
|
85
|
-
self.output = format!(
|
|
86
|
-
"{}\n{}",
|
|
87
|
-
tishlang_jsx_web::VDOM_PRELUDE,
|
|
88
|
-
self.output
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
JsxMode::LattishH => {}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
60
|
Ok(())
|
|
95
61
|
}
|
|
96
62
|
|
|
@@ -331,20 +297,34 @@ impl Codegen {
|
|
|
331
297
|
|
|
332
298
|
fn emit_params(
|
|
333
299
|
&mut self,
|
|
334
|
-
params: &[
|
|
300
|
+
params: &[FunParam],
|
|
335
301
|
rest_param: Option<&tishlang_ast::TypedParam>,
|
|
336
302
|
) -> Result<String, CompileError> {
|
|
337
|
-
let mut parts: Vec<String> =
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
303
|
+
let mut parts: Vec<String> = Vec::new();
|
|
304
|
+
for p in params {
|
|
305
|
+
match p {
|
|
306
|
+
FunParam::Simple(tp) => {
|
|
307
|
+
let n = Self::escape_ident(tp.name.as_ref());
|
|
308
|
+
let s = if let Some(ref d) = tp.default {
|
|
309
|
+
format!("{} = {}", n, self.emit_expr(d)?)
|
|
310
|
+
} else {
|
|
311
|
+
n
|
|
312
|
+
};
|
|
313
|
+
parts.push(s);
|
|
345
314
|
}
|
|
346
|
-
|
|
347
|
-
|
|
315
|
+
FunParam::Destructure {
|
|
316
|
+
pattern,
|
|
317
|
+
type_ann: _,
|
|
318
|
+
default,
|
|
319
|
+
} => {
|
|
320
|
+
let mut s = self.emit_destruct_pattern(pattern)?;
|
|
321
|
+
if let Some(ref d) = default {
|
|
322
|
+
s = format!("{} = {}", s, self.emit_expr(d)?);
|
|
323
|
+
}
|
|
324
|
+
parts.push(s);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
348
328
|
if let Some(rest) = rest_param {
|
|
349
329
|
parts.push(format!("...{}", Self::escape_ident(rest.name.as_ref())));
|
|
350
330
|
}
|
|
@@ -448,16 +428,6 @@ impl Codegen {
|
|
|
448
428
|
}
|
|
449
429
|
}
|
|
450
430
|
Expr::Call { callee, args, .. } => {
|
|
451
|
-
if let Some(kind) =
|
|
452
|
-
JsIntrinsics::classify_call(callee.as_ref(), args)?
|
|
453
|
-
{
|
|
454
|
-
self.intrinsics.mark(kind);
|
|
455
|
-
if kind == JsIntrinsic::Uint8Array {
|
|
456
|
-
let n = self.emit_call_arg(&args[0])?;
|
|
457
|
-
return Ok(JsIntrinsics::emit_expr(kind, &n));
|
|
458
|
-
}
|
|
459
|
-
return Ok(JsIntrinsics::emit_expr(kind, ""));
|
|
460
|
-
}
|
|
461
431
|
let c = self.emit_expr(callee)?;
|
|
462
432
|
let arg_strs: Result<Vec<_>, _> =
|
|
463
433
|
args.iter().map(|a| self.emit_call_arg(a)).collect();
|
|
@@ -465,6 +435,13 @@ impl Codegen {
|
|
|
465
435
|
// Tish uses null for undefined (e.g. empty array pop/shift)
|
|
466
436
|
format!("({}({}) ?? null)", c, arg_strs)
|
|
467
437
|
}
|
|
438
|
+
Expr::New { callee, args, .. } => {
|
|
439
|
+
let c = self.emit_expr(callee)?;
|
|
440
|
+
let arg_strs: Result<Vec<_>, _> =
|
|
441
|
+
args.iter().map(|a| self.emit_call_arg(a)).collect();
|
|
442
|
+
let arg_strs = arg_strs?.join(", ");
|
|
443
|
+
format!("(new {}({}) ?? null)", c, arg_strs)
|
|
444
|
+
}
|
|
468
445
|
Expr::Member {
|
|
469
446
|
object,
|
|
470
447
|
prop,
|
|
@@ -652,39 +629,11 @@ impl Codegen {
|
|
|
652
629
|
let o = self.emit_expr(operand)?;
|
|
653
630
|
format!("(await {})", o)
|
|
654
631
|
}
|
|
655
|
-
Expr::JsxElement {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
format!("{:?}", tag.as_ref())
|
|
661
|
-
};
|
|
662
|
-
let props_str = self.emit_jsx_props(props)?;
|
|
663
|
-
let children_strs: Result<Vec<_>, _> =
|
|
664
|
-
children.iter().map(|c| self.emit_jsx_child(c)).collect();
|
|
665
|
-
let children_str = children_strs?.join(", ");
|
|
666
|
-
match self.jsx_mode {
|
|
667
|
-
JsxMode::LattishH => {
|
|
668
|
-
format!("h({}, {}, [{}])", tag_str, props_str, children_str)
|
|
669
|
-
}
|
|
670
|
-
JsxMode::Vdom => {
|
|
671
|
-
format!("__vdom_h({}, {}, [{}])", tag_str, props_str, children_str)
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
Expr::JsxFragment { children, .. } => {
|
|
676
|
-
self.uses_jsx = true;
|
|
677
|
-
let children_strs: Result<Vec<_>, _> =
|
|
678
|
-
children.iter().map(|c| self.emit_jsx_child(c)).collect();
|
|
679
|
-
let children_str = children_strs?.join(", ");
|
|
680
|
-
match self.jsx_mode {
|
|
681
|
-
JsxMode::LattishH => {
|
|
682
|
-
format!("h(Fragment, null, [{}])", children_str)
|
|
683
|
-
}
|
|
684
|
-
JsxMode::Vdom => {
|
|
685
|
-
format!("__vdom_h(__Fragment, null, [{}])", children_str)
|
|
686
|
-
}
|
|
687
|
-
}
|
|
632
|
+
Expr::JsxElement { .. } | Expr::JsxFragment { .. } => {
|
|
633
|
+
tishlang_ui::jsx::emit_jsx_js(expr, &mut |e| {
|
|
634
|
+
self.emit_expr(e).map_err(|ce| ce.message)
|
|
635
|
+
})
|
|
636
|
+
.map_err(|m| CompileError { message: m })?
|
|
688
637
|
}
|
|
689
638
|
Expr::NativeModuleLoad { spec, .. } => {
|
|
690
639
|
return Err(CompileError {
|
|
@@ -704,68 +653,16 @@ impl Codegen {
|
|
|
704
653
|
}
|
|
705
654
|
}
|
|
706
655
|
|
|
707
|
-
fn emit_jsx_props(&mut self, props: &[JsxProp]) -> Result<String, CompileError> {
|
|
708
|
-
if props.is_empty() {
|
|
709
|
-
return Ok("null".to_string());
|
|
710
|
-
}
|
|
711
|
-
let parts: Result<Vec<_>, _> = props
|
|
712
|
-
.iter()
|
|
713
|
-
.map(|p| match p {
|
|
714
|
-
JsxProp::Attr { name, value } => {
|
|
715
|
-
let val = match value {
|
|
716
|
-
JsxAttrValue::String(s) => format!("{:?}", s.as_ref()),
|
|
717
|
-
JsxAttrValue::Expr(e) => self.emit_expr(e)?,
|
|
718
|
-
JsxAttrValue::ImplicitTrue => "true".to_string(),
|
|
719
|
-
};
|
|
720
|
-
let key = name.as_ref();
|
|
721
|
-
Ok(if key.chars().all(|c| c.is_alphanumeric() || c == '_') {
|
|
722
|
-
format!("{}: {}", key, val)
|
|
723
|
-
} else {
|
|
724
|
-
format!("{:?}: {}", key, val)
|
|
725
|
-
})
|
|
726
|
-
}
|
|
727
|
-
JsxProp::Spread(e) => Ok(format!("...{}", self.emit_expr(e)?)),
|
|
728
|
-
})
|
|
729
|
-
.collect();
|
|
730
|
-
Ok(format!("{{ {} }}", parts?.join(", ")))
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
fn emit_jsx_child(&mut self, child: &JsxChild) -> Result<String, CompileError> {
|
|
734
|
-
match child {
|
|
735
|
-
JsxChild::Text(s) => Ok(format!("{:?}", s.as_ref())),
|
|
736
|
-
JsxChild::Expr(e) => {
|
|
737
|
-
let inner = self.emit_expr(e)?;
|
|
738
|
-
// Only wrap literals we know are primitives (number, bool, null). Never wrap:
|
|
739
|
-
// string/template (already strings), JSX (elements), Call (components), Array/Ident (may hold elements).
|
|
740
|
-
let needs_string = matches!(
|
|
741
|
-
e,
|
|
742
|
-
Expr::Literal {
|
|
743
|
-
value: Literal::Number(_) | Literal::Bool(_) | Literal::Null,
|
|
744
|
-
..
|
|
745
|
-
}
|
|
746
|
-
);
|
|
747
|
-
Ok(if needs_string {
|
|
748
|
-
format!("String({})", inner)
|
|
749
|
-
} else {
|
|
750
|
-
inner
|
|
751
|
-
})
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
656
|
}
|
|
756
657
|
|
|
757
|
-
/// Compile a single program (no imports) to JavaScript.
|
|
758
|
-
pub fn compile_with_jsx(
|
|
759
|
-
program: &Program,
|
|
760
|
-
optimize: bool,
|
|
761
|
-
jsx_mode: JsxMode,
|
|
762
|
-
) -> Result<String, CompileError> {
|
|
658
|
+
/// Compile a single program (no imports) to JavaScript. JSX lowers to `h` / `Fragment` (Lattish).
|
|
659
|
+
pub fn compile_with_jsx(program: &Program, optimize: bool) -> Result<String, CompileError> {
|
|
763
660
|
let program = if optimize {
|
|
764
661
|
tishlang_opt::optimize(program)
|
|
765
662
|
} else {
|
|
766
663
|
program.clone()
|
|
767
664
|
};
|
|
768
|
-
let mut g = Codegen::new(
|
|
665
|
+
let mut g = Codegen::new();
|
|
769
666
|
g.emit_program(&program)?;
|
|
770
667
|
Ok(g.output)
|
|
771
668
|
}
|
|
@@ -776,7 +673,6 @@ pub fn compile_project_with_jsx(
|
|
|
776
673
|
entry_path: &std::path::Path,
|
|
777
674
|
project_root: Option<&std::path::Path>,
|
|
778
675
|
optimize: bool,
|
|
779
|
-
jsx_mode: JsxMode,
|
|
780
676
|
) -> Result<String, CompileError> {
|
|
781
677
|
use tishlang_ast::Statement;
|
|
782
678
|
let modules = tishlang_compile::resolve_project(entry_path, project_root)
|
|
@@ -802,7 +698,7 @@ pub fn compile_project_with_jsx(
|
|
|
802
698
|
None
|
|
803
699
|
}
|
|
804
700
|
});
|
|
805
|
-
let mut js = compile_with_jsx(&program, optimize
|
|
701
|
+
let mut js = compile_with_jsx(&program, optimize)?;
|
|
806
702
|
if let Some(name) = default_export {
|
|
807
703
|
js.push_str(&format!("\nexport default {};\n", name));
|
|
808
704
|
}
|
|
@@ -3,19 +3,16 @@
|
|
|
3
3
|
|
|
4
4
|
mod codegen;
|
|
5
5
|
mod error;
|
|
6
|
-
mod js_intrinsics;
|
|
7
6
|
|
|
8
7
|
#[cfg(test)]
|
|
9
8
|
mod tests_jsx;
|
|
10
9
|
|
|
11
|
-
pub use codegen::{compile_project_with_jsx, compile_with_jsx
|
|
10
|
+
pub use codegen::{compile_project_with_jsx, compile_with_jsx};
|
|
12
11
|
pub use error::CompileError;
|
|
13
12
|
|
|
14
|
-
///
|
|
15
|
-
/// `lattish` (e.g. `useState`, `createRoot`); the merged bundle includes the JSX runtime from that
|
|
16
|
-
/// module. JSX-only files can use `import {} from "lattish"` to pull it in without bindings.
|
|
13
|
+
/// JSX lowers to `h` / `Fragment`; merge the `lattish` runtime for hooks and DOM.
|
|
17
14
|
pub fn compile(program: &tishlang_ast::Program, optimize: bool) -> Result<String, CompileError> {
|
|
18
|
-
compile_with_jsx(program, optimize
|
|
15
|
+
compile_with_jsx(program, optimize)
|
|
19
16
|
}
|
|
20
17
|
|
|
21
18
|
pub fn compile_project(
|
|
@@ -23,5 +20,5 @@ pub fn compile_project(
|
|
|
23
20
|
project_root: Option<&std::path::Path>,
|
|
24
21
|
optimize: bool,
|
|
25
22
|
) -> Result<String, CompileError> {
|
|
26
|
-
compile_project_with_jsx(entry_path, project_root, optimize
|
|
23
|
+
compile_project_with_jsx(entry_path, project_root, optimize)
|
|
27
24
|
}
|
|
@@ -4,13 +4,13 @@ mod tests {
|
|
|
4
4
|
|
|
5
5
|
use tishlang_parser::parse;
|
|
6
6
|
|
|
7
|
-
use crate::{compile_project_with_jsx, compile_with_jsx
|
|
7
|
+
use crate::{compile_project_with_jsx, compile_with_jsx};
|
|
8
8
|
|
|
9
9
|
#[test]
|
|
10
10
|
fn lattish_jsx_emits_h_with_children_array() {
|
|
11
11
|
let src = r#"fn X() { return <div class="a">{"hi"}</div> }"#;
|
|
12
12
|
let program = parse(src).unwrap();
|
|
13
|
-
let js = compile_with_jsx(&program, false
|
|
13
|
+
let js = compile_with_jsx(&program, false).unwrap();
|
|
14
14
|
assert!(js.contains("h(\"div\", { class: \"a\" }, [\"hi\"])"), "{}", js);
|
|
15
15
|
assert!(!js.contains("function __h("));
|
|
16
16
|
}
|
|
@@ -19,7 +19,7 @@ mod tests {
|
|
|
19
19
|
fn fragment_lattish_uses_fragment_symbol() {
|
|
20
20
|
let src = "fn X() { return <><b>{\"1\"}</b></> }";
|
|
21
21
|
let program = parse(src).unwrap();
|
|
22
|
-
let js = compile_with_jsx(&program, false
|
|
22
|
+
let js = compile_with_jsx(&program, false).unwrap();
|
|
23
23
|
assert!(js.contains("h(Fragment, null, ["));
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -27,7 +27,7 @@ mod tests {
|
|
|
27
27
|
fn jsx_text_whitespace_coalesced() {
|
|
28
28
|
let src = r#"fn X() { return <p>First paragraph</p> }"#;
|
|
29
29
|
let program = parse(src).unwrap();
|
|
30
|
-
let js = compile_with_jsx(&program, false
|
|
30
|
+
let js = compile_with_jsx(&program, false).unwrap();
|
|
31
31
|
assert!(
|
|
32
32
|
js.contains("\"First paragraph\""),
|
|
33
33
|
"expected \"First paragraph\" in output, got: {}",
|
|
@@ -43,7 +43,7 @@ mod tests {
|
|
|
43
43
|
fn jsx_text_whitespace_coalesced_multiline() {
|
|
44
44
|
let src = "fn App() {\n return <p>First paragraph</p>\n}";
|
|
45
45
|
let program = parse(src).unwrap();
|
|
46
|
-
let js = compile_with_jsx(&program, false
|
|
46
|
+
let js = compile_with_jsx(&program, false).unwrap();
|
|
47
47
|
assert!(
|
|
48
48
|
js.contains("\"First paragraph\""),
|
|
49
49
|
"multiline: expected \"First paragraph\", got: {}",
|
|
@@ -56,7 +56,7 @@ mod tests {
|
|
|
56
56
|
// Punctuation (e.g. !) concatenates without space: "work!" not "work !"
|
|
57
57
|
let src = r#"fn X() { return <p>work!</p> }"#;
|
|
58
58
|
let program = parse(src).unwrap();
|
|
59
|
-
let js = compile_with_jsx(&program, false
|
|
59
|
+
let js = compile_with_jsx(&program, false).unwrap();
|
|
60
60
|
assert!(js.contains(r#""work!""#), "expected 'work!', got: {}", &js[..400.min(js.len())]);
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -64,7 +64,7 @@ mod tests {
|
|
|
64
64
|
fn jsx_text_emojis() {
|
|
65
65
|
let src = r#"fn X() { return <p>hello 😔</p> }"#;
|
|
66
66
|
let program = parse(src).unwrap();
|
|
67
|
-
let js = compile_with_jsx(&program, false
|
|
67
|
+
let js = compile_with_jsx(&program, false).unwrap();
|
|
68
68
|
assert!(js.contains("😔"), "expected emoji, got: {}", &js[..400.min(js.len())]);
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -78,7 +78,7 @@ mod tests {
|
|
|
78
78
|
f.write_all(src.as_bytes()).unwrap();
|
|
79
79
|
f.sync_all().unwrap();
|
|
80
80
|
drop(f);
|
|
81
|
-
let js = compile_project_with_jsx(&path, Some(&dir), false
|
|
81
|
+
let js = compile_project_with_jsx(&path, Some(&dir), false)
|
|
82
82
|
.expect("compile_project_with_jsx failed");
|
|
83
83
|
assert!(
|
|
84
84
|
js.contains("\"First paragraph\""),
|
|
@@ -89,12 +89,14 @@ mod tests {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
#[test]
|
|
92
|
-
fn
|
|
93
|
-
let src = r#"fn X() { return <
|
|
92
|
+
fn jsx_never_emits_vdom_helpers_or_prelude_flags() {
|
|
93
|
+
let src = r#"fn X() { return <div class="x">{"a"}</div> }"#;
|
|
94
94
|
let program = parse(src).unwrap();
|
|
95
|
-
let js = compile_with_jsx(&program, false
|
|
96
|
-
assert!(js.contains("
|
|
97
|
-
assert!(js.contains("
|
|
95
|
+
let js = compile_with_jsx(&program, false).unwrap();
|
|
96
|
+
assert!(js.contains("h(\"div\", { class: \"x\" }"), "{}", &js[..500.min(js.len())]);
|
|
97
|
+
assert!(!js.contains("__vdom_h"), "{}", &js[..600.min(js.len())]);
|
|
98
|
+
assert!(!js.contains("window.__LATTISH_JSX_VDOM"), "{}", &js[..600.min(js.len())]);
|
|
99
|
+
assert!(!js.contains("__lattishVdomPatch"), "{}", &js[..600.min(js.len())]);
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
/// Component calls like {Panel()} return DOM elements. Wrapping in String() produces [object HTMLDivElement].
|
|
@@ -105,7 +107,7 @@ fn Panel() { return <div class="p">content</div> }
|
|
|
105
107
|
fn App() { return <div>{Panel()}</div> }
|
|
106
108
|
"#;
|
|
107
109
|
let program = parse(src).unwrap();
|
|
108
|
-
let js = compile_with_jsx(&program, false
|
|
110
|
+
let js = compile_with_jsx(&program, false).unwrap();
|
|
109
111
|
assert!(
|
|
110
112
|
js.contains("Panel()"),
|
|
111
113
|
"component call should appear as Panel(), got: {}",
|
|
@@ -123,7 +125,7 @@ fn App() { return <div>{Panel()}</div> }
|
|
|
123
125
|
fn jsx_nested_element_not_wrapped_in_string() {
|
|
124
126
|
let src = r#"fn X() { return <div><span>inner</span></div> }"#;
|
|
125
127
|
let program = parse(src).unwrap();
|
|
126
|
-
let js = compile_with_jsx(&program, false
|
|
128
|
+
let js = compile_with_jsx(&program, false).unwrap();
|
|
127
129
|
assert!(
|
|
128
130
|
!js.contains("String(h("),
|
|
129
131
|
"nested JSX elements must NOT be wrapped in String(). got: {}",
|
|
@@ -136,7 +138,7 @@ fn App() { return <div>{Panel()}</div> }
|
|
|
136
138
|
fn jsx_literal_number_wrapped_in_string() {
|
|
137
139
|
let src = r#"fn X() { return <span>{42}</span> }"#;
|
|
138
140
|
let program = parse(src).unwrap();
|
|
139
|
-
let js = compile_with_jsx(&program, false
|
|
141
|
+
let js = compile_with_jsx(&program, false).unwrap();
|
|
140
142
|
assert!(
|
|
141
143
|
js.contains("String(42)"),
|
|
142
144
|
"literal number in JSX should be wrapped in String(). got: {}",
|
|
@@ -156,7 +158,7 @@ fn FileList() {
|
|
|
156
158
|
}
|
|
157
159
|
"#;
|
|
158
160
|
let program = parse(src).unwrap();
|
|
159
|
-
let js = compile_with_jsx(&program, false
|
|
161
|
+
let js = compile_with_jsx(&program, false).unwrap();
|
|
160
162
|
assert!(
|
|
161
163
|
!js.contains("String(items)"),
|
|
162
164
|
"array/ident in JSX must NOT be wrapped in String() - causes [object HTMLButtonElement]. got: {}",
|
|
@@ -177,7 +179,7 @@ fn FileList() {
|
|
|
177
179
|
>{"ok"}</button>
|
|
178
180
|
}"#;
|
|
179
181
|
let program = parse(src).expect("parse multi-line JSX with > comparison in attr");
|
|
180
|
-
let js = compile_with_jsx(&program, false
|
|
182
|
+
let js = compile_with_jsx(&program, false).expect("compile");
|
|
181
183
|
assert!(
|
|
182
184
|
js.contains("length > 0") || js.contains("length>0"),
|
|
183
185
|
"expected compiled JS to preserve greater-than comparison, got: {}",
|
|
@@ -197,7 +199,7 @@ fn FileList() {
|
|
|
197
199
|
>{"outer"}</button>
|
|
198
200
|
}"#;
|
|
199
201
|
let program = parse(src).expect("parse nested JSX inside onclick");
|
|
200
|
-
let js = compile_with_jsx(&program, false
|
|
202
|
+
let js = compile_with_jsx(&program, false).expect("compile");
|
|
201
203
|
assert!(
|
|
202
204
|
js.contains("\"inner\""),
|
|
203
205
|
"expected nested span text in output, got: {}",
|
|
@@ -209,4 +211,72 @@ fn FileList() {
|
|
|
209
211
|
&js[..900.min(js.len())]
|
|
210
212
|
);
|
|
211
213
|
}
|
|
214
|
+
|
|
215
|
+
#[test]
|
|
216
|
+
fn new_date_global_emits_valid_js_with_and_without_optimize() {
|
|
217
|
+
let src = "let epoch = new Date(0)\nconsole.log(epoch.getTime())";
|
|
218
|
+
let program = parse(src).expect("parse");
|
|
219
|
+
for optimize in [false, true] {
|
|
220
|
+
let js = compile_with_jsx(&program, optimize).expect("compile");
|
|
221
|
+
assert!(
|
|
222
|
+
js.contains("new Date(0)"),
|
|
223
|
+
"optimize={optimize}: expected `new Date(0)` in JS output:\n{js}"
|
|
224
|
+
);
|
|
225
|
+
assert!(
|
|
226
|
+
!js.contains("let epoch = new;"),
|
|
227
|
+
"optimize={optimize}: broken `new` emission (missing constructor):\n{js}"
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#[test]
|
|
233
|
+
fn new_uint8array_emits_direct_new_no_preamble() {
|
|
234
|
+
let src = "fn f(n) { return new Uint8Array(n) }";
|
|
235
|
+
let program = parse(src).expect("parse");
|
|
236
|
+
let js = compile_with_jsx(&program, false).expect("compile");
|
|
237
|
+
assert!(
|
|
238
|
+
js.contains("new Uint8Array("),
|
|
239
|
+
"expected direct new Uint8Array, got: {}",
|
|
240
|
+
&js[..500.min(js.len())]
|
|
241
|
+
);
|
|
242
|
+
assert!(
|
|
243
|
+
!js.contains("__tishUint8Array"),
|
|
244
|
+
"should not emit legacy intrinsic helper"
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#[test]
|
|
249
|
+
fn new_audio_context_emits_direct_new_no_preamble() {
|
|
250
|
+
let src = "fn f() { return new AudioContext() }";
|
|
251
|
+
let program = parse(src).expect("parse");
|
|
252
|
+
let js = compile_with_jsx(&program, false).expect("compile");
|
|
253
|
+
assert!(
|
|
254
|
+
js.contains("new AudioContext("),
|
|
255
|
+
"expected new AudioContext, got: {}",
|
|
256
|
+
&js[..500.min(js.len())]
|
|
257
|
+
);
|
|
258
|
+
assert!(
|
|
259
|
+
!js.contains("__tishWebAudioCreateContext"),
|
|
260
|
+
"should not emit legacy intrinsic helper"
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#[test]
|
|
265
|
+
fn new_class_name_emits_direct_new_js() {
|
|
266
|
+
let src = r#"
|
|
267
|
+
fn ClassName(x) {
|
|
268
|
+
return x
|
|
269
|
+
}
|
|
270
|
+
fn factory() {
|
|
271
|
+
return new ClassName(42)
|
|
272
|
+
}
|
|
273
|
+
"#;
|
|
274
|
+
let program = parse(src).expect("parse");
|
|
275
|
+
let js = compile_with_jsx(&program, false).expect("compile");
|
|
276
|
+
assert!(
|
|
277
|
+
js.contains("new ClassName("),
|
|
278
|
+
"expected new ClassName( in JS output, got: {}",
|
|
279
|
+
&js[..800.min(js.len())]
|
|
280
|
+
);
|
|
281
|
+
}
|
|
212
282
|
}
|
|
@@ -10,7 +10,6 @@ mod resolve_virtual;
|
|
|
10
10
|
use base64::Engine;
|
|
11
11
|
use resolve_virtual::{detect_cycles_virtual, merge_modules_virtual, resolve_virtual};
|
|
12
12
|
use std::collections::HashMap;
|
|
13
|
-
use tishlang_compile_js::JsxMode;
|
|
14
13
|
use wasm_bindgen::prelude::*;
|
|
15
14
|
|
|
16
15
|
#[wasm_bindgen]
|
|
@@ -24,7 +23,7 @@ pub fn compile_to_bytecode(source: &str) -> Result<String, JsValue> {
|
|
|
24
23
|
#[wasm_bindgen]
|
|
25
24
|
pub fn compile_to_js(source: &str) -> Result<String, JsValue> {
|
|
26
25
|
let program = tishlang_parser::parse(source.trim()).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
|
27
|
-
tishlang_compile_js::compile_with_jsx(&program, true
|
|
26
|
+
tishlang_compile_js::compile_with_jsx(&program, true)
|
|
28
27
|
.map_err(|e| JsValue::from_str(&e.message))
|
|
29
28
|
}
|
|
30
29
|
|
|
@@ -50,6 +49,6 @@ pub fn compile_to_js_with_imports(entry_path: &str, files_json: &str) -> Result<
|
|
|
50
49
|
detect_cycles_virtual(&modules).map_err(|e| JsValue::from_str(&e))?;
|
|
51
50
|
let program = merge_modules_virtual(modules).map_err(|e| JsValue::from_str(&e))?;
|
|
52
51
|
let program = tishlang_opt::optimize(&program);
|
|
53
|
-
tishlang_compile_js::compile_with_jsx(&program, true
|
|
52
|
+
tishlang_compile_js::compile_with_jsx(&program, true)
|
|
54
53
|
.map_err(|e| JsValue::from_str(&e.message))
|
|
55
54
|
}
|
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
//! booleans, null, and object structure are easier to scan.
|
|
5
5
|
|
|
6
6
|
use std::io::IsTerminal;
|
|
7
|
+
use std::sync::OnceLock;
|
|
7
8
|
|
|
8
9
|
use crate::Value;
|
|
9
10
|
|
|
11
|
+
static CONSOLE_USES_COLORS: OnceLock<bool> = OnceLock::new();
|
|
12
|
+
|
|
10
13
|
/// ANSI escape codes (standard 4-bit + bright black for dim).
|
|
11
14
|
const RESET: &str = "\x1b[0m";
|
|
12
15
|
/// Number: yellow (Node-style)
|
|
@@ -25,8 +28,11 @@ const PUNCT: &str = "\x1b[90m";
|
|
|
25
28
|
const SPECIAL: &str = "\x1b[90m";
|
|
26
29
|
|
|
27
30
|
/// Returns whether console output should use colors (stdout is a TTY).
|
|
31
|
+
///
|
|
32
|
+
/// Cached for the process lifetime. `is_terminal()` is a syscall; benchmarks and
|
|
33
|
+
/// scripts with many `console.log` calls must not pay it on every line.
|
|
28
34
|
pub fn use_console_colors() -> bool {
|
|
29
|
-
std::io::stdout().is_terminal()
|
|
35
|
+
*CONSOLE_USES_COLORS.get_or_init(|| std::io::stdout().is_terminal())
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
/// Format a single value for console with optional ANSI colors (Node/Bun-style).
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
use crate::Value;
|
|
4
4
|
use std::cell::RefCell;
|
|
5
|
-
use std::collections::HashMap;
|
|
6
5
|
use std::rc::Rc;
|
|
7
6
|
use std::sync::Arc;
|
|
8
7
|
|
|
@@ -260,7 +259,7 @@ fn parse_array(input: &str) -> Result<(Value, &str), String> {
|
|
|
260
259
|
|
|
261
260
|
fn parse_object(input: &str) -> Result<(Value, &str), String> {
|
|
262
261
|
let mut input = &input[1..]; // skip '{'
|
|
263
|
-
let mut map =
|
|
262
|
+
let mut map = crate::ObjectMap::default();
|
|
264
263
|
|
|
265
264
|
input = input.trim_start();
|
|
266
265
|
if let Some(rest) = input.strip_prefix('}') {
|
|
@@ -24,11 +24,10 @@
|
|
|
24
24
|
macro_rules! tish_module {
|
|
25
25
|
($($name:expr => $fn:expr),* $(,)?) => {{
|
|
26
26
|
use std::cell::RefCell;
|
|
27
|
-
use std::collections::HashMap;
|
|
28
27
|
use std::rc::Rc;
|
|
29
28
|
use std::sync::Arc;
|
|
30
|
-
use $crate::Value;
|
|
31
|
-
let mut map =
|
|
29
|
+
use $crate::{ObjectMap, Value};
|
|
30
|
+
let mut map = ObjectMap::default();
|
|
32
31
|
$(
|
|
33
32
|
map.insert(Arc::from($name), Value::Function(Rc::new($fn)));
|
|
34
33
|
)*
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
//! Unified Value type for Tish runtime values.
|
|
2
2
|
|
|
3
3
|
use std::cell::RefCell;
|
|
4
|
-
use std::collections::HashMap;
|
|
5
4
|
use std::rc::Rc;
|
|
6
5
|
use std::sync::Arc;
|
|
7
6
|
|
|
7
|
+
use ahash::AHashMap;
|
|
8
|
+
|
|
9
|
+
/// Property map for objects and other `Arc<str>` → `Value` tables (VM globals, scopes).
|
|
10
|
+
/// Uses a faster hasher than `std::collections::HashMap` for string-heavy workloads.
|
|
11
|
+
pub type ObjectMap = AHashMap<Arc<str>, Value>;
|
|
12
|
+
|
|
8
13
|
#[cfg(feature = "regex")]
|
|
9
14
|
use fancy_regex::Regex;
|
|
10
15
|
|
|
@@ -147,7 +152,7 @@ pub enum Value {
|
|
|
147
152
|
Bool(bool),
|
|
148
153
|
Null,
|
|
149
154
|
Array(Rc<RefCell<Vec<Value>>>),
|
|
150
|
-
Object(Rc<RefCell<
|
|
155
|
+
Object(Rc<RefCell<ObjectMap>>),
|
|
151
156
|
Function(NativeFn),
|
|
152
157
|
#[cfg(feature = "regex")]
|
|
153
158
|
RegExp(Rc<RefCell<TishRegExp>>),
|
|
@@ -256,8 +261,8 @@ impl Value {
|
|
|
256
261
|
Value::Array(Rc::new(RefCell::new(items)))
|
|
257
262
|
}
|
|
258
263
|
|
|
259
|
-
/// Create a new object Value from a
|
|
260
|
-
pub fn object(map:
|
|
264
|
+
/// Create a new object Value from a property map.
|
|
265
|
+
pub fn object(map: ObjectMap) -> Self {
|
|
261
266
|
Value::Object(Rc::new(RefCell::new(map)))
|
|
262
267
|
}
|
|
263
268
|
|
|
@@ -268,7 +273,7 @@ impl Value {
|
|
|
268
273
|
|
|
269
274
|
/// Create an empty object Value.
|
|
270
275
|
pub fn empty_object() -> Self {
|
|
271
|
-
Value::Object(Rc::new(RefCell::new(
|
|
276
|
+
Value::Object(Rc::new(RefCell::new(ObjectMap::default())))
|
|
272
277
|
}
|
|
273
278
|
|
|
274
279
|
/// Extract the number value, if this is a Number.
|