PEGO

83. Inline Eligible Typed Recovery Bodies

  • The typed Go generator compiles eligible unfinished ordinary, left-recursion and Operators Rule bodies containing #recover with the structural emitter. The runtime retains rule invocation, memo and result ownership; left-recursion growth and Operators Rule selection remain in their existing wrappers. Node and TypeScript output are unchanged. Node direct rules and ordinary typed methods with Go-local captures still exclude recovery; unfinished typed bodies use runtime-owned frames. Typed layouts the collector cannot match remain on general expression dispatch.

  • The emitted recovery path uses the runtime's full marks and expectation isolation/keep helpers. It preserves recovered-error state, capture and variable rollback, failed-skip handling, and zero-progress checks. Cut state remains scoped by the existing structural emitter. Generated recovery code resolves the Span helper through the same collision-safe name mapping as other typed code.

  • The generation-only disableTypedRecoveryBodies control emits the same generator's general expression bodies for affected recovery rules and lines. It adds no generated-parser option or matching-time selector.

  • TestGeneratedTypedRecoveryBodies checks all 15 eligible typed grammars in both position units against that control and Node conversion. The cases cover capture/environment rollback, zero-progress and failed skips, nested recovery, error ordering, lookahead, optional and repeated scopes, cut scopes, dead skip syntax, lists of join errors, the Call.Span name collision, and recovery inside left-recursion and Operators Rule bodies. TestGeneratedTypedRecoveryWorkloads also compares ordinary, LR and Operators Rule recovery on accepted, recovered and rejected inputs at sizes 1 and 128. It checks both position units, parser and pool reuse, returned AST and error immutability, retained Error nodes and SyntaxErrors, and cleared frame, depth, capture-trail and undo state.

  • Matching measurements used five alternating 300 ms direct/general pairs on prebuilt fixtures with Go 1.27.1 on darwin/arm64, Apple M3 Max, 16 CPUs. Ratios are medians of per-pair direct/general times, with paired min–max ranges. The table uses n=128: 128 items, plus an incomplete item in rejected inputs. Each Operators Rule and LR item has two recovery sites; each ordinary item has one:

    Workload Unit Mode Median ratio Paired range
    Ordinary CodePoints Accepted 0.956× 0.928–1.021×
    Ordinary Bytes Accepted 0.948× 0.940–1.012×
    Ordinary CodePoints Recovered 0.956× 0.921–0.985×
    Ordinary Bytes Recovered 0.962× 0.927–1.025×
    Ordinary CodePoints Rejected 0.980× 0.956–1.027×
    Ordinary Bytes Rejected 0.980× 0.920–1.046×
    Operators Rule CodePoints Accepted 0.891× 0.783–0.981×
    Operators Rule Bytes Accepted 0.911× 0.860–0.973×
    Operators Rule CodePoints Recovered 0.953× 0.882–0.995×
    Operators Rule Bytes Recovered 0.986× 0.923–1.011×
    Operators Rule CodePoints Rejected 0.916× 0.906–0.957×
    Operators Rule Bytes Rejected 0.920× 0.899–0.982×
    Left recursion CodePoints Accepted 0.984× 0.902–1.003×
    Left recursion Bytes Accepted 0.941× 0.881–0.994×
    Left recursion CodePoints Recovered 0.959× 0.882–1.018×
    Left recursion Bytes Recovered 0.970× 0.872–1.046×
    Left recursion CodePoints Rejected 0.940× 0.901–0.997×
    Left recursion Bytes Rejected 0.966× 0.880–0.985×

    Operators Rule accepted and rejected cases in both units, plus recovered CodePoints, are below parity for every pair. Other ranges mostly cross parity. All n=1 ranges cross parity; their median ratios span 0.915–1.043×, so the small cases establish neither a uniform gain nor cost. Allocation counts are unchanged between direct and general routes for every workload, mode, size and unit:

    Workload n=1, accepted/recovered/rejected n=128, accepted/recovered/rejected
    Ordinary 4 / 13 / 13 8 / 782 / 18
    Operators Rule 6 / 19 / 16 15 / 1,250 / 26
    Left recursion 5 / 29 / 21 15 / 2,773 / 33

    Cumulative bytes per operation are similar, with minor pooled-memory variation. These synthetic fixtures do not establish a full-parser speedup.

  • Generation measurements also used five alternating 300 ms pairs on Go 1.27.1, darwin/arm64, Apple M3 Max, 16 CPUs. Grammar parsing is outside the timed loop; generation includes formatting and runtime embedding:

    Fixture Median ratio Paired range Generated bytes, direct/general B/op, direct/general allocs/op, direct/general
    Ordinary recovery 0.995× 0.891–1.016× 106,869 / 106,216 3.433 / 3.397 MB 56,732 / 56,294
    Operators Rule recovery 0.987× 0.950–1.037× 107,928 / 106,905 3.520 / 3.475 MB 57,934 / 57,329
    LR recovery 1.023× 0.965–1.065× 108,376 / 108,192 3.501 / 3.490 MB 58,329 / 58,379
    Calculator 0.983× 0.891–1.021× 123,205 / 123,205 — —
    TypeScript 0.991× 0.985–0.997× 5,542,942 / 5,542,942 — —

    The ordinary, Operators Rule and LR generation ranges, and the calculator range, cross parity. The TypeScript range is below parity, but its generated output is identical and does not use this typed Go optimization; that ratio is not attributed to recovery-body inlining. The synthetic typed fixtures show small source and allocation increases, not an established generation-time cost or gain.

Generate same-generator direct and reference fixtures and check their parity:

export PEGO_TYPED_RECOVERY_DIR=/tmp/pego-typed-recovery
go test ./internal/engine -run '^TestGeneratedTypedRecoveryWorkloads$' -count=1

Build each workload and route as a separate benchmark binary:

for workload in ordinary operatorsRule lr; do
  for variant in direct general; do
    fixture="$PEGO_TYPED_RECOVERY_DIR/$workload/$variant"
    (cd "$fixture" && go test -c -o "$PEGO_TYPED_RECOVERY_DIR/$workload-$variant.test" .)
  done
done

Alternate the order of each direct/general pair across five samples:

for pair in 1 2 3 4 5; do
  if [ "$((pair % 2))" -eq 1 ]; then
    first=general; second=direct
  else
    first=direct; second=general
  fi
  for workload in ordinary operatorsRule lr; do
    for variant in "$first" "$second"; do
      "$PEGO_TYPED_RECOVERY_DIR/$workload-$variant.test" -test.run='^$' \
        -test.bench='^BenchmarkTypedRecovery$' -test.benchtime=300ms -test.benchmem
    done
  done
done

Build the engine benchmark binary once, then alternate its reference setting over five pairs. Grammar parsing is outside the benchmark's timed loop:

go test -c -o /tmp/pego-typed-recovery-engine.test ./internal/engine
(cd internal/engine && for pair in 1 2 3 4 5; do
  if [ "$((pair % 2))" -eq 1 ]; then first=1; second=0; else first=0; second=1; fi
  for reference in "$first" "$second"; do
    PEGO_REFERENCE_RECOVERY=$reference /tmp/pego-typed-recovery-engine.test \
      -test.run='^$' -test.bench='^BenchmarkTypedRecoveryGeneration$' \
      -test.benchtime=300ms -test.benchmem
  done
done)