///|
priv struct EvalContext {
row : Binding
budget : Budget
group : Array[Binding]?
}
///|
fn boolean(value : Bool) -> Term {
{
kind: LiteralKind,
lexical: if value {
"true"
} else {
"false"
},
datatype: xsd_namespace() + "boolean",
language: None,
}
}
///|
fn integer(value : Int32) -> Term {
{
kind: LiteralKind,
lexical: value.to_string(),
datatype: xsd_namespace() + "integer",
language: None,
}
}
///|
fn numeric_type(t : Term) -> Bool {
t.kind == LiteralKind &&
["integer", "decimal", "double"].any(suffix => {
t.datatype == xsd_namespace() + suffix
})
}
///|
fn ebv(t : Term, digits : Int32) -> Bool raise QueryError {
if t.kind != LiteralKind {
raise Evaluation("term has no effective boolean value")
}
if t.datatype == xsd_namespace() + "boolean" {
match t.lexical.trim().to_owned() {
"true" | "1" => true
"false" | "0" => false
_ => raise Evaluation("invalid boolean lexical form")
}
} else if t.datatype == xsd_namespace() + "string" || t.language is Some(_) {
t.lexical.length() != 0
} else if numeric_type(t) {
match numeric(t, digits) {
Exact(v, _, _) => !v.is_zero()
Floating(v) => !v.is_nan() && v != 0.0
}
} else {
raise Evaluation("unsupported effective boolean datatype")
}
}
///|
fn value_equal(a : Term, b : Term, digits : Int32) -> Bool raise QueryError {
if numeric_type(a) && numeric_type(b) {
numeric(a, digits).compare(numeric(b, digits)) == Some(0)
} else if a.kind != LiteralKind || b.kind != LiteralKind {
a == b
} else if a.datatype == xsd_namespace() + "boolean" &&
b.datatype == a.datatype {
ebv(a, digits) == ebv(b, digits)
} else if a.datatype == xsd_namespace() + "string" || a.language is Some(_) {
if b.datatype == xsd_namespace() + "string" || b.language is Some(_) {
a.datatype == b.datatype &&
a.language == b.language &&
a.lexical == b.lexical
} else {
raise Evaluation("incomparable literal values")
}
} else if a == b {
true
} else {
raise Evaluation("incomparable literal values")
}
}
///|
fn value_compare(a : Term, b : Term, digits : Int32) -> Int32? raise QueryError {
if numeric_type(a) && numeric_type(b) {
numeric(a, digits).compare(numeric(b, digits))
} else if a.kind == LiteralKind &&
b.kind == LiteralKind &&
a.datatype == xsd_namespace() + "string" &&
b.datatype == a.datatype {
Some(codepoint_compare(a.lexical, b.lexical))
} else if a.datatype == xsd_namespace() + "boolean" &&
b.datatype == a.datatype {
let x = ebv(a, digits)
let y = ebv(b, digits)
Some(if x == y { 0 } else if x { 1 } else { -1 })
} else {
raise Evaluation("values do not have an ordered comparison")
}
}
///|
fn Expr::maybe(self : Expr, context : EvalContext) -> Term? raise QueryError {
Some(self.eval(context)) catch {
Evaluation(_) => None
e => raise e
}
}
///|
fn Expr::truth(self : Expr, context : EvalContext) -> Bool? raise QueryError {
Some(ebv(self.eval(context), context.budget.limits.max_numeric_digits)) catch {
Evaluation(_) => None
e => raise e
}
}
///|
fn Expr::eval(self : Expr, context : EvalContext) -> Term raise QueryError {
let b = context.budget
b.tick()
match self {
Constant(t) => t
Reference(name) =>
match context.row.get(name) {
Some(t) => t
None => raise Evaluation("unbound variable " + name)
}
Unary(op, child) => {
let value = child.eval(context)
if op == "!" {
boolean(!ebv(value, b.limits.max_numeric_digits))
} else if op == "+" {
numeric(value, b.limits.max_numeric_digits).term(
b.limits.max_numeric_digits,
)
} else {
numeric_op(
"*",
numeric(value, b.limits.max_numeric_digits),
Exact(@bigint.BigInt::from_int(-1), 0, false),
b.limits.max_numeric_digits,
)
}
}
Binary(op, left, right) =>
if op == "&&" || op == "||" {
let a = left.truth(context)
if op == "&&" && a == Some(false) {
return boolean(false)
}
if op == "||" && a == Some(true) {
return boolean(true)
}
let c = right.truth(context)
if op == "&&" && c == Some(false) {
boolean(false)
} else if op == "||" && c == Some(true) {
boolean(true)
} else if a is None || c is None {
raise Evaluation("boolean operand error")
} else {
boolean(op == "&&")
}
} else {
let a = left.eval(context)
let c = right.eval(context)
if ["+", "-", "*", "/"].contains(op) {
b.tick(n=a.lexical.length() + c.lexical.length())
numeric_op(
op,
numeric(a, b.limits.max_numeric_digits),
numeric(c, b.limits.max_numeric_digits),
b.limits.max_numeric_digits,
)
} else if op == "=" || op == "!=" {
let equal = value_equal(a, c, b.limits.max_numeric_digits)
boolean(if op == "=" { equal } else { !equal })
} else {
let comparison = value_compare(a, c, b.limits.max_numeric_digits)
boolean(
match comparison {
None => false
Some(n) =>
match op {
"<" => n < 0
">" => n > 0
"<=" => n <= 0
">=" => n >= 0
_ => raise Evaluation("invalid comparison")
}
},
)
}
}
Call(name, args) => evaluate_call(name, args, context)
Aggregate(name, e, distinct) => aggregate_value(name, e, distinct, context)
}
}
///|
fn string_value(t : Term) -> String raise QueryError {
if t.kind != LiteralKind ||
(t.datatype != xsd_namespace() + "string" && t.language is None) {
raise Evaluation("string literal required")
}
t.lexical
}
///|
fn string_result(text : String, original : Term) -> Term raise QueryError {
match original.language {
Some(lang) => language_literal(text, lang)
None => literal(text)
}
}
///|
fn compatible_strings(a : Term, b : Term) -> Unit raise QueryError {
ignore(string_value(a))
ignore(string_value(b))
if b.language is Some(_) && a.language != b.language {
raise Evaluation("incompatible language tags")
}
}
///|
fn evaluate_call(
name : String,
args : Array[Expr],
ctx : EvalContext,
) -> Term raise QueryError {
let digits = ctx.budget.limits.max_numeric_digits
if name == "BOUND" {
if args.length() != 1 {
raise Evaluation("BOUND arity")
}
return match args[0] {
Reference(v) => boolean(ctx.row.get(v) is Some(_))
_ => raise Evaluation("BOUND requires variable")
}
}
if name == "IF" {
if args.length() != 3 {
raise Evaluation("IF arity")
}
return if ebv(args[0].eval(ctx), digits) {
args[1].eval(ctx)
} else {
args[2].eval(ctx)
}
}
if name == "COALESCE" {
for arg in args {
if arg.maybe(ctx) is Some(t) {
return t
}
}
raise Evaluation("COALESCE has no value")
}
if name == "IN" {
let value = args[0].eval(ctx)
let mut error = false
for i = 1; i < args.length(); i = i + 1 {
let equal = Some(value_equal(value, args[i].eval(ctx), digits)) catch {
Evaluation(_) => None
e => raise e
}
if equal == Some(true) {
return boolean(true)
}
if equal is None {
error = true
}
}
if error {
raise Evaluation("IN element error")
}
return boolean(false)
}
let values = args.map(arg => arg.eval(ctx))
if name == "CONCAT" {
let out = StringBuilder()
let common = if values.length() == 0 { None } else { values[0].language }
let mut same = true
for t in values {
out.write_string(string_value(t))
if t.language != common {
same = false
}
}
return if same && common is Some(lang) {
language_literal(out.to_string(), lang)
} else {
literal(out.to_string())
}
}
let binary = [
"SAMETERM", "CONTAINS", "STRSTARTS", "STRENDS", "LANGMATCHES", "STRDT", "STRLANG",
].contains(name)
if values.length() != (if binary { 2 } else { 1 }) {
raise Evaluation(name + " arity")
}
let a = values[0]
if binary {
let b = values[1]
return match name {
"SAMETERM" => boolean(a == b)
"STRDT" => {
if a.language is Some(_) || b.kind != IriKind {
raise Evaluation("STRDT types")
}
typed_literal(string_value(a), b.lexical)
}
"STRLANG" => language_literal(string_value(a), string_value(b))
"LANGMATCHES" => {
let lang = string_value(a).to_lower()
let range = string_value(b).to_lower()
boolean(
if range == "*" {
lang.length() > 0
} else {
lang == range || lang.has_prefix(range + "-")
},
)
}
_ => {
compatible_strings(a, b)
boolean(
match name {
"CONTAINS" => a.lexical.contains(b.lexical)
"STRSTARTS" => a.lexical.has_prefix(b.lexical)
"STRENDS" => a.lexical.has_suffix(b.lexical)
_ => raise Evaluation("unknown binary function")
},
)
}
}
}
match name {
"ISIRI" | "ISURI" => boolean(a.kind == IriKind)
"ISBLANK" => boolean(a.kind == BlankKind)
"ISLITERAL" => boolean(a.kind == LiteralKind)
"ISNUMERIC" => {
let valid = try {
ignore(numeric(a, digits))
true
} catch {
Evaluation(_) => false
e => raise e
}
boolean(valid)
}
"STR" =>
if a.kind == BlankKind {
raise Evaluation("STR of blank node")
} else {
literal(a.lexical)
}
"LANG" =>
if a.kind != LiteralKind {
raise Evaluation("LANG requires literal")
} else {
literal(a.language.unwrap_or(""))
}
"DATATYPE" =>
if a.kind != LiteralKind {
raise Evaluation("DATATYPE requires literal")
} else {
iri(a.datatype)
}
"STRLEN" => integer(string_value(a).to_array().length())
"LCASE" => string_result(string_value(a).to_lower(), a)
"UCASE" => string_result(string_value(a).to_upper(), a)
"ABS" | "CEIL" | "FLOOR" | "ROUND" =>
numeric_unary(name, numeric(a, digits)).term(digits)
_ => raise Evaluation("unknown function " + name)
}
}
///|
fn numeric_unary(name : String, value : Numeric) -> Numeric {
match value {
Floating(v) =>
Floating(
match name {
"ABS" => v.abs()
"CEIL" => v.ceil()
"FLOOR" => v.floor()
_ => (v + 0.5).floor()
},
)
Exact(c, scale, decimal) => {
if name == "ABS" {
return Exact(big_abs(c), scale, decimal)
}
let divisor = pow10(scale)
let mut q = c / divisor
let remainder = c % divisor
let negative = c < @bigint.BigInt::from_int(0)
let one = @bigint.BigInt::from_int(1)
if name == "FLOOR" && negative && !remainder.is_zero() {
q -= one
}
if name == "CEIL" && !negative && !remainder.is_zero() {
q += one
}
if name == "ROUND" {
let cmp = (big_abs(remainder) * @bigint.BigInt::from_int(2)).compare(
divisor,
)
if (!negative && cmp >= 0) || (negative && cmp > 0) {
q += if negative { -one } else { one }
}
}
Exact(q, 0, decimal)
}
}
}