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 - `DUP` - Duplicate top
### Variables ### 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 - `STORE <name>` - Pop and store in variable
### Arithmetic ### Arithmetic

27
SPEC.md
View File

@ -86,6 +86,11 @@ class Scope {
2. If not found, recursively check parent 2. If not found, recursively check parent
3. If not found anywhere, throw error 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)**: **Variable Assignment (STORE)**:
1. If variable exists in current scope, update it 1. If variable exists in current scope, update it
2. Else if variable exists in any parent scope, update it there 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) **Effect**: Store top of stack into variable (following scope chain rules)
**Stack**: [value] → [] **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 ### Arithmetic Operations
All arithmetic operations pop two values, perform operation, push result as number. 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] DUP, // operand: none | stack: [value] → [value, value]
// variables // variables
LOAD, // operand: variable name (identifier) | stack: [] → [value] LOAD, // operand: variable name (identifier) | stack: [] → [value]
STORE, // 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) // math (coerce to number, pop 2, push result)
ADD, // operand: none | stack: [a, b] → [a + b] ADD, // operand: none | stack: [a, b] → [a + b]

View File

@ -116,7 +116,7 @@ export class VM {
this.stopped = true this.stopped = true
break break
case OpCode.LOAD: case OpCode.LOAD: {
const varName = instruction.operand as string const varName = instruction.operand as string
const value = this.scope.get(varName) const value = this.scope.get(varName)
@ -125,6 +125,19 @@ export class VM {
this.stack.push(value) this.stack.push(value)
break 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: case OpCode.STORE:
const name = instruction.operand as string 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 }) 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 () => { test("JUMP - relative jump forward", async () => {
const str = ` const str = `
PUSH 1 PUSH 1