Skip to content

feat(cel): preserve native values - #173

Open
moshloop wants to merge 1 commit into
mainfrom
feat/preserve-cel-native-values
Open

feat(cel): preserve native values#173
moshloop wants to merge 1 commit into
mainfrom
feat/preserve-cel-native-values

Conversation

@moshloop

@moshloop moshloop commented Aug 11, 2026

Copy link
Copy Markdown
Member

What

  • Preserve registered native Go values and JSON field mappings in CEL.
  • Upgrade cel-go to v0.31 and add optimized execution decorators.
  • Cap regex programs at 10,000 instructions.

Notes

  • Improve benchmark comparison and CSV regression reporting.
  • Document CEL behavior and upcoming collection helpers.

Summary by CodeRabbit

  • New Features

    • CEL expressions can now work directly with registered native Go types, including JSON-based field names.
    • Added safer expression handling with context-aware evaluation, caching, and improved serialization.
    • Added a 10,000-instruction limit for CEL regular expressions.
    • Improved nil-safe evaluation across supported operations.
  • Bug Fixes

    • Fixed native type registration, cache invalidation, and concurrent evaluation behavior.
    • Improved benchmark regression detection and reporting.
  • Documentation

    • Documented native CEL types, regex limits, and planned coalesce, first, and last behavior.

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
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedgithub.com/​google/​cel-go@​v0.27.0 ⏵ v0.31.074 +1100 +2100100100

View full report

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

Benchstat

Base: 31a8cb108062431a578f5bcc41d30d5946f1abd2
Head: b14bd92796ee2dca6ba4f9324efa602fb96923de

4 minor regression(s) (all within 5% threshold)

Benchmark Metric Base Head Change p-value
RunExpressionContext/cache=hit/environment=small-4 sec/op 1.31 us/op 1.36 us/op +4.20% 0.003
RunExpressionContextCompile/cache=miss/environment=small-4 B/op 1.99e+05 B/op 2.07e+05 B/op +4.14% 0.000
RunExpressionContextCompile/cache=miss/environment=large-4 B/op 2.01e+05 B/op 2.09e+05 B/op +4.13% 0.000
RunExpressionContext/cache=hit/environment=large-4 sec/op 8.12 us/op 8.2 us/op +1.05% 0.007
13 improvement(s)
Benchmark Metric Base Head Change p-value
CELEnvExtend/variables=1/functions=0-4 sec/op 71.3 us/op 3.6 us/op -94.95% 0.000
CELEnvExtend/variables=10/functions=0-4 sec/op 72.8 us/op 4.95 us/op -93.21% 0.000
CELEnvExtend/variables=1/functions=0-4 B/op 3.77e+04 B/op 3.43e+03 B/op -90.89% 0.000
CELEnvExtend/variables=10/functions=0-4 B/op 3.94e+04 B/op 5.15e+03 B/op -86.93% 0.000
CELEnvExtend/variables=100/functions=0-4 sec/op 85.9 us/op 16.8 us/op -80.40% 0.000
CELEnvExtend/variables=100/functions=0-4 B/op 5.73e+04 B/op 2.3e+04 B/op -59.78% 0.000
CELEnvExtend/variables=1/functions=0-4 allocs/op 132 allocs/op 60 allocs/op -54.55% 0.000
CELEnvExtend/variables=10/functions=0-4 allocs/op 151 allocs/op 79 allocs/op -47.68% 0.000
CELEnvExtend/variables=100/functions=0-4 allocs/op 253 allocs/op 181 allocs/op -28.46% 0.000
RunExpressionContextCompile/cache=miss/environment=small-4 sec/op 248 us/op 231 us/op -6.59% 0.000
RunExpressionContextCompile/cache=miss/environment=large-4 sec/op 256 us/op 240 us/op -6.13% 0.000
RunExpressionContextCompile/cache=miss/environment=small-4 allocs/op 2.25e+03 allocs/op 2.23e+03 allocs/op -1.15% 0.000
RunExpressionContextCompile/cache=miss/environment=large-4 allocs/op 2.34e+03 allocs/op 2.31e+03 allocs/op -1.11% 0.000
Full benchstat output
goos: linux
goarch: amd64
pkg: github.com/flanksource/gomplate/v3
cpu: AMD EPYC 9V74 80-Core Processor                
                                                                      │ bench-base.txt │           bench-head.txt            │
                                                                      │     sec/op     │   sec/op     vs base                │
