Chapter 1. Learning SMoL - standard model of languages
Link: <https://smol-tutor.xyz/tutor/>.
`def1`
SMoL - Standard Model of Languages
Captures the shared computational core across widely-used languages (C#, Java, JavaScript, Python, Scala, Racket, and more).
8 core features:
Lexical scope.
Nested scope.
Eager evaluation.
Sequential evaluation (per thread).
Mutable first-order variables.
Mutable first-class structures (objects, vectors, etc.).
Higher-order functions with closures.
Automated memory management (garbage collection).
Inclusion criteria: Feature must be present across many diverse languages.
Excluded: static types (not universal), objects (too much variation).
Descriptive, not prescriptive - models what languages are, not what they should be.
Variable definition -> creates a binding between a name and a value.
lisp(defvar x 1)pythonx = 1Binding:
xbound to1.Lisp syntax:
(operator arg1 arg2 ...)- prefix notation, everything parenthesized.
Function definition
lisp(deffun (f x y) body)jsxfunction f(x, y) { return body; }Formal parameters -> variables in the definition.
Actual parameters (arguments) -> values passed at call time.
Nested definitions: function bodies can contain variable definitions.
(defvar n (+ x y))inside function ->nis a local variable.
Block -> a sequence of definitions and expressions.
Two places definitions can appear: top-level block and function bodies.
Blocks form a tree-like (nested) structure.
Values -> typical computation results: numbers, strings, booleans, etc.
Error -> another possible outcome of running a program.
Errors halt execution immediately - subsequent expressions don't run.
Sequential evaluation: expressions run in order until completion or error.
`def2`
Variable lookup follows the hierarchical block structure (lexical scope). Lookup order:
Check current block.
If not found -> check enclosing block (where current block is defined).
Repeat recursively outward.
Top-level block's parent -> primordial block.
If still not found -> error.
Primordial block -> invisible block enclosing top-level; contains language built-ins (e.g.,
+,/,print).This is lexical/static scoping: variable resolution based on code structure, not runtime call stack.
Scope -> the region of a program where a variable can be referenced.
Scope includes: the block where it's defined + all sub-blocks.
Shadowing -> when an inner block defines a variable with the same name as an outer block, the inner definition "shadows" (hides) the outer one.
`def3`
Variables are bound to values (not expressions).
Eager evaluation: expressions are evaluated immediately at definition time.
(defvar x (+ 1 2))->xis bound to3, not to(+ 1 2).Happens even if the variable is never used later.
Function calls: arguments evaluated immediately before binding to formal parameters.
Even if a parameter is never used in the function body.
Sequential evaluation: blocks execute in reading order (top-to-bottom, left-to-right).
`post1`
Nested functions can access variables from all enclosing scopes (lexical scope chain).
Local variables are not visible outside their defining block.
Referencing an undefined variable -> error.
Definitions processed top-to-bottom - can't use a variable before it's defined.
Eager evaluation: RHS expression must be computable at definition time.
`vectors1`
Mutable values -> values that can be changed after creation (e.g., vectors).
Vector -> an ordered collection that holds references to other values.
lisp(mvec ...)python[...]Vectors can contain: numbers, other vectors, or be empty. Examples:
(mvec 1 2)->vec[1, 2](two elements).(mvec 3)->vec[3](one element).(mvec)->vec[](empty).(mvec (mvec 1 2) (mvec 3) (mvec))->vec[vec[1,2], vec[3], vec[]](nested - holds references to the other three).Vectors hold references to values, not copies.
lisp(vec-ref vec idx)pythonvec[idx]0-indexed: first element is at position
0.
Mutation -> changing the contents of a value after it's created.
lisp(vec-set! vec idx val)pythonvec[idx] = val!convention in Lisp: functions ending with!typically mutate something.The vector itself isn't replaced; its contents are modified in place.
`vectors2`
Aliasing -> multiple references pointing to the same vector (no copying).
Vectors are never implicitly copied:
Assigning to a new variable -> same vector.
Passing to a function -> same vector.
Nesting in another vector -> same vector.
Heap -> memory region where vectors live; maps addresses to vectors.
Heap address -> unique identifier for each vector (e.g.,
@100,@200)Notation only - actual numbers are arbitrary/random.
Bindings vs. Heap are independent:
Creating a vector doesn't require a variable binding.
Creating a binding doesn't modify the heap.
Implication: mutating a vector through one reference affects all aliases.
`vectors3`
Heap notation & Rules
@ddd(e.g.,@100,@200) -> heap addresses (arbitrary numbers)#(...)-> vector contents at that addressHeap maps addresses only to vectors (not primitives like
33)Vectors store values (numbers or addresses), not printed representations like
#(3 4).
`post2`
Assigning a vector to another variable creates an alias, not a copy - mutations via one affect the other.
Can't reference a variable before its definition (eager, sequential evaluation).
Passing a vector to a function passes the reference - mutations inside the function affect the original.
Nested functions access enclosing scopes (lexical scope chain).
Local variables inside functions are not visible at top-level -> error.
Vectors can reference themselves (self-referential structures are valid).
Nested vectors store references - mutating an inner vector is visible through the outer vector's reference.
`mutvars1`
Variable assignment (
set!) mutates a variable's binding to a new value.Variables are not aliased - assignment affects only the mutated variable, not others.
Assigning
y = xcopies the value, not a reference to the variable - later changes toxdon't affecty.Vectors capture values at creation time - later variable reassignment doesn't affect stored values.
Reassigning a variable holding a vector creates a new binding, doesn't affect other references to the old vector.
Function parameters are independent bindings - mutating one doesn't affect the other.
Key distinction:
set!changes which value a variable points to;vec-set!changes contents of the value itself.
`mutvars2`
Variable assignment (
set!) mutates existing bindings - does not create new bindings; assigning to an undefined variable errors.Functions see the latest values of outer variables at call time, not the values from when the function was defined.
Environments (not blocks) bind variables to values - created dynamically at runtime like vectors on the heap.
Every function call creates a new environment - same function called twice = two separate environments with independent bindings.
What creates bindings: definitions and function calls (for parameters).
What mutates bindings: only variable mutations (
set!).
`post3`
Vectors are aliased - multiple variables pointing to the same vector share it; mutations via one affect all.
Variable assignment (
set!) only rebinds that variable - doesn't affect other variables or vectors that already captured the old value.Vectors passed to functions share the same reference - mutating vector contents inside a function affects the original.
Function parameters are separate bindings - mutating a parameter doesn't affect the caller's variable.
Functions always see the current value of outer variables at call time, not values from when the function was defined.
`refactor`
Each
(mvec ...)call creates a new, distinct vector - reusing a variable instead of callingmvecagain introduces aliasing.Aliasing can break code - if code expects two independent vectors, making them share the same reference causes mutations to affect both.
Passing the same variable vs. a fresh vector to a function - can break if the function mutates the argument (original vector gets modified).
Order of definitions matters - can't reference a variable before it's defined.
Safe refactorings: copying element-by-element into a new vector preserves independence; reusing variables for immutable/primitive values is safe.
`lambda1`
Functions are first-class values - they can be treated like any other value (numbers, vectors, etc.).
Functions can be: bound to variables, passed as arguments, returned from other functions, and stored in vectors.
A function stored in a variable can be called via that variable - multiple variables can reference the same function.
`lambda2`
Closures - functions remember the environment in which they were defined, "closing over" the bindings available at creation time
Each function call creates a new environment - calling the same outer function twice produces two closures with independent bindings
Closures can mutate variables in their enclosing environment, and each closure's mutations affect only its own captured environment
Closures see the current value of outer variables when called - if an outer variable is mutated after the closure is created, the closure sees the updated value
`lambda3`
Lambda expressions create anonymous functions (functions without names).
deffunis shorthand fordefvar+lambda:(deffun (f x y z) body)≡(defvar f (lambda (x y z) body)).
`post4`
Closures see the current value of outer variables at call time - mutations to outer variables are visible to the closure.
Variables are not aliased - assignment copies values, not references to bindings.
Different calls to a function create different closures with independent environments.
Mutating a parameter doesn't affect the caller's variable.
Reassigning a variable doesn't affect vectors/closures that already captured the old value.
`begin (lispy)`
beginevaluates sub-expressions left-to-right and returns the value of the last expression. Example:(begin 2 3 4)->4.ifrequires exactly 3 sub-expressions: condition, then-branch, else-branch - too few or too many is a syntax error.Use
beginto group multiple expressions into a single expression (useful forifbranches with side effects).(if cond thn els)- only one expression per branch allowed.(if (eq? n 0) 1 (begin (set! x 1) (* n x)))- usebeginfor multiple expressions.
`local (lispy)`
let basics:
Binds variables to values for use within its body
Bindings are local (not accessible outside the
let)Syntax requires brackets:
([var expr])not(var expr)
let scoping rule:
Left-hand-side variables are only in scope within the body
Right-hand-side expressions cannot reference any of the
letbound variablesThis means recursive functions and cross-references between bindings won't work
letrec scoping rule:
All left-hand-side variables are in scope for all right-hand-side expressions
Enables recursive and mutually recursive function definitions
All left-hand-side variable names must be distinct
let* scoping rule:
Each left-hand-side variable is in scope for all subsequent right-hand-side expressions
Allows sequential/chained bindings where later bindings can reference earlier ones
Same variable name can be rebound multiple times
Key distinction:
let: body only.letrec: all RHS expressions + body.let*: subsequent RHS expressions + body.