Skip to content

feat(vm): interpreter loop, VM-CLI wiring, native dispatch, console - #7

Merged
romancitodev merged 13 commits into
mainfrom
feat/vm-execution
Sep 18, 2026
Merged

romancitodev merged 13 commits into
mainfrom
feat/vm-execution

Conversation

@romancitodev

@romancitodev romancitodev commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Segundo slice de la VM: el loop de ejecución de verdad, arriba del scaffold del PR anterior, y todo lo que hace falta para que un .wlk corra de punta a punta desde el CLI.

Interpreter loop

Separé Program (lo compilado, fijo) de Vm (heap + caches, cambia en runtime) para no pelearme con el borrow checker al recursar en Send. Los selectores se internan a u32 antes de tiempo así Send compara enteros, no strings. El vtable de cada clase se resuelve una sola vez. Y subí la inline cache de monomórfica a polimórfica (4 casilleros) para que un call-site que alterna entre un par de clases no thrashee cada vez.

wollok-compiler

Conecta el AST (wollok-ast) con el bytecode de la VM. Clases y objects singleton con campos y métodos, constructores sintetizados por campo, new, sends (incluidos operadores), locals, if/return, literales. Sin arrays/sets/closures/try-catch/super todavía (ver docs/backlog.md).

Dispatch nativo + wollok-std

Send ya no asume que el receptor es un objeto de heap: los primitivos (Int/Float/Bool/Str) despachan contra una tabla nativa (NativeTable) que la crate nueva wollok-std llena por registro, no un switch en el medio del intérprete. Un objeto cuya clase no define el selector cae a un bucket de defaults (toString) antes de fallar.

console, native methods

console es Wollok fuente de verdad (compilado como cualquier object), con println declarado native (sin cuerpo) y resuelto por nombre de clase en runtime. Acepta cualquier cantidad de argumentos. De paso, native se sumó como palabra clave del lenguaje (mismo patrón que abstract).

Otros arreglos

Los strings dejaron de vivir en Program (fijo tras compilar) y pasaron a Vm (con interning real), porque un toString/concat necesita un lugar mutable donde poner el resultado. Los mensajes de error del compilador ahora incluyen el nodo real que falló, no un string genérico.

162 tests en el workspace, clippy limpio, fmt limpio (rustfmt adoptado en este PR también).

…ethods, frames)

Wires up the actual execution loop on top of the scaffolding from the
previous PR. New pieces, each with unit tests:

- selector.rs: interns (name, arity) into a MethodNameIdx so Send only
  ever compares a u32, never a string, in the hot path (Wollok
  overloads by arity: do() and do(a) are different methods)
- method.rs: MethodTable, compiled method bodies indexed by MethodRef
- class.rs: ClassTable, a flat selector->method vtable per class,
  resolved once at class-definition time (never a hierarchy walk)
- frame.rs: one method activation's locals + operand stack + ip
- program.rs: splits out everything fixed once compiled (methods,
  classes, selectors, consts) from Vm (heap + inline caches, which DO
  change while running) — this is what lets run_method hold a method
  borrowed from `program` across recursive Send calls without fighting
  the borrow checker over &mut self on one do-everything struct
- vm.rs: the actual instruction loop. Implements PushConst/Null/True/
  False, Load/StoreLocal, Load/StoreField, Send (through the inline
  cache), Jump/JumpIfFalse, Return, NewInstance (fields start Null —
  constructors aren't wired on the parser side yet either). Everything
  else (NewArray/NewSet/NewClosure, try/catch, SendSuper) panics with
  todo!() naming itself rather than silently doing nothing.

No compiler and no end-to-end test yet — next up.
…lots)

A single-entry cache evicts on every call at a call-site that
alternates between a couple of receiver classes, which is common
enough in real code that it's worth the extra few bytes. Bumps
InlineCache to a small fixed array (Hölzle/Chambers/Ungar, ECOOP '91 —
real call-site polymorphism essentially never exceeds a handful of
classes, so a linear scan over 4 slots beats a hashmap here), evicting
round-robin only once all 4 are full. Same tiered design CPython 3.11's
specializing adaptive interpreter (PEP 659) and Brunthaler's "Inline
Caching Meets Quickening" (ECOOP '10) use, minus the opcode rewriting
part of quickening — Program is deliberately immutable once compiled,
so specialization happens in the cache data, not in the bytecode
itself.
Pop was a real gap the first end-to-end test surfaced: a message send
used as a statement (its result unwanted) had nowhere to put that
result down, so the operand stack would just accumulate garbage across
statements.

tests/interpreter.rs hand-assembles bytecode (no compiler yet) and
runs it through the real Vm, proving the whole chain works together,
not just each module in isolation:
- new object -> Send -> field read/write across two separate calls
- the inline cache slot actually gets populated after a Send
- the SAME call-site correctly dispatching to two different classes,
  round-tripping back to the first without losing it (the direct
  end-to-end proof that the polymorphic cache doesn't thrash)
- Jump/JumpIfFalse branching both ways
- sending an unimplemented selector panics with a clear message
Compiler (wollok-compiler) now runs through the VM end to end via the
CLI. Send dispatches to native Rust methods for Int/Float/Bool/Str
(never heap objects) through a registration table (native::NativeTable)
that wollok-std populates instead of wollok-vm hardcoding them, so the
standard library can grow on its own. Object receivers whose class
doesn't define a selector fall back to a small native default bucket
(PrimitiveKind::Object) for now just toString - not real inheritance,
still tracked separately in docs/backlog.md item 4. Vm::send lets
native code call back into user-defined methods dynamically. Program::
strings moved to Vm::strings (with real interning) since native
methods that produce text (toString, concat) need a mutable place to
put it at runtime.
A method can now be declared `native` (no body, like `abstract`),
resolved at runtime by wollok-vm's native method table instead of
compiled bytecode. Also fixes parse_object_body, which parsed plain
method items only and silently rejected override/fallible/abstract
(and now native) prefixes inside object bodies.
console is real Wollok source (wollok-std's console::SRC), merged into
the user's Scope at compile time, not something wollok-compiler knows
about by name. Its println method is declared `native` and resolved by
class name (NativeTable::register_for_class), since its ClassId isn't
known yet when the native gets registered. Accepts any number of
arguments (Arity::Any) since Wollok selectors are arity-fixed by
design and can't express variadics on their own.

Also: CompileError::Unsupported now carries the actual unsupported
AST node instead of a generic string, so failures are debuggable.
Shared test helpers (assert_str, test_fn!) moved into a dedicated
testing module, usable from every wollok-std submodule.
@romancitodev romancitodev changed the title feat(vm): interpreter loop + polymorphic inline cache feat(vm): interpreter loop, VM-CLI wiring, native dispatch, console Sep 17, 2026
Missing # Panics/# Errors sections, possibly-truncating usize -> u32
casts without try_from, a non-single-variant wildcard match, an
always-Ok Result, and two functions over clippy's line-count limit
(interpreter loop and the compiler entry point, both allowed with a
one-line reason: splitting either would hurt more than help).
…a const property

A `property` field now gets a free getter (arity 0) and setter
(`name=`, arity 1, matching what assignment syntax already compiles
to) without writing either by hand. `const property` generates only
the getter. An explicit `method name()`/`method name=(v)` still wins
over the synthesized one, since it's inserted into the same vtable
slot later in the same pass.
Four programs that actually compile and run end to end through the
CLI, not pseudocode: a singleton with mutable state, a class with a
guard clause, two instances of the same class messaging each other,
and property/const property (getter/setter auto-generation, an
explicit method overriding the synthesized getter).
The crate tree and processing flow still described the parser-only
era, missing wollok-compiler/wollok-vm/wollok-std entirely and
marking compilation/evaluation as "planned". Also adds a short
section up top on what actually runs end to end now.
@romancitodev
romancitodev merged commit 3a6f133 into main Sep 18, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant