add TYPE opcode

This commit is contained in:
Chris Wanstrath 2025-11-08 00:01:21 -08:00
parent 47e227f50c
commit f439c25742
7 changed files with 241 additions and 0 deletions

View File

@ -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 <name>` - 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

13
SPEC.md
View File

@ -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

View File

@ -23,6 +23,8 @@ type InstructionTuple =
| ["PUSH", Atom]
| ["POP"]
| ["DUP"]
| ["SWAP"]
| ["TYPE"]
// Variables
| ["LOAD", string]

View File

@ -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]

View File

@ -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,

View File

@ -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()!

View File

@ -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 = `