← Computing foundations · Chapter 4

From code to behavior: languages, compilers, and DSLs

Understand parsing, semantic checks, compilation, interpretation, and JIT, then enforce allowed nodes and resource bounds in a small expression language.

After this guide, you can

Explain execution stages and implement a teaching evaluator limited to bounded integers, addition, and multiplication.

Executing code requires more than text that resembles a language. Characters must become a structure, that structure must satisfy language rules, and an execution mechanism must produce behavior. These stages explain the distinction between syntax, type, and runtime errors. They also help decide whether an expression proposed by a model may be executed.

We will define a small language supporting only integers, addition, and multiplication. It expresses a teaching calculation, not arbitrary Python programs, function calls, or file operations. Such a restricted language is a domain-specific language, or DSL.

Syntax, semantics, and runtime

Syntax defines valid structures, including matching parentheses and connecting operands with operators. Semantics defines their meaning: name binding, value calculation, and type constraints. Syntactic validity does not establish business permission or successful execution.

Programming paradigms describe ways to organize computation. Imperative code expresses state and steps; functional approaches emphasize functions and data transformations; object-oriented approaches organize data and behavior through objects. Many languages support several approaches. A paradigm alone cannot rank speed or maintenance cost.

The runtime environment also includes libraries, system interfaces, memory management, and resource permissions. The same source can behave differently across versions or dependencies. Record language and runtime versions when analyzing an error.

Stages a compiler may perform

Lexing identifies tokens. Parsing combines them into structures such as an abstract syntax tree, or AST. Semantic checks handle types, names, and other language constraints. A compiler may then produce an intermediate representation, optimize it, and generate target code.

These stages provide a useful framework, not a universal sequence of identical files and steps. An AST omits some surface-syntax details. Intermediate representations usually support analysis and transformation. LLVM's frontend tutorial illustrates the separation without implying that every language uses LLVM.

A successful compilation establishes only that its checks passed. Missing files, permissions, external failures, and logic errors can still appear at runtime. Static types do not establish the truth of network inputs either.

Interpretation and JIT execution

An interpreter executes the meaning of a program representation, which need not be the original source text. An implementation can first compile to bytecode and execute it in a runtime. Just-in-time compilation can translate some representations into machine code during execution.

Compiled and interpreted are therefore not necessarily exclusive language properties. Ask what a particular implementation checks, which representation it produces and when, who executes it, and how startup and optimization costs are distributed.

None of these labels proves an application is faster. Data size, hot paths, allocation, call boundaries, and environment affect results. Performance claims require the relevant workload rather than a language category.

Define the DSL's allowed behavior

Specify input length, permitted literals, operators, numeric bounds, and error behavior before implementation. We use Python's AST parser but accept only exact integer constants and addition or multiplication nodes. Every other node is rejected.

Calls, names, attributes, and lists cannot execute. Negative values commonly parse as unary negation applied to a constant. This example rejects unary operations intentionally; Python itself supports negative numbers.

An AST is a structure, not a security sandbox. The boundary comes from an explicit allowlist, resource bounds, and the execution method. Checking a tree and then handing it to eval would introduce another execution path with unnecessary capabilities.

Evaluate a bounded expression

The example limits input to 80 characters, integer constants to absolute values of 1000, and intermediate results to absolute values of 1000000. Its recursive evaluator handles only permitted nodes. Parsing establishes operator precedence instead of guessing it through string replacement.

import ast

def calc(text):
    if len(text) > 80:
        raise ValueError("too long")
    def run(node):
        if isinstance(node, ast.Constant) and type(node.value) is int:
            if abs(node.value) > 1000:
                raise ValueError("too large")
            return node.value
        if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Mult)):
            left, right = run(node.left), run(node.right)
            result = left + right if isinstance(node.op, ast.Add) else left * right
            if abs(result) > 1000000:
                raise ValueError("too large")
            return result
        raise ValueError("unsupported expression")
    return run(ast.parse(text, mode="eval").body)

assert calc("2 + 3 * 4") == 14
try:
    calc("f(1)")
    raise AssertionError("call accepted")
except ValueError:
    pass
print(calc("2 + 3 * 4"))

The output is 14, and a function call is rejected. Here, mode="eval" requests expression parsing; it does not call Python's eval function. The exact type check also excludes Boolean constants.

This is a limited teaching interpreter without a complete user-facing error layer. Invalid syntax may raise SyntaxError; unsupported structures raise ValueError. An actual API should map these to stable errors and validate input type and request size without exposing stack traces.

Extend the contract before the language

Before adding division, decide whether it returns integers or fractions, how division by zero fails, and how rounding works. Before adding variables, specify their source, allowed names, missing values, and versioning. Every feature expands the input and execution boundary.

Document valid inputs, precedence, ranges, errors, format version, and upgrade behavior. Stored expressions should carry a version so changed language rules do not silently reinterpret them.

Exercise: test boundaries without expanding authority

Add checks for invalid parentheses, Booleans, floats, negative values, attributes, excessive length, and excessive intermediate results. These must fail while permitted additions, multiplications, and parentheses still calculate correctly.

Then add one defined capability, such as bounded unary negation. Define its rules and failures before implementation. Verify that the change introduces no name lookup, calls, file access, or networking, and record the language-version change.

Sources & further reading

Sources checked on 2026-11-10. Original engineering explanation; examples are teaching data, not production measurements.