Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: e2e

on:
push:
branches: [main]
pull_request:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
flake-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22
- run: nix flake check -L
37 changes: 37 additions & 0 deletions .github/workflows/ruff.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Ruff

on:
pull_request:
paths:
- '**/*.py'
- 'pyproject.toml'
- '.github/workflows/ruff.yml'
push:
branches:
- main
paths:
- '**/*.py'
- 'pyproject.toml'
- '.github/workflows/ruff.yml'
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
ruff:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
python-version: "3.12"
- name: Run ruff check
run: uv run ruff check
- name: Run ruff format --check
run: uv run ruff format --check
33 changes: 33 additions & 0 deletions .github/workflows/spdx.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: SPDX Check

on:
pull_request:
paths:
- '**/*.py'
- 'pyproject.toml'
- '.github/workflows/spdx.yml'
push:
branches:
- main
paths:
- '**/*.py'
- 'pyproject.toml'
- '.github/workflows/spdx.yml'
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
check-spdx-headers:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: enarx/spdx@d4020ee98e3101dd487c5184f27c6a6fb4f88709 # master
with:
licenses: MIT
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
# Transpiler output
manifest.json
node__*.nix

# Exceptions
!examples/*/manifest.json
!examples/*/node__*.nix

# Nix build results
result
result-*
eif1
eif2
nodes

# System
.DS_Store

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
Expand Down
144 changes: 143 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,144 @@
# NixReflect
A transpiler for mutually-referential reflective programming in Nix

