Compare commits
2 Commits
060fa064fe
...
4e2869ebd9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e2869ebd9 | ||
|
|
c4bd5219f8 |
170
CLAUDE.md
170
CLAUDE.md
|
|
@ -161,6 +161,176 @@ HALT
|
||||||
- Prefer Bun APIs over Node.js equivalents when available
|
- Prefer Bun APIs over Node.js equivalents when available
|
||||||
- See .cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc for detailed Bun usage
|
- See .cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc for detailed Bun usage
|
||||||
|
|
||||||
|
## Adding a New OpCode
|
||||||
|
|
||||||
|
When adding a new instruction to ReefVM, you must update multiple files in a specific order. Follow this checklist:
|
||||||
|
|
||||||
|
### 1. Define the OpCode (src/opcode.ts)
|
||||||
|
|
||||||
|
Add the new opcode to the `OpCode` enum with comprehensive documentation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export enum OpCode {
|
||||||
|
// ... existing opcodes
|
||||||
|
|
||||||
|
MY_NEW_OP, // operand: <type> | stack: [inputs] → [outputs]
|
||||||
|
// Description of what it does
|
||||||
|
// Any important behavioral notes
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Implement VM Execution (src/vm.ts)
|
||||||
|
|
||||||
|
Add a case to the `execute()` method's switch statement:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async execute(instruction: Instruction) {
|
||||||
|
switch (instruction.op) {
|
||||||
|
// ... existing cases
|
||||||
|
|
||||||
|
case OpCode.MY_NEW_OP:
|
||||||
|
// Implementation
|
||||||
|
// - Pop values from this.stack as needed
|
||||||
|
// - Perform the operation
|
||||||
|
// - Push results to this.stack
|
||||||
|
// - Throw errors for invalid operations
|
||||||
|
// - Use await for async operations
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Common helper methods:
|
||||||
|
- `this.binaryOp((a, b) => ...)` - For binary arithmetic/comparison
|
||||||
|
- `toNumber(value)`, `toString(value)`, `isTrue(value)`, `isEqual(a, b)` - Type coercion
|
||||||
|
- `this.scope.get(name)`, `this.scope.set(name, value)` - Variable access
|
||||||
|
|
||||||
|
### 3. Update Validator (src/validator.ts)
|
||||||
|
|
||||||
|
Add the opcode to the appropriate set:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// If your opcode requires an operand:
|
||||||
|
const OPCODES_WITH_OPERANDS = new Set([
|
||||||
|
// ... existing
|
||||||
|
OpCode.MY_NEW_OP,
|
||||||
|
])
|
||||||
|
|
||||||
|
// If your opcode takes no operand:
|
||||||
|
const OPCODES_WITHOUT_OPERANDS = new Set([
|
||||||
|
// ... existing
|
||||||
|
OpCode.MY_NEW_OP,
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
If your opcode has complex operand validation, add a specific check in the validation loop around line 154.
|
||||||
|
|
||||||
|
### 4. Update Array API (src/bytecode.ts)
|
||||||
|
|
||||||
|
Add your instruction to the `InstructionTuple` type:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type InstructionTuple =
|
||||||
|
// ... existing types
|
||||||
|
| ["MY_NEW_OP"] // No operand
|
||||||
|
| ["MY_NEW_OP", string] // String operand
|
||||||
|
| ["MY_NEW_OP", number] // Number operand
|
||||||
|
| ["MY_NEW_OP", string, number] // Multiple operands
|
||||||
|
```
|
||||||
|
|
||||||
|
If your opcode has special operand handling, add a case in `toBytecodeFromArray()` around line 241.
|
||||||
|
|
||||||
|
### 5. Write Tests (REQUIRED)
|
||||||
|
|
||||||
|
Create tests in the appropriate test file:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tests/basic.test.ts, tests/functions.test.ts, etc.
|
||||||
|
|
||||||
|
test("MY_NEW_OP description", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
// Setup
|
||||||
|
["PUSH", 42],
|
||||||
|
["MY_NEW_OP"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
expect(result).toEqual({ type: "number", value: 42 })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test edge cases
|
||||||
|
test("MY_NEW_OP with invalid input", async () => {
|
||||||
|
// Test error conditions
|
||||||
|
await expect(run(bytecode)).rejects.toThrow()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**ALWAYS write tests.** Test both success cases and error conditions. Add integration tests showing real-world usage.
|
||||||
|
|
||||||
|
### 6. Document Specification (SPEC.md)
|
||||||
|
|
||||||
|
Add a formal specification entry:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
#### MY_NEW_OP
|
||||||
|
|
||||||
|
**Operand**: `<type>`
|
||||||
|
**Stack**: `[input] → [output]`
|
||||||
|
|
||||||
|
Description of what the instruction does.
|
||||||
|
|
||||||
|
**Behavior**:
|
||||||
|
- Specific behavior point 1
|
||||||
|
- Specific behavior point 2
|
||||||
|
|
||||||
|
**Errors**:
|
||||||
|
- Error condition 1
|
||||||
|
- Error condition 2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Update Compiler Guide (GUIDE.md)
|
||||||
|
|
||||||
|
If your opcode introduces new patterns, add examples to GUIDE.md:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### New Pattern Name
|
||||||
|
|
||||||
|
\```
|
||||||
|
PUSH value
|
||||||
|
MY_NEW_OP
|
||||||
|
STORE result
|
||||||
|
\```
|
||||||
|
|
||||||
|
Description of the pattern and when to use it.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Add Examples (Optional)
|
||||||
|
|
||||||
|
If your opcode enables new functionality, add an example to `examples/`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// examples/my_feature.reef or examples/my_feature.ts
|
||||||
|
const example = toBytecode([
|
||||||
|
// Demonstrate the new opcode
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Checklist Summary
|
||||||
|
|
||||||
|
When adding an opcode, update in this order:
|
||||||
|
|
||||||
|
- [ ] `src/opcode.ts` - Add enum value with docs
|
||||||
|
- [ ] `src/vm.ts` - Implement execution logic
|
||||||
|
- [ ] `src/validator.ts` - Add to operand requirement set
|
||||||
|
- [ ] `src/bytecode.ts` - Add to InstructionTuple type
|
||||||
|
- [ ] `tests/*.test.ts` - Write comprehensive tests (**REQUIRED**)
|
||||||
|
- [ ] `SPEC.md` - Document formal specification
|
||||||
|
- [ ] `GUIDE.md` - Add compiler patterns (if applicable)
|
||||||
|
- [ ] `examples/` - Add example code (if applicable)
|
||||||
|
|
||||||
|
Run `bun test` to verify all tests pass before committing.
|
||||||
|
|
||||||
## Common Gotchas
|
## Common Gotchas
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
|
||||||
|
|
@ -1,279 +0,0 @@
|
||||||
import type { Bytecode, Constant } from "./bytecode"
|
|
||||||
import { OpCode } from "./opcode"
|
|
||||||
import { toValue } from "./value"
|
|
||||||
|
|
||||||
// Instruction types
|
|
||||||
type PrimitiveValue = number | string | boolean | null
|
|
||||||
|
|
||||||
type InstructionTuple =
|
|
||||||
// Stack
|
|
||||||
| ["PUSH", PrimitiveValue]
|
|
||||||
| ["POP"]
|
|
||||||
| ["DUP"]
|
|
||||||
|
|
||||||
// Variables
|
|
||||||
| ["LOAD", string]
|
|
||||||
| ["STORE", string]
|
|
||||||
|
|
||||||
// Arithmetic
|
|
||||||
| ["ADD"] | ["SUB"] | ["MUL"] | ["DIV"] | ["MOD"]
|
|
||||||
|
|
||||||
// Comparison
|
|
||||||
| ["EQ"] | ["NEQ"] | ["LT"] | ["GT"] | ["LTE"] | ["GTE"]
|
|
||||||
|
|
||||||
// Logical
|
|
||||||
| ["NOT"]
|
|
||||||
|
|
||||||
// Control flow
|
|
||||||
| ["JUMP", string | number]
|
|
||||||
| ["JUMP_IF_FALSE", string | number]
|
|
||||||
| ["JUMP_IF_TRUE", string | number]
|
|
||||||
| ["BREAK"]
|
|
||||||
|
|
||||||
// Exception handling
|
|
||||||
| ["PUSH_TRY", string | number]
|
|
||||||
| ["PUSH_FINALLY", string | number]
|
|
||||||
| ["POP_TRY"]
|
|
||||||
| ["THROW"]
|
|
||||||
|
|
||||||
// Functions
|
|
||||||
| ["MAKE_FUNCTION", string[], string | number]
|
|
||||||
| ["CALL"]
|
|
||||||
| ["TAIL_CALL"]
|
|
||||||
| ["RETURN"]
|
|
||||||
|
|
||||||
// Arrays
|
|
||||||
| ["MAKE_ARRAY", number]
|
|
||||||
| ["ARRAY_GET"]
|
|
||||||
| ["ARRAY_SET"]
|
|
||||||
| ["ARRAY_PUSH"]
|
|
||||||
| ["ARRAY_LEN"]
|
|
||||||
|
|
||||||
// Dicts
|
|
||||||
| ["MAKE_DICT", number]
|
|
||||||
| ["DICT_GET"]
|
|
||||||
| ["DICT_SET"]
|
|
||||||
| ["DICT_HAS"]
|
|
||||||
|
|
||||||
// Native
|
|
||||||
| ["CALL_NATIVE", string]
|
|
||||||
|
|
||||||
// Special
|
|
||||||
| ["HALT"]
|
|
||||||
|
|
||||||
type LabelDefinition = [string] // Just ".label_name"
|
|
||||||
|
|
||||||
type ProgramItem = InstructionTuple | LabelDefinition
|
|
||||||
|
|
||||||
function isLabelDefinition(item: ProgramItem): item is LabelDefinition {
|
|
||||||
return item.length === 1 && typeof item[0] === "string" && item[0].startsWith(".")
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLabelReference(value: string | number): value is string {
|
|
||||||
return typeof value === "string" && value.startsWith(".")
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseFunctionParams(params: string[]): {
|
|
||||||
params: string[]
|
|
||||||
defaults: Record<string, number>
|
|
||||||
variadic: boolean
|
|
||||||
named: boolean
|
|
||||||
defaultConstants: Constant[]
|
|
||||||
} {
|
|
||||||
const resultParams: string[] = []
|
|
||||||
const defaults: Record<string, number> = {}
|
|
||||||
const defaultConstants: Constant[] = []
|
|
||||||
let variadic = false
|
|
||||||
let named = false
|
|
||||||
|
|
||||||
for (const param of params) {
|
|
||||||
if (param.startsWith("@")) {
|
|
||||||
// Named parameter
|
|
||||||
named = true
|
|
||||||
resultParams.push(param.slice(1))
|
|
||||||
} else if (param.startsWith("...")) {
|
|
||||||
// Variadic parameter
|
|
||||||
variadic = true
|
|
||||||
resultParams.push(param.slice(3))
|
|
||||||
} else if (param.includes("=")) {
|
|
||||||
// Default parameter
|
|
||||||
const [name, defaultValue] = param.split("=").map(s => s.trim())
|
|
||||||
resultParams.push(name!)
|
|
||||||
|
|
||||||
// Parse default value
|
|
||||||
if (/^-?\d+(\.\d+)?$/.test(defaultValue!)) {
|
|
||||||
defaultConstants.push(toValue(parseFloat(defaultValue!)))
|
|
||||||
} else if (defaultValue === "true") {
|
|
||||||
defaultConstants.push(toValue(true))
|
|
||||||
} else if (defaultValue === "false") {
|
|
||||||
defaultConstants.push(toValue(false))
|
|
||||||
} else if (defaultValue === "null") {
|
|
||||||
defaultConstants.push(toValue(null))
|
|
||||||
} else if (/^['"].*['"]$/.test(defaultValue!)) {
|
|
||||||
defaultConstants.push(toValue(defaultValue!.slice(1, -1)))
|
|
||||||
} else {
|
|
||||||
throw new Error(`Invalid default value: ${defaultValue}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
defaults[name!] = -1 // Will be fixed after we know constant indices
|
|
||||||
} else {
|
|
||||||
// Regular parameter
|
|
||||||
resultParams.push(param)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { params: resultParams, defaults, variadic, named, defaultConstants }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fromInstructions(program: ProgramItem[]): Bytecode {
|
|
||||||
const constants: Constant[] = []
|
|
||||||
const instructions: any[] = []
|
|
||||||
const labels = new Map<string, number>()
|
|
||||||
|
|
||||||
// First pass: collect labels and their positions
|
|
||||||
const filteredProgram: InstructionTuple[] = []
|
|
||||||
for (const item of program) {
|
|
||||||
if (isLabelDefinition(item)) {
|
|
||||||
const labelName = item[0].slice(1) // Remove leading "."
|
|
||||||
labels.set(labelName, filteredProgram.length)
|
|
||||||
} else {
|
|
||||||
filteredProgram.push(item as InstructionTuple)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second pass: build instructions
|
|
||||||
for (let i = 0; i < filteredProgram.length; i++) {
|
|
||||||
const item = filteredProgram[i]!
|
|
||||||
const op = item[0] as string
|
|
||||||
const opCode = OpCode[op as keyof typeof OpCode]
|
|
||||||
|
|
||||||
if (opCode === undefined) {
|
|
||||||
throw new Error(`Unknown opcode: ${op}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
let operandValue: number | string | undefined = undefined
|
|
||||||
|
|
||||||
if (item.length > 1) {
|
|
||||||
const operand = item[1]
|
|
||||||
|
|
||||||
switch (op) {
|
|
||||||
case "PUSH":
|
|
||||||
// Add to constants pool
|
|
||||||
constants.push(toValue(operand as PrimitiveValue))
|
|
||||||
operandValue = constants.length - 1
|
|
||||||
break
|
|
||||||
|
|
||||||
case "MAKE_FUNCTION": {
|
|
||||||
const params = operand as string[]
|
|
||||||
const body = item[2]
|
|
||||||
|
|
||||||
if (body === undefined) {
|
|
||||||
throw new Error("MAKE_FUNCTION requires body address")
|
|
||||||
}
|
|
||||||
|
|
||||||
const { params: resultParams, defaults, variadic, named, defaultConstants } = parseFunctionParams(params)
|
|
||||||
|
|
||||||
// Add default constants to pool and update indices
|
|
||||||
const defaultIndices: Record<string, number> = {}
|
|
||||||
for (const [paramName, _] of Object.entries(defaults)) {
|
|
||||||
const defaultConst = defaultConstants.shift()!
|
|
||||||
constants.push(defaultConst)
|
|
||||||
defaultIndices[paramName] = constants.length - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve body label or use numeric value
|
|
||||||
let bodyAddress: number
|
|
||||||
if (isLabelReference(body)) {
|
|
||||||
const labelName = body.slice(1)
|
|
||||||
const labelPos = labels.get(labelName)
|
|
||||||
if (labelPos === undefined) {
|
|
||||||
throw new Error(`Undefined label: ${labelName}`)
|
|
||||||
}
|
|
||||||
bodyAddress = labelPos
|
|
||||||
} else {
|
|
||||||
bodyAddress = body as number
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add function definition to constants
|
|
||||||
constants.push({
|
|
||||||
type: "function_def",
|
|
||||||
params: resultParams,
|
|
||||||
defaults: defaultIndices,
|
|
||||||
body: bodyAddress,
|
|
||||||
variadic,
|
|
||||||
named
|
|
||||||
})
|
|
||||||
|
|
||||||
operandValue = constants.length - 1
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "JUMP":
|
|
||||||
case "JUMP_IF_FALSE":
|
|
||||||
case "JUMP_IF_TRUE": {
|
|
||||||
// Relative jump
|
|
||||||
if (isLabelReference(operand as string | number)) {
|
|
||||||
const labelName = (operand as string).slice(1)
|
|
||||||
const labelPos = labels.get(labelName)
|
|
||||||
if (labelPos === undefined) {
|
|
||||||
throw new Error(`Undefined label: ${labelName}`)
|
|
||||||
}
|
|
||||||
operandValue = labelPos - (i + 1) // Relative offset
|
|
||||||
} else {
|
|
||||||
operandValue = operand as number
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "PUSH_TRY":
|
|
||||||
case "PUSH_FINALLY": {
|
|
||||||
// Absolute address
|
|
||||||
if (isLabelReference(operand as string | number)) {
|
|
||||||
const labelName = (operand as string).slice(1)
|
|
||||||
const labelPos = labels.get(labelName)
|
|
||||||
if (labelPos === undefined) {
|
|
||||||
throw new Error(`Undefined label: ${labelName}`)
|
|
||||||
}
|
|
||||||
operandValue = labelPos
|
|
||||||
} else {
|
|
||||||
operandValue = operand as number
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "LOAD":
|
|
||||||
case "STORE":
|
|
||||||
case "CALL_NATIVE":
|
|
||||||
// String operand
|
|
||||||
operandValue = operand as string
|
|
||||||
break
|
|
||||||
|
|
||||||
case "MAKE_ARRAY":
|
|
||||||
case "MAKE_DICT":
|
|
||||||
// Numeric operand
|
|
||||||
operandValue = operand as number
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`Unexpected operand for ${op}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
instructions.push({
|
|
||||||
op: opCode,
|
|
||||||
operand: operandValue
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build labels map for debugger (instruction index -> label name)
|
|
||||||
const labelsByIndex = new Map<number, string>()
|
|
||||||
for (const [name, index] of labels.entries()) {
|
|
||||||
labelsByIndex.set(index, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
instructions,
|
|
||||||
constants,
|
|
||||||
labels: labelsByIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user