Appel's other compiler book, "Compiling with Continuations", goes into more depth while falling short of being a complete tutorial on writing a compiler for ML.
893 karma · joined December 13, 2010
Appel's other compiler book, "Compiling with Continuations", goes into more depth while falling short of being a complete tutorial on writing a compiler for ML.
It wasn't as effective as OoO in the end, but not for lack of attention to the problem.
It is quite possible for objects that are expected to tenure to be allocated directly into the major heap. Many systems use various criteria to decide when this should be done. In particular it is common to allocate very large objects directly to avoid ever having to copy them.
There are also static analyses which attempt to determine which objects can be safely allocated in an arena-like way, that is, region inference. In some systems in addition to inference the programmer is given ways to specify that a given object should live within a certain region. MLkit is the usual example of such a system. Region systems don't seem to have been particularly popular, but there's still some work being done on them here and there.
The return type is inferred, and the type is what the type class machinery is driven by. You can get some more visibility into that by playing in ghci:
return 0 :: [Int] => [0]
return 0 :: Maybe Int => Just 0
Having behaviour driven by return type takes a bit of getting used to.Why does it exist at all? Speaking loosely, monads are about composing functions that look like a -> m b. Here's regular composition alongside the 'monadic' one:
(.) :: (b -> c) -> (a -> b) -> (a -> c)
(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)
As you can see, the same except for the m wrapping each function's results. Just like normal function composition, there is an identity element - that's what return is. Again, a comparison: id :: a -> a
return :: Monad m => a -> m a
Again, the same except for the m wrapping the result. Both composition operators are associative, and both have the same relationship with their identity operation: id . f = f return <=< f = f
f . id = f f <=< return = f
(That's why there are monad laws, to ensure those properties hold.)So return is very straightforward: it is the identity of functions that return monads. It's unfortunate that both the name and the presentation of monads in Haskell tend to obscure this.
I would imagine this is the case for several of the other examples, too.
A major difficulty here is that undefined behaviour is a property of a particular execution of a C program, rather than a static property of the program itself. Tools that dynamically detect UB are useful, but will not demonstrate that there are no inputs for which a program will go wrong.
It's true that the given C code doesn't have those semantics, but you can see what the GP was driving at if you squint a bit. Uh, and if you know how these instructions work already.
1652 If an object that has automatic storage duration is not initialized explicitly, its value is indeterminate.
(x is an 'auto' variable in the given program.) void do_something(int *);
int f() {
int x;
do_something(&x);
return x;
}
Is the return of x a use of initialized memory? Maybe. Relink the program and the answer might change.It was not a successful idea, but there was a method to the madness.
Constants that fit in a byte don't have this problem and can be easily placed in the instruction stream using dedicated byte constant instructions, sometimes including dedicated single-byte instructions for common constants like zero, one, etc.
typedef x;
Even "better", C declarators can be empty in order to allow for struct/enum declarations that do not list any variables. So you can leave out the variable, add qualifiers, etc: typedef;
const typedef;
typedef const;
It's a really fun syntax in some ways.I'm guessing that the lexical borrow problems that Rust are facing are an instance of the same problem. It will be interesting to read the follow up post and see what relation, if any, the suggested solution has to the graph approaches that I am familiar with.
PIC can be emitted on older x86 machines without RIP-relative addressing, but the code is larger and slower. As an example, consider -m32 gcc output for the C program
int x;
int getx() {
return x;
}
With no PIC: getx:
movl x, %eax
ret
With PIC: getx:
call __x86.get_pc_thunk.cx
addl $_GLOBAL_OFFSET_TABLE_, %ecx
movl x@GOT(%ecx), %eax
movl (%eax), %eax
ret
And with -m64, which emits PIC and uses x86-64s RIP-relative addressing: getx:
movl x(%rip), %eax
ret
Hopefully that makes the motivation clear.Edit: I was really referring to language families, so including things like OCaml, Modula, etc.
In addition to that, dedicated implementations can take various shortcuts to make their job easier - there are some nice examples given in the link. LuaJIT is example of a compiler project that benefits from being heavily specialised to a particular job, to remarkable effect.
I don't think this is right, unless given an unnecessarily strict definition of `well-known`. A contrived counterexample:
let example x y =
let f =
let z = big_calculation x in
(fun t -> if t then z else 0) in
(fun q -> f q + f y)
Here f is bound to a known function, which is only called and never escapes. All of the known-function optimisations should apply. However the variable z is not in scope at the call sites of f.Optimising this example effectively is a bit tricky - the 'standard' thing to do is construct a closure that contains z (but need not contain a code pointer, since all call sites are known), which is itself contained within the closure for (fun q -> ...). A more aggressive method would be to discover that all references to f are within (fun q -> ...) and inline the free variables of f into its closure, avoiding an indirection.
It's quite a fun subject - there are various implementation tricks for curried functions and partial application too, which aren't very important in Scheme but are crucial for ML.
Of course, running significantly different code in production and test has its own issues.
Another possibility that doesn't require such heavy language machinery is something like OCaml's private types, where the scope of unrestricted access is unsafe blocks rather than the body of a module. In this approach there is no type-level correspondence between the value of the length member and the extent of the array: private types only ensure that any potentially invariant-breaking mutation is restricted to a certain scope, not that the code is correct.
If you wished to implement a subset of Lisp in LC faithfully you would need an encoding for sums - which would not be all that hard, but does reflect the distance between Lisp values and the raw abstractions of the lambda calculus. (There is also the more substantial matter of RPLACD, etc, but let's just pretend that particular rabbit hole is not there.)