<?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>Forem: Bryan MacLee</title>
    <description>The latest articles on Forem by Bryan MacLee (@bryan_maclee).</description>
    <link>https://forem.com/bryan_maclee</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%2F3886582%2F3438885a-1dec-48e3-8207-e888be43bb88.png</url>
      <title>Forem: Bryan MacLee</title>
      <link>https://forem.com/bryan_maclee</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/bryan_maclee"/>
    <language>en</language>
    <item>
      <title>Null was a billion-dollar mistake. Falsy was the second.</title>
      <dc:creator>Bryan MacLee</dc:creator>
      <pubDate>Sun, 19 Apr 2026 21:10:21 +0000</pubDate>
      <link>https://forem.com/bryan_maclee/null-was-a-billion-dollar-mistake-falsy-was-the-second-3o61</link>
      <guid>https://forem.com/bryan_maclee/null-was-a-billion-dollar-mistake-falsy-was-the-second-3o61</guid>
      <description>&lt;h1&gt;
  
  
  Null was a billion-dollar mistake. Falsy was the second.
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;"I call it my billion-dollar mistake. It was the invention of the null reference in 1965."&lt;br&gt;
— Tony Hoare, 2009&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Tony Hoare gets to call it a billion-dollar mistake because he invented &lt;code&gt;null&lt;/code&gt; and watched the industry pay for it for forty years. Most of us inherited that mistake and added our own contributions on top. JavaScript, in particular, made two design choices in the 1990s that we are still paying for in 2026 and will keep paying for in any new codebase that ships tomorrow.&lt;/p&gt;

&lt;p&gt;The first is having &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt; as two distinct values for the same thing.&lt;/p&gt;

&lt;p&gt;The second is "falsy."&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://scrml.dev" rel="noopener noreferrer"&gt;scrml&lt;/a&gt; — a single-file, full-stack reactive web language — partly because I got tired of paying. The intro post is over &lt;a href="https://dev.to/bryan_maclee/introducing-scrml-a-single-file-full-stack-reactive-web-language-9dp"&gt;here&lt;/a&gt;; this post is about the absence-and-truthiness fix that scrml makes possible because it is its own language with its own rules. It's also a rant, because some design decisions deserve one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake 1: two values for "no value"
&lt;/h2&gt;

&lt;p&gt;JavaScript decided in 1995 that there should be both &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt;. They mean almost the same thing. They are not interchangeable. They behave subtly differently across operators, methods, JSON, equality, and type coercion.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;         &lt;span class="c1"&gt;// "object"     ← yes, really&lt;/span&gt;
&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;    &lt;span class="c1"&gt;// "undefined"&lt;/span&gt;

&lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;   &lt;span class="c1"&gt;// true   (loose equality treats them as the same)&lt;/span&gt;
&lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;  &lt;span class="c1"&gt;// false  (strict equality does not)&lt;/span&gt;

&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// '{"a":null}'      ← undefined is silently dropped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where does each one come from? You can't predict it. &lt;code&gt;null&lt;/code&gt; shows up wherever a developer typed it. &lt;code&gt;undefined&lt;/code&gt; shows up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;when a variable is declared but not initialized (&lt;code&gt;let x;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;when an object property doesn't exist (&lt;code&gt;obj.missing&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;when an array index is out of bounds (&lt;code&gt;arr[999]&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;when a function returns nothing (&lt;code&gt;function f() {}&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;when optional chaining short-circuits (&lt;code&gt;a?.b?.c&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;when destructuring a missing key (&lt;code&gt;const { x } = {}&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;when a function parameter is omitted (&lt;code&gt;f()&lt;/code&gt; where &lt;code&gt;f(x)&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some libraries normalize on &lt;code&gt;null&lt;/code&gt;. Some normalize on &lt;code&gt;undefined&lt;/code&gt;. Most do whatever the original author thought was right that day. You do not get to know in advance which one any given codepath will hand you.&lt;/p&gt;

&lt;p&gt;So we write defensive code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or the clever version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&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="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// != catches both null and undefined&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The clever version works because of loose equality. So now your "safe" check requires the operator your linter has spent five years trying to get rid of. Every codebase has both forms. Every code review is a small argument about which to use. None of this is solving a problem; it is mopping up a design choice from 1995.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake 2: falsy
&lt;/h2&gt;

&lt;p&gt;The second mistake is bigger. JavaScript decided that in any boolean context — &lt;code&gt;if&lt;/code&gt;, &lt;code&gt;while&lt;/code&gt;, &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, &lt;code&gt;||&lt;/code&gt;, the ternary — six values should evaluate as &lt;code&gt;false&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;""&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;null&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;undefined&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NaN&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is called &lt;em&gt;falsy&lt;/em&gt;. It is the most dangerous abstraction in the language.&lt;/p&gt;

&lt;p&gt;The danger is that &lt;em&gt;falsy conflates absence with valid-but-zero/empty&lt;/em&gt;. Those are different things, and "different things" is the entire reason types exist.&lt;/p&gt;

