diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-28 10:29:05 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-28 10:29:05 +0100 |
| commit | 78e04c2afe4878aa29108785cce1e32b9072c265 (patch) | |
| tree | b67362186083886b05beac0a0a53156f226ac610 | |
| parent | 3802f311f135346487c5c1d5120e34b3aed82b13 (diff) | |
| download | generat3D-78e04c2afe4878aa29108785cce1e32b9072c265.tar.gz generat3D-78e04c2afe4878aa29108785cce1e32b9072c265.zip | |
Make formula evaluation fail only with Error
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
| -rw-r--r-- | generat3d/formula.py | 33 | ||||
| -rw-r--r-- | tests/test_formula.py | 36 |
2 files changed, 57 insertions, 12 deletions
diff --git a/generat3d/formula.py b/generat3d/formula.py index 4a6a0d9..67d25e0 100644 --- a/generat3d/formula.py +++ b/generat3d/formula.py @@ -68,14 +68,18 @@ class Graph: return None path.append(key) total, factor = prop.value, 1.0 - for stage, ops, source in self.formulas.get(key, ()): - r = self._run(key, ops, done, path, source) - if r is None: - continue - if stage == "sum": - total += r - else: - factor *= r + try: + for stage, ops, source in self.formulas.get(key, ()): + r = self._run(key, ops, done, path, source) + if r is None: + continue + if stage == "sum": + total += r + else: + factor *= r + except RecursionError: + # ponytail: recursive evaluation; make it iterative if real content hits this + raise Error("formula dependency chain too deep") from None path.pop() v = total * factor if prop.clamped: @@ -105,11 +109,16 @@ class Graph: stack.append(_spline_op(name, stack)) else: raise Error(f"{source}: unknown formula operation {name!r} for {key}") - except (IndexError, KeyError, TypeError, ZeroDivisionError, ValueError): + if len(stack) != 1: + raise Error(f"{source}: malformed formula for {key}") + r = stack[0] + if not (isinstance(r, (int, float)) and not isinstance(r, bool) + and math.isfinite(r)): + raise Error(f"{source}: malformed formula for {key}") + return r + except (IndexError, KeyError, TypeError, ZeroDivisionError, ValueError, + OverflowError): raise Error(f"{source}: malformed formula for {key}") from None - if len(stack) != 1: - raise Error(f"{source}: malformed formula for {key}") - return stack[0] def _spline_op(name, stack): diff --git a/tests/test_formula.py b/tests/test_formula.py index 8074e5a..72f41ea 100644 --- a/tests/test_formula.py +++ b/tests/test_formula.py @@ -94,3 +94,39 @@ class GraphTest(unittest.TestCase): self.g.add_formula("b", [val(1), val(2)], source="f") with self.assertRaisesRegex(Error, "f: malformed formula for b"): self.g.evaluate() + + def test_non_numeric_result_is_fatal(self): + self.g.add_formula("b", [val("x")], source="f") + with self.assertRaisesRegex(Error, "f: malformed formula for b"): + self.g.evaluate() + + def test_overflow_is_fatal(self): + self.g.add_formula("b", [val(10**400), val(0.5), {"op": "mult"}], source="f") + with self.assertRaisesRegex(Error, "f: malformed formula for b"): + self.g.evaluate() + + def test_nan_result_is_fatal(self): + self.g.add_formula("b", [val(float("nan"))], source="f") + with self.assertRaisesRegex(Error, "f: malformed formula for b"): + self.g.evaluate() + + def test_deep_chain_is_fatal(self): + g = Graph() + n = 2000 + for i in range(n): + g.add_prop(f"p{i}") + for i in range(n - 1): + g.add_formula(f"p{i}", [push(f"p{i + 1}")]) + with self.assertRaisesRegex(Error, "too deep"): + g.evaluate() + + def test_diamond_dependency(self): + g = Graph() + g.add_prop("a", 1.0) + g.add_prop("b") + g.add_prop("c") + g.add_prop("d") + g.add_formula("b", [push("a")]) + g.add_formula("c", [push("a"), push("a"), {"op": "add"}]) + g.add_formula("d", [push("b"), push("c"), {"op": "add"}]) + self.assertEqual(g.evaluate()["d"], 3) |
