Compare commits
20 Commits
1f505e484d
...
b0ad0a0768
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0ad0a0768 | ||
|
|
ccf8f41544 | ||
|
|
6da00dd3c8 | ||
|
|
d4cb86b50b | ||
|
|
c7d4db8528 | ||
|
|
de0d43a1d6 | ||
|
|
4d718ef12b | ||
|
|
4b57728072 | ||
|
|
2c0723b8a3 | ||
|
|
c6e5c44755 | ||
|
|
1d2c85b19c | ||
|
|
52e100cab3 | ||
|
|
5010a9584c | ||
|
|
4b6f6a127f | ||
|
|
38eaed490c | ||
|
|
16cb47ddcc | ||
|
|
a5b2802c0c | ||
|
|
1b80a159c5 | ||
|
|
109c7ff9f6 | ||
| 49919e9f85 |
|
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
import { colors, globals as prelude } from '../src/prelude'
|
import { colors, globals as prelude } from '../src/prelude'
|
||||||
import { treeToString } from '../src/utils/tree'
|
import { treeToString2 } from '../src/utils/tree'
|
||||||
import { runCode, runFile, compileFile, parseCode } from '../src'
|
import { runCode, runFile, compileFile, parseCode } from '../src'
|
||||||
import { resolve } from 'path'
|
import { resolve } from 'path'
|
||||||
import { bytecodeToString } from 'reefvm'
|
import { bytecodeToString } from 'reefvm'
|
||||||
|
|
@ -143,7 +143,7 @@ async function main() {
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
const input = readFileSync(file, 'utf-8')
|
const input = readFileSync(file, 'utf-8')
|
||||||
console.log(treeToString(parseCode(input), input))
|
console.log(treeToString2(parseCode(input), input))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { CompilerError } from '#compiler/compilerError.ts'
|
import { CompilerError } from '#compiler/compilerError.ts'
|
||||||
import { parseToTree as parse } from '#parser/parser2'
|
import { parse } from '#parser/parser2'
|
||||||
import { Tree, SyntaxNode } from '#parser/node'
|
import { SyntaxNode, Tree } from '#parser/node'
|
||||||
|
import { parser } from '#parser/shrimp.ts'
|
||||||
import * as terms from '#parser/shrimp.terms'
|
import * as terms from '#parser/shrimp.terms'
|
||||||
import { setGlobals } from '#parser/tokenizer'
|
import { setGlobals } from '#parser/tokenizer'
|
||||||
|
import { tokenizeCurlyString } from '#parser/curlyTokenizer'
|
||||||
import { assert, errorMessage } from '#utils/utils'
|
import { assert, errorMessage } from '#utils/utils'
|
||||||
import { toBytecode, type Bytecode, type ProgramItem, bytecodeToString } from 'reefvm'
|
import { toBytecode, type Bytecode, type ProgramItem, bytecodeToString } from 'reefvm'
|
||||||
import {
|
import {
|
||||||
|
|
@ -62,7 +64,8 @@ export class Compiler {
|
||||||
constructor(public input: string, globals?: string[] | Record<string, any>) {
|
constructor(public input: string, globals?: string[] | Record<string, any>) {
|
||||||
try {
|
try {
|
||||||
if (globals) setGlobals(Array.isArray(globals) ? globals : Object.keys(globals))
|
if (globals) setGlobals(Array.isArray(globals) ? globals : Object.keys(globals))
|
||||||
const cst = parse(input)
|
const ast = parse(input)
|
||||||
|
const cst = new Tree(ast)
|
||||||
// const errors = checkTreeForErrors(cst)
|
// const errors = checkTreeForErrors(cst)
|
||||||
|
|
||||||
// const firstError = errors[0]
|
// const firstError = errors[0]
|
||||||
|
|
@ -88,8 +91,8 @@ export class Compiler {
|
||||||
}
|
}
|
||||||
|
|
||||||
#compileCst(cst: Tree, input: string) {
|
#compileCst(cst: Tree, input: string) {
|
||||||
const isProgram = cst.topNode.typeId === terms.Program
|
const isProgram = cst.topNode.type.id === terms.Program
|
||||||
assert(isProgram, `Expected Program node, got ${cst.topNode.type}`)
|
assert(isProgram, `Expected Program node, got ${cst.topNode.type.name}`)
|
||||||
|
|
||||||
let child = cst.topNode.firstChild
|
let child = cst.topNode.firstChild
|
||||||
while (child) {
|
while (child) {
|
||||||
|
|
@ -104,7 +107,7 @@ export class Compiler {
|
||||||
const value = input.slice(node.from, node.to)
|
const value = input.slice(node.from, node.to)
|
||||||
if (DEBUG) console.log(`🫦 ${node.name}: ${value}`)
|
if (DEBUG) console.log(`🫦 ${node.name}: ${value}`)
|
||||||
|
|
||||||
switch (node.typeId) {
|
switch (node.type.id) {
|
||||||
case terms.Number:
|
case terms.Number:
|
||||||
// Handle sign prefix for hex, binary, and octal literals
|
// Handle sign prefix for hex, binary, and octal literals
|
||||||
// Number() doesn't parse '-0xFF', '+0xFF', '-0o77', etc. correctly
|
// Number() doesn't parse '-0xFF', '+0xFF', '-0o77', etc. correctly
|
||||||
|
|
@ -123,6 +126,9 @@ export class Compiler {
|
||||||
return [[`PUSH`, numberValue]]
|
return [[`PUSH`, numberValue]]
|
||||||
|
|
||||||
case terms.String: {
|
case terms.String: {
|
||||||
|
if (node.firstChild?.type.id === terms.CurlyString)
|
||||||
|
return this.#compileCurlyString(value, input)
|
||||||
|
|
||||||
const { parts, hasInterpolation } = getStringParts(node, input)
|
const { parts, hasInterpolation } = getStringParts(node, input)
|
||||||
|
|
||||||
// Simple string without interpolation or escapes - extract text directly
|
// Simple string without interpolation or escapes - extract text directly
|
||||||
|
|
@ -137,7 +143,7 @@ export class Compiler {
|
||||||
parts.forEach((part) => {
|
parts.forEach((part) => {
|
||||||
const partValue = input.slice(part.from, part.to)
|
const partValue = input.slice(part.from, part.to)
|
||||||
|
|
||||||
switch (part.typeId) {
|
switch (part.type.id) {
|
||||||
case terms.StringFragment:
|
case terms.StringFragment:
|
||||||
// Plain text fragment - just push as-is
|
// Plain text fragment - just push as-is
|
||||||
instructions.push(['PUSH', partValue])
|
instructions.push(['PUSH', partValue])
|
||||||
|
|
@ -161,7 +167,7 @@ export class Compiler {
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`Unexpected string part: ${part.type}`,
|
`Unexpected string part: ${part.type.name}`,
|
||||||
part.from,
|
part.from,
|
||||||
part.to
|
part.to
|
||||||
)
|
)
|
||||||
|
|
@ -218,7 +224,7 @@ export class Compiler {
|
||||||
instructions.push(['TRY_LOAD', objectName])
|
instructions.push(['TRY_LOAD', objectName])
|
||||||
|
|
||||||
const flattenProperty = (prop: SyntaxNode): void => {
|
const flattenProperty = (prop: SyntaxNode): void => {
|
||||||
if (prop.typeId === terms.DotGet) {
|
if (prop.type.id === terms.DotGet) {
|
||||||
const nestedParts = getDotGetParts(prop, input)
|
const nestedParts = getDotGetParts(prop, input)
|
||||||
|
|
||||||
const nestedObjectValue = input.slice(nestedParts.object.from, nestedParts.object.to)
|
const nestedObjectValue = input.slice(nestedParts.object.from, nestedParts.object.to)
|
||||||
|
|
@ -227,7 +233,7 @@ export class Compiler {
|
||||||
|
|
||||||
flattenProperty(nestedParts.property)
|
flattenProperty(nestedParts.property)
|
||||||
} else {
|
} else {
|
||||||
if (prop.typeId === terms.ParenExpr) {
|
if (prop.type.id === terms.ParenExpr) {
|
||||||
instructions.push(...this.#compileNode(prop, input))
|
instructions.push(...this.#compileNode(prop, input))
|
||||||
} else {
|
} else {
|
||||||
const propertyValue = input.slice(prop.from, prop.to)
|
const propertyValue = input.slice(prop.from, prop.to)
|
||||||
|
|
@ -436,7 +442,7 @@ export class Compiler {
|
||||||
}
|
}
|
||||||
|
|
||||||
case terms.FunctionCallOrIdentifier: {
|
case terms.FunctionCallOrIdentifier: {
|
||||||
if (node.firstChild?.typeId === terms.DotGet) {
|
if (node.firstChild?.type.id === terms.DotGet) {
|
||||||
const instructions: ProgramItem[] = []
|
const instructions: ProgramItem[] = []
|
||||||
const callLabel: Label = `.call_dotget_${++this.labelCount}`
|
const callLabel: Label = `.call_dotget_${++this.labelCount}`
|
||||||
const afterLabel: Label = `.after_dotget_${++this.labelCount}`
|
const afterLabel: Label = `.after_dotget_${++this.labelCount}`
|
||||||
|
|
@ -527,20 +533,20 @@ export class Compiler {
|
||||||
instructions.push([`${fnLabel}:`])
|
instructions.push([`${fnLabel}:`])
|
||||||
instructions.push(
|
instructions.push(
|
||||||
...block
|
...block
|
||||||
.filter((x) => x.type !== 'keyword')
|
.filter((x) => x.type.name !== 'keyword')
|
||||||
.map((x) => this.#compileNode(x!, input))
|
.map((x) => this.#compileNode(x!, input))
|
||||||
.flat()
|
.flat()
|
||||||
)
|
)
|
||||||
instructions.push(['RETURN'])
|
instructions.push(['RETURN'])
|
||||||
instructions.push([`${afterLabel}:`])
|
instructions.push([`${afterLabel}:`])
|
||||||
|
|
||||||
if (fn?.typeId === terms.FunctionCallOrIdentifier) {
|
if (fn?.type.id === terms.FunctionCallOrIdentifier) {
|
||||||
instructions.push(['LOAD', input.slice(fn!.from, fn!.to)])
|
instructions.push(['LOAD', input.slice(fn!.from, fn!.to)])
|
||||||
instructions.push(['MAKE_FUNCTION', [], fnLabel])
|
instructions.push(['MAKE_FUNCTION', [], fnLabel])
|
||||||
instructions.push(['PUSH', 1])
|
instructions.push(['PUSH', 1])
|
||||||
instructions.push(['PUSH', 0])
|
instructions.push(['PUSH', 0])
|
||||||
instructions.push(['CALL'])
|
instructions.push(['CALL'])
|
||||||
} else if (fn?.typeId === terms.FunctionCall) {
|
} else if (fn?.type.id === terms.FunctionCall) {
|
||||||
let body = this.#compileNode(fn!, input)
|
let body = this.#compileNode(fn!, input)
|
||||||
const namedArgCount = (body[body.length - 2]![1] as number) * 2
|
const namedArgCount = (body[body.length - 2]![1] as number) * 2
|
||||||
const startSlice = body.length - namedArgCount - 3
|
const startSlice = body.length - namedArgCount - 3
|
||||||
|
|
@ -733,11 +739,11 @@ export class Compiler {
|
||||||
instructions.push(...this.#compileNode(identifierNode, input))
|
instructions.push(...this.#compileNode(identifierNode, input))
|
||||||
|
|
||||||
const isUnderscoreInPositionalArgs = positionalArgs.some(
|
const isUnderscoreInPositionalArgs = positionalArgs.some(
|
||||||
(arg) => arg.typeId === terms.Underscore
|
(arg) => arg.type.id === terms.Underscore
|
||||||
)
|
)
|
||||||
const isUnderscoreInNamedArgs = namedArgs.some((arg) => {
|
const isUnderscoreInNamedArgs = namedArgs.some((arg) => {
|
||||||
const { valueNode } = getNamedArgParts(arg, input)
|
const { valueNode } = getNamedArgParts(arg, input)
|
||||||
return valueNode.typeId === terms.Underscore
|
return valueNode.type.id === terms.Underscore
|
||||||
})
|
})
|
||||||
|
|
||||||
const shouldPushPositionalArg = !isUnderscoreInPositionalArgs && !isUnderscoreInNamedArgs
|
const shouldPushPositionalArg = !isUnderscoreInPositionalArgs && !isUnderscoreInNamedArgs
|
||||||
|
|
@ -748,7 +754,7 @@ export class Compiler {
|
||||||
}
|
}
|
||||||
|
|
||||||
positionalArgs.forEach((arg) => {
|
positionalArgs.forEach((arg) => {
|
||||||
if (arg.typeId === terms.Underscore) {
|
if (arg.type.id === terms.Underscore) {
|
||||||
instructions.push(['LOAD', pipeValName])
|
instructions.push(['LOAD', pipeValName])
|
||||||
} else {
|
} else {
|
||||||
instructions.push(...this.#compileNode(arg, input))
|
instructions.push(...this.#compileNode(arg, input))
|
||||||
|
|
@ -758,7 +764,7 @@ 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])
|
||||||
if (valueNode.typeId === terms.Underscore) {
|
if (valueNode.type.id === terms.Underscore) {
|
||||||
instructions.push(['LOAD', pipeValName])
|
instructions.push(['LOAD', pipeValName])
|
||||||
} else {
|
} else {
|
||||||
instructions.push(...this.#compileNode(valueNode, input))
|
instructions.push(...this.#compileNode(valueNode, input))
|
||||||
|
|
@ -780,7 +786,7 @@ export class Compiler {
|
||||||
// = can be a valid word, and is also valid inside words, so for now we cheat
|
// = can be a valid word, and is also valid inside words, so for now we cheat
|
||||||
// and check for arrays that look like `[ = ]` to interpret them as
|
// and check for arrays that look like `[ = ]` to interpret them as
|
||||||
// empty dicts
|
// empty dicts
|
||||||
if (children.length === 1 && children[0]!.typeId === terms.Word) {
|
if (children.length === 1 && children[0]!.type.id === terms.Word) {
|
||||||
const child = children[0]!
|
const child = children[0]!
|
||||||
if (input.slice(child.from, child.to) === '=') {
|
if (input.slice(child.from, child.to) === '=') {
|
||||||
return [['MAKE_DICT', 0]]
|
return [['MAKE_DICT', 0]]
|
||||||
|
|
@ -832,8 +838,8 @@ export class Compiler {
|
||||||
case terms.Import: {
|
case terms.Import: {
|
||||||
const instructions: ProgramItem[] = []
|
const instructions: ProgramItem[] = []
|
||||||
const [_import, ...nodes] = getAllChildren(node)
|
const [_import, ...nodes] = getAllChildren(node)
|
||||||
const args = nodes.filter(node => node.typeId === terms.Identifier)
|
const args = nodes.filter(node => node.type.id === terms.Identifier)
|
||||||
const namedArgs = nodes.filter(node => node.typeId === terms.NamedArg)
|
const namedArgs = nodes.filter(node => node.type.id === terms.NamedArg)
|
||||||
|
|
||||||
instructions.push(['LOAD', 'import'])
|
instructions.push(['LOAD', 'import'])
|
||||||
|
|
||||||
|
|
@ -860,7 +866,7 @@ export class Compiler {
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`Compiler doesn't know how to handle a "${node.type}" (${node.typeId}) node.`,
|
`Compiler doesn't know how to handle a "${node.type.name}" (${node.type.id}) node.`,
|
||||||
node.from,
|
node.from,
|
||||||
node.to
|
node.to
|
||||||
)
|
)
|
||||||
|
|
@ -914,4 +920,26 @@ export class Compiler {
|
||||||
|
|
||||||
return instructions
|
return instructions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#compileCurlyString(value: string, input: string): ProgramItem[] {
|
||||||
|
const instructions: ProgramItem[] = []
|
||||||
|
const nodes = tokenizeCurlyString(value)
|
||||||
|
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
if (typeof node === 'string') {
|
||||||
|
instructions.push(['PUSH', node])
|
||||||
|
} else {
|
||||||
|
const [input, topNode] = node
|
||||||
|
let child = topNode.firstChild
|
||||||
|
while (child) {
|
||||||
|
instructions.push(...this.#compileNode(child, input))
|
||||||
|
child = child.nextSibling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
instructions.push(['STR_CONCAT', nodes.length])
|
||||||
|
|
||||||
|
return instructions
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { CompilerError } from '#compiler/compilerError.ts'
|
import { CompilerError } from '#compiler/compilerError.ts'
|
||||||
import type { SyntaxNode, Tree } from '#parser/node'
|
|
||||||
import * as terms from '#parser/shrimp.terms'
|
import * as terms from '#parser/shrimp.terms'
|
||||||
|
import type { SyntaxNode, Tree } from '#parser/node'
|
||||||
|
|
||||||
export const checkTreeForErrors = (tree: Tree): CompilerError[] => {
|
export const checkTreeForErrors = (tree: Tree): CompilerError[] => {
|
||||||
const errors: CompilerError[] = []
|
const errors: CompilerError[] = []
|
||||||
|
|
@ -24,7 +24,7 @@ export const getAllChildren = (node: SyntaxNode): SyntaxNode[] => {
|
||||||
child = child.nextSibling
|
child = child.nextSibling
|
||||||
}
|
}
|
||||||
|
|
||||||
return children.filter((n) => n.typeId !== terms.Comment)
|
return children.filter((n) => n.type.id !== terms.Comment)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getBinaryParts = (node: SyntaxNode) => {
|
export const getBinaryParts = (node: SyntaxNode) => {
|
||||||
|
|
@ -51,14 +51,15 @@ export const getAssignmentParts = (node: SyntaxNode) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// array destructuring
|
// array destructuring
|
||||||
if (left && left.typeId === terms.Array) {
|
if (left && left.type.id === terms.Array) {
|
||||||
const identifiers = getAllChildren(left).filter((child) => child.typeId === terms.Identifier)
|
const identifiers = getAllChildren(left).filter((child) => child.type.id === terms.Identifier)
|
||||||
return { arrayPattern: identifiers, right }
|
return { arrayPattern: identifiers, right }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!left || left.typeId !== terms.AssignableIdentifier) {
|
if (!left || left.type.id !== terms.AssignableIdentifier) {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`Assign left child must be an AssignableIdentifier or Array, got ${left ? left.type : 'none'
|
`Assign left child must be an AssignableIdentifier or Array, got ${
|
||||||
|
left ? left.type.name : 'none'
|
||||||
}`,
|
}`,
|
||||||
node.from,
|
node.from,
|
||||||
node.to
|
node.to
|
||||||
|
|
@ -72,9 +73,10 @@ export const getCompoundAssignmentParts = (node: SyntaxNode) => {
|
||||||
const children = getAllChildren(node)
|
const children = getAllChildren(node)
|
||||||
const [left, operator, right] = children
|
const [left, operator, right] = children
|
||||||
|
|
||||||
if (!left || left.typeId !== terms.AssignableIdentifier) {
|
if (!left || left.type.id !== terms.AssignableIdentifier) {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`CompoundAssign left child must be an AssignableIdentifier, got ${left ? left.type : 'none'
|
`CompoundAssign left child must be an AssignableIdentifier, got ${
|
||||||
|
left ? left.type.name : 'none'
|
||||||
}`,
|
}`,
|
||||||
node.from,
|
node.from,
|
||||||
node.to
|
node.to
|
||||||
|
|
@ -103,9 +105,9 @@ export const getFunctionDefParts = (node: SyntaxNode, input: string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const paramNames = getAllChildren(paramsNode).map((param) => {
|
const paramNames = getAllChildren(paramsNode).map((param) => {
|
||||||
if (param.typeId !== terms.Identifier && param.typeId !== terms.NamedParam) {
|
if (param.type.id !== terms.Identifier && param.type.id !== terms.NamedParam) {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`FunctionDef params must be Identifier or NamedParam, got ${param.type}`,
|
`FunctionDef params must be Identifier or NamedParam, got ${param.type.name}`,
|
||||||
param.from,
|
param.from,
|
||||||
param.to
|
param.to
|
||||||
)
|
)
|
||||||
|
|
@ -122,7 +124,7 @@ export const getFunctionDefParts = (node: SyntaxNode, input: string) => {
|
||||||
let finallyBody: SyntaxNode | undefined
|
let finallyBody: SyntaxNode | undefined
|
||||||
|
|
||||||
for (const child of rest) {
|
for (const child of rest) {
|
||||||
if (child.typeId === terms.CatchExpr) {
|
if (child.type.id === terms.CatchExpr) {
|
||||||
catchExpr = child
|
catchExpr = child
|
||||||
const catchChildren = getAllChildren(child)
|
const catchChildren = getAllChildren(child)
|
||||||
const [_catchKeyword, identifierNode, _colon, body] = catchChildren
|
const [_catchKeyword, identifierNode, _colon, body] = catchChildren
|
||||||
|
|
@ -135,7 +137,7 @@ export const getFunctionDefParts = (node: SyntaxNode, input: string) => {
|
||||||
}
|
}
|
||||||
catchVariable = input.slice(identifierNode.from, identifierNode.to)
|
catchVariable = input.slice(identifierNode.from, identifierNode.to)
|
||||||
catchBody = body
|
catchBody = body
|
||||||
} else if (child.typeId === terms.FinallyExpr) {
|
} else if (child.type.id === terms.FinallyExpr) {
|
||||||
finallyExpr = child
|
finallyExpr = child
|
||||||
const finallyChildren = getAllChildren(child)
|
const finallyChildren = getAllChildren(child)
|
||||||
const [_finallyKeyword, _colon, body] = finallyChildren
|
const [_finallyKeyword, _colon, body] = finallyChildren
|
||||||
|
|
@ -147,7 +149,7 @@ export const getFunctionDefParts = (node: SyntaxNode, input: string) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
finallyBody = body
|
finallyBody = body
|
||||||
} else if (child.type === 'keyword' && input.slice(child.from, child.to) === 'end') {
|
} else if (child.type.name === 'keyword' && input.slice(child.from, child.to) === 'end') {
|
||||||
// Skip the end keyword
|
// Skip the end keyword
|
||||||
} else {
|
} else {
|
||||||
bodyNodes.push(child)
|
bodyNodes.push(child)
|
||||||
|
|
@ -164,9 +166,9 @@ export const getFunctionCallParts = (node: SyntaxNode, input: string) => {
|
||||||
throw new CompilerError(`FunctionCall expected at least 1 child, got 0`, node.from, node.to)
|
throw new CompilerError(`FunctionCall expected at least 1 child, got 0`, node.from, node.to)
|
||||||
}
|
}
|
||||||
|
|
||||||
const namedArgs = args.filter((arg) => arg.typeId === terms.NamedArg)
|
const namedArgs = args.filter((arg) => arg.type.id === terms.NamedArg)
|
||||||
const positionalArgs = args
|
const positionalArgs = args
|
||||||
.filter((arg) => arg.typeId === terms.PositionalArg)
|
.filter((arg) => arg.type.id === terms.PositionalArg)
|
||||||
.map((arg) => {
|
.map((arg) => {
|
||||||
const child = arg.firstChild
|
const child = arg.firstChild
|
||||||
if (!child) throw new CompilerError(`PositionalArg has no child`, arg.from, arg.to)
|
if (!child) throw new CompilerError(`PositionalArg has no child`, arg.from, arg.to)
|
||||||
|
|
@ -207,16 +209,16 @@ export const getIfExprParts = (node: SyntaxNode, input: string) => {
|
||||||
rest.forEach((child) => {
|
rest.forEach((child) => {
|
||||||
const parts = getAllChildren(child)
|
const parts = getAllChildren(child)
|
||||||
|
|
||||||
if (child.typeId === terms.ElseExpr) {
|
if (child.type.id === terms.ElseExpr) {
|
||||||
if (parts.length !== 3) {
|
if (parts.length !== 3) {
|
||||||
const message = `ElseExpr expected 1 child, got ${parts.length}`
|
const message = `ElseExpr expected 1 child, got ${parts.length}`
|
||||||
throw new CompilerError(message, child.from, child.to)
|
throw new CompilerError(message, child.from, child.to)
|
||||||
}
|
}
|
||||||
elseThenBlock = parts.at(-1)
|
elseThenBlock = parts.at(-1)
|
||||||
} else if (child.typeId === terms.ElseIfExpr) {
|
} else if (child.type.id === terms.ElseIfExpr) {
|
||||||
const [_else, _if, conditional, _colon, thenBlock] = parts
|
const [_else, _if, conditional, _colon, thenBlock] = parts
|
||||||
if (!conditional || !thenBlock) {
|
if (!conditional || !thenBlock) {
|
||||||
const names = parts.map((p) => p.type).join(', ')
|
const names = parts.map((p) => p.type.name).join(', ')
|
||||||
const message = `ElseIfExpr expected conditional and thenBlock, got ${names}`
|
const message = `ElseIfExpr expected conditional and thenBlock, got ${names}`
|
||||||
throw new CompilerError(message, child.from, child.to)
|
throw new CompilerError(message, child.from, child.to)
|
||||||
}
|
}
|
||||||
|
|
@ -248,10 +250,10 @@ export const getStringParts = (node: SyntaxNode, input: string) => {
|
||||||
// The text is just between the quotes
|
// The text is just between the quotes
|
||||||
const parts = children.filter((child) => {
|
const parts = children.filter((child) => {
|
||||||
return (
|
return (
|
||||||
child.typeId === terms.StringFragment ||
|
child.type.id === terms.StringFragment ||
|
||||||
child.typeId === terms.Interpolation ||
|
child.type.id === terms.Interpolation ||
|
||||||
child.typeId === terms.EscapeSeq ||
|
child.type.id === terms.EscapeSeq ||
|
||||||
child.typeId === terms.CurlyString
|
child.type.id === terms.CurlyString
|
||||||
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
@ -259,13 +261,13 @@ export const getStringParts = (node: SyntaxNode, input: string) => {
|
||||||
// Validate each part is the expected type
|
// Validate each part is the expected type
|
||||||
parts.forEach((part) => {
|
parts.forEach((part) => {
|
||||||
if (
|
if (
|
||||||
part.typeId !== terms.StringFragment &&
|
part.type.id !== terms.StringFragment &&
|
||||||
part.typeId !== terms.Interpolation &&
|
part.type.id !== terms.Interpolation &&
|
||||||
part.typeId !== terms.EscapeSeq &&
|
part.type.id !== terms.EscapeSeq &&
|
||||||
part.typeId !== terms.CurlyString
|
part.type.id !== terms.CurlyString
|
||||||
) {
|
) {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`String child must be StringFragment, Interpolation, or EscapeSeq, got ${part.type}`,
|
`String child must be StringFragment, Interpolation, or EscapeSeq, got ${part.type.name}`,
|
||||||
part.from,
|
part.from,
|
||||||
part.to
|
part.to
|
||||||
)
|
)
|
||||||
|
|
@ -275,7 +277,7 @@ export const getStringParts = (node: SyntaxNode, input: string) => {
|
||||||
// hasInterpolation means the string has interpolation ($var) or escape sequences (\n)
|
// hasInterpolation means the string has interpolation ($var) or escape sequences (\n)
|
||||||
// A simple string like 'hello' has one StringFragment but no interpolation
|
// A simple string like 'hello' has one StringFragment but no interpolation
|
||||||
const hasInterpolation = parts.some(
|
const hasInterpolation = parts.some(
|
||||||
(p) => p.typeId === terms.Interpolation || p.typeId === terms.EscapeSeq
|
(p) => p.type.id === terms.Interpolation || p.type.id === terms.EscapeSeq
|
||||||
)
|
)
|
||||||
return { parts, hasInterpolation }
|
return { parts, hasInterpolation }
|
||||||
}
|
}
|
||||||
|
|
@ -292,17 +294,17 @@ export const getDotGetParts = (node: SyntaxNode, input: string) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (object.typeId !== terms.IdentifierBeforeDot && object.typeId !== terms.Dollar) {
|
if (object.type.id !== terms.IdentifierBeforeDot && object.type.id !== terms.Dollar) {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`DotGet object must be an IdentifierBeforeDot, got ${object.type}`,
|
`DotGet object must be an IdentifierBeforeDot, got ${object.type.name}`,
|
||||||
object.from,
|
object.from,
|
||||||
object.to
|
object.to
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (![terms.Identifier, terms.Number, terms.ParenExpr, terms.DotGet].includes(property.typeId)) {
|
if (![terms.Identifier, terms.Number, terms.ParenExpr, terms.DotGet].includes(property.type.id)) {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`DotGet property must be an Identifier, Number, ParenExpr, or DotGet, got ${property.type}`,
|
`DotGet property must be an Identifier, Number, ParenExpr, or DotGet, got ${property.type.name}`,
|
||||||
property.from,
|
property.from,
|
||||||
property.to
|
property.to
|
||||||
)
|
)
|
||||||
|
|
@ -334,7 +336,7 @@ export const getTryExprParts = (node: SyntaxNode, input: string) => {
|
||||||
let finallyBody: SyntaxNode | undefined
|
let finallyBody: SyntaxNode | undefined
|
||||||
|
|
||||||
rest.forEach((child) => {
|
rest.forEach((child) => {
|
||||||
if (child.typeId === terms.CatchExpr) {
|
if (child.type.id === terms.CatchExpr) {
|
||||||
catchExpr = child
|
catchExpr = child
|
||||||
const catchChildren = getAllChildren(child)
|
const catchChildren = getAllChildren(child)
|
||||||
const [_catchKeyword, identifierNode, _colon, body] = catchChildren
|
const [_catchKeyword, identifierNode, _colon, body] = catchChildren
|
||||||
|
|
@ -347,7 +349,7 @@ export const getTryExprParts = (node: SyntaxNode, input: string) => {
|
||||||
}
|
}
|
||||||
catchVariable = input.slice(identifierNode.from, identifierNode.to)
|
catchVariable = input.slice(identifierNode.from, identifierNode.to)
|
||||||
catchBody = body
|
catchBody = body
|
||||||
} else if (child.typeId === terms.FinallyExpr) {
|
} else if (child.type.id === terms.FinallyExpr) {
|
||||||
finallyExpr = child
|
finallyExpr = child
|
||||||
const finallyChildren = getAllChildren(child)
|
const finallyChildren = getAllChildren(child)
|
||||||
const [_finallyKeyword, _colon, body] = finallyChildren
|
const [_finallyKeyword, _colon, body] = finallyChildren
|
||||||
|
|
|
||||||
14
src/index.ts
14
src/index.ts
|
|
@ -1,15 +1,15 @@
|
||||||
import { readFileSync } from 'fs'
|
import { readFileSync } from 'fs'
|
||||||
import { VM, fromValue, toValue, isValue, type Bytecode } from 'reefvm'
|
import { VM, fromValue, toValue, isValue, type Bytecode } from 'reefvm'
|
||||||
import { type Tree } from '@lezer/common'
|
|
||||||
import { Compiler } from '#compiler/compiler'
|
import { Compiler } from '#compiler/compiler'
|
||||||
import { parser } from '#parser/shrimp'
|
import { parse } from '#parser/parser2'
|
||||||
|
import { type SyntaxNode, Tree } from '#parser/node'
|
||||||
import { globals as parserGlobals, setGlobals as setParserGlobals } from '#parser/tokenizer'
|
import { globals as parserGlobals, setGlobals as setParserGlobals } from '#parser/tokenizer'
|
||||||
import { globals as prelude } from '#prelude'
|
import { globals as prelude } from '#prelude'
|
||||||
|
|
||||||
export { Compiler } from '#compiler/compiler'
|
export { Compiler } from '#compiler/compiler'
|
||||||
export { parser } from '#parser/shrimp'
|
export { parse } from '#parser/parser2'
|
||||||
|
export { type SyntaxNode, Tree } from '#parser/node'
|
||||||
export { globals as prelude } from '#prelude'
|
export { globals as prelude } from '#prelude'
|
||||||
export type { Tree } from '@lezer/common'
|
|
||||||
export { type Value, type Bytecode } from 'reefvm'
|
export { type Value, type Bytecode } from 'reefvm'
|
||||||
export { toValue, fromValue, isValue, Scope, VM, bytecodeToString } from 'reefvm'
|
export { toValue, fromValue, isValue, Scope, VM, bytecodeToString } from 'reefvm'
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@ export class Shrimp {
|
||||||
return isValue(result) ? fromValue(result, this.vm) : result
|
return isValue(result) ? fromValue(result, this.vm) : result
|
||||||
}
|
}
|
||||||
|
|
||||||
parse(code: string): Tree {
|
parse(code: string): SyntaxNode {
|
||||||
return parseCode(code, this.globals)
|
return parseCode(code, this.globals)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,8 +105,8 @@ export function parseCode(code: string, globals?: Record<string, any>): Tree {
|
||||||
const globalNames = [...Object.keys(prelude), ...(globals ? Object.keys(globals) : [])]
|
const globalNames = [...Object.keys(prelude), ...(globals ? Object.keys(globals) : [])]
|
||||||
|
|
||||||
setParserGlobals(globalNames)
|
setParserGlobals(globalNames)
|
||||||
const result = parser.parse(code)
|
const result = parse(code)
|
||||||
setParserGlobals(oldGlobals)
|
setParserGlobals(oldGlobals)
|
||||||
|
|
||||||
return result
|
return new Tree(result)
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { parser } from '#parser/shrimp.ts'
|
import { parser } from '#parser/shrimp.ts'
|
||||||
import type { SyntaxNode } from '@lezer/common'
|
import { parse } from '#parser/parser2'
|
||||||
|
import type { SyntaxNode } from '#parser/node'
|
||||||
import { isIdentStart, isIdentChar } from './tokenizer'
|
import { isIdentStart, isIdentChar } from './tokenizer'
|
||||||
|
|
||||||
// Turns a { curly string } into strings and nodes for interpolation
|
// Turns a { curly string } into strings and nodes for interpolation
|
||||||
|
|
@ -37,7 +38,7 @@ export const tokenizeCurlyString = (value: string): (string | [string, SyntaxNod
|
||||||
}
|
}
|
||||||
|
|
||||||
const input = value.slice(start + 2, pos) // skip '$('
|
const input = value.slice(start + 2, pos) // skip '$('
|
||||||
tokens.push([input, parser.parse(input).topNode])
|
tokens.push([input, parse(input)])
|
||||||
start = ++pos // skip ')'
|
start = ++pos // skip ')'
|
||||||
} else {
|
} else {
|
||||||
char = value[++pos]
|
char = value[++pos]
|
||||||
|
|
@ -48,7 +49,7 @@ export const tokenizeCurlyString = (value: string): (string | [string, SyntaxNod
|
||||||
char = value[++pos]
|
char = value[++pos]
|
||||||
|
|
||||||
const input = value.slice(start + 1, pos) // skip '$'
|
const input = value.slice(start + 1, pos) // skip '$'
|
||||||
tokens.push([input, parser.parse(input).topNode])
|
tokens.push([input, parse(input)])
|
||||||
start = pos-- // backtrack and start over
|
start = pos-- // backtrack and start over
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { type Token, TokenType } from './tokenizer2'
|
import { type Token, TokenType } from './tokenizer2'
|
||||||
import { nameToId } from './terms'
|
import * as term from './shrimp.terms'
|
||||||
|
|
||||||
export type NodeType =
|
export type NodeType =
|
||||||
| 'Program'
|
| 'Program'
|
||||||
|
|
@ -58,6 +58,7 @@ export type NodeType =
|
||||||
|
|
||||||
| 'Import'
|
| 'Import'
|
||||||
| 'Do'
|
| 'Do'
|
||||||
|
| 'Underscore'
|
||||||
| 'colon'
|
| 'colon'
|
||||||
| 'keyword'
|
| 'keyword'
|
||||||
| 'operator'
|
| 'operator'
|
||||||
|
|
@ -113,32 +114,224 @@ export const operators: Record<string, any> = {
|
||||||
|
|
||||||
export class Tree {
|
export class Tree {
|
||||||
constructor(public topNode: SyntaxNode) { }
|
constructor(public topNode: SyntaxNode) { }
|
||||||
|
|
||||||
|
get length(): number {
|
||||||
|
return this.topNode.to
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor() {
|
||||||
|
return {
|
||||||
|
type: this.topNode.type,
|
||||||
|
from: this.topNode.from,
|
||||||
|
to: this.topNode.to,
|
||||||
|
node: this.topNode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: TEMPORARY SHIM
|
||||||
|
class SyntaxNodeType {
|
||||||
|
constructor(public nodeType: NodeType) { }
|
||||||
|
|
||||||
|
is(other: string) {
|
||||||
|
return this.nodeType === other
|
||||||
|
}
|
||||||
|
|
||||||
|
get id(): number {
|
||||||
|
switch (this.nodeType) {
|
||||||
|
case 'Program':
|
||||||
|
return term.Program
|
||||||
|
|
||||||
|
case 'Block':
|
||||||
|
return term.Block
|
||||||
|
|
||||||
|
case 'FunctionCall':
|
||||||
|
return term.FunctionCall
|
||||||
|
|
||||||
|
case 'FunctionCallOrIdentifier':
|
||||||
|
return term.FunctionCallOrIdentifier
|
||||||
|
|
||||||
|
case 'FunctionCallWithBlock':
|
||||||
|
return term.FunctionCallWithBlock
|
||||||
|
|
||||||
|
case 'PositionalArg':
|
||||||
|
return term.PositionalArg
|
||||||
|
|
||||||
|
case 'NamedArg':
|
||||||
|
return term.NamedArg
|
||||||
|
|
||||||
|
case 'FunctionDef':
|
||||||
|
return term.FunctionDef
|
||||||
|
|
||||||
|
case 'Params':
|
||||||
|
return term.Params
|
||||||
|
|
||||||
|
case 'NamedParam':
|
||||||
|
return term.NamedParam
|
||||||
|
|
||||||
|
case 'Null':
|
||||||
|
return term.Null
|
||||||
|
|
||||||
|
case 'Boolean':
|
||||||
|
return term.Boolean
|
||||||
|
|
||||||
|
case 'Number':
|
||||||
|
return term.Number
|
||||||
|
|
||||||
|
case 'String':
|
||||||
|
return term.String
|
||||||
|
|
||||||
|
case 'StringFragment':
|
||||||
|
return term.StringFragment
|
||||||
|
|
||||||
|
case 'CurlyString':
|
||||||
|
return term.CurlyString
|
||||||
|
|
||||||
|
case 'DoubleQuote':
|
||||||
|
return term.DoubleQuote
|
||||||
|
|
||||||
|
case 'EscapeSeq':
|
||||||
|
return term.EscapeSeq
|
||||||
|
|
||||||
|
case 'Interpolation':
|
||||||
|
return term.Interpolation
|
||||||
|
|
||||||
|
case 'Regex':
|
||||||
|
return term.Regex
|
||||||
|
|
||||||
|
case 'Identifier':
|
||||||
|
return term.Identifier
|
||||||
|
|
||||||
|
case 'AssignableIdentifier':
|
||||||
|
return term.AssignableIdentifier
|
||||||
|
|
||||||
|
case 'IdentifierBeforeDot':
|
||||||
|
return term.IdentifierBeforeDot
|
||||||
|
|
||||||
|
case 'Word':
|
||||||
|
return term.Word
|
||||||
|
|
||||||
|
case 'Array':
|
||||||
|
return term.Array
|
||||||
|
|
||||||
|
case 'Dict':
|
||||||
|
return term.Dict
|
||||||
|
|
||||||
|
case 'Comment':
|
||||||
|
return term.Comment
|
||||||
|
|
||||||
|
case 'BinOp':
|
||||||
|
return term.BinOp
|
||||||
|
|
||||||
|
case 'ConditionalOp':
|
||||||
|
return term.ConditionalOp
|
||||||
|
|
||||||
|
case 'ParenExpr':
|
||||||
|
return term.ParenExpr
|
||||||
|
|
||||||
|
case 'Assign':
|
||||||
|
return term.Assign
|
||||||
|
|
||||||
|
case 'CompoundAssign':
|
||||||
|
return term.CompoundAssign
|
||||||
|
|
||||||
|
case 'DotGet':
|
||||||
|
return term.DotGet
|
||||||
|
|
||||||
|
case 'PipeExpr':
|
||||||
|
return term.PipeExpr
|
||||||
|
|
||||||
|
case 'IfExpr':
|
||||||
|
return term.IfExpr
|
||||||
|
|
||||||
|
case 'ElseIfExpr':
|
||||||
|
return term.ElseIfExpr
|
||||||
|
|
||||||
|
case 'ElseExpr':
|
||||||
|
return term.ElseExpr
|
||||||
|
|
||||||
|
case 'WhileExpr':
|
||||||
|
return term.WhileExpr
|
||||||
|
|
||||||
|
case 'TryExpr':
|
||||||
|
return term.TryExpr
|
||||||
|
|
||||||
|
case 'CatchExpr':
|
||||||
|
return term.CatchExpr
|
||||||
|
|
||||||
|
case 'FinallyExpr':
|
||||||
|
return term.FinallyExpr
|
||||||
|
|
||||||
|
case 'Throw':
|
||||||
|
return term.Throw
|
||||||
|
|
||||||
|
case 'Eq':
|
||||||
|
return term.Eq
|
||||||
|
|
||||||
|
case 'Modulo':
|
||||||
|
return term.Modulo
|
||||||
|
|
||||||
|
case 'Plus':
|
||||||
|
return term.Plus
|
||||||
|
|
||||||
|
case 'Star':
|
||||||
|
return term.Star
|
||||||
|
|
||||||
|
case 'Slash':
|
||||||
|
return term.Slash
|
||||||
|
|
||||||
|
case 'Import':
|
||||||
|
return term.Import
|
||||||
|
|
||||||
|
case 'Do':
|
||||||
|
return term.Do
|
||||||
|
|
||||||
|
case 'Underscore':
|
||||||
|
return term.Underscore
|
||||||
|
|
||||||
|
case 'colon':
|
||||||
|
return term.colon
|
||||||
|
|
||||||
|
case 'keyword':
|
||||||
|
return term.keyword
|
||||||
|
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
get name(): string {
|
||||||
|
return this.nodeType
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SyntaxNode {
|
export class SyntaxNode {
|
||||||
type: NodeType
|
#type: NodeType
|
||||||
from: number
|
from: number
|
||||||
to: number
|
to: number
|
||||||
parent: SyntaxNode | null
|
parent: SyntaxNode | null
|
||||||
children: SyntaxNode[] = []
|
children: SyntaxNode[] = []
|
||||||
|
|
||||||
constructor(type: NodeType, from: number, to: number, parent: SyntaxNode | null = null) {
|
constructor(type: NodeType, from: number, to: number, parent: SyntaxNode | null = null) {
|
||||||
this.type = type
|
this.#type = type
|
||||||
this.from = from
|
this.from = from
|
||||||
this.to = to
|
this.to = to
|
||||||
this.parent = parent
|
this.parent = parent
|
||||||
}
|
}
|
||||||
|
|
||||||
get typeId(): number {
|
|
||||||
return nameToId(this.type)
|
|
||||||
}
|
|
||||||
|
|
||||||
static from(token: Token, parent?: SyntaxNode): SyntaxNode {
|
static from(token: Token, parent?: SyntaxNode): SyntaxNode {
|
||||||
return new SyntaxNode(TokenType[token.type] as NodeType, token.from, token.to, parent ?? null)
|
return new SyntaxNode(TokenType[token.type] as NodeType, token.from, token.to, parent ?? null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get type(): SyntaxNodeType {
|
||||||
|
return new SyntaxNodeType(this.#type)
|
||||||
|
}
|
||||||
|
|
||||||
|
set type(name: NodeType) {
|
||||||
|
this.#type = name
|
||||||
|
}
|
||||||
|
|
||||||
get name(): string {
|
get name(): string {
|
||||||
return this.type
|
return this.type.name
|
||||||
}
|
}
|
||||||
|
|
||||||
get isError(): boolean {
|
get isError(): boolean {
|
||||||
|
|
@ -179,7 +372,7 @@ export class SyntaxNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
toString(): string {
|
toString(): string {
|
||||||
return this.type
|
return this.type.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,7 +393,7 @@ export const precedence: Record<string, number> = {
|
||||||
// Nullish coalescing
|
// Nullish coalescing
|
||||||
'??': 35,
|
'??': 35,
|
||||||
|
|
||||||
// Bitwise shift (lower precedence than addition)
|
// Bitwise shifts (lower precedence than addition)
|
||||||
'<<': 37,
|
'<<': 37,
|
||||||
'>>': 37,
|
'>>': 37,
|
||||||
'>>>': 37,
|
'>>>': 37,
|
||||||
|
|
@ -209,7 +402,7 @@ export const precedence: Record<string, number> = {
|
||||||
'+': 40,
|
'+': 40,
|
||||||
'-': 40,
|
'-': 40,
|
||||||
|
|
||||||
// Bitwise AND/OR/XOR (between addition and multiplication)
|
// Bitwise AND/OR/XOR (higher precedence than addition)
|
||||||
'band': 45,
|
'band': 45,
|
||||||
'bor': 45,
|
'bor': 45,
|
||||||
'bxor': 45,
|
'bxor': 45,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Scanner, type Token, TokenType } from './tokenizer2'
|
import { Scanner, type Token, TokenType } from './tokenizer2'
|
||||||
import { Tree, SyntaxNode, operators, precedence, conditionals, compounds } from './node'
|
import { SyntaxNode, operators, precedence, conditionals, compounds } from './node'
|
||||||
import { globals } from './tokenizer'
|
import { globals } from './tokenizer'
|
||||||
import { parseString } from './stringParser'
|
import { parseString } from './stringParser'
|
||||||
|
|
||||||
|
|
@ -10,10 +10,6 @@ export const parse = (input: string): SyntaxNode => {
|
||||||
return parser.parse(input)
|
return parser.parse(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parseToTree = (input: string): Tree => {
|
|
||||||
return new Tree(parse(input))
|
|
||||||
}
|
|
||||||
|
|
||||||
class Scope {
|
class Scope {
|
||||||
parent?: Scope
|
parent?: Scope
|
||||||
set = new Set<string>()
|
set = new Set<string>()
|
||||||
|
|
@ -122,19 +118,17 @@ export class Parser {
|
||||||
expr = this.exprWithPrecedence()
|
expr = this.exprWithPrecedence()
|
||||||
|
|
||||||
// check for destructuring
|
// check for destructuring
|
||||||
if (expr.type === 'Array' && this.is($T.Operator, '='))
|
if (expr.type.is('Array') && this.is($T.Operator, '='))
|
||||||
return this.destructure(expr)
|
return this.destructure(expr)
|
||||||
|
|
||||||
// check for parens function call
|
// check for parens function call
|
||||||
// ex: (ref my-func) my-arg
|
// ex: (ref my-func) my-arg
|
||||||
// but not if followed by operator: (x) + 1
|
if (expr.type.is('ParenExpr') && !this.isExprEnd())
|
||||||
if (expr.type === 'ParenExpr' && !this.isExprEnd() && !this.is($T.Operator))
|
|
||||||
expr = this.functionCall(expr)
|
expr = this.functionCall(expr)
|
||||||
|
|
||||||
// if there's an operator (not pipe), continue with precedence parsing
|
// if dotget is followed by binary operator, continue parsing as binary expression
|
||||||
if (this.is($T.Operator) && !this.isPipe()) {
|
if (expr.type.is('DotGet') && this.is($T.Operator) && !this.is($T.Operator, '|'))
|
||||||
expr = this.continueWithPrecedence(expr)
|
expr = this.dotGetBinOp(expr)
|
||||||
}
|
|
||||||
|
|
||||||
// one | echo
|
// one | echo
|
||||||
if (allowPipe && this.isPipe())
|
if (allowPipe && this.isPipe())
|
||||||
|
|
@ -145,33 +139,6 @@ export class Parser {
|
||||||
return expr
|
return expr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue parsing with precedence after we already have a left side
|
|
||||||
continueWithPrecedence(left: SyntaxNode, minBp = 0): SyntaxNode {
|
|
||||||
while (this.is($T.Operator)) {
|
|
||||||
const op = this.current().value!
|
|
||||||
const bp = precedence[op]
|
|
||||||
|
|
||||||
// operator has lower precedence than required, stop
|
|
||||||
if (bp === undefined || bp < minBp) break
|
|
||||||
|
|
||||||
const opNode = this.op()
|
|
||||||
|
|
||||||
// right-associative operators (like **) use same bp, others use bp + 1
|
|
||||||
const nextMinBp = op === '**' ? bp : bp + 1
|
|
||||||
|
|
||||||
// parse right-hand side with higher precedence
|
|
||||||
const right = this.exprWithPrecedence(nextMinBp)
|
|
||||||
|
|
||||||
const nodeType = conditionals.has(op) ? 'ConditionalOp' : 'BinOp'
|
|
||||||
const node = new SyntaxNode(nodeType, left.from, right.to)
|
|
||||||
|
|
||||||
node.push(left, opNode, right)
|
|
||||||
left = node
|
|
||||||
}
|
|
||||||
|
|
||||||
return left
|
|
||||||
}
|
|
||||||
|
|
||||||
// piping | stuff | is | cool
|
// piping | stuff | is | cool
|
||||||
pipe(left: SyntaxNode): SyntaxNode {
|
pipe(left: SyntaxNode): SyntaxNode {
|
||||||
const canLookPastNewlines = this.inParens === 0
|
const canLookPastNewlines = this.inParens === 0
|
||||||
|
|
@ -278,6 +245,22 @@ export class Parser {
|
||||||
// parse specific nodes
|
// parse specific nodes
|
||||||
//
|
//
|
||||||
|
|
||||||
|
// raw determines whether we just want the SyntaxNodes or we want to
|
||||||
|
// wrap them in a PositionalArg
|
||||||
|
arg(raw = false): SyntaxNode {
|
||||||
|
// 'do' is a special function arg - it doesn't need to be wrapped
|
||||||
|
// in parens. otherwise, args are regular value()s
|
||||||
|
const val = this.is($T.Keyword, 'do') ? this.do() : this.value()
|
||||||
|
|
||||||
|
if (raw) {
|
||||||
|
return val
|
||||||
|
} else {
|
||||||
|
const arg = new SyntaxNode('PositionalArg', val.from, val.to)
|
||||||
|
arg.add(val)
|
||||||
|
return arg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// [ 1 2 3 ]
|
// [ 1 2 3 ]
|
||||||
array(): SyntaxNode {
|
array(): SyntaxNode {
|
||||||
const open = this.expect($T.OpenBracket)
|
const open = this.expect($T.OpenBracket)
|
||||||
|
|
@ -358,7 +341,7 @@ export class Parser {
|
||||||
}
|
}
|
||||||
|
|
||||||
// atoms are the basic building blocks: literals, identifiers, words
|
// atoms are the basic building blocks: literals, identifiers, words
|
||||||
atom() {
|
atom(): SyntaxNode {
|
||||||
if (this.is($T.String))
|
if (this.is($T.String))
|
||||||
return this.string()
|
return this.string()
|
||||||
|
|
||||||
|
|
@ -462,15 +445,7 @@ export class Parser {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.is($T.NamedArgPrefix)) {
|
values.push(this.is($T.NamedArgPrefix) ? this.namedArg() : this.arg())
|
||||||
const prefix = SyntaxNode.from(this.next())
|
|
||||||
const val = this.is($T.Keyword, 'do') ? this.do() : this.value()
|
|
||||||
const arg = new SyntaxNode('NamedArg', prefix.from, val.to)
|
|
||||||
arg.push(prefix, val)
|
|
||||||
values.push(arg)
|
|
||||||
} else {
|
|
||||||
values.push(this.value())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const close = this.expect($T.CloseBracket)
|
const close = this.expect($T.CloseBracket)
|
||||||
|
|
@ -549,7 +524,7 @@ export class Parser {
|
||||||
if (!this.scope.has(ident))
|
if (!this.scope.has(ident))
|
||||||
return this.word(left)
|
return this.word(left)
|
||||||
|
|
||||||
if (left.type === 'Identifier') left.type = 'IdentifierBeforeDot'
|
if (left.type.is('Identifier')) left.type = 'IdentifierBeforeDot'
|
||||||
|
|
||||||
let parts = []
|
let parts = []
|
||||||
while (this.is($T.Operator, '.')) {
|
while (this.is($T.Operator, '.')) {
|
||||||
|
|
@ -564,19 +539,37 @@ export class Parser {
|
||||||
return nodes ? node.push(left, nodes!) : node.push(left, ...parts)
|
return nodes ? node.push(left, nodes!) : node.push(left, ...parts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// continue parsing dotget/word binary operation
|
||||||
|
dotGetBinOp(left: SyntaxNode): SyntaxNode {
|
||||||
|
while (this.is($T.Operator) && !this.is($T.Operator, '|')) {
|
||||||
|
const op = this.current().value!
|
||||||
|
const bp = precedence[op]
|
||||||
|
if (bp === undefined) break
|
||||||
|
|
||||||
|
const opNode = this.op()
|
||||||
|
const right = this.exprWithPrecedence(bp + 1)
|
||||||
|
|
||||||
|
const nodeType = conditionals.has(op) ? 'ConditionalOp' : 'BinOp'
|
||||||
|
const node = new SyntaxNode(nodeType, left.from, right.to)
|
||||||
|
node.push(left, opNode, right)
|
||||||
|
left = node
|
||||||
|
}
|
||||||
|
return left
|
||||||
|
}
|
||||||
|
|
||||||
// dotget in a statement/expression (something.blah) or (something.blah arg1)
|
// dotget in a statement/expression (something.blah) or (something.blah arg1)
|
||||||
dotGetFunctionCall(): SyntaxNode {
|
dotGetFunctionCall(): SyntaxNode {
|
||||||
const dotGet = this.dotGet()
|
const dotGet = this.dotGet()
|
||||||
|
|
||||||
// dotget not in scope, regular Word
|
// if followed by a binary operator (not pipe), return dotGet/Word as-is for expression parser
|
||||||
if (dotGet.type === 'Word') return dotGet
|
if (this.is($T.Operator) && !this.is($T.Operator, '|'))
|
||||||
|
|
||||||
if (this.is($T.Operator) && !this.isPipe())
|
|
||||||
return dotGet
|
return dotGet
|
||||||
|
|
||||||
else if (this.isPipe() || this.isExprEnd())
|
// dotget not in scope, regular Word
|
||||||
return this.functionCallOrIdentifier(dotGet)
|
if (dotGet.type.is('Word')) return dotGet
|
||||||
|
|
||||||
|
if (this.isExprEnd())
|
||||||
|
return this.functionCallOrIdentifier(dotGet)
|
||||||
else
|
else
|
||||||
return this.functionCall(dotGet)
|
return this.functionCall(dotGet)
|
||||||
}
|
}
|
||||||
|
|
@ -595,17 +588,8 @@ export class Parser {
|
||||||
const ident = fn ?? this.identifier()
|
const ident = fn ?? this.identifier()
|
||||||
|
|
||||||
const args: SyntaxNode[] = []
|
const args: SyntaxNode[] = []
|
||||||
while (!this.isExprEnd() && !this.is($T.Operator, '|')) {
|
while (!this.isExprEnd())
|
||||||
if (this.is($T.NamedArgPrefix)) {
|
args.push(this.is($T.NamedArgPrefix) ? this.namedArg() : this.arg())
|
||||||
args.push(this.namedArg())
|
|
||||||
} else {
|
|
||||||
// 'do' is the only keyword allowed as a function argument
|
|
||||||
const val = this.is($T.Keyword, 'do') ? this.do() : this.value()
|
|
||||||
const arg = new SyntaxNode('PositionalArg', val.from, val.to)
|
|
||||||
arg.add(val)
|
|
||||||
args.push(arg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const node = new SyntaxNode('FunctionCall', ident.from, (args.at(-1) || ident).to)
|
const node = new SyntaxNode('FunctionCall', ident.from, (args.at(-1) || ident).to)
|
||||||
node.push(ident, ...args)
|
node.push(ident, ...args)
|
||||||
|
|
@ -626,7 +610,7 @@ export class Parser {
|
||||||
inner = this.dotGet()
|
inner = this.dotGet()
|
||||||
|
|
||||||
// if the dotGet was just a Word, bail
|
// if the dotGet was just a Word, bail
|
||||||
if (inner.type === 'Word') return inner
|
if (inner.type.is('Word')) return inner
|
||||||
}
|
}
|
||||||
|
|
||||||
inner ??= this.identifier()
|
inner ??= this.identifier()
|
||||||
|
|
@ -715,7 +699,7 @@ export class Parser {
|
||||||
// abc= true
|
// abc= true
|
||||||
namedArg(): SyntaxNode {
|
namedArg(): SyntaxNode {
|
||||||
const prefix = SyntaxNode.from(this.expect($T.NamedArgPrefix))
|
const prefix = SyntaxNode.from(this.expect($T.NamedArgPrefix))
|
||||||
const val = this.value()
|
const val = this.arg(true)
|
||||||
const node = new SyntaxNode('NamedArg', prefix.from, val.to)
|
const node = new SyntaxNode('NamedArg', prefix.from, val.to)
|
||||||
return node.push(prefix, val)
|
return node.push(prefix, val)
|
||||||
}
|
}
|
||||||
|
|
@ -725,7 +709,7 @@ export class Parser {
|
||||||
const prefix = SyntaxNode.from(this.expect($T.NamedArgPrefix))
|
const prefix = SyntaxNode.from(this.expect($T.NamedArgPrefix))
|
||||||
const val = this.value()
|
const val = this.value()
|
||||||
|
|
||||||
if (!['Null', 'Boolean', 'Number', 'String'].includes(val.type))
|
if (!['Null', 'Boolean', 'Number', 'String'].includes(val.type.name))
|
||||||
throw `[namedParam] default value must be Null|Bool|Num|Str, got ${val.type}\n\n ${this.input}\n`
|
throw `[namedParam] default value must be Null|Bool|Num|Str, got ${val.type}\n\n ${this.input}\n`
|
||||||
|
|
||||||
const node = new SyntaxNode('NamedParam', prefix.from, val.to)
|
const node = new SyntaxNode('NamedParam', prefix.from, val.to)
|
||||||
|
|
@ -771,7 +755,7 @@ export class Parser {
|
||||||
// throw blah
|
// throw blah
|
||||||
throw(): SyntaxNode {
|
throw(): SyntaxNode {
|
||||||
const keyword = this.keyword('throw')
|
const keyword = this.keyword('throw')
|
||||||
const val = this.value()
|
const val = this.expression()
|
||||||
const node = new SyntaxNode('Throw', keyword.from, val.to)
|
const node = new SyntaxNode('Throw', keyword.from, val.to)
|
||||||
return node.push(keyword, val)
|
return node.push(keyword, val)
|
||||||
}
|
}
|
||||||
|
|
@ -885,6 +869,7 @@ export class Parser {
|
||||||
|
|
||||||
isExprEnd(): boolean {
|
isExprEnd(): boolean {
|
||||||
return this.isAny($T.Colon, $T.Semicolon, $T.Newline, $T.CloseParen, $T.CloseBracket) ||
|
return this.isAny($T.Colon, $T.Semicolon, $T.Newline, $T.CloseParen, $T.CloseBracket) ||
|
||||||
|
this.is($T.Operator, '|') ||
|
||||||
this.isExprEndKeyword() || !this.current()
|
this.isExprEndKeyword() || !this.current()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -933,9 +918,9 @@ function collapseDotGets(origNodes: SyntaxNode[]): SyntaxNode {
|
||||||
while (nodes.length > 0) {
|
while (nodes.length > 0) {
|
||||||
const left = nodes.pop()!
|
const left = nodes.pop()!
|
||||||
|
|
||||||
if (left.type === 'Identifier') left.type = 'IdentifierBeforeDot'
|
if (left.type.is('Identifier')) left.type = 'IdentifierBeforeDot'
|
||||||
|
|
||||||
const dot = new SyntaxNode("DotGet", left.from, right.to);
|
const dot = new SyntaxNode("DotGet", left.from, right.to)
|
||||||
dot.push(left, right)
|
dot.push(left, right)
|
||||||
|
|
||||||
right = dot
|
right = dot
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,32 @@
|
||||||
import { SyntaxNode } from './node'
|
import { SyntaxNode } from './node'
|
||||||
|
|
||||||
|
/**
|
||||||
// Parse string contents into fragments, interpolations, and escape sequences.
|
* Parse string contents into fragments, interpolations, and escape sequences.
|
||||||
|
*
|
||||||
|
* Input: full string including quotes, e.g. "'hello $name'"
|
||||||
|
* Output: SyntaxNode tree with StringFragment, Interpolation, EscapeSeq children
|
||||||
|
*/
|
||||||
export const parseString = (input: string, from: number, to: number, parser: any): SyntaxNode => {
|
export const parseString = (input: string, from: number, to: number, parser: any): SyntaxNode => {
|
||||||
const stringNode = new SyntaxNode('String', from, to)
|
const stringNode = new SyntaxNode('String', from, to)
|
||||||
const content = input.slice(from, to)
|
const content = input.slice(from, to)
|
||||||
|
|
||||||
|
// Determine string type
|
||||||
const firstChar = content[0]
|
const firstChar = content[0]
|
||||||
|
|
||||||
// double quotes: no interpolation or escapes
|
// Double-quoted strings: no interpolation or escapes
|
||||||
if (firstChar === '"') {
|
if (firstChar === '"') {
|
||||||
const fragment = new SyntaxNode('DoubleQuote', from, to)
|
const fragment = new SyntaxNode('DoubleQuote', from, to)
|
||||||
stringNode.add(fragment)
|
stringNode.add(fragment)
|
||||||
return stringNode
|
return stringNode
|
||||||
}
|
}
|
||||||
|
|
||||||
// curlies: interpolation but no escapes
|
// Curly strings: interpolation but no escapes
|
||||||
if (firstChar === '{') {
|
if (firstChar === '{') {
|
||||||
parseCurlyString(stringNode, input, from, to, parser)
|
parseCurlyString(stringNode, input, from, to, parser)
|
||||||
return stringNode
|
return stringNode
|
||||||
}
|
}
|
||||||
|
|
||||||
// single-quotes: interpolation and escapes
|
// Single-quoted strings: interpolation and escapes
|
||||||
if (firstChar === "'") {
|
if (firstChar === "'") {
|
||||||
parseSingleQuoteString(stringNode, input, from, to, parser)
|
parseSingleQuoteString(stringNode, input, from, to, parser)
|
||||||
return stringNode
|
return stringNode
|
||||||
|
|
@ -30,19 +35,26 @@ export const parseString = (input: string, from: number, to: number, parser: any
|
||||||
throw `Unknown string type starting with: ${firstChar}`
|
throw `Unknown string type starting with: ${firstChar}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse single-quoted string: 'hello $name\n'
|
||||||
|
* Supports: interpolation ($var, $(expr)), escape sequences (\n, \$, etc)
|
||||||
|
*/
|
||||||
const parseSingleQuoteString = (stringNode: SyntaxNode, input: string, from: number, to: number, parser: any) => {
|
const parseSingleQuoteString = (stringNode: SyntaxNode, input: string, from: number, to: number, parser: any) => {
|
||||||
let pos = from + 1 // skip opening '
|
let pos = from + 1 // Skip opening '
|
||||||
let fragmentStart = pos
|
let fragmentStart = pos
|
||||||
|
|
||||||
while (pos < to - 1) { // -1 to skip closing '
|
while (pos < to - 1) { // -1 to skip closing '
|
||||||
const char = input[pos]
|
const char = input[pos]
|
||||||
|
|
||||||
|
// Escape sequence
|
||||||
if (char === '\\' && pos + 1 < to - 1) {
|
if (char === '\\' && pos + 1 < to - 1) {
|
||||||
|
// Push accumulated fragment
|
||||||
if (pos > fragmentStart) {
|
if (pos > fragmentStart) {
|
||||||
const frag = new SyntaxNode('StringFragment', fragmentStart, pos)
|
const frag = new SyntaxNode('StringFragment', fragmentStart, pos)
|
||||||
stringNode.add(frag)
|
stringNode.add(frag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add escape sequence node
|
||||||
const escNode = new SyntaxNode('EscapeSeq', pos, pos + 2)
|
const escNode = new SyntaxNode('EscapeSeq', pos, pos + 2)
|
||||||
stringNode.add(escNode)
|
stringNode.add(escNode)
|
||||||
|
|
||||||
|
|
@ -51,15 +63,19 @@ const parseSingleQuoteString = (stringNode: SyntaxNode, input: string, from: num
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Interpolation
|
||||||
if (char === '$') {
|
if (char === '$') {
|
||||||
|
// Push accumulated fragment
|
||||||
if (pos > fragmentStart) {
|
if (pos > fragmentStart) {
|
||||||
const frag = new SyntaxNode('StringFragment', fragmentStart, pos)
|
const frag = new SyntaxNode('StringFragment', fragmentStart, pos)
|
||||||
stringNode.add(frag)
|
stringNode.add(frag)
|
||||||
}
|
}
|
||||||
|
|
||||||
pos++ // skip $
|
pos++ // Skip $
|
||||||
|
|
||||||
|
// Parse interpolation content
|
||||||
if (input[pos] === '(') {
|
if (input[pos] === '(') {
|
||||||
|
// Expression interpolation: $(expr)
|
||||||
const interpStart = pos - 1 // Include the $
|
const interpStart = pos - 1 // Include the $
|
||||||
const exprResult = parseInterpolationExpr(input, pos, parser)
|
const exprResult = parseInterpolationExpr(input, pos, parser)
|
||||||
const interpNode = new SyntaxNode('Interpolation', interpStart, exprResult.endPos)
|
const interpNode = new SyntaxNode('Interpolation', interpStart, exprResult.endPos)
|
||||||
|
|
@ -67,6 +83,7 @@ const parseSingleQuoteString = (stringNode: SyntaxNode, input: string, from: num
|
||||||
stringNode.add(interpNode)
|
stringNode.add(interpNode)
|
||||||
pos = exprResult.endPos
|
pos = exprResult.endPos
|
||||||
} else {
|
} else {
|
||||||
|
// Variable interpolation: $name
|
||||||
const interpStart = pos - 1
|
const interpStart = pos - 1
|
||||||
const identEnd = findIdentifierEnd(input, pos, to - 1)
|
const identEnd = findIdentifierEnd(input, pos, to - 1)
|
||||||
const identNode = new SyntaxNode('FunctionCallOrIdentifier', pos, identEnd)
|
const identNode = new SyntaxNode('FunctionCallOrIdentifier', pos, identEnd)
|
||||||
|
|
@ -86,21 +103,27 @@ const parseSingleQuoteString = (stringNode: SyntaxNode, input: string, from: num
|
||||||
pos++
|
pos++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Push final fragment
|
||||||
if (pos > fragmentStart && fragmentStart < to - 1) {
|
if (pos > fragmentStart && fragmentStart < to - 1) {
|
||||||
const frag = new SyntaxNode('StringFragment', fragmentStart, pos)
|
const frag = new SyntaxNode('StringFragment', fragmentStart, pos)
|
||||||
stringNode.add(frag)
|
stringNode.add(frag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse curly string: { hello $name }
|
||||||
|
* Supports: interpolation ($var, $(expr)), nested braces
|
||||||
|
* Does NOT support: escape sequences (raw content)
|
||||||
|
*/
|
||||||
const parseCurlyString = (stringNode: SyntaxNode, input: string, from: number, to: number, parser: any) => {
|
const parseCurlyString = (stringNode: SyntaxNode, input: string, from: number, to: number, parser: any) => {
|
||||||
let pos = from + 1 // skip opening {
|
let pos = from + 1 // Skip opening {
|
||||||
let fragmentStart = from // include the opening { in the fragment
|
let fragmentStart = from // Include the opening { in the fragment
|
||||||
let depth = 1
|
let depth = 1
|
||||||
|
|
||||||
while (pos < to && depth > 0) {
|
while (pos < to && depth > 0) {
|
||||||
const char = input[pos]
|
const char = input[pos]
|
||||||
|
|
||||||
// track nesting
|
// Track brace nesting
|
||||||
if (char === '{') {
|
if (char === '{') {
|
||||||
depth++
|
depth++
|
||||||
pos++
|
pos++
|
||||||
|
|
@ -110,6 +133,7 @@ const parseCurlyString = (stringNode: SyntaxNode, input: string, from: number, t
|
||||||
if (char === '}') {
|
if (char === '}') {
|
||||||
depth--
|
depth--
|
||||||
if (depth === 0) {
|
if (depth === 0) {
|
||||||
|
// Push final fragment including closing }
|
||||||
const frag = new SyntaxNode('CurlyString', fragmentStart, pos + 1)
|
const frag = new SyntaxNode('CurlyString', fragmentStart, pos + 1)
|
||||||
stringNode.add(frag)
|
stringNode.add(frag)
|
||||||
break
|
break
|
||||||
|
|
@ -118,29 +142,19 @@ const parseCurlyString = (stringNode: SyntaxNode, input: string, from: number, t
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (char === '\\' && pos + 1 < to && input[pos + 1] === '$') {
|
// Interpolation
|
||||||
if (pos > fragmentStart) {
|
|
||||||
const frag = new SyntaxNode('CurlyString', fragmentStart, pos)
|
|
||||||
stringNode.add(frag)
|
|
||||||
}
|
|
||||||
|
|
||||||
const escapedFrag = new SyntaxNode('CurlyString', pos + 1, pos + 2)
|
|
||||||
stringNode.add(escapedFrag)
|
|
||||||
|
|
||||||
pos += 2 // skip \ and $
|
|
||||||
fragmentStart = pos
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (char === '$') {
|
if (char === '$') {
|
||||||
|
// Push accumulated fragment
|
||||||
if (pos > fragmentStart) {
|
if (pos > fragmentStart) {
|
||||||
const frag = new SyntaxNode('CurlyString', fragmentStart, pos)
|
const frag = new SyntaxNode('CurlyString', fragmentStart, pos)
|
||||||
stringNode.add(frag)
|
stringNode.add(frag)
|
||||||
}
|
}
|
||||||
|
|
||||||
pos++ // skip $
|
pos++ // Skip $
|
||||||
|
|
||||||
|
// Parse interpolation content
|
||||||
if (input[pos] === '(') {
|
if (input[pos] === '(') {
|
||||||
|
// Expression interpolation: $(expr)
|
||||||
const interpStart = pos - 1
|
const interpStart = pos - 1
|
||||||
const exprResult = parseInterpolationExpr(input, pos, parser)
|
const exprResult = parseInterpolationExpr(input, pos, parser)
|
||||||
const interpNode = new SyntaxNode('Interpolation', interpStart, exprResult.endPos)
|
const interpNode = new SyntaxNode('Interpolation', interpStart, exprResult.endPos)
|
||||||
|
|
@ -148,6 +162,7 @@ const parseCurlyString = (stringNode: SyntaxNode, input: string, from: number, t
|
||||||
stringNode.add(interpNode)
|
stringNode.add(interpNode)
|
||||||
pos = exprResult.endPos
|
pos = exprResult.endPos
|
||||||
} else {
|
} else {
|
||||||
|
// Variable interpolation: $name
|
||||||
const interpStart = pos - 1
|
const interpStart = pos - 1
|
||||||
const identEnd = findIdentifierEnd(input, pos, to)
|
const identEnd = findIdentifierEnd(input, pos, to)
|
||||||
const identNode = new SyntaxNode('FunctionCallOrIdentifier', pos, identEnd)
|
const identNode = new SyntaxNode('FunctionCallOrIdentifier', pos, identEnd)
|
||||||
|
|
@ -168,10 +183,16 @@ const parseCurlyString = (stringNode: SyntaxNode, input: string, from: number, t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a parenthesized expression interpolation: $(a + b)
|
||||||
|
* Returns the parsed expression node and the position after the closing )
|
||||||
|
* pos is position of the opening ( in the full input string
|
||||||
|
*/
|
||||||
const parseInterpolationExpr = (input: string, pos: number, parser: any): { node: SyntaxNode, endPos: number } => {
|
const parseInterpolationExpr = (input: string, pos: number, parser: any): { node: SyntaxNode, endPos: number } => {
|
||||||
|
// Find matching closing paren
|
||||||
let depth = 1
|
let depth = 1
|
||||||
let start = pos
|
let start = pos
|
||||||
let end = pos + 1 // start after opening (
|
let end = pos + 1 // Start after opening (
|
||||||
|
|
||||||
while (end < input.length && depth > 0) {
|
while (end < input.length && depth > 0) {
|
||||||
if (input[end] === '(') depth++
|
if (input[end] === '(') depth++
|
||||||
|
|
@ -184,21 +205,28 @@ const parseInterpolationExpr = (input: string, pos: number, parser: any): { node
|
||||||
|
|
||||||
const exprContent = input.slice(start + 1, end) // Content between ( and )
|
const exprContent = input.slice(start + 1, end) // Content between ( and )
|
||||||
const closeParen = end
|
const closeParen = end
|
||||||
end++ // move past closing )
|
end++ // Move past closing )
|
||||||
|
|
||||||
|
// Use the main parser to parse the expression
|
||||||
const exprNode = parser.parse(exprContent)
|
const exprNode = parser.parse(exprContent)
|
||||||
|
|
||||||
|
// Get the first real node (skip Program wrapper)
|
||||||
const innerNode = exprNode.firstChild || exprNode
|
const innerNode = exprNode.firstChild || exprNode
|
||||||
|
|
||||||
const offset = start + 1 // position where exprContent starts in input
|
// Adjust node positions: they're relative to exprContent, need to offset to full input
|
||||||
|
const offset = start + 1 // Position where exprContent starts in full input
|
||||||
adjustNodePositions(innerNode, offset)
|
adjustNodePositions(innerNode, offset)
|
||||||
|
|
||||||
|
// Wrap in ParenExpr - use positions in the full string
|
||||||
const parenNode = new SyntaxNode('ParenExpr', start, closeParen + 1)
|
const parenNode = new SyntaxNode('ParenExpr', start, closeParen + 1)
|
||||||
parenNode.add(innerNode)
|
parenNode.add(innerNode)
|
||||||
|
|
||||||
return { node: parenNode, endPos: end }
|
return { node: parenNode, endPos: end }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively adjust all node positions by adding an offset
|
||||||
|
*/
|
||||||
const adjustNodePositions = (node: SyntaxNode, offset: number) => {
|
const adjustNodePositions = (node: SyntaxNode, offset: number) => {
|
||||||
node.from += offset
|
node.from += offset
|
||||||
node.to += offset
|
node.to += offset
|
||||||
|
|
@ -208,11 +236,15 @@ const adjustNodePositions = (node: SyntaxNode, offset: number) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the end position of an identifier starting at pos
|
||||||
|
* Identifiers: lowercase letter or emoji, followed by letters/digits/dashes/emoji
|
||||||
|
*/
|
||||||
const findIdentifierEnd = (input: string, pos: number, maxPos: number): number => {
|
const findIdentifierEnd = (input: string, pos: number, maxPos: number): number => {
|
||||||
let end = pos
|
let end = pos
|
||||||
|
|
||||||
while (end < maxPos) {
|
while (end < maxPos) {
|
||||||
const char = input[end]!
|
const char = input[end]
|
||||||
|
|
||||||
// Stop at non-identifier characters
|
// Stop at non-identifier characters
|
||||||
if (!/[a-z0-9\-?]/.test(char)) {
|
if (!/[a-z0-9\-?]/.test(char)) {
|
||||||
|
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
import * as terms from '#parser/shrimp.terms'
|
|
||||||
|
|
||||||
export function nameToId(name: string): number {
|
|
||||||
switch (name) {
|
|
||||||
case 'Star': return terms.Star
|
|
||||||
case 'Slash': return terms.Slash
|
|
||||||
case 'Plus': return terms.Plus
|
|
||||||
case 'Minus': return terms.Minus
|
|
||||||
case 'And': return terms.And
|
|
||||||
case 'Or': return terms.Or
|
|
||||||
case 'Eq': return terms.Eq
|
|
||||||
case 'EqEq': return terms.EqEq
|
|
||||||
case 'Neq': return terms.Neq
|
|
||||||
case 'Lt': return terms.Lt
|
|
||||||
case 'Lte': return terms.Lte
|
|
||||||
case 'Gt': return terms.Gt
|
|
||||||
case 'Gte': return terms.Gte
|
|
||||||
case 'Modulo': return terms.Modulo
|
|
||||||
case 'PlusEq': return terms.PlusEq
|
|
||||||
case 'MinusEq': return terms.MinusEq
|
|
||||||
case 'StarEq': return terms.StarEq
|
|
||||||
case 'SlashEq': return terms.SlashEq
|
|
||||||
case 'ModuloEq': return terms.ModuloEq
|
|
||||||
case 'Band': return terms.Band
|
|
||||||
case 'Bor': return terms.Bor
|
|
||||||
case 'Bxor': return terms.Bxor
|
|
||||||
case 'Shl': return terms.Shl
|
|
||||||
case 'Shr': return terms.Shr
|
|
||||||
case 'Ushr': return terms.Ushr
|
|
||||||
case 'NullishCoalesce': return terms.NullishCoalesce
|
|
||||||
case 'NullishEq': return terms.NullishEq
|
|
||||||
case 'Identifier': return terms.Identifier
|
|
||||||
case 'AssignableIdentifier': return terms.AssignableIdentifier
|
|
||||||
case 'Word': return terms.Word
|
|
||||||
case 'IdentifierBeforeDot': return terms.IdentifierBeforeDot
|
|
||||||
case 'CurlyString': return terms.CurlyString
|
|
||||||
case 'newline': return terms.newline
|
|
||||||
case 'pipeStartsLine': return terms.pipeStartsLine
|
|
||||||
case 'Do': return terms.Do
|
|
||||||
case 'Comment': return terms.Comment
|
|
||||||
case 'Program': return terms.Program
|
|
||||||
case 'PipeExpr': return terms.PipeExpr
|
|
||||||
case 'WhileExpr': return terms.WhileExpr
|
|
||||||
case 'keyword': return terms.keyword
|
|
||||||
case 'ConditionalOp': return terms.ConditionalOp
|
|
||||||
case 'ParenExpr': return terms.ParenExpr
|
|
||||||
case 'FunctionCallWithNewlines': return terms.FunctionCallWithNewlines
|
|
||||||
case 'DotGet': return terms.DotGet
|
|
||||||
case 'Number': return terms.Number
|
|
||||||
case 'Dollar': return terms.Dollar
|
|
||||||
case 'PositionalArg': return terms.PositionalArg
|
|
||||||
case 'FunctionDef': return terms.FunctionDef
|
|
||||||
case 'Params': return terms.Params
|
|
||||||
case 'NamedParam': return terms.NamedParam
|
|
||||||
case 'NamedArgPrefix': return terms.NamedArgPrefix
|
|
||||||
case 'String': return terms.String
|
|
||||||
case 'StringFragment': return terms.StringFragment
|
|
||||||
case 'Interpolation': return terms.Interpolation
|
|
||||||
case 'FunctionCallOrIdentifier': return terms.FunctionCallOrIdentifier
|
|
||||||
case 'EscapeSeq': return terms.EscapeSeq
|
|
||||||
case 'DoubleQuote': return terms.DoubleQuote
|
|
||||||
case 'Boolean': return terms.Boolean
|
|
||||||
case 'Null': return terms.Null
|
|
||||||
case 'colon': return terms.colon
|
|
||||||
case 'CatchExpr': return terms.CatchExpr
|
|
||||||
case 'Block': return terms.Block
|
|
||||||
case 'FinallyExpr': return terms.FinallyExpr
|
|
||||||
case 'Underscore': return terms.Underscore
|
|
||||||
case 'NamedArg': return terms.NamedArg
|
|
||||||
case 'IfExpr': return terms.IfExpr
|
|
||||||
case 'FunctionCall': return terms.FunctionCall
|
|
||||||
case 'ElseIfExpr': return terms.ElseIfExpr
|
|
||||||
case 'ElseExpr': return terms.ElseExpr
|
|
||||||
case 'BinOp': return terms.BinOp
|
|
||||||
case 'Regex': return terms.Regex
|
|
||||||
case 'Dict': return terms.Dict
|
|
||||||
case 'Array': return terms.Array
|
|
||||||
case 'FunctionCallWithBlock': return terms.FunctionCallWithBlock
|
|
||||||
case 'TryExpr': return terms.TryExpr
|
|
||||||
case 'Throw': return terms.Throw
|
|
||||||
case 'Import': return terms.Import
|
|
||||||
case 'CompoundAssign': return terms.CompoundAssign
|
|
||||||
case 'Assign': return terms.Assign
|
|
||||||
default: throw `unknown term: ${name}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -139,11 +139,24 @@ describe('try/catch/finally/throw', () => {
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('parses throw statement with BinOp', () => {
|
||||||
|
expect("throw 'error message:' + msg").toMatchTree(`
|
||||||
|
Throw
|
||||||
|
keyword throw
|
||||||
|
BinOp
|
||||||
|
String
|
||||||
|
StringFragment error message:
|
||||||
|
Plus +
|
||||||
|
Identifier msg
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
test('parses throw statement with identifier', () => {
|
test('parses throw statement with identifier', () => {
|
||||||
expect('throw error-object').toMatchTree(`
|
expect('throw error-object').toMatchTree(`
|
||||||
Throw
|
Throw
|
||||||
keyword throw
|
keyword throw
|
||||||
Identifier error-object
|
FunctionCallOrIdentifier
|
||||||
|
Identifier error-object
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,58 @@ describe('calling functions', () => {
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('call with function', () => {
|
||||||
|
expect(`tail do x: x end`).toMatchTree(`
|
||||||
|
FunctionCall
|
||||||
|
Identifier tail
|
||||||
|
PositionalArg
|
||||||
|
FunctionDef
|
||||||
|
Do do
|
||||||
|
Params
|
||||||
|
Identifier x
|
||||||
|
colon :
|
||||||
|
FunctionCallOrIdentifier
|
||||||
|
Identifier x
|
||||||
|
keyword end
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('call with arg and function', () => {
|
||||||
|
expect(`tail true do x: x end`).toMatchTree(`
|
||||||
|
FunctionCall
|
||||||
|
Identifier tail
|
||||||
|
PositionalArg
|
||||||
|
Boolean true
|
||||||
|
PositionalArg
|
||||||
|
FunctionDef
|
||||||
|
Do do
|
||||||
|
Params
|
||||||
|
Identifier x
|
||||||
|
colon :
|
||||||
|
FunctionCallOrIdentifier
|
||||||
|
Identifier x
|
||||||
|
keyword end
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('call with function in named arg', () => {
|
||||||
|
expect(`tail callback=do x: x end`).toMatchTree(`
|
||||||
|
FunctionCall
|
||||||
|
Identifier tail
|
||||||
|
NamedArg
|
||||||
|
NamedArgPrefix callback=
|
||||||
|
FunctionDef
|
||||||
|
Do do
|
||||||
|
Params
|
||||||
|
Identifier x
|
||||||
|
colon :
|
||||||
|
FunctionCallOrIdentifier
|
||||||
|
Identifier x
|
||||||
|
keyword end
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
test('command with arg that is also a command', () => {
|
test('command with arg that is also a command', () => {
|
||||||
expect('tail tail').toMatchTree(`
|
expect('tail tail').toMatchTree(`
|
||||||
FunctionCall
|
FunctionCall
|
||||||
|
|
|
||||||
|
|
@ -336,6 +336,22 @@ describe('dict literals', () => {
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('work with functions', () => {
|
||||||
|
expect(`[trap=do x: x end]`).toMatchTree(`
|
||||||
|
Dict
|
||||||
|
NamedArg
|
||||||
|
NamedArgPrefix trap=
|
||||||
|
FunctionDef
|
||||||
|
Do do
|
||||||
|
Params
|
||||||
|
Identifier x
|
||||||
|
colon :
|
||||||
|
FunctionCallOrIdentifier
|
||||||
|
Identifier x
|
||||||
|
keyword end
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
test('can be nested', () => {
|
test('can be nested', () => {
|
||||||
expect('[a=one b=[two [c=three]]]').toMatchTree(`
|
expect('[a=one b=[two [c=three]]]').toMatchTree(`
|
||||||
Dict
|
Dict
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,43 @@ describe('pipe expressions', () => {
|
||||||
Identifier echo
|
Identifier echo
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('parenthesized expressions can be piped', () => {
|
||||||
|
expect(`(1 + 2) | echo`).toMatchTree(`
|
||||||
|
PipeExpr
|
||||||
|
ParenExpr
|
||||||
|
BinOp
|
||||||
|
Number 1
|
||||||
|
Plus +
|
||||||
|
Number 2
|
||||||
|
operator |
|
||||||
|
FunctionCallOrIdentifier
|
||||||
|
Identifier echo
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('complex parenthesized expressions with pipes', () => {
|
||||||
|
expect(`((math.random) * 10 + 1) | math.floor`).toMatchTree(`
|
||||||
|
PipeExpr
|
||||||
|
ParenExpr
|
||||||
|
BinOp
|
||||||
|
BinOp
|
||||||
|
ParenExpr
|
||||||
|
FunctionCallOrIdentifier
|
||||||
|
DotGet
|
||||||
|
IdentifierBeforeDot math
|
||||||
|
Identifier random
|
||||||
|
Star *
|
||||||
|
Number 10
|
||||||
|
Plus +
|
||||||
|
Number 1
|
||||||
|
operator |
|
||||||
|
FunctionCallOrIdentifier
|
||||||
|
DotGet
|
||||||
|
IdentifierBeforeDot math
|
||||||
|
Identifier floor
|
||||||
|
`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('pipe continuation', () => {
|
describe('pipe continuation', () => {
|
||||||
|
|
@ -309,7 +346,7 @@ grep h`).toMatchTree(`
|
||||||
Identifier split
|
Identifier split
|
||||||
PositionalArg
|
PositionalArg
|
||||||
String
|
String
|
||||||
StringFragment
|
StringFragment (space)
|
||||||
operator |
|
operator |
|
||||||
FunctionCall
|
FunctionCall
|
||||||
Identifier map
|
Identifier map
|
||||||
|
|
@ -333,3 +370,41 @@ grep h`).toMatchTree(`
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Underscore', () => {
|
||||||
|
test('works in pipes', () => {
|
||||||
|
expect(`sub 3 1 | div (sub 110 9 | sub 1) _ | div 5`).toMatchTree(`
|
||||||
|
PipeExpr
|
||||||
|
FunctionCall
|
||||||
|
Identifier sub
|
||||||
|
PositionalArg
|
||||||
|
Number 3
|
||||||
|
PositionalArg
|
||||||
|
Number 1
|
||||||
|
operator |
|
||||||
|
FunctionCall
|
||||||
|
Identifier div
|
||||||
|
PositionalArg
|
||||||
|
ParenExpr
|
||||||
|
PipeExpr
|
||||||
|
FunctionCall
|
||||||
|
Identifier sub
|
||||||
|
PositionalArg
|
||||||
|
Number 110
|
||||||
|
PositionalArg
|
||||||
|
Number 9
|
||||||
|
operator |
|
||||||
|
FunctionCall
|
||||||
|
Identifier sub
|
||||||
|
PositionalArg
|
||||||
|
Number 1
|
||||||
|
PositionalArg
|
||||||
|
Underscore _
|
||||||
|
operator |
|
||||||
|
FunctionCall
|
||||||
|
Identifier div
|
||||||
|
PositionalArg
|
||||||
|
Number 5
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -38,7 +38,8 @@ const valueTokens = new Set([
|
||||||
TokenType.Comment,
|
TokenType.Comment,
|
||||||
TokenType.Keyword, TokenType.Operator,
|
TokenType.Keyword, TokenType.Operator,
|
||||||
TokenType.Identifier, TokenType.Word, TokenType.NamedArgPrefix,
|
TokenType.Identifier, TokenType.Word, TokenType.NamedArgPrefix,
|
||||||
TokenType.Boolean, TokenType.Number, TokenType.String, TokenType.Regex
|
TokenType.Boolean, TokenType.Number, TokenType.String, TokenType.Regex,
|
||||||
|
TokenType.Underscore
|
||||||
])
|
])
|
||||||
|
|
||||||
const operators = new Set([
|
const operators = new Set([
|
||||||
|
|
@ -106,8 +107,8 @@ const keywords = new Set([
|
||||||
])
|
])
|
||||||
|
|
||||||
// helper
|
// helper
|
||||||
function c(strings: TemplateStringsArray) {
|
function c(strings: TemplateStringsArray, ...values: any[]) {
|
||||||
return strings[0]!.charCodeAt(0)
|
return strings.reduce((result, str, i) => result + str + (values[i] ?? ""), "").charCodeAt(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function s(c: number): string {
|
function s(c: number): string {
|
||||||
|
|
@ -289,7 +290,7 @@ export class Scanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
readCurlyString() {
|
readCurlyString() {
|
||||||
this.start = this.pos - 1 // include opening {
|
this.start = this.pos - 1
|
||||||
let depth = 1
|
let depth = 1
|
||||||
this.next()
|
this.next()
|
||||||
|
|
||||||
|
|
@ -338,7 +339,7 @@ export class Scanner {
|
||||||
|
|
||||||
// classify the token based on what we read
|
// classify the token based on what we read
|
||||||
if (word === '_')
|
if (word === '_')
|
||||||
this.pushChar(TokenType.Underscore)
|
this.push(TokenType.Underscore)
|
||||||
|
|
||||||
else if (word === 'null')
|
else if (word === 'null')
|
||||||
this.push(TokenType.Null)
|
this.push(TokenType.Null)
|
||||||
|
|
@ -386,25 +387,37 @@ export class Scanner {
|
||||||
this.start = this.pos - 1
|
this.start = this.pos - 1
|
||||||
this.next() // skip 2nd /
|
this.next() // skip 2nd /
|
||||||
|
|
||||||
let foundClosing = false
|
|
||||||
while (this.char > 0) {
|
while (this.char > 0) {
|
||||||
if (this.char === c`/` && this.peek() === c`/`) {
|
if (this.char === c`/` && this.peek() === c`/`) {
|
||||||
this.next() // skip /
|
this.next() // skip /
|
||||||
this.next() // skip /
|
this.next() // skip /
|
||||||
foundClosing = true
|
|
||||||
|
// read regex flags
|
||||||
|
while (this.char > 0 && isIdentStart(this.char))
|
||||||
|
this.next()
|
||||||
|
|
||||||
|
// validate regex
|
||||||
|
const to = this.pos - getCharSize(this.char)
|
||||||
|
const regexText = this.input.slice(this.start, to)
|
||||||
|
const [_, pattern, flags] = regexText.match(/^\/\/(.*)\/\/([gimsuy]*)$/) || []
|
||||||
|
|
||||||
|
if (pattern) {
|
||||||
|
try {
|
||||||
|
new RegExp(pattern, flags)
|
||||||
|
this.push(TokenType.Regex)
|
||||||
|
break
|
||||||
|
} catch (e) {
|
||||||
|
// invalid regex - fall through to Word
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// invalid regex is treated as Word
|
||||||
|
this.push(TokenType.Word)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
this.next()
|
this.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
const closing = new Set([c`g`, c`i`, c`m`, c`s`, c`u`, c`y`])
|
|
||||||
|
|
||||||
// read flags (e.g., 'gi', 'gim', etc.)
|
|
||||||
if (foundClosing)
|
|
||||||
while (closing.has(this.char)) this.next()
|
|
||||||
|
|
||||||
this.push(TokenType.Regex)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
canBeDotGet(lastToken?: Token): boolean {
|
canBeDotGet(lastToken?: Token): boolean {
|
||||||
|
|
|
||||||
|
|
@ -1,90 +1,89 @@
|
||||||
import { expect, describe, test } from 'bun:test'
|
import { expect, describe, test } from 'bun:test'
|
||||||
import { globals } from '#prelude'
|
|
||||||
|
|
||||||
describe('var and var?', () => {
|
describe('var and var?', () => {
|
||||||
test('var? checks if a variable exists', async () => {
|
test('var? checks if a variable exists', async () => {
|
||||||
await expect(`var? 'nada'`).toEvaluateTo(false, globals)
|
await expect(`var? 'nada'`).toEvaluateTo(false)
|
||||||
await expect(`var? 'info'`).toEvaluateTo(false, globals)
|
await expect(`var? 'info'`).toEvaluateTo(false)
|
||||||
await expect(`abc = abc; var? 'abc'`).toEvaluateTo(true, globals)
|
await expect(`abc = abc; var? 'abc'`).toEvaluateTo(true)
|
||||||
await expect(`var? 'var?'`).toEvaluateTo(true, globals)
|
await expect(`var? 'var?'`).toEvaluateTo(true)
|
||||||
|
|
||||||
await expect(`var? 'dict'`).toEvaluateTo(true, globals)
|
await expect(`var? 'dict'`).toEvaluateTo(true)
|
||||||
await expect(`var? dict`).toEvaluateTo(true, globals)
|
await expect(`var? dict`).toEvaluateTo(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('var returns a value or null', async () => {
|
test('var returns a value or null', async () => {
|
||||||
await expect(`var 'nada'`).toEvaluateTo(null, globals)
|
await expect(`var 'nada'`).toEvaluateTo(null)
|
||||||
await expect(`var nada`).toEvaluateTo(null, globals)
|
await expect(`var nada`).toEvaluateTo(null)
|
||||||
await expect(`var 'info'`).toEvaluateTo(null, globals)
|
await expect(`var 'info'`).toEvaluateTo(null)
|
||||||
await expect(`abc = my-string; var 'abc'`).toEvaluateTo('my-string', globals)
|
await expect(`abc = my-string; var 'abc'`).toEvaluateTo('my-string')
|
||||||
await expect(`abc = my-string; var abc`).toEvaluateTo(null, globals)
|
await expect(`abc = my-string; var abc`).toEvaluateTo(null)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('type predicates', () => {
|
describe('type predicates', () => {
|
||||||
test('string? checks for string type', async () => {
|
test('string? checks for string type', async () => {
|
||||||
await expect(`string? 'hello'`).toEvaluateTo(true, globals)
|
await expect(`string? 'hello'`).toEvaluateTo(true)
|
||||||
await expect(`string? 42`).toEvaluateTo(false, globals)
|
await expect(`string? 42`).toEvaluateTo(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('number? checks for number type', async () => {
|
test('number? checks for number type', async () => {
|
||||||
await expect(`number? 42`).toEvaluateTo(true, globals)
|
await expect(`number? 42`).toEvaluateTo(true)
|
||||||
await expect(`number? 'hello'`).toEvaluateTo(false, globals)
|
await expect(`number? 'hello'`).toEvaluateTo(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('boolean? checks for boolean type', async () => {
|
test('boolean? checks for boolean type', async () => {
|
||||||
await expect(`boolean? true`).toEvaluateTo(true, globals)
|
await expect(`boolean? true`).toEvaluateTo(true)
|
||||||
await expect(`boolean? 42`).toEvaluateTo(false, globals)
|
await expect(`boolean? 42`).toEvaluateTo(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('array? checks for array type', async () => {
|
test('array? checks for array type', async () => {
|
||||||
await expect(`array? [1 2 3]`).toEvaluateTo(true, globals)
|
await expect(`array? [1 2 3]`).toEvaluateTo(true)
|
||||||
await expect(`array? 42`).toEvaluateTo(false, globals)
|
await expect(`array? 42`).toEvaluateTo(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('dict? checks for dict type', async () => {
|
test('dict? checks for dict type', async () => {
|
||||||
await expect(`dict? [a=1]`).toEvaluateTo(true, globals)
|
await expect(`dict? [a=1]`).toEvaluateTo(true)
|
||||||
await expect(`dict? []`).toEvaluateTo(false, globals)
|
await expect(`dict? []`).toEvaluateTo(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('null? checks for null type', async () => {
|
test('null? checks for null type', async () => {
|
||||||
await expect(`null? null`).toEvaluateTo(true, globals)
|
await expect(`null? null`).toEvaluateTo(true)
|
||||||
await expect(`null? 42`).toEvaluateTo(false, globals)
|
await expect(`null? 42`).toEvaluateTo(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('some? checks for non-null', async () => {
|
test('some? checks for non-null', async () => {
|
||||||
await expect(`some? 42`).toEvaluateTo(true, globals)
|
await expect(`some? 42`).toEvaluateTo(true)
|
||||||
await expect(`some? null`).toEvaluateTo(false, globals)
|
await expect(`some? null`).toEvaluateTo(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('introspection', () => {
|
describe('introspection', () => {
|
||||||
test('type returns proper types', async () => {
|
test('type returns proper types', async () => {
|
||||||
await expect(`type 'hello'`).toEvaluateTo('string', globals)
|
await expect(`type 'hello'`).toEvaluateTo('string')
|
||||||
await expect(`type 42`).toEvaluateTo('number', globals)
|
await expect(`type 42`).toEvaluateTo('number')
|
||||||
await expect(`type true`).toEvaluateTo('boolean', globals)
|
await expect(`type true`).toEvaluateTo('boolean')
|
||||||
await expect(`type false`).toEvaluateTo('boolean', globals)
|
await expect(`type false`).toEvaluateTo('boolean')
|
||||||
await expect(`type null`).toEvaluateTo('null', globals)
|
await expect(`type null`).toEvaluateTo('null')
|
||||||
await expect(`type [1 2 3]`).toEvaluateTo('array', globals)
|
await expect(`type [1 2 3]`).toEvaluateTo('array')
|
||||||
await expect(`type [a=1 b=2]`).toEvaluateTo('dict', globals)
|
await expect(`type [a=1 b=2]`).toEvaluateTo('dict')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('inspect formats values', async () => {
|
test('inspect formats values', async () => {
|
||||||
await expect(`inspect 'hello'`).toEvaluateTo("\u001b[32m'hello\u001b[32m'\u001b[0m", globals)
|
await expect(`inspect 'hello'`).toEvaluateTo("\u001b[32m'hello\u001b[32m'\u001b[0m")
|
||||||
})
|
})
|
||||||
|
|
||||||
test('describe describes values', async () => {
|
test('describe describes values', async () => {
|
||||||
await expect(`describe 'hello'`).toEvaluateTo("#<string: \u001b[32m'hello\u001b[32m'\u001b[0m>", globals)
|
await expect(`describe 'hello'`).toEvaluateTo("#<string: \u001b[32m'hello\u001b[32m'\u001b[0m>")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('environment', () => {
|
describe('environment', () => {
|
||||||
test('args is an array', async () => {
|
test('args is an array', async () => {
|
||||||
await expect(`array? $.args`).toEvaluateTo(true, globals)
|
await expect(`array? $.args`).toEvaluateTo(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('args can be accessed', async () => {
|
test('args can be accessed', async () => {
|
||||||
await expect(`type $.args`).toEvaluateTo('array', globals)
|
await expect(`type $.args`).toEvaluateTo('array')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('argv includes more than just the args', async () => {
|
test('argv includes more than just the args', async () => {
|
||||||
|
|
@ -106,35 +105,35 @@ describe('ref', () => {
|
||||||
|
|
||||||
describe('$ global dictionary', () => {
|
describe('$ global dictionary', () => {
|
||||||
test('$.args is an array', async () => {
|
test('$.args is an array', async () => {
|
||||||
await expect(`$.args | array?`).toEvaluateTo(true, globals)
|
await expect(`$.args | array?`).toEvaluateTo(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('$.args can be accessed', async () => {
|
test('$.args can be accessed', async () => {
|
||||||
await expect(`$.args | type`).toEvaluateTo('array', globals)
|
await expect(`$.args | type`).toEvaluateTo('array')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('$.script.name is a string', async () => {
|
test('$.script.name is a string', async () => {
|
||||||
await expect(`$.script.name | string?`).toEvaluateTo(true, globals)
|
await expect(`$.script.name | string?`).toEvaluateTo(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('$.script.path is a string', async () => {
|
test('$.script.path is a string', async () => {
|
||||||
await expect(`$.script.path | string?`).toEvaluateTo(true, globals)
|
await expect(`$.script.path | string?`).toEvaluateTo(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('$.env is a dict', async () => {
|
test('$.env is a dict', async () => {
|
||||||
await expect(`$.env | dict?`).toEvaluateTo(true, globals)
|
await expect(`$.env | dict?`).toEvaluateTo(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('$.pid is a number', async () => {
|
test('$.pid is a number', async () => {
|
||||||
await expect(`$.pid | number?`).toEvaluateTo(true, globals)
|
await expect(`$.pid | number?`).toEvaluateTo(true)
|
||||||
await expect(`$.pid > 0`).toEvaluateTo(true, globals)
|
await expect(`$.pid > 0`).toEvaluateTo(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('$.cwd is a string', async () => {
|
test('$.cwd is a string', async () => {
|
||||||
await expect(`$.cwd | string?`).toEvaluateTo(true, globals)
|
await expect(`$.cwd | string?`).toEvaluateTo(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('$.cwd returns current working directory', async () => {
|
test('$.cwd returns current working directory', async () => {
|
||||||
await expect(`$.cwd`).toEvaluateTo(process.cwd(), globals)
|
await expect(`$.cwd`).toEvaluateTo(process.cwd())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,41 @@
|
||||||
import { expect, describe, test } from 'bun:test'
|
import { expect, describe, test } from 'bun:test'
|
||||||
import { globals } from '#prelude'
|
|
||||||
|
|
||||||
describe('loading a file', () => {
|
describe('loading a file', () => {
|
||||||
test(`imports all a file's functions`, async () => {
|
test(`imports all a file's functions`, async () => {
|
||||||
expect(`
|
expect(`
|
||||||
math = load ./src/prelude/tests/math.sh
|
math = load ./src/prelude/tests/math.sh
|
||||||
math.double 4
|
math.double 4
|
||||||
`).toEvaluateTo(8, globals)
|
`).toEvaluateTo(8)
|
||||||
|
|
||||||
expect(`
|
expect(`
|
||||||
math = load ./src/prelude/tests/math.sh
|
math = load ./src/prelude/tests/math.sh
|
||||||
math.double (math.double 4)
|
math.double (math.double 4)
|
||||||
`).toEvaluateTo(16, globals)
|
`).toEvaluateTo(16)
|
||||||
|
|
||||||
expect(`
|
expect(`
|
||||||
math = load ./src/prelude/tests/math.sh
|
math = load ./src/prelude/tests/math.sh
|
||||||
dbl = ref math.double
|
dbl = ref math.double
|
||||||
dbl (dbl 2)
|
dbl (dbl 2)
|
||||||
`).toEvaluateTo(8, globals)
|
`).toEvaluateTo(8)
|
||||||
|
|
||||||
expect(`
|
expect(`
|
||||||
math = load ./src/prelude/tests/math.sh
|
math = load ./src/prelude/tests/math.sh
|
||||||
math.pi
|
math.pi
|
||||||
`).toEvaluateTo(3.14, globals)
|
`).toEvaluateTo(3.14)
|
||||||
|
|
||||||
expect(`
|
expect(`
|
||||||
math = load ./src/prelude/tests/math.sh
|
math = load ./src/prelude/tests/math.sh
|
||||||
math | at 🥧
|
math | at 🥧
|
||||||
`).toEvaluateTo(3.14159265359, globals)
|
`).toEvaluateTo(3.14159265359)
|
||||||
|
|
||||||
expect(`
|
expect(`
|
||||||
math = load ./src/prelude/tests/math.sh
|
math = load ./src/prelude/tests/math.sh
|
||||||
math.🥧
|
math.🥧
|
||||||
`).toEvaluateTo(3.14159265359, globals)
|
`).toEvaluateTo(3.14159265359)
|
||||||
|
|
||||||
expect(`
|
expect(`
|
||||||
math = load ./src/prelude/tests/math.sh
|
math = load ./src/prelude/tests/math.sh
|
||||||
math.add1 5
|
math.add1 5
|
||||||
`).toEvaluateTo(6, globals)
|
`).toEvaluateTo(6)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { expect, describe, test } from 'bun:test'
|
import { expect, describe, test } from 'bun:test'
|
||||||
import { globals } from '#prelude'
|
|
||||||
|
|
||||||
describe('string operations', () => {
|
describe('string operations', () => {
|
||||||
test('to-upper converts to uppercase', async () => {
|
test('to-upper converts to uppercase', async () => {
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,9 @@ const nodeToString = (node: SyntaxNode, input: string, depth = 0): string => {
|
||||||
return `${indent}${nodeName}`
|
return `${indent}${nodeName}`
|
||||||
} else {
|
} else {
|
||||||
// Only strip quotes from whole String nodes (legacy DoubleQuote), not StringFragment/EscapeSeq/CurlyString
|
// Only strip quotes from whole String nodes (legacy DoubleQuote), not StringFragment/EscapeSeq/CurlyString
|
||||||
const cleanText = nodeName === 'String' ? text.slice(1, -1) : text
|
let cleanText = nodeName === 'String' ? text.slice(1, -1) : text
|
||||||
return `${indent}${nodeName} ${cleanText}`
|
if (cleanText === ' ') cleanText = '(space)'
|
||||||
|
return cleanText ? `${indent}${nodeName} ${cleanText}` : `${indent}${nodeName}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user