Compare commits

..

No commits in common. "b2a6021fb8a3fcdd69de36a01438a55bc26de672" and "5ade60827818eb3bd2cfc3b1a5274c06f8ee1567" have entirely different histories.

5 changed files with 68 additions and 81 deletions

View File

@ -55,7 +55,7 @@ No build step required - Bun runs TypeScript directly.
### Critical Design Decisions ### Critical Design Decisions
**Label-based jumps**: All JUMP instructions (`JUMP`, `JUMP_IF_FALSE`, `JUMP_IF_TRUE`) require label operands (`.label`), not numeric offsets. Labels are resolved to PC-relative offsets during compilation, making bytecode position-independent. PUSH_TRY/PUSH_FINALLY use absolute addresses and can accept either labels or numeric offsets. **Relative jumps**: All JUMP instructions use PC-relative offsets (not absolute addresses), making bytecode position-independent. PUSH_TRY/PUSH_FINALLY use absolute addresses.
**Truthiness semantics**: Only `null` and `false` are falsy. Unlike JavaScript, `0`, `""`, empty arrays, and empty dicts are truthy. **Truthiness semantics**: Only `null` and `false` are falsy. Unlike JavaScript, `0`, `""`, empty arrays, and empty dicts are truthy.
@ -229,8 +229,8 @@ await vm.call('log', 'Hello!')
- Automatically converts arguments to ReefVM Values - Automatically converts arguments to ReefVM Values
- Converts result back to JavaScript types - Converts result back to JavaScript types
### Label Usage (Required for JUMP instructions) ### Label Usage (Preferred)
All JUMP instructions must use labels: Use labels instead of numeric offsets for readability:
``` ```
JUMP .skip JUMP .skip
PUSH 42 PUSH 42
@ -486,7 +486,7 @@ Run `bun test` to verify all tests pass before committing.
## Common Gotchas ## Common Gotchas
**Label requirements**: JUMP/JUMP_IF_FALSE/JUMP_IF_TRUE require label operands (`.label`), not numeric offsets. The bytecode compiler resolves labels to PC-relative offsets internally. PUSH_TRY/PUSH_FINALLY can use either labels or absolute instruction indices (`#N`). **Jump offsets**: JUMP/JUMP_IF_FALSE/JUMP_IF_TRUE use relative offsets from the next instruction (PC + 1). PUSH_TRY/PUSH_FINALLY use absolute instruction indices.
**Stack operations**: Most binary operations pop in reverse order (second operand is popped first, then first operand). **Stack operations**: Most binary operations pop in reverse order (second operand is popped first, then first operand).

43
SPEC.md
View File

