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?"
Do you know?
Do you actually know?
Then what is a "programming language"?
01
which strings are programs, and what they are made of
02
what those programs mean
03
a program that runs programs
04
a program that translates programs
Four words. Each one is a field.
Syntax can be a lot of things (maybe), but for our purposes:
which strings are programs;
what those strings are made of.
Semantics can be a lot of things (for sure), among which:
The theory module takes some of these seriously. Here we take one, and run it.
Executes a program, written in a formal language.
The classic shape is the REPL: Read → Eval → Print → Loop.
… this is getting weird.
Translates a program from a source language to a target language. The result runs later.
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.
That's a silly question.
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.
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 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
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.
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.)
\( 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.
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 · 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 of let z = 10: every rule used.
Abstract syntax tree: only what matters for meaning.
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.
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
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.
…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:
...
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}'")
>>> evaluate(example, {"x": 4.0})
14.0
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.
\( ⟦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.
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.
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.
Beyond calculators
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.
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.
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…
Access time is linear in the number of previous updates. Crazy!
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.
So are domain-specific languages. Unusual examples to think about:
clearly a sequence of "instructions".
on a shop's web site: an interpreter for filter expressions.
synthesisers, ComfyUI, LabVIEW, Scratch, After Effects, Max/MSP, PureData…
abstract interpretation, model checking, type checking.
watch a system and check that it respects given properties.
simplification, instrumentation for performance or debugging.
run a program many times; find the corner cases that must be tested.
Something to be tested, checked statically, or monitored
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.
Think about it.
To ask the right question you need a domain of discourse. We shall call such domains semantic domains.
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.
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
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.
The short answer: yes.
The long answer: the rest of this course.
Practical matters
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
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.
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.
A: Yes.