TRY_LOAD opcode

This commit is contained in:
Chris Wanstrath 2025-10-07 21:42:14 -07:00
parent 27857bfae8
commit fd447abea8
5 changed files with 193 additions and 6 deletions

View File

@ -195,7 +195,8 @@ CALL
- `DUP` - Duplicate top
### Variables
- `LOAD <name>` - Push variable value
- `LOAD <name>` - Push variable value (throws if not found)
- `TRY_LOAD <name>` - Push variable value if found, otherwise push name as string (never throws)
- `STORE <name>` - Pop and store in variable
### Arithmetic

27
SPEC.md
View File

@ -86,6 +86,11 @@ class Scope {
2. If not found, recursively check parent
3. If not found anywhere, throw error
**Variable Resolution (TRY_LOAD)**:
1. Check current scope's locals
2. If not found, recursively check parent
3. If not found anywhere, return variable name as string (no error)
**Variable Assignment (STORE)**:
1. If variable exists in current scope, update it
2. Else if variable exists in any parent scope, update it there
@ -146,6 +151,28 @@ type ExceptionHandler = {
**Effect**: Store top of stack into variable (following scope chain rules)
**Stack**: [value] → []
#### TRY_LOAD
**Operand**: Variable name (string)
**Effect**: Push variable value onto stack if found, otherwise push variable name as string
**Stack**: [] → [value | name]
**Errors**: Never throws (unlike LOAD)
**Behavior**:
1. Search for variable in scope chain (current scope and all parents)
2. If found, push the variable's value onto stack
3. If not found, push the variable name as a string value onto stack
**Use Cases**:
- Shell-like behavior where strings don't need quotes
**Example**:
```
PUSH 42
STORE x
TRY_LOAD x ; Pushes 42 (variable exists)
TRY_LOAD y ; Pushes "y" (variable doesn't exist)
```
### Arithmetic Operations
All arithmetic operations pop two values, perform operation, push result as number.

View File

@ -5,8 +5,9 @@ export enum OpCode {
DUP, // operand: none | stack: [value] → [value, value]
// variables
LOAD, // operand: variable name (identifier) | stack: [] → [value]
STORE, // operand: variable name (identifier) | stack: [value] → []
LOAD, // operand: variable name (identifier) | stack: [] → [value]
STORE, // operand: variable name (identifier) | stack: [value] → []
TRY_LOAD, // operand: variable name (identifier) | stack: [] → [value] | load a variable (if found) or a string
// math (coerce to number, pop 2, push result)
ADD, // operand: none | stack: [a, b] → [a + b]

View File

@ -116,7 +116,7 @@ export class VM {
this.stopped = true
break
case OpCode.LOAD:
case OpCode.LOAD: {
const varName = instruction.operand as string
const value = this.scope.get(varName)
@ -125,6 +125,19 @@ export class VM {
this.stack.push(value)
break
}
case OpCode.TRY_LOAD: {
const varName = instruction.operand as string
const value = this.scope.get(varName)
if (value === undefined)
this.stack.push(toValue(varName))
else
this.stack.push(value)
break
}
case OpCode.STORE:
const name = instruction.operand as string

View File

@ -327,6 +327,151 @@ test("STORE and LOAD - multiple variables", async () => {
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 30 })
})
test("TRY_LOAD - variable found", async () => {
const str = `
PUSH 100
STORE count
TRY_LOAD count
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 100 })
const str2 = `
PUSH 'Bobby'
STORE name
TRY_LOAD name
`
expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: 'Bobby' })
})
test("TRY_LOAD - variable missing", async () => {
const str = `
PUSH 100
STORE count
TRY_LOAD count1
`
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'count1' })
const str2 = `
PUSH 'Bobby'
STORE name
TRY_LOAD full-name
`
expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: 'full-name' })
})
test("TRY_LOAD - with different value types", async () => {
// Array
const str1 = `
PUSH 1
PUSH 2
PUSH 3
MAKE_ARRAY #3
STORE arr
TRY_LOAD arr
`
const result1 = await run(toBytecode(str1))
expect(result1.type).toBe('array')
// Dict
const str2 = `
PUSH 'key'
PUSH 'value'
MAKE_DICT #1
STORE dict
TRY_LOAD dict
`
const result2 = await run(toBytecode(str2))
expect(result2.type).toBe('dict')
// Boolean
const str3 = `
PUSH true
STORE flag
TRY_LOAD flag
`
expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: true })
// Null
const str4 = `
PUSH null
STORE empty
TRY_LOAD empty
`
expect(await run(toBytecode(str4))).toEqual({ type: 'null', value: null })
})
test("TRY_LOAD - in nested scope", async () => {
// Function should be able to TRY_LOAD variable from parent scope
const str = `
PUSH 42
STORE outer
MAKE_FUNCTION () .fn
PUSH 0
PUSH 0
CALL
HALT
.fn:
TRY_LOAD outer
RETURN
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
})
test("TRY_LOAD - missing variable in nested scope returns name", async () => {
// If variable doesn't exist in any scope, should return name as string
const str = `
PUSH 42
STORE outer
MAKE_FUNCTION () .fn
PUSH 0
PUSH 0
CALL
HALT
.fn:
TRY_LOAD inner
RETURN
`
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'inner' })
})
test("TRY_LOAD - used for conditional variable existence check", async () => {
// Pattern: use TRY_LOAD to check if variable exists and get its value or name
const str = `
PUSH 100
STORE count
TRY_LOAD count
PUSH 'count'
EQ
`
// Variable exists, so TRY_LOAD returns 100, which != 'count'
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false })
const str2 = `
PUSH 100
STORE count
TRY_LOAD missing
PUSH 'missing'
EQ
`
// Variable missing, so TRY_LOAD returns 'missing', which == 'missing'
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: true })
})
test("TRY_LOAD - with function value", async () => {
const str = `
MAKE_FUNCTION () .fn
STORE myFunc
JUMP .skip
.fn:
PUSH 99
RETURN
.skip:
TRY_LOAD myFunc
`
const result = await run(toBytecode(str))
expect(result.type).toBe('function')
})
test("JUMP - relative jump forward", async () => {
const str = `
PUSH 1