Speaker 1: Have you ever felt, you know, like programming languages are
Speaker 1: handed down from on high by these unapproachable wizards.
Speaker 2: Oh. Absolutely, Like there's this mystical council of language designers
Speaker 2: somewhere right.
Speaker 1: And you, the lowly programmer, you just have to accept
Speaker 1: their arbitrary rules.
Speaker 2: Yeah, the rules about type systems or memory management or
Speaker 2: scope exactly.
Speaker 1: And it's a super pervasive mindset in software engineering.
Speaker 2: Yeah, it really is. I mean we're conditioned from our
Speaker 2: very first Hello World tutorial to view a programming language
Speaker 2: as this, well, this unyielding law of physics.
Speaker 1: Yeah. You just memorize the syntax, you parse the documentation,
Speaker 1: and you try your best to operate inside the sandbox
Speaker 1: that somebody else.
Speaker 2: Built for you, which is incredibly limiting, honestly it is.
Speaker 1: So today's deep dive is about entirely shattering that illusion.
Speaker 1: We're using the classic computer science text Essentials of Programming Languages,
Speaker 1: specifically the third edition by Daniel P. Friedman and Mitchell Wan,
Speaker 1: a total classic. Yeah, and we are going back to
Speaker 1: the foundational mechanism of language design. We're exploring how an
Speaker 1: interpreter is actually, when you get down to it, just
Speaker 1: another program, right, It's not magic exactly, and mastering that
Speaker 1: concept shifts your mindset from being a mere consumer of
Speaker 1: a language to being an architect. Okay, let's unpack this.
Speaker 1: Where do we even begin with a paradigm shift like.
Speaker 2: That, Well, we start by dismantling that artificial wall, you know,
Speaker 2: the one between a program and a programming language.
Speaker 1: Right.
Speaker 2: In the forward to the book, hal Abelson shares this
Speaker 2: really revealing story about his early career. He was building
Speaker 2: this massive data entry and information retrieval system using PLI. Yeah,
Speaker 2: using PLI, which was this notoriously heavy, feature rich language
Speaker 2: at the time.
Speaker 1: Right, and he viewed PLI as this rigid rule book like.
Speaker 1: His job, as he saw it back then, was to
Speaker 1: pick through this massive manual, select predefined features, and then
Speaker 1: painstakingly fit them together into a monolith exactly.
Speaker 2: He describes a resulting program as a mosaic. Mosaic yeah,
Speaker 2: meaning every individual piece of logic had to be carefully
Speaker 2: chiseled to fit exactly into place. It was fragile, it
Speaker 2: was inflexible.
Speaker 1: Because he never considered that there was an underlying architectural
Speaker 1: logic to how PLI was organized, or that he actually
Speaker 1: had the agency to override those original designer's decisions right to.
Speaker 2: Better suit his own domain. But you don't question the
Speaker 2: wizards who wrote the compiler.
Speaker 1: No, you just pray you don't trigger a syntax error, right.
Speaker 2: But the revelation Ableson ultimately shares is that the interpreter
Speaker 2: for a computer language is literally just another program executing instructions.
Speaker 2: It's just code, just code. I mean, almost every complex
Speaker 2: software system you interact with today, whether it's a sophisticated
Speaker 2: CAD tool, or a modern web browser's rendering engine, or
Speaker 2: even the logic loop of a video game engine, they
Speaker 2: all contain an embedded interpreter.
Speaker 1: Oh wow, I guess I never thought about it like that.
Speaker 2: Yeah, that interpreter acts as the dynamic glue, allowing users
Speaker 2: to combine individual operations into customs patterns. And what's fascinating
Speaker 2: here is that once you internalize this, you realize that
Speaker 2: advanced programmers don't just write monolithic programs in one fixed
Speaker 2: general purpose language they don't know they view themselves as
Speaker 2: creators of custom embedded sub languages, like domain specific languages
Speaker 2: tailored specifically for the problem they're trying to solve.
Speaker 1: Okay, I picture the traditional mindset like building a Lego
Speaker 1: set using only the instruction manual that came in the box.
Speaker 2: That's a great way to look at it.
Speaker 1: Yeah, you follow the predetermined steps, you build the specific
Speaker 1: spaceship on the cover, and you stop. But this master
Speaker 1: programmer mindset they're describing, it's like having a plastic molding
Speaker 1: machine in your living room exactly.
Speaker 2: You're making your own pieces, right.
Speaker 1: You're inventing entirely new Lego shapes with custom interlocking mechanisms
Speaker 1: to solve a problem the original Lego designers never even anticipated.
Speaker 1: But wait, if we're inventing our own Lego bricks, how
Speaker 1: do we systematically describe their shapes? I mean, we can't
Speaker 1: just throw melted plastic at the wall and hope a
Speaker 1: valid expression emerges.
Speaker 2: Well, no, you definitely can't do that. To build a
Speaker 2: language that a computer can actually execute, we first need
Speaker 2: a mathematically rigorous way to define the data, the new
Speaker 2: brex that our language is going to parse and manipulate.
Speaker 1: Okay, and this is where the math comes in.
Speaker 2: Yeah. The book grounds this in the concepts of inductive
Speaker 2: definitions and grammars.
Speaker 1: Which involves defining complex structures from the ground up using
Speaker 1: rules of inference. So, if you're listening to this and
Speaker 1: trying to visualize it. Picture like a horizontal line on
Speaker 1: a talkboard. Above that line you write your hypotheses, the
Speaker 1: axioms or previously proven facts you know are true. Below
Speaker 1: the line, you write your conclusion. If the conditions above
Speaker 1: the line are met, the statement below the line is
Speaker 1: mathematically guaranteed to be true exactly.
Speaker 2: And by using these rules of inference, you build these
Speaker 2: complex nested data structures out of simpler atomic ones. And
Speaker 2: to handle this systematically for programming languages, the book Formal Grammars,
Speaker 2: right the grams. Yeah, So a grammar consists of non
Speaker 2: terminal symbols, which represent the abstract sets of things being defined,
Speaker 2: like an expression or a statement.
Speaker 1: Got it.
Speaker 2: Then you have terminal symbols, which are the literal strings
Speaker 2: or characters in the source code.
Speaker 1: So a terminal symbol is your literal parenthesis, your comma,
Speaker 1: or your your let keyword yes exactly.
Speaker 2: And then finally you have productions, which are the actual
Speaker 2: rules connecting them. They're typically denoted with a double colon
Speaker 2: and an equal sign along with notation like the clean
Speaker 2: star for you know, zero or more repetitions.
Speaker 1: Okay, So a non terminal is the overarching concept like
Speaker 1: a list of integers, and the production rule dictates that
Speaker 1: a list of integers can be an empty list, or
Speaker 1: it can be a single integer attached to another list
Speaker 1: of injuries.
Speaker 2: Exactly. It's inherently recursive, right, And that recursion brings us
Speaker 2: to the first really monumental takeaway slogan of the book,
Speaker 2: which is follow the grammar.
Speaker 1: Follow the grammar. It sounds like a mantra.
Speaker 2: It is a mantra, but it's also the architectural secret
Speaker 2: to writing code that processes complex nested data safely. How So, well,
Speaker 2: when you write procedures that operate on inductively defined data,
Speaker 2: the control flow of your interpreter must perfectly mirror the
Speaker 2: recursive structure of your grammar.
Speaker 1: Oh perfectly mirror it.
Speaker 2: Yes, For every single non terminal in your language's grammar,
Speaker 2: you write exactly one corresponding procedure, and inside that procedure,
Speaker 2: you write one specific branching alternative, like a case in
Speaker 2: the switch statement, for every single production rule that defines
Speaker 2: that nonterminal.
Speaker 1: And the authors pair that with a second equally strict slogan. Right,
Speaker 1: no mysterious auxiliaries, yes, meaning, if you introduce a helper
Speaker 1: function into your interpreter, like maybe something tracking the current
Speaker 1: index position. While you're traversing an abstract syntax tree. You
Speaker 1: are required to formally specify the behavior of that function
Speaker 1: on all possible inputs, not just the initial starting state.
Speaker 2: Precisely, you have to be exhausted.
Speaker 1: I have to admit, as developer, my instinct is to push.
Speaker 2: Back on this. Oh really, why?
Speaker 1: It just feels unbelievably pedantic, Like if I write a
Speaker 1: complex grammar for a binary search tree, are you telling
Speaker 1: me I must write a dedicated function for the interior
Speaker 1: nodes and a completely separate dedicated function just for the
Speaker 1: leaf nodes, even if they share some logic.
Speaker 2: Yes, that's exactly what they're saying.
Speaker 1: It feels like baking a three tier cake and being
Speaker 1: told my recipe must have explicitly separate standalone chapters for
Speaker 1: the bottom tier, the middle tier, and the top tier,
Speaker 1: otherwise the kitchen will literally catch fire.
Speaker 2: This raises an important question, why demands such strict, pedantic
Speaker 2: rigidity from the programmer?
Speaker 1: Yeah? Why?
Speaker 2: The answer lies in the mechanics of a mathematical concept
Speaker 2: called structural induction, which is honestly your only defense against catastrophic,
Speaker 2: hard to trace bugs in compiler design.
Speaker 1: Okay, lay the mechanics of structural induction on me.
Speaker 2: So when your code structure perfectly mirrors your grammar's structure,
Speaker 2: you unlock the ability to mathematically prove your program's care.
Speaker 1: Yeah.
Speaker 2: Yeah. Structural induction works in two steps. First, you prove
Speaker 2: your procedure works on the base cases, the simplest, smallest
Speaker 2: pieces of data, like an empty list or the absolute
Speaker 2: leaf of a binary tree.
Speaker 1: Right, the absolute boto right.
Speaker 2: Second, you prove the combination step, meaning, assuming the procedure
Speaker 2: works on a small subset of the tree, you prove
Speaker 2: it will correctly process a node that combines those subsets.
Speaker 1: Okay, I see. By forcing the interpreter's code to look
Speaker 1: exactly like the data's grammar, you automatically eliminate those bizarre
Speaker 1: edge cases where nested structures mysteriously cause a stack overflow
Speaker 1: or just you know, silently fake.
Speaker 2: Exactly, if the base cases are sound and the combination
Speaker 2: logic is sound, the entire infinite tree is sound.
Speaker 1: But once we have defined all this incredibly detailed data,
Speaker 1: I certainly don't want to look at those messy structural
Speaker 1: details every time I write an evaluation step. I mean,
Speaker 1: when building a language. We need a mechanism to hide
Speaker 1: that underlying complexity, don't.
Speaker 2: We we do, and that requirement pivots us directly into
Speaker 2: data abstraction. Data abstraction is the practice of strictly dividing
Speaker 2: our data types into a public interface and a hidden implementation.
Speaker 1: So the interface dictates what the data can do, the
Speaker 1: constructors used to build it in memory, and the observers
Speaker 1: used to extract information from it, and the implementation is
Speaker 1: the raw memory management and pointer logic operating under.
Speaker 2: The hood, exactly. And to demonstrate the power of this separation,
Speaker 2: Friedman and Wand used this great conceptual example of natural numbers.
Speaker 2: Oh yeah, the interface for a natural number requires very little.
Speaker 2: You just need a constructor for zero, an observer to
Speaker 2: check if a value is zero, and constructors to generate
Speaker 2: the successor or predecessor of a given number.
Speaker 1: Right, So the client code just calls successor five and
Speaker 1: expects six exactly, But.
Speaker 2: The underlying implementation of those natural numbers can be wildly different.
Speaker 2: The book introduces a unary representation where numbers are essentially
Speaker 2: represented as linked lists of boolean values.
Speaker 1: Low my mind. So under the hood, the number two
Speaker 1: isn't a binary integer it's literally a list containing two
Speaker 1: true values.
Speaker 2: Right and zero is just an empty list.
Speaker 1: Which is incredibly inefficient for computation.
Speaker 2: Oh terribly inefficient, but it flawlessly satisfies the required interface.
Speaker 2: A more practical implementation they discuss is big nums. So
Speaker 2: instead of relying on the host machine's standard memory registers,
Speaker 2: which might overflow if an integer exceeds sixty four bits,
Speaker 2: big numbs represent massive integers as lists of smaller digits
Speaker 2: in a very large base. Wow. This allows your language
Speaker 2: to handle astronomical calculations without the client code ever knowing
Speaker 2: the memory allocation strategy actually changed.
Speaker 1: The rest of the interpreter remains blissfully ignorant. It asks
Speaker 1: for a number, and it gets a number. But here's
Speaker 1: where it gets really interesting, at least for me. It
Speaker 1: was when the authors applied this abstraction concept to environments. Oh.
Speaker 2: Yes, environments are crucial. They're the mechanical backbone of lexical
Speaker 2: scoping in any.
Speaker 1: So, an environment is simply the data structure that associates
Speaker 1: variable names with their bound values. Like when your language
Speaker 1: evaluates an expression containing the variable X, it queries the
Speaker 1: environment to retrieve the value currently bound to X.
Speaker 2: Right and you could implement an environment as an a list,
Speaker 2: which is just a linear list of paired variable value temples, okay.
Speaker 2: Or you could use a ribcage structure where you maintain
Speaker 2: one list of variables running parallel to a separate list
Speaker 2: of corresponding values.
Speaker 1: But then the book introduces the procedural representation.
Speaker 2: Break down the mechanics of the procedural representation because it's brilliant.
Speaker 1: Okay. So instead of representing an environment as passive data
Speaker 1: sitting in memory, an environment can be represented by its action.
Speaker 1: Invoking the environment actually executes a first class procedure that
Speaker 1: was constructed dynamically for the sole purpose of remembering a
Speaker 1: specific variable binding.
Speaker 2: It's so cool.
Speaker 1: Think of it like this. The traditional data structure approach
Speaker 1: is writing a friends addressed down on a physical Rolodex card.
Speaker 1: When you need it, you manually iterate through the kurds.
Speaker 2: Right flipping through them.
Speaker 1: Yeah, but the procedural representation is like hiring a highly
Speaker 1: trained smart assistant whose single purpose in the universe is
Speaker 1: to stand in your office and wait for you to ask,
Speaker 1: what is John's address?
Speaker 2: Huh? Exactly?
Speaker 1: When queried, they immediately execute their function and speak the
Speaker 1: answer you aren't storing the raw string data. You are
Speaker 1: storing an executable action that yields the data, and that.
Speaker 2: Assistant is literally a closure holding onto state. You are
Speaker 2: exploiting the advanced features of your host language, specifically higher
Speaker 2: order functions, to create a living computational mapping.
Speaker 1: But suppose your underlying implementation language like C doesn't support
Speaker 1: higher order functions, you can't hire the.
Speaker 2: Smart assistant, right, You're stuck.
Speaker 1: You have to take the exact instructions that assistant would
Speaker 1: have followed, encode them into a concrete structor record, and
Speaker 1: pass that passive data strepsture to a dedicated apply procedure function.
Speaker 2: And that mechanical transformation is the technique called defunctionalization.
Speaker 1: So by defunctionalizing, we convert dynamic procedural behavior back into
Speaker 1: static data structures.
Speaker 2: Exactly, we're separating the pure executable meaning of the code
Speaker 2: from the way it's actually represented in memory.
Speaker 1: Which perfectly maps to the broader architectural separation of abstract
Speaker 1: syntax from concrete syntax. Yes, exactly, Concrete syntax is the
Speaker 1: literal texts the programmer types, the brackets, the semicolons. The
Speaker 1: parser reads that text and converts it into an abstract
Speaker 1: syntax tree, stripping away the formatting and leaving only the
Speaker 1: structural meaning.
Speaker 2: And by combining data abstraction, defunctionalized environments, and abstract syntax trees,
Speaker 2: you unlock what the book terms the interpreter recipe.
Speaker 1: The interpreter recipe which dictates the control flow of the
Speaker 1: entire system. Step one, look at a piece of abstract
Speaker 1: syntax data. Step two, determine its grammatical type. Step three
Speaker 1: extract its underline components using the observer interface, and recursively
Speaker 1: apply the correct behavior.
Speaker 2: And armed with procedural environments to manage state and that
Speaker 2: interpreter recipe to traverse the syntax trees, we finally have
Speaker 2: the architectural runway to dissect the actual mini languages constructed
Speaker 2: in the book.
Speaker 1: Yes, this is where we bring the theory to life.
Speaker 1: The book systematically upgrades our capability by walking through three
Speaker 1: progressive languages LET, praio, C, and LITREK.
Speaker 2: Let's start with LED. The LED language establishes our baseline
Speaker 2: for local scope. It introduces the let expression, allowing a
Speaker 2: programmer to bind a variable to a computed value, but
Speaker 2: strictly within an isolated block of code.
Speaker 1: Right, So, when a let expression is evaluated, the interpreter
Speaker 1: takes the current environment, creates a new extended environment containing
Speaker 1: the new binding, and evaluates the inner block of code using.
Speaker 2: That new environment exactly. And once the block finishes, that
Speaker 2: extended environment is just discarded and the temporary variable vanishes.
Speaker 1: It's elegant memory management, but lee T is just the foundation.
Speaker 1: The real paradigm shift arrives with the PROC language.
Speaker 2: Oh PROC is huge. PROC elevates procedures from static top
Speaker 2: level definitions into first class express.
Speaker 1: Values, meaning in PRC, a function is treated exactly like
Speaker 1: an integer or a boolean.
Speaker 2: Yes, you can pass a function as an argument to
Speaker 2: another function, you can return a function from a computation,
Speaker 2: or bind a function to a variable.
Speaker 1: But to make first class functions work, we have to
Speaker 1: confront the mechanics of lexical scope enclosures. The authors emphasize
Speaker 1: that a procedure's behavior must be governed by the environment
Speaker 1: that existed at the exact moment the procedure was created,
Speaker 1: not the environment where it happens to be executed later, and.
Speaker 2: The underlying data structure that enforces this rule is called
Speaker 2: a closure. When the interpreter evaluates a procedure definition it
Speaker 2: allocates a closure in memory.
Speaker 1: Okay, what's in it?
Speaker 2: That closure captures three distinct pieces of data, the formal parameter,
Speaker 2: which is the input variable name, the abstract syntax tree
Speaker 2: of the procedure's body, and crucially, a pointer to the
Speaker 2: exact active environment at that moment of creation.
Speaker 1: You know, while studying this, I initially questioned the necessity
Speaker 1: of that captured environment pointer. Really yeah, I kept thinking,
Speaker 1: why wouldn't a function just grab the closest available variable
Speaker 1: bindings from the call stack when it is actually executed.
Speaker 1: That avoids storing the environment entirely? Ah? I see, And
Speaker 1: the alternative I was imagining is actually known as dynamic scope.
Speaker 2: Right, and the authors include exercises exploring dynamic scope to
Speaker 2: demonstrate exactly why it's considered an anti pattern in modern
Speaker 2: language design. Yeah.
Speaker 1: I came up with an analogy for this. Lexical scoping
Speaker 1: is like packing a highly specific lunch box in your
Speaker 1: own kitchen before you leave for the day.
Speaker 2: Okay, I like this.
Speaker 1: It doesn't matter if you travel to a corporate office,
Speaker 1: a park bench, or submarine. When you invoke your lunch
Speaker 1: break and open that box, you consume the exact sandwich
Speaker 1: prepared in your home environment. The function carries its native
Speaker 1: context with it.
Speaker 2: That makes perfect sense, But.
Speaker 1: Dynamic scoping is like walking into whatever room you currently
Speaker 1: happen to be in and trying to eat whatever items
Speaker 1: are lying around. Oh wow, Yeah, if you're called inside
Speaker 1: a bakery, you eat bread. If you're called inside a
Speaker 1: hardware store, you eat nails. It's chaotic because merely renaming
Speaker 1: a local variable in the calling function can completely hijack
Speaker 1: the behavior of the executed function.
Speaker 2: That sounds like a debugging nightmare. And if we connect
Speaker 2: this to the bigger picture, that lexical lunchbox. The closure
Speaker 2: is honestly one of the most mechanically powerful inventions in
Speaker 2: the history of computer science. Really yes, Because a function
Speaker 2: securely remembers its creation environment, it enables advanced functional programming patterns.
Speaker 1: Like currying right named after the mathematician Haskell Curry exactly.
Speaker 2: Currying is the mechanical process of taking a function that
Speaker 2: requires multiple arguments and breaking it down into a sequence
Speaker 2: of functions that each take single argument. You pass the
Speaker 2: first argument to the outer function, which returns an inner function,
Speaker 2: and because of lexical closures, that inner function inherently remembers
Speaker 2: the environment where the first argument.
Speaker 1: Was bound, so you can chain these closures together dynamically,
Speaker 1: building these complex stataware pipelines without ever relying on global
Speaker 1: variables exactly. But there is a glaring mechanical hurdle when
Speaker 1: environments and functions operate like this. How do you implement
Speaker 1: a recursive function?
Speaker 2: Ah the recursive nott.
Speaker 1: Right. If I'm writing a function named calculate factorial, its
Speaker 1: internal body needs to call calculate factorial, but I am
Speaker 1: currently in the middle of defining it. Yep, the environment
Speaker 1: doesn't contain the binding for calculate factorial yet because the
Speaker 1: closure hasn't finished being built.
Speaker 2: It is the ultimate compiler chicken and egg paradox. A
Speaker 2: function cannot be added to an environment until its closure
Speaker 2: is created, but the closure needs to capture the environment
Speaker 2: containing the function.
Speaker 1: So how do they solve it?
Speaker 2: Solving this requires the final language progression. The trick introduces
Speaker 2: a new specialized environment operation called extend on.
Speaker 1: Veriic okay, extend on verik yeah, and.
Speaker 2: It solves the paradox by fundamentally mutating the environment structure
Speaker 2: to tie a recursive knot. Mechanically, the interpreter allocates a
Speaker 2: new environment frame with an empty placeholder or an uninitialized
Speaker 2: pointer for the function name okay. It then creates the closure,
Speaker 2: pointing the closure's captured environment back to this new frame. Finally,
Speaker 2: it reaches back into the frame and overwrites the empty
Speaker 2: placeholder with a pointer to the newly minted closure.
Speaker 1: So it creates a circular reference in memory. Yes, the
Speaker 1: environment points to the closure and the closure points back
Speaker 1: to the environment. It allows the function to look up
Speaker 1: its own definition within its own native context, safely executing
Speaker 1: recursion without unraveling the fabric of the interpreter. It's brilliant,
Speaker 1: it really is. So what does this all mean. Let's
Speaker 1: bring this vast mechanical landscape back to you, the listener.
Speaker 1: We started this deep dive by asking if programming languages
Speaker 1: felt like arbitrary magic spells handed down by untouchable wizards.
Speaker 2: And hopefully we've proved they aren't exactly.
Speaker 1: You've seen that language design is entirely systematic. If you
Speaker 1: follow the grammar, you can predictably parse infinite complexity. By
Speaker 1: leveraging data abstraction, you separate interface from memory implementation seamlessly,
Speaker 1: upgrading from unary lists to massive integer arrays. You've learned
Speaker 1: that environments can be passive lists or dynamic procedural closures
Speaker 1: that capture their native state like a packed lunch box,
Speaker 1: and you understand the memory mutation required to tie a
Speaker 1: recursive knot.
Speaker 2: Programming languages are not mystical incantations. They're just meticulously structured
Speaker 2: data processed by recursive recipes.
Speaker 1: Just data and recipes.
Speaker 2: Yeah. An interpreter is nothing more than a program that
Speaker 2: traverses an abstract syntax tree, resolves variable bindings in an
Speaker 2: environment frame, and applies operations. By studying the mechanics and
Speaker 2: essentials of programming languages, you acquire the conceptual blueprint to
Speaker 2: look at any modern language, whether it's Python's dynamic typing,
Speaker 2: Russ's barrochecker, or JavaScript's event loop, and actually understand the
Speaker 2: architectural trade offs operating under the hood.
Speaker 1: You transition from a mere user of tools into a
Speaker 1: designer of systems exactly which leads us, with one final
Speaker 1: brain bending mechanical paradox, to mull over.
Speaker 2: Oh, I know where this is going.
Speaker 1: We learned today that an interpreter is just a program
Speaker 1: written in a foundational defining language. The authors use scheme
Speaker 1: to construct the interpreters for LT and PROC, but consider
Speaker 1: the frontier of language design. What happens if you decide
Speaker 1: to write the interpreter for a brand new language entirely
Speaker 1: in that new.
Speaker 2: Language you're referencing the compiler bootstrapping paradise.
Speaker 1: Yes, imagine you design language X and you write the
Speaker 1: compiler for language X using the syntax of language X.
Speaker 1: How could that compiler possibly execute for the very first
Speaker 1: time if language X does not currently exist on the
Speaker 1: machine to translate the instructions into binary.
Speaker 2: It's wild to think about.
Speaker 1: Do you write a crude temporary interpreter an assembly just
Speaker 1: to run the compiler once so it can compile itself.
Speaker 1: It's a mechanical raboros and it is something for you
Speaker 1: to mul over the next time you hit the build button.
Speaker 1: Remember those wizards handing down the rules, They had to
Speaker 1: construct their own wands from scratch first. Thanks for joining
Speaker 1: us on this deep dive.