Skip to content

Releases: flax-lang/flax

Scoped Imports, FFI-as

04 Oct 17:18
Compare
Choose a tag to compare

More QoL changes:

  1. imports can now either be private (the default), or public. Private imports are the old behaviour (I think -- I can't remember if we successfully removed re-exporting previously), where whatever the module imports is not transitively exported to other modules that import it. The reverse is true of public imports.

Given the following:

export foo
public fn foo() { }

// ...

export bar
public import "foo"

// ...

export baz
private import "foo" // you'll actually get a warning that it's redundant
fn main()
{
    // foo::foo is visible from bar, as bar::foo::foo()
    bar::foo::foo()

    // foo is not visible from baz!
    // baz::foo::foo()
}
  1. Declare foreign functions (ffi) as something -- these alias names participate in overloading (and the 'real' names are no longer visible). Particularly useful for a certain someone:
export gl
public ffi fn glVertex2i(x: int, y: int) as vertex
public ffi fn glVertex2f(x: float, y: float) as vertex
public ffi fn glVertex2d(x: double, y: double) as vertex
public ffi fn glVertex3i(x: int, y: int, z: int) as vertex
public ffi fn glVertex3f(x: float, y: float, z: float) as vertex
public ffi fn glVertex3d(x: double, y: double, z: double) as vertex

// ...
import "gl"
gl::vertex(1, 1) // calls glVertex2i
gl::vertex(1.0, 1.0, 0.5) // calls glVertex3d

Prebuilt Dependencies for Windows v6 -- LLVM, MPIR, MPFR, libFFI

04 Oct 17:37
Compare
Choose a tag to compare

Binaries and headers are licensed under their respective licenses.

  • Updated for LLVM 7 on 24/03/19
  • Updated to include libffi on 21/06/19
  • Updated for LLVM 9.0.0 on 18/10/19
  • Updated for LLVM 11.0.0 on 01/12/20 (old versions added for posterity)

Optional Arguments

29 Jun 12:19
Compare
Choose a tag to compare

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

Compile-time Execution

26 Jun 16:09
Compare
Choose a tag to compare

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

Transparent Fields

28 Apr 16:15
Compare
Choose a tag to compare

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.

C-Style Unions

21 Mar 02:02
Compare
Choose a tag to compare

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.

Clang Bugfix

05 Nov 17:12
Compare
Choose a tag to compare

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.

Syntax Rejiggering

05 Nov 09:58
Compare
Choose a tag to compare

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.