// Scalar built-in application helpers: positional argument access, the // range/substring/toString value builders, and the datetime/duration // constructors with their checked integer arithmetic. Split from funcs.go, // which holds the function registry (ResolveFuncOp) and the ApplyFunc // dispatch.
package eval import ( ) // arg reads argv[i], Null when absent. func arg( []value.Value, int) value.Value { if < len() { return [] } return value.Null() } // numArg is the single numeric argument of a math function as float64 // (Int or Float), comma-ok. func numArg( []value.Value) (float64, bool) { return arg(, 0).AsFloat() } // applyRange is range(start, end[, step]): the inclusive integer sequence // (step defaults to 1; a zero step is null). func applyRange( []value.Value) value.Value { , := arg(, 0).AsInt() , := arg(, 1).AsInt() if ! || ! { return value.Null() } := int64(1) if len() >= 3 { if , := arg(, 2).AsInt(); { = } } if == 0 { return value.Null() } var []value.Value for := ; ( > 0 && <= ) || ( < 0 && >= ); += { = append(, value.Int()) } return value.List() } // asciiOnly reports whether s holds only single-byte runes, so character // indexing equals byte indexing. func asciiOnly( string) bool { for := 0; < len(); ++ { if [] >= 0x80 { return false } } return true } // applySubstring is substring(s, start[, len]): character-based, start // past the end yields ""; a null string or negative start/len is null. // An all-ASCII source -- the overwhelmingly common case -- slices the // input string directly (a Go substring shares the backing), skipping the // rune conversion and the fresh string a hot aggregate argument would // otherwise pay per row. func applySubstring( []value.Value) value.Value { , := arg(, 0).AsStr() , := arg(, 1).AsInt() if ! || ! || < 0 { return value.Null() } if asciiOnly() { := min(int(), len()) if len() >= 3 { , := arg(, 2).AsInt() if ! || < 0 { return value.Null() } := min(+int(), len()) return value.Str([:]) } return value.Str([:]) } := []rune() := min(int(), len()) if len() >= 3 { , := arg(, 2).AsInt() if ! || < 0 { return value.Null() } := min(+int(), len()) return value.Str(string([:])) } return value.Str(string([:])) } // applyToString renders a scalar as a string: an integral Float keeps a // trailing .0 (openCypher prints 1.0, not 1); inf/NaN render as the Rust // engine prints them so cross-engine goldens agree. func applyToString( value.Value) value.Value { switch .Kind() { case value.KindInt: , := .AsInt() return value.Str(strconv.FormatInt(, 10)) case value.KindFloat: , := .AsFloat() switch { case math.IsNaN(): return value.Str("NaN") case math.IsInf(, 1): return value.Str("inf") case math.IsInf(, -1): return value.Str("-inf") case == math.Trunc(): return value.Str(strconv.FormatFloat(, 'f', 1, 64)) } return value.Str(strconv.FormatFloat(, 'f', -1, 64)) case value.KindBool: , := .AsBool() return value.Str(strconv.FormatBool()) case value.KindStr: return case value.KindTemporal: , , := .AsTemporal() return value.Str(ISOString(, )) } // A duration has no single established string form here (the wasm/Python // surfaces emit the {months, days, millis} component form, not a string), // so toString(duration) stays Null rather than bake an arbitrary choice. return value.Null() } // buildDatetime builds a temporal from a datetime(...) argument: an ISO // string, an i64 epoch-millis, another temporal (kind changes), an // {epochMillis: n} map, or a {year, month, day, hour, ...} component map. func buildDatetime( value.Value, value.TemporalKind) value.Value { switch .Kind() { case value.KindStr: , := .AsStr() if , := ParseISO(); { return value.Temporal(, ) } case value.KindInt: , := .AsInt() return value.Temporal(, ) case value.KindTemporal: , , := .AsTemporal() return value.Temporal(, ) case value.KindMap: , := .AsMap() := func( string) (int64, bool) { return mapGet(, ).AsInt() } if , := ("epochMillis"); { return value.Temporal(, ) } if , := ("year"); { := func( string, int64) int64 { if , := (); { return } return } , := sumScaled( [2]int64{("hour", 0), 3_600_000}, [2]int64{("minute", 0), 60_000}, [2]int64{("second", 0), 1000}, [2]int64{("millisecond", 0), 1}) if ! { return value.Null() } , := civilMillis(, uint32(("month", 1)), uint32(("day", 1)), ) if ! { return value.Null() } return value.Temporal(, ) } } return value.Null() } // buildDuration builds a duration from a duration({years, months, weeks, // days, hours, minutes, seconds, milliseconds}) map. func buildDuration( []value.MapEntry) value.Value { := func( string) int64 { if , := mapGet(, ).AsInt(); { return } return 0 } // Unit conversions overflow at build time, before any temporal is // touched; a wrapped duration would silently corrupt every later add. , := addMul(("years"), 12, ("months")) , := addMul(("weeks"), 7, ("days")) , := sumScaled( [2]int64{("hours"), 3_600_000}, [2]int64{("minutes"), 60_000}, [2]int64{("seconds"), 1000}, [2]int64{("milliseconds"), 1}) if ! || ! || ! { return value.Null() } return value.Duration(, , ) } // addMul is checked a*k + b, comma-ok. func addMul(, , int64) (int64, bool) { , := mulChk(, ) if ! { return 0, false } return addChk(, ) } // sumScaled is the checked sum of value*scale terms, comma-ok. func sumScaled( ...[2]int64) (int64, bool) { var int64 for , := range { , := mulChk([0], [1]) if ! { return 0, false } if , = addChk(, ); ! { return 0, false } } return , true }