Module: Rigor::Inference::BudgetTrace

Defined in:
lib/rigor/inference/budget_trace.rb

Overview

Opt-in counters for the hard-coded inference cutoffs — the "budget" guards that silently return Dynamic[top] / nil / a fallback bound rather than emitting a diagnostic. These are the operative cutoffs in the engine today (the configurable budgets: table in docs/type-specification/inference-budgets.md is not yet wired); counting how often each fires on a real project is the only way to see where inference actually stops.

Three categories, one per guard site:

  • RECURSION_GUARDExpressionTyper#infer_user_method_return detected a (receiver, method) cycle and returned Dynamic[top] (the de-facto recursion-depth budget, effective depth 1).
  • ANCESTOR_WALK_LIMITresolve_user_def_through_ancestors hit the 100-node BFS cap and gave up resolving the self-call.
  • HKT_FUEL_EXHAUSTEDHktReducer ran out of its reduction fuel budget and unwound to app.bound.
  • RECURSION_UNROLL_FUEL — the constant-arg recursion unroll (ADR-55 slice 1) exhausted its per-entry fuel and fell back to the plain (receiver, method) guard (in-cycle call → Dynamic[top]).
  • RECURSION_FIXPOINT_CAP — the fixpoint return-summary iteration (ADR-55 slice 2) hit its 3-evaluation cap without converging and collapsed the summary to untyped (today's behaviour).

Enabled only when RIGOR_BUDGET_TRACE is set (to any non-empty value) in the environment, or via BudgetTrace.enable! in tests. When disabled, BudgetTrace.hit is a single boolean check and returns immediately, so normal runs pay nothing.

Counters are process-global (Mutex-guarded) so they aggregate across threads, but they do NOT cross fork boundaries — run rigor check --workers 0 to keep all inference in one process when collecting a trace.

Constant Summary collapse

RECURSION_GUARD =
:recursion_guard
ANCESTOR_WALK_LIMIT =
:ancestor_walk_limit
HKT_FUEL_EXHAUSTED =
:hkt_fuel_exhausted
RECURSION_UNROLL_FUEL =

ExpressionTyper#infer_user_method_return exhausted its constant-arg unroll fuel (ADR-55 slice 1) and fell back to the plain (receiver, method) recursion guard — i.e. the in-cycle call widened to Dynamic[top] exactly as it does without the unroll.

:recursion_unroll_fuel
RECURSION_FIXPOINT_CAP =

ExpressionTyper#infer_user_method_return ran the fixpoint return-summary iteration (ADR-55 slice 2) to its 3-evaluation cap without reaching convergence and collapsed the summary to untyped — the in-cycle result widens to Dynamic[top] exactly as it does without the fixpoint.

:recursion_fixpoint_cap
BLOCK_WRITEBACK_CAP =

BodyFixpoint#converge (ADR-56 slice A — non-escaping block captured-local write-back) ran its 3-evaluation cap without the written local's join converging and collapsed that local to Dynamic[top] (the escaping-block floor). Shared by slice B's loop-body fixpoint.

:block_writeback_cap
CATEGORIES =
[
  RECURSION_GUARD, ANCESTOR_WALK_LIMIT, HKT_FUEL_EXHAUSTED, RECURSION_UNROLL_FUEL,
  RECURSION_FIXPOINT_CAP, BLOCK_WRITEBACK_CAP
].freeze
UNION_ARITY =

Distribution (histogram) categories — read-only observations of a value's size at a site, used to choose budget defaults from an observed tail rather than a guess (ADR-41 WD3 / Slice 2a). No cap is enforced; these only record. UNION_ARITY is the member count of every Type::Union that Combinator.union produces — the distribution the union_size budget default should be set from.

:union_arity
DISTRIBUTION_CATEGORIES =
[UNION_ARITY].freeze

Class Method Summary collapse

Class Method Details

.disable!Object



77
78
79
# File 'lib/rigor/inference/budget_trace.rb', line 77

def disable!
  @enabled = false
end

.distribution(category) ⇒ Object

Frozen {value => count} histogram for a distribution category.



105
106
107
# File 'lib/rigor/inference/budget_trace.rb', line 105

def distribution(category)
  @mutex.synchronize { @distributions[category].dup.freeze }
end

.enable!Object

Test / programmatic toggles. Production enablement is the RIGOR_BUDGET_TRACE env var read once at load time.



73
74
75
# File 'lib/rigor/inference/budget_trace.rb', line 73

def enable!
  @enabled = true
end

.enabled?Boolean

Returns:

  • (Boolean)


67
68
69
# File 'lib/rigor/inference/budget_trace.rb', line 67

def enabled?
  @enabled
end

.hit(category) ⇒ Object

Records one firing of category. No-op (one boolean check) when tracing is disabled.



82
83
84
85
86
# File 'lib/rigor/inference/budget_trace.rb', line 82

def hit(category)
  return unless @enabled

  @mutex.synchronize { @counts[category] += 1 }
end

.observe(category, value) ⇒ Object

Records one observation of value (an Integer size) into category's histogram. No-op (one boolean check) when disabled.



98
99
100
101
102
# File 'lib/rigor/inference/budget_trace.rb', line 98

def observe(category, value)
  return unless @enabled

  @mutex.synchronize { @distributions[category][value] += 1 }
end

.percentile(hist, total, fraction) ⇒ Object

Nearest-rank percentile over a {value => count} histogram without materialising the full sample.



126
127
128
129
130
131
132
133
134
# File 'lib/rigor/inference/budget_trace.rb', line 126

def percentile(hist, total, fraction)
  rank = (fraction * total).ceil
  cumulative = 0
  hist.keys.sort.each do |value|
    cumulative += hist[value]
    return value if cumulative >= rank
  end
  hist.keys.max
end

.resetObject



136
137
138
139
140
141
# File 'lib/rigor/inference/budget_trace.rb', line 136

def reset
  @mutex.synchronize do
    @counts.clear
    @distributions.clear
  end
end

.snapshotObject

Frozen snapshot of the current counts, every known category present (zero-filled) so consumers can render a stable table.



90
91
92
93
94
# File 'lib/rigor/inference/budget_trace.rb', line 90

def snapshot
  @mutex.synchronize do
    CATEGORIES.to_h { |category| [category, @counts[category]] }.freeze
  end
end

.summarize(category, over: []) ⇒ Object

Summary of a distribution category: total observation count, max observed value, selected percentiles, and how many observations met or exceeded each threshold in over. Percentiles use the nearest-rank method over the expanded sample.



112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/rigor/inference/budget_trace.rb', line 112

def summarize(category, over: [])
  hist = distribution(category)
  total = hist.values.sum
  return { count: 0, max: 0, percentiles: {}, over: over.to_h { |t| [t, 0] } } if total.zero?

  sorted = hist.keys.sort
  { count: total,
    max: sorted.last,
    percentiles: { p50: percentile(hist, total, 0.50), p90: percentile(hist, total, 0.90),
                   p99: percentile(hist, total, 0.99) },
    over: over.to_h { |t| [t, hist.sum { |value, n| value >= t ? n : 0 }] } }
end