Our initial type setting function didn't assign the Dynamic type to
DefDynamic forms; after this commit, it will. It also wasn't an
exhaustive match, leading to ugly non-exhaustive pattern match errors in
rare cases. This commit adds a clause to prevent that.
This commit adds a new obj, `LetDef` which we use to give let bindings a
similar form to Def bindings and DefDynamic bindings. This enables us to
type check `set!` calls on let bindings just as we do def bindings and
defdynamic bindings.
Now that we properly omit all member fields for Unit types, the C
structs we instantiate cannot initialize *any* fields, rendering the
zero-initialization syntax invalid. We use an empty struct instead.
This commit enables support for using values of type () (Unit) in
user-defined types such as product and sumtypes. After this commit,
types such as:
(deftype Units [action-one () action-two ()])
Are valid, and can be instantiated in the obvious way:
(Units.init (IO.println "foo") ())
Some important things to note about the implementation:
- The C structs emitted for types containing Unit members *completely
omit all unit members*. If a type in Carp has N members, the
corresponding C struct will have (N-U) members where U is the number of
members with the type `Unit`.
For example, this type:
(deftype (Foo [one Unit two Int]))
will produce the following typedef in C:
typedef struct {
int two;
} Foo;
As a special case, types that *only* have Unit's as members are represented and
initialized as completely empty structs:
(deftype Foo [empty Unit])
// emits
typedef struct {
} Foo;
Foo Foo_init() {
Foo instance = {};
return instance;
}
Such a type is merely a container for side effects.
- Side effects are not stored at all in the types that contain Unit
members. Instead, any side effects will be lifted out of the emitted C
struct and called prior to initialization.
For example, initializing `(deftype Foo [empty Unit])` with `(Foo.init
(IO.println "foo"))` will produce the following C:
main(...) {
//...
static String _10 = "foo";
String *_10_ref = &_10;
IO_println(_10_ref);
Foo _12 = Foo_init();
//...
}
- The typical operations on product fields are supported on Unit type
members, but they have slightly custom semantics. Since we don't
actually store any values of type Unit in custom types, most
updaters/getters/setters simply run a side effect.
This is mostly only supported to make the use of such members more
intuitive and allow programmers to chain side-effects within some
context, much like monadic IO in Haskell.
- Match forms also work on Unit types for parity, but again, there is no
meaningful behavior here, since Unit only has a single type
inhabitant.
As a bonus, this commit also makes it possible to use `Unit` and `()`
interchangeably in type signatures.