diff --git a/conf/config.go b/conf/config.go index 685241b0f..f7c95d203 100644 --- a/conf/config.go +++ b/conf/config.go @@ -36,6 +36,11 @@ type Config struct { Builtins FunctionsTable Disabled map[string]bool // disabled builtins NtCache nature.Cache + // DisableIfOperator disables the built-in `if ... { } else { }` operator syntax + // so that users can use a custom function named `if(...)` without conflicts. + // When enabled, the lexer treats `if`/`else` as identifiers and the parser + // will not parse `if` statements. + DisableIfOperator bool } // CreateNew creates new config with default values. diff --git a/expr.go b/expr.go index ff4ae5942..280605e67 100644 --- a/expr.go +++ b/expr.go @@ -109,6 +109,14 @@ func AsFloat64() Option { } } +// DisableIfOperator disables the `if ... else ...` operator syntax so a custom +// function named `if(...)` can be used without conflicts. +func DisableIfOperator() Option { + return func(c *conf.Config) { + c.DisableIfOperator = true + } +} + // WarnOnAny tells the compiler to warn if expression return any type. func WarnOnAny() Option { return func(c *conf.Config) { diff --git a/expr_test.go b/expr_test.go index 14715889e..ba1f001ec 100644 --- a/expr_test.go +++ b/expr_test.go @@ -70,6 +70,17 @@ func ExampleCompile() { // Output: true } +func TestDisableIfOperator_AllowsIfFunction(t *testing.T) { + env := map[string]any{ + "if": func(x int) int { return x + 1 }, + } + program, err := expr.Compile("if(41)", expr.Env(env), expr.DisableIfOperator()) + require.NoError(t, err) + out, err := expr.Run(program, env) + require.NoError(t, err) + assert.Equal(t, 42, out) +} + func ExampleEnv() { type Segment struct { Origin string diff --git a/parser/lexer/lexer.go b/parser/lexer/lexer.go index fe41e824a..5daf95037 100644 --- a/parser/lexer/lexer.go +++ b/parser/lexer/lexer.go @@ -46,6 +46,9 @@ type Lexer struct { byte, rune int } eof bool + // When true, keywords `if`/`else` are not treated as operators and + // will be emitted as identifiers instead (for compatibility with custom if()). + DisableIfOperator bool } func (l *Lexer) Reset(source file.Source) { diff --git a/parser/lexer/state.go b/parser/lexer/state.go index e5ad45bcd..91857eade 100644 --- a/parser/lexer/state.go +++ b/parser/lexer/state.go @@ -129,8 +129,14 @@ loop: switch l.word() { case "not": return not - case "in", "or", "and", "matches", "contains", "startsWith", "endsWith", "let", "if", "else": + case "in", "or", "and", "matches", "contains", "startsWith", "endsWith", "let": l.emit(Operator) + case "if", "else": + if !l.DisableIfOperator { + l.emit(Operator) + } else { + l.emit(Identifier) + } default: l.emit(Identifier) } diff --git a/parser/parser.go b/parser/parser.go index 5f3ffb362..9ccf47830 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -62,6 +62,14 @@ func (p *Parser) Parse(input string, config *conf.Config) (*Tree, error) { p.lexer = New() } p.config = config + // propagate config flags to lexer + if p.lexer != nil { + if config != nil { + p.lexer.DisableIfOperator = config.DisableIfOperator + } else { + p.lexer.DisableIfOperator = false + } + } source := file.NewSource(input) p.lexer.Reset(source) p.next() @@ -218,7 +226,7 @@ func (p *Parser) parseExpression(precedence int) Node { return p.parseVariableDeclaration() } - if precedence == 0 && p.current.Is(Operator, "if") { + if precedence == 0 && (p.config == nil || !p.config.DisableIfOperator) && p.current.Is(Operator, "if") { return p.parseConditionalIf() }