Skip to content

linalg.solve_linear returns null on a singular system instead of raising, so callers index null and crash far from the cause #1047

Description

@InauguralPhysicist

Found by the phugoid consumer at rung 3, 2026-08-25, eigenscript v0.41.0. Ledger entry: phugoid GAPS.md G6. Filed late — G6 was the one entry in that ledger recorded without an upstream issue, which its own opening rule forbids.

Summary

lib/linalg.eigs's solve_linear returns null when the system is singular. It does not raise, and the null carries no information about which system failed. Callers that index the result get cannot index null at a site that can be far from the cause.

Reproduction

A 3x3 with a zero row:

load_file of "lib/linalg.eigs"
r is linalg.solve_linear of [[[1.0, 2.0, 3.0], [0.0, 0.0, 0.0], [4.0, 5.0, 6.0]], [1.0, 2.0, 3.0]]
print of f"result: {r}"
print of f"r[0] -> "
print of r[0]

result: null, exit 0 from the first two prints, then cannot index null.

How it presented

Building a planted-fault matrix for a trim solver, one plant zeroed the aircraft's control derivatives. That makes the delta-e column of the trim Jacobian identically zero, so the Newton step is singular. The observed failure was cannot index null inside trim_solve, several frames from the plant, with nothing naming the singular system. The plant was rewritten to avoid the crash — the consumer's own workaround — and the gap recorded rather than silently absorbed.

The consumer-side robustness bug (indexing an unchecked result) is the consumer's to fix. What is upstream is that a linear solver signals "no solution" the same way a dictionary signals "no such key", so no caller can distinguish them without a shape check it should not need to write.

What would resolve it

Either raising on a singular system, or returning a discriminated result the caller can test ({"ok": 0, "reason": ...}) as the estimator modules in this ecosystem already do. Related policy: the fail-soft reform track (#971/#975) owns whether the answer here is "raise" or "structured refusal"; this issue is a concrete instance for that decision rather than a request for a particular one.

Note the same family sits one level down: a missing dict field also returns null silently (phugoid GAPS.md W4), which is what makes the null from this function indistinguishable from a typo at the call site.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions