CLI
silk run game/server.silk

With Silk by Lotus Mundi
James J. Tolton · Clojure/Conj 2026



Open letter to Udacity CEO Sebastian Thrun · December 1, 2016 · ipetitions.com/petition/gameagentaind












By Lotus Mundi











A (love) language

A composition language

A tensor language

MyGame/Assets/
├── Scripts/Combat.cs
└── Silk~/game/combat.silk
As of October 2, 2026


silk run game/server.silk
silk compile <target> -o <dst>
./<dst>
_vm.Eval("(require 'game.combat)");
var damage = _vm.Resolve(
Sym.Ns("game.combat"),
Sym.Name("damage"));
damage.Invoke(30L, 10L);
var output = _vm.OpenPort(
SilkRT.Kw("t04", "out"));
foreach (var msg in output.Drain())
Handle(msg);
silk node --name game@host \
--cookie demo app.silk

At evaluation
One artifact
Unity · downstream builds

345 suites written for Silk test the language, runtime and standard library with clojure.test: 11,958 assertions, run on every backend. 142 xUnit tests cover the .NET host.
Silk’s results are compared with those of a reference implementation, namely real Clojure under Babashka and stock Erlang/OTP. J is a second reference for APL questions. A deliberate difference is recorded as a divergence.
Hosts and backends are checked against each other. A compiled program must reproduce the interpreter’s output exactly, the HLSL that ships is checked against the CPU run, and every tensor run is judged for backend parity.
OTP invariants are stated as properties and checked on generated inputs, with stock Erlang/OTP as the reference. The suites run continuously.
Fuzz scenarios are generated from a seed and can be replayed. A long-running soak process re-runs each failure to confirm it and records the seed that reproduces it. Since June, 756 soak campaigns have run.
83 suites ported from Clojure’s own test tree run against Silk, and all pass. A test marked as expected to fail that starts passing fails the build until the mark is removed.
A Scryer Prolog grammar enumerates 93,855 tensor programs from a formal presentation of the language. Each is judged against Dyalog APL, and none fails; no LLM writes or selects cases.
Language behavior is first specified as executable acceptance tests in Farley’s four-layer style, before the runtime implements it. A lint fails the build if a host term leaks into the upper layers.
Every product and application is itself a test: each game, mod, tutorial, IDE integration and manual example. Unity play-mode tests start a real Erlang node and check its output.
Linters and static checks run in the fast gate. silk fmt --check rejects unformatted code, and the editor warns on any definition that cannot run on the strict profile.


Choosing a programming paradigm

An immutable language in a mutable world

Solving the hardest problem in game systems design
How do we keep the wheels from falling off this thing? Also…


Into the game loop

MonoBehaviour components
UActorComponent
Scripts on a node tree

C# components



Scenes + prefabs
Levels + Blueprints
Nested scenes

Scenes + prefabs

C++ engine · Lua mods

Java · Kahlua

C# · Unity · Harmony

C# · MonoGame · SMAPI

Osiris · Lua via Script Extender

Original C · C# port



The WASM model
(ns game.combat)
(defn damage [hp amount]
(- hp amount))
private ISilkGameVm _vm;
private ISilkFn _damage;
void Awake()
{
_vm = SilkVm.Attach(
() => (long)(Time.fixedTime * 1000));
_vm.Eval("(require 'game.combat)");
_damage = _vm.Resolve(
Sym.Ns("game.combat"),
Sym.Name("damage"));
}
object Attack()
=> _damage.Invoke(30L, 10L); // 20

[SilkHostFn("t02.log", "info",
Doc = "Write to the Unity console.",
Capability = ":t02/log")]
internal static void Info(string msg)
=> Debug.Log(msg);
(ns t02.app
(:require [t02.log]))
(defn greeting [] "hello from t02")
(defn hello! []
(t02.log/info (greeting)))
return function(rt)
return {
pack = "course/t02-log",
provides = {"t02/log"},
host_fns = {
["t02.log/info"] = function(msg)
print(msg)
end,
},
}
end

???
An immutable language in a mutable world


OTP
Going from simple to easy
public sealed class PlayerCombat : MonoBehaviour
{
[SerializeField] Skeleton target;
public void Attack() => target.TakeDamage(10);
}
public sealed class Skeleton : MonoBehaviour
{
[SerializeField] int hitPoints = 30;
public void TakeDamage(int amount)
{
hitPoints -= amount;
if (hitPoints <= 0) Destroy(gameObject);
}
}

view = f(data)Who owns state, order, and lifetime?


Colony Optimizer · Silk × UI Toolkit: demand radar, convergence, champion base, orders in flight


A composition language
(actor/spawn
(fn skeleton [ui-manager-pid]
(loop [state {:hp 30}]
(let [state (handle state (actor/receive!))]
(actor/send! ui-manager-pid {:hp (:hp state)})
(recur state))))
ui-manager-pid)
(defrecord Counter []
gen/IGenServer
(gen-init [_ _ [n]] (r/=>Ok n))
(handle-call [_ _ _ _ n] (r/=>Reply n n))
(handle-cast [_ _ delta n]
(r/=>NoReply (+ n delta)))
(handle-info [_ _ _ n] (r/=>NoReply n))
(handle-continue [_ _ _ n] (r/=>NoReply n))
(terminate [_ _ _ _] nil))
;; client API: the only surface a caller sees
(defn start-link [parent n]
(gen/start-link parent (->Counter) [n]))
(defn add! [counter delta]
(gen/cast counter delta))
(defn value [counter]
(gen/call counter :read 3000))
;; a caller
(let [c (start-link parent 40)]
(add! c 2)
(value c)) ;; => 42
State in → tagged result out · the client functions do the cast and the call
(handle-event [_ _ _ event state data]
(case [state event]
[:saving :destination-ready]
(sr/=>KeepState data [(sr/=>Postpone)])
[:saving :source-saved]
(sr/=>NextState :joining data)
[:joining :destination-ready]
(sr/=>NextState :playing data)
(sr/=>KeepState data)))
;; client API over the state machine
(defn start-link [node]
(statem/start-link node (->Transfer) false []))
(defn destination-ready! [t]
(gen/cast t :destination-ready))
(defn source-saved! [t]
(gen/cast t :source-saved))
(defn state [t]
(gen/call t :read 3000))
;; a caller
(destination-ready! t) (state t) ;; => :saving
(source-saved! t) (state t) ;; => :playing
The early :destination-ready is postponed in :saving and replayed after :source-saved



Fred Hébert, “The Zen of Erlang,” ferd.ca, February 8, 2016 · repeatable = Bohrbugs, transient = Heisenbugs (Jim Gray, 1985)

Ports
void Awake()
{
_vm = SilkVm.Attach(
() => (long)(Time.fixedTime * 1000));
_out = _vm.OpenPort(SilkRT.Kw("t04", "out"));
_vm.Resolve(Sym.Ns("t04.app"),
Sym.Name("start!")).Invoke();
}
void Update()
{
_vm.StepFor(stepBudgetMs, stepChunkTurns);
foreach (var msg in _out.Drain())
HandleGameEffect(msg);
}

Modern Software Engineering



David Farley, Modern Software Engineering (2021) · Humble & Farley, Continuous Delivery (2010) · the MSE channel on YouTube




Peter Norvig, Paradigms of Artificial Intelligence Programming (1992) · github.com/norvig/paip-lisp





Markus Triska, The Power of Prolog · metalevel.at/prolog · youtube.com/@ThePowerOfProlog · Scryer Prolog · libscryer-clj

A (love) language




