Write the rules.
Explore every outcome.

Photonic is a language of rules. A program is data and the rules that change it. The runtime explores every configuration those rules can reach, and Prism checks results exactly.

Light
[Light] Red
[Light] Green
[Light] Blue
Three rules can consume Light. Each is a possible event, so the runtime explores three futures. Select a configuration to see the events that leave it.
Every example runs

Each program in this book was executed by the Rust runtime compiled to WebAssembly, and each graph is its real output. At photonic.vantle.org, or in a checkout served with the command below, you can edit any example and run it again.

bazel run -c opt //toolchain/browser:serve

A served checkout opens at 127.0.0.1:8080. Opened as a local file, the book shows the recorded runs.

Part one

The language

Language · 1

Atoms, particles and coherences

Photonic has one kind of data: occurrences of atoms, grouped into particles, living in coherences.

An atom is any run of characters other than ( ) [ ] . , and whitespace. True, 0, ->, $x and 人 are atoms. Atoms mean nothing by themselves; rules give them meaning.

A dot joins occurrences into a particle. A particle is unordered and counts repeats: A.B and B.A are the same particle, and A.A holds two occurrences of A.

A comma separates coherences, the independent places where particles live. A.B, C is two coherences. () is one empty coherence; an empty program has none.

A configuration is everything that exists at one moment: its coherences and the rules that are live.

Next: Rules consume and produce

Language · 2

Rules consume and produce

A rule names what it consumes in brackets, then what it produces.

A.X
[A] B
[A] B matched the A inside A.X, consumed it and produced B. It never mentioned X, so X stayed. X is the remainder.

Matching is open: a pattern needs only its own occurrences, and the rest of the coherence passes to the output.

To consume the whole particle, name all of it:

A.X
[A.X] B

An event consumes one match. Two occurrences of A take two events:

A.A
[A] B

Several inputs, several outputs

Commas inside the brackets select several coherences; commas after them produce several. Every output receives the remainder of every consumed coherence.

A.X, B.Y
[A, B] (C, D)
[C, D] E
The first rule joins A.X and B.Y and splits into C and D; both carry X.Y. The second rule joins them again. The dashed arrow is an inference; chapter 6 explains it.

Empty inputs and outputs

RuleConsumesProduces
[A] Ban AB with the remainder
[A]an Anothing: the coherence is gone
[A] ()an Aone coherence holding only the remainder
[] Anothinga new coherence A, every time it fires
[()] Aany one coherencethat coherence with A added
[,] Aany two coherencesone coherence: both remainders and A
A.X, B.Y
[A],
[B] ()
[A] removes A.X entirely; [B] () leaves Y. The comma after [A] ends that rule. Without it, [A] [B] () is one rule whose output is the rule [B] ().

The lens shows how each form reads. Compare [A], [B] C with [A] [B] C:

Next: Occurrences have identity

Language · 3

Occurrences have identity

Two occurrences can carry the same atom and still be different occurrences. Photonic tracks which is which.

When one event produces several outputs, they all receive the same remainder: the same occurrence appears in each. When those coherences join again, a shared occurrence counts once.

Seed.X
[Seed] A((), ())
[A, A] ()
A((), ()) is shorthand for two outputs, each A. They share one X, so joining them gives back one X.

Occurrences written separately are independent, and they add up:

A(X, X)
[A, A] ()
Two coherences, A.X and A.X, with two independent Xs. Joining keeps both: X.X.

Adding up is arithmetic. A natural number can be a coherence of independent Units, and addition is a join:

Add.Unit.Unit,
Add.Unit.Unit.Unit
[Add, Add] ()
2 + 3 = 5. The rule consumes both Add labels and joins the remainders, and the five independent units survive.

Every explicit output is a new occurrence. Two branches that each produce Y produce two Ys, even from one ancestor.

Identities are anonymous. Configurations that differ only in how occurrences are named are the same configuration, and the runtime stores it once.

Next: Every future

Language · 4

Every future

When several events are possible, the runtime explores all of them. The result is a graph: configurations are nodes, events are arrows.

An event is one rule applied to one match. Different rules, and different matches of one rule, give different events.