&lt;p&gt;A counter at &lt;code&gt;0&lt;/code&gt; is not absent. An empty string is a valid string. &lt;code&gt;NaN&lt;/code&gt; is a real result in the number domain. &lt;code&gt;false&lt;/code&gt; is the boolean answer to a question, not a missing one. &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt; mean something else entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;showCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                    &lt;span class="c1"&gt;// ← bug&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Total: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No items&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;showCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// "No items"  ← wrong, there are 0 items, that IS a count&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every JavaScript developer has shipped this bug. It comes back in different shapes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;// fails on legit empty name&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;// fails on legit 0 timeout&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;// fails on legit empty-string body&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="c1"&gt;// works (kind of) but only by coincidence&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is to write the actual question you meant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="c1"&gt;// "is this absent?"&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="c1"&gt;// "is this positive?"&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;// "is this nonzero?"&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// "is this a number at all?"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each of those is a different question. &lt;code&gt;if (count)&lt;/code&gt; answered all four at once and produced one answer for all four. That's not a feature; that's a category error baked into the language.&lt;/p&gt;




&lt;h2&gt;
  
  
  What strict TypeScript does fix, and what it doesn't
&lt;/h2&gt;

&lt;p&gt;TypeScript's &lt;code&gt;strictNullChecks&lt;/code&gt; is genuinely good. It forces you to type optionality explicitly (&lt;code&gt;T | null&lt;/code&gt;, &lt;code&gt;T | undefined&lt;/code&gt;) and rejects code that doesn't handle the absence case. It also gives you &lt;code&gt;??&lt;/code&gt; (nullish coalescing) and &lt;code&gt;?.&lt;/code&gt; (optional chaining), which treat &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt; together — an implicit acknowledgement from the language designers that distinguishing them was a mistake.&lt;/p&gt;

&lt;p&gt;What strict TS does NOT fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt; are still &lt;strong&gt;two distinct values&lt;/strong&gt;. The type system tracks them; the runtime still has both.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;falsy rule is unchanged&lt;/strong&gt;. TS does not touch JavaScript's runtime semantics. &lt;code&gt;if (count)&lt;/code&gt; still silently fails on &lt;code&gt;0&lt;/code&gt;. The compiler will not warn you. Your linter might, if you've configured it. Most people have not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TS strict is the best you can do without designing a new language. It is also not enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  What scrml does
&lt;/h2&gt;

