Compare commits

..

1 Commits

Author SHA1 Message Date
1452b2e00e Formatted EVERYTHING with prettier 2025-12-17 09:58:02 -08:00
61 changed files with 1176 additions and 1455 deletions

View File

@ -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) {

View File

@ -58,7 +58,10 @@ export class Compiler {
bytecode: Bytecode bytecode: Bytecode
pipeCounter = 0 pipeCounter = 0
constructor(public input: string, globals?: string[] | Record<string, any>) { constructor(
public input: string,
globals?: string[] | Record<string, any>,
) {
try { try {
if (globals) setGlobals(Array.isArray(globals) ? globals : Object.keys(globals)) if (globals) setGlobals(Array.isArray(globals) ? globals : Object.keys(globals))
const ast = parse(input) const ast = parse(input)
@ -109,9 +112,15 @@ 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 (value.startsWith('-') && (value.includes('0x') || value.includes('0b') || value.includes('0o'))) { if (
value.startsWith('-') &&
(value.includes('0x') || value.includes('0b') || value.includes('0o'))
) {
numberValue = -Number(value.slice(1)) numberValue = -Number(value.slice(1))
} else if (value.startsWith('+') && (value.includes('0x') || value.includes('0b') || value.includes('0o'))) { } else if (
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)
@ -123,8 +132,7 @@ export class Compiler {
return [[`PUSH`, numberValue]] return [[`PUSH`, numberValue]]
case 'String': { case 'String': {
if (node.firstChild?.type.is('CurlyString')) if (node.firstChild?.type.is('CurlyString')) return this.#compileCurlyString(value, input)
return this.#compileCurlyString(value, input)
const { parts, hasInterpolation } = getStringParts(node, input) const { parts, hasInterpolation } = getStringParts(node, input)
@ -166,7 +174,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,
) )
} }
}) })
@ -351,7 +359,7 @@ export class Compiler {
} }
// Standard compound assignments: evaluate both sides, then operate // Standard compound assignments: evaluate both sides, then operate
instructions.push(['LOAD', identifierName]) // will throw if undefined instructions.push(['LOAD', identifierName]) // will throw if undefined
instructions.push(...this.#compileNode(right, input)) instructions.push(...this.#compileNode(right, input))
switch (opValue) { switch (opValue) {
@ -374,7 +382,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,
) )
} }
@ -422,8 +430,8 @@ export class Compiler {
catchVariable, catchVariable,
catchBody, catchBody,
finallyBody, finallyBody,
input input,
) ),
) )
} else { } else {
instructions.push(...compileFunctionBody()) instructions.push(...compileFunctionBody())
@ -532,7 +540,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}:`])
@ -559,7 +567,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`,
) )
} }
@ -574,7 +582,7 @@ export class Compiler {
catchVariable, catchVariable,
catchBody, catchBody,
finallyBody, finallyBody,
input input,
) )
} }
@ -587,7 +595,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,
) )
} }
@ -601,7 +609,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))
@ -732,13 +740,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( const isUnderscoreInPositionalArgs = positionalArgs.some((arg) =>
(arg) => arg.type.is('Underscore') arg.type.is('Underscore'),
) )
const isUnderscoreInNamedArgs = namedArgs.some((arg) => { const isUnderscoreInNamedArgs = namedArgs.some((arg) => {
const { valueNode } = getNamedArgParts(arg, input) const { valueNode } = getNamedArgParts(arg, input)
@ -837,14 +845,12 @@ 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) => args.forEach((dict) => instructions.push(['PUSH', input.slice(dict.from, dict.to)]))
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)
@ -867,7 +873,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,
) )
} }
} }
@ -877,7 +883,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++

View File

@ -1,5 +1,9 @@
export class CompilerError extends Error { export class CompilerError extends Error {
constructor(message: string, private from: number, private to: number) { constructor(
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) {

View File

@ -112,8 +112,12 @@ 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('bleep') expect(`bloop = [ go=do: 'bleep' end ]; abc = do x: x end; abc (bloop.go)`).toEvaluateTo(
expect(`num = ((math.random) * 10 + 1) | math.floor; num >= 1 and num <= 10 `).toEvaluateTo(true) 'bleep',
)
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', () => {
@ -376,7 +380,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 })
}) })
}) })
@ -408,7 +412,9 @@ describe('Nullish coalescing operator (??)', () => {
}) })
test('short-circuits evaluation', () => { test('short-circuits evaluation', () => {
const throwError = () => { throw new Error('Should not evaluate') } const throwError = () => {
throw new Error('Should not evaluate')
}
expect('5 ?? throw-error').toEvaluateTo(5, { 'throw-error': throwError }) expect('5 ?? throw-error').toEvaluateTo(5, { 'throw-error': throwError })
}) })
@ -424,7 +430,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,
}) })
}) })
}) })
@ -456,7 +462,9 @@ describe('Nullish coalescing assignment (??=)', () => {
}) })
test('short-circuits evaluation when not null', () => { test('short-circuits evaluation when not null', () => {
const throwError = () => { throw new Error('Should not evaluate') } const throwError = () => {
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 })
}) })

View File

@ -10,12 +10,16 @@ describe('single line function blocks', () => {
}) })
test('work with named args', () => { test('work with named args', () => {
expect(`attach = do signal fn: [ signal (fn) ] end; attach signal='exit': true end`).toEvaluateTo(['exit', true]) expect(
`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(['EXIT', true]) expect(`signals = [trap=do x y: [x (y)] end]; signals.trap 'EXIT': true end`).toEvaluateTo([
'EXIT',
true,
])
}) })
}) })
@ -44,7 +48,6 @@ 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]

View File

@ -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')

View File

@ -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',
}) })
}) })

View File

@ -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,10 +20,12 @@ 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) { (fn as any).raw = true } function raw(fn: Function) {
;(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}"`)
@ -39,14 +41,13 @@ 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().join(' ') .reverse()
.join(' ')
.replaceAll(` ${NOSPACE_TOKEN} `, '') .replaceAll(` ${NOSPACE_TOKEN} `, '')
if (SELF_CLOSING.includes(tagName)) if (SELF_CLOSING.includes(tagName)) buffer.push(`<${tagName}${space}${attrs.join(' ')} />`)
buffer.push(`<${tagName}${space}${attrs.join(' ')} />`) else buffer.push(`<${tagName}${space}${attrs.join(' ')}>${children}</${tagName}>`)
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[]) => {
@ -60,10 +61,25 @@ 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 = ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"] const SELF_CLOSING = [
'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(`
@ -74,11 +90,14 @@ 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(`<head> `).toEvaluateTo(
`<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>`, ribbitGlobals) </head>`,
ribbitGlobals,
)
}) })
test('custom tags', () => { test('custom tags', () => {
@ -90,11 +109,14 @@ ribbit:
li two li two
li three li three
end end
end`).toEvaluateTo(`<ul class="list"> end`).toEvaluateTo(
`<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>`, ribbitGlobals) </ul>`,
ribbitGlobals,
)
}) })
test('inline expressions', () => { test('inline expressions', () => {
@ -110,6 +132,8 @@ end`).toEvaluateTo(`<ul class="list">
<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>`, ribbitGlobals) </p>`,
ribbitGlobals,
)
}) })
}) })

View File

@ -10,8 +10,7 @@ describe('while', () => {
a = false a = false
b = done b = done
end end
b`) b`).toEvaluateTo('done')
.toEvaluateTo('done')
}) })
test('basic expression', () => { test('basic expression', () => {
@ -20,8 +19,7 @@ describe('while', () => {
while a < 10: while a < 10:
a += 1 a += 1
end end
a`) a`).toEvaluateTo(10)
.toEvaluateTo(10)
}) })
test('compound expression', () => { test('compound expression', () => {
@ -31,8 +29,7 @@ describe('while', () => {
while a > 0 and b < 100: while a > 0 and b < 100:
b += 1 b += 1
end end
b`) b`).toEvaluateTo(100)
.toEvaluateTo(100)
}) })
test('returns value', () => { test('returns value', () => {
@ -42,7 +39,6 @@ describe('while', () => {
a += 1 a += 1
done done
end end
ret`) ret`).toEvaluateTo('done')
.toEvaluateTo('done')
}) })
}) })

View File

@ -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,10 +57,11 @@ 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 ${left ? left.type.name : 'none' `Assign left child must be an AssignableIdentifier or Array, got ${
left ? left.type.name : 'none'
}`, }`,
node.from, node.from,
node.to node.to,
) )
} }
@ -73,16 +74,17 @@ 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 ${left ? left.type.name : 'none' `CompoundAssign left child must be an AssignableIdentifier, got ${
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,
) )
} }
@ -97,7 +99,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,
) )
} }
@ -106,7 +108,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)
@ -129,7 +131,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)
@ -142,7 +144,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
@ -197,7 +199,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,
) )
} }
@ -251,7 +253,6 @@ 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')
) )
}) })
@ -266,16 +267,14 @@ 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( const hasInterpolation = parts.some((p) => p.type.is('Interpolation') || p.type.is('EscapeSeq'))
(p) => p.type.is('Interpolation') || p.type.is('EscapeSeq')
)
return { parts, hasInterpolation } return { parts, hasInterpolation }
} }
@ -287,7 +286,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,
) )
} }
@ -295,7 +294,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,
) )
} }
@ -303,7 +302,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,
) )
} }
@ -322,7 +321,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,
) )
} }
@ -341,7 +340,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)
@ -354,7 +353,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

View File

@ -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(', ')}}`
} }

View File

