<?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.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 #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.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.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"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&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.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.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"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&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.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.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"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&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.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.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"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&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.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.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"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&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.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.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"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&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.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.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"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&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.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.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"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&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.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.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"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&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>
    <item>
      <title>ABEND dump #26</title>
      <dc:creator>Hercules Lemke Merscher</dc:creator>
      <pubDate>Wed, 08 Apr 2026 21:27:00 +0000</pubDate>
      <link>https://dev.to/bitmaybewise/abend-dump-26-47eh</link>
      <guid>https://dev.to/bitmaybewise/abend-dump-26-47eh</guid>
      <description>&lt;p&gt;Welcome to the ABEND dump #26!&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-25-5640" class="crayons-story__hidden-navigation-link"&gt;ABEND dump #25&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.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-3296778" 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.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-25-5640" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Feb 28&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-25-5640" id="article-link-3296778"&gt;
          ABEND dump #25
        &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-25-5640#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"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

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

&lt;/div&gt;





&lt;h2&gt;
  
  
  &lt;a href="https://www.theaustralian.com.au/business/technology/tech-boss-uses-ai-and-chatgpt-to-create-cancer-vaccine-for-his-dying-dog/news-story/292a21bcbe93efa17810bfcfcdfadbf7" rel="noopener noreferrer"&gt;Tech boss uses AI and ChatGPT to create cancer vaccine for his dying dog&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;This is WILD! This is what I want AI for, a truly relevant use case. I feel like there's so much potential for applications in medicine. What a time to be alive! &lt;/p&gt;

&lt;p&gt;I know, I know. There's plenty of reasons to be wary about AI, but let's be positive for a moment, this is truly good and magical!&lt;/p&gt;

&lt;p&gt;And speaking of which...&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://osteosarc.com/" rel="noopener noreferrer"&gt;Explore Sid's Data&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://sytse.com/cancer" rel="noopener noreferrer"&gt;Sid Sijbrandij&lt;/a&gt;, former GitLab CEO, is fighting an osteosarcoma (a rare bone cancer). He's open-sourcing his experimental treatment data to help advancing cancer research. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sid rocks!&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://www.youtube.com/watch?v=aoag03mSuXQ" rel="noopener noreferrer"&gt;The internet was weeks away from disaster and no one knew&lt;/a&gt;
&lt;/h2&gt;

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

&lt;h2&gt;
  
  
  &lt;a href="https://www.technologyreview.com/2026/03/10/1134099/how-pokemon-go-is-helping-robots-deliver-pizza-on-time/" rel="noopener noreferrer"&gt;How Pokémon Go is giving delivery robots an inch-perfect view of the world&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;"When the product is free, you are the product" -- in the AI era, this sentence never made more sense than now.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://blundercheck.timberschroff.com/p/systems-thinking-is-brain-rot-for" rel="noopener noreferrer"&gt;Systems Thinking is Brain Rot for Analysts&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;This got me thinking.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://www.goodreads.com/book/show/59616977-building-a-second-brain" rel="noopener noreferrer"&gt;Building a Second Brain&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;I forgot to recommend this book in the previous issue. &lt;/p&gt;

&lt;p&gt;Last year I got hooked into the &lt;strong&gt;Zettelkasten&lt;/strong&gt; system to organize my thoughts in a digital world, but it was simply too complex to grasp, and what really made it work for me was following the &lt;a href="https://fortelabs.com/blog/para/" rel="noopener noreferrer"&gt;PARA Method&lt;/a&gt; presented by &lt;a href="https://www.goodreads.com/author/show/17177938.Tiago_Forte" rel="noopener noreferrer"&gt;Tiago Forte&lt;/a&gt; in &lt;strong&gt;Building a Second Brain&lt;/strong&gt; book.&lt;/p&gt;

&lt;p&gt;I may write more about it here, eventually.&lt;/p&gt;

&lt;p&gt;There's a version of this book written in &lt;a href="https://www.goodreads.com/book/show/182090676-criando-um-segundo-c-rebro" rel="noopener noreferrer"&gt;portuguese&lt;/a&gt; too, which was the one I read.&lt;/p&gt;




&lt;p&gt;Thanks for reading the ABEND dump! AI curing cancer, robots delivering pizza, open-source treatment data -- what a time to be alive. &lt;a href="https://bitmaybewise.substack.com" rel="noopener noreferrer"&gt;Subscribe&lt;/a&gt; and we'll keep marveling at it together.&lt;/p&gt;

</description>
      <category>abenddump</category>
    </item>
    <item>
      <title>Tsonnet #34 - Dabbling with untouched bindings</title>
      <dc:creator>Hercules Lemke Merscher</dc:creator>
      <pubDate>Sun, 15 Mar 2026 16:26:39 +0000</pubDate>
      <link>https://dev.to/bitmaybewise/tsonnet-34-dabbling-with-untouched-bindings-i5l</link>
      <guid>https://dev.to/bitmaybewise/tsonnet-34-dabbling-with-untouched-bindings-i5l</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 the arithmetic tutorial:&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/bitmaybewise" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&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%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" alt="bitmaybewise"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/bitmaybewise/tsonnet-33-the-arithmetic-tutorial-must-go-on-1694" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Tsonnet #33 - The arithmetic tutorial must go on&lt;/h2&gt;
      &lt;h3&gt;Hercules Lemke Merscher ・ Mar 13&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#tsonnet&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#jsonnet&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#compiler&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


&lt;p&gt;This time, before proceeding to cover the next tutorial, I'm going to revisit one laziness aspect: the evaluation (or no-evaluation) of untouched bindings. &lt;/p&gt;

&lt;h2&gt;
  
  
  Warnings, not errors
&lt;/h2&gt;

&lt;p&gt;Up until now, Tsonnet would hard-error on cyclic references regardless of whether the offending variable was ever touched. That's a bit heavy-handed. Jsonnet is lazily evaluated -- if a variable has a cycle but is never used, the program should still run. We should warn, not panic. For example:&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/variables/untouched_variable.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;a&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="err"&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;=&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="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;b&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jsonnet behaves like this:&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/variables/untouched_variable.jsonnet
42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jsonnet does nothing to alert the programmer that something is unused. Maybe that belongs to a linter, but the compiler not even warning is a bad experience, IMHO.&lt;/p&gt;

&lt;p&gt;The same logic applies to unused variables: they're suspicious, but they shouldn't crash anything.&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/objects/untouched_field.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;result&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="err"&gt;b:&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="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;result.b&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 shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;jsonnet samples/objects/untouched_field.jsonnet
42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's work through each of these in order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Warn on cyclic refs for unused variables
&lt;/h2&gt;

&lt;p&gt;The first thing we need is a way to tell which variables are actually reachable from the body of a &lt;code&gt;local&lt;/code&gt; expression. For that, I added &lt;code&gt;collect_free_idents&lt;/code&gt; and &lt;code&gt;reachable_bindings&lt;/code&gt; to the type checker:&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="k"&gt;rec&lt;/span&gt; &lt;span class="n"&gt;collect_free_idents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;function&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;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;[]&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Ident&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;name&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="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Array&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;exprs&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;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;concat_map&lt;/span&gt; &lt;span class="n"&gt;collect_free_idents&lt;/span&gt; &lt;span class="n"&gt;exprs&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;BinOp&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;_&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e1&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e2&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;collect_free_idents&lt;/span&gt; &lt;span class="n"&gt;e1&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;collect_free_idents&lt;/span&gt; &lt;span class="n"&gt;e2&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;UnaryOp&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;_&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&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;collect_free_idents&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Seq&lt;/span&gt; &lt;span class="n"&gt;exprs&lt;/span&gt; &lt;span class="o"&gt;-&amp;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;concat_map&lt;/span&gt; &lt;span class="n"&gt;collect_free_idents&lt;/span&gt; &lt;span class="n"&gt;exprs&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;ParsedObject&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;entries&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;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;concat_map&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt;
      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;ObjectField&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;e&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;collect_free_idents&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;ObjectExpr&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;collect_free_idents&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&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;_&lt;/span&gt;&lt;span class="o"&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;exprs&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;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;concat_map&lt;/span&gt; &lt;span class="n"&gt;collect_free_idents&lt;/span&gt; &lt;span class="n"&gt;exprs&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;IndexedExpr&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;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&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;name&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="n"&gt;collect_free_idents&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Local&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;vars&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;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;concat_map&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;e&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;collect_free_idents&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;vars&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="bp"&gt;[]&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;reachable_bindings&lt;/span&gt; &lt;span class="n"&gt;bindings&lt;/span&gt; &lt;span class="n"&gt;initial_idents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;rec&lt;/span&gt; &lt;span class="n"&gt;go&lt;/span&gt; &lt;span class="n"&gt;visited&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;
    &lt;span class="o"&gt;|&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;visited&lt;/span&gt;
    &lt;span class="o"&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;rest&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;mem&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="n"&gt;visited&lt;/span&gt;
      &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="n"&gt;go&lt;/span&gt; &lt;span class="n"&gt;visited&lt;/span&gt; &lt;span class="n"&gt;rest&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;new_idents&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;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assoc_opt&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="n"&gt;bindings&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;collect_free_idents&lt;/span&gt; &lt;span class="n"&gt;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="bp"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="n"&gt;go&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;visited&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_idents&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;rest&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;go&lt;/span&gt; &lt;span class="bp"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;initial_idents&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;collect_free_idents&lt;/code&gt; walks an expression and collects every identifier referenced. &lt;code&gt;reachable_bindings&lt;/code&gt; does a simple graph traversal starting from the identifiers used in the body, following variable references transitively. If a binding is never reachable from the body, it's unused.&lt;/p&gt;

