From 219397339cf933e1a73e13c41c1bfaf6def06dcb Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 16 Oct 2025 17:47:01 -0700 Subject: [PATCH] feat(parser): add scope tracking context tracker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/parser/scopeTracker.ts | 120 +++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/parser/scopeTracker.ts diff --git a/src/parser/scopeTracker.ts b/src/parser/scopeTracker.ts new file mode 100644 index 0000000..fd44057 --- /dev/null +++ b/src/parser/scopeTracker.ts @@ -0,0 +1,120 @@ +import { ContextTracker } from '@lezer/lr' +import * as terms from './shrimp.terms' + +export class Scope { + constructor( + public parent: Scope | null, + public vars: Set + ) {} + + has(name: string): boolean { + return this.vars.has(name) || (this.parent?.has(name) ?? false) + } + + add(name: string): Scope { + const newVars = new Set(this.vars) + newVars.add(name) + return new Scope(this.parent, newVars) + } + + addAll(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, new Set()) + } + + pop(): Scope { + return this.parent ?? new Scope(null, new Set()) + } + + hash(): number { + let h = 0 + for (const name of this.vars) { + for (let i = 0; i < name.length; i++) { + h = (h << 5) - h + name.charCodeAt(i) + h |= 0 + } + } + if (this.parent) { + h = (h << 5) - h + this.parent.hash() + h |= 0 + } + return h + } +} + +// Module-level state for tracking identifiers +let pendingIdentifiers: string[] = [] +let isInParams = false + +// Term ID for 'fn' keyword - verified by parsing and inspecting the tree +const FN_KEYWORD = 32 + +export const trackScope = new ContextTracker({ + start: new Scope(null, new Set()), + + shift(context, term, stack, input) { + // Track fn keyword to enter param capture mode + if (term === FN_KEYWORD) { + isInParams = true + pendingIdentifiers = [] + return context + } + + // Capture identifiers + if (term === terms.Identifier) { + const text = input.read(input.pos, stack.pos) + + // Capture ALL identifiers when in params + if (isInParams) { + pendingIdentifiers.push(text) + } + // Capture FIRST identifier for assignments + else if (pendingIdentifiers.length === 0) { + pendingIdentifiers.push(text) + } + } + + return context + }, + + reduce(context, term, stack, input) { + // Add assignment variable to scope + if (term === terms.Assign && pendingIdentifiers.length > 0) { + const newContext = context.add(pendingIdentifiers[0]) + pendingIdentifiers = [] + return newContext + } + + // Push new scope and add parameters + if (term === terms.Params) { + const newScope = context.push() + if (pendingIdentifiers.length > 0) { + const newContext = newScope.addAll(pendingIdentifiers) + pendingIdentifiers = [] + isInParams = false + return newContext + } + isInParams = false + return newScope + } + + // Pop scope when exiting function + if (term === terms.FunctionDef) { + return context.pop() + } + + // Clear stale identifiers after non-assignment statements + if (term === terms.PropertyAccess || term === terms.FunctionCallOrIdentifier) { + pendingIdentifiers = [] + } + + return context + }, + + hash: (context) => context.hash() +})