From 58475269ad38859b0aa73470178bb2c832680496 Mon Sep 17 00:00:00 2001 From: Matt Fysh Date: Thu, 28 Aug 2025 14:57:58 +1000 Subject: [PATCH 1/7] new tree walker (wip) --- bun.lockb | Bin 89696 -> 90064 bytes packages/get/src/execute.ts | 321 +++++++++--------- packages/get/src/modifiers.ts | 22 +- packages/get/src/value.ts | 14 +- packages/parser/src/ast/ast.ts | 266 ++++++++------- packages/parser/src/ast/print.ts | 202 +++++------ packages/parser/src/grammar.ts | 6 +- packages/parser/src/grammar/getlang.ne | 7 +- packages/parser/src/grammar/parse.ts | 59 ++-- packages/parser/src/passes/analyze.ts | 46 +-- packages/parser/src/passes/desugar.ts | 28 +- packages/parser/src/passes/desugar/context.ts | 113 +++--- packages/parser/src/passes/desugar/links.ts | 12 +- .../parser/src/passes/desugar/reqparse.ts | 54 +-- .../parser/src/passes/desugar/slicedeps.ts | 36 +- packages/parser/src/passes/inference/calls.ts | 43 +-- .../parser/src/passes/inference/typeinfo.ts | 249 +++++++------- packages/parser/src/utils.ts | 32 +- packages/parser/src/visitor/visitor.ts | 3 +- packages/walker/package.json | 21 ++ packages/walker/src/index.ts | 53 +++ packages/walker/src/path.ts | 56 +++ packages/walker/src/scope.ts | 100 ++++++ packages/walker/src/visitor.ts | 27 ++ packages/walker/tsconfig.json | 3 + test/values.spec.ts | 12 +- 26 files changed, 1010 insertions(+), 775 deletions(-) create mode 100644 packages/walker/package.json create mode 100644 packages/walker/src/index.ts create mode 100644 packages/walker/src/path.ts create mode 100644 packages/walker/src/scope.ts create mode 100644 packages/walker/src/visitor.ts create mode 100644 packages/walker/tsconfig.json diff --git a/bun.lockb b/bun.lockb index 820b891ab58aa63557265e184cd55dd65b295866..fde1cf8cc8ef36cffda79a590dc4908ff434626f 100755 GIT binary patch delta 12364 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 XESFDYlRA{c+2kLGz1G1;ty}g#1YbXJ delta 12401 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, ) { - async function executeBody(visit: (stmt: Stmt) => void, body: Stmt[]) { - scope.push() - for (const stmt of body) { - await visit(stmt) - if (stmt.kind === NodeKind.ExtractStmt) { - break - } - } - return scope.pop() - } - const executeModule: Execute = async (entry, inputs) => { const provided = new Set(Object.keys(inputs)) const unknown = provided.difference(entry.inputs) invariant(unknown.size === 0, new UnknownInputsError([...unknown])) - scope.push() - let ex: any - await visit>(entry.program, { + const scope = new ScopeTracker() + + // function withItemContext(cb) { + // const ctx = scope.context + // if (ctx?.typeInfo.type === Type.List) { + // scope.push() + // const list = waitMap(ctx.data, item => { + // scope.context = { data: item, typeInfo: ctx.typeInfo.of } + // return withItemContext(cb) + // }) + // return wait(list, list => { + // scope.pop() + // return list + // }) + // } + // return cb() + // } + + const visitor: WalkOptions = { + scope, + + /** + * Statement nodes + */ + + InputDeclStmt: { + async enter(node, visit) { + const inputName = node.id.value + let inputValue = inputs[inputName] + if (inputValue === undefined) { + if (!node.optional) { + throw new NullInputError(inputName) + } + inputValue = node.defaultValue + ? await visit(node.defaultValue) + : new NullSelection(`input:${inputName}`) + } + scope.vars[inputName] = inputValue + }, + }, + + ExtractStmt(node) { + assert(node.value) + }, + + Program() { + return scope.extracted + }, + /** * Expression nodes */ - TemplateExpr(node, path, origNode) { + TemplateExpr(node, { node: orig }) { const firstNull = node.elements.find(el => el instanceof NullSelection) if (firstNull) { const parents = path.slice(0, -1) - const isRoot = !parents.find(n => n.kind === NodeKind.TemplateExpr) + const isRoot = !parents.find(n => n.kind === 'TemplateExpr') return isRoot ? firstNull : '' } const els = node.elements.map((el, i) => { - const og = origNode.elements[i]! + const og = orig.elements[i]! return isToken(og) ? og.value : toValue(el, og.typeInfo) }) return els.join('') }, - SliceExpr: { - async enter(node, visit) { - return withContext(scope, node, visit, async context => { - const { slice } = node - try { - const deps = context && toValue(context.value, context.typeInfo) - const value = await hooks.slice(slice.value, deps) - const ret = - value === undefined ? new NullSelection('') : value - const optional = node.typeInfo.type === Type.Maybe - return optional ? ret : assert(ret) - } catch (e) { - throw new SliceError({ cause: e }) - } - }) - }, + async SliceExpr({ slice, typeInfo }) { + try { + const ctx = scope.context + const deps = ctx && toValue(ctx.data, ctx.typeInfo) + const ret = await hooks.slice(slice.value, deps) + const data = ret === undefined ? new NullSelection('') : ret + return assert({ data, typeInfo }) + } catch (e) { + throw new SliceError({ cause: e }) + } }, - IdentifierExpr: { - async enter(node, visit) { - return withContext(scope, node, visit, async () => { - const id = node.id.value - const value = id ? scope.vars[id] : scope.context - invariant( - value !== undefined, - new ValueReferenceError(node.id.value), - ) - return value - }) - }, + IdentifierExpr(node) { + return scope.lookup(node.id.value) }, - SelectorExpr: { - async enter(node, visit) { - return withContext(scope, node, visit, async context => { - const selector = await visit(node.selector) - invariant( - typeof selector === 'string', - new ValueTypeError('Expected selector string'), - ) - const args = [context!.value, selector, node.expand] as const - - function select(typeInfo: TypeInfo) { - switch (typeInfo.type) { - case Type.Maybe: - return select(typeInfo.option) - case Type.Html: - return html.select(...args) - case Type.Js: - return js.select(...args) - case Type.Headers: - return headers.select(...args) - case Type.Cookies: - return cookies.select(...args) - default: - return json.select(...args) - } - } + DrillIdentifierExpr(node) { + return scope.lookup(node.id.value) + }, - const value = select(context!.typeInfo) - const optional = node.typeInfo.type === Type.Maybe - return optional ? value : assert(value) - }) - }, + SelectorExpr(node) { + invariant( + typeof node.selector === 'string', + new ValueTypeError('Expected selector string'), + ) + + const args = [scope.context.data, node.selector, node.expand] as const + + function select(typeInfo: TypeInfo) { + switch (typeInfo.type) { + case Type.Maybe: + return select(typeInfo.option) + case Type.Html: + return html.select(...args) + case Type.Js: + return js.select(...args) + case Type.Headers: + return headers.select(...args) + case Type.Cookies: + return cookies.select(...args) + default: + return json.select(...args) + } + } + + const data = select(scope.context.typeInfo) + return { data, typeInfo: node.typeInfo } }, - ModifierExpr: { - enter(node, visit) { - return withContext(scope, node, visit, async context => { - const args = await visit(node.args) - const { value, typeInfo } = context! - return callModifier(node, args, value, typeInfo) - }) - }, + ModifierExpr(node) { + return { + data: callModifier(node, scope.context), + typeInfo: node.typeInfo, + } }, ModuleExpr: { @@ -152,30 +164,59 @@ export async function execute( }, }, - ObjectLiteralExpr: { - async enter(node, visit) { - return withContext(scope, node, visit, async () => { - const obj: Record = {} - for (const entry of node.entries) { - const value = await visit(entry.value) - if (!(value instanceof NullSelection)) { - const key = await visit(entry.key) - obj[key] = value + ObjectEntryExpr(node) { + const value = assert(node.value) + return [node.key, value.data] + }, + + ObjectLiteralExpr(node) { + const data = Object.fromEntries( + node.entries.filter(e => !(e[1] instanceof NullSelection)), + ) + return { data, typeInfo: node.typeInfo } + }, + + SubqueryExpr() { + const ex = scope.extracted + invariant(ex, new QuerySyntaxError('Subquery must extract a value')) + return ex + }, + + DrillExpr(node) { + return node.body.at(-1) + }, + + DrillBitExpr: { + async enter(node) { + const ctx = scope.context + const optional = node.typeInfo.type === Type.Maybe + if (optional && ctx?.data instanceof NullSelection) { + return scope.context + } + + async function withItemContext() { + const ctx = scope.context + if (ctx?.typeInfo.type === Type.List) { + const list = [] + scope.push() + for (const data of ctx.data) { + scope.context = { data, typeInfo: ctx.typeInfo.of } + const item = await withItemContext() + list.push(item) } + scope.pop() + return list } - return obj - }) - }, - }, + const { data } = await walk(node.bit, visitor) + return data + } - SubqueryExpr: { - async enter(node, visit) { - return withContext(scope, node, visit, async () => { - const ex = await executeBody(visit, node.body) - const err = new QuerySyntaxError('Subquery must extract a value') - invariant(ex, err) - return ex - }) + const data = await withItemContext() + const bit = { data, typeInfo: node.typeInfo } + return { ...node, bit } + }, + exit(node) { + return node.bit }, }, @@ -183,7 +224,7 @@ export async function execute( const method = node.method.value const url = node.url const body = node.body ?? '' - return await http.request( + const data = await http.request( method, url, node.headers, @@ -191,61 +232,15 @@ export async function execute( body, hooks.request, ) + return { data, typeInfo: node.typeInfo } }, + } - /** - * Statement nodes - */ - - DeclInputsStmt() {}, - - InputDeclStmt: { - async enter(node, visit) { - const inputName = node.id.value - let inputValue = inputs[inputName] - if (inputValue === undefined) { - if (!node.optional) { - throw new NullInputError(inputName) - } - inputValue = node.defaultValue - ? await visit(node.defaultValue) - : new NullSelection(`input:${inputName}`) - } - scope.vars[inputName] = inputValue - }, - }, - - AssignmentStmt(node) { - scope.vars[node.name.value] = node.value - }, - - RequestStmt(node) { - scope.pushContext(node.request) - }, - - ExtractStmt(node) { - scope.extracted = assert(node.value) - }, - - Program: { - async enter(node, visit) { - ex = await executeBody(visit, node.body) - }, - }, - }) - - return ex + return walk(entry.program, visitor) } - const scope = new RootScope() const modules = new Modules(hooks, executeModule) - const rootEntry = await modules.import(rootModule) const ex = await executeModule(rootEntry, rootInputs) - - const retType: any = rootEntry.program.body.find( - stmt => stmt.kind === NodeKind.ExtractStmt, - ) - - return retType ? toValue(ex, retType.value.typeInfo) : ex + return ex && toValue(ex.data, ex.typeInfo) } diff --git a/packages/get/src/modifiers.ts b/packages/get/src/modifiers.ts index b1bc7cd..1d09a3d 100644 --- a/packages/get/src/modifiers.ts +++ b/packages/get/src/modifiers.ts @@ -1,33 +1,29 @@ import { cookies, html, js, json } from '@getlang/lib' import type { ModifierExpr } from '@getlang/parser/ast' -import type { TypeInfo } from '@getlang/parser/typeinfo' import { NullSelection } from '@getlang/utils' import { ValueReferenceError } from '@getlang/utils/errors' +import type { RuntimeValue } from './value.js' import { toValue } from './value.js' -export function callModifier( - node: ModifierExpr, - args: any, - value: any, - typeInfo: TypeInfo, -) { - const mod = node.modifier.value +export function callModifier(node: ModifierExpr, context: RuntimeValue) { + let { data, typeInfo } = context + const mod = node.modifier.value if (mod === 'link') { - const tag = value.type === 'tag' ? value.name : undefined + const tag = data.type === 'tag' ? data.name : undefined if (tag === 'a') { - value = html.select(value, 'xpath:@href', false) + data = html.select(data, 'xpath:@href', false) } else if (tag === 'img') { - value = html.select(value, 'xpath:@src', false) + data = html.select(data, 'xpath:@src', false) } } - const doc = toValue(value, typeInfo) + const doc = toValue(data, typeInfo) switch (mod) { case 'link': return doc - ? new URL(doc, args.base).toString() + ? new URL(doc, node.args.base).toString() : new NullSelection('@link') case 'html': return html.parse(doc) diff --git a/packages/get/src/value.ts b/packages/get/src/value.ts index 446abac..35a3ba2 100644 --- a/packages/get/src/value.ts +++ b/packages/get/src/value.ts @@ -1,10 +1,15 @@ import { cookies, headers, html, js } from '@getlang/lib' import type { TypeInfo } from '@getlang/parser/typeinfo' import { Type } from '@getlang/parser/typeinfo' -import { NullSelection } from '@getlang/utils' +import { invariant, NullSelection } from '@getlang/utils' import { NullSelectionError, ValueTypeError } from '@getlang/utils/errors' import { mapValues } from 'lodash-es' +export type RuntimeValue = { + data: any + typeInfo: TypeInfo +} + export function toValue(value: any, typeInfo: TypeInfo): any { switch (typeInfo.type) { case Type.Html: @@ -28,9 +33,10 @@ export function toValue(value: any, typeInfo: TypeInfo): any { } } -export function assert(value: any) { - if (value instanceof NullSelection) { - throw new NullSelectionError(value.selector) +export function assert(value: RuntimeValue) { + const optional = value.typeInfo.type === Type.Maybe + if (!optional && value.data instanceof NullSelection) { + throw new NullSelectionError(value.data.selector) } return value } diff --git a/packages/parser/src/ast/ast.ts b/packages/parser/src/ast/ast.ts index 9c8486b..6728a5e 100644 --- a/packages/parser/src/ast/ast.ts +++ b/packages/parser/src/ast/ast.ts @@ -7,139 +7,141 @@ export function isToken(value: unknown): value is Token { return !!value && typeof value === 'object' && 'offset' in value } -export enum NodeKind { - Program = 'Program', - ExtractStmt = 'ExtractStmt', - AssignmentStmt = 'AssignmentStmt', - DeclInputsStmt = 'DeclInputsStmt', - InputDeclStmt = 'InputDeclStmt', - RequestStmt = 'RequestStmt', - RequestExpr = 'RequestExpr', - TemplateExpr = 'TemplateExpr', - IdentifierExpr = 'IdentifierExpr', - SelectorExpr = 'SelectorExpr', - ModifierExpr = 'ModifierExpr', - ModuleExpr = 'ModuleExpr', - SubqueryExpr = 'SubqueryExpr', - ObjectLiteralExpr = 'ObjectLiteralExpr', - SliceExpr = 'SliceExpr', -} - -type ObjectEntry = { - key: Expr - value: Expr - optional: boolean -} - export type Program = { - kind: NodeKind.Program + kind: 'Program' body: Stmt[] } type ExtractStmt = { - kind: NodeKind.ExtractStmt + kind: 'ExtractStmt' value: Expr } type AssignmentStmt = { - kind: NodeKind.AssignmentStmt + kind: 'AssignmentStmt' name: Token value: Expr optional: boolean } export type DeclInputsStmt = { - kind: NodeKind.DeclInputsStmt + kind: 'DeclInputsStmt' inputs: InputDeclStmt[] } export type InputDeclStmt = { - kind: NodeKind.InputDeclStmt + kind: 'InputDeclStmt' id: Token optional: boolean defaultValue?: SliceExpr } type RequestStmt = { - kind: NodeKind.RequestStmt + kind: 'RequestStmt' request: RequestExpr } -type RequestBlocks = { - query?: ObjectLiteralExpr - cookies?: ObjectLiteralExpr - json?: ObjectLiteralExpr - form?: ObjectLiteralExpr -} - export type RequestExpr = { - kind: NodeKind.RequestExpr + kind: 'RequestExpr' method: Token url: Expr - headers: ObjectLiteralExpr - blocks: RequestBlocks - body?: Expr + headers: RequestBlockExpr + blocks: RequestBlockExpr[] + body: Expr + typeInfo: TypeInfo +} + +type RequestBlockExpr = { + kind: 'RequestBlockExpr' + name: Token + entries: RequestEntryExpr[] + typeInfo: TypeInfo +} + +type RequestEntryExpr = { + kind: 'RequestEntryExpr' + key: Expr + value: Expr typeInfo: TypeInfo } export type TemplateExpr = { - kind: NodeKind.TemplateExpr + kind: 'TemplateExpr' elements: (Expr | Token)[] typeInfo: TypeInfo } type IdentifierExpr = { - kind: NodeKind.IdentifierExpr + kind: 'IdentifierExpr' id: Token - expand: boolean isUrlComponent: boolean - context?: Expr + typeInfo: TypeInfo +} + +type DrillIdentifierExpr = { + kind: 'DrillIdentifierExpr' + id: Token + expand: boolean typeInfo: TypeInfo } type SelectorExpr = { - kind: NodeKind.SelectorExpr - selector: TemplateExpr + kind: 'SelectorExpr' + selector: Expr expand: boolean - context?: Expr typeInfo: TypeInfo } export type ModifierExpr = { - kind: NodeKind.ModifierExpr + kind: 'ModifierExpr' modifier: Token args: ObjectLiteralExpr - context?: Expr typeInfo: TypeInfo } export type ModuleExpr = { - kind: NodeKind.ModuleExpr + kind: 'ModuleExpr' module: Token call: boolean args: ObjectLiteralExpr - context?: Expr typeInfo: TypeInfo } type SubqueryExpr = { - kind: NodeKind.SubqueryExpr + kind: 'SubqueryExpr' body: Stmt[] - context?: Expr + typeInfo: TypeInfo +} + +type DrillExpr = { + kind: 'DrillExpr' + body: DrillBitExpr[] + typeInfo: TypeInfo +} + +type DrillBitExpr = { + kind: 'DrillBitExpr' + bit: Expr + typeInfo: TypeInfo +} + +type ObjectEntryExpr = { + kind: 'ObjectEntryExpr' + key: Expr + value: Expr + optional: boolean typeInfo: TypeInfo } type ObjectLiteralExpr = { - kind: NodeKind.ObjectLiteralExpr - entries: ObjectEntry[] - context?: Expr + kind: 'ObjectLiteralExpr' + entries: ObjectEntryExpr[] typeInfo: TypeInfo } type SliceExpr = { - kind: NodeKind.SliceExpr + kind: 'SliceExpr' slice: Token - context?: Expr typeInfo: TypeInfo } @@ -153,20 +155,25 @@ export type Stmt = export type Expr = | RequestExpr + | RequestBlockExpr + | RequestEntryExpr | TemplateExpr | IdentifierExpr + | DrillIdentifierExpr | SelectorExpr | ModifierExpr | ModuleExpr | SubqueryExpr + | ObjectEntryExpr | ObjectLiteralExpr | SliceExpr + | DrillExpr + | DrillBitExpr -export type CExpr = Extract export type Node = Stmt | Expr const program = (body: Stmt[]): Program => ({ - kind: NodeKind.Program, + kind: 'Program', body, }) @@ -175,14 +182,14 @@ const assignmentStmt = ( value: Expr, optional: boolean, ): AssignmentStmt => ({ - kind: NodeKind.AssignmentStmt, + kind: 'AssignmentStmt', name, optional, value, }) const declInputsStmt = (inputs: InputDeclStmt[]): DeclInputsStmt => ({ - kind: NodeKind.DeclInputsStmt, + kind: 'DeclInputsStmt', inputs, }) @@ -191,122 +198,146 @@ const inputDeclStmt = ( optional: boolean, defaultValue?: SliceExpr, ): InputDeclStmt => ({ - kind: NodeKind.InputDeclStmt, + kind: 'InputDeclStmt', id, optional, defaultValue, }) const extractStmt = (value: Expr): ExtractStmt => ({ - kind: NodeKind.ExtractStmt, + kind: 'ExtractStmt', value, }) const requestStmt = (request: RequestExpr): RequestStmt => ({ - kind: NodeKind.RequestStmt, + kind: 'RequestStmt', request, }) const requestExpr = ( method: Token, url: Expr, - headers: ObjectLiteralExpr, - blocks: RequestBlocks, + headers: RequestBlockExpr, + blocks: RequestBlockExpr[], body: Expr, ): RequestExpr => ({ - kind: NodeKind.RequestExpr, + kind: 'RequestExpr', + typeInfo: { type: Type.Value }, method, url, headers, blocks, body, +}) + +const requestBlockExpr = ( + name: Token, + entries: RequestEntryExpr[], +): RequestBlockExpr => ({ + kind: 'RequestBlockExpr', typeInfo: { type: Type.Value }, + name, + entries, }) -const subqueryExpr = (body: Stmt[], context?: Expr): SubqueryExpr => ({ - kind: NodeKind.SubqueryExpr, +const requestEntryExpr = (key: Expr, value: Expr): RequestEntryExpr => ({ + kind: 'RequestEntryExpr', + typeInfo: { type: Type.Value }, + key, + value, +}) + +const subqueryExpr = (body: Stmt[]): SubqueryExpr => ({ + kind: 'SubqueryExpr', + typeInfo: { type: Type.Value }, body, +}) + +const drillExpr = (body: DrillBitExpr[]): DrillExpr => ({ + kind: 'DrillExpr', typeInfo: { type: Type.Value }, - context, + body, }) -const identifierExpr = ( - id: Token, - expand = false, - context?: Expr, -): IdentifierExpr => ({ - kind: NodeKind.IdentifierExpr, +const drillBitExpr = (bit: Expr): DrillBitExpr => ({ + kind: 'DrillBitExpr', + typeInfo: { type: Type.Value }, + bit, +}) + +const identifierExpr = (id: Token): IdentifierExpr => ({ + kind: 'IdentifierExpr', + typeInfo: { type: Type.Value }, id, - expand, isUrlComponent: id.text.startsWith(':'), - typeInfo: { type: Type.Value }, - context, }) -const selectorExpr = ( - selector: TemplateExpr, +const drillIdentifierExpr = ( + id: Token, expand: boolean, - context?: Expr, -): SelectorExpr => ({ - kind: NodeKind.SelectorExpr, +): DrillIdentifierExpr => ({ + kind: 'DrillIdentifierExpr', + typeInfo: { type: Type.Value }, + id, expand, - selector, +}) + +const selectorExpr = (selector: Expr, expand: boolean): SelectorExpr => ({ + kind: 'SelectorExpr', typeInfo: { type: Type.Value }, - context, + expand, + selector, }) const modifierExpr = ( modifier: Token, inputs: ObjectLiteralExpr = objectLiteralExpr([]), - context?: Expr, ): ModifierExpr => ({ - kind: NodeKind.ModifierExpr, + kind: 'ModifierExpr', + typeInfo: { type: Type.Value }, modifier, args: inputs, - typeInfo: { type: Type.Value }, - context, }) const moduleExpr = ( module: Token, inputs: ObjectLiteralExpr = objectLiteralExpr([]), - context?: Expr, ): ModuleExpr => ({ - kind: NodeKind.ModuleExpr, + kind: 'ModuleExpr', + typeInfo: { type: Type.Value }, module, call: false, args: inputs, - typeInfo: { type: Type.Value }, - context, }) -const objectLiteralExpr = ( - entries: ObjectEntry[], - context?: Expr, -): ObjectLiteralExpr => ({ - kind: NodeKind.ObjectLiteralExpr, - entries, - typeInfo: { type: Type.Value }, - context, -}) - -const objectEntry = ( +const objectEntryExpr = ( key: Expr, value: Expr, optional = false, -): ObjectEntry => ({ key, value, optional }) +): ObjectEntryExpr => ({ + kind: 'ObjectEntryExpr', + typeInfo: { type: Type.Value }, + key, + optional, + value, +}) -const sliceExpr = (slice: Token, context?: Expr): SliceExpr => ({ - kind: NodeKind.SliceExpr, - slice, +const objectLiteralExpr = (entries: ObjectEntryExpr[]): ObjectLiteralExpr => ({ + kind: 'ObjectLiteralExpr', + typeInfo: { type: Type.Value }, + entries, +}) + +const sliceExpr = (slice: Token): SliceExpr => ({ + kind: 'SliceExpr', typeInfo: { type: Type.Value }, - context, + slice, }) const templateExpr = (elements: (Expr | Token)[]): TemplateExpr => ({ - kind: NodeKind.TemplateExpr, - elements, + kind: 'TemplateExpr', typeInfo: { type: Type.Value }, + elements, }) export const t = { @@ -317,13 +348,18 @@ export const t = { extractStmt, requestStmt, requestExpr, + requestBlockExpr, + requestEntryExpr, templateExpr, identifierExpr, + drillIdentifierExpr, selectorExpr, modifierExpr, moduleExpr, sliceExpr, + objectEntryExpr, objectLiteralExpr, - objectEntry, subqueryExpr, + drillExpr, + drillBitExpr, } diff --git a/packages/parser/src/ast/print.ts b/packages/parser/src/ast/print.ts index 7dd41f5..6eaba18 100644 --- a/packages/parser/src/ast/print.ts +++ b/packages/parser/src/ast/print.ts @@ -1,9 +1,9 @@ +import type { Visitor } from '@getlang/walker' +import { walk } from '@getlang/walker' import { builders, printer } from 'prettier/doc' import { render } from '../utils.js' -import type { InterpretVisitor } from '../visitor/visitor.js' -import { visit } from '../visitor/visitor.js' import type { Node } from './ast.js' -import { isToken, NodeKind } from './ast.js' +import { isToken } from './ast.js' type Doc = builders.Doc @@ -12,7 +12,7 @@ type Doc = builders.Doc const { group, indent, join, line, hardline, softline, ifBreak } = builders -const printVisitor: InterpretVisitor = { +const printVisitor: Visitor = { Program(node) { return join(hardline, node.body) }, @@ -26,31 +26,32 @@ const printVisitor: InterpretVisitor = { ]) }, - RequestExpr: { - enter(node, visit) { - const parts: Doc[] = [node.method.value, ' ', visit(node.url)] - for (const h of node.headers.entries) { - parts.push(hardline, visit(h.key), ': ', visit(h.value)) - } - for (const [blockName, block] of Object.entries(node.blocks)) { - parts.push(hardline, '[', blockName, ']') - for (const e of block.entries) { - parts.push(hardline, visit(e.key), ': ', visit(e.value)) - } - } - if (node.body) { - parts.push( - hardline, - '[body]', - hardline, - visit(node.body), - hardline, - '[/body]', - ) - } - parts.push(hardline) // terminal - return group(parts) - }, + RequestExpr(node) { + const parts: Doc[] = [node.method.value, ' ', node.url] + for (const block of node.blocks) { + parts.push(block) + } + if (node.body) { + parts.push(hardline, '[body]', hardline, node.body, hardline, '[/body]') + } + parts.push(hardline) // terminal + return group(parts) + }, + + RequestBlockExpr(node) { + const parts: Doc[] = [] + const name = node.name.value + if (name) { + parts.shift(hardline, '[', name, ']') + } + for (const entry of node.entries) { + parts.push(hardline, entry) + } + return parts + }, + + RequestEntryExpr(node) { + return [node.kind, ': ', node.value] }, InputDeclStmt(node) { @@ -82,56 +83,73 @@ const printVisitor: InterpretVisitor = { return group(['extract ', node.value]) }, - ObjectLiteralExpr(node, _path, orig) { - const shouldBreak = orig.entries.some( - e => e.value.kind === NodeKind.SelectorExpr, - ) - const entries = node.entries.map((entry, i) => { - const origEntry = orig.entries[i] - if (!origEntry) { - throw new Error('Unmatched object literal entry') - } + DrillExpr(node) { + const [first, ...rest] = node.body + const [, arrow, bit] = first.contents + const lead = arrow === '=> ' ? [arrow, bit] : bit + return [lead, ...rest] + }, - if (origEntry.value.kind === NodeKind.IdentifierExpr) { - const key = render(origEntry.key) - const value = origEntry.value.id.value - if (key === value || (key === '$' && value === '')) { - return entry.value - } - } + DrillBitExpr(node, { node: orig }) { + let arrow = '-> ' + if ( + (orig.kind === 'SelectorExpr' || orig.kind === 'DrillIdentifierExpr') && + orig.expand + ) { + arrow = '=> ' + } + return indent([line, arrow, node.bit]) + }, - const keyGroup: Doc[] = [entry.key] - if (entry.optional) { - keyGroup.push('?') + ObjectEntryExpr(node, { node: orig }) { + if (orig.value.kind === 'IdentifierExpr') { + const key = render(orig.key) + const value = orig.value.id.value + if (key === value || (key === '$' && value === '')) { + return node.value } + } - // seperator - keyGroup.push(': ') - - // value - const value = entry.value - let shValue: Doc = entry.value - if ( - Array.isArray(shValue) && - shValue.length === 1 && - typeof shValue[0] === 'string' - ) { - shValue = shValue[0] - } - if (typeof shValue === 'string' && entry.key === shValue) { - return [value, entry.optional ? '?' : ''] + const keyGroup: Doc[] = [node.key] + if (node.optional) { + keyGroup.push('?') + } + + // seperator + keyGroup.push(': ') + + // value + const value = node.value + let shValue: Doc = node.value + if ( + Array.isArray(shValue) && + shValue.length === 1 && + typeof shValue[0] === 'string' + ) { + shValue = shValue[0] + } + if (typeof shValue === 'string' && node.key === shValue) { + return [value, node.optional ? '?' : ''] + } + return group([keyGroup, value]) + }, + + ObjectLiteralExpr(node, { node: orig }) { + const shouldBreak = orig.entries.some(e => { + switch (e.value.kind) { + case 'SelectorExpr': + return true + case 'DrillExpr': + return e.value.body.at(-1).bit.kind === 'SelectorExpr' } - return group([keyGroup, value]) }) - const sep = ifBreak(line, [',', line]) - const obj = group(['{', indent([line, join(sep, entries)]), line, '}'], { + return group(['{', indent([line, join(sep, node.entries)]), line, '}'], { shouldBreak, }) - return node.context ? [node.context, indent([line, '-> ', obj])] : obj }, - TemplateExpr(node, _path, orig) { + TemplateExpr(node, { node: orig }) { return node.elements.map((el, i) => { const og = orig.elements[i]! if (isToken(og)) { @@ -140,9 +158,9 @@ const printVisitor: InterpretVisitor = { if (typeof el !== 'string' && !Array.isArray(el)) { throw new Error(`Unsupported template node: ${el.type} command`) - } else if (og.kind === NodeKind.TemplateExpr) { + } else if (og.kind === 'TemplateExpr') { return ['$[', el, ']'] - } else if (og.kind !== NodeKind.IdentifierExpr) { + } else if (og.kind !== 'IdentifierExpr') { throw new Error(`Unexpected template node: ${og?.kind}`) } @@ -157,71 +175,55 @@ const printVisitor: InterpretVisitor = { }, IdentifierExpr(node) { - const id = node.id.value - if (!node.context) { - const arrow = node.expand ? '=> ' : '' - return [arrow, '$', id] - } - const arrow = node.expand ? '=> ' : '-> ' - return [node.context, indent([line, arrow, '$', node.id.value])] + return ['$', node.id.value] + }, + + DrillIdentifierExpr(node) { + return ['$', node.id.value] }, SelectorExpr(node) { - if (!node.context) { - const arrow = node.expand ? '=> ' : '' - return [arrow, node.selector] - } - const arrow = node.expand ? '=> ' : '-> ' - return [node.context, indent([line, arrow, node.selector])] + return node.selector }, - ModifierExpr(node, _path, orig) { + ModifierExpr(node, { node: orig }) { const call: Doc[] = ['@', node.modifier.value] if (orig.args.entries.length) { call.push('(', node.args, ')') } - return node.context ? [node.context, indent([line, '-> ', call])] : call + return call }, - ModuleExpr(node, _path, orig) { + ModuleExpr(node, { node: orig }) { const call: Doc[] = ['@', node.module.value] if (orig.args.entries.length) { call.push('(', node.args, ')') } - return node.context ? [node.context, indent([line, '-> ', call])] : call + return call }, SliceExpr(node) { const { value } = node.slice const quot = value.includes('`') ? '|' : '`' const lines = value.split('\n') - const slice = group([ + return group([ quot, indent([softline, join(hardline, lines)]), softline, quot, ]) - return node.context ? [node.context, indent([line, '-> ', slice])] : slice }, SubqueryExpr(node) { - const subquery = [ - '(', - indent(node.body.flatMap(x => [hardline, x])), - hardline, - ')', - ] - return node.context - ? [node.context, indent([line, '-> ', subquery])] - : subquery + return ['(', indent(node.body.flatMap(x => [hardline, x])), hardline, ')'] }, } export function print(ast: Node) { - if (!(ast.kind === NodeKind.Program)) { + if (!(ast.kind === 'Program')) { throw new Error(`Non-program AST node provided: ${ast}`) } - const doc = visit(ast, printVisitor) + const doc = walk(ast, printVisitor) // propagateBreaks(doc) return printer.printDocToString(doc, { printWidth: 70, diff --git a/packages/parser/src/grammar.ts b/packages/parser/src/grammar.ts index 6dd8310..e32373a 100644 --- a/packages/parser/src/grammar.ts +++ b/packages/parser/src/grammar.ts @@ -105,11 +105,13 @@ const grammar: Grammar = { {"name": "expression$ebnf$1", "symbols": ["expression$ebnf$1$subexpression$1"], "postprocess": id}, {"name": "expression$ebnf$1", "symbols": [], "postprocess": () => null}, {"name": "expression", "symbols": ["expression$ebnf$1", (lexer.has("link") ? {type: "link"} : link), "_", "drill"], "postprocess": p.link}, - {"name": "drill", "symbols": ["drill", "_", (lexer.has("drill_arrow") ? {type: "drill_arrow"} : drill_arrow), "_", "bit"], "postprocess": p.drill}, {"name": "drill$ebnf$1$subexpression$1", "symbols": [(lexer.has("drill_arrow") ? {type: "drill_arrow"} : drill_arrow), "_"]}, {"name": "drill$ebnf$1", "symbols": ["drill$ebnf$1$subexpression$1"], "postprocess": id}, {"name": "drill$ebnf$1", "symbols": [], "postprocess": () => null}, - {"name": "drill", "symbols": ["drill$ebnf$1", "bit"], "postprocess": p.drillContext}, + {"name": "drill$ebnf$2", "symbols": []}, + {"name": "drill$ebnf$2$subexpression$1", "symbols": ["_", (lexer.has("drill_arrow") ? {type: "drill_arrow"} : drill_arrow), "_", "bit"]}, + {"name": "drill$ebnf$2", "symbols": ["drill$ebnf$2", "drill$ebnf$2$subexpression$1"], "postprocess": (d) => d[0].concat([d[1]])}, + {"name": "drill", "symbols": ["drill$ebnf$1", "bit", "drill$ebnf$2"], "postprocess": p.drill}, {"name": "bit$subexpression$1", "symbols": ["template"]}, {"name": "bit$subexpression$1", "symbols": ["slice"]}, {"name": "bit$subexpression$1", "symbols": ["call"]}, diff --git a/packages/parser/src/grammar/getlang.ne b/packages/parser/src/grammar/getlang.ne index bb0a758..6cbfa54 100644 --- a/packages/parser/src/grammar/getlang.ne +++ b/packages/parser/src/grammar/getlang.ne @@ -11,7 +11,7 @@ program -> _ (inputs line_sep):? statements _ {% p.program %} statements -> statement (line_sep statement):* {% p.statements %} statement -> (request | assignment | extract) {% p.idd %} -# keyswords +# keywords inputs -> "inputs" __ "{" _ input_decl (_ "," _ input_decl):* _ "}" {% p.declInputs %} assignment -> "set" __ %identifier "?":? _ "=" _ expression {% p.assignment %} extract -> "extract" __ expression {% p.extract %} @@ -24,17 +24,16 @@ input_default -> slice {% id %} request -> %request_verb template (line_sep request_block):? request_blocks {% p.request %} request_blocks -> (line_sep request_block_named):* (line_sep request_block_body):? {% p.requestBlocks %} request_block_named -> %request_block_name line_sep request_block {% p.requestBlockNamed %} -request_block_body -> %request_block_body template %request_block_body_end {% p.requestBlockBody %} request_block -> request_entry (line_sep request_entry):* {% p.requestBlock %} request_entry -> template ":" (__ template):? {% p.requestEntry %} +request_block_body -> %request_block_body template %request_block_body_end {% p.requestBlockBody %} # expression expression -> drill {% id %} expression -> (drill _ %drill_arrow _):? %link _ drill {% p.link %} # drill -drill -> drill _ %drill_arrow _ bit {% p.drill %} # left-associativity -drill -> (%drill_arrow _):? bit {% p.drillContext %} +drill -> (%drill_arrow _):? bit (_ %drill_arrow _ bit):* {% p.drill %} # drill bit bit -> (template | slice | call | object | subquery) {% p.idd %} diff --git a/packages/parser/src/grammar/parse.ts b/packages/parser/src/grammar/parse.ts index 1255189..b9f1834 100644 --- a/packages/parser/src/grammar/parse.ts +++ b/packages/parser/src/grammar/parse.ts @@ -1,7 +1,7 @@ import { invariant } from '@getlang/utils' import { QuerySyntaxError } from '@getlang/utils/errors' -import type { CExpr, Expr, TemplateExpr } from '../ast/ast.js' -import { isToken, NodeKind, t } from '../ast/ast.js' +import type { Expr } from '../ast/ast.js' +import { isToken, t } from '../ast/ast.js' import { tx } from '../utils.js' type PP = nearley.Postprocessor @@ -31,16 +31,12 @@ export const inputDecl: PP = ([id, optional, maybeDefault]) => { } export const request: PP = ([method, url, headerBlock, { blocks, body }]) => { - const headers = headerBlock?.[1] ?? t.objectLiteralExpr([]) - return t.requestStmt(t.requestExpr(method, url, headers, blocks, body)) + const headers = t.requestBlockExpr(tx.token(''), headerBlock?.[1] ?? []) + const req = t.requestExpr(method, url, headers, blocks, body) + return t.requestStmt(req) } -export const requestBlocks: PP = ([namedBlocks, maybeBody]) => { - const blocks: Record = {} - for (const [, block] of namedBlocks) { - blocks[block.name] = block.entries - } - +export const requestBlocks: PP = ([blocks, maybeBody]) => { const body = maybeBody?.[1] if (body) { for (const el of body.elements) { @@ -54,12 +50,15 @@ export const requestBlocks: PP = ([namedBlocks, maybeBody]) => { return { blocks, body } } -export const requestBlockNamed: PP = ([name, , entries]) => ({ name, entries }) +export const requestBlockNamed: PP = ([name, , entries]) => + t.requestBlockExpr(name, entries) export const requestBlockBody: PP = ([, body]) => body -export const requestBlock: PP = ([entry, entries]) => - t.objectLiteralExpr([entry, ...entries.map((d: any) => d[1])]) +export const requestBlock: PP = ([entry, entries]) => [ + entry, + ...entries.map((d: any) => d[1]), +] export const requestEntry: PP = ([key, , maybeValue]) => { let value = maybeValue?.[1] @@ -70,7 +69,7 @@ export const requestEntry: PP = ([key, , maybeValue]) => { text: '', } } - return { key, value, optional: true } + return t.requestEntryExpr(key, value) } export const assignment: PP = ([, , name, optional, , , , expr]) => @@ -90,7 +89,7 @@ export const call: PP = ([callee, maybeInputs]) => { export const link: PP = ([maybePrior, callee, _, link]) => { const bit = t.moduleExpr( callee, - t.objectLiteralExpr([t.objectEntry(tx.template('@link'), link, true)]), + t.objectLiteralExpr([t.objectEntryExpr(tx.template('@link'), link, true)]), ) if (!maybePrior) { return bit @@ -109,16 +108,12 @@ export const objectEntry: PP = ([callkey, identifier, optional, , , value]) => { ...identifier, value: `${callkey ? '@' : ''}${identifier.value || '$'}`, } - return { - key: t.templateExpr([key]), - value, - optional: Boolean(optional), - } + return t.objectEntryExpr(t.templateExpr([key]), value, Boolean(optional)) } export const objectEntryShorthandSelect: PP = ([identifier, optional]) => { const value = t.templateExpr([identifier]) - const selector = t.selectorExpr(value, false) + const selector = t.drillExpr([t.drillBitExpr(t.selectorExpr(value, false))]) return objectEntry([null, identifier, optional, null, null, selector]) } @@ -127,24 +122,24 @@ export const objectEntryShorthandIdent: PP = ([identifier, optional]) => { return objectEntry([null, identifier, optional, null, null, value]) } -function drillBase(bit: CExpr | TemplateExpr, arrow?: string, context?: Expr) { +function drillBase(bit: Expr, arrow?: string): Expr { const expand = arrow === '=>' - if (bit.kind === NodeKind.TemplateExpr) { + if (bit.kind === 'TemplateExpr') { bit = t.selectorExpr(bit, expand) - } else if (bit.kind === NodeKind.IdentifierExpr) { - bit.expand = expand + } else if (bit.kind === 'IdentifierExpr') { + bit = t.drillIdentifierExpr(bit.id, expand) } else if (expand) { throw new QuerySyntaxError('Wide arrow drill requires selector on RHS') } - bit.context = context - return bit + return t.drillBitExpr(bit) } -export const drill: PP = ([context, , arrow, , bit]) => - drillBase(bit, arrow.value, context) - -export const drillContext: PP = ([arrow, bit]) => - drillBase(bit, arrow?.[0].value) +export const drill: PP = ([arrow, bit, bits]) => { + return t.drillExpr([ + drillBase(bit, arrow?.[0].value), + ...bits.map(([, arrow, , bit]: any) => drillBase(bit, arrow.value)), + ]) +} export const identifier: PP = ([id]) => { return t.identifierExpr(id) diff --git a/packages/parser/src/passes/analyze.ts b/packages/parser/src/passes/analyze.ts index 3bac519..c551ccf 100644 --- a/packages/parser/src/passes/analyze.ts +++ b/packages/parser/src/passes/analyze.ts @@ -1,49 +1,27 @@ -import type { CExpr, Program } from '../ast/ast.js' -import { Type } from '../ast/typeinfo.js' -import type { TransformVisitor } from '../visitor/transform.js' -import { visit } from '../visitor/visitor.js' -import { traceVisitor } from './trace.js' +import { ScopeTracker, walk } from '@getlang/walker' +import type { Program } from '../ast/ast.js' export function analyze(ast: Program) { - const { scope, trace } = traceVisitor() + const scope = new ScopeTracker() const inputs = new Set() const imports = new Set() let isMacro = false - function checkMacro(node: CExpr) { - if (!node.context) { - const implicitType = scope.context?.typeInfo.type - isMacro ||= implicitType === Type.Context - } - } - - const visitor: TransformVisitor = { - ...trace, + walk(ast, { + scope, InputDeclStmt(node) { inputs.add(node.id.value) - return trace.InputDeclStmt(node) }, - SelectorExpr: { - enter(node, visit) { - checkMacro(node) - return trace.SelectorExpr.enter(node, visit) - }, + ModuleExpr(node) { + imports.add(node.module.value) }, - ModifierExpr: { - enter(node, visit) { - checkMacro(node) - return trace.ModifierExpr.enter(node, visit) - }, + SelectorExpr() { + isMacro ||= !scope.context }, - ModuleExpr: { - enter(node, visit) { - imports.add(node.module.value) - return trace.ModuleExpr.enter(node, visit) - }, + ModifierExpr() { + isMacro ||= !scope.context }, - } - - visit(ast, visitor) + }) return { inputs, imports, isMacro } } diff --git a/packages/parser/src/passes/desugar.ts b/packages/parser/src/passes/desugar.ts index f999b5d..684b7c4 100644 --- a/packages/parser/src/passes/desugar.ts +++ b/packages/parser/src/passes/desugar.ts @@ -1,36 +1,36 @@ +import { walk } from '@getlang/walker' import type { Program } from '../ast/ast.js' -import type { TransformVisitor } from '../visitor/visitor.js' -import { visit } from '../visitor/visitor.js' import { resolveContext } from './desugar/context.js' import { settleLinks } from './desugar/links.js' import { RequestParsers } from './desugar/reqparse.js' import { insertSliceDeps } from './desugar/slicedeps.js' import { registerCalls } from './inference/calls.js' -export type DesugarPass = (tools: { - parsers: RequestParsers - macros: string[] -}) => TransformVisitor +export type DesugarPass = ( + ast: Program, + tools: { + parsers: RequestParsers + macros: string[] + }, +) => Program function listCalls(ast: Program) { const calls = new Set() - visit(ast, { + walk(ast, { ModuleExpr(node) { - if (node.call) { - calls.add(node.module.value) - } + node.call && calls.add(node.module.value) }, - } as TransformVisitor) + }) return calls } export function desugar(ast: Program, macros: string[] = []) { const parsers = new RequestParsers() - const visitors = [resolveContext, settleLinks, insertSliceDeps] + // const visitors = [resolveContext, settleLinks, insertSliceDeps] + const visitors = [resolveContext, insertSliceDeps] let program = visitors.reduce((ast, pass) => { parsers.reset() - const visitor = pass({ parsers, macros }) - return visit(ast, visitor) + return pass(ast, { parsers, macros }) }, ast) // inference pass `registerCalls` is included in the desugar phase diff --git a/packages/parser/src/passes/desugar/context.ts b/packages/parser/src/passes/desugar/context.ts index 7519a4e..33a5fe5 100644 --- a/packages/parser/src/passes/desugar/context.ts +++ b/packages/parser/src/passes/desugar/context.ts @@ -1,84 +1,63 @@ import { invariant } from '@getlang/utils' import { QuerySyntaxError } from '@getlang/utils/errors' -import type { CExpr, Expr } from '../../ast/ast.js' -import { NodeKind } from '../../ast/ast.js' -import { tx } from '../../utils.js' +import { ScopeTracker, walk } from '@getlang/walker' +import { t } from '../../ast/ast.js' import type { DesugarPass } from '../desugar.js' -import { traceVisitor } from '../trace.js' -export const resolveContext: DesugarPass = ({ parsers, macros }) => { - const { scope, trace } = traceVisitor() +export const resolveContext: DesugarPass = (ast, { parsers, macros }) => { + const scope = new ScopeTracker() - function infer(node: CExpr, mod?: string) { - let resolved: Expr - let from: Expr | undefined - if (node.context) { - resolved = node.context - } else { - from = scope.context - invariant(from, new QuerySyntaxError('Unresolved context')) - if (from.kind === NodeKind.RequestExpr) { - const field = mod === 'link' ? 'url' : mod - resolved = parsers.lookup(from, field) - } else { - resolved = tx.ident('') - } - } - return { resolved, from } - } + const program = walk(ast, { + scope, + + Program(node) { + const body = parsers.insert(node.body) + return { ...node, body } + }, - return { - ...trace, + SubqueryExpr(node) { + const body = parsers.insert(node.body) + return { ...node, body } + }, RequestExpr(node) { parsers.visit(node) - return node }, - Program: { - enter(node, visit) { - const xnode = trace.Program.enter(node, visit) - return { ...xnode, body: parsers.insert(xnode.body) } - }, - }, + DrillBitExpr(node, path) { + const { bit } = node + const isModifier = bit.kind === 'ModifierExpr' + const requireContext = + isModifier || + bit.kind === 'SelectorExpr' || + (bit.kind === 'ModuleExpr' && macros.includes(bit.module.value)) - SubqueryExpr: { - enter(node, visit) { - const xnode = trace.SubqueryExpr.enter(node, visit) - return { ...xnode, body: parsers.insert(xnode.body) } - }, - }, + if (!requireContext) { + return + } - SelectorExpr: { - enter(node, visit) { - const { resolved: context } = infer(node) - return trace.SelectorExpr.enter({ ...node, context }, visit) - }, - }, + const ctx = scope.context + invariant(ctx, new QuerySyntaxError('Unresolved context')) + if (ctx.kind !== 'RequestExpr') { + return + } - ModifierExpr: { - enter(node, visit) { - const modifier = node.modifier.value - const { resolved: context, from } = infer(node, modifier) - const xnode = trace.ModifierExpr.enter({ ...node, context }, visit) - const onRequest = from?.kind === NodeKind.RequestExpr - // when inferred to request parser, replace modifier - if (onRequest) { - invariant(xnode.context, new QuerySyntaxError('Unresolved context')) - return xnode.context - } - return xnode - }, - }, + const field = isModifier ? bit.modifier.value : undefined + const resolved = t.drillBitExpr(parsers.lookup(ctx, field)) - ModuleExpr: { - enter(node, visit) { - const module = node.module.value - const context = macros.includes(module) - ? infer(node).resolved - : node.context - return trace.ModuleExpr.enter({ ...node, context }, visit) - }, + if (isModifier) { + // replace modifier with shared parser + return resolved + } + + path.insertBefore(resolved) }, - } + }) + + invariant( + program.kind === 'Program', + new QuerySyntaxError('Context inference exception'), + ) + + return program } diff --git a/packages/parser/src/passes/desugar/links.ts b/packages/parser/src/passes/desugar/links.ts index 2024357..bfca376 100644 --- a/packages/parser/src/passes/desugar/links.ts +++ b/packages/parser/src/passes/desugar/links.ts @@ -1,7 +1,7 @@ import { invariant } from '@getlang/utils' import { QuerySyntaxError, ValueReferenceError } from '@getlang/utils/errors' import type { Expr, RequestExpr } from '../../ast/ast.js' -import { NodeKind, t } from '../../ast/ast.js' +import { t } from '../../ast/ast.js' import { render, tx } from '../../utils.js' import type { DesugarPass } from '../desugar.js' import { traceVisitor } from '../trace.js' @@ -42,7 +42,7 @@ export const settleLinks: DesugarPass = ({ parsers }) => { enter(node, visit) { const xnode = trace.ModifierExpr.enter(node, visit) invariant( - xnode.args.kind === NodeKind.ObjectLiteralExpr, + xnode.args.kind === 'ObjectLiteralExpr', new QuerySyntaxError('Modifier options must be an object'), ) @@ -51,7 +51,7 @@ export const settleLinks: DesugarPass = ({ parsers }) => { const hasBase = xnode.args.entries.some(e => render(e.key) === 'base') if (contextBase && !hasBase) { xnode.args.entries.push( - t.objectEntry( + t.objectEntryExpr( tx.template('base'), parsers.lookup(contextBase, 'url'), ), @@ -74,7 +74,7 @@ export const settleLinks: DesugarPass = ({ parsers }) => { entries: node.args.entries.map(e => { if ( render(e.key) !== '@link' || - (e.value.kind === NodeKind.ModifierExpr && + (e.value.kind === 'ModifierExpr' && e.value.modifier.value === 'link') ) { return e @@ -104,9 +104,7 @@ export const settleLinks: DesugarPass = ({ parsers }) => { SubqueryExpr: { enter(node, visit) { let xnode = trace.SubqueryExpr.enter(node, visit) - const extracted = xnode.body.find( - stmt => stmt.kind === NodeKind.ExtractStmt, - ) + const extracted = xnode.body.find(stmt => stmt.kind === 'ExtractStmt') xnode = { ...xnode, body: parsers.insert(xnode.body) } extracted && inherit(extracted.value, xnode) return xnode diff --git a/packages/parser/src/passes/desugar/reqparse.ts b/packages/parser/src/passes/desugar/reqparse.ts index e535e6e..39311e6 100644 --- a/packages/parser/src/passes/desugar/reqparse.ts +++ b/packages/parser/src/passes/desugar/reqparse.ts @@ -1,7 +1,7 @@ import { invariant } from '@getlang/utils' import { QuerySyntaxError } from '@getlang/utils/errors' -import type { Expr, RequestExpr, Stmt } from '../../ast/ast.js' -import { NodeKind, t } from '../../ast/ast.js' +import type { RequestExpr, Stmt } from '../../ast/ast.js' +import { t } from '../../ast/ast.js' import { getContentField, tx } from '../../utils.js' type Parsers = Record @@ -37,33 +37,41 @@ export class RequestParsers { } private writeParser(idx: number, field: string): Stmt { - function expr() { - const reqId = tx.ident('') - let context: Expr - switch (field) { - case 'url': - return tx.select('url', reqId) - case 'headers': - return tx.select('headers', reqId) - case 'cookies': { - context = tx.select('set-cookie', tx.select('headers', reqId)) - break - } - default: { - context = tx.select('body', reqId) - } + const req = t.drillIdentifierExpr(tx.token(''), false) + const id = tx.token(this.id(idx, field)) + const modbit = t.modifierExpr(tx.token(field)) + + switch (field) { + case 'link': { + const expr = tx.drill(req, tx.select('url')) + return t.assignmentStmt(id, expr, false) + } + + case 'headers': { + const expr = tx.drill(req, tx.select('headers')) + return t.assignmentStmt(id, expr, false) } - return t.modifierExpr(tx.token(field), undefined, context) - } - const id = this.id(idx, field) - const optional = field === 'cookies' - return t.assignmentStmt(tx.token(id), expr(), optional) + case 'cookies': { + const expr = tx.drill( + req, + tx.select('headers'), + tx.select('set-cookie'), + modbit, + ) + return t.assignmentStmt(id, expr, true) + } + + default: { + const expr = tx.drill(req, tx.select('body'), modbit) + return t.assignmentStmt(id, expr, false) + } + } } insert(stmts: Stmt[]) { return stmts.flatMap(stmt => { - if (stmt.kind !== NodeKind.RequestStmt) { + if (stmt.kind !== 'RequestStmt') { return stmt } const idx = this.require(stmt.request) diff --git a/packages/parser/src/passes/desugar/slicedeps.ts b/packages/parser/src/passes/desugar/slicedeps.ts index b87b147..7ba6330 100644 --- a/packages/parser/src/passes/desugar/slicedeps.ts +++ b/packages/parser/src/passes/desugar/slicedeps.ts @@ -1,9 +1,10 @@ import { invariant } from '@getlang/utils' import { SliceSyntaxError } from '@getlang/utils/errors' +import { ScopeTracker, walk } from '@getlang/walker' import { parse as acorn } from 'acorn' import { traverse } from 'estree-toolkit' import globals from 'globals' -import { NodeKind, t } from '../../ast/ast.js' +import { t } from '../../ast/ast.js' import { render, tx } from '../../utils.js' import type { DesugarPass } from '../desugar.js' @@ -66,37 +67,40 @@ const analyzeSlice = (slice: string) => { return { source, deps, usesVars } } -export const insertSliceDeps: DesugarPass = () => { - return { +export const insertSliceDeps: DesugarPass = ast => { + const scope = new ScopeTracker() + return walk(ast, { + scope, SliceExpr(node) { + if (node.kind !== 'SliceExpr') { + return + } + const stat = analyzeSlice(node.slice.value) if (!stat) { return node } const { source, deps, usesVars } = stat - const slice = tx.token(source) - let context = node.context + const xnode = { ...node, slice: tx.token(source) } + + let context = scope.context if (usesVars) { - if (context?.kind !== NodeKind.ObjectLiteralExpr) { - context = t.objectLiteralExpr([], context) + if (context?.kind !== 'ObjectLiteralExpr') { + context = t.objectLiteralExpr([]) } const keys = new Set(context.entries.map(e => render(e.key))) const missing = deps.difference(keys) for (const dep of missing) { const id = tx.token(dep, dep === '$' ? '' : dep) - context.entries.push({ - key: tx.template(dep), - value: t.identifierExpr(id), - optional: false, - }) + context.entries.push( + t.objectEntryExpr(tx.template(dep), t.identifierExpr(id), false), + ) } - } else if (deps.size === 1 && !context) { - context = t.identifierExpr(tx.token('$', ''), false, context) } - return { ...node, slice, context } + return context === scope.context ? xnode : [context, xnode] }, - } + }) } diff --git a/packages/parser/src/passes/inference/calls.ts b/packages/parser/src/passes/inference/calls.ts index cd2eca9..31b8d4e 100644 --- a/packages/parser/src/passes/inference/calls.ts +++ b/packages/parser/src/passes/inference/calls.ts @@ -1,40 +1,34 @@ +import { ScopeTracker, walk } from '@getlang/walker' import type { Expr, Program } from '../../ast/ast.js' -import { isToken, NodeKind } from '../../ast/ast.js' -import type { TransformVisitor } from '../../visitor/visitor.js' -import { visit } from '../../visitor/visitor.js' -import { traceVisitor } from '../trace.js' +import { isToken } from '../../ast/ast.js' export function registerCalls(ast: Program, macros: string[] = []) { - const { scope, trace } = traceVisitor() - const mutable = visit(ast, {} as TransformVisitor) + const scope = new ScopeTracker() function registerCall(node?: Expr) { switch (node?.kind) { - case NodeKind.IdentifierExpr: { + case 'IdentifierExpr': { const id = node.id.value if (id) { return registerCall(scope.vars[id]) } const ctxs = scope.scopeStack.flatMap(s => s.contextStack) return registerCall( - ctxs.findLast( - c => c.kind !== NodeKind.IdentifierExpr || c.id.value !== '', - ), + ctxs.findLast(c => c.kind !== 'IdentifierExpr' || c.id.value !== ''), ) } - case NodeKind.SubqueryExpr: { - const ex = node.body.find(s => s.kind === NodeKind.ExtractStmt) + case 'SubqueryExpr': { + const ex = node.body.find(s => s.kind === 'ExtractStmt') return registerCall(ex?.value) } - case NodeKind.ModuleExpr: { + case 'ModuleExpr': { node.call = true } } } - const visitor: TransformVisitor = { - ...trace, - + return walk(ast, { + scope, TemplateExpr: { enter(node) { for (const el of node.elements) { @@ -47,29 +41,24 @@ export function registerCalls(ast: Program, macros: string[] = []) { }, SelectorExpr: { - enter(node, visit) { - registerCall(node.context) - return trace.SelectorExpr.enter(node, visit) + enter() { + registerCall(scope.context) }, }, ModifierExpr: { - enter(node, visit) { - registerCall(node.context) - return trace.ModifierExpr.enter(node, visit) + enter() { + registerCall(scope.context) }, }, ModuleExpr: { - enter(node, visit) { + enter(node) { const module = node.module.value if (macros.includes(module)) { registerCall(node) } - return trace.ModuleExpr.enter(node, visit) }, }, - } - - return visit(mutable, visitor) + }) } diff --git a/packages/parser/src/passes/inference/typeinfo.ts b/packages/parser/src/passes/inference/typeinfo.ts index 321d4e2..ea3662b 100644 --- a/packages/parser/src/passes/inference/typeinfo.ts +++ b/packages/parser/src/passes/inference/typeinfo.ts @@ -1,22 +1,12 @@ import { invariant } from '@getlang/utils' -import { QuerySyntaxError, ValueReferenceError } from '@getlang/utils/errors' -import type { CExpr, Program } from '../../ast/ast.js' -import { NodeKind, t } from '../../ast/ast.js' +import { QuerySyntaxError } from '@getlang/utils/errors' +import { ScopeTracker, walk } from '@getlang/walker' +import { toPath } from 'lodash-es' +import type { Program } from '../../ast/ast.js' +import { t } from '../../ast/ast.js' import type { TypeInfo } from '../../ast/typeinfo.js' import { Type } from '../../ast/typeinfo.js' -import { render, selectTypeInfo } from '../../utils.js' -import type { TransformVisitor, Visit } from '../../visitor/transform.js' -import { visit } from '../../visitor/visitor.js' -import { traceVisitor } from '../trace.js' - -const modTypeMap: Record = { - html: { type: Type.Html }, - js: { type: Type.Js }, - json: { type: Type.Value }, - link: { type: Type.Value }, - headers: { type: Type.Headers }, - cookies: { type: Type.Cookies }, -} +import { render } from '../../utils.js' function unwrap(typeInfo: TypeInfo) { switch (typeInfo.type) { @@ -25,14 +15,14 @@ function unwrap(typeInfo: TypeInfo) { case Type.Maybe: return unwrap(typeInfo.option) default: - return typeInfo + return structuredClone(typeInfo) } } function rewrap( typeInfo: TypeInfo | undefined, itemTypeInfo: TypeInfo, - optional: boolean, + optional?: boolean, ): TypeInfo { switch (typeInfo?.type) { case Type.List: @@ -79,15 +69,9 @@ type ResolveTypeOptions = { export function resolveTypes(ast: Program, options: ResolveTypeOptions) { const { returnTypes, contextType } = options - const { scope, trace } = traceVisitor(contextType) - let optional = false - function setOptional(opt: boolean, cb: () => T): T { - const last = optional - optional = opt - const ret = cb() - optional = last - return ret - } + const scope = new ScopeTracker() + + const optional: boolean[] = [false] function withContext(cb: (tnode: C, ivisit: Visit) => C) { return function enter(node: C, visit: Visit): C { @@ -110,9 +94,8 @@ export function resolveTypes(ast: Program, options: ResolveTypeOptions) { } } - const visitor: TransformVisitor = { - ...trace, - + const program: Program = walk(ast, { + scope, InputDeclStmt: { enter(node, visit) { const xnode = { ...node } @@ -128,24 +111,24 @@ export function resolveTypes(ast: Program, options: ResolveTypeOptions) { }, AssignmentStmt: { - enter(node, visit) { - const value = setOptional(node.optional, () => visit(node.value)) - return trace.AssignmentStmt({ ...node, value }) + enter(node) { + optional.push(node.optional) + }, + exit() { + optional.pop() }, }, - IdentifierExpr: { - enter: withContext((node, visit) => { - const id = node.id.value - const xnode = trace.IdentifierExpr.enter(node, visit) - const value = id ? scope.vars[id] : xnode.context || scope.context - invariant(value, new ValueReferenceError(id)) - let typeInfo = structuredClone(value.typeInfo) - if (xnode.expand) { - typeInfo = { type: Type.List, of: typeInfo } - } - return { ...xnode, typeInfo } - }), + IdentifierExpr(node) { + const value = scope.lookup(node.id.value) + const typeInfo = structuredClone(value.typeInfo) + return { ...node, typeInfo } + }, + + DrillIdentifierExpr(node) { + const value = scope.lookup(node.id.value) + const typeInfo = structuredClone(value.typeInfo) + return { ...node, typeInfo } }, RequestExpr(node) { @@ -163,52 +146,61 @@ export function resolveTypes(ast: Program, options: ResolveTypeOptions) { } }, - SliceExpr: { - enter: withContext((node, visit) => { - const xnode = trace.SliceExpr.enter(node, visit) - let typeInfo: TypeInfo = { type: Type.Value } - if (optional) { - typeInfo = { type: Type.Maybe, option: typeInfo } - } - return { ...xnode, typeInfo } - }), + SliceExpr(node) { + let typeInfo: TypeInfo = { type: Type.Value } + if (optional.at(-1)) { + typeInfo = { type: Type.Maybe, option: typeInfo } + } + return { ...node, typeInfo } }, - SelectorExpr: { - enter: withContext((node, visit) => { - const xnode = trace.SelectorExpr.enter(node, visit) - let typeInfo: TypeInfo = unwrap( - xnode.context?.typeInfo ?? { type: Type.Value }, + SelectorExpr(node) { + function selectorTypeInfo() { + invariant( + node.selector.kind === 'TemplateExpr', + new QuerySyntaxError('Selector requires template'), ) - - if (typeInfo.type === Type.Struct) { - typeInfo = selectTypeInfo(typeInfo, xnode.selector) ?? { - type: Type.Value, + const scopeT = scope.context.typeInfo + switch (scopeT.type) { + case Type.Headers: + case Type.Cookies: + return { type: Type.Value } + case Type.Struct: { + const sel = render(node.selector) + return toPath(sel).reduce( + (acc, cur) => + (acc.type === Type.Struct && acc.schema[cur]) || { + type: Type.Value, + }, + scopeT, + ) } - } else if ( - typeInfo.type === Type.Headers || - typeInfo.type === Type.Cookies - ) { - typeInfo = { type: Type.Value } - } - - if (xnode.expand) { - typeInfo = { type: Type.List, of: typeInfo } - } else if (optional) { - typeInfo = { type: Type.Maybe, option: typeInfo } + default: + return scopeT } + } - return { ...xnode, typeInfo } - }), + let typeInfo = structuredClone(selectorTypeInfo()) + if (node.expand) { + typeInfo = { type: Type.List, of: typeInfo } + } else if (optional.at(-1)) { + typeInfo = { type: Type.Maybe, option: typeInfo } + } + return { ...node, typeInfo } }, - ModifierExpr: { - enter: withContext((node, visit) => { - const xnode = trace.ModifierExpr.enter(node, visit) - const typeInfo = modTypeMap[node.modifier.value] - invariant(typeInfo, 'Modifier type lookup failed') - return { ...xnode, typeInfo } - }), + ModifierExpr(node) { + const modTypeMap: Record = { + html: { type: Type.Html }, + js: { type: Type.Js }, + json: { type: Type.Value }, + link: { type: Type.Value }, + headers: { type: Type.Headers }, + cookies: { type: Type.Cookies }, + } + const typeInfo = modTypeMap[node.modifier.value] + invariant(typeInfo, 'Modifier type lookup failed') + return { ...node, typeInfo } }, ModuleExpr: { @@ -224,52 +216,63 @@ export function resolveTypes(ast: Program, options: ResolveTypeOptions) { }), }, - SubqueryExpr: { - enter: withContext((node, visit) => { - const xnode = trace.SubqueryExpr.enter(node, visit) - const typeInfo = xnode.body.find( - stmt => stmt.kind === NodeKind.ExtractStmt, - )?.value.typeInfo ?? { type: Type.Never } - return { ...xnode, typeInfo } - }), + SubqueryExpr(node) { + const ex = node.body.findLast(s => s.kind === 'ExtractStmt') + const typeInfo = ex?.value.typeInfo || { type: Type.Never } + return { ...node, typeInfo: structuredClone(typeInfo) } }, - ObjectLiteralExpr: { - enter: withContext((node, visit) => { - const xnode = trace.ObjectLiteralExpr.enter(node, child => { - if (child === node.context) { - return visit(child) - } - const entry = node.entries.find(e => e.value === child) - invariant( - entry, - new QuerySyntaxError('Object entry missing typeinfo'), - ) - return setOptional(entry.optional, () => visit(child)) - }) + DrillExpr(node) { + const typeInfo = structuredClone(node.body.at(-1)!.typeInfo) + return { ...node, typeInfo } + }, - const typeInfo: TypeInfo = { - type: Type.Struct, - schema: Object.fromEntries( - xnode.entries.map(e => { - const key = render(e.key) - invariant( - key, - new QuerySyntaxError('Object keys must be string literals'), - ) - const value = e.value.typeInfo - return [key, value] - }), - ), - } + DrillBitExpr: { + enter() { + const ctx = scope.context + const itemCtx = ctx && { ...ctx, typeInfo: unwrap(ctx.typeInfo) } + scope.push(itemCtx) + }, + exit(node) { + scope.pop() + const ctx = scope.context + const itemTypeInfo = structuredClone(node.bit.typeInfo) + const typeInfo = ctx + ? rewrap(ctx.typeInfo, itemTypeInfo, optional.at(-1)) + : itemTypeInfo + return { ...node, typeInfo } + }, + }, - return { ...xnode, typeInfo } - }), + ObjectEntryExpr: { + enter(node) { + optional.push(node.optional) + }, + exit() { + optional.pop() + }, }, - } - const program: Program = visit(ast, visitor) - const ex = program.body.find(s => s.kind === NodeKind.ExtractStmt) + ObjectLiteralExpr(node) { + const typeInfo: TypeInfo = { + type: Type.Struct, + schema: Object.fromEntries( + node.entries.map(e => { + const key = render(e.key) + invariant( + key, + new QuerySyntaxError('Object keys must be string literals'), + ) + const value = structuredClone(e.value.typeInfo) + return [key, value] + }), + ), + } + return { ...node, typeInfo } + }, + }) + + const ex = program.body.find(s => s.kind === 'ExtractStmt') const returnType = ex?.value.typeInfo ?? { type: Type.Never } return { program, returnType } diff --git a/packages/parser/src/utils.ts b/packages/parser/src/utils.ts index 1ff33cf..c77466e 100644 --- a/packages/parser/src/utils.ts +++ b/packages/parser/src/utils.ts @@ -1,32 +1,14 @@ -import { toPath } from 'lodash-es' import type { Expr, RequestExpr } from './ast/ast.js' -import { isToken, NodeKind, t } from './ast/ast.js' -import type { Struct, TypeInfo } from './ast/typeinfo.js' -import { Type } from './ast/typeinfo.js' +import { isToken, t } from './ast/ast.js' export const render = (template: Expr) => { - if (template.kind !== NodeKind.TemplateExpr) { + if (template.kind !== 'TemplateExpr') { return null } const els = template.elements return els?.every(isToken) ? els.map(el => el.value).join('') : null } -export function selectTypeInfo( - typeInfo: Struct, - selector: Expr, -): TypeInfo | null { - const sel = render(selector) - if (!sel) { - return null - } - return toPath(sel).reduce( - (acc, cur) => - (acc.type === Type.Struct && acc.schema[cur]) || { type: Type.Value }, - typeInfo, - ) -} - export function getContentField(req: RequestExpr) { const accept = req.headers.entries.find( e => render(e.key)?.toLowerCase() === 'accept', @@ -60,8 +42,12 @@ function template(contents: string) { return t.templateExpr([token(contents)]) } -function select(selector: string, context?: Expr) { - return t.selectorExpr(template(selector), false, context) +function select(selector: string) { + return t.selectorExpr(template(selector), false) +} + +function drill(...bits: Expr[]) { + return t.drillExpr(bits.map(bit => t.drillBitExpr(bit))) } -export const tx = { token, ident, template, select } +export const tx = { token, ident, template, select, drill } diff --git a/packages/parser/src/visitor/visitor.ts b/packages/parser/src/visitor/visitor.ts index bc5e5cb..3be8682 100644 --- a/packages/parser/src/visitor/visitor.ts +++ b/packages/parser/src/visitor/visitor.ts @@ -1,6 +1,5 @@ import { wait, waitMap } from '@getlang/utils' import type { Node } from '../ast/ast.js' -import { NodeKind } from '../ast/ast.js' import type { AsyncInterpretVisitor, InterpretVisitor } from './interpret.js' import type { TransformVisitor } from './transform.js' @@ -58,5 +57,5 @@ export function visit( function isNode(value: unknown): value is Node { const kind = (value as any)?.kind - return typeof kind === 'string' && Object.keys(NodeKind).includes(kind) + return typeof kind === 'string' } diff --git a/packages/walker/package.json b/packages/walker/package.json new file mode 100644 index 0000000..18c5ee6 --- /dev/null +++ b/packages/walker/package.json @@ -0,0 +1,21 @@ +{ + "name": "@getlang/walker", + "version": "0.1.6", + "license": "Apache-2.0", + "type": "module", + "exports": { + ".": { + "bun": "./src/index.ts", + "default": "./dist/index.js" + } + }, + "bugs": { + "url": "https://github.com/getlang-dev/get/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/getlang-dev/get.git", + "directory": "packages/walker" + }, + "homepage": "https://getlang.dev" +} diff --git a/packages/walker/src/index.ts b/packages/walker/src/index.ts new file mode 100644 index 0000000..23508b3 --- /dev/null +++ b/packages/walker/src/index.ts @@ -0,0 +1,53 @@ +import type { Node } from '@getlang/parser/ast' +import { wait, waitMap } from '@getlang/utils' +import { Path } from './path.js' +import type { ScopeTracker } from './scope.js' +import type { Visitor } from './visitor.js' +import { normalize } from './visitor.js' + +export { ScopeTracker } from './scope.js' +export { Path } +export type { Visitor } + +export type WalkOptions = Visitor & { + scope?: ScopeTracker +} + +export function walk(node: Node, options: WalkOptions, parent?: Path) { + const { scope, ...visitor } = options + const { enter, exit } = normalize(visitor[node.kind]) + + scope?.enter(node) + + return wait(enter(node, parent), e => { + const entered = e || node + const path = parent?.add(entered) || new Path(entered) + + const entries = waitMap(Object.entries(path.node), e => { + const [key, value] = e + let val = value + if (Array.isArray(value)) { + val = waitMap(value, el => (isNode(el) ? walk(el, options, path) : el)) + } else if (isNode(value)) { + val = walk(value, options, path) + } + return wait(val, x => [key, x]) + }) + + const transformed = wait(entries, Object.fromEntries) + const visited = wait(transformed, t => { + return wait(exit(t, path), x => x || t) + }) + return wait(visited, xnode => { + const applied = path.apply(xnode) + scope?.exit(applied, path) + return applied + }) + }) +} + +const isNode = (test: unknown): test is Node => + typeof test === 'object' && + test !== null && + 'kind' in test && + typeof test.kind === 'string' diff --git a/packages/walker/src/path.ts b/packages/walker/src/path.ts new file mode 100644 index 0000000..796e85a --- /dev/null +++ b/packages/walker/src/path.ts @@ -0,0 +1,56 @@ +import type { Node } from '@getlang/parser/ast' +import { invariant } from '@getlang/utils' + +type Staging = { + before: Node[] +} + +type Mutation = Map + +export class Path { + private staging: Staging = { before: [] } + protected mutations: Mutation = new Map() + + constructor( + public node: Node, + private parent?: Path, + ) {} + + add(node: Node) { + return new Path(node, this) + } + + insertBefore(node: Node) { + this.staging.before.push(node) + } + + private mutate(node: Node) { + if (this.mutations.size === 0) { + return node + } + + const entries = [] + for (const [key, value] of Object.entries(node)) { + let val = value + if (Array.isArray(value)) { + val = value.flatMap(el => { + const mut = this.mutations.get(el) + const { before = [] } = mut ?? {} + return [...before, el] + }) + } + entries.push([key, val]) + } + + return Object.fromEntries(entries) + } + + apply(node: Node) { + const applied = this.mutate(node) + if (this.staging.before.length) { + invariant(this.parent, 'Unable to apply path mutations') + this.parent.mutations.set(applied, this.staging) + } + return applied + } +} diff --git a/packages/walker/src/scope.ts b/packages/walker/src/scope.ts new file mode 100644 index 0000000..2a1a42d --- /dev/null +++ b/packages/walker/src/scope.ts @@ -0,0 +1,100 @@ +import type { Node } from '@getlang/parser/ast' +import { invariant } from '@getlang/utils' +import { ValueReferenceError } from '@getlang/utils/errors' +import type { Path } from './index.js' + +class Scope { + extracted: any + constructor( + public vars: { [name: string]: any }, + public context: any, + ) {} + + lookup(id: string) { + const value = id ? this.vars[id] : this.context + invariant(value !== undefined, new ValueReferenceError(id)) + return value + } +} + +export class ScopeTracker { + scopeStack: Scope[] = [] + + push(context: any = this.head?.context) { + const vars = Object.create(this.head?.vars ?? null) + this.scopeStack.push(new Scope(vars, context)) + } + + pop() { + this.scopeStack.pop() + } + + private get head() { + return this.scopeStack.at(-1) + } + + private get ensure() { + invariant(this.head, new ValueReferenceError('Invalid scope stack')) + return this.head + } + + set context(value: any) { + this.ensure.context = value + } + + get context() { + return this.ensure.context + } + + get vars() { + return this.ensure.vars + } + + get extracted() { + return this.ensure.extracted + } + + set extracted(data: any) { + this.ensure.extracted = data + } + + lookup(id: string) { + return this.ensure.lookup(id) + } + + enter(node: Node) { + switch (node.kind) { + case 'Program': + case 'SubqueryExpr': + case 'DrillExpr': + this.push() + break + } + } + + exit(xnode: any, { node }: Path) { + switch (node.kind) { + case 'Program': + case 'SubqueryExpr': + case 'DrillExpr': + this.pop() + break + + case 'DrillBitExpr': + this.context = xnode + break + + case 'RequestStmt': + this.context = xnode.request + break + + case 'AssignmentStmt': + this.vars[node.name.value] = xnode.value + break + + case 'ExtractStmt': + this.extracted = xnode.value + break + } + } +} diff --git a/packages/walker/src/visitor.ts b/packages/walker/src/visitor.ts new file mode 100644 index 0000000..24debf2 --- /dev/null +++ b/packages/walker/src/visitor.ts @@ -0,0 +1,27 @@ +import type { Node } from '@getlang/parser/ast' +import type { Path } from './index.js' + +type Visit = + | ((node: N, path: Path) => N) + | ((node: N, path: Path) => void) +type NodeVisitor = { enter: Visit; exit: Visit } +type NodeConfig = Visit | Partial> + +export type Visitor = Partial<{ + [N in Node as N['kind']]: NodeConfig +}> + +export function normalize( + visitor?: NodeConfig, +): NodeVisitor { + if (!visitor) { + return { enter: () => {}, exit: () => {} } + } else if (typeof visitor === 'function') { + return { enter: () => {}, exit: visitor } + } else { + return { + enter: visitor.enter || (() => {}), + exit: visitor.exit || (() => {}), + } + } +} diff --git a/packages/walker/tsconfig.json b/packages/walker/tsconfig.json new file mode 100644 index 0000000..9536a0f --- /dev/null +++ b/packages/walker/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.base.json" +} diff --git a/test/values.spec.ts b/test/values.spec.ts index 0d2d079..288aa96 100644 --- a/test/values.spec.ts +++ b/test/values.spec.ts @@ -9,7 +9,7 @@ import { execute, SELSYN } from './helpers.js' describe('values', () => { test('into JS object', async () => { const result = await execute(` - set obj = \`{ a: "b" }\` + set obj = |{ a: "b" }| extract $obj -> a `) expect(result).toEqual('b') @@ -149,7 +149,7 @@ describe('values', () => { test('wide arrow expansion', async () => { const result = await execute(` - set json = \`'{"data": { "list": [{"name": "item one"}, {"name": "item two"}] } }'\` + set json = |'{"data": { "list": [{"name": "item one"}, {"name": "item two"}] } }'| extract $json -> @json => data.list -> ( extract name ) @@ -231,7 +231,7 @@ describe('values', () => { describe('js ast', () => { test('parse string', async () => { const result = await execute(` - set js = \`'var a = 2;'\` + set js = |'var a = 2;'| extract $js -> @js -> Literal `) expect(result).toEqual(2) @@ -239,7 +239,7 @@ describe('values', () => { test('select from tree', async () => { const result = await execute(` - set js = \`'var a = 2;'\` + set js = |'var a = 2;'| set ast = $js -> @js set descend = $ast -> VariableDeclaration Literal set child = $ast -> VariableDeclarator > Literal @@ -379,7 +379,7 @@ describe('values', () => { test('null, zero, or empty string are valid', async () => { const result = await execute(` - set o = \`return {"nul":null,"num":0,"str":""}\` + set o = |return {"nul":null,"num":0,"str":""}| extract { nul: $o -> nul num: $o -> num @@ -391,7 +391,7 @@ describe('values', () => { test('optional value selection', async () => { const result = await execute(` - set json = \`{ x: 'test' }\` + set json = |{ x: 'test' }| extract { x: $json -> x opt?: $json -> a -> b -> c From 782607fabbd0ba5b7a6c8132c2f9287f1bde0606 Mon Sep 17 00:00:00 2001 From: Matt Fysh Date: Fri, 29 Aug 2025 10:09:22 +1000 Subject: [PATCH 2/7] new tree walker (wip ii) --- packages/get/src/execute.ts | 131 ++++++++------- packages/get/src/modifiers.ts | 12 +- packages/parser/src/ast/ast.ts | 20 ++- packages/parser/src/ast/print.ts | 6 +- packages/parser/src/grammar/parse.ts | 6 +- packages/parser/src/passes/analyze.ts | 2 +- packages/parser/src/passes/desugar.ts | 3 +- packages/parser/src/passes/desugar/links.ts | 157 +++++++++--------- .../parser/src/passes/desugar/slicedeps.ts | 17 +- .../parser/src/passes/inference/typeinfo.ts | 25 ++- packages/parser/src/passes/trace.ts | 2 +- packages/walker/src/path.ts | 2 +- packages/walker/src/scope.ts | 4 + test/values.spec.ts | 2 +- 14 files changed, 202 insertions(+), 187 deletions(-) diff --git a/packages/get/src/execute.ts b/packages/get/src/execute.ts index e3c90c1..645a95c 100644 --- a/packages/get/src/execute.ts +++ b/packages/get/src/execute.ts @@ -3,7 +3,7 @@ import { isToken } from '@getlang/parser/ast' import type { TypeInfo } from '@getlang/parser/typeinfo' import { Type } from '@getlang/parser/typeinfo' import type { Hooks, Inputs } from '@getlang/utils' -import { invariant, NullSelection, wait, waitMap } from '@getlang/utils' +import { invariant, NullSelection } from '@getlang/utils' import * as errors from '@getlang/utils/errors' import type { WalkOptions } from '@getlang/walker' import { ScopeTracker, walk } from '@getlang/walker' @@ -33,22 +33,6 @@ export async function execute( const scope = new ScopeTracker() - // function withItemContext(cb) { - // const ctx = scope.context - // if (ctx?.typeInfo.type === Type.List) { - // scope.push() - // const list = waitMap(ctx.data, item => { - // scope.context = { data: item, typeInfo: ctx.typeInfo.of } - // return withItemContext(cb) - // }) - // return wait(list, list => { - // scope.pop() - // return list - // }) - // } - // return cb() - // } - const visitor: WalkOptions = { scope, @@ -56,45 +40,50 @@ export async function execute( * Statement nodes */ - InputDeclStmt: { - async enter(node, visit) { - const inputName = node.id.value - let inputValue = inputs[inputName] - if (inputValue === undefined) { - if (!node.optional) { - throw new NullInputError(inputName) - } - inputValue = node.defaultValue - ? await visit(node.defaultValue) - : new NullSelection(`input:${inputName}`) + InputExpr(node) { + const name = node.id.value + let data = inputs[name] + if (data === undefined) { + if (!node.optional) { + throw new NullInputError(name) + } else if (node.defaultValue) { + data = node.defaultValue.data + } else { + data = new NullSelection(`input:${name}`) } - scope.vars[inputName] = inputValue - }, + } + return { data, typeInfo: node.typeInfo } }, ExtractStmt(node) { assert(node.value) }, - Program() { - return scope.extracted + Program: { + enter() { + scope.extracted = { data: null, typeInfo: { type: Type.Value } } + }, + exit() { + return scope.extracted + }, }, /** * Expression nodes */ - TemplateExpr(node, { node: orig }) { - const firstNull = node.elements.find(el => el instanceof NullSelection) + TemplateExpr(node, path) { + const firstNull = node.elements.find( + el => el.data instanceof NullSelection, + ) if (firstNull) { - const parents = path.slice(0, -1) - const isRoot = !parents.find(n => n.kind === 'TemplateExpr') + const isRoot = path.parent?.node.kind !== 'TemplateExpr' return isRoot ? firstNull : '' } - const els = node.elements.map((el, i) => { - const og = orig.elements[i]! - return isToken(og) ? og.value : toValue(el, og.typeInfo) + const els = node.elements.map(el => { + return isToken(el) ? el.value : toValue(el.data, el.typeInfo) }) - return els.join('') + const data = els.join('') + return { data, typeInfo: node.typeInfo } }, async SliceExpr({ slice, typeInfo }) { @@ -118,12 +107,13 @@ export async function execute( }, SelectorExpr(node) { + const selector = node.selector.data invariant( - typeof node.selector === 'string', + typeof selector === 'string', new ValueTypeError('Expected selector string'), ) - const args = [scope.context.data, node.selector, node.expand] as const + const args = [scope.context.data, selector, node.expand] as const function select(typeInfo: TypeInfo) { switch (typeInfo.type) { @@ -147,8 +137,10 @@ export async function execute( }, ModifierExpr(node) { + const mod = node.modifier.value + const args = node.args.data return { - data: callModifier(node, scope.context), + data: callModifier(mod, args, scope.context), typeInfo: node.typeInfo, } }, @@ -166,7 +158,7 @@ export async function execute( ObjectEntryExpr(node) { const value = assert(node.value) - return [node.key, value.data] + return [node.key.data, value.data] }, ObjectLiteralExpr(node) { @@ -191,24 +183,24 @@ export async function execute( const ctx = scope.context const optional = node.typeInfo.type === Type.Maybe if (optional && ctx?.data instanceof NullSelection) { - return scope.context + return ctx } async function withItemContext() { const ctx = scope.context - if (ctx?.typeInfo.type === Type.List) { - const list = [] - scope.push() - for (const data of ctx.data) { - scope.context = { data, typeInfo: ctx.typeInfo.of } - const item = await withItemContext() - list.push(item) - } - scope.pop() - return list + if (ctx?.typeInfo.type !== Type.List) { + const { data } = await walk(node.bit, visitor) + return data + } + const list = [] + scope.push() + for (const data of ctx.data) { + scope.context = { data, typeInfo: ctx.typeInfo.of } + const item = await withItemContext() + list.push(item) } - const { data } = await walk(node.bit, visitor) - return data + scope.pop() + return list } const data = await withItemContext() @@ -222,18 +214,35 @@ export async function execute( async RequestExpr(node) { const method = node.method.value - const url = node.url - const body = node.body ?? '' + const url = node.url.data + const body = node.body?.data ?? '' + + const headers = node.headers.data[1] + const blocks = Object.fromEntries(node.blocks.map(v => v.data)) + const data = await http.request( method, url, - node.headers, - node.blocks, + headers, + blocks, body, hooks.request, ) return { data, typeInfo: node.typeInfo } }, + + RequestBlockExpr(node) { + const value = Object.fromEntries( + node.entries.filter(e => !(e[1] instanceof NullSelection)), + ) + const data = [node.name.value, value] + return { data, typeInfo: node.typeInfo } + }, + + RequestEntryExpr(node) { + const value = assert(node.value) + return [node.key.data, value.data] + }, } return walk(entry.program, visitor) diff --git a/packages/get/src/modifiers.ts b/packages/get/src/modifiers.ts index 1d09a3d..6e445ac 100644 --- a/packages/get/src/modifiers.ts +++ b/packages/get/src/modifiers.ts @@ -1,14 +1,16 @@ import { cookies, html, js, json } from '@getlang/lib' -import type { ModifierExpr } from '@getlang/parser/ast' + import { NullSelection } from '@getlang/utils' import { ValueReferenceError } from '@getlang/utils/errors' import type { RuntimeValue } from './value.js' import { toValue } from './value.js' -export function callModifier(node: ModifierExpr, context: RuntimeValue) { +export function callModifier( + mod: string, + args: Record, + context: RuntimeValue, +) { let { data, typeInfo } = context - - const mod = node.modifier.value if (mod === 'link') { const tag = data.type === 'tag' ? data.name : undefined if (tag === 'a') { @@ -23,7 +25,7 @@ export function callModifier(node: ModifierExpr, context: RuntimeValue) { switch (mod) { case 'link': return doc - ? new URL(doc, node.args.base).toString() + ? new URL(doc, args.base).toString() : new NullSelection('@link') case 'html': return html.parse(doc) diff --git a/packages/parser/src/ast/ast.ts b/packages/parser/src/ast/ast.ts index 6728a5e..d12079b 100644 --- a/packages/parser/src/ast/ast.ts +++ b/packages/parser/src/ast/ast.ts @@ -26,14 +26,15 @@ type AssignmentStmt = { export type DeclInputsStmt = { kind: 'DeclInputsStmt' - inputs: InputDeclStmt[] + inputs: InputExpr[] } -export type InputDeclStmt = { - kind: 'InputDeclStmt' +export type InputExpr = { + kind: 'InputExpr' id: Token optional: boolean defaultValue?: SliceExpr + typeInfo: TypeInfo } type RequestStmt = { @@ -150,10 +151,10 @@ export type Stmt = | ExtractStmt | AssignmentStmt | DeclInputsStmt - | InputDeclStmt | RequestStmt export type Expr = + | InputExpr | RequestExpr | RequestBlockExpr | RequestEntryExpr @@ -188,17 +189,18 @@ const assignmentStmt = ( value, }) -const declInputsStmt = (inputs: InputDeclStmt[]): DeclInputsStmt => ({ +const declInputsStmt = (inputs: InputExpr[]): DeclInputsStmt => ({ kind: 'DeclInputsStmt', inputs, }) -const inputDeclStmt = ( +const InputExpr = ( id: Token, optional: boolean, defaultValue?: SliceExpr, -): InputDeclStmt => ({ - kind: 'InputDeclStmt', +): InputExpr => ({ + kind: 'InputExpr', + typeInfo: { type: Type.Value }, id, optional, defaultValue, @@ -344,7 +346,7 @@ export const t = { program, assignmentStmt, declInputsStmt, - inputDeclStmt, + InputExpr, extractStmt, requestStmt, requestExpr, diff --git a/packages/parser/src/ast/print.ts b/packages/parser/src/ast/print.ts index 6eaba18..a7f2380 100644 --- a/packages/parser/src/ast/print.ts +++ b/packages/parser/src/ast/print.ts @@ -42,7 +42,7 @@ const printVisitor: Visitor = { const parts: Doc[] = [] const name = node.name.value if (name) { - parts.shift(hardline, '[', name, ']') + parts.unshift(hardline, '[', name, ']') } for (const entry of node.entries) { parts.push(hardline, entry) @@ -51,10 +51,10 @@ const printVisitor: Visitor = { }, RequestEntryExpr(node) { - return [node.kind, ': ', node.value] + return [node.key, ': ', node.value] }, - InputDeclStmt(node) { + InputExpr(node) { const parts: Doc[] = [node.id.text] if (node.optional) { parts.push('?') diff --git a/packages/parser/src/grammar/parse.ts b/packages/parser/src/grammar/parse.ts index b9f1834..f8e7abb 100644 --- a/packages/parser/src/grammar/parse.ts +++ b/packages/parser/src/grammar/parse.ts @@ -27,7 +27,7 @@ export const declInputs: PP = ([, , , , first, maybeRest]) => { export const inputDecl: PP = ([id, optional, maybeDefault]) => { const defaultValue = maybeDefault?.[3] - return t.inputDeclStmt(id, Boolean(optional || defaultValue), defaultValue) + return t.InputExpr(id, Boolean(optional || defaultValue), defaultValue) } export const request: PP = ([method, url, headerBlock, { blocks, body }]) => { @@ -36,7 +36,9 @@ export const request: PP = ([method, url, headerBlock, { blocks, body }]) => { return t.requestStmt(req) } -export const requestBlocks: PP = ([blocks, maybeBody]) => { +export const requestBlocks: PP = ([namedBlocks, maybeBody]) => { + const blocks = namedBlocks.map(d => d[1]) + const body = maybeBody?.[1] if (body) { for (const el of body.elements) { diff --git a/packages/parser/src/passes/analyze.ts b/packages/parser/src/passes/analyze.ts index c551ccf..027f3bf 100644 --- a/packages/parser/src/passes/analyze.ts +++ b/packages/parser/src/passes/analyze.ts @@ -9,7 +9,7 @@ export function analyze(ast: Program) { walk(ast, { scope, - InputDeclStmt(node) { + InputExpr(node) { inputs.add(node.id.value) }, ModuleExpr(node) { diff --git a/packages/parser/src/passes/desugar.ts b/packages/parser/src/passes/desugar.ts index 684b7c4..450f983 100644 --- a/packages/parser/src/passes/desugar.ts +++ b/packages/parser/src/passes/desugar.ts @@ -26,8 +26,7 @@ function listCalls(ast: Program) { export function desugar(ast: Program, macros: string[] = []) { const parsers = new RequestParsers() - // const visitors = [resolveContext, settleLinks, insertSliceDeps] - const visitors = [resolveContext, insertSliceDeps] + const visitors = [resolveContext, settleLinks, insertSliceDeps] let program = visitors.reduce((ast, pass) => { parsers.reset() return pass(ast, { parsers, macros }) diff --git a/packages/parser/src/passes/desugar/links.ts b/packages/parser/src/passes/desugar/links.ts index bfca376..1b18a1e 100644 --- a/packages/parser/src/passes/desugar/links.ts +++ b/packages/parser/src/passes/desugar/links.ts @@ -1,13 +1,13 @@ import { invariant } from '@getlang/utils' -import { QuerySyntaxError, ValueReferenceError } from '@getlang/utils/errors' +import { QuerySyntaxError } from '@getlang/utils/errors' +import { ScopeTracker, walk } from '@getlang/walker' import type { Expr, RequestExpr } from '../../ast/ast.js' import { t } from '../../ast/ast.js' import { render, tx } from '../../utils.js' import type { DesugarPass } from '../desugar.js' -import { traceVisitor } from '../trace.js' -export const settleLinks: DesugarPass = ({ parsers }) => { - const { scope, trace } = traceVisitor() +export const settleLinks: DesugarPass = (ast, { parsers }) => { + const scope = new ScopeTracker() const bases = new Map() function inherit(c: Expr, n: Expr) { @@ -15,77 +15,74 @@ export const settleLinks: DesugarPass = ({ parsers }) => { base && bases.set(n, base) } - return { - ...trace, - - IdentifierExpr: { - enter(node, visit) { - const id = node.id.value - const xnode = trace.IdentifierExpr.enter(node, visit) - const value = id ? scope.vars[id] : scope.context - invariant(value, new ValueReferenceError(id)) - inherit(value, xnode) - return xnode - }, + const ret = walk(ast, { + scope, + + IdentifierExpr(node) { + const value = scope.lookup(node.id.value) + inherit(value, node) }, - SelectorExpr: { - enter(node, visit) { - const xnode = trace.SelectorExpr.enter(node, visit) - invariant(xnode.context, new QuerySyntaxError('Unresolved context')) - inherit(xnode.context, xnode) - return xnode - }, + DrillExpr(node) { + inherit(node.body.at(-1), node) }, - ModifierExpr: { - enter(node, visit) { - const xnode = trace.ModifierExpr.enter(node, visit) - invariant( - xnode.args.kind === 'ObjectLiteralExpr', - new QuerySyntaxError('Modifier options must be an object'), - ) - - if (xnode.modifier.value === 'link' && xnode.context) { - const contextBase = bases.get(xnode.context) - const hasBase = xnode.args.entries.some(e => render(e.key) === 'base') - if (contextBase && !hasBase) { - xnode.args.entries.push( - t.objectEntryExpr( - tx.template('base'), - parsers.lookup(contextBase, 'url'), - ), - ) - } - } + DrillBitExpr(node) { + inherit(node.bit, node) + }, - invariant(xnode.context, new QuerySyntaxError('Unresolved context')) - inherit(xnode.context, xnode) - return xnode - }, + DrillIdentifierExpr(node) { + const value = scope.lookup(node.id.value) + inherit(value, node) }, - ModuleExpr: { - enter(node, visit) { - const tnode = { - ...node, - args: { - ...node.args, - entries: node.args.entries.map(e => { - if ( - render(e.key) !== '@link' || - (e.value.kind === 'ModifierExpr' && - e.value.modifier.value === 'link') - ) { - return e - } - const value = t.modifierExpr(tx.token('link'), undefined, e.value) - return { ...e, value } - }), - }, + SelectorExpr(node) { + invariant(scope.context, new QuerySyntaxError('Unresolved context')) + inherit(scope.context, node) + }, + + ModifierExpr(node) { + invariant( + node.args.kind === 'ObjectLiteralExpr', + new QuerySyntaxError('Modifier options must be an object'), + ) + + const ctx = scope.context + if (node.modifier.value === 'link' && ctx) { + const contextBase = bases.get(ctx) + const hasBase = node.args.entries.some(e => render(e.key) === 'base') + if (contextBase && !hasBase) { + node.args.entries.push( + t.objectEntryExpr( + tx.template('base'), + parsers.lookup(contextBase, 'link'), + ), + ) } - return trace.ModuleExpr.enter(tnode, visit) - }, + } + + invariant(ctx, new QuerySyntaxError('Unresolved context')) + inherit(ctx, node) + }, + + ModuleExpr(node) { + return { + ...node, + args: { + ...node.args, + entries: node.args.entries.map(e => { + if ( + render(e.key) !== '@link' || + (e.value.kind === 'ModifierExpr' && + e.value.modifier.value === 'link') + ) { + return e + } + const value = t.modifierExpr(tx.token('link'), undefined, e.value) + return { ...e, value } + }), + }, + } }, RequestExpr(node) { @@ -94,21 +91,19 @@ export const settleLinks: DesugarPass = ({ parsers }) => { return node }, - Program: { - enter(node, visit) { - const xnode = trace.Program.enter(node, visit) - return { ...xnode, body: parsers.insert(xnode.body) } - }, + Program(node) { + const body = parsers.insert(node.body) + return { ...node, body } }, - SubqueryExpr: { - enter(node, visit) { - let xnode = trace.SubqueryExpr.enter(node, visit) - const extracted = xnode.body.find(stmt => stmt.kind === 'ExtractStmt') - xnode = { ...xnode, body: parsers.insert(xnode.body) } - extracted && inherit(extracted.value, xnode) - return xnode - }, + SubqueryExpr(node) { + if (scope.extracted) { + inherit(scope.extracted, node) + } + const body = parsers.insert(node.body) + return { ...node, body } }, - } + }) + + return ret } diff --git a/packages/parser/src/passes/desugar/slicedeps.ts b/packages/parser/src/passes/desugar/slicedeps.ts index 7ba6330..67d8b29 100644 --- a/packages/parser/src/passes/desugar/slicedeps.ts +++ b/packages/parser/src/passes/desugar/slicedeps.ts @@ -71,14 +71,11 @@ export const insertSliceDeps: DesugarPass = ast => { const scope = new ScopeTracker() return walk(ast, { scope, - SliceExpr(node) { - if (node.kind !== 'SliceExpr') { - return - } + SliceExpr(node, path) { const stat = analyzeSlice(node.slice.value) if (!stat) { - return node + return } const { source, deps, usesVars } = stat @@ -100,7 +97,15 @@ export const insertSliceDeps: DesugarPass = ast => { } } - return context === scope.context ? xnode : [context, xnode] + if (context !== scope.context) { + invariant( + path.parent?.node.kind === 'DrillBitExpr', + 'Slice dependencies require drill expression', + ) + path.parent.insertBefore(t.drillBitExpr(context)) + } + + return xnode }, }) } diff --git a/packages/parser/src/passes/inference/typeinfo.ts b/packages/parser/src/passes/inference/typeinfo.ts index ea3662b..87a66c3 100644 --- a/packages/parser/src/passes/inference/typeinfo.ts +++ b/packages/parser/src/passes/inference/typeinfo.ts @@ -3,7 +3,6 @@ import { QuerySyntaxError } from '@getlang/utils/errors' import { ScopeTracker, walk } from '@getlang/walker' import { toPath } from 'lodash-es' import type { Program } from '../../ast/ast.js' -import { t } from '../../ast/ast.js' import type { TypeInfo } from '../../ast/typeinfo.js' import { Type } from '../../ast/typeinfo.js' import { render } from '../../utils.js' @@ -96,18 +95,13 @@ export function resolveTypes(ast: Program, options: ResolveTypeOptions) { const program: Program = walk(ast, { scope, - InputDeclStmt: { - enter(node, visit) { - const xnode = { ...node } - const dv = node.defaultValue - xnode.defaultValue = dv && setOptional(node.optional, () => visit(dv)) - const input = t.identifierExpr(node.id) - if (node.optional) { - input.typeInfo = { type: Type.Maybe, option: input.typeInfo } - } - scope.vars[node.id.value] = input - return xnode - }, + + InputExpr(node) { + let typeInfo: TypeInfo = { type: Type.Value } + if (node.optional) { + typeInfo = { type: Type.Maybe, option: typeInfo } + } + return { ...node, typeInfo } }, AssignmentStmt: { @@ -127,7 +121,10 @@ export function resolveTypes(ast: Program, options: ResolveTypeOptions) { DrillIdentifierExpr(node) { const value = scope.lookup(node.id.value) - const typeInfo = structuredClone(value.typeInfo) + let typeInfo = structuredClone(value.typeInfo) + if (node.expand) { + typeInfo = { type: Type.List, of: typeInfo } + } return { ...node, typeInfo } }, diff --git a/packages/parser/src/passes/trace.ts b/packages/parser/src/passes/trace.ts index 1148610..616c5ee 100644 --- a/packages/parser/src/passes/trace.ts +++ b/packages/parser/src/passes/trace.ts @@ -25,7 +25,7 @@ export function traceVisitor(contextType: TypeInfo = { type: Type.Context }) { const trace = { // statements with scope affect - InputDeclStmt(node) { + InputExpr(node) { scope.vars[node.id.value] = tx.select('') return node }, diff --git a/packages/walker/src/path.ts b/packages/walker/src/path.ts index 796e85a..99cacac 100644 --- a/packages/walker/src/path.ts +++ b/packages/walker/src/path.ts @@ -13,7 +13,7 @@ export class Path { constructor( public node: Node, - private parent?: Path, + public parent?: Path, ) {} add(node: Node) { diff --git a/packages/walker/src/scope.ts b/packages/walker/src/scope.ts index 2a1a42d..330befc 100644 --- a/packages/walker/src/scope.ts +++ b/packages/walker/src/scope.ts @@ -88,6 +88,10 @@ export class ScopeTracker { this.context = xnode.request break + case 'InputExpr': + this.vars[node.id.value] = xnode + break + case 'AssignmentStmt': this.vars[node.name.value] = xnode.value break diff --git a/test/values.spec.ts b/test/values.spec.ts index 288aa96..e69a586 100644 --- a/test/values.spec.ts +++ b/test/values.spec.ts @@ -43,7 +43,7 @@ describe('values', () => { test('wide arrow expands drill into variable', async () => { const result = await execute(` - set list = \`[{a: 1}, {a: 2}]\` + set list = |[{a: 1}, {a: 2}]| extract $list => $ -> a `) expect(result).toEqual([1, 2]) From 4e39531ab821a45c3502b987fea47ab8617077e9 Mon Sep 17 00:00:00 2001 From: Matt Fysh Date: Fri, 29 Aug 2025 18:15:05 +1000 Subject: [PATCH 3/7] all tests passing --- packages/get/src/execute.ts | 22 ++-- packages/get/src/modules.ts | 2 +- packages/parser/src/grammar/parse.ts | 12 +- packages/parser/src/passes/desugar/context.ts | 9 +- packages/parser/src/passes/desugar/links.ts | 84 ++++---------- packages/parser/src/passes/inference/calls.ts | 71 ++++-------- .../parser/src/passes/inference/typeinfo.ts | 50 +++----- packages/parser/src/passes/lineage.ts | 51 ++++++++ test/calls.spec.ts | 109 ++++++++++++------ 9 files changed, 212 insertions(+), 198 deletions(-) create mode 100644 packages/parser/src/passes/lineage.ts diff --git a/packages/get/src/execute.ts b/packages/get/src/execute.ts index 645a95c..7a04a36 100644 --- a/packages/get/src/execute.ts +++ b/packages/get/src/execute.ts @@ -7,7 +7,6 @@ import { invariant, NullSelection } from '@getlang/utils' import * as errors from '@getlang/utils/errors' import type { WalkOptions } from '@getlang/walker' import { ScopeTracker, walk } from '@getlang/walker' -import { withContext } from './context.js' import { callModifier } from './modifiers.js' import type { Execute } from './modules.js' import { Modules } from './modules.js' @@ -26,13 +25,13 @@ export async function execute( rootInputs: Inputs, hooks: Required, ) { + const scope = new ScopeTracker() + const executeModule: Execute = async (entry, inputs) => { const provided = new Set(Object.keys(inputs)) const unknown = provided.difference(entry.inputs) invariant(unknown.size === 0, new UnknownInputsError([...unknown])) - const scope = new ScopeTracker() - const visitor: WalkOptions = { scope, @@ -145,15 +144,14 @@ export async function execute( } }, - ModuleExpr: { - enter(node, visit) { - return withContext(scope, node, visit, async context => { - const args = await visit(node.args) - return node.call - ? modules.call(node, args, context?.typeInfo) - : toValue(args, node.args.typeInfo) - }) - }, + ModuleExpr(node) { + if (node.call) { + return modules.call(node, node.args.data, scope.context?.typeInfo) + } + return { + data: toValue(node.args.data, node.args.typeInfo), + typeInfo: node.typeInfo, + } }, ObjectEntryExpr(node) { diff --git a/packages/get/src/modules.ts b/packages/get/src/modules.ts index 0bb7e1a..ed70f14 100644 --- a/packages/get/src/modules.ts +++ b/packages/get/src/modules.ts @@ -131,7 +131,7 @@ export class Modules { if (typeof extracted === 'undefined') { extracted = await this.execute(entry, inputs) } - await this.hooks.extract(module, inputs, extracted) + await this.hooks.extract(module, inputs, extracted.data) function dropWarning(reason: string) { if (attrArgs.length) { diff --git a/packages/parser/src/grammar/parse.ts b/packages/parser/src/grammar/parse.ts index f8e7abb..94192a4 100644 --- a/packages/parser/src/grammar/parse.ts +++ b/packages/parser/src/grammar/parse.ts @@ -89,15 +89,17 @@ export const call: PP = ([callee, maybeInputs]) => { } export const link: PP = ([maybePrior, callee, _, link]) => { + const body = [] + const [context, , arrow] = maybePrior || [] + if (context) { + body.push(...context.body) + } const bit = t.moduleExpr( callee, t.objectLiteralExpr([t.objectEntryExpr(tx.template('@link'), link, true)]), ) - if (!maybePrior) { - return bit - } - const [context, , arrow] = maybePrior - return drill([context, null, arrow, null, bit]) + body.push(drillBase(bit, arrow)) + return t.drillExpr(body) } export const object: PP = d => { diff --git a/packages/parser/src/passes/desugar/context.ts b/packages/parser/src/passes/desugar/context.ts index 33a5fe5..5808e11 100644 --- a/packages/parser/src/passes/desugar/context.ts +++ b/packages/parser/src/passes/desugar/context.ts @@ -25,6 +25,7 @@ export const resolveContext: DesugarPass = (ast, { parsers, macros }) => { }, DrillBitExpr(node, path) { + const ctx = scope.context const { bit } = node const isModifier = bit.kind === 'ModifierExpr' const requireContext = @@ -32,13 +33,7 @@ export const resolveContext: DesugarPass = (ast, { parsers, macros }) => { bit.kind === 'SelectorExpr' || (bit.kind === 'ModuleExpr' && macros.includes(bit.module.value)) - if (!requireContext) { - return - } - - const ctx = scope.context - invariant(ctx, new QuerySyntaxError('Unresolved context')) - if (ctx.kind !== 'RequestExpr') { + if (!requireContext || ctx?.kind !== 'RequestExpr') { return } diff --git a/packages/parser/src/passes/desugar/links.ts b/packages/parser/src/passes/desugar/links.ts index 1b18a1e..f4813b5 100644 --- a/packages/parser/src/passes/desugar/links.ts +++ b/packages/parser/src/passes/desugar/links.ts @@ -1,46 +1,17 @@ import { invariant } from '@getlang/utils' import { QuerySyntaxError } from '@getlang/utils/errors' -import { ScopeTracker, walk } from '@getlang/walker' -import type { Expr, RequestExpr } from '../../ast/ast.js' +import { walk } from '@getlang/walker' import { t } from '../../ast/ast.js' import { render, tx } from '../../utils.js' import type { DesugarPass } from '../desugar.js' +import { LineageTracker } from '../lineage.js' export const settleLinks: DesugarPass = (ast, { parsers }) => { - const scope = new ScopeTracker() - - const bases = new Map() - function inherit(c: Expr, n: Expr) { - const base = bases.get(c) - base && bases.set(n, base) - } + const scope = new LineageTracker() const ret = walk(ast, { scope, - IdentifierExpr(node) { - const value = scope.lookup(node.id.value) - inherit(value, node) - }, - - DrillExpr(node) { - inherit(node.body.at(-1), node) - }, - - DrillBitExpr(node) { - inherit(node.bit, node) - }, - - DrillIdentifierExpr(node) { - const value = scope.lookup(node.id.value) - inherit(value, node) - }, - - SelectorExpr(node) { - invariant(scope.context, new QuerySyntaxError('Unresolved context')) - inherit(scope.context, node) - }, - ModifierExpr(node) { invariant( node.args.kind === 'ObjectLiteralExpr', @@ -49,46 +20,44 @@ export const settleLinks: DesugarPass = (ast, { parsers }) => { const ctx = scope.context if (node.modifier.value === 'link' && ctx) { - const contextBase = bases.get(ctx) + const lineage = scope.traceLineageRoot(ctx) const hasBase = node.args.entries.some(e => render(e.key) === 'base') - if (contextBase && !hasBase) { + if (lineage?.kind === 'RequestExpr' && !hasBase) { node.args.entries.push( t.objectEntryExpr( tx.template('base'), - parsers.lookup(contextBase, 'link'), + parsers.lookup(lineage, 'link'), ), ) } } - - invariant(ctx, new QuerySyntaxError('Unresolved context')) - inherit(ctx, node) }, ModuleExpr(node) { - return { - ...node, - args: { - ...node.args, - entries: node.args.entries.map(e => { - if ( - render(e.key) !== '@link' || - (e.value.kind === 'ModifierExpr' && - e.value.modifier.value === 'link') - ) { - return e - } - const value = t.modifierExpr(tx.token('link'), undefined, e.value) - return { ...e, value } - }), - }, + const linkArg = node.args.entries.find(e => render(e.key) === '@link') + if (!linkArg) { + return } + const { value } = linkArg + invariant(value.kind === 'DrillExpr', 'Module links [1]') + const base = scope.getLineage(value) + invariant(base?.kind === 'DrillBitExpr', 'Module links [2]') + if (base.bit.kind === 'ModifierExpr') { + return + } + const root = scope.traceLineageRoot(value) + invariant(root?.kind === 'RequestExpr', 'Module links [3]') + const mod = t.modifierExpr( + tx.token('link'), + t.objectLiteralExpr([ + t.objectEntryExpr(tx.template('base'), parsers.lookup(root, 'link')), + ]), + ) + value.body.push(t.drillBitExpr(mod)) }, RequestExpr(node) { parsers.visit(node) - bases.set(node, node) - return node }, Program(node) { @@ -97,9 +66,6 @@ export const settleLinks: DesugarPass = (ast, { parsers }) => { }, SubqueryExpr(node) { - if (scope.extracted) { - inherit(scope.extracted, node) - } const body = parsers.insert(node.body) return { ...node, body } }, diff --git a/packages/parser/src/passes/inference/calls.ts b/packages/parser/src/passes/inference/calls.ts index 31b8d4e..1ce330b 100644 --- a/packages/parser/src/passes/inference/calls.ts +++ b/packages/parser/src/passes/inference/calls.ts @@ -1,64 +1,43 @@ -import { ScopeTracker, walk } from '@getlang/walker' +import { walk } from '@getlang/walker' import type { Expr, Program } from '../../ast/ast.js' import { isToken } from '../../ast/ast.js' +import { LineageTracker } from '../lineage.js' export function registerCalls(ast: Program, macros: string[] = []) { - const scope = new ScopeTracker() + const scope = new LineageTracker() function registerCall(node?: Expr) { - switch (node?.kind) { - case 'IdentifierExpr': { - const id = node.id.value - if (id) { - return registerCall(scope.vars[id]) - } - const ctxs = scope.scopeStack.flatMap(s => s.contextStack) - return registerCall( - ctxs.findLast(c => c.kind !== 'IdentifierExpr' || c.id.value !== ''), - ) - } - case 'SubqueryExpr': { - const ex = node.body.find(s => s.kind === 'ExtractStmt') - return registerCall(ex?.value) - } - case 'ModuleExpr': { - node.call = true - } + const lineage = node && scope.traceLineageRoot(node) + if (lineage?.kind === 'ModuleExpr') { + lineage.call = true } } return walk(ast, { scope, - TemplateExpr: { - enter(node) { - for (const el of node.elements) { - if (!isToken(el)) { - registerCall(el) - } - } - return node - }, - }, - SelectorExpr: { - enter() { - registerCall(scope.context) - }, + TemplateExpr(node) { + for (const el of node.elements) { + if (!isToken(el)) { + registerCall(el) + } + } + return node }, - ModifierExpr: { - enter() { - registerCall(scope.context) - }, - }, + DrillBitExpr({ bit }) { + switch (bit.kind) { + case 'SelectorExpr': + case 'ModifierExpr': + registerCall(scope.context) + break - ModuleExpr: { - enter(node) { - const module = node.module.value - if (macros.includes(module)) { - registerCall(node) - } - }, + case 'ModuleExpr': + if (macros.includes(bit.module.value)) { + bit.call = true + } + break + } }, }) } diff --git a/packages/parser/src/passes/inference/typeinfo.ts b/packages/parser/src/passes/inference/typeinfo.ts index 87a66c3..205f13e 100644 --- a/packages/parser/src/passes/inference/typeinfo.ts +++ b/packages/parser/src/passes/inference/typeinfo.ts @@ -3,9 +3,10 @@ import { QuerySyntaxError } from '@getlang/utils/errors' import { ScopeTracker, walk } from '@getlang/walker' import { toPath } from 'lodash-es' import type { Program } from '../../ast/ast.js' +import { t } from '../../ast/ast.js' import type { TypeInfo } from '../../ast/typeinfo.js' import { Type } from '../../ast/typeinfo.js' -import { render } from '../../utils.js' +import { render, tx } from '../../utils.js' function unwrap(typeInfo: TypeInfo) { switch (typeInfo.type) { @@ -67,35 +68,23 @@ type ResolveTypeOptions = { } export function resolveTypes(ast: Program, options: ResolveTypeOptions) { - const { returnTypes, contextType } = options + const { returnTypes, contextType = { type: Type.Context } } = options const scope = new ScopeTracker() const optional: boolean[] = [false] - function withContext(cb: (tnode: C, ivisit: Visit) => C) { - return function enter(node: C, visit: Visit): C { - if (!node.context) { - return cb(node, visit) - } - const context = visit(node.context) - const itemContext: any = { - ...context, - typeInfo: unwrap(context.typeInfo), - } - - const ivisit: Visit = child => - child === itemContext ? itemContext : visit(child) - - const xnode = cb({ ...node, context: itemContext }, ivisit) - - const typeInfo = rewrap(context.typeInfo, xnode.typeInfo, optional) - return { ...xnode, context, typeInfo } - } - } - const program: Program = walk(ast, { scope, + Program: { + enter() { + scope.context = { + ...t.InputExpr(tx.token(''), false), + typeInfo: contextType, + } + }, + }, + InputExpr(node) { let typeInfo: TypeInfo = { type: Type.Value } if (node.optional) { @@ -200,17 +189,14 @@ export function resolveTypes(ast: Program, options: ResolveTypeOptions) { return { ...node, typeInfo } }, - ModuleExpr: { - enter: withContext((node, visit) => { - const xnode = trace.ModuleExpr.enter(node, visit) - if (!node.call) { - return { ...xnode, typeInfo: { type: Type.Value } } - } + ModuleExpr(node) { + let typeInfo: TypeInfo = { type: Type.Value } + if (node.call) { const returnType = returnTypes[node.module.value] invariant(returnType, 'Module return type lookup failed') - const typeInfo = specialize(returnType, xnode.context?.typeInfo) - return { ...xnode, typeInfo } - }), + typeInfo = specialize(returnType, scope.context?.typeInfo) + } + return { ...node, typeInfo } }, SubqueryExpr(node) { diff --git a/packages/parser/src/passes/lineage.ts b/packages/parser/src/passes/lineage.ts new file mode 100644 index 0000000..e4a430f --- /dev/null +++ b/packages/parser/src/passes/lineage.ts @@ -0,0 +1,51 @@ +import type { Path } from '@getlang/walker' +import { ScopeTracker } from '@getlang/walker' +import type { Expr, Node } from '../ast/ast.js' + +export class LineageTracker extends ScopeTracker { + private lineage = new Map() + + getLineage(expr: Expr) { + return this.lineage.get(expr) + } + + traceLineageRoot(expr: Expr) { + let parent = this.lineage.get(expr) + while (parent && this.lineage.has(parent)) { + parent = this.lineage.get(parent) + } + return parent + } + + override exit(node: Node, path: Path) { + const derive = (base: Expr) => this.lineage.set(node as Expr, base) + + switch (node.kind) { + case 'IdentifierExpr': + case 'DrillIdentifierExpr': + derive(this.lookup(node.id.value)) + break + + case 'DrillBitExpr': + derive(node.bit) + break + + case 'DrillExpr': + derive(node.body.at(-1)) + break + + case 'ModifierExpr': + case 'SelectorExpr': + derive(this.context) + break + + case 'SubqueryExpr': + if (this.extracted) { + derive(this.extracted) + } + break + } + + super.exit(node, path) + } +} diff --git a/test/calls.spec.ts b/test/calls.spec.ts index 5a872b9..f5963f3 100644 --- a/test/calls.spec.ts +++ b/test/calls.spec.ts @@ -15,49 +15,86 @@ describe('calls', () => { }) }) - test('semantics', async () => { - const modules = { - Call: ` - inputs { called } - extract { called: \`true\` } - `, - Home: ` - set called = \`false\` - set as_var = @Call({ $called }) + describe('semantics', () => { + const Call = ` + inputs { called } + extract { called: |true| } + ` + + test('select', async () => { + const modules = { + Call, + Home: ` + set called = |false| + extract { + select: @Call({ $called }) -> called + } + `, + } + const result = await execute(modules) + expect(result).toEqual({ select: true }) + }) + + test('from var', async () => { + const modules = { + Call, + Home: ` + set called = |false| + set as_var = @Call({ $called }) + extract { + from_var: $as_var -> called + } + `, + } + const result = await execute(modules) + expect(result).toEqual({ from_var: true }) + }) + + test('subquery', async () => { + const modules = { + Call, + Home: ` + set called = |false| + extract { + subquery: ( + extract @Call({ $called }) + ) -> called + } + `, + } + const result = await execute(modules) + expect(result).toEqual({ subquery: true }) + }) + + test('from subquery', async () => { + const modules = { + Call, + Home: ` + set called = |false| set as_subquery = ( extract @Call({ $called }) ) extract { - select: @Call({ $called }) -> called - from_var: $as_var -> called - subquery: ( extract @Call({ $called }) ) -> called from_subquery: $as_subquery -> called } `, - } - const result = await execute(modules) - expect(result).toEqual({ - select: true, - from_var: true, - subquery: true, - from_subquery: true, + } + const result = await execute(modules) + expect(result).toEqual({ from_subquery: true }) }) - }) - test.skip('semantics - object key', async () => { - const modules = { - Call: ` - inputs { called } - extract { called: \`true\` } - `, - Home: ` - set called = \`false\` + test.skip('object key', async () => { + const modules = { + Call, + Home: ` + set called = |false| extract { object: as_entry = { key: @Call({ $called }) } -> key -> called } `, - } - const result = await execute(modules) - expect(result).toEqual({ object: true }) + } + const result = await execute(modules) + expect(result).toEqual({ object: true }) + }) }) test('drill return value', async () => { @@ -173,7 +210,7 @@ describe('calls', () => { `, Link: ` extract { - _module: \`'Link'\` + _module: |'Link'| } `, } @@ -205,8 +242,8 @@ describe('calls', () => { GET http://stub extract { - a: @Data({text: \`'first'\`}) - b: @Data({text: \`'second'\`}) + a: @Data({ text: |'first'| }) + b: @Data({ text: |'second'| }) } `, Data: ` @@ -240,7 +277,7 @@ describe('calls', () => { Home: ` GET http://stub - extract @Data({text: \`'first'\`}) + extract @Data({text: |'first'|}) -> xpath:@data-json -> @json `, @@ -290,7 +327,7 @@ describe('calls', () => { Page: ` extract { value: @Home({ - called: \`false\` + called: |false| }) } `, From 2afd764fda2f1e9792747b387747394f304c4831 Mon Sep 17 00:00:00 2001 From: Matt Fysh Date: Sat, 30 Aug 2025 08:19:23 +1000 Subject: [PATCH 4/7] reorg --- bun.lockb | Bin 90064 -> 90680 bytes knip.json | 3 + packages/ast/package.json | 24 ++++ packages/{parser/src/ast => ast/src}/ast.ts | 3 + packages/ast/src/index.ts | 3 + packages/ast/src/typeinfo.ts | 33 +++++ packages/ast/tsconfig.json | 3 + packages/get/package.json | 2 + packages/get/src/context.ts | 47 ------- packages/get/src/execute.ts | 5 +- packages/get/src/hooks.spec.ts | 10 +- packages/get/src/modules.ts | 5 +- packages/get/src/value.ts | 6 +- packages/parser/package.json | 18 +-- packages/parser/src/ast/scope.ts | 71 ---------- packages/parser/src/ast/typeinfo.ts | 53 -------- packages/parser/src/grammar/parse.ts | 4 +- packages/parser/src/index.ts | 31 +---- packages/parser/src/parse.ts | 25 ++++ packages/parser/src/passes/analyze.ts | 2 +- packages/parser/src/passes/desugar.ts | 2 +- packages/parser/src/passes/desugar/context.ts | 2 +- packages/parser/src/passes/desugar/links.ts | 2 +- .../parser/src/passes/desugar/reqparse.ts | 4 +- .../parser/src/passes/desugar/slicedeps.ts | 2 +- packages/parser/src/passes/inference.ts | 3 +- packages/parser/src/passes/inference/calls.ts | 4 +- .../parser/src/passes/inference/typeinfo.ts | 6 +- packages/parser/src/passes/lineage.ts | 2 +- packages/parser/src/passes/trace.ts | 122 ------------------ packages/parser/src/{ast => }/print.ts | 8 +- packages/parser/src/utils.ts | 4 +- packages/parser/src/visitor/interpret.ts | 52 -------- packages/parser/src/visitor/transform.ts | 39 ------ packages/parser/src/visitor/visitor.ts | 61 --------- packages/walker/package.json | 8 +- packages/walker/src/index.ts | 2 +- packages/walker/src/path.ts | 2 +- packages/walker/src/scope.ts | 2 +- packages/walker/src/visitor.ts | 2 +- test/modules.spec.ts | 12 +- test/objects.spec.ts | 20 +-- test/request.spec.ts | 22 ++-- test/slice.spec.ts | 36 +++--- test/values.spec.ts | 68 +++++----- 45 files changed, 224 insertions(+), 611 deletions(-) create mode 100644 packages/ast/package.json rename packages/{parser/src/ast => ast/src}/ast.ts (99%) create mode 100644 packages/ast/src/index.ts create mode 100644 packages/ast/src/typeinfo.ts create mode 100644 packages/ast/tsconfig.json delete mode 100644 packages/get/src/context.ts delete mode 100644 packages/parser/src/ast/scope.ts delete mode 100644 packages/parser/src/ast/typeinfo.ts create mode 100644 packages/parser/src/parse.ts delete mode 100644 packages/parser/src/passes/trace.ts rename packages/parser/src/{ast => }/print.ts (97%) delete mode 100644 packages/parser/src/visitor/interpret.ts delete mode 100644 packages/parser/src/visitor/transform.ts delete mode 100644 packages/parser/src/visitor/visitor.ts diff --git a/bun.lockb b/bun.lockb index fde1cf8cc8ef36cffda79a590dc4908ff434626f..756f218f14429850dabd13173de93ec128b345ed 100755 GIT binary patch delta 13602 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 delta 13475 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 KL9Vi~mH!RiGLY5) diff --git a/knip.json b/knip.json index f33e519..1fe5f25 100644 --- a/knip.json +++ b/knip.json @@ -6,5 +6,8 @@ ], "ignoreBinaries": [ "open" + ], + "exclude": [ + "enumMembers" ] } diff --git a/packages/ast/package.json b/packages/ast/package.json new file mode 100644 index 0000000..0fd5db9 --- /dev/null +++ b/packages/ast/package.json @@ -0,0 +1,24 @@ +{ + "name": "@getlang/ast", + "version": "0.0.1", + "license": "Apache-2.0", + "type": "module", + "exports": { + ".": { + "bun": "./src/index.ts", + "default": "./dist/index.js" + } + }, + "bugs": { + "url": "https://github.com/getlang-dev/get/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/getlang-dev/get.git", + "directory": "packages/ast" + }, + "homepage": "https://getlang.dev", + "dependencies": { + "moo": "^0.5.2" + } +} diff --git a/packages/parser/src/ast/ast.ts b/packages/ast/src/ast.ts similarity index 99% rename from packages/parser/src/ast/ast.ts rename to packages/ast/src/ast.ts index d12079b..c620b98 100644 --- a/packages/parser/src/ast/ast.ts +++ b/packages/ast/src/ast.ts @@ -2,6 +2,9 @@ import type { Token as MooToken } from 'moo' import type { TypeInfo } from './typeinfo.js' import { Type } from './typeinfo.js' +export { Type } +export type { TypeInfo } + export type Token = Omit export function isToken(value: unknown): value is Token { return !!value && typeof value === 'object' && 'offset' in value diff --git a/packages/ast/src/index.ts b/packages/ast/src/index.ts new file mode 100644 index 0000000..0b59d73 --- /dev/null +++ b/packages/ast/src/index.ts @@ -0,0 +1,3 @@ +export * from './ast.js' +export type { TypeInfo } from './typeinfo.js' +export { Type } from './typeinfo.js' diff --git a/packages/ast/src/typeinfo.ts b/packages/ast/src/typeinfo.ts new file mode 100644 index 0000000..3a97873 --- /dev/null +++ b/packages/ast/src/typeinfo.ts @@ -0,0 +1,33 @@ +export enum Type { + Value = 'value', + Html = 'html', + Js = 'js', + Headers = 'headers', + Cookies = 'cookies', + Context = 'context', + List = 'list', + Struct = 'struct', + Never = 'never', + Maybe = 'maybe', +} + +type List = { + type: Type.List + of: TypeInfo +} + +type Struct = { + type: Type.Struct + schema: Record +} + +type Maybe = { + type: Type.Maybe + option: TypeInfo +} + +type ScalarType = { + type: Exclude +} + +export type TypeInfo = ScalarType | List | Struct | Maybe diff --git a/packages/ast/tsconfig.json b/packages/ast/tsconfig.json new file mode 100644 index 0000000..9536a0f --- /dev/null +++ b/packages/ast/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.base.json" +} diff --git a/packages/get/package.json b/packages/get/package.json index f8e5962..86a5251 100644 --- a/packages/get/package.json +++ b/packages/get/package.json @@ -17,9 +17,11 @@ }, "homepage": "https://getlang.dev", "dependencies": { + "@getlang/ast": "workspace:^0.0.1", "@getlang/lib": "workspace:^0.1.5", "@getlang/parser": "workspace:^0.3.4", "@getlang/utils": "workspace:^0.1.6", + "@getlang/walker": "workspace:^0.0.1", "lodash-es": "^4.17.21" }, "devDependencies": { diff --git a/packages/get/src/context.ts b/packages/get/src/context.ts deleted file mode 100644 index 2726df6..0000000 --- a/packages/get/src/context.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { CExpr, Expr } from '@getlang/parser/ast' -import type { RootScope } from '@getlang/parser/scope' -import type { TypeInfo } from '@getlang/parser/typeinfo' -import { Type } from '@getlang/parser/typeinfo' -import type { MaybePromise } from '@getlang/utils' -import { NullSelection } from '@getlang/utils' -import { assert } from './value.js' - -type Contextual = { value: any; typeInfo: TypeInfo } - -export async function withContext( - scope: RootScope, - node: CExpr, - visit: (node: Expr) => MaybePromise, - cb: (ctx?: Contextual) => MaybePromise, -): Promise { - async function unwrap( - context: Contextual | undefined, - cb: (ctx?: Contextual) => MaybePromise, - ): Promise { - if (context?.typeInfo.type === Type.List) { - const list = [] - for (const item of context.value) { - const itemCtx = { value: item, typeInfo: context.typeInfo.of } - list.push(await unwrap(itemCtx, cb)) - } - return list - } - - context && scope.pushContext(context.value) - const value = await cb(context) - context && scope.popContext() - return value - } - - let context: Contextual | undefined - if (node.context) { - let value = await visit(node.context) - const optional = node.typeInfo.type === Type.Maybe - value = optional ? value : assert(value) - if (value instanceof NullSelection) { - return value - } - context = { value, typeInfo: node.context.typeInfo } - } - return unwrap(context, cb) -} diff --git a/packages/get/src/execute.ts b/packages/get/src/execute.ts index 7a04a36..390080f 100644 --- a/packages/get/src/execute.ts +++ b/packages/get/src/execute.ts @@ -1,7 +1,6 @@ +import type { TypeInfo } from '@getlang/ast' +import { isToken, Type } from '@getlang/ast' import { cookies, headers, html, http, js, json } from '@getlang/lib' -import { isToken } from '@getlang/parser/ast' -import type { TypeInfo } from '@getlang/parser/typeinfo' -import { Type } from '@getlang/parser/typeinfo' import type { Hooks, Inputs } from '@getlang/utils' import { invariant, NullSelection } from '@getlang/utils' import * as errors from '@getlang/utils/errors' diff --git a/packages/get/src/hooks.spec.ts b/packages/get/src/hooks.spec.ts index ac46c44..153e379 100644 --- a/packages/get/src/hooks.spec.ts +++ b/packages/get/src/hooks.spec.ts @@ -49,26 +49,26 @@ describe('hook', () => { const modules: Record = { Top: ` inputs { inputA } - extract { value: \`"top::" + inputA\` } + extract { value: |"top::" + inputA| } `, Mid: ` - set inputA = \`"bar"\` + set inputA = |"bar"| extract { value: { topValue: @Top({ $inputA }) -> value - midValue: \`"mid"\` + midValue: |"mid"| } } `, } const src = ` - set inputA = \`"foo"\` + set inputA = |"foo"| extract { topValue: @Top({ $inputA }) -> value midValue: @Mid -> value - botValue: \`"bot"\` + botValue: |"bot"| } ` diff --git a/packages/get/src/modules.ts b/packages/get/src/modules.ts index ed70f14..0e3b323 100644 --- a/packages/get/src/modules.ts +++ b/packages/get/src/modules.ts @@ -1,7 +1,6 @@ +import type { ModuleExpr, Program, TypeInfo } from '@getlang/ast' +import { Type } from '@getlang/ast' import { analyze, desugar, inference, parse } from '@getlang/parser' -import type { ModuleExpr, Program } from '@getlang/parser/ast' -import type { TypeInfo } from '@getlang/parser/typeinfo' -import { Type } from '@getlang/parser/typeinfo' import type { Hooks, Inputs } from '@getlang/utils' import { ImportError, diff --git a/packages/get/src/value.ts b/packages/get/src/value.ts index 35a3ba2..2fcfab1 100644 --- a/packages/get/src/value.ts +++ b/packages/get/src/value.ts @@ -1,7 +1,7 @@ +import type { TypeInfo } from '@getlang/ast' +import { Type } from '@getlang/ast' import { cookies, headers, html, js } from '@getlang/lib' -import type { TypeInfo } from '@getlang/parser/typeinfo' -import { Type } from '@getlang/parser/typeinfo' -import { invariant, NullSelection } from '@getlang/utils' +import { NullSelection } from '@getlang/utils' import { NullSelectionError, ValueTypeError } from '@getlang/utils/errors' import { mapValues } from 'lodash-es' diff --git a/packages/parser/package.json b/packages/parser/package.json index 414fd00..49b91db 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -7,22 +7,6 @@ ".": { "bun": "./src/index.ts", "default": "./dist/index.js" - }, - "./ast": { - "bun": "./src/ast/ast.ts", - "default": "./dist/ast/ast.js" - }, - "./scope": { - "bun": "./src/ast/scope.ts", - "default": "./dist/ast/scope.js" - }, - "./typeinfo": { - "bun": "./src/ast/typeinfo.ts", - "default": "./dist/ast/typeinfo.js" - }, - "./visitor": { - "bun": "./src/visitor/visitor.ts", - "default": "./dist/visitor/visitor.js" } }, "bugs": { @@ -39,7 +23,9 @@ "railroad": "nearley-railroad src/grammar/getlang.ne -o grammar.html && open grammar.html" }, "dependencies": { + "@getlang/ast": "workspace:^0.0.1", "@getlang/utils": "workspace:^0.1.6", + "@getlang/walker": "workspace:^0.0.1", "@types/moo": "^0.5.10", "@types/nearley": "^2.11.5", "acorn": "^8.15.0", diff --git a/packages/parser/src/ast/scope.ts b/packages/parser/src/ast/scope.ts deleted file mode 100644 index 52e9dbb..0000000 --- a/packages/parser/src/ast/scope.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { invariant } from '@getlang/utils' -import { ValueReferenceError } from '@getlang/utils/errors' - -class Scope { - extracted: T | undefined - contextStack: T[] = [] - - constructor( - public vars: Record, - context?: T, - ) { - context && this.push(context) - } - - get context() { - return this.contextStack.at(-1) - } - - push(context: T) { - this.contextStack.push(context) - } - - pop() { - this.contextStack.pop() - } -} - -export class RootScope { - scopeStack: Scope[] = [] - - private get head() { - return this.scopeStack.at(-1) - } - - private get ensure() { - const scope = this.head - invariant(scope, new ValueReferenceError('Invalid scope stack')) - return scope - } - - get vars() { - return this.ensure.vars - } - - get context() { - return this.head?.context - } - - pushContext(context: T) { - this.ensure.push(context) - } - - popContext() { - this.ensure.pop() - } - - set extracted(data: T) { - this.ensure.extracted = data - } - - push(context: T | undefined = this.head?.context) { - const vars = Object.create(this.head?.vars ?? null) - this.scopeStack.push(new Scope(vars, context)) - } - - pop() { - const data = this.ensure.extracted - this.scopeStack.pop() - return data - } -} diff --git a/packages/parser/src/ast/typeinfo.ts b/packages/parser/src/ast/typeinfo.ts deleted file mode 100644 index 741e427..0000000 --- a/packages/parser/src/ast/typeinfo.ts +++ /dev/null @@ -1,53 +0,0 @@ -export enum Type { - Value = 'value', - Html = 'html', - Js = 'js', - Headers = 'headers', - Cookies = 'cookies', - Context = 'context', - List = 'list', - Struct = 'struct', - Never = 'never', - Maybe = 'maybe', -} - -export type List = { - type: Type.List - of: TypeInfo -} - -export type Struct = { - type: Type.Struct - schema: Record -} - -export type Maybe = { - type: Type.Maybe - option: TypeInfo -} - -type ScalarType = { - type: Exclude -} - -export type TypeInfo = ScalarType | List | Struct | Maybe - -export function tequal(a: TypeInfo, b: TypeInfo): boolean { - if (a.type === 'list' && b.type === 'list') { - return tequal(a.of, b.of) - } else if (a.type === 'struct' && b.type === 'struct') { - const ax = Object.entries(a.schema) - const bx = Object.entries(b.schema) - if (ax.length !== bx.length) { - return false - } - return ax.every(([ak, av]) => { - const bv = b.schema[ak] - return bv && tequal(av, bv) - }) - } else if (a.type === 'maybe' && b.type === 'maybe') { - return tequal(a.option, b.option) - } else { - return a.type === b.type - } -} diff --git a/packages/parser/src/grammar/parse.ts b/packages/parser/src/grammar/parse.ts index 94192a4..09e122f 100644 --- a/packages/parser/src/grammar/parse.ts +++ b/packages/parser/src/grammar/parse.ts @@ -1,7 +1,7 @@ +import type { Expr } from '@getlang/ast' +import { isToken, t } from '@getlang/ast' import { invariant } from '@getlang/utils' import { QuerySyntaxError } from '@getlang/utils/errors' -import type { Expr } from '../ast/ast.js' -import { isToken, t } from '../ast/ast.js' import { tx } from '../utils.js' type PP = nearley.Postprocessor diff --git a/packages/parser/src/index.ts b/packages/parser/src/index.ts index 89e2827..bec5167 100644 --- a/packages/parser/src/index.ts +++ b/packages/parser/src/index.ts @@ -1,31 +1,6 @@ -import { invariant } from '@getlang/utils' -import { QuerySyntaxError } from '@getlang/utils/errors' -import nearley from 'nearley' -import type { Program } from './ast/ast.js' -import lexer from './grammar/lexer.js' -import grammar from './grammar.js' - -export { lexer } -export { print } from './ast/print.js' +export { default as lexer } from './grammar/lexer.js' +export { parse } from './parse.js' export { analyze } from './passes/analyze.js' export { desugar } from './passes/desugar.js' export { inference } from './passes/inference.js' - -export function parse(source: string): Program { - const parser = new nearley.Parser(nearley.Grammar.fromCompiled(grammar)) - try { - parser.feed(source) - } catch (e: unknown) { - if (typeof e === 'object' && e && 'token' in e) { - throw new QuerySyntaxError( - lexer.formatError(e.token, 'SyntaxError: Invalid token'), - ) - } - throw e - } - - const [ast, ...rest] = parser.results - invariant(ast, new QuerySyntaxError('Unexpected end of input')) - invariant(!rest.length, new QuerySyntaxError('Unexpected parsing error')) - return ast -} +export { print } from './print.js' diff --git a/packages/parser/src/parse.ts b/packages/parser/src/parse.ts new file mode 100644 index 0000000..9998649 --- /dev/null +++ b/packages/parser/src/parse.ts @@ -0,0 +1,25 @@ +import type { Program } from '@getlang/ast' +import { invariant } from '@getlang/utils' +import { QuerySyntaxError } from '@getlang/utils/errors' +import nearley from 'nearley' +import lexer from './grammar/lexer.js' +import grammar from './grammar.js' + +export function parse(source: string): Program { + const parser = new nearley.Parser(nearley.Grammar.fromCompiled(grammar)) + try { + parser.feed(source) + } catch (e: unknown) { + if (typeof e === 'object' && e && 'token' in e) { + throw new QuerySyntaxError( + lexer.formatError(e.token, 'SyntaxError: Invalid token'), + ) + } + throw e + } + + const [ast, ...rest] = parser.results + invariant(ast, new QuerySyntaxError('Unexpected end of input')) + invariant(!rest.length, new QuerySyntaxError('Unexpected parsing error')) + return ast +} diff --git a/packages/parser/src/passes/analyze.ts b/packages/parser/src/passes/analyze.ts index 027f3bf..1ea496c 100644 --- a/packages/parser/src/passes/analyze.ts +++ b/packages/parser/src/passes/analyze.ts @@ -1,5 +1,5 @@ +import type { Program } from '@getlang/ast' import { ScopeTracker, walk } from '@getlang/walker' -import type { Program } from '../ast/ast.js' export function analyze(ast: Program) { const scope = new ScopeTracker() diff --git a/packages/parser/src/passes/desugar.ts b/packages/parser/src/passes/desugar.ts index 450f983..e73ec34 100644 --- a/packages/parser/src/passes/desugar.ts +++ b/packages/parser/src/passes/desugar.ts @@ -1,5 +1,5 @@ +import type { Program } from '@getlang/ast' import { walk } from '@getlang/walker' -import type { Program } from '../ast/ast.js' import { resolveContext } from './desugar/context.js' import { settleLinks } from './desugar/links.js' import { RequestParsers } from './desugar/reqparse.js' diff --git a/packages/parser/src/passes/desugar/context.ts b/packages/parser/src/passes/desugar/context.ts index 5808e11..6eb8f74 100644 --- a/packages/parser/src/passes/desugar/context.ts +++ b/packages/parser/src/passes/desugar/context.ts @@ -1,7 +1,7 @@ +import { t } from '@getlang/ast' import { invariant } from '@getlang/utils' import { QuerySyntaxError } from '@getlang/utils/errors' import { ScopeTracker, walk } from '@getlang/walker' -import { t } from '../../ast/ast.js' import type { DesugarPass } from '../desugar.js' export const resolveContext: DesugarPass = (ast, { parsers, macros }) => { diff --git a/packages/parser/src/passes/desugar/links.ts b/packages/parser/src/passes/desugar/links.ts index f4813b5..df51883 100644 --- a/packages/parser/src/passes/desugar/links.ts +++ b/packages/parser/src/passes/desugar/links.ts @@ -1,7 +1,7 @@ +import { t } from '@getlang/ast' import { invariant } from '@getlang/utils' import { QuerySyntaxError } from '@getlang/utils/errors' import { walk } from '@getlang/walker' -import { t } from '../../ast/ast.js' import { render, tx } from '../../utils.js' import type { DesugarPass } from '../desugar.js' import { LineageTracker } from '../lineage.js' diff --git a/packages/parser/src/passes/desugar/reqparse.ts b/packages/parser/src/passes/desugar/reqparse.ts index 39311e6..0ef6577 100644 --- a/packages/parser/src/passes/desugar/reqparse.ts +++ b/packages/parser/src/passes/desugar/reqparse.ts @@ -1,7 +1,7 @@ +import type { RequestExpr, Stmt } from '@getlang/ast' +import { t } from '@getlang/ast' import { invariant } from '@getlang/utils' import { QuerySyntaxError } from '@getlang/utils/errors' -import type { RequestExpr, Stmt } from '../../ast/ast.js' -import { t } from '../../ast/ast.js' import { getContentField, tx } from '../../utils.js' type Parsers = Record diff --git a/packages/parser/src/passes/desugar/slicedeps.ts b/packages/parser/src/passes/desugar/slicedeps.ts index 67d8b29..555451f 100644 --- a/packages/parser/src/passes/desugar/slicedeps.ts +++ b/packages/parser/src/passes/desugar/slicedeps.ts @@ -1,10 +1,10 @@ +import { t } from '@getlang/ast' import { invariant } from '@getlang/utils' import { SliceSyntaxError } from '@getlang/utils/errors' import { ScopeTracker, walk } from '@getlang/walker' import { parse as acorn } from 'acorn' import { traverse } from 'estree-toolkit' import globals from 'globals' -import { t } from '../../ast/ast.js' import { render, tx } from '../../utils.js' import type { DesugarPass } from '../desugar.js' diff --git a/packages/parser/src/passes/inference.ts b/packages/parser/src/passes/inference.ts index 912797c..180b769 100644 --- a/packages/parser/src/passes/inference.ts +++ b/packages/parser/src/passes/inference.ts @@ -1,5 +1,4 @@ -import type { Program } from '../ast/ast.js' -import type { TypeInfo } from '../ast/typeinfo.js' +import type { Program, TypeInfo } from '@getlang/ast' import { resolveTypes } from './inference/typeinfo.js' type InferenceOptions = { diff --git a/packages/parser/src/passes/inference/calls.ts b/packages/parser/src/passes/inference/calls.ts index 1ce330b..9b1516e 100644 --- a/packages/parser/src/passes/inference/calls.ts +++ b/packages/parser/src/passes/inference/calls.ts @@ -1,6 +1,6 @@ +import type { Expr, Program } from '@getlang/ast' +import { isToken } from '@getlang/ast' import { walk } from '@getlang/walker' -import type { Expr, Program } from '../../ast/ast.js' -import { isToken } from '../../ast/ast.js' import { LineageTracker } from '../lineage.js' export function registerCalls(ast: Program, macros: string[] = []) { diff --git a/packages/parser/src/passes/inference/typeinfo.ts b/packages/parser/src/passes/inference/typeinfo.ts index 205f13e..1a35fb5 100644 --- a/packages/parser/src/passes/inference/typeinfo.ts +++ b/packages/parser/src/passes/inference/typeinfo.ts @@ -1,11 +1,9 @@ +import type { Program, TypeInfo } from '@getlang/ast' +import { Type, t } from '@getlang/ast' import { invariant } from '@getlang/utils' import { QuerySyntaxError } from '@getlang/utils/errors' import { ScopeTracker, walk } from '@getlang/walker' import { toPath } from 'lodash-es' -import type { Program } from '../../ast/ast.js' -import { t } from '../../ast/ast.js' -import type { TypeInfo } from '../../ast/typeinfo.js' -import { Type } from '../../ast/typeinfo.js' import { render, tx } from '../../utils.js' function unwrap(typeInfo: TypeInfo) { diff --git a/packages/parser/src/passes/lineage.ts b/packages/parser/src/passes/lineage.ts index e4a430f..064ea14 100644 --- a/packages/parser/src/passes/lineage.ts +++ b/packages/parser/src/passes/lineage.ts @@ -1,6 +1,6 @@ +import type { Expr, Node } from '@getlang/ast' import type { Path } from '@getlang/walker' import { ScopeTracker } from '@getlang/walker' -import type { Expr, Node } from '../ast/ast.js' export class LineageTracker extends ScopeTracker { private lineage = new Map() diff --git a/packages/parser/src/passes/trace.ts b/packages/parser/src/passes/trace.ts deleted file mode 100644 index 616c5ee..0000000 --- a/packages/parser/src/passes/trace.ts +++ /dev/null @@ -1,122 +0,0 @@ -import type { CExpr, Expr } from '../ast/ast.js' -import { RootScope } from '../ast/scope.js' -import type { TypeInfo } from '../ast/typeinfo.js' -import { Type } from '../ast/typeinfo.js' -import { tx } from '../utils.js' -import type { TransformVisitor, Visit } from '../visitor/transform.js' - -export function traceVisitor(contextType: TypeInfo = { type: Type.Context }) { - const scope = new RootScope() - - function withContext( - node: C, - visit: Visit, - cb: (tnode: C) => C, - ) { - if (!node.context) { - return cb(node) - } - const context = visit(node.context) - scope.pushContext(context) - const xnode = cb({ ...node, context }) - scope.popContext() - return xnode - } - - const trace = { - // statements with scope affect - InputExpr(node) { - scope.vars[node.id.value] = tx.select('') - return node - }, - - RequestStmt(node) { - scope.pushContext(node.request) - return node - }, - - AssignmentStmt(node) { - scope.vars[node.name.value] = node.value - return node - }, - - ExtractStmt(node) { - scope.extracted = node.value - return node - }, - - Program: { - enter(node, visit) { - const programContext = tx.select('') - programContext.typeInfo = contextType - scope.push(programContext) - const body = node.body.map(visit) - scope.pop() - return { ...node, body } - }, - }, - - // contextual expressions - SubqueryExpr: { - enter(node, visit) { - return withContext(node, visit, node => { - scope.push() - const body = node.body.map(visit) - scope.pop() - return { ...node, body } - }) - }, - }, - - ObjectLiteralExpr: { - enter(node, visit) { - return withContext(node, visit, node => { - const entries = node.entries.map(e => { - const value = visit(e.value) - return { ...e, value } - }) - return { ...node, entries } - }) - }, - }, - - SelectorExpr: { - enter(node, visit) { - return withContext(node, visit, node => { - return { ...node, selector: visit(node.selector) } - }) - }, - }, - - ModifierExpr: { - enter(node, visit) { - return withContext(node, visit, node => { - return { ...node, args: visit(node.args) } - }) - }, - }, - - ModuleExpr: { - enter(node, visit) { - return withContext(node, visit, node => { - return { ...node, args: visit(node.args) } - }) - }, - }, - - // simple contextual expressions - IdentifierExpr: { - enter(node, visit) { - return withContext(node, visit, node => node) - }, - }, - - SliceExpr: { - enter(node, visit) { - return withContext(node, visit, node => node) - }, - }, - } satisfies TransformVisitor - - return { scope, trace } -} diff --git a/packages/parser/src/ast/print.ts b/packages/parser/src/print.ts similarity index 97% rename from packages/parser/src/ast/print.ts rename to packages/parser/src/print.ts index a7f2380..fb7becd 100644 --- a/packages/parser/src/ast/print.ts +++ b/packages/parser/src/print.ts @@ -1,9 +1,9 @@ +import type { Node } from '@getlang/ast' +import { isToken } from '@getlang/ast' import type { Visitor } from '@getlang/walker' import { walk } from '@getlang/walker' import { builders, printer } from 'prettier/doc' -import { render } from '../utils.js' -import type { Node } from './ast.js' -import { isToken } from './ast.js' +import { render } from './utils.js' type Doc = builders.Doc @@ -141,6 +141,8 @@ const printVisitor: Visitor = { return true case 'DrillExpr': return e.value.body.at(-1).bit.kind === 'SelectorExpr' + default: + return false } }) const sep = ifBreak(line, [',', line]) diff --git a/packages/parser/src/utils.ts b/packages/parser/src/utils.ts index c77466e..c49aa5c 100644 --- a/packages/parser/src/utils.ts +++ b/packages/parser/src/utils.ts @@ -1,5 +1,5 @@ -import type { Expr, RequestExpr } from './ast/ast.js' -import { isToken, t } from './ast/ast.js' +import type { Expr, RequestExpr } from '@getlang/ast' +import { isToken, t } from '@getlang/ast' export const render = (template: Expr) => { if (template.kind !== 'TemplateExpr') { diff --git a/packages/parser/src/visitor/interpret.ts b/packages/parser/src/visitor/interpret.ts deleted file mode 100644 index aeae13d..0000000 --- a/packages/parser/src/visitor/interpret.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { MaybePromise } from '@getlang/utils' -import type { Expr, Node, Stmt } from '../ast/ast.js' - -type Transform = V extends V - ? V extends Stmt - ? S - : V extends Expr - ? E - : { [K in keyof V]: Transform } - : never - -type TransformNode = { - [K in keyof N]: Transform -} - -type NodeConfig< - N extends Node, - ST, - ET, - A extends boolean = false, - TN = TransformNode, - XN = Transform, - Visit = ( - child: C, - ) => A extends true - ? MaybePromise> - : Transform, - EntryVisitor = ( - node: N, - visit: Visit, - ) => A extends true ? MaybePromise : TN, - ExitVisitor = ( - node: TN, - path: Node[], - originalNode: N, - ) => A extends true ? MaybePromise : XN, - EntryExitVisitor = ( - node: N, - visit: Visit, - ) => A extends true ? MaybePromise : XN, -> = - | ExitVisitor - | { enter?: EntryVisitor; exit?: ExitVisitor } - | { enter: EntryExitVisitor } - -export type InterpretVisitor = { - [N in Node as N['kind']]: NodeConfig -} - -export type AsyncInterpretVisitor = { - [N in Node as N['kind']]: NodeConfig -} diff --git a/packages/parser/src/visitor/transform.ts b/packages/parser/src/visitor/transform.ts deleted file mode 100644 index 0ef461d..0000000 --- a/packages/parser/src/visitor/transform.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Expr, Node, Stmt } from '../ast/ast.js' -import type { TypeInfo } from '../ast/typeinfo.js' - -type FilterNarrow = Expr extends T - ? FilterNarrow> - : T extends Stmt | TypeInfo - ? never - : T extends Expr - ? T - : T extends readonly (infer E)[] - ? FilterNarrow - : T extends object - ? Collect - : never - -type Collect = T extends T - ? { [K in keyof T]: FilterNarrow }[keyof T] - : never - -type NarrowExpr = Collect - -type Transform = N extends NarrowExpr ? N : N extends Expr ? Expr : Stmt -export type Visit = (child: C) => Transform - -type NodeConfig< - N extends Node, - TN = Transform, - EntryVisitor = (node: N, visit: Visit) => TN, - ExitVisitor = (node: N) => TN, -> = - | ExitVisitor - | { - enter?: EntryVisitor - exit?: ExitVisitor - } - -export type TransformVisitor = { - [N in Node as N['kind']]?: NodeConfig -} diff --git a/packages/parser/src/visitor/visitor.ts b/packages/parser/src/visitor/visitor.ts deleted file mode 100644 index 3be8682..0000000 --- a/packages/parser/src/visitor/visitor.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { wait, waitMap } from '@getlang/utils' -import type { Node } from '../ast/ast.js' -import type { AsyncInterpretVisitor, InterpretVisitor } from './interpret.js' -import type { TransformVisitor } from './transform.js' - -export type { TransformVisitor, InterpretVisitor, AsyncInterpretVisitor } - -interface NodeVisitor { - enter?: (node: Node, visit: (c: Node) => any) => any - exit?: (node: any, path: Node[], originalNode: Node) => any -} - -type Visitor = - | TransformVisitor - | InterpretVisitor - | AsyncInterpretVisitor - -export function visit( - node: N, - visitor: V, -): any { - function impl(node: N, path: Node[]) { - function transform(value: T, isRoot?: boolean): any { - if (!isRoot && isNode(value)) { - return impl(value, path) - } else if (Array.isArray(value)) { - return waitMap(value, el => transform(el)) - } else if (typeof value === 'object' && value) { - const entries = waitMap(Object.entries(value), e => - wait(transform(e[1]), v => [e[0], v]), - ) - return wait(entries, e => Object.fromEntries(e)) - } else { - return value - } - } - - const config = visitor[node.kind] ?? {} - - const { enter, exit } = ( - typeof config === 'function' ? { exit: config } : config - ) as NodeVisitor - - path.push(node) - const tnode = enter - ? enter(node, n => impl(n, path)) - : transform(node, true) - const xnode = wait(tnode, t => (exit ? exit(t, path, node) : t)) - return wait(xnode, x => { - path.pop() - return x - }) - } - - return impl(node, []) -} - -function isNode(value: unknown): value is Node { - const kind = (value as any)?.kind - return typeof kind === 'string' -} diff --git a/packages/walker/package.json b/packages/walker/package.json index 18c5ee6..d606acc 100644 --- a/packages/walker/package.json +++ b/packages/walker/package.json @@ -1,6 +1,6 @@ { "name": "@getlang/walker", - "version": "0.1.6", + "version": "0.0.1", "license": "Apache-2.0", "type": "module", "exports": { @@ -17,5 +17,9 @@ "url": "git+https://github.com/getlang-dev/get.git", "directory": "packages/walker" }, - "homepage": "https://getlang.dev" + "homepage": "https://getlang.dev", + "dependencies": { + "@getlang/ast": "workspace:^0.0.1", + "@getlang/utils": "workspace:^0.1.6" + } } diff --git a/packages/walker/src/index.ts b/packages/walker/src/index.ts index 23508b3..70375cc 100644 --- a/packages/walker/src/index.ts +++ b/packages/walker/src/index.ts @@ -1,4 +1,4 @@ -import type { Node } from '@getlang/parser/ast' +import type { Node } from '@getlang/ast' import { wait, waitMap } from '@getlang/utils' import { Path } from './path.js' import type { ScopeTracker } from './scope.js' diff --git a/packages/walker/src/path.ts b/packages/walker/src/path.ts index 99cacac..4a77e83 100644 --- a/packages/walker/src/path.ts +++ b/packages/walker/src/path.ts @@ -1,4 +1,4 @@ -import type { Node } from '@getlang/parser/ast' +import type { Node } from '@getlang/ast' import { invariant } from '@getlang/utils' type Staging = { diff --git a/packages/walker/src/scope.ts b/packages/walker/src/scope.ts index 330befc..08cdfae 100644 --- a/packages/walker/src/scope.ts +++ b/packages/walker/src/scope.ts @@ -1,4 +1,4 @@ -import type { Node } from '@getlang/parser/ast' +import type { Node } from '@getlang/ast' import { invariant } from '@getlang/utils' import { ValueReferenceError } from '@getlang/utils/errors' import type { Path } from './index.js' diff --git a/packages/walker/src/visitor.ts b/packages/walker/src/visitor.ts index 24debf2..943fc1f 100644 --- a/packages/walker/src/visitor.ts +++ b/packages/walker/src/visitor.ts @@ -1,4 +1,4 @@ -import type { Node } from '@getlang/parser/ast' +import type { Node } from '@getlang/ast' import type { Path } from './index.js' type Visit = diff --git a/test/modules.spec.ts b/test/modules.spec.ts index 30d9495..f1ff3d2 100644 --- a/test/modules.spec.ts +++ b/test/modules.spec.ts @@ -65,7 +65,7 @@ describe('modules', () => { test('optional input', async () => { const src = ` inputs { value? } - extract \`12\` + extract |12| ` const result = await execute(src) // does not throw error @@ -74,7 +74,7 @@ describe('modules', () => { test('default value', async () => { const src = ` - inputs { stopA = \`'big sur'\`, stopB = \`'carmel'\` } + inputs { stopA = |'big sur'|, stopB = |'carmel'| } extract { $stopA, $stopB } ` @@ -87,7 +87,7 @@ describe('modules', () => { test('falsy default value', async () => { const src = ` - inputs { offset = \`0\` } + inputs { offset = |0| } extract { $offset } ` const result = await execute(src, {}) @@ -97,7 +97,7 @@ describe('modules', () => { test('variables', async () => { const result = await execute(` - set x = \`{ test: true }\` + set x = |{ test: true }| extract $x `) expect(result).toEqual({ test: true }) @@ -105,7 +105,7 @@ describe('modules', () => { test('subquery scope with context', async () => { const result = await execute(` - set x = \`{ test: true }\` + set x = |{ test: true }| extract $x -> ( extract $ ) `) expect(result).toEqual({ test: true }) @@ -113,7 +113,7 @@ describe('modules', () => { test('subquery scope with closures', async () => { const result = await execute(` - set x = \`{ test: true }\` + set x = |{ test: true }| extract ( extract $x ) diff --git a/test/objects.spec.ts b/test/objects.spec.ts index 01265ac..fa49593 100644 --- a/test/objects.spec.ts +++ b/test/objects.spec.ts @@ -9,7 +9,7 @@ describe('objects', () => { test('variable', async () => { const result = await execute(` - set x = { test: \`true\` } + set x = { test: |true| } extract $x `) expect(result).toEqual({ test: true }) @@ -17,7 +17,7 @@ describe('objects', () => { test('variable ref', async () => { const result = await execute(` - set x = \`"varref"\` + set x = |"varref"| extract { x: $x } `) expect(result).toEqual({ x: 'varref' }) @@ -25,7 +25,7 @@ describe('objects', () => { test('variable ref shorthand', async () => { const result = await execute(` - set x = \`"varref"\` + set x = |"varref"| extract { $x } `) expect(result).toEqual({ x: 'varref' }) @@ -33,12 +33,12 @@ describe('objects', () => { test('variable combination', async () => { const result = await execute(` - set foo = \`"foo"\` - set bar = \`"bar"\` + set foo = |"foo"| + set bar = |"bar"| extract { $foo, bar: $bar - baz: \`"baz"\` + baz: |"baz"| xyz: $foo } `) @@ -54,9 +54,9 @@ describe('objects', () => { test('nested inline', async () => { const result = await execute(` extract { - pos: \`"outer"\`, + pos: |"outer"|, value: { - pos: \`"inner"\` + pos: |"inner"| } } `) @@ -70,9 +70,9 @@ describe('objects', () => { test('nested variable ref shorthand', async () => { const result = await execute(` - set value = { pos: \`"inner"\` } + set value = { pos: |"inner"| } extract { - pos: \`"outer"\`, + pos: |"outer"|, $value } `) diff --git a/test/request.spec.ts b/test/request.spec.ts index 2debf3d..79fd4a1 100644 --- a/test/request.spec.ts +++ b/test/request.spec.ts @@ -75,7 +75,7 @@ describe('request', () => { test('identifier', async () => { await execute(` - set ident = \`'http://ident.com'\` + set ident = |'http://ident.com'| GET $ident `) await expect(mockFetch).toHaveServed( @@ -87,7 +87,7 @@ describe('request', () => { test('interpolated', async () => { await execute(` - set query = \`'monterey'\` + set query = |'monterey'| GET https://boogle.com/search/$query `) await expect(mockFetch).toHaveServed( @@ -99,7 +99,7 @@ describe('request', () => { test('interpolated expression', async () => { await execute(` - set query = \`'big sur'\` + set query = |'big sur'| GET https://ging.com/\${query}_results `) await expect(mockFetch).toHaveServed( @@ -111,7 +111,7 @@ describe('request', () => { test('interpolated value', async () => { await execute(` - set loc = \`'
sea ranch
'\` -> @html + set loc = |'
sea ranch
'| -> @html GET https://goto.ca/:loc `) await expect(mockFetch).toHaveServed( @@ -122,7 +122,7 @@ describe('request', () => { test('headers', async () => { await execute(` - set token = \`123\` + set token = |123| GET http://api.unweb.com Authorization: Bearer $token @@ -143,8 +143,8 @@ describe('request', () => { describe('blocks', () => { test('querystring', async () => { await execute(` - set ident = \`"b"\` - set interp = \`"olated"\` + set ident = |"b"| + set interp = |"olated"| GET https://example.com X-Test: true @@ -201,7 +201,7 @@ describe('request', () => { test('raw body', async () => { await execute(` - set hello = \`"hi there"\` + set hello = |"hi there"| POST https://example.com [body] @@ -223,7 +223,7 @@ describe('request', () => { test('omits undefined', async () => { await execute(` - set foo? = \`undefined\` + set foo? = |undefined| POST https://example.com X-Foo: $foo @@ -253,7 +253,7 @@ describe('request', () => { test('optional template groups', async () => { await execute(` - set foo? = \`undefined\` + set foo? = |undefined| GET https://example.com/pre$[/:foo]/post X-Foo: $foo @@ -346,7 +346,7 @@ describe('request', () => { test('updates subquery context', async () => { const result = await execute(` - set x? = \`'

'\` -> @html -> ( + set x? = |'

'| -> @html -> ( GET http://example.com extract h1 diff --git a/test/slice.spec.ts b/test/slice.spec.ts index b9136d0..072a3e6 100644 --- a/test/slice.spec.ts +++ b/test/slice.spec.ts @@ -11,10 +11,10 @@ describe('slice', () => { it('has access to script variables', async () => { const result = await execute( ` - inputs { id, foo = \`[1]\` } + inputs { id, foo = |[1]| } - set bar = \`['x']\` - set baz = \`{ id, foo, bar }\` + set bar = |['x']| + set baz = |{ id, foo, bar }| extract { $baz } `, @@ -35,12 +35,12 @@ describe('slice', () => { // variables should not cause ConversionError's (ie the BinaryExpression) const result = await execute( ` - set js = \`'5 + 1'\` + set js = |'5 + 1'| set x = $js -> @js -> BinaryExpression set y = $js -> @js -> Literal set out = ( extract { - greet: \`'hi there ' + y\` + greet: |'hi there ' + y| } ) extract $out @@ -55,9 +55,9 @@ describe('slice', () => { it('can reference and convert context', async () => { const result = await execute( ` - set html = \`'

title

para 1

para 2

'\` + set html = |'

title

para 1

para 2

'| - extract $html -> @html => h1, p -> \`$\` + extract $html -> @html => h1, p -> |$| `, ) expect(result).toEqual(['title', 'para 1', 'para 2']) @@ -65,9 +65,9 @@ describe('slice', () => { it('can reference both context and outer variables', async () => { const result = await execute(` - set obj = \`{key:"x"}\` - set html = \`"
keyval
"\` -> @html - extract $html -> span -> \`obj[$]\` + set obj = |{key:"x"}| + set html = |"
keyval
"| -> @html + extract $html -> span -> |obj[$]| `) expect(result).toEqual('x') }) @@ -84,26 +84,26 @@ describe('slice', () => { it('converts context to value prior to run', async () => { const result = await execute(` - set html = \`'
  • one
  • two
'\` - extract $html -> @html -> xpath://li -> \`$\` + set html = |'
  • one
  • two
'| + extract $html -> @html -> xpath://li -> |$| `) expect(result).toEqual('one') }) it('operates on list item context', async () => { const result = await execute(` - set html = \`'
  • one
  • two
'\` - extract $html -> @html => li -> \`$\` + set html = |'
  • one
  • two
'| + extract $html -> @html => li -> |$| `) expect(result).toEqual(['one', 'two']) }) it('supports analysis on nested slices', async () => { const result = await execute(` - set x = \`0\` + set x = |0| extract $x -> ( - set y = \`1\` + set y = |1| extract $y ) `) @@ -113,7 +113,7 @@ describe('slice', () => { describe('errors', () => { test.skip('parsing', () => { const result = execute(` - extract \`{ a: "b" \` + extract |{ a: "b" | `) // expect(result).rejects.toThrow() @@ -122,7 +122,7 @@ describe('slice', () => { test('running', () => { const result = execute(` - extract \`({}).no.no.yes\` + extract |({}).no.no.yes| `) return expect(result).rejects.toThrow( /^An exception was thrown by the client-side slice/, diff --git a/test/values.spec.ts b/test/values.spec.ts index e69a586..4b7b198 100644 --- a/test/values.spec.ts +++ b/test/values.spec.ts @@ -17,7 +17,7 @@ describe('values', () => { test('unbound drills can be variable ref', async () => { const result = await execute(` - set x = \`{ a: "b" }\` + set x = |{ a: "b" }| extract $x -> a `) expect(result).toEqual('b') @@ -25,7 +25,7 @@ describe('values', () => { test('nested drills into JSON', async () => { const result = await execute(` - set x = \`{ a: { b: ["c", "d"] } }\` + set x = |{ a: { b: ["c", "d"] } }| extract $x -> a -> b[1] `) expect(result).toEqual('d') @@ -33,9 +33,9 @@ describe('values', () => { test('nested drill not a variable ref', async () => { const result = await execute(` - set a = \`"unused A"\` - set b = \`"unused B"\` - set obj = \`{ a: { b: "c" } }\` + set a = |"unused A"| + set b = |"unused B"| + set obj = |{ a: { b: "c" } }| extract $obj -> a -> b `) expect(result).toEqual('c') @@ -51,7 +51,7 @@ describe('values', () => { test('wide arrow expands drill into context', async () => { const result = await execute(` - set list = \`[{a: 1}, {a: 2}]\` + set list = |[{a: 1}, {a: 2}]| extract $list => $ -> ( extract a ) @@ -63,7 +63,7 @@ describe('values', () => { // nested scope context is an element of `list` // `n` in the nested scope selects from this context const result = await execute(` - set list = \`[{n:'one'},{n:'two'},{n:'three'}]\` + set list = |[{n:'one'},{n:'two'},{n:'three'}]| extract $list => $ -> ( extract n ) @@ -73,7 +73,7 @@ describe('values', () => { test('make reference to context variable ($)', async () => { const result = await execute(` - set list = \`['one','two','three']\` + set list = |['one','two','three']| extract $list => $ -> { id: $ } @@ -83,13 +83,13 @@ describe('values', () => { test('thin arrow does not expand list', async () => { let result = await execute(` - set list = \`[{a: 1}, {a: 2}]\` + set list = |[{a: 1}, {a: 2}]| extract $list -> 0 `) expect(result).toEqual({ a: 1 }) result = await execute(` - set list = \`[{a: 1}, {a: 2}]\` + set list = |[{a: 1}, {a: 2}]| extract $list -> ( extract [1].a ) @@ -99,7 +99,7 @@ describe('values', () => { test('list of lists', async () => { const result = await execute(` - set data = \`[ {list: [{n: "one"}, {n: "two"}]}, {list: [{n: "three"}, {n: "four"}]} ]\` + set data = |[ {list: [{n: "one"}, {n: "two"}]}, {list: [{n: "three"}, {n: "four"}]} ]| extract $data => $ => list -> n `) @@ -112,7 +112,7 @@ describe('values', () => { test('values not closed until final extract stmt', async () => { const result = await execute(` set fn_out = ( - set html = \`"

unweb

"\` + set html = |"

unweb

"| extract { doc: $html -> @html } @@ -125,7 +125,7 @@ describe('values', () => { describe('json', () => { test('parse string', async () => { const result = await execute(` - set json = \`'{"test": true }'\` + set json = |'{"test": true }'| extract $json -> @json `) expect(result).toEqual({ test: true }) @@ -133,7 +133,7 @@ describe('values', () => { test('select from value', async () => { const result = await execute(` - set json = \`'{"test": true }'\` + set json = |'{"test": true }'| extract $json -> @json -> test `) expect(result).toEqual(true) @@ -141,7 +141,7 @@ describe('values', () => { test('nested selectors', async () => { const result = await execute(` - set json = \`'{"data": { "list": ["item one", "item two"] } }'\` + set json = |'{"data": { "list": ["item one", "item two"] } }'| extract $json -> @json -> data -> list[1] `) expect(result).toEqual('item two') @@ -161,7 +161,7 @@ describe('values', () => { describe('html', () => { test('parse string', async () => { const result = await execute(` - set html = \`"

unweb

"\` + set html = |"

unweb

"| extract $html -> @html `) expect(result).toEqual('unweb') @@ -169,7 +169,7 @@ describe('values', () => { test('select from doc', async () => { const result = await execute(` - set html = \`"

unweb

welcome

"\` + set html = |"

unweb

welcome

"| extract $html -> @html -> p `) expect(result).toEqual('welcome') @@ -177,7 +177,7 @@ describe('values', () => { test.if(SELSYN)('css parsing error', () => { const result = execute(` - set html = \`'
test
'\` + set html = |'
test
'| extract $html -> @html -> p/*&@#^ `) return expect(result).rejects.toThrow( @@ -187,7 +187,7 @@ describe('values', () => { test('nested selectors', async () => { const result = await execute(` - set html = \`"

unweb

  • item one
  • item two
"\` + set html = |"

unweb

  • item one
  • item two
"| extract $html -> @html -> ul -> li:nth-child(2) `) expect(result).toEqual('item two') @@ -195,7 +195,7 @@ describe('values', () => { test('xpath selector', async () => { const result = await execute(` - set html = \`"

unweb

welcome

"\` + set html = |"

unweb

welcome

"| extract $html -> @html -> xpath://p/@class `) expect(result).toEqual('intro') @@ -203,7 +203,7 @@ describe('values', () => { test.if(SELSYN)('xpath parsing error', async () => { const result = execute(` - set html = \`'
test
'\` + set html = |'
test
'| extract $html -> @html -> xpath:p/*&@#^ `) return expect(result).rejects.toThrow( @@ -213,7 +213,7 @@ describe('values', () => { test('wide arrow expansion', async () => { const result = await execute(` - set html = \`"

unweb

  • item one
  • item two
"\` + set html = |"

unweb

  • item one
  • item two
"| extract $html -> @html => ul li `) expect(result).toEqual(['item one', 'item two']) @@ -221,7 +221,7 @@ describe('values', () => { test('drilling into items in an expanded lists', async () => { const result = await execute(` - set html = \`"

unweb

  • item one
  • item two
"\` + set html = |"

unweb

  • item one
  • item two
"| extract $html -> @html => ul li -> span `) expect(result).toEqual(['one', 'two']) @@ -251,7 +251,7 @@ describe('values', () => { test.if(SELSYN)('esquery parsing error', () => { const result = execute(` - set js = \`'console.log(1 + 2)'\` + set js = |'console.log(1 + 2)'| extract $js -> @js -> Litera#$*& ><<>F `) return expect(result).rejects.toThrow( @@ -261,7 +261,7 @@ describe('values', () => { test('select non-literal from tree throws conversion error', () => { const result = execute(` - set js = \`'var a = 2;'\` + set js = |'var a = 2;'| extract $js -> @js -> Identifier `) return expect(result).rejects.toThrow(new ConversionError('Identifier')) @@ -269,7 +269,7 @@ describe('values', () => { test('nested selector', async () => { const result = await execute(` - set js = \`'var a = 501;'\` + set js = |'var a = 501;'| extract $js -> @js -> VariableDeclarator -> Literal `) expect(result).toEqual(501) @@ -277,7 +277,7 @@ describe('values', () => { test('wide arrow expansion', async () => { const result = await execute(` - set js = \`'var a = 501; var x = "many"'\` + set js = |'var a = 501; var x = "many"'| extract $js -> @js => Literal `) expect(result).toEqual([501, 'many']) @@ -319,7 +319,7 @@ describe('values', () => { describe('cookies', () => { test('parse string', async () => { const result = await execute(` - set cookies = \`"gt=1326368972816650241; Max-Age=10800; Domain=.twitter.com; Path=/; Secure"\` + set cookies = |"gt=1326368972816650241; Max-Age=10800; Domain=.twitter.com; Path=/; Secure"| extract $cookies -> @cookies `) expect(result).toEqual({ gt: '1326368972816650241' }) @@ -327,7 +327,7 @@ describe('values', () => { test('select from cookie set', async () => { const result = await execute(` - set cookies = \`"gt=1326368972816650241; Max-Age=10800; Domain=.twitter.com; Path=/; Secure"\` + set cookies = |"gt=1326368972816650241; Max-Age=10800; Domain=.twitter.com; Path=/; Secure"| extract $cookies -> @cookies -> gt `) expect(result).toEqual('1326368972816650241') @@ -363,7 +363,7 @@ describe('values', () => { describe('optional v required', () => { test('error when html selector fails to locate', () => { const result = execute(` - set html = \`'
test
'\` + set html = |'
test
'| extract $html -> @html -> p `) return expect(result).rejects.toThrow(new NullSelectionError('p')) @@ -371,7 +371,7 @@ describe('values', () => { test('error when json selector fails to locate', () => { const result = execute(` - set val = \`{x: 1}\` + set val = |{x: 1}| extract $val -> y `) return expect(result).rejects.toThrow(new NullSelectionError('y')) @@ -402,7 +402,7 @@ describe('values', () => { test('optional html selection', async () => { const result = await execute(` - set html = \`'
test
'\` -> @html + set html = |'
test
'| -> @html extract { el?: $html -> p } @@ -412,7 +412,7 @@ describe('values', () => { test('optional html selection chaining', async () => { const result = await execute(` - set html = \`'
test
'\` -> @html + set html = |'
test
'| -> @html extract { el?: $html -> p -> span } @@ -422,7 +422,7 @@ describe('values', () => { test('optional js selection', async () => { const result = await execute(` - set js = \`'const test = {};'\` -> @js + set js = |'const test = {};'| -> @js extract { val?: $js -> Literal } From 48122dd2ce22f47d3a557433664fb31ac709c4f0 Mon Sep 17 00:00:00 2001 From: Matt Fysh Date: Sat, 30 Aug 2025 16:50:01 +1000 Subject: [PATCH 5/7] remove drill bit nodes --- packages/ast/src/ast.ts | 18 +--- packages/get/src/execute.ts | 98 ++++++++++--------- packages/get/src/modifiers.ts | 5 +- packages/parser/src/grammar/parse.ts | 40 ++++---- packages/parser/src/passes/desugar.ts | 4 +- packages/parser/src/passes/desugar/context.ts | 32 +++--- .../parser/src/passes/desugar/dropdrill.ts | 17 ++++ packages/parser/src/passes/desugar/links.ts | 6 +- .../parser/src/passes/desugar/reqparse.ts | 10 +- .../parser/src/passes/desugar/slicedeps.ts | 4 +- packages/parser/src/passes/inference/calls.ts | 29 +++--- .../parser/src/passes/inference/typeinfo.ts | 67 +++++++------ packages/parser/src/passes/lineage.ts | 4 - packages/parser/src/print.ts | 26 +++-- packages/parser/src/utils.ts | 6 +- packages/walker/src/index.ts | 54 +++++----- packages/walker/src/path.ts | 5 + packages/walker/src/scope.ts | 17 ++-- packages/walker/src/visitor.ts | 3 +- 19 files changed, 232 insertions(+), 213 deletions(-) create mode 100644 packages/parser/src/passes/desugar/dropdrill.ts diff --git a/packages/ast/src/ast.ts b/packages/ast/src/ast.ts index c620b98..a3cde17 100644 --- a/packages/ast/src/ast.ts +++ b/packages/ast/src/ast.ts @@ -119,13 +119,7 @@ type SubqueryExpr = { type DrillExpr = { kind: 'DrillExpr' - body: DrillBitExpr[] - typeInfo: TypeInfo -} - -type DrillBitExpr = { - kind: 'DrillBitExpr' - bit: Expr + body: Expr[] typeInfo: TypeInfo } @@ -172,7 +166,6 @@ export type Expr = | ObjectLiteralExpr | SliceExpr | DrillExpr - | DrillBitExpr export type Node = Stmt | Expr @@ -258,18 +251,12 @@ const subqueryExpr = (body: Stmt[]): SubqueryExpr => ({ body, }) -const drillExpr = (body: DrillBitExpr[]): DrillExpr => ({ +const drillExpr = (body: Expr[]): DrillExpr => ({ kind: 'DrillExpr', typeInfo: { type: Type.Value }, body, }) -const drillBitExpr = (bit: Expr): DrillBitExpr => ({ - kind: 'DrillBitExpr', - typeInfo: { type: Type.Value }, - bit, -}) - const identifierExpr = (id: Token): IdentifierExpr => ({ kind: 'IdentifierExpr', typeInfo: { type: Type.Value }, @@ -366,5 +353,4 @@ export const t = { objectLiteralExpr, subqueryExpr, drillExpr, - drillBitExpr, } diff --git a/packages/get/src/execute.ts b/packages/get/src/execute.ts index 390080f..185efe8 100644 --- a/packages/get/src/execute.ts +++ b/packages/get/src/execute.ts @@ -1,14 +1,15 @@ -import type { TypeInfo } from '@getlang/ast' +import type { Expr, TypeInfo } from '@getlang/ast' import { isToken, Type } from '@getlang/ast' import { cookies, headers, html, http, js, json } from '@getlang/lib' import type { Hooks, Inputs } from '@getlang/utils' import { invariant, NullSelection } from '@getlang/utils' import * as errors from '@getlang/utils/errors' -import type { WalkOptions } from '@getlang/walker' +import type { Path, WalkOptions } from '@getlang/walker' import { ScopeTracker, walk } from '@getlang/walker' import { callModifier } from './modifiers.js' import type { Execute } from './modules.js' import { Modules } from './modules.js' +import type { RuntimeValue } from './value.js' import { assert, toValue } from './value.js' const { @@ -19,18 +20,42 @@ const { ValueTypeError, } = errors +class ExecutionTracker extends ScopeTracker { + override exit(value: RuntimeValue, path: Path) { + if ('typeInfo' in path.node) { + assert(value) + } + super.exit(value, path) + } +} + export async function execute( rootModule: string, rootInputs: Inputs, hooks: Required, ) { - const scope = new ScopeTracker() + const scope = new ExecutionTracker() const executeModule: Execute = async (entry, inputs) => { const provided = new Set(Object.keys(inputs)) const unknown = provided.difference(entry.inputs) invariant(unknown.size === 0, new UnknownInputsError([...unknown])) + async function withItemContext(expr: Expr): Promise { + const ctx = scope.context + if (ctx?.typeInfo.type !== Type.List) { + return walk(expr, visitor) + } + const list = [] + for (const data of ctx.data) { + scope.push({ data, typeInfo: ctx.typeInfo.of }) + const value = await withItemContext(expr) + list.push(value.data) + scope.pop() + } + return { data: list, typeInfo: expr.typeInfo } + } + const visitor: WalkOptions = { scope, @@ -53,10 +78,6 @@ export async function execute( return { data, typeInfo: node.typeInfo } }, - ExtractStmt(node) { - assert(node.value) - }, - Program: { enter() { scope.extracted = { data: null, typeInfo: { type: Type.Value } } @@ -90,7 +111,7 @@ export async function execute( const deps = ctx && toValue(ctx.data, ctx.typeInfo) const ret = await hooks.slice(slice.value, deps) const data = ret === undefined ? new NullSelection('') : ret - return assert({ data, typeInfo }) + return { data, typeInfo } } catch (e) { throw new SliceError({ cause: e }) } @@ -101,7 +122,8 @@ export async function execute( }, DrillIdentifierExpr(node) { - return scope.lookup(node.id.value) + const { data } = scope.lookup(node.id.value) + return { data, typeInfo: node.typeInfo } }, SelectorExpr(node) { @@ -154,13 +176,15 @@ export async function execute( }, ObjectEntryExpr(node) { - const value = assert(node.value) - return [node.key.data, value.data] + const data = [node.key.data, node.value.data] + return { data, typeInfo: { type: Type.Value } } }, ObjectLiteralExpr(node) { const data = Object.fromEntries( - node.entries.filter(e => !(e[1] instanceof NullSelection)), + node.entries + .map(e => e.data) + .filter(e => !(e[1] instanceof NullSelection)), ) return { data, typeInfo: node.typeInfo } }, @@ -171,41 +195,17 @@ export async function execute( return ex }, - DrillExpr(node) { - return node.body.at(-1) - }, - - DrillBitExpr: { - async enter(node) { - const ctx = scope.context - const optional = node.typeInfo.type === Type.Maybe - if (optional && ctx?.data instanceof NullSelection) { - return ctx - } - - async function withItemContext() { - const ctx = scope.context - if (ctx?.typeInfo.type !== Type.List) { - const { data } = await walk(node.bit, visitor) - return data - } - const list = [] - scope.push() - for (const data of ctx.data) { - scope.context = { data, typeInfo: ctx.typeInfo.of } - const item = await withItemContext() - list.push(item) + DrillExpr: { + async enter(node, path) { + for (const expr of node.body) { + scope.context = await withItemContext(expr) + const optional = expr.typeInfo.type === Type.Maybe + if (optional && scope.context.data instanceof NullSelection) { + break } - scope.pop() - return list } - - const data = await withItemContext() - const bit = { data, typeInfo: node.typeInfo } - return { ...node, bit } - }, - exit(node) { - return node.bit + path.skip() + return scope.context }, }, @@ -230,15 +230,17 @@ export async function execute( RequestBlockExpr(node) { const value = Object.fromEntries( - node.entries.filter(e => !(e[1] instanceof NullSelection)), + node.entries + .map(e => e.data) + .filter(e => !(e[1] instanceof NullSelection)), ) const data = [node.name.value, value] return { data, typeInfo: node.typeInfo } }, RequestEntryExpr(node) { - const value = assert(node.value) - return [node.key.data, value.data] + const data = [node.key.data, node.value.data] + return { data, typeInfo: { type: Type.Value } } }, } diff --git a/packages/get/src/modifiers.ts b/packages/get/src/modifiers.ts index 6e445ac..0803a71 100644 --- a/packages/get/src/modifiers.ts +++ b/packages/get/src/modifiers.ts @@ -1,6 +1,5 @@ import { cookies, html, js, json } from '@getlang/lib' -import { NullSelection } from '@getlang/utils' import { ValueReferenceError } from '@getlang/utils/errors' import type { RuntimeValue } from './value.js' import { toValue } from './value.js' @@ -24,9 +23,7 @@ export function callModifier( switch (mod) { case 'link': - return doc - ? new URL(doc, args.base).toString() - : new NullSelection('@link') + return new URL(doc, args.base).toString() case 'html': return html.parse(doc) case 'js': diff --git a/packages/parser/src/grammar/parse.ts b/packages/parser/src/grammar/parse.ts index 09e122f..caa7eea 100644 --- a/packages/parser/src/grammar/parse.ts +++ b/packages/parser/src/grammar/parse.ts @@ -37,7 +37,7 @@ export const request: PP = ([method, url, headerBlock, { blocks, body }]) => { } export const requestBlocks: PP = ([namedBlocks, maybeBody]) => { - const blocks = namedBlocks.map(d => d[1]) + const blocks = namedBlocks.map((d: any) => d[1]) const body = maybeBody?.[1] if (body) { @@ -88,18 +88,14 @@ export const call: PP = ([callee, maybeInputs]) => { : t.moduleExpr(callee, inputs) } -export const link: PP = ([maybePrior, callee, _, link]) => { - const body = [] - const [context, , arrow] = maybePrior || [] - if (context) { - body.push(...context.body) - } +export const link: PP = ([context, callee, _, link]) => { const bit = t.moduleExpr( callee, t.objectLiteralExpr([t.objectEntryExpr(tx.template('@link'), link, true)]), ) - body.push(drillBase(bit, arrow)) - return t.drillExpr(body) + const [drill, , arrow] = context || [] + const body = drill?.body || [] + return t.drillExpr([...body, drillBase(bit, arrow)]) } export const object: PP = d => { @@ -117,7 +113,7 @@ export const objectEntry: PP = ([callkey, identifier, optional, , , value]) => { export const objectEntryShorthandSelect: PP = ([identifier, optional]) => { const value = t.templateExpr([identifier]) - const selector = t.drillExpr([t.drillBitExpr(t.selectorExpr(value, false))]) + const selector = t.selectorExpr(value, false) return objectEntry([null, identifier, optional, null, null, selector]) } @@ -128,21 +124,23 @@ export const objectEntryShorthandIdent: PP = ([identifier, optional]) => { function drillBase(bit: Expr, arrow?: string): Expr { const expand = arrow === '=>' - if (bit.kind === 'TemplateExpr') { - bit = t.selectorExpr(bit, expand) - } else if (bit.kind === 'IdentifierExpr') { - bit = t.drillIdentifierExpr(bit.id, expand) - } else if (expand) { - throw new QuerySyntaxError('Wide arrow drill requires selector on RHS') + switch (bit.kind) { + case 'TemplateExpr': + return t.selectorExpr(bit, expand) + case 'IdentifierExpr': + return t.drillIdentifierExpr(bit.id, expand) + default: + invariant(!expand, new QuerySyntaxError('Misplaced wide arrow drill')) + return bit } - return t.drillBitExpr(bit) } export const drill: PP = ([arrow, bit, bits]) => { - return t.drillExpr([ - drillBase(bit, arrow?.[0].value), - ...bits.map(([, arrow, , bit]: any) => drillBase(bit, arrow.value)), - ]) + const expr = drillBase(bit, arrow?.[0].value) + const exprs = bits.map(([, arrow, , bit]: any) => { + return drillBase(bit, arrow.value) + }) + return t.drillExpr([expr, ...exprs]) } export const identifier: PP = ([id]) => { diff --git a/packages/parser/src/passes/desugar.ts b/packages/parser/src/passes/desugar.ts index e73ec34..b7b1f9a 100644 --- a/packages/parser/src/passes/desugar.ts +++ b/packages/parser/src/passes/desugar.ts @@ -1,6 +1,7 @@ import type { Program } from '@getlang/ast' import { walk } from '@getlang/walker' import { resolveContext } from './desugar/context.js' +import { dropDrills } from './desugar/dropdrill.js' import { settleLinks } from './desugar/links.js' import { RequestParsers } from './desugar/reqparse.js' import { insertSliceDeps } from './desugar/slicedeps.js' @@ -24,9 +25,10 @@ function listCalls(ast: Program) { return calls } +const visitors = [resolveContext, settleLinks, insertSliceDeps, dropDrills] + export function desugar(ast: Program, macros: string[] = []) { const parsers = new RequestParsers() - const visitors = [resolveContext, settleLinks, insertSliceDeps] let program = visitors.reduce((ast, pass) => { parsers.reset() return pass(ast, { parsers, macros }) diff --git a/packages/parser/src/passes/desugar/context.ts b/packages/parser/src/passes/desugar/context.ts index 6eb8f74..d65807b 100644 --- a/packages/parser/src/passes/desugar/context.ts +++ b/packages/parser/src/passes/desugar/context.ts @@ -1,4 +1,3 @@ -import { t } from '@getlang/ast' import { invariant } from '@getlang/utils' import { QuerySyntaxError } from '@getlang/utils/errors' import { ScopeTracker, walk } from '@getlang/walker' @@ -24,28 +23,27 @@ export const resolveContext: DesugarPass = (ast, { parsers, macros }) => { parsers.visit(node) }, - DrillBitExpr(node, path) { + SelectorExpr(_node, path) { const ctx = scope.context - const { bit } = node - const isModifier = bit.kind === 'ModifierExpr' - const requireContext = - isModifier || - bit.kind === 'SelectorExpr' || - (bit.kind === 'ModuleExpr' && macros.includes(bit.module.value)) - - if (!requireContext || ctx?.kind !== 'RequestExpr') { - return + if (ctx?.kind === 'RequestExpr') { + path.insertBefore(parsers.lookup(ctx)) } + }, - const field = isModifier ? bit.modifier.value : undefined - const resolved = t.drillBitExpr(parsers.lookup(ctx, field)) - - if (isModifier) { + ModifierExpr(node) { + const ctx = scope.context + if (ctx?.kind === 'RequestExpr') { + const mod = node.modifier.value // replace modifier with shared parser - return resolved + return parsers.lookup(ctx, mod) } + }, - path.insertBefore(resolved) + ModuleExpr(node, path) { + const ctx = scope.context + if (macros.includes(node.module.value) && ctx?.kind === 'RequestExpr') { + path.insertBefore(parsers.lookup(ctx)) + } }, }) diff --git a/packages/parser/src/passes/desugar/dropdrill.ts b/packages/parser/src/passes/desugar/dropdrill.ts new file mode 100644 index 0000000..8868c9b --- /dev/null +++ b/packages/parser/src/passes/desugar/dropdrill.ts @@ -0,0 +1,17 @@ +import type { Program } from '@getlang/ast' +import { ScopeTracker, walk } from '@getlang/walker' + +export function dropDrills(ast: Program) { + const scope = new ScopeTracker() + + return walk(ast, { + scope, + + DrillExpr(node) { + const [first, ...rest] = node.body + if (rest.length === 0) { + return first + } + }, + }) +} diff --git a/packages/parser/src/passes/desugar/links.ts b/packages/parser/src/passes/desugar/links.ts index df51883..787b5dd 100644 --- a/packages/parser/src/passes/desugar/links.ts +++ b/packages/parser/src/passes/desugar/links.ts @@ -41,8 +41,8 @@ export const settleLinks: DesugarPass = (ast, { parsers }) => { const { value } = linkArg invariant(value.kind === 'DrillExpr', 'Module links [1]') const base = scope.getLineage(value) - invariant(base?.kind === 'DrillBitExpr', 'Module links [2]') - if (base.bit.kind === 'ModifierExpr') { + invariant(base, 'Module links [2]') + if (base.kind === 'ModifierExpr') { return } const root = scope.traceLineageRoot(value) @@ -53,7 +53,7 @@ export const settleLinks: DesugarPass = (ast, { parsers }) => { t.objectEntryExpr(tx.template('base'), parsers.lookup(root, 'link')), ]), ) - value.body.push(t.drillBitExpr(mod)) + value.body.push(mod) }, RequestExpr(node) { diff --git a/packages/parser/src/passes/desugar/reqparse.ts b/packages/parser/src/passes/desugar/reqparse.ts index 0ef6577..88ffff3 100644 --- a/packages/parser/src/passes/desugar/reqparse.ts +++ b/packages/parser/src/passes/desugar/reqparse.ts @@ -43,27 +43,27 @@ export class RequestParsers { switch (field) { case 'link': { - const expr = tx.drill(req, tx.select('url')) + const expr = t.drillExpr([req, tx.select('url')]) return t.assignmentStmt(id, expr, false) } case 'headers': { - const expr = tx.drill(req, tx.select('headers')) + const expr = t.drillExpr([req, tx.select('headers')]) return t.assignmentStmt(id, expr, false) } case 'cookies': { - const expr = tx.drill( + const expr = t.drillExpr([ req, tx.select('headers'), tx.select('set-cookie'), modbit, - ) + ]) return t.assignmentStmt(id, expr, true) } default: { - const expr = tx.drill(req, tx.select('body'), modbit) + const expr = t.drillExpr([req, tx.select('body'), modbit]) return t.assignmentStmt(id, expr, false) } } diff --git a/packages/parser/src/passes/desugar/slicedeps.ts b/packages/parser/src/passes/desugar/slicedeps.ts index 555451f..a1f54d9 100644 --- a/packages/parser/src/passes/desugar/slicedeps.ts +++ b/packages/parser/src/passes/desugar/slicedeps.ts @@ -99,10 +99,10 @@ export const insertSliceDeps: DesugarPass = ast => { if (context !== scope.context) { invariant( - path.parent?.node.kind === 'DrillBitExpr', + path.parent?.node.kind === 'DrillExpr', 'Slice dependencies require drill expression', ) - path.parent.insertBefore(t.drillBitExpr(context)) + path.insertBefore(context) } return xnode diff --git a/packages/parser/src/passes/inference/calls.ts b/packages/parser/src/passes/inference/calls.ts index 9b1516e..02d5243 100644 --- a/packages/parser/src/passes/inference/calls.ts +++ b/packages/parser/src/passes/inference/calls.ts @@ -6,8 +6,8 @@ import { LineageTracker } from '../lineage.js' export function registerCalls(ast: Program, macros: string[] = []) { const scope = new LineageTracker() - function registerCall(node?: Expr) { - const lineage = node && scope.traceLineageRoot(node) + function registerCall(node: Expr) { + const lineage = scope.traceLineageRoot(node) || node if (lineage?.kind === 'ModuleExpr') { lineage.call = true } @@ -25,18 +25,21 @@ export function registerCalls(ast: Program, macros: string[] = []) { return node }, - DrillBitExpr({ bit }) { - switch (bit.kind) { - case 'SelectorExpr': - case 'ModifierExpr': - registerCall(scope.context) - break + SelectorExpr() { + if (scope.context) { + registerCall(scope.context) + } + }, + + ModifierExpr() { + if (scope.context) { + registerCall(scope.context) + } + }, - case 'ModuleExpr': - if (macros.includes(bit.module.value)) { - bit.call = true - } - break + ModuleExpr(node) { + if (macros.includes(node.module.value)) { + return { ...node, call: true } } }, }) diff --git a/packages/parser/src/passes/inference/typeinfo.ts b/packages/parser/src/passes/inference/typeinfo.ts index 1a35fb5..81f66b6 100644 --- a/packages/parser/src/passes/inference/typeinfo.ts +++ b/packages/parser/src/passes/inference/typeinfo.ts @@ -1,7 +1,8 @@ -import type { Program, TypeInfo } from '@getlang/ast' +import type { Node, Program, TypeInfo } from '@getlang/ast' import { Type, t } from '@getlang/ast' import { invariant } from '@getlang/utils' import { QuerySyntaxError } from '@getlang/utils/errors' +import type { Path, WalkOptions } from '@getlang/walker' import { ScopeTracker, walk } from '@getlang/walker' import { toPath } from 'lodash-es' import { render, tx } from '../../utils.js' @@ -60,6 +61,32 @@ function specialize(macroType: TypeInfo, contextType?: TypeInfo) { return walk(macroType) } +class ItemScopeTracker extends ScopeTracker { + optional = [false] + + override enter(node: Node) { + super.enter(node) + if (this.context && 'typeInfo' in node) { + this.push({ + ...this.context, + typeInfo: unwrap(this.context.typeInfo), + }) + } + } + + override exit(node: Node, path: Path) { + if (this.context && 'typeInfo' in node) { + this.pop() + node.typeInfo = rewrap( + this.context.typeInfo, + structuredClone(node.typeInfo), + this.optional.at(-1), + ) + } + super.exit(node, path) + } +} + type ResolveTypeOptions = { returnTypes: { [module: string]: TypeInfo } contextType?: TypeInfo @@ -67,11 +94,9 @@ type ResolveTypeOptions = { export function resolveTypes(ast: Program, options: ResolveTypeOptions) { const { returnTypes, contextType = { type: Type.Context } } = options - const scope = new ScopeTracker() + const scope = new ItemScopeTracker() - const optional: boolean[] = [false] - - const program: Program = walk(ast, { + const visitor: WalkOptions = { scope, Program: { @@ -93,10 +118,10 @@ export function resolveTypes(ast: Program, options: ResolveTypeOptions) { AssignmentStmt: { enter(node) { - optional.push(node.optional) + scope.optional.push(node.optional) }, exit() { - optional.pop() + scope.optional.pop() }, }, @@ -132,7 +157,7 @@ export function resolveTypes(ast: Program, options: ResolveTypeOptions) { SliceExpr(node) { let typeInfo: TypeInfo = { type: Type.Value } - if (optional.at(-1)) { + if (scope.optional.at(-1)) { typeInfo = { type: Type.Maybe, option: typeInfo } } return { ...node, typeInfo } @@ -167,7 +192,7 @@ export function resolveTypes(ast: Program, options: ResolveTypeOptions) { let typeInfo = structuredClone(selectorTypeInfo()) if (node.expand) { typeInfo = { type: Type.List, of: typeInfo } - } else if (optional.at(-1)) { + } else if (scope.optional.at(-1)) { typeInfo = { type: Type.Maybe, option: typeInfo } } return { ...node, typeInfo } @@ -208,29 +233,12 @@ export function resolveTypes(ast: Program, options: ResolveTypeOptions) { return { ...node, typeInfo } }, - DrillBitExpr: { - enter() { - const ctx = scope.context - const itemCtx = ctx && { ...ctx, typeInfo: unwrap(ctx.typeInfo) } - scope.push(itemCtx) - }, - exit(node) { - scope.pop() - const ctx = scope.context - const itemTypeInfo = structuredClone(node.bit.typeInfo) - const typeInfo = ctx - ? rewrap(ctx.typeInfo, itemTypeInfo, optional.at(-1)) - : itemTypeInfo - return { ...node, typeInfo } - }, - }, - ObjectEntryExpr: { enter(node) { - optional.push(node.optional) + scope.optional.push(node.optional) }, exit() { - optional.pop() + scope.optional.pop() }, }, @@ -251,8 +259,9 @@ export function resolveTypes(ast: Program, options: ResolveTypeOptions) { } return { ...node, typeInfo } }, - }) + } + const program = walk(ast, visitor) const ex = program.body.find(s => s.kind === 'ExtractStmt') const returnType = ex?.value.typeInfo ?? { type: Type.Never } diff --git a/packages/parser/src/passes/lineage.ts b/packages/parser/src/passes/lineage.ts index 064ea14..0c81c8a 100644 --- a/packages/parser/src/passes/lineage.ts +++ b/packages/parser/src/passes/lineage.ts @@ -26,10 +26,6 @@ export class LineageTracker extends ScopeTracker { derive(this.lookup(node.id.value)) break - case 'DrillBitExpr': - derive(node.bit) - break - case 'DrillExpr': derive(node.body.at(-1)) break diff --git a/packages/parser/src/print.ts b/packages/parser/src/print.ts index fb7becd..d208c33 100644 --- a/packages/parser/src/print.ts +++ b/packages/parser/src/print.ts @@ -83,24 +83,22 @@ const printVisitor: Visitor = { return group(['extract ', node.value]) }, - DrillExpr(node) { - const [first, ...rest] = node.body + DrillExpr(node, { node: orig }) { + const body = node.body.map((expr, i) => { + const og = orig.body[i] + let expand = false + if (og.kind === 'SelectorExpr' || og.kind === 'DrillIdentifierExpr') { + expand = og.expand + } + const arrow = expand ? '=> ' : '-> ' + return indent([line, arrow, expr]) + }) + const [first, ...rest] = body const [, arrow, bit] = first.contents const lead = arrow === '=> ' ? [arrow, bit] : bit return [lead, ...rest] }, - DrillBitExpr(node, { node: orig }) { - let arrow = '-> ' - if ( - (orig.kind === 'SelectorExpr' || orig.kind === 'DrillIdentifierExpr') && - orig.expand - ) { - arrow = '=> ' - } - return indent([line, arrow, node.bit]) - }, - ObjectEntryExpr(node, { node: orig }) { if (orig.value.kind === 'IdentifierExpr') { const key = render(orig.key) @@ -140,7 +138,7 @@ const printVisitor: Visitor = { case 'SelectorExpr': return true case 'DrillExpr': - return e.value.body.at(-1).bit.kind === 'SelectorExpr' + return e.value.body.at(-1).kind === 'SelectorExpr' default: return false } diff --git a/packages/parser/src/utils.ts b/packages/parser/src/utils.ts index c49aa5c..9fd4a66 100644 --- a/packages/parser/src/utils.ts +++ b/packages/parser/src/utils.ts @@ -46,8 +46,4 @@ function select(selector: string) { return t.selectorExpr(template(selector), false) } -function drill(...bits: Expr[]) { - return t.drillExpr(bits.map(bit => t.drillBitExpr(bit))) -} - -export const tx = { token, ident, template, select, drill } +export const tx = { token, ident, template, select } diff --git a/packages/walker/src/index.ts b/packages/walker/src/index.ts index 70375cc..35d620a 100644 --- a/packages/walker/src/index.ts +++ b/packages/walker/src/index.ts @@ -2,42 +2,52 @@ import type { Node } from '@getlang/ast' import { wait, waitMap } from '@getlang/utils' import { Path } from './path.js' import type { ScopeTracker } from './scope.js' -import type { Visitor } from './visitor.js' +import type { NodeConfig, Visitor } from './visitor.js' import { normalize } from './visitor.js' export { ScopeTracker } from './scope.js' -export { Path } -export type { Visitor } +export type { Visitor, Path } export type WalkOptions = Visitor & { scope?: ScopeTracker } -export function walk(node: Node, options: WalkOptions, parent?: Path) { +export const walk = ( + node: N, + options: WalkOptions, + parent?: Path, +) => { const { scope, ...visitor } = options - const { enter, exit } = normalize(visitor[node.kind]) + const config = visitor[node.kind] as NodeConfig + const { enter, exit } = normalize(config) scope?.enter(node) - return wait(enter(node, parent), e => { + const path = parent?.add(node) || new Path(node) + return wait(enter(node, path), e => { const entered = e || node - const path = parent?.add(entered) || new Path(entered) - - const entries = waitMap(Object.entries(path.node), e => { - const [key, value] = e - let val = value - if (Array.isArray(value)) { - val = waitMap(value, el => (isNode(el) ? walk(el, options, path) : el)) - } else if (isNode(value)) { - val = walk(value, options, path) - } - return wait(val, x => [key, x]) - }) + let visited = entered + + if (!path.skipped) { + const entries = waitMap(Object.entries(entered), e => { + const [key, value] = e + let val = value + if (Array.isArray(value)) { + val = waitMap(value, el => + isNode(el) ? walk(el, options, path) : el, + ) + } else if (isNode(value)) { + val = walk(value, options, path) + } + return wait(val, x => [key, x]) + }) + + const transformed = wait(entries, Object.fromEntries) + visited = wait(transformed, t => { + return wait(exit(t, path), x => x || t) + }) + } - const transformed = wait(entries, Object.fromEntries) - const visited = wait(transformed, t => { - return wait(exit(t, path), x => x || t) - }) return wait(visited, xnode => { const applied = path.apply(xnode) scope?.exit(applied, path) diff --git a/packages/walker/src/path.ts b/packages/walker/src/path.ts index 4a77e83..989643e 100644 --- a/packages/walker/src/path.ts +++ b/packages/walker/src/path.ts @@ -10,6 +10,7 @@ type Mutation = Map export class Path { private staging: Staging = { before: [] } protected mutations: Mutation = new Map() + public skipped = false constructor( public node: Node, @@ -24,6 +25,10 @@ export class Path { this.staging.before.push(node) } + skip() { + this.skipped = true + } + private mutate(node: Node) { if (this.mutations.size === 0) { return node diff --git a/packages/walker/src/scope.ts b/packages/walker/src/scope.ts index 08cdfae..6f9790b 100644 --- a/packages/walker/src/scope.ts +++ b/packages/walker/src/scope.ts @@ -5,6 +5,7 @@ import type { Path } from './index.js' class Scope { extracted: any + constructor( public vars: { [name: string]: any }, public context: any, @@ -72,33 +73,33 @@ export class ScopeTracker { } } - exit(xnode: any, { node }: Path) { - switch (node.kind) { + exit(xnode: any, path: Path) { + switch (path.node.kind) { case 'Program': case 'SubqueryExpr': case 'DrillExpr': this.pop() break - case 'DrillBitExpr': - this.context = xnode - break - case 'RequestStmt': this.context = xnode.request break case 'InputExpr': - this.vars[node.id.value] = xnode + this.vars[path.node.id.value] = xnode break case 'AssignmentStmt': - this.vars[node.name.value] = xnode.value + this.vars[path.node.name.value] = xnode.value break case 'ExtractStmt': this.extracted = xnode.value break } + + if (path.parent?.node.kind === 'DrillExpr') { + this.context = xnode + } } } diff --git a/packages/walker/src/visitor.ts b/packages/walker/src/visitor.ts index 943fc1f..001c4dc 100644 --- a/packages/walker/src/visitor.ts +++ b/packages/walker/src/visitor.ts @@ -5,7 +5,8 @@ type Visit = | ((node: N, path: Path) => N) | ((node: N, path: Path) => void) type NodeVisitor = { enter: Visit; exit: Visit } -type NodeConfig = Visit | Partial> + +export type NodeConfig = Visit | Partial> export type Visitor = Partial<{ [N in Node as N['kind']]: NodeConfig From 3a7de411b67a16b4d6301b1f6865c836e5c0073d Mon Sep 17 00:00:00 2001 From: Matt Fysh Date: Sun, 31 Aug 2025 14:18:23 +1000 Subject: [PATCH 6/7] types passing --- packages/get/src/execute.ts | 33 +++++--- packages/get/src/modifiers.ts | 3 +- packages/get/src/modules.ts | 10 +-- packages/parser/src/passes/analyze.ts | 4 +- packages/parser/src/passes/desugar.ts | 4 +- packages/parser/src/passes/desugar/context.ts | 6 +- .../parser/src/passes/desugar/dropdrill.ts | 4 +- packages/parser/src/passes/desugar/links.ts | 4 +- .../parser/src/passes/desugar/slicedeps.ts | 9 ++- packages/parser/src/passes/inference/calls.ts | 4 +- .../parser/src/passes/inference/typeinfo.ts | 32 ++++---- packages/parser/src/passes/lineage.ts | 6 +- packages/parser/src/print.ts | 21 +++-- packages/walker/src/index.ts | 81 +++++++++++++------ packages/walker/src/path.ts | 14 ++-- packages/walker/src/scope.ts | 22 ++--- packages/walker/src/visitor.ts | 59 +++++++++----- 17 files changed, 186 insertions(+), 130 deletions(-) diff --git a/packages/get/src/execute.ts b/packages/get/src/execute.ts index 185efe8..74b3bd6 100644 --- a/packages/get/src/execute.ts +++ b/packages/get/src/execute.ts @@ -4,8 +4,8 @@ import { cookies, headers, html, http, js, json } from '@getlang/lib' import type { Hooks, Inputs } from '@getlang/utils' import { invariant, NullSelection } from '@getlang/utils' import * as errors from '@getlang/utils/errors' -import type { Path, WalkOptions } from '@getlang/walker' -import { ScopeTracker, walk } from '@getlang/walker' +import type { Path, ReduceVisitor } from '@getlang/walker' +import { reduce, ScopeTracker } from '@getlang/walker' import { callModifier } from './modifiers.js' import type { Execute } from './modules.js' import { Modules } from './modules.js' @@ -20,7 +20,7 @@ const { ValueTypeError, } = errors -class ExecutionTracker extends ScopeTracker { +class ExecutionTracker extends ScopeTracker { override exit(value: RuntimeValue, path: Path) { if ('typeInfo' in path.node) { assert(value) @@ -44,7 +44,7 @@ export async function execute( async function withItemContext(expr: Expr): Promise { const ctx = scope.context if (ctx?.typeInfo.type !== Type.List) { - return walk(expr, visitor) + return reduce(expr, options) } const list = [] for (const data of ctx.data) { @@ -56,9 +56,9 @@ export async function execute( return { data: list, typeInfo: expr.typeInfo } } - const visitor: WalkOptions = { - scope, + let ex: RuntimeValue | undefined + const visitor: ReduceVisitor = { /** * Statement nodes */ @@ -83,7 +83,7 @@ export async function execute( scope.extracted = { data: null, typeInfo: { type: Type.Value } } }, exit() { - return scope.extracted + ex = scope.extracted }, }, @@ -92,7 +92,7 @@ export async function execute( */ TemplateExpr(node, path) { const firstNull = node.elements.find( - el => el.data instanceof NullSelection, + el => 'data' in el && el.data instanceof NullSelection, ) if (firstNull) { const isRoot = path.parent?.node.kind !== 'TemplateExpr' @@ -127,6 +127,8 @@ export async function execute( }, SelectorExpr(node) { + invariant(scope.context, 'Unresolved context') + const selector = node.selector.data invariant( typeof selector === 'string', @@ -157,8 +159,10 @@ export async function execute( }, ModifierExpr(node) { + invariant(scope.context, 'Unresolved context') const mod = node.modifier.value const args = node.args.data + return { data: callModifier(mod, args, scope.context), typeInfo: node.typeInfo, @@ -167,7 +171,11 @@ export async function execute( ModuleExpr(node) { if (node.call) { - return modules.call(node, node.args.data, scope.context?.typeInfo) + return modules.call( + node.module.value, + node.args, + scope.context?.typeInfo, + ) } return { data: toValue(node.args.data, node.args.typeInfo), @@ -204,8 +212,7 @@ export async function execute( break } } - path.skip() - return scope.context + path.replace(scope.context) }, }, @@ -244,7 +251,9 @@ export async function execute( }, } - return walk(entry.program, visitor) + const options = { scope, ...visitor } + await reduce(entry.program, options) + return ex } const modules = new Modules(hooks, executeModule) diff --git a/packages/get/src/modifiers.ts b/packages/get/src/modifiers.ts index 0803a71..e446d48 100644 --- a/packages/get/src/modifiers.ts +++ b/packages/get/src/modifiers.ts @@ -1,5 +1,5 @@ import { cookies, html, js, json } from '@getlang/lib' - +import { invariant } from '@getlang/utils' import { ValueReferenceError } from '@getlang/utils/errors' import type { RuntimeValue } from './value.js' import { toValue } from './value.js' @@ -23,6 +23,7 @@ export function callModifier( switch (mod) { case 'link': + invariant(typeof args.base === 'string', '@link requires base url') return new URL(doc, args.base).toString() case 'html': return html.parse(doc) diff --git a/packages/get/src/modules.ts b/packages/get/src/modules.ts index 0e3b323..3465258 100644 --- a/packages/get/src/modules.ts +++ b/packages/get/src/modules.ts @@ -1,4 +1,4 @@ -import type { ModuleExpr, Program, TypeInfo } from '@getlang/ast' +import type { Program, TypeInfo } from '@getlang/ast' import { Type } from '@getlang/ast' import { analyze, desugar, inference, parse } from '@getlang/parser' import type { Hooks, Inputs } from '@getlang/utils' @@ -8,6 +8,7 @@ import { ValueTypeError, } from '@getlang/utils/errors' import { partition } from 'lodash-es' +import type { RuntimeValue } from './value.js' import { toValue } from './value.js' type Info = { @@ -113,8 +114,7 @@ export class Modules { return this.entries[key] } - async call(node: ModuleExpr, args: any, contextType?: TypeInfo) { - const module = node.module.value + async call(module: string, args: RuntimeValue, contextType?: TypeInfo) { let entry: Entry try { entry = await this.import(module, [], contextType) @@ -122,7 +122,7 @@ export class Modules { const err = `Failed to import module: ${module}` throw new ImportError(err, { cause: e }) } - const [inputArgs, attrArgs] = partition(Object.entries(args), e => + const [inputArgs, attrArgs] = partition(Object.entries(args.data), e => entry.inputs.has(e[0]), ) const inputs = Object.fromEntries(inputArgs) @@ -154,7 +154,7 @@ export class Modules { } const attrs = Object.fromEntries(attrArgs) - const raster = toValue(attrs, node.args.typeInfo) + const raster = toValue(attrs, args.typeInfo) return { ...raster, ...extracted } } } diff --git a/packages/parser/src/passes/analyze.ts b/packages/parser/src/passes/analyze.ts index 1ea496c..fe254fa 100644 --- a/packages/parser/src/passes/analyze.ts +++ b/packages/parser/src/passes/analyze.ts @@ -1,5 +1,5 @@ import type { Program } from '@getlang/ast' -import { ScopeTracker, walk } from '@getlang/walker' +import { ScopeTracker, transform } from '@getlang/walker' export function analyze(ast: Program) { const scope = new ScopeTracker() @@ -7,7 +7,7 @@ export function analyze(ast: Program) { const imports = new Set() let isMacro = false - walk(ast, { + transform(ast, { scope, InputExpr(node) { inputs.add(node.id.value) diff --git a/packages/parser/src/passes/desugar.ts b/packages/parser/src/passes/desugar.ts index b7b1f9a..d41d805 100644 --- a/packages/parser/src/passes/desugar.ts +++ b/packages/parser/src/passes/desugar.ts @@ -1,5 +1,5 @@ import type { Program } from '@getlang/ast' -import { walk } from '@getlang/walker' +import { transform } from '@getlang/walker' import { resolveContext } from './desugar/context.js' import { dropDrills } from './desugar/dropdrill.js' import { settleLinks } from './desugar/links.js' @@ -17,7 +17,7 @@ export type DesugarPass = ( function listCalls(ast: Program) { const calls = new Set() - walk(ast, { + transform(ast, { ModuleExpr(node) { node.call && calls.add(node.module.value) }, diff --git a/packages/parser/src/passes/desugar/context.ts b/packages/parser/src/passes/desugar/context.ts index d65807b..529282a 100644 --- a/packages/parser/src/passes/desugar/context.ts +++ b/packages/parser/src/passes/desugar/context.ts @@ -1,12 +1,12 @@ import { invariant } from '@getlang/utils' import { QuerySyntaxError } from '@getlang/utils/errors' -import { ScopeTracker, walk } from '@getlang/walker' +import { ScopeTracker, transform } from '@getlang/walker' import type { DesugarPass } from '../desugar.js' export const resolveContext: DesugarPass = (ast, { parsers, macros }) => { const scope = new ScopeTracker() - const program = walk(ast, { + const program = transform(ast, { scope, Program(node) { @@ -20,6 +20,8 @@ export const resolveContext: DesugarPass = (ast, { parsers, macros }) => { }, RequestExpr(node) { + invariant(node.headers.kind === 'RequestBlockExpr', '') + node.headers parsers.visit(node) }, diff --git a/packages/parser/src/passes/desugar/dropdrill.ts b/packages/parser/src/passes/desugar/dropdrill.ts index 8868c9b..6ef91c2 100644 --- a/packages/parser/src/passes/desugar/dropdrill.ts +++ b/packages/parser/src/passes/desugar/dropdrill.ts @@ -1,10 +1,10 @@ import type { Program } from '@getlang/ast' -import { ScopeTracker, walk } from '@getlang/walker' +import { ScopeTracker, transform } from '@getlang/walker' export function dropDrills(ast: Program) { const scope = new ScopeTracker() - return walk(ast, { + return transform(ast, { scope, DrillExpr(node) { diff --git a/packages/parser/src/passes/desugar/links.ts b/packages/parser/src/passes/desugar/links.ts index 787b5dd..1bd5da9 100644 --- a/packages/parser/src/passes/desugar/links.ts +++ b/packages/parser/src/passes/desugar/links.ts @@ -1,7 +1,7 @@ import { t } from '@getlang/ast' import { invariant } from '@getlang/utils' import { QuerySyntaxError } from '@getlang/utils/errors' -import { walk } from '@getlang/walker' +import { transform } from '@getlang/walker' import { render, tx } from '../../utils.js' import type { DesugarPass } from '../desugar.js' import { LineageTracker } from '../lineage.js' @@ -9,7 +9,7 @@ import { LineageTracker } from '../lineage.js' export const settleLinks: DesugarPass = (ast, { parsers }) => { const scope = new LineageTracker() - const ret = walk(ast, { + const ret = transform(ast, { scope, ModifierExpr(node) { diff --git a/packages/parser/src/passes/desugar/slicedeps.ts b/packages/parser/src/passes/desugar/slicedeps.ts index a1f54d9..f3beb10 100644 --- a/packages/parser/src/passes/desugar/slicedeps.ts +++ b/packages/parser/src/passes/desugar/slicedeps.ts @@ -1,7 +1,8 @@ +import type { Expr } from '@getlang/ast' import { t } from '@getlang/ast' import { invariant } from '@getlang/utils' import { SliceSyntaxError } from '@getlang/utils/errors' -import { ScopeTracker, walk } from '@getlang/walker' +import { ScopeTracker, transform } from '@getlang/walker' import { parse as acorn } from 'acorn' import { traverse } from 'estree-toolkit' import globals from 'globals' @@ -68,8 +69,8 @@ const analyzeSlice = (slice: string) => { } export const insertSliceDeps: DesugarPass = ast => { - const scope = new ScopeTracker() - return walk(ast, { + const scope = new ScopeTracker() + return transform(ast, { scope, SliceExpr(node, path) { @@ -97,7 +98,7 @@ export const insertSliceDeps: DesugarPass = ast => { } } - if (context !== scope.context) { + if (context && context !== scope.context) { invariant( path.parent?.node.kind === 'DrillExpr', 'Slice dependencies require drill expression', diff --git a/packages/parser/src/passes/inference/calls.ts b/packages/parser/src/passes/inference/calls.ts index 02d5243..abebaed 100644 --- a/packages/parser/src/passes/inference/calls.ts +++ b/packages/parser/src/passes/inference/calls.ts @@ -1,6 +1,6 @@ import type { Expr, Program } from '@getlang/ast' import { isToken } from '@getlang/ast' -import { walk } from '@getlang/walker' +import { transform } from '@getlang/walker' import { LineageTracker } from '../lineage.js' export function registerCalls(ast: Program, macros: string[] = []) { @@ -13,7 +13,7 @@ export function registerCalls(ast: Program, macros: string[] = []) { } } - return walk(ast, { + return transform(ast, { scope, TemplateExpr(node) { diff --git a/packages/parser/src/passes/inference/typeinfo.ts b/packages/parser/src/passes/inference/typeinfo.ts index 81f66b6..2cbc4da 100644 --- a/packages/parser/src/passes/inference/typeinfo.ts +++ b/packages/parser/src/passes/inference/typeinfo.ts @@ -1,9 +1,9 @@ -import type { Node, Program, TypeInfo } from '@getlang/ast' +import type { Expr, Node, Program, TypeInfo } from '@getlang/ast' import { Type, t } from '@getlang/ast' import { invariant } from '@getlang/utils' import { QuerySyntaxError } from '@getlang/utils/errors' -import type { Path, WalkOptions } from '@getlang/walker' -import { ScopeTracker, walk } from '@getlang/walker' +import type { Path, TransformVisitor } from '@getlang/walker' +import { ScopeTracker, transform } from '@getlang/walker' import { toPath } from 'lodash-es' import { render, tx } from '../../utils.js' @@ -61,7 +61,7 @@ function specialize(macroType: TypeInfo, contextType?: TypeInfo) { return walk(macroType) } -class ItemScopeTracker extends ScopeTracker { +class ItemScopeTracker extends ScopeTracker { optional = [false] override enter(node: Node) { @@ -95,10 +95,9 @@ type ResolveTypeOptions = { export function resolveTypes(ast: Program, options: ResolveTypeOptions) { const { returnTypes, contextType = { type: Type.Context } } = options const scope = new ItemScopeTracker() + let ex: Expr | undefined - const visitor: WalkOptions = { - scope, - + const visitor: TransformVisitor = { Program: { enter() { scope.context = { @@ -106,6 +105,9 @@ export function resolveTypes(ast: Program, options: ResolveTypeOptions) { typeInfo: contextType, } }, + exit() { + ex = scope.extracted + }, }, InputExpr(node) { @@ -164,12 +166,12 @@ export function resolveTypes(ast: Program, options: ResolveTypeOptions) { }, SelectorExpr(node) { - function selectorTypeInfo() { + function selectorTypeInfo(): TypeInfo { invariant( node.selector.kind === 'TemplateExpr', new QuerySyntaxError('Selector requires template'), ) - const scopeT = scope.context.typeInfo + const scopeT = scope.context!.typeInfo switch (scopeT.type) { case Type.Headers: case Type.Cookies: @@ -189,7 +191,7 @@ export function resolveTypes(ast: Program, options: ResolveTypeOptions) { } } - let typeInfo = structuredClone(selectorTypeInfo()) + let typeInfo: TypeInfo = structuredClone(selectorTypeInfo()) if (node.expand) { typeInfo = { type: Type.List, of: typeInfo } } else if (scope.optional.at(-1)) { @@ -223,8 +225,7 @@ export function resolveTypes(ast: Program, options: ResolveTypeOptions) { }, SubqueryExpr(node) { - const ex = node.body.findLast(s => s.kind === 'ExtractStmt') - const typeInfo = ex?.value.typeInfo || { type: Type.Never } + const typeInfo = scope.extracted?.typeInfo || { type: Type.Never } return { ...node, typeInfo: structuredClone(typeInfo) } }, @@ -261,9 +262,8 @@ export function resolveTypes(ast: Program, options: ResolveTypeOptions) { }, } - const program = walk(ast, visitor) - const ex = program.body.find(s => s.kind === 'ExtractStmt') - const returnType = ex?.value.typeInfo ?? { type: Type.Never } + const program = transform(ast, { scope, ...visitor }) + const returnType = ex?.typeInfo ?? { type: Type.Never } - return { program, returnType } + return { program, returnType: returnType } } diff --git a/packages/parser/src/passes/lineage.ts b/packages/parser/src/passes/lineage.ts index 0c81c8a..72497c2 100644 --- a/packages/parser/src/passes/lineage.ts +++ b/packages/parser/src/passes/lineage.ts @@ -2,7 +2,7 @@ import type { Expr, Node } from '@getlang/ast' import type { Path } from '@getlang/walker' import { ScopeTracker } from '@getlang/walker' -export class LineageTracker extends ScopeTracker { +export class LineageTracker extends ScopeTracker { private lineage = new Map() getLineage(expr: Expr) { @@ -27,12 +27,12 @@ export class LineageTracker extends ScopeTracker { break case 'DrillExpr': - derive(node.body.at(-1)) + derive(node.body.at(-1)!) break case 'ModifierExpr': case 'SelectorExpr': - derive(this.context) + derive(this.context!) break case 'SubqueryExpr': diff --git a/packages/parser/src/print.ts b/packages/parser/src/print.ts index d208c33..680dd08 100644 --- a/packages/parser/src/print.ts +++ b/packages/parser/src/print.ts @@ -1,7 +1,7 @@ import type { Node } from '@getlang/ast' import { isToken } from '@getlang/ast' -import type { Visitor } from '@getlang/walker' -import { walk } from '@getlang/walker' +import type { ReduceVisitor } from '@getlang/walker' +import { reduce } from '@getlang/walker' import { builders, printer } from 'prettier/doc' import { render } from './utils.js' @@ -12,7 +12,7 @@ type Doc = builders.Doc const { group, indent, join, line, hardline, softline, ifBreak } = builders -const printVisitor: Visitor = { +const printVisitor: ReduceVisitor = { Program(node) { return join(hardline, node.body) }, @@ -84,19 +84,18 @@ const printVisitor: Visitor = { }, DrillExpr(node, { node: orig }) { - const body = node.body.map((expr, i) => { - const og = orig.body[i] + return node.body.map((expr, i) => { + const og = orig.body[i]! let expand = false if (og.kind === 'SelectorExpr' || og.kind === 'DrillIdentifierExpr') { expand = og.expand } + if (i === 0) { + return expand ? ['=> ', expr] : expr + } const arrow = expand ? '=> ' : '-> ' return indent([line, arrow, expr]) }) - const [first, ...rest] = body - const [, arrow, bit] = first.contents - const lead = arrow === '=> ' ? [arrow, bit] : bit - return [lead, ...rest] }, ObjectEntryExpr(node, { node: orig }) { @@ -138,7 +137,7 @@ const printVisitor: Visitor = { case 'SelectorExpr': return true case 'DrillExpr': - return e.value.body.at(-1).kind === 'SelectorExpr' + return e.value.body.at(-1)!.kind === 'SelectorExpr' default: return false } @@ -223,7 +222,7 @@ export function print(ast: Node) { if (!(ast.kind === 'Program')) { throw new Error(`Non-program AST node provided: ${ast}`) } - const doc = walk(ast, printVisitor) + const doc = reduce(ast, printVisitor) // propagateBreaks(doc) return printer.printDocToString(doc, { printWidth: 70, diff --git a/packages/walker/src/index.ts b/packages/walker/src/index.ts index 35d620a..818de2c 100644 --- a/packages/walker/src/index.ts +++ b/packages/walker/src/index.ts @@ -2,53 +2,77 @@ import type { Node } from '@getlang/ast' import { wait, waitMap } from '@getlang/utils' import { Path } from './path.js' import type { ScopeTracker } from './scope.js' -import type { NodeConfig, Visitor } from './visitor.js' -import { normalize } from './visitor.js' +import type { + NodeConfig, + NodeVisitor, + ReduceVisitor, + TransformVisitor, +} from './visitor.js' export { ScopeTracker } from './scope.js' -export type { Visitor, Path } +export type { TransformVisitor, ReduceVisitor, Path } -export type WalkOptions = Visitor & { +export type WalkOptions = Visitor & { scope?: ScopeTracker } -export const walk = ( +function normalize( + visitor?: NodeConfig, +): NodeVisitor { + if (!visitor) { + return { enter: () => {}, exit: () => {} } + } else if (typeof visitor === 'function') { + return { enter: () => {}, exit: visitor } + } else { + return { + enter: visitor.enter || (() => {}), + exit: visitor.exit || (() => {}), + } + } +} + +const isNode = (test: unknown): test is Node => + typeof test === 'object' && + test !== null && + 'kind' in test && + typeof test.kind === 'string' + +function walk( node: N, - options: WalkOptions, + visitor: TransformVisitor, + scope?: ScopeTracker, parent?: Path, -) => { - const { scope, ...visitor } = options - const config = visitor[node.kind] as NodeConfig - const { enter, exit } = normalize(config) +) { + const config = visitor[node.kind] as NodeConfig + const { enter, exit } = normalize(config) scope?.enter(node) const path = parent?.add(node) || new Path(node) - return wait(enter(node, path), e => { - const entered = e || node - let visited = entered - - if (!path.skipped) { - const entries = waitMap(Object.entries(entered), e => { + return wait(enter(node, path), () => { + let xnode: any = path.replacement + if (!xnode) { + const entries = waitMap(Object.entries(node), e => { const [key, value] = e let val = value if (Array.isArray(value)) { val = waitMap(value, el => - isNode(el) ? walk(el, options, path) : el, + isNode(el) ? walk(el, visitor, scope, path) : el, ) } else if (isNode(value)) { - val = walk(value, options, path) + val = walk(value, visitor, scope, path) } return wait(val, x => [key, x]) }) - const transformed = wait(entries, Object.fromEntries) - visited = wait(transformed, t => { + const tnode = wait(entries, Object.fromEntries) + + xnode = wait(tnode, t => { return wait(exit(t, path), x => x || t) }) } - return wait(visited, xnode => { + return wait(xnode, xnode => { const applied = path.apply(xnode) scope?.exit(applied, path) return applied @@ -56,8 +80,13 @@ export const walk = ( }) } -const isNode = (test: unknown): test is Node => - typeof test === 'object' && - test !== null && - 'kind' in test && - typeof test.kind === 'string' +export function transform(node: Node, options: WalkOptions) { + return walk(node, options, options.scope) +} + +export function reduce( + node: Node, + options: WalkOptions>, +) { + return walk(node, options as TransformVisitor, options.scope) +} diff --git a/packages/walker/src/path.ts b/packages/walker/src/path.ts index 989643e..20384e9 100644 --- a/packages/walker/src/path.ts +++ b/packages/walker/src/path.ts @@ -7,26 +7,26 @@ type Staging = { type Mutation = Map -export class Path { +export class Path { private staging: Staging = { before: [] } protected mutations: Mutation = new Map() - public skipped = false + public replacement: unknown constructor( - public node: Node, + public node: N, public parent?: Path, ) {} - add(node: Node) { - return new Path(node, this) + add(node: N) { + return new Path(node, this) } insertBefore(node: Node) { this.staging.before.push(node) } - skip() { - this.skipped = true + replace(value: any) { + this.replacement = value } private mutate(node: Node) { diff --git a/packages/walker/src/scope.ts b/packages/walker/src/scope.ts index 6f9790b..960a1b1 100644 --- a/packages/walker/src/scope.ts +++ b/packages/walker/src/scope.ts @@ -3,12 +3,12 @@ import { invariant } from '@getlang/utils' import { ValueReferenceError } from '@getlang/utils/errors' import type { Path } from './index.js' -class Scope { - extracted: any +class Scope { + extracted?: T constructor( - public vars: { [name: string]: any }, - public context: any, + public vars: { [name: string]: T }, + public context: T | undefined, ) {} lookup(id: string) { @@ -18,10 +18,10 @@ class Scope { } } -export class ScopeTracker { - scopeStack: Scope[] = [] +export class ScopeTracker { + scopeStack: Scope[] = [] - push(context: any = this.head?.context) { + push(context: T | undefined = this.head?.context) { const vars = Object.create(this.head?.vars ?? null) this.scopeStack.push(new Scope(vars, context)) } @@ -39,11 +39,11 @@ export class ScopeTracker { return this.head } - set context(value: any) { + set context(value: T) { this.ensure.context = value } - get context() { + get context(): T | undefined { return this.ensure.context } @@ -51,11 +51,11 @@ export class ScopeTracker { return this.ensure.vars } - get extracted() { + get extracted(): T | undefined { return this.ensure.extracted } - set extracted(data: any) { + set extracted(data: T) { this.ensure.extracted = data } diff --git a/packages/walker/src/visitor.ts b/packages/walker/src/visitor.ts index 001c4dc..76718a8 100644 --- a/packages/walker/src/visitor.ts +++ b/packages/walker/src/visitor.ts @@ -1,28 +1,43 @@ -import type { Node } from '@getlang/ast' +import type { Expr, Node, Stmt } from '@getlang/ast' import type { Path } from './index.js' -type Visit = - | ((node: N, path: Path) => N) - | ((node: N, path: Path) => void) -type NodeVisitor = { enter: Visit; exit: Visit } +// -------- Utilities -export type NodeConfig = Visit | Partial> +type Transform = V extends readonly (infer T)[] + ? Transform[] + : V extends (...args: any) => any + ? V + : V extends object + ? V extends Stmt + ? S + : V extends Expr + ? E + : { [K in keyof V]: Transform } + : V -export type Visitor = Partial<{ - [N in Node as N['kind']]: NodeConfig -}> +type TNode = { + [K in keyof N]: Transform +} + +type NodeResult = N extends Stmt ? S : N extends Expr ? E : never -export function normalize( - visitor?: NodeConfig, -): NodeVisitor { - if (!visitor) { - return { enter: () => {}, exit: () => {} } - } else if (typeof visitor === 'function') { - return { enter: () => {}, exit: visitor } - } else { - return { - enter: visitor.enter || (() => {}), - exit: visitor.exit || (() => {}), - } - } +type Visit = + | ((node: X, path: Path) => R) + | ((node: X, path: Path) => void) + +export type NodeVisitor = { + enter: Visit + exit: Visit } + +export type NodeConfig = + | Visit + | Partial> + +export type TransformVisitor = Partial<{ + [N in Node as N['kind']]: NodeConfig +}> + +export type ReduceVisitor = Partial<{ + [N in Node as N['kind']]: NodeConfig, NodeResult> +}> From 356a09a81cddcba4c788a103451c8c9517ec596d Mon Sep 17 00:00:00 2001 From: Matt Fysh Date: Sun, 31 Aug 2025 14:19:46 +1000 Subject: [PATCH 7/7] add changeset --- .changeset/deep-trees-knock.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/deep-trees-knock.md diff --git a/.changeset/deep-trees-knock.md b/.changeset/deep-trees-knock.md new file mode 100644 index 0000000..3470596 --- /dev/null +++ b/.changeset/deep-trees-knock.md @@ -0,0 +1,8 @@ +--- +"@getlang/parser": patch +"@getlang/walker": patch +"@getlang/ast": patch +"@getlang/get": patch +--- + +drill-based walker