Why ML/OCaml are good for writing compilers
flint.cs.yale.edu
flint.cs.yale.edu
type tree = Empty
| Leaf of int
| Node of tree * treehttp://manticore-wiki.cs.uchicago.edu/index.php/Image:Mantic...
For example, in the AST representation, you can still have anything on the right hand side of an "=". Once you get to BOM, it has been normalized, so every subexpression has a unique variable attached to it. So, "val x = 1+2y" is now "val x'=2y" and "val x=1+x'". This transformation makes identifying common subexpressions trivial.
In a compiler written in C, because creating a new set of IR types involves either copying header files or horrible template magic that terribly ties all portions of the compiler together (I've seen people try!), most people end up just keeping one IR and doing passes over it. Some variables are valid at some stages; some are not.
A good example is the very nice V8 javascript compiler. I was playing around with it a few months ago, and just understanding what invariants were valid after which phase was challenging. And that doesn't even cover dealing with an endless series of conflicting changes because every time somebody changed anything it involved changes to core structures.
I'm a c++ person but I'm curious how folks feel about this statement from the article.
Does that mean there's static safeguards against out-of-memory errors? (This is not snarky; I don't know ML or how it handles this situation.)
Well, unless we're talking about smartphones or virtualized environments (e.g. VPS). Both not very relevant in a compiler context, of course.
Never mind that you can run out of swap, too…
However, I think it's a bit extreme to say a Lisp programmer could never be sure that their program was going to work...that's just disingenuous. Lisp is a dynamically typed language, but it is a safe language.
To contrast this with C++. In C++, if I call some third party mystery function, I'm not sure if it'll cause some side effect and corrupt my process memory. I can arbitrarily create a pointer, cast things to whatever I want, do some memcpys and memsets and bring the whole thing down. Not to say they're bad languages, I'm a C++ guy myself...but a lot of the security vulnerabilities in operating systems and major libraries/apps is due to the fact they're written in C or C++. Languages like ML and Lisp prevent that.
It's an interesting puzzle, I'm not sure if it's because ML users and implementers are primarily interested in some other problem and they implement ML as a tool to work on it or if the wars about extending semantics are rooted somewhere else.
On the upside, it seems like a new implementation crops up every few years. Caml and ocaml are definitely the most popular and the largest communities though.
SML/NJ is still supported but there is little active development going on. We had a small infrastructure grant a few years back and cleaned up a bunch of cobwebs that had grown in the runtime, particularly around Windows support, and still support its active use in a few classes. But, it is fairly mature and stable. The only pieces of work I am aware of are some cleanup work related to the integration of the FLINT code generation backend with the frontend and some work we've been doing identifying places where the Definition was vague or the language needs to be extended.
MLton is also actively supported, and at this point it is stable and provides extremely high performance for sequential programs (competitive with hand-coded C, though all bets are off if you start cheating and using compiler intrinsics). The primary maintainer has a large list of potential additional projects, but I steal a signifiant portion of his time picking his brain, as he is a co-PI on Manticore :-)
I don't know anything about AliceML.
Also (unfortunately, IMHO), this talks a lot about speed, which was a big problem in the Pentium 200 days with minimal RAM, cache, etc. Nowadays, any old poorly written, garbage-collected program runs speedy as hell. Seems like lost is the golden days of optimization.
Of course if you are a believer in higher-order abstract syntax (the idea that one should use constructs such as lambda abstractions as part of your syntax tree), Scheme is a better fit, as ML's type system doesn't allow for such wildly typed syntax trees, nor does it allow data-as-code. (I think HOAS is hogwash, but then I'm a firm believer in well-typed code and against data-as-code.)
Of course, if your AST is more a graph than a tree, you're better with a logic language such as Mercury or Prolog. I've had very good experiences writing compilers and interpreters in Mercury.
:- pred asg_nodes(asg, node).
:- mode asg_nodes(in, out) is nondet.
:- mode asg_nodes(in, in) is semidet.
:- pred asg_edges(asg, node, node).
:- mode asg_edges(in, out, out) is nondet.
:- mode asg_edges(in, in, in) is semidet.
Underlying are traditional sets or what-have-you but you never have to deal with these at high levels of coding. You could then do fancy stuff like defining ancestry as:
:- pred asg_ancestor(asg, node, node).
:- mode asg_ancestor(in, out, out) is nondet.
:- mode asg_ancestor(in, in, in) is semidet.
asg_ancestor(ASG, A, A).
asg_ancestor(ASG, A, B) :- asg_edge(A, C), asg_ancestor(C, B).
Of course you'd want to wrap this stuff up in a typeclass or some such and give them useful names.
But what of actually parsing a program in HOAS? The idea of HOAS is to translate the text "\x -> x + 5" into the expression Lam (\x -> Op("+", Var x, Const 5)). But how does one translate the string "x" into the variable name x short of data-as-code (or a similar meta-facility)?
The best Google turns up for this problem is here: http://books.google.com/books?id=OxeBw-sH4SUC&lpg=PA50... which seems to address the problem using a meta-facility of lambda-Prolog.
import scala.util.parsing.combinator.syntactical.StandardTokenParsers
import scala.util.parsing.input._
import scala.util.parsing.syntax._
object HoasTest extends StandardTokenParsers {
lexical.delimiters ++= List("(", ")", "\\", ".", ":", "=", "->", "+", "{", "}", ",", "*")
lexical.reserved ++= List("Bool", "Nat", "true", "false", "if", "then", "else", "succ",
"pred", "iszero", "let", "in", "fst", "snd")
import lexical.NumericLit
import lexical.Identifier
def Term(base: Parser[Term]): Parser[Term] = positioned(
SimpleTerm(base) ~ SimpleTerm(base) ^^ { case fun~arg => Apply(fun,arg) } // should be left-assoc
| SimpleTerm(base) ~ ("+" ~> SimpleTerm(base)) ^^ { case a~b => Plus(a,b) } // should be left-assoc
| SimpleTerm(base)
| failure("illegal start of term"))
def SimpleTerm(base: Parser[Term]): Parser[Term] = positioned(
(("\\" ~> ident <~ ".") into (x => Parser { in =>
def body(arg: Term): ParseResult[Term] = Term(Identifier(x) ^^^ arg | base)(in)
body(new Term {}) match {
case Success(_,rest) => Success(Lambda(x, (arg) => body(arg).get), rest)
case f => f
}
}))
| "(" ~> Term(base) <~ ")"
| base
| failure("illegal start of simple term"))
def BaseTerm: Parser[Term] = positioned(
numericLit ^^ { case n => Num(n.toInt) }
| failure("unrecognized base term"))
abstract class Term extends Positional
case class Num(n: Int) extends Term
case class Plus(a: Term, b: Term) extends Term
case class Lambda(x: String, body: Term=>Term) extends Term {
override def toString = "\\"+x+".("+body(new Term { override def toString = x }).toString+")"
}
case class Apply(fun: Term, arg: Term) extends Term
}-Where it fails though still...and this is a big deal in this day and age, kernel level thread support. User level threads can solve a lot of problems, but when you're dealing with heavy number crunching and the algorithms of the future (computer vision, AI, highly parallel search, etc)...you need to use all the cores! Intel is talking about having thousands of cores on a single die by the end of the decade. We have to run 1000 OCaml processes and do process level messaging passing?? A lot of people are now looking to Microsoft's F# (virtually the same as OCaml sans OOP) as it targets .NET and supports true parallelism and a thread safe garbage collector.
-One thing about the original post which mentioned the lack of OOP. Perhaps not in SML, but OOP is well supported in OCaml (hence the O). People haven't given it a chance. It's really nifty...the biggest complaint I have is that type information from the compiler is hard to read due to the notation used for object types. Also, all of the standard libraries and most of the community only use the functional subset. There's good reason too, functional programming is very flexible.
-There are a lot of libraries out there for OCaml created by the community. However they vary in level of documentation. For the most part though, I've found 3rd party OCaml libraries to be of high quality due to the elegance of OCaml. Also, it's pretty easy to take a C or C++ library and write an OCaml binding for it. This is true of most languages, but it's annoying when the thing is already written for C++/Java/Python/etc
-Oh one really really cool feature of OCaml that nobody ever talks about -- you can actually read the code for the standard library! Have you ever looked at files like "iostream" for C++ or "stdio.h" in C. There are macros and templates and all sort of ugly craziness that nobody can read. I was able to open the standard library in OCaml and actually read it. I could see how they implemented standard modules like List, Thread, and Array. What's interesting is that most of the code would be considered inefficient by imperative programmers due to heavy use of recursion. However simple tail call optimizations by the compiler save the day!