Skip to content

Refuse a function defined twice in one file - #25

Merged
martin-k-m merged 2 commits into
mainfrom
check/refuse-redefinition
Sep 3, 2026
Merged

martin-k-m merged 2 commits into
mainfrom
check/refuse-redefinition

Conversation

@martin-k-m

Copy link
Copy Markdown
Collaborator

#24 wrote the footgun down. This is the fix.

$ twill check dup.tw
dup.tw:2: shape error: f is already defined on line 1; the later definition is the one that runs, so the earlier one is dead. Delete whichever is stale, or rename one.
  2 | fn f() = 2

The evaluator takes the last definition and says nothing, so a replacement written above the body it was meant to replace leaves the old body running and the file reads as though it does not. spool shipped that in two files during the 1.9.0 sort adoption — through a passing test suite, a passing source gate and passing CI, because nothing in any of them looked at whether a name was defined twice.

Where it goes

In the prelude pass that already walks every top-level FnDecl to register its name, so it costs one map and no extra traversal. Both checkers get it, internal/checker/checker.go and src/check.tw, and the two messages are byte-identical — the differential harness compares stdout exactly, so parity was not optional:

$ twill check dup.tw            | sha256 == $ twill run src/main.tw check dup.tw

The message names the winner, because the whole failure is someone believing the other one won, and it points at the redefinition rather than the original, which is the line to go look at.

What was measured, not assumed

  • It catches the real bug. Against spool@HEAD~1:src/strutil.tw, the actual file before the fix: sort_strs is already defined on line 338.
  • It fires on nothing else. 458 .tw files across src, std, testdata, examples and all six satellites. Zero hits.

There is no conditional compilation in twill, so the readings under which somebody means a second declaration — a platform variant, a debug build — do not exist. That is why the false-positive rate should be zero, and it is.

Tests

internal/checker/redefine_test.go, ten of them: the message, the line number (2, not 1), the severity, one report per redefinition so deleting one does not hide the next, spool's exact shape, and the two things that must not trip it — a local shadowing a function name, and a function deliberately shadowing a builtin, which twill supports and which this must not break.

Also moves the entry out of docs/BUGS.md's Open section into the numbered record as entry 11, and adds the Unreleased changelog note.

🤖 Generated with Claude Code

martin-k-m and others added 2 commits September 2, 2026 23:47
The evaluator takes the last definition and says nothing, so a replacement
written above the body it was meant to replace leaves the old body running
and the file reads as though it does not. spool had that in two files at
once and it went through a passing test suite, a passing source gate and
passing CI.

The check is in the pass that already walks every top-level FnDecl to
register its name, so it costs one map. Both checkers get it, and the
message is byte-identical between them, which the differential harness
requires.

The message names the winner, because the whole failure is someone
believing the other one won, and it points at the redefinition rather than
the original, which is the line to go look at.

There is no conditional compilation in this language, so there is no
reading under which a second declaration of one name in one file is meant.
Swept 458 .tw files across twill, std, testdata and the six satellites:
no false positives, and it catches spool's bug at the commit before the
fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BUGS.md entry 11, with what it cost, why the prelude pass let it through
and what the sweep measured. The Open section had it as of an hour ago
with the fix described as obvious and not written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@martin-k-m
martin-k-m merged commit 698a868 into main Sep 3, 2026
3 checks passed
@martin-k-m
martin-k-m deleted the check/refuse-redefinition branch September 3, 2026 05:05
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