; SPDX-License-Identifier: Apache-2.0 ; ===================================================================== ; prelude.vla - the language's own standard library, written IN VLA. ; F.3: this file used to be VLA.bas's PreludeMacros() function, a ; 322-line VBA string built one "s = s & ""..."" " line at a time - ; every embedded quote doubled to survive being inside a VBA string ; literal (the escape-stack bug class this move deletes), no lint ; coverage of its own, and un-editable by anyone who does not already ; read VBA. Loaded fresh on every compile (VlaTranspile/ ; VlaCompileToForms read this file's text and prepend it to the ; user's source, exactly as PreludeMacros()'s return value used to ; be) - not cached, not injected, the same discipline every other ; per-compile table in this engine already follows (LESSONS.md ; XXXII: a recompile drops module state, so nothing here may depend ; on surviving one). ; ; Every defmacro below is a template macro, not a procedure: pure ; syntactic substitution, no gensym, no eval. The caller names every ; binding (the anti-gensym doctrine) - two uses of a binding template ; in one procedure that share a name collide at VBA's own compile ; step, which is the loud failure this project chooses over a hidden ; hygiene mechanism. ; ===================================================================== ; L3 / P.L0: the earliest control-flow spellings. (defmacro (when test & body) "run the body when the test is true" (if test (then body))) (defmacro (unless test & body) "run the body when the test is false" (if (not test) (then body))) (defmacro (dotimes v n & body) "count v from 1 to n, running the body each time" (for (v 1 n) body)) (defmacro (inc! x) "add 1 to a variable" (set! x (+ x 1))) (defmacro (dec! x) "subtract 1 from a variable" (set! x (- x 1))) (defmacro (add! x n) "add an amount to a variable" (set! x (+ x n))) ; ----------------------------------------------------------------- ; P.L1 (P-PRED): the predicate prelude - Lisp's ?-suffix naming, ; legal here because '?' is an ordinary symbol character to the ; tokenizer and a predicate call expands away before any identifier ; reaches VBA. These are the prelude's first EXPRESSION-shaped ; macros: the expansion walk rebuilds every list element (ExpandMacros' ; no-macro-at-head path), so a predicate expands identically in a ; condition, a set!, or an argument position - no statement-position ; privilege exists to lose. One-line templates, trivially past the ; four questions: one fixed parameter, no rest, no iteration, no ; decisions. 1:1 semantics, stated where VBA has edges: ; even?/odd? ride VBA's Mod, which coerces fractional operands to ; whole numbers first (banker's rounding) - whatever ; Mod says, the predicate says. ; empty? VBA's IsEmpty verbatim: an uninitialized Variant, an ; unwritten cell's Value. ; blank? the spreadsheet-flavored cousin - Empty, Null, "" and ; whitespace-only all read blank, via (& x "") coercion ; (Null & "" is "" by VBA's own & rule), then Trim and ; Len. A multi-cell range value inside blank? type- ; mismatches at run time exactly as the raw VBA would - ; nothing smoothed. ; Bucket-3 note: the vocabulary's condition templates read through ; these ((if (blank? (range {r})) ...)) - the ledger's stated purpose ; for shipping P-PRED first. ; ----------------------------------------------------------------- (defmacro (zero? x) "true when the value is 0" (= x 0)) (defmacro (positive? x) "true when the value is greater than 0" (> x 0)) (defmacro (negative? x) "true when the value is less than 0" (< x 0)) (defmacro (even? x) "true when the value divides by 2 evenly" (= (mod x 2) 0)) (defmacro (odd? x) "true when the value does not divide by 2 evenly" (<> (mod x 2) 0)) (defmacro (empty? x) "true for an uninitialized value - VBA's IsEmpty" (isempty x)) (defmacro (blank? x) "true when the value reads as empty text - Empty, Null, or whitespace" (= (len (trim (& x ""))) 0)) ; ----------------------------------------------------------------- ; P.L2 (P-WHENLET): binding-conditionals, Clojure school. The "if the ; cell has a value, use it" pattern that costs three forms (dim, set, ; if) costs one and reads as intent: ; (when-let x expr & body) body only when x is something ; (if-let x expr then-body else-form) one branch either way; ; then-body and else-form are ; ONE form each - wrap multiple ; statements in (begin ...) ; C1 doctrine throughout: the caller names the binding, the template ; dims it (let-one's precedent), so two uses sharing a name in one ; procedure collide at VBA's compile step - the loud failure the ; doctrine promises. Values only, like let-one - binding an object ; needs Set semantics, so write (dim)/(obj-set!)/(if ...) by hand. ; Dim is procedure-scoped, so the binding OUTLIVES the body - a ; feature here: after a when-let, x still holds the value (or Empty) ; for later statements, exactly as the expansion says it does. ; "Something" means precisely (not (isempty x)) - VBA's Empty: an ; unwritten cell's Value, an uninitialized Variant. Null is NOT Empty ; (a Null database field IS something here), and 0 and "" are ; something too. A caller wanting the broader blank? gate writes the ; if by hand - one form, one meaning, deliberately. The templates ; consume P.L1's empty? - the P-layer building on itself; the stepper ; (VlaExpandStep) shows the intermediate frame with the predicate ; still folded. ; ----------------------------------------------------------------- (defmacro (when-let x expr & body) "bind a value and run the body only when it is not empty" (begin (dim x) (set! x expr) (if (not (empty? x)) (then body)))) (defmacro (if-let x expr then-body else-form) "bind a value and pick the branch by whether it is empty" (begin (dim x) (set! x expr) (if (not (empty? x)) (then then-body) (else else-form)))) ; ----------------------------------------------------------------- ; P.L3 (P-TRYELSE): the ignore-errors-with-default idiom - the ; Lisper's (or (ignore-errors x) default), the spreadsheet automator's ; "the cell's number, or 0" - as one form: ; (try-else tmp expr fallback) ; Six moves: dim, arm resume-next, try the set, check err.number, ; clear-and-fall-back, disarm. C1 doctrine: the caller names the ; binding, the template dims it, duplicate-Dim is the loud collision; ; values only, like its siblings. ; 1:1 facts the template leans on: arming ANY On Error form clears Err ; (VBA's own rule - with-fast-excel's comment records it), which is ; what makes the err.number check honest when expr succeeds. ; Honest limits, stated: ; - the FALLBACK runs under the same Resume Next blanket, so a ; failing fallback leaves the binding Empty and never raises - ; keep fallbacks simple (a literal, a bound name). ; - On Error state is procedure-scoped, not lexical: the closing ; (on-error goto 0) disarms ANY enclosing handler for the rest of ; the procedure (with-fast-excel's recorded physics, same ; statement) - inside with-error-handler, re-arm after the form ; or keep the two apart. ; ----------------------------------------------------------------- (defmacro (try-else tmp expr fallback) "bind an expression's value, or a fallback when it errors" (begin (dim tmp) (on-error resume-next) (set! tmp expr) (if (<> err.number 0) (then (err.clear) (set! tmp fallback))) (on-error goto 0))) ; ----------------------------------------------------------------- ; L16: the small-verbs batch. Templates only, C1's temporary-as- ; parameter doctrine throughout: the caller names tmp, the template ; dims it (let-one's precedent), and two uses sharing a name in one ; procedure collide at VBA's compile step - the loud failure the ; doctrine promises. Values only, like let-one. ; (dec!, the definition's third verb, already lives above - L3 ; shipped it beside inc!; discovered at L16 time, pinned since.) ; swap! the classic three-move swap ; returning CL's prog1 in statement clothes: compute the answer ; FIRST, tidy up, then return the saved value. Ends in ; (return tmp), so a use inside a Sub meets the emitter's ; own trappable teaching refusal - loud and worded, never ; a compile modal. ; ----------------------------------------------------------------- (defmacro (swap! a b tmp) "swap two values through a caller-named temporary" (begin (dim tmp) (set! tmp a) (set! a b) (set! b tmp))) (defmacro (returning tmp expr & body) "compute the answer first, tidy up, then return the saved value" (begin (dim tmp) (set! tmp expr) body (return tmp))) ; ----------------------------------------------------------------- ; L7: (time ...) and (trace-form ...) - V6's spirit at VLA level, as ; templates. C1 throughout: time's start-stamp is a caller-named ; temporary (the definition's C1 clause licenses the extra parameter ; over its shorthand signature); trace-form needs no temporary at all ; - the label IS the trace, entry and exit. ; Honest limits, stated: Timer granularity is ~16 ms and wraps at ; midnight (a run across it prints negative) - single-shot times are ; for the macro scale; micro-benchmarks compose the reps themselves: ; (time "1000 adds" t1 (dotimes i 1000 (add! x 1))). ; (time label tmp & body) label: "...": s ; (trace-form label & body) >> label ... body ... << label ; ----------------------------------------------------------------- (defmacro (time label tmp & body) "print how many seconds the body took, labeled" (begin (dim tmp) (set! tmp (timer)) body (debug-print (& label ": " (- (timer) tmp) " s")))) (defmacro (trace-form label & body) "print entry and exit brackets around the body" (begin (debug-print (& ">> " label)) body (debug-print (& "<< " label)))) ; ----------------------------------------------------------------- ; L8: the check family - unit tests written IN VLA, runnable via ; VlaTry, embeddable as raw rows. Templates on the L7 pattern: the ; caller supplies the label, so no expansion-time stringification ; exists anywhere near the four questions. The definition's "etc." ; resolved to the two workhorses (more verbs are one-line adds when ; demanded - the small-verbs doctrine). ; Honest limit, stated: check='s operands appear in BOTH the ; comparison and the failure message, so they re-evaluate when the ; check FAILS - bind a side-effecting expression with let-one first. ; A passing check evaluates each operand once. ; ----------------------------------------------------------------- (defmacro (check label cond) "raise unless the condition holds, labeled" (if (not cond) (then (err.raise 5 "check" (& "check failed: " label))))) (defmacro (check= label expected expr) "raise unless expected equals the expression, showing both" (if (<> expected expr) (then (err.raise 5 "check" (& "check failed: " label " - expected " expected " but got " expr))))) ; ----------------------------------------------------------------- ; L3: binding forms and control spellings. Templates only, like ; everything here - the user names every binding (the anti-gensym ; doctrine), so two let-ones sharing a name in one procedure collide ; at VBA's compile step, the loud failure C1 promises. ; ----------------------------------------------------------------- ; let-one: the roadmap's expansion verbatim. The binding is a Variant ; (VBA's own untyped default - 1:1, nothing smoothed); let-one-as ; names the type when it matters. Dim is procedure-scoped in VBA, not ; block-scoped: the binding outlives the body, exactly as the ; expansion says it does. Values only - binding an object needs Set ; semantics, so write (dim)/(obj-set!) by hand. (defmacro (let-one x expr & body) "dim a variable, set it, and run the body with it" (begin (dim x) (set! x expr) body)) (defmacro (let-one-as x type expr & body) "let-one with a named type" (begin (dim x type) (set! x expr) body)) ; if-not / while-not: the roadmap's own names, and twin spellings of ; forms already spoken - if-not is unless letter for letter (readers ; arriving from VBA's "If Not ... Then" reach for one name, Lispers ; the other; the vocabulary layer keeps two spellings of one template ; on the same grounds), and while-not is do-until's Do While Not ; cousin. Cheap on purpose: a synonym template costs one line and no ; semantics. (defmacro (if-not test & body) "run the body when the test is false - unless, in VBA accent" (if (not test) (then body))) (defmacro (while-not test & body) "loop the body while the test is false" (while (not test) body)) ; comment: expands to (begin), which emits nothing - for disabling ; CODE. The content must still read as balanced forms (the reader ; sees it before the expander drops it), so prose stays in ; comments, ; which never reach the reader at all. (defmacro (comment & junk) "expand to nothing - disable balanced forms" (begin)) ; ----------------------------------------------------------------- ; C1: the with- library. These are template macros like everything ; above, so every label and temporary is a caller-supplied parameter - ; the documented price of having no gensym. Two uses in one procedure ; therefore need two distinct names; VBA's own compile step catches a ; collision (duplicate label / duplicate Dim), which is the loud ; failure we want. ; ----------------------------------------------------------------- ; with-fast-excel: screen updating and calculation off around the ; body, restored on BOTH exits. Normal completion falls through the ; label with Err.Number = 0 (arming On Error cleared it), so the ; re-raise guard stays quiet and the trailing (on-error goto 0) ; disarms the label for the rest of the procedure. A body error jumps ; to the label, restores, and re-raises - the handler is active by ; then, so the raise cannot be re-trapped here and propagates to the ; caller, which is exactly the contract: guaranteed restore, error ; still reported. Restores to Excel's defaults (True / ; xlCalculationAutomatic) rather than saved prior state, deliberately ; - the same contract as the English sentence "Turn on screen ; updating.", and saving prior state would cost a second temporary ; parameter for a mode nobody runs in. (defmacro (with-fast-excel restore-label & body) "screen and calc off around the body, restored on both exits" (set! application.screenupdating false) (set! application.calculation xlcalculationmanual) (on-error goto restore-label) body (label restore-label) (set! application.screenupdating true) (set! application.calculation xlcalculationautomatic) (if (<> err.number 0) (then (err.raise err.number err.source err.description))) (on-error goto 0)) ; with-error-handler: the classic procedure-tail idiom, verbatim. ; (with-error-handler