Compare commits
17 Commits
0.18.0
...
2627627493
| Author | SHA1 | Date | |
|---|---|---|---|
| 2627627493 | |||
| c008126b14 | |||
| 6b97b210ca | |||
|
|
71653311ce | ||
| 0cdc0bfc34 | |||
| c36d963640 | |||
| 72e5810ca9 | |||
| b96a3f2ef0 | |||
| 6780b242b1 | |||
| 94514f7dd0 | |||
| 43e83be9a4 | |||
| 3717942589 | |||
| b8e2743103 | |||
| 25bfe139e8 | |||
| f2beb86d8a | |||
| 5024a2be4c | |||
| fccee3e61c |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@
|
|||||||
/Dockerfile
|
/Dockerfile
|
||||||
/config.dhall
|
/config.dhall
|
||||||
/result
|
/result
|
||||||
|
.aider*
|
||||||
WD
|
WD
|
||||||
bin/
|
bin/
|
||||||
dist*
|
dist*
|
||||||
|
|||||||
46
README.md
46
README.md
@@ -2,37 +2,31 @@
|
|||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
tricu (pronounced "tree-shoe") is a purely functional interpreted language implemented in Haskell. It is fundamentally based on the application of [Tree Calculus](https://github.com/barry-jay-personal/typed_tree_calculus/blob/main/typed_program_analysis.pdf) terms, but minimal syntax sugar is included to provide a useful programming tool.
|
tricu (pronounced "tree-shoe") is a programming language experiment in Haskell. It is fundamentally based on the application of [Triage Calculus](https://olydis.medium.com/a-visual-introduction-to-tree-calculus-2f4a34ceffc2), an extended form of [Tree Calculus](https://github.com/barry-jay-personal/typed_tree_calculus/blob/main/typed_program_analysis.pdf), terms, but minimal syntax sugar is included.
|
||||||
|
|
||||||
*tricu is under active development and you should expect breaking changes with every commit.*
|
|
||||||
|
|
||||||
tricu is the word for "tree" in Lojban: `(x1) is a tree of species/cultivar (x2)`.
|
tricu is the word for "tree" in Lojban: `(x1) is a tree of species/cultivar (x2)`.
|
||||||
|
|
||||||
## Features
|
## Acknowledgements
|
||||||
|
|
||||||
- Tree Calculus operator: `t`
|
Tree Calculus was discovered by [Barry Jay](https://github.com/barry-jay-personal/blog). The addition of Triage rules were suggested by [Johannes Bader](https://johannes-bader.com/). Johannes is also the creator of [treecalcul.us](https://treecalcul.us) which has a great intuitive code playground using his language LambAda.
|
||||||
- Assignments: `x = t t`
|
|
||||||
- Immutable definitions
|
## Versioning
|
||||||
- Lambda abstraction syntax: `id = (\a : a)`
|
|
||||||
- List, Number, and String literals: `[(2) ("Hello")]`
|
This really is a repo for experimentation so I'm not doing anything sane with the versioning for now. If I decide to stabilize the project we'll start anew at 2.0.
|
||||||
- Function application: `not (not false)`
|
|
||||||
- Higher order/first-class functions: `map (\a : append a "!") [("Hello")]`
|
|
||||||
- Intensionality blurs the distinction between functions and data (see REPL examples)
|
|
||||||
- Simple module system for code organization
|
|
||||||
|
|
||||||
## REPL examples
|
## REPL examples
|
||||||
|
|
||||||
```
|
```
|
||||||
tricu < -- Anything after `--` on a single line is a comment
|
tricu < -- Anything after `--` on a single line is a comment
|
||||||
tricu < id = (\a : a) -- Lambda abstraction is eliminated to tree calculus terms
|
tricu < id = (a : a) -- Lambda abstraction is eliminated to tree calculus terms
|
||||||
tricu < head (map (\i : append i " world!") [("Hello, ")])
|
tricu < head (map (i : append i " world!") [("Hello, ")])
|
||||||
tricu > "Hello, world!"
|
tricu > "Hello, world!"
|
||||||
tricu < id (head (map (\i : append i " world!") [("Hello, ")]))
|
tricu < id (head (map (i : append i " world!") [("Hello, ")]))
|
||||||
tricu > "Hello, world!"
|
tricu > "Hello, world!"
|
||||||
|
|
||||||
tricu < -- Intensionality! We can inspect the structure of a function or data.
|
tricu < -- Intensionality! We can inspect the structure of a function or data.
|
||||||
tricu < triage = (\a b c : t (t a b) c)
|
tricu < triage = (a b c : t (t a b) c)
|
||||||
tricu < test = triage "Leaf" (\z : "Stem") (\a b : "Fork")
|
tricu < test = triage "Leaf" (z : "Stem") (a b : "Fork")
|
||||||
tricu < test (t t)
|
tricu < test (t t)
|
||||||
tricu > "Stem"
|
tricu > "Stem"
|
||||||
tricu < -- We can even convert a term back to source code (/demos/toSource.tri)
|
tricu < -- We can even convert a term back to source code (/demos/toSource.tri)
|
||||||
@@ -55,9 +49,7 @@ tricu < !version -- Print tricu version
|
|||||||
|
|
||||||
## Installation and Use
|
## Installation and Use
|
||||||
|
|
||||||
[Releases are available for Linux.](https://git.eversole.co/James/tricu/releases)
|
You can easily build and run this project using [Nix](https://nixos.org/download/).
|
||||||
|
|
||||||
Or you can easily build and run this project using [Nix](https://nixos.org/download/).
|
|
||||||
|
|
||||||
- Quick Start (REPL):
|
- Quick Start (REPL):
|
||||||
- `nix run git+https://git.eversole.co/James/tricu`
|
- `nix run git+https://git.eversole.co/James/tricu`
|
||||||
@@ -93,15 +85,3 @@ tricu decode [OPTIONS]
|
|||||||
-f --file=FILE Optional input file path to attempt decoding.
|
-f --file=FILE Optional input file path to attempt decoding.
|
||||||
Defaults to stdin.
|
Defaults to stdin.
|
||||||
```
|
```
|
||||||
|
|
||||||
## Collaborating
|
|
||||||
|
|
||||||
I am happy to accept issue reports, pull requests, or questions about tricu [via email](mailto:james@eversole.co).
|
|
||||||
|
|
||||||
If you want to collaborate but don't want to email back-and-forth, please reach out via email once to let me know and I will provision a git.eversole.co account for you.
|
|
||||||
|
|
||||||
## Acknowledgements
|
|
||||||
|
|
||||||
Tree Calculus was discovered by [Barry Jay](https://github.com/barry-jay-personal/blog).
|
|
||||||
|
|
||||||
[treecalcul.us](https://treecalcul.us) is an excellent website with an intuitive Tree Calculus code playground created by [Johannes Bader](https://johannes-bader.com/) that introduced me to Tree Calculus.
|
|
||||||
|
|||||||
@@ -11,20 +11,17 @@ demo_true = t t
|
|||||||
not_TC? = t (t (t t) (t t t)) (t t (t t t))
|
not_TC? = t (t (t t) (t t t)) (t t (t t t))
|
||||||
|
|
||||||
-- /demos/toSource.tri contains an explanation of `triage`
|
-- /demos/toSource.tri contains an explanation of `triage`
|
||||||
demo_triage = \a b c : t (t a b) c
|
demo_triage = a b c : t (t a b) c
|
||||||
demo_matchBool = (\ot of : demo_triage
|
demo_matchBool = a b : demo_triage b (_ : a) (_ _ : a)
|
||||||
of
|
|
||||||
(\_ : ot)
|
|
||||||
(\_ _ : ot)
|
|
||||||
)
|
|
||||||
-- Lambda representation of the Boolean `not` function
|
-- Lambda representation of the Boolean `not` function
|
||||||
not_Lambda? = demo_matchBool demo_false demo_true
|
not_Lambda? = demo_matchBool demo_false demo_true
|
||||||
|
|
||||||
-- Since tricu eliminates Lambda terms to SKI combinators, the tree form of many
|
-- As tricu eliminates Lambda terms to SKI combinators, the tree form of many
|
||||||
-- functions defined via Lambda terms are larger than the most efficient TC
|
-- functions defined via Lambda terms are larger than the most efficient TC
|
||||||
-- representation. Between different languages that evaluate to tree calculus
|
-- representation possible. Between different languages that evaluate to tree
|
||||||
-- terms, the exact implementation of Lambda elimination may differ and lead
|
-- calculus terms, the exact implementation of Lambda elimination may differ
|
||||||
-- to different tree representations even if they share extensional behavior.
|
-- and lead to different trees even if they share extensional behavior.
|
||||||
|
|
||||||
-- Let's see if these are the same:
|
-- Let's see if these are the same:
|
||||||
lambdaEqualsTC = equal? not_TC? not_Lambda?
|
lambdaEqualsTC = equal? not_TC? not_Lambda?
|
||||||
|
|||||||
@@ -18,47 +18,47 @@ main = exampleTwo
|
|||||||
-- / / \
|
-- / / \
|
||||||
-- 4 5 6
|
-- 4 5 6
|
||||||
|
|
||||||
label = \node : head node
|
label = node : head node
|
||||||
|
|
||||||
left = (\node : if (emptyList? node)
|
left = node : (if (emptyList? node)
|
||||||
[]
|
[]
|
||||||
(if (emptyList? (tail node))
|
(if (emptyList? (tail node))
|
||||||
[]
|
[]
|
||||||
(head (tail node))))
|
(head (tail node))))
|
||||||
|
|
||||||
right = (\node : if (emptyList? node)
|
right = node : (if (emptyList? node)
|
||||||
[]
|
[]
|
||||||
(if (emptyList? (tail node))
|
(if (emptyList? (tail node))
|
||||||
[]
|
[]
|
||||||
(if (emptyList? (tail (tail node)))
|
(if (emptyList? (tail (tail node)))
|
||||||
[]
|
[]
|
||||||
(head (tail (tail node))))))
|
(head (tail (tail node))))))
|
||||||
|
|
||||||
processLevel = y (\self queue : if (emptyList? queue)
|
processLevel = y (self queue : if (emptyList? queue)
|
||||||
[]
|
[]
|
||||||
(pair (map label queue) (self (filter
|
(pair (map label queue) (self (filter
|
||||||
(\node : not? (emptyList? node))
|
(node : not? (emptyList? node))
|
||||||
(append (map left queue) (map right queue))))))
|
(append (map left queue) (map right queue))))))
|
||||||
|
|
||||||
levelOrderTraversal_ = \a : processLevel (t a t)
|
levelOrderTraversal_ = a : processLevel (t a t)
|
||||||
|
|
||||||
toLineString = y (\self levels : if (emptyList? levels)
|
toLineString = y (self levels : if (emptyList? levels)
|
||||||
""
|
""
|
||||||
(append
|
(append
|
||||||
(append (map (\x : append x " ") (head levels)) "")
|
(append (map (x : append x " ") (head levels)) "")
|
||||||
(if (emptyList? (tail levels)) "" (append (t (t 10 t) t) (self (tail levels))))))
|
(if (emptyList? (tail levels)) "" (append (t (t 10 t) t) (self (tail levels))))))
|
||||||
|
|
||||||
levelOrderToString = \s : toLineString (levelOrderTraversal_ s)
|
levelOrderToString = s : toLineString (levelOrderTraversal_ s)
|
||||||
|
|
||||||
flatten = foldl (\acc x : append acc x) ""
|
flatten = foldl (acc x : append acc x) ""
|
||||||
|
|
||||||
levelOrderTraversal = \s : append (t 10 t) (flatten (levelOrderToString s))
|
levelOrderTraversal = s : append (t 10 t) (flatten (levelOrderToString s))
|
||||||
|
|
||||||
exampleOne = levelOrderTraversal [("1")
|
exampleOne = levelOrderTraversal [("1")
|
||||||
[("2") [("4") t t] t]
|
[("2") [("4") t t] t]
|
||||||
[("3") [("5") t t] [("6") t t]]]
|
[("3") [("5") t t] [("6") t t]]]
|
||||||
|
|
||||||
exampleTwo = levelOrderTraversal [("1")
|
exampleTwo = levelOrderTraversal [("1")
|
||||||
[("2") [("4") [("8") t t] [("9") t t]]
|
[("2") [("4") [("8") t t] [("9") t t]]
|
||||||
[("6") [("10") t t] [("12") t t]]]
|
[("6") [("10") t t] [("12") t t]]]
|
||||||
[("3") [("5") [("11") t t] t] [("7") t t]]]
|
[("3") [("5") [("11") t t] t] [("7") t t]]]
|
||||||
|
|||||||
37
demos/patternMatching.tri
Normal file
37
demos/patternMatching.tri
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
!import "../lib/patterns.tri" !Local
|
||||||
|
|
||||||
|
-- We can do conditional pattern matching by providing a list of lists, where
|
||||||
|
-- each sublist contains a boolean expression and a function to return if said
|
||||||
|
-- boolean expression evaluates to true.
|
||||||
|
|
||||||
|
value = 42
|
||||||
|
main = match value [[(equal? "Hello") (_ : ", world!")] [(equal? 42) (_ : "The answer.")]]
|
||||||
|
|
||||||
|
-- < main
|
||||||
|
-- > "The answer."
|
||||||
|
|
||||||
|
matchExample = (x : match x
|
||||||
|
[[(equal? 1) (_ : "one")]
|
||||||
|
[(equal? 2) (_ : "two")]
|
||||||
|
[(equal? 3) (_ : "three")]
|
||||||
|
[(equal? 4) (_ : "four")]
|
||||||
|
[(equal? 5) (_ : "five")]
|
||||||
|
[(equal? 6) (_ : "six")]
|
||||||
|
[(equal? 7) (_ : "seven")]
|
||||||
|
[(equal? 8) (_ : "eight")]
|
||||||
|
[(equal? 9) (_ : "nine")]
|
||||||
|
[(equal? 10) (_ : "ten")]
|
||||||
|
[ otherwise (_ : "I ran out of fingers!")]])
|
||||||
|
|
||||||
|
-- < matchExample 3
|
||||||
|
-- > "three"
|
||||||
|
-- < matchExample 5
|
||||||
|
-- > "five"
|
||||||
|
-- < matchExample 9
|
||||||
|
-- > "nine"
|
||||||
|
-- < matchExample 11
|
||||||
|
-- > "I ran out of fingers!"
|
||||||
|
-- < matchExample "three"
|
||||||
|
-- > "I ran out of fingers!"
|
||||||
|
-- < matchExample [("hello") ("world")]
|
||||||
|
-- > "I ran out of fingers!"
|
||||||
@@ -3,11 +3,9 @@
|
|||||||
|
|
||||||
main = size size
|
main = size size
|
||||||
|
|
||||||
size = (\x :
|
size = x : y (self x : compose succ (triage
|
||||||
(y (\self x :
|
id
|
||||||
compose succ
|
self
|
||||||
(triage
|
(x y : compose (self x) (self y))
|
||||||
(\x : x)
|
x)
|
||||||
self
|
) x 0
|
||||||
(\x y : compose (self x) (self y))
|
|
||||||
x)) x 0))
|
|
||||||
|
|||||||
@@ -18,25 +18,25 @@ main = toSource not?
|
|||||||
sourceLeaf = t (head "t")
|
sourceLeaf = t (head "t")
|
||||||
|
|
||||||
-- Stem case
|
-- Stem case
|
||||||
sourceStem = (\convert : (\a rest :
|
sourceStem = convert : (a rest :
|
||||||
t (head "(") -- Start with a left parenthesis "(".
|
t (head "(") -- Start with a left parenthesis "(".
|
||||||
(t (head "t") -- Add a "t"
|
(t (head "t") -- Add a "t"
|
||||||
(t (head " ") -- Add a space.
|
(t (head " ") -- Add a space.
|
||||||
(convert a -- Recursively convert the argument.
|
(convert a -- Recursively convert the argument.
|
||||||
(t (head ")") rest)))))) -- Close with ")" and append the rest.
|
(t (head ")") rest))))) -- Close with ")" and append the rest.
|
||||||
|
|
||||||
-- Fork case
|
-- Fork case
|
||||||
sourceFork = (\convert : (\a b rest :
|
sourceFork = convert : (a b rest :
|
||||||
t (head "(") -- Start with a left parenthesis "(".
|
t (head "(") -- Start with a left parenthesis "(".
|
||||||
(t (head "t") -- Add a "t"
|
(t (head "t") -- Add a "t"
|
||||||
(t (head " ") -- Add a space.
|
(t (head " ") -- Add a space.
|
||||||
(convert a -- Recursively convert the first arg.
|
(convert a -- Recursively convert the first arg.
|
||||||
(t (head " ") -- Add another space.
|
(t (head " ") -- Add another space.
|
||||||
(convert b -- Recursively convert the second arg.
|
(convert b -- Recursively convert the second arg.
|
||||||
(t (head ")") rest)))))))) -- Close with ")" and append the rest.
|
(t (head ")") rest))))))) -- Close with ")" and append the rest.
|
||||||
|
|
||||||
-- Wrapper around triage
|
-- Wrapper around triage
|
||||||
toSource_ = y (\self arg :
|
toSource_ = y (self arg :
|
||||||
triage
|
triage
|
||||||
sourceLeaf -- `triage` "a" case, Leaf
|
sourceLeaf -- `triage` "a" case, Leaf
|
||||||
(sourceStem self) -- `triage` "b" case, Stem
|
(sourceStem self) -- `triage` "b" case, Stem
|
||||||
@@ -44,7 +44,7 @@ toSource_ = y (\self arg :
|
|||||||
arg) -- The term to be inspected
|
arg) -- The term to be inspected
|
||||||
|
|
||||||
-- toSource takes a single TC term and returns a String
|
-- toSource takes a single TC term and returns a String
|
||||||
toSource = \v : toSource_ v ""
|
toSource = v : toSource_ v ""
|
||||||
|
|
||||||
exampleOne = toSource true -- OUT: "(t t)"
|
exampleOne = toSource true -- OUT: "(t t)"
|
||||||
exampleTwo = toSource not? -- OUT: "(t (t (t t) (t t t)) (t t (t t t)))"
|
exampleTwo = toSource not? -- OUT: "(t (t (t t) (t t t)) (t t (t t t)))"
|
||||||
|
|||||||
104
flake.nix
104
flake.nix
@@ -2,57 +2,63 @@
|
|||||||
description = "tricu";
|
description = "tricu";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs = {
|
nixpkgs.url = "github:NixOS/nixpkgs";
|
||||||
url = "https://github.com/nh2/nixpkgs/archive/ede5282c487a1fd2de64303ba59adad6726f1225.tar.gz";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
type = "tarball";
|
|
||||||
flake = false;
|
|
||||||
};
|
|
||||||
static-haskell-nix = {
|
|
||||||
url = "github:nh2/static-haskell-nix";
|
|
||||||
flake = false;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, static-haskell-nix }:
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
let
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
system = "x86_64-linux";
|
let
|
||||||
compiler = "ghc948";
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
packageName = "tricu";
|
packageName = "tricu";
|
||||||
|
|
||||||
overlay = self: super: {
|
haskellPackages = pkgs.haskellPackages;
|
||||||
haskell = super.haskell // {
|
hsLib = pkgs.haskell.lib;
|
||||||
packages = super.haskell.packages // {
|
|
||||||
${compiler} = super.haskell.packages.${compiler}.override {
|
tricuPackage =
|
||||||
overrides = final: prev: {
|
haskellPackages.callCabal2nix packageName self {};
|
||||||
${packageName} = prev.callCabal2nix packageName ./. {};
|
|
||||||
};
|
tricuTests =
|
||||||
};
|
hsLib.overrideCabal tricuPackage (old: {
|
||||||
};
|
doCheck = true;
|
||||||
|
|
||||||
|
configureFlags = (old.configureFlags or []) ++ [
|
||||||
|
"--enable-tests"
|
||||||
|
];
|
||||||
|
|
||||||
|
checkPhase = ''
|
||||||
|
runHook preCheck
|
||||||
|
./Setup test tricu-tests --show-details=direct
|
||||||
|
runHook postCheck
|
||||||
|
'';
|
||||||
|
});
|
||||||
|
|
||||||
|
customGHC = haskellPackages.ghcWithPackages (hpkgs: with hpkgs; [
|
||||||
|
megaparsec
|
||||||
|
]);
|
||||||
|
in {
|
||||||
|
packages.${packageName} = tricuPackage;
|
||||||
|
packages.default = tricuPackage;
|
||||||
|
|
||||||
|
packages.test = tricuTests;
|
||||||
|
|
||||||
|
checks.${packageName} = tricuTests;
|
||||||
|
checks.default = tricuTests;
|
||||||
|
|
||||||
|
defaultPackage = self.packages.${system}.default;
|
||||||
|
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
haskellPackages.cabal-install
|
||||||
|
haskellPackages.ghc-events
|
||||||
|
haskellPackages.ghcid
|
||||||
|
customGHC
|
||||||
|
upx
|
||||||
|
];
|
||||||
|
|
||||||
|
inputsFrom = [
|
||||||
|
tricuPackage
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
});
|
||||||
|
|
||||||
overlays = [overlay];
|
|
||||||
|
|
||||||
normalPkgs = import nixpkgs { inherit overlays system; };
|
|
||||||
|
|
||||||
survey = import "${static-haskell-nix}/survey" { inherit compiler normalPkgs; };
|
|
||||||
|
|
||||||
tricuStatic = survey.haskellPackages.${packageName};
|
|
||||||
|
|
||||||
in {
|
|
||||||
packages.${system}.default = tricuStatic;
|
|
||||||
|
|
||||||
devShells.default = normalPkgs.mkShell {
|
|
||||||
buildInputs = with normalPkgs; [
|
|
||||||
normalPkgs.haskellPackages.cabal-install
|
|
||||||
normalPkgs.haskellPackages.ghc-events
|
|
||||||
normalPkgs.haskellPackages.ghcid
|
|
||||||
normalPkgs.upx
|
|
||||||
];
|
|
||||||
inputsFrom = builtins.attrValues self.packages.${system};
|
|
||||||
};
|
|
||||||
|
|
||||||
devShell = self.devShells.${system}.default;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
64
lib/base.tri
64
lib/base.tri
@@ -1,74 +1,74 @@
|
|||||||
false = t
|
false = t
|
||||||
_ = t
|
_ = t
|
||||||
true = t t
|
true = t t
|
||||||
id = \a : a
|
id = a : a
|
||||||
const = \a b : a
|
const = a b : a
|
||||||
pair = t
|
pair = t
|
||||||
if = \cond then else : t (t else (t t then)) t cond
|
if = cond then else : t (t else (t t then)) t cond
|
||||||
|
|
||||||
y = ((\mut wait fun : wait mut (\x : fun (wait mut x)))
|
y = ((mut wait fun : wait mut (x : fun (wait mut x)))
|
||||||
(\x : x x)
|
(x : x x)
|
||||||
(\a0 a1 a2 : t (t a0) (t t a2) a1))
|
(a0 a1 a2 : t (t a0) (t t a2) a1))
|
||||||
|
|
||||||
compose = \f g x : f (g x)
|
compose = f g x : f (g x)
|
||||||
|
|
||||||
triage = \leaf stem fork : t (t leaf stem) fork
|
triage = leaf stem fork : t (t leaf stem) fork
|
||||||
test = triage "Leaf" (\_ : "Stem") (\_ _ : "Fork")
|
test = triage "Leaf" (_ : "Stem") (_ _ : "Fork")
|
||||||
|
|
||||||
matchBool = (\ot of : triage
|
matchBool = (ot of : triage
|
||||||
of
|
of
|
||||||
(\_ : ot)
|
(_ : ot)
|
||||||
(\_ _ : ot)
|
(_ _ : ot)
|
||||||
)
|
)
|
||||||
|
|
||||||
lAnd = (triage
|
lAnd = (triage
|
||||||
(\_ : false)
|
(_ : false)
|
||||||
(\_ x : x)
|
(_ x : x)
|
||||||
(\_ _ x : x))
|
(_ _ x : x))
|
||||||
|
|
||||||
lOr = (triage
|
lOr = (triage
|
||||||
(\x : x)
|
(x : x)
|
||||||
(\_ _ : true)
|
(_ _ : true)
|
||||||
(\_ _ _ : true))
|
(_ _ _ : true))
|
||||||
|
|
||||||
matchPair = \a : triage _ _ a
|
matchPair = a : triage _ _ a
|
||||||
|
|
||||||
not? = matchBool false true
|
not? = matchBool false true
|
||||||
and? = matchBool id (\_ : false)
|
and? = matchBool id (_ : false)
|
||||||
|
|
||||||
or? = (\x z :
|
or? = (x z :
|
||||||
matchBool
|
matchBool
|
||||||
(matchBool true true z)
|
(matchBool true true z)
|
||||||
(matchBool true false z)
|
(matchBool true false z)
|
||||||
x)
|
x)
|
||||||
|
|
||||||
xor? = (\x z :
|
xor? = (x z :
|
||||||
matchBool
|
matchBool
|
||||||
(matchBool false true z)
|
(matchBool false true z)
|
||||||
(matchBool true false z)
|
(matchBool true false z)
|
||||||
x)
|
x)
|
||||||
|
|
||||||
equal? = y (\self : triage
|
equal? = y (self : triage
|
||||||
(triage
|
(triage
|
||||||
true
|
true
|
||||||
(\_ : false)
|
(_ : false)
|
||||||
(\_ _ : false))
|
(_ _ : false))
|
||||||
(\ax :
|
(ax :
|
||||||
triage
|
triage
|
||||||
false
|
false
|
||||||
(self ax)
|
(self ax)
|
||||||
(\_ _ : false))
|
(_ _ : false))
|
||||||
(\ax ay :
|
(ax ay :
|
||||||
triage
|
triage
|
||||||
false
|
false
|
||||||
(\_ : false)
|
(_ : false)
|
||||||
(\bx by : lAnd (self ax bx) (self ay by))))
|
(bx by : lAnd (self ax bx) (self ay by))))
|
||||||
|
|
||||||
succ = y (\self :
|
succ = y (self :
|
||||||
triage
|
triage
|
||||||
1
|
1
|
||||||
t
|
t
|
||||||
(triage
|
(triage
|
||||||
(t (t t))
|
(t (t t))
|
||||||
(\_ tail : t t (self tail))
|
(_ tail : t t (self tail))
|
||||||
t))
|
t))
|
||||||
|
|||||||
74
lib/list.tri
74
lib/list.tri
@@ -1,68 +1,70 @@
|
|||||||
!import "base.tri" !Local
|
!import "base.tri" !Local
|
||||||
|
|
||||||
matchList = \a b : triage a _ b
|
_ = t
|
||||||
|
|
||||||
emptyList? = matchList true (\_ _ : false)
|
matchList = a b : triage a _ b
|
||||||
head = matchList t (\head _ : head)
|
|
||||||
tail = matchList t (\_ tail : tail)
|
|
||||||
|
|
||||||
append = y (\self : matchList
|
emptyList? = matchList true (_ _ : false)
|
||||||
(\k : k)
|
head = matchList t (head _ : head)
|
||||||
(\h r k : pair h (self r k)))
|
tail = matchList t (_ tail : tail)
|
||||||
|
|
||||||
lExist? = y (\self x : matchList
|
append = y (self : matchList
|
||||||
|
(k : k)
|
||||||
|
(h r k : pair h (self r k)))
|
||||||
|
|
||||||
|
lExist? = y (self x : matchList
|
||||||
false
|
false
|
||||||
(\h z : or? (equal? x h) (self x z)))
|
(h z : or? (equal? x h) (self x z)))
|
||||||
|
|
||||||
map_ = y (\self :
|
map_ = y (self :
|
||||||
matchList
|
matchList
|
||||||
(\_ : t)
|
(_ : t)
|
||||||
(\head tail f : pair (f head) (self tail f)))
|
(head tail f : pair (f head) (self tail f)))
|
||||||
map = \f l : map_ l f
|
map = f l : map_ l f
|
||||||
|
|
||||||
filter_ = y (\self : matchList
|
filter_ = y (self : matchList
|
||||||
(\_ : t)
|
(_ : t)
|
||||||
(\head tail f : matchBool (t head) id (f head) (self tail f)))
|
(head tail f : matchBool (t head) id (f head) (self tail f)))
|
||||||
filter = \f l : filter_ l f
|
filter = f l : filter_ l f
|
||||||
|
|
||||||
foldl_ = y (\self f l x : matchList (\acc : acc) (\head tail acc : self f tail (f acc head)) l x)
|
foldl_ = y (self f l x : matchList (acc : acc) (head tail acc : self f tail (f acc head)) l x)
|
||||||
foldl = \f x l : foldl_ f l x
|
foldl = f x l : foldl_ f l x
|
||||||
|
|
||||||
foldr_ = y (\self x f l : matchList x (\head tail : f (self x f tail) head) l)
|
foldr_ = y (self x f l : matchList x (head tail : f (self x f tail) head) l)
|
||||||
foldr = \f x l : foldr_ x f l
|
foldr = f x l : foldr_ x f l
|
||||||
|
|
||||||
length = y (\self : matchList
|
length = y (self : matchList
|
||||||
0
|
0
|
||||||
(\_ tail : succ (self tail)))
|
(_ tail : succ (self tail)))
|
||||||
|
|
||||||
reverse = y (\self : matchList
|
reverse = y (self : matchList
|
||||||
t
|
t
|
||||||
(\head tail : append (self tail) (pair head t)))
|
(head tail : append (self tail) (pair head t)))
|
||||||
|
|
||||||
snoc = y (\self x : matchList
|
snoc = y (self x : matchList
|
||||||
(pair x t)
|
(pair x t)
|
||||||
(\h z : pair h (self x z)))
|
(h z : pair h (self x z)))
|
||||||
|
|
||||||
count = y (\self x : matchList
|
count = y (self x : matchList
|
||||||
0
|
0
|
||||||
(\h z : matchBool
|
(h z : matchBool
|
||||||
(succ (self x z))
|
(succ (self x z))
|
||||||
(self x z)
|
(self x z)
|
||||||
(equal? x h)))
|
(equal? x h)))
|
||||||
|
|
||||||
last = y (\self : matchList
|
last = y (self : matchList
|
||||||
t
|
t
|
||||||
(\hd tl : matchBool
|
(hd tl : matchBool
|
||||||
hd
|
hd
|
||||||
(self tl)
|
(self tl)
|
||||||
(emptyList? tl)))
|
(emptyList? tl)))
|
||||||
|
|
||||||
all? = y (\self pred : matchList
|
all? = y (self pred : matchList
|
||||||
true
|
true
|
||||||
(\h z : and? (pred h) (self pred z)))
|
(h z : and? (pred h) (self pred z)))
|
||||||
|
|
||||||
any? = y (\self pred : matchList
|
any? = y (self pred : matchList
|
||||||
false
|
false
|
||||||
(\h z : or? (pred h) (self pred z)))
|
(h z : or? (pred h) (self pred z)))
|
||||||
|
|
||||||
intersect = \xs ys : filter (\x : lExist? x ys) xs
|
intersect = xs ys : filter (x : lExist? x ys) xs
|
||||||
|
|||||||
@@ -1,36 +1,24 @@
|
|||||||
|
!import "base.tri" !Local
|
||||||
!import "list.tri" !Local
|
!import "list.tri" !Local
|
||||||
|
|
||||||
match_ = y (\self value patterns :
|
match_ = y (self value patterns :
|
||||||
triage
|
triage
|
||||||
t
|
t
|
||||||
(\_ : t)
|
(_ : t)
|
||||||
(\pattern rest :
|
(pattern rest :
|
||||||
triage
|
triage
|
||||||
t
|
t
|
||||||
(\_ : t)
|
(_ : t)
|
||||||
(\test result :
|
(test result :
|
||||||
if (test value)
|
if (test value)
|
||||||
(result value)
|
(result value)
|
||||||
(self value rest))
|
(self value rest))
|
||||||
pattern)
|
pattern)
|
||||||
patterns)
|
patterns)
|
||||||
|
|
||||||
match = (\value patterns :
|
match = (value patterns :
|
||||||
match_ value (map (\sublist :
|
match_ value (map (sublist :
|
||||||
pair (head sublist) (head (tail sublist)))
|
pair (head sublist) (head (tail sublist)))
|
||||||
patterns))
|
patterns))
|
||||||
|
|
||||||
otherwise = const (t t)
|
otherwise = const (t t)
|
||||||
|
|
||||||
matchExample = (\x : match x
|
|
||||||
[[(equal? 1) (\_ : "one")]
|
|
||||||
[(equal? 2) (\_ : "two")]
|
|
||||||
[(equal? 3) (\_ : "three")]
|
|
||||||
[(equal? 4) (\_ : "four")]
|
|
||||||
[(equal? 5) (\_ : "five")]
|
|
||||||
[(equal? 6) (\_ : "six")]
|
|
||||||
[(equal? 7) (\_ : "seven")]
|
|
||||||
[(equal? 8) (\_ : "eight")]
|
|
||||||
[(equal? 9) (\_ : "nine")]
|
|
||||||
[(equal? 10) (\_ : "ten")]
|
|
||||||
[ otherwise (\_ : "I ran out of fingers!")]])
|
|
||||||
|
|||||||
255
src/ContentStore.hs
Normal file
255
src/ContentStore.hs
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
module ContentStore where
|
||||||
|
|
||||||
|
import Research
|
||||||
|
import Parser
|
||||||
|
|
||||||
|
import Control.Monad (foldM, forM_, void)
|
||||||
|
import Data.ByteString (ByteString)
|
||||||
|
import Data.List (nub, sort)
|
||||||
|
import Data.Maybe (catMaybes, fromMaybe)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import Database.SQLite.Simple
|
||||||
|
import Database.SQLite.Simple.FromRow (FromRow(..), field)
|
||||||
|
import System.Directory (createDirectoryIfMissing, getXdgDirectory, XdgDirectory(..))
|
||||||
|
import System.FilePath ((</>), takeDirectory)
|
||||||
|
|
||||||
|
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
import qualified Data.Text as T
|
||||||
|
|
||||||
|
data StoredNode = StoredNode ByteString deriving (Show)
|
||||||
|
|
||||||
|
instance FromRow StoredNode where
|
||||||
|
fromRow = StoredNode <$> field
|
||||||
|
|
||||||
|
data StoredTerm = StoredTerm
|
||||||
|
{ termHash :: Text
|
||||||
|
, termNames :: Text
|
||||||
|
, termMetadata :: Text
|
||||||
|
, termCreatedAt :: Integer
|
||||||
|
, termTags :: Text
|
||||||
|
} deriving (Show)
|
||||||
|
|
||||||
|
instance FromRow StoredTerm where
|
||||||
|
fromRow = StoredTerm <$> field <*> field <*> field <*> field <*> field
|
||||||
|
|
||||||
|
parseNameList :: Text -> [Text]
|
||||||
|
parseNameList = filter (not . T.null) . T.splitOn ","
|
||||||
|
|
||||||
|
serializeNameList :: [Text] -> Text
|
||||||
|
serializeNameList = T.intercalate "," . nub . sort
|
||||||
|
|
||||||
|
initContentStore :: IO Connection
|
||||||
|
initContentStore = do
|
||||||
|
dbPath <- getContentStorePath
|
||||||
|
createDirectoryIfMissing True (takeDirectory dbPath)
|
||||||
|
conn <- open dbPath
|
||||||
|
execute_ conn "CREATE TABLE IF NOT EXISTS terms (\
|
||||||
|
\hash TEXT PRIMARY KEY, \
|
||||||
|
\names TEXT, \
|
||||||
|
\metadata TEXT, \
|
||||||
|
\created_at INTEGER DEFAULT (strftime('%s','now')), \
|
||||||
|
\tags TEXT DEFAULT '')"
|
||||||
|
execute_ conn "CREATE INDEX IF NOT EXISTS terms_names_idx ON terms(names)"
|
||||||
|
execute_ conn "CREATE INDEX IF NOT EXISTS terms_tags_idx ON terms(tags)"
|
||||||
|
execute_ conn "CREATE TABLE IF NOT EXISTS merkle_nodes (\
|
||||||
|
\hash TEXT PRIMARY KEY, \
|
||||||
|
\node_data BLOB NOT NULL)"
|
||||||
|
return conn
|
||||||
|
|
||||||
|
getContentStorePath :: IO FilePath
|
||||||
|
getContentStorePath = do
|
||||||
|
dataDir <- getXdgDirectory XdgData "tricu"
|
||||||
|
return $ dataDir </> "content-store.db"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
hashTerm :: T -> Text
|
||||||
|
hashTerm = nodeHash . buildMerkle
|
||||||
|
|
||||||
|
storeTerm :: Connection -> [String] -> T -> IO Text
|
||||||
|
storeTerm conn newNamesStrList term = do
|
||||||
|
let termHashText = hashTerm term
|
||||||
|
newNamesTextList = map T.pack newNamesStrList
|
||||||
|
metadataText = T.pack "{}"
|
||||||
|
-- Store all Merkle nodes for this term
|
||||||
|
_ <- storeMerkleNodes conn term
|
||||||
|
existingNamesQuery <- query conn
|
||||||
|
"SELECT names FROM terms WHERE hash = ?"
|
||||||
|
(Only termHashText) :: IO [Only Text]
|
||||||
|
|
||||||
|
case existingNamesQuery of
|
||||||
|
[] -> do
|
||||||
|
let allNamesToStore = serializeNameList newNamesTextList
|
||||||
|
execute conn
|
||||||
|
"INSERT INTO terms (hash, names, metadata, tags) VALUES (?, ?, ?, ?)"
|
||||||
|
(termHashText, allNamesToStore, metadataText, T.pack "")
|
||||||
|
[(Only currentNamesText)] -> do
|
||||||
|
let currentNamesList = parseNameList currentNamesText
|
||||||
|
let combinedNamesList = currentNamesList ++ newNamesTextList
|
||||||
|
let allNamesToStore = serializeNameList combinedNamesList
|
||||||
|
execute conn
|
||||||
|
"UPDATE terms SET names = ?, metadata = ? WHERE hash = ?"
|
||||||
|
(allNamesToStore, metadataText, termHashText)
|
||||||
|
_ -> errorWithoutStackTrace $ "Multiple terms with same hash? " ++ show (length existingNamesQuery)
|
||||||
|
|
||||||
|
return termHashText
|
||||||
|
|
||||||
|
-- | Reconstruct a Tree Calculus term from its Merkle root hash.
|
||||||
|
-- Recursively loads nodes and rebuilds the T structure.
|
||||||
|
loadTree conn h
|
||||||
|
| h == nodeHash NLeaf = return (Just Leaf) -- NLeaf is implicit, not stored
|
||||||
|
| otherwise = do
|
||||||
|
maybeNode <- getNodeMerkle conn h
|
||||||
|
case maybeNode of
|
||||||
|
Nothing -> return Nothing
|
||||||
|
Just node -> Just <$> buildTree node
|
||||||
|
where
|
||||||
|
buildTree :: Node -> IO T
|
||||||
|
buildTree (NStem childHash) = do
|
||||||
|
child <- fromMaybe (errorWithoutStackTrace "BUG: stored hash not found") <$> loadTree conn childHash
|
||||||
|
return (Stem child)
|
||||||
|
buildTree (NFork lHash rHash) = do
|
||||||
|
left <- fromMaybe (errorWithoutStackTrace "BUG: stored hash not found") <$> loadTree conn lHash
|
||||||
|
right <- fromMaybe (errorWithoutStackTrace "BUG: stored hash not found") <$> loadTree conn rHash
|
||||||
|
return (Fork left right)
|
||||||
|
|
||||||
|
-- | Store all nodes of a Merkle DAG by traversing the Term and building/storing nodes.
|
||||||
|
-- Returns the hash of the root node.
|
||||||
|
storeMerkleNodes :: Connection -> T -> IO MerkleHash
|
||||||
|
storeMerkleNodes _ Leaf = return $ nodeHash NLeaf
|
||||||
|
storeMerkleNodes conn (Stem t) = do
|
||||||
|
childHash <- storeMerkleNodes conn t
|
||||||
|
let thisNode = NStem childHash
|
||||||
|
putMerkleNode conn thisNode
|
||||||
|
return $ nodeHash thisNode
|
||||||
|
storeMerkleNodes conn (Fork l r) = do
|
||||||
|
leftHash <- storeMerkleNodes conn l
|
||||||
|
rightHash <- storeMerkleNodes conn r
|
||||||
|
let thisNode = NFork leftHash rightHash
|
||||||
|
putMerkleNode conn thisNode
|
||||||
|
return $ nodeHash thisNode
|
||||||
|
|
||||||
|
|
||||||
|
-- | Insert a Merkle node into the store (idempotent).
|
||||||
|
putMerkleNode :: Connection -> Node -> IO ()
|
||||||
|
putMerkleNode conn node =
|
||||||
|
execute conn "INSERT OR IGNORE INTO merkle_nodes (hash, node_data) VALUES (?, ?)"
|
||||||
|
(nodeHash node, serializeNode node)
|
||||||
|
|
||||||
|
-- | Retrieve a Merkle node by its hash.
|
||||||
|
getNodeMerkle :: Connection -> MerkleHash -> IO (Maybe Node)
|
||||||
|
getNodeMerkle conn h =
|
||||||
|
queryMaybeOne conn "SELECT node_data FROM merkle_nodes WHERE hash = ?" (Only h) >>= \case
|
||||||
|
Just (StoredNode bs) -> return $ Just (deserializeNode bs)
|
||||||
|
Nothing -> return Nothing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
hashToTerm :: Connection -> Text -> IO (Maybe StoredTerm)
|
||||||
|
hashToTerm conn hashText =
|
||||||
|
queryMaybeOne conn (selectStoredTermFields <> " WHERE hash = ?") (Only hashText)
|
||||||
|
|
||||||
|
nameToTerm :: Connection -> Text -> IO (Maybe StoredTerm)
|
||||||
|
nameToTerm conn nameText =
|
||||||
|
queryMaybeOne conn
|
||||||
|
(selectStoredTermFields <> " WHERE (names = ? OR names LIKE ? OR names LIKE ? OR names LIKE ?) ORDER BY created_at DESC LIMIT 1")
|
||||||
|
(nameText, nameText <> T.pack ",%", T.pack "%," <> nameText <> T.pack ",%", T.pack "%," <> nameText)
|
||||||
|
|
||||||
|
listStoredTerms :: Connection -> IO [StoredTerm]
|
||||||
|
listStoredTerms conn =
|
||||||
|
query_ conn (selectStoredTermFields <> " ORDER BY created_at DESC")
|
||||||
|
|
||||||
|
storeEnvironment :: Connection -> Env -> IO ()
|
||||||
|
storeEnvironment conn env = do
|
||||||
|
let defs = Map.toList $ Map.delete "!result" env
|
||||||
|
let groupedDefs = Map.toList $ Map.fromListWith (++) [(term, [name]) | (name, term) <- defs]
|
||||||
|
|
||||||
|
forM_ groupedDefs $ \(term, namesList) -> case namesList of
|
||||||
|
n:ns -> void $ storeTerm conn namesList term
|
||||||
|
_ -> errorWithoutStackTrace "storeEnvironment: empty names list"
|
||||||
|
|
||||||
|
loadTerm :: Connection -> String -> IO (Maybe T)
|
||||||
|
loadTerm conn identifier = do
|
||||||
|
result <- getTerm conn (T.pack identifier)
|
||||||
|
case result of
|
||||||
|
Just storedTerm -> loadTree conn (termHash storedTerm)
|
||||||
|
Nothing -> return Nothing
|
||||||
|
|
||||||
|
getTerm :: Connection -> Text -> IO (Maybe StoredTerm)
|
||||||
|
getTerm conn identifier = do
|
||||||
|
if '#' `elem` (T.unpack identifier)
|
||||||
|
then hashToTerm conn (T.pack $ drop 1 (T.unpack identifier))
|
||||||
|
else nameToTerm conn identifier
|
||||||
|
|
||||||
|
loadEnvironment :: Connection -> IO Env
|
||||||
|
loadEnvironment conn = do
|
||||||
|
terms <- listStoredTerms conn
|
||||||
|
foldM addTermToEnv Map.empty terms
|
||||||
|
where
|
||||||
|
addTermToEnv env storedTerm = do
|
||||||
|
maybeT <- loadTree conn (termHash storedTerm)
|
||||||
|
case maybeT of
|
||||||
|
Just t -> do
|
||||||
|
let namesList = parseNameList (termNames storedTerm)
|
||||||
|
return $ foldl (\e name -> Map.insert (T.unpack name) t e) env namesList
|
||||||
|
Nothing -> return env
|
||||||
|
|
||||||
|
termVersions :: Connection -> String -> IO [(Text, T, Integer)]
|
||||||
|
termVersions conn name = do
|
||||||
|
let nameText = T.pack name
|
||||||
|
results <- query conn
|
||||||
|
("SELECT hash, created_at FROM terms WHERE (names = ? OR names LIKE ? OR names LIKE ? OR names LIKE ?) ORDER BY created_at DESC")
|
||||||
|
(nameText, nameText <> T.pack ",%", T.pack "%," <> nameText <> T.pack ",%", T.pack "%," <> nameText)
|
||||||
|
|
||||||
|
catMaybes <$> mapM (\(hashVal, timestamp) -> do
|
||||||
|
maybeT <- loadTree conn hashVal
|
||||||
|
return $ fmap (\t -> (hashVal, t, timestamp)) maybeT
|
||||||
|
) results
|
||||||
|
|
||||||
|
setTag :: Connection -> Text -> Text -> IO ()
|
||||||
|
setTag conn hash tagValue = do
|
||||||
|
exists <- termExists conn hash
|
||||||
|
if exists
|
||||||
|
then do
|
||||||
|
currentTagsQuery <- query conn "SELECT tags FROM terms WHERE hash = ?" (Only hash) :: IO [Only Text]
|
||||||
|
case currentTagsQuery of
|
||||||
|
[Only tagsText] -> do
|
||||||
|
let tagsList = parseNameList tagsText
|
||||||
|
newTagsList = tagValue : tagsList
|
||||||
|
newTags = serializeNameList newTagsList
|
||||||
|
execute conn "UPDATE terms SET tags = ? WHERE hash = ?" (newTags, hash)
|
||||||
|
_ -> putStrLn $ "Term with hash " ++ T.unpack hash ++ " not found (should not happen if exists is true)"
|
||||||
|
else
|
||||||
|
putStrLn $ "Term with hash " ++ T.unpack hash ++ " does not exist"
|
||||||
|
|
||||||
|
termExists :: Connection -> Text -> IO Bool
|
||||||
|
termExists conn hash = do
|
||||||
|
results <- query conn "SELECT 1 FROM terms WHERE hash = ? LIMIT 1" (Only hash) :: IO [[Int]]
|
||||||
|
return $ not (null results)
|
||||||
|
|
||||||
|
termToTags :: Connection -> Text -> IO [Text]
|
||||||
|
termToTags conn hash = do
|
||||||
|
tagsQuery <- query conn "SELECT tags FROM terms WHERE hash = ?" (Only hash) :: IO [Only Text]
|
||||||
|
case tagsQuery of
|
||||||
|
[Only tagsText] -> return $ parseNameList tagsText
|
||||||
|
_ -> return []
|
||||||
|
|
||||||
|
tagToTerm :: Connection -> Text -> IO [StoredTerm]
|
||||||
|
tagToTerm conn tagValue = do
|
||||||
|
let pattern = "%" <> tagValue <> "%"
|
||||||
|
query conn (selectStoredTermFields <> " WHERE tags LIKE ? ORDER BY created_at DESC") (Only pattern)
|
||||||
|
|
||||||
|
allTermTags :: Connection -> IO [StoredTerm]
|
||||||
|
allTermTags conn = do
|
||||||
|
query_ conn (selectStoredTermFields <> " WHERE tags IS NOT NULL AND tags != '' ORDER BY created_at DESC")
|
||||||
|
|
||||||
|
selectStoredTermFields :: Query
|
||||||
|
selectStoredTermFields = "SELECT hash, names, metadata, created_at, tags FROM terms"
|
||||||
|
|
||||||
|
queryMaybeOne :: (FromRow r, ToRow q) => Connection -> Query -> q -> IO (Maybe r)
|
||||||
|
queryMaybeOne conn qry params = do
|
||||||
|
results <- query conn qry params
|
||||||
|
case results of
|
||||||
|
[row] -> return $ Just row
|
||||||
|
_ -> return Nothing
|
||||||
559
src/Eval.hs
559
src/Eval.hs
@@ -1,35 +1,60 @@
|
|||||||
module Eval where
|
module Eval where
|
||||||
|
|
||||||
|
import ContentStore
|
||||||
import Parser
|
import Parser
|
||||||
import Research
|
import Research
|
||||||
|
|
||||||
import Data.List (partition, (\\))
|
import Control.Monad (foldM)
|
||||||
|
import Data.List (partition, (\\), elemIndex, foldl')
|
||||||
import Data.Map (Map)
|
import Data.Map (Map)
|
||||||
|
import Data.Set (Set)
|
||||||
|
import Database.SQLite.Simple
|
||||||
|
|
||||||
|
import qualified Data.Foldable as F
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
|
import qualified Data.Text as T
|
||||||
|
|
||||||
|
data DB
|
||||||
|
= BVar Int -- bound (0 = nearest binder)
|
||||||
|
| BFree String -- free/global
|
||||||
|
| BLam DB
|
||||||
|
| BApp DB DB
|
||||||
|
| BLeaf
|
||||||
|
| BStem DB
|
||||||
|
| BFork DB DB
|
||||||
|
| BStr String
|
||||||
|
| BInt Integer
|
||||||
|
| BList [DB]
|
||||||
|
| BEmpty
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
type Uses = [Bool]
|
||||||
|
|
||||||
evalSingle :: Env -> TricuAST -> Env
|
evalSingle :: Env -> TricuAST -> Env
|
||||||
evalSingle env term
|
evalSingle env term
|
||||||
| SDef name [] body <- term
|
| SDef name [] body <- term
|
||||||
= case Map.lookup name env of
|
= case Map.lookup name env of
|
||||||
Just existingValue
|
Just existingValue
|
||||||
| existingValue == evalAST env body -> env
|
| existingValue == evalASTSync env body -> env
|
||||||
| otherwise -> errorWithoutStackTrace $
|
| otherwise
|
||||||
"Unable to rebind immutable identifier: " ++ name
|
-> let res = evalASTSync env body
|
||||||
Nothing ->
|
in Map.insert "!result" res (Map.insert name res env)
|
||||||
let res = evalAST env body
|
Nothing
|
||||||
in Map.insert "!result" res (Map.insert name res env)
|
-> let res = evalASTSync env body
|
||||||
|
in Map.insert "!result" res (Map.insert name res env)
|
||||||
| SApp func arg <- term
|
| SApp func arg <- term
|
||||||
= let res = apply (evalAST env func) (evalAST env arg)
|
= let res = apply (evalASTSync env func) (evalASTSync env arg)
|
||||||
in Map.insert "!result" res env
|
in Map.insert "!result" res env
|
||||||
| SVar name <- term
|
| SVar name Nothing <- term
|
||||||
= case Map.lookup name env of
|
= case Map.lookup name env of
|
||||||
Just v -> Map.insert "!result" v env
|
Just v -> Map.insert "!result" v env
|
||||||
Nothing ->
|
Nothing -> errorWithoutStackTrace $ "Variable " ++ name ++ " not defined"
|
||||||
errorWithoutStackTrace $ "Variable `" ++ name ++ "` not defined\n\
|
| SVar name (Just hash) <- term
|
||||||
\This error should never occur here. Please report this as an issue."
|
= errorWithoutStackTrace $ "Hash-specific variable lookup not supported in local evaluation: " ++ name ++ "#" ++ hash
|
||||||
| otherwise
|
| otherwise
|
||||||
= Map.insert "!result" (evalAST env term) env
|
= let res = evalASTSync env term
|
||||||
|
in Map.insert "!result" res env
|
||||||
|
|
||||||
evalTricu :: Env -> [TricuAST] -> Env
|
evalTricu :: Env -> [TricuAST] -> Env
|
||||||
evalTricu env x = go env (reorderDefs env x)
|
evalTricu env x = go env (reorderDefs env x)
|
||||||
@@ -41,80 +66,160 @@ evalTricu env x = go env (reorderDefs env x)
|
|||||||
go env (x:xs) =
|
go env (x:xs) =
|
||||||
evalTricu (evalSingle env x) xs
|
evalTricu (evalSingle env x) xs
|
||||||
|
|
||||||
evalAST :: Env -> TricuAST -> T
|
evalASTSync :: Env -> TricuAST -> T
|
||||||
evalAST env term
|
evalASTSync env term = case term of
|
||||||
| SLambda _ _ <- term = evalAST env (elimLambda term)
|
SLambda _ _ -> evalASTSync env (elimLambda term)
|
||||||
| SVar name <- term = evalVar name
|
SVar name Nothing -> case Map.lookup name env of
|
||||||
| TLeaf <- term = Leaf
|
Just v -> v
|
||||||
| TStem t <- term = Stem (evalAST env t)
|
Nothing -> errorWithoutStackTrace $ "Variable " ++ name ++ " not defined"
|
||||||
| TFork t u <- term = Fork (evalAST env t) (evalAST env u)
|
SVar name (Just hash) ->
|
||||||
| SApp t u <- term = apply (evalAST env t) (evalAST env u)
|
case Map.lookup (name ++ "#" ++ hash) env of
|
||||||
| SStr s <- term = ofString s
|
Just v -> v
|
||||||
| SInt n <- term = ofNumber n
|
Nothing -> errorWithoutStackTrace $
|
||||||
| SList xs <- term = ofList (map (evalAST env) xs)
|
"Variable " ++ name ++ " with hash " ++ hash ++ " not found in environment"
|
||||||
| SEmpty <- term = Leaf
|
TLeaf -> Leaf
|
||||||
| otherwise = errorWithoutStackTrace "Unexpected AST term"
|
TStem t -> Stem (evalASTSync env t)
|
||||||
where
|
TFork t u -> Fork (evalASTSync env t) (evalASTSync env u)
|
||||||
evalVar name = Map.findWithDefault
|
SApp t u -> apply (evalASTSync env t) (evalASTSync env u)
|
||||||
(errorWithoutStackTrace $ "Variable " ++ name ++ " not defined")
|
SStr s -> ofString s
|
||||||
name env
|
SInt n -> ofNumber n
|
||||||
|
SList xs -> ofList (map (evalASTSync env) xs)
|
||||||
|
SEmpty -> Leaf
|
||||||
|
_ -> errorWithoutStackTrace $ "Unexpected AST term: " ++ show term
|
||||||
|
|
||||||
|
evalAST :: Maybe Connection -> Map.Map String T.Text -> TricuAST -> IO T
|
||||||
|
evalAST mconn selectedVersions ast = do
|
||||||
|
let varNames = collectVarNames ast
|
||||||
|
resolvedEnv <- resolveTermsFromStore mconn selectedVersions varNames
|
||||||
|
return $ evalASTSync resolvedEnv ast
|
||||||
|
|
||||||
|
collectVarNames :: TricuAST -> [(String, Maybe String)]
|
||||||
|
collectVarNames = go []
|
||||||
|
where
|
||||||
|
go acc (SVar name mhash) = (name, mhash) : acc
|
||||||
|
go acc (SApp t u) = go (go acc t) u
|
||||||
|
go acc (SLambda vars body) =
|
||||||
|
let boundVars = Set.fromList vars
|
||||||
|
collected = go [] body
|
||||||
|
in acc ++ filter (\(name, _) -> not $ Set.member name boundVars) collected
|
||||||
|
go acc (TStem t) = go acc t
|
||||||
|
go acc (TFork t u) = go (go acc t) u
|
||||||
|
go acc (SList xs) = foldl' go acc xs
|
||||||
|
go acc _ = acc
|
||||||
|
|
||||||
|
resolveTermsFromStore :: Maybe Connection -> Map.Map String T.Text -> [(String, Maybe String)] -> IO Env
|
||||||
|
resolveTermsFromStore Nothing _ _ = return Map.empty
|
||||||
|
resolveTermsFromStore (Just conn) selectedVersions varNames = do
|
||||||
|
foldM (\env (name, mhash) -> do
|
||||||
|
term <- resolveTermFromStore conn selectedVersions name mhash
|
||||||
|
case term of
|
||||||
|
Just t -> return $ Map.insert (getVarKey name mhash) t env
|
||||||
|
Nothing -> return env
|
||||||
|
) Map.empty varNames
|
||||||
|
where
|
||||||
|
getVarKey name Nothing = name
|
||||||
|
getVarKey name (Just hash) = name ++ "#" ++ hash
|
||||||
|
|
||||||
|
resolveTermFromStore :: Connection -> Map.Map String T.Text -> String -> Maybe String -> IO (Maybe T)
|
||||||
|
resolveTermFromStore conn selectedVersions name mhash = case mhash of
|
||||||
|
Just hashPrefix -> do
|
||||||
|
versions <- termVersions conn name
|
||||||
|
let matchingVersions = filter (\(hash, _, _) ->
|
||||||
|
T.isPrefixOf (T.pack hashPrefix) hash) versions
|
||||||
|
case matchingVersions of
|
||||||
|
[] -> return Nothing
|
||||||
|
[(_, term, _)] -> return $ Just term
|
||||||
|
_ -> return Nothing -- Ambiguous or too many matches
|
||||||
|
Nothing -> case Map.lookup name selectedVersions of
|
||||||
|
Just hash -> loadTree conn hash
|
||||||
|
Nothing -> do
|
||||||
|
versions <- termVersions conn name
|
||||||
|
case versions of
|
||||||
|
[] -> return Nothing
|
||||||
|
[(_, term, _)] -> return $ Just term
|
||||||
|
_ -> return $ Just $ (\(_, t, _) -> t) $ case versions of (_:_) -> head versions; _ -> error "resolveTermFromStore: unexpected empty versions list"
|
||||||
|
|
||||||
elimLambda :: TricuAST -> TricuAST
|
elimLambda :: TricuAST -> TricuAST
|
||||||
elimLambda = go
|
elimLambda = go
|
||||||
where
|
where
|
||||||
-- η-reduction
|
go term
|
||||||
go (SLambda [v] (SApp f (SVar x)))
|
| etaReduction term = go (etaReduceResult term)
|
||||||
| v == x && not (isFree v f) = elimLambda f
|
| triagePattern term = _TRI
|
||||||
-- Triage optimization
|
| composePattern term = _B
|
||||||
go (SLambda [a] (SLambda [b] (SLambda [c] body)))
|
| lambdaList term = go (lambdaListResult term)
|
||||||
| body == triageBody = _TRIAGE
|
| nestedLambda term = nestedLambdaResult term
|
||||||
|
| application term = applicationResult term
|
||||||
|
| isSList term = slistTransform term
|
||||||
|
| otherwise = term
|
||||||
|
|
||||||
|
etaReduction (SLambda [v] (SApp f (SVar x Nothing))) = v == x && not (usesBinder v f)
|
||||||
|
etaReduction _ = False
|
||||||
|
|
||||||
|
-- triage: \a b c -> TLeaf (TLeaf a b) c (checked in DB with a↦2, b↦1, c↦0)
|
||||||
|
triagePattern (SLambda [a] (SLambda [b] (SLambda [c] body))) =
|
||||||
|
toDB [c,b,a] body == triageBodyDB
|
||||||
|
triagePattern _ = False
|
||||||
|
|
||||||
|
-- compose: \f g x -> f (g x) (checked in DB with f↦2, g↦1, x↦0)
|
||||||
|
composePattern (SLambda [f] (SLambda [g] (SLambda [x] body))) =
|
||||||
|
toDB [x,g,f] body == composeBodyDB
|
||||||
|
composePattern _ = False
|
||||||
|
|
||||||
|
lambdaList (SLambda [_] (SList _)) = True
|
||||||
|
lambdaList _ = False
|
||||||
|
|
||||||
|
nestedLambda (SLambda (_:_) _) = True
|
||||||
|
nestedLambda _ = False
|
||||||
|
|
||||||
|
application (SApp _ _) = True
|
||||||
|
application _ = False
|
||||||
|
|
||||||
|
-- rewrites
|
||||||
|
etaReduceResult (SLambda [_] (SApp f _)) = f
|
||||||
|
|
||||||
|
lambdaListResult (SLambda [v] (SList xs)) =
|
||||||
|
SLambda [v] (foldr wrapTLeaf TLeaf xs)
|
||||||
where
|
where
|
||||||
triageBody =
|
wrapTLeaf m r = SApp (SApp TLeaf m) r
|
||||||
SApp (SApp TLeaf (SApp (SApp TLeaf (SVar a)) (SVar b))) (SVar c)
|
|
||||||
-- Composition optimization
|
|
||||||
go (SLambda [f] (SLambda [g] (SLambda [x] body)))
|
|
||||||
| body == SApp (SVar f) (SApp (SVar g) (SVar x)) = _B
|
|
||||||
-- General elimination
|
|
||||||
go (SLambda [v] (SList xs))
|
|
||||||
= elimLambda (SLambda [v] (foldr wrapTLeaf TLeaf xs))
|
|
||||||
where wrapTLeaf m r = SApp (SApp TLeaf m) r
|
|
||||||
go (SLambda (v:vs) body)
|
|
||||||
| null vs = toSKI v (elimLambda body)
|
|
||||||
| otherwise = elimLambda (SLambda [v] (SLambda vs body))
|
|
||||||
go (SApp f g) = SApp (elimLambda f) (elimLambda g)
|
|
||||||
go x = x
|
|
||||||
|
|
||||||
toSKI x (SVar y)
|
nestedLambdaResult (SLambda (v:vs) body)
|
||||||
| x == y = _I
|
| null vs =
|
||||||
| otherwise = SApp _K (SVar y)
|
let body' = go body
|
||||||
toSKI x t@(SApp n u)
|
db = toDB [v] body'
|
||||||
| not (isFree x t) = SApp _K t
|
in toSKIKiselyov db
|
||||||
| otherwise = SApp (SApp _S (toSKI x n)) (toSKI x u)
|
| otherwise = go (SLambda [v] (SLambda vs body))
|
||||||
toSKI x t
|
|
||||||
| not (isFree x t) = SApp _K t
|
|
||||||
| otherwise = errorWithoutStackTrace "Unhandled toSKI conversion"
|
|
||||||
|
|
||||||
_S = parseSingle "t (t (t t t)) t"
|
applicationResult (SApp f g) = SApp (go f) (go g)
|
||||||
_K = parseSingle "t t"
|
|
||||||
_I = parseSingle "t (t (t t)) t"
|
isSList (SList _) = True
|
||||||
_B = parseSingle "t (t (t t (t (t (t t t)) t))) (t t)"
|
isSList _ = False
|
||||||
_TRIAGE = parseSingle "t (t (t t (t (t (t t t))))) t"
|
|
||||||
|
slistTransform :: TricuAST -> TricuAST
|
||||||
|
slistTransform (SList xs) = foldr (\m r -> SApp (SApp TLeaf (go m)) r) TLeaf xs
|
||||||
|
slistTransform ast = ast -- Should not be reached if isSList is the guard
|
||||||
|
|
||||||
|
_S = parseSingle "t (t (t t t)) t"
|
||||||
|
_K = parseSingle "t t"
|
||||||
|
_I = parseSingle "t (t (t t)) t"
|
||||||
|
_R = parseSingle "(t (t (t t (t (t (t (t (t (t (t t (t (t (t t t)) t))) (t (t (t t (t t))) (t (t (t t t)) t)))) (t t (t t))))))) (t t))"
|
||||||
|
_C = parseSingle "(t (t (t (t (t t (t (t (t t t)) t))) (t (t (t t (t t))) (t (t (t t t)) t)))) (t t (t t)))"
|
||||||
|
_B = parseSingle "t (t (t t (t (t (t t t)) t))) (t t)"
|
||||||
|
_T = SApp _C _I
|
||||||
|
_TRI = parseSingle "t (t (t t (t (t (t t t))))) t"
|
||||||
|
|
||||||
|
triageBody a b c = SApp (SApp TLeaf (SApp (SApp TLeaf (SVar a Nothing)) (SVar b Nothing))) (SVar c Nothing)
|
||||||
|
composeBody f g x = SApp (SVar f Nothing) (SApp (SVar g Nothing) (SVar x Nothing))
|
||||||
|
|
||||||
isFree :: String -> TricuAST -> Bool
|
isFree :: String -> TricuAST -> Bool
|
||||||
isFree x = Set.member x . freeVars
|
isFree x t = Set.member x (freeVars t)
|
||||||
|
|
||||||
freeVars :: TricuAST -> Set.Set String
|
-- Keep old freeVars for compatibility with reorderDefs which still uses TricuAST
|
||||||
freeVars (SVar v ) = Set.singleton v
|
freeVars :: TricuAST -> Set String
|
||||||
freeVars (SInt _ ) = Set.empty
|
freeVars (SVar v Nothing) = Set.singleton v
|
||||||
freeVars (SStr _ ) = Set.empty
|
freeVars (SVar v (Just _)) = Set.singleton v
|
||||||
freeVars (SList s ) = foldMap freeVars s
|
freeVars (SApp t u) = Set.union (freeVars t) (freeVars u)
|
||||||
freeVars (SApp f a ) = freeVars f <> freeVars a
|
freeVars (SLambda vs body) = Set.difference (freeVars body) (Set.fromList vs)
|
||||||
freeVars TLeaf = Set.empty
|
freeVars _ = Set.empty
|
||||||
freeVars (SDef _ _ b) = freeVars b
|
|
||||||
freeVars (TStem t ) = freeVars t
|
|
||||||
freeVars (TFork l r ) = freeVars l <> freeVars r
|
|
||||||
freeVars (SLambda v b ) = foldr Set.delete (freeVars b) v
|
|
||||||
freeVars _ = Set.empty
|
|
||||||
|
|
||||||
reorderDefs :: Env -> [TricuAST] -> [TricuAST]
|
reorderDefs :: Env -> [TricuAST] -> [TricuAST]
|
||||||
reorderDefs env defs
|
reorderDefs env defs
|
||||||
@@ -131,7 +236,7 @@ reorderDefs env defs
|
|||||||
graph = buildDepGraph defsOnly
|
graph = buildDepGraph defsOnly
|
||||||
sortedDefs = sortDeps graph
|
sortedDefs = sortDeps graph
|
||||||
defMap = Map.fromList [(name, def) | def@(SDef name _ _) <- defsOnly]
|
defMap = Map.fromList [(name, def) | def@(SDef name _ _) <- defsOnly]
|
||||||
orderedDefs = map (\name -> defMap Map.! name) sortedDefs
|
orderedDefs = map (defMap Map.!) sortedDefs
|
||||||
|
|
||||||
freeVarsDefs = foldMap snd defsWithFreeVars
|
freeVarsDefs = foldMap snd defsWithFreeVars
|
||||||
freeVarsOthers = foldMap freeVars others
|
freeVarsOthers = foldMap freeVars others
|
||||||
@@ -139,8 +244,8 @@ reorderDefs env defs
|
|||||||
validNames = Set.fromList defNames `Set.union` Set.fromList (Map.keys env)
|
validNames = Set.fromList defNames `Set.union` Set.fromList (Map.keys env)
|
||||||
missingDeps = Set.toList (allFreeVars `Set.difference` validNames)
|
missingDeps = Set.toList (allFreeVars `Set.difference` validNames)
|
||||||
|
|
||||||
isDef (SDef _ _ _) = True
|
isDef SDef {} = True
|
||||||
isDef _ = False
|
isDef _ = False
|
||||||
|
|
||||||
buildDepGraph :: [TricuAST] -> Map.Map String (Set.Set String)
|
buildDepGraph :: [TricuAST] -> Map.Map String (Set.Set String)
|
||||||
buildDepGraph topDefs
|
buildDepGraph topDefs
|
||||||
@@ -195,3 +300,293 @@ mainResult :: Env -> T
|
|||||||
mainResult r = case Map.lookup "main" r of
|
mainResult r = case Map.lookup "main" r of
|
||||||
Just a -> a
|
Just a -> a
|
||||||
Nothing -> errorWithoutStackTrace "No valid definition for `main` found."
|
Nothing -> errorWithoutStackTrace "No valid definition for `main` found."
|
||||||
|
|
||||||
|
findVarNames :: TricuAST -> [String]
|
||||||
|
findVarNames ast = case ast of
|
||||||
|
SVar name _ -> [name]
|
||||||
|
SApp a b -> findVarNames a ++ findVarNames b
|
||||||
|
SLambda args body -> findVarNames body \\ args
|
||||||
|
SDef name args body -> name : (findVarNames body \\ args)
|
||||||
|
_ -> []
|
||||||
|
|
||||||
|
-- Convert named TricuAST to De Bruijn form
|
||||||
|
toDB :: [String] -> TricuAST -> DB
|
||||||
|
toDB env = \case
|
||||||
|
SVar v _ -> maybe (BFree v) BVar (elemIndex v env)
|
||||||
|
SLambda vs b ->
|
||||||
|
let env' = reverse vs ++ env
|
||||||
|
body = toDB env' b
|
||||||
|
in foldr (\_ acc -> BLam acc) body vs
|
||||||
|
SApp f a -> BApp (toDB env f) (toDB env a)
|
||||||
|
TLeaf -> BLeaf
|
||||||
|
TStem t -> BStem (toDB env t)
|
||||||
|
TFork l r -> BFork (toDB env l) (toDB env r)
|
||||||
|
SStr s -> BStr s
|
||||||
|
SInt n -> BInt n
|
||||||
|
SList xs -> BList (map (toDB env) xs)
|
||||||
|
SEmpty -> BEmpty
|
||||||
|
SDef{} -> error "toDB: unexpected SDef at this stage"
|
||||||
|
SImport _ _ -> BEmpty
|
||||||
|
|
||||||
|
-- Does a term depend on the current binder (level 0)?
|
||||||
|
dependsOnLevel :: Int -> DB -> Bool
|
||||||
|
dependsOnLevel lvl = \case
|
||||||
|
BVar k -> k == lvl
|
||||||
|
BLam t -> dependsOnLevel (lvl + 1) t
|
||||||
|
BApp f a -> dependsOnLevel lvl f || dependsOnLevel lvl a
|
||||||
|
BStem t -> dependsOnLevel lvl t
|
||||||
|
BFork l r -> dependsOnLevel lvl l || dependsOnLevel lvl r
|
||||||
|
BList xs -> any (dependsOnLevel lvl) xs
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
-- Collect free *global* names (i.e., unbound)
|
||||||
|
freeDBNames :: DB -> Set String
|
||||||
|
freeDBNames = \case
|
||||||
|
BFree s -> Set.singleton s
|
||||||
|
BVar _ -> mempty
|
||||||
|
BLam t -> freeDBNames t
|
||||||
|
BApp f a -> freeDBNames f <> freeDBNames a
|
||||||
|
BLeaf -> mempty
|
||||||
|
BStem t -> freeDBNames t
|
||||||
|
BFork l r -> freeDBNames l <> freeDBNames r
|
||||||
|
BStr _ -> mempty
|
||||||
|
BInt _ -> mempty
|
||||||
|
BList xs -> foldMap freeDBNames xs
|
||||||
|
BEmpty -> mempty
|
||||||
|
|
||||||
|
-- Helper: “is the binder named v used in body?”
|
||||||
|
usesBinder :: String -> TricuAST -> Bool
|
||||||
|
usesBinder v body = dependsOnLevel 0 (toDB [v] body)
|
||||||
|
|
||||||
|
-- Expected DB bodies for the named special patterns (under env [a,b,c] -> indices 2,1,0)
|
||||||
|
triageBodyDB :: DB
|
||||||
|
triageBodyDB =
|
||||||
|
BApp (BApp BLeaf (BApp (BApp BLeaf (BVar 2)) (BVar 1))) (BVar 0)
|
||||||
|
|
||||||
|
composeBodyDB :: DB
|
||||||
|
composeBodyDB =
|
||||||
|
BApp (BVar 2) (BApp (BVar 1) (BVar 0))
|
||||||
|
|
||||||
|
-- Convert DB -> TricuAST for subterms that contain NO binders (no BLam, no BVar)
|
||||||
|
fromDBClosed :: DB -> TricuAST
|
||||||
|
fromDBClosed = \case
|
||||||
|
BFree s -> SVar s Nothing
|
||||||
|
BApp f a -> SApp (fromDBClosed f) (fromDBClosed a)
|
||||||
|
BLeaf -> TLeaf
|
||||||
|
BStem t -> TStem (fromDBClosed t)
|
||||||
|
BFork l r -> TFork (fromDBClosed l) (fromDBClosed r)
|
||||||
|
BStr s -> SStr s
|
||||||
|
BInt n -> SInt n
|
||||||
|
BList xs -> SList (map fromDBClosed xs)
|
||||||
|
BEmpty -> SEmpty
|
||||||
|
-- Anything bound would be a logic error if we call this correctly.
|
||||||
|
BLam _ -> error "fromDBClosed: unexpected BLam"
|
||||||
|
BVar _ -> error "fromDBClosed: unexpected bound variable"
|
||||||
|
|
||||||
|
-- DB-native bracket abstraction over the innermost binder (level 0).
|
||||||
|
-- This mirrors your old toSKI, but is purely index-driven.
|
||||||
|
toSKIDB :: DB -> TricuAST
|
||||||
|
toSKIDB t
|
||||||
|
| not (dependsOnLevel 0 t) = SApp _K (fromDBClosed t)
|
||||||
|
toSKIDB (BVar 0) = _I
|
||||||
|
toSKIDB (BApp n u) = SApp (SApp _S (toSKIDB n)) (toSKIDB u)
|
||||||
|
toSKIDB (BList xs) =
|
||||||
|
let anyUses = any (dependsOnLevel 0) xs
|
||||||
|
in if not anyUses
|
||||||
|
then SApp _K (SList (map fromDBClosed xs))
|
||||||
|
else SList (map toSKIDB xs)
|
||||||
|
toSKIDB other
|
||||||
|
| not (dependsOnLevel 0 other) = SApp _K (fromDBClosed other)
|
||||||
|
toSKIDB other = _K `SApp` TLeaf
|
||||||
|
|
||||||
|
app2 :: TricuAST -> TricuAST -> TricuAST
|
||||||
|
app2 f x = SApp f x
|
||||||
|
|
||||||
|
app3 :: TricuAST -> TricuAST -> TricuAST -> TricuAST
|
||||||
|
app3 f x y = SApp (SApp f x) y
|
||||||
|
|
||||||
|
-- Core converter that *does not* perform the λ-step; it just returns (Γ, d).
|
||||||
|
-- Supported shapes: variables, applications, closed literals (Leaf/Int/Str/Empty),
|
||||||
|
-- closed lists. For anything where the binder occurs under structural nodes
|
||||||
|
-- (Stem/Fork/List-with-use), we deliberately bail so the caller can fall back.
|
||||||
|
kisConv :: DB -> Either String (Uses, TricuAST)
|
||||||
|
kisConv = \case
|
||||||
|
BVar 0 -> Right ([True], _I)
|
||||||
|
BVar n | n > 0 -> do
|
||||||
|
(g,d) <- kisConv (BVar (n - 1))
|
||||||
|
Right (False:g, d)
|
||||||
|
BApp e1 e2 -> do
|
||||||
|
(g1,d1) <- kisConv e1
|
||||||
|
(g2,d2) <- kisConv e2
|
||||||
|
let g = zipWithDefault False (||) g1 g2 -- <— propagate Γ outside (#)
|
||||||
|
d = kisHash (g1,d1) (g2,d2) -- <— (#) yields only the term
|
||||||
|
Right (g, d)
|
||||||
|
-- Treat closed constants as free 'combinator leaves' (no binder use).
|
||||||
|
BLeaf -> Right ([], TLeaf)
|
||||||
|
BStr s -> Right ([], SStr s)
|
||||||
|
BInt n -> Right ([], SInt n)
|
||||||
|
BEmpty -> Right ([], SEmpty)
|
||||||
|
-- Closed list: allowed. If binder is used anywhere, we punt to fallback.
|
||||||
|
BList xs
|
||||||
|
| any (dependsOnLevel 0) xs -> Left "List with binder use: fallback"
|
||||||
|
| otherwise -> Right ([], SList (map fromDBClosed xs))
|
||||||
|
-- For structural nodes, only allow if *closed* wrt the binder.
|
||||||
|
BStem t
|
||||||
|
| dependsOnLevel 0 t -> Left "Stem with binder use: fallback"
|
||||||
|
| otherwise -> Right ([], TStem (fromDBClosed t))
|
||||||
|
BFork l r
|
||||||
|
| dependsOnLevel 0 l || dependsOnLevel 0 r -> Left "Fork with binder use: fallback"
|
||||||
|
| otherwise -> Right ([], TFork (fromDBClosed l) (fromDBClosed r))
|
||||||
|
-- We shouldn’t see BLam under elim; treat as unsupported so we fallback.
|
||||||
|
BLam _ -> Left "Nested lambda under body: fallback"
|
||||||
|
BFree s -> Right ([], SVar s Nothing)
|
||||||
|
|
||||||
|
-- Application combiner with K-optimization (lazy weakening).
|
||||||
|
-- Mirrors Lynn’s 'optK' rules: choose among S, B, C, R based on leading flags.
|
||||||
|
-- η-aware (#) with K-optimization (adapted from TS kiselyov_eta)
|
||||||
|
kisHash :: (Uses, TricuAST) -> (Uses, TricuAST) -> TricuAST
|
||||||
|
kisHash (g1, d1) (g2, d2) =
|
||||||
|
case g1 of
|
||||||
|
[] -> case g2 of
|
||||||
|
[] -> SApp d1 d2
|
||||||
|
True:gs2 -> if isId2 (g2, d2)
|
||||||
|
then d1
|
||||||
|
else kisHash ([], SApp _B d1) (gs2, d2)
|
||||||
|
False:gs2 -> kisHash ([], d1) (gs2, d2)
|
||||||
|
|
||||||
|
True:gs1 -> case g2 of
|
||||||
|
[] -> if isId2 (g1, d1)
|
||||||
|
then SApp _T d2
|
||||||
|
else kisHash ([], SApp _R d2) (gs1, d1)
|
||||||
|
_ ->
|
||||||
|
if isId2 (g1, d1) && case g2 of { False:_ -> True; _ -> False }
|
||||||
|
then kisHash ([], _T) (drop1 g2, d2)
|
||||||
|
else
|
||||||
|
-- NEW: coalesce the longest run of identical head pairs and apply bulk op once
|
||||||
|
let ((h1, h2), count) = headPairRun g1 g2
|
||||||
|
g1' = drop count g1
|
||||||
|
g2' = drop count g2
|
||||||
|
in case (h1, h2) of
|
||||||
|
(False, False) ->
|
||||||
|
kisHash (g1', d1) (g2', d2)
|
||||||
|
(False, True) ->
|
||||||
|
let d1' = kisHash ([], bulkB count) (g1', d1)
|
||||||
|
in kisHash (g1', d1') (g2', d2)
|
||||||
|
(True, False) ->
|
||||||
|
let d1' = kisHash ([], bulkC count) (g1', d1)
|
||||||
|
in kisHash (g1', d1') (g2', d2)
|
||||||
|
(True, True) ->
|
||||||
|
let d1' = kisHash ([], bulkS count) (g1', d1)
|
||||||
|
in kisHash (g1', d1') (g2', d2)
|
||||||
|
|
||||||
|
False:gs1 -> case g2 of
|
||||||
|
[] -> kisHash (gs1, d1) ([], d2)
|
||||||
|
_ ->
|
||||||
|
if isId2 (g1, d1) && case g2 of { False:_ -> True; _ -> False }
|
||||||
|
then kisHash ([], _T) (drop1 g2, d2)
|
||||||
|
else case g2 of
|
||||||
|
True:gs2 ->
|
||||||
|
let d1' = kisHash ([], _B) (gs1, d1)
|
||||||
|
in kisHash (gs1, d1') (gs2, d2)
|
||||||
|
False:gs2 ->
|
||||||
|
kisHash (gs1, d1) (gs2, d2)
|
||||||
|
where
|
||||||
|
drop1 (_:xs) = xs
|
||||||
|
drop1 [] = []
|
||||||
|
|
||||||
|
|
||||||
|
toSKIKiselyov :: DB -> TricuAST
|
||||||
|
toSKIKiselyov body =
|
||||||
|
case kisConv body of
|
||||||
|
Right ([], d) -> SApp _K d
|
||||||
|
Right (True:_ , d) -> d
|
||||||
|
Right (False:g, d) -> kisHash ([], _K) (g, d) -- no snd
|
||||||
|
Left _ -> starSKIBCOpEtaDB body -- was: toSKIDB body
|
||||||
|
|
||||||
|
zipWithDefault :: a -> (a -> a -> a) -> [a] -> [a] -> [a]
|
||||||
|
zipWithDefault d f [] ys = map (f d) ys
|
||||||
|
zipWithDefault d f xs [] = map (\x -> f x d) xs
|
||||||
|
zipWithDefault d f (x:xs) (y:ys) = f x y : zipWithDefault d f xs ys
|
||||||
|
|
||||||
|
isNode :: TricuAST -> Bool
|
||||||
|
isNode t = case t of
|
||||||
|
TLeaf -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
isApp2 :: TricuAST -> Maybe (TricuAST, TricuAST)
|
||||||
|
isApp2 (SApp a b) = Just (a, b)
|
||||||
|
isApp2 _ = Nothing
|
||||||
|
|
||||||
|
isKop :: TricuAST -> Bool
|
||||||
|
isKop t = case isApp2 t of
|
||||||
|
Just (a,b) -> isNode a && isNode b
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
-- detects the two canonical I-shapes in the tree calculus:
|
||||||
|
-- △ (△ (△ △)) x OR △ (△ △ △) △
|
||||||
|
isId :: TricuAST -> Bool
|
||||||
|
isId t = case isApp2 t of
|
||||||
|
Just (ab, c) -> case isApp2 ab of
|
||||||
|
Just (a, b) | isNode a ->
|
||||||
|
case isApp2 b of
|
||||||
|
Just (b1, b2) ->
|
||||||
|
(isNode b1 && isKop b2) ||
|
||||||
|
(isKop b1 && isNode b2 && isNode c)
|
||||||
|
_ -> False
|
||||||
|
_ -> False
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
-- head-True only, tail empty, and term is identity
|
||||||
|
isId2 :: (Uses, TricuAST) -> Bool
|
||||||
|
isId2 (True:[], t) = isId t
|
||||||
|
isId2 _ = False
|
||||||
|
|
||||||
|
-- Bulk helpers built from SKI (no new primitives)
|
||||||
|
bPrime :: TricuAST
|
||||||
|
bPrime = SApp _B _B -- B' = B B
|
||||||
|
|
||||||
|
cPrime :: TricuAST
|
||||||
|
cPrime = SApp (SApp _B (SApp _B _C)) _B -- C' = B (B C) B
|
||||||
|
|
||||||
|
sPrime :: TricuAST
|
||||||
|
sPrime = SApp (SApp _B (SApp _B _S)) _B -- S' = B (B S) B
|
||||||
|
|
||||||
|
bulkB :: Int -> TricuAST
|
||||||
|
bulkB n | n <= 1 = _B
|
||||||
|
| otherwise = SApp bPrime (bulkB (n - 1))
|
||||||
|
|
||||||
|
bulkC :: Int -> TricuAST
|
||||||
|
bulkC n | n <= 1 = _C
|
||||||
|
| otherwise = SApp cPrime (bulkC (n - 1))
|
||||||
|
|
||||||
|
bulkS :: Int -> TricuAST
|
||||||
|
bulkS n | n <= 1 = _S
|
||||||
|
| otherwise = SApp sPrime (bulkS (n - 1))
|
||||||
|
|
||||||
|
-- Count how many leading pairs (a,b) repeat at the head of zip g1 g2
|
||||||
|
headPairRun :: [Bool] -> [Bool] -> ((Bool, Bool), Int)
|
||||||
|
headPairRun g1 g2 =
|
||||||
|
case zip g1 g2 of
|
||||||
|
[] -> ((False, False), 0)
|
||||||
|
(h:rest) -> (h, 1 + length (takeWhile (== h) rest))
|
||||||
|
|
||||||
|
-- DB-native star_skibc_op_eta (adapted from strategies.mts), binder = level 0
|
||||||
|
starSKIBCOpEtaDB :: DB -> TricuAST
|
||||||
|
starSKIBCOpEtaDB t
|
||||||
|
| not (dependsOnLevel 0 t) = SApp _K (fromDBClosed t)
|
||||||
|
starSKIBCOpEtaDB (BVar 0) = _I
|
||||||
|
starSKIBCOpEtaDB (BApp e1 e2)
|
||||||
|
-- if binder not in right: use C
|
||||||
|
| not (dependsOnLevel 0 e2)
|
||||||
|
= SApp (SApp _C (starSKIBCOpEtaDB e1)) (fromDBClosed e2)
|
||||||
|
-- if binder not in left:
|
||||||
|
| not (dependsOnLevel 0 e1)
|
||||||
|
= case e2 of
|
||||||
|
-- η case: \x. f x ==> f
|
||||||
|
BVar 0 -> fromDBClosed e1
|
||||||
|
_ -> SApp (SApp _B (fromDBClosed e1)) (starSKIBCOpEtaDB e2)
|
||||||
|
-- otherwise: S
|
||||||
|
| otherwise
|
||||||
|
= SApp (SApp _S (starSKIBCOpEtaDB e1)) (starSKIBCOpEtaDB e2)
|
||||||
|
-- Structural nodes with binder underneath: fall back to plain SKI (rare)
|
||||||
|
starSKIBCOpEtaDB other = toSKIDB other
|
||||||
|
|||||||
@@ -109,9 +109,9 @@ nsDefinition moduleName other =
|
|||||||
nsBody moduleName other
|
nsBody moduleName other
|
||||||
|
|
||||||
nsBody :: String -> TricuAST -> TricuAST
|
nsBody :: String -> TricuAST -> TricuAST
|
||||||
nsBody moduleName (SVar name)
|
nsBody moduleName (SVar name mhash)
|
||||||
| isPrefixed name = SVar name
|
| isPrefixed name = SVar name mhash
|
||||||
| otherwise = SVar (nsVariable moduleName name)
|
| otherwise = SVar (nsVariable moduleName name) mhash
|
||||||
nsBody moduleName (SApp func arg) =
|
nsBody moduleName (SApp func arg) =
|
||||||
SApp (nsBody moduleName func) (nsBody moduleName arg)
|
SApp (nsBody moduleName func) (nsBody moduleName arg)
|
||||||
nsBody moduleName (SLambda args body) =
|
nsBody moduleName (SLambda args body) =
|
||||||
@@ -122,18 +122,16 @@ nsBody moduleName (TFork left right) =
|
|||||||
TFork (nsBody moduleName left) (nsBody moduleName right)
|
TFork (nsBody moduleName left) (nsBody moduleName right)
|
||||||
nsBody moduleName (TStem subtree) =
|
nsBody moduleName (TStem subtree) =
|
||||||
TStem (nsBody moduleName subtree)
|
TStem (nsBody moduleName subtree)
|
||||||
nsBody moduleName (SDef name args body)
|
nsBody moduleName (SDef name args body) =
|
||||||
| isPrefixed name = SDef name args (nsBody moduleName body)
|
SDef (nsVariable moduleName name) args (nsBodyScoped moduleName args body)
|
||||||
| otherwise = SDef (nsVariable moduleName name)
|
|
||||||
args (nsBody moduleName body)
|
|
||||||
nsBody _ other = other
|
nsBody _ other = other
|
||||||
|
|
||||||
nsBodyScoped :: String -> [String] -> TricuAST -> TricuAST
|
nsBodyScoped :: String -> [String] -> TricuAST -> TricuAST
|
||||||
nsBodyScoped moduleName args body = case body of
|
nsBodyScoped moduleName args body = case body of
|
||||||
SVar name ->
|
SVar name mhash ->
|
||||||
if name `elem` args
|
if name `elem` args
|
||||||
then SVar name
|
then SVar name mhash
|
||||||
else nsBody moduleName (SVar name)
|
else nsBody moduleName (SVar name mhash)
|
||||||
SApp func arg ->
|
SApp func arg ->
|
||||||
SApp (nsBodyScoped moduleName args func) (nsBodyScoped moduleName args arg)
|
SApp (nsBodyScoped moduleName args func) (nsBodyScoped moduleName args arg)
|
||||||
SLambda innerArgs innerBody ->
|
SLambda innerArgs innerBody ->
|
||||||
@@ -141,13 +139,11 @@ nsBodyScoped moduleName args body = case body of
|
|||||||
SList items ->
|
SList items ->
|
||||||
SList (map (nsBodyScoped moduleName args) items)
|
SList (map (nsBodyScoped moduleName args) items)
|
||||||
TFork left right ->
|
TFork left right ->
|
||||||
TFork (nsBodyScoped moduleName args left)
|
TFork (nsBodyScoped moduleName args left) (nsBodyScoped moduleName args right)
|
||||||
(nsBodyScoped moduleName args right)
|
|
||||||
TStem subtree ->
|
TStem subtree ->
|
||||||
TStem (nsBodyScoped moduleName args subtree)
|
TStem (nsBodyScoped moduleName args subtree)
|
||||||
SDef name innerArgs innerBody ->
|
SDef name innerArgs innerBody ->
|
||||||
SDef (nsVariable moduleName name) innerArgs
|
SDef (nsVariable moduleName name) innerArgs (nsBodyScoped moduleName (args ++ innerArgs) innerBody)
|
||||||
(nsBodyScoped moduleName (args ++ innerArgs) innerBody)
|
|
||||||
other -> other
|
other -> other
|
||||||
|
|
||||||
isPrefixed :: String -> Bool
|
isPrefixed :: String -> Bool
|
||||||
|
|||||||
47
src/Lexer.hs
47
src/Lexer.hs
@@ -35,13 +35,13 @@ tricuLexer = do
|
|||||||
[ try lnewline
|
[ try lnewline
|
||||||
, try namespace
|
, try namespace
|
||||||
, try dot
|
, try dot
|
||||||
|
, try identifierWithHash
|
||||||
, try identifier
|
, try identifier
|
||||||
, try keywordT
|
, try keywordT
|
||||||
, try integerLiteral
|
, try integerLiteral
|
||||||
, try stringLiteral
|
, try stringLiteral
|
||||||
, assign
|
, assign
|
||||||
, colon
|
, colon
|
||||||
, backslash
|
|
||||||
, openParen
|
, openParen
|
||||||
, closeParen
|
, closeParen
|
||||||
, openBracket
|
, openBracket
|
||||||
@@ -57,12 +57,33 @@ lexTricu input = case runParser tricuLexer "" input of
|
|||||||
keywordT :: Lexer LToken
|
keywordT :: Lexer LToken
|
||||||
keywordT = string "t" *> notFollowedBy alphaNumChar $> LKeywordT
|
keywordT = string "t" *> notFollowedBy alphaNumChar $> LKeywordT
|
||||||
|
|
||||||
|
identifierWithHash :: Lexer LToken
|
||||||
|
identifierWithHash = do
|
||||||
|
first <- lowerChar <|> char '_'
|
||||||
|
rest <- many $ letterChar
|
||||||
|
<|> digitChar <|> char '_' <|> char '-' <|> char '?'
|
||||||
|
<|> char '$' <|> char '@' <|> char '%'
|
||||||
|
_ <- char '#' -- Consume '#'
|
||||||
|
hashString <- some (alphaNumChar <|> char '-') -- Ensures at least one char for hash
|
||||||
|
<?> "hash characters (alphanumeric or hyphen)"
|
||||||
|
|
||||||
|
let name = first : rest
|
||||||
|
let hashLen = length hashString
|
||||||
|
if name == "t" || name == "!result"
|
||||||
|
then fail "Keywords (`t`, `!result`) cannot be used with a hash suffix."
|
||||||
|
else if hashLen < 16 then
|
||||||
|
fail $ "Hash suffix for '" ++ name ++ "' must be at least 16 characters long. Got " ++ show hashLen ++ " ('" ++ hashString ++ "')."
|
||||||
|
else if hashLen > 64 then -- Assuming SHA256, max 64
|
||||||
|
fail $ "Hash suffix for '" ++ name ++ "' cannot be longer than 64 characters (SHA256). Got " ++ show hashLen ++ " ('" ++ hashString ++ "')."
|
||||||
|
else
|
||||||
|
return (LIdentifierWithHash name hashString)
|
||||||
|
|
||||||
identifier :: Lexer LToken
|
identifier :: Lexer LToken
|
||||||
identifier = do
|
identifier = do
|
||||||
first <- lowerChar <|> char '_'
|
first <- lowerChar <|> char '_'
|
||||||
rest <- many $ letterChar
|
rest <- many $ letterChar
|
||||||
<|> digitChar <|> char '_' <|> char '-' <|> char '?'
|
<|> digitChar <|> char '_' <|> char '-' <|> char '?'
|
||||||
<|> char '$' <|> char '#' <|> char '@' <|> char '%'
|
<|> char '$' <|> char '@' <|> char '%'
|
||||||
let name = first : rest
|
let name = first : rest
|
||||||
if name == "t" || name == "!result"
|
if name == "t" || name == "!result"
|
||||||
then fail "Keywords (`t`, `!result`) cannot be used as an identifier"
|
then fail "Keywords (`t`, `!result`) cannot be used as an identifier"
|
||||||
@@ -94,9 +115,6 @@ assign = char '=' $> LAssign
|
|||||||
colon :: Lexer LToken
|
colon :: Lexer LToken
|
||||||
colon = char ':' $> LColon
|
colon = char ':' $> LColon
|
||||||
|
|
||||||
backslash :: Lexer LToken
|
|
||||||
backslash = char '\\' $> LBackslash
|
|
||||||
|
|
||||||
openParen :: Lexer LToken
|
openParen :: Lexer LToken
|
||||||
openParen = char '(' $> LOpenParen
|
openParen = char '(' $> LOpenParen
|
||||||
|
|
||||||
@@ -126,7 +144,22 @@ integerLiteral = do
|
|||||||
stringLiteral :: Lexer LToken
|
stringLiteral :: Lexer LToken
|
||||||
stringLiteral = do
|
stringLiteral = do
|
||||||
char '"'
|
char '"'
|
||||||
content <- many (noneOf ['"'])
|
content <- manyTill Lexer.charLiteral (char '"')
|
||||||
char '"' --"
|
|
||||||
return (LStringLiteral content)
|
return (LStringLiteral content)
|
||||||
|
|
||||||
|
charLiteral :: Lexer Char
|
||||||
|
charLiteral = escapedChar <|> normalChar
|
||||||
|
where
|
||||||
|
normalChar = noneOf ['"', '\\']
|
||||||
|
escapedChar = do
|
||||||
|
void $ char '\\'
|
||||||
|
c <- oneOf ['n', 't', 'r', 'f', 'b', '\\', '"', '\'']
|
||||||
|
return $ case c of
|
||||||
|
'n' -> '\n'
|
||||||
|
't' -> '\t'
|
||||||
|
'r' -> '\r'
|
||||||
|
'f' -> '\f'
|
||||||
|
'b' -> '\b'
|
||||||
|
'\\' -> '\\'
|
||||||
|
'"' -> '"'
|
||||||
|
'\'' -> '\''
|
||||||
|
|||||||
53
src/Main.hs
53
src/Main.hs
@@ -5,6 +5,7 @@ import FileEval
|
|||||||
import Parser (parseTricu)
|
import Parser (parseTricu)
|
||||||
import REPL
|
import REPL
|
||||||
import Research
|
import Research
|
||||||
|
import ContentStore
|
||||||
|
|
||||||
import Control.Monad (foldM)
|
import Control.Monad (foldM)
|
||||||
import Control.Monad.IO.Class (liftIO)
|
import Control.Monad.IO.Class (liftIO)
|
||||||
@@ -63,18 +64,16 @@ main = do
|
|||||||
case args of
|
case args of
|
||||||
Repl -> do
|
Repl -> do
|
||||||
putStrLn "Welcome to the tricu REPL"
|
putStrLn "Welcome to the tricu REPL"
|
||||||
putStrLn "You can exit with `CTRL+D` or the `!exit` command.`"
|
putStrLn "You may exit with `CTRL+D` or the `!exit` command."
|
||||||
repl Map.empty
|
repl
|
||||||
Evaluate { file = filePaths, form = form } -> do
|
Evaluate { file = filePaths, form = form } -> do
|
||||||
result <- case filePaths of
|
result <- case filePaths of
|
||||||
[] -> do
|
[] -> runTricuT <$> getContents
|
||||||
t <- getContents
|
|
||||||
pure $ runTricu t
|
|
||||||
(filePath:restFilePaths) -> do
|
(filePath:restFilePaths) -> do
|
||||||
initialEnv <- evaluateFile filePath
|
initialEnv <- evaluateFile filePath
|
||||||
finalEnv <- foldM evaluateFileWithContext initialEnv restFilePaths
|
finalEnv <- foldM evaluateFileWithContext initialEnv restFilePaths
|
||||||
pure $ mainResult finalEnv
|
pure $ mainResult finalEnv
|
||||||
let fRes = formatResult form result
|
let fRes = formatT form result
|
||||||
putStr fRes
|
putStr fRes
|
||||||
TDecode { file = filePaths } -> do
|
TDecode { file = filePaths } -> do
|
||||||
value <- case filePaths of
|
value <- case filePaths of
|
||||||
@@ -82,8 +81,46 @@ main = do
|
|||||||
(filePath:_) -> readFile filePath
|
(filePath:_) -> readFile filePath
|
||||||
putStrLn $ decodeResult $ result $ evalTricu Map.empty $ parseTricu value
|
putStrLn $ decodeResult $ result $ evalTricu Map.empty $ parseTricu value
|
||||||
|
|
||||||
runTricu :: String -> T
|
runTricu :: String -> String
|
||||||
runTricu input =
|
runTricu = formatT TreeCalculus . runTricuT
|
||||||
|
|
||||||
|
runTricuT :: String -> T
|
||||||
|
runTricuT input =
|
||||||
let asts = parseTricu input
|
let asts = parseTricu input
|
||||||
finalEnv = evalTricu Map.empty asts
|
finalEnv = evalTricu Map.empty asts
|
||||||
in result finalEnv
|
in result finalEnv
|
||||||
|
|
||||||
|
runTricuEnv :: Env -> String -> String
|
||||||
|
runTricuEnv env = formatT TreeCalculus . runTricuTEnv env
|
||||||
|
|
||||||
|
runTricuTEnv :: Env -> String -> T
|
||||||
|
runTricuTEnv env input =
|
||||||
|
let asts = parseTricu input
|
||||||
|
finalEnv = evalTricu env asts
|
||||||
|
in result finalEnv
|
||||||
|
|
||||||
|
runTricuWithEnvT :: String -> (Env, T)
|
||||||
|
runTricuWithEnvT input =
|
||||||
|
let asts = parseTricu input
|
||||||
|
finalEnv = evalTricu Map.empty asts
|
||||||
|
in (finalEnv, result finalEnv)
|
||||||
|
|
||||||
|
runTricuWithEnv :: String -> (Env, String)
|
||||||
|
runTricuWithEnv input =
|
||||||
|
let asts = parseTricu input
|
||||||
|
finalEnv = evalTricu Map.empty asts
|
||||||
|
res = result finalEnv
|
||||||
|
in (finalEnv, formatT TreeCalculus res)
|
||||||
|
|
||||||
|
runTricuEnvWithEnvT :: Env -> String -> (Env, T)
|
||||||
|
runTricuEnvWithEnvT env input =
|
||||||
|
let asts = parseTricu input
|
||||||
|
finalEnv = evalTricu env asts
|
||||||
|
in (finalEnv, result finalEnv)
|
||||||
|
|
||||||
|
runTricuEnvWithEnv :: Env -> String -> (Env, String)
|
||||||
|
runTricuEnvWithEnv env input =
|
||||||
|
let asts = parseTricu input
|
||||||
|
finalEnv = evalTricu env asts
|
||||||
|
res = result finalEnv
|
||||||
|
in (finalEnv, formatT TreeCalculus res)
|
||||||
|
|||||||
@@ -130,7 +130,6 @@ parseFunctionM = do
|
|||||||
parseLambdaM :: ParserM TricuAST
|
parseLambdaM :: ParserM TricuAST
|
||||||
parseLambdaM = do
|
parseLambdaM = do
|
||||||
let ident = (\case LIdentifier _ -> True; _ -> False)
|
let ident = (\case LIdentifier _ -> True; _ -> False)
|
||||||
_ <- satisfyM (== LBackslash)
|
|
||||||
params <- some (satisfyM ident)
|
params <- some (satisfyM ident)
|
||||||
_ <- satisfyM (== LColon)
|
_ <- satisfyM (== LColon)
|
||||||
scnParserM
|
scnParserM
|
||||||
@@ -145,11 +144,11 @@ parseLambdaExpressionM = choice
|
|||||||
|
|
||||||
parseAtomicLambdaM :: ParserM TricuAST
|
parseAtomicLambdaM :: ParserM TricuAST
|
||||||
parseAtomicLambdaM = choice
|
parseAtomicLambdaM = choice
|
||||||
[ parseVarM
|
[ try parseLambdaM
|
||||||
|
, parseVarM
|
||||||
, parseTreeLeafM
|
, parseTreeLeafM
|
||||||
, parseLiteralM
|
, parseLiteralM
|
||||||
, parseListLiteralM
|
, parseListLiteralM
|
||||||
, try parseLambdaM
|
|
||||||
, between (satisfyM (== LOpenParen)) (satisfyM (== LCloseParen)) parseLambdaExpressionM
|
, between (satisfyM (== LOpenParen)) (satisfyM (== LCloseParen)) parseLambdaExpressionM
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -205,7 +204,8 @@ parseTreeLeafOrParenthesizedM = choice
|
|||||||
|
|
||||||
parseAtomicM :: ParserM TricuAST
|
parseAtomicM :: ParserM TricuAST
|
||||||
parseAtomicM = choice
|
parseAtomicM = choice
|
||||||
[ parseVarM
|
[ try parseLambdaM
|
||||||
|
, parseVarM
|
||||||
, parseTreeLeafM
|
, parseTreeLeafM
|
||||||
, parseListLiteralM
|
, parseListLiteralM
|
||||||
, parseGroupedM
|
, parseGroupedM
|
||||||
@@ -249,7 +249,7 @@ parseGroupedItemM = do
|
|||||||
parseSingleItemM :: ParserM TricuAST
|
parseSingleItemM :: ParserM TricuAST
|
||||||
parseSingleItemM = do
|
parseSingleItemM = do
|
||||||
token <- satisfyM (\case LIdentifier _ -> True; LKeywordT -> True; _ -> False)
|
token <- satisfyM (\case LIdentifier _ -> True; LKeywordT -> True; _ -> False)
|
||||||
if | LIdentifier name <- token -> pure (SVar name)
|
if | LIdentifier name <- token -> pure (SVar name Nothing)
|
||||||
| token == LKeywordT -> pure TLeaf
|
| token == LKeywordT -> pure TLeaf
|
||||||
| otherwise -> fail "Unexpected token in list item"
|
| otherwise -> fail "Unexpected token in list item"
|
||||||
|
|
||||||
@@ -258,16 +258,25 @@ parseVarM = do
|
|||||||
token <- satisfyM (\case
|
token <- satisfyM (\case
|
||||||
LNamespace _ -> True
|
LNamespace _ -> True
|
||||||
LIdentifier _ -> True
|
LIdentifier _ -> True
|
||||||
|
LIdentifierWithHash _ _ -> True
|
||||||
_ -> False)
|
_ -> False)
|
||||||
|
|
||||||
case token of
|
case token of
|
||||||
LNamespace ns -> do
|
LNamespace ns -> do
|
||||||
_ <- satisfyM (== LDot)
|
_ <- satisfyM (== LDot)
|
||||||
LIdentifier name <- satisfyM (\case LIdentifier _ -> True; _ -> False)
|
LIdentifier name <- satisfyM (\case LIdentifier _ -> True; _ -> False)
|
||||||
pure $ SVar (ns ++ "." ++ name)
|
pure $ SVar (ns ++ "." ++ name) Nothing
|
||||||
|
|
||||||
LIdentifier name
|
LIdentifier name
|
||||||
| name == "t" || name == "!result" ->
|
| name == "t" || name == "!result" ->
|
||||||
fail ("Reserved keyword: " ++ name ++ " cannot be assigned.")
|
fail ("Reserved keyword: " ++ name ++ " cannot be assigned.")
|
||||||
| otherwise -> pure (SVar name)
|
| otherwise -> pure (SVar name Nothing)
|
||||||
|
|
||||||
|
LIdentifierWithHash name hash ->
|
||||||
|
if name == "t" || name == "!result"
|
||||||
|
then fail ("Reserved keyword: " ++ name ++ " cannot be assigned.")
|
||||||
|
else pure (SVar name (Just hash))
|
||||||
|
|
||||||
_ -> fail "Unexpected token while parsing variable"
|
_ -> fail "Unexpected token while parsing variable"
|
||||||
|
|
||||||
parseIntLiteralM :: ParserM TricuAST
|
parseIntLiteralM :: ParserM TricuAST
|
||||||
@@ -275,7 +284,7 @@ parseIntLiteralM = do
|
|||||||
let intL = (\case LIntegerLiteral _ -> True; _ -> False)
|
let intL = (\case LIntegerLiteral _ -> True; _ -> False)
|
||||||
token <- satisfyM intL
|
token <- satisfyM intL
|
||||||
if | LIntegerLiteral value <- token ->
|
if | LIntegerLiteral value <- token ->
|
||||||
pure (SInt value)
|
pure (SInt (fromIntegral value))
|
||||||
| otherwise ->
|
| otherwise ->
|
||||||
fail "Unexpected token while parsing integer literal"
|
fail "Unexpected token while parsing integer literal"
|
||||||
|
|
||||||
|
|||||||
637
src/REPL.hs
637
src/REPL.hs
@@ -5,27 +5,57 @@ import FileEval
|
|||||||
import Lexer
|
import Lexer
|
||||||
import Parser
|
import Parser
|
||||||
import Research
|
import Research
|
||||||
|
import ContentStore
|
||||||
|
|
||||||
import Control.Exception (IOException, SomeException, catch, displayException)
|
import Control.Concurrent (forkIO, threadDelay, killThread, ThreadId)
|
||||||
|
import Control.Monad (forever, void, when, forM, forM_, foldM, unless)
|
||||||
|
import Data.ByteString (ByteString)
|
||||||
|
import Data.Maybe (isNothing, isJust, fromJust, catMaybes)
|
||||||
|
import Database.SQLite.Simple (Connection, Only(..), query, query_, execute, execute_, open)
|
||||||
|
import System.Directory (doesFileExist, createDirectoryIfMissing)
|
||||||
|
import System.FSNotify
|
||||||
|
import System.FilePath (takeDirectory, (</>))
|
||||||
|
import Text.Read (readMaybe)
|
||||||
|
|
||||||
|
import Control.Exception (IOException, SomeException, catch
|
||||||
|
, displayException)
|
||||||
import Control.Monad (forM_)
|
import Control.Monad (forM_)
|
||||||
import Control.Monad.IO.Class (liftIO)
|
|
||||||
import Control.Monad.Catch (handle, MonadCatch)
|
import Control.Monad.Catch (handle, MonadCatch)
|
||||||
|
import Control.Monad.IO.Class (liftIO)
|
||||||
import Control.Monad.Trans.Class (lift)
|
import Control.Monad.Trans.Class (lift)
|
||||||
import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT)
|
import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT)
|
||||||
import Data.Char (isSpace, isUpper)
|
import Data.Char (isSpace, isUpper)
|
||||||
import Data.List ( dropWhile
|
import Data.List ((\\), dropWhile, dropWhileEnd, isPrefixOf, nub, sortBy, groupBy, intercalate, find)
|
||||||
, dropWhileEnd
|
|
||||||
, isPrefixOf)
|
|
||||||
import System.Console.Haskeline
|
|
||||||
import Paths_tricu (version)
|
|
||||||
import Data.Version (showVersion)
|
import Data.Version (showVersion)
|
||||||
|
import Paths_tricu (version)
|
||||||
|
import System.Console.Haskeline
|
||||||
|
import System.Console.ANSI (setSGR, SGR(..), ConsoleLayer(..), ColorIntensity(..),
|
||||||
|
Color(..), ConsoleIntensity(..), clearFromCursorToLineEnd)
|
||||||
|
|
||||||
|
import qualified Data.Map as Map
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Data.Text.IO as T
|
import qualified Data.Text.IO as T
|
||||||
|
|
||||||
import qualified Data.Map as Map
|
import Control.Concurrent (forkIO, threadDelay)
|
||||||
|
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
|
||||||
|
import Data.Time (UTCTime, getCurrentTime, diffUTCTime)
|
||||||
|
import Control.Concurrent.MVar (MVar, newMVar, putMVar, takeMVar)
|
||||||
|
|
||||||
repl :: Env -> IO ()
|
import Data.Time.Format (formatTime, defaultTimeLocale)
|
||||||
repl env = runInputT settings (withInterrupt (loop env Decode))
|
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
|
||||||
|
|
||||||
|
data REPLState = REPLState
|
||||||
|
{ replForm :: EvaluatedForm
|
||||||
|
, replContentStore :: Maybe Connection
|
||||||
|
, replWatchedFile :: Maybe FilePath
|
||||||
|
, replSelectedVersions :: Map.Map String T.Text
|
||||||
|
, replWatcherThread :: Maybe ThreadId
|
||||||
|
}
|
||||||
|
|
||||||
|
repl :: IO ()
|
||||||
|
repl = do
|
||||||
|
conn <- ContentStore.initContentStore
|
||||||
|
runInputT settings (withInterrupt (loop (REPLState Decode (Just conn) Nothing Map.empty Nothing)))
|
||||||
where
|
where
|
||||||
settings :: Settings IO
|
settings :: Settings IO
|
||||||
settings = Settings
|
settings = Settings
|
||||||
@@ -40,49 +70,64 @@ repl env = runInputT settings (withInterrupt (loop env Decode))
|
|||||||
where
|
where
|
||||||
commands = [ "!exit"
|
commands = [ "!exit"
|
||||||
, "!output"
|
, "!output"
|
||||||
, "!definitions"
|
|
||||||
, "!import"
|
, "!import"
|
||||||
, "!clear"
|
, "!clear"
|
||||||
, "!save"
|
|
||||||
, "!reset"
|
, "!reset"
|
||||||
, "!version"
|
, "!help"
|
||||||
|
, "!definitions"
|
||||||
|
, "!watch"
|
||||||
|
, "!refresh"
|
||||||
|
, "!versions"
|
||||||
|
, "!select"
|
||||||
|
, "!tag"
|
||||||
]
|
]
|
||||||
|
|
||||||
loop :: Env -> EvaluatedForm -> InputT IO ()
|
loop :: REPLState -> InputT IO ()
|
||||||
loop env form = handle (interruptHandler env form) $ do
|
loop state = handle (\Interrupt -> interruptHandler state Interrupt) $ do
|
||||||
minput <- getInputLine "tricu < "
|
minput <- getInputLine "tricu < "
|
||||||
case minput of
|
case minput of
|
||||||
Nothing -> outputStrLn "Exiting tricu"
|
Nothing -> return ()
|
||||||
Just s
|
Just s
|
||||||
| strip s == "" -> loop env form
|
| strip s == "" -> loop state
|
||||||
| strip s == "!exit" -> outputStrLn "Exiting tricu"
|
| strip s == "!exit" -> outputStrLn "Exiting tricu"
|
||||||
| strip s == "!clear" -> do
|
| strip s == "!clear" -> do
|
||||||
liftIO $ putStr "\ESC[2J\ESC[H"
|
liftIO $ putStr "\ESC[2J\ESC[H"
|
||||||
loop env form
|
loop state
|
||||||
| strip s == "!reset" -> do
|
| strip s == "!reset" -> do
|
||||||
outputStrLn "Environment reset to initial state"
|
outputStrLn "Selected versions reset"
|
||||||
loop Map.empty form
|
loop state { replSelectedVersions = Map.empty }
|
||||||
| strip s == "!version" -> do
|
| strip s == "!help" -> do
|
||||||
outputStrLn $ "tricu version " ++ showVersion version
|
outputStrLn $ "tricu version " ++ showVersion version
|
||||||
loop env form
|
outputStrLn "Available commands:"
|
||||||
| "!save" `isPrefixOf` strip s -> handleSave env form
|
outputStrLn " !exit - Exit the REPL"
|
||||||
| strip s == "!output" -> handleOutput env form
|
outputStrLn " !clear - Clear the screen"
|
||||||
| strip s == "!definitions" -> do
|
outputStrLn " !reset - Reset preferences for selected versions"
|
||||||
let defs = Map.keys $ Map.delete "!result" env
|
outputStrLn " !help - Show tricu version and available commands"
|
||||||
if null defs
|
outputStrLn " !output - Change output format (tree|fsl|ast|ternary|ascii|decode)"
|
||||||
then outputStrLn "No definitions discovered."
|
outputStrLn " !definitions - List all defined terms in the content store"
|
||||||
else do
|
outputStrLn " !import - Import definitions from file to the content store"
|
||||||
outputStrLn "Available definitions:"
|
outputStrLn " !watch - Watch a file for changes, evaluate terms, and store them"
|
||||||
mapM_ outputStrLn defs
|
outputStrLn " !versions - Show all versions of a term by name"
|
||||||
loop env form
|
outputStrLn " !select - Select a specific version of a term for subsequent lookups"
|
||||||
| "!import" `isPrefixOf` strip s -> handleImport env form
|
outputStrLn " !tag - Add or update a tag for a term by hash or name"
|
||||||
| take 2 s == "--" -> loop env form
|
loop state
|
||||||
|
| strip s == "!output" -> handleOutput state
|
||||||
|
| strip s == "!definitions" -> handleDefinitions state
|
||||||
|
| "!import" `isPrefixOf` strip s -> handleImport state
|
||||||
|
| "!watch" `isPrefixOf` strip s -> handleWatch state
|
||||||
|
| strip s == "!refresh" -> handleRefresh state
|
||||||
|
| "!versions" `isPrefixOf` strip s -> handleVersions state
|
||||||
|
| "!select" `isPrefixOf` strip s -> handleSelect state
|
||||||
|
| "!tag" `isPrefixOf` strip s -> handleTag state
|
||||||
|
| take 2 s == "--" -> loop state
|
||||||
| otherwise -> do
|
| otherwise -> do
|
||||||
newEnv <- liftIO $ processInput env s form `catch` errorHandler env
|
result <- liftIO $ catch
|
||||||
loop newEnv form
|
(processInput state s)
|
||||||
|
(errorHandler state)
|
||||||
|
loop result
|
||||||
|
|
||||||
handleOutput :: Env -> EvaluatedForm -> InputT IO ()
|
handleOutput :: REPLState -> InputT IO ()
|
||||||
handleOutput env currentForm = do
|
handleOutput state = do
|
||||||
let formats = [Decode, TreeCalculus, FSL, AST, Ternary, Ascii]
|
let formats = [Decode, TreeCalculus, FSL, AST, Ternary, Ascii]
|
||||||
outputStrLn "Available output formats:"
|
outputStrLn "Available output formats:"
|
||||||
mapM_ (\(i, f) -> outputStrLn $ show i ++ ". " ++ show f)
|
mapM_ (\(i, f) -> outputStrLn $ show i ++ ". " ++ show f)
|
||||||
@@ -98,94 +143,462 @@ repl env = runInputT settings (withInterrupt (loop env Decode))
|
|||||||
case result of
|
case result of
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
outputStrLn "Invalid selection. Keeping current output format."
|
outputStrLn "Invalid selection. Keeping current output format."
|
||||||
loop env currentForm
|
loop state
|
||||||
Just newForm -> do
|
Just newForm -> do
|
||||||
outputStrLn $ "Output format changed to: " ++ show newForm
|
outputStrLn $ "Output format changed to: " ++ show newForm
|
||||||
loop env newForm
|
loop state { replForm = newForm }
|
||||||
|
|
||||||
handleImport :: Env -> EvaluatedForm -> InputT IO ()
|
handleDefinitions :: REPLState -> InputT IO ()
|
||||||
handleImport env form = do
|
handleDefinitions state = case replContentStore state of
|
||||||
res <- runMaybeT $ do
|
Nothing -> do
|
||||||
let fset = setComplete completeFilename defaultSettings
|
liftIO $ printError "Content store not initialized"
|
||||||
path <- MaybeT $ runInputT fset $
|
loop state
|
||||||
getInputLineWithInitial "File path to load < " ("", "")
|
Just conn -> do
|
||||||
|
terms <- liftIO $ ContentStore.listStoredTerms conn
|
||||||
|
|
||||||
|
if null terms
|
||||||
|
then do
|
||||||
|
liftIO $ printWarning "No terms in content store."
|
||||||
|
loop state
|
||||||
|
else do
|
||||||
|
liftIO $ do
|
||||||
|
printSuccess $ "Content store contains " ++ show (length terms) ++ " terms:"
|
||||||
|
|
||||||
text <- MaybeT $ liftIO $ handle (\e -> do
|
let maxNameWidth = maximum $ map (length . T.unpack . termNames) terms
|
||||||
putStrLn $ "Error reading file: " ++ displayException (e :: IOException)
|
|
||||||
return Nothing
|
|
||||||
) $ Just <$> readFile (strip path)
|
|
||||||
|
|
||||||
case parseProgram (lexTricu text) of
|
forM_ terms $ \term -> do
|
||||||
Left err -> do
|
let namesStr = T.unpack (termNames term)
|
||||||
lift $ outputStrLn $ "Parse error: " ++ handleParseError err
|
hash = termHash term
|
||||||
MaybeT $ return Nothing
|
padding = replicate (maxNameWidth - length namesStr) ' '
|
||||||
Right ast -> do
|
|
||||||
ns <- MaybeT $ runInputT defaultSettings $
|
liftIO $ do
|
||||||
getInputLineWithInitial "Namespace (or !Local for no namespace) < " ("", "")
|
putStr " "
|
||||||
|
printVariable namesStr
|
||||||
|
putStr padding
|
||||||
|
putStr " [hash: "
|
||||||
|
displayColoredHash hash
|
||||||
|
putStrLn "]"
|
||||||
|
|
||||||
|
tags <- ContentStore.termToTags conn hash
|
||||||
|
unless (null tags) $ displayTags tags
|
||||||
|
|
||||||
let name = strip ns
|
loop state
|
||||||
if (name /= "!Local" && (null name || not (isUpper (head name)))) then do
|
|
||||||
lift $ outputStrLn "Namespace must start with an uppercase letter"
|
handleImport :: REPLState -> InputT IO ()
|
||||||
MaybeT $ return Nothing
|
handleImport state = do
|
||||||
else do
|
let fset = setComplete completeFilename defaultSettings
|
||||||
prog <- liftIO $ preprocessFile (strip path)
|
filename <- runInputT fset $ getInputLineWithInitial "File to import: " ("", "")
|
||||||
let code = case name of
|
case filename of
|
||||||
"!Local" -> prog
|
Nothing -> loop state
|
||||||
_ -> nsDefinitions name prog
|
Just f -> do
|
||||||
env' = evalTricu env code
|
let cleanFilename = strip f
|
||||||
return env'
|
exists <- liftIO $ doesFileExist cleanFilename
|
||||||
case res of
|
if not exists
|
||||||
|
then do
|
||||||
|
liftIO $ printError $ "File not found: " ++ cleanFilename
|
||||||
|
loop state
|
||||||
|
else importFile state cleanFilename
|
||||||
|
|
||||||
|
importFile :: REPLState -> String -> InputT IO ()
|
||||||
|
importFile state cleanFilename = do
|
||||||
|
code <- liftIO $ readFile cleanFilename
|
||||||
|
case replContentStore state of
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
outputStrLn "Import cancelled"
|
liftIO $ printError "Content store not initialized"
|
||||||
loop env form
|
loop state
|
||||||
Just env' ->
|
Just conn -> do
|
||||||
loop (Map.delete "!result" env') form
|
env <- liftIO $ evaluateFile cleanFilename
|
||||||
|
|
||||||
|
liftIO $ do
|
||||||
|
printSuccess $ "Importing file: " ++ cleanFilename
|
||||||
|
let defs = Map.toList $ Map.delete "!result" env
|
||||||
|
|
||||||
|
importedCount <- foldM (\count (name, term) -> do
|
||||||
|
hash <- ContentStore.storeTerm conn [name] term
|
||||||
|
printSuccess $ "Stored definition: " ++ name ++ " with hash " ++ T.unpack hash
|
||||||
|
return (count + 1)
|
||||||
|
) 0 defs
|
||||||
|
|
||||||
|
printSuccess $ "Imported " ++ show importedCount ++ " definitions successfully"
|
||||||
|
|
||||||
|
loop state
|
||||||
|
|
||||||
interruptHandler :: Env -> EvaluatedForm -> Interrupt -> InputT IO ()
|
handleWatch :: REPLState -> InputT IO ()
|
||||||
interruptHandler env form _ = do
|
handleWatch state = do
|
||||||
outputStrLn "Interrupted with CTRL+C\n\
|
dbPath <- liftIO ContentStore.getContentStorePath
|
||||||
\You can use the !exit command or CTRL+D to exit"
|
let filepath = takeDirectory dbPath </> "scratch.tri"
|
||||||
loop env form
|
let dirPath = takeDirectory filepath
|
||||||
|
|
||||||
processInput :: Env -> String -> EvaluatedForm -> IO Env
|
liftIO $ createDirectoryIfMissing True dirPath
|
||||||
processInput env input form = do
|
|
||||||
let asts = parseTricu input
|
|
||||||
newEnv = evalTricu env asts
|
|
||||||
case Map.lookup "!result" newEnv of
|
|
||||||
Just r -> do
|
|
||||||
putStrLn $ "tricu > " ++ formatResult form r
|
|
||||||
Nothing -> pure ()
|
|
||||||
return newEnv
|
|
||||||
|
|
||||||
errorHandler :: Env -> SomeException -> IO (Env)
|
fileExists <- liftIO $ doesFileExist filepath
|
||||||
errorHandler env e = do
|
unless fileExists $ liftIO $ writeFile filepath "-- tricu scratch file\n\n"
|
||||||
putStrLn $ "Error: " ++ show e
|
|
||||||
return env
|
outputStrLn $ "Using scratch file: " ++ filepath
|
||||||
|
|
||||||
|
when (isJust (replWatcherThread state)) $ do
|
||||||
|
outputStrLn "Stopping previous file watch"
|
||||||
|
liftIO $ killThread (fromJust $ replWatcherThread state)
|
||||||
|
|
||||||
|
outputStrLn $ "Starting to watch file: " ++ filepath
|
||||||
|
outputStrLn "Press Ctrl+C to stop watching and return to REPL"
|
||||||
|
|
||||||
|
liftIO $ processWatchedFile filepath (replContentStore state) (replSelectedVersions state) (replForm state)
|
||||||
|
|
||||||
|
lastProcessedRef <- liftIO $ newIORef =<< getCurrentTime
|
||||||
|
|
||||||
|
watcherId <- liftIO $ forkIO $ withManager $ \mgr -> do
|
||||||
|
stopAction <- watchDir mgr dirPath (\event -> eventPath event == filepath) $ \event -> do
|
||||||
|
now <- getCurrentTime
|
||||||
|
lastProcessed <- readIORef lastProcessedRef
|
||||||
|
when (diffUTCTime now lastProcessed > 0.5) $ do
|
||||||
|
putStrLn $ "\nFile changed: " ++ filepath
|
||||||
|
processWatchedFile filepath (replContentStore state) (replSelectedVersions state) (replForm state)
|
||||||
|
writeIORef lastProcessedRef now
|
||||||
|
forever $ threadDelay 1000000
|
||||||
|
|
||||||
|
watchLoop state { replWatchedFile = Just filepath, replWatcherThread = Just watcherId }
|
||||||
|
|
||||||
|
handleUnwatch :: REPLState -> InputT IO ()
|
||||||
|
handleUnwatch state = case replWatchedFile state of
|
||||||
|
Nothing -> do
|
||||||
|
outputStrLn "No file is currently being watched"
|
||||||
|
loop state
|
||||||
|
Just path -> do
|
||||||
|
outputStrLn $ "Stopped watching " ++ path
|
||||||
|
when (isJust (replWatcherThread state)) $ do
|
||||||
|
liftIO $ killThread (fromJust $ replWatcherThread state)
|
||||||
|
loop state { replWatchedFile = Nothing, replWatcherThread = Nothing }
|
||||||
|
|
||||||
|
handleRefresh :: REPLState -> InputT IO ()
|
||||||
|
handleRefresh state = case replContentStore state of
|
||||||
|
Nothing -> do
|
||||||
|
outputStrLn "Content store not initialized"
|
||||||
|
loop state
|
||||||
|
Just conn -> do
|
||||||
|
outputStrLn "Environment refreshed from content store (definitions are live)"
|
||||||
|
loop state
|
||||||
|
|
||||||
|
handleVersions :: REPLState -> InputT IO ()
|
||||||
|
handleVersions state = case replContentStore state of
|
||||||
|
Nothing -> do
|
||||||
|
liftIO $ printError "Content store not initialized"
|
||||||
|
loop state
|
||||||
|
Just conn -> do
|
||||||
|
liftIO $ printPrompt "Term name: "
|
||||||
|
nameInput <- getInputLine ""
|
||||||
|
case nameInput of
|
||||||
|
Nothing -> loop state
|
||||||
|
Just n -> do
|
||||||
|
let termName = strip n
|
||||||
|
versions <- liftIO $ ContentStore.termVersions conn termName
|
||||||
|
if null versions
|
||||||
|
then liftIO $ printError $ "No versions found for term: " ++ termName
|
||||||
|
else do
|
||||||
|
liftIO $ do
|
||||||
|
printKeyword "Versions of "
|
||||||
|
printVariable termName
|
||||||
|
putStrLn ":"
|
||||||
|
|
||||||
|
forM_ (zip [1..] versions) $ \(i, (hash, _, ts)) -> do
|
||||||
|
tags <- ContentStore.termToTags conn hash
|
||||||
|
putStr $ show (i :: Int) ++ ". "
|
||||||
|
displayColoredHash hash
|
||||||
|
putStr $ " (" ++ formatTimestamp ts ++ ")"
|
||||||
|
unless (null tags) $ do
|
||||||
|
putStr " ["
|
||||||
|
printKeyword "Tags: "
|
||||||
|
forM_ (zip [0..] tags) $ \(j, tag) -> do
|
||||||
|
printTag (T.unpack tag)
|
||||||
|
when (j < length tags - 1) $ putStr ", "
|
||||||
|
putStr "]"
|
||||||
|
putStrLn ""
|
||||||
|
loop state
|
||||||
|
|
||||||
|
handleSelect :: REPLState -> InputT IO ()
|
||||||
|
handleSelect state = case replContentStore state of
|
||||||
|
Nothing -> do
|
||||||
|
liftIO $ printError "Content store not initialized"
|
||||||
|
loop state
|
||||||
|
Just conn -> do
|
||||||
|
liftIO $ printPrompt "Term name: "
|
||||||
|
nameInput <- getInputLine ""
|
||||||
|
case nameInput of
|
||||||
|
Nothing -> loop state
|
||||||
|
Just n -> do
|
||||||
|
let cleanName = strip n
|
||||||
|
versions <- liftIO $ ContentStore.termVersions conn cleanName
|
||||||
|
if null versions
|
||||||
|
then do
|
||||||
|
liftIO $ printError $ "No versions found for term: " ++ cleanName
|
||||||
|
loop state
|
||||||
|
else do
|
||||||
|
liftIO $ do
|
||||||
|
printKeyword "Versions of "
|
||||||
|
printVariable cleanName
|
||||||
|
putStrLn ":"
|
||||||
|
|
||||||
|
forM_ (zip [1..] versions) $ \(i, (hash, _, ts)) -> do
|
||||||
|
tags <- ContentStore.termToTags conn hash
|
||||||
|
putStr $ show (i :: Int) ++ ". "
|
||||||
|
displayColoredHash hash
|
||||||
|
putStr $ " (" ++ formatTimestamp ts ++ ")"
|
||||||
|
unless (null tags) $ do
|
||||||
|
putStr " ["
|
||||||
|
printKeyword "Tags: "
|
||||||
|
forM_ (zip [0..] tags) $ \(j, tag) -> do
|
||||||
|
printTag (T.unpack tag)
|
||||||
|
when (j < length tags - 1) $ putStr ", "
|
||||||
|
putStr "]"
|
||||||
|
putStrLn ""
|
||||||
|
|
||||||
|
liftIO $ printPrompt "Select version (number or full hash, Enter to cancel): "
|
||||||
|
choiceInput <- getInputLine ""
|
||||||
|
let choice = strip <$> choiceInput
|
||||||
|
|
||||||
|
selectedHash <- case choice of
|
||||||
|
Just selectedStr | not (null selectedStr) -> do
|
||||||
|
case readMaybe selectedStr :: Maybe Int of
|
||||||
|
Just idx | idx > 0 && idx <= length versions -> do
|
||||||
|
let (h, _, _) = versions !! (idx - 1)
|
||||||
|
return $ Just h
|
||||||
|
_ -> do
|
||||||
|
let potentialHash = T.pack selectedStr
|
||||||
|
let foundByHash = find (\(h, _, _) -> T.isPrefixOf potentialHash h) versions
|
||||||
|
case foundByHash of
|
||||||
|
Just (h, _, _) -> return $ Just h
|
||||||
|
Nothing -> do
|
||||||
|
liftIO $ printError "Invalid selection or hash not found in list."
|
||||||
|
return Nothing
|
||||||
|
_ -> return Nothing
|
||||||
|
|
||||||
|
case selectedHash of
|
||||||
|
Just hashToSelect -> do
|
||||||
|
let newState = state { replSelectedVersions =
|
||||||
|
Map.insert cleanName hashToSelect (replSelectedVersions state) }
|
||||||
|
liftIO $ do
|
||||||
|
printSuccess "Selected version "
|
||||||
|
displayColoredHash hashToSelect
|
||||||
|
putStr " for term "
|
||||||
|
printVariable cleanName
|
||||||
|
putStrLn ""
|
||||||
|
loop newState
|
||||||
|
Nothing -> loop state
|
||||||
|
|
||||||
|
handleTag :: REPLState -> InputT IO ()
|
||||||
|
handleTag state = case replContentStore state of
|
||||||
|
Nothing -> do
|
||||||
|
liftIO $ printError "Content store not initialized"
|
||||||
|
loop state
|
||||||
|
Just conn -> do
|
||||||
|
liftIO $ printPrompt "Term hash (full or prefix) or name (most recent version will be used): "
|
||||||
|
identInput <- getInputLine ""
|
||||||
|
case identInput of
|
||||||
|
Nothing -> loop state
|
||||||
|
Just ident -> do
|
||||||
|
let cleanIdent = strip ident
|
||||||
|
|
||||||
|
mFullHash <- liftIO $ resolveIdentifierToHash conn cleanIdent
|
||||||
|
|
||||||
|
case mFullHash of
|
||||||
|
Nothing -> do
|
||||||
|
liftIO $ printError $ "Could not resolve identifier: " ++ cleanIdent
|
||||||
|
loop state
|
||||||
|
Just fullHash -> do
|
||||||
|
liftIO $ do
|
||||||
|
putStr "Tagging term with hash: "
|
||||||
|
displayColoredHash fullHash
|
||||||
|
putStrLn ""
|
||||||
|
tags <- liftIO $ ContentStore.termToTags conn fullHash
|
||||||
|
unless (null tags) $ do
|
||||||
|
liftIO $ do
|
||||||
|
printKeyword "Existing tags:"
|
||||||
|
displayTags tags
|
||||||
|
|
||||||
|
liftIO $ printPrompt "Tag to add/set: "
|
||||||
|
tagValueInput <- getInputLine ""
|
||||||
|
case tagValueInput of
|
||||||
|
Nothing -> loop state
|
||||||
|
Just tv -> do
|
||||||
|
let tagVal = T.pack (strip tv)
|
||||||
|
liftIO $ do
|
||||||
|
ContentStore.setTag conn fullHash tagVal
|
||||||
|
printSuccess $ "Tag '"
|
||||||
|
printTag (T.unpack tagVal)
|
||||||
|
putStr "' set for term with hash "
|
||||||
|
displayColoredHash fullHash
|
||||||
|
putStrLn ""
|
||||||
|
loop state
|
||||||
|
|
||||||
|
resolveIdentifierToHash :: Connection -> String -> IO (Maybe T.Text)
|
||||||
|
resolveIdentifierToHash conn ident
|
||||||
|
| T.pack "#" `T.isInfixOf` T.pack ident = do
|
||||||
|
let hashPrefix = T.pack ident
|
||||||
|
matchingHashes <- liftIO $ query conn "SELECT hash FROM terms WHERE hash LIKE ?" (Only (hashPrefix <> "%")) :: IO [Only T.Text]
|
||||||
|
case matchingHashes of
|
||||||
|
[Only fullHash] -> return $ Just fullHash
|
||||||
|
[] -> do printError $ "No hash found starting with: " ++ T.unpack hashPrefix; return Nothing
|
||||||
|
_ -> do printError $ "Ambiguous hash prefix: " ++ T.unpack hashPrefix; return Nothing
|
||||||
|
| otherwise = do
|
||||||
|
versions <- ContentStore.termVersions conn ident
|
||||||
|
if null versions
|
||||||
|
then do printError $ "No versions found for term name: " ++ ident; return Nothing
|
||||||
|
else return $ Just $ (\(h,_,_) -> h) $ head versions
|
||||||
|
|
||||||
|
interruptHandler :: REPLState -> Interrupt -> InputT IO ()
|
||||||
|
interruptHandler state _ = do
|
||||||
|
liftIO $ do
|
||||||
|
printWarning "Interrupted with CTRL+C"
|
||||||
|
printWarning "You can use the !exit command or CTRL+D to exit"
|
||||||
|
loop state
|
||||||
|
|
||||||
|
errorHandler :: REPLState -> SomeException -> IO REPLState
|
||||||
|
errorHandler state e = do
|
||||||
|
printError $ "Error: " ++ displayException e
|
||||||
|
return state
|
||||||
|
|
||||||
|
processInput :: REPLState -> String -> IO REPLState
|
||||||
|
processInput state input = do
|
||||||
|
let asts = parseTricu input
|
||||||
|
case asts of
|
||||||
|
[] -> return state
|
||||||
|
_ -> case replContentStore state of
|
||||||
|
Nothing -> do
|
||||||
|
printError "Content store not initialized"
|
||||||
|
return state
|
||||||
|
Just conn -> do
|
||||||
|
newState <- foldM (\s astNode -> do
|
||||||
|
let varsInAst = Eval.findVarNames astNode
|
||||||
|
foldM (\currentSelectionState varName ->
|
||||||
|
if Map.member varName (replSelectedVersions currentSelectionState)
|
||||||
|
then return currentSelectionState
|
||||||
|
else do
|
||||||
|
versions <- ContentStore.termVersions conn varName
|
||||||
|
if length versions > 1
|
||||||
|
then do
|
||||||
|
let (latestHash, _, _) = head versions
|
||||||
|
liftIO $ printWarning $ "Multiple versions of '" ++ varName ++ "' found. Using most recent."
|
||||||
|
return currentSelectionState { replSelectedVersions = Map.insert varName latestHash (replSelectedVersions currentSelectionState) }
|
||||||
|
else return currentSelectionState
|
||||||
|
) s varsInAst
|
||||||
|
) state asts
|
||||||
|
|
||||||
|
forM_ asts $ \ast -> do
|
||||||
|
case ast of
|
||||||
|
SDef name [] body -> do
|
||||||
|
result <- evalAST (Just conn) (replSelectedVersions newState) body
|
||||||
|
hash <- ContentStore.storeTerm conn [name] result
|
||||||
|
|
||||||
|
liftIO $ do
|
||||||
|
putStr "tricu > "
|
||||||
|
printSuccess "Stored definition: "
|
||||||
|
printVariable name
|
||||||
|
putStr " with hash "
|
||||||
|
displayColoredHash hash
|
||||||
|
putStrLn ""
|
||||||
|
|
||||||
|
putStr "tricu > "
|
||||||
|
printResult $ formatT (replForm newState) result
|
||||||
|
putStrLn ""
|
||||||
|
|
||||||
|
_ -> do
|
||||||
|
result <- evalAST (Just conn) (replSelectedVersions newState) ast
|
||||||
|
liftIO $ do
|
||||||
|
putStr "tricu > "
|
||||||
|
printResult $ formatT (replForm newState) result
|
||||||
|
putStrLn ""
|
||||||
|
return newState
|
||||||
|
|
||||||
strip :: String -> String
|
strip :: String -> String
|
||||||
strip = dropWhileEnd isSpace . dropWhile isSpace
|
strip = dropWhileEnd isSpace . dropWhile isSpace
|
||||||
|
|
||||||
handleSave :: Env -> EvaluatedForm -> InputT IO ()
|
watchLoop :: REPLState -> InputT IO ()
|
||||||
handleSave env form = do
|
watchLoop state = handle (\Interrupt -> do
|
||||||
let fset = setComplete completeFilename defaultSettings
|
outputStrLn "\nStopped watching file"
|
||||||
path <- runInputT fset $
|
when (isJust (replWatcherThread state)) $ do
|
||||||
getInputLineWithInitial "File to save < " ("", "")
|
liftIO $ killThread (fromJust $ replWatcherThread state)
|
||||||
|
loop state { replWatchedFile = Nothing, replWatcherThread = Nothing }) $ do
|
||||||
|
liftIO $ threadDelay 1000000
|
||||||
|
watchLoop state
|
||||||
|
|
||||||
case path of
|
processWatchedFile :: FilePath -> Maybe Connection -> Map.Map String T.Text -> EvaluatedForm -> IO ()
|
||||||
Nothing -> do
|
processWatchedFile filepath mconn selectedVersions outputForm = do
|
||||||
outputStrLn "Save cancelled"
|
content <- readFile filepath
|
||||||
loop env form
|
let asts = parseTricu content
|
||||||
Just p -> do
|
|
||||||
let definitions = Map.toList $ Map.delete "!result" env
|
|
||||||
filepath = strip p
|
|
||||||
|
|
||||||
outputStrLn "Starting save..."
|
case mconn of
|
||||||
liftIO $ writeFile filepath ""
|
Nothing -> putStrLn "Content store not initialized for watched file processing."
|
||||||
outputStrLn "File created..."
|
Just conn -> do
|
||||||
forM_ definitions $ \(name, value) -> do
|
forM_ asts $ \ast -> case ast of
|
||||||
let content = name ++ " = " ++ formatResult TreeCalculus value ++ "\n"
|
SDef name [] body -> do
|
||||||
outputStrLn $ "Writing definition: " ++ name ++ " with length " ++ show (length content)
|
result <- evalAST (Just conn) selectedVersions body
|
||||||
liftIO $ appendFile filepath content
|
hash <- ContentStore.storeTerm conn [name] result
|
||||||
outputStrLn $ "Saved " ++ show (length definitions) ++ " definitions to " ++ p
|
putStrLn $ "tricu > Stored definition: " ++ name ++ " with hash " ++ T.unpack hash
|
||||||
|
putStrLn $ "tricu > " ++ name ++ " = " ++ formatT outputForm result
|
||||||
|
_ -> do
|
||||||
|
result <- evalAST (Just conn) selectedVersions ast
|
||||||
|
putStrLn $ "tricu > Result: " ++ formatT outputForm result
|
||||||
|
putStrLn $ "tricu > Processed file: " ++ filepath
|
||||||
|
|
||||||
loop env form
|
formatTimestamp :: Integer -> String
|
||||||
|
formatTimestamp ts = formatTime defaultTimeLocale "%Y-%m-%d %H:%M:%S" (posixSecondsToUTCTime (fromIntegral ts))
|
||||||
|
|
||||||
|
displayColoredHash :: T.Text -> IO ()
|
||||||
|
displayColoredHash hash = do
|
||||||
|
let (prefix, rest) = T.splitAt 16 hash
|
||||||
|
setSGR [SetColor Foreground Vivid Cyan]
|
||||||
|
putStr $ T.unpack prefix
|
||||||
|
setSGR [SetColor Foreground Dull White]
|
||||||
|
putStr $ T.unpack rest
|
||||||
|
setSGR [Reset]
|
||||||
|
|
||||||
|
coloredHashString :: T.Text -> String
|
||||||
|
coloredHashString hash =
|
||||||
|
"\ESC[1;36m" ++ T.unpack (T.take 16 hash) ++
|
||||||
|
"\ESC[0;37m" ++ T.unpack (T.drop 16 hash) ++
|
||||||
|
"\ESC[0m"
|
||||||
|
|
||||||
|
withColor :: ColorIntensity -> Color -> IO () -> IO ()
|
||||||
|
withColor intensity color action = do
|
||||||
|
setSGR [SetColor Foreground intensity color]
|
||||||
|
action
|
||||||
|
setSGR [Reset]
|
||||||
|
|
||||||
|
printColored :: ColorIntensity -> Color -> String -> IO ()
|
||||||
|
printColored intensity color text = withColor intensity color $ putStr text
|
||||||
|
|
||||||
|
printlnColored :: ColorIntensity -> Color -> String -> IO ()
|
||||||
|
printlnColored intensity color text = withColor intensity color $ putStrLn text
|
||||||
|
|
||||||
|
printSuccess :: String -> IO ()
|
||||||
|
printSuccess = printlnColored Vivid Green
|
||||||
|
|
||||||
|
printError :: String -> IO ()
|
||||||
|
printError = printlnColored Vivid Red
|
||||||
|
|
||||||
|
printWarning :: String -> IO ()
|
||||||
|
printWarning = printlnColored Vivid Yellow
|
||||||
|
|
||||||
|
printPrompt :: String -> IO ()
|
||||||
|
printPrompt = printColored Vivid Blue
|
||||||
|
|
||||||
|
printVariable :: String -> IO ()
|
||||||
|
printVariable = printColored Vivid Magenta
|
||||||
|
|
||||||
|
printTag :: String -> IO ()
|
||||||
|
printTag = printColored Vivid Yellow
|
||||||
|
|
||||||
|
printKeyword :: String -> IO ()
|
||||||
|
printKeyword = printColored Vivid Blue
|
||||||
|
|
||||||
|
printResult :: String -> IO ()
|
||||||
|
printResult = printColored Dull White
|
||||||
|
|
||||||
|
displayTags :: [T.Text] -> IO ()
|
||||||
|
displayTags [] = return ()
|
||||||
|
displayTags tags = do
|
||||||
|
putStr " Tags: "
|
||||||
|
forM_ (zip [0..] tags) $ \(i, tag) -> do
|
||||||
|
printTag (T.unpack tag)
|
||||||
|
when (i < length tags - 1) $ putStr ", "
|
||||||
|
putStrLn ""
|
||||||
|
|||||||
113
src/Research.hs
113
src/Research.hs
@@ -1,12 +1,17 @@
|
|||||||
module Research where
|
module Research where
|
||||||
|
|
||||||
|
import Data.ByteArray (convert)
|
||||||
|
import Data.ByteString.Base16 (decode, encode)
|
||||||
import Data.List (intercalate)
|
import Data.List (intercalate)
|
||||||
import Data.Map (Map)
|
import Data.Map (Map)
|
||||||
import Data.Text (Text, replace)
|
import Data.Text (Text, replace, pack)
|
||||||
|
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
|
||||||
import System.Console.CmdArgs (Data, Typeable)
|
import System.Console.CmdArgs (Data, Typeable)
|
||||||
|
|
||||||
|
import qualified Data.ByteString as BS
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
|
import Crypto.Hash (hash, SHA256, Digest)
|
||||||
|
|
||||||
-- Tree Calculus Types
|
-- Tree Calculus Types
|
||||||
data T = Leaf | Stem T | Fork T T
|
data T = Leaf | Stem T | Fork T T
|
||||||
@@ -14,8 +19,8 @@ data T = Leaf | Stem T | Fork T T
|
|||||||
|
|
||||||
-- Abstract Syntax Tree for tricu
|
-- Abstract Syntax Tree for tricu
|
||||||
data TricuAST
|
data TricuAST
|
||||||
= SVar String
|
= SVar String (Maybe String) -- Variable name and optional hash prefix
|
||||||
| SInt Int
|
| SInt Integer
|
||||||
| SStr String
|
| SStr String
|
||||||
| SList [TricuAST]
|
| SList [TricuAST]
|
||||||
| SDef String [String] TricuAST
|
| SDef String [String] TricuAST
|
||||||
@@ -30,22 +35,22 @@ data TricuAST
|
|||||||
|
|
||||||
-- Lexer Tokens
|
-- Lexer Tokens
|
||||||
data LToken
|
data LToken
|
||||||
= LKeywordT
|
= LIdentifier String
|
||||||
| LIdentifier String
|
| LIdentifierWithHash String String
|
||||||
|
| LKeywordT
|
||||||
| LNamespace String
|
| LNamespace String
|
||||||
| LIntegerLiteral Int
|
| LImport String String
|
||||||
| LStringLiteral String
|
|
||||||
| LAssign
|
| LAssign
|
||||||
| LColon
|
| LColon
|
||||||
| LDot
|
| LDot
|
||||||
| LBackslash
|
|
||||||
| LOpenParen
|
| LOpenParen
|
||||||
| LCloseParen
|
| LCloseParen
|
||||||
| LOpenBracket
|
| LOpenBracket
|
||||||
| LCloseBracket
|
| LCloseBracket
|
||||||
|
| LStringLiteral String
|
||||||
|
| LIntegerLiteral Int
|
||||||
| LNewline
|
| LNewline
|
||||||
| LImport String String
|
deriving (Eq, Show, Ord)
|
||||||
deriving (Show, Eq, Ord)
|
|
||||||
|
|
||||||
-- Output formats
|
-- Output formats
|
||||||
data EvaluatedForm = TreeCalculus | FSL | AST | Ternary | Ascii | Decode
|
data EvaluatedForm = TreeCalculus | FSL | AST | Ternary | Ascii | Decode
|
||||||
@@ -54,8 +59,68 @@ data EvaluatedForm = TreeCalculus | FSL | AST | Ternary | Ascii | Decode
|
|||||||
-- Environment containing previously evaluated TC terms
|
-- Environment containing previously evaluated TC terms
|
||||||
type Env = Map.Map String T
|
type Env = Map.Map String T
|
||||||
|
|
||||||
|
-- Merkle DAG Node types
|
||||||
|
-- Each Tree Calculus node becomes a content-addressed object.
|
||||||
|
|
||||||
|
type MerkleHash = Text
|
||||||
|
|
||||||
|
data Node
|
||||||
|
= NLeaf
|
||||||
|
| NStem MerkleHash
|
||||||
|
| NFork MerkleHash MerkleHash
|
||||||
|
deriving (Show, Eq, Ord)
|
||||||
|
|
||||||
|
-- | Canonical serialization of a Node for hashing.
|
||||||
|
-- Leaf: 0x00
|
||||||
|
-- Stem: 0x01 || child_hash (32 bytes)
|
||||||
|
-- Fork: 0x02 || left_hash (32 bytes) || right_hash (32 bytes)
|
||||||
|
serializeNode :: Node -> BS.ByteString
|
||||||
|
serializeNode NLeaf = BS.pack [0x00]
|
||||||
|
serializeNode (NStem h) = BS.pack [0x01] <> go (decode (encodeUtf8 h))
|
||||||
|
where go (Left _) = error "Research.serializeNode: invalid hex hash"
|
||||||
|
go (Right bs) = bs
|
||||||
|
serializeNode (NFork l r) = BS.pack [0x02] <> go (decode (encodeUtf8 l)) <> go (decode (encodeUtf8 r))
|
||||||
|
where go (Left _) = error "Research.serializeNode: invalid hex hash"
|
||||||
|
go (Right bs) = bs
|
||||||
|
|
||||||
|
-- | Hash a node per the Merkle content-addressing spec.
|
||||||
|
-- hash = SHA256( "tricu.merkle.node.v1" <> 0x00 <> node_payload )
|
||||||
|
nodeHash :: Node -> MerkleHash
|
||||||
|
nodeHash node = decodeUtf8 (encode (sha256WithPrefix (serializeNode node)))
|
||||||
|
where sha256WithPrefix payload =
|
||||||
|
convert . (hash :: BS.ByteString -> Digest SHA256) $ utf8Tag <> BS.pack [0x00] <> payload
|
||||||
|
utf8Tag = BS.pack $ map fromIntegral $ BS.unpack "tricu.merkle.node.v1"
|
||||||
|
|
||||||
|
-- | Deserialize a Node from canonical bytes.
|
||||||
|
deserializeNode :: BS.ByteString -> Node
|
||||||
|
deserializeNode bs =
|
||||||
|
case BS.uncons bs of
|
||||||
|
Just (0x00, rest)
|
||||||
|
| BS.null rest -> NLeaf
|
||||||
|
|
||||||
|
Just (0x01, rest)
|
||||||
|
| BS.length rest == 32 ->
|
||||||
|
NStem $ decodeUtf8 (encode rest)
|
||||||
|
|
||||||
|
Just (0x02, rest)
|
||||||
|
| BS.length rest == 64 ->
|
||||||
|
let (l, r) = BS.splitAt 32 rest
|
||||||
|
in NFork (decodeUtf8 (encode l)) (decodeUtf8 (encode r))
|
||||||
|
|
||||||
|
_ -> errorWithoutStackTrace "invalid merkle node payload"
|
||||||
|
|
||||||
|
-- | Build a Merkle DAG from a Tree Calculus term.
|
||||||
|
buildMerkle :: T -> Node
|
||||||
|
buildMerkle Leaf = NLeaf
|
||||||
|
buildMerkle (Stem t) = NStem (nodeHash child)
|
||||||
|
where child = buildMerkle t
|
||||||
|
buildMerkle (Fork l r) = NFork (nodeHash left) (nodeHash right)
|
||||||
|
where
|
||||||
|
left = buildMerkle l
|
||||||
|
right = buildMerkle r
|
||||||
|
|
||||||
-- Tree Calculus Reduction Rules
|
-- Tree Calculus Reduction Rules
|
||||||
{-
|
{-
|
||||||
The t operator is left associative.
|
The t operator is left associative.
|
||||||
1. t t a b -> a
|
1. t t a b -> a
|
||||||
2. t (t a) b c -> a c (b c)
|
2. t (t a) b c -> a c (b c)
|
||||||
@@ -85,9 +150,9 @@ _not = Fork (Fork _true (Fork Leaf _false)) Leaf
|
|||||||
|
|
||||||
-- Marshalling
|
-- Marshalling
|
||||||
ofString :: String -> T
|
ofString :: String -> T
|
||||||
ofString str = ofList $ map (ofNumber . fromEnum) str
|
ofString str = ofList $ map (ofNumber . toInteger . fromEnum) str
|
||||||
|
|
||||||
ofNumber :: Int -> T
|
ofNumber :: Integer -> T
|
||||||
ofNumber 0 = Leaf
|
ofNumber 0 = Leaf
|
||||||
ofNumber n =
|
ofNumber n =
|
||||||
Fork
|
Fork
|
||||||
@@ -97,7 +162,7 @@ ofNumber n =
|
|||||||
ofList :: [T] -> T
|
ofList :: [T] -> T
|
||||||
ofList = foldr Fork Leaf
|
ofList = foldr Fork Leaf
|
||||||
|
|
||||||
toNumber :: T -> Either String Int
|
toNumber :: T -> Either String Integer
|
||||||
toNumber Leaf = Right 0
|
toNumber Leaf = Right 0
|
||||||
toNumber (Fork Leaf rest) = case toNumber rest of
|
toNumber (Fork Leaf rest) = case toNumber rest of
|
||||||
Right n -> Right (2 * n)
|
Right n -> Right (2 * n)
|
||||||
@@ -109,7 +174,7 @@ toNumber _ = Left "Invalid Tree Calculus number"
|
|||||||
|
|
||||||
toString :: T -> Either String String
|
toString :: T -> Either String String
|
||||||
toString tc = case toList tc of
|
toString tc = case toList tc of
|
||||||
Right list -> traverse (fmap toEnum . toNumber) list
|
Right list -> traverse (fmap (toEnum . fromInteger) . toNumber) list
|
||||||
Left err -> Left "Invalid Tree Calculus string"
|
Left err -> Left "Invalid Tree Calculus string"
|
||||||
|
|
||||||
toList :: T -> Either String [T]
|
toList :: T -> Either String [T]
|
||||||
@@ -120,13 +185,13 @@ toList (Fork x rest) = case toList rest of
|
|||||||
toList _ = Left "Invalid Tree Calculus list"
|
toList _ = Left "Invalid Tree Calculus list"
|
||||||
|
|
||||||
-- Outputs
|
-- Outputs
|
||||||
formatResult :: EvaluatedForm -> T -> String
|
formatT :: EvaluatedForm -> T -> String
|
||||||
formatResult TreeCalculus = toSimpleT . show
|
formatT TreeCalculus = toSimpleT . show
|
||||||
formatResult FSL = show
|
formatT FSL = show
|
||||||
formatResult AST = show . toAST
|
formatT AST = show . toAST
|
||||||
formatResult Ternary = toTernaryString
|
formatT Ternary = toTernaryString
|
||||||
formatResult Ascii = toAscii
|
formatT Ascii = toAscii
|
||||||
formatResult Decode = decodeResult
|
formatT Decode = decodeResult
|
||||||
|
|
||||||
toSimpleT :: String -> String
|
toSimpleT :: String -> String
|
||||||
toSimpleT s = T.unpack
|
toSimpleT s = T.unpack
|
||||||
@@ -167,7 +232,7 @@ decodeResult tc =
|
|||||||
(_, _, Right n) -> show n
|
(_, _, Right n) -> show n
|
||||||
(_, Right xs@(_:_), _) -> "[" ++ intercalate ", " (map decodeResult xs) ++ "]"
|
(_, Right xs@(_:_), _) -> "[" ++ intercalate ", " (map decodeResult xs) ++ "]"
|
||||||
(_, Right [], _) -> "[]"
|
(_, Right [], _) -> "[]"
|
||||||
_ -> formatResult TreeCalculus tc
|
_ -> formatT TreeCalculus tc
|
||||||
where
|
where
|
||||||
isCommonChar c =
|
isCommonChar c =
|
||||||
let n = fromEnum c
|
let n = fromEnum c
|
||||||
|
|||||||
205
test/Spec.hs
205
test/Spec.hs
@@ -12,7 +12,6 @@ import Control.Monad.IO.Class (liftIO)
|
|||||||
import Data.List (isInfixOf)
|
import Data.List (isInfixOf)
|
||||||
import Test.Tasty
|
import Test.Tasty
|
||||||
import Test.Tasty.HUnit
|
import Test.Tasty.HUnit
|
||||||
import Test.Tasty.QuickCheck
|
|
||||||
import Text.Megaparsec (runParser)
|
import Text.Megaparsec (runParser)
|
||||||
|
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
@@ -21,8 +20,8 @@ import qualified Data.Set as Set
|
|||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = defaultMain tests
|
main = defaultMain tests
|
||||||
|
|
||||||
runTricu :: String -> String
|
tricuTestString :: String -> String
|
||||||
runTricu s = show $ result (evalTricu Map.empty $ parseTricu s)
|
tricuTestString s = show $ result (evalTricu Map.empty $ parseTricu s)
|
||||||
|
|
||||||
tests :: TestTree
|
tests :: TestTree
|
||||||
tests = testGroup "Tricu Tests"
|
tests = testGroup "Tricu Tests"
|
||||||
@@ -35,6 +34,8 @@ tests = testGroup "Tricu Tests"
|
|||||||
, modules
|
, modules
|
||||||
, demos
|
, demos
|
||||||
, decoding
|
, decoding
|
||||||
|
, elimLambdaSingle
|
||||||
|
, stressElimLambda
|
||||||
]
|
]
|
||||||
|
|
||||||
lexer :: TestTree
|
lexer :: TestTree
|
||||||
@@ -51,7 +52,22 @@ lexer = testGroup "Lexer Tests"
|
|||||||
|
|
||||||
, testCase "Lex escaped characters in strings" $ do
|
, testCase "Lex escaped characters in strings" $ do
|
||||||
let input = "\"hello\\nworld\""
|
let input = "\"hello\\nworld\""
|
||||||
expect = Right [LStringLiteral "hello\\nworld"]
|
expect = Right [LStringLiteral "hello\nworld"]
|
||||||
|
runParser tricuLexer "" input @?= expect
|
||||||
|
|
||||||
|
, testCase "Lex multiple escaped characters in strings" $ do
|
||||||
|
let input = "\"tab:\\t newline:\\n quote:\\\" backslash:\\\\\""
|
||||||
|
expect = Right [LStringLiteral "tab:\t newline:\n quote:\" backslash:\\"]
|
||||||
|
runParser tricuLexer "" input @?= expect
|
||||||
|
|
||||||
|
, testCase "Lex escaped characters in string literals" $ do
|
||||||
|
let input = "x = \"line1\\nline2\\tindented\""
|
||||||
|
expect = Right [LIdentifier "x", LAssign, LStringLiteral "line1\nline2\tindented"]
|
||||||
|
runParser tricuLexer "" input @?= expect
|
||||||
|
|
||||||
|
, testCase "Lex empty string with escape sequence" $ do
|
||||||
|
let input = "\"\\\"\""
|
||||||
|
expect = Right [LStringLiteral "\""]
|
||||||
runParser tricuLexer "" input @?= expect
|
runParser tricuLexer "" input @?= expect
|
||||||
|
|
||||||
, testCase "Lex mixed literals" $ do
|
, testCase "Lex mixed literals" $ do
|
||||||
@@ -87,8 +103,8 @@ parser = testGroup "Parser Tests"
|
|||||||
Right _ -> assertFailure "Expected failure when trying to assign the value of T"
|
Right _ -> assertFailure "Expected failure when trying to assign the value of T"
|
||||||
|
|
||||||
, testCase "Parse function definitions" $ do
|
, testCase "Parse function definitions" $ do
|
||||||
let input = "x = (\\a b c : a)"
|
let input = "x = (a b c : a)"
|
||||||
expect = SDef "x" [] (SLambda ["a"] (SLambda ["b"] (SLambda ["c"] (SVar "a"))))
|
expect = SDef "x" [] (SLambda ["a"] (SLambda ["b"] (SLambda ["c"] (SVar "a" Nothing))))
|
||||||
parseSingle input @?= expect
|
parseSingle input @?= expect
|
||||||
|
|
||||||
, testCase "Parse nested Tree Calculus terms" $ do
|
, testCase "Parse nested Tree Calculus terms" $ do
|
||||||
@@ -107,8 +123,8 @@ parser = testGroup "Parser Tests"
|
|||||||
parseSingle input @?= expect
|
parseSingle input @?= expect
|
||||||
|
|
||||||
, testCase "Parse function with applications" $ do
|
, testCase "Parse function with applications" $ do
|
||||||
let input = "f = (\\x : t x)"
|
let input = "f = (x : t x)"
|
||||||
expect = SDef "f" [] (SLambda ["x"] (SApp TLeaf (SVar "x")))
|
expect = SDef "f" [] (SLambda ["x"] (SApp TLeaf (SVar "x" Nothing)))
|
||||||
parseSingle input @?= expect
|
parseSingle input @?= expect
|
||||||
|
|
||||||
, testCase "Parse nested lists" $ do
|
, testCase "Parse nested lists" $ do
|
||||||
@@ -149,23 +165,23 @@ parser = testGroup "Parser Tests"
|
|||||||
parseSingle input @?= expect
|
parseSingle input @?= expect
|
||||||
|
|
||||||
, testCase "Parse nested parentheses in function body" $ do
|
, testCase "Parse nested parentheses in function body" $ do
|
||||||
let input = "f = (\\x : t (t (t t)))"
|
let input = "f = (x : t (t (t t)))"
|
||||||
expect = SDef "f" [] (SLambda ["x"] (SApp TLeaf (SApp TLeaf (SApp TLeaf TLeaf))))
|
expect = SDef "f" [] (SLambda ["x"] (SApp TLeaf (SApp TLeaf (SApp TLeaf TLeaf))))
|
||||||
parseSingle input @?= expect
|
parseSingle input @?= expect
|
||||||
|
|
||||||
, testCase "Parse lambda abstractions" $ do
|
, testCase "Parse lambda abstractions" $ do
|
||||||
let input = "(\\a : a)"
|
let input = "(a : a)"
|
||||||
expect = (SLambda ["a"] (SVar "a"))
|
expect = (SLambda ["a"] (SVar "a" Nothing))
|
||||||
parseSingle input @?= expect
|
parseSingle input @?= expect
|
||||||
|
|
||||||
, testCase "Parse multiple arguments to lambda abstractions" $ do
|
, testCase "Parse multiple arguments to lambda abstractions" $ do
|
||||||
let input = "x = (\\a b : a)"
|
let input = "x = (a b : a)"
|
||||||
expect = SDef "x" [] (SLambda ["a"] (SLambda ["b"] (SVar "a")))
|
expect = SDef "x" [] (SLambda ["a"] (SLambda ["b"] (SVar "a" Nothing)))
|
||||||
parseSingle input @?= expect
|
parseSingle input @?= expect
|
||||||
|
|
||||||
, testCase "Grouping T terms with parentheses in function application" $ do
|
, testCase "Grouping T terms with parentheses in function application" $ do
|
||||||
let input = "x = (\\a : a)\nx (t)"
|
let input = "x = (a : a)\nx (t)"
|
||||||
expect = [SDef "x" [] (SLambda ["a"] (SVar "a")),SApp (SVar "x") TLeaf]
|
expect = [SDef "x" [] (SLambda ["a"] (SVar "a" Nothing)),SApp (SVar "x" Nothing) TLeaf]
|
||||||
parseTricu input @?= expect
|
parseTricu input @?= expect
|
||||||
|
|
||||||
, testCase "Comments 1" $ do
|
, testCase "Comments 1" $ do
|
||||||
@@ -251,7 +267,7 @@ simpleEvaluation = testGroup "Evaluation Tests"
|
|||||||
, testCase "Immutable definitions" $ do
|
, testCase "Immutable definitions" $ do
|
||||||
let input = "x = t t\nx = t\nx"
|
let input = "x = t t\nx = t\nx"
|
||||||
env = evalTricu Map.empty (parseTricu input)
|
env = evalTricu Map.empty (parseTricu input)
|
||||||
result <- try (evaluate (runTricu input)) :: IO (Either SomeException String)
|
result <- try (evaluate (tricuTestString input)) :: IO (Either SomeException String)
|
||||||
case result of
|
case result of
|
||||||
Left _ -> return ()
|
Left _ -> return ()
|
||||||
Right _ -> assertFailure "Expected evaluation error"
|
Right _ -> assertFailure "Expected evaluation error"
|
||||||
@@ -259,7 +275,7 @@ simpleEvaluation = testGroup "Evaluation Tests"
|
|||||||
|
|
||||||
, testCase "Apply identity to Boolean Not" $ do
|
, testCase "Apply identity to Boolean Not" $ do
|
||||||
let not = "(t (t (t t) (t t t)) t)"
|
let not = "(t (t (t t) (t t t)) t)"
|
||||||
let input = "x = (\\a : a)\nx " ++ not
|
let input = "x = (a : a)\nx " ++ not
|
||||||
env = evalTricu Map.empty (parseTricu input)
|
env = evalTricu Map.empty (parseTricu input)
|
||||||
result env @?= Fork (Fork (Stem Leaf) (Fork Leaf Leaf)) Leaf
|
result env @?= Fork (Fork (Stem Leaf) (Fork Leaf Leaf)) Leaf
|
||||||
]
|
]
|
||||||
@@ -267,85 +283,85 @@ simpleEvaluation = testGroup "Evaluation Tests"
|
|||||||
lambdas :: TestTree
|
lambdas :: TestTree
|
||||||
lambdas = testGroup "Lambda Evaluation Tests"
|
lambdas = testGroup "Lambda Evaluation Tests"
|
||||||
[ testCase "Lambda Identity Function" $ do
|
[ testCase "Lambda Identity Function" $ do
|
||||||
let input = "id = (\\x : x)\nid t"
|
let input = "id = (x : x)\nid t"
|
||||||
runTricu input @?= "Leaf"
|
tricuTestString input @?= "Leaf"
|
||||||
|
|
||||||
, testCase "Lambda Constant Function (K combinator)" $ do
|
, testCase "Lambda Constant Function (K combinator)" $ do
|
||||||
let input = "k = (\\x y : x)\nk t (t t)"
|
let input = "k = (x y : x)\nk t (t t)"
|
||||||
runTricu input @?= "Leaf"
|
tricuTestString input @?= "Leaf"
|
||||||
|
|
||||||
, testCase "Lambda Application with Variable" $ do
|
, testCase "Lambda Application with Variable" $ do
|
||||||
let input = "id = (\\x : x)\nval = t t\nid val"
|
let input = "id = (x : x)\nval = t t\nid val"
|
||||||
runTricu input @?= "Stem Leaf"
|
tricuTestString input @?= "Stem Leaf"
|
||||||
|
|
||||||
, testCase "Lambda Application with Multiple Arguments" $ do
|
, testCase "Lambda Application with Multiple Arguments" $ do
|
||||||
let input = "apply = (\\f x y : f x y)\nk = (\\a b : a)\napply k t (t t)"
|
let input = "apply = (f x y : f x y)\nk = (a b : a)\napply k t (t t)"
|
||||||
runTricu input @?= "Leaf"
|
tricuTestString input @?= "Leaf"
|
||||||
|
|
||||||
, testCase "Nested Lambda Application" $ do
|
, testCase "Nested Lambda Application" $ do
|
||||||
let input = "apply = (\\f x y : f x y)\nid = (\\x : x)\napply (\\f x : f x) id t"
|
let input = "apply = (f x y : f x y)\nid = (x : x)\napply (f x : f x) id t"
|
||||||
runTricu input @?= "Leaf"
|
tricuTestString input @?= "Leaf"
|
||||||
|
|
||||||
, testCase "Lambda with a complex body" $ do
|
, testCase "Lambda with a complex body" $ do
|
||||||
let input = "f = (\\x : t (t x))\nf t"
|
let input = "f = (x : t (t x))\nf t"
|
||||||
runTricu input @?= "Stem (Stem Leaf)"
|
tricuTestString input @?= "Stem (Stem Leaf)"
|
||||||
|
|
||||||
, testCase "Lambda returning a function" $ do
|
, testCase "Lambda returning a function" $ do
|
||||||
let input = "f = (\\x : (\\y : x))\ng = f t\ng (t t)"
|
let input = "f = (x : (y : x))\ng = f t\ng (t t)"
|
||||||
runTricu input @?= "Leaf"
|
tricuTestString input @?= "Leaf"
|
||||||
|
|
||||||
, testCase "Lambda with Shadowing" $ do
|
, testCase "Lambda with Shadowing" $ do
|
||||||
let input = "f = (\\x : (\\x : x))\nf t (t t)"
|
let input = "f = (x : (x : x))\nf t (t t)"
|
||||||
runTricu input @?= "Stem Leaf"
|
tricuTestString input @?= "Stem Leaf"
|
||||||
|
|
||||||
, testCase "Lambda returning another lambda" $ do
|
, testCase "Lambda returning another lambda" $ do
|
||||||
let input = "k = (\\x : (\\y : x))\nk_app = k t\nk_app (t t)"
|
let input = "k = (x : (y : x))\nk_app = k t\nk_app (t t)"
|
||||||
runTricu input @?= "Leaf"
|
tricuTestString input @?= "Leaf"
|
||||||
|
|
||||||
, testCase "Lambda with free variables" $ do
|
, testCase "Lambda with free variables" $ do
|
||||||
let input = "y = t t\nf = (\\x : y)\nf t"
|
let input = "y = t t\nf = (x : y)\nf t"
|
||||||
runTricu input @?= "Stem Leaf"
|
tricuTestString input @?= "Stem Leaf"
|
||||||
|
|
||||||
, testCase "SKI Composition" $ do
|
, testCase "SKI Composition" $ do
|
||||||
let input = "s = (\\x y z : x z (y z))\nk = (\\x y : x)\ni = (\\x : x)\ncomp = s k i\ncomp t (t t)"
|
let input = "s = (x y z : x z (y z))\nk = (x y : x)\ni = (x : x)\ncomp = s k i\ncomp t (t t)"
|
||||||
runTricu input @?= "Stem (Stem Leaf)"
|
tricuTestString input @?= "Stem (Stem Leaf)"
|
||||||
|
|
||||||
, testCase "Lambda with multiple parameters and application" $ do
|
, testCase "Lambda with multiple parameters and application" $ do
|
||||||
let input = "f = (\\a b c : t a b c)\nf t (t t) (t t t)"
|
let input = "f = (a b c : t a b c)\nf t (t t) (t t t)"
|
||||||
runTricu input @?= "Stem Leaf"
|
tricuTestString input @?= "Stem Leaf"
|
||||||
|
|
||||||
, testCase "Lambda with nested application in the body" $ do
|
, testCase "Lambda with nested application in the body" $ do
|
||||||
let input = "f = (\\x : t (t (t x)))\nf t"
|
let input = "f = (x : t (t (t x)))\nf t"
|
||||||
runTricu input @?= "Stem (Stem (Stem Leaf))"
|
tricuTestString input @?= "Stem (Stem (Stem Leaf))"
|
||||||
|
|
||||||
, testCase "Lambda returning a function and applying it" $ do
|
, testCase "Lambda returning a function and applying it" $ do
|
||||||
let input = "f = (\\x : (\\y : t x y))\ng = f t\ng (t t)"
|
let input = "f = (x : (y : t x y))\ng = f t\ng (t t)"
|
||||||
runTricu input @?= "Fork Leaf (Stem Leaf)"
|
tricuTestString input @?= "Fork Leaf (Stem Leaf)"
|
||||||
|
|
||||||
, testCase "Lambda applying a variable" $ do
|
, testCase "Lambda applying a variable" $ do
|
||||||
let input = "id = (\\x : x)\na = t t\nid a"
|
let input = "id = (x : x)\na = t t\nid a"
|
||||||
runTricu input @?= "Stem Leaf"
|
tricuTestString input @?= "Stem Leaf"
|
||||||
|
|
||||||
, testCase "Nested lambda abstractions in the same expression" $ do
|
, testCase "Nested lambda abstractions in the same expression" $ do
|
||||||
let input = "f = (\\x : (\\y : x y))\ng = (\\z : z)\nf g t"
|
let input = "f = (x : (y : x y))\ng = (z : z)\nf g t"
|
||||||
runTricu input @?= "Leaf"
|
tricuTestString input @?= "Leaf"
|
||||||
|
|
||||||
, testCase "Lambda applied to string literal" $ do
|
, testCase "Lambda applied to string literal" $ do
|
||||||
let input = "f = (\\x : x)\nf \"hello\""
|
let input = "f = (x : x)\nf \"hello\""
|
||||||
runTricu input @?= "Fork (Fork Leaf (Fork Leaf (Fork Leaf (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) Leaf))))))) (Fork (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork Leaf (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) Leaf))))))) (Fork (Fork Leaf (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) Leaf))))))) (Fork (Fork Leaf (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) Leaf))))))) (Fork (Fork (Stem Leaf) (Fork (Stem Leaf) (Fork (Stem Leaf) (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) Leaf))))))) Leaf))))"
|
tricuTestString input @?= "Fork (Fork Leaf (Fork Leaf (Fork Leaf (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) Leaf))))))) (Fork (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork Leaf (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) Leaf))))))) (Fork (Fork Leaf (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) Leaf))))))) (Fork (Fork Leaf (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) Leaf))))))) (Fork (Fork (Stem Leaf) (Fork (Stem Leaf) (Fork (Stem Leaf) (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) Leaf))))))) Leaf))))"
|
||||||
|
|
||||||
|
|
||||||
, testCase "Lambda applied to integer literal" $ do
|
, testCase "Lambda applied to integer literal" $ do
|
||||||
let input = "f = (\\x : x)\nf 42"
|
let input = "f = (x : x)\nf 42"
|
||||||
runTricu input @?= "Fork Leaf (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) Leaf)))))"
|
tricuTestString input @?= "Fork Leaf (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) Leaf)))))"
|
||||||
|
|
||||||
, testCase "Lambda applied to list literal" $ do
|
, testCase "Lambda applied to list literal" $ do
|
||||||
let input = "f = (\\x : x)\nf [t (t t)]"
|
let input = "f = (x : x)\nf [t (t t)]"
|
||||||
runTricu input @?= "Fork Leaf (Fork (Stem Leaf) Leaf)"
|
tricuTestString input @?= "Fork Leaf (Fork (Stem Leaf) Leaf)"
|
||||||
|
|
||||||
, testCase "Lambda containing list literal" $ do
|
, testCase "Lambda containing list literal" $ do
|
||||||
let input = "(\\a : [(a)]) 1"
|
let input = "(a : [(a)]) 1"
|
||||||
runTricu input @?= "Fork (Fork (Stem Leaf) Leaf) Leaf"
|
tricuTestString input @?= "Fork (Fork (Stem Leaf) Leaf) Leaf"
|
||||||
]
|
]
|
||||||
|
|
||||||
providedLibraries :: TestTree
|
providedLibraries :: TestTree
|
||||||
@@ -419,7 +435,7 @@ providedLibraries = testGroup "Library Tests"
|
|||||||
|
|
||||||
, testCase "List map" $ do
|
, testCase "List map" $ do
|
||||||
library <- evaluateFile "./lib/list.tri"
|
library <- evaluateFile "./lib/list.tri"
|
||||||
let input = "head (tail (map (\\a : (t t t)) [(t) (t) (t)]))"
|
let input = "head (tail (map (a : (t t t)) [(t) (t) (t)]))"
|
||||||
env = evalTricu library (parseTricu input)
|
env = evalTricu library (parseTricu input)
|
||||||
result env @?= Fork Leaf Leaf
|
result env @?= Fork Leaf Leaf
|
||||||
|
|
||||||
@@ -518,7 +534,7 @@ demos = testGroup "Test provided demo functionality"
|
|||||||
decodeResult res @?= "\"(t (t (t t) (t t t)) (t t (t t t)))\""
|
decodeResult res @?= "\"(t (t (t t) (t t t)) (t t (t t t)))\""
|
||||||
, testCase "Determining the size of functions" $ do
|
, testCase "Determining the size of functions" $ do
|
||||||
res <- liftIO $ evaluateFileResult "./demos/size.tri"
|
res <- liftIO $ evaluateFileResult "./demos/size.tri"
|
||||||
decodeResult res @?= "454"
|
decodeResult res @?= "321"
|
||||||
, testCase "Level Order Traversal demo" $ do
|
, testCase "Level Order Traversal demo" $ do
|
||||||
res <- liftIO $ evaluateFileResult "./demos/levelOrderTraversal.tri"
|
res <- liftIO $ evaluateFileResult "./demos/levelOrderTraversal.tri"
|
||||||
decodeResult res @?= "\"\n1 \n2 3 \n4 5 6 7 \n8 11 10 9 12 \""
|
decodeResult res @?= "\"\n1 \n2 3 \n4 5 6 7 \n8 11 10 9 12 \""
|
||||||
@@ -554,4 +570,73 @@ decoding = testGroup "Decoding Tests"
|
|||||||
, testCase "Decode nested lists with strings" $ do
|
, testCase "Decode nested lists with strings" $ do
|
||||||
let input = ofList [ofList [ofString "nested"], ofString "string"]
|
let input = ofList [ofList [ofString "nested"], ofString "string"]
|
||||||
decodeResult input @?= "[[\"nested\"], \"string\"]"
|
decodeResult input @?= "[[\"nested\"], \"string\"]"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
elimLambdaSingle :: TestTree
|
||||||
|
elimLambdaSingle = testCase "elimLambda preserves eval, fires eta, and SDef binds" $ do
|
||||||
|
-- 1) eta reduction, purely structural and parsed from source
|
||||||
|
let [etaIn] = parseTricu "x : f x"
|
||||||
|
[fRef ] = parseTricu "f"
|
||||||
|
elimLambda etaIn @?= fRef
|
||||||
|
|
||||||
|
-- 2) SDef binds its own name and parameters
|
||||||
|
let [defFXY] = parseTricu "f x y : f x"
|
||||||
|
fv = freeVars defFXY
|
||||||
|
assertBool "f should be bound in SDef" ("f" `Set.notMember` fv)
|
||||||
|
assertBool "x should be bound in SDef" ("x" `Set.notMember` fv)
|
||||||
|
assertBool "y should be bound in SDef" ("y" `Set.notMember` fv)
|
||||||
|
|
||||||
|
-- 3) semantics preserved on a small program that exercises compose and triage
|
||||||
|
let src =
|
||||||
|
unlines
|
||||||
|
[ "false = t"
|
||||||
|
, "_ = t"
|
||||||
|
, "true = t t"
|
||||||
|
, "id = a : a"
|
||||||
|
, "const = a b : a"
|
||||||
|
, "compose = f g x : f (g x)"
|
||||||
|
, "triage = leaf stem fork : t (t leaf stem) fork"
|
||||||
|
, "test = triage \"Leaf\" (_ : \"Stem\") (_ _ : \"Fork\")"
|
||||||
|
, "main = compose id id test"
|
||||||
|
]
|
||||||
|
prog = parseTricu src
|
||||||
|
progElim = map elimLambda prog
|
||||||
|
evalBefore = result (evalTricu Map.empty prog)
|
||||||
|
evalAfter = result (evalTricu Map.empty progElim)
|
||||||
|
evalAfter @?= evalBefore
|
||||||
|
|
||||||
|
stressElimLambda :: TestTree
|
||||||
|
stressElimLambda = testCase "stress elimLambda on wide list under deep curried lambda" $ do
|
||||||
|
let numVars = 200
|
||||||
|
numBody = 800
|
||||||
|
vars = [ "x" ++ show i | i <- [1..numVars] ]
|
||||||
|
body = "(" ++ unwords (replicate numBody "t") ++ ")"
|
||||||
|
etaOne = "h : f h"
|
||||||
|
etaTwo = "k : id k"
|
||||||
|
defId = "id = a : a"
|
||||||
|
lambda = unwords vars ++ " : " ++ body
|
||||||
|
src = unlines
|
||||||
|
[ defId
|
||||||
|
, etaOne
|
||||||
|
, "compose = f g x : f (g x)"
|
||||||
|
, "f = t t"
|
||||||
|
, etaTwo
|
||||||
|
, lambda
|
||||||
|
, "main = compose id id (" ++ head vars ++ " : f " ++ head vars ++ ")"
|
||||||
|
]
|
||||||
|
prog = parseTricu src
|
||||||
|
|
||||||
|
let out = map elimLambda prog
|
||||||
|
let noLambda term = case term of
|
||||||
|
SLambda _ _ -> False
|
||||||
|
SApp f g -> noLambda f && noLambda g
|
||||||
|
SList xs -> all noLambda xs
|
||||||
|
TFork l r -> noLambda l && noLambda r
|
||||||
|
TStem u -> noLambda u
|
||||||
|
_ -> True
|
||||||
|
|
||||||
|
assertBool "all lambdas eliminated" (all noLambda out)
|
||||||
|
|
||||||
|
let before = result (evalTricu Map.empty prog)
|
||||||
|
after = result (evalTricu Map.empty out)
|
||||||
|
after @?= before
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
-- This is a tricu comment!
|
-- This is a tricu comment!
|
||||||
-- t (t t) (t (t t t))
|
-- t (t t) (t (t t t))
|
||||||
-- t (t t t) (t t)
|
-- t (t t t) (t t)
|
||||||
-- x = (\a : a)
|
-- x = (a : a)
|
||||||
main = t (t t) t -- Fork (Stem Leaf) Leaf
|
main = t (t t) t -- Fork (Stem Leaf) Leaf
|
||||||
-- t t
|
-- t t
|
||||||
-- x
|
-- x
|
||||||
-- x = (\a : a)
|
-- x = (a : a)
|
||||||
-- t
|
-- t
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
main = (\x : x) t
|
main = (x : x) t
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
x = map (\i : append "Successfully concatenated " i) [("two strings!")]
|
x = map (i : append "Successfully concatenated " i) [("two strings!")]
|
||||||
main = equal? x [("Successfully concatenated two strings!")]
|
main = equal? x [("Successfully concatenated two strings!")]
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
compose = \f g x : f (g x)
|
compose = f g x : f (g x)
|
||||||
|
|
||||||
succ = y (\self :
|
succ = y (self :
|
||||||
triage
|
triage
|
||||||
1
|
1
|
||||||
t
|
t
|
||||||
(triage
|
(triage
|
||||||
(t (t t))
|
(t (t t))
|
||||||
(\_ tail : t t (self tail))
|
(_ tail : t t (self tail))
|
||||||
t))
|
t))
|
||||||
|
|
||||||
size = (\x :
|
size = (x :
|
||||||
(y (\self x :
|
(y (self x :
|
||||||
compose succ
|
compose succ
|
||||||
(triage
|
(triage
|
||||||
(\x : x)
|
(x : x)
|
||||||
self
|
self
|
||||||
(\x y : compose (self x) (self y))
|
(x y : compose (self x) (self y))
|
||||||
x)) x 0))
|
x)) x 0))
|
||||||
|
|
||||||
size size
|
size size
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
head (map (\i : append "String " i) [("test!")])
|
head (map (i : append "String " i) [("test!")])
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
y = \x : x
|
y = x : x
|
||||||
|
|||||||
37
tricu.cabal
37
tricu.cabal
@@ -1,8 +1,8 @@
|
|||||||
cabal-version: 1.12
|
cabal-version: 1.12
|
||||||
|
|
||||||
name: tricu
|
name: tricu
|
||||||
version: 0.18.0
|
version: 1.1.0
|
||||||
description: A micro-language for exploring Tree Calculus
|
description: A language for exploring Tree Calculus
|
||||||
author: James Eversole
|
author: James Eversole
|
||||||
maintainer: james@eversole.co
|
maintainer: james@eversole.co
|
||||||
copyright: James Eversole
|
copyright: James Eversole
|
||||||
@@ -21,18 +21,34 @@ executable tricu
|
|||||||
LambdaCase
|
LambdaCase
|
||||||
MultiWayIf
|
MultiWayIf
|
||||||
OverloadedStrings
|
OverloadedStrings
|
||||||
|
ScopedTypeVariables
|
||||||
ghc-options: -threaded -rtsopts -with-rtsopts=-N -optl-pthread -fPIC
|
ghc-options: -threaded -rtsopts -with-rtsopts=-N -optl-pthread -fPIC
|
||||||
build-depends:
|
build-depends:
|
||||||
base >=4.7
|
base >=4.7
|
||||||
|
, aeson
|
||||||
|
, ansi-terminal
|
||||||
|
, base16-bytestring
|
||||||
|
, base64-bytestring
|
||||||
|
, bytestring
|
||||||
|
, cereal
|
||||||
, cmdargs
|
, cmdargs
|
||||||
, containers
|
, containers
|
||||||
|
, cryptonite
|
||||||
|
, directory
|
||||||
, exceptions
|
, exceptions
|
||||||
, filepath
|
, filepath
|
||||||
|
, fsnotify
|
||||||
, haskeline
|
, haskeline
|
||||||
, megaparsec
|
, megaparsec
|
||||||
|
, memory
|
||||||
, mtl
|
, mtl
|
||||||
|
, sqlite-simple
|
||||||
|
, tasty
|
||||||
|
, tasty-hunit
|
||||||
, text
|
, text
|
||||||
|
, time
|
||||||
, transformers
|
, transformers
|
||||||
|
, zlib
|
||||||
other-modules:
|
other-modules:
|
||||||
Eval
|
Eval
|
||||||
FileEval
|
FileEval
|
||||||
@@ -51,20 +67,33 @@ test-suite tricu-tests
|
|||||||
LambdaCase
|
LambdaCase
|
||||||
MultiWayIf
|
MultiWayIf
|
||||||
OverloadedStrings
|
OverloadedStrings
|
||||||
|
ScopedTypeVariables
|
||||||
build-depends:
|
build-depends:
|
||||||
base
|
base >=4.7
|
||||||
|
, aeson
|
||||||
|
, ansi-terminal
|
||||||
|
, base16-bytestring
|
||||||
|
, base64-bytestring
|
||||||
|
, bytestring
|
||||||
|
, cereal
|
||||||
, cmdargs
|
, cmdargs
|
||||||
, containers
|
, containers
|
||||||
|
, cryptonite
|
||||||
|
, directory
|
||||||
, exceptions
|
, exceptions
|
||||||
, filepath
|
, filepath
|
||||||
|
, fsnotify
|
||||||
, haskeline
|
, haskeline
|
||||||
, megaparsec
|
, megaparsec
|
||||||
|
, memory
|
||||||
, mtl
|
, mtl
|
||||||
|
, sqlite-simple
|
||||||
, tasty
|
, tasty
|
||||||
, tasty-hunit
|
, tasty-hunit
|
||||||
, tasty-quickcheck
|
|
||||||
, text
|
, text
|
||||||
|
, time
|
||||||
, transformers
|
, transformers
|
||||||
|
, zlib
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
other-modules:
|
other-modules:
|
||||||
Eval
|
Eval
|
||||||
|
|||||||
Reference in New Issue
Block a user