rudus/bytecode_thoughts.md
Scott Richmond 48754f92a4 do work
2024-12-17 23:45:39 -05:00

4.5 KiB

Working notes on bytecode stuff

2024-12-15

So far, I've done the easy stuff: constants, and ifs.

There's still some easy stuff left:

  • lists
  • dicts
  • when
  • panic

So I'll do those next.

But then we've got two doozies: patterns and bindings, and tuples.

Tuples make things hard

In fact, it's tuples that make things hard. The idea is that, when possible, tuples should be stored on the stack. That makes them a different creature than anything else. But the goal is to be able, in a function call, to just push a tuple onto the stack, and then match against it. Because a tuple isn't just another Value, that makes things challenging. BUT: matching against all other Values should be straightforward enough?

I think that the way to do this is to reify patterns. Rather than try to emit bytecodes to embody patterns, the patterns are some kind of data that get compiled and pushed onto a stack like keywords and interned strings and whatnot. And then you can push a pattern onto the stack right behind a value, and then have a match opcode that pops them off.

Things get a bit gnarly since patterns can be nested. I'll start with the basic cases and run from there.

But when things get very gnarly is considering tuples on the stack. How do you pop off a tuple?

Two thoughts:

  1. Just put tuples on the heap. And treat function arguments/matching differently.
  2. Have a "register" that stages values to be pattern matched.
Regarding the first option

I recall seeing somebody somewhere make a comment that trying to represent function arguments as tuples caused tons of pain. I can see why that would be the case, from an implementation standpoint. We should have values, and don't do fancy bookkeeping if we don't have to.

Conceptually, it makes a great deal of sense to think of tuples as being deeply the same as function invocation. But practically, they are different things, especially with Rust underneath.

This feels like this cuts along the grain, and so this is what I will try.

I suspect that I'll end up specializing a lot around function arguments and calling, but that feels more tractable than the bookkeeping around stack-based tuples.

2024-12-17

Next thoughts: take some things systematically rather than choosing an approach first.

Things that always match

  • Placeholder.

    • I think this is just a no-op. A let expression leaves its rhs pushed on the stack.
  • Word: put something on the stack, and bind a name.

    • This should follow the logic of locals as articulated in Crafting Interpreters.

In both of these cases, there's no conditional logic, simply a bind.

Things that never bind

  • Atomic values: put the rhs on the stack, then do an equality check, and panic if it fails. Leave the thing on the stack.

Analysis

In terms of bytecode, I think one thing to do, in the simple case, is to do the following:

  • push a pattern onto the stack
  • match--pops the pattern and the value off the stack, and then applies the pattern to the value. It leaves the value on the stack, and pushes a special value onto the stack representing a match, or not.
    • We'll probably want match-1, match-2, match-3, etc., opcodes for matching a value that's that far back in the stack. E.g., match-1 matches against not the top element, but the top - 1 element.
    • This is specifically for matching function arguments and loop forms.
  • There are a few different things we might do from here:
    • panic_if_no_match: panic if the last thing is a no_match, or just keep going if not.
    • jump_if_no_match: in a match form or a function, we'll want to move to the next clause if there's no match, so jump to the next clause's pattern push code.
  • Compound patterns are going to be more complex.
    • I think, for example, what you're going to need to do is to get opcodes that work on our data structures, so, for example, when you have a match_compound opcode and you start digging into the pattern.
  • Compound patterns are specifically data structures. So simple structures should be stack-allocated, and and complex structures should be pointers to something on the heap. Maybe?

A little note

For instructions that need more than 256 possibilities, we'll need to mush two u8s together into a u16. The one liner for this is:

let number = ((first as u16) << 8) | second as u16;

Oy, stacks and expressions

One thing that's giving me grief is when to pop and when to note on the value stack.

Consider