To compile a highly dynamic, inheritance-heavy language like LPC into a
strict, fixed-width 32-bit Register Machine, the Compiler must act as a heavy
abstraction layer. The VM execution core is deliberately dumb; it knows
nothing about "inheritance," "classes," or "closures." It only knows about
registers, memory offsets, and Syscalls.
The Compilation Pipeline bridges this gap. Here is the architectural
specification for Phase 4.
1. The JSON-Serializable AST Structure
Because our MVP compiler is written in JavaScript (and to support your
ast-xplorer.js visualization tool), the Lexer and Parser emit a strict,
JSON-serializable Abstract Syntax Tree.
Every node contains a type string, allowing for recursive descent generation.
Example AST for write("Hello");
{
"type": "Program",
"body": [
{
"type": "FunctionDeclaration",
"name": "main",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": { "type": "Identifier", "name": "write" },
"arguments": [
{ "type": "Literal", "value": "Hello", "datatype": "string" }
]
}
}
]
}
]
}
During Semantic Analysis, the compiler walks this tree, registers "Hello" in
the Blueprint's Constant Pool, and replaces the CallExpression with a SYSCALL
instruction pointing to the IVT ID for write.
2. Variable Scope & Resolution Mapping
LPC has two primary scopes: Local (inside a function) and Global
(object-wide). The compiler maps these to entirely different bytecode
mechanisms.
* Local Variables: Mapped to the Shadow Stack.
* The compiler assigns local variables to specific Saved Registers (R3-R7).
* If a function has more than 5 local variables, the compiler uses a
LOAD_LOCAL <offset> opcode to pull them directly from the Shadow Stack memory
block relative to the SP.
* Global Variables: Mapped to the Master Object Index (MOI).
* The compiler converts all global variable names into fixed integer
offsets (e.g., hit_points becomes Index 0, max_hp becomes Index 1).
* Accessing them emits LOAD_VAR <index> or STORE_VAR <index>. The VM
resolves this at runtime using the state_offset pointer in the MOI.
3. Complex Data Structures (Mappings & Arrays)
LPC syntax allows for deep mapping interactions: player["stats"]["strength"]
+= 5;.
Because our VM registers only hold Tagged Pointers, the compiler translates
this high-level syntax into a sequence of Host Syscalls.
The compiler expands map["key"] = val into:
* LOAD_VAR R1, <map_index> (Load the Mapping Handle into Arg 1)
* LOAD_CONST R2, <key_index> (Load the String Handle into Arg 2)
* LOAD_VAR R3, <val_index> (Load the Value Handle into Arg 3)
* SYSCALL R0, SYS_MAP_SET (Yield to the Host to perform the memory operation)
This keeps the VM core completely decoupled from the memory complexities of
hash map resizing and collision resolution.
4. Closures & Function Pointers
LPC heavily uses closures (e.g., (: do_damage, 50 :)).
A closure is a function pointer bound to an execution environment (an Object).
* The Handle: The compiler treats closures as just another dynamic data type.
It assigns them Tag 101 (Closure Handle).
* The Ledger Node: When a closure is created, the bytecode emits SYSCALL
SYS_CLOSURE_ALLOC. The Host creates a node in the Global Handle Ledger that
stores:
* The Object ID (OID) of the target.
* The 32-bit Hash of the function name.
* An Array Handle for any pre-bound arguments.
* Execution: When the bytecode evaluates a closure, it emits ECALL_CLOSURE
[Handle_Reg]. The VM looks up the Ledger Node, resolves the OID, pushes the
bound arguments to the Shadow Stack, and jumps to the function hash.
5. Inheritance (The Blueprint Flattener)
This is the most critical compiler feature. LPC allows multiple inheritance,
but our Blueprint VM has a single, flat code segment and a single VTable.
When monster.c inherits npc.c:
* VTable Merging: The compiler pulls the VTable from npc.c and merges it into
monster.c. If monster.c overrides a function (e.g., die()), the VTable Hash
for die is updated to point to the new instruction offset in monster.c.
* State Flattening: The compiler reads npc.c's global variables (e.g., 5
variables) and assigns them indices 0 through 4. It then takes monster.c's
variables and assigns them indices 5 onwards.
* The Result: The final compiled Blueprint for monster.c contains all the
logic of npc.c natively embedded.
To the execution engine, inheritance does not exist. It just sees a flat
Blueprint with a large VTable and a block of variables.
The Master Architecture is Complete
We have now designed the entire HAUKI-X86 LPC Virtual Machine from the ground
up:
* The Execution Core: 32-bit Register VM, Tagged Pointers, Shadow Stack.
* The Memory Model: Global Handle Ledger, Master Object Index.
* The Concurrency Model: Yield-based Context Switching, I/O Wait Tables.
* The Compilation Pipeline: AST Generation, State Flattening, Closure
Handles.
You have a complete, cohesive blueprint for a hyper-modern, crash-proof,
perfectly isomorphic systems language engine.