Compare commits

...

2 Commits

Author SHA1 Message Date
Scott Richmond
6ded94f7d0 oops: put working document under version control 2025-06-06 00:06:23 -04:00
Scott Richmond
0347d10db7 first draft of complex string matching, discover jump mistake 2025-06-05 23:26:42 -04:00
6 changed files with 411 additions and 38 deletions

View File

@ -17,3 +17,4 @@ ordered-float = "4.5.0"
index_vec = "0.1.4"
num-derive = "0.4.2"
num-traits = "0.2.19"
regex = "1.11.1"

272
may_2025_thoughts.md Normal file
View File

@ -0,0 +1,272 @@
# Catching back up
## May 2025
### Bugs
#### `match` is not popping things correctly
```
=== source code ===
let foo = match :foo with {
:foo -> 1
:bar -> 2
:baz -> 3
}
foo
=== chunk: test ===
IDX | CODE | INFO
0000: reset_match
0001: constant 0000: :foo
0003: match_constant 0000: :foo
0005: jump_if_no_match 0006
0007: constant 0001: 1
0009: store
0010: pop
0011: jump 0023
0013: match_constant 0002: :bar
0015: jump_if_no_match 0006
0017: constant 0003: 2
0019: store
0020: pop
0021: jump 0013
0023: match_constant 0004: :baz
0025: jump_if_no_match 0006
0027: constant 0005: 3
0029: store
0030: pop
0031: jump 0003
0033: panic_no_match
0034: load
0035: match_word
0036: panic_if_no_match
0037: push_binding 0000
0039: store
0040: pop
0041: pop
0042: load
=== vm run: test ===
0000: [] nil
0000: reset_match
0001: [] nil
0001: constant 0000: :foo
0003: [:10] nil
0003: match_constant 0000: :foo
0005: [:10] nil
0005: jump_if_no_match 0006
0007: [:10] nil
0007: constant 0001: 1
0009: [:10|1] nil
0009: store
0010: [:10|nil] 1
0010: pop
0011: [:10] 1
0011: jump 0023
0036: [:10] 1
0036: panic_if_no_match
0037: [:10] 1 <== Should "return" from match here
0037: push_binding 0000
0039: [:10|:10] 1
0039: store
0040: [:10|nil] :10
0040: pop
0041: [:10] :10
0041: pop
0042: [] :10
0042: load
:foo
```
Should return `1`.
Instruction `0037` is where it goes off the rails.
### Things left undone
Many. But things that were actively under development and in a state of unfinishedness:
1. Tuple patterns
2. Loops
3. Function calls
#### Tuple patterns
This is the blocking issue for loops, function calls, etc.
You need tuple pattern matching to get proper looping and function calls.
Here are some of the issues I'm having:
* Is it possible to represent tuples on the stack? Right now they're allocated on the heap, which isn't great for function calls.
* How to represent complex patterns? There are a few possibilities:
- Hard-coded into the bytecode (this is probably the thing to do?)
- Represented as a data structure, which itself would have to be allocated on the heap
- Some hybrid of the two:
* Easy scalar values are hard-coded: `nil`, `true`, `:foo`, `10` are all built into the bytecode + constants table
* Perhaps dict patterns will have to be data structures
#### Patterns, generally
On reflection, I think the easiest (perhaps not simplest!) way to go is to model the patterns as separate datatypes stored per-chunk in a vec.
The idea is that we push a value onto the stack, and then have a `match` instruction that takes an index into the pattern vec.
We don't even need to store all pattern types in that vec: constants (which already get stored in the constant vec), interpolations, and compound patterns.
`nil`, `false`, etc. are singletons and can be handled like (but not exactly as) the `nil` and `false` _values_.
This also means we can outsource the pattern matching mechanics to Rust, which means we don't have to fuss with "efficient compiling of pattern matching" titchiness.
This also has the benefit, while probably being fast _enough_, of reflecting the conceptual domain of Ludus, in which patterns and values are different DSLs within the language.
So: model it that way.
### Now that we've got tuple patterns
#### May 23, 2025
A few thoughts:
* Patterns aren't _things_, they're complex conditional forms. I had not really learned that; note that the "compiling pattern matching efficiently" paper is about how to optimize those conditionals. The tuple pattern compilation more closely resembles an `if` or `when` form.
* Tuple patterns break the nice stack-based semantics of binding. So do other patterns. That means I had to separate out bindings and the stack. I did this by introducing a representation of the stack into the compiler (it's just a stack-depth counter).
- This ended up being rather titchy. I think there's a lot of room to simplify things by avoiding manipulating this counter directly. My sense is that I should probably move a lot of the `emit_op` calls into methods that ensure all the bookkeeping happens automatically.
* `when` is much closer to `if` than `match`; remember that!
* Function calls should be different from tuple pattern matching. Tuples are currently (and maybe forever?) allocated on the heap. Function calls should *not* have to pass through the heap. The good news: `Arguments` is already a different AST node type than `Tuple`; we'll want an `ArgumentsPattern` pattern node type that's different from (and thus compiled differently than) `TuplePattern`. They'll be similar--the matching logic is the same, after all--but the arguments will be on the stack already, and won't need to be unpacked in the same way.
- One difficulty will be matching against different arities? But actually, we should compile these arities as different functions.
- Given splats, can we actually compile functions into different arities? Consider the following:
```
fn foo {
(x) -> & arity 1
(y, z) -> & arity 2
(x, y, 2) -> & arity 3
(...z) -> & arity 0+
}
```
`fn(1, 2, 3)` and `fn(1, 2, 4)` would invoke different "arities" of the function, 3 and 0+, respectively. I suspect the simpler thing really is to just compile each function as a singleton, and just keep track of the number of arguments you're matching.
* Before we get to function calls, `loop`/`recur` make sense as the starting ground. I had started that before, and there's some code in those branches of the compiler. But I ran into tuple pattern matching. That's now done, although actually, the `loop`/`recur` situation probably needs a rewrite from the ground up.
* Just remember: we're not aiming for *fast*, we're aiming for *fast enough*. And I don't have a ton of time. So the thing to do is to be as little clever as possible.
* I suspect the dominoes will fall reasonably quickly from here through the following:
- [x] `list` and `dict` patterns
- [x] updating `repeat`
- [x] standing up `loop`/`recur`
- [x] standing up functions
- [x] more complex synthetic expressions
- [x] `do` expressions
* That will get me a lot of the way there. What's left after that which might be challenging?
- [x] string interpolation
- [x] splats
- [ ] splatterns
- [x] string patterns
- [x] partial application
- [ ] tail calls
- [ ] stack traces in panics
- [ ] actually good lexing, parsing, and validation errors. I got some of the way there in the fall, but everything needs to be "good enough."
* After that, we're in integration hell: taking this thing and putting it together for Computer Class 1. Other things that I want (e.g., `test` forms) are for later on.
* There's then a whole host of things I'll need to get done for CC2:
- some kind of actual parsing strategy (that's good enough for "Dissociated Press"/Markov chains)
- actors
- animation in the frontend
* In addition to a lot of this, I think I need some kind of testing solution. The Janet interpreter is pretty well-behaved.
### Now that we've got some additional opcodes and loop/recur working
#### 2025-05-27
The `loop` compilation is _almost_ the same as a function body. That said, the thing that's different is that we don't know the arity of the function that's called.
A few possibilities:
* Probably the best option: enforce a new requirement that splat patterns in function clauses *must* be longer than any explicit arity of the function. So, taking the above:
```
fn foo {
(x) -> & arity 1
(y, z) -> & arity 2
(x, y, 2) -> & arity 3
(...z) -> & arity 0+
}
```
This would give you a validation error that splats must be longer than any other arity.
Similarly, we could enforce this:
```
fn foo {
(x) -> & arity 1
(x, y) -> & arity 2
(x, ...) & arity n > 1; error! too short
(x, y, ...) & arity n > 2; ok!
}
```
The algorithm for compiling functions ends up being a little bit titchy, because we'll have to store multiple functions (i.e. chunks) with different arities.
Each arity gets a different chunk.
And the call function opcode comes with a second argument that specifies the number of arguments, 0 to 7.
(That means we only get a max of 7 arguments, unless I decide to give the call opcode two bytes, and make the call/return register much bigger.)
Todo, then:
* [x] reduce the call/return register to 7
* [x] implement single-arity compilation
* [x] implement single-arity calls, but with two bytes
* [x] compile multiple-arity functions
* [x] add closures
### Some thoughts while chattin w/ MNL
On `or` and `and`: these should be reserved words with special casing in the parser.
You can't pass them as functions, because that would change their semantics.
So they *look* like functions, but they don't *behave* like functions.
In Clojure, if you try `(def foo or)`, you get an error that you "can't take the value of a macro."
I'll need to change Ludus so that `or` and `and` expressions actually generate different AST nodes, and then compile them from there.
AND: done.
### Implementing functions & calls, finally
#### 2025-06-01
Just to explain where I am to myself:
* I have a rough draft (probably not yet fully functional) of function compilation in the compiler.
* Now I have to implement two op codes: `Call` and `Return`.
* I now need to implement call frames.
* A frame in _Crafting Interpreters_ has: a pointer to a Lox function object, an ip, and an index into the value stack that indicates the stack bottom for this function call. Taking each of these in turn:
* The lifetime for the pointer to the function:
- The pointer to the function object cannot, I think, be an explicit lifetime reference, since I don't think I know how to prove to the Rust borrow checker that the function will live long enough, especially since it's inside an `Rc`.
- That suggests that I actually need the whole `Value::Fn` struct and not just the inner `LFn` struct, so I can borrow it.
* The ip and stack bottom are just placeholders and don't change.
### Partially applied functions
#### 2025-06-05
Partially applied functions are a little complicated, because they involve both the compiler and the VM.
Maybe.
My sense is that, perhaps, the way to do them is actually to create a different value branch.
They ....
### Jumping! Numbers! And giving up the grind
#### 2025-06-05
Ok! So.
This won't be ready for next week.
That's clear enough now--even though I've made swell progress!
One thing I just discovered, which, well, it feels silly I haven't found this before.
Jump instructions, all of them, need 16 bits, not 8.
That means some fancy bit shifting, and likely some refactoring of the compiler & vm to make them easier to work with.
For reference, here's the algorithm for munging u8s and u16s:
```rust
let a: u16 = 14261;
let b_high: u8 = (a >> 8) as u8;
let b_low: u8 = a as u8;
let c: u16 = ((b_high as u16) << 8) + b_low as u16;
println!("{a} // {b_high}/{b_low} // {c}");
```
To reiterate the punch list that *I would have needed for Computer Class 1*:
* [ ] jump instructions need 16 bits of operand
* [ ] splatterns
- [ ] validator should ensure splatterns are the longest patterns in a form
* [ ] add guards to loop forms
* [ ] check loop forms against function calls: do they still work the way we want them to?
* [ ] tail call elimination
* [ ] stack traces in panics
* [ ] actually good error messages
- [ ] parsing
- [ ] my memory is that validator messages are already good?
- [ ] panics, esp. no match panics
* [ ] getting to prelude
- [ ] `base` should load into Prelude
- [ ] prelude should run properly
- [ ] prelude should be loaded into every context
* [ ] packaging things up
- [ ] add a `to_json` method for values
- [ ] teach Rudus to speak our protocols (stdout and turtle graphics)
- [ ] there should be a Rust function that takes Ludus source and returns valid Ludus status json
- [ ] compile Rust to WASM
- [ ] wire rust-based WASM into JS
- [ ] FINALLY, test Rudus against Ludus test cases
So this is the work of the week of June 16, maybe?
Just trying to get a sense of what needs to happen for CC2:
* [ ] Actor model (objects, Spacewar!)
* [ ] Animation hooked into the web frontend (Spacewar!)
* [ ] Saving and loading data into Ludus (perceptrons, dissociated press)

View File

@ -5,7 +5,7 @@ use crate::value::*;
use chumsky::prelude::SimpleSpan;
use num_derive::{FromPrimitive, ToPrimitive};
use num_traits::FromPrimitive;
use std::borrow::Borrow;
use regex::Regex;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
@ -35,6 +35,8 @@ pub enum Op {
MatchFalse,
PanicIfNoMatch,
MatchConstant,
MatchString,
PushStringMatches,
MatchType,
MatchTuple,
PushTuple,
@ -153,6 +155,8 @@ impl std::fmt::Display for Op {
ResetMatch => "reset_match",
PanicIfNoMatch => "panic_if_no_match",
MatchConstant => "match_constant",
MatchString => "match_string",
PushStringMatches => "push_string_matches",
MatchType => "match_type",
MatchTuple => "match_tuple",
PushTuple => "push_tuple",
@ -223,12 +227,18 @@ pub struct Upvalue {
stack_pos: usize,
}
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug)]
pub struct StrPattern {
pub words: Vec<String>,
pub re: Regex,
}
#[derive(Clone, Debug)]
pub struct Chunk {
pub constants: Vec<Value>,
pub bytecode: Vec<u8>,
pub strings: Vec<&'static str>,
pub keywords: Vec<&'static str>,
pub string_patterns: Vec<StrPattern>,
}
impl Chunk {
@ -253,7 +263,7 @@ impl Chunk {
PushBinding | MatchTuple | MatchList | MatchDict | LoadDictValue | PushTuple
| PushBox | Jump | JumpIfFalse | JumpIfTrue | JumpIfNoMatch | JumpIfMatch
| JumpBack | JumpIfZero | MatchDepth | PopN | StoreAt | Call | SetUpvalue
| GetUpvalue | Partial => {
| GetUpvalue | Partial | MatchString | PushStringMatches => {
let next = self.bytecode[*i + 1];
println!("{i:04}: {:16} {next:03}", op.to_string());
*i += 1;
@ -311,7 +321,7 @@ fn get_builtin(name: &str, arity: usize) -> Option<Op> {
}
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone)]
pub struct Compiler<'a> {
pub chunk: Chunk,
pub bindings: Vec<Binding>,
@ -353,10 +363,8 @@ impl<'a> Compiler<'a> {
let chunk = Chunk {
constants: vec![],
bytecode: vec![],
strings: vec![],
keywords: vec![
"nil", "bool", "number", "keyword", "string", "tuple", "list", "dict", "box", "fn",
],
keywords: vec![],
string_patterns: vec![],
};
Compiler {
chunk,
@ -511,7 +519,6 @@ impl<'a> Compiler<'a> {
// }
fn pop(&mut self) {
println!("Popping from: {}", self.ast);
self.emit_op(Op::Pop);
self.stack_depth -= 1;
}
@ -694,14 +701,14 @@ impl<'a> Compiler<'a> {
let jump_idx = self.len();
self.emit_byte(0xff);
for idx in jump_idxes {
self.chunk.bytecode[idx] = self.len() as u8 - idx as u8 - 1;
self.chunk.bytecode[idx] = (self.len() - idx) as u8 - 1;
}
for _ in 0..members.len() {
self.emit_op(Op::Pop);
}
self.chunk.bytecode[before_load_tup_idx] =
self.len() as u8 - before_load_tup_idx as u8 - 1;
self.chunk.bytecode[jump_idx] = self.len() as u8 - jump_idx as u8 - 1;
(self.len() - before_load_tup_idx) as u8 - 1;
self.chunk.bytecode[jump_idx] = (self.len() - jump_idx) as u8 - 1;
}
ListPattern(members) => {
self.emit_op(Op::MatchList);
@ -726,14 +733,14 @@ impl<'a> Compiler<'a> {
let jump_idx = self.len();
self.emit_byte(0xff);
for idx in jump_idxes {
self.chunk.bytecode[idx] = self.len() as u8 - idx as u8 - 1;
self.chunk.bytecode[idx] = (self.len() - idx) as u8 - 1;
}
for _ in 0..members.len() {
self.emit_op(Op::Pop);
}
self.chunk.bytecode[before_load_list_idx] =
self.len() as u8 - before_load_list_idx as u8 - 1;
self.chunk.bytecode[jump_idx] = self.len() as u8 - jump_idx as u8 - 1;
(self.len() - before_load_list_idx) as u8 - 1;
self.chunk.bytecode[jump_idx] = (self.len() - jump_idx) as u8 - 1;
}
DictPattern(pairs) => {
self.emit_op(Op::MatchDict);
@ -759,14 +766,66 @@ impl<'a> Compiler<'a> {
let jump_idx = self.len();
self.emit_byte(0xff);
for idx in jump_idxes {
self.chunk.bytecode[idx] = self.len() as u8 - idx as u8 - 1;
self.chunk.bytecode[idx] = (self.len() - idx) as u8 - 1;
}
for _ in 0..pairs.len() {
self.emit_op(Op::Pop);
}
self.chunk.bytecode[before_load_dict_idx] =
self.len() as u8 - before_load_dict_idx as u8 - 1;
self.chunk.bytecode[jump_idx] = self.len() as u8 - jump_idx as u8 - 1;
(self.len() - before_load_dict_idx) as u8 - 1;
self.chunk.bytecode[jump_idx] = (self.len() - jump_idx) as u8 - 1;
}
Splattern(..) => {
todo!()
}
InterpolatedPattern(parts, _) => {
println!("An interpolated pattern of {} parts", parts.len());
let mut pattern = "".to_string();
let mut words = vec![];
for (part, _) in parts {
match part {
StringPart::Word(word) => {
println!("wordpart: {word}");
words.push(word.clone());
pattern.push_str("(.*)");
}
StringPart::Data(data) => {
println!("datapart: {data}");
let data = regex::escape(data);
pattern.push_str(data.as_str());
}
StringPart::Inline(..) => unreachable!(),
}
}
let re = Regex::new(pattern.as_str()).unwrap();
let moar_words = words.clone();
let string_pattern = StrPattern { words, re };
let pattern_idx = self.chunk.string_patterns.len();
self.chunk.string_patterns.push(string_pattern);
self.emit_op(Op::MatchString);
self.emit_byte(pattern_idx);
self.emit_op(Op::JumpIfNoMatch);
let jnm_idx = self.len();
self.emit_byte(0xff);
self.emit_op(Op::PushStringMatches);
self.emit_byte(pattern_idx);
for word in moar_words {
let name: &'static str = std::string::String::leak(word);
let binding = Binding {
name,
depth: self.scope_depth,
stack_pos: self.stack_depth,
};
self.bindings.push(binding);
self.stack_depth += 1;
}
self.chunk.bytecode[jnm_idx] = (self.len() - jnm_idx - 1) as u8;
}
PairPattern(_, _) => unreachable!(),
Tuple(members) => {
@ -938,11 +997,11 @@ impl<'a> Compiler<'a> {
self.emit_op(Op::Jump);
jump_idxes.push(self.len());
self.emit_byte(0xff);
self.chunk.bytecode[jif_jump_idx] = self.len() as u8 - jif_jump_idx as u8 - 1;
self.chunk.bytecode[jif_jump_idx] = (self.len() - jif_jump_idx) as u8 - 1;
}
self.emit_op(Op::PanicNoWhen);
for idx in jump_idxes {
self.chunk.bytecode[idx] = self.len() as u8 - idx as u8 - 1;
self.chunk.bytecode[idx] = (self.len() - idx) as u8 - 1;
}
self.stack_depth += 1;
}
@ -986,12 +1045,12 @@ impl<'a> Compiler<'a> {
jump_idxes.push(self.len());
self.emit_byte(0xff);
for idx in no_match_jumps {
self.chunk.bytecode[idx] = self.len() as u8 - idx as u8 - 1;
self.chunk.bytecode[idx] = (self.len() - idx) as u8 - 1;
}
}
self.emit_op(Op::PanicNoMatch);
for idx in jump_idxes {
self.chunk.bytecode[idx] = self.len() as u8 - idx as u8 - 1;
self.chunk.bytecode[idx] = (self.len() - idx) as u8 - 1;
}
while self.stack_depth > stack_depth {
self.pop();
@ -1214,12 +1273,12 @@ impl<'a> Compiler<'a> {
let jump_idx = self.len();
self.emit_byte(0xff);
for idx in tup_jump_idxes {
self.chunk.bytecode[idx] = self.len() as u8 - idx as u8 - 2;
self.chunk.bytecode[idx] = (self.len() - idx) as u8 - 2;
}
for _ in 0..arity {
self.emit_op(Op::Pop);
}
self.chunk.bytecode[jump_idx] = self.len() as u8 - jump_idx as u8 - 1;
self.chunk.bytecode[jump_idx] = (self.len() - jump_idx) as u8 - 1;
self.emit_op(Op::JumpIfNoMatch);
let jnm_idx = self.len();
self.emit_byte(0xff);
@ -1240,12 +1299,12 @@ impl<'a> Compiler<'a> {
self.emit_op(Op::Jump);
jump_idxes.push(self.len());
self.emit_byte(0xff);
self.chunk.bytecode[jnm_idx] = self.len() as u8 - jnm_idx as u8;
self.chunk.bytecode[jnm_idx] = (self.len() - jnm_idx) as u8;
self.scope_depth -= 1;
}
self.emit_op(Op::PanicNoMatch);
for idx in jump_idxes {
self.chunk.bytecode[idx] = self.len() as u8 - idx as u8 - 1;
self.chunk.bytecode[idx] = (self.len() - idx) as u8 - 1;
}
self.emit_op(Op::PopN);
self.emit_byte(arity);
@ -1305,10 +1364,7 @@ impl<'a> Compiler<'a> {
Placeholder => {
self.emit_op(Op::Nothing);
}
Arguments(..) | Placeholder | InterpolatedPattern(..) | Splattern(..) => {
todo!()
}
And | Or => unreachable!(),
And | Or | Arguments(..) => unreachable!(),
}
}

View File

@ -74,12 +74,19 @@ pub fn run(src: &'static str) {
}
pub fn main() {
env::set_var("RUST_BACKTRACE", "1");
// env::set_var("RUST_BACKTRACE", "1");
let src = "
fn add2 (x, y) -> add (x, y)
let x = {
match #{:a 1, :b 2, :c 3} with {
#{a} -> :one
#{a, b, :c 3} -> :two
#{a, b, c} -> :three
(1, 2, 3) -> :thing
(4, 5, (6, 7, a)) -> if or (true, false, false, true) then :thing_1 else :thing_2
([:a, :b, :c, [:d, [:e, (:f, :g)]]]) -> if or (true, false, false, true) then :thing_1 else :thing_2
}
}
add2 (_, 2) (2)
";
";
run(src);
}

View File

@ -6,7 +6,7 @@ use imbl::{HashMap, Vector};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug)]
pub enum LFn {
Declared {
name: &'static str,
@ -107,7 +107,7 @@ impl PartialEq for Value {
(List(x), List(y)) => x == y,
(Dict(x), Dict(y)) => x == y,
(Box(x), Box(y)) => std::ptr::eq(x.as_ref().as_ptr(), y.as_ref().as_ptr()),
(Fn(x), Fn(y)) => x == y,
(Fn(x), Fn(y)) => std::ptr::eq(x, y),
(BaseFn(x), BaseFn(y)) => std::ptr::eq(x, y),
(Partial(x), Partial(y)) => x == y,
_ => false,

View File

@ -342,6 +342,43 @@ impl Vm {
self.matches = self.stack[idx] == self.chunk().constants[const_idx as usize];
self.ip += 2;
}
MatchString => {
let pattern_idx = self.chunk().bytecode[self.ip + 1];
self.ip += 2;
let scrutinee_idx = self.stack.len() - self.match_depth as usize - 1;
let scrutinee = self.stack[scrutinee_idx].clone();
self.matches = match scrutinee {
Value::String(str) => self.chunk().string_patterns[pattern_idx as usize]
.re
.is_match(str.as_str()),
Value::Interned(str) => self.chunk().string_patterns[pattern_idx as usize]
.re
.is_match(str),
_ => false,
};
}
PushStringMatches => {
let pattern_idx = self.chunk().bytecode[self.ip + 1];
self.ip += 2;
let pattern_len = self.chunk().string_patterns[pattern_idx as usize]
.words
.len();
let scrutinee_idx = self.stack.len() - self.match_depth as usize - 1;
let scrutinee = self.stack[scrutinee_idx].clone();
let scrutinee = match scrutinee {
Value::String(str) => str.as_ref().clone(),
Value::Interned(str) => str.to_string(),
_ => unreachable!(),
};
let captures = self.chunk().string_patterns[pattern_idx as usize]
.re
.captures(scrutinee.as_str())
.unwrap();
for cap in 0..pattern_len {
self.push(Value::String(Rc::new(captures[cap + 1].to_string())))
}
self.match_depth += pattern_len as u8;
}
MatchTuple => {
let idx = self.stack.len() - self.match_depth as usize - 1;
let tuple_len = self.chunk().bytecode[self.ip + 1];