Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bugfix: subquery context switching #16

Merged
merged 2 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/witty-wolves-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@getlang/parser": patch
---

bugfix: subquery context switching
Binary file modified bun.lockb
Binary file not shown.
15 changes: 9 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "get",
"license": "Apache-2.0",
"private": true,
"packageManager": "[email protected].22",
"packageManager": "[email protected].27",
"scripts": {
"format": "biome check --write",
"lint": "bun lint:check && bun lint:types && bun lint:unused && bun lint:repo",
Expand All @@ -20,10 +20,10 @@
"devDependencies": {
"@biomejs/biome": "1.8.2",
"@changesets/changelog-github": "^0.5.0",
"@changesets/cli": "^2.27.7",
"@types/bun": "^1.1.6",
"knip": "^5.27.2",
"sherif": "^0.10.0",
"@changesets/cli": "^2.27.8",
"@types/bun": "^1.1.8",
"knip": "^5.30.0",
"sherif": "^1.0.0",
"typescript": "^5.5.4"
},
"knip": {
Expand All @@ -36,7 +36,10 @@
],
"workspaces": {
"packages/parser": {
"entry": ["index.ts", "grammar.ts"]
"entry": [
"index.ts",
"grammar.ts"
]
}
}
}
Expand Down
152 changes: 75 additions & 77 deletions packages/parser/desugar/inference/typeinfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ import type { TransformVisitor, Visit } from '../../visitor/transform.js'
import { traceVisitor } from '../trace.js'
import { render, selectTypeInfo } from '../utils.js'

function clone(a: unknown) {
return JSON.parse(JSON.stringify(a))
const modTypeMap: Record<string, TypeInfo> = {
html: { type: Type.Html },
json: { type: Type.Value },
js: { type: Type.Js },
headers: { type: Type.Headers },
cookies: { type: Type.Cookies },
link: { type: Type.Value },
}

function unwrap(typeInfo: TypeInfo) {
Expand All @@ -25,6 +30,26 @@ function unwrap(typeInfo: TypeInfo) {
}
}

function rewrap(
typeInfo: TypeInfo | undefined,
itemTypeInfo: TypeInfo,
optional: boolean,
): TypeInfo {
switch (typeInfo?.type) {
case Type.List:
return { ...typeInfo, of: rewrap(typeInfo.of, itemTypeInfo, optional) }
case Type.Maybe: {
const option = rewrap(typeInfo.option, itemTypeInfo, optional)
if (option.type === Type.Maybe || !optional) {
return option
}
return { ...typeInfo, option }
}
default:
return structuredClone(itemTypeInfo)
}
}

export function inferTypeInfo(): TransformVisitor {
const scope = new RootScope<Expr>()

Expand All @@ -37,41 +62,25 @@ export function inferTypeInfo(): TransformVisitor {
return ret
}

function rewrap(
typeInfo: TypeInfo | undefined,
itemTypeInfo: TypeInfo,
): TypeInfo {
switch (typeInfo?.type) {
case Type.List:
return { ...typeInfo, of: rewrap(typeInfo.of, itemTypeInfo) }
case Type.Maybe: {
const option = rewrap(typeInfo.option, itemTypeInfo)
if (option.type === Type.Maybe || !optional) {
return option
}
return { ...typeInfo, option }
function ctx<C extends CExpr>(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),
}
default:
return clone(itemTypeInfo)
}
}

function itemVisit(node: CExpr, visit: Visit): Visit {
return child => {
if (child === node.context || !scope.context?.typeInfo) {
return visit(child)
}
scope.pushContext({
...scope.context,
typeInfo: unwrap(scope.context.typeInfo),
})
const xnode = setOptional(false, () => visit(child))
scope.popContext()
return xnode
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 }
}
}

// SliceExpr and ModuleCallExpr use default Type.Value
const trace = traceVisitor(scope)

return {
Expand Down Expand Up @@ -102,8 +111,7 @@ export function inferTypeInfo(): TransformVisitor {
const id = node.value.value
const value = scope.vars[id]
invariant(value, new ValueReferenceError(node.value.value))
const { typeInfo } = value
return { ...node, typeInfo: clone(typeInfo) }
return { ...node, typeInfo: structuredClone(value.typeInfo) }
},

RequestExpr(node) {
Expand All @@ -122,17 +130,17 @@ export function inferTypeInfo(): TransformVisitor {
},

SliceExpr: {
enter(node, visit) {
const xnode = trace.SliceExpr.enter(node, itemVisit(node, visit))
enter: ctx((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: rewrap(xnode.context?.typeInfo, typeInfo) }
},
return { ...xnode, typeInfo }
}),
},

SelectorExpr: {
enter(node, visit) {
const xnode = trace.SelectorExpr.enter(node, itemVisit(node, visit))
enter: ctx((node, visit) => {
const xnode = trace.SelectorExpr.enter(node, visit)
let typeInfo: TypeInfo = unwrap(
xnode.context?.typeInfo ?? { type: Type.Value },
)
Expand All @@ -153,52 +161,42 @@ export function inferTypeInfo(): TransformVisitor {
} else if (optional) {
typeInfo = { type: Type.Maybe, option: typeInfo }
}
return { ...xnode, typeInfo: rewrap(xnode.context?.typeInfo, typeInfo) }
},

return { ...xnode, typeInfo }
}),
},

ModifierExpr: {
enter(node, visit) {
const modTypeMap: Record<string, TypeInfo> = {
html: { type: Type.Html },
json: { type: Type.Value },
js: { type: Type.Js },
headers: { type: Type.Headers },
cookies: { type: Type.Cookies },
link: { type: Type.Value },
}
const xnode = trace.ModifierExpr.enter(node, itemVisit(node, visit))
enter: ctx((node, visit) => {
const xnode = trace.ModifierExpr.enter(node, visit)
const mod = xnode.value.value
const typeInfo = modTypeMap[mod]
invariant(typeInfo, new QuerySyntaxError(`Unknown modifier: ${mod}`))
return { ...xnode, typeInfo: rewrap(xnode.context?.typeInfo, typeInfo) }
},
return { ...xnode, typeInfo }
}),
},

SubqueryExpr: {
enter(node, visit) {
const xnode = trace.SubqueryExpr.enter(node, itemVisit(node, visit))
enter: ctx((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: rewrap(xnode.context?.typeInfo, typeInfo) }
},
return { ...xnode, typeInfo }
}),
},

ObjectLiteralExpr: {
enter(node, visit) {
const xnode = trace.ObjectLiteralExpr.enter(
node,
itemVisit(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))
}),
)
enter: ctx((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))
})

const typeInfo: TypeInfo = {
type: Type.Struct,
Expand All @@ -215,16 +213,16 @@ export function inferTypeInfo(): TransformVisitor {
),
}

return { ...xnode, typeInfo: rewrap(xnode.context?.typeInfo, typeInfo) }
},
return { ...xnode, typeInfo }
}),
},

ModuleCallExpr: {
enter(node, visit) {
const xnode = trace.ModuleCallExpr.enter(node, itemVisit(node, visit))
enter: ctx((node, visit) => {
const xnode = trace.ModuleCallExpr.enter(node, visit)
const typeInfo: TypeInfo = { type: Type.Value }
return { ...xnode, typeInfo: rewrap(xnode.context?.typeInfo, typeInfo) }
},
return { ...xnode, typeInfo }
}),
},
}
}
9 changes: 5 additions & 4 deletions packages/parser/desugar/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import type { TransformVisitor, Visit } from '../visitor/transform.js'

export function traceVisitor(scope: RootScope<Expr>) {
function ctx<C extends CExpr>(node: C, visit: Visit, cb: (tnode: C) => C) {
const context = node.context && visit(node.context)
context && scope.pushContext(context)
if (!node.context) return cb(node)
const context = visit(node.context)
scope.pushContext(context)
const xnode = cb({ ...node, context })
context && scope.popContext()
scope.popContext()
return xnode
}

Expand Down Expand Up @@ -84,7 +85,7 @@ export function traceVisitor(scope: RootScope<Expr>) {

SliceExpr: {
enter(node, visit) {
// contains no additional expressions beyond .context
// contains no additional expressions (only .context)
return ctx(node, visit, node => node)
},
},
Expand Down
14 changes: 14 additions & 0 deletions test/request.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,20 @@ describe('request', () => {
`)
expect(result).toEqual({ whicha: 1, whichb: 2 })
})

it('updates subquery context', async () => {
const result = await execute(`
set x? = \`'<p>'\` -> @html -> (
GET http://example.com

extract h1
)

extract $x
`)

expect(result).toEqual('test')
})
})

describe('inference', () => {
Expand Down