diff --git a/ROADMAP.md b/ROADMAP.md index d5715ea..2df9569 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -50,7 +50,7 @@ Folders without a `test.sh` are skipped until backfilled. One language per step, each landing as its own PR with implementation, README, test.sh, and expected output, CI-green before merge. Order roughly by reach: -- [ ] Rust +- [x] Rust - [ ] C# - [ ] Swift - [ ] Scala diff --git a/languages/r/rust/README.md b/languages/r/rust/README.md new file mode 100644 index 0000000..2d2bf97 --- /dev/null +++ b/languages/r/rust/README.md @@ -0,0 +1,54 @@ +# Rust Lambda Core + +This implementation expresses the project's Church booleans and Church +numerals as callable Rust values. It includes `TRUE`, `FALSE`, `NOT`, `AND`, +`OR`, `ZERO`, `SUCC`, `PRED`, and `ONE`, then checks and prints examples of +each operation. + +## Requirements + +- A stable Rust compiler (`rustc`) +- No external crates or other dependencies + +Rust is included in GitHub's current +[`ubuntu-latest` runner image](https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Readme.md#rust-tools), +so this folder does not require a workflow installation step. + +## Run and verify + +From this directory, run: + +```sh +sh test.sh +``` + +The test script compiles `lambda-core.rs` with warnings treated as errors, +runs it, and removes the temporary executable. From the repository root, +`./run-tests.sh` also runs this script and compares its output with +`expected-output.txt`. + +## How the representation works + +A lambda-calculus term accepts one term and returns another. Rust cannot give +a directly recursive closure type a finite size, so `Term` stores the closure +behind `Rc`: + +```rust +struct Term(Rc Term>); +``` + +`Rc` is part of Rust's standard library. It supplies the required indirection +and lets a captured term be referenced more than once; it does not implement +any boolean or numeral behavior itself. + +The core definitions are direct translations of the lambda-calculus forms. +For example, `TRUE` selects its first argument, `FALSE` selects its second, +and `SUCC` applies a supplied function once more than its input numeral does. +`PRED` uses the standard higher-order predecessor expression shown beside its +implementation. + +Only the two output helpers cross back into native Rust values. The boolean +helper gives a term two distinct markers and reports which marker it selects. +The numeral helper gives a term an incrementing function and counts how many +times that function is applied. The encodings and operations themselves never +store or inspect a Rust `bool` or integer. diff --git a/languages/r/rust/expected-output.txt b/languages/r/rust/expected-output.txt new file mode 100644 index 0000000..ee3f6fe --- /dev/null +++ b/languages/r/rust/expected-output.txt @@ -0,0 +1,20 @@ +CHURCH BOOLEANS +TRUE: true +FALSE: false +NOT TRUE: false +NOT FALSE: true +FALSE AND FALSE: false +FALSE AND TRUE: false +TRUE AND FALSE: false +TRUE AND TRUE: true +FALSE OR FALSE: false +FALSE OR TRUE: true +TRUE OR FALSE: true +TRUE OR TRUE: true +CHURCH NUMERALS +ZERO: 0 +ONE: 1 +SUCC ONE: 2 +PRED ZERO: 0 +PRED ONE: 0 +PRED (SUCC ONE): 1 diff --git a/languages/r/rust/lambda-core.rs b/languages/r/rust/lambda-core.rs new file mode 100644 index 0000000..5d726a8 --- /dev/null +++ b/languages/r/rust/lambda-core.rs @@ -0,0 +1,197 @@ +use std::cell::Cell; +use std::rc::Rc; + +// A lambda-calculus term is a function from one term to another term. +// Rc supplies the indirection needed for this recursive type and lets a term +// be referenced more than once without copying the closure inside it. +#[derive(Clone)] +struct Term(Rc Term>); + +impl Term { + fn new(function: impl Fn(Term) -> Term + 'static) -> Self { + Self(Rc::new(function)) + } + + fn apply(&self, argument: Term) -> Term { + (self.0)(argument) + } + + fn is_same_term_as(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.0, &other.0) + } +} + +// TRUE = λx.λy.x +fn church_true() -> Term { + Term::new(|x| Term::new(move |_y| x.clone())) +} + +// FALSE = λx.λy.y +fn church_false() -> Term { + Term::new(|_x| Term::new(|y| y)) +} + +// NOT = λb.b FALSE TRUE +fn church_not() -> Term { + Term::new(|boolean| boolean.apply(church_false()).apply(church_true())) +} + +// AND = λp.λq.p q FALSE +fn church_and() -> Term { + Term::new(|left| Term::new(move |right| left.apply(right).apply(church_false()))) +} + +// OR = λp.λq.p TRUE q +fn church_or() -> Term { + Term::new(|left| Term::new(move |right| left.apply(church_true()).apply(right))) +} + +// ZERO = λf.λx.x +fn zero() -> Term { + Term::new(|_function| Term::new(|value| value)) +} + +// SUCC = λn.λf.λx.f (n f x) +fn successor() -> Term { + Term::new(|numeral| { + Term::new(move |function| { + let numeral = numeral.clone(); + + Term::new(move |value| { + let previous = numeral.apply(function.clone()).apply(value); + function.apply(previous) + }) + }) + }) +} + +// PRED = λn.λf.λx. +// n (λg.λh.h (g f)) (λu.x) (λu.u) +fn predecessor() -> Term { + Term::new(|numeral| { + Term::new(move |function| { + let numeral = numeral.clone(); + + Term::new(move |value| { + let function_for_step = function.clone(); + let step = Term::new(move |g| { + let function = function_for_step.clone(); + Term::new(move |h| h.apply(g.apply(function.clone()))) + }); + + let constant = { + let value = value.clone(); + Term::new(move |_unused| value.clone()) + }; + let identity = Term::new(|term| term); + + numeral.apply(step).apply(constant).apply(identity) + }) + }) + }) +} + +fn one() -> Term { + successor().apply(zero()) +} + +// The observers below exist only to turn encoded values into printable Rust +// values. None of the encodings or operations use Rust bools or integers. +fn church_to_bool(boolean: Term) -> bool { + let when_true = Term::new(|term| term); + let when_false = Term::new(|term| term); + let selected = boolean.apply(when_true.clone()).apply(when_false.clone()); + + if selected.is_same_term_as(&when_true) { + true + } else if selected.is_same_term_as(&when_false) { + false + } else { + panic!("a Church boolean did not select either argument") + } +} + +fn church_to_usize(numeral: Term) -> usize { + let applications = Rc::new(Cell::new(0)); + let count = applications.clone(); + let increment = Term::new(move |term| { + count.set(count.get() + 1); + term + }); + let seed = Term::new(|term| term); + + numeral.apply(increment).apply(seed); + applications.get() +} + +fn check_bool(label: &str, boolean: Term, expected: bool) { + let actual = church_to_bool(boolean); + assert_eq!(actual, expected, "{label}"); + println!("{label}: {actual}"); +} + +fn check_numeral(label: &str, numeral: Term, expected: usize) { + let actual = church_to_usize(numeral); + assert_eq!(actual, expected, "{label}"); + println!("{label}: {actual}"); +} + +fn main() { + println!("CHURCH BOOLEANS"); + check_bool("TRUE", church_true(), true); + check_bool("FALSE", church_false(), false); + check_bool("NOT TRUE", church_not().apply(church_true()), false); + check_bool("NOT FALSE", church_not().apply(church_false()), true); + check_bool( + "FALSE AND FALSE", + church_and().apply(church_false()).apply(church_false()), + false, + ); + check_bool( + "FALSE AND TRUE", + church_and().apply(church_false()).apply(church_true()), + false, + ); + check_bool( + "TRUE AND FALSE", + church_and().apply(church_true()).apply(church_false()), + false, + ); + check_bool( + "TRUE AND TRUE", + church_and().apply(church_true()).apply(church_true()), + true, + ); + check_bool( + "FALSE OR FALSE", + church_or().apply(church_false()).apply(church_false()), + false, + ); + check_bool( + "FALSE OR TRUE", + church_or().apply(church_false()).apply(church_true()), + true, + ); + check_bool( + "TRUE OR FALSE", + church_or().apply(church_true()).apply(church_false()), + true, + ); + check_bool( + "TRUE OR TRUE", + church_or().apply(church_true()).apply(church_true()), + true, + ); + + println!("CHURCH NUMERALS"); + check_numeral("ZERO", zero(), 0); + check_numeral("ONE", one(), 1); + check_numeral("SUCC ONE", successor().apply(one()), 2); + check_numeral("PRED ZERO", predecessor().apply(zero()), 0); + check_numeral("PRED ONE", predecessor().apply(one()), 0); + check_numeral( + "PRED (SUCC ONE)", + predecessor().apply(successor().apply(one())), + 1, + ); +} diff --git a/languages/r/rust/test.sh b/languages/r/rust/test.sh new file mode 100644 index 0000000..0095ed1 --- /dev/null +++ b/languages/r/rust/test.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -eu + +command -v rustc >/dev/null 2>&1 || exit 42 + +directory=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +build_directory=$(mktemp -d) + +cleanup() { + [ ! -e "$build_directory/lambda-core" ] || rm -f "$build_directory/lambda-core" + [ ! -d "$build_directory" ] || rmdir "$build_directory" +} +trap cleanup EXIT HUP INT TERM + +rustc --edition=2021 -D warnings \ + "$directory/lambda-core.rs" \ + -o "$build_directory/lambda-core" +"$build_directory/lambda-core"