This commit is contained in:
Corey Johnson 2025-10-19 10:18:52 -07:00
parent 78ae96fc72
commit 0f7d3126a2
2 changed files with 40 additions and 51 deletions

View File

@ -5,21 +5,7 @@ export class Scope {
constructor(public parent: Scope | null, public vars = new Set<string>()) {} constructor(public parent: Scope | null, public vars = new Set<string>()) {}
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) ?? false)
}
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
} }
hash(): number { hash(): number {
@ -36,35 +22,27 @@ export class Scope {
} }
return h 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)
} }
// Wrapper that adds temporary state for identifier capture push(): Scope {
export class ScopeContext { return new Scope(this, new Set())
}
pop(): Scope {
return this.parent ?? this
}
}
// Tracker context that combines Scope with temporary pending identifiers
class TrackerContext {
constructor(public scope: Scope, public pendingIds: string[] = []) {} 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 // Extract identifier text from input stream
@ -79,23 +57,36 @@ const readIdentifierText = (input: InputStream, start: number, end: number): str
return text return text
} }
export const trackScope = new ContextTracker<ScopeContext>({ export const trackScope = new ContextTracker<TrackerContext>({
start: new ScopeContext(new Scope(null, new Set())), start: new TrackerContext(new Scope(null, new Set())),
shift(context, term, stack, input) { shift(context, term, stack, input) {
if (term !== terms.AssignableIdentifier) return context if (term !== terms.AssignableIdentifier) return context
const text = readIdentifierText(input, input.pos, stack.pos) const text = readIdentifierText(input, input.pos, stack.pos)
return context.withPending(text) return new TrackerContext(context.scope, [...context.pendingIds, text])
}, },
reduce(context, term) { reduce(context, term) {
if (term === terms.Assign) return context.consumeLast() // Add assignment variable to scope
if (term === terms.Params) return context.consumeAll() 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 // Pop scope when exiting function
if (term === terms.FunctionDef) { if (term === terms.FunctionDef) {
return new ScopeContext(context.scope.pop()) return new TrackerContext(context.scope.pop(), [])
} }
return context return context

View File

@ -1,6 +1,5 @@
import { ExternalTokenizer, InputStream, Stack } from '@lezer/lr' import { ExternalTokenizer, InputStream, Stack } from '@lezer/lr'
import { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot } from './shrimp.terms' 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. // 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 // Returns IdentifierBeforeDot token if in scope, null otherwise
const checkForDotGet = (input: InputStream, stack: Stack, pos: number): number | null => { const checkForDotGet = (input: InputStream, stack: Stack, pos: number): number | null => {
const identifierText = buildIdentifierText(input, pos) const identifierText = buildIdentifierText(input, pos)
const scopeContext = stack.context as ScopeContext | undefined const context = stack.context as { scope: { has(name: string): boolean } } | undefined
const scope = scopeContext?.scope
// If identifier is in scope, this is property access (e.g., obj.prop) // 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) // 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 // Decide between AssignableIdentifier and Identifier using grammar state + peek-ahead