A
[A] B
[A] C
[B, C] D
B and C are alternatives: each future holds one of them, never both, so [B, C] D never applies. Prism proves D unreachable.
A
[A] B, C
[B, C] D
Here one event produces B and C in the same configuration, so they can join.

Only occurrences that coexist in one configuration can be matched together.

Cycles and endless programs

A
[A] B
[B] A
Returning to a configuration closes a cycle. The graph stays finite.
[] A
[] A adds a coherence every time it fires, so the graph never ends. Exploration stops at its budget and says so.

Exploration runs within budgets on configurations, coherences, occurrences, scopes, records and work. A search stopped by a budget can resume with a larger one. Until it finishes, whatever it has not found is unknown.

Next: Scopes

Language · 5

Scopes

An output group that contains rules opens a scope: a local place with its own rules.

Brew.Tea
[Brew] (Kettle [Kettle.Tea] Cup)
Consuming Brew opens a scope holding Kettle and the remainder Tea. The scope's own rule [Kettle.Tea] Cup fires, and because the rule belongs to the scope, its output returns to the enclosing scope: Cup.

A scope works like a function body: it receives the remainder, runs its local rules and returns through them.

Next: Inference

Language · 6

Inference

A rule can apply to what a configuration can become. The event happens at the original configuration.

A
[A] B.C
[B] D
In order, the rules give B.C and then C.D. But A can become B, so [B] D also applies to A directly: it consumes A and produces D. C existed only along the way, so it is not part of that result. The dashed arrow is the inferred event.

Abstraction through rules

Inference makes abstraction ordinary. Here two ordinary rules make True and False count as Boolean:

Not.True
[True] Boolean
[False] Boolean
[Not.Boolean] (
    [True] False,
    [False] True,
)
[Not.Boolean] needs a Boolean. True can become one, so the rule applies to Not.True. The scope receives the concrete True, not Boolean, and its local rule answers False.

Inference splits a match in two. Occurrences matched as written, here Not, are exact. Occurrences reached through evidence, here True seen as Boolean, are the witness. A literal output consumes both. A scope consumes the exact part and receives the witness. Hover an event to outline what it consumes: solid for exact, dashed for witness.

And.True.False.Extra
[True] Boolean,
[False] Boolean,
[And.Boolean.Boolean] (
    [True.True] True,
    [True.False] False,
    [False.False] False,
)
Both operands become Boolean, so the scope receives True.False.Extra. One table row covers both operand orders, because True.False and False.True are one particle. Extra is remainder, so the answer is False.Extra.

Next: Rules are values

Language · 7

Rules are values

A rule in brackets inside a particle is a value. It can be produced, carried, matched, consumed and replaced.

Seed.A
[Seed] [A] B
[Seed] [A] B produces the rule [A] B as a value. A rule value in a coherence is live and applies within that coherence, turning A into B. Inference also reaches Seed.B: executing a rule reads it without consuming it.
([A] B).A
[[A] B] [A] C
[[A] B] [A] C matches the whole rule value [A] B, consumes it and produces [A] C. Matching compares complete code; a rule never looks inside another.

Loaded rules are live occurrences too, and a rule can consume one:

[A] B
[([A] B)] C
[([A] B)] C consumes the loaded rule [A] B and produces C. The target lists every rule that survives.

Next: Fields and shorthand

Language · 8

Fields and shorthand

Particles are unordered, so a role needs its value attached. A one-rule value does that.

Digit.0.Carry.1 cannot say which number is the digit. ([Digit] 0).([Carry] 1) can: each role and its value form one rule value, a field.

Fields are live rules. Beside a bare P, the field ([P] True) fires and turns it into True. The theorems use this on purpose, and the library never uses a role name as a plain atom.

P.([P] True)

Shared prefixes

Parentheses in a particle distribute: A(B, C) means A.B, A.C, and (A, B).(C, D) means four coherences. They build nothing: Box(A.B) is exactly Box.A.B. In a rule's output, adjacent groups are separate outputs: [A] (B)(C) produces two coherences.

Next: Prism

Part two

Verification

Verification · 9

Prism

Prism answers one question: can this exact configuration be reached?

A target is a complete configuration: every coherence, every occurrence and every live rule. A.X [A] B reaches B.X [A] B, where the rule is still live, but never plain B.X.

A.X
[A] B
The start counts: every configuration reaches itself. When the book is served, edit the targets and run again.
VerdictMeaning
reachedA supported configuration equals the target.
unreachableExploration finished without it. Only a complete exploration can say this.
unknownNeither: a budget stopped the search first. Unknown never passes a test.
[] A
Two coherences of A appear early. A.A never does, but the exploration never finishes, so Prism cannot prove it unreachable.

Direct paths

Large programs are checked along a direct path: one execution, followed until it reaches the target, returns to a configuration it has seen, or no rule applies. A path can prove a target reachable; it can never prove one unreachable. Programs that build linked data, and most theorems, are checked along paths.

Next: The workbench

Verification · 10

The workbench

Two views of one program. The state graph shows every configuration; the hypergraph shows how one execution moves through its coherences.

In the hypergraph each coherence is a lane that runs from the event that produced it to the event that consumes it. An event is a hyperedge: it consumes whole coherences and produces new ones, and every other lane passes it untouched. Lanes that never meet evolve independently. Select a configuration in the state graph to draw the execution that reaches it.

Every explored example in this book has a Workbench button that opens it here. For your own programs, the sandbox gives the same views a full page, keeps your draft, and makes links that carry the whole program.

Filter by pattern

Type a pattern in the filter field, or press ⌘ K or / anywhere in the book. The workbench keeps what matches and everything computed after it, and hides the rest.

PatternMatches
BA coherence that holds B
B.XA coherence that holds both B and X
B, CA configuration in which two different coherences hold B and C
([A] B)A coherence that holds the rule value [A] B
[B, C] DAn event that applies the rule [B, C] D

A pattern matches the way a rule's input does: a coherence matches when it contains every occurrence the pattern names, whatever else it holds. In the hypergraph, each coherence of a pattern selects lanes on its own.

Next: Tests and tools

Verification · 11

Tests and tools

Bazel builds, runs and tests Photonic. It is the only tool to install; it downloads everything else.

load("//photonic:defs.bzl", "photonic_binary", "photonic_library", "photonic_test")

photonic_library(
    name = "logic",
    srcs = ["logic.particle"],
    deps = ["//library/boolean:not"],
)

photonic_binary(
    name = "example",
    srcs = ["request.wave", "value.particle"],
    deps = [":logic"],
)

photonic_test(
    name = "negation",
    source = "Invoke.Boolean.Not.True",
    targets = ["False"],
    preserve = True,
    deps = ["//library/boolean:not"],
)

A library holds only rules. A binary combines the coherences of its sources and loads each library file once. .particle and .wave share one grammar; by convention .particle holds reusable definitions and .wave runnable programs.

Test attributeMeaning
source, srcs, depsThe program: literal source, files and libraries.
targetsComplete configurations. "A, B" is one target with two coherences; ["A", "B"] is two targets.
preserveRequired. True adds every loaded root rule to each target.
expect"reached", the default, or "unreachable".
match"all", the default: every target meets the expectation. "any": at least one does.
pathTrue follows a direct path per target, which can only expect reached.
steps, states, cells, frames, coherences, recordsBudgets for work, configurations, occurrences, scopes, coherences and records.

Command line

bazel run -c opt //command:photonic -- parse "$PWD/program/language/inference.wave"
bazel run -c opt //command:photonic -- lower "$PWD/program/language/inference.wave"
bazel run -c opt //command:photonic -- run "$PWD/program/language/inference.wave"
bazel run -c opt //command:photonic -- prism "$PWD/program/language/inference.wave" --target "$PWD/target.json"

parse prints the syntax tree, lower the program as JSON, run the explored configurations, and prism a verdict for a target in lowered JSON. lower --context copies a source's root rules into a target; --library loads a rules-only file; --path follows a direct path. bazel run starts in its runfiles directory, so pass absolute paths.

bazel test -c opt //...
bazel run -c opt //program/language:inference
bazel test -c opt //program/language:conjunction.check

This book

bazel run -c opt //toolchain/browser:serve
bazel run -c opt //book:record
bazel test -c opt //book:record.check

The server runs every example live in WebAssembly. //book:record reruns every example, lens and expression in this page and stores the results in book/record.js, which makes the book work as a local file. //book:record.check fails when that record is stale or an example contradicts its stated verdict.

