Changes
6 changed files (+462/-0)
-
.gitignore (new)
-
@@ -0,0 +1,1 @@/target
-
-
Cargo.lock (new)
-
@@ -0,0 +1,158 @@# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "bitflags" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "getrandom" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ "cfg-if", "libc", "r-efi", "wasi", ] [[package]] name = "lambda" version = "0.1.0" dependencies = [ "rand", ] [[package]] name = "libc" version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro2" version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" [[package]] name = "rand" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ "rand_chacha", "rand_core", "zerocopy", ] [[package]] name = "rand_chacha" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom", ] [[package]] name = "syn" version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] [[package]] name = "wit-bindgen-rt" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags", ] [[package]] name = "zerocopy" version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" dependencies = [ "proc-macro2", "quote", "syn", ]
-
-
Cargo.toml (new)
-
@@ -0,0 +1,7 @@[package] name = "lambda" version = "0.1.0" edition = "2024" [dependencies] rand = "0.9.0"
-
-
src/main.hs (new)
-
@@ -0,0 +1,14 @@data Term = L Int Term | A Term Term | I Int deriving (Eq, Show) sub (L var body) bvar rep = L var (sub body bvar rep) sub (A fn arg) bvar rep = A (sub fn bvar rep) (sub arg bvar rep) sub x bvar rep = if x == I bvar then rep else x red (L var body) = L var (red body) red (A fn arg) = case rfn of (L var body) -> red (sub body var arg) _ -> A rfn (red arg) where rfn = red fn red x = x
-
-
src/main.py (new)
-
@@ -0,0 +1,221 @@# Don't use any features above Python 3.9! # Since that's what Ren'Py uses # So no pattern matching sadly # Format with ruff format lambcalc.py # Must specify file or it'll mess with the vendored deps too import sys from random import randrange from typing import Any, NamedTuple ## Lambda term class L(NamedTuple): var: int body: Any # Application term class A(NamedTuple): fn: Any arg: Any # Recursively substitute var with rep in term def sub(term, var, rep): if type(term) is L: # assert term.var != var if term.var == var: # Shadowing return term return L(term.var, sub(term.body, var, rep)) if type(term) is A: return A(sub(term.fn, var, rep), sub(term.arg, var, rep)) if term == var: return rep return term # Add offset to all bound vars def alpha(term, offset, bound): if type(term) is L: return L(term.var + offset, alpha(term.body, offset, bound | {term.var})) if type(term) is A: return A(alpha(term.fn, offset, bound), alpha(term.arg, offset, bound)) # if term in bound: return term + offset return term # Simplify term using beta reduction def red(term): if type(term) is L: return L(term.var, red(term.body)) if type(term) is A: fn = red(term.fn) if type(fn) is L: # Rename bound vars in fn.body to avoid name collisions return red(sub(fn.body, fn.var, alpha(term.arg, randrange(10**8), set()))) return A(fn, red(term.arg)) return term # Simplify term using beta reduction without var renaming def red_dumb(term): if type(term) is L: return L(term.var, red_dumb(term.body)) if type(term) is A: fn = red_dumb(term.fn) if type(fn) is L: return red_dumb(sub(fn.body, fn.var, term.arg)) return A(fn, red_dumb(term.arg)) return term # Rename vars using first available int # Input term must be reduced first def canonicalize(term, bound, free): if type(term) is L: # Shadowing should never happen assert not term.var in bound if len(bound) == 0 and len(free) == 0: var2 = 1 else: var2 = max((bound | free).values()) + 1 return L(var2, canonicalize(term.body, bound | {term.var: var2}, free)) if type(term) is A: return A( canonicalize(term.fn, bound, free), canonicalize(term.arg, bound, free) ) if term in bound: return bound[term] if len(bound) == 0 and len(free) == 0: free[term] = 1 elif not term in free: free[term] = max((bound | free).values()) + 1 return free[term] # Get size of term def size(term): if type(term) is L: return 1 + size(term.body) if type(term) is A: return size(term.fn) + size(term.arg) return 1 # Check equality def eq(term1, term2): return canonicalize(term1, {}, {}) == canonicalize(term2, {}, {}) animals = " 🦊🐱🐸🐷🐼🐶🐭🐻🐨🐯🐺🦁🐮🐹🐰🐵🦝🤔😱" # Get friendly uncurried repr of canon term def animal_repr_canon(term): if type(term) is str: return term if type(term) is L: if type(term.body) is L: return f"{animal_repr_canon(term.var)}{animal_repr_canon(term.body)}" return f"{animal_repr_canon(term.var)}->{animal_repr_canon(term.body)}|" if type(term) is A: return f"{animal_repr_canon(term.fn)}({animal_repr_canon(term.arg)})" return animals[term] # Wrapper func def animal_repr(term): return animal_repr_canon(canonicalize(term, {}, {})) # https://en.wikipedia.org/wiki/SKI_combinator_calculus I = L(1, 1) K = L(1, L(2, 1)) S = L(1, L(2, L(3, A(A(1, 3), A(2, 3))))) # https://en.wikipedia.org/wiki/Fixed-point_combinator # red(Y) doesn't terminate though... Y = L(1, A(L(2, A(1, A(2, 2))), L(2, A(1, A(2, 2))))) Z = L(1, A(L(2, A(1, L(3, A(A(2, 2), 3)))), L(2, A(1, L(3, A(A(2, 2), 3)))))) # From https://lambster.dev/ # Same as K ltrue = L(1, L(2, 1)) lfalse = L(1, L(2, 2)) land = L(1, L(2, A(A(1, 2), 1))) lor = L(1, L(2, A(A(1, 1), 2))) lnot = L(1, A(A(1, lfalse), ltrue)) lif = L(1, L(2, L(3, A(A(1, 2), 3)))) # Can also use pair/first/second for lists lpair = L(1, L(2, L(3, A(A(3, 1), 2)))) lfirst = L(1, A(1, ltrue)) lsecond = L(1, A(1, lfalse)) lnil = L(1, ltrue) lnull = L(1, A(1, L(2, L(3, lfalse)))) ltree = L(1, L(2, L(3, A(A(1, 2), 3)))) ldatum = L(1, A(1, lfirst)) lleft = L(1, A(A(1, lsecond), lfirst)) lright = L(1, A(1, lsecond)) lincr = L(1, L(2, L(3, A(2, A(A(1, 2), 3))))) lplus = L(1, L(2, A(1, lincr))) ltimes = L(1, L(2, A(1, A(lplus, 2)))) liszero = L(1, A(A(1, L(2, lfalse)), ltrue)) # Church numerals lzero = lfalse lone = L(1, L(2, A(1, 2))) ltwo = L(1, L(2, A(1, A(1, 2)))) lthree = L(1, L(2, A(1, A(1, A(1, 2))))) lfour = L(1, L(2, A(1, A(1, A(1, A(1, 2)))))) lfive = L(1, L(2, A(1, A(1, A(1, A(1, A(1, 2))))))) lsix = L(1, L(2, A(1, A(1, A(1, A(1, A(1, A(1, 2)))))))) lseven = L(1, L(2, A(1, A(1, A(1, A(1, A(1, A(1, A(1, 2))))))))) leight = L(1, L(2, A(1, A(1, A(1, A(1, A(1, A(1, A(1, A(1, 2)))))))))) lnine = L(1, L(2, A(1, A(1, A(1, A(1, A(1, A(1, A(1, A(1, A(1, 2))))))))))) # Tests assert eq(red(A(A(A(S, K), I), A(A(K, I), S))), I) assert eq(red(A(A(A(S, K), S), K)), K) assert eq(red(A(A(A(S, K), I), K)), K) assert eq(red(A(A(K, S), A(I, A(A(A(S, K), S), I)))), S) # eta reduction assert eq(red(L(1, A(L(2, A(2, 2)), 1))), L(1, A(1, 1))) # Rename bound variable in lambda after name collision assert eq(red(A(K, K)), L(1, L(2, L(3, 2)))) # Avoid name capture of bound variables # https://www.cs.yale.edu/homes/hudak/CS201S08/lambda.pdf assert eq(red(A(L(1, L(2, 1)), 2)), L(2, 3)) # Should two free vars refer to the same var?? # Like maybe we should just assume all vars are bound above us somewhere # I think that massively simplifies the code, since then red doesn't need to track currently bound vars # I should choose the one that makes for better puzzles assert eq(red(A(L(1, A(2, 1)), 2)), A(2, 2)) # Brute force!! def solve(terms, moves, depth): cnt = sum(type(term) is int for term in terms) if cnt == len(terms): print(moves) return if len(terms) - cnt > depth: # Can only increase cnt by one per turn anyways return for i in range(len(terms)): for j in range(len(terms)): newterms = terms.copy() try: newterms[j] = red_dumb(A(terms[i], terms[j])) # if size(newterms[j]) > 50: # Heuristic: stop if term is blowing up # continue except: continue solve(newterms, moves + [i, j], depth - 1) if __name__ == "__main__": sys.setrecursionlimit(50) # solve([A(1, 3), L(5, L(2, 5)), L(1, L(2, 2))], [], 9)
-
-
src/main.rs (new)
-
@@ -0,0 +1,61 @@use std::collections::HashSet; #[derive(PartialEq, Clone, Debug)] enum E { L(i64, Box<E>), A(Box<E>, Box<E>), I(i64), } fn sub(term: E, var: i64, rep: &E, offset: i64, bound: &mut HashSet<i64>) -> E { match term { E::L(var, body) => { bound.insert(var); let x = E::L(var + offset, Box::new(sub(*body, var, rep, offset, bound))); bound.remove(&var); x } E::A(func, arg) => E::A( Box::new(sub(*func, var, rep, offset, bound)), Box::new(sub(*arg, var, rep, offset, bound)), ), x => { if x == E::I(var) { rep.clone() } else { x } } } } fn red(term: E) -> E { match term { E::L(var, body) => E::L(var, Box::new(red(*body))), E::A(func, arg) => { let rfunc = red(*func); match rfunc { E::L(var, body) => red(sub( *body, var, &arg, rand::random_range(0..1 << 32), &mut HashSet::<i64>::new(), )), x => x, } } x => x, } } fn main() { // Prints nothing? println!( "{:?}", red(E::A( Box::new(E::L(1, Box::new(E::L(2, Box::new(E::I(1)))))), Box::new(E::I(2)) )) ); }
-