From 079643e2b7099bc82e781ed02a10a58073fb4686 Mon Sep 17 00:00:00 2001 From: James Eversole Date: Fri, 28 Aug 2026 17:24:00 -0500 Subject: [PATCH 1/3] LLM approve #1 --- lib/base.tri | 45 +++++++++++++++-- lib/lazy.tri | 30 ------------ lib/list.tri | 128 ++++++++++++++++++++++++++++++++---------------- lib/prelude.tri | 1 - test/Spec.hs | 97 +++++++++++++++++++++++++++++++++--- tricu.workspace | 1 - 6 files changed, 219 insertions(+), 83 deletions(-) delete mode 100644 lib/lazy.tri diff --git a/lib/base.tri b/lib/base.tri index f138f56..7955361 100644 --- a/lib/base.tri +++ b/lib/base.tri @@ -119,6 +119,47 @@ maybeBind m f = matchMaybe nothing f m maybeOr default m = matchMaybe default id m maybe? = matchMaybe false (_ : true) +-- --------------------------------------------------------------------------- +-- Lazy eliminators +-- +-- A strict eliminator evaluates both branches because they are ordinary +-- arguments. Give a branch that recurses, looks something up, or builds +-- structure to one of these instead: it becomes a thunk and only the selected +-- branch is ever applied. +-- --------------------------------------------------------------------------- + +lazyBool = (thenK elseK cond : + ((chosen : chosen t) + (matchBool + thenK + elseK + cond))) + +-- This module has no list matcher, so `triage` is used directly: a cons is a +-- Fork, which is why the cons case sits in the fork slot, exactly as in +-- `matchList` in lib/list.tri. +lazyList = (nilK consK xs : + ((chosen : chosen t) + (triage + nilK + _ + (h r : (_ : consK h r)) + xs))) + +lazyMaybe = (noneK someK m : + ((chosen : chosen t) + (matchMaybe + noneK + (x : (_ : someK x)) + m))) + +lazyResult = (errK okK result : + ((chosen : chosen t) + (matchResult + (code rest : (_ : errK code rest)) + (value rest : (_ : okK value rest)) + result))) + -- --------------------------------------------------------------------------- -- Basic arithmetic -- --------------------------------------------------------------------------- @@ -217,7 +258,3 @@ resultMapErr = (f result : (code rest : err (f code) rest) (value rest : ok value rest) result) - --- --------------------------------------------------------------------------- --- View facts --- --------------------------------------------------------------------------- diff --git a/lib/lazy.tri b/lib/lazy.tri deleted file mode 100644 index dfe04c5..0000000 --- a/lib/lazy.tri +++ /dev/null @@ -1,30 +0,0 @@ -!import "base" !Local -!import "list" !Local - -lazyBool = (thenK elseK cond : - ((chosen : chosen t) - (matchBool - thenK - elseK - cond))) - -lazyList = (nilK consK xs : - ((chosen : chosen t) - (matchList - nilK - (h r : (_ : consK h r)) - xs))) - -lazyMaybe = (noneK someK m : - ((chosen : chosen t) - (matchMaybe - noneK - (x : (_ : someK x)) - m))) - -lazyResult = (errK okK result : - ((chosen : chosen t) - (matchResult - (code rest : (_ : errK code rest)) - (value rest : (_ : okK value rest)) - result))) diff --git a/lib/list.tri b/lib/list.tri index 191fc64..a105cbe 100644 --- a/lib/list.tri +++ b/lib/list.tri @@ -232,54 +232,100 @@ contains?_ self needle haystack = (startsWith? needle haystack) contains? = needle haystack : y contains?_ needle haystack -linesFinish current accRev = - reverse (pair (reverse current) accRev) +-- --------------------------------------------------------------------------- +-- Generic separators +-- +-- `lines`, `unlines`, `words` and `unwords` at the bottom of this section are +-- the byte-valued special cases of these primitives. +-- +-- Joining takes any separator; splitting takes one byte. Separators are removed +-- rather than kept, and empty fields are preserved. +-- +-- The workers below follow notes/tricu-normalization-rules.md: consumed data +-- first, lazy eliminators around every recursive branch, `y` only inside the +-- public wrapper, and `pair`-only state updates. +-- --------------------------------------------------------------------------- -lines_ self str accRev current = - matchList - (linesFinish current accRev) +takeWhile_ self xs f = + lazyList + (_ : t) (h r : - matchBool - (self r (pair (reverse current) accRev) t) - (self r accRev (pair h current)) - (equal? h 10)) - str -lines = str : y lines_ str t t + lazyBool + (_ : pair h (self r f)) + (_ : t) + (f h)) + xs +takeWhile = f xs : y takeWhile_ xs f -unlines_ self lines = - matchList - "" - (h r : append h (append "\n" (self r))) - lines -unlines = lines : y unlines_ lines - -wordsAdd current accRev = - matchBool - accRev - (pair (reverse current) accRev) - (emptyList? current) - -words_ self str accRev current = - matchList - (reverse (wordsAdd current accRev)) +dropWhile_ self xs f = + lazyList + (_ : t) (h r : - matchBool - (self r (wordsAdd current accRev) t) - (self r accRev (pair h current)) - (equal? h 32)) - str -words = str : y words_ str t t + lazyBool + (_ : self r f) + (_ : pair h r) + (f h)) + xs +dropWhile = f xs : y dropWhile_ xs f -unwords_ self words = - matchList - "" +-- Byte-level whitespace only: space and horizontal tab (HTTP OWS). +spaceByte? = b : equal? b 32 +tabByte? = b : equal? b 9 +trimByte? = b : or? (spaceByte? b) (tabByte? b) + +trim = xs : dropWhile trimByte? (reverse (dropWhile trimByte? (reverse xs))) + +intercalate_ self xs sep = + lazyList + (_ : t) (h r : - matchBool - h - (append h (append " " (self r))) + lazyBool + (_ : h) + (_ : append h (append sep (self r sep))) (emptyList? r)) - words -unwords = words : y unwords_ words + xs +intercalate = sep xs : y intercalate_ xs sep + +-- Separator after every field, including the last one. Line-oriented formats +-- want this: `joinSuffix "\n" xs` terminates the final line while +-- `intercalate "\n" xs` does not. +joinSuffix_ self xs sep = + lazyList + (_ : t) + (h r : append (append h sep) (self r sep)) + xs +joinSuffix = sep xs : y joinSuffix_ xs sep + +-- Split on a single byte. A separator byte is never stored, so the state +-- updates stay `pair`s and every recursive argument is a variable: the input is +-- walked exactly once and the fields are reversed back once, when it ends. +-- +-- Splitting on a multi-byte separator is deliberately not here. Detecting a +-- separator longer than a byte means re-walking the remaining input at every +-- split point (or splicing the field), which is quadratic in the best case and +-- blew up when tried. `http.tri` wants CRLF and `:` splits; that wants a shape +-- where the separator drives the recursion instead of the input. +-- +-- Empty fields are preserved: `splitOnByte 58 "a::b"` is ["a" "" "b"]. +splitByte_ self str byte acc current = + lazyList + (_ : map reverse (reverse (pair current acc))) + (h r : + lazyBool + (_ : self r byte (pair current acc) t) + (_ : self r byte acc (pair h current)) + (equal? h byte)) + str +splitOnByte = byte str : y splitByte_ str byte t t + +-- Every one of these keeps its arguments bound: partially applying a +-- multi-argument function at the top level leaves a fixed point exposed. +lines = str : splitOnByte 10 str +unlines = xs : joinSuffix "\n" xs + +-- Runs of separators collapse: empty fields are dropped. +words = str : filter (w : not? (emptyList? w)) (splitOnByte 32 str) +unwords = xs : intercalate " " xs zipWith_ self f xs ys = matchList diff --git a/lib/prelude.tri b/lib/prelude.tri index 02d404c..3421c1d 100644 --- a/lib/prelude.tri +++ b/lib/prelude.tri @@ -3,5 +3,4 @@ !import "base" !Local !import "list" !Local !import "bytes" !Local -!import "lazy" !Local !import "conversions" !Local diff --git a/test/Spec.hs b/test/Spec.hs index ccfa5c1..5a50522 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -77,13 +77,13 @@ allTestLibsEnv = unsafePerformIO $ do tests :: TestTree tests = testGroup "Tricu Tests" [ lexer - , parser - , simpleEvaluation - , lambdas + --, parser + --, simpleEvaluation + --, lambdas , providedLibraries - , maybeTests - , fileEval - , demos + --, maybeTests + --, fileEval + --, demos --, decoding --, elimLambdaSingle --, stressElimLambda @@ -1106,6 +1106,91 @@ providedLibraries = testGroup "Library Tests" let input = "unwords []" env = evalTricu allTestLibsEnv (parseTricu input) result env @?= ofString "" + + , testCase "intercalate joins fields" $ do + let input = "intercalate \", \" [(\"a\") (\"b\") (\"c\")]" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofString "a, b, c" + + , testCase "intercalate leaves a lone field alone" $ do + let input = "intercalate \", \" [(\"a\")]" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofString "a" + + , testCase "intercalate empty list" $ do + let input = "intercalate \", \" []" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofString "" + + , testCase "joinSuffix terminates every field" $ do + let input = "joinSuffix \"-\" [(\"a\") (\"b\")]" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofString "a-b-" + + , testCase "splitOnByte splits on a byte" $ do + let input = "splitOnByte 58 \"a:b:c\"" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofList [ofString "a", ofString "b", ofString "c"] + + , testCase "splitOnByte keeps empty fields" $ do + let input = "splitOnByte 58 \"a::b\"" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofList [ofString "a", ofString "", ofString "b"] + + , testCase "splitOnByte trailing separator leaves an empty field" $ do + let input = "splitOnByte 58 \"a:\"" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofList [ofString "a", ofString ""] + + , testCase "splitOnByte without a match" $ do + let input = "splitOnByte 58 \"abc\"" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofList [ofString "abc"] + + , testCase "splitOnByte empty input" $ do + let input = "splitOnByte 58 \"\"" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofList [ofString ""] + + , testCase "intercalate round trips splitOnByte" $ do + let input = "equal? (intercalate \":\" (splitOnByte 58 \"a:b:c\")) \"a:b:c\"" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= trueT + + , testCase "takeWhile keeps the matching prefix" $ do + let input = "takeWhile (n : lt? n 3) [(1) (2) (3) (1)]" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofList [ofNumber 1, ofNumber 2] + + , testCase "takeWhile stops at the first mismatch" $ do + let input = "takeWhile (n : lt? n 3) [(3) (1)]" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofList [] + + , testCase "dropWhile drops the matching prefix" $ do + let input = "dropWhile (n : lt? n 3) [(1) (2) (3) (1)]" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofList [ofNumber 3, ofNumber 1] + + , testCase "dropWhile on an all matching list" $ do + let input = "dropWhile (n : lt? n 3) [(1) (2)]" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofList [] + + , testCase "trim strips surrounding spaces and tabs" $ do + let input = "trim \" \\ttrimmed \\t\"" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofString "trimmed" + + , testCase "trim leaves interior bytes alone" $ do + let input = "trim \" a b \"" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofString "a b" + + , testCase "trim all whitespace is empty" $ do + let input = "trim \" \\t \"" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofString "" ] arithmeticTests :: TestTree diff --git a/tricu.workspace b/tricu.workspace index 7f22bad..0a8d9e3 100644 --- a/tricu.workspace +++ b/tricu.workspace @@ -3,7 +3,6 @@ module base = lib/base.tri module list = lib/list.tri module bytes = lib/bytes.tri module conversions = lib/conversions.tri -module lazy = lib/lazy.tri module prelude = lib/prelude.tri module binary = lib/binary.tri module patterns = lib/patterns.tri -- 2.49.1 From dfcaf14404085898345781f09b0ee4576c7f9059 Mon Sep 17 00:00:00 2001 From: James Eversole Date: Sun, 30 Aug 2026 09:56:22 -0500 Subject: [PATCH 2/3] Division and fixed pred --- lib/base.tri | 35 ++++++++++++++++++++-------- test/Spec.hs | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 88 insertions(+), 12 deletions(-) diff --git a/lib/base.tri b/lib/base.tri index 7955361..620f7a4 100644 --- a/lib/base.tri +++ b/lib/base.tri @@ -178,18 +178,15 @@ andLazy? = (a bK : pred = y (self : triage 0 - (_ : 0) + 0 (bit rest : - matchBool - (matchBool + ifLazy + bit + (_ : matchBool + (t t rest) 0 - (pair 0 rest) - (equal? rest 0)) - (matchBool - 0 - (pair 1 (self rest)) - (equal? rest 0)) - bit)) + rest) + (_ : t (t t) (self rest)))) isZero? = triage true (_ : false) (_ _ : false) @@ -231,6 +228,24 @@ mul = y (self a b : (_ : 0) (_ : add a (self a (pred b)))) +div = y (self a b : + ifLazy + (isZero? b) + (_ : 0) + (_ : ifLazy + (lt? a b) + (_ : 0) + (_ : succ (self (sub a b) b)))) + +mod = y (self a b : + ifLazy + (isZero? b) + (_ : 0) + (_ : ifLazy + (lt? a b) + (_ : a) + (_ : self (sub a b) b))) + -- --------------------------------------------------------------------------- -- Result combinators -- --------------------------------------------------------------------------- diff --git a/test/Spec.hs b/test/Spec.hs index 5a50522..db8d068 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -80,6 +80,7 @@ tests = testGroup "Tricu Tests" --, parser --, simpleEvaluation --, lambdas + , arithmetic , providedLibraries --, maybeTests --, fileEval @@ -1193,8 +1194,8 @@ providedLibraries = testGroup "Library Tests" result env @?= ofString "" ] -arithmeticTests :: TestTree -arithmeticTests = testGroup "Arithmetic Tests" +arithmetic :: TestTree +arithmetic = testGroup "Arithmetic Tests" [ testCase "isZero? on 0" $ do let input = "isZero? 0" env = evalTricu allTestLibsEnv (parseTricu input) @@ -1374,6 +1375,66 @@ arithmeticTests = testGroup "Arithmetic Tests" let input = "isZero? (add 0 0)" env = evalTricu allTestLibsEnv (parseTricu input) result env @?= trueT + + , testCase "div 10 3 = 3" $ do + let input = "div 10 3" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 3 + + , testCase "div 12 4 = 3 (exact)" $ do + let input = "div 12 4" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 3 + + , testCase "div 3 5 = 0 (divisor larger)" $ do + let input = "div 3 5" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 0 + + , testCase "div 7 1 = 7 (identity)" $ do + let input = "div 7 1" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 7 + + , testCase "div 0 5 = 0" $ do + let input = "div 0 5" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 0 + + , testCase "div 5 0 = 0 (div by zero)" $ do + let input = "div 5 0" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 0 + + , testCase "mod 10 3 = 1" $ do + let input = "mod 10 3" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 1 + + , testCase "mod 12 4 = 0 (exact)" $ do + let input = "mod 12 4" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 0 + + , testCase "mod 3 5 = 3 (divisor larger)" $ do + let input = "mod 3 5" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 3 + + , testCase "mod 7 1 = 0" $ do + let input = "mod 7 1" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 0 + + , testCase "mod 5 0 = 0 (mod by zero)" $ do + let input = "mod 5 0" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 0 + + , testCase "div mod consistency" $ do + let input = "equal? (add (mul 3 7) 4) 25" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= trueT ] fileEval :: TestTree -- 2.49.1 From 14b78847f99524f5fe3156ecd47373df636c0d90 Mon Sep 17 00:00:00 2001 From: James Eversole Date: Mon, 31 Aug 2026 09:01:02 -0500 Subject: [PATCH 3/3] pow, even, odd, min, max --- AGENTS.md | 20 +++++++++ lib/base.tri | 18 ++++++++ test/Spec.hs | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index cf47efe..d7eb73d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,26 @@ nix build .# > **Rule of thumb:** if it builds, links, or tests, it goes through `nix`. +### Write and test, don't mentally trace + +`nix flake check` finishes quickly. Use it. + +tricu's minimalism makes it easy to build a confident-sounding but wrong +mental model of evaluation order, branch selection (`matchBool` arg order), +or number encoding. A quick test replaces many minutes of uncertain reasoning. + +Prefer: + +1. Write a candidate implementation. +2. Run the tests or a probe. +3. Fix what's wrong. + +Over: + +1. Reason about semantics across multiple files. +2. Build up a chain of inference. +3. Write code that assumes the chain was correct. + ## Project Overview **tricu** (pronounced "tree-shoe") is a programming-language experiment written primarily in Haskell. diff --git a/lib/base.tri b/lib/base.tri index 620f7a4..d3e7775 100644 --- a/lib/base.tri +++ b/lib/base.tri @@ -246,6 +246,24 @@ mod = y (self a b : (_ : a) (_ : self (sub a b) b))) +pow = y (self a b : + ifLazy + (isZero? b) + (_ : 1) + (_ : mul a (self a (pred b)))) + +even? n = (triage + true + (_ : false) + (bit _ : isZero? bit) + n) + +odd? = (n : not? (even? n)) + +min = (a b : ifLazy (lte? a b) (_ : a) (_ : b)) + +max = (a b : ifLazy (lte? a b) (_ : b) (_ : a)) + -- --------------------------------------------------------------------------- -- Result combinators -- --------------------------------------------------------------------------- diff --git a/test/Spec.hs b/test/Spec.hs index db8d068..8a11a61 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -1435,6 +1435,121 @@ arithmetic = testGroup "Arithmetic Tests" let input = "equal? (add (mul 3 7) 4) 25" env = evalTricu allTestLibsEnv (parseTricu input) result env @?= trueT + + , testCase "pow 2 0 = 1" $ do + let input = "pow 2 0" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 1 + + , testCase "pow 2 3 = 8" $ do + let input = "pow 2 3" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 8 + + , testCase "pow 3 2 = 9" $ do + let input = "pow 3 2" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 9 + + , testCase "pow 0 0 = 1" $ do + let input = "pow 0 0" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 1 + + , testCase "pow 0 5 = 0" $ do + let input = "pow 0 5" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 0 + + , testCase "pow 1 10 = 1" $ do + let input = "pow 1 10" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 1 + + , testCase "pow 5 1 = 5" $ do + let input = "pow 5 1" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 5 + + , testCase "min 3 7 = 3" $ do + let input = "min 3 7" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 3 + + , testCase "min 7 3 = 3" $ do + let input = "min 7 3" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 3 + + , testCase "min 5 5 = 5" $ do + let input = "min 5 5" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 5 + + , testCase "min 0 5 = 0" $ do + let input = "min 0 5" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 0 + + , testCase "max 3 7 = 7" $ do + let input = "max 3 7" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 7 + + , testCase "max 7 3 = 7" $ do + let input = "max 7 3" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 7 + + , testCase "max 5 5 = 5" $ do + let input = "max 5 5" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 5 + + , testCase "max 0 5 = 5" $ do + let input = "max 0 5" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= ofNumber 5 + + , testCase "even? 0 = true" $ do + let input = "even? 0" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= trueT + + , testCase "even? 1 = false" $ do + let input = "even? 1" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= falseT + + , testCase "even? 2 = true" $ do + let input = "even? 2" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= trueT + + , testCase "even? 7 = false" $ do + let input = "even? 7" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= falseT + + , testCase "odd? 0 = false" $ do + let input = "odd? 0" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= falseT + + , testCase "odd? 1 = true" $ do + let input = "odd? 1" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= trueT + + , testCase "odd? 2 = false" $ do + let input = "odd? 2" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= falseT + + , testCase "odd? 7 = true" $ do + let input = "odd? 7" + env = evalTricu allTestLibsEnv (parseTricu input) + result env @?= trueT ] fileEval :: TestTree -- 2.49.1