Skip to content
This repository has been archived by the owner on Jun 6, 2021. It is now read-only.
Alex Rønne Petersen edited this page Jun 8, 2014 · 20 revisions

Here you'll find our answers to some commonly asked questions related to Flect's language design. Note that many of the answers are rather informal and some even slightly subjective.

General

What does the name mean? Flect isn't even a word!

Indeed, 'flect' isn't a word by itself. It's a suffix meaning "to bend". Make of that what you will!

So is Flect useful for actual systems programming - kernels, drivers, that kind of thing?

Yes. Probably the most important design goal of Flect is to be able to replace C in real world systems programming. That means being able to compile to bare metal, letting the programmer write code that doesn't use a garbage collector, and so on.

Wait, Flect has a garbage collector?!

Yes. But fear not. The garbage collector is only compiled in when targeting user land. In the bare metal target, it is left out, and it is the programmer's job to either not use managed memory boxes or implement their own GC. Even in user land, the GC can still be left out if desired.

The GC is really just there to make normal application-level programming easy. We fully recognize that GC is not for everyone.

Also note that the Flect GC lets you tune it at run time. You can disable it for periods of time, specify timeouts for collections, instruct it to allocate memory from the OS ahead of time, etc. You can also manually free GC memory, though this is very unsafe.

So is Flect meant to be a better C?

No. There is no such thing as a better C. Every single language that has tried to be a better C has failed at it. The reason is simple: C, while a fairly reasonable language for its time, has some fundamental design flaws making it a bad language to build upon for radically different language designs.

To name a few things that are wrong with C:

  • The type syntax is unwieldy and mixed with declarations.
  • It has tons of dangerous implicit conversions.
  • It uses an ancient, outdated compilation model.
  • Its approach to strings is inefficient and very error-prone.
  • The way things are declared is hard to parse and not friendly towards language extension.

These things cannot be fixed without breaking backwards compatibility. Changing any of these while building a language on top of C means that the language is no longer a proper superset - C++ is one such language.

It would make more sense to think of Flect as ML for systems programming.

Interoperability

Can I link against code written in, for example, C or C++?

Any code that conforms to the calling conventions supported by the Flect compiler can be used from Flect. That definitely includes C, and probably also other languages. Plain assembly code should work too.

There are plans for some limited C++ support, but it is not likely that it will be incredibly useful due to the extreme complexity of the C++ language. We cannot afford to implement an entire C++ parser in the Flect compiler.

What aspects of C can Flect not model?

There are some things we decided not to support, either for simplicity, because people rarely use them, or because they don't make sense in Flect. Namely:

  • The volatile, const, and restrict qualifiers. These can simply be omitted in bindings.
  • The _Complex and _Imaginary type modifiers added in C99.
  • The _Atomic type modifier added in C11. This can simply be omitted in bindings.
  • union data types. These are usually used as glorified casts, so we don't feel a need to support them.

If Flect doesn't support _Atomic, what should I use instead?

In bindings, simply omit the modifier. To get atomic semantics on operations, use the core::atomic module.

How do I get volatile semantics in Flect?

The core::volatile module provides intrinsics to perform volatile loads and stores. In C bindings, you can leave the volatile off as long as you remember to use the core::volatile module where you would normally expect volatile semantics.

Should I always omit const in bindings?

It's always safe to do so. However, in some cases, you can use Flect's imm keyword.

Consider this C function:

void foo(int const *p);

This can be translated to:

pub fn ext "cdecl" foo(*imm i32) -> unit;

Omitting the restrict keyword is a bad idea. Why would you recommend that?

Because you don't really have any other option in Flect. It wouldn't make much sense in Flect's type system, so there's no way we can model it.

Exercise care when dealing with APIs that use restrict.

How do I bind to global and thread-local variables from C?

Regular global and thread-local variables in Flect are declared like so:

priv glob foo : int = 42;
priv tls bar : int = 24;

A mut keyword can follow the glob/tls to indicate that the variable is mutable.

To bind to external variables instead, you just tack ext and a mangling convention on them and leave out the initializer:

priv glob ext "cdecl" foo : int;
priv tls ext "cdecl" bar : int;

How do I expose global or thread-local variables to other languages?

As with binding external variables, you tack ext and a mangling convention on them, but you also write an initializer:

priv glob ext "cdecl" foo : int = 42;
priv tls ext "cdecl" bar : int = 24;

Adding an initializer in addition to a mangling convention lets the compiler know that the variable is to be defined in the Flect module it's declared in, and not bound to.

Syntax

What's with the C-like syntax in a language that takes lots of concepts from ML, Haskell, and F#?

Good question! Like the Rust developers, we just sort of like it that way. We are probably poor, misguided C souls.

Why square brackets for generics instead of angle brackets?

This is to solve an ambiguity and to keep the language LL(1). If we used angle brackets, consider:

foo<Bar>();

This is ambiguous, as innocent as it looks. It can be interpreted in two ways by an LL(1) parser:

  • A call to the function foo, passing int as a type parameter.
  • A less-than comparison between the value foo and the value Bar.

An alternative would be to require programmers to write:

foo::<Bar>();

This is what Rust does. We opted for a uniform, unambiguous syntax instead.

Why is there no ternary ?: operator?

Because the if expression works like that. For example, in C, you could write:

int x = y > 0 ? y : z;

In Flect, you would write:

let x = if y > 0 { y; } else { z; };

Slightly more verbose, but it means we don't need a special construct in the language.

Type System

Does Flect have ownership types?

No. This was planned early on but was eventually decided against. Ownership types make it hard to have a unified pointer type (& in Flect), which further complicates writing generic code. We also feel that being able to put things on the stack and pass them around with ref should cover most (but admittedly not all) use cases.

The Rust language solved this problem through pointer borrowing analysis which is one approach that has its own set of limitations.

Does Flect support higher-kinded types?

Not at the moment. It's hard to say whether we'll explore adding this to the language in the future. The possibility is there, but we'll need some real use cases in systems programming to justify the added complexity.

Why the decision against a class-based object system?

Because our experiences tell us that OOP is harmful in systems programming. A full discussion is beyond the scope of this FAQ, but in short, we don't feel that OOP is an adequate abstraction for typical patterns in systems software, therefore not justifying its costs (dynamic dispatch, interface calls, slow casts, etc) and implementation complexity. Further, we strongly dislike the idea of coupling data structures with algorithms (which is why Flect has a type class system instead).

Why is there no float type that uses the biggest FP type on the machine?

We did consider adding this to make it easier to use e.g. x86's 80-bit reals, but there are some serious problems with having such a type. Most notably, it would mean that we would have to rewrite all of the standard C functions for FP math because they all operate on 64-bit reals. We also think that portability is important, which a float type would make harder to achieve.

Why can fn() -> *int not be converted to fn() -> &int when fn(*int) -> unit can be converted to fn(&int) -> unit?

This has to do with the semantics of a cast from *T to &T.

When converting some plain *T to &T, a null check is inserted. For instance:

let i : int = 42;
let x : *int = &i;
let y : &int = x as &int; // This cast performs an implicit null check.

The same happens when casting a tuple such as (*int, f32) to (&int, f32).

The reason that casting fn(*int) -> unit to fn(&int) -> unit is OK is that any value of type &T is already guaranteed to have been null-checked. So, we do not need to null-check it when passing it to the function pointer or anything like that. The same cannot be said for the return value of a function pointer; the function could easily return null and since we see it as &int we would happily think that it couldn't possibly be null, making such a design unsound.

Macros and CTE

Why can't I use macros to generate declarations?

This is for the same reason that we don't do whole-program type inference. We think that the interface of a module should be crystal clear and not be hidden behind macros, unspecified types, etc.

Why not use FFI to call external functions during CTE?

Three reasons:

  • It would make implementing the language much harder.
  • It would pretty much assume that the language a Flect compiler is implemented in has a full-featured FFI library available for the build platform.
  • It could result in very unpredictable and/or non-repeatable compilation runs.

If FFI was allowed in CTE, all non-portable code might as well be. This is clearly a bad idea.

Why aren't string mixins (as in D) supported?

They complicate a compiler a lot. Instead of simply being fed an AST structure, the compiler suddenly has to parse a random string of source code during semantic analysis.

Worse yet, non-trivial string mixins are incredibly hard to debug. To make matters worse, writing them is very error-prone.

Clone this wiki locally