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_SET` - Pop value, key, dict; mutate dict
|
||||||
- `DICT_HAS` - Pop key and dict, push boolean
|
- `DICT_HAS` - Pop key and dict, push boolean
|
||||||
|
|
||||||
|
### Strings
|
||||||
|
- `STR_CONCAT #N` - Pop N values, convert to strings, concatenate, push result
|
||||||
|
|
||||||
### Exceptions
|
### Exceptions
|
||||||
- `PUSH_TRY .catch` - Register exception handler
|
- `PUSH_TRY .catch` - Register exception handler
|
||||||
- `PUSH_FINALLY .finally` - Add finally to current 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
|
- Shell-like languages where unknown identifiers become strings
|
||||||
- Templating systems with optional transformers
|
- 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
|
## Key Concepts
|
||||||
|
|
||||||
### Truthiness
|
### Truthiness
|
||||||
|
|
|
||||||
50
SPEC.md
50
SPEC.md
|
|
@ -509,6 +509,56 @@ Key is coerced to string.
|
||||||
Key is coerced to string.
|
Key is coerced to string.
|
||||||
**Errors**: Throws if not dict
|
**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
|
### TypeScript Interop
|
||||||
|
|
||||||
#### CALL_NATIVE
|
#### CALL_NATIVE
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,9 @@ type InstructionTuple =
|
||||||
| ["DICT_SET"]
|
| ["DICT_SET"]
|
||||||
| ["DICT_HAS"]
|
| ["DICT_HAS"]
|
||||||
|
|
||||||
|
// Strings
|
||||||
|
| ["STR_CONCAT", number]
|
||||||
|
|
||||||
// Native
|
// Native
|
||||||
| ["CALL_NATIVE", string]
|
| ["CALL_NATIVE", string]
|
||||||
|
|
||||||
|
|
@ -339,6 +342,7 @@ function toBytecodeFromArray(program: ProgramItem[]): Bytecode /* throws */ {
|
||||||
|
|
||||||
case "MAKE_ARRAY":
|
case "MAKE_ARRAY":
|
||||||
case "MAKE_DICT":
|
case "MAKE_DICT":
|
||||||
|
case "STR_CONCAT":
|
||||||
operandValue = operand as number
|
operandValue = operand as number
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,9 @@ export enum OpCode {
|
||||||
DICT_SET, // operand: none | stack: [dict, key, value] → [] | mutates dict
|
DICT_SET, // operand: none | stack: [dict, key, value] → [] | mutates dict
|
||||||
DICT_HAS, // operand: none | stack: [dict, key] → [boolean]
|
DICT_HAS, // operand: none | stack: [dict, key] → [boolean]
|
||||||
|
|
||||||
|
// strings
|
||||||
|
STR_CONCAT, // operand: value count (number) | stack: [val1, ..., valN] → [string] | concatenate N values
|
||||||
|
|
||||||
// typescript interop
|
// typescript interop
|
||||||
CALL_NATIVE, // operand: function name (identifier) | stack: [...args] → [result] | consumes entire stack
|
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.PUSH_FINALLY,
|
||||||
OpCode.MAKE_ARRAY,
|
OpCode.MAKE_ARRAY,
|
||||||
OpCode.MAKE_DICT,
|
OpCode.MAKE_DICT,
|
||||||
|
OpCode.STR_CONCAT,
|
||||||
OpCode.MAKE_FUNCTION,
|
OpCode.MAKE_FUNCTION,
|
||||||
OpCode.CALL_NATIVE,
|
OpCode.CALL_NATIVE,
|
||||||
])
|
])
|
||||||
|
|
@ -91,6 +92,7 @@ const OPCODES_REQUIRING_IMMEDIATE_OR_LABEL = new Set([
|
||||||
const OPCODES_REQUIRING_IMMEDIATE = new Set([
|
const OPCODES_REQUIRING_IMMEDIATE = new Set([
|
||||||
OpCode.MAKE_ARRAY,
|
OpCode.MAKE_ARRAY,
|
||||||
OpCode.MAKE_DICT,
|
OpCode.MAKE_DICT,
|
||||||
|
OpCode.STR_CONCAT,
|
||||||
])
|
])
|
||||||
|
|
||||||
export function validateBytecode(source: string): ValidationResult {
|
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)) })
|
this.stack.push({ type: 'boolean', value: hasDict.value.has(toString(hasKey)) })
|
||||||
break
|
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:
|
case OpCode.MAKE_FUNCTION:
|
||||||
const fnDefIdx = instruction.operand as number
|
const fnDefIdx = instruction.operand as number
|
||||||
const fnDef = this.constants[fnDefIdx]
|
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 })
|
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 () => {
|
test("BREAK - throws error when no break target", async () => {
|
||||||
// BREAK requires a break target frame on the call stack
|
// BREAK requires a break target frame on the call stack
|
||||||
// A single function call has no previous frame to mark as break target
|
// A single function call has no previous frame to mark as break target
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user