refactor(parser): move pendingIdentifiers and isInParams into Scope class

Replace module-level mutable state with immutable state managed within the Scope
class itself. This eliminates state leakage between parser invocations and makes
the code more functional and predictable.

Changes:
- Add pendingIdentifiers and isInParams as Scope constructor parameters
- Add helper methods: withPendingIdentifiers(), withIsInParams(), clearPending()
- Update hash() to include new state fields
- Convert all mutable state operations to return new Scope instances
- Remove module-level variables entirely

Benefits:
- No state leakage between tests or parser invocations
- Easier to reason about - state is explicit in the context
- More functional programming style with immutable updates
- Eliminates entire class of bugs related to stale module state

🤖 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-17 10:46:52 -07:00
parent a33f6cd191
commit a652f83b63

View File

@ -2,7 +2,12 @@ import { ContextTracker } from '@lezer/lr'
import * as terms from './shrimp.terms' import * as terms from './shrimp.terms'
export class Scope { export class Scope {
constructor(public parent: Scope | null, public vars: Set<string>) {} constructor(
public parent: Scope | null,
public vars: Set<string>,
public pendingIdentifiers: string[] = [],
public isInParams: boolean = false
) {}
has(name: string): boolean { has(name: string): boolean {
return this.vars.has(name) ?? this.parent?.has(name) return this.vars.has(name) ?? this.parent?.has(name)
@ -11,15 +16,27 @@ export class Scope {
add(...names: string[]): Scope { add(...names: string[]): Scope {
const newVars = new Set(this.vars) const newVars = new Set(this.vars)
names.forEach((name) => newVars.add(name)) names.forEach((name) => newVars.add(name))
return new Scope(this.parent, newVars) return new Scope(this.parent, newVars, [], this.isInParams)
} }
push(): Scope { push(): Scope {
return new Scope(this, new Set()) return new Scope(this, new Set(), [], false)
} }
pop(): Scope { pop(): Scope {
return this.parent ?? new Scope(null, new Set()) return this.parent ?? new Scope(null, new Set(), [], false)
}
withPendingIdentifiers(ids: string[]): Scope {
return new Scope(this.parent, this.vars, ids, this.isInParams)
}
withIsInParams(value: boolean): Scope {
return new Scope(this.parent, this.vars, this.pendingIdentifiers, value)
}
clearPending(): Scope {
return new Scope(this.parent, this.vars, [], this.isInParams)
} }
hash(): number { hash(): number {
@ -34,23 +51,21 @@ export class Scope {
h = (h << 5) - h + this.parent.hash() h = (h << 5) - h + this.parent.hash()
h |= 0 h |= 0
} }
// Include pendingIdentifiers and isInParams in hash
h = (h << 5) - h + this.pendingIdentifiers.length
h = (h << 5) - h + (this.isInParams ? 1 : 0)
h |= 0
return h return h
} }
} }
// Module-level state for tracking identifiers
let pendingIdentifiers: string[] = []
let isInParams = false
export const trackScope = new ContextTracker<Scope>({ export const trackScope = new ContextTracker<Scope>({
start: new Scope(null, new Set()), start: new Scope(null, new Set(), [], false),
shift(context, term, stack, input) { shift(context, term, stack, input) {
// Track fn keyword to enter param capture mode // Track fn keyword to enter param capture mode
if (term === terms.Fn) { if (term === terms.Fn) {
isInParams = true return context.withIsInParams(true).withPendingIdentifiers([])
pendingIdentifiers = []
return context
} }
// Capture identifiers // Capture identifiers
@ -66,14 +81,13 @@ export const trackScope = new ContextTracker<Scope>({
text += String.fromCharCode(ch) text += String.fromCharCode(ch)
} }
// Capture ALL identifiers when in params // Capture ALL identifiers when in params
if (isInParams) { if (context.isInParams) {
pendingIdentifiers.push(text) return context.withPendingIdentifiers([...context.pendingIdentifiers, text])
} }
// Capture FIRST identifier for assignments // Capture FIRST identifier for assignments
else if (pendingIdentifiers.length === 0) { else if (context.pendingIdentifiers.length === 0) {
pendingIdentifiers.push(text) return context.withPendingIdentifiers([text])
} }
} }
@ -82,23 +96,17 @@ export const trackScope = new ContextTracker<Scope>({
reduce(context, term, stack, input) { reduce(context, term, stack, input) {
// Add assignment variable to scope // Add assignment variable to scope
if (term === terms.Assign && pendingIdentifiers.length > 0) { if (term === terms.Assign && context.pendingIdentifiers.length > 0) {
const newContext = context.add(pendingIdentifiers[0]!) return context.add(context.pendingIdentifiers[0]!)
pendingIdentifiers = []
return newContext
} }
// Push new scope and add parameters // Push new scope and add parameters
if (term === terms.Params) { if (term === terms.Params) {
const newScope = context.push() const newScope = context.push()
if (pendingIdentifiers.length > 0) { if (context.pendingIdentifiers.length > 0) {
const newContext = newScope.add(...pendingIdentifiers) return newScope.add(...context.pendingIdentifiers).withIsInParams(false)
pendingIdentifiers = []
isInParams = false
return newContext
} }
isInParams = false return newScope.withIsInParams(false)
return newScope
} }
// Pop scope when exiting function // Pop scope when exiting function
@ -108,7 +116,7 @@ export const trackScope = new ContextTracker<Scope>({
// Clear stale identifiers after non-assignment statements // Clear stale identifiers after non-assignment statements
if (term === terms.DotGet || term === terms.FunctionCallOrIdentifier || term === terms.FunctionCall) { if (term === terms.DotGet || term === terms.FunctionCallOrIdentifier || term === terms.FunctionCall) {
pendingIdentifiers = [] return context.clearPending()
} }
return context return context