![SemVer](https://img.shields.io/badge/NixReflect-0.1.0-white)
![Python Version](https://img.shields.io/badge/Python-3.12-blue)
![Nix](https://img.shields.io/badge/Nix-flakes-5277C3)
[![License](https://img.shields.io/badge/License-MIT-red)](/LICENSE)

A transpiler for mutually-referential reflective programming in **Nix** — the Nix-targeting sibling of [PyReflect](https://github.com/acompany-develop/PyReflect).
The transpiler is implemented in Python and is available as a library (`nixreflect`) and a command-line tool (`nixreflect`); its *output* is a set of mutually-referential standalone `.nix` files.

## Overview

NixReflect is a transpiler that makes *mutually-referential* programs — programs made of several nodes — possible.
Every node can reference the source code of itself and of the other nodes, without relying on any external source (e.g. file, stdin, registry).

Naïvely embedding each node's code inside the others has no solution — it is a fixed-point problem with an infinite regress.
**Kleene's second recursion theorem** dissolves it: instead of the code itself, each node embeds a *code generator* together with its input data, from which the exact code of every node (itself and its peers) can be reconstructed intrinsically.
For the full background, see [PyReflect's README](https://github.com/acompany-develop/PyReflect#readme) — NixReflect is the same construction with Nix as the target language.

Because the generated nodes are Nix expressions, and Nix builds are deterministic functions of their sources, the reflection extends beyond source code: a node can intrinsically derive any *reproducible build artifact* of its peers — hashes, container layers, or (see below) AWS Nitro Enclave images and their measurements.

## Install

```bash
uv pip install git+https://github.com/acompany-develop/NixReflect
```

This exposes the `nixreflect` command and the importable `nixreflect` package.
Alternatively, with Nix:

```bash
nix run .#nixreflect -- TEMPLATE.json OUTPUT_DIR
```

## Usage

### Library API

```py
from nixreflect import parse_template, transpile

with open(path, encoding="utf-8") as f:
# Read
text = f.read()
# Parse
template = parse_template(text)
# Transpile
nodes = transpile(template)
```

### Transpiler CLI

```bash
# Input from file
nixreflect TEMPLATE.json OUTPUT_DIR

# Input from stdin
cat TEMPLATE.json | nixreflect - OUTPUT_DIR
```

It writes one file per node (`node_<id>.nix`) with a `manifest.json` mapping node-id to filename.

## Transpiler details

The template format, the transformation, and the framework API are the same as [PyReflect's](https://github.com/acompany-develop/PyReflect#readme), with `__nixreflect_*__` in place of `__pyreflect_*__` — see there for the full specification.
The Nix-specific differences:

- A `code` body must be a single **Nix expression**; it becomes the emitted file's result expression.
Node-id tokens are rewritten into the function application `(__nixreflect_render__ "<target-node-id>")`.
- Where PyReflect embeds the template as a Base64 blob decoded at runtime, NixReflect embeds the JSON text directly as a string literal (`__nixreflect_DATA__`) — pure Nix has no Base64 decoder, but `builtins.fromJSON` is built in.
The framework's `__nixreflect_nix_str__` mirrors the transpiler's string-literal escaping exactly, which is what makes the reconstruction byte-exact.

## Example code

Generated `.nix` files are evaluated with `nix eval` (any recent Nix with `nix-command` enabled; `nix-instantiate --eval --strict` works too).

### quine

Single node that evaluates to its own code.

```bash
# Transpile
nixreflect examples/quine/template.json examples/quine/

# Run Node: Display its own code
nix eval --raw -f examples/quine/node___NODE.nix

# Verify
nix eval --raw -f examples/quine/node___NODE.nix | diff - examples/quine/node___NODE.nix
```

### mutual_quine

Two nodes, each of which evaluates to the other's *SHA-256 digest*.
The peer value is obtained intrinsically — node 1 reconstructs node 2's source from its own embedded data and hashes it, and vice versa — so node 1's self hash equals the value node 2 reports as its expected peer reference, and vice versa.

```bash
# Transpile
nixreflect examples/mutual_quine/template.json examples/mutual_quine/

# Run Node 1: Display the SHA-256 digest of Node 2
nix eval --raw -f examples/mutual_quine/node___NODE1.nix; echo
# Run Node 2: Display the SHA-256 digest of Node 1
nix eval --raw -f examples/mutual_quine/node___NODE2.nix; echo

# Verify
sha256sum examples/mutual_quine/*.nix
```

### trinity_quine

A variant of `mutual_quine` with three nodes wired into a cycle: node 1 yields node 2's digest, node 2 yields node 3's, node 3 yields node 1's (1 → 2 → 3 → 1).
It demonstrates that the transpiler handles arbitrary n-node reference graphs, not just the symmetric two-node case.

```bash
# Transpile
nixreflect examples/trinity_quine/template.json examples/trinity_quine/

# Run Nodes
nix eval --raw -f examples/trinity_quine/node___NODE1.nix; echo
nix eval --raw -f examples/trinity_quine/node___NODE2.nix; echo
nix eval --raw -f examples/trinity_quine/node___NODE3.nix; echo

# Verify
sha256sum examples/trinity_quine/*.nix
```

### mutual_quine_ne — mutual quine as AWS Nitro Enclaves

Two **Nitro Enclave images** (EIFs), built reproducibly with [monzo/aws-nitro-util](https://github.com/monzo/aws-nitro-util), each of which *rebuilds the other enclave's image inside itself at runtime* and prints the peer's reference **PCR values** — the measurements the Nitro hypervisor attests — derived intrinsically, with no network and no shared state.

Unlike the examples above, running this one requires an **EC2 instance with AWS Nitro Enclaves enabled** and ample resources — it builds the EIFs on the host and each enclave rebuilds its peer's EIF in RAM (the tested setups are `m6a.xlarge` for x86\_64 and `m6g.xlarge` for AArch64: 4 vCPUs / 16 GiB, of which 2 vCPUs / 8 GiB go to the enclave).
See the full tutorial in [`examples/mutual_quine_ne/`](examples/mutual_quine_ne/README.md) for the instance prerequisites, host setup, and a step-by-step walk-through.

## Tests

All examples are covered end to end by flake checks ([`examples/checks.nix`](examples/checks.nix)), run in CI on every push:

```bash
nix flake check -L
```

For each example this transpiles the template from scratch, asserts the output is byte-identical to the committed node files, evaluates the emitted nodes and asserts their quine property (self-reproduction / peer digests).
For `mutual_quine_ne` the `mutual-quine-ne-verify-*` checks additionally re-run the enclave entrypoint against each image's rootfs and demand that the EIF it reconstructs for its peer matches the peer's actual build — the mutual-quine property is proven without Nitro hardware.
69 changes: 69 additions & 0 deletions examples/checks.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# E2E checks for the transpiler examples. For each template:
# 1. transpile it with the in-repo transpiler,
# 2. assert the output is byte-identical to the committed node files
# (drift detection),
# 3. evaluate the emitted nodes with nix-instantiate and assert the
# quine property they claim (self-reproduction / peer digests).
# The mutual_quine_ne example has its own, much stronger checks in
# ./mutual_quine_ne/default.nix (EIF reconstruction).
{ pkgs }:
let
# >= 3.12 for PEP 695; keep in sync with mutual_quine_ne/default.nix
transpiled = name: pkgs.runCommand "nixreflect-${name}-transpiled"
{ nativeBuildInputs = [ pkgs.python312 ]; } ''
export PYTHONPATH=${../src}
mkdir -p $out
python3 -m nixreflect ${./. + "/${name}/template.json"} $out
'';

# nix-instantiate performs pure evaluation only, but still wants writable
# state and cache locations (same trick as mutual_quine_ne/run.sh)
check = name: script: pkgs.runCommand "nixreflect-e2e-${name}"
{ nativeBuildInputs = [ pkgs.nix pkgs.jq ]; } ''
export HOME="$TMPDIR/home" XDG_CACHE_HOME="$TMPDIR/cache" \
NIX_STATE_DIR="$TMPDIR/nix/state" NIX_LOG_DIR="$TMPDIR/nix/log" \
NIX_CONF_DIR="$TMPDIR/nix/conf"
mkdir -p "$HOME" "$XDG_CACHE_HOME" "$TMPDIR/nix/state" \
"$TMPDIR/nix/log" "$TMPDIR/nix/conf"
evalRaw() { nix-instantiate --eval --strict --json "$1" | jq -j .; }
digest() { sha256sum "$1" | cut -d" " -f1; }
${script}
touch $out
'';

driftCheck = name: pkgs.runCommand "nixreflect-drift-${name}" { } ''
cd ${transpiled name}
for f in *; do
echo "comparing $f against the committed copy"
diff "$f" ${./. + "/${name}"}/"$f"
done
touch $out
'';
in
{
# the node evaluates to its own exact source
example-quine = check "quine" ''
d=${transpiled "quine"}
evalRaw "$d/node___NODE.nix" | cmp - "$d/node___NODE.nix"
'';

# each node evaluates to the SHA-256 digest of its peer's source
example-mutual-quine = check "mutual-quine" ''
d=${transpiled "mutual_quine"}
[ "$(evalRaw $d/node___NODE1.nix)" = "$(digest $d/node___NODE2.nix)" ]
[ "$(evalRaw $d/node___NODE2.nix)" = "$(digest $d/node___NODE1.nix)" ]
'';

# three nodes in a cycle: 1 -> 2 -> 3 -> 1
example-trinity-quine = check "trinity-quine" ''
d=${transpiled "trinity_quine"}
[ "$(evalRaw $d/node___NODE1.nix)" = "$(digest $d/node___NODE2.nix)" ]
[ "$(evalRaw $d/node___NODE2.nix)" = "$(digest $d/node___NODE3.nix)" ]
[ "$(evalRaw $d/node___NODE3.nix)" = "$(digest $d/node___NODE1.nix)" ]
'';

# the committed example outputs are exactly what the transpiler emits today
transpile-drift-quine = driftCheck "quine";
transpile-drift-mutual-quine = driftCheck "mutual_quine";
transpile-drift-trinity-quine = driftCheck "trinity_quine";
}
4 changes: 4 additions & 0 deletions examples/mutual_quine/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"__NODE1": "node___NODE1.nix",
"__NODE2": "node___NODE2.nix"
}
28 changes: 28 additions & 0 deletions examples/mutual_quine/node___NODE1.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# auto-generated mutually-referential attestation node -- do not edit
let
__nixreflect_SELF__ = "__NODE1";
__nixreflect_DATA__ = "{\"bodies\":{\"__NODE1\":\"# ========== BODY ========== \\nbuiltins.hashString \\\"sha256\\\" (__nixreflect_render__ \\\"__NODE2\\\")\\n\",\"__NODE2\":\"# ========== BODY ========== \\nbuiltins.hashString \\\"sha256\\\" (__nixreflect_render__ \\\"__NODE1\\\")\\n\"},\"framework\":\" # ========== NixReflect FRAMEWORK ==========\\n\\n __nixreflect_blob__ = builtins.fromJSON __nixreflect_DATA__;\\n\\n # Nix double-quoted string literal for `s`; mirrors nixreflect.transpiler.nix_str.\\n __nixreflect_nix_str__ = s: \\\"\\\\\\\"\\\" + builtins.replaceStrings\\n [ \\\"\\\\\\\\\\\" \\\"\\\\\\\"\\\" \\\"\\\\\${\\\" ] [ \\\"\\\\\\\\\\\\\\\\\\\" \\\"\\\\\\\\\\\\\\\"\\\" \\\"\\\\\\\\\\\\\${\\\" ] s + \\\"\\\\\\\"\\\";\\n\\n # Exact source code (string) of node `target`, reconstructed from embedded data.\\n __nixreflect_render__ = target:\\n __nixreflect_blob__.header\\n + \\\" __nixreflect_SELF__ = \\\" + __nixreflect_nix_str__ target + \\\";\\\\n\\\"\\n + \\\" __nixreflect_DATA__ = \\\" + __nixreflect_nix_str__ __nixreflect_DATA__ + \\\";\\\\n\\\"\\n + \\\"\\\\n\\\"\\n + __nixreflect_blob__.framework + \\\"\\\\n\\\"\\n + __nixreflect_blob__.bodies.\${target};\\n\\n __nixreflect_node_ids__ = __nixreflect_blob__.nodes;\\n\\n __nixreflect_self_id__ = __nixreflect_SELF__;\\nin\",\"header\":\"# auto-generated mutually-referential attestation node -- do not edit\\nlet\\n\",\"nodes\":[\"__NODE1\",\"__NODE2\"]}";

# ========== NixReflect FRAMEWORK ==========

__nixreflect_blob__ = builtins.fromJSON __nixreflect_DATA__;

# Nix double-quoted string literal for `s`; mirrors nixreflect.transpiler.nix_str.
__nixreflect_nix_str__ = s: "\"" + builtins.replaceStrings
[ "\\" "\"" "\${" ] [ "\\\\" "\\\"" "\\\${" ] s + "\"";

# Exact source code (string) of node `target`, reconstructed from embedded data.
__nixreflect_render__ = target:
__nixreflect_blob__.header
+ " __nixreflect_SELF__ = " + __nixreflect_nix_str__ target + ";\n"
+ " __nixreflect_DATA__ = " + __nixreflect_nix_str__ __nixreflect_DATA__ + ";\n"
+ "\n"
+ __nixreflect_blob__.framework + "\n"
+ __nixreflect_blob__.bodies.${target};

__nixreflect_node_ids__ = __nixreflect_blob__.nodes;

__nixreflect_self_id__ = __nixreflect_SELF__;
in
# ========== BODY ==========
builtins.hashString "sha256" (__nixreflect_render__ "__NODE2")
28 changes: 28 additions & 0 deletions examples/mutual_quine/node___NODE2.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# auto-generated mutually-referential attestation node -- do not edit
let
__nixreflect_SELF__ = "__NODE2";
__nixreflect_DATA__ = "{\"bodies\":{\"__NODE1\":\"# ========== BODY ========== \\nbuiltins.hashString \\\"sha256\\\" (__nixreflect_render__ \\\"__NODE2\\\")\\n\",\"__NODE2\":\"# ========== BODY ========== \\nbuiltins.hashString \\\"sha256\\\" (__nixreflect_render__ \\\"__NODE1\\\")\\n\"},\"framework\":\" # ========== NixReflect FRAMEWORK ==========\\n\\n __nixreflect_blob__ = builtins.fromJSON __nixreflect_DATA__;\\n\\n # Nix double-quoted string literal for `s`; mirrors nixreflect.transpiler.nix_str.\\n __nixreflect_nix_str__ = s: \\\"\\\\\\\"\\\" + builtins.replaceStrings\\n [ \\\"\\\\\\\\\\\" \\\"\\\\\\\"\\\" \\\"\\\\\${\\\" ] [ \\\"\\\\\\\\\\\\\\\\\\\" \\\"\\\\\\\\\\\\\\\"\\\" \\\"\\\\\\\\\\\\\${\\\" ] s + \\\"\\\\\\\"\\\";\\n\\n # Exact source code (string) of node `target`, reconstructed from embedded data.\\n __nixreflect_render__ = target:\\n __nixreflect_blob__.header\\n + \\\" __nixreflect_SELF__ = \\\" + __nixreflect_nix_str__ target + \\\";\\\\n\\\"\\n + \\\" __nixreflect_DATA__ = \\\" + __nixreflect_nix_str__ __nixreflect_DATA__ + \\\";\\\\n\\\"\\n + \\\"\\\\n\\\"\\n + __nixreflect_blob__.framework + \\\"\\\\n\\\"\\n + __nixreflect_blob__.bodies.\${target};\\n\\n __nixreflect_node_ids__ = __nixreflect_blob__.nodes;\\n\\n __nixreflect_self_id__ = __nixreflect_SELF__;\\nin\",\"header\":\"# auto-generated mutually-referential attestation node -- do not edit\\nlet\\n\",\"nodes\":[\"__NODE1\",\"__NODE2\"]}";

# ========== NixReflect FRAMEWORK ==========

__nixreflect_blob__ = builtins.fromJSON __nixreflect_DATA__;

# Nix double-quoted string literal for `s`; mirrors nixreflect.transpiler.nix_str.
__nixreflect_nix_str__ = s: "\"" + builtins.replaceStrings
[ "\\" "\"" "\${" ] [ "\\\\" "\\\"" "\\\${" ] s + "\"";

# Exact source code (string) of node `target`, reconstructed from embedded data.
__nixreflect_render__ = target:
__nixreflect_blob__.header
+ " __nixreflect_SELF__ = " + __nixreflect_nix_str__ target + ";\n"
+ " __nixreflect_DATA__ = " + __nixreflect_nix_str__ __nixreflect_DATA__ + ";\n"
+ "\n"
+ __nixreflect_blob__.framework + "\n"
+ __nixreflect_blob__.bodies.${target};

__nixreflect_node_ids__ = __nixreflect_blob__.nodes;

__nixreflect_self_id__ = __nixreflect_SELF__;
in
# ========== BODY ==========
builtins.hashString "sha256" (__nixreflect_render__ "__NODE1")
Loading