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
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);
}
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:
'x
expands to $code x
.,x
expands to $insert "%" x
. $insert takes a format string and varargs of 'Code' which it inserts into the corresponding percent signs.x.y.z
expands to $field x "y" "z"
. $field can take varargs of strings, symbols, or numbers and evaluates to that field/element.