Language.

May 10, 2025

I have a problem. Every time I sit down to start a project it goes well... until it doesn't. It usually takes about one night of sleep, or 500 lines, whichever comes first, to realize I won't be able to continue my project. It has either become too much for me to hold in my head or there's some pitfall in my language of choice that seems insurmountable. Now the first problem can likely only be solved by therapy (e.g. writing this article) to get over my fears, but that still leaves us problem number two.

"If you want something done right you often have to do it yourself." I knew this, but ignored it for as long as could. Now the time has come, because I recognized the pattern: I will never be emotionally satisfied enough to finish a project in a language I don't respect. These languages I'm bashing aren't fundamentally wrong most of the time; they just don't jive with what I want from a language. But that doesn't mean I won't take their good qualities and mash them all together into a frankenstein's monster of beauty(?).

The list of languages that I'll use as inspiration for this language is quite large. Here we go.

Okay, so what does that inspiration make? Well here's my favorite syntax so far.

$using ($import "syntax/odin-d-amalgamation")

enum Code_Kind
  IDENTIFIER
  KEYWORD
  INTEGER
  FLOAT
  STRING
  TUPLE

union Code_Data
  : as_atom string
  : as_tuple [dynamic]^Code

struct Code
  : kind Code_Kind
  : data Code_Data
  using data

struct Parse_Result
  : code ^Code
  : next-pos uint

proc parse_code () Parse_Result
  ; important stuff goes here
  return (Parse_Result #code nil #next-pos p)

That's quite literally the AST structure of the compiler. It's meant to be as simple to parse as Lisp, with a bit of added complexity in the implicitness of parentheses on each newline, and indentation being the method of grouping multiple expressions without parentheses. You may have noticed the $ in front of $using and $import; these are builtins. Builtins are the only thing the interpreter provides. The default environment is empty. I'm trying to keep the number of builtins below 20... we'll see how that goes. There are no reserved keywords.

Anyway, all forms must expand down to the builtins for any work to be done. The interpreter has no knowledge of compilation. Compilation is simply performed by importing a module that's full of procedures and macros which boil down to builtins. These macros take the code, perform their manipulation of it, and output whatever you requested from them. If I want a regular program like I'd have in C it would look like this:

$define void ($type 'VOID)
$define c-char ($type 'INTEGER '(#bits 8 #signed true))
$define c-int ($type 'INTEGER '(#bits 32 #signed true))
$define char* ($type 'POINTER '(#kind 'MULTIPLE #child c-char))
$define 'printf
  $extern 'printf #library "msvcrt"
    $type 'PROCEDURE '(#parameters (char*) #return c-int #callconv 'C #flags 'VARARGS)

$proc 'main '() void #callconv 'C
  printf "The answer to the ultimate question is %d.\n" 42

$using ($import "compilation")
create-pe64-executable "My Cool Project" #entry main #subsystem 'WINDOWS #target 'AMD64

The "create executable" macro can just convert all $extern builtins to static/dynamic libraries' procedure calls, etc. As you can see, using builtins is quite ugly compared to the custom syntaxes you can make to wrap them. Of course it's easy to go overboard, but like Cakelisp, we put the trust in the programmer and give them the tools to shoot off their feet. There could be fewer builtins, such as removing $using since it's only about three lines to implement it at user-level. But having it built-in makes one-line syntax switches possible, so it stays for now.

For now it's called Z, because it's better than C, D, and V. And possibly because I am nostalgic for H1Z1: King of the Kill Pre-season 3. Now wish me luck I can settle on an implementation language and not give up on making my dream come true. Fortunately, the syntax is so expressive that I could most certainly use it as an integrated shell language and compiled language for a simple operating system (still complex just simpler than the bloat that is Windows).

Email me and let me know what you think. Peace. github

UPDATE: May 19, 2025

I'VE HAD IT! These languages are all trash. I discovered Nelua, which was really quite nice, even though I hate Lua. It has a great comptime evaluator (literally just embedded Lua). Unfortunately, I couldn't use it as my implementation language because the `hashtable` module appears to have a bug which causes my lookups to not work 75% of the time. Also it didn't have forward referencing so I was forced to use e.g. `sequence(pointer)` instead of `sequence(*Code)`. What I've decided after much fumbling in design is that I, as a human, want something that doesn't resemble Lisp at all. It's just too annoying for moderately complex expressions to be parsed by a human easily. Where does that leave us? Well I've thought more about what I want from a language and decided on more features.

And here's what that looks like in my mind right now.

using import(#basic.types, only=.[Code, U32, String])

KEYWORDS :: Code.[
  #struct,
  #enum,
  #union,
  #for,
  #while,
  #if,
  #ifx,
  #else,
  #using,
  #import,
  #mixin,
  // ...
];

Token :: struct {
  Kind :: enum {
    IDENTIFIER;
    for KEYWORDS mixin("KEYWORD_%;\n", it);
  }

  kind: Kind;
  location: U32;
  as_string: String;
}

Here `import` could be implemented at user-level (it's built-in because its hard to import an import library without a built-in import). Its signature would look something like:

`import` :: (path: Code, only: ?[]Code = null, except: ?[]Code = null) {
  assert(!only or !except);
  return read_entire_file(path.stringof).compile_as_struct.only_or_except(only, except);
}

UPDATE 2: May 20, 2025

Forget everything from the previous update. I discovered Terra. It's almost exactly what I want from a language. It is still Lua-like, though. Or more accurately: it is Lua... with an embedded DSL inside of it that can be extracted and produce native executables. However, I can't tell how well maintained it is, so I am scared to use it for production. That doesn't matter anyway, because I want the SAME language to be the metaprogramming environment and the embedded DSL. Which leads us back to Lisp-style; it's just too good. I then discovered two things: 1. builtins can just be macros, so they are aliasable by the user; 2. macros are just procedures which don't evaluate their 'Code' arguments and their body chooses which arguments to evaluate. Also, syntax clusters in my previous iteration would occur (e.g. inside of parameter lists would be many nested parentheses). This can be fixed with macros. The body can just expect symbols as separators (good for human visual parsing) and completely ignore them for execution. Everything else is still just an S-expression, and macros must result in S-expressions as well. Okay, how are we looking now? Like this:

$using ($import "syntax/alpha") #only '(:: := = ! .^ Type Code Void Bool ^ true false proc macro union struct return `return defer)

proc Result (T : Type | E : Type) Type
  union Inner_Data
    value T
    error E
  struct Inner
    #using Inner_Data
    ok Bool
  return Inner

macro try (result : Result) ($type_of Result.value) ; macro so `return can be used
  if (! result.ok) (`return result)
  return result.value

macro return-ok (value : Code) Void
  `return '(#value ,value #ok true)

macro return-err (error : Code) Void
  `return '(#error ,error #ok false)

enum Allocation-Error
  OUT_OF_MEMORY

proc new (T : Type) (Result (^ T) Allocation-Error)
  return-err 'OUT_OF_MEMORY

proc example () (Result (^ Bool) Allocation-Error)
  := bool-ptr (try (new Bool))
  defer (= (.^ bool-ptr) true)
  = (.^ bool-ptr) false
  return-ok bool-ptr

Along with those changes comes the sugar: