Skip to content

fix: unify divergent expression evaluation paths (#176)#179

Merged
adnaan merged 10 commits intomainfrom
claude/issue-176-plan-Wch2I
Mar 12, 2026
Merged

fix: unify divergent expression evaluation paths (#176)#179
adnaan merged 10 commits intomainfrom
claude/issue-176-plan-Wch2I

Conversation

@adnaan
Copy link
Copy Markdown
Contributor

@adnaan adnaan commented Mar 11, 2026

Extract buildExecData() helper to consolidate three independent
map-building functions (evaluateActionWithVars, transformAndEvalWithVars,
transformConditionWithVars) that diverged over time, causing:

  • .Type resolving as nil in mixed var+dot expressions like {{$c.Method .Type}}
  • Raw "ERROR: ..." strings leaking into rendered HTML
  • Partial match bugs ($col corrupted by $c replacement)
  • Non-UTF8-safe capitalization corrupting multi-byte variable names

Changes:

  • Move capitalizeFieldName() and mergeFieldsIntoMap() to var_context.go
    as shared infrastructure
  • Make capitalizeFieldName() UTF8-safe using utf8.DecodeRuneInString
  • Create buildExecData() combining best of all three paths:
    descending-length sort, UTF8 capitalization, root var detection,
    dot context merging
  • Fix evaluateActionWithVars() to return (string, error) instead of
    embedding errors as rendered content
  • Add cross-path parameterized tests (var_eval_test.go)
  • Add standard library parity tests (stdlib_parity_test.go)

https://claude.ai/code/session_011iXr6f9X1MT3p2N6QpWGdu

claude added 2 commits March 11, 2026 19:04
Extract buildExecData() helper to consolidate three independent
map-building functions (evaluateActionWithVars, transformAndEvalWithVars,
transformConditionWithVars) that diverged over time, causing:

- .Type resolving as nil in mixed var+dot expressions like {{$c.Method .Type}}
- Raw "ERROR: ..." strings leaking into rendered HTML
- Partial match bugs ($col corrupted by $c replacement)
- Non-UTF8-safe capitalization corrupting multi-byte variable names

Changes:
- Move capitalizeFieldName() and mergeFieldsIntoMap() to var_context.go
  as shared infrastructure
- Make capitalizeFieldName() UTF8-safe using utf8.DecodeRuneInString
- Create buildExecData() combining best of all three paths:
  descending-length sort, UTF8 capitalization, root var detection,
  dot context merging
- Fix evaluateActionWithVars() to return (string, error) instead of
  embedding errors as rendered content
- Add cross-path parameterized tests (var_eval_test.go)
- Add standard library parity tests (stdlib_parity_test.go)

https://claude.ai/code/session_011iXr6f9X1MT3p2N6QpWGdu
Pre-existing bug found during #176 work: $ (root variable) inside
{{with}} blocks resolves against the narrowed context instead of root.

https://claude.ai/code/session_011iXr6f9X1MT3p2N6QpWGdu
Copilot AI review requested due to automatic review settings March 11, 2026 20:03
@claude
Copy link
Copy Markdown

claude bot commented Mar 11, 2026

Code Review

Good consolidation work overall. The three divergent expression evaluation paths were a real maintenance hazard, and the bugs they caused — partial matches, ERROR strings in HTML, broken .Type access in mixed var+dot expressions — were all legitimate. Here is my feedback.


Issues

1. issue-with-root-var-bug.txt should not be a committed file

This belongs as a GitHub issue, not a text file in the repository. The commented-out test in stdlib_parity_test.go already documents the divergence clearly. Open a GitHub issue and reference it from the test comment instead.

2. Subtle behavioral change in the evaluateActionWithVars early-return guard

Old guard: if usedVars.Len() == 0 && !usesRootVar { return "" }
New guard: if len(execData) == 0 { return "", nil }

Since buildExecData unconditionally calls mergeFieldsIntoMap(varCtx.dot, execData), a non-empty dot context populates execData even when no $ references appear in the expression. The len(execData) == 0 guard is therefore effectively dead code whenever dot is non-empty. In practice this is fine since evaluateActionWithVars is only called from the "has variables" path, but the behavioral difference is undocumented. Either remove the guard entirely or add a comment explaining when it can realistically trigger.

3. sortedVarNames called twice in transformAndEvalWithVars

The function first iterates sortedNames to compute usesVar, then calls buildExecData which calls sortedVarNames again internally — two allocations and two sort passes over the same data. For typical short variable lists this is negligible but unnecessary. The fast-path check and the transformation could share one pass.

4. transformConditionWithVars is now a trivial alias

The function is a single-line pass-through to buildExecData with no added semantics. The one caller in conditional.go could call buildExecData directly. Per project conventions, avoid indirection layers that do not earn their keep.


Correctness Concerns

5. treeToHTML test helper may drift from internal/render

stdlib_parity_test.go implements its own treeToHTML that interleaves statics and dynamics, duplicating render package logic. If the wire format or tree structure changes, the parity tests could stay green while the real render diverges. A comment explicitly linking this helper to render.TreeToHTML would help future maintainers keep them in sync.

6. The usesRoot fast-path fix in transformAndEvalWithVars lacks a targeted unit test

The PR adds usesRoot detection so that $.Field expressions without named variables no longer fall through to the wrong fast path. This is covered only indirectly via lvtRender in the stdlib parity tests. A direct unit test calling transformAndEvalWithVars with a $.Field expression and no named vars would make the fix explicit and protect against future regressions.


Positive Observations

  • Error propagation: Fixing evaluateActionWithVars to return (string, error) instead of embedding ERROR: ... strings in rendered HTML is the right call. Errors must never leak as visible content.
  • Descending-length sort: The $c/$col partial-match fix is correct and the targeted tests in var_eval_test.go cover it well.
  • UTF8 safety: Consolidating into a single capitalizeFieldName using utf8.DecodeRuneInString ensures consistent behavior across all three former paths.
  • Test coverage: var_eval_test.go has thorough parameterized coverage of buildExecData. The stdlib parity approach is a useful pattern for tracking template engine divergence over time.
  • usesRoot fast-path fix in transformAndEvalWithVars: A genuine correctness fix — $.Field expressions without named vars were previously evaluated against the wrong context.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR consolidates three previously diverged variable/dot/root expression-evaluation paths into a shared buildExecData() helper to fix correctness issues (mixed var+dot expressions, UTF-8 capitalization, partial matches) and to stop embedding "ERROR: ..." strings into rendered output.

Changes:

  • Adds buildExecData() plus shared helpers (capitalizeFieldName, mergeFieldsIntoMap) to unify $var/$. transformations and dot-context merging.
  • Updates action evaluation to return (string, error) and propagate errors instead of rendering error strings.
  • Adds focused unit tests (var_eval_test.go) and stdlib parity tests (stdlib_parity_test.go) to prevent regressions.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
internal/parse/var_context.go Introduces buildExecData() and shared helpers; updates transformAndEvalWithVars() to use unified logic.
internal/parse/field.go Changes evaluateActionWithVars() to return errors and uses buildExecData().
internal/parse/conditional.go Removes bespoke condition var/root transformation and delegates to buildExecData().
internal/parse/field_test.go Adapts tests to new (string, error) signature for action evaluation.
internal/parse/var_eval_test.go Adds tests for UTF-8 capitalization, partial match prevention, root handling, dot merging, and error propagation.
internal/parse/stdlib_parity_test.go Adds parity tests comparing livetemplate output to html/template for core constructs.
issue-with-root-var-bug.txt Adds a write-up for a known $-inside-with root resolution divergence (not fixed here).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/parse/conditional.go Outdated
Comment on lines +61 to +65
@@ -63,7 +62,7 @@ func handleIfNodeWithVars(node *parse.IfNode, varCtx *varContext, keyGen KeyGene
}

// Condition uses variables or root - transform it
transformedCond, execData, err := transformConditionWithVars(pipeStr, varCtx, usesRoot)
transformedCond, execData, err := transformConditionWithVars(pipeStr, varCtx)
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

The decision to take the vars/root path is driven by usesRoot := detectsRootVariable(pipeStr, ...). detectsRootVariable() currently reports true for '$' inside quoted strings (per existing tests), which can force the execData/map evaluation path for conditions that don’t actually use root, potentially breaking dot method access. Consider tightening the root detection used for this branching decision (e.g., only treat "$." / standalone "$" as root usage, or parse the pipe and look for VariableNodes) so string literals containing '$' don’t change evaluation mode.

Copilot uses AI. Check for mistakes.
Comment on lines +189 to +192
if detectsRootVariable(expr, varCtx.vars) {
transformedExpr = strings.ReplaceAll(transformedExpr, "$.", ".RootData.")
execData["RootData"] = varCtx.parent
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

buildExecData() enables root-variable handling via detectsRootVariable(), but it only rewrites "$." to ".RootData.". This leaves standalone "$" usages (e.g. "{{$}}", "$ | func", etc.) untransformed, so the wrapper template will resolve "$" against execData rather than varCtx.parent. Consider rewriting standalone root "$" tokens to ".RootData" as well (while avoiding "$var" named variables).

Copilot uses AI. Check for mistakes.
Comment on lines +180 to +186
for _, varName := range sortedNames {
if strings.Contains(expr, "$"+varName) {
fieldName := capitalizeFieldName(varName)
transformedExpr = strings.ReplaceAll(transformedExpr, "$"+varName, "."+fieldName)
varValue, _ := varCtx.vars.Get(varName)
execData[fieldName] = varValue
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

Variable usage detection in buildExecData() uses strings.Contains(expr, "$"+varName). This can produce false positives when one var name is a prefix of another (e.g. varName="c" will match "$col"), causing unused variables to be added to execData and potentially changing semantics for expressions that use "." (e.g. len/range over dot). Consider matching variable references with token boundaries (or parsing the template/pipe AST and collecting VariableNodes) instead of substring matching.

Copilot uses AI. Check for mistakes.
Comment on lines +216 to 220
usesRoot := detectsRootVariable(expr, varCtx.vars)

if !usesVar {
if !usesVar && !usesRoot {
return evaluatePipeWithCache(ctx.TemplateName, expr, varCtx.dot, ctx)
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

transformAndEvalWithVars() now treats any detectsRootVariable()==true as a reason to switch to the synthetic execData map path. detectsRootVariable() currently returns true for '$' inside string literals (see existing tests), so expressions like {{eq .Val "$"}} may now be evaluated against a map (merged fields) instead of the original dot value, which can break method access and other dot semantics. Consider tightening the root-var detection used for this branch decision (e.g., only trigger on "$.", or parse the expression and look for VariableNodes) so '$' in quoted strings doesn't force the execData path.

Copilot uses AI. Check for mistakes.
Comment thread issue-with-root-var-bug.txt Outdated
Comment on lines +1 to +54
bug: $ (root variable) inside {{with}} resolves against narrowed context instead of root data

---

## Description

When using `$` or `$.Field` inside a `{{with}}` block, the root variable resolves against the narrowed `with` context instead of the original root data. This diverges from Go's standard `text/template` behavior where `$` always refers to the root data passed to `Execute()`.

## Reproduction

```go
tmpl := `{{with .Item}}{{.Name}} from {{$.Title}}{{end}}`
data := map[string]interface{}{
"Title": "root",
"Item": map[string]interface{}{"Name": "child"},
}
```

**Expected (stdlib `html/template`):** `child from root`
**Actual (livetemplate):** `child from ` (empty — `$.Title` resolves against the narrowed `.Item` context which has no `Title` field)

## Root Cause

In `internal/parse/with.go`, `handleWithNode()` (the non-var path) passes the narrowed context as `data` to `buildTreeFromAST`:

```go
// Line 34 — newContext is the {{with}} value, not the original root data
return buildTreeFromAST(node.List, newContext, keyGen, ctx)
```

Go's template engine tracks the root data (`$`) separately from the current dot (`.`). When `$` is used inside a `with` block, it should resolve against the original root, but `handleWithNode` loses the root reference by passing only `newContext`.

The var-aware path (`handleWithNodeWithVars`) handles this correctly by maintaining `varCtx.parent` as the root data. The non-var path has no equivalent mechanism.

## Affected Code

- `internal/parse/with.go:34` — `handleWithNode()` loses root data reference
- The var-aware path (`handleWithNodeWithVars`, line 63) correctly sets `parent: varCtx.parent`

## Suggested Fix

Pass the original `data` alongside `newContext` so that `$` can resolve against the root. Options:

1. **Always use the var-aware path** for `with` blocks (convert to `varContext` even when no explicit variables exist)
2. **Thread root data** through the non-var `buildTreeFromAST` call chain so `$` resolves correctly
3. **Wrap the with body** in a `varContext` with `parent = data` and `dot = newContext` (minimal change)

## Discovered During

Found by the standard library parity tests added in the PR for #176. The test is currently commented out in `internal/parse/stdlib_parity_test.go` with a note referencing this as a pre-existing divergence.

## Impact

Any template using `$.Field` inside a `{{with}}` block without other variable declarations will silently produce empty/wrong values for the root field access. This is a correctness bug — the output diverges from what Go's standard template engine produces.
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

This looks like an ad-hoc issue report added at the repository root. To keep the repo organized and ensure it’s discoverable/maintained, consider moving this write-up into the existing docs/ structure (or tracking it as a GitHub issue and linking from docs/tests) rather than keeping a standalone root-level txt file.

Suggested change
bug: $ (root variable) inside {{with}} resolves against narrowed context instead of root data
---
## Description
When using `$` or `$.Field` inside a `{{with}}` block, the root variable resolves against the narrowed `with` context instead of the original root data. This diverges from Go's standard `text/template` behavior where `$` always refers to the root data passed to `Execute()`.
## Reproduction
```go
tmpl := `{{with .Item}}{{.Name}} from {{$.Title}}{{end}}`
data := map[string]interface{}{
"Title": "root",
"Item": map[string]interface{}{"Name": "child"},
}
```
**Expected (stdlib `html/template`):** `child from root`
**Actual (livetemplate):** `child from ` (empty — `$.Title` resolves against the narrowed `.Item` context which has no `Title` field)
## Root Cause
In `internal/parse/with.go`, `handleWithNode()` (the non-var path) passes the narrowed context as `data` to `buildTreeFromAST`:
```go
// Line 34 — newContext is the {{with}} value, not the original root data
return buildTreeFromAST(node.List, newContext, keyGen, ctx)
```
Go's template engine tracks the root data (`$`) separately from the current dot (`.`). When `$` is used inside a `with` block, it should resolve against the original root, but `handleWithNode` loses the root reference by passing only `newContext`.
The var-aware path (`handleWithNodeWithVars`) handles this correctly by maintaining `varCtx.parent` as the root data. The non-var path has no equivalent mechanism.
## Affected Code
- `internal/parse/with.go:34` — `handleWithNode()` loses root data reference
- The var-aware path (`handleWithNodeWithVars`, line 63) correctly sets `parent: varCtx.parent`
## Suggested Fix
Pass the original `data` alongside `newContext` so that `$` can resolve against the root. Options:
1. **Always use the var-aware path** for `with` blocks (convert to `varContext` even when no explicit variables exist)
2. **Thread root data** through the non-var `buildTreeFromAST` call chain so `$` resolves correctly
3. **Wrap the with body** in a `varContext` with `parent = data` and `dot = newContext` (minimal change)
## Discovered During
Found by the standard library parity tests added in the PR for #176. The test is currently commented out in `internal/parse/stdlib_parity_test.go` with a note referencing this as a pre-existing divergence.
## Impact
Any template using `$.Field` inside a `{{with}}` block without other variable declarations will silently produce empty/wrong values for the root field access. This is a correctness bug — the output diverges from what Go's standard template engine produces.
Root variable ($) resolution inside {{with}} blocks
This ad-hoc issue report has been moved into the project documentation to keep the repository root clean and make the bug easier to discover and maintain over time.
Please see the canonical write-up in the docs/ hierarchy (for example, a file such as:
docs/known-issues/root-var-with-block.md
or the corresponding tracked GitHub issue referenced from that document.

Copilot uses AI. Check for mistakes.
handleWithNode() was passing the narrowed with-context as data to
buildTreeFromAST, causing $ to resolve against the narrowed context
instead of the original root data. This diverged from Go's stdlib
behavior where $ always refers to the root data passed to Execute().

Fix: use the var-aware path (buildTreeFromASTWithVars) with a varContext
that preserves the original data as parent and uses the narrowed context
as dot. This matches what handleWithNodeWithVars already does correctly.

https://claude.ai/code/session_011iXr6f9X1MT3p2N6QpWGdu
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 11, 2026

✅ Performance Benchmark Results

Status: No significant regressions detected

Benchmark Comparison
                                               │ testdata/benchmarks/baseline.txt │  current-bench.txt   │
                                               │              sec/op              │    sec/op      vs base   │
DispatchWithState_Cached-8                                           348.1n ± ∞ ¹
E2EUserJourney-8                                                     5.640m ± ∞ ¹
E2ETodoApp-8                                                         459.6µ ± ∞ ¹
E2ERangeOperations/add-items-8                                       192.9µ ± ∞ ¹
E2ERangeOperations/remove-items-8                                    87.29µ ± ∞ ¹
E2ERangeOperations/reorder-items-8                                   155.0µ ± ∞ ¹
E2ERangeOperations/update-items-8                                    144.7µ ± ∞ ¹
E2EMultipleSessions/sessions-1-8                                     53.10µ ± ∞ ¹
E2EMultipleSessions/sessions-10-8                                    914.7µ ± ∞ ¹
E2EMultipleSessions/sessions-100-8                                   10.76m ± ∞ ¹
SpecificationCompliance-8                                            42.17µ ± ∞ ¹
ErrorPaths/invalid-template-syntax-8                                 14.19m ± ∞ ¹
ErrorPaths/missing-field-8                                           35.70µ ± ∞ ¹
ErrorPaths/nil-data-8                                                39.44µ ± ∞ ¹
ErrorPaths/empty-template-8                                          14.93m ± ∞ ¹
TemplateExecute/initial-render-8                                     15.43m ± ∞ ¹
TemplateExecute/subsequent-render-8                                  65.91µ ± ∞ ¹
TemplateExecuteUpdates/no-changes-8                                  30.77µ ± ∞ ¹
TemplateExecuteUpdates/small-update-8                                82.31µ ± ∞ ¹
TemplateExecuteUpdates/large-update-8                                180.4µ ± ∞ ¹
TemplateComplexity/simple-fields-8                                   76.31µ ± ∞ ¹
TemplateComplexity/with-conditionals-8                               51.50µ ± ∞ ¹
TemplateComplexity/with-ranges-8                                     73.13µ ± ∞ ¹
TemplateComplexity/deeply-nested-8                                   64.11µ ± ∞ ¹
TemplateConcurrent/goroutines-1-8                                    73.77µ ± ∞ ¹
TemplateConcurrent/goroutines-10-8                                   39.89µ ± ∞ ¹
TemplateConcurrent/goroutines-100-8                                  32.98µ ± ∞ ¹
Template_Execute-8                                                   32.87µ ± ∞ ¹
Template_ExecuteUpdates-8                                            40.92µ ± ∞ ¹
TreeNodeCreation/flat-8                                              622.9n ± ∞ ¹
TreeNodeCreation/nested-small-8                                      4.762µ ± ∞ ¹
TreeNodeCreation/nested-medium-8                                     14.81µ ± ∞ ¹
TreeNodeCreation/nested-large-8                                      45.58µ ± ∞ ¹
TreeNodeMarshalJSON/flat-8                                           3.983µ ± ∞ ¹
TreeNodeMarshalJSON/nested-small-8                                   46.73µ ± ∞ ¹
TreeNodeMarshalJSON/nested-medium-8                                  169.3µ ± ∞ ¹
WrapperInjection/full-html-8                                         2.855µ ± ∞ ¹
WrapperInjection/fragment-8                                          1.850µ ± ∞ ¹
ExtractWrapperContent-8                                              1.795µ ± ∞ ¹
ContextOperations/with-statics-8                                     3.551µ ± ∞ ¹
ContextOperations/without-statics-8                                  3.589µ ± ∞ ¹
TreeNodeClone/flat-8                                                 409.8n ± ∞ ¹
TreeNodeClone/nested-small-8                                         3.774µ ± ∞ ¹
TreeNodeClone/nested-medium-8                                        13.61µ ± ∞ ¹
TreeNodeToMap/flat-8                                                 747.6n ± ∞ ¹
TreeNodeToMap/nested-small-8                                         5.291µ ± ∞ ¹
TreeNodeToMap/nested-medium-8                                        15.69µ ± ∞ ¹
GenerateRandomID-8                                                   93.83n ± ∞ ¹
CalculateStructureFingerprint_Small-8                                408.3n ± ∞ ¹
CalculateStructureFingerprint_Medium-8                               9.164µ ± ∞ ¹
CalculateStructureFingerprint_Large-8                                91.89µ ± ∞ ¹
CalculateStructureFingerprint_DeepNested-8                           6.889µ ± ∞ ¹
CalculateStructureFingerprint_Range100-8                             435.3n ± ∞ ¹
CalculateStructureFingerprint_Range1000-8                            481.4n ± ∞ ¹
ExecuteTemplateWithContext_Struct-8                                  2.134µ ± ∞ ¹
ExecuteTemplateWithContext_Map-8                                     2.139µ ± ∞ ¹
CompareTreesNoChanges-8                                              700.2n ± ∞ ¹
CompareTreesSmallChange-8                                            202.5n ± ∞ ¹
CompareTreesLargeChange/10-8                                         1.025µ ± ∞ ¹
CompareTreesLargeChange/100-8                                        10.27µ ± ∞ ¹
CompareTreesLargeChange/1000-8                                       131.4µ ± ∞ ¹
RangeDiffUpdate-8                                                    7.527µ ± ∞ ¹
RangeDiffInsert-8                                                    7.826µ ± ∞ ¹
RangeDiffRemove-8                                                    9.394µ ± ∞ ¹
PrepareTreeForClient/with-statics-8                                  3.012n ± ∞ ¹
PrepareTreeForClient/without-statics-8                               3.905µ ± ∞ ¹
ClientNeedsStatics_SameStructure-8                                   4.672n ± ∞ ¹
ClientNeedsStatics_DifferentStructure-8                              4.298n ± ∞ ¹
ClientNeedsStatics_DeepNested-8                                      4.557n ± ∞ ¹
ClientNeedsStatics_Range-8                                           4.476n ± ∞ ¹
ClientNeedsStatics_NilOld-8                                          2.211n ± ∞ ¹
WireSize_WithStatics-8                                               9.471µ ± ∞ ¹
WireSize_WithoutStatics-8                                            7.653µ ± ∞ ¹
WireSizeComparison/small_5_with_statics-8                            1.771µ ± ∞ ¹
WireSizeComparison/small_5_without_statics-8                         1.517µ ± ∞ ¹
WireSizeComparison/medium_20_with_statics-8                          7.436µ ± ∞ ¹
WireSizeComparison/medium_20_without_statics-8                       6.296µ ± ∞ ¹
WireSizeComparison/large_100_with_statics-8                          28.29µ ± ∞ ¹
WireSizeComparison/large_100_without_statics-8                       31.39µ ± ∞ ¹
Parse/simple-8                                                       1.965µ ± ∞ ¹
Parse/conditional-8                                                  3.245µ ± ∞ ¹
Parse/range-8                                                        3.030µ ± ∞ ¹
Parse/nested-8                                                       3.735µ ± ∞ ¹
Parse/complex-8                                                      8.850µ ± ∞ ¹
BuildTree/simple-8                                                   7.252µ ± ∞ ¹
BuildTree/conditional-true-8                                         14.44µ ± ∞ ¹
BuildTree/conditional-false-8                                        8.526µ ± ∞ ¹
BuildTree/range-small-8                                              107.3µ ± ∞ ¹
BuildTreeScale/small-10-8                                            83.82µ ± ∞ ¹
BuildTreeScale/medium-100-8                                          873.8µ ± ∞ ¹
BuildTreeScale/large-1000-8                                          9.671m ± ∞ ¹
NodeRender-8                                                         111.1n ± ∞ ¹
TreeToHTML/simple-8                                                  175.4n ± ∞ ¹
TreeToHTML/nested-8                                                  327.8n ± ∞ ¹
TreeToHTML/with-ranges-8                                             298.4n ± ∞ ¹
TreeToHTMLScale/small-10-8                                           1.761µ ± ∞ ¹
TreeToHTMLScale/medium-100-8                                         12.13µ ± ∞ ¹
TreeToHTMLScale/large-1000-8                                         149.4µ ± ∞ ¹
IsVoidElement-8                                                      111.7n ± ∞ ¹
NodeRenderComplex-8                                                  242.9n ± ∞ ¹
MinifyHTML-8                                                         1.274µ ± ∞ ¹
ParseActionFromHTTP-8                                                3.904µ ± ∞ ¹
ParseActionFromWebSocket-8                                           1.209µ ± ∞ ¹
PrepareUpdate/without-errors-8                                      0.3851n ± ∞ ¹
PrepareUpdate/with-errors-8                                          25.40n ± ∞ ¹
SerializeUpdate-8                                                    1.364µ ± ∞ ¹
PrepareAndSerialize/simple-update-8                                  1.055µ ± ∞ ¹
PrepareAndSerialize/with-metadata-8                                  1.411µ ± ∞ ¹
ParseActionScale/small-http-8                                        2.071µ ± ∞ ¹
ParseActionScale/small-ws-8                                          794.7n ± ∞ ¹
ParseActionScale/medium-http-8                                       4.597µ ± ∞ ¹
ParseActionScale/medium-ws-8                                         1.745µ ± ∞ ¹
ParseActionScale/large-http-8                                        4.441µ ± ∞ ¹
ParseActionScale/large-ws-8                                          2.673µ ± ∞ ¹
SerializeUpdateScale/simple-8                                        1.215µ ± ∞ ¹
SerializeUpdateScale/nested-8                                        2.110µ ± ∞ ¹
SerializeUpdateScale/multiple-fields-8                               1.592µ ± ∞ ¹
ConcurrentConnections/:00_connections-8                              153.7µ ± ∞ ¹
RegisterUnregister-8                                                 2.410µ ± ∞ ¹
GetByGroup-8                                                         326.2n ± ∞ ¹
CloseConnection-8                                                    3.039µ ± ∞ ¹
MemoryUsage-8                                                        70.54µ ± ∞ ¹
BroadcastToGroup-8                                                   28.09µ ± ∞ ¹
ConcurrentRegistrations-8                                            3.421µ ± ∞ ¹
GetByGroupExcept-8                                                   466.5n ± ∞ ¹
DispatchWithState_Cached-4                                                           422.3n ± ∞ ¹
E2EUserJourney-4                                                                     3.591m ± ∞ ¹
E2ETodoApp-4                                                                         230.2µ ± ∞ ¹
E2ERangeOperations/add-items-4                                                       127.9µ ± ∞ ¹
E2ERangeOperations/remove-items-4                                                    70.20µ ± ∞ ¹
E2ERangeOperations/reorder-items-4                                                   90.78µ ± ∞ ¹
E2ERangeOperations/update-items-4                                                    91.10µ ± ∞ ¹
E2EMultipleSessions/sessions-1-4                                                     35.62µ ± ∞ ¹
E2EMultipleSessions/sessions-10-4                                                    366.4µ ± ∞ ¹
E2EMultipleSessions/sessions-100-4                                                   3.610m ± ∞ ¹
SpecificationCompliance-4                                                            31.87µ ± ∞ ¹
ErrorPaths/invalid-template-syntax-4                                                 1.403m ± ∞ ¹
ErrorPaths/missing-field-4                                                           34.00µ ± ∞ ¹
ErrorPaths/nil-data-4                                                                32.37µ ± ∞ ¹
ErrorPaths/empty-template-4                                                          1.441m ± ∞ ¹
TemplateExecute/initial-render-4                                                     1.484m ± ∞ ¹
TemplateExecute/subsequent-render-4                                                  36.09µ ± ∞ ¹
TemplateExecuteUpdates/no-changes-4                                                  34.17µ ± ∞ ¹
TemplateExecuteUpdates/small-update-4                                                34.14µ ± ∞ ¹
TemplateExecuteUpdates/large-update-4                                                114.3µ ± ∞ ¹
TemplateComplexity/simple-fields-4                                                   71.30µ ± ∞ ¹
TemplateComplexity/with-conditionals-4                                               57.01µ ± ∞ ¹
TemplateComplexity/with-ranges-4                                                     92.96µ ± ∞ ¹
TemplateComplexity/deeply-nested-4                                                   81.12µ ± ∞ ¹
TemplateConcurrent/goroutines-1-4                                                    46.37µ ± ∞ ¹
TemplateConcurrent/goroutines-10-4                                                   54.99µ ± ∞ ¹
TemplateConcurrent/goroutines-100-4                                                  57.61µ ± ∞ ¹
Template_Execute-4                                                                   34.44µ ± ∞ ¹
Template_ExecuteUpdates-4                                                            33.63µ ± ∞ ¹
TreeNodeCreation/flat-4                                                              1.076µ ± ∞ ¹
TreeNodeCreation/nested-small-4                                                      8.199µ ± ∞ ¹
TreeNodeCreation/nested-medium-4                                                     25.21µ ± ∞ ¹
TreeNodeCreation/nested-large-4                                                      76.61µ ± ∞ ¹
TreeNodeMarshalJSON/flat-4                                                           7.082µ ± ∞ ¹
TreeNodeMarshalJSON/nested-small-4                                                   75.07µ ± ∞ ¹
TreeNodeMarshalJSON/nested-medium-4                                                  270.7µ ± ∞ ¹
WrapperInjection/full-html-4                                                         5.531µ ± ∞ ¹
WrapperInjection/fragment-4                                                          3.603µ ± ∞ ¹
ExtractWrapperContent-4                                                              3.517µ ± ∞ ¹
ContextOperations/with-statics-4                                                     5.980µ ± ∞ ¹
ContextOperations/without-statics-4                                                  5.978µ ± ∞ ¹
TreeNodeClone/flat-4                                                                 698.4n ± ∞ ¹
TreeNodeClone/nested-small-4                                                         5.978µ ± ∞ ¹
TreeNodeClone/nested-medium-4                                                        18.48µ ± ∞ ¹
TreeNodeToMap/flat-4                                                                 1.062µ ± ∞ ¹
TreeNodeToMap/nested-small-4                                                         7.777µ ± ∞ ¹
TreeNodeToMap/nested-medium-4                                                        23.89µ ± ∞ ¹
GenerateRandomID-4                                                                   99.48n ± ∞ ¹
CalculateStructureFingerprint_Small-4                                                635.8n ± ∞ ¹
CalculateStructureFingerprint_Medium-4                                               13.47µ ± ∞ ¹
CalculateStructureFingerprint_Large-4                                                135.9µ ± ∞ ¹
CalculateStructureFingerprint_DeepNested-4                                           11.19µ ± ∞ ¹
CalculateStructureFingerprint_Range100-4                                             640.5n ± ∞ ¹
CalculateStructureFingerprint_Range1000-4                                            650.0n ± ∞ ¹
ExecuteTemplateWithContext_Struct-4                                                  3.386µ ± ∞ ¹
ExecuteTemplateWithContext_Map-4                                                     3.686µ ± ∞ ¹
CompareTreesNoChanges-4                                                              680.0n ± ∞ ¹
CompareTreesSmallChange-4                                                            283.6n ± ∞ ¹
CompareTreesLargeChange/10-4                                                         1.510µ ± ∞ ¹
CompareTreesLargeChange/100-4                                                        14.66µ ± ∞ ¹
CompareTreesLargeChange/1000-4                                                       193.3µ ± ∞ ¹
RangeDiffUpdate-4                                                                    8.820µ ± ∞ ¹
RangeDiffInsert-4                                                                    8.926µ ± ∞ ¹
RangeDiffRemove-4                                                                    8.890µ ± ∞ ¹
PrepareTreeForClient/with-statics-4                                                  2.397n ± ∞ ¹
PrepareTreeForClient/without-statics-4                                               4.666µ ± ∞ ¹
ClientNeedsStatics_SameStructure-4                                                   5.926n ± ∞ ¹
ClientNeedsStatics_DifferentStructure-4                                              5.604n ± ∞ ¹
ClientNeedsStatics_DeepNested-4                                                      5.613n ± ∞ ¹
ClientNeedsStatics_Range-4                                                           5.919n ± ∞ ¹
ClientNeedsStatics_NilOld-4                                                          2.182n ± ∞ ¹
WireSize_WithStatics-4                                                               9.751µ ± ∞ ¹
WireSize_WithoutStatics-4                                                            10.28µ ± ∞ ¹
WireSizeComparison/small_5_with_statics-4                                            2.683µ ± ∞ ¹
WireSizeComparison/small_5_without_statics-4                                         2.367µ ± ∞ ¹
WireSizeComparison/medium_20_with_statics-4                                          9.716µ ± ∞ ¹
WireSizeComparison/medium_20_without_statics-4                                       10.26µ ± ∞ ¹
WireSizeComparison/large_100_with_statics-4                                          45.26µ ± ∞ ¹
WireSizeComparison/large_100_without_statics-4                                       50.27µ ± ∞ ¹
Parse/simple-4                                                                       3.248µ ± ∞ ¹
Parse/conditional-4                                                                  5.865µ ± ∞ ¹
Parse/range-4                                                                        5.150µ ± ∞ ¹
Parse/nested-4                                                                       6.624µ ± ∞ ¹
Parse/complex-4                                                                      12.92µ ± ∞ ¹
BuildTree/simple-4                                                                   12.02µ ± ∞ ¹
BuildTree/conditional-true-4                                                         23.49µ ± ∞ ¹
BuildTree/conditional-false-4                                                        11.37µ ± ∞ ¹
BuildTree/range-small-4                                                              43.38µ ± ∞ ¹
BuildTreeScale/small-10-4                                                            138.7µ ± ∞ ¹
BuildTreeScale/medium-100-4                                                          1.375m ± ∞ ¹
BuildTreeScale/large-1000-4                                                          15.39m ± ∞ ¹
NodeRender-4                                                                         99.85n ± ∞ ¹
TreeToHTML/simple-4                                                                  218.5n ± ∞ ¹
TreeToHTML/nested-4                                                                  356.0n ± ∞ ¹
TreeToHTML/with-ranges-4                                                             335.9n ± ∞ ¹
TreeToHTMLScale/small-10-4                                                           1.316µ ± ∞ ¹
TreeToHTMLScale/medium-100-4                                                         11.25µ ± ∞ ¹
TreeToHTMLScale/large-1000-4                                                         116.1µ ± ∞ ¹
IsVoidElement-4                                                                      102.2n ± ∞ ¹
NodeRenderComplex-4                                                                  295.4n ± ∞ ¹
MinifyHTML-4                                                                         1.609µ ± ∞ ¹
ParseActionFromHTTP-4                                                                3.769µ ± ∞ ¹
ParseActionFromWebSocket-4                                                           1.367µ ± ∞ ¹
PrepareUpdate/without-errors-4                                                      0.3115n ± ∞ ¹
PrepareUpdate/with-errors-4                                                          26.28n ± ∞ ¹
SerializeUpdate-4                                                                    1.626µ ± ∞ ¹
PrepareAndSerialize/simple-update-4                                                  1.668µ ± ∞ ¹
PrepareAndSerialize/with-metadata-4                                                  2.124µ ± ∞ ¹
ParseActionScale/small-http-4                                                        3.756µ ± ∞ ¹
ParseActionScale/small-ws-4                                                          1.296µ ± ∞ ¹
ParseActionScale/medium-http-4                                                       4.920µ ± ∞ ¹
ParseActionScale/medium-ws-4                                                         2.362µ ± ∞ ¹
ParseActionScale/large-http-4                                                        7.342µ ± ∞ ¹
ParseActionScale/large-ws-4                                                          4.610µ ± ∞ ¹
SerializeUpdateScale/simple-4                                                        1.628µ ± ∞ ¹
SerializeUpdateScale/nested-4                                                        3.409µ ± ∞ ¹
SerializeUpdateScale/multiple-fields-4                                               2.663µ ± ∞ ¹
ConcurrentConnections/100_connections-4                                              25.98µ ± ∞ ¹
ConcurrentConnections/:00_connections-4                                              275.6µ ± ∞ ¹
RegisterUnregister-4                                                                 4.302µ ± ∞ ¹
GetByGroup-4                                                                         456.5n ± ∞ ¹
CloseConnection-4                                                                    6.634µ ± ∞ ¹
MemoryUsage-4                                                                        70.76µ ± ∞ ¹
BroadcastToGroup-4                                                                   25.03µ ± ∞ ¹
ConcurrentRegistrations-4                                                            4.381µ ± ∞ ¹
GetByGroupExcept-4                                                                   541.1n ± ∞ ¹
geomean                                                              6.025µ          6.934µ        ? ² ³
¹ need >= 6 samples for confidence interval at level 0.95
² benchmark set differs from baseline; geomeans may not be comparable
³ ratios must be >0 to compute geomean

                                               │ testdata/benchmarks/baseline.txt │   current-bench.txt    │
                                               │               B/op               │     B/op       vs base │
DispatchWithState_Cached-8                                            80.00 ± ∞ ¹
E2EUserJourney-8                                                    2.784Mi ± ∞ ¹
E2ETodoApp-8                                                        141.8Ki ± ∞ ¹
E2ERangeOperations/add-items-8                                      82.73Ki ± ∞ ¹
E2ERangeOperations/remove-items-8                                   44.81Ki ± ∞ ¹
E2ERangeOperations/reorder-items-8                                  58.43Ki ± ∞ ¹
E2ERangeOperations/update-items-8                                   58.43Ki ± ∞ ¹
E2EMultipleSessions/sessions-1-8                                    28.50Ki ± ∞ ¹
E2EMultipleSessions/sessions-10-8                                   284.8Ki ± ∞ ¹
E2EMultipleSessions/sessions-100-8                                  2.781Mi ± ∞ ¹
SpecificationCompliance-8                                           27.58Ki ± ∞ ¹
ErrorPaths/invalid-template-syntax-8                                2.809Mi ± ∞ ¹
ErrorPaths/missing-field-8                                          28.27Ki ± ∞ ¹
ErrorPaths/nil-data-8                                               27.41Ki ± ∞ ¹
ErrorPaths/empty-template-8                                         2.837Mi ± ∞ ¹
TemplateExecute/initial-render-8                                    2.851Mi ± ∞ ¹
TemplateExecute/subsequent-render-8                                 28.62Ki ± ∞ ¹
TemplateExecuteUpdates/no-changes-8                                 27.66Ki ± ∞ ¹
TemplateExecuteUpdates/small-update-8                               27.66Ki ± ∞ ¹
TemplateExecuteUpdates/large-update-8                               76.78Ki ± ∞ ¹
TemplateComplexity/simple-fields-8                                  51.14Ki ± ∞ ¹
TemplateComplexity/with-conditionals-8                              40.57Ki ± ∞ ¹
TemplateComplexity/with-ranges-8                                    59.35Ki ± ∞ ¹
TemplateComplexity/deeply-nested-8                                  49.01Ki ± ∞ ¹
TemplateConcurrent/goroutines-1-8                                   28.62Ki ± ∞ ¹
TemplateConcurrent/goroutines-10-8                                  28.62Ki ± ∞ ¹
TemplateConcurrent/goroutines-100-8                                 28.62Ki ± ∞ ¹
Template_Execute-8                                                  28.82Ki ± ∞ ¹
Template_ExecuteUpdates-8                                           27.80Ki ± ∞ ¹
TreeNodeCreation/flat-8                                             1.281Ki ± ∞ ¹
TreeNodeCreation/nested-small-8                                     10.32Ki ± ∞ ¹
TreeNodeCreation/nested-medium-8                                    31.42Ki ± ∞ ¹
TreeNodeCreation/nested-large-8                                     94.72Ki ± ∞ ¹
TreeNodeMarshalJSON/flat-8                                          3.472Ki ± ∞ ¹
TreeNodeMarshalJSON/nested-small-8                                  27.28Ki ± ∞ ¹
TreeNodeMarshalJSON/nested-medium-8                                 89.09Ki ± ∞ ¹
WrapperInjection/full-html-8                                        6.930Ki ± ∞ ¹
WrapperInjection/fragment-8                                         6.266Ki ± ∞ ¹
ExtractWrapperContent-8                                             5.883Ki ± ∞ ¹
ContextOperations/with-statics-8                                    9.047Ki ± ∞ ¹
ContextOperations/without-statics-8                                 9.047Ki ± ∞ ¹
TreeNodeClone/flat-8                                                1.047Ki ± ∞ ¹
TreeNodeClone/nested-small-8                                        9.047Ki ± ∞ ¹
TreeNodeClone/nested-medium-8                                       27.61Ki ± ∞ ¹
TreeNodeToMap/flat-8                                                2.109Ki ± ∞ ¹
TreeNodeToMap/nested-small-8                                        14.06Ki ± ∞ ¹
TreeNodeToMap/nested-medium-8                                       42.54Ki ± ∞ ¹
GenerateRandomID-8                                                    24.00 ± ∞ ¹
CalculateStructureFingerprint_Small-8                                 192.0 ± ∞ ¹
CalculateStructureFingerprint_Medium-8                              1.898Ki ± ∞ ¹
CalculateStructureFingerprint_Large-8                               24.60Ki ± ∞ ¹
CalculateStructureFingerprint_DeepNested-8                          2.078Ki ± ∞ ¹
CalculateStructureFingerprint_Range100-8                              208.0 ± ∞ ¹
CalculateStructureFingerprint_Range1000-8                             208.0 ± ∞ ¹
ExecuteTemplateWithContext_Struct-8                                 1.018Ki ± ∞ ¹
ExecuteTemplateWithContext_Map-8                                    1.189Ki ± ∞ ¹
CompareTreesNoChanges-8                                               128.0 ± ∞ ¹
CompareTreesSmallChange-8                                             416.0 ± ∞ ¹
CompareTreesLargeChange/10-8                                        1.008Ki ± ∞ ¹
CompareTreesLargeChange/100-8                                       9.227Ki ± ∞ ¹
CompareTreesLargeChange/1000-8                                      156.7Ki ± ∞ ¹
RangeDiffUpdate-8                                                   18.50Ki ± ∞ ¹
RangeDiffInsert-8                                                   18.50Ki ± ∞ ¹
RangeDiffRemove-8                                                   18.50Ki ± ∞ ¹
PrepareTreeForClient/with-statics-8                                   0.000 ± ∞ ¹
PrepareTreeForClient/without-statics-8                              4.914Ki ± ∞ ¹
ClientNeedsStatics_SameStructure-8                                    0.000 ± ∞ ¹
ClientNeedsStatics_DifferentStructure-8                               0.000 ± ∞ ¹
ClientNeedsStatics_DeepNested-8                                       0.000 ± ∞ ¹
ClientNeedsStatics_Range-8                                            0.000 ± ∞ ¹
ClientNeedsStatics_NilOld-8                                           0.000 ± ∞ ¹
WireSize_WithStatics-8                                              4.271Ki ± ∞ ¹
WireSize_WithoutStatics-8                                           5.382Ki ± ∞ ¹
WireSizeComparison/small_5_with_statics-8                            1017.0 ± ∞ ¹
WireSizeComparison/small_5_without_statics-8                        1.220Ki ± ∞ ¹
WireSizeComparison/medium_20_with_statics-8                         4.271Ki ± ∞ ¹
WireSizeComparison/medium_20_without_statics-8                      5.382Ki ± ∞ ¹
WireSizeComparison/large_100_with_statics-8                         18.82Ki ± ∞ ¹
WireSizeComparison/large_100_without_statics-8                      23.72Ki ± ∞ ¹
Parse/simple-8                                                      3.656Ki ± ∞ ¹
Parse/conditional-8                                                 4.398Ki ± ∞ ¹
Parse/range-8                                                       4.234Ki ± ∞ ¹
Parse/nested-8                                                      4.609Ki ± ∞ ¹
Parse/complex-8                                                     6.906Ki ± ∞ ¹
BuildTree/simple-8                                                  10.98Ki ± ∞ ¹
BuildTree/conditional-true-8                                        21.42Ki ± ∞ ¹
BuildTree/conditional-false-8                                       10.74Ki ± ∞ ¹
BuildTree/range-small-8                                             36.08Ki ± ∞ ¹
BuildTreeScale/small-10-8                                           117.2Ki ± ∞ ¹
BuildTreeScale/medium-100-8                                         1.135Mi ± ∞ ¹
BuildTreeScale/large-1000-8                                         11.36Mi ± ∞ ¹
NodeRender-8                                                          56.00 ± ∞ ¹
TreeToHTML/simple-8                                                   64.00 ± ∞ ¹
TreeToHTML/nested-8                                                   144.0 ± ∞ ¹
TreeToHTML/with-ranges-8                                              72.00 ± ∞ ¹
TreeToHTMLScale/small-10-8                                            544.0 ± ∞ ¹
TreeToHTMLScale/medium-100-8                                        3.634Ki ± ∞ ¹
TreeToHTMLScale/large-1000-8                                        65.43Ki ± ∞ ¹
IsVoidElement-8                                                       0.000 ± ∞ ¹
NodeRenderComplex-8                                                   248.0 ± ∞ ¹
MinifyHTML-8                                                        1.641Ki ± ∞ ¹
ParseActionFromHTTP-8                                               6.799Ki ± ∞ ¹
ParseActionFromWebSocket-8                                            656.0 ± ∞ ¹
PrepareUpdate/without-errors-8                                        0.000 ± ∞ ¹
PrepareUpdate/with-errors-8                                           32.00 ± ∞ ¹
SerializeUpdate-8                                                     648.0 ± ∞ ¹
PrepareAndSerialize/simple-update-8                                   672.0 ± ∞ ¹
PrepareAndSerialize/with-metadata-8                                   848.0 ± ∞ ¹
ParseActionScale/small-http-8                                       6.791Ki ± ∞ ¹
ParseActionScale/small-ws-8                                           648.0 ± ∞ ¹
ParseActionScale/medium-http-8                                      6.955Ki ± ∞ ¹
ParseActionScale/medium-ws-8                                          760.0 ± ∞ ¹
ParseActionScale/large-http-8                                       7.885Ki ± ∞ ¹
ParseActionScale/large-ws-8                                         1.578Ki ± ∞ ¹
SerializeUpdateScale/simple-8                                         648.0 ± ∞ ¹
SerializeUpdateScale/nested-8                                       1.282Ki ± ∞ ¹
SerializeUpdateScale/multiple-fields-8                                968.0 ± ∞ ¹
ConcurrentConnections/:00_connections-8                             35.16Ki ± ∞ ¹
RegisterUnregister-8                                                1.182Ki ± ∞ ¹
GetByGroup-8                                                          896.0 ± ∞ ¹
CloseConnection-8                                                     253.0 ± ∞ ¹
MemoryUsage-8                                                       95.80Ki ± ∞ ¹
BroadcastToGroup-8                                                  4.000Ki ± ∞ ¹
ConcurrentRegistrations-8                                           1.219Ki ± ∞ ¹
GetByGroupExcept-8                                                    896.0 ± ∞ ¹
DispatchWithState_Cached-4                                                            80.00 ± ∞ ¹
E2EUserJourney-4                                                                    2.788Mi ± ∞ ¹
E2ETodoApp-4                                                                        142.0Ki ± ∞ ¹
E2ERangeOperations/add-items-4                                                      82.85Ki ± ∞ ¹
E2ERangeOperations/remove-items-4                                                   44.88Ki ± ∞ ¹
E2ERangeOperations/reorder-items-4                                                  58.52Ki ± ∞ ¹
E2ERangeOperations/update-items-4                                                   58.52Ki ± ∞ ¹
E2EMultipleSessions/sessions-1-4                                                    28.54Ki ± ∞ ¹
E2EMultipleSessions/sessions-10-4                                                   285.2Ki ± ∞ ¹
E2EMultipleSessions/sessions-100-4                                                  2.783Mi ± ∞ ¹
SpecificationCompliance-4                                                           27.58Ki ± ∞ ¹
ErrorPaths/invalid-template-syntax-4                                                300.2Ki ± ∞ ¹
ErrorPaths/missing-field-4                                                          28.27Ki ± ∞ ¹
ErrorPaths/nil-data-4                                                               27.41Ki ± ∞ ¹
ErrorPaths/empty-template-4                                                         327.2Ki ± ∞ ¹
TemplateExecute/initial-render-4                                                    341.1Ki ± ∞ ¹
TemplateExecute/subsequent-render-4                                                 28.62Ki ± ∞ ¹
TemplateExecuteUpdates/no-changes-4                                                 27.66Ki ± ∞ ¹
TemplateExecuteUpdates/small-update-4                                               27.66Ki ± ∞ ¹
TemplateExecuteUpdates/large-update-4                                               76.79Ki ± ∞ ¹
TemplateComplexity/simple-fields-4                                                  51.14Ki ± ∞ ¹
TemplateComplexity/with-conditionals-4                                              40.57Ki ± ∞ ¹
TemplateComplexity/with-ranges-4                                                    59.36Ki ± ∞ ¹
TemplateComplexity/deeply-nested-4                                                  48.95Ki ± ∞ ¹
TemplateConcurrent/goroutines-1-4                                                   28.62Ki ± ∞ ¹
TemplateConcurrent/goroutines-10-4                                                  28.62Ki ± ∞ ¹
TemplateConcurrent/goroutines-100-4                                                 28.63Ki ± ∞ ¹
Template_Execute-4                                                                  28.82Ki ± ∞ ¹
Template_ExecuteUpdates-4                                                           27.80Ki ± ∞ ¹
TreeNodeCreation/flat-4                                                             1.281Ki ± ∞ ¹
TreeNodeCreation/nested-small-4                                                     10.31Ki ± ∞ ¹
TreeNodeCreation/nested-medium-4                                                    31.41Ki ± ∞ ¹
TreeNodeCreation/nested-large-4                                                     94.71Ki ± ∞ ¹
TreeNodeMarshalJSON/flat-4                                                          3.470Ki ± ∞ ¹
TreeNodeMarshalJSON/nested-small-4                                                  27.27Ki ± ∞ ¹
TreeNodeMarshalJSON/nested-medium-4                                                 88.99Ki ± ∞ ¹
WrapperInjection/full-html-4                                                        6.930Ki ± ∞ ¹
WrapperInjection/fragment-4                                                         6.266Ki ± ∞ ¹
ExtractWrapperContent-4                                                             5.883Ki ± ∞ ¹
ContextOperations/with-statics-4                                                    9.047Ki ± ∞ ¹
ContextOperations/without-statics-4                                                 9.047Ki ± ∞ ¹
TreeNodeClone/flat-4                                                                1.047Ki ± ∞ ¹
TreeNodeClone/nested-small-4                                                        9.047Ki ± ∞ ¹
TreeNodeClone/nested-medium-4                                                       27.61Ki ± ∞ ¹
TreeNodeToMap/flat-4                                                                2.109Ki ± ∞ ¹
TreeNodeToMap/nested-small-4                                                        14.06Ki ± ∞ ¹
TreeNodeToMap/nested-medium-4                                                       42.54Ki ± ∞ ¹
GenerateRandomID-4                                                                    24.00 ± ∞ ¹
CalculateStructureFingerprint_Small-4                                                 192.0 ± ∞ ¹
CalculateStructureFingerprint_Medium-4                                              1.898Ki ± ∞ ¹
CalculateStructureFingerprint_Large-4                                               24.60Ki ± ∞ ¹
CalculateStructureFingerprint_DeepNested-4                                          2.078Ki ± ∞ ¹
CalculateStructureFingerprint_Range100-4                                              208.0 ± ∞ ¹
CalculateStructureFingerprint_Range1000-4                                             208.0 ± ∞ ¹
ExecuteTemplateWithContext_Struct-4                                                 1.018Ki ± ∞ ¹
ExecuteTemplateWithContext_Map-4                                                    1.189Ki ± ∞ ¹
CompareTreesNoChanges-4                                                               128.0 ± ∞ ¹
CompareTreesSmallChange-4                                                             416.0 ± ∞ ¹
CompareTreesLargeChange/10-4                                                        1.008Ki ± ∞ ¹
CompareTreesLargeChange/100-4                                                       9.227Ki ± ∞ ¹
CompareTreesLargeChange/1000-4                                                      156.7Ki ± ∞ ¹
RangeDiffUpdate-4                                                                   18.50Ki ± ∞ ¹
RangeDiffInsert-4                                                                   18.50Ki ± ∞ ¹
RangeDiffRemove-4                                                                   18.50Ki ± ∞ ¹
PrepareTreeForClient/with-statics-4                                                   0.000 ± ∞ ¹
PrepareTreeForClient/without-statics-4                                              4.914Ki ± ∞ ¹
ClientNeedsStatics_SameStructure-4                                                    0.000 ± ∞ ¹
ClientNeedsStatics_DifferentStructure-4                                               0.000 ± ∞ ¹
ClientNeedsStatics_DeepNested-4                                                       0.000 ± ∞ ¹
ClientNeedsStatics_Range-4                                                            0.000 ± ∞ ¹
ClientNeedsStatics_NilOld-4                                                           0.000 ± ∞ ¹
WireSize_WithStatics-4                                                              4.269Ki ± ∞ ¹
WireSize_WithoutStatics-4                                                           5.378Ki ± ∞ ¹
WireSizeComparison/small_5_with_statics-4                                            1016.0 ± ∞ ¹
WireSizeComparison/small_5_without_statics-4                                        1.219Ki ± ∞ ¹
WireSizeComparison/medium_20_with_statics-4                                         4.268Ki ± ∞ ¹
WireSizeComparison/medium_20_without_statics-4                                      5.378Ki ± ∞ ¹
WireSizeComparison/large_100_with_statics-4                                         18.81Ki ± ∞ ¹
WireSizeComparison/large_100_without_statics-4                                      23.70Ki ± ∞ ¹
Parse/simple-4                                                                      3.656Ki ± ∞ ¹
Parse/conditional-4                                                                 4.398Ki ± ∞ ¹
Parse/range-4                                                                       4.234Ki ± ∞ ¹
Parse/nested-4                                                                      4.609Ki ± ∞ ¹
Parse/complex-4                                                                     6.906Ki ± ∞ ¹
BuildTree/simple-4                                                                  10.97Ki ± ∞ ¹
BuildTree/conditional-true-4                                                        21.41Ki ± ∞ ¹
BuildTree/conditional-false-4                                                       10.74Ki ± ∞ ¹
BuildTree/range-small-4                                                             36.05Ki ± ∞ ¹
BuildTreeScale/small-10-4                                                           117.1Ki ± ∞ ¹
BuildTreeScale/medium-100-4                                                         1.134Mi ± ∞ ¹
BuildTreeScale/large-1000-4                                                         11.35Mi ± ∞ ¹
NodeRender-4                                                                          56.00 ± ∞ ¹
TreeToHTML/simple-4                                                                   64.00 ± ∞ ¹
TreeToHTML/nested-4                                                                   144.0 ± ∞ ¹
TreeToHTML/with-ranges-4                                                              72.00 ± ∞ ¹
TreeToHTMLScale/small-10-4                                                            544.0 ± ∞ ¹
TreeToHTMLScale/medium-100-4                                                        3.633Ki ± ∞ ¹
TreeToHTMLScale/large-1000-4                                                        65.41Ki ± ∞ ¹
IsVoidElement-4                                                                       0.000 ± ∞ ¹
NodeRenderComplex-4                                                                   248.0 ± ∞ ¹
MinifyHTML-4                                                                        1.641Ki ± ∞ ¹
ParseActionFromHTTP-4                                                               6.798Ki ± ∞ ¹
ParseActionFromWebSocket-4                                                            656.0 ± ∞ ¹
PrepareUpdate/without-errors-4                                                        0.000 ± ∞ ¹
PrepareUpdate/with-errors-4                                                           32.00 ± ∞ ¹
SerializeUpdate-4                                                                     648.0 ± ∞ ¹
PrepareAndSerialize/simple-update-4                                                   672.0 ± ∞ ¹
PrepareAndSerialize/with-metadata-4                                                   848.0 ± ∞ ¹
ParseActionScale/small-http-4                                                       6.790Ki ± ∞ ¹
ParseActionScale/small-ws-4                                                           648.0 ± ∞ ¹
ParseActionScale/medium-http-4                                                      6.954Ki ± ∞ ¹
ParseActionScale/medium-ws-4                                                          760.0 ± ∞ ¹
ParseActionScale/large-http-4                                                       7.884Ki ± ∞ ¹
ParseActionScale/large-ws-4                                                         1.578Ki ± ∞ ¹
SerializeUpdateScale/simple-4                                                         648.0 ± ∞ ¹
SerializeUpdateScale/nested-4                                                       1.281Ki ± ∞ ¹
SerializeUpdateScale/multiple-fields-4                                                968.0 ± ∞ ¹
ConcurrentConnections/100_connections-4                                             3.516Ki ± ∞ ¹
ConcurrentConnections/:00_connections-4                                             35.16Ki ± ∞ ¹
RegisterUnregister-4                                                                1.182Ki ± ∞ ¹
GetByGroup-4                                                                          896.0 ± ∞ ¹
CloseConnection-4                                                                     258.0 ± ∞ ¹
MemoryUsage-4                                                                       95.71Ki ± ∞ ¹
BroadcastToGroup-4                                                                  4.000Ki ± ∞ ¹
ConcurrentRegistrations-4                                                           1.185Ki ± ∞ ¹
GetByGroupExcept-4                                                                    896.0 ± ∞ ¹
geomean                                                                         ²                  ? ³ ² ⁴
¹ need >= 6 samples for confidence interval at level 0.95
² summaries must be >0 to compute geomean
³ benchmark set differs from baseline; geomeans may not be comparable
⁴ ratios must be >0 to compute geomean

                                               │ testdata/benchmarks/baseline.txt │   current-bench.txt   │
                                               │            allocs/op             │  allocs/op    vs base │
DispatchWithState_Cached-8                                            4.000 ± ∞ ¹
E2EUserJourney-8                                                     23.65k ± ∞ ¹
E2ETodoApp-8                                                         1.532k ± ∞ ¹
E2ERangeOperations/add-items-8                                        844.0 ± ∞ ¹
E2ERangeOperations/remove-items-8                                     456.0 ± ∞ ¹
E2ERangeOperations/reorder-items-8                                    588.0 ± ∞ ¹
E2ERangeOperations/update-items-8                                     588.0 ± ∞ ¹
E2EMultipleSessions/sessions-1-8                                      235.0 ± ∞ ¹
E2EMultipleSessions/sessions-10-8                                    2.359k ± ∞ ¹
E2EMultipleSessions/sessions-100-8                                   23.45k ± ∞ ¹
SpecificationCompliance-8                                             220.0 ± ∞ ¹
ErrorPaths/invalid-template-syntax-8                                 25.80k ± ∞ ¹
ErrorPaths/missing-field-8                                            235.0 ± ∞ ¹
ErrorPaths/nil-data-8                                                 220.0 ± ∞ ¹
ErrorPaths/empty-template-8                                          25.98k ± ∞ ¹
TemplateExecute/initial-render-8                                     26.16k ± ∞ ¹
TemplateExecute/subsequent-render-8                                   245.0 ± ∞ ¹
TemplateExecuteUpdates/no-changes-8                                   229.0 ± ∞ ¹
TemplateExecuteUpdates/small-update-8                                 229.0 ± ∞ ¹
TemplateExecuteUpdates/large-update-8                                 752.0 ± ∞ ¹
TemplateComplexity/simple-fields-8                                    489.0 ± ∞ ¹
TemplateComplexity/with-conditionals-8                                388.0 ± ∞ ¹
TemplateComplexity/with-ranges-8                                      615.0 ± ∞ ¹
TemplateComplexity/deeply-nested-8                                    561.0 ± ∞ ¹
TemplateConcurrent/goroutines-1-8                                     245.0 ± ∞ ¹
TemplateConcurrent/goroutines-10-8                                    245.0 ± ∞ ¹
TemplateConcurrent/goroutines-100-8                                   245.0 ± ∞ ¹
Template_Execute-8                                                    248.0 ± ∞ ¹
Template_ExecuteUpdates-8                                             231.0 ± ∞ ¹
TreeNodeCreation/flat-8                                               19.00 ± ∞ ¹
TreeNodeCreation/nested-small-8                                       133.0 ± ∞ ¹
TreeNodeCreation/nested-medium-8                                      403.0 ± ∞ ¹
TreeNodeCreation/nested-large-8                                      1.213k ± ∞ ¹
TreeNodeMarshalJSON/flat-8                                            52.00 ± ∞ ¹
TreeNodeMarshalJSON/nested-small-8                                    358.0 ± ∞ ¹
TreeNodeMarshalJSON/nested-medium-8                                  1.087k ± ∞ ¹
WrapperInjection/full-html-8                                          37.00 ± ∞ ¹
WrapperInjection/fragment-8                                           28.00 ± ∞ ¹
ExtractWrapperContent-8                                               28.00 ± ∞ ¹
ContextOperations/with-statics-8                                      106.0 ± ∞ ¹
ContextOperations/without-statics-8                                   106.0 ± ∞ ¹
TreeNodeClone/flat-8                                                  14.00 ± ∞ ¹
TreeNodeClone/nested-small-8                                          106.0 ± ∞ ¹
TreeNodeClone/nested-medium-8                                         322.0 ± ∞ ¹
TreeNodeToMap/flat-8                                                  18.00 ± ∞ ¹
TreeNodeToMap/nested-small-8                                          120.0 ± ∞ ¹
TreeNodeToMap/nested-medium-8                                         363.0 ± ∞ ¹
GenerateRandomID-8                                                    1.000 ± ∞ ¹
CalculateStructureFingerprint_Small-8                                 9.000 ± ∞ ¹
CalculateStructureFingerprint_Medium-8                                188.0 ± ∞ ¹
CalculateStructureFingerprint_Large-8                                1.808k ± ∞ ¹
CalculateStructureFingerprint_DeepNested-8                            154.0 ± ∞ ¹
CalculateStructureFingerprint_Range100-8                              13.00 ± ∞ ¹
CalculateStructureFingerprint_Range1000-8                             13.00 ± ∞ ¹
ExecuteTemplateWithContext_Struct-8                                   29.00 ± ∞ ¹
ExecuteTemplateWithContext_Map-8                                      36.00 ± ∞ ¹
CompareTreesNoChanges-8                                               2.000 ± ∞ ¹
CompareTreesSmallChange-8                                             3.000 ± ∞ ¹
CompareTreesLargeChange/10-8                                          6.000 ± ∞ ¹
CompareTreesLargeChange/100-8                                         12.00 ± ∞ ¹
CompareTreesLargeChange/1000-8                                        23.00 ± ∞ ¹
RangeDiffUpdate-8                                                     37.00 ± ∞ ¹
RangeDiffInsert-8                                                     37.00 ± ∞ ¹
RangeDiffRemove-8                                                     37.00 ± ∞ ¹
PrepareTreeForClient/with-statics-8                                   0.000 ± ∞ ¹
PrepareTreeForClient/without-statics-8                                5.000 ± ∞ ¹
ClientNeedsStatics_SameStructure-8                                    0.000 ± ∞ ¹
ClientNeedsStatics_DifferentStructure-8                               0.000 ± ∞ ¹
ClientNeedsStatics_DeepNested-8                                       0.000 ± ∞ ¹
ClientNeedsStatics_Range-8                                            0.000 ± ∞ ¹
ClientNeedsStatics_NilOld-8                                           0.000 ± ∞ ¹
WireSize_WithStatics-8                                                53.00 ± ∞ ¹
WireSize_WithoutStatics-8                                             55.00 ± ∞ ¹
WireSizeComparison/small_5_with_statics-8                             18.00 ± ∞ ¹
WireSizeComparison/small_5_without_statics-8                          18.00 ± ∞ ¹
WireSizeComparison/medium_20_with_statics-8                           53.00 ± ∞ ¹
WireSizeComparison/medium_20_without_statics-8                        55.00 ± ∞ ¹
WireSizeComparison/large_100_with_statics-8                           215.0 ± ∞ ¹
WireSizeComparison/large_100_without_statics-8                        219.0 ± ∞ ¹
Parse/simple-8                                                        43.00 ± ∞ ¹
Parse/conditional-8                                                   64.00 ± ∞ ¹
Parse/range-8                                                         59.00 ± ∞ ¹
Parse/nested-8                                                        69.00 ± ∞ ¹
Parse/complex-8                                                       126.0 ± ∞ ¹
BuildTree/simple-8                                                    100.0 ± ∞ ¹
BuildTree/conditional-true-8                                          195.0 ± ∞ ¹
BuildTree/conditional-false-8                                         102.0 ± ∞ ¹
BuildTree/range-small-8                                               378.0 ± ∞ ¹
BuildTreeScale/small-10-8                                            1.197k ± ∞ ¹
BuildTreeScale/medium-100-8                                          11.74k ± ∞ ¹
BuildTreeScale/large-1000-8                                          117.1k ± ∞ ¹
NodeRender-8                                                          3.000 ± ∞ ¹
TreeToHTML/simple-8                                                   4.000 ± ∞ ¹
TreeToHTML/nested-8                                                   7.000 ± ∞ ¹
TreeToHTML/with-ranges-8                                              5.000 ± ∞ ¹
TreeToHTMLScale/small-10-8                                            16.00 ± ∞ ¹
TreeToHTMLScale/medium-100-8                                          109.0 ± ∞ ¹
TreeToHTMLScale/large-1000-8                                         1.017k ± ∞ ¹
IsVoidElement-8                                                       0.000 ± ∞ ¹
NodeRenderComplex-8                                                   5.000 ± ∞ ¹
MinifyHTML-8                                                          13.00 ± ∞ ¹
ParseActionFromHTTP-8                                                 30.00 ± ∞ ¹
ParseActionFromWebSocket-8                                            13.00 ± ∞ ¹
PrepareUpdate/without-errors-8                                        0.000 ± ∞ ¹
PrepareUpdate/with-errors-8                                           1.000 ± ∞ ¹
SerializeUpdate-8                                                     10.00 ± ∞ ¹
PrepareAndSerialize/simple-update-8                                   11.00 ± ∞ ¹
PrepareAndSerialize/with-metadata-8                                   15.00 ± ∞ ¹
ParseActionScale/small-http-8                                         30.00 ± ∞ ¹
ParseActionScale/small-ws-8                                           13.00 ± ∞ ¹
ParseActionScale/medium-http-8                                        40.00 ± ∞ ¹
ParseActionScale/medium-ws-8                                          23.00 ± ∞ ¹
ParseActionScale/large-http-8                                         63.00 ± ∞ ¹
ParseActionScale/large-ws-8                                           46.00 ± ∞ ¹
SerializeUpdateScale/simple-8                                         10.00 ± ∞ ¹
SerializeUpdateScale/nested-8                                         19.00 ± ∞ ¹
SerializeUpdateScale/multiple-fields-8                                16.00 ± ∞ ¹
ConcurrentConnections/:00_connections-8                              2.000k ± ∞ ¹
RegisterUnregister-8                                                  13.00 ± ∞ ¹
GetByGroup-8                                                          1.000 ± ∞ ¹
CloseConnection-8                                                     3.000 ± ∞ ¹
MemoryUsage-8                                                         620.0 ± ∞ ¹
BroadcastToGroup-8                                                    101.0 ± ∞ ¹
ConcurrentRegistrations-8                                             11.00 ± ∞ ¹
GetByGroupExcept-8                                                    1.000 ± ∞ ¹
DispatchWithState_Cached-4                                                           4.000 ± ∞ ¹
E2EUserJourney-4                                                                    23.68k ± ∞ ¹
E2ETodoApp-4                                                                        1.533k ± ∞ ¹
E2ERangeOperations/add-items-4                                                       844.0 ± ∞ ¹
E2ERangeOperations/remove-items-4                                                    456.0 ± ∞ ¹
E2ERangeOperations/reorder-items-4                                                   588.0 ± ∞ ¹
E2ERangeOperations/update-items-4                                                    588.0 ± ∞ ¹
E2EMultipleSessions/sessions-1-4                                                     236.0 ± ∞ ¹
E2EMultipleSessions/sessions-10-4                                                   2.361k ± ∞ ¹
E2EMultipleSessions/sessions-100-4                                                  23.52k ± ∞ ¹
SpecificationCompliance-4                                                            221.0 ± ∞ ¹
ErrorPaths/invalid-template-syntax-4                                                3.494k ± ∞ ¹
ErrorPaths/missing-field-4                                                           235.0 ± ∞ ¹
ErrorPaths/nil-data-4                                                                220.0 ± ∞ ¹
ErrorPaths/empty-template-4                                                         3.667k ± ∞ ¹
TemplateExecute/initial-render-4                                                    3.854k ± ∞ ¹
TemplateExecute/subsequent-render-4                                                  245.0 ± ∞ ¹
TemplateExecuteUpdates/no-changes-4                                                  229.0 ± ∞ ¹
TemplateExecuteUpdates/small-update-4                                                229.0 ± ∞ ¹
TemplateExecuteUpdates/large-update-4                                                752.0 ± ∞ ¹
TemplateComplexity/simple-fields-4                                                   489.0 ± ∞ ¹
TemplateComplexity/with-conditionals-4                                               388.0 ± ∞ ¹
TemplateComplexity/with-ranges-4                                                     615.0 ± ∞ ¹
TemplateComplexity/deeply-nested-4                                                   559.0 ± ∞ ¹
TemplateConcurrent/goroutines-1-4                                                    245.0 ± ∞ ¹
TemplateConcurrent/goroutines-10-4                                                   245.0 ± ∞ ¹
TemplateConcurrent/goroutines-100-4                                                  245.0 ± ∞ ¹
Template_Execute-4                                                                   248.0 ± ∞ ¹
Template_ExecuteUpdates-4                                                            231.0 ± ∞ ¹
TreeNodeCreation/flat-4                                                              19.00 ± ∞ ¹
TreeNodeCreation/nested-small-4                                                      133.0 ± ∞ ¹
TreeNodeCreation/nested-medium-4                                                     403.0 ± ∞ ¹
TreeNodeCreation/nested-large-4                                                     1.213k ± ∞ ¹
TreeNodeMarshalJSON/flat-4                                                           52.00 ± ∞ ¹
TreeNodeMarshalJSON/nested-small-4                                                   358.0 ± ∞ ¹
TreeNodeMarshalJSON/nested-medium-4                                                 1.087k ± ∞ ¹
WrapperInjection/full-html-4                                                         37.00 ± ∞ ¹
WrapperInjection/fragment-4                                                          28.00 ± ∞ ¹
ExtractWrapperContent-4                                                              28.00 ± ∞ ¹
ContextOperations/with-statics-4                                                     106.0 ± ∞ ¹
ContextOperations/without-statics-4                                                  106.0 ± ∞ ¹
TreeNodeClone/flat-4                                                                 14.00 ± ∞ ¹
TreeNodeClone/nested-small-4                                                         106.0 ± ∞ ¹
TreeNodeClone/nested-medium-4                                                        322.0 ± ∞ ¹
TreeNodeToMap/flat-4                                                                 18.00 ± ∞ ¹
TreeNodeToMap/nested-small-4                                                         120.0 ± ∞ ¹
TreeNodeToMap/nested-medium-4                                                        363.0 ± ∞ ¹
GenerateRandomID-4                                                                   1.000 ± ∞ ¹
CalculateStructureFingerprint_Small-4                                                9.000 ± ∞ ¹
CalculateStructureFingerprint_Medium-4                                               188.0 ± ∞ ¹
CalculateStructureFingerprint_Large-4                                               1.808k ± ∞ ¹
CalculateStructureFingerprint_DeepNested-4                                           154.0 ± ∞ ¹
CalculateStructureFingerprint_Range100-4                                             13.00 ± ∞ ¹
CalculateStructureFingerprint_Range1000-4                                            13.00 ± ∞ ¹
ExecuteTemplateWithContext_Struct-4                                                  29.00 ± ∞ ¹
ExecuteTemplateWithContext_Map-4                                                     36.00 ± ∞ ¹
CompareTreesNoChanges-4                                                              2.000 ± ∞ ¹
CompareTreesSmallChange-4                                                            3.000 ± ∞ ¹
CompareTreesLargeChange/10-4                                                         6.000 ± ∞ ¹
CompareTreesLargeChange/100-4                                                        12.00 ± ∞ ¹
CompareTreesLargeChange/1000-4                                                       23.00 ± ∞ ¹
RangeDiffUpdate-4                                                                    37.00 ± ∞ ¹
RangeDiffInsert-4                                                                    37.00 ± ∞ ¹
RangeDiffRemove-4                                                                    37.00 ± ∞ ¹
PrepareTreeForClient/with-statics-4                                                  0.000 ± ∞ ¹
PrepareTreeForClient/without-statics-4                                               5.000 ± ∞ ¹
ClientNeedsStatics_SameStructure-4                                                   0.000 ± ∞ ¹
ClientNeedsStatics_DifferentStructure-4                                              0.000 ± ∞ ¹
ClientNeedsStatics_DeepNested-4                                                      0.000 ± ∞ ¹
ClientNeedsStatics_Range-4                                                           0.000 ± ∞ ¹
ClientNeedsStatics_NilOld-4                                                          0.000 ± ∞ ¹
WireSize_WithStatics-4                                                               53.00 ± ∞ ¹
WireSize_WithoutStatics-4                                                            55.00 ± ∞ ¹
WireSizeComparison/small_5_with_statics-4                                            18.00 ± ∞ ¹
WireSizeComparison/small_5_without_statics-4                                         18.00 ± ∞ ¹
WireSizeComparison/medium_20_with_statics-4                                          53.00 ± ∞ ¹
WireSizeComparison/medium_20_without_statics-4                                       55.00 ± ∞ ¹
WireSizeComparison/large_100_with_statics-4                                          215.0 ± ∞ ¹
WireSizeComparison/large_100_without_statics-4                                       219.0 ± ∞ ¹
Parse/simple-4                                                                       43.00 ± ∞ ¹
Parse/conditional-4                                                                  64.00 ± ∞ ¹
Parse/range-4                                                                        59.00 ± ∞ ¹
Parse/nested-4                                                                       69.00 ± ∞ ¹
Parse/complex-4                                                                      126.0 ± ∞ ¹
BuildTree/simple-4                                                                   100.0 ± ∞ ¹
BuildTree/conditional-true-4                                                         195.0 ± ∞ ¹
BuildTree/conditional-false-4                                                        102.0 ± ∞ ¹
BuildTree/range-small-4                                                              378.0 ± ∞ ¹
BuildTreeScale/small-10-4                                                           1.197k ± ∞ ¹
BuildTreeScale/medium-100-4                                                         11.73k ± ∞ ¹
BuildTreeScale/large-1000-4                                                         117.1k ± ∞ ¹
NodeRender-4                                                                         3.000 ± ∞ ¹
TreeToHTML/simple-4                                                                  4.000 ± ∞ ¹
TreeToHTML/nested-4                                                                  7.000 ± ∞ ¹
TreeToHTML/with-ranges-4                                                             5.000 ± ∞ ¹
TreeToHTMLScale/small-10-4                                                           16.00 ± ∞ ¹
TreeToHTMLScale/medium-100-4                                                         109.0 ± ∞ ¹
TreeToHTMLScale/large-1000-4                                                        1.017k ± ∞ ¹
IsVoidElement-4                                                                      0.000 ± ∞ ¹
NodeRenderComplex-4                                                                  5.000 ± ∞ ¹
MinifyHTML-4                                                                         13.00 ± ∞ ¹
ParseActionFromHTTP-4                                                                30.00 ± ∞ ¹
ParseActionFromWebSocket-4                                                           13.00 ± ∞ ¹
PrepareUpdate/without-errors-4                                                       0.000 ± ∞ ¹
PrepareUpdate/with-errors-4                                                          1.000 ± ∞ ¹
SerializeUpdate-4                                                                    10.00 ± ∞ ¹
PrepareAndSerialize/simple-update-4                                                  11.00 ± ∞ ¹
PrepareAndSerialize/with-metadata-4                                                  15.00 ± ∞ ¹
ParseActionScale/small-http-4                                                        30.00 ± ∞ ¹
ParseActionScale/small-ws-4                                                          13.00 ± ∞ ¹
ParseActionScale/medium-http-4                                                       40.00 ± ∞ ¹
ParseActionScale/medium-ws-4                                                         23.00 ± ∞ ¹
ParseActionScale/large-http-4                                                        63.00 ± ∞ ¹
ParseActionScale/large-ws-4                                                          46.00 ± ∞ ¹
SerializeUpdateScale/simple-4                                                        10.00 ± ∞ ¹
SerializeUpdateScale/nested-4                                                        19.00 ± ∞ ¹
SerializeUpdateScale/multiple-fields-4                                               16.00 ± ∞ ¹
ConcurrentConnections/100_connections-4                                              200.0 ± ∞ ¹
ConcurrentConnections/:00_connections-4                                             2.000k ± ∞ ¹
RegisterUnregister-4                                                                 13.00 ± ∞ ¹
GetByGroup-4                                                                         1.000 ± ∞ ¹
CloseConnection-4                                                                    3.000 ± ∞ ¹
MemoryUsage-4                                                                        619.0 ± ∞ ¹
BroadcastToGroup-4                                                                   101.0 ± ∞ ¹
ConcurrentRegistrations-4                                                            11.00 ± ∞ ¹
GetByGroupExcept-4                                                                   1.000 ± ∞ ¹
geomean                                                                         ²                 ? ³ ² ⁴
¹ need >= 6 samples for confidence interval at level 0.95
² summaries must be >0 to compute geomean
³ benchmark set differs from baseline; geomeans may not be comparable
⁴ ratios must be >0 to compute geomean

                                               │ testdata/benchmarks/baseline.txt │  current-bench.txt   │
                                               │             bytes/op             │   bytes/op     vs base   │
WireSize_WithStatics-8                                                294.0 ± ∞ ¹
WireSize_WithoutStatics-8                                             251.0 ± ∞ ¹
WireSizeComparison/small_5_with_statics-8                             104.0 ± ∞ ¹
WireSizeComparison/small_5_without_statics-8                          61.00 ± ∞ ¹
WireSizeComparison/medium_20_with_statics-8                           294.0 ± ∞ ¹
WireSizeComparison/medium_20_without_statics-8                        251.0 ± ∞ ¹
WireSizeComparison/large_100_with_statics-8                         1.203Ki ± ∞ ¹
WireSizeComparison/large_100_without_statics-8                      1.203Ki ± ∞ ¹
WireSize_WithStatics-4                                                                294.0 ± ∞ ¹
WireSize_WithoutStatics-4                                                             251.0 ± ∞ ¹
WireSizeComparison/small_5_with_statics-4                                             104.0 ± ∞ ¹
WireSizeComparison/small_5_without_statics-4                                          61.00 ± ∞ ¹
WireSizeComparison/medium_20_with_statics-4                                           294.0 ± ∞ ¹
WireSizeComparison/medium_20_without_statics-4                                        251.0 ± ∞ ¹
WireSizeComparison/large_100_with_statics-4                                         1.203Ki ± ∞ ¹
WireSizeComparison/large_100_without_statics-4                                      1.203Ki ± ∞ ¹
geomean                                                               291.7           291.7        ? ² ³
¹ need >= 6 samples for confidence interval at level 0.95
² benchmark set differs from baseline; geomeans may not be comparable
³ ratios must be >0 to compute geomean

Thresholds

  • ⚠️ Warning: Regressions >10% on critical benchmarks
  • ❌ Failure: Regressions >20% on critical benchmarks
  • Critical benchmarks: Benchmark(E2E|Template|CompareTrees|RangeDiff|PrepareTree).*

See benchmarking guide for details.

@claude
Copy link
Copy Markdown

claude bot commented Mar 11, 2026

Code Review

This is a well-motivated cleanup. The three divergent evaluation paths were a genuine correctness hazard, and the consolidated buildExecData() approach is the right abstraction. Specific findings below.


Issues

1. with.go: all {{with}} bodies now route through the vars-aware path regardless of whether $ is used

The fix correctly makes $.Field work inside non-vars {{with}} contexts:

bodyVarCtx := &varContext{parent: data, vars: newOrderedVars(), dot: newContext}
return buildTreeFromASTWithVars(node.List, bodyVarCtx, keyGen, ctx)

But this allocates a varContext + orderedVars for every {{with}} invocation, even when no variables are present. The stdlib parity test "with root access via $" confirms the fix is correct. The trade-off should be noted in a comment since {{with}} blocks are very common. A targeted guard (if bodyUsesRoot(node.List)) would eliminate overhead for the common case, though the current approach is simple and correct.

2. evaluateActionWithVars early-return guard is effectively dead code for most callers

Old: if usedVars.Len() == 0 && !usesRootVar { return "" }
New: if len(execData) == 0 { return "", nil }

After buildExecData, mergeFieldsIntoMap(varCtx.dot, execData) runs unconditionally, so execData is non-empty whenever dot context has fields. Since evaluateActionWithVars is only called from the "has variables" path, dot is almost always non-nil. The guard will almost never fire. This isn't a correctness bug, but the guard is misleading — a comment or removal would clarify intent.

3. sortedVarNames called twice in transformAndEvalWithVars

The function iterates sortedNames once to compute usesVar, then buildExecData calls sortedVarNames again internally — two allocations and two sort passes over the same data. For typical 1–3 variable loops this is negligible, but easily avoided by passing the fast-path result into buildExecData.

4. transformConditionWithVars is a one-line alias with no added semantics

The sole caller in conditional.go could call buildExecData directly. Thin wrappers with no invariants or documentation cost add indirection for no gain. Per project conventions, keep it only if there's a semantic boundary worth enforcing.


Correctness Observations

5. TestBuildExecData_RangeVarPrefixConvention documents a gap

The test shows that vars stored with a $ prefix (e.g., varCtx.vars.Set("$v", "value")) are not matched by buildExecData since it searches for "$$v". If executeRangeBodyWithVars actually stores vars with the prefix, range vars inside nested expressions would silently not resolve. The test documents the behavior but it is worth verifying against the actual range implementation that this path is never exercised, or adding an explicit "this cannot happen" comment.

6. treeToHTML in stdlib_parity_test.go duplicates render logic without a link

The helper reconstructs HTML by interleaving statics and dynamics, mirroring internal/render.TreeToHTML. If the wire format changes, parity tests could stay green while the real render diverges. A comment linking to the render package would help maintainers keep them in sync.


Positive Observations

  • Error propagation — Returning (string, error) from evaluateActionWithVars instead of "ERROR: ..." strings is a critical correctness fix. Error strings leaking into HTML output are both a UI bug and a potential information disclosure issue.
  • Partial-match fix — Descending-length sort in buildExecData correctly prevents $c from corrupting $col references. The targeted parameterized tests in var_eval_test.go are exactly the right coverage.
  • UTF8 safety — Consolidating into a single capitalizeFieldName using utf8.DecodeRuneInString / unicode.ToUpper eliminates the multi-byte corruption issue uniformly across all callers.
  • usesRoot fast-path fix — Adding usesRoot to the bypass condition in transformAndEvalWithVars is a genuine correctness fix; $.Field without named vars previously evaluated against the wrong context.
  • stdlib parity test pattern — A solid regression harness for tracking engine divergence over time. Worth expanding to cover range and nested-with scenarios.

The previous commit routes all {{with}} blocks through
buildTreeFromASTWithVars, but that path was missing:

1. CommentNode and TemplateNode handling (would error on templates
   with comments inside with blocks)
2. Range comprehension embedding in buildTreeFromListWithVars
   (range trees need to be nested, not merged by statics)
3. Variable declaration delegation (with blocks containing
   {{$var := .}} inside their body)

Add the missing node type handlers and align
buildTreeFromListWithVars with buildTreeFromList behavior.

https://claude.ai/code/session_011iXr6f9X1MT3p2N6QpWGdu
@claude
Copy link
Copy Markdown

claude bot commented Mar 11, 2026

test

1 similar comment
@claude
Copy link
Copy Markdown

claude bot commented Mar 11, 2026

test

@claude
Copy link
Copy Markdown

claude bot commented Mar 11, 2026

Code Review

This is a solid refactor that addresses several real bugs. Extracting buildExecData() as a single canonical path for variable transformation is the right architectural move.

What is Well Done

  • Partial match fix is correct. sortedVarNames returns names in descending length order, so $col is replaced before $c. TestBuildExecData_PartialMatchPrevention covers this well.
  • UTF8-safe capitalization. Using utf8.DecodeRuneInString + unicode.ToUpper is the right approach; the old strings.ToUpper(varName[:1]) was silently corrupting multi-byte rune boundaries.
  • Error propagation fix in evaluateActionWithVars. Returning (string, error) instead of embedding ERROR strings is a critical correctness fix. Silently rendering errors as HTML content is both a reliability and security issue.
  • with.go root variable fix. Switching handleWithNode to buildTreeFromASTWithVars with parent: data correctly implements Go template semantics where $ always refers to the top-level data, not the narrowed with context.
  • CommentNode handling. Returning an empty tree instead of an error for comment nodes is correct.
  • Test quality. var_eval_test.go covers key properties of buildExecData directly. stdlib_parity_test.go is a useful regression harness.

Issues

1. Redundant computation in transformAndEvalWithVars

transformAndEvalWithVars (var_context.go:207) calls sortedVarNames and iterates for usesVar, then calls buildExecData which internally calls sortedVarNames and detectsRootVariable again. This means two full passes over sorted names and two detectsRootVariable calls per invocation. Not a bug, but inconsistent with the goal of a unified path.

2. detectsRootVariable has a dead vars parameter

detectsRootVariable (field.go:106) accepts vars orderedVars but never uses it in the function body. This is pre-existing dead code, but since this PR touches multiple call sites it is a good opportunity to clean it up.

3. evaluateActionWithVars: execData empty-check semantics have shifted

The guard if len(execData) == 0 { return "", nil } previously corresponded to usedVars.Len() == 0 && !usesRootVar. Now buildExecData unconditionally merges varCtx.dot into execData, so execData is empty only when no vars match AND no root AND dot is nil or empty. The silent empty-string return still fires when the template was parsed as having variable nodes but none resolved — returning an error in that branch would be more robust than silent data loss.

4. Missing test coverage for changed code paths

  • with.go change: No tests cover $.Field access inside {{with}} blocks via the non-vars path (handleWithNode). A test like {{with .Item}}{{$.Title}}{{end}} exercising the new bodyVarCtx would prevent regressions.
  • CommentNode handling: No test covers a template comment inside a var-context block.
  • buildTreeFromListWithVars range handling: The new HasRange() short-circuit path (var_context.go:303-311) is untested. A range nested inside a var context should be covered.

5. treeToHTML in stdlib_parity_test.go is an incomplete test helper

treeToHTML (stdlib_parity_test.go:300) only interleaves Statics with Dynamics keyed by string index. It does not handle RangeData, nested conditionals, or Dynamics keys not mapping to a statics gap. For the current test cases this works, but it will silently produce wrong output for more complex templates, making it easy to write a parity test that passes but tests nothing meaningful. Consider using the render package directly if accessible from this test package.


Summary

The refactor achieves its goal: three divergent paths are consolidated, the bugs are fixed, and buildExecData coverage is good. The main asks are test coverage for the changed with.go path and the new range handling in buildTreeFromListWithVars. The redundant computation and dead parameter are minor cleanups worth making.

The executeRangeBodyWithVars function stored range variables WITH the $
prefix (e.g., "$index"), but buildExecData searches for "$" + varName,
creating patterns like "$$index" that never match {{$index}} in
templates. This caused pipe expressions like {{$index | printf "#%d"}}
to fail with "$index" treated as a string literal.

Strip the $ prefix (matching executeRangeBodyWithVarsAndInherited) so
that buildExecData correctly constructs "$" + "index" = "$index".

Fixes test: TestE2EInvariantGuarantee

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Mar 12, 2026

Code Review

This is a well-motivated refactor that fixes several real bugs. The core idea — consolidating three divergent execData-building paths into a single buildExecData() function — is sound. Here are my observations:


What's good

Bug fixes are correct:

  • The partial-match bug ($col corrupted by $c) is properly fixed via descending-length sort in sortedVarNames.
  • UTF-8-safe capitalizeFieldName using utf8.DecodeRuneInString + unicode.ToUpper is the right approach.
  • Error propagation in evaluateActionWithVars (removing the "ERROR: ..." embedded-string anti-pattern) is a proper fix.
  • The handleWithNode change to use buildTreeFromASTWithVars with parent: data correctly enables $.Field access inside {{with}} blocks resolving to the original data rather than the narrowed context.
  • transformAndEvalWithVars now correctly includes usesRoot in the fast-path check (!usesVar && !usesRoot), fixing cases where $.Field expressions in conditions were silently mis-evaluated.

Test quality:

  • stdlib_parity_test.go is an excellent approach — comparing against the standard library prevents regression drift.
  • Parameterized tests in var_eval_test.go cover the specific bugs being fixed with good clarity.

Issues to address

1. Subtle semantic shift in evaluateActionWithVars empty-check (potential bug)

// field.go
if len(execData) == 0 {
    return "", nil
}

The old guard was usedVars.Len() == 0 && !usesRootVar — checked before building execData. The new guard fires after mergeFieldsIntoMap runs, which means: if varCtx.dot is a struct or map with fields, execData will be non-empty even when no $vars matched. The code then tries to execute the (still-containing-$var) template, which will fail at tmpl.Execute.

In practice this path shouldn't be reached (the caller confirms variable references exist), but the guard's intent has changed. A comment explaining why len(execData) == 0 is the right sentinel here would clarify this, or an explicit check for !anyVarMatched && !usesRoot before calling mergeFieldsIntoMap would restore the original semantics more clearly.

2. TestBuildExecData_RangeVarPrefixConvention documents removed behavior

This test (line ~1118 in var_eval_test.go) verifies that a var stored as "$v" (with $ prefix) is NOT matched by buildExecData. But range.go in this very PR strips the $ prefix before storing variables:

varName := strings.TrimPrefix(node.Pipe.Decl[0].Ident[0], "$")

So the "$v" prefix scenario this test covers can no longer arise from executeRangeBodyWithVars. The test name is misleading ("RangeVarPrefixConvention") since the convention is now the opposite. Either remove the test or rename/reframe it as testing defensive behavior for invalid direct usage.

3. Unconditional mergeFieldsIntoMap call — minor performance regression

buildExecData always calls mergeFieldsIntoMap(varCtx.dot, execData) via reflection, even for simple {{$x}} expressions on a large struct dot context. The previous evaluateActionWithVars did NOT merge dot context — it only substituted $vars. This is intentional for the fix (enabling {{$c.Method .Type}}), but worth documenting explicitly so future maintainers understand the cost is deliberate. Consider adding a comment like:

// Merge dot fields so mixed-context expressions (e.g. {{$c.Method .Type}}) work.
// This involves reflection; kept minimal by the caller's hasVars guard.
if err := mergeFieldsIntoMap(varCtx.dot, execData); err != nil {

4. Double-scan in transformAndEvalWithVars

The function now scans sortedNames for usesVar, then calls buildExecData which scans again. This is a minor redundancy. Not a bug, but could be simplified by letting buildExecData return whether any var was substituted, or by removing the pre-scan and always calling buildExecData (since it's already the fast path for no-var cases). Not blocking.

5. Missing test: {{with}} + $.Field access

The change to handleWithNode is the most behaviorally significant change in this PR (the $-root-in-with fix), but neither stdlib_parity_test.go nor the existing var_eval_test.go explicitly validates this end-to-end:

// Missing: confirms $.Field inside {{with}} resolves to original data
data := map[string]interface{}{
    "Title": "root",
    "Item":  map[string]interface{}{"Name": "child"},
}
// {{with .Item}}{{$.Title}}{{end}} should produce "root"

The stdlib_parity_test.go has a "with root access via $" case, but that goes through lvtRenderBuildTreehandleWithNode. Confirm this test actually exercises the fixed path.


Nits

  • transformConditionWithVars is now a one-liner wrapper that just delegates to buildExecData. Its docstring is still present but the function adds no value — consider inlining the call at the call site in handleIfNodeWithVars and removing the wrapper.
  • The usesRoot variable in handleIfNodeWithVars is now computed at line 57 but the transformConditionWithVars signature no longer accepts it (it was dropped). Confirm the variable is still used elsewhere in that function; if not, it would be a compile error (Go rejects unused variables).

Summary

The refactor is correct and the bugs being fixed are real. The main thing to verify before merging is item #1 (the len(execData) == 0 semantic shift) and to add a test for the handleWithNode + $.Field end-to-end path (#5). Items #2#4 are low priority.

adnaan added a commit to livetemplate/lvt that referenced this pull request Mar 12, 2026
The core library (livetemplate/livetemplate#179) fixed a bug where
range variables in executeRangeBodyWithVars were stored with the $
prefix, causing buildExecData to construct "$$index" patterns that
never matched {{$index}} in templates. The fix strips the $ prefix
so that pipe expressions like {{$index | printf "#%d"}} now correctly
resolve to "#0", "#1", etc. instead of empty strings.

Updates all affected golden files:
- update_01: Initial range items now have "#0", "#1", "#2" for field 2
- update_02: Adds ["u", "todo-3", {"2": "#1"}] after remove (index shift)
- update_03: Adds index updates for completed/reordered items
- update_05a: Adds index updates for existing items when prepending
- update_05b: Adds index updates for existing items when inserting middle
- update_06: Adds index updates for multi-operation scenario

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add comment explaining len(execData) == 0 guard semantics (Claude #1)
- Rename TestBuildExecData_RangeVarPrefixConvention to
  TestBuildExecData_DollarPrefixedVarNotMatched and reframe as
  defensive test since $ prefix is now stripped (Claude #2)
- Add comment on mergeFieldsIntoMap explaining deliberate reflection
  cost for mixed-context expressions (Claude #3)
- Inline transformConditionWithVars one-liner wrapper into call site
  and remove the dead function (Claude nit)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Mar 12, 2026

Code Review

This is a well-motivated refactoring that correctly addresses real bugs. The consolidation into buildExecData() eliminates divergent code paths that were the root cause of multiple issues. Overall the direction is right.

What is good

Bug fixes are genuine and correctly solved:

  • Descending-length sort in sortedVarNames prevents partial-match corruption ($col replaced before $c). Fix is in the right place.
  • Dollar-prefix stripping in range.go aligns with how buildExecData builds its search patterns. The updated comment makes the contract explicit.
  • UTF8-safe capitalizeFieldName using utf8.DecodeRuneInString is correct; the previous varName[:1] byte slice was only safe for ASCII.
  • evaluateActionWithVars returning (string, error) instead of embedding error strings in rendered HTML is a meaningful improvement.

The with.go fix is important: handleWithNode now uses the var-aware path so $ correctly resolves to the outer root data rather than the narrowed with context. This matches Go template semantics.

Test coverage is solid. The parameterized unit tests in var_eval_test.go and the stdlib parity tests are a good pattern for regression prevention.

Issues

1. Double call to detectsRootVariable

In transformAndEvalWithVars (~line 218), the function calls detectsRootVariable for the fast-path guard, then immediately calls buildExecData which calls detectsRootVariable again internally (~line 189). The function scans variables twice for the same expression. Not a correctness issue -- could be deduplicated by passing a usesRoot bool into buildExecData, or by collapsing the fast-path check into buildExecData.

2. buildExecData always runs mergeFieldsIntoMap regardless of expression content

mergeFieldsIntoMap uses reflection and is called unconditionally, even when the expression contains only variable references with no dot-field accesses. The comment says "kept minimal by the caller's hasVars guard" but that guard is in the caller (deciding when to call buildExecData), not inside buildExecData itself. For large structs as dot context this adds unnecessary reflection cost on every var-containing expression.

3. Missing stdlib parity test for the primary bug scenario

The PR description cites mixed $var + .DotField expressions as the key regression. While TestBuildExecData_DotContextMerging and TestEvaluateActionWithVars_DotContextAccess cover this at unit level, there is no TestStdlibParity_* test exercising this end-to-end through BuildTree. A parity test for a range body accessing both a declared var and a dot field would give a regression guard for the exact scenario described in the PR.

4. treeToHTML test helper does not handle RangeData

The helper in stdlib_parity_test.go correctly handles nested *TreeNode dynamics but skips range data entirely. Any parity test involving {{range}} will silently produce incomplete HTML from the LVT side. The current tests avoid range intentionally, but this limitation should be documented in the helper comment so future contributors do not add range parity tests that produce misleading results.

5. with nil/falsy case not covered in parity tests

TestStdlibParity_WithBlocks only tests truthy with paths. The falsy path where the value is nil is a common edge case. Given the with.go change, adding a falsy parity test would complete the coverage.

Nits

  • buildExecData godoc mentions "descending-length sort" -- accurate, but the sort is performed by sortedVarNames. A cross-reference would help readers.
  • TestBuildExecData_NoVarsNoRoot stores an unused variable that never appears in the expression. The test validates dot-field merging regardless -- useful -- but TestBuildExecData_OnlyDotFields would be a clearer name.

Summary

The core refactoring is correct and the bugs fixed are real. Main asks before merging:

  1. Add an end-to-end stdlib parity test for the mixed $var + .DotField expression case (the primary regression scenario)
  2. Add a {{with}} falsy parity test
  3. Document the range limitation in treeToHTML

The double detectsRootVariable call and unconditional dot merging are worth a follow-up but should not block this.

…lsy cases

Addresses Claude bot review:
- Add end-to-end parity test for mixed $var + .DotField in range bodies
  (the primary regression scenario from this PR)
- Add {{with}} falsy parity tests (nil, else, empty string)
- Document treeToHTML range limitation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Mar 12, 2026

PR Review: fix: unify divergent expression evaluation paths. This is a solid refactor that fixes four real bugs through a well-motivated consolidation. Please see the full review details attached.

@claude
Copy link
Copy Markdown

claude bot commented Mar 12, 2026

Full review text for the fix: unify divergent expression evaluation paths PR follows. This placeholder will be replaced.

@claude
Copy link
Copy Markdown

claude bot commented Mar 12, 2026

Contributing to LiveTemplate Core Library

Thank you for your interest in contributing to the LiveTemplate core library! This guide covers contributions to the Go server-side library only.

Contributing to Other Components

LiveTemplate is distributed across multiple repositories. Please use the appropriate contribution guide:


This guide will help you get started with core library contributions.

Table of Contents

Prerequisites

Before you begin, ensure you have the following installed:

  • Go 1.21+ - Required for building and testing the core library
  • golangci-lint - Required for linting (pre-commit hook)
    # macOS
    brew install golangci-lint
    
    # Linux/WSL
    curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin
  • Chrome/Chromium - Required for E2E browser tests (chromedp)

Note: For client library development (TypeScript), see the client repository.

Setup

  1. Fork and clone the repository

    git clone https://github.com/yourusername/livetemplate.git
    cd livetemplate
  2. Install dependencies

    # Go dependencies (automatically handled by Go modules)
    go mod download
  3. Install pre-commit hook (automatically validates before each commit)

    cp scripts/pre-commit.sh .git/hooks/pre-commit
    chmod +x .git/hooks/pre-commit
  4. Verify setup

    # Run all tests
    go test -v ./... -timeout=30s
    
    # Run linter
    golangci-lint run

Development Workflow

Making Changes

  1. Create a feature branch

    git checkout -b feature/your-feature-name
    # or
    git checkout -b fix/your-bug-fix
  2. Make your changes

    • Follow existing code patterns and conventions
    • Add tests for new functionality
    • Update documentation if needed
  3. Run tests frequently

    # Quick feedback loop
    go test -v ./...
    
    # Or test specific packages
    go test -v -run TestYourSpecificTest
  4. Commit your changes

    git add .
    git commit -m "your commit message"
    # Pre-commit hook will automatically run validation

Testing Core Changes with LVT/Examples

When making changes to the core library, you may want to test how they affect LVT or examples before releasing.

Recommended: Go Workspace (Automatic)

The easiest way is to use Go workspaces (Go 1.18+). This automatically uses local checkouts without modifying any go.mod files.

Directory structure:

parent/
├── livetemplate/  (core library - this repo)
├── lvt/           (CLI tool)
├── examples/      (example apps)
├── client/        (TypeScript client - optional)
└── setup-workspace.sh  (run this once)

One-time setup:

# Clone sibling repositories
cd ..  # Go to parent directory
git clone https://github.com/livetemplate/lvt.git
git clone https://github.com/livetemplate/examples.git

# Create workspace (run once)
./setup-workspace.sh

That's it! Now all go commands automatically use your local versions:

# Test LVT with your core changes
cd lvt
go test ./...  # Automatically uses ../livetemplate

# Test examples
cd ../examples
./test-all.sh  # Automatically uses ../livetemplate and ../lvt

# Build an example
cd counter
go build  # Uses local livetemplate

To remove workspace:

cd /path/to/parent
./setup-workspace.sh --clean

How it works:

  • Creates a go.work file in the parent directory
  • Go automatically finds it and uses listed modules
  • No go.mod changes needed
  • go.work is gitignored (never committed)

Alternative: Manual Replace Directives

If you prefer manual control or can't use workspaces, use the helper scripts in each repo:

cd ../lvt
./scripts/setup-local-dev.sh

cd ../examples
./scripts/setup-local-dev.sh

Revert with --undo flag when done.

Automated CI Checks

The core library has automated CI checks that test LVT and examples against your PR. These checks will catch breaking changes before merge.

Directory Structure (Core Library)

livetemplate/
├── template.go          # Main API - Template type and orchestrator
├── tree.go              # Tree operations (private implementation)
├── action.go            # Action protocol and data binding
├── mount.go             # Controller+State pattern and HTTP/WebSocket handlers
├── context.go           # Unified Context type for action handlers
├── state.go             # State interface and AsState wrapper
├── session.go           # Session management
├── internal/            # Internal packages
│   ├── parse/           # AST-based template parser
│   │   ├── parser.go    # Main parser entry point
│   │   ├── constructs.go# Construct type definitions
│   │   ├── compile.go   # Compilation logic
│   │   ├── hydrate.go   # Hydration logic
│   │   └── helpers.go   # Utility functions
│   ├── build/           # Tree building and operations
│   │   ├── builder.go   # Tree construction
│   │   ├── tree_ops.go  # Tree manipulation
│   │   ├── fingerprint.go# Change detection
│   │   └── types.go     # Core tree types
│   ├── diff/            # Tree comparison
│   │   ├── tree_compare.go # Main comparison logic
│   │   ├── range_ops.go    # Range differential operations
│   │   ├── prepare.go      # Wire format preparation
│   │   └── helpers.go      # Comparison helpers
│   └── observe/         # Observability
│       ├── logger.go    # Structured logging (slog)
│       ├── metrics.go   # Operational metrics
│       └── context.go   # Context enrichment
├── testdata/            # Test fixtures and golden files
│   ├── e2e/
│   └── fixtures/        # Shared test templates
├── docs/                # Documentation
└── scripts/             # Development scripts

Note: The client library, CLI tool, and examples are now in separate repositories:

Pre-commit Hook

The pre-commit hook is CRITICAL for maintaining code quality. It automatically:

  1. Auto-formats Go code using go fmt
  2. Runs golangci-lint to catch common issues
  3. Runs all Go tests with timeout

Important Rules

  1. NEVER skip the pre-commit hook using --no-verify

    • The hook is there to catch issues early
    • Skipping it will break CI and block your PR
  2. Fix failures before committing

    • Linting errors: Fix the code issues
    • Test failures: Ensure all tests pass
    • If stuck, ask for help (see Getting Help)
  3. Formatted files are auto-added

    • The hook runs go fmt and stages formatted files automatically
    • No need to manually format before committing

Example Hook Output

🔄 Running pre-commit validation...
📝 Auto-formatting Go code...
✅ Code formatting completed
🔍 Running golangci-lint...
✅ Linting passed
🧪 Running Go tests...
✅ All Go tests passed
✅ Pre-commit validation completed successfully

Testing

Test Categories

  1. Unit Tests - Fast tests for individual functions

    go test -v ./... -short
  2. E2E Tests - End-to-end tests with template rendering

    go test -run TestTemplate_E2E -v
  3. Browser Tests - Chromedp tests for real browser interactions

    go test -run TestE2E -v
  4. Fuzz Tests - Randomized input testing

    go test -fuzz=FuzzTree -fuzztime=30s

Note: For client library tests, see the client repository.

Golden Files

Many E2E tests use golden files in testdata/e2e/:

  • *.html - Expected rendered HTML output
  • *.json - Expected tree updates

To update golden files after intentional changes:

UPDATE_GOLDEN=1 go test -run TestTemplate_E2E -v

Writing Tests

Follow these patterns:

Unit test example:

func TestNewFeature(t *testing.T) {
    t.Run("description of test case", func(t *testing.T) {
        // Arrange
        input := "test input"

        // Act
        result := YourFunction(input)

        // Assert
        if result != expected {
            t.Errorf("expected %v, got %v", expected, result)
        }
    })
}

E2E browser test example:

func TestFeature(t *testing.T) {
    // Setup server and browser context
    ctx, cancel := chromedp.NewContext(context.Background())
    defer cancel()

    // Run test actions
    err := chromedp.Run(ctx,
        chromedp.Navigate("http://localhost:8080"),
        chromedp.Click("button#submit"),
        chromedp.WaitVisible("#result"),
    )

    if err != nil {
        t.Fatal(err)
    }
}

Code Style

General Principles

  1. No unnecessary comments - Code should be self-documenting
  2. Follow existing patterns - Check neighboring code for conventions
  3. Use existing utilities - Don't reinvent the wheel
  4. Maintain idiomatic Go - Follow Go best practices

Naming Conventions

  • Public API (exported): PascalCase
    • Template, Context, State, Session, AsState
  • Internal implementation (unexported): camelCase
    • treeNode, keyGenerator, parseAction
  • Test functions: TestFeatureName
  • Benchmark functions: BenchmarkFeatureName

Public API Guidelines

The public API surface is minimal by design. Only export:

  • Types that users directly interact with
  • Functions that users must call
  • Interfaces that users implement

Keep implementation details private.

Documentation

  • Add godoc comments for all public types and functions
  • Document non-obvious behavior and edge cases
  • Include examples in godoc when helpful
// Template represents a parsed template that can generate updates.
// It maintains state between renders to produce minimal diffs.
type Template struct {
    // ...
}

// ExecuteToUpdate renders the template and returns a JSON update.
// This is more efficient than ExecuteToHTML for subsequent renders.
func (t *Template) ExecuteToUpdate(data interface{}) (*UpdateResponse, error) {
    // ...
}

Commit Messages

Use conventional commit format:

<type>(<scope>): <subject>

<body>

<footer>

Types

  • feat: New feature
  • fix: Bug fix
  • refactor: Code restructuring without behavior change
  • test: Adding or updating tests
  • docs: Documentation changes
  • perf: Performance improvements
  • chore: Build process or tooling changes

Examples

feat(template): add support for nested template invokes

Implements recursive template invocation to support complex
component hierarchies. Updates tree parser to handle nested
{{template}} calls correctly.

Closes #123
fix(client): prevent duplicate WebSocket connections

Adds connection state tracking to prevent race condition where
multiple connections could be established during reconnection.

Fixes #456
refactor: minimize public API surface

BREAKING CHANGE: Internal types like TreeNode and KeyGenerator
are now private. Users should only interact with Template,
Store, and ActionContext interfaces.

Pull Requests

Before Submitting

  1. Ensure all tests pass locally
  2. Update documentation if needed
  3. Add tests for new features
  4. Rebase on latest main branch
  5. Run the pre-commit hook manually if needed:
    .git/hooks/pre-commit

PR Description Template

## Description
Brief description of changes

## Motivation
Why is this change needed?

## Changes
- List of specific changes
- One per line

## Testing
How was this tested?

## Checklist
- [ ] Tests added/updated
- [ ] Documentation updated
- [ ] Pre-commit hook passes
- [ ] No breaking changes (or documented if necessary)

Review Process

  1. PRs require at least one approval
  2. CI must pass (tests, linting, formatting)
  3. Address reviewer feedback
  4. Maintainer will merge when ready

Where to Start

Good First Issues

Look for issues labeled good first issue - these are:

  • Well-defined and scoped
  • Don't require deep system knowledge
  • Good for getting familiar with the codebase

Areas to Explore

  1. Core template engine (template.go, tree.go, internal/)

    • Template parsing improvements
    • Tree diffing optimizations
    • New Go template constructs
  2. Documentation (docs/)

    • Improve existing docs
    • Add examples
    • Fix typos or unclear sections
  3. Testing (various *_test.go files)

    • Add test coverage
    • Improve E2E tests
    • Add edge case tests
  4. HTTP/WebSocket handling (mount.go, session.go, context.go)

    • Controller+State pattern implementation
    • Session management improvements
    • Performance optimizations

For other components:

Learning the Codebase

  1. Start with the Contributor Walkthrough

  2. Read the architecture docs

    • CLAUDE.md - Development guidelines
    • docs/design/ARCHITECTURE.md - System architecture and design decisions
  3. Run the examples

    # Clone the examples repository
    git clone https://github.com/livetemplate/examples.git
    cd examples/counter
    go run main.go
    # Open http://localhost:8080
  4. Read the tests

    • Tests are excellent documentation
    • Start with e2e_test.go for high-level flow
    • Check template_test.go for core functionality
  5. Experiment

    • Make small changes
    • Run tests to see what breaks
    • Use debugger to step through code

Getting Help

  • Questions: Open a discussion on GitHub
  • Bugs: Open an issue with reproduction steps
  • Features: Open an issue to discuss before implementing
  • Real-time help: Check if there's a Discord/Slack (if available)

License

By contributing, you agree that your contributions will be licensed under the same license as the project (check LICENSE file).


Thank you for contributing to LiveTemplate!

@claude
Copy link
Copy Markdown

claude bot commented Mar 12, 2026

PR Review: fix: unify divergent expression evaluation paths

This is a solid refactor that fixes four real bugs through a well-motivated consolidation. Overall the changes are correct and the test coverage is good.


What works well:

buildExecData() consolidation is the right call. Three functions (evaluateActionWithVars, transformAndEvalWithVars, transformConditionWithVars) had drifted apart and the extraction cleanly captures the invariants once. The descending-length sort to prevent partial matches is the correct approach and directly fixes a real edge case.

Error propagation fix in evaluateActionWithVars from returning string to (string, error) is exactly right. Error strings leaking into rendered HTML is a correctness bug masquerading as output, and the new signature makes failures first-class.

UTF8-safe capitalizeFieldName using utf8.DecodeRuneInString and unicode.ToUpper is correct. The old byte-slice approach varName[:1] is valid for ASCII but silently wrong for multi-byte runes.

Test files (var_eval_test.go and stdlib_parity_test.go) are well-structured.


Issues to address:

Issue 1: detectsRootVariable called twice in conditional path

In handleIfNodeWithVars (conditional.go:57), detectsRootVariable is called to decide the fast path, then buildExecData calls it again internally (var_context.go:189). Both produce the same result. Minor redundancy, worth noting.

Issue 2: buildExecData always reflects over varCtx.dot

The mergeFieldsIntoMap call runs unconditionally on every buildExecData invocation, including inside range body hot paths. The function comment says kept minimal by the callers hasVars guard but that guard only exists in transformAndEvalWithVars. The other two callers (evaluateActionWithVars and handleIfNodeWithVars) do not short-circuit when dot is nil/empty. A cheap guard would suffice: check if varCtx.dot is nil before calling mergeFieldsIntoMap.

Issue 3: The empty-execData guard in evaluateActionWithVars has shifted semantics

Old guard: usedVars.Len() == 0 and not usesRootVar. New guard: len(execData) == 0. Since buildExecData now merges dot fields unconditionally, any non-nil varCtx.dot populates execData even if no named variables matched. The guard no longer reliably means nothing to evaluate. Worth calling out explicitly.

Issue 4: handleWithNode routing change needs a TemplateNode invariant check

The change in with.go from buildTreeFromAST to buildTreeFromASTWithVars is semantically significant. The stdlib parity tests cover the happy path, but there is no test for a template invocation inside a with-block hitting the new TemplateNode error path. The new code errors with: template invocation found - should be flattened. If the flattening invariant ever breaks, this surfaces as a template execution failure. Worth a comment or test confirming flattening is always applied before handleWithNode is reached.

Issue 5: treeToHTML naming in stdlib_parity_test.go

The helper is flat-tree-only (no RangeData). Since TestStdlibParity_RangeVarWithDotField uses a completely different reconstruction path, having two very different approaches in the same file may confuse contributors. A rename to flatTreeToHTML would signal the limitation at every call site.


Summary:

The core fixes (UTF8, partial-match, error propagation, dot-context merging) are correct and test coverage is solid. Main action items:

  1. nil-guard before mergeFieldsIntoMap in buildExecData to avoid unnecessary reflection when dot is nil.
  2. Test or comment confirming the TemplateNode-inside-with invariant holds before shipping.
  3. (Nice-to-have) Rename treeToHTML to flatTreeToHTML.

Items 1 and 3 are polish; item 2 is the one most worth resolving before merging.

- Add nil guard before mergeFieldsIntoMap to avoid reflection when dot is nil
- Rename treeToHTML to flatTreeToHTML to signal range limitation at call sites
- Add flattening precondition comment in with.go for TemplateNode invariant

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Mar 12, 2026

PR Review: fix: unify divergent expression evaluation paths (#176)

This is a solid refactoring that tackles a genuine architectural problem. The consolidation of three divergent evaluation paths into a single buildExecData() is the right approach, and the bugs listed in the description (nil .Type, embedded ERROR: strings, partial $col/$c matches, non-UTF8-safe capitalisation) are all real.


What's good

buildExecData() design — Descending-length sort for variable replacement is the correct fix for the partial-match bug. UTF8-safe capitalisation via utf8.DecodeRuneInString / unicode.ToUpper is better than the previous strings.ToUpper(varName[:1]) which silently truncated multi-byte runes.

Error propagation — Changing evaluateActionWithVars from string (with embedded "ERROR: ...") to (string, error) is strictly better. Error strings leaking into rendered HTML are a silent, hard-to-debug failure mode.

range.go $ prefix stripping — Aligning variable storage convention (varName without $) with the lookup convention ("$" + varName) is the right fix; the old comment about intentional $$i double-prefix was a red flag.

Test coveragevar_eval_test.go and stdlib_parity_test.go are comprehensive. The parity-test harness (stdlibRender + lvtRender) is a valuable long-term investment.


Issues / Concerns

1. RootData key collision (pre-existing, worth documenting)

buildExecData synthesises a "RootData" key in execData to represent $.:

execData["RootData"] = varCtx.parent

If the template data legitimately has a field or map key called RootData, it will be silently shadowed by the root-variable substitution. This isn't new in this PR, but since you're now centralising this logic it's a good time to add a // NOTE: "RootData" is a reserved synthetic key comment and, ideally, a test that asserts the collision doesn't produce confusing output.

2. len(execData) == 0 guard in evaluateActionWithVars is now almost dead code

// field.go
if len(execData) == 0 {
    return "", nil
}

Because buildExecData always calls mergeFieldsIntoMap(varCtx.dot, execData), this guard only fires when varCtx.dot is nil/empty AND there are no variable or root references. Given handleActionNodeWithVars only calls evaluateActionWithVars after detecting a VariableNode, the comment saying "This can only happen if the caller incorrectly detected a VariableNode" is accurate but the guard is now misleading. Consider either removing it (it returns "" which is wrong — the caller will render an empty dynamic slot) or keeping it with a // should never happen note and a logged warning.

3. detectsRootVariable is called twice in some code paths

In handleIfNodeWithVars (conditional.go) the caller still inverts the check before calling buildExecData:

// conditional.go — caller checks usesRoot before calling buildExecData
transformedCond, execData, err := buildExecData(pipeStr, varCtx)

But buildExecData now calls detectsRootVariable again internally. Meanwhile transformAndEvalWithVars in var_context.go has its own explicit usesRoot check before delegating:

usesRoot := detectsRootVariable(expr, varCtx.vars)
if !usesVar && !usesRoot {
    return evaluatePipeWithCache(...)
}
transformedExpr, execData, err := buildExecData(expr, varCtx)

This means detectsRootVariable is called once by transformAndEvalWithVars, and then again inside buildExecData. It's not a correctness issue, but it's a small redundancy worth noting. Consider accepting usesRoot as a pre-computed hint, or just document that buildExecData re-checks it.

4. with.go behaviour change — $ root access in {{with}} blocks

The change from buildTreeFromASTbuildTreeFromASTWithVars in handleWithNode is the right fix to make {{with .Item}}{{$.Title}}{{end}} work. The stdlib-parity tests cover this. Two things to confirm:

  • handleWithNodeWithVars (the existing var-propagation path) also delegates to buildTreeFromASTWithVars. Make sure the two paths (handleWithNodebuildTreeFromASTWithVars vs. handleWithNodeWithVarsbuildTreeFromASTWithVars) don't diverge in their varCtx initialisation in a way that could cause inconsistency when a {{with}} is nested inside a range with declared variables.
  • The precondition comment ("template invocations must be flattened before reaching this point") is correct but fragile as documentation. If FlattenTemplate is ever skipped or template invocations sneak through, the buildTreeFromASTWithVars TemplateNode case now returns an error, which is better than the previous silent failure — good.

5. flatTreeToHTML test helper silently drops range output

// NOTE: This helper does not handle RangeData (tree.Range). Any parity test
// involving {{range}} will produce incomplete HTML from the LVT side.

This is correctly documented, but it means TestStdlibParity_* tests that use templates with a top-level {{range}} will silently pass with an empty LVT output (because the root tree returned by BuildTree for a pure-range template carries no statics/dynamics, only Range). The TestStdlibParity_RangeVarWithDotField test correctly avoids this by inspecting tree.Range directly — worth adding a if tree.Range != nil { t.Skip("use TestStdlibParity_RangeVarWithDotField pattern") } guard to lvtRender so future callers don't hit the silent-empty-output footgun.


Minor

  • stdlib_parity_test.go imports html/template for parity testing, but lvtRender uses parse.Parse + BuildTree which internally uses text/template. HTML-escaping differences could cause false positives in TestStdlibParity_HTMLEscaping. Worth verifying that BuildTree goes through an html/template execution path, not text/template.
  • buildExecData comment block is clear and complete — leave it.
  • Removing the now-unused reflect import from conditional.go is a nice cleanup detail.

Summary

The core idea is correct and the implementation is solid. The three issues worth addressing before merge:

  1. Document/test RootData key collision
  2. Clarify or remove the len(execData) == 0 guard + its comment
  3. Add a guard in lvtRender/flatTreeToHTML to prevent silent empty-output in future range parity tests

Everything else is minor. Good PR.

- Document RootData as reserved synthetic key in buildExecData
- Update len(execData)==0 guard comment to clarify defensive semantics
- Add range guard in lvtRender to prevent silent empty output in future tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Mar 12, 2026

Code Review

The consolidation into buildExecData() is the right approach — three divergent map-building paths were a real correctness hazard. Here is my analysis of issues not yet covered by the existing review.


Correctness Issues

1. with.go: outer-scope variable inheritance is silently dropped

bodyVarCtx := &varContext{
    parent: data,
    vars:   newOrderedVars(),   // ← empty!
    dot:    newContext,
}
return buildTreeFromASTWithVars(node.List, bodyVarCtx, keyGen, ctx)

The new varContext starts with empty vars. If the {{with}} block is inside a {{range}} or another enclosing var scope (e.g. {{range $i, $v := .Items}}{{with $v.Detail}}{{$i}}{{end}}{{end}}), any outer variables like $i will silently resolve to empty string rather than raising an error. The old buildTreeFromAST path had no variable awareness at all, so this is an improvement, but the regression scenario — outer vars that were previously forwarded by the var-aware handleWithNodeWithVars path — needs a test.

2. RootData is a load-bearing reserved name with no collision protection

// NOTE: "RootData" is a reserved synthetic key. If template data has a field
// named "RootData", it will be shadowed by the root variable substitution.
execData["RootData"] = varCtx.parent

This comment appears in buildExecData but not surfaced in user-facing docs or error messages. A data struct with a RootData field will silently produce wrong output whenever $ is used. Given this is shared infrastructure called from all three former paths, the blast radius is significant. Consider either:

  • Using an unexportable synthetic key like __rootData__ that cannot collide with Go exported field names (exported fields must start with an uppercase letter; __ is valid but highly unlikely)
  • Or validating in mergeFieldsIntoMap that the key "RootData" is not already present before overwriting

3. buildExecData always merges dot fields, including when there are no $ references

Looking at TestBuildExecData_NoVarsNoRoot:

// Expression unchanged since no $ references
if transformed != "{{.Field}}" { ... }
// Dot fields should still be merged
if execData["Field"] != "value" { ... }

So buildExecData("{{.Field}}", varCtx) returns a non-empty execData containing dot fields even though no variable substitution occurred. The evaluateActionWithVars caller then executes the unchanged expression with this map as data — which works, but it means the early-return guard if len(execData) == 0 cannot realistically fire whenever varCtx.dot is populated. This makes the guard misleading. Add a comment or remove it.


Missing Regression Test

4. No test for the primary bug case {{$c.Method .Type}}

The PR description specifically cites .Type resolving as nil in mixed var+dot expressions like {{$c.Method .Type}} as the primary bug. However, var_eval_test.go tests buildExecData unit-level and TestEvaluateActionWithVars_DotContextAccess tests {{$cls}} {{.Type}} (simple field concat), not a method call on the variable itself. A test covering:

// $c is a value with a method; .Type comes from dot context
{{$c.SomeField}} with .Type

would directly validate the regression case and prevent it from silently re-breaking.


Minor Points

5. issue-with-root-var-bug.txt should not be in the repository

A text file as a bug tracker entry is not a convention in this repo. The commented-out test already captures the known divergence clearly. Move the context to a GitHub issue and reference the issue number from the test comment.

6. flatTreeToHTML in stdlib_parity_test.go duplicates internal/render logic

The test helper re-implements static/dynamic interleaving. If the tree format changes, parity tests could stay green while the real render diverges. Add a comment linking it to render.TreeToHTML so future maintainers know to keep them in sync.


What's Good

  • Error propagation fix ((string, error) instead of "ERROR: ..." strings) is exactly right
  • Descending-length sort for partial-match prevention is correct and well-tested
  • UTF-8-safe capitalizeFieldName consolidation removes a class of silent corruption
  • usesRoot fast-path fix in transformAndEvalWithVars is a genuine correctness fix
  • The stdlib parity test pattern is a valuable addition for future regression detection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Mar 12, 2026

Code Review — PR #176: Unify divergent expression evaluation paths

This is a well-motivated refactor that addresses real correctness bugs. The consolidation into buildExecData() is the right approach. Comments below are organized from blocking to nit.


Potential bugs

1. RootData key collision with a variable named rootData

In buildExecData (var_context.go), named variables are written to execData first, then root is handled:

for _, varName := range sortedNames {
    // ... execData[fieldName] = varValue  // could set execData["RootData"]
}
if detectsRootVariable(expr, varCtx.vars) {
    execData["RootData"] = varCtx.parent  // silently overwrites
}

If a user declares {{$rootData := "x"}} and also uses $., the variable value is silently overwritten by varCtx.parent. The comment documents the collision from the data side ("If template data has a field named 'RootData'"), but not the variable name side. Either add a test for this edge case or apply root substitution before the named-var loop so the override direction is explicit.

2. strings.Contains(expr, ...) uses the original expression for matching

for _, varName := range sortedNames {
    if strings.Contains(expr, "$"+varName) {  // checks ORIGINAL
        ...
        execData[fieldName] = varValue
    }
}

With vars {c: "short", col: "long"} and expression {{$col}}:

  • strings.Contains("{{$col}}", "$col") → true, adds Col — correct
  • strings.Contains("{{$col}}", "$c") → true (substring match!) → adds C unnecessarily

execData["C"] ends up populated even though $c is not used. Go templates ignore unused data fields so execution is correct, and descending-sort ensures the string replacement is safe. But extra entries add confusion when debugging. One fix: check strings.Contains(transformedExpr, ...) after replacing longer names first.


Design observations (non-blocking)

3. detectsRootVariable called twice on the root-variable path

transformAndEvalWithVars calls detectsRootVariable for its early-exit check, then buildExecData calls it again internally. Not a measurable perf issue at current usage, but it's inconsistent. Consider accepting a precomputed usesRoot bool or documenting this as intentional.

4. buildExecData runs mergeFieldsIntoMap (reflection) on every call, even for pure-$var expressions

The nil guard (if varCtx.dot != nil) added in the last round helps, but for expressions that only use $var references with no dot access, reflection still runs. Correct and necessary for mixed expressions; worth keeping in mind when profiling hot template paths.

5. len(execData) == 0 guard in evaluateActionWithVars

If only dot fields contributed to execData (no $var references), len(execData) could be non-zero but the original variable still wasn't resolved. Is this case reachable? The comment could clarify this guards the specific "caller detected VariableNode but vars list produced no matches" path, not "no data at all".


Positives

  • UTF-8 fix is correct. The old varName[:1] byte slice was wrong for multi-byte rune names. utf8.DecodeRuneInString + unicode.ToUpper is right and well-tested.
  • Error propagation fix is the right call. Returning "ERROR: ..." strings into rendered HTML was a category error — actual errors now surface properly.
  • Descending-length sort for partial-match prevention is a robust solution and clearly documented.
  • with.go root-variable fix is clean: routing through buildTreeFromASTWithVars with original data as parent matches Go stdlib semantics correctly.
  • Stdlib parity tests are excellent. flatTreeToHTML + stdlibRender + lvtRender is a solid testing pattern. TestStdlibParity_RangeVarWithDotField directly covers the primary regression scenario, and the range limitation note is honest and clear.

Nits

  • "RootData" appears as a string literal in multiple places — an unexported package-level constant would make it easier to grep and rename.
  • buildExecData godoc could note that sortedVarNames guarantees descending-length order, since the partial-match fix depends on that invariant being maintained.

Overall this is a sound fix. Issues 1 and 2 are worth addressing before merge; the rest are suggestions.

@adnaan adnaan merged commit 855c13e into main Mar 12, 2026
6 of 7 checks passed
adnaan added a commit to livetemplate/lvt that referenced this pull request Mar 13, 2026
* fix: update golden files for corrected $index resolution in range

The core library (livetemplate/livetemplate#179) fixed a bug where
range variables in executeRangeBodyWithVars were stored with the $
prefix, causing buildExecData to construct "$$index" patterns that
never matched {{$index}} in templates. The fix strips the $ prefix
so that pipe expressions like {{$index | printf "#%d"}} now correctly
resolve to "#0", "#1", etc. instead of empty strings.

Updates all affected golden files:
- update_01: Initial range items now have "#0", "#1", "#2" for field 2
- update_02: Adds ["u", "todo-3", {"2": "#1"}] after remove (index shift)
- update_03: Adds index updates for completed/reordered items
- update_05a: Adds index updates for existing items when prepending
- update_05b: Adds index updates for existing items when inserting middle
- update_06: Adds index updates for multi-operation scenario

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: update go.mod to use merged core library commit

Points to squash-merged PR #179 on livetemplate/livetemplate main branch
which fixes $index resolution in pipe expressions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants