Skip to content
objectionaryPublic

About

Command-Line Normalizer, Rewriter, and Dataizer of 𝜑-Calculus Expressions

Topics

Resources

Stars

19 stars

Watchers

2 watching

Forks

Repository files navigation

Command-Line Manipulator of 𝜑-Calculus Expressions

DevOps By Rultor.com

phino on Hackage cabal-linux stack-linux codecov Haddock License Hits-of-Code PDD status

This is a command-line normalizer, rewriter, and dataizer of 𝜑-calculus expressions.

First, you write a simple 𝜑-calculus expression 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.160
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:

In a GitHub Actions workflow, one step downloads the binary for the runner and puts it on the PATH:

- uses: objectionary/phino@0.0.160
  with:
    version-file: phino-version.txt
- run: phino --version

The file holds the version to install, such as 0.0.150. Give version: 0.0.150 instead of version-file to write the version in the workflow itself.

Build

To build phino from source, clone this repository and build it with Cabal:

git clone git@github.com:objectionary/phino.git
cd phino
cabal build all
sudo cp "$(cabal list-bin phino)" /usr/local/bin/phino

The global --pin option fails the run when the installed version is not the one given, and --pin-file reads that version from a file:

phino --pin=0.0.0.67 dataize hello.phi

Dataize

Then, you dataize the expression:

$ phino dataize hello.phi
68-65-6C-6C-6F

Symbolic λ functions

phino implements no λ function. They come from a YAML file given with --symbolic, one entry per λ function. An entry never computes a value. It answers with a term that holds a symbol, 𝜎, which names a value nobody worked out. Here is atoms.yaml, which the examples below use:

- λ: L_number_(plus|times)
  dataize:
    𝛿1: $.ρ
    𝛿2: $.x
  𝑛: Φ.number( φ ↦ ⟦ λ ⤍ 𝜎 ⟧ )
$ cat sum.phi
⟦
  bytes ↦ ⟦ φ ↦ ∅ ⟧,
  number ↦ ⟦ φ ↦ ∅, plus(ρ, x) ↦ ⟦ λ ⤍ L_number_plus ⟧ ⟧,
  φ ↦ 5.plus( 6 )
⟧
$ phino dataize --symbolic=atoms.yaml --sweet --hide-rho sum.phi
40-45-00-00-00-00-00-00

Dataizing a symbol always gives the same bytes, the number 42, as above.

Inside an entry, $ is the formation being fired and Φ is the universe. A meta is a named placeholder: 𝛿1, 𝛿2 and so on hold bytes, while 𝑛1, 𝑛2 and so on hold terms. An entry may hold only the keys below, and any other key refuses the whole file:

  • λ is a regular expression that matches the whole λ name. No two entries may match the same name.
  • dataize binds a bytes meta to the data a term comes down to through dataization, 𝔻. The term is a path from $, as in 𝛿1: $.ρ, or a meta another line binds, as in 𝛿2: 𝑛1.
  • morph binds a term meta to the normal form a term reaches through morphing, 𝕄. The term is a path from $, as in 𝑛1: $.x, or a meta another line binds, as in 𝑛4: 𝑛3.
  • undress binds a term meta as morph does, and then, while the term is a formation binding φ alone beside its ρ, takes its φ and morphs that.
  • evaluate binds a term meta to the answer of firing the formation another meta holds, through evaluation, 𝔼, as in 𝑛2: 𝑛1.
  • defer binds a term meta to a fresh symbol when another meta holds a copy of an object whose argument is a bare symbol, ⟦ λ ⤍ 𝜎1 ⟧, and to that term as it is otherwise.
  • stay binds a term meta to the term another meta holds, with every ( ρ ↦ … ) applied to a dispatch dropped.
  • symbolize binds a term meta to the term another meta holds, with every datum turned into a fresh symbol.
  • rewrite binds a term meta to the term the meta under of holds, with the rules under rules applied and nothing normalized afterwards. The rules are written as in a rule file.
  • cast holds exactly two lines and binds their two metas as one pair. When one term is a bare symbol, ⟦ λ ⤍ 𝜎1 ⟧, and the other is a formation binding φ, the bare symbol becomes a copy of the other term with 𝜎1 in place of the term its φ chain ends in, whatever that term is.
  • 𝑛 is the term the entry answers with. A bare 𝜎 in it stands for a fresh symbol, and no 𝛿 may appear in it.
  • join answers with one term made of two sides, in place of 𝑛. It holds on, the condition, which is a bytes meta of dataize or a path from $. It holds left and right, the two sides, each a path from $ or a term meta. It may hold otherwise, a λ name that joins two formations of different shapes one attribute at a time, and the file must answer that name too.

An entry holds either 𝑛 or join, never both. The lines of all blocks run in the order of the metas they read, not in the order of the blocks. A line that reads a meta nothing binds refuses the file.

Partial evaluation

A λ name that no entry answers stops the run with an error. With --partial, the run prints the residual program instead: what the known inputs decide is computed, and the rest stays in place:

$ cat partial.phi
⟦
  bytes ↦ ⟦ φ ↦ ∅ ⟧,
  number ↦ ⟦
    φ ↦ ∅,
    plus(ρ, x) ↦ ⟦ λ ⤍ L_number_plus ⟧,
    times(ρ, x) ↦ ⟦ λ ⤍ L_number_times ⟧,
    as-bool ↦ ⟦ λ ⤍ L_number_as_bool ⟧
  ⟧,
  φ ↦ 2.times( 3 ).plus( 4 ).as-bool
⟧
$ phino dataize --symbolic=atoms.yaml --sweet --hide-rho partial.phi
[ERROR]: No entry of --symbolic answers the λ function 'L_number_as_bool'
$ phino dataize --symbolic=atoms.yaml --partial --sweet --hide-rho partial.phi
L_number_as_bool:λ

Here 2.times( 3 ).plus( 4 ) was computed, and the symbol it came to sits in the hidden ρ, while as-bool stays where it was. An operand that reaches the terminator ⊥ is left in place the same way.

Reducing a term inside a universe

The --inside option reduces a term the program does not hold, taking the program as the universe Φ. It cannot be combined with --locator:

$ cat universe.phi
⟦
  bytes ↦ ⟦ φ ↦ ∅ ⟧,
  number ↦ ⟦ φ ↦ ∅, plus(ρ, x) ↦ ⟦ λ ⤍ L_number_plus ⟧ ⟧
⟧
$ phino dataize --symbolic=atoms.yaml --inside='5.plus( 6 )' universe.phi
40-45-00-00-00-00-00-00

Limits

Three options stop a run that goes on for too long:

  • --max-steps bounds how deep morphing and dataization may nest, 1000 by default;
  • --max-firings bounds how many λ functions the whole run may fire;
  • --max-seconds bounds how many seconds the whole run may take.

--partial leaves a site in place when it runs out of steps or firings, but a run out of time always fails.

Morph

Morphing, 𝕄, evaluates as far as the object model allows without asking for data, and stops at the first formation it reaches:

$ cat two.phi
⟦
  bytes ↦ ⟦ φ ↦ ∅ ⟧,
  number ↦ ⟦ φ ↦ ∅, plus(ρ, x) ↦ ⟦ λ ⤍ L_number_plus ⟧ ⟧,
  φ ↦ 5.plus( 6 ).plus( 7 )
⟧
$ phino dataize --symbolic=atoms.yaml --sweet --hide-rho two.phi
40-45-00-00-00-00-00-00
$ phino morph --symbolic=atoms.yaml --locator=Q.φ --sweet --hide-rho two.phi
⟦ x ↦ 7.0, λ ⤍ L_number_plus ⟧

The inner 5.plus( 6 ) fires, because .plus is dispatched on its result, while the outer call is left for dataization. Where no formation is reachable, morph prints ⊥. It fails on a term that is not a normal form. It takes every option of dataize.

Acyclic morphing

