Skip to content

scheme globals: indentation doesn't track scope — the rule is 42.7% precise, and the #2651 col-0 anchor trades 67 false positives for 32 real globals #2674

Description

@squid-protocol

Split out of the #2660 audit (Batch B.2 of #2669). #2660 listed scheme as a candidate for the #2651 ^[ \t]*^(?![ \t]) anchor change. There is a real defect — a bad one — but the anchor is the wrong instrument, and the measurement below is the reason. Filing separately so #2660 can close on go alone.

Current rule

"globals": re.compile(r"^[ \t]*\([ \t]*define\s+[a-zA-Z0-9_!?*+/<>=.~$%^&:-]+\s+[^(\s]")

In Scheme, indentation does not track scope — the enclosing form does. Two constructs are indented identically and mean opposite things:

(define (f x)
  (define y 5)        ; internal define -> a LOCAL binding. False positive.
  y)

(let ()               ; whole-file module wrapper (a standard Chez/R6RS idiom)
  (define track-counts #f)   ; module-level binding -> a REAL global, and it is indented.
  ...)

Measurement

I wrote a paren-depth classifier (skipping strings, ; comments, #| |# blocks, #\( char literals) that resolves each (define …) to its nearest enclosing form and labels it module-level or internal, then joined that against what each regex actually matches, over the real language-crucible scheme sources (Chez + Racket, 1510 defines total: 619 module-level, 891 internal).

Restricted to the lines this rule's form actually targets:

rule true globals internal (false positives) precision
current (^[ \t]*) 53 71 42.7%
#2651 anchored (^(?![ \t])) 21 4 84.0%

The current rule is wrong more often than it is right. But the anchor is not the fix: it deletes 67 false positives and 32 real globals, cutting true positives from 53 to 21. Precision doubles, recall drops ~60%, and on a risk metric that is undercounting global state, not fixing it.

Why the recall collapse — per-file (define at column 0 vs indented:

file col-0 indented
cpnanopass.ss (Chez; entire body inside (let () …) from line 16) 0 682
io.ss 0 285
syntax.ss 318 341
schemify.rkt 9 119
thread.rkt 83 61

Two of five real files have no column-0 defines at all. The anchor takes them to zero globals — the rule goes blind on the commonest Scheme file layout rather than getting more precise on it.

Fix shape

The discriminator is body position, not column. A (define …) is internal iff its nearest enclosing form is a body position (lambda, case-lambda, the let family, a procedure-form (define (name …) …), when/unless/cond/parameterize, …) and module-level otherwise (top level, or nested only in library / module / define-library / a depth-0 (let () …) file wrapper).

Applied to the same corpus that predicate yields 53 true globals and 0 false positives — it keeps every true positive the current rule finds and drops all 71 false ones.

This is deliberately not a regex change, and therefore not B.3 material. A flat pattern cannot see the enclosing form; this needs a paren-depth pass in the detector, i.e. the slicer layer, which #2669's stacking rule keeps out of a registry-regex PR. The classifier that produced the numbers above is ~60 lines and can seed it. Machinery precedent exists: scheme already has a func_start rule for the procedure form —

"func_start": r"^[ \t\n]*\([ \t\n]*define[ \t\n]+\([ \t\n]*([a-zA-Z0-9_!?*+/<>=.~$%^&:-]+)…"

— so the engine already models "this define opens a procedure"; what is missing is using that to decide whether a later define sits inside its body.

Recommendation

  1. Do not apply the zig + dart: globals rule anchors ^[ \t]* so function-local var/const count as globals (+150% on the rosetta corpus) #2651 anchor to scheme. Recorded on globals rule audit: 11 more languages share zig/dart's ^[ \t]* any-indentation anchor (#2651 follow-up) #2660 as audited-and-rejected.
  2. Treat the body-position predicate as a scoped design item — small, self-contained, one language, with a ready-made oracle (the classifier) and a clear pass mark (53/0 on the crucible corpus).
  3. Until then, ledger scheme globals as a known 42.7%-precision over-count. The rosetta corpus reads the planted globals 2 and cannot see any of this, so nothing here blocks a rebless — and nothing here will be caught by one either.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugUnintended behavior or logic failure in the enginecore-engineModifications to the central physics and parsing enginemetricsHeuristics, risk exposures, and topological math updates

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions