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
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CLAUDE.md
AGENTS.md
.claude/
.agentnotes/
.cursor/
.mcp.json
tasks/lessons.md
tasks/todo.md
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# lambda.

A from-scratch semantic parser for natural language. It takes an English sentence, builds a Combinatory Categorial Grammar (CCG) derivation, and outputs a first-order logic formula using standard quantifier notation (∀, ∃, ∧, , ¬).
A from-scratch semantic parser for natural language. It takes an English sentence, builds a Combinatory Categorial Grammar (CCG) derivation, and outputs a first-order logic formula using standard quantifier notation (∀, ∃, ∧, ->, ¬).

## What is CCG?

Combinatory Categorial Grammar is a grammar formalism where every word is assigned a syntactic **category** that encodes what it needs from its neighbours to form a larger constituent. A transitive verb like *likes* carries the category `(S\NP)/NP`, meaning: "give me an NP to my right (the object), then an NP to my left (the subject), and I'll produce a sentence S." Categories combine via a small set of rulesforward/backward application and compositionwithout any separate phrase-structure rules. The grammar is entirely lexical.
Combinatory Categorial Grammar is a grammar formalism where every word is assigned a syntactic **category** that encodes what it needs from its neighbours to form a larger constituent. A transitive verb like *likes* carries the category `(S\NP)/NP`, meaning: "give me an NP to my right (the object), then an NP to my left (the subject), and I'll produce a sentence S." Categories combine via a small set of rules, forward/backward application and composition, without any separate phrase-structure rules. The grammar is entirely lexical.

Each lexical entry also carries a **lambda expression** that captures its meaning. When two categories combine, their lambda expressions compose via beta-reduction. Chaining these reductions bottom-up over the parse chart produces the logical form for the whole sentence.

Expand All @@ -22,7 +22,7 @@ Each lexical entry also carries a **lambda expression** that captures its meanin

## Install & run

No external NLP libraries are required only Python 3.11+.
No external NLP libraries are required, only Python 3.11+.

