diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8e5b8ff..f727812 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,11 +11,20 @@ jobs: steps: - uses: actions/checkout@v4 - # Python, Node.js, Perl, GCC/G++ 14 come preinstalled on the runner. + # Python, Node.js, Perl, GCC/G++ 14, and the .NET SDK (F#) come + # preinstalled on the runner. - name: Install toolchains not preinstalled on the runner run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends racket + sudo apt-get install -y --no-install-recommends \ + racket ruby lua5.4 ghc ocaml elixir + + # Clojure's deps.edn CLI (clj) is not in apt; use the official installer. + - name: Install Clojure CLI + run: | + curl -L -O https://github.com/clojure/brew-install/releases/latest/download/linux-install.sh + chmod +x linux-install.sh + sudo ./linux-install.sh - uses: oven-sh/setup-bun@v2 diff --git a/ROADMAP.md b/ROADMAP.md index bfd0cc4..d5715ea 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -15,14 +15,22 @@ Folders without a `test.sh` are skipped until backfilled. - [x] **Step 1 — harness + locally verifiable languages.** Workflow file, `run-tests.sh`, and test.sh/expected-output.txt for Python, JavaScript, TypeScript (Bun), Perl, Racket, and C. All six verified locally. -- [ ] **Step 2 — C++ and Go.** C++ needs GCC 14 (`` header) — not on +- [x] **Step 2 — C++ and Go.** C++ needs GCC 14 (`` header) — not on the dev machine, so capture its output from the CI runner log on the PR, then commit it as the expected file. Go is preinstalled on runners. -- [ ] **Step 3 — Ruby, Lua, Haskell.** All installable on the runner via apt - (`ruby`, `lua5.4`, `ghc`); add installs to the workflow's toolchain step. -- [ ] **Step 4 — OCaml, Elixir, Clojure, F#.** apt/preinstalled on runners - (`ocaml`, `elixir`, `clojure`; F# via preinstalled dotnet). Clojure runs - through its own deps.edn test runner — wrap it in test.sh. +- [x] **Step 3 — Ruby, Lua, Haskell.** `test.sh` added for all three + (`ruby lambda-core.rb`, `lua5.4 lambda-core.lua`, `runghc lambda-core.hs`), + the apt installs (`ruby lua5.4 ghc`) added to the workflow, and each + folder's `expected-output.txt` captured from a real local run. All three + PASS locally. +- [x] **Step 4 — OCaml, Elixir, Clojure, F#.** `test.sh` for all four + (`ocaml lambda_core.ml`, `elixir lambda-core.exs`, `dotnet fsi + lambda-core.fsx`, and `cd lambda-core && clojure -M:test` for Clojure's + deps.edn test runner). Workflow installs `ocaml`+`elixir` via apt and the + Clojure CLI via its official installer; F# uses the runner's preinstalled + dotnet. OCaml + Elixir captured locally; F# and Clojure captured from the + CI log (PR #31). Clojure uses `clojure`, not the interactive `clj` wrapper + (which printed an rlwrap notice instead of running). All four green on CI. - [ ] **Step 5 — Java and Kotlin.** Java: PR #29 (the `java` branch) fixes the numeral typing so PRED works; add test.sh + expected output to that branch, let CI validate it, then merge #29. Kotlin: runner has a JDK; diff --git a/languages/c/c++/expected-output.txt b/languages/c/c++/expected-output.txt new file mode 100644 index 0000000..2cf8dc8 --- /dev/null +++ b/languages/c/c++/expected-output.txt @@ -0,0 +1,2 @@ +true +1 diff --git a/languages/c/c++/test.sh b/languages/c/c++/test.sh new file mode 100644 index 0000000..ea9b07d --- /dev/null +++ b/languages/c/c++/test.sh @@ -0,0 +1,13 @@ +# C++23 needs GCC 14+; g++-14 is preinstalled on the CI runner. If it +# isn't present (e.g. an older dev machine) skip (exit 42) rather than fail on a +# plain g++ that can't compile the source. +CXX=$(command -v g++-14) +if [ -z "$CXX" ]; then + echo "g++-14 not found — needs GCC 14+ for C++23 " >&2 + exit 42 +fi +"$CXX" lambda-core.cpp -std=c++23 -o lambda-core.test || exit 1 +./lambda-core.test +status=$? +rm -f lambda-core.test +exit $status diff --git a/languages/c/clojure/expected-output.txt b/languages/c/clojure/expected-output.txt new file mode 100644 index 0000000..df8e066 --- /dev/null +++ b/languages/c/clojure/expected-output.txt @@ -0,0 +1,65 @@ + +Testing numerals-test +("numeral expressions") = (toInt ((minus ((mult (fromInt 3)) ((plus (fromInt 2)) (fromInt 5)))) ((exp (fromInt 2)) (fromInt 3)))) => 13 +("addition") = (toInt ((plus (fromInt 7)) (fromInt 5))) => 12 +("addition") = (toInt ((plus (fromInt 7)) ((plus (fromInt 6)) (fromInt 2)))) => 15 +("subtraction") = (toInt ((minus (fromInt 7)) (fromInt 5))) => 2 +("subtraction") = (toInt ((minus (fromInt 7)) ((minus (fromInt 6)) (fromInt 2)))) => 3 +("multiplication") = (toInt ((mult (fromInt 2)) (fromInt 3))) => 6 +("multiplication") = (toInt ((mult (fromInt 2)) ((mult (fromInt 5)) (fromInt 3)))) => 30 +("exponentiation") = (toInt ((exp (fromInt 2)) (fromInt 3))) => 8 +("exponentiation") = (toInt ((exp (fromInt 2)) ((exp (fromInt 2)) (fromInt 3)))) => 256 +("zero") = (toInt zero) => 0 +("one") = (toInt (succ zero)) => 1 +("one") = (toInt one) => 1 +("two") = (toInt (succ (succ zero))) => 2 +("two") = (toInt two) => 2 +("three") = (toInt (succ (succ (succ zero)))) => 3 +("predecessor") = (toInt (pred one)) => 0 +("predecessor") = (toInt (pred two)) => 1 +("predecessor") = (toInt (pred (succ (succ (succ zero))))) => 2 +("predecessor") = (toInt (pred (fromInt 10))) => 9 +("toStr") = (toStr zero) => "λf.λn.(n)" +("toStr") = (toStr one) => "λf.λn.(f(n))" +("toStr") = (toStr two) => "λf.λn.(f(f(n)))" +("toStr") = (toStr (succ (succ (succ zero)))) => "λf.λn.(f(f(f(n))))" +("toStr") = (toStr (fromInt 5)) => "λf.λn.(f(f(f(f(f(n))))))" + +Testing booleans-test +("true") = (toBoolean T) => true +("false") = (toBoolean F) => false +("If") = (toBoolean (((If T) T) F)) => true +("If") = (toBoolean (((If F) T) F)) => false +("And") = (toBoolean ((And T) T)) => true +("And") = (toBoolean ((And T) F)) => false +("And") = (toBoolean ((And F) T)) => false +("And") = (toBoolean ((And F) F)) => false +("And") = (toBoolean ((And T) ((And T) T))) => true +("And") = (toBoolean ((And T) ((And F) T))) => false +("Or") = (toBoolean ((Or T) T)) => true +("Or") = (toBoolean ((Or T) F)) => true +("Or") = (toBoolean ((Or F) T)) => true +("Or") = (toBoolean ((Or F) F)) => false +("Or") = (toBoolean ((Or F) ((Or F) F))) => false +("Or") = (toBoolean ((Or F) ((Or T) F))) => true +("Not") = (toBoolean (Not T)) => false +("Not") = (toBoolean (Not F)) => true +("Not") = (toBoolean (Not (Not T))) => true +("Not") = (toBoolean (Not (Not F))) => false +("Xor") = (toBoolean ((Xor T) T)) => false +("Xor") = (toBoolean ((Xor T) F)) => true +("Xor") = (toBoolean ((Xor F) T)) => true +("Xor") = (toBoolean ((Xor F) F)) => false +("Expressions") = (toBoolean ((And T) ((And T) ((Or F) T)))) => true +("Expressions") = (toBoolean ((And T) ((And F) ((Or F) ((And T) T))))) => false +("Expressions") = (toBoolean ((Or T) ((And F) ((Or F) ((And T) T))))) => true +("Expressions") = (toBoolean ((Or F) ((And F) ((Or F) ((And F) T))))) => false +("Expressions") = (toBoolean ((And ((Or F) F)) ((And T) ((Or F) T)))) => false +("Expressions") = (toBoolean ((And ((Or T) F)) ((And T) ((Or F) T)))) => true + +Testing combinators-test +("Z-factorial") = (toInt (Z-factorial (fromInt 9) one)) => 362880 +("Y-factorial") = (toInt (Y-factorial (fromInt 9) one)) => 362880 + +Ran 7 tests containing 56 assertions. +0 failures, 0 errors. diff --git a/languages/c/clojure/test.sh b/languages/c/clojure/test.sh new file mode 100644 index 0000000..20207d5 --- /dev/null +++ b/languages/c/clojure/test.sh @@ -0,0 +1,4 @@ +# The Clojure impl is a deps.edn project with its own clojure.test runner. +# Use `clojure`, not `clj`: `clj` is the interactive rlwrap wrapper and prints +# an "install rlwrap / use clojure instead" notice when run non-interactively. +cd lambda-core && clojure -M:test diff --git a/languages/e/elixir/expected-output.txt b/languages/e/elixir/expected-output.txt new file mode 100644 index 0000000..e39dc20 --- /dev/null +++ b/languages/e/elixir/expected-output.txt @@ -0,0 +1,30 @@ +LOGIC +-------------- +TRUE/FALSE +t +f +NOT +f +t +AND +t +f +f +f +OR +t +t +t +f + +CHURCH NUMERALS +-------------- +ZERO/SUCC +0 +1 +2 +3 +PRED +0 +1 +2 diff --git a/languages/e/elixir/test.sh b/languages/e/elixir/test.sh new file mode 100644 index 0000000..2d0d27f --- /dev/null +++ b/languages/e/elixir/test.sh @@ -0,0 +1 @@ +elixir lambda-core.exs diff --git a/languages/f/fsharp/expected-output.txt b/languages/f/fsharp/expected-output.txt new file mode 100644 index 0000000..3d915b6 --- /dev/null +++ b/languages/f/fsharp/expected-output.txt @@ -0,0 +1,29 @@ +LOGIC +---------------- +TRUE/FALSE +true +false +NOT +false +true +AND +true +false +false +false +OR +true +true +true +false +CHURCH NUMERALS +---------------- +ZERO/SUCC +0 +1 +2 +3 +PRED +0 +1 +2 diff --git a/languages/f/fsharp/test.sh b/languages/f/fsharp/test.sh new file mode 100644 index 0000000..c14d39c --- /dev/null +++ b/languages/f/fsharp/test.sh @@ -0,0 +1,3 @@ +# DOTNET_NOLOGO / telemetry opt-out keep the first-run banner off stdout so the +# captured output is just the program's. +DOTNET_NOLOGO=1 DOTNET_CLI_TELEMETRY_OPTOUT=1 dotnet fsi lambda-core.fsx diff --git a/languages/g/go/expected-output.txt b/languages/g/go/expected-output.txt new file mode 100644 index 0000000..4ba30d7 --- /dev/null +++ b/languages/g/go/expected-output.txt @@ -0,0 +1,30 @@ +LOGIC +------------- +TRUE/FALSE +true +false +NOT +false +true +AND +true +false +false +false +OR +true +true +true +false + +CHURCH NUMERALS +------------- +ZERO/SUCC +0 +1 +2 +3 +PRED +0 +1 +2 diff --git a/languages/g/go/test.sh b/languages/g/go/test.sh new file mode 100644 index 0000000..4beb55a --- /dev/null +++ b/languages/g/go/test.sh @@ -0,0 +1 @@ +go run . diff --git a/languages/h/haskell/expected-output.txt b/languages/h/haskell/expected-output.txt new file mode 100644 index 0000000..49e9e5e --- /dev/null +++ b/languages/h/haskell/expected-output.txt @@ -0,0 +1,16 @@ +True +False +False +True +False +False +False +True +False +True +True +True +0 +1 +1 +0 diff --git a/languages/h/haskell/test.sh b/languages/h/haskell/test.sh new file mode 100644 index 0000000..571ee04 --- /dev/null +++ b/languages/h/haskell/test.sh @@ -0,0 +1 @@ +runghc lambda-core.hs diff --git a/languages/l/lua/expected-output.txt b/languages/l/lua/expected-output.txt new file mode 100644 index 0000000..80087d5 --- /dev/null +++ b/languages/l/lua/expected-output.txt @@ -0,0 +1,22 @@ +true +false +false +true +true +false +false +false +true +true +true +false +0 +1 +2 +3 +0 +0 +1 +1 +2 +2 diff --git a/languages/l/lua/test.sh b/languages/l/lua/test.sh new file mode 100644 index 0000000..520a342 --- /dev/null +++ b/languages/l/lua/test.sh @@ -0,0 +1 @@ +lua5.4 lambda-core.lua diff --git a/languages/o/ocaml/expected-output.txt b/languages/o/ocaml/expected-output.txt new file mode 100644 index 0000000..e00ba50 --- /dev/null +++ b/languages/o/ocaml/expected-output.txt @@ -0,0 +1,30 @@ +LOGIC +---------------- +TRUE/FALSE +true +false +NOT +false +true +AND +true +false +false +false +OR +true +true +true +false + +CHURCH NUMERALS +---------------- +ZERO/SUCC +0 +1 +2 +3 +PRED +0 +1 +2 diff --git a/languages/o/ocaml/test.sh b/languages/o/ocaml/test.sh new file mode 100644 index 0000000..3ad92ab --- /dev/null +++ b/languages/o/ocaml/test.sh @@ -0,0 +1 @@ +ocaml lambda_core.ml diff --git a/languages/r/ruby/expected-output.txt b/languages/r/ruby/expected-output.txt new file mode 100644 index 0000000..1b055f1 --- /dev/null +++ b/languages/r/ruby/expected-output.txt @@ -0,0 +1,30 @@ +LOGIC +--------------- +TRUE/FALSE +t +f +NOT +f +t +AND +t +f +f +f +OR +t +t +t +f + +CHURCH NUMERALS +--------------- +ZERO/SUCC +0 +1 +2 +3 +PRED +0 +1 +2 diff --git a/languages/r/ruby/test.sh b/languages/r/ruby/test.sh new file mode 100644 index 0000000..2d8723c --- /dev/null +++ b/languages/r/ruby/test.sh @@ -0,0 +1 @@ +ruby lambda-core.rb diff --git a/run-tests.sh b/run-tests.sh index 705f109..0435023 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -1,35 +1,79 @@ #!/bin/sh -# Runs every language folder that has a test.sh and compares its stdout -# against that folder's expected-output.txt. Exits nonzero if any differ. -# Folders without a test.sh are skipped (not yet backfilled). +# Runs every language folder that has a test.sh and compares its stdout against +# that folder's expected-output.txt. Result categories: +# +# PASS ran, exit 0, output matched the baseline. +# FAIL ran but exited nonzero or output did not match. A real problem. +# This is the ONLY category that makes the script exit nonzero. +# SKIP the language's toolchain is not available in this environment: the +# command was not found (exit 127) or the test.sh opted out (exit 42). +# Skipping is normal locally for CI-only toolchains. On CI (the CI env +# var is set) a skip is upgraded to a FAIL, because the runner is +# supposed to have every toolchain installed — a missing one there is +# a broken install, not an expected absence. +# PENDING the folder has a test.sh but no expected-output.txt baseline yet. +# Its output is printed so it can be captured; never a failure. +# +# Folders without a test.sh are skipped entirely (not yet backfilled). set -u root=$(cd "$(dirname "$0")" && pwd) +on_ci=${CI:-} + +pass=0 fail=0 -ran=0 +skip=0 +pending=0 +failed_names="" for script in "$root"/languages/*/*/test.sh; do [ -e "$script" ] || continue dir=$(dirname "$script") name=${dir#"$root"/languages/} - ran=$((ran + 1)) + + if [ ! -f "$dir/expected-output.txt" ]; then + actual=$( (cd "$dir" && sh test.sh 2>/dev/null) ) + echo "PENDING $name (no expected-output.txt baseline yet)" + echo "--- captured output (paste into expected-output.txt once verified) ---" + echo "$actual" + echo "---" + pending=$((pending + 1)) + continue + fi actual=$( (cd "$dir" && sh test.sh 2>/dev/null) ) status=$? expected=$(cat "$dir/expected-output.txt") if [ "$status" -eq 0 ] && [ "$actual" = "$expected" ]; then - echo "PASS $name" + echo "PASS $name" + pass=$((pass + 1)) + elif [ "$status" -eq 42 ] || [ "$status" -eq 127 ]; then + if [ -n "$on_ci" ]; then + echo "FAIL $name (toolchain missing on CI — exit $status)" + fail=$((fail + 1)) + failed_names="$failed_names $name" + else + echo "SKIP $name (toolchain not installed here)" + skip=$((skip + 1)) + fi else - echo "FAIL $name (exit $status)" + echo "FAIL $name (exit $status)" echo "--- expected ---" echo "$expected" echo "--- actual ---" echo "$actual" echo "---" - fail=1 + fail=$((fail + 1)) + failed_names="$failed_names $name" fi done -echo "$ran folder(s) tested" -exit $fail +echo +echo "Summary: $pass passed, $fail failed, $skip skipped, $pending pending" +if [ -n "$failed_names" ]; then + echo "FAILED:$failed_names" +fi + +[ "$fail" -eq 0 ] || exit 1 +exit 0