feat(parser): add scope tracking context tracker

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Corey Johnson 2025-10-16 17:47:01 -07:00
parent 863163d01e
commit 219397339c

120
src/parser/scopeTracker.ts Normal file
View File

@ -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<string>
) {}
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<Scope>({
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()
})