general purpose programming language, in the vein of C++
Patched up the backend stuff on all platforms, and added some code to find the MSVC toolchain on Windows to let us generate exes.
All of the CI environments now test all 3 configurations: JIT with LLVM, Interpreter, and EXE output.
Arguments follow normal (gcc-style) conventions, eg:
$ flaxc foo.flx # produces an executable foo
$ flaxc -o bar foo.flx # produces 'bar'
Of course, -sysroot
should continue to be used.
When lexer errors are encountered (missing quotes or invalid tokens), we used to throw a warning:
attempting to lex file while file is already being lexed, stop it
. That happens because we want to print the context, which requires reading the file, which happens to read the tokens, which... etc. etc. We've just fixed it by returning the current state if we request file contents while lexing.
Types were not usable from imported modules. Anything that required fetching the SST node of a given fir::Type
would not work if the type in question was defined in an imported module. I guess we didn't have a test case for this, but the main issue was that the typeDefnMap
of imported modules was not actually copied to the importing module.
Fixed by a small refactor.
Functions now support optional arguments:
fn foo(a: int, b: int, c: int, x: int = 9, y: int = 8, z: int = 7)
{
printf("a = %d, b = %d, c = %d\n", a, b, c)
printf("x = %d, y = %d, z = %d\n", x, y, z)
}
Their use is fairly self-explanatory. A few things, though:
All optional arguments must be passed by name, unlike C++ or something. For example, foo(1, 2, 3, 4)
is illegal, and foo(1, 2, 3, y: 4)
must be used instead.
Optional arguments, while they must be declared after all positional (normal) parameters in a function declaration, can be passed anywhere in the argument list: foo(x: 4, 1, y: 5, 2, z: 6, 3)
will result in a call like this: foo(a=1, b=2, c=3, x=4, y=5, z=6)
. This was added mainly to reduce the friction resulting from the next limitation:
Optional arguments in a variadic function must be specified somewhere in the argument list, to prevent the variadic arguments from being "stolen" by the positional parameter (then you get an error about how you must pass the optional argument by name, even though that wasn't your intention).
For example, this:
fn qux(x: str, y: int = 3, args: [str: ...])
{
// ...
}
// ...
qux("hello, world!", "hi", "my", "name", "is", "bob", "ross")
Will result in this error:
error: optional argument 'y' must be passed by name
at: ultratiny.flx:28:26
|
28 | qux("hello, world!", "hi", "my", "name", "is", "bob", "ross")
| ‾‾‾‾
Oh yes! That's right! Things can be executed at compile time!
Currently two constructs are supported: #run
and #if
. In both of these, any and all code is valid, via the use of an interpreter that runs the Flax IR.
#run
directiveThis simply runs an expression or a block at compile-time, and saves the output (if it was an expression). For example:
fn foo(a: int, b: int) -> int => a * b
// ...
let x = #run foo(10, 20)
The generated IR will simply store the constant value 200
into x
. If a block is being run, then no value can be yielded from it (like calling a void function).
#run {
// put statements here...
}
#if
directiveAs the name suggests, this is a compile-time if, and the syntax is identical to the normal runtime if
, except that the first if
is replaced with an #if
instead. Note that the else if
and the else
remain undecorated.
Unlike preprocessor #if
in C/C++, all code inside every branch must be syntactically valid -- ie. it must be parsed correctly. However, they do not need to be semantically correct, since code that lives in the false branches are not type-checked. The main purpose of this is to do OS-specific things, for instance:
// real example from libc.flx
#if os::name == "windows"
{
public ffi fn fdopen(fd: i32, mode: &i8) -> &void as "_fdopen"
}
else
{
public ffi fn fdopen(fd: i32, mode: &i8) -> &void
}
Also, we now have an os
namespace at the top-level, which currently only has two members -- name
and vendor
, which are both strings. We currently set them like this:
#if defined(_WIN32)
name = "windows";
vendor = "microsoft";
#elif __MINGW__
name = "mingw";
#elif __CYGWIN__
name = "cygwin";
#elif __APPLE__
vendor = "apple";
#include "TargetConditionals.h"
#if TARGET_IPHONE_SIMULATOR
name = "iossimulator";
#elif TARGET_OS_IOS
name = "ios";
#elif TARGET_OS_WATCH
name = "watchos";
#elif TARGET_OS_TV
name = "tvos";
#elif TARGET_OS_OSX
name = "macos";
#else
#error "unknown apple operating system"
#endif
#elif __ANDROID__
vendor = "google";
name = "android";
#elif __linux__ || __linux || linux
name = "linux";
#elif __FreeBSD__
name = "freebsd";
#elif __OpenBSD__
name = "openbsd";
#elif __NetBSD__
name = "netbsd";
#elif __DragonFly__
name = "dragonflybsd";
#elif __unix__
name = "unix";
#elif defined(_POSIX_VERSION)
name = "posix";
#endif
In the future more things will be added, such as the version (possibly, not sure what we can extract from header files alone), compiler information, target information, etc.
The bulk of the work was in finishing up the interpreter, which lives in fir/interp
. Other than that, there are some limitations wrt. "crossing the barrier" between compile-time and runtime. Currently, only primitive types (integers, floating points, and booleans) can be "retrieved" from the interpreter.
Of course all code can be run, just that fetching the value (eg. let x = #run "x"
) will fail, because (in this case), str
isn't a supported type (yet).
Transparent fields are now possible inside struct
s and @raw union
s, and they function like the anonymous structs and unions in C.
They are declared with _
as their name, and of course there can be more than one per struct/union. For example:
struct point3
{
_: @raw union {
_: struct {
x: f64
y: f64
z: f64
}
raw: [f64: 3]
}
}
var pt: point3
(pt.x, pt.y, pt.z) = (3.1, 1.7, 5.8)
assert(pt.raw[0] == 3.1)
They don't play nicely with constructors yet, but that's been added to the list of things to do.
Of course, you may have noticed a slight change: there's no more let
or var
in front of field declarations in struct bodies, it's just name: type
now. Also, we've removed the ability for structs to have nested types and static members to simplify stuff a bit.
As the name suggests. In reality, these are a slight modification of the existing tagged unions -- just without tags. Additionally (because they are not tagged) the usage syntax is slightly different, and follows the conventional C-style of usage.
@raw union foo
{
a: i32
b: [u8: 4]
}
var f: foo
f.b[0] = 0xFF
f.b[2] = 0xFF
printf("%d\n", f.a)
// prints 16711935
The implementation is similar to that of tagged unions (and the any
type); the size of the raw union is simply the size of the largest variant. Since its purpose is type-punning, no conversions take place.
PS: all the CI environments happened to die, so there are no binaries for this release. oops.
So apparently clang doesn't let you capture structurally-bound names in lambdas:
auto [ a, b ] = std::make_tuple(1, 2);
auto lam = [a]() -> void { };
This fails... and we were using it in one place somewhere, so fixed.
Couple of small changes:
::
instead of .
, along the veins of C++ and Rust.Foo!<...>
(the exclamation mark) -- making it less likely to be ambiguous.Foo!<int>
instead of Foo!<T: int>
. The rules are similar to that of funtion calls -- no positional arguments after named arguments.The release marking the end of the rewrite!
While the rewrite
branch was merged and deleted a while ago, feature-parity and general polish was only achieved recently with the completion of generic unions.
Changes since the last release, most of which don't affect the user-facing bits:
let x: &T = null // old: let x: T* = null
let y: [T] = [ ] // old: let y: T[] = [ ]
let z: [T:] = [ ] // old: let z: T[:] = [ ]
let p: [T: 10] = [ ] // old: let p: T[10] = [ ]
let q: fn(T) -> K = f // old: let q: [(T) -> K] = f
let q = pow(base: 2, index: 3.1)
let y = [ 1, 2, 3, 4 ]
let x = some_func(x: 30, y: 40, ...y)
namespace foo { fn some_func() { } }
namespace bar { fn other_func() { } }
using foo as f
using bar as _
f.bar()
other_func()
alloc
: let dim = 30
alloc Foo ("some", "args") [dim] {
// code for each element
it.id = i // it is the element, i is the index
}
let m: &mut int = ...
let i: &int = ...
let s: [mut T:] = ...
[char:]
aka slice of char let x = [as f64: 1, 2, 3, 4]
$
as alias for .length
in subscripts (similar to D): let y = x[:$-1] // this slices from the first element to the second-last element.
union<T> Optional
{
some: T
none
}
// varying levels of inference:
let x = Optional<T: int>.some(301)
let y = Optional.some("hello")
// note: can use specific instantiations (eg. using Optional<T: int>) to only import those specific monomorphisations
using Optional as _
let z = some(3.1415)
let x = 30;
let t = 10 < x < 31 < 491 // t == true
// syntax sugar for:
let t1 = (10 < x) && (x < 31) && (31 < 491)
Small improvements to life:
==
and !=
, using memcmp
.