forked from defunkt/ReefVM
TRY_LOAD opcode
This commit is contained in:
parent
27857bfae8
commit
fd447abea8
3
GUIDE.md
3
GUIDE.md
|
|
@ -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
27
SPEC.md
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export enum OpCode {
|
||||||
// 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]
|
||||||
|
|
|
||||||
15
src/vm.ts
15
src/vm.ts
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user