phino stops no recursion by itself, so a run that never ends fails once --max-steps runs out. The --acyclic=<mode> option makes the run notice a loop instead. When a reduction is about to enter a formation it is already inside, the run answers a copy of an object binding φ with a fresh symbol and goes on, and stops at any other formation and leaves the term in place, as --partial does. The mode says when two formations are the same:

  • proven means equal up to a renaming of symbols;
  • plausible also means that the later one holds the earlier one under wrappers it gained, such as a growing accumulator, so it may cut a recursion that would have stopped.

Under plausible, a formation is fired only once, and a later firing of it takes the answer of the first one. The --max-loops=<n> option lets a reduction enter such a formation n more times before it is cut. dataize takes --acyclic too, and fails on a term left in place unless --partial is given, since such a term holds no data to print.

Protocol

The --protocol option writes every λ function the run fired into a file, as a tree. A file whose name ends in .xml gets the tree as XML, for a program to read, and any other name gets indented text. Here, L_down answers with a call of its own object over a fresh symbol, so without --acyclic the run would go on until --max-steps runs out:

$ cat down.yaml
- λ: L_down
  dataize:
    𝛿1: $.x
  𝑛: Φ.down( x ↦ ⟦ λ ⤍ 𝜎 ⟧ )
$ cat down.phi
⟦
  down ↦ ⟦ x ↦ ∅, λ ⤍ L_down ⟧,
  y ↦ Φ.down( x ↦ ⟦ λ ⤍ 𝜎1 ⟧ ).φ
⟧
$ phino morph --symbolic=down.yaml --locator=Q.y --acyclic=proven \
    --protocol=down.txt --sweet --hide-rho down.phi
⟦ x ↦ 𝜎1:λ, λ ⤍ L_down ⟧.φ
$ cat down.txt
𝕄(Φ.y):
  𝑛·0·1 := Φ.down( x ↦ 𝜎1:λ )
  𝑛·0·2 := ⟦ x ↦ 𝜎1:λ, λ ⤍ L_down ⟧
  𝔼(L_down):
    𝔻(ξ.x):
      𝛿·1·1 := 𝔻(𝜎1:λ)
    𝛿1·1 := 𝛿·1·1  # 𝔻(ξ.x)
    𝑛·1·1 := Φ.down( x ↦ 𝜎2:λ )  # 𝑛
    𝕄(𝑛·1·1):
      𝑛·1·2 := Φ.down( x ↦ 𝜎2:λ )
      𝑛·1·3 := ⟦ x ↦ 𝜎2:λ, λ ⤍ L_down ⟧
      morphed 𝑛·1·4 := 𝑛·1·3
    𝑛·1·5 := 𝑛·1·3  # 𝕄(𝑛·1·1)
  looped 𝑛·0·2  # proven

In a name such as 𝑛·1·2, the first number counts the firings, 0 outside every firing, and the second counts the names within that firing. The name 𝛿1·1 is the meta 𝛿1 of the first firing. A protocol may hold these lines:

  • # … lines open the file with the phino that wrote it, the command, the world, the λ functions, the locator, and the expression it points at.
  • 𝔻(…): and 𝕄(…): open a block where dataization or morphing starts at a new site, and what it does stands under it.
  • 𝔼(…): opens one firing, named by its λ function.
  • 𝑛·0·1 := … names an application, and the line after it names the object the application made.
  • 𝛿1·1 := … binds a meta of the entry, and the comment names the judgment and the term it came from.
  • 𝑛·1·1 := … # 𝑛 is the term the entry answered with, and the line commented with 𝕄(𝑛·1·1) is its normal form.
  • boxed … is dataization stepping into the φ of a formation.
  • undressed … is an undress line taking the φ of a formation.
  • 𝛿·1·1 := … closes a 𝔻 block with the data it found, or with 𝔻(𝜎1:λ) where it came down to a symbol.
  • morphed … closes a 𝕄 block with the term it came to.
  • joined …, fabricated …, diverged … and unripe … are what a join made of its two sides, or why it could not join them.
  • deferred … and symbolized … are the symbols a defer or a symbolize line minted.
  • cast … binds a meta to the copy a cast block made of a bare symbol, and the comment names the meta it read and the meta whose shape it took.
  • unanswered … is a λ name no entry answers.
  • stuck … closes a firing that got stuck, and stall … stands under one that --acyclic=plausible answered with an earlier stall.
  • looped … is where --acyclic cut a loop, and looped 𝜎1 := … names the call of the object it answered with that symbol.
  • starved …, spent … and timeout … are where --max-steps, --max-firings and --max-seconds ran out.
  • msec …, firings … and fps … close the file, and the listing above leaves them and the # … lines out, since they differ from run to run.

