Summary
NewCachingCompiler can change the meaning of valid CEL string literals. It lifts the raw source text between quotes into vars, while CEL normally decodes escapes when parsing the literal. Once the rewritten expression references vars.a, that value bypasses CEL string decoding.
As a result, direct CEL evaluation and cached/lifted evaluation of the same expression can return different results.
Reproduction
func TestCachingCompile_StringEscapeSemantics(t *testing.T) {
env := newEnv()
expression := `event.value.matches("^prefix\\/suffix$")`
input := map[string]any{
"event": map[string]any{"value": "prefix/suffix"},
}
directAST, directIssues := env.Compile(expression)
require.Nil(t, directIssues)
directProgram, err := env.Program(directAST)
require.NoError(t, err)
direct, _, err := directProgram.Eval(input)
require.NoError(t, err)
require.Equal(t, true, direct.Value())
cachedAST, cachedIssues, vars := NewCachingCompiler(env, nil).Compile(expression)
require.Nil(t, cachedIssues)
cachedProgram, err := env.Program(cachedAST)
require.NoError(t, err)
input[VarPrefix] = vars.Map()
cached, _, err := cachedProgram.Eval(input)
require.NoError(t, err)
require.Equal(t, direct.Value(), cached.Value())
}
On current main (6e2f3dcb86677f1b54fbda7eb8cb80f8f224d012), the direct result is true and the cached result is false.
Root cause
consumeString skips over escape pairs while scanning, but argMapValue.get later returns the original source slice unchanged. The rewritten AST references vars.a, so cel-go never parses that value as a literal and never applies CEL unescaping.
The scanner change in #46 prevents incorrect string termination, but it intentionally keeps the raw source bytes and therefore does not preserve literal semantics.
Suggested fix
Do not lift string literals that contain escape sequences. Leaving those literals in the rewritten expression lets cel-go remain the source of truth for CEL unescaping and validation, while ordinary unescaped literals can still be lifted and cached as they are today.
Please add parity coverage for escaped backslashes, quotes, newlines, and Unicode escapes.
Summary
NewCachingCompilercan change the meaning of valid CEL string literals. It lifts the raw source text between quotes intovars, while CEL normally decodes escapes when parsing the literal. Once the rewritten expression referencesvars.a, that value bypasses CEL string decoding.As a result, direct CEL evaluation and cached/lifted evaluation of the same expression can return different results.
Reproduction
On current
main(6e2f3dcb86677f1b54fbda7eb8cb80f8f224d012), the direct result istrueand the cached result isfalse.Root cause
consumeStringskips over escape pairs while scanning, butargMapValue.getlater returns the original source slice unchanged. The rewritten AST referencesvars.a, so cel-go never parses that value as a literal and never applies CEL unescaping.The scanner change in #46 prevents incorrect string termination, but it intentionally keeps the raw source bytes and therefore does not preserve literal semantics.
Suggested fix
Do not lift string literals that contain escape sequences. Leaving those literals in the rewritten expression lets cel-go remain the source of truth for CEL unescaping and validation, while ordinary unescaped literals can still be lifted and cached as they are today.
Please add parity coverage for escaped backslashes, quotes, newlines, and Unicode escapes.