Flax Lang Flax Versions Save

general purpose programming language, in the vein of C++

0.41.2

4 years ago

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.

0.41.3

4 years ago
  1. 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.

  2. 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.

0.41.0

4 years ago

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:

  1. 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.

  2. 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:

  3. 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")                                                    
   |                         ‾‾‾‾

0.40.0

4 years ago

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 directive

This 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 directive

As 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.

implementation details

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).

0.34.0

5 years ago

Transparent fields are now possible inside structs and @raw unions, 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.

0.33.0

5 years ago

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.

0.31.1

5 years ago

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.

0.31.0

5 years ago

Couple of small changes:

  1. Static access now uses :: instead of ., along the veins of C++ and Rust.
  2. Polymorph instantiations now take the form of Foo!<...> (the exclamation mark) -- making it less likely to be ambiguous.
  3. Polymorph arguments can now be positional, meaning Foo!<int> instead of Foo!<T: int>. The rules are similar to that of funtion calls -- no positional arguments after named arguments.

0.30.0

5 years ago

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:

  1. Near-complete rewrite of the middle-end, including a proper typechecker
  2. Syntax of types changed:
    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 
  1. Named arguments in function calls:
    let q = pow(base: 2, index: 3.1)
  1. Splatting of arrays into variadic functions:
    let y = [ 1, 2, 3, 4 ]
    let x = some_func(x: 30, y: 40, ...y)
  1. Class inheritance
  2. Using namespaces, enums, and unions:
    namespace foo { fn some_func() { } }
    namespace bar { fn other_func() { } }

    using foo as f
    using bar as _
    f.bar()
    other_func()
  1. More robust alloc:
    let dim = 30
    alloc Foo ("some", "args") [dim] {
        // code for each element
        it.id = i // it is the element, i is the index
    }
  1. Mutable and immutable pointers and slices, pointers are now immutable by default:
    let m: &mut int = ...
    let i: &int = ...
    let s: [mut T:] = ...
  1. String literals now have a type of [char:] aka slice of char
  2. Specifying the type of an array literal:
    let x = [as f64: 1, 2, 3, 4]
  1. Dollar $ as alias for .length in subscripts (similar to D):
    let y = x[:$-1]   // this slices from the first element to the second-last element.
  1. Tagged unions, which can be generic:
    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)
  1. Chained comparison operators (shortcircuiting)
    let x = 30;
    let t = 10 < x < 31 < 491   // t == true

    // syntax sugar for:
    let t1 = (10 < x) && (x < 31) && (31 < 491)

0.24.2

7 years ago

Small improvements to life:

  1. Automatic struct comparison with == and !=, using memcmp.
  2. Made passing overloaded functions by name into things slightly less stupid