rtmleval
API
rtmleval
packageAPI reference for the rtmleval
package.
Imports
(5)
F
function
Eval
Eval evaluates an RTML expression string against a variable lookup.
lookup(name) returns the value for a variable.
Parameters
expr
string
lookup
func(string) (any, bool)
Returns
any
error
rtmleval/eval.go:15-20
func Eval(expr string, lookup func(string) (any, bool)) (any, error)
{
p := &parser{input: expr, lookup: lookup}
p.readChar()
p.next()
return p.parseTernary()
}
F
function
Bool
Bool evaluates and coerces the result to bool.
Parameters
expr
string
lookup
func(string) (any, bool)
Returns
bool
error
rtmleval/eval.go:23-29
func Bool(expr string, lookup func(string) (any, bool)) (bool, error)
{
v, err := Eval(expr, lookup)
if err != nil {
return false, err
}
return toBool(v), nil
}
F
function
String
String returns the evaluated result as string.
Parameters
expr
string
lookup
func(string) (any, bool)
Returns
string
error
rtmleval/eval.go:32-38
func String(expr string, lookup func(string) (any, bool)) (string, error)
{
v, err := Eval(expr, lookup)
if err != nil {
return "", err
}
return toString(v), nil
}
F
function
toBool
toBool coerces a Go value to bool.
Parameters
v
any
Returns
bool
rtmleval/eval.go:41-56
func toBool(v any) bool
{
switch val := v.(type) {
case bool:
return val
case int:
return val != 0
case float64:
return val != 0
case string:
return val != "" && val != "false" && val != "0"
case nil:
return false
default:
return v != nil
}
}
F
function
toFloat
toFloat coerces a Go value to float64.
Parameters
v
any
Returns
float64
bool
rtmleval/eval.go:59-71
func toFloat(v any) (float64, bool)
{
switch val := v.(type) {
case float64:
return val, true
case int:
return float64(val), true
case string:
f, err := strconv.ParseFloat(val, 64)
return f, err == nil
default:
return 0, false
}
}
F
function
toString
toString coerces a Go value to string.
Parameters
v
any
Returns
string
rtmleval/eval.go:74-82
func toString(v any) string
{
if v == nil {
return ""
}
if s, ok := v.(string); ok {
return s
}
return fmt.Sprintf("%v", v)
}
T
type
tokenType
rtmleval/eval.go:86-86
type tokenType int
S
struct
token
rtmleval/eval.go:118-121
type token struct
Fields
| Name | Type | Description |
|---|---|---|
| typ | tokenType | |
| val | string |
Uses
S
struct
parser
rtmleval/eval.go:123-129
type parser struct
Methods
readChar
Method
func (*parser) readChar()
{
if p.pos >= len(p.input) {
p.ch = 0
} else {
p.ch = p.input[p.pos]
}
p.pos++
}
peek
Method
Returns
byte
func (*parser) peek() byte
{
if p.pos >= len(p.input) {
return 0
}
return p.input[p.pos]
}
skipWhitespace
Method
func (*parser) skipWhitespace()
{
for p.ch != 0 && (p.ch == ' ' || p.ch == '\t' || p.ch == '\n' || p.ch == '\r') {
p.readChar()
}
}
next
Method
func (*parser) next()
{
p.skipWhitespace()
if p.ch == 0 {
p.cur = token{tEOF, ""}
return
}
switch p.ch {
case '(':
p.cur = token{tLParen, "("}
p.readChar()
return
case ')':
p.cur = token{tRParen, ")"}
p.readChar()
return
case '+':
p.cur = token{tPlus, "+"}
p.readChar()
return
case '-':
p.cur = token{tMinus, "-"}
p.readChar()
return
case '*':
p.cur = token{tStar, "*"}
p.readChar()
return
case '/':
p.cur = token{tSlash, "/"}
p.readChar()
return
case '.':
p.cur = token{tDot, "."}
p.readChar()
return
case ':':
p.cur = token{tColon, ":"}
p.readChar()
return
case '"':
p.readString('"')
return
case '\'':
p.readString('\'')
return
case '=':
if p.peek() == '=' {
p.readChar()
p.readChar()
p.cur = token{tEq, "=="}
return
}
// Single '=' is not an operator in our grammar; skip it.
p.readChar()
p.next()
return
case '!':
if p.peek() == '=' {
p.readChar()
p.readChar()
p.cur = token{tNeq, "!="}
return
}
p.cur = token{tNot, "!"}
p.readChar()
return
case '<':
if p.peek() == '=' {
p.readChar()
p.readChar()
p.cur = token{tLte, "<="}
return
}
p.cur = token{tLt, "<"}
p.readChar()
return
case '>':
if p.peek() == '=' {
p.readChar()
p.readChar()
p.cur = token{tGte, ">="}
return
}
p.cur = token{tGt, ">"}
p.readChar()
return
case '&':
if p.peek() == '&' {
p.readChar()
p.readChar()
p.cur = token{tAnd, "&&"}
return
}
// Single '&' is not an operator; skip it.
p.readChar()
p.next()
return
case '|':
if p.peek() == '|' {
p.readChar()
p.readChar()
p.cur = token{tOr, "||"}
return
}
// Single '|' is not an operator; skip it.
p.readChar()
p.next()
return
case '?':
p.cur = token{tQuestion, "?"}
p.readChar()
return
}
if p.ch == '"' {
p.readString('"')
return
}
if p.ch == '\'' {
p.readString('\'')
return
}
if isDigit(p.ch) || (p.ch == '.' && isDigit(p.peek())) {
p.readNumber()
return
}
if isIdentStart(p.ch) {
p.readIdent()
return
}
// Unknown character: skip and continue.
p.readChar()
p.next()
}
readString
Method
Parameters
quote
byte
func (*parser) readString(quote byte)
{
var sb strings.Builder
p.readChar()
for p.ch != quote && p.ch != 0 {
if p.ch == '\\' {
p.readChar()
switch p.ch {
case 'n':
sb.WriteByte('\n')
case 't':
sb.WriteByte('\t')
case 'r':
sb.WriteByte('\r')
case '\\':
sb.WriteByte('\\')
case '"':
sb.WriteByte('"')
case '\'':
sb.WriteByte('\'')
default:
sb.WriteByte(p.ch)
}
} else {
sb.WriteByte(p.ch)
}
p.readChar()
}
if p.ch == quote {
p.readChar()
}
p.cur = token{tString, sb.String()}
}
readNumber
Method
func (*parser) readNumber()
{
start := p.pos - 1
for isDigit(p.ch) || p.ch == '.' {
p.readChar()
}
p.cur = token{tNumber, p.input[start : p.pos-1]}
}
readIdent
Method
func (*parser) readIdent()
{
start := p.pos - 1
for isIdentPart(p.ch) || p.ch == '-' {
p.readChar()
}
val := p.input[start : p.pos-1]
// Allow qualified store:/signal:/prop: references as a single identifier so
// @if/@expr conditions can reference reactive sources (same refs @for uses).
// The ternary ':' always follows an expression/space, never one of these
// bare prefixes immediately, so this does not clash with ternary parsing.
if (val == "store" || val == "signal" || val == "prop") && p.ch == ':' {
p.readChar()
for isIdentPart(p.ch) || p.ch == '-' {
p.readChar()
}
p.cur = token{tIdent, p.input[start : p.pos-1]}
return
}
switch val {
case "true":
p.cur = token{tTrue, val}
case "false":
p.cur = token{tFalse, val}
case "and":
p.cur = token{tAnd, val}
case "or":
p.cur = token{tOr, val}
case "not":
p.cur = token{tNot, val}
case "is":
p.cur = token{tIs, val}
case "then":
p.cur = token{tThen, val}
case "else":
p.cur = token{tElse, val}
default:
p.cur = token{tIdent, val}
}
}
parseTernary
Method
parseTernary handles: cond then X else Y Also handles the symbol form: cond ? X : Y (kept for backward compat)
Returns
any
error
func (*parser) parseTernary() (any, error)
{
cond, err := p.parseOr()
if err != nil {
return nil, err
}
if p.cur.typ == tThen {
p.next()
thenVal, err := p.parseOr()
if err != nil {
return nil, err
}
if p.cur.typ != tElse {
return nil, fmt.Errorf("expected 'else' in ternary expression")
}
p.next()
elseVal, err := p.parseTernary()
if err != nil {
return nil, err
}
if toBool(cond) {
return thenVal, nil
}
return elseVal, nil
}
// Backward compat: symbol ? X : Y
if p.cur.typ == tQuestion {
p.next()
thenVal, err := p.parseOr()
if err != nil {
return nil, err
}
if p.cur.typ != tColon {
return nil, fmt.Errorf("expected ':' in ternary expression")
}
p.next()
elseVal, err := p.parseTernary()
if err != nil {
return nil, err
}
if toBool(cond) {
return thenVal, nil
}
return elseVal, nil
}
return cond, nil
}
parseOr
Method
Returns
any
error
func (*parser) parseOr() (any, error)
{
lhs, err := p.parseAnd()
if err != nil {
return nil, err
}
for p.cur.typ == tOr {
p.next()
rhs, err := p.parseAnd()
if err != nil {
return nil, err
}
lhs = toBool(lhs) || toBool(rhs)
}
return lhs, nil
}
parseAnd
Method
Returns
any
error
func (*parser) parseAnd() (any, error)
{
lhs, err := p.parseEquality()
if err != nil {
return nil, err
}
for p.cur.typ == tAnd {
p.next()
rhs, err := p.parseEquality()
if err != nil {
return nil, err
}
lhs = toBool(lhs) && toBool(rhs)
}
return lhs, nil
}
parseEquality
Method
parseEquality handles ==, !=, is, is not
Returns
any
error
func (*parser) parseEquality() (any, error)
{
lhs, err := p.parseRelational()
if err != nil {
return nil, err
}
for {
switch p.cur.typ {
case tEq:
p.next()
rhs, err := p.parseRelational()
if err != nil {
return nil, err
}
lhs = cmpEqual(lhs, rhs)
case tNeq:
p.next()
rhs, err := p.parseRelational()
if err != nil {
return nil, err
}
lhs = !cmpEqual(lhs, rhs)
case tIs:
p.next()
// Check for "is not" (negated equality)
if p.cur.typ == tNot {
p.next()
rhs, err := p.parseRelational()
if err != nil {
return nil, err
}
lhs = !cmpEqual(lhs, rhs)
} else {
rhs, err := p.parseRelational()
if err != nil {
return nil, err
}
lhs = cmpEqual(lhs, rhs)
}
default:
return lhs, nil
}
}
}
parseRelational
Method
Returns
any
error
func (*parser) parseRelational() (any, error)
{
lhs, err := p.parseAdditive()
if err != nil {
return nil, err
}
for p.cur.typ == tLt || p.cur.typ == tLte || p.cur.typ == tGt || p.cur.typ == tGte {
op := p.cur.typ
p.next()
rhs, err := p.parseAdditive()
if err != nil {
return nil, err
}
a, aok := toFloat(lhs)
b, bok := toFloat(rhs)
if !aok || !bok {
return false, nil
}
switch op {
case tLt:
lhs = a < b
case tLte:
lhs = a <= b
case tGt:
lhs = a > b
case tGte:
lhs = a >= b
}
}
return lhs, nil
}
parseAdditive
Method
Returns
any
error
func (*parser) parseAdditive() (any, error)
{
lhs, err := p.parseMultiplicative()
if err != nil {
return nil, err
}
for p.cur.typ == tPlus || p.cur.typ == tMinus {
op := p.cur.typ
p.next()
rhs, err := p.parseMultiplicative()
if err != nil {
return nil, err
}
a, aok := toFloat(lhs)
b, bok := toFloat(rhs)
if aok && bok {
if op == tPlus {
lhs = a + b
} else {
lhs = a - b
}
} else {
// String concatenation.
lhs = toString(lhs) + toString(rhs)
}
}
return lhs, nil
}
parseMultiplicative
Method
Returns
any
error
func (*parser) parseMultiplicative() (any, error)
{
lhs, err := p.parseUnary()
if err != nil {
return nil, err
}
for p.cur.typ == tStar || p.cur.typ == tSlash {
op := p.cur.typ
p.next()
rhs, err := p.parseUnary()
if err != nil {
return nil, err
}
a, aok := toFloat(lhs)
b, bok := toFloat(rhs)
if !aok || !bok {
return nil, fmt.Errorf("incompatible types for * or /")
}
if op == tStar {
lhs = a * b
} else {
lhs = a / b
}
}
return lhs, nil
}
parseUnary
Method
Returns
any
error
func (*parser) parseUnary() (any, error)
{
if p.cur.typ == tNot {
p.next()
v, err := p.parseUnary()
if err != nil {
return nil, err
}
return !toBool(v), nil
}
if p.cur.typ == tMinus {
p.next()
v, err := p.parseUnary()
if err != nil {
return nil, err
}
f, ok := toFloat(v)
if !ok {
return nil, fmt.Errorf("cannot negate non-numeric value")
}
return -f, nil
}
return p.parsePrimary()
}
parsePrimary
Method
Returns
any
error
func (*parser) parsePrimary() (any, error)
{
switch p.cur.typ {
case tString:
v := p.cur.val
p.next()
return v, nil
case tNumber:
v, err := strconv.ParseFloat(p.cur.val, 64)
if err != nil {
return nil, err
}
p.next()
return v, nil
case tTrue:
p.next()
return true, nil
case tFalse:
p.next()
return false, nil
case tIdent:
name := p.cur.val
p.next()
// Field access: ident.ident.ident
for p.cur.typ == tDot {
p.next()
if p.cur.typ != tIdent {
return nil, fmt.Errorf("expected field name after dot")
}
name += "." + p.cur.val
p.next()
}
// Variable lookup.
if p.lookup != nil {
if v, ok := p.lookup(name); ok {
return v, nil
}
}
return name, nil // fallback: return the name as a string
case tLParen:
p.next()
expr, err := p.parseTernary()
if err != nil {
return nil, err
}
if p.cur.typ != tRParen {
return nil, fmt.Errorf("expected )")
}
p.next()
return expr, nil
default:
return nil, fmt.Errorf("unexpected token %q", p.cur.val)
}
}
Fields
| Name | Type | Description |
|---|---|---|
| input | string | |
| pos | int | |
| ch | byte | |
| cur | token | |
| lookup | func(string) (any, bool) |
Uses
F
function
isIdentStart
Parameters
ch
byte
Returns
bool
rtmleval/eval.go:370-370
func isIdentStart(ch byte) bool
{ return unicode.IsLetter(rune(ch)) || ch == '_' }
F
function
isIdentPart
Parameters
ch
byte
Returns
bool
rtmleval/eval.go:371-373
func isIdentPart(ch byte) bool
{
return unicode.IsLetter(rune(ch)) || unicode.IsDigit(rune(ch)) || ch == '_' || ch == '.'
}
F
function
isDigit
Parameters
ch
byte
Returns
bool
rtmleval/eval.go:374-374
func isDigit(ch byte) bool
{ return '0' <= ch && ch <= '9' }
F
function
cmpEqual
Parameters
a
any
b
any
Returns
bool
rtmleval/eval.go:667-694
func cmpEqual(a, b any) bool
{
// Fast paths for same type.
switch av := a.(type) {
case bool:
if bv, ok := b.(bool); ok {
return av == bv
}
case string:
if bv, ok := b.(string); ok {
return av == bv
}
case float64:
if bv, ok := b.(float64); ok {
return av == bv
}
case int:
if bv, ok := b.(int); ok {
return av == bv
}
}
// Fallback: numeric comparison.
af, aok := toFloat(a)
bf, bok := toFloat(b)
if aok && bok {
return af == bf
}
return toString(a) == toString(b)
}
F
function
TestEvalOperators
Parameters
t
rtmleval/eval_test.go:7-50
func TestEvalOperators(t *testing.T)
{
lookup := func(name string) (any, bool) {
m := map[string]any{
"count": 10,
"zero": 0,
"name": "world",
}
v, ok := m[name]
return v, ok
}
tests := []struct {
expr string
want any
}{
{"count == 10", true},
{"count != 5", true},
{"count > 5", true},
{"count < 20", true},
{"count >= 10", true},
{"count <= 10", true},
{"count > 5 && count < 20", true},
{"count > 5 || count < 3", true},
{"!false", true},
{"!(count == 5)", true},
{"\"hello\" + \" \" + \"world\"", "hello world"},
{"zero == 0", true},
{"'hello' + ' ' + 'world'", "hello world"},
{"name is 'world'", true},
{"name is not 'world'", false},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
got, err := Eval(tt.expr, lookup)
if err != nil {
t.Fatalf("eval error: %v", err)
}
if got != tt.want {
t.Errorf("eval(%q) = %v, want %v", tt.expr, got, tt.want)
}
})
}
}