=== Goal A memory-safe language with a simple translator to x86 that can be feasibly written without itself needing a translator. Memory-safe: it should be impossible to: a) create a pointer out of arbitrary data, or b) to access heap memory after it's been freed. Simple: do all the work in a 2-pass translator: Pass 1: check each instruction's types in isolation. Pass 2: emit code for each instruction in isolation. === Overview of the language A program consists of a series of type, function and global variable declarations. (Also constants and tests, but let's focus on these.) Type declarations basically follow Hindley-Milner with product and (tagged) sum types. Types are written in s-expression form. There's a `ref` type that's a type-safe fat pointer, with an alloc id that gets incremented after each allocation. Memory allocation and reclamation is manual. Dereferencing a ref after its underlying memory is reclaimed (pointer alloc id no longer matches payload alloc id) is guaranteed to immediately kill the program (like a segfault). # product type type foo [ x : int y : (ref int) z : bar ] # sum type choice bar [ x : int y : point ] Functions have a header and a series of instructions in the body: fn f a : int -> b : int [ ... ] Instructions have the following format: io1, io2, ... <- operation i1, i2, ... i1, i2 operands on the right hand side are immutable. io1, io2 are in-out operands. They're written to, and may also be read. User-defined functions will be called with the same syntax. They'll translate to a sequence of push instructions (one per operand, both in and in-out), a call instruction, and a sequence of pop instructions, either to a black hole (in operands) or a location (in-out operands). This follows the standard Unix calling convention. Each operand needs to be something push/pop can accept. Primitive operations depend on the underlying processor. We'd like each primitive operation supported by the language to map to a single instruction in the ISA. Sometimes we have to violate that (see below), but we definitely won't be writing to any temporary locations behind the scenes. The language affords control over registers, and tracking unused registers gets complex, and besides we may have no unused registers at a specific point. Instructions only modify their operands. In most ISAs, instructions operate on at most a word of data at a time. They also tend to not have more than 2-3 operands, and not modify more than 2 locations in memory. Since the number of reads from memory is limited, we break up complex high-level operations using a special type called `address`. Addresses are strictly short-term entities. They can't be stored in a compound type, and they can't be passed into or returned from a user-defined function. They also can't be used after a function call (because it could free the underlying memory) or label (because it gets complex to check control flow, and we want to translate each instruction simply and in isolation). === Compilation to 32-bit x86 Values can be stored: in code (literals) in registers on the stack on the global segment Variables on the stack are stored at *(ESP+n) Global variables are stored at *disp32, where disp32 is statically known Address variables have to be in a reg
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.