Welcome to CS374!

int main(void) {
int y = 5;
int y2 = y * y;
printf("%d squared is %d.", y, y2);
}
(define square
(lambda(n)
(* n n)
)
)
(square 5)
x86-64
movl $5, -4(%rbp)
movl -4(%rbp), %eax
imull %eax, %eax
movl %eax, -8(%rbp)
movl $0, %eax
MIPS
li $2,5
sw $2,0($fp)
lw $2,0($fp)
mul $2,$2,$2
sw $2,4($fp)
00100000000010100000000000000101 # li t2 5
10101111110010100000000000000000 # sw
10001111110010100000000000000000 # lw
00000001010010100000000000011000 # mult
00000000000000000101000000010010 # mflo t2
10101111110010100000000000000100 # sw 4(fp)
The evolution of programming languages from assembly language to intermediate languages to our favorite high level languages
Programming paradigms
You do not need to write any code today. Bring curiosity and a laptop; today is discussion and paper-and-pencil tracing.
Every programmer uses languages, but few understand why they work the way they do — or how to evaluate a new one quickly.
Studying programming language theory is like learning music theory: you can play guitar without it, but understanding harmony, rhythm, and form lets you compose music rather than just repeat what you’ve heard.
This course gives you the composer’s toolkit — you’ll read languages, compare them, and ultimately build one from scratch. The companion reading The Arc of This Course: From Symbols to Languages previews every destination on that map; today we take the first step.
By December, your team will have built a programming language of your own: a lexer, a parser, and an interpreter, assembled from components you write one assignment at a time. Today we ask why that journey is worth taking. We move from what a language is → why languages differ → the pipeline from text to behavior → how this course works.
Before You Begin: this activity assumes you can write and run a basic Python script (loops, functions, list comprehensions), describe what a function call does at a high level, and read a simple regular expression like
\d+. If any of these feel shaky, review them first.
Throughout this course, we work in POGIL-style teams of three or four with rotating roles:
Consider each model below and answer the questions provided. First reflect on your own briefly, then discuss and compare with your group. Report out on areas of disagreement. After class, respond to the reflective prompt individually in your notebook.
A programming language is a precise notation for computation. It is an agreement among three parties: the human who writes, the machine that executes, and, most often forgotten, the other humans who read. Every language is a set of design decisions about syntax (what programs look like), semantics (what programs mean), and pragmatics (what programs are easy or hard to express).
You already speak several. Python, probably Java or C, perhaps SQL or regular expressions; each made different choices. Studying principles of programming languages means learning the design space itself.
First, you become a better programmer in every language, because you see through syntax to the semantics beneath.
Second, you become able to build languages: configuration formats, query languages, and domain-specific notations are everyday engineering artifacts.
Third, you join an intellectual tradition connecting logic, linguistics, and computing, from the lambda calculus of the 1930s to the languages being designed this year.
Intuition: Think of the same recipe written in four different styles — a traditional French cookbook, a quick-reference cheat sheet, a flowchart, and a voice command to a smart speaker. The dish comes out the same, but each style reflects different assumptions about who’s cooking and what they already know. The four code snippets that follow do the same arithmetic, but each reveals a different programming paradigm — a different mental model for organizing computation.
The same computation — summing the squares of the even numbers in a list — in four notations:
Comprehension (declarative):
nums = list(range(1, 11))
total = sum(x*x for x in nums if x % 2 == 0)
print(total) # 220
Imperative (explicit state):
total = 0
for x in nums:
if x % 2 == 0:
total += x * x
print(total) # 220
Continuing with the same nums:
Object-oriented:
class EvenSquares:
def __init__(self, data): self.data = data
def total(self):
return sum(x*x for x in self.data if x % 2 == 0)
print(EvenSquares(nums).total()) # 220
Scheme-style functional:
from functools import reduce
even = filter(lambda x: x % 2 == 0, nums)
squared = map(lambda x: x * x, even)
print(reduce(lambda a, b: a + b, squared, 0)) # 220
For each version, identify what the programmer must keep track of (loop counters, intermediate state, nothing?). Which version says what to compute and which says how?
Rank the four for readability by a newcomer, and separately for your own confidence that each is correct. Did the rankings differ? Why might they?
The Scheme-style version is built from three reusable pieces (filter, map, reduce). Identify the analogous pieces hiding inside the comprehension.
Watch out! “Declarative” does not mean “shorter.” A SQL query can be more verbose than an equivalent Python loop. Declarative means you describe what result you want, not the steps to produce it. Don’t confuse conciseness with paradigm.
Every implementation answers the same question: how does this string of characters become behavior? The classical pipeline, which is also the skeleton of this course and of your project, proceeds in stages:
\[\text{characters} \xrightarrow{\text{lexer}} \text{tokens} \xrightarrow{\text{parser}} \text{syntax tree} \xrightarrow{\text{interpreter}} \text{value}\]The lexer (scanner) groups characters into meaningful units called tokens, using the machinery of regular expressions and finite automata. The parser assembles tokens into a tree according to a grammar.
The interpreter walks the tree, computing values within environments that give names their meanings. A compiler shares the front half and differs at the back, emitting code instead of computing values; we focus on interpretation, and the principles transfer.
What flows through the pipeline is built from each language’s primitives: its values (numbers, strings, booleans), the operators that combine them (+, <, and), and the forms that structure behavior (assignment, selection, iteration, function application).
Every language is some choice of primitives plus rules for composing them; the Type Systems and Control Flow activities make those choices precise.
In the pipeline above, the component whose job is to decide that the characters c, o, u, n, t form a single identifier token is:
The lexer. Grouping characters into tokens is exactly its job; every later stage sees tokens, never raw characters.
Intuition: Imagine a customs officer at an airport breaking your passport down into individual fields — name, date of birth, nationality — before any decision gets made. The lexer does exactly that: it reads a raw stream of characters and groups them into labeled chunks (“this is a number,” “this is a variable name”) so the later stages never have to squint at raw text again. In this model you’ll write a tiny version of that officer.
Consider the source text: total = 3 + price * 2
A minimal Python tokenizer — watch the pipeline live:
import re
source = "total = 3 + price * 2"
TOKEN_SPEC = [ # (type, pattern)
("NUMBER", r"\d+(\.\d*)?"),
("IDENT", r"[A-Za-z_]\w*"),
("ASSIGN", r"="),
("PLUS", r"\+"),
("STAR", r"\*"),
("WS", r"\s+"),
]
master = "|".join(f"(?P<{name}>{pat})" for name, pat in TOKEN_SPEC)
tokens = []
for m in re.finditer(master, source):
kind = m.lastgroup
if kind != "WS":
tokens.append((kind, m.group()))
for tok in tokens:
print(tok)
As a team, list the tokens the lexer produced. How many are there? Did anyone’s count differ?
The interpreter must compute price * 2 before adding 3. Where in the pipeline is that ordering decided: the lexer, the parser, or the interpreter? Defend your answer.
Suppose the text were total = 3 + * 2. At which stage should the error be caught, and what should a helpful error message say?
Watch out! The lexer cannot catch all errors — it only sees one token at a time and has no memory of what came before.
total = 3 + * 2contains five perfectly valid tokens; the sequence is illegal. Only the parser, which knows the grammar rules about how tokens may be combined, can flag that two operators in a row is a syntax error.
Intuition: You’ve been using Python as a black box — type code in, get output out. Here we open the lid and watch Python process its own source code stage by stage, the way a mechanic puts a car on a lift. Python uses the very same pipeline you just traced by hand, and you can inspect every stage of it.
import ast, dis, tokenize, io
source = "total = 3 + 2 * 5"
# Stage 1: Tokens
tokens = list(tokenize.generate_tokens(io.StringIO(source).readline))
for tok in tokens:
if tok.type not in (tokenize.NL, tokenize.NEWLINE, tokenize.ENDMARKER):
print(f" {tokenize.tok_name[tok.type]:10} {tok.string!r}")
# Stage 2: AST
tree = ast.parse(source)
print(ast.dump(tree, indent=2))
# Stage 3: Bytecode (compiled)
code = compile(source, "<string>", "exec")
dis.dis(code)
Watch out! Python is both compiled and interpreted: it compiles source to bytecode, then a virtual machine interprets that bytecode. When we say “interpreter” in this course, we mean the tree-walking variety that reads an AST directly.
How many tokens does Python produce for total = 3 + 2 * 5? Which token is the operator precedence information not encoded in (it appears in the AST instead)?
The AST shows a BinOp with Mult nested inside Add. How does the tree encode precedence without any explicit precedence rules?
The bytecode shows BINARY_OP instructions. These are the output of Python’s compiler. What is the input to an interpreter, by contrast?
The first half of the semester builds your skills bottom-up through scaffolded individual assignments: regular expressions, automata, a lexer, a parser, an interpreter.
In the second half, your team snaps those components together into a language of your own design, developed in sprints with rotating roles, a gallery walk peer review, and a public Demo Day. Along the way we study languages as artifacts (Scheme, the lambda calculus, modern features) so your design choices are informed by sixty years of others’ choices.
In your notebook: describe one moment when a programming language fought you — when the thing you wanted to say was hard to express. Knowing you will design a language this semester, what would you change to make that moment easier? And after seeing Python’s own tokens/AST/bytecode pipeline, does Python feel more or less like magic to you?
ast module docs: help(ast) in Python shows every node type you’ll encounter.Up next: the Programming Paradigms activity tours the worldviews your language could adopt — and the Overview and Warmup assignments begin the journey.