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 ↦ ⟦⟧ ⟧
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 --versionOr 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 --versionDownload paths are:
- Ubuntu 22.04: http://phino.objectionary.com/releases/ubuntu-22.04/phino-latest
- Ubuntu 24.04: http://phino.objectionary.com/releases/ubuntu-24.04/phino-latest
- MacOS (ARM): http://phino.objectionary.com/releases/macos-15/phino-latest
- MacOS (Intel): http://phino.objectionary.com/releases/macos-14-large/phino-latest
- Windows: http://phino.objectionary.com/releases/windows-2022/phino-latest.exe
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 --versionThe 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.
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/phinoThe 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.phiThen, you dataize the expression:
$ phino dataize hello.phi
68-65-6C-6C-6Fphino 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-00Dataizing 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.dataizebinds 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.morphbinds 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.undressbinds a term meta asmorphdoes, and then, while the term is a formation bindingφalone beside itsρ, takes itsφand morphs that.evaluatebinds a term meta to the answer of firing the formation another meta holds, through evaluation, 𝔼, as in𝑛2: 𝑛1.deferbinds 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.staybinds a term meta to the term another meta holds, with every( ρ ↦ … )applied to a dispatch dropped.symbolizebinds a term meta to the term another meta holds, with every datum turned into a fresh symbol.rewritebinds a term meta to the term the meta underofholds, with the rules underrulesapplied and nothing normalized afterwards. The rules are written as in a rule file.castholds 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𝜎1in 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.joinanswers with one term made of two sides, in place of𝑛. It holdson, the condition, which is a bytes meta ofdataizeor a path from$. It holdsleftandright, the two sides, each a path from$or a term meta. It may holdotherwise, 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.
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.
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-00Three options stop a run that goes on for too long:
--max-stepsbounds how deep morphing and dataization may nest,1000by default;--max-firingsbounds how many λ functions the whole run may fire;--max-secondsbounds 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.
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.
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:
provenmeans equal up to a renaming of symbols;plausiblealso 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.
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 # provenIn 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 anundressline 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 …andunripe …are what ajoinmade of its two sides, or why it could not join them.deferred …andsymbolized …are the symbols adeferor asymbolizeline minted.cast …binds a meta to the copy acastblock 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, andstall …stands under one that--acyclic=plausibleanswered with an earlier stall.looped …is where--acycliccut a loop, andlooped 𝜎1 := …names the call of the object it answered with that symbol.starved …,spent …andtimeout …are where--max-steps,--max-firingsand--max-secondsran out.msec …,firings …andfps …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.
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.phiSome 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.xmirAlso 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 ⊥.
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:λ ⟧
⟧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-6FBy 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 allThe 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.
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 --normalizeFor more details, use phino [COMMAND] --help option.
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 rulesrandom-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 unixsedfunction. Accepts two arguments: target expression and pattern. Pattern must start withs/, consists of three parts separated by/, for example, this patterns/\\s+//greplaces 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%xand%dformatters with random hex numbers and decimals accordingly. Uniqueness is guaranteed during one execution ofphino.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Φ.stringorΦ.numberor attribute and converts it toΦ.string.number- acceptsΦ.stringand converts itΦ.numbersum- accepts list ofΦ.numberorΦ.bytesand returns sum of them asΦ.numberjoin- accepts list of bindings and returns list of joined bindings. Duplicatedρ,Δandλattributes are ignored, all other duplicated attributes are replaced with unique attributes usingrandom-taufunction.
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 explicitnf: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 explicitabsolute:ornf: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:𝜎1is one concrete symbol, which no substitution ever binds, and a bare𝜎in the answer of a--symbolicentry 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.
To run performance benchmarks, you need Java 8+ and curl:
make benchOn 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.
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 allTo generate a local coverage report for development, run:
make coverageTo build a phino executable into the root of the repository, run:
make phinoYou will need GHC ≥ 9.6.7 and Cabal ≥ 3.0 (recommended) or Stack ≥ 3.0 installed.