Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
210 changes: 210 additions & 0 deletions languages/c/csharp/LambdaCore.cs
Original file line number Diff line number Diff line change
@@ -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<Term, Term> function;

public Term(Func<Term, Term> function)
{
this.function = function;
}

public Term Apply(Term argument)
{
return function(argument);
}
}

private static Term Lambda(Func<Term, Term> 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);
}
}
9 changes: 9 additions & 0 deletions languages/c/csharp/LambdaCore.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
57 changes: 57 additions & 0 deletions languages/c/csharp/README.md
Original file line number Diff line number Diff line change
@@ -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<Term, Term>`:

```csharp
private sealed class Term
{
private readonly Func<Term, Term> 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.
22 changes: 22 additions & 0 deletions languages/c/csharp/expected-output.txt
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions languages/c/csharp/test.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading