Hacker News new | past | comments | ask | show | jobs | submit login
Crafting Interpreters: Classes (craftinginterpreters.com)
132 points by benhoyt on Oct 30, 2017 | hide | past | favorite | 32 comments



As always, I'm happy to answer any questions or talk about this. :)


Slightly off-topic, but (why) did you give up on Magpie? I followed its development with considerable interest, but then it seemed to drop off. I was actually just reading your old Magpie blog posts today.

In particular, I'm curious why you dropped multimethods in favor of classes in Wren.

EDIT: I see you've already answered this question (http://wren.io/qa.html#what-about-your-other-languages)


Oh, man, that's a good question. I still really really like multimethods, but I ran out of steam on Magpie for a couple reasons:

1. Julia came out. It looks to me like a really nice language that is heavily based on multimethods, and is orders of magnitude more mature than Magpie. In many ways Julia fills the niche I was aiming for with Magpie and probably better than I could have.

2. I hit a dead end in the VM implementation. This is an amateur mistake on my part, and obvious in retrospect, but it turns out writing a VM in C++ with virtual methods and also using a moving GC is a Bad Thing. It means in the middle of a method, "this" could disappear out from under you. Once I realized that, I realized I'd need to significantly rework the entire VM to fix it. Oops.

3. I'm not a sophisticated enough VM hacker to implement multimethods efficiently. I might get there eventually, but getting Magpie to a state where it's fast enough to be usable for real programs is very likely beyond my skills, especially with my limited free time.

Wren was designed deliberately to be a language that I personally have the ability to design, implement, and ship. There's definitely something to be said for being more ambitious and trying to push one's own envelope, but I wanted Wren to be a think that I could make and get to something usable with a realistic amount of effort.


Have you looked closely at Julia, or are you just kind of aware of its existence? I've been learning it recently, and I'm actually quite impressed with its suitability as a general purpose programming language because of how much of the marketing is oriented toward scientific uses. I'm going through Crafting Interpreters and translating it into Julia (mostly so I'm sure I understand what I'm reading) and it's been somewhat interesting to not have to deal with the expression problem.


I'm slightly more than just aware of its existence — I've read a bunch of docs and seen some talks. But I haven't sat down and written any code myself yet.

> it's been somewhat interesting to not have to deal with the expression problem.

Yeah, I think it would be really fun to hack on a compiler in a language with multimethods.


Looking forward to this.

There's very different approaches taken by production grade VM's today, ie the difference between JVM, MRI and CPython. Will there be any introspection into the theory vs implementations in production today.

I've found books like "Ruby under a Microscope" to be invaluable when driving into a completely new runtime. Was hoping this would be a good spot to collect many VM approaches.


If you have read or checked out http://www.buildyourownlisp.com/, how would you compare your book to buildyourownlisp?


Good question. I have read "Build Your Own Lisp", and quite enjoyed it. A few random differences:

1. BYOL is done and my book isn't (yet). :)

2. BYOL aims to both teach you how to program in C and how to implement a language at the same time. Mine assumes you are passably comfortable in Java and C and focuses on the language stuff.

3. Lisp is, of course, a very different language from the imperative OOP language my book uses. BYOL teaches you about s-exprs and some macro-related stuff. Mine covers a more complex grammar, assignment, classes, etc.

4. BYOL uses a provided parser combinator library for building the parser. Mine has you build a lexer and recursive descent parser from scratch and goes into greater detail (though not as much as in a PL textbook) about grammars and the theory behind it.

5. If I recall, BYOL avoids needing a GC by passing everything by value, including deep copying trees. That's an interesting way to sidestep the problem but is an uncommon choice for a language. In mine, we first rely on the Java GC to do our dirty work. In the C interpreter, we'll build a precise mark-sweep GC from scratch.

6. My book, for better or worse, is going to end up being quite a bit larger than BYOL. You can probably work through his over a long weekend, where mine is more of a holiday vacation kind of thing.


Do you also will do multi-methods? And how it could be another path for object-oriented programming? This is unexpected for me.

---

Other pet peeve of mine is how build control-flows. Using continuations is complicated (I'm using F#) and cause a drop in performance. I wonder what else could be use that allow to make exceptions/coroutines.

Or how make delimited continuations to work reasonable well. The code I have found is in scheme/lisp and it look hard to dechiper.


> Do you also will do multi-methods?

No, the book will only go into implementing single dispatch. I'd love to talk about implementing multimethods, but I don't know how to implement them very well either. :) I've read a few papers on it, but there doesn't seem to be a lot of literature. Maybe it's mostly locked up in tribal knowledge and you have to be in the right room with the right people to learn it.


> Maybe it's mostly locked up in tribal knowledge and you have to be in the right room with the right people to learn it.

This is sad.


Tell me about it. As someone who works in programming languages professionally but has no academic background in it, it's maddening how much stuff is impossible to learn because "you can't publish a paper on it because everyone knows it already".


Years ago I dreamed that Lambda the Ultimate had added a wiki, so language implementation was quickening. Waking up... :(


Similarly in the sciences, the gaps can be surprisingly large between insightful discussion among principals at conferences, and "you have to know the people involved to evaluate them" narrow papers, and misleading texts, and at great remove, introductory material.


I'm assuming that you've seen this then? http://people.csail.mit.edu/jrb/Projects/pd.pdf


https://github.com/googoogaga/goo continued for a bit, then didn't.

OT: With Google Chrome's new Turbofan, and its altered inlining budgeting, there now seems a chance of inlining dispatch trees. Combined with PICs, and cpu branch prediction and speculation... there seems hope for wizzy multiple dispatch in javascript. Maybe. I don't know of anyone pushing on it.


No, I haven't. Thanks for the reference!


You mention CLOS in your article; to dive deeper into implementation issues, see also https://en.wikipedia.org/wiki/The_Art_of_the_Metaobject_Prot....


Yup, I read AMOP several years ago. But as I recall, it doesn't talk too much about low-level implementation shenanigans.


What I liked about it is the discussion about design decisions: where and how to let the protocol be open to extensions, etc. (btw, sorry about the pedantic tone of the previous comment).


Also: Any idea why multi-methods are resolved on runtime? Is not possible for a static compiler?


One way to do delimited continuations might be to build on the state machine transformation behind generators and async/await. You can drop the `await`/`yield` primitives that C#/Javascript use and introduce a `shift` primitive instead. In non-recursive situations you can even make the continuation objects statically sized.

Kotlin goes this direction, for example, though it still requires an annotation on all functions that participate in continuations. This makes it fairly straightforward to tell where the `prompt` is- the root `suspend fun` passed to whatever API drives it. Making every function implicitly `suspend` would let you treat any function as a `prompt`.


Any example in how this is done? The info I have gathered is none what you have described!


The best examples would just be the implementations of yield/async/await in C#, Javascript, Kotlin, Rust, etc. The only novel thing I'm proposing here is a shift in the syntax from explicit suspension points and implicit initiation of async operations, to implict suspension points and explicit initiation of async operations. Kotlin (which I mentioned) already mostly does this.


As someone who comes to software engineering from a non-traditional background, I've found Crafting Interpreters particularly useful for helping to fill in a bunch of stuff that I missed out on by never doing a Compilers class. Thank you!


Not question but just a note of appreciation:

I've actually been hands deep in the bowels of a widespread production-use interpreter, so I'm hardly new to the topic. Yet as soon as I started reading your book yesterday on the way home, I haven't been able to stop reading. While I have a reasonable grasp about most things covered (so far), I find myself making additional connections and having implicit questions answered that I didn't know I had.

So thank you very, very much for this!


PS: There are a couple of extremely minor things I've come across that I'd consider correcting. Do you care for those? If so, github pull request? I'll give an example below.

IIRC you say that all non-trivial interpreters have abolished ref-counting and usually using some mix of tracing and ref-counting (o//r just tracing GC). Your example for an interpreter that was using ref-counting is Ruby.

The example of a non-trivial interpreter that continues to be fully ref-counted is perl.

[rant]Worse yet, perl goes fast and loose in one edge case: it eschews ref-count operations on values on argument stacks, which is a source of bugs that couldn't be more hilarious.

So yeah, perl doesn't detect cycles. It's really quite sad. It's also not fundamentally impossible to add in a tracing GC for detecting cycles. But it'd be a reasonably daunting task given the code base.

I've occasionally thought about whether it's possible (and if so at what cost) to move perl (5) away from ref-counting. Perl (the language) happens to give strong guarantees about when objects are destroyed / hooks are triggered, and that's commonly relied upon in RAII-like use cases.[/rant]


Hey, just wanted to say thanks for the work you're putting into this! It's been a great resource for me when I tinker with my own programming language projects.


Trying to get Donald Knuth's Man or Boy test to run, but it dies with "Stack overflow.".

Any ideas on how to make it run?

fun A (k, x1, x2, x3, x4, x5) { print k; fun B() { k = k - 1; return A(k, B, x1, x2, x3, x4); } if (k <= 0) { return x4() + x5(); } else { return B(); } }

fun F(i) { fun G() { return i; } return G; }

print A(10, F(1), F(-1), F(-1), F(1), F(0)); // expect: -67


Just wanted to say how much I enjoy getting each new chapter. You have an amazingly approachable writing style. Many thanks.


Thank you!


This looks fantastic. I look forward to reading, and implementing my next special snowflake language -- with a real VM this time!




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: