diff --git a/GUIDE.md b/GUIDE.md index b8c5c43..62453ca 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -195,7 +195,8 @@ CALL - `DUP` - Duplicate top ### Variables -- `LOAD ` - Push variable value +- `LOAD ` - Push variable value (throws if not found) +- `TRY_LOAD ` - Push variable value if found, otherwise push name as string (never throws) - `STORE ` - Pop and store in variable ### Arithmetic diff --git a/SPEC.md b/SPEC.md index d1a326c..d884a0f 100644 --- a/SPEC.md +++ b/SPEC.md @@ -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 @@ -142,10 +147,32 @@ type ExceptionHandler = { **Errors**: Throws if variable not found in scope chain #### STORE -**Operand**: Variable name (string) -**Effect**: Store top of stack into variable (following scope chain rules) +**Operand**: Variable name (string) +**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. diff --git a/src/opcode.ts b/src/opcode.ts index 9bc272a..a200bbe 100644 --- a/src/opcode.ts +++ b/src/opcode.ts @@ -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] diff --git a/src/vm.ts b/src/vm.ts index f9f4e44..6442f39 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -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 diff --git a/tests/basic.test.ts b/tests/basic.test.ts index 595057d..5e14837 100644 --- a/tests/basic.test.ts +++ b/tests/basic.test.ts @@ -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