|
| 1 | +^:kindly/hide-code |
| 2 | +^{:clay {:title "A Grammar of Physics" |
| 3 | + :quarto {:type :post |
| 4 | + :author [:timothypratley] |
| 5 | + :date "2025-09-07" |
| 6 | + :description "Build physics simulations from words with Instaparse, sans compilation." |
| 7 | + :category :math |
| 8 | + :tags [:physics] |
| 9 | + :keywords [:instaparse :parsing :grammar]}}} |
| 10 | +(ns instaparse.grammar-of-physics.rigid-body |
| 11 | + (:require [scicloj.kindly.v4.kind :as kind])) |
| 12 | + |
| 13 | +;; As Clojurists, we naturally think in terms of data structures and transformations. |
| 14 | +;; Yet we often overlook one of our most powerful tools: the ability to create custom languages through grammars. |
| 15 | +;; Today we'll explore how the amazing [Instaparse](https://github.com/Engelberg/instaparse) |
| 16 | +;; can be used to define a grammar of physics, and how a custom evaluator |
| 17 | +;; can bring that grammar to life as a physics simulation. |
| 18 | + |
| 19 | +;; ## The Experiment |
| 20 | + |
| 21 | +^:kindly/hide-code |
| 22 | +(kind/hiccup |
| 23 | + [:div |
| 24 | + [:div.text-muted.small.mb-2 "Click and drag the objects to move them"] |
| 25 | + [:div#matter-canvas-container {:style {:width "100%"}}] |
| 26 | + [:div.d-flex.justify-content-between.align-items-center.mb-3 |
| 27 | + [:div.btn-group.btn-group-sm |
| 28 | + [:button.btn.btn-danger {:onClick "explode()"} "explode"] |
| 29 | + [:button.btn.btn-outline-primary {:onClick "rotate()"} "rotate"]] |
| 30 | + [:div.btn-group.btn-group-sm |
| 31 | + [:button.btn.btn-outline-secondary {:onClick "loadExample(0)"} "load1"] |
| 32 | + [:button.btn.btn-outline-secondary {:onClick "loadExample(1)"} "load2"] |
| 33 | + [:button.btn.btn-outline-secondary {:onClick "loadExample(2)"} "load3"] |
| 34 | + [:button.btn.btn-outline-secondary {:onClick "loadExample(3)"} "load4"] |
| 35 | + [:button.btn.btn-outline-secondary {:onClick "loadExample(4)"} "load5"]]] |
| 36 | + [:div.mb-3 |
| 37 | + [:textarea#program.form-control.font-monospace.mb-2.bg-light |
| 38 | + {:rows "12" |
| 39 | + :style {:font-family "monospace" |
| 40 | + :font-size "0.85rem"}}] |
| 41 | + [:div.d-flex.justify-content-end.mb-2 |
| 42 | + [:button.btn.btn-success {:onClick "evalProgram()"} "evaluate"]] |
| 43 | + [:pre#parse-error.alert.alert-danger.mt-2 |
| 44 | + {:role "alert" |
| 45 | + :style {:font-family "monospace" |
| 46 | + :font-size "0.85rem"}}]]]) |
| 47 | + |
| 48 | +;; ::: {.callout-tip} |
| 49 | +;; Modify the scene and click "evaluate" to see the changes. |
| 50 | +;; Try replacing `--` connections with `%%` springs. |
| 51 | +;; ::: |
| 52 | + |
| 53 | +;; What you see above is a rigid body simulation. |
| 54 | +;; The scene was generated from a text description using |
| 55 | +;; a Domain-Specific Language (DSL) for physics. |
| 56 | + |
| 57 | +;; ## The Hypothesis |
| 58 | + |
| 59 | +;; Look familiar? |
| 60 | +;; If you have worked with graph visualization tools like Graphviz, |
| 61 | +;; you will recognize the declarative style. |
| 62 | +;; But instead of describing visual relationships, |
| 63 | +;; we are describing *physical* ones: bodies, constraints, and composites. |
| 64 | + |
| 65 | +;; The beauty lies in the syntax: `ball -- a` creates a rigid connection, |
| 66 | +;; `a ~~ b` forms a rope, and `b %% floor` adds a spring. |
| 67 | +;; Functions like `domino` let us abstract common patterns. |
| 68 | +;; Attributes in brackets fine-tune physical properties like mass and stiffness. |
| 69 | + |
| 70 | +;; Some affordances for layout are made with the `grid` function, |
| 71 | +;; but a lot more could be done here to automate positioning. |
| 72 | + |
| 73 | +;; ## The Theory |
| 74 | + |
| 75 | +;; Here is my formal grammar of rigid body physics for simulations. |
| 76 | + |
| 77 | +^:kindly/hide-code |
| 78 | +(kind/hiccup |
| 79 | + [:div.mb-3 |
| 80 | + [:textarea#grammar.form-control.font-monospace.bg-light |
| 81 | + {:rows "18" |
| 82 | + :style {:font-family "monospace" |
| 83 | + :font-size "0.85rem"}}] |
| 84 | + [:div.btn-group.btn-group-sm.mt-2 |
| 85 | + [:button.btn.btn-primary {:onClick "instaparse()"} "Instaparse"] |
| 86 | + [:button.btn.btn-outline-secondary {:onClick "resetGrammar()"} "reset"]] |
| 87 | + [:div#grammar-error.alert.alert-danger.mt-2 |
| 88 | + {:role "alert" |
| 89 | + :style {:font-family "monospace" |
| 90 | + :font-size "0.85rem"}}]]) |
| 91 | + |
| 92 | +;; This EBNF (Extended Backus-Naur Form) specification defines our language's constructs, |
| 93 | +;; including bodies with various shapes, constraints, functions, calls, numbers, and symbols. |
| 94 | + |
| 95 | +;; ::: {.callout-tip} |
| 96 | +;; Try changing the grammar, clicking Instaparse, and see what happens. |
| 97 | +;; ::: |
| 98 | + |
| 99 | +;; ## The Application |
| 100 | + |
| 101 | +;; Instaparse takes the grammar and makes a parser. |
| 102 | +;; When we parse a program the result is an Abstract Syntax Tree (AST). |
| 103 | + |
| 104 | +^:kindly/hide-code |
| 105 | +(kind/hiccup |
| 106 | + [:div.mb-3 |
| 107 | + [:h4.h5.text-muted.mb-3 "Abstract Syntax Tree"] |
| 108 | + [:pre#ast.border.rounded.p-3.bg-light |
| 109 | + {:style {:min-height "200px" |
| 110 | + :max-height "500px" |
| 111 | + :overflow "auto" |
| 112 | + :font-family "monospace" |
| 113 | + :font-size "0.85rem"}}]]) |
| 114 | + |
| 115 | +;; The AST is the structure of our physics program. |
| 116 | +;; Every concept is organized into a tree that preserves syntax and semantics, as data. |
| 117 | + |
| 118 | +;; ## The Equation |
| 119 | + |
| 120 | +;; The AST is a data representation of our program, not a data representation of the simulation itself. |
| 121 | +;; To bring it to life, we need to evaluate the program. |
| 122 | +;; The full implementation is in [rigid_body.cljs](rigid_body.cljs), but for now, |
| 123 | +;; let's examine a small part of it. |
| 124 | + |
| 125 | +;; ```clojure |
| 126 | +;; :call (let [[fn-name & args] xs |
| 127 | +;; fn-val (or (get-in env [:labels fn-name]) |
| 128 | +;; (get core fn-name) |
| 129 | +;; (throw (ex-info (str "Function not found: " fn-name) |
| 130 | +;; {:id ::function-not-found |
| 131 | +;; :fn-name fn-name |
| 132 | +;; :env env}))) |
| 133 | +;; [env arg-vals] (eval-vector env args) |
| 134 | +;; [fn-env result] (apply fn-val env arg-vals) |
| 135 | +;; env (merge env (select-keys fn-env (vals entity-plural)))] |
| 136 | +;; [env result]) |
| 137 | +;; ``` |
| 138 | + |
| 139 | +;; This is the rule for handling `:call` nodes in the AST. |
| 140 | +;; The body of the call node contains a function name and arguments. |
| 141 | +;; We look up the function in the environment. |
| 142 | +;; Arguments are evaluated, the function is applied, and the environment modified. |
| 143 | + |
| 144 | +;; Our evaluator walks the AST, interprets each node according to our physics semantics, |
| 145 | +;; and adds bodies, constraints, and composites into an environment. |
| 146 | +;; That environment is then traversed to hydrate a Matter.js rigid body simulation. |
| 147 | + |
| 148 | +;; What I love most about the evaluator and hydration is that all the tedious |
| 149 | +;; `Matter.World.add(Matter.Bodies.create(...))` nonsense disappears. |
| 150 | +;; When all you have is a code API, |
| 151 | +;; boilerplate clutters up your simulation to the point that it becomes difficult to reason about. |
| 152 | +;; The textual descriptions, in contrast, only describe the domain. |
| 153 | + |
| 154 | +;; Here we start to see how programs beat data representations; **compression through implication**. |
| 155 | +;; Our statement `domino: (x, y => rectangle x y 10 50)` labels a function that implies object creation. |
| 156 | +;; When we write `ball -- a`, it implies lookup by label and constraint creation. |
| 157 | +;; Programs let us work at the level of *intent*. |
| 158 | + |
| 159 | +;; An alternative solution is to create a "world builder" that writes the simulation as data. |
| 160 | +;; Indeed, there is fertile middle ground here. |
| 161 | +;; Our program produces a data representation from the AST before hydrating the world, |
| 162 | +;; so this approach can work in conjunction with a world builder. |
| 163 | +;; The grammar feels more powerful and compositional for initial creation, |
| 164 | +;; whereas a world builder excels at making adjustments in a more tactile way. |
| 165 | + |
| 166 | +;; ## Breaking the Laws of Compilation |
| 167 | + |
| 168 | +;; This Instaparse demo runs in your browser, |
| 169 | +;; but [rigid_body.cljs](rigid_body.cljs) has not been compiled to JavaScript. |
| 170 | +;; Isn't that impossible? |
| 171 | +;; Libraries like Instaparse require compilation! |
| 172 | + |
| 173 | +;; [Scittle](https://github.com/babashka/scittle) brings the Small Clojure Interpreter (SCI) to the browser as a script. |
| 174 | +;; Scittle interprets ClojureScript without compilation. |
| 175 | +;; [Scittle Kitchen](https://timothypratley.github.io/scittle-kitchen/) |
| 176 | +;; is a collection of pre-prepared ClojureScript libraries as scittle plugins, |
| 177 | +;; including Instaparse. |
| 178 | +;; Currently, there are 15 unofficial plugins, including asami, clara-rules, emmy, and geom. |
| 179 | + |
| 180 | +;; ## The Laboratory |
| 181 | + |
| 182 | +;; I include the plugins and [rigid_body.cljs](rigid_body.cljs) in this page as scripts: |
| 183 | + |
| 184 | +(kind/hiccup |
| 185 | + [:div |
| 186 | + [:script {:src "https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.20.0/matter.min.js"}] |
| 187 | + [:script {:src "https://cdn.jsdelivr.net/npm/scittle-kitchen/dist/dev/scittle.js"}] |
| 188 | + [:script {:src "https://cdn.jsdelivr.net/npm/scittle-kitchen/dist/dev/scittle.pprint.js"}] |
| 189 | + [:script {:src "https://cdn.jsdelivr.net/npm/scittle-kitchen/dist/dev/scittle.instaparse.js"}] |
| 190 | + [:script {:src "https://cdn.jsdelivr.net/npm/scittle-kitchen/dist/dev/scittle.cljs-devtools.js"}] |
| 191 | + [:script {:type "application/x-scittle" |
| 192 | + :src "rigid_body.cljs"}]]) |
| 193 | + |
| 194 | +;; This loads Matter.js for the physics simulation, |
| 195 | +;; Scittle for ClojureScript evaluation, |
| 196 | +;; the Instaparse plugin, development tools, and our custom evaluator. |
| 197 | + |
| 198 | +;; When I save this namespace, Clay reloads the page for me. |
| 199 | +;; When I save [rigid_body.cljs](rigid_body.cljs), Clay hot-loads just the code for me. |
| 200 | +;; This is a convenient way to mess around with an idea, |
| 201 | +;; and produce a working artifact that I can share with people. |
| 202 | + |
| 203 | +;; ## The Universe of Possibilities |
| 204 | + |
| 205 | +;; The Grammar of Graphics influenced data visualization by providing a systematic way to describe visual mappings. |
| 206 | +;; Libraries like ggplot2 and Vega became powerful precisely because they gave users a *language* to express their intent. |
| 207 | +;; Can we apply this same principle in physics? |
| 208 | + |
| 209 | +;; ::: {.callout-note} |
| 210 | +;; In the lab, we build setups to explore principles, test boundaries, and learn by doing. |
| 211 | +;; By constructing a grammar and evaluator, we create a playground for ideas. |
| 212 | +;; ::: |
| 213 | + |
| 214 | +;; A well-thought-out set of grammars for physical simulation setup would enable modeling and experimentation. |
| 215 | +;; Consider how experimental physics setups are naturally described as connected components: |
| 216 | +;; `laser -> mirror -> beam-splitter -> detector` could define an interferometer setup. |
| 217 | +;; The value is in being able to quickly and precisely describe an experiment, |
| 218 | +;; making it easy to communicate, share, and reproduce. |
| 219 | +;; Imagine chemical reaction networks where |
| 220 | +;; `H2 + O2 => H2O [catalyst=Pt, temp=400K]` defines stoichiometry and conditions, |
| 221 | +;; or ecosystem models where `grass -> rabbit -> fox` creates food webs with population dynamics. |
| 222 | + |
| 223 | +;; It's possible to build up libraries of functional compositions of the components in such a grammar. |
| 224 | +;; With these you could quickly prototype setups by combining and adjusting. |
| 225 | +;; This would make it easier to share experimental designs as text, |
| 226 | +;; or generate documentation and visualizations. |
| 227 | + |
| 228 | +;; ## The Result |
| 229 | + |
| 230 | +;; The intersection of grammars and physics is worth exploring. |
| 231 | +;; Tools like [Scittle](https://github.com/babashka/scittle), |
| 232 | +;; [Scittle Kitchen](https://timothypratley.github.io/scittle-kitchen/), |
| 233 | +;; and [Clay](https://github.com/scicloj/clay) |
| 234 | +;; make it fun to try ideas with minimal setup, from the comfort of my favorite editor. |
| 235 | + |
| 236 | +;; We explored three ideas: |
| 237 | + |
| 238 | +;; **1. Grammar**: |
| 239 | +;; By creating a grammar tailored to physics simulation, |
| 240 | +;; we make concepts accessible through syntax, |
| 241 | +;; and a means of combination. |
| 242 | + |
| 243 | +;; **2. Interpretation**: |
| 244 | +;; Our evaluator transforms the parsed AST into physics simulations, |
| 245 | +;; applies functions, and makes Matter.js API calls. |
| 246 | + |
| 247 | +;; **3. Uncompiled**: |
| 248 | +;; Instaparse enables redefinition of the grammar on the fly, |
| 249 | +;; and Scittle-kitchen allows me to use it without compilation. |
| 250 | + |
| 251 | +;; Scittle Kitchen plugins include many other interesting ClojureScript libraries. |
| 252 | +;; If grammars aren't your cup of tea, I'm sure you'll find something else of interest. |
| 253 | + |
| 254 | +;; Until next time, |
| 255 | +;; keep parsing the possibilities. |
| 256 | + |
| 257 | +;;  |
0 commit comments