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