From 7a25220b6e716b055347bc16c52594567121ff4e Mon Sep 17 00:00:00 2001 From: Oscar Dalton Date: Thu, 10 Sep 2026 17:12:13 -0400 Subject: [PATCH] Editorial pass and pending work --- .gitignore | 8 ++++++++ README.md | 6 +++--- src/category.hpp | 2 +- src/ccg_rules.cpp | 8 ++++---- src/lambda_calc.hpp | 2 +- src/lexicon.cpp | 42 +++++++++++++++++++++--------------------- src/parser.cpp | 2 +- src/parser.hpp | 4 ++-- src/pretty_print.cpp | 4 ++-- src/term.hpp | 4 ++-- tests/test_lambda.hpp | 16 ++++++++-------- tests/test_parser.hpp | 2 +- 12 files changed, 54 insertions(+), 46 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f73fab --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +CLAUDE.md +AGENTS.md +.claude/ +.agentnotes/ +.cursor/ +.mcp.json +tasks/lessons.md +tasks/todo.md diff --git a/README.md b/README.md index c4608d4..1e920da 100644 --- a/README.md +++ b/README.md @@ -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 rules—forward/backward application and composition—without 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. @@ -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: diff --git a/src/category.hpp b/src/category.hpp index f29bb47..b674943 100644 --- a/src/category.hpp +++ b/src/category.hpp @@ -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 diff --git a/src/ccg_rules.cpp b/src/ccg_rules.cpp index 7989328..852ba78 100644 --- a/src/ccg_rules.cpp +++ b/src/ccg_rules.cpp @@ -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) @@ -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) @@ -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) @@ -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) diff --git a/src/lambda_calc.hpp b/src/lambda_calc.hpp index 2c88d1b..b0fed36 100644 --- a/src/lambda_calc.hpp +++ b/src/lambda_calc.hpp @@ -35,7 +35,7 @@ Term beta_reduce(const Term& t, int max_steps = 500); std::vector 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); diff --git a/src/lexicon.cpp b/src/lexicon.cpp index db66986..b2e041c 100644 --- a/src/lexicon.cpp +++ b/src/lexicon.cpp @@ -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", @@ -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")); @@ -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")); @@ -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"); @@ -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"); @@ -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"); @@ -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"); @@ -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"); @@ -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"); @@ -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"); @@ -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"); @@ -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"); @@ -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>{ {"in", "in"}, {"on", "on"}, {"at", "at"}, @@ -1217,13 +1217,13 @@ static std::vector 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; @@ -1231,16 +1231,16 @@ static std::vector morph_fallback(const std::string& key) { } // -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)); @@ -1250,7 +1250,7 @@ static std::vector 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; diff --git a/src/parser.cpp b/src/parser.cpp index 5f37a33..b65280e 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -52,7 +52,7 @@ dedup_by_structure(const std::vector& 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 apply_type_raising(const std::vector& entries) { diff --git a/src/parser.hpp b/src/parser.hpp index 4995d13..fd79689 100644 --- a/src/parser.hpp +++ b/src/parser.hpp @@ -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 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 steps; }; std::vector parse_steps(const std::string& sentence); diff --git a/src/pretty_print.cpp b/src/pretty_print.cpp index c1dc1cd..7a0a9ba 100644 --- a/src/pretty_print.cpp +++ b/src/pretty_print.cpp @@ -102,7 +102,7 @@ static bool is_atom_term(const Term& t) { std::holds_alternative(t->data); } -// Uncurry: App(App(f, a), b) → (f, [a, b]) +// Uncurry: App(App(f, a), b) -> (f, [a, b]) static std::pair> uncurry(const Term& t) { std::vector args; Term cur = t; @@ -114,7 +114,7 @@ static std::pair> 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 strip_lam(const Term& t) { std::string vars; Term cur = t; diff --git a/src/term.hpp b/src/term.hpp index 6b256bd..af078ac 100644 --- a/src/term.hpp +++ b/src/term.hpp @@ -4,7 +4,7 @@ #include // --------------------------------------------------------------------------- -// 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; @@ -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; diff --git a/tests/test_lambda.hpp b/tests/test_lambda.hpp index 8c6f66b..8b78c22 100644 --- a/tests/test_lambda.hpp +++ b/tests/test_lambda.hpp @@ -9,14 +9,14 @@ inline void test_lambda(TestRunner& R) { // ------------------------------------------------------------------ CHECK(R, free_vars(make_var("x")) == std::set{"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{"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")); } @@ -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", @@ -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")))); } diff --git a/tests/test_parser.hpp b/tests/test_parser.hpp index bc2c26d..ac3a2fa 100644 --- a/tests/test_parser.hpp +++ b/tests/test_parser.hpp @@ -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")); // ∧