diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 4438020..f1c1e01 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -1,6 +1,7 @@ import { CompilerError } from '#compiler/compilerError.ts' import { parser } from '#parser/shrimp.ts' import * as terms from '#parser/shrimp.terms' +import { setGlobals } from '#parser/tokenizer' import type { SyntaxNode, Tree } from '@lezer/common' import { assert, errorMessage } from '#utils/utils' import { toBytecode, type Bytecode, type ProgramItem, bytecodeToString } from 'reefvm' @@ -53,8 +54,9 @@ export class Compiler { bytecode: Bytecode pipeCounter = 0 - constructor(public input: string) { + constructor(public input: string, globals?: string[]) { try { + if (globals) setGlobals(globals) const cst = parser.parse(input) const errors = checkTreeForErrors(cst) diff --git a/src/parser/tokenizer.ts b/src/parser/tokenizer.ts index cef4446..6b2e67a 100644 --- a/src/parser/tokenizer.ts +++ b/src/parser/tokenizer.ts @@ -6,6 +6,13 @@ export function specializeKeyword(ident: string) { return ident === 'do' ? Do : -1 } +// tell the dotGet searcher about builtin globals +export const globals: string[] = [] +export const setGlobals = (newGlobals: string[]) => { + globals.length = 0 + globals.push(...newGlobals) +} + // The only chars that can't be words are whitespace, apostrophes, closing parens, and EOF. export const tokenizer = new ExternalTokenizer( @@ -152,7 +159,7 @@ const checkForDotGet = (input: InputStream, stack: Stack, pos: number): number | // If identifier is in scope, this is property access (e.g., obj.prop) // If not in scope, it should be consumed as a Word (e.g., file.txt) - return context?.scope.has(identifierText) ? IdentifierBeforeDot : null + return context?.scope.has(identifierText) || globals.includes(identifierText) ? IdentifierBeforeDot : null } // Decide between AssignableIdentifier and Identifier using grammar state + peek-ahead diff --git a/src/prelude/index.ts b/src/prelude/index.ts index 7bb82f1..46f3dbf 100644 --- a/src/prelude/index.ts +++ b/src/prelude/index.ts @@ -49,6 +49,9 @@ export const globalFunctions = { 'to-upper': (str: string) => str.toUpperCase(), 'to-lower': (str: string) => str.toLowerCase(), trim: (str: string) => str.trim(), + str: { + trim: (str: string) => str.trim(), + }, // collections at: (collection: any, index: number | string) => collection[index], diff --git a/src/prelude/tests/prelude.test.ts b/src/prelude/tests/prelude.test.ts index 72d7c5c..f3d934c 100644 --- a/src/prelude/tests/prelude.test.ts +++ b/src/prelude/tests/prelude.test.ts @@ -13,8 +13,8 @@ describe('string operations', () => { }) test('trim removes whitespace', async () => { - await expect(`trim ' hello '`).toEvaluateTo('hello', globalFunctions) - await expect(`trim '\\n\\thello\\t\\n'`).toEvaluateTo('hello', globalFunctions) + await expect(`str.trim ' hello '`).toEvaluateTo('hello', globalFunctions) + await expect(`str.trim '\\n\\thello\\t\\n'`).toEvaluateTo('hello', globalFunctions) }) test('split divides string by separator', async () => { diff --git a/src/testSetup.ts b/src/testSetup.ts index c6d5551..3904828 100644 --- a/src/testSetup.ts +++ b/src/testSetup.ts @@ -1,9 +1,10 @@ import { expect } from 'bun:test' import { parser } from '#parser/shrimp' +import { setGlobals } from '#parser/tokenizer' import { $ } from 'bun' import { assert, errorMessage } from '#utils/utils' import { Compiler } from '#compiler/compiler' -import { run, VM, type TypeScriptFunction } from 'reefvm' +import { run, VM } from 'reefvm' import { treeToString, VMResultToValue } from '#utils/tree' const regenerateParser = async () => { @@ -30,7 +31,7 @@ await regenerateParser() // Type declaration for TypeScript declare module 'bun:test' { interface Matchers { - toMatchTree(expected: string): T + toMatchTree(expected: string, globals?: Record): T toMatchExpression(expected: string): T toFailParse(): T toEvaluateTo(expected: unknown, globals?: Record): Promise @@ -39,9 +40,10 @@ declare module 'bun:test' { } expect.extend({ - toMatchTree(received: unknown, expected: string) { + toMatchTree(received: unknown, expected: string, globals?: Record) { assert(typeof received === 'string', 'toMatchTree can only be used with string values') + if (globals) setGlobals(Object.keys(globals)) const tree = parser.parse(received) const actual = treeToString(tree, received) const normalizedExpected = trimWhitespace(expected) @@ -97,6 +99,7 @@ expect.extend({ assert(typeof received === 'string', 'toEvaluateTo can only be used with string values') try { + if (globals) setGlobals(Object.keys(globals)) const compiler = new Compiler(received) const result = await run(compiler.bytecode, globals) let value = VMResultToValue(result)