Releases: flax-lang/flax
Scoped Imports, FFI-as
More QoL changes:
import
s can now either beprivate
(the default), orpublic
. 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()
}
- 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
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
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, andfoo(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")
| ‾‾‾‾
Compile-time Execution
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
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.
C-Style Unions
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
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
Couple of small changes:
- Static access now uses
::
instead of.
, along the veins of C++ and Rust. - Polymorph instantiations now take the form of
Foo!<...>
(the exclamation mark) -- making it less likely to be ambiguous. - Polymorph arguments can now be positional, meaning
Foo!<int>
instead ofFoo!<T: int>
. The rules are similar to that of funtion calls -- no positional arguments after named arguments.