From 0f7d3126a2c81d5dddeae3a41ee91a921b8467e4 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Sun, 19 Oct 2025 10:18:52 -0700 Subject: [PATCH] workin' --- src/parser/scopeTracker.ts | 85 +++++++++++++++++--------------------- src/parser/tokenizer.ts | 6 +-- 2 files changed, 40 insertions(+), 51 deletions(-) diff --git a/src/parser/scopeTracker.ts b/src/parser/scopeTracker.ts index 751a204..8854cad 100644 --- a/src/parser/scopeTracker.ts +++ b/src/parser/scopeTracker.ts @@ -5,21 +5,7 @@ export class Scope { constructor(public parent: Scope | null, public vars = new Set()) {} has(name: string): boolean { - return this.vars.has(name) ?? this.parent?.has(name) - } - - add(...names: string[]): Scope { - const newVars = new Set(this.vars) - names.forEach((name) => newVars.add(name)) - return new Scope(this.parent, newVars) - } - - push(): Scope { - return new Scope(this) - } - - pop(): Scope { - return this.parent ?? this + return this.vars.has(name) || (this.parent?.has(name) ?? false) } hash(): number { @@ -36,35 +22,27 @@ export class Scope { } return h } + + // Static methods that return new Scopes (immutable operations) + + static add(scope: Scope, ...names: string[]): Scope { + const newVars = new Set(scope.vars) + names.forEach((name) => newVars.add(name)) + return new Scope(scope.parent, newVars) + } + + push(): Scope { + return new Scope(this, new Set()) + } + + pop(): Scope { + return this.parent ?? this + } } -// Wrapper that adds temporary state for identifier capture -export class ScopeContext { +// Tracker context that combines Scope with temporary pending identifiers +class TrackerContext { constructor(public scope: Scope, public pendingIds: string[] = []) {} - - // Helper to append identifier to pending list - withPending(id: string): ScopeContext { - return new ScopeContext(this.scope, [...this.pendingIds, id]) - } - - // Helper to consume last pending identifier and add to scope - consumeLast(): ScopeContext { - const varName = this.pendingIds.at(-1) - if (!varName) return this - return new ScopeContext(this.scope.add(varName), this.pendingIds.slice(0, -1)) - } - - // Helper to consume all pending identifiers and add to new scope - consumeAll(): ScopeContext { - let newScope = this.scope.push() - newScope = this.pendingIds.length > 0 ? newScope.add(...this.pendingIds) : newScope - return new ScopeContext(newScope) - } - - // Helper to clear pending without adding to scope - clearPending(): ScopeContext { - return new ScopeContext(this.scope) - } } // Extract identifier text from input stream @@ -79,23 +57,36 @@ const readIdentifierText = (input: InputStream, start: number, end: number): str return text } -export const trackScope = new ContextTracker({ - start: new ScopeContext(new Scope(null, new Set())), +export const trackScope = new ContextTracker({ + start: new TrackerContext(new Scope(null, new Set())), shift(context, term, stack, input) { if (term !== terms.AssignableIdentifier) return context const text = readIdentifierText(input, input.pos, stack.pos) - return context.withPending(text) + return new TrackerContext(context.scope, [...context.pendingIds, text]) }, reduce(context, term) { - if (term === terms.Assign) return context.consumeLast() - if (term === terms.Params) return context.consumeAll() + // Add assignment variable to scope + if (term === terms.Assign) { + const varName = context.pendingIds.at(-1) + if (!varName) return context + return new TrackerContext(Scope.add(context.scope, varName), context.pendingIds.slice(0, -1)) + } + + // Push new scope and add all parameters + if (term === terms.Params) { + let newScope = context.scope.push() + if (context.pendingIds.length > 0) { + newScope = Scope.add(newScope, ...context.pendingIds) + } + return new TrackerContext(newScope, []) + } // Pop scope when exiting function if (term === terms.FunctionDef) { - return new ScopeContext(context.scope.pop()) + return new TrackerContext(context.scope.pop(), []) } return context diff --git a/src/parser/tokenizer.ts b/src/parser/tokenizer.ts index d59a0ca..767c2b6 100644 --- a/src/parser/tokenizer.ts +++ b/src/parser/tokenizer.ts @@ -1,6 +1,5 @@ import { ExternalTokenizer, InputStream, Stack } from '@lezer/lr' import { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot } from './shrimp.terms' -import type { ScopeContext } from './scopeTracker' // The only chars that can't be words are whitespace, apostrophes, closing parens, and EOF. @@ -138,12 +137,11 @@ const consumeRestOfWord = (input: InputStream, startPos: number, canBeWord: bool // Returns IdentifierBeforeDot token if in scope, null otherwise const checkForDotGet = (input: InputStream, stack: Stack, pos: number): number | null => { const identifierText = buildIdentifierText(input, pos) - const scopeContext = stack.context as ScopeContext | undefined - const scope = scopeContext?.scope + const context = stack.context as { scope: { has(name: string): boolean } } | undefined // 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 scope?.has(identifierText) ? IdentifierBeforeDot : null + return context?.scope.has(identifierText) ? IdentifierBeforeDot : null } // Decide between AssignableIdentifier and Identifier using grammar state + peek-ahead