Rob J Meijer avatar

Version 0.3 of the Merg-E language specification : Flow control and exceptions.

pibara

Published: 10 Jun 2026 › Updated: 10 Jun 2026Version 0.3 of the Merg-E language specification : Flow control and exceptions.

Version 0.3 of the Merg-E language specification : Flow control and exceptions.

merg-e.jpg

This is part twenty-three in a series on the language spec for the Merg-E Domain Specific Language for the InnuenDo Web 3.0 stack. This post refers to both version v0.3 and version v0.4 of the language spec. I'll add more parts to the below list as the spec progresses:

  • part 1 : coding style, files, merging, scoping, name resolution and synchronisation
  • part 2 : reverse markdown for documentation
  • part 3 : Actors and pools.
  • part 4 : Semantic locks, blockers, continuation points and hazardous blockers
  • part 5 : Semantic lexing, DAGs, prune / ent and alias.
  • part 6 : DAGs and DataFrames as only data structures, and inline lambdas for pure compute.
  • part 7 : Freezing
  • part 8 : Attenuation, decomposition, and membranes
  • part 9 : Sensitive data in immutables and future vault support.
  • part 10 : Scalars and High Fidelity JSON
  • part 11 : Operators, expressions and precedence.
  • part 12 : Robust integers and integer bitwidth generic programming
  • part 13 : The Merg-E ownership model, capture rules, and the --trustmebro compiler flag.
  • part 14 : Actorcitos and structural iterators
  • part 15 : Explicit actorcitos, non-inline structural iterators, runtimes, and abstract scheduler pipeline.
  • part 16 : async functions and resources and the full use of InnuenDo VaultFS
  • part 17: RAM-Points, RAM-points normalization bag, and the quota-membrane.
  • part 18: Literal operators & Rational and Complex numbers.
  • part 19: Interaction between operators, integer bitwidth generics, and the full numeric type-system.
  • part 20 (v0.4): Compile-time dimensional analysis, SI/Planck units and the scaling literal operator.
  • part 21 (v0.4): Tensors and tensor literals.
  • part 22 (v0.4): Deprecating float/complex for rquantity/cquantity for full dimensional type-safety.
  • part 23 : Flow control and exceptions.
  • part24: The merge parts of Merg-E
  • part25: The $threshold keyword and logging.

In earlier chapters we showed both flow control constructs and error handling, but both were mostly in code examples and underspecified. We went into details a bit when discussing actorcitos and structural iterators, and disclosed that flow control is just an abstraction, let's call it a macro or a skin-transform on top of these primitives. In this post we are going to address that. While looking at the actual skin-transforms, I ran into problems. Problems that we need to address by invalidating earlier code examples. We don't invalidate any actual language spec from earlier posts, but some of the code samples are now invalid (I'll go back to the posts later to fix this and possible other inconsistencies in sample code).

A little word on what is what in the Merg-E execution pipeline.

As discussed before, Merg-E is designed to have multiple runtimes. I'm currently targeting just the testing runtime, a runtime that has the working name fafnir. This name refers to the half dragon Fáfnir, a smaller dragon that started out as a dwarf. This is a fitting name, because Níðhǫggr is the Merg-E scheduler and (for future runtimes) memory manager, the name of a much larger dragon, so think of fafnir as a wannabe Níðhǫggr. The fafnir runtime will run on top of Python and will run an almost human readable (and slow) third stage parse tree on top of it's minimal Níðhǫggr and workers implementation. Towards the future, we envision multiple runtimes:

  • fafnir : A testing runtime on top of Python meant for language design evaluation. Runs HF-JSON third stage parse tree.
  • nidhogg-s : A compiled generic runtime. Runs both HF-JSON third stage parse tree and bytecode third stage parse tree.
  • nidhogg-e : A compiled extended runtime. Suitable inline (lambda) code compiled. Executable embedded bytecode.
  • jormungandr : Like nidhogg-e but with extensive use of SYCL.
  • nidhogg-b : A version of nidhogg-s for the BEAM virtual machine.
  • fucanglong : An imagined single threaded zkVM runtime. Should be nidhogg-s like, functionally, but aimed specifically at zero knowledge proof of code existence.

The third stage parse tree is generated by a set of three other tools in the pipeline:bones : Does structure and literal parsing and partial lexing. Any token that isn't structure, literal, or core operator, or fully canonically specified is left unresolved.

  • bones : creates a first stage parse tree.
  • muscle : Does name resolution and least authority lexical scoping. After this phase, the resulting second stage parse tree is fully canonical, and all the parse tree adheres to the language least authority guarantee. Muscle creates a second stage parse tree.
  • skin : Maps skin-transforms in the second stage parse tree to actorcitos and structural iterators that can run on the Níðhǫggr/workers of the target runtime.

Finally we have edsger, the linter. The name Edsger is a remnant of an earlier candidate name for Merg-E, that was meant as a tribute to Edsger Dijkstra. It would have been a second choice if Mark Miller would not have given me permission to name the language Merg-E (after his E language that inspired the least authority model of Merg-E). After introducing hazardous and the --trustmebro flag to Merg-E, the choice for Merg-E over Edscher became more irreversible because professor Dijkstra would have hated those Merg-E aspects. But for a super strict linter that will go beyond what bones, muscle and skin enforce, naming that linter edsger seems perfectly fitting.

deprecating switch/case and reimagining the error body.

In earlier examples we saw this bit of code:

      }!!{
        switch $scope.exceptions[-1] {
          range_error : {
            ...
            };
          };
          exception : {
            ...
            };
        };

Note exception is our catch all. Use sparingly.

It assumed the error body to be a monolith that would need to handle all errors in a big switch/case like construct. Mapping things to structural iterators and actorcitos this created a problem, because basically a switch is flow control, and needing flow control inside of the smallest building block of flow control creates a bit of an issue as you can imagine.

Next to that, switch itself doesn't really fit all that well into the structural iterator mindset in the first place.

Because of all of this, I decided that switch is no longer part of the language spec, it was never part of the spec other than in sample code, but nevertheless by lack of a better word, switch is now deprecated.

As a result of this deprecation, and for maintained convenience, we need to redefine the error scope as more of a dictionary:

      }!!{
        range_error : {
          ...
          },
        exception : {
          ...
          }};

v0.4 only: exception aliases

As we discussed before, exceptions in v0.3 and v0.4 of the language are type only for least authority reasons, and Merg-E has no user defined types. But as we saw with v0.4 tensor frames, we can predefine a collection of types as a resource in the language that a disciplined developer can then allias to. In future versions of the language we may want to add proper least authority resource management on these types, but for now a collection of pre-defined user types that we can alias should do the trick.

We define usererror1024 up to usererror2047 as a range of 1024 exception types that a user can alias to when they take good care to avoid collisions.

Like we saw with tensor frames, we can claim these through a resource consume.

alias $scope.export.not_a_prime_error = consume resource.errors;

As before, this creates an alias to a type, a usererror type, that we can raise and catch.

Handling unhandled exceptions

In Merg-E, any invocation that is not inline or otherwise part of an inline or lambda pattern, and that is either not bound to a blocker, or is part of a blocker that is never awaited, will not bubble up like you may expect from having used other languages. It is fire and forget as far as the invoker is concerned.

When an error occurs in an invokable and that error isn't handled by the invokable's own error body, that error body will attach a deep error to the execution state of any blocker it is attached to. Here v0.3 and v0.4 diverge, in v0.3, if it isn't attached to any actual blocker, the exception is just silently discarded, or the runtime might log something to stderr, but no programmatic intervention is possible. In v0.4 however, the runtime creates a blocker for the invokable's scope and will attach it to a cleanup slate, more on that later.

The syntax for handling deep exceptions is close to before, but now the map is nested:

}!!{
  range_error : {
    ...
    },
  deep : {
    range_error {
      ...
      }},
  exception : {
    ...
    }};

v0.4 only: Program level cleanup slate

In v0.4 we add an extra optional program level body map to main. Where main itself has a regular error body, we define a cleanup slate body just for main using !!!!{} instead of !!{}. This is a very powerful part of the program and should be used with absolute care, because it has access to the scope, not just of the invokable that raised the immediate error, but also the scope of invocables that originally threw an error deeper down the chain. The syntax with nested maps is a bit tricky but this is done to try and add some level of least authority to the leaves of the error catching tree. Please note that one level of deep is already implied. Note that this body will never emit any errors itself. Also note that all matches will be processed, not just the most shallow ones, and they will be processed deep to shallow.

}!!!!{
  range_error : {
    ...
    },
  deep : {
    range_error {
      ...
      },
    deep : {
      range_error {
        ..
        }}},
  exception : {
    ...
    }};

It is important to note that $scope within this body is just the normal scope of main. But $scope.scope contains the entire scope of whatever raised the exception.

if

Now let's look at flow control. The simplest one we need to look at is if. In Merg-E an if looks like this:

if x == 4 {
  ...
  }{
    ...
    }!!{
      ...
      };

In other languages this would be a try catch around an if/then/else. In Merg-E it's a simple three-body if macro.

foreach

A second flow control macro handled by skin is foreach. This foreach is the flow control closest to the implementation in actorcitos and structural iterators.:

foreach myiterable {
  ...
  }!!{
    ...
    };

Note that $scope.current will always hold a reference to the current item. Note, and this is a detail of the skin transform that has no big practical use, that this mechanism goes even for an if, where $scope.current will reference a static whole8 (v0.4) or uint8 (v0.3) with a value of 0.

If we want an empty iterable to trigger an error condition we use the noempty modifier.

noempty foreach myiterable {
  ...
  }!!{
    empty : {
      ...
      };

If we want to do something special when the iterable is exhausted, we can use noexhaust.

noexhaust foreach myiterable {
  ...
  }!!{
    exhausted : {
      ...
      };

If we want to tell the runtime that the items in the iterable might be processed in any order and in parallel, we define the foreach as reentrant.

reentrant foreach myiterable {
  ...
  }!!{
    
    }

take

While foreach will work fine on a whole or uint, it is idiomatic to use the alias take for such cases.

take 20 {
  ...
  }!!{
    ...
    };

Again, we can use it with noexhaust

noexhaust take 20 {
  ...
  }!!{
    exhausted : {
      ...
      }};

until and while

While other languages often prefer 'while' over 'do while', in Merg-E 'do while' is called until, and it is considered idiomatic to use it over while when possible. Note that Merg-E does NOT allow unbounded until or while loops. A whole/uint bound should always be given. and an exhausted handler should be defined:

until done == true 4096 {
  ...
  }!!{
    exhausted : {
    }};

Note that validating if all possible exceptions are handled isn't a compiler (bones/skin) task in Merg-E, it is a linter (edsger) task.

Finally, the while. Use sparsingly, until rich code is considered more readable idiomatic Merg-E code:

while done == false 4096 {
  ...
  }!!{
    exhausted : {
    }};

Note that the reason to use while sparingly is triple. An until heavy code base makes the code look more distinctly Merg-E flavoured, it shows a clear intent when while is actually used as the exception to the rule, and it prevents for-nothing scheduling if the while condition is met immediately.

Conclusions

In this post we specified a few error handling constructs and flow control macros that thus far had been underspecified and/or accidentally misrepresented because of underspecification in earlier sample code. I hope this better specification will be enough to complete the implementation of a first skin implementation, and that the error handling (partially delayed to v0.4 for that reason) will turn out to be complete and workable enough to serve the language. It is important to note that especially the error handling v0.4 features are currently still very much in flux as the least authority implications of the current design are not completely clear. Experiments with v0.3 should show us if we are on the right track, or if the preliminary v0.4 specs should be revised.

Leave Version 0.3 of the Merg-E language specification : Flow control and exceptions. to:

Written by

Author of #HIVE-first-published mythpunk novel "Ragnarok Conspiracy", multidisciplinary engineer and data/infosec geek, working on making #coinZdense a reality.

Read more #merg-e posts


Best Posts From Rob J Meijer

We have not curated any of pibara's posts yet. But you can encourage our curation team to review posts by visiting them regularly and by referring other readers. Because we give priority to frequently read content.

More Posts From Rob J Meijer