Skip to content

lim x->2 signum(x) overflows the stack #704

Description

@Rafael-SOWNet

On current master:

"signum(x)".ToEntity().Limit("x", 2)   // Stack overflow, process dies

It is not an exception — the process is killed, so a caller cannot guard against it.

Why

Signumf.ComputeLimitDivideEtImpera is the only node whose override hands back an unevaluated Limitf of itself:

partial record Signumf
{
    // TODO:
    internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side)
        => new Limitf(this, x, dist, side);
}

The two-sided finite path then compares the two one-sided results with ExpressionNumerical.AreEqual, which evaluates them; evaluating a Limitf calls Limit again; and that returns the same node. The cycle is ComputeLimit → Limitf.InnerSimplify → Limit → ComputeLimit, repeated some four thousand times before the stack runs out.

lim x->2 abs(x) is fine, because Absf maps the limit through its argument instead.

The fix

Returning null rather than a Limitf of itself. null means the same thing to the caller — no reading — and takes the branch that falls through to l'Hopital's rule and then returns, instead of the branch that evaluates and re-enters. The user still gets an unevaluated Limitf back from the public API, so nothing about the answer changes; only the recursion goes away.

Better still would be an actual reading: signum is continuous away from 0, so the limit is the value at the point wherever the argument's limit is non-zero.

I hit this writing #703, where a new node had the same shape and the same crash; there I gave it a real limit case. I have not touched Signumf in that PR since it is unrelated to modulus, but I am happy to send a separate one.

Found on 6c1f6b49, .NET 10, Linux.

Activity

  1. Rafael-SOWNet commented on Aug 4, 2026

    @Rafael-SOWNet
    MemberAuthor

    Fixed in #705, merged as 0bde6372.

    "signum(x)".ToEntity().Limit("x", 2)   // 1, where it used to kill the process
    

    Signumf now reads the limit off its argument: wherever the argument tends to anything but zero, the limit is the sign of that, including at the infinities where it is 1 and -1. At zero it returns null — the sign is 1 on one side and -1 on the other — and null is what stops the recursion, since it takes the branch that returns rather than the one that evaluates and re-enters.

    13 tests, three of which assert termination rather than a value, so a regression fails the run instead of taking the runner down with it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions