Skip to content

Commit

Permalink
fix: refactor and reuse scope handling from evaluator
Browse files Browse the repository at this point in the history
  • Loading branch information
sgulseth committed Mar 5, 2024
1 parent d83871c commit 7bed827
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 63 deletions.
40 changes: 22 additions & 18 deletions src/typeEvaluator/evaluateQueryType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import type {
import {parse} from '../parser'
import {handleFuncCallNode} from './functions'
import {optimizeUnions} from './optimizations'
import {createContext, createScope, Scope} from './scope'
import {Context, Scope} from './scope'
import type {
ArrayTypeNode,
BooleanTypeNode,
Expand Down Expand Up @@ -57,7 +57,7 @@ const $warn = debug('typeEvaluator:evaluate::warn')
export function typeEvaluate(ast: ExprNode, schema: Schema): TypeNode {
const parsed = walk({
node: ast,
scope: createScope([], undefined, createContext(schema)),
scope: new Scope([], undefined, new Context(schema)),
})

$trace('evaluateQueryType.parsed %O', parsed)
Expand Down Expand Up @@ -352,7 +352,7 @@ function handleSelectNode(node: SelectNode, scope: Scope): TypeNode {
const conditionValue = walk({node: alternative.condition, scope})
const conditionScope = resolveFilter(alternative.condition, scope)
if (conditionScope.type === 'union' && conditionScope.of.length > 0) {
values.push(walk({node: alternative.value, scope: scope.subscope(conditionScope.of, true)}))
values.push(walk({node: alternative.value, scope: scope.createHidden(conditionScope.of)}))
}
if (conditionValue.type === 'boolean' && conditionValue.value === true) {
guaranteed = true
Expand Down Expand Up @@ -388,7 +388,7 @@ function handleFlatMap(node: FlatMapNode, scope: Scope): TypeNode {
return {type: 'null'}
}

return walk({node: node.expr, scope: scope.subscope([base.of], true)})
return walk({node: node.expr, scope: scope.createHidden([base.of])})
})
}
function handleMap(node: MapNode, scope: Scope): TypeNode {
Expand All @@ -400,7 +400,7 @@ function handleMap(node: MapNode, scope: Scope): TypeNode {
}

if (base.of.type === 'union') {
const value = walk({node: node.expr, scope: scope.subscope(base.of.of, true)}) // re use the current parent, this is a "sub" scope
const value = walk({node: node.expr, scope: scope.createHidden(base.of.of)}) // re use the current parent, this is a "sub" scope
$trace('map.expr %O', value)

return {
Expand Down Expand Up @@ -451,20 +451,20 @@ function handleProjectionNode(node: ProjectionNode, scope: Scope): TypeNode {
}
return walk({
node: node.expr,
scope: scope.subscope([field]),
scope: scope.createNested([field]),
})
})
}

function createFilterScope(base: TypeNode, scope: Scope): Scope {
if (base.type === 'array') {
if (base.of.type === 'union') {
return scope.subscope(base.of.of)
return scope.createNested(base.of.of)
}
return scope.subscope([base.of])
return scope.createNested([base.of])
}

return scope.subscope([base])
return scope.createNested([base])
}
function handleFilterNode(node: FilterNode, scope: Scope): TypeNode {
const base = walk({node: node.base, scope})
Expand Down Expand Up @@ -612,16 +612,20 @@ function handleValueNode(node: ValueNode, scope: Scope): TypeNode {
}
}

function handleParentNode(node: ParentNode, scope: Scope): TypeNode {
let newScope: Scope | undefined = scope
for (let n = node.n; n > 0; n--) {
newScope = newScope?.parent
function handleParentNode({n}: ParentNode, scope: Scope): TypeNode {
let current: Scope = scope
for (let i = 0; i < n; i++) {
if (!current.parent) {
return {type: 'null'} satisfies NullTypeNode
}
current = current.parent
}
$trace('parent.scope %d %O', node.n, newScope)
if (newScope !== undefined) {
return newScope.value
$trace('parent.scope %d %O', n, current.value)
if (current.value.of.length === 0) {
return {type: 'null'} satisfies NullTypeNode
}
return {type: 'null'} satisfies NullTypeNode

return current.value
}

function handleNotNode(node: NotNode, scope: Scope): TypeNode {
Expand Down Expand Up @@ -1066,7 +1070,7 @@ function resolveFilter(expr: ExprNode, scope: Scope): TypeNode {
(node) =>
// create a new scope with the current scopes parent as the parent. It's only a temporary scope since we only want to resolve the condition
// check if the result is true or undefined. Undefined means that the condition can't be resolved, and we should keep the node
resolveCondition(expr, scope.subscope([node], true)) !== false,
resolveCondition(expr, scope.createHidden([node])) !== false,
)
$trace(
`resolveFilter ${expr.type === 'OpCall' ? `${expr.type}/${expr.op}` : expr.type} %O`,
Expand Down
87 changes: 43 additions & 44 deletions src/typeEvaluator/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,63 +5,62 @@ import {NullTypeNode, ReferenceTypeNode, Schema, TypeNode, UnionTypeNode} from '
const $trace = debug('typeEvaluator:scope:trace')
$trace.log = console.log.bind(console) // eslint-disable-line no-console

export interface Context {
export class Context {
readonly schema: Schema

lookupRef(ref: ReferenceTypeNode): TypeNode
lookupType(name: ReferenceTypeNode): TypeNode
}

export function createContext(schema: Schema): Context {
return {
schema,
constructor(schema: Schema) {
this.schema = schema
}

lookupRef(ref) {
for (const val of this.schema) {
if (val.type === 'document') {
if (val.name === ref.to) {
return {
type: 'object',
attributes: val.attributes,
}
lookupRef(ref: ReferenceTypeNode): TypeNode {
for (const val of this.schema) {
if (val.type === 'document') {
if (val.name === ref.to) {
return {
type: 'object',
attributes: val.attributes,
}
}
}
return {type: 'null'} satisfies NullTypeNode
},
}
return {type: 'null'} satisfies NullTypeNode
}

lookupType(ref) {
for (const val of this.schema) {
if (val.type === 'type') {
if (val.name === ref.to) {
return val.value
}
lookupType(ref: ReferenceTypeNode): TypeNode {
for (const val of this.schema) {
if (val.type === 'type') {
if (val.name === ref.to) {
return val.value
}
}
return {type: 'null'} satisfies NullTypeNode
},
}
return {type: 'null'} satisfies NullTypeNode
}
}

export interface Scope {
value: UnionTypeNode
parent: Scope | undefined
context: Context
export class Scope {
public value: UnionTypeNode
public parent: Scope | undefined
public context: Context
public isHidden: boolean

subscope(value: TypeNode[], hidden?: boolean): Scope
}
export function createScope(value: TypeNode[], parent?: Scope, context?: Context): Scope {
$trace('createScope', JSON.stringify({value, hasParent: parent !== undefined}, null, 2))
return {
value: {type: 'union', of: value} satisfies UnionTypeNode,
parent,
context: context || parent?.context || createContext([]),
constructor(value: TypeNode[], parent?: Scope, context?: Context) {
this.value = {type: 'union', of: value} satisfies UnionTypeNode
this.parent = parent
this.context = context || parent?.context || new Context([])
this.isHidden = false
}

subscope(value, hidden = false) {
if (hidden) {
return createScope(value, this.parent, this.context)
}
return createScope(value, this, this.context)
},
createNested(value: TypeNode[]): Scope {
if (this.isHidden) {
return new Scope(value, this.parent, this.context)
}
return new Scope(value, this, this.context)
}

createHidden(value: TypeNode[]): Scope {
const result = this.createNested(value)
result.isHidden = true
return result
}
}
12 changes: 12 additions & 0 deletions tap-snapshots/test/evaluateQueryType.test.ts.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,12 @@ exports[`test/evaluateQueryType.test.ts TAP filter order doesnt matter > must ma
Object {
"of": Object {
"attributes": Object {
"_createdAt": Object {
"type": "objectAttribute",
"value": Object {
"type": "string",
},
},
"_id": Object {
"type": "objectAttribute",
"value": Object {
Expand All @@ -656,6 +662,12 @@ Object {
"value": "author",
},
},
"age": Object {
"type": "objectAttribute",
"value": Object {
"type": "number",
},
},
"firstname": Object {
"type": "objectAttribute",
"value": Object {
Expand Down
33 changes: 32 additions & 1 deletion test/evaluateQueryType.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
TypeNode,
UnionTypeNode,
} from '../src/typeEvaluator/types'
import {satisfies} from '../src/typeEvaluator/satisfies'

const postDocument = {
type: 'document',
Expand Down Expand Up @@ -43,6 +42,13 @@ const postDocument = {
},
optional: true,
} satisfies ObjectAttribute,
publishedAt: {
type: 'objectAttribute',
value: {
type: 'string',
},
optional: true,
} satisfies ObjectAttribute,
author: {
type: 'objectAttribute',
value: {
Expand Down Expand Up @@ -133,6 +139,12 @@ const authorDocument = {
type: 'string',
},
},
_createdAt: {
type: 'objectAttribute',
value: {
type: 'string',
},
},
age: {
type: 'objectAttribute',
value: {
Expand Down Expand Up @@ -820,6 +832,13 @@ t.test('deref with projection union', (t) => {
},
optional: true,
},
publishedAt: {
type: 'objectAttribute',
value: {
type: 'string',
},
optional: true,
},
author: {
type: 'objectAttribute',
value: {
Expand Down Expand Up @@ -1318,6 +1337,18 @@ t.test('with splat', (t) => {
type: 'string',
},
},
_createdAt: {
type: 'objectAttribute',
value: {
type: 'string',
},
},
age: {
type: 'objectAttribute',
value: {
type: 'number',
},
},
object: {
type: 'objectAttribute',
value: {
Expand Down

0 comments on commit 7bed827

Please sign in to comment.