|
39 | 39 | (string->number v) |
40 | 40 | 2)))) |
41 | 41 |
|
| 42 | +;; `!`-purity enforcement (Phase 6 — design-purity.md). Seeded from |
| 43 | +;; BEAGLE_PURITY ('off | 'warn | 'error). Phase 6.0 ships DARK: the default |
| 44 | +;; is 'off, which short-circuits check-purity! entirely so the pass is inert |
| 45 | +;; in every build and cannot turn into a new diagnostic for the live |
| 46 | +;; consumers. Mirrors the BEAGLE_CHECK_PROFILE env precedent exactly. Flip on |
| 47 | +;; per rollout stage (warn/error) without touching consumer source. |
| 48 | +(define current-purity-enforcement |
| 49 | + (make-parameter |
| 50 | + (case (getenv "BEAGLE_PURITY") |
| 51 | + [("warn") 'warn] |
| 52 | + [("error") 'error] |
| 53 | + [else 'off]))) |
| 54 | + |
42 | 55 | (define (merge-types . ts) |
43 | 56 | (define non-any (filter (λ (t) (not (any-type? t))) ts)) |
44 | 57 | (cond |
|
314 | 327 | [(template-splice) "E016"] |
315 | 328 | [(macro-expansion-type-error) "E017"] |
316 | 329 | [(unresolved-alias) "E018"] |
| 330 | + [(purity-leak) "E019"] |
317 | 331 | [else "E000"])) |
318 | 332 |
|
319 | 333 | ;; Expected/actual detail pair carrying BOTH the human strings (kept verbatim, |
|
490 | 504 | (check-form form env)))) |
491 | 505 | (check-qualified-resolution! prog env) |
492 | 506 | (check-zig-world-escape! prog) |
493 | | - (check-scalar-provenance! prog))) |
| 507 | + (check-scalar-provenance! prog) |
| 508 | + (check-purity! prog))) |
494 | 509 |
|
495 | 510 | ;; --- environment ----------------------------------------------------------- |
496 | 511 |
|
|
3212 | 3227 | syms) |
3213 | 3228 |
|
3214 | 3229 |
|
| 3230 | +;; --- `!`-purity enforcement (Phase 6 — design-purity.md) ------------------- |
| 3231 | +;; |
| 3232 | +;; The operative thesis's load-bearing promise is static-reasoning recovery: |
| 3233 | +;; "the absence of mutation markers in a piece of code means that code is |
| 3234 | +;; functionally pure." check-purity! makes the `!`-suffix naming convention a |
| 3235 | +;; checked invariant, one direction only and purely syntactically: |
| 3236 | +;; |
| 3237 | +;; A defn/defn- whose NAME does not end in `!` must have a PURE BODY — its |
| 3238 | +;; body must contain no mutation marker (no set!-form, and no call whose head |
| 3239 | +;; is a symbol ending in `!`). If it does, that is a 'purity-leak. |
| 3240 | +;; |
| 3241 | +;; Intraprocedural and syntactic only: it descends let/if/do/fn/when/cond/… |
| 3242 | +;; (an inner fn's effects still run when this function is called) but never |
| 3243 | +;; across defn/def boundaries — those are separate definitions. No |
| 3244 | +;; interprocedural inference, no effect rows; the converse (a `!`-named defn |
| 3245 | +;; with a pure body) is allowed. |
| 3246 | +;; |
| 3247 | +;; GATING (so it never breaks the live consumers): |
| 3248 | +;; * mode gate — runs only under (define-mode strict); |
| 3249 | +;; * env/feature flag — current-purity-enforcement, seeded from BEAGLE_PURITY, |
| 3250 | +;; default 'off. 'off short-circuits the whole pass: it ships DARK. |
| 3251 | +;; * severity is profile-keyed: profile < 3 => warn-only (never blocks the |
| 3252 | +;; build); profile >= 3 => hard error via raise-diag. 'off overrides both. |
| 3253 | + |
| 3254 | +(define (bang-name? sym) |
| 3255 | + (and (symbol? sym) |
| 3256 | + (let ([s (symbol->string sym)]) |
| 3257 | + (and (> (string-length s) 0) |
| 3258 | + (char=? (string-ref s (sub1 (string-length s))) #\!))))) |
| 3259 | + |
| 3260 | +;; Collect every mutation marker (the symbol 'set!, or the head symbol of a |
| 3261 | +;; `!`-headed call) lexically present in an AST subtree. Reuses the same |
| 3262 | +;; sub-expression descent as walk-for-provenance/symbols-in so it tracks new |
| 3263 | +;; forms automatically; it does NOT recurse across nested defn/def boundaries. |
| 3264 | +(define (collect-markers node) |
| 3265 | + (define markers '()) |
| 3266 | + (define (note! m) (set! markers (cons m markers))) |
| 3267 | + (define (walk e) |
| 3268 | + (cond |
| 3269 | + [(set!-form? e) |
| 3270 | + (note! 'set!) |
| 3271 | + (walk (set!-form-target e)) |
| 3272 | + (walk (set!-form-value e))] |
| 3273 | + [(call-form? e) |
| 3274 | + (define fn (call-form-fn e)) |
| 3275 | + (when (bang-name? fn) (note! fn)) |
| 3276 | + (walk fn) |
| 3277 | + (for-each walk (call-form-args e))] |
| 3278 | + [(let-form? e) |
| 3279 | + (for ([b (in-list (let-form-bindings e))]) (walk (let-binding-value b))) |
| 3280 | + (for-each walk (let-form-body e))] |
| 3281 | + [(if-form? e) |
| 3282 | + (walk (if-form-cond-expr e)) |
| 3283 | + (walk (if-form-then-expr e)) |
| 3284 | + (when (if-form-else-expr e) (walk (if-form-else-expr e)))] |
| 3285 | + [(when-form? e) |
| 3286 | + (walk (when-form-cond-expr e)) |
| 3287 | + (for-each walk (when-form-body e))] |
| 3288 | + [(do-form? e) |
| 3289 | + (for-each walk (do-form-body e))] |
| 3290 | + ;; Inner fns count — their effects execute in this function's call. |
| 3291 | + [(fn-form? e) |
| 3292 | + (for-each walk (fn-form-body e))] |
| 3293 | + [(cond-form? e) |
| 3294 | + (for ([c (in-list (cond-form-clauses e))]) |
| 3295 | + (walk (cond-clause-test c)) |
| 3296 | + (for-each walk (cond-clause-body c)))] |
| 3297 | + [(for-form? e) |
| 3298 | + (for ([c (in-list (for-form-clauses e))]) |
| 3299 | + (when (for-binding? c) (walk (for-binding-expr c)))) |
| 3300 | + (for-each walk (for-form-body e))] |
| 3301 | + [(doseq-form? e) |
| 3302 | + (for ([c (in-list (doseq-form-clauses e))]) |
| 3303 | + (when (for-binding? c) (walk (for-binding-expr c)))) |
| 3304 | + (for-each walk (doseq-form-body e))] |
| 3305 | + [(case-form? e) |
| 3306 | + (walk (case-form-test e)) |
| 3307 | + (for ([c (in-list (case-form-clauses e))]) (walk (case-clause-body c))) |
| 3308 | + (when (case-form-default e) (walk (case-form-default e)))] |
| 3309 | + [(loop-form? e) |
| 3310 | + (for-each walk (loop-form-body e))] |
| 3311 | + [(match-form? e) |
| 3312 | + (walk (match-form-target e)) |
| 3313 | + (for ([c (in-list (match-form-clauses e))]) |
| 3314 | + (for-each walk (match-clause-body c)))] |
| 3315 | + [(try-form? e) |
| 3316 | + (for-each walk (try-form-body e)) |
| 3317 | + (for ([c (in-list (try-form-catches e))]) (for-each walk (catch-clause-body c))) |
| 3318 | + (when (try-form-finally-body e) (for-each walk (try-form-finally-body e)))] |
| 3319 | + [(with-form? e) |
| 3320 | + (walk (with-form-target e)) |
| 3321 | + (for ([u (in-list (with-form-updates e))]) (walk (with-update-value u)))] |
| 3322 | + [(vec-form? e) |
| 3323 | + (for-each walk (vec-form-items e))] |
| 3324 | + [(map-form? e) |
| 3325 | + (for ([p (in-list (map-form-pairs e))]) (walk (car p)) (walk (cdr p)))] |
| 3326 | + ;; Stop at nested definitions — those are separate (intraprocedural rule). |
| 3327 | + [(defn-form? e) (void)] |
| 3328 | + [(defn-multi? e) (void)] |
| 3329 | + [(def-form? e) (void)] |
| 3330 | + [(pair? e) (for-each walk e)] |
| 3331 | + [else (void)])) |
| 3332 | + (for-each walk (if (list? node) node (list node))) |
| 3333 | + (remove-duplicates (reverse markers))) |
| 3334 | + |
| 3335 | +;; Effective severity from the two enforcement dials. |
| 3336 | +;; 'off flag -> 'off (nothing fires; the pass is dark) |
| 3337 | +;; 'error flag -> 'error (author pins a hard stop) |
| 3338 | +;; 'warn flag -> 'warn below profile 3, escalated to 'error at >= 3 |
| 3339 | +;; (severity profile-keyed per design-purity.md §b) |
| 3340 | +(define (purity-severity) |
| 3341 | + (case (current-purity-enforcement) |
| 3342 | + [(off) 'off] |
| 3343 | + [(error) 'error] |
| 3344 | + [(warn) (if (>= (current-check-profile) 3) 'error 'warn)] |
| 3345 | + [else 'off])) |
| 3346 | + |
| 3347 | +(define (check-defn-purity name body src-table node) |
| 3348 | + (define markers (collect-markers body)) |
| 3349 | + (when (and (not (bang-name? name)) (pair? markers)) |
| 3350 | + (define src (and src-table (hash-ref src-table node #f))) |
| 3351 | + (define msg |
| 3352 | + (format "purity leak: '~a' has no '!' suffix but its body uses ~a — rename to '~a!' or remove the effect" |
| 3353 | + name |
| 3354 | + (string-join (map (lambda (m) (format "~a" m)) markers) ", ") |
| 3355 | + name)) |
| 3356 | + (case (purity-severity) |
| 3357 | + [(warn) |
| 3358 | + (fprintf (current-error-port) |
| 3359 | + "warning: ~a~a\n" |
| 3360 | + msg |
| 3361 | + (if src (format "\n --> ~a:~a" (or (src-loc-source src) "?") (src-loc-line src)) ""))] |
| 3362 | + [(error) |
| 3363 | + (raise-diag 'purity-leak msg (hasheq) #:src src)] |
| 3364 | + [else (void)]))) |
| 3365 | + |
| 3366 | +(define (check-purity! prog) |
| 3367 | + (when (and (eq? (program-mode prog) 'strict) |
| 3368 | + (not (eq? (current-purity-enforcement) 'off))) |
| 3369 | + ;; Mode + flag gate passed; per-diagnostic severity is decided by |
| 3370 | + ;; purity-severity (warn below profile 3, hard error at >= 3). |
| 3371 | + (define st (program-src-table prog)) |
| 3372 | + (for ([form (in-list (program-forms prog))]) |
| 3373 | + (let walk ([f form]) |
| 3374 | + (cond |
| 3375 | + [(defn-form? f) |
| 3376 | + (check-defn-purity (defn-form-name f) (defn-form-body f) st f)] |
| 3377 | + [(defn-multi? f) |
| 3378 | + (for ([a (in-list (defn-multi-arities f))]) |
| 3379 | + (check-defn-purity (defn-multi-name f) (arity-clause-body a) st f))] |
| 3380 | + ;; Descend through wrapper forms (js/export, target-case, …) that may |
| 3381 | + ;; carry a defn payload — same transitive walk type-check! uses. |
| 3382 | + [(and (pair? f) (list? f)) (for-each walk (filter pair? (cdr f)))] |
| 3383 | + [else (void)]))))) |
| 3384 | + |
| 3385 | + |
3215 | 3386 | ;; --- zig world-escape check (thread 20260612232001, Phase 2) --------------- |
3216 | 3387 | ;; |
3217 | 3388 | ;; Convention B, generalized to systems (ECS direction, 2026-06-13): |
|
3507 | 3678 |
|
3508 | 3679 | (provide type-check! type-check-with-locs! |
3509 | 3680 | check-scalar-provenance! |
| 3681 | + check-purity! |
3510 | 3682 | beagle-diagnostic beagle-diagnostic? |
3511 | 3683 | beagle-diagnostic-kind beagle-diagnostic-details |
3512 | 3684 | kind->error-code |
3513 | 3685 | current-check-profile |
| 3686 | + current-purity-enforcement |
3514 | 3687 | check-form infer-expr build-initial-env) |
0 commit comments