Next: The standard library

Part three

The standard library

Library · 12

The standard library

The standard library is written in Photonic: 69 rule files in 14 packages. Rust only loads and checks it.

Every operation is named Type.Verb, and each type belongs to one package. A request carries Function; its answer carries Return. Scalar requests run inside Invoke, which is the whole of invoke.particle:

[Invoke] (
    Function
    [Return] ()
)

Invoke opens a scope holding Function and the rest of the request. The scope's own rule [Return] () returns whatever accompanies Return. An implementation is a table:

[Function.Boolean.Not.True] Return.False
[Function.Boolean.Not.False] Return.True
[Boolean.Not.([Each] Boolean.Not)] Function.Boolean.Not
Invoke.Boolean.Not.True
Enter the scope, rewrite Function.Boolean.Not.True to Return.False, return False. The third rule lets collections name Boolean.Not as a callback.
Invoke.Ternary.Add.2.2
Digit tables answer with fields. In base three, 2 + 2 is digit 1, carry 1.

Conventions

PackageProvides
functionInvoke, Identity, Compose
booleanNot, And, Or, Equal
ternaryTrit Add, Sum, Multiply, Successor, Compare, Equal, Subtract, Select
binaryBit Sum and Multiply
carryKill, Propagate and Generate: Combine, Equal, Evaluate
collectionPairs: Produce, Copy, Repeat, Broadcast, Unpack, Choose, Map, Gather, Reduce
selectionFilter, Check, Count, Reduce
fieldPack and Unpack for positions 0 to 3 and values 0 to 2
streamA streaming Successor
chainLinked cells: Push, Read, Peek, Forget, Reverse, Erase
naturalUnbounded base-three naturals: copy, trim, normalize, successor, add, subtract, difference, multiply, divide, compare
integerSigned Add, Subtract, Multiply, Divide
expressionTokens, parsing and evaluation of infix expressions
vectorRecursive vectors: link, insert, take, reverse, erase, stable merge sort

The library reference lists every request and answer.

Next: Linked data

Library · 13

Linked data

Unbounded values are linked: a handle coherence reaches cells elsewhere in the same scope.

A chain handle is Zero or a Head carrying a private seal and its methods. Requests sit beside the handle:

Request beside a chainAnswer
Push.symbolBuilt beside the longer chain
ReadYield.symbol beside the tail, or Yield.End.Zero
PeekSeen.symbol beside a new reference to the tail; the cells stay
ForgetClean once this reference is dropped

Every output of an event receives the remainder, so a rule with two outputs duplicates a handle. Programs therefore move a chain by sending it on and forgetting the other copy, then continue on Clean. This program pushes 2, 0 and 1, reverses the chain and reads back 2, 0, 1:

Push.([Digit] 2).Zero, Stage.1
[Built, Stage.1] (Push.([Digit] 0)) (Forget.Stage.2)
[Built, Clean.Stage.2] (Push.([Digit] 1)) (Forget.Stage.3)
[Built, Clean.Stage.3] (Function.Chain.Reverse) (Forget.Inspect.Start)
[Return.Chain.Reverse, Clean.Inspect.Start] (Read) (Forget.Inspect.0)
[Yield.([Digit] 2), Clean.Inspect.0] (Read) (Forget.Inspect.1)
[Yield.([Digit] 0), Clean.Inspect.1] (Read) (Forget.Inspect.2)
[Yield.([Digit] 1), Clean.Inspect.2] Done

A vector holds any value that answers Forget, including naturals, chains and other vectors, by reference. Function.Vector.Sort sorts a vector of naturals with a stable, adaptive merge sort: one pass splits the input into runs, balanced passes merge them pairwise, and alternating orientation means no merge result is ever reversed. For n items in r runs it makes at most n − 1 + n⌈log₂ r⌉ comparisons.

bazel test -c opt //program/ternary:reversal.check
bazel run -c opt //program/vector:sort

Next: Arithmetic

Library · 14

Arithmetic

A natural is a chain of base-three digits, least significant first. All of its arithmetic runs as Photonic rules.

Function.Natural.Add sits beside Operand.Left and Operand.Right and answers Return.Natural.Add. Subtraction answers a number or Error.Underflow; division answers a quotient and a remainder, or Error.Divisor; comparison answers Less, Equal or Greater and leaves both operands intact. Integers add a sign: zero is Positive, and division truncates toward zero.

The expression package parses and evaluates infix expressions with precedence, left association, parentheses and unary minus, and answers Error.Syntax, Error.Stack or Error.Divisor for bad input. Below, the host only turns your characters into a token tape. Parsing and every digit of arithmetic are Photonic rules.

The panel shows the program your expression becomes. When the book is served, step through every event of its path.

Next: Proof by execution

Part four

Proof

Proof · 15

Proof by execution

A theorem program lists every case of a claim, checks each one, and concludes Theorem only when all of them hold. Prism checks that it reaches exactly Theorem.

Case.(([P] True), ([P] False))

[Claim] Function.Boolean.Not.Once.P
[Return.Once] Function.Boolean.Not.Twice
[Return.Twice] Function.Boolean.Equal.Verdict.P

[Holds.(([P] True), ([P] False))] Theorem
Negating twice returns the original value. Step through every event of the proof.

The first line expands into two cases. ([P] True) is a field, so every P in its case becomes True. case.particle gives each case its own scope: a true verdict returns Holds with the case's assignment, a false one Counterexample. The last rule needs one Holds for every assignment, and consuming them leaves exactly Theorem.

Four kinds of claims

ClaimHow it decides each case
EvaluatedRuns the library's own operations and compares the answers.
CoveredAssigns truth values to propositions about unknown relations; each rule closes the cases where a hypothesis fails or the conclusion holds.
JudgedDerives values from definitions, then a judge table upholds or overturns each case.
DerivedAn equational proof: facts are equations, rules are congruence and transitivity.

Every term is Photonic structure that rules can match: Times.([Left] X).([Right] Y), Meet.([Of] X).([Of] Y), Less.([Left] X).([Right] Y), Equation.([Side] S).([Side] T). Two Of fields form a multiset, so commutativity needs no proof.

What is proved

1 · LogicThe laws of every Boolean algebra.
2 · RelationsLaws of every relation satisfying their hypotheses: orders, equivalences, partitions.
3 · Constructed typesOrders on products, sums and sequences, and the ternary comparison.
4 · Arithmetic schemesThe carry algebra and induction steps that hold at every width.
5 · CombinatoricsPigeonhole, Schur, van der Waerden and Ramsey numbers, each refuted one size below.
6 · AlgebraGroup, lattice and ring laws derived from their axioms.
7 · CountingEvery ternary digit table is counting with the successor.
8 · Chain cellsA cell returns exactly its item and the chain below, for every item.
9 · Linked naturalsEach step of the library's own addition, subtraction, comparison, trim, reversal and successor engines.

84 claims: 79 proofs and 5 refutations. The last three layers prove the standard library itself, so linked addition, subtraction, comparison and successor are correct at every width. The theorem guide explains how to write one.

bazel test -c opt //theorem/...

Next: Inside the runtime

Part five

The system

System · 16

Inside the runtime

The runtime is Rust. One transition kernel applies every rule; two searches decide which rules to apply.

ParseAn eight-line pest grammar builds a lossless syntax tree with byte spans.
LowerShorthand expands; particles, rules and scopes become a program.
CompileAtoms and rules are interned; rules with equal inputs share plans.
MatchIndexes and incremental joins deliver every binding.
ApplyOne kernel consumes, carries the remainder, introduces outputs and opens scopes.
CanonicalizeConfigurations equal up to renaming become one state.
ExploreExhaustive search takes every event; a direct path follows one.
VerifyPrism compares the exact target and reports its verdict.

The kernel is language/evaluation.rs. Exhaustive search in language/runtime.rs also performs inference and records support: the events and derivations that justify each configuration. A configuration counts as reached only with support grounded in the start, so a cycle cannot justify itself. Direct paths live in language/path.rs, and Prism in language/prism.rs.

Reports are identical with one, two or four workers; the tests check it. A budget defers work instead of dropping it, so a larger budget resumes.

BudgetWorkConfigurationsCoherencesOccurrencesScopesRecords
Command line12,00080464101,000,000
photonic_test2,000,0004,09664256642,000,000
This book, explored20,0001281612816100,000
This book, direct paths1,000,00032,7685128,1921,0242,000,000

The occurrence contract states loading, ownership and consumption precisely, and the runtime roadmap sets the priorities for runtime work.

Next: The repository

System · 17

The repository

Bazel is the only dependency. It downloads pinned Rust, LLVM, Node and every crate, and it checks formatting and lints on every build.

DirectoryResponsibility
frontendGrammar, parsing and lowering
languageExecution, exploration, canonical states and Prism
commandThe photonic command
libraryThe standard library, in Photonic
programRunnable programs by subject, with their tests
theoremTheorems proved by execution
photonicBazel rules, assembly, launcher and the Prism test runner
bookThis webbook's styles, scripts and recorded runs
toolchainHermetic tools, checks and the WebAssembly engine
arithmeticGenerated arithmetic circuits and the infix encoder
benchmarkRuntime measurements
code, machine, translationLabel-free programs, exact flat execution and conversion to Photonic text
network, gpu, learning, randomThe program optimization learner
platformSupported platforms
documentContracts, the roadmap and dated runtime records

The learner

//learning:learning optimizes Photonic programs. It edits rules with a planner guided by a label-free transformer, keeps every example correct under every schedule, and minimizes parallel time and program size. solve finds the cheapest small flat program for a task, even one defined only by tests, and proves it optimal by exhausting every program that could beat it.

bazel run -c opt //learning:learning -- train --duration 3600
bazel run -c opt //learning:learning -- solve --input "$PWD/input.wave" --output "$PWD/output.wave"

The learner guide describes the objective, the search and the results. Build organization and continuous verification describe the build and the Buildkite pipeline.

Next: Grammar and glossary

Appendix

Reference

Reference · 18

Grammar and glossary

The whole grammar, every form and every term.

Grammar

module = { SOI ~ item* ~ EOI }
item = _{ concept | group | context | continuation | coherence | space }
group = { "(" ~ item* ~ ")" }
context = { "[" ~ item* ~ "]" }
continuation = { "." }
coherence = { "," }
space = @{ (" " | "\t" | "\r" | "\n" | "\u{000B}" | "\u{000C}")+ }
concept = @{ (!("(" | ")" | "[" | "]" | "." | "," | space) ~ ANY)+ }

This is the complete parser grammar. There are no keywords, operators or reserved words: Not, -> and unless are ordinary atoms.

Forms

FormExampleMeaning
AtomTrueOne occurrence
ParticleA.BOccurrences together, unordered
CoherencesA, BSeparate coherences
Empty coherence()One coherence with no occurrences
Rule[A] BConsume A, produce B with the remainder
Join and split[A, B] C, DConsume from two coherences, produce two
Separate outputs[A] (B)(C)Two output coherences
Rule with no output[A], [B] CA comma ends it before the next rule
Zero inputs[] AProduce a coherence from nothing
Any coherence[()] A, [,] AMatch one or two coherences by position alone
Scope[A] (B [B] C)Open a scope with a local rule
Produced rule[Seed] [A] BProduce the rule [A] B as a value
Matched rule[[A] B] CConsume the rule [A] B
Field([Digit] 0)A role with its value
Shared prefixA(B, C)A.B, A.C

Glossary

Atom
A run of characters without delimiters. It means nothing until rules use it.
Occurrence
One appearance of an atom or rule value, with its own identity.
Particle
An unordered multiset of occurrences in one coherence.
Coherence
An independent place where a particle lives.
Configuration
All coherences and live rules at one moment; a node of the graph.
Rule
Inputs in brackets and outputs after them. Also a value.
Event
One rule applied to one match; an arrow of the graph.
Remainder
What a match leaves in its coherences; every output receives it.
Scope
A local place opened by an output group with rules. Its own rules return to the enclosing scope.
Inference
Applying a rule to what a configuration can become, at the configuration itself.
Witness
The concrete occurrences an inferred match reached through evidence.
Field
A one-rule value such as ([Digit] 0) that keeps a role with its value.
Target
A complete configuration, including live rules, that Prism looks for.
Verdict
Reached, unreachable or unknown.
Direct path
One execution, followed until it reaches the target or can go no further.
Budget
A limit on exploration. Exhausting it defers work and leaves answers unknown.

Read next