feat(cel): preserve native values - #173
Conversation
Add concurrency-safe native type registration so CEL preserves registered Go values and JSON field mappings. Upgrade cel-go to v0.31, support optimized execution decorators, and enforce a 10,000-instruction regex limit. Rework benchmark CI to interleave base/head samples and report metric-level CSV regressions with tests. Document the new CEL behavior and upcoming collection helpers. Claude-Session-Id: 019fef64-038b-7471-9962-6019a7d08109
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
BenchstatBase: 4 minor regression(s) (all within 5% threshold)
13 improvement(s)
Full benchstat output |
WalkthroughThe PR upgrades CEL integration to cel-go v0.31.0, adds native type registration and cached expression execution, updates nil-safe evaluation, and adds regex limits. It also updates benchmark execution, CSV reporting, benchmark coverage, documentation, and future function specifications. ChangesCEL runtime integration
Benchmark reporting and measurement
CEL function requirements
Sequence Diagram(s)sequenceDiagram
participant Caller
participant RegisterType
participant RunExpression
participant CELProgram
Caller->>RegisterType: register native Go or CEL type
RegisterType-->>RunExpression: publish generation-tracked environment
Caller->>RunExpression: evaluate template with environment
RunExpression->>CELProgram: compile or load cached program
CELProgram-->>RunExpression: return evaluation result
RunExpression-->>Caller: return native value
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (2)
specs/REQUIREMENTS-coalesce.md (1)
173-185: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExpand the CEL test matrix.
The acceptance criteria include empty lists and maps,
false, optional inner empty values, mixed optional/plain values, and 1–5+ arities. The current table does not cover all of these cases.Add tests for those boundaries, including a six-argument case after resolving the variadic design. This prevents the CEL binding from diverging from the existing Go helper.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@specs/REQUIREMENTS-coalesce.md` around lines 173 - 185, Expand the CEL integration test table in the requirements around tests/cel_test.go to cover empty lists and maps, false, optional values containing empty inner values, mixed optional/plain arguments, and arities from one through five or more. After the variadic design is resolved, add a six-argument case, preserving expected coalescing behavior consistent with the existing Go helper.specs/REQUIREMENTS-first-last.md (1)
174-207: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winComplete the acceptance-test matrix.
The unit table only uses
[]any{}for an empty list, so it cannot validate the typed zero values in lines 137-140. The CEL table omitslast(null), typed-null string/map cases,x.missing.first(), member calls for string and map inputs, and gomplate invocations. Add these cases before marking the success criteria complete.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@specs/REQUIREMENTS-first-last.md` around lines 174 - 207, Expand the acceptance-test matrices in the requirements document to cover typed zero-value inputs from the implementation, including typed-null strings and maps, plus last(null). Add CEL cases for x.missing.first(), string and map member-call forms, and the supported gomplate invocations, preserving the documented expected outputs for each case.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/benchmark.yml:
- Around line 77-80: Update the benchmark file-copy commands near the existing
serialize_bench_test.go and run_expression_bench_test.go overrides to also copy
cel_v031_bench_test.go into .tmp/bench/base, ensuring the base worktree includes
the benchmarks selected by the filter.
In `@cel_expression.go`:
- Around line 120-126: Update the registered function binding in the surrounding
CEL environment setup to use that environment’s type adapter, including the
adapter configured by nativeTypes.envOption, instead of types.DefaultTypeAdapter
when converting function() results. Add a regression test where a registered
function returns a native person struct and person().display_name evaluates
successfully.
In `@cel_tracker_test.go`:
- Around line 83-85: Add a separate tracked evaluation near the existing
RunExpressionContext call in the test, using the expression
optional.none().orValue("") == "" and the same tracked context setup, then
assert it completes without error and returns true. Keep the existing non-empty
optional evaluation unchanged.
In `@specs/REQUIREMENTS-coalesce.md`:
- Around line 32-33: Remove the trailing space inside the inline code span in
the User Story text, changing the referenced null-coalescing operator from `?? `
to `??` while preserving the surrounding wording.
- Around line 5-12: Update the coalesce requirements to acknowledge and reuse
the existing coll.Coalesce helper instead of proposing a new primitive or
reflection-based implementation. Revise the Phase 1 checklist and affected
sections to verify or extend coll.Coalesce as needed, and specify that the
gomplate binding delegates to this helper while preserving its existing
contract.
- Around line 21-40: Define the zero-argument behavior for coalesce consistently
across the Go helper, gomplate binding, and CEL binding: either reject calls
with no arguments or explicitly return nil, then document that choice in FR-1.
Update the relevant coalesce symbols and add coverage for zero-argument calls in
both bindings while preserving the existing one-or-more-argument behavior.
- Around line 93-99: Clarify the CEL contract in the “CEL variadic constraint”
section by explicitly defining behavior for calls with more than five arguments.
Preserve coalesce([a, b]) as a valid one-argument list call, and either
document/register an explicit list form or function name, or state and enforce a
supported maximum. Add tests covering the selected six-argument boundary and the
one-argument list behavior.
In `@specs/REQUIREMENTS-first-last.md`:
- Around line 71-80: Update FR-4 to explicitly constrain map support to
string-keyed maps, matching the proposed map<string,dyn> CEL overload and
map[string]any Go implementation. Clarify that lexicographic ordering applies to
string keys and retain the existing empty-map behavior.
- Around line 3-9: Clarify the requirements tables and Go/CEL sections by
separating semantic return values from rendered output: specify null or the
appropriate typed zero for null, empty, and out-of-range inputs, while labeling
empty-string entries as rendered output only where applicable. Update the
affected sections around the binding descriptions and CEL matrix, and ensure the
stated behavior consistently preserves the null contract across CEL, Go, and
gomplate bindings.
- Around line 57-66: Update FR-3 to define first/last string behavior in terms
of Unicode code points, including expected non-ASCII outputs and empty-string
results. Implement matching code-point semantics in both bindings: preserve the
Go binding’s []rune behavior and extend the CEL binding’s first/last handling
beyond lists and maps to support strings. Add global and member CEL tests
covering non-ASCII code points.
- Around line 120-145: Update the zero-value strategy in the requirements and
related tests to define empty or null lists, including typed lists accepted by
cel.ListType(cel.DynType), as returning types.NullValue through celFirst and
celLast. Remove conflicting type-specific list zero-value expectations, while
retaining rendered-template expectations of "" only when they represent rendered
CEL null.
---
Nitpick comments:
In `@specs/REQUIREMENTS-coalesce.md`:
- Around line 173-185: Expand the CEL integration test table in the requirements
around tests/cel_test.go to cover empty lists and maps, false, optional values
containing empty inner values, mixed optional/plain arguments, and arities from
one through five or more. After the variadic design is resolved, add a
six-argument case, preserving expected coalescing behavior consistent with the
existing Go helper.
In `@specs/REQUIREMENTS-first-last.md`:
- Around line 174-207: Expand the acceptance-test matrices in the requirements
document to cover typed zero-value inputs from the implementation, including
typed-null strings and maps, plus last(null). Add CEL cases for
x.missing.first(), string and map member-call forms, and the supported gomplate
invocations, preserving the documented expected outputs for each case.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 3e9d7d25-c6cb-4b3c-9cf1-b570a32c40dd
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (22)
.github/scripts/benchstat-summary.py.github/scripts/benchstat-summary_test.py.github/workflows/benchmark.yml.gitignoreCEL.mdREADME.mdcel.gocel_expression.gocel_native.gocel_native_test.gocel_tracker_test.gocel_v031_bench_test.gogo.modnilsafe/nilsafe.gonilsafe/zeroval.gorun_expression_bench_test.goserialize.goserialize_bench_test.gospecs/REQUIREMENTS-coalesce.mdspecs/REQUIREMENTS-first-last.mdtemplate.gotemplate_test.go
| # Override the base's bench files with HEAD's so both runs execute | ||
| # the same benchmark suite — only the implementation under test differs. | ||
| cp serialize_bench_test.go .bench/base/serialize_bench_test.go | ||
| cp run_expression_bench_test.go .bench/base/run_expression_bench_test.go | ||
| - name: Benchmark base | ||
| cp serialize_bench_test.go .tmp/bench/base/serialize_bench_test.go | ||
| cp run_expression_bench_test.go .tmp/bench/base/run_expression_bench_test.go |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
fd '^cel_v031_bench_test\.go$' . -t f -x \
rg -n -P '^\s*func\s+Benchmark(?:CELEnvExtend|CELProgramEvaluation)\b' {}
rg -n -C2 'cp .*_bench_test\.go|Benchmark\(Serialize\|RunExpressionContext\|CELEnvExtend\|CELProgramEvaluation' \
.github/workflows/benchmark.ymlRepository: flanksource/gomplate
Length of output: 1288
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- benchmark source files ---'
fd -t f '.*_bench_test\.go$' . | sort
printf '%s\n' '--- CEL benchmark definition ---'
cel_file="$(fd -t f '^cel_v031_bench_test\.go$' . | head -n1)"
if [ -n "$cel_file" ]; then
cat -n "$cel_file" | sed -n '1,90p'
fi
printf '%s\n' '--- workflow setup and benchmark commands ---'
cat -n .github/workflows/benchmark.yml | sed -n '45,110p'
printf '%s\n' '--- references to CEL benchmark source ---'
rg -n -C3 'cel_v031_bench_test\.go|BenchmarkCELEnvExtend|BenchmarkCELProgramEvaluation' .Repository: flanksource/gomplate
Length of output: 8794
Copy cel_v031_bench_test.go into the base worktree.
cel_v031_bench_test.go defines BenchmarkCELProgramEvaluation and BenchmarkCELEnvExtendCustomFunction. The benchmark filter selects both, but the workflow does not copy this file. The base binary omits these benchmarks, so benchstat cannot compare them or detect regressions.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/benchmark.yml around lines 77 - 80, Update the benchmark
file-copy commands near the existing serialize_bench_test.go and
run_expression_bench_test.go overrides to also copy cel_v031_bench_test.go into
.tmp/bench/base, ensuring the base worktree includes the benchmarks selected by
the filter.
| cel.FunctionBinding(func(_ ...ref.Val) ref.Val { | ||
| function, ok := registeredFunction.(func() any) | ||
| if !ok { | ||
| return types.WrapErr(fmt.Errorf("%s is expected to be of type func() any", functionName)) | ||
| } | ||
| return types.DefaultTypeAdapter.NativeToValue(function()) | ||
| }), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
fd -t f 'cel_expression\.go$|go\.mod$|.*_test\.go$' . | head -80
printf '%s\n' '--- cel_expression.go locations ---'
cel_file="$(fd -t f 'cel_expression\.go$' . | head -1)"
if [ -n "${cel_file}" ]; then
wc -l "$cel_file"
cat -n "$cel_file" | sed -n '1,190p'
fi
printf '%s\n' '--- nativeTypes and function binding references ---'
rg -n -C 4 'nativeTypes|registeredFunction|FunctionBinding|Template\.Functions|RunExpressionContext|DefaultTypeAdapter' .Repository: flanksource/gomplate
Length of output: 50377
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- CEL and native-type definitions ---'
rg -n -C 6 'type nativeType|type nativeTypeSnapshot|func currentNativeTypes|func RegisterType|envOption|Template struct|Functions map' \
--glob '*.go' .
printf '%s\n' '--- existing native-type tests ---'
cat -n cel_native_test.go | sed -n '1,260p'
printf '%s\n' '--- existing function tests and Template definitions ---'
rg -n -C 8 'Functions:|Template\{|func\(\) any|display_name|RegisterType' \
--glob '*_test.go' --glob '*.go' . | head -500
printf '%s\n' '--- dependency versions ---'
rg -n 'cel-go|go ' go.mod go.sumRepository: flanksource/gomplate
Length of output: 42574
🏁 Script executed:
#!/bin/bash
set -eu
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL \
https://raw.githubusercontent.com/google/cel-go/v0.31.0/common/types/provider.go \
-o "$tmpdir/provider.go"
curl -fsSL \
https://raw.githubusercontent.com/google/cel-go/v0.31.0/ext/native_types.go \
-o "$tmpdir/native_types.go"
curl -fsSL \
https://raw.githubusercontent.com/google/cel-go/v0.31.0/cel/env.go \
-o "$tmpdir/env.go"
printf '%s\n' '--- adapter and native-type provider definitions ---'
rg -n -C 8 'DefaultTypeAdapter|NativeToValue|NativeTypes|TypeAdapter|Provider' "$tmpdir"
printf '%s\n' '--- relevant cel-go source excerpts ---'
sed -n '1,240p' "$tmpdir/provider.go"
sed -n '1,180p' "$tmpdir/native_types.go"
rg -n -C 12 'func \(.*Env.*\)|TypeAdapter|adapter|provider' "$tmpdir/env.go"Repository: flanksource/gomplate
Length of output: 209
🏁 Script executed:
#!/bin/bash
set -eu
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
base='https://raw.githubusercontent.com/google/cel-go/v0.31.0'
for path in \
common/types/provider.go \
common/types/provider_test.go \
ext/native_types.go \
cel/env.go \
common/types/adapter.go; do
url="$base/$path"
status="$(curl -sS -o "$tmpdir/$(basename "$path")" -w '%{http_code}' "$url")"
printf '%s %s\n' "$status" "$path"
done
printf '%s\n' '--- available provider-related source ---'
rg -n -C 8 'DefaultTypeAdapter|NativeToValue|NativeTypes|TypeAdapter|Provider|adapter' "$tmpdir" || trueRepository: flanksource/gomplate
Length of output: 50377
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- native registration and environment wiring ---'
cat -n cel_native.go | sed -n '1,150p'
cat -n cel.go | sed -n '45,95p'
printf '%s\n' '--- cel-go v0.31.0 NativeToValue decision paths ---'
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL https://raw.githubusercontent.com/google/cel-go/v0.31.0/common/types/provider.go \
-o "$tmpdir/provider.go"
sed -n '560,590p' "$tmpdir/provider.go"
sed -n '730,780p' "$tmpdir/provider.go"
sed -n '898,912p' "$tmpdir/provider.go"
printf '%s\n' '--- environment adapter access and extension behavior ---'
curl -fsSL https://raw.githubusercontent.com/google/cel-go/v0.31.0/cel/env.go \
-o "$tmpdir/env.go"
sed -n '527,590p' "$tmpdir/env.go"
sed -n '775,800p' "$tmpdir/env.go"
printf '%s\n' '--- available Go module cache ---'
go env GOMODCACHE GOPATH 2>/dev/null || true
find "$(go env GOMODCACHE 2>/dev/null || printf /nonexistent)" \
-path '*google*cel-go*v0.31.0*' -type f -name 'provider.go' -print 2>/dev/null | headRepository: flanksource/gomplate
Length of output: 14170
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- standalone CEL behavior probe ---'
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
cat >"$tmpdir/go.mod" <<'EOF'
module cel-adapter-probe
go 1.26
require github.com/google/cel-go v0.31.0
EOF
cat >"$tmpdir/main.go" <<'EOF'
package main
import (
"fmt"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/ext"
)
type person struct {
DisplayName string `json:"display_name"`
}
func evaluate(adapter ref.TypeAdapter) (any, error) {
env, err := cel.NewEnv(
ext.NativeTypes(ext.ParseStructField(func(field reflect.StructField) string {
return field.Tag.Get("json")
}), person{}),
cel.Function("person", cel.Overload(
"person",
nil,
cel.AnyType,
cel.FunctionBinding(func(...ref.Val) ref.Val {
return adapter.NativeToValue(person{DisplayName: "Ada"})
}),
)),
)
if err != nil {
return nil, err
}
ast, issues := env.Compile(`person().display_name`)
if issues != nil && issues.Err() != nil {
return nil, issues.Err()
}
out, _, err := env.Program(ast).Eval(cel.NoVars())
if err != nil {
return nil, err
}
return out.Value(), nil
}
func main() {
_ = types.DefaultTypeAdapter
fmt.Println("probe requires the environment adapter to be captured after env construction")
}
EOF
# Show the exact ext.NativeTypes signature before compiling the focused probe.
rg -n -C 8 'func NativeTypes|ParseStructField' \
"$(go env GOMODCACHE)/github.com/google/cel-go@v0.31.0/ext" \
"$(go env GOMODCACHE)/github.com/google/cel-go@v0.31.0/common/types"Repository: flanksource/gomplate
Length of output: 15951
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- standalone CEL adapter behavior probe ---'
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
cat >"$tmpdir/go.mod" <<'EOF'
module cel-adapter-probe
go 1.26
require github.com/google/cel-go v0.31.0
EOF
cat >"$tmpdir/main.go" <<'EOF'
package main
import (
"fmt"
"reflect"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/ext"
)
type person struct {
DisplayName string `json:"display_name"`
}
func evaluate(useEnvironmentAdapter bool) (any, error) {
var adapter ref.TypeAdapter = types.DefaultTypeAdapter
env, err := cel.NewEnv(
ext.NativeTypes(
ext.ParseStructField(func(field reflect.StructField) string {
return field.Tag.Get("json")
}),
person{},
),
cel.Function("person", cel.Overload(
"person",
nil,
cel.AnyType,
cel.FunctionBinding(func(...ref.Val) ref.Val {
return adapter.NativeToValue(person{DisplayName: "Ada"})
}),
)),
)
if err != nil {
return nil, err
}
if useEnvironmentAdapter {
adapter = env.TypeAdapter()
}
ast, issues := env.Compile(`person().display_name`)
if issues != nil && issues.Err() != nil {
return nil, issues.Err()
}
out, _, err := env.Program(ast).Eval(cel.NoVars())
if err != nil {
return nil, err
}
return out.Value(), nil
}
func main() {
for _, useEnvironmentAdapter := range []bool{false, true} {
value, err := evaluate(useEnvironmentAdapter)
fmt.Printf("environment_adapter=%t value=%#v error=%v\n",
useEnvironmentAdapter, value, err)
}
}
EOF
(
cd "$tmpdir"
GOPROXY=off GOSUMDB=off go run .
)Repository: flanksource/gomplate
Length of output: 863
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- standalone CEL adapter behavior probe using repository dependency pins ---'
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
cat >"$tmpdir/main.go" <<'EOF'
package main
import (
"fmt"
"reflect"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/ext"
)
type person struct {
DisplayName string `json:"display_name"`
}
func evaluate(useEnvironmentAdapter bool) (any, error) {
var adapter ref.TypeAdapter = types.DefaultTypeAdapter
env, err := cel.NewEnv(
ext.NativeTypes(
ext.ParseStructField(func(field reflect.StructField) string {
return field.Tag.Get("json")
}),
person{},
),
cel.Function("person", cel.Overload(
"person",
nil,
cel.AnyType,
cel.FunctionBinding(func(...ref.Val) ref.Val {
return adapter.NativeToValue(person{DisplayName: "Ada"})
}),
)),
)
if err != nil {
return nil, err
}
if useEnvironmentAdapter {
adapter = env.TypeAdapter()
}
ast, issues := env.Compile(`person().display_name`)
if issues != nil && issues.Err() != nil {
return nil, issues.Err()
}
out, _, err := env.Program(ast).Eval(cel.NoVars())
if err != nil {
return nil, err
}
return out.Value(), nil
}
func main() {
for _, useEnvironmentAdapter := range []bool{false, true} {
value, err := evaluate(useEnvironmentAdapter)
fmt.Printf("environment_adapter=%t value=%#v error=%v\n",
useEnvironmentAdapter, value, err)
}
}
EOF
go run "$tmpdir/main.go"Repository: flanksource/gomplate
Length of output: 390
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- standalone CEL adapter behavior probe ---'
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
cat >"$tmpdir/main.go" <<'EOF'
package main
import (
"fmt"
"reflect"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/ext"
)
type person struct {
DisplayName string `json:"display_name"`
}
func evaluate(useEnvironmentAdapter bool) (any, error) {
var adapter ref.TypeAdapter = types.DefaultTypeAdapter
env, err := cel.NewEnv(
ext.NativeTypes(
ext.ParseStructField(func(field reflect.StructField) string {
return field.Tag.Get("json")
}),
person{},
),
cel.Function("person", cel.Overload(
"person",
nil,
cel.AnyType,
cel.FunctionBinding(func(...ref.Val) ref.Val {
return adapter.NativeToValue(person{DisplayName: "Ada"})
}),
)),
)
if err != nil {
return nil, err
}
if useEnvironmentAdapter {
adapter = env.TypeAdapter()
}
ast, issues := env.Compile(`person().display_name`)
if issues != nil && issues.Err() != nil {
return nil, issues.Err()
}
program, err := env.Program(ast)
if err != nil {
return nil, err
}
out, _, err := program.Eval(cel.NoVars())
if err != nil {
return nil, err
}
return out.Value(), nil
}
func main() {
for _, useEnvironmentAdapter := range []bool{false, true} {
value, err := evaluate(useEnvironmentAdapter)
fmt.Printf("environment_adapter=%t value=%#v error=%v\n",
useEnvironmentAdapter, value, err)
}
}
EOF
go run "$tmpdir/main.go"Repository: flanksource/gomplate
Length of output: 437
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- standalone CEL adapter behavior probe with reflect.Type registration ---'
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
cat >"$tmpdir/main.go" <<'EOF'
package main
import (
"fmt"
"reflect"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/ext"
)
type person struct {
DisplayName string `json:"display_name"`
}
func evaluate(useEnvironmentAdapter bool) (any, error) {
var adapter ref.TypeAdapter = types.DefaultTypeAdapter
env, err := cel.NewEnv(
ext.NativeTypes(
ext.ParseStructField(func(field reflect.StructField) string {
return field.Tag.Get("json")
}),
reflect.TypeOf(person{}),
),
cel.Function("person", cel.Overload(
"person",
nil,
cel.AnyType,
cel.FunctionBinding(func(...ref.Val) ref.Val {
return adapter.NativeToValue(person{DisplayName: "Ada"})
}),
)),
)
if err != nil {
return nil, err
}
if useEnvironmentAdapter {
adapter = env.TypeAdapter()
}
ast, issues := env.Compile(`person().display_name`)
if issues != nil && issues.Err() != nil {
return nil, issues.Err()
}
program, err := env.Program(ast)
if err != nil {
return nil, err
}
out, _, err := program.Eval(cel.NoVars())
if err != nil {
return nil, err
}
return out.Value(), nil
}
func main() {
for _, useEnvironmentAdapter := range []bool{false, true} {
value, err := evaluate(useEnvironmentAdapter)
fmt.Printf("environment_adapter=%t value=%#v error=%v\n",
useEnvironmentAdapter, value, err)
}
}
EOF
go run "$tmpdir/main.go"Repository: flanksource/gomplate
Length of output: 382
Use the environment type adapter for function results.
types.DefaultTypeAdapter cannot adapt registered native structs. A function that returns a registered person makes person().display_name fail with unsupported conversion to ref.Val. Use the adapter from the environment that includes nativeTypes.envOption. Add a regression test for this case.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cel_expression.go` around lines 120 - 126, Update the registered function
binding in the surrounding CEL environment setup to use that environment’s type
adapter, including the adapter configured by nativeTypes.envOption, instead of
types.DefaultTypeAdapter when converting function() results. Add a regression
test where a registered function returns a native person struct and
person().display_name evaluates successfully.
| expression := `([1, 2] + [3, 4]).size() == 4 && optional.of(name).orValue("") == "Ada" && name.matches("^A.*")` | ||
|
|
||
| result, err := RunExpressionContext(newTrackedContext(tracker), map[string]any{"name": "Ada"}, Template{Expression: expression}) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- tracked test context ---'
fd -t f 'cel_tracker_test.go|go.mod|go.sum' .
printf '%s\n' '--- relevant test lines ---'
sed -n '1,130p' cel_tracker_test.go
printf '%s\n' '--- CEL optional usage and dependency declarations ---'
rg -n -C 3 'optional\.|orValue|cel-go|google\.api\.expr\.runtime\.v1\.Optional|Optional' . -g '*.go' -g 'go.mod' -g 'go.sum'Repository: flanksource/gomplate
Length of output: 33904
🌐 Web query:
cel-go v0.31.0 optional.of empty optional orValue syntax
💡 Result:
In cel-go (including v0.31.0), the syntax for handling optional values requires enabling the OptionalTypes library via cel.OptionalTypes(true) in your environment configuration [1][2]. To address your query regarding syntax for "empty" or default values, there is no single syntax; instead, the CEL library provides specific functions and methods to handle absent or empty optional values: 1. Handling Optional Absence: Use optional.none to represent an empty optional value [3][4]. For example: optional.none 2. Providing a Default Value (orValue): The.orValue method is used to extract the contained value if present, or provide a default fallback if the optional is empty [5][4]. Syntax: <optional(T)>.orValue(defaultValue) Example: [].first.orValue('default_value') // returns 'default_value' 3. Creating Optional Values: - optional.of(value): Wraps a value in an optional [3][4]. - optional.ofNonZeroValue(value): A helper that creates an optional.none if the input is a zero value (e.g., empty string, 0, null, or empty map/list), otherwise creates an optional of the value [3][4]. For context, optional field selection (e.g., msg.?field) is supported when using the appropriate CEL configurations, allowing you to interact with protobuf messages or map keys that may not be populated [6]. Top Results: [3][5][4]
Citations:
- 1: Question: How should callers check for unpopulated root variables cel-expr/cel-go#970
- 2: Library for creating and working with optional values. cel-expr/cel-go#581
- 3: https://github.com/cel-expr/cel-spec/wiki/proposal-246
- 4: https://github.com/NVIDIA/aicr/blob/main/vendor/github.com/google/cel-go/cel/library.go
- 5: https://github.com/google/cel-go/blob/master/ext/README.md
- 6: Parse and type-check support for optional field selection cel-expr/cel-go#601
🏁 Script executed:
#!/bin/bash
set -eu
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
base='https://raw.githubusercontent.com/google/cel-go/v0.31.0'
for file in common/decls/decls.go cel/options.go ext/README.md; do
curl -fsSL "$base/$file" -o "$tmpdir/$(basename "$file")" || true
done
printf '%s\n' '--- v0.31.0 optional declarations and implementations ---'
rg -n -C 5 'optional\.none|optional\.of|orValue|OptionalTypes|OptionalTypesVersion' "$tmpdir" || true
printf '%s\n' '--- project optional environment configuration ---'
sed -n '20,48p' cel.go
printf '%s\n' '--- expression execution entry points ---'
rg -n -A35 -B8 'func RunExpression(Context)?|RunExpressionContext' --glob '*.go' .Repository: flanksource/gomplate
Length of output: 28550
🌐 Web query:
site:github.com/google/cel-go v0.31.0 "optional.none()" "orValue"
💡 Result:
In Common Expression Language (CEL), optional.none() and orValue() are part of the optional types library, which is used to safely handle potentially missing values [1][2]. optional.none() represents an empty optional value, signifying that a value is absent [3][1]. Conversely, orValue(default) is a method available on optional types that returns the contained value if it is present; if the optional is empty (i.e., optional.none()), it returns the provided default value [3][1]. To use these features, you must enable optional types in your environment configuration using cel.OptionalTypes() [1][2]. Example usage: // If status is not present, orValue returns "default_value" order.status.orValue("default_value") If order.status is absent (represented as optional.none()), the expression above evaluates to "default_value". If it is present, it evaluates to the actual status value [3][1]. Regarding version v0.31.0 specifically, it is a version of the cel-go library. While specific release notes for v0.31.0 were not surfaced in the search, the optional library functionality, including orValue, has been a stable part of cel-go since its introduction (e.g., in v0.12.0 and refined in subsequent versions) [2]. Development of these features continued through various releases, such as improvements to optional.or and orValue argument handling in v0.28.0 [4][5].
Citations:
- 1: [Question] Expression functions isEmpty, isNotEmpty, notIn cel-expr/cel-go#834
- 2: Library for creating and working with optional values. cel-expr/cel-go#581
- 3: https://github.com/google/cel-go/blob/master/ext/README.md
- 4: https://github.com/google/cel-go/releases/tag/v0.28.0
- 5: https://github.com/google/cel-go/releases
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- available cel-go source ---'
go env GOMODCACHE 2>/dev/null || true
find /go/pkg/mod "$HOME/go/pkg/mod" -path '*google*cel-go*0.31.0*' -print 2>/dev/null | head -20 || true
printf '%s\n' '--- v0.31.0 repository files related to optional types ---'
curl -fsSL 'https://api.github.com/repos/google/cel-go/git/trees/v0.31.0?recursive=1' |
jq -r '.tree[] | select(.path | test("optional|library|README"; "i")) | .path' |
head -80Repository: flanksource/gomplate
Length of output: 2194
🌐 Web query:
github google cel-go v0.31.0 optional.go optional.none source
💡 Result:
In the google/cel-go library, optional.none is a function used within Common Expression Language (CEL) expressions to represent an empty or non-existent value when the optional types feature is enabled [1][2]. Key details regarding this functionality include: 1. Purpose and Usage: The optional.none() function returns an empty optional value [2]. It is often used alongside optional.of(value) to construct optional types, or returned when an optional field or index is unpopulated [3][2]. 2. Enabling Optional Types: To use optional.none() and other related optional features within your CEL expressions, you must enable optional types in the Go environment configuration by passing cel.OptionalTypes(true) to the cel.NewEnv() call [1][3]. 3. Source File: The implementation of optional values, including the core logic for optional.none, is located in the common/types/optional.go file within the repository [4][5]. 4. Library Integration: Within the CEL library (typically found in cel/library.go), optional.none is registered as a standard library function, allowing it to be called directly in CEL scripts as optional.none() [2]. As of August 2026, please note that the repository has moved from github.com/google/cel-go to github.com/cel-expr/cel-go [6]. Users should ensure their dependencies and links are updated to reflect this change [7][6].
Citations:
- 1: Library for creating and working with optional values. cel-expr/cel-go#581
- 2: https://github.com/NVIDIA/aicr/blob/main/vendor/github.com/google/cel-go/cel/library.go
- 3: Question: How should callers check for unpopulated root variables cel-expr/cel-go#970
- 4: https://github.com/google/cel-go/tree/master/common/types
- 5: cel-expr/cel-go@9c99fa7
- 6: https://github.com/google/cel-go?tab=readme-ov-file
- 7: https://pkg.go.dev/github.com/google/cel-go/cel
🏁 Script executed:
#!/bin/bash
set -eu
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
cat >"$tmpdir/go.mod" <<'EOF'
module cel-optional-probe
go 1.23
require github.com/google/cel-go v0.31.0
EOF
cat >"$tmpdir/main.go" <<'EOF'
package main
import (
"fmt"
"github.com/google/cel-go/cel"
)
func main() {
env, err := cel.NewEnv(
cel.OptionalTypes(cel.OptionalTypesVersion(1)),
)
if err != nil {
panic(err)
}
ast, issues := env.Compile(`optional.none().orValue("") == ""`)
if issues.Err() != nil {
panic(issues.Err())
}
program, err := env.Program(ast)
if err != nil {
panic(err)
}
value, _, err := program.Eval(nil)
if err != nil {
panic(err)
}
fmt.Printf("value=%v type=%T\n", value, value)
}
EOF
(
cd "$tmpdir"
GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off go run .
)Repository: flanksource/gomplate
Length of output: 319
🏁 Script executed:
#!/bin/bash
set -eu
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
cat >"$tmpdir/go.mod" <<'EOF'
module cel-optional-probe
go 1.23
require github.com/google/cel-go v0.31.0
EOF
cp go.sum "$tmpdir/go.sum"
cat >"$tmpdir/main.go" <<'EOF'
package main
import (
"fmt"
"github.com/google/cel-go/cel"
)
func main() {
env, err := cel.NewEnv(cel.OptionalTypes(cel.OptionalTypesVersion(1)))
if err != nil {
panic(err)
}
ast, issues := env.Compile(`optional.none().orValue("") == ""`)
if issues.Err() != nil {
panic(issues.Err())
}
program, err := env.Program(ast)
if err != nil {
panic(err)
}
value, _, err := program.Eval(nil)
if err != nil {
panic(err)
}
fmt.Printf("value=%v type=%T\n", value, value)
}
EOF
(
cd "$tmpdir"
GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly go run .
)Repository: flanksource/gomplate
Length of output: 242
🏁 Script executed:
#!/bin/bash
set -eu
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
cp go.mod "$tmpdir/go.mod"
cp go.sum "$tmpdir/go.sum"
cat >"$tmpdir/main.go" <<'EOF'
package main
import (
"fmt"
"github.com/google/cel-go/cel"
)
func main() {
env, err := cel.NewEnv(cel.OptionalTypes(cel.OptionalTypesVersion(1)))
if err != nil {
panic(err)
}
ast, issues := env.Compile(`optional.none().orValue("") == ""`)
if issues.Err() != nil {
panic(issues.Err())
}
program, err := env.Program(ast)
if err != nil {
panic(err)
}
value, _, err := program.Eval(nil)
if err != nil {
panic(err)
}
fmt.Printf("value=%v type=%T\n", value, value)
}
EOF
(
cd "$tmpdir"
GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly go run .
)Repository: flanksource/gomplate
Length of output: 324
🏁 Script executed:
#!/bin/bash
set -eu
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
cp go.mod "$tmpdir/go.mod"
cp go.sum "$tmpdir/go.sum"
cat >"$tmpdir/main.go" <<'EOF'
package main
import (
"fmt"
"github.com/google/cel-go/cel"
)
func main() {
env, err := cel.NewEnv(cel.OptionalTypes(cel.OptionalTypesVersion(1)))
if err != nil {
panic(err)
}
ast, issues := env.Compile(`optional.none().orValue("") == ""`)
if issues.Err() != nil {
panic(issues.Err())
}
program, err := env.Program(ast)
if err != nil {
panic(err)
}
value, _, err := program.Eval(map[string]any{})
if err != nil {
panic(err)
}
fmt.Printf("value=%v type=%T\n", value, value)
}
EOF
(
cd "$tmpdir"
GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly go run .
)Repository: flanksource/gomplate
Length of output: 185
Exercise the empty optional branch.
Add a separate tracked evaluation with optional.none().orValue("") == "" and assert true. optional.none() is supported by cel-go v0.31.0 with OptionalTypes version 1.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cel_tracker_test.go` around lines 83 - 85, Add a separate tracked evaluation
near the existing RunExpressionContext call in the test, using the expression
optional.none().orValue("") == "" and the same tracked context setup, then
assert it completes without error and returns true. Keep the existing non-empty
optional evaluation unchanged.
| Add a `coalesce` function available in both CEL expressions and gomplate templates. | ||
| It returns the first argument that is neither null/nil nor empty (empty string `""`, | ||
| empty list `[]`, empty map `{}`). When every argument is null/empty it returns `null`. | ||
|
|
||
| **Problem**: Users frequently need a fallback chain such as | ||
| `coalesce(config.override, config.default, "fallback")` where any value in the | ||
| chain may be a CEL `optional<T>`, a plain value, or outright null. No such | ||
| primitive exists in the codebase today. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Reuse the existing coll.Coalesce implementation.
The supplied context already contains coll/coalesce.go Lines 9-16 and coll/coalesce_test.go Lines 9-35. coll.Coalesce already implements the first-non-empty fallback and has unit coverage. The “No such primitive exists” statement and the Phase 1 “new file” checklist are stale.
If implemented literally, this plan can duplicate Coalesce or replace its existing contract. Change Phase 1 to verify or extend the existing helper. Route the gomplate binding through that helper instead of prescribing a second reflection-based emptiness implementation.
Also applies to: 83-91, 138-141, 189-195
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@specs/REQUIREMENTS-coalesce.md` around lines 5 - 12, Update the coalesce
requirements to acknowledge and reuse the existing coll.Coalesce helper instead
of proposing a new primitive or reflection-based implementation. Revise the
Phase 1 checklist and affected sections to verify or extend coll.Coalesce as
needed, and specify that the gomplate binding delegates to this helper while
preserving its existing contract.
| ### FR-1: Variadic coalesce returning first non-null, non-empty value | ||
|
|
||
| **Description**: `coalesce` accepts any number of arguments (≥ 1) and returns | ||
| the first one that is neither null nor empty. Empty is defined as: | ||
| - `null` / `nil` | ||
| - empty string `""` | ||
| - empty list `[]` | ||
| - empty map `{}` | ||
|
|
||
| Numeric zero (`0`, `0.0`) and boolean `false` are **not** considered empty. | ||
|
|
||
| **User Story**: As a CEL/template user, I want `coalesce(a, b, c)` so that I | ||
| get the first meaningful value without writing nested `?? ` / `if` chains. | ||
|
|
||
| **Acceptance Criteria**: | ||
| - [ ] Returns the first non-null, non-empty argument | ||
| - [ ] Numeric `0` and `false` are treated as valid (non-empty) values | ||
| - [ ] Returns `null` when all arguments are null/empty | ||
| - [ ] Accepts one or more arguments (variadic) | ||
| - [ ] Works with mixed types in a single call |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Define the zero-argument behavior.
FR-1 requires one or more arguments. The existing variadic Go helper returns nil for zero arguments because its loop has no iterations. A gomplate template can pass zero arguments, while the CEL plan starts at arity one.
Specify whether zero arguments are rejected or return nil. Apply the same rule to both bindings and add a test.
Also applies to: 65-78, 193-200
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 33-33: Spaces inside code span elements
(MD038, no-space-in-code)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@specs/REQUIREMENTS-coalesce.md` around lines 21 - 40, Define the
zero-argument behavior for coalesce consistently across the Go helper, gomplate
binding, and CEL binding: either reject calls with no arguments or explicitly
return nil, then document that choice in FR-1. Update the relevant coalesce
symbols and add coverage for zero-argument calls in both bindings while
preserving the existing one-or-more-argument behavior.
| ### CEL variadic constraint | ||
|
|
||
| CEL-go does not support variadic functions natively | ||
| ([upstream issue](https://github.com/google/cel-go/issues/476)). | ||
| The standard pattern in this codebase is to declare overloads for arities 1–N. | ||
| Define overloads for 1, 2, 3, 4, 5 arguments with `cel.DynType` params, plus a | ||
| catch-all receiver-style overload if needed. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 6 'celCoalesce|coalesce|Overload|DynType' funcs tests
rg -n 'github.com/google/cel-go' go.mod go.sumRepository: flanksource/gomplate
Length of output: 50377
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- specification ---'
sed -n '1,240p' specs/REQUIREMENTS-coalesce.md
printf '%s\n' '--- coalesce implementation and tests ---'
for f in coll/coalesce.go coll/coalesce_test.go funcs/coll_gen.go tests/cel_test.go; do
if [ -f "$f" ]; then
printf '\n--- %s ---\n' "$f"
rg -n -C 12 'Coalesce|coalesce|optional|empty' "$f"
fi
done
printf '%s\n' '--- registration and dependency version ---'
rg -n -C 8 'coll.*Gen|coalesce|Register|cel-go' --glob '*.go' --glob 'go.mod' --glob 'go.sum' .Repository: flanksource/gomplate
Length of output: 9131
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- focused specification lines ---'
cat -n specs/REQUIREMENTS-coalesce.md | sed -n '1,230p'
printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(coalesce|coll_gen|cel_test|go\.(mod|sum))'
printf '%s\n' '--- coalesce references ---'
rg -n -C 15 'coalesce|Coalesce' --glob '*.go' --glob '*.md' .
printf '%s\n' '--- CEL dependency ---'
rg -n 'github.com/google/cel-go' go.mod go.sumRepository: flanksource/gomplate
Length of output: 42113
🌐 Web query:
cel-go v0.31.0 cel.Overload variadic functions overload arity FunctionBinding source
💡 Result:
In cel-go (including version v0.31.0), variadic functions and function overloading are primarily managed through the cel.Function and cel.Overload abstractions [1]. 1. Variadic Functions: CEL does not support "variadic" functions in the traditional sense where a single function definition accepts a variable number of arguments [2][1]. Instead, CEL handles variable arity by defining multiple explicit overloads for the same function name, each with a specific argument count [2][1]. For example, if you want a function to handle both one and two arguments, you must register two separate cel.Overload instances under the same cel.Function name, or use macros if you need to support arbitrary arity at parse time [1][3][4]. 2. Overload Arity: When defining a function, you use cel.Function(name, ...overloads) [1]. Each cel.Overload requires a unique identifier (the overload ID), the slice of argument types ([]*cel.Type), and the return type [1]. The CEL dispatcher uses the number and types of the provided arguments to select the correct overload at both check time and evaluation time [2][1]. 3. FunctionBinding: The cel.Function options (such as cel.Overload or cel.MemberOverload) include a FunctionBinding (often provided via helpers like cel.BinaryBinding, cel.UnaryBinding, etc.) that defines the actual Go implementation executed when that specific overload is matched [1]. Because binding happens at the overload level, you can provide different logic for each arity/type signature combination [1]. 4. Advanced Usage: If you require dynamic or late-bound function resolution (where dispatch logic is deferred until evaluation time), you can utilize features like cel.LateFunctionBinding [5] or the functions.Overload abstraction directly [6], though these methods typically bypass the static type-checking and arity-based resolution provided by the standard cel.Env configuration [6]. Summary for developers: To implement "variadic" behavior, define multiple cel.Overload entries for a single cel.Function, each with the expected argument count [1]. If the number of arguments is truly dynamic or unknown at compile time, macros are the appropriate mechanism to transform the expression at parse time [3][4].
Citations:
- 1: https://github.com/google/cel-go/blob/master/examples/README.md
- 2: https://celbyexample.com/custom-functions/
- 3: https://chromium.googlesource.com/external/github.com/google/cel-go/+/HEAD/cel/macro.go
- 4: https://github.com/google/cel-go/blob/master/parser/macro.go
- 5: Add LateFunctionBinding declaration and fix constant folding cel-expr/cel-go#1117
- 6: feature: Late Bind Function Calls at Evaluation Time cel-expr/cel-go#1172
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import re
from pathlib import Path
source = Path("funcs/coll.go").read_text()
overloads = re.findall(r'cel\.Overload\("coalesce_(\d+)"', source)
print("registered_coalesce_arities:", sorted(map(int, overloads)))
print("has_list_fallback:", bool(re.search(r'coalesce.*ListType|ListType.*coalesce', source, re.S)))
tests = Path("tests/cel_test.go").read_text()
calls = re.findall(r'coalesce\(([^()]*)\)', tests)
for call in calls:
print("test_argument_count:", 0 if not call.strip() else len(call.split(",")), repr(call))
PYRepository: flanksource/gomplate
Length of output: 478
Resolve the CEL contract for more than five arguments.
CEL-go dispatches by argument count. The binding defines only arities 1–5, so six-argument calls are unsupported. A list fallback is not registered, and coalesce([a, b]) must remain a valid one-argument call that returns the non-empty list. Define an explicit list form or function name, or define a supported maximum. Add tests for the selected boundary behavior.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@specs/REQUIREMENTS-coalesce.md` around lines 93 - 99, Clarify the CEL
contract in the “CEL variadic constraint” section by explicitly defining
behavior for calls with more than five arguments. Preserve coalesce([a, b]) as a
valid one-argument list call, and either document/register an explicit list form
or function name, or state and enforce a supported maximum. Add tests covering
the selected six-argument boundary and the one-argument list behavior.
| ## Overview | ||
|
|
||
| Add `first` and `last` functions available as CEL global functions, CEL member | ||
| methods, and gomplate template functions. They return the first/last element of | ||
| a list, character of a string, or value (by sorted-key order) of a map, with | ||
| nil-safe behaviour: null/empty/out-of-range inputs return the zero value of the | ||
| element type rather than panicking or producing an error. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Separate semantic results from rendered output.
The overview and FR-1/FR-2 define null or a typed zero, but the Go section returns nil and the CEL matrix uses "" for first(null) and first([]). The supplied tests/cel_test.go:803-826 context appears to use empty strings as rendered output for null cases. State the semantic result for each binding and label the test column as rendered output when it is not the CEL value. Otherwise, an empty string implementation can satisfy the table while violating the null contract.
Also applies to: 147-156, 190-207
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@specs/REQUIREMENTS-first-last.md` around lines 3 - 9, Clarify the
requirements tables and Go/CEL sections by separating semantic return values
from rendered output: specify null or the appropriate typed zero for null,
empty, and out-of-range inputs, while labeling empty-string entries as rendered
output only where applicable. Update the affected sections around the binding
descriptions and CEL matrix, and ensure the stated behavior consistently
preserves the null contract across CEL, Go, and gomplate bindings.
| ### FR-3: String support — first/last character | ||
|
|
||
| **Description**: When the input is a string, `first` returns the first character | ||
| (as a single-character string) and `last` returns the last character. | ||
|
|
||
| **Acceptance Criteria**: | ||
| - [ ] `first("hello")` → `"h"` | ||
| - [ ] `last("hello")` → `"o"` | ||
| - [ ] `first("")` → `""` (empty string zero value) | ||
| - [ ] `last("")` → `""` |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
git ls-files | grep -E '(^|/)(REQUIREMENTS-first-last\.md|firstlast\.go|coll\.go|cel_test\.go)$' || true
printf '%s\n' '--- requirement context ---'
sed -n '1,140p' specs/REQUIREMENTS-first-last.md
printf '%s\n' '--- first/last definitions and uses ---'
rg -n -C 5 'First|Last|first|last|first-last|FirstLast' --glob '*.go' --glob '*.md' .Repository: flanksource/gomplate
Length of output: 50379
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- funcs/coll.go relevant symbols ---'
rg -n -C 12 'celFirst|celLast|First\(|Last\(|MemberOverload|Overload' funcs/coll.go funcs/cel_exports.go
printf '%s\n' '--- tests relevant to first/last ---'
rg -n -C 8 'first|last|First|Last|non.?ASCII|Unicode|rune|emoji|é' coll funcs tests cel_test.go --glob '*.go'
printf '%s\n' '--- source outlines ---'
ast-grep outline coll/firstlast.go --match '$_' --view compact || true
ast-grep outline funcs/coll.go --match '$_' --view compact || trueRepository: flanksource/gomplate
Length of output: 45290
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re
go = Path("coll/firstlast.go").read_text()
cel = Path("funcs/coll.go").read_text()
tests = Path("tests/cel_test.go").read_text()
assert "runes := []rune(s)" in go
assert 'return string(runes[0])' in go
assert 'return string(runes[len(runes)-1])' in go
impl = re.search(
r"func celFirstLastImpl\(arg ref\.Val, first bool\) ref\.Val \{(?P<body>.*?)\n\}",
cel,
re.S,
)
assert impl, "celFirstLastImpl not found"
body = impl.group("body")
assert "traits.Lister" in body
assert "traits.Mapper" in body
assert "return types.NullValue" in body
assert "types.String" not in body and "StringType" not in body
test_fn = re.search(
r"func TestCelFirstLast\(t \*testing\.T\) \{(?P<body>.*?)\n\}",
tests,
re.S,
)
assert test_fn, "TestCelFirstLast not found"
test_body = test_fn.group("body")
assert 'first("hello")' not in test_body
assert 'last("hello")' not in test_body
assert '".first()' not in test_body
assert '".last()' not in test_body
print("Go binding: Unicode code-point indexing via []rune")
print("CEL binding: no string handling; unsupported values return null")
print("CEL tests: no string first/last coverage")
PYRepository: flanksource/gomplate
Length of output: 314
Define and implement Unicode code-point semantics for strings.
Use Unicode code points for first and last in both bindings. The Go binding uses []rune, but the CEL binding handles only lists and maps, so CEL string calls return null. Add global and member CEL tests for non-ASCII code points and state the expected outputs in FR-3.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@specs/REQUIREMENTS-first-last.md` around lines 57 - 66, Update FR-3 to define
first/last string behavior in terms of Unicode code points, including expected
non-ASCII outputs and empty-string results. Implement matching code-point
semantics in both bindings: preserve the Go binding’s []rune behavior and extend
the CEL binding’s first/last handling beyond lists and maps to support strings.
Add global and member CEL tests covering non-ASCII code points.
| ### FR-4: Map support — first/last value by sorted key | ||
|
|
||
| **Description**: When the input is a map, `first` returns the value at the | ||
| lexicographically smallest key, and `last` returns the value at the largest key. | ||
|
|
||
| **Acceptance Criteria**: | ||
| - [ ] `first({"b": 2, "a": 1})` → `1` (key "a" sorts first) | ||
| - [ ] `last({"b": 2, "a": 1})` → `2` (key "b" sorts last) | ||
| - [ ] `first({})` → `null` | ||
| - [ ] `last({})` → `null` |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Constrain FR-4 to string-keyed maps.
The requirement says map in general and orders keys lexicographically, but the proposed CEL overload is map<string,dyn> and the Go side names map[string]any. Either change the requirement to string-keyed maps, or define overloads and deterministic ordering for other supported key types. Otherwise CEL and gomplate expose different input domains.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@specs/REQUIREMENTS-first-last.md` around lines 71 - 80, Update FR-4 to
explicitly constrain map support to string-keyed maps, matching the proposed
map<string,dyn> CEL overload and map[string]any Go implementation. Clarify that
lexicographic ordering applies to string keys and retain the existing empty-map
behavior.
| // Global: first(list) | ||
| cel.Overload("first_list", []*cel.Type{cel.ListType(cel.DynType)}, cel.DynType, ...) | ||
| cel.Overload("first_string", []*cel.Type{cel.StringType}, cel.StringType, ...) | ||
| cel.Overload("first_map", []*cel.Type{cel.MapType(cel.StringType, cel.DynType)}, cel.DynType, ...) | ||
|
|
||
| // Member: list.first() | ||
| cel.MemberOverload("list_first", []*cel.Type{cel.ListType(cel.DynType)}, cel.DynType, ...) | ||
| cel.MemberOverload("string_first", []*cel.Type{cel.StringType}, cel.StringType, ...) | ||
| cel.MemberOverload("map_first", []*cel.Type{cel.MapType(cel.StringType, cel.DynType)}, cel.DynType, ...) | ||
| ``` | ||
|
|
||
| ### Zero value strategy | ||
|
|
||
| Return type-matched zero values following the same pattern as `nilsafe/zeroval.go`: | ||
|
|
||
| | Input type | Empty/null result | | ||
| |------------|------------------| | ||
| | `list<string>` | `""` | | ||
| | `list<int>` | `0` | | ||
| | `list<bool>` | `false` | | ||
| | `list<dyn>` / unknown | `null` | | ||
| | `string` | `""` | | ||
| | `map` (empty/null) | `null` | | ||
|
|
||
| In practice, since CEL lists are typed `dyn` at runtime, use `types.NullValue` | ||
| for empty lists unless the first element's type can be inferred. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n 'github.com/google/cel-go' --glob 'go.mod' --glob 'go.sum'
rg -n 'cel\.(ListType|MapType|DynType)|MemberOverload|Overload|first|last' --glob '*.go'Repository: flanksource/gomplate
Length of output: 158
🏁 Script executed:
set -euo pipefail
printf '%s\n' '--- candidate files ---'
git ls-files 'specs/REQUIREMENTS-first-last.md' '*firstlast*' '*coll*' 'go.mod' 'go.sum' '*.go' | sed -n '1,160p'
printf '%s\n' '--- requirement document outline and relevant sections ---'
wc -l specs/REQUIREMENTS-first-last.md
sed -n '1,230p' specs/REQUIREMENTS-first-last.md
printf '%s\n' '--- CEL dependency declarations ---'
rg -n -i 'cel-go|google/cel|cel\.' --glob 'go.mod' --glob 'go.sum' --glob '*.go' --glob '*.yaml' --glob '*.yml' . || trueRepository: flanksource/gomplate
Length of output: 50379
🌐 Web query:
cel-go v0.31 ListType(DynType) overload accepts list<int> type checking CEL
💡 Result:
In cel-go, using ListType(DynType) in a function overload allows the function to accept lists of any element type [1][2]. Because DynType acts as a dynamic type that is resolved at runtime, the type checker will permit the use of list<int> (or any other list type) when calling a function defined with a list(dyn) argument [1]. If you have a function overload defined with cel.ListType(cel.DynType), the CEL type checker will accept any list input because list(T) is assignable to list(dyn) [1][2]. If you require stricter type checking—for example, to ensure that only list<int> is accepted—you should use cel.ListType(cel.IntType) instead [1]. Additionally, if you want to enforce homogeneous types within your list literals (e.g., preventing lists like [1, "a"]), you can enable cel.HomogeneousAggregateLiterals() in your environment configuration [1]. This option helps prevent the type checker from defaulting to list(dyn) for mixed-type lists, ensuring that your list<int> overloads are invoked only when appropriate [1].
Citations:
- 1: Calling ListType Overloads with mixed types incorrectly compile & match invalid overload cel-expr/cel-go#660
- 2: https://chromium.googlesource.com/external/github.com/google/cel-go/+/HEAD/cel/decls.go
🏁 Script executed:
set -euo pipefail
printf '%s\n' '--- existing typed-list overload patterns ---'
sed -n '90,190p' kubernetes/lists.go
sed -n '320,345p' kubernetes/lists.go
printf '%s\n' '--- null-safe zero-value implementation and decorator ---'
sed -n '1,220p' nilsafe/zeroval.go
sed -n '1,180p' nilsafe/nilsafe.go
printf '%s\n' '--- repository CEL environment and function registration ---'
sed -n '1,115p' cel.go
sed -n '1,220p' funcs/cel_exports.go
sed -n '1,220p' funcs/coll.go
printf '%s\n' '--- requirements history/status ---'
git status --short
git diff --statRepository: flanksource/gomplate
Length of output: 26631
🏁 Script executed:
set -euo pipefail
printf '%s\n' '--- first/last declarations and implementations ---'
rg -n -C 12 'celFirst|celLast|func First|func Last|first_list|last_list|first_string|last_string|first_map|last_map' funcs coll tests nilsafe --glob '*.go'
printf '%s\n' '--- complete first/last implementation files ---'
for f in coll/firstlast.go coll/firstlast_test.go funcs/coll.go; do
if test -f "$f"; then
printf '\n### %s\n' "$f"
wc -l "$f"
sed -n '1,280p' "$f"
fi
done
printf '%s\n' '--- relevant generated/function source around CEL bindings ---'
rg -l 'var celFirst|var celLast|celFirst =' --glob '*.go' | while read -r f; do
printf '\n### %s\n' "$f"
sed -n '1,260p' "$f"
doneRepository: flanksource/gomplate
Length of output: 16847
🏁 Script executed:
set -euo pipefail
printf '%s\n' '--- CEL first/last implementation ---'
sed -n '270,332p' funcs/coll.go
printf '%s\n' '--- current integration and nil-safe tests mentioning first/last ---'
rg -n -C 8 'first|last|celFirstLastImpl' tests funcs coll nilsafe --glob '*_test.go'
printf '%s\n' '--- requirement line references for all conflicting contracts ---'
nl -ba specs/REQUIREMENTS-first-last.md | sed -n '25,62p;88,150p;188,205p'Repository: flanksource/gomplate
Length of output: 16796
Align the empty-list contract with the CEL implementation.
cel.ListType(cel.DynType) accepts typed lists in cel-go v0.31. The current celFirst and celLast bindings return types.NullValue for empty or null lists. They do not return type-specific zero values.
Define null as the result for empty or unknown lists. Update the conflicting requirements and tests. Keep the rendered-template expectation as "" only where it represents rendered CEL null.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@specs/REQUIREMENTS-first-last.md` around lines 120 - 145, Update the
zero-value strategy in the requirements and related tests to define empty or
null lists, including typed lists accepted by cel.ListType(cel.DynType), as
returning types.NullValue through celFirst and celLast. Remove conflicting
type-specific list zero-value expectations, while retaining rendered-template
expectations of "" only when they represent rendered CEL null.
What
Notes
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
coalesce,first, andlastbehavior.