The --abridged option folds every formation longer than sixty-four characters, or than --abridged=WIDTH, into its φ, Δ and λ and a count of the rest. The --abridged-data option also cuts every byte string longer than eight bytes down to its first two bytes and its last two.

Rewrite

You can rewrite this expression with the help of rules defined in the my-rule.yml YAML file (here, the !d1 and !B1 are capturing groups, similar to regular expressions):

name: My custom rule
pattern: ⟦ Δ ⤍ !d1, !B1 ⟧
result: ⟦ Δ ⤍ 62-79-65, !B1 ⟧

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, alone or together with your own rules:

phino rewrite --normalize --rule=my-rule.yaml hello.phi

Some rules mint fresh names with the random-string function, and --seed picks the seed of its random generator, 0 by default.

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.io.stdout("hello") ]]' | phino rewrite
⟦
  φ ↦ Φ.io.stdout( α0 ↦ Φ.string( φ ↦ Φ.bytes( φ ↦ ⟦ Δ ⤍ 68-65-6C-6C-6F ⟧ ) ) )
⟧

The two styles may be mixed in one expression, unless --monoform is given.

A formation of a single binding may be written as its asset, a colon, and the attribute the asset is bound to:

⟦ Δ ⤍ FF-AA ⟧    = FF-AA:Δ    = FF-AA:D
⟦ λ ⤍ 𝜎1 ⟧       = 𝜎1:λ       = !S1:L
⟦ a ↦ ∅ ⟧        = ∅:a        = ?:a
⟦ φ ↦ ξ.a ⟧      = ξ.a:φ      = $.a:@

The colon binds as tightly as a dot, so ξ.a:φ.b is ⟦ φ ↦ ξ.a ⟧.b. With --sweet, phino prints every such formation this way.

A formation has a receiver ρ only when it declares one among its voids, the way EO declares ^, as in ⟦ ρ ↦ ∅, t ↦ ξ.ρ.k ⟧. phino adds none of its own, so ξ.ρ in a formation that declares none is ⊥.

Merge

You can merge several 𝜑-expressions into a single one by merging their top level formations:

$ cat bytes.phi
⟦ bytes ↦ ⟦ φ ↦ ∅ ⟧ ⟧
$ cat number.phi
⟦
  number ↦ ⟦
    φ ↦ ∅,
    plus(x) ↦ ⟦ λ ⤍ L_number_plus ⟧
  ⟧
⟧
$ cat minus.phi
⟦ number ↦ ⟦ minus(x) ↦ ⟦ λ ⤍ L_number_minus ⟧ ⟧ ⟧
$ phino merge bytes.phi number.phi minus.phi --sweet
⟦
  bytes(φ) ↦ ⟦⟧,
  number(φ) ↦ ⟦ plus(x) ↦ L_number_plus:λ, minus(x) ↦ L_number_minus:λ ⟧
⟧

Match

You can test the 𝜑-expression 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

Compile

By default, phino reads its rules from YAML and interprets them at every step. The compile command turns the built-in rules of normalization, contextualization, morphing and dataization, and every --rule file, into the Haskell module compiled/generated/Compiled.hs, and a second build links it in:

phino compile
cabal build all

The build links the module in only when the Cabal flag compiled is on. If there is no cabal.project.local, compile creates one that turns the flag on, and otherwise it prints the lines to add to it. The compiled rules take exactly the same steps as the YAML ones. compile refuses a rule it cannot turn into Haskell and names the reason. A binary built this way refuses to run once the built-in rules changed after the last compile. To test the whole suite against the compiled rules, run make compiled.

Explain

