diff --git a/src/assembler.ts b/src/assembler.ts index 2d9af90c..9adb2cb9 100644 --- a/src/assembler.ts +++ b/src/assembler.ts @@ -118,7 +118,7 @@ const compileModule = (opts: CompileExprOpts) => { const compileBlock = (opts: CompileExprOpts) => { return opts.mod.block( null, - opts.expr.body.toArray().map((expr, index, array) => { + opts.expr.body.map((expr, index, array) => { if (index === array.length - 1) { return compileExpression({ ...opts, expr, isReturnExpr: true }); } diff --git a/src/lib/fast-shift-array.ts b/src/lib/fast-shift-array.ts index 08887d20..fa3d1379 100644 --- a/src/lib/fast-shift-array.ts +++ b/src/lib/fast-shift-array.ts @@ -1,3 +1,5 @@ +import { at } from "vitest/dist/chunks/reporters.C_zwCd4j.js"; + // TODO: Add map, filter, reduce, amd findIndex export class FastShiftArray { private items: T[]; @@ -18,15 +20,6 @@ export class FastShiftArray { return value; } - unshift(...items: T[]): number { - if (items.length === 0) return this.length; - this.headIndex = Math.max(this.headIndex - items.length, 0); - for (let i = 0; i < items.length; i++) { - this.items[this.headIndex + i] = items[i]; - } - return this.length; - } - push(...items: T[]): number { this.items.push(...items); return this.length; @@ -37,6 +30,11 @@ export class FastShiftArray { return this.items.pop(); } + unshift(...items: T[]): number { + this.items.splice(this.headIndex, 0, ...items); + return this.length; + } + at(index: number): T | undefined { if (index < 0) { index = this.length + index; @@ -85,4 +83,8 @@ export class FastShiftArray { this.items.splice(0, this.headIndex); this.headIndex = 0; } + + forEach(callbackfn: (value: T, index: number, array: T[]) => void): void { + this.items.slice(this.headIndex).forEach(callbackfn); + } } diff --git a/src/parser/grammar.ts b/src/parser/grammar.ts index 1662db5c..59dedc90 100644 --- a/src/parser/grammar.ts +++ b/src/parser/grammar.ts @@ -90,7 +90,7 @@ export const isInfixOpIdentifier = (op?: Identifier) => export const isOp = (op?: Expr): boolean => isInfixOp(op) || isPrefixOp(op); export const prefixOps: OpMap = new Map([ - ["#", 7], + ["#", 0], ["&", 7], ["!", 7], ["~", 7], diff --git a/src/parser/parse-chars.ts b/src/parser/parse-chars.ts index b41a5290..af7b0535 100644 --- a/src/parser/parse-chars.ts +++ b/src/parser/parse-chars.ts @@ -31,7 +31,7 @@ export const parseChars = ( if (token.is("(")) { const subList = parseChars(file, { nested: true }); - subList.mayBeTuple = true; + subList.setAttribute("tuple?", true); list.push(subList); continue; } diff --git a/src/parser/reader-macros/comment.ts b/src/parser/reader-macros/comment.ts index 7fa82436..067f7769 100644 --- a/src/parser/reader-macros/comment.ts +++ b/src/parser/reader-macros/comment.ts @@ -1,4 +1,4 @@ -import { noop } from "../../syntax-objects/index.js"; +import { nop } from "../../syntax-objects/index.js"; import { ReaderMacro } from "./types.js"; export const comment: ReaderMacro = { @@ -9,6 +9,6 @@ export const comment: ReaderMacro = { file.consumeChar(); } - return noop(); + return nop(); }, }; diff --git a/src/parser/syntax-macros/functional-notation.ts b/src/parser/syntax-macros/functional-notation.ts index b64900d2..bbeff4e3 100644 --- a/src/parser/syntax-macros/functional-notation.ts +++ b/src/parser/syntax-macros/functional-notation.ts @@ -1,57 +1,87 @@ import { idIs, isOp } from "../grammar.js"; -import { Expr, List } from "../../syntax-objects/index.js"; +import { Expr, List, ListValue } from "../../syntax-objects/index.js"; export const functionalNotation = (list: List): List => { + const array = list.toArray(); let isTuple = false; - const result = list.mapFilter((expr, index, array) => { - if (expr.isList()) return functionalNotation(expr); - if (expr.isWhitespace()) return expr; + const { result } = array.reduce( + (acc, expr, index) => { + if (acc.skip > 0) { + acc.skip--; + return acc; + } - const nextExpr = array[index + 1]; - if (nextExpr && nextExpr.isList() && !(isOp(expr) || idIs(expr, ","))) { - return processFnCall(expr, nextExpr, array, index); - } + if (expr.isList()) { + acc.result.push(functionalNotation(expr)); + return acc; + } - if (list.mayBeTuple && idIs(expr, ",")) { - isTuple = true; - } + if (expr.isWhitespace()) { + acc.result.push(expr); + return acc; + } - return expr; - }); + const nextExpr = array[index + 1]; - if (isTuple) { - result.insert("tuple"); - result.insert(","); - } + if (nextExpr && nextExpr.isList() && !(isOp(expr) || idIs(expr, ","))) { + return handleNextExpression(acc, expr, nextExpr, array, index); + } - return result; + if (list.getAttribute("tuple?") && idIs(expr, ",")) { + isTuple = true; + } + + acc.result.push(expr); + return acc; + }, + { result: [], skip: 0 } as Accumulator + ); + + return finalizeResult(result, isTuple); }; -const processFnCall = ( +type Accumulator = { result: ListValue[]; skip: number }; + +const handleNextExpression = ( + acc: Accumulator, expr: Expr, - nextExpr: List, + nextExpr: Expr, array: Expr[], index: number -): List => { - if (nextExpr.calls("generics")) { - return processGenerics(expr, array, index); +) => { + if ((nextExpr as List).calls("generics")) { + const generics = nextExpr as List; + const nextNextExpr = array[index + 2]; + if (nextNextExpr && nextNextExpr.isList()) { + acc.result.push(processGenerics(expr, generics, nextNextExpr as List)); + acc.skip = 2; // Skip next two expressions + } else { + acc.result.push(processGenerics(expr, generics)); + acc.skip = 1; // Skip next expression + } + } else { + acc.result.push(processParamList(expr, nextExpr as List)); + acc.skip = 1; // Skip next expression } - - return processParamList(expr, array, index); + return acc; }; -const processGenerics = (expr: Expr, array: Expr[], index: number): List => { - const generics = array.splice(index + 1, 1)[0] as List; - generics.mayBeTuple = false; +const finalizeResult = (result: ListValue[], isTuple: boolean): List => { + if (isTuple) { + result.unshift(","); + result.unshift("tuple"); + } + return new List(result); +}; - const list = array[index + 1]?.isList() - ? (array.splice(index + 1, 1)[0] as List) - : new List({}); +const processGenerics = (expr: Expr, generics: List, params?: List): List => { + generics.setAttribute("tuple?", false); - list.insert(","); + const list = params || new List([]); list.insert(expr); - list.mayBeTuple = false; + list.insert(",", 1); + list.setAttribute("tuple?", false); const functional = functionalNotation(list); functional.insert(functionalNotation(generics), 2); @@ -59,10 +89,9 @@ const processGenerics = (expr: Expr, array: Expr[], index: number): List => { return functional; }; -const processParamList = (expr: Expr, array: Expr[], index: number): List => { - const list = array.splice(index + 1, 1)[0] as List; - list.insert(expr); - list.insert(",", 1); - list.mayBeTuple = false; - return functionalNotation(list); +const processParamList = (expr: Expr, params: List): List => { + params.insert(expr); + params.insert(",", 1); + params.setAttribute("tuple?", false); + return functionalNotation(params); }; diff --git a/src/semantics/__tests__/__snapshots__/regular-macros.test.ts.snap b/src/semantics/__tests__/__snapshots__/regular-macros.test.ts.snap index dbf51abe..8db56d23 100644 --- a/src/semantics/__tests__/__snapshots__/regular-macros.test.ts.snap +++ b/src/semantics/__tests__/__snapshots__/regular-macros.test.ts.snap @@ -7,7 +7,7 @@ exports[`regular macro expansion 1`] = ` [ "exports", [ - "std#730", + "std#729", ], ], [ @@ -47,7 +47,7 @@ exports[`regular macro expansion 1`] = ` ], [ "regular-macro", - "\`#759", + "\`#758", [ "parameters", ], @@ -68,7 +68,7 @@ exports[`regular macro expansion 1`] = ` ], [ "regular-macro", - "let#811", + "let#805", [ "parameters", ], @@ -121,7 +121,7 @@ exports[`regular macro expansion 1`] = ` ], [ "regular-macro", - "fn#1462", + "fn#1408", [ "parameters", ], diff --git a/src/semantics/check-types.ts b/src/semantics/check-types.ts index 66a5eded..0f70d851 100644 --- a/src/semantics/check-types.ts +++ b/src/semantics/check-types.ts @@ -1,7 +1,7 @@ import { List, Expr, - noop, + nop, Identifier, ObjectType, Type, @@ -22,7 +22,7 @@ import { getExprType } from "./resolution/get-expr-type.js"; import { typesAreEquivalent } from "./resolution/index.js"; export const checkTypes = (expr: Expr | undefined): Expr => { - if (!expr) return noop(); + if (!expr) return nop(); if (expr.isBlock()) return checkBlockTypes(expr); if (expr.isCall()) return checkCallTypes(expr); if (expr.isFn()) return checkFnTypes(expr); diff --git a/src/semantics/modules.ts b/src/semantics/modules.ts index fb50c814..d4a31284 100644 --- a/src/semantics/modules.ts +++ b/src/semantics/modules.ts @@ -51,7 +51,7 @@ const registerModule = ({ } if (!existingModule && (name === "index" || name === "mod")) { - parentModule.unshift(...ast.toArray()); + parentModule.unshift(...ast.toArray().reverse()); return; } @@ -73,7 +73,7 @@ const registerModule = ({ } if (existingModule && !rest.length) { - module.unshift(...ast.toArray()); + module.unshift(...ast.toArray().reverse()); } if (!rest.length) return; diff --git a/src/semantics/regular-macros.ts b/src/semantics/regular-macros.ts index b94d0d49..f44198ff 100644 --- a/src/semantics/regular-macros.ts +++ b/src/semantics/regular-macros.ts @@ -1,4 +1,4 @@ -import { getIdStr } from "../syntax-objects/get-id-str.js"; +import { getIdStr } from "../syntax-objects/lib/get-id-str.js"; import { Bool, Expr, @@ -14,6 +14,7 @@ import { VoidModule, Block, Use, + nop, } from "../syntax-objects/index.js"; import { registerExports, @@ -256,7 +257,7 @@ const functions: Record = { }, quote: (quote: List) => { const expand = (body: List): List => - body.mapFilter((exp) => { + body.flatMap((exp) => { if (exp.isList() && exp.calls("$")) { const val = exp.at(1) ?? nop(); return evalMacroExpr(val); @@ -264,7 +265,7 @@ const functions: Record = { if (exp.isList() && exp.calls("$@")) { const val = exp.at(1) ?? nop(); - return (evalMacroExpr(val) as List).insert("splice_quote"); + return (evalMacroExpr(val) as List).toArray(); } if (exp.isList()) return expand(exp); @@ -385,8 +386,6 @@ const handleOptionalConditionParenthesis = (expr: Expr): Expr => { return expr; }; -const nop = () => new List({}).push(Identifier.from("splice_quote")); - /** Binary logical comparison */ const bl = (args: List, fn: (l: any, r: any) => boolean) => { // TODO Assertions / validation diff --git a/src/semantics/resolution/resolve-fn-type.ts b/src/semantics/resolution/resolve-fn-type.ts index 09c2b475..ac0e8981 100644 --- a/src/semantics/resolution/resolve-fn-type.ts +++ b/src/semantics/resolution/resolve-fn-type.ts @@ -45,17 +45,14 @@ export const resolveFnTypes = (fn: Fn, call?: Call): Fn => { const resolveParameters = (params: Parameter[]) => { params.forEach((p) => { - if (p.type) { - return; - } + if (p.type) return; if (!p.typeExpr) { throw new Error(`Unable to determine type for ${p}`); } p.typeExpr = resolveTypes(p.typeExpr); - const type = getExprType(p.typeExpr); - p.type = type; + p.type = getExprType(p.typeExpr); }); }; diff --git a/src/semantics/resolution/resolve-types.ts b/src/semantics/resolution/resolve-types.ts index 3272b4ac..5a223751 100644 --- a/src/semantics/resolution/resolve-types.ts +++ b/src/semantics/resolution/resolve-types.ts @@ -1,6 +1,6 @@ import { Block } from "../../syntax-objects/block.js"; import { Expr } from "../../syntax-objects/expr.js"; -import { noop } from "../../syntax-objects/helpers.js"; +import { nop } from "../../syntax-objects/helpers.js"; import { List } from "../../syntax-objects/list.js"; import { VoidModule } from "../../syntax-objects/module.js"; import { ObjectLiteral } from "../../syntax-objects/object-literal.js"; @@ -22,7 +22,7 @@ import { resolveUse } from "./resolve-use.js"; * Returned tree not guaranteed to be same as supplied tree */ export const resolveTypes = (expr: Expr | undefined): Expr => { - if (!expr) return noop(); + if (!expr) return nop(); if (expr.isBlock()) return resolveBlockTypes(expr); if (expr.isCall()) return resolveCallTypes(expr); if (expr.isFn()) return resolveFnTypes(expr); diff --git a/src/semantics/resolution/resolve-use.ts b/src/semantics/resolution/resolve-use.ts index b38a443b..09a4afb7 100644 --- a/src/semantics/resolution/resolve-use.ts +++ b/src/semantics/resolution/resolve-use.ts @@ -139,7 +139,7 @@ export const resolveExport = (call: Call) => { const block = call.argAt(0); if (!block?.isBlock()) return call; - const entities = block.body.toArray().map(resolveTypes); + const entities = block.body.map(resolveTypes); registerExports(call, entities, resolveModuleTypes); return call; diff --git a/src/syntax-objects/README.md b/src/syntax-objects/README.md new file mode 100644 index 00000000..386530bb --- /dev/null +++ b/src/syntax-objects/README.md @@ -0,0 +1,19 @@ +# Syntax Objects + +Syntax objects are data structures that represent concepts within the +language. Such as functions, function calls, variables etc. + +# Guidelines + +- Each Syntax Object should be part of the `Expr` union +- Syntax objects must track their parent-child relationship. A child typically + belongs to a parent when it was directly defined with the parent. I.E. + parameters of functions, expressions / variables of block. These parent + relationships are stored via the parent pointer and must stay up to date + during clones. Use `ChildList` and `ChildMap` data types to help keep + make this easier. +- Resolved values should not be considered a child of the expression they + were resolved from. The type expression (`typeExpr`) of a parameter is + a child of the parameter, but the type it resolves to should never + accidentally be marked as a child of the parameter. Nor should it be + included in the clone (instead, they type resolution can be run again) diff --git a/src/syntax-objects/block.ts b/src/syntax-objects/block.ts index 12a29b2c..c058d5f7 100644 --- a/src/syntax-objects/block.ts +++ b/src/syntax-objects/block.ts @@ -1,11 +1,12 @@ import { Expr } from "./expr.js"; +import { ChildList } from "./lib/child-list.js"; import { List } from "./list.js"; import { ScopedSyntax, ScopedSyntaxMetadata } from "./scoped-entity.js"; import { Type } from "./types.js"; export class Block extends ScopedSyntax { readonly syntaxType = "block"; - private _body!: List; + #body = new ChildList(undefined, this); type?: Type; constructor( @@ -15,57 +16,51 @@ export class Block extends ScopedSyntax { } ) { super(opts); - this.body = - opts.body instanceof Array ? new List({ value: opts.body }) : opts.body; - this.type = opts.type; + const { body, type } = opts; + this.#body.push(...(body instanceof Array ? body : body.toArray())); + this.type = type; } get children() { - return this.body.toArray(); + return this.#body.toArray(); } get body() { - return this._body; + return this.#body.toArray(); } - set body(body: List) { - if (body) { - body.parent = this; - } - - this._body = body; + set body(body: Expr[]) { + this.#body = new ChildList(body, this); } lastExpr() { - return this.body.last(); + return this.body.at(-1); } each(fn: (expr: Expr, index: number, array: Expr[]) => Expr) { - this.body.each(fn); + this.body.forEach(fn); return this; } /** Sets the parent on each element immediately before the mapping of the next */ applyMap(fn: (expr: Expr, index: number, array: Expr[]) => Expr) { - const body = this.body; - this.body = new List({ ...this.body.metadata, value: [] }); - body.each((expr, index, array) => this.body.push(fn(expr, index, array))); + this.#body.applyMap(fn); return this; } /** Calls the evaluator function on the block's body and returns the result of the last evaluation. */ evaluate(evaluator: (expr: Expr) => Expr): Expr | undefined { - return this.body.map(evaluator).last(); + return this.body.map(evaluator).at(-1); } toJSON() { - return ["block", ...this.body.toJSON()]; + return ["block", ...this.body]; } clone(parent?: Expr) { return new Block({ ...this.getCloneOpts(parent), - body: this.body.clone(), + body: this.#body.toClonedArray(), }); } } diff --git a/src/syntax-objects/call.ts b/src/syntax-objects/call.ts index 7005df0e..81a66b08 100644 --- a/src/syntax-objects/call.ts +++ b/src/syntax-objects/call.ts @@ -1,7 +1,7 @@ import { Expr } from "./expr.js"; import { Fn } from "./fn.js"; import { Identifier } from "./identifier.js"; -import { LexicalContext } from "./lexical-context.js"; +import { LexicalContext } from "./lib/lexical-context.js"; import { List } from "./list.js"; import { Syntax, SyntaxMetadata } from "./syntax.js"; import { ObjectType, Type } from "./types.js"; @@ -13,7 +13,7 @@ export class Call extends Syntax { fnName: Identifier; args: List; typeArgs?: List; - _type?: Type; + #type?: Type; constructor( opts: SyntaxMetadata & { @@ -32,7 +32,7 @@ export class Call extends Syntax { this.args.parent = this; this.typeArgs = opts.typeArgs; if (this.typeArgs) this.typeArgs.parent = this; - this._type = opts.type; + this.#type = opts.type; } get children() { @@ -40,19 +40,19 @@ export class Call extends Syntax { } set type(type: Type | undefined) { - this._type = type; + this.#type = type; } get type() { - if (!this._type && this.fn?.isFn()) { - this._type = this.fn.returnType; + if (!this.#type && this.fn?.isFn()) { + this.#type = this.fn.returnType; } - if (!this._type && this.fn?.isObjectType()) { - this._type = this.fn; + if (!this.#type && this.fn?.isObjectType()) { + this.#type = this.fn; } - return this._type; + return this.#type; } eachArg(fn: (expr: Expr) => void) { diff --git a/src/syntax-objects/expr.ts b/src/syntax-objects/expr.ts index ba686fc0..ea452baa 100644 --- a/src/syntax-objects/expr.ts +++ b/src/syntax-objects/expr.ts @@ -20,6 +20,7 @@ import { Declaration } from "./declaration.js"; import { Use } from "./use.js"; import { ObjectLiteral } from "./object-literal.js"; import { Match } from "./match.js"; +import { Nop } from "./nop.js"; export type Expr = | PrimitiveExpr @@ -37,7 +38,8 @@ export type Expr = | Declaration | Use | ObjectLiteral - | Match; + | Match + | Nop; /** * These are the Expr types that must be returned until all macros have been expanded (reader, syntax, and regular) diff --git a/src/syntax-objects/fn.ts b/src/syntax-objects/fn.ts index 636c8a0a..6b5dabbf 100644 --- a/src/syntax-objects/fn.ts +++ b/src/syntax-objects/fn.ts @@ -1,5 +1,7 @@ import type { Expr } from "./expr.js"; import { Identifier } from "./identifier.js"; +import { ChildList } from "./lib/child-list.js"; +import { Child } from "./lib/child.js"; import { ScopedNamedEntity, ScopedNamedEntityOpts } from "./named-entity.js"; import { Parameter } from "./parameter.js"; import { FnType, Type } from "./types.js"; @@ -7,83 +9,72 @@ import { Variable } from "./variable.js"; export class Fn extends ScopedNamedEntity { readonly syntaxType = "fn"; + readonly #parameters = new ChildList([], this); + readonly #body = new Child(undefined, this); + readonly #returnTypeExpr = new Child(undefined, this); + readonly #genericInstances = new ChildList([], this); + #typeParams = new ChildList([], this); variables: Variable[] = []; - _parameters: Parameter[] = []; - typeParameters?: Identifier[]; - appliedTypeArgs?: Type[] = []; - /** When a function has generics, resolved versions of the functions go here */ - genericInstances?: Fn[] = []; - returnType?: Type; - _returnTypeExpr?: Expr; + returnType?: Type; // When a function has generics, resolved versions of the functions go here inferredReturnType?: Type; annotatedReturnType?: Type; + appliedTypeArgs?: Type[] = []; typesResolved?: boolean; - private _body?: Expr; constructor( opts: ScopedNamedEntityOpts & { - returnType?: Type; returnTypeExpr?: Expr; variables?: Variable[]; - parameters: Parameter[]; + parameters?: Parameter[]; typeParameters?: Identifier[]; - genericInstances?: Fn[]; body?: Expr; } ) { super(opts); - this.returnType = opts.returnType; - this.parameters = opts.parameters ?? []; - this.variables = opts.variables ?? []; - this.typeParameters = opts.typeParameters; - this.genericInstances = opts.genericInstances; + this.#parameters.push(...(opts.parameters ?? [])); + this.#typeParams.push(...(opts.typeParameters ?? [])); this.returnTypeExpr = opts.returnTypeExpr; + this.variables = opts.variables ?? []; this.body = opts.body; } get body() { - return this._body; + return this.#body.value; } set body(body: Expr | undefined) { - if (body) { - body.parent = this; - } - - this._body = body; + this.#body.value = body; } get parameters() { - return this._parameters; - } - - set parameters(parameters: Parameter[]) { - this._parameters = parameters; - parameters.forEach((p) => { - p.parent = this; - this.registerEntity(p); - }); + return this.#parameters.toArray(); } get returnTypeExpr() { - return this._returnTypeExpr; + return this.#returnTypeExpr.value; } set returnTypeExpr(returnTypeExpr: Expr | undefined) { - if (returnTypeExpr) { - returnTypeExpr.parent = this; - } + this.#returnTypeExpr.value = returnTypeExpr; + } - this._returnTypeExpr = returnTypeExpr; + get genericInstances() { + const instances = this.#genericInstances.toArray(); + return !instances.length ? undefined : instances; + } + + get typeParameters() { + const params = this.#typeParams.toArray(); + return !params.length ? undefined : params; + } + + set typeParameters(params: Identifier[] | undefined) { + this.#typeParams = new ChildList(params ?? [], this); } // Register a version of this function with resolved generics registerGenericInstance(fn: Fn) { - if (!this.genericInstances) { - this.genericInstances = []; - } - - this.genericInstances.push(fn); + this.#genericInstances.push(fn); } getNameStr(): string { @@ -127,15 +118,6 @@ export class Fn extends ScopedNamedEntity { ); } - registerLocal(local: Variable | Parameter) { - if (local.syntaxType === "variable") { - this.variables.push(local); - return; - } - - this.parameters.push(local); - } - toString() { return this.id; } @@ -144,11 +126,11 @@ export class Fn extends ScopedNamedEntity { // Don't clone generic instances return new Fn({ ...super.getCloneOpts(parent), - variables: this.variables.map((v) => v.clone()), - parameters: this.parameters.map((p) => p.clone()), + variables: this.variables, returnTypeExpr: this.returnTypeExpr?.clone(), + parameters: this.#parameters.toClonedArray(), + typeParameters: this.#typeParams.toClonedArray(), body: this.body?.clone(), - typeParameters: this.typeParameters?.map((tp) => tp.clone()), }); } @@ -157,7 +139,7 @@ export class Fn extends ScopedNamedEntity { "fn", this.id, ["parameters", ...this.parameters], - ["type-parameters", ...(this.typeParameters ?? [])], + ["type-parameters", ...(this.#typeParams.toArray() ?? [])], ["return-type", this.returnType], this.body, ]; diff --git a/src/syntax-objects/helpers.ts b/src/syntax-objects/helpers.ts index 72dfeb78..60de5b79 100644 --- a/src/syntax-objects/helpers.ts +++ b/src/syntax-objects/helpers.ts @@ -1,5 +1,15 @@ -import { List } from "./list.js"; +import { Nop } from "./nop.js"; import { Whitespace } from "./whitespace.js"; export const newLine = () => new Whitespace({ value: "\n" }); -export const noop = () => new List({ value: ["splice_quote"] }); + +let nopCache: Nop | undefined = undefined; +export const nop = () => { + if (!nopCache) { + const n = new Nop({}); + nopCache = n; + return n; + } + + return nopCache; +}; diff --git a/src/syntax-objects/identifier.ts b/src/syntax-objects/identifier.ts index 6b0c21f7..d528e5ef 100644 --- a/src/syntax-objects/identifier.ts +++ b/src/syntax-objects/identifier.ts @@ -75,17 +75,17 @@ export class Identifier extends Syntax { } export class MockIdentifier extends Identifier { - private readonly _entity?: NamedEntity; + readonly #entity?: NamedEntity; constructor( opts: IdentifierOpts & { entity?: NamedEntity; // The entity this identifier resolves to } ) { super(opts); - this._entity = opts.entity; + this.#entity = opts.entity; } resolve() { - return this._entity; + return this.#entity; } } diff --git a/src/syntax-objects/lib/child-list.ts b/src/syntax-objects/lib/child-list.ts new file mode 100644 index 00000000..9f0c430a --- /dev/null +++ b/src/syntax-objects/lib/child-list.ts @@ -0,0 +1,128 @@ +import { FastShiftArray } from "../../lib/fast-shift-array.js"; +import { Expr } from "../expr.js"; +import { NamedEntity } from "../named-entity.js"; + +export class ChildList { + private store: FastShiftArray = new FastShiftArray(); + #parent: Expr; + + constructor(children: T[] = [], parent: Expr) { + this.#parent = parent; + this.push(...children); + } + + get parent() { + return this.#parent; + } + + set parent(parent: Expr) { + this.#parent = parent; + this.store.forEach((expr) => { + expr.parent = parent; + }); + } + + get children() { + return this.store.toArray(); + } + + get hasChildren() { + return !!this.store.length; + } + + get length() { + return this.store.length; + } + + private registerExpr(expr: T) { + expr.parent = this.parent; + if (expr instanceof NamedEntity) { + this.parent.registerEntity(expr); + } + } + + at(index: number): Expr | undefined { + return this.store.at(index); + } + + set(index: number, expr: T) { + this.registerExpr(expr); + this.store.set(index, expr); + return this; + } + + consume(): T { + const next = this.store.shift(); + if (!next) throw new Error("No remaining expressions"); + return next; + } + + /** Returns all but the first element in an array */ + argsArray(): T[] { + return this.store.toArray().slice(1); + } + + pop(): T | undefined { + return this.store.pop(); + } + + push(...expr: T[]) { + expr.forEach((ex) => { + this.registerExpr(ex); + this.store.push(ex); + }); + return this; + } + + insert(expr: T, at = 0) { + this.registerExpr(expr); + this.store.splice(at, 0, expr); + return this; + } + + remove(index: number, count = 1) { + this.store.splice(index, count); + return this; + } + + each(fn: (expr: T, index: number, array: T[]) => void): ChildList { + this.toArray().forEach(fn); + return this; + } + + applyMap(fn: (expr: T, index: number, array: T[]) => T): ChildList { + this.store.forEach((expr, index, array) => { + this.set(index, fn(expr, index, array)); + }); + return this; + } + + slice(start?: number, end?: number): T[] { + return this.store.slice(start, end); + } + + shift(): T | undefined { + return this.store.shift(); + } + + unshift(...expr: T[]) { + expr.forEach((ex) => { + this.registerExpr(ex); + this.store.unshift(ex); + }); + + return this; + } + + toArray(): T[] { + return this.store.toArray(); + } + + toClonedArray(): T[] { + return this.toArray().map((expr: T): T => expr.clone() as T); + } + + toJSON() { + return this.toArray(); + } +} diff --git a/src/syntax-objects/lib/child.ts b/src/syntax-objects/lib/child.ts new file mode 100644 index 00000000..7082f129 --- /dev/null +++ b/src/syntax-objects/lib/child.ts @@ -0,0 +1,44 @@ +import { Expr } from "../expr.js"; +import { NamedEntity } from "../named-entity.js"; + +/** Use to store a child of a syntax object. Will keep track of parent */ +export class Child { + #value: T; + #parent: Expr; + + constructor(value: T, parent: Expr) { + this.#parent = parent; + this.#value = value; + if (this.#value) this.#value.parent = parent; + } + + get parent() { + return this.#parent; + } + + set parent(parent: Expr) { + this.#parent = parent; + if (this.#value) this.#value.parent = parent; + } + + get value() { + return this.#value; + } + + set value(value: T) { + if (value) { + if (value instanceof NamedEntity) this.#parent.registerEntity(value); + value.parent = this.#parent; + } + + this.#value = value; + } + + toJSON() { + return this.#value?.toJSON(); + } + + clone(parent?: Expr) { + return new Child(this.#value?.clone(parent), parent ?? this.#parent); + } +} diff --git a/src/syntax-objects/get-id-str.ts b/src/syntax-objects/lib/get-id-str.ts similarity index 77% rename from src/syntax-objects/get-id-str.ts rename to src/syntax-objects/lib/get-id-str.ts index dd54decd..837754da 100644 --- a/src/syntax-objects/get-id-str.ts +++ b/src/syntax-objects/lib/get-id-str.ts @@ -1,4 +1,4 @@ -import { Id } from "./identifier.js"; +import { Id } from "../identifier.js"; export const getIdStr = (id: Id) => { if (!id) { diff --git a/src/syntax-objects/lexical-context.ts b/src/syntax-objects/lib/lexical-context.ts similarity index 90% rename from src/syntax-objects/lexical-context.ts rename to src/syntax-objects/lib/lexical-context.ts index ecb7eb0d..27825ffa 100644 --- a/src/syntax-objects/lexical-context.ts +++ b/src/syntax-objects/lib/lexical-context.ts @@ -1,7 +1,7 @@ -import type { Fn } from "./fn.js"; +import type { Fn } from "../fn.js"; import { getIdStr } from "./get-id-str.js"; -import type { Id } from "./identifier.js"; -import { NamedEntity } from "./named-entity.js"; +import type { Id } from "../identifier.js"; +import { NamedEntity } from "../named-entity.js"; export class LexicalContext { private readonly fns: Map = new Map(); diff --git a/src/syntax-objects/list.ts b/src/syntax-objects/list.ts index 47d2e603..2e9db1a1 100644 --- a/src/syntax-objects/list.ts +++ b/src/syntax-objects/list.ts @@ -1,57 +1,52 @@ -import { FastShiftArray } from "../lib/fast-shift-array.js"; import { Expr } from "./expr.js"; import { Float } from "./float.js"; -import { getIdStr } from "./get-id-str.js"; +import { getIdStr } from "./lib/get-id-str.js"; import { Id, Identifier } from "./identifier.js"; import { Int } from "./int.js"; -import { NamedEntity } from "./named-entity.js"; import { Syntax, SyntaxMetadata } from "./syntax.js"; +import { ChildList } from "./lib/child-list.js"; + +type ListOpts = + | ListValue[] + | (SyntaxMetadata & { + value?: ListValue[] | List | ChildList; + isParentheticalList?: boolean; + }); export class List extends Syntax { readonly syntaxType = "list"; - /** True when the list was defined by the user using parenthesis i.e. (hey, there) */ - mayBeTuple?: boolean; - store: FastShiftArray = new FastShiftArray(); - - constructor( - opts: - | ListValue[] - | (SyntaxMetadata & { - value?: ListValue[] | List; - isParentheticalList?: boolean; - }) - ) { + #store = new ChildList(undefined, this); + + constructor(opts: ListOpts) { opts = Array.isArray(opts) ? { value: opts } : opts; super(opts); - const value = opts.value; - this.mayBeTuple = opts.isParentheticalList; if (!value || value instanceof Array) { this.push(...(value ?? [])); - } else { + } else if (value instanceof List) { this.push(...value.toArray()); } } get children() { - return this.store.toArray(); + return this.toArray(); } get hasChildren() { - return !!this.store.length; + return !!this.#store.length; } get length() { - return this.store.length; + return this.#store.length; } at(index: number): Expr | undefined { - return this.store.at(index); + return this.#store.at(index); } exprAt(index: number): Expr { - const expr = this.store.at(index); + const expr = this.#store.at(index); if (!expr) { throw new Error(`No expr at ${index}`); } @@ -83,8 +78,7 @@ export class List extends Syntax { set(index: number, expr: Expr | string) { const result = typeof expr === "string" ? Identifier.from(expr) : expr; - result.parent = this; - this.store.set(index, result); + this.#store.set(index, result); return this; } @@ -98,42 +92,40 @@ export class List extends Syntax { } consume(): Expr { - const next = this.store.shift(); - if (!next) throw new Error("No remaining expressions"); - return next; + return this.#store.consume(); } first(): Expr | undefined { - return this.store.at(0); + return this.#store.at(0); } last(): Expr | undefined { - return this.store.at(-1); + return this.#store.at(-1); } /** Returns all but the first element in an array */ argsArray(): Expr[] { - return this.store.toArray().slice(1); + return this.#store.toArray().slice(1); } pop(): Expr | undefined { - return this.store.pop(); + return this.#store.pop(); } push(...expr: ListValue[]) { expr.forEach((ex) => { if (typeof ex === "string") { - this.store.push(new Identifier({ value: ex, parent: this })); + this.#store.push(new Identifier({ value: ex, parent: this })); return; } if (typeof ex === "number" && Number.isInteger(ex)) { - this.store.push(new Int({ value: ex, parent: this })); + this.#store.push(new Int({ value: ex, parent: this })); return; } if (typeof ex === "number") { - this.store.push(new Float({ value: ex, parent: this })); + this.#store.push(new Float({ value: ex, parent: this })); return; } @@ -142,18 +134,11 @@ export class List extends Syntax { return; } - ex.parent = this; - - if (ex instanceof NamedEntity) { - this.registerEntity(ex); - } - - if (ex.isList() && ex.calls("splice_quote")) { - this.store.push(...ex.argsArray()); + if (ex.syntaxType === "nop") { return; } - this.store.push(ex); + this.#store.push(ex); }); return this; @@ -165,23 +150,15 @@ export class List extends Syntax { insert(expr: Expr | string, at = 0) { const result = typeof expr === "string" ? Identifier.from(expr) : expr; - result.parent = this; - this.store.splice(at, 0, result); + this.#store.insert(result, at); return this; } remove(index: number, count = 1) { - this.store.splice(index, count); + this.#store.remove(index, count); return this; } - filter(fn: (expr: Expr, index: number, array: Expr[]) => boolean): List { - return new List({ - ...super.getCloneOpts(), - value: this.toArray().filter(fn), - }); - } - each(fn: (expr: Expr, index: number, array: Expr[]) => void): List { this.toArray().forEach(fn); return this; @@ -194,32 +171,34 @@ export class List extends Syntax { }); } - /** Like a regular map, but omits undefined values returned from the mapper */ - mapFilter( - fn: (expr: Expr, index: number, array: Expr[]) => Expr | undefined + flatMap( + fn: (expr: Expr, index: number, array: Expr[]) => Expr | Expr[] + ): List { + return new List({ + ...super.getCloneOpts(), + value: this.toArray().flatMap(fn), + }); + } + + reduce( + fn: (acc: List, expr: Expr, index: number, array: Expr[]) => List ): List { - const list = new List({ ...super.getCloneOpts() }); - return this.toArray().reduce((newList: List, expr, index, array) => { - if (!expr) return newList; - const result = fn(expr, index, array); - if (!result) return newList; - return newList.push(result); - }, list); + return this.toArray().reduce(fn, new List({ ...super.getCloneOpts() })); } slice(start?: number, end?: number): List { return new List({ ...super.getCloneOpts(), - value: this.store.slice(start, end), + value: this.#store.slice(start, end), }); } sliceAsArray(start?: number, end?: number) { - return this.store.slice(start, end); + return this.children.slice(start, end); } toArray(): Expr[] { - return this.store.toArray(); + return this.#store.toArray(); } toJSON() { @@ -230,8 +209,7 @@ export class List extends Syntax { clone(parent?: Expr): List { return new List({ ...super.getCloneOpts(parent), - value: this.toArray().map((v) => v.clone()), - isParentheticalList: this.mayBeTuple, + value: this.#store.toClonedArray(), }); } } diff --git a/src/syntax-objects/module.ts b/src/syntax-objects/module.ts index e6ccd263..48ad4340 100644 --- a/src/syntax-objects/module.ts +++ b/src/syntax-objects/module.ts @@ -1,9 +1,7 @@ import { Expr } from "./expr.js"; -import { Float } from "./float.js"; -import { Id, Identifier } from "./identifier.js"; -import { Int } from "./int.js"; -import { LexicalContext } from "./lexical-context.js"; -import { List, ListValue } from "./list.js"; +import { Id } from "./identifier.js"; +import { ChildList } from "./lib/child-list.js"; +import { LexicalContext } from "./lib/lexical-context.js"; import { NamedEntity, ScopedNamedEntity, @@ -11,7 +9,7 @@ import { } from "./named-entity.js"; export type VoidModuleOpts = ScopedNamedEntityOpts & { - value?: ListValue[]; + value?: Expr[]; phase?: number; isIndex?: boolean; exports?: LexicalContext; @@ -23,7 +21,7 @@ export class VoidModule extends ScopedNamedEntity { readonly isRoot: boolean = false; /** This module is the entry point of the user src code */ isIndex = false; - value: Expr[] = []; + #value = new ChildList(undefined, this); /** * 0 = init, * 1 = expanding regular macros, @@ -41,6 +39,15 @@ export class VoidModule extends ScopedNamedEntity { this.isIndex = opts.isIndex ?? false; } + get value() { + return this.#value.toArray(); + } + + set value(value: Expr[]) { + this.#value = new ChildList(undefined, this); + this.push(...value); + } + registerExport(entity: NamedEntity, alias?: string) { this.exports.registerEntity(entity, alias); } @@ -104,50 +111,13 @@ export class VoidModule extends ScopedNamedEntity { ]; } - push(...expr: ListValue[]) { - expr.forEach((ex) => { - if (typeof ex === "string") { - this.value.push(new Identifier({ value: ex, parent: this })); - return; - } - - if (ex instanceof Array) { - this.push(new List({ value: ex, parent: this })); - return; - } - - if (typeof ex === "number" && Number.isInteger(ex)) { - this.value.push(new Int({ value: ex, parent: this })); - return; - } - - if (typeof ex === "number") { - this.value.push(new Float({ value: ex, parent: this })); - return; - } - - ex.parent = this; - - if (ex instanceof NamedEntity) { - this.registerEntity(ex); - } - - if (ex.isList() && ex.calls("splice_quote")) { - this.value.push(...ex.argsArray()); - return; - } - - this.value.push(ex); - }); - + push(...expr: Expr[]) { + this.#value.push(...expr); return this; } - unshift(...expr: ListValue[]) { - const old = this.value; - this.value = []; - this.push(...expr); - this.push(...old); + unshift(...expr: Expr[]) { + this.#value.unshift(...expr); return this; } } diff --git a/src/syntax-objects/named-entity.ts b/src/syntax-objects/named-entity.ts index ea8e8b9e..a8698de9 100644 --- a/src/syntax-objects/named-entity.ts +++ b/src/syntax-objects/named-entity.ts @@ -1,6 +1,6 @@ import { Expr } from "./expr.js"; import { Id, Identifier } from "./identifier.js"; -import { LexicalContext } from "./lexical-context.js"; +import { LexicalContext } from "./lib/lexical-context.js"; import { Syntax, SyntaxMetadata } from "./syntax.js"; export type NamedEntityOpts = SyntaxMetadata & { diff --git a/src/syntax-objects/nop.ts b/src/syntax-objects/nop.ts new file mode 100644 index 00000000..56747080 --- /dev/null +++ b/src/syntax-objects/nop.ts @@ -0,0 +1,18 @@ +import { Expr } from "./expr.js"; +import { Syntax, SyntaxMetadata } from "./syntax.js"; + +export class Nop extends Syntax { + readonly syntaxType = "nop"; + + constructor(opts: SyntaxMetadata) { + super(opts); + } + + clone(parent?: Expr): Nop { + return this; + } + + toJSON() { + return "nop"; + } +} diff --git a/src/syntax-objects/scoped-entity.ts b/src/syntax-objects/scoped-entity.ts index 33d3a073..afcf32a6 100644 --- a/src/syntax-objects/scoped-entity.ts +++ b/src/syntax-objects/scoped-entity.ts @@ -1,5 +1,5 @@ import { Expr } from "./expr.js"; -import { LexicalContext } from "./lexical-context.js"; +import { LexicalContext } from "./lib/lexical-context.js"; import { Syntax, SyntaxMetadata } from "./syntax.js"; export type ScopedEntity = Expr & { diff --git a/src/syntax-objects/syntax.ts b/src/syntax-objects/syntax.ts index 07580c97..27dd1d6f 100644 --- a/src/syntax-objects/syntax.ts +++ b/src/syntax-objects/syntax.ts @@ -8,7 +8,7 @@ import type { Global } from "./global.js"; import type { Id, Identifier } from "./identifier.js"; import type { Int } from "./int.js"; import { type VoidModule } from "./module.js"; -import { LexicalContext } from "./lexical-context.js"; +import { LexicalContext } from "./lib/lexical-context.js"; import type { List } from "./list.js"; import type { MacroLambda } from "./macro-lambda.js"; import type { MacroVariable } from "./macro-variable.js"; @@ -32,15 +32,19 @@ import { Declaration } from "./declaration.js"; import { Use } from "./use.js"; import { Match } from "./match.js"; +export type Attributes = { [key: string]: unknown }; + export type SyntaxMetadata = { location?: SourceLocation; parent?: Expr; + attributes?: Attributes; }; export abstract class Syntax { /** For tagged unions */ abstract readonly syntaxType: string; readonly syntaxId = getSyntaxId(); + #attributes?: Attributes; location?: SourceLocation; parent?: Expr; @@ -48,6 +52,7 @@ export abstract class Syntax { const { location, parent } = metadata; this.location = location; this.parent = parent; + this.#attributes = metadata.attributes; } get parentFn(): Fn | undefined { @@ -62,6 +67,7 @@ export abstract class Syntax { return { location: this.location, parent: this.parent, + attributes: this.#attributes ? { ...this.#attributes } : undefined, }; } @@ -126,6 +132,21 @@ export abstract class Syntax { /** Should emit in compliance with core language spec */ abstract toJSON(): unknown; + setAttribute(key: string, value: unknown) { + if (!this.#attributes) this.#attributes = {}; + this.#attributes[key] = value; + } + + getAttribute(key: string): unknown { + if (!this.#attributes) return undefined; + return this.#attributes[key]; + } + + hasAttribute(key: string): boolean { + if (!this.#attributes) return false; + return this.#attributes[key] !== undefined; + } + isScopedEntity(): this is ScopedEntity { return (this as unknown as ScopedEntity).lexicon instanceof LexicalContext; } diff --git a/src/syntax-objects/types.ts b/src/syntax-objects/types.ts index d05ae591..96efeeba 100644 --- a/src/syntax-objects/types.ts +++ b/src/syntax-objects/types.ts @@ -2,8 +2,8 @@ import { Expr } from "./expr.js"; import { Parameter } from "./parameter.js"; import { NamedEntity, NamedEntityOpts } from "./named-entity.js"; import { Id, Identifier } from "./identifier.js"; -import { getIdStr } from "./get-id-str.js"; -import { LexicalContext } from "./lexical-context.js"; +import { getIdStr } from "./lib/get-id-str.js"; +import { LexicalContext } from "./lib/lexical-context.js"; export type Type = | PrimitiveType