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
33 changes: 33 additions & 0 deletions .github/workflows/ruff.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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:

permissions:
contents: read

jobs:
ruff:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: enarx/spdx@d4020ee98e3101dd487c5184f27c6a6fb4f88709 # master
with:
licenses: MIT
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# Transpiler output
manifest.json
node__*.py

# System
.DS_Store

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
Expand Down
136 changes: 135 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,135 @@
# PyReflect
# PyReflect

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

A transpiler for mutually-referential reflective programming in Python. It is available as a library (`pyreflect`) and a command-line tool (`pyreflect`).

## Overview

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

Suppose node A and node B each want to reference the other's code. The naïve approach is to embed A's code inside B and B's code inside A. But then A's code has changed, so the copy of A embedded in B must be updated; that changes B's code, so the copy of B embedded in A must be updated; that changes A's code again, *ad infinitum*. In general this "fixed point" problem has no solution: for A to contain B we need $\mathsf{len}\left(A\right) > \mathsf{len}\left(B\right)$, and for B to contain A we need $\mathsf{len}\left(B\right) > \mathsf{len}\left(A\right)$ — a contradiction.

This infinite regress can be resolved by **Kleene's second recursion theorem**. Instead of embedding the code, each node embeds a *code generator* together with its input data. The generator can reconstruct the exact code of every node (itself and its peers) from the embedded input data — derived intrinsically, never fetched from any external source.

## Install

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

This exposes the `pyreflect` command and the importable `pyreflect` package.

## Usage

### Library API

```py
from pyreflect 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
pyreflect TEMPLATE.json OUTPUT_DIR

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

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

## Transpiler details

The `pyreflect` transpiler converts a template into N standalone Python programs. A template is a JSON list of node objects:

```json
[ {"node-id": "__NODE1", "code": "<python source>"},
{"node-id": "__NODE2", "code": "<python source>"},
... ]
```

Each node has a `node-id` and a `code` body that may reference any subset of the node-ids (itself included).

The transpiler does two things:

1. rewrites every node-id token in a code body into `__pyreflect_render__("<target-node-id>")`;
2. wraps each body with an identical framework, including the definition of the `__pyreflect_render__` function.

The `__pyreflect_render__` function reconstructs the exact source code of the specified node from data embedded in the emitted source code without any external source of information.

The framework exposes the following API to each body:

```plaintext
__pyreflect_SELF__ this node's id
__pyreflect_DATA__ Base64-encoded template
__pyreflect_self_id__() this node's id
__pyreflect_node_ids__() all node ids, in template order
__pyreflect_render__(target) exact source code (string) of the target node
```

A body must not redefine the reserved names of the form `__pyreflect_*__`.

## Example code

### quine

Single node that prints its own code.

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

# Run Node: Display its own code
uv run examples/quine/node___NODE.py

# Verify
cat examples/quine/node___NODE.py
```

### mutual_quine

Two nodes, each of which prints 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
pyreflect examples/mutual_quine/template.json examples/mutual_quine/

# Run Node 1: Display the SHA-256 digest of Node 2
uv run examples/mutual_quine/node___NODE1.py
# Run Node 2: Display the SHA-256 digest of Node 1
uv run examples/mutual_quine/node___NODE2.py

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

### trinity_quine

A variant of `mutual_quine` with three nodes wired into a cycle: node 1 prints node 2's digest, node 2 prints node 3's, node 3 prints 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
pyreflect examples/trinity_quine/template.json examples/trinity_quine/

# Run Node 1: Display the SHA-256 digest of Node 2
uv run examples/trinity_quine/node___NODE1.py
# Run Node 2: Display the SHA-256 digest of Node 3
uv run examples/trinity_quine/node___NODE2.py
# Run Node 3: Display the SHA-256 digest of Node 1
uv run examples/trinity_quine/node___NODE3.py

# Verify
sha256sum examples/trinity_quine/*.py
```
10 changes: 10 additions & 0 deletions examples/mutual_quine/template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
"node-id": "__NODE1",
"code": "# ========== BODY ========== \nimport hashlib\nprint(hashlib.sha256(__NODE2.encode('utf-8')).hexdigest())"
},
{
"node-id": "__NODE2",
"code": "# ========== BODY ========== \nimport hashlib\nprint(hashlib.sha256(__NODE1.encode('utf-8')).hexdigest())"
}
]
6 changes: 6 additions & 0 deletions examples/quine/template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
{
"node-id": "__NODE",
"code": "# ========== BODY ========== \nprint(__NODE)"
}
]
14 changes: 14 additions & 0 deletions examples/trinity_quine/template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"node-id": "__NODE1",
"code": "# ========== BODY ========== \nimport hashlib\nprint(hashlib.sha256(__NODE2.encode('utf-8')).hexdigest())"
},
{
"node-id": "__NODE2",
"code": "# ========== BODY ========== \nimport hashlib\nprint(hashlib.sha256(__NODE3.encode('utf-8')).hexdigest())"
},
{
"node-id": "__NODE3",
"code": "# ========== BODY ========== \nimport hashlib\nprint(hashlib.sha256(__NODE1.encode('utf-8')).hexdigest())"
}
]
31 changes: 31 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[project]
name = "PyReflect"
version = "0.1.0"
readme = "README.md"
requires-python = ">= 3.12"
license = "MIT"
dependencies = []

[project.scripts]
pyreflect = "pyreflect.cli:main"

[build-system]
requires = ["hatchling>=1.30"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/pyreflect"]

[tool.ruff]
target-version = "py312"

[dependency-groups]
dev = ["ruff>=0.15.4"]

[tool.ruff.lint]
select = ["B", "E", "F", "I", "PL", "SIM", "TC", "W"]
ignore = []

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
51 changes: 51 additions & 0 deletions src/pyreflect/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# SPDX-License-Identifier: MIT

"""PyReflect -- a general mutual-reference transpiler.

Turns a template describing N nodes into N standalone Python programs, each able
to reconstruct the exact source of every node (itself included) from data
embedded in itself.

Public API::

from pyreflect import load_template, parse_template, transpile

template = load_template("template.json")
nodes = transpile(template) # {node_id: source_code}
"""

__version__ = "0.1.0"

from .template import (
Node,
Template,
node_ids,
parse_template,
validate_template,
)
from .transpiler import (
FRAMEWORK,
HEADER,
build_blob,
build_bodies,
render_node,
rewrite_placeholders,
transpile,
)

__all__ = [
"__version__",
"Node",
"Template",
"load_template",
"parse_template",
"validate_template",
"node_ids",
"transpile",
"build_blob",
"build_bodies",
"rewrite_placeholders",
"render_node",
"FRAMEWORK",
"HEADER",
]
Loading