You can explain the built-in rules by printing them in LaTeX format. Pass exactly one of --normalize, --morph, --dataize or --contextualize for the rewriting, morphing (𝕄), dataization (𝔻) or contextualization (𝒞) rules (or --rule for a custom rule file):

phino explain --normalize

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 𝜑-expression 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
  | eq:                  # compare two comparable objects
      - Comparable
      - Comparable
  | in:                  # returns True if every given attribute exists in
      - Attribute' | [Attribute'] # the union of the given bindings
      - Binding' | [Binding']
  | nf: Expression'      # returns True if given expression in normal form
                         # which means that no more other normalization rules
                         # can be applied
  | absolute: Expression' # returns True if given expression is xi-free, i.e.
                         # there is no ξ outside of a formation: it is Φ, a
                         # formation, a dispatch with a xi-free subject, or an
                         # application with a xi-free subject and argument.
                         # Combined with a normal-form check by the '𝑘'/'!k'
                         # meta variable, which ranges over the absolute
                         # expressions 𝒦 ⊆ 𝒩, used by the Rcopy rule.
  | 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'
  | formation:           # returns True if given expression is a formation
      Expression'        # (an abstraction ⟦…⟧); used by morphing 'md'
                         # as 'not (formation 𝑛)', so a non-formation head is
                         # morphed and a formation head is left to 'ml'
  | gt:                  # returns True if the first comparable object is
      - Comparable       # greater than the second one
      - Comparable
  | disjoint:            # returns True if none of the given attributes exists
      - [Attribute']     # in the given bindings
      - Binding'

Comparable:              # comparable object that may be used in 'eq' condition
  = Attribute'
  | Number
  | Expression'

Number:                  # comparable number
  = Integer              # just regular integer
  | IndexMeta'           # 𝑖 (or !i), the index captured by an α𝑖 argument
  | length: BiMeta'      # calculate length of bindings by given meta binding
  | domain: BiMeta'      # calculate number of unique attributes in given
                         # meta binding (excluding 'assets')

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'           # !t

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
  • random-tau - creates an attribute with a random unique name. It accepts no arguments; uniqueness is guaranteed across all names already taken in the document.
  • 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.

Meta variables

The phino supports meta variables to write 𝜑-expression patterns for capturing attributes, bindings, etc.

This is the list of supported meta variables:

  • !t || 𝜏 - attribute
  • !i || 𝑖 - the index of a positional (α) application argument, captured by writing α𝑖 (or ~!i)
  • !e || 𝑒 - any expression
  • !n || 𝑛 - any expression that is already in normal form (behaves like !e/𝑒, but only binds a sub-expression in NF, so no explicit nf: guard is needed)
  • !k || 𝑘 - any expression that is absolute, i.e. xi-free and in normal form (ranges over 𝒦 ⊆ 𝒩); behaves like !e/𝑒 but only binds an absolute sub-expression, so no explicit absolute: or nf: guard is needed
  • !B || 𝐵 - list of bindings
  • !d || 𝛿 - bytes in meta delta binding
  • !F || 𝑓 - function name in meta lambda binding
  • !S || 𝜎 - a symbol standing where a λ name stands (see Symbolic λ functions). It is spelled the way a meta variable is spelled but is a name and no capture: 𝜎1 is one concrete symbol, which no substitution ever binds, and a bare 𝜎 in the answer of a --symbolic entry asks for a fresh one

A meta variable carries a suffix, like !B1 or 𝜏2, to name what it captured, so that the result, when, where and having of a rule can read it back. A suffix starts with one, so !B0 is refused. Without a suffix, a meta variable is anonymous: it matches any term on its own, binds no name, and is refused anywhere but in a pattern, so ⟦ 𝜏 ↦ 𝑒, 𝜏 ↦ 𝑒 ⟧ matches any two attributes.

A positional (α) application argument is written as α0, ~0 (ASCII), or α𝑖/~!i when its index is captured by an !i/𝑖 meta variable.

Benchmark

To run performance benchmarks, you need Java 8+ and curl:

make bench

On the first run, make bench downloads the compiled Native class of JNA, turns it into XMIR with jeo-maven-plugin and then into 𝜑, and caches the results in benchmark/tmp/. Besides parsing, printing and rewriting that class, the suite morphs the entries of benchmark/demo.phi symbolically, one case per entry.

=== parse/phi ===
  warmup:     3 iterations
  batches:    10 x 1
  total:      1608083.040 μs
  avg:        160808.304 μs
  min:        143101.612 μs
  max:        193265.817 μs
  std dev:    18245.966 μs
=== parse/xmir ===
  warmup:     3 iterations
  batches:    10 x 1
  total:      7316618.299 μs
  avg:        731661.830 μs
  min:        682143.329 μs
  max:        830099.553 μs
  std dev:    50454.098 μs
=== rewrite/normalize ===
  warmup:     3 iterations
  batches:    10 x 1
  total:      14.944 μs
  avg:        1.494 μs
  min:        1.202 μs
  max:        1.954 μs
  std dev:    0.202 μs
=== print/sweet/multiline ===
  warmup:     3 iterations
  batches:    10 x 1
  total:      4809921.339 μs
  avg:        480992.134 μs
  min:        454276.182 μs
  max:        516075.593 μs
  std dev:    22669.487 μs
=== print/sweet/flat ===
  warmup:     3 iterations
  batches:    10 x 1
  total:      614959.297 μs
  avg:        61495.930 μs
  min:        56067.793 μs
  max:        72915.649 μs
  std dev:    6427.649 μs
=== print/salty/multiline ===
  warmup:     3 iterations
  batches:    10 x 1
  total:      15201256.227 μs
  avg:        1520125.623 μs
  min:        1459961.284 μs
  max:        1572281.268 μs
  std dev:    33340.580 μs
=== morph/symbolic/demo/e1 ===
  warmup:     3 iterations
  batches:    10 x 12
  total:      8709.287 μs
  avg:        72.577 μs
  min:        70.323 μs
  max:        81.607 μs
  std dev:    3.304 μs
=== morph/symbolic/demo/e2 ===
  warmup:     3 iterations
  batches:    10 x 207
  total:      147258.026 μs
  avg:        71.139 μs
  min:        69.520 μs
  max:        78.360 μs
  std dev:    2.574 μs
=== morph/symbolic/demo/e3 ===
  warmup:     3 iterations
  batches:    10 x 206
  total:      146732.045 μs
  avg:        71.229 μs
  min:        70.312 μs
  max:        72.132 μs
  std dev:    0.475 μs
=== morph/symbolic/demo/e4 ===
  warmup:     3 iterations
  batches:    10 x 171
  total:      120546.559 μs
  avg:        70.495 μs
  min:        69.951 μs
  max:        71.223 μs
  std dev:    0.437 μs
=== morph/symbolic/demo/e5 ===
  warmup:     3 iterations
  batches:    10 x 203
  total:      147811.931 μs
  avg:        72.814 μs
  min:        72.141 μs
  max:        73.392 μs
  std dev:    0.357 μs
=== morph/symbolic/native/e5 ===
  warmup:     3 iterations
  batches:    10 x 4
  total:      32249.873 μs
  avg:        806.247 μs
  min:        777.274 μs
  max:        851.917 μs
  std dev:    25.021 μs
=== morph/symbolic/accum/0 ===
  warmup:     3 iterations
  batches:    10 x 100
  total:      59457.025 μs
  avg:        59.457 μs
  min:        58.827 μs
  max:        60.580 μs
  std dev:    0.517 μs
=== morph/symbolic/accum/400 ===
  warmup:     3 iterations
  batches:    10 x 99
  total:      72053.382 μs
  avg:        72.781 μs
  min:        71.956 μs
  max:        76.229 μs
  std dev:    1.186 μs

The results were calculated in this GHA job on 2026-10-30 at 14:06, 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

To build a phino executable into the root of the repository, run:

make phino

You will need GHC ≥ 9.6.7 and Cabal ≥ 3.0 (recommended) or Stack ≥ 3.0 installed.

About

Command-Line Normalizer, Rewriter, and Dataizer of 𝜑-Calculus Expressions

Topics

Resources

Stars

19 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages