Command-Line Manipulator of 𝜑-Calculus Expressions


This is a command-line normalizer, rewriter, and dataizer
of 𝜑-calculus expressions.
First, you write a simple 𝜑-calculus program
in the hello.phi file:
Φ ↦ ⟦ φ ↦ ⟦ Δ ⤍ 68-65-6C-6C-6F ⟧, t ↦ ξ.k, k ↦ ⟦⟧ ⟧
Installation
Then you can install phino in two ways:
Install Cabal first and then:
cabal update
cabal install --overwrite-policy=always phino-0.0.0.68
phino --version
Or download binary from the internet using curl or
wget:
sudo curl -o /usr/local/bin/phino http://phino.objectionary.com/releases/macos-15/phino-latest
sudo chmod +x /usr/local/bin/phino
phino --version
Download paths are:
Build
To build phino from source, clone this repository:
git clone git@github.com:objectionary/phino.git
cd phino
Then, run the following command (ensure you have Cabal installed):
cabal build all
Next, run this command to install phino system-wide:
sudo cp "$(cabal list-bin phino)" /usr/local/bin/phino
Verify that phino is installed correctly:
$ phino --version
0.0.0.0
You can ensure scripts are run with a specific version of phino using
the --pin global option. It exits with an error when the version supplied
doesn't match the installed one:
phino --pin=0.0.0.67 dataize hello.phi
Dataize
Then, you dataize the program:
$ phino dataize hello.phi
68-65-6C-6C-6F
Rewrite
You can rewrite this expression with the help of rules
defined in the my-rule.yml YAML file (here, the !d is a capturing group,
similar to regular expressions):
name: My custom rule
pattern: Δ ⤍ !d
result: Δ ⤍ 62-79-65
Then, rewrite:
$ phino rewrite --rule=my-rule.yml hello.phi
Φ ↦ ⟦ φ ↦ ⟦ Δ ⤍ 62-79-65 ⟧, t ↦ ξ.k, k ↦ ⟦⟧ ⟧
If you want to use many rules, just use --rule as many times as you need:
phino rewrite --rule=rule1.yaml --rule=rule2.yaml ...
You can also use built-in rules, which are designed
to normalize expressions:
phino rewrite --normalize hello.phi
If no input file is provided, the 𝜑-expression is taken from stdin:
$ echo 'Φ ↦ ⟦ φ ↦ ⟦ Δ ⤍ 68-65-6C-6C-6F ⟧ ⟧' | phino rewrite --rule=my-rule.yml
Φ ↦ ⟦ φ ↦ ⟦ Δ ⤍ 62-79-65 ⟧ ⟧
You're able to pass XMIR as input. Use --input=xmir and phino
will parse given XMIR from file or stdin and convert it to phi AST.
phino rewrite --rule=my-rule.yaml --input=xmir file.xmir
Also phino supports 𝜑-expressions in
ASCII format and with
syntax sugar. The rewrite command also allows you to desugar the expression
and print it in canonical syntax:
$ echo 'Q -> [[ @ -> Q.io.stdout("hello") ]]' | phino rewrite
Φ ↦ ⟦
φ ↦ Φ.io.stdout(
α0 ↦ Φ.string(
α0 ↦ Φ.bytes(
α0 ↦ ⟦ Δ ⤍ 68-65-6C-6C-6F ⟧
)
)
)
⟧
Merge
You can merge several 𝜑-programs into a single one by merging their
top level formations:
$ cat bytes.phi
{⟦ bytes(data) ↦ ⟦ φ ↦ data ⟧ ⟧}
$ cat number.phi
{⟦
number(as-bytes) ↦ ⟦
φ ↦ as-bytes,
plus(x) ↦ ⟦ λ ⤍ L_number_plus ⟧
⟧
⟧}
$ cat minus.phi
{⟦ number ↦ ⟦ minus(x) ↦ ⟦ λ ⤍ L_number_minus ⟧ ⟧ ⟧}
$ phino merge bytes.phi number.phi minus.phi --sweet
{⟦
bytes(data) ↦ ⟦ φ ↦ data ⟧,
number(as-bytes) ↦ ⟦
φ ↦ as-bytes,
plus(x) ↦ ⟦ λ ⤍ L_number_plus ⟧,
minus(x) ↦ ⟦ λ ⤍ L_number_minus ⟧
⟧
⟧}
Match
You can test the 𝜑-program matches against the rule
pattern. The result output contains matched substitutions:
$ phino match --pattern='⟦ Δ ⤍ !d, !B ⟧' hello.phi
B >> ⟦ ρ ↦ ∅ ⟧
d >> 68-65-6C-6C-6F
Explain
You can explain rewriting rule by printing them in LaTeX format:
$ phino explain --normalize
\begin{tabular}{rl}
\trrule{alpha}
{ [[ B_1, \tau_1 -> ?, B_2 ]] ( \tau_2 -> e ) }
{ [[ B_1, \tau_1 -> ?, B_2 ]] ( \tau_1 -> e ) }
{ if $ \indexof{ \tau_2 } = \vert B_1 \vert $ }
{ }
\trrule{dc}
{ T ( \tau -> e ) }
{ T }
{ }
{ }
...
\trrule{stop}
{ [[ B ]] . \tau }
{ T }
{ if $ \tau \notin B \;\text{and}\; @ \notin B \;\text{and}\; L \notin B $ }
{ }
\end{tabular}
For more details, use phino [COMMAND] --help option.
Rule structure
This is BNF-like yaml rule structure. Here types ended with
apostrophe, like Attribute' are built types from 𝜑-program AST
Rule:
name: String
pattern: String
result: String
when: Condition? # predicate, works with substitutions before extension
where: [Extension]? # substitution extensions
having: Condition? # predicate, works with substitutions after extension
Condition:
= and: [Condition] # logical AND
| or: [Condition] # logical OR
| not: Condition # logical NOT
| alpha: Attribute' # check if given attribute is alpha
| eq: # compare two comparable objects
- Comparable
- Comparable
| in: # check if attributes exist in bindings
- Attribute'
- Binding'
| nf: Expression' # returns True if given expression in normal form
# which means that no more other normalization rules
# can be applied
| xi: Expression' # special condition for Rcopy normalization rule to
# avoid infinite recursion while the condition checking
# returns True if there's no ξ outside of the formation
# in given expression.
| matches: # returns True if given expression after dataization
- String # matches to given regex
- Expression
| part-of: # returns True if given expression is attached to any
- Expression' # attribute in ginve bindings
- BiMeta'
Comparable: # comparable object that may be used in 'eq' condition
= Attribute'
| Number
| Expression'
Number: # comparable number
= Integer # just regular integer
| index: Attribute' # calculate index of alpha attribute
| length: BiMeta' # calculate length of bindings by given meta binding
Extension: # substitutions extension used to introduce new meta variables
meta: [ExtArgument] # new introduced meta variable
function: String # name of the function
args: [ExtArgument] # arguments of the function
ExtArgument
= Bytes' # !d
| Binding' # !B
| Expression' # !e
| Attribute' # !a
Here's list of functions that are supported for extensions:
contextualize - function of two arguments, that rewrites given expression
depending on provided context according to the contextualization
rules
scope - resolves the scope for given expression. Works only with meta
expressions denotes as 𝑒 or !e. The scope is nearest outer formation,
if it's present. In all other cases the default scope is used, which is
anonymous formation ⟦ ρ ↦ ∅ ⟧.
random-tau - creates attribute with random unique name. Accepts bindings,
and attributes. Ensures that created attribute is not present in list of
provided attributes and does not exist as attribute in provided bindings.
dataize - dataizes given expression and returns bytes.
concat - accepts bytes or dataizable expressions as arguments,
concatenates them into single sequence and convert it to expression
that can be pretty printed as human readable string:
Φ.string(Φ.bytes⟦ Δ ⤍ !d ⟧).
sed - pattern replacer, works like unix sed function.
Accepts two arguments: target expression and pattern.
Pattern must start with s/, consists of three parts
separated by /, for example, this pattern s/\\s+//g
replaces all the spaces with empty string. To escape braces and slashes
in pattern and replacement parts - use them with \\,
e.g. s/\\(.+\\)//g.
random-string - accepts dataizable expression or bytes as pattern.
Replaces %x and %d formatters with random hex numbers and
decimals accordingly. Uniqueness is guaranteed during one
execution of phino.
size - accepts exactly one meta binding and returns size of it and
Φ.number.
tau - accepts Φ.string, dataizes it and converts it to attribute.
If dataized string can't be converted to attribute - an error is thrown.
string - accepts Φ.string or Φ.number or attribute and converts it
to Φ.string.
number - accepts Φ.string and converts it Φ.number
sum - accepts list of Φ.number or Φ.bytes and returns sum of them as Φ.number
join - accepts list of bindings and returns list of joined bindings. Duplicated
ρ, Δ and λ attributes are ignored, all other duplicated attributes are replaced
with unique attributes using random-tau function.
splice - accepts three arguments: input bindings 𝐵-in, a sentinel expression,
and replacement bindings 𝐵-rep. Returns a new binding group where 𝐵-rep
is inserted in front of every binding in 𝐵-in whose value is a formation
with φ equal to the sentinel. Every spliced copy of 𝐵-rep has its
τ-labelled attributes renamed via random-tau so the resulting binding
group has no duplicates. 𝐵-rep must contain only τ-labelled bindings
(BiTau (AtLabel _) _ or BiVoid (AtLabel _)); any ρ, Δ, λ, φ,
α, or meta-attribute binding in 𝐵-rep makes the function fail fast
because such bindings cannot be renamed and would produce duplicates
when spliced at more than one position. When no binding matches the
sentinel, the output equals the input unchanged.
The phino supports meta variables to write 𝜑-expression patterns for
capturing attributes, bindings, etc.
This is the list of supported meta variables:
!a || 𝜏 - attribute
!e || 𝑒 - any expression
!B || 𝐵 - list of bindings
!d || δ - bytes in meta delta binding
!t - tail after expression, sequence of applications and/or dispatches,
must start only with dispatch
!F - function name in meta lambda binding
Every meta variable may also be used with an integer index, like !B1 or 𝜏0.
Incorrect usage of meta variables in 𝜑-expression patterns leads to
parsing errors.
Benchmark
To run performance benchmarks, you need Java 8+ and curl.
Maven is downloaded automatically on first run via benchmark/mvnw.
The benchmark uses the compiled Native class from
JNA — a large real-world Java class — as its test input.
On first run, make bench downloads the class, disassembles it to
XMIR via jeo-maven-plugin, converts it to 𝜑 using
phino rewrite, and caches the results in benchmark/tmp/.
Subsequent runs skip straight to the benchmarks.
make bench
=== parse/phi ===
warmup: 3 iterations
batches: 10 x 1
total: 1431411.012 μs
avg: 143141.101 μs
min: 127073.145 μs
max: 171130.864 μs
std dev: 15649.917 μs
=== parse/xmir ===
warmup: 3 iterations
batches: 10 x 1
total: 7598416.608 μs
avg: 759841.661 μs
min: 697811.420 μs
max: 822708.096 μs
std dev: 41396.013 μs
=== rewrite/normalize ===
warmup: 3 iterations
batches: 10 x 1
total: 364316.104 μs
avg: 36431.610 μs
min: 35585.946 μs
max: 38039.285 μs
std dev: 724.383 μs
=== print/sweet/multiline ===
warmup: 3 iterations
batches: 10 x 1
total: 3936466.911 μs
avg: 393646.691 μs
min: 388074.584 μs
max: 398140.650 μs
std dev: 3449.956 μs
=== print/sweet/flat ===
warmup: 3 iterations
batches: 10 x 1
total: 3778721.383 μs
avg: 377872.138 μs
min: 355092.443 μs
max: 394891.090 μs
std dev: 13649.562 μs
=== print/salty/multiline ===
warmup: 3 iterations
batches: 10 x 1
total: 13588867.661 μs
avg: 1358886.766 μs
min: 1332657.250 μs
max: 1380376.704 μs
std dev: 13948.257 μs
The results were calculated in this GHA job
on 2026-05-18 at 22:53,
on Linux with 4 CPUs.
How to Contribute
Fork repository, make changes, then send us a pull request.
We will review your changes and apply them to the master branch shortly,
provided they don't violate our quality standards. To avoid frustration,
before sending us your pull request please make sure all your tests pass:
make all
To generate a local coverage report for development, run:
make coverage
You will need GHC ≥ 9.6.7 and Cabal ≥ 3.0 (recommended)
or Stack ≥ 3.0 installed.