<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hercules Lemke Merscher</title>
    <description>The latest articles on DEV Community by Hercules Lemke Merscher (@bitmaybewise).</description>
    <link>https://dev.to/bitmaybewise</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg</url>
      <title>DEV Community: Hercules Lemke Merscher</title>
      <link>https://dev.to/bitmaybewise</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bitmaybewise"/>
    <language>en</language>
    <item>
      <title>Tsonnet #45 - The devil in the details #1</title>
      <dc:creator>Hercules Lemke Merscher</dc:creator>
      <pubDate>Sun, 19 Jul 2026 17:18:38 +0000</pubDate>
      <link>https://dev.to/bitmaybewise/tsonnet-45-the-devil-in-the-details-1-1hdi</link>
      <guid>https://dev.to/bitmaybewise/tsonnet-45-the-devil-in-the-details-1-1hdi</guid>
      <description>&lt;p&gt;Welcome to the &lt;a href="https://gitlab.com/bitmaybewise/tsonnet" rel="noopener noreferrer"&gt;Tsonnet&lt;/a&gt; series!&lt;/p&gt;

&lt;p&gt;If you're not following along, check out how it all started in &lt;a href="https://dev.to/bitmaybewise/tsonnet-0-from-zero-to-interpreter-54na"&gt;the first post of the series&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the previous post, we removed every catch-all pattern match across the compiler, letting OCaml's exhaustiveness checker tell us exactly which variants were being silently ignored:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bitmaybewise/tsonnet-44-the-devil-in-the-details-0-4nm0" class="crayons-story__hidden-navigation-link"&gt;Tsonnet #44 - The devil in the details #0&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/bitmaybewise" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" alt="bitmaybewise profile" class="crayons-avatar__image" width="279" height="279"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/bitmaybewise" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Hercules Lemke Merscher
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Hercules Lemke Merscher
                
              
              &lt;div id="story-author-preview-content-4174385" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/bitmaybewise" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" class="crayons-avatar__image" alt="" width="279" height="279"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Hercules Lemke Merscher&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/bitmaybewise/tsonnet-44-the-devil-in-the-details-0-4nm0" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 18&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/bitmaybewise/tsonnet-44-the-devil-in-the-details-0-4nm0" id="article-link-4174385"&gt;
          Tsonnet #44 - The devil in the details #0
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/jsonnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;jsonnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tsonnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tsonnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/compiler"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;compiler&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ocaml"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ocaml&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/bitmaybewise/tsonnet-44-the-devil-in-the-details-0-4nm0" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/bitmaybewise/tsonnet-44-the-devil-in-the-details-0-4nm0#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            4 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;The scope checker is on the operating table first. Let's go.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwfwveb61m3l1gktatvda.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwfwveb61m3l1gktatvda.jpeg" alt="Telescope" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope-checking conditionals
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;If&lt;/code&gt; variant was the most obvious omission — the scope checker had no arm for it. Before the catch-all removal, it was swallowed by &lt;code&gt;| _ -&amp;gt; ok ()&lt;/code&gt;. After the first cleanup pass in the previous post it was still sitting in a list of variants that were silently accepted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ocaml"&gt;&lt;code&gt;&lt;span class="c"&gt;(* For any other expression types, no special scope validation needed *)&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Null&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Bool&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;EvaluatedObject&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;RuntimeObject&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;ObjectPtr&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;FunctionDef&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;FunctionCall&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Closure&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;If&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's wrong. An &lt;code&gt;if&lt;/code&gt; can contain &lt;code&gt;self&lt;/code&gt; or &lt;code&gt;$&lt;/code&gt; in its condition, then-branch, or else-branch. If that &lt;code&gt;if&lt;/code&gt; is outside an object, the scope checker should catch it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/errors/conditional_self_out_of_scope.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;then&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;self.one&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;value&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before this fix, that passed without error. &lt;code&gt;validate_if&lt;/code&gt; recursively walks all three branches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/scope.ml b/lib/scope.ml
index 48dfa83..b4e36a3 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/scope.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/scope.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -51,10 +50,12 @@&lt;/span&gt; let rec _validate expr context =
     _validate expr context
   | IndexedExpr (_, _, index_expr) -&amp;gt;
     _validate index_expr context
&lt;span class="gi"&gt;+  | If (_, cond_expr, then_expr, else_expr_opt) -&amp;gt;
+    validate_if cond_expr then_expr else_expr_opt context
&lt;/span&gt;   (* For any other expression types, no special scope validation needed *)
   | Unit | Null _ | Number _ | String _ | Bool _ | EvaluatedObject _
   | RuntimeObject _ | ObjectPtr _ | FunctionDef _ | FunctionCall _ | Closure _
&lt;span class="gd"&gt;-  | If _ -&amp;gt; ok ()
&lt;/span&gt;&lt;span class="gi"&gt;+    -&amp;gt; ok ()
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ocaml"&gt;&lt;code&gt;&lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;validate_if&lt;/span&gt; &lt;span class="n"&gt;cond_expr&lt;/span&gt; &lt;span class="n"&gt;then_expr&lt;/span&gt; &lt;span class="n"&gt;else_expr_opt&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="n"&gt;_validate&lt;/span&gt; &lt;span class="n"&gt;cond_expr&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
  &lt;span class="n"&gt;_validate&lt;/span&gt; &lt;span class="n"&gt;then_expr&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;else_expr_opt&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt; &lt;span class="n"&gt;else_expr&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_validate&lt;/span&gt; &lt;span class="n"&gt;else_expr&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the earlier example correctly errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;dune &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; tsonnet samples/errors/conditional_self_out_of_scope.jsonnet
ERROR: samples/errors/conditional_self_out_of_scope.jsonnet:1:27 Can&lt;span class="s1"&gt;'t use self outside of an object

1: local value = if true then self.one else 1;
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[1]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The positive case — &lt;code&gt;self&lt;/code&gt; inside an &lt;code&gt;if&lt;/code&gt; inside an object — still works as expected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/conditionals/conditional_object_scope.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;one:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;two:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;then&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;self.one&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;three:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;then&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$.one&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Scope-checking indexed object field access
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;ObjectFieldAccess&lt;/code&gt; handler was already checking the scope reference — &lt;code&gt;self&lt;/code&gt;, &lt;code&gt;$&lt;/code&gt;, or &lt;code&gt;ObjVarRef&lt;/code&gt; — against the context. But it was ignoring the chain of field expressions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ocaml"&gt;&lt;code&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;ObjectFieldAccess&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
  &lt;span class="n"&gt;validate_object_field_access&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chain (e.g. &lt;code&gt;[self.one]&lt;/code&gt; in &lt;code&gt;obj[self.one].two&lt;/code&gt;) was not being validated. A chain expression containing &lt;code&gt;self&lt;/code&gt; or &lt;code&gt;$&lt;/code&gt; outside an object went undetected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/errors/object_field_access_index_self_out_of_scope.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;obj&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;one:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;self.one&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;.two&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/errors/object_field_access_computed_self_out_of_scope.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;obj&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;one:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;obj.&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;self.one&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;.two&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/errors/object_field_access_index_toplevel_out_of_scope.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;obj&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;one:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;$.one&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;.two&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix threads the chain through to &lt;code&gt;validate_expression_list&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-  | ObjectFieldAccess (pos, scope, _) -&amp;gt;
-    validate_object_field_access pos scope context
&lt;/span&gt;&lt;span class="gi"&gt;+  | ObjectFieldAccess (pos, scope, chain) -&amp;gt;
+    validate_object_field_access pos scope chain context
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside &lt;code&gt;validate_object_field_access&lt;/code&gt;, after the scope check runs, the chain gets validated in sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-  match scope with
-  | Self | TopLevel -&amp;gt;
-    if not context.in_object then
-      let with_error_msg = match scope with
-        | Self -&amp;gt; Error.Msg.self_out_of_scope
-        | TopLevel -&amp;gt; Error.Msg.no_toplevel_object
-        | ObjVarRef _ -&amp;gt; "" (* unreachable *)
-      in
-      Error.error_at pos with_error_msg
-    else ok ()
-  | ObjVarRef _ -&amp;gt;
-    (* Variable references are allowed anywhere *)
-    ok ()
&lt;/span&gt;&lt;span class="gi"&gt;+  let* () =
+    match scope with
+    | Self | TopLevel -&amp;gt;
+      if not context.in_object then
+        let with_error_msg = match scope with
+          | Self -&amp;gt; Error.Msg.self_out_of_scope
+          | TopLevel -&amp;gt; Error.Msg.no_toplevel_object
+          | ObjVarRef _ -&amp;gt; "" (* unreachable *)
+        in
+        Error.error_at pos with_error_msg
+      else ok ()
+    | ObjVarRef _ -&amp;gt;
+      (* Variable references are allowed anywhere *)
+      ok ()
+  in
+  validate_expression_list chain context
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three variations now produce the correct scope errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope-checking function definitions
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;FunctionDef&lt;/code&gt; was completely unvalidated. A function defined at the top level could reference &lt;code&gt;self&lt;/code&gt; or &lt;code&gt;$&lt;/code&gt; in its body or parameter defaults without triggering any error. &lt;code&gt;validate_function_def&lt;/code&gt; walks each parameter's optional default expression, then validates the body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gi"&gt;+  | FunctionDef (_, def) -&amp;gt;
+    validate_function_def def context
&lt;/span&gt;   (* For any other expression types, no special scope validation needed *)
   | Unit | Null _ | Number _ | String _ | Bool _ | EvaluatedObject _
&lt;span class="gd"&gt;-  | RuntimeObject _ | ObjectPtr _ | FunctionDef _ | FunctionCall _ | Closure _
&lt;/span&gt;&lt;span class="gi"&gt;+  | RuntimeObject _ | ObjectPtr _ | FunctionCall _ | Closure _
&lt;/span&gt;     -&amp;gt; ok ()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ocaml"&gt;&lt;code&gt;&lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;validate_function_def&lt;/span&gt; &lt;span class="n"&gt;def&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fold_left&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_expr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;default_expr&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_validate&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;
      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;def&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;
  &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="n"&gt;_validate&lt;/span&gt; &lt;span class="n"&gt;def&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The four error samples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/errors/function_def_self_out_of_scope.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;get_value()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;self.value;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;get_value()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/errors/function_def_toplevel_out_of_scope.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;get_value()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$.value;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;get_value()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/errors/function_def_default_self_out_of_scope.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;get_value(value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;self.value)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;value;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;get_value()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/errors/function_def_default_toplevel_out_of_scope.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;get_value(value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$.value)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;value;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;get_value()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All four now produce the correct scope errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope-checking function calls
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;FunctionCall&lt;/code&gt; was the next silent passer. A call like &lt;code&gt;get_value(self.value)&lt;/code&gt; at the top level should fail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/errors/function_call_arg_self_out_of_scope.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;get_value(value)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;value;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;get_value(self.value)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;validate_function_call&lt;/code&gt; validates the callee expression and every argument, positional or named:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gi"&gt;+  | FunctionCall (_, call) -&amp;gt;
+    validate_function_call call context
&lt;/span&gt;   (* Terminal variants *)
   | Null _ | Number _ | String _ | Bool _ -&amp;gt; ok ()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ocaml"&gt;&lt;code&gt;&lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;validate_function_call&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_validate&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;callee&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fold_left&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Positional&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_validate&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;
      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Named&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_validate&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six error samples cover callee, positional args, and named args for both &lt;code&gt;self&lt;/code&gt; and &lt;code&gt;$&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/errors/function_call_callee_self_out_of_scope.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;(self.get_value)()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/errors/function_call_callee_toplevel_out_of_scope.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;($.get_value)()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/errors/function_call_arg_self_out_of_scope.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;get_value(value)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;value;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;get_value(self.value)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/errors/function_call_arg_toplevel_out_of_scope.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;get_value(value)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;value;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;get_value($.value)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/errors/function_call_named_arg_self_out_of_scope.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;get_value(value)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;value;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;get_value(value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;self.value)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/errors/function_call_named_arg_toplevel_out_of_scope.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;get_value(value)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;value;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;get_value(value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$.value)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Scope-checking closures (unified with function definitions)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Closure&lt;/code&gt; was the last variant sitting in the catch-all. It has the same structure as &lt;code&gt;FunctionDef&lt;/code&gt; -- both have params with optional defaults and a body. Instead of duplicating the validation, I unified them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-  | FunctionDef (_, def) -&amp;gt;
-    validate_function_def def context
&lt;/span&gt;&lt;span class="gi"&gt;+  | FunctionDef (_, {name = _; params; body})
+  | Closure (_, {params; body}) -&amp;gt;
+    validate_function_signature_and_body params body context
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The old &lt;code&gt;validate_function_def&lt;/code&gt; renamed to &lt;code&gt;validate_function_signature_and_body&lt;/code&gt; and takes &lt;code&gt;params&lt;/code&gt; and &lt;code&gt;body&lt;/code&gt; directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-and validate_function_def def context =
&lt;/span&gt;&lt;span class="gi"&gt;+and validate_function_signature_and_body params body context =
&lt;/span&gt;   let* () = List.fold_left
     (fun acc (_, default_expr) -&amp;gt;
       acc &amp;gt;&amp;gt;= fun () -&amp;gt;
       match default_expr with
       | Some expr -&amp;gt; _validate expr context
       | None -&amp;gt; ok ()
     )
     (ok ())
&lt;span class="gd"&gt;-    def.params
&lt;/span&gt;&lt;span class="gi"&gt;+    params
&lt;/span&gt;   in
&lt;span class="gd"&gt;-  _validate def.body context
&lt;/span&gt;&lt;span class="gi"&gt;+  _validate body context
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four error samples cover body and defaults for both &lt;code&gt;self&lt;/code&gt; and &lt;code&gt;$&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/errors/closure_self_out_of_scope.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;function()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;self.value&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/errors/closure_toplevel_out_of_scope.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;function()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$.value&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/errors/closure_default_self_out_of_scope.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;function(value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;self.value)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;value&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/errors/closure_default_toplevel_out_of_scope.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;function(value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$.value)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;value&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cleaning up the non-parser variants
&lt;/h2&gt;

&lt;p&gt;The last diff separates the remaining no-ops into explicit groups with comments explaining why each group doesn't need validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-  (* For any other expression types, no special scope validation needed *)
-  | Unit | Null _ | Number _ | String _ | Bool _ | EvaluatedObject _
-  | RuntimeObject _ | ObjectPtr _ | FunctionCall _ | Closure _
-    -&amp;gt; ok ()
&lt;/span&gt;&lt;span class="gi"&gt;+  (* Terminal variants *)
+  | Null _ | Number _ | String _ | Bool _ -&amp;gt; ok ()
+  (* These variants are not produced by parsing source files.
+     Scope validation runs before type checking/interpreting,
+     so they only appear through internal or runtime, and have
+     no source-level scope to check. *)
+  | Unit | EvaluatedObject _ | RuntimeObject _ | ObjectPtr _ -&amp;gt; ok ()
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three groups: terminal literals that have no sub-expressions, internal/runtime-only variants that parsing never produces, and — after the closure unification — nothing left. The last catch-all is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;The cram tests cover both the positive case and every error case. The &lt;code&gt;conditionals.t&lt;/code&gt; test confirms that &lt;code&gt;self&lt;/code&gt; and &lt;code&gt;$&lt;/code&gt; inside a conditional within an object still works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/test/cram/conditionals.t b/test/cram/conditionals.t
&lt;/span&gt;&lt;span class="p"&gt;@@ -18,3 +18,7 @@&lt;/span&gt;
   2:   [if true then 42]: "value"
      ^^^^^^^^^^^^^^^^^^^^^
   [1]
&lt;span class="gi"&gt;+
+
+  $ tsonnet ../../samples/conditionals/conditional_object_scope.jsonnet
+  { "one": 1, "three": 3, "two": 2 }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;errors.t&lt;/code&gt; file collects all the new scope error tests — conditionals, object field access, function definitions, function calls, and closures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/test/cram/errors.t b/test/cram/errors.t
&lt;/span&gt;&lt;span class="gi"&gt;+  $ tsonnet ../../samples/errors/conditional_self_out_of_scope.jsonnet
+  ERROR: ...:1:27 Can't use self outside of an object
+
+  $ tsonnet ../../samples/errors/conditional_toplevel_out_of_scope.jsonnet
+  ERROR: ...:1:35 No top-level object found
+
+  $ tsonnet ../../samples/errors/object_field_access_index_self_out_of_scope.jsonnet
+  ERROR: ...:2:4 Can't use self outside of an object
+
+  $ tsonnet ../../samples/errors/object_field_access_computed_self_out_of_scope.jsonnet
+  ERROR: ...:2:5 Can't use self outside of an object
+
+  $ tsonnet ../../samples/errors/object_field_access_index_toplevel_out_of_scope.jsonnet
+  ERROR: ...:2:4 No top-level object found
+
+  $ tsonnet ../../samples/errors/function_def_self_out_of_scope.jsonnet
+  ERROR: ...:1:20 Can't use self outside of an object
+
+  $ tsonnet ../../samples/errors/function_def_toplevel_out_of_scope.jsonnet
+  ERROR: ...:1:20 No top-level object found
+
+  $ tsonnet ../../samples/errors/function_def_default_self_out_of_scope.jsonnet
+  ERROR: ...:1:24 Can't use self outside of an object
+
+  $ tsonnet ../../samples/errors/function_def_default_toplevel_out_of_scope.jsonnet
+  ERROR: ...:1:24 No top-level object found
+
+  $ tsonnet ../../samples/errors/function_call_callee_self_out_of_scope.jsonnet
+  ERROR: ...:1:1 Can't use self outside of an object
+
+  $ tsonnet ../../samples/errors/function_call_callee_toplevel_out_of_scope.jsonnet
+  ERROR: ...:1:1 No top-level object found
+
+  $ tsonnet ../../samples/errors/function_call_arg_self_out_of_scope.jsonnet
+  ERROR: ...:2:10 Can't use self outside of an object
+
+  $ tsonnet ../../samples/errors/function_call_arg_toplevel_out_of_scope.jsonnet
+  ERROR: ...:2:10 No top-level object found
+
+  $ tsonnet ../../samples/errors/function_call_named_arg_self_out_of_scope.jsonnet
+  ERROR: ...:2:18 Can't use self outside of an object
+
+  $ tsonnet ../../samples/errors/function_call_named_arg_toplevel_out_of_scope.jsonnet
+  ERROR: ...:2:18 No top-level object found
+
+  $ tsonnet ../../samples/errors/closure_self_out_of_scope.jsonnet
+  ERROR: ...:1:11 Can't use self outside of an object
+
+  $ tsonnet ../../samples/errors/closure_toplevel_out_of_scope.jsonnet
+  ERROR: ...:1:11 No top-level object found
+
+  $ tsonnet ../../samples/errors/closure_default_self_out_of_scope.jsonnet
+  ERROR: ...:1:17 Can't use self outside of an object
+
+  $ tsonnet ../../samples/errors/closure_default_toplevel_out_of_scope.jsonnet
+  ERROR: ...:1:17 No top-level object found
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All 19 new tests pass, and every existing cram test still passes unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The scope checker is clean. Every &lt;code&gt;expr&lt;/code&gt; variant that can appear during scope analysis now has explicit handling.&lt;/p&gt;

&lt;p&gt;The type checker is next. It has at least as many loose ends as the scope checker did — probably more. Future me gets to sort them out.&lt;/p&gt;

&lt;p&gt;The entire diff can be seen &lt;a href="https://gitlab.com/-/snippets/6009202" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Thanks for reading Bit Maybe Wise! &lt;a href="https://bitmaybewise.com/" rel="noopener noreferrer"&gt;Subscribe&lt;/a&gt; — and if you don't, the only thing out of scope is your inbox.&lt;/p&gt;

&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@ansleycreative" rel="noopener noreferrer"&gt;Matthew Ansley&lt;/a&gt; on &lt;a href="https://unsplash.com" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tsonnet</category>
      <category>jsonnet</category>
      <category>compiler</category>
    </item>
    <item>
      <title>Tsonnet #44 - The devil in the details #0</title>
      <dc:creator>Hercules Lemke Merscher</dc:creator>
      <pubDate>Sat, 18 Jul 2026 14:50:12 +0000</pubDate>
      <link>https://dev.to/bitmaybewise/tsonnet-44-the-devil-in-the-details-0-4nm0</link>
      <guid>https://dev.to/bitmaybewise/tsonnet-44-the-devil-in-the-details-0-4nm0</guid>
      <description>&lt;p&gt;Welcome to the &lt;a href="https://gitlab.com/bitmaybewise/tsonnet" rel="noopener noreferrer"&gt;Tsonnet&lt;/a&gt; series!&lt;/p&gt;

&lt;p&gt;If you're not following along, check out how it all started in &lt;a href="https://dev.to/bitmaybewise/tsonnet-0-from-zero-to-interpreter-54na"&gt;the first post of the series&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the previous post, we got computed field names working:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bitmaybewise/tsonnet-43-object-fields-now-with-an-opt-out-clause-3j0f" class="crayons-story__hidden-navigation-link"&gt;Tsonnet #43 - Object fields, now with an opt-out clause&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/bitmaybewise" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" alt="bitmaybewise profile" class="crayons-avatar__image" width="279" height="279"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/bitmaybewise" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Hercules Lemke Merscher
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Hercules Lemke Merscher
                
              
              &lt;div id="story-author-preview-content-3801552" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/bitmaybewise" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" class="crayons-avatar__image" alt="" width="279" height="279"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Hercules Lemke Merscher&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/bitmaybewise/tsonnet-43-object-fields-now-with-an-opt-out-clause-3j0f" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 2&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/bitmaybewise/tsonnet-43-object-fields-now-with-an-opt-out-clause-3j0f" id="article-link-3801552"&gt;
          Tsonnet #43 - Object fields, now with an opt-out clause
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tsonnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tsonnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/jsonnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;jsonnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/compiler"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;compiler&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/bitmaybewise/tsonnet-43-object-fields-now-with-an-opt-out-clause-3j0f#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            12 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;Turns out the catch-all patterns I'd been relying on were defeating the one thing that makes pattern-matching useful.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwxocdl9k0k3fybvqhbe0.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwxocdl9k0k3fybvqhbe0.jpeg" alt="plan" width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Compilers are amazing co-pilots when you're working with strongly typed languages. They catch invariants when we forget. But that only works if &lt;strong&gt;we&lt;/strong&gt; enforce checking all pattern-matching cases, one by one. A catch-all &lt;code&gt;| _ -&amp;gt;&lt;/code&gt; or &lt;code&gt;| expr -&amp;gt;&lt;/code&gt; is the compiler equivalent of duct tape — it shuts it up, but it also hides every variant you forgot to handle.&lt;/p&gt;

&lt;p&gt;So far, I'd been mistakenly ignoring new type variants in pattern-matching across &lt;code&gt;lib/scope.ml&lt;/code&gt;, &lt;code&gt;lib/type.ml&lt;/code&gt;, and &lt;code&gt;lib/interpreter.ml&lt;/code&gt; because of catch-all cases. Every time a new &lt;code&gt;expr&lt;/code&gt; variant was added, the compiler stayed quiet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: explicit exhaustiveness
&lt;/h2&gt;

&lt;p&gt;The first thing to do is remove the catch-all cases and list every variant explicitly. This way, when a new variant gets added to the AST, OCaml will refuse to compile until every match is updated.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;lib/scope.ml&lt;/code&gt; — &lt;code&gt;_validate&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/scope.ml b/lib/scope.ml
index 3f329fe..48dfa83 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/scope.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/scope.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -31,7 +31,6 @@&lt;/span&gt; let add_locals_to_context locals context = {
&lt;span class="err"&gt;
&lt;/span&gt; let rec _validate expr context =
   match expr with
&lt;span class="gd"&gt;-  | Unit | Null _ | Number _ | String _ | Bool _ -&amp;gt; ok ()
&lt;/span&gt;   | Ident (pos, varname) -&amp;gt;
     (* Identifier validation - the heart of scope checking *)
     validate_ident pos varname context
&lt;span class="p"&gt;@@ -52,9 +51,10 @@&lt;/span&gt; let rec _validate expr context =
     _validate expr context
   | IndexedExpr (_, _, index_expr) -&amp;gt;
     _validate index_expr context
&lt;span class="gd"&gt;-  | _ -&amp;gt;
-    (* For any other expression types, no special scope validation needed *)
-    ok ()
&lt;/span&gt;&lt;span class="gi"&gt;+  (* For any other expression types, no special scope validation needed *)
+  | Unit | Null _ | Number _ | String _ | Bool _ | EvaluatedObject _
+  | RuntimeObject _ | ObjectPtr _ | FunctionDef _ | FunctionCall _ | Closure _
+  | If _ -&amp;gt; ok ()
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;lib/type.ml&lt;/code&gt; — three matches
&lt;/h3&gt;

&lt;p&gt;The type checker had the most catch-all cases. Three separate matches needed the treatment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/type.ml b/lib/type.ml
index 79fa6aa..28c8909 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/type.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/type.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -100,7 +100,6 @@&lt;/span&gt; let rec to_string = function
   | Tunresolved -&amp;gt; "&amp;lt;unresolved&amp;gt;"
&lt;span class="err"&gt;
&lt;/span&gt; let rec collect_free_idents = function
&lt;span class="gd"&gt;-  | Unit | Null _ | Number _ | String _ | Bool _ -&amp;gt; []
&lt;/span&gt;   | Ident (_, name) -&amp;gt; [name]
   | Array (_, exprs) -&amp;gt; List.concat_map collect_free_idents exprs
   | BinOp (_, _, e1, e2) -&amp;gt; collect_free_idents e1 @ collect_free_idents e2
&lt;span class="p"&gt;@@ -126,7 +125,9 @@&lt;/span&gt; let rec collect_free_idents = function
       | Named (_, e) -&amp;gt; collect_free_idents e
     ) call.args
   | Closure (_, closure) -&amp;gt; collect_free_idents closure.body
&lt;span class="gd"&gt;-  | _ -&amp;gt; []
&lt;/span&gt;&lt;span class="gi"&gt;+  | Unit | Null _ | Number _ | String _ | Bool _ | EvaluatedObject _
+  | RuntimeObject _ | ObjectPtr _ | FunctionDef _
+  | If _ -&amp;gt; []
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;@@ -154,7 +155,6 @@&lt;/span&gt; let rec check_cyclic_refs venv varname seen pos =
     | _ -&amp;gt; ok ()
 and check_expr_for_cycles venv expr seen =
   match expr with
&lt;span class="gd"&gt;-  | Unit | Null _ | Number _ | String _ | Bool _ -&amp;gt; ok ()
&lt;/span&gt;   | Array (_, exprs) -&amp;gt; iter_for_cycles venv seen exprs
   | ParsedObject (_, entries) -&amp;gt; check_object_for_cycles venv entries seen
   | ObjectFieldAccess (pos, scope, exprs) -&amp;gt; check_object_field_chain_for_cycles venv (pos, scope, exprs) seen
&lt;span class="p"&gt;@@ -163,7 +163,10 @@&lt;/span&gt; and check_expr_for_cycles venv expr seen =
   | UnaryOp (_, _, e) -&amp;gt; check_expr_for_cycles venv e seen
   | Seq exprs -&amp;gt; iter_for_cycles venv seen exprs
   | If (_, cond_expr, then_expr, else_expr_opt) -&amp;gt; check_conditional_for_cycles venv (cond_expr, then_expr, else_expr_opt) seen
&lt;span class="gd"&gt;-  | _ -&amp;gt; ok ()
&lt;/span&gt;&lt;span class="gi"&gt;+  | Unit | Null _ | Number _ | String _ | Bool _ | EvaluatedObject _
+  | RuntimeObject _ | ObjectPtr _ | FunctionDef _ | FunctionCall _ | Closure _
+  | Local _ | IndexedExpr _ -&amp;gt; ok ()
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;@@ -235,7 +238,7 @@&lt;/span&gt; let rec translate venv expr =
   | Closure (pos, closure) -&amp;gt; translate_closure venv (pos, closure)
   | If (pos, cond_expr, then_expr, else_expr_opt) -&amp;gt;
     translate_conditional venv (pos, cond_expr, then_expr, else_expr_opt)
&lt;span class="gd"&gt;-  | expr' -&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+  | EvaluatedObject _ | RuntimeObject _ | ObjectPtr _ as expr' -&amp;gt;
&lt;/span&gt;     error (Error.Msg.type_invalid_expr (string_of_type expr'))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;lib/interpreter.ml&lt;/code&gt; — &lt;code&gt;deep_eval&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/interpreter.ml b/lib/interpreter.ml
index 40b9eea..1751554 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/interpreter.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/interpreter.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -562,7 +562,9 @@&lt;/span&gt; let rec deep_eval expr =
   | RuntimeObject _ -&amp;gt;
     let* (_, evaluated) = interpret Env.empty expr in
     deep_eval evaluated
&lt;span class="gd"&gt;-  | expr -&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+  | Ident _ | ParsedObject _ | ObjectPtr _ | ObjectFieldAccess _
+  | BinOp _ | UnaryOp _ | IndexedExpr _ | Local _ | Seq _
+  | FunctionDef _ | FunctionCall _ | Closure _ | If _ as expr -&amp;gt;
&lt;/span&gt;     let* (_, evaluated) = interpret Env.empty expr in
     deep_eval evaluated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every catch-all is gone. Now when I add a new variant to the &lt;code&gt;expr&lt;/code&gt; type, the compiler will tell me exactly which function needs a new arm — silently swallowing it is no longer an option.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does it change anything? (No)
&lt;/h2&gt;

&lt;p&gt;This improves nothing in the runtime behaviour. Every explicit list above matches exactly what the catch-all was doing. But now the compiler can do its job — if I forget to update a match when adding a new variant, the build breaks.&lt;/p&gt;

&lt;p&gt;The existing cram tests pass without changes. No output changed — as expected, since the semantics are identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;This post was just the tip of the iceberg — removing the catch-all exposed the loose ends, but now I actually need to deal with them. In the next one, I'll start with the scope check, where there are variants in &lt;code&gt;_validate&lt;/code&gt; that are silently passing through when they probably shouldn't. But first, I needed to see them.&lt;/p&gt;

&lt;p&gt;The diff is small — the habit it breaks is the point.&lt;/p&gt;

&lt;p&gt;The entire diff can be seen &lt;a href="https://gitlab.com/-/snippets/6009056" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Thanks for reading Bit Maybe Wise! Your inbox deserves exhaustive coverage — &lt;a href="https://bitmaybewise.com/" rel="noopener noreferrer"&gt;subscribe&lt;/a&gt; and never miss a variant.&lt;/p&gt;

&lt;p&gt;Photo by&amp;nbsp;&lt;a href="https://unsplash.com/@lunarts" rel="noopener noreferrer"&gt;Volodymyr Hryshchenko&lt;/a&gt;&amp;nbsp;on&amp;nbsp;&lt;a href="https://unsplash.com/" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>jsonnet</category>
      <category>tsonnet</category>
      <category>compiler</category>
      <category>ocaml</category>
    </item>
    <item>
      <title>ABEND dump #27</title>
      <dc:creator>Hercules Lemke Merscher</dc:creator>
      <pubDate>Thu, 02 Jul 2026 21:31:59 +0000</pubDate>
      <link>https://dev.to/bitmaybewise/abend-dump-27-3p6b</link>
      <guid>https://dev.to/bitmaybewise/abend-dump-27-3p6b</guid>
      <description>&lt;p&gt;Welcome to the ABEND dump #27!&lt;/p&gt;

&lt;p&gt;Don’t know what is an “ABEND dump”?!&amp;nbsp;&lt;a href="https://bitmaybewise.substack.com/i/68092102/what-is-abend-dump" rel="noopener noreferrer"&gt;I’m glad you asked&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can check the previous ABEND dump here: &lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bitmaybewise/abend-dump-26-47eh" class="crayons-story__hidden-navigation-link"&gt;ABEND dump #26&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/bitmaybewise" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" alt="bitmaybewise profile" class="crayons-avatar__image" width="279" height="279"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/bitmaybewise" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Hercules Lemke Merscher
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Hercules Lemke Merscher
                
              
              &lt;div id="story-author-preview-content-3473154" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/bitmaybewise" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" class="crayons-avatar__image" alt="" width="279" height="279"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Hercules Lemke Merscher&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/bitmaybewise/abend-dump-26-47eh" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Apr 8&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/bitmaybewise/abend-dump-26-47eh" id="article-link-3473154"&gt;
          ABEND dump #26
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/abenddump"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;abenddump&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/bitmaybewise/abend-dump-26-47eh#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            2 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;I haven't been reading that much lately, unfortunately. Or, I did read, but if I don't remember, it's not worth mentioning.&lt;/p&gt;

&lt;p&gt;So, here goes just a tiny bit of content that stood out for the past 3 months.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;a href="https://youtu.be/K-Xv8D8NjTk" rel="noopener noreferrer"&gt;TypeScript, C# and Turbo Pascal with Anders Hejlsberg&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Here's an interview with Anders Hejlsberg. This guy is a legend! Always inspiring to watch his talks and interviews.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/K-Xv8D8NjTk"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://open.substack.com/pub/tidyfirst/p/trust-factory?r=915pz&amp;amp;utm_campaign=post-expanded-share&amp;amp;utm_medium=web" rel="noopener noreferrer"&gt;Trust Factory&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;As always, Kent Beck makes me think, and yes, I agree with his statement here.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://blog.playstation.com/2026/07/01/physical-disc-production-ending-in-january-2028-for-new-games-releasing-on-playstation-consoles/" rel="noopener noreferrer"&gt;Physical disc production ending in January 2028 for new games releasing on PlayStation consoles&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;After Sony announced they aren't going to release physical discs for games, &lt;a href="https://www.eurogamer.net/xbox-project-helix-no-disc-drive-reports" rel="noopener noreferrer"&gt;Xbox followed&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It's a sad time for games preservation. I wish there was at least a way to keep them as physical copies, to play offline in the future, but I doubt most studios would offer this option.&lt;/p&gt;

&lt;p&gt;I don't like this direction where everything is turning into a subscription.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://x.com/cory/status/2046085866133614720?s=20" rel="noopener noreferrer"&gt;MIT students turned a building into a giant playable game of Tetris&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;This was sitting here for a while, and just now I managed to find it to share. &lt;/p&gt;

&lt;p&gt;How cool is that?! :D&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcvvvholbi34yycxyk6yp.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcvvvholbi34yycxyk6yp.jpg" alt="MIT tetris in the building" width="800" height="1342"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Thanks for reading the ABEND dump! No discs, no problem -- as long as MIT keeps turning buildings into Tetris boards, the future's still fun. &lt;a href="https://bitmaybewise.substack.com" rel="noopener noreferrer"&gt;Subscribe&lt;/a&gt; and let's see what they turn into a giant Pac-Man next.&lt;/p&gt;

</description>
      <category>abenddump</category>
    </item>
    <item>
      <title>Tsonnet #43 - Object fields, now with an opt-out clause</title>
      <dc:creator>Hercules Lemke Merscher</dc:creator>
      <pubDate>Tue, 02 Jun 2026 08:55:10 +0000</pubDate>
      <link>https://dev.to/bitmaybewise/tsonnet-43-object-fields-now-with-an-opt-out-clause-3j0f</link>
      <guid>https://dev.to/bitmaybewise/tsonnet-43-object-fields-now-with-an-opt-out-clause-3j0f</guid>
      <description>&lt;p&gt;Welcome to the &lt;a href="https://gitlab.com/bitmaybewise/tsonnet" rel="noopener noreferrer"&gt;Tsonnet&lt;/a&gt; series!&lt;/p&gt;

&lt;p&gt;If you're not following along, check out how it all started in &lt;a href="https://dev.to/bitmaybewise/tsonnet-0-from-zero-to-interpreter-54na"&gt;the first post of the series&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the previous post, we got conditionals working:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bitmaybewise/tsonnet-42-if-then-else-finally-led" class="crayons-story__hidden-navigation-link"&gt;Tsonnet #42 - If then else (finally)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/bitmaybewise" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" alt="bitmaybewise profile" class="crayons-avatar__image" width="279" height="279"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/bitmaybewise" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Hercules Lemke Merscher
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Hercules Lemke Merscher
                
              
              &lt;div id="story-author-preview-content-3716145" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/bitmaybewise" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" class="crayons-avatar__image" alt="" width="279" height="279"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Hercules Lemke Merscher&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/bitmaybewise/tsonnet-42-if-then-else-finally-led" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;May 21&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/bitmaybewise/tsonnet-42-if-then-else-finally-led" id="article-link-3716145"&gt;
          Tsonnet #42 - If then else (finally)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tsonnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tsonnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/jsonnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;jsonnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/compiler"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;compiler&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/bitmaybewise/tsonnet-42-if-then-else-finally-led#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            7 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;Turns out conditionals had one more trick to pull: deciding whether a field exists at all.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh0jjdula24vf1q5zaowt.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh0jjdula24vf1q5zaowt.jpeg" alt="fork path" width="800" height="536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The target
&lt;/h2&gt;

&lt;p&gt;I need to cover the &lt;a href="https://jsonnet.org/learning/tutorial.html#computed_field_names" rel="noopener noreferrer"&gt;Computed Field Names&lt;/a&gt; tutorial as the next step for Tsonnet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/tutorials/computed-fields.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Margarita(salted)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;ingredients:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;kind:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Tequila&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Blanco'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;qty:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;kind:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Lime'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;qty:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;kind:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Cointreau'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;qty:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;salted&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;then&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'garnish'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Salt'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;Margarita:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Margarita(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;'Margarita&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Unsalted':&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Margarita(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's target only the part we are interested in, adding new attributes to &lt;code&gt;samples/conditionals/conditionals.jsonnet&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/conditionals/conditionals.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; 

    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;then&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'conditional_attribute_then'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;then&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'...'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;then&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'...'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'conditional_attribute_else'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's one caveat though:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;dune &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; tsonnet samples/conditionals/conditionals.jsonnet
ERROR: samples/conditionals/conditionals.jsonnet:17:5 Parsing error. Invalid syntax:

17:     &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s1"&gt;'conditional_attribute_then'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;: &lt;span class="nb"&gt;true&lt;/span&gt;,
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

&lt;span class="nv"&gt;$ &lt;/span&gt;dune &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; tsonnet samples/tutorials/computed-fields.jsonnet
ERROR: samples/tutorials/computed-fields.jsonnet:7:3 Parsing error. Invalid syntax:

7:   &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;if &lt;/span&gt;salted &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s1"&gt;'garnish'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;: &lt;span class="s1"&gt;'Salt'&lt;/span&gt;,
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no object's conditional attribute support yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's add new testing samples
&lt;/h2&gt;

&lt;p&gt;We need to cover a few cases other than the three conditional attributes shown above.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Non string attributes:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/conditionals/conditional_attr_not_string.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;then&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"value"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cyclic references in conditional fields:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/semantics/invalid_conditional_field_cyclic_key.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;a:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;self.b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;b:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;self.a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;then&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;self.a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ignored cyclic reference field:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/semantics/valid_conditional_field_access_cyclic.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;obj&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;a:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;then&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;self.c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;c:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;self.b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;obj.a&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A new object entry variant
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/ast.ml b/lib/ast.ml
index 35f88a6..fdb5bbe 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/ast.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/ast.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -97,6 +97,7 @@&lt;/span&gt; type expr =
&lt;span class="err"&gt;
&lt;/span&gt; and object_entry =
   | ObjectField of string * expr
&lt;span class="gi"&gt;+  | ObjectConditionalField of expr * expr
&lt;/span&gt;   | ObjectExpr of expr
 and object_scope =
   | Self
&lt;span class="p"&gt;@@ -236,6 +237,7 @@&lt;/span&gt; let rec string_of_type = function
&lt;span class="err"&gt;
&lt;/span&gt; and string_of_object_entry = function
   | ObjectField (field, expr) -&amp;gt; field ^ ": " ^ string_of_type expr
&lt;span class="gi"&gt;+  | ObjectConditionalField (field, expr) -&amp;gt; string_of_type field ^ ": " ^ string_of_type expr
&lt;/span&gt;   | ObjectExpr expr -&amp;gt; string_of_type expr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parser needs only one more rule for &lt;code&gt;obj_field&lt;/code&gt; -- pretty straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/parser.mly b/lib/parser.mly
index 40858f4..4e7775e 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/parser.mly
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/parser.mly
&lt;/span&gt;&lt;span class="p"&gt;@@ -113,6 +113,7 @@&lt;/span&gt; obj_field:
         Closure (with_pos $startpos $endpos, { params = params; body = body })
       )
     }
&lt;span class="gi"&gt;+  | LEFT_SQR_BRACKET; k = conditional; RIGHT_SQR_BRACKET; COLON; e = assignable_expr { ObjectConditionalField (k, e) }
&lt;/span&gt;   | e = single_var { ObjectExpr e }
   ;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scope validation is also straightforward. First we check the &lt;code&gt;field_expr&lt;/code&gt;, followed by the assignment &lt;code&gt;expr&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/scope.ml b/lib/scope.ml
index df55eea..3f329fe 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/scope.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/scope.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -82,6 +82,7 @@&lt;/span&gt; and validate_object_entries entries context =
       acc &amp;gt;&amp;gt;= fun _ -&amp;gt;
       match entry with
       | ObjectField (_, expr) -&amp;gt; _validate expr context
&lt;span class="gi"&gt;+      | ObjectConditionalField (field_expr, expr) -&amp;gt; _validate field_expr context &amp;gt;&amp;gt;= fun () -&amp;gt; _validate expr context
&lt;/span&gt;       | ObjectExpr expr -&amp;gt; _validate expr context
     )
     (ok ())
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Type checking
&lt;/h2&gt;

&lt;p&gt;The type checking phase is where most of the complexity for this feature lies. Let's break it down.&lt;/p&gt;

&lt;p&gt;I had to carry the &lt;code&gt;string&lt;/code&gt; with the &lt;code&gt;Tstring&lt;/code&gt; type variant to be able to do certain checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/type.ml b/lib/type.ml
index 88e5b3a..ce61163 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/type.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/type.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -9,7 +9,7 @@&lt;/span&gt; type tsonnet_type =
   | Tnull
   | Tbool
   | Tnumber
&lt;span class="gd"&gt;-  | Tstring
&lt;/span&gt;&lt;span class="gi"&gt;+  | Tstring of string
&lt;/span&gt;   | Tany
   | Tarray of tsonnet_type
   | Tobject of t_object_entry list
&lt;span class="p"&gt;@@ -47,7 +47,7 @@&lt;/span&gt; let rec to_string = function
   | Tnull -&amp;gt; "Null"
   | Tbool -&amp;gt; "Bool"
   | Tnumber -&amp;gt; "Number"
&lt;span class="gd"&gt;-  | Tstring -&amp;gt; "String"
&lt;/span&gt;&lt;span class="gi"&gt;+  | Tstring _ -&amp;gt; "String"
&lt;/span&gt;   | Tany -&amp;gt; "Any"
   | Tarray ty -&amp;gt; "Array of " ^ to_string ty
   | Tobject fields -&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;let rec translate venv expr =
&lt;/span&gt;   match expr with
   | Unit -&amp;gt; ok (venv, Tunit)
   | Null _ -&amp;gt; ok (venv, Tnull)
   | Bool _ -&amp;gt; ok (venv, Tbool)
   | Number _ -&amp;gt; ok (venv, Tnumber)
&lt;span class="gd"&gt;-  | String _ -&amp;gt; ok (venv, Tstring)
&lt;/span&gt;&lt;span class="gi"&gt;+  | String (_, s) -&amp;gt; ok (venv, Tstring s)
&lt;/span&gt;   | Ident (pos, varname) -&amp;gt; translate_ident venv pos varname
   | Array (_pos, elems) -&amp;gt; translate_array venv elems
   | ParsedObject (pos, entries) -&amp;gt; translate_object venv pos entries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This, of course, led to many changes down the line. The semantics didn't change — I only had to fix the pattern-matching. Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;@@ -466,7 +528,7 @@&lt;/span&gt; and translate_object_field_access venv pos scope chain_exprs =
       | Number (pos, _) -&amp;gt;
         (* Handle numeric indexing of strings and arrays *)
         (match prev_ty with
&lt;span class="gd"&gt;-        | Tstring -&amp;gt; ok (venv, Tstring)
&lt;/span&gt;&lt;span class="gi"&gt;+        | Tstring _ as ty -&amp;gt; ok (venv, ty)
&lt;/span&gt;         | Tarray elem_ty -&amp;gt; ok (venv, elem_ty)
         | _ -&amp;gt; Error.error_at pos (Error.Msg.type_non_indexable_type (to_string prev_ty))
         )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm intentionally dropping the other cases like that to keep this post cleaner, and more to the point. The entire thing can be seen in the complete diff shared in the conclusion.&lt;/p&gt;

&lt;p&gt;We'll need the &lt;code&gt;semantic_type_equal&lt;/code&gt; function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ocaml"&gt;&lt;code&gt;&lt;span class="c"&gt;(* Semantic equality for types. *)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;semantic_type_equal&lt;/span&gt; &lt;span class="n"&gt;ty_a&lt;/span&gt; &lt;span class="n"&gt;ty_b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;ty_a&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ty_b&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
  &lt;span class="c"&gt;(* Ignores representation details that do not affect type identity,
     such as the concrete value carried by Tstring.  *)&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Tstring&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Tstring&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;
  &lt;span class="c"&gt;(* Falls back to structural equality otherwise. *)&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ty_a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ty_b&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything other than &lt;code&gt;Tstring&lt;/code&gt; can be compared structurally. Though this will likely need to be revisited soon.&lt;/p&gt;

&lt;p&gt;When calling &lt;code&gt;translate_object&lt;/code&gt;, we must pattern-match the new variant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;@@ -359,22 +381,49 @@&lt;/span&gt; and translate_object venv pos entries =
         let* (venv', _) = translate venv expr in (ok venv')
       | ObjectField (attr, expr) -&amp;gt;
         ok (Env.add_obj_field attr (Lazy expr) obj_id venv)
&lt;span class="gi"&gt;+      | ObjectConditionalField (attr_expr, expr) -&amp;gt;
+        (match check_expr_for_cycles venv attr_expr [] with
+        | Ok () -&amp;gt; ()
+        | Error _ -&amp;gt;
+          let attr_pos = match attr_expr with If (p, _, _, _) -&amp;gt; p | _ -&amp;gt; pos in
+          Error.warn Error.Msg.type_cyclic_conditional_field_key attr_pos
+        );
+        (* It's past attr_expr, which means it has no cyclic refs.
+           Now we need to add the expr to the attr name it resolves to. *)
+        let* (_, ty) = translate venv attr_expr in
+        (match ty with
+        | Tstring attr -&amp;gt; ok (Env.add_obj_field attr (Lazy expr) obj_id venv)
+        | Tnull -&amp;gt; ok venv
+        | _ -&amp;gt;
+          let attr_pos = match attr_expr with If (p, _, _, _) -&amp;gt; p | _ -&amp;gt; pos in
+          Error.error_at attr_pos
+            (Error.Msg.invalid_conditional_field_key (to_string ty))
+        )
&lt;/span&gt;     )
     (ok venv)
     entries
   in
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since this field is a conditional, we must check the &lt;code&gt;expr&lt;/code&gt; for cycles. After that, we can translate it, and extract the field name from &lt;code&gt;Tstring&lt;/code&gt; to add the body &lt;code&gt;expr&lt;/code&gt; to the environment. Nulls are ignored, just like Jsonnet.&lt;/p&gt;

