@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.
Files changed (127) hide show
  1. package/Cargo.toml +43 -0
  2. package/LICENSE +13 -0
  3. package/README.md +66 -0
  4. package/crates/js_to_tish/Cargo.toml +9 -0
  5. package/crates/js_to_tish/README.md +18 -0
  6. package/crates/js_to_tish/src/error.rs +61 -0
  7. package/crates/js_to_tish/src/lib.rs +11 -0
  8. package/crates/js_to_tish/src/span_util.rs +35 -0
  9. package/crates/js_to_tish/src/transform/expr.rs +608 -0
  10. package/crates/js_to_tish/src/transform/stmt.rs +474 -0
  11. package/crates/js_to_tish/src/transform.rs +60 -0
  12. package/crates/tish/Cargo.toml +44 -0
  13. package/crates/tish/src/main.rs +585 -0
  14. package/crates/tish/src/repl_completion.rs +200 -0
  15. package/crates/tish/tests/integration_test.rs +726 -0
  16. package/crates/tish_ast/Cargo.toml +7 -0
  17. package/crates/tish_ast/src/ast.rs +494 -0
  18. package/crates/tish_ast/src/lib.rs +5 -0
  19. package/crates/tish_build_utils/Cargo.toml +5 -0
  20. package/crates/tish_build_utils/src/lib.rs +175 -0
  21. package/crates/tish_builtins/Cargo.toml +12 -0
  22. package/crates/tish_builtins/src/array.rs +410 -0
  23. package/crates/tish_builtins/src/globals.rs +197 -0
  24. package/crates/tish_builtins/src/helpers.rs +38 -0
  25. package/crates/tish_builtins/src/lib.rs +14 -0
  26. package/crates/tish_builtins/src/math.rs +80 -0
  27. package/crates/tish_builtins/src/object.rs +36 -0
  28. package/crates/tish_builtins/src/string.rs +253 -0
  29. package/crates/tish_bytecode/Cargo.toml +15 -0
  30. package/crates/tish_bytecode/src/chunk.rs +97 -0
  31. package/crates/tish_bytecode/src/compiler.rs +1361 -0
  32. package/crates/tish_bytecode/src/encoding.rs +100 -0
  33. package/crates/tish_bytecode/src/lib.rs +19 -0
  34. package/crates/tish_bytecode/src/opcode.rs +110 -0
  35. package/crates/tish_bytecode/src/peephole.rs +159 -0
  36. package/crates/tish_bytecode/src/serialize.rs +163 -0
  37. package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
  38. package/crates/tish_bytecode/tests/shortcircuit.rs +49 -0
  39. package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
  40. package/crates/tish_compile/Cargo.toml +21 -0
  41. package/crates/tish_compile/src/codegen.rs +3316 -0
  42. package/crates/tish_compile/src/lib.rs +71 -0
  43. package/crates/tish_compile/src/resolve.rs +631 -0
  44. package/crates/tish_compile/src/types.rs +304 -0
  45. package/crates/tish_compile_js/Cargo.toml +16 -0
  46. package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
  47. package/crates/tish_compile_js/src/codegen.rs +794 -0
  48. package/crates/tish_compile_js/src/error.rs +20 -0
  49. package/crates/tish_compile_js/src/js_intrinsics.rs +82 -0
  50. package/crates/tish_compile_js/src/lib.rs +27 -0
  51. package/crates/tish_compile_js/src/tests_jsx.rs +32 -0
  52. package/crates/tish_compiler_wasm/Cargo.toml +19 -0
  53. package/crates/tish_compiler_wasm/src/lib.rs +55 -0
  54. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +462 -0
  55. package/crates/tish_core/Cargo.toml +11 -0
  56. package/crates/tish_core/src/console_style.rs +128 -0
  57. package/crates/tish_core/src/json.rs +327 -0
  58. package/crates/tish_core/src/lib.rs +15 -0
  59. package/crates/tish_core/src/macros.rs +37 -0
  60. package/crates/tish_core/src/uri.rs +115 -0
  61. package/crates/tish_core/src/value.rs +376 -0
  62. package/crates/tish_cranelift/Cargo.toml +17 -0
  63. package/crates/tish_cranelift/src/lib.rs +41 -0
  64. package/crates/tish_cranelift/src/link.rs +120 -0
  65. package/crates/tish_cranelift/src/lower.rs +77 -0
  66. package/crates/tish_cranelift_runtime/Cargo.toml +19 -0
  67. package/crates/tish_cranelift_runtime/src/lib.rs +43 -0
  68. package/crates/tish_eval/Cargo.toml +26 -0
  69. package/crates/tish_eval/src/eval.rs +3205 -0
  70. package/crates/tish_eval/src/http.rs +122 -0
  71. package/crates/tish_eval/src/lib.rs +59 -0
  72. package/crates/tish_eval/src/natives.rs +301 -0
  73. package/crates/tish_eval/src/promise.rs +173 -0
  74. package/crates/tish_eval/src/regex.rs +298 -0
  75. package/crates/tish_eval/src/timers.rs +111 -0
  76. package/crates/tish_eval/src/value.rs +224 -0
  77. package/crates/tish_eval/src/value_convert.rs +85 -0
  78. package/crates/tish_fmt/Cargo.toml +16 -0
  79. package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
  80. package/crates/tish_fmt/src/lib.rs +884 -0
  81. package/crates/tish_jsx_web/Cargo.toml +7 -0
  82. package/crates/tish_jsx_web/README.md +18 -0
  83. package/crates/tish_jsx_web/src/lib.rs +157 -0
  84. package/crates/tish_jsx_web/vendor/Lattish.tish +347 -0
  85. package/crates/tish_lexer/Cargo.toml +7 -0
  86. package/crates/tish_lexer/src/lib.rs +430 -0
  87. package/crates/tish_lexer/src/token.rs +155 -0
  88. package/crates/tish_lint/Cargo.toml +17 -0
  89. package/crates/tish_lint/src/bin/tish-lint.rs +77 -0
  90. package/crates/tish_lint/src/lib.rs +278 -0
  91. package/crates/tish_llvm/Cargo.toml +11 -0
  92. package/crates/tish_llvm/src/lib.rs +106 -0
  93. package/crates/tish_lsp/Cargo.toml +22 -0
  94. package/crates/tish_lsp/README.md +26 -0
  95. package/crates/tish_lsp/src/main.rs +615 -0
  96. package/crates/tish_native/Cargo.toml +14 -0
  97. package/crates/tish_native/src/build.rs +102 -0
  98. package/crates/tish_native/src/lib.rs +237 -0
  99. package/crates/tish_opt/Cargo.toml +11 -0
  100. package/crates/tish_opt/src/lib.rs +896 -0
  101. package/crates/tish_parser/Cargo.toml +9 -0
  102. package/crates/tish_parser/src/lib.rs +123 -0
  103. package/crates/tish_parser/src/parser.rs +1714 -0
  104. package/crates/tish_runtime/Cargo.toml +26 -0
  105. package/crates/tish_runtime/src/http.rs +308 -0
  106. package/crates/tish_runtime/src/http_fetch.rs +453 -0
  107. package/crates/tish_runtime/src/lib.rs +1004 -0
  108. package/crates/tish_runtime/src/native_promise.rs +26 -0
  109. package/crates/tish_runtime/src/promise.rs +77 -0
  110. package/crates/tish_runtime/src/promise_io.rs +41 -0
  111. package/crates/tish_runtime/src/timers.rs +125 -0
  112. package/crates/tish_runtime/src/ws.rs +725 -0
  113. package/crates/tish_runtime/tests/fetch_readable_stream.rs +99 -0
  114. package/crates/tish_vm/Cargo.toml +31 -0
  115. package/crates/tish_vm/src/lib.rs +39 -0
  116. package/crates/tish_vm/src/vm.rs +1399 -0
  117. package/crates/tish_wasm/Cargo.toml +13 -0
  118. package/crates/tish_wasm/src/lib.rs +358 -0
  119. package/crates/tish_wasm_runtime/Cargo.toml +25 -0
  120. package/crates/tish_wasm_runtime/src/lib.rs +36 -0
  121. package/justfile +260 -0
  122. package/package.json +8 -3
  123. package/platform/darwin-arm64/tish +0 -0
  124. package/platform/darwin-x64/tish +0 -0
  125. package/platform/linux-arm64/tish +0 -0
  126. package/platform/linux-x64/tish +0 -0
  127. package/platform/win32-x64/tish.exe +0 -0
@@ -0,0 +1,100 @@
1
+ //! Canonical u8 encoding for AST operators in bytecode.
2
+ //! Single source of truth: compiler encodes with *\_to_u8, VM decodes with u8_to\_*.
3
+
4
+ use tish_ast::{BinOp, CompoundOp, UnaryOp};
5
+
6
+ /// Encode BinOp for bytecode operand. Used by compiler.
7
+ pub fn binop_to_u8(op: BinOp) -> u8 {
8
+ use tish_ast::BinOp::*;
9
+ match op {
10
+ Add => 0,
11
+ Sub => 1,
12
+ Mul => 2,
13
+ Div => 3,
14
+ Mod => 4,
15
+ Pow => 5,
16
+ Eq => 6,
17
+ Ne => 7,
18
+ StrictEq => 8,
19
+ StrictNe => 9,
20
+ Lt => 10,
21
+ Le => 11,
22
+ Gt => 12,
23
+ Ge => 13,
24
+ And => 14,
25
+ Or => 15,
26
+ BitAnd => 16,
27
+ BitOr => 17,
28
+ BitXor => 18,
29
+ Shl => 19,
30
+ Shr => 20,
31
+ In => 21,
32
+ }
33
+ }
34
+
35
+ /// Decode bytecode operand to BinOp. Used by VM.
36
+ pub fn u8_to_binop(b: u8) -> Option<BinOp> {
37
+ use tish_ast::BinOp::*;
38
+ Some(match b {
39
+ 0 => Add,
40
+ 1 => Sub,
41
+ 2 => Mul,
42
+ 3 => Div,
43
+ 4 => Mod,
44
+ 5 => Pow,
45
+ 6 => Eq,
46
+ 7 => Ne,
47
+ 8 => StrictEq,
48
+ 9 => StrictNe,
49
+ 10 => Lt,
50
+ 11 => Le,
51
+ 12 => Gt,
52
+ 13 => Ge,
53
+ 14 => And,
54
+ 15 => Or,
55
+ 16 => BitAnd,
56
+ 17 => BitOr,
57
+ 18 => BitXor,
58
+ 19 => Shl,
59
+ 20 => Shr,
60
+ 21 => In,
61
+ _ => return None,
62
+ })
63
+ }
64
+
65
+ /// Encode CompoundOp for bytecode (same numeric subset as BinOp: Add,Sub,Mul,Div,Mod).
66
+ pub fn compound_op_to_u8(op: CompoundOp) -> u8 {
67
+ use tish_ast::CompoundOp::*;
68
+ match op {
69
+ Add => 0,
70
+ Sub => 1,
71
+ Mul => 2,
72
+ Div => 3,
73
+ Mod => 4,
74
+ }
75
+ }
76
+
77
+ /// Encode UnaryOp for bytecode operand. Used by compiler.
78
+ pub fn unaryop_to_u8(op: UnaryOp) -> u8 {
79
+ use tish_ast::UnaryOp::*;
80
+ match op {
81
+ Not => 0,
82
+ Neg => 1,
83
+ Pos => 2,
84
+ BitNot => 3,
85
+ Void => 4,
86
+ }
87
+ }
88
+
89
+ /// Decode bytecode operand to UnaryOp. Used by VM.
90
+ pub fn u8_to_unaryop(b: u8) -> Option<UnaryOp> {
91
+ use tish_ast::UnaryOp::*;
92
+ Some(match b {
93
+ 0 => Not,
94
+ 1 => Neg,
95
+ 2 => Pos,
96
+ 3 => BitNot,
97
+ 4 => Void,
98
+ _ => return None,
99
+ })
100
+ }
@@ -0,0 +1,19 @@
1
+ //! Bytecode compiler for Tish.
2
+ //! Compiles AST to stack-based bytecode for VM execution.
3
+
4
+ mod chunk;
5
+ mod compiler;
6
+ mod encoding;
7
+ mod opcode;
8
+ mod peephole;
9
+ mod serialize;
10
+
11
+ pub const NO_REST_PARAM: u16 = 0xFFFF;
12
+
13
+ pub use chunk::{Chunk, Constant};
14
+ pub use compiler::{
15
+ compile, compile_for_repl, compile_for_repl_unoptimized, compile_unoptimized, CompileError,
16
+ };
17
+ pub use encoding::{binop_to_u8, compound_op_to_u8, u8_to_binop, u8_to_unaryop, unaryop_to_u8};
18
+ pub use opcode::Opcode;
19
+ pub use serialize::{deserialize, serialize};
@@ -0,0 +1,110 @@
1
+ //! Bytecode opcodes for the Tish VM.
2
+
3
+ /// Stack-based bytecode opcodes.
4
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
5
+ #[repr(u8)]
6
+ pub enum Opcode {
7
+ /// No operation
8
+ Nop = 0,
9
+ /// Push constant from constants table (operand: u16 index)
10
+ LoadConst = 1,
11
+ /// Load variable from scope (operand: u16 name index)
12
+ LoadVar = 2,
13
+ /// Store top of stack to variable (operand: u16 name index)
14
+ StoreVar = 3,
15
+ /// Discard top of stack
16
+ Pop = 4,
17
+ /// Duplicate top of stack
18
+ Dup = 5,
19
+ /// Call function with n args (operand: u16 arg count). Callee and args on stack.
20
+ Call = 6,
21
+ /// Return from function. Top of stack is return value.
22
+ Return = 7,
23
+ /// Unconditional jump forward (operand: u16 byte offset)
24
+ Jump = 8,
25
+ /// Pop top; if falsy, jump forward (operand: u16 byte offset)
26
+ JumpIfFalse = 9,
27
+ /// Unconditional jump backward (operand: u16 byte offset)
28
+ JumpBack = 10,
29
+ /// Binary operation (operand: u8 BinOp variant). Pops left, right; pushes result.
30
+ BinOp = 11,
31
+ /// Unary operation (operand: u8 UnaryOp variant). Pops operand; pushes result.
32
+ UnaryOp = 12,
33
+ /// Get property: obj.prop (operand: u16 prop name index). Pops obj; pushes value.
34
+ GetMember = 13,
35
+ /// Set property: obj.prop = val (operand: u16 prop name index). Pops obj, val.
36
+ SetMember = 14,
37
+ /// Get index: obj[idx]. Pops obj, idx; pushes value.
38
+ GetIndex = 15,
39
+ /// Set index: obj[idx] = val. Pops obj, idx, val.
40
+ SetIndex = 16,
41
+ /// Create array with n elements (operand: u16 count). Elements on stack.
42
+ NewArray = 17,
43
+ /// Create object with n key-value pairs (operand: u16 count). Keys and vals interleaved.
44
+ NewObject = 18,
45
+ /// Load from global scope (operand: u16 name index)
46
+ LoadGlobal = 19,
47
+ /// Store to global scope (operand: u16 name index)
48
+ StoreGlobal = 20,
49
+ /// Create closure: push function (operand: u16 chunk index for nested function)
50
+ Closure = 21,
51
+ /// Pop and discard n values (operand: u16 count)
52
+ PopN = 22,
53
+ /// Load `this` or receiver (for method calls)
54
+ LoadThis = 23,
55
+ /// Throw: pop value, unwind to catch handler, push value, jump
56
+ Throw = 24,
57
+ /// EnterTry: push handler (catch offset u16). Catch offset = bytes from end of this insn.
58
+ EnterTry = 25,
59
+ /// ExitTry: pop try handler
60
+ ExitTry = 26,
61
+ /// Concat arrays: pop right, pop left, push left.concat(right). For spread.
62
+ ConcatArray = 27,
63
+ /// Merge objects: pop right, pop left, push Object.assign({}, left, right). For object spread.
64
+ MergeObject = 28,
65
+ /// Call with spread: pop args array, pop callee, call callee(...args).
66
+ CallSpread = 29,
67
+ /// Get property optional: like GetMember but returns Null if obj is null or prop missing.
68
+ GetMemberOptional = 30,
69
+ /// Pop array, sort numerically in place (operand: u8 0=asc, 1=desc), push array.
70
+ /// Fast path for arr.sort((a,b)=>a-b) / arr.sort((a,b)=>b-a).
71
+ ArraySortNumeric = 31,
72
+ /// Pop array, sort by numeric property (operands: u16 prop_name_const_idx, u16 0=asc/1=desc).
73
+ /// Fast path for arr.sort((a,b)=>a.prop-b.prop).
74
+ ArraySortByProperty = 32,
75
+ /// arr.map(x => x) - identity, returns array clone.
76
+ ArrayMapIdentity = 33,
77
+ /// arr.map(x => x op const) or arr.map(x => const op x). Operands: u8 binop, u16 const_idx, u8 param_left (0=param on left e.g. x*2, 1=param on right e.g. 2*x).
78
+ ArrayMapBinOp = 34,
79
+ /// arr.filter(x => x op const) or arr.filter(x => const op x). Operands: u8 binop, u16 const_idx, u8 param_left. Keeps elements where result is truthy.
80
+ ArrayFilterBinOp = 35,
81
+ /// Load built-in module export. Operands: u16 spec_const_idx, u16 export_name_const_idx. Pushes Value.
82
+ LoadNativeExport = 36,
83
+ }
84
+
85
+ impl Opcode {
86
+ /// Decode byte to opcode. Safe for b in 0..=36 (matches #[repr(u8)] discriminants).
87
+ #[inline]
88
+ pub fn from_u8(b: u8) -> Option<Opcode> {
89
+ if b <= 36 {
90
+ Some(unsafe { std::mem::transmute(b) })
91
+ } else {
92
+ None
93
+ }
94
+ }
95
+
96
+ /// Size in bytes of this instruction at `ip` (including operands). Returns None if truncated.
97
+ pub fn instruction_size(self, code: &[u8], ip: usize) -> Option<usize> {
98
+ let size = match self {
99
+ Opcode::Nop | Opcode::Pop | Opcode::Dup | Opcode::Return | Opcode::ExitTry
100
+ | Opcode::ArrayMapIdentity => 1,
101
+ Opcode::ArraySortByProperty | Opcode::ArrayMapBinOp | Opcode::ArrayFilterBinOp
102
+ | Opcode::LoadNativeExport => 5,
103
+ _ => 3,
104
+ };
105
+ if ip + size > code.len() {
106
+ return None;
107
+ }
108
+ Some(size)
109
+ }
110
+ }
@@ -0,0 +1,159 @@
1
+ //! Peephole optimizations on bytecode (post-emission).
2
+ //! B2 from optimization plan: jump chaining, etc.
3
+
4
+ use crate::opcode::Opcode;
5
+ use crate::Chunk;
6
+
7
+ fn read_u16(code: &[u8], pos: usize) -> u16 {
8
+ if pos + 1 >= code.len() {
9
+ return 0;
10
+ }
11
+ let a = code[pos] as u16;
12
+ let b = code[pos + 1] as u16;
13
+ (a << 8) | b
14
+ }
15
+
16
+ fn read_i16(code: &[u8], pos: usize) -> i16 {
17
+ read_u16(code, pos) as i16
18
+ }
19
+
20
+ fn write_u16(code: &mut [u8], pos: usize, v: u16) {
21
+ if pos + 1 < code.len() {
22
+ let bytes = v.to_be_bytes();
23
+ code[pos] = bytes[0];
24
+ code[pos + 1] = bytes[1];
25
+ }
26
+ }
27
+
28
+ /// Size of instruction at `ip` in bytes. Returns None if invalid/truncated.
29
+ fn instruction_size(code: &[u8], ip: usize) -> Option<usize> {
30
+ if ip >= code.len() {
31
+ return None;
32
+ }
33
+ let opcode = Opcode::from_u8(code[ip])?;
34
+ opcode.instruction_size(code, ip)
35
+ }
36
+
37
+ /// For a Jump or JumpIfFalse at `ip`, return the final target IP after following
38
+ /// a chain of jumps (Jump -> Jump -> ... -> non-jump).
39
+ fn final_jump_target(code: &[u8], jump_ip: usize) -> Option<usize> {
40
+ let mut ip = jump_ip;
41
+ let mut visited = 0u32;
42
+ const MAX_CHAIN: u32 = 1000;
43
+ loop {
44
+ if visited > MAX_CHAIN {
45
+ return None;
46
+ }
47
+ visited += 1;
48
+ let _ = instruction_size(code, ip)?;
49
+ let op = Opcode::from_u8(code[ip])?;
50
+ match op {
51
+ Opcode::Jump => {
52
+ let offset = read_i16(code, ip + 1) as isize;
53
+ ip = (ip as isize + 3 + offset).max(0) as usize;
54
+ }
55
+ Opcode::JumpIfFalse => {
56
+ let offset = read_i16(code, ip + 1) as isize;
57
+ ip = (ip as isize + 3 + offset).max(0) as usize;
58
+ }
59
+ _ => return Some(ip),
60
+ }
61
+ }
62
+ }
63
+
64
+ /// Replace instruction at [ip..ip+len) with Nops (preserves length, no offset updates).
65
+ fn nop_out(code: &mut [u8], ip: usize, len: usize) {
66
+ for i in 0..len {
67
+ if ip + i < code.len() {
68
+ code[ip + i] = Opcode::Nop as u8;
69
+ }
70
+ }
71
+ }
72
+
73
+ /// Remove redundant Dup + Pop (dup top then discard = no-op).
74
+ fn remove_dup_pop(code: &mut [u8]) {
75
+ let mut ip = 0;
76
+ while ip + 2 <= code.len() {
77
+ if Opcode::from_u8(code[ip]) == Some(Opcode::Dup)
78
+ && Opcode::from_u8(code[ip + 1]) == Some(Opcode::Pop)
79
+ {
80
+ nop_out(code, ip, 2);
81
+ }
82
+ ip += instruction_size(code, ip).unwrap_or(1);
83
+ }
84
+ }
85
+
86
+ /// Remove redundant LoadConst + Pop (load constant then discard = no-op).
87
+ fn remove_loadconst_pop(code: &mut [u8]) {
88
+ let mut ip = 0;
89
+ while ip + 4 <= code.len() {
90
+ if Opcode::from_u8(code[ip]) == Some(Opcode::LoadConst)
91
+ && Opcode::from_u8(code[ip + 3]) == Some(Opcode::Pop)
92
+ {
93
+ nop_out(code, ip, 4);
94
+ }
95
+ ip += instruction_size(code, ip).unwrap_or(1);
96
+ }
97
+ }
98
+
99
+ /// Replace no-op jumps (Jump with offset 0) with Nops.
100
+ fn remove_noop_jumps(code: &mut [u8]) {
101
+ let mut ip = 0;
102
+ while ip < code.len() {
103
+ if Opcode::from_u8(code[ip]) == Some(Opcode::Jump) {
104
+ let offset = read_u16(code, ip + 1);
105
+ if offset == 0 {
106
+ nop_out(code, ip, 3);
107
+ }
108
+ }
109
+ ip += instruction_size(code, ip).unwrap_or(1);
110
+ }
111
+ }
112
+
113
+ /// Apply jump chaining: if Jump/JumpIfFalse targets another jump, update to
114
+ /// jump directly to the final target.
115
+ fn chain_jumps(code: &mut [u8]) {
116
+ let mut ip = 0;
117
+ while ip < code.len() {
118
+ let op = match Opcode::from_u8(code[ip]) {
119
+ Some(o) => o,
120
+ None => {
121
+ ip += 1;
122
+ continue;
123
+ }
124
+ };
125
+ let size = match instruction_size(code, ip) {
126
+ Some(s) => s,
127
+ None => break,
128
+ };
129
+ match op {
130
+ Opcode::Jump | Opcode::JumpIfFalse => {
131
+ let current_offset = read_i16(code, ip + 1) as isize;
132
+ let current_target = (ip as isize + 3 + current_offset).max(0) as usize;
133
+ if let Some(final_target) = final_jump_target(code, ip) {
134
+ if final_target != current_target {
135
+ let new_offset = final_target as i32 - (ip + 3) as i32;
136
+ let bytes = (new_offset as i16).to_be_bytes();
137
+ if ip + 2 < code.len() {
138
+ code[ip + 1] = bytes[0];
139
+ code[ip + 2] = bytes[1];
140
+ }
141
+ }
142
+ }
143
+ }
144
+ _ => {}
145
+ }
146
+ ip += size;
147
+ }
148
+ }
149
+
150
+ /// Run peephole optimizations on a chunk (and nested chunks).
151
+ pub fn optimize(chunk: &mut Chunk) {
152
+ remove_loadconst_pop(&mut chunk.code);
153
+ remove_dup_pop(&mut chunk.code);
154
+ remove_noop_jumps(&mut chunk.code);
155
+ chain_jumps(&mut chunk.code);
156
+ for nested in &mut chunk.nested {
157
+ optimize(nested);
158
+ }
159
+ }
@@ -0,0 +1,163 @@
1
+ //! Chunk serialization for embedding in native/WASM outputs.
2
+ //! Format: code, constants, names, nested (recursive).
3
+
4
+ use std::sync::Arc;
5
+
6
+ use super::{Chunk, Constant};
7
+
8
+ /// Serialize a chunk to bytes (includes nested chunks for functions).
9
+ pub fn serialize(chunk: &Chunk) -> Vec<u8> {
10
+ let mut out = Vec::new();
11
+ out.extend_from_slice(&(chunk.code.len() as u64).to_le_bytes());
12
+ out.extend_from_slice(&chunk.code);
13
+ out.extend_from_slice(&(chunk.constants.len() as u64).to_le_bytes());
14
+ for c in &chunk.constants {
15
+ match c {
16
+ Constant::Number(n) => {
17
+ out.push(0);
18
+ out.extend_from_slice(&n.to_le_bytes());
19
+ }
20
+ Constant::String(s) => {
21
+ out.push(1);
22
+ let b = s.as_bytes();
23
+ out.extend_from_slice(&(b.len() as u64).to_le_bytes());
24
+ out.extend_from_slice(b);
25
+ }
26
+ Constant::Bool(b) => {
27
+ out.push(2);
28
+ out.push(if *b { 1 } else { 0 });
29
+ }
30
+ Constant::Null => out.push(3),
31
+ Constant::Closure(idx) => {
32
+ out.push(4);
33
+ out.extend_from_slice(&(*idx as u64).to_le_bytes());
34
+ }
35
+ }
36
+ }
37
+ out.extend_from_slice(&(chunk.names.len() as u64).to_le_bytes());
38
+ for n in &chunk.names {
39
+ let b = n.as_bytes();
40
+ out.extend_from_slice(&(b.len() as u64).to_le_bytes());
41
+ out.extend_from_slice(b);
42
+ }
43
+ out.extend_from_slice(&(chunk.nested.len() as u64).to_le_bytes());
44
+ for nested in &chunk.nested {
45
+ let nested_bytes = serialize(nested);
46
+ out.extend_from_slice(&(nested_bytes.len() as u64).to_le_bytes());
47
+ out.extend_from_slice(&nested_bytes);
48
+ }
49
+ out.extend_from_slice(&chunk.rest_param_index.to_le_bytes());
50
+ out.extend_from_slice(&chunk.param_count.to_le_bytes());
51
+ out
52
+ }
53
+
54
+ /// Deserialize a chunk from bytes.
55
+ pub fn deserialize(mut data: &[u8]) -> Result<Chunk, String> {
56
+ let read_u64 = |d: &mut &[u8]| {
57
+ if d.len() < 8 {
58
+ return Err("Unexpected EOF".to_string());
59
+ }
60
+ let (head, tail) = d.split_at(8);
61
+ *d = tail;
62
+ Ok(u64::from_le_bytes(head.try_into().unwrap()))
63
+ };
64
+
65
+ let code_len = read_u64(&mut data)? as usize;
66
+ if data.len() < code_len {
67
+ return Err("Truncated code".to_string());
68
+ }
69
+ let (code_bytes, rest) = data.split_at(code_len);
70
+ data = rest;
71
+ let code = code_bytes.to_vec();
72
+
73
+ let const_count = read_u64(&mut data)? as usize;
74
+ let mut constants = Vec::with_capacity(const_count);
75
+ for _ in 0..const_count {
76
+ if data.is_empty() {
77
+ return Err("Truncated constant".to_string());
78
+ }
79
+ let tag = data[0];
80
+ data = &data[1..];
81
+ let c = match tag {
82
+ 0 => {
83
+ if data.len() < 8 {
84
+ return Err("Truncated number".to_string());
85
+ }
86
+ let (n_bytes, rest) = data.split_at(8);
87
+ data = rest;
88
+ Constant::Number(f64::from_le_bytes(n_bytes.try_into().unwrap()))
89
+ }
90
+ 1 => {
91
+ let str_len = read_u64(&mut data)? as usize;
92
+ if data.len() < str_len {
93
+ return Err("Truncated string".to_string());
94
+ }
95
+ let (s_bytes, rest) = data.split_at(str_len);
96
+ data = rest;
97
+ Constant::String(Arc::from(String::from_utf8_lossy(s_bytes).into_owned()))
98
+ }
99
+ 2 => {
100
+ if data.is_empty() {
101
+ return Err("Truncated bool".to_string());
102
+ }
103
+ let b = data[0] != 0;
104
+ data = &data[1..];
105
+ Constant::Bool(b)
106
+ }
107
+ 3 => Constant::Null,
108
+ 4 => {
109
+ let idx = read_u64(&mut data)? as usize;
110
+ Constant::Closure(idx)
111
+ }
112
+ _ => return Err(format!("Unknown constant tag: {}", tag)),
113
+ };
114
+ constants.push(c);
115
+ }
116
+
117
+ let names_count = read_u64(&mut data)? as usize;
118
+ let mut names = Vec::with_capacity(names_count);
119
+ for _ in 0..names_count {
120
+ let n_len = read_u64(&mut data)? as usize;
121
+ if data.len() < n_len {
122
+ return Err("Truncated name".to_string());
123
+ }
124
+ let (n_bytes, rest) = data.split_at(n_len);
125
+ data = rest;
126
+ names.push(Arc::from(String::from_utf8_lossy(n_bytes).into_owned()));
127
+ }
128
+
129
+ let nested_count = read_u64(&mut data)? as usize;
130
+ let mut nested = Vec::with_capacity(nested_count);
131
+ for _ in 0..nested_count {
132
+ let nested_len = read_u64(&mut data)? as usize;
133
+ if data.len() < nested_len {
134
+ return Err("Truncated nested chunk".to_string());
135
+ }
136
+ let (nested_data, rest) = data.split_at(nested_len);
137
+ data = rest;
138
+ nested.push(deserialize(nested_data)?);
139
+ }
140
+
141
+ let rest_param_index = if data.len() >= 2 {
142
+ let (r_bytes, rest) = data.split_at(2);
143
+ data = rest;
144
+ u16::from_le_bytes(r_bytes.try_into().unwrap())
145
+ } else {
146
+ super::NO_REST_PARAM
147
+ };
148
+ let param_count = if data.len() >= 2 {
149
+ let (p_bytes, _) = data.split_at(2);
150
+ u16::from_le_bytes(p_bytes.try_into().unwrap())
151
+ } else {
152
+ 0
153
+ };
154
+
155
+ Ok(Chunk {
156
+ code,
157
+ constants,
158
+ names,
159
+ nested,
160
+ rest_param_index,
161
+ param_count,
162
+ })
163
+ }
@@ -0,0 +1,84 @@
1
+ //! Verify AST optimization (constant folding) yields expected bytecode.
2
+ //! Uses tish_opt::optimize before compile to match the pipeline used by run/compile.
3
+
4
+ use tish_bytecode::{compile, Chunk, Opcode};
5
+ use tish_parser::parse;
6
+
7
+ fn chunk_contains_opcode(chunk: &Chunk, op: u8) -> bool {
8
+ if chunk.code.contains(&op) {
9
+ return true;
10
+ }
11
+ for nested in &chunk.nested {
12
+ if chunk_contains_opcode(nested, op) {
13
+ return true;
14
+ }
15
+ }
16
+ false
17
+ }
18
+
19
+ /// 1 + 2 should fold to constant 3; no BinOp in bytecode.
20
+ #[test]
21
+ fn constant_fold_binary_no_binop() {
22
+ let source = "1 + 2";
23
+ let program = parse(source).expect("parse");
24
+ let optimized = tish_opt::optimize(&program);
25
+ let chunk = compile(&optimized).expect("compile");
26
+ assert!(
27
+ !chunk_contains_opcode(&chunk, Opcode::BinOp as u8),
28
+ "Expected no BinOp for 1+2 after constant folding"
29
+ );
30
+ assert!(
31
+ chunk.constants.iter().any(|c| matches!(c, tish_bytecode::Constant::Number(n) if (*n - 3.0).abs() < f64::EPSILON)),
32
+ "Expected constant 3 in chunk"
33
+ );
34
+ }
35
+
36
+ /// Peephole: Dup+Pop should be removed (replaced with Nop Nop).
37
+ /// Compile a program that may emit Dup+Pop; verify it runs and chunk has no consecutive Dup,Pop.
38
+ fn chunk_has_dup_pop_sequence(code: &[u8]) -> bool {
39
+ let dup = Opcode::Dup as u8;
40
+ let pop = Opcode::Pop as u8;
41
+ for i in 0..code.len().saturating_sub(1) {
42
+ if code[i] == dup && code[i + 1] == pop {
43
+ return true;
44
+ }
45
+ }
46
+ false
47
+ }
48
+
49
+ fn chunk_contains_dup_pop(chunk: &Chunk) -> bool {
50
+ if chunk_has_dup_pop_sequence(&chunk.code) {
51
+ return true;
52
+ }
53
+ for nested in &chunk.nested {
54
+ if chunk_contains_dup_pop(nested) {
55
+ return true;
56
+ }
57
+ }
58
+ false
59
+ }
60
+
61
+ #[test]
62
+ fn peephole_remove_dup_pop() {
63
+ let source = "let o = {a:1}; o?.a";
64
+ let program = parse(source).expect("parse");
65
+ let optimized = tish_opt::optimize(&program);
66
+ let chunk = compile(&optimized).expect("compile");
67
+ assert!(
68
+ !chunk_contains_dup_pop(&chunk),
69
+ "Peephole should remove Dup+Pop sequences"
70
+ );
71
+ }
72
+
73
+ /// -42 should fold to constant -42; no UnaryOp in bytecode.
74
+ #[test]
75
+ fn constant_fold_unary_no_unaryop() {
76
+ let source = "-42";
77
+ let program = parse(source).expect("parse");
78
+ let optimized = tish_opt::optimize(&program);
79
+ let chunk = compile(&optimized).expect("compile");
80
+ assert!(
81
+ !chunk_contains_opcode(&chunk, Opcode::UnaryOp as u8),
82
+ "Expected no UnaryOp for -42 after constant folding"
83
+ );
84
+ }
@@ -0,0 +1,49 @@
1
+ //! Verify && and || short-circuit (JumpIfFalse before evaluating right side).
2
+ use std::path::Path;
3
+ use tish_bytecode::{compile, compile_unoptimized, Opcode};
4
+ use tish_compile::{merge_modules, resolve_project};
5
+ use tish_parser::parse;
6
+ use tish_opt;
7
+ use tish_vm;
8
+
9
+ #[test]
10
+ fn test_and_shortcircuit_emits_jump() {
11
+ let source = "let x = null; let y = x != null && x.foo;";
12
+ let program = parse(source).expect("parse");
13
+ let chunk = compile_unoptimized(&program).expect("compile");
14
+ let code = &chunk.code;
15
+ let has_jump_if_false = code.windows(1).any(|w| w[0] == Opcode::JumpIfFalse as u8);
16
+ assert!(has_jump_if_false, "And should emit JumpIfFalse for short-circuit");
17
+ }
18
+
19
+ #[test]
20
+ fn test_and_shortcircuit_runs_unoptimized() {
21
+ let source = "let x = null; let y = x != null && x.foo;";
22
+ let program = parse(source).expect("parse");
23
+ let chunk = compile_unoptimized(&program).expect("compile");
24
+ let result = tish_vm::run(&chunk);
25
+ assert!(result.is_ok(), "Should not throw (short-circuit avoids x.foo): {:?}", result.err());
26
+ }
27
+
28
+ #[test]
29
+ fn test_and_shortcircuit_runs_optimized() {
30
+ let source = "let x = null; let y = x != null && x.foo;";
31
+ let program = parse(source).expect("parse");
32
+ let program = tish_opt::optimize(&program);
33
+ let chunk = tish_bytecode::compile(&program).expect("compile");
34
+ let result = tish_vm::run(&chunk);
35
+ assert!(result.is_ok(), "Should not throw with peephole (short-circuit): {:?}", result.err());
36
+ }
37
+
38
+ #[test]
39
+ fn test_and_shortcircuit_via_resolve_project() {
40
+ let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/shortcircuit.tish");
41
+ let path = path.canonicalize().expect("path");
42
+ let project_root = path.parent().unwrap();
43
+ let modules = resolve_project(&path, Some(project_root)).expect("resolve");
44
+ let program = merge_modules(modules).expect("merge");
45
+ let program = tish_opt::optimize(&program); // Mirror CLI
46
+ let chunk = compile(&program).expect("compile");
47
+ let result = tish_vm::run(&chunk);
48
+ assert!(result.is_ok(), "Should not throw via resolve+merge+opt (CLI path): {:?}", result.err());
49
+ }