From 38fc11c0fea90d68650d137b0fda355375cb069a Mon Sep 17 00:00:00 2001 From: Matt Fysh Date: Wed, 20 Aug 2025 21:42:32 +1000 Subject: [PATCH 1/2] slice dependencies analysis --- bun.lockb | Bin 90248 -> 89696 bytes package.json | 4 +- packages/get/src/execute.ts | 38 +++--- packages/get/src/hooks.spec.ts | 4 +- packages/get/src/modules.ts | 49 ++++---- packages/lib/src/index.ts | 6 +- packages/lib/src/values/html/patch-dom.ts | 6 - packages/lib/src/values/js.ts | 5 +- packages/parser/package.json | 2 +- packages/parser/src/ast/ast.ts | 24 ++-- packages/parser/src/ast/print.ts | 36 ++++-- packages/parser/src/ast/scope.ts | 14 +-- packages/parser/src/grammar/lex/slice.ts | 12 +- packages/parser/src/grammar/lexer.ts | 5 + packages/parser/src/grammar/parse.ts | 37 +++--- packages/parser/src/passes/analyze.ts | 12 +- .../src/passes/desugar/acorn-globals.d.ts | 6 - packages/parser/src/passes/desugar/links.ts | 15 ++- .../parser/src/passes/desugar/reqparse.ts | 4 +- .../parser/src/passes/desugar/slicedeps.ts | 108 +++++++++++------- packages/parser/src/passes/inference/calls.ts | 12 +- .../parser/src/passes/inference/typeinfo.ts | 17 ++- packages/parser/src/passes/trace.ts | 8 +- packages/utils/src/hooks.ts | 1 - test/slice.spec.ts | 28 +++-- test/values.spec.ts | 2 +- 26 files changed, 250 insertions(+), 205 deletions(-) delete mode 100644 packages/parser/src/passes/desugar/acorn-globals.d.ts diff --git a/bun.lockb b/bun.lockb index fe035903f427213abf624de80a7320254bae69fe..820b891ab58aa63557265e184cd55dd65b295866 100755 GIT binary patch delta 11731 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 diff --git a/package.json b/package.json index a02785a..c32c2c1 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,9 @@ "test" ], "devDependencies": { - "@biomejs/biome": "^2.1.4", + "@biomejs/biome": "^2.2.0", "@changesets/changelog-github": "^0.5.1", - "@changesets/cli": "^2.29.5", + "@changesets/cli": "^2.29.6", "@types/bun": "^1.2.20", "knip": "^5.62.0", "sherif": "^1.6.1", diff --git a/packages/get/src/execute.ts b/packages/get/src/execute.ts index 8fcf8ae..c8242e5 100644 --- a/packages/get/src/execute.ts +++ b/packages/get/src/execute.ts @@ -21,6 +21,7 @@ const { SliceError, UnknownInputsError, ValueReferenceError, + ValueTypeError, } = errors export async function execute( @@ -64,25 +65,13 @@ export async function execute( return els.join('') }, - IdentifierExpr(node) { - const value = scope.vars[node.value.value] - invariant( - value !== undefined, - new ValueReferenceError(node.value.value), - ) - return value - }, - SliceExpr: { async enter(node, visit) { return withContext(scope, node, visit, async context => { const { slice } = node try { - const value = await hooks.slice( - slice.value, - context ? toValue(context.value, context.typeInfo) : {}, - context?.value ?? {}, - ) + 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 @@ -94,13 +83,28 @@ export async function execute( }, }, + 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 + }) + }, + }, + SelectorExpr: { async enter(node, visit) { return withContext(scope, node, visit, async context => { const selector = await visit(node.selector) - if (typeof selector !== 'string') { - return selector - } + invariant( + typeof selector === 'string', + new ValueTypeError('Expected selector string'), + ) const args = [context!.value, selector, node.expand] as const function select(typeInfo: TypeInfo) { diff --git a/packages/get/src/hooks.spec.ts b/packages/get/src/hooks.spec.ts index d891193..ac46c44 100644 --- a/packages/get/src/hooks.spec.ts +++ b/packages/get/src/hooks.spec.ts @@ -40,10 +40,8 @@ describe('hook', () => { test('on slice', async () => { const sliceHook = mock(() => 3) - const result = await execute('extract `1 + 2`', {}, { slice: sliceHook }) - - expect(sliceHook).toHaveBeenCalledWith('return 1 + 2', {}, {}) + expect(sliceHook).toHaveBeenCalledWith('return 1 + 2;;', undefined) expect(result).toEqual(3) }) diff --git a/packages/get/src/modules.ts b/packages/get/src/modules.ts index 5a07be8..0bb7e1a 100644 --- a/packages/get/src/modules.ts +++ b/packages/get/src/modules.ts @@ -26,27 +26,27 @@ type Entry = { export type Execute = (entry: Entry, inputs: Inputs) => Promise -function buildImportKey(module: string, typeInfo?: TypeInfo) { - function repr(ti: TypeInfo): string { - switch (ti.type) { - case Type.Maybe: - return `maybe<${repr(ti.option)}>` - case Type.List: - return `${repr(ti.of)}[]` - case Type.Struct: { - const fields = Object.entries(ti.schema) - .map(e => `${e[0]}: ${repr(e[1])};`) - .join(' ') - return `{ ${fields} }` - } - case Type.Context: - case Type.Never: - throw new ValueTypeError('Unsupported key type') - default: - return ti.type +function repr(ti: TypeInfo): string { + switch (ti.type) { + case Type.Maybe: + return `maybe<${repr(ti.option)}>` + case Type.List: + return `${repr(ti.of)}[]` + case Type.Struct: { + const fields = Object.entries(ti.schema) + .map(e => `${e[0]}: ${repr(e[1])};`) + .join(' ') + return `{ ${fields} }` } + case Type.Context: + case Type.Never: + throw new ValueTypeError('Unsupported key type') + default: + return ti.type } +} +function buildImportKey(module: string, typeInfo?: TypeInfo) { let key = module if (typeInfo) { key += `<${repr(typeInfo)}>` @@ -133,15 +133,24 @@ export class Modules { } await this.hooks.extract(module, inputs, extracted) - if (typeof extracted !== 'object' || entry.returnType.type !== Type.Value) { + function dropWarning(reason: string) { if (attrArgs.length) { const dropped = attrArgs.map(e => e[0]).join(', ') const err = [ - `Module '${module}' returned a primitive`, + `Module '${module}' ${reason}`, `dropping view attributes: ${dropped}`, ].join(', ') console.warn(err) } + } + + if (entry.returnType.type !== Type.Value) { + dropWarning(`returned ${repr(entry.returnType)}`) + return extracted + } + + if (typeof extracted !== 'object') { + dropWarning('returned a primitive') return extracted } diff --git a/packages/lib/src/index.ts b/packages/lib/src/index.ts index ff692c8..4ef2db5 100644 --- a/packages/lib/src/index.ts +++ b/packages/lib/src/index.ts @@ -5,8 +5,10 @@ export * as html from './values/html.js' export * as js from './values/js.js' export * as json from './values/json.js' -function runSlice(slice: string, context: unknown = {}, raw: unknown = {}) { - return new Function('$', '$$', slice)(context, raw) +const AsyncFunction: any = (async () => {}).constructor + +function runSlice(slice: string, context: unknown = {}) { + return new AsyncFunction('$', slice)(context) } export const slice = { runSlice } diff --git a/packages/lib/src/values/html/patch-dom.ts b/packages/lib/src/values/html/patch-dom.ts index 94821cc..2d762dd 100644 --- a/packages/lib/src/values/html/patch-dom.ts +++ b/packages/lib/src/values/html/patch-dom.ts @@ -19,12 +19,6 @@ function main() { return ds(this) } - Object.defineProperty(Element.prototype, 'outerHTML', { - get() { - return ds(this) - }, - }) - Object.defineProperty(Node.prototype, 'nodeName', { get: function () { return this.name diff --git a/packages/lib/src/values/js.ts b/packages/lib/src/values/js.ts index a935752..bb76c9b 100644 --- a/packages/lib/src/values/js.ts +++ b/packages/lib/src/values/js.ts @@ -10,7 +10,10 @@ import esquery from 'esquery' export const parse = (js: string): AnyNode => { try { - return acorn(js, { ecmaVersion: 'latest' }) + return acorn(js, { + ecmaVersion: 'latest', + allowAwaitOutsideFunction: true, + }) } catch (e) { throw new SliceSyntaxError('Could not parse slice', { cause: e }) } diff --git a/packages/parser/package.json b/packages/parser/package.json index 908d3a0..414fd00 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -43,7 +43,7 @@ "@types/moo": "^0.5.10", "@types/nearley": "^2.11.5", "acorn": "^8.15.0", - "acorn-globals": "^7.0.1", + "estree-toolkit": "^1.7.13", "globals": "^16.3.0", "lodash-es": "^4.17.21", "moo": "^0.5.2", diff --git a/packages/parser/src/ast/ast.ts b/packages/parser/src/ast/ast.ts index b925866..9c8486b 100644 --- a/packages/parser/src/ast/ast.ts +++ b/packages/parser/src/ast/ast.ts @@ -90,8 +90,10 @@ export type TemplateExpr = { type IdentifierExpr = { kind: NodeKind.IdentifierExpr - value: Token + id: Token + expand: boolean isUrlComponent: boolean + context?: Expr typeInfo: TypeInfo } @@ -228,11 +230,17 @@ const subqueryExpr = (body: Stmt[], context?: Expr): SubqueryExpr => ({ context, }) -const identifierExpr = (value: Token): IdentifierExpr => ({ +const identifierExpr = ( + id: Token, + expand = false, + context?: Expr, +): IdentifierExpr => ({ kind: NodeKind.IdentifierExpr, - value, - isUrlComponent: value.text.startsWith(':'), + id, + expand, + isUrlComponent: id.text.startsWith(':'), typeInfo: { type: Type.Value }, + context, }) const selectorExpr = ( @@ -303,20 +311,14 @@ const templateExpr = (elements: (Expr | Token)[]): TemplateExpr => ({ export const t = { program, - - // STATEMENTS assignmentStmt, declInputsStmt, inputDeclStmt, extractStmt, requestStmt, - - // EXPRESSIONS requestExpr, - identifierExpr, templateExpr, - - // CONTEXTUAL EXPRESSIONS + identifierExpr, selectorExpr, modifierExpr, moduleExpr, diff --git a/packages/parser/src/ast/print.ts b/packages/parser/src/ast/print.ts index 8e8f982..7dd41f5 100644 --- a/packages/parser/src/ast/print.ts +++ b/packages/parser/src/ast/print.ts @@ -1,4 +1,5 @@ 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' @@ -82,7 +83,6 @@ const printVisitor: InterpretVisitor = { }, ObjectLiteralExpr(node, _path, orig) { - const shorthand: Doc[] = [] const shouldBreak = orig.entries.some( e => e.value.kind === NodeKind.SelectorExpr, ) @@ -91,6 +91,15 @@ const printVisitor: InterpretVisitor = { if (!origEntry) { throw new Error('Unmatched object literal entry') } + + 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 + } + } + const keyGroup: Doc[] = [entry.key] if (entry.optional) { keyGroup.push('?') @@ -110,14 +119,13 @@ const printVisitor: InterpretVisitor = { shValue = shValue[0] } if (typeof shValue === 'string' && entry.key === shValue) { - shorthand[i] = [value, entry.optional ? '?' : ''] + return [value, entry.optional ? '?' : ''] } - return { ...entry, key: keyGroup, value } + return group([keyGroup, value]) }) - const inner = entries.map((e, i) => shorthand[i] || group([e.key, e.value])) const sep = ifBreak(line, [',', line]) - const obj = group(['{', indent([line, join(sep, inner)]), line, '}'], { + const obj = group(['{', indent([line, join(sep, entries)]), line, '}'], { shouldBreak, }) return node.context ? [node.context, indent([line, '-> ', obj])] : obj @@ -138,20 +146,24 @@ const printVisitor: InterpretVisitor = { throw new Error(`Unexpected template node: ${og?.kind}`) } - // strip the leading `$` character - let ret = el.slice(1) - + let id: Doc = [og.id.value] const nextEl = node.elements[i + 1] if (isToken(nextEl) && /^\w/.test(nextEl.value)) { // use ${id} syntax to delineate against next element in template - ret = ['{', ret, '}'] + id = ['{', id, '}'] } - return [og.isUrlComponent ? ':' : '$', ret] + return [og.isUrlComponent ? ':' : '$', id] }) }, IdentifierExpr(node) { - return ['$', node.value.value] + 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])] }, SelectorExpr(node) { @@ -181,7 +193,7 @@ const printVisitor: InterpretVisitor = { SliceExpr(node) { const { value } = node.slice - const quot = value.includes('`') ? '```' : '`' + const quot = value.includes('`') ? '|' : '`' const lines = value.split('\n') const slice = group([ quot, diff --git a/packages/parser/src/ast/scope.ts b/packages/parser/src/ast/scope.ts index 60ebb1a..52e9dbb 100644 --- a/packages/parser/src/ast/scope.ts +++ b/packages/parser/src/ast/scope.ts @@ -3,7 +3,7 @@ import { ValueReferenceError } from '@getlang/utils/errors' class Scope { extracted: T | undefined - private contextStack: T[] = [] + contextStack: T[] = [] constructor( public vars: Record, @@ -16,27 +16,17 @@ class Scope { return this.contextStack.at(-1) } - private update() { - if (this.context) { - this.vars[''] = this.context - } else { - delete this.vars[''] - } - } - push(context: T) { this.contextStack.push(context) - this.update() } pop() { this.contextStack.pop() - this.update() } } export class RootScope { - private scopeStack: Scope[] = [] + scopeStack: Scope[] = [] private get head() { return this.scopeStack.at(-1) diff --git a/packages/parser/src/grammar/lex/slice.ts b/packages/parser/src/grammar/lex/slice.ts index 3e54e5b..47d8780 100644 --- a/packages/parser/src/grammar/lex/slice.ts +++ b/packages/parser/src/grammar/lex/slice.ts @@ -2,8 +2,8 @@ import { invariant } from '@getlang/utils' import { QuerySyntaxError } from '@getlang/utils/errors' import { until } from './templates.js' -const getSliceValue = (text: string, places = 1) => { - const src = text.slice(places, -places).replace(/\\`/g, '`') +const getSliceValue = (text: string) => { + const src = text.slice(1, -1).replace(/\\`/g, '`') let lines = src.split('\n') const firstIdx = lines.findIndex(x => x.trim().length) invariant(firstIdx !== -1, new QuerySyntaxError('Slice must contain source')) @@ -15,16 +15,14 @@ const getSliceValue = (text: string, places = 1) => { return lines.join('\n').trim() } -const getSliceBlockValue = (text: string) => getSliceValue(text, 3) - export const slice_block = { defaultType: 'slice', - match: until(/```(?!`)/, { - prefix: /```/, + match: until(/\|/, { + prefix: /\|/, inclusive: true, }), lineBreaks: true, - value: getSliceBlockValue, + value: getSliceValue, pop: 1, } diff --git a/packages/parser/src/grammar/lexer.ts b/packages/parser/src/grammar/lexer.ts index 233ba78..3f03283 100644 --- a/packages/parser/src/grammar/lexer.ts +++ b/packages/parser/src/grammar/lexer.ts @@ -63,6 +63,11 @@ const expr = { match: /[{(]/, pop: 1, }, + template_interp: { + defaultType: 'ws', + match: /(?=\${)/, + next: 'template', + }, identifier_expr: { match: patterns.identifierExpr, value: (text: string) => text.slice(1), diff --git a/packages/parser/src/grammar/parse.ts b/packages/parser/src/grammar/parse.ts index 0887948..1255189 100644 --- a/packages/parser/src/grammar/parse.ts +++ b/packages/parser/src/grammar/parse.ts @@ -1,5 +1,6 @@ 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 { tx } from '../utils.js' @@ -106,7 +107,7 @@ export const object: PP = d => { export const objectEntry: PP = ([callkey, identifier, optional, , , value]) => { const key = { ...identifier, - value: `${callkey ? '@' : ''}${identifier.value}`, + value: `${callkey ? '@' : ''}${identifier.value || '$'}`, } return { key: t.templateExpr([key]), @@ -126,32 +127,24 @@ export const objectEntryShorthandIdent: PP = ([identifier, optional]) => { return objectEntry([null, identifier, optional, null, null, value]) } -const expandingSelectors = [NodeKind.TemplateExpr, NodeKind.IdentifierExpr] -export const drill: PP = ([context, , arrow, , bit]) => { - const expand = arrow.value.startsWith('=') - if (expandingSelectors.includes(bit.kind)) { - return t.selectorExpr(bit, expand, context) +function drillBase(bit: CExpr | TemplateExpr, arrow?: string, context?: Expr) { + const expand = arrow === '=>' + if (bit.kind === NodeKind.TemplateExpr) { + bit = t.selectorExpr(bit, expand) + } else if (bit.kind === NodeKind.IdentifierExpr) { + bit.expand = expand + } else if (expand) { + throw new QuerySyntaxError('Wide arrow drill requires selector on RHS') } - invariant( - !expand, - new QuerySyntaxError('Wide arrow drill requires selector on RHS'), - ) - invariant('context' in bit, new QuerySyntaxError('Invalid drill value')) bit.context = context return bit } -export const drillContext: PP = ([arrow, expr]) => { - const expand = arrow?.[0].value === '=>' - if (expr.kind === NodeKind.TemplateExpr) { - return t.selectorExpr(expr, expand) - } - invariant( - !expand, - new QuerySyntaxError('Wide arrow drill requires selector on RHS'), - ) - return expr -} +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 identifier: PP = ([id]) => { return t.identifierExpr(id) diff --git a/packages/parser/src/passes/analyze.ts b/packages/parser/src/passes/analyze.ts index b78b697..3bac519 100644 --- a/packages/parser/src/passes/analyze.ts +++ b/packages/parser/src/passes/analyze.ts @@ -23,6 +23,12 @@ export function analyze(ast: Program) { inputs.add(node.id.value) return trace.InputDeclStmt(node) }, + SelectorExpr: { + enter(node, visit) { + checkMacro(node) + return trace.SelectorExpr.enter(node, visit) + }, + }, ModifierExpr: { enter(node, visit) { checkMacro(node) @@ -35,12 +41,6 @@ export function analyze(ast: Program) { return trace.ModuleExpr.enter(node, visit) }, }, - SelectorExpr: { - enter(node, visit) { - checkMacro(node) - return trace.SelectorExpr.enter(node, visit) - }, - }, } visit(ast, visitor) diff --git a/packages/parser/src/passes/desugar/acorn-globals.d.ts b/packages/parser/src/passes/desugar/acorn-globals.d.ts deleted file mode 100644 index c78e831..0000000 --- a/packages/parser/src/passes/desugar/acorn-globals.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare module 'acorn-globals' { - import type { Program } from 'acorn' - type Ref = { name: string } - function detect(source: Program): Ref[] - export default detect -} diff --git a/packages/parser/src/passes/desugar/links.ts b/packages/parser/src/passes/desugar/links.ts index 6925589..2024357 100644 --- a/packages/parser/src/passes/desugar/links.ts +++ b/packages/parser/src/passes/desugar/links.ts @@ -18,12 +18,15 @@ export const settleLinks: DesugarPass = ({ parsers }) => { return { ...trace, - IdentifierExpr(node) { - const id = node.value.value - const value = scope.vars[id] - invariant(value, new ValueReferenceError(id)) - inherit(value, node) - return node + 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 + }, }, SelectorExpr: { diff --git a/packages/parser/src/passes/desugar/reqparse.ts b/packages/parser/src/passes/desugar/reqparse.ts index 146051a..e535e6e 100644 --- a/packages/parser/src/passes/desugar/reqparse.ts +++ b/packages/parser/src/passes/desugar/reqparse.ts @@ -11,7 +11,7 @@ export class RequestParsers { private parsers: Parsers[] = [] private require(req: RequestExpr) { - const idx = this.requests.findIndex(r => r === req) + const idx = this.requests.indexOf(req) invariant(idx !== -1, new QuerySyntaxError('Unmapped request')) return idx } @@ -21,7 +21,7 @@ export class RequestParsers { } visit(req: RequestExpr) { - let idx = this.requests.findIndex(r => r === req) + let idx = this.requests.indexOf(req) if (idx === -1) { idx = this.requests.length this.requests.push(req) diff --git a/packages/parser/src/passes/desugar/slicedeps.ts b/packages/parser/src/passes/desugar/slicedeps.ts index 973620a..b87b147 100644 --- a/packages/parser/src/passes/desugar/slicedeps.ts +++ b/packages/parser/src/passes/desugar/slicedeps.ts @@ -1,14 +1,10 @@ -/// - import { invariant } from '@getlang/utils' import { SliceSyntaxError } from '@getlang/utils/errors' -import type { Program } from 'acorn' -import { parse } from 'acorn' -import detect from 'acorn-globals' +import { parse as acorn } from 'acorn' +import { traverse } from 'estree-toolkit' import globals from 'globals' -import type { Expr } from '../../ast/ast.js' -import { t } from '../../ast/ast.js' -import { tx } from '../../utils.js' +import { NodeKind, t } from '../../ast/ast.js' +import { render, tx } from '../../utils.js' import type { DesugarPass } from '../desugar.js' const browserGlobals = [ @@ -16,66 +12,90 @@ const browserGlobals = [ ...Object.keys(globals.builtin), ] -const analyzeSlice = (_source: string, analyzeDeps: boolean) => { - let ast: Program +function parse(source: string) { try { - ast = parse(_source, { + return acorn(source, { ecmaVersion: 'latest', allowReturnOutsideFunction: true, + allowAwaitOutsideFunction: true, }) } catch (e) { throw new SliceSyntaxError('Could not parse slice', { cause: e }) } +} - let source = _source +const validAutoInserts = ['ExpressionStatement', 'BlockStatement'] - // auto-insert the return statement - if (ast.body.length === 1 && ast.body[0]?.type !== 'ReturnStatement') { - source = `return ${source}` - } +const analyzeSlice = (slice: string) => { + let source = slice - const deps: string[] = [] - if (analyzeDeps) { - for (const dep of detect(ast).map(id => id.name)) { - if (!browserGlobals.includes(dep)) { - deps.push(dep) - } - } + const ast = parse(slice) + if (ast.body.at(-1)?.type === 'EmptyStatement') { + return null } - const usesContext = deps.some(d => ['$', '$$'].includes(d)) - const usesVars = deps.some(d => !['$', '$$'].includes(d)) + const init = ast.body[0] + invariant(init, new SliceSyntaxError('Empty slice body')) + if (ast.body.length === 1 && init.type !== 'ReturnStatement') { + // auto-insert the return statement + invariant( + validAutoInserts.includes(init.type), + new SliceSyntaxError(`Invalid slice body: ${init.type}`), + ) + source = `return ${source}` + } - invariant( - !(usesContext && usesVars), - new SliceSyntaxError('Slice must not use context ($) and outer variables'), - ) + let ids: string[] = [] + traverse(ast, { + $: { scope: true }, + Program(path) { + ids = Object.keys(path.scope?.globalBindings ?? {}) + }, + }) + ids = ids.filter(id => !browserGlobals.includes(id)) + const usesVars = ids.some(d => d !== '$') + const deps = new Set(ids) if (usesVars) { - const contextVars = deps.join(', ') - const loadContext = `const { ${contextVars} } = $\n` - source = loadContext + source + const names = [...deps].join(', ') + source = `var { ${names} } = $\n${source}` } - return { source, deps, usesContext } + // add postmark to prevent slice from being re-processed + source = `${source};;` + return { source, deps, usesVars } } export const insertSliceDeps: DesugarPass = () => { return { SliceExpr(node) { - const stat = analyzeSlice(node.slice.value, !node.context) - const slice = tx.token(stat.source) - let context: Expr | undefined = node.context - if (!node.context) { - if (stat.usesContext) { - context = tx.ident('') - } else if (stat.deps.length) { - const deps = stat.deps.map(id => - t.objectEntry(tx.template(id), tx.ident(id)), - ) - context = t.objectLiteralExpr(deps) + const stat = analyzeSlice(node.slice.value) + if (!stat) { + return node + } + + const { source, deps, usesVars } = stat + const slice = tx.token(source) + let context = node.context + + if (usesVars) { + if (context?.kind !== NodeKind.ObjectLiteralExpr) { + context = t.objectLiteralExpr([], context) + } + 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, + }) } + } else if (deps.size === 1 && !context) { + context = t.identifierExpr(tx.token('$', ''), false, context) } + return { ...node, slice, context } }, } diff --git a/packages/parser/src/passes/inference/calls.ts b/packages/parser/src/passes/inference/calls.ts index 1814127..cd2eca9 100644 --- a/packages/parser/src/passes/inference/calls.ts +++ b/packages/parser/src/passes/inference/calls.ts @@ -11,8 +11,16 @@ export function registerCalls(ast: Program, macros: string[] = []) { function registerCall(node?: Expr) { switch (node?.kind) { case NodeKind.IdentifierExpr: { - const value = scope.vars[node.value.value] - return registerCall(value) + 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 !== '', + ), + ) } case NodeKind.SubqueryExpr: { const ex = node.body.find(s => s.kind === NodeKind.ExtractStmt) diff --git a/packages/parser/src/passes/inference/typeinfo.ts b/packages/parser/src/passes/inference/typeinfo.ts index a9031a0..321d4e2 100644 --- a/packages/parser/src/passes/inference/typeinfo.ts +++ b/packages/parser/src/passes/inference/typeinfo.ts @@ -134,11 +134,18 @@ export function resolveTypes(ast: Program, options: ResolveTypeOptions) { }, }, - IdentifierExpr(node) { - const id = node.value.value - const value = scope.vars[id] - invariant(value, new ValueReferenceError(node.value.value)) - return { ...node, typeInfo: structuredClone(value.typeInfo) } + 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 } + }), }, RequestExpr(node) { diff --git a/packages/parser/src/passes/trace.ts b/packages/parser/src/passes/trace.ts index 43780a6..1148610 100644 --- a/packages/parser/src/passes/trace.ts +++ b/packages/parser/src/passes/trace.ts @@ -104,9 +104,15 @@ export function traceVisitor(contextType: TypeInfo = { type: Type.Context }) { }, }, + // simple contextual expressions + IdentifierExpr: { + enter(node, visit) { + return withContext(node, visit, node => node) + }, + }, + SliceExpr: { enter(node, visit) { - // contains no additional expressions (only .context) return withContext(node, visit, node => node) }, }, diff --git a/packages/utils/src/hooks.ts b/packages/utils/src/hooks.ts index 24628fd..c5fe4f7 100644 --- a/packages/utils/src/hooks.ts +++ b/packages/utils/src/hooks.ts @@ -13,7 +13,6 @@ export type RequestHook = ( export type SliceHook = ( slice: string, context?: unknown, - raw?: unknown, ) => MaybePromise export type ExtractHook = ( diff --git a/test/slice.spec.ts b/test/slice.spec.ts index feb551a..b9136d0 100644 --- a/test/slice.spec.ts +++ b/test/slice.spec.ts @@ -57,13 +57,19 @@ describe('slice', () => { ` set html = \`'

title

para 1

para 2

'\` - extract $html -> @html => h1, p -> ( - set raw = \`$$.outerHTML\` - extract $raw - ) + extract $html -> @html => h1, p -> \`$\` `, ) - expect(result).toEqual(['

title

', '

para 1

', '

para 2

']) + expect(result).toEqual(['title', 'para 1', 'para 2']) + }) + + 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[$]\` + `) + expect(result).toEqual('x') }) it('supports escaped backticks', async () => { @@ -71,8 +77,8 @@ describe('slice', () => { expect(result).toEqual('escaped') }) - it('triple backticks as delimiter allow non-escaped backticks', async () => { - const result = await execute('extract ```return `escaped````') + it('blocks allow non-escaped backticks', async () => { + const result = await execute('extract |return `escaped`|') expect(result).toEqual('escaped') }) @@ -84,14 +90,6 @@ describe('slice', () => { expect(result).toEqual('one') }) - it('provides a variable for raw context ($$)', async () => { - const result = await execute(` - set html = \`'
  • one
  • two
'\` - extract $html -> @html -> ul -> \`$$.outerHTML\` - `) - expect(result).toEqual('
  • one
  • two
') - }) - it('operates on list item context', async () => { const result = await execute(` set html = \`'
  • one
  • two
'\` diff --git a/test/values.spec.ts b/test/values.spec.ts index ad6af88..0d2d079 100644 --- a/test/values.spec.ts +++ b/test/values.spec.ts @@ -347,7 +347,7 @@ describe('values', () => { /* eslint-enable prefer-template */ const src = ` - set all = \`\`\`(${slice.toString()})()\`\`\` + set all = |(${slice.toString()})()| extract $all -> @json -> docHtml -> @html -> pre From 0f1b076e347b6ea8d36dd6e29af4c70a22f0fd41 Mon Sep 17 00:00:00 2001 From: Matt Fysh Date: Wed, 20 Aug 2025 21:43:19 +1000 Subject: [PATCH 2/2] add changeset --- .changeset/curly-moose-play.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/curly-moose-play.md diff --git a/.changeset/curly-moose-play.md b/.changeset/curly-moose-play.md new file mode 100644 index 0000000..ce6c0ec --- /dev/null +++ b/.changeset/curly-moose-play.md @@ -0,0 +1,8 @@ +--- +"@getlang/parser": minor +"@getlang/utils": minor +"@getlang/get": minor +"@getlang/lib": minor +--- + +slice dependencies analysis