IronKernel is a dialect of John N. Shutt’s Kernel for .NET 10.
Kernel is Scheme-like but more homoiconic: combiners and environments are first-class, and the core abstraction is the operative (vau) rather than macros.
This tree is a hybrid CLR runtime: programs are analyzed to a Core IR and compiled to CLR delegates while preserving runtime combiner dispatch, with a trampolined CPS interpreter for full Kernel semantics (vau, first-class eval/environments, call/cc, shift/reset).
dotnet build
dotnet testRequires the .NET 10 SDK.
Compiler, runtime, symbol lookup, and CLR resolution benchmarks use BenchmarkDotNet and must run in Release mode:
dotnet run --project IronKernel.Benchmarks -c Release -- --filter '*CompilerBenchmarks*'
dotnet run --project IronKernel.Benchmarks -c ReleaseUse --job Dry only to verify benchmark discovery and execution; its single
iteration is not a performance measurement. Benchmark reports are written to
BenchmarkDotNet.Artifacts/. Performance changes should include before/after
results from the same machine and runtime.
- CI (
.github/workflows/ci.yml) runsdotnet teston Ubuntu for pushes/PRs tomain/master. - Release (
.github/workflows/release.yml) triggers on tagsv*(e.g.v0.4.0): verifies the tag matches the rootversionfile, tests on Linux, then publishes self-contained single-file binaries forlinux-x64,win-x64,osx-arm64, andosx-x64, plus NuGet packages forIronKernel.ToolandIronKernel.Sdk. Binaries are attached asironkernel-<rid>.tar.gz(binary +kernel.ikr/promises.ikr). - Versioning: edit the root
versionfile (single source of truth viaDirectory.Build.props). Commit, then tag and pushv$(tr -d '[:space:]' < version). The release job fails if the tag and file disagree.ik --versionand the REPL banner read the assembly informational version produced from that file.
The promotional site and docs live in website/ and target ironkernel.org (Iron = It runs on .NET; the .net TLD was taken).
# local preview
python3 -m http.server -d website 8080GitHub Pages deploys from website/ via .github/workflows/pages.yml (enable Pages → “GitHub Actions” in repo settings, then point the domain’s DNS at GitHub).
Preferred: install the ik global tool from NuGet.org (requires the
.NET 10 SDK), then use the CLI directly.
Self-contained binaries are also on
GitHub Releases.
Full steps and VS Code runtime discovery are in the
getting-started guide.
dotnet tool install -g IronKernel.Tool
# Ensure ~/.dotnet/tools is on PATH
ik --version
ik new app helloPackages on NuGet.org:
IronKernel.Tool (ik) and
IronKernel.Sdk (MSBuild SDK for
.ikproj). If dotnet tool install reports a missing DotnetToolSettings.xml,
install or select a .NET 10 SDK — that error is the usual symptom of an older SDK
trying to install a net10.0 tool.
For contributors building this repository:
dotnet build
dotnet run --project IronKernel -- Examples/hello.ikrSee Examples/README.md for runnable programs.
.ikproj files are MSBuild-compatible IronKernel projects. Use the ik tool
(or the same subcommands on the IronKernel release binary):
ik new app hello
cd hello
ik run
ik test
ik restore
ik add Acme.IronKernel.Http 1.2.0
ik add Npgsql 9.0.0 --clr
ik tree
ik build
ik packProjects use standard NuGet PackageReference entries and commit
packages.lock.json. Restored IronKernel package sources under
ironkernel/src/**/*.ikr load before project source; declared CLR runtime
assemblies are loaded for interop.
Public packages use NuGet.org initially. See
docs/packages.md for package layout and
ADR 0001 for the
extension and ecosystem decision.
ik
# or: IronKernelLoads kernel.ikr and promises.ikr, then presents an interactive prompt. Type quit to exit.
ik path/to/program.ikr arg1 arg2
# Equivalent explicit form:
ik run path/to/program.ikr arg1 arg2Script mode loads kernel.ikr and promises.ikr in a fresh environment, then
binds command-line arguments to args. Evaluation and startup errors are written
to stderr and produce a non-zero exit code.
ik compile path/to/program.ikr -o program.ikc
ik run program.ikcCompilation parses and analyzes the source without executing it, then writes an
architecture-neutral IKC2 package containing versioned Core IR and typed
constants. At run time IronKernel loads the standard library, decodes the Core
IR, compiles it to delegates, and executes it without parsing or analyzing the
original source. Source locations and relevant lines remain as diagnostic
metadata. IKC1 source packages must be rebuilt. Omitting -o writes
<source-name>.ikc.
Use --help for all commands and --version for the runtime version.
ik compile path/to/program.ikr --managed -o publish
dotnet publish/program.dllManaged artifacts contain generated CLR entry points and typed constants rather
than the original Kernel source. They require .NET 10, but startup does not parse
the program or build expression-tree delegates. Published output references the
parser-free IronKernel.Runtime library and excludes the compiler and FParsec.
The static backend supports literals, variables, quoted values, statically named
combinations, top-level definitions, lazy conditionals, and sequencing. Generated
guards preserve primitive rebinding semantics, and embedded source spans retain
runtime diagnostics. kernel.ikr and promises.ikr are compiled into artifact
initialization functions, so standard-library features are available without
shipping or parsing those files at startup; unsupported forms fail compilation. See
ADR 0002 for the managed and NativeAOT
roadmap.
ik --profile minimal compile path/to/program.ikr --native osx-arm64 -o publish
./publish/programNative artifacts are RID-specific, self-contained executables produced with
.NET NativeAOT. They require neither the dotnet host nor Kernel source at run
time. The native backend supports the minimal and safe profiles and the same
Core IR subset as managed artifacts. Safe artifacts include capability-checked,
generated CLR bindings without enabling raw reflection. On macOS, publishing
requires the Xcode command line tools plus Homebrew openssl@3 and brotli;
IronKernel statically links those Homebrew libraries so the resulting executable
has no Homebrew runtime dependency.
Parse and runtime failures include the source path, line and column, offending
line, and a caret range. CLI modes write diagnostics to stderr and return a
non-zero exit code. Compiled source reports the narrowest retained span, such as
an unbound operator or the selected branch of a guarded if:
program.ikr:2:2: Getting an unbound variable: 'missing'
(missing 42)
^^^^^^^
Syntax constructed as a runtime LispVal and passed to eval has no original
source span. Errors from such code use the nearest enclosing source location
when one is available.
IronKernel can construct root environments with different host authority:
| Profile | Host access |
|---|---|
minimal |
Kernel evaluation and data primitives only |
safe |
Minimal profile plus reviewed generated CLR wrappers |
unrestricted |
Raw CLR reflection, source loading, and host I/O (default) |
ik --profile safe path/to/safe-clr.ikrSafe wrappers such as Console.write-line, String.concat, and Math.sqrt
are generated from manifests/safe-clr-bindings.json.
The generator resolves one exact public static signature and emits direct typed
calls—there is no runtime overload selection or reflection in the generated
path:
dotnet fsi tools/generate-clr-bindings.fsx \
manifests/safe-clr-bindings.json IronKernel/Generated/Bindings.Safe.fsChild environments intersect their parents' capability sets. Imported or stolen
interop values still check the authority of the environment where they are
invoked, so copying a binding cannot grant host access. See
docs/capabilities.md for the security model and limits.
Prompt tags are unforgeable runtime values. Tagged shift selects the nearest
matching reset, while prompt installs a deep effect handler:
(define request (make-prompt-tag))
(prompt request
(lambda (value k) (resume k (+ value 1)))
(+ 1 (perform request 40)))
; ⇒ 42perform supplies the operation value and a one-shot resumption to the handler.
Resuming reinstalls the same tagged handler; attempting to resume twice is an
error. Existing untagged (reset body) and (shift handler) remain multi-shot
and backward compatible.
The unrestricted profile also provides (task-delay milliseconds value) and
(await-task task). Task callbacks only publish an outcome; Eval.runAsync
resumes the trampoline serially rather than evaluating on a CLR callback thread.
See Examples/effects-async.ikr.
Contracts are optional combiner metadata. They distinguish raw operative operands from evaluated applicative arguments and validate fixed value shapes:
(define double (lambda (x) (+ x x)))
(contract double applicative (number) number pure #t)
(define raw (vau operands _ operands))
(contract raw operative (any) any pure #t)Supported shapes are any, number, integer, string, boolean, atom,
list, prompt-tag, and resumption. The final fields declare the effect
summary (pure or effectful) and whether the combiner is intended to be
inlineable.
User contracts are asserted metadata and are never executed by the compiler.
Reviewed primitive contracts are certified; pure literal calls such as
(+ 20 22) may be folded behind a binding-cell/version/structural contract snapshot
guard. Rebinding the operator selects the untouched generic combination before
any operand effects occur. Dynamic eval, control effects, CLR calls, and async
operations remain residual.
The extension in editors/vscode/ provides IronKernel syntax
highlighting, snippets, run/compile commands, Problems diagnostics, and a
playground backed by the real CLI. Build a local VSIX with:
cd editors/vscode
npm install
npm run packageThe playground requires a trusted workspace because IronKernel programs can
invoke .NET APIs. See the extension README for
runtime discovery and .ikr file-association guidance.
Under the unrestricted profile you can open namespaces and use short type names:
(clr-open System System.IO)
(clr-alias SB System.Text.StringBuilder)
(Guid/NewGuid) ; static method
(StringBuilder.) ; constructor
(.Append sb "hi") ; instance method
(.-Length sb) ; field / property
(clr-type Path) ; first-class System.Type valueFull names such as System.Console remain valid. Ambiguous short names after
clr-open raise an error; use clr-alias or a full name to disambiguate.
IronKernel keeps a LISP / Kernel S-expression surface (parentheses are intentional). Notable surface forms:
| Form | Meaning |
|---|---|
(…) |
Lists / combinations |
[…] |
Vectors |
(a & b) |
Improper / dotted lists (& instead of .) |
:keyword |
Keywords |
#t #f #inert |
Booleans and inert |
λ / ϝ |
Aliases for lambda / vau |
Source syntax may nest lists and vectors up to 256 levels. Deeper input is rejected with a located parse error before recursive parser processing begins; parentheses and brackets inside strings or comments do not count toward the limit.
| Module | Role |
|---|---|
Parser.fs |
FParsec → homoiconic LispVal |
Ir.fs / Analyze.fs |
Core IR + binding-aware guarded analysis |
Eval.fs |
Trampolined CPS interpreter (TCO-capable) |
Compiler.fs |
Guarded Expression-tree compiler with generic fallback |
Emit.fs |
IKC package emit / load |
Runtime.fs |
Primitive operatives & applicatives |
kernel.ikr |
Stdlib (lambda, let, modules, …) |
Compiler fast paths are guarded by stable binding-cell identity and version. Rebinding or shadowing a primitive invalidates its guard before any specialized work begins, so execution falls back to ordinary Kernel combiner dispatch.
Apache 2.0 — see COPYING.