From f439c25742ed69bb141e92f41eeab8bbfd6ac12c Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sat, 8 Nov 2025 00:01:21 -0800 Subject: [PATCH] add TYPE opcode --- GUIDE.md | 51 +++++++++++++ SPEC.md | 13 ++++ src/bytecode.ts | 2 + src/opcode.ts | 3 + src/validator.ts | 2 + src/vm.ts | 5 ++ tests/opcodes.test.ts | 165 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 241 insertions(+) diff --git a/GUIDE.md b/GUIDE.md index fcbbc9e..5a4434a 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -186,6 +186,7 @@ CALL - `POP` - Remove top - `DUP` - Duplicate top - `SWAP` - Swap top two values +- `TYPE` - Pop value, push its type as string ### Variables - `LOAD ` - Push variable value (throws if not found) @@ -434,6 +435,56 @@ BIT_USHR ; → 2147483647 (unsigned shift) - Color manipulation: extract RGB components - Low-level bit manipulation for protocols or file formats +### Runtime Type Checking (TYPE) +Get the type of a value as a string for runtime introspection: + +``` +; Basic type check +PUSH 42 +TYPE ; → "number" + +PUSH "hello" +TYPE ; → "string" + +MAKE_ARRAY #3 +TYPE ; → "array" +``` + +**Type Guard Pattern** (check type before operation): +``` +; Safe addition - only add if both are numbers +LOAD x +DUP +TYPE +PUSH "number" +EQ +JUMP_IF_FALSE .not_number + +LOAD y +DUP +TYPE +PUSH "number" +EQ +JUMP_IF_FALSE .cleanup_not_number + +ADD ; Safe to add +JUMP .end + +.cleanup_not_number: + POP ; Remove y +.not_number: + POP ; Remove x + PUSH null +.end: +``` + +**Common Use Cases**: +- Type validation before operations +- Polymorphic functions that handle multiple types +- Debugging and introspection +- Dynamic dispatch in DSLs +- Safe coercion with fallbacks + ### Try-Catch ``` PUSH_TRY .catch diff --git a/SPEC.md b/SPEC.md index d639c71..86cb794 100644 --- a/SPEC.md +++ b/SPEC.md @@ -143,6 +143,19 @@ type ExceptionHandler = { **Effect**: Swap the top two values on the stack **Stack**: [value1, value2] → [value2, value1] +#### TYPE +**Operand**: None +**Effect**: Pop value from stack, push its type as a string +**Stack**: [value] → [typeString] + +Returns the type of a value as a string. + +**Example**: +``` +PUSH 42 +TYPE ; Pushes "number" +``` + ### Variable Operations #### LOAD diff --git a/src/bytecode.ts b/src/bytecode.ts index 9175e80..ae0f7e9 100644 --- a/src/bytecode.ts +++ b/src/bytecode.ts @@ -23,6 +23,8 @@ type InstructionTuple = | ["PUSH", Atom] | ["POP"] | ["DUP"] + | ["SWAP"] + | ["TYPE"] // Variables | ["LOAD", string] diff --git a/src/opcode.ts b/src/opcode.ts index 3a7df0f..90b273c 100644 --- a/src/opcode.ts +++ b/src/opcode.ts @@ -10,6 +10,9 @@ export enum OpCode { STORE, // operand: variable name (identifier) | stack: [value] → [] TRY_LOAD, // operand: variable name (identifier) | stack: [] → [value] | load a variable (if found) or a string + // information + TYPE, // operand: none | stack: [a] → [] + // math (coerce to number, pop 2, push result) ADD, // operand: none | stack: [a, b] → [a + b] SUB, // operand: none | stack: [a, b] → [a - b] diff --git a/src/validator.ts b/src/validator.ts index 1d7b6a8..7d44816 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -50,6 +50,8 @@ const OPCODES_WITH_OPERANDS = new Set([ const OPCODES_WITHOUT_OPERANDS = new Set([ OpCode.POP, OpCode.DUP, + OpCode.SWAP, + OpCode.TYPE, OpCode.ADD, OpCode.SUB, OpCode.MUL, diff --git a/src/vm.ts b/src/vm.ts index f0fc1e1..24ea531 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -285,6 +285,11 @@ export class VM { break } + case OpCode.TYPE: + const value = this.stack.pop()! + this.stack.push(toValue(value.type)) + break + case OpCode.STORE: const name = instruction.operand as string const toStore = this.stack.pop()! diff --git a/tests/opcodes.test.ts b/tests/opcodes.test.ts index a921c86..8ecca3f 100644 --- a/tests/opcodes.test.ts +++ b/tests/opcodes.test.ts @@ -828,6 +828,171 @@ describe("HALT", () => { }) }) +describe("TYPE", () => { + test("null type", async () => { + const bytecode = toBytecode([ + ["PUSH", null], + ["TYPE"], + ["HALT"] + ]) + expect(await run(bytecode)).toEqual({ type: 'string', value: 'null' }) + }) + + test("boolean type", async () => { + const bytecode1 = toBytecode([ + ["PUSH", true], + ["TYPE"], + ["HALT"] + ]) + expect(await run(bytecode1)).toEqual({ type: 'string', value: 'boolean' }) + + const bytecode2 = toBytecode([ + ["PUSH", false], + ["TYPE"], + ["HALT"] + ]) + expect(await run(bytecode2)).toEqual({ type: 'string', value: 'boolean' }) + }) + + test("number type", async () => { + const bytecode1 = toBytecode([ + ["PUSH", 42], + ["TYPE"], + ["HALT"] + ]) + expect(await run(bytecode1)).toEqual({ type: 'string', value: 'number' }) + + const bytecode2 = toBytecode([ + ["PUSH", 0], + ["TYPE"], + ["HALT"] + ]) + expect(await run(bytecode2)).toEqual({ type: 'string', value: 'number' }) + + const bytecode3 = toBytecode([ + ["PUSH", -3.14], + ["TYPE"], + ["HALT"] + ]) + expect(await run(bytecode3)).toEqual({ type: 'string', value: 'number' }) + }) + + test("string type", async () => { + const bytecode1 = toBytecode([ + ["PUSH", "hello"], + ["TYPE"], + ["HALT"] + ]) + expect(await run(bytecode1)).toEqual({ type: 'string', value: 'string' }) + + const bytecode2 = toBytecode([ + ["PUSH", ""], + ["TYPE"], + ["HALT"] + ]) + expect(await run(bytecode2)).toEqual({ type: 'string', value: 'string' }) + }) + + test("array type", async () => { + const bytecode = toBytecode([ + ["PUSH", 1], + ["PUSH", 2], + ["PUSH", 3], + ["MAKE_ARRAY", 3], + ["TYPE"], + ["HALT"] + ]) + expect(await run(bytecode)).toEqual({ type: 'string', value: 'array' }) + }) + + test("empty array type", async () => { + const bytecode = toBytecode([ + ["MAKE_ARRAY", 0], + ["TYPE"], + ["HALT"] + ]) + expect(await run(bytecode)).toEqual({ type: 'string', value: 'array' }) + }) + + test("dict type", async () => { + const bytecode = toBytecode([ + ["PUSH", "name"], + ["PUSH", "Alice"], + ["PUSH", "age"], + ["PUSH", 30], + ["MAKE_DICT", 2], + ["TYPE"], + ["HALT"] + ]) + expect(await run(bytecode)).toEqual({ type: 'string', value: 'dict' }) + }) + + test("empty dict type", async () => { + const bytecode = toBytecode([ + ["MAKE_DICT", 0], + ["TYPE"], + ["HALT"] + ]) + expect(await run(bytecode)).toEqual({ type: 'string', value: 'dict' }) + }) + + test("function type", async () => { + const bytecode = toBytecode([ + ["MAKE_FUNCTION", ["x"], ".body"], + ["TYPE"], + ["HALT"], + [".body:"], + ["LOAD", "x"], + ["RETURN"] + ]) + expect(await run(bytecode)).toEqual({ type: 'string', value: 'function' }) + }) + + test("native function type", async () => { + const bytecode = toBytecode([ + ["LOAD", "add"], + ["TYPE"], + ["HALT"] + ]) + const result = await run(bytecode, { + add: (a: number, b: number) => a + b + }) + expect(result).toEqual({ type: 'string', value: 'native' }) + }) + + test("regex type", async () => { + const bytecode = toBytecode([ + ["PUSH", /test/i], + ["TYPE"], + ["HALT"] + ]) + expect(await run(bytecode)).toEqual({ type: 'string', value: 'regex' }) + }) + + test("TYPE with stored result", async () => { + const bytecode = toBytecode([ + ["PUSH", 100], + ["TYPE"], + ["STORE", "myType"], + ["LOAD", "myType"], + ["HALT"] + ]) + expect(await run(bytecode)).toEqual({ type: 'string', value: 'number' }) + }) + + test("TYPE comparison - type guards", async () => { + const bytecode = toBytecode([ + ["PUSH", "hello world"], + ["DUP"], + ["TYPE"], + ["PUSH", "string"], + ["EQ"], + ["HALT"] + ]) + expect(await run(bytecode)).toEqual({ type: 'boolean', value: true }) + }) +}) + describe("LOAD / STORE", () => { test("variables", async () => { const str = `