Jax programming language

Experimental language project to learn how to make a compiler

Jax is my experimental language project to learn how to make a compiler and also design a language with some specific features. My aim is to design a language that is as close to ease-of-use and with metaprogramming abilities like Python but with the performance of a type-checked compiled language.

Python has a number of advantages that are attractive to developers over other languages. I will ignore syntactic differences because to me they don't make much difference.

The number one advantage is a minimal compile time. Python code is compiled at runtime into bytecode but it does it very fast. Comparitively, C++ can take up to 10 seconds for a basic program which in my opinion is simply unacceptable. I believe that the complexity of C++ as a language is the root cause of slow compile times, not the fact that it compiles to machine code. If you look at C it compiles faster and still has to do type-checking and compile to machine code. Even a non-optimized machine code program will be faster than Python. All we have to do is write a compiler for a language that's less complex than C++.

The second advantage is that Python has incredible metaprogramming abilities. Often you want to write some code that generalized a struct/data class in your program. With Python you can inspect any object at runtime and see what methods/fields it has. This means libraries can be written extremely easily that is generic for any class. It also means class definitions can be extension points for a library, for example in Marshmallow or Django ORM a class is a schema that the underlying library uses to generate functions, methods and other structures. This means to generate a JSON view of an object you can just pass it to a library function.

A lot of people assume that this is only possible using a dynamic language, or at least with a language with runtime type reflection like Java etc. Why should this be the case? I believe that almost all runtime type reflection that you want to do can be instead done at compile time. If the class is known at compile time then library code can run at compile time to generate functions, methods and other structures and be generated into machine code. This would mean the same ease-of-use as passing an object to a function to generate JSON, but the code would already be compiled at compile-time.

This requires that the programming language can interact with your code at compile time, in the same way that Python can inspect type information at runtime. One of the pain points of C and C++ is the infuriating macro system. This is the current mechanism that people have to use for your program to communicate with the compiler. Effectively its a really basic preprocessor that has nothing to do with your actual underlying code. It seems obvious to me that if we made a language with first class support for compile-time constructs in the exact same language as the regular program, we could have great metaprogramming abilities and completely replace any preprocessor magic.

Currently the language looks like this. The syntax is kinda like Go but it doesn't have a garbage collector. Its implementation is based off http://craftinginterpreters.com but with a lot of my own features (type-checking is the main one). It's almost a fully single-pass compiler - it compiles functions out of order so you don't have to forward declare anything. It tokenizes, parses, typechecks and generates AST in one pass. Currently the AST is then conerted into C code and compiled directly, because I don't want to mess with machine code yet.

code example goes here

You can run any code at compile time using a static block. You can then call add_code to add code to the executable. This code is executed using a bytecode VM.

Lambdas are NOT first class citizens at runtime, but they are at compile-time. That means you can construct a lambda (created using the block keyword) and pass it around as a first class object in a static block. Runtime functions can also accept a lambda as a parameter as long as the function is inlined. Because the function and lambda is inlined it means you can access the outer scope of the lambda like a closure, but without any heap allocations. It also means you can return early from within a lambda, the following is compiled to a single while loop. No magic.