Scopes work in ordinary code, and macros can write by-colour

The Scopes section shows a with-scope in a defun, and own!, own-disposable
and release!. The Macros section has a section on by-colour.

🤖 Generated with [ECA](https://eca.dev) (anthropic/claude-opus-5-5)

Co-Authored-By: eca-agent <git@eca.dev>
This commit is contained in:
Linus Björnstam 2026-10-10 22:45:31 +02:00
parent 8ca519a8ed
commit bb8cefba19
2 changed files with 56 additions and 2 deletions

View file

@ -767,7 +767,34 @@ Depending on the desired lifecycle and supervision strategy, fibers can be start
| @code{(spawn/detached (f x))} | Runs detached; unmonitored by the enclosing scope |
}
Because scope forms suspend until their child fibers conclude, they must be called from within a bjoroutine.
The scope forms work in both colours. In a bjoroutine, the wait at the end of the scope suspends. In ordinary code, a @code{defun} or a @code{(fun ...)}, it parks the thread until the children have ended, as @code{sync/blocking} does. So an ordinary function can start fibers and know that they are done when it returns:
@codeblock[#:lang "bjolang"]|{
(: fetch-all (-> (Vec string) void))
(defun (fetch-all names)
(with-scope
(ignore (own! (fun () (println "released"))))
(loop (:for n names)
(:do (spawn (fetch n))))
(println "all started")))
(defun (main)
(fetch-all ["a" "b"])
(println "back in main")
0)
}|
@codeblock[#:lang "text"]|{
all started
fetched a
fetched b
released
back in main
}|
A scope also owns resources. @code{(own! thunk)} registers a release on the innermost scope, and @code{(own-disposable x)} registers @code{x}'s @code{.Dispose}. When the scope ends, the releases run after the fibers have ended, newest first. @code{(release! h)} runs one early, with the handle @code{own!} returned. These are ordinary functions, so they also work in both colours. A release is a plain thunk: it cannot suspend, and a release that blocks also blocks the end of the scope.
Do not park a bjoroutine's thread by accident. A scope in an ordinary @code{(fun ...)} that a bjoroutine calls parks the thread of that bjoroutine, and the compiler warns about it. Write the scope in the bjoroutine itself. Or, if the function the lambda is given to takes a @code{-?->} parameter, write the lambda as a @code{(bjoroutine ...)}.
@subsubsection{Work that is not a fiber}
@ -1352,6 +1379,33 @@ The long name is a promise about what the compiler cannot check: that the form h
It may use literals, functions, and the defs of other modules. A macro that puts its caller's code inside the form passes these rules on to its caller, so keep the form to parts the macro built itself.
@subsubsection{One expansion for both colours: by-colour}
A macro does not know if its expansion will land in a bjoroutine or in ordinary code. When the two need different code, the expansion writes @code{(by-colour (#:sync body ...) (#:bjo body ...))}, and the compiler keeps the body for the colour of the code the expansion is in. This is how @code{with-scope} works in both colours: it ends the scope with a suspending wait in a bjoroutine and with a parking wait in ordinary code.
@codeblock[#:lang "bjolang"]|{
;; (await ev) syncs on ev, in code of either colour.
(def/macro (await form inject compare)
(syntax-match form
((_ ev) #'(by-colour (#:sync (sync/blocking ,ev))
(#:bjo (sync ,ev))))))
}|
The colour is the colour of the innermost of these:
@read-table{
| Where | Body |
|------------------------------------------------------------------------+--------------------|
| @code{defun}, @code{(fun ...)}, @code{(seq ...)}, top-level @code{def} | @code{#:sync} |
| @code{defbjo}, @code{(bjoroutine ...)}, @code{(bjo ...)} | @code{#:bjo} |
| @code{defbjouble} | the body it is in |
| a @code{defun} inside a body | the body around it |
}
A @code{defun} with a @code{by-colour} in it gets a suspending copy, as a @code{defun} that calls @code{file-read-text} does, and a bjoroutine calls that copy. If the @code{#:bjo} body never suspends, the copy is dropped, and the @code{#:sync} body runs everywhere. So write a @code{#:bjo} body that suspends and a @code{#:sync} body that does the same work without suspending.
A lambda applied where it is written, @code{((fun () body))}, is replaced by its body before colours are found. A @code{by-colour} in it has the colour of the code around it.
@subsubsection{Macro limitations}
When designing macros, keep the following constraints in mind:

View file

@ -310,7 +310,7 @@ Bjolang solves this problem at the compiler level using @em{colour twins}:
2. Source-level twin generation: Before type checking occurs, the @code{ColourTwins} pass scans all declarations. When it detects a function with a @code{-?->} parameter, it automatically generates a second definition from the same source body, repainted with suspending signatures. Both definitions are checked independently. This ensures that effects and await points are fully verified without needing error-prone AST copying after inference.
3. Reaching twins: Any function defined with @code{defun} that calls a dual-mode function (such as @code{defbjouble} primitives that provide both synchronous and fiber implementations) automatically receives a suspending twin as well. If an author explicitly wants a function to remain strictly synchronous, they can mark it with @code{#:sync}.
3. Reaching twins: Any function defined with @code{defun} that calls a dual-mode function (such as @code{defbjouble} primitives that provide both synchronous and fiber implementations) automatically receives a suspending twin as well, and so does one with a @code{(by-colour ...)} in it, whose @code{#:bjo} body goes into the twin and whose @code{#:sync} body stays in the original. If an author explicitly wants a function to remain strictly synchronous, they can mark it with @code{#:sync}.
4. Automatic call-site selection: When a function is called, the compiler checks the colour of the caller. If an ordinary @code{defun} calls @code{list-map}, it calls the synchronous twin. If a bjoroutine (@code{defbjo}) calls @code{list-map} with a suspending callback, it automatically links to the suspending twin. The author writes the higher-order function once, and the language transparently provides both versions.