Skip to content

Engine fixes#4

Merged
triviajon merged 8 commits into
triviajon:mainfrom
DIJamner:engine-fixes
Jun 29, 2026
Merged

Engine fixes#4
triviajon merged 8 commits into
triviajon:mainfrom
DIJamner:engine-fixes

Conversation

@DIJamner

Copy link
Copy Markdown
Contributor

Warning: the code in this PR was AI-generated and not human-validated. It should be treated with suspicion until reviewed line-by-line or rewritten.

Includes LLM fixes to a number of bugs in the engine, especially around inductives and fixpoints, including adding inductive hypotheses to the principles generated by inductive datatypes.

Supersedes #2 and should be seen as a prerequisite for #3 , which is now based on this branch.

DIJamner and others added 8 commits June 28, 2026 04:16
The auto-generated <T>_ind was a dependent case-analysis principle: each
constructor case was `forall args, P (c args)`, with no hypothesis for
recursive arguments. That makes induction impossible (apply <T>_ind gives
no IH to work with).

Add an induction hypothesis for every first-order recursive constructor
argument, so e.g. nat_ind's step case becomes
`forall (n:nat), P n -> P (S n)` and tree_ind's becomes
`forall l r, P l -> P r -> P (node l r)`. Non-recursive arguments and
non-recursive types (bool) are unaffected.

Scope: non-parametric inductives only. Parametric `_ind` generation is
still skipped for a separate, pre-existing reason (constructor types use
fresh per-constructor parameter copies that don't match the builder's
parameter variables) and is left for a follow-up.

Adds a semantic regression test: a full abstract induction proof that
only type-checks when the IH is present.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GNvD2hCU3jo1wq6fCLv9GT
Previously the generator skipped the induction principle entirely for
parametric inductives (param_count > 0), because the principle was built
with the inductive's own param_vars while each constructor's stored type
used fresh per-constructor parameter copies in a divergent context branch.

Build the principle with its own parameter set, bound once after ind_var,
and derive each constructor case by *applying* the constructor to those
parameters (letting the kernel substitute them) rather than textually
stripping parameter binders. Argument variables are created fresh and the
remaining telescope is recomputed from the partial application after each
one, so argument and return types are rebased onto our variables and stay
valid even when a type mentions an earlier argument or a parameter (e.g.
cons's tail of type `list A`). This unifies the parametric and
non-parametric paths.

list_ind now matches Coq:
  forall A P, P (nil A) ->
    (forall a l, P l -> P (cons A a l)) -> forall l, P l

Adds a regression test that proves an abstract list induction (only
type-checks if list_ind has the IH-bearing shape).

Also adds bugs/ with two minimal reproducers (plus a clean-failure
contrast) for pre-existing crashes when inducting over goals stated with a
Fixpoint: `apply` with a fixpoint-mentioning motive, and type-checking an
explicit eliminator term whose step case must reduce `add (S n) O`. These
are documented, not fixed (Tier 2: symbolic iota/fix reduction).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GNvD2hCU3jo1wq6fCLv9GT
Two kernel fixes (split out from the original stdlib-benchmark commit, engine
side only):
- src/kernel/normalize.c: _normalize_cbv now unfolds (fix ...) arg in the APP
  case, so cbv/Eval actually compute applied fixpoints (add (S O) O -> S O).
- src/kernel/expression.c: MatchBranch arrays are shared by pointer across arena
  nodes (normalize/conversion rebuild a match reusing its branches); GC shutdown
  now frees each shared allocation exactly once instead of double-freeing.
examples/computational_eliminator.me is the regression; bugs/README documents it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GNvD2hCU3jo1wq6fCLv9GT
Induction over a goal stated with a Fixpoint applied to a symbolic recursive
argument (e.g. proving `forall n, add n O = n` by `nat_ind`) crashed the engine
with SIGSEGV. The shared root cause was non-termination when a fixpoint unfolds
on a variable recursive argument: `add n O` reduced to `match n with ... S p =>
S (add p O)`, and conversion recursed into the branch, unfolding forever.

Fixes:
- fix_reduction.c: add `fix_reduce_app`, the guarded fix rule — an application
  whose head is a fix reduces only once its decreasing argument is in constructor
  head normal form. conversion_whnf, normalize_whnf, and cbv use it instead of
  reducing fixpoints unconditionally; a bare fix is now a value.
- expression.c: register the fix node itself as the recursive variable's body
  (not the eta-expanded lambda), so unfolding the constant yields a fix node the
  guard can govern. fix_reduce therefore no longer substitutes the fix inline
  (which captured the shared argument binders) — it just strips to the lambda
  abstraction; recursive calls resolve through the recursive variable via delta.
- command_exec.c: the Definition command accepts a declared type convertible
  (not merely structurally congruent) to the inferred type, so computational
  types such as `add O O = O` are accepted.

The three bugs/ reproducers now run to completion; bugs/README.md and the
reproducer headers are updated. examples/computational_eliminator.me gains an
end-to-end induction proof over a fixpoint as regression coverage. All 431
kernel tests and every examples/*.me still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GNvD2hCU3jo1wq6fCLv9GT
Engine fixes (split out from the original stdlib-benchmark commit) that let
computational induction be driven by rewrite on the induction hypothesis:
- src/kernel/fix_reduction.c, normalize.c: a symbolic-fixpoint reduction leaves
  a stuck recursive call headed by the constant (add n O, not a bare fix node),
  so rewrite/congruence can match it after simpl.
- src/engine/unify.c, rewrite_internal.c, src/tacticlanguage/tactic_interp.c,
  src/runtime/core.c: rewrite reads the equation off an IH whose stored type is
  the eliminator's beta-redex (motive) n by whnf-normalizing it first;
  _get_lhs_eq fails cleanly (NULL) on a non-eq type.
- src/kernel/expression.c: a redex-typed IH can be applied (the syntactic-Pi
  precheck in init_app_expression_wc is removed; _construct_app_type whnfs).
- src/kernel/type_compat.c: the eliminator's index hole left by a quantified
  motive is recorded during fill, instead of lingering as a stray open goal.
examples/computational_induction_rewrite.me is the regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GNvD2hCU3jo1wq6fCLv9GT
…city)

Engine fixes (split out from the Lists benchmark commit) that unblock all
reduction over parametric inductives such as list A:
- src/kernel/mixed_subst.c: preserve a match parameter-slot pattern variable's
  delta-reducible body alias (_ := A) when rebuilding a branch under a
  substitution. Both rebuilders dropped it, so reducing a fixpoint over a
  parametric constructor (app A (cons A x xs) k) lost the alias, failed to
  type-check the branch body, and crashed on the resulting NULL.
- src/common/map.c: map_new_with_capacity rounds up to a power of two. The
  open-addressing map masks indices with hash & (capacity - 1); iota_reduce
  sized its pattern map to the constructor arg count (3 for cons), so absent-key
  lookups in the full table probed forever.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GNvD2hCU3jo1wq6fCLv9GT
`mengine_runtime_new` borrows MEngineOptions by pointer (rt->options =
options) and reads through it for the runtime's whole life, while the
integration test's make_rt hands it a stack-local that dies on return.
Benign without ASan (the freed stack slot still holds the old bytes), but a
genuine stack-use-after-return that aborts `make check` under AddressSanitizer.
Captured here as a known, separate issue with root cause, ASan stack, repro
command, and fix options.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GNvD2hCU3jo1wq6fCLv9GT
mengine_runtime_new borrowed its options by pointer (rt->options = options)
and read through it for the runtime's whole life, while a caller passing a
stack-local options struct (e.g. the integration test's make_rt) left it
dangling on return. Benign without ASan, but a genuine stack-use-after-return
that aborted `make check` under AddressSanitizer.

The runtime now mallocs and copies the options, and frees the copy in
mengine_runtime_free. Mutations through rt->options (the quiet toggle, the
execution_type) were already runtime-internal; main.c passes options by value
and never reads them back, so owning a copy changes no observable behaviour.

make check under ASan now runs to completion (431/431, no use-after-return).
Updates bugs/note_dangling_options_pointer.md to record the fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GNvD2hCU3jo1wq6fCLv9GT
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.

2 participants