Notes
2025/06/14

RC week 4: building a language to write a blog post

This blog post is written using a simple site generator written in Vorpal, the programming language I've been building at the Recurse Center for the past few weeks.

Here's the full source code for this post:

Html lang: "en" stylesheet: "../styles/style.css" title: "RC week 4: building a language to write a blog post" sections: [
    Note date: ["2025", "06", "14"]
    Section blocks: [
        ["This blog post is written using a simple site generator written in ", Link href: "013-rc-week-1-vorpal-a-minimal-and-malleable-language.html" text: "Vorpal", ", the programming language I've been building at the ", Link href: "https://www.recurse.com" text: "Recurse Center", " for the past few weeks."]
        ["Here's the full source code for this post:"]
        Code(quine!())
    ]
    Section title: "Why?" blocks: [
        ["It seemed like a great way to write a non-trivial and useful program in my language. But more importantly, it allowed me to test both the “prelude” (the 10 or so functions that are currently included in every program) and Vorpal's ability to encode data in a natural and readable way."]
        ["Vorpal is quite minimal, closer to a (purely functional) Lisp (with effect handlers) than most other languages, but it does support quite a bit of syntactic sugar, which is general enough that applies to all functions and data structures. The file shown above is just normal code which uses Vorpal's keyword call syntax, which is borrowed from ", Link href: "https://hexdocs.pm/elixir/main/optional-syntax.html" text: "keyword lists in Elixir:", " The call ", Code(#"Note date: ["2025", "06", "14"]"#), " is just sugar for “calling” the atomic string ", Code("Note"), " with the combined keyword list as the last argument, in other words ", Code(#"Note([["date", ["2025", "06", "14"]]])"#), "."]
        ["Notice also how the page description, which is data, contains a call to the ", Code("quine!"), " effect, which suspends execution of the VM, loads the file that is being processed (using the Rust host) and then resume the VM with the code as a string. This is not really a quine, at most a ", Link href: "https://en.wikipedia.org/wiki/Quine_(computing)#%22Cheating%22_quines" text: "cheating quine", ", but hey, I wanted to get this blog post done and this seemed like a nice way to illustrate how you can mix data and effects."]
    ]
    Section title: "How?" blocks: [
        ["The program that generates the HTML based on the input data is relatively short, around 70 lines of code. Here's the function that transforms sections into HTML:"]
        Code(##"::gen-section(:title, :section) = {
    match (section) with: [
        Note([["date", [:yy, :mm, :dd]]]) -> { [
            #"<p><a href="..#notes">Notes</a><br />"#, yy, "/", mm, "/", dd, "</p>"
            "<h1>", escape(title), "</h1>"
        ] }
        Section([["blocks", :blocks]]) -> { [
            blocks |> map(gen-block("<p>", "</p>"))
        ] }
        Section([["title", :title], ["blocks", :blocks]]) -> { [
            "<h2>", escape(title), "</h2>"
            blocks |> map(gen-block("<p>", "</p>"))
        ] }
        Section([["title", :title], ["id", :id], ["blocks", :blocks]]) -> { [
            #"<h2 id=""#, id, #"">"#, escape(title), "</h2>"
            blocks |> map(gen-block("<p>", "</p>"))
        ] }
    ]
}"##)
    ]
    Section title: "What?" blocks: [
        ["Does this count as a full static site generator? No. Will it ever be usable by others? No, not really. But what I like about it is precisely that it's purpose-built for my website and that it doesn't need to support every HTML tag under the sun. Instead, it is ", Em("malleable"), " enough that I can quickly add whatever I need. This is exactly the niche that I want my language to fill: Building small, bespoke pieces of ", Link href: "https://www.inkandswitch.com/essay/malleable-software/" text: "malleable software", ", which feel more like a ", Link href: "https://www.robinsloan.com/notes/home-cooked-app/" text: "home-cooked meal", " than something mass-produced."]
        ["So, that's it. I want to gradually convert the existing pages of this website to use the site generator and hopefully finally add some RSS support!"]
    ]
]

Why?

It seemed like a great way to write a non-trivial and useful program in my language. But more importantly, it allowed me to test both the “prelude” (the 10 or so functions that are currently included in every program) and Vorpal's ability to encode data in a natural and readable way.

Vorpal is quite minimal, closer to a (purely functional) Lisp (with effect handlers) than most other languages, but it does support quite a bit of syntactic sugar, which is general enough that applies to all functions and data structures. The file shown above is just normal code which uses Vorpal's keyword call syntax, which is borrowed from keyword lists in Elixir: The call Note date: ["2025", "06", "14"] is just sugar for “calling” the atomic string Note with the combined keyword list as the last argument, in other words Note([["date", ["2025", "06", "14"]]]).

Notice also how the page description, which is data, contains a call to the quine! effect, which suspends execution of the VM, loads the file that is being processed (using the Rust host) and then resume the VM with the code as a string. This is not really a quine, at most a cheating quine, but hey, I wanted to get this blog post done and this seemed like a nice way to illustrate how you can mix data and effects.

How?

The program that generates the HTML based on the input data is relatively short, around 70 lines of code. Here's the function that transforms sections into HTML:

::gen-section(:title, :section) = {
    match (section) with: [
        Note([["date", [:yy, :mm, :dd]]]) -> { [
            #"<p><a href="..#notes">Notes</a><br />"#, yy, "/", mm, "/", dd, "</p>"
            "<h1>", escape(title), "</h1>"
        ] }
        Section([["blocks", :blocks]]) -> { [
            blocks |> map(gen-block("<p>", "</p>"))
        ] }
        Section([["title", :title], ["blocks", :blocks]]) -> { [
            "<h2>", escape(title), "</h2>"
            blocks |> map(gen-block("<p>", "</p>"))
        ] }
        Section([["title", :title], ["id", :id], ["blocks", :blocks]]) -> { [
            #"<h2 id=""#, id, #"">"#, escape(title), "</h2>"
            blocks |> map(gen-block("<p>", "</p>"))
        ] }
    ]
}

What?

Does this count as a full static site generator? No. Will it ever be usable by others? No, not really. But what I like about it is precisely that it's purpose-built for my website and that it doesn't need to support every HTML tag under the sun. Instead, it is malleable enough that I can quickly add whatever I need. This is exactly the niche that I want my language to fill: Building small, bespoke pieces of malleable software, which feel more like a home-cooked meal than something mass-produced.

So, that's it. I want to gradually convert the existing pages of this website to use the site generator and hopefully finally add some RSS support!