// The core interpreter: evaluate a bound expression against a slot-indexed // row. Aggregate functions are handled by the executor, never here (they // evaluate to Null if reached).
package eval import ( ) // Eval evaluates expr against row (slot-indexed bindings). func ( *Ctx, ast.Expr, []value.Value, map[string]int) value.Value { switch e := .(type) { case *ast.Lit: return LitValue(, .Value) case *ast.Var: return slotValue(, , .Name) case *ast.Prop: return propRead(, slotValue(, , .Var), .Key) case *ast.Unary: := (, .Expr, , ) switch .Op { case ast.Not: if , := .AsBool(); { return value.Bool(!) } return value.Null() default: // ast.Neg if , := .AsInt(); { // -MinInt64 overflows; Null rather than wrap. if == math.MinInt64 { return value.Null() } return value.Int(-) } if , := .AsFloat(); && .Kind() == value.KindFloat { return value.Float(-) } return value.Null() } case *ast.Binary: return evalBinary(, , , ) case *ast.ListExpr: := make([]value.Value, len(.Elems)) for , := range .Elems { [] = (, , , ) } return value.List() case *ast.In: return evalIn(, , , ) case *ast.IsTruth: := (, .Expr, , ) , := .AsBool() := && == .Want return value.Bool( != .Negated) case *ast.IsTyped: := (, .Expr, , ) var bool switch .Kind { case "integer": = .Kind() == value.KindInt case "float": = .Kind() == value.KindFloat case "string": = .Kind() == value.KindStr case "boolean": = .Kind() == value.KindBool case "list": = .Kind() == value.KindList case "node": = .Kind() == value.KindNode case "relationship": = .Kind() == value.KindRel } return value.Bool( != .Negated) case *ast.IsNull: := (, .Expr, , ) return value.Bool(.IsNull() != .Negated) case *ast.Case: return evalCase(, , , ) case *ast.Exists: return value.Bool(evalExists(, .Pattern, .Where, , )) case *ast.CountSub: return value.Int(evalCountSub(, .Pattern, .Where, , )) case *ast.PatternComp: return evalPatternComp(, , , ) case *ast.Cost: // Weighted shortest-path cost has no GQL surface; the CALL-proc // route lands with the traversal milestone. TODO(M17): dispatch // through the shortest-path kernels. return value.Null() case *ast.Func: return evalScalarFunc(, , , ) case *ast.ListPred: return evalListPred(, , , ) case *ast.Reduce: return evalReduce(, , , ) case *ast.ListComp: return evalListComp(, , , ) case *ast.Index: return evalIndex(, , , ) case *ast.Slice: return evalSlice(, , , ) case *ast.PropOf: return propRead(, (, .Base, , ), .Key) case *ast.MapProj: return evalMapProj(, , , ) case *ast.MapLit: := make([]value.MapEntry, len(.Fields)) for , := range .Fields { [] = value.MapEntry{Key: .Key, Val: (, .Val, , )} } return value.Map() case *ast.HasLabelExpr: if , := slotValue(, , .Var).AsNode(); { return value.Bool(evalLabelExpr(, , .Expr)) } // Unbound (e.g. a null from OPTIONAL MATCH) or non-node: null. return value.Null() } return value.Null() } // slotValue reads a variable's bound value from the row; unbound is Null. func slotValue( []value.Value, map[string]int, string) value.Value { if , := []; && >= 0 && < len() { return [] } return value.Null() } // propRead is property access on any base value: a node or rel reads the // graph; a map reads its entries; a temporal reads a component; an i64 // reads as epoch millis with the accessor as the type signal (someInt.year // would otherwise be a type error yielding null, so the temporal reading // is the only useful one). Anything else is Null. func propRead( *Ctx, value.Value, string) value.Value { switch .Kind() { case value.KindNode: , := .AsNode() if , := .G.NodeProp(, ); { return } case value.KindRel: , := .AsRel() if , := .G.RelProp(, ); { return } case value.KindMap: , := .AsMap() return mapGet(, ) case value.KindTemporal: , , := .AsTemporal() return temporalComponentOf(, ) case value.KindInt: , := .AsInt() return temporalComponentOf(, ) case value.KindDuration: , , , := .AsDuration() if , := DurationComponent(, , , ); { return value.Int() } } return value.Null() } // mapGet looks a key up in a map value's entries (case-sensitive), // returning the field or Null when absent. func mapGet( []value.MapEntry, string) value.Value { for , := range { if .Key == { return .Val } } return value.Null() } // temporalComponentOf reads an integer as epoch milliseconds and returns // the named component; an unknown component is Null. func temporalComponentOf( int64, string) value.Value { if , := Component(, ); { return value.Int() } return value.Null() } // evalLabelExpr evaluates a boolean label expression against a node. func evalLabelExpr( *Ctx, uint32, *ast.LabelExpr) bool { switch .Kind { case ast.LabelName: return .G.HasLabel(, .Name) case ast.LabelWild: // %: the node carries at least one label (label counts are small). for , := range .G.LabelNames() { if .G.HasLabel(, ) { return true } } return false case ast.LabelAnd: return (, , .L) && (, , .R) case ast.LabelOr: return (, , .L) || (, , .R) default: // ast.LabelNot return !(, , .L) } } // evalIn is `expr IN list` with openCypher null semantics: a null operand // is null; a miss is null (not false) when the list contains a null // element; a non-list operand is null. func evalIn( *Ctx, *ast.In, []value.Value, map[string]int) value.Value { := Eval(, .Expr, , ) if .IsNull() { return value.Null() } , := Eval(, .List, , ).AsList() if ! { return value.Null() } := false for , := range { if value.Equal(, ) { return value.Bool(true) } if .IsNull() { = true } } if { return value.Null() } return value.Bool(false) } // evalCase: the simple form compares the operand for equality against each // WHEN value; the searched form takes the first truthy WHEN condition. func evalCase( *Ctx, *ast.Case, []value.Value, map[string]int) value.Value { if .Operand != nil { := Eval(, .Operand, , ) for , := range .Whens { if value.Equal(Eval(, .Cond, , ), ) { return Eval(, .Result, , ) } } } else { for , := range .Whens { if Eval(, .Cond, , ).IsTruthy() { return Eval(, .Result, , ) } } } if .Else != nil { return Eval(, .Else, , ) } return value.Null() } // evalIndex is base[index]: 0-based list indexing, a negative index counts // from the end; out of range or non-list/non-int yields null. func evalIndex( *Ctx, *ast.Index, []value.Value, map[string]int) value.Value { , := Eval(, .Base, , ).AsList() if ! { return value.Null() } , := Eval(, .Idx, , ).AsInt() if ! { return value.Null() } if < 0 { += int64(len()) } if < 0 || >= int64(len()) { return value.Null() } return [] } // evalSlice is base[from..to]: the sublist [from, to). Omitted bounds // default to the list ends; negative bounds count from the end; bounds // clamp and an empty/inverted range yields []. A non-list base or // non-integer bound yields null. func evalSlice( *Ctx, *ast.Slice, []value.Value, map[string]int) value.Value { , := Eval(, .Base, , ).AsList() if ! { return value.Null() } := int64(len()) := func( ast.Expr, int64) (int64, bool) { if == nil { return , true } , := Eval(, , , ).AsInt() if ! { return 0, false } if < 0 { += } return , true } , := (.From, 0) , := (.To, ) if ! || ! { return value.Null() } = min(max(, 0), ) = min(max(, 0), ) if >= { return value.List(nil) } return value.List([:]) } // evalMapProj builds a map by reading the base variable's properties // (.key), evaluating computed fields (name: expr), and expanding .* to // every property the node carries, preserving written order. func evalMapProj( *Ctx, *ast.MapProj, []value.Value, map[string]int) value.Value { := slotValue(, , .Var) := make([]value.MapEntry, 0, len(.Entries)) for , := range .Entries { switch .Kind { case ast.MapProjProp: = append(, value.MapEntry{Key: .Key, Val: propRead(, , .Key)}) case ast.MapProjField: = append(, value.MapEntry{Key: .Key, Val: Eval(, .Expr, , )}) default: // ast.MapProjAll -- only nodes have enumerable properties. if , := .AsNode(); { for , := range .G.NodePropKeys() { , := .G.NodeProp(, ) = append(, value.MapEntry{Key: , Val: }) } } } } return value.Map() } // evalBinary: AND/OR short-circuit under three-valued logic; comparisons // go through value.Compare; arithmetic and string predicates below. func evalBinary( *Ctx, *ast.Binary, []value.Value, map[string]int) value.Value { switch .Op { case ast.OpAnd: , := value.ThreeValued(Eval(, .LHS, , )) if && ! { return value.Bool(false) } , := value.ThreeValued(Eval(, .RHS, , )) return value.KleeneAnd(, , , ) case ast.OpOr: , := value.ThreeValued(Eval(, .LHS, , )) if && { return value.Bool(true) } , := value.ThreeValued(Eval(, .RHS, , )) return value.KleeneOr(, , , ) case ast.OpXor: // Three-valued XOR: null when either side is null (no // short-circuit -- unlike AND/OR, one known side never decides). , := value.ThreeValued(Eval(, .LHS, , )) , := value.ThreeValued(Eval(, .RHS, , )) if ! || ! { return value.Null() } return value.Bool( != ) } := Eval(, .LHS, , ) := Eval(, .RHS, , ) switch .Op { case ast.OpEq: return CmpBool(, , func( int) bool { return == 0 }) case ast.OpNeq: return CmpBool(, , func( int) bool { return != 0 }) case ast.OpLt: return CmpBool(, , func( int) bool { return < 0 }) case ast.OpLte: return CmpBool(, , func( int) bool { return <= 0 }) case ast.OpGt: return CmpBool(, , func( int) bool { return > 0 }) case ast.OpGte: return CmpBool(, , func( int) bool { return >= 0 }) case ast.OpConcat: return Concat(, ) case ast.OpStartsWith, ast.OpEndsWith, ast.OpContains: return StrPred(.Op, , ) default: return Arith(.Op, , ) } } // CmpBool maps a three-valued comparison through a predicate on the // ordering; incomparable is Null. Shared with the compiled path. func (, value.Value, func(int) bool) value.Value { if , := value.Compare(, ); { return value.Bool(()) } return value.Null() } // Concat is the || operator: string or list concatenation only -- unlike // +, it never adds numbers; any other operand pair is Null. func (, value.Value) value.Value { if , := .AsStr(); { if , := .AsStr(); { return value.Str( + ) } return value.Null() } if , := .AsList(); { if , := .AsList(); { := make([]value.Value, 0, len()+len()) = append(, ...) = append(, ...) return value.List() } } return value.Null() }