Skip to content

Match OpenSCAD's argument and arithmetic diagnostics - #99

Merged
revarbat merged 1 commit into
mainfrom
openscad-diagnostic-parity
Aug 16, 2026
Merged

Match OpenSCAD's argument and arithmetic diagnostics#99
revarbat merged 1 commit into
mainfrom
openscad-diagnostic-parity

Conversation

@revarbat

Copy link
Copy Markdown
Member

Why

ord(undef) printed no warning where real OpenSCAD does. Chasing that found the same hole everywhere: we produced the right undef value for a bad argument or operand — silently — for every builtin and every arithmetic operator. A misspelled variable reaching abs(), len() or + said nothing at all.

How it was checked

Generated 1002 cases and ran them through OpenSCAD 2026.02.01 (~/Desktop/OpenSCAD-dev.app, not the 2022 build in /Applications — it differs) and through us, comparing both value and warning text:

Harness Cases Covers
operator matrix 392 every ordered pair of {undef, number, bool, string, vector, range, function} for + - * / % ^ and unary -
builtin conversions 205 every builtin with each argument wrong in turn
search() 225 needle × haystack × num_returns × index_col
undef survey 87 the original question, incl. all comparators
arity / edge / nested 93 missing + extra args, div-by-zero, ragged matrices

All 1002 now agree, except three rands() cases (random) and version() (the binary is 2026.02.01; we report 2026.01.01).

Diagnostics added

All reference-exact, none invented:

undefined operation (undefined + number)              + - * / % ^, unary -
NAME() parameter could not be converted: argument N: expected T, found T (v)
NAME() parameter could not be converted: vector element N: ...      max/min
NAME() number of parameters does not match: expected N, found M
vector*vector requires matching lengths (2 != 3)      + matrix siblings
Multiplication is undefined on empty vectors
Matrix must be rectangular / must contain only numbers. Problem at row N
Incorrect arguments to norm()
Invalid vector size of parameter for cross()
Invalid value in parameter vector for cross()
Invalid entry in search vector at index N, required ...
Parent module index (N) greater than the number of modules on the stack

Value fixes found along the way

Each verified against the binary, not inferred:

Expression Was Now (= OpenSCAD)
-7 % 3 2 -1
5 % 0 undef nan
0 ^ -1 undef inf
5 / [1,2] undef [5, 2.5]
[] * [] 0 undef + warning
search(undef, "abc") [] undef
search("a", undef) undef []
search("a", ["a","b"]) [0] [] + warning
has_key(obj, undef) false undef
chr(65, 66) "A" "AB"
chr(-1), chr(1e9) raw invalid UTF-8 ""
echo(object(a=1)) object(a = 1) { a = 1; }
abs(1, 2) 1 undef + warning

⚠️ % is the one change that can alter rendered output. Ours was a floored/Python modulo; the reference is C's fmod, so the result takes the left operand's sign. A real BOSL2 model (spur_gear + prismoid + path_sweep) exports byte-identical before and after, so nothing in normal use depended on the old sign.

Also: oscTypeName called ranges and functions "undefined", which put a phantom undef operand into every message naming one.

Comparators were already correct and are untouched< > <= >= warn and answer undef; == != never warn.

Note on search()

The port follows current upstream, which includes a crash fix (f2be6d0d2, 2026-07-02) that the 2026.02.01 binary predates — a non-string entry at index_col aborts that binary with bad_variant_access. Those shapes are excluded from the binary comparison and follow the fixed source.

Tests

878 pass under OSCAD_BYTECODE_VM=0 and =1. BelfrySCAD's own 794 pass against this build. Five existing tests encoded the old wrong behavior and were corrected with the reference output alongside.

Version 0.32.2 → 0.33.0 (minor: real behavior changes).

🤖 Generated with Claude Code

`ord(undef)` printed no warning where real OpenSCAD does. Chasing that found
the same hole everywhere: we emitted the right undef VALUE for a bad
argument or a bad operand type, silently, for every builtin and every
arithmetic operator. A misspelled variable reaching abs(), len() or `+` said
nothing at all.

Checked case-by-case against OpenSCAD 2026.02.01 (~/Desktop/OpenSCAD-dev.app,
not the 2022 build in /Applications, which differs) over 1002 generated
cases: every ordered type pair for + - * / % ^ and unary minus, every builtin
with each argument wrong in turn, and search() across needle/haystack/
num_returns/index_col combinations. All 1002 now agree on both value and
warning, bar three rands() cases (random) and version() (the binary is
2026.02.01, we report 2026.01.01).

Diagnostics added, all reference-exact:

  undefined operation (undefined + number)          -- + - * / % ^, unary -
  NAME() parameter could not be converted: argument N: expected T, found T (v)
  NAME() parameter could not be converted: vector element N: ...   -- max/min
  NAME() number of parameters does not match: expected N, found M
  vector*vector requires matching lengths (2 != 3), and its matrix siblings
  Multiplication is undefined on empty vectors
  Matrix must be rectangular / must contain only numbers. Problem at row N
  Incorrect arguments to norm()
  Invalid vector size of parameter for cross()
  Invalid value in parameter vector for cross()
  Invalid entry in search vector at index N, required ...
  Parent module index (N) greater than the number of modules on the stack

Value fixes found while doing it -- each one verified against the binary,
not inferred:

- `%` was a floored/Python modulo. The reference is C's fmod, so the result
  takes the LEFT operand's sign: -7 % 3 is -1, not 2. `x % 0` is nan, not
  undef. This is the one change here that can alter rendered output; a real
  BOSL2 model (spur_gear + prismoid + path_sweep) exports byte-identical
  before and after, so nothing in normal use depends on the old sign.
- `0 ^ -1` is inf, not undef -- pow() straight through, no zero special-case.
- `number / vector` divides element-wise: 5 / [1,2] is [5, 2.5], was undef.
- `[] * []` is undef with a message, was 0.
- search() dispatches on what is searched FOR (number/string/vector only,
  everything else undef) and never type-checks what it searches IN (a
  non-vector is an empty table). Both were backwards: search(undef, "abc")
  answered [] and search("a", undef) answered undef. A string needle in a
  vector table also requires vector entries, so search("a", ["a","b"]) is []
  with a warning, not [0].
- has_key(obj, undef) is undef, not false -- false reads as "no such key"
  rather than "that is not a key".
- chr() is variadic (chr(65,66) == "AB") and range-checked. An out-of-range
  or surrogate codepoint contributes nothing; it used to emit raw invalid
  UTF-8 that escaped and broke the caller's own decoding.
- Objects echo as `{ a = 1; b = "x"; }`, not `object(a = 1, b = "x")`.
- oscTypeName called ranges and functions "undefined", which put a phantom
  undef operand into every message naming one.

Comparators were already correct and are unchanged: < > <= >= warn and
answer undef, == and != never warn.

The extra-argument case now answers undef as the reference does, so
abs(1, 2) and is_num(1, 2) are undef rather than quietly using the first
argument. search()'s port also picks up an upstream crash fix (f2be6d0d2,
2026-07-02) that the 2026.02.01 binary predates -- a non-string entry at
index_col aborts that binary with bad_variant_access.

878 tests pass under both engines; BelfrySCAD's own 794 pass against this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@revarbat
revarbat merged commit a96b064 into main Aug 16, 2026
3 checks passed
@revarbat
revarbat deleted the openscad-diagnostic-parity branch August 16, 2026 14:58
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.

1 participant