← board

Small NilPy syntax gaps found by the 2026-08-06 differential sweep

(Extended later the same day as the sweep widened from unit-shaped probes to real programs; the last four rows came from a JSON writer, a hash table and a matrix builder rather than from targeted probing.)

All of these fail loudly, and most with a clear, accurate diagnostic — none is a silent wrong value, and each names its workaround. They are grouped in one ticket because each is small and they were found in one sweep; split if any turns out to be substantial. The last three rows have the weakest diagnostics — "undefined variable (c)" and "expected expression" do not tell the author that the FORM is unsupported, which is worth fixing even before the forms themselves are.

form today's diagnostic
"{} {} {}".format(a, b, c) ".format() takes one or two arguments here; three or more placeholders are not implemented yet — use an f-string"
enumerate("ab") "enumerate() over a str is not supported yet — enumerate(list(s)) works"
type(1) == int "type(x) is only supported as type(x).name"
lambda x, y=1: x + y "a lambda default capture must be a plain name (key=key)"
print("a", "b", sep="-") "print sep= is not supported yet"
d = dict(x=1, y=2) "dict has no parameter named 'x'"
f.update(b=2) "TPyDict.update has no parameter named 'b'"
c[::2] = [7, 8] (extended-slice assign) "assigning to an extended slice (with a step) is not implemented; only x[lo:hi] = src is"
self.__class__.__name__ AttributeError: 'Base' object has no attribute '__class__' (runtime) — note type(self).__name__ works
(c, d), e = (3, 4), 5 (nested unpacking) "undefined variable (c)"
t = 1, 2, 3 (bare tuple, no parens) "expected expression" — note t = (1, 2, 3) and p, q = two() both work
[(i, j) for i in range(2) for j in range(2)] (two-for comprehension) "undefined variable (j)"

Everything else the sweep covered agrees with CPython: int/float arithmetic and division semantics, string methods and slicing, list/dict/set operations and comprehensions, classes with inheritance and super(), __str__/__eq__/ __lt__/__add__/__len__ on a statically-known instance, closures, default args, exceptions (try/except/else/finally, custom classes, IndexError/ KeyError/ValueError), aliasing and mutable-default semantics, f-strings with width/precision/alignment specs, %-formatting, for/else, while/else, chained comparisons, and sorted(key=…, reverse=…).

The real bugs that same sweep found are filed separately: [[bug-nilpy-augmented-assignment-truncates-to-32-bits]], [[bug-nilpy-floordiv-mod-compare-and-float-narrow-a-variant-held-bignum]], [[bug-nilpy-int-of-a-long-decimal-string-narrows]], [[bug-nilpy-for-rejects-an-inline-suite]].

sorted() over instances with __lt__ also raises, but that is already [[bug-nilpy-dunders-not-dispatched-through-containers]] (blocked on [[decide-nilpy-runtime-dunder-dispatch-strategy]]) — this sweep re-confirmed it, it is not a new find.

Gate

Per-fix loop, per item. A .npy test per form diffed against CPython.

2026-08-08 — two rows done: print(sep=) and .format() with 3+ (and 0)

Re-measured all twelve rows first; every one still failed as recorded. Two are now fixed. The ticket stays open for the other ten.

print("a", "b", sep="-")

The refusal carried its own reason — "separators are injected DURING the argument loop, while a keyword argument is only seen after it" — and that was true, so the fix was to stop reading it in the loop. PyPrintSepAhead (pyparser.inc) scans this call's own token range for sep=<string literal> before any argument is parsed, so the value is known when the first separator is built. Depth-tracked over parens, brackets and braces, so a NESTED call's own sep= keyword, a list/dict display, an f-string or a subscript inside the argument list cannot be mistaken for print's own.

sep="" injects nothing (the arguments abut). A literal is required, like end= — the separator is materialised at parse time as the constant between two arguments, so a run-time value would need a different lowering. print(*x) with sep= is refused: pyprint_star renders a whole unpacked run into one string with the space hard-coded, and silently ignoring the sep on the starred run only would be worse than saying so.

"{} {} {}".format(a, b, c)

The one-proc-per-arity scheme (pystr_format, pystr_format2 — separate names because FindProc is not arity-aware) does not extend, so it ends here rather than growing a third name. pystr_formatn is one FIXED-arity proc taking eight Variants plus a real count; the frontend pads the unused slots with pynone. Past eight it refuses loudly and names f-strings, which have no limit.

The substitution itself moved to ONE place — PyFormatApply now takes the argument LIST rather than (a, b, nArgs) — so positional indices ({0} {2} {1}, {2}-{2}) and format specs cannot drift between arities. Arity 1 and 2 go through the same code via one- and two-element lists.

Zero arguments ("plain".format()) is valid CPython and was refused by the same gate; it routes through pystr_formatn with n=0.

Tests: test/test_nilpy_print_sep.npy, test/test_nilpy_format_multiarg.npy, both wired into make test-nilpy and diffed against CPython (exact match).

Still open

enumerate(str), type(x) == int, non-name lambda default, dict(x=1), .update(b=2), extended-slice assign, self.__class__.__name__, nested unpacking, bare tuple t = 1, 2, 3, two-for comprehension.