Planet Scheme

Sunday, October 25, 2026

Scheme Requests for Implementation

SRFI 288: Compound Conditions

SRFI 288 is now in draft status.

A condition object system is introduced that attempts to be implementable in and interoperable with many implementation’s condition systems, including the condition system of the R6RS. The condition system includes compound conditions, but does not include subtyping of conditions. Simple conditions can be any implementation-defined value, and are also objects that can be made with the interface in this SRFI. An implementation may add other features, such as introspection and serialization.

by Peter McGoron at Sunday, October 25, 2026

Gwen Weinholt

Akku.scm 1.2.0 released

Akku.scm version 1.2.0, the language package manager for R6RS and R7RS Scheme, is now generally available. It can be downloaded from GitLab. The most important change is a fix to the dependency solver, which was running very slowly due to a bug triggered by the many versions of the chez-srfi package.

Akku is a project-based package manager, where each project has its own set of dependencies. You use .akku/env to set shell variables that let your Scheme implementation know where to find everything.

You can upload your own packages to the archive. They go through a review process to ensure nothing malicious is distributed.

Akku also performs automatic conversion from the R7RS define-library form to the R6RS library form. This lets, e.g., Chez Scheme use R7RS libraries. Packages from the Snow package manager, which has many R7RS packages, are also available via Akku.

Further reading

by weinholt at Sunday, October 25, 2026

Saturday, October 24, 2026

jointhefreeworld

Emacs finally folds to my liking

I’ve been slowly turning my Emacs config into something I actually enjoy using, not just something that works, and folding was the last place where that wasn’t true.

Every mode had its own idea of what folding is: treesit-fold in Tree-sitter modes, Hideshow in the Lisp-ish ones, Outline in everything that resembles text, Org with its own invisible text that no one else’s commands can undo.

So every buffer had different keys, a different look, and worst of all different performance characteristics. Folding a big file sometimes felt like holding my breath.

I’m running Emacs 32.0.50 from master (built from source, as one does), and it’s finally one system.

kirigami is the piece I didn’t know I needed  #

kirigami doesn’t fold anything itself. It dispatches: it keeps a list of folding engines, checks which one is actually active in the buffer, and forwards to it. Tree-sitter, Hideshow, Outline, Org, Vdiff — all behind one interface.

So on top of it I put one set of keys, the same in a Kotlin buffer, YAML, a Scheme or ELisp buffer and an Org file:

Key Does
C-c v c close this fold
C-c v C close every fold
C-c v o open this fold
C-c v O open this fold and everything under it
C-c v r reveal everything
C-c v t toggle the fold at point

That sounds small. It is not small. The number of times a day I used to think “wait, what does TAB do in this mode again” is now zero, and the muscle memory transfers across an entire polyglot config.

One wrinkle worth mentioning: kirigami answers with the first engine in its list whose mode is on, and outline-minor-mode is on in nearly every buffer I have. In the buffers where I deliberately turned on Hideshow — Scheme, for instance — that choice has to win, otherwise C-c v c folds “headings” a Scheme file doesn’t have. So I reorder kirigami-fold-list to put Hideshow first instead of rewriting it; engines kirigami adds later still get used wherever Hideshow is off.

Tree-sitter folding, with guides that are actually lines  #

Most of my programming buffers are *-ts-mode, so treesit-fold does the work there — bash, C, C++, CMake, CSS, Dockerfile, Elixir, Go, HTML, Java, JS/TS/TSX, JSON, Kotlin, Lua, Nix, PHP, Python, Ruby, Rust, Scala, TOML, YAML. Line counts are shown inline, which I find genuinely useful when deciding whether to open a fold.

The indicators took the most fiddling. treesit-fold-indicators draws a block’s guide with a bitmap 23 rows tall, centered in the line beside it. A line in my config is 29 pixels — the font’s 23 plus six of line-spacing — so every row of the guide was a 23-pixel dash with three pixels of nothing above and below it. A folded block looked like a dotted line running down the fringe. Not subtle, just wrong.

There’s no setting for that, so I replaced the bitmaps: a single-row bitmap that define-fringe-bitmap repeats down however tall the line is (no spacing can introduce a gap), plus a +=/-= box and an end-elbow cut to the exact height of a line, so the guide runs from the marker into the row below and turns toward the text where the block ends. It’s an advice on the function that builds the string for each line. It looks like a bracket now, not a perforation.

And the colour: the indicators inherited shadow, which most themes make as bright as a comment. Fine for one marker on one line, wrong for a marker on every line of a block — the grey became a rule drawn through the code. They’re now shadow blended 50% toward the theme’s background, recomputed on every theme and scale change, so it fades correctly on light themes too and no theme needs a special entry.

Hideshow earns its keep  #

Not everything has a grammar, and I’m not going to pretend otherwise:

  • Scheme — my Emacs build has no Scheme grammar, so Scheme buffers use Hideshow with hs-block-start-regexp set to "^(" and hs-forward-sexp-function set to forward-sexp. A Scheme file is a sequence of top-level forms, so each definition is a fold, and collapsing the one you’re inside works from anywhere within it. That’s the behaviour I wanted from the start and Hideshow doesn’t guess it by default.
  • Emacs Lisp — the prog-mode fallback, same keys.
  • XML — folds from start tag to end tag using nxml-scan-element-forward, skipping , comments, DOCTYPE and self-closing elements.
  • YAML (the non-Tree-sitter mode) — indentation-based blocks with a small forward-sexp of my own that walks to the end of the indented block.

Hideshow’s own fringe indicators ( hs-show-indicators + hs-indicator-type 'fringe) are on in all of these, so it doesn’t feel like a downgrade from the Tree-sitter buffers.

The rule I enforce per buffer is exactly one set of indicators: Tree-sitter buffers get treesit-fold and Hideshow is turned off; Hideshow buffers get Hideshow and Outline’s clickable buttons are turned off so the two don’t double up; outline-only buffers (Markdown, diff) keep the buttons. Nobody ends up with two markers per block.

Org, which needed its own everything  #

Org folds with text properties, not overlays, so neither Tree-sitter nor Hideshow nor Outline’s buttons can draw a marker for it. So I wrote its own indicators: a fringe triangle pointing down for an open heading, right for a folded one, clickable with mouse-1 to toggle exactly like TAB. Folding itself is untouched — TAB, S-TAB and the C-c v keys all still work.

The performance part is where the real win is. A naive “redraw indicators after every command” implementation is a disaster in a large file, because it re-searches the buffer and rebuilds every overlay on every pause. Mine is driven by the text instead:

  • if buffer-chars-modified-tick didn’t change, only the indicators that are on screen are redrawn — off-screen headings can wait until they’re scrolled to;
  • if it changed and the change was recorded, only the dirty region is rebuilt;
  • if it changed with no recorded change, only then is the whole buffer rebuilt.

Folding changes text properties rather than characters, so folding alone takes the cheap path. Fold state is read back from Org on each redraw rather than remembered, so external folding can never leave a stale marker.

Two config choices matter just as much: org-fold-core-style is text-properties (so a visibility cycle doesn’t re-parse the file), org-element-use-cache is on, and Org starts with showeverything — which is both what I want (a note is read, not consulted) and the cheaper state, because it skips the visibility passes that walk every heading on open. I also removed a org-restart-font-lock that was re-fontifying the whole buffer a second time on open; JIT-lock handles what’s on screen.

It feels smooth, and that’s the whole point  #

The end result is that folding is boring now. C-c v c in Kotlin, Scheme, Elisp, Org, XML, YAML, Markdown. Guides that look like brackets. Indicators quiet enough to ignore until you need them. No pause on a 3000-line Org file. No two competing markers.

It honestly changes how much I collapse and expand while reading code. I used to avoid folding because it felt fiddly; now it’s free and mouse friendly.

If you want to read the actual code: it’s in dev-folding.el for the engines and indicators, the heks/fold-* wrappers in heks-emacs.el for the keys, and the Org indicators live in org.el. Everything is GPL-3.0, and it’s tested: maak verify asserts a fold actually covers a whole definition and that the documented keys are bound, and maak keys fails on a key that does two jobs or names a command that doesn’t exist.

Happy to answer questions about any of it, especially if you’ve fought the treesit-fold-indicators bitmaps yourself, because that one cost me an evening and I’d love to know if there’s a cleaner way.

Saturday, October 24, 2026

OrgWebAlchemy: Benchmarking, Profiling, and the Long Road to Real Org Support

OrgWebAlchemy is a Guile library that parses Org mode with a Parsing Expression Grammar and renders it to HTML, Markdown, or SXML. I wrote an earlier article about why a PEG is the right tool for Org: the grammar describes the language, the parser builds a tree, the renderers turn that tree into output.

This is the follow-up, and it turned into a harder question than I expected: how do you make a parser fast without making it wrong? I didn’t want something that’s quick on my test files and quietly behaves differently on yours.

So: the features I added, how I measured them, what the profiler said, the two rounds of fixes, and how I checked the language didn’t change. Every number below comes with a way to reproduce it, because a number you can’t check is advertising, not evidence.

TLDR; Here is the whole article in five sentences:

  • Parsing a 58 KB Org document now takes 5.1 ms, where it used to take 32.2 ms, a factor of 6.4.
  • The same parse now allocates 3.9 MB, where it used to allocate 30.3 MB, 7.8 times less garbage.
  • A document consisting of one 80 KB line went from 8.2 seconds to 0.37 milliseconds, because the old path was quadratic in the length of a line and the new one is linear.
  • The library gained a Markdown renderer, figures, named elements, cross-references, a proper table reader and mixed nested lists, and it says plainly what it still doesn’t do.
  • None of this was guessed: there are 155 test definitions and 281 assertions, an equivalence run compared the new parser with the previous one on about 70,000 inputs, and a test sweeps about 390,000 positions to check the scanner’s stop set.
One Org document, rendered by both renderers.

Free software, and why this is a library  #

OrgWebAlchemy is free software under the GNU Lesser General Public License, version 3 or later, with this documentation under the GNU Free Documentation License. Run it, study it, change it, ship your changed versions.

It could have been a service instead: you send your Org file to a server you don’t control, and get HTML back. That can be convenient, and it can even be gratis, but it leaves you helpless in the ways that matter. You can’t study it, can’t fix it, and can’t keep using it when the operator loses interest or changes the terms. A library goes into your program, on your machine, under your control.

The LGPL is a compromise, and I’ll name it as one. The GPL would keep every program that uses this code free too, and that’s the stronger guarantee for users. For libraries the GNU Project has long accepted a weaker license so that more programs can use them, including some that aren’t free, and I’ve followed that tradition. Use it in a free program and you’re helping the community; use it in a nonfree one and I’d rather you made that program free.

There’s a second reason it’s a library. Org mode is a language, and a language is worth as much as the number of programs that can read it. A parser only Emacs can use is a parser for one program. One that any Guile program can call is a parser for a text editor, a static site generator, a wiki, a notebook, a mail archiver, a package’s docs build, or something nobody has written yet. I don’t know what you’ll build with it, and that’s the point.

And a word on what it isn’t: it’s not Emacs and doesn’t try to be. Emacs is where Org lives, where it gets edited, and where its export machinery started. OrgWebAlchemy doesn’t replace that, it just lets other Scheme programs read the same files. When it disagrees with Emacs about some corner of the syntax, Emacs is right and this is a bug.

What the program is now  #

OrgWebAlchemy is still three small modules. What changed is how much of the language each one covers.

Module Lines What it owns
(orgwebalchemy peg) 1,700 the grammar, the AST, the run scanners, link classification, name resolution
(orgwebalchemy html) 720 rendering to HTML and SXML, and every CSS class parameter
(orgwebalchemy markdown) 430 rendering the same AST to Markdown
Test suite 1,670 155 test definitions and 281 assertions, many of them table-driven

The API is still small enough to hold in your head.

(use-modules (orgwebalchemy html)
             (orgwebalchemy markdown)
             (orgwebalchemy peg))

(org->html  "This is ~test~ code.")
 ;;  => "This is test code."

(org->markdown  "This is ~test~ code.")
 ;;  => "This is `test` code."

 ;;  parse once, render as often as you like
( define  ast (org->ast  "* Hello\nA *bold* line.\n"))
(org-ast->html ast)
(org-ast->markdown ast)

The rest of this article is about what happens between those calls.

Teaching the parser more of Org  #

“Supporting Org” hides a lot of work, because Org isn’t a markup language with a tidy grammar. It’s a pile of conventions accumulated over years of a GNU Emacs major mode, some of them deliberately informal, and each one has to be considered on its own terms. Every feature below started as a sentence in the Org manual, became a grammar pattern, then became a test.

Links: decide once, in the parser  #

An Org link’s target says what kind of link it is. A target starting with https:// is external. file:photo.png is a file, and so is a bare notes.org. #custom-id points inside the document, *Heading at a heading, and a ::search suffix says where inside the target you land.

The old code let each renderer read the target and decide for itself, which is exactly how two back ends drift apart. HTML grows a special case for image extensions, Markdown grows a slightly different one, and after a year of small repairs the same document means two things depending on which function you call. A user who notices won’t know which one is right, and will be right not to trust either.

So classification moved into the parser, where it happens once.

(org-link-parts  "https://example.org/a")    ;;  => (https . "//example.org/a")
(org-link-parts  "file:photo.png")           ;;  => (file . "photo.png")
(org-link-parts  "notes.org")                ;;  => (file . "notes.org")
(org-link-parts  "#custom-id")               ;;  => (internal . "custom-id")
(org-link-parts  "*My Section")              ;;  => (heading . "My Section")
(org-link-parts  "notes.org::*Heading")      ;;  => (file . "notes.org")
(org-link-search  "notes.org::*Heading")     ;;  => "*Heading"
(org-image-link?  "photo.jpeg?raw=1")        ;;  => #t

Both renderers then ask the same questions and follow the same answer.

Org HTML Markdown
[[file:photo.png][a photo]] a photo ![a photo](photo.png)
[[images/diagram.svg]] ... ![images/diagram.svg](images/diagram.svg)
[[file:notes.org][notes]] notes [notes](notes.org)
[[#custom-id][there]] there [there](#custom-id)
[[*My Section][up]] up [up](#my-section)
[[https://gnu.org][GNU]] GNU [GNU](https://gnu.org)

Two details deserve a sentence each. The ::search suffix is set aside before the path is used as a file name, so file:notes.org::*Heading doesn’t produce a broken file name. And C:/tmp/x.txt isn’t mistaken for a link with a scheme named c, because a real scheme is lower case. Small stuff, but it’s the kind of small stuff that decides whether a program is pleasant to use on somebody else’s computer.

Figures  #

Org gives the element after a #+CAPTION: or #+NAME: keyword a caption and a name. When that element is a link to an image, the three things are one object: a figure. The parser folds them together, so renderers get a single node instead of three neighbors they have to guess about.

(org->html  "#+NAME: fig:arch\n#+CAPTION: The architecture\n[[file:diagram.png][a diagram]]\n")
 ;;  => "
\"a
The architecture
"

The name the figure carries is the identifier that a [[fig:arch]] link resolves to, so the cross-reference can’t break. Both facts come from one place, and one place can’t disagree with itself.

(org->html  "#+NAME: fig:arch\n#+CAPTION: The arch\n[[file:a.png]]\n\nSee [[fig:arch]].\n")
 ;;  => "
...

See fig:arch.

"

Markdown has no figure element, so it gets the image, the caption as an emphasised line, and an explicit anchor for the cross-reference. What counts as a figure is deliberately narrow: the keywords must sit immediately before a paragraph holding exactly one image link and nothing else. Everything else stays a keyword line. Inventing a caption for an element this parser doesn’t caption is worse than saying nothing, because the reader would never learn that something got silently dropped.

Named elements  #

A figure is the image case of a more general rule. A #+NAME: line names the element after it, and anything that renders as something a link can land on can be named: a table, a source block, a quote, a list, a separator, a paragraph.

(org->html  "#+NAME: tbl:users\n| Users | 1024 |\n\nSee [[tbl:users]].\n")
 ;;  => "...

See tbl:users.

"

HTML puts the identifier on the element itself. Markdown has nothing to hang it on, so it emits an anchor in front, same as for a figure. A bare target that matches the name of an element in the same document resolves as a cross-reference, which is why [[tbl:users]] and [[notes]] work. A target that already says what it is — a scheme, a file: prefix, a leading # or *, or a ::search suffix — keeps the meaning it wrote.

One asymmetry, documented rather than hidden: a #+CAPTION: in front of a named table isn’t folded into it, because captions on tables aren’t rendered yet. The line stays an ordinary keyword line where the writer put it.

Tables, read by their bars  #

Tables looked finished until I looked at the edges. What should | on a line of its own be? What is | a ||? What happens to a row that ends the input with no newline?

The old pattern counted separators and made one cell for each, which meant a row with no cells could raise an error, and a row that ended the input got a phantom empty cell from the newline it didn’t have. The rewrite reads a row the way Org does: by the bars that separate the cells, where a bar closes a row when nothing but the end of the line follows it. That one rule answers all three questions.

  • A line that is only its closing bar ( | on its own) is a row of one empty cell, not a row with no cells.
  • | a || is a row of two cells, the second of them empty, rather than a paragraph.
  • A row that ends the input keeps exactly the cells that were written.

An empty cell also keeps its column, because an empty value is now the empty string instead of the bare tag symbol that an empty (* ...) match leaves behind. The difference sounds academic until it is your table.

Lists that mix and nest  #

Org lets a list change its bullet type part-way through, and it lets a nested list use a different kind from its parent. So the grammar stopped having three separate list productions and grew one.

(define-peg-pattern org-list all
                    (+ ( or desc-item unordered-item ordered-item)))

The renderers group consecutive items of the same kind into the proper element, which is what makes - outer with 1. nested inside it come out as an unordered list containing an ordered one. Checkboxes, description items and arbitrary nesting depth follow from the same rule.

The rest of the surface  #

Between those features and the earlier work, the supported set is now: headings with TODO keywords, priorities and tags; paragraphs; unordered, ordered and description lists in any mixture and at any depth; checkboxes; tables with empty cells; source, example, quote, verse, center, export and comment blocks; keywords and in-buffer settings; comments; fixed-width lines; separators; footnotes, whether references, definitions or inline; bold, italic, underline, strikethrough, code, verbatim, superscript, subscript, escaped markup and forced line breaks; links with and without descriptions, bare URLs, images, figures, named elements and heading anchors.

That’s most of what people actually write. Not all of Org, and the manual has a section listing the gaps instead of hiding them, because a library that lies about its coverage just wastes everyone’s time.

Measuring the work  #

Before optimizing anything I wanted a benchmark that could tell me whether a change helped, and that couldn’t be fooled by the change itself.

The benchmarks live in test/veritas/benchmark/scope.scm. They use the same Veritas framework as the unit suite, selected by test type so they never slow down maak test.

 VERITAS_TEST_TYPE=benchmark  \
guix shell -m manifest.scm -- guile -L ./src -L ./test  \
  -c  '((@ (veritas runner) run-tests))'

The suite measures five workloads and prints three tables — wall clock, allocation, and the collapse pass — formatted as Markdown, ready to paste into the manual. That last part matters: a number you have to retype by hand quietly stops being true.

Workload What it exercises
Parse the feature fixture every construct the parser knows, in 1.8 KB
Parse README.org a real 58 KB document with prose, tables and code blocks
Render the feature fixture to HTML parsing plus the HTML renderer
Render the feature fixture to Markdown parsing plus the Markdown renderer
Parse a 50-clause inline paragraph the inline grammar, under maximum pressure

Five runs, keep the fastest  #

One wall clock reading on a laptop isn’t a measurement, it’s a rumor. The suite runs each workload five times and reports the fastest, on the theory that the other four got interrupted by a browser, a compiler, or the collector arriving at a bad moment.

It’s not statistically pure and I won’t pretend it is. It’s a lower bound on the cost of the work, measured on a machine that’s also doing other things. Allocation is the stable half and stays within a few percent; wall clock moves 10–20% between sessions. Repeat it and get a different number and you’ve found the noise, not an error. Be suspicious of anyone who reports wall clock figures without it.

Allocation, not only time  #

Guile’s collector is good enough that you can write a parser allocating a megabyte per kilobyte of input and still call it fast, right up until it’s in a server serving several documents at once. So the suite measures the heap too. The heap is collected before each workload, and what’s reported is heap-total-allocated: everything the workload asked for, including garbage it dropped immediately. That’s allocation pressure, the thing that triggers collections, not the memory a result keeps.

The last column is Guile’s gc-time-taken, which is collector processor time. Guile sums its marker threads, so one full collection of a big heap can be reported as several times its wall clock cost on a many-core machine. Read it as how much collector work a workload causes, not as a share of the clock. Same note is in the benchmark’s docstring, because an easy-to-misread number will get misread.

Fragments and the collapse pass  #

The last measurement in the suite is the one that eventually explained everything: how many string fragments a parse hands to the pass that merges them, and how long the merging takes.

Take a pattern written as (* (and (not-followed-by eol) peg-any)), meaning “consume characters until the newline”. The compiled pattern produces one parse object and one string fragment per character. A line of n characters costs n one-character substrings, n list cells, and a merge over all of them. On a document of short lines nobody notices; on a 45 KB line it’s the whole program.

The fragment table made that visible with no profiling at all.

Document Fragments handed to the collapse pass Collapse (ms)
Feature fixture (1.8 KB) 82 0.04
README.org (58 KB) 1,264 0.39
Plain prose (32 KB) 400 0.22
One 45 KB line 1 0.005

The last row is the interesting one. Forty-five kilobytes of text arrive as a single fragment, because a run scanner took them in one substring instead of forty-five thousand. The README, 58 KB of input, arrives as about 1,300 fragments — roughly one per line of prose instead of one per character.

What the profile said  #

Veritas profiles every benchmark with Guile’s statprof, and that’s what turned a vague feeling that big files were slow into a statement about one procedure.

I fed the old parser a document made of one 40 KB line and looked at the profile.

%     cumulative   self              procedure
 97.55      1.77      1.77  list?
  0.61    115.98      0.01  ice-9/boot-9.scm:220:5:map1
  0.61      0.03      0.01  orgwebalchemy/peg.scm:339:0
  0.61      0.01      0.01  reverse
  0.00  23786.61      0.00  ice-9/peg/simplify-tree.scm:48:0:string-group
  0.00      3.56      0.00  ice-9/peg/simplify-tree.scm:65:0:string-collapse

97.5% of the time is inside list?. Not the grammar, not the renderers, not any of my own code: one predicate, called by Guile’s own tree simplifier.

My hypothesis was that the PEG engine was slow. The truth was a helper in (ice-9 peg simplify-tree) asking (list? lst) of the rest of the list at every level of its recursion, which walks the list quadratically.

Here is the relevant part of string-group.

( define ( string-group lst)
  ( if (not (list? lst))        ;;  <- O(length) at every level
      lst
      ( if (null? lst)
          '()
          ( let ((next (string-group (cdr lst))))    ;;  non-tail recursion
            ...))))

For a run of n fragments that’s about n²/2 list steps. A line of 40,000 characters means 800 million of them, which is exactly the 1.77 seconds in the profile. Doubling the line quadruples the bill, and the measurements agree: 20 KB took about 420 ms, 40 KB about 1,760 ms.

So there were two problems and only one was mine. My grammar produced one fragment per character, handing the quadratic pass an enormous run to chew on. And the collapse pass was quadratic in the length of a run, so even a modest run cost more than it should. Fix only the first and a slow path waits for anyone who writes a long line; fix only the second and you still allocate megabytes for nothing. Both had to go.

The two rounds of repair  #

First round: fast paths in the grammar  #

The first round was micro-optimization of the grammar. It helped, though not where I expected.

The scheme alternation used to be fifteen whole-string comparisons.

(define-peg-pattern url-scheme all
                    ( or  "http://"  "https://"  "ftp://"  "ftps://"
                         "file://"  "ssh://"  "sftp://"  "ws://"  "wss://"
                         "git://"  "gopher://"  "tel:"  "mailto:"
                         "irc://"  "ircs://"))

Grouping the alternatives by first character means a position that can’t begin a scheme fails after one comparison instead of fifteen.

(define-peg-pattern url-scheme all
                    ( or ( and  "h" ( or  "ttps://"  "ttp://"))
                        ( and  "f" ( or  "tps://"  "tp://"  "ile://"))
                        ( and  "s" ( or  "sh://"  "ftp://"))
                        ( and  "w" ( or  "ss://"  "s://"))
                        ( and  "g" ( or  "opher://"  "it://"))
                         "tel:"
                         "mailto:"
                        ( and  "i" ( or  "rcs://"  "rc://"))))

The alternation of inline constructs was badly ordered too. It tried the multi-character constructs, footnotes, links and the scheme scan before the single-character markers, so a line full of *bold* and /italic/ paid for the expensive branches first. Putting the one-character markers first and the link and URL machinery last costs nothing.

(define-peg-pattern inline-special body
                    ( or inline-bold inline-italic inline-code inline-verbatim
                        inline-underline inline-strike
                        inline-superscript inline-subscript
                        inline-line-break inline-escape
                        inline-footnote-inline inline-footnote-def inline-footnote-ref
                        inline-link-with-desc inline-link-no-desc
                        inline-raw-url))

I also added a hand-written class for plain characters, so ordinary text gets taken with a few range tests instead of a not-followed-by check against the whole inline grammar at every position.

The result: the inline benchmark doubled, the document benchmarks improved by about a third, and the single-line case didn’t move at all. That last bit was the useful part, because it said the remaining cost wasn’t in the grammar.

Second round: run scanners  #

A compiled PEG pattern in Guile is just a procedure of (string length position) returning (end . body) or #f. Nothing stops you writing that procedure by hand and naming it in the grammar like any other pattern. That’s the whole idea of the run scanners: find the end of the run in one C-level scan with string-index and return a single substring.

( define ( run-end str len pos stop-chars stop?)
   "Return the end of the run from POS that stops at the first character
of STOP-CHARS at which STOP? holds, or at LEN."
  ( let  loop ((i pos))
    ( let ((candidate ( or (string-index str stop-chars i len) len)))
      ( if ( or (>= candidate len) (stop? str len candidate))
          candidate
          (loop (1+ candidate))))))

( define ( text-run str len pos stop-chars stop?)
  ( let ((end (run-end str len pos stop-chars stop?)))
    (list end (substring str pos end))))

( define ( line-text-scanner str len pos)
   "Consume the rest of the line as one string."
  (text-run str len pos nl-chars always-stop))

(define-peg-pattern line-content all line-text-scanner)

There are thirteen such scanners now, one for each construct whose body is “text up to a delimiter”: a line, a table cell, a link target, a link description, a footnote body, a keyword value, the body of a source block, a description key, the body of a raw URL.

The delicate part is the stop? predicate. For a line, every newline really does end the run. For inline text, an asterisk ends the run only if inline-special really matches there; otherwise it’s an asterisk in the middle of a sentence. So the scanner stops at every candidate, asks the grammar whether something begins there, and if not, continues past that character. Still one string-index call per candidate, and there’s at most one candidate per markup character in a line.

Thirteen scanners would be a maintenance problem if each had its own idea of where text ends, so the character sets live in one place, each with a comment saying who stops there.

( define  nl-chars (char-set #\newline #\return))
( define  table-stop-chars (char-set #\| #\newline #\return))
( define  inline-stop-chars
   ;;  every character that can begin an inline construct: the markup markers,
   ;;  the letters a raw URL scheme can start with, and the newline
  (char-set #\* #\/ #\~ #\= #\_ #\+ #\^ #\\ #\[
            #\h #\f #\s #\w #\g #\t #\m #\i #\r
            #\newline #\return))
( define  link-stop-chars (char-set #\] #\newline #\return))

Second round, second part: a linear collapse pass  #

The other half was dropping the quadratic string-collapse. The library now has its own collapse-tree, keeping the exact contract of Guile’s procedure: children collapsed before their parent, a maximal run of adjacent strings concatenated, a list of strings not joining the run around it, a one-element result unwrapped. The difference is that grouping happens in one pass with one reverse.

( define ( string-group-linear lst)
  ( let  loop ((nodes lst) (run '()) (out '()))
    ( cond
      ((null? nodes)
       (reverse ( if (null? run) out (cons (reverse run) out))))
      ((string? (car nodes))
       (loop (cdr nodes) (cons (car nodes) run) out))
      ( else
       (loop (cdr nodes) '()
             ( if (null? run)
                 (cons (car nodes) out)
                 (cons (car nodes) (cons (reverse run) out))))))))

Same contract, not merely a similar one, so it produces identical trees. That’s what let me prove the rewrite safe instead of hoping it was. On its own it’s about 530× faster at 32,000 fragments.

The measurements  #

Everything below was measured in the project’s Guix shell with Guile 3.0.9, on the Ryzen 5 7640U laptop I write on, with a browser open. Five runs per workload, fastest reported. Three columns: before any optimization, after the grammar fast paths, and now.

Workload Before (µs) Fast paths (µs) Now (µs) Total speed-up
Parse the feature fixture (1.8 KB) 790 590 320 2.5×
Parse README.org (58 KB) 32,200 23,600 5,070 6.4×
Render the fixture to HTML 3,480 2,360 1,840 1.9×
Render the fixture to Markdown 3,090 2,250 1,690 1.8×
Parse a 50-clause inline paragraph 6,010 3,070 2,270 2.7×

The allocation table tells the same story from the other side.

Workload Before (KiB/op) Fast paths (KiB/op) Now (KiB/op)
Parse the feature fixture 900 620 230
Parse README.org 30,300 19,700 3,890
Render the fixture to HTML 3,310 2,200 1,360
Render the fixture to Markdown 3,140 2,160 1,360
Parse the 50-clause inline paragraph 5,660 2,740 1,690

Parsing the 58 KB README now allocates about 68 bytes per input byte, where it used to allocate roughly 530. That’s the difference between a parse that reliably triggers a collection of its own and one that often doesn’t.

One long line: from eight seconds to a third of a millisecond  #

The clearest way to see the quadratic is to hold everything else constant and grow a single line. Here’s a document whose entire content is one line of plain prose.

Line length Before (ms) Fast paths (ms) Now (ms)
5 KB 26.4 26.3 0.07
10 KB 109 105 0.08
20 KB 423 408 0.12
40 KB 1,760 1,630 0.22
80 KB 8,180 7,110 0.37

Read the first two columns: each doubling of the line multiplies the time by four. That’s a quadratic curve, and the grammar fast paths barely dented it, because per-character fragments and a quadratic merge are exactly what grammar fast paths don’t touch. Now the third column: each doubling roughly doubles the time, because string-index runs at memory speed and there’s one fragment per line instead of one per character.

At 80 KB that’s a factor of about 22,000. That’s the extreme end of the curve, though. The realistic headline is the 6.4× on the README, where the time is spread over a whole document instead of concentrated in one pathological line.

The collapse pass on its own  #

To separate the two effects I benchmarked Guile’s string-collapse against the library’s collapse-tree on plain lists of string fragments, with no parser and no renderers involved.

Fragments string-collapse (ms) collapse-tree (ms)
2,000 3.8 0.09
4,000 15.4 0.17
8,000 58.6 0.37
16,000 241 0.72
32,000 1,000 1.9

Four times per doubling on the left, two on the right. That’s the whole profile in one table.

What the profile looks like now  #

The point of a rewrite is that the profile stops pointing at one thing. Parsing the README two hundred times now gives this.

%     cumulative   self              procedure
 26.32      0.75      0.75  %after-gc-thunk
  8.27      0.41      0.24  string-append
  6.02      0.17      0.17  string=
  4.51      0.24      0.13  orgwebalchemy/peg.scm:99:0
  4.51      0.15      0.13  reverse
  3.76      0.15      0.11  string-group-linear
  3.01      1.43      0.09  orgwebalchemy/peg.scm:375:0
  2.26      1.86      0.06  collapse-tree

The biggest entry now is the collector itself, about a quarter of the time. Then the string work that building a tree implies, then the collapse pass at a few percent, then a long tail of small pattern procedures. No more 97%. Further gains have to come from allocating less, not from finding another bottleneck.

Why you can believe the measurements  #

Speed is easy to claim and easy to fake, because you can make any parser faster by making it wrong. Since the work touched the scanners that decide where text ends, I needed a way to show the language hadn’t changed.

Equivalence with the previous parser  #

The test suites can’t express “the parser still does what it did before”, because they encode what it ought to do. So there’s a separate check that loads the old parser and the new one into one process — the old one under renamed modules pulled out with git show REV:src/orgwebalchemy/peg.scm and its neighbors — and compares six results for every input: the document tree, the inline tree, and the HTML and Markdown each renderer writes.

The scanner rewrite was checked this way on about 70,000 inputs. Those included every pair of interesting characters (printable ASCII, tab, newline, carriage return, the other control characters and two multi-byte characters) as documents, as inline text, and inside a link, a footnote and a description list; the README; the test fixtures; hand-written edge cases such as markup at every position, tables with empty cells and lines without a newline; and about 6,000 randomly generated documents.

The empty-value change was checked again on about 38,000 of those inputs. It reported about 230 differences, every one of them a change I’d made on purpose. More interesting: it found no input that raised an error on the new side, where the old code raised on 21 of them, each a table row with no cells in it. That’s how a bug fix should show up — as a difference you can name and explain.

The table change was checked on about 38,000 inputs and found 16 that parse differently. All 16 were table lines whose last bar is followed by nothing but the end of the line, which is exactly what changed.

The stop set  #

A run scanner may leap over a character only if it’s sure that character can’t begin a construct. Get that set wrong by one character and the scanner silently swallows the beginning of a link or a footnote, and no equivalence test against the previous parser would catch it, because the previous parser had no scanners at all.

So inline-stop-chars is checked against the grammar itself: at every position where inline-special really matches, the character that begins it must be in the set. Sweeping about 390,000 positions of the README, the fixtures, every pair of bytes and some random markup found about 6,000 constructs and no gap.

Three deliberate differences  #

Three differences came out of these runs, all now pinned down by tests.

  1. A run of two or more #+CAPTION: or #+NAME: lines before an element that isn’t a figure used to come out of the folding step in reverse order. It now keeps document order, which is what the docs always claimed and what a reader expects of anything called a sequence.
  2. The text of something empty is now the empty string, instead of the bare tag symbol an empty match leaves behind. This is what keeps an empty cell in its column, keeps a source block without a language rendering as a source block, and keeps an empty keyword, comment, fixed-width line, footnote or block in the document instead of letting it vanish.
  3. A table row is read by its bars, as described above.

Everything else produces identical output, and the naming feature is an addition to the language, so it’s covered by tests rather than equivalence.

A trap when adding tests  #

One trap is worth recording, because I lost an afternoon to it. Veritas collects a test’s assertions by filtering the body for procedures, so assertions wrapped in a let, a when or a parameterize are silently reduced to the last one. Keep every assertion at the top level of the test body, and call a helper when you need a parameterization. The tests for the class parameters show the pattern.

What is still expensive  #

Honesty is part of the design, so here’s what these numbers don’t say.

  • Inline parsing is the most expensive stage per byte. A 50-clause paragraph of inline markup, 4.1 KB, takes 2.3 ms, about 1.8 MB per second, against 11 MB per second for a whole document of prose. Some of that is inherent in a grammar where every position has to try the alternatives. Still, it’s the number I’d attack next.
  • Allocation is still dominated by the tree. About 68 bytes of allocation per input byte, and a quarter of the parse is the collector. A cheaper node representation, or flattening parts of the tree while parsing, would buy real time.
  • There’s no incremental path. A changed document is parsed from the beginning. For an editor or a live preview, that’s the feature that would matter most, and the one I most want to write.
  • The benchmarks measure this implementation on its own. They don’t compare it with other Org implementations, and they don’t measure the incremental case. Both would be welcome additions.

What Org has that this library does not  #

The manual has a “Current limitations” section so this article doesn’t have to pretend. The known gaps:

  • Captions on non-figures. A #+CAPTION: before a table, a source block or a block quote is still an ordinary keyword line, because those captions aren’t rendered yet. Figures are the exception, and they’re done.
  • Bare superscripts and subscripts. Only the braced forms, x^{2} and H_{2}O, are recognized. Bare forms like x^2 are not, deliberately, since they can’t always be told apart from ordinary text.
  • Babel, the agenda, the export dispatcher. OrgWebAlchemy is a library, not an Emacs package. It parses Org; it doesn’t execute code blocks and doesn’t hook into Org’s export machinery.
  • The long tail. Org has years of conventions in it: #+ATTR_HTML, #+RESULTS, column width cookies, archived subtrees, custom TODO sequences read from the document, and more. Each one needs a decision about whether to parse it, render it or leave it as text, and this library would rather leave it as text than guess.

If one of these gets in your way, that’s exactly the kind of report worth sending, ideally with the smallest Org snippet that shows the problem.

Distribution  #

A library nobody can install is a library nobody uses, so packaging caught up too.

  • Guix. A self-contained package recipe is in the manual, pinned at version 1.2.1 with its hash, and the project is on its way into Guix itself as guile-orgwebalchemy.
  • Nix. A flake.nix and a default.nix build the working tree with the same derivation as the nixpkgs package, so a change that breaks one breaks the other for the same reason. They cover x86_64 and aarch64, on GNU/Linux and on Darwin.
  • Continuous integration. Pushes to trunk trigger a Woodpecker pipeline that submits the maak-test and maak-deploy tasks to Byggsteg, my own CI system, itself written in Scheme.
  • Documentation. The manual is the library’s README, and the API reference is generated from the docstrings in the source with guile-documenta: the OrgWebAlchemy API documentation.

Reproduce the measurements  #

Every number here comes from the repository and from commands the manual documents. If you want to check my work:

git clone https://codeberg.org/jjba23/orgwebalchemy.git
 cd orgwebalchemy

 #  the unit suite
guix shell -m manifest.scm -- guile -L ./src -L ./test  \
  -c  '((@ (veritas runner) run-tests))'

 #  the benchmarks (they print the tables above)
 VERITAS_TEST_TYPE=benchmark  \
guix shell -m manifest.scm -- guile -L ./src -L ./test  \
  -c  '((@ (veritas runner) run-tests))'

Run them yourself. If your numbers differ a lot from mine, I’d like to know — a parser that’s fast on one laptop and slow on another has a wrong assumption hidden in it.

What I take from this  #

The lesson I keep relearning here is that correctness and speed aren’t two phases of the work. Writing the scanners forced me to state exactly which characters can begin a construct, and that list is now a specification a test sweeps about 390,000 positions. The equivalence harness that showed the rewrite was safe is also the reason I dared attempt it.

Which brings me to the larger point, the reason I care about parsing Org at all. Org mode is one of the good ideas of our time: plain text people can read and write without special software, meaning that survives the programs, grown inside a free editor where its users could extend it. Every program that can read Org makes the format a bit more useful, and a format more programs can read is one people can trust with years of their writing.

This library is a small contribution to that. It lets a Scheme program read what you wrote in Org, render it as HTML a browser can show without running anyone else’s code, or as Markdown other tools understand — under a free license, on your machine, with source you can read. The web has no shortage of ways to take a document away from its author. I’d like to add one more way to give it back.

Links  #

If you build something with OrgWebAlchemy, send a link — it’s always good to see where the library travels. And if this is worth supporting, you can buy me a cup of coffee ☕ so I can keep at it.

Saturday, October 24, 2026

Saturday, October 17, 2026

Scheme Requests for Implementation

SRFI 278: Supplemental Numerics

SRFI 278 is now in final status.

This SRFI defines procedures on Scheme numbers that aim to improve the portability and usefulness of Scheme arithmetic. These procedures include predicates with extended domains, hyperbolic and inverse hyperbolic functions, the R6RS numeric tower, and additional procedures for rounding. Also included are recommendations and justifications for mixed inexact/exact arithmetic to make complex number arithmetic more predictable.

by Peter McGoron at Saturday, October 17, 2026

Friday, October 16, 2026

Retropikzel's blog

Thursday, October 15, 2026

Scheme Requests for Implementation

SRFI 275: URIs, IRIs, and basic paths

SRFI 275 is now in final status.

This SRFI proposes a programming interface for working with RFC 3986 universal resource identifiers (URIs), as well as RFC 3987’s generalization to internationalized resource identifiers (IRIs). This document defines record types, normalization procedures, and conversion between URIs and IRIs. The proposal also specifies a basic programming interface for working with paths in isolation, as this is a pervasive usage of URIs and IRIs. Finally, we contribute a test suite to specify the behaviour of normalization with respect to relative references, which has in the past been a source of divergence between implementations.

by Duncan Guthrie at Thursday, October 15, 2026

SRFI 287: Graphs

SRFI 287 is now in draft status.

This SRFI proposes a programming interface for working with graphs, based on Martin Erwig’s functional graph library design. The FGL design takes a rather different view of graphs from most graph presentations, in that the data structures are persistent, which is a good fit for Scheme. The interface is generic to any node and edge label data-type, as long as a SRFI 128 comparator can be specified.

by Duncan Guthrie at Thursday, October 15, 2026

spritely.institute

End-to-end encrypted relays for OCapN

Creating a direct connection between any two computers (not just from client to server) over the internet is notoriously difficult. Many home networks are behind NATs which prevent new inbound connections. Sometimes these NATs are from the ISP (carrier-grade NATs) which are impossible to work around with port forwarding and/or UPnP. While hole punching techniques can break through NATs, it's far from guaranteed. Thus, we need something that works in the general case: relays.

Relays are servers, accessible over the public internet, that act as a proxy between two computers that cannot directly connect. In Goblins 0.12 we introduced a federated relay named "prelay". While OCapN traffic is encrypted in transit, the prelay crucially lacks end-to-end encryption (E2EE). E2EE secures all the messages so they can only be read by their intended recipient, preventing intemediatory parties (like the relay) from tampering with or reading them. Due to the lack of E2EE, the relay was named "prelay" (pre-relay) to dissuade its use. Thanks to a grant from NLnet, we're happy to announce that the next release of Goblins will include a federated, E2EE relay implementation!

Having a relay that we could recommend has become even more of a critical part of the OCapN story as we've developed Hoot, our Scheme to WebAssembly compiler. The web is a challenging environment in which to build peer-to-peer applications. Most of the patterns and available APIs are designed around client-server architectures and centralized certificate authorities for security. While relays are unsatisfying compared to making direct connections, they enable peer-to-peer applications to communicate in these hostile environments.

In addition to implementing the E2EE relay for Goblins, our NLnet grant also includes writing a specification for the OCapN group so that the relay can be broadly supported by other OCapN implementations.

Design

We wanted OCapN relays to have the following qualities:

  1. Stateless: no storage of user data; easy to replace or migrate
  2. Federated: reduces the power any one relay holds over the wider network
  3. End-to-end encrypted: prevents man-in-the-middle and replay attacks; perfect forward secrecy

Our existing "prelay" already has the first two properties. To get E2EE, we chose to build upon the Noise Protocol.

Noise is actually a well-established framework of different patterns and ciphers. This allows applications to pick the pattern which is most suitable to their needs. It's also a protocol family which has widespread use (WireGuard, WhatsApp, etc.) and has been scrutinised a great deal. None of us at Spritely are cryptographers by training, so using an off-the-shelf protocol also ensures we're avoiding any mistakes in protocol design.

So how can we use Noise with OCapN? When connecting to an OCapN peer, there is a Locator which encodes three components about the remote peer:

  1. Designator: self-authenticating identity (usually a public key)
  2. Transport: a symbol identifying the kind of netlayer it is (such as onion)
  3. Hints: a hashmap of routing data (such as IP, port, etc.)

Each netlayer decides what goes into these fields, and the union of both the designator and transport should uniquely identify the peer. Noise uses two kinds of keys:

  1. static: a key that doesn't change between sessions
  2. ephemeral: a key that is generated fresh for each session

An important step each netlayer needs to consider is something called peer authentication, which verifies that a peer is who they say they are. OCapN requires that there exists at most one connection between any two peers at a given time. Ensuring there is only ever one session between any two peers is essential to allow for object identity comparison and preventing unnecessary 3rd party handoffs. An OCapN implementation will enforce this by reusing connections or closing new inbound connections if one already exists. Without peer authentication, a malicious peer could impersonate another peer causing a denial of service to the peer whose identity was stolen.

As described above, the designator is a public key, which allows the peer to prove their identity by using its private key. Noise has a pattern for this known as XK, where the initiator knows the key of the responder and will verify it. The X is referring to the initiator's static key being transmitted to the responder. The K is referring to the initiator already knowing the responder's public key. The public key of a peer is encoded and placed in the designator for the initiator to use.

Noise also allows the use of different key generation algorithms, cipher suites, and hashes. This is great as we needed to select something which has implementations both in commonly available cryptographic libraries and within the browser (for Hoot). X25519 keys are well supported in all those situations, as are AESGCM cipher suites and SHA256 hashes. These are all available in GnuTLS (what we're using on the Guile VM) and in the SubtleCrypto API (that we're using on the web).

Guile Noise library

Of course, to use Noise we need an implementation. Noise is popular enough that there's quite a few implementations for different languages. Unfortunately, Guile is not one of them, so that meant writing our own Noise library.

The Guile Noise library we've written doesn't require users to know the intricate workings of Noise, but Noise is interesting and so let's take a brief look at how it works and my implementation experience. The Noise specification is actually pretty approachable and is built around three conceptual objects:

  • CipherState: holds a key and a hash; provides encryption, decryption, and rekeying
  • SymmetricState: holds a chaining key and hash output; uses CipherState for encryption, as well as deriving new keys and hashes
  • HandshakeState: holds state for the initial handshake and handles reading and writing of handshake messages

The Noise protocol starts with a handshake. This is turn-based where the initiator goes first and writes their handshake message. The receiver then reads that message and writes their own. This continues until the handshake is complete. After which you only need the CipherState which can encrypt and decrypt messages (and if desired rekey).

Interestingly, Noise defines a small intepreter which defines a limited set of operations. The handshake patterns are then defined in terms of these operations. I won't go through all of them (see the spec if you're interested), but the XK pattern is defined as:

pre-message <- s
-> e es
<- e ee
-> s se

This is a handshake consisting of three messages (and a pre-message). Pre-messages are not exchanged as part of the Noise handshake itself, they're messages which are expected to have happened out-of-band prior to connection. In this case <- s means the static key from the responder is sent to the initiator. This happens by it being encoded in the OCapN peer locator as we discussed above. The arrows show the direction with the left side being the initiator and the right being the responder.

I'll just cover the first message, -> e es. This message is for the initiator to send to the responder as it starts with ->. The operations are:

  • e: generates an emphemeral key, transmits the public part, and mixes it into an internal hash
  • es: derives a new key (from the ephemeral key and responders static key) and mixes it into its own key

This allows a Noise library to easily support lots of different patterns. Each pattern can define it's pre-messages and handshake messages (all of which are defined in the spec) and the implementation just needs to go through and perform the steps.

Once the handshake is complete, the Noise library will create two CipherState objects. The first is used for encrypting and decrypting the initiator's messages, the second for the responder's messages.

The Guile library implements all 12 of the fundamental interactive patterns. We built this library to work on both Hoot and the Guile VM. It currently uses SubtleCrypto for Hoot and GnuTLS for the Guile VM, but it's written so users of the library can add support for other crypto libraries. Our library comes with support for X25519 key generation, AES-GCM ciphers, and SHA256 and SHA512 hash functions. Noise not only provides an approachable spec, but also a set of test vectors to help implementation. These are a set of messages which Noise should exchange for a given pattern and keys. Our library has been tested against all the vectors for patterns and crypto that we support and the test runner checks them on Hoot as well as the Guile VM.

Noise netlayer design

OCapN is a layered protocol consisting of CapTP (Capability Transport Protocol) and many netlayers. A netlayer sends encoded messages between two peers over a particular medium. This allows the higher-level CapTP to be transport agnostic. For example, the Onion netlayer provides transport over Tor Onion services. However, netlayers can be defined not only in terms of a specific transport protocol, but also in terms of another netlayer. We call these meta netlayers.

Noise itself isn't designed with a specific transport protocol in mind, and the Noise netlayer isn't either. Indeed many netlayers might want to rely on the properties of Noise for many different reasons. Even netlayers which provide strong encryption, like Tor, may wish to rely on Noise for its bidirectional peer authentication properties (Tor authenticates outbound connections to a hidden service, but not the reverse).

The Noise netlayer thus composes with any other netlayer. Composing the Noise netlayer and prelay netlayer yields an end-to-end encrypted relay netlayer!

How does this work? The Noise netlayer spawns a sort of "bootstrap" object (not the same bootstrap if you're familiar with CapTP). The bootstrap object is encoded within the OCapN peer locator, and can be enlivened to make a live reference when making a new outbound connection. Each side spawns an inbox and outbox pair, and the initiating peer delivers their outbox to the bootstrap object. The bootstrap object sets up its side of Noise and makes its own inbox-outbox pair returning its outbox. Each side is then able to deliver messages to each other by sending messages to the remote outbox object. Each side then follows the handshake procedure. Once complete, encrypted messages can be exchanged by using the Noise encrypt procedure and decrypted when read from the inbox.

Looking at the test code which uses the TCP+TLS netlayer, layering on Noise encryption is just a few lines of code:

(define a-mycapn
  (with-vat vat-a
    ;; Spawn the TCP+TLS netlayer
    (define tcp-tls-netlayer (spawn ^tcp-tls-netlayer "127.0.0.1"))
    ;; Spawn the Noise netlayer, passing in the TCP+TLS netlayer
    (define noise-netlayer (spawn ^noise-netlayer tcp-tls-netlayer))
    ;; Spawn the MyCapN object (CapTP)
    (spawn-mycapn noise-netlayer)))

The Relay netlayer

So, how does this work with the relay? Well, since we already have an non-E2EE relay and now have seen we can compose Noise and get the properties of both, we can simply pass in the prelay netlayer to Noise netlayer and get an E2EE relay. Of course, we don't want to ask users of the netlayer to do this. We'll be replacing the prelay module with a new relay module that is always end-to-end encrypted.

While the relay design (in its non-E2EE form) isn't new to Goblins, it's worth quickly taking a look at how the relay works. The relay itself is built on the idea of a relay client and a relay server. On the relay server there are two actors:

  1. Controller: allows the client to steer its own relay accepting new inbound connections and making new outbound connections
  2. Endpoint: which is encoded in the OCapN locator and is publicly accessible, a relay server messages this object when establishing a new connection to the peer

This makes the relay federated. Two OCapN peers can use different relay servers and still communicate using OCapN between the two relay servers. When a client wants to make an outbound connection to a peer on another relay server, the relay can enliven the endpoint sturdyref (getting a live object) and send a message to it.

The animation above shows the flow between two peers using federation. It starts with two actors on two different peers, Alice (on peer A) and Bob (on peer B). Alice has a sturdyref (an OCapN locator specifying an object) to Bob. Here's how the flow works:

  1. Peer A first sets itself up with the controller by registering an object a-incoming with the controller. This object can be used when establishing new connections.
  2. Alice has a sturdyref to Bob but wants a a live reference to Bob that she can message, she asks her MyCapN to /enliven/ it.
  3. MyCapN creates an adeliver object which faciliates the connection. It then sends a message to the relay controller with the sturdyref and adeliver object and asks the relay to make a new connection.
  4. The controller extracts the endpoint object for Peer B's relay from the sturdyref and enlivens it forming a new connection between the two relays. It also spawns a new rarelay object to facilitate communication between these two peers and hands this back to adeliver.
  5. The controller then sends the rarelay object to the endpoint on B's relay. B's relay then spawns its own analog to the object rbrelay.
  6. Peer B's relay reaches out to the b-incoming object, allowing it to establish a new connection to Peer B. It passes along the rbrelay object.
  7. Peer B's MyCapN object spawns a new bdeliver object which facilitates its connection.

This gives the two peers a connection to one another, even though neither share a common relay server. The relay is designed to be completely stateless, i.e. it doesn't hold on to data, configuration, or anything else.

One of the lessons we've learned since co-authoring ActivityPub is it's crucial to reduce the impact of servers on their users. In ActivityPub, each instance (a server) stores all the data for its users and performs all the operations their behalf. This means the user needs to trust the server a lot, and if the user's instance shuts down, or they simply wish to migrate, moving to another server is difficult.

Learning from those problems, it was critical for us to make sure the server was stateless. The relay itself stores no state/data for a user and, aside from connecting to another peer, the only behavior it's expected to perform is relaying messages. This makes changing relay servers incredibly easy. The user simply needs to find another (or setup their own) relay server and include that in their sturdy references instead.

Future work

The work isn't done, though. Adding support to Goblins was just one part of the NLnet grant. We'll be contributing specifications for the encrypted relay to the OCapN standardization group so others can implement it. Ridley, who has implemented OCapN in Dart, has already implemented their own Noise library in preparation for the E2EE relay. As part of the grant work within the OCapN group, we'll also be adding support to the test suite to allow both testing new relay implementations, and testing the rest of OCapN over the relay.

Another milestone of the grant is to implement a Guix service to make it easy to run an instance of the relay. The relay doesn't take many resources to run and it has very few dependencies, so it should be easy for others to package for their distributions and run on the cheapest VPS or old computers users might have around their home. We are looking forward to the deployment of a vast network of OCapN relays.

Thanks to our supporters

Your support makes our work possible! If you like what we do, please consider becoming a Spritely supporter today!

Diamond tier

  • Aeva Palecek
  • David Anderson
  • Holmes Wilson
  • Jonathan Frederickson
  • Lassi Kiuru

Gold tier

  • Alex Sassmannshausen
  • Juan Lizarraga Cubillos
  • Nerine Pekowsky
  • Serra Allgood

Silver tier

  • Austin Robinson
  • Brit Butler
  • Charlie McMackin
  • Dan Connolly
  • Deb Nicholson
  • Diana Thayer
  • Eric Schultz
  • Evangelo Stavro Prodromou
  • Evgeni Ku
  • Glenn Thompson
  • James Luke
  • Jonathan Smith
  • Jonathan Wright
  • Justin Sheehy
  • Michel Lind
  • Mike Ledoux
  • Moto A
  • Nia Bickford
  • Noah Beasley
  • Steve Sprang
  • Travis Smith

Bronze tier

  • Alan Zimmerman
  • Ben Hamill
  • Benjamin Grimm-Lebsanft
  • Brooke Vibber
  • Brooklyn Zelenka
  • Carl A
  • Crazypedia No
  • Ellie High
  • François Joulaud
  • Gabor Udvari
  • Grant Gould
  • Gregory Buhtz
  • Ivan Sagalaev
  • Jason Wodicka
  • Jeff Forcier
  • Marty McGuire
  • Mason DeVries
  • Michael Orbinpost
  • Neil Brudnak
  • Nelson Pavlosky
  • Philipp Nassua
  • Rodion Goritskov
  • Ron Welch
  • Stefan Magdalinski
  • Stephen Herrick
  • Steven De Herdt
  • Tamara Schmitz
  • Thomas Talbot
  • William Murphy
  • a b
  • chee rabbits
  • r g
  • terra tauri

by Jessica Tallon at Thursday, October 15, 2026

Tuesday, October 13, 2026

The Racket Blog

Racket Infrastructure Compromise

On or before August 10th, a macOS system at Northwestern University which builds and hosts Racket snapshots was compromised. The attackers installed a crypto miner, which was detected by Northwestern network monitoring. By August 18th, the machine was fully deactivated, and was subsequently wiped.

After our investigation, there is no evidence that the attackers did anything other than install a crypto miner, or that they knew the system was connected to the Racket project, or that they used credentials on the system to access any other Racket infrastructure. In particular, we have confirmed that no changes were made to any Racket installers or package catalogs.

However, the system had access to signing keys for signing Apple binaries, and access to a machine that could sign Windows executables with the Racket Programming Language Foundation signing key. Therefore, we have generated new signing keys for all of these purposes. All binaries and installers signed with the potentially compromised keys have been replaced on download.racket-lang.org with newly signed versions. We have informed both Apple and GlobalSign, the issuers of these certificates, of the compromise.

GlobalSign has now revoked the Windows signing certificate. As a result, Windows installers signed with the old certificate may no longer be able to be installed; anyone who downloaded such an installer should download a new one from download.racket-lang.org. However, installed Windows binaries are not affected.

Unfortunately, once the Apple certificate is revoked, existing installations of signed macOS binaries will no longer function. To avoid excessive disruption to students and classes in the middle of the term, we expect to have the Apple certificate revoked at the end of the current semester. However, we recommend that anyone who has a macOS installation of Racket versions from 8.12 through 9.3 install new binaries from download.racket-lang.org now, rather than waiting for the revocation. If you installed “Minimal Racket”, then after reinstalling, you can upgrade all installed packages that include signed libraries with raco pkg update --all.

To ensure that this cannot happen again, we have made several changes to our infrastructure. The relevant machine, now wiped and reinstalled, no longer handles many different Racket services. Additionally, systems that sign macOS snapshot binaries are no longer able to access Windows signing keys. Also, we now use separate signing keys for snapshots and releases, so a problem with snapshot systems cannot affect releases in this way.

We recognize that everyone who uses Racket trusts the project, and trusts us, to provide them with trustworthy software. We apologize for the failure here, and for the difficulties it imposes on all of our users.

Tuesday, October 13, 2026

jointhefreeworld

Where hexagonal architecture shines, and what to reach for instead

Ports and adapters, also called hexagonal architecture, is the pattern I reach for when a system has rules worth protecting, and the pattern I refuse when it has none. This article is the map between those two cases.

If you have read Hexagon of Doom, you know the other half of the argument: on a small service with a thin domain, the hexagon is pure indirection, and the indirection costs more than the boundary buys. On the tech radar, ports and adapters now sits in Adopt, with exactly that qualification in its note: the boundary earns its keep wherever the domain is the complicated part.

Both statements are true at once, and the whole skill is telling which one describes the code in front of you. Read “hexagonal” as a shortening of “hex-a-GO-nal”, and “ports and adapters” as plain English: the domain declares what it needs, and the outside world supplies it.

The one thing the pattern actually requires  #

A port is an interface, owned by the domain, written in the domain’s own words. A adapter is the code outside the domain that satisfies it: a database, an HTTP client, a clock, a message queue, a filesystem. That is the entire pattern. Folders, layers and names like infrastructure/ports/out/ are decoration on top of one rule:

The domain must not depend on the world. The world depends on the domain.

Everything the pattern buys follows from that rule, and nothing else does. The tests you gain are free of I/O because the domain never asked for I/O; the swap you gain is cheap because a swap is one function call in one place; the framework upgrade that does not touch your rules is quiet for the same reason.

The deeper benefit, and the one people forget to mention, is thinking: a port is a sentence about what the domain needs. Writing that sentence out loud is often worth more than the code underneath it, because it forces you to notice that a “payment service” was really a price lookup plus a ledger entry.

Four questions that decide it  #

Before you draw a hexagon, answer these. They are the same questions I use on the radar, and they are honest about the answer “no”.

Question If the answer is yes If the answer is no
Is the domain the complicated part? the boundary protects something worth it you are protecting an empty room
Will there be more than one adapter, for real? the seam pays on the second adapter you built a seam for one implementation
Must the rules run without I/O? the port is the only way to test them the test will hit the database anyway
Will the domain outlive this framework? the boundary is the insurance, and cheap the framework is the system, and you know it

The last one is the question I care about most, and the one that is hardest to answer honestly on a Monday morning. A rule about how invoices are priced will outlive any framework you name. A controller that validates a form will not.

Kotlin and Spring: the pattern as the framework intended  #

Kotlin is a lovely language for ports, because an interface with one method is a sentence, and Spring already does dependency injection well. The port is an interface; the domain service takes one as a constructor parameter; the adapter wears the annotation.

 typealias  Sku =  String
 typealias  Cents =  Int

 data  class  Line( val  sku:  Sku,  val  quantity:  Int)

 //  The port: the domain's own interface, in the domain's own words.
 interface  Catalogue {
     fun  priceOf(sku:  Sku):  Cents
}

 //  The rule, in the only place it is allowed to live.
 class  QuoteService( private  val  catalogue:  Catalogue) {
     fun  total(lines:  List):  Cents =
        lines.sumOf { line -> catalogue.priceOf(line.sku) * line.quantity }
}

The adapter is the only Spring-aware part, and it should be the only part that imports the framework at all:

@ Repository
 class  JdbcCatalogue( private  val  jdbc:  JdbcTemplate) :  Catalogue {
     override  fun  priceOf(sku:  Sku):  Cents =
        jdbc.queryForObject(
             "select price from product where sku = ?",  Int:: class.java, sku,
        ) ?: error( "unknown sku:  $sku ")
}

Two things are worth saying about this. First, Kotlin’s own tools are enough to express the port: an interface, a class, a function type. Where the port has exactly one method, a function type is often better than an interface, because a lambda is the cheapest adapter in the world.

Second, and this is the tell I look for in a review: if an annotation from the framework appears inside the domain, there is no hexagon, whatever the folder names say. A @Transactional on a domain method, a @Entity on an aggregate, a @Component on a service that holds a rule: each one welds the domain to Spring, and the boundary you paid for is gone. Spring’s convenience is not the same thing as an architecture.

Where Spring’s own DI is already enough  #

Spring wires your objects, and that is genuinely most of what a small application needs. If the “domain” is a controller calling two repositories and assembling a response, ports add a second indirection on top of the container, and the container was already the seam. That is the Hexagon of Doom case, in Kotlin, and it is more common than anyone admits.

Guile Scheme: the same shape with none of the ceremony  #

In Scheme you do not need a container to invert a dependency. A port is a record of procedures, the composition root is a function, and the wiring happens in one place that a human being can read in ten seconds. Here is the same shop domain, with the module boundary carrying the architecture:

 ;;;  The domain, and the port it declares.  Nothing here knows that a
 ;;;  database, a web server or a clock exists.
( define-module ( shop domain pricing)
   #:declarative? #t
   #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-9)
   #:export (make-catalogue catalogue? catalogue-price quote-total))

 ;;;  A catalogue is not a class: it is a record holding one procedure,
 ;;;  from a SKU to a price in cents.  This is the port.
( define-record-type  
  (make-catalogue price-of) catalogue?
  (price-of catalogue-price-of))

( define ( catalogue-price catalogue sku)
  ((catalogue-price-of catalogue) sku))

 ;;;  The rule of the domain, in the only place it is allowed to live.
( define ( quote-total catalogue lines)
   "Return the total in cents of LINES, a list of (SKU . QUANTITY)."
  (fold ( lambda (line total)
          (+ total (* (catalogue-price catalogue (car line)) (cdr line))))
        0
        lines))

An adapter is a function that returns one of those records, and it is impossible to tell a clever adapter from a boring one:

( define-module ( shop adapters catalogue)
   #:declarative? #t
   #:use-module (shop domain pricing)
   #:export (fixed-catalogue alist-catalogue))

 ;;;  For tests and for the REPL: prices known in advance.
( define ( fixed-catalogue prices)
  (make-catalogue ( lambda (sku)
                    ( or (assoc-ref prices sku)
                        (error  "unknown sku" sku)))))

 ;;;  For development: a price list held in memory.
( define ( alist-catalogue rows)
  (make-catalogue ( lambda (sku)
                    ( or (assoc-ref rows sku)
                        (error  "unknown sku" sku)))))

The composition root names the adapter, and it is the only place that does:

( define ( main)
  ( let ((catalogue (alist-catalogue '(( "tea" . 650) ( "pot" . 2400)))))
    (format #t  "total: ~a cents~%"
            (quote-total catalogue '(( "tea" . 2) ( "pot" . 1))))))

Running it shows the arithmetic, and nothing else:

total: 3700 cents

And the test needs no mocking framework, because a catalogue that returns fixed prices is a catalogue:

( define ( test-quote-total)
  ( let ((catalogue (fixed-catalogue '(( "tea" . 100) ( "pot" . 500)))))
    ( unless (= 700 (quote-total catalogue '(( "tea" . 2) ( "pot" . 1))))
      (error  "quote-total is wrong")))
  (display  "quote-total: ok\n"))
quote-total: ok

This is why I keep coming back to Lisp for this pattern. The indirection tax that makes the hexagon expensive in Java-shaped code does not exist here: a port is data, an adapter is a closure, and the REPL lets you replace the adapter while the system runs and ask the domain what it thinks. LucidPlan and Byggsteg both put the data layer behind exactly this kind of seam, and I wrote about the repository eDSL that makes the SQLite adapter one define-repo-method long.

The one discipline Lisp demands is the discipline no compiler will give you: nothing stops you from calling sqlite3 inside the domain module. The protection is your module header, and a reviewer who reads it.

Haskell: the port is a value, and the types keep the promise  #

Haskell gives you the same record-of-functions port, except the compiler now checks the sentence you wrote:

 type  Sku  =  String
 type  Cents  =  Integer

 --  The port: a record of functions.  Nothing about a database here.
 data  Catalogue m  =  Catalogue { priceOf  ::  Sku  -> m  Cents }

 --  The rule, and the only place it is allowed to live.
 quoteTotal  ::  Monad m  =>  Catalogue m  -> [( Sku,  Int)]  -> m  Cents
 quoteTotal catalogue lines  =
  sum  <$> traverse ( \(sku, qty)  -> ( * qty)  <$> priceOf catalogue sku) lines

Now look at the type of quoteTotal. It says the function needs a catalogue and a monad, and it says nothing about a database handle, a connection pool or a logger, because it does not have one. In a language where effects are visible in the type, that is not a comment about your intent: it is a fact about the program.

The usual alternative is a type class ( class Catalogue m where priceOf :: Sku -> m Cents), which reads even better at the call site and is how a lot of production Haskell is written. It has one trap worth naming: a type class with a single instance for the whole program quietly says “everything in this monad may talk to the database”, and blessing all of IO is not the same as handing one component the capability it needs. When I want the boundary to be real, I pass the record.

Domain-driven design is the part that actually pays  #

If you remember one thing from this article, remember this: the abstraction that pays for itself is almost never the architecture, it is the model. Ubiquitous language, aggregates with real invariants, a repository that behaves like a collection of domain objects, domain events where the business already speaks in events. Do that well and the ports fall out almost as a side effect, because a good model has an obvious boundary.

The Repository pattern deserves its own paragraph here, because it is my favourite and the most misused. A repository is not a DAO with a nicer name. It exists so that the domain can say “give me the aggregate with this identity, save this aggregate, forget it” and stay ignorant of SQL, of tables and of the shape of rows. The scheme repository eDSL I linked above does precisely this: one record per repository, one procedure per operation, and the concrete implementation living in a module the domain never imports.

The failure mode is DDD vocabulary without DDD discipline: folders named domain/, entities that are bags of setters, an “aggregate” with no invariant, a “value object” that is a String. That is ceremony, and ceremony is what makes people write articles called Hexagon of Doom. If your model is anemic, no boundary will save it: anemic models are exactly the case where the hexagon has nothing to protect and the indirection is pure cost.

The tiny microservice: here, KISS wins  #

A service that receives a request, validates it, calls two other services and writes one row does not have a domain. It has a flow. Drawing a hexagon around a flow costs you four files per change, a debugging session that crosses three layers, and an onboarding document nobody reads. I have written that code, and I have deleted it again.

The signs that you are in this case, all at once:

  • the rules fit in one function, and that function is under twenty lines;
  • there is exactly one adapter per port, and there will be exactly one for the foreseeable future;
  • the tests are about wiring, not about behaviour;
  • a change to a business rule touches the port, the adapter and the wiring before it touches the rule.

When you see that, collapse the layers. Keep the naming good, keep the validation at the edge where the untrusted data is, and give the domain to the framework’s own mechanisms. The whole point of KISS is not to be lazy; it is to spend your complexity budget on the part of the system that is genuinely hard.

MVC still wins where the application is a conversation about pages  #

Now the part I find myself defending most often, because MVC has become unfashionable while quietly remaining the best fit for a large class of programs: content sites, admin tools, internal CRUD applications, anything where the user’s interaction is “ask for a page, get a page”.

MVC and hexagonal are not rivals. Look at MVC honestly and you find the same dependency arrows: the controller is an adapter for the transport, the view is an adapter for presentation, and the model is the domain. The difference is the cut: MVC slices the system by the request lifecycle, which is exactly the axis along which a page-serving application is understood.

My favourite implementation of that cut is GNU Artanis with SXML and the repository pattern, and this is the stack I reach for by default:

  • The route is the controller. Artanis hands the handler a request context, and the handler asks the repository for the model it needs.
  • The view is data. SXML is a list, so a page is a value. Artanis renders either a template file or an S-expression through the same tpl->html, which means a view can be a function, a fragment or a file, and there is no template language to learn beyond parentheses.
  • The repository is the door to the database. The SQLite implementation stays in its own module, behind the same eDSL as the rest of my projects.

A route in that shape is short enough to read out loud:

 ;;  The route is the controller.  tpl->html takes either a template file
 ;;  or an S-expression, and here the view is a value.
(get  "/jobs/:id"
  ( lambda (rc)
    ( let ((job (job-repo-find rc (params rc  "id"))))
      (tpl->html `(section (@ (class  "job"))
                           (h1 ,(job-ref job))
                           (pre ,(job-command job)))))))

Rendering is the same primitive Guile ships, which is why I can show you the output instead of promising it:

(use-modules (sxml simple))
(sxml->xml `(section (@ (class  "order"))
                     (h2  "order-1")
                     (p  "total: " ,total  " cents")))

order-1

total: 1300 cents

Why this still beats a browser application with a client-side framework for such a site:

  • One source of truth for state. There is no client-side copy of the model to synchronize, so there is no class of bug where the two copies disagree.
  • The browser does what it is good at. Links, forms, back button, bookmarks, printing and a page that works when the JavaScript does not arrive.
  • The whole stack is one language and one process. A stack trace goes from the route to the SQL without crossing a serialization boundary, and the same repository code serves the views and the jobs.
  • HTML is data. SXML means the view can be composed, tested and refactored like any other function, without a template engine that cannot be debugged.

MVC fails when the interaction becomes a conversation about state rather than a request for a page: a drag-and-drop editor, a live dashboard, a collaborative document. There the client is the application, and pretending otherwise is how you end up with a page reload per keystroke. Know which of the two you are building, and pick accordingly: that is the whole decision.

CQRS, when it is interesting  #

Command Query Responsibility Segregation, pronounced in full whenever you have to justify it, is the idea that writing and reading are different jobs, and that the write side should record facts while the read side derives whatever shape the reader wants.

I like to show it in its smallest honest form, with no framework at all:

 ;;;  The write side records facts.  It does not maintain a table, and it
 ;;;  does not know what anybody will read.
( define ( record-order-placed log order total)
   "Return LOG with one more fact: ORDER was placed for TOTAL cents."
  (cons `(order-placed ,order ,total) log))

( define ( record-order-refunded log order amount)
   "Return LOG with one more fact: AMOUNT cents were refunded on ORDER."
  (cons `(order-refunded ,order ,amount) log))

 ;;;  The read side is a fold over the facts.  It holds no state of its
 ;;;  own, so it can be thrown away and rebuilt whenever you ask.
( define ( order-summary log)
  (fold ( lambda (event summary)
          ( cond
           (( and (pair? event) (eq? 'order-placed (car event)))
            (put-summary summary (cadr event) (caddr event)))
           (( and (pair? event) (eq? 'order-refunded (car event)))
            (put-summary summary (cadr event) (- (caddr event))))
           ( else summary)))
        '()
        (reverse log)))

Three facts in, one derived summary out:

facts in the log: 3
read model: ((order-2 . 3700) (order-1 . 500))

That is CQRS in twenty lines: the write side appends facts, the read side folds them. The interesting part is not the code, it is the freedom: a new question about orders is a new fold over the same facts, without touching the write model or migrating anything.

CQRS earns its keep when at least one of these is true:

  • the read side needs several shapes of the same data, and each shape would otherwise be a query with six joins;
  • reads and writes have different load profiles, so they want to scale apart;
  • the history matters as much as the current state, for audit, for replay, or for the business itself;
  • the same facts feed more than one consumer: a screen, a report, a search index, an email digest.

It becomes a bill you cannot pay when there is one read shape, one table and one transaction, because now you own two models and the eventual consistency between them, and the only thing you bought is typing. The tell is a projection that exists to reproduce a SELECT you could have written in an afternoon.

The two gotchas  #

Two failures I have seen more than once, so you can recognize them earlier than I did:

  • Two models, one transaction, no story. CQRS is not free consistency. If your projection must be updated in the same transaction as the write, you have not separated anything; you have renamed it.
  • Events as a data dump. A fact should be written in the language of the business: “order placed”, “refund issued”. An event that carries a whole table row is a change log wearing a domain’s clothes, and it will break the first time you refactor the schema.

Choosing, in one table  #

Situation Reach for Because
Rules are the hard part, several adapters are real ports and adapters the boundary protects the expensive part
A flow: validate, call two services, write a row the framework, plainly there is no domain to protect
Pages asked for by a user, database behind them MVC, a repository, server-rendered views the request lifecycle is the natural cut
Several read shapes over the same facts CQRS, with a projection each question becomes a fold, not a migration
A model you cannot explain to a colleague domain-driven design, before any architecture the model is the abstraction that pays
A framework you might leave in two years a port at that seam, and only there insurance is cheap when it is bought in one place

The rows are not exclusive. My own projects use MVC at the edge, repositories for the data layer, ports where a real second implementation exists, and a fold wherever history is more useful than state. The radar entry and this table say the same thing, and they will keep saying it: match the pattern to the complexity you actually have, and be honest about which part of your system is complicated.

If you take one habit away from all of this, take the repository as a seam and the question “what would I have to change to satisfy this port a second time?”. If the answer is “one file”, you have an architecture. If the answer is “everything”, you have a folder structure.

Tuesday, October 13, 2026

Thursday, October 8, 2026

Scheme Requests for Implementation

SRFI 286: Controlling Library Availability

SRFI 286 is now in draft status.

R7RS Scheme allows programs to query library presence at macro-expansion time using
(cond-expand ((library ⟨library name ⟩) ...)). However, when an implementation ships a shared library tree across interpreters built with different feature configurations, or when third-party libraries depend on conditionally present implementation features, standardizing library presence detection becomes problematic. This proposal specifies library mask files (mask.slm), a declarative mechanism placed in library root directories to dynamically mask unavailable libraries based on the current feature set without requiring physical file removal or upfront parsing of library contents.

by Sergei Egorov at Thursday, October 8, 2026

Wednesday, September 30, 2026

Scheme Requests for Implementation

SRFI 269: Portable Test Definitions

SRFI 269 is now in final status.

This SRFI defines a portable API for test definitions that is decoupled from test execution and reporting. It provides four definition forms: the universal is macro for assertions, testing for giving groups of assertions human-readable context, test for grouping assertions into independently executable units, and suite for organizing tests into hierarchies. The exported metadata auxiliary syntax attaches user-provided information to tests and suites to adjust the behavior of a test runner, for example to select tests by tags or to enforce timeout values. The API is tiny, yet capable and flexible. By focusing on the definition, and leaving execution semantics to test runners, this SRFI offers a common ground that can reduce fragmentation among testing libraries.

Unlike side-effect-driven testing frameworks (e.g. SRFI 64), this API produces first-class runtime entities, making it easy to filter, schedule, wrap them in exception guards and continuation barriers, run in arbitrary order, and re-run dynamically generated test subsets. In addition to the usual CLI test runners, it enables runtime-friendly test runners that integrate well with highly interactive development workflows inside REPLs and IDEs, significantly increasing control over test execution, and shortening the feedback loop.

To bridge the test definitions and test runners, the SRFI specifies a message-passing programming interface and test loading and execution semantics recommendations for test runner implementers.

by Andrew Tropin and Ramin Honary at Wednesday, September 30, 2026

Friday, September 25, 2026

jointhefreeworld

Parsing Expression Grammar in Lisp: Converting Org to HTML with (ice-9 peg) in Guile

Hi everyone. In this blog post I want to take you in an adventure of parsing Org mode with Parsing Expression Grammars (PEG) in Guile Scheme (ice-9 peg) and converting to HTML (via SXML): OrgWebAlchemy.

I wanted to share something with you all that I’ve been working on for a while. It all started with some naive regular expressions to parse Org mode content, but I pretty quickly realized I needed something smarter than that to get to where I want to. It’s taken a while but I am finally more knowledgeable of what Parsing Expression Grammars can do, thanks to GNU’s great (ice-9 peg) module and tutorials.

I thought it might be interesting to people here who enjoy Lisp, Scheme, parsing, Org mode, or the general idea of meta-meta-meta-programming as I like to call it. Disclosure, AI has helped me get a grip of PEG and debug some things, but development of OrgWebAlchemy is “my own spaghetti” and the unit tests and manual verification (and lots of pretty printing the AST) has guided me towards quite a nice implementation (if I may say so myself).

Project’s source code @ Codeberg: https://codeberg.org/jjba23/orgwebalchemy

OrgWebAlchemy is a Guile Scheme library for parsing Org-mode documents into an AST and rendering them to HTML. My main use-case is to export Org to HTML without needing Emacs, and to integrate this feature into some projects of mine, allowing me to write Org mode and have it pretty rendered.

The basic idea is pretty simple:

(use-modules (orgwebalchemy html))

(org->html  "This is ~test~ code.")

becomes something like:

This is < code>testcode> code.

But the interesting part is what happens in between.

Org document

v Parsing Expression Grammar

v AST

v SXML -> HTML

See here an example showing how OrgWebAlchemy enables the LucidPlan project to render pretty Org mode to HTML



More resources  #

PEG vs. a mountain of regexes?  #

Org-mode looks simple until you actually try to parse it. Headings are easy. A paragraph is easy. A list is easy (wait actually no, this has made me sweat).

And then suddenly you have:

  • nested lists
  • ordered, unordered and description lists
  • different indentation levels
  • inline markup
  • links containing descriptions
  • source blocks
  • example blocks
  • quote blocks
  • tables
  • escaping
  • constructs which must stop consuming input at exactly the right place

At this point, the usual approach of adding another regular expression starts to become somewhat… adventurous. :-)

You end up with things like:

match this, unless that follows it, except inside this block, unless it is a description, but don’t consume the newline, unless the previous line was a list item…

That is not really describing a language anymore. It is describing the history of your parser’s bugs.

So OrgWebAlchemy uses Parsing Expression Grammars (PEGs) through Guile’s excellent (ice-9 peg) module. e.g.

(define-peg-pattern element body
  ( or empty-line
      heading
      separator
      table
      src-block
      quote-block
      example-block
      export-html-block
      description-list
      unordered-list
      ordered-list
      paragraph))

This is rather nice because the grammar itself starts looking like documentation for the language.

And Guile lets us express PEGs directly as S-expressions (alternatively you can also use the more traditional syntax if you don’t like it), which makes the Lisper in me very happy.

One thing I particularly like about this approach is that we have loose coupling and the detail of generating SXML and then rendering HTML is a “presentation concern”. this opens possibilities to later exporting to Markdown or other formats.

For example:

- name :: Josep
- project :: orgwebalchemy
- language :: Scheme

can become an AST along the lines of:

(description-list
 (unordered-item
  (desc-key  "name")
  (line-content  "Josep"))
 ...)

I’m still busy with the exact representation and getting it all right. But as of now v1.0 has some stability :-) I would really love feedback on the project from the great smart people that hang out around here.



Of course Org mode is a huge piece of (great) software, so I am far from supporting all features, but some core important constructs are there:

  • Headings (lines starting by n *)
  • Paragraphs (any “non-special” text)
  • Unordered, Ordered and Description lists (with any level of nesting)
  • Italic, Bold, Inline Code
  • Links with and without description (with nested parsing)
  • Horizontal separators (---–—) five or more dashes
  • Tables
  • #+begin_src
  • #+begin_example
  • #+begin_quote (with nested parsing)
  • #+begin_export html : Org syntax is parsed by your PEG grammar, but raw HTML export blocks bypass the Org inline parser and are emitted as trusted literal output.


YAY recursive lists  #

One of the fun parts has been getting nested Org lists right.

Something like:

- Item 1
  - Item 1.1
  - Item 1.2
- Item 2

should become a quasi-tree

The parser initially produces the flat sequence of list items, and the AST processing phase turns indentation into nested structure.

The HTML renderer can then naturally produce:

< ul>
  < li>
    Item 1
    < ul>
      < li>Item 1.1li>
      < li>Item 1.2li>
    ul>
  li>
  < li>Item 2li>
ul>

I do still have a small issue here, and that is about the mixing of different list types in nested way. Hopefully it’s a subtle bug to fix.

The HTML side uses SXML, because if we’re already writing Lisp, we might as well represent our HTML as Lisp data too. :-) that really helps a lot and makes building the markup tree so much nicer

I’ve taken care to allow full customization to the output HTML (via Guile parameters) so that the renderer isn’t hard-coded to one particular website’s idea of what HTML ought to look like.Most of them are plain list of classes, but per-heading-level customization is a bit more flexible:

(heading-classes
 ( lambda (level)
   ( case level
     ((1) '( "text-4xl"  "font-bold"))
     ((2) '( "text-2xl"  "font-semibold"))
     ( else '( "text-base")))))

Why am I making this?  #

Partly because I wanted it, I like a challenge, and it’s super fun to work with parsing, ASTs and the lot… I could just use Emacs to do this job as there is no better implementation of Org.

The way it’s coming together though, I like the idea of having a small, hackable, free-software Org parser written in Lisp that other people can extend and customize (perhaps add more renderers, or Org features).

Free software  #

OrgWebAlchemy is licensed under the GNU LGPL v3 or later.

The project is intended to soon be packaged for GNU Guix as guile-orgwebalchemy.

There is also a test suite in the repository which is already proving to be a good safety net and showcase of what the parser can do.



Closing thoughts  #

I’d be really happy to hear your thoughts, especially about the grammar, AST design, parser architecture, or interesting Org constructs that I have not handled yet.

Happy hacking! ✨

Friday, September 25, 2026

Wednesday, September 23, 2026