import type { DeepReadonly } from 'vue' import type { Identifier, IdentifierOrOperatorIdentifier, IdentifierOrOperatorIdentifierToken, IdentifierToken, Module, NodeChild, Owned, RawNodeChild, SpanMap, SyncTokenId, TypeOrConstructorIdentifier, } from '.' import { MutableModule, ROOT_ID, Token, asOwned, escapeTextLiteral, isIdentifier, isToken, isTokenChild, isTokenId, newExternalId, parentId, } from '.' import { assert, assertDefined, assertEqual, bail } from '../util/assert' import type { Result } from '../util/data/result' import { Err, Ok } from '../util/data/result' import type { SourceRangeEdit } from '../util/data/text' import { allKeys } from '../util/types' import type { ExternalId, VisualizationMetadata } from '../yjsModel' import { visMetadataEquals } from '../yjsModel' import * as RawAst from './generated/ast' import { applyTextEditsToAst, parse, parseBlock, print, printAst, printBlock, printDocumented, syncToCode, } from './parse' declare const brandAstId: unique symbol export type AstId = string & { [brandAstId]: never } /** @internal */ export interface MetadataFields { externalId: ExternalId } export interface NodeMetadataFields { position?: { x: number; y: number } | undefined visualization?: VisualizationMetadata | undefined colorOverride?: string | undefined } const nodeMetadataKeys = allKeys({ position: null, visualization: null, colorOverride: null, }) export type NodeMetadata = FixedMapView export type MutableNodeMetadata = FixedMap export function asNodeMetadata(map: Map): NodeMetadata { return map as unknown as NodeMetadata } /** @internal */ interface RawAstFields { id: AstId type: string parent: AstId | undefined metadata: FixedMap } export interface AstFields extends RawAstFields, LegalFieldContent {} const astFieldKeys = allKeys({ id: null, type: null, parent: null, metadata: null, }) export abstract class Ast { readonly module: Module /** @internal */ readonly fields: FixedMapView get id(): AstId { return this.fields.get('id') } get externalId(): ExternalId { const id = this.fields.get('metadata').get('externalId') assert(id != null) return id } get nodeMetadata(): NodeMetadata { const metadata = this.fields.get('metadata') return metadata as FixedMapView } /** Returns a JSON-compatible object containing all metadata properties. */ serializeMetadata(): MetadataFields & NodeMetadataFields { return this.fields.get('metadata').toJSON() as any } typeName(): string { return this.fields.get('type') } /** * Return whether `this` and `other` are the same object, possibly in different modules. */ is(other: T): boolean { return this.id === other.id } innerExpression(): Ast { return this.wrappedExpression()?.innerExpression() ?? this } wrappedExpression(): Ast | undefined { return undefined } wrappingExpression(): Ast | undefined { const parent = this.parent() return parent?.wrappedExpression()?.is(this) ? parent : undefined } wrappingExpressionRoot(): Ast { return this.wrappingExpression()?.wrappingExpressionRoot() ?? this } documentingAncestor(): Documented | undefined { return this.wrappingExpression()?.documentingAncestor() } code(): string { return print(this).code } visitRecursive(visit: (node: Ast | Token) => void): void { visit(this) for (const child of this.children()) { if (isToken(child)) { visit(child) } else { child.visitRecursive(visit) } } } visitRecursiveAst(visit: (ast: Ast) => void | boolean): void { if (visit(this) === false) return for (const child of this.children()) { if (!isToken(child)) child.visitRecursiveAst(visit) } } printSubtree( info: SpanMap, offset: number, parentIndent: string | undefined, verbatim?: boolean, ): string { return printAst(this, info, offset, parentIndent, verbatim) } /** Returns child subtrees, without information about the whitespace between them. */ *children(): IterableIterator { for (const child of this.concreteChildren()) { if (isTokenId(child.node)) { yield this.module.getToken(child.node) } else { const node = this.module.get(child.node) if (node) yield node } } } get parentId(): AstId | undefined { const parentId = this.fields.get('parent') if (parentId !== ROOT_ID) return parentId } parent(): Ast | undefined { return this.module.get(this.parentId) } static parseBlock(source: string, inModule?: MutableModule) { return parseBlock(source, inModule) } static parse(source: string, module?: MutableModule) { return parse(source, module) } //////////////////// protected constructor(module: Module, fields: FixedMapView) { this.module = module this.fields = fields } /** @internal * Returns child subtrees, including information about the whitespace between them. */ abstract concreteChildren(verbatim?: boolean): IterableIterator } export interface MutableAst {} export abstract class MutableAst extends Ast { declare readonly module: MutableModule declare readonly fields: FixedMap setExternalId(id: ExternalId) { this.fields.get('metadata').set('externalId', id) } mutableNodeMetadata(): MutableNodeMetadata { const metadata = this.fields.get('metadata') return metadata as FixedMap } setNodeMetadata(nodeMeta: NodeMetadataFields) { const metadata = this.fields.get('metadata') as unknown as Map for (const [key, value] of Object.entries(nodeMeta)) { if (!nodeMetadataKeys.has(key)) continue if (value === undefined) { metadata.delete(key) } else { metadata.set(key, value) } } } /** Modify the parent of this node to refer to a new object instead. Return the object, which now has no parent. */ replace(replacement: Owned): Owned { const parentId = this.fields.get('parent') if (parentId) { const parent = this.module.get(parentId) parent.replaceChild(this.id, replacement) this.fields.set('parent', undefined) } return asOwned(this) } /** Change the value of the object referred to by the `target` ID. (The initial ID of `replacement` will be ignored.) * Returns the old value, with a new (unreferenced) ID. */ replaceValue(replacement: Owned): Owned { const replacement_ = this.module.copyIfForeign(replacement) const old = this.replace(replacement_) replacement_.fields.set('metadata', old.fields.get('metadata').clone()) old.setExternalId(newExternalId()) return old } replaceValueChecked(replacement: Owned): Owned { const parentId = this.fields.get('parent') assertDefined(parentId) return this.replaceValue(replacement) } /** Replace the parent of this object with a reference to a new placeholder object. * Returns the object, now parentless, and the placeholder. */ takeToReplace(): Removed { if (parentId(this)) { const placeholder = Wildcard.new(this.module) const node = this.replace(placeholder) return { node, placeholder } } else { return { node: asOwned(this), placeholder: undefined } } } /** Replace the parent of this object with a reference to a new placeholder object. * Returns the object, now parentless. */ take(): Owned { return this.replace(Wildcard.new(this.module)) } takeIfParented(): Owned { const parent = parentId(this) if (parent) { const parentAst = this.module.get(parent) const placeholder = Wildcard.new(this.module) parentAst.replaceChild(this.id, placeholder) this.fields.set('parent', undefined) } return asOwned(this) } /** Replace the value assigned to the given ID with a placeholder. * Returns the removed value, with a new unreferenced ID. **/ takeValue(): Removed { const placeholder = Wildcard.new(this.module) const node = this.replaceValue(placeholder) return { node, placeholder } } /** Take this node from the tree, and replace it with the result of applying the given function to it. * * Note that this is a modification of the *parent* node. Any `Ast` objects or `AstId`s that pointed to the old value * will still point to the old value. */ update(f: (x: Owned) => Owned): T { const taken = this.takeToReplace() assertDefined(taken.placeholder, 'To replace an `Ast`, it must have a parent.') const replacement = f(taken.node) taken.placeholder.replace(replacement) return replacement } /** Take this node from the tree, and replace it with the result of applying the given function to it; transfer the * metadata from this node to the replacement. * * Note that this is a modification of the *parent* node. Any `Ast` objects or `AstId`s that pointed to the old value * will still point to the old value. */ updateValue(f: (x: Owned) => Owned): T { const taken = this.takeValue() assertDefined(taken.placeholder, 'To replace an `Ast`, it must have a parent.') const replacement = f(taken.node) taken.placeholder.replaceValue(replacement) return replacement } mutableParent(): MutableAst | undefined { const parentId = this.fields.get('parent') if (parentId === 'ROOT_ID') return return this.module.get(parentId) } /** Modify this tree to represent the given code, while minimizing changes from the current set of `Ast`s. */ syncToCode(code: string, metadataSource?: Module) { syncToCode(this, code, metadataSource) } /** Update the AST according to changes to its corresponding source code. */ applyTextEdits(textEdits: SourceRangeEdit[], metadataSource?: Module) { applyTextEditsToAst(this, textEdits, metadataSource ?? this.module) } getOrInitDocumentation(): MutableDocumented { const existing = this.documentingAncestor() if (existing) return this.module.getVersion(existing) return this.module .getVersion(this.wrappingExpressionRoot()) .updateValue((ast) => Documented.new('', ast)) } /////////////////// /** @internal */ importReferences(module: Module) { if (module === this.module) return for (const child of this.concreteChildren()) { if (!isTokenId(child.node)) { const childInForeignModule = module.get(child.node) assert(childInForeignModule !== undefined) const importedChild = this.module.copy(childInForeignModule) importedChild.fields.set('parent', undefined) this.replaceChild(child.node, asOwned(importedChild)) } } } /** @internal */ replaceChild(target: AstId, replacement: Owned) { const replacementId = this.claimChild(replacement) const changes = rewriteRefs(this, (id) => (id === target ? replacementId : undefined)) assertEqual(changes, 1) } protected claimChild(child: Owned): AstId protected claimChild(child: Owned | undefined): AstId | undefined protected claimChild(child: Owned | undefined): AstId | undefined { return child ? claimChild(this.module, child, this.id) : undefined } } /** Values that may be found in fields of `Ast` subtypes. */ type FieldData = | NonArrayFieldData | NonArrayFieldData[] | (T['ast'] | T['token'])[] // Logically `FieldData[]` could be a type of `FieldData`, but the type needs to be non-recursive so that it can be // used with `DeepReadonly`. type NonArrayFieldData = T['ast'] | T['token'] | undefined | StructuralField /** Objects that do not directly contain `AstId`s or `SyncTokenId`s, but may have `NodeChild` fields. */ type StructuralField = | MultiSegmentAppSegment | Line | OpenCloseTokens | NameSpecification | TextElement | ArgumentDefinition | VectorElement /** Type whose fields are all suitable for storage as `Ast` fields. */ interface FieldObject { [field: string]: FieldData } /** Returns the fields of an `Ast` subtype that are not part of `AstFields`. */ function* fieldDataEntries(map: FixedMapView) { for (const entry of map.entries()) { // All fields that are not from `AstFields` are `FieldData`. if (!astFieldKeys.has(entry[0])) yield entry as [string, DeepReadonly] } } function idRewriter( f: (id: AstId) => AstId | undefined, ): (field: DeepReadonly) => FieldData | undefined { return (field: DeepReadonly) => { if (typeof field !== 'object') return if (!('node' in field)) return if (isTokenId(field.node)) return const newId = f(field.node) if (!newId) return return { whitespace: field.whitespace, node: newId } } } /** Apply the given function to each `AstId` in the fields of `ast`. For each value that it returns an output, that * output will be substituted for the input ID. */ export function rewriteRefs(ast: MutableAst, f: (id: AstId) => AstId | undefined) { let fieldsChanged = 0 for (const [key, value] of fieldDataEntries(ast.fields)) { const newValue = rewriteFieldRefs(value, idRewriter(f)) if (newValue !== undefined) { ast.fields.set(key as any, newValue) fieldsChanged += 1 } } return fieldsChanged } /** Copy all fields except the `Ast` base fields from `ast2` to `ast1`. A reference-rewriting function will be applied * to `AstId`s in copied fields; see {@link rewriteRefs}. */ export function syncFields(ast1: MutableAst, ast2: Ast, f: (id: AstId) => AstId | undefined) { for (const [key, value] of fieldDataEntries(ast2.fields)) { const newValue = mapRefs(value, idRewriter(f)) if (!fieldEqual(ast1.fields.get(key as any), newValue)) ast1.fields.set(key as any, newValue) } } export function syncNodeMetadata(target: MutableNodeMetadata, source: NodeMetadata) { const oldPos = target.get('position') const newPos = source.get('position') if (oldPos?.x !== newPos?.x || oldPos?.y !== newPos?.y) target.set('position', newPos) const newVis = source.get('visualization') if (!visMetadataEquals(target.get('visualization'), newVis)) target.set('visualization', newVis) } function rewriteFieldRefs( field: DeepReadonly>, f: (t: DeepReadonly>) => FieldData | undefined, ): FieldData { const newValue = f(field) if (newValue) return newValue if (typeof field !== 'object') return // `Array.isArray` doesn't work with `DeepReadonly`, but we just need a narrowing that distinguishes it from all // `StructuralField` types. if ('forEach' in field) { const newValues = new Map>() field.forEach((subfield, i) => { const newValue = rewriteFieldRefs(subfield, f) if (newValue !== undefined) newValues.set(i, newValue) }) if (newValues.size) return Array.from(field, (oldValue, i) => newValues.get(i) ?? oldValue) } else { const fieldObject = field satisfies DeepReadonly const newValues = new Map>() for (const [key, value] of Object.entries(fieldObject)) { const newValue = rewriteFieldRefs(value, f) if (newValue !== undefined) newValues.set(key, newValue) } if (newValues.size) return Object.fromEntries( Object.entries(fieldObject).map(([key, oldValue]) => [key, newValues.get(key) ?? oldValue]), ) } } type MapRef = (t: FieldData) => FieldData | undefined // This operation can transform any `FieldData` type parameterized by some `TreeRefs` into the same type parameterized // by another `TreeRefs`, but it is not possible to express that generalization to TypeScript as such. function mapRefs( field: ImportFields, f: MapRef, ): ImportFields function mapRefs( field: TextToken, f: MapRef, ): TextToken function mapRefs( field: TextElement, f: MapRef, ): TextElement function mapRefs( field: ArgumentDefinition, f: MapRef, ): ArgumentDefinition function mapRefs( field: VectorElement, f: MapRef, ): VectorElement function mapRefs( field: FieldData, f: MapRef, ): FieldData function mapRefs( field: FieldData, f: MapRef, ): FieldData { return rewriteFieldRefs(field, f) ?? field } function fieldEqual(field1: FieldData, field2: FieldData): boolean { if (typeof field1 !== 'object') return field1 === field2 if (typeof field2 !== 'object') return false if ('node' in field1 && 'node' in field2) { if (field1['whitespace'] !== field2['whitespace']) return false if (isTokenId(field1.node) && isTokenId(field2.node)) return Token.equal(field1.node, field2.node) else return field1.node === field2.node } else if ('node' in field1 || 'node' in field2) { return false } else if (Array.isArray(field1) && Array.isArray(field2)) { return ( field1.length === field2.length && field1.every((value1, i) => fieldEqual(value1, field2[i])) ) } else if (Array.isArray(field1) || Array.isArray(field2)) { return false } else { const fieldObject1 = field1 satisfies StructuralField const fieldObject2 = field2 satisfies StructuralField const keys = new Set() for (const key of Object.keys(fieldObject1)) keys.add(key) for (const key of Object.keys(fieldObject2)) keys.add(key) for (const key of keys) if (!fieldEqual((fieldObject1 as any)[key], (fieldObject2 as any)[key])) return false return true } } function applyMixins(derivedCtor: any, constructors: any[]) { constructors.forEach((baseCtor) => { Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => { Object.defineProperty( derivedCtor.prototype, name, Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || Object.create(null), ) }) }) } interface AppFields { function: NodeChild parens: OpenCloseTokens | undefined nameSpecification: NameSpecification | undefined argument: NodeChild } interface OpenCloseTokens { open: T['token'] close: T['token'] } interface NameSpecification { name: T['token'] equals: T['token'] } export class App extends Ast { declare fields: FixedMap constructor(module: Module, fields: FixedMapView) { super(module, fields) } static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableApp) return parsed } static concrete( module: MutableModule, func: NodeChild, parens: OpenCloseTokens | undefined, nameSpecification: NameSpecification | undefined, argument: NodeChild, ) { const base = module.baseObject('App') const id_ = base.get('id') const fields = composeFieldData(base, { function: concreteChild(module, func, id_), parens, nameSpecification, argument: concreteChild(module, argument, id_), }) return asOwned(new MutableApp(module, fields)) } static new( module: MutableModule, func: Owned, argumentName: StrictIdentLike | undefined, argument: Owned, ) { return App.concrete( module, autospaced(func), undefined, nameSpecification(argumentName), autospaced(argument), ) } static positional(func: Owned, argument: Owned, module?: MutableModule): Owned { return App.new(module ?? MutableModule.Transient(), func, undefined, argument) } static PositionalSequence(func: Owned, args: Owned[]): Owned { return args.reduce( (expression, argument) => App.new(func.module, expression, undefined, argument), func, ) } get function(): Ast { return this.module.get(this.fields.get('function').node) } get argumentName(): Token | undefined { return this.module.getToken(this.fields.get('nameSpecification')?.name.node) } get argument(): Ast { return this.module.get(this.fields.get('argument').node) } *concreteChildren(verbatim?: boolean): IterableIterator { const { function: function_, parens, nameSpecification, argument } = getAll(this.fields) yield ensureUnspaced(function_, verbatim) const useParens = !!(parens && (nameSpecification || verbatim)) const spacedEquals = useParens && !!nameSpecification?.equals.whitespace if (useParens) yield ensureSpaced(parens.open, verbatim) if (nameSpecification) { yield useParens ? preferUnspaced(nameSpecification.name) : ensureSpaced(nameSpecification.name, verbatim) yield ensureSpacedOnlyIf(nameSpecification.equals, spacedEquals, verbatim) } yield ensureSpacedOnlyIf(argument, !nameSpecification || spacedEquals, verbatim) if (useParens) yield preferUnspaced(parens.close) } printSubtree( info: SpanMap, offset: number, parentIndent: string | undefined, verbatim?: boolean, ): string { const verbatim_ = verbatim ?? (this.function instanceof Invalid || this.argument instanceof Invalid) return super.printSubtree(info, offset, parentIndent, verbatim_) } } function ensureSpacedOnlyIf( child: NodeChild, condition: boolean, verbatim: boolean | undefined, ): ConcreteChild { return condition ? ensureSpaced(child, verbatim) : ensureUnspaced(child, verbatim) } type ConcreteChild = { whitespace: string; node: T } function isConcrete(child: NodeChild): child is ConcreteChild { return child.whitespace !== undefined } function tryAsConcrete(child: NodeChild): ConcreteChild | undefined { return isConcrete(child) ? child : undefined } function ensureSpaced(child: NodeChild, verbatim: boolean | undefined): ConcreteChild { const concreteInput = tryAsConcrete(child) if (verbatim && concreteInput) return concreteInput return concreteInput?.whitespace ? concreteInput : { ...child, whitespace: ' ' } } function ensureUnspaced(child: NodeChild, verbatim: boolean | undefined): ConcreteChild { const concreteInput = tryAsConcrete(child) if (verbatim && concreteInput) return concreteInput return concreteInput?.whitespace === '' ? concreteInput : { ...child, whitespace: '' } } function preferSpacedIf(child: NodeChild, condition: boolean): ConcreteChild { return condition ? preferSpaced(child) : preferUnspaced(child) } function preferUnspaced(child: NodeChild): ConcreteChild { return tryAsConcrete(child) ?? { ...child, whitespace: '' } } function preferSpaced(child: NodeChild): ConcreteChild { return tryAsConcrete(child) ?? { ...child, whitespace: ' ' } } export class MutableApp extends App implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap setFunction(value: Owned) { setNode(this.fields, 'function', this.claimChild(value)) } setArgumentName(name: StrictIdentLike | undefined) { this.fields.set('nameSpecification', nameSpecification(name)) } setArgument(value: Owned) { setNode(this.fields, 'argument', this.claimChild(value)) } } export interface MutableApp extends App, MutableAst { get function(): MutableAst get argument(): MutableAst } applyMixins(MutableApp, [MutableAst]) interface UnaryOprAppFields { operator: NodeChild argument: NodeChild | undefined } export class UnaryOprApp extends Ast { declare fields: FixedMapView constructor(module: Module, fields: FixedMapView) { super(module, fields) } static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableUnaryOprApp) return parsed } static concrete( module: MutableModule, operator: NodeChild, argument: NodeChild | undefined, ) { const base = module.baseObject('UnaryOprApp') const id_ = base.get('id') const fields = composeFieldData(base, { operator, argument: concreteChild(module, argument, id_), }) return asOwned(new MutableUnaryOprApp(module, fields)) } static new(module: MutableModule, operator: Token, argument: Owned | undefined) { return this.concrete(module, unspaced(operator), argument ? autospaced(argument) : undefined) } get operator(): Token { return this.module.getToken(this.fields.get('operator').node) } get argument(): Ast | undefined { return this.module.get(this.fields.get('argument')?.node) } *concreteChildren(_verbatim?: boolean): IterableIterator { const { operator, argument } = getAll(this.fields) yield operator if (argument) yield argument } } export class MutableUnaryOprApp extends UnaryOprApp implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap setOperator(value: Token) { this.fields.set('operator', unspaced(value)) } setArgument(argument: Owned | undefined) { setNode(this.fields, 'argument', this.claimChild(argument)) } } export interface MutableUnaryOprApp extends UnaryOprApp, MutableAst { get argument(): MutableAst | undefined } applyMixins(MutableUnaryOprApp, [MutableAst]) interface AutoscopedIdentifierFields { operator: NodeChild identifier: NodeChild } export class AutoscopedIdentifier extends Ast { declare fields: FixedMapView constructor(module: Module, fields: FixedMapView) { super(module, fields) } static tryParse( source: string, module?: MutableModule, ): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableAutoscopedIdentifier) return parsed } static concrete(module: MutableModule, operator: NodeChild, identifier: NodeChild) { const base = module.baseObject('AutoscopedIdentifier') const fields = composeFieldData(base, { operator, identifier, }) return asOwned(new MutableAutoscopedIdentifier(module, fields)) } static new( identifier: TypeOrConstructorIdentifier, module?: MutableModule, ): Owned { const module_ = module || MutableModule.Transient() const operator = Token.new('..') const ident = Token.new(identifier, RawAst.Token.Type.Ident) return this.concrete(module_, unspaced(operator), unspaced(ident)) } *concreteChildren(_verbatim?: boolean): IterableIterator { const { operator, identifier } = getAll(this.fields) yield operator yield identifier } } export class MutableAutoscopedIdentifier extends AutoscopedIdentifier implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap setIdentifier(value: TypeOrConstructorIdentifier) { const token = Token.new(value, RawAst.Token.Type.Ident) this.fields.set('identifier', unspaced(token)) } } export interface MutableAutoscopedIdentifier extends AutoscopedIdentifier, MutableAst {} applyMixins(MutableAutoscopedIdentifier, [MutableAst]) interface NegationAppFields { operator: NodeChild argument: NodeChild } export class NegationApp extends Ast { declare fields: FixedMapView constructor(module: Module, fields: FixedMapView) { super(module, fields) } static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableNegationApp) return parsed } static concrete(module: MutableModule, operator: NodeChild, argument: NodeChild) { const base = module.baseObject('NegationApp') const id_ = base.get('id') const fields = composeFieldData(base, { operator, argument: concreteChild(module, argument, id_), }) return asOwned(new MutableNegationApp(module, fields)) } static new(module: MutableModule, operator: Token, argument: Owned) { return this.concrete(module, unspaced(operator), autospaced(argument)) } get operator(): Token { return this.module.getToken(this.fields.get('operator').node) } get argument(): Ast { return this.module.get(this.fields.get('argument').node) } *concreteChildren(_verbatim?: boolean): IterableIterator { const { operator, argument } = getAll(this.fields) yield operator if (argument) yield argument } } export class MutableNegationApp extends NegationApp implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap setArgument(value: Owned) { setNode(this.fields, 'argument', this.claimChild(value)) } } export interface MutableNegationApp extends NegationApp, MutableAst { get argument(): MutableAst } applyMixins(MutableNegationApp, [MutableAst]) interface OprAppFields { lhs: NodeChild | undefined operators: NodeChild[] rhs: NodeChild | undefined } export class OprApp extends Ast { declare fields: FixedMapView constructor(module: Module, fields: FixedMapView) { super(module, fields) } static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableOprApp) return parsed } static concrete( module: MutableModule, lhs: NodeChild | undefined, operators: NodeChild[], rhs: NodeChild | undefined, ) { const base = module.baseObject('OprApp') const id_ = base.get('id') const fields = composeFieldData(base, { lhs: concreteChild(module, lhs, id_), operators, rhs: concreteChild(module, rhs, id_), }) return asOwned(new MutableOprApp(module, fields)) } static new( module: MutableModule, lhs: Owned | undefined, operator: Token | string, rhs: Owned | undefined, ) { const operatorToken = operator instanceof Token ? operator : Token.new(operator, RawAst.Token.Type.Operator) return OprApp.concrete(module, unspaced(lhs), [autospaced(operatorToken)], autospaced(rhs)) } get lhs(): Ast | undefined { return this.module.get(this.fields.get('lhs')?.node) } get operator(): Result[]> { const operators = this.fields.get('operators') const operators_ = operators.map((child) => ({ ...child, node: this.module.getToken(child.node), })) const [opr] = operators_ return opr ? Ok(opr.node) : Err(operators_) } get rhs(): Ast | undefined { return this.module.get(this.fields.get('rhs')?.node) } *concreteChildren(_verbatim?: boolean): IterableIterator { const { lhs, operators, rhs } = getAll(this.fields) if (lhs) yield lhs yield* operators if (rhs) yield rhs } } export class MutableOprApp extends OprApp implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap setLhs(value: Owned) { setNode(this.fields, 'lhs', this.claimChild(value)) } setOperator(value: Token) { this.fields.set('operators', [unspaced(value)]) } setRhs(value: Owned) { setNode(this.fields, 'rhs', this.claimChild(value)) } } export interface MutableOprApp extends OprApp, MutableAst { get lhs(): MutableAst | undefined get rhs(): MutableAst | undefined } applyMixins(MutableOprApp, [MutableAst]) interface PropertyAccessFields { lhs: NodeChild | undefined operator: NodeChild rhs: NodeChild } export class PropertyAccess extends Ast { declare fields: FixedMapView constructor(module: Module, fields: FixedMapView) { super(module, fields) } static tryParse( source: string, module?: MutableModule, ): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutablePropertyAccess) return parsed } static new(module: MutableModule, lhs: Owned, rhs: IdentLike, style?: { spaced?: boolean }) { const dot = Token.new('.', RawAst.Token.Type.Operator) const whitespace = style?.spaced ? ' ' : '' return this.concrete( module, unspaced(lhs), { whitespace, node: dot }, { whitespace, node: Ident.newAllowingOperators(module, toIdent(rhs)) }, ) } static Sequence( segments: [StrictIdentLike, ...StrictIdentLike[]], module: MutableModule, ): Owned | Owned static Sequence( segments: [StrictIdentLike, ...StrictIdentLike[], IdentLike], module: MutableModule, ): Owned | Owned static Sequence( segments: IdentLike[], module: MutableModule, ): Owned | Owned | undefined static Sequence( segments: IdentLike[], module: MutableModule, ): Owned | Owned | undefined { let path: Owned | Owned | undefined let operatorInNonFinalSegment = false segments.forEach((s, i) => { const t = toIdent(s) if (i !== segments.length - 1 && !isIdentifier(t.code())) operatorInNonFinalSegment = true path = path ? this.new(module, path, t) : Ident.newAllowingOperators(module, t) }) if (!operatorInNonFinalSegment) return path } static concrete( module: MutableModule, lhs: NodeChild | undefined, operator: NodeChild, rhs: NodeChild>, ) { const base = module.baseObject('PropertyAccess') const id_ = base.get('id') const fields = composeFieldData(base, { lhs: concreteChild(module, lhs, id_), operator, rhs: concreteChild(module, rhs, id_), }) return asOwned(new MutablePropertyAccess(module, fields)) } get lhs(): Ast | undefined { return this.module.get(this.fields.get('lhs')?.node) } get operator(): Token { return this.module.getToken(this.fields.get('operator').node) } get rhs(): IdentifierOrOperatorIdentifierToken { const ast = this.module.get(this.fields.get('rhs').node) assert(ast instanceof Ident) return ast.token as IdentifierOrOperatorIdentifierToken } *concreteChildren(_verbatim?: boolean): IterableIterator { const { lhs, operator, rhs } = getAll(this.fields) if (lhs) yield lhs yield operator yield rhs } } export class MutablePropertyAccess extends PropertyAccess implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap setLhs(value: Owned | undefined) { setNode(this.fields, 'lhs', this.claimChild(value)) } setRhs(ident: IdentLike) { const node = this.claimChild(Ident.newAllowingOperators(this.module, ident)) const old = this.fields.get('rhs') this.fields.set('rhs', old ? { ...old, node } : unspaced(node)) } } export interface MutablePropertyAccess extends PropertyAccess, MutableAst { get lhs(): MutableAst | undefined } applyMixins(MutablePropertyAccess, [MutableAst]) /** Unroll the provided chain of `PropertyAccess` nodes, returning the first non-access as `subject` and the accesses * from left-to-right. */ export function accessChain(ast: Ast): { subject: Ast; accessChain: PropertyAccess[] } { const accessChain = new Array() while (ast instanceof PropertyAccess && ast.lhs) { accessChain.push(ast) ast = ast.lhs } accessChain.reverse() return { subject: ast, accessChain } } interface GenericFields { children: RawNodeChild[] } export class Generic extends Ast { declare fields: FixedMapView constructor(module: Module, fields: FixedMapView) { super(module, fields) } static concrete(module: MutableModule, children: (NodeChild | NodeChild)[]) { const base = module.baseObject('Generic') const id_ = base.get('id') const fields = composeFieldData(base, { children: children.map((child) => concreteChild(module, child, id_)), }) return asOwned(new MutableGeneric(module, fields)) } concreteChildren(_verbatim?: boolean): IterableIterator { return this.fields.get('children')[Symbol.iterator]() } } export class MutableGeneric extends Generic implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap } export interface MutableGeneric extends Generic, MutableAst {} applyMixins(MutableGeneric, [MutableAst]) interface MultiSegmentAppSegment { header: T['token'] body: T['ast'] | undefined } function multiSegmentAppSegment( header: string, body: Owned, ): MultiSegmentAppSegment function multiSegmentAppSegment( header: string, body: Owned | undefined, ): MultiSegmentAppSegment | undefined function multiSegmentAppSegment( header: string, body: Owned | undefined, ): MultiSegmentAppSegment | undefined { return { header: autospaced(Token.new(header, RawAst.Token.Type.Ident)), body: spaced(body ? (body as any) : undefined), } } function multiSegmentAppSegmentToRaw( module: MutableModule, msas: MultiSegmentAppSegment | undefined, parent: AstId, ): MultiSegmentAppSegment | undefined { if (!msas) return undefined return { ...msas, body: concreteChild(module, msas.body, parent), } } interface ImportFields extends FieldObject { polyglot: MultiSegmentAppSegment | undefined from: MultiSegmentAppSegment | undefined import: MultiSegmentAppSegment all: T['token'] | undefined as: MultiSegmentAppSegment | undefined hiding: MultiSegmentAppSegment | undefined } export class Import extends Ast { declare fields: FixedMapView constructor(module: Module, fields: FixedMapView) { super(module, fields) } static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableImport) return parsed } get polyglot(): Ast | undefined { return this.module.get(this.fields.get('polyglot')?.body?.node) } get from(): Ast | undefined { return this.module.get(this.fields.get('from')?.body?.node) } get import_(): Ast | undefined { return this.module.get(this.fields.get('import').body?.node) } get all(): Token | undefined { return this.module.getToken(this.fields.get('all')?.node) } get as(): Ast | undefined { return this.module.get(this.fields.get('as')?.body?.node) } get hiding(): Ast | undefined { return this.module.get(this.fields.get('hiding')?.body?.node) } static concrete( module: MutableModule, polyglot: MultiSegmentAppSegment | undefined, from: MultiSegmentAppSegment | undefined, import_: MultiSegmentAppSegment, all: NodeChild | undefined, as: MultiSegmentAppSegment | undefined, hiding: MultiSegmentAppSegment | undefined, ) { const base = module.baseObject('Import') const id_ = base.get('id') const ownedFields: ImportFields = { polyglot, from, import: import_, all, as, hiding, } const rawFields = mapRefs(ownedFields, ownedToRaw(module, id_)) const fields = composeFieldData(base, rawFields) return asOwned(new MutableImport(module, fields)) } static Qualified(path: IdentLike[], module: MutableModule): Owned | undefined { const path_ = PropertyAccess.Sequence(path, module) if (!path_) return return MutableImport.concrete( module, undefined, undefined, multiSegmentAppSegment('import', path_), undefined, undefined, undefined, ) } static Unqualified( path: IdentLike[], name: IdentLike, module: MutableModule, ): Owned | undefined { const path_ = PropertyAccess.Sequence(path, module) if (!path_) return const name_ = Ident.newAllowingOperators(module, name) return MutableImport.concrete( module, undefined, multiSegmentAppSegment('from', path_), multiSegmentAppSegment('import', name_), undefined, undefined, undefined, ) } *concreteChildren(_verbatim?: boolean): IterableIterator { const segment = (segment: MultiSegmentAppSegment | undefined) => { const parts = [] if (segment) parts.push(segment.header) if (segment?.body) parts.push(segment.body) return parts } const { polyglot, from, import: import_, all, as, hiding } = getAll(this.fields) yield* segment(polyglot) yield* segment(from) yield* segment(import_) if (all) yield all yield* segment(as) yield* segment(hiding) } } export class MutableImport extends Import implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap private toRaw(msas: MultiSegmentAppSegment): MultiSegmentAppSegment private toRaw( msas: MultiSegmentAppSegment | undefined, ): MultiSegmentAppSegment | undefined private toRaw( msas: MultiSegmentAppSegment | undefined, ): MultiSegmentAppSegment | undefined { return multiSegmentAppSegmentToRaw(this.module, msas, this.id) } setPolyglot(value: Owned | undefined) { this.fields.set( 'polyglot', value ? this.toRaw(multiSegmentAppSegment('polyglot', value)) : undefined, ) } setFrom(value: Owned | undefined) { this.fields.set('from', value ? this.toRaw(multiSegmentAppSegment('from', value)) : value) } setImport(value: Owned) { this.fields.set('import', this.toRaw(multiSegmentAppSegment('import', value))) } setAll(value: Token | undefined) { this.fields.set('all', spaced(value)) } setAs(value: Owned | undefined) { this.fields.set('as', this.toRaw(multiSegmentAppSegment('as', value))) } setHiding(value: Owned | undefined) { this.fields.set('hiding', this.toRaw(multiSegmentAppSegment('hiding', value))) } } export interface MutableImport extends Import, MutableAst { get polyglot(): MutableAst | undefined get from(): MutableAst | undefined get import_(): MutableAst | undefined get as(): MutableAst | undefined get hiding(): MutableAst | undefined } applyMixins(MutableImport, [MutableAst]) interface TreeRefs { token: any ast: any } type RefMap = ( field: FieldData, ) => FieldData | undefined type RawRefs = { token: NodeChild ast: NodeChild } export type OwnedRefs = { token: NodeChild ast: NodeChild } type ConcreteRefs = { token: NodeChild ast: NodeChild } function ownedToRaw(module: MutableModule, parentId: AstId): RefMap { return (child: FieldData) => { if (typeof child !== 'object') return if (!('node' in child)) return if (isToken(child.node)) return return { ...child, node: claimChild(module, child.node, parentId) } } } function rawToConcrete(module: Module): RefMap { return (child: FieldData) => { if (typeof child !== 'object') return if (!('node' in child)) return if (isTokenId(child.node)) return { ...child, node: module.getToken(child.node) } else return { ...child, node: module.get(child.node) } } } function concreteToOwned(module: MutableModule): RefMap { return (child: FieldData) => { if (typeof child !== 'object') return if (!('node' in child)) return if (isTokenChild(child)) return child else return { ...child, node: module.copy(child.node) } } } export interface TextToken { type: 'token' readonly token: T['token'] readonly interpreted?: string | undefined } export interface TextSplice { type: 'splice' readonly open: T['token'] readonly expression: T['ast'] | undefined readonly close: T['token'] } export type TextElement = TextToken | TextSplice function textElementValue(element: TextElement): string { switch (element.type) { case 'token': { if (element.interpreted != null) return element.interpreted // The logical newline is not necessarily the same as the concrete token, e.g. the token could be a CRLF. if (element.token.node.tokenType_ === RawAst.Token.Type.TextNewline) return '\n' // The token is an invalid escape-sequence or a text-section; return it verbatim. return element.token.node.code() } case 'splice': { let s = '' s += element.open.node.code() if (element.expression) { s += element.expression.whitespace ?? '' s += element.expression.node.code() } s += element.close.whitespace ?? '' s += element.close.node.code() return s } } } function rawTextElementValue(raw: TextElement, module: Module): string { return textElementValue(mapRefs(raw, rawToConcrete(module))) } function uninterpolatedText(elements: DeepReadonly, module: Module): string { return elements.reduce((s, e) => s + rawTextElementValue(e, module), '') } function fieldConcreteChildren(field: FieldData) { const children = new Array() rewriteFieldRefs(field, (subfield: FieldData) => { if (typeof subfield === 'object' && 'node' in subfield) children.push(subfield) }) return children } interface TextLiteralFields { open: NodeChild | undefined newline: NodeChild | undefined elements: TextElement[] close: NodeChild | undefined } export class TextLiteral extends Ast { declare fields: FixedMapView constructor(module: Module, fields: FixedMapView) { super(module, fields) } static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableTextLiteral) return parsed } static concrete( module: MutableModule, open: NodeChild | undefined, newline: NodeChild | undefined, elements: TextElement[], close: NodeChild | undefined, ) { const base = module.baseObject('TextLiteral') const id_ = base.get('id') const fields = composeFieldData(base, { open, newline, elements: elements.map((e) => mapRefs(e, ownedToRaw(module, id_))), close, }) return asOwned(new MutableTextLiteral(module, fields)) } static new(rawText: string, module?: MutableModule): Owned { const escaped = escapeTextLiteral(rawText) const parsed = parse(`'${escaped}'`, module) if (!(parsed instanceof MutableTextLiteral)) { console.error(`Failed to escape string for interpolated text`, rawText, escaped, parsed) const safeText = rawText.replaceAll(/[^-+A-Za-z0-9_. ]/g, '') return TextLiteral.new(safeText, module) } return parsed } /** * Return the literal value of the string with all escape sequences applied, but without * evaluating any interpolated expressions. */ get rawTextContent(): string { return uninterpolatedText(this.fields.get('elements'), this.module) } *concreteChildren(_verbatim?: boolean): IterableIterator { const { open, newline, elements, close } = getAll(this.fields) if (open) yield open if (newline) yield newline for (const e of elements) yield* fieldConcreteChildren(e) if (close) yield close } boundaryTokenCode(): string | undefined { return (this.open || this.close)?.code() } isInterpolated(): boolean { const token = this.boundaryTokenCode() return token === "'" || token === "'''" } get open(): Token | undefined { return this.module.getToken(this.fields.get('open')?.node) } get close(): Token | undefined { return this.module.getToken(this.fields.get('close')?.node) } get elements(): TextElement[] { return this.fields.get('elements').map((e) => mapRefs(e, rawToConcrete(this.module))) } } export class MutableTextLiteral extends TextLiteral implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap setBoundaries(code: string) { this.fields.set('open', unspaced(Token.new(code))) this.fields.set('close', unspaced(Token.new(code))) } setElements(elements: TextElement[]) { this.fields.set( 'elements', elements.map((e) => mapRefs(e, ownedToRaw(this.module, this.id))), ) } /** * Set literal value of the string. The code representation of assigned text will be automatically * transformed to use escape sequences when necessary. */ setRawTextContent(rawText: string) { let boundary = this.boundaryTokenCode() const isInterpolated = this.isInterpolated() const mustBecomeInterpolated = !isInterpolated && (!boundary || rawText.match(/["\n\r]/)) if (mustBecomeInterpolated) { boundary = "'" this.setBoundaries(boundary) } const literalContents = isInterpolated || mustBecomeInterpolated ? escapeTextLiteral(rawText) : rawText const parsed = parse(`${boundary}${literalContents}${boundary}`) assert(parsed instanceof TextLiteral) const elements = parsed.elements.map((e) => mapRefs(e, concreteToOwned(this.module))) this.setElements(elements) } } export interface MutableTextLiteral extends TextLiteral, MutableAst {} applyMixins(MutableTextLiteral, [MutableAst]) interface DocumentedFields { open: NodeChild elements: TextToken[] newlines: NodeChild[] expression: NodeChild | undefined } export class Documented extends Ast { declare fields: FixedMapView constructor(module: Module, fields: FixedMapView) { super(module, fields) } static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableDocumented) return parsed } static new(text: string, expression: Owned) { return this.concrete( expression.module, undefined, textToUninterpolatedElements(text), undefined, autospaced(expression), ) } static concrete( module: MutableModule, open: NodeChild | undefined, elements: TextToken[], newlines: NodeChild[] | undefined, expression: NodeChild | undefined, ) { const base = module.baseObject('Documented') const id_ = base.get('id') const fields = composeFieldData(base, { open: open ?? unspaced(Token.new('##', RawAst.Token.Type.Operator)), elements: elements.map((e) => mapRefs(e, ownedToRaw(module, id_))), newlines: newlines ?? [unspaced(Token.new('\n', RawAst.Token.Type.Newline))], expression: concreteChild(module, expression, id_), }) return asOwned(new MutableDocumented(module, fields)) } get expression(): Ast | undefined { return this.module.get(this.fields.get('expression')?.node) } /** Return the string value of the documentation. */ documentation(): string { const raw = uninterpolatedText(this.fields.get('elements'), this.module) return raw.startsWith(' ') ? raw.slice(1) : raw } wrappedExpression(): Ast | undefined { return this.expression } documentingAncestor(): Documented | undefined { return this } *concreteChildren(_verbatim?: boolean): IterableIterator { const { open, elements, newlines, expression } = getAll(this.fields) yield open for (const { token } of elements) yield token yield* newlines if (expression) yield expression } printSubtree( info: SpanMap, offset: number, parentIndent: string | undefined, verbatim?: boolean, ): string { return printDocumented(this, info, offset, parentIndent, verbatim) } } export class MutableDocumented extends Documented implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap setDocumentationText(text: string) { this.fields.set( 'elements', textToUninterpolatedElements(text).map((owned) => mapRefs(owned, ownedToRaw(this.module, this.id)), ), ) } setExpression(value: Owned | undefined) { this.fields.set('expression', unspaced(this.claimChild(value))) } } export interface MutableDocumented extends Documented, MutableAst { get expression(): MutableAst | undefined } applyMixins(MutableDocumented, [MutableAst]) function textToUninterpolatedElements(text: string): TextToken[] { const elements = new Array>() text.split('\n').forEach((line, i) => { if (i) elements.push({ type: 'token', token: unspaced(Token.new('\n', RawAst.Token.Type.TextNewline)), }) elements.push({ type: 'token', token: autospaced(Token.new(line, RawAst.Token.Type.TextSection)), }) }) return elements } interface InvalidFields { expression: NodeChild } export class Invalid extends Ast { declare fields: FixedMapView constructor(module: Module, fields: FixedMapView) { super(module, fields) } static concrete(module: MutableModule, expression: NodeChild) { const base = module.baseObject('Invalid') return asOwned(new MutableInvalid(module, invalidFields(module, base, expression))) } get expression(): Ast { return this.module.get(this.fields.get('expression').node) } *concreteChildren(_verbatim?: boolean): IterableIterator { yield this.fields.get('expression') } printSubtree( info: SpanMap, offset: number, parentIndent: string | undefined, _verbatim?: boolean, ): string { return super.printSubtree(info, offset, parentIndent, true) } } export function invalidFields( module: MutableModule, base: FixedMap, expression: NodeChild, ): FixedMap { const id_ = base.get('id') return composeFieldData(base, { expression: concreteChild(module, expression, id_) }) } export class MutableInvalid extends Invalid implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap } export interface MutableInvalid extends Invalid, MutableAst { /** The `expression` getter is intentionally not narrowed to provide mutable access: * It makes more sense to `.replace` the `Invalid` node. */ } applyMixins(MutableInvalid, [MutableAst]) interface GroupFields { open: NodeChild | undefined expression: NodeChild | undefined close: NodeChild | undefined } export class Group extends Ast { declare fields: FixedMapView constructor(module: Module, fields: FixedMapView) { super(module, fields) } static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableGroup) return parsed } static concrete( module: MutableModule, open: NodeChild | undefined, expression: NodeChild | undefined, close: NodeChild | undefined, ) { const base = module.baseObject('Group') const id_ = base.get('id') const fields = composeFieldData(base, { open, expression: concreteChild(module, expression, id_), close, }) return asOwned(new MutableGroup(module, fields)) } static new(module: MutableModule, expression: Owned) { const open = unspaced(Token.new('(', RawAst.Token.Type.OpenSymbol)) const close = unspaced(Token.new(')', RawAst.Token.Type.CloseSymbol)) return this.concrete(module, open, unspaced(expression), close) } get expression(): Ast | undefined { return this.module.get(this.fields.get('expression')?.node) } *concreteChildren(_verbatim?: boolean): IterableIterator { const { open, expression, close } = getAll(this.fields) if (open) yield open if (expression) yield expression if (close) yield close } } export class MutableGroup extends Group implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap setExpression(value: Owned | undefined) { this.fields.set('expression', unspaced(this.claimChild(value))) } } export interface MutableGroup extends Group, MutableAst { get expression(): MutableAst | undefined } applyMixins(MutableGroup, [MutableAst]) interface NumericLiteralFields { tokens: NodeChild[] } export class NumericLiteral extends Ast { declare fields: FixedMapView constructor(module: Module, fields: FixedMapView) { super(module, fields) } static tryParse( source: string, module?: MutableModule, ): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableNumericLiteral) return parsed } static concrete(module: MutableModule, tokens: NodeChild[]) { const base = module.baseObject('NumericLiteral') const fields = composeFieldData(base, { tokens }) return asOwned(new MutableNumericLiteral(module, fields)) } concreteChildren(_verbatim?: boolean): IterableIterator { return this.fields.get('tokens')[Symbol.iterator]() } } export class MutableNumericLiteral extends NumericLiteral implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap } export interface MutableNumericLiteral extends NumericLiteral, MutableAst {} applyMixins(MutableNumericLiteral, [MutableAst]) /** The actual contents of an `ArgumentDefinition` are complex, but probably of more interest to the compiler than the * GUI. We just need to represent them faithfully and create the simple cases. */ type ArgumentDefinition = (T['ast'] | T['token'])[] interface FunctionFields { name: NodeChild argumentDefinitions: ArgumentDefinition[] equals: NodeChild body: NodeChild | undefined } export class Function extends Ast { declare fields: FixedMapView constructor(module: Module, fields: FixedMapView) { super(module, fields) } static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableFunction) return parsed } get name(): Ast { return this.module.get(this.fields.get('name').node) } get body(): Ast | undefined { return this.module.get(this.fields.get('body')?.node) } get argumentDefinitions(): ArgumentDefinition[] { return this.fields .get('argumentDefinitions') .map((raw) => raw.map((part) => this.module.getConcrete(part))) } static concrete( module: MutableModule, name: NodeChild, argumentDefinitions: ArgumentDefinition[], equals: NodeChild, body: NodeChild | undefined, ) { const base = module.baseObject('Function') const id_ = base.get('id') const fields = composeFieldData(base, { name: concreteChild(module, name, id_), argumentDefinitions: argumentDefinitions.map((def) => mapRefs(def, ownedToRaw(module, id_))), equals, body: concreteChild(module, body, id_), }) return asOwned(new MutableFunction(module, fields)) } static new( module: MutableModule, name: IdentLike, argumentDefinitions: ArgumentDefinition[], body: Owned, ): Owned { // Note that a function name may not be an operator if the function is not in the body of a type definition, but we // can't easily enforce that because we don't currently make a syntactic distinction between top-level functions and // type methods. return MutableFunction.concrete( module, unspaced(Ident.newAllowingOperators(module, name)), argumentDefinitions, spaced(makeEquals()), autospaced(body), ) } /** Construct a function with simple (name-only) arguments and a body block. */ static fromStatements( module: MutableModule, name: IdentLike, argumentNames: StrictIdentLike[], statements: Owned[], ): Owned { const statements_: OwnedBlockLine[] = statements.map((statement) => ({ expression: unspaced(statement), })) const argumentDefinitions = argumentNames.map((name) => [spaced(Ident.new(module, name))]) const body = BodyBlock.new(statements_, module) return MutableFunction.new(module, name, argumentDefinitions, body) } *bodyExpressions(): IterableIterator { const body = this.body if (body instanceof BodyBlock) { yield* body.statements() } else if (body) { yield body } } *concreteChildren(_verbatim?: boolean): IterableIterator { const { name, argumentDefinitions, equals, body } = getAll(this.fields) yield name for (const def of argumentDefinitions) yield* def yield { whitespace: equals.whitespace ?? ' ', node: this.module.getToken(equals.node) } if (body) yield preferSpacedIf(body, this.module.tryGet(body.node) instanceof BodyBlock) } } export class MutableFunction extends Function implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap setName(value: Owned) { this.fields.set('name', unspaced(this.claimChild(value))) } setBody(value: Owned | undefined) { this.fields.set('body', unspaced(this.claimChild(value))) } setArgumentDefinitions(defs: ArgumentDefinition[]) { this.fields.set( 'argumentDefinitions', defs.map((def) => mapRefs(def, ownedToRaw(this.module, this.id))), ) } /** Returns the body, after converting it to a block if it was empty or an inline expression. */ bodyAsBlock(): MutableBodyBlock { const oldBody = this.body if (oldBody instanceof MutableBodyBlock) return oldBody const newBody = BodyBlock.new([], this.module) if (oldBody) newBody.push(oldBody.take()) return newBody } } export interface MutableFunction extends Function, MutableAst { get name(): MutableAst get body(): MutableAst | undefined } applyMixins(MutableFunction, [MutableAst]) interface AssignmentFields { pattern: NodeChild equals: NodeChild expression: NodeChild } export class Assignment extends Ast { declare fields: FixedMapView constructor(module: Module, fields: FixedMapView) { super(module, fields) } static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableAssignment) return parsed } static concrete( module: MutableModule, pattern: NodeChild, equals: NodeChild, expression: NodeChild, ) { const base = module.baseObject('Assignment') const id_ = base.get('id') const fields = composeFieldData(base, { pattern: concreteChild(module, pattern, id_), equals, expression: concreteChild(module, expression, id_), }) return asOwned(new MutableAssignment(module, fields)) } static new(module: MutableModule, ident: StrictIdentLike, expression: Owned) { return Assignment.concrete( module, unspaced(Ident.new(module, ident)), spaced(makeEquals()), spaced(expression), ) } get pattern(): Ast { return this.module.get(this.fields.get('pattern').node) } get expression(): Ast { return this.module.get(this.fields.get('expression').node) } *concreteChildren(verbatim?: boolean): IterableIterator { const { pattern, equals, expression } = getAll(this.fields) yield ensureUnspaced(pattern, verbatim) yield ensureSpacedOnlyIf(equals, expression.whitespace !== '', verbatim) yield preferSpaced(expression) } } export class MutableAssignment extends Assignment implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap setPattern(value: Owned) { this.fields.set('pattern', unspaced(this.claimChild(value))) } setExpression(value: Owned) { setNode(this.fields, 'expression', this.claimChild(value)) } } export interface MutableAssignment extends Assignment, MutableAst { get pattern(): MutableAst get expression(): MutableAst } applyMixins(MutableAssignment, [MutableAst]) interface BodyBlockFields { lines: RawBlockLine[] } export class BodyBlock extends Ast { declare fields: FixedMapView constructor(module: Module, fields: FixedMapView) { super(module, fields) } static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableBodyBlock) return parsed } static concrete(module: MutableModule, lines: OwnedBlockLine[]) { const base = module.baseObject('BodyBlock') const id_ = base.get('id') const fields = composeFieldData(base, { lines: lines.map((line) => lineToRaw(line, module, id_)), }) return asOwned(new MutableBodyBlock(module, fields)) } static new(lines: OwnedBlockLine[], module: MutableModule) { return BodyBlock.concrete(module, lines) } get lines(): BlockLine[] { return this.fields.get('lines').map((line) => lineFromRaw(line, this.module)) } *statements(): IterableIterator { for (const line of this.lines) { if (line.expression) yield line.expression.node } } *concreteChildren(_verbatim?: boolean): IterableIterator { for (const line of this.fields.get('lines')) { yield preferUnspaced(line.newline) if (line.expression) yield line.expression } } printSubtree( info: SpanMap, offset: number, parentIndent: string | undefined, verbatim?: boolean, ): string { return printBlock(this, info, offset, parentIndent, verbatim) } } export class MutableBodyBlock extends BodyBlock implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap updateLines(map: (lines: OwnedBlockLine[]) => OwnedBlockLine[]) { return this.setLines(map(this.takeLines())) } takeLines(): OwnedBlockLine[] { return this.fields.get('lines').map((line) => ownedLineFromRaw(line, this.module)) } setLines(lines: OwnedBlockLine[]) { this.fields.set( 'lines', lines.map((line) => lineToRaw(line, this.module, this.id)), ) } /** Insert the given statement(s) starting at the specified line index. */ insert(index: number, ...statements: (Owned | undefined)[]) { const before = this.fields.get('lines').slice(0, index) const insertions = statements.map((statement) => ({ newline: unspaced(Token.new('\n', RawAst.Token.Type.Newline)), expression: statement && unspaced(this.claimChild(statement)), })) const after = this.fields.get('lines').slice(index) this.fields.set('lines', [...before, ...insertions, ...after]) } push(statement: Owned) { const oldLines = this.fields.get('lines') const newLine = { newline: unspaced(Token.new('\n', RawAst.Token.Type.Newline)), expression: unspaced(this.claimChild(statement)), } this.fields.set('lines', [...oldLines, newLine]) } filter(keep: (ast: MutableAst) => boolean) { const oldLines = this.fields.get('lines') const filteredLines = oldLines.filter((line) => { if (!line.expression) return true return keep(this.module.get(line.expression.node)) }) this.fields.set('lines', filteredLines) } } export interface MutableBodyBlock extends BodyBlock, MutableAst { statements(): IterableIterator } applyMixins(MutableBodyBlock, [MutableAst]) interface RawLine { newline: T['token'] expression: T['ast'] | undefined } interface Line { newline?: T['token'] | undefined expression: T['ast'] | undefined } type RawBlockLine = RawLine export type BlockLine = Line export type OwnedBlockLine = Line function lineFromRaw(raw: RawBlockLine, module: Module): BlockLine { const expression = raw.expression ? module.get(raw.expression.node) : undefined return { newline: { ...raw.newline, node: module.getToken(raw.newline.node) }, expression: expression ? { whitespace: raw.expression?.whitespace, node: expression, } : undefined, } } function ownedLineFromRaw(raw: RawBlockLine, module: MutableModule): OwnedBlockLine { const expression = raw.expression ? module.get(raw.expression.node).takeIfParented() : undefined return { newline: { ...raw.newline, node: module.getToken(raw.newline.node) }, expression: expression ? { whitespace: raw.expression?.whitespace, node: expression, } : undefined, } } function lineToRaw(line: OwnedBlockLine, module: MutableModule, block: AstId): RawBlockLine { return { newline: line.newline ?? unspaced(Token.new('\n', RawAst.Token.Type.Newline)), expression: line.expression ? { whitespace: line.expression?.whitespace, node: claimChild(module, line.expression.node, block), } : undefined, } } interface IdentFields { token: NodeChild } export class Ident extends Ast { declare fields: FixedMapView constructor(module: Module, fields: FixedMapView) { super(module, fields) } static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableIdent) return parsed } get token(): IdentifierToken { return this.module.getToken(this.fields.get('token').node) as IdentifierToken } static concrete(module: MutableModule, token: NodeChild) { const base = module.baseObject('Ident') const fields = composeFieldData(base, { token }) return asOwned(new MutableIdent(module, fields)) } static new(module: MutableModule, ident: StrictIdentLike) { return Ident.concrete(module, unspaced(toIdentStrict(ident))) } /** @internal */ static newAllowingOperators(module: MutableModule, ident: IdentLike) { return Ident.concrete(module, unspaced(toIdent(ident))) } *concreteChildren(_verbatim?: boolean): IterableIterator { yield this.fields.get('token') } code(): Identifier { return this.token.code() as Identifier } } export class MutableIdent extends Ident implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap setToken(ident: IdentLike) { this.fields.set('token', unspaced(toIdent(ident))) } code(): Identifier { return this.token.code() } } export interface MutableIdent extends Ident, MutableAst {} applyMixins(MutableIdent, [MutableAst]) interface WildcardFields { token: NodeChild } export class Wildcard extends Ast { declare fields: FixedMapView constructor(module: Module, fields: FixedMapView) { super(module, fields) } static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableWildcard) return parsed } get token(): Token { return this.module.getToken(this.fields.get('token').node) } static concrete(module: MutableModule, token: NodeChild) { const base = module.baseObject('Wildcard') const fields = composeFieldData(base, { token }) return asOwned(new MutableWildcard(module, fields)) } static new(module?: MutableModule) { const token = Token.new('_', RawAst.Token.Type.Wildcard) return this.concrete(module ?? MutableModule.Transient(), unspaced(token)) } *concreteChildren(_verbatim?: boolean): IterableIterator { yield this.fields.get('token') } } export class MutableWildcard extends Wildcard implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap } export interface MutableWildcard extends Wildcard, MutableAst {} applyMixins(MutableWildcard, [MutableAst]) type AbstractVectorElement = { delimiter?: T['token'] value: T['ast'] | undefined } function delimitVectorElement(element: AbstractVectorElement): VectorElement { return { ...element, delimiter: element.delimiter ?? unspaced(Token.new(',', RawAst.Token.Type.Operator)), } } type VectorElement = { delimiter: T['token']; value: T['ast'] | undefined } interface VectorFields { open: NodeChild elements: VectorElement[] close: NodeChild } export class Vector extends Ast { declare fields: FixedMapView constructor(module: Module, fields: FixedMapView) { super(module, fields) } static tryParse(source: string, module?: MutableModule): Owned | undefined { const parsed = parse(source, module) if (parsed instanceof MutableVector) return parsed } static concrete( module: MutableModule, open: NodeChild | undefined, elements: AbstractVectorElement[], close: NodeChild | undefined, ) { const base = module.baseObject('Vector') const id_ = base.get('id') const fields = composeFieldData(base, { open: open ?? unspaced(Token.new('[', RawAst.Token.Type.OpenSymbol)), elements: elements.map(delimitVectorElement).map((e) => mapRefs(e, ownedToRaw(module, id_))), close: close ?? unspaced(Token.new(']', RawAst.Token.Type.CloseSymbol)), }) return asOwned(new MutableVector(module, fields)) } static new(module: MutableModule, elements: Owned[]) { return this.concrete( module, undefined, elements.map((value) => ({ value: autospaced(value) })), undefined, ) } static tryBuild( inputs: Iterable, elementBuilder: (input: T, module: MutableModule) => Owned, edit?: MutableModule, ): Owned static tryBuild( inputs: Iterable, elementBuilder: (input: T, module: MutableModule) => Owned | undefined, edit?: MutableModule, ): Owned | undefined static tryBuild( inputs: Iterable, valueBuilder: (input: T, module: MutableModule) => Owned | undefined, edit?: MutableModule, ): Owned | undefined { const module = edit ?? MutableModule.Transient() const elements = new Array>() for (const input of inputs) { const value = valueBuilder(input, module) if (!value) return elements.push({ value: autospaced(value) }) } return Vector.concrete(module, undefined, elements, undefined) } static build( inputs: Iterable, elementBuilder: (input: T, module: MutableModule) => Owned, edit?: MutableModule, ): Owned { return Vector.tryBuild(inputs, elementBuilder, edit) } *concreteChildren(verbatim?: boolean): IterableIterator { const { open, elements, close } = getAll(this.fields) yield ensureUnspaced(open, verbatim) let isFirst = true for (const { delimiter, value } of elements) { if (isFirst && value) { yield preferUnspaced(value) } else { yield preferUnspaced(delimiter) if (value) yield preferSpaced(value) } isFirst = false } yield preferUnspaced(close) } *values(): IterableIterator { for (const element of this.fields.get('elements')) if (element.value) yield this.module.get(element.value.node) } } export class MutableVector extends Vector implements MutableAst { declare readonly module: MutableModule declare readonly fields: FixedMap push(value: Owned) { const elements = this.fields.get('elements') const element = mapRefs( delimitVectorElement({ value: autospaced(value) }), ownedToRaw(this.module, this.id), ) this.fields.set('elements', [...elements, element]) } keep(predicate: (ast: Ast) => boolean) { const elements = this.fields.get('elements') const filtered = elements.filter( (element) => element.value && predicate(this.module.get(element.value.node)), ) this.fields.set('elements', filtered) } } export interface MutableVector extends Vector, MutableAst { values(): IterableIterator } applyMixins(MutableVector, [MutableAst]) export type Mutable = T extends App ? MutableApp : T extends Assignment ? MutableAssignment : T extends BodyBlock ? MutableBodyBlock : T extends Documented ? MutableDocumented : T extends Function ? MutableFunction : T extends Generic ? MutableGeneric : T extends Group ? MutableGroup : T extends Ident ? MutableIdent : T extends Import ? MutableImport : T extends Invalid ? MutableInvalid : T extends NegationApp ? MutableNegationApp : T extends NumericLiteral ? MutableNumericLiteral : T extends OprApp ? MutableOprApp : T extends PropertyAccess ? MutablePropertyAccess : T extends TextLiteral ? MutableTextLiteral : T extends UnaryOprApp ? MutableUnaryOprApp : T extends Vector ? MutableVector : T extends Wildcard ? MutableWildcard : MutableAst export function materializeMutable(module: MutableModule, fields: FixedMap): MutableAst { const type = fields.get('type') const fieldsForType = fields as FixedMap switch (type) { case 'App': return new MutableApp(module, fieldsForType) case 'Assignment': return new MutableAssignment(module, fieldsForType) case 'BodyBlock': return new MutableBodyBlock(module, fieldsForType) case 'Documented': return new MutableDocumented(module, fieldsForType) case 'Function': return new MutableFunction(module, fieldsForType) case 'Generic': return new MutableGeneric(module, fieldsForType) case 'Group': return new MutableGroup(module, fieldsForType) case 'Ident': return new MutableIdent(module, fieldsForType) case 'Import': return new MutableImport(module, fieldsForType) case 'Invalid': return new MutableInvalid(module, fieldsForType) case 'NegationApp': return new MutableNegationApp(module, fieldsForType) case 'NumericLiteral': return new MutableNumericLiteral(module, fieldsForType) case 'OprApp': return new MutableOprApp(module, fieldsForType) case 'PropertyAccess': return new MutablePropertyAccess(module, fieldsForType) case 'TextLiteral': return new MutableTextLiteral(module, fieldsForType) case 'UnaryOprApp': return new MutableUnaryOprApp(module, fieldsForType) case 'AutoscopedIdentifier': return new MutableAutoscopedIdentifier(module, fieldsForType) case 'Vector': return new MutableVector(module, fieldsForType) case 'Wildcard': return new MutableWildcard(module, fieldsForType) } bail(`Invalid type: ${type}`) } export function materialize(module: Module, fields: FixedMapView): Ast { const type = fields.get('type') const fields_ = fields as FixedMapView switch (type) { case 'App': return new App(module, fields_) case 'Assignment': return new Assignment(module, fields_) case 'BodyBlock': return new BodyBlock(module, fields_) case 'Documented': return new Documented(module, fields_) case 'Function': return new Function(module, fields_) case 'Generic': return new Generic(module, fields_) case 'Group': return new Group(module, fields_) case 'Ident': return new Ident(module, fields_) case 'Import': return new Import(module, fields_) case 'Invalid': return new Invalid(module, fields_) case 'NegationApp': return new NegationApp(module, fields_) case 'NumericLiteral': return new NumericLiteral(module, fields_) case 'OprApp': return new OprApp(module, fields_) case 'PropertyAccess': return new PropertyAccess(module, fields_) case 'TextLiteral': return new TextLiteral(module, fields_) case 'UnaryOprApp': return new UnaryOprApp(module, fields_) case 'AutoscopedIdentifier': return new AutoscopedIdentifier(module, fields_) case 'Vector': return new Vector(module, fields_) case 'Wildcard': return new Wildcard(module, fields_) } bail(`Invalid type: ${type}`) } export interface FixedMapView { get(key: Key): DeepReadonly /** @internal Unsafe. The caller must ensure the yielded values are not modified. */ entries(): IterableIterator clone(): FixedMap has(key: string): boolean toJSON(): object } export interface FixedMap extends FixedMapView { set(key: Key, value: Fields[Key]): void } type DeepReadonlyFields = { [K in keyof T]: DeepReadonly } function getAll(map: FixedMapView): DeepReadonlyFields { return Object.fromEntries(map.entries()) as DeepReadonlyFields } declare const brandLegalFieldContent: unique symbol /** Used to add a constraint to all `AstFields`s subtypes ensuring that they were produced by `composeFieldData`, which * enforces a requirement that the provided fields extend `FieldObject`. */ interface LegalFieldContent { [brandLegalFieldContent]: never } /** Modifies the input `map`. Returns the same object with an extended type. */ export function setAll>( map: FixedMap, fields: Fields2, ): FixedMap { const map_ = map as FixedMap for (const [k, v] of Object.entries(fields)) { const k_ = k as string & (keyof Fields1 | keyof Fields2) map_.set(k_, v as any) } return map_ } /** Modifies the input `map`. Returns the same object with an extended type. The added fields are required to have only * types extending `FieldData`; the returned object is branded as `LegalFieldContent`. */ export function composeFieldData>( map: FixedMap, fields: Fields2, ): FixedMap { return setAll(map, fields) as FixedMap } function claimChild( module: MutableModule, child: Owned, parent: AstId, ): AstId { if (child.module === module) assertEqual(child.fields.get('parent'), undefined) const child_ = module.copyIfForeign(child) child_.fields.set('parent', parent) return child_.id } function concreteChild( module: MutableModule, child: NodeChild, parent: AstId, ): NodeChild function concreteChild( module: MutableModule, child: NodeChild | undefined, parent: AstId, ): NodeChild | undefined function concreteChild( module: MutableModule, child: NodeChild, parent: AstId, ): NodeChild | NodeChild function concreteChild( module: MutableModule, child: NodeChild | undefined, parent: AstId, ): NodeChild | NodeChild | undefined function concreteChild( module: MutableModule, child: NodeChild | undefined, parent: AstId, ): NodeChild | NodeChild | undefined { if (!child) return undefined if (isTokenId(child.node)) return child as NodeChild return { ...child, node: claimChild(module, child.node, parent) } } type StrictIdentLike = Identifier | IdentifierToken function toIdentStrict(ident: StrictIdentLike): IdentifierToken function toIdentStrict(ident: StrictIdentLike | undefined): IdentifierToken | undefined function toIdentStrict(ident: StrictIdentLike | undefined): IdentifierToken | undefined { return ( ident ? isToken(ident) ? ident : (Token.new(ident, RawAst.Token.Type.Ident) as IdentifierToken) : undefined ) } type IdentLike = IdentifierOrOperatorIdentifier | IdentifierOrOperatorIdentifierToken function toIdent(ident: IdentLike): IdentifierOrOperatorIdentifierToken function toIdent(ident: IdentLike | undefined): IdentifierOrOperatorIdentifierToken | undefined function toIdent(ident: IdentLike | undefined): IdentifierOrOperatorIdentifierToken | undefined { return ( ident ? isToken(ident) ? ident : (Token.new(ident, RawAst.Token.Type.Ident) as IdentifierOrOperatorIdentifierToken) : undefined ) } function makeEquals(): Token { return Token.new('=', RawAst.Token.Type.Operator) } function nameSpecification( name: StrictIdentLike | undefined, ): { name: NodeChild; equals: NodeChild } | undefined { return name && { name: autospaced(toIdentStrict(name)), equals: unspaced(makeEquals()) } } type KeysOfFieldType = { [K in keyof Fields]: Fields[K] extends T ? K : never }[keyof Fields] function setNode>>( map: FixedMap, key: Key, node: AstId, ): void function setNode< Fields, Key extends string & KeysOfFieldType | undefined>, >(map: FixedMap, key: Key, node: AstId | undefined): void function setNode< Fields, Key extends string & KeysOfFieldType | undefined>, >(map: FixedMap, key: Key, node: AstId | undefined): void { // The signature correctly only allows this function to be called if `Fields[Key] instanceof NodeChild`, // but it doesn't prove that property to TSC, so we have to cast here. const old = map.get(key as string & keyof Fields) as DeepReadonly> const updated = old ? { ...old, node } : autospaced(node) map.set(key, updated as Fields[Key]) } function spaced(node: T): NodeChild function spaced(node: T | undefined): NodeChild | undefined function spaced(node: T | undefined): NodeChild | undefined { if (node === undefined) return node return { whitespace: ' ', node } } function unspaced(node: T): NodeChild function unspaced(node: T | undefined): NodeChild | undefined function unspaced(node: T | undefined): NodeChild | undefined { if (node === undefined) return node return { whitespace: '', node } } export function autospaced(node: T): NodeChild export function autospaced(node: T | undefined): NodeChild | undefined export function autospaced( node: T | undefined, ): NodeChild | undefined { if (node === undefined) return node return { whitespace: undefined, node } } export interface Removed { node: Owned placeholder: MutableWildcard | undefined }