```bash
# Clone / copy the project, then:
Expand Down
2 changes: 1 addition & 1 deletion src/category.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ extern const Category CAT_PP;
extern const Category CAT_CONJ;
extern const Category CAT_Q; // question sentence

// Derived common categories built on first use
// Derived common categories, built on first use
Category cat_SbNP_fNP(); // (S\NP)/NP transitive verb
Category cat_SbNP(); // S\NP intransitive verb / VP
Category cat_NPfN(); // NP/N determiner
Expand Down
8 changes: 4 additions & 4 deletions src/ccg_rules.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ static Term maybe_reduce(Term t) {
}

// ---------------------------------------------------------------------------
// Forward Application: (A/B) B A sem: f(x)
// Forward Application: (A/B) B -> A sem: f(x)
// ---------------------------------------------------------------------------
OptResult forward_apply(const Category& lc, const Term& ls,
const Category& rc, const Term& rs)
Expand All @@ -24,7 +24,7 @@ OptResult forward_apply(const Category& lc, const Term& ls,
}

// ---------------------------------------------------------------------------
// Backward Application: B (A\B) A sem: f(x)
// Backward Application: B (A\B) -> A sem: f(x)
// ---------------------------------------------------------------------------
OptResult backward_apply(const Category& lc, const Term& ls,
const Category& rc, const Term& rs)
Expand All @@ -35,7 +35,7 @@ OptResult backward_apply(const Category& lc, const Term& ls,
}

// ---------------------------------------------------------------------------
// Forward Composition: (A/B)(B/C) A/C sem: λx. f(g(x))
// Forward Composition: (A/B)(B/C) -> A/C sem: λx. f(g(x))
// ---------------------------------------------------------------------------
OptResult forward_compose(const Category& lc, const Term& ls,
const Category& rc, const Term& rs)
Expand All @@ -49,7 +49,7 @@ OptResult forward_compose(const Category& lc, const Term& ls,
}

// ---------------------------------------------------------------------------
// Backward Composition: (B\C)(A\B) A\C sem: λx. g(f(x))
// Backward Composition: (B\C)(A\B) -> A\C sem: λx. g(f(x))
// ---------------------------------------------------------------------------
OptResult backward_compose(const Category& lc, const Term& ls,
const Category& rc, const Term& rs)
Expand Down
2 changes: 1 addition & 1 deletion src/lambda_calc.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Term beta_reduce(const Term& t, int max_steps = 500);
std::vector<Term> beta_reduce_trace(const Term& t, int max_steps = 500);

// ---------------------------------------------------------------------------
// Canonical string used for structural deduplication (bound vars renamed
// Canonical string, used for structural deduplication (bound vars renamed
// depth-first to positional names _0, _1, ...)
// ---------------------------------------------------------------------------
std::string canonical_str(const Term& t);
42 changes: 21 additions & 21 deletions src/lexicon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ static Term t_noun(const std::string& pred) {
return make_lam("x", make_app(make_const(pred), make_var("x")));
}

// NP/N (every / each / all / any / both / most): λP. λQ. ∀x. P(x) Q(x)
// NP/N (every / each / all / any / both / most): λP. λQ. ∀x. P(x) -> Q(x)
static Term t_det_every() {
return make_lam("P", make_lam("Q",
make_forall("x",
Expand Down Expand Up @@ -172,12 +172,12 @@ static Term t_cop_eq() {
make_app(make_var("S"), make_lam("x", app_O))));
}

// (S\NP)/(S\NP): identity copula for predicative adjectives "John is brave"
// (S\NP)/(S\NP): identity, copula for predicative adjectives "John is brave"
static Term t_cop_id() {
return make_lam("V", make_lam("S", make_app(make_var("V"), make_var("S"))));
}

// Coordination AND
// Coordination, AND
// (NP\NP)/NP: λB. λA. λP. A(P) ∧ B(P)
static Term t_conj_and_gq() {
auto AP = make_app(make_var("A"), make_var("P"));
Expand All @@ -201,7 +201,7 @@ static Term t_conj_and_s() {
return make_lam("B", make_lam("A", make_and(make_var("A"), make_var("B"))));
}

// Coordination OR
// Coordination, OR
static Term t_conj_or_gq() {
auto AP = make_app(make_var("A"), make_var("P"));
auto BP = make_app(make_var("B"), make_var("P"));
Expand Down Expand Up @@ -301,7 +301,7 @@ static Table build_table() {
Table t;

// -----------------------------------------------------------------------
// Nouns social roles & professions
// Nouns, social roles & professions
// -----------------------------------------------------------------------
add_noun(t, "student", "students", "student");
add_noun(t, "teacher", "teachers", "teacher");
Expand Down Expand Up @@ -384,7 +384,7 @@ static Table build_table() {
add_noun(t, "citizen", "citizens", "citizen");

// -----------------------------------------------------------------------
// Nouns animals
// Nouns, animals
// -----------------------------------------------------------------------
add_noun(t, "cat", "cats", "cat");
add_noun(t, "dog", "dogs", "dog");
Expand Down Expand Up @@ -422,7 +422,7 @@ static Table build_table() {
add_noun(t, "dragon", "dragons", "dragon");

// -----------------------------------------------------------------------
// Nouns objects & places
// Nouns, objects & places
// -----------------------------------------------------------------------
add_noun(t, "book", "books", "book");
add_noun(t, "table", "tables", "table");
Expand Down Expand Up @@ -508,7 +508,7 @@ static Table build_table() {
add_noun(t, "street", "streets", "street");

// -----------------------------------------------------------------------
// Nouns abstract concepts
// Nouns, abstract concepts
// -----------------------------------------------------------------------
add_noun(t, "idea", "ideas", "idea");
add_noun(t, "thought", "thoughts", "thought");
Expand Down Expand Up @@ -576,7 +576,7 @@ static Table build_table() {
add_noun(t, "beginning", "beginnings", "beginning");

// -----------------------------------------------------------------------
// Nouns natural / body
// Nouns, natural / body
// -----------------------------------------------------------------------
add_noun(t, "fire", "fires", "fire");
add_noun(t, "wind", "winds", "wind");
Expand Down Expand Up @@ -608,7 +608,7 @@ static Table build_table() {
add_noun(t, "bone", "bones", "bone");

// -----------------------------------------------------------------------
// Nouns food & drink
// Nouns, food & drink
// -----------------------------------------------------------------------
add_noun(t, "apple", "apples", "apple");
add_noun(t, "bread", "breads", "bread");
Expand Down Expand Up @@ -826,7 +826,7 @@ static Table build_table() {
// (the add_tv in TV section would overwrite, so we do it last)


// Past tense intransitive
// Past tense, intransitive
add_iv_past(t, "ran", "run");
add_iv_past(t, "fell", "fall");
add_iv_past(t, "rose", "rise");
Expand Down Expand Up @@ -1019,7 +1019,7 @@ static Table build_table() {
add_tv(t, "watches", "watch", "watch");
add_tv(t, "learns", "learn", "learn");

// Past tense transitive (irregular)
// Past tense, transitive (irregular)
add_tv_past(t, "liked", "like");
add_tv_past(t, "saw", "see");
add_tv_past(t, "loved", "love");
Expand Down Expand Up @@ -1080,7 +1080,7 @@ static Table build_table() {
add_dtv(t, "promises", "promise", "promise");
add_dtv(t, "denies", "deny", "deny");

// Past tense ditransitive
// Past tense, ditransitive
add_dtv_past(t, "gave", "give");
add_dtv_past(t, "showed", "show");
add_dtv_past(t, "lent", "lend");
Expand Down Expand Up @@ -1147,7 +1147,7 @@ static Table build_table() {
}

// -----------------------------------------------------------------------
// Prepositions VP adjunct + N post-modifier
// Prepositions, VP adjunct + N post-modifier
// -----------------------------------------------------------------------
for (auto& [w, pred] : std::initializer_list<std::pair<const char*,const char*>>{
{"in", "in"}, {"on", "on"}, {"at", "at"},
Expand Down Expand Up @@ -1217,30 +1217,30 @@ static std::vector<LexEntry> morph_fallback(const std::string& key) {

// -ed past tense / past participle
if (key.size() > 3 && key.substr(key.size()-2) == "ed") {
// loved love, chased chase
// loved -> love, chased -> chase
auto s1 = try_stem(key.substr(0, key.size()-1));
if (!s1.empty()) return s1;
// walked walk, talked talk
// walked -> walk, talked -> talk
auto s2 = try_stem(key.substr(0, key.size()-2));
if (!s2.empty()) return s2;
// tried try (ied y)
// tried -> try (ied -> y)
if (key.size() > 4 && key.substr(key.size()-3) == "ied") {
auto s3 = try_stem(key.substr(0, key.size()-3) + "y");
if (!s3.empty()) return s3;
}
}
// -ing
if (key.size() > 4 && key.substr(key.size()-3) == "ing") {
// running run (consonant doubling already stripped by removing 1 char)
// running -> run (consonant doubling already stripped by removing 1 char)
auto s1 = try_stem(key.substr(0, key.size()-3));
if (!s1.empty()) return s1;
// liking like (silent e dropped)
// liking -> like (silent e dropped)
auto s2 = try_stem(key.substr(0, key.size()-3) + "e");
if (!s2.empty()) return s2;
}
// -s/-es plurals / 3sg
if (key.size() > 3 && key.substr(key.size()-2) == "es") {
// boxes box, chases chase
// boxes -> box, chases -> chase
auto s1 = try_stem(key.substr(0, key.size()-2));
if (!s1.empty()) return s1;
auto s2 = try_stem(key.substr(0, key.size()-1));
Expand All @@ -1250,7 +1250,7 @@ static std::vector<LexEntry> morph_fallback(const std::string& key) {
auto s1 = try_stem(key.substr(0, key.size()-1));
if (!s1.empty()) return s1;
}
// -ies -y (flies fly)
// -ies -> -y (flies -> fly)
if (key.size() > 3 && key.substr(key.size()-3) == "ies") {
auto s1 = try_stem(key.substr(0, key.size()-3) + "y");
if (!s1.empty()) return s1;
Expand Down
2 changes: 1 addition & 1 deletion src/parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ dedup_by_structure(const std::vector<ParseResult>& entries) {
}

// ---------------------------------------------------------------------------
// Apply type-raising to a list of entries (only NP lifted NP)
// Apply type-raising to a list of entries (only NP -> lifted NP)
// ---------------------------------------------------------------------------
static std::vector<ParseResult>
apply_type_raising(const std::vector<ParseResult>& entries) {
Expand Down
4 changes: 2 additions & 2 deletions src/parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ struct ParseError : std::runtime_error {
};

// ---------------------------------------------------------------------------
// Parse returns all S/Q-spanning results, deduplicated by structure
// Parse, returns all S/Q-spanning results, deduplicated by structure
// ---------------------------------------------------------------------------
std::vector<ParseResult> parse(const std::string& sentence);

// ---------------------------------------------------------------------------
// Parse with step tracing returns raw term + reduction steps per parse
// Parse with step tracing, returns raw term + reduction steps per parse
// ---------------------------------------------------------------------------
struct StepResult { Term raw; std::vector<Term> steps; };
std::vector<StepResult> parse_steps(const std::string& sentence);
4 changes: 2 additions & 2 deletions src/pretty_print.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ static bool is_atom_term(const Term& t) {
std::holds_alternative<Const>(t->data);
}

// Uncurry: App(App(f, a), b) (f, [a, b])
// Uncurry: App(App(f, a), b) -> (f, [a, b])
static std::pair<Term, std::vector<Term>> uncurry(const Term& t) {
std::vector<Term> args;
Term cur = t;
Expand All @@ -114,7 +114,7 @@ static std::pair<Term, std::vector<Term>> uncurry(const Term& t) {
return { cur, args };
}

// Strip nested Lam: λx. λy. body ("x y", body)
// Strip nested Lam: λx. λy. body -> ("x y", body)
static std::pair<std::string, Term> strip_lam(const Term& t) {
std::string vars;
Term cur = t;
Expand Down
4 changes: 2 additions & 2 deletions src/term.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#include <variant>

// ---------------------------------------------------------------------------
// Forward declaration allows Term to be used inside node structs
// Forward declaration, allows Term to be used inside node structs
// ---------------------------------------------------------------------------
struct TermNode;
using Term = std::shared_ptr<TermNode>;
Expand All @@ -22,7 +22,7 @@ struct And { Term left; Term right; };
struct Or { Term left; Term right; };
struct Implies { Term left; Term right; };
struct Not { Term body; };
struct Iota { std::string var; Term body; }; // ιx. P(x) definite description
struct Iota { std::string var; Term body; }; // ιx. P(x), definite description

using TermVariant = std::variant<Var, Const, App, Lam, Forall, Exists,
And, Or, Implies, Not, Iota>;
Expand Down
16 changes: 8 additions & 8 deletions tests/test_lambda.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ inline void test_lambda(TestRunner& R) {
// ------------------------------------------------------------------
CHECK(R, free_vars(make_var("x")) == std::set<std::string>{"x"});
CHECK(R, free_vars(make_const("john")).empty());
// λx. x x is bound
// λx. x, x is bound
CHECK(R, free_vars(make_lam("x", make_var("x"))).empty());
// λx. y y is free
// λx. y, y is free
CHECK(R, free_vars(make_lam("x", make_var("y"))) == std::set<std::string>{"y"});
// f(x) both free
// f(x), both free
{ auto fv = free_vars(make_app(make_var("f"), make_var("x")));
CHECK(R, fv.count("f") && fv.count("x")); }
// ∀x. P(x) P is free, x is bound
// ∀x. P(x), P is free, x is bound
{ auto fv = free_vars(make_forall("x", make_app(make_var("P"), make_var("x"))));
CHECK(R, fv.count("P") && !fv.count("x")); }

Expand All @@ -43,16 +43,16 @@ inline void test_lambda(TestRunner& R) {
// ------------------------------------------------------------------
R.suite("beta_reduce");
// ------------------------------------------------------------------
// (λx. x)(z) z
// (λx. x)(z) -> z
{ auto t = make_app(make_lam("x", make_var("x")), make_var("z"));
CHECK(R, term_equal(beta_reduce(t), make_var("z"))); }
// (λx. likes(john,x))(mary) likes(john,mary)
// (λx. likes(john,x))(mary) -> likes(john,mary)
{ auto t = make_app(
make_lam("x", make_app(make_app(make_const("likes"), make_const("john")), make_var("x"))),
make_const("mary"));
auto expected = make_app(make_app(make_const("likes"), make_const("john")), make_const("mary"));
CHECK(R, term_equal(beta_reduce(t), expected)); }
// Curried: (λx.λy.likes(x,y))(john)(mary) likes(john,mary)
// Curried: (λx.λy.likes(x,y))(john)(mary) -> likes(john,mary)
{ auto t = make_app(
make_app(
make_lam("x", make_lam("y",
Expand All @@ -64,7 +64,7 @@ inline void test_lambda(TestRunner& R) {
// Already normal form
{ auto t = make_app(make_const("f"), make_const("a"));
CHECK(R, term_equal(beta_reduce(t), t)); }
// Under lambda: λy. (λx.x)(y) λy. y
// Under lambda: λy. (λx.x)(y) -> λy. y
{ auto t = make_lam("y", make_app(make_lam("x", make_var("x")), make_var("y")));
CHECK(R, term_equal(beta_reduce(t), make_lam("y", make_var("y")))); }

Expand Down
2 changes: 1 addition & 1 deletion tests/test_parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ inline void test_parser(TestRunner& R) {
auto pp = pretty(results[0].sem);
CHECK(R, contains(pp, "\u2200")); // ∀
CHECK(R, contains(pp, "student"));
CHECK(R, contains(pp, "\u2192")); //
CHECK(R, contains(pp, "\u2192")); // ->
CHECK(R, contains(pp, "\u2203")); // ∃
CHECK(R, contains(pp, "cat"));
CHECK(R, contains(pp, "\u2227")); // ∧
Expand Down