From 0e4958a7164d6912c9f568e1425c17ba0f583fa4 Mon Sep 17 00:00:00 2001 From: Kyle Serrecchia Date: Mon, 10 Aug 2026 12:41:04 -0700 Subject: [PATCH] Add C# lambda core implementation --- ROADMAP.md | 2 +- languages/c/csharp/LambdaCore.cs | 210 +++++++++++++++++++++++++ languages/c/csharp/LambdaCore.csproj | 9 ++ languages/c/csharp/README.md | 57 +++++++ languages/c/csharp/expected-output.txt | 22 +++ languages/c/csharp/test.sh | 25 +++ 6 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 languages/c/csharp/LambdaCore.cs create mode 100644 languages/c/csharp/LambdaCore.csproj create mode 100644 languages/c/csharp/README.md create mode 100644 languages/c/csharp/expected-output.txt create mode 100644 languages/c/csharp/test.sh diff --git a/ROADMAP.md b/ROADMAP.md index d5715ea..cf0aa6a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -51,7 +51,7 @@ 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 -- [ ] C# +- [x] C# - [ ] Swift - [ ] Scala - [ ] Zig diff --git a/languages/c/csharp/LambdaCore.cs b/languages/c/csharp/LambdaCore.cs new file mode 100644 index 0000000..02311b5 --- /dev/null +++ b/languages/c/csharp/LambdaCore.cs @@ -0,0 +1,210 @@ +using System; + +internal static class LambdaCore +{ + // A lambda-calculus term is a function from one term to another term. + // The wrapper makes that recursive type expressible in C#. + private sealed class Term + { + private readonly Func function; + + public Term(Func function) + { + this.function = function; + } + + public Term Apply(Term argument) + { + return function(argument); + } + } + + private static Term Lambda(Func function) + { + return new Term(function); + } + + // TRUE = λx.λy.x + private static Term ChurchTrue() + { + return Lambda(x => Lambda(_ => x)); + } + + // FALSE = λx.λy.y + private static Term ChurchFalse() + { + return Lambda(_ => Lambda(y => y)); + } + + // NOT = λb.b FALSE TRUE + private static Term ChurchNot() + { + return Lambda(boolean => + boolean.Apply(ChurchFalse()).Apply(ChurchTrue())); + } + + // AND = λp.λq.p q FALSE + private static Term ChurchAnd() + { + return Lambda(left => Lambda(right => + left.Apply(right).Apply(ChurchFalse()))); + } + + // OR = λp.λq.p TRUE q + private static Term ChurchOr() + { + return Lambda(left => Lambda(right => + left.Apply(ChurchTrue()).Apply(right))); + } + + // ZERO = λf.λx.x + private static Term Zero() + { + return Lambda(_ => Lambda(value => value)); + } + + // SUCC = λn.λf.λx.f (n f x) + private static Term Successor() + { + return Lambda(numeral => Lambda(function => Lambda(value => + function.Apply(numeral.Apply(function).Apply(value))))); + } + + // PRED = λn.λf.λx. + // n (λg.λh.h (g f)) (λu.x) (λu.u) + private static Term Predecessor() + { + return Lambda(numeral => Lambda(function => Lambda(value => + numeral + .Apply(Lambda(g => Lambda(h => + h.Apply(g.Apply(function))))) + .Apply(Lambda(_ => value)) + .Apply(Lambda(term => term))))); + } + + private static Term One() + { + return Successor().Apply(Zero()); + } + + // These observers only turn encoded results into printable C# values. + // The encodings and their operations do not use C# bools or integers. + private static bool ChurchToBool(Term boolean) + { + Term whenTrue = Lambda(term => term); + Term whenFalse = Lambda(term => term); + Term selected = boolean.Apply(whenTrue).Apply(whenFalse); + + if (object.ReferenceEquals(selected, whenTrue)) + { + return true; + } + + if (object.ReferenceEquals(selected, whenFalse)) + { + return false; + } + + throw new InvalidOperationException( + "A Church boolean did not select either argument."); + } + + private static int ChurchToInt(Term numeral) + { + int applications = 0; + Term increment = Lambda(term => + { + applications += 1; + return term; + }); + Term seed = Lambda(term => term); + + numeral.Apply(increment).Apply(seed); + return applications; + } + + private static void CheckBoolean(string label, Term boolean, bool expected) + { + bool actual = ChurchToBool(boolean); + if (actual != expected) + { + throw new InvalidOperationException( + $"{label}: expected {expected}, got {actual}."); + } + + Console.WriteLine($"{label}: {actual.ToString().ToLowerInvariant()}"); + } + + private static void CheckNumeral(string label, Term numeral, int expected) + { + int actual = ChurchToInt(numeral); + if (actual != expected) + { + throw new InvalidOperationException( + $"{label}: expected {expected}, got {actual}."); + } + + Console.WriteLine($"{label}: {actual}"); + } + + private static void Main() + { + Console.WriteLine("CHURCH BOOLEANS"); + CheckBoolean("TRUE", ChurchTrue(), true); + CheckBoolean("FALSE", ChurchFalse(), false); + CheckBoolean("NOT TRUE", ChurchNot().Apply(ChurchTrue()), false); + CheckBoolean("NOT FALSE", ChurchNot().Apply(ChurchFalse()), true); + CheckBoolean( + "FALSE AND FALSE", + ChurchAnd().Apply(ChurchFalse()).Apply(ChurchFalse()), + false); + CheckBoolean( + "FALSE AND TRUE", + ChurchAnd().Apply(ChurchFalse()).Apply(ChurchTrue()), + false); + CheckBoolean( + "TRUE AND FALSE", + ChurchAnd().Apply(ChurchTrue()).Apply(ChurchFalse()), + false); + CheckBoolean( + "TRUE AND TRUE", + ChurchAnd().Apply(ChurchTrue()).Apply(ChurchTrue()), + true); + CheckBoolean( + "FALSE OR FALSE", + ChurchOr().Apply(ChurchFalse()).Apply(ChurchFalse()), + false); + CheckBoolean( + "FALSE OR TRUE", + ChurchOr().Apply(ChurchFalse()).Apply(ChurchTrue()), + true); + CheckBoolean( + "TRUE OR FALSE", + ChurchOr().Apply(ChurchTrue()).Apply(ChurchFalse()), + true); + CheckBoolean( + "TRUE OR TRUE", + ChurchOr().Apply(ChurchTrue()).Apply(ChurchTrue()), + true); + + Console.WriteLine("CHURCH NUMERALS"); + CheckNumeral("ZERO", Zero(), 0); + CheckNumeral("ONE", One(), 1); + CheckNumeral("SUCC ONE", Successor().Apply(One()), 2); + CheckNumeral( + "SUCC (SUCC ONE)", + Successor().Apply(Successor().Apply(One())), + 3); + CheckNumeral("PRED ZERO", Predecessor().Apply(Zero()), 0); + CheckNumeral("PRED ONE", Predecessor().Apply(One()), 0); + CheckNumeral( + "PRED (SUCC ONE)", + Predecessor().Apply(Successor().Apply(One())), + 1); + CheckNumeral( + "PRED (SUCC (SUCC ONE))", + Predecessor().Apply( + Successor().Apply(Successor().Apply(One()))), + 2); + } +} diff --git a/languages/c/csharp/LambdaCore.csproj b/languages/c/csharp/LambdaCore.csproj new file mode 100644 index 0000000..0cb396d --- /dev/null +++ b/languages/c/csharp/LambdaCore.csproj @@ -0,0 +1,9 @@ + + + Exe + net8.0 + disable + enable + true + + diff --git a/languages/c/csharp/README.md b/languages/c/csharp/README.md new file mode 100644 index 0000000..bf55010 --- /dev/null +++ b/languages/c/csharp/README.md @@ -0,0 +1,57 @@ +# C# Lambda Core + +This implementation expresses the project's Church booleans and Church +numerals as callable C# values. It includes `TRUE`, `FALSE`, `NOT`, `AND`, +`OR`, `ZERO`, `SUCC`, `PRED`, and `ONE`, then checks and prints examples of +each operation. + +## Requirements + +- The .NET 8 SDK or newer +- No NuGet packages or other external dependencies + +The .NET 8 SDK is included in GitHub's current +[`ubuntu-latest` runner image](https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Readme.md#net-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 builds and runs `LambdaCore.csproj` with warnings treated as +errors. It redirects all compiler output into a temporary directory and +removes that directory afterward, so it leaves no `bin` or `obj` artifacts in +the repository. 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. That definition +is recursive, so the `Term` class wraps a standard C# delegate of type +`Func`: + +```csharp +private sealed class Term +{ + private readonly Func function; +} +``` + +The wrapper only makes the recursive function type possible. It does not +contain a boolean, number, or special case for any operation. + +The core definitions are direct translations of their lambda-calculus forms. +For example, `TRUE` returns its first argument, `FALSE` returns 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 C# values. The boolean +helper gives a term two distinct marker objects and reports which one it +selects. The numeral helper gives a term an incrementing function and counts +how many times it is applied. The encodings and operations themselves never +store or inspect a C# `bool` or integer. diff --git a/languages/c/csharp/expected-output.txt b/languages/c/csharp/expected-output.txt new file mode 100644 index 0000000..b109aba --- /dev/null +++ b/languages/c/csharp/expected-output.txt @@ -0,0 +1,22 @@ +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 +SUCC (SUCC ONE): 3 +PRED ZERO: 0 +PRED ONE: 0 +PRED (SUCC ONE): 1 +PRED (SUCC (SUCC ONE)): 2 diff --git a/languages/c/csharp/test.sh b/languages/c/csharp/test.sh new file mode 100644 index 0000000..4a1a73f --- /dev/null +++ b/languages/c/csharp/test.sh @@ -0,0 +1,25 @@ +#!/bin/sh +set -eu + +command -v dotnet >/dev/null 2>&1 || exit 42 + +directory=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +build_directory=$(mktemp -d) +trap 'rm -rf "$build_directory"' EXIT HUP INT TERM + +export DOTNET_CLI_HOME="$build_directory/dotnet-home" +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +export DOTNET_NOLOGO=1 +export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 +export NUGET_PACKAGES="$build_directory/nuget-packages" + +dotnet build \ + "$directory/LambdaCore.csproj" \ + --configuration Release \ + --nologo \ + --verbosity quiet \ + --property:BaseOutputPath="$build_directory/bin/" \ + --property:BaseIntermediateOutputPath="$build_directory/obj/" \ + 1>&2 + +dotnet "$build_directory/bin/Release/net8.0/LambdaCore.dll"