wip
This commit is contained in:
parent
fe19191246
commit
a53db50b1a
111
CLAUDE.md
111
CLAUDE.md
|
|
@ -35,6 +35,31 @@ Shrimp is a shell-like scripting language that combines command-line simplicity
|
||||||
|
|
||||||
Key references: [Lezer System Guide](https://lezer.codemirror.net/docs/guide/) | [Lezer API](https://lezer.codemirror.net/docs/ref/)
|
Key references: [Lezer System Guide](https://lezer.codemirror.net/docs/guide/) | [Lezer API](https://lezer.codemirror.net/docs/ref/)
|
||||||
|
|
||||||
|
## Reading the Codebase: What to Look For
|
||||||
|
|
||||||
|
When exploring Shrimp, focus on these key files in order:
|
||||||
|
|
||||||
|
1. **src/parser/shrimp.grammar** - Language syntax rules
|
||||||
|
|
||||||
|
- Note the `expressionWithoutIdentifier` pattern and its comment
|
||||||
|
- See how `consumeToTerminator` handles statement-level parsing
|
||||||
|
|
||||||
|
2. **src/parser/tokenizer.ts** - How Identifier vs Word is determined
|
||||||
|
|
||||||
|
- Check the emoji Unicode ranges and surrogate pair handling
|
||||||
|
- See context-aware termination logic (`;`, `)`, `:`)
|
||||||
|
|
||||||
|
3. **src/compiler/compiler.ts** - CST to bytecode transformation
|
||||||
|
|
||||||
|
- See how functions become labels in `fnLabels` map
|
||||||
|
- Check short-circuit logic for `and`/`or` (lines 267-282)
|
||||||
|
- Notice `TRY_CALL` emission for bare identifiers (line 152)
|
||||||
|
|
||||||
|
4. **packages/ReefVM/src/vm.ts** - Bytecode execution
|
||||||
|
- See `TRY_CALL` fall-through to `CALL` (lines 357-375)
|
||||||
|
- Check `TRY_LOAD` string coercion (lines 135-145)
|
||||||
|
- Notice NOSE-style named parameter binding (lines 425-443)
|
||||||
|
|
||||||
## Development Commands
|
## Development Commands
|
||||||
|
|
||||||
### Running Files
|
### Running Files
|
||||||
|
|
@ -141,14 +166,69 @@ function parseExpression(input: string) {
|
||||||
|
|
||||||
**Whitespace-sensitive parsing**: Spaces distinguish operators from identifiers (`x-1` vs `x - 1`). This enables natural shell-like syntax.
|
**Whitespace-sensitive parsing**: Spaces distinguish operators from identifiers (`x-1` vs `x - 1`). This enables natural shell-like syntax.
|
||||||
|
|
||||||
**Identifier vs Word tokenization**: Custom tokenizer determines if a token is an assignable identifier (lowercase/emoji start) or a non-assignable word (paths, URLs). This allows `./file.txt` without quotes.
|
**Identifier vs Word tokenization**: The custom tokenizer (tokenizer.ts) is sophisticated:
|
||||||
|
|
||||||
**Ambiguous identifier resolution**: Bare identifiers like `myVar` could be function calls or variable references. The parser creates `FunctionCallOrIdentifier` nodes, resolved at runtime.
|
- **Surrogate pair handling**: Processes emoji as full Unicode code points (lines 51-65)
|
||||||
|
- **Context-aware termination**: Stops at `;`, `)`, `:` only when followed by whitespace (lines 19-24)
|
||||||
|
- This allows `basename ./cool;` to parse correctly
|
||||||
|
- But `basename ./cool; 2` treats the semicolon as a terminator
|
||||||
|
- **GLR state checking**: Uses `stack.canShift(Word)` to decide whether to track identifier validity
|
||||||
|
- **Permissive Words**: Anything that's not an identifier is a Word (paths, URLs, @mentions, #hashtags)
|
||||||
|
|
||||||
|
**Why this matters**: This complexity is what enables shell-like syntax. Without it, you'd need quotes around `./file.txt` or special handling for paths.
|
||||||
|
|
||||||
|
**Identifier rules**: Must start with lowercase letter or emoji, can contain lowercase, digits, dashes, and emoji.
|
||||||
|
|
||||||
|
**Word rules**: Everything else that isn't whitespace or a delimiter.
|
||||||
|
|
||||||
|
**Ambiguous identifier resolution**: Bare identifiers like `myVar` could be function calls or variable references. The parser creates `FunctionCallOrIdentifier` nodes, resolved at runtime using the `TRY_CALL` opcode.
|
||||||
|
|
||||||
|
**How it works**:
|
||||||
|
|
||||||
|
- The compiler emits `TRY_CALL varname` for bare identifiers (src/compiler/compiler.ts:152)
|
||||||
|
- ReefVM checks if the variable is a function at runtime (vm.ts:357-373)
|
||||||
|
- If it's a function, TRY_CALL intentionally falls through to CALL opcode (no break statement)
|
||||||
|
- If it's not a function or undefined, it pushes the value/string and returns
|
||||||
|
- This runtime resolution enables shell-like "echo hello" without quotes
|
||||||
|
|
||||||
|
**Unbound symbols become strings**: When `TRY_LOAD` encounters an undefined variable, it pushes the variable name as a string (vm.ts:135-145). This is a first-class language feature implemented as a VM opcode, not a parser trick.
|
||||||
|
|
||||||
**Expression-oriented design**: Everything returns a value - commands, assignments, functions. This enables composition and functional patterns.
|
**Expression-oriented design**: Everything returns a value - commands, assignments, functions. This enables composition and functional patterns.
|
||||||
|
|
||||||
**EOF handling**: The grammar uses `(statement | newlineOrSemicolon)+ eof?` to handle empty lines and end-of-file without infinite loops.
|
**EOF handling**: The grammar uses `(statement | newlineOrSemicolon)+ eof?` to handle empty lines and end-of-file without infinite loops.
|
||||||
|
|
||||||
|
## Compiler Architecture
|
||||||
|
|
||||||
|
**Function compilation strategy**: The compiler doesn't create inline function objects. Instead it:
|
||||||
|
|
||||||
|
1. Generates unique labels (`.func_0`, `.func_1`) for each function body (compiler.ts:137)
|
||||||
|
2. Stores function body instructions in `fnLabels` map during compilation
|
||||||
|
3. Appends all function bodies to the end of bytecode with RETURN instructions (compiler.ts:36-41)
|
||||||
|
4. Emits `MAKE_FUNCTION` with parameters and label reference
|
||||||
|
|
||||||
|
This approach keeps the main program linear and allows ReefVM to jump to function bodies by label.
|
||||||
|
|
||||||
|
**Short-circuit logic**: ReefVM has no AND/OR opcodes. The compiler implements short-circuit evaluation using:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// For `a and b`:
|
||||||
|
LOAD a
|
||||||
|
DUP // Duplicate so we can return it if falsy
|
||||||
|
JUMP_IF_FALSE skip // If false, skip evaluating b
|
||||||
|
POP // Remove duplicate if we're continuing
|
||||||
|
LOAD b // Evaluate right side
|
||||||
|
skip:
|
||||||
|
```
|
||||||
|
|
||||||
|
See compiler.ts:267-282 for the full implementation. The `or` operator uses `JUMP_IF_TRUE` instead.
|
||||||
|
|
||||||
|
**If/else compilation**: The compiler uses label-based jumps:
|
||||||
|
|
||||||
|
- `JUMP_IF_FALSE` skips the then-block when condition is false
|
||||||
|
- Each branch ends with `JUMP endLabel` to skip remaining branches
|
||||||
|
- The final label marks where all branches converge
|
||||||
|
- If there's no else branch, compiler emits `PUSH null` as the default value
|
||||||
|
|
||||||
## Grammar Development
|
## Grammar Development
|
||||||
|
|
||||||
### Grammar Structure
|
### Grammar Structure
|
||||||
|
|
@ -206,6 +286,21 @@ The `toMatchTree` helper compares parser output with expected CST structure.
|
||||||
|
|
||||||
**Empty line parsing**: The grammar structure `(statement | newlineOrSemicolon)+ eof?` allows proper empty line and EOF handling.
|
**Empty line parsing**: The grammar structure `(statement | newlineOrSemicolon)+ eof?` allows proper empty line and EOF handling.
|
||||||
|
|
||||||
|
### Why expressionWithoutIdentifier Exists
|
||||||
|
|
||||||
|
The grammar has an unusual pattern: `expressionWithoutIdentifier`. This exists to solve a GLR conflict:
|
||||||
|
|
||||||
|
```
|
||||||
|
consumeToTerminator {
|
||||||
|
ambiguousFunctionCall | // → FunctionCallOrIdentifier → Identifier
|
||||||
|
expression // → Identifier
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Without `expressionWithoutIdentifier`, parsing `my-var` at statement level creates two paths that both want the Identifier token. The grammar comment (shrimp.grammar lines 157-164) explains we "gave up trying to use GLR to fix it."
|
||||||
|
|
||||||
|
**The solution**: Remove Identifier from the `expression` path by creating `expressionWithoutIdentifier`, forcing standalone identifiers through `ambiguousFunctionCall`. This is pragmatic over theoretical purity.
|
||||||
|
|
||||||
## Testing Strategy
|
## Testing Strategy
|
||||||
|
|
||||||
### Parser Tests (`src/parser/parser.test.ts`)
|
### Parser Tests (`src/parser/parser.test.ts`)
|
||||||
|
|
@ -259,3 +354,15 @@ When grammar isn't parsing correctly:
|
||||||
2. **Test simpler cases first** - build up from basic to complex
|
2. **Test simpler cases first** - build up from basic to complex
|
||||||
3. **Use `toMatchTree` output** - see what the parser actually produces
|
3. **Use `toMatchTree` output** - see what the parser actually produces
|
||||||
4. **Check external tokenizer** - identifier vs word logic in `tokenizers.ts`
|
4. **Check external tokenizer** - identifier vs word logic in `tokenizers.ts`
|
||||||
|
|
||||||
|
## Common Misconceptions
|
||||||
|
|
||||||
|
**"The parser handles unbound symbols as strings"** → False. The _VM_ does this via `TRY_LOAD` opcode. The parser creates `FunctionCallOrIdentifier` nodes; the compiler emits `TRY_LOAD`/`TRY_CALL`; the VM resolves at runtime.
|
||||||
|
|
||||||
|
**"Words are just paths"** → False. Words are _anything_ that isn't an identifier. Paths, URLs, `@mentions`, `#hashtags` all parse as Words. The tokenizer accepts any non-whitespace that doesn't match identifier rules.
|
||||||
|
|
||||||
|
**"Functions are first-class values"** → True, but they're compiled to labels, not inline bytecode. The VM creates closures with label references, not embedded instructions.
|
||||||
|
|
||||||
|
**"The grammar is simple"** → False. It has pragmatic workarounds for GLR conflicts (`expressionWithoutIdentifier`), complex EOF handling, and relies heavily on the external tokenizer for correctness.
|
||||||
|
|
||||||
|
**"Short-circuit logic is a VM feature"** → False. It's a compiler pattern using `DUP`, `JUMP_IF_FALSE/TRUE`, and `POP`. The VM has no AND/OR opcodes.
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { parser } from '#parser/shrimp.ts'
|
||||||
import * as terms from '#parser/shrimp.terms'
|
import * as terms from '#parser/shrimp.terms'
|
||||||
import type { SyntaxNode, Tree } from '@lezer/common'
|
import type { SyntaxNode, Tree } from '@lezer/common'
|
||||||
import { assert, errorMessage } from '#utils/utils'
|
import { assert, errorMessage } from '#utils/utils'
|
||||||
import { toBytecode, type Bytecode } from 'reefvm'
|
import { toBytecode, type Bytecode, type ProgramItem } from 'reefvm'
|
||||||
import {
|
import {
|
||||||
checkTreeForErrors,
|
checkTreeForErrors,
|
||||||
getAllChildren,
|
getAllChildren,
|
||||||
|
|
@ -15,9 +15,10 @@ import {
|
||||||
getNamedArgParts,
|
getNamedArgParts,
|
||||||
} from '#compiler/utils'
|
} from '#compiler/utils'
|
||||||
|
|
||||||
|
type Label = `.${string}`
|
||||||
export class Compiler {
|
export class Compiler {
|
||||||
instructions: string[] = []
|
instructions: ProgramItem[] = []
|
||||||
fnLabels = new Map<string, string[]>()
|
fnLabels = new Map<Label, ProgramItem[]>()
|
||||||
ifLabelCount = 0
|
ifLabelCount = 0
|
||||||
bytecode: Bytecode
|
bytecode: Bytecode
|
||||||
|
|
||||||
|
|
@ -34,13 +35,14 @@ export class Compiler {
|
||||||
|
|
||||||
// Add the labels
|
// Add the labels
|
||||||
for (const [label, labelInstructions] of this.fnLabels) {
|
for (const [label, labelInstructions] of this.fnLabels) {
|
||||||
this.instructions.push(`${label}:`)
|
this.instructions.push([`${label}:`])
|
||||||
this.instructions.push(...labelInstructions.map((instr) => ` ${instr}`))
|
this.instructions.push(...labelInstructions)
|
||||||
this.instructions.push(' RETURN')
|
this.instructions.push(['RETURN'])
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log(`\n🤖 instructions:\n----------------\n${this.instructions.join('\n')}\n\n`)
|
// logInstructions(this.instructions)
|
||||||
this.bytecode = toBytecode(this.instructions.join('\n'))
|
|
||||||
|
this.bytecode = toBytecode(this.instructions)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof CompilerError) {
|
if (error instanceof CompilerError) {
|
||||||
throw new Error(error.toReadableString(input))
|
throw new Error(error.toReadableString(input))
|
||||||
|
|
@ -60,46 +62,50 @@ export class Compiler {
|
||||||
child = child.nextSibling
|
child = child.nextSibling
|
||||||
}
|
}
|
||||||
|
|
||||||
this.instructions.push('HALT')
|
this.instructions.push(['HALT'])
|
||||||
}
|
}
|
||||||
|
|
||||||
#compileNode(node: SyntaxNode, input: string): string[] {
|
#compileNode(node: SyntaxNode, input: string): ProgramItem[] {
|
||||||
const value = input.slice(node.from, node.to)
|
const value = input.slice(node.from, node.to)
|
||||||
switch (node.type.id) {
|
switch (node.type.id) {
|
||||||
case terms.Number:
|
case terms.Number:
|
||||||
return [`PUSH ${value}`]
|
const number = Number(value)
|
||||||
|
if (Number.isNaN(number))
|
||||||
|
throw new CompilerError(`Invalid number literal: ${value}`, node.from, node.to)
|
||||||
|
|
||||||
|
return [[`PUSH`, number]]
|
||||||
|
|
||||||
case terms.String:
|
case terms.String:
|
||||||
const strValue = value.slice(1, -1).replace(/\\/g, '')
|
const strValue = value.slice(1, -1).replace(/\\/g, '')
|
||||||
return [`PUSH "${strValue}"`]
|
return [[`PUSH`, strValue]]
|
||||||
|
|
||||||
case terms.Boolean: {
|
case terms.Boolean: {
|
||||||
return [`PUSH ${value}`]
|
return [[`PUSH`, value === 'true']]
|
||||||
}
|
}
|
||||||
|
|
||||||
case terms.Identifier: {
|
case terms.Identifier: {
|
||||||
return [`TRY_LOAD ${value}`]
|
return [[`TRY_LOAD`, value]]
|
||||||
}
|
}
|
||||||
|
|
||||||
case terms.BinOp: {
|
case terms.BinOp: {
|
||||||
const { left, op, right } = getBinaryParts(node)
|
const { left, op, right } = getBinaryParts(node)
|
||||||
const instructions: string[] = []
|
const instructions: ProgramItem[] = []
|
||||||
instructions.push(...this.#compileNode(left, input))
|
instructions.push(...this.#compileNode(left, input))
|
||||||
instructions.push(...this.#compileNode(right, input))
|
instructions.push(...this.#compileNode(right, input))
|
||||||
|
|
||||||
const opValue = input.slice(op.from, op.to)
|
const opValue = input.slice(op.from, op.to)
|
||||||
switch (opValue) {
|
switch (opValue) {
|
||||||
case '+':
|
case '+':
|
||||||
instructions.push('ADD')
|
instructions.push(['ADD'])
|
||||||
break
|
break
|
||||||
case '-':
|
case '-':
|
||||||
instructions.push('SUB')
|
instructions.push(['SUB'])
|
||||||
break
|
break
|
||||||
case '*':
|
case '*':
|
||||||
instructions.push('MUL')
|
instructions.push(['MUL'])
|
||||||
break
|
break
|
||||||
case '/':
|
case '/':
|
||||||
instructions.push('DIV')
|
instructions.push(['DIV'])
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
throw new CompilerError(`Unsupported binary operator: ${opValue}`, op.from, op.to)
|
throw new CompilerError(`Unsupported binary operator: ${opValue}`, op.from, op.to)
|
||||||
|
|
@ -110,10 +116,10 @@ export class Compiler {
|
||||||
|
|
||||||
case terms.Assign: {
|
case terms.Assign: {
|
||||||
const { identifier, right } = getAssignmentParts(node)
|
const { identifier, right } = getAssignmentParts(node)
|
||||||
const instructions: string[] = []
|
const instructions: ProgramItem[] = []
|
||||||
instructions.push(...this.#compileNode(right, input))
|
instructions.push(...this.#compileNode(right, input))
|
||||||
const identifierName = input.slice(identifier.from, identifier.to)
|
const identifierName = input.slice(identifier.from, identifier.to)
|
||||||
instructions.push(`STORE ${identifierName}`)
|
instructions.push(['STORE', identifierName])
|
||||||
|
|
||||||
return instructions
|
return instructions
|
||||||
}
|
}
|
||||||
|
|
@ -127,23 +133,23 @@ export class Compiler {
|
||||||
|
|
||||||
case terms.FunctionDef: {
|
case terms.FunctionDef: {
|
||||||
const { paramNames, bodyNode } = getFunctionDefParts(node, input)
|
const { paramNames, bodyNode } = getFunctionDefParts(node, input)
|
||||||
const instructions: string[] = []
|
const instructions: ProgramItem[] = []
|
||||||
const functionName = `.func_${this.fnLabels.size}`
|
const functionLabel: Label = `.func_${this.fnLabels.size}`
|
||||||
const bodyInstructions: string[] = []
|
const bodyInstructions: ProgramItem[] = []
|
||||||
if (this.fnLabels.has(functionName)) {
|
if (this.fnLabels.has(functionLabel)) {
|
||||||
throw new CompilerError(`Function name collision: ${functionName}`, node.from, node.to)
|
throw new CompilerError(`Function name collision: ${functionLabel}`, node.from, node.to)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fnLabels.set(functionName, bodyInstructions)
|
this.fnLabels.set(functionLabel, bodyInstructions)
|
||||||
|
|
||||||
instructions.push(`MAKE_FUNCTION (${paramNames}) ${functionName}`)
|
instructions.push(['MAKE_FUNCTION', paramNames, functionLabel])
|
||||||
bodyInstructions.push(...this.#compileNode(bodyNode, input))
|
bodyInstructions.push(...this.#compileNode(bodyNode, input))
|
||||||
|
|
||||||
return instructions
|
return instructions
|
||||||
}
|
}
|
||||||
|
|
||||||
case terms.FunctionCallOrIdentifier: {
|
case terms.FunctionCallOrIdentifier: {
|
||||||
return [`TRY_CALL ${value}`]
|
return [['TRY_CALL', value]]
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -161,7 +167,7 @@ export class Compiler {
|
||||||
*/
|
*/
|
||||||
case terms.FunctionCall: {
|
case terms.FunctionCall: {
|
||||||
const { identifierNode, namedArgs, positionalArgs } = getFunctionCallParts(node, input)
|
const { identifierNode, namedArgs, positionalArgs } = getFunctionCallParts(node, input)
|
||||||
const instructions: string[] = []
|
const instructions: ProgramItem[] = []
|
||||||
instructions.push(...this.#compileNode(identifierNode, input))
|
instructions.push(...this.#compileNode(identifierNode, input))
|
||||||
|
|
||||||
positionalArgs.forEach((arg) => {
|
positionalArgs.forEach((arg) => {
|
||||||
|
|
@ -170,13 +176,13 @@ export class Compiler {
|
||||||
|
|
||||||
namedArgs.forEach((arg) => {
|
namedArgs.forEach((arg) => {
|
||||||
const { name, valueNode } = getNamedArgParts(arg, input)
|
const { name, valueNode } = getNamedArgParts(arg, input)
|
||||||
instructions.push(`PUSH "${name}"`)
|
instructions.push(['PUSH', name])
|
||||||
instructions.push(...this.#compileNode(valueNode, input))
|
instructions.push(...this.#compileNode(valueNode, input))
|
||||||
})
|
})
|
||||||
|
|
||||||
instructions.push(`PUSH ${positionalArgs.length}`)
|
instructions.push(['PUSH', positionalArgs.length])
|
||||||
instructions.push(`PUSH ${namedArgs.length}`)
|
instructions.push(['PUSH', namedArgs.length])
|
||||||
instructions.push(`CALL`)
|
instructions.push(['CALL'])
|
||||||
return instructions
|
return instructions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -193,86 +199,84 @@ export class Compiler {
|
||||||
node,
|
node,
|
||||||
input
|
input
|
||||||
)
|
)
|
||||||
const instructions: string[] = []
|
const instructions: ProgramItem[] = []
|
||||||
instructions.push(...this.#compileNode(conditionNode, input))
|
instructions.push(...this.#compileNode(conditionNode, input))
|
||||||
this.ifLabelCount++
|
this.ifLabelCount++
|
||||||
const elseLabel = `.else_${this.ifLabelCount}`
|
const endLabel: Label = `.end_${this.ifLabelCount}`
|
||||||
const endLabel = `.end_${this.ifLabelCount}`
|
|
||||||
|
|
||||||
const thenBlockInstructions = this.#compileNode(thenBlock, input)
|
const thenBlockInstructions = this.#compileNode(thenBlock, input)
|
||||||
instructions.push(`JUMP_IF_FALSE #${thenBlockInstructions.length + 1}`)
|
instructions.push(['JUMP_IF_FALSE', thenBlockInstructions.length + 1])
|
||||||
instructions.push(...thenBlockInstructions)
|
instructions.push(...thenBlockInstructions)
|
||||||
instructions.push(`JUMP ${endLabel}`)
|
instructions.push(['JUMP', endLabel])
|
||||||
|
|
||||||
// Else if
|
// Else if
|
||||||
elseIfBlocks.forEach(({ conditional, thenBlock }, index) => {
|
elseIfBlocks.forEach(({ conditional, thenBlock }) => {
|
||||||
instructions.push(...this.#compileNode(conditional, input))
|
instructions.push(...this.#compileNode(conditional, input))
|
||||||
const elseIfInstructions = this.#compileNode(thenBlock, input)
|
const elseIfInstructions = this.#compileNode(thenBlock, input)
|
||||||
instructions.push(`JUMP_IF_FALSE #${elseIfInstructions.length + 1}`)
|
instructions.push(['JUMP_IF_FALSE', elseIfInstructions.length + 1])
|
||||||
instructions.push(...elseIfInstructions)
|
instructions.push(...elseIfInstructions)
|
||||||
instructions.push(`JUMP ${endLabel}`)
|
instructions.push(['JUMP', endLabel])
|
||||||
})
|
})
|
||||||
|
|
||||||
// Else
|
// Else
|
||||||
instructions.push(`${elseLabel}:`)
|
|
||||||
if (elseThenBlock) {
|
if (elseThenBlock) {
|
||||||
const elseThenInstructions = this.#compileNode(elseThenBlock, input).map((i) => ` ${i}`)
|
const elseThenInstructions = this.#compileNode(elseThenBlock, input)
|
||||||
instructions.push(...elseThenInstructions)
|
instructions.push(...elseThenInstructions)
|
||||||
} else {
|
} else {
|
||||||
instructions.push(` PUSH null`)
|
instructions.push(['PUSH', null])
|
||||||
}
|
}
|
||||||
|
|
||||||
instructions.push(`${endLabel}:`)
|
instructions.push([`${endLabel}:`])
|
||||||
|
|
||||||
return instructions
|
return instructions
|
||||||
}
|
}
|
||||||
|
|
||||||
// - `EQ`, `NEQ`, `LT`, `GT`, `LTE`, `GTE` - Pop 2, push boolean
|
// - `EQ`, `NEQ`, `LT`, `GT`, `LTE`, `GTE` - Pop 2, push boolean
|
||||||
case terms.ConditionalOp: {
|
case terms.ConditionalOp: {
|
||||||
const instructions: string[] = []
|
const instructions: ProgramItem[] = []
|
||||||
const { left, op, right } = getBinaryParts(node)
|
const { left, op, right } = getBinaryParts(node)
|
||||||
const leftInstructions: string[] = this.#compileNode(left, input)
|
const leftInstructions: ProgramItem[] = this.#compileNode(left, input)
|
||||||
const rightInstructions: string[] = this.#compileNode(right, input)
|
const rightInstructions: ProgramItem[] = this.#compileNode(right, input)
|
||||||
|
|
||||||
const opValue = input.slice(op.from, op.to)
|
const opValue = input.slice(op.from, op.to)
|
||||||
switch (opValue) {
|
switch (opValue) {
|
||||||
case '=':
|
case '=':
|
||||||
instructions.push(...leftInstructions, ...rightInstructions, 'EQ')
|
instructions.push(...leftInstructions, ...rightInstructions, ['EQ'])
|
||||||
break
|
break
|
||||||
|
|
||||||
case '!=':
|
case '!=':
|
||||||
instructions.push(...leftInstructions, ...rightInstructions, 'NEQ')
|
instructions.push(...leftInstructions, ...rightInstructions, ['NEQ'])
|
||||||
break
|
break
|
||||||
|
|
||||||
case '<':
|
case '<':
|
||||||
instructions.push(...leftInstructions, ...rightInstructions, 'LT')
|
instructions.push(...leftInstructions, ...rightInstructions, ['LT'])
|
||||||
break
|
break
|
||||||
|
|
||||||
case '>':
|
case '>':
|
||||||
instructions.push(...leftInstructions, ...rightInstructions, 'GT')
|
instructions.push(...leftInstructions, ...rightInstructions, ['GT'])
|
||||||
break
|
break
|
||||||
|
|
||||||
case '<=':
|
case '<=':
|
||||||
instructions.push(...leftInstructions, ...rightInstructions, 'LTE')
|
instructions.push(...leftInstructions, ...rightInstructions, ['LTE'])
|
||||||
break
|
break
|
||||||
|
|
||||||
case '>=':
|
case '>=':
|
||||||
instructions.push(...leftInstructions, ...rightInstructions, 'GTE')
|
instructions.push(...leftInstructions, ...rightInstructions, ['GTE'])
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'and':
|
case 'and':
|
||||||
instructions.push(...leftInstructions)
|
instructions.push(...leftInstructions)
|
||||||
instructions.push('DUP')
|
instructions.push(['DUP'])
|
||||||
instructions.push(`JUMP_IF_FALSE #${rightInstructions.length + 1}`)
|
instructions.push(['JUMP_IF_FALSE', rightInstructions.length + 1])
|
||||||
instructions.push('POP')
|
instructions.push(['POP'])
|
||||||
instructions.push(...rightInstructions)
|
instructions.push(...rightInstructions)
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'or':
|
case 'or':
|
||||||
instructions.push(...leftInstructions)
|
instructions.push(...leftInstructions)
|
||||||
instructions.push('PUSH 9')
|
instructions.push(['DUP'])
|
||||||
instructions.push(`JUMP_IF_TRUE #${rightInstructions.length + 1}`)
|
instructions.push(['JUMP_IF_TRUE', rightInstructions.length + 1])
|
||||||
instructions.push('POP')
|
instructions.push(['POP'])
|
||||||
instructions.push(...rightInstructions)
|
instructions.push(...rightInstructions)
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
@ -289,3 +293,19 @@ export class Compiler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logInstructions = (instructions: ProgramItem[]) => {
|
||||||
|
const instructionsString = instructions
|
||||||
|
.map((parts) => {
|
||||||
|
const isPush = parts[0] === 'PUSH'
|
||||||
|
return parts
|
||||||
|
.map((part, i) => {
|
||||||
|
const partAsString = typeof part == 'string' && isPush ? `'${part}'` : part!.toString()
|
||||||
|
return i > 0 ? partAsString : part
|
||||||
|
})
|
||||||
|
.join(' ')
|
||||||
|
})
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
|
console.log(`\n🤖 instructions:\n----------------\n${instructionsString}\n\n`)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -245,16 +245,17 @@ describe('BinOp', () => {
|
||||||
|
|
||||||
describe('Fn', () => {
|
describe('Fn', () => {
|
||||||
test('parses function no parameters', () => {
|
test('parses function no parameters', () => {
|
||||||
expect('fn: 1').toMatchTree(`
|
expect('fn: 1 end').toMatchTree(`
|
||||||
FunctionDef
|
FunctionDef
|
||||||
keyword fn
|
keyword fn
|
||||||
Params
|
Params
|
||||||
colon :
|
colon :
|
||||||
Number 1`)
|
Number 1
|
||||||
|
end end`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('parses function with single parameter', () => {
|
test('parses function with single parameter', () => {
|
||||||
expect('fn x: x + 1').toMatchTree(`
|
expect('fn x: x + 1 end').toMatchTree(`
|
||||||
FunctionDef
|
FunctionDef
|
||||||
keyword fn
|
keyword fn
|
||||||
Params
|
Params
|
||||||
|
|
@ -263,11 +264,12 @@ describe('Fn', () => {
|
||||||
BinOp
|
BinOp
|
||||||
Identifier x
|
Identifier x
|
||||||
operator +
|
operator +
|
||||||
Number 1`)
|
Number 1
|
||||||
|
end end`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('parses function with multiple parameters', () => {
|
test('parses function with multiple parameters', () => {
|
||||||
expect('fn x y: x * y').toMatchTree(`
|
expect('fn x y: x * y end').toMatchTree(`
|
||||||
FunctionDef
|
FunctionDef
|
||||||
keyword fn
|
keyword fn
|
||||||
Params
|
Params
|
||||||
|
|
@ -277,7 +279,8 @@ describe('Fn', () => {
|
||||||
BinOp
|
BinOp
|
||||||
Identifier x
|
Identifier x
|
||||||
operator *
|
operator *
|
||||||
Identifier y`)
|
Identifier y
|
||||||
|
end end`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('parses multiline function with multiple statements', () => {
|
test('parses multiline function with multiple statements', () => {
|
||||||
|
|
@ -381,7 +384,7 @@ describe('Assign', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('parses assignment with functions', () => {
|
test('parses assignment with functions', () => {
|
||||||
expect('add = fn a b: a + b').toMatchTree(`
|
expect('add = fn a b: a + b end').toMatchTree(`
|
||||||
Assign
|
Assign
|
||||||
Identifier add
|
Identifier add
|
||||||
operator =
|
operator =
|
||||||
|
|
@ -394,7 +397,8 @@ describe('Assign', () => {
|
||||||
BinOp
|
BinOp
|
||||||
Identifier a
|
Identifier a
|
||||||
operator +
|
operator +
|
||||||
Identifier b`)
|
Identifier b
|
||||||
|
end end`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user