RunExpressionContext/cache=hit/environment=small-4                         1.309µ ± 4%   1.364µ ± 6%   +4.20% (p=0.003 n=10)
RunExpressionContext/cache=hit/environment=large-4                         8.118µ ± 1%   8.203µ ± 1%   +1.05% (p=0.007 n=10)
RunExpressionContextCompile/cache=miss/environment=small-4                 247.6µ ± 3%   231.2µ ± 3%   -6.59% (p=0.000 n=10)
RunExpressionContextCompile/cache=miss/environment=large-4                 255.9µ ± 2%   240.2µ ± 3%   -6.13% (p=0.000 n=10)
CELEnvExtend/variables=1/functions=0-4                                    71.328µ ± 3%   3.603µ ± 4%  -94.95% (p=0.000 n=10)
CELEnvExtend/variables=10/functions=0-4                                   72.831µ ± 3%   4.946µ ± 3%  -93.21% (p=0.000 n=10)
CELEnvExtend/variables=100/functions=0-4                                   85.85µ ± 3%   16.83µ ± 3%  -80.40% (p=0.000 n=10)
Serialize/items=10/native_values=true-4                                    26.42µ ± 1%   26.56µ ± 1%        ~ (p=0.436 n=10)
Serialize/items=100/native_values=true-4                                   251.1µ ± 2%   252.9µ ± 1%        ~ (p=0.143 n=10)
Serialize/items=1000/native_values=true-4                                  2.577m ± 1%   2.579m ± 1%        ~ (p=0.853 n=10)
Serialize/items=10000/native_values=true-4                                 33.38m ± 5%   33.37m ± 4%        ~ (p=0.631 n=10)
Serialize_NoNativeTypes/items=100/native_values=false-4                    68.26µ ± 3%   68.30µ ± 1%        ~ (p=0.912 n=10)
Serialize_NoNativeTypes/items=1000/native_values=false-4                   690.7µ ± 2%   687.0µ ± 1%        ~ (p=0.631 n=10)
Serialize_NoNativeTypes/items=10000/native_values=false-4                  6.806m ± 4%   6.815m ± 5%        ~ (p=0.853 n=10)
CELEnvExtendCustomFunction-4                                                             54.74µ ± 5%
CELProgramEvaluation/expression=scalar/optimized=false-4                                 160.0n ± 1%
CELProgramEvaluation/expression=scalar/optimized=true-4                                  159.9n ± 1%
CELProgramEvaluation/expression=list_optional_regex/optimized=false-4                    4.254µ ± 3%
CELProgramEvaluation/expression=list_optional_regex/optimized=true-4                     1.100µ ± 2%
CELProgramEvaluation/expression=comprehension/optimized=false-4                          5.441µ ± 2%
CELProgramEvaluation/expression=comprehension/optimized=true-4                           5.077µ ± 1%
RunExpressionNativeInput/input=map-4                                                     2.044µ ± 2%
RunExpressionNativeInput/input=native_struct-4                                           2.204µ ± 3%
geomean                                                                    185.0µ        23.30µ       -40.95%

                                                                      │ bench-base.txt │             bench-head.txt             │
                                                                      │      B/op      │     B/op      vs base                  │
