What a language is made of

Lecture 1, part A. From here on, everything is in the exam programme.

Programming Languages Lab · University of Pisa
Vincenzo Ciancia, CNR-ISTI
vincenzo.ciancia@isti.cnr.it · github.com/vincenzoml/PLDW

"Programming languages?"

You know what a programming language is!

Do you know?

Do you actually know?

Then what is a "programming language"?

A lot of things, actually

01

Syntax

which strings are programs, and what they are made of

02

Semantics

what those programs mean

03

Interpreter

a program that runs programs

04

Compiler

a program that translates programs

Four words. Each one is a field.

"Syntax"

Syntax can be a lot of things (maybe), but for our purposes:

context-free grammars

which strings are programs;

syntax trees

what those strings are made of.

"Semantics"

Semantics can be a lot of things (for sure), among which:

operationaldenotationallogical transition systemsrewrite rulesterm or graph rewriting fixed pointscategory theoryequivalences coalgebrasdialgebrasbialgebras bisimilaritysimulationmany other things…

The theory module takes some of these seriously. Here we take one, and run it.

"Interpreter"

Executes a program, written in a formal language.

The classic shape is the REPL: Read → Eval → Print → Loop.

ReadEvalPrintLoopthe only stepthis course studies

Exercise: how many interpreters can you name?

LISPPythonBashMatlabOctave RMS-DOSJavaScriptBASIC ExcelOfficeChromePhotoshop ApachesshdPDFPostScript SVGBitwig StudioComfyUI

… this is getting weird.

"Compiler"

Translates a program from a source language to a target language. The result runs later.

programinterpreterresultinterpreter: runs it nowprogramcompilerprogram′later: runresultcompiler: translates it, the result runs later

Let us name a few compilers

C · C++ · Fortran · Pascal · assembler — too far back in time?

Java · C# · F# · LaTeX

Transpilers: Fable (F# to JavaScript), Emscripten (C to WebAssembly)…

The compiler inside your graphics driver, compiling shaders while the game starts.

Which is faster, an interpreter or a compiler?

That's a silly question.

Faster at what?

tree-walkinginterpreterbytecode +virtual machineJIT compilerahead-of-timecompilerstarts fastruns fastPython lives hereVoxLogicA lives here

Faster at what — starting, or running for a day?

And most real systems are both: Python compiles to bytecode and interprets it; a JIT compiles while it interprets.

The characters are not the program

2 + (x * 3)2+(x*3)2 + (x * 3)parse2x3*+one tree
Three strings, one tree. Spaces, line breaks and brackets are gone; the shape stays.

Same program, three times. What stays fixed is a tree: an addition whose right operand is a multiplication.

The parentheses are not in the tree. They only told the reader which tree: 2 + x * 3 could have been (2 + x) * 3.

Abstract and concrete

ABSTRACT SYNTAX TREE

The tree is the abstract syntax tree (AST): it has forgotten spaces, brackets, line breaks — everything that does not affect meaning.

CONCRETE SYNTAX

The characters are the concrete syntax: one of many ways to write the tree down.

PARSER

Characters → tree is the job of a parser (Chapter 3). Until then, we write the trees by hand.

Half one

Syntax: which trees exist

The syntax, in Python


from dataclasses import dataclass

@dataclass(frozen=True)
class Number:
    value: float

@dataclass(frozen=True)
class Variable:
    name: str

@dataclass(frozen=True)
class BinaryOp:
    left: Expr
    operator: str
    right: Expr

type Expr = Number | Variable | BinaryOp
    

Read the last line as a definition

An expression is a number, or a variable, or a binary operation — and nothing else.

The tree for 2 + (x * 3)


example = BinaryOp(
    left=Number(2.0),
    operator="+",
    right=BinaryOp(left=Variable("x"), operator="*", right=Number(3.0)),
)
      

A BinaryOp contains two further expressions: the definition is recursive, so trees of any depth exist.

frozen=True: a tree never changes after it is built. (Chapter 2 says why.)

Number 2Variable xNumber 3BinaryOp *BinaryOp +

The same thing, as a mathematician writes it

\( e ::= n \;\mid\; x \;\mid\; e \; \mathit{op} \; e \)

"An expression is a numeral, or a name, or an expression, an operator and an expression."

This is a grammar, in the notation Chomsky's work led to. The Python and the grammar say the same thing. The grammar is shorter; the Python runs.

Moving between the two is a skill this course teaches.

A grammar, with production rules

Production rules


Expr  ::= Var | Int | Expr + Expr
        | let Var = Expr | ( Expr )
Var   ::= x | y | z | …
Int   ::= Digit | Digit Int
Digit ::= 0 | 1 | … | 9
      

A derivation


Expr
→ let Var = Expr
→ let z = Expr
→ let z = ( Expr )
→ let z = ( Expr + Expr )
→ let z = ( Var + Expr )
→ let z = ( x + Expr )
→ let z = ( x + Int )
→ let z = ( x + Digit Int )
→ let z = ( x + 4 Int )
→ let z = ( x + 4 2 )
      

Derivation and parsing

DERIVATION · grammar → string

Derivation generates a string: repeatedly replace a non-terminal symbol using a production rule, until all symbols are terminal.

PARSING · string → rules

Parsing goes the other way: given a string, find a set of rule choices that derives it.

We will not study parsing algorithms. From Chapter 3 on, a parser generator does it for us, from the grammar.

Parse tree vs abstract syntax tree

letzVar=1Digit0DigitIntIntExprExpr

Parse tree of let z = 10: every rule used.

z10let

Abstract syntax tree: only what matters for meaning.

Abstract syntax

WHY

Avoids most complications of infix and prefix operators, precedence, parentheses, digits.

FORMALLY

It is an initial algebra. Not a topic in this course: we just assume we have an in-language representation of the AST.

HERE

In Python, that representation is the dataclasses you just saw.

What syntax does not say

÷0

An ice cream, divided by zero. A perfectly good tree.

Syntax does not say

that an ice cream can be divided at all,

that division by zero is forbidden,

or that ÷ divides.

Syntax says which trees exist. It is deliberately silent about what they mean.

Half two

Semantics: what a tree means

After parsing, execution

How is that done? Depends on the language!

We will use formal semantics, and implement its mathematical definition directly in Python.

−

not going to be super efficient;

+

going to be very clear;

+

good for prototyping, easy to refine for efficiency later.

The meaning of an expression

…is a number — once we know the value of every variable.

So the meaning of a tree is a function from an environment (a table from names to values) to a number.


type Env = dict[str, float]

def evaluate(expr: Expr, env: Env) -> float:
    ...
    

One case per kind of node


def evaluate(expr: Expr, env: Env) -> float:
    match expr:
        case Number(value):
            return value
        case Variable(name):
            if name not in env:
                raise NameError(f"variable '{name}' is not defined")
            return env[name]
        case BinaryOp(left, operator, right):
            lv = evaluate(left, env)
            rv = evaluate(right, env)
            match operator:
                case "+": return lv + rv
                case "-": return lv - rv
                case "*": return lv * rv
                case "/": return lv / rv
                case _: raise ValueError(f"unknown operator '{operator}'")
  

It runs


>>> evaluate(example, {"x": 4.0})
14.0
      
22x433*12+14
Each subtree first, then the node: 4 × 3 = 12, then 2 + 12 = 14. With ρ = {x ↦ 4}.

01
The shape of the function is the shape of the syntax

One case per kind of node, nothing else. match takes the tree apart: case BinaryOp(left, operator, right) names the three children.

Add a kind of node to Expr → add a case to evaluate.

Not a coincidence of Python. This is what a definition of meaning looks like.

02
Recursive exactly where the syntax is

\( ⟦e_1 + e_2⟧\rho \;=\; ⟦e_1⟧\rho + ⟦e_2⟧\rho \)

"The meaning of the tree e₁ + e₂ in environment ρ is the sum of the meanings of the subtrees."

The + on the left is a piece of syntax.

The + on the right is addition of numbers.

The equation and the line case "+": return lv + rv say the same thing.

03
The decisions are visible

MISSING VARIABLE

we raise. A spreadsheet returns 0. A computer algebra system leaves x in the answer.

1 / 0

right now, whatever Python does.

SO

None of these is forced by the syntax. Each is a decision about what the language means.

A designer who has not made them has not finished designing.

Same characters, different trees


left_first  = BinaryOp(BinaryOp(Number(2), "*", Number(3)), "+", Number(4))
right_first = BinaryOp(Number(2), "*", BinaryOp(Number(3), "+", Number(4)))

>>> evaluate(left_first, {}), evaluate(right_first, {})
(10.0, 14.0)
      

evaluate does not know about precedence and does not need to. Precedence is a rule about which tree a string stands for: it belongs to the parser.

Keeping the two apart lets you change the surface of a language without touching its meaning — and the other way round.

23*4+10234+*14(2 * 3) + 42 * (3 + 4)
The number beside a node is its value. Same characters, different trees, different numbers.

Beyond calculators

Why semantics, in practice

Example: variable assignment

C-like syntax: x = 3. What does it do? Modify the memory!

How do we formalise it?

Memory is a partial function from variables to values.*

* this is not even precise — we will study it in detail.

memx1y7z2mem′ = update(mem, x, 3)x3y7z2x = 3

What is memory update, formally?

x = k is interpreted as \( \mathit{update}(\mathit{mem}, x, k) = \mathit{mem}' \) where

\( \mathit{mem}'(x) = k \)

\( \mathit{mem}'(y) = \mathit{mem}(y) \text{ if } y \neq x \)

Q: mathematically, to which set do mem and mem′ belong?

\( \mathit{Var} \rightharpoonup \mathit{Val} \): the partial functions from variables to values.

Why am I mentioning all this?

In hardware, memory update changes one cell. Access time stays constant.

Now implement memory as a chain of function updates, as on the previous slide: each lookup asks the newest function, which asks the previous one, which…

mem₃x ↦ 5mem₂z ↦ 0mem₁x ↦ 3mem₀y ↦ 7, …lookup(y): not me, ask the previous one …found after 4 steps

Access time is linear in the number of previous updates. Crazy!

Easy to fix?

Just use arrays for memory, and array update in place of functional update.

BUT

then it is no longer formalised. Program semantics is the way to formalise arrays — so using arrays to formalise program semantics is circular.

WAY OUT: ABSTRACTION

put memory operations in a module, class or library; define them inefficiently, to study the language; define them efficiently, at a second stage.

Interpreters are everywhere

So are domain-specific languages. Unusual examples to think about:

Undo / redo

clearly a sequence of "instructions".

Filtering products

on a shop's web site: an interpreter for filter expressions.

Graph-based programming

synthesisers, ComfyUI, LabVIEW, Scratch, After Effects, Max/MSP, PureData…

Why study semantics from a practical perspective?

Program analysis and verification

abstract interpretation, model checking, type checking.

System monitoring

watch a system and check that it respects given properties.

Program transformation

simplification, instrumentation for performance or debugging.

Testing

run a program many times; find the corner cases that must be tested.

Something to be tested, checked statically, or monitored

What is a "property"?

What is your formal model?

Begging the question? No. How can you test properties of something, if you don't know what you can actually observe?

It is like watching a bicycle race and testing for how many goals have been scored.

cycloid: x = r(t − sin t), y = r(1 − cos t)
12 pentagons · 20 hexagons

Think about it.

To ask the right question you need a domain of discourse. We shall call such domains semantic domains.

Observing a system

Consider a system — not even "programmed", any system in the universe. How do we analyse its behaviour? Via observations. Quite obvious.

When the system is a programming language, the observations are the semantics:

\( \mathit{SEM} : \mathit{PROG} \to D \)

Q: what is D? The semantic domain.

PROGD — what we can observe2 + 24x := 1; y := x4{x ↦ 1, y ↦ 1}SEM

Operational or denotational?

It is all very well to aim for a more 'abstract' and a 'cleaner' approach to semantics, but if the plan is to be any good, the operational aspects cannot be completely ignored.

Dana S. Scott, Outline of a Mathematical Theory of Computation, Oxford PRG-2, 1970

What matters is what

Not how a program computes, but the result — still stating the obvious. But the "result" could be:

01

just a value;

02

the execution trace leading to that value;

03

the tree of possible choices leading to many possible values, depending on user interaction;

04

…

Transition systems, non-determinism, bisimilarity, coalgebras, categorical models: that is where this road goes.

So, what are we? Programming language designers?

The short answer: yes.

The long answer: the rest of this course.

Practical matters

How this course works

What you build

You pick an application domain — images, music, geometry, graphs, sound, text, 3D models, or one you propose — and design a domain-specific language for it:

01

analyse the domain and choose its primitives;

02

decide their semantics;

03

build an interpreter, and make the results visible;

04

write examples that show the language is good for something.

Tools and rules

TOOLS

Python 3.12+; lark from Chapter 3 on.

AI

AI assistants are allowed and expected. Not hidden, not confessed.

OWNERSHIP

What you must own: every primitive, every example, every design decision.

A language you cannot explain is not your language, whoever typed it.

Prerequisites: functions, lists, dictionaries, recursion. Rusty? Part B of this lecture.

What we will not do

parsing theory, lexing, automata — a parser generator does it for us;

efficient implementations;

proper input and output;

compilers — but you will understand the basics of compilers after this course.

Q: Shall we stop here?

A: Yes.