Planet Scheme

Monday, August 3, 2026

Scheme Requests for Implementation

SRFI 273: Extensions to Data (Type-)Checking

SRFI 273 is now in final status.

The original SRFI 253 established a basis for type-checked (or otherwise checked) data handling. But it lacked some quality-of-life features. This SRFI extends SRFI 253 to match existing implementation practice and common sense. Provided extensions are: check aliasing with define-check; pre- and post-declaration of type / check with declare-checked; return value checks in lambda-checked, case-lambda-checked, and define-checked; and some optimizable, supported, and explicitly unsupported patterns suggested to implementors.

by Artyom Bologov at Monday, August 3, 2026

Friday, July 31, 2026

Gwen Weinholt

The State of Chez Scheme in Debian

I have uploaded Chez Scheme 10.4.0 to Debian unstable. It has been a few years since there was a new Chez Scheme version in Debian, and that is all on me. 😅

The new release builds fine on all architectures according to the build logs. In case you missed it, Chez Scheme got an infusion of energy from the Racket people and gained portable bytecode support a few years ago. So for those architectures where there is no native backend, Chez instead generates portable bytecode.

There was a problem with m68k and hppa where they would sometimes get the wrong endianness for the portable bytecode, possibly depending on which buildd picked them up. But that should be fixed now as debian/rules constructs the machine type from Debian’s build variables.

Debian Scheme Dream Team

I moved the package to the Debian Scheme Dream Team! So now there are more people who can help maintain it. The team has been gathering some mass recently, which is really nice to see. I hope that together we can make Scheme a stronger language in Debian.

Cross-compilation was broken

I enabled cross-compilation from amd64 to arm64 in the Salsa pipelines and found that it was actually broken! The problem was that cross-compilation kicks off a secondary build where several of our build parameters were missing. So the secondary build couldn’t find zuo and also got the wrong C compiler.

This has been fixed by patching build.zuo. This patch should be upstreamed.

Future work

The chezscheme-dev package is not something I have actually tested myself. It ships libkernel.a, main.o and scheme.h. Could be working, nobody has ever said otherwise. :)

Then there are the portable bytecodes! It would be possible to de-dupe those in the archive. They could be built as Architecture: all packages and be reused. Now, e.g., sparc64 and ppc64 both build threaded 64-bit big endian bytecode, so those exist at least twice in the archive.

Reproducible builds

Last, but not least, Chez Scheme builds are not reproducible. This is becoming a real problem now because Debian’s release team has made reproducible builds mandatory. Chez Scheme will not be part of future Debian releases unless this gets fixed.

Thankfully it does seem to be fixable. The root of the problem is that unique identifiers are used to support separate compilation. If anyone’s interested in the background then they can check out Oscar Waddell’s Ph.D. thesis (warning: .ps.gz file).

The implementation described in Section 3.5 supports both internal and top-level modules. For internal modules, the new names generated by the expander must be locally unique, i.e., not otherwise visible within the same top-level expression. For top-level modules within a single compilation unit, the names must be unique within the compilation unit. When multiple compilation units may be linked together, the names must be unique across compilation units.

– Oscar Waddell, Extending the Scope of Syntactic Abstraction, §3.6.1

Chez Scheme generates a UUID for each session that gets embedded into gensyms and that then gets embedded into the code. This satisfies the need for unique identifiers that are different between separate compilations. It ensures that things work smoothly when you are using the compiler yourself. But we want reproducible builds, meaning byte-for-byte identical builds, so the UUID is a problem.

When building packages for Linux distributions, things are a bit different than when you’re using the compiler yourself. Our build system can tell us what code went into the build, including the dependencies that brought in Scheme code, and if those stay the same then there is no need to use different identifiers compared to the previous time we built the same code.

I’m toying with the idea of generating a session key from the package version numbers and passing it to configure. I think it can be done without changing anything outside of the build system (the Zuo code). Conceptually we would be doing this:

(#%$set-top-level-value! '$session-key "k-")
(compile-file "s/foo.ss")

It remains to be seen if this is enough or if there are other sources of non-determinism.

by weinholt at Friday, July 31, 2026

Tuesday, July 21, 2026

jointhefreeworld

Emacs Eglot for Scala and Kotlin (JVM)

When Emacs 29 made eglot the built-in, default Language Server Protocol (LSP) client, many of us rejoiced.

It is lightweight, fast, adheres strictly to Emacs philosophy, and doesn’t try to reinvent the wheel.

However, being minimal means that when an LSP server steps out of line or acts quirky, eglot doesn’t provide a million customizable toggles to fix it out-of-the-box. Instead, it expects you to leverage the power of Emacs Lisp.

In this post, I will dissect my production-ready eglot setup (part of my heks-emacs configuration) which I use in my day-to-day work, with Scala and Kotlin (and some Java).

For reference, find my full Eglot config here: https://codeberg.org/jjba23/heks-emacs/src/branch/trunk/src/modules/eglot.el

We will walk through basic language setups, specialized workspace configuration handling, and dive deep into some advanced JSON-RPC and advice-based workarounds for Scala (Metals) and Kotlin that make development truly seamless from Emacs and liberate you from IntelliJ ☺️.

It’s not perfect, but it’s pretty darn close to perfection if you ask me, and the developer experience and speed that it enables is just wild. Thank you Emacs, thank you GNU, thank you Eglot! 🐂



Before looking at the code, let’s talk about why we are doing this. For years, the conventional wisdom stated that if you write JVM languages, especially Scala or Kotlin, you must use IntelliJ IDEA. The narrative claimed that these languages are too complex for a standard text editor.

But what do you actually get with IntelliJ? A massive, monolithic Java application that frequently hogs 8GB+ of RAM, locks up your system while “indexing pre-built binaries,” and forces you into a closed proprietary ecosystem.

Emacs turns this paradigm on its head through three core strengths:

  • The Unix Philosophy of LSP: Instead of a single IDE trying to compile, index, and render your code simultaneously, Emacs splits these duties. Eglot acts as a lean, protocol-first transport layer that talks to dedicated language servers via JSON-RPC.
  • Infinite Hackability: If IntelliJ has a bug in how it auto-completes Kotlin code, you are stuck waiting for JetBrains to issue a patch. In Emacs, you can write a 10-line Lisp advice function to intercept the network payload and patch the bug live in your editor buffer.
  • Unified Interface: You use the same text-manipulation utilities, text-jumping tools ( xref), and completion frameworks ( corfu, company, etc.) whether you are adjusting a Nix expression, editing a Markdown file, or refactoring a massive Scala service.


Hooks, Keybindings, and Initial Configurations  #

Let’s start with how eglot is initialized. I use Elpaca and use-package to manage the configuration, ensuring it doesn’t download an external package since it is built-in ( :ensure nil). Then I add some hooks to automatically start the language server for certain modes.

( use-package eglot
   :ensure nil
   :hook ((scala-ts-mode . eglot-ensure)
         (sh-mode . eglot-ensure)
         (markdown-mode . eglot-ensure)
         (markdown-ts-mode . eglot-ensure)
         (nix-ts-mode . eglot-ensure)
         (html-mode . eglot-ensure)
         (css-mode . eglot-ensure)
         (css-ts-mode . eglot-ensure)
         (html-ts-mode . eglot-ensure)
         (js-mode . eglot-ensure)
         (js-ts-mode . eglot-ensure)
         (kotlin-ts-mode . eglot-ensure)
         (yaml-mode . eglot-ensure)
         (yaml-ts-mode . eglot-ensure)
          ;;  formatting
         (before-save . eglot-format-buffer))
   ;;  ..................
   ;;  more config
  )
  • Eglot-Ensure Everywhere: I hook eglot-ensure into almost every programming mode I use, adapting both classic modes and modern Tree-sitter ( *-ts-mode) alternatives.
  • Auto-Formatting: Adding eglot-format-buffer to before-save guarantees code style compliance automatically every time a file hits the disk.

My keybindings are nested under the C-c i prefix, keeping them memorable and consistent across languages. The mnemonic keyword is “IDE” .

 :bind (( "C-c i i" . eglot-find-implementation)
       ( "C-c i e" . eglot)
       ( "C-c i k" . eglot-shutdown-all)
       ( "C-c i r" . eglot-rename)
       ( "C-c i x" . eglot-reconnect)
       ( "C-c i a" . eglot-code-actions)
       ( "C-c i m" . eglot-menu)
       ( "C-c i f" . eglot-format-buffer)
       ( "C-c i h" . eglot-inlay-hints-mode))
 :init
( setq eglot-autoshutdown t
      eglot-confirm-server-edits nil
      eglot-report-progress t
      eglot-extend-to-xref t
      eglot-sync-connect 1
      eglot-connect-timeout 60
      eglot-autoreconnect t)

Then with these :init settings:

  • eglot-autoshutdown cleans up language server processes as soon as the last buffer managed by them is killed.
  • eglot-extend-to-xref allows Emacs’ cross-referencing commands to smoothly transition into external library files outside your workspace directory.

Fine-Tuning Server Definitions and Workspaces  #

Under the :config block, we begin optimizing specific language servers. For instance, removing default configurations before re-adding custom entries prevents collisions.

 :config
( setopt eglot-code-action-indications nil)  ;;  Cleans up Emacs 31 visual noise

 ;;  Clean slate for Scala and Kotlin
( setq eglot-server-programs (assq-delete-all 'scala-mode eglot-server-programs))
( setq eglot-server-programs (assq-delete-all 'scala-ts-mode eglot-server-programs))
( setq eglot-server-programs (assoc-delete-all 'scala-ts-mode eglot-server-programs))

(add-to-list 'eglot-server-programs `(scala-ts-mode . ( "metals"
                                                        "-Xmx4G"
                                                        "-XX:+UseZGC"
                                                        "-Dmetals.http=true"
                                                        :initializationOptions ( :isHttpEnabled t))))

( setq eglot-server-programs (assoc-delete-all 'kotlin-ts-mode eglot-server-programs))
(add-to-list 'eglot-server-programs '(kotlin-ts-mode . ( "intellij-server"  "--stdio")))

Why these changes?

  • Scala (Metals): I pass specific JVM tuning flags directly to Metals (allocating a comfortable 4GB heap and utilizing the Z Garbage Collector for minimal latency). Also, enabling Metals HTTP communication via initialization options lets us hook into specialized UI features if needed.
  • Kotlin: I swap out standard options for the IntelliJ-backed Kotlin Language Server ( intellij-server --stdio).

Global Workspace Configurations  #

eglot-workspace-configuration lets you pass customized variables downstream to your language servers. This section of my configuration acts like a universal settings.json:

( setq-default eglot-workspace-configuration
              '(
                 :metals (  :autoImportBuild  "all"
                           :isHttpEnabled t
                           :superMethodLensesEnabled t
                           :showInferredType t
                           :enableSemanticHighlighting t
                           :inlayHints (  :inferredTypes ( :enable t )
                                         :implicitArguments ( :enable nil)
                                         :implicitConversions ( :enable nil )
                                         :typeParameters ( :enable t )
                                         :hintsInPatternMatch ( :enable nil ))
                           :bloopJvmProperties [ "-Xmx4G"])
                 :haskell ( :formattingProvider  "ormolu")
                 :typescript ( :format ( :baseIndentSize 0
                                                       :convertTabsToSpaces t
                                                       :indentSize 2
                                                       :semicolons  "remove"
                                                       :tabSize 2))
                 :javascript ( :format ( :baseIndentSize 0
                                                       :convertTabsToSpaces t
                                                       :indentSize 2
                                                       :semicolons  "remove"
                                                       :tabSize 2))
                 :rust-analyzer ( :check ( :command  "clippy")
                                        :cargo ( :sysroot  "discover"
                                                         :features  "all"
                                                         :buildScripts ( :enable t))
                                        :diagnostics ( :disabled [ "macro-error"])
                                        :procMacro ( :enable t))

                 :yaml (  :format ( :enable t)
                         :validate t
                         :hover t
                         :completion t
                         :schemas (
                                  https://codeberg.org/jjba23/pop-test/raw/branch/trunk/resources/json-schema/pop-test.json [ "golden-test.yaml"  "golden-test.yml"  "pop-test.yaml"  "pop-test.yml"]
                                  https://raw.githubusercontent.com/Vandebron/gh-mpyl/refs/heads/main/src/mpyl/schema/project.schema.yml [ "project.yml"]
                                  https://json.schemastore.org/yamllint.json [ "/*.yml"])
                         :schemaStore ( :enable t))
                 :nil ( :formatting ( :command [ "nixfmt"]))))

Notable Configurations here:

  • Metals: Granular inlay hints are activated specifically for inferred types and type parameters while muting implicit conversions to keep buffers readable. (more options here: https://scalameta.org/metals/docs/editors/user-configuration/)
  • YAML Schema Mapping: Maps distinct internet-hosted JSON schemas straight to patterns of YAML files automatically.


Deep Dive: The Workarounds  #

This is where things get interesting. Sometimes servers violate standard LSP expectations, requiring custom Emacs Lisp logic to bridge the gap.

Fixing Eldoc Overload  #

By default, eldoc can easily get flooded by different feedback mechanisms. This block prioritizes structural code diagnostics over generic hover data:

(add-hook 'eglot-managed-mode-hook
          ( lambda ()
             ;;  Show flymake diagnostics first.
            ( setq eldoc-documentation-functions
                  (cons #'flymake-eldoc-function
                        (remove #'flymake-eldoc-function eldoc-documentation-functions)))
             ;;  Show all eldoc feedback.
            ( setq eldoc-documentation-strategy #'eldoc-documentation-compose)))

Kotlin Source Navigation (Jar URI Translation)  #

When traversing into a dependency library using Kotlin, the server returns file references formatted as jar:///path/to/library.jar!/File.kt. Emacs can’t resolve this scheme directly out of the box, throwing errors when you try to jump to definition.

By wrapping Eglot’s URI translators with advice, we can map this custom scheme into something Emacs understands (especially alongside companion extensions like jarchive):

( defun  heks/eglot-uri-to-path-kotlin (orig-fn uri  &rest; args)
  ( if ( and (stringp uri) (string-prefix-p  "jar:///" uri))
      (apply orig-fn (replace-regexp-in-string  "^jar:///"  "jar:file:///" uri) args)
    (apply orig-fn uri args)))

( defun  heks/eglot-path-to-uri-kotlin (orig-fn path  &rest; args)
  ( if ( and (stringp path) (string-prefix-p  "jar:file:///" path))
      (replace-regexp-in-string  "^jar:file:///"  "jar:///" path)
    (apply orig-fn path args)))

( if (fboundp 'eglot-uri-to-path)
    ( progn
      (advice-add 'eglot-uri-to-path  :around #'heks/eglot-uri-to-path-kotlin)
      (advice-add 'eglot-path-to-uri  :around #'heks/eglot-path-to-uri-kotlin))
  ( progn
    (advice-add 'eglot--uri-to-path  :around #'heks/eglot-uri-to-path-kotlin)
    (advice-add 'eglot--path-to-uri  :around #'heks/eglot-path-to-uri-kotlin)))

Intercepting the Kotlin Empty newText Auto-Completion Bug  #

A notorious issue in certain Kotlin LSP releases occurs during auto-completion. The server reports matching candidates, but mistakenly attaches a textEdit field containing an empty string ( newText: ""). This causes Eglot to wipe out the word you are completing entirely.

To solve this, I intercept the incoming JSON-RPC response payloads, both synchronous and asynchronous. If a Kotlin completion candidate returns an empty string edit, we strip the `textEdit` attribute completely, forcing Eglot to fall back gracefully to standard prefix matching.

( defun  my-jsonrpc-request-kotlin-fix (orig-fn connection method params  &rest; args)
   "Fix kotlin-lsp empty newText bug by removing textEdit to trigger Eglot fallback."
  ( let ((result (apply orig-fn connection method params args)))
    ( when ( and (eq method  :textDocument/completion)
               (derived-mode-p 'kotlin-mode 'kotlin-ts-mode)
               result)
      ( let ((items ( if (vectorp result) result (plist-get result  :items))))
        (seq-do ( lambda (item)
                  ( let ((text-edit (plist-get item  :textEdit)))
                     ;;  If the server sent an empty newText, strip textEdit completely
                     ;;  so Eglot falls back to replacing the actual prefix.
                    ( when ( and text-edit (equal (plist-get text-edit  :newText)  ""))
                      (plist-put item  :textEdit nil))))
                items)))
    result))

( defun  my-jsonrpc-async-request-kotlin-fix (orig-fn connection method params  &rest; args)
   "Fix kotlin-lsp empty newText bug in asynchronous Eglot requests."
  ( if ( and (eq method  :textDocument/completion)
           (derived-mode-p 'kotlin-mode 'kotlin-ts-mode))
      ( let* ((orig-success (plist-get args  :success-fn))
             (new-success ( lambda (result)
                            ( let ((items ( if (vectorp result) result (plist-get result  :items))))
                              (seq-do ( lambda (item)
                                        ( let ((text-edit (plist-get item  :textEdit)))
                                          ( when ( and text-edit (equal (plist-get text-edit  :newText)  ""))
                                            (plist-put item  :textEdit nil))))
                                      items))
                            (funcall orig-success result)))
             (new-args (plist-put (copy-sequence args)  :success-fn new-success)))
        (apply orig-fn connection method params new-args))
    (apply orig-fn connection method params args)))

(advice-add 'jsonrpc-request  :around #'my-jsonrpc-request-kotlin-fix)
(advice-add 'jsonrpc-async-request  :around #'my-jsonrpc-async-request-kotlin-fix)

Silencing Metals Semantic Refresh Flickering  #

Scala Metals aggressively forces full buffer semantic token refreshes. In large projects, this results in visual layout flickering and unnecessary CPU strain. Disabling this also can solve some startup issues for Metals.

( defun  my/eglot-disable-metals-semantic-refresh (orig-fn server)
  ( let* ((caps (funcall orig-fn server))
         (workspace (plist-get caps  :workspace))
         (tokens (plist-get workspace  :semanticTokens)))
    ( when tokens
      (plist-put tokens  :refreshSupport  :json-false))
    caps))

(advice-add 'eglot-client-capabilities  :around #'my/eglot-disable-metals-semantic-refresh)


Companion Packages: Java and Compressed Archives  #

To complete the setup, I load complementary minor modes outside of Eglot’s core file, ensuring smooth operations for Java and deep navigation for packed jars:

( use-package eglot-java
   :ensure t
   :after (eglot)
   :hook ((java-mode . eglot-java-mode)
         (java-ts-mode . eglot-java-mode)))

( use-package jarchive
   :ensure t
   :config
  (jarchive-mode))
  • eglot-java: Provisions proper workspace configurations specifically for Eclipse JDT LS seamlessly.
  • jarchive: Works harmoniously alongside the Kotlin JAR-URI translation hack, opening zipped up source containers into regular, viewable Emacs buffers.

The way I like it on reproducibility  #

I generally don’t use the “global” system wide JDK installation, but I use isolated development reproducible shells with Nix flakes.

I’ll eventually probably move to using Guix, but for now package availability isn’t quite there for JVM world so Nix it is.

This way you can easily work on the same machine with many environments and projects (e.g. different Java versions) and no need for SDKMan or version managers, but clean isolated per-project reproducible builds.

So I create a flake.nix and add it to Git.

Kotlin development flake (TODO intellij-server via Nix):

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    systems.url = "github:nix-systems/default";
  };
  outputs = { systems, nixpkgs, ... }:
    let
      eachSystem = f:
        nixpkgs.lib.genAttrs (import systems)
        (system: f nixpkgs.legacyPackages.${system});
    in {
      devShells = eachSystem (pkgs: {
        default = pkgs.mkShell {
          buildInputs = with pkgs; [
            ktfmt
            ktlint
            kotlin
            jdk25
            nil
            just
            yaml-language-server
          ];
        };
      });
    };
}

Scala development flake.

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    systems.url = "github:nix-systems/default";
  };
  outputs = { systems, nixpkgs, ... }:
    let
      eachSystem = f:
        nixpkgs.lib.genAttrs (import systems)
        (system: f nixpkgs.legacyPackages.${system});
    in {
      devShells = eachSystem (pkgs: {
        default = pkgs.mkShell {
          buildInputs = with pkgs; [
            scala_2_13
            jdk25
            metals
            sbt
            scalafmt
            scalafix
            scala-cli
            yaml-language-server
            coursier
          ];
        };
      });
    };
}

Then I load the flake with direnv so I create a .envrc file .

use flake

This way and inside Emacs I can use emacs-direnv to dynamically switch contexts inside Emacs LSPs and have even multiple running.

I also plug direnv into my Bash shell configurations and thus complete the development environment.

Conclusion  #

Eglot’s minimal, built-in design doesn’t mean you have to settle for sub-par language server behavior. After all, you are using Emacs, so the power is infinite!

By intercepting communication at the JSON-RPC level via advice-add, you can tailor client-server behaviors exactly to your liking.

Happy hacking! ✨

Tuesday, July 21, 2026

Saturday, July 18, 2026

Scheme Requests for Implementation

SRFI 270: Hexadecimal Floating-Point Constants

SRFI 270 is now in final status.

Floating-point numbers are usually stored in radix 2, but are written by users in radix 10. This SRFI introduces Scheme syntax for hexadecimal floating point constants based on C99’s syntax. They use radix 16 for writing the integer and fractional part, and a radix 10 exponent part that raises the whole value to a power of 2.

by Peter McGoron at Saturday, July 18, 2026

SRFI 271: Random port libraries

SRFI 271 is now in final status.

This SRFI proposes a pattern of libraries for binary input ports that produce random bytes. Libraries are divided into “randomized” and “determinized” categories to address different uses of random data. The design leaves the details of random number generation to the implementer and the transformation of bytes to other types (floats, etc.) to higher-level libraries. A mechanism for saving random-port states and propagating them to new ports is also provided.

by Wolfgang Corcoran-Mathe at Saturday, July 18, 2026

SRFI 278: Supplemental Numerics

SRFI 278 is now in draft status.

This SRFI defines miscellaneous procedures on numbers that were either missing from the R7RS or are common extensions.

by Peter McGoron at Saturday, July 18, 2026

Wednesday, July 15, 2026

jointhefreeworld

Maak: The power of Lisp that powers your trusty command runner and the enlightments

The infinitely extensible command runner, control plane and project automator Ă  la Make (Guile Scheme - Lisp)

Find the project at https://codeberg.org/jjba23/maak

Most build tools invent their own limited domain-specific language. Maak embraces the powerful Îť Lisp.

This also means your code is solid, reliable and robust for the next 50 years at least.

If you like my work, please support me by buying me a cup of coffee ☕ so I can continue with a lot of motivation.



Core features  #

Instead of learning a limited DSL, you can leverage your existing Lisp skills to define tasks, manage data, and automate your workflows with ease. Define functions, manipulate lists, use conditional, create macros—the entire language is at your disposal.

You can also easily call external shell commands and integrate with your existing scripts and tools.

All you will need to do to use Maak is to write a file (by default ./maak.scm) where you define your Maak file module and import Maak library:

( define-module ( maak)
   #:declarative? #t
   #:use-module (maak maak))


Defining tasks  #

Any function (with or without arguments) you define in this file becomes a runnable task.

Task Export & Visibility Rule  #

  • Open Fallback (No Exports): If you do not write an #:export directive in your module (or leave it empty), Maak is completely “open season”: all procedures in the file are treated as runnable tasks.
  • Strict Lock (With Exports): If you define anything in your #:export directive (e.g., #:export (quick-math)), then only the explicitly exported procedures will be treated as tasks. Any other functions become private helper procedures and are completely hidden from the CLI and --list command.

If no task is passed via command-line, Maak will run the default task.

Tasks can:

  • Run shell commands with the $ helper.
  • Print logs with log-info.
  • Call other Maak tasks.
  • Use any Guile Scheme function you define (or import from a library).
  • Some syntactic sugar for working with Guix dev shells and time-machine, e.g. manifest-shell and program-shell as well as time-machine-manifest-shell and more

⚠️ Beware not to use names for tasks that conflict with names of built-in Scheme functions as this can cause some problems. For example, avoid calling something format or display, choose something else instead.

You can extend Maak by defining new tasks — just like writing functions in Scheme.

Here’s an example:

( define ( hello)
   "Say hello from Maak!"
  (display  "Hello from Maak!")
  (display (format #f  "~a + ~a = ~a" 3 4 (+ 3 4))))

( define ( quick-math x-arg y-arg)
   ;;  beware task args come always as strings due to CLI parse
  ( let* ((x (string->number x-arg))
         (y (string->number y-arg)))
    (format #t  "~a + ~a = ~a" x y
            (+ x y))))

You can now run it:

maak hello
maak quick-math 23 42

You can also run multiple tasks easily (sequentially) via a comma-separated list :

maak fmt,compile,test

Often it’s just cleaner to do it directly in Lisp (Scheme code ) in your Maakfile (where it’s also trivial to add concurrent computations)

More advanced examples here and below:

 ;;  Format Scheme source code files according to the Guix style guide.
( define ( fmt)
  (log-info  "Format Scheme files using Guix Style")
  ($ '( "find . -maxdepth 8 -name '*.scm'"
        "-type f -exec guix style -f {} \\;")
      #:verbose? #t))

 ;;  Display program help screen.
( define ( help)
  ($ '( "guix shell -f guix.scm -- maak --help")))

 ;;  By default (no task given) run help.
( define ( default)
  ($ '( "maak --list"))

 ;;  Do some quick math
( define ( arithmetics)
  (fmt)  ;;  call another task first
  ( let* ((entries '(1 3 5 7 9))
         (arithmetics ( lambda(x) (* x x 3)))
         (some-data ( map arithmetics entries)))
    (log-info  "Running default task")
    (log-info  "Performed some fun arithmetics:\nResult: ~a" some-data)))


The project logo of maak was generated by the ChatGPT Dall-E LLM when fed this very document



Command Line Interface Options  #

Maak provides standard CLI options to manage your execution pipeline:

Short Flag Long Flag Argument Description
-f --file FILE Specify the path to the Maak file (defaults to ./maak.scm).
-h --help None Display the help screen and exit.
-l --list None List all available tasks in the currently loaded Maak file.
-n --dry-run None Dry-run mode. Print shell commands instead of executing them.
-q --quiet None Quiet mode. Suppress standard logging, banners, and header outputs.

To pass arguments directly to a task, it’s recommended to separate options from tasks with a double-dash ( --):

 #  Run clean and build in dry-run mode, quietly
maak -n -q -- clean,build

 #  Run a custom task with arguments
maak -f ./my-tasks.scm -- deploy production


Examples of maak files  #

Find below a list of some projects using maak for their automation. Reminder: maak.scm is the default name of a maak file, but you can choose to use a different one.

  • Heks GNU/Linux - The witches’ GNU/Linux: modular, flexible, reproducible, powered by Lisp and Fedora / Debian + GNOME / Niri
  • LucidPlan - Project management (CMS) for everyone - free and open
  • Veritas - Unit, Integration and Black Box testing framework powered by Lisp (Guile Scheme)
  • Mutastructura - Relational Schema and Database Migrations powered by Lisp (Guile Scheme)
  • GGG (Guile Glyph Generator) - Create SVG images, handy useful glyphs, org/markdown badges.
  • Hygguile - UI framework for cozy and professional user-interfaces for everyone with the power of Scheme.
  • SSS (Supreme Sexp System) - SSS is a Lisp machine adventure, where the hacking culture is celebrated. This custom GNU + Linux setup enhances customization to infinity, encourages the hacking spirit.


Why write Maak ?  #

Have you ever found Makefile to be repetitive, lacking expressiveness and having a weird, limited syntax and unexpected arcane behavior?

No fear, Maak is here. With the full power of Lisp directly in your command runner/control plane, easily define functions, data, lists, loop through them, macros, etc. Maak replaces the arcane syntax of Make with the power and elegance of a full-featured functional programming language: GNU Guile Scheme Îť.

Maak replaces Make’s runes and mystery with the power and clarity of GNU Guile Scheme λ.

The Problems with Make  #

Many developers find GNU Make and related tooling to be at times frustrating and not intuitive, despite being so powerful.

Makefile often contains repetitive code, particularly when dealing with similar targets or file types. You might have to write a separate rule for every single output file, even if the process is exactly the same. The limited syntax makes it difficult to abstract this logic into reusable functions or macros, leading to a lot of copy-pasting.

Make’s syntax is a Domain-Specific Language (DSL), not a general-purpose programming language. While it’s powerful for its intended purpose of managing dependencies, it’s terrible for anything else.

Defining variables, using conditionals, or looping over a list of items can be surprisingly clunky and often requires arcane, non-standard constructs.

Make has many “gotchas” that can trip up even experienced users.

How Maak Innovates  #

Maak positions itself as a modern infinitely extensible task runner, using the functional programming language GNU Guile Scheme (a dialect of Lisp). No need for .PHONY recipes here.

Maak gives you the power of Scheme. You’re not restricted to a limited, weird syntax. This means you can easily define functions to avoid repetition, create complex data structures (like lists and maps), and use control flow statements (like loops and conditionals) to write much cleaner and more expressive scripts.

Instead of having to learn a new, limited language, you can leverage your existing Lisp knowledge to define tasks, manage data, and automate your workflows. This leads to code that is much easier to read, write, and maintain. For example, you can write a simple loop to process all your source files instead of writing a separate rule for each one.

Maak is designed to be your central control plane. While Make is primarily focused on building software from source, Maak is a general-purpose command runner. This means you can use it for tasks like running tests, deploying applications, or managing your development environment. It’s meant to be a more flexible and powerful alternative for all your project’s automation needs.



Parameterize: customize the running of tasks and commands  #

Dry-run and quietness can be defined globally using the command-line options ( -n / -q), but they are also easily controlled and overridden in lexical scope via Lisp (Scheme) parameters.

This gives you a highly precise control mechanism when testing. For example, you can write a test task that dynamically runs destructive file manipulations safely in a localized dry-run scope, without requiring the user to pass a CLI flag.

Dry-runs for example are good for testing.

( define ( fmt)
   "Format Scheme source code files according to the Guix style guide."
  (syscall  "ls")  ;;  this will run
   ;;  this will not
  ( parameterize ((dry-run? #t)
                 (quiet? #f))
    (delete-file-recursively  "tmp")
    ($ '( "find . -maxdepth 8 -name '*.scm'"
          "-type f -exec guix style -f {} \\;")
        #:verbose? #t)))
 ;;  it will print instead or unning
 ;;  [DRY-RUN]: rm -rfv tmp
 ;;  [DRY-RUN]: find . -maxdepth 8 -name '*.scm' -type f -exec guix style -f {} \;


Help  #

The project’s automation is done using Maak itself, check the maak.scm file.

Also, find the technical Guile Scheme API documentation of Maak here:

https://jointhefreeworld.org/api-docs/maak/API.html

You can see the program’s help by invoking ggg with the --help argument or looking at resources/help.txt.



Installing  #

maak is officially distributed via:

  • Guix package manager
  • Podman/Docker images

That being said, feel free to use it as you wish, within the terms of the GNU General Public License v3 or newer.

On Guix  #

Maak is Guix-first and caters to Guix as first-class citizen in favor of other package managers or build systems. Maak targets exclusively systems that can run Guile Scheme (and optionally Guix). Check the maak.scm, the guix.scm and manifest.scm for more details.

Requirements:

  • Guix: The Guix package manager will ensure a reproducible working software, and will manage all needed dependencies for you.
  • Guile Scheme: This entire program is written using the official GNU extension language, Guile Scheme.

If you just want to quickly install it to your profile:

guix package --install-from-file=./guix.scm

For example, to enter an environment shell with maak temporarily you can use:

guix time-machine --channels=channels.scm -- shell -f guix.scm

You can also chain commands to it:

guix time-machine --channels=channels.scm --  \
     shell -f guix.scm --  \
     maak --help

You can also run a dev shell (with manifest)

guix time-machine --channels=channels.scm --  \
     shell -m manifest.scm -- guile -L ./src -c  \
      '((@(maak main) main))' --list

Maak is available in upstream Guix as maak in the module (gnu packages build-tools)

On Podman/Docker  #

Maak is also available as a Docker container, from DockerHub (also compatible with Podman).

https://hub.docker.com/repository/docker/jjba23/maak/general

You can also build images of maak yourself, using guix pack. See the maak.scm file for more. To load these tarball images, you can do podman load < my.tar.gz

Then you can run Maak from the container, and bind your local filesystem to give access, for example:

docker container run -v /home/joe:/home/joe  \
    docker.io/jjba23/maak:latest  \
    maak -f /home/joe/hacking/maak/maak.scm --list


Shell completions: Bash, ZSH, Fish  #

Maak offers a simple but powerful shell completion. When you are in a directory with maak.scm files, you can type maak followed by space, then hit TAB and you will see the names of maak tasks appear. This is done by reading the current file and extracting task names from it with some awk magic.

See the scripts at ./scripts . You should “source” these script if you want these completions to be available for you.

You might find these variables useful (for your .bashrc, .zshrc or fish config) specially if you run Guix, but feel free to download and load the scripts from ./scripts at your will in your shell.

For ZSH and fish shell, scripts are also provided, and should be loaded in similar fashion. See ./scripts

ZSH users will want to add the Maak completion script to $FPATH. Fish users will want to look at $fish_complete_path.

Bash example:

 maak_bin_install_dir=$( which maak)
 maak_install_dir=$( realpath "${maak_bin_install_dir}")
 maak_bin=$( dirname "${maak_install_dir}")
 maak_store=$( dirname "${maak_bin}")
 # !/usr/bin/ env  bash
 maak_completions= "${maak_store}/share/scripts/maak-completion.bash"

 #  Load Maak auto-completions
 if [[ -f  "${maak_completions}" ]];  then
   source  "${maak_completions}" || true
 fi


Maak integrations  #

Creating a connection to Maak from your favourite programmable environment should be simple.

You can see an Emacs integration here: maak.el



Licensing  #

Maak and all of its source code are free software, licensed under the GNU General Public License v3 (or newer at your convenience).

https://www.gnu.org/licenses/gpl-3.0.html

The documentation and examples, including this document, which are provided with Maak, are all licensed under the GNU Free Documentation License v1.3 (or newer at your convenience).

https://www.gnu.org/licenses/fdl-1.3.html



REPL: Interactive workflow, developer power  #

A REPL (Read-Eval-Print Loop) is an interactive environment, which can be used connected to your console, running application, language compiler and more, which gives you superpowers as an engineer 🦸🏼.

Lisp dialects, more specifically Guile Scheme, have great support for this. I personally of course like to do this with Guix, Emacs, ( Arei/Ares + sesman) you can get an ultimate extensible powerful editor experience, miles ahead of traditional IDEs 🐂 .

It fundamentally changes the development workflow by eliminating the slow edit, save, compile, run cycle. Instead of writing a whole program and then running it to see what happens, you get a fast, conversational workflow. What does this mean for in practice?

  • Incremental Development: Write, test, inspect, evaluate one function or even one line at a time. Get immediate feedback without running the entire app.
  • Powerful Debugging: Forget adding print statements and restarting. You can pause, inspect objects, change values, and even redefine a broken function on the fly to test a fix in any environment (yes even in production, while running).
  • Fast Prototyping & Learning: Instantly experiment with a new library or API. Just load it and start calling functions to see how they work, which is much faster than only reading documentation.

When integrated into your code editor, you can execute any piece of code (a line, a selection, or a file) with a keyboard shortcut and see the result instantly, creating a seamless and powerful development experience.



AI Policy  #

This project adheres to the jointhefreeworld AI (Artificial Intelligence) policy.

Our core principle is simple: AI should assist human creativity and problem-solving, never replace human reasoning.

While tools like Large Language Models (LLMs) and interactive chatbots can be beneficial for reviewing, refactoring small functions, or acting as a sounding board, they should be used with moderation.

We require a human in the loop for all contributions. The use of autonomous AI agents to automatically generate and submit pull requests to this project is strictly prohibited.



Code of conduct  #

This project adheres to the jointhefreeworld code of conduct. Find it here:

https://jointhefreeworld.org/blog/articles/personal/jointhefreeworld-code-of-conduct/index.html

In summary, we foster an inclusive, respectful, and cooperative environment for all contributors and users of this free software project. Inspired by the ideals of the GNU Project, we strive to uphold freedom, equality, and community as guiding principles. We believe that collaboration in a community of mutual respect is essential to creating excellent free software.



Maak Project  #

Contributing to free software is a uniquely beautiful act because it embodies principles of generosity, collaboration, and empowerment.

We welcome everyone to feel invited to the Maak Project, and encourage active contribution in all forms, to improve it and/or suggest improvements, brainstorm with me, make it more modular/flexible, etc, feel free to contact me @gmail.com> to chat, discuss or report feedback.

Find here the Backlog and Kanban boards for Maak: https://lucidplan.jointhefreeworld.org/tickets/maak

Maak embodies the spirit of GNU: simplicity, freedom, and curiosity. It’s both a tool and a playground for learning Lisp-based automation.

As you grow comfortable, extend Maak — define your own DSLs, orchestrate builds, or automate your projects in elegant Scheme.

Happy hacking! ✨



The Philosophy of Maak  #

Maak was designed with a few simple but powerful ideas in mind — ideas rooted in the GNU tradition and Lisp philosophy.

Lisp as the Language of Tasks  #

Every build rule, every script, is a first-class Scheme function. This means your automation scripts are composable, readable, and hackable.

Purity and Reproducibility  #

Maak believes in pure, deterministic environments, that’s why it integrates so well with GNU Guix 🐂.

Small Is Beautiful  #

No YAML, HOCON, INI or configs, no hidden logic, no magic — just clean Scheme code.

Every part of Maak can be read, understood, and extended within a good afternoon of hacking ☕.

Free as in Freedom  #

It’s free software under the GNU GPL, built to encourage curiosity, learning, and contribution.

You own your build logic, you can read it, change it, and share it freely.



Hacking on Maak  #

In systems where maak is already installed, a good way to compile from source and test all program functionalities is a pure shell:

guix shell --pure -f guix.scm  \
     bash coreutils util-linux-with-udev guile --  \
     maak --list

 #  run project tests
guix time-machine --channels=channels.scm --  \
     shell -f guix.scm --pure --  \
     maak test

Wednesday, July 15, 2026

Tuesday, July 7, 2026

Scheme Requests for Implementation

SRFI 277: Cyclic ports

SRFI 277 is now in draft status.

Cyclic ports are like infinite string and bytevector input ports: they produce the elements of a given sequence repeatedly, forever. While their intended use is as reusable seeds for SRFI 271 random ports, they are also useful whenever a repeating sequence of one or more bytes or characters is needed.

by Wolfgang Corcoran-Mathe at Tuesday, July 7, 2026

Wednesday, July 1, 2026

jointhefreeworld

Hacking Freedom: Compiling GNU Emacs from Source

By compiling GNU Emacs directly from the upstream Savannah repositories, you unlock the absolute bleeding edge of the extensible, self-documenting operating system disguised as a text editor.

True autonomy over your computing environment sometimes involves building your own tools and customizing many programs.

We start by cloning the live development branch straight from the GNU project’s forge.

 #  maker sure to have Git
sudo apt update
sudo apt upgrade

sudo apt install git

 cd ~/Fork  #  or wherever
git clone https://git.savannah.gnu.org/git/emacs.git emacs-build
 cd emacs-build

Dependencies  #

Depending on the compilation options we choose, we need some build and compile time dependencies. Before we can shape the metal, we need the furnace. For example in Debian we need things like:

sudo apt update
sudo apt install build-essential autoconf imagemagick libmagickwand-dev libgtk-3-dev librsvg2-dev libsqlite3-dev libgccjit0 libgccjit-15-dev libgnutls28-dev libtree-sitter-dev

Pro-tip: If you want a quick shortcut to grab standard development headers for graphics and window management libraries, you can run sudo apt-get build-dep emacs before moving to the next step.

Autogen  #

Run the autogen.sh the first time This script will generate the configuration scaffold. You only really need to do this once (and I always forget about it for this very reason). Simply do this on the command line:

./autogen.sh It checks that you have all you need to get started and prints output like this:

Checking whether you have the necessary tools...
(Read INSTALL.REPO for more details on building Emacs)
Checking for autoconf (need at least version 2.65) ... ok
Your system has the required tools.
Building aclocal.m4 ...
Running 'autoreconf -fi -I m4' ...
Building 'aclocal.m4' in exec ...
Running 'autoreconf -fi' in exec ...
Configuring local git repository...
'.git/config' -> '.git/config.~1~'
git config transfer.fsckObjects 'true'
git config diff.cpp.xfuncname '!^[ 	]*[A-Za-z_][A-Za-z_0-9]*:[[:space:]]*($|/[/*])
^((::[[:space:]]*)?[A-Za-z_][A-Za-z_0-9]*[[:space:]]*\(.*)$
^((#define[[:space:]]|DEFUN).*)$'
git config diff.elisp.xfuncname '^\([^[:space:]]*def[^[:space:]]+[[:space:]]+([^()[:space:]]+)'
git config diff.m4.xfuncname '^((m4_)?define|A._DEFUN(_ONCE)?)\([^),]*'
git config diff.make.xfuncname '^([$.[:alnum:]_].*:|[[:alnum:]_]+[[:space:]]*([*:+]?[:?]?|!?)=|define .*)'
git config diff.shell.xfuncname '^([[:space:]]*[[:alpha:]_][[:alnum:]_]*[[:space:]]*\(\)|[[:alpha:]_][[:alnum:]_]*=)'
git config diff.texinfo.xfuncname '^@node[[:space:]]+([^,[:space:]][^,]+)'
Installing git hooks...
'build-aux/git-hooks/commit-msg' -> '.git/hooks/commit-msg'
'build-aux/git-hooks/pre-commit' -> '.git/hooks/pre-commit'
'build-aux/git-hooks/prepare-commit-msg' -> '.git/hooks/prepare-commit-msg'
'build-aux/git-hooks/post-commit' -> '.git/hooks/post-commit'
'build-aux/git-hooks/pre-push' -> '.git/hooks/pre-push'
'build-aux/git-hooks/commit-msg-files.awk' -> '.git/hooks/commit-msg-files.awk'
'.git/hooks/applypatch-msg.sample' -> '.git/hooks/applypatch-msg'
'.git/hooks/pre-applypatch.sample' -> '.git/hooks/pre-applypatch'

You can now run ./configure Do not be intimidated by it. Focus on the final line instead, which directs you to the configure directive.

Configuration Flags  #

This is where you sculpt Emacs to your exact workflow. True hackers audit their build environment—you can inspect every configuration option available by running ./configure --help.

For a modern, highly optimized, Wayland-native hacker workstation, these choices are optimal:

./configure --with-native-compilation=aot  \
            --with-tree-sitter  \
            --with-pgtk  \
            --with-dbus  \
            --with-imagemagick  \
            --with-mailutils

Why these flags matter:  #

--with-native-compilation=aot
Compiles Emacs Lisp directly into native machine code Ahead-Of-Time. Maximum performance, zero lag.
--with-tree-sitter
Swaps out old regex parsing for high-performance, incremental AST parsing. Better structural navigation and syntax awareness.
--with-pgtk
Pure GTK. Critical if you are running a modern Wayland compositor and want to bypass the legacy Xwayland translation layer completely.

Compile and Install  #

First, ensure you are starting from a completely pristine state. If you are rebuilding an older tree or updating a previous commit, wipe away old compilation artifacts:

make clean

Now, unleash the compiler. Instead of hardcoding an arbitrary job number, let’s query your machine to leverage every single parallel thread your processor has to offer:

make -j$( nproc)

Once the compilation wraps up successfully, verify your creation in-place before deploying it system-wide:

./src/emacs --version

You should be greeted by that glorious declaration of software liberty:

GNU Emacs 31.0.50
Copyright (C) 2026 Free Software Foundation, Inc.
GNU Emacs comes with ABSOLUTELY NO WARRANTY.
You may redistribute copies of GNU Emacs
under the terms of the GNU General Public License.
For more information about these matters, see the file named COPYING.

If everything looks pristine, purge any pre-packaged, stale distro binaries and inject your custom-built system into the local environment:

sudo apt remove emacs emacs-pgtk
rm -rf ~/.emacs.d
sudo make install

Do a final check to confirm your shell path resolves directly to your new build:

which emacs
emacs --version

How to Roll Back  #

A good hacker respects filesystem layout and modularity. Do not delete this build directory. Keeping this folder intact ensures you retain the local blueprint of your installation. If you ever want to upgrade to a newer upstream commit or cleanly purge this build from your system, simply navigate back here and run:

sudo make uninstall

Your operating environment remains clean, untainted, and completely under your control. Welcome to the bleeding edge. Happy hacking! 🚀

Wednesday, July 1, 2026

Tuesday, June 30, 2026

Scheme Requests for Implementation

SRFI 124: Ephemerons

SRFI 124 is now in withdrawn status.

An ephemeron is an object with two components called its key and its datum. It differs from an ordinary pair as follows: if the garbage collector (GC) can prove that there are no references to the key except from the ephemeron itself and possibly from the datum, then it is free to break the ephemeron, dropping its reference to both key and datum. In other words, an ephemeron can be broken when nobody else cares about its key. Ephemerons can be used to construct weak vectors or lists and (possibly in combination with finalizers) weak hash tables.

Much of this specification is derived with thanks from the MIT Scheme Reference Manual.

by John Cowan at Tuesday, June 30, 2026

SRFI 254: Ephemerons and Guardians

SRFI 254 is now in final status.

This SRFI describes three concepts associated with the storage management of a Scheme system, ephemerons, guardians, and transport cell guardians.

An ephemeron is a record structure with a key and a value field. An ephemeron can be broken. Breaking an ephemeron replaces the key and value with #f. An implementation of this SRFI breaks an ephemeron when it proves that the storage occupied by the key could be reclaimed if the ephemeron were broken.

A guardian is a structure containing objects as guarded or resurrected elements. Initially, guardians are empty. Objects can be added in guarded elements to the guardian by the programmer. An implementation of this SRFI resurrects an element when it proves that the storage occupied by the object could be reclaimed if all guardians in the system were empty. Objects from resurrected elements can be queried and removed from the guardian by the programmer. Instead of the object itself, a representative can be returned.

A transport cell guardian is a structure containing transport cells, similar to a guardian. Whenever an object in a guarded transport cell in the transport cell guardian is moved by the garbage collector, the transport cell is resurrected so that it can be queried by the programmer.

by Marc Nieper-Wißkirchen at Tuesday, June 30, 2026

Saturday, June 27, 2026

Gwen Weinholt

Loko Scheme 0.13.0

Loko Scheme 0.13.0 is now available from:

A bootable disk image for 64-bit PCs is available from:

The signatures are made with the GnuPG key 0xDD839B748F10AD4D.

Loko Scheme 0.13.0 fixes bugs, improves performance and adds features. See NEWS.md in the distribution for a more detailed summary of changes.

Loko Scheme is an optimizing Scheme compiler that builds statically linked binaries for bare metal, Linux and NetBSD/amd64. It supports the R6RS Scheme and R7RS Scheme standards.

Loko Scheme’s web site is https://scheme.fail, where you can find the release tarballs and the manual. There is also a mailing list at https://lists.scheme.fail.

Loko Scheme is licensed under the EUPL v. 1.2 or later.

by weinholt at Saturday, June 27, 2026

Tuesday, June 23, 2026

Scheme Requests for Implementation

SRFI 276: Type-specific Flonum Libraries

SRFI 276 is now in draft status.

This SRFI is an updated version of SRFI 144 that allows an implementation to support multiple flonum representations. Each flonum has its own separate library. Each library also has the ability to inspect properties of the flonum operations, such as rounding mode and deviations from IEEE 754 arithmetic. New flonum operations are also available, such as random number generation and serialization.

by Peter McGoron at Tuesday, June 23, 2026

Mark Damon Hughes

Under Stone 1.1

I made this for the Lisp Game Jam Spring 2026, and a month later I have a much more complete role-playing game:

UNDER STONE 1.1 release!

Complete game with:
Help!
Saving!
Job change!
Items!
Merchant!
Second dungeon!

WOW! $10 or PWYW.

Get it now on itch.io

by mdhughes at Tuesday, June 23, 2026

Monday, June 22, 2026

The Racket Blog

Rhombus v1.0

Rhombus version 1.0 is now available!

Rhombus major contributors: Mashfi Ishtiaque Ahmad, Taylor Allred, Nia Angle, Wing Hei Chan, Stephen De Gabrielle, Robert Bruce Findler, Jacqueline Firth, Matthew Flatt, Oliver Flatt, Kiran Gopinathan, Ben Greenman, Siddhartha Kasivajhula, Alex Knauth, Jay McCarthy, Lucas Myers, Alec Mills, Sam Phillips, Sorawee Porncharoenwase, Jens Axel Søgaard, and Sam Tobin-Hochstadt.

Rhombus Goals

Modern programming languages reflect a consensus on the most important programming concepts, including lexically scoped variables, closures, objects, pattern matching, and type parametricity. Why, then, yet another programming language?

Beyond the basics, there are still more good ideas for programming constructs than can fit in any one language specification. Furthermore, specific domains benefit from language support that is tailored to the domain. Language extensibility helps to balance the competing goals of a manageable language size versus fit-to-purpose for a wide range of tasks.

Many newer languages include a macro system to enable extensibility, but other macro systems have not achieved the expressiveness and fluidity of macros as they exist within the Lisp tradition, which includes Racket. At the same time, that expressiveness has been difficult to detangle from Lisp’s minimalistic, parenthesis-oriented notation.

Rhombus is designed to be

  • approachable and easy to use for everyday purposes (that do not need macros), which in part means a conventional syntax; and

  • as extensible as Racket, while making Racket’s state-of-the-art facilities more consistent and accessible to a wide audience.

Frequently Asked Questions

  • What kind of programming language is Rhombus?

Rhombus is a general-purpose, functional, extensible programming language with good performance, extensive documentation, and practical libraries. It’s a dynamic language that offers interactivity and flexibility, but it also has the static and abstraction-enforcing constructs that are needed to scale from small scripts to large systems.

  • Aren’t there a lot of languages like that already?

While there are many small things we think are unique to Rhombus, including compact repetitions using ellipses (…) and a default set of functional data stuctures with good asymptoptic complexity, the big difference is extensibility. See Rhombus Goals.

  • Is it fast?

Here are some benchmarks.

  • How do I get started?

See Getting Started.

  • Do I have to use DrRacket?

The DrRacket programming environment is the easiest way to get started, but see Magic Racket for VSCode or Racket mode (with its racket-hash-lang-mode major mode) for Emacs.

  • What is the relationship of Rhombus to Racket?

Rhombus is built on Racket, and it relies on many Racket tools, including the DrRacket programming environment and the raco command-line suite. Roughly, the languages are related in the same way as Elixir and Erlang or Kotlin and Java.

Then again, it would be fair to say that Rhombus is just Racket, because Racket is meant to be a multi-language ecosystem, and simply starting a Racket module with #lang rhombus instead of #lang racket makes it a Rhombus module. Rhombus, in turn, is meant to push Racket’s multi-language capabilities forward and enable more languages and dialects that are built on Racket and Rhombus.

  • Rhombus is simply Racket with a different syntax, right?

A new syntax reflects the main goal of Rhombus, but #lang rhombus also improves on #lang racket in other ways: better predefined data structures (especially lists), a new class system, pervasive pattern matching, extensible static information as a new point on the spectrum of contracts to types, hierarchical namespace organization, and more.

These general language improvements could have been implemented for a Racket dialect that’s based on S-expressions, but language–syntax codesign for Rhombus opened more possibilities and produced a whole that’s greater than the sum of the parts.

  • Rhombus is Racket without S-expressions, so the syntax is not homoiconic, right?

Hello, fellow Lisper! Rhombus has a bicameral syntax, where the analog to the S-expression layer is shrubbery notation. This is an important part of Rhombus’s approach to macros and metaprogramming. You might be amused by this little metacircular interpreter.

  • Is Rhombus useful only if I want to get into extensible languages, domain-specific languages (DSLs), and/or macros?

Using Rhombus does not necessarily mean writing macros, because Rhombus gives you everything you expect (and probably a lot more) in the base language. The fact that a rich base language is made possible by macro extensibility could be considered an implementation detail or an academic concern. If you enjoy functional, dynamic languages and are interested in a modern synthesis, Rhombus might be for you.

  • Are macros actually a good idea?

The design of Rhombus reflects a conviction that metaprogramming is fundamental to software construction, and that the most effective approach to metaprogramming is one that is integerated with a general-purpose language.

In particular, accomodating domain-specific languages (DSLs) within a general-purpose language avoids some common DSL pitfalls, such as siloed languages that are difficult to integrate into an application, or half-baked abstraction constructs added to a DSL that itself inevitably needs to evolve. Meanwhile, taking metaprogramming seriously benefits not only DSLs, but also metaprogramming tasks such as documentation, analysis, and tool support.

The term macro conjures a variety of meanings and connotations. The approach taken in Rhombus might be more precisely characterized as compile-time metaprogramming or an open-compiler API, but its origins are in Lisp-style macros.

  • Is Rhombus an academic language? A research language? A teaching language?

Rhombus is rooted in academia, but it is not a teaching language, and it is not just a research language. It is intended for production use.

Rhombus cannot yet provide the wealth of libraries available for the most widely used languages. But as an outgrowth of Racket, it has the resources and community needed to persist and evolve. Users should expect a similar level of stability, consistency, and support that Racket has offered for decades.

  • Do we need new programmings languages or DSLs in an age of autonomous coding agents?

Who knows?

A common early prediction around AI coding was that it would spell the end of new languages, because AI would only be able to use the most popular languages as represented in training data. That prediction has not panned out. As of May 2026, (even before Rhombus 1.0), coding agents are pretty good at writing idiomatic Rhombus code. Maybe good documentation helps.

As for DSLs, it seems possible that raising the level of discourse in programming is good for human programmers, good for autonomous programming agents, and good for conversations between them. In that case, we’ll want languages with better DSL support, and that is Rhombus’s goal.

Example Rhombus Programs

The Rhombus web page at https://rhombus-lang.org/ includes a carousel of short examples.

For larger and real-world examples, it’s still early days, but Rhombus contributors have used Rhombus themselves for a number of tasks — including, of course, libraries in the Rhombus distribution.

  • Pille is a new language that is built on Rhombus. It exercises Rhombus’s language-building facilities while using LLVM as a back end. This is a metaprogramming-heavy example.

  • Economancy is a tabletop game with Rhombus implementations of a referee, player programs, and a minimal GUI interface, all implemented in Rhombus as part of a course on functional programming. It demonstrates everyday functional programming with Rhombus.

  • rhombus-html-lib is a package included with Rhombus. It provides a full HTML 5 parser that was AI-implemented following the HTML 5 specification. The implementation is more Java-style and imperative than ideal for Rhombus code, and there’s room for performance improvement, but it demonstrates a sizeable use of Rhombus.

  • Slides for a networking and security were all implemented in Rhombus and its animated-picture library, pict. Slide code is not typical, and as some of the oldest Rhombus code, it’s not the most modern, but it’s a substantial code base.

  • pict-demo is even more pict and even more metaprogramming. The repo contains a draft artifact for an upcoming ICFP’26 paper about the pict library. The running example involves animating evaluation steps, and the implementation uses an eval_tree.rhm library that expands a program into a combination of evaluation and animation components.

  • Shplait is another teaching tool: a language that combines ML’s type system and Rhombus syntax. It’s used in the programming languages course at Utah.

  • rhombus-draw-lib is another package included with Rhombus. It wraps and refines the racket/draw library to implement the Rhombus draw version. Like some other Rhombus packages, this one illustrates an approach to reusing Racket libraries.

by Matthew Flatt at Monday, June 22, 2026