@ -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)}`,
) )
}) })

View File

@ -31,5 +31,5 @@ export const debugTags = ViewPlugin.fromClass(
order: -1, order: -1,
}) })
} }
} },
) )

View File

@ -58,5 +58,5 @@ export const shrimpErrors = ViewPlugin.fromClass(
}, },
{ {
decorations: (v) => v.decorations, decorations: (v) => v.decorations,
} },
) )

View File

@ -215,7 +215,7 @@ export const inlineHints = [
} }
}, },
}, },
} },
), ),
ghostTextTheme, ghostTextTheme,
] ]

View File

@ -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 = () => {

View File

@ -56,5 +56,5 @@ export const shrimpTheme = EditorView.theme(
backgroundColor: 'var(--color-string)', backgroundColor: 'var(--color-string)',
}, },
}, },
{ dark: true } { dark: true },
) )

View File

@ -14,99 +14,101 @@ export { type Value, type Bytecode } from 'reefvm'
export { toValue, fromValue, isValue, Scope, VM, bytecodeToString } from 'reefvm' export { toValue, fromValue, isValue, Scope, VM, bytecodeToString } from 'reefvm'
export class Shrimp { export class Shrimp {
vm: VM vm: VM
private globals?: Record<string, any> private globals?: Record<string, any>
constructor(globals?: Record<string, any>) { constructor(globals?: Record<string, any>) {
const emptyBytecode = { instructions: [], constants: [], labels: new Map() } const emptyBytecode = { instructions: [], constants: [], labels: new Map() }
this.vm = new VM(emptyBytecode, Object.assign({}, prelude, globals ?? {})) this.vm = new VM(emptyBytecode, Object.assign({}, prelude, globals ?? {}))
this.globals = globals this.globals = globals
}
get(name: string): any {
const value = this.vm.scope.get(name)
return value ? fromValue(value, this.vm) : null
}
set(name: string, value: any) {
this.vm.scope.set(name, toValue(value, this.vm))
}
has(name: string): boolean {
return this.vm.scope.has(name)
}
async call(name: string, ...args: any[]): Promise<any> {
const result = await this.vm.call(name, ...args)
return isValue(result) ? fromValue(result, this.vm) : result
}
parse(code: string): Tree {
return parseCode(code, this.globals)
}
compile(code: string): Bytecode {
return compileCode(code, this.globals)
}
async run(code: string | Bytecode, locals?: Record<string, any>): Promise<any> {
let bytecode
if (typeof code === 'string') {
const compiler = new Compiler(
code,
Object.keys(Object.assign({}, prelude, this.globals ?? {}, locals ?? {})),
)
bytecode = compiler.bytecode
} else {
bytecode = code
} }
get(name: string): any { if (locals) this.vm.pushScope(locals)
const value = this.vm.scope.get(name) this.vm.appendBytecode(bytecode)
return value ? fromValue(value, this.vm) : null await this.vm.continue()
} if (locals) this.vm.popScope()
set(name: string, value: any) {
this.vm.scope.set(name, toValue(value, this.vm))
}
has(name: string): boolean {
return this.vm.scope.has(name)
}
async call(name: string, ...args: any[]): Promise<any> {
const result = await this.vm.call(name, ...args)
return isValue(result) ? fromValue(result, this.vm) : result
}
parse(code: string): Tree {
return parseCode(code, this.globals)
}
compile(code: string): Bytecode {
return compileCode(code, this.globals)
}
async run(code: string | Bytecode, locals?: Record<string, any>): Promise<any> {
let bytecode
if (typeof code === 'string') {
const compiler = new Compiler(code, Object.keys(Object.assign({}, prelude, this.globals ?? {}, locals ?? {})))
bytecode = compiler.bytecode
} else {
bytecode = code
}
if (locals) this.vm.pushScope(locals)
this.vm.appendBytecode(bytecode)
await this.vm.continue()
if (locals) this.vm.popScope()
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> {
const code = readFileSync(path, 'utf-8') const code = readFileSync(path, 'utf-8')
return await runCode(code, globals) return await runCode(code, globals)
} }
export async function runCode(code: string, globals?: Record<string, any>): Promise<any> { export async function runCode(code: string, globals?: Record<string, any>): Promise<any> {
return await runBytecode(compileCode(code, globals), globals) return await runBytecode(compileCode(code, globals), globals)
} }
export async function runBytecode(bytecode: Bytecode, globals?: Record<string, any>): Promise<any> { export async function runBytecode(bytecode: Bytecode, globals?: Record<string, any>): Promise<any> {
const vm = new VM(bytecode, Object.assign({}, prelude, globals)) const vm = new VM(bytecode, Object.assign({}, prelude, globals))
await vm.run() await vm.run()
return vm.stack.length ? fromValue(vm.stack[vm.stack.length - 1]!, vm) : null return vm.stack.length ? fromValue(vm.stack[vm.stack.length - 1]!, vm) : null
} }
export function compileFile(path: string, globals?: Record<string, any>): Bytecode { export function compileFile(path: string, globals?: Record<string, any>): Bytecode {
const code = readFileSync(path, 'utf-8') const code = readFileSync(path, 'utf-8')
return compileCode(code, globals) return compileCode(code, globals)
} }
export function compileCode(code: string, globals?: Record<string, any>): Bytecode { export function compileCode(code: string, globals?: Record<string, any>): Bytecode {
const globalNames = [...Object.keys(prelude), ...(globals ? Object.keys(globals) : [])] const globalNames = [...Object.keys(prelude), ...(globals ? Object.keys(globals) : [])]
const compiler = new Compiler(code, globalNames) const compiler = new Compiler(code, globalNames)
return compiler.bytecode return compiler.bytecode
} }
export function parseFile(path: string, globals?: Record<string, any>): Tree { export function parseFile(path: string, globals?: Record<string, any>): Tree {
const code = readFileSync(path, 'utf-8') const code = readFileSync(path, 'utf-8')
return parseCode(code, globals) return parseCode(code, globals)
} }
export function parseCode(code: string, globals?: Record<string, any>): Tree { export function parseCode(code: string, globals?: Record<string, any>): Tree {
const oldGlobals = [...parserGlobals] const oldGlobals = [...parserGlobals]
const globalNames = [...Object.keys(prelude), ...(globals ? Object.keys(globals) : [])] const globalNames = [...Object.keys(prelude), ...(globals ? Object.keys(globals) : [])]
setParserGlobals(globalNames) setParserGlobals(globalNames)
const result = parse(code) const result = parse(code)
setParserGlobals(oldGlobals) setParserGlobals(oldGlobals)
return new Tree(result) return new Tree(result)
} }

View File

@ -44,8 +44,7 @@ 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))) while (char && isIdentChar(char.charCodeAt(0))) char = value[++pos]
char = value[++pos]
const input = value.slice(start + 1, pos) // skip '$' const input = value.slice(start + 1, pos) // skip '$'
tokens.push([input, parse(input)]) tokens.push([input, parse(input)])

View File

@ -3,18 +3,15 @@ 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'
@ -32,7 +29,6 @@ export type NodeType =
| 'Array' | 'Array'
| 'Dict' | 'Dict'
| 'Comment' | 'Comment'
| 'BinOp' | 'BinOp'
| 'ConditionalOp' | 'ConditionalOp'
| 'ParenExpr' | 'ParenExpr'
@ -40,7 +36,6 @@ export type NodeType =
| 'CompoundAssign' | 'CompoundAssign'
| 'DotGet' | 'DotGet'
| 'PipeExpr' | 'PipeExpr'
| 'IfExpr' | 'IfExpr'
| 'ElseIfExpr' | 'ElseIfExpr'
| 'ElseExpr' | 'ElseExpr'
@ -49,14 +44,12 @@ 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'
@ -67,13 +60,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',
@ -114,7 +107,7 @@ export const operators: Record<string, any> = {
} }
export class Tree { export class Tree {
constructor(public topNode: SyntaxNode) { } constructor(public topNode: SyntaxNode) {}
get length(): number { get length(): number {
return this.topNode.to return this.topNode.to
@ -158,12 +151,17 @@ 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(): { type: NodeType, name: NodeType, isError: boolean, is: (other: NodeType) => boolean } { get type(): {
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,
} }
} }
@ -211,7 +209,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
} }
@ -224,8 +222,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,
@ -248,9 +246,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,
@ -261,10 +259,6 @@ export const precedence: Record<string, number> = {
'**': 60, '**': 60,
} }
export const conditionals = new Set([ export const conditionals = new Set(['==', '!=', '<', '>', '<=', '>=', '??', 'and', 'or'])
'==', '!=', '<', '>', '<=', '>=', '??', 'and', 'or'
])
export const compounds = [ export const compounds = ['??=', '+=', '-=', '*=', '/=', '%=']
'??=', '+=', '-=', '*=', '/=', '%='
]

View File

@ -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,14 +78,11 @@ 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)) if (this.is($T.Comment)) return this.comment()
return this.comment()
while (this.is($T.Newline) || this.is($T.Semicolon)) while (this.is($T.Newline) || this.is($T.Semicolon)) this.next()
this.next()
if (this.isEOF() || this.isExprEndKeyword()) if (this.isEOF() || this.isExprEndKeyword()) return null
return null
return this.expression() return this.expression()
} }
@ -99,51 +96,38 @@ export class Parser {
let expr let expr
// x = value // x = value
if (this.is($T.Identifier) && ( if (
this.nextIs($T.Operator, '=') || compounds.some(x => this.nextIs($T.Operator, x)) this.is($T.Identifier) &&
)) (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)) else if (this.is($T.Keyword)) expr = this.keywords()
expr = this.keywords()
// dotget // dotget
else if (this.nextIs($T.Operator, '.')) else if (this.nextIs($T.Operator, '.')) expr = this.dotGetFunctionCall()
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()) else if (this.is($T.Identifier) && this.nextIsExprEnd()) expr = this.functionCallOrIdentifier()
expr = this.functionCallOrIdentifier()
// everything else // everything else
else else expr = this.exprWithPrecedence()
expr = this.exprWithPrecedence()
// check for destructuring // check for destructuring
if (expr.type.is('Array') && this.is($T.Operator, '=')) if (expr.type.is('Array') && this.is($T.Operator, '=')) return this.destructure(expr)
return this.destructure(expr)
// check for parens function call // check for parens function call
// ex: (ref my-func) my-arg // ex: (ref my-func) my-arg
if (expr.type.is('ParenExpr') && !this.isExprEnd()) if (expr.type.is('ParenExpr') && !this.isExprEnd()) expr = this.functionCall(expr)
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()) if (allowPipe && this.isPipe()) return this.pipe(expr)
return this.pipe(expr)
// regular // regular
else else return expr
return expr
} }
// piping | stuff | is | cool // piping | stuff | is | cool
@ -207,26 +191,19 @@ export class Parser {
// if, while, do, etc // if, while, do, etc
keywords(): SyntaxNode { keywords(): SyntaxNode {
if (this.is($T.Keyword, 'if')) if (this.is($T.Keyword, 'if')) return this.if()
return this.if()
if (this.is($T.Keyword, 'while')) if (this.is($T.Keyword, 'while')) return this.while()
return this.while()
if (this.is($T.Keyword, 'do')) if (this.is($T.Keyword, 'do')) return this.do()
return this.do()
if (this.is($T.Keyword, 'try')) if (this.is($T.Keyword, 'try')) return this.try()
return this.try()
if (this.is($T.Keyword, 'throw')) if (this.is($T.Keyword, 'throw')) return this.throw()
return this.throw()
if (this.is($T.Keyword, 'not')) if (this.is($T.Keyword, 'not')) return this.not()
return this.not()
if (this.is($T.Keyword, 'import')) if (this.is($T.Keyword, 'import')) return this.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
} }
@ -238,15 +215,12 @@ 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)) if (this.is($T.OpenParen)) return this.parens()
return this.parens()
if (this.is($T.OpenBracket)) if (this.is($T.OpenBracket)) return this.arrayOrDict()
return this.arrayOrDict()
// dotget // dotget
if (this.nextIs($T.Operator, '.')) if (this.nextIs($T.Operator, '.')) return this.dotGet()
return this.dotGet()
return this.atom() return this.atom()
} }
@ -324,8 +298,7 @@ export class Parser {
} }
// probably an array // probably an array
if (curr.type !== $T.Comment && curr.type !== $T.Semicolon && curr.type !== $T.Newline) if (curr.type !== $T.Comment && curr.type !== $T.Semicolon && curr.type !== $T.Newline) break
break
curr = this.peek(peek++) curr = this.peek(peek++)
} }
@ -343,7 +316,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)
@ -360,8 +333,7 @@ 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)) if (this.is($T.String)) return this.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())
@ -402,8 +374,7 @@ export class Parser {
const keyword = this.keyword('catch') const keyword = this.keyword('catch')
let catchVar let catchVar
if (this.is($T.Identifier)) if (this.is($T.Identifier)) catchVar = this.identifier()
catchVar = this.identifier()
const block = this.block() const block = this.block()
@ -507,12 +478,14 @@ export class Parser {
this.scope.add(varName) this.scope.add(varName)
let arg let arg
if (this.is($T.Identifier)) if (this.is($T.Identifier)) arg = this.identifier()
arg = this.identifier() else if (this.is($T.NamedArgPrefix)) arg = this.namedParam()
else if (this.is($T.NamedArgPrefix))
arg = this.namedParam()
else else
throw new CompilerError(`Expected Identifier or NamedArgPrefix, got ${TokenType[this.current().type]}`, this.current().from, this.current().to) throw new CompilerError(
`Expected Identifier or NamedArgPrefix, got ${TokenType[this.current().type]}`,
this.current().from,
this.current().to,
)
params.push(arg) params.push(arg)
} }
@ -520,11 +493,9 @@ 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')) if (this.is($T.Keyword, 'catch')) catchNode = this.catch()
catchNode = this.catch()
if (this.is($T.Keyword, 'finally')) if (this.is($T.Keyword, 'finally')) finalNode = this.finally()
finalNode = this.finally()
const end = this.keyword('end') const end = this.keyword('end')
@ -536,11 +507,7 @@ export class Parser {
node.add(doNode) node.add(doNode)
const paramsNode = new SyntaxNode( const paramsNode = new SyntaxNode('Params', params[0]?.from ?? 0, params.at(-1)?.to ?? 0)
'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)
@ -561,8 +528,7 @@ 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)) if (!this.scope.has(ident)) return this.word(left)
return this.word(left)
if (left.type.is('Identifier')) left.type = 'IdentifierBeforeDot' if (left.type.is('Identifier')) left.type = 'IdentifierBeforeDot'
@ -602,16 +568,13 @@ 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, '|')) if (this.is($T.Operator) && !this.is($T.Operator, '|')) return dotGet
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()) if (this.isExprEnd()) return this.functionCallOrIdentifier(dotGet)
return this.functionCallOrIdentifier(dotGet) else return this.functionCall(dotGet)
else
return this.functionCall(dotGet)
} }
// can be used in functions or try block // can be used in functions or try block
@ -763,7 +726,11 @@ 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(`Default value must be null, boolean, number, or string, got ${val.type.name}`, val.from, val.to) throw new CompilerError(
`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)
@ -781,7 +748,8 @@ 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) throw new CompilerError(`Operator not registered: ${token.value!}`, token.from, token.to) if (!name)
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)
} }
@ -828,11 +796,9 @@ 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')) if (this.is($T.Keyword, 'catch')) catchNode = this.catch()
catchNode = this.catch()
if (this.is($T.Keyword, 'finally')) if (this.is($T.Keyword, 'finally')) finalNode = this.finally()
finalNode = this.finally()
const end = this.keyword('end') const end = this.keyword('end')
@ -842,11 +808,9 @@ 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) if (catchNode) node.push(catchNode)
node.push(catchNode)
if (finalNode) if (finalNode) node.push(finalNode)
node.push(finalNode)
return node.push(end) return node.push(end)
} }
@ -868,8 +832,7 @@ 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)) if (this.isAny($T.Word, $T.Identifier, $T.Number)) parts.push(this.next())
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)
@ -892,8 +855,7 @@ 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) while (peek && peek.type === $T.Newline) peek = this.peek(++offset)
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
@ -914,7 +876,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 {
@ -925,43 +887,58 @@ 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 this.isAny($T.Colon, $T.Semicolon, $T.Newline, $T.CloseParen, $T.CloseBracket) || return (
this.isAny($T.Colon, $T.Semicolon, $T.Newline, $T.CloseParen, $T.CloseBracket) ||
this.is($T.Operator, '|') || this.is($T.Operator, '|') ||
this.isExprEndKeyword() || !this.current() this.isExprEndKeyword() ||
!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, '|')) if (this.nextIs($T.Operator, '|')) return true
return true
return this.nextIsAny($T.Colon, $T.Semicolon, $T.Newline, $T.CloseBracket, $T.CloseParen) || return (
this.nextIs($T.Keyword, 'end') || this.nextIs($T.Keyword, 'else') || this.nextIsAny($T.Colon, $T.Semicolon, $T.Newline, $T.CloseBracket, $T.CloseParen) ||
this.nextIs($T.Keyword, 'catch') || this.nextIs($T.Keyword, 'finally') || this.nextIs($T.Keyword, 'end') ||
this.nextIs($T.Keyword, 'else') ||
this.nextIs($T.Keyword, 'catch') ||
this.nextIs($T.Keyword, 'finally') ||
!this.peek() !this.peek()
)
} }
isExprEndKeyword(): boolean { isExprEndKeyword(): boolean {
return this.is($T.Keyword, 'end') || this.is($T.Keyword, 'else') || return (
this.is($T.Keyword, 'catch') || this.is($T.Keyword, 'finally') this.is($T.Keyword, 'end') ||
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 this.is($T.Operator, '|') || return (
(canLookPastNewlines && this.peekPastNewlines($T.Operator, '|')) this.is($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(`Expected ${TokenType[type]}${value ? ` "${value}"` : ''}, got ${TokenType[token?.type || 0]}${token?.value ? ` "${token.value}"` : ''} at position ${this.pos}`, token.from, token.to) 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,
)
} }
return this.next() return this.next()
} }
@ -981,7 +958,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

View File

@ -39,11 +39,18 @@ 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 = (stringNode: SyntaxNode, input: string, from: number, to: number, parser: any) => { const parseSingleQuoteString = (
stringNode: SyntaxNode,
input: string,
from: number,
to: number,
parser: any,
) => {
let pos = from + 1 // Skip opening ' let pos = from + 1 // Skip opening '
let fragmentStart = pos let fragmentStart = pos
while (pos < to - 1) { // -1 to skip closing ' while (pos < to - 1) {
// -1 to skip closing '
const char = input[pos] const char = input[pos]
// Escape sequence // Escape sequence
@ -115,7 +122,13 @@ const parseSingleQuoteString = (stringNode: SyntaxNode, input: string, from: num
* 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 = (stringNode: SyntaxNode, input: string, from: number, to: number, parser: any) => { const parseCurlyString = (
stringNode: SyntaxNode,
input: string,
from: number,
to: number,
parser: any,
) => {
let pos = from + 1 // Skip opening { let pos = from + 1 // Skip opening {
let fragmentStart = from // Include the opening { in the fragment let fragmentStart = from // Include the opening { in the fragment
let depth = 1 let depth = 1
@ -188,7 +201,11 @@ const parseCurlyString = (stringNode: SyntaxNode, input: string, from: number, t
* 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 = (input: string, pos: number, parser: any): { node: SyntaxNode, endPos: number } => { const parseInterpolationExpr = (
input: string,
pos: number,
parser: any,
): { node: SyntaxNode; endPos: number } => {
// Find matching closing paren // Find matching closing paren
let depth = 1 let depth = 1
let start = pos let start = pos

View File

@ -195,7 +195,6 @@ 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
@ -286,7 +285,6 @@ 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
@ -344,7 +342,6 @@ 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:

View File

@ -14,8 +14,7 @@ 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', () => {
@ -33,8 +32,7 @@ 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', () => {
@ -54,11 +52,9 @@ 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
@ -81,8 +77,7 @@ describe('single line function blocks', () => {
Identifier bye Identifier bye
PositionalArg PositionalArg
Identifier bye Identifier bye
keyword end` keyword end`)
)
}) })
}) })
@ -104,8 +99,7 @@ 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', () => {
@ -126,8 +120,7 @@ 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', () => {
@ -153,11 +146,9 @@ 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 = [=]
@ -184,8 +175,7 @@ end`).toMatchTree(`
Identifier bye Identifier bye
PositionalArg PositionalArg
Identifier bye Identifier bye
keyword end` keyword end`)
)
}) })
}) })
@ -262,8 +252,7 @@ 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`) end`).toMatchTree(`
.toMatchTree(`
FunctionCallWithBlock FunctionCallWithBlock
FunctionCallOrIdentifier FunctionCallOrIdentifier
Identifier p Identifier p

View File

@ -92,7 +92,6 @@ 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

View File

@ -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"`)

View File

@ -15,10 +15,7 @@ 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( expect(`100.`).toMatchTokens({ type: 'Number', value: '100' }, { type: 'Operator', value: '.' })
{ type: 'Number', value: '100' },
{ type: 'Operator', value: '.' },
)
}) })
test('simple numbers', () => { test('simple numbers', () => {
@ -130,10 +127,7 @@ 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( expect('EXIT:').toMatchTokens({ type: 'Word', value: 'EXIT' }, { type: 'Colon' })
{ 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' },
@ -214,18 +208,24 @@ describe('curly strings', () => {
expect(`{ expect(`{
one one
two two
three }`).toMatchToken('String', `{ three }`).toMatchToken(
'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('String', `{ { one } { three } }`).toMatchToken(
'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,7 +506,6 @@ 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' },
@ -552,25 +551,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' },
) )
}) })
}) })
@ -649,11 +648,7 @@ describe('empty and whitespace input', () => {
}) })
test('only newlines', () => { test('only newlines', () => {
expect('\n\n\n').toMatchTokens( expect('\n\n\n').toMatchTokens({ type: 'Newline' }, { type: 'Newline' }, { type: 'Newline' })
{ type: 'Newline' },
{ type: 'Newline' },
{ type: 'Newline' },
)
}) })
}) })
@ -665,14 +660,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' },

View File

@ -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,10 +36,16 @@ export enum TokenType {
const valueTokens = new Set([ const valueTokens = new Set([
TokenType.Comment, TokenType.Comment,
TokenType.Keyword, TokenType.Operator, TokenType.Keyword,
TokenType.Identifier, TokenType.Word, TokenType.NamedArgPrefix, TokenType.Operator,
TokenType.Boolean, TokenType.Number, TokenType.String, TokenType.Regex, TokenType.Identifier,
TokenType.Underscore TokenType.Word,
TokenType.NamedArgPrefix,
TokenType.Boolean,
TokenType.Number,
TokenType.String,
TokenType.Regex,
TokenType.Underscore,
]) ])
const operators = new Set([ const operators = new Set([
@ -109,7 +115,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 {
@ -155,11 +161,17 @@ 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(Object.assign({}, { this.tokens.push(
type, Object.assign(
from, {},
to, {
}, valueTokens.has(type) ? { value: this.input.slice(from, to) } : {})) type,
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)
@ -238,8 +250,7 @@ export class Scanner {
} }
if (char === c`\n`) { if (char === c`\n`) {
if (this.inParen === 0 && this.inBracket === 0) if (this.inParen === 0 && this.inBracket === 0) this.pushChar(TokenType.Newline)
this.pushChar(TokenType.Newline)
this.next() this.next()
continue continue
} }
@ -266,16 +277,20 @@ export class Scanner {
switch (this.char) { switch (this.char) {
case c`(`: case c`(`:
this.inParen++ this.inParen++
this.pushChar(TokenType.OpenParen); break this.pushChar(TokenType.OpenParen)
break
case c`)`: case c`)`:
this.inParen-- this.inParen--
this.pushChar(TokenType.CloseParen); break this.pushChar(TokenType.CloseParen)
break
case c`[`: case c`[`:
this.inBracket++ this.inBracket++
this.pushChar(TokenType.OpenBracket); break this.pushChar(TokenType.OpenBracket)
break
case c`]`: case c`]`:
this.inBracket-- this.inBracket--
this.pushChar(TokenType.CloseBracket); break this.pushChar(TokenType.CloseBracket)
break
} }
this.next() this.next()
} }
@ -339,29 +354,14 @@ 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 === '_') if (word === '_') this.push(TokenType.Underscore)
this.push(TokenType.Underscore) else if (word === 'null') this.push(TokenType.Null)
else if (word === 'true' || word === 'false') this.push(TokenType.Boolean)
else if (word === 'null') else if (isKeyword(word)) this.push(TokenType.Keyword)
this.push(TokenType.Null) else if (isOperator(word)) this.push(TokenType.Operator)
else if (isIdentifer(word)) this.push(TokenType.Identifier)
else if (word === 'true' || word === 'false') else if (word.endsWith('=')) this.push(TokenType.NamedArgPrefix)
this.push(TokenType.Boolean) else this.push(TokenType.Word)
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,8 +394,7 @@ export class Scanner {
this.next() // skip / this.next() // skip /
// read regex flags // read regex flags
while (this.char > 0 && isIdentStart(this.char)) while (this.char > 0 && isIdentStart(this.char)) this.next()
this.next()
// validate regex // validate regex
const to = this.pos - getCharSize(this.char) const to = this.pos - getCharSize(this.char)
@ -422,30 +421,29 @@ export class Scanner {
} }
canBeDotGet(lastToken?: Token): boolean { canBeDotGet(lastToken?: Token): boolean {
return !this.prevIsWhitespace && !!lastToken && return (
!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)) if (/^[+-]?\d+(_?\d+)*(\.(\d+(_?\d+)*))?$/.test(word)) return true
return true
// binary // binary
if (/^[+-]?0b[01]+(_?[01]+)*(\.[01](_?[01]*))?$/.test(word)) if (/^[+-]?0b[01]+(_?[01]+)*(\.[01](_?[01]*))?$/.test(word)) return true
return true
// octal // octal
if (/^[+-]?0o[0-7]+(_?[0-7]+)*(\.[0-7](_?[0-7]*))?$/.test(word)) if (/^[+-]?0o[0-7]+(_?[0-7]+)*(\.[0-7](_?[0-7]*))?$/.test(word)) return true
return true
// hex // hex
if (/^[+-]?0x[0-9a-f]+([0-9a-f]_?[0-9a-f]+)*(\.([0-9a-f]_?[0-9a-f]*))?$/i.test(word)) if (/^[+-]?0x[0-9a-f]+([0-9a-f]_?[0-9a-f]+)*(\.([0-9a-f]_?[0-9a-f]*))?$/i.test(word)) return true
return true
return false return false
} }
@ -461,14 +459,14 @@ const isIdentifer = (s: string): boolean => {
chars.push(out) chars.push(out)
} }
if (chars.length === 1) if (chars.length === 1) return isIdentStart(chars[0]!)
return isIdentStart(chars[0]!) else if (chars.length === 2) return isIdentStart(chars[0]!) && isIdentEnd(chars[1]!)
else if (chars.length === 2)
return isIdentStart(chars[0]!) && isIdentEnd(chars[1]!)
else else
return isIdentStart(chars[0]!) && return (
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 => {
@ -498,9 +496,14 @@ const isDigit = (ch: number): boolean => {
} }
const isWhitespace = (ch: number): boolean => { const isWhitespace = (ch: number): boolean => {
return ch === 32 /* space */ || ch === 9 /* tab */ || return (
ch === 13 /* \r */ || ch === 10 /* \n */ || ch === 32 /* space */ ||
ch === -1 || ch === 0 /* EOF */ ch === 9 /* tab */ ||
ch === 13 /* \r */ ||
ch === 10 /* \n */ ||
ch === -1 ||
ch === 0
) /* EOF */
} }
const isWordChar = (ch: number): boolean => { const isWordChar = (ch: number): boolean => {
@ -527,8 +530,7 @@ 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) => const getCharSize = (ch: number) => (ch > 0xffff ? 2 : 1) // emoji takes 2 UTF-16 code units
(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

View File

@ -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(),
} }

View File

@ -3,9 +3,11 @@ 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>) => Object.entries(dict).map(([k, v]) => ({ key: k, value: v })), entries: (dict: Record<string, any>) =>
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) => dict[key] ?? defaultValue, get: (dict: Record<string, any>, key: string, defaultValue: any = null) =>
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)
@ -30,6 +32,6 @@ export const dict = {
'from-entries': (entries: [string, any][]) => Object.fromEntries(entries), 'from-entries': (entries: [string, any][]) => Object.fromEntries(entries),
} }
// 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!
; (dict.set as any).raw = true ;(dict.set as any).raw = true

View File

@ -1,23 +1,33 @@
import { join, resolve, basename, dirname, extname } from 'path' import { join, resolve, basename, dirname, extname } from 'path'
import { import {
readdirSync, mkdirSync, rmdirSync, readdirSync,
readFileSync, writeFileSync, appendFileSync, mkdirSync,
rmSync, copyFileSync, rmdirSync,
statSync, lstatSync, chmodSync, symlinkSync, readlinkSync, readFileSync,
watch writeFileSync,
} from "fs" appendFileSync,
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) => rmdirSync(path === '/' || path === '' ? '/tmp/*' : path, { recursive: true }), rmdir: (path: string) =>
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),
// Reading // Reading
read: (path: string) => readFileSync(path, 'utf-8'), read: (path: string) => readFileSync(path, 'utf-8'),
cat: (path: string) => { }, // added below cat: (path: string) => {}, // added below
'read-bytes': (path: string) => [...readFileSync(path)], 'read-bytes': (path: string) => [...readFileSync(path)],
// Writing // Writing
@ -26,13 +36,13 @@ export const fs = {
// File operations // File operations
delete: (path: string) => rmSync(path), delete: (path: string) => rmSync(path),
rm: (path: string) => { }, // added below rm: (path: string) => {}, // added below
copy: (from: string, to: string) => copyFileSync(from, to), copy: (from: string, to: string) => copyFileSync(from, to),
move: (from: string, to: string) => { move: (from: string, to: string) => {
fs.copy(from, to) fs.copy(from, to)
fs.rm(from) fs.rm(from)
}, },
mv: (from: string, to: string) => { }, // added below mv: (from: string, to: string) => {}, // added below
// Path operations // Path operations
basename: (path: string) => basename(path), basename: (path: string) => basename(path),
@ -58,39 +68,50 @@ 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 { return statSync(path).isFile() } try {
catch { return false } return statSync(path).isFile()
} catch {
return false
}
}, },
'dir?': (path: string) => { 'dir?': (path: string) => {
try { return statSync(path).isDirectory() } try {
catch { return false } return statSync(path).isDirectory()
} catch {
return false
}
}, },
'symlink?': (path: string) => { 'symlink?': (path: string) => {
try { return lstatSync(path).isSymbolicLink() } try {
catch { return false } return lstatSync(path).isSymbolicLink()
} 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 { return statSync(path).size } try {
catch { return 0 } return statSync(path).size
} catch {
return 0
}
}, },
// Permissions // Permissions
@ -114,15 +135,12 @@ 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).mv = fs.move
; (fs as any).cat = fs.read ;(fs as any).cp = fs.copy
; (fs as any).mv = fs.move ;(fs as any).rm = fs.delete
; (fs as any).cp = fs.copy
; (fs as any).rm = fs.delete

View File

@ -2,8 +2,12 @@
import { join, resolve } from 'path' import { join, resolve } from 'path'
import { import {
type Value, type VM, toValue, type Value,
extractParamInfo, isWrapped, getOriginalFunction, type VM,
toValue,
extractParamInfo,
isWrapped,
getOriginalFunction,
} from 'reefvm' } from 'reefvm'
import { date } from './date' import { date } from './date'
@ -35,16 +39,18 @@ 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(...args.map(a => { console.log(
const v = toValue(a) ...args.map((a) => {
return ['array', 'dict'].includes(v.type) ? formatValue(v, true) : v.value const v = toValue(a)
})) return ['array', 'dict'].includes(v.type) ? formatValue(v, true) : v.value
}),
)
return toValue(null) return toValue(null)
}, },
@ -63,11 +69,10 @@ 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)
@ -100,9 +105,13 @@ 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 'array': return value.value.length case 'string':
case 'dict': return value.value.size case 'array':
default: throw new Error(`length: expected string, array, or dict, got ${value.type}`) return value.value.length
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) => {
@ -110,7 +119,9 @@ 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(`at: index ${idx} out of bounds for ${value.type} of length ${value.value.length}`) throw new Error(
`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') {
@ -137,7 +148,8 @@ 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 'array': case 'string':
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
@ -151,7 +163,6 @@ 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 = {
@ -164,7 +175,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 {
@ -178,15 +189,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()).reverse() const entries = Array.from(value.value.entries())
.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) if (entries.length === 0) return `${colors.blue}[=]${colors.reset}`
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': {
@ -206,5 +217,4 @@ 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)) for (const [key, value] of Object.entries(types)) globals[key] = value
globals[key] = value

View File

@ -2,6 +2,5 @@ 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

View File

@ -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,12 +143,11 @@ 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 ;(list.push as any).raw = true
; (list.push as any).raw = true ;(list.pop as any).raw = true
; (list.pop as any).raw = true ;(list.shift as any).raw = true
; (list.shift as any).raw = true ;(list.unshift as any).raw = true
; (list.unshift as any).raw = true ;(list.insert as any).raw = true
; (list.insert as any).raw = true

View File

@ -20,8 +20,7 @@ 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()) for (const [name, value] of this.scope.locals.entries()) module[name] = value
module[name] = value
this.scope = scope this.scope = scope
this.pc = pc this.pc = pc

View File

@ -17,17 +17,23 @@ 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) => String(str ?? '').replace(search, replacement), replace: (str: string, search: string, replacement: string) =>
'replace-all': (str: string, search: string, replacement: string) => String(str ?? '').replaceAll(search, replacement), String(str ?? '').replace(search, replacement),
slice: (str: string, start: number, end?: number | null) => String(str ?? '').slice(start, end ?? undefined), 'replace-all': (str: string, search: string, replacement: string) =>
substring: (str: string, start: number, end?: number | null) => String(str ?? '').substring(start, end ?? undefined), String(str ?? '').replaceAll(search, replacement),
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 = ' ') => String(str ?? '').padStart(length, pad), 'pad-start': (str: string, length: number, pad: string = ' ') =>
'pad-end': (str: string, length: number, pad: string = ' ') => String(str ?? '').padEnd(length, pad), String(str ?? '').padStart(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()

View File

@ -314,14 +314,16 @@ describe('fs - other', () => {
writeFileSync(file, 'initial') writeFileSync(file, 'initial')
let called = false let called = false
const watcher = fs.watch(file, () => { called = true }) const watcher = fs.watch(file, () => {
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?.()

View File

@ -5,14 +5,22 @@ 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({ a: true, b: false, c: "yeah" }) expect(`json.decode '{"a": true, "b": false, "c": "yeah"}'`).toEvaluateTo({
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({ a: true, b: false, c: "yeah" }) expect(`json.encode [a=true b=false c='yeah'] | json.decode`).toEvaluateTo({
a: true,
b: false,
c: 'yeah',
})
}) })
test('edge cases - empty structures', () => { test('edge cases - empty structures', () => {
@ -51,27 +59,31 @@ describe('json', () => {
}) })
test('nested structures - arrays', () => { test('nested structures - arrays', () => {
expect(`json.decode '[[1,2],[3,4],[5,6]]'`).toEvaluateTo([[1, 2], [3, 4], [5, 6]]) expect(`json.decode '[[1,2],[3,4],[5,6]]'`).toEvaluateTo([
[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 },
}) })
}) })

View File

@ -277,7 +277,10 @@ 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([[1, 3], [2, 4]]) await expect(`list.zip [1 2] [3 4]`).toEvaluateTo([
[1, 3],
[2, 4],
])
}) })
test('list.first returns first element', async () => { test('list.first returns first element', async () => {
@ -447,7 +450,10 @@ 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([[3, 4, 5], [1, 2]]) `).toEvaluateTo([
[3, 4, 5],
[1, 2],
])
}) })
test('list.compact removes null values', async () => { test('list.compact removes null values', async () => {

View File

@ -10,7 +10,6 @@ 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',

View File

@ -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 {

View File

@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />

View File

@ -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,7 +166,8 @@ expect.extend({
if (!token) { if (!token) {
return { return {
message: () => `Expected token to be ${expectedValue.replaceAll('\n', '\\n')}, got ${token}`, message: () =>
`Expected token to be ${expectedValue.replaceAll('\n', '\\n')}, got ${token}`,
pass: false, pass: false,
} }
} }
@ -174,13 +175,14 @@ 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: () => `Expected token to be ${expectedValue.replaceAll('\n', '\\n')}, but got ${token.value}`, message: () =>
pass: token.value === expectedValue `Expected token to be ${expectedValue.replaceAll('\n', '\\n')}, but got ${token.value}`,
pass: token.value === expectedValue,
} }
} catch (error) { } catch (error) {
return { return {
@ -189,11 +191,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 {
@ -207,7 +209,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 {
@ -215,18 +217,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,
} }
} }
@ -241,7 +243,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)
@ -257,7 +259,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)
@ -265,7 +267,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')
} }
} }

View File

@ -5,10 +5,7 @@
"name": "Run Extension", "name": "Run Extension",
"type": "extensionHost", "type": "extensionHost",
"request": "launch", "request": "launch",
"args": [ "args": ["--extensionDevelopmentPath=${workspaceFolder}", "--profile=Shrimp Dev"],
"--extensionDevelopmentPath=${workspaceFolder}",
"--profile=Shrimp Dev"
],
"outFiles": [ "outFiles": [
"${workspaceFolder}/client/dist/**/*.js", "${workspaceFolder}/client/dist/**/*.js",
"${workspaceFolder}/server/dist/**/*.js" "${workspaceFolder}/server/dist/**/*.js"

View File

@ -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}"`)
}) }),
) )
} }

View File

@ -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`,
) )

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -41,14 +41,14 @@ 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),
end: document.positionAt(node.to), end: document.positionAt(node.to),
}) })
const nameLength = text.length - 1 // Everything except the = const nameLength = text.length - 1 // Everything except the =
const start = document.positionAt(node.from) const start = document.positionAt(node.from)
// Emit token for the name part (e.g., "color") // Emit token for the name part (e.g., "color")
@ -57,16 +57,16 @@ 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
builder.push( builder.push(
start.line, start.line,
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

View File

@ -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
} }

View File

@ -1,4 +1,8 @@
import { SignatureHelp, SignatureInformation, ParameterInformation } from 'vscode-languageserver/node' import {
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'
@ -6,7 +10,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)
@ -100,6 +104,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 }
} }