Compare commits
No commits in common. "prettier" and "main" have entirely different histories.
|
|
@ -42,7 +42,7 @@ function analyzeParser(filePath: string): Map<string, CallInfo> {
|
||||||
methods.set(currentMethod, {
|
methods.set(currentMethod, {
|
||||||
method: currentMethod,
|
method: currentMethod,
|
||||||
line: i + 1,
|
line: i + 1,
|
||||||
calls: new Set(),
|
calls: new Set()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +85,7 @@ function buildTree(
|
||||||
indent = '',
|
indent = '',
|
||||||
isLast = true,
|
isLast = true,
|
||||||
depth = 0,
|
depth = 0,
|
||||||
maxDepth = 3,
|
maxDepth = 3
|
||||||
): string[] {
|
): string[] {
|
||||||
const lines: string[] = []
|
const lines: string[] = []
|
||||||
const info = callGraph.get(method)
|
const info = callGraph.get(method)
|
||||||
|
|
@ -93,7 +93,7 @@ function buildTree(
|
||||||
if (!info) return lines
|
if (!info) return lines
|
||||||
|
|
||||||
// Add current method
|
// Add current method
|
||||||
const prefix = depth === 0 ? '' : isLast ? '└─> ' : '├─> '
|
const prefix = depth === 0 ? '' : (isLast ? '└─> ' : '├─> ')
|
||||||
const suffix = info.isRecursive ? ' (recursive)' : ''
|
const suffix = info.isRecursive ? ' (recursive)' : ''
|
||||||
const lineNum = `[line ${info.line}]`
|
const lineNum = `[line ${info.line}]`
|
||||||
lines.push(`${indent}${prefix}${method}() ${lineNum}${suffix}`)
|
lines.push(`${indent}${prefix}${method}() ${lineNum}${suffix}`)
|
||||||
|
|
@ -116,9 +116,9 @@ function buildTree(
|
||||||
|
|
||||||
// Get sorted unique calls (filter out recursive self-calls for display)
|
// Get sorted unique calls (filter out recursive self-calls for display)
|
||||||
const calls = Array.from(info.calls)
|
const calls = Array.from(info.calls)
|
||||||
.filter((c) => callGraph.has(c)) // Only show parser methods
|
.filter(c => callGraph.has(c)) // Only show parser methods
|
||||||
.filter((c) => c !== method) // Don't show immediate self-recursion
|
.filter(c => c !== method) // Don't show immediate self-recursion
|
||||||
.filter((c) => !helperPatterns.test(c)) // Filter out helpers
|
.filter(c => !helperPatterns.test(c)) // Filter out helpers
|
||||||
.sort()
|
.sort()
|
||||||
|
|
||||||
// Add children
|
// Add children
|
||||||
|
|
@ -131,7 +131,7 @@ function buildTree(
|
||||||
newIndent,
|
newIndent,
|
||||||
idx === calls.length - 1,
|
idx === calls.length - 1,
|
||||||
depth + 1,
|
depth + 1,
|
||||||
maxDepth,
|
maxDepth
|
||||||
)
|
)
|
||||||
lines.push(...childLines)
|
lines.push(...childLines)
|
||||||
})
|
})
|
||||||
|
|
@ -163,11 +163,11 @@ console.log(` Entry point: parse()`)
|
||||||
// Find methods that are never called (potential dead code or entry points)
|
// Find methods that are never called (potential dead code or entry points)
|
||||||
const allCalled = new Set<string>()
|
const allCalled = new Set<string>()
|
||||||
for (const info of callGraph.values()) {
|
for (const info of callGraph.values()) {
|
||||||
info.calls.forEach((c) => allCalled.add(c))
|
info.calls.forEach(c => allCalled.add(c))
|
||||||
}
|
}
|
||||||
|
|
||||||
const uncalled = Array.from(callGraph.keys())
|
const uncalled = Array.from(callGraph.keys())
|
||||||
.filter((m) => !allCalled.has(m) && m !== 'parse')
|
.filter(m => !allCalled.has(m) && m !== 'parse')
|
||||||
.sort()
|
.sort()
|
||||||
|
|
||||||
if (uncalled.length > 0) {
|
if (uncalled.length > 0) {
|
||||||
|
|
|
||||||
|
|
@ -58,10 +58,7 @@ export class Compiler {
|
||||||
bytecode: Bytecode
|
bytecode: Bytecode
|
||||||
pipeCounter = 0
|
pipeCounter = 0
|
||||||
|
|
||||||
constructor(
|
constructor(public input: string, globals?: string[] | Record<string, any>) {
|
||||||
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 ast = parse(input)
|
const ast = parse(input)
|
||||||
|
|
@ -112,15 +109,9 @@ export class Compiler {
|
||||||
// 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
|
||||||
let numberValue: number
|
let numberValue: number
|
||||||
if (
|
if (value.startsWith('-') && (value.includes('0x') || value.includes('0b') || value.includes('0o'))) {
|
||||||
value.startsWith('-') &&
|
|
||||||
(value.includes('0x') || value.includes('0b') || value.includes('0o'))
|
|
||||||
) {
|
|
||||||
numberValue = -Number(value.slice(1))
|
numberValue = -Number(value.slice(1))
|
||||||
} else if (
|
} else if (value.startsWith('+') && (value.includes('0x') || value.includes('0b') || value.includes('0o'))) {
|
||||||
value.startsWith('+') &&
|
|
||||||
(value.includes('0x') || value.includes('0b') || value.includes('0o'))
|
|
||||||
) {
|
|
||||||
numberValue = Number(value.slice(1))
|
numberValue = Number(value.slice(1))
|
||||||
} else {
|
} else {
|
||||||
numberValue = Number(value)
|
numberValue = Number(value)
|
||||||
|
|
@ -132,7 +123,8 @@ export class Compiler {
|
||||||
return [[`PUSH`, numberValue]]
|
return [[`PUSH`, numberValue]]
|
||||||
|
|
||||||
case 'String': {
|
case 'String': {
|
||||||
if (node.firstChild?.type.is('CurlyString')) return this.#compileCurlyString(value, input)
|
if (node.firstChild?.type.is('CurlyString'))
|
||||||
|
return this.#compileCurlyString(value, input)
|
||||||
|
|
||||||
const { parts, hasInterpolation } = getStringParts(node, input)
|
const { parts, hasInterpolation } = getStringParts(node, input)
|
||||||
|
|
||||||
|
|
@ -174,7 +166,7 @@ export class Compiler {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`Unexpected string part: ${part.type.name}`,
|
`Unexpected string part: ${part.type.name}`,
|
||||||
part.from,
|
part.from,
|
||||||
part.to,
|
part.to
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -382,7 +374,7 @@ export class Compiler {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`Unknown compound operator: ${opValue}`,
|
`Unknown compound operator: ${opValue}`,
|
||||||
operator.from,
|
operator.from,
|
||||||
operator.to,
|
operator.to
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -430,8 +422,8 @@ export class Compiler {
|
||||||
catchVariable,
|
catchVariable,
|
||||||
catchBody,
|
catchBody,
|
||||||
finallyBody,
|
finallyBody,
|
||||||
input,
|
input
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
instructions.push(...compileFunctionBody())
|
instructions.push(...compileFunctionBody())
|
||||||
|
|
@ -540,7 +532,7 @@ export class Compiler {
|
||||||
...block
|
...block
|
||||||
.filter((x) => x.type.name !== '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}:`])
|
||||||
|
|
@ -567,7 +559,7 @@ export class Compiler {
|
||||||
instructions.push(...body)
|
instructions.push(...body)
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`FunctionCallWithBlock: Expected FunctionCallOrIdentifier or FunctionCall`,
|
`FunctionCallWithBlock: Expected FunctionCallOrIdentifier or FunctionCall`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -582,7 +574,7 @@ export class Compiler {
|
||||||
catchVariable,
|
catchVariable,
|
||||||
catchBody,
|
catchBody,
|
||||||
finallyBody,
|
finallyBody,
|
||||||
input,
|
input
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -595,7 +587,7 @@ export class Compiler {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`${keyword} expected expression, got ${children.length} children`,
|
`${keyword} expected expression, got ${children.length} children`,
|
||||||
node.from,
|
node.from,
|
||||||
node.to,
|
node.to
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -609,7 +601,7 @@ export class Compiler {
|
||||||
case 'IfExpr': {
|
case 'IfExpr': {
|
||||||
const { conditionNode, thenBlock, elseIfBlocks, elseThenBlock } = getIfExprParts(
|
const { conditionNode, thenBlock, elseIfBlocks, elseThenBlock } = getIfExprParts(
|
||||||
node,
|
node,
|
||||||
input,
|
input
|
||||||
)
|
)
|
||||||
const instructions: ProgramItem[] = []
|
const instructions: ProgramItem[] = []
|
||||||
instructions.push(...this.#compileNode(conditionNode, input))
|
instructions.push(...this.#compileNode(conditionNode, input))
|
||||||
|
|
@ -740,13 +732,13 @@ export class Compiler {
|
||||||
|
|
||||||
const { identifierNode, namedArgs, positionalArgs } = getFunctionCallParts(
|
const { identifierNode, namedArgs, positionalArgs } = getFunctionCallParts(
|
||||||
pipeReceiver,
|
pipeReceiver,
|
||||||
input,
|
input
|
||||||
)
|
)
|
||||||
|
|
||||||
instructions.push(...this.#compileNode(identifierNode, input))
|
instructions.push(...this.#compileNode(identifierNode, input))
|
||||||
|
|
||||||
const isUnderscoreInPositionalArgs = positionalArgs.some((arg) =>
|
const isUnderscoreInPositionalArgs = positionalArgs.some(
|
||||||
arg.type.is('Underscore'),
|
(arg) => arg.type.is('Underscore')
|
||||||
)
|
)
|
||||||
const isUnderscoreInNamedArgs = namedArgs.some((arg) => {
|
const isUnderscoreInNamedArgs = namedArgs.some((arg) => {
|
||||||
const { valueNode } = getNamedArgParts(arg, input)
|
const { valueNode } = getNamedArgParts(arg, input)
|
||||||
|
|
@ -845,12 +837,14 @@ export class Compiler {
|
||||||
case 'Import': {
|
case 'Import': {
|
||||||
const instructions: ProgramItem[] = []
|
const instructions: ProgramItem[] = []
|
||||||
const [_import, ...nodes] = getAllChildren(node)
|
const [_import, ...nodes] = getAllChildren(node)
|
||||||
const args = nodes.filter((node) => node.type.is('Identifier'))
|
const args = nodes.filter(node => node.type.is('Identifier'))
|
||||||
const namedArgs = nodes.filter((node) => node.type.is('NamedArg'))
|
const namedArgs = nodes.filter(node => node.type.is('NamedArg'))
|
||||||
|
|
||||||
instructions.push(['LOAD', 'import'])
|
instructions.push(['LOAD', 'import'])
|
||||||
|
|
||||||
args.forEach((dict) => instructions.push(['PUSH', input.slice(dict.from, dict.to)]))
|
args.forEach((dict) =>
|
||||||
|
instructions.push(['PUSH', input.slice(dict.from, dict.to)])
|
||||||
|
)
|
||||||
|
|
||||||
namedArgs.forEach((arg) => {
|
namedArgs.forEach((arg) => {
|
||||||
const { name, valueNode } = getNamedArgParts(arg, input)
|
const { name, valueNode } = getNamedArgParts(arg, input)
|
||||||
|
|
@ -873,7 +867,7 @@ export class Compiler {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`Compiler doesn't know how to handle a "${node.type.name}" node.`,
|
`Compiler doesn't know how to handle a "${node.type.name}" node.`,
|
||||||
node.from,
|
node.from,
|
||||||
node.to,
|
node.to
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -883,7 +877,7 @@ export class Compiler {
|
||||||
catchVariable: string | undefined,
|
catchVariable: string | undefined,
|
||||||
catchBody: SyntaxNode | undefined,
|
catchBody: SyntaxNode | undefined,
|
||||||
finallyBody: SyntaxNode | undefined,
|
finallyBody: SyntaxNode | undefined,
|
||||||
input: string,
|
input: string
|
||||||
): ProgramItem[] {
|
): ProgramItem[] {
|
||||||
const instructions: ProgramItem[] = []
|
const instructions: ProgramItem[] = []
|
||||||
this.tryLabelCount++
|
this.tryLabelCount++
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
export class CompilerError extends Error {
|
export class CompilerError extends Error {
|
||||||
constructor(
|
constructor(message: string, private from: number, private to: number) {
|
||||||
message: string,
|
|
||||||
private from: number,
|
|
||||||
private to: number,
|
|
||||||
) {
|
|
||||||
super(message)
|
super(message)
|
||||||
|
|
||||||
if (from < 0 || to < 0 || to < from) {
|
if (from < 0 || to < 0 || to < from) {
|
||||||
|
|
|
||||||
|
|
@ -112,12 +112,8 @@ describe('compiler', () => {
|
||||||
test('function call with no args', () => {
|
test('function call with no args', () => {
|
||||||
expect(`bloop = do: 'bleep' end; bloop`).toEvaluateTo('bleep')
|
expect(`bloop = do: 'bleep' end; bloop`).toEvaluateTo('bleep')
|
||||||
expect(`bloop = [ go=do: 'bleep' end ]; bloop.go`).toEvaluateTo('bleep')
|
expect(`bloop = [ go=do: 'bleep' end ]; bloop.go`).toEvaluateTo('bleep')
|
||||||
expect(`bloop = [ go=do: 'bleep' end ]; abc = do x: x end; abc (bloop.go)`).toEvaluateTo(
|
expect(`bloop = [ go=do: 'bleep' end ]; abc = do x: x end; abc (bloop.go)`).toEvaluateTo('bleep')
|
||||||
'bleep',
|
expect(`num = ((math.random) * 10 + 1) | math.floor; num >= 1 and num <= 10 `).toEvaluateTo(true)
|
||||||
)
|
|
||||||
expect(`num = ((math.random) * 10 + 1) | math.floor; num >= 1 and num <= 10 `).toEvaluateTo(
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('function call with if statement and multiple expressions', () => {
|
test('function call with if statement and multiple expressions', () => {
|
||||||
|
|
@ -380,7 +376,7 @@ describe('default params', () => {
|
||||||
age: 60,
|
age: 60,
|
||||||
})
|
})
|
||||||
expect(
|
expect(
|
||||||
'make-person = do person=[name=Bob age=60]: person end; make-person [name=Jon age=21]',
|
'make-person = do person=[name=Bob age=60]: person end; make-person [name=Jon age=21]'
|
||||||
).toEvaluateTo({ name: 'Jon', age: 21 })
|
).toEvaluateTo({ name: 'Jon', age: 21 })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -412,9 +408,7 @@ describe('Nullish coalescing operator (??)', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('short-circuits evaluation', () => {
|
test('short-circuits evaluation', () => {
|
||||||
const throwError = () => {
|
const throwError = () => { throw new Error('Should not evaluate') }
|
||||||
throw new Error('Should not evaluate')
|
|
||||||
}
|
|
||||||
expect('5 ?? throw-error').toEvaluateTo(5, { 'throw-error': throwError })
|
expect('5 ?? throw-error').toEvaluateTo(5, { 'throw-error': throwError })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -430,7 +424,7 @@ describe('Nullish coalescing operator (??)', () => {
|
||||||
// Use explicit call syntax to invoke the function
|
// Use explicit call syntax to invoke the function
|
||||||
expect('(get-value) ?? (get-default)').toEvaluateTo(42, {
|
expect('(get-value) ?? (get-default)').toEvaluateTo(42, {
|
||||||
'get-value': getValue,
|
'get-value': getValue,
|
||||||
'get-default': getDefault,
|
'get-default': getDefault
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -462,9 +456,7 @@ describe('Nullish coalescing assignment (??=)', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('short-circuits evaluation when not null', () => {
|
test('short-circuits evaluation when not null', () => {
|
||||||
const throwError = () => {
|
const throwError = () => { throw new Error('Should not evaluate') }
|
||||||
throw new Error('Should not evaluate')
|
|
||||||
}
|
|
||||||
expect('x = 5; x ??= throw-error; x').toEvaluateTo(5, { 'throw-error': throwError })
|
expect('x = 5; x ??= throw-error; x').toEvaluateTo(5, { 'throw-error': throwError })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,16 +10,12 @@ describe('single line function blocks', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('work with named args', () => {
|
test('work with named args', () => {
|
||||||
expect(
|
expect(`attach = do signal fn: [ signal (fn) ] end; attach signal='exit': true end`).toEvaluateTo(['exit', true])
|
||||||
`attach = do signal fn: [ signal (fn) ] end; attach signal='exit': true end`,
|
|
||||||
).toEvaluateTo(['exit', true])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
test('work with dot-get', () => {
|
test('work with dot-get', () => {
|
||||||
expect(`signals = [trap=do x y: [x (y)] end]; signals.trap 'EXIT': true end`).toEvaluateTo([
|
expect(`signals = [trap=do x y: [x (y)] end]; signals.trap 'EXIT': true end`).toEvaluateTo(['EXIT', true])
|
||||||
'EXIT',
|
|
||||||
true,
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -48,6 +44,7 @@ attach signal='exit':
|
||||||
end`).toEvaluateTo(['exit', true])
|
end`).toEvaluateTo(['exit', true])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
test('work with dot-get', () => {
|
test('work with dot-get', () => {
|
||||||
expect(`
|
expect(`
|
||||||
signals = [trap=do x y: [x (y)] end]
|
signals = [trap=do x y: [x (y)] end]
|
||||||
|
|
|
||||||
|
|
@ -219,7 +219,7 @@ describe('dict literals', () => {
|
||||||
|
|
||||||
describe('curly strings', () => {
|
describe('curly strings', () => {
|
||||||
test('work on one line', () => {
|
test('work on one line', () => {
|
||||||
expect('{ one two three }').toEvaluateTo(' one two three ')
|
expect('{ one two three }').toEvaluateTo(" one two three ")
|
||||||
})
|
})
|
||||||
|
|
||||||
test('work on multiple lines', () => {
|
test('work on multiple lines', () => {
|
||||||
|
|
@ -227,7 +227,7 @@ describe('curly strings', () => {
|
||||||
one
|
one
|
||||||
two
|
two
|
||||||
three
|
three
|
||||||
}`).toEvaluateTo('\n one\n two\n three\n ')
|
}`).toEvaluateTo("\n one\n two\n three\n ")
|
||||||
})
|
})
|
||||||
|
|
||||||
test('can contain other curlies', () => {
|
test('can contain other curlies', () => {
|
||||||
|
|
@ -235,7 +235,7 @@ describe('curly strings', () => {
|
||||||
{ one }
|
{ one }
|
||||||
two
|
two
|
||||||
{ three }
|
{ three }
|
||||||
}`).toEvaluateTo('\n { one }\n two\n { three }\n ')
|
}`).toEvaluateTo("\n { one }\n two\n { three }\n ")
|
||||||
})
|
})
|
||||||
|
|
||||||
test('interpolates variables', () => {
|
test('interpolates variables', () => {
|
||||||
|
|
@ -263,7 +263,7 @@ describe('curly strings', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('double quoted strings', () => {
|
describe('double quoted strings', () => {
|
||||||
test('work', () => {
|
test("work", () => {
|
||||||
expect(`"hello world"`).toEvaluateTo('hello world')
|
expect(`"hello world"`).toEvaluateTo('hello world')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -272,11 +272,11 @@ describe('double quoted strings', () => {
|
||||||
expect(`"hello $(1 + 2)"`).toEvaluateTo('hello $(1 + 2)')
|
expect(`"hello $(1 + 2)"`).toEvaluateTo('hello $(1 + 2)')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('equal regular strings', () => {
|
test("equal regular strings", () => {
|
||||||
expect(`"hello world" == 'hello world'`).toEvaluateTo(true)
|
expect(`"hello world" == 'hello world'`).toEvaluateTo(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('can contain newlines', () => {
|
test("can contain newlines", () => {
|
||||||
expect(`
|
expect(`
|
||||||
"hello
|
"hello
|
||||||
world"`).toEvaluateTo('hello\n world')
|
world"`).toEvaluateTo('hello\n world')
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ describe('Native Function Exceptions', () => {
|
||||||
const vm = new VM(compiler.bytecode)
|
const vm = new VM(compiler.bytecode)
|
||||||
|
|
||||||
vm.set('async-fail', async () => {
|
vm.set('async-fail', async () => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1))
|
await new Promise(resolve => setTimeout(resolve, 1))
|
||||||
throw new Error('async error')
|
throw new Error('async error')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -237,7 +237,7 @@ describe('Native Function Exceptions', () => {
|
||||||
const result = await vm.run()
|
const result = await vm.run()
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
type: 'string',
|
type: 'string',
|
||||||
value: 'This is a very specific error message with details',
|
value: 'This is a very specific error message with details'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ const buffer: string[] = []
|
||||||
const ribbitGlobals = {
|
const ribbitGlobals = {
|
||||||
ribbit: async (cb: Function) => {
|
ribbit: async (cb: Function) => {
|
||||||
await cb()
|
await cb()
|
||||||
return buffer.join('\n')
|
return buffer.join("\n")
|
||||||
},
|
},
|
||||||
tag: async (tagFn: Function, atDefaults = {}) => {
|
tag: async (tagFn: Function, atDefaults = {}) => {
|
||||||
return (atNamed = {}, ...args: any[]) => tagFn(Object.assign({}, atDefaults, atNamed), ...args)
|
return (atNamed = {}, ...args: any[]) => tagFn(Object.assign({}, atDefaults, atNamed), ...args)
|
||||||
|
|
@ -20,12 +20,10 @@ const ribbitGlobals = {
|
||||||
ul: (atNamed: {}, ...args: any[]) => tag('ul', atNamed, ...args),
|
ul: (atNamed: {}, ...args: any[]) => tag('ul', atNamed, ...args),
|
||||||
li: (atNamed: {}, ...args: any[]) => tag('li', atNamed, ...args),
|
li: (atNamed: {}, ...args: any[]) => tag('li', atNamed, ...args),
|
||||||
nospace: () => NOSPACE_TOKEN,
|
nospace: () => NOSPACE_TOKEN,
|
||||||
echo: (...args: any[]) => console.log(...args),
|
echo: (...args: any[]) => console.log(...args)
|
||||||
}
|
}
|
||||||
|
|
||||||
function raw(fn: Function) {
|
function raw(fn: Function) { (fn as any).raw = true }
|
||||||
;(fn as any).raw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const tagBlock = async (tagName: string, props = {}, fn: Function) => {
|
const tagBlock = async (tagName: string, props = {}, fn: Function) => {
|
||||||
const attrs = Object.entries(props).map(([key, value]) => `${key}="${value}"`)
|
const attrs = Object.entries(props).map(([key, value]) => `${key}="${value}"`)
|
||||||
|
|
@ -41,13 +39,14 @@ const tagCall = (tagName: string, atNamed = {}, ...args: any[]) => {
|
||||||
const space = attrs.length ? ' ' : ''
|
const space = attrs.length ? ' ' : ''
|
||||||
const children = args
|
const children = args
|
||||||
.reverse()
|
.reverse()
|
||||||
.map((a) => (a === TAG_TOKEN ? buffer.pop() : a))
|
.map(a => a === TAG_TOKEN ? buffer.pop() : a)
|
||||||
.reverse()
|
.reverse().join(' ')
|
||||||
.join(' ')
|
|
||||||
.replaceAll(` ${NOSPACE_TOKEN} `, '')
|
.replaceAll(` ${NOSPACE_TOKEN} `, '')
|
||||||
|
|
||||||
if (SELF_CLOSING.includes(tagName)) buffer.push(`<${tagName}${space}${attrs.join(' ')} />`)
|
if (SELF_CLOSING.includes(tagName))
|
||||||
else buffer.push(`<${tagName}${space}${attrs.join(' ')}>${children}</${tagName}>`)
|
buffer.push(`<${tagName}${space}${attrs.join(' ')} />`)
|
||||||
|
else
|
||||||
|
buffer.push(`<${tagName}${space}${attrs.join(' ')}>${children}</${tagName}>`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const tag = async (tagName: string, atNamed = {}, ...args: any[]) => {
|
const tag = async (tagName: string, atNamed = {}, ...args: any[]) => {
|
||||||
|
|
@ -61,25 +60,10 @@ const tag = async (tagName: string, atNamed = {}, ...args: any[]) => {
|
||||||
|
|
||||||
const NOSPACE_TOKEN = '!!ribbit-nospace!!'
|
const NOSPACE_TOKEN = '!!ribbit-nospace!!'
|
||||||
const TAG_TOKEN = '!!ribbit-tag!!'
|
const TAG_TOKEN = '!!ribbit-tag!!'
|
||||||
const SELF_CLOSING = [
|
const SELF_CLOSING = ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]
|
||||||
'area',
|
|
||||||
'base',
|
|
||||||
'br',
|
|
||||||
'col',
|
|
||||||
'embed',
|
|
||||||
'hr',
|
|
||||||
'img',
|
|
||||||
'input',
|
|
||||||
'link',
|
|
||||||
'meta',
|
|
||||||
'param',
|
|
||||||
'source',
|
|
||||||
'track',
|
|
||||||
'wbr',
|
|
||||||
]
|
|
||||||
|
|
||||||
describe('ribbit', () => {
|
describe('ribbit', () => {
|
||||||
beforeEach(() => (buffer.length = 0))
|
beforeEach(() => buffer.length = 0)
|
||||||
|
|
||||||
test('head tag', () => {
|
test('head tag', () => {
|
||||||
expect(`
|
expect(`
|
||||||
|
|
@ -90,14 +74,11 @@ ribbit:
|
||||||
meta name=viewport content='width=device-width, initial-scale=1, viewport-fit=cover'
|
meta name=viewport content='width=device-width, initial-scale=1, viewport-fit=cover'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
`).toEvaluateTo(
|
`).toEvaluateTo(`<head>
|
||||||
`<head>
|
|
||||||
<title>What up</title>
|
<title>What up</title>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
</head>`,
|
</head>`, ribbitGlobals)
|
||||||
ribbitGlobals,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('custom tags', () => {
|
test('custom tags', () => {
|
||||||
|
|
@ -109,14 +90,11 @@ ribbit:
|
||||||
li two
|
li two
|
||||||
li three
|
li three
|
||||||
end
|
end
|
||||||
end`).toEvaluateTo(
|
end`).toEvaluateTo(`<ul class="list">
|
||||||
`<ul class="list">
|
|
||||||
<li border-bottom="1px solid black">one</li>
|
<li border-bottom="1px solid black">one</li>
|
||||||
<li>two</li>
|
<li>two</li>
|
||||||
<li>three</li>
|
<li>three</li>
|
||||||
</ul>`,
|
</ul>`, ribbitGlobals)
|
||||||
ribbitGlobals,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('inline expressions', () => {
|
test('inline expressions', () => {
|
||||||
|
|
@ -132,8 +110,6 @@ end`).toEvaluateTo(
|
||||||
<h1 class="bright" style="font-family: helvetica">Heya</h1>
|
<h1 class="bright" style="font-family: helvetica">Heya</h1>
|
||||||
<h2>man that is <b>wild</b>!</h2>
|
<h2>man that is <b>wild</b>!</h2>
|
||||||
<p>Double the fun.</p>
|
<p>Double the fun.</p>
|
||||||
</p>`,
|
</p>`, ribbitGlobals)
|
||||||
ribbitGlobals,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -10,7 +10,8 @@ describe('while', () => {
|
||||||
a = false
|
a = false
|
||||||
b = done
|
b = done
|
||||||
end
|
end
|
||||||
b`).toEvaluateTo('done')
|
b`)
|
||||||
|
.toEvaluateTo('done')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('basic expression', () => {
|
test('basic expression', () => {
|
||||||
|
|
@ -19,7 +20,8 @@ describe('while', () => {
|
||||||
while a < 10:
|
while a < 10:
|
||||||
a += 1
|
a += 1
|
||||||
end
|
end
|
||||||
a`).toEvaluateTo(10)
|
a`)
|
||||||
|
.toEvaluateTo(10)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('compound expression', () => {
|
test('compound expression', () => {
|
||||||
|
|
@ -29,7 +31,8 @@ describe('while', () => {
|
||||||
while a > 0 and b < 100:
|
while a > 0 and b < 100:
|
||||||
b += 1
|
b += 1
|
||||||
end
|
end
|
||||||
b`).toEvaluateTo(100)
|
b`)
|
||||||
|
.toEvaluateTo(100)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('returns value', () => {
|
test('returns value', () => {
|
||||||
|
|
@ -39,6 +42,7 @@ describe('while', () => {
|
||||||
a += 1
|
a += 1
|
||||||
done
|
done
|
||||||
end
|
end
|
||||||
ret`).toEvaluateTo('done')
|
ret`)
|
||||||
|
.toEvaluateTo('done')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -45,7 +45,7 @@ export const getAssignmentParts = (node: SyntaxNode) => {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`Assign expected 3 children, got ${children.length}`,
|
`Assign expected 3 children, got ${children.length}`,
|
||||||
node.from,
|
node.from,
|
||||||
node.to,
|
node.to
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,11 +57,10 @@ export const getAssignmentParts = (node: SyntaxNode) => {
|
||||||
|
|
||||||
if (!left || !left.type.is('AssignableIdentifier')) {
|
if (!left || !left.type.is('AssignableIdentifier')) {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`Assign left child must be an AssignableIdentifier or Array, got ${
|
`Assign left child must be an AssignableIdentifier or Array, got ${left ? left.type.name : 'none'
|
||||||
left ? left.type.name : 'none'
|
|
||||||
}`,
|
}`,
|
||||||
node.from,
|
node.from,
|
||||||
node.to,
|
node.to
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,17 +73,16 @@ export const getCompoundAssignmentParts = (node: SyntaxNode) => {
|
||||||
|
|
||||||
if (!left || !left.type.is('AssignableIdentifier')) {
|
if (!left || !left.type.is('AssignableIdentifier')) {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`CompoundAssign left child must be an AssignableIdentifier, got ${
|
`CompoundAssign left child must be an AssignableIdentifier, got ${left ? left.type.name : 'none'
|
||||||
left ? left.type.name : 'none'
|
|
||||||
}`,
|
}`,
|
||||||
node.from,
|
node.from,
|
||||||
node.to,
|
node.to
|
||||||
)
|
)
|
||||||
} else if (!operator || !right) {
|
} else if (!operator || !right) {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`CompoundAssign expected 3 children, got ${children.length}`,
|
`CompoundAssign expected 3 children, got ${children.length}`,
|
||||||
node.from,
|
node.from,
|
||||||
node.to,
|
node.to
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,7 +97,7 @@ export const getFunctionDefParts = (node: SyntaxNode, input: string) => {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`FunctionDef expected at least 4 children, got ${children.length}`,
|
`FunctionDef expected at least 4 children, got ${children.length}`,
|
||||||
node.from,
|
node.from,
|
||||||
node.to,
|
node.to
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,7 +106,7 @@ export const getFunctionDefParts = (node: SyntaxNode, input: string) => {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`FunctionDef params must be Identifier or NamedParam, got ${param.type.name}`,
|
`FunctionDef params must be Identifier or NamedParam, got ${param.type.name}`,
|
||||||
param.from,
|
param.from,
|
||||||
param.to,
|
param.to
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return input.slice(param.from, param.to)
|
return input.slice(param.from, param.to)
|
||||||
|
|
@ -131,7 +129,7 @@ export const getFunctionDefParts = (node: SyntaxNode, input: string) => {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`CatchExpr expected identifier and body, got ${catchChildren.length} children`,
|
`CatchExpr expected identifier and body, got ${catchChildren.length} children`,
|
||||||
child.from,
|
child.from,
|
||||||
child.to,
|
child.to
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
catchVariable = input.slice(identifierNode.from, identifierNode.to)
|
catchVariable = input.slice(identifierNode.from, identifierNode.to)
|
||||||
|
|
@ -144,7 +142,7 @@ export const getFunctionDefParts = (node: SyntaxNode, input: string) => {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`FinallyExpr expected body, got ${finallyChildren.length} children`,
|
`FinallyExpr expected body, got ${finallyChildren.length} children`,
|
||||||
child.from,
|
child.from,
|
||||||
child.to,
|
child.to
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
finallyBody = body
|
finallyBody = body
|
||||||
|
|
@ -199,7 +197,7 @@ export const getIfExprParts = (node: SyntaxNode, input: string) => {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`IfExpr expected at least 4 children, got ${children.length}`,
|
`IfExpr expected at least 4 children, got ${children.length}`,
|
||||||
node.from,
|
node.from,
|
||||||
node.to,
|
node.to
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,6 +251,7 @@ export const getStringParts = (node: SyntaxNode, input: string) => {
|
||||||
child.type.is('Interpolation') ||
|
child.type.is('Interpolation') ||
|
||||||
child.type.is('EscapeSeq') ||
|
child.type.is('EscapeSeq') ||
|
||||||
child.type.is('CurlyString')
|
child.type.is('CurlyString')
|
||||||
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -267,14 +266,16 @@ export const getStringParts = (node: SyntaxNode, input: string) => {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`String child must be StringFragment, Interpolation, or EscapeSeq, got ${part.type.name}`,
|
`String child must be StringFragment, Interpolation, or EscapeSeq, got ${part.type.name}`,
|
||||||
part.from,
|
part.from,
|
||||||
part.to,
|
part.to
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 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((p) => p.type.is('Interpolation') || p.type.is('EscapeSeq'))
|
const hasInterpolation = parts.some(
|
||||||
|
(p) => p.type.is('Interpolation') || p.type.is('EscapeSeq')
|
||||||
|
)
|
||||||
return { parts, hasInterpolation }
|
return { parts, hasInterpolation }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -286,7 +287,7 @@ export const getDotGetParts = (node: SyntaxNode, input: string) => {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`DotGet expected 2 identifier children, got ${children.length}`,
|
`DotGet expected 2 identifier children, got ${children.length}`,
|
||||||
node.from,
|
node.from,
|
||||||
node.to,
|
node.to
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -294,7 +295,7 @@ export const getDotGetParts = (node: SyntaxNode, input: string) => {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`DotGet object must be an IdentifierBeforeDot, got ${object.type.name}`,
|
`DotGet object must be an IdentifierBeforeDot, got ${object.type.name}`,
|
||||||
object.from,
|
object.from,
|
||||||
object.to,
|
object.to
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -302,7 +303,7 @@ export const getDotGetParts = (node: SyntaxNode, input: string) => {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`DotGet property must be an Identifier, Number, ParenExpr, or DotGet, got ${property.type.name}`,
|
`DotGet property must be an Identifier, Number, ParenExpr, or DotGet, got ${property.type.name}`,
|
||||||
property.from,
|
property.from,
|
||||||
property.to,
|
property.to
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -321,7 +322,7 @@ export const getTryExprParts = (node: SyntaxNode, input: string) => {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`TryExpr expected at least 3 children, got ${children.length}`,
|
`TryExpr expected at least 3 children, got ${children.length}`,
|
||||||
node.from,
|
node.from,
|
||||||
node.to,
|
node.to
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,7 +341,7 @@ export const getTryExprParts = (node: SyntaxNode, input: string) => {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`CatchExpr expected identifier and body, got ${catchChildren.length} children`,
|
`CatchExpr expected identifier and body, got ${catchChildren.length} children`,
|
||||||
child.from,
|
child.from,
|
||||||
child.to,
|
child.to
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
catchVariable = input.slice(identifierNode.from, identifierNode.to)
|
catchVariable = input.slice(identifierNode.from, identifierNode.to)
|
||||||
|
|
@ -353,7 +354,7 @@ export const getTryExprParts = (node: SyntaxNode, input: string) => {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`FinallyExpr expected body, got ${finallyChildren.length} children`,
|
`FinallyExpr expected body, got ${finallyChildren.length} children`,
|
||||||
child.from,
|
child.from,
|
||||||
child.to,
|
child.to
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
finallyBody = body
|
finallyBody = body
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ const valueToString = (value: Value | string): string => {
|
||||||
return `${value.value.map(valueToString).join('\n')}`
|
return `${value.value.map(valueToString).join('\n')}`
|
||||||
case 'dict': {
|
case 'dict': {
|
||||||
const entries = Array.from(value.value.entries()).map(
|
const entries = Array.from(value.value.entries()).map(
|
||||||
([key, val]) => `"${key}": ${valueToString(val)}`,
|
([key, val]) => `"${key}": ${valueToString(val)}`
|
||||||
)
|
)
|
||||||
return `{${entries.join(', ')}}`
|
return `{${entries.join(', ')}}`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,6 @@ import { EditorView } from '@codemirror/view'
|
||||||
export const catchErrors = EditorView.exceptionSink.of((exception) => {
|
export const catchErrors = EditorView.exceptionSink.of((exception) => {
|
||||||
console.error('CodeMirror error:', exception)
|
console.error('CodeMirror error:', exception)
|
||||||
errorSignal.emit(
|
errorSignal.emit(
|
||||||
`Editor error: ${exception instanceof Error ? exception.message : String(exception)}`,
|
`Editor error: ${exception instanceof Error ? exception.message : String(exception)}`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -31,5 +31,5 @@ export const debugTags = ViewPlugin.fromClass(
|
||||||
order: -1,
|
order: -1,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -58,5 +58,5 @@ export const shrimpErrors = ViewPlugin.fromClass(
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
decorations: (v) => v.decorations,
|
decorations: (v) => v.decorations,
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,7 @@ export const inlineHints = [
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
),
|
),
|
||||||
ghostTextTheme,
|
ghostTextTheme,
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export const persistencePlugin = ViewPlugin.fromClass(
|
||||||
destroy() {
|
destroy() {
|
||||||
if (this.saveTimeout) clearTimeout(this.saveTimeout)
|
if (this.saveTimeout) clearTimeout(this.saveTimeout)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const getContent = () => {
|
export const getContent = () => {
|
||||||
|
|
|
||||||
|
|
@ -56,5 +56,5 @@ export const shrimpTheme = EditorView.theme(
|
||||||
backgroundColor: 'var(--color-string)',
|
backgroundColor: 'var(--color-string)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ dark: true },
|
{ dark: true }
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -53,10 +53,7 @@ export class Shrimp {
|
||||||
let bytecode
|
let bytecode
|
||||||
|
|
||||||
if (typeof code === 'string') {
|
if (typeof code === 'string') {
|
||||||
const compiler = new Compiler(
|
const compiler = new Compiler(code, Object.keys(Object.assign({}, prelude, this.globals ?? {}, locals ?? {})))
|
||||||
code,
|
|
||||||
Object.keys(Object.assign({}, prelude, this.globals ?? {}, locals ?? {})),
|
|
||||||
)
|
|
||||||
bytecode = compiler.bytecode
|
bytecode = compiler.bytecode
|
||||||
} else {
|
} else {
|
||||||
bytecode = code
|
bytecode = code
|
||||||
|
|
@ -69,6 +66,7 @@ export class Shrimp {
|
||||||
|
|
||||||
return this.vm.stack.length ? fromValue(this.vm.stack.at(-1)!, this.vm) : null
|
return this.vm.stack.length ? fromValue(this.vm.stack.at(-1)!, this.vm) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runFile(path: string, globals?: Record<string, any>): Promise<any> {
|
export async function runFile(path: string, globals?: Record<string, any>): Promise<any> {
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,8 @@ export const tokenizeCurlyString = (value: string): (string | [string, SyntaxNod
|
||||||
if (!char) break
|
if (!char) break
|
||||||
if (!isIdentStart(char.charCodeAt(0))) break
|
if (!isIdentStart(char.charCodeAt(0))) break
|
||||||
|
|
||||||
while (char && isIdentChar(char.charCodeAt(0))) char = value[++pos]
|
while (char && isIdentChar(char.charCodeAt(0)))
|
||||||
|
char = value[++pos]
|
||||||
|
|
||||||
const input = value.slice(start + 1, pos) // skip '$'
|
const input = value.slice(start + 1, pos) // skip '$'
|
||||||
tokens.push([input, parse(input)])
|
tokens.push([input, parse(input)])
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,18 @@ import { type Token, TokenType } from './tokenizer2'
|
||||||
export type NodeType =
|
export type NodeType =
|
||||||
| 'Program'
|
| 'Program'
|
||||||
| 'Block'
|
| 'Block'
|
||||||
|
|
||||||
| 'FunctionCall'
|
| 'FunctionCall'
|
||||||
| 'FunctionCallOrIdentifier'
|
| 'FunctionCallOrIdentifier'
|
||||||
| 'FunctionCallWithBlock'
|
| 'FunctionCallWithBlock'
|
||||||
| 'PositionalArg'
|
| 'PositionalArg'
|
||||||
| 'NamedArg'
|
| 'NamedArg'
|
||||||
| 'NamedArgPrefix'
|
| 'NamedArgPrefix'
|
||||||
|
|
||||||
| 'FunctionDef'
|
| 'FunctionDef'
|
||||||
| 'Params'
|
| 'Params'
|
||||||
| 'NamedParam'
|
| 'NamedParam'
|
||||||
|
|
||||||
| 'Null'
|
| 'Null'
|
||||||
| 'Boolean'
|
| 'Boolean'
|
||||||
| 'Number'
|
| 'Number'
|
||||||
|
|
@ -29,6 +32,7 @@ export type NodeType =
|
||||||
| 'Array'
|
| 'Array'
|
||||||
| 'Dict'
|
| 'Dict'
|
||||||
| 'Comment'
|
| 'Comment'
|
||||||
|
|
||||||
| 'BinOp'
|
| 'BinOp'
|
||||||
| 'ConditionalOp'
|
| 'ConditionalOp'
|
||||||
| 'ParenExpr'
|
| 'ParenExpr'
|
||||||
|
|
@ -36,6 +40,7 @@ export type NodeType =
|
||||||
| 'CompoundAssign'
|
| 'CompoundAssign'
|
||||||
| 'DotGet'
|
| 'DotGet'
|
||||||
| 'PipeExpr'
|
| 'PipeExpr'
|
||||||
|
|
||||||
| 'IfExpr'
|
| 'IfExpr'
|
||||||
| 'ElseIfExpr'
|
| 'ElseIfExpr'
|
||||||
| 'ElseExpr'
|
| 'ElseExpr'
|
||||||
|
|
@ -44,12 +49,14 @@ export type NodeType =
|
||||||
| 'CatchExpr'
|
| 'CatchExpr'
|
||||||
| 'FinallyExpr'
|
| 'FinallyExpr'
|
||||||
| 'Throw'
|
| 'Throw'
|
||||||
|
|
||||||
| 'Not'
|
| 'Not'
|
||||||
| 'Eq'
|
| 'Eq'
|
||||||
| 'Modulo'
|
| 'Modulo'
|
||||||
| 'Plus'
|
| 'Plus'
|
||||||
| 'Star'
|
| 'Star'
|
||||||
| 'Slash'
|
| 'Slash'
|
||||||
|
|
||||||
| 'Import'
|
| 'Import'
|
||||||
| 'Do'
|
| 'Do'
|
||||||
| 'Underscore'
|
| 'Underscore'
|
||||||
|
|
@ -60,13 +67,13 @@ export type NodeType =
|
||||||
// TODO: remove this when we switch from lezer
|
// TODO: remove this when we switch from lezer
|
||||||
export const operators: Record<string, any> = {
|
export const operators: Record<string, any> = {
|
||||||
// Logic
|
// Logic
|
||||||
and: 'And',
|
'and': 'And',
|
||||||
or: 'Or',
|
'or': 'Or',
|
||||||
|
|
||||||
// Bitwise
|
// Bitwise
|
||||||
band: 'Band',
|
'band': 'Band',
|
||||||
bor: 'Bor',
|
'bor': 'Bor',
|
||||||
bxor: 'Bxor',
|
'bxor': 'Bxor',
|
||||||
'>>>': 'Ushr',
|
'>>>': 'Ushr',
|
||||||
'>>': 'Shr',
|
'>>': 'Shr',
|
||||||
'<<': 'Shl',
|
'<<': 'Shl',
|
||||||
|
|
@ -151,17 +158,12 @@ export class 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(): {
|
get type(): { type: NodeType, name: NodeType, isError: boolean, is: (other: NodeType) => boolean } {
|
||||||
type: NodeType
|
|
||||||
name: NodeType
|
|
||||||
isError: boolean
|
|
||||||
is: (other: NodeType) => boolean
|
|
||||||
} {
|
|
||||||
return {
|
return {
|
||||||
type: this.#type,
|
type: this.#type,
|
||||||
name: this.#type,
|
name: this.#type,
|
||||||
isError: this.#isError,
|
isError: this.#isError,
|
||||||
is: (other: NodeType) => other === this.#type,
|
is: (other: NodeType) => other === this.#type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,7 +211,7 @@ export class SyntaxNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
push(...nodes: SyntaxNode[]): SyntaxNode {
|
push(...nodes: SyntaxNode[]): SyntaxNode {
|
||||||
nodes.forEach((child) => (child.parent = this))
|
nodes.forEach(child => child.parent = this)
|
||||||
this.children.push(...nodes)
|
this.children.push(...nodes)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
@ -222,8 +224,8 @@ export class SyntaxNode {
|
||||||
// Operator precedence (binding power) - higher = tighter binding
|
// Operator precedence (binding power) - higher = tighter binding
|
||||||
export const precedence: Record<string, number> = {
|
export const precedence: Record<string, number> = {
|
||||||
// Logical
|
// Logical
|
||||||
or: 10,
|
'or': 10,
|
||||||
and: 20,
|
'and': 20,
|
||||||
|
|
||||||
// Comparison
|
// Comparison
|
||||||
'==': 30,
|
'==': 30,
|
||||||
|
|
@ -246,9 +248,9 @@ export const precedence: Record<string, number> = {
|
||||||
'-': 40,
|
'-': 40,
|
||||||
|
|
||||||
// Bitwise AND/OR/XOR (higher precedence than addition)
|
// Bitwise AND/OR/XOR (higher precedence than addition)
|
||||||
band: 45,
|
'band': 45,
|
||||||
bor: 45,
|
'bor': 45,
|
||||||
bxor: 45,
|
'bxor': 45,
|
||||||
|
|
||||||
// Multiplication/Division/Modulo
|
// Multiplication/Division/Modulo
|
||||||
'*': 50,
|
'*': 50,
|
||||||
|
|
@ -259,6 +261,10 @@ export const precedence: Record<string, number> = {
|
||||||
'**': 60,
|
'**': 60,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const conditionals = new Set(['==', '!=', '<', '>', '<=', '>=', '??', 'and', 'or'])
|
export const conditionals = new Set([
|
||||||
|
'==', '!=', '<', '>', '<=', '>=', '??', 'and', 'or'
|
||||||
|
])
|
||||||
|
|
||||||
export const compounds = ['??=', '+=', '-=', '*=', '/=', '%=']
|
export const compounds = [
|
||||||
|
'??=', '+=', '-=', '*=', '/=', '%='
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ export class Parser {
|
||||||
pos = 0
|
pos = 0
|
||||||
inParens = 0
|
inParens = 0
|
||||||
input = ''
|
input = ''
|
||||||
scope = new Scope()
|
scope = new Scope
|
||||||
inTestExpr = false
|
inTestExpr = false
|
||||||
|
|
||||||
parse(input: string): SyntaxNode {
|
parse(input: string): SyntaxNode {
|
||||||
|
|
@ -78,11 +78,14 @@ export class Parser {
|
||||||
|
|
||||||
// statement is a line of code
|
// statement is a line of code
|
||||||
statement(): SyntaxNode | null {
|
statement(): SyntaxNode | null {
|
||||||
if (this.is($T.Comment)) return this.comment()
|
if (this.is($T.Comment))
|
||||||
|
return this.comment()
|
||||||
|
|
||||||
while (this.is($T.Newline) || this.is($T.Semicolon)) this.next()
|
while (this.is($T.Newline) || this.is($T.Semicolon))
|
||||||
|
this.next()
|
||||||
|
|
||||||
if (this.isEOF() || this.isExprEndKeyword()) return null
|
if (this.isEOF() || this.isExprEndKeyword())
|
||||||
|
return null
|
||||||
|
|
||||||
return this.expression()
|
return this.expression()
|
||||||
}
|
}
|
||||||
|
|
@ -96,38 +99,51 @@ export class Parser {
|
||||||
let expr
|
let expr
|
||||||
|
|
||||||
// x = value
|
// x = value
|
||||||
if (
|
if (this.is($T.Identifier) && (
|
||||||
this.is($T.Identifier) &&
|
this.nextIs($T.Operator, '=') || compounds.some(x => this.nextIs($T.Operator, x))
|
||||||
(this.nextIs($T.Operator, '=') || compounds.some((x) => this.nextIs($T.Operator, x)))
|
))
|
||||||
)
|
|
||||||
expr = this.assign()
|
expr = this.assign()
|
||||||
|
|
||||||
// if, while, do, etc
|
// if, while, do, etc
|
||||||
else if (this.is($T.Keyword)) expr = this.keywords()
|
else if (this.is($T.Keyword))
|
||||||
|
expr = this.keywords()
|
||||||
|
|
||||||
// dotget
|
// dotget
|
||||||
else if (this.nextIs($T.Operator, '.')) expr = this.dotGetFunctionCall()
|
else if (this.nextIs($T.Operator, '.'))
|
||||||
|
expr = this.dotGetFunctionCall()
|
||||||
|
|
||||||
// echo hello world
|
// echo hello world
|
||||||
else if (this.is($T.Identifier) && !this.nextIs($T.Operator) && !this.nextIsExprEnd())
|
else if (this.is($T.Identifier) && !this.nextIs($T.Operator) && !this.nextIsExprEnd())
|
||||||
expr = this.functionCall()
|
expr = this.functionCall()
|
||||||
|
|
||||||
// bare-function-call
|
// bare-function-call
|
||||||
else if (this.is($T.Identifier) && this.nextIsExprEnd()) expr = this.functionCallOrIdentifier()
|
else if (this.is($T.Identifier) && this.nextIsExprEnd())
|
||||||
|
expr = this.functionCallOrIdentifier()
|
||||||
|
|
||||||
// everything else
|
// everything else
|
||||||
else expr = this.exprWithPrecedence()
|
else
|
||||||
|
expr = this.exprWithPrecedence()
|
||||||
|
|
||||||
// check for destructuring
|
// check for destructuring
|
||||||
if (expr.type.is('Array') && this.is($T.Operator, '=')) return this.destructure(expr)
|
if (expr.type.is('Array') && this.is($T.Operator, '='))
|
||||||
|
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
|
||||||
if (expr.type.is('ParenExpr') && !this.isExprEnd()) expr = this.functionCall(expr)
|
if (expr.type.is('ParenExpr') && !this.isExprEnd())
|
||||||
|
expr = this.functionCall(expr)
|
||||||
|
|
||||||
// if dotget is followed by binary operator, continue parsing as binary expression
|
// if dotget is followed by binary operator, continue parsing as binary expression
|
||||||
if (expr.type.is('DotGet') && this.is($T.Operator) && !this.is($T.Operator, '|'))
|
if (expr.type.is('DotGet') && this.is($T.Operator) && !this.is($T.Operator, '|'))
|
||||||
expr = this.dotGetBinOp(expr)
|
expr = this.dotGetBinOp(expr)
|
||||||
|
|
||||||
// one | echo
|
// one | echo
|
||||||
if (allowPipe && this.isPipe()) return this.pipe(expr)
|
if (allowPipe && this.isPipe())
|
||||||
|
return this.pipe(expr)
|
||||||
|
|
||||||
// regular
|
// regular
|
||||||
else return expr
|
else
|
||||||
|
return expr
|
||||||
}
|
}
|
||||||
|
|
||||||
// piping | stuff | is | cool
|
// piping | stuff | is | cool
|
||||||
|
|
@ -191,19 +207,26 @@ export class Parser {
|
||||||
|
|
||||||
// if, while, do, etc
|
// if, while, do, etc
|
||||||
keywords(): SyntaxNode {
|
keywords(): SyntaxNode {
|
||||||
if (this.is($T.Keyword, 'if')) return this.if()
|
if (this.is($T.Keyword, 'if'))
|
||||||
|
return this.if()
|
||||||
|
|
||||||
if (this.is($T.Keyword, 'while')) return this.while()
|
if (this.is($T.Keyword, 'while'))
|
||||||
|
return this.while()
|
||||||
|
|
||||||
if (this.is($T.Keyword, 'do')) return this.do()
|
if (this.is($T.Keyword, 'do'))
|
||||||
|
return this.do()
|
||||||
|
|
||||||
if (this.is($T.Keyword, 'try')) return this.try()
|
if (this.is($T.Keyword, 'try'))
|
||||||
|
return this.try()
|
||||||
|
|
||||||
if (this.is($T.Keyword, 'throw')) return this.throw()
|
if (this.is($T.Keyword, 'throw'))
|
||||||
|
return this.throw()
|
||||||
|
|
||||||
if (this.is($T.Keyword, 'not')) return this.not()
|
if (this.is($T.Keyword, 'not'))
|
||||||
|
return this.not()
|
||||||
|
|
||||||
if (this.is($T.Keyword, 'import')) return this.import()
|
if (this.is($T.Keyword, 'import'))
|
||||||
|
return this.import()
|
||||||
|
|
||||||
return this.expect($T.Keyword, 'if/while/do/import') as never
|
return this.expect($T.Keyword, 'if/while/do/import') as never
|
||||||
}
|
}
|
||||||
|
|
@ -215,12 +238,15 @@ export class Parser {
|
||||||
// 3. binary operations
|
// 3. binary operations
|
||||||
// 4. anywhere an expression can be used
|
// 4. anywhere an expression can be used
|
||||||
value(): SyntaxNode {
|
value(): SyntaxNode {
|
||||||
if (this.is($T.OpenParen)) return this.parens()
|
if (this.is($T.OpenParen))
|
||||||
|
return this.parens()
|
||||||
|
|
||||||
if (this.is($T.OpenBracket)) return this.arrayOrDict()
|
if (this.is($T.OpenBracket))
|
||||||
|
return this.arrayOrDict()
|
||||||
|
|
||||||
// dotget
|
// dotget
|
||||||
if (this.nextIs($T.Operator, '.')) return this.dotGet()
|
if (this.nextIs($T.Operator, '.'))
|
||||||
|
return this.dotGet()
|
||||||
|
|
||||||
return this.atom()
|
return this.atom()
|
||||||
}
|
}
|
||||||
|
|
@ -298,7 +324,8 @@ export class Parser {
|
||||||
}
|
}
|
||||||
|
|
||||||
// probably an array
|
// probably an array
|
||||||
if (curr.type !== $T.Comment && curr.type !== $T.Semicolon && curr.type !== $T.Newline) break
|
if (curr.type !== $T.Comment && curr.type !== $T.Semicolon && curr.type !== $T.Newline)
|
||||||
|
break
|
||||||
|
|
||||||
curr = this.peek(peek++)
|
curr = this.peek(peek++)
|
||||||
}
|
}
|
||||||
|
|
@ -316,7 +343,7 @@ export class Parser {
|
||||||
const node = new SyntaxNode(
|
const node = new SyntaxNode(
|
||||||
opToken.value === '=' ? 'Assign' : 'CompoundAssign',
|
opToken.value === '=' ? 'Assign' : 'CompoundAssign',
|
||||||
ident.from,
|
ident.from,
|
||||||
expr.to,
|
expr.to
|
||||||
)
|
)
|
||||||
|
|
||||||
return node.push(ident, op, expr)
|
return node.push(ident, op, expr)
|
||||||
|
|
@ -333,7 +360,8 @@ export class Parser {
|
||||||
|
|
||||||
// atoms are the basic building blocks: literals, identifiers, words
|
// atoms are the basic building blocks: literals, identifiers, words
|
||||||
atom(): SyntaxNode {
|
atom(): SyntaxNode {
|
||||||
if (this.is($T.String)) return this.string()
|
if (this.is($T.String))
|
||||||
|
return this.string()
|
||||||
|
|
||||||
if (this.isAny($T.Null, $T.Boolean, $T.Number, $T.Identifier, $T.Word, $T.Regex, $T.Underscore))
|
if (this.isAny($T.Null, $T.Boolean, $T.Number, $T.Identifier, $T.Word, $T.Regex, $T.Underscore))
|
||||||
return SyntaxNode.from(this.next())
|
return SyntaxNode.from(this.next())
|
||||||
|
|
@ -374,7 +402,8 @@ export class Parser {
|
||||||
const keyword = this.keyword('catch')
|
const keyword = this.keyword('catch')
|
||||||
|
|
||||||
let catchVar
|
let catchVar
|
||||||
if (this.is($T.Identifier)) catchVar = this.identifier()
|
if (this.is($T.Identifier))
|
||||||
|
catchVar = this.identifier()
|
||||||
|
|
||||||
const block = this.block()
|
const block = this.block()
|
||||||
|
|
||||||
|
|
@ -478,14 +507,12 @@ export class Parser {
|
||||||
this.scope.add(varName)
|
this.scope.add(varName)
|
||||||
|
|
||||||
let arg
|
let arg
|
||||||
if (this.is($T.Identifier)) arg = this.identifier()
|
if (this.is($T.Identifier))
|
||||||
else if (this.is($T.NamedArgPrefix)) arg = this.namedParam()
|
arg = this.identifier()
|
||||||
|
else if (this.is($T.NamedArgPrefix))
|
||||||
|
arg = this.namedParam()
|
||||||
else
|
else
|
||||||
throw new CompilerError(
|
throw new CompilerError(`Expected Identifier or NamedArgPrefix, got ${TokenType[this.current().type]}`, this.current().from, this.current().to)
|
||||||
`Expected Identifier or NamedArgPrefix, got ${TokenType[this.current().type]}`,
|
|
||||||
this.current().from,
|
|
||||||
this.current().to,
|
|
||||||
)
|
|
||||||
|
|
||||||
params.push(arg)
|
params.push(arg)
|
||||||
}
|
}
|
||||||
|
|
@ -493,9 +520,11 @@ export class Parser {
|
||||||
const block = this.block(false)
|
const block = this.block(false)
|
||||||
let catchNode, finalNode
|
let catchNode, finalNode
|
||||||
|
|
||||||
if (this.is($T.Keyword, 'catch')) catchNode = this.catch()
|
if (this.is($T.Keyword, 'catch'))
|
||||||
|
catchNode = this.catch()
|
||||||
|
|
||||||
if (this.is($T.Keyword, 'finally')) finalNode = this.finally()
|
if (this.is($T.Keyword, 'finally'))
|
||||||
|
finalNode = this.finally()
|
||||||
|
|
||||||
const end = this.keyword('end')
|
const end = this.keyword('end')
|
||||||
|
|
||||||
|
|
@ -507,7 +536,11 @@ export class Parser {
|
||||||
|
|
||||||
node.add(doNode)
|
node.add(doNode)
|
||||||
|
|
||||||
const paramsNode = new SyntaxNode('Params', params[0]?.from ?? 0, params.at(-1)?.to ?? 0)
|
const paramsNode = new SyntaxNode(
|
||||||
|
'Params',
|
||||||
|
params[0]?.from ?? 0,
|
||||||
|
params.at(-1)?.to ?? 0
|
||||||
|
)
|
||||||
|
|
||||||
if (params.length) paramsNode.push(...params)
|
if (params.length) paramsNode.push(...params)
|
||||||
node.add(paramsNode)
|
node.add(paramsNode)
|
||||||
|
|
@ -528,7 +561,8 @@ export class Parser {
|
||||||
const ident = this.input.slice(left.from, left.to)
|
const ident = this.input.slice(left.from, left.to)
|
||||||
|
|
||||||
// not in scope, just return Word
|
// not in scope, just return Word
|
||||||
if (!this.scope.has(ident)) return this.word(left)
|
if (!this.scope.has(ident))
|
||||||
|
return this.word(left)
|
||||||
|
|
||||||
if (left.type.is('Identifier')) left.type = 'IdentifierBeforeDot'
|
if (left.type.is('Identifier')) left.type = 'IdentifierBeforeDot'
|
||||||
|
|
||||||
|
|
@ -568,13 +602,16 @@ export class Parser {
|
||||||
const dotGet = this.dotGet()
|
const dotGet = this.dotGet()
|
||||||
|
|
||||||
// if followed by a binary operator (not pipe), return dotGet/Word as-is for expression parser
|
// if followed by a binary operator (not pipe), return dotGet/Word as-is for expression parser
|
||||||
if (this.is($T.Operator) && !this.is($T.Operator, '|')) return dotGet
|
if (this.is($T.Operator) && !this.is($T.Operator, '|'))
|
||||||
|
return dotGet
|
||||||
|
|
||||||
// dotget not in scope, regular Word
|
// dotget not in scope, regular Word
|
||||||
if (dotGet.type.is('Word')) return dotGet
|
if (dotGet.type.is('Word')) return dotGet
|
||||||
|
|
||||||
if (this.isExprEnd()) return this.functionCallOrIdentifier(dotGet)
|
if (this.isExprEnd())
|
||||||
else return this.functionCall(dotGet)
|
return this.functionCallOrIdentifier(dotGet)
|
||||||
|
else
|
||||||
|
return this.functionCall(dotGet)
|
||||||
}
|
}
|
||||||
|
|
||||||
// can be used in functions or try block
|
// can be used in functions or try block
|
||||||
|
|
@ -726,11 +763,7 @@ export class Parser {
|
||||||
const val = this.value()
|
const val = this.value()
|
||||||
|
|
||||||
if (!['Null', 'Boolean', 'Number', 'String'].includes(val.type.name))
|
if (!['Null', 'Boolean', 'Number', 'String'].includes(val.type.name))
|
||||||
throw new CompilerError(
|
throw new CompilerError(`Default value must be null, boolean, number, or string, got ${val.type.name}`, val.from, val.to)
|
||||||
`Default value must be null, boolean, number, or string, got ${val.type.name}`,
|
|
||||||
val.from,
|
|
||||||
val.to,
|
|
||||||
)
|
|
||||||
|
|
||||||
const node = new SyntaxNode('NamedParam', prefix.from, val.to)
|
const node = new SyntaxNode('NamedParam', prefix.from, val.to)
|
||||||
return node.push(prefix, val)
|
return node.push(prefix, val)
|
||||||
|
|
@ -748,8 +781,7 @@ export class Parser {
|
||||||
op(op?: string): SyntaxNode {
|
op(op?: string): SyntaxNode {
|
||||||
const token = op ? this.expect($T.Operator, op) : this.expect($T.Operator)
|
const token = op ? this.expect($T.Operator, op) : this.expect($T.Operator)
|
||||||
const name = operators[token.value!]
|
const name = operators[token.value!]
|
||||||
if (!name)
|
if (!name) throw new CompilerError(`Operator not registered: ${token.value!}`, token.from, token.to)
|
||||||
throw new CompilerError(`Operator not registered: ${token.value!}`, token.from, token.to)
|
|
||||||
return new SyntaxNode(name, token.from, token.to)
|
return new SyntaxNode(name, token.from, token.to)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -796,9 +828,11 @@ export class Parser {
|
||||||
let last = tryBlock.at(-1)
|
let last = tryBlock.at(-1)
|
||||||
let catchNode, finalNode
|
let catchNode, finalNode
|
||||||
|
|
||||||
if (this.is($T.Keyword, 'catch')) catchNode = this.catch()
|
if (this.is($T.Keyword, 'catch'))
|
||||||
|
catchNode = this.catch()
|
||||||
|
|
||||||
if (this.is($T.Keyword, 'finally')) finalNode = this.finally()
|
if (this.is($T.Keyword, 'finally'))
|
||||||
|
finalNode = this.finally()
|
||||||
|
|
||||||
const end = this.keyword('end')
|
const end = this.keyword('end')
|
||||||
|
|
||||||
|
|
@ -808,9 +842,11 @@ export class Parser {
|
||||||
const node = new SyntaxNode('TryExpr', tryNode.from, last!.to)
|
const node = new SyntaxNode('TryExpr', tryNode.from, last!.to)
|
||||||
node.push(tryNode, ...tryBlock)
|
node.push(tryNode, ...tryBlock)
|
||||||
|
|
||||||
if (catchNode) node.push(catchNode)
|
if (catchNode)
|
||||||
|
node.push(catchNode)
|
||||||
|
|
||||||
if (finalNode) node.push(finalNode)
|
if (finalNode)
|
||||||
|
node.push(finalNode)
|
||||||
|
|
||||||
return node.push(end)
|
return node.push(end)
|
||||||
}
|
}
|
||||||
|
|
@ -832,7 +868,8 @@ export class Parser {
|
||||||
|
|
||||||
while (this.is($T.Operator, '.')) {
|
while (this.is($T.Operator, '.')) {
|
||||||
this.next()
|
this.next()
|
||||||
if (this.isAny($T.Word, $T.Identifier, $T.Number)) parts.push(this.next())
|
if (this.isAny($T.Word, $T.Identifier, $T.Number))
|
||||||
|
parts.push(this.next())
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SyntaxNode('Word', parts[0]!.from, parts.at(-1)!.to)
|
return new SyntaxNode('Word', parts[0]!.from, parts.at(-1)!.to)
|
||||||
|
|
@ -855,7 +892,8 @@ export class Parser {
|
||||||
let offset = 1
|
let offset = 1
|
||||||
let peek = this.peek(offset)
|
let peek = this.peek(offset)
|
||||||
|
|
||||||
while (peek && peek.type === $T.Newline) peek = this.peek(++offset)
|
while (peek && peek.type === $T.Newline)
|
||||||
|
peek = this.peek(++offset)
|
||||||
|
|
||||||
if (!peek || peek.type !== type) return false
|
if (!peek || peek.type !== type) return false
|
||||||
if (value !== undefined && peek.value !== value) return false
|
if (value !== undefined && peek.value !== value) return false
|
||||||
|
|
@ -876,7 +914,7 @@ export class Parser {
|
||||||
}
|
}
|
||||||
|
|
||||||
isAny(...type: TokenType[]): boolean {
|
isAny(...type: TokenType[]): boolean {
|
||||||
return type.some((x) => this.is(x))
|
return type.some(x => this.is(x))
|
||||||
}
|
}
|
||||||
|
|
||||||
nextIs(type: TokenType, value?: string): boolean {
|
nextIs(type: TokenType, value?: string): boolean {
|
||||||
|
|
@ -887,58 +925,43 @@ export class Parser {
|
||||||
}
|
}
|
||||||
|
|
||||||
nextIsAny(...type: TokenType[]): boolean {
|
nextIsAny(...type: TokenType[]): boolean {
|
||||||
return type.some((x) => this.nextIs(x))
|
return type.some(x => this.nextIs(x))
|
||||||
}
|
}
|
||||||
|
|
||||||
isExprEnd(): boolean {
|
isExprEnd(): boolean {
|
||||||
return (
|
return this.isAny($T.Colon, $T.Semicolon, $T.Newline, $T.CloseParen, $T.CloseBracket) ||
|
||||||
this.isAny($T.Colon, $T.Semicolon, $T.Newline, $T.CloseParen, $T.CloseBracket) ||
|
|
||||||
this.is($T.Operator, '|') ||
|
this.is($T.Operator, '|') ||
|
||||||
this.isExprEndKeyword() ||
|
this.isExprEndKeyword() || !this.current()
|
||||||
!this.current()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nextIsExprEnd(): boolean {
|
nextIsExprEnd(): boolean {
|
||||||
// pipes act like expression end for function arg parsing
|
// pipes act like expression end for function arg parsing
|
||||||
if (this.nextIs($T.Operator, '|')) return true
|
if (this.nextIs($T.Operator, '|'))
|
||||||
|
return true
|
||||||
|
|
||||||
return (
|
return this.nextIsAny($T.Colon, $T.Semicolon, $T.Newline, $T.CloseBracket, $T.CloseParen) ||
|
||||||
this.nextIsAny($T.Colon, $T.Semicolon, $T.Newline, $T.CloseBracket, $T.CloseParen) ||
|
this.nextIs($T.Keyword, 'end') || this.nextIs($T.Keyword, 'else') ||
|
||||||
this.nextIs($T.Keyword, 'end') ||
|
this.nextIs($T.Keyword, 'catch') || this.nextIs($T.Keyword, 'finally') ||
|
||||||
this.nextIs($T.Keyword, 'else') ||
|
|
||||||
this.nextIs($T.Keyword, 'catch') ||
|
|
||||||
this.nextIs($T.Keyword, 'finally') ||
|
|
||||||
!this.peek()
|
!this.peek()
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isExprEndKeyword(): boolean {
|
isExprEndKeyword(): boolean {
|
||||||
return (
|
return this.is($T.Keyword, 'end') || this.is($T.Keyword, 'else') ||
|
||||||
this.is($T.Keyword, 'end') ||
|
this.is($T.Keyword, 'catch') || this.is($T.Keyword, 'finally')
|
||||||
this.is($T.Keyword, 'else') ||
|
|
||||||
this.is($T.Keyword, 'catch') ||
|
|
||||||
this.is($T.Keyword, 'finally')
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isPipe(): boolean {
|
isPipe(): boolean {
|
||||||
// inside parens, only look for pipes on same line (don't look past newlines)
|
// inside parens, only look for pipes on same line (don't look past newlines)
|
||||||
const canLookPastNewlines = this.inParens === 0
|
const canLookPastNewlines = this.inParens === 0
|
||||||
|
|
||||||
return (
|
return this.is($T.Operator, '|') ||
|
||||||
this.is($T.Operator, '|') || (canLookPastNewlines && this.peekPastNewlines($T.Operator, '|'))
|
(canLookPastNewlines && this.peekPastNewlines($T.Operator, '|'))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(type: TokenType, value?: string): Token | never {
|
expect(type: TokenType, value?: string): Token | never {
|
||||||
if (!this.is(type, value)) {
|
if (!this.is(type, value)) {
|
||||||
const token = this.current()
|
const token = this.current()
|
||||||
throw new CompilerError(
|
throw new CompilerError(`Expected ${TokenType[type]}${value ? ` "${value}"` : ''}, got ${TokenType[token?.type || 0]}${token?.value ? ` "${token.value}"` : ''} at position ${this.pos}`, token.from, token.to)
|
||||||
`Expected ${TokenType[type]}${value ? ` "${value}"` : ''}, got ${TokenType[token?.type || 0]}${token?.value ? ` "${token.value}"` : ''} at position ${this.pos}`,
|
|
||||||
token.from,
|
|
||||||
token.to,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return this.next()
|
return this.next()
|
||||||
}
|
}
|
||||||
|
|
@ -958,7 +981,7 @@ function collapseDotGets(origNodes: SyntaxNode[]): SyntaxNode {
|
||||||
|
|
||||||
if (left.type.is('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
|
||||||
|
|
|
||||||
|
|
@ -39,18 +39,11 @@ export const parseString = (input: string, from: number, to: number, parser: any
|
||||||
* Parse single-quoted string: 'hello $name\n'
|
* Parse single-quoted string: 'hello $name\n'
|
||||||
* Supports: interpolation ($var, $(expr)), escape sequences (\n, \$, etc)
|
* Supports: interpolation ($var, $(expr)), escape sequences (\n, \$, etc)
|
||||||
*/
|
*/
|
||||||
const parseSingleQuoteString = (
|
const parseSingleQuoteString = (stringNode: SyntaxNode, input: string, from: number, to: number, parser: any) => {
|
||||||
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) {
|
while (pos < to - 1) { // -1 to skip closing '
|
||||||
// -1 to skip closing '
|
|
||||||
const char = input[pos]
|
const char = input[pos]
|
||||||
|
|
||||||
// Escape sequence
|
// Escape sequence
|
||||||
|
|
@ -122,13 +115,7 @@ const parseSingleQuoteString = (
|
||||||
* Supports: interpolation ($var, $(expr)), nested braces
|
* Supports: interpolation ($var, $(expr)), nested braces
|
||||||
* Does NOT support: escape sequences (raw content)
|
* Does NOT support: escape sequences (raw content)
|
||||||
*/
|
*/
|
||||||
const parseCurlyString = (
|
const parseCurlyString = (stringNode: SyntaxNode, input: string, from: number, to: number, parser: any) => {
|
||||||
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
|
||||||
|
|
@ -201,11 +188,7 @@ const parseCurlyString = (
|
||||||
* Returns the parsed expression node and the position after the closing )
|
* Returns the parsed expression node and the position after the closing )
|
||||||
* pos is position of the opening ( in the full input string
|
* pos is position of the opening ( in the full input string
|
||||||
*/
|
*/
|
||||||
const parseInterpolationExpr = (
|
const parseInterpolationExpr = (input: string, pos: number, parser: any): { node: SyntaxNode, endPos: number } => {
|
||||||
input: string,
|
|
||||||
pos: number,
|
|
||||||
parser: any,
|
|
||||||
): { node: SyntaxNode; endPos: number } => {
|
|
||||||
// Find matching closing paren
|
// Find matching closing paren
|
||||||
let depth = 1
|
let depth = 1
|
||||||
let start = pos
|
let start = pos
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,7 @@ describe('if/else if/else', () => {
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
test('parses function calls in else-if tests', () => {
|
test('parses function calls in else-if tests', () => {
|
||||||
expect(`if false: true else if var? 'abc': true end`).toMatchTree(`
|
expect(`if false: true else if var? 'abc': true end`).toMatchTree(`
|
||||||
IfExpr
|
IfExpr
|
||||||
|
|
@ -285,6 +286,7 @@ describe('while', () => {
|
||||||
keyword end`)
|
keyword end`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
test('compound expression', () => {
|
test('compound expression', () => {
|
||||||
expect(`while a > 0 and b < 100 and c < 1000: true end`).toMatchTree(`
|
expect(`while a > 0 and b < 100 and c < 1000: true end`).toMatchTree(`
|
||||||
WhileExpr
|
WhileExpr
|
||||||
|
|
@ -342,6 +344,7 @@ describe('while', () => {
|
||||||
keyword end`)
|
keyword end`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
test('multiline compound expression', () => {
|
test('multiline compound expression', () => {
|
||||||
expect(`
|
expect(`
|
||||||
while a > 0 and b < 100 and c < 1000:
|
while a > 0 and b < 100 and c < 1000:
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ describe('single line function blocks', () => {
|
||||||
Identifier bye
|
Identifier bye
|
||||||
PositionalArg
|
PositionalArg
|
||||||
Identifier bye
|
Identifier bye
|
||||||
keyword end`)
|
keyword end`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('work with one arg', () => {
|
test('work with one arg', () => {
|
||||||
|
|
@ -32,7 +33,8 @@ describe('single line function blocks', () => {
|
||||||
Identifier bye
|
Identifier bye
|
||||||
PositionalArg
|
PositionalArg
|
||||||
Identifier bye
|
Identifier bye
|
||||||
keyword end`)
|
keyword end`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('work with named args', () => {
|
test('work with named args', () => {
|
||||||
|
|
@ -52,9 +54,11 @@ describe('single line function blocks', () => {
|
||||||
Identifier bye
|
Identifier bye
|
||||||
PositionalArg
|
PositionalArg
|
||||||
Identifier bye
|
Identifier bye
|
||||||
keyword end`)
|
keyword end`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
test('work with dot-get', () => {
|
test('work with dot-get', () => {
|
||||||
expect(`signals = [=]; signals.trap 'EXIT': echo bye bye end`).toMatchTree(`
|
expect(`signals = [=]; signals.trap 'EXIT': echo bye bye end`).toMatchTree(`
|
||||||
Assign
|
Assign
|
||||||
|
|
@ -77,7 +81,8 @@ describe('single line function blocks', () => {
|
||||||
Identifier bye
|
Identifier bye
|
||||||
PositionalArg
|
PositionalArg
|
||||||
Identifier bye
|
Identifier bye
|
||||||
keyword end`)
|
keyword end`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -99,7 +104,8 @@ end
|
||||||
Identifier bye
|
Identifier bye
|
||||||
PositionalArg
|
PositionalArg
|
||||||
Identifier bye
|
Identifier bye
|
||||||
keyword end`)
|
keyword end`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('work with one arg', () => {
|
test('work with one arg', () => {
|
||||||
|
|
@ -120,7 +126,8 @@ end`).toMatchTree(`
|
||||||
Identifier bye
|
Identifier bye
|
||||||
PositionalArg
|
PositionalArg
|
||||||
Identifier bye
|
Identifier bye
|
||||||
keyword end`)
|
keyword end`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('work with named args', () => {
|
test('work with named args', () => {
|
||||||
|
|
@ -146,9 +153,11 @@ end`).toMatchTree(`
|
||||||
Identifier bye
|
Identifier bye
|
||||||
PositionalArg
|
PositionalArg
|
||||||
Identifier bye
|
Identifier bye
|
||||||
keyword end`)
|
keyword end`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
test('work with dot-get', () => {
|
test('work with dot-get', () => {
|
||||||
expect(`
|
expect(`
|
||||||
signals = [=]
|
signals = [=]
|
||||||
|
|
@ -175,7 +184,8 @@ end`).toMatchTree(`
|
||||||
Identifier bye
|
Identifier bye
|
||||||
PositionalArg
|
PositionalArg
|
||||||
Identifier bye
|
Identifier bye
|
||||||
keyword end`)
|
keyword end`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -252,7 +262,8 @@ end`).toMatchTree(`
|
||||||
p:
|
p:
|
||||||
h1 class=bright style='font-family: helvetica' Heya
|
h1 class=bright style='font-family: helvetica' Heya
|
||||||
h2 man that is (b wild)!
|
h2 man that is (b wild)!
|
||||||
end`).toMatchTree(`
|
end`)
|
||||||
|
.toMatchTree(`
|
||||||
FunctionCallWithBlock
|
FunctionCallWithBlock
|
||||||
FunctionCallOrIdentifier
|
FunctionCallOrIdentifier
|
||||||
Identifier p
|
Identifier p
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ describe('calling functions', () => {
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ describe('curly strings', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('double quoted strings', () => {
|
describe('double quoted strings', () => {
|
||||||
test('work', () => {
|
test("work", () => {
|
||||||
expect(`"hello world"`).toMatchTree(`
|
expect(`"hello world"`).toMatchTree(`
|
||||||
String
|
String
|
||||||
DoubleQuote "hello world"`)
|
DoubleQuote "hello world"`)
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,10 @@ describe('numbers', () => {
|
||||||
test('non-numbers', () => {
|
test('non-numbers', () => {
|
||||||
expect(`1st`).toMatchToken('Word', '1st')
|
expect(`1st`).toMatchToken('Word', '1st')
|
||||||
expect(`1_`).toMatchToken('Word', '1_')
|
expect(`1_`).toMatchToken('Word', '1_')
|
||||||
expect(`100.`).toMatchTokens({ type: 'Number', value: '100' }, { type: 'Operator', value: '.' })
|
expect(`100.`).toMatchTokens(
|
||||||
|
{ type: 'Number', value: '100' },
|
||||||
|
{ type: 'Operator', value: '.' },
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('simple numbers', () => {
|
test('simple numbers', () => {
|
||||||
|
|
@ -127,7 +130,10 @@ describe('identifiers', () => {
|
||||||
expect('dog#pound').toMatchToken('Word', 'dog#pound')
|
expect('dog#pound').toMatchToken('Word', 'dog#pound')
|
||||||
expect('http://website.com').toMatchToken('Word', 'http://website.com')
|
expect('http://website.com').toMatchToken('Word', 'http://website.com')
|
||||||
expect('school$cool').toMatchToken('Identifier', 'school$cool')
|
expect('school$cool').toMatchToken('Identifier', 'school$cool')
|
||||||
expect('EXIT:').toMatchTokens({ type: 'Word', value: 'EXIT' }, { type: 'Colon' })
|
expect('EXIT:').toMatchTokens(
|
||||||
|
{ type: 'Word', value: 'EXIT' },
|
||||||
|
{ type: 'Colon' },
|
||||||
|
)
|
||||||
expect(`if y == 1: 'cool' end`).toMatchTokens(
|
expect(`if y == 1: 'cool' end`).toMatchTokens(
|
||||||
{ type: 'Keyword', value: 'if' },
|
{ type: 'Keyword', value: 'if' },
|
||||||
{ type: 'Identifier', value: 'y' },
|
{ type: 'Identifier', value: 'y' },
|
||||||
|
|
@ -208,24 +214,18 @@ describe('curly strings', () => {
|
||||||
expect(`{
|
expect(`{
|
||||||
one
|
one
|
||||||
two
|
two
|
||||||
three }`).toMatchToken(
|
three }`).toMatchToken('String', `{
|
||||||
'String',
|
|
||||||
`{
|
|
||||||
one
|
one
|
||||||
two
|
two
|
||||||
three }`,
|
three }`)
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('can contain other curlies', () => {
|
test('can contain other curlies', () => {
|
||||||
expect(`{ { one }
|
expect(`{ { one }
|
||||||
two
|
two
|
||||||
{ three } }`).toMatchToken(
|
{ three } }`).toMatchToken('String', `{ { one }
|
||||||
'String',
|
|
||||||
`{ { one }
|
|
||||||
two
|
two
|
||||||
{ three } }`,
|
{ three } }`)
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('empty curly string', () => {
|
test('empty curly string', () => {
|
||||||
|
|
@ -408,12 +408,12 @@ f
|
||||||
|
|
||||||
]`).toMatchTokens(
|
]`).toMatchTokens(
|
||||||
{ type: 'OpenBracket' },
|
{ type: 'OpenBracket' },
|
||||||
{ type: 'Identifier', value: 'a' },
|
{ type: 'Identifier', value: "a" },
|
||||||
{ type: 'Identifier', value: 'b' },
|
{ type: 'Identifier', value: "b" },
|
||||||
{ type: 'Identifier', value: 'c' },
|
{ type: 'Identifier', value: "c" },
|
||||||
{ type: 'Identifier', value: 'd' },
|
{ type: 'Identifier', value: "d" },
|
||||||
{ type: 'Identifier', value: 'e' },
|
{ type: 'Identifier', value: "e" },
|
||||||
{ type: 'Identifier', value: 'f' },
|
{ type: 'Identifier', value: "f" },
|
||||||
{ type: 'CloseBracket' },
|
{ type: 'CloseBracket' },
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
@ -506,6 +506,7 @@ f
|
||||||
{ type: 'Identifier', value: 'y' },
|
{ type: 'Identifier', value: 'y' },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
expect(`if (var? 'abc'): y`).toMatchTokens(
|
expect(`if (var? 'abc'): y`).toMatchTokens(
|
||||||
{ type: 'Keyword', value: 'if' },
|
{ type: 'Keyword', value: 'if' },
|
||||||
{ type: 'OpenParen' },
|
{ type: 'OpenParen' },
|
||||||
|
|
@ -551,25 +552,25 @@ end`).toMatchTokens(
|
||||||
|
|
||||||
test('dot operator beginning word with slash', () => {
|
test('dot operator beginning word with slash', () => {
|
||||||
expect(`(basename ./cool)`).toMatchTokens(
|
expect(`(basename ./cool)`).toMatchTokens(
|
||||||
{ type: 'OpenParen' },
|
{ 'type': 'OpenParen' },
|
||||||
{ type: 'Identifier', value: 'basename' },
|
{ 'type': 'Identifier', 'value': 'basename' },
|
||||||
{ type: 'Word', value: './cool' },
|
{ 'type': 'Word', 'value': './cool' },
|
||||||
{ type: 'CloseParen' },
|
{ 'type': 'CloseParen' }
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('dot word after identifier with space', () => {
|
test('dot word after identifier with space', () => {
|
||||||
expect(`expand-path .git`).toMatchTokens(
|
expect(`expand-path .git`).toMatchTokens(
|
||||||
{ type: 'Identifier', value: 'expand-path' },
|
{ 'type': 'Identifier', 'value': 'expand-path' },
|
||||||
{ type: 'Word', value: '.git' },
|
{ 'type': 'Word', 'value': '.git' },
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('dot operator after identifier without space', () => {
|
test('dot operator after identifier without space', () => {
|
||||||
expect(`config.path`).toMatchTokens(
|
expect(`config.path`).toMatchTokens(
|
||||||
{ type: 'Identifier', value: 'config' },
|
{ 'type': 'Identifier', 'value': 'config' },
|
||||||
{ type: 'Operator', value: '.' },
|
{ 'type': 'Operator', 'value': '.' },
|
||||||
{ type: 'Identifier', value: 'path' },
|
{ 'type': 'Identifier', 'value': 'path' },
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -648,7 +649,11 @@ describe('empty and whitespace input', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('only newlines', () => {
|
test('only newlines', () => {
|
||||||
expect('\n\n\n').toMatchTokens({ type: 'Newline' }, { type: 'Newline' }, { type: 'Newline' })
|
expect('\n\n\n').toMatchTokens(
|
||||||
|
{ type: 'Newline' },
|
||||||
|
{ type: 'Newline' },
|
||||||
|
{ type: 'Newline' },
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -660,14 +665,14 @@ describe('named args', () => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('can have spaces', () => {
|
test("can have spaces", () => {
|
||||||
expect(`named= arg`).toMatchTokens(
|
expect(`named= arg`).toMatchTokens(
|
||||||
{ type: 'NamedArgPrefix', value: 'named=' },
|
{ type: 'NamedArgPrefix', value: 'named=' },
|
||||||
{ type: 'Identifier', value: 'arg' },
|
{ type: 'Identifier', value: 'arg' },
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('can include numbers', () => {
|
test("can include numbers", () => {
|
||||||
expect(`named123= arg`).toMatchTokens(
|
expect(`named123= arg`).toMatchTokens(
|
||||||
{ type: 'NamedArgPrefix', value: 'named123=' },
|
{ type: 'NamedArgPrefix', value: 'named123=' },
|
||||||
{ type: 'Identifier', value: 'arg' },
|
{ type: 'Identifier', value: 'arg' },
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ const DEBUG = process.env.DEBUG || false
|
||||||
|
|
||||||
export type Token = {
|
export type Token = {
|
||||||
type: TokenType
|
type: TokenType
|
||||||
value?: string
|
value?: string,
|
||||||
from: number
|
from: number,
|
||||||
to: number
|
to: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TokenType {
|
export enum TokenType {
|
||||||
|
|
@ -36,16 +36,10 @@ export enum TokenType {
|
||||||
|
|
||||||
const valueTokens = new Set([
|
const valueTokens = new Set([
|
||||||
TokenType.Comment,
|
TokenType.Comment,
|
||||||
TokenType.Keyword,
|
TokenType.Keyword, TokenType.Operator,
|
||||||
TokenType.Operator,
|
TokenType.Identifier, TokenType.Word, TokenType.NamedArgPrefix,
|
||||||
TokenType.Identifier,
|
TokenType.Boolean, TokenType.Number, TokenType.String, TokenType.Regex,
|
||||||
TokenType.Word,
|
TokenType.Underscore
|
||||||
TokenType.NamedArgPrefix,
|
|
||||||
TokenType.Boolean,
|
|
||||||
TokenType.Number,
|
|
||||||
TokenType.String,
|
|
||||||
TokenType.Regex,
|
|
||||||
TokenType.Underscore,
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const operators = new Set([
|
const operators = new Set([
|
||||||
|
|
@ -115,7 +109,7 @@ const keywords = new Set([
|
||||||
|
|
||||||
// helper
|
// helper
|
||||||
function c(strings: TemplateStringsArray, ...values: any[]) {
|
function c(strings: TemplateStringsArray, ...values: any[]) {
|
||||||
return strings.reduce((result, str, i) => result + str + (values[i] ?? ''), '').charCodeAt(0)
|
return strings.reduce((result, str, i) => result + str + (values[i] ?? ""), "").charCodeAt(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function s(c: number): string {
|
function s(c: number): string {
|
||||||
|
|
@ -161,17 +155,11 @@ export class Scanner {
|
||||||
to ??= this.pos - getCharSize(this.char)
|
to ??= this.pos - getCharSize(this.char)
|
||||||
if (to < from) to = from
|
if (to < from) to = from
|
||||||
|
|
||||||
this.tokens.push(
|
this.tokens.push(Object.assign({}, {
|
||||||
Object.assign(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
type,
|
type,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
},
|
}, valueTokens.has(type) ? { value: this.input.slice(from, to) } : {}))
|
||||||
valueTokens.has(type) ? { value: this.input.slice(from, to) } : {},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
const tok = this.tokens.at(-1)
|
const tok = this.tokens.at(-1)
|
||||||
|
|
@ -250,7 +238,8 @@ export class Scanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (char === c`\n`) {
|
if (char === c`\n`) {
|
||||||
if (this.inParen === 0 && this.inBracket === 0) this.pushChar(TokenType.Newline)
|
if (this.inParen === 0 && this.inBracket === 0)
|
||||||
|
this.pushChar(TokenType.Newline)
|
||||||
this.next()
|
this.next()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -277,20 +266,16 @@ export class Scanner {
|
||||||
switch (this.char) {
|
switch (this.char) {
|
||||||
case c`(`:
|
case c`(`:
|
||||||
this.inParen++
|
this.inParen++
|
||||||
this.pushChar(TokenType.OpenParen)
|
this.pushChar(TokenType.OpenParen); break
|
||||||
break
|
|
||||||
case c`)`:
|
case c`)`:
|
||||||
this.inParen--
|
this.inParen--
|
||||||
this.pushChar(TokenType.CloseParen)
|
this.pushChar(TokenType.CloseParen); break
|
||||||
break
|
|
||||||
case c`[`:
|
case c`[`:
|
||||||
this.inBracket++
|
this.inBracket++
|
||||||
this.pushChar(TokenType.OpenBracket)
|
this.pushChar(TokenType.OpenBracket); break
|
||||||
break
|
|
||||||
case c`]`:
|
case c`]`:
|
||||||
this.inBracket--
|
this.inBracket--
|
||||||
this.pushChar(TokenType.CloseBracket)
|
this.pushChar(TokenType.CloseBracket); break
|
||||||
break
|
|
||||||
}
|
}
|
||||||
this.next()
|
this.next()
|
||||||
}
|
}
|
||||||
|
|
@ -354,14 +339,29 @@ export class Scanner {
|
||||||
const word = this.input.slice(this.start, this.pos - getCharSize(this.char))
|
const word = this.input.slice(this.start, this.pos - getCharSize(this.char))
|
||||||
|
|
||||||
// classify the token based on what we read
|
// classify the token based on what we read
|
||||||
if (word === '_') this.push(TokenType.Underscore)
|
if (word === '_')
|
||||||
else if (word === 'null') this.push(TokenType.Null)
|
this.push(TokenType.Underscore)
|
||||||
else if (word === 'true' || word === 'false') this.push(TokenType.Boolean)
|
|
||||||
else if (isKeyword(word)) this.push(TokenType.Keyword)
|
else if (word === 'null')
|
||||||
else if (isOperator(word)) this.push(TokenType.Operator)
|
this.push(TokenType.Null)
|
||||||
else if (isIdentifer(word)) this.push(TokenType.Identifier)
|
|
||||||
else if (word.endsWith('=')) this.push(TokenType.NamedArgPrefix)
|
else if (word === 'true' || word === 'false')
|
||||||
else this.push(TokenType.Word)
|
this.push(TokenType.Boolean)
|
||||||
|
|
||||||
|
else if (isKeyword(word))
|
||||||
|
this.push(TokenType.Keyword)
|
||||||
|
|
||||||
|
else if (isOperator(word))
|
||||||
|
this.push(TokenType.Operator)
|
||||||
|
|
||||||
|
else if (isIdentifer(word))
|
||||||
|
this.push(TokenType.Identifier)
|
||||||
|
|
||||||
|
else if (word.endsWith('='))
|
||||||
|
this.push(TokenType.NamedArgPrefix)
|
||||||
|
|
||||||
|
else
|
||||||
|
this.push(TokenType.Word)
|
||||||
}
|
}
|
||||||
|
|
||||||
readNumber() {
|
readNumber() {
|
||||||
|
|
@ -394,7 +394,8 @@ export class Scanner {
|
||||||
this.next() // skip /
|
this.next() // skip /
|
||||||
|
|
||||||
// read regex flags
|
// read regex flags
|
||||||
while (this.char > 0 && isIdentStart(this.char)) this.next()
|
while (this.char > 0 && isIdentStart(this.char))
|
||||||
|
this.next()
|
||||||
|
|
||||||
// validate regex
|
// validate regex
|
||||||
const to = this.pos - getCharSize(this.char)
|
const to = this.pos - getCharSize(this.char)
|
||||||
|
|
@ -421,29 +422,30 @@ export class Scanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
canBeDotGet(lastToken?: Token): boolean {
|
canBeDotGet(lastToken?: Token): boolean {
|
||||||
return (
|
return !this.prevIsWhitespace && !!lastToken &&
|
||||||
!this.prevIsWhitespace &&
|
|
||||||
!!lastToken &&
|
|
||||||
(lastToken.type === TokenType.Identifier ||
|
(lastToken.type === TokenType.Identifier ||
|
||||||
lastToken.type === TokenType.Number ||
|
lastToken.type === TokenType.Number ||
|
||||||
lastToken.type === TokenType.CloseParen ||
|
lastToken.type === TokenType.CloseParen ||
|
||||||
lastToken.type === TokenType.CloseBracket)
|
lastToken.type === TokenType.CloseBracket)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isNumber = (word: string): boolean => {
|
const isNumber = (word: string): boolean => {
|
||||||
// regular number
|
// regular number
|
||||||
if (/^[+-]?\d+(_?\d+)*(\.(\d+(_?\d+)*))?$/.test(word)) return true
|
if (/^[+-]?\d+(_?\d+)*(\.(\d+(_?\d+)*))?$/.test(word))
|
||||||
|
return true
|
||||||
|
|
||||||
// binary
|
// binary
|
||||||
if (/^[+-]?0b[01]+(_?[01]+)*(\.[01](_?[01]*))?$/.test(word)) return true
|
if (/^[+-]?0b[01]+(_?[01]+)*(\.[01](_?[01]*))?$/.test(word))
|
||||||
|
return true
|
||||||
|
|
||||||
// octal
|
// octal
|
||||||
if (/^[+-]?0o[0-7]+(_?[0-7]+)*(\.[0-7](_?[0-7]*))?$/.test(word)) return true
|
if (/^[+-]?0o[0-7]+(_?[0-7]+)*(\.[0-7](_?[0-7]*))?$/.test(word))
|
||||||
|
return true
|
||||||
|
|
||||||
// hex
|
// hex
|
||||||
if (/^[+-]?0x[0-9a-f]+([0-9a-f]_?[0-9a-f]+)*(\.([0-9a-f]_?[0-9a-f]*))?$/i.test(word)) return true
|
if (/^[+-]?0x[0-9a-f]+([0-9a-f]_?[0-9a-f]+)*(\.([0-9a-f]_?[0-9a-f]*))?$/i.test(word))
|
||||||
|
return true
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -459,14 +461,14 @@ const isIdentifer = (s: string): boolean => {
|
||||||
chars.push(out)
|
chars.push(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chars.length === 1) return isIdentStart(chars[0]!)
|
if (chars.length === 1)
|
||||||
else if (chars.length === 2) return isIdentStart(chars[0]!) && isIdentEnd(chars[1]!)
|
return isIdentStart(chars[0]!)
|
||||||
|
else if (chars.length === 2)
|
||||||
|
return isIdentStart(chars[0]!) && isIdentEnd(chars[1]!)
|
||||||
else
|
else
|
||||||
return (
|
return isIdentStart(chars[0]!) &&
|
||||||
isIdentStart(chars[0]!) &&
|
|
||||||
chars.slice(1, chars.length - 1).every(isIdentChar) &&
|
chars.slice(1, chars.length - 1).every(isIdentChar) &&
|
||||||
isIdentEnd(chars.at(-1)!)
|
isIdentEnd(chars.at(-1)!)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isStringDelim = (ch: number): boolean => {
|
const isStringDelim = (ch: number): boolean => {
|
||||||
|
|
@ -496,14 +498,9 @@ const isDigit = (ch: number): boolean => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const isWhitespace = (ch: number): boolean => {
|
const isWhitespace = (ch: number): boolean => {
|
||||||
return (
|
return ch === 32 /* space */ || ch === 9 /* tab */ ||
|
||||||
ch === 32 /* space */ ||
|
ch === 13 /* \r */ || ch === 10 /* \n */ ||
|
||||||
ch === 9 /* tab */ ||
|
ch === -1 || ch === 0 /* EOF */
|
||||||
ch === 13 /* \r */ ||
|
|
||||||
ch === 10 /* \n */ ||
|
|
||||||
ch === -1 ||
|
|
||||||
ch === 0
|
|
||||||
) /* EOF */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isWordChar = (ch: number): boolean => {
|
const isWordChar = (ch: number): boolean => {
|
||||||
|
|
@ -530,7 +527,8 @@ const isBracket = (char: number): boolean => {
|
||||||
return char === c`(` || char === c`)` || char === c`[` || char === c`]`
|
return char === c`(` || char === c`)` || char === c`[` || char === c`]`
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCharSize = (ch: number) => (ch > 0xffff ? 2 : 1) // emoji takes 2 UTF-16 code units
|
const getCharSize = (ch: number) =>
|
||||||
|
(ch > 0xffff ? 2 : 1) // emoji takes 2 UTF-16 code units
|
||||||
|
|
||||||
const getFullCodePoint = (input: string, pos: number): number => {
|
const getFullCodePoint = (input: string, pos: number): number => {
|
||||||
const ch = input[pos]?.charCodeAt(0) || 0
|
const ch = input[pos]?.charCodeAt(0) || 0
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
export const date = {
|
export const date = {
|
||||||
now: () => Date.now(),
|
now: () => Date.now(),
|
||||||
year: (time: number) => new Date(time).getFullYear(),
|
year: (time: number) => (new Date(time)).getFullYear(),
|
||||||
month: (time: number) => new Date(time).getMonth(),
|
month: (time: number) => (new Date(time)).getMonth(),
|
||||||
date: (time: number) => new Date(time).getDate(),
|
date: (time: number) => (new Date(time)).getDate(),
|
||||||
hour: (time: number) => new Date(time).getHours(),
|
hour: (time: number) => (new Date(time)).getHours(),
|
||||||
minute: (time: number) => new Date(time).getMinutes(),
|
minute: (time: number) => (new Date(time)).getMinutes(),
|
||||||
second: (time: number) => new Date(time).getSeconds(),
|
second: (time: number) => (new Date(time)).getSeconds(),
|
||||||
ms: (time: number) => new Date(time).getMilliseconds(),
|
ms: (time: number) => (new Date(time)).getMilliseconds(),
|
||||||
new: (year: number, month: number, day: number, hour = 0, minute = 0, second = 0, ms = 0) =>
|
new: (year: number, month: number, day: number, hour = 0, minute = 0, second = 0, ms = 0) =>
|
||||||
new Date(year, month, day, hour, minute, second, ms).getTime(),
|
new Date(year, month, day, hour, minute, second, ms).getTime()
|
||||||
}
|
}
|
||||||
|
|
@ -3,11 +3,9 @@ import { type Value, toString } from 'reefvm'
|
||||||
export const dict = {
|
export const dict = {
|
||||||
keys: (dict: Record<string, any>) => Object.keys(dict),
|
keys: (dict: Record<string, any>) => Object.keys(dict),
|
||||||
values: (dict: Record<string, any>) => Object.values(dict),
|
values: (dict: Record<string, any>) => Object.values(dict),
|
||||||
entries: (dict: Record<string, any>) =>
|
entries: (dict: Record<string, any>) => Object.entries(dict).map(([k, v]) => ({ key: k, value: v })),
|
||||||
Object.entries(dict).map(([k, v]) => ({ key: k, value: v })),
|
|
||||||
'has?': (dict: Record<string, any>, key: string) => key in dict,
|
'has?': (dict: Record<string, any>, key: string) => key in dict,
|
||||||
get: (dict: Record<string, any>, key: string, defaultValue: any = null) =>
|
get: (dict: Record<string, any>, key: string, defaultValue: any = null) => dict[key] ?? defaultValue,
|
||||||
dict[key] ?? defaultValue,
|
|
||||||
set: (dict: Value, key: Value, value: Value) => {
|
set: (dict: Value, key: Value, value: Value) => {
|
||||||
const map = dict.value as Map<string, Value>
|
const map = dict.value as Map<string, Value>
|
||||||
map.set(toString(key), value)
|
map.set(toString(key), value)
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,17 @@
|
||||||
import { join, resolve, basename, dirname, extname } from 'path'
|
import { join, resolve, basename, dirname, extname } from 'path'
|
||||||
import {
|
import {
|
||||||
readdirSync,
|
readdirSync, mkdirSync, rmdirSync,
|
||||||
mkdirSync,
|
readFileSync, writeFileSync, appendFileSync,
|
||||||
rmdirSync,
|
rmSync, copyFileSync,
|
||||||
readFileSync,
|
statSync, lstatSync, chmodSync, symlinkSync, readlinkSync,
|
||||||
writeFileSync,
|
watch
|
||||||
appendFileSync,
|
} from "fs"
|
||||||
rmSync,
|
|
||||||
copyFileSync,
|
|
||||||
statSync,
|
|
||||||
lstatSync,
|
|
||||||
chmodSync,
|
|
||||||
symlinkSync,
|
|
||||||
readlinkSync,
|
|
||||||
watch,
|
|
||||||
} from 'fs'
|
|
||||||
|
|
||||||
export const fs = {
|
export const fs = {
|
||||||
// Directory operations
|
// Directory operations
|
||||||
ls: (path: string) => readdirSync(path),
|
ls: (path: string) => readdirSync(path),
|
||||||
mkdir: (path: string) => mkdirSync(path, { recursive: true }),
|
mkdir: (path: string) => mkdirSync(path, { recursive: true }),
|
||||||
rmdir: (path: string) =>
|
rmdir: (path: string) => rmdirSync(path === '/' || path === '' ? '/tmp/*' : path, { recursive: true }),
|
||||||
rmdirSync(path === '/' || path === '' ? '/tmp/*' : path, { recursive: true }),
|
|
||||||
pwd: () => process.cwd(),
|
pwd: () => process.cwd(),
|
||||||
cd: (path: string) => process.chdir(path),
|
cd: (path: string) => process.chdir(path),
|
||||||
|
|
||||||
|
|
@ -68,50 +58,39 @@ export const fs = {
|
||||||
} catch {
|
} catch {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
'exists?': (path: string) => {
|
'exists?': (path: string) => {
|
||||||
try {
|
try {
|
||||||
statSync(path)
|
statSync(path)
|
||||||
return true
|
return true
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'file?': (path: string) => {
|
'file?': (path: string) => {
|
||||||
try {
|
try { return statSync(path).isFile() }
|
||||||
return statSync(path).isFile()
|
catch { return false }
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
'dir?': (path: string) => {
|
'dir?': (path: string) => {
|
||||||
try {
|
try { return statSync(path).isDirectory() }
|
||||||
return statSync(path).isDirectory()
|
catch { return false }
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
'symlink?': (path: string) => {
|
'symlink?': (path: string) => {
|
||||||
try {
|
try { return lstatSync(path).isSymbolicLink() }
|
||||||
return lstatSync(path).isSymbolicLink()
|
catch { return false }
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
'exec?': (path: string) => {
|
'exec?': (path: string) => {
|
||||||
try {
|
try {
|
||||||
const stats = statSync(path)
|
const stats = statSync(path)
|
||||||
return !!(stats.mode & 0o111)
|
return !!(stats.mode & 0o111)
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
catch { return false }
|
||||||
},
|
},
|
||||||
size: (path: string) => {
|
size: (path: string) => {
|
||||||
try {
|
try { return statSync(path).size }
|
||||||
return statSync(path).size
|
catch { return 0 }
|
||||||
} catch {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Permissions
|
// Permissions
|
||||||
|
|
@ -135,11 +114,14 @@ export const fs = {
|
||||||
return readdirSync(dir)
|
return readdirSync(dir)
|
||||||
.filter((f) => f.endsWith(ext))
|
.filter((f) => f.endsWith(ext))
|
||||||
.map((f) => join(dir, f))
|
.map((f) => join(dir, f))
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: (path: string, callback: Function) =>
|
watch: (path: string, callback: Function) =>
|
||||||
watch(path, (event, filename) => callback(event, filename)),
|
watch(path, (event, filename) => callback(event, filename)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
; (fs as any).cat = fs.read
|
; (fs as any).cat = fs.read
|
||||||
; (fs as any).mv = fs.move
|
; (fs as any).mv = fs.move
|
||||||
; (fs as any).cp = fs.copy
|
; (fs as any).cp = fs.copy
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,8 @@
|
||||||
|
|
||||||
import { join, resolve } from 'path'
|
import { join, resolve } from 'path'
|
||||||
import {
|
import {
|
||||||
type Value,
|
type Value, type VM, toValue,
|
||||||
type VM,
|
extractParamInfo, isWrapped, getOriginalFunction,
|
||||||
toValue,
|
|
||||||
extractParamInfo,
|
|
||||||
isWrapped,
|
|
||||||
getOriginalFunction,
|
|
||||||
} from 'reefvm'
|
} from 'reefvm'
|
||||||
|
|
||||||
import { date } from './date'
|
import { date } from './date'
|
||||||
|
|
@ -39,18 +35,16 @@ export const globals: Record<string, any> = {
|
||||||
cwd: process.env.PWD,
|
cwd: process.env.PWD,
|
||||||
script: {
|
script: {
|
||||||
name: Bun.argv[2] || '(shrimp)',
|
name: Bun.argv[2] || '(shrimp)',
|
||||||
path: resolve(join('.', Bun.argv[2] ?? '')),
|
path: resolve(join('.', Bun.argv[2] ?? ''))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// hello
|
// hello
|
||||||
echo: (...args: any[]) => {
|
echo: (...args: any[]) => {
|
||||||
console.log(
|
console.log(...args.map(a => {
|
||||||
...args.map((a) => {
|
|
||||||
const v = toValue(a)
|
const v = toValue(a)
|
||||||
return ['array', 'dict'].includes(v.type) ? formatValue(v, true) : v.value
|
return ['array', 'dict'].includes(v.type) ? formatValue(v, true) : v.value
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
return toValue(null)
|
return toValue(null)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -69,10 +63,11 @@ export const globals: Record<string, any> = {
|
||||||
},
|
},
|
||||||
ref: (fn: Function) => fn,
|
ref: (fn: Function) => fn,
|
||||||
import: function (this: VM, atNamed: Record<any, string | string[]> = {}, ...idents: string[]) {
|
import: function (this: VM, atNamed: Record<any, string | string[]> = {}, ...idents: string[]) {
|
||||||
const onlyArray = Array.isArray(atNamed.only) ? atNamed.only : [atNamed.only].filter((a) => a)
|
const onlyArray = Array.isArray(atNamed.only) ? atNamed.only : [atNamed.only].filter(a => a)
|
||||||
const only = new Set(onlyArray)
|
const only = new Set(onlyArray)
|
||||||
const wantsOnly = only.size > 0
|
const wantsOnly = only.size > 0
|
||||||
|
|
||||||
|
|
||||||
for (const ident of idents) {
|
for (const ident of idents) {
|
||||||
const module = this.get(ident)
|
const module = this.get(ident)
|
||||||
|
|
||||||
|
|
@ -105,13 +100,9 @@ export const globals: Record<string, any> = {
|
||||||
length: (v: any) => {
|
length: (v: any) => {
|
||||||
const value = toValue(v)
|
const value = toValue(v)
|
||||||
switch (value.type) {
|
switch (value.type) {
|
||||||
case 'string':
|
case 'string': case 'array': return value.value.length
|
||||||
case 'array':
|
case 'dict': return value.value.size
|
||||||
return value.value.length
|
default: throw new Error(`length: expected string, array, or dict, got ${value.type}`)
|
||||||
case 'dict':
|
|
||||||
return value.value.size
|
|
||||||
default:
|
|
||||||
throw new Error(`length: expected string, array, or dict, got ${value.type}`)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
at: (collection: any, index: number | string) => {
|
at: (collection: any, index: number | string) => {
|
||||||
|
|
@ -119,9 +110,7 @@ export const globals: Record<string, any> = {
|
||||||
if (value.type === 'string' || value.type === 'array') {
|
if (value.type === 'string' || value.type === 'array') {
|
||||||
const idx = typeof index === 'number' ? index : parseInt(index as string)
|
const idx = typeof index === 'number' ? index : parseInt(index as string)
|
||||||
if (idx < 0 || idx >= value.value.length) {
|
if (idx < 0 || idx >= value.value.length) {
|
||||||
throw new Error(
|
throw new Error(`at: index ${idx} out of bounds for ${value.type} of length ${value.value.length}`)
|
||||||
`at: index ${idx} out of bounds for ${value.type} of length ${value.value.length}`,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return value.value[idx]
|
return value.value[idx]
|
||||||
} else if (value.type === 'dict') {
|
} else if (value.type === 'dict') {
|
||||||
|
|
@ -148,8 +137,7 @@ export const globals: Record<string, any> = {
|
||||||
'empty?': (v: any) => {
|
'empty?': (v: any) => {
|
||||||
const value = toValue(v)
|
const value = toValue(v)
|
||||||
switch (value.type) {
|
switch (value.type) {
|
||||||
case 'string':
|
case 'string': case 'array':
|
||||||
case 'array':
|
|
||||||
return value.value.length === 0
|
return value.value.length === 0
|
||||||
case 'dict':
|
case 'dict':
|
||||||
return value.value.size === 0
|
return value.value.size === 0
|
||||||
|
|
@ -163,6 +151,7 @@ export const globals: Record<string, any> = {
|
||||||
for (const value of list) await cb(value)
|
for (const value of list) await cb(value)
|
||||||
return list
|
return list
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const colors = {
|
export const colors = {
|
||||||
|
|
@ -175,7 +164,7 @@ export const colors = {
|
||||||
red: '\x1b[31m',
|
red: '\x1b[31m',
|
||||||
blue: '\x1b[34m',
|
blue: '\x1b[34m',
|
||||||
magenta: '\x1b[35m',
|
magenta: '\x1b[35m',
|
||||||
pink: '\x1b[38;2;255;105;180m',
|
pink: '\x1b[38;2;255;105;180m'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatValue(value: Value, inner = false): string {
|
export function formatValue(value: Value, inner = false): string {
|
||||||
|
|
@ -189,15 +178,15 @@ export function formatValue(value: Value, inner = false): string {
|
||||||
case 'null':
|
case 'null':
|
||||||
return `${colors.dim}null${colors.reset}`
|
return `${colors.dim}null${colors.reset}`
|
||||||
case 'array': {
|
case 'array': {
|
||||||
const items = value.value.map((x) => formatValue(x, true)).join(' ')
|
const items = value.value.map(x => formatValue(x, true)).join(' ')
|
||||||
return `${colors.blue}[${colors.reset}${items}${colors.blue}]${colors.reset}`
|
return `${colors.blue}[${colors.reset}${items}${colors.blue}]${colors.reset}`
|
||||||
}
|
}
|
||||||
case 'dict': {
|
case 'dict': {
|
||||||
const entries = Array.from(value.value.entries())
|
const entries = Array.from(value.value.entries()).reverse()
|
||||||
.reverse()
|
|
||||||
.map(([k, v]) => `${k.trim()}${colors.blue}=${colors.reset}${formatValue(v, true)}`)
|
.map(([k, v]) => `${k.trim()}${colors.blue}=${colors.reset}${formatValue(v, true)}`)
|
||||||
.join(' ')
|
.join(' ')
|
||||||
if (entries.length === 0) return `${colors.blue}[=]${colors.reset}`
|
if (entries.length === 0)
|
||||||
|
return `${colors.blue}[=]${colors.reset}`
|
||||||
return `${colors.blue}[${colors.reset}${entries}${colors.blue}]${colors.reset}`
|
return `${colors.blue}[${colors.reset}${entries}${colors.blue}]${colors.reset}`
|
||||||
}
|
}
|
||||||
case 'function': {
|
case 'function': {
|
||||||
|
|
@ -217,4 +206,5 @@ export function formatValue(value: Value, inner = false): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// add types functions to top-level namespace
|
// add types functions to top-level namespace
|
||||||
for (const [key, value] of Object.entries(types)) globals[key] = value
|
for (const [key, value] of Object.entries(types))
|
||||||
|
globals[key] = value
|
||||||
|
|
@ -2,5 +2,6 @@ export const json = {
|
||||||
encode: (s: any) => JSON.stringify(s),
|
encode: (s: any) => JSON.stringify(s),
|
||||||
decode: (s: string) => JSON.parse(s),
|
decode: (s: string) => JSON.parse(s),
|
||||||
}
|
}
|
||||||
|
|
||||||
; (json as any).parse = json.decode
|
; (json as any).parse = json.decode
|
||||||
; (json as any).stringify = json.encode
|
; (json as any).stringify = json.encode
|
||||||
|
|
@ -46,7 +46,7 @@ export const list = {
|
||||||
},
|
},
|
||||||
'all?': async (list: any[], cb: Function) => {
|
'all?': async (list: any[], cb: Function) => {
|
||||||
for (const value of list) {
|
for (const value of list) {
|
||||||
if (!(await cb(value))) return false
|
if (!await cb(value)) return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
|
@ -131,7 +131,7 @@ export const list = {
|
||||||
}
|
}
|
||||||
return [truthy, falsy]
|
return [truthy, falsy]
|
||||||
},
|
},
|
||||||
compact: (list: any[]) => list.filter((x) => x != null),
|
compact: (list: any[]) => list.filter(x => x != null),
|
||||||
'group-by': async (list: any[], cb: Function) => {
|
'group-by': async (list: any[], cb: Function) => {
|
||||||
const groups: Record<string, any[]> = {}
|
const groups: Record<string, any[]> = {}
|
||||||
for (const value of list) {
|
for (const value of list) {
|
||||||
|
|
@ -143,6 +143,7 @@ export const list = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// raw functions deal directly in Value types, meaning we can modify collection
|
// raw functions deal directly in Value types, meaning we can modify collection
|
||||||
// careful - they MUST return a Value!
|
// careful - they MUST return a Value!
|
||||||
; (list.splice as any).raw = true
|
; (list.splice as any).raw = true
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@ export const load = async function (this: VM, path: string): Promise<Record<stri
|
||||||
await this.continue()
|
await this.continue()
|
||||||
|
|
||||||
const module: Record<string, Value> = {}
|
const module: Record<string, Value> = {}
|
||||||
for (const [name, value] of this.scope.locals.entries()) module[name] = value
|
for (const [name, value] of this.scope.locals.entries())
|
||||||
|
module[name] = value
|
||||||
|
|
||||||
this.scope = scope
|
this.scope = scope
|
||||||
this.pc = pc
|
this.pc = pc
|
||||||
|
|
|
||||||
|
|
@ -17,23 +17,17 @@ export const str = {
|
||||||
'last-index-of': (str: string, search: string) => String(str ?? '').lastIndexOf(search),
|
'last-index-of': (str: string, search: string) => String(str ?? '').lastIndexOf(search),
|
||||||
|
|
||||||
// transformations
|
// transformations
|
||||||
replace: (str: string, search: string, replacement: string) =>
|
replace: (str: string, search: string, replacement: string) => String(str ?? '').replace(search, replacement),
|
||||||
String(str ?? '').replace(search, replacement),
|
'replace-all': (str: string, search: string, replacement: string) => String(str ?? '').replaceAll(search, replacement),
|
||||||
'replace-all': (str: string, search: string, replacement: string) =>
|
slice: (str: string, start: number, end?: number | null) => String(str ?? '').slice(start, end ?? undefined),
|
||||||
String(str ?? '').replaceAll(search, replacement),
|
substring: (str: string, start: number, end?: number | null) => String(str ?? '').substring(start, end ?? undefined),
|
||||||
slice: (str: string, start: number, end?: number | null) =>
|
|
||||||
String(str ?? '').slice(start, end ?? undefined),
|
|
||||||
substring: (str: string, start: number, end?: number | null) =>
|
|
||||||
String(str ?? '').substring(start, end ?? undefined),
|
|
||||||
repeat: (str: string, count: number) => {
|
repeat: (str: string, count: number) => {
|
||||||
if (count < 0) throw new Error(`repeat: count must be non-negative, got ${count}`)
|
if (count < 0) throw new Error(`repeat: count must be non-negative, got ${count}`)
|
||||||
if (!Number.isInteger(count)) throw new Error(`repeat: count must be an integer, got ${count}`)
|
if (!Number.isInteger(count)) throw new Error(`repeat: count must be an integer, got ${count}`)
|
||||||
return String(str ?? '').repeat(count)
|
return String(str ?? '').repeat(count)
|
||||||
},
|
},
|
||||||
'pad-start': (str: string, length: number, pad: string = ' ') =>
|
'pad-start': (str: string, length: number, pad: string = ' ') => String(str ?? '').padStart(length, pad),
|
||||||
String(str ?? '').padStart(length, pad),
|
'pad-end': (str: string, length: number, pad: string = ' ') => String(str ?? '').padEnd(length, pad),
|
||||||
'pad-end': (str: string, length: number, pad: string = ' ') =>
|
|
||||||
String(str ?? '').padEnd(length, pad),
|
|
||||||
capitalize: (str: string) => {
|
capitalize: (str: string) => {
|
||||||
const s = String(str ?? '')
|
const s = String(str ?? '')
|
||||||
return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase()
|
return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase()
|
||||||
|
|
|
||||||
|
|
@ -314,16 +314,14 @@ describe('fs - other', () => {
|
||||||
writeFileSync(file, 'initial')
|
writeFileSync(file, 'initial')
|
||||||
|
|
||||||
let called = false
|
let called = false
|
||||||
const watcher = fs.watch(file, () => {
|
const watcher = fs.watch(file, () => { called = true })
|
||||||
called = true
|
|
||||||
})
|
|
||||||
|
|
||||||
// Trigger change
|
// Trigger change
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
await new Promise(resolve => setTimeout(resolve, 100))
|
||||||
writeFileSync(file, 'updated')
|
writeFileSync(file, 'updated')
|
||||||
|
|
||||||
// Wait for watcher
|
// Wait for watcher
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
|
||||||
expect(called).toBe(true)
|
expect(called).toBe(true)
|
||||||
watcher.close?.()
|
watcher.close?.()
|
||||||
|
|
|
||||||
|
|
@ -5,22 +5,14 @@ describe('json', () => {
|
||||||
expect(`json.decode '[1,2,3]'`).toEvaluateTo([1, 2, 3])
|
expect(`json.decode '[1,2,3]'`).toEvaluateTo([1, 2, 3])
|
||||||
expect(`json.decode '"heya"'`).toEvaluateTo('heya')
|
expect(`json.decode '"heya"'`).toEvaluateTo('heya')
|
||||||
expect(`json.decode '[true, false, null]'`).toEvaluateTo([true, false, null])
|
expect(`json.decode '[true, false, null]'`).toEvaluateTo([true, false, null])
|
||||||
expect(`json.decode '{"a": true, "b": false, "c": "yeah"}'`).toEvaluateTo({
|
expect(`json.decode '{"a": true, "b": false, "c": "yeah"}'`).toEvaluateTo({ a: true, b: false, c: "yeah" })
|
||||||
a: true,
|
|
||||||
b: false,
|
|
||||||
c: 'yeah',
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('json.encode', () => {
|
test('json.encode', () => {
|
||||||
expect(`json.encode [1 2 3]`).toEvaluateTo('[1,2,3]')
|
expect(`json.encode [1 2 3]`).toEvaluateTo('[1,2,3]')
|
||||||
expect(`json.encode 'heya'`).toEvaluateTo('"heya"')
|
expect(`json.encode 'heya'`).toEvaluateTo('"heya"')
|
||||||
expect(`json.encode [true false null]`).toEvaluateTo('[true,false,null]')
|
expect(`json.encode [true false null]`).toEvaluateTo('[true,false,null]')
|
||||||
expect(`json.encode [a=true b=false c='yeah'] | json.decode`).toEvaluateTo({
|
expect(`json.encode [a=true b=false c='yeah'] | json.decode`).toEvaluateTo({ a: true, b: false, c: "yeah" })
|
||||||
a: true,
|
|
||||||
b: false,
|
|
||||||
c: 'yeah',
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('edge cases - empty structures', () => {
|
test('edge cases - empty structures', () => {
|
||||||
|
|
@ -59,31 +51,27 @@ describe('json', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('nested structures - arrays', () => {
|
test('nested structures - arrays', () => {
|
||||||
expect(`json.decode '[[1,2],[3,4],[5,6]]'`).toEvaluateTo([
|
expect(`json.decode '[[1,2],[3,4],[5,6]]'`).toEvaluateTo([[1, 2], [3, 4], [5, 6]])
|
||||||
[1, 2],
|
|
||||||
[3, 4],
|
|
||||||
[5, 6],
|
|
||||||
])
|
|
||||||
expect(`json.decode '[1,[2,[3,[4]]]]'`).toEvaluateTo([1, [2, [3, [4]]]])
|
expect(`json.decode '[1,[2,[3,[4]]]]'`).toEvaluateTo([1, [2, [3, [4]]]])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('nested structures - objects', () => {
|
test('nested structures - objects', () => {
|
||||||
expect(`json.decode '{"user":{"name":"Alice","age":30}}'`).toEvaluateTo({
|
expect(`json.decode '{"user":{"name":"Alice","age":30}}'`).toEvaluateTo({
|
||||||
user: { name: 'Alice', age: 30 },
|
user: { name: 'Alice', age: 30 }
|
||||||
})
|
})
|
||||||
expect(`json.decode '{"a":{"b":{"c":"deep"}}}'`).toEvaluateTo({
|
expect(`json.decode '{"a":{"b":{"c":"deep"}}}'`).toEvaluateTo({
|
||||||
a: { b: { c: 'deep' } },
|
a: { b: { c: 'deep' } }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('nested structures - mixed arrays and objects', () => {
|
test('nested structures - mixed arrays and objects', () => {
|
||||||
expect(`json.decode '[{"id":1,"tags":["a","b"]},{"id":2,"tags":["c"]}]'`).toEvaluateTo([
|
expect(`json.decode '[{"id":1,"tags":["a","b"]},{"id":2,"tags":["c"]}]'`).toEvaluateTo([
|
||||||
{ id: 1, tags: ['a', 'b'] },
|
{ id: 1, tags: ['a', 'b'] },
|
||||||
{ id: 2, tags: ['c'] },
|
{ id: 2, tags: ['c'] }
|
||||||
])
|
])
|
||||||
expect(`json.decode '{"items":[1,2,3],"meta":{"count":3}}'`).toEvaluateTo({
|
expect(`json.decode '{"items":[1,2,3],"meta":{"count":3}}'`).toEvaluateTo({
|
||||||
items: [1, 2, 3],
|
items: [1, 2, 3],
|
||||||
meta: { count: 3 },
|
meta: { count: 3 }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -277,10 +277,7 @@ describe('collections', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.zip combines two arrays', async () => {
|
test('list.zip combines two arrays', async () => {
|
||||||
await expect(`list.zip [1 2] [3 4]`).toEvaluateTo([
|
await expect(`list.zip [1 2] [3 4]`).toEvaluateTo([[1, 3], [2, 4]])
|
||||||
[1, 3],
|
|
||||||
[2, 4],
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.first returns first element', async () => {
|
test('list.first returns first element', async () => {
|
||||||
|
|
@ -450,10 +447,7 @@ describe('collections', () => {
|
||||||
await expect(`
|
await expect(`
|
||||||
gt-two = do x: x > 2 end
|
gt-two = do x: x > 2 end
|
||||||
list.partition [1 2 3 4 5] gt-two
|
list.partition [1 2 3 4 5] gt-two
|
||||||
`).toEvaluateTo([
|
`).toEvaluateTo([[3, 4, 5], [1, 2]])
|
||||||
[3, 4, 5],
|
|
||||||
[1, 2],
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.compact removes null values', async () => {
|
test('list.compact removes null values', async () => {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ export const types = {
|
||||||
'string?': (v: any) => toValue(v).type === 'string',
|
'string?': (v: any) => toValue(v).type === 'string',
|
||||||
string: (v: any) => String(v),
|
string: (v: any) => String(v),
|
||||||
|
|
||||||
|
|
||||||
'array?': (v: any) => toValue(v).type === 'array',
|
'array?': (v: any) => toValue(v).type === 'array',
|
||||||
'list?': (v: any) => toValue(v).type === 'array',
|
'list?': (v: any) => toValue(v).type === 'array',
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,50 @@
|
||||||
:root {
|
:root {
|
||||||
/* Background colors */
|
/* Background colors */
|
||||||
--bg-editor: #011627;
|
--bg-editor: #011627;
|
||||||
--bg-output: #40318d;
|
--bg-output: #40318D;
|
||||||
--bg-status-bar: #1e2a4a;
|
--bg-status-bar: #1E2A4A;
|
||||||
--bg-status-border: #0e1a3a;
|
--bg-status-border: #0E1A3A;
|
||||||
--bg-selection: #1d3b53;
|
--bg-selection: #1D3B53;
|
||||||
--bg-variable-def: #1e2a4a;
|
--bg-variable-def: #1E2A4A;
|
||||||
|
|
||||||
/* Text colors */
|
/* Text colors */
|
||||||
--text-editor: #d6deeb;
|
--text-editor: #D6DEEB;
|
||||||
--text-output: #7c70da;
|
--text-output: #7C70DA;
|
||||||
--text-status: #b3a9ff55;
|
--text-status: #B3A9FF55;
|
||||||
--caret: #80a4c2;
|
--caret: #80A4C2;
|
||||||
|
|
||||||
/* Syntax highlighting colors */
|
/* Syntax highlighting colors */
|
||||||
--color-keyword: #c792ea;
|
--color-keyword: #C792EA;
|
||||||
--color-function: #82aaff;
|
--color-function: #82AAFF;
|
||||||
--color-string: #c3e88d;
|
--color-string: #C3E88D;
|
||||||
--color-number: #f78c6c;
|
--color-number: #F78C6C;
|
||||||
--color-bool: #ff5370;
|
--color-bool: #FF5370;
|
||||||
--color-operator: #89ddff;
|
--color-operator: #89DDFF;
|
||||||
--color-paren: #676e95;
|
--color-paren: #676E95;
|
||||||
--color-function-call: #ff9cac;
|
--color-function-call: #FF9CAC;
|
||||||
--color-variable-def: #ffcb6b;
|
--color-variable-def: #FFCB6B;
|
||||||
--color-error: #ff6e6e;
|
--color-error: #FF6E6E;
|
||||||
--color-regex: #e1acff;
|
--color-regex: #E1ACFF;
|
||||||
|
|
||||||
/* ANSI terminal colors */
|
/* ANSI terminal colors */
|
||||||
--ansi-black: #011627;
|
--ansi-black: #011627;
|
||||||
--ansi-red: #ff5370;
|
--ansi-red: #FF5370;
|
||||||
--ansi-green: #c3e88d;
|
--ansi-green: #C3E88D;
|
||||||
--ansi-yellow: #ffcb6b;
|
--ansi-yellow: #FFCB6B;
|
||||||
--ansi-blue: #82aaff;
|
--ansi-blue: #82AAFF;
|
||||||
--ansi-magenta: #c792ea;
|
--ansi-magenta: #C792EA;
|
||||||
--ansi-cyan: #89ddff;
|
--ansi-cyan: #89DDFF;
|
||||||
--ansi-white: #d6deeb;
|
--ansi-white: #D6DEEB;
|
||||||
|
|
||||||
/* ANSI bright colors (slightly more vibrant) */
|
/* ANSI bright colors (slightly more vibrant) */
|
||||||
--ansi-bright-black: #676e95;
|
--ansi-bright-black: #676E95;
|
||||||
--ansi-bright-red: #ff6e90;
|
--ansi-bright-red: #FF6E90;
|
||||||
--ansi-bright-green: #d4f6a8;
|
--ansi-bright-green: #D4F6A8;
|
||||||
--ansi-bright-yellow: #ffe082;
|
--ansi-bright-yellow: #FFE082;
|
||||||
--ansi-bright-blue: #a8c7fa;
|
--ansi-bright-blue: #A8C7FA;
|
||||||
--ansi-bright-magenta: #e1acff;
|
--ansi-bright-magenta: #E1ACFF;
|
||||||
--ansi-bright-cyan: #a8f5ff;
|
--ansi-bright-cyan: #A8F5FF;
|
||||||
--ansi-bright-white: #ffffff;
|
--ansi-bright-white: #FFFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ declare module 'bun:test' {
|
||||||
toFailEvaluation(): Promise<T>
|
toFailEvaluation(): Promise<T>
|
||||||
toBeToken(expected: string): T
|
toBeToken(expected: string): T
|
||||||
toMatchToken(typeOrValue: string, value?: string): T
|
toMatchToken(typeOrValue: string, value?: string): T
|
||||||
toMatchTokens(...tokens: { type: string; value?: string }[]): T
|
toMatchTokens(...tokens: { type: string, value?: string }[]): T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,7 +146,7 @@ expect.extend({
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: () => `Expected token type to be ${expected}, but got ${TokenType[value.type]}`,
|
message: () => `Expected token type to be ${expected}, but got ${TokenType[value.type]}`,
|
||||||
pass: value.type === target,
|
pass: value.type === target
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -166,8 +166,7 @@ expect.extend({
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return {
|
return {
|
||||||
message: () =>
|
message: () => `Expected token to be ${expectedValue.replaceAll('\n', '\\n')}, got ${token}`,
|
||||||
`Expected token to be ${expectedValue.replaceAll('\n', '\\n')}, got ${token}`,
|
|
||||||
pass: false,
|
pass: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -175,14 +174,13 @@ expect.extend({
|
||||||
if (expectedType && TokenType[expectedType as keyof typeof TokenType] !== token.type) {
|
if (expectedType && TokenType[expectedType as keyof typeof TokenType] !== token.type) {
|
||||||
return {
|
return {
|
||||||
message: () => `Expected token to be ${expectedType}, but got ${TokenType[token.type]}`,
|
message: () => `Expected token to be ${expectedType}, but got ${TokenType[token.type]}`,
|
||||||
pass: false,
|
pass: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: () =>
|
message: () => `Expected token to be ${expectedValue.replaceAll('\n', '\\n')}, but got ${token.value}`,
|
||||||
`Expected token to be ${expectedValue.replaceAll('\n', '\\n')}, but got ${token.value}`,
|
pass: token.value === expectedValue
|
||||||
pass: token.value === expectedValue,
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -191,11 +189,11 @@ expect.extend({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toMatchTokens(received: unknown, ...tokens: { type: string; value?: string }[]) {
|
toMatchTokens(received: unknown, ...tokens: { type: string, value?: string }[]) {
|
||||||
assert(typeof received === 'string', 'toMatchTokens can only be used with string values')
|
assert(typeof received === 'string', 'toMatchTokens can only be used with string values')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = tokenize(received).map((t) => toHumanToken(t))
|
const result = tokenize(received).map(t => toHumanToken(t))
|
||||||
|
|
||||||
if (result.length === 0 && tokens.length > 0) {
|
if (result.length === 0 && tokens.length > 0) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -209,7 +207,7 @@ expect.extend({
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: () => `Tokens don't match: \n\n${diff(actual, expected)}`,
|
message: () => `Tokens don't match: \n\n${diff(actual, expected)}`,
|
||||||
pass: expected == actual,
|
pass: expected == actual
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -217,18 +215,18 @@ expect.extend({
|
||||||
pass: false,
|
pass: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const tokenize = (code: string): Token[] => {
|
const tokenize = (code: string): Token[] => {
|
||||||
const scanner = new Scanner()
|
const scanner = new Scanner
|
||||||
return scanner.tokenize(code)
|
return scanner.tokenize(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toHumanToken = (tok: Token): { type: string; value?: string } => {
|
const toHumanToken = (tok: Token): { type: string, value?: string } => {
|
||||||
return {
|
return {
|
||||||
type: TokenType[tok.type],
|
type: TokenType[tok.type],
|
||||||
value: tok.value,
|
value: tok.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -243,7 +241,7 @@ const trimWhitespace = (str: string): string => {
|
||||||
if (!line.startsWith(leadingWhitespace)) {
|
if (!line.startsWith(leadingWhitespace)) {
|
||||||
let foundWhitespace = line.match(/^(\s*)/)?.[1] || ''
|
let foundWhitespace = line.match(/^(\s*)/)?.[1] || ''
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Line has inconsistent leading whitespace: "${line}"(found "${foundWhitespace}", expected "${leadingWhitespace}")`,
|
`Line has inconsistent leading whitespace: "${line}"(found "${foundWhitespace}", expected "${leadingWhitespace}")`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return line.slice(leadingWhitespace.length)
|
return line.slice(leadingWhitespace.length)
|
||||||
|
|
@ -259,7 +257,7 @@ const diff = (a: string, b: string): string => {
|
||||||
if (expected !== actual) {
|
if (expected !== actual) {
|
||||||
const changes = diffLines(actual, expected)
|
const changes = diffLines(actual, expected)
|
||||||
for (const part of changes) {
|
for (const part of changes) {
|
||||||
const sign = part.added ? '+' : part.removed ? '-' : ' '
|
const sign = part.added ? "+" : part.removed ? "-" : " "
|
||||||
let line = sign + part.value
|
let line = sign + part.value
|
||||||
if (part.added) {
|
if (part.added) {
|
||||||
line = color.green(line)
|
line = color.green(line)
|
||||||
|
|
@ -267,7 +265,7 @@ const diff = (a: string, b: string): string => {
|
||||||
line = color.red(line)
|
line = color.red(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push(line.endsWith('\n') || line.endsWith('\n\u001b[39m') ? line : line + '\n')
|
lines.push(line.endsWith("\n") || line.endsWith("\n\u001b[39m") ? line : line + "\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
5
vscode-extension/.vscode/launch.json
vendored
5
vscode-extension/.vscode/launch.json
vendored
|
|
@ -5,7 +5,10 @@
|
||||||
"name": "Run Extension",
|
"name": "Run Extension",
|
||||||
"type": "extensionHost",
|
"type": "extensionHost",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "--profile=Shrimp Dev"],
|
"args": [
|
||||||
|
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||||
|
"--profile=Shrimp Dev"
|
||||||
|
],
|
||||||
"outFiles": [
|
"outFiles": [
|
||||||
"${workspaceFolder}/client/dist/**/*.js",
|
"${workspaceFolder}/client/dist/**/*.js",
|
||||||
"${workspaceFolder}/server/dist/**/*.js"
|
"${workspaceFolder}/server/dist/**/*.js"
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||||
'shrimpLanguageServer',
|
'shrimpLanguageServer',
|
||||||
'Shrimp Language Server',
|
'Shrimp Language Server',
|
||||||
serverOptions,
|
serverOptions,
|
||||||
clientOptions,
|
clientOptions
|
||||||
)
|
)
|
||||||
|
|
||||||
client.start()
|
client.start()
|
||||||
|
|
@ -46,7 +46,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||||
language: 'text',
|
language: 'text',
|
||||||
})
|
})
|
||||||
await vscode.window.showTextDocument(doc, { preview: false })
|
await vscode.window.showTextDocument(doc, { preview: false })
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
// Command: Show Bytecode
|
// Command: Show Bytecode
|
||||||
|
|
@ -67,7 +67,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||||
language: 'text',
|
language: 'text',
|
||||||
})
|
})
|
||||||
await vscode.window.showTextDocument(doc, { preview: false })
|
await vscode.window.showTextDocument(doc, { preview: false })
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
// Command: Run File
|
// Command: Run File
|
||||||
|
|
@ -93,7 +93,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||||
const terminal = vscode.window.createTerminal('Shrimp')
|
const terminal = vscode.window.createTerminal('Shrimp')
|
||||||
terminal.show()
|
terminal.show()
|
||||||
terminal.sendText(`${binaryPath} "${filePath}"`)
|
terminal.sendText(`${binaryPath} "${filePath}"`)
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,5 +113,5 @@ console.log(`✓ Generated ${names.length} prelude names to server/src/metadata/
|
||||||
console.log(
|
console.log(
|
||||||
`✓ Generated completions for ${
|
`✓ Generated completions for ${
|
||||||
Object.keys(moduleMetadata).length
|
Object.keys(moduleMetadata).length
|
||||||
} modules to server/src/metadata/prelude-completions.ts`,
|
} modules to server/src/metadata/prelude-completions.ts`
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { analyzeCompletionContext } from './contextAnalyzer'
|
||||||
*/
|
*/
|
||||||
export const provideCompletions = (
|
export const provideCompletions = (
|
||||||
document: TextDocument,
|
document: TextDocument,
|
||||||
position: { line: number; character: number },
|
position: { line: number; character: number }
|
||||||
): CompletionItem[] => {
|
): CompletionItem[] => {
|
||||||
const context = analyzeCompletionContext(document, position)
|
const context = analyzeCompletionContext(document, position)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export type CompletionContext =
|
||||||
*/
|
*/
|
||||||
export const analyzeCompletionContext = (
|
export const analyzeCompletionContext = (
|
||||||
document: TextDocument,
|
document: TextDocument,
|
||||||
position: { line: number; character: number },
|
position: { line: number; character: number }
|
||||||
): CompletionContext => {
|
): CompletionContext => {
|
||||||
const offset = document.offsetAt(position)
|
const offset = document.offsetAt(position)
|
||||||
const text = document.getText()
|
const text = document.getText()
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -2,44 +2,44 @@
|
||||||
// Do not edit manually - run 'bun run generate-prelude-metadata' to regenerate
|
// Do not edit manually - run 'bun run generate-prelude-metadata' to regenerate
|
||||||
|
|
||||||
export const PRELUDE_NAMES = [
|
export const PRELUDE_NAMES = [
|
||||||
'$',
|
"$",
|
||||||
'array?',
|
"array?",
|
||||||
'at',
|
"at",
|
||||||
'bnot',
|
"bnot",
|
||||||
'boolean',
|
"boolean",
|
||||||
'boolean?',
|
"boolean?",
|
||||||
'date',
|
"date",
|
||||||
'dec',
|
"dec",
|
||||||
'describe',
|
"describe",
|
||||||
'dict',
|
"dict",
|
||||||
'dict?',
|
"dict?",
|
||||||
'each',
|
"each",
|
||||||
'echo',
|
"echo",
|
||||||
'empty?',
|
"empty?",
|
||||||
'exit',
|
"exit",
|
||||||
'fs',
|
"fs",
|
||||||
'function?',
|
"function?",
|
||||||
'identity',
|
"identity",
|
||||||
'import',
|
"import",
|
||||||
'inc',
|
"inc",
|
||||||
'inspect',
|
"inspect",
|
||||||
'json',
|
"json",
|
||||||
'length',
|
"length",
|
||||||
'list',
|
"list",
|
||||||
'list?',
|
"list?",
|
||||||
'load',
|
"load",
|
||||||
'math',
|
"math",
|
||||||
'not',
|
"not",
|
||||||
'null?',
|
"null?",
|
||||||
'number',
|
"number",
|
||||||
'number?',
|
"number?",
|
||||||
'range',
|
"range",
|
||||||
'ref',
|
"ref",
|
||||||
'some?',
|
"some?",
|
||||||
'str',
|
"str",
|
||||||
'string',
|
"string",
|
||||||
'string?',
|
"string?",
|
||||||
'type',
|
"type",
|
||||||
'var',
|
"var",
|
||||||
'var?',
|
"var?"
|
||||||
] as const
|
] as const
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ export function buildSemanticTokens(document: TextDocument, tree: Tree): number[
|
||||||
function emitNamedArgPrefix(
|
function emitNamedArgPrefix(
|
||||||
node: SyntaxNode,
|
node: SyntaxNode,
|
||||||
document: TextDocument,
|
document: TextDocument,
|
||||||
builder: SemanticTokensBuilder,
|
builder: SemanticTokensBuilder
|
||||||
) {
|
) {
|
||||||
const text = document.getText({
|
const text = document.getText({
|
||||||
start: document.positionAt(node.from),
|
start: document.positionAt(node.from),
|
||||||
|
|
@ -57,7 +57,7 @@ function emitNamedArgPrefix(
|
||||||
start.character,
|
start.character,
|
||||||
nameLength,
|
nameLength,
|
||||||
TOKEN_TYPES.indexOf(SemanticTokenTypes.property),
|
TOKEN_TYPES.indexOf(SemanticTokenTypes.property),
|
||||||
0,
|
0
|
||||||
)
|
)
|
||||||
|
|
||||||
// Emit token for the "=" part
|
// Emit token for the "=" part
|
||||||
|
|
@ -66,7 +66,7 @@ function emitNamedArgPrefix(
|
||||||
start.character + nameLength,
|
start.character + nameLength,
|
||||||
1, // Just the = character
|
1, // Just the = character
|
||||||
TOKEN_TYPES.indexOf(SemanticTokenTypes.operator),
|
TOKEN_TYPES.indexOf(SemanticTokenTypes.operator),
|
||||||
0,
|
0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,7 +75,7 @@ function walkTree(
|
||||||
node: SyntaxNode,
|
node: SyntaxNode,
|
||||||
document: TextDocument,
|
document: TextDocument,
|
||||||
builder: SemanticTokensBuilder,
|
builder: SemanticTokensBuilder,
|
||||||
scopeTracker: EditorScopeAnalyzer,
|
scopeTracker: EditorScopeAnalyzer
|
||||||
) {
|
) {
|
||||||
// Special handling for NamedArgPrefix to split "name=" into two tokens
|
// Special handling for NamedArgPrefix to split "name=" into two tokens
|
||||||
if (node.type.id === Terms.NamedArgPrefix) {
|
if (node.type.id === Terms.NamedArgPrefix) {
|
||||||
|
|
@ -102,7 +102,7 @@ type TokenInfo = { type: number; modifiers: number } | undefined
|
||||||
function getTokenType(
|
function getTokenType(
|
||||||
node: SyntaxNode,
|
node: SyntaxNode,
|
||||||
document: TextDocument,
|
document: TextDocument,
|
||||||
scopeTracker: EditorScopeAnalyzer,
|
scopeTracker: EditorScopeAnalyzer
|
||||||
): TokenInfo {
|
): TokenInfo {
|
||||||
const nodeTypeId = node.type.id
|
const nodeTypeId = node.type.id
|
||||||
const parentTypeId = node.parent?.type.id
|
const parentTypeId = node.parent?.type.id
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ function handleCompletion(params: any) {
|
||||||
if (contextCompletions.length > 0) {
|
if (contextCompletions.length > 0) {
|
||||||
console.log(
|
console.log(
|
||||||
`✅ Returning ${contextCompletions.length} completions:`,
|
`✅ Returning ${contextCompletions.length} completions:`,
|
||||||
contextCompletions.map((c) => c.label).join(', '),
|
contextCompletions.map((c) => c.label).join(', ')
|
||||||
)
|
)
|
||||||
return contextCompletions
|
return contextCompletions
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
import {
|
import { SignatureHelp, SignatureInformation, ParameterInformation } from 'vscode-languageserver/node'
|
||||||
SignatureHelp,
|
|
||||||
SignatureInformation,
|
|
||||||
ParameterInformation,
|
|
||||||
} from 'vscode-languageserver/node'
|
|
||||||
import { TextDocument } from 'vscode-languageserver-textdocument'
|
import { TextDocument } from 'vscode-languageserver-textdocument'
|
||||||
import { Tree, SyntaxNode } from '@lezer/common'
|
import { Tree, SyntaxNode } from '@lezer/common'
|
||||||
import { parser } from '../../../src/parser/shrimp'
|
import { parser } from '../../../src/parser/shrimp'
|
||||||
|
|
@ -10,7 +6,7 @@ import { completions } from './metadata/prelude-completions'
|
||||||
|
|
||||||
export const provideSignatureHelp = (
|
export const provideSignatureHelp = (
|
||||||
document: TextDocument,
|
document: TextDocument,
|
||||||
position: { line: number; character: number },
|
position: { line: number; character: number }
|
||||||
): SignatureHelp | undefined => {
|
): SignatureHelp | undefined => {
|
||||||
const text = document.getText()
|
const text = document.getText()
|
||||||
const tree = parser.parse(text)
|
const tree = parser.parse(text)
|
||||||
|
|
@ -104,6 +100,6 @@ const lookupFunctionParams = (funcName: string): string[] | undefined => {
|
||||||
|
|
||||||
const buildSignature = (funcName: string, params: string[]): SignatureInformation => {
|
const buildSignature = (funcName: string, params: string[]): SignatureInformation => {
|
||||||
const label = `${funcName}(${params.join(', ')})`
|
const label = `${funcName}(${params.join(', ')})`
|
||||||
const parameters: ParameterInformation[] = params.map((p) => ({ label: p }))
|
const parameters: ParameterInformation[] = params.map(p => ({ label: p }))
|
||||||
return { label, parameters }
|
return { label, parameters }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user