Skip to content

Repository files navigation

IronKernel

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).

Kernel conformance

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.

Build & test

dotnet build
dotnet test

Requires the .NET 10 SDK.

Benchmarks

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 Release

Use --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 & releases

  • CI (.github/workflows/ci.yml) runs dotnet test on Ubuntu for pushes/PRs to main/master.
  • Release (.github/workflows/release.yml) triggers on tags v* (e.g. v0.4.0): verifies the tag matches the root version file, tests on Linux, then publishes self-contained single-file binaries for linux-x64, win-x64, osx-arm64, and osx-x64, plus NuGet packages for IronKernel.Tool and IronKernel.Sdk. Binaries are attached as ironkernel-<rid>.tar.gz (binary + kernel.ikr / promises.ikr).
  • Versioning: edit the root version file (single source of truth via Directory.Build.props). Commit, then tag and push v$(tr -d '[:space:]' < version). The release job fails if the tag and file disagree. ik --version and the REPL banner read the assembly informational version produced from that file.

Website

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 8080

GitHub Pages deploys from website/ via .github/workflows/pages.yml (enable Pages → “GitHub Actions” in repo settings, then point the domain’s DNS at GitHub).

Getting started

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 hello

Packages 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.ikr

See Examples/README.md for runnable programs.

Projects and packages

.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 pack

Projects 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.

Libraries in this repository

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

REPL

ik
# or: IronKernel

Loads kernel.ikr and promises.ikr, then presents an interactive prompt. Type quit to exit.

Run a script

ik path/to/program.ikr arg1 arg2
# Equivalent explicit form:
ik run path/to/program.ikr arg1 arg2

Script 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.

Compile to an IKC package

ik compile path/to/program.ikr -o program.ikc
ik run program.ikc

Compilation 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.

Compile to a managed artifact

ik compile path/to/program.ikr --managed -o publish
dotnet publish/program.dll

Managed 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.

Compile to a native artifact

ik --profile minimal compile path/to/program.ikr --native osx-arm64 -o publish
./publish/program

Native 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.

Diagnostics

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.

Capability profiles and generated CLR bindings

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.ikr

Safe 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.fs

Child 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.

Tagged handlers and async

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)))
; ⇒ 42

perform 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.

Operative contracts and partial evaluation

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.

See Examples/contracts.ikr.

VS Code extension and playground

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 package

The playground requires a trusted workspace because IronKernel programs can invoke .NET APIs. See the extension README for runtime discovery and .ikr file-association guidance.

CLR namespaces and Clojure-style calls

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 value

Full 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.

Syntax

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.

Architecture

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.

License

Apache 2.0 — see COPYING.

About

IronKernel is a dialect of the Kernel programming language for the .NET framework.

Topics

Resources

Stars

14 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages