STR_CONCAT #n
This commit is contained in:
parent
82e7b181ec
commit
df9af925d3
43
GUIDE.md
43
GUIDE.md
|
|
@ -235,6 +235,9 @@ CALL
|
|||
- `DICT_SET` - Pop value, key, dict; mutate dict
|
||||
- `DICT_HAS` - Pop key and dict, push boolean
|
||||
|
||||
### Strings
|
||||
- `STR_CONCAT #N` - Pop N values, convert to strings, concatenate, push result
|
||||
|
||||
### Exceptions
|
||||
- `PUSH_TRY .catch` - Register exception handler
|
||||
- `PUSH_FINALLY .finally` - Add finally to current handler
|
||||
|
|
@ -431,6 +434,46 @@ TRY_CALL unknown ; Pushes "unknown" as string
|
|||
- Shell-like languages where unknown identifiers become strings
|
||||
- Templating systems with optional transformers
|
||||
|
||||
### String Concatenation
|
||||
Build strings from multiple values:
|
||||
```
|
||||
; Simple concatenation
|
||||
PUSH "Hello"
|
||||
PUSH " "
|
||||
PUSH "World"
|
||||
STR_CONCAT #3 ; → "Hello World"
|
||||
|
||||
; With variables
|
||||
PUSH "Name: "
|
||||
LOAD userName
|
||||
STR_CONCAT #2 ; → "Name: Alice"
|
||||
|
||||
; With expressions and type coercion
|
||||
PUSH "Result: "
|
||||
PUSH 10
|
||||
PUSH 5
|
||||
ADD
|
||||
STR_CONCAT #2 ; → "Result: 15"
|
||||
|
||||
; Template-like interpolation
|
||||
PUSH "User "
|
||||
LOAD userId
|
||||
PUSH " has "
|
||||
LOAD count
|
||||
PUSH " items"
|
||||
STR_CONCAT #5 ; → "User 42 has 3 items"
|
||||
```
|
||||
|
||||
**Composability**: Results can be concatenated again
|
||||
```
|
||||
PUSH "Hello"
|
||||
PUSH " "
|
||||
PUSH "World"
|
||||
STR_CONCAT #3
|
||||
PUSH "!"
|
||||
STR_CONCAT #2 ; → "Hello World!"
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Truthiness
|
||||
|
|
|
|||
50
SPEC.md
50
SPEC.md
|
|
@ -509,6 +509,56 @@ Key is coerced to string.
|
|||
Key is coerced to string.
|
||||
**Errors**: Throws if not dict
|
||||
|
||||
### String Operations
|
||||
|
||||
#### STR_CONCAT
|
||||
**Operand**: Number of values to concatenate (number)
|
||||
**Effect**: Concatenate N values from stack into a single string
|
||||
**Stack**: [val1, val2, ..., valN] → [string]
|
||||
|
||||
**Behavior**:
|
||||
1. Pop N values from stack (in reverse order)
|
||||
2. Convert each value to string using `toString()`
|
||||
3. Concatenate all strings in order (val1 + val2 + ... + valN)
|
||||
4. Push resulting string onto stack
|
||||
|
||||
**Type Coercion**:
|
||||
- Numbers → string representation (e.g., `42` → `"42"`)
|
||||
- Booleans → `"true"` or `"false"`
|
||||
- Null → `"null"`
|
||||
- Strings → identity
|
||||
- Arrays → `"[item, item]"` format
|
||||
- Dicts → `"{key: value, ...}"` format
|
||||
- Functions → `"<function>"`
|
||||
|
||||
**Use Cases**:
|
||||
- Building dynamic strings from multiple parts
|
||||
- Template string interpolation
|
||||
- String formatting with mixed types
|
||||
|
||||
**Composability**:
|
||||
- Results can be concatenated again with additional STR_CONCAT operations
|
||||
- Can leave values on stack (only consumes specified count)
|
||||
|
||||
**Example**:
|
||||
```
|
||||
PUSH "Hello"
|
||||
PUSH " "
|
||||
PUSH "World"
|
||||
STR_CONCAT #3 ; → "Hello World"
|
||||
|
||||
PUSH "Count: "
|
||||
PUSH 42
|
||||
PUSH ", Active: "
|
||||
PUSH true
|
||||
STR_CONCAT #4 ; → "Count: 42, Active: true"
|
||||
```
|
||||
|
||||
**Edge Cases**:
|
||||
- `STR_CONCAT #0` produces empty string `""`
|
||||
- `STR_CONCAT #1` converts single value to string
|
||||
- If stack has fewer values than count, behavior depends on implementation (may use empty strings or throw)
|
||||
|
||||
### TypeScript Interop
|
||||
|
||||
#### CALL_NATIVE
|
||||
|
|
|
|||
|
|
@ -70,6 +70,9 @@ type InstructionTuple =
|
|||
| ["DICT_SET"]
|
||||
| ["DICT_HAS"]
|
||||
|
||||
// Strings
|
||||
| ["STR_CONCAT", number]
|
||||
|
||||
// Native
|
||||
| ["CALL_NATIVE", string]
|
||||
|
||||
|
|
@ -339,6 +342,7 @@ function toBytecodeFromArray(program: ProgramItem[]): Bytecode /* throws */ {
|
|||
|
||||
case "MAKE_ARRAY":
|
||||
case "MAKE_DICT":
|
||||
case "STR_CONCAT":
|
||||
operandValue = operand as number
|
||||
break
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
// strings
|
||||
STR_CONCAT, // operand: value count (number) | stack: [val1, ..., valN] → [string] | concatenate N values
|
||||
|
||||
// typescript interop
|
||||
CALL_NATIVE, // operand: function name (identifier) | stack: [...args] → [result] | consumes entire stack
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ const OPCODES_WITH_OPERANDS = new Set([
|
|||
OpCode.PUSH_FINALLY,
|
||||
OpCode.MAKE_ARRAY,
|
||||
OpCode.MAKE_DICT,
|
||||
OpCode.STR_CONCAT,
|
||||
OpCode.MAKE_FUNCTION,
|
||||
OpCode.CALL_NATIVE,
|
||||
])
|
||||
|
|
@ -91,6 +92,7 @@ const OPCODES_REQUIRING_IMMEDIATE_OR_LABEL = new Set([
|
|||
const OPCODES_REQUIRING_IMMEDIATE = new Set([
|
||||
OpCode.MAKE_ARRAY,
|
||||
OpCode.MAKE_DICT,
|
||||
OpCode.STR_CONCAT,
|
||||
])
|
||||
|
||||
export function validateBytecode(source: string): ValidationResult {
|
||||
|
|
|
|||
10
src/vm.ts
10
src/vm.ts
|
|
@ -334,6 +334,16 @@ export class VM {
|
|||
this.stack.push({ type: 'boolean', value: hasDict.value.has(toString(hasKey)) })
|
||||
break
|
||||
|
||||
case OpCode.STR_CONCAT:
|
||||
let count = instruction.operand as number
|
||||
let parts = []
|
||||
|
||||
while (count-- > 0 && this.stack.length)
|
||||
parts.unshift(toString(this.stack.pop()!))
|
||||
|
||||
this.stack.push(toValue(parts.join('')))
|
||||
break
|
||||
|
||||
case OpCode.MAKE_FUNCTION:
|
||||
const fnDefIdx = instruction.operand as number
|
||||
const fnDef = this.constants[fnDefIdx]
|
||||
|
|
|
|||
|
|
@ -674,6 +674,186 @@ test("DICT_HAS - checks key missing", async () => {
|
|||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false })
|
||||
})
|
||||
|
||||
test("STR_CONCAT - concats together strings", async () => {
|
||||
const str = `
|
||||
PUSH "Hi "
|
||||
PUSH "friend"
|
||||
PUSH "!"
|
||||
STR_CONCAT #3
|
||||
`
|
||||
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Hi friend!" })
|
||||
|
||||
const str2 = `
|
||||
PUSH "Holy smokes!"
|
||||
PUSH "It's "
|
||||
PUSH "alive!"
|
||||
STR_CONCAT #2
|
||||
`
|
||||
|
||||
expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: "It's alive!" })
|
||||
|
||||
|
||||
const str3 = `
|
||||
PUSH 1
|
||||
PUSH " + "
|
||||
PUSH 1
|
||||
PUSH " = "
|
||||
PUSH 1
|
||||
PUSH 1
|
||||
ADD
|
||||
STR_CONCAT #5
|
||||
`
|
||||
|
||||
expect(await run(toBytecode(str3))).toEqual({ type: 'string', value: "1 + 1 = 2" })
|
||||
})
|
||||
|
||||
test("STR_CONCAT - empty concat (count=0)", async () => {
|
||||
const str = `
|
||||
PUSH "leftover"
|
||||
STR_CONCAT #0
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "" })
|
||||
})
|
||||
|
||||
test("STR_CONCAT - single string", async () => {
|
||||
const str = `
|
||||
PUSH "hello"
|
||||
STR_CONCAT #1
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "hello" })
|
||||
})
|
||||
|
||||
test("STR_CONCAT - converts numbers to strings", async () => {
|
||||
const str = `
|
||||
PUSH 42
|
||||
PUSH 100
|
||||
PUSH 7
|
||||
STR_CONCAT #3
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "421007" })
|
||||
})
|
||||
|
||||
test("STR_CONCAT - converts booleans to strings", async () => {
|
||||
const str = `
|
||||
PUSH "Result: "
|
||||
PUSH true
|
||||
STR_CONCAT #2
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Result: true" })
|
||||
|
||||
const str2 = `
|
||||
PUSH false
|
||||
PUSH " is false"
|
||||
STR_CONCAT #2
|
||||
`
|
||||
expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: "false is false" })
|
||||
})
|
||||
|
||||
test("STR_CONCAT - converts null to strings", async () => {
|
||||
const str = `
|
||||
PUSH "Value: "
|
||||
PUSH null
|
||||
STR_CONCAT #2
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Value: null" })
|
||||
})
|
||||
|
||||
test("STR_CONCAT - mixed types", async () => {
|
||||
const str = `
|
||||
PUSH "Count: "
|
||||
PUSH 42
|
||||
PUSH ", Active: "
|
||||
PUSH true
|
||||
PUSH ", Total: "
|
||||
PUSH null
|
||||
STR_CONCAT #6
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Count: 42, Active: true, Total: null" })
|
||||
})
|
||||
|
||||
test("STR_CONCAT - array format", async () => {
|
||||
const bytecode = toBytecode([
|
||||
["PUSH", "Hello"],
|
||||
["PUSH", " "],
|
||||
["PUSH", "World"],
|
||||
["STR_CONCAT", 3],
|
||||
["HALT"]
|
||||
])
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result).toEqual({ type: 'string', value: "Hello World" })
|
||||
})
|
||||
|
||||
test("STR_CONCAT - with variables", async () => {
|
||||
const str = `
|
||||
PUSH "Alice"
|
||||
STORE name
|
||||
PUSH "Hello, "
|
||||
LOAD name
|
||||
PUSH "!"
|
||||
STR_CONCAT #3
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Hello, Alice!" })
|
||||
})
|
||||
|
||||
test("STR_CONCAT - composable (multiple concatenations)", async () => {
|
||||
const str = `
|
||||
PUSH "Hello"
|
||||
PUSH " "
|
||||
PUSH "World"
|
||||
STR_CONCAT #3
|
||||
PUSH "!"
|
||||
STR_CONCAT #2
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Hello World!" })
|
||||
})
|
||||
|
||||
test("STR_CONCAT - with emoji and unicode", async () => {
|
||||
const str = `
|
||||
PUSH "Hello "
|
||||
PUSH "🌍"
|
||||
PUSH "!"
|
||||
STR_CONCAT #3
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Hello 🌍!" })
|
||||
|
||||
const str2 = `
|
||||
PUSH "こんにちは"
|
||||
PUSH "世界"
|
||||
STR_CONCAT #2
|
||||
`
|
||||
expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: "こんにちは世界" })
|
||||
})
|
||||
|
||||
test("STR_CONCAT - with expressions", async () => {
|
||||
const str = `
|
||||
PUSH "Result: "
|
||||
PUSH 10
|
||||
PUSH 5
|
||||
ADD
|
||||
STR_CONCAT #2
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Result: 15" })
|
||||
})
|
||||
|
||||
test("STR_CONCAT - large concat", async () => {
|
||||
const str = `
|
||||
PUSH "a"
|
||||
PUSH "b"
|
||||
PUSH "c"
|
||||
PUSH "d"
|
||||
PUSH "e"
|
||||
PUSH "f"
|
||||
PUSH "g"
|
||||
PUSH "h"
|
||||
PUSH "i"
|
||||
PUSH "j"
|
||||
STR_CONCAT #10
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "abcdefghij" })
|
||||
})
|
||||
|
||||
test("BREAK - throws error when no break target", async () => {
|
||||
// BREAK requires a break target frame on the call stack
|
||||
// A single function call has no previous frame to mark as break target
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user