RunExpressionContext/cache=hit/environment=small-4                          664.0 ± 0%     664.0 ± 0%        ~ (p=1.000 n=10) ¹
RunExpressionContext/cache=hit/environment=large-4                        2.445Ki ± 0%   2.445Ki ± 0%        ~ (p=1.000 n=10) ¹
RunExpressionContextCompile/cache=miss/environment=small-4                194.4Ki ± 0%   202.4Ki ± 0%   +4.14% (p=0.000 n=10)
RunExpressionContextCompile/cache=miss/environment=large-4                196.3Ki ± 0%   204.4Ki ± 0%   +4.13% (p=0.000 n=10)
CELEnvExtend/variables=1/functions=0-4                                   36.803Ki ± 0%   3.352Ki ± 0%  -90.89% (p=0.000 n=10)
CELEnvExtend/variables=10/functions=0-4                                  38.483Ki ± 0%   5.031Ki ± 0%  -86.93% (p=0.000 n=10)
CELEnvExtend/variables=100/functions=0-4                                  55.96Ki ± 0%   22.50Ki ± 0%  -59.78% (p=0.000 n=10)
Serialize/items=10/native_values=true-4                                   11.06Ki ± 0%   11.06Ki ± 0%        ~ (p=1.000 n=10) ¹
Serialize/items=100/native_values=true-4                                  95.49Ki ± 0%   95.49Ki ± 0%        ~ (p=0.467 n=10)
Serialize/items=1000/native_values=true-4                                 950.5Ki ± 0%   950.6Ki ± 0%        ~ (p=0.084 n=10)
Serialize/items=10000/native_values=true-4                                9.877Mi ± 0%   9.861Mi ± 0%        ~ (p=0.362 n=10)
Serialize_NoNativeTypes/items=100/native_values=false-4                   36.33Ki ± 0%   36.33Ki ± 0%        ~ (p=1.000 n=10)
Serialize_NoNativeTypes/items=1000/native_values=false-4                  365.6Ki ± 0%   365.6Ki ± 0%        ~ (p=1.000 n=10)
Serialize_NoNativeTypes/items=10000/native_values=false-4                 3.584Mi ± 0%   3.584Mi ± 0%        ~ (p=0.869 n=10)
CELEnvExtendCustomFunction-4                                                             45.19Ki ± 0%
CELProgramEvaluation/expression=scalar/optimized=false-4                                   64.00 ± 0%
CELProgramEvaluation/expression=scalar/optimized=true-4                                    64.00 ± 0%
CELProgramEvaluation/expression=list_optional_regex/optimized=false-4                    4.463Ki ± 0%
CELProgramEvaluation/expression=list_optional_regex/optimized=true-4                       741.0 ± 0%
CELProgramEvaluation/expression=comprehension/optimized=false-4                          2.822Ki ± 0%
CELProgramEvaluation/expression=comprehension/optimized=true-4                           2.438Ki ± 0%
RunExpressionNativeInput/input=map-4                                                       808.0 ± 0%
RunExpressionNativeInput/input=native_struct-4                                            1000.0 ± 0%
geomean                                                                   90.24Ki        12.93Ki       -31.33%
¹ all samples are equal

                                                                      │ bench-base.txt │            bench-head.txt             │
                                                                      │   allocs/op    │  allocs/op   vs base                  │
RunExpressionContext/cache=hit/environment=small-4                          20.00 ± 0%    20.00 ± 0%        ~ (p=1.000 n=10) ¹
RunExpressionContext/cache=hit/environment=large-4                          101.0 ± 0%    101.0 ± 0%        ~ (p=1.000 n=10) ¹
RunExpressionContextCompile/cache=miss/environment=small-4                 2.253k ± 0%   2.227k ± 0%   -1.15% (p=0.000 n=10)
RunExpressionContextCompile/cache=miss/environment=large-4                 2.336k ± 0%   2.310k ± 0%   -1.11% (p=0.000 n=10)
CELEnvExtend/variables=1/functions=0-4                                     132.00 ± 0%    60.00 ± 0%  -54.55% (p=0.000 n=10)
CELEnvExtend/variables=10/functions=0-4                                    151.00 ± 0%    79.00 ± 0%  -47.68% (p=0.000 n=10)
CELEnvExtend/variables=100/functions=0-4                                    253.0 ± 0%    181.0 ± 0%  -28.46% (p=0.000 n=10)
Serialize/items=10/native_values=true-4                                     268.0 ± 0%    268.0 ± 0%        ~ (p=1.000 n=10) ¹
Serialize/items=100/native_values=true-4                                   2.518k ± 0%   2.518k ± 0%        ~ (p=1.000 n=10) ¹
Serialize/items=1000/native_values=true-4                                  25.79k ± 0%   25.79k ± 0%        ~ (p=0.350 n=10)
Serialize/items=10000/native_values=true-4                                 263.9k ± 0%   263.8k ± 0%        ~ (p=0.303 n=10)
Serialize_NoNativeTypes/items=100/native_values=false-4                     911.0 ± 0%    911.0 ± 0%        ~ (p=1.000 n=10) ¹
Serialize_NoNativeTypes/items=1000/native_values=false-4                   9.757k ± 0%   9.757k ± 0%        ~ (p=1.000 n=10) ¹
Serialize_NoNativeTypes/items=10000/native_values=false-4                  99.98k ± 0%   99.98k ± 0%        ~ (p=0.777 n=10)
CELEnvExtendCustomFunction-4                                                              100.0 ± 0%
CELProgramEvaluation/expression=scalar/optimized=false-4                                  3.000 ± 0%
CELProgramEvaluation/expression=scalar/optimized=true-4                                   3.000 ± 0%
CELProgramEvaluation/expression=list_optional_regex/optimized=false-4                     105.0 ± 0%
CELProgramEvaluation/expression=list_optional_regex/optimized=true-4                      23.00 ± 0%
CELProgramEvaluation/expression=comprehension/optimized=false-4                           92.00 ± 0%
CELProgramEvaluation/expression=comprehension/optimized=true-4                            82.00 ± 0%
RunExpressionNativeInput/input=map-4                                                      27.00 ± 0%
RunExpressionNativeInput/input=native_struct-4                                            26.00 ± 0%
geomean                                                                    1.469k         289.6       -12.03%
¹ all samples are equal

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The 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.

Changes

CEL runtime integration

Layer / File(s) Summary
CEL environment and dependency setup
go.mod, cel.go, template.go, README.md
CEL uses v0.31.0, regex programs use a 10,000-instruction limit, and native type environment options participate in environment construction and cache state.
Native type registration
cel_native.go, CEL.md, cel_native_test.go
RegisterType supports validated Go and CEL types, reflection forms, JSON field names, deduplication, generation tracking, and concurrent publication.
CEL expression execution and serialization
cel_expression.go, serialize.go, template_test.go
Expression runners serialize inputs, compile or reuse programs, track evaluations, wrap errors, and include native type generations in cache keys.
Nil-safe V2 execution
nilsafe/nilsafe.go, nilsafe/zeroval.go
Nil-safe attributes, calls, and zero-value comparisons support both Eval and Exec through shared logic.
CEL behavior validation
cel_native_test.go, cel_tracker_test.go, CEL.md
Tests cover native registration, cache invalidation, concurrency, tracking, and literal or dynamic regex limits.

Benchmark reporting and measurement

Layer / File(s) Summary
CSV parsing and threshold-aware rendering
.github/scripts/benchstat-summary.py, .github/scripts/benchstat-summary_test.py
The summary script parses metric tables, formats values, renders Markdown tables, emphasizes qualifying regressions, and supports --no-fail.
Benchmark workflow integration
.github/workflows/benchmark.yml, .gitignore
The workflow builds separate binaries, runs interleaved samples, produces CSV and Markdown reports, uploads artifacts, and checks CSV regression output.
CEL and serialization benchmark coverage
cel_v031_bench_test.go, run_expression_bench_test.go, serialize_bench_test.go
Benchmarks cover environment extension, expression optimization and caching, native inputs, serialization modes, and deterministic fixtures.

CEL function requirements

Layer / File(s) Summary
Coalesce specification
specs/REQUIREMENTS-coalesce.md
The specification defines coalesce fallback, emptiness, optional handling, registrations, overloads, tests, and implementation phases.
First and last specification
specs/REQUIREMENTS-first-last.md
The specification defines first and last behavior for lists, strings, maps, null values, CEL forms, gomplate functions, tests, and implementation phases.

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
Loading

Possibly related PRs

Suggested reviewers: hairyhenderson, adityathebe

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary change: preserving native values in CEL.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/preserve-cel-native-values
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/preserve-cel-native-values

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (2)
specs/REQUIREMENTS-coalesce.md (1)

173-185: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Expand 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 win

Complete 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 omits last(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

📥 Commits

Reviewing files that changed from the base of the PR and between 31a8cb1 and b14bd92.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (22)
  • .github/scripts/benchstat-summary.py
  • .github/scripts/benchstat-summary_test.py
  • .github/workflows/benchmark.yml
  • .gitignore
  • CEL.md
  • README.md
  • cel.go
  • cel_expression.go
  • cel_native.go
  • cel_native_test.go
  • cel_tracker_test.go
  • cel_v031_bench_test.go
  • go.mod
  • nilsafe/nilsafe.go
  • nilsafe/zeroval.go
  • run_expression_bench_test.go
  • serialize.go
  • serialize_bench_test.go
  • specs/REQUIREMENTS-coalesce.md
  • specs/REQUIREMENTS-first-last.md
  • template.go
  • template_test.go

Comment on lines 77 to +80
# 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.yml

Repository: 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.

Comment thread cel_expression.go
Comment on lines +120 to +126
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())
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.sum

Repository: 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" || true

Repository: 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 | head

Repository: 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.

Comment thread cel_tracker_test.go
Comment on lines +83 to +85
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})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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:


🏁 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:


🏁 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 -80

Repository: 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:


🏁 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.

Comment on lines +5 to +12
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Comment on lines +21 to +40
### 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment on lines +93 to +99
### 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.sum

Repository: 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.sum

Repository: 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:


🏁 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))
PY

Repository: 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.

Comment on lines +3 to +9
## 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Comment on lines +57 to +66
### 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("")` → `""`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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 || true

Repository: 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")
PY

Repository: 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.

Comment on lines +71 to +80
### 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`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Comment on lines +120 to +145
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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' . || true

Repository: 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:


🏁 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 --stat

Repository: 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"
done

Repository: 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant