cond-expand in library declarations in Stak Scheme
- Date: 2026-10-18
- Model: Claude Opus 5.5
- Repository:
raviqqe/stak - Version:
mainat1243edc02
Summary
- A
cond-expandlibrary declaration is silently dropped today.register-library!keeps onlyimport,export, andbegindeclarations, and both compilers register libraries before macro expansion, where thecond-expandmacro lives. The compiler also has no feature list; only the prelude’sdefine-featuresmacro has one. - The recommended design splices
cond-expanddeclarations inregister-library!and moves the feature list from the prelude into the compiler. One requirement evaluator there serves both library declarations and a generalized$$if-libraryspecial form behind the expression-level macro. - R7RS has two different
includeforms. The expression-level one is syntax and belongs in macro expansion, while the library declaration one, together withinclude-ciandinclude-library-declarations, belongs in the library stage. Today one syntactic pass handles both before either stage, so it reads files in unselectedcond-expandbranches and inside quoted data, regardless of bindings. - Moving the expression-level
includeinto macro expansion needs the directory of the code being expanded. Adirectoryfield on the macro context, set by a$$directorymarker form thatexpand-library-bodieswraps around each library body, carries it across the stage boundary. Nested includes need no marker because the expander knows each included file’s path when it reads the file.
Current behavior (verified)
-
Library declarations:
register-library!(around line 445 ofcompile.scm) collects declarations withcollect-bodies, which filters the declarations of adefine-libraryform by their head symbols. Acond-expanddeclaration matches none of them and disappears together with the imports inside it. The program below fails withprocedure expectedbecause the library body never sees(scheme base).(define-library (foo) (export foo) (cond-expand (stak (import (scheme base))) (else (import (scheme base) (scheme write)))) (begin (define (foo) (write-u8 65)))) (import (foo)) (foo) -
Stage order: the batch compiler runs
include-files,expand-libraries, andexpand-macrosin this order incompile-program. The runtime compiler thatinceptgenerates as(stak compile)runs the same steps for each expression.cond-expandis asyntax-rulesmacro exported by(scheme base), so it cannot run while library declarations are read. In the batch compiler, no macro exists at that point because macros are defined while library bodies are expanded. -
Feature list: the only list is in the
define-featuresmacro around line 333 ofprelude.scm, which defines both(features)andcond-expand. Feature identifiers are matched assyntax-rulesliterals, sofull-unicode,ieee-float, andlittle-endian, which(scheme base)does not export as identifiers, never match in user code even though(features)lists them. The compiler knows only the$$if-libraryspecial form (around line 847 ofcompile.scm), which the macro uses for(library ...)requirements. It checks registered libraries and.sldfiles on the load path withlibrary-exists?. -
Inclusion:
include-filesruns before both stages and rewritesincludeandinclude-library-declarationsanywhere in the source tree, including unselectedcond-expandbranches. The program below fails withcannot open file.includeis not bound anywhere, asprelude.scmnever mentions it, andinclude-ciis not implemented.(import (scheme base)) (cond-expand (foo (include "missing.scm")) (else (write-u8 65)))
Where to evaluate the clauses
Option A: splice them in register-library! (recommended)
Before collect-bodies runs, flatten the declarations recursively and replace each cond-expand declaration with the declarations of its first fulfilled clause.
(library ...)requirements uselibrary-exists?, which sees the prelude’s libraries, libraries defined earlier in the program, and.sldfiles on the load path. This matches the expression-level behavior.- The batch compiler, the runtime compiler, and
.sldloading shareregister-library!through the compiler frontend, so all of them get the feature. - The change is local to the library system.
include-filesstill reads files in unselected branches, as it does for expression-levelcond-expandtoday.
Option B: evaluate them in include-files
Dropping unselected branches before inclusion avoids reading their files. However, no library is registered yet when include-files runs, so (library ...) requirements cannot see the prelude’s libraries or libraries defined in the program.
Option C: process all library declarations in the library stage
This is Option A plus processing the include, include-ci, and include-library-declarations declarations one at a time while registering a library, with include-files called only on the selected begin bodies. It follows R7RS most closely and never reads files in unselected branches, but it is the largest change. It matters when a branch for another implementation includes a file that does not exist.
Rejected: reusing the cond-expand macro
Library registration precedes macro expansion in both compilers, and the batch compiler has no macros at that point.
Where the feature list lives
Option a: copy the list into the compiler
This is the least work, but the two copies of the list can drift apart.
Option b: move the list into the compiler (recommended)
Generalize $$if-library into a special form that evaluates whole feature requirements (and, or, not, library, and feature identifiers), and let Option A’s splicing call the same evaluator. The prelude macro then shrinks to iterating over clauses.
(define-syntax cond-expand
(syntax-rules (else)
((_)
(syntax-error "unfulfilled cond-expand"))
((_ (else body ...))
(relaxed-begin body ...))
((_ (requirement body ...) clause ...)
($$if-requirement
requirement
(relaxed-begin body ...)
(cond-expand clause ...)))))
(features) would get the list from another small special form. Because requirements are evaluated as data rather than matched as literals, full-unicode, ieee-float, and little-endian start matching. Requirement symbols need de-renaming before comparison, as $$if-library already does for library names.
Option c: keep the list in the prelude and let the compiler read it
The runtime compiler would also need the list shipped as metadata, as $$libraries is. This has more moving parts than Option b for no benefit.
The two include forms
- The expression-level
includeandinclude-ciare syntax (R7RS section 4.1.7) exported by(scheme base), so they belong in macro expansion. There,includeno longer fires in unselectedcond-expandbranches, in quoted data orcasedatums, or where a program binds its owninclude. - The
include,include-ci, andinclude-library-declarationslibrary declarations (R7RS section 5.6.1) belong in the library stage.include-library-declarationshas to be there because it can bring inimportandexportdeclarations, which are needed before macro expansion. A declaration-levelincludebecomes abeginbody, which an existing scenario infeatures/library.featurerelies on.
Moving the expression-level form changes some behavior:
(scheme base)has to exportinclude, soincludeworks only after importing it.- A file included at a program’s top level can no longer contain
importordefine-libraryforms because the library stage runs first. R7RS does not allow this, and no scenario relies on it.
Once both forms move, include-files disappears. The two moves can land separately. The library stage comes first because declaration-level cond-expand needs it, and include-files skips define-library forms in the meantime so that it does not read files in unselected declaration branches.
Carrying the originating directory
Nested includes resolve relative to the including file, and includes in a library body resolve relative to its .sld file. The macro expander has no source locations, and expand-library-bodies splices library bodies into the program, which loses where they came from. Therefore, a directory has to cross the boundary between the two stages.
Library stage
load-libraryalready passes(path-directory path)toinclude-files, and it would pass the directory toregister-library!instead. Libraries defined in the program get"", the current directory, as they do today.- A new
directoryfield on thelibraryrecord stores the directory. The runtime compiler’s table of built-in libraries passes"". expand-library-bodieswraps each body as($$directory "<directory>" body ...).
Macro expansion stage
-
macro-contextgets adirectoryfield next tostateandlocals.macro-context-appendcopies it, and the initial contexts inexpand-macrosand the runtime compiler start with"". -
expand-macrogets two cases next to$$if-library.$$directoryexpands its body in a context with the directory replaced and returns($$begin ...). This is safe at the top level because$$definealways defines a global.$$includeresolves each path against the context’s directory and expands each file’s forms in a context whose directory is the file’s own. This handles nested includes without markers.
-
(scheme base)exports the user-facing form.(define-syntax include (syntax-rules () ((_ path ...) ($$include path ...))))
The marker survives the library stage because symbols prefixed with $$ are built-in symbols that library renaming leaves alone. It disappears in macro expansion, so later passes never see it. Both compilers share the code, so eval supports it too.
Decisions this design forces
- Use site or definition site: a
syntax-rulestransformer returns the use-site context with its renames appended, so anincludeproduced by a macro resolves relative to where the macro is used. To resolve it relative to where the macro is defined,make-transformerwould capture the directory of its own context and put it into the context it returns. R7RS leaves this open. - The program’s own directory stays
"". The driver could pass the script’s directory instead, which would make top-level includes stop resolving against the current directory, but that is a separate behavior change.
Rejected alternatives
- Handing
(directory . forms)chunks from the library stage to macro expansion and expanding each chunk with its directory needs no marker. However, it changes the interface betweenexpand-librariesandexpand-macrosin both compilers, and the runtime compiler’s import case returns a single$$beginexpression. - Syntax objects with source locations are the general solution and would also give file names and line numbers in errors, but the expander works on plain S-expressions throughout.
- A side table keyed by list cells with
eq?breaks becauseresolve-environment-symbolsrebuilds every list in library bodies. - Making include paths absolute in the library stage requires finding
includeforms syntactically, which is the problem being removed.
Recommendation
- Splice
cond-expandlibrary declarations inregister-library!(Option A) with a requirement evaluator in the compiler frontend, and move the feature list there (Option b), generalizing$$if-libraryfor the expression-level macro. - Process the
include,include-ci, andinclude-library-declarationsdeclarations in the library stage as well (Option C), withinclude-filesskippingdefine-libraryforms. This can wait until a library hits a missing file in an unselected branch. - Move the expression-level
includeinto macro expansion with adirectoryfield on the macro context and a$$directorymarker around library bodies, which removesinclude-files.
Every step changes the compiler frontend. The bytecode snapshots of the programs that embed the compiler for eval (run.scm, repl.scm, compile.scm, and the eval benchmark) change, and the frontend has to stay portable R7RS for the CI job that compares bytecode compiled by Chibi Scheme, Guile, and Stak byte for byte.
Decisions only the maintainer can make
- Whether an
includeinside a macro template resolves relative to the macro’s use site or its definition site. - Whether top-level program includes should resolve relative to the script’s directory.
- Whether to support
cond-expandaroundimportdeclarations at a program’s top level. R7RS does not require it. Chibi Scheme 0.12.0 and Guile 3.0.11 accept it, while Gauche 0.9.15 does not becausecond-expandis unbound there before the first import. The same splicing could apply to the top-level forms inexpand-libraries.
Sources
- R7RS small, sections 4.1.7 (inclusion), 4.2.1 (
cond-expand), and 5.6.1 (library syntax) - stak#4141, which added
$$if-library - stak#4144, which added
include-library-declarations