From 8ffe4a2b9b9d7448e52d057a5f672f32424e94b0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Dec 2025 18:09:32 +0000 Subject: [PATCH 01/42] Add bytecode VM implementation plan and core headers Design a stack-based bytecode VM as Hemlock's third execution backend: Instruction Set (82 opcodes): - Constants & Literals: 10 opcodes (CONST, ARRAY, OBJECT, CLOSURE, etc.) - Variables: 12 opcodes (GET/SET_LOCAL, GET/SET_UPVALUE, properties, indexing) - Arithmetic: 11 opcodes (ADD, SUB, MUL, DIV, MOD + i32 fast paths) - Comparison: 8 opcodes (EQ, NE, LT, LE, GT, GE + i32 fast paths) - Logical/Bitwise: 9 opcodes (NOT, BIT_AND/OR/XOR, shifts, coalesce) - Control Flow: 13 opcodes (JUMP, conditionals, BREAK, CONTINUE, FOR_IN) - Functions: 8 opcodes (CALL, CALL_METHOD, RETURN, APPLY, TAIL_CALL) - Exceptions: 6 opcodes (TRY, CATCH, FINALLY, THROW, DEFER) - Async: 8 opcodes (SPAWN, AWAIT, JOIN, CHANNEL, SEND, RECV, SELECT) - Types: 5 opcodes (TYPEOF, CAST, CHECK_TYPE, DEFINE_TYPE/ENUM) - Debug: 5 opcodes (NOP, PRINT, ASSERT, DEBUG_BREAK, HALT) Test Strategy: - 103 parity tests organized into 4 phases - Phase 1: Core language (46 tests) - primitives to error handling - Phase 2: Builtins (36 tests) - memory, I/O, async, FFI, atomics - Phase 3: Methods (4 tests) - string/array methods - Phase 4: Modules (17 tests) - imports, exports, stdlib Files added: - docs/bytecode-vm-plan.md: Complete implementation plan - src/backends/vm/instruction.h: Opcode definitions (82 ops, 79 builtins) - src/backends/vm/chunk.h: Bytecode container with constant pool - src/backends/vm/vm.h: VM state, call frames, execution API - src/backends/vm/Makefile: Build configuration --- docs/bytecode-vm-plan.md | 737 ++++++++++++++++++++++++++++++++++ src/backends/vm/Makefile | 73 ++++ src/backends/vm/chunk.h | 200 +++++++++ src/backends/vm/instruction.h | 319 +++++++++++++++ src/backends/vm/vm.h | 299 ++++++++++++++ 5 files changed, 1628 insertions(+) create mode 100644 docs/bytecode-vm-plan.md create mode 100644 src/backends/vm/Makefile create mode 100644 src/backends/vm/chunk.h create mode 100644 src/backends/vm/instruction.h create mode 100644 src/backends/vm/vm.h diff --git a/docs/bytecode-vm-plan.md b/docs/bytecode-vm-plan.md new file mode 100644 index 00000000..bdb5f467 --- /dev/null +++ b/docs/bytecode-vm-plan.md @@ -0,0 +1,737 @@ +# Hemlock Bytecode VM Implementation Plan + +## Overview + +A stack-based bytecode virtual machine as a third execution backend for Hemlock, providing faster execution than tree-walking interpretation while maintaining full parity with existing backends. + +``` +Source (.hml) + ↓ +┌─────────────────────────────┐ +│ SHARED FRONTEND │ +│ - Lexer, Parser, AST │ +│ - Resolver (variable res) │ +└─────────────────────────────┘ + ↓ ↓ ↓ +┌────────┐ ┌────────┐ ┌────────┐ +│Interp │ │ VM │ │Compiler│ +│(tree) │ │(byte) │ │(C gen) │ +└────────┘ └────────┘ └────────┘ +``` + +--- + +## Directory Structure + +``` +src/backends/vm/ +├── main.c # Entry point, CLI handling +├── vm.h # Public VM API +├── vm.c # Main execution loop (dispatch) +├── chunk.h # Bytecode chunk format +├── chunk.c # Chunk creation, constant pool +├── instruction.h # Opcode definitions +├── compiler.h # AST → bytecode compiler API +├── compiler.c # Main compilation driver +├── compile_expr.c # Expression compilation +├── compile_stmt.c # Statement compilation +├── compile_call.c # Function/method call compilation +├── frame.h # Call frame management +├── frame.c # Stack frame operations +├── debug.h # Disassembler, debugging +├── debug.c # Bytecode disassembly +└── Makefile # Build rules +``` + +--- + +## Instruction Set Architecture + +### Encoding Format + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 8-bit opcode │ operands (variable length, 0-3 bytes) │ +└─────────────────────────────────────────────────────────────┘ + +Operand types: +- None: OP alone +- Byte: OP + 1-byte immediate +- Short: OP + 2-byte immediate (big-endian) +- Constant: OP + 2-byte constant pool index +- Jump: OP + 2-byte signed offset +``` + +### Complete Instruction Set (82 opcodes) + +#### Category 1: Constants & Literals (10 opcodes) + +| Opcode | Encoding | Stack Effect | Description | +|--------|----------|--------------|-------------| +| `OP_CONST` | `[op][idx:16]` | +1 | Push constant from pool | +| `OP_CONST_BYTE` | `[op][val:8]` | +1 | Push small integer (0-255) | +| `OP_NULL` | `[op]` | +1 | Push null | +| `OP_TRUE` | `[op]` | +1 | Push true | +| `OP_FALSE` | `[op]` | +1 | Push false | +| `OP_ARRAY` | `[op][count:16]` | -(n-1) | Pop n, push array | +| `OP_OBJECT` | `[op][count:16]` | -(2n-1) | Pop n key-value pairs, push object | +| `OP_STRING_INTERP` | `[op][parts:16]` | -(n-1) | Interpolate n parts into string | +| `OP_CLOSURE` | `[op][idx:16][upvals:8]` | +1 | Create closure with upvalues | +| `OP_ENUM_VALUE` | `[op][idx:16]` | +1 | Push enum variant value | + +#### Category 2: Variables (12 opcodes) + +| Opcode | Encoding | Stack Effect | Description | +|--------|----------|--------------|-------------| +| `OP_GET_LOCAL` | `[op][slot:8]` | +1 | Load local variable | +| `OP_SET_LOCAL` | `[op][slot:8]` | 0 | Store to local (keep on stack) | +| `OP_GET_UPVALUE` | `[op][slot:8]` | +1 | Load captured variable | +| `OP_SET_UPVALUE` | `[op][slot:8]` | 0 | Store to captured variable | +| `OP_GET_GLOBAL` | `[op][idx:16]` | +1 | Load global by name | +| `OP_SET_GLOBAL` | `[op][idx:16]` | 0 | Store to global | +| `OP_DEFINE_GLOBAL` | `[op][idx:16]` | -1 | Define new global | +| `OP_GET_PROPERTY` | `[op][idx:16]` | 0 | Get object property | +| `OP_SET_PROPERTY` | `[op][idx:16]` | -1 | Set object property | +| `OP_GET_INDEX` | `[op]` | -1 | array[index] or object[key] | +| `OP_SET_INDEX` | `[op]` | -2 | array[index] = value | +| `OP_CLOSE_UPVALUE` | `[op]` | 0 | Close upvalue on stack top | + +#### Category 3: Arithmetic (11 opcodes) + +| Opcode | Encoding | Stack Effect | Description | +|--------|----------|--------------|-------------| +| `OP_ADD` | `[op]` | -1 | a + b (type promotion, string concat) | +| `OP_SUB` | `[op]` | -1 | a - b | +| `OP_MUL` | `[op]` | -1 | a * b | +| `OP_DIV` | `[op]` | -1 | a / b (always f64) | +| `OP_MOD` | `[op]` | -1 | a % b | +| `OP_NEGATE` | `[op]` | 0 | -a | +| `OP_INC` | `[op]` | 0 | ++a (in-place) | +| `OP_DEC` | `[op]` | 0 | --a (in-place) | +| `OP_ADD_I32` | `[op]` | -1 | Fast path: i32 + i32 | +| `OP_SUB_I32` | `[op]` | -1 | Fast path: i32 - i32 | +| `OP_MUL_I32` | `[op]` | -1 | Fast path: i32 * i32 | + +#### Category 4: Comparison (8 opcodes) + +| Opcode | Encoding | Stack Effect | Description | +|--------|----------|--------------|-------------| +| `OP_EQ` | `[op]` | -1 | a == b | +| `OP_NE` | `[op]` | -1 | a != b | +| `OP_LT` | `[op]` | -1 | a < b | +| `OP_LE` | `[op]` | -1 | a <= b | +| `OP_GT` | `[op]` | -1 | a > b | +| `OP_GE` | `[op]` | -1 | a >= b | +| `OP_EQ_I32` | `[op]` | -1 | Fast path: i32 == i32 | +| `OP_LT_I32` | `[op]` | -1 | Fast path: i32 < i32 | + +#### Category 5: Logical & Bitwise (9 opcodes) + +| Opcode | Encoding | Stack Effect | Description | +|--------|----------|--------------|-------------| +| `OP_NOT` | `[op]` | 0 | !a | +| `OP_BIT_NOT` | `[op]` | 0 | ~a | +| `OP_BIT_AND` | `[op]` | -1 | a & b | +| `OP_BIT_OR` | `[op]` | -1 | a \| b | +| `OP_BIT_XOR` | `[op]` | -1 | a ^ b | +| `OP_LSHIFT` | `[op]` | -1 | a << b | +| `OP_RSHIFT` | `[op]` | -1 | a >> b | +| `OP_COALESCE` | `[op][offset:16]` | 0 or -1 | a ?? b (short-circuit) | +| `OP_OPTIONAL_CHAIN` | `[op][offset:16]` | 0 | a?.b (short-circuit if null) | + +#### Category 6: Control Flow (12 opcodes) + +| Opcode | Encoding | Stack Effect | Description | +|--------|----------|--------------|-------------| +| `OP_JUMP` | `[op][offset:16]` | 0 | Unconditional jump | +| `OP_JUMP_IF_FALSE` | `[op][offset:16]` | -1 | Jump if top is falsy | +| `OP_JUMP_IF_TRUE` | `[op][offset:16]` | -1 | Jump if top is truthy | +| `OP_JUMP_IF_FALSE_POP` | `[op][offset:16]` | -1 | Jump if false, always pop | +| `OP_LOOP` | `[op][offset:16]` | 0 | Jump backward (loop) | +| `OP_BREAK` | `[op]` | 0 | Break from loop | +| `OP_CONTINUE` | `[op]` | 0 | Continue to next iteration | +| `OP_SWITCH` | `[op][cases:16]` | -1 | Jump table dispatch | +| `OP_CASE` | `[op][offset:16]` | 0 | Case label marker | +| `OP_FOR_IN_INIT` | `[op]` | +1 | Initialize for-in iterator | +| `OP_FOR_IN_NEXT` | `[op][offset:16]` | +1 or jump | Get next or jump to end | +| `OP_POP` | `[op]` | -1 | Discard top of stack | + +#### Category 7: Functions & Calls (8 opcodes) + +| Opcode | Encoding | Stack Effect | Description | +|--------|----------|--------------|-------------| +| `OP_CALL` | `[op][argc:8]` | -(argc) | Call function | +| `OP_CALL_METHOD` | `[op][idx:16][argc:8]` | -(argc) | Call method on object | +| `OP_CALL_BUILTIN` | `[op][id:16][argc:8]` | -(argc) | Call builtin function | +| `OP_RETURN` | `[op]` | special | Return from function | +| `OP_APPLY` | `[op]` | -1 | apply(fn, args_array) | +| `OP_TAIL_CALL` | `[op][argc:8]` | special | Tail call optimization | +| `OP_SUPER` | `[op][idx:16]` | 0 | Access super method | +| `OP_INVOKE` | `[op][idx:16][argc:8]` | -(argc) | Optimized method call | + +#### Category 8: Exception Handling (6 opcodes) + +| Opcode | Encoding | Stack Effect | Description | +|--------|----------|--------------|-------------| +| `OP_TRY` | `[op][catch:16][finally:16]` | 0 | Begin try block | +| `OP_CATCH` | `[op]` | +1 | Begin catch (push exception) | +| `OP_FINALLY` | `[op]` | 0 | Begin finally block | +| `OP_END_TRY` | `[op]` | 0 | End try-catch-finally | +| `OP_THROW` | `[op]` | -1 | Throw exception | +| `OP_DEFER` | `[op][idx:16]` | 0 | Register deferred call | + +#### Category 9: Async & Concurrency (8 opcodes) + +| Opcode | Encoding | Stack Effect | Description | +|--------|----------|--------------|-------------| +| `OP_SPAWN` | `[op][argc:8]` | -(argc) | Spawn async task | +| `OP_AWAIT` | `[op]` | 0 | Await task result | +| `OP_JOIN` | `[op]` | 0 | Join task (explicit) | +| `OP_DETACH` | `[op]` | -1 | Detach task | +| `OP_CHANNEL` | `[op]` | 0 | Create channel (capacity on stack) | +| `OP_SEND` | `[op]` | -2 | Send on channel | +| `OP_RECV` | `[op]` | 0 | Receive from channel | +| `OP_SELECT` | `[op]` | 0 | Select on multiple channels | + +#### Category 10: Type Operations (5 opcodes) + +| Opcode | Encoding | Stack Effect | Description | +|--------|----------|--------------|-------------| +| `OP_TYPEOF` | `[op]` | 0 | Get type string | +| `OP_CAST` | `[op][type:8]` | 0 | Explicit type cast | +| `OP_CHECK_TYPE` | `[op][type:8]` | 0 | Runtime type check | +| `OP_DEFINE_TYPE` | `[op][idx:16]` | 0 | Register type definition | +| `OP_DEFINE_ENUM` | `[op][idx:16]` | 0 | Register enum definition | + +#### Category 11: Debug & Misc (3 opcodes) + +| Opcode | Encoding | Stack Effect | Description | +|--------|----------|--------------|-------------| +| `OP_NOP` | `[op]` | 0 | No operation | +| `OP_PRINT` | `[op][argc:8]` | -(argc) | Print values | +| `OP_ASSERT` | `[op]` | -1 or -2 | Assert with optional message | + +**Total: 82 opcodes** + +--- + +## Constant Pool Design + +```c +typedef enum { + CONST_I32, + CONST_I64, + CONST_F64, + CONST_STRING, + CONST_FUNCTION, + CONST_IDENTIFIER, // for globals/properties +} ConstantType; + +typedef struct { + ConstantType type; + union { + int32_t i32; + int64_t i64; + double f64; + struct { char *data; int len; } string; + struct Chunk *function; + } as; +} Constant; + +typedef struct Chunk { + uint8_t *code; // Bytecode array + int code_count; + int code_capacity; + + Constant *constants; // Constant pool + int const_count; + int const_capacity; + + int *lines; // Line numbers for debugging + + // Function metadata + char *name; + int arity; + int upvalue_count; + bool is_async; + + // Type annotations (optional) + ValueType *param_types; + ValueType return_type; +} Chunk; +``` + +--- + +## VM Execution State + +```c +typedef struct { + Chunk *chunk; // Current bytecode chunk + uint8_t *ip; // Instruction pointer + Value *slots; // Local variable slots + int slot_count; + struct CallFrame *prev; // For call stack +} CallFrame; + +typedef struct { + // Value stack + Value *stack; + Value *stack_top; + int stack_capacity; + + // Call frames + CallFrame *frames; + int frame_count; + int frame_capacity; + + // Upvalues (for closures) + ObjUpvalue *open_upvalues; + + // Global variables + Table globals; + + // Control flow state + bool is_returning; + Value return_value; + bool is_throwing; + Value exception; + bool is_breaking; + bool is_continuing; + + // Defer stack + DeferEntry *defers; + int defer_count; + + // GC/memory + Object *objects; // All allocated objects (for cleanup) + size_t bytes_allocated; +} VM; +``` + +--- + +## Builtin Function Table + +Map builtin IDs to function pointers: + +```c +typedef Value (*BuiltinFn)(VM *vm, int argc, Value *args); + +typedef struct { + const char *name; + BuiltinFn fn; + int min_arity; + int max_arity; // -1 for variadic +} BuiltinEntry; + +// 67 builtins organized by category +static BuiltinEntry builtins[] = { + // Memory (0-10) + {"alloc", builtin_alloc, 1, 1}, + {"talloc", builtin_talloc, 2, 2}, + {"realloc", builtin_realloc, 2, 2}, + {"free", builtin_free, 1, 1}, + {"memset", builtin_memset, 3, 3}, + {"memcpy", builtin_memcpy, 3, 3}, + {"sizeof", builtin_sizeof, 1, 1}, + {"buffer", builtin_buffer, 1, 1}, + {"ptr_to_buffer", builtin_ptr_to_buffer, 2, 2}, + {"buffer_ptr", builtin_buffer_ptr, 1, 1}, + {"ptr_null", builtin_ptr_null, 0, 0}, + + // I/O (11-14) + {"print", builtin_print, 0, -1}, + {"eprint", builtin_eprint, 0, -1}, + {"read_line", builtin_read_line, 0, 0}, + {"open", builtin_open, 2, 2}, + + // Type (15-17) + {"typeof", builtin_typeof, 1, 1}, + {"assert", builtin_assert, 1, 2}, + {"panic", builtin_panic, 1, 1}, + + // Async (18-24) + {"spawn", builtin_spawn, 1, -1}, + {"join", builtin_join, 1, 1}, + {"detach", builtin_detach, 1, 1}, + {"channel", builtin_channel, 1, 1}, + {"select", builtin_select, 1, 2}, + {"task_debug_info", builtin_task_debug_info, 1, 1}, + {"apply", builtin_apply, 2, 2}, + + // Pointer ops (25-50) + {"ptr_read_i32", builtin_ptr_read_i32, 1, 1}, + {"ptr_write_i32", builtin_ptr_write_i32, 2, 2}, + // ... (all ptr_read/write variants) + + // Atomics (51-67) + {"atomic_load_i32", builtin_atomic_load_i32, 1, 1}, + {"atomic_store_i32", builtin_atomic_store_i32, 2, 2}, + {"atomic_add_i32", builtin_atomic_add_i32, 2, 2}, + // ... (all atomic variants) + {"atomic_fence", builtin_atomic_fence, 0, 0}, + + // ... more builtins +}; +``` + +--- + +## Test Suite Strategy + +### Phase 1: Core Language (46 tests) + +Run in order of dependency: + +``` +Phase 1a: Primitives & Variables (8 tests) +├── primitives.hml ✓ literals, basic types +├── booleans.hml ✓ boolean ops +├── constants.hml ✓ const declarations +├── scientific_notation.hml ✓ number parsing +├── variables.hml ✓ let, assignment +├── scope_shadowing.hml ✓ nested scopes +├── runtime_prefix_names.hml ✓ edge case names +└── string_interpolation.hml ✓ template strings + +Phase 1b: Operations (9 tests) +├── arithmetic.hml ✓ +, -, *, /, % +├── compound_assignment.hml ✓ +=, -=, etc. +├── increment_decrement.hml ✓ ++, -- +├── comparisons.hml ✓ ==, <, >, etc. +├── truthiness.hml ✓ truthy/falsy +├── logical.hml ✓ &&, ||, ! +├── null_coalesce.hml ✓ ?? +├── bitwise.hml ✓ &, |, ^, <<, >> +└── ternary.hml ✓ ? : + +Phase 1c: Control Flow (7 tests) +├── if_else.hml ✓ if/else if/else +├── loops.hml ✓ while, for +├── for_in.hml ✓ for-in iteration +├── break_continue.hml ✓ loop control +├── switch.hml ✓ switch/case +├── switch_fallthrough.hml ✓ fallthrough +└── defer.hml ✓ defer statement + +Phase 1d: Functions (8 tests) +├── functions.hml ✓ definition, calls +├── recursion.hml ✓ recursive calls +├── higher_order.hml ✓ first-class functions +├── closures.hml ✓ closure capture +├── nested_closures.hml ✓ multi-level capture +├── optional_params.hml ✓ default parameters +├── rest_params.hml ✓ ...args +└── many_params.hml ✓ large param counts + +Phase 1e: Data Structures (7 tests) +├── arrays.hml ✓ array ops +├── typed_arrays.hml ✓ array +├── objects.hml ✓ object ops +├── object_edge_cases.hml ✓ edge cases +├── type_definitions.hml ✓ define +├── enums.hml ✓ enum +└── string_utf8_iteration.hml ✓ UTF-8 + +Phase 1f: Error Handling & Types (7 tests) +├── exceptions.hml ✓ try/catch/throw +├── conversions.hml ✓ type conversion +├── type_coercion.hml ✓ coercion rules +├── type_promotion.hml ✓ i32 → i64 → f64 +├── type_checking.hml ✓ runtime checks +├── c_keyword_names.hml ✓ edge case +└── string_null_concat.hml ✓ null handling +``` + +### Phase 2: Builtins (36 tests) + +``` +Phase 2a: Memory & Pointers (5 tests) +├── memory.hml ✓ alloc/free/buffer +├── pointer_ops.hml ✓ pointer arithmetic +├── read_ptr.hml ✓ ptr_read/write +├── sizeof.hml ✓ sizeof +└── buffers.hml ✓ bounded buffers + +Phase 2b: I/O & System (8 tests) +├── print_typeof.hml ✓ print, typeof +├── file_io.hml ✓ open/read/write/close +├── env.hml ✓ getenv/setenv +├── exec.hml ✓ exec commands +├── exec_argv.hml ✓ exec with args +├── signals.hml ✓ signal handling +├── signal_tty_constants.hml ✓ signal constants +└── stack_limit.hml ✓ stack limit + +Phase 2c: Async & Concurrency (5 tests) +├── async.hml ✓ spawn/join/await +├── async_many_args.hml ✓ many async args +├── channels.hml ✓ channel ops +├── unbuffered_channels.hml ✓ unbuffered semantics +└── channel_timeout.hml ✓ timeout + +Phase 2d: FFI (5 tests) +├── ffi_callback.hml ✓ callbacks +├── ffi_float.hml ✓ float args +├── ffi_ptr_helpers.hml ✓ ptr helpers +├── ffi_struct.hml ✓ struct support +└── ffi_struct_concurrent.hml ✓ concurrent FFI + +Phase 2e: Math & Misc (8 tests) +├── math.hml ✓ math functions +├── assert.hml ✓ assertions +├── serialization.hml ✓ serialize/deserialize +├── apply.hml ✓ dynamic calls +├── atomic_operations.hml ✓ atomics +├── poll_constants.hml ✓ poll constants +├── socket_constants.hml ✓ socket constants +└── socket_nonblocking.hml ✓ non-blocking IO + +Phase 2f: Network & HTTP (2 tests) +├── http_request_fn.hml ✓ HTTP requests +└── ws_send_binary_fn.hml ✓ WebSocket +``` + +### Phase 3: Methods (4 tests) + +``` +├── string_methods.hml ✓ 19 string methods +├── string_to_bytes.hml ✓ UTF-8 bytes +├── array_methods.hml ✓ 18 array methods +└── map_filter_reduce.hml ✓ functional methods +``` + +### Phase 4: Modules (17 tests) + +``` +Phase 4a: Import/Export (6 tests) +├── named_import.hml ✓ { x } from +├── star_import.hml ✓ * from +├── namespace_import.hml ✓ import module +├── chained_import.hml ✓ re-exports +├── export_define.hml ✓ export types +└── export_extern.hml ✓ export FFI + +Phase 4b: Standard Library (11 tests) +├── stdlib_math.hml ✓ @stdlib/math +├── stdlib_json.hml ✓ @stdlib/json +├── stdlib_collections.hml ✓ @stdlib/collections +├── stdlib_encoding.hml ✓ @stdlib/encoding +├── stdlib_fs.hml ✓ @stdlib/fs +├── stdlib_hash.hml ✓ @stdlib/hash +├── stdlib_os.hml ✓ @stdlib/os +├── stdlib_strings.hml ✓ @stdlib/strings +├── stdlib_time.hml ✓ @stdlib/time +├── stdlib_path.hml ✓ @stdlib/path +└── stdlib_url.hml ✓ @stdlib/url +``` + +--- + +## Implementation Phases + +### Phase 1: Foundation (Week 1-2) +- [ ] Instruction encoding (`instruction.h`) +- [ ] Chunk structure (`chunk.h`, `chunk.c`) +- [ ] Constant pool management +- [ ] Basic VM loop with stack operations +- [ ] Disassembler for debugging (`debug.c`) + +**Milestone:** Can execute `OP_CONST`, `OP_ADD`, `OP_PRINT` + +### Phase 2: Expressions (Week 2-3) +- [ ] Arithmetic operations (all 11 opcodes) +- [ ] Comparison operations (8 opcodes) +- [ ] Logical & bitwise (9 opcodes) +- [ ] Variable load/store (12 opcodes) +- [ ] Array/object literals +- [ ] Property access +- [ ] Index operations + +**Milestone:** Pass `primitives.hml`, `arithmetic.hml`, `comparisons.hml` + +### Phase 3: Control Flow (Week 3-4) +- [ ] Jump instructions +- [ ] If/else compilation +- [ ] While/for loops +- [ ] For-in iteration +- [ ] Break/continue +- [ ] Switch statement +- [ ] Ternary operator + +**Milestone:** Pass all control flow tests (7 tests) + +### Phase 4: Functions (Week 4-5) +- [ ] Function compilation to chunks +- [ ] Call frame management +- [ ] Parameter binding +- [ ] Return handling +- [ ] Closure capture (upvalues) +- [ ] Rest parameters +- [ ] Optional parameters + +**Milestone:** Pass all function tests (8 tests) + +### Phase 5: Error Handling (Week 5-6) +- [ ] Try/catch/finally +- [ ] Exception propagation +- [ ] Defer stack +- [ ] Type checking/casting +- [ ] Runtime type errors + +**Milestone:** Pass `exceptions.hml`, `defer.hml` + +### Phase 6: Builtins (Week 6-7) +- [ ] Builtin dispatch table +- [ ] Memory builtins (alloc, free, etc.) +- [ ] I/O builtins (print, open, etc.) +- [ ] Math builtins +- [ ] Type builtins + +**Milestone:** Pass all builtin tests (36 tests) + +### Phase 7: Methods (Week 7-8) +- [ ] String method dispatch (19 methods) +- [ ] Array method dispatch (18 methods) +- [ ] Method call optimization + +**Milestone:** Pass all method tests (4 tests) + +### Phase 8: Async (Week 8-9) +- [ ] Task creation (spawn) +- [ ] Task join/await +- [ ] Channel implementation +- [ ] Select operation +- [ ] Thread-safe value copying + +**Milestone:** Pass all async tests (5 tests) + +### Phase 9: Modules (Week 9-10) +- [ ] Module loading +- [ ] Import/export handling +- [ ] Stdlib integration + +**Milestone:** Pass all module tests (17 tests) + +### Phase 10: Polish (Week 10-11) +- [ ] Performance optimization +- [ ] Fast paths for i32 operations +- [ ] Memory optimization +- [ ] Full parity verification +- [ ] Documentation + +**Milestone:** 103/103 parity tests passing + +--- + +## Parity Testing Strategy + +### Test Runner + +```bash +#!/bin/bash +# scripts/test_vm_parity.sh + +PASS=0 +FAIL=0 +INTERP_ONLY=0 +VM_ONLY=0 + +for test in tests/parity/**/*.hml; do + expected="${test%.hml}.expected" + + # Run interpreter + interp_out=$(timeout 10 ./hemlock "$test" 2>&1) + interp_rc=$? + + # Run VM + vm_out=$(timeout 10 ./hemlockvm "$test" 2>&1) + vm_rc=$? + + # Compare + if [ "$interp_out" = "$vm_out" ] && [ "$interp_rc" = "$vm_rc" ]; then + if [ "$interp_out" = "$(cat $expected)" ]; then + echo "✓ PASS: $test" + ((PASS++)) + else + echo "✗ BOTH_WRONG: $test" + ((FAIL++)) + fi + elif [ "$interp_out" = "$(cat $expected)" ]; then + echo "◐ INTERP_ONLY: $test" + ((INTERP_ONLY++)) + elif [ "$vm_out" = "$(cat $expected)" ]; then + echo "◑ VM_ONLY: $test" + ((VM_ONLY++)) + else + echo "✗ BOTH_FAIL: $test" + ((FAIL++)) + fi +done + +echo "" +echo "Results: $PASS passed, $FAIL failed, $INTERP_ONLY interp-only, $VM_ONLY vm-only" +echo "Parity: $PASS / $((PASS + INTERP_ONLY + VM_ONLY + FAIL)) tests" +``` + +### Incremental Parity Tracking + +``` +tests/parity/ +├── vm_status.json # Track VM implementation status +└── ... + +{ + "language/primitives.hml": "pass", + "language/closures.hml": "interp_only", + "builtins/async.hml": "not_started", + ... +} +``` + +### CI Integration + +```makefile +# Makefile additions + +vm: src/backends/vm/*.c + $(CC) $(CFLAGS) -o hemlockvm $^ + +test-vm: vm + ./scripts/test_vm_parity.sh + +parity-vm: hemlock hemlockvm + ./scripts/test_vm_parity.sh --strict # Fail if any test doesn't pass both +``` + +--- + +## Performance Targets + +| Metric | Tree-Walker | Bytecode VM | C Compiler | +|--------|-------------|-------------|------------| +| Startup | ~1ms | ~2ms | ~500ms (compile) | +| Fib(30) | ~800ms | ~200ms | ~50ms | +| Loop 1M | ~500ms | ~100ms | ~10ms | +| Memory | Low | Medium | Low (at runtime) | + +Target: **4-8x faster than tree-walker** for compute-heavy code. + +--- + +## Open Questions + +1. **Upvalue representation**: Open upvalues on stack vs. heap-allocated? +2. **NaN boxing**: Pack values into 64-bit with NaN tagging for faster ops? +3. **JIT preparation**: Structure bytecode to enable future JIT compilation? +4. **Debug info**: Full source mapping vs. line numbers only? + +--- + +## References + +- Crafting Interpreters (Bob Nystrom) - Bytecode VM design +- Lua 5.x source - Register-based inspiration +- Python/CPython - Stack-based bytecode +- Existing Hemlock interpreter - Semantic reference diff --git a/src/backends/vm/Makefile b/src/backends/vm/Makefile new file mode 100644 index 00000000..57437af6 --- /dev/null +++ b/src/backends/vm/Makefile @@ -0,0 +1,73 @@ +# Hemlock Bytecode VM Makefile + +CC = gcc +CFLAGS = -Wall -Wextra -g -O2 -std=c11 +CFLAGS += -I../../../include -I. + +# Source files +SRCS = main.c \ + vm.c \ + chunk.c \ + compiler.c \ + compile_expr.c \ + compile_stmt.c \ + compile_call.c \ + debug.c \ + builtins.c + +OBJS = $(SRCS:.c=.o) + +# Shared frontend objects +FRONTEND_OBJS = ../../frontend/lexer.o \ + ../../frontend/parser/parser.o \ + ../../frontend/parser/expressions.o \ + ../../frontend/parser/statements.o \ + ../../frontend/parser/patterns.o \ + ../../frontend/ast.o \ + ../../frontend/module.o + +# Interpreter objects we can reuse (values, environment) +SHARED_OBJS = ../interpreter/values.o \ + ../interpreter/environment.o \ + ../interpreter/resolver.o + +# Output binary +TARGET = hemlockvm + +.PHONY: all clean test parity + +all: $(TARGET) + +$(TARGET): $(OBJS) $(FRONTEND_OBJS) $(SHARED_OBJS) + $(CC) $(CFLAGS) -o $@ $^ -lpthread -lm -ldl -lffi + +%.o: %.c + $(CC) $(CFLAGS) -c -o $@ $< + +# Run VM-specific tests +test: $(TARGET) + @echo "Running bytecode VM tests..." + @./scripts/test_vm.sh + +# Run parity tests (compare output with interpreter) +parity: $(TARGET) + @echo "Running parity tests..." + @./scripts/test_vm_parity.sh + +# Disassemble a file (for debugging) +disasm: $(TARGET) + ./$(TARGET) --disasm $(FILE) + +clean: + rm -f $(OBJS) $(TARGET) + +# Dependencies +main.o: main.c vm.h chunk.h instruction.h +vm.o: vm.c vm.h chunk.h instruction.h +chunk.o: chunk.c chunk.h instruction.h +compiler.o: compiler.c compiler.h chunk.h instruction.h +compile_expr.o: compile_expr.c compiler.h chunk.h instruction.h +compile_stmt.o: compile_stmt.c compiler.h chunk.h instruction.h +compile_call.o: compile_call.c compiler.h chunk.h instruction.h +debug.o: debug.c debug.h chunk.h instruction.h +builtins.o: builtins.c vm.h instruction.h diff --git a/src/backends/vm/chunk.h b/src/backends/vm/chunk.h new file mode 100644 index 00000000..805ce281 --- /dev/null +++ b/src/backends/vm/chunk.h @@ -0,0 +1,200 @@ +/* + * Hemlock Bytecode VM - Chunk (Bytecode Container) + * + * A Chunk represents a compiled unit of bytecode (function, script, etc.) + * with its constant pool, debug info, and metadata. + */ + +#ifndef HEMLOCK_VM_CHUNK_H +#define HEMLOCK_VM_CHUNK_H + +#include +#include +#include "instruction.h" + +// Forward declarations +typedef struct Value Value; +typedef struct Chunk Chunk; + +// ============================================ +// Constant Pool Entry +// ============================================ + +typedef enum { + CONST_I32, // 32-bit signed integer + CONST_I64, // 64-bit signed integer + CONST_F64, // 64-bit float + CONST_STRING, // Interned string + CONST_FUNCTION, // Compiled function (Chunk*) + CONST_IDENTIFIER, // Variable/property name +} ConstantType; + +typedef struct { + ConstantType type; + union { + int32_t i32; + int64_t i64; + double f64; + struct { + char *data; + int length; + uint32_t hash; // For fast comparison + } string; + Chunk *function; + } as; +} Constant; + +// ============================================ +// Upvalue Description (for closures) +// ============================================ + +typedef struct { + uint8_t index; // Index in enclosing scope + bool is_local; // true = local in enclosing, false = upvalue of enclosing +} UpvalueDesc; + +// ============================================ +// Chunk Structure +// ============================================ + +struct Chunk { + // Bytecode + uint8_t *code; + int code_count; + int code_capacity; + + // Constant pool + Constant *constants; + int const_count; + int const_capacity; + + // Line number info (for error messages) + // Run-length encoded: [count, line, count, line, ...] + int *lines; + int lines_count; + int lines_capacity; + + // Function metadata + char *name; // Function name (NULL for script) + int arity; // Required parameter count + int optional_count; // Optional parameter count + bool has_rest_param; // Has ...rest parameter + bool is_async; // Is async function + + // Closure info + UpvalueDesc *upvalues; + int upvalue_count; + + // Type annotations (optional, for runtime checks) + TypeId *param_types; // Array of parameter types (NULL = any) + TypeId return_type; // Return type (TYPE_ID_NULL = any) + + // Scope info for locals + int local_count; // Number of local variable slots needed + int max_stack; // Maximum stack depth needed +}; + +// ============================================ +// Chunk Operations +// ============================================ + +// Create/destroy +Chunk* chunk_new(void); +void chunk_free(Chunk *chunk); + +// Write bytecode +void chunk_write_byte(Chunk *chunk, uint8_t byte, int line); +void chunk_write_short(Chunk *chunk, uint16_t value, int line); +int chunk_write_jump(Chunk *chunk, OpCode op, int line); // Returns offset for patching +void chunk_patch_jump(Chunk *chunk, int offset); + +// Add constants (returns index) +int chunk_add_constant(Chunk *chunk, Constant constant); +int chunk_add_i32(Chunk *chunk, int32_t value); +int chunk_add_i64(Chunk *chunk, int64_t value); +int chunk_add_f64(Chunk *chunk, double value); +int chunk_add_string(Chunk *chunk, const char *str, int length); +int chunk_add_function(Chunk *chunk, Chunk *function); +int chunk_add_identifier(Chunk *chunk, const char *name); + +// Query +int chunk_get_line(Chunk *chunk, int offset); +Constant* chunk_get_constant(Chunk *chunk, int index); + +// ============================================ +// Debug/Disassembly +// ============================================ + +void chunk_disassemble(Chunk *chunk, const char *name); +int chunk_disassemble_instruction(Chunk *chunk, int offset); + +// ============================================ +// Serialization (for caching compiled bytecode) +// ============================================ + +// Write chunk to binary format +int chunk_serialize(Chunk *chunk, uint8_t **out_data, size_t *out_size); + +// Read chunk from binary format +Chunk* chunk_deserialize(const uint8_t *data, size_t size); + +// ============================================ +// Chunk Builder (for compiler) +// ============================================ + +typedef struct { + Chunk *chunk; + + // Current scope tracking + struct { + char *name; + int depth; + bool is_captured; // Captured by closure + bool is_const; // Is const variable + TypeId type; // Declared type (or TYPE_ID_NULL) + } *locals; + int local_count; + int local_capacity; + int scope_depth; + + // Upvalue tracking + UpvalueDesc *upvalues; + int upvalue_count; + + // Loop tracking (for break/continue) + struct { + int start; // Loop start offset + int scope_depth; // Scope depth at loop start + int *breaks; // Break jump offsets to patch + int break_count; + int break_capacity; + } *loops; + int loop_count; + int loop_capacity; + + // Enclosing function (for closures) + struct ChunkBuilder *enclosing; + +} ChunkBuilder; + +ChunkBuilder* chunk_builder_new(ChunkBuilder *enclosing); +void chunk_builder_free(ChunkBuilder *builder); +Chunk* chunk_builder_finish(ChunkBuilder *builder); + +// Scope management +void builder_begin_scope(ChunkBuilder *builder); +void builder_end_scope(ChunkBuilder *builder); + +// Local variable management +int builder_declare_local(ChunkBuilder *builder, const char *name, bool is_const, TypeId type); +int builder_resolve_local(ChunkBuilder *builder, const char *name); +int builder_resolve_upvalue(ChunkBuilder *builder, const char *name); +void builder_mark_initialized(ChunkBuilder *builder); + +// Loop management +void builder_begin_loop(ChunkBuilder *builder); +void builder_end_loop(ChunkBuilder *builder); +void builder_emit_break(ChunkBuilder *builder); +void builder_emit_continue(ChunkBuilder *builder); + +#endif // HEMLOCK_VM_CHUNK_H diff --git a/src/backends/vm/instruction.h b/src/backends/vm/instruction.h new file mode 100644 index 00000000..b3c714ba --- /dev/null +++ b/src/backends/vm/instruction.h @@ -0,0 +1,319 @@ +/* + * Hemlock Bytecode VM - Instruction Set + * + * 82 opcodes organized into 11 categories. + * Each opcode is 1 byte, with 0-3 bytes of operands. + */ + +#ifndef HEMLOCK_VM_INSTRUCTION_H +#define HEMLOCK_VM_INSTRUCTION_H + +#include + +typedef enum { + // ======================================== + // Category 1: Constants & Literals (0x00-0x0F) + // ======================================== + OP_CONST = 0x00, // [idx:16] Push constant from pool + OP_CONST_BYTE = 0x01, // [val:8] Push small integer (0-255) + OP_NULL = 0x02, // [] Push null + OP_TRUE = 0x03, // [] Push true + OP_FALSE = 0x04, // [] Push false + OP_ARRAY = 0x05, // [cnt:16] Pop n elements, push array + OP_OBJECT = 0x06, // [cnt:16] Pop n key-value pairs, push object + OP_STRING_INTERP = 0x07, // [cnt:16] Interpolate n parts into string + OP_CLOSURE = 0x08, // [idx:16][upvals:8] Create closure + OP_ENUM_VALUE = 0x09, // [idx:16] Push enum variant value + + // ======================================== + // Category 2: Variables (0x10-0x1F) + // ======================================== + OP_GET_LOCAL = 0x10, // [slot:8] Load local variable + OP_SET_LOCAL = 0x11, // [slot:8] Store to local + OP_GET_UPVALUE = 0x12, // [slot:8] Load captured variable + OP_SET_UPVALUE = 0x13, // [slot:8] Store to captured variable + OP_GET_GLOBAL = 0x14, // [idx:16] Load global by name + OP_SET_GLOBAL = 0x15, // [idx:16] Store to global + OP_DEFINE_GLOBAL = 0x16, // [idx:16] Define new global + OP_GET_PROPERTY = 0x17, // [idx:16] Get object property + OP_SET_PROPERTY = 0x18, // [idx:16] Set object property + OP_GET_INDEX = 0x19, // [] array[index] or object[key] + OP_SET_INDEX = 0x1A, // [] array[index] = value + OP_CLOSE_UPVALUE = 0x1B, // [] Close upvalue on stack top + + // ======================================== + // Category 3: Arithmetic (0x20-0x2F) + // ======================================== + OP_ADD = 0x20, // [] a + b (type promotion, string concat) + OP_SUB = 0x21, // [] a - b + OP_MUL = 0x22, // [] a * b + OP_DIV = 0x23, // [] a / b (always returns f64) + OP_MOD = 0x24, // [] a % b + OP_NEGATE = 0x25, // [] -a + OP_INC = 0x26, // [] ++a (in-place) + OP_DEC = 0x27, // [] --a (in-place) + OP_ADD_I32 = 0x28, // [] Fast path: i32 + i32 + OP_SUB_I32 = 0x29, // [] Fast path: i32 - i32 + OP_MUL_I32 = 0x2A, // [] Fast path: i32 * i32 + + // ======================================== + // Category 4: Comparison (0x30-0x3F) + // ======================================== + OP_EQ = 0x30, // [] a == b + OP_NE = 0x31, // [] a != b + OP_LT = 0x32, // [] a < b + OP_LE = 0x33, // [] a <= b + OP_GT = 0x34, // [] a > b + OP_GE = 0x35, // [] a >= b + OP_EQ_I32 = 0x36, // [] Fast path: i32 == i32 + OP_LT_I32 = 0x37, // [] Fast path: i32 < i32 + + // ======================================== + // Category 5: Logical & Bitwise (0x40-0x4F) + // ======================================== + OP_NOT = 0x40, // [] !a + OP_BIT_NOT = 0x41, // [] ~a + OP_BIT_AND = 0x42, // [] a & b + OP_BIT_OR = 0x43, // [] a | b + OP_BIT_XOR = 0x44, // [] a ^ b + OP_LSHIFT = 0x45, // [] a << b + OP_RSHIFT = 0x46, // [] a >> b + OP_COALESCE = 0x47, // [off:16] a ?? b (short-circuit) + OP_OPTIONAL_CHAIN = 0x48, // [off:16] a?.b (null short-circuit) + + // ======================================== + // Category 6: Control Flow (0x50-0x5F) + // ======================================== + OP_JUMP = 0x50, // [off:16] Unconditional jump + OP_JUMP_IF_FALSE = 0x51, // [off:16] Jump if top is falsy + OP_JUMP_IF_TRUE = 0x52, // [off:16] Jump if top is truthy + OP_JUMP_IF_FALSE_POP= 0x53, // [off:16] Jump if false, always pop + OP_LOOP = 0x54, // [off:16] Jump backward (loop) + OP_BREAK = 0x55, // [] Break from loop + OP_CONTINUE = 0x56, // [] Continue to next iteration + OP_SWITCH = 0x57, // [cnt:16] Jump table dispatch + OP_CASE = 0x58, // [off:16] Case label marker + OP_FOR_IN_INIT = 0x59, // [] Initialize for-in iterator + OP_FOR_IN_NEXT = 0x5A, // [off:16] Get next or jump to end + OP_POP = 0x5B, // [] Discard top of stack + OP_POPN = 0x5C, // [n:8] Discard n values from stack + + // ======================================== + // Category 7: Functions & Calls (0x60-0x6F) + // ======================================== + OP_CALL = 0x60, // [argc:8] Call function + OP_CALL_METHOD = 0x61, // [idx:16][argc:8] Call method on object + OP_CALL_BUILTIN = 0x62, // [id:16][argc:8] Call builtin function + OP_RETURN = 0x63, // [] Return from function + OP_APPLY = 0x64, // [] apply(fn, args_array) + OP_TAIL_CALL = 0x65, // [argc:8] Tail call optimization + OP_SUPER = 0x66, // [idx:16] Access super method + OP_INVOKE = 0x67, // [idx:16][argc:8] Optimized method call + + // ======================================== + // Category 8: Exception Handling (0x70-0x7F) + // ======================================== + OP_TRY = 0x70, // [catch:16][finally:16] Begin try block + OP_CATCH = 0x71, // [] Begin catch (push exception) + OP_FINALLY = 0x72, // [] Begin finally block + OP_END_TRY = 0x73, // [] End try-catch-finally + OP_THROW = 0x74, // [] Throw exception + OP_DEFER = 0x75, // [idx:16] Register deferred call + + // ======================================== + // Category 9: Async & Concurrency (0x80-0x8F) + // ======================================== + OP_SPAWN = 0x80, // [argc:8] Spawn async task + OP_AWAIT = 0x81, // [] Await task result + OP_JOIN = 0x82, // [] Join task (explicit) + OP_DETACH = 0x83, // [] Detach task + OP_CHANNEL = 0x84, // [] Create channel (capacity on stack) + OP_SEND = 0x85, // [] Send on channel + OP_RECV = 0x86, // [] Receive from channel + OP_SELECT = 0x87, // [] Select on multiple channels + + // ======================================== + // Category 10: Type Operations (0x90-0x9F) + // ======================================== + OP_TYPEOF = 0x90, // [] Get type string + OP_CAST = 0x91, // [type:8] Explicit type cast + OP_CHECK_TYPE = 0x92, // [type:8] Runtime type check + OP_DEFINE_TYPE = 0x93, // [idx:16] Register type definition + OP_DEFINE_ENUM = 0x94, // [idx:16] Register enum definition + + // ======================================== + // Category 11: Debug & Misc (0xF0-0xFF) + // ======================================== + OP_NOP = 0xF0, // [] No operation + OP_PRINT = 0xF1, // [argc:8] Print values + OP_ASSERT = 0xF2, // [] Assert with optional message + OP_DEBUG_BREAK = 0xFE, // [] Debugger breakpoint + OP_HALT = 0xFF, // [] Stop execution + +} OpCode; + +// Instruction metadata for disassembly and verification +typedef struct { + const char *name; // Human-readable name + int operand_bytes; // Number of operand bytes (0, 1, 2, 3, 4) + int stack_effect; // Net stack change (-128 = variable) +} InstructionInfo; + +// Get instruction info by opcode +const InstructionInfo* instruction_info(OpCode op); + +// Operand encoding helpers +#define READ_BYTE(ip) (*(ip)++) +#define READ_SHORT(ip) ((ip) += 2, (uint16_t)((ip)[-2] << 8 | (ip)[-1])) +#define READ_SIGNED_SHORT(ip) ((int16_t)READ_SHORT(ip)) + +// Instruction size helpers +#define INSTR_SIZE_0 1 // opcode only +#define INSTR_SIZE_1 2 // opcode + 1 byte operand +#define INSTR_SIZE_2 3 // opcode + 2 byte operand +#define INSTR_SIZE_3 4 // opcode + 3 byte operand +#define INSTR_SIZE_4 5 // opcode + 4 byte operand + +// Stack effect constants +#define STACK_EFFECT_VARIABLE -128 + +// Builtin function IDs (for OP_CALL_BUILTIN) +typedef enum { + // Memory (0-10) + BUILTIN_ALLOC = 0, + BUILTIN_TALLOC, + BUILTIN_REALLOC, + BUILTIN_FREE, + BUILTIN_MEMSET, + BUILTIN_MEMCPY, + BUILTIN_SIZEOF, + BUILTIN_BUFFER, + BUILTIN_PTR_TO_BUFFER, + BUILTIN_BUFFER_PTR, + BUILTIN_PTR_NULL, + + // I/O (11-14) + BUILTIN_PRINT, + BUILTIN_EPRINT, + BUILTIN_READ_LINE, + BUILTIN_OPEN, + + // Type (15-17) + BUILTIN_TYPEOF, + BUILTIN_ASSERT, + BUILTIN_PANIC, + + // Async (18-24) + BUILTIN_SPAWN, + BUILTIN_JOIN, + BUILTIN_DETACH, + BUILTIN_CHANNEL, + BUILTIN_SELECT, + BUILTIN_TASK_DEBUG_INFO, + BUILTIN_APPLY, + + // Signal (25-26) + BUILTIN_SIGNAL, + BUILTIN_RAISE, + + // Exec (27-28) + BUILTIN_EXEC, + BUILTIN_EXEC_ARGV, + + // Pointer read (29-41) + BUILTIN_PTR_READ_I8, + BUILTIN_PTR_READ_I16, + BUILTIN_PTR_READ_I32, + BUILTIN_PTR_READ_I64, + BUILTIN_PTR_READ_U8, + BUILTIN_PTR_READ_U16, + BUILTIN_PTR_READ_U32, + BUILTIN_PTR_READ_U64, + BUILTIN_PTR_READ_F32, + BUILTIN_PTR_READ_F64, + BUILTIN_PTR_READ_PTR, + BUILTIN_PTR_OFFSET, + BUILTIN_PTR_DEREF_I32, + + // Pointer write (42-52) + BUILTIN_PTR_WRITE_I8, + BUILTIN_PTR_WRITE_I16, + BUILTIN_PTR_WRITE_I32, + BUILTIN_PTR_WRITE_I64, + BUILTIN_PTR_WRITE_U8, + BUILTIN_PTR_WRITE_U16, + BUILTIN_PTR_WRITE_U32, + BUILTIN_PTR_WRITE_U64, + BUILTIN_PTR_WRITE_F32, + BUILTIN_PTR_WRITE_F64, + BUILTIN_PTR_WRITE_PTR, + + // Atomics i32 (53-61) + BUILTIN_ATOMIC_LOAD_I32, + BUILTIN_ATOMIC_STORE_I32, + BUILTIN_ATOMIC_ADD_I32, + BUILTIN_ATOMIC_SUB_I32, + BUILTIN_ATOMIC_AND_I32, + BUILTIN_ATOMIC_OR_I32, + BUILTIN_ATOMIC_XOR_I32, + BUILTIN_ATOMIC_CAS_I32, + BUILTIN_ATOMIC_EXCHANGE_I32, + + // Atomics i64 (62-70) + BUILTIN_ATOMIC_LOAD_I64, + BUILTIN_ATOMIC_STORE_I64, + BUILTIN_ATOMIC_ADD_I64, + BUILTIN_ATOMIC_SUB_I64, + BUILTIN_ATOMIC_AND_I64, + BUILTIN_ATOMIC_OR_I64, + BUILTIN_ATOMIC_XOR_I64, + BUILTIN_ATOMIC_CAS_I64, + BUILTIN_ATOMIC_EXCHANGE_I64, + + // Atomics misc (71) + BUILTIN_ATOMIC_FENCE, + + // Callback (72-73) + BUILTIN_CALLBACK, + BUILTIN_CALLBACK_FREE, + + // Stack (74-75) + BUILTIN_SET_STACK_LIMIT, + BUILTIN_GET_STACK_LIMIT, + + // DNS/Network (76-78) + BUILTIN_DNS_RESOLVE, + BUILTIN_SOCKET_CREATE, + BUILTIN_POLL, + + BUILTIN_COUNT // Total number of builtins +} BuiltinId; + +// Type IDs for OP_CAST and OP_CHECK_TYPE +typedef enum { + TYPE_ID_I8 = 0, + TYPE_ID_I16, + TYPE_ID_I32, + TYPE_ID_I64, + TYPE_ID_U8, + TYPE_ID_U16, + TYPE_ID_U32, + TYPE_ID_U64, + TYPE_ID_F32, + TYPE_ID_F64, + TYPE_ID_BOOL, + TYPE_ID_STRING, + TYPE_ID_RUNE, + TYPE_ID_ARRAY, + TYPE_ID_OBJECT, + TYPE_ID_PTR, + TYPE_ID_BUFFER, + TYPE_ID_NULL, + TYPE_ID_FUNCTION, + TYPE_ID_TASK, + TYPE_ID_CHANNEL, + TYPE_ID_FILE, + TYPE_ID_ENUM, +} TypeId; + +#endif // HEMLOCK_VM_INSTRUCTION_H diff --git a/src/backends/vm/vm.h b/src/backends/vm/vm.h new file mode 100644 index 00000000..26490222 --- /dev/null +++ b/src/backends/vm/vm.h @@ -0,0 +1,299 @@ +/* + * Hemlock Bytecode VM - Virtual Machine + * + * Stack-based virtual machine for executing Hemlock bytecode. + * Supports closures, async/await, channels, and full parity with interpreter. + */ + +#ifndef HEMLOCK_VM_VM_H +#define HEMLOCK_VM_VM_H + +#include +#include +#include "chunk.h" +#include "instruction.h" + +// Use the shared Value type from interpreter +#include "../../../include/interpreter.h" + +// ============================================ +// Forward Declarations +// ============================================ + +typedef struct VM VM; +typedef struct CallFrame CallFrame; +typedef struct ObjUpvalue ObjUpvalue; +typedef struct DeferEntry DeferEntry; + +// ============================================ +// Call Frame +// ============================================ + +struct CallFrame { + Chunk *chunk; // Bytecode being executed + uint8_t *ip; // Instruction pointer + Value *slots; // First slot in VM stack for this frame + ObjUpvalue *upvalues; // Captured variables (for closures) + int slot_count; // Number of local slots +}; + +// ============================================ +// Open Upvalue (for closures) +// ============================================ + +struct ObjUpvalue { + Value *location; // Points to stack slot while open + Value closed; // Holds value when closed + struct ObjUpvalue *next;// Linked list of open upvalues +}; + +// ============================================ +// Defer Entry +// ============================================ + +struct DeferEntry { + Chunk *chunk; // Deferred function chunk + Value *args; // Captured arguments + int arg_count; + CallFrame *frame; // Frame that registered the defer +}; + +// ============================================ +// VM State +// ============================================ + +typedef enum { + VM_OK, + VM_RUNTIME_ERROR, + VM_COMPILE_ERROR, +} VMResult; + +struct VM { + // Value stack + Value *stack; + Value *stack_top; + int stack_capacity; + + // Call frames + CallFrame *frames; + int frame_count; + int frame_capacity; + + // Open upvalues (for closures) + ObjUpvalue *open_upvalues; + + // Global variables + struct { + char **names; + Value *values; + bool *is_const; + int count; + int capacity; + int *hash_table; + int hash_capacity; + } globals; + + // Control flow state + bool is_returning; + Value return_value; + + bool is_throwing; + Value exception; + CallFrame *exception_frame; + + bool is_breaking; + bool is_continuing; + + // Defer stack + DeferEntry *defers; + int defer_count; + int defer_capacity; + + // Module cache + struct { + char **paths; + Value *modules; + int count; + int capacity; + } module_cache; + + // Object tracking (for cleanup) + void *objects; // Linked list of allocated objects + size_t bytes_allocated; + size_t next_gc; + + // Stack limit + int max_stack_depth; + + // Async task context (when running in spawned task) + void *task; // Task* when running async +}; + +// ============================================ +// VM Lifecycle +// ============================================ + +VM* vm_new(void); +void vm_free(VM *vm); +void vm_reset(VM *vm); + +// ============================================ +// Execution +// ============================================ + +// Run a compiled chunk (main entry point) +VMResult vm_run(VM *vm, Chunk *chunk); + +// Run a source file (compile + execute) +VMResult vm_run_file(VM *vm, const char *path); + +// Run a source string (compile + execute) +VMResult vm_run_source(VM *vm, const char *source, const char *path); + +// Call a function value with arguments +Value vm_call(VM *vm, Value callable, int argc, Value *args); + +// ============================================ +// Stack Operations +// ============================================ + +void vm_push(VM *vm, Value value); +Value vm_pop(VM *vm); +Value vm_peek(VM *vm, int distance); +void vm_popn(VM *vm, int count); + +// ============================================ +// Global Variables +// ============================================ + +void vm_define_global(VM *vm, const char *name, Value value, bool is_const); +bool vm_get_global(VM *vm, const char *name, Value *out); +bool vm_set_global(VM *vm, const char *name, Value value); + +// ============================================ +// Builtins +// ============================================ + +typedef Value (*BuiltinFn)(VM *vm, int argc, Value *args); + +typedef struct { + const char *name; + BuiltinFn fn; + int min_arity; + int max_arity; // -1 for variadic +} BuiltinEntry; + +// Get builtin by ID +const BuiltinEntry* vm_get_builtin(BuiltinId id); + +// Register all builtins +void vm_register_builtins(VM *vm); + +// ============================================ +// Error Handling +// ============================================ + +// Report runtime error (sets is_throwing) +void vm_runtime_error(VM *vm, const char *format, ...); + +// Get current line number +int vm_current_line(VM *vm); + +// Print stack trace +void vm_print_stack_trace(VM *vm); + +// ============================================ +// Closures & Upvalues +// ============================================ + +ObjUpvalue* vm_capture_upvalue(VM *vm, Value *local); +void vm_close_upvalues(VM *vm, Value *last); + +// ============================================ +// Defer +// ============================================ + +void vm_push_defer(VM *vm, Chunk *chunk, Value *args, int arg_count); +void vm_execute_defers(VM *vm, CallFrame *until_frame); + +// ============================================ +// Modules +// ============================================ + +Value vm_import_module(VM *vm, const char *path); +Value vm_import_stdlib(VM *vm, const char *module_name); + +// ============================================ +// Async Support +// ============================================ + +// Create task for async function +Value vm_spawn_task(VM *vm, Value func, int argc, Value *args); + +// Wait for task completion +Value vm_join_task(VM *vm, Value task); + +// Create channel +Value vm_create_channel(VM *vm, int capacity); + +// ============================================ +// Debug +// ============================================ + +void vm_trace_execution(VM *vm, bool enable); +void vm_dump_stack(VM *vm); +void vm_dump_globals(VM *vm); + +// ============================================ +// Configuration +// ============================================ + +#define VM_STACK_INITIAL 256 +#define VM_STACK_MAX 65536 +#define VM_FRAMES_INITIAL 64 +#define VM_FRAMES_MAX 1024 +#define VM_GLOBALS_INITIAL 64 +#define VM_DEFER_INITIAL 16 + +// ============================================ +// Dispatch Loop Macros +// ============================================ + +// Computed goto dispatch (if supported) +#ifdef __GNUC__ +#define VM_USE_COMPUTED_GOTO 1 +#else +#define VM_USE_COMPUTED_GOTO 0 +#endif + +#if VM_USE_COMPUTED_GOTO + #define DISPATCH() goto *dispatch_table[READ_BYTE(ip)] + #define CASE(op) case_##op: + #define NEXT() DISPATCH() +#else + #define DISPATCH() switch (READ_BYTE(ip)) + #define CASE(op) case op: + #define NEXT() break +#endif + +// Stack manipulation macros +#define PUSH(v) (*vm->stack_top++ = (v)) +#define POP() (*--vm->stack_top) +#define PEEK(n) (vm->stack_top[-1 - (n)]) +#define DROP(n) (vm->stack_top -= (n)) + +// Binary operation macro with type promotion +#define BINARY_OP(op, result_type) do { \ + Value b = POP(); \ + Value a = POP(); \ + PUSH(value_binary_##op(a, b)); \ +} while (0) + +// Fast path i32 binary operation +#define BINARY_OP_I32(op) do { \ + Value b = POP(); \ + Value a = POP(); \ + PUSH(VALUE_I32(a.as.as_i32 op b.as.as_i32)); \ +} while (0) + +#endif // HEMLOCK_VM_VM_H From 082c11b26feef674f9c68ed3ab3866a09a01a231 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Dec 2025 23:09:30 +0000 Subject: [PATCH 02/42] Implement bytecode VM core execution engine Add stack-based bytecode virtual machine with: - 82 opcodes supporting all Hemlock language features - Constant pool with string interning - Stack operations and call frames - Bytecode compiler (AST to bytecode) - Disassembler for debugging - BC_PRINT pushes null return value for expression statement POP - BC_JUMP_IF_FALSE peeks instead of popping condition Passes primitives parity test with interpreter output matching. --- src/backends/vm/Makefile | 92 +-- src/backends/vm/chunk.c | 573 +++++++++++++++++ src/backends/vm/chunk.h | 2 +- src/backends/vm/compiler.c | 761 +++++++++++++++++++++++ src/backends/vm/compiler.h | 41 ++ src/backends/vm/debug.c | 388 ++++++++++++ src/backends/vm/debug.h | 25 + src/backends/vm/instruction.c | 303 +++++++++ src/backends/vm/instruction.h | 354 ++++++----- src/backends/vm/main.c | 231 +++++++ src/backends/vm/vm.c | 1090 +++++++++++++++++++++++++++++++++ src/backends/vm/vm.h | 7 +- 12 files changed, 3656 insertions(+), 211 deletions(-) create mode 100644 src/backends/vm/chunk.c create mode 100644 src/backends/vm/compiler.c create mode 100644 src/backends/vm/compiler.h create mode 100644 src/backends/vm/debug.c create mode 100644 src/backends/vm/debug.h create mode 100644 src/backends/vm/instruction.c create mode 100644 src/backends/vm/main.c create mode 100644 src/backends/vm/vm.c diff --git a/src/backends/vm/Makefile b/src/backends/vm/Makefile index 57437af6..ce7dd6ce 100644 --- a/src/backends/vm/Makefile +++ b/src/backends/vm/Makefile @@ -1,35 +1,31 @@ # Hemlock Bytecode VM Makefile CC = gcc -CFLAGS = -Wall -Wextra -g -O2 -std=c11 -CFLAGS += -I../../../include -I. - -# Source files -SRCS = main.c \ - vm.c \ - chunk.c \ - compiler.c \ - compile_expr.c \ - compile_stmt.c \ - compile_call.c \ - debug.c \ - builtins.c - -OBJS = $(SRCS:.c=.o) +CFLAGS = -Wall -Wextra -g -O2 -std=c11 -D_POSIX_C_SOURCE=200809L +CFLAGS += -I../../../include -I. -I../../frontend -I.. + +# Source files (VM-specific) +VM_SRCS = main.c \ + vm.c \ + chunk.c \ + compiler.c \ + instruction.c \ + debug.c + +VM_OBJS = $(VM_SRCS:.c=.o) # Shared frontend objects -FRONTEND_OBJS = ../../frontend/lexer.o \ - ../../frontend/parser/parser.o \ - ../../frontend/parser/expressions.o \ - ../../frontend/parser/statements.o \ - ../../frontend/parser/patterns.o \ - ../../frontend/ast.o \ - ../../frontend/module.o - -# Interpreter objects we can reuse (values, environment) -SHARED_OBJS = ../interpreter/values.o \ - ../interpreter/environment.o \ - ../interpreter/resolver.o +FRONTEND_DIR = ../../frontend +FRONTEND_SRCS = $(FRONTEND_DIR)/lexer.c \ + $(FRONTEND_DIR)/parser/core.c \ + $(FRONTEND_DIR)/parser/expressions.c \ + $(FRONTEND_DIR)/parser/statements.c \ + $(FRONTEND_DIR)/ast.c + +# Interpreter objects we can reuse +INTERP_DIR = ../interpreter +SHARED_SRCS = $(INTERP_DIR)/resolver.c \ + $(INTERP_DIR)/optimizer.c # Output binary TARGET = hemlockvm @@ -38,36 +34,56 @@ TARGET = hemlockvm all: $(TARGET) -$(TARGET): $(OBJS) $(FRONTEND_OBJS) $(SHARED_OBJS) - $(CC) $(CFLAGS) -o $@ $^ -lpthread -lm -ldl -lffi +# Build by compiling all sources directly +$(TARGET): $(VM_SRCS) $(FRONTEND_SRCS) $(SHARED_SRCS) + $(CC) $(CFLAGS) -o $@ $^ -lpthread -lm -ldl +# Individual object compilation %.o: %.c $(CC) $(CFLAGS) -c -o $@ $< # Run VM-specific tests test: $(TARGET) @echo "Running bytecode VM tests..." - @./scripts/test_vm.sh + @./$(TARGET) ../../../tests/parity/language/primitives.hml + +# Quick test +quick: $(TARGET) + @echo "Testing: print(42);" + @echo 'print(42);' | ./$(TARGET) # Run parity tests (compare output with interpreter) parity: $(TARGET) @echo "Running parity tests..." - @./scripts/test_vm_parity.sh + @for f in ../../../tests/parity/language/*.hml; do \ + echo -n "Testing $$f... "; \ + expected=$${f%.hml}.expected; \ + if [ -f "$$expected" ]; then \ + output=$$(./$(TARGET) "$$f" 2>&1); \ + expected_content=$$(cat "$$expected"); \ + if [ "$$output" = "$$expected_content" ]; then \ + echo "PASS"; \ + else \ + echo "FAIL"; \ + echo "Expected:"; cat "$$expected"; \ + echo "Got:"; echo "$$output"; \ + fi \ + else \ + echo "SKIP (no expected file)"; \ + fi \ + done # Disassemble a file (for debugging) disasm: $(TARGET) ./$(TARGET) --disasm $(FILE) clean: - rm -f $(OBJS) $(TARGET) + rm -f $(VM_OBJS) $(TARGET) # Dependencies -main.o: main.c vm.h chunk.h instruction.h -vm.o: vm.c vm.h chunk.h instruction.h +main.o: main.c vm.h chunk.h instruction.h compiler.h +vm.o: vm.c vm.h chunk.h instruction.h debug.h chunk.o: chunk.c chunk.h instruction.h compiler.o: compiler.c compiler.h chunk.h instruction.h -compile_expr.o: compile_expr.c compiler.h chunk.h instruction.h -compile_stmt.o: compile_stmt.c compiler.h chunk.h instruction.h -compile_call.o: compile_call.c compiler.h chunk.h instruction.h +instruction.o: instruction.c instruction.h debug.o: debug.c debug.h chunk.h instruction.h -builtins.o: builtins.c vm.h instruction.h diff --git a/src/backends/vm/chunk.c b/src/backends/vm/chunk.c new file mode 100644 index 00000000..73ffc34b --- /dev/null +++ b/src/backends/vm/chunk.c @@ -0,0 +1,573 @@ +/* + * Hemlock Bytecode VM - Chunk Implementation + * + * Bytecode container with constant pool and debug info. + */ + +#define _POSIX_C_SOURCE 200809L + +#include "chunk.h" +#include +#include +#include + +// Initial capacities +#define CHUNK_CODE_INITIAL 256 +#define CHUNK_CONST_INITIAL 64 +#define CHUNK_LINES_INITIAL 64 +#define BUILDER_LOCALS_INITIAL 32 +#define BUILDER_LOOPS_INITIAL 8 + +// ============================================ +// Chunk Lifecycle +// ============================================ + +Chunk* chunk_new(void) { + Chunk *chunk = malloc(sizeof(Chunk)); + if (!chunk) return NULL; + + chunk->code = malloc(CHUNK_CODE_INITIAL); + chunk->code_count = 0; + chunk->code_capacity = CHUNK_CODE_INITIAL; + + chunk->constants = malloc(sizeof(Constant) * CHUNK_CONST_INITIAL); + chunk->const_count = 0; + chunk->const_capacity = CHUNK_CONST_INITIAL; + + chunk->lines = malloc(sizeof(int) * CHUNK_LINES_INITIAL); + chunk->lines_count = 0; + chunk->lines_capacity = CHUNK_LINES_INITIAL; + + chunk->name = NULL; + chunk->arity = 0; + chunk->optional_count = 0; + chunk->has_rest_param = false; + chunk->is_async = false; + + chunk->upvalues = NULL; + chunk->upvalue_count = 0; + + chunk->param_types = NULL; + chunk->return_type = TYPE_ID_NULL; + + chunk->local_count = 0; + chunk->max_stack = 0; + + return chunk; +} + +void chunk_free(Chunk *chunk) { + if (!chunk) return; + + free(chunk->code); + + // Free constant pool + for (int i = 0; i < chunk->const_count; i++) { + Constant *c = &chunk->constants[i]; + if (c->type == CONST_STRING || c->type == CONST_IDENTIFIER) { + free(c->as.string.data); + } else if (c->type == CONST_FUNCTION) { + chunk_free(c->as.function); + } + } + free(chunk->constants); + + free(chunk->lines); + free(chunk->name); + free(chunk->upvalues); + free(chunk->param_types); + free(chunk); +} + +// ============================================ +// Bytecode Writing +// ============================================ + +static void ensure_code_capacity(Chunk *chunk, int needed) { + if (chunk->code_count + needed <= chunk->code_capacity) return; + + int new_capacity = chunk->code_capacity * 2; + while (new_capacity < chunk->code_count + needed) { + new_capacity *= 2; + } + chunk->code = realloc(chunk->code, new_capacity); + chunk->code_capacity = new_capacity; +} + +static void ensure_lines_capacity(Chunk *chunk) { + if (chunk->lines_count + 2 <= chunk->lines_capacity) return; + + chunk->lines_capacity *= 2; + chunk->lines = realloc(chunk->lines, sizeof(int) * chunk->lines_capacity); +} + +void chunk_write_byte(Chunk *chunk, uint8_t byte, int line) { + ensure_code_capacity(chunk, 1); + chunk->code[chunk->code_count++] = byte; + + // Run-length encode line numbers + // Format: [count, line, count, line, ...] + if (chunk->lines_count >= 2 && + chunk->lines[chunk->lines_count - 1] == line) { + // Same line as previous, increment count + chunk->lines[chunk->lines_count - 2]++; + } else { + // New line + ensure_lines_capacity(chunk); + chunk->lines[chunk->lines_count++] = 1; // count + chunk->lines[chunk->lines_count++] = line; // line number + } +} + +void chunk_write_short(Chunk *chunk, uint16_t value, int line) { + chunk_write_byte(chunk, (value >> 8) & 0xFF, line); + chunk_write_byte(chunk, value & 0xFF, line); +} + +int chunk_write_jump(Chunk *chunk, OpCode op, int line) { + chunk_write_byte(chunk, op, line); + chunk_write_byte(chunk, 0xFF, line); // Placeholder + chunk_write_byte(chunk, 0xFF, line); // Placeholder + return chunk->code_count - 2; // Return offset to patch +} + +void chunk_patch_jump(Chunk *chunk, int offset) { + // Calculate jump distance from after the jump instruction + int jump = chunk->code_count - offset - 2; + + if (jump > 0xFFFF) { + fprintf(stderr, "Error: Jump too large (%d bytes)\n", jump); + return; + } + + chunk->code[offset] = (jump >> 8) & 0xFF; + chunk->code[offset + 1] = jump & 0xFF; +} + +// Patch a loop (backward jump) +void chunk_patch_loop(Chunk *chunk, int loop_start) { + int offset = chunk->code_count - loop_start + 2; + + if (offset > 0xFFFF) { + fprintf(stderr, "Error: Loop too large (%d bytes)\n", offset); + return; + } + + chunk_write_byte(chunk, BC_LOOP, 0); + chunk_write_byte(chunk, (offset >> 8) & 0xFF, 0); + chunk_write_byte(chunk, offset & 0xFF, 0); +} + +// ============================================ +// Constant Pool +// ============================================ + +static int ensure_const_capacity(Chunk *chunk) { + if (chunk->const_count < chunk->const_capacity) { + return 1; + } + + chunk->const_capacity *= 2; + chunk->constants = realloc(chunk->constants, + sizeof(Constant) * chunk->const_capacity); + return chunk->constants != NULL; +} + +int chunk_add_constant(Chunk *chunk, Constant constant) { + ensure_const_capacity(chunk); + chunk->constants[chunk->const_count] = constant; + return chunk->const_count++; +} + +int chunk_add_i32(Chunk *chunk, int32_t value) { + // Check for existing constant + for (int i = 0; i < chunk->const_count; i++) { + Constant *c = &chunk->constants[i]; + if (c->type == CONST_I32 && c->as.i32 == value) { + return i; + } + } + + Constant c = {.type = CONST_I32, .as.i32 = value}; + return chunk_add_constant(chunk, c); +} + +int chunk_add_i64(Chunk *chunk, int64_t value) { + // Check for existing constant + for (int i = 0; i < chunk->const_count; i++) { + Constant *c = &chunk->constants[i]; + if (c->type == CONST_I64 && c->as.i64 == value) { + return i; + } + } + + Constant c = {.type = CONST_I64, .as.i64 = value}; + return chunk_add_constant(chunk, c); +} + +int chunk_add_f64(Chunk *chunk, double value) { + // Check for existing constant (careful with NaN comparison) + for (int i = 0; i < chunk->const_count; i++) { + Constant *c = &chunk->constants[i]; + if (c->type == CONST_F64 && c->as.f64 == value) { + return i; + } + } + + Constant c = {.type = CONST_F64, .as.f64 = value}; + return chunk_add_constant(chunk, c); +} + +// Simple hash function for string interning +static uint32_t hash_string(const char *str, int length) { + uint32_t hash = 2166136261u; + for (int i = 0; i < length; i++) { + hash ^= (uint8_t)str[i]; + hash *= 16777619; + } + return hash; +} + +int chunk_add_string(Chunk *chunk, const char *str, int length) { + uint32_t hash = hash_string(str, length); + + // Check for existing constant (string interning) + for (int i = 0; i < chunk->const_count; i++) { + Constant *c = &chunk->constants[i]; + if (c->type == CONST_STRING && + c->as.string.hash == hash && + c->as.string.length == length && + memcmp(c->as.string.data, str, length) == 0) { + return i; + } + } + + // Add new string + char *copy = malloc(length + 1); + memcpy(copy, str, length); + copy[length] = '\0'; + + Constant c = { + .type = CONST_STRING, + .as.string = {.data = copy, .length = length, .hash = hash} + }; + return chunk_add_constant(chunk, c); +} + +int chunk_add_function(Chunk *chunk, Chunk *function) { + Constant c = {.type = CONST_FUNCTION, .as.function = function}; + return chunk_add_constant(chunk, c); +} + +int chunk_add_identifier(Chunk *chunk, const char *name) { + int length = strlen(name); + uint32_t hash = hash_string(name, length); + + // Check for existing identifier + for (int i = 0; i < chunk->const_count; i++) { + Constant *c = &chunk->constants[i]; + if (c->type == CONST_IDENTIFIER && + c->as.string.hash == hash && + c->as.string.length == length && + memcmp(c->as.string.data, name, length) == 0) { + return i; + } + } + + // Add new identifier + char *copy = malloc(length + 1); + memcpy(copy, name, length); + copy[length] = '\0'; + + Constant c = { + .type = CONST_IDENTIFIER, + .as.string = {.data = copy, .length = length, .hash = hash} + }; + return chunk_add_constant(chunk, c); +} + +// ============================================ +// Query Functions +// ============================================ + +int chunk_get_line(Chunk *chunk, int offset) { + // Decode run-length encoded lines + int current_offset = 0; + for (int i = 0; i < chunk->lines_count; i += 2) { + int count = chunk->lines[i]; + int line = chunk->lines[i + 1]; + current_offset += count; + if (current_offset > offset) { + return line; + } + } + return 0; // Unknown line +} + +Constant* chunk_get_constant(Chunk *chunk, int index) { + if (index < 0 || index >= chunk->const_count) { + return NULL; + } + return &chunk->constants[index]; +} + +// ============================================ +// Chunk Builder +// ============================================ + +ChunkBuilder* chunk_builder_new(ChunkBuilder *enclosing) { + ChunkBuilder *builder = malloc(sizeof(ChunkBuilder)); + if (!builder) return NULL; + + builder->chunk = chunk_new(); + if (!builder->chunk) { + free(builder); + return NULL; + } + + builder->locals = malloc(sizeof(*builder->locals) * BUILDER_LOCALS_INITIAL); + builder->local_count = 0; + builder->local_capacity = BUILDER_LOCALS_INITIAL; + builder->scope_depth = 0; + + builder->upvalues = NULL; + builder->upvalue_count = 0; + + builder->loops = malloc(sizeof(*builder->loops) * BUILDER_LOOPS_INITIAL); + builder->loop_count = 0; + builder->loop_capacity = BUILDER_LOOPS_INITIAL; + + builder->enclosing = enclosing; + + return builder; +} + +void chunk_builder_free(ChunkBuilder *builder) { + if (!builder) return; + + // Free local names + for (int i = 0; i < builder->local_count; i++) { + free(builder->locals[i].name); + } + free(builder->locals); + + // Free loop break lists + for (int i = 0; i < builder->loop_count; i++) { + free(builder->loops[i].breaks); + } + free(builder->loops); + + free(builder->upvalues); + free(builder); +} + +Chunk* chunk_builder_finish(ChunkBuilder *builder) { + Chunk *chunk = builder->chunk; + + // Transfer upvalue info + if (builder->upvalue_count > 0) { + chunk->upvalues = malloc(sizeof(UpvalueDesc) * builder->upvalue_count); + memcpy(chunk->upvalues, builder->upvalues, + sizeof(UpvalueDesc) * builder->upvalue_count); + chunk->upvalue_count = builder->upvalue_count; + } + + chunk->local_count = builder->local_count; + + // Don't free the chunk, we're returning it + builder->chunk = NULL; + chunk_builder_free(builder); + + return chunk; +} + +// ============================================ +// Scope Management +// ============================================ + +void builder_begin_scope(ChunkBuilder *builder) { + builder->scope_depth++; +} + +void builder_end_scope(ChunkBuilder *builder) { + builder->scope_depth--; + + // Pop locals in this scope + while (builder->local_count > 0 && + builder->locals[builder->local_count - 1].depth > builder->scope_depth) { + // Close upvalues if captured + if (builder->locals[builder->local_count - 1].is_captured) { + chunk_write_byte(builder->chunk, BC_CLOSE_UPVALUE, 0); + } else { + chunk_write_byte(builder->chunk, BC_POP, 0); + } + builder->local_count--; + } +} + +// ============================================ +// Local Variable Management +// ============================================ + +static void ensure_locals_capacity(ChunkBuilder *builder) { + if (builder->local_count < builder->local_capacity) return; + + builder->local_capacity *= 2; + builder->locals = realloc(builder->locals, + sizeof(*builder->locals) * builder->local_capacity); +} + +int builder_declare_local(ChunkBuilder *builder, const char *name, bool is_const, TypeId type) { + // Check for duplicate in current scope + for (int i = builder->local_count - 1; i >= 0; i--) { + if (builder->locals[i].depth < builder->scope_depth) break; + if (strcmp(builder->locals[i].name, name) == 0) { + return -1; // Duplicate + } + } + + ensure_locals_capacity(builder); + + int slot = builder->local_count; + builder->locals[slot].name = strdup(name); + builder->locals[slot].depth = builder->scope_depth; + builder->locals[slot].is_captured = false; + builder->locals[slot].is_const = is_const; + builder->locals[slot].type = type; + builder->local_count++; + + return slot; +} + +int builder_resolve_local(ChunkBuilder *builder, const char *name) { + for (int i = builder->local_count - 1; i >= 0; i--) { + if (strcmp(builder->locals[i].name, name) == 0) { + return i; + } + } + return -1; // Not found +} + +static int add_upvalue(ChunkBuilder *builder, uint8_t index, bool is_local) { + // Check for existing upvalue + for (int i = 0; i < builder->upvalue_count; i++) { + if (builder->upvalues[i].index == index && + builder->upvalues[i].is_local == is_local) { + return i; + } + } + + // Add new upvalue + if (builder->upvalues == NULL) { + builder->upvalues = malloc(sizeof(UpvalueDesc) * 8); + } else if (builder->upvalue_count >= 8) { + // Grow capacity + builder->upvalues = realloc(builder->upvalues, + sizeof(UpvalueDesc) * builder->upvalue_count * 2); + } + + builder->upvalues[builder->upvalue_count].index = index; + builder->upvalues[builder->upvalue_count].is_local = is_local; + return builder->upvalue_count++; +} + +int builder_resolve_upvalue(ChunkBuilder *builder, const char *name) { + if (builder->enclosing == NULL) return -1; + + // Look in enclosing function's locals + int local = builder_resolve_local(builder->enclosing, name); + if (local != -1) { + builder->enclosing->locals[local].is_captured = true; + return add_upvalue(builder, (uint8_t)local, true); + } + + // Look in enclosing function's upvalues + int upvalue = builder_resolve_upvalue(builder->enclosing, name); + if (upvalue != -1) { + return add_upvalue(builder, (uint8_t)upvalue, false); + } + + return -1; +} + +void builder_mark_initialized(ChunkBuilder *builder) { + // The most recent local is now initialized + // (For now, we don't track uninitialized state) +} + +// ============================================ +// Loop Management +// ============================================ + +void builder_begin_loop(ChunkBuilder *builder) { + if (builder->loop_count >= builder->loop_capacity) { + builder->loop_capacity *= 2; + builder->loops = realloc(builder->loops, + sizeof(*builder->loops) * builder->loop_capacity); + } + + int i = builder->loop_count++; + builder->loops[i].start = builder->chunk->code_count; + builder->loops[i].scope_depth = builder->scope_depth; + builder->loops[i].breaks = malloc(sizeof(int) * 8); + builder->loops[i].break_count = 0; + builder->loops[i].break_capacity = 8; +} + +void builder_end_loop(ChunkBuilder *builder) { + if (builder->loop_count == 0) return; + + int i = builder->loop_count - 1; + + // Patch all break jumps + for (int j = 0; j < builder->loops[i].break_count; j++) { + chunk_patch_jump(builder->chunk, builder->loops[i].breaks[j]); + } + + free(builder->loops[i].breaks); + builder->loop_count--; +} + +void builder_emit_break(ChunkBuilder *builder) { + if (builder->loop_count == 0) return; + + int i = builder->loop_count - 1; + + // Pop locals to loop scope + int depth = builder->loops[i].scope_depth; + for (int j = builder->local_count - 1; j >= 0 && builder->locals[j].depth > depth; j--) { + if (builder->locals[j].is_captured) { + chunk_write_byte(builder->chunk, BC_CLOSE_UPVALUE, 0); + } else { + chunk_write_byte(builder->chunk, BC_POP, 0); + } + } + + // Emit break as jump (to be patched) + if (builder->loops[i].break_count >= builder->loops[i].break_capacity) { + builder->loops[i].break_capacity *= 2; + builder->loops[i].breaks = realloc(builder->loops[i].breaks, + sizeof(int) * builder->loops[i].break_capacity); + } + int offset = chunk_write_jump(builder->chunk, BC_JUMP, 0); + builder->loops[i].breaks[builder->loops[i].break_count++] = offset; +} + +void builder_emit_continue(ChunkBuilder *builder) { + if (builder->loop_count == 0) return; + + int i = builder->loop_count - 1; + + // Pop locals to loop scope + int depth = builder->loops[i].scope_depth; + for (int j = builder->local_count - 1; j >= 0 && builder->locals[j].depth > depth; j--) { + if (builder->locals[j].is_captured) { + chunk_write_byte(builder->chunk, BC_CLOSE_UPVALUE, 0); + } else { + chunk_write_byte(builder->chunk, BC_POP, 0); + } + } + + // Jump back to loop start + chunk_patch_loop(builder->chunk, builder->loops[i].start); +} diff --git a/src/backends/vm/chunk.h b/src/backends/vm/chunk.h index 805ce281..b9b003c4 100644 --- a/src/backends/vm/chunk.h +++ b/src/backends/vm/chunk.h @@ -142,7 +142,7 @@ Chunk* chunk_deserialize(const uint8_t *data, size_t size); // Chunk Builder (for compiler) // ============================================ -typedef struct { +typedef struct ChunkBuilder { Chunk *chunk; // Current scope tracking diff --git a/src/backends/vm/compiler.c b/src/backends/vm/compiler.c new file mode 100644 index 00000000..886e05c7 --- /dev/null +++ b/src/backends/vm/compiler.c @@ -0,0 +1,761 @@ +/* + * Hemlock Bytecode VM - Compiler Implementation + * + * AST to bytecode compilation. + */ + +#include "compiler.h" +#include "instruction.h" +#include +#include +#include + +// ============================================ +// Compiler Lifecycle +// ============================================ + +static Compiler* compiler_new(Compiler *enclosing) { + Compiler *compiler = malloc(sizeof(Compiler)); + compiler->builder = chunk_builder_new(enclosing ? enclosing->builder : NULL); + compiler->enclosing = enclosing; + compiler->function_name = NULL; + compiler->is_async = false; + compiler->current_line = 1; + compiler->had_error = false; + compiler->panic_mode = false; + return compiler; +} + +static void compiler_free(Compiler *compiler) { + if (compiler->builder) { + chunk_builder_free(compiler->builder); + } + free(compiler->function_name); + free(compiler); +} + +// ============================================ +// Error Handling +// ============================================ + +void compiler_error(Compiler *compiler, const char *message) { + compiler_error_at(compiler, compiler->current_line, message); +} + +void compiler_error_at(Compiler *compiler, int line, const char *message) { + if (compiler->panic_mode) return; + compiler->panic_mode = true; + compiler->had_error = true; + fprintf(stderr, "[line %d] Error: %s\n", line, message); +} + +// ============================================ +// Bytecode Emission Helpers +// ============================================ + +static void emit_byte(Compiler *compiler, uint8_t byte) { + chunk_write_byte(compiler->builder->chunk, byte, compiler->current_line); +} + +static void emit_bytes(Compiler *compiler, uint8_t b1, uint8_t b2) { + emit_byte(compiler, b1); + emit_byte(compiler, b2); +} + +static void emit_short(Compiler *compiler, uint16_t value) { + chunk_write_short(compiler->builder->chunk, value, compiler->current_line); +} + +static int emit_jump(Compiler *compiler, OpCode op) { + return chunk_write_jump(compiler->builder->chunk, op, compiler->current_line); +} + +static void patch_jump(Compiler *compiler, int offset) { + chunk_patch_jump(compiler->builder->chunk, offset); +} + +static void emit_loop(Compiler *compiler, int loop_start) { + int offset = compiler->builder->chunk->code_count - loop_start + 3; + if (offset > 0xFFFF) { + compiler_error(compiler, "Loop body too large"); + return; + } + emit_byte(compiler, BC_LOOP); + emit_byte(compiler, (offset >> 8) & 0xFF); + emit_byte(compiler, offset & 0xFF); +} + +static int make_constant(Compiler *compiler, Constant constant) { + int idx = chunk_add_constant(compiler->builder->chunk, constant); + if (idx > 0xFFFF) { + compiler_error(compiler, "Too many constants in one chunk"); + return 0; + } + return idx; +} + +static void emit_constant(Compiler *compiler, Constant constant) { + int idx = make_constant(compiler, constant); + emit_byte(compiler, BC_CONST); + emit_short(compiler, idx); +} + +// ============================================ +// Expression Compilation +// ============================================ + +// Forward declaration +static void compile_expression(Compiler *compiler, Expr *expr); + +static void compile_number(Compiler *compiler, Expr *expr) { + // Check if we can use a byte constant (integer 0-255) + if (!expr->as.number.is_float && expr->as.number.int_value >= 0 && expr->as.number.int_value <= 255) { + emit_byte(compiler, BC_CONST_BYTE); + emit_byte(compiler, (uint8_t)expr->as.number.int_value); + return; + } + + Constant c; + if (!expr->as.number.is_float) { + if (expr->as.number.int_value >= INT32_MIN && expr->as.number.int_value <= INT32_MAX) { + c.type = CONST_I32; + c.as.i32 = (int32_t)expr->as.number.int_value; + } else { + c.type = CONST_I64; + c.as.i64 = expr->as.number.int_value; + } + } else { + c.type = CONST_F64; + c.as.f64 = expr->as.number.float_value; + } + emit_constant(compiler, c); +} + +static void compile_bool(Compiler *compiler, Expr *expr) { + emit_byte(compiler, expr->as.boolean ? BC_TRUE : BC_FALSE); +} + +static void compile_null(Compiler *compiler) { + emit_byte(compiler, BC_NULL); +} + +static void compile_string(Compiler *compiler, Expr *expr) { + const char *str = expr->as.string; + int len = strlen(str); + int idx = chunk_add_string(compiler->builder->chunk, str, len); + emit_byte(compiler, BC_CONST); + emit_short(compiler, idx); +} + +static void compile_rune(Compiler *compiler, Expr *expr) { + // Runes are stored as i32 constants + Constant c = {.type = CONST_I32, .as.i32 = (int32_t)expr->as.rune}; + int idx = make_constant(compiler, c); + emit_byte(compiler, BC_CONST); + emit_short(compiler, idx); + // TODO: Mark as rune type +} + +static void compile_identifier(Compiler *compiler, Expr *expr) { + const char *name = expr->as.ident.name; + + // Check for resolved local + if (expr->as.ident.resolved.is_resolved) { + int depth = expr->as.ident.resolved.depth; + int slot = expr->as.ident.resolved.slot; + + if (depth == 0) { + emit_byte(compiler, BC_GET_LOCAL); + emit_byte(compiler, (uint8_t)slot); + } else { + // Upvalue + int upvalue = builder_resolve_upvalue(compiler->builder, name); + if (upvalue != -1) { + emit_byte(compiler, BC_GET_UPVALUE); + emit_byte(compiler, (uint8_t)upvalue); + } else { + compiler_error(compiler, "Cannot resolve variable"); + } + } + return; + } + + // Try local + int local = builder_resolve_local(compiler->builder, name); + if (local != -1) { + emit_byte(compiler, BC_GET_LOCAL); + emit_byte(compiler, (uint8_t)local); + return; + } + + // Try upvalue + int upvalue = builder_resolve_upvalue(compiler->builder, name); + if (upvalue != -1) { + emit_byte(compiler, BC_GET_UPVALUE); + emit_byte(compiler, (uint8_t)upvalue); + return; + } + + // Global + int idx = chunk_add_identifier(compiler->builder->chunk, name); + emit_byte(compiler, BC_GET_GLOBAL); + emit_short(compiler, idx); +} + +static void compile_binary(Compiler *compiler, Expr *expr) { + BinaryOp op = expr->as.binary.op; + + // Short-circuit operators need special handling + if (op == OP_AND) { + compile_expression(compiler, expr->as.binary.left); + int end_jump = emit_jump(compiler, BC_JUMP_IF_FALSE); + emit_byte(compiler, BC_POP); + compile_expression(compiler, expr->as.binary.right); + patch_jump(compiler, end_jump); + return; + } + + if (op == OP_OR) { + compile_expression(compiler, expr->as.binary.left); + int else_jump = emit_jump(compiler, BC_JUMP_IF_FALSE); + int end_jump = emit_jump(compiler, BC_JUMP); + patch_jump(compiler, else_jump); + emit_byte(compiler, BC_POP); + compile_expression(compiler, expr->as.binary.right); + patch_jump(compiler, end_jump); + return; + } + + // Regular binary ops: compile both operands, then operator + compile_expression(compiler, expr->as.binary.left); + compile_expression(compiler, expr->as.binary.right); + + switch (op) { + case OP_ADD: emit_byte(compiler, BC_ADD); break; + case OP_SUB: emit_byte(compiler, BC_SUB); break; + case OP_MUL: emit_byte(compiler, BC_MUL); break; + case OP_DIV: emit_byte(compiler, BC_DIV); break; + case OP_MOD: emit_byte(compiler, BC_MOD); break; + + case OP_EQUAL: emit_byte(compiler, BC_EQ); break; + case OP_NOT_EQUAL: emit_byte(compiler, BC_NE); break; + case OP_LESS: emit_byte(compiler, BC_LT); break; + case OP_LESS_EQUAL: emit_byte(compiler, BC_LE); break; + case OP_GREATER: emit_byte(compiler, BC_GT); break; + case OP_GREATER_EQUAL: emit_byte(compiler, BC_GE); break; + + case OP_BIT_AND: emit_byte(compiler, BC_BIT_AND); break; + case OP_BIT_OR: emit_byte(compiler, BC_BIT_OR); break; + case OP_BIT_XOR: emit_byte(compiler, BC_BIT_XOR); break; + case OP_BIT_LSHIFT: emit_byte(compiler, BC_LSHIFT); break; + case OP_BIT_RSHIFT: emit_byte(compiler, BC_RSHIFT); break; + + default: + compiler_error(compiler, "Unknown binary operator"); + } +} + +static void compile_unary(Compiler *compiler, Expr *expr) { + compile_expression(compiler, expr->as.unary.operand); + + switch (expr->as.unary.op) { + case UNARY_NEGATE: + emit_byte(compiler, BC_NEGATE); + break; + case UNARY_NOT: + emit_byte(compiler, BC_NOT); + break; + case UNARY_BIT_NOT: + emit_byte(compiler, BC_BIT_NOT); + break; + default: + compiler_error(compiler, "Unknown unary operator"); + } +} + +static void compile_ternary(Compiler *compiler, Expr *expr) { + compile_expression(compiler, expr->as.ternary.condition); + + int then_jump = emit_jump(compiler, BC_JUMP_IF_FALSE); + emit_byte(compiler, BC_POP); // Pop condition + + compile_expression(compiler, expr->as.ternary.true_expr); + + int else_jump = emit_jump(compiler, BC_JUMP); + patch_jump(compiler, then_jump); + emit_byte(compiler, BC_POP); // Pop condition + + compile_expression(compiler, expr->as.ternary.false_expr); + + patch_jump(compiler, else_jump); +} + +static void compile_assign(Compiler *compiler, Expr *expr) { + const char *name = expr->as.assign.name; + + // Compile the value first + compile_expression(compiler, expr->as.assign.value); + + // Check for resolved local + if (expr->as.assign.resolved.is_resolved) { + int depth = expr->as.assign.resolved.depth; + int slot = expr->as.assign.resolved.slot; + + if (depth == 0) { + emit_byte(compiler, BC_SET_LOCAL); + emit_byte(compiler, (uint8_t)slot); + } else { + int upvalue = builder_resolve_upvalue(compiler->builder, name); + emit_byte(compiler, BC_SET_UPVALUE); + emit_byte(compiler, (uint8_t)upvalue); + } + return; + } + + // Try local + int local = builder_resolve_local(compiler->builder, name); + if (local != -1) { + emit_byte(compiler, BC_SET_LOCAL); + emit_byte(compiler, (uint8_t)local); + return; + } + + // Try upvalue + int upvalue = builder_resolve_upvalue(compiler->builder, name); + if (upvalue != -1) { + emit_byte(compiler, BC_SET_UPVALUE); + emit_byte(compiler, (uint8_t)upvalue); + return; + } + + // Global + int idx = chunk_add_identifier(compiler->builder->chunk, name); + emit_byte(compiler, BC_SET_GLOBAL); + emit_short(compiler, idx); +} + +static void compile_call(Compiler *compiler, Expr *expr) { + Expr *func = expr->as.call.func; + int argc = expr->as.call.num_args; + + // Check for builtin calls + if (func->type == EXPR_IDENT) { + const char *name = func->as.ident.name; + + // Check if it's a builtin + BuiltinId builtin = builtin_lookup(name); + if (builtin != (BuiltinId)-1) { + // Compile arguments + for (int i = 0; i < argc; i++) { + compile_expression(compiler, expr->as.call.args[i]); + } + + // Special case for print (it has its own opcode) + if (builtin == BUILTIN_PRINT) { + emit_byte(compiler, BC_PRINT); + emit_byte(compiler, (uint8_t)argc); + return; + } + + // General builtin call + emit_byte(compiler, BC_CALL_BUILTIN); + emit_short(compiler, (uint16_t)builtin); + emit_byte(compiler, (uint8_t)argc); + return; + } + } + + // General function call + compile_expression(compiler, func); + + // Compile arguments + for (int i = 0; i < argc; i++) { + compile_expression(compiler, expr->as.call.args[i]); + } + + emit_byte(compiler, BC_CALL); + emit_byte(compiler, (uint8_t)argc); +} + +static void compile_array_literal(Compiler *compiler, Expr *expr) { + int count = expr->as.array_literal.num_elements; + + // Compile each element + for (int i = 0; i < count; i++) { + compile_expression(compiler, expr->as.array_literal.elements[i]); + } + + emit_byte(compiler, BC_ARRAY); + emit_short(compiler, (uint16_t)count); +} + +static void compile_object_literal(Compiler *compiler, Expr *expr) { + int count = expr->as.object_literal.num_fields; + + // Compile each key-value pair + for (int i = 0; i < count; i++) { + // Key as string constant + const char *key = expr->as.object_literal.field_names[i]; + int idx = chunk_add_string(compiler->builder->chunk, key, strlen(key)); + emit_byte(compiler, BC_CONST); + emit_short(compiler, idx); + + // Value + compile_expression(compiler, expr->as.object_literal.field_values[i]); + } + + emit_byte(compiler, BC_OBJECT); + emit_short(compiler, (uint16_t)count); +} + +static void compile_get_property(Compiler *compiler, Expr *expr) { + compile_expression(compiler, expr->as.get_property.object); + + const char *name = expr->as.get_property.property; + int idx = chunk_add_identifier(compiler->builder->chunk, name); + emit_byte(compiler, BC_GET_PROPERTY); + emit_short(compiler, idx); +} + +static void compile_set_property(Compiler *compiler, Expr *expr) { + compile_expression(compiler, expr->as.set_property.object); + compile_expression(compiler, expr->as.set_property.value); + + const char *name = expr->as.set_property.property; + int idx = chunk_add_identifier(compiler->builder->chunk, name); + emit_byte(compiler, BC_SET_PROPERTY); + emit_short(compiler, idx); +} + +static void compile_index(Compiler *compiler, Expr *expr) { + compile_expression(compiler, expr->as.index.object); + compile_expression(compiler, expr->as.index.index); + emit_byte(compiler, BC_GET_INDEX); +} + +static void compile_index_assign(Compiler *compiler, Expr *expr) { + compile_expression(compiler, expr->as.index_assign.object); + compile_expression(compiler, expr->as.index_assign.index); + compile_expression(compiler, expr->as.index_assign.value); + emit_byte(compiler, BC_SET_INDEX); +} + +static void compile_prefix_inc(Compiler *compiler, Expr *expr) { + // Get current value + compile_expression(compiler, expr->as.prefix_inc.operand); + // Increment + emit_byte(compiler, BC_CONST_BYTE); + emit_byte(compiler, 1); + emit_byte(compiler, BC_ADD); + // Store back and leave new value on stack + // TODO: Need to handle assignment target properly +} + +static void compile_null_coalesce(Compiler *compiler, Expr *expr) { + compile_expression(compiler, expr->as.null_coalesce.left); + // If not null, skip the right side + int end_jump = emit_jump(compiler, BC_COALESCE); + emit_byte(compiler, BC_POP); + compile_expression(compiler, expr->as.null_coalesce.right); + patch_jump(compiler, end_jump); +} + +static void compile_expression(Compiler *compiler, Expr *expr) { + if (!expr) { + emit_byte(compiler, BC_NULL); + return; + } + + compiler->current_line = expr->line; + + switch (expr->type) { + case EXPR_NUMBER: + compile_number(compiler, expr); + break; + case EXPR_BOOL: + compile_bool(compiler, expr); + break; + case EXPR_STRING: + compile_string(compiler, expr); + break; + case EXPR_RUNE: + compile_rune(compiler, expr); + break; + case EXPR_IDENT: + compile_identifier(compiler, expr); + break; + case EXPR_NULL: + compile_null(compiler); + break; + case EXPR_BINARY: + compile_binary(compiler, expr); + break; + case EXPR_UNARY: + compile_unary(compiler, expr); + break; + case EXPR_TERNARY: + compile_ternary(compiler, expr); + break; + case EXPR_CALL: + compile_call(compiler, expr); + break; + case EXPR_ASSIGN: + compile_assign(compiler, expr); + break; + case EXPR_GET_PROPERTY: + compile_get_property(compiler, expr); + break; + case EXPR_SET_PROPERTY: + compile_set_property(compiler, expr); + break; + case EXPR_INDEX: + compile_index(compiler, expr); + break; + case EXPR_INDEX_ASSIGN: + compile_index_assign(compiler, expr); + break; + case EXPR_ARRAY_LITERAL: + compile_array_literal(compiler, expr); + break; + case EXPR_OBJECT_LITERAL: + compile_object_literal(compiler, expr); + break; + case EXPR_PREFIX_INC: + compile_prefix_inc(compiler, expr); + break; + case EXPR_NULL_COALESCE: + compile_null_coalesce(compiler, expr); + break; + case EXPR_FUNCTION: + // TODO: Compile function + emit_byte(compiler, BC_NULL); + break; + case EXPR_AWAIT: + // TODO: Compile await + compile_expression(compiler, expr->as.await_expr.awaited_expr); + emit_byte(compiler, BC_AWAIT); + break; + default: + compiler_error(compiler, "Unsupported expression type"); + emit_byte(compiler, BC_NULL); + } +} + +// ============================================ +// Statement Compilation +// ============================================ + +static void compile_statement(Compiler *compiler, Stmt *stmt); + +static void compile_let(Compiler *compiler, Stmt *stmt, bool is_const) { + const char *name = stmt->as.let.name; + + // Compile initializer + if (stmt->as.let.value) { + compile_expression(compiler, stmt->as.let.value); + } else { + emit_byte(compiler, BC_NULL); + } + + // Define variable + if (compiler->builder->scope_depth > 0) { + // Local variable + int slot = builder_declare_local(compiler->builder, name, is_const, TYPE_ID_NULL); + if (slot == -1) { + compiler_error(compiler, "Variable already declared in this scope"); + } + builder_mark_initialized(compiler->builder); + // Value is already on stack in the right slot + } else { + // Global variable + int idx = chunk_add_identifier(compiler->builder->chunk, name); + emit_byte(compiler, BC_DEFINE_GLOBAL); + emit_short(compiler, idx); + } +} + +static void compile_expr_stmt(Compiler *compiler, Stmt *stmt) { + compile_expression(compiler, stmt->as.expr); + emit_byte(compiler, BC_POP); +} + +static void compile_if(Compiler *compiler, Stmt *stmt) { + compile_expression(compiler, stmt->as.if_stmt.condition); + + int then_jump = emit_jump(compiler, BC_JUMP_IF_FALSE); + emit_byte(compiler, BC_POP); // Pop condition + + compile_statement(compiler, stmt->as.if_stmt.then_branch); + + int else_jump = emit_jump(compiler, BC_JUMP); + patch_jump(compiler, then_jump); + emit_byte(compiler, BC_POP); // Pop condition + + if (stmt->as.if_stmt.else_branch) { + compile_statement(compiler, stmt->as.if_stmt.else_branch); + } + + patch_jump(compiler, else_jump); +} + +static void compile_while(Compiler *compiler, Stmt *stmt) { + int loop_start = compiler->builder->chunk->code_count; + builder_begin_loop(compiler->builder); + + compile_expression(compiler, stmt->as.while_stmt.condition); + + int exit_jump = emit_jump(compiler, BC_JUMP_IF_FALSE); + emit_byte(compiler, BC_POP); // Pop condition + + compile_statement(compiler, stmt->as.while_stmt.body); + + emit_loop(compiler, loop_start); + + patch_jump(compiler, exit_jump); + emit_byte(compiler, BC_POP); // Pop condition + + builder_end_loop(compiler->builder); +} + +static void compile_for(Compiler *compiler, Stmt *stmt) { + builder_begin_scope(compiler->builder); + + // Initializer + if (stmt->as.for_loop.initializer) { + compile_statement(compiler, stmt->as.for_loop.initializer); + } + + int loop_start = compiler->builder->chunk->code_count; + builder_begin_loop(compiler->builder); + + // Condition + int exit_jump = -1; + if (stmt->as.for_loop.condition) { + compile_expression(compiler, stmt->as.for_loop.condition); + exit_jump = emit_jump(compiler, BC_JUMP_IF_FALSE); + emit_byte(compiler, BC_POP); + } + + // Body + compile_statement(compiler, stmt->as.for_loop.body); + + // Increment + if (stmt->as.for_loop.increment) { + compile_expression(compiler, stmt->as.for_loop.increment); + emit_byte(compiler, BC_POP); + } + + emit_loop(compiler, loop_start); + + if (exit_jump != -1) { + patch_jump(compiler, exit_jump); + emit_byte(compiler, BC_POP); + } + + builder_end_loop(compiler->builder); + builder_end_scope(compiler->builder); +} + +static void compile_block(Compiler *compiler, Stmt *stmt) { + builder_begin_scope(compiler->builder); + for (int i = 0; i < stmt->as.block.count; i++) { + compile_statement(compiler, stmt->as.block.statements[i]); + } + builder_end_scope(compiler->builder); +} + +static void compile_return(Compiler *compiler, Stmt *stmt) { + if (stmt->as.return_stmt.value) { + compile_expression(compiler, stmt->as.return_stmt.value); + } else { + emit_byte(compiler, BC_NULL); + } + emit_byte(compiler, BC_RETURN); +} + +static void compile_break(Compiler *compiler) { + builder_emit_break(compiler->builder); +} + +static void compile_continue(Compiler *compiler) { + builder_emit_continue(compiler->builder); +} + +static void compile_statement(Compiler *compiler, Stmt *stmt) { + if (!stmt) return; + + compiler->current_line = stmt->line; + + switch (stmt->type) { + case STMT_LET: + compile_let(compiler, stmt, false); + break; + case STMT_CONST: + compile_let(compiler, stmt, true); + break; + case STMT_EXPR: + compile_expr_stmt(compiler, stmt); + break; + case STMT_IF: + compile_if(compiler, stmt); + break; + case STMT_WHILE: + compile_while(compiler, stmt); + break; + case STMT_FOR: + compile_for(compiler, stmt); + break; + case STMT_BLOCK: + compile_block(compiler, stmt); + break; + case STMT_RETURN: + compile_return(compiler, stmt); + break; + case STMT_BREAK: + compile_break(compiler); + break; + case STMT_CONTINUE: + compile_continue(compiler); + break; + default: + // TODO: Handle other statement types + break; + } +} + +// ============================================ +// Public API +// ============================================ + +void compile_stmt(Compiler *compiler, Stmt *stmt) { + compile_statement(compiler, stmt); +} + +void compile_expr(Compiler *compiler, Expr *expr) { + compile_expression(compiler, expr); +} + +Chunk* compile_program(Stmt **stmts, int count) { + Compiler *compiler = compiler_new(NULL); + + // Compile all statements + for (int i = 0; i < count; i++) { + compile_statement(compiler, stmts[i]); + } + + // End with return + emit_byte(compiler, BC_NULL); + emit_byte(compiler, BC_RETURN); + + if (compiler->had_error) { + chunk_builder_free(compiler->builder); + free(compiler); + return NULL; + } + + Chunk *chunk = chunk_builder_finish(compiler->builder); + compiler->builder = NULL; + free(compiler); + + return chunk; +} diff --git a/src/backends/vm/compiler.h b/src/backends/vm/compiler.h new file mode 100644 index 00000000..d6dc19f3 --- /dev/null +++ b/src/backends/vm/compiler.h @@ -0,0 +1,41 @@ +/* + * Hemlock Bytecode VM - Compiler + * + * AST to bytecode compiler. + */ + +#ifndef HEMLOCK_VM_COMPILER_H +#define HEMLOCK_VM_COMPILER_H + +#include "chunk.h" +#include "ast.h" + +// Compiler state +typedef struct Compiler { + ChunkBuilder *builder; // Current chunk being built + struct Compiler *enclosing; // Enclosing compiler (for nested functions) + + // Current function info + char *function_name; + bool is_async; + int current_line; + + // Error state + bool had_error; + bool panic_mode; +} Compiler; + +// Compile a program (list of statements) to bytecode +Chunk* compile_program(Stmt **stmts, int count); + +// Compile a single statement +void compile_stmt(Compiler *compiler, Stmt *stmt); + +// Compile an expression +void compile_expr(Compiler *compiler, Expr *expr); + +// Error reporting +void compiler_error(Compiler *compiler, const char *message); +void compiler_error_at(Compiler *compiler, int line, const char *message); + +#endif // HEMLOCK_VM_COMPILER_H diff --git a/src/backends/vm/debug.c b/src/backends/vm/debug.c new file mode 100644 index 00000000..1ab440d5 --- /dev/null +++ b/src/backends/vm/debug.c @@ -0,0 +1,388 @@ +/* + * Hemlock Bytecode VM - Debug Utilities + * + * Disassembler and debugging tools. + */ + +#include "debug.h" +#include "instruction.h" +#include +#include + +// ============================================ +// Constant Printing +// ============================================ + +void print_constant(Constant *constant) { + switch (constant->type) { + case CONST_I32: + printf("%d", constant->as.i32); + break; + case CONST_I64: + printf("%lld", (long long)constant->as.i64); + break; + case CONST_F64: + printf("%g", constant->as.f64); + break; + case CONST_STRING: + printf("\"%.*s\"", constant->as.string.length, constant->as.string.data); + break; + case CONST_IDENTIFIER: + printf("'%s'", constant->as.string.data); + break; + case CONST_FUNCTION: + if (constant->as.function->name) { + printf("", constant->as.function->name); + } else { + printf(""); + } + break; + default: + printf(""); + } +} + +// ============================================ +// Value Printing (for runtime debugging) +// ============================================ + +void print_value(Value value) { + switch (value.type) { + case VAL_NULL: + printf("null"); + break; + case VAL_BOOL: + printf("%s", value.as.as_bool ? "true" : "false"); + break; + case VAL_I8: + printf("%d", value.as.as_i8); + break; + case VAL_I16: + printf("%d", value.as.as_i16); + break; + case VAL_I32: + printf("%d", value.as.as_i32); + break; + case VAL_I64: + printf("%lld", (long long)value.as.as_i64); + break; + case VAL_U8: + printf("%u", value.as.as_u8); + break; + case VAL_U16: + printf("%u", value.as.as_u16); + break; + case VAL_U32: + printf("%u", value.as.as_u32); + break; + case VAL_U64: + printf("%llu", (unsigned long long)value.as.as_u64); + break; + case VAL_F32: + printf("%g", value.as.as_f32); + break; + case VAL_F64: + printf("%g", value.as.as_f64); + break; + case VAL_STRING: + if (value.as.as_string) { + printf("\"%s\"", value.as.as_string->data); + } else { + printf("\"\""); + } + break; + case VAL_RUNE: + if (value.as.as_rune < 128) { + printf("'%c'", (char)value.as.as_rune); + } else { + printf("'\\u%04X'", value.as.as_rune); + } + break; + case VAL_ARRAY: + printf(""); + break; + case VAL_OBJECT: + printf(""); + break; + case VAL_FUNCTION: + printf(""); + break; + case VAL_PTR: + printf("", value.as.as_ptr); + break; + case VAL_BUFFER: + printf(""); + break; + case VAL_TASK: + printf(""); + break; + case VAL_CHANNEL: + printf(""); + break; + case VAL_FILE: + printf(""); + break; + default: + printf(""); + } +} + +// ============================================ +// Instruction Disassembly +// ============================================ + +static int simple_instruction(const char *name, int offset) { + printf("%s\n", name); + return offset + 1; +} + +static int byte_instruction(const char *name, Chunk *chunk, int offset) { + uint8_t slot = chunk->code[offset + 1]; + printf("%-16s %4d\n", name, slot); + return offset + 2; +} + +static int short_instruction(const char *name, Chunk *chunk, int offset) { + uint16_t value = (chunk->code[offset + 1] << 8) | chunk->code[offset + 2]; + printf("%-16s %4d\n", name, value); + return offset + 3; +} + +static int constant_instruction(const char *name, Chunk *chunk, int offset) { + uint16_t index = (chunk->code[offset + 1] << 8) | chunk->code[offset + 2]; + printf("%-16s %4d ", name, index); + if (index < chunk->const_count) { + print_constant(&chunk->constants[index]); + } + printf("\n"); + return offset + 3; +} + +static int jump_instruction(const char *name, int sign, Chunk *chunk, int offset) { + uint16_t jump = (chunk->code[offset + 1] << 8) | chunk->code[offset + 2]; + int target = offset + 3 + sign * jump; + printf("%-16s %4d -> %d\n", name, jump, target); + return offset + 3; +} + +static int invoke_instruction(const char *name, Chunk *chunk, int offset) { + uint16_t index = (chunk->code[offset + 1] << 8) | chunk->code[offset + 2]; + uint8_t argc = chunk->code[offset + 3]; + printf("%-16s %4d ", name, index); + if (index < chunk->const_count) { + print_constant(&chunk->constants[index]); + } + printf(" (%d args)\n", argc); + return offset + 4; +} + +static int closure_instruction(Chunk *chunk, int offset) { + uint16_t index = (chunk->code[offset + 1] << 8) | chunk->code[offset + 2]; + uint8_t upvalue_count = chunk->code[offset + 3]; + + printf("%-16s %4d ", "CLOSURE", index); + if (index < chunk->const_count) { + print_constant(&chunk->constants[index]); + } + printf("\n"); + + // Print upvalue descriptors + int current = offset + 4; + for (int i = 0; i < upvalue_count; i++) { + uint8_t is_local = chunk->code[current++]; + uint8_t idx = chunk->code[current++]; + printf(" | %s %d\n", + is_local ? "local" : "upvalue", idx); + } + + return current; +} + +int disassemble_instruction(Chunk *chunk, int offset) { + printf("%04d ", offset); + + // Print line number + int line = chunk_get_line(chunk, offset); + if (offset > 0 && line == chunk_get_line(chunk, offset - 1)) { + printf(" | "); + } else { + printf("%4d ", line); + } + + uint8_t instruction = chunk->code[offset]; + const InstructionInfo *info = instruction_info(instruction); + + switch (instruction) { + // Simple instructions (no operands) + case BC_NULL: + case BC_TRUE: + case BC_FALSE: + case BC_ADD: + case BC_SUB: + case BC_MUL: + case BC_DIV: + case BC_MOD: + case BC_NEGATE: + case BC_INC: + case BC_DEC: + case BC_ADD_I32: + case BC_SUB_I32: + case BC_MUL_I32: + case BC_EQ: + case BC_NE: + case BC_LT: + case BC_LE: + case BC_GT: + case BC_GE: + case BC_EQ_I32: + case BC_LT_I32: + case BC_NOT: + case BC_BIT_NOT: + case BC_BIT_AND: + case BC_BIT_OR: + case BC_BIT_XOR: + case BC_LSHIFT: + case BC_RSHIFT: + case BC_GET_INDEX: + case BC_SET_INDEX: + case BC_CLOSE_UPVALUE: + case BC_RETURN: + case BC_APPLY: + case BC_POP: + case BC_CATCH: + case BC_FINALLY: + case BC_END_TRY: + case BC_THROW: + case BC_AWAIT: + case BC_JOIN: + case BC_DETACH: + case BC_CHANNEL: + case BC_SEND: + case BC_RECV: + case BC_SELECT: + case BC_TYPEOF: + case BC_NOP: + case BC_ASSERT: + case BC_DEBUG_BREAK: + case BC_HALT: + case BC_BREAK: + case BC_CONTINUE: + case BC_FOR_IN_INIT: + return simple_instruction(info->name, offset); + + // Byte operand instructions + case BC_CONST_BYTE: + case BC_GET_LOCAL: + case BC_SET_LOCAL: + case BC_GET_UPVALUE: + case BC_SET_UPVALUE: + case BC_CALL: + case BC_TAIL_CALL: + case BC_SPAWN: + case BC_PRINT: + case BC_POPN: + case BC_CAST: + case BC_CHECK_TYPE: + return byte_instruction(info->name, chunk, offset); + + // Short operand instructions (constant pool index) + case BC_CONST: + case BC_GET_GLOBAL: + case BC_SET_GLOBAL: + case BC_DEFINE_GLOBAL: + case BC_GET_PROPERTY: + case BC_SET_PROPERTY: + case BC_SUPER: + case BC_DEFER: + case BC_DEFINE_TYPE: + case BC_DEFINE_ENUM: + case BC_ENUM_VALUE: + return constant_instruction(info->name, chunk, offset); + + // Jump instructions + case BC_JUMP: + case BC_JUMP_IF_FALSE: + case BC_JUMP_IF_TRUE: + case BC_JUMP_IF_FALSE_POP: + case BC_COALESCE: + case BC_OPTIONAL_CHAIN: + case BC_CASE: + case BC_FOR_IN_NEXT: + return jump_instruction(info->name, 1, chunk, offset); + + case BC_LOOP: + return jump_instruction(info->name, -1, chunk, offset); + + // Array/object construction + case BC_ARRAY: + case BC_OBJECT: + case BC_STRING_INTERP: + case BC_SWITCH: + return short_instruction(info->name, chunk, offset); + + // Invoke instructions (idx:16 + argc:8) + case BC_CALL_METHOD: + case BC_CALL_BUILTIN: + case BC_INVOKE: + return invoke_instruction(info->name, chunk, offset); + + // Closure (special handling for upvalue descriptors) + case BC_CLOSURE: + return closure_instruction(chunk, offset); + + // Try (catch:16 + finally:16) + case BC_TRY: { + uint16_t catch_offset = (chunk->code[offset + 1] << 8) | chunk->code[offset + 2]; + uint16_t finally_offset = (chunk->code[offset + 3] << 8) | chunk->code[offset + 4]; + printf("%-16s catch->%d finally->%d\n", "TRY", + offset + 5 + catch_offset, offset + 5 + finally_offset); + return offset + 5; + } + + default: + printf("Unknown opcode %d\n", instruction); + return offset + 1; + } +} + +// ============================================ +// Chunk Disassembly +// ============================================ + +void disassemble_chunk(Chunk *chunk, const char *name) { + printf("== %s ==\n", name); + + // Print function info + if (chunk->arity > 0 || chunk->optional_count > 0 || chunk->has_rest_param) { + printf("arity: %d, optional: %d, rest: %s, async: %s\n", + chunk->arity, chunk->optional_count, + chunk->has_rest_param ? "yes" : "no", + chunk->is_async ? "yes" : "no"); + } + + // Print constants + if (chunk->const_count > 0) { + printf("-- constants --\n"); + for (int i = 0; i < chunk->const_count; i++) { + printf(" %4d: ", i); + print_constant(&chunk->constants[i]); + printf("\n"); + } + } + + // Print bytecode + printf("-- code --\n"); + int offset = 0; + while (offset < chunk->code_count) { + offset = disassemble_instruction(chunk, offset); + } + + // Print nested functions + for (int i = 0; i < chunk->const_count; i++) { + if (chunk->constants[i].type == CONST_FUNCTION) { + Chunk *fn = chunk->constants[i].as.function; + printf("\n"); + disassemble_chunk(fn, fn->name ? fn->name : ""); + } + } +} diff --git a/src/backends/vm/debug.h b/src/backends/vm/debug.h new file mode 100644 index 00000000..33f6ddf0 --- /dev/null +++ b/src/backends/vm/debug.h @@ -0,0 +1,25 @@ +/* + * Hemlock Bytecode VM - Debug Utilities + * + * Disassembler and debugging tools. + */ + +#ifndef HEMLOCK_VM_DEBUG_H +#define HEMLOCK_VM_DEBUG_H + +#include "chunk.h" +#include "interpreter.h" // For Value type + +// Disassemble an entire chunk +void disassemble_chunk(Chunk *chunk, const char *name); + +// Disassemble a single instruction, returns next instruction offset +int disassemble_instruction(Chunk *chunk, int offset); + +// Print constant value +void print_constant(Constant *constant); + +// Print value (for stack traces) +void print_value(Value value); + +#endif // HEMLOCK_VM_DEBUG_H diff --git a/src/backends/vm/instruction.c b/src/backends/vm/instruction.c new file mode 100644 index 00000000..52e4e7e0 --- /dev/null +++ b/src/backends/vm/instruction.c @@ -0,0 +1,303 @@ +/* + * Hemlock Bytecode VM - Instruction Info + * + * Metadata for each opcode: name, operand bytes, stack effect. + */ + +#include "instruction.h" +#include +#include + +// Instruction info table indexed by opcode +static const InstructionInfo info_table[256] = { + // Category 1: Constants & Literals (0x00-0x0F) + [BC_CONST] = {"CONST", 2, 1}, + [BC_CONST_BYTE] = {"CONST_BYTE", 1, 1}, + [BC_NULL] = {"NULL", 0, 1}, + [BC_TRUE] = {"TRUE", 0, 1}, + [BC_FALSE] = {"FALSE", 0, 1}, + [BC_ARRAY] = {"ARRAY", 2, STACK_EFFECT_VARIABLE}, + [BC_OBJECT] = {"OBJECT", 2, STACK_EFFECT_VARIABLE}, + [BC_STRING_INTERP] = {"STRING_INTERP", 2, STACK_EFFECT_VARIABLE}, + [BC_CLOSURE] = {"CLOSURE", 3, 1}, + [BC_ENUM_VALUE] = {"ENUM_VALUE", 2, 1}, + + // Category 2: Variables (0x10-0x1F) + [BC_GET_LOCAL] = {"GET_LOCAL", 1, 1}, + [BC_SET_LOCAL] = {"SET_LOCAL", 1, 0}, + [BC_GET_UPVALUE] = {"GET_UPVALUE", 1, 1}, + [BC_SET_UPVALUE] = {"SET_UPVALUE", 1, 0}, + [BC_GET_GLOBAL] = {"GET_GLOBAL", 2, 1}, + [BC_SET_GLOBAL] = {"SET_GLOBAL", 2, 0}, + [BC_DEFINE_GLOBAL] = {"DEFINE_GLOBAL", 2, -1}, + [BC_GET_PROPERTY] = {"GET_PROPERTY", 2, 0}, + [BC_SET_PROPERTY] = {"SET_PROPERTY", 2, -1}, + [BC_GET_INDEX] = {"GET_INDEX", 0, -1}, + [BC_SET_INDEX] = {"SET_INDEX", 0, -2}, + [BC_CLOSE_UPVALUE] = {"CLOSE_UPVALUE", 0, 0}, + + // Category 3: Arithmetic (0x20-0x2F) + [BC_ADD] = {"ADD", 0, -1}, + [BC_SUB] = {"SUB", 0, -1}, + [BC_MUL] = {"MUL", 0, -1}, + [BC_DIV] = {"DIV", 0, -1}, + [BC_MOD] = {"MOD", 0, -1}, + [BC_NEGATE] = {"NEGATE", 0, 0}, + [BC_INC] = {"INC", 0, 0}, + [BC_DEC] = {"DEC", 0, 0}, + [BC_ADD_I32] = {"ADD_I32", 0, -1}, + [BC_SUB_I32] = {"SUB_I32", 0, -1}, + [BC_MUL_I32] = {"MUL_I32", 0, -1}, + + // Category 4: Comparison (0x30-0x3F) + [BC_EQ] = {"EQ", 0, -1}, + [BC_NE] = {"NE", 0, -1}, + [BC_LT] = {"LT", 0, -1}, + [BC_LE] = {"LE", 0, -1}, + [BC_GT] = {"GT", 0, -1}, + [BC_GE] = {"GE", 0, -1}, + [BC_EQ_I32] = {"EQ_I32", 0, -1}, + [BC_LT_I32] = {"LT_I32", 0, -1}, + + // Category 5: Logical & Bitwise (0x40-0x4F) + [BC_NOT] = {"NOT", 0, 0}, + [BC_BIT_NOT] = {"BIT_NOT", 0, 0}, + [BC_BIT_AND] = {"BIT_AND", 0, -1}, + [BC_BIT_OR] = {"BIT_OR", 0, -1}, + [BC_BIT_XOR] = {"BIT_XOR", 0, -1}, + [BC_LSHIFT] = {"LSHIFT", 0, -1}, + [BC_RSHIFT] = {"RSHIFT", 0, -1}, + [BC_COALESCE] = {"COALESCE", 2, 0}, + [BC_OPTIONAL_CHAIN] = {"OPTIONAL_CHAIN", 2, 0}, + + // Category 6: Control Flow (0x50-0x5F) + [BC_JUMP] = {"JUMP", 2, 0}, + [BC_JUMP_IF_FALSE] = {"JUMP_IF_FALSE", 2, -1}, + [BC_JUMP_IF_TRUE] = {"JUMP_IF_TRUE", 2, -1}, + [BC_JUMP_IF_FALSE_POP] = {"JUMP_IF_FALSE_POP", 2, -1}, + [BC_LOOP] = {"LOOP", 2, 0}, + [BC_BREAK] = {"BREAK", 0, 0}, + [BC_CONTINUE] = {"CONTINUE", 0, 0}, + [BC_SWITCH] = {"SWITCH", 2, -1}, + [BC_CASE] = {"CASE", 2, 0}, + [BC_FOR_IN_INIT] = {"FOR_IN_INIT", 0, 1}, + [BC_FOR_IN_NEXT] = {"FOR_IN_NEXT", 2, 1}, + [BC_POP] = {"POP", 0, -1}, + [BC_POPN] = {"POPN", 1, STACK_EFFECT_VARIABLE}, + + // Category 7: Functions & Calls (0x60-0x6F) + [BC_CALL] = {"CALL", 1, STACK_EFFECT_VARIABLE}, + [BC_CALL_METHOD] = {"CALL_METHOD", 3, STACK_EFFECT_VARIABLE}, + [BC_CALL_BUILTIN] = {"CALL_BUILTIN", 3, STACK_EFFECT_VARIABLE}, + [BC_RETURN] = {"RETURN", 0, 0}, + [BC_APPLY] = {"APPLY", 0, -1}, + [BC_TAIL_CALL] = {"TAIL_CALL", 1, STACK_EFFECT_VARIABLE}, + [BC_SUPER] = {"SUPER", 2, 0}, + [BC_INVOKE] = {"INVOKE", 3, STACK_EFFECT_VARIABLE}, + + // Category 8: Exception Handling (0x70-0x7F) + [BC_TRY] = {"TRY", 4, 0}, + [BC_CATCH] = {"CATCH", 0, 1}, + [BC_FINALLY] = {"FINALLY", 0, 0}, + [BC_END_TRY] = {"END_TRY", 0, 0}, + [BC_THROW] = {"THROW", 0, -1}, + [BC_DEFER] = {"DEFER", 2, 0}, + + // Category 9: Async & Concurrency (0x80-0x8F) + [BC_SPAWN] = {"SPAWN", 1, STACK_EFFECT_VARIABLE}, + [BC_AWAIT] = {"AWAIT", 0, 0}, + [BC_JOIN] = {"JOIN", 0, 0}, + [BC_DETACH] = {"DETACH", 0, -1}, + [BC_CHANNEL] = {"CHANNEL", 0, 0}, + [BC_SEND] = {"SEND", 0, -2}, + [BC_RECV] = {"RECV", 0, 0}, + [BC_SELECT] = {"SELECT", 0, 0}, + + // Category 10: Type Operations (0x90-0x9F) + [BC_TYPEOF] = {"TYPEOF", 0, 0}, + [BC_CAST] = {"CAST", 1, 0}, + [BC_CHECK_TYPE] = {"CHECK_TYPE", 1, 0}, + [BC_DEFINE_TYPE] = {"DEFINE_TYPE", 2, 0}, + [BC_DEFINE_ENUM] = {"DEFINE_ENUM", 2, 0}, + + // Category 11: Debug & Misc (0xF0-0xFF) + [BC_NOP] = {"NOP", 0, 0}, + [BC_PRINT] = {"PRINT", 1, STACK_EFFECT_VARIABLE}, + [BC_ASSERT] = {"ASSERT", 0, STACK_EFFECT_VARIABLE}, + [BC_DEBUG_BREAK] = {"DEBUG_BREAK", 0, 0}, + [BC_HALT] = {"HALT", 0, 0}, +}; + +const InstructionInfo* instruction_info(OpCode op) { + if (info_table[op].name == NULL) { + static const InstructionInfo unknown = {"UNKNOWN", 0, 0}; + return &unknown; + } + return &info_table[op]; +} + +// Get the size of an instruction in bytes (opcode + operands) +int instruction_size(OpCode op) { + return 1 + instruction_info(op)->operand_bytes; +} + +// Builtin name table +static const char* builtin_names[BUILTIN_COUNT] = { + // Memory (0-10) + [BUILTIN_ALLOC] = "alloc", + [BUILTIN_TALLOC] = "talloc", + [BUILTIN_REALLOC] = "realloc", + [BUILTIN_FREE] = "free", + [BUILTIN_MEMSET] = "memset", + [BUILTIN_MEMCPY] = "memcpy", + [BUILTIN_SIZEOF] = "sizeof", + [BUILTIN_BUFFER] = "buffer", + [BUILTIN_PTR_TO_BUFFER] = "ptr_to_buffer", + [BUILTIN_BUFFER_PTR] = "buffer_ptr", + [BUILTIN_PTR_NULL] = "ptr_null", + + // I/O (11-14) + [BUILTIN_PRINT] = "print", + [BUILTIN_EPRINT] = "eprint", + [BUILTIN_READ_LINE] = "read_line", + [BUILTIN_OPEN] = "open", + + // Type (15-17) + [BUILTIN_TYPEOF] = "typeof", + [BUILTIN_ASSERT] = "assert", + [BUILTIN_PANIC] = "panic", + + // Async (18-24) + [BUILTIN_SPAWN] = "spawn", + [BUILTIN_JOIN] = "join", + [BUILTIN_DETACH] = "detach", + [BUILTIN_CHANNEL] = "channel", + [BUILTIN_SELECT] = "select", + [BUILTIN_TASK_DEBUG_INFO] = "task_debug_info", + [BUILTIN_APPLY] = "apply", + + // Signal (25-26) + [BUILTIN_SIGNAL] = "signal", + [BUILTIN_RAISE] = "raise", + + // Exec (27-28) + [BUILTIN_EXEC] = "exec", + [BUILTIN_EXEC_ARGV] = "exec_argv", + + // Pointer read (29-41) + [BUILTIN_PTR_READ_I8] = "ptr_read_i8", + [BUILTIN_PTR_READ_I16] = "ptr_read_i16", + [BUILTIN_PTR_READ_I32] = "ptr_read_i32", + [BUILTIN_PTR_READ_I64] = "ptr_read_i64", + [BUILTIN_PTR_READ_U8] = "ptr_read_u8", + [BUILTIN_PTR_READ_U16] = "ptr_read_u16", + [BUILTIN_PTR_READ_U32] = "ptr_read_u32", + [BUILTIN_PTR_READ_U64] = "ptr_read_u64", + [BUILTIN_PTR_READ_F32] = "ptr_read_f32", + [BUILTIN_PTR_READ_F64] = "ptr_read_f64", + [BUILTIN_PTR_READ_PTR] = "ptr_read_ptr", + [BUILTIN_PTR_OFFSET] = "ptr_offset", + [BUILTIN_PTR_DEREF_I32] = "ptr_deref_i32", + + // Pointer write (42-52) + [BUILTIN_PTR_WRITE_I8] = "ptr_write_i8", + [BUILTIN_PTR_WRITE_I16] = "ptr_write_i16", + [BUILTIN_PTR_WRITE_I32] = "ptr_write_i32", + [BUILTIN_PTR_WRITE_I64] = "ptr_write_i64", + [BUILTIN_PTR_WRITE_U8] = "ptr_write_u8", + [BUILTIN_PTR_WRITE_U16] = "ptr_write_u16", + [BUILTIN_PTR_WRITE_U32] = "ptr_write_u32", + [BUILTIN_PTR_WRITE_U64] = "ptr_write_u64", + [BUILTIN_PTR_WRITE_F32] = "ptr_write_f32", + [BUILTIN_PTR_WRITE_F64] = "ptr_write_f64", + [BUILTIN_PTR_WRITE_PTR] = "ptr_write_ptr", + + // Atomics i32 (53-61) + [BUILTIN_ATOMIC_LOAD_I32] = "atomic_load_i32", + [BUILTIN_ATOMIC_STORE_I32] = "atomic_store_i32", + [BUILTIN_ATOMIC_ADD_I32] = "atomic_add_i32", + [BUILTIN_ATOMIC_SUB_I32] = "atomic_sub_i32", + [BUILTIN_ATOMIC_AND_I32] = "atomic_and_i32", + [BUILTIN_ATOMIC_OR_I32] = "atomic_or_i32", + [BUILTIN_ATOMIC_XOR_I32] = "atomic_xor_i32", + [BUILTIN_ATOMIC_CAS_I32] = "atomic_cas_i32", + [BUILTIN_ATOMIC_EXCHANGE_I32] = "atomic_exchange_i32", + + // Atomics i64 (62-70) + [BUILTIN_ATOMIC_LOAD_I64] = "atomic_load_i64", + [BUILTIN_ATOMIC_STORE_I64] = "atomic_store_i64", + [BUILTIN_ATOMIC_ADD_I64] = "atomic_add_i64", + [BUILTIN_ATOMIC_SUB_I64] = "atomic_sub_i64", + [BUILTIN_ATOMIC_AND_I64] = "atomic_and_i64", + [BUILTIN_ATOMIC_OR_I64] = "atomic_or_i64", + [BUILTIN_ATOMIC_XOR_I64] = "atomic_xor_i64", + [BUILTIN_ATOMIC_CAS_I64] = "atomic_cas_i64", + [BUILTIN_ATOMIC_EXCHANGE_I64] = "atomic_exchange_i64", + + // Atomics misc (71) + [BUILTIN_ATOMIC_FENCE] = "atomic_fence", + + // Callback (72-73) + [BUILTIN_CALLBACK] = "callback", + [BUILTIN_CALLBACK_FREE] = "callback_free", + + // Stack (74-75) + [BUILTIN_SET_STACK_LIMIT] = "set_stack_limit", + [BUILTIN_GET_STACK_LIMIT] = "get_stack_limit", + + // DNS/Network (76-78) + [BUILTIN_DNS_RESOLVE] = "dns_resolve", + [BUILTIN_SOCKET_CREATE] = "socket_create", + [BUILTIN_POLL] = "poll", +}; + +const char* builtin_name(BuiltinId id) { + if (id >= 0 && id < BUILTIN_COUNT && builtin_names[id] != NULL) { + return builtin_names[id]; + } + return "unknown_builtin"; +} + +// Lookup builtin ID by name +BuiltinId builtin_lookup(const char *name) { + for (int i = 0; i < BUILTIN_COUNT; i++) { + if (builtin_names[i] != NULL && strcmp(builtin_names[i], name) == 0) { + return (BuiltinId)i; + } + } + return -1; +} + +// Type name table +static const char* type_names[] = { + [TYPE_ID_I8] = "i8", + [TYPE_ID_I16] = "i16", + [TYPE_ID_I32] = "i32", + [TYPE_ID_I64] = "i64", + [TYPE_ID_U8] = "u8", + [TYPE_ID_U16] = "u16", + [TYPE_ID_U32] = "u32", + [TYPE_ID_U64] = "u64", + [TYPE_ID_F32] = "f32", + [TYPE_ID_F64] = "f64", + [TYPE_ID_BOOL] = "bool", + [TYPE_ID_STRING] = "string", + [TYPE_ID_RUNE] = "rune", + [TYPE_ID_ARRAY] = "array", + [TYPE_ID_OBJECT] = "object", + [TYPE_ID_PTR] = "ptr", + [TYPE_ID_BUFFER] = "buffer", + [TYPE_ID_NULL] = "null", + [TYPE_ID_FUNCTION] = "function", + [TYPE_ID_TASK] = "task", + [TYPE_ID_CHANNEL] = "channel", + [TYPE_ID_FILE] = "file", + [TYPE_ID_ENUM] = "enum", +}; + +const char* type_id_name(TypeId id) { + if (id >= 0 && id <= TYPE_ID_ENUM) { + return type_names[id]; + } + return "unknown"; +} diff --git a/src/backends/vm/instruction.h b/src/backends/vm/instruction.h index b3c714ba..61efc086 100644 --- a/src/backends/vm/instruction.h +++ b/src/backends/vm/instruction.h @@ -3,181 +3,17 @@ * * 82 opcodes organized into 11 categories. * Each opcode is 1 byte, with 0-3 bytes of operands. + * + * Prefix: BC_ (bytecode) to avoid conflicts with AST BinaryOp enum */ #ifndef HEMLOCK_VM_INSTRUCTION_H #define HEMLOCK_VM_INSTRUCTION_H #include +#include -typedef enum { - // ======================================== - // Category 1: Constants & Literals (0x00-0x0F) - // ======================================== - OP_CONST = 0x00, // [idx:16] Push constant from pool - OP_CONST_BYTE = 0x01, // [val:8] Push small integer (0-255) - OP_NULL = 0x02, // [] Push null - OP_TRUE = 0x03, // [] Push true - OP_FALSE = 0x04, // [] Push false - OP_ARRAY = 0x05, // [cnt:16] Pop n elements, push array - OP_OBJECT = 0x06, // [cnt:16] Pop n key-value pairs, push object - OP_STRING_INTERP = 0x07, // [cnt:16] Interpolate n parts into string - OP_CLOSURE = 0x08, // [idx:16][upvals:8] Create closure - OP_ENUM_VALUE = 0x09, // [idx:16] Push enum variant value - - // ======================================== - // Category 2: Variables (0x10-0x1F) - // ======================================== - OP_GET_LOCAL = 0x10, // [slot:8] Load local variable - OP_SET_LOCAL = 0x11, // [slot:8] Store to local - OP_GET_UPVALUE = 0x12, // [slot:8] Load captured variable - OP_SET_UPVALUE = 0x13, // [slot:8] Store to captured variable - OP_GET_GLOBAL = 0x14, // [idx:16] Load global by name - OP_SET_GLOBAL = 0x15, // [idx:16] Store to global - OP_DEFINE_GLOBAL = 0x16, // [idx:16] Define new global - OP_GET_PROPERTY = 0x17, // [idx:16] Get object property - OP_SET_PROPERTY = 0x18, // [idx:16] Set object property - OP_GET_INDEX = 0x19, // [] array[index] or object[key] - OP_SET_INDEX = 0x1A, // [] array[index] = value - OP_CLOSE_UPVALUE = 0x1B, // [] Close upvalue on stack top - - // ======================================== - // Category 3: Arithmetic (0x20-0x2F) - // ======================================== - OP_ADD = 0x20, // [] a + b (type promotion, string concat) - OP_SUB = 0x21, // [] a - b - OP_MUL = 0x22, // [] a * b - OP_DIV = 0x23, // [] a / b (always returns f64) - OP_MOD = 0x24, // [] a % b - OP_NEGATE = 0x25, // [] -a - OP_INC = 0x26, // [] ++a (in-place) - OP_DEC = 0x27, // [] --a (in-place) - OP_ADD_I32 = 0x28, // [] Fast path: i32 + i32 - OP_SUB_I32 = 0x29, // [] Fast path: i32 - i32 - OP_MUL_I32 = 0x2A, // [] Fast path: i32 * i32 - - // ======================================== - // Category 4: Comparison (0x30-0x3F) - // ======================================== - OP_EQ = 0x30, // [] a == b - OP_NE = 0x31, // [] a != b - OP_LT = 0x32, // [] a < b - OP_LE = 0x33, // [] a <= b - OP_GT = 0x34, // [] a > b - OP_GE = 0x35, // [] a >= b - OP_EQ_I32 = 0x36, // [] Fast path: i32 == i32 - OP_LT_I32 = 0x37, // [] Fast path: i32 < i32 - - // ======================================== - // Category 5: Logical & Bitwise (0x40-0x4F) - // ======================================== - OP_NOT = 0x40, // [] !a - OP_BIT_NOT = 0x41, // [] ~a - OP_BIT_AND = 0x42, // [] a & b - OP_BIT_OR = 0x43, // [] a | b - OP_BIT_XOR = 0x44, // [] a ^ b - OP_LSHIFT = 0x45, // [] a << b - OP_RSHIFT = 0x46, // [] a >> b - OP_COALESCE = 0x47, // [off:16] a ?? b (short-circuit) - OP_OPTIONAL_CHAIN = 0x48, // [off:16] a?.b (null short-circuit) - - // ======================================== - // Category 6: Control Flow (0x50-0x5F) - // ======================================== - OP_JUMP = 0x50, // [off:16] Unconditional jump - OP_JUMP_IF_FALSE = 0x51, // [off:16] Jump if top is falsy - OP_JUMP_IF_TRUE = 0x52, // [off:16] Jump if top is truthy - OP_JUMP_IF_FALSE_POP= 0x53, // [off:16] Jump if false, always pop - OP_LOOP = 0x54, // [off:16] Jump backward (loop) - OP_BREAK = 0x55, // [] Break from loop - OP_CONTINUE = 0x56, // [] Continue to next iteration - OP_SWITCH = 0x57, // [cnt:16] Jump table dispatch - OP_CASE = 0x58, // [off:16] Case label marker - OP_FOR_IN_INIT = 0x59, // [] Initialize for-in iterator - OP_FOR_IN_NEXT = 0x5A, // [off:16] Get next or jump to end - OP_POP = 0x5B, // [] Discard top of stack - OP_POPN = 0x5C, // [n:8] Discard n values from stack - - // ======================================== - // Category 7: Functions & Calls (0x60-0x6F) - // ======================================== - OP_CALL = 0x60, // [argc:8] Call function - OP_CALL_METHOD = 0x61, // [idx:16][argc:8] Call method on object - OP_CALL_BUILTIN = 0x62, // [id:16][argc:8] Call builtin function - OP_RETURN = 0x63, // [] Return from function - OP_APPLY = 0x64, // [] apply(fn, args_array) - OP_TAIL_CALL = 0x65, // [argc:8] Tail call optimization - OP_SUPER = 0x66, // [idx:16] Access super method - OP_INVOKE = 0x67, // [idx:16][argc:8] Optimized method call - - // ======================================== - // Category 8: Exception Handling (0x70-0x7F) - // ======================================== - OP_TRY = 0x70, // [catch:16][finally:16] Begin try block - OP_CATCH = 0x71, // [] Begin catch (push exception) - OP_FINALLY = 0x72, // [] Begin finally block - OP_END_TRY = 0x73, // [] End try-catch-finally - OP_THROW = 0x74, // [] Throw exception - OP_DEFER = 0x75, // [idx:16] Register deferred call - - // ======================================== - // Category 9: Async & Concurrency (0x80-0x8F) - // ======================================== - OP_SPAWN = 0x80, // [argc:8] Spawn async task - OP_AWAIT = 0x81, // [] Await task result - OP_JOIN = 0x82, // [] Join task (explicit) - OP_DETACH = 0x83, // [] Detach task - OP_CHANNEL = 0x84, // [] Create channel (capacity on stack) - OP_SEND = 0x85, // [] Send on channel - OP_RECV = 0x86, // [] Receive from channel - OP_SELECT = 0x87, // [] Select on multiple channels - - // ======================================== - // Category 10: Type Operations (0x90-0x9F) - // ======================================== - OP_TYPEOF = 0x90, // [] Get type string - OP_CAST = 0x91, // [type:8] Explicit type cast - OP_CHECK_TYPE = 0x92, // [type:8] Runtime type check - OP_DEFINE_TYPE = 0x93, // [idx:16] Register type definition - OP_DEFINE_ENUM = 0x94, // [idx:16] Register enum definition - - // ======================================== - // Category 11: Debug & Misc (0xF0-0xFF) - // ======================================== - OP_NOP = 0xF0, // [] No operation - OP_PRINT = 0xF1, // [argc:8] Print values - OP_ASSERT = 0xF2, // [] Assert with optional message - OP_DEBUG_BREAK = 0xFE, // [] Debugger breakpoint - OP_HALT = 0xFF, // [] Stop execution - -} OpCode; - -// Instruction metadata for disassembly and verification -typedef struct { - const char *name; // Human-readable name - int operand_bytes; // Number of operand bytes (0, 1, 2, 3, 4) - int stack_effect; // Net stack change (-128 = variable) -} InstructionInfo; - -// Get instruction info by opcode -const InstructionInfo* instruction_info(OpCode op); - -// Operand encoding helpers -#define READ_BYTE(ip) (*(ip)++) -#define READ_SHORT(ip) ((ip) += 2, (uint16_t)((ip)[-2] << 8 | (ip)[-1])) -#define READ_SIGNED_SHORT(ip) ((int16_t)READ_SHORT(ip)) - -// Instruction size helpers -#define INSTR_SIZE_0 1 // opcode only -#define INSTR_SIZE_1 2 // opcode + 1 byte operand -#define INSTR_SIZE_2 3 // opcode + 2 byte operand -#define INSTR_SIZE_3 4 // opcode + 3 byte operand -#define INSTR_SIZE_4 5 // opcode + 4 byte operand - -// Stack effect constants -#define STACK_EFFECT_VARIABLE -128 - -// Builtin function IDs (for OP_CALL_BUILTIN) +// Builtin function IDs (for BC_CALL_BUILTIN) typedef enum { // Memory (0-10) BUILTIN_ALLOC = 0, @@ -289,7 +125,7 @@ typedef enum { BUILTIN_COUNT // Total number of builtins } BuiltinId; -// Type IDs for OP_CAST and OP_CHECK_TYPE +// Type IDs for BC_CAST and BC_CHECK_TYPE typedef enum { TYPE_ID_I8 = 0, TYPE_ID_I16, @@ -316,4 +152,184 @@ typedef enum { TYPE_ID_ENUM, } TypeId; +// Bytecode opcodes (BC_ prefix to avoid conflicts with AST enums) +typedef enum { + // ======================================== + // Category 1: Constants & Literals (0x00-0x0F) + // ======================================== + BC_CONST = 0x00, // [idx:16] Push constant from pool + BC_CONST_BYTE = 0x01, // [val:8] Push small integer (0-255) + BC_NULL = 0x02, // [] Push null + BC_TRUE = 0x03, // [] Push true + BC_FALSE = 0x04, // [] Push false + BC_ARRAY = 0x05, // [cnt:16] Pop n elements, push array + BC_OBJECT = 0x06, // [cnt:16] Pop n key-value pairs, push object + BC_STRING_INTERP = 0x07, // [cnt:16] Interpolate n parts into string + BC_CLOSURE = 0x08, // [idx:16][upvals:8] Create closure + BC_ENUM_VALUE = 0x09, // [idx:16] Push enum variant value + + // ======================================== + // Category 2: Variables (0x10-0x1F) + // ======================================== + BC_GET_LOCAL = 0x10, // [slot:8] Load local variable + BC_SET_LOCAL = 0x11, // [slot:8] Store to local + BC_GET_UPVALUE = 0x12, // [slot:8] Load captured variable + BC_SET_UPVALUE = 0x13, // [slot:8] Store to captured variable + BC_GET_GLOBAL = 0x14, // [idx:16] Load global by name + BC_SET_GLOBAL = 0x15, // [idx:16] Store to global + BC_DEFINE_GLOBAL = 0x16, // [idx:16] Define new global + BC_GET_PROPERTY = 0x17, // [idx:16] Get object property + BC_SET_PROPERTY = 0x18, // [idx:16] Set object property + BC_GET_INDEX = 0x19, // [] array[index] or object[key] + BC_SET_INDEX = 0x1A, // [] array[index] = value + BC_CLOSE_UPVALUE = 0x1B, // [] Close upvalue on stack top + + // ======================================== + // Category 3: Arithmetic (0x20-0x2F) + // ======================================== + BC_ADD = 0x20, // [] a + b (type promotion, string concat) + BC_SUB = 0x21, // [] a - b + BC_MUL = 0x22, // [] a * b + BC_DIV = 0x23, // [] a / b (always returns f64) + BC_MOD = 0x24, // [] a % b + BC_NEGATE = 0x25, // [] -a + BC_INC = 0x26, // [] ++a (in-place) + BC_DEC = 0x27, // [] --a (in-place) + BC_ADD_I32 = 0x28, // [] Fast path: i32 + i32 + BC_SUB_I32 = 0x29, // [] Fast path: i32 - i32 + BC_MUL_I32 = 0x2A, // [] Fast path: i32 * i32 + + // ======================================== + // Category 4: Comparison (0x30-0x3F) + // ======================================== + BC_EQ = 0x30, // [] a == b + BC_NE = 0x31, // [] a != b + BC_LT = 0x32, // [] a < b + BC_LE = 0x33, // [] a <= b + BC_GT = 0x34, // [] a > b + BC_GE = 0x35, // [] a >= b + BC_EQ_I32 = 0x36, // [] Fast path: i32 == i32 + BC_LT_I32 = 0x37, // [] Fast path: i32 < i32 + + // ======================================== + // Category 5: Logical & Bitwise (0x40-0x4F) + // ======================================== + BC_NOT = 0x40, // [] !a + BC_BIT_NOT = 0x41, // [] ~a + BC_BIT_AND = 0x42, // [] a & b + BC_BIT_OR = 0x43, // [] a | b + BC_BIT_XOR = 0x44, // [] a ^ b + BC_LSHIFT = 0x45, // [] a << b + BC_RSHIFT = 0x46, // [] a >> b + BC_COALESCE = 0x47, // [off:16] a ?? b (short-circuit) + BC_OPTIONAL_CHAIN = 0x48, // [off:16] a?.b (null short-circuit) + + // ======================================== + // Category 6: Control Flow (0x50-0x5F) + // ======================================== + BC_JUMP = 0x50, // [off:16] Unconditional jump + BC_JUMP_IF_FALSE = 0x51, // [off:16] Jump if top is falsy + BC_JUMP_IF_TRUE = 0x52, // [off:16] Jump if top is truthy + BC_JUMP_IF_FALSE_POP= 0x53, // [off:16] Jump if false, always pop + BC_LOOP = 0x54, // [off:16] Jump backward (loop) + BC_BREAK = 0x55, // [] Break from loop + BC_CONTINUE = 0x56, // [] Continue to next iteration + BC_SWITCH = 0x57, // [cnt:16] Jump table dispatch + BC_CASE = 0x58, // [off:16] Case label marker + BC_FOR_IN_INIT = 0x59, // [] Initialize for-in iterator + BC_FOR_IN_NEXT = 0x5A, // [off:16] Get next or jump to end + BC_POP = 0x5B, // [] Discard top of stack + BC_POPN = 0x5C, // [n:8] Discard n values from stack + + // ======================================== + // Category 7: Functions & Calls (0x60-0x6F) + // ======================================== + BC_CALL = 0x60, // [argc:8] Call function + BC_CALL_METHOD = 0x61, // [idx:16][argc:8] Call method on object + BC_CALL_BUILTIN = 0x62, // [id:16][argc:8] Call builtin function + BC_RETURN = 0x63, // [] Return from function + BC_APPLY = 0x64, // [] apply(fn, args_array) + BC_TAIL_CALL = 0x65, // [argc:8] Tail call optimization + BC_SUPER = 0x66, // [idx:16] Access super method + BC_INVOKE = 0x67, // [idx:16][argc:8] Optimized method call + + // ======================================== + // Category 8: Exception Handling (0x70-0x7F) + // ======================================== + BC_TRY = 0x70, // [catch:16][finally:16] Begin try block + BC_CATCH = 0x71, // [] Begin catch (push exception) + BC_FINALLY = 0x72, // [] Begin finally block + BC_END_TRY = 0x73, // [] End try-catch-finally + BC_THROW = 0x74, // [] Throw exception + BC_DEFER = 0x75, // [idx:16] Register deferred call + + // ======================================== + // Category 9: Async & Concurrency (0x80-0x8F) + // ======================================== + BC_SPAWN = 0x80, // [argc:8] Spawn async task + BC_AWAIT = 0x81, // [] Await task result + BC_JOIN = 0x82, // [] Join task (explicit) + BC_DETACH = 0x83, // [] Detach task + BC_CHANNEL = 0x84, // [] Create channel (capacity on stack) + BC_SEND = 0x85, // [] Send on channel + BC_RECV = 0x86, // [] Receive from channel + BC_SELECT = 0x87, // [] Select on multiple channels + + // ======================================== + // Category 10: Type Operations (0x90-0x9F) + // ======================================== + BC_TYPEOF = 0x90, // [] Get type string + BC_CAST = 0x91, // [type:8] Explicit type cast + BC_CHECK_TYPE = 0x92, // [type:8] Runtime type check + BC_DEFINE_TYPE = 0x93, // [idx:16] Register type definition + BC_DEFINE_ENUM = 0x94, // [idx:16] Register enum definition + + // ======================================== + // Category 11: Debug & Misc (0xF0-0xFF) + // ======================================== + BC_NOP = 0xF0, // [] No operation + BC_PRINT = 0xF1, // [argc:8] Print values + BC_ASSERT = 0xF2, // [] Assert with optional message + BC_DEBUG_BREAK = 0xFE, // [] Debugger breakpoint + BC_HALT = 0xFF, // [] Stop execution + +} OpCode; + +// Instruction metadata for disassembly and verification +typedef struct { + const char *name; // Human-readable name + int operand_bytes; // Number of operand bytes (0, 1, 2, 3, 4) + int stack_effect; // Net stack change (-128 = variable) +} InstructionInfo; + +// Get instruction info by opcode +const InstructionInfo* instruction_info(OpCode op); + +// Get instruction size in bytes +int instruction_size(OpCode op); + +// Get builtin function name +const char* builtin_name(BuiltinId id); + +// Lookup builtin ID by name (-1 if not found) +BuiltinId builtin_lookup(const char *name); + +// Get type name by ID +const char* type_id_name(TypeId id); + +// Operand encoding helpers +#define READ_BYTE(ip) (*(ip)++) +#define READ_SHORT(ip) ((ip) += 2, (uint16_t)((ip)[-2] << 8 | (ip)[-1])) +#define READ_SIGNED_SHORT(ip) ((int16_t)READ_SHORT(ip)) + +// Instruction size helpers +#define INSTR_SIZE_0 1 // opcode only +#define INSTR_SIZE_1 2 // opcode + 1 byte operand +#define INSTR_SIZE_2 3 // opcode + 2 byte operand +#define INSTR_SIZE_3 4 // opcode + 3 byte operand +#define INSTR_SIZE_4 5 // opcode + 4 byte operand + +// Stack effect constants +#define STACK_EFFECT_VARIABLE -128 + #endif // HEMLOCK_VM_INSTRUCTION_H diff --git a/src/backends/vm/main.c b/src/backends/vm/main.c new file mode 100644 index 00000000..f00d4033 --- /dev/null +++ b/src/backends/vm/main.c @@ -0,0 +1,231 @@ +/* + * Hemlock Bytecode VM - Main Entry Point + * + * hemlockvm - Bytecode VM interpreter for Hemlock + */ + +#include +#include +#include +#include "vm.h" +#include "compiler.h" +#include "debug.h" +#include "lexer.h" +#include "parser.h" +#include "ast.h" +#include "resolver.h" +#include "optimizer.h" +#include "version.h" + +// Read entire file into a string +static char* read_file(const char *path) { + FILE *file = fopen(path, "rb"); + if (file == NULL) { + fprintf(stderr, "Error: Could not open file '%s'\n", path); + return NULL; + } + + fseek(file, 0, SEEK_END); + long size = ftell(file); + rewind(file); + + char *buffer = malloc(size + 1); + if (buffer == NULL) { + fprintf(stderr, "Error: Could not allocate memory for file\n"); + fclose(file); + return NULL; + } + + size_t bytes_read = fread(buffer, 1, size, file); + if (bytes_read < (size_t)size) { + fprintf(stderr, "Error: Could not read file\n"); + free(buffer); + fclose(file); + return NULL; + } + + buffer[size] = '\0'; + fclose(file); + return buffer; +} + +// Run source code through the bytecode VM +static int run_source(const char *source, bool disassemble, bool trace) { + // Parse + Lexer lexer; + lexer_init(&lexer, source); + + Parser parser; + parser_init(&parser, &lexer); + + int stmt_count; + Stmt **statements = parse_program(&parser, &stmt_count); + + if (parser.had_error) { + fprintf(stderr, "Parse failed!\n"); + return 1; + } + + // Resolve variables + resolve_program(statements, stmt_count); + + // Optimize AST + optimize_program(statements, stmt_count); + + // Compile to bytecode + Chunk *chunk = compile_program(statements, stmt_count); + if (!chunk) { + fprintf(stderr, "Compilation failed!\n"); + return 1; + } + + // Disassemble if requested + if (disassemble) { + disassemble_chunk(chunk, "script"); + chunk_free(chunk); + return 0; + } + + // Execute + VM *vm = vm_new(); + vm_trace_execution(vm, trace); + + VMResult result = vm_run(vm, chunk); + + vm_free(vm); + chunk_free(chunk); + + // Free AST + for (int i = 0; i < stmt_count; i++) { + stmt_free(statements[i]); + } + free(statements); + + return (result == VM_OK) ? 0 : 1; +} + +// Run file +static int run_file(const char *path, bool disassemble, bool trace) { + char *source = read_file(path); + if (!source) { + return 1; + } + + int result = run_source(source, disassemble, trace); + free(source); + return result; +} + +// REPL +static void run_repl(void) { + printf("Hemlock Bytecode VM %s\n", HEMLOCK_VERSION); + printf("Type 'exit' to quit.\n\n"); + + VM *vm = vm_new(); + char line[1024]; + + while (1) { + printf(">>> "); + fflush(stdout); + + if (!fgets(line, sizeof(line), stdin)) { + printf("\n"); + break; + } + + // Remove trailing newline + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\n') { + line[len - 1] = '\0'; + } + + // Check for exit + if (strcmp(line, "exit") == 0 || strcmp(line, "quit") == 0) { + break; + } + + // Skip empty lines + if (strlen(line) == 0) { + continue; + } + + // Parse and run + Lexer lexer; + lexer_init(&lexer, line); + + Parser parser; + parser_init(&parser, &lexer); + + int stmt_count; + Stmt **statements = parse_program(&parser, &stmt_count); + + if (!parser.had_error && stmt_count > 0) { + resolve_program(statements, stmt_count); + optimize_program(statements, stmt_count); + + Chunk *chunk = compile_program(statements, stmt_count); + if (chunk) { + vm_reset(vm); + vm_run(vm, chunk); + chunk_free(chunk); + } + } + + // Free AST + for (int i = 0; i < stmt_count; i++) { + stmt_free(statements[i]); + } + free(statements); + } + + vm_free(vm); +} + +static void print_usage(const char *program) { + fprintf(stderr, "Usage: %s [options] [file.hml]\n", program); + fprintf(stderr, "\nOptions:\n"); + fprintf(stderr, " --disasm, -d Disassemble bytecode instead of running\n"); + fprintf(stderr, " --trace, -t Trace execution (debug)\n"); + fprintf(stderr, " --version, -v Show version information\n"); + fprintf(stderr, " --help, -h Show this help message\n"); + fprintf(stderr, "\nIf no file is provided, starts an interactive REPL.\n"); +} + +static void print_version(void) { + printf("Hemlock Bytecode VM %s\n", HEMLOCK_VERSION); + printf("Build date: %s %s\n", __DATE__, __TIME__); +} + +int main(int argc, char **argv) { + const char *file = NULL; + bool disassemble = false; + bool trace = false; + + // Parse arguments + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { + print_usage(argv[0]); + return 0; + } else if (strcmp(argv[i], "--version") == 0 || strcmp(argv[i], "-v") == 0) { + print_version(); + return 0; + } else if (strcmp(argv[i], "--disasm") == 0 || strcmp(argv[i], "-d") == 0) { + disassemble = true; + } else if (strcmp(argv[i], "--trace") == 0 || strcmp(argv[i], "-t") == 0) { + trace = true; + } else if (argv[i][0] != '-') { + file = argv[i]; + } else { + fprintf(stderr, "Unknown option: %s\n", argv[i]); + print_usage(argv[0]); + return 1; + } + } + + if (file) { + return run_file(file, disassemble, trace); + } else { + run_repl(); + return 0; + } +} diff --git a/src/backends/vm/vm.c b/src/backends/vm/vm.c new file mode 100644 index 00000000..ae8dc5db --- /dev/null +++ b/src/backends/vm/vm.c @@ -0,0 +1,1090 @@ +/* + * Hemlock Bytecode VM - Virtual Machine Implementation + * + * Stack-based bytecode interpreter with computed goto dispatch. + */ + +#define _POSIX_C_SOURCE 200809L + +#include "vm.h" +#include "debug.h" +#include +#include +#include +#include +#include + +// Debug tracing +static int vm_trace_enabled = 0; + +// ============================================ +// Value Helpers (matching interpreter semantics) +// ============================================ + +static Value vm_null_value(void) { + Value v = {.type = VAL_NULL}; + return v; +} + +static Value val_bool_vm(int b) { + Value v = {.type = VAL_BOOL, .as.as_bool = b}; + return v; +} + +static Value val_i32_vm(int32_t i) { + Value v = {.type = VAL_I32, .as.as_i32 = i}; + return v; +} + +static Value val_i64_vm(int64_t i) { + Value v = {.type = VAL_I64, .as.as_i64 = i}; + return v; +} + +static Value val_f64_vm(double f) { + Value v = {.type = VAL_F64, .as.as_f64 = f}; + return v; +} + +static int value_is_truthy(Value v) { + switch (v.type) { + case VAL_NULL: return 0; + case VAL_BOOL: return v.as.as_bool != 0; + case VAL_I32: return v.as.as_i32 != 0; + case VAL_I64: return v.as.as_i64 != 0; + case VAL_F64: return v.as.as_f64 != 0.0; + case VAL_STRING: return v.as.as_string && v.as.as_string->length > 0; + case VAL_ARRAY: return v.as.as_array && v.as.as_array->length > 0; + default: return 1; // Non-null objects are truthy + } +} + +// Convert value to double for arithmetic +static double value_to_f64(Value v) { + switch (v.type) { + case VAL_I8: return (double)v.as.as_i8; + case VAL_I16: return (double)v.as.as_i16; + case VAL_I32: return (double)v.as.as_i32; + case VAL_I64: return (double)v.as.as_i64; + case VAL_U8: return (double)v.as.as_u8; + case VAL_U16: return (double)v.as.as_u16; + case VAL_U32: return (double)v.as.as_u32; + case VAL_U64: return (double)v.as.as_u64; + case VAL_F32: return (double)v.as.as_f32; + case VAL_F64: return v.as.as_f64; + default: return 0.0; + } +} + +// Convert value to i64 for integer ops +static int64_t value_to_i64(Value v) { + switch (v.type) { + case VAL_I8: return (int64_t)v.as.as_i8; + case VAL_I16: return (int64_t)v.as.as_i16; + case VAL_I32: return (int64_t)v.as.as_i32; + case VAL_I64: return v.as.as_i64; + case VAL_U8: return (int64_t)v.as.as_u8; + case VAL_U16: return (int64_t)v.as.as_u16; + case VAL_U32: return (int64_t)v.as.as_u32; + case VAL_U64: return (int64_t)v.as.as_u64; + case VAL_F32: return (int64_t)v.as.as_f32; + case VAL_F64: return (int64_t)v.as.as_f64; + default: return 0; + } +} + +// Check if value is a numeric type +static int is_numeric(ValueType t) { + return t >= VAL_I8 && t <= VAL_F64; +} + +// Check if value is a float type +static int is_float(ValueType t) { + return t == VAL_F32 || t == VAL_F64; +} + +// Convert ValueType to a string name +static const char* val_type_name(ValueType t) { + switch (t) { + case VAL_NULL: return "null"; + case VAL_BOOL: return "bool"; + case VAL_I8: return "i8"; + case VAL_I16: return "i16"; + case VAL_I32: return "i32"; + case VAL_I64: return "i64"; + case VAL_U8: return "u8"; + case VAL_U16: return "u16"; + case VAL_U32: return "u32"; + case VAL_U64: return "u64"; + case VAL_F32: return "f32"; + case VAL_F64: return "f64"; + case VAL_STRING: return "string"; + case VAL_RUNE: return "rune"; + case VAL_PTR: return "ptr"; + case VAL_BUFFER: return "buffer"; + case VAL_ARRAY: return "array"; + case VAL_OBJECT: return "object"; + case VAL_FILE: return "file"; + case VAL_FUNCTION: return "function"; + case VAL_TASK: return "task"; + case VAL_CHANNEL: return "channel"; + default: return "unknown"; + } +} + +// ============================================ +// VM Lifecycle +// ============================================ + +VM* vm_new(void) { + VM *vm = malloc(sizeof(VM)); + if (!vm) return NULL; + + // Initialize stack + vm->stack = malloc(sizeof(Value) * VM_STACK_INITIAL); + vm->stack_top = vm->stack; + vm->stack_capacity = VM_STACK_INITIAL; + + // Initialize call frames + vm->frames = malloc(sizeof(CallFrame) * VM_FRAMES_INITIAL); + vm->frame_count = 0; + vm->frame_capacity = VM_FRAMES_INITIAL; + + // Initialize globals + vm->globals.names = malloc(sizeof(char*) * VM_GLOBALS_INITIAL); + vm->globals.values = malloc(sizeof(Value) * VM_GLOBALS_INITIAL); + vm->globals.is_const = malloc(sizeof(bool) * VM_GLOBALS_INITIAL); + vm->globals.count = 0; + vm->globals.capacity = VM_GLOBALS_INITIAL; + vm->globals.hash_table = NULL; + vm->globals.hash_capacity = 0; + + // Control flow state + vm->is_returning = false; + vm->return_value = vm_null_value(); + vm->is_throwing = false; + vm->exception = vm_null_value(); + vm->exception_frame = NULL; + vm->is_breaking = false; + vm->is_continuing = false; + + // Defers + vm->defers = malloc(sizeof(DeferEntry) * VM_DEFER_INITIAL); + vm->defer_count = 0; + vm->defer_capacity = VM_DEFER_INITIAL; + + // Module cache + vm->module_cache.paths = NULL; + vm->module_cache.modules = NULL; + vm->module_cache.count = 0; + vm->module_cache.capacity = 0; + + // GC/memory + vm->open_upvalues = NULL; + vm->objects = NULL; + vm->bytes_allocated = 0; + vm->next_gc = 1024 * 1024; // 1MB + + vm->max_stack_depth = 1024; + vm->task = NULL; + + return vm; +} + +void vm_free(VM *vm) { + if (!vm) return; + + free(vm->stack); + free(vm->frames); + + // Free globals + for (int i = 0; i < vm->globals.count; i++) { + free(vm->globals.names[i]); + // TODO: VALUE_RELEASE(vm->globals.values[i]); + } + free(vm->globals.names); + free(vm->globals.values); + free(vm->globals.is_const); + free(vm->globals.hash_table); + + free(vm->defers); + + // Free module cache + for (int i = 0; i < vm->module_cache.count; i++) { + free(vm->module_cache.paths[i]); + } + free(vm->module_cache.paths); + free(vm->module_cache.modules); + + // TODO: Free all allocated objects + + free(vm); +} + +void vm_reset(VM *vm) { + vm->stack_top = vm->stack; + vm->frame_count = 0; + vm->is_returning = false; + vm->is_throwing = false; + vm->is_breaking = false; + vm->is_continuing = false; + vm->defer_count = 0; +} + +// ============================================ +// Stack Operations +// ============================================ + +void vm_push(VM *vm, Value value) { + if (vm->stack_top - vm->stack >= vm->stack_capacity) { + // Grow stack + int old_top = vm->stack_top - vm->stack; + vm->stack_capacity *= 2; + vm->stack = realloc(vm->stack, sizeof(Value) * vm->stack_capacity); + vm->stack_top = vm->stack + old_top; + } + *vm->stack_top++ = value; +} + +Value vm_pop(VM *vm) { + if (vm->stack_top <= vm->stack) { + vm_runtime_error(vm, "Stack underflow"); + return vm_null_value(); + } + return *--vm->stack_top; +} + +Value vm_peek(VM *vm, int distance) { + return vm->stack_top[-1 - distance]; +} + +void vm_popn(VM *vm, int count) { + vm->stack_top -= count; + if (vm->stack_top < vm->stack) { + vm->stack_top = vm->stack; + vm_runtime_error(vm, "Stack underflow"); + } +} + +// ============================================ +// Globals +// ============================================ + +static uint32_t hash_string_vm(const char *str) { + uint32_t hash = 2166136261u; + while (*str) { + hash ^= (uint8_t)*str++; + hash *= 16777619; + } + return hash; +} + +void vm_define_global(VM *vm, const char *name, Value value, bool is_const) { + // Check for existing + for (int i = 0; i < vm->globals.count; i++) { + if (strcmp(vm->globals.names[i], name) == 0) { + vm->globals.values[i] = value; + return; + } + } + + // Add new + if (vm->globals.count >= vm->globals.capacity) { + vm->globals.capacity *= 2; + vm->globals.names = realloc(vm->globals.names, sizeof(char*) * vm->globals.capacity); + vm->globals.values = realloc(vm->globals.values, sizeof(Value) * vm->globals.capacity); + vm->globals.is_const = realloc(vm->globals.is_const, sizeof(bool) * vm->globals.capacity); + } + + vm->globals.names[vm->globals.count] = strdup(name); + vm->globals.values[vm->globals.count] = value; + vm->globals.is_const[vm->globals.count] = is_const; + vm->globals.count++; +} + +bool vm_get_global(VM *vm, const char *name, Value *out) { + for (int i = 0; i < vm->globals.count; i++) { + if (strcmp(vm->globals.names[i], name) == 0) { + *out = vm->globals.values[i]; + return true; + } + } + return false; +} + +bool vm_set_global(VM *vm, const char *name, Value value) { + for (int i = 0; i < vm->globals.count; i++) { + if (strcmp(vm->globals.names[i], name) == 0) { + if (vm->globals.is_const[i]) { + vm_runtime_error(vm, "Cannot reassign constant '%s'", name); + return false; + } + vm->globals.values[i] = value; + return true; + } + } + vm_runtime_error(vm, "Undefined variable '%s'", name); + return false; +} + +// ============================================ +// Error Handling +// ============================================ + +void vm_runtime_error(VM *vm, const char *format, ...) { + va_list args; + va_start(args, format); + fprintf(stderr, "Runtime error: "); + vfprintf(stderr, format, args); + fprintf(stderr, "\n"); + va_end(args); + + // Print stack trace + vm_print_stack_trace(vm); + + vm->is_throwing = true; +} + +int vm_current_line(VM *vm) { + if (vm->frame_count == 0) return 0; + CallFrame *frame = &vm->frames[vm->frame_count - 1]; + int offset = frame->ip - frame->chunk->code; + return chunk_get_line(frame->chunk, offset); +} + +void vm_print_stack_trace(VM *vm) { + for (int i = vm->frame_count - 1; i >= 0; i--) { + CallFrame *frame = &vm->frames[i]; + int offset = frame->ip - frame->chunk->code; + int line = chunk_get_line(frame->chunk, offset); + fprintf(stderr, " at %s:%d\n", + frame->chunk->name ? frame->chunk->name : "