Semantic Parsing and Meaning Representations › Code Implementation

Michael BrenndoerferMay 13, 202513 min read
Language AI HandbookAll chapters
Part 8 of 67Semantics and Information Extraction
Chapter 55Semantic Parsing and Meaning Representations › Code Implementation
Reading section 9 of 13 · Code Implementation · 13 minSection 9 of 13 · 13 min

Section 9 of 13 · 13 min read

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

Notes & comments

We will build three small pieces:

  1. A small lambda-term evaluator to show compositional semantics.
  2. An AMR graph representation and a readable edge summary.
  3. 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]
CollapseExpand
import re
import sqlite3
from dataclasses import dataclass
from typing import Any

import matplotlib.pyplot as plt
import numpy as np
PythonIn [12]
CollapseExpand
@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: Any

The 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]
CollapseExpand
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]
CollapseExpand
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]
CollapseExpand
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]
CollapseExpand
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]
CollapseExpand
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]
CollapseExpand
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]
CollapseExpand
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_reads
ConsoleOut [20]
CollapseExpand
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]
CollapseExpand
@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]
CollapseExpand
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]
CollapseExpand
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]
CollapseExpand
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]
CollapseExpand
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 xx and apply the object quantifier to λy. read(x,y)\lambda y.\,\text{read}(x,y). Then abstract over xx. 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]
CollapseExpand
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]
CollapseExpand
Tree grouping the subject quantifier, verb phrase, verb, and object quantifier.
Components of the surface-scope reading of 'Every student reads a book'. The upper combination applies the subject quantifier to the verb-phrase predicate; the lower grouping requires the abstraction explained in the text.

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]
CollapseExpand
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]
CollapseExpand
Heatmap of allowed SQL tokens at each step of a fixed toy grammar.
A toy grammar mask for SQL decoding. Each row is a decoding step; each column is a vocabulary token. Colored cells mark tokens allowed by the grammar at that step.

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!

Newsletter

Stay up to date

Get new articles, book updates, and notes on AI and technology.

Unsubscribe at any time. Privacy policy