&lt;p&gt;scrml is a new language, so it has the privilege of fixing both mistakes at the source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is one value for absence.&lt;/strong&gt; It is called &lt;code&gt;not&lt;/code&gt;. The keywords &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt; are not valid identifiers in scrml source. Writing them is a compile error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let x = not                    // ok
let x = null                   // E-SYNTAX-042
let x = undefined              // E-SYNTAX-042
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;There is one way to check for absence.&lt;/strong&gt; It is &lt;code&gt;is not&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (x is not) { handleAbsence() }
if (x is some) { handlePresence(x) }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;There is no falsy rule.&lt;/strong&gt; Boolean contexts accept booleans. The only &lt;code&gt;false&lt;/code&gt; thing is &lt;code&gt;false&lt;/code&gt;. The only absent thing is &lt;code&gt;not&lt;/code&gt;. &lt;code&gt;0&lt;/code&gt; is a number. &lt;code&gt;""&lt;/code&gt; is a string. They are not "false-ish" or "absent-ish" — they are zero and empty, respectively, and you have to ask the question you actually meant.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (count is not) { ... }      // "no value"
if (count == 0) { ... }        // "zero"
if (count &amp;gt; 0) { ... }         // "positive"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three different questions. Three different answers. The language refuses to let you confuse them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Narrowing is enforced by the type system.&lt;/strong&gt; When a variable is &lt;code&gt;T | not&lt;/code&gt;, you can't use it as &lt;code&gt;T&lt;/code&gt; until you've handled the absence case. The way you do that is &lt;code&gt;given&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;${
    given x =&amp;gt; {
        // x is T here, not T | not
        // the | not has been narrowed away
        use(x)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;given&lt;/code&gt; block runs only when &lt;code&gt;x is some&lt;/code&gt;, and inside it the compiler narrows &lt;code&gt;x&lt;/code&gt; to its non-absent type. Forgetting to handle absence is not "best practice"; it is a compile error.&lt;/p&gt;

&lt;p&gt;For pattern matching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;${
    match x {
        not       =&amp;gt; handleAbsence()
        given x   =&amp;gt; handlePresence(x)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Match on a &lt;code&gt;T | not&lt;/code&gt; without a &lt;code&gt;not&lt;/code&gt; arm? Compile error (E-MATCH-012). Exhaustiveness for absence is forced.&lt;/p&gt;




&lt;h2&gt;
  
  
  What about the JS interop?
&lt;/h2&gt;

&lt;p&gt;The compiler emits plain JavaScript. JavaScript libraries hand back &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt; indiscriminately. Doesn't this all fall apart at the boundary?&lt;/p&gt;

&lt;p&gt;No. &lt;code&gt;is not&lt;/code&gt; compiles to a check that catches both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;x is not       →    (x === null || x === undefined)
x is some      →    (x !== null &amp;amp;&amp;amp; x !== undefined)
given x        →    if (x !== null &amp;amp;&amp;amp; x !== undefined) { ... }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chaos stays in the runtime. The source stays clean. When a JS library returns either, the absence check still works correctly. You write one form; the compiler emits the safe form for you.&lt;/p&gt;

&lt;p&gt;The same applies to SQL results (&lt;code&gt;?{}.get()&lt;/code&gt; returns &lt;code&gt;T | not&lt;/code&gt;, not &lt;code&gt;T | null&lt;/code&gt;), to optional fields, to function returns. Everywhere absence might come from, the language gives you one way to express it and one way to handle it.&lt;/p&gt;




&lt;h2&gt;
  
  
  "But this is more keystrokes than &lt;code&gt;if (x)&lt;/code&gt;"
&lt;/h2&gt;

&lt;p&gt;Yes. Eight characters more, in fact: &lt;code&gt;if (x is some)&lt;/code&gt; vs &lt;code&gt;if (x)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It is also a category of bug that does not exist in scrml programs. The line where you wrote &lt;code&gt;if (count)&lt;/code&gt; and thought you handled zero correctly is the line that cost your team a day of debugging. The line where you wrote &lt;code&gt;if (user)&lt;/code&gt; and the API returned &lt;code&gt;null&lt;/code&gt; once for one user is the line that put a 500 in front of one customer for a week.&lt;/p&gt;

&lt;p&gt;Languages do not have a moral obligation to be terse. They have a moral obligation to make wrong programs hard to write. JavaScript, by treating absence as identical to zero and empty and false in boolean contexts, made a particular class of wrong program very easy to write. We have been paying for that ever since with sentry alerts, post-mortems, and "I swear it worked locally."&lt;/p&gt;

&lt;p&gt;The trade is a few extra characters for one fewer category of recurring bug. It is the most obvious trade in language design and it is the trade JavaScript could not make in 1995, because it had a ten-day deadline and was trying not to break the web.&lt;/p&gt;

&lt;p&gt;We don't have that excuse anymore.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it / follow along
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Site:&lt;/strong&gt; &lt;a href="https://scrml.dev" rel="noopener noreferrer"&gt;https://scrml.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repo + spec:&lt;/strong&gt; &lt;a href="https://github.com/bryanmaclee/scrmlTS" rel="noopener noreferrer"&gt;https://github.com/bryanmaclee/scrmlTS&lt;/a&gt; — see &lt;code&gt;compiler/SPEC.md&lt;/code&gt; §42 for the full &lt;code&gt;not&lt;/code&gt; semantics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intro post:&lt;/strong&gt; &lt;a href="https://dev.to/bryan_maclee/introducing-scrml-a-single-file-full-stack-reactive-web-language-9dp"&gt;Introducing scrml&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The living compiler post:&lt;/strong&gt; &lt;a href="https://dev.to/bryan_maclee/scrmls-living-compiler-23f9"&gt;It's Alive — A Living Compiler&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X / Twitter:&lt;/strong&gt; &lt;a href="https://x.com/BryanMaclee" rel="noopener noreferrer"&gt;@BryanMaclee&lt;/a&gt; — there's a thread version of this argument up there if you prefer the rant in 14 tweets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If &lt;code&gt;null&lt;/code&gt; vs &lt;code&gt;undefined&lt;/code&gt; has bitten you in the last month, I want to hear the story. If you think falsy is fine and I'm overreacting, I want to hear that too — there's a defence of the design choice and I haven't heard one that holds up under load yet.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>scrml's Living Compiler</title>
      <dc:creator>Bryan MacLee</dc:creator>
      <pubDate>Sun, 19 Apr 2026 16:06:01 +0000</pubDate>
      <link>https://forem.com/bryan_maclee/scrmls-living-compiler-23f9</link>
      <guid>https://forem.com/bryan_maclee/scrmls-living-compiler-23f9</guid>
      <description>&lt;h2&gt;
  
  
  It's Alive
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"It's alive! It's alive!" — Henry Frankenstein, 1931&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you read &lt;a href="https://dev.to/bryan_maclee/introducing-scrml-a-single-file-full-stack-reactive-web-language-9dp"&gt;the intro post&lt;/a&gt;, you saw scrml's headline pitch: one file, full stack, compiler does everything.&lt;/p&gt;

&lt;p&gt;This post is about the design choice that scares me the most. The one where, every time I describe it, somebody pauses and says &lt;em&gt;wait, you're doing **what&lt;/em&gt;&lt;em&gt;?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;scrml has a &lt;strong&gt;living compiler&lt;/strong&gt; (in phase 4 of impl). The codegen layer isn't fixed. Community-contributed transformations compete for usage, the population decides which graduate to canonical, and the compiler evolves with the ecosystem instead of with a release cycle.&lt;/p&gt;

&lt;p&gt;That's the monster. Let me explain how it's stitched together.&lt;/p&gt;




&lt;h2&gt;
  
  
  The frozen compiler is convenient until it isn't
&lt;/h2&gt;

&lt;p&gt;Compilers, in the normal world, are frozen things. A standards committee meets. RFCs are drafted. Implementations follow. New patterns wait years. The codegen layer, in particular, is sacred; it's where the language's identity lives.&lt;/p&gt;

&lt;p&gt;That model has good reasons behind it. Stability. Reproducibility. A clear blame surface when something breaks. You know what your compiler does because someone with merge rights decided what it does.&lt;/p&gt;

&lt;p&gt;The cost of that model is everything that doesn't fit committee timelines. A new browser API ships and you wait three years for the codegen to adopt it. A pattern emerges in real apps that &lt;em&gt;would&lt;/em&gt; compile to better output, but the maintainers can't justify the risk to existing users. A specific domain (games, dashboards, embedded) has codegen needs that the general-purpose path will never optimise for.&lt;/p&gt;

&lt;p&gt;In the JavaScript world, we route around this with &lt;strong&gt;userland libraries&lt;/strong&gt;. We &lt;code&gt;npm install&lt;/code&gt; the new pattern, write a runtime adapter, and pay the cost in bundle size, indirection, and supply-chain risk. The compiler stays frozen and the ecosystem grows messy around it.&lt;/p&gt;

&lt;p&gt;scrml does the opposite. The compiler stays &lt;em&gt;alive&lt;/em&gt; and the package layer goes away.&lt;/p&gt;




&lt;h2&gt;
  
  
  The argument I'm extending
&lt;/h2&gt;

&lt;p&gt;This is not my argument. It's an argument &lt;a href="https://www.gingerbill.org/" rel="noopener noreferrer"&gt;gingerBill&lt;/a&gt; has been making for years with &lt;a href="https://odin-lang.org/" rel="noopener noreferrer"&gt;Odin&lt;/a&gt;, and I'm pushing it one step further.&lt;/p&gt;

&lt;p&gt;gingerBill's case — in talks, blog posts, and the Odin language itself — is that &lt;strong&gt;the package layer is the problem&lt;/strong&gt;. Central registries, transitive dependencies, opaque update cadences aren't solving a problem; they &lt;em&gt;are&lt;/em&gt; the problem, restated. Odin's answer is radical and consistent: no package manager, no registry, vendor everything, dependencies-as-liabilities.&lt;/p&gt;

&lt;p&gt;scrml inherits that premise completely. Every time you read "no npm, no transitive trust, vendor-everything" in this post, that's gingerBill's argument, and it's worth reading him directly before reading the rest of what I'm about to say.&lt;/p&gt;

&lt;p&gt;The living compiler, is scrml taking his rejection seriously. Then going one step further. Odin rejects package management. scrml rejects it too, and then notices that even if you vendor every library, the &lt;strong&gt;compiler itself is still a frozen dependency&lt;/strong&gt; — and freezing the compiler has its own costs. So we keep the registry idea, but make it distribute &lt;em&gt;compiler transformations&lt;/em&gt; instead of runtime code, and let the population drive what graduates.&lt;/p&gt;

&lt;p&gt;That's the moon-shot. It only exists because gingerBill already made (proved, IMO) the first half of the argument.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "living" means
&lt;/h2&gt;

&lt;p&gt;Inside the compiler, a scrml program decomposes into a graph of &lt;strong&gt;transformation signatures&lt;/strong&gt; — typed shapes that say "this construct, in this context, with this type, becomes this output." A signature is the unit of codegen. The compiler ships with one canonical transformation per signature in the box, and that's what your code compiles down to today.&lt;/p&gt;

&lt;p&gt;Now imagine the next part:&lt;/p&gt;

&lt;p&gt;A developer writes an alternative transformation for one of those signatures. Maybe their version emits faster code for hot paths. Maybe it produces smaller output for a specific browser target. Maybe it specializes for an architecture pattern the canonical path didn't anticipate. They publish it.&lt;/p&gt;

&lt;p&gt;Other developers can opt in to that alternative — explicitly, by &lt;code&gt;use&lt;/code&gt;-ing it, the same way you'd &lt;code&gt;use&lt;/code&gt; a syntax extension or a stdlib module. The compiler picks it up, runs it through a verification pass, and starts emitting that codegen for matching signatures in projects that opted in.&lt;/p&gt;

&lt;p&gt;The interesting part isn't that alternatives can exist. It's what happens next.&lt;/p&gt;

&lt;p&gt;The compiler observes which alternatives are getting used. Population-level signals — adoption, regression rate, performance deltas, error counts — feed a quality gate. Alternatives that stay green and grow adoption past a threshold &lt;em&gt;graduate to canonical&lt;/em&gt;. Alternatives that regress get demoted. The canonical transformation for a given signature is whatever the population, through actual use, has converged on.&lt;/p&gt;

&lt;p&gt;The compiler evolves with the ecosystem. Not with a release cycle. Not with a committee. But with the people writing apps in it.&lt;/p&gt;




&lt;h2&gt;
  
  
  This isn't a package registry. It's a registry of &lt;em&gt;transformations&lt;/em&gt;.
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;npm&lt;/code&gt; distributes &lt;strong&gt;runtime code&lt;/strong&gt;. Every dependency you install becomes JavaScript that runs in your app. Transitive dependencies pull more JavaScript. The blast radius of a single bad package is everything downstream that runs your code, including code you didn't write and didn't read.&lt;/p&gt;

&lt;p&gt;scrml's living-compiler registry distributes &lt;strong&gt;compile-time codegen patterns&lt;/strong&gt;. A transformation alternative isn't a runtime library — it's a function that takes an AST node and emits compiled output. It runs once, at build time, in the compiler's process. Its output is the same kind of plain JS the canonical transformation would have emitted. There's no transitive dependency tree because compiled output isn't a dependency. Your app links against the compiler, not against the alternatives the compiler considered.&lt;/p&gt;

&lt;p&gt;That changes the threat model in ways that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No transitive runtime exposure.&lt;/strong&gt; A bad transformation can emit bad code, but it can't pull in 47 sub-dependencies that each get to run on your users' devices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output is verifiable.&lt;/strong&gt; The compiler runs a verification pass over emitted code (sandboxed evaluation, type-shape check, regression suite). A transformation that emits something the verifier rejects never ships.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The blast radius is the signature, not the app.&lt;/strong&gt; A bad transformation affects the codegen for one signature, in projects that explicitly opted in to that alternative. It doesn't get to touch unrelated parts of your code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;npm&lt;/code&gt;'s problem isn't packages. It's the runtime trust model around packages. The living-compiler registry has a different trust model because it distributes a different thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Yes, you're allowed to be suspicious — there's a trust gradient
&lt;/h2&gt;

&lt;p&gt;Three tiers, named at project creation. Pick the one that matches your paranoia level:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;scrml init&lt;/code&gt;&lt;/strong&gt; (default — the living compiler in full). Quality-gated alternatives are pulled in based on signatures in your code. Audit log committed to your repo so you can see exactly which transformations the build used. Fast, evolves with the ecosystem, takes the population's word for what's canonical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;scrml init --stable&lt;/code&gt;&lt;/strong&gt; (lock-file mode). Same registry, but pinned. Your project locks the transformation set at init time and only updates when you say so. You get the population's choices but on your release cadence, not theirs. This is the closest analogue to a typical "lockfile + versioned deps" workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;scrml init --secure&lt;/code&gt;&lt;/strong&gt; (vendor-everything). No registry trust. All transformations live in your repo, copied in as source, audited by you. The compiler never reaches out at build time. The population's signals don't touch your build. This is the &lt;a href="https://www.gingerbill.org/" rel="noopener noreferrer"&gt;gingerBill&lt;/a&gt; / Odin philosophy: dependencies are liabilities; vendor everything.&lt;/p&gt;

&lt;p&gt;The default is the bold one. The other two exist because not every project wants to be on the bleeding edge of the ecosystem's collective opinion, and that's fine.&lt;/p&gt;




&lt;h2&gt;
  
  
  The objections, plus what we're doing about them
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"You're going to need telemetry to make this work."&lt;/strong&gt; Yes — opt-in, anonymised, signature-shaped (no source code, no project identifiers). Population signals are &lt;em&gt;aggregate&lt;/em&gt; counts: how many projects use this transformation, what fraction green-build with it, what their build-time and bundle-size deltas are. Projects on &lt;code&gt;--secure&lt;/code&gt; participate in nothing. Projects on &lt;code&gt;--stable&lt;/code&gt; opt in to whichever surface they want. The default tier is opt-in by default to a minimal surface; the consent prompt at &lt;code&gt;scrml init&lt;/code&gt; is concrete about what's measured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"The supply-chain attack surface is enormous."&lt;/strong&gt; It's a real surface, and it's the reason the verification pass and signing exist. Every transformation runs in a sandbox at build time. Output is verified against a regression suite before it can graduate. Transformations are cryptographically signed by their authors. Compromised authors get revoked at the registry layer. The threat model is documented and the mitigations are first-class concerns, not afterthoughts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Who decides when an alternative graduates?"&lt;/strong&gt; The population does, against quality gates the compiler enforces. Humans confirm. The graduation step is git-committable to the registry repo, which is itself open. This is closer to the canary-test model used in big distributed systems than to a maintainer-pick or vote. The system recommends, humans decide, the recommendation is auditable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"This sounds like it could go very wrong."&lt;/strong&gt; It could. The closing argument for it is also the closing argument against it: &lt;strong&gt;the compiler grows with its users&lt;/strong&gt;. If the ecosystem produces bad transformations, the ecosystem owns that. There is no maintainer-of-last-resort to blame. The compiler's identity is whatever the people writing apps in it have collectively converged on. That's either the most exciting governance story in language design or a slow-motion catastrophe, and honestly the difference depends on what we build now to make verification, signing, and graduation work as well as the verification of the language itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where it is today
&lt;/h2&gt;

&lt;p&gt;scrml is pre-1.0. The full living-compiler vision — the registry, the graduation pipeline, the telemetry surface — is &lt;strong&gt;Phase 4&lt;/strong&gt; work. The foundations are landing now: the &lt;code&gt;use&lt;/code&gt; keyword that lets you opt in to vendored extensions, the &lt;code&gt;vendor/&lt;/code&gt; model that makes copy-everything trivial, the &lt;code&gt;^{}&lt;/code&gt; meta layer that lets local code override compiler behaviour today. Those are the rails the registry rolls onto.&lt;/p&gt;

&lt;p&gt;What that means for you, today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;use vendor:my-extension&lt;/code&gt; import path &lt;strong&gt;works&lt;/strong&gt; — you can write a transformation alternative as a vendored module and use it in your project right now. It just doesn't graduate anywhere.&lt;/li&gt;
&lt;li&gt;The compiler is open and watchable. You can read the canonical transformations, write your own, and see what the registry will eventually distribute.&lt;/li&gt;
&lt;li&gt;The threat model and verification design are in the open. The deep-dives are in the &lt;a href="https://github.com/bryanmaclee/scrmlTS" rel="noopener noreferrer"&gt;scrml-support repo&lt;/a&gt; under &lt;code&gt;docs/deep-dives/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the idea sounds interesting, the most useful thing is to write a couple of &lt;code&gt;vendor:&lt;/code&gt; transformations now and see how it feels. The shape of those is the shape of what the registry will distribute later.&lt;/p&gt;




&lt;h2&gt;
  
  
  It's alive
&lt;/h2&gt;

&lt;p&gt;Henry Frankenstein doesn't get to keep his monster. The story is about hubris, the cost of stitching things together you didn't fully understand, and the obligations you take on when you give something its own life.&lt;/p&gt;

&lt;p&gt;I think about that a lot. The living compiler is the part of scrml's design that makes me, the maintainer, least comfortable — and the part I'm most certain about. Frozen compilers are conservative for good reasons. Frozen compilers also calcify. The web ecosystem already has one of those, and we paid for it with &lt;code&gt;node_modules&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The bet here is that giving the codegen layer its own life, with verification and signing and population-level quality gates around it, lets the ecosystem grow without needing a kingmaker — and that the obligations that come with that are obligations worth taking on.&lt;/p&gt;

&lt;p&gt;It's alive. Now we have to keep it that way.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it / follow along
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Landing + quick start:&lt;/strong&gt; &lt;a href="https://scrml.dev" rel="noopener noreferrer"&gt;https://scrml.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repo + spec:&lt;/strong&gt; &lt;a href="https://github.com/bryanmaclee/scrmlTS" rel="noopener noreferrer"&gt;https://github.com/bryanmaclee/scrmlTS&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X / Twitter:&lt;/strong&gt; &lt;a href="https://x.com/BryanMaclee" rel="noopener noreferrer"&gt;@BryanMaclee&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Living-compiler design notes:&lt;/strong&gt; look in &lt;code&gt;scrml-support/docs/deep-dives/&lt;/code&gt; for &lt;code&gt;transformation-registry-design-2026-04-08.md&lt;/code&gt; and &lt;code&gt;debate-transformation-registry-2026-04-08.md&lt;/code&gt; — the architecture is open-source, including the arguments against it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this design choice seems sketchy. Thats because it is. I am building this language to push the envelope into the future, or off a cliff. Either way, should be a fun ride.&lt;/p&gt;

&lt;p&gt;Reply here, on X (&lt;a href="https://x.com/BryanMaclee" rel="noopener noreferrer"&gt;@BryanMaclee&lt;/a&gt;), or open an issue on the repo.&lt;/p&gt;

</description>
      <category>compiling</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Introducing scrml: a single-file, full-stack reactive web language</title>
      <dc:creator>Bryan MacLee</dc:creator>
      <pubDate>Sat, 18 Apr 2026 21:28:46 +0000</pubDate>
      <link>https://forem.com/bryan_maclee/introducing-scrml-a-single-file-full-stack-reactive-web-language-9dp</link>
      <guid>https://forem.com/bryan_maclee/introducing-scrml-a-single-file-full-stack-reactive-web-language-9dp</guid>
      <description>&lt;h2&gt;
  
  
  Introducing scrml
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;scrml&lt;/strong&gt; is a compiled language that puts your markup, reactive state, scoped CSS, SQL, server functions, WebSocket channels, and tests in the same file — and lets the compiler handle everything in between. The compiler splits server from client, wires reactivity, routes HTTP, types your database schema, and emits plain HTML/CSS/JS. No build config, no separate route files, no state-management library, no &lt;code&gt;node_modules&lt;/code&gt; mountain.&lt;/p&gt;

&lt;p&gt;This post is an introduction. scrml is pre-1.0 and &lt;strong&gt;not production-ready&lt;/strong&gt; — the language surface will still shift, some diagnostics are rough, and I'm sharing it now mainly to get design feedback before things calcify.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why another language?
&lt;/h2&gt;

&lt;p&gt;A typical modern web app spreads across five or more tools and files. You have React on the client, Node or Next on the server, a state library (Redux, Zustand, Jotai...), a separate router config, an API layer keeping client and server types in sync, a CSS system, a build toolchain, and your ORM. Each of those tools makes reasonable local choices, and collectively they produce the sprawl everyone complains about but nobody quite knows how to fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if the compiler owned the whole stack?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the bet. Same file, same language, one compile pass. The compiler knows which functions are reachable from the client and which stay on the server, because it parses both. It knows which &lt;code&gt;@var&lt;/code&gt; is read in which DOM node, because it builds a dependency graph. It knows your SQL schema, because it ran the schema extraction. So it can enforce things across those boundaries instead of leaving them to runtime coordination.&lt;/p&gt;




&lt;h2&gt;
  
  
  A counter in one file
&lt;/h2&gt;

&lt;p&gt;Here's the entire program:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;program&amp;gt;

${
  @count = 0
  @step = 1

  function increment() { @count = @count + @step }
  function decrement() { @count = @count - @step }
  function reset()     { @count = 0 }
}

&amp;lt;div class="flex flex-col items-center gap-6 p-8 min-h-screen bg-gray-50"&amp;gt;
  &amp;lt;h1 class="text-3xl font-bold text-gray-800"&amp;gt;Counter&amp;lt;/h1&amp;gt;
  &amp;lt;p class="text-6xl font-bold text-blue-600"&amp;gt;${@count}&amp;lt;/p&amp;gt;

  &amp;lt;div class="flex gap-2"&amp;gt;
    &amp;lt;button class="px-5 py-2 text-lg bg-red-500 text-white rounded-lg cursor-pointer hover:bg-red-600" onclick=decrement()&amp;gt;−&amp;lt;/button&amp;gt;
    &amp;lt;button class="px-5 py-2 text-lg bg-gray-200 rounded-lg cursor-pointer hover:bg-gray-300" onclick=reset()&amp;gt;Reset&amp;lt;/button&amp;gt;
    &amp;lt;button class="px-5 py-2 text-lg bg-green-500 text-white rounded-lg cursor-pointer hover:bg-green-600" onclick=increment()&amp;gt;+&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;

  &amp;lt;label class="flex items-center gap-2 text-sm text-gray-600"&amp;gt;
    Step:
    &amp;lt;input type="number" class="w-16 p-1 text-center border border-gray-300 rounded" bind:value=@step min="1" max="100"&amp;gt;
  &amp;lt;/label&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;/program&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@count&lt;/code&gt; and &lt;code&gt;@step&lt;/code&gt; are reactive variables — language primitives, not library wrappers. &lt;code&gt;bind:value&lt;/code&gt; is two-way binding. &lt;code&gt;onclick=increment()&lt;/code&gt; wires the handler. The compiler emits direct DOM updates (no vdom, no diffing) for every site that reads &lt;code&gt;@count&lt;/code&gt;. The Tailwind utility classes above compile via a &lt;strong&gt;built-in Tailwind engine&lt;/strong&gt; — no &lt;code&gt;tailwind.config.js&lt;/code&gt;, no &lt;code&gt;postcss&lt;/code&gt;, no content scan — so utility CSS works out of the box and you can still drop into a scoped &lt;code&gt;#{}&lt;/code&gt; block when utilities aren't enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three things that are different
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. State is a first-class type
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;&amp;lt; Card&amp;gt;&lt;/code&gt; declares a state type; &lt;code&gt;&amp;lt;Card&amp;gt;&lt;/code&gt; instantiates one. HTML elements like &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; &lt;strong&gt;are&lt;/strong&gt; state types — the language treats them uniformly with your own types. Every state value flows through &lt;code&gt;match&lt;/code&gt;, through &lt;code&gt;fn&lt;/code&gt; signatures, and across the server/client boundary with static checks. The compiler knows what shape a &lt;code&gt;Contact&lt;/code&gt; is, which fields are &lt;code&gt;protect&lt;/code&gt;ed (server-only), and which routes need to serialize what. You write Types once; they hold across the stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Any variable can carry a compile-time contract
&lt;/h3&gt;

&lt;p&gt;Contracts come in three flavours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Value predicate&lt;/strong&gt; — reject out-of-range writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@price: number(&amp;gt;0 &amp;amp;&amp;amp; &amp;lt;10000) = 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Presence lifecycle&lt;/strong&gt; (&lt;code&gt;lin&lt;/code&gt;) — must be consumed exactly once; the compiler refuses a double-use or a silent drop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lin token = fetchCsrfToken()
submitForm(token)    // consumed — compile error if you used it twice or not at all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;State transitions&lt;/strong&gt; — only legal moves allowed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type DoorState:enum = { Locked, Unlocked }

&amp;lt; machine name=DoorMachine for=DoorState&amp;gt;
    .Locked   =&amp;gt; .Unlocked
    .Unlocked =&amp;gt; .Locked
&amp;lt;/&amp;gt;

@door: DoorMachine = DoorState.Locked
@door = .Unlocked    // ok
@door = .Locked      // ok — Unlocked =&amp;gt; Locked is declared
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;lt; machine&amp;gt;&lt;/code&gt; rejects illegal transitions at &lt;strong&gt;both&lt;/strong&gt; compile time and runtime. If the compiler can prove the destination is unreachable from the current state, it errors then and there. If something dynamic sneaks through (a network response, user input routed into a transition), the runtime enforces the same table. One source of truth, two layers of enforcement.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. N+1 gets rewritten automatically
&lt;/h3&gt;

&lt;p&gt;A pattern like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for (let user of users) {
  let orders = ?{`SELECT * FROM orders WHERE user_id = ${user.id}`}.all()
  ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;becomes a single &lt;code&gt;WHERE user_id IN (...)&lt;/code&gt; pre-fetch plus a keyed &lt;code&gt;Map&lt;/code&gt; lookup. Because the compiler owns both the query context and the loop context, it can see that the per-iteration query is a safe batching candidate. When it isn't safe, you get a &lt;code&gt;D-BATCH-001&lt;/code&gt; diagnostic with the exact disqualifier and a &lt;code&gt;?{...}.nobatch()&lt;/code&gt; escape hatch.&lt;/p&gt;

&lt;p&gt;Measured on on-disk WAL &lt;code&gt;bun:sqlite&lt;/code&gt; (median of 50 iterations after 5 warmups, table size 1000, full results in &lt;a href="https://github.com/bryanmaclee/scrmlTS/blob/main/benchmarks/sql-batching/RESULTS.md" rel="noopener noreferrer"&gt;&lt;code&gt;benchmarks/sql-batching/RESULTS.md&lt;/code&gt;&lt;/a&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th&gt;Baseline (ms)&lt;/th&gt;
&lt;th&gt;Optimized (ms)&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0.0111&lt;/td&gt;
&lt;td&gt;0.0057&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.95×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;0.1068&lt;/td&gt;
&lt;td&gt;0.0410&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.60×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;0.5124&lt;/td&gt;
&lt;td&gt;0.1654&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.10×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;1.0490&lt;/td&gt;
&lt;td&gt;0.2625&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.00×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Upper bound is &lt;code&gt;SQLITE_MAX_VARIABLE_NUMBER&lt;/code&gt; (32,766). Network-attached storage would widen the gap further.&lt;/p&gt;




&lt;h2&gt;
  
  
  There's more that didn't fit here
&lt;/h2&gt;

&lt;p&gt;To keep this post focused, I left out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WebSocket channels&lt;/strong&gt; (&lt;code&gt;&amp;lt;channel&amp;gt;&lt;/code&gt;) — auto-generated upgrade routes, auto-reconnect, and &lt;code&gt;@shared&lt;/code&gt; vars that sync across connected clients&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web Workers as nested &lt;code&gt;&amp;lt;program&amp;gt;&lt;/code&gt;s&lt;/strong&gt; — heavy work compiles to a worker with typed RPC and supervised restarts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoped CSS&lt;/strong&gt; (&lt;code&gt;#{}&lt;/code&gt;) that applies only inside the component it's declared in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typed SQL&lt;/strong&gt; where the schema extraction feeds back into type checking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inline tests&lt;/strong&gt; as language constructs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each is worth its own writeup. I'll do follow-up posts if there's interest.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where it is today
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pre-1.0&lt;/strong&gt; — breaking changes likely, rough edges, not production-ready&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MIT&lt;/strong&gt; — compiler is fully public&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6,800+ tests passing&lt;/strong&gt; — every language feature is test-covered&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~45 ms&lt;/strong&gt; — full compile for a TodoMVC-sized app on a 2021 laptop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bun today&lt;/strong&gt; — Node/Deno ports are possible but not priorities&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try it / follow along
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Landing + quick start:&lt;/strong&gt; &lt;a href="https://scrml.dev" rel="noopener noreferrer"&gt;https://scrml.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repo + spec:&lt;/strong&gt; &lt;a href="https://github.com/bryanmaclee/scrmlTS" rel="noopener noreferrer"&gt;https://github.com/bryanmaclee/scrmlTS&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X / Twitter:&lt;/strong&gt; &lt;a href="https://x.com/BryanMaclee" rel="noopener noreferrer"&gt;@BryanMaclee&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The spec&lt;/strong&gt; (if you want the depth): &lt;code&gt;compiler/SPEC.md&lt;/code&gt; in the repo — sections 14 (types), 18 (match), 42 (lifecycle), 51 (machines), 52 (server authority) are the meatiest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback on the design is the thing I'm actually optimising for right now. If something looks wrong, looks over-engineered, looks like it'll trip on a real app — I want to hear about it here, on X (&lt;a href="https://x.com/BryanMaclee" rel="noopener noreferrer"&gt;@BryanMaclee&lt;/a&gt;), or as an issue on the repo. Happy to chase threads before the language surface calcifies.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
