lucaswerkmeister / graaleneyj

experimental Graal implementation of eneyj (part of AbstractText)
GNU General Public License v2.0
8 stars 1 forks source link

Decide strategy for evaluating references #2

Closed lucaswerkmeister closed 3 years ago

lucaswerkmeister commented 4 years ago

GraalEneyj needs a strategy for evaluating references: at what point is e. g. the reference Z28 turned into the actual project_name object?

Broadly speaking, I see two ways forward:

  1. References are runtime objects. Evaluating a reference AST node merely returns that runtime object; resolving the actual reference happens at a later stage, at the discretion of whoever is holding such a reference instance (e. g. received as a function argument).
  2. References are evaluated as soon as a reference AST node is evaluated and do not exist at runtime. If an object of type Z9 is created (e. g. by the Z38 “abstract” builtin, which turns a list of (key, reified value) pairs into an object), it is evaluated immediately.

The problem with the second approach is that eneyj contains a lot of circular references, and a naive implementation of that approach runs into a problem like this:

Introducing references as runtime objects, as I did in 73eeb26c1adcd89872506b893a07c97594f83b0a, is one way to break that infinite loop: you never actually evaluate Z6, you just have a reference with that ID. But this also has its downsides: I’m already beginning to see how it gets annoying that wherever you have a value, you need to deal with the possibility that it’s actually a reference which you need to evaluate first (e. g. in ZFunctionCallNode and ZValueBuiltin). So I’d like to look into the alternative as well.

Another way to break the cycle that I can think of is to register objects early, before they’re complete. The naive approach looked something like this:

But we could also try something like this:

With some Truffle tricks, it should be possible to optimize this just as well as if objects were truly immutable from the start, even though at the beginning they’re actually mutable.

vrandezo commented 4 years ago

Yes, you identified the core problem. That's called lazy vs eager evaluation, and then there are steps in between. Both should return exactly the same results, but run into different corner-cases where one of them doesn't return at all because it runs out of time or memory.

In eneyj I went for a lazy approach if the implementation used a Z7 function call, for an eager approach if the implementation was a Z16 code, and customised for builtins. Particularly the Z31 if can easily run into endless loops if we first evaluate the consequent and the alternative before evaluating the condition and then only choosing one.

I was starting to implement Z100 evaluate to allow any implementation to choose its evaluation strategy, but I think I overshot it and that's now too complicated. This should be simplified.

Here's the reasoning for the evaluation strategy that eneyj chooses:

I am really not sure eneyj got it right, and I expect over time to see people rewriting that core evaluator again and again. So my suggestion would be to try whatever is easiest for you now, and then see which of the tests work and which don't, and then see if adjustments are needed.

lucaswerkmeister commented 3 years ago

I think we can close this. Approach one seems to be working reasonably well for now: references are runtime objects and usually passed around as-is, and only a few builtins resolve them on-the-fly. (Maybe we should resolve references before calling code implementations, but that shouldn’t be a big issue.)