forked from defunkt/ReefVM
DOT_GET
This commit is contained in:
parent
b16351ac95
commit
1a18a713d7
53
GUIDE.md
53
GUIDE.md
|
|
@ -235,6 +235,9 @@ CALL
|
|||
- `DICT_SET` - Pop value, key, dict; mutate dict
|
||||
- `DICT_HAS` - Pop key and dict, push boolean
|
||||
|
||||
### Unified Access
|
||||
- `DOT_GET` - Pop index/key and array/dict, push value (null if missing)
|
||||
|
||||
### Strings
|
||||
- `STR_CONCAT #N` - Pop N values, convert to strings, concatenate, push result
|
||||
|
||||
|
|
@ -474,6 +477,56 @@ PUSH "!"
|
|||
STR_CONCAT #2 ; → "Hello World!"
|
||||
```
|
||||
|
||||
### Unified Access (DOT_GET)
|
||||
DOT_GET provides a single opcode for accessing both arrays and dicts:
|
||||
|
||||
```
|
||||
; Array access
|
||||
PUSH 10
|
||||
PUSH 20
|
||||
PUSH 30
|
||||
MAKE_ARRAY #3
|
||||
PUSH 1
|
||||
DOT_GET ; → 20
|
||||
|
||||
; Dict access
|
||||
PUSH 'name'
|
||||
PUSH 'Alice'
|
||||
MAKE_DICT #1
|
||||
PUSH 'name'
|
||||
DOT_GET ; → 'Alice'
|
||||
```
|
||||
|
||||
**Chained access**:
|
||||
```
|
||||
; Access dict['users'][0]['name']
|
||||
LOAD dict
|
||||
PUSH 'users'
|
||||
DOT_GET ; Get users array
|
||||
PUSH 0
|
||||
DOT_GET ; Get first user
|
||||
PUSH 'name'
|
||||
DOT_GET ; Get name field
|
||||
```
|
||||
|
||||
**With variables**:
|
||||
```
|
||||
LOAD data
|
||||
LOAD key ; Key can be string or number
|
||||
DOT_GET ; Works for both array and dict
|
||||
```
|
||||
|
||||
**Null safety**: Returns null for missing keys or out-of-bounds indices
|
||||
```
|
||||
MAKE_ARRAY #0
|
||||
PUSH 0
|
||||
DOT_GET ; → null (empty array)
|
||||
|
||||
MAKE_DICT #0
|
||||
PUSH 'key'
|
||||
DOT_GET ; → null (missing key)
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Truthiness
|
||||
|
|
|
|||
45
SPEC.md
45
SPEC.md
|
|
@ -509,6 +509,51 @@ Key is coerced to string.
|
|||
Key is coerced to string.
|
||||
**Errors**: Throws if not dict
|
||||
|
||||
### Unified Access
|
||||
|
||||
#### DOT_GET
|
||||
**Operand**: None
|
||||
**Effect**: Get value from array or dict
|
||||
**Stack**: [array|dict, index|key] → [value]
|
||||
|
||||
**Behavior**:
|
||||
- If target is array: coerce index to number and access `array[index]`
|
||||
- If target is dict: coerce key to string and access `dict.get(key)`
|
||||
- Returns null if index out of bounds or key not found
|
||||
|
||||
**Errors**: Throws if target is not array or dict
|
||||
|
||||
**Use Cases**:
|
||||
- Unified syntax for accessing both arrays and dicts
|
||||
- Chaining access operations: `obj.users.0.name`
|
||||
- Generic accessor that works with any indexable type
|
||||
|
||||
**Example**:
|
||||
```
|
||||
; Array access
|
||||
PUSH 10
|
||||
PUSH 20
|
||||
PUSH 30
|
||||
MAKE_ARRAY #3
|
||||
PUSH 1
|
||||
DOT_GET ; → 20
|
||||
|
||||
; Dict access
|
||||
PUSH 'name'
|
||||
PUSH 'Alice'
|
||||
MAKE_DICT #1
|
||||
PUSH 'name'
|
||||
DOT_GET ; → 'Alice'
|
||||
|
||||
; Chained access
|
||||
; dict['users'][0]
|
||||
LOAD dict
|
||||
PUSH 'users'
|
||||
DOT_GET
|
||||
PUSH 0
|
||||
DOT_GET
|
||||
```
|
||||
|
||||
### String Operations
|
||||
|
||||
#### STR_CONCAT
|
||||
|
|
|
|||
|
|
@ -59,6 +59,9 @@ export enum OpCode {
|
|||
DICT_SET, // operand: none | stack: [dict, key, value] → [] | mutates dict
|
||||
DICT_HAS, // operand: none | stack: [dict, key] → [boolean]
|
||||
|
||||
// arrays and dicts
|
||||
DOT_GET, // operand: none | stack: [array|dict, index|key] → [value] | unified accessor, returns null if missing
|
||||
|
||||
// strings
|
||||
STR_CONCAT, // operand: value count (number) | stack: [val1, ..., valN] → [string] | concatenate N values
|
||||
|
||||
|
|
|
|||
16
src/vm.ts
16
src/vm.ts
|
|
@ -338,6 +338,22 @@ export class VM {
|
|||
this.stack.push({ type: 'boolean', value: hasDict.value.has(toString(hasKey)) })
|
||||
break
|
||||
|
||||
case OpCode.DOT_GET: {
|
||||
const index = this.stack.pop()!
|
||||
const target = this.stack.pop()!
|
||||
|
||||
if (target.type === 'array')
|
||||
this.stack.push(toValue(target.value?.[Number(index.value)]))
|
||||
|
||||
else if (target.type === 'dict')
|
||||
this.stack.push(toValue(target.value?.get(String(index.value))))
|
||||
|
||||
else
|
||||
throw new Error(`DOT_GET: ${target.type} not supported`)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case OpCode.STR_CONCAT:
|
||||
let count = instruction.operand as number
|
||||
let parts = []
|
||||
|
|
|
|||
|
|
@ -786,6 +786,459 @@ describe("DICT_HAS", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("DOT_GET", () => {
|
||||
test("gets an ARRAY index", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 100
|
||||
PUSH 200
|
||||
PUSH 300
|
||||
PUSH 400
|
||||
MAKE_ARRAY #4
|
||||
PUSH 2
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
expect(await run(bytecode)).toEqual({ type: 'number', value: 300 })
|
||||
})
|
||||
|
||||
test("gets a DICT value", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 'name'
|
||||
PUSH 'Bob'
|
||||
PUSH 'age'
|
||||
PUSH 106
|
||||
MAKE_DICT #2
|
||||
PUSH 'name'
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
expect(await run(bytecode)).toEqual({ type: 'string', value: 'Bob' })
|
||||
})
|
||||
|
||||
test("fails to work on NUMBER", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 500
|
||||
PUSH 0
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
try {
|
||||
await run(bytecode)
|
||||
expect(true).toBe(false) // Should not reach here
|
||||
} catch (e: any) {
|
||||
expect(e.message).toContain('DOT_GET: number not supported')
|
||||
}
|
||||
})
|
||||
|
||||
test("fails to work on STRING", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 'ShrimpVM?'
|
||||
PUSH 1
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
try {
|
||||
await run(bytecode)
|
||||
expect(true).toBe(false) // Should not reach here
|
||||
} catch (e: any) {
|
||||
expect(e.message).toContain('DOT_GET: string not supported')
|
||||
}
|
||||
})
|
||||
|
||||
test("array - returns null for out of bounds", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 10
|
||||
PUSH 20
|
||||
MAKE_ARRAY #2
|
||||
PUSH 5
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
expect(await run(bytecode)).toEqual({ type: 'null', value: null })
|
||||
})
|
||||
|
||||
test("array - negative index", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 10
|
||||
PUSH 20
|
||||
PUSH 30
|
||||
MAKE_ARRAY #3
|
||||
PUSH -1
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
expect(await run(bytecode)).toEqual({ type: 'null', value: null })
|
||||
})
|
||||
|
||||
test("array - first and last element", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 100
|
||||
PUSH 200
|
||||
PUSH 300
|
||||
MAKE_ARRAY #3
|
||||
DUP
|
||||
PUSH 0
|
||||
DOT_GET
|
||||
STORE first
|
||||
PUSH 2
|
||||
DOT_GET
|
||||
STORE last
|
||||
LOAD first
|
||||
LOAD last
|
||||
ADD
|
||||
`)
|
||||
|
||||
expect(await run(bytecode)).toEqual({ type: 'number', value: 400 })
|
||||
})
|
||||
|
||||
test("dict - returns null for missing key", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 'name'
|
||||
PUSH 'Alice'
|
||||
MAKE_DICT #1
|
||||
PUSH 'age'
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
expect(await run(bytecode)).toEqual({ type: 'null', value: null })
|
||||
})
|
||||
|
||||
test("dict - with numeric key", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH '123'
|
||||
PUSH 'value'
|
||||
MAKE_DICT #1
|
||||
PUSH 123
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
expect(await run(bytecode)).toEqual({ type: 'string', value: 'value' })
|
||||
})
|
||||
|
||||
test("array - with string value", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 'foo'
|
||||
PUSH 'bar'
|
||||
PUSH 'baz'
|
||||
MAKE_ARRAY #3
|
||||
PUSH 1
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
expect(await run(bytecode)).toEqual({ type: 'string', value: 'bar' })
|
||||
})
|
||||
|
||||
test("array - with boolean values", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH true
|
||||
PUSH false
|
||||
PUSH true
|
||||
MAKE_ARRAY #3
|
||||
PUSH 1
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
expect(await run(bytecode)).toEqual({ type: 'boolean', value: false })
|
||||
})
|
||||
|
||||
test("dict - with boolean value", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 'active'
|
||||
PUSH true
|
||||
PUSH 'visible'
|
||||
PUSH false
|
||||
MAKE_DICT #2
|
||||
PUSH 'active'
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
expect(await run(bytecode)).toEqual({ type: 'boolean', value: true })
|
||||
})
|
||||
|
||||
test("array - nested arrays", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 1
|
||||
PUSH 2
|
||||
MAKE_ARRAY #2
|
||||
PUSH 3
|
||||
PUSH 4
|
||||
MAKE_ARRAY #2
|
||||
MAKE_ARRAY #2
|
||||
PUSH 1
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result.type).toBe('array')
|
||||
if (result.type === 'array') {
|
||||
expect(result.value).toHaveLength(2)
|
||||
expect(result.value[0]).toEqual({ type: 'number', value: 3 })
|
||||
expect(result.value[1]).toEqual({ type: 'number', value: 4 })
|
||||
}
|
||||
})
|
||||
|
||||
test("dict - nested dicts", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 'user'
|
||||
PUSH 'name'
|
||||
PUSH 'Alice'
|
||||
MAKE_DICT #1
|
||||
MAKE_DICT #1
|
||||
PUSH 'user'
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result.type).toBe('dict')
|
||||
if (result.type === 'dict') {
|
||||
expect(result.value.get('name')).toEqual({ type: 'string', value: 'Alice' })
|
||||
}
|
||||
})
|
||||
|
||||
test("array format", async () => {
|
||||
const bytecode = toBytecode([
|
||||
["PUSH", "apple"],
|
||||
["PUSH", "banana"],
|
||||
["PUSH", "cherry"],
|
||||
["MAKE_ARRAY", 3],
|
||||
["PUSH", 1],
|
||||
["DOT_GET"],
|
||||
["HALT"]
|
||||
])
|
||||
|
||||
expect(await run(bytecode)).toEqual({ type: 'string', value: 'banana' })
|
||||
})
|
||||
|
||||
test("with variables - array", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 10
|
||||
PUSH 20
|
||||
PUSH 30
|
||||
MAKE_ARRAY #3
|
||||
STORE arr
|
||||
PUSH 1
|
||||
STORE idx
|
||||
LOAD arr
|
||||
LOAD idx
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
expect(await run(bytecode)).toEqual({ type: 'number', value: 20 })
|
||||
})
|
||||
|
||||
test("with variables - dict", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 'x'
|
||||
PUSH 100
|
||||
PUSH 'y'
|
||||
PUSH 200
|
||||
MAKE_DICT #2
|
||||
STORE point
|
||||
PUSH 'y'
|
||||
STORE key
|
||||
LOAD point
|
||||
LOAD key
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
expect(await run(bytecode)).toEqual({ type: 'number', value: 200 })
|
||||
})
|
||||
|
||||
test("with expression as index", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 10
|
||||
PUSH 20
|
||||
PUSH 30
|
||||
PUSH 40
|
||||
MAKE_ARRAY #4
|
||||
PUSH 1
|
||||
PUSH 2
|
||||
ADD
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
expect(await run(bytecode)).toEqual({ type: 'number', value: 40 })
|
||||
})
|
||||
|
||||
test("chained access - array of dicts", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 'name'
|
||||
PUSH 'Alice'
|
||||
MAKE_DICT #1
|
||||
PUSH 'name'
|
||||
PUSH 'Bob'
|
||||
MAKE_DICT #1
|
||||
MAKE_ARRAY #2
|
||||
PUSH 1
|
||||
DOT_GET
|
||||
PUSH 'name'
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
expect(await run(bytecode)).toEqual({ type: 'string', value: 'Bob' })
|
||||
})
|
||||
|
||||
test("chained access - dict of arrays", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 'nums'
|
||||
PUSH 1
|
||||
PUSH 2
|
||||
PUSH 3
|
||||
MAKE_ARRAY #3
|
||||
MAKE_DICT #1
|
||||
PUSH 'nums'
|
||||
DOT_GET
|
||||
PUSH 1
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
expect(await run(bytecode)).toEqual({ type: 'number', value: 2 })
|
||||
})
|
||||
|
||||
test("with null value in array", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 10
|
||||
PUSH null
|
||||
PUSH 30
|
||||
MAKE_ARRAY #3
|
||||
PUSH 1
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
expect(await run(bytecode)).toEqual({ type: 'null', value: null })
|
||||
})
|
||||
|
||||
test("with null value in dict", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 'name'
|
||||
PUSH 'Alice'
|
||||
PUSH 'middle'
|
||||
PUSH null
|
||||
MAKE_DICT #2
|
||||
PUSH 'middle'
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
expect(await run(bytecode)).toEqual({ type: 'null', value: null })
|
||||
})
|
||||
|
||||
test("array with regex values", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH /foo/
|
||||
PUSH /bar/i
|
||||
PUSH /baz/g
|
||||
MAKE_ARRAY #3
|
||||
PUSH 1
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result.type).toBe('regex')
|
||||
if (result.type === 'regex') {
|
||||
expect(result.value.source).toBe('bar')
|
||||
expect(result.value.ignoreCase).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
test("dict with regex values", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 'email'
|
||||
PUSH /^[a-z@.]+$/i
|
||||
PUSH 'url'
|
||||
PUSH /^https?:\\/\\//
|
||||
MAKE_DICT #2
|
||||
PUSH 'email'
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result.type).toBe('regex')
|
||||
if (result.type === 'regex') {
|
||||
expect(result.value.source).toBe('^[a-z@.]+$')
|
||||
expect(result.value.ignoreCase).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
test("dict - key coercion from number", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH '0'
|
||||
PUSH 'zero'
|
||||
PUSH '1'
|
||||
PUSH 'one'
|
||||
MAKE_DICT #2
|
||||
PUSH 0
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
expect(await run(bytecode)).toEqual({ type: 'string', value: 'zero' })
|
||||
})
|
||||
|
||||
test("fails on boolean", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH true
|
||||
PUSH 0
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
try {
|
||||
await run(bytecode)
|
||||
expect(true).toBe(false)
|
||||
} catch (e: any) {
|
||||
expect(e.message).toContain('DOT_GET: boolean not supported')
|
||||
}
|
||||
})
|
||||
|
||||
test("fails on null", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH null
|
||||
PUSH 0
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
try {
|
||||
await run(bytecode)
|
||||
expect(true).toBe(false)
|
||||
} catch (e: any) {
|
||||
expect(e.message).toContain('DOT_GET: null not supported')
|
||||
}
|
||||
})
|
||||
|
||||
test("fails on regex", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH /test/
|
||||
PUSH 0
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
try {
|
||||
await run(bytecode)
|
||||
expect(true).toBe(false)
|
||||
} catch (e: any) {
|
||||
expect(e.message).toContain('DOT_GET: regex not supported')
|
||||
}
|
||||
})
|
||||
|
||||
test("empty array access", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
MAKE_ARRAY #0
|
||||
PUSH 0
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
expect(await run(bytecode)).toEqual({ type: 'null', value: null })
|
||||
})
|
||||
|
||||
test("empty dict access", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
MAKE_DICT #0
|
||||
PUSH 'key'
|
||||
DOT_GET
|
||||
`)
|
||||
|
||||
expect(await run(bytecode)).toEqual({ type: 'null', value: null })
|
||||
})
|
||||
})
|
||||
|
||||
describe("STR_CONCAT", () => {
|
||||
test("concats together strings", async () => {
|
||||
const str = `
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user