Overview
This article describes several design decisions and arguments supporting the direction I chose.
What Influenced Ivory's Design?
Ivory derives mainly from C. It is also influenced by SML. One can also find elements of C++ and Java.
Why Stop Partial Program Compilation at HLR?
The answer to this is twofold. First, in order to allow declaration and use of incomplete types, the partial program must be written before types are removed altogether and the size of everything is determined.
Secondly, why not? Object files as we know them are written so late because in the old days, when compiling anything more complex than factorial took 20 minutes, it made sense to compile partial programs to a format that took a minimum of processing to link. This is done at the cost of efficiency, as by the time a program is object code, it is too late to do whole-program optimizations. This, in turn leads workarounds like preprocessor macros. Compilation time in the modern world is not even an issue.
Why Not Include Dynamic Allocation? Garbage Collection?
Ivory's central principle is that it should be able to implement a freestanding OS kernel without anything else aside from a bootloader. Integrated dynamic allocation confounds two sections of the kernel in particular beyond recovery. First and foremost, the allocator itself is extremely difficult to implement in a language that uses the allocator.
Secondly, there are other areas of the kernel which are unable to use the allocator. Chief among these are the synchronization primitives and the scheduler. The synchronization primitives use the scheduler to do their work. If the scheduler uses the dynamic allocator, then the allocator cannot use the regular synchronization, and must rely on very low level mechanisms like spin locks. Spin locks are a non-reentrant form of synchronization (and any reentrant form will require the scheduler), and so they must be interrupt-atomic (which includes the scheduler interrupt).
Furthermore, in a demand-paged heap kernel, an allocation at worst requires a full swap-out. That means any kernel using dynamic allocation in its scheduler has a worst-case scheduler latency of one disk access (multiple timeslices). That is truly awful. Since the allocator must also be interrupt atomic, this means nothing gets through for a whole disk access (we can't switch threads, because that goes back into the scheduler, and that will lead us back to the allocator). That means the OS sporadically "spaces out" for several whole timeslices...Totally unacceptable.
Garbage collection would be even worse, as it is also interrupt atomic, and sporadically locks down the heap for several timeslices at a time.
Why Not Intrinsic Threading, Synchronization, et cetera?
For the same reasons I just illustrated, these cannot be included. Additionally, threading requires an external scheduler.
Why Not Run Time Type Information
Runtime type information requires dynamic allocation to create the tag structures.
Why Not Objects and Classes?
Structures (especially in Ivory) do about 90% of what objects do without requiring dynamic binding, run time type information, and dynamic allocation.
Inner Functions
Inner functions were added to make the language more expressive. Inner functions hide declarations, and reduce the number of arguments to functions in complex call trees. Furthermore, they encourage functional abstraction.
Inner functions were added to structures because they confer great benefit at little cost. Adding functions defined in the scope of a structure allows many object-oriented-like features. The only added cost is that the functions must have backlink to the structure in question. This is already paid for, however, with the implementation of inner functions in functions.
Why Not Lexical Closures?
Lexical closures, in the traditional sense, translate to the possible persistence of a stack frame after the return of a function. This requires dynamic allocation.
However, lexical closures of a sort may still find their way into Ivory. If functions are treated as a compound type, the situation is different. A function can be treated as a compound type (a sort of struct or module), which has a pointer to its code, as well as a backlink (aka static link). A function's scope can be treated as a type of structure, which can be copied to a storage location. If a function desires to return one of its inner functions, it may copy its own frame to a permanent storage location, then set the returned function value's static link to point to this storage. In this way, lexical closures can be implemented, with a little more manual work and without dynamic allocation intrinsic to the language.
At this point it is up to cost/benefit analysis, but it is definitely a possibility.
Why Not Overloading?
Overloading requires name-mangling, possibly runtime type information, introduces possible ambiguity, and makes parsing phenominally harder. Many believe it does not actually help make code more readable.
Most damningly, it confounds the storage of symbols in executables, and makes binding very difficult. All this makes dynamic loading and interfacing with other languages more difficult.
Modules
Modules isolate code, creating less confusion and encouraging abstraction. In older days, abstraction resulted in slower code, but this is no longer true. If anything, this abstraction makes program analysis and optimization easier, increasing the efficiency in all likelihood
Templates
Templates allow generic code to be written for any type. This improves management of source and promotes abstraction.
Incomplete Types
Allowing partial use and declaration of incomplete types also allows for more generic code, and also promotes abstraction, like templates.
Exceptions
C++-style exceptions are thrown based on any type, with no other means of identification. However, SML-style exceptions are declared beforehand, and are not polymorphic types. This is much friendlier to a systems language, as each exception can be given an ID, and a catch can have semantics similar to a switch statement. Exception metadata is stored to a global location, and the handler can be gotten out of a simple array. This mechanism is simple, fast, and efficient.
The only major restriction on exceptions is that a throw statement cannot cross the boundry of an execution context. Some environments may have several "execution contexts", such as user mode, kernel mode, scheduler mode, and interrupt mode. Exceptions as they are cannot cross these boundries.
The benefit from exceptions is well-appreciated. Exceptions are a powerful tool which results in much cleaner, better organized code. It is possible that they may become the single most appreciated feature of Ivory.
Pointers
Pointers were the cause of much thought. The alternative to pointers is a C++ style reference structure. However, systems languages must often directly manipulate memory. Additionally, requiring values to be initialized removes many pointer errors. Lastly, though none exist now, a managed pointer type that throws exceptions for dereferencing or arithmetic on a pointer type could be introduced. In the end, the benefits of a reference type can be gained by imposing optional restrictions on pointers. The second is more in keeping with Ivory's design.
List Literals
The list literals that appear in Ivory are essentially the same as the array or struct initializers found in C, except they can also be used in an expression. The brackets are needed to make the grammar context free.
Allowing list literals to act as left-hand values in an assignment allows the quick interpretation of a structure or array. This concept comes from SML.
Why Remove the Preprocessor?
The preprocessor is very controversial. It is the cause of both great benefit as well as many hideous hacks. The beneficial functionality of the preprocessor can be achieved with templates, incomplete types, modules, inline functions and constants, whereas the hacks are impossible. The Ivory include statement takes advantage of the fact that the language is designed to allow the contents of a file to also serve as the contents of a module. The fact that it imports declarations as opposed to straight text should cause no problem, and should serve to eliminate hacks.
Why Remove goto?
The goto statement is universally maligned, confers no real benefit, and is quite harmful. What tiny benefit in may bring in efficiency can be gotten with profiling and likelihood prediction and subexpression elimination, and the tiny improvement it may confer does not compare to the optimizations it prevents.
Why Remove Casting?
This was done after much consideration. Any legitimate use of a cast can be done with a union, or was addressed by a change to the semantics. Unlike gotos, which are mostly, if not totally harmful, casts do actually have legitimate uses in C. Of important note, however, are Java's casts, which are not actually casts, but explicit conversions. They do not "force" a conversion as in C.
Systems languages must often convert from one "type" to another to pull data out of a file, off the wire, or to create some data structure required by hardware or another execution context. Two mechanisms do exist for doing this, however. Unions provide a safer, more explicit means of converting between representations, and void pointers or arrays provide a polymorphic pointer type. The only other use of casts involved pointer arithmetic, and was used nine times out of ten on void pointers. The definition of pointer arithmetic on void pointers solves this issue.
Pointer Arithmetic on Void Pointers
This was done mostly to eliminate the need for casts, but also because in C, void pointers are quite commonly cast to char pointers for arithmetic. Realistically, the restriction has no effect.
A second argument for this is the fact that a C style void pointer can still be accomplished with a strict qualifier.
Structure Alignment
Structure alignment has been a morass in the C language for a long time. Some treat structures as strictly aligned, others do not.
Ivory splits the issue in two. Strict structures are directly controlled, non-strict structures are not controlled at all. Non strict structures are able to be aligned for cache behavior, as they are not even guaranteed to be similarly aligned in another program. Strict structures create memory structures suitable for writing to disk or wire.
Switch Statements on Any Literal
This is not official yet, but it will not take alot of convincing. This is a great improvement in expressive power, a great improvement in efficiency (avoiding many complex if statements), for a small cost.
Inline Assembly
This is intended to exist, but is not implemented yet. Inline assembly is a requirement for implementing OS kernels. It is included in Ivory to put it in a more compiler-friendly form.