Printing: written and displayed

->str gives way to two traits, as Scheme's write and display: a value
as the reader reads it, and the value for a person. Every type of your
own is written in constructor syntax. The examples, the REPL session and
the expect output follow.

🤖 Generated with [ECA](https://eca.dev)

Co-Authored-By: eca-agent <git@eca.dev>
This commit is contained in:
Linus Björnstam 2026-10-21 15:38:01 +02:00
parent 481538a1cb
commit e448ee0d93
2 changed files with 107 additions and 49 deletions

View file

@ -22,18 +22,22 @@ Here is the deck from the first part as a module of its own. Put it in @code{dec
(: Rank (Union (: Pip int) Jack Queen King Ace))
(: Card (Struct (: rank Rank) (: suit Suit))))
(impl (->str Suit)
(defun (->str s)
(match s (Clubs "♣") (Diamonds "♦") (Hearts "♥") (Spades "♠"))))
(impl (Displayed Suit)
(defun (display-into s b)
(stringbuilder-add-string! b
(match s (Clubs "♣") (Diamonds "♦") (Hearts "♥") (Spades "♠")))))
(impl (->str Rank)
(defun (->str r)
(match r
((Pip n) (int->string n))
(Jack "J") (Queen "Q") (King "K") (Ace "A"))))
(impl (Displayed Rank)
(defun (display-into r b)
(stringbuilder-add-string! b
(match r
((Pip n) (int->string n))
(Jack "J") (Queen "Q") (King "K") (Ace "A")))))
(impl (->str Card)
(defun (->str c) #"${(record-ref c rank)}${(record-ref c suit)}"))
(impl (Displayed Card)
(defun (display-into c b)
(display-into (record-ref c rank) b)
(display-into (record-ref c suit) b)))
;; Not exported: only this module needs them.
(def all-suits [Clubs Diamonds Hearts Spades])
@ -78,7 +82,7 @@ Notice how the imports and exports interact here:
@read-list{
- Relative imports: @code{(import "deck.bjo")} specifies a file path relative to the importing file. Bjolang compiles @code{deck.bjo} into @code{deck.dll} ahead of time, automatically rebuilding it whenever it (or any of its dependencies) changes.
- Type exports are complete: Exporting a type exposes all its constructors, union cases, and fields. Because @code{Card} is exported, @code{game.bjo} can freely construct @code{(Card (rank Ace) (suit Spades))} and pattern-match on its fields.
- Trait implementations are automatic: Although @code{->str} was not explicitly listed in the export list, its implementations still work in @code{game.bjo}. Because @code{impl} blocks define anonymous implementations rather than named bindings, they cannot be exported individually; they are automatically carried along whenever the module defining them is loaded.
- Trait implementations are automatic: Although @code{Displayed} was not explicitly listed in the export list, its implementations still work in @code{game.bjo}. Because @code{impl} blocks define anonymous implementations rather than named bindings, they cannot be exported individually; they are automatically carried along whenever the module defining them is loaded.
- Private definitions: Unexported bindings like @code{all-suits} remain private to @code{deck.bjo} and are inaccessible from outside.
}
@ -148,7 +152,7 @@ so its representation is visible only to the code of pile. A value of it can be
passed on here, and built and taken apart through the functions that module exports.
}|
Even though the type's representation is private, any trait implementations defined in @code{pile.bjo} remain available to consumers. For instance, an @code{(impl (->str Pile) ...)} inside @code{pile.bjo} allows any importing module to convert a @code{Pile} to a string.
Even though the type's representation is private, any trait implementations defined in @code{pile.bjo} remain available to consumers. For instance, an @code{(impl (Displayed Pile) ...)} inside @code{pile.bjo} allows any importing module to print a @code{Pile}. Without one, a @code{Pile} is written as @code{#<Pile>}, so printing it shows nothing of its representation.
@subsubsection{Import modifiers}
@ -221,7 +225,7 @@ While @code{export} publishes definitions authored in the current module, @code{
(re-export Card Rank Suit full-deck shuffle Pile new-pile draw)
}|
Re-exporting a type exposes the exact original type, not a distinct copy; a @code{Card} referenced via @code{cards.bjo} is identical to one from @code{deck.bjo}. However, trait implementations (@code{impl}) do not transfer through re-exports. They remain bound to the module where they were defined, meaning any file that wants to format cards with @code{->str} must still import @code{deck.bjo}.
Re-exporting a type exposes the exact original type, not a distinct copy; a @code{Card} referenced via @code{cards.bjo} is identical to one from @code{deck.bjo}. However, trait implementations (@code{impl}) do not transfer through re-exports. They remain bound to the module where they were defined, meaning any file that wants to print cards with their @code{Displayed} implementation must still import @code{deck.bjo}.
You can provide alternative names for bindings or macros using @code{(:alias new old)}. Combining @code{:alias} with @code{export} or @code{re-export} allows libraries to offer cleaner or more idiomatic public APIs:
@ -471,7 +475,7 @@ To illustrate, we can add a persistent high-score ledger to our card game. Each
(defun (main)
(save-score "ada" 31)
(save-score "bo" 27)
(println (read-scores)) ;; [(ada, 31) (bo, 27)]
(println (read-scores)) ;; [(Tuple ada 31) (Tuple bo 27)]
0)
}|
@ -1730,7 +1734,7 @@ Invalid literal syntax is flagged with a compile-time error at the point of use:
The hash macro 'card' failed at bad.bjo:3: a suit is clubs, diamonds, hearts or spades — in hurts
}|
Notice the @code{re-export} in @code{cardmacros.bjo}: because the macro expansion emits constructor symbols like @code{Card} and @code{Queen}, those types must be resolvable in the module invoking the macro. Re-exporting them ensures that importing @code{cardmacros.bjo} alone brings the required types into scope. Importing @code{deck.bjo} alongside it remains recommended so that the corresponding @code{->str} implementations are also present.
Notice the @code{re-export} in @code{cardmacros.bjo}: because the macro expansion emits constructor symbols like @code{Card} and @code{Queen}, those types must be resolvable in the module invoking the macro. Re-exporting them ensures that importing @code{cardmacros.bjo} alone brings the required types into scope. Importing @code{deck.bjo} alongside it remains recommended so that the corresponding @code{Displayed} implementations are also present.
@subsubsection{Literals read by a grammar}
@ -1850,7 +1854,7 @@ ok: an ace is worth 11
ok: shuffling keeps the cards
}|
Whenever an assertion fails, the output reports both the computed value and the expected value: @code{FAILURE: deliberately wrong gave 2 but wanted 3}.
Whenever an assertion fails, the output reports both the computed value and the expected value, each as @code{written} gives it: @code{FAILURE: deliberately wrong gave 2 but wanted 3}, or @code{gave "4" but wanted "5"} for strings. So @code{expect} needs a @code{Written} for the type, which every type has.
For asynchronous or resource-intensive testing, @code{(with-test "name" body ...)} executes the test body within an isolated structured concurrency scope configured with a default deadline. Any background fibers spawned during the test are automatically awaited, and the test fails if unclosed file ports remain open when the scope exits. Because @code{with-test} forms a concurrency scope, it must be invoked from within a bjoroutine.
@ -1885,14 +1889,18 @@ You can launch an interactive Read-Eval-Print Loop by running @code{bjo repl}. W
bjo> (+ 1 2)
3
bjo> (import "deck.bjo")
bjo> (vec-slice (full-deck) 0 3)
[2♣ 3♣ 4♣]
bjo> (vec-slice (full-deck) 0 2)
[(Card (rank (Pip 2)) (suit Clubs)) (Card (rank (Pip 3)) (suit Clubs))]
bjo> (displayed (vec-slice (full-deck) 0 3))
"[2♣ 3♣ 4♣]"
bjo> (defun (double (: x int)) : int (* x 2))
double
bjo> (double 21)
42
}|
The REPL shows a result with @code{written}, as the reader reads it: a string in quotes, a card as it is built. @code{println} and @code{displayed} give the text a person reads.
Unlike many Lisp REPLs, Bjolang's REPL does not interpret forms. Instead, each entered interaction is compiled on the fly into an in-memory assembly using the exact same compiler pipeline applied to files on disk, ensuring identical semantics between interactive sessions and compiled production builds.
Top-level functions declared in the REPL require explicit type signatures, mirroring the rules for modules. You can supply parameter and return types inline directly on the @code{defun}, or submit a standalone signature on its own line immediately preceding the definition: