Shipmind Labs

A parser that can raise RecursionError has one error message it did not write and cannot explain.

That is what unbounded recursive descent costs you. The failure comes from inside Python, it carries a stack trace instead of a position, and it tells the user nothing about which formula broke or where.

In finmodel, our open-source financial modelling project, the formula module refuses what it cannot evaluate, and it refuses at the boundary.

Depth is a declared limit. The module states MAX_DEPTH = 128 and threads a depth counter through the recursive descent in the parser. Parsing starts the sum rule at depth 0, and the sum, product and signed rules each call a _deeper step before they descend. The atom rule calls it too, when it opens a parenthesis. Once depth reaches MAX_DEPTH, _deeper raises a FormulaError naming the position where the bound was passed.

The part worth copying: nested parentheses, a long left-leaning chain of operations and repeated unary signs all count the same way. No shape of input grows the Python stack without growing the counter too. A deep formula turns into a named error instead of a RecursionError from somewhere in the middle of the parser.

Values get the same treatment, checked where they enter rather than where they explode. The conversion function passes a NotApplicable through untouched, runs a Decimal through a finiteness check, converts a non-bool int directly, and routes a float or string through Decimal(str(value)) before that same finiteness check. The check asks whether the value is finite, and if it is not, it raises a FormulaError saying the value of the named line is not a finite number.

So a NaN or an infinity gets rejected by the line that held it. It does not travel downstream and resurface as an InvalidOperation out of rounding, or out of a comparison in a verification check, where the message would name the wrong line.

The tokeniser closes the last gap: the token regex is compiled with re.ASCII, so the number pattern matches ASCII digits only. A digit from another script is not quietly accepted as a number, it is an unexpected character, reported at its position.

One principle sits under all three. Every input that could produce an error from inside the language gets converted, at the boundary, into an error in the system's own vocabulary, with a position and a line name attached. Depth bound, finiteness check, ASCII tokens: three different mechanisms, one rule.

A practical test for any parser or evaluator you own is to list every exception type a bad input can produce. If that list includes something from the standard library's internals, there is a boundary check missing.

Where you draw that boundary is the choice you are left with, at parse time or at evaluation time.

Was this useful?

Building something similar?

or email hello@shipmindlabs.com