Bytecode Generation
Bytecode generation is where an AST feature becomes executable semantics.
For module authors, the most important rule is stack and ownership discipline: emit only the operations your feature owns, in the order expected by the backend pipeline.
When to read this page
Read this page after AST Nodes when a feature already has parser/AST ownership and now needs runtime behavior.
Goal
Understand how AST visitors lower feature-owned nodes into bytecode without leaking syntax assumptions into later stages.
Where bytecode is produced
Frontend modules usually register AST visitors through InitAstTranslator:
public void InitAstTranslator(IAstToBytecodeTranslator translator) =>
translator.AddVisitors(...);A visitor receives a node and decides whether it owns that node. If it does, it translates children and emits bytecode instructions.
A simplified lowering path looks like this:
AST node
-> owning visitor
-> translate child nodes
-> emit feature-owned bytecode instructions
-> bytecode post-processing
-> AIR/backend pathExample: arithmetic lowering
Arithmetic lowering follows a simple shape:
- Check whether the AST node is an arithmetic operation.
- Translate child nodes first.
- Emit the arithmetic operation instruction.
Conceptually:
left operand
right operand
operationThis stack-oriented order matters. If children are translated in the wrong order, or if the operation is emitted too early, interpreter and compiler behavior can diverge.
Visitor self-filtering
A bytecode visitor must return without emitting when it does not own the node:
if (!IsOwnedNode(data.Node))
return;This is not optional. Translators may invoke multiple visitors, and a visitor that emits for unrelated nodes can create duplicate or invalid bytecode.
Stack discipline
Bytecode generation should respect the expected stack behavior of the feature.
For expression-like features:
- child expressions usually push values;
- the parent operation consumes those values;
- the parent operation pushes the result or produces a documented control-flow effect.
For statement-like or control-flow features:
- define what values remain on the stack;
- document whether a body result is preserved or discarded;
- test both interpreter and compiler paths.
Never rely on accidental stack cleanup by a later backend.
Bytecode post-processing
IFrontendCoreModule includes a ProcessBytecode hook:
Bytecode ProcessBytecode(Bytecode current) => current;Use this only when the module genuinely owns a bytecode-level transformation. Do not use post-processing as a workaround for missing parser or AST ownership.
Good use:
- normalize feature-owned bytecode;
- validate feature-owned instruction shape;
- apply a documented transformation with tests.
Bad use:
- scan instructions to guess missing syntax;
- rewrite unrelated module instructions;
- hide backend incompatibilities;
- depend on previous compilations or global mutable state.
Backend awareness
Bytecode is consumed by backend paths. A feature is not complete until intended backends understand the emitted semantics.
When both compiler and interpreter are enabled, add parity tests:
same source
-> interpreter result
-> compiler result
-> compare observable behaviorIf a feature is intentionally supported by only one backend, document that limitation and test that unsupported modes fail explicitly.
Optimizers are not semantics
Optimizers may improve or normalize bytecode/AIR, but they must not be required to make basic semantics correct.
Build in this order:
- parser/AST ownership;
- unoptimized bytecode semantics;
- backend execution;
- parity tests;
- optimizers.
If a program works only after an optimizer is enabled, the base feature is probably incomplete.
What to test
For bytecode generation, test:
- simple valid source;
- nested source;
- malformed source;
- disabled dialect behavior;
- interpreter execution when supported;
- compiler execution when supported;
- parity when both backends are supported;
- optimizer safety when optimizers touch the feature.
Common mistakes
- Emitting bytecode for nodes not owned by the visitor.
- Depending on child order without a test.
- Leaving unexpected values on the stack.
- Fixing parser mistakes in bytecode post-processing.
- Adding compiler behavior but forgetting interpreter behavior.
- Using optimizers as required semantic lowering.
Next
Continue with Semantic Tags.