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)
movl $5, -4(%rbp)
movl -4(%rbp), %eax
imull %eax, %eax
movl %eax, -8(%rbp)
movl $0, %eax
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
The provenance of modern conveniences
Programming paradigms
Algorithms that read, translate, and execute code
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 the end of this activity, you will be able to:
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 — arguments in, return value out
- Read a simple regular expression like
\d+and say what it matchesIf 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 the questions on your own briefly, before discussing and comparing your thoughts with your group. Report out on areas of disagreement or items for which your group identified alternative approaches. 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, so that the next language you meet (or invent) is a configuration of familiar choices rather than a new world.
Three payoffs. 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 below 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:
Python (imperative/functional blend):
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
total = sum(x*x for x in nums if x % 2 == 0)
print(total) # 220
Python OO style:
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
class VowelCounter: # reusing class structure for illustration
def __init__(self, data): self.data = data
def sum_even_squares(self):
return sum(x*x for x in self.data if x % 2 == 0)
vc = VowelCounter(nums)
print(vc.sum_even_squares()) # 220
Scheme-style functional (Python simulation):
from functools import reduce
nums = list(range(1, 11))
even = list(filter(lambda x: x % 2 == 0, nums))
squared = list(map(lambda x: x * x, even))
total = reduce(lambda a, b: a + b, squared, 0)
print(total) # 220
All four approaches — same answer, different mental models:
nums = list(range(1, 11))
# Imperative: explicit state
total_imp = 0
for x in nums:
if x % 2 == 0:
total_imp += x * x
# Functional: composition
from functools import reduce
total_func = reduce(lambda a,b: a+b, map(lambda x: x*x, filter(lambda x: x%2==0, nums)), 0)
# Comprehension: declarative
total_comp = sum(x*x for x in nums if x % 2 == 0)
print(f"Imperative: {total_imp}")
print(f"Functional: {total_func}")
print(f"Comprehension: {total_comp}")
print(f"All equal? {total_imp == total_func == total_comp}")
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?
filter, map, reduce). Identify the analogous pieces hiding inside the Python 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 this pipeline is built from each language’s primitives — the small set of atoms the language treats as given rather than defined:
its primitive values (numbers, strings, booleans), the operators that combine them (+, <, and), and the forms that structure behavior (assignment, selection, iteration, function application).
Every language in this course — and the one your team designs — is some choice of primitives plus rules for composing them; the Type Systems and Control Flow and Statement Semantics activities are where those choices get made precise, one primitive at a time.
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:
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"
# A simple token spec: (type, pattern)
TOKEN_SPEC = [
("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)
price * 2 before adding 3. Where in the pipeline is that ordering decided: the lexer, the parser, or the interpreter? Defend your answer.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. In this model you’ll open the lid and watch Python process its own source code step by step, the same way a mechanic puts a car on a lift to show you what’s happening beneath the hood. Seeing Python’s actual tokens, AST, and bytecode demystifies the interpreter and previews exactly what you’ll build in your own project.
Python itself uses the same pipeline. You can inspect every stage:
import ast, dis, tokenize, io
source = "total = 3 + 2 * 5"
# Stage 1: Tokens
print("=== 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
print("\n=== AST ===")
tree = ast.parse(source)
print(ast.dump(tree, indent=2))
# Stage 3: Bytecode (compiled)
print("\n=== BYTECODE ===")
code = compile(source, "<string>", "exec")
dis.dis(code)
total = 3 + 2 * 5? Which token is the operator precedence information not encoded in (it appears in the AST instead)?BinOp with Mult nested inside Add. How does the tree encode precedence without any explicit precedence rules?BINARY_OP instructions. These are the output of Python’s compiler. What is the input to an interpreter, by contrast?Watch out! Python is both compiled and interpreted: it compiles your source to bytecode (the
.pycfiles), 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 — not CPython’s bytecode VM. Both are interpreters in spirit; just be clear which level you’re describing.
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.
(, ), -, /, and floating-point numbers like 3.14. Test it on result = (a - 3.14) / b. How many tokens does it produce?2 * (x + 1): list tokens, draw the parse tree, and show the evaluation order.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.