Semantic Parsing and Meaning Representations › Code Implementation
Language AI HandbookAll chapters Semantic Parsing and Meaning Representations › Code ImplementationSection view · Sections in this chapter9 / 13 ↓
Reading tools Definitions, links & notes
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
We will build three small pieces:
- A small lambda-term evaluator to show compositional semantics.
- An AMR graph representation and a readable edge summary.
- A toy text-to-SQL pipeline with SQLite and a grammar mask for constrained decoding.
The code is deliberately small. The goal is to make the mechanisms visible, not to build a production semantic parser.
Each piece corresponds to one of the representation families discussed above. The examples use the Python standard library, NumPy, Matplotlib, and the shared book plotting helper. They run locally on small explicit data and do not download model checkpoints.
A Small Lambda-Term Evaluator
We start with abstract syntax for variables, constants, application, and lambda abstraction. The intended types were explained above, but this evaluator does not implement a type checker; invalid combinations may therefore fail at runtime.
PythonIn [11]Expand
import re
import sqlite3
from dataclasses import dataclass
from typing import Any
import matplotlib.pyplot as plt
import numpy as npPythonIn [12]Expand
@dataclass(frozen=True)
class Var:
name: str
@dataclass(frozen=True)
class Const:
name: str
@dataclass(frozen=True)
class App:
fn: Any
arg: Any
@dataclass(frozen=True)
class Lam:
var: str
body: AnyThe dataclasses give us a compact abstract syntax tree. A Var is a variable reference, a Const is a named constant, an App is a function application, and a Lam is a lambda abstraction. The Any types are a deliberate simplification: an implementation could add a typed representation or explicit validation, while this toy lets Python's dynamic typing carry the values.
The evaluator turns a term into a Python value. A variable looks itself up in the environment. A constant looks itself up in the constant table. A lambda becomes a Python function. An application evaluates the function and the argument, then applies one to the other.
The default arguments capture the current term's parameter name, body, and lexical environment for the returned closure. On application, the evaluator copies that environment and binds the supplied argument in the copy.
PythonIn [13]Expand
def evaluate(term, env, constants):
if isinstance(term, Var):
return env[term.name]
if isinstance(term, Const):
return constants[term.name]
if isinstance(term, Lam):
def abstraction(arg, _var=term.var, _body=term.body, _env=env):
new_env = dict(_env)
new_env[_var] = arg
return evaluate(_body, new_env, constants)
return abstraction
if isinstance(term, App):
fn = evaluate(term.fn, env, constants)
arg = evaluate(term.arg, env, constants)
return fn(arg)
raise TypeError(f"Unknown term: {term!r}")We also need a way to print terms in a readable form.
PythonIn [14]Expand
def show(term):
if isinstance(term, Var):
return term.name
if isinstance(term, Const):
return term.name
if isinstance(term, Lam):
return f"(λ{term.var}.{show(term.body)})"
if isinstance(term, App):
return f"({show(term.fn)} {show(term.arg)})"
return str(term)Now we define a tiny domain. The domain contains people and books. The predicates are Python functions over that domain. The quantifiers every and a are generalized quantifiers.
The domain is finite, which is what lets us evaluate the quantifiers by looping over all entities. Other systems may use symbolic procedures or restrict evaluation to supported database queries. Such procedures apply only to the theories, representations, and queries they handle; they do not provide a general terminating evaluator for arbitrary infinite models.
PythonIn [15]Expand
domain = ["alice", "bob", "carol", "b1", "b2"]
students = {"alice", "bob", "carol"}
books = {"b1", "b2"}
reads = {("alice", "b1"), ("bob", "b2"), ("carol", "b1")}
likes = {("alice", "bob"), ("bob", "carol")}
constants = {
"alice": "alice",
"bob": "bob",
"student": lambda x: x in students,
"book": lambda x: x in books,
"read": lambda x: lambda y: (x, y) in reads,
"like": lambda x: lambda y: (x, y) in likes,
"every": lambda p: lambda q: all((not p(x)) or q(x) for x in domain),
"a": lambda p: lambda q: any(p(x) and q(x) for x in domain),
}The implementation uses subject-first currying for like and read: supply the subject, then the object. This differs from the earlier object-first lexical likes example, which modeled surface constituent combination. Here we construct semantic ASTs directly. We can build the term for "Alice likes Bob" by applying the like constant twice.
The inner application applies like to Alice, producing a predicate that expects the object. The outer application applies that predicate to Bob, producing a truth value.
PythonIn [16]Expand
alice_likes_bob = App(App(Const("like"), Const("alice")), Const("bob"))For the quantified sentence, we build the generalized quantifier "every student" and the predicate "reads a book". Then we apply one to the other.
The lambda abstractions are the glue. They turn the open formulas student(x) and book(y) into predicates that the quantifiers can consume.
PythonIn [17]Expand
every_student = App(Const("every"), Lam("x", App(Const("student"), Var("x"))))
a_book = App(Const("a"), Lam("y", App(Const("book"), Var("y"))))
reads_a_book = Lam(
"x",
App(a_book, Lam("y", App(App(Const("read"), Var("x")), Var("y")))),
)
sentence = App(every_student, reads_a_book)ConsoleOut [18]Expand
Logical form: ((every (λx.(student x))) (λx.((a (λy.(book y))) (λy.((read x) y))))) Alice likes Bob: True Every student reads a book: True
The logical form is a nested application, and the evaluator returns True because each student in our tiny domain reads at least one book. Alice likes Bob also returns True because the pair (alice, bob) is in the likes relation.
Notice that the truth value depends on the model, not on the syntax. If we change the reads relation, the same formula can flip from true to false.
We can also see what happens when Bob stops reading. The same formula now evaluates to False.
PythonIn [19]Expand
no_bob_reads = {("alice", "b1"), ("carol", "b1")}
constants_without_bob = dict(constants)
constants_without_bob["read"] = lambda x: lambda y: (x, y) in no_bob_readsConsoleOut [20]Expand
Every student reads a book after removing Bob's reading: False
It returns False: Bob is a student, and after removing his reading there is no book y such that (bob, y) is in the reduced relation. This is the core of compositional semantics. The sentence meaning is a function of the lexical meanings and the way they combine. The quantifier is not a special case hard-coded into the sentence. It is a function that takes predicates as arguments.
An AMR Graph and Edge Summary
Next, we represent a small AMR graph as a set of nodes and labeled edges. This toy treats a string target present in nodes as a node reference and any other string target as a literal display value.
PythonIn [21]Expand
@dataclass
class AMRGraph:
nodes: dict[str, str]
edges: list[tuple[str, str, str]]
amr = AMRGraph(
nodes={"w": "want-01", "b": "boy", "g": "go-02"},
edges=[
("w", "ARG0", "b"),
("w", "ARG1", "g"),
("g", "ARG0", "b"),
],
)
def amr_triples(graph):
return [
(graph.nodes[src], role, graph.nodes.get(tgt, tgt))
for src, role, tgt in graph.edges
]ConsoleOut [22]Expand
AMR triples:
('want-01', 'ARG0', 'boy')
('want-01', 'ARG1', 'go-02')
('go-02', 'ARG0', 'boy')The output contains the triples ('want-01', 'ARG0', 'boy'), ('want-01', 'ARG1', 'go-02'), and ('go-02', 'ARG0', 'boy'). These triples show the predicate-argument structure directly. The edge ("g", "ARG0", "b") is the reentrancy: the goer is the same boy who wants to go. In a larger graph, this structure can be much richer, but the basic representation is the same.
The amr_triples function returns a list of edge summaries using concept labels. It discards node identities, so two different nodes with the same concept label would become indistinguishable. This is a teaching display, not a lossless AMR serialization or a Smatch implementation; Smatch preserves variables until the node-alignment step.
Text-to-SQL Against SQLite
Now we build a tiny database and a toy text-to-SQL converter. The converter recognizes one question pattern and produces a SQL query with a parameter for the threshold.
The database is small enough to inspect by eye, which makes it easy to verify the result of the query.
PythonIn [23]Expand
conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.executescript(
"""
CREATE TABLE departments (
department_id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE students (
student_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
department_id INTEGER REFERENCES departments(department_id)
);
CREATE TABLE enrollments (
student_id INTEGER REFERENCES students(student_id),
course_id INTEGER NOT NULL
);
INSERT INTO departments VALUES
(1, 'Computer Science'),
(2, 'Mathematics'),
(3, 'Physics');
INSERT INTO students VALUES
(1, 'Alice', 1),
(2, 'Bob', 1),
(3, 'Carol', 2),
(4, 'Dave', 1);
INSERT INTO enrollments VALUES
(1, 101),
(2, 101),
(4, 102),
(3, 201);
"""
)The question is fixed for this toy example. We extract the number word, map it to an integer, and fill a SQL template. The threshold is passed as a parameter, not interpolated into the string.
Binding the threshold keeps that value separate from SQL syntax and lets the same query template be reused with different thresholds. This protects bound values in the fixed statement; generated identifiers or SQL structure still require their own validation.
PythonIn [24]Expand
question = "Which departments have more than two enrolled students?"
number_words = {"one": 1, "two": 2, "three": 3, "four": 4}
match = re.search(r"more than (\w+)", question)
threshold = number_words[match.group(1)]
sql = """
SELECT d.name
FROM departments AS d
JOIN students AS s
ON s.department_id = d.department_id
JOIN enrollments AS e
ON e.student_id = s.student_id
GROUP BY d.department_id, d.name
HAVING COUNT(DISTINCT e.student_id) > ?
ORDER BY d.name;
"""
rows = cur.execute(sql, (threshold,)).fetchall()ConsoleOut [25]Expand
Generated SQL:
SELECT d.name
FROM departments AS d
JOIN students AS s
ON s.department_id = d.department_id
JOIN enrollments AS e
ON e.student_id = s.student_id
GROUP BY d.department_id, d.name
HAVING COUNT(DISTINCT e.student_id) > ?
ORDER BY d.name;
Threshold: 2
Result rows: [('Computer Science',)]On this tiny database, only Computer Science has more than two enrolled students. The query is valid SQL and its denotation is a one-row table. An execution-based check judges that result against the intended answer.
The query also illustrates schema linking in a simplified form. The word "departments" maps to the departments table, "students" to the students table, "enrolled" to the enrollments table, and "more than two" to the numeric threshold. A learned parser may predict these mappings; this example supplies them explicitly.
Visualizing Composition
The following tree shows one reading of "Every student reads a book". It is a simplified composition tree, not a full syntactic derivation. The point is to show how the quantifier, verb, and object phrase combine.
The root is a sentence of type t. The subject quantifier consumes the verb-phrase predicate. To construct that predicate, fix a subject and apply the object quantifier to . Then abstract over . The diagram groups the lexical components; its lower edges do not stand for direct function application between the verb and the object quantifier.
PythonIn [26]Expand
semantic_tree = {
"label": "S : t",
"children": [
{"label": "Subject : every(student)"},
{
"label": "VP : property of x\nx reads a book",
"children": [
{"label": "Verb : read"},
{"label": "Object : a(book)"},
],
},
],
}
def leaf_count(node):
if "children" not in node:
return 1
return sum(leaf_count(child) for child in node["children"])
def layout(node, x0, x1, y, positions, edges):
x = (x0 + x1) / 2
positions[id(node)] = (x, y, node["label"])
if "children" in node:
total = sum(leaf_count(child) for child in node["children"])
cursor = x0
for child in node["children"]:
width = (x1 - x0) * leaf_count(child) / total
child_x = cursor + width / 2
edges.append(((x, y), (child_x, y - 1)))
layout(child, cursor, cursor + width, y - 1, positions, edges)
cursor += width
positions, edges = {}, []
layout(semantic_tree, 0, 1, 0, positions, edges)VisualizationOut [27]Expand

The diagram shows the components of the scoped reading. The object quantifier consumes a predicate of possible books for each fixed subject; abstracting over the subject produces the verb phrase. The subject quantifier then consumes that predicate, yielding a truth value.
A full derivation would include more nodes, such as the determiner and noun inside each noun phrase, and would annotate each node with its type. The simplified version is enough to show the shape of the composition.
A Grammar Mask for Constrained Decoding
Finally, we build a tiny binary mask for SQL decoding. Rows are decoding steps. Columns are vocabulary tokens. A cell is 1 if the token is allowed at that step.
The mask is a simplified grammar. It does not allow joins or other query forms, but it does include one fixed filter (WHERE age). This separate syntax illustration uses a toy students/age vocabulary rather than the preceding department database schema. It demonstrates the token-masking mechanism used in a full grammar-constrained decoder.
PythonIn [28]Expand
vocab = [
"SELECT",
"name",
"department",
"FROM",
"students",
"WHERE",
"age",
">",
"2",
";",
"DROP",
]
allowed_sets = [
{"SELECT"},
{"name", "department"},
{"FROM"},
{"students"},
{"WHERE"},
{"age"},
{">"},
{"2"},
{";"},
]
mask = np.array(
[[int(token in allowed) for token in vocab] for allowed in allowed_sets]
)VisualizationOut [29]Expand

At step 1, only SELECT is allowed. At step 2, the grammar allows name or department. The dangerous token DROP is never allowed in this simple grammar. The mask guarantees syntactic validity for this toy language. It does not guarantee that the resulting query is semantically correct for the user's question. Semantic correctness depends on matching the intended question. Schema validation, type checking, and execution feedback can help detect errors, but they do not by themselves guarantee the intended meaning.
The heatmap marks the allowed tokens in each row. In a full decoder, columns correspond to model-vocabulary items and the allowed set depends on the current prefix; its size and density depend on the grammar and prefix.
Comments
Add to the conversation
A question, a correction, or an example from your own work.
No comments yet. Be the first to share your thoughts!