-
Notifications
You must be signed in to change notification settings - Fork 3
FAQ
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.
Indeed, 'flect' isn't a word by itself. It's a suffix meaning "to bend". Make of that what you will!
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.
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.
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.
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.
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
, andrestrict
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.
In bindings, simply omit the modifier. To get atomic semantics on operations, use the core::atomic
module.
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.
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;
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
.
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;
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.
Good question! Like the Rust developers, we just sort of like it that way. We are probably poor, misguided C souls.
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
, passingint
as a type parameter. - A less-than comparison between the value
foo
and the valueBar
.
An alternative would be to require programmers to write:
foo::<Bar>();
This is what Rust does. We opted for a uniform, unambiguous syntax instead.
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.
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.
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.
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).
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 ∫ // 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.
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.
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.
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 mixin
s are incredibly hard to debug. To make matters worse, writing them is very error-prone.
- Home
- Introduction
- Motivation
- Features
- Tutorial
- Library
- FAQ
- General
- Interoperability
- Syntax
- Type System
- Macros and CTE
- Specification
- Introduction
- Lexical
- Common Grammar Elements
- Modules and Bundles
- Type System
- Declarations
- Expressions
- Macros
- Compile-Time Evaluation
- Memory Management
- Application Binary Interface
- Foreign Function Interface
- Unit Testing
- Documentation Comments
- Style
- Indentation
- Braces
- Spacing
- Naming