&lt;p&gt;I'm delegating part of what used to be &lt;code&gt;Local&lt;/code&gt;-processing into the &lt;code&gt;translate_seq&lt;/code&gt; function, where the logic actually runs, to keep things tidy:&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 fbe68b1..2f4bf41 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;@@ -154,23 +187,15 @@&lt;/span&gt; let rec translate venv expr =
     )
   | ParsedObject (pos, entries) -&amp;gt; translate_object venv pos entries
   | ObjectFieldAccess (pos, scope, chain) -&amp;gt; translate_object_field_access venv pos scope chain
&lt;span class="gd"&gt;-  | Local (pos, vars) -&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+  | Local (_pos, vars) -&amp;gt;
&lt;/span&gt;     let venv' = List.fold_left
       (* Adds an expr to the env to be evaluated at a later point in time (when required) *)
       (fun venv (varname, var_expr) -&amp;gt; Env.add_local varname (Lazy var_expr) venv)
       venv
       vars
&lt;span class="gd"&gt;-    in
-    let* _ = List.fold_left
-      (fun ok' (varname, _) -&amp;gt; ok' &amp;gt;&amp;gt;= fun _ -&amp;gt; check_cyclic_refs venv' varname [] pos)
-      (ok ())
-      vars
&lt;/span&gt;     in ok (venv', Tunit)
   | Seq exprs -&amp;gt;
&lt;span class="gd"&gt;-    List.fold_left
-      (fun acc expr -&amp;gt; acc &amp;gt;&amp;gt;= fun (venv, _) -&amp;gt; translate venv expr)
-      (ok (venv, Tunit))
-      exprs
&lt;/span&gt;&lt;span class="gi"&gt;+    translate_seq venv exprs
&lt;/span&gt;   | BinOp (pos, op, e1, e2) -&amp;gt;
     translate_bin_op venv pos op e1 e2
   | UnaryOp (pos, op, expr) -&amp;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;translate_seq&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt; &lt;span class="n"&gt;exprs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;rec&lt;/span&gt; &lt;span class="n"&gt;collect_locals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Local&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;vars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="n"&gt;rest&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_vars&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="n"&gt;collect_locals&lt;/span&gt; &lt;span class="n"&gt;rest&lt;/span&gt; &lt;span class="k"&gt;in&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;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;v&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;pos&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="n"&gt;vars&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;all_vars&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="n"&gt;rest&lt;/span&gt; &lt;span class="o"&gt;-&amp;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;rest&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="k"&gt;rec&lt;/span&gt; &lt;span class="n"&gt;go&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;
    &lt;span class="o"&gt;|&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;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;Tunit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&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;translate&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Local&lt;/span&gt; &lt;span class="n"&gt;_&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="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exprs&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_pos_vars&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="n"&gt;collect_locals&lt;/span&gt; &lt;span class="n"&gt;exprs&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;all_vars&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="n"&gt;snd&lt;/span&gt; &lt;span class="n"&gt;all_pos_vars&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'&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;venv&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;varname&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;var_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="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;varname&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Lazy&lt;/span&gt; &lt;span class="n"&gt;var_expr&lt;/span&gt;&lt;span class="p"&gt;)&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;venv&lt;/span&gt;
        &lt;span class="n"&gt;all_vars&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_idents&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;concat_map&lt;/span&gt; &lt;span class="n"&gt;collect_free_idents&lt;/span&gt; &lt;span class="n"&gt;body&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;reachable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reachable_bindings&lt;/span&gt; &lt;span class="n"&gt;all_vars&lt;/span&gt; &lt;span class="n"&gt;body_idents&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="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;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;varname&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;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;check_cyclic_refs&lt;/span&gt; &lt;span class="n"&gt;venv'&lt;/span&gt; &lt;span class="n"&gt;varname&lt;/span&gt; &lt;span class="bp"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;pos&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;Ok&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;ok&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt;
          &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="n"&gt;msg&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;mem&lt;/span&gt; &lt;span class="n"&gt;varname&lt;/span&gt; &lt;span class="n"&gt;reachable&lt;/span&gt;
            &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;
            &lt;span class="k"&gt;else&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="n"&gt;warn&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_cyclic_reference&lt;/span&gt; &lt;span class="n"&gt;varname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;pos&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="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;all_pos_vars&lt;/span&gt;
      &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="n"&gt;go&lt;/span&gt; &lt;span class="n"&gt;venv'&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="n"&gt;rest&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;_&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;expr&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="n"&gt;go&lt;/span&gt; &lt;span class="n"&gt;venv'&lt;/span&gt; &lt;span class="n"&gt;rest&lt;/span&gt;
  &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="n"&gt;go&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt; &lt;span class="n"&gt;exprs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key part: if a cyclic reference involves a variable that's reachable from the body, we still error. If it's unreachable, we just warn and move on. Here's what it looks like in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dune &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; tsonnet samples/variables/untouched_invalid_variable.jsonnet
&lt;span class="go"&gt;Warning: .../untouched_invalid_variable.jsonnet:1:31 Cyclic reference found for c

&lt;/span&gt;&lt;span class="gp"&gt;1: local a = 1, b = a, c = d, d = c;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Warning: .../untouched_invalid_variable.jsonnet:1:24 Cyclic reference found for d

&lt;/span&gt;&lt;span class="gp"&gt;1: local a = 1, b = a, c = d, d = c;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;   ^^^^^^^^^^^^^^^^^^^^^^^^^^
1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;c&lt;/code&gt; and &lt;code&gt;d&lt;/code&gt; have a cycle between them, but &lt;code&gt;b&lt;/code&gt; is what the program actually evaluates. So we warn, and produce the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Warn on unused variables
&lt;/h2&gt;

&lt;p&gt;With reachability analysis in place, unused variable warnings follow naturally. A variable is unused if it's not in the &lt;code&gt;reachable&lt;/code&gt; set. Adding that to &lt;code&gt;translate_seq&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 2f4bf41..0e7a680 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;@@ -247,6 +247,11 @@&lt;/span&gt; and translate_seq venv exprs =
       (* Determine which vars are reachable from the body *)
       let body_idents = List.concat_map collect_free_idents body in
       let reachable = reachable_bindings all_vars body_idents in
&lt;span class="gi"&gt;+      (* Warn on unused variables *)
+      List.iter (fun (pos, (varname, _)) -&amp;gt;
+        if not (List.mem varname reachable)
+        then Error.warn (Error.Msg.type_unused_variable varname) pos
+      ) all_pos_vars;
&lt;/span&gt;       (* Check cycles: error for reachable, warn for unreachable *)
       let* () = List.fold_left
         (fun acc (pos, (varname, _)) -&amp;gt; acc &amp;gt;&amp;gt;= fun () -&amp;gt;
&lt;span class="p"&gt;@@ -255,7 +260,7 @@&lt;/span&gt; and translate_seq venv exprs =
           | Error msg -&amp;gt;
             if List.mem varname reachable
             then error msg
&lt;span class="gd"&gt;-            else (prerr_endline ("Warning: " ^ msg); ok ())
&lt;/span&gt;&lt;span class="gi"&gt;+            else (Error.warn (Error.Msg.type_cyclic_reference varname) pos; ok ())
&lt;/span&gt;         )
         (ok ())
         all_pos_vars
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also refactored the warning infrastructure a bit. Instead of calling &lt;code&gt;prerr_endline&lt;/code&gt; inline, there's now a proper &lt;code&gt;Error.warn&lt;/code&gt; function:&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 ac8ae48..508ae0c 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;@@ -21,6 +21,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_unused_variable varname = "Unused variable " ^ varname
&lt;/span&gt;   let type_non_indexable_value ty = ty ^ " is a non indexable value"
   let type_expected_integer_index ty = "Expected Integer index, got " ^ ty
   let type_invalid_expr expr = "Invalid type " ^ expr
&lt;span class="p"&gt;@@ -104,3 +105,8 @@&lt;/span&gt; let trace (err: string) (pos: position) : (string, string) result =
     (fun content -&amp;gt; ok (Printf.sprintf "%s\n%s" (trace_file_position err pos) content))
&lt;span class="err"&gt;
&lt;/span&gt; let error_at pos = fun msg -&amp;gt; trace msg pos &amp;gt;&amp;gt;= error
&lt;span class="gi"&gt;+
+let warn msg pos =
+  match trace msg pos with
+  | Ok formatted -&amp;gt; prerr_endline ("Warning: " ^ formatted)
+  | Error _ -&amp;gt; prerr_endline ("Warning: " ^ msg)
&lt;/span&gt;&lt;span class="gh"&gt;diff --git a/lib/error.mli b/lib/error.mli
index 0bdd708..c09b930 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;@@ -18,6 +18,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_unused_variable : string -&amp;gt; string
&lt;/span&gt;   val type_non_indexable_value : string -&amp;gt; string
   val type_expected_integer_index : string -&amp;gt; string
   val type_invalid_expr : string -&amp;gt; string
&lt;span class="p"&gt;@@ -37,3 +38,4 @@&lt;/span&gt; end
&lt;span class="err"&gt;
&lt;/span&gt; val trace : string -&amp;gt; Ast.position -&amp;gt; (string, string) result
 val error_at : Ast.position -&amp;gt; string -&amp;gt; ('a, string) result
&lt;span class="gi"&gt;+val warn : string -&amp;gt; Ast.position -&amp;gt; unit
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Much cleaner. Let's see it in action:&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/variables/untouched_variable.jsonnet
Warning: .../untouched_variable.jsonnet:1:0 Unused variable a

1: &lt;span class="nb"&gt;local &lt;/span&gt;a &lt;span class="o"&gt;=&lt;/span&gt; 1, b &lt;span class="o"&gt;=&lt;/span&gt; 42&lt;span class="p"&gt;;&lt;/span&gt;
   ^^^^^^^^^^^^^^^^^^^^
42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;a&lt;/code&gt; is defined but never used. The program still returns &lt;code&gt;42&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Warn on untouched object fields
&lt;/h2&gt;

&lt;p&gt;Variables were the easy part. Objects are more involved because we're dealing with two separate phases -- the type checker and the interpreter -- and both need to handle cyclic field references gracefully.&lt;/p&gt;

&lt;p&gt;The approach is the same: if a cyclic object field is never accessed during evaluation, warn instead of error. The tricky bit is detecting "accessed during evaluation" correctly.&lt;/p&gt;

&lt;p&gt;Both modules now track which fields are actively being evaluated using a mutable &lt;code&gt;ObjectFields&lt;/code&gt; set:&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;(* In interpreter.ml *)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;evaluating_fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ref&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="c"&gt;(* In type.ml *)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;translating_fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ref&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we start evaluating a field, we add it to the set. When we're done (or on error), we remove it. If we try to evaluate a field already in the set -- cycle detected:&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 fe248b9..391c1e2 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;@@ -21,9 +23,17 @@&lt;/span&gt; let rec interpret env expr =
   | ObjectPtr _ as obj_ptr -&amp;gt; ok (env, obj_ptr)
   | ObjectFieldAccess (pos, scope, chain) -&amp;gt; interpret_object_field_access env (pos, scope, chain)
   | Ident (pos, varname) -&amp;gt;
&lt;span class="gd"&gt;-    Env.find_var varname env
-      ~succ:(fun env' expr -&amp;gt; interpret env' expr)
-      ~err:(Error.error_at pos)
&lt;/span&gt;&lt;span class="gi"&gt;+    if ObjectFields.mem varname !evaluating_fields then
+      Error.error_at pos (Error.Msg.type_cyclic_reference varname)
+    else begin
+      evaluating_fields := ObjectFields.add varname !evaluating_fields;
+      let result = Env.find_var varname env
+        ~succ:(fun env' expr -&amp;gt; interpret env' expr)
+        ~err:(Error.error_at pos)
+      in
+      evaluating_fields := ObjectFields.remove varname !evaluating_fields;
+      result
+    end
&lt;/span&gt;   | BinOp (pos, op, e1, e2) -&amp;gt; interpret_bin_op env (pos, op, e1, e2)
   | UnaryOp (pos, op, expr) -&amp;gt;
     let* (env', expr') = interpret env expr in
&lt;span class="p"&gt;@@ -201,9 +211,18 @@&lt;/span&gt; and interpret_object_field_access env (pos, scope, chain_exprs) =
       match field_expr with
       | String (pos, field) | Ident (pos, field) -&amp;gt;
         let* (obj_id, field_env) = get_obj_id in
&lt;span class="gd"&gt;-        Env.get_obj_field field obj_id field_env
-          ~succ:(interpret)
-          ~err:(Error.error_at pos)
&lt;/span&gt;&lt;span class="gi"&gt;+        let key = Env.uniq_field_ident obj_id field in
+        if ObjectFields.mem key !evaluating_fields then
+          Error.error_at pos (Error.Msg.type_cyclic_reference key)
+        else begin
+          evaluating_fields := ObjectFields.add key !evaluating_fields;
+          let result = Env.get_obj_field field obj_id field_env
+            ~succ:(interpret)
+            ~err:(Error.error_at pos)
+          in
+          evaluating_fields := ObjectFields.remove key !evaluating_fields;
+          result
+        end
&lt;/span&gt;       | Number _ as index_expr -&amp;gt;
         (* Handle array/string indexing: prev_expr[number] *)
         Result.fold
&lt;span class="p"&gt;@@ -223,19 +242,27 @@&lt;/span&gt; and interpret_runtime_object env (pos, obj_env, fields) =
 and interpret_runtime_object_fields obj_env fields =
   match Env.Map.find_opt "self" obj_env with
   | Some (ObjectPtr (obj_id, _)) -&amp;gt;
&lt;span class="gd"&gt;-    let* field_list =
&lt;/span&gt;&lt;span class="gi"&gt;+    let field_list =
&lt;/span&gt;       ObjectFields.fold
         (fun field acc -&amp;gt;
&lt;span class="gd"&gt;-          let* evaluated_fields = acc in
&lt;/span&gt;           let key = Env.uniq_field_ident obj_id field in
&lt;span class="gd"&gt;-          match Env.Map.find_opt key obj_env with
-          | Some expr -&amp;gt;
-            let* (_, evaluated) = interpret obj_env expr in
-            ok ((field, evaluated) :: evaluated_fields)
-          | None -&amp;gt; acc
&lt;/span&gt;&lt;span class="gi"&gt;+          if ObjectFields.mem key !evaluating_fields then
+            acc (* Skip: cyclic reference detected *)
+          else
+            match Env.Map.find_opt key obj_env with
+            | Some expr -&amp;gt;
+              evaluating_fields := ObjectFields.add key !evaluating_fields;
+              let result =
+                match interpret obj_env expr with
+                | Ok (_, evaluated) -&amp;gt; (field, evaluated) :: acc
+                | Error _ -&amp;gt; acc
+              in
+              evaluating_fields := ObjectFields.remove key !evaluating_fields;
+              result
+            | None -&amp;gt; acc
&lt;/span&gt;         )
         fields
&lt;span class="gd"&gt;-        (ok [])
&lt;/span&gt;&lt;span class="gi"&gt;+        []
&lt;/span&gt;     in ok (List.rev field_list)
   | _ -&amp;gt; ok []
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For object fields that are never accessed, the type checker now warns instead of erroring. &lt;code&gt;translate_object&lt;/code&gt; was changed to iterate over entries and emit warnings rather than propagate errors:&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 0e7a680..8b41545 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;@@ -152,13 +154,21 @@&lt;/span&gt; let rec translate venv expr =
   | Number _ -&amp;gt; ok (venv, Tnumber)
   | String _ -&amp;gt; ok (venv, Tstring)
   | Ident (pos, varname) -&amp;gt;
&lt;span class="gd"&gt;-    Env.find_var varname venv
-      ~succ:(fun venv ty -&amp;gt;
-        match ty with
-        | Lazy expr -&amp;gt; translate venv expr
-        | _ -&amp;gt; ok (venv, ty)
-      )
-      ~err:(Error.error_at pos)
&lt;/span&gt;&lt;span class="gi"&gt;+    if ObjectFields.mem varname !translating_fields then
+      Error.error_at pos (Error.Msg.type_cyclic_reference varname)
+    else begin
+      translating_fields := ObjectFields.add varname !translating_fields;
+      let result = Env.find_var varname venv
+        ~succ:(fun venv ty -&amp;gt;
+          match ty with
+          | Lazy expr -&amp;gt; translate venv expr
+          | _ -&amp;gt; ok (venv, ty)
+        )
+        ~err:(Error.error_at pos)
+      in
+      translating_fields := ObjectFields.remove varname !translating_fields;
+      result
+    end
&lt;/span&gt;   | Array (_pos, elems) -&amp;gt;
     (* As of now, we compare each element and if all have the same type,
       it is an array of this type, otherwise it will be an array of any.
&lt;span class="p"&gt;@@ -298,32 +308,36 @@&lt;/span&gt; and translate_object venv pos entries =
     entries
   in
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-  (* Check for cyclical references among object fields *)
-  let* () = List.fold_left
-      (fun ok' entry -&amp;gt; ok' &amp;gt;&amp;gt;= fun _ -&amp;gt;
-        match entry with
-        | ObjectField (attr, _) -&amp;gt;
-          check_cyclic_refs venv (Env.uniq_field_ident obj_id attr) [] pos
-        | _ -&amp;gt; ok'
-      )
-      (ok ())
-      entries
-  in
&lt;/span&gt;&lt;span class="gi"&gt;+  (* Check for cyclical references among object fields
+    (warn, don't error when the reference is not part of
+    the evaluation tree)
+  *)
+  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; ()
+        | Error _ -&amp;gt; Error.warn (Error.Msg.type_cyclic_reference (Env.uniq_field_ident obj_id attr)) pos)
+      | _ -&amp;gt; ()
+    )
+    entries;
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-  (* Then translate object fields *)
-  let* entry_types = List.fold_left
-    (fun result entry -&amp;gt;
-      let* entries' = result in
&lt;/span&gt;&lt;span class="gi"&gt;+  (* Translate object fields lazily: warn on errors, skip invalid fields *)
+  let entry_types = List.fold_left
+    (fun entries' entry -&amp;gt;
&lt;/span&gt;       match entry with
       | ObjectField (attr, _) -&amp;gt;
&lt;span class="gd"&gt;-        let* (_, entry_ty) = Env.get_obj_field attr obj_id venv
&lt;/span&gt;&lt;span class="gi"&gt;+        (match Env.get_obj_field attr obj_id venv
&lt;/span&gt;           ~succ:translate_lazy
           ~err:(Error.error_at pos)
&lt;span class="gd"&gt;-        in ok (entries' @ [TobjectField (attr, entry_ty)])
&lt;/span&gt;&lt;span class="gi"&gt;+        with
+        | Ok (_, entry_ty) -&amp;gt; entries' @ [TobjectField (attr, entry_ty)]
+        | Error _ -&amp;gt; entries')
&lt;/span&gt;       | _ -&amp;gt;
&lt;span class="gd"&gt;-        result
&lt;/span&gt;&lt;span class="gi"&gt;+        entries'
&lt;/span&gt;     )
&lt;span class="gd"&gt;-    (ok [])
&lt;/span&gt;&lt;span class="gi"&gt;+    []
&lt;/span&gt;     entries
   in
   (* Remove self and $ from the environment to prevent leaking *)
&lt;span class="p"&gt;@@ -372,9 +386,18 @@&lt;/span&gt; and translate_object_field_access venv pos scope chain_exprs =
       match field_expr with
       | String (_, field) | Ident (_, field) -&amp;gt;
         let* obj_id = get_obj_id in
&lt;span class="gd"&gt;-        Env.get_obj_field field obj_id venv
-          ~succ:translate_lazy
-          ~err:(Error.error_at pos)
&lt;/span&gt;&lt;span class="gi"&gt;+        let key = Env.uniq_field_ident obj_id field in
+        if ObjectFields.mem key !translating_fields then
+          Error.error_at pos (Error.Msg.type_cyclic_reference key)
+        else begin
+          translating_fields := ObjectFields.add key !translating_fields;
+          let result = Env.get_obj_field field obj_id venv
+            ~succ:translate_lazy
+            ~err:(Error.error_at pos)
+          in
+          translating_fields := ObjectFields.remove key !translating_fields;
+          result
+        end
&lt;/span&gt;       | Number (pos, _) -&amp;gt;
         (* Handle numeric indexing of strings and arrays *)
         (match prev_ty with
&lt;span class="p"&gt;@@ -429,4 +452,5 @@&lt;/span&gt; let check (config : Config.t) expr  =
   else
     let* _ = translate Env.empty expr in
     Env.Id.reset ();
&lt;span class="gi"&gt;+    translating_fields := ObjectFields.empty;
&lt;/span&gt;     ok expr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's a sample that demonstrates the distinction. When we only access &lt;code&gt;result.b&lt;/code&gt;, the cyclic &lt;code&gt;c&lt;/code&gt;/&lt;code&gt;d&lt;/code&gt; pair just produces warnings:&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/objects/untouched_invalid_field.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;result&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="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="err"&gt;c:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;self.d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;d:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;self.c&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;result.b&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 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/objects/untouched_invalid_field.jsonnet
Warning: .../untouched_invalid_field.jsonnet:1:0 Unused variable result

...

Warning: .../untouched_invalid_field.jsonnet:1:15 Cyclic reference found &lt;span class="k"&gt;for &lt;/span&gt;1-&amp;gt;c

...

Warning: .../untouched_invalid_field.jsonnet:1:15 Cyclic reference found &lt;span class="k"&gt;for &lt;/span&gt;1-&amp;gt;d

...
1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Meanwhile, if a cyclic field is actually accessed at runtime, it still errors hard:&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_object_with_cyclic_field.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="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;b:&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="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 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_object_with_cyclic_field.jsonnet
Warning: .../invalid_object_with_cyclic_field.jsonnet:1:0 Cyclic reference found &lt;span class="k"&gt;for &lt;/span&gt;1-&amp;gt;b
...
.../invalid_object_with_cyclic_field.jsonnet:3:12 Cyclic reference found &lt;span class="k"&gt;for &lt;/span&gt;1-&amp;gt;c
...
&lt;span class="o"&gt;[&lt;/span&gt;1]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The type checker warns (because it doesn't know at type-check time which fields will be accessed), but the interpreter finds the cycle at runtime and errors properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Warn on cyclic refs for untouched object fields
&lt;/h2&gt;

&lt;p&gt;The previous changes got the type checker side right, but the interpreter was still getting it wrong. When rendering an object, &lt;code&gt;interpret_runtime_object_fields&lt;/code&gt; was folding into a plain list and silently skipping any field that errored -- including cyclic ones. So if you did access a cyclic field at runtime, you'd get an empty result instead of an error. Not great.&lt;/p&gt;

&lt;p&gt;The fix is straightforward: go back to a monadic fold and let errors propagate normally. The cycle detection in &lt;code&gt;interpret_object_field_access&lt;/code&gt; already handles the "is this field in a cycle?" question via &lt;code&gt;evaluating_fields&lt;/code&gt; -- &lt;code&gt;interpret_runtime_object_fields&lt;/code&gt; doesn't need to second-guess 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/interpreter.ml b/lib/interpreter.ml
index 391c1e2..60f8a72 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;@@ -242,27 +242,19 @@&lt;/span&gt; and interpret_runtime_object env (pos, obj_env, fields) =
 and interpret_runtime_object_fields obj_env fields =
   match Env.Map.find_opt "self" obj_env with
   | Some (ObjectPtr (obj_id, _)) -&amp;gt;
&lt;span class="gd"&gt;-    let field_list =
&lt;/span&gt;&lt;span class="gi"&gt;+    let* field_list =
&lt;/span&gt;       ObjectFields.fold
         (fun field acc -&amp;gt;
&lt;span class="gi"&gt;+          let* evaluated_fields = acc in
&lt;/span&gt;           let key = Env.uniq_field_ident obj_id field in
&lt;span class="gd"&gt;-          if ObjectFields.mem key !evaluating_fields then
-            acc (* Skip: cyclic reference detected *)
-          else
-            match Env.Map.find_opt key obj_env with
-            | Some expr -&amp;gt;
-              evaluating_fields := ObjectFields.add key !evaluating_fields;
-              let result =
-                match interpret obj_env expr with
-                | Ok (_, evaluated) -&amp;gt; (field, evaluated) :: acc
-                | Error _ -&amp;gt; acc
-              in
-              evaluating_fields := ObjectFields.remove key !evaluating_fields;
-              result
-            | None -&amp;gt; acc
&lt;/span&gt;&lt;span class="gi"&gt;+          match Env.Map.find_opt key obj_env with
+          | Some expr -&amp;gt;
+            let* (_, evaluated) = interpret obj_env expr in
+            ok ((field, evaluated) :: evaluated_fields)
+          | None -&amp;gt; acc
&lt;/span&gt;         )
         fields
&lt;span class="gd"&gt;-        []
&lt;/span&gt;&lt;span class="gi"&gt;+        (ok [])
&lt;/span&gt;     in ok (List.rev field_list)
   | _ -&amp;gt; ok []
&lt;span class="err"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also a small fix in the type checker's &lt;code&gt;translate_object_field_access&lt;/code&gt;. When chaining into a &lt;code&gt;TruntimeObject&lt;/code&gt;, the field lookup was using the outer &lt;code&gt;venv&lt;/code&gt; -- meaning &lt;code&gt;self&lt;/code&gt; and &lt;code&gt;$&lt;/code&gt; weren't in scope. The fix builds a proper &lt;code&gt;field_venv&lt;/code&gt; before looking up the field, the same way the interpreter does 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/type.ml b/lib/type.ml
index 8b41545..200f533 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;@@ -376,22 +376,31 @@&lt;/span&gt; and translate_object_field_access venv pos scope chain_exprs =
     (fun acc field_expr -&amp;gt;
       let* (venv, prev_ty) = acc in
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-      let get_obj_id =
&lt;/span&gt;&lt;span class="gi"&gt;+      let get_obj_id_and_env =
&lt;/span&gt;         match prev_ty with
&lt;span class="gd"&gt;-        | TobjectPtr (obj_id, _) -&amp;gt; ok obj_id
-        | TruntimeObject (obj_id, _) -&amp;gt; ok obj_id
&lt;/span&gt;&lt;span class="gi"&gt;+        | TobjectPtr (obj_id, _) -&amp;gt; ok (obj_id, venv)
+        | TruntimeObject (obj_id, _) -&amp;gt;
+          (* TODO: we haven't included the environment in TruntimeObject yet.
+             It must be done such as Ast.RuntimeObject *)
+          let field_venv =
+            Env.add_local "self" (TobjectPtr (obj_id, TobjectSelf)) venv
+          in
+          let field_venv =
+            Env.add_local_when_not_present "$" (TobjectPtr (obj_id, TobjectTopLevel)) field_venv |&amp;gt; fst
+          in
+          ok (obj_id, field_venv)
&lt;/span&gt;         | _ -&amp;gt; Error.error_at pos Error.Msg.must_be_object
       in
&lt;span class="err"&gt;
&lt;/span&gt;       match field_expr with
       | String (_, field) | Ident (_, field) -&amp;gt;
&lt;span class="gd"&gt;-        let* obj_id = get_obj_id in
&lt;/span&gt;&lt;span class="gi"&gt;+        let* (obj_id, field_venv) = get_obj_id_and_env in
&lt;/span&gt;         let key = Env.uniq_field_ident obj_id field in
         if ObjectFields.mem key !translating_fields then
           Error.error_at pos (Error.Msg.type_cyclic_reference key)
         else begin
           translating_fields := ObjectFields.add key !translating_fields;
&lt;span class="gd"&gt;-          let result = Env.get_obj_field field obj_id venv
&lt;/span&gt;&lt;span class="gi"&gt;+          let result = Env.get_obj_field field obj_id field_venv
&lt;/span&gt;             ~succ:translate_lazy
             ~err:(Error.error_at pos)
           in
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I sprinkled a &lt;code&gt;TODO&lt;/code&gt; here because I don't want to do this refactoring now. XD&lt;/p&gt;

&lt;p&gt;Everything is captured in 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/samples/semantics/valid_object_with_cyclic_field.jsonnet b/samples/semantics/invalid_object_with_cyclic_field.jsonnet
&lt;/span&gt;&lt;span class="p"&gt;similarity index 100%
rename from samples/semantics/valid_object_with_cyclic_field.jsonnet
rename to samples/semantics/invalid_object_with_cyclic_field.jsonnet
&lt;/span&gt;&lt;span class="gh"&gt;diff --git a/test/cram/objects.t b/test/cram/objects.t
index b200032..e7ae092 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;@@ -24,7 +24,7 @@&lt;/span&gt;
&lt;span class="err"&gt;
&lt;/span&gt;   $ tsonnet ../../samples/objects/untouched_field.jsonnet
   Warning: ../../samples/objects/untouched_field.jsonnet:1:0 Unused variable result
&lt;span class="gd"&gt;-  
&lt;/span&gt;&lt;span class="gi"&gt;+
&lt;/span&gt;   1: local result = {
      ^^^^^^^^^^^^^^^^
   2:     a: 1,
&lt;span class="p"&gt;@@ -32,3 +32,39 @@&lt;/span&gt;
   3:     b: 42,
      ^^^^^^^^^^
   42
&lt;span class="gi"&gt;+
+
+  $ tsonnet ../../samples/objects/untouched_invalid_field.jsonnet
+  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
+     ^^^^^^^^^^^^^
+  Warning: ../../samples/objects/untouched_invalid_field.jsonnet:1:15 Cyclic reference found for 1-&amp;gt;c
+
+  1: local result = {
+     ^^^^^^^^^^^^^^^^
+  2:     a: 1,
+
+  3:     b: self.a,
+
+  4:     c: self.d,
+
+  Warning: ../../samples/objects/untouched_invalid_field.jsonnet:1:15 Cyclic reference found for 1-&amp;gt;d
+
+  1: local result = {
+     ^^^^^^^^^^^^^^^^
+  2:     a: 1,
+
+  3:     b: self.a,
+
+  4:     c: self.d,
+
+  1
&lt;/span&gt;&lt;span class="gh"&gt;diff --git a/test/cram/semantics.t b/test/cram/semantics.t
index 4513915..c98851d 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;@@ -52,7 +52,11 @@&lt;/span&gt;
      ^^^^^^^^^^^^^^^^
   4:     local c = b,
      ^^^^^^^^^^^^^^^^
&lt;span class="gd"&gt;-  {}
&lt;/span&gt;&lt;span class="gi"&gt;+  ../../samples/semantics/invalid_binding_cycle_object_locals.jsonnet:4:14 Cyclic reference found for b
+  
+  4:     local c = b,
+     ^^^^^^^^^^^^^^^^
+  [1]
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;   $ tsonnet ../../samples/semantics/invalid_binding_cycle_binop.jsonnet
   ../../samples/semantics/invalid_binding_cycle_binop.jsonnet:2:10 Cyclic reference found for a
&lt;span class="p"&gt;@@ -92,7 +96,11 @@&lt;/span&gt;
      ^^^^^^^^^^^^^^
   3:     b: self.a,
      ^^^^^^^^^^^^^^
&lt;span class="gd"&gt;-  {}
&lt;/span&gt;&lt;span class="gi"&gt;+  ../../samples/semantics/invalid_binding_cycle_object_fields.jsonnet:2:12 Cyclic reference found for 1-&amp;gt;b
+  
+  2:     a: self.b,
+     ^^^^^^^^^^^^^^
+  [1]
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;   $ tsonnet ../../samples/semantics/invalid_binding_cycle_object_nested_field.jsonnet
   Warning: ../../samples/semantics/invalid_binding_cycle_object_nested_field.jsonnet:1:0 Cyclic reference found for 1-&amp;gt;a
&lt;span class="p"&gt;@@ -135,7 +143,11 @@&lt;/span&gt;
               ^^^^^^^^^
   4:     },
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-  { "a": {} }
&lt;/span&gt;&lt;span class="gi"&gt;+  ../../samples/semantics/invalid_binding_cycle_object_nested_field.jsonnet:3:17 Cyclic reference found for 1-&amp;gt;b
+  
+  3:         value: $.b
+     ^^^^^^^^^^^^^^^^^^
+  [1]
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;   $ tsonnet ../../samples/semantics/invalid_binding_cycle_outer_object_fields.jsonnet
   Warning: ../../samples/semantics/invalid_binding_cycle_outer_object_fields.jsonnet:1:0 Cyclic reference found for 1-&amp;gt;a
&lt;span class="p"&gt;@@ -154,7 +166,11 @@&lt;/span&gt;
      ^^^^^^^^^^^
   3:     b: $.a,
      ^^^^^^^^^^^
&lt;span class="gd"&gt;-  {}
&lt;/span&gt;&lt;span class="gi"&gt;+  ../../samples/semantics/invalid_binding_cycle_outer_object_fields.jsonnet:2:9 Cyclic reference found for 1-&amp;gt;b
+  
+  2:     a: $.b,
+     ^^^^^^^^^^^
+  [1]
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;   $ tsonnet ../../samples/semantics/invalid_binding_cycle_object_field_and_local.jsonnet
   Warning: ../../samples/semantics/invalid_binding_cycle_object_field_and_local.jsonnet:1:0 Cyclic reference found for 1-&amp;gt;b
&lt;span class="p"&gt;@@ -165,7 +181,11 @@&lt;/span&gt;
      ^^^^^^^^^^^^^^^^^^^^^
   3:     b: a,
      ^^^^^^^^^
&lt;span class="gd"&gt;-  {}
&lt;/span&gt;&lt;span class="gi"&gt;+  ../../samples/semantics/invalid_binding_cycle_object_field_and_local.jsonnet:3:7 Cyclic reference found for a
+  
+  3:     b: a,
+     ^^^^^^^^^
+  [1]
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;   $ tsonnet ../../samples/semantics/invalid_binding_cycle_indexed_field.jsonnet
   Warning: ../../samples/semantics/invalid_binding_cycle_indexed_field.jsonnet:1:0 Cyclic reference found for 1-&amp;gt;arr
&lt;span class="p"&gt;@@ -184,10 +204,14 @@&lt;/span&gt;
      ^^^^^^^^^^^^^^^^^^^^^^
   3:     first: self.arr[0]
      ^^^^^^^^^^^^^^^^^^^^^^
&lt;span class="gd"&gt;-  {}
&lt;/span&gt;&lt;span class="gi"&gt;+  ../../samples/semantics/invalid_binding_cycle_indexed_field.jsonnet:2:15 Cyclic reference found for 1-&amp;gt;first
+  
+  2:     arr: [self.first],
+     ^^^^^^^^^^^^^^^^^^^^^^
+  [1]
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-  $ tsonnet ../../samples/semantics/valid_object_with_cyclic_field.jsonnet
-  Warning: ../../samples/semantics/valid_object_with_cyclic_field.jsonnet:1:0 Cyclic reference found for 1-&amp;gt;b
&lt;/span&gt;&lt;span class="gi"&gt;+  $ tsonnet ../../samples/semantics/invalid_object_with_cyclic_field.jsonnet
+  Warning: ../../samples/semantics/invalid_object_with_cyclic_field.jsonnet:1:0 Cyclic reference found for 1-&amp;gt;b
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;   1: {
      ^
&lt;span class="p"&gt;@@ -197,7 +221,7 @@&lt;/span&gt;
      ^^^^^^^^^^^^^^
   4:     c: self.b,
      ^^^^^^^^^^^^^^
&lt;span class="gd"&gt;-  Warning: ../../samples/semantics/valid_object_with_cyclic_field.jsonnet:1:0 Cyclic reference found for 1-&amp;gt;c
&lt;/span&gt;&lt;span class="gi"&gt;+  Warning: ../../samples/semantics/invalid_object_with_cyclic_field.jsonnet:1:0 Cyclic reference found for 1-&amp;gt;c
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;   1: {
      ^
&lt;span class="p"&gt;@@ -207,7 +231,11 @@&lt;/span&gt;
      ^^^^^^^^^^^^^^
   4:     c: self.b,
      ^^^^^^^^^^^^^^
&lt;span class="gd"&gt;-  { "a": 1 }
&lt;/span&gt;&lt;span class="gi"&gt;+  ../../samples/semantics/invalid_object_with_cyclic_field.jsonnet:3:12 Cyclic reference found for 1-&amp;gt;c
+  
+  3:     b: self.c,
+     ^^^^^^^^^^^^^^
+  [1]
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;   $ tsonnet ../../samples/semantics/valid_object_access_non_cyclic_field.jsonnet
   Warning: ../../samples/semantics/valid_object_access_non_cyclic_field.jsonnet:1:0 Unused variable obj
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing I noticed while working through the test cases: the sample that used to be called &lt;code&gt;valid_object_with_cyclic_field.jsonnet&lt;/code&gt; is not actually valid -- it errors when the cyclic fields are accessed. Renamed it to &lt;code&gt;invalid_object_with_cyclic_field.jsonnet&lt;/code&gt;. These things happen when you're naming files before you've implemented the feature that would tell you whether they're valid or not.&lt;/p&gt;

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

&lt;p&gt;The reachability analysis for variables and the &lt;code&gt;evaluating_fields&lt;/code&gt; tracking for objects both push in the same direction -- lean on lazy evaluation instead of fighting it. This is the nature of Jsonnet, and Tsonnet should embrace it.&lt;/p&gt;

&lt;p&gt;You can check the entire diff &lt;a href="https://gitlab.com/-/snippets/5969491" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I think, after that, we can start playing with much more fun things: functions!&lt;/p&gt;




&lt;p&gt;Thanks for reading Bit Maybe Wise! If you too believe a cycle you never touch shouldn't ruin your day, &lt;a href="https://bitmaybewise.substack.com/" rel="noopener noreferrer"&gt;subscribe&lt;/a&gt; and let's keep being reasonably lenient together.&lt;/p&gt;

&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@sebastian_unrau" rel="noopener noreferrer"&gt;Sebastian Unrau&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 #33 - The arithmetic tutorial must go on</title>
      <dc:creator>Hercules Lemke Merscher</dc:creator>
      <pubDate>Fri, 13 Mar 2026 10:04:57 +0000</pubDate>
      <link>https://dev.to/bitmaybewise/tsonnet-33-the-arithmetic-tutorial-must-go-on-1694</link>
      <guid>https://dev.to/bitmaybewise/tsonnet-33-the-arithmetic-tutorial-must-go-on-1694</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 a full set of binary operators:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bitmaybewise/tsonnet-32-done-but-getting-there-4h7m" class="crayons-story__hidden-navigation-link"&gt;Tsonnet #32 - != done, but getting there&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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" alt="bitmaybewise profile" class="crayons-avatar__image"&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-3311307" 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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F187316%2Faed95d8a-d5c5-4d3e-8dab-513ed83b24c3.jpeg" class="crayons-avatar__image" alt=""&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-32-done-but-getting-there-4h7m" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Mar 5&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-32-done-but-getting-there-4h7m" id="article-link-3311307"&gt;
          Tsonnet #32 - != done, but getting there
        &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-32-done-but-getting-there-4h7m" 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="18" height="18"&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-32-done-but-getting-there-4h7m#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;
            11 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&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;We were &lt;em&gt;almost&lt;/em&gt; done with the &lt;a href="https://jsonnet.org/learning/tutorial.html#arithmetic" rel="noopener noreferrer"&gt;arithmetic tutorial&lt;/a&gt;. Two pieces were still missing: object merging, and division by zero error handling. Let's wrap those up.&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%2Ftv9bdgo4vk5usyxzrm8e.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%2Ftv9bdgo4vk5usyxzrm8e.jpeg" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Merging objects
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;+&lt;/code&gt; operator is overloaded to act as an operator to merge two objects.&lt;/p&gt;

&lt;p&gt;This sample file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsonnet"&gt;&lt;code&gt;&lt;span class="c1"&gt;// samples/objects/merge.jsonnet&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;a&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="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&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="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Results in:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"a"&lt;/span&gt;&lt;span class="p"&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="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"b"&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="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"c"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&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 right-hand side overrides the left-hand side fields.&lt;/p&gt;

&lt;p&gt;The cram test looks like this:&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 9c12260..bfb25bd 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;@@ -18,3 +18,6 @@&lt;/span&gt;
&lt;span class="err"&gt;
&lt;/span&gt;   $ tsonnet ../../samples/objects/toplevel_field_lookup_chain.jsonnet
   { "answer": { "value": 42 }, "answer_to_the_ultimate_question": 42 }
&lt;span class="gi"&gt;+
+  $ tsonnet ../../samples/objects/merge.jsonnet
+  { "a": 1, "b": 3, "c": 4 }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm adding a helper function in the &lt;code&gt;Ast&lt;/code&gt; module to merge two lists of fields:&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;module&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;merge_fields&lt;/span&gt; &lt;span class="n"&gt;fields1&lt;/span&gt; &lt;span class="n"&gt;fields2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="c"&gt;(* fields2 comes after and has preference over fields1 *)&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;updated&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="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="k"&gt;match&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;assoc_opt&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="n"&gt;fields2&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;v'&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;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;|&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="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="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;fields1&lt;/span&gt;
    &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="c"&gt;(* (k,v) in fields2 not present in fields1 *)&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;new_fields&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;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;k&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;not&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;mem_assoc&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="n"&gt;fields1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="n"&gt;fields2&lt;/span&gt;
    &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="c"&gt;(* then we merge *)&lt;/span&gt;
    &lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;new_fields&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The type checker only needs to include the overloaded operation:&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 d553f5c..fbe68b1 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;@@ -321,6 +321,8 @@&lt;/span&gt; and translate_bin_op venv pos op e1 e2 =
   | Add, _, Tstring | Add, Tstring, _ -&amp;gt; ok (venv'', Tstring)
   | Add, Tnumber, Tnumber -&amp;gt; ok (venv'', Tnumber)
   | Add, (Tarray _), (Tarray _) -&amp;gt; ok (venv'', Tarray Tany)
&lt;span class="gi"&gt;+  | Add, (Tobject _ | TruntimeObject _ | TobjectPtr _), (Tobject _ | TruntimeObject _ | TobjectPtr _) -&amp;gt;
+    ok (venv'', Tany)
&lt;/span&gt;   | Subtract, Tnumber, Tnumber -&amp;gt; ok (venv'', Tnumber)
   | Multiply, Tnumber, Tnumber -&amp;gt; ok (venv'', Tnumber)
   | Divide, Tnumber, Tnumber -&amp;gt; ok (venv'', Tnumber)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interpreter implements the new &lt;code&gt;interpret_object_merge_op&lt;/code&gt; function to deal with that, and we pattern-match in the &lt;code&gt;interpret_bin_op&lt;/code&gt; function to add 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/interpreter.ml b/lib/interpreter.ml
index ddc8041..24f0595 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;@@ -66,6 +66,19 @@&lt;/span&gt; and interpret_array_concat_op env e1 e2 =
   | _ -&amp;gt;
     error Error.Msg.interp_invalid_concat
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+and interpret_object_merge_op env pos e1 e2 =
+  let eval_object = function
+    | EvaluatedObject _ as obj -&amp;gt; ok (env, obj)
+    | RuntimeObject _ as obj -&amp;gt; interpret env obj
+    | _ -&amp;gt; error Error.Msg.invalid_binary_op
+  in
+  let* (_, e1') = eval_object e1 in
+  let* (_, e2') = eval_object e2 in
+  match e1', e2' with
+  | EvaluatedObject (_, fields1), EvaluatedObject (_, fields2) -&amp;gt;
+    ok (env, EvaluatedObject (pos, Object.merge_fields fields1 fields2))
+  | _ -&amp;gt; error Error.Msg.invalid_binary_op
+
&lt;/span&gt; and interpret_array env (pos, exprs) =
   let* (env', evaluated_exprs) = List.fold_left
     (fun result expr -&amp;gt;
&lt;span class="p"&gt;@@ -234,6 +247,8 @@&lt;/span&gt; and interpret_bin_op env (pos, op, e1, e2) =
    | Add, (Array _ as v1), (Array _ as v2)  -&amp;gt;
      interpret_array_concat_op env2 v1 v2
&lt;span class="gi"&gt;+   | Add, (EvaluatedObject _ | RuntimeObject _ as v1), (EvaluatedObject _ | RuntimeObject _ as v2) -&amp;gt;
+     interpret_object_merge_op env2 pos v1 v2
&lt;/span&gt;    | In, (String _ | Ident _ as field), (EvaluatedObject _ | RuntimeObject (_, _, _) as obj) -&amp;gt;
      interpret_in_op env2 pos field obj
    | _, v1, v2 -&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done!&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/objects/merge.jsonnet
&lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"a"&lt;/span&gt;: 1, &lt;span class="s2"&gt;"b"&lt;/span&gt;: 3, &lt;span class="s2"&gt;"c"&lt;/span&gt;: 4 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Division by zero
&lt;/h2&gt;

&lt;p&gt;Next we should deal with the division by zero error.&lt;/p&gt;

&lt;p&gt;Here's how Jsonnet handles it:&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/errors/divide_by_zero.jsonnet
RUNTIME ERROR: Division by zero.
    samples/errors/divide_by_zero.jsonnet:1:1-6 &lt;span class="err"&gt;$&lt;/span&gt;
    During evaluation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here are the sample files and how I want it to look in Tsonnet:&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/errors/divide_by_zero.jsonnet b/samples/errors/divide_by_zero.jsonnet
&lt;/span&gt;&lt;span class="p"&gt;new file mode 100644
&lt;/span&gt;&lt;span class="gh"&gt;index 0000000..59a5d52
&lt;/span&gt;&lt;span class="gd"&gt;--- /dev/null
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/samples/errors/divide_by_zero.jsonnet
&lt;/span&gt;&lt;span class="p"&gt;@@ -0,0 +1 @@&lt;/span&gt;
&lt;span class="gi"&gt;+5 / 0
&lt;/span&gt;&lt;span class="gh"&gt;diff --git a/samples/errors/divide_by_zero_float.jsonnet b/samples/errors/divide_by_zero_float.jsonnet
&lt;/span&gt;&lt;span class="p"&gt;new file mode 100644
&lt;/span&gt;&lt;span class="gh"&gt;index 0000000..20f3764
&lt;/span&gt;&lt;span class="gd"&gt;--- /dev/null
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/samples/errors/divide_by_zero_float.jsonnet
&lt;/span&gt;&lt;span class="p"&gt;@@ -0,0 +1 @@&lt;/span&gt;
&lt;span class="gi"&gt;+5 / 0.0
&lt;/span&gt;&lt;span class="gh"&gt;diff --git a/samples/errors/modulo_by_zero.jsonnet b/samples/errors/modulo_by_zero.jsonnet
&lt;/span&gt;&lt;span class="p"&gt;new file mode 100644
&lt;/span&gt;&lt;span class="gh"&gt;index 0000000..be4046b
&lt;/span&gt;&lt;span class="gd"&gt;--- /dev/null
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/samples/errors/modulo_by_zero.jsonnet
&lt;/span&gt;&lt;span class="p"&gt;@@ -0,0 +1 @@&lt;/span&gt;
&lt;span class="gi"&gt;+5 % 0
&lt;/span&gt;&lt;span class="gh"&gt;diff --git a/samples/errors/modulo_by_zero_float.jsonnet b/samples/errors/modulo_by_zero_float.jsonnet
&lt;/span&gt;&lt;span class="p"&gt;new file mode 100644
&lt;/span&gt;&lt;span class="gh"&gt;index 0000000..8f666b8
&lt;/span&gt;&lt;span class="gd"&gt;--- /dev/null
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/samples/errors/modulo_by_zero_float.jsonnet
&lt;/span&gt;&lt;span class="p"&gt;@@ -0,0 +1 @@&lt;/span&gt;
&lt;span class="gi"&gt;+5 % 0.0
&lt;/span&gt;&lt;span class="gh"&gt;diff --git a/test/cram/errors.t b/test/cram/errors.t
index 7c117b8..dfcac7e 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/test/cram/errors.t
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/test/cram/errors.t
&lt;/span&gt;&lt;span class="p"&gt;@@ -93,9 +105,43 @@&lt;/span&gt;
   $ tsonnet ../../samples/errors/object_outer_most_ref_out_of_scope.jsonnet
   ../../samples/errors/object_outer_most_ref_out_of_scope.jsonnet:2:13 No top-level object found
&lt;span class="err"&gt;
&lt;/span&gt;   2: local _two = $.one + 1;
      ^^^^^^^^^^^^^^^^^^^^^^^
   [1]
&lt;span class="gi"&gt;+
+
+  $ tsonnet ../../samples/errors/divide_by_zero.jsonnet
+  ../../samples/errors/divide_by_zero.jsonnet:1:0 Division by zero
+  
+  1: 5 / 0
+     ^^^^^
+  [1]
+
+
+  $ tsonnet ../../samples/errors/divide_by_zero_float.jsonnet
+  ../../samples/errors/divide_by_zero_float.jsonnet:1:0 Division by zero
+  
+  1: 5 / 0.0
+     ^^^^^^^
+  [1]
+
+
+  $ tsonnet ../../samples/errors/modulo_by_zero.jsonnet
+  ../../samples/errors/modulo_by_zero.jsonnet:1:0 Division by zero
+  
+  1: 5 % 0
+     ^^^^^
+  [1]
+
+
+  $ tsonnet ../../samples/errors/modulo_by_zero_float.jsonnet
+  ../../samples/errors/modulo_by_zero_float.jsonnet:1:0 Division by zero
+  
+  1: 5 % 0.0
+     ^^^^^^^
+  [1]
+
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not a huge difference from the reference implementation, but I like seeing the faulty operation highlighted in the source — much easier to spot and fix.&lt;/p&gt;

&lt;p&gt;Let's add the 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 8c6b53f..ac8ae48 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;@@ -29,6 +29,7 @@&lt;/span&gt; module Msg = struct
   let type_invalid_lookup_key expr = "Invalid object lookup key: " ^ expr
&lt;span class="err"&gt;
&lt;/span&gt;   (* Interpreter messages *)
&lt;span class="gi"&gt;+  let interp_division_by_zero = "Division by zero"
&lt;/span&gt;   let interp_invalid_concat = "Invalid concatenation operation"
   let interp_invalid_lookup = "Invalid object lookup"
   let interp_cannot_interpret expr = Printf.sprintf "Expression %s cannot be interpreted" expr
&lt;span class="gh"&gt;diff --git a/lib/error.mli b/lib/error.mli
index 58d7e8e..0bdd708 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_invalid_lookup_key : string -&amp;gt; string
&lt;span class="err"&gt;
&lt;/span&gt;   (* Interpreter messages *)
&lt;span class="gi"&gt;+  val interp_division_by_zero : string
&lt;/span&gt;   val interp_invalid_concat : string
   val interp_invalid_lookup : string
   val interp_cannot_interpret : string -&amp;gt; string
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;interpret_arith_op&lt;/code&gt; function only needs to pattern-match on &lt;code&gt;Int&lt;/code&gt; and &lt;code&gt;Float&lt;/code&gt; zero before the division — pretty simple. Have I mentioned how much I enjoy pattern matching?&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 24f0595..fe248b9 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;@@ -280,6 +280,8 @@&lt;/span&gt; and interpret_arith_op env (pos, bin_op, n1, n2) =
     ok (env, Number (pos, Float ((float_of_int a) *. b)))
   | Multiply, Number (_, Float a), Number (_, Float b) -&amp;gt;
     ok (env, Number (pos, Float (a *. b)))
&lt;span class="gi"&gt;+  | Divide, _, Number (_, n) when n = Int 0 || n = Float 0.0 -&amp;gt;
+    Error.error_at pos Error.Msg.interp_division_by_zero
&lt;/span&gt;   | Divide, Number (_, Int a), Number (_, Int b) -&amp;gt;
     ok (env, Number (pos, Float ((float_of_int a) /. (float_of_int b))))
   | Divide, Number (_, Float a), Number (_, Int b) -&amp;gt;
&lt;span class="p"&gt;@@ -288,6 +290,8 @@&lt;/span&gt; and interpret_arith_op env (pos, bin_op, n1, n2) =
     ok (env, Number (pos, Float ((float_of_int a) /. b)))
   | Divide, Number (_, Float a), Number (_, Float b) -&amp;gt;
     ok (env, Number (pos, Float (a /. b)))
&lt;span class="gi"&gt;+  | Modulo, _, Number (_, n) when n = Int 0 || n = Float 0.0 -&amp;gt;
+    Error.error_at pos Error.Msg.interp_division_by_zero
&lt;/span&gt;   | Modulo, Number (_, Int a), Number (_, Int b) -&amp;gt;
     ok (env, Number (pos, Int (a mod b)))
   | Modulo, Number (_, Float a), Number (_, Int b) -&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The guard clauses (&lt;code&gt;when n = Int 0 || n = Float 0.0&lt;/code&gt;) catch both integer and float zero before the division cases get a chance to run. &lt;/p&gt;

&lt;h2&gt;
  
  
  Completing the tutorial
&lt;/h2&gt;

&lt;p&gt;With those two additions in place, &lt;code&gt;samples/tutorials/arith.jsonnet&lt;/code&gt; is now fully supported -- minus the string formatting bits I'm deliberately setting aside for later. Here's the file with those commented out:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;concat_array:&lt;/span&gt;&lt;span class="w"&gt; &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="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="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;concat_string:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="mi"&gt;123&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="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;equality&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;1&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="mi"&gt;1&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;equality&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="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;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="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="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;x:&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="err"&gt;ex&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;1&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="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;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;(&lt;/span&gt;&lt;span class="mi"&gt;4&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;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;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Bitwise&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;operations&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;cast&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;int.&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;ex&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;self.ex&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;|&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="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;Modulo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;operator.&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;ex&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;self.ex&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;%&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Boolean&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;logic&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;ex&lt;/span&gt;&lt;span class="mi"&gt;4&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="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &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;(&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;&amp;lt;=&lt;/span&gt;&lt;span class="w"&gt; &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;||&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Mixing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;objects&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;together&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="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="err"&gt;b:&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="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;b:&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="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="mi"&gt;4&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;Test&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;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;field&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;in&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;obj_member:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'foo'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&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;foo:&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="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;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;formatting&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;str&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;'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;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;self.ex&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;is&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;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;self.ex&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;+&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;str&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;'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;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;self.ex&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;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;%g.'&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;self.ex&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;str&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;'ex&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;=%&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="err"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ex&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;=%&lt;/span&gt;&lt;span class="mf"&gt;0.2&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;//&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;self.ex&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;self.ex&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="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;By&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;passing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;we&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;allow&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ex&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;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ex&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;to&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;be&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;extracted&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;internally.&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;str&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'ex&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;=%(ex&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="err"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ex&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;=%(ex&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="mf"&gt;0.2&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;//&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&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;Do&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;textual&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;templating&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;entire&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;files:&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;str&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;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;ex&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;=%(ex&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="mf"&gt;0.2&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;//&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="err"&gt;ex&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;=%(ex&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="mf"&gt;0.2&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;//&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;self&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;Running it:&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/tutorials/arith.jsonnet
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"concat_array"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt; 1, 2, 3, 4 &lt;span class="o"&gt;]&lt;/span&gt;,
  &lt;span class="s2"&gt;"concat_string"&lt;/span&gt;: &lt;span class="s2"&gt;"1234"&lt;/span&gt;,
  &lt;span class="s2"&gt;"equality1"&lt;/span&gt;: &lt;span class="nb"&gt;false&lt;/span&gt;,
  &lt;span class="s2"&gt;"equality2"&lt;/span&gt;: &lt;span class="nb"&gt;true&lt;/span&gt;,
  &lt;span class="s2"&gt;"ex1"&lt;/span&gt;: 1.6666666666666665,
  &lt;span class="s2"&gt;"ex2"&lt;/span&gt;: 3,
  &lt;span class="s2"&gt;"ex3"&lt;/span&gt;: 1.6666666666666665,
  &lt;span class="s2"&gt;"ex4"&lt;/span&gt;: &lt;span class="nb"&gt;true&lt;/span&gt;,
  &lt;span class="s2"&gt;"obj"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"a"&lt;/span&gt;: 1, &lt;span class="s2"&gt;"b"&lt;/span&gt;: 3, &lt;span class="s2"&gt;"c"&lt;/span&gt;: 4 &lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="s2"&gt;"obj_member"&lt;/span&gt;: &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;String formatting is next on the list -- but there are more interesting features to tackle first, so I'm parking it for now.&lt;/p&gt;

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

&lt;p&gt;Two small additions -- object merging and division by zero -- and the arithmetic tutorial is effectively done. The pattern-matching guard clauses made the zero-division check almost embarrassingly straightforward, and the &lt;code&gt;merge_fields&lt;/code&gt; helper slotted in without touching anything else. &lt;/p&gt;

&lt;p&gt;You can check the entire diff &lt;a href="https://gitlab.com/-/snippets/5969112" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;If you, too, believe dividing by zero should be an error and not a NaN, &lt;a href="https://bitmaybewise.substack.com/" rel="noopener noreferrer"&gt;subscribe&lt;/a&gt; and let's keep each other honest.&lt;/p&gt;

&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@lg17" rel="noopener noreferrer"&gt;Lance Grandahl&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>
