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).
docs/kernel-conformance.md tracks IronKernel against
the Revised-1 Report on the Kernel Programming Language
(R-1RK), feature by feature. Of the report's 135 feature entries, 135 are verified
by a behavioural check and 0 are absent; nothing is merely bound-but-unchecked.
34 entries belong to modules the report marks optional. The matrix also reports
status per module, which R-1RK 1.3.2 makes the unit of conformance: 44 of 44
modules are complete.
That is not the same as claiming conformance. §1.3.2 makes the module the unit, and support additionally requires the modules a module assumes and the report's baseline representation requirements — and a passing check exercises an entry rather than proving every requirement in it. The divergences below the summary are where the difference lives, and they are the part worth reading.
Every entry of chapter 12 now has a passing check — Numbers (12.5), Inexact
(12.6), Narrow inexact (12.7), Rational (12.8), Real (12.9) and Complex (12.10).
Exact arithmetic meets what R-1RK 12.3.2 requires: exact integers are of arbitrary
size and promote rather than wrapping, dividing them gives an exact ratio, so
(/ 1 3) is a third and three of them are exactly one, and both exact real
infinities exist. Ratios are kept in least terms and read back as 1/3; (max)
is #e-infinity and (gcd) is #e+infinity, as the report specifies. Inexact
reals take the representation 12.2 sanctions directly — non-robust, bounded by the
infinities — and with-strict-arithmetic decides whether a result with no primary
value signals or is returned.
Pairs are mutable cons cells, so module Pair mutation (4.7, 5.8, 6.4) is
complete: set-car!, set-cdr!, encycle! and append! all work, eq? is
object identity, and the traversals that a cycle would otherwise trap — equal?,
the reader's list patterns, get-list-metrics, length — measure a cycle instead
of walking into one. (length p) of a cyclic list is #e+infinity. Mutability
follows provenance: the reader produces immutable pairs, cons and list
mutable ones, so a captured algorithm cannot be rewritten under the combiner that
captured it. See ADR 0005.
Chapter 15's ports are complete too, in all three of the lifetimes its preamble
describes: implicit and closed on return (with-output-to-file), an explicit
reference closed on return (call-with-output-file), and open/close left entirely
to the programmer. get-module evaluates a file into a fresh standard environment
and hands it back, so a module's definitions reach its caller only through that
environment.
Chapter 7's continuations are complete, including the entry/exit guards: an
abnormal pass selects interceptors by walking the source and destination
continuation chains, and dynamic-wind is derivable from them exactly as the
report's own rationale writes it.
Signalling an error is one of those passes, as R-1RK 7.2.7 requires, rather than a
value on a separate channel. The continuation hierarchy is therefore the exception
mechanism: an exit guard selecting on error-continuation fires when the guarded
extent signals, so dynamic-wind cleans up after a failure, a port closes when its
body signals, and the report's derivation of $binds? from an error exit-guard
(6.7.1) works. ADR 0006 records how it
was migrated.
The matrix is generated by probing a real bootstrapped environment, and dotnet test
fails if the committed copy is stale, so it cannot drift from the implementation. The
feature list comes from the report's own table of contents
(docs/kernel-r-1rk-features.json, extracted by
tools/extract-r-1rk-features.py), so features
IronKernel has never implemented still appear.
Every supported feature now resolves under the report's own name. IronKernel also
binds shorter spellings — if for $if, define for $define! — as an extension
in the sense of R-1RK 1.3.2; both spellings denote the same combiner. The one
remaining divergence, external representations (R-1RK 3.6), is listed in the matrix.
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.
lib/ holds first-party IronKernel packages, each an ordinary
.ikproj built with ik test / ik pack.
| Package | Provides |
|---|---|
IronKernel.Amb |
Nondeterministic search: amb, require, bracketed choice, and pluggable search strategies over multi-shot delimited continuations |
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.Runtime/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, …) |
Eval.fs, Runtime.fs, and the other shared core modules live in the
IronKernel.Runtime project; Parser.fs, Ir.fs, Analyze.fs, Compiler.fs,
and Emit.fs live in the IronKernel (ik) project, which references
IronKernel.Runtime.
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.