@ -327,45 +327,39 @@ All comparison operations pop two values, compare, push boolean result.
``` ```
<evaluate left> <evaluate left>
DUP DUP
JUMP_IF_FALSE .end JUMP_IF_FALSE #2 # skip POP and <evaluate right>
POP POP
<evaluate right> <evaluate right>
.end: end:
``` ```
**OR pattern** (short-circuits if left side is true): **OR pattern** (short-circuits if left side is true):
``` ```
<evaluate left> <evaluate left>
DUP DUP
JUMP_IF_TRUE .end JUMP_IF_TRUE #2 # skip POP and <evaluate right>
POP POP
<evaluate right> <evaluate right>
.end: end:
``` ```
### Control Flow ### Control Flow
#### JUMP #### JUMP
**Operand**: Label (string) **Operand**: Offset (number)
**Effect**: Jump to the specified label **Effect**: Add offset to PC (relative jump)
**Stack**: No change **Stack**: No change
**Note**: JUMP only accepts label operands (`.label`), not numeric offsets. The VM resolves labels to relative offsets internally.
#### JUMP_IF_FALSE #### JUMP_IF_FALSE
**Operand**: Label (string) **Operand**: Offset (number)
**Effect**: If top of stack is falsy, jump to the specified label **Effect**: If top of stack is falsy, add offset to PC (relative jump)
**Stack**: [condition] → [] **Stack**: [condition] → []
**Note**: JUMP_IF_FALSE only accepts label operands (`.label`), not numeric offsets.
#### JUMP_IF_TRUE #### JUMP_IF_TRUE
**Operand**: Label (string) **Operand**: Offset (number)
**Effect**: If top of stack is truthy, jump to the specified label **Effect**: If top of stack is truthy, add offset to PC (relative jump)
**Stack**: [condition] → [] **Stack**: [condition] → []
**Note**: JUMP_IF_TRUE only accepts label operands (`.label`), not numeric offsets.
#### BREAK #### BREAK
**Operand**: None **Operand**: None
**Effect**: Unwind call stack until frame with `isBreakTarget = true`, resume there **Effect**: Unwind call stack until frame with `isBreakTarget = true`, resume there
@ -820,16 +814,14 @@ CALL ; → "Hi, Bob!"
## Label Syntax ## Label Syntax
The bytecode format requires labels for control flow jumps: The bytecode format supports labels for improved readability:
**Label Definition**: `.label_name:` marks an instruction position **Label Definition**: `.label_name:` marks an instruction position
**Label Reference**: `.label_name` in operands (e.g., `JUMP .loop_start`) **Label Reference**: `.label_name` in operands (e.g., `JUMP .loop_start`)
Labels are resolved to relative PC offsets during bytecode compilation. All JUMP instructions (`JUMP`, `JUMP_IF_FALSE`, `JUMP_IF_TRUE`) require label operands. Labels are resolved to numeric offsets during parsing. The original numeric offset syntax (`#N`) is still supported for backwards compatibility.
**Note**: Exception handling instructions (`PUSH_TRY`, `PUSH_FINALLY`) and function definitions (`MAKE_FUNCTION`) can use either labels or absolute instruction indices (`#N`). Example with labels:
Example:
``` ```
JUMP .skip JUMP .skip
.middle: .middle:
@ -840,6 +832,15 @@ JUMP .skip
HALT HALT
``` ```
Equivalent with numeric offsets:
```
JUMP #2
PUSH 999
HALT
PUSH 42
HALT
```
## Common Bytecode Patterns ## Common Bytecode Patterns
### If-Else Statement ### If-Else Statement

View File

@ -44,9 +44,9 @@ type InstructionTuple =
| ["NOT"] | ["NOT"]
// Control flow // Control flow
| ["JUMP", string] | ["JUMP", string | number]
| ["JUMP_IF_FALSE", string] | ["JUMP_IF_FALSE", string | number]
| ["JUMP_IF_TRUE", string] | ["JUMP_IF_TRUE", string | number]
| ["BREAK"] | ["BREAK"]
// Exception handling // Exception handling
@ -56,7 +56,7 @@ type InstructionTuple =
| ["THROW"] | ["THROW"]
// Functions // Functions
| ["MAKE_FUNCTION", string[], string] | ["MAKE_FUNCTION", string[], string | number]
| ["CALL"] | ["CALL"]
| ["TAIL_CALL"] | ["TAIL_CALL"]
| ["RETURN"] | ["RETURN"]
@ -88,6 +88,30 @@ type LabelDefinition = [string] // Just ".label_name:"
export type ProgramItem = InstructionTuple | LabelDefinition export type ProgramItem = InstructionTuple | LabelDefinition
//
// Parse bytecode from human-readable string format.
// Operand types are determined by prefix/literal:
// #42 -> immediate number (e.g., JUMP #5, MAKE_ARRAY #3)
// .label -> label reference (e.g., JUMP .loop_start, MAKE_FUNCTION (x y) .body)
// name -> variable/function name (e.g., LOAD x, LOAD_NATIVE add)
// 42 -> number constant (e.g., PUSH 42)
// "str" -> string constant (e.g., PUSH "hello")
// 'str' -> string constant (e.g., PUSH 'hello')
// true -> boolean constant (e.g., PUSH true)
// false -> boolean constant (e.g., PUSH false)
// null -> null constant (e.g., PUSH null)
//
// Labels:
// .label_name: -> label definition (marks current instruction position)
//
// Function definitions:
// MAKE_FUNCTION (x y) #7 -> basic function (numeric offset)
// MAKE_FUNCTION (x y) .body -> basic function (label reference)
// MAKE_FUNCTION (x y=42) #7 -> with defaults
// MAKE_FUNCTION (x ...rest) #7 -> variadic
// MAKE_FUNCTION (x @named) #7 -> named
//
function parseFunctionParams(paramStr: string, constants: Constant[]): { function parseFunctionParams(paramStr: string, constants: Constant[]): {
params: string[] params: string[]
defaults: Record<string, number> defaults: Record<string, number>
@ -349,29 +373,6 @@ function toBytecodeFromArray(program: ProgramItem[]): Bytecode /* throws */ {
} }
} }
////
// Parse bytecode from human-readable string format.
// Operand types are determined by prefix/literal:
// #42 -> immediate number (e.g., JUMP #5, MAKE_ARRAY #3)
// .label -> label reference (e.g., JUMP .loop_start, MAKE_FUNCTION (x y) .body)
// name -> variable/function name (e.g., LOAD x, LOAD_NATIVE add)
// 42 -> number constant (e.g., PUSH 42)
// "str" -> string constant (e.g., PUSH "hello")
// 'str' -> string constant (e.g., PUSH 'hello')
// true -> boolean constant (e.g., PUSH true)
// false -> boolean constant (e.g., PUSH false)
// null -> null constant (e.g., PUSH null)
//
// Labels:
// .label_name: -> label definition (marks current instruction position)
//
// Function definitions:
// MAKE_FUNCTION (x y) #7 -> basic function (numeric offset)
// MAKE_FUNCTION (x y) .body -> basic function (label reference)
// MAKE_FUNCTION (x y=42) #7 -> with defaults
// MAKE_FUNCTION (x ...rest) #7 -> variadic
// MAKE_FUNCTION (x @named) #7 -> named
function toBytecodeFromString(str: string): Bytecode /* throws */ { function toBytecodeFromString(str: string): Bytecode /* throws */ {
const lines = str.trim().split("\n") const lines = str.trim().split("\n")
@ -390,7 +391,7 @@ function toBytecodeFromString(str: string): Bytecode /* throws */ {
if (!trimmed) continue if (!trimmed) continue
// Check for label definition (.label_name:) // Check for label definition (.label_name:)
if (/^\.[a-zA-Z_][a-zA-Z0-9_.]*:$/.test(trimmed)) { if (/^\.[a-zA-Z_][a-zA-Z0-9_]*:$/.test(trimmed)) {
const labelName = trimmed.slice(1, -1) const labelName = trimmed.slice(1, -1)
labels.set(labelName, cleanLines.length) labels.set(labelName, cleanLines.length)
continue continue

View File

@ -87,15 +87,11 @@ const OPCODES_WITHOUT_OPERANDS = new Set([
OpCode.DOT_GET, OpCode.DOT_GET,
]) ])
// JUMP* instructions require labels only (no numeric immediates) // immediate = immediate number, eg #5
const OPCODES_REQUIRING_LABEL = new Set([ const OPCODES_REQUIRING_IMMEDIATE_OR_LABEL = new Set([
OpCode.JUMP, OpCode.JUMP,
OpCode.JUMP_IF_FALSE, OpCode.JUMP_IF_FALSE,
OpCode.JUMP_IF_TRUE, OpCode.JUMP_IF_TRUE,
])
// PUSH_TRY/PUSH_FINALLY still allow immediate or label
const OPCODES_REQUIRING_IMMEDIATE_OR_LABEL = new Set([
OpCode.PUSH_TRY, OpCode.PUSH_TRY,
OpCode.PUSH_FINALLY, OpCode.PUSH_FINALLY,
]) ])
@ -201,16 +197,6 @@ export function validateBytecode(source: string): ValidationResult {
// Validate specific operand formats // Validate specific operand formats
if (operand) { if (operand) {
if (OPCODES_REQUIRING_LABEL.has(opCode)) {
if (!operand.startsWith('.')) {
errors.push({
line: lineNum,
message: `${opName} requires label (.label), got: ${operand}`,
})
continue
}
}
if (OPCODES_REQUIRING_IMMEDIATE_OR_LABEL.has(opCode)) { if (OPCODES_REQUIRING_IMMEDIATE_OR_LABEL.has(opCode)) {
if (!operand.startsWith('#') && !operand.startsWith('.')) { if (!operand.startsWith('#') && !operand.startsWith('.')) {
errors.push({ errors.push({
@ -324,11 +310,11 @@ export function validateBytecode(source: string): ValidationResult {
} }
} }
// Validate body address (must be a label) // Validate body address
if (!bodyAddr!.startsWith('.')) { if (!bodyAddr!.startsWith('.') && !bodyAddr!.startsWith('#')) {
errors.push({ errors.push({
line: lineNum, line: lineNum,
message: `Invalid body address: expected .label, got: ${bodyAddr}`, message: `Invalid body address: expected .label or #offset`,
}) })
} }

View File

@ -201,17 +201,17 @@ test("formatValidationErrors produces readable output", () => {
expect(formatted).toContain("UNKNOWN") expect(formatted).toContain("UNKNOWN")
}) })
test("detects JUMP without .label", () => { test("detects JUMP without # or .label", () => {
const source = ` const source = `
JUMP 5 JUMP 5
HALT HALT
` `
const result = validateBytecode(source) const result = validateBytecode(source)
expect(result.valid).toBe(false) expect(result.valid).toBe(false)
expect(result.errors[0]!.message).toContain("JUMP requires label (.label)") expect(result.errors[0]!.message).toContain("JUMP requires immediate (#number) or label (.label)")
}) })
test("detects JUMP_IF_TRUE without .label", () => { test("detects JUMP_IF_TRUE without # or .label", () => {
const source = ` const source = `
PUSH true PUSH true
JUMP_IF_TRUE 2 JUMP_IF_TRUE 2
@ -219,10 +219,10 @@ test("detects JUMP_IF_TRUE without .label", () => {
` `
const result = validateBytecode(source) const result = validateBytecode(source)
expect(result.valid).toBe(false) expect(result.valid).toBe(false)
expect(result.errors[0]!.message).toContain("JUMP_IF_TRUE requires label (.label)") expect(result.errors[0]!.message).toContain("JUMP_IF_TRUE requires immediate (#number) or label (.label)")
}) })
test("detects JUMP_IF_FALSE without .label", () => { test("detects JUMP_IF_FALSE without # or .label", () => {
const source = ` const source = `
PUSH false PUSH false
JUMP_IF_FALSE 2 JUMP_IF_FALSE 2
@ -230,18 +230,17 @@ test("detects JUMP_IF_FALSE without .label", () => {
` `
const result = validateBytecode(source) const result = validateBytecode(source)
expect(result.valid).toBe(false) expect(result.valid).toBe(false)
expect(result.errors[0]!.message).toContain("JUMP_IF_FALSE requires label (.label)") expect(result.errors[0]!.message).toContain("JUMP_IF_FALSE requires immediate (#number) or label (.label)")
}) })
test("rejects JUMP with immediate number", () => { test("allows JUMP with immediate number", () => {
const source = ` const source = `
JUMP #2 JUMP #2
PUSH 999 PUSH 999
HALT HALT
` `
const result = validateBytecode(source) const result = validateBytecode(source)
expect(result.valid).toBe(false) expect(result.valid).toBe(true)
expect(result.errors[0]!.message).toContain("JUMP requires label (.label)")
}) })
test("detects MAKE_ARRAY without #", () => { test("detects MAKE_ARRAY without #", () => {