&lt;p&gt;The next step is the cyclic reference check for the attributes' body &lt;code&gt;expr&lt;/code&gt;, and conditional fields are not different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;   (* Check for cyclical references among object fields
&lt;span class="gd"&gt;-    (warn, don't error when the reference is not part of
-    the evaluation tree)
-  *)
&lt;/span&gt;&lt;span class="gi"&gt;+    (warn, don't error when the reference is not part of the evaluation tree) *)
&lt;/span&gt;   List.iter
     (fun entry -&amp;gt;
       match entry with
       | ObjectField (attr, _) -&amp;gt;
         (match check_cyclic_refs venv (Env.uniq_field_ident obj_id attr) [] pos with
         | Ok () -&amp;gt; ()
&lt;span class="gd"&gt;-        | Error _ -&amp;gt; Error.warn (Error.Msg.type_cyclic_reference (Env.uniq_field_ident obj_id attr)) pos)
&lt;/span&gt;&lt;span class="gi"&gt;+        | Error _ -&amp;gt; Error.warn (Error.Msg.type_cyclic_reference (Env.uniq_field_ident obj_id attr)) pos
+        )
+      | ObjectConditionalField (attr_expr, _) -&amp;gt;
+        (* attr_expr must be translated first to discover its name, then we can check_cyclic_refs *)
+        (match translate venv attr_expr with
+        | Ok (_, Tstring attr) -&amp;gt;
+          (match check_cyclic_refs venv (Env.uniq_field_ident obj_id attr) [] pos with
+          | Ok () -&amp;gt; ()
+          | Error _ -&amp;gt; Error.warn (Error.Msg.type_cyclic_reference (Env.uniq_field_ident obj_id attr)) pos
+          )
+        | _ -&amp;gt; ()
+        )
&lt;/span&gt;       | _ -&amp;gt; ()
     )
     entries;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the last step in &lt;code&gt;translate_object&lt;/code&gt; is to translate each field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;@@ -382,16 +431,23 @@&lt;/span&gt; and translate_object venv pos entries =
   (* Translate object fields lazily: warn on errors, skip invalid fields *)
   let entry_types = List.fold_left
     (fun entries' entry -&amp;gt;
&lt;span class="gd"&gt;-      match entry with
-      | ObjectField (attr, _) -&amp;gt;
-        (match Env.get_obj_field attr obj_id venv
-          ~succ:translate_lazy
-          ~err:(Error.error_at pos)
-        with
&lt;/span&gt;&lt;span class="gi"&gt;+      (* Resolve the field from the env to reuse memoized lazy translations
+        instead of retyping the field expression. *)
+      let get_field_type_from_env attr =
+        let obj_field = Env.get_obj_field attr obj_id venv ~succ:translate_lazy ~err:(Error.error_at pos) in
+        (match obj_field with
&lt;/span&gt;         | Ok (_, entry_ty) -&amp;gt; entries' @ [TobjectField (attr, entry_ty)]
&lt;span class="gd"&gt;-        | Error _ -&amp;gt; entries')
-      | _ -&amp;gt;
-        entries'
&lt;/span&gt;&lt;span class="gi"&gt;+        | Error _ -&amp;gt; entries'
+        )
+      in
+      match entry with
+      | ObjectField (attr, _) -&amp;gt; get_field_type_from_env attr
+      | ObjectConditionalField (attr_expr, _) -&amp;gt;
+        (match translate venv attr_expr with
+        | Ok (_, Tstring attr) -&amp;gt; get_field_type_from_env attr
+        | _ -&amp;gt; entries'
+        )
+      | _ -&amp;gt; entries'
&lt;/span&gt;     )
     []
     entries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cycle check above requires new code to deal with &lt;code&gt;ObjectConditionalField&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;@@ -100,6 +109,7 @@&lt;/span&gt; let rec collect_free_idents = function
   | ParsedObject (_, entries) -&amp;gt;
     List.concat_map (function
       | ObjectField (_, e) -&amp;gt; collect_free_idents e
&lt;span class="gi"&gt;+      | ObjectConditionalField (field, e) -&amp;gt; List.append (collect_free_idents field) (collect_free_idents e)
&lt;/span&gt;       | ObjectExpr e -&amp;gt; collect_free_idents e
     ) entries
   | ObjectFieldAccess (_, scope, exprs) -&amp;gt;
&lt;span class="p"&gt;@@ -152,6 +162,7 @@&lt;/span&gt; and check_expr_for_cycles venv expr seen =
   | BinOp (_, _, e1, e2) -&amp;gt; iter_for_cycles venv seen [e1; e2]
   | UnaryOp (_, _, e) -&amp;gt; check_expr_for_cycles venv e seen
   | Seq exprs -&amp;gt; iter_for_cycles venv seen exprs
&lt;span class="gi"&gt;+  | If (_, cond_expr, then_expr, else_expr_opt) -&amp;gt; check_conditional_for_cycles venv (cond_expr, then_expr, else_expr_opt) seen
&lt;/span&gt;   | _ -&amp;gt; ok ()
 and iter_for_cycles venv seen exprs =
   List.fold_left
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where we check each &lt;code&gt;expr&lt;/code&gt; for cycles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ocaml"&gt;&lt;code&gt;&lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;check_conditional_for_cycles&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cond_expr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;then_expr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;else_expr_opt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;check_expr_for_cycles&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt; &lt;span class="n"&gt;cond_expr&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;check_expr_for_cycles&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt; &lt;span class="n"&gt;then_expr&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;else_expr_opt&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt; &lt;span class="n"&gt;else_expr&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;check_expr_for_cycles&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt; &lt;span class="n"&gt;else_expr&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;@@ -163,6 +174,9 @@&lt;/span&gt; and check_object_for_cycles venv entries seen =
     (fun ok entry -&amp;gt; ok &amp;gt;&amp;gt;= fun _ -&amp;gt;
       match entry with
       | ObjectField (field, expr) -&amp;gt; check_expr_for_cycles venv expr (field :: seen)
&lt;span class="gi"&gt;+      | ObjectConditionalField (field, expr) -&amp;gt;
+        check_expr_for_cycles venv field seen &amp;gt;&amp;gt;= fun () -&amp;gt;
+        check_expr_for_cycles venv expr seen
&lt;/span&gt;       | ObjectExpr expr -&amp;gt; check_expr_for_cycles venv expr seen
     )
     (ok ())
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And to finish it off the type checking phase, we check if the two branch types are semantically equal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;@@ -714,13 +785,13 @@&lt;/span&gt; and translate_conditional venv (pos, cond_expr, then_expr, else_expr_opt) =
     (match else_expr_opt with
     | Some else_expr -&amp;gt;
       let* (_, else_type) = translate venv else_expr in
&lt;span class="gd"&gt;-      if then_type = else_type
&lt;/span&gt;&lt;span class="gi"&gt;+      if semantic_type_equal then_type else_type
&lt;/span&gt;       then ok (venv, then_type)
&lt;span class="gd"&gt;-      else Error.error_at pos
-        (Error.Msg.type_conditional_branches_mismatch
-          ~then_type:(to_string then_type)
-          ~else_type:(to_string else_type)
-        )
&lt;/span&gt;&lt;span class="gi"&gt;+        else Error.error_at pos
+          (Error.Msg.type_conditional_branches_mismatch
+            ~then_type:(to_string then_type)
+            ~else_type:(to_string else_type)
+          )
&lt;/span&gt;     | None -&amp;gt;
       ok (venv, then_type)
     )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And with that, we also add more error keys to the error module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/error.ml b/lib/error.ml
index b1ae567..422456a 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/error.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/error.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -14,6 +14,8 @@&lt;/span&gt; module Msg = struct
   let invalid_binary_op = "Invalid binary operation"
   let invalid_unary_op = "Invalid unary operation"
   let must_be_object = "Must be an object"
&lt;span class="gi"&gt;+  let invalid_conditional_field_key ty =
+    Printf.sprintf "Conditional field key must be String or Null, got %s" ty
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;   (* Parser messages *)
   let parse_error = "Parsing error. Invalid syntax:"
&lt;span class="p"&gt;@@ -21,6 +23,7 @@&lt;/span&gt; module Msg = struct
&lt;span class="err"&gt;
&lt;/span&gt;   (* Type checker messages *)
   let type_cyclic_reference varname = "Cyclic reference found for " ^ varname
&lt;span class="gi"&gt;+  let type_cyclic_conditional_field_key = "Cyclic reference found in conditional field key"
&lt;/span&gt;   let type_unused_variable varname = "Unused variable " ^ varname
   let type_non_indexable_value ty = ty ^ " is a non indexable value"
   let type_expected_integer_index ty = "Expected Integer index, got " ^ ty
&lt;span class="gh"&gt;diff --git a/lib/error.mli b/lib/error.mli
index 28cd496..ec9d177 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/error.mli
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/error.mli
&lt;/span&gt;&lt;span class="p"&gt;@@ -11,6 +11,7 @@&lt;/span&gt; module Msg : sig
   val invalid_binary_op : string
   val invalid_unary_op : string
   val must_be_object : string
&lt;span class="gi"&gt;+  val invalid_conditional_field_key : string -&amp;gt; string
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;   (* Parser messages *)
   val parse_error : string
&lt;span class="p"&gt;@@ -18,6 +19,7 @@&lt;/span&gt; module Msg : sig
&lt;span class="err"&gt;
&lt;/span&gt;   (* Type checker messages *)
   val type_cyclic_reference : string -&amp;gt; string
&lt;span class="gi"&gt;+  val type_cyclic_conditional_field_key : string
&lt;/span&gt;   val type_unused_variable : string -&amp;gt; string
   val type_non_indexable_value : string -&amp;gt; string
   val type_expected_integer_index : string -&amp;gt; string
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Evaluating
&lt;/h2&gt;

&lt;p&gt;The evaluation part is straightforward, after the type checker did the heavy lifting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/interpreter.ml b/lib/interpreter.ml
index 235b90e..40b9eea 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/interpreter.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/interpreter.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -137,16 +137,29 @@&lt;/span&gt; and interpret_object env (pos, entries) =
   let* (obj_env, fields) = List.fold_left
     (fun result entry -&amp;gt;
       let* (env', fields) = result in
&lt;span class="gi"&gt;+      let add_field name expr obj_id env =
+        (* Object fields are kept lazy -- they will be evaluated only when accessed.
+           This prevents infinite loops from circular references. *)
+        let env' = Env.add_obj_field name expr obj_id env in
+        ok (env', ObjectFields.add name fields)
+      in
&lt;/span&gt;       match entry with
       | ObjectExpr expr -&amp;gt;
         (* ObjectExpr holds a single local. Interpreting
           it will add the expr to the environment *)
         let* (env', _) = interpret env' expr in ok (env', fields)
       | ObjectField (name, expr) -&amp;gt;
&lt;span class="gd"&gt;-        (* Object fields are kept lazy -- they will be evaluated only when accessed.
-           This prevents infinite loops from circular references. *)
-        let env' = Env.add_obj_field name expr obj_id env' in
-        ok (env', ObjectFields.add name fields)
&lt;/span&gt;&lt;span class="gi"&gt;+        add_field name expr obj_id env'
+      | ObjectConditionalField (field_expr, expr) -&amp;gt;
+        let* (_, ident) = interpret env field_expr in
+        (match ident with
+        | Null _ -&amp;gt; ok (env', fields) (* null attribute names are ignored *)
+        | String (_, name) -&amp;gt; add_field name expr obj_id env'
+        | _ -&amp;gt;
+          let field_pos = match field_expr with If (p, _, _, _) -&amp;gt; p | _ -&amp;gt; pos in
+          Error.error_at field_pos
+            (Error.Msg.invalid_conditional_field_key (string_of_type ident))
+        )
&lt;/span&gt;     )
     (ok (obj_env, ObjectFields.empty))
     entries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Moment of truth
&lt;/h2&gt;

&lt;p&gt;Jsonnet does not allow referencing the own object in conditional fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;jsonnet samples/semantics/invalid_conditional_field_cyclic_key.jsonnet
samples/semantics/invalid_conditional_field_cyclic_key.jsonnet:4:19-23 Can&lt;span class="s1"&gt;'t use self outside of an object.

    [if true then self.a else "x"]: "value",

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tsonnet does, and will complain about the cyclic access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;dune &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; tsonnet samples/semantics/invalid_conditional_field_cyclic_key.jsonnet
ERROR: samples/semantics/invalid_conditional_field_cyclic_key.jsonnet:3:7 Cyclic reference found &lt;span class="k"&gt;for &lt;/span&gt;1-&amp;gt;a

3:     b: self.a,
   ^^^^^^^^^^^^^^
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To be honest, I'm not certain which behavior is best to keep here. I believe allowing self references makes sense in a lazy language accessing references from its own scope. I will keep betting on this behavior until I hit a wall of unmanageable complexity. Until then, better checks for us!&lt;/p&gt;

&lt;p&gt;In another scenario, Jsonnet goes on with the cyclic access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;jsonnet samples/semantics/invalid_conditional_field_cyclic_value.jsonnet
RUNTIME ERROR: max stack frames exceeded.
    samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:3:25-31    object &amp;lt;anonymous&amp;gt;
    samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:4:8-14 object &amp;lt;anonymous&amp;gt;
    samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:3:25-31    object &amp;lt;anonymous&amp;gt;
    samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:4:8-14 object &amp;lt;anonymous&amp;gt;
    samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:3:25-31    object &amp;lt;anonymous&amp;gt;
    samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:4:8-14 object &amp;lt;anonymous&amp;gt;
    samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:3:25-31    object &amp;lt;anonymous&amp;gt;
    samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:4:8-14 object &amp;lt;anonymous&amp;gt;
    samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:3:25-31    object &amp;lt;anonymous&amp;gt;
    samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:4:8-14 object &amp;lt;anonymous&amp;gt;
    ...
    samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:4:8-14 object &amp;lt;anonymous&amp;gt;
    samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:3:25-31    object &amp;lt;anonymous&amp;gt;
    samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:4:8-14 object &amp;lt;anonymous&amp;gt;
    samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:3:25-31    object &amp;lt;anonymous&amp;gt;
    samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:4:8-14 object &amp;lt;anonymous&amp;gt;
    samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:3:25-31    object &amp;lt;anonymous&amp;gt;
    samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:4:8-14 object &amp;lt;anonymous&amp;gt;
    samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:3:25-31    object &amp;lt;anonymous&amp;gt;
    Field &lt;span class="s2"&gt;"b"&lt;/span&gt;
    During manifestation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tsonnet will report an error -- plus warnings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dune &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; tsonnet samples/semantics/invalid_conditional_field_cyclic_value.jsonnet
WARNING: samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:1:0 Cyclic reference found &lt;span class="k"&gt;for &lt;/span&gt;1-&amp;gt;b

1: &lt;span class="o"&gt;{&lt;/span&gt;
   ^
2:     a: 1,
   ^^^^^^^^^
3:     &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s2"&gt;"b"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;: self.c,
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4:     c: self.b,
   ^^^^^^^^^^^^^^
&lt;span class="nt"&gt;---&lt;/span&gt;
WARNING: samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:1:0 Cyclic reference found &lt;span class="k"&gt;for &lt;/span&gt;1-&amp;gt;c

1: &lt;span class="o"&gt;{&lt;/span&gt;
   ^
2:     a: 1,
   ^^^^^^^^^
3:     &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s2"&gt;"b"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;: self.c,
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4:     c: self.b,
   ^^^^^^^^^^^^^^
&lt;span class="nt"&gt;---&lt;/span&gt;
ERROR: samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:3:29 Cyclic reference found &lt;span class="k"&gt;for &lt;/span&gt;1-&amp;gt;c

3:     &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s2"&gt;"b"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;: self.c,
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For untouched object fields with cyclic references, Jsonnet ignores them, as expected of a lazy language:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;jsonnet samples/semantics/valid_conditional_field_access_cyclic.jsonnet
1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But we can do better, and Tsonnet warns about the cyclic references, without erroring in this case because the code is semantically valid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;dune &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; tsonnet samples/semantics/valid_conditional_field_access_cyclic.jsonnet
WARNING: samples/semantics/valid_conditional_field_access_cyclic.jsonnet:1:12 Cyclic reference found &lt;span class="k"&gt;for &lt;/span&gt;1-&amp;gt;b

1: &lt;span class="nb"&gt;local &lt;/span&gt;obj &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
   ^^^^^^^^^^^^^
2:     a: 1,

3:     &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s2"&gt;"b"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;: self.c,
               ^^^^^^^^^^^^^^^^^^^
4:     c: self.b,
               ^^
&lt;span class="nt"&gt;---&lt;/span&gt;
WARNING: samples/semantics/valid_conditional_field_access_cyclic.jsonnet:1:12 Cyclic reference found &lt;span class="k"&gt;for &lt;/span&gt;1-&amp;gt;c

1: &lt;span class="nb"&gt;local &lt;/span&gt;obj &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
   ^^^^^^^^^^^^^
2:     a: 1,

3:     &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s2"&gt;"b"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;: self.c,
               ^^^^^^^^^^^^^^^^^^^
4:     c: self.b,
               ^^
&lt;span class="nt"&gt;---&lt;/span&gt;
1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the kind of behavior that passes during development, but can be tightened to treat warnings as errors before pushing code to production. Eventually, I'll add a flag to the compiler to enforce that.&lt;/p&gt;

&lt;p&gt;Here are the cram tests to capture all the content above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/test/cram/conditionals.t b/test/cram/conditionals.t
&lt;/span&gt;&lt;span class="p"&gt;new file mode 100644
&lt;/span&gt;&lt;span class="gh"&gt;index 0000000..a12933d
&lt;/span&gt;&lt;span class="gd"&gt;--- /dev/null
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/test/cram/conditionals.t
&lt;/span&gt;&lt;span class="p"&gt;@@ -0,0 +1,20 @@&lt;/span&gt;
&lt;span class="gi"&gt;+  $ tsonnet ../../samples/conditionals/conditionals.jsonnet
+  {
+    "cond_else_expr": 15,
+    "cond_else_false": "else branch",
+    "cond_else_true": "then branch",
+    "cond_nested_chain": 42,
+    "cond_nested_object": { "result": "this one" },
+    "cond_null": null,
+    "cond_true": "if true works!",
+    "conditional_attribute_else": false,
+    "conditional_attribute_then": true
+  }
+
+
+  $ tsonnet ../../samples/conditionals/conditional_attr_not_string.jsonnet
+  ERROR: ../../samples/conditionals/conditional_attr_not_string.jsonnet:2:3 Conditional field key must be String or Null, got Number
+  
+  2:   [if true then 42]: "value"
+     ^^^^^^^^^^^^^^^^^^^^^
+  [1]
&lt;/span&gt;&lt;span class="gh"&gt;diff --git a/test/cram/semantics.t b/test/cram/semantics.t
index 14f5911..67bd204 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/test/cram/semantics.t
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/test/cram/semantics.t
&lt;/span&gt;&lt;span class="p"&gt;@@ -332,3 +332,75 @@&lt;/span&gt;
      ^^^^^^^^^^^^^^^^^^^^^^^^^^
   [1]
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+
+  $ tsonnet ../../samples/semantics/invalid_conditional_field_cyclic_value.jsonnet
+  WARNING: ../../samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:1:0 Cyclic reference found for 1-&amp;gt;b
+  
+  1: {
+     ^
+  2:     a: 1,
+     ^^^^^^^^^
+  3:     [if true then "b"]: self.c,
+     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+  4:     c: self.b,
+     ^^^^^^^^^^^^^^
+  ---
+  WARNING: ../../samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:1:0 Cyclic reference found for 1-&amp;gt;c
+  
+  1: {
+     ^
+  2:     a: 1,
+     ^^^^^^^^^
+  3:     [if true then "b"]: self.c,
+     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+  4:     c: self.b,
+     ^^^^^^^^^^^^^^
+  ---
+  ERROR: ../../samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:3:29 Cyclic reference found for 1-&amp;gt;c
+  
+  3:     [if true then "b"]: self.c,
+     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+  [1]
+
+
+  $ tsonnet ../../samples/semantics/valid_conditional_field_access_cyclic.jsonnet
+  WARNING: ../../samples/semantics/valid_conditional_field_access_cyclic.jsonnet:1:12 Cyclic reference found for 1-&amp;gt;b
+  
+  1: local obj = {
+     ^^^^^^^^^^^^^
+  2:     a: 1,
+                 
+  3:     [if true then "b"]: self.c,
+                 ^^^^^^^^^^^^^^^^^^^
+  4:     c: self.b,
+                 ^^
+  ---
+  WARNING: ../../samples/semantics/valid_conditional_field_access_cyclic.jsonnet:1:12 Cyclic reference found for 1-&amp;gt;c
+  
+  1: local obj = {
+     ^^^^^^^^^^^^^
+  2:     a: 1,
+                 
+  3:     [if true then "b"]: self.c,
+                 ^^^^^^^^^^^^^^^^^^^
+  4:     c: self.b,
+                 ^^
+  ---
+  ERROR: ../../samples/semantics/valid_conditional_field_access_cyclic.jsonnet:4:7 Cyclic reference found for 1-&amp;gt;b
+  
+  4:     c: self.b,
+     ^^^^^^^^^^^^^^
+  [1]
+
+
+  $ tsonnet ../../samples/semantics/invalid_conditional_field_cyclic_key.jsonnet
+  WARNING: ../../samples/semantics/invalid_conditional_field_cyclic_key.jsonnet:4:5 Cyclic reference found in conditional field key
+  
+  4:     [if true then self.a else "x"]: "value",
+     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+  ---
+  ERROR: ../../samples/semantics/invalid_conditional_field_cyclic_key.jsonnet:3:7 Cyclic reference found for 1-&amp;gt;a
+  
+  3:     b: self.a,
+     ^^^^^^^^^^^^^^
+  [1]
&lt;/span&gt;&lt;span class="gh"&gt;diff --git a/test/cram/tutorials.t b/test/cram/tutorials.t
index b3c1bc9..0ba664c 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/test/cram/tutorials.t
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/test/cram/tutorials.t
&lt;/span&gt;&lt;span class="p"&gt;@@ -175,3 +175,22 @@&lt;/span&gt;
       "served": "Straight Up"
     }
   }
&lt;span class="gi"&gt;+
+  $ tsonnet ../../samples/tutorials/computed-fields.jsonnet
+  {
+    "Margarita": {
+      "garnish": "Salt",
+      "ingredients": [
+        { "kind": "Tequila Blanco", "qty": 2 },
+        { "kind": "Lime", "qty": 1 },
+        { "kind": "Cointreau", "qty": 1 }
+      ]
+    },
+    "Margarita Unsalted": {
+      "ingredients": [
+        { "kind": "Tequila Blanco", "qty": 2 },
+        { "kind": "Lime", "qty": 1 },
+        { "kind": "Cointreau", "qty": 1 }
+      ]
+    }
+  }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Computed field names done — and with them, the type checker took on most of the weight.&lt;/p&gt;

&lt;p&gt;I'm still betting on letting self-references slide where Jsonnet won't, trading strictness for warnings I can promote to errors later. Future me gets to decide if that was wisdom or hubris.&lt;/p&gt;

&lt;p&gt;The entire diff can be seen &lt;a href="https://gitlab.com/-/snippets/5995142" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Thanks for reading Bit Maybe Wise! &lt;a href="https://bitmaybewise.com/" rel="noopener noreferrer"&gt;Subscribe&lt;/a&gt; — and the condition for getting the next post is, conveniently, always true.&lt;/p&gt;

&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@madebyjens" rel="noopener noreferrer"&gt;Jens Lelie&lt;/a&gt; on &lt;a href="https://unsplash.com" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tsonnet</category>
      <category>jsonnet</category>
      <category>compiler</category>
    </item>
    <item>
      <title>Tsonnet #42 - If then else (finally)</title>
      <dc:creator>Hercules Lemke Merscher</dc:creator>
      <pubDate>Thu, 21 May 2026 09:39:04 +0000</pubDate>
      <link>https://dev.to/bitmaybewise/tsonnet-42-if-then-else-finally-led</link>
      <guid>https://dev.to/bitmaybewise/tsonnet-42-if-then-else-finally-led</guid>
      <description>&lt;p&gt;Welcome to the &lt;a href="https://gitlab.com/bitmaybewise/tsonnet" rel="noopener noreferrer"&gt;Tsonnet&lt;/a&gt; series!&lt;/p&gt;

&lt;p&gt;If you're not following along, check out how it all started in &lt;a href="https://dev.to/bitmaybewise/tsonnet-0-from-zero-to-interpreter-54na"&gt;the first post of the series&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the previous post, we fixed function calls with zero arguments:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bitmaybewise/tsonnet-41-call-me-maybe-but-make-it-argless-1g7p" class="crayons-story__hidden-navigation-link"&gt;Tsonnet #41 - Call me maybe, but make it argless&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/bitmaybewise" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" alt="bitmaybewise profile" class="crayons-avatar__image" width="279" height="279"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/bitmaybewise" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Hercules Lemke Merscher
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Hercules Lemke Merscher
                
              
              &lt;div id="story-author-preview-content-3676455" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/bitmaybewise" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" class="crayons-avatar__image" alt="" width="279" height="279"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Hercules Lemke Merscher&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/bitmaybewise/tsonnet-41-call-me-maybe-but-make-it-argless-1g7p" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;May 15&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/bitmaybewise/tsonnet-41-call-me-maybe-but-make-it-argless-1g7p" id="article-link-3676455"&gt;
          Tsonnet #41 - Call me maybe, but make it argless
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tsonnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tsonnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/jsonnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;jsonnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/compiler"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;compiler&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/bitmaybewise/tsonnet-41-call-me-maybe-but-make-it-argless-1g7p#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;Now conditionals are on the menu. Literally.&lt;/p&gt;

&lt;h3&gt;
  
  
  The target
&lt;/h3&gt;

&lt;p&gt;This is the &lt;a href="https://jsonnet.org/learning/tutorial.html#conditionals" rel="noopener noreferrer"&gt;Jsonnet tutorial for conditionals&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/tutorials/conditionals.jsonnet&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Mojito(virgin=&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;large=&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;next&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;fields&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ends&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;'.&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;factor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;large&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;then&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ingredients&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;split&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;into&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;arrays&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;middle&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;one&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;either&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;ingredients:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;kind:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Mint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;action:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'muddle'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;qty:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;factor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;unit:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'leaves'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;virgin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;then&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;kind:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Banks'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;qty:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;factor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;kind:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Lime'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;qty:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;factor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;kind:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Simple&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Syrup'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;qty:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;factor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;kind:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Soda'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;qty:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;factor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Returns&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;large.&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;garnish:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;large&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;then&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Lime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;wedge'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;served:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Over&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;crushed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;Mojito:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Mojito()&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;'Virgin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Mojito':&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Mojito(virgin=&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;'Large&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Mojito':&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Mojito(large=&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll finish this post having this implemented.&lt;/p&gt;

&lt;h3&gt;
  
  
  A new variant
&lt;/h3&gt;

&lt;p&gt;Let's add a new &lt;code&gt;expr&lt;/code&gt; variant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/ast.ml b/lib/ast.ml
index 1b5724b..35f88a6 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/ast.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/ast.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -93,6 +93,7 @@&lt;/span&gt; type expr =
   | FunctionDef of position * function_def
   | FunctionCall of position * function_call
   | Closure of position * closure
&lt;span class="gi"&gt;+  | If of position * expr * expr * expr option
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt; and object_entry =
   | ObjectField of string * expr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last field is an &lt;code&gt;option&lt;/code&gt; because the &lt;code&gt;else&lt;/code&gt; branch isn't always mandatory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three new keywords
&lt;/h3&gt;

&lt;p&gt;Nothing complex here. Just some precedence rules to ensure the right evaluation order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/lexer.mll b/lib/lexer.mll
index 4d65f55..3426d02 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/lexer.mll
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/lexer.mll
&lt;/span&gt;&lt;span class="p"&gt;@@ -80,6 +80,9 @@&lt;/span&gt; rule read =
   | "self" { SELF }
   | "$" { TOP_LEVEL_OBJ }
   | "in" { IN }
&lt;span class="gi"&gt;+  | "if" { IF }
+  | "then" { THEN }
+  | "else" { ELSE }
&lt;/span&gt;   | "function" { FUNCTION }
   | id { ID (Lexing.lexeme lexbuf) }
   | _ { raise (SyntaxError ("Unexpected char: " ^ Lexing.lexeme lexbuf)) }
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gh"&gt;diff --git a/lib/parser.mly b/lib/parser.mly
index 781ec5d..40858f4 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/parser.mly
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/parser.mly
&lt;/span&gt;&lt;span class="p"&gt;@@ -21,6 +21,9 @@&lt;/span&gt;
 %token COLON
 %token DOT
 %token SELF TOP_LEVEL_OBJ
&lt;span class="gi"&gt;+%token IF THEN ELSE
+%nonassoc THEN
+%nonassoc ELSE
&lt;/span&gt; %token PLUS MINUS MULTIPLY DIVIDE MODULO
 %nonassoc FUNCTION
 %left PLUS MINUS
&lt;span class="p"&gt;@@ -61,6 +64,7 @@&lt;/span&gt; assignable_expr:
   | op = unary_op; e = assignable_expr { UnaryOp (with_pos $startpos $endpos, op, e) }
   | e = indexed_expr { e }
   | e = obj_field_access { e }
&lt;span class="gi"&gt;+  | e = conditional { e }
&lt;/span&gt;   | e = funcall { e }
   | e = closure { e }
   ;
&lt;span class="p"&gt;@@ -241,4 +245,17 @@&lt;/span&gt; closure:
     }
     (* precedence here will transform "function(x) x * x" into "function(x) (x * x)" *)
     %prec FUNCTION
&lt;span class="gd"&gt;-  ;
&lt;/span&gt;&lt;span class="gi"&gt;+  ;
+
+conditional:
+  (* precedence here parses "if a then b + c" as "if a then (b + c)" *)
+  | IF; cond_expr = assignable_expr;
+    THEN; then_expr = assignable_expr;
+    ELSE; else_expr = assignable_expr
+    { If (with_pos $startpos $endpos, cond_expr, then_expr, Some else_expr) }
+    %prec ELSE
+  | IF; cond_expr = assignable_expr;
+    THEN; then_expr = assignable_expr
+    { If (with_pos $startpos $endpos, cond_expr, then_expr, None) }
+    %prec THEN
+  ;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Branch discipline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/type.ml b/lib/type.ml
index c537187..88e5b3a 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/type.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/type.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -211,6 +211,8 @@&lt;/span&gt; let rec translate venv expr =
   | FunctionDef (pos, def) -&amp;gt; translate_function_def venv (pos, def)
   | FunctionCall (pos, call) -&amp;gt; translate_function_call venv (pos, call)
   | Closure (pos, closure) -&amp;gt; translate_closure venv (pos, closure)
&lt;span class="gi"&gt;+  | If (pos, cond_expr, then_expr, else_expr_opt) -&amp;gt;
+      translate_conditional venv (pos, cond_expr, then_expr, else_expr_opt)
&lt;/span&gt;   | expr' -&amp;gt;
     error (Error.Msg.type_invalid_expr (string_of_type expr'))
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;@@ -704,6 +706,30 @@&lt;/span&gt; and translate_closure_call venv (pos, def_params, body, call_args) =
     let* (_, body_type) = translate body_venv body in
     ok (venv, body_type)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;translate_conditional&lt;/code&gt; type checks the condition and both branches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ocaml"&gt;&lt;code&gt;&lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;translate_conditional&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cond_expr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;then_expr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;else_expr_opt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cond_ty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;translate&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt; &lt;span class="n"&gt;cond_expr&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;cond_ty&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Tbool&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;then_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;translate&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt; &lt;span class="n"&gt;then_expr&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;else_expr_opt&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt; &lt;span class="n"&gt;else_expr&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;else_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;translate&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt; &lt;span class="n"&gt;else_expr&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;then_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;else_type&lt;/span&gt;
      &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;venv&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;then_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_at&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type_conditional_branches_mismatch&lt;/span&gt;
          &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;then_type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_string&lt;/span&gt; &lt;span class="n"&gt;then_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;else_type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_string&lt;/span&gt; &lt;span class="n"&gt;else_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;venv&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;then_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_at&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type_mismatch&lt;/span&gt;
      &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string_of_type&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
      &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string_of_type&lt;/span&gt; &lt;span class="n"&gt;cond_expr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the &lt;code&gt;else&lt;/code&gt; branch is absent, the &lt;code&gt;then&lt;/code&gt; branch is the return type of the entire expression. When both are present, they must match — otherwise we raise a type mismatch error.&lt;/p&gt;

&lt;p&gt;This is a deliberate design choice. TypeScript would return a union type like &lt;code&gt;string | number&lt;/code&gt;, which is flexible and plays well with dynamic languages, but it adds complexity. Most statically typed languages require both branches to be the same type when returning an expression — setting aside languages that treat conditional branches as statements, those are a different story.&lt;/p&gt;

&lt;p&gt;And the new error message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/error.ml b/lib/error.ml
index cefc8d1..b1ae567 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/error.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/error.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -30,6 +30,10 @@&lt;/span&gt; module Msg = struct
   let type_invalid_lookup_key expr = "Invalid object lookup key: " ^ expr
   let type_mismatch ~expected ~got =
     Printf.sprintf "Expected type %s, got %s" expected got
&lt;span class="gi"&gt;+  let type_conditional_branches_mismatch ~then_type ~else_type =
+    Printf.sprintf
+      "Conditional branches have different types: then branch returns %s, else branch returns %s"
+      then_type else_type
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;   (* Interpreter messages *)
   let interp_division_by_zero = "Division by zero"
&lt;span class="gh"&gt;diff --git a/lib/error.mli b/lib/error.mli
index b3e7da6..28cd496 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/error.mli
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/error.mli
&lt;/span&gt;&lt;span class="p"&gt;@@ -26,6 +26,7 @@&lt;/span&gt; module Msg : sig
   val type_non_indexable_field : string -&amp;gt; string
   val type_invalid_lookup_key : string -&amp;gt; string
   val type_mismatch : expected:string -&amp;gt; got:string -&amp;gt; string
&lt;span class="gi"&gt;+  val type_conditional_branches_mismatch : then_type:string -&amp;gt; else_type:string -&amp;gt; string
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;   (* Interpreter messages *)
   val interp_division_by_zero : string
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The boring part (by design)
&lt;/h3&gt;

&lt;p&gt;The interpreter is the boring part. It just picks one branch or the other:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/interpreter.ml b/lib/interpreter.ml
index 37e367e..235b90e 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/interpreter.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/interpreter.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -29,6 +29,8 @@&lt;/span&gt; let rec interpret env expr =
   | FunctionDef (pos, def) -&amp;gt; interpret_function_def env (pos, def)
   | FunctionCall (pos, call) -&amp;gt; interpret_function_call env (pos, call)
   | Closure _ -&amp;gt; ok (env, expr)
&lt;span class="gi"&gt;+  | If (pos, cond_expr, then_expr, else_expr_opt) -&amp;gt;
+      interpret_conditional env (pos, cond_expr, then_expr, else_expr_opt)
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt; and interpret_indexed_expr env (pos, varname, index_expr) =
   let* (env', index_expr') = interpret env index_expr in
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ocaml"&gt;&lt;code&gt;&lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;interpret_conditional&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cond_expr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;then_expr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;else_expr_opt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cond&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;interpret&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="n"&gt;cond_expr&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;cond&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;interpret&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="n"&gt;then_expr&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;else_expr_opt&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt; &lt;span class="n"&gt;else_expr&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;interpret&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="n"&gt;else_expr&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Null&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;(* Unreachable: type checker ensures cond_expr is Bool *)&lt;/span&gt;
    &lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_at&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="s2"&gt;"interpret_conditional: non-boolean condition (type checker should prevent this)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The unreachable branch at the end is starting to bother me. The type checker already guarantees the condition is a &lt;code&gt;Bool&lt;/code&gt;, so that match arm will never fire. I keep thinking about using phantom types to encode this statically and let the compiler enforce it, instead of leaving a comment that future me will pretend not to see. Something to revisit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tests
&lt;/h3&gt;

&lt;p&gt;And it works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;dune &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; tsonnet samples/tutorials/conditionals.jsonnet
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"Large Mojito"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"garnish"&lt;/span&gt;: &lt;span class="s2"&gt;"Lime wedge"&lt;/span&gt;,
    &lt;span class="s2"&gt;"ingredients"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;
      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"action"&lt;/span&gt;: &lt;span class="s2"&gt;"muddle"&lt;/span&gt;, &lt;span class="s2"&gt;"kind"&lt;/span&gt;: &lt;span class="s2"&gt;"Mint"&lt;/span&gt;, &lt;span class="s2"&gt;"qty"&lt;/span&gt;: 12, &lt;span class="s2"&gt;"unit"&lt;/span&gt;: &lt;span class="s2"&gt;"leaves"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;,
      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"kind"&lt;/span&gt;: &lt;span class="s2"&gt;"Banks"&lt;/span&gt;, &lt;span class="s2"&gt;"qty"&lt;/span&gt;: 3.0 &lt;span class="o"&gt;}&lt;/span&gt;,
      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"kind"&lt;/span&gt;: &lt;span class="s2"&gt;"Lime"&lt;/span&gt;, &lt;span class="s2"&gt;"qty"&lt;/span&gt;: 1.0 &lt;span class="o"&gt;}&lt;/span&gt;,
      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"kind"&lt;/span&gt;: &lt;span class="s2"&gt;"Simple Syrup"&lt;/span&gt;, &lt;span class="s2"&gt;"qty"&lt;/span&gt;: 1.0 &lt;span class="o"&gt;}&lt;/span&gt;,
      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"kind"&lt;/span&gt;: &lt;span class="s2"&gt;"Soda"&lt;/span&gt;, &lt;span class="s2"&gt;"qty"&lt;/span&gt;: 6 &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;]&lt;/span&gt;,
    &lt;span class="s2"&gt;"served"&lt;/span&gt;: &lt;span class="s2"&gt;"Over crushed ice"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="s2"&gt;"Mojito"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"garnish"&lt;/span&gt;: null,
    &lt;span class="s2"&gt;"ingredients"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;
      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"action"&lt;/span&gt;: &lt;span class="s2"&gt;"muddle"&lt;/span&gt;, &lt;span class="s2"&gt;"kind"&lt;/span&gt;: &lt;span class="s2"&gt;"Mint"&lt;/span&gt;, &lt;span class="s2"&gt;"qty"&lt;/span&gt;: 6, &lt;span class="s2"&gt;"unit"&lt;/span&gt;: &lt;span class="s2"&gt;"leaves"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;,
      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"kind"&lt;/span&gt;: &lt;span class="s2"&gt;"Banks"&lt;/span&gt;, &lt;span class="s2"&gt;"qty"&lt;/span&gt;: 1.5 &lt;span class="o"&gt;}&lt;/span&gt;,
      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"kind"&lt;/span&gt;: &lt;span class="s2"&gt;"Lime"&lt;/span&gt;, &lt;span class="s2"&gt;"qty"&lt;/span&gt;: 0.5 &lt;span class="o"&gt;}&lt;/span&gt;,
      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"kind"&lt;/span&gt;: &lt;span class="s2"&gt;"Simple Syrup"&lt;/span&gt;, &lt;span class="s2"&gt;"qty"&lt;/span&gt;: 0.5 &lt;span class="o"&gt;}&lt;/span&gt;,
      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"kind"&lt;/span&gt;: &lt;span class="s2"&gt;"Soda"&lt;/span&gt;, &lt;span class="s2"&gt;"qty"&lt;/span&gt;: 3 &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;]&lt;/span&gt;,
    &lt;span class="s2"&gt;"served"&lt;/span&gt;: &lt;span class="s2"&gt;"Over crushed ice"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="s2"&gt;"Virgin Mojito"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"garnish"&lt;/span&gt;: null,
    &lt;span class="s2"&gt;"ingredients"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;
      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"action"&lt;/span&gt;: &lt;span class="s2"&gt;"muddle"&lt;/span&gt;, &lt;span class="s2"&gt;"kind"&lt;/span&gt;: &lt;span class="s2"&gt;"Mint"&lt;/span&gt;, &lt;span class="s2"&gt;"qty"&lt;/span&gt;: 6, &lt;span class="s2"&gt;"unit"&lt;/span&gt;: &lt;span class="s2"&gt;"leaves"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;,
      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"kind"&lt;/span&gt;: &lt;span class="s2"&gt;"Lime"&lt;/span&gt;, &lt;span class="s2"&gt;"qty"&lt;/span&gt;: 0.5 &lt;span class="o"&gt;}&lt;/span&gt;,
      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"kind"&lt;/span&gt;: &lt;span class="s2"&gt;"Simple Syrup"&lt;/span&gt;, &lt;span class="s2"&gt;"qty"&lt;/span&gt;: 0.5 &lt;span class="o"&gt;}&lt;/span&gt;,
      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"kind"&lt;/span&gt;: &lt;span class="s2"&gt;"Soda"&lt;/span&gt;, &lt;span class="s2"&gt;"qty"&lt;/span&gt;: 3 &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;]&lt;/span&gt;,
    &lt;span class="s2"&gt;"served"&lt;/span&gt;: &lt;span class="s2"&gt;"Over crushed ice"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cram tests will keep it honest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/samples/conditionals/conditionals.jsonnet b/samples/conditionals/conditionals.jsonnet
&lt;/span&gt;&lt;span class="p"&gt;new file mode 100644
&lt;/span&gt;&lt;span class="gh"&gt;index 0000000..6b39918
&lt;/span&gt;&lt;span class="gd"&gt;--- /dev/null
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/samples/conditionals/conditionals.jsonnet
&lt;/span&gt;&lt;span class="p"&gt;@@ -0,0 +1,17 @@&lt;/span&gt;
&lt;span class="gi"&gt;+{
+    'cond_true': if true then 'if true works!',
+    'cond_null': if false then 'unreachable!',
+    'cond_else_true': if true then 'then branch' else 'else branch',
+    'cond_else_false': if false then 'then branch' else 'else branch',
+    'cond_else_expr': if true then 10 + 5 else 20 + 5,
+    'cond_nested_object': {
+        result: if false then 'not this' else 'this one'
+    },
+    'cond_nested_chain':
+        if true then
+            if false then 0
+            else
+                if true then 42
+                else 1
+        else 2
+}
&lt;/span&gt;&lt;span class="gh"&gt;diff --git a/samples/semantics/invalid_conditional_branches_type.jsonnet b/samples/semantics/invalid_conditional_branches_type.jsonnet
&lt;/span&gt;&lt;span class="p"&gt;new file mode 100644
&lt;/span&gt;&lt;span class="gh"&gt;index 0000000..1ea1aef
&lt;/span&gt;&lt;span class="gd"&gt;--- /dev/null
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/samples/semantics/invalid_conditional_branches_type.jsonnet
&lt;/span&gt;&lt;span class="p"&gt;@@ -0,0 +1 @@&lt;/span&gt;
&lt;span class="gi"&gt;+if true then 1 else "oops"
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gh"&gt;diff --git a/test/cram/semantics.t b/test/cram/semantics.t
index 3f26924..14f5911 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/test/cram/semantics.t
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/test/cram/semantics.t
&lt;/span&gt;&lt;span class="p"&gt;@@ -324,3 +324,11 @@&lt;/span&gt;
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   [1]
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+
+  $ tsonnet ../../samples/semantics/invalid_conditional_branches_type.jsonnet
+  ERROR: ../../samples/semantics/invalid_conditional_branches_type.jsonnet:1:0 Conditional branches have different types: then branch returns Number, else branch returns String
+  
+  1: if true then 1 else "oops"
+     ^^^^^^^^^^^^^^^^^^^^^^^^^^
+  [1]
+
&lt;/span&gt;&lt;span class="gh"&gt;diff --git a/test/cram/tutorials.t b/test/cram/tutorials.t
index 9990a7e..b3c1bc9 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/test/cram/tutorials.t
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/test/cram/tutorials.t
&lt;/span&gt;&lt;span class="p"&gt;@@ -92,6 +92,42 @@&lt;/span&gt;
     }
   }
&lt;span class="gi"&gt;+  $ tsonnet ../../samples/tutorials/conditionals.jsonnet
+  {
+    "Large Mojito": {
+      "garnish": "Lime wedge",
+      "ingredients": [
+        { "action": "muddle", "kind": "Mint", "qty": 12, "unit": "leaves" },
+        { "kind": "Banks", "qty": 3.0 },
+        { "kind": "Lime", "qty": 1.0 },
+        { "kind": "Simple Syrup", "qty": 1.0 },
+        { "kind": "Soda", "qty": 6 }
+      ],
+      "served": "Over crushed ice"
+    },
+    "Mojito": {
+      "garnish": null,
+      "ingredients": [
+        { "action": "muddle", "kind": "Mint", "qty": 6, "unit": "leaves" },
+        { "kind": "Banks", "qty": 1.5 },
+        { "kind": "Lime", "qty": 0.5 },
+        { "kind": "Simple Syrup", "qty": 0.5 },
+        { "kind": "Soda", "qty": 3 }
+      ],
+      "served": "Over crushed ice"
+    },
+    "Virgin Mojito": {
+      "garnish": null,
+      "ingredients": [
+        { "action": "muddle", "kind": "Mint", "qty": 6, "unit": "leaves" },
+        { "kind": "Lime", "qty": 0.5 },
+        { "kind": "Simple Syrup", "qty": 0.5 },
+        { "kind": "Soda", "qty": 3 }
+      ],
+      "served": "Over crushed ice"
+    }
+  }
+
&lt;/span&gt;   $ tsonnet ../../samples/tutorials/arith.jsonnet
   {
     "concat_array": [ 1, 2, 3, 4 ],
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Conditionals are in. Not a lot of ceremony for something this fundamental — which is usually the sign of a clean design paying off.&lt;/p&gt;

&lt;p&gt;Next up: probably, for loops and comprehensions.&lt;/p&gt;

&lt;p&gt;The entire diff is available &lt;a href="https://gitlab.com/-/snippets/5994118" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Thanks for reading Bit Maybe Wise! If conditionals finally work in Tsonnet, maybe you should conditionally &lt;a href="https://bitmaybewise.com/" rel="noopener noreferrer"&gt;subscribe&lt;/a&gt; — but make the condition &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@marcokaufmann" rel="noopener noreferrer"&gt;Marco Kaufmann&lt;/a&gt; on &lt;a href="https://unsplash.com" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tsonnet</category>
      <category>jsonnet</category>
      <category>compiler</category>
    </item>
    <item>
      <title>Tsonnet #41 - Call me maybe, but make it argless</title>
      <dc:creator>Hercules Lemke Merscher</dc:creator>
      <pubDate>Fri, 15 May 2026 09:20:51 +0000</pubDate>
      <link>https://dev.to/bitmaybewise/tsonnet-41-call-me-maybe-but-make-it-argless-1g7p</link>
      <guid>https://dev.to/bitmaybewise/tsonnet-41-call-me-maybe-but-make-it-argless-1g7p</guid>
      <description>&lt;p&gt;Welcome to the &lt;a href="https://gitlab.com/bitmaybewise/tsonnet" rel="noopener noreferrer"&gt;Tsonnet&lt;/a&gt; series!&lt;/p&gt;

&lt;p&gt;If you're not following along, check out how it all started in &lt;a href="https://dev.to/bitmaybewise/tsonnet-0-from-zero-to-interpreter-54na"&gt;the first post of the series&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the previous post, we wrapped up function support with named parameters at call sites:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bitmaybewise/tsonnet-40-call-me-maybe-but-make-it-typed-part-6-31ld" class="crayons-story__hidden-navigation-link"&gt;Tsonnet #40 - Call me maybe, but make it typed, part 6&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/bitmaybewise" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" alt="bitmaybewise profile" class="crayons-avatar__image" width="279" height="279"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/bitmaybewise" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Hercules Lemke Merscher
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Hercules Lemke Merscher
                
              
              &lt;div id="story-author-preview-content-3537960" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/bitmaybewise" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" class="crayons-avatar__image" alt="" width="279" height="279"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Hercules Lemke Merscher&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/bitmaybewise/tsonnet-40-call-me-maybe-but-make-it-typed-part-6-31ld" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Apr 23&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/bitmaybewise/tsonnet-40-call-me-maybe-but-make-it-typed-part-6-31ld" id="article-link-3537960"&gt;
          Tsonnet #40 - Call me maybe, but make it typed, part 6
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tsonnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tsonnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/jsonnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;jsonnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/compiler"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;compiler&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/bitmaybewise/tsonnet-40-call-me-maybe-but-make-it-typed-part-6-31ld#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            8 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;Oops, I forgot function calls without arguments.&lt;/p&gt;

&lt;p&gt;Classic. I left them home alone.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzb9fho6i3rb1iwdrnoex.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzb9fho6i3rb1iwdrnoex.webp" alt="Kevin home alone" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What must be validated
&lt;/h2&gt;

&lt;p&gt;Here's what we need to handle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;greet()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'hello';&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;make_obj()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;x:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;y:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;apply(f)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;f();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;obj&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;m():&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;greet:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;greet()&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;obj:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;make_obj()&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;inline:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(function()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="err"&gt;)()&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;applied:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;apply(function()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'world')&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;method_call:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;obj.m()&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Parsing
&lt;/h2&gt;

&lt;p&gt;The fix is very straightforward. The only change required is substituting &lt;code&gt;separated_nonempty_list&lt;/code&gt; by &lt;code&gt;separated_list&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/parser.mly b/lib/parser.mly
index 94f1a2f..781ec5d 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/parser.mly
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/parser.mly
&lt;/span&gt;&lt;span class="p"&gt;@@ -102,7 +102,7 @@&lt;/span&gt; obj_key:
 obj_field:
   | k = obj_key; COLON; e = assignable_expr { ObjectField (k, e) }
   | k = obj_key;
&lt;span class="gd"&gt;-    LEFT_PAREN; params = separated_nonempty_list(COMMA, fundef_param); RIGHT_PAREN;
&lt;/span&gt;&lt;span class="gi"&gt;+    LEFT_PAREN; params = separated_list(COMMA, fundef_param); RIGHT_PAREN;
&lt;/span&gt;     COLON; body = assignable_expr
     { ObjectField (
         k,
&lt;span class="p"&gt;@@ -201,7 +201,7 @@&lt;/span&gt; fundef_param:
&lt;span class="err"&gt;
&lt;/span&gt; fundef:
   | fname = ID;
&lt;span class="gd"&gt;-    LEFT_PAREN; params = separated_nonempty_list(COMMA, fundef_param); RIGHT_PAREN;
&lt;/span&gt;&lt;span class="gi"&gt;+    LEFT_PAREN; params = separated_list(COMMA, fundef_param); RIGHT_PAREN;
&lt;/span&gt;     ASSIGN;
     body = fundef_body { { name = fname; params = params; body = body } }
   ;
&lt;span class="p"&gt;@@ -218,7 +218,7 @@&lt;/span&gt; funcall_arg:
&lt;span class="err"&gt;
&lt;/span&gt; funcall:
   | fname = ID;
&lt;span class="gd"&gt;-    LEFT_PAREN; params = separated_nonempty_list(COMMA, funcall_arg); RIGHT_PAREN
&lt;/span&gt;&lt;span class="gi"&gt;+    LEFT_PAREN; params = separated_list(COMMA, funcall_arg); RIGHT_PAREN
&lt;/span&gt;     { FunctionCall
       (with_pos $startpos $endpos, {
         callee = Ident (with_pos $startpos(fname) $endpos(fname), fname);
&lt;span class="p"&gt;@@ -226,16 +226,16 @@&lt;/span&gt; funcall:
       })
     }
   | callee = scoped_expr;
&lt;span class="gd"&gt;-    LEFT_PAREN; params = separated_nonempty_list(COMMA, funcall_arg); RIGHT_PAREN
&lt;/span&gt;&lt;span class="gi"&gt;+    LEFT_PAREN; params = separated_list(COMMA, funcall_arg); RIGHT_PAREN
&lt;/span&gt;     { FunctionCall (with_pos $startpos $endpos, { callee = callee; args = params }) }
   | callee = obj_field_access;
&lt;span class="gd"&gt;-    LEFT_PAREN; params = separated_nonempty_list(COMMA, funcall_arg); RIGHT_PAREN
&lt;/span&gt;&lt;span class="gi"&gt;+    LEFT_PAREN; params = separated_list(COMMA, funcall_arg); RIGHT_PAREN
&lt;/span&gt;     { FunctionCall (with_pos $startpos $endpos, { callee = callee; args = params }) }
   ;
&lt;span class="err"&gt;
&lt;/span&gt; closure:
   | FUNCTION;
&lt;span class="gd"&gt;-    LEFT_PAREN; params = separated_nonempty_list(COMMA, fundef_param); RIGHT_PAREN;
&lt;/span&gt;&lt;span class="gi"&gt;+    LEFT_PAREN; params = separated_list(COMMA, fundef_param); RIGHT_PAREN;
&lt;/span&gt;     body = assignable_expr {
       Closure (with_pos $startpos $endpos, { params = params; body = body })
     }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every place we parse parameter or argument lists gets the same treatment: function definitions, object method definitions, function calls, and closures. Swap &lt;code&gt;nonempty&lt;/code&gt; out, and zero-argument calls stop crashing the parser.&lt;/p&gt;

&lt;p&gt;How could I forget this one? XD&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;A new sample and a cram test entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/test/cram/functions.t b/test/cram/functions.t
index 89cfd27..44f7d44 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/test/cram/functions.t
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/test/cram/functions.t
&lt;/span&gt;&lt;span class="p"&gt;@@ -15,3 +15,12 @@&lt;/span&gt;
&lt;span class="err"&gt;
&lt;/span&gt;   $ tsonnet ../../samples/functions/method.jsonnet
   4
&lt;span class="gi"&gt;+
+  $ tsonnet ../../samples/functions/no_args.jsonnet
+  {
+    "applied": "world",
+    "greet": "hello",
+    "inline": 42,
+    "method_call": 7,
+    "obj": { "x": 1, "y": 2 }
+  }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero arguments, zero drama.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Short one, but a necessary one. No-argument function calls were quietly broken, and the fix turned out to be a one-word change per grammar rule. Easy to overlook, easy to fix once you notice.&lt;/p&gt;

&lt;p&gt;Next up: conditionals. The Jsonnet tutorial can't wait forever.&lt;/p&gt;

&lt;p&gt;The entire diff can be seen &lt;a href="https://gitlab.com/-/snippets/5992418" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Thanks for reading Bit Maybe Wise! Left no-arg calls home alone. &lt;a href="https://bitmaybewise.com/" rel="noopener noreferrer"&gt;Subscribe&lt;/a&gt; so you don't miss when conditionals finally move in.&lt;/p&gt;

&lt;p&gt;Photo from &lt;a href="https://www.businessinsider.com/photos-show-inside-real-life-home-alone-house-chicago-illinois-2020-12" rel="noopener noreferrer"&gt;https://www.businessinsider.com/photos-show-inside-real-life-home-alone-house-chicago-illinois-2020-12&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tsonnet</category>
      <category>jsonnet</category>
      <category>compiler</category>
    </item>
    <item>
      <title>Tsonnet #40 - Call me maybe, but make it typed, part 6</title>
      <dc:creator>Hercules Lemke Merscher</dc:creator>
      <pubDate>Thu, 23 Apr 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/bitmaybewise/tsonnet-40-call-me-maybe-but-make-it-typed-part-6-31ld</link>
      <guid>https://dev.to/bitmaybewise/tsonnet-40-call-me-maybe-but-make-it-typed-part-6-31ld</guid>
      <description>&lt;p&gt;Welcome to the &lt;a href="https://gitlab.com/bitmaybewise/tsonnet" rel="noopener noreferrer"&gt;Tsonnet&lt;/a&gt; series!&lt;/p&gt;

&lt;p&gt;If you're not following along, check out how it all started in &lt;a href="https://dev.to/bitmaybewise/tsonnet-0-from-zero-to-interpreter-54na"&gt;the first post of the series&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the previous post, we implemented methods:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bitmaybewise/tsonnet-39-call-me-maybe-but-make-it-typed-part-5-3bf" class="crayons-story__hidden-navigation-link"&gt;Tsonnet #39 - Call me maybe, but make it typed, part 5&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/bitmaybewise" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" alt="bitmaybewise profile" class="crayons-avatar__image" width="279" height="279"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/bitmaybewise" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Hercules Lemke Merscher
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Hercules Lemke Merscher
                
              
              &lt;div id="story-author-preview-content-3533377" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/bitmaybewise" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" class="crayons-avatar__image" alt="" width="279" height="279"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Hercules Lemke Merscher&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/bitmaybewise/tsonnet-39-call-me-maybe-but-make-it-typed-part-5-3bf" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Apr 22&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/bitmaybewise/tsonnet-39-call-me-maybe-but-make-it-typed-part-5-3bf" id="article-link-3533377"&gt;
          Tsonnet #39 - Call me maybe, but make it typed, part 5
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tsonnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tsonnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/jsonnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;jsonnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/compiler"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;compiler&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/bitmaybewise/tsonnet-39-call-me-maybe-but-make-it-typed-part-5-3bf#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;After all this work implementing functions and methods, there's still one thing missing: function calls with named parameters. I implemented the definition, but not the call site when named parameters are present.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk36mrspqh4nncwcbxty9.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk36mrspqh4nncwcbxty9.jpeg" alt="yellow telephone" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  This is what I want to test
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/functions/named_params.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;my_function(x,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;y=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;y;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;my_function(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;y=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A new argument in the room
&lt;/h2&gt;

&lt;p&gt;This calls for a new type variant: &lt;code&gt;call_arg&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/ast.ml b/lib/ast.ml
index 4b1102e..1b5724b 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/ast.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/ast.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -107,9 +107,12 @@&lt;/span&gt; and function_def = {
   params: (string * expr option) list;
   body: expr;
 }
&lt;span class="gi"&gt;+and call_arg =
+  | Positional of expr
+  | Named of string * expr
&lt;/span&gt; and function_call = {
   callee: expr;
&lt;span class="gd"&gt;-  args: expr list;
&lt;/span&gt;&lt;span class="gi"&gt;+  args: call_arg list;
&lt;/span&gt; }
 and closure = {
   params: (string * expr option) list;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way we can leverage the expressiveness of the OCaml type system to enforce a &lt;code&gt;Positional&lt;/code&gt; or &lt;code&gt;Named&lt;/code&gt; argument.&lt;/p&gt;

&lt;p&gt;The parser needs a little tweaking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/parser.mly b/lib/parser.mly
index 0c5601d..94f1a2f 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/parser.mly
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/parser.mly
&lt;/span&gt;&lt;span class="p"&gt;@@ -211,9 +211,14 @@&lt;/span&gt; fundef_body:
   | local_bindings = vars; SEMICOLON; body = fundef_body { Seq [local_bindings; body] }
   ;
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+funcall_arg:
+  | name = ID; ASSIGN; e = assignable_expr { Named (name, e) }
+  | e = assignable_expr { Positional e }
+  ;
+
&lt;/span&gt; funcall:
   | fname = ID;
&lt;span class="gd"&gt;-    LEFT_PAREN; params = separated_nonempty_list(COMMA, assignable_expr); RIGHT_PAREN
&lt;/span&gt;&lt;span class="gi"&gt;+    LEFT_PAREN; params = separated_nonempty_list(COMMA, funcall_arg); RIGHT_PAREN
&lt;/span&gt;     { FunctionCall
       (with_pos $startpos $endpos, {
         callee = Ident (with_pos $startpos(fname) $endpos(fname), fname);
&lt;span class="p"&gt;@@ -221,10 +226,10 @@&lt;/span&gt; funcall:
       })
     }
   | callee = scoped_expr;
&lt;span class="gd"&gt;-    LEFT_PAREN; params = separated_nonempty_list(COMMA, assignable_expr); RIGHT_PAREN
&lt;/span&gt;&lt;span class="gi"&gt;+    LEFT_PAREN; params = separated_nonempty_list(COMMA, funcall_arg); RIGHT_PAREN
&lt;/span&gt;     { FunctionCall (with_pos $startpos $endpos, { callee = callee; args = params }) }
   | callee = obj_field_access;
&lt;span class="gd"&gt;-    LEFT_PAREN; params = separated_nonempty_list(COMMA, assignable_expr); RIGHT_PAREN
&lt;/span&gt;&lt;span class="gi"&gt;+    LEFT_PAREN; params = separated_nonempty_list(COMMA, funcall_arg); RIGHT_PAREN
&lt;/span&gt;     { FunctionCall (with_pos $startpos $endpos, { callee = callee; args = params }) }
   ;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Teaching the typechecker to count on its fingers
&lt;/h2&gt;

&lt;p&gt;With the addition of named parameters to function calls, they need to be translated by the type checker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/type.ml b/lib/type.ml
index 4b438db..c537187 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/type.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/type.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -111,7 +111,10 @@&lt;/span&gt; let rec collect_free_idents = function
   | IndexedExpr (_, name, e) -&amp;gt; name :: collect_free_idents e
   | Local (_, vars) -&amp;gt; List.concat_map (fun (_, e) -&amp;gt; collect_free_idents e) vars
   | FunctionCall (_, call) -&amp;gt;
&lt;span class="gd"&gt;-    collect_free_idents call.callee @ List.concat_map collect_free_idents call.args
&lt;/span&gt;&lt;span class="gi"&gt;+    collect_free_idents call.callee @ List.concat_map (function
+      | Positional e -&amp;gt; collect_free_idents e
+      | Named (_, e) -&amp;gt; collect_free_idents e
+    ) call.args
&lt;/span&gt;   | Closure (_, closure) -&amp;gt; collect_free_idents closure.body
   | _ -&amp;gt; []
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;@@ -551,27 +554,38 @@&lt;/span&gt; and translate_function_call venv (pos, call) =
     )
&lt;span class="err"&gt;
&lt;/span&gt; and translate_named_function_call venv (pos, name, args) =
&lt;span class="gi"&gt;+  let positional_args =
+    List.filter_map (function Positional e -&amp;gt; Some e | Named _ -&amp;gt; None) args
+  in
+  let named_args =
+    List.filter_map (function Named (n, e) -&amp;gt; Some (n, e) | Positional _ -&amp;gt; None) args
+  in
+  let num_positional = List.length positional_args in
&lt;/span&gt;   match Env.find_opt name venv with
   | Some (TfunctionDef { params = def_params;
&lt;span class="gd"&gt;-                         body = body_expr;
-                         return = return_type;
-                       }) -&amp;gt;
-    let num_call = List.length args in
&lt;/span&gt;&lt;span class="gi"&gt;+                          body = body_expr;
+                          return = return_type;
+                        }) -&amp;gt;
&lt;/span&gt;     let num_def = List.length def_params in
&lt;span class="gd"&gt;-    if num_call &amp;gt; num_def
&lt;/span&gt;&lt;span class="gi"&gt;+    let num_provided = num_positional + List.length named_args in
+    if num_provided &amp;gt; num_def
&lt;/span&gt;     then
       Error.error_at pos
&lt;span class="gd"&gt;-        (Error.Msg.wrong_number_of_params num_def num_call)
&lt;/span&gt;&lt;span class="gi"&gt;+        (Error.Msg.wrong_number_of_params num_def num_provided)
&lt;/span&gt;     else
       let* (venv', resolved_params) =
         List.fold_left
           (fun acc (index, (param_name, def_param_type)) -&amp;gt;
             let* (venv', params') = acc in
&lt;span class="gd"&gt;-            if index &amp;lt; num_call
-            then
-              let call_param = List.nth args index in
&lt;/span&gt;&lt;span class="gi"&gt;+            let call_expr_opt =
+              if index &amp;lt; num_positional
+              then Some (List.nth positional_args index)
+              else Option.map snd (List.find_opt (fun (n, _) -&amp;gt; n = param_name) named_args)
+            in
+            match call_expr_opt with
+            | Some call_param -&amp;gt;
&lt;/span&gt;               let* (venv'', call_param_type) = translate venv' call_param in
&lt;span class="gd"&gt;-              match def_param_type with
&lt;/span&gt;&lt;span class="gi"&gt;+              (match def_param_type with
&lt;/span&gt;               | Tunresolved -&amp;gt;
                 ok (venv'', params' @ [(param_name, call_param_type)])
               | expected -&amp;gt;
&lt;span class="p"&gt;@@ -581,7 +595,8 @@&lt;/span&gt; and translate_named_function_call venv (pos, name, args) =
                   (Error.Msg.type_mismatch
                     ~expected:(to_string expected)
                     ~got:(to_string call_param_type))
&lt;span class="gd"&gt;-            else
&lt;/span&gt;&lt;span class="gi"&gt;+              )
+            | None -&amp;gt;
&lt;/span&gt;               ok (venv', params' @ [(param_name, def_param_type)])
           )
           (ok (venv, []))
&lt;span class="p"&gt;@@ -640,30 +655,43 @@&lt;/span&gt; and translate_closure venv (_pos, closure) =
   in
   ok (venv, Tclosure { params = params_typed; body = closure.body })
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-and translate_closure_call venv (pos, def_params, body, call_params) =
-  let num_call = List.length call_params in
&lt;/span&gt;&lt;span class="gi"&gt;+and translate_closure_call venv (pos, def_params, body, call_args) =
+  let positional_args =
+    List.filter_map (function Positional e -&amp;gt; Some e | Named _ -&amp;gt; None) call_args
+  in
+  let named_args =
+    List.filter_map (function Named (n, e) -&amp;gt; Some (n, e) | Positional _ -&amp;gt; None) call_args
+  in
+  let num_positional = List.length positional_args in
&lt;/span&gt;   let num_def = List.length def_params in
   let num_required =
     List.length (List.filter (fun (_, default) -&amp;gt; Option.is_none default) def_params)
   in
&lt;span class="gd"&gt;-  if num_call &amp;lt; num_required || num_call &amp;gt; num_def
-  then Error.error_at pos (Error.Msg.wrong_number_of_params num_def num_call)
&lt;/span&gt;&lt;span class="gi"&gt;+  let num_provided = num_positional + List.length named_args in
+  if num_provided &amp;lt; num_required || num_provided &amp;gt; num_def
+  then Error.error_at pos (Error.Msg.wrong_number_of_params num_def num_provided)
&lt;/span&gt;   else
     let* (venv', resolved_params) =
       List.fold_left
         (fun acc (index, (param_name, default)) -&amp;gt;
           let* (venv', params') = acc in
&lt;span class="gd"&gt;-          if index &amp;lt; num_call
-          then
-            let call_param = List.nth call_params index in
&lt;/span&gt;&lt;span class="gi"&gt;+          let call_expr_opt =
+            if index &amp;lt; num_positional
+            then Some (List.nth positional_args index)
+            else Option.map snd (List.find_opt (fun (n, _) -&amp;gt; n = param_name) named_args)
+          in
+          match call_expr_opt with
+          | Some call_param -&amp;gt;
&lt;/span&gt;             let* (venv'', call_param_type) = translate venv' call_param in
             ok (venv'', params' @ [(param_name, call_param_type)])
&lt;span class="gd"&gt;-          else
-            match default with
&lt;/span&gt;&lt;span class="gi"&gt;+          | None -&amp;gt;
+            (match default with
&lt;/span&gt;             | Some default_expr -&amp;gt;
               let* (venv'', default_type) = translate venv' default_expr in
               ok (venv'', params' @ [(param_name, default_type)])
&lt;span class="gd"&gt;-            | None -&amp;gt; Error.error_at pos (Error.Msg.wrong_number_of_params num_def num_call)
&lt;/span&gt;&lt;span class="gi"&gt;+            | None -&amp;gt;
+              Error.error_at pos (Error.Msg.wrong_number_of_params num_def num_provided)
+            )
&lt;/span&gt;         )
         (ok (venv, []))
         (List.mapi (fun i p -&amp;gt; (i, p)) def_params)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I feel like &lt;code&gt;translate_named_function_call&lt;/code&gt; and &lt;code&gt;translate_closure_call&lt;/code&gt; could be unified, similar to what I did with &lt;code&gt;apply_function&lt;/code&gt; in the interpreter. They differ in subtle ways though, so I'll leave that for another day.&lt;/p&gt;

&lt;p&gt;The interpreter changes are only following the new type variant addition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/interpreter.ml b/lib/interpreter.ml
index 15f9932..37e367e 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/interpreter.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/interpreter.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -420,19 +420,26 @@&lt;/span&gt; and interpret_function_def env (pos, def) =
   let env' = Env.add_local def.name (FunctionDef (pos, def)) env in
   ok (env', Unit)
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-and apply_function env pos def_params body call_params =
-  let num_call = List.length call_params in
&lt;/span&gt;&lt;span class="gi"&gt;+and apply_function env pos def_params body call_args =
+  let positional_args =
+    List.filter_map (function Positional e -&amp;gt; Some e | Named _ -&amp;gt; None) call_args
+  in
+  let named_args =
+    List.filter_map (function Named (n, e) -&amp;gt; Some (n, e) | Positional _ -&amp;gt; None) call_args
+  in
+  let num_positional = List.length positional_args in
&lt;/span&gt;   let num_def = List.length def_params in
   let num_required =
     List.length (
       List.filter (fun (_, default) -&amp;gt; Option.is_none default) def_params
     )
   in
&lt;span class="gd"&gt;-  if num_call &amp;lt; num_required || num_call &amp;gt; num_def
&lt;/span&gt;&lt;span class="gi"&gt;+  let num_provided = num_positional + List.length named_args in
+  if num_provided &amp;lt; num_required || num_provided &amp;gt; num_def
&lt;/span&gt;   then
&lt;span class="gd"&gt;-    Error.error_at pos (Error.Msg.wrong_number_of_params num_def num_call)
&lt;/span&gt;&lt;span class="gi"&gt;+    Error.error_at pos (Error.Msg.wrong_number_of_params num_def num_provided)
&lt;/span&gt;   else
&lt;span class="gd"&gt;-    let* evaluated_call_params =
&lt;/span&gt;&lt;span class="gi"&gt;+    let* evaluated_positional =
&lt;/span&gt;       List.fold_left
         (fun acc param -&amp;gt;
           let* params = acc in
&lt;span class="p"&gt;@@ -440,19 +447,32 @@&lt;/span&gt; and apply_function env pos def_params body call_params =
           ok (params @ [v])
         )
         (ok [])
&lt;span class="gd"&gt;-        call_params
&lt;/span&gt;&lt;span class="gi"&gt;+        positional_args
+    in
+    let* evaluated_named =
+      List.fold_left
+        (fun acc (name, expr) -&amp;gt;
+          let* params = acc in
+          let* (_, v) = interpret env expr in
+          ok (params @ [(name, v)])
+        )
+        (ok [])
+        named_args
&lt;/span&gt;     in
     let* bindings =
       List.fold_left
         (fun acc (index, (param_name, default)) -&amp;gt;
           let* bindings = acc in
&lt;span class="gd"&gt;-          if index &amp;lt; num_call
&lt;/span&gt;&lt;span class="gi"&gt;+          if index &amp;lt; num_positional
&lt;/span&gt;           then
&lt;span class="gd"&gt;-            ok (bindings @ [(param_name, List.nth evaluated_call_params index)])
&lt;/span&gt;&lt;span class="gi"&gt;+            ok (bindings @ [(param_name, List.nth evaluated_positional index)])
&lt;/span&gt;           else
&lt;span class="gd"&gt;-            match default with
-            | Some default_expr -&amp;gt; ok (bindings @ [(param_name, default_expr)])
-            | None -&amp;gt; Error.error_at pos (Error.Msg.wrong_number_of_params num_def num_call)
&lt;/span&gt;&lt;span class="gi"&gt;+            match List.assoc_opt param_name evaluated_named with
+            | Some v -&amp;gt; ok (bindings @ [(param_name, v)])
+            | None -&amp;gt;
+              match default with
+              | Some default_expr -&amp;gt; ok (bindings @ [(param_name, default_expr)])
+              | None -&amp;gt; Error.error_at pos (Error.Msg.wrong_number_of_params num_def num_provided)
&lt;/span&gt;         )
         (ok [])
         (List.mapi (fun i p -&amp;gt; (i, p)) def_params)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Does it actually work? (Yes.)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/samples/functions/named_params.jsonnet b/samples/functions/named_params.jsonnet
&lt;/span&gt;&lt;span class="p"&gt;new file mode 100644
&lt;/span&gt;&lt;span class="gh"&gt;index 0000000..76406b1
&lt;/span&gt;&lt;span class="gd"&gt;--- /dev/null
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/samples/functions/named_params.jsonnet
&lt;/span&gt;&lt;span class="p"&gt;@@ -0,0 +1,2 @@&lt;/span&gt;
&lt;span class="gi"&gt;+local my_function(x, y=10) = x + y;
+my_function(2, y=3)
&lt;/span&gt;&lt;span class="gh"&gt;diff --git a/test/cram/functions.t b/test/cram/functions.t
index 9a9fe4d..89cfd27 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/test/cram/functions.t
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/test/cram/functions.t
&lt;/span&gt;&lt;span class="p"&gt;@@ -10,5 +10,8 @@&lt;/span&gt;
   $ tsonnet ../../samples/functions/closure.jsonnet
   25
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+  $ tsonnet ../../samples/functions/named_params.jsonnet
+  5
+
&lt;/span&gt;   $ tsonnet ../../samples/functions/method.jsonnet
   4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Putting it all together (with cocktails)
&lt;/h2&gt;

&lt;p&gt;With that I can put a closure (pun intended) to the &lt;a href="https://jsonnet.org/learning/tutorial.html#functions" rel="noopener noreferrer"&gt;function tutorial&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/tutorials/functions.jsonnet&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Define&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;function.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Default&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;arguments&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;like&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Python:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;my_function(x,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;y=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;y;&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Define&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;multiline&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;function.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;multiline_function(x)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;One&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;can&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;nest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;locals.&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;temp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Every&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ends&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;semi-colon.&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;temp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;method&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;my_method(x):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Functions&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;first&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;citizens.&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;call_inline_function:&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;(function(x)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;x)(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;call_multiline_function:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;multiline_function(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Using&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;variable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;fetches&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;function&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;parens&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;function.&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;call:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;my_function(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Like&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Python&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;parameters&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;can&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;be&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;named&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;at&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;time.&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;named_params:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;my_function(x=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;This&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;allows&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;changing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;their&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;order&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;named_params&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;my_function(y=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;x=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;object.my_method&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;returns&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;function&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;which&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;then&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;called&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;like&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;other.&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;call_method&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;object.my_method(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;TODO&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;standard_lib:&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;std.join('&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;std.split('foo/bar'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'/'))&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;len:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;std.length('hello')&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;std.length(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The standard library has to wait. We must implement other building blocks, such as conditionals, for loops, etc — the usual stuff we can't live without in a programming language.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/tutorials/sours.jsonnet&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;This&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;returns&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;an&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;object.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Although&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;braces&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;look&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;like&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Java&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;C++&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;they&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;do&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;mean&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;statement&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;block,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;they&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;instead&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;being&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;returned.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Sour(spirit,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;garnish='Lemon&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;twist')&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;ingredients:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;kind:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;spirit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;qty:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;kind:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Egg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;white'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;qty:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;kind:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Lemon&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Juice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;qty:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;kind:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Simple&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Syrup'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;qty:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;garnish:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;garnish&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;served:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Straight&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Up'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;'Whiskey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Sour':&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Sour('Bulleit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Bourbon'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                       &lt;/span&gt;&lt;span class="err"&gt;'Orange&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;bitters')&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;'Pisco&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Sour':&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Sour('Machu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Pisco'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                     &lt;/span&gt;&lt;span class="err"&gt;'Angostura&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;bitters')&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Have you tried Pisco Sour already? If you're into alcoholic drinks, give it a try. It has a tasty flavour.&lt;/p&gt;

&lt;p&gt;Ta-da! Cram tests in place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/test/cram/tutorials.t b/test/cram/tutorials.t
index f3780c1..9990a7e 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/test/cram/tutorials.t
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/test/cram/tutorials.t
&lt;/span&gt;&lt;span class="p"&gt;@@ -105,3 +105,37 @@&lt;/span&gt;
     "obj": { "a": 1, "b": 3, "c": 4 },
     "obj_member": true
   }
&lt;span class="gi"&gt;+
+  $ tsonnet ../../samples/tutorials/functions.jsonnet
+  {
+    "call": 12,
+    "call_inline_function": 25,
+    "call_method1": 9,
+    "call_multiline_function": [ 8, 9 ],
+    "named_params": 12,
+    "named_params2": 5
+  }
+
+  $ tsonnet ../../samples/tutorials/sours.jsonnet
+  {
+    "Pisco Sour": {
+      "garnish": "Angostura bitters",
+      "ingredients": [
+        { "kind": "Machu Pisco", "qty": 2 },
+        { "kind": "Egg white", "qty": 1 },
+        { "kind": "Lemon Juice", "qty": 1 },
+        { "kind": "Simple Syrup", "qty": 1 }
+      ],
+      "served": "Straight Up"
+    },
+    "Whiskey Sour": {
+      "garnish": "Orange bitters",
+      "ingredients": [
+        { "kind": "Bulleit Bourbon", "qty": 2 },
+        { "kind": "Egg white", "qty": 1 },
+        { "kind": "Lemon Juice", "qty": 1 },
+        { "kind": "Simple Syrup", "qty": 1 }
+      ],
+      "served": "Straight Up"
+    }
+  }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;And that's a wrap on Tsonnet's function support (for now). Named parameters, default values, closures, methods — it's all there. The type checker knows how to count positional arguments, match them against names, and fill in defaults. Not bad for a type system that started life unable to tell a function from a hole in the ground.&lt;/p&gt;

&lt;p&gt;This closes the chapter on functions in the Jsonnet tutorial too. We went from bare locals all the way to &lt;code&gt;my_function(y=3, x=2)&lt;/code&gt; working correctly, with a Pisco Sour to show for it. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://gitlab.com/-/snippets/5983272" rel="noopener noreferrer"&gt;Here&lt;/a&gt; is the entire diff.&lt;/p&gt;




&lt;p&gt;Thanks for reading Bit Maybe Wise! That's a closure on functions. &lt;a href="https://bitmaybewise.com/" rel="noopener noreferrer"&gt;Subscribe&lt;/a&gt; so you don't miss what comes next — conditionals, loops, and poor life choices.&lt;/p&gt;

&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@mike_meyers" rel="noopener noreferrer"&gt;Mike Meyers&lt;/a&gt; on &lt;a href="https://unsplash.com" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tsonnet</category>
      <category>jsonnet</category>
      <category>compiler</category>
    </item>
    <item>
      <title>Tsonnet #39 - Call me maybe, but make it typed, part 5</title>
      <dc:creator>Hercules Lemke Merscher</dc:creator>
      <pubDate>Wed, 22 Apr 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/bitmaybewise/tsonnet-39-call-me-maybe-but-make-it-typed-part-5-3bf</link>
      <guid>https://dev.to/bitmaybewise/tsonnet-39-call-me-maybe-but-make-it-typed-part-5-3bf</guid>
      <description>&lt;p&gt;Welcome to the &lt;a href="https://gitlab.com/bitmaybewise/tsonnet" rel="noopener noreferrer"&gt;Tsonnet&lt;/a&gt; series!&lt;/p&gt;

&lt;p&gt;If you're not following along, check out how it all started in &lt;a href="https://dev.to/bitmaybewise/tsonnet-0-from-zero-to-interpreter-54na"&gt;the first post of the series&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the previous post, we refactored the AST to use named records, collapsed &lt;code&gt;ClosureCall&lt;/code&gt; into &lt;code&gt;FunctionCall&lt;/code&gt;, and cleaned up the grammar conflict that was keeping closures as second-class citizens:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bitmaybewise/tsonnet-38-call-me-maybe-but-make-it-typed-part-3-2564" class="crayons-story__hidden-navigation-link"&gt;Tsonnet #38 - Call me maybe, but make it typed, part 4&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/bitmaybewise" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" alt="bitmaybewise profile" class="crayons-avatar__image" width="279" height="279"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/bitmaybewise" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Hercules Lemke Merscher
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Hercules Lemke Merscher
                
              
              &lt;div id="story-author-preview-content-3528671" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/bitmaybewise" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" class="crayons-avatar__image" alt="" width="279" height="279"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Hercules Lemke Merscher&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/bitmaybewise/tsonnet-38-call-me-maybe-but-make-it-typed-part-3-2564" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Apr 21&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/bitmaybewise/tsonnet-38-call-me-maybe-but-make-it-typed-part-3-2564" id="article-link-3528671"&gt;
          Tsonnet #38 - Call me maybe, but make it typed, part 4
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tsonnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tsonnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/jsonnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;jsonnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/compiler"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;compiler&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/bitmaybewise/tsonnet-38-call-me-maybe-but-make-it-typed-part-3-2564#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            9 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;Now that we support functions, just one more thing is missing on that front: &lt;strong&gt;methods&lt;/strong&gt;!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdv8ric6fw0r1krygxwqc.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdv8ric6fw0r1krygxwqc.jpeg" alt="yellow pages" width="800" height="1176"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The plan (such as it is)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/functions/method.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;my_method(x):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;object.my_method(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Sneaking methods past the grammar
&lt;/h3&gt;

&lt;p&gt;After implementing functions and pausing for a moment to reflect on how I'd implement methods, I realised that methods can be easily represented as closures that bind to an object field. So I did it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/parser.mly b/lib/parser.mly
index e7b7771..0c5601d 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/parser.mly
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/parser.mly
&lt;/span&gt;&lt;span class="p"&gt;@@ -101,6 +101,14 @@&lt;/span&gt; obj_key:
&lt;span class="err"&gt;
&lt;/span&gt; obj_field:
   | k = obj_key; COLON; e = assignable_expr { ObjectField (k, e) }
&lt;span class="gi"&gt;+  | k = obj_key;
+    LEFT_PAREN; params = separated_nonempty_list(COMMA, fundef_param); RIGHT_PAREN;
+    COLON; body = assignable_expr
+    { ObjectField (
+        k,
+        Closure (with_pos $startpos $endpos, { params = params; body = body })
+      )
+    }
&lt;/span&gt;   | e = single_var { ObjectExpr e }
   ;
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;@@ -215,6 +223,9 @@&lt;/span&gt; funcall:
   | callee = scoped_expr;
     LEFT_PAREN; params = separated_nonempty_list(COMMA, assignable_expr); RIGHT_PAREN
     { FunctionCall (with_pos $startpos $endpos, { callee = callee; args = params }) }
&lt;span class="gi"&gt;+  | callee = obj_field_access;
+    LEFT_PAREN; params = separated_nonempty_list(COMMA, assignable_expr); RIGHT_PAREN
+    { FunctionCall (with_pos $startpos $endpos, { callee = callee; args = params }) }
&lt;/span&gt;   ;
&lt;span class="err"&gt;
&lt;/span&gt; closure:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach avoids adding one more abstraction, and maps precisely onto the semantics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Turns out, scope bugs don't fix themselves
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/type.ml b/lib/type.ml
index ad4f333..4b438db 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/type.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/type.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -102,7 +102,12 @@&lt;/span&gt; let rec collect_free_idents = function
       | ObjectField (_, e) -&amp;gt; collect_free_idents e
       | ObjectExpr e -&amp;gt; collect_free_idents e
     ) entries
&lt;span class="gd"&gt;-  | ObjectFieldAccess (_, _, exprs) -&amp;gt; List.concat_map collect_free_idents exprs
&lt;/span&gt;&lt;span class="gi"&gt;+  | ObjectFieldAccess (_, scope, exprs) -&amp;gt;
+    let scope_idents = match scope with
+      | ObjVarRef name -&amp;gt; [name]
+      | Self | TopLevel -&amp;gt; []
+    in
+    scope_idents @ List.concat_map collect_free_idents exprs
&lt;/span&gt;   | IndexedExpr (_, name, e) -&amp;gt; name :: collect_free_idents e
   | Local (_, vars) -&amp;gt; List.concat_map (fun (_, e) -&amp;gt; collect_free_idents e) vars
   | FunctionCall (_, call) -&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cram tests corrected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/test/cram/objects.t b/test/cram/objects.t
index 1a402df..11cc9d1 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/test/cram/objects.t
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/test/cram/objects.t
&lt;/span&gt;&lt;span class="p"&gt;@@ -23,32 +23,10 @@&lt;/span&gt;
   { "a": 1, "b": 3, "c": 4 }
&lt;span class="err"&gt;
&lt;/span&gt;   $ tsonnet ../../samples/objects/untouched_field.jsonnet
&lt;span class="gd"&gt;-  WARNING: ../../samples/objects/untouched_field.jsonnet:1:0 Unused variable result
-  
-  1: local result = {
-     ^^^^^^^^^^^^^^^^
-  2:     a: 1,
-     ^^^^^^^^^
-  3:     b: 42,
-     ^^^^^^^^^^
-  ---
&lt;/span&gt;   42
&lt;span class="err"&gt;

&lt;/span&gt;   $ tsonnet ../../samples/objects/untouched_invalid_field.jsonnet
&lt;span class="gd"&gt;-  WARNING: ../../samples/objects/untouched_invalid_field.jsonnet:1:0 Unused variable result
-  
-  1: local result = {
-     ^^^^^^^^^^^^^^^^
-  2:     a: 1,
-     ^^^^^^^^^
-  3:     b: self.a,
-     ^^^^^^^^^^^^^^
-  4:     c: self.d,
-     ^^^^^^^^^^^^^^
-  5:     d: self.c
-     ^^^^^^^^^^^^^
-  ---
&lt;/span&gt;   WARNING: ../../samples/objects/untouched_invalid_field.jsonnet:1:15 Cyclic reference found for 1-&amp;gt;c
&lt;span class="err"&gt;
&lt;/span&gt;   1: local result = {
&lt;span class="gh"&gt;diff --git a/test/cram/semantics.t b/test/cram/semantics.t
index c9a790e..3f26924 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/test/cram/semantics.t
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/test/cram/semantics.t
&lt;/span&gt;&lt;span class="p"&gt;@@ -288,17 +288,6 @@&lt;/span&gt;
&lt;span class="err"&gt;

&lt;/span&gt;   $ tsonnet ../../samples/semantics/valid_object_access_non_cyclic_field.jsonnet
&lt;span class="gd"&gt;-  WARNING: ../../samples/semantics/valid_object_access_non_cyclic_field.jsonnet:1:0 Unused variable obj
-  
-  1: local obj = {
-     ^^^^^^^^^^^^^
-  2:     a: 1,
-     ^^^^^^^^^
-  3:     b: self.c,
-     ^^^^^^^^^^^^^^
-  4:     c: self.b,
-     ^^^^^^^^^^^^^^
-  ---
&lt;/span&gt;   WARNING: ../../samples/semantics/valid_object_access_non_cyclic_field.jsonnet:1:12 Cyclic reference found for 1-&amp;gt;b
&lt;span class="err"&gt;
&lt;/span&gt;   1: local obj = {
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Does it work?
&lt;/h2&gt;

&lt;p&gt;Yes, it does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;dune &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; tsonnet samples/functions/method.jsonnet
4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/test/cram/functions.t b/test/cram/functions.t
index 5c77837..9a9fe4d 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/test/cram/functions.t
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/test/cram/functions.t
&lt;/span&gt;&lt;span class="p"&gt;@@ -9,3 +9,6 @@&lt;/span&gt;
&lt;span class="err"&gt;
&lt;/span&gt;   $ tsonnet ../../samples/functions/closure.jsonnet
   25
&lt;span class="gi"&gt;+
+  $ tsonnet ../../samples/functions/method.jsonnet
+  4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Methods turned out to be less of a feature and more of a realisation: if you squint at them right, they're just closures sitting inside object fields, and the parser was already halfway there. Two small grammar rules and a scope bug fix later, &lt;code&gt;object.my_method(2)&lt;/code&gt; just works. The scope fix was an interesting part — &lt;code&gt;collect_free_idents&lt;/code&gt; was silently dropping the receiver when traversing &lt;code&gt;ObjectFieldAccess&lt;/code&gt;, which meant variables used as method receivers could slip past the unused-variable analysis unnoticed. Worth catching.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gitlab.com/-/snippets/5979858" rel="noopener noreferrer"&gt;Here&lt;/a&gt; is the entire diff.&lt;/p&gt;

&lt;p&gt;Next up, time to wrap it up!&lt;/p&gt;




&lt;p&gt;Thanks for reading Bit Maybe Wise! &lt;code&gt;object.my_method(2)&lt;/code&gt; now returns &lt;code&gt;4&lt;/code&gt;. Your inbox could return something good too -- &lt;a href="https://bitmaybewise.com/" rel="noopener noreferrer"&gt;subscribe&lt;/a&gt; and square up.&lt;/p&gt;

&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@waldemarbrandt67w" rel="noopener noreferrer"&gt;Waldemar Brandt&lt;/a&gt; on &lt;a href="https://unsplash.com" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tsonnet</category>
      <category>jsonnet</category>
      <category>compiler</category>
    </item>
    <item>
      <title>Tsonnet #38 - Call me maybe, but make it typed, part 4</title>
      <dc:creator>Hercules Lemke Merscher</dc:creator>
      <pubDate>Tue, 21 Apr 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/bitmaybewise/tsonnet-38-call-me-maybe-but-make-it-typed-part-3-2564</link>
      <guid>https://dev.to/bitmaybewise/tsonnet-38-call-me-maybe-but-make-it-typed-part-3-2564</guid>
      <description>&lt;p&gt;Welcome to the &lt;a href="https://gitlab.com/bitmaybewise/tsonnet" rel="noopener noreferrer"&gt;Tsonnet&lt;/a&gt; series!&lt;/p&gt;

&lt;p&gt;If you're not following along, check out how it all started in &lt;a href="https://dev.to/bitmaybewise/tsonnet-0-from-zero-to-interpreter-54na"&gt;the first post of the series&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the previous post, we implemented closures — both bound to locals and immediately invoked:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bitmaybewise/tsonnet-37-call-me-maybe-but-make-it-typed-part-3-e4p" class="crayons-story__hidden-navigation-link"&gt;Tsonnet #37 - Call me maybe, but make it typed, part 3&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/bitmaybewise" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" alt="bitmaybewise profile" class="crayons-avatar__image" width="279" height="279"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/bitmaybewise" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Hercules Lemke Merscher
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Hercules Lemke Merscher
                
              
              &lt;div id="story-author-preview-content-3522309" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/bitmaybewise" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" class="crayons-avatar__image" alt="" width="279" height="279"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Hercules Lemke Merscher&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/bitmaybewise/tsonnet-37-call-me-maybe-but-make-it-typed-part-3-e4p" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Apr 19&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/bitmaybewise/tsonnet-37-call-me-maybe-but-make-it-typed-part-3-e4p" id="article-link-3522309"&gt;
          Tsonnet #37 - Call me maybe, but make it typed, part 3
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tsonnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tsonnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/jsonnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;jsonnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/compiler"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;compiler&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/bitmaybewise/tsonnet-37-call-me-maybe-but-make-it-typed-part-3-e4p#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            8 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;I was annoyed by the fact that function type variants were difficult to read, so went on an errand to make it simpler. We can do better!&lt;/p&gt;

&lt;p&gt;Let's see how it turned out.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F05hsrsybfip2sugaq342.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F05hsrsybfip2sugaq342.jpeg" alt="Telephone" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A makeover for the AST
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/ast.ml b/lib/ast.ml
index e317b6e..4b1102e 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/ast.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/ast.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -90,10 +90,9 @@&lt;/span&gt; type expr =
   | Local of position * (string * expr) list
   | Seq of expr list
   | IndexedExpr of position * string * expr
&lt;span class="gd"&gt;-  | FunctionDef of position * (string * (string * expr option) list * expr)
-  | FunctionCall of position * string * expr list
-  | Closure of position * ((string * expr option) list * expr)
-  | ClosureCall of position * (string * expr option) list * expr * expr list
&lt;/span&gt;&lt;span class="gi"&gt;+  | FunctionDef of position * function_def
+  | FunctionCall of position * function_call
+  | Closure of position * closure
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt; and object_entry =
   | ObjectField of string * expr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ClosureCall&lt;/code&gt; is gone. It's basically &lt;code&gt;FunctionCall&lt;/code&gt;, so there's no need to have a duplicated type variant.&lt;/p&gt;

&lt;p&gt;The type variants &lt;code&gt;function_def&lt;/code&gt;, &lt;code&gt;function_call&lt;/code&gt;, and &lt;code&gt;closure&lt;/code&gt; are represented as records, which makes them much easier to reason about since we can have names to represent what they are holding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;@@ -102,6 +101,20 @@&lt;/span&gt; and object_scope =
   | Self
   | TopLevel
   | ObjVarRef of string
&lt;span class="gi"&gt;+
+and function_def = {
+  name: string;
+  params: (string * expr option) list;
+  body: expr;
+}
+and function_call = {
+  callee: expr;
+  args: expr list;
+}
+and closure = {
+  params: (string * expr option) list;
+  body: expr;
+}
&lt;/span&gt; [@@deriving show]
&lt;span class="err"&gt;
&lt;/span&gt; let dummy_expr = Unit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this AST change, it became easier to simplify the parsing rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/parser.mly b/lib/parser.mly
index 8b3821e..e7b7771 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/parser.mly
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/parser.mly
&lt;/span&gt;&lt;span class="p"&gt;@@ -22,6 +22,7 @@&lt;/span&gt;
 %token DOT
 %token SELF TOP_LEVEL_OBJ
 %token PLUS MINUS MULTIPLY DIVIDE MODULO
&lt;span class="gi"&gt;+%nonassoc FUNCTION
&lt;/span&gt; %left PLUS MINUS
 %left MULTIPLY DIVIDE MODULO
 %token &amp;lt;string&amp;gt; ID
&lt;span class="p"&gt;@@ -43,13 +44,11 @@&lt;/span&gt;
 prog:
   | e = expr; EOF { e }
   | e = expr_seq; EOF { e }
&lt;span class="gd"&gt;-  | e = closure; EOF { e }
&lt;/span&gt;   ;
&lt;span class="err"&gt;
&lt;/span&gt; expr:
   | e = assignable_expr { e }
   | e = vars { e }
&lt;span class="gd"&gt;-  | e = closure_call { e }
&lt;/span&gt;   ;
&lt;span class="err"&gt;
&lt;/span&gt; expr_seq:
&lt;span class="p"&gt;@@ -63,6 +62,7 @@&lt;/span&gt; assignable_expr:
   | e = indexed_expr { e }
   | e = obj_field_access { e }
   | e = funcall { e }
&lt;span class="gi"&gt;+  | e = closure { e }
&lt;/span&gt;   ;
&lt;span class="err"&gt;
&lt;/span&gt; indexed_expr:
&lt;span class="p"&gt;@@ -175,7 +175,6 @@&lt;/span&gt; obj_field_access:
&lt;span class="err"&gt;
&lt;/span&gt; var:
   | varname = ID; ASSIGN; e = assignable_expr { (varname, e) }
&lt;span class="gd"&gt;-  | varname = ID; ASSIGN; e = closure { (varname, e) }
&lt;/span&gt;   ;
&lt;span class="err"&gt;
&lt;/span&gt; vars:
&lt;span class="p"&gt;@@ -196,7 +195,7 @@&lt;/span&gt; fundef:
   | fname = ID;
     LEFT_PAREN; params = separated_nonempty_list(COMMA, fundef_param); RIGHT_PAREN;
     ASSIGN;
&lt;span class="gd"&gt;-    body = fundef_body { (fname, params, body) }
&lt;/span&gt;&lt;span class="gi"&gt;+    body = fundef_body { { name = fname; params = params; body = body } }
&lt;/span&gt;   ;
&lt;span class="err"&gt;
&lt;/span&gt; fundef_body:
&lt;span class="p"&gt;@@ -207,20 +206,23 @@&lt;/span&gt; fundef_body:
 funcall:
   | fname = ID;
     LEFT_PAREN; params = separated_nonempty_list(COMMA, assignable_expr); RIGHT_PAREN
&lt;span class="gd"&gt;-    { FunctionCall (with_pos $startpos $endpos, fname, params) }
&lt;/span&gt;&lt;span class="gi"&gt;+    { FunctionCall
+      (with_pos $startpos $endpos, {
+        callee = Ident (with_pos $startpos(fname) $endpos(fname), fname);
+        args = params;
+      })
+    }
+  | callee = scoped_expr;
+    LEFT_PAREN; params = separated_nonempty_list(COMMA, assignable_expr); RIGHT_PAREN
+    { FunctionCall (with_pos $startpos $endpos, { callee = callee; args = params }) }
&lt;/span&gt;   ;
&lt;span class="err"&gt;
&lt;/span&gt; closure:
   | FUNCTION;
     LEFT_PAREN; params = separated_nonempty_list(COMMA, fundef_param); RIGHT_PAREN;
&lt;span class="gd"&gt;-    body = assignable_expr { Closure (with_pos $startpos $endpos, (params, body)) }
-  ;
-
-closure_call:
-  | LEFT_PAREN; FUNCTION;
-    LEFT_PAREN; def_params = separated_nonempty_list(COMMA, fundef_param); RIGHT_PAREN;
-    body = assignable_expr;
-    RIGHT_PAREN;
-    LEFT_PAREN; call_params = separated_nonempty_list(COMMA, assignable_expr); RIGHT_PAREN;
-    { ClosureCall (with_pos $startpos $endpos, def_params, body, call_params) }
&lt;/span&gt;&lt;span class="gi"&gt;+    body = assignable_expr {
+      Closure (with_pos $startpos $endpos, { params = params; body = body })
+    }
+    (* precedence here will transform "function(x) x * x" into "function(x) (x * x)" *)
+    %prec FUNCTION
&lt;/span&gt;   ;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I could finally move &lt;code&gt;closure&lt;/code&gt; out of &lt;code&gt;prog&lt;/code&gt; and &lt;code&gt;expr&lt;/code&gt;. It was never meant to be there. But part of the problem was not having &lt;code&gt;FUNCTION&lt;/code&gt; flagged as non-associative.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interpreter catches up
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;ClosureCall&lt;/code&gt; gone and &lt;code&gt;FunctionCall&lt;/code&gt; now carrying a &lt;code&gt;callee: expr&lt;/code&gt;, the interpreter needed a bit more surgery -- resolving the callee before dispatching to &lt;code&gt;apply_function&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/interpreter.ml b/lib/interpreter.ml
index 7412fb9..15f9932 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/interpreter.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/interpreter.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -27,9 +27,8 @@&lt;/span&gt; let rec interpret env expr =
   | Seq exprs -&amp;gt; interpret_seq env exprs
   | IndexedExpr (pos, varname, index_expr) -&amp;gt; interpret_indexed_expr env (pos, varname, index_expr)
   | FunctionDef (pos, def) -&amp;gt; interpret_function_def env (pos, def)
&lt;span class="gd"&gt;-  | FunctionCall (pos, fname, params) -&amp;gt; interpret_function_call env (pos, fname, params)
&lt;/span&gt;&lt;span class="gi"&gt;+  | FunctionCall (pos, call) -&amp;gt; interpret_function_call env (pos, call)
&lt;/span&gt;   | Closure _ -&amp;gt; ok (env, expr)
&lt;span class="gd"&gt;-  | ClosureCall (pos, def_params, body, params) -&amp;gt; interpret_closure_call env (pos, def_params, body, params)
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt; and interpret_indexed_expr env (pos, varname, index_expr) =
   let* (env', index_expr') = interpret env index_expr in
&lt;span class="p"&gt;@@ -417,8 +416,8 @@&lt;/span&gt; and interpret_ident env pos varname =
     result
   end
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-and interpret_function_def env (pos, (fname, params, body)) =
-  let env' = Env.add_local fname (FunctionDef (pos, (fname, params, body))) env in
&lt;/span&gt;&lt;span class="gi"&gt;+and interpret_function_def env (pos, def) =
+  let env' = Env.add_local def.name (FunctionDef (pos, def)) env in
&lt;/span&gt;   ok (env', Unit)
&lt;span class="err"&gt;
&lt;/span&gt; and apply_function env pos def_params body call_params =
&lt;span class="p"&gt;@@ -468,16 +467,23 @@&lt;/span&gt; and apply_function env pos def_params body call_params =
     in
     ok (env, result)
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-and interpret_function_call env (pos, fname, call_params) =
-  match Env.find_opt fname env with
-  | Some (Closure (pos, (def_params, body)))
-  | Some (FunctionDef (pos, (_, def_params, body))) -&amp;gt;
-    apply_function env pos def_params body call_params
&lt;/span&gt;&lt;span class="gi"&gt;+and interpret_function_call env (pos, call) =
+  let* (env', callee_val) =
+    match call.callee with
+    | Ident (pos, name) -&amp;gt;
+      (match Env.find_opt name env with
+      | Some expr -&amp;gt; ok (env, expr)
+      | None -&amp;gt; Error.error_at pos (Error.Msg.var_not_found name)
+      )
+    | _ -&amp;gt; interpret env call.callee
+  in
+  match callee_val with
+  | Closure (_, closure) -&amp;gt;
+    apply_function env' pos closure.params closure.body call.args
+  | FunctionDef (_, def) -&amp;gt;
+    apply_function env' pos def.params def.body call.args
&lt;/span&gt;   | _ -&amp;gt;
&lt;span class="gd"&gt;-    Error.error_at pos (Error.Msg.var_not_found fname)
-
-and interpret_closure_call env (pos, def_params, body, call_params) =
-  apply_function env pos def_params body call_params
&lt;/span&gt;&lt;span class="gi"&gt;+    Error.error_at pos (Error.Msg.var_not_found (string_of_type call.callee))
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt; let rec deep_eval expr =
   match expr with
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The type checker gets the memo
&lt;/h2&gt;

&lt;p&gt;The type checker can also be improved by using the new type variants as records:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/type.ml b/lib/type.ml
index ee912b3..ad4f333 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/type.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/type.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -17,10 +17,10 @@&lt;/span&gt; type tsonnet_type =
   | TobjectPtr of Env.env_id * t_object_scope
   | Lazy of expr
   | Tunresolved
&lt;span class="gd"&gt;-  | TfunctionDef of (string * tsonnet_type) list (* params: name * type *) * expr (* body *) * tsonnet_type (* return *)
-  | TfunctionCall of tsonnet_type list * tsonnet_type
-  | Tclosure of (string * tsonnet_type) list * expr
-  | TclosureCall of tsonnet_type list * tsonnet_type
&lt;/span&gt;&lt;span class="gi"&gt;+  | TfunctionDef of t_function_def
+  | TfunctionCall of t_function_call
+  | Tclosure of t_closure
+
&lt;/span&gt; and t_object_entry =
   | TobjectField of string * tsonnet_type
   | TobjectExpr of tsonnet_type
&lt;span class="p"&gt;@@ -28,6 +28,20 @@&lt;/span&gt; and t_object_scope =
   | TobjectSelf
   | TobjectTopLevel
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+and t_function_def = {
+  params: (string * tsonnet_type) list;
+  body: expr;
+  return: tsonnet_type;
+}
+and t_function_call = {
+  params: tsonnet_type list;
+  return: tsonnet_type;
+}
+and t_closure = {
+  params: (string * tsonnet_type) list;
+  body: expr;
+}
+
&lt;/span&gt; let rec to_string = function
   | Tunit -&amp;gt; "()"
   | Tnull -&amp;gt; "Null"
&lt;span class="p"&gt;@@ -59,25 +73,21 @@&lt;/span&gt; let rec to_string = function
       | TobjectTopLevel -&amp;gt; "$"
     in Printf.sprintf "%s (%d)" s id
   | Lazy ty -&amp;gt; string_of_type ty
&lt;span class="gd"&gt;-  | TfunctionDef (params, _, return) -&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+  | TfunctionDef {params; return; _} -&amp;gt;
&lt;/span&gt;     Printf.sprintf "function(%s) -&amp;gt; %s"
       (List.map (fun (name, ty) -&amp;gt; name ^ ": " ^ to_string ty) params
       |&amp;gt; String.concat ", "
       )
       (to_string return)
&lt;span class="gd"&gt;-  | TfunctionCall (params_type, return) -&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+  | TfunctionCall {params=params_type; return} -&amp;gt;
&lt;/span&gt;     Printf.sprintf "function(%s) -&amp;gt; %s"
       (List.map to_string params_type |&amp;gt; String.concat ", ")
       (to_string return)
&lt;span class="gd"&gt;-  | Tclosure (params, _) -&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+  | Tclosure {params; _} -&amp;gt;
&lt;/span&gt;     Printf.sprintf "function(%s)"
       (List.map (fun (name, ty) -&amp;gt; name ^ ": " ^ to_string ty) params
       |&amp;gt; String.concat ", "
       )
&lt;span class="gd"&gt;-  | TclosureCall (params_type, return) -&amp;gt;
-    Printf.sprintf "function(%s) -&amp;gt; %s"
-      (List.map to_string params_type |&amp;gt; String.concat ", ")
-      (to_string return)
&lt;/span&gt;   | Tunresolved -&amp;gt; "&amp;lt;unresolved&amp;gt;"
&lt;span class="err"&gt;
&lt;/span&gt; let rec collect_free_idents = function
&lt;span class="p"&gt;@@ -95,10 +105,9 @@&lt;/span&gt; let rec collect_free_idents = function
   | ObjectFieldAccess (_, _, exprs) -&amp;gt; List.concat_map collect_free_idents exprs
   | IndexedExpr (_, name, e) -&amp;gt; name :: collect_free_idents e
   | Local (_, vars) -&amp;gt; List.concat_map (fun (_, e) -&amp;gt; collect_free_idents e) vars
&lt;span class="gd"&gt;-  | FunctionCall (_, name, args) -&amp;gt; name :: List.concat_map collect_free_idents args
-  | Closure (_, (_, body)) -&amp;gt; collect_free_idents body
-  | ClosureCall (_, _, body, args) -&amp;gt;
-    collect_free_idents body @ List.concat_map collect_free_idents args
&lt;/span&gt;&lt;span class="gi"&gt;+  | FunctionCall (_, call) -&amp;gt;
+    collect_free_idents call.callee @ List.concat_map collect_free_idents call.args
+  | Closure (_, closure) -&amp;gt; collect_free_idents closure.body
&lt;/span&gt;   | _ -&amp;gt; []
&lt;span class="err"&gt;
&lt;/span&gt; let reachable_bindings bindings initial_idents =
&lt;span class="p"&gt;@@ -192,10 +201,8 @@&lt;/span&gt; let rec translate venv expr =
   | UnaryOp (pos, op, expr) -&amp;gt; translate_unary_op venv (pos, op, expr)
   | IndexedExpr (pos, varname, index_expr) -&amp;gt; translate_indexed_expr venv (pos, varname, index_expr)
   | FunctionDef (pos, def) -&amp;gt; translate_function_def venv (pos, def)
&lt;span class="gd"&gt;-  | FunctionCall (pos, fname, params) -&amp;gt; translate_function_call venv (pos, fname, params)
-  | Closure (pos, (params, body)) -&amp;gt; translate_closure venv (pos, params, body)
-  | ClosureCall (pos, def_params, body, call_params) -&amp;gt;
-    translate_closure_call venv (pos, def_params, body, call_params)
&lt;/span&gt;&lt;span class="gi"&gt;+  | FunctionCall (pos, call) -&amp;gt; translate_function_call venv (pos, call)
+  | Closure (pos, closure) -&amp;gt; translate_closure venv (pos, closure)
&lt;/span&gt;   | expr' -&amp;gt;
     error (Error.Msg.type_invalid_expr (string_of_type expr')) 
&lt;span class="p"&gt;@@ -493,7 +500,7 @@&lt;/span&gt; and translate_bin_op venv pos op e1 e2 =
   | _, Tany, _ | _, _, Tany -&amp;gt; ok (venv'', Tany)
   | _ -&amp;gt; Error.error_at pos Error.Msg.invalid_binary_op
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-and translate_function_def venv (pos, (fun_name, params, body)) =
&lt;/span&gt;&lt;span class="gi"&gt;+and translate_function_def venv (pos, def) =
&lt;/span&gt;   (* For params with defaults, we can infer the type from the default expression;
      params without defaults remain Tunresolved until the first call *)
   let* params_typed = List.fold_left
&lt;span class="p"&gt;@@ -507,36 +514,57 @@&lt;/span&gt; and translate_function_def venv (pos, (fun_name, params, body)) =
         ok (params' @ [(name, Tunresolved)])
     )
     (ok [])
&lt;span class="gd"&gt;-    params
&lt;/span&gt;&lt;span class="gi"&gt;+    def.params
+  in
+  let fun_def = TfunctionDef
+    { params = params_typed;
+      body = def.body;
+      return = Tunresolved;
+    }
&lt;/span&gt;   in
&lt;span class="gd"&gt;-  let fun_def = TfunctionDef (params_typed, body, Tunresolved) in
&lt;/span&gt;   (* So, function declaration will have an unresolved type definition,
      that only later it will be translated: before function call translation!
      After first function call, concrete types are set and subsequent calls will
      type check against the initial type assignment(s). *)
&lt;span class="gd"&gt;-  let venv' = Env.add_local fun_name fun_def venv in
&lt;/span&gt;&lt;span class="gi"&gt;+  let venv' = Env.add_local def.name fun_def venv in
&lt;/span&gt;   ok (venv', fun_def)
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-and translate_function_call venv (pos, fname, call_params) =
-  (* 1. retrieve TfunctionDef from venv *)
-  match Env.find_opt fname venv with
-  | Some (TfunctionDef (def_params, body_expr, return_type)) -&amp;gt;
-    (* check arity: allow fewer args if defaults exist *)
-    let num_call = List.length call_params in
&lt;/span&gt;&lt;span class="gi"&gt;+and translate_function_call venv (pos, call) =
+  match call.callee with
+  | Ident (_, name) -&amp;gt;
+    translate_named_function_call venv (pos, name, call.args)
+  | Closure (_, closure) -&amp;gt;
+    translate_closure_call venv (pos, closure.params, closure.body, call.args)
+  | _ -&amp;gt;
+    let* (venv', callee_ty) = translate venv call.callee in
+    (match callee_ty with
+    | Tclosure { params = closure_params; body = body_expr } -&amp;gt;
+      let def_params = List.map (fun (name, _ty) -&amp;gt; (name, None)) closure_params in
+      translate_closure_call venv' (pos, def_params, body_expr, call.args)
+    | _ -&amp;gt;
+      Error.error_at pos (Error.Msg.type_invalid_expr (string_of_type call.callee))
+    )
+
+and translate_named_function_call venv (pos, name, args) =
+  match Env.find_opt name venv with
+  | Some (TfunctionDef { params = def_params;
+                         body = body_expr;
+                         return = return_type;
+                       }) -&amp;gt;
+    let num_call = List.length args in
&lt;/span&gt;     let num_def = List.length def_params in
     if num_call &amp;gt; num_def
     then
       Error.error_at pos
         (Error.Msg.wrong_number_of_params num_def num_call)
     else
&lt;span class="gd"&gt;-      (* 2. type check each positional parameter passed in the function call *)
&lt;/span&gt;       let* (venv', resolved_params) =
         List.fold_left
           (fun acc (index, (param_name, def_param_type)) -&amp;gt;
             let* (venv', params') = acc in
             if index &amp;lt; num_call
             then
&lt;span class="gd"&gt;-              let call_param = List.nth call_params index in
&lt;/span&gt;&lt;span class="gi"&gt;+              let call_param = List.nth args index in
&lt;/span&gt;               let* (venv'', call_param_type) = translate venv' call_param in
               match def_param_type with
               | Tunresolved -&amp;gt;
&lt;span class="p"&gt;@@ -549,26 +577,22 @@&lt;/span&gt; and translate_function_call venv (pos, fname, call_params) =
                     ~expected:(to_string expected)
                     ~got:(to_string call_param_type))
             else
&lt;span class="gd"&gt;-              (* default arg — resolve type from the original AST default expression *)
&lt;/span&gt;               ok (venv', params' @ [(param_name, def_param_type)])
           )
           (ok (venv, []))
           (List.mapi (fun i p -&amp;gt; (i, p)) def_params)
       in
&lt;span class="gd"&gt;-      (* 3. type check return *)
&lt;/span&gt;       let body_venv = List.fold_left
         (fun env (name, ty) -&amp;gt; Env.add_local name ty env)
         venv'
         resolved_params
       in
&lt;span class="gd"&gt;-      (* translate the body with resolved param types in scope *)
&lt;/span&gt;       let* (_, body_type) = translate body_venv body_expr in
&lt;span class="gd"&gt;-      let* resolved_return = match return_type with
&lt;/span&gt;&lt;span class="gi"&gt;+      let* resolved_return =
+        match return_type with
&lt;/span&gt;         | Tunresolved -&amp;gt;
&lt;span class="gd"&gt;-          (* 3a. first call: infer return type from body *)
&lt;/span&gt;           ok body_type
         | expected -&amp;gt;
&lt;span class="gd"&gt;-          (* 3b. subsequent calls: check body type matches *)
&lt;/span&gt;           if body_type = expected
           then ok expected
           else Error.error_at pos
&lt;span class="p"&gt;@@ -576,23 +600,26 @@&lt;/span&gt; and translate_function_call venv (pos, fname, call_params) =
               ~expected:(to_string expected)
               ~got:(to_string body_type))
       in
&lt;span class="gd"&gt;-      (* 4. update env with the now-resolved function type *)
-      let resolved_fun = TfunctionDef (resolved_params, body_expr, resolved_return) in
-      let venv_with_resolved_fun = Env.add_local fname resolved_fun venv' in
&lt;/span&gt;&lt;span class="gi"&gt;+      let resolved_fun =
+        TfunctionDef {
+          params = resolved_params;
+          body = body_expr;
+          return = resolved_return;
+        }
+      in
+      let venv_with_resolved_fun = Env.add_local name resolved_fun venv' in
&lt;/span&gt;       ok (venv_with_resolved_fun, resolved_return)
   | Some (Lazy expr) -&amp;gt;
     let* (venv', resolved) = translate venv expr in
&lt;span class="gd"&gt;-    (* Re-dispatch with the resolved type *)
-    let venv'' = Env.add_local fname resolved venv' in
-    translate_function_call venv'' (pos, fname, call_params)
-  | Some (Tclosure (def_params, body_expr)) -&amp;gt;
-    translate_closure_call venv (pos, 
-      List.map (fun (name, _ty) -&amp;gt; (name, None)) def_params,
-      body_expr, call_params)
&lt;/span&gt;&lt;span class="gi"&gt;+    let venv'' = Env.add_local name resolved venv' in
+    translate_named_function_call venv'' (pos, name, args)
+  | Some (Tclosure { params = closure_params; body = body_expr }) -&amp;gt;
+    let def_params = List.map (fun (name, _ty) -&amp;gt; (name, None)) closure_params in
+    translate_closure_call venv (pos, def_params, body_expr, args)
&lt;/span&gt;   | _ -&amp;gt;
&lt;span class="gd"&gt;-    Error.error_at pos (Error.Msg.var_not_found fname)
&lt;/span&gt;&lt;span class="gi"&gt;+    Error.error_at pos (Error.Msg.var_not_found name)
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-and translate_closure venv (_pos, params, body) =
&lt;/span&gt;&lt;span class="gi"&gt;+and translate_closure venv (_pos, closure) =
&lt;/span&gt;   let* params_typed = List.fold_left
     (fun acc (name, default) -&amp;gt;
       let* params' = acc in
&lt;span class="p"&gt;@@ -604,11 +631,11 @@&lt;/span&gt; and translate_closure venv (_pos, params, body) =
         ok (params' @ [(name, Tunresolved)])
     )
     (ok [])
&lt;span class="gd"&gt;-    params
&lt;/span&gt;&lt;span class="gi"&gt;+    closure.params
&lt;/span&gt;   in
&lt;span class="gd"&gt;-  ok (venv, Tclosure (params_typed, body))
&lt;/span&gt;&lt;span class="gi"&gt;+  ok (venv, Tclosure { params = params_typed; body = closure.body })
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-and translate_closure_call venv (pos, def_params, body_expr, call_params) =
&lt;/span&gt;&lt;span class="gi"&gt;+and translate_closure_call venv (pos, def_params, body, call_params) =
&lt;/span&gt;   let num_call = List.length call_params in
   let num_def = List.length def_params in
   let num_required =
&lt;span class="p"&gt;@@ -641,7 +668,7 @@&lt;/span&gt; and translate_closure_call venv (pos, def_params, body_expr, call_params) =
       venv'
       resolved_params
     in
&lt;span class="gd"&gt;-    let* (_, body_type) = translate body_venv body_expr in
&lt;/span&gt;&lt;span class="gi"&gt;+    let* (_, body_type) = translate body_venv body in
&lt;/span&gt;     ok (venv, body_type)
&lt;span class="err"&gt;
&lt;/span&gt; let check (config : Config.t) expr  =
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Switching from tuples to named records across the AST, type checker, and interpreter was one of those refactors that feels almost mechanical — until it isn't. The real win was collapsing &lt;code&gt;ClosureCall&lt;/code&gt; into &lt;code&gt;FunctionCall&lt;/code&gt; and generalising the callee to an arbitrary expression. The grammar conflict that made &lt;code&gt;closure&lt;/code&gt; a second-class citizen in the previous post? Gone. A bit of precedence annotation and a small restructuring of &lt;code&gt;funcall&lt;/code&gt; was all it took.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gitlab.com/-/snippets/5979474" rel="noopener noreferrer"&gt;Here&lt;/a&gt; is the entire diff.&lt;/p&gt;

&lt;p&gt;Next up, methods!&lt;/p&gt;




&lt;p&gt;Thanks for reading Bit Maybe Wise! Tuples are out, records are in. Your inbox deserves named fields too — &lt;a href="https://bitmaybewise.com/" rel="noopener noreferrer"&gt;subscribe&lt;/a&gt; and stop guessing what position two holds.&lt;/p&gt;

&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@quinoal" rel="noopener noreferrer"&gt;Quino Al&lt;/a&gt; on &lt;a href="https://unsplash.com" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tsonnet</category>
      <category>jsonnet</category>
      <category>compiler</category>
    </item>
    <item>
      <title>Tsonnet #37 - Call me maybe, but make it typed, part 3</title>
      <dc:creator>Hercules Lemke Merscher</dc:creator>
      <pubDate>Sun, 19 Apr 2026 10:40:23 +0000</pubDate>
      <link>https://dev.to/bitmaybewise/tsonnet-37-call-me-maybe-but-make-it-typed-part-3-e4p</link>
      <guid>https://dev.to/bitmaybewise/tsonnet-37-call-me-maybe-but-make-it-typed-part-3-e4p</guid>
      <description>&lt;p&gt;Welcome to the &lt;a href="https://gitlab.com/bitmaybewise/tsonnet" rel="noopener noreferrer"&gt;Tsonnet&lt;/a&gt; series!&lt;/p&gt;

&lt;p&gt;If you're not following along, check out how it all started in &lt;a href="https://dev.to/bitmaybewise/tsonnet-0-from-zero-to-interpreter-54na"&gt;the first post of the series&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the previous post, we added default function arguments:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bitmaybewise/tsonnet-36-call-me-maybe-but-make-it-typed-part-2-18fn" class="crayons-story__hidden-navigation-link"&gt;Tsonnet #36 — Call me maybe, but make it typed, part 2&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/bitmaybewise" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" alt="bitmaybewise profile" class="crayons-avatar__image" width="279" height="279"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/bitmaybewise" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Hercules Lemke Merscher
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Hercules Lemke Merscher
                
              
              &lt;div id="story-author-preview-content-3512061" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/bitmaybewise" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" class="crayons-avatar__image" alt="" width="279" height="279"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Hercules Lemke Merscher&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/bitmaybewise/tsonnet-36-call-me-maybe-but-make-it-typed-part-2-18fn" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Apr 17&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/bitmaybewise/tsonnet-36-call-me-maybe-but-make-it-typed-part-2-18fn" id="article-link-3512061"&gt;
          Tsonnet #36 — Call me maybe, but make it typed, part 2
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tsonnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tsonnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/jsonnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;jsonnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/compiler"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;compiler&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/bitmaybewise/tsonnet-36-call-me-maybe-but-make-it-typed-part-2-18fn#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;Now let's implement &lt;strong&gt;closures&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frtwv36viik628amgtwxn.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frtwv36viik628amgtwxn.jpeg" alt="Telephone cabins" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Captured and ready to be called
&lt;/h2&gt;

&lt;p&gt;We should accept a closure that is going to be assigned to a local, and immediately called closures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/functions/closure.jsonnet&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;binding&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;closure&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;function(x)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;x;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;immediately&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;called&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;closure&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;(function(x)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;id(x)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;id(x))(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Parsing: teaching the grammar new tricks
&lt;/h2&gt;

&lt;p&gt;We need two new AST variants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/ast.ml b/lib/ast.ml
index f4c1472..e317b6e 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/ast.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/ast.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -92,6 +92,8 @@&lt;/span&gt; type expr =
   | IndexedExpr of position * string * expr
   | FunctionDef of position * (string * (string * expr option) list * expr)
   | FunctionCall of position * string * expr list
&lt;span class="gi"&gt;+  | Closure of position * ((string * expr option) list * expr)
+  | ClosureCall of position * (string * expr option) list * expr * expr list
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt; and object_entry =
   | ObjectField of string * expr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we update the lexer to recognise the new keyword:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/lexer.mll b/lib/lexer.mll
index a4c3745..4d65f55 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/lexer.mll
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/lexer.mll
&lt;/span&gt;&lt;span class="p"&gt;@@ -80,6 +80,7 @@&lt;/span&gt; rule read =
   | "self" { SELF }
   | "$" { TOP_LEVEL_OBJ }
   | "in" { IN }
&lt;span class="gi"&gt;+  | "function" { FUNCTION }
&lt;/span&gt;   | id { ID (Lexing.lexeme lexbuf) }
   | _ { raise (SyntaxError ("Unexpected char: " ^ Lexing.lexeme lexbuf)) }
   | eof { EOF }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the parser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/parser.mly b/lib/parser.mly
index 290b280..8b3821e 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/parser.mly
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/parser.mly
&lt;/span&gt;&lt;span class="p"&gt;@@ -13,6 +13,7 @@&lt;/span&gt;
 %token NULL
 %token &amp;lt;bool&amp;gt; BOOL
 %token &amp;lt;string&amp;gt; STRING
&lt;span class="gi"&gt;+%token FUNCTION
&lt;/span&gt; %token LEFT_SQR_BRACKET RIGHT_SQR_BRACKET
 %token LEFT_PAREN RIGHT_PAREN
 %token COMMA
&lt;span class="p"&gt;@@ -42,11 +43,13 @@&lt;/span&gt;
 prog:
   | e = expr; EOF { e }
   | e = expr_seq; EOF { e }
&lt;span class="gi"&gt;+  | e = closure; EOF { e }
&lt;/span&gt;   ;
&lt;span class="err"&gt;
&lt;/span&gt; expr:
   | e = assignable_expr { e }
   | e = vars { e }
&lt;span class="gi"&gt;+  | e = closure_call { e }
&lt;/span&gt;   ;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I stumbled upon a shift-reduce issue trying to make closure part of the &lt;code&gt;assignable_expr&lt;/code&gt; parsing rule — it would make more semantic sense there, but the grammar conflicts beat me. I'll come back to fix it later.&lt;/p&gt;

&lt;p&gt;Here's the short version of why it's not feasible as-is: if &lt;code&gt;closure&lt;/code&gt; lives in &lt;code&gt;assignable_expr&lt;/code&gt;, then &lt;code&gt;(function(x) x)&lt;/code&gt; becomes reachable via two paths simultaneously — as a parenthesised closure via &lt;code&gt;scoped_expr&lt;/code&gt;, and as the first half of a &lt;code&gt;closure_call&lt;/code&gt;. When the parser sees &lt;code&gt;( function(x) body . )&lt;/code&gt; and hits &lt;code&gt;)&lt;/code&gt;, it can't decide whether to reduce into &lt;code&gt;scoped_expr&lt;/code&gt; or shift &lt;code&gt;)&lt;/code&gt; as part of &lt;code&gt;closure_call&lt;/code&gt; — that would require two tokens of lookahead, which LR(1) doesn't have. The only clean fix would be generalising &lt;code&gt;funcall&lt;/code&gt; to accept any expression as the callee, so &lt;code&gt;(function(x) x)(5)&lt;/code&gt; parses naturally as "call this expression with these args" — but that's a bigger refactor touching the AST, type checker, and interpreter.&lt;/p&gt;

&lt;p&gt;Here's how the new &lt;code&gt;closure&lt;/code&gt; and &lt;code&gt;closure_call&lt;/code&gt; rules are implemented for now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;@@ -171,7 +174,9 @@&lt;/span&gt; obj_field_access:
   ;
&lt;span class="err"&gt;
&lt;/span&gt; var:
&lt;span class="gd"&gt;-  varname = ID; ASSIGN; e = assignable_expr { (varname, e) };
&lt;/span&gt;&lt;span class="gi"&gt;+  | varname = ID; ASSIGN; e = assignable_expr { (varname, e) }
+  | varname = ID; ASSIGN; e = closure { (varname, e) }
+  ;
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt; vars:
   | LOCAL; vars = separated_nonempty_list(COMMA, var) { Local (with_pos $startpos $endpos, vars) }
&lt;span class="p"&gt;@@ -203,4 +208,19 @@&lt;/span&gt; funcall:
   | fname = ID;
     LEFT_PAREN; params = separated_nonempty_list(COMMA, assignable_expr); RIGHT_PAREN
     { FunctionCall (with_pos $startpos $endpos, fname, params) }
&lt;span class="gi"&gt;+  ;
+
+closure:
+  | FUNCTION;
+    LEFT_PAREN; params = separated_nonempty_list(COMMA, fundef_param); RIGHT_PAREN;
+    body = assignable_expr { Closure (with_pos $startpos $endpos, (params, body)) }
+  ;
+
+closure_call:
+  | LEFT_PAREN; FUNCTION;
+    LEFT_PAREN; def_params = separated_nonempty_list(COMMA, fundef_param); RIGHT_PAREN;
+    body = assignable_expr;
+    RIGHT_PAREN;
+    LEFT_PAREN; call_params = separated_nonempty_list(COMMA, assignable_expr); RIGHT_PAREN;
+    { ClosureCall (with_pos $startpos $endpos, def_params, body, call_params) }
&lt;/span&gt;   ;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Type checking: the elephant in the room
&lt;/h2&gt;

&lt;p&gt;The new type variants mirror the ones in &lt;code&gt;ast.ml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/type.ml b/lib/type.ml
index 05bc187..ee912b3 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/type.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/type.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -19,6 +19,8 @@&lt;/span&gt; type tsonnet_type =
   | Tunresolved
   | TfunctionDef of (string * tsonnet_type) list (* params: name * type *) * expr (* body *) * tsonnet_type (* return *)
   | TfunctionCall of tsonnet_type list * tsonnet_type
&lt;span class="gi"&gt;+  | Tclosure of (string * tsonnet_type) list * expr
+  | TclosureCall of tsonnet_type list * tsonnet_type
&lt;/span&gt; and t_object_entry =
   | TobjectField of string * tsonnet_type
   | TobjectExpr of tsonnet_type
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;collect_free_idents&lt;/code&gt; function needs updating for the new variants — I also noticed I'd forgotten &lt;code&gt;FunctionCall&lt;/code&gt; here earlier (oops!):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;@@ -84,6 +95,10 @@&lt;/span&gt; let rec collect_free_idents = function
   | ObjectFieldAccess (_, _, exprs) -&amp;gt; List.concat_map collect_free_idents exprs
   | IndexedExpr (_, name, e) -&amp;gt; name :: collect_free_idents e
   | Local (_, vars) -&amp;gt; List.concat_map (fun (_, e) -&amp;gt; collect_free_idents e) vars
&lt;span class="gi"&gt;+  | FunctionCall (_, name, args) -&amp;gt; name :: List.concat_map collect_free_idents args
+  | Closure (_, (_, body)) -&amp;gt; collect_free_idents body
+  | ClosureCall (_, _, body, args) -&amp;gt;
+    collect_free_idents body @ List.concat_map collect_free_idents args
&lt;/span&gt;   | _ -&amp;gt; []
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we pattern-match on the new AST nodes in &lt;code&gt;translate&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;@@ -178,6 +193,9 @@&lt;/span&gt; let rec translate venv expr =
   | IndexedExpr (pos, varname, index_expr) -&amp;gt; translate_indexed_expr venv (pos, varname, index_expr)
   | FunctionDef (pos, def) -&amp;gt; translate_function_def venv (pos, def)
   | FunctionCall (pos, fname, params) -&amp;gt; translate_function_call venv (pos, fname, params)
&lt;span class="gi"&gt;+  | Closure (pos, (params, body)) -&amp;gt; translate_closure venv (pos, params, body)
+  | ClosureCall (pos, def_params, body, call_params) -&amp;gt;
+    translate_closure_call venv (pos, def_params, body, call_params)
&lt;/span&gt;   | expr' -&amp;gt;
     error (Error.Msg.type_invalid_expr (string_of_type expr'))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also added a catch-all for &lt;code&gt;Tany&lt;/code&gt; in binary operations — if either side is &lt;code&gt;Tany&lt;/code&gt;, we let it through rather than erroring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;@@ -472,6 +490,7 @@&lt;/span&gt; and translate_bin_op venv pos op e1 e2 =
   | LessThan, Tstring, Tstring -&amp;gt; ok (venv'', Tbool)
   | LessThanOrEqual, Tstring, Tstring -&amp;gt; ok (venv'', Tbool)
   | In, Tstring, (Tobject _ | Tany | TruntimeObject _ | TobjectPtr _) -&amp;gt; ok (venv'', Tbool)
&lt;span class="gi"&gt;+  | _, Tany, _ | _, _, Tany -&amp;gt; ok (venv'', Tany)
&lt;/span&gt;   | _ -&amp;gt; Error.error_at pos Error.Msg.invalid_binary_op
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;translate_function_call&lt;/code&gt; also needs to handle &lt;code&gt;Tclosure&lt;/code&gt; — if a bound name resolves to a closure, we re-dispatch to &lt;code&gt;translate_closure_call&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;@@ -561,9 +580,70 @@&lt;/span&gt; and translate_function_call venv (pos, fname, call_params) =
       let resolved_fun = TfunctionDef (resolved_params, body_expr, resolved_return) in
       let venv_with_resolved_fun = Env.add_local fname resolved_fun venv' in
       ok (venv_with_resolved_fun, resolved_return)
&lt;span class="gi"&gt;+  | Some (Lazy expr) -&amp;gt;
+    let* (venv', resolved) = translate venv expr in
+    (* Re-dispatch with the resolved type *)
+    let venv'' = Env.add_local fname resolved venv' in
+    translate_function_call venv'' (pos, fname, call_params)
+  | Some (Tclosure (def_params, body_expr)) -&amp;gt;
+    translate_closure_call venv (pos, 
+      List.map (fun (name, _ty) -&amp;gt; (name, None)) def_params,
+      body_expr, call_params)
&lt;/span&gt;   | _ -&amp;gt;
     Error.error_at pos (Error.Msg.var_not_found fname)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;translate_closure&lt;/code&gt; is pretty similar to &lt;code&gt;translate_function_def&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ocaml"&gt;&lt;code&gt;&lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;translate_closure&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_pos&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;params_typed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fold_left&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;params'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt; &lt;span class="n"&gt;default_expr&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_ty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;translate&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt; &lt;span class="n"&gt;default_expr&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params'&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_ty&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params'&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Tunresolved&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="bp"&gt;[]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;
  &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;venv&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Tclosure&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params_typed&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;translate_closure_call&lt;/code&gt; is pretty similar to &lt;code&gt;translate_function_call&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ocaml"&gt;&lt;code&gt;&lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;translate_closure_call&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;def_params&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body_expr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;num_call&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="n"&gt;call_params&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;num_def&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="n"&gt;def_params&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;num_required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_none&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;def_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;num_call&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;num_required&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;num_call&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;num_def&lt;/span&gt;
  &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_at&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wrong_number_of_params&lt;/span&gt; &lt;span class="n"&gt;num_def&lt;/span&gt; &lt;span class="n"&gt;num_call&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;venv'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resolved_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fold_left&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;param_name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
          &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;venv'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
          &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;num_call&lt;/span&gt;
          &lt;span class="k"&gt;then&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;call_param&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nth&lt;/span&gt; &lt;span class="n"&gt;call_params&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;venv''&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_param_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;translate&lt;/span&gt; &lt;span class="n"&gt;venv'&lt;/span&gt; &lt;span class="n"&gt;call_param&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;venv''&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params'&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;param_name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_param_type&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
          &lt;span class="k"&gt;else&lt;/span&gt;
            &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
            &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt; &lt;span class="n"&gt;default_expr&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
              &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;venv''&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;translate&lt;/span&gt; &lt;span class="n"&gt;venv'&lt;/span&gt; &lt;span class="n"&gt;default_expr&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
              &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;venv''&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params'&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;param_name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_type&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
            &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_at&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wrong_number_of_params&lt;/span&gt; &lt;span class="n"&gt;num_def&lt;/span&gt; &lt;span class="n"&gt;num_call&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;venv&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;[]&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mapi&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;def_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;body_venv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fold_left&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_local&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="n"&gt;ty&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;venv'&lt;/span&gt;
      &lt;span class="n"&gt;resolved_params&lt;/span&gt;
    &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;translate&lt;/span&gt; &lt;span class="n"&gt;body_venv&lt;/span&gt; &lt;span class="n"&gt;body_expr&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;venv&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's room for simplification here, but let's move on for now.&lt;/p&gt;

&lt;p&gt;It's worth pausing on a behaviour difference between Tsonnet and standard Jsonnet. Given this sample:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/semantics/invalid_function_call_type.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;my_function(x)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;my_function(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;my_function(&lt;/span&gt;&lt;span class="s2"&gt;"oops"&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jsonnet fails at runtime after evaluating the first call, because &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; expects a boolean and gets a number first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;jsonnet samples/semantics/invalid_function_call_type.jsonnet
RUNTIME ERROR: Unexpected &lt;span class="nb"&gt;type &lt;/span&gt;number, expected boolean
    samples/semantics/invalid_function_call_type.jsonnet:2:1-38 &lt;span class="err"&gt;$&lt;/span&gt;
    During evaluation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tsonnet instead type-checks the function calls before even getting to the &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, and catches the type mismatch earlier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dune &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; tsonnet samples/semantics/invalid_function_call_type.jsonnet
ERROR: samples/semantics/invalid_function_call_type.jsonnet:2:18 Expected &lt;span class="nb"&gt;type &lt;/span&gt;Number, got String

2: my_function&lt;span class="o"&gt;(&lt;/span&gt;3&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; my_function&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"oops"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I think that's a more sensible error. Worth noting: I want to support polymorphic parameters and return types eventually, but for this prototype, this is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interpreting: where closures actually close over things
&lt;/h2&gt;

&lt;p&gt;During interpretation, a closure definition is returned as-is — it just gets stored in the environment. The actual work happens at call time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/interpreter.ml b/lib/interpreter.ml
index 6ad259e..7412fb9 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/interpreter.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/interpreter.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -28,6 +28,8 @@&lt;/span&gt; let rec interpret env expr =
   | IndexedExpr (pos, varname, index_expr) -&amp;gt; interpret_indexed_expr env (pos, varname, index_expr)
   | FunctionDef (pos, def) -&amp;gt; interpret_function_def env (pos, def)
   | FunctionCall (pos, fname, params) -&amp;gt; interpret_function_call env (pos, fname, params)
&lt;span class="gi"&gt;+  | Closure _ -&amp;gt; ok (env, expr)
+  | ClosureCall (pos, def_params, body, params) -&amp;gt; interpret_closure_call env (pos, def_params, body, params)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function application logic was straightforward to generalise into &lt;code&gt;apply_function&lt;/code&gt;, which both &lt;code&gt;interpret_function_call&lt;/code&gt; and &lt;code&gt;interpret_closure_call&lt;/code&gt; now share:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ocaml"&gt;&lt;code&gt;&lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;apply_function&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="n"&gt;def_params&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="n"&gt;call_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;num_call&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="n"&gt;call_params&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;num_def&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="n"&gt;def_params&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;num_required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_none&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;def_params&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;num_call&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;num_required&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;num_call&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;num_def&lt;/span&gt;
  &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_at&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wrong_number_of_params&lt;/span&gt; &lt;span class="n"&gt;num_def&lt;/span&gt; &lt;span class="n"&gt;num_call&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;evaluated_call_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fold_left&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
          &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
          &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;interpret&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
          &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="bp"&gt;[]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;call_params&lt;/span&gt;
    &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;bindings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fold_left&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;param_name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
          &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;bindings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
          &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;num_call&lt;/span&gt;
          &lt;span class="k"&gt;then&lt;/span&gt;
            &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bindings&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;param_name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nth&lt;/span&gt; &lt;span class="n"&gt;evaluated_call_params&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
          &lt;span class="k"&gt;else&lt;/span&gt;
            &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
            &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt; &lt;span class="n"&gt;default_expr&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bindings&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;param_name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_expr&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
            &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_at&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wrong_number_of_params&lt;/span&gt; &lt;span class="n"&gt;num_def&lt;/span&gt; &lt;span class="n"&gt;num_call&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="bp"&gt;[]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mapi&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;def_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;env'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fold_left&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_local&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;env&lt;/span&gt;
      &lt;span class="n"&gt;bindings&lt;/span&gt;
    &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;with_fresh_evaluating_bindings&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;interpret&lt;/span&gt; &lt;span class="n"&gt;env'&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;interpret_function_call&lt;/code&gt; and &lt;code&gt;interpret_closure_call&lt;/code&gt; now just delegate to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; and interpret_function_call env (pos, fname, call_params) =
   match Env.find_opt fname env with
&lt;span class="gi"&gt;+  | Some (Closure (pos, (def_params, body)))
&lt;/span&gt;   | Some (FunctionDef (pos, (_, def_params, body))) -&amp;gt;
&lt;span class="gd"&gt;-    ...
&lt;/span&gt;&lt;span class="gi"&gt;+    apply_function env pos def_params body call_params
&lt;/span&gt;   | _ -&amp;gt;
     Error.error_at pos (Error.Msg.var_not_found fname)
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+and interpret_closure_call env (pos, def_params, body, call_params) =
+  apply_function env pos def_params body call_params
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also used this opportunity to fix a leftover hard-coded error string I'd missed during an earlier refactoring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/error.ml b/lib/error.ml
&lt;/span&gt;&lt;span class="gd"&gt;-  let type_wrong_number_of_params expected got =
-    Printf.sprintf "Expected %d argument(s), got %d" expected got
&lt;/span&gt;&lt;span class="gi"&gt;+  let wrong_number_of_params expected got =
+    Printf.sprintf "Expected %d argument(s), got %d" expected got
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rename also moves the message out of the &lt;code&gt;type_&lt;/code&gt;-prefixed block, since both the type checker and the interpreter now share it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does it work? (Yes it does)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;dune &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; tsonnet samples/functions/closure.jsonnet
25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the cram test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/test/cram/functions.t b/test/cram/functions.t
index fa29868..5c77837 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/test/cram/functions.t
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/test/cram/functions.t
&lt;/span&gt;&lt;span class="p"&gt;@@ -6,3 +6,6 @@&lt;/span&gt;
&lt;span class="err"&gt;
&lt;/span&gt;   $ tsonnet ../../samples/functions/default_args.jsonnet
   12
&lt;span class="gi"&gt;+
+  $ tsonnet ../../samples/functions/closure.jsonnet
+  25
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Closures are in. Binding them to locals, calling them immediately, passing them around — it all works. The type checker was the trickier part, requiring extra plumbing to re-dispatch when a bound name turns out to be a closure, but the interpreter side practically wrote itself once &lt;code&gt;apply_function&lt;/code&gt; was factored out. The shift-reduce issue in the parser is still there, nagging at me from the grammar file — but that's a problem for future me, probably involving a refactor to let &lt;code&gt;funcall&lt;/code&gt; accept any expression as callee.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gitlab.com/-/snippets/5979464" rel="noopener noreferrer"&gt;Here&lt;/a&gt; is the entire diff.&lt;/p&gt;

&lt;p&gt;Next up, I think this can be simplified, and I will do so.&lt;/p&gt;




&lt;p&gt;Thanks for reading Bit Maybe Wise! Closures capture their environment. This newsletter captures compilers being built in the open. &lt;a href="https://bitmaybewise.com/" rel="noopener noreferrer"&gt;Subscribe&lt;/a&gt; before the environment gets GC'd.&lt;/p&gt;

&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@thenowtime" rel="noopener noreferrer"&gt;The Now Time&lt;/a&gt; on &lt;a href="https://unsplash.com" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tsonnet</category>
      <category>jsonnet</category>
      <category>compiler</category>
    </item>
    <item>
      <title>Tsonnet #36 — Call me maybe, but make it typed, part 2</title>
      <dc:creator>Hercules Lemke Merscher</dc:creator>
      <pubDate>Fri, 17 Apr 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/bitmaybewise/tsonnet-36-call-me-maybe-but-make-it-typed-part-2-18fn</link>
      <guid>https://dev.to/bitmaybewise/tsonnet-36-call-me-maybe-but-make-it-typed-part-2-18fn</guid>
      <description>&lt;p&gt;Welcome to the &lt;a href="https://gitlab.com/bitmaybewise/tsonnet" rel="noopener noreferrer"&gt;Tsonnet&lt;/a&gt; series!&lt;/p&gt;

&lt;p&gt;If you're not following along, check out how it all started in &lt;a href="https://dev.to/bitmaybewise/tsonnet-0-from-zero-to-interpreter-54na"&gt;the first post of the series&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the previous post, we added basic function support — positional parameters, multiline bodies, and type inference that resolves at call time:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bitmaybewise/tsonnet-35-call-me-maybe-but-make-it-typed-part-1-58i2" class="crayons-story__hidden-navigation-link"&gt;Tsonnet #35 — Call me maybe, but make it typed, part 1&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/bitmaybewise" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" alt="bitmaybewise profile" class="crayons-avatar__image" width="279" height="279"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/bitmaybewise" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Hercules Lemke Merscher
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Hercules Lemke Merscher
                
              
              &lt;div id="story-author-preview-content-3501867" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/bitmaybewise" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" class="crayons-avatar__image" alt="" width="279" height="279"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Hercules Lemke Merscher&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/bitmaybewise/tsonnet-35-call-me-maybe-but-make-it-typed-part-1-58i2" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Apr 15&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/bitmaybewise/tsonnet-35-call-me-maybe-but-make-it-typed-part-1-58i2" id="article-link-3501867"&gt;
          Tsonnet #35 — Call me maybe, but make it typed, part 1
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tsonnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tsonnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/jsonnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;jsonnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/compiler"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;compiler&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/bitmaybewise/tsonnet-35-call-me-maybe-but-make-it-typed-part-1-58i2#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            8 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;Now let's make functions a little more forgiving. In this post, we'll implement &lt;strong&gt;default function arguments&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's on the menu?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/functions/default_args.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;my_function(x,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;y=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;y;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;my_function(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The call passes only one argument. &lt;code&gt;y&lt;/code&gt; falls back to its default value of &lt;code&gt;10&lt;/code&gt;, so the result is &lt;code&gt;12&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Teaching the parser about optionals
&lt;/h2&gt;

&lt;p&gt;The parser needs a small adjustment to accept an optional default expression per parameter. Previously, each parameter was just an &lt;code&gt;ID&lt;/code&gt;. Now it can optionally include &lt;code&gt;= &amp;lt;expr&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/parser.mly b/lib/parser.mly
index 102d202..290b280 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/parser.mly
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/parser.mly
&lt;/span&gt;&lt;span class="p"&gt;@@ -182,9 +182,14 @@&lt;/span&gt; single_var:
   | LOCAL; var_expr = var { Local (with_pos $startpos $endpos, [var_expr]) }
   ;
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+fundef_param:
+  | name = ID { (name, None) }
+  | name = ID; ASSIGN; default = assignable_expr { (name, Some default) }
+  ;
+
&lt;/span&gt; fundef:
   | fname = ID;
&lt;span class="gd"&gt;-    LEFT_PAREN; params = separated_nonempty_list(COMMA, ID); RIGHT_PAREN;
&lt;/span&gt;&lt;span class="gi"&gt;+    LEFT_PAREN; params = separated_nonempty_list(COMMA, fundef_param); RIGHT_PAREN;
&lt;/span&gt;     ASSIGN;
     body = fundef_body { (fname, params, body) }
   ;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the AST type updates to carry the optional default alongside the parameter name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/ast.ml b/lib/ast.ml
index 106f37c..f4c1472 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/ast.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/ast.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -90,7 +90,7 @@&lt;/span&gt; type expr =
   | Local of position * (string * expr) list
   | Seq of expr list
   | IndexedExpr of position * string * expr
&lt;span class="gd"&gt;-  | FunctionDef of position * (string * string list * expr)
&lt;/span&gt;&lt;span class="gi"&gt;+  | FunctionDef of position * (string * (string * expr option) list * expr)
&lt;/span&gt;   | FunctionCall of position * string * expr list
&lt;span class="err"&gt;
&lt;/span&gt; and object_entry =
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each parameter goes from a plain &lt;code&gt;string&lt;/code&gt; to a &lt;code&gt;string * expr option&lt;/code&gt; — &lt;code&gt;None&lt;/code&gt; for required, &lt;code&gt;Some expr&lt;/code&gt; for optional with a default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filling in the blanks
&lt;/h2&gt;

&lt;p&gt;The arity check is a bit more involved now, since we have to distinguish between required and optional parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/interpreter.ml b/lib/interpreter.ml
index d512c0f..6ad259e 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/interpreter.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/interpreter.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -422,16 +422,30 @@&lt;/span&gt; and interpret_function_def env (pos, (fname, params, body)) =
 and interpret_function_call env (pos, fname, call_params) =
   match Env.find_opt fname env with
   | Some (FunctionDef (pos, (_, def_params, body))) -&amp;gt;
&lt;span class="gd"&gt;-    if List.compare_lengths call_params def_params &amp;lt;&amp;gt; 0
&lt;/span&gt;&lt;span class="gi"&gt;+    let num_call = List.length call_params in
+    let num_def = List.length def_params in
+    let num_required =
+      List.length (
+        List.filter (fun (_, default) -&amp;gt; Option.is_none default) def_params
+      )
+    in
+    if num_call &amp;lt; num_required || num_call &amp;gt; num_def
&lt;/span&gt;     then Error.error_at pos "wrong number of param(s)"
     else
&lt;span class="gd"&gt;-      let bindings =
-        List.mapi
-          (fun index value -&amp;gt;
-            let param_name = List.nth def_params index in
-            (param_name, value)
&lt;/span&gt;&lt;span class="gi"&gt;+      let* bindings =
+        List.fold_left
+          (fun acc (index, (param_name, default)) -&amp;gt;
+            let* bindings = acc in
+            if index &amp;lt; num_call
+            then
+              ok (bindings @ [(param_name, List.nth call_params index)])
+            else
+              match default with
+              | Some default_expr -&amp;gt; ok (bindings @ [(param_name, default_expr)])
+              | None -&amp;gt; Error.error_at pos "wrong number of param(s)"
&lt;/span&gt;           )
&lt;span class="gd"&gt;-          call_params
&lt;/span&gt;&lt;span class="gi"&gt;+          (ok [])
+          (List.mapi (fun i p -&amp;gt; (i, p)) def_params)
&lt;/span&gt;       in
       let env' = List.fold_left
         (fun env (k, v) -&amp;gt; Env.add_local k v env)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key change: instead of checking for an exact match, we now compute both the total number of defined parameters (&lt;code&gt;num_def&lt;/code&gt;) and the number of required ones (&lt;code&gt;num_required&lt;/code&gt;). The call is valid if the number of supplied arguments falls anywhere in the range &lt;code&gt;[num_required, num_def]&lt;/code&gt;. For arguments not supplied by the caller, we fall back to the default expression.&lt;/p&gt;

&lt;h2&gt;
  
  
  Knowing the type before the call
&lt;/h2&gt;

&lt;p&gt;Default arguments give the type checker a nice bonus: when a parameter has a default value, we can infer its type right at declaration time instead of waiting for the first call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/type.ml b/lib/type.ml
index bdbf357..05bc187 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/type.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/type.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -475,9 +475,21 @@&lt;/span&gt; and translate_bin_op venv pos op e1 e2 =
   | _ -&amp;gt; Error.error_at pos Error.Msg.invalid_binary_op
&lt;span class="err"&gt;
&lt;/span&gt; and translate_function_def venv (pos, (fun_name, params, body)) =
&lt;span class="gd"&gt;-  (* As of now, we don't know the input types at declaration *)
-  let params_typed = List.map (fun name -&amp;gt; (name, Tunresolved)) params in
-  (* We also don't know the result type *)
&lt;/span&gt;&lt;span class="gi"&gt;+  (* For params with defaults, we can infer the type from the default expression;
+     params without defaults remain Tunresolved until the first call *)
+  let* params_typed = List.fold_left
+    (fun acc (name, default) -&amp;gt;
+      let* params' = acc in
+      match default with
+      | Some default_expr -&amp;gt;
+        let* (_, default_ty) = translate venv default_expr in
+        ok (params' @ [(name, default_ty)])
+      | None -&amp;gt;
+        ok (params' @ [(name, Tunresolved)])
+    )
+    (ok [])
+    params
+  in
&lt;/span&gt;   let fun_def = TfunctionDef (params_typed, body, Tunresolved) in
   (* So, function declaration will have an unresolved type definition,
      that only later it will be translated: before function call translation!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parameters without defaults stay &lt;code&gt;Tunresolved&lt;/code&gt; and get resolved at the call site, same as before. Parameters with defaults get their type resolved immediately. In our example, &lt;code&gt;y=10&lt;/code&gt; means &lt;code&gt;y&lt;/code&gt; is typed as &lt;code&gt;Number&lt;/code&gt; from the moment the function is declared.&lt;/p&gt;

&lt;p&gt;The arity check at the call site follows the same logic as the interpreter — allow fewer arguments than the total, as long as the mandatory ones are all there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;@@ -490,35 +502,39 @@&lt;/span&gt; and translate_function_call venv (pos, fname, call_params) =
   (* 1. retrieve TfunctionDef from venv *)
   match Env.find_opt fname venv with
   | Some (TfunctionDef (def_params, body_expr, return_type)) -&amp;gt;
&lt;span class="gd"&gt;-    (* check arity *)
-    if List.compare_lengths call_params def_params &amp;lt;&amp;gt; 0
&lt;/span&gt;&lt;span class="gi"&gt;+    (* check arity: allow fewer args if defaults exist *)
+    let num_call = List.length call_params in
+    let num_def = List.length def_params in
+    if num_call &amp;gt; num_def
&lt;/span&gt;     then
       Error.error_at pos
&lt;span class="gd"&gt;-        (Error.Msg.type_wrong_number_of_params
-          (List.length def_params) (List.length call_params))
&lt;/span&gt;&lt;span class="gi"&gt;+        (Error.Msg.type_wrong_number_of_params num_def num_call)
&lt;/span&gt;     else
       (* 2. type check each positional parameter passed in the function call *)
       let* (venv', resolved_params) =
&lt;span class="gd"&gt;-        List.fold_left2
-          (fun acc call_param (param_name, def_param_type) -&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+        List.fold_left
+          (fun acc (index, (param_name, def_param_type)) -&amp;gt;
&lt;/span&gt;             let* (venv', params') = acc in
&lt;span class="gd"&gt;-            let* (venv'', call_param_type) = translate venv' call_param in
-            match def_param_type with
-            | Tunresolved -&amp;gt;
-              (* 2a. unresolved: accept and record the concrete type *)
-              ok (venv'', params' @ [(param_name, call_param_type)])
-            | expected -&amp;gt;
-              (* 2b. resolved: type check against the concrete type *)
-              if call_param_type = expected
-              then ok (venv'', params' @ [(param_name, expected)])
-              else Error.error_at pos
-                (Error.Msg.type_mismatch
-                  ~expected:(to_string expected)
-                  ~got:(to_string call_param_type))
&lt;/span&gt;&lt;span class="gi"&gt;+            if index &amp;lt; num_call
+            then
+              let call_param = List.nth call_params index in
+              let* (venv'', call_param_type) = translate venv' call_param in
+              match def_param_type with
+              | Tunresolved -&amp;gt;
+                ok (venv'', params' @ [(param_name, call_param_type)])
+              | expected -&amp;gt;
+                if call_param_type = expected
+                then ok (venv'', params' @ [(param_name, expected)])
+                else Error.error_at pos
+                  (Error.Msg.type_mismatch
+                    ~expected:(to_string expected)
+                    ~got:(to_string call_param_type))
+            else
+              (* default arg — resolve type from the original AST default expression *)
+              ok (venv', params' @ [(param_name, def_param_type)])
&lt;/span&gt;           )
           (ok (venv, []))
&lt;span class="gd"&gt;-          call_params
-          def_params
&lt;/span&gt;&lt;span class="gi"&gt;+          (List.mapi (fun i p -&amp;gt; (i, p)) def_params)
&lt;/span&gt;       in
       (* 3. type check return *)
       let body_venv = List.fold_left
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the caller omits an argument that has a default, we skip the type-checking step for that parameter and carry its already-resolved type forward. No extra work needed — the type was inferred when the function was declared.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment of truth
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/test/cram/functions.t b/test/cram/functions.t
index 0ad7e43..fa29868 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/test/cram/functions.t
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/test/cram/functions.t
&lt;/span&gt;&lt;span class="p"&gt;@@ -3,3 +3,6 @@&lt;/span&gt;
&lt;span class="err"&gt;
&lt;/span&gt;   $ tsonnet ../../samples/functions/multiline.jsonnet
   [ 6, 7 ]
&lt;span class="gi"&gt;+
+  $ tsonnet ../../samples/functions/default_args.jsonnet
+  12
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;my_function(2)&lt;/code&gt; with &lt;code&gt;y=10&lt;/code&gt; gives &lt;code&gt;12&lt;/code&gt;. Working as expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Default arguments are in. With a surprisingly contained change across parser, AST, interpreter, and type checker, &lt;code&gt;my_function(x, y=10)&lt;/code&gt; now works as you'd expect — and the type checker even gets to resolve &lt;code&gt;y&lt;/code&gt;'s type at declaration time rather than waiting for the first call.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gitlab.com/-/snippets/5979461" rel="noopener noreferrer"&gt;Here&lt;/a&gt; is the entire diff.&lt;/p&gt;

&lt;p&gt;Next up, functions get a serious upgrade: closures.&lt;/p&gt;




&lt;p&gt;Thanks for reading Bit Maybe Wise! Default arguments are optional. Subscribing isn't — but I'd never force you. &lt;a href="https://bitmaybewise.com/" rel="noopener noreferrer"&gt;Subscribe&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@comparefibre" rel="noopener noreferrer"&gt;Compare Fibre&lt;/a&gt; on &lt;a href="https://unsplash.com" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tsonnet</category>
      <category>jsonnet</category>
      <category>compiler</category>
    </item>
    <item>
      <title>Tsonnet #35 — Call me maybe, but make it typed, part 1</title>
      <dc:creator>Hercules Lemke Merscher</dc:creator>
      <pubDate>Wed, 15 Apr 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/bitmaybewise/tsonnet-35-call-me-maybe-but-make-it-typed-part-1-58i2</link>
      <guid>https://dev.to/bitmaybewise/tsonnet-35-call-me-maybe-but-make-it-typed-part-1-58i2</guid>
      <description>&lt;p&gt;Welcome to the &lt;a href="https://gitlab.com/bitmaybewise/tsonnet" rel="noopener noreferrer"&gt;Tsonnet&lt;/a&gt; series!&lt;/p&gt;

&lt;p&gt;If you're not following along, check out how it all started in &lt;a href="https://dev.to/bitmaybewise/tsonnet-0-from-zero-to-interpreter-54na"&gt;the first post of the series&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the previous post, we added warnings for unused variables and untouched bindings:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bitmaybewise/tsonnet-34-dabbling-with-untouched-bindings-i5l" class="crayons-story__hidden-navigation-link"&gt;Tsonnet #34 - Dabbling with untouched bindings&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/bitmaybewise" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" alt="bitmaybewise profile" class="crayons-avatar__image" width="279" height="279"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/bitmaybewise" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Hercules Lemke Merscher
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Hercules Lemke Merscher
                
              
              &lt;div id="story-author-preview-content-3355366" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/bitmaybewise" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" class="crayons-avatar__image" alt="" width="279" height="279"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Hercules Lemke Merscher&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/bitmaybewise/tsonnet-34-dabbling-with-untouched-bindings-i5l" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Mar 15&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/bitmaybewise/tsonnet-34-dabbling-with-untouched-bindings-i5l" id="article-link-3355366"&gt;
          Tsonnet #34 - Dabbling with untouched bindings
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tsonnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tsonnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/jsonnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;jsonnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/compiler"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;compiler&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/bitmaybewise/tsonnet-34-dabbling-with-untouched-bindings-i5l#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            14 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;Now let's get to the most exciting feature in any programming language: &lt;strong&gt;functions&lt;/strong&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  What are we even working with here?
&lt;/h2&gt;

&lt;p&gt;A simple inline function with a positional parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/functions/positional_params.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;my_function(x)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;my_function(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a function with a multiline body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/functions/multiline.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;simple_function(x,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;y)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;y;&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;multiline_function(x)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;temp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;temp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;multiline_function(&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;simple_function(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Parsing (a.k.a. making sense of text)
&lt;/h2&gt;

&lt;p&gt;We need two new AST variants -- one for function definition, one for function call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/ast.ml b/lib/ast.ml
index ace6241..106f37c 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/ast.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/ast.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -90,6 +90,8 @@&lt;/span&gt; type expr =
   | Local of position * (string * expr) list
   | Seq of expr list
   | IndexedExpr of position * string * expr
&lt;span class="gi"&gt;+  | FunctionDef of position * (string * string list * expr)
+  | FunctionCall of position * string * expr list
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt; and object_entry =
   | ObjectField of string * expr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the parser rules to match:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/parser.mly b/lib/parser.mly
index 7adb1ee..102d202 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/parser.mly
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/parser.mly
&lt;/span&gt;&lt;span class="p"&gt;@@ -59,6 +59,7 @@&lt;/span&gt; assignable_expr:
   | op = unary_op; e = assignable_expr { UnaryOp (with_pos $startpos $endpos, op, e) }
   | e = indexed_expr { e }
   | e = obj_field_access { e }
&lt;span class="gi"&gt;+  | e = funcall { e }
&lt;/span&gt;   ;
&lt;span class="err"&gt;
&lt;/span&gt; indexed_expr:
&lt;span class="p"&gt;@@ -173,7 +174,28 @@&lt;/span&gt; var:
   varname = ID; ASSIGN; e = assignable_expr { (varname, e) };
&lt;span class="err"&gt;
&lt;/span&gt; vars:
&lt;span class="gd"&gt;-  LOCAL; vars = separated_nonempty_list(COMMA, var) { Local (with_pos $startpos $endpos, vars) };
&lt;/span&gt;&lt;span class="gi"&gt;+  | LOCAL; vars = separated_nonempty_list(COMMA, var) { Local (with_pos $startpos $endpos, vars) }
+  | LOCAL; def = fundef { FunctionDef (with_pos $startpos $endpos, def) }
+  ;
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt; single_var:
&lt;span class="gd"&gt;-  LOCAL; var_expr = var { Local (with_pos $startpos $endpos, [var_expr]) };
&lt;/span&gt;&lt;span class="gi"&gt;+  | LOCAL; var_expr = var { Local (with_pos $startpos $endpos, [var_expr]) }
+  ;
+
+fundef:
+  | fname = ID;
+    LEFT_PAREN; params = separated_nonempty_list(COMMA, ID); RIGHT_PAREN;
+    ASSIGN;
+    body = fundef_body { (fname, params, body) }
+  ;
+
+fundef_body:
+  | e = assignable_expr { e }
+  | local_bindings = vars; SEMICOLON; body = fundef_body { Seq [local_bindings; body] }
+  ;
+
+funcall:
+  | fname = ID;
+    LEFT_PAREN; params = separated_nonempty_list(COMMA, assignable_expr); RIGHT_PAREN
+    { FunctionCall (with_pos $startpos $endpos, fname, params) }
+  ;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fundef_body&lt;/code&gt; deserves a note: it lets us nest &lt;code&gt;local&lt;/code&gt; bindings inside the function body, which is what makes &lt;code&gt;multiline_function&lt;/code&gt; work. A function body is either a plain expression or a local binding followed by a semicolon and the rest of the body, recursively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interpreting: where things actually happen
&lt;/h2&gt;

&lt;p&gt;While working on the interpreter, I noticed that &lt;code&gt;evaluating_fields&lt;/code&gt; was a misleading name — &lt;code&gt;interpret_ident&lt;/code&gt; handles not just object fields, but regular local bindings too. Renamed it to &lt;code&gt;evaluating_bindings&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/interpreter.ml b/lib/interpreter.ml
index c8755bc..d512c0f 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/interpreter.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/interpreter.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -2,7 +2,14 @@&lt;/span&gt; open Ast
 open Result
 open Syntax_sugar
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-let evaluating_fields = ref ObjectFields.empty
&lt;/span&gt;&lt;span class="gi"&gt;+let evaluating_bindings = ref ObjectFields.empty
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also needed a helper to run a function in a fresh evaluation environment, then restore the previous state afterwards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ocaml"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;with_fresh_evaluating_bindings&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;saved_evaluating_bindings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;evaluating_bindings&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="n"&gt;evaluating_bindings&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nn"&gt;ObjectFields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="n"&gt;evaluating_bindings&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;saved_evaluating_bindings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters for function calls: we don't want the caller's evaluation state leaking into the function body.&lt;/p&gt;

&lt;p&gt;The new pattern-matching cases route to their respective handlers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; (** [interpret expr] interprets and reduce the intermediate AST [expr] into a result AST. *)
 let rec interpret env expr =
&lt;span class="p"&gt;@@ -19,6 +26,8 @@&lt;/span&gt; let rec interpret env expr =
   | Local (_, vars) -&amp;gt; interpret_local env vars
   | Seq exprs -&amp;gt; interpret_seq env exprs
   | IndexedExpr (pos, varname, index_expr) -&amp;gt; interpret_indexed_expr env (pos, varname, index_expr)
&lt;span class="gi"&gt;+  | FunctionDef (pos, def) -&amp;gt; interpret_function_def env (pos, def)
+  | FunctionCall (pos, fname, params) -&amp;gt; interpret_function_call env (pos, fname, params)
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt; and interpret_indexed_expr env (pos, varname, index_expr) =
   let* (env', index_expr') = interpret env index_expr in
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Function definition just registers the function in the environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ocaml"&gt;&lt;code&gt;&lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;interpret_function_def&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;env'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_local&lt;/span&gt; &lt;span class="n"&gt;fname&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;FunctionDef&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Function call is where things get interesting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ocaml"&gt;&lt;code&gt;&lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;interpret_function_call&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nn"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find_opt&lt;/span&gt; &lt;span class="n"&gt;fname&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;FunctionDef&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;def_params&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compare_lengths&lt;/span&gt; &lt;span class="n"&gt;call_params&lt;/span&gt; &lt;span class="n"&gt;def_params&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_at&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="s2"&gt;"wrong number of param(s)"&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;bindings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mapi&lt;/span&gt;
          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;param_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nth&lt;/span&gt; &lt;span class="n"&gt;def_params&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;param_name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="n"&gt;call_params&lt;/span&gt;
      &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;env'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fold_left&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_local&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;env&lt;/span&gt;
        &lt;span class="n"&gt;bindings&lt;/span&gt;
      &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;with_fresh_evaluating_bindings&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;interpret&lt;/span&gt; &lt;span class="n"&gt;env'&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_at&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;var_not_found&lt;/span&gt; &lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step by step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check that the number of arguments matches the definition. I could skip this in the interpreter and rely on the type checker alone, but I'm keeping it in both for now -- type checking can be bypassed during development for faster iteration.&lt;/li&gt;
&lt;li&gt;Pair up each call argument with its corresponding parameter name.&lt;/li&gt;
&lt;li&gt;Add those bindings to the environment.&lt;/li&gt;
&lt;li&gt;Interpret the function body in that environment, being careful to return the &lt;em&gt;caller's&lt;/em&gt; environment, not the one modified inside the body.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Making sure this isn’t completely illegal
&lt;/h2&gt;

&lt;p&gt;We don't have type annotations yet, so the type checker needs to infer parameter types and the return type. Here's the motivating example -- the first call is fine, the second should error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;samples/semantics/invalid_function_call_type.jsonnet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;my_function(x)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;my_function(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;my_function(&lt;/span&gt;&lt;span class="s2"&gt;"oops"&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We need three new type variants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/type.ml b/lib/type.ml
index cad9ad1..bdbf357 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/type.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/type.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -16,6 +16,9 @@&lt;/span&gt; type tsonnet_type =
   | TruntimeObject of Env.env_id * t_object_entry list
   | TobjectPtr of Env.env_id * t_object_scope
   | Lazy of expr
&lt;span class="gi"&gt;+  | Tunresolved
+  | TfunctionDef of (string * tsonnet_type) list (* params: name * type *) * expr (* body *) * tsonnet_type (* return *)
+  | TfunctionCall of tsonnet_type list * tsonnet_type
&lt;/span&gt; and t_object_entry =
   | TobjectField of string * tsonnet_type
   | TobjectExpr of tsonnet_type
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;@@ -54,6 +57,17 @@&lt;/span&gt; let rec to_string = function
       | TobjectTopLevel -&amp;gt; "$"
     in Printf.sprintf "%s (%d)" s id
   | Lazy ty -&amp;gt; string_of_type ty
&lt;span class="gi"&gt;+  | TfunctionDef (params, _, return) -&amp;gt;
+    Printf.sprintf "function(%s) -&amp;gt; %s"
+      (List.map (fun (name, ty) -&amp;gt; name ^ ": " ^ to_string ty) params
+      |&amp;gt; String.concat ", "
+      )
+      (to_string return)
+  | TfunctionCall (params_type, return) -&amp;gt;
+    Printf.sprintf "function(%s) -&amp;gt; %s"
+      (List.map to_string params_type |&amp;gt; String.concat ", ")
+      (to_string return)
+  | Tunresolved -&amp;gt; "&amp;lt;unresolved&amp;gt;"
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt; let rec collect_free_idents = function
   | Unit | Null _ | Number _ | String _ | Bool _ -&amp;gt; []
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Tunresolved&lt;/code&gt; is the key one. At declaration time, we don't know what types the parameters will have -- that only becomes clear at the call site. So we use &lt;code&gt;Tunresolved&lt;/code&gt; as a placeholder and fill it in later.&lt;/p&gt;

&lt;p&gt;Two new error messages to go with it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/error.ml b/lib/error.ml
index e287af9..e08c670 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/error.ml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/error.ml
&lt;/span&gt;&lt;span class="p"&gt;@@ -28,6 +28,10 @@&lt;/span&gt; module Msg = struct
   let type_non_indexable_type ty = ty ^ " is a non-indexable type"
   let type_non_indexable_field field = field ^ " is a non-indexable value"
   let type_invalid_lookup_key expr = "Invalid object lookup key: " ^ expr
&lt;span class="gi"&gt;+  let type_wrong_number_of_params expected got =
+    Printf.sprintf "Expected %d argument(s), got %d" expected got
+  let type_mismatch ~expected ~got =
+    Printf.sprintf "Expected type %s, got %s" expected got
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;   (* Interpreter messages *)
   let interp_division_by_zero = "Division by zero"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new cases in &lt;code&gt;translate&lt;/code&gt; forward to specialised functions, same pattern as always:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;@@ -162,6 +176,8 @@&lt;/span&gt; let rec translate venv expr =
   | BinOp (pos, op, e1, e2) -&amp;gt; translate_bin_op venv pos op e1 e2
   | UnaryOp (pos, op, expr) -&amp;gt; translate_unary_op venv (pos, op, expr)
   | IndexedExpr (pos, varname, index_expr) -&amp;gt; translate_indexed_expr venv (pos, varname, index_expr)
&lt;span class="gi"&gt;+  | FunctionDef (pos, def) -&amp;gt; translate_function_def venv (pos, def)
+  | FunctionCall (pos, fname, params) -&amp;gt; translate_function_call venv (pos, fname, params)
&lt;/span&gt;   | expr' -&amp;gt;
     error (Error.Msg.type_invalid_expr (string_of_type expr'))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At declaration time, all types are &lt;code&gt;Tunresolved&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ocaml"&gt;&lt;code&gt;&lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;translate_function_def&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fun_name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="c"&gt;(* As of now, we don't know the input types at declaration *)&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;params_typed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Tunresolved&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="c"&gt;(* We also don't know the result type *)&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;fun_def&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TfunctionDef&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params_typed&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Tunresolved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="c"&gt;(* So, function declaration will have an unresolved type definition,
     that only later it will be translated: before function call translation!
     After first function call, concrete types are set and subsequent calls will
     type check against the initial type assignment(s). *)&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;venv'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_local&lt;/span&gt; &lt;span class="n"&gt;fun_name&lt;/span&gt; &lt;span class="n"&gt;fun_def&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;venv'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fun_def&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At call time, &lt;code&gt;Tunresolved&lt;/code&gt; gets replaced with concrete types -- and subsequent calls are checked against those:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ocaml"&gt;&lt;code&gt;&lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;translate_function_call&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="c"&gt;(* 1. retrieve TfunctionDef from venv *)&lt;/span&gt;
  &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nn"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find_opt&lt;/span&gt; &lt;span class="n"&gt;fname&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TfunctionDef&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;def_params&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body_expr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_type&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;(* check arity *)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compare_lengths&lt;/span&gt; &lt;span class="n"&gt;call_params&lt;/span&gt; &lt;span class="n"&gt;def_params&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;then&lt;/span&gt;
      &lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_at&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type_wrong_number_of_params&lt;/span&gt;
          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="n"&gt;def_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="n"&gt;call_params&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="c"&gt;(* 2. type check each positional parameter passed in the function call *)&lt;/span&gt;
      &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;venv'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resolved_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fold_left2&lt;/span&gt;
          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="n"&gt;call_param&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;param_name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;def_param_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;venv'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;venv''&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_param_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;translate&lt;/span&gt; &lt;span class="n"&gt;venv'&lt;/span&gt; &lt;span class="n"&gt;call_param&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;def_param_type&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
            &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Tunresolved&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
              &lt;span class="c"&gt;(* 2a. unresolved: accept and record the concrete type *)&lt;/span&gt;
              &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;venv''&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params'&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;param_name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_param_type&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
            &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
              &lt;span class="c"&gt;(* 2b. resolved: type check against the concrete type *)&lt;/span&gt;
              &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;call_param_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;
              &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;venv''&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params'&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;param_name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
              &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_at&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type_mismatch&lt;/span&gt;
                  &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_string&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                  &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_string&lt;/span&gt; &lt;span class="n"&gt;call_param_type&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
          &lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;venv&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;[]&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
          &lt;span class="n"&gt;call_params&lt;/span&gt;
          &lt;span class="n"&gt;def_params&lt;/span&gt;
      &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="c"&gt;(* 3. type check return *)&lt;/span&gt;
      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;body_venv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fold_left&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_local&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="n"&gt;ty&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;venv'&lt;/span&gt;
        &lt;span class="n"&gt;resolved_params&lt;/span&gt;
      &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="c"&gt;(* translate the body with resolved param types in scope *)&lt;/span&gt;
      &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;translate&lt;/span&gt; &lt;span class="n"&gt;body_venv&lt;/span&gt; &lt;span class="n"&gt;body_expr&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;resolved_return&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;return_type&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
        &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Tunresolved&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
          &lt;span class="c"&gt;(* 3a. first call: infer return type from body *)&lt;/span&gt;
          &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="n"&gt;body_type&lt;/span&gt;
        &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
          &lt;span class="c"&gt;(* 3b. subsequent calls: check body type matches *)&lt;/span&gt;
          &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;body_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;
          &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;
          &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_at&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type_mismatch&lt;/span&gt;
              &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_string&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_string&lt;/span&gt; &lt;span class="n"&gt;body_type&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="c"&gt;(* 4. update env with the now-resolved function type *)&lt;/span&gt;
      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;resolved_fun&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TfunctionDef&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolved_params&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body_expr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resolved_return&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;venv_with_resolved_fun&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_local&lt;/span&gt; &lt;span class="n"&gt;fname&lt;/span&gt; &lt;span class="n"&gt;resolved_fun&lt;/span&gt; &lt;span class="n"&gt;venv'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;venv_with_resolved_fun&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resolved_return&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_at&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;var_not_found&lt;/span&gt; &lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One limitation worth mentioning: the first call wins. If the first call passes a value with the wrong type for the intended use, the type checker won't catch it -- that's what type annotations are for. They're coming once Tsonnet reaches a reasonable level of Jsonnet compliance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proof that it (mostly) works
&lt;/h2&gt;

&lt;p&gt;Ta-da!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;dune &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; tsonnet samples/functions/positional_params.jsonnet
6

&lt;span class="nv"&gt;$ &lt;/span&gt;dune &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; tsonnet samples/functions/multiline.jsonnet
&lt;span class="o"&gt;[&lt;/span&gt; 6, 7 &lt;span class="o"&gt;]&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;dune &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; tsonnet samples/semantics/invalid_function_call_type.jsonnet
ERROR: samples/semantics/invalid_function_call_type.jsonnet:2:18 Expected &lt;span class="nb"&gt;type &lt;/span&gt;Number, got String

2: my_function&lt;span class="o"&gt;(&lt;/span&gt;3&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; my_function&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"oops"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error could be more precise -- ideally it would point at the offending argument rather than the entire expression. That's a detail I'll get to later; not something that adds much right now.&lt;/p&gt;

&lt;p&gt;The cram tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/test/cram/functions.t b/test/cram/functions.t
&lt;/span&gt;&lt;span class="p"&gt;new file mode 100644
&lt;/span&gt;&lt;span class="gh"&gt;index 0000000..0ad7e43
&lt;/span&gt;&lt;span class="gd"&gt;--- /dev/null
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/test/cram/functions.t
&lt;/span&gt;&lt;span class="p"&gt;@@ -0,0 +1,5 @@&lt;/span&gt;
&lt;span class="gi"&gt;+  $ tsonnet ../../samples/functions/positional_params.jsonnet
+  6
+
+  $ tsonnet ../../samples/functions/multiline.jsonnet
+  [ 6, 7 ]
&lt;/span&gt;&lt;span class="gh"&gt;diff --git a/test/cram/semantics.t b/test/cram/semantics.t
index de60a1b..c9a790e 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/test/cram/semantics.t
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/test/cram/semantics.t
&lt;/span&gt;&lt;span class="p"&gt;@@ -289,3 +322,16 @@&lt;/span&gt;
                  ^^
   ---
   1
&lt;span class="gi"&gt;+
+  $ tsonnet ../../samples/semantics/invalid_function_call_type.jsonnet
+  ERROR: ../../samples/semantics/invalid_function_call_type.jsonnet:2:18 Expected type Number, got String
+  
+  2: my_function(3) &amp;amp;&amp;amp; my_function("oops")
+     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+  [1]
+
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Basic functions are in: positional parameters, multiline bodies, arity checks, and type inference that resolves on the first call. Not bad for a first round.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gitlab.com/-/snippets/5977975" rel="noopener noreferrer"&gt;Here&lt;/a&gt; is the entire diff.&lt;/p&gt;

&lt;p&gt;Next up, we make function calls a little more forgiving — default arguments are coming.&lt;/p&gt;




&lt;p&gt;Thanks for reading Bit Maybe Wise! First caller wins the type. &lt;a href="https://bitmaybewise.com/" rel="noopener noreferrer"&gt;Subscribe&lt;/a&gt; to lock in yours.&lt;/p&gt;

&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@anniespratt" rel="noopener noreferrer"&gt;Annie Spratt&lt;/a&gt; on &lt;a href="https://unsplash.com" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tsonnet</category>
      <category>jsonnet</category>
      <category>compiler</category>
    </item>
  </channel>
</rss>
