From d660d3b35d1cdec352136ae87035ec4468f6ffa7 Mon Sep 17 00:00:00 2001 From: Matt Fysh Date: Thu, 18 Sep 2025 14:07:26 +1000 Subject: [PATCH 1/2] fix modifier context --- packages/get/src/calls.ts | 10 ++++-- packages/get/src/registry.ts | 27 +++++++++------- packages/parser/src/passes/analyze.ts | 6 ++-- packages/parser/src/passes/desugar.ts | 8 ++--- packages/parser/src/passes/desugar/context.ts | 9 +++--- packages/parser/src/passes/inference/calls.ts | 7 +++-- test/modifiers.spec.ts | 31 ++++++++++++++++++- 7 files changed, 69 insertions(+), 29 deletions(-) diff --git a/packages/get/src/calls.ts b/packages/get/src/calls.ts index 998be88..096bdc8 100644 --- a/packages/get/src/calls.ts +++ b/packages/get/src/calls.ts @@ -16,8 +16,14 @@ export async function callModifier( ) { const entry = await registry.importMod(mod) if (entry) { - const ctx = - context && entry.materialize ? materialize(context) : context?.data + let ctx: any + if (entry?.useContext) { + if (context && entry.materialize) { + ctx = materialize(context) + } else { + ctx = context?.data + } + } return entry.mod(ctx, args) } diff --git a/packages/get/src/registry.ts b/packages/get/src/registry.ts index a3a889d..b317165 100644 --- a/packages/get/src/registry.ts +++ b/packages/get/src/registry.ts @@ -13,7 +13,8 @@ type ModEntry = { type Info = { ast: Program - imports: Set + imports: string[] + contextMods: string[] isMacro: boolean } @@ -86,15 +87,17 @@ export class Registry { this.info[module] ??= Promise.resolve().then(async () => { const source = await this.hooks.import(module) const ast = parse(source) - const { imports, ...info } = analyze(ast) - let isMacro = info.hasUnboundSelector - for (const [mod, unbound] of info.modifiers) { - if (unbound && !isMacro) { - const entry = await this.importMod(mod) - isMacro = entry?.useContext || false + const info = analyze(ast) + const imports = [...info.imports] + const contextMods: string[] = [] + for (const mod of info.modifiers) { + const entry = await this.importMod(mod) + if (entry?.useContext ?? true) { + contextMods.push(mod) } } - return { ast, imports, isMacro } + const isMacro = info.hasUnboundSelector || contextMods.length > 0 + return { ast, imports, contextMods, isMacro } }) return this.info[module] } @@ -106,15 +109,15 @@ export class Registry { } const key = buildImportKey(module, contextType) this.entries[key] ??= Promise.resolve().then(async () => { - const { ast, imports } = await this.getInfo(module) - const macros: string[] = [] + const { ast, imports, contextMods } = await this.getInfo(module) + const contextual = [...contextMods] for (const i of imports) { const depInfo = await this.getInfo(i) if (depInfo.isMacro) { - macros.push(i) + contextual.push(i) } } - const simplified = desugar(ast, macros) + const simplified = desugar(ast, contextual) const { inputs, calls, modifiers } = analyze(simplified) const returnTypes: Record = {} diff --git a/packages/parser/src/passes/analyze.ts b/packages/parser/src/passes/analyze.ts index 0e2bd1a..66f00df 100644 --- a/packages/parser/src/passes/analyze.ts +++ b/packages/parser/src/passes/analyze.ts @@ -5,7 +5,7 @@ export function analyze(ast: Program) { const scope = new ScopeTracker() const inputs = new Set() const calls = new Set() - const modifiers = new Map() + const modifiers = new Set() const imports = new Set() let hasUnboundSelector = false @@ -22,9 +22,7 @@ export function analyze(ast: Program) { hasUnboundSelector ||= !scope.context }, ModifierExpr(node) { - const mod = node.modifier.value - const unbound = modifiers.get(mod) || !scope.context - modifiers.set(mod, unbound) + modifiers.add(node.modifier.value) }, }) diff --git a/packages/parser/src/passes/desugar.ts b/packages/parser/src/passes/desugar.ts index f2dc033..fd3f44b 100644 --- a/packages/parser/src/passes/desugar.ts +++ b/packages/parser/src/passes/desugar.ts @@ -11,7 +11,7 @@ export type DesugarPass = ( ast: Program, tools: { parsers: RequestParsers - macros: string[] + contextual: string[] }, ) => Program @@ -23,13 +23,13 @@ const visitors = [ dropDrills, ] -export function desugar(ast: Program, macros: string[] = []): Program { +export function desugar(ast: Program, contextual: string[] = []): Program { const parsers = new RequestParsers() const program = visitors.reduce((ast, pass) => { parsers.reset() - return pass(ast, { parsers, macros }) + return pass(ast, { parsers, contextual }) }, ast) // inference pass `registerCalls` is included in the desugar phase // it produces the list of called modules required for type inference - return registerCalls(program, macros) + return registerCalls(program, contextual) } diff --git a/packages/parser/src/passes/desugar/context.ts b/packages/parser/src/passes/desugar/context.ts index b45bbab..de6c82d 100644 --- a/packages/parser/src/passes/desugar/context.ts +++ b/packages/parser/src/passes/desugar/context.ts @@ -3,7 +3,7 @@ import { QuerySyntaxError } from '@getlang/lib/errors' import { ScopeTracker, transform } from '@getlang/walker' import type { DesugarPass } from '../desugar.js' -export const resolveContext: DesugarPass = (ast, { parsers, macros }) => { +export const resolveContext: DesugarPass = (ast, { parsers, contextual }) => { const scope = new ScopeTracker() const program = transform(ast, { @@ -34,8 +34,8 @@ export const resolveContext: DesugarPass = (ast, { parsers, macros }) => { ModifierExpr(node) { const ctx = scope.context - if (ctx?.kind === 'RequestExpr') { - const mod = node.modifier.value + const mod = node.modifier.value + if (contextual.includes(mod) && ctx?.kind === 'RequestExpr') { // replace modifier with shared parser return parsers.lookup(ctx, mod) } @@ -43,7 +43,8 @@ export const resolveContext: DesugarPass = (ast, { parsers, macros }) => { ModuleExpr(node, path) { const ctx = scope.context - if (macros.includes(node.module.value) && ctx?.kind === 'RequestExpr') { + const module = node.module.value + if (contextual.includes(module) && ctx?.kind === 'RequestExpr') { path.insertBefore(parsers.lookup(ctx)) } }, diff --git a/packages/parser/src/passes/inference/calls.ts b/packages/parser/src/passes/inference/calls.ts index 0048e27..986564d 100644 --- a/packages/parser/src/passes/inference/calls.ts +++ b/packages/parser/src/passes/inference/calls.ts @@ -3,7 +3,10 @@ import { isToken } from '@getlang/ast' import { transform } from '@getlang/walker' import { LineageTracker } from '../lineage.js' -export function registerCalls(ast: Program, macros: string[] = []): Program { +export function registerCalls( + ast: Program, + contextual: string[] = [], +): Program { const scope = new LineageTracker() function registerCall(node: Expr) { @@ -38,7 +41,7 @@ export function registerCalls(ast: Program, macros: string[] = []): Program { }, ModuleExpr(node) { - if (macros.includes(node.module.value)) { + if (contextual.includes(node.module.value)) { return { ...node, call: true } } }, diff --git a/test/modifiers.spec.ts b/test/modifiers.spec.ts index 84e427b..013b5b5 100644 --- a/test/modifiers.spec.ts +++ b/test/modifiers.spec.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from 'bun:test' import type { Modifier, ModifierHook } from '@getlang/lib' import { invariant } from '@getlang/lib' import { ValueTypeError } from '@getlang/lib/errors' +import type { Fetch } from './helpers.js' import { execute as exec } from './helpers.js' function execute( @@ -9,12 +10,13 @@ function execute( name: string, fn: Modifier, useContext?: boolean, + fetch?: Fetch, ) { const modifier: ModifierHook = mod => { expect(mod).toEqual(name) return { modifier: fn, useContext } } - return exec(source, {}, { hooks: { modifier } }) + return exec(source, {}, { fetch, hooks: { modifier } }) } describe('modifiers', () => { @@ -31,10 +33,37 @@ describe('modifiers', () => { expect(ctx).toEqual(1) return ctx + 1 }, + true, ) expect(result).toEqual(2) }) + test('without context', async () => { + const result = await execute( + ` + GET http://example.com + + set url = $ -> url + set value = @nocontext + + extract { $url, $value } + `, + 'nocontext', + ctx => { + expect(ctx).toBeUndefined() + return 123 + }, + false, + () => + new Response('

test

', { + headers: { + 'content-type': 'text/html', + }, + }), + ) + expect(result).toEqual({ url: 'http://example.com/', value: 123 }) + }) + test('with args', async () => { const result = await execute( 'extract @product({ a: 7, b: 6 })', From ecd87ab290fbccc7d61bc82950f82d8ed8b974d5 Mon Sep 17 00:00:00 2001 From: Matt Fysh Date: Thu, 18 Sep 2025 14:12:35 +1000 Subject: [PATCH 2/2] add changeset --- .changeset/loose-baboons-take.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/loose-baboons-take.md diff --git a/.changeset/loose-baboons-take.md b/.changeset/loose-baboons-take.md new file mode 100644 index 0000000..d5b1633 --- /dev/null +++ b/.changeset/loose-baboons-take.md @@ -0,0 +1,6 @@ +--- +"@getlang/parser": patch +"@getlang/get": patch +--- + +fix modifier context