diff --git a/CLAUDE.md b/CLAUDE.md index 1107a7104..5ae67884e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,83 +1,112 @@ -# CLAUDE.md +# Io Language VM -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +## What is this? -## Common Development Commands +The Io programming language — a dynamic prototype-based language built on message passing. See `README.md` for the full project description and build instructions for all platforms. -### Building Io +## Build ```bash -# Create build directory (one-time setup) -mkdir build && cd build +cd build +cmake --build . +``` -# Configure for development (debug mode) -cmake .. +The binary lands at `_build/binaries/io`. Tests at `_build/binaries/test_iterative_eval`. -# Configure for production (with optimizations) -cmake -DCMAKE_BUILD_TYPE=release .. +## Test -# Build the project -make +```bash +# Run a one-liner +./_build/binaries/io -e '"hello" println' -# Install (requires sudo on Unix systems) -sudo make install -``` +# Run a file +./_build/binaries/io path/to/script.io -### Running Tests +# C test suite (iterative evaluator) +./_build/binaries/test_iterative_eval -```bash -# Run the main test suite (from build directory) -io ../libs/iovm/tests/correctness/run.io +# Io test suite (from build directory) +io ../libs/iovm/tests/correctness/run.io # Full suite +io ../libs/iovm/tests/correctness/ListTest.io # Single test +IO_TEST_VERBOSE=1 io ../libs/iovm/tests/correctness/run.io # Verbose -# Run a specific test file -io ../libs/iovm/tests/correctness/Test.io +# Quick smoke test for control flow + exceptions +./_build/binaries/io -e 'if(true, "yes") println; for(i,1,3, i println); list(1,2,3) foreach(v, v println); e := try(1 unknownMethod); e error println; "done" println' ``` -### Development Workflow - -```bash -# Clean rebuild -cd build && make clean && make +## Project Structure -# Run the Io REPL -io - -# Execute an Io script -io script.io +``` +libs/iovm/source/ — Core VM implementation (C) +libs/iovm/io/ — Io standard library (.io files, compiled to C via io2c) +libs/iovm/tests/ — C test files +tools/source/main.c — REPL / CLI entry point +agents/ — Design docs for the continuations/stackless work ``` -## Architecture Overview - -### Core Structure +### Key Source Files + +| File | Purpose | +|------|---------| +| `IoState_iterative.c` | Iterative eval loop with frame state machine | +| `IoState_eval.c` | Entry points (`IoState_doCString_`, `IoState_on_doCString_withLabel_`) | +| `IoMessage.c` | `IoMessage_locals_performOn_` — recursive evaluator | +| `IoBlock.c` | Block activation (currently uses recursive evaluator) | +| `IoObject_flow.c` | Control flow primitives: `if`, `while`, `for`, `loop`, `break`, `continue`, `return` | +| `IoCoroutine.c` | Coroutine implementation (frame-based, no C stack switching) | +| `IoContinuation.c` | First-class continuations (`callcc`, capture, invoke) | +| `IoEvalFrame.h/c` | Frame structure and state machine enums | +| `IoState_inline.h` | Inline helpers, arg pre-evaluation | +| `IoState.h` | VM state structure | + +## Branch: `stackless` (off `continuations`) + +Replacing C stack recursion with heap-allocated frames to enable: +- First-class continuations (serializable, network-transmittable) +- Portable coroutines (no platform-specific assembly) +- No setjmp/longjmp, no ucontext, no fibers + +See `agents/` for detailed design docs: +- `CONTINUATIONS_TODO.md` — Phase tracker and implementation notes +- `C_STACK_ELIMINATION_PLAN.md` — Overall architecture plan +- `VM_EVAL_LOOP.md` — Eval loop design reference + +## Important Conventions + +### IoVMInit.c regeneration +When `.io` files in `libs/iovm/io/` are modified, the generated `libs/iovm/source/IoVMInit.c` must be regenerated. CMake's `io2c` step doesn't track `.io` file changes, so force it: +```bash +rm libs/iovm/source/IoVMInit.c +cd build && cmake --build . +``` -The Io language is a dynamic, prototype-based programming language implemented in C. The architecture consists of: +### Evaluator +- **Iterative** (`IoState_iterative.c`): Frame state machine. Used for all evaluation including control flow, continuations, coroutine switching. +- `IoMessage_locals_performOn_` redirects to the iterative eval loop when `currentFrame` is set. A bootstrap-only recursive fallback exists for VM initialization (before the first eval loop starts). -1. **Virtual Machine Core** (`libs/iovm/`): The interpreter and runtime system - - Objects clone from prototypes (no classes) - - Everything is an object that responds to messages - - Message passing is the fundamental operation - - Coroutines provide lightweight concurrency +### Special forms +Messages whose arguments must NOT be pre-evaluated: `if`, `while`, `loop`, `for`, `callcc`, `method`, `block`, `foreach`, `reverseForeach`, `foreachLine`. Checked in two places in `IoState_iterative.c`. -2. **Foundation Libraries** (`libs/`): - - `basekit`: Cross-platform C utilities and data structures - - `coroutine`: Architecture-specific context switching (x86, x86_64, ARM64, PowerPC) - - `garbagecollector`: Mark-and-sweep garbage collection +### Error handling +- C-level: `IoState_error_` sets `state->errorRaised = 1`. Eval loop unwinds frames. +- Io-level: `Exception raise` calls `rawSignalException` which also sets `errorRaised`. +- Helper functions return early on error (no longjmp). Eval loop handles all unwinding. -3. **Standard Library**: Split between C (`libs/iovm/source/`) and Io (`libs/iovm/io/`) implementations - - Core objects like IoObject, IoMessage, IoState in C - - Higher-level functionality in Io for flexibility +### Debug compile flags +- `DEBUG_EVAL_LOOP` — verbose iterative eval loop tracing +- `DEBUG_CORO_EVAL` — coroutine operation tracing -### Key Design Patterns +## Architecture (General) -- **Prototype-based OOP**: Objects clone from prototypes rather than instantiating classes -- **Message Passing**: All operations are messages sent to objects -- **C Naming Convention**: `IoObjectName_methodName` for C functions -- **Minimal Core**: Keep VM small, implement features in Io when possible +Every Io object is a `CollectorMarker` (`typedef struct CollectorMarker IoObject`). The marker's `object` field points to `IoObjectData` containing: a `tag` (vtable), `slots` (PHash cuckoo hash table), `protos` array, and a data union for primitive values. -### Testing Approach +All operations are message sends. The parser produces message chains, then `IoMessage_opShuffle.c` rewrites them by operator precedence. Assignment operators: `:=` → `setSlot`, `=` → `updateSlot`, `::=` → `newSlot`. -Tests are written in Io using the built-in UnitTest framework. The test runner (`libs/iovm/tests/correctness/run.io`) discovers and executes all test files ending with `Test.io`. +Standard library files in `libs/iovm/io/` are loaded in the explicit order listed in `CMakeLists.txt`: bootstrap files (`List_bootstrap.io`, `Object_bootstrap.io`, `OperatorTable.io`, `Object.io`, `List.io`, `Exception.io`) first, then alphabetical core, then `CLI.io`, `Importer.io` last. -### Build System +## Code Style -CMake-based build system with hierarchical CMakeLists.txt files. Each library manages its own build configuration and dependencies. \ No newline at end of file +- **Indentation**: Tabs in both C and Io code +- **C naming**: `IoObjectName_methodName` (e.g., `IoList_append_`) +- **C method macro**: `IO_METHOD(CLASS, NAME)` expands to `IoObject *CLASS_NAME(CLASS *self, IoObject *locals, IoMessage *m)` +- **Io naming**: camelCase for methods, PascalCase for objects diff --git a/CMakeLists.txt b/CMakeLists.txt index 547ca14d6..f79b72020 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,9 +35,6 @@ endif() # spaces, or anything silly like that please. project(IoLanguage C) -# Enable ASM language for ARM64 coroutine support -enable_language(ASM) - # Default config when building with gcc variants IF(CMAKE_COMPILER_IS_GNUCC OR (CMAKE_C_COMPILER_ID MATCHES "Clang")) SET(CMAKE_BUILD_TYPE_DebugFast) @@ -101,7 +98,6 @@ else() endif(${CMAKE_SYSTEM_NAME} MATCHES "Windows") # Definitions on where we can find headers and whatnot. Convenience definitions. -set(COROUTINE_SOURCE_DIR ${PROJECT_SOURCE_DIR}/libs/coroutine/source) set(BASEKIT_SOURCE_DIR ${PROJECT_SOURCE_DIR}/libs/basekit/source) set(GARBAGECOLLECTOR_SOURCE_DIR ${PROJECT_SOURCE_DIR}/libs/garbagecollector/source) set(IOVM_SOURCE_DIR ${PROJECT_SOURCE_DIR}/libs/iovm/source) @@ -115,7 +111,6 @@ add_subdirectory(tools) make_build_bundle(_build) # Next we NEED to copy all the libs headers into one single dir in the bundle. -copy_files(coroutine_headers ${PROJECT_SOURCE_DIR}/libs/coroutine/source/*.h ${CMAKE_CURRENT_BINARY_DIR}/_build/headers) copy_files(basekit_headers ${PROJECT_SOURCE_DIR}/libs/basekit/source/*.h ${CMAKE_CURRENT_BINARY_DIR}/_build/headers) copy_files(garbagecollector_headers ${PROJECT_SOURCE_DIR}/libs/garbagecollector/source/*.h ${CMAKE_CURRENT_BINARY_DIR}/_build/headers) copy_files(iovm_headers ${PROJECT_SOURCE_DIR}/libs/iovm/source/*.h ${CMAKE_CURRENT_BINARY_DIR}/_build/headers) diff --git a/agents/CONTINUATIONS_TODO.md b/agents/CONTINUATIONS_TODO.md new file mode 100644 index 000000000..dc82a0f5c --- /dev/null +++ b/agents/CONTINUATIONS_TODO.md @@ -0,0 +1,414 @@ +# Continuations Implementation - TODO + +## CRITICAL: NO C STACK MANIPULATION + +**The entire purpose of this project is to remove C stack dependency.** + +This means: +- **NO setjmp/longjmp** - not even for "error handling" +- **NO ucontext** - not even for "lightweight switching" +- **NO fibers** - not even for "performance" +- **NO assembly coroutine code** - this is what we're removing +- **NO Coro_switchTo_** - libcoroutine is deleted + +The goal is portable, serializable, network-transmittable execution state. +All execution state must be in heap-allocated frames, not on the C stack. + +**Error handling:** `IoState_error_` sets `errorRaised = 1` and returns +normally. Every CFunction that calls a helper which might raise an error +must check `IOSTATE->errorRaised` and return early. The eval loop checks +`errorRaised` after each CFunction returns and unwinds frames. + +--- + +## New VM Architecture + +### Core Concept + +One operation (SEND) + frame manipulation. Control flow primitives manipulate frames directly. + +### Frame Structure + +```c +typedef struct Frame { + IoMessage *message; // current message (instruction pointer) + struct Frame *caller; // return address (calling frame) + IoObject *locals; // context for slot lookup + IoObject *target; // current receiver + IoObject *result; // evaluation result + IoObject *originalTarget; // reset point for semicolon + bool tailCall; // if true, reuse frame instead of push + bool isBlockBoundary; // for return unwinding + bool isLoopBoundary; // for break/continue unwinding +} Frame; +``` + +### Sentinel Value + +```c +#define SKIP_NORMAL_DISPATCH ((IoObject *)-1) +``` + +When a primitive returns this, it has already manipulated the frame directly. + +### Two Primitive Styles + +**Normal primitives** - receive evaluated args, return value: + +```c +IO_METHOD(IoNumber, add) { + double a = IoNumber_asDouble(self); + double b = IoNumber_asDouble(args[0]); + return IoNumber_new(state, a + b); +} +``` + +**Control flow primitives** - receive frame + unevaluated message, manipulate frame: + +```c +IO_METHOD(IoTrue, ifTrue) { + // Push frame to evaluate the argument + Frame *argFrame = Frame_new(); + argFrame->message = IoMessage_argAt(m, 0); + argFrame->locals = frame->locals; + argFrame->target = frame->target; + argFrame->caller = frame; + + state->currentFrame = argFrame; + frame->message = IoMessage_next(m); + + return SKIP_NORMAL_DISPATCH; +} + +IO_METHOD(IoFalse, ifTrue) { + frame->result = self; + frame->target = self; + frame->message = IoMessage_next(m); + return SKIP_NORMAL_DISPATCH; +} +``` + +### Minimal C Primitives Required + +**Must be C (frame manipulation):** + +| Primitive | Reason | +|-----------|--------| +| `ifTrue` on true | Conditional - push frame for branch | +| `ifTrue` on false | Conditional - skip branch | +| `ifFalse` on true | Conditional - skip branch | +| `ifFalse` on false | Conditional - push frame for branch | +| `while` | Loop with break/continue support | +| `for` | Loop with break/continue support | +| `loop` | Loop with break/continue support | +| `return` | Unwind to block boundary | +| `break` | Unwind to loop boundary, exit | +| `continue` | Unwind to loop boundary, restart | +| `try`/`catch` | Exception frame handling | + +**Should be C (performance):** +- Number arithmetic (+, -, *, /, etc.) +- String operations +- List/Array operations +- Slot operations (setSlot, getSlot, etc.) + +**Can be Io:** +- `if` - built on ifTrue/ifFalse +- `forEach`, `map`, `select` - built on while/for +- Most object methods + +### Control Flow in Io + +```io +// Built on ifTrue/ifFalse +if := method(cond, then, else, + cond ifTrue(return then) + else +) + +// forEach built on while (which is C) +List forEach := method(indexName, valueName, body, + i := 0 + while(i < self size, + call sender setSlot(indexName, i) + call sender setSlot(valueName, self at(i)) + body doInContext(call sender) + i = i + 1 + ) +) +``` + +## Eval Loop + +See `VM_EVAL_LOOP.md` for detailed implementation. + +Core loop handles: +1. End of message chain → return to caller +2. Cached literals → use directly +3. Slot lookup → find value +4. Forward → handle missing slot +5. Activation → CFunction or Block +6. TCO → reuse frame when in tail position +7. Stop status → handle return/break/continue + +## Argument Evaluation + +**Fast path (synchronous):** +- Literals (cached result) +- Simple slot lookup (single message, no args, non-activatable) + +**Slow path (frame-based):** +- Complex expressions +- Method calls +- Message chains + +```c +IoObject *Frame_evaluateArgSync(Frame *frame, IoMessage *m, int i) { + IoMessage *argMsg = IoMessage_argAt(m, i); + + // Literal + if (IoMessage_cachedResult(argMsg)) { + return IoMessage_cachedResult(argMsg); + } + + // Simple slot lookup + if (!IoMessage_next(argMsg) && IoMessage_argCount(argMsg) == 0) { + IoObject *slotValue = IoObject_getSlot_(frame->locals, IoMessage_name(argMsg)); + if (slotValue && !IoObject_isActivatable(slotValue)) { + return slotValue; + } + } + + return NEEDS_FRAME_EVAL; +} +``` + +## Stop Status Handling + +```c +typedef enum { + STOP_NONE, + STOP_RETURN, + STOP_BREAK, + STOP_CONTINUE +} StopStatus; +``` + +**return:** Unwind to `isBlockBoundary`, set result, continue after block. + +**break:** Unwind to `isLoopBoundary`, exit loop entirely. + +**continue:** Unwind to `isLoopBoundary`, restart loop iteration. + +## Implementation Layers + +``` +┌─────────────────────────────────────────┐ +│ Io Code │ +│ - if, forEach, map, select, etc. │ +├─────────────────────────────────────────┤ +│ Control Flow (C, frame manipulation) │ +│ - ifTrue/ifFalse, while, for, loop │ +│ - return, break, continue │ +├─────────────────────────────────────────┤ +│ Core Primitives (C, non-reentrant) │ +│ - Number, String, List, Map │ +├─────────────────────────────────────────┤ +│ VM (C) │ +│ - eval loop, frames, GC │ +├─────────────────────────────────────────┤ +│ Platform Bindings (FFI) │ +│ - JS for WASM, C libs for native │ +└─────────────────────────────────────────┘ +``` + +## Result + +- **No re-entrancy** - primitives never call evaluator +- **Portable coroutines** - all state in frames (serializable) +- **Clean break/continue** - loops as C primitives +- **TCO** - recursive calls in tail position reuse frames +- **Minimal C** - ~300 lines eval loop + primitives + +## Work Items + +### Phase 1: Core VM ✅ COMPLETE +- [x] Define new Frame structure (`IoEvalFrame.h`) +- [x] Implement eval loop (~800 lines in `IoState_iterative.c`) +- [x] Implement frame push/pop with pooling +- [x] Implement TCO detection (direct tail calls + through if branches) +- [x] Implement stop status handling for loops (break/continue) +- [x] Add GC marking for frame stack (`IoEvalFrame_mark`) + +### Phase 2: Control Flow Primitives ✅ COMPLETE +- [x] Implement `if` with lazy branch evaluation (frame state machine) +- [x] Implement `while` (loop with break/continue) +- [x] Implement `for` (loop with break/continue) +- [x] Implement `loop` (infinite loop with break) +- [x] `return`, `break`, `continue` work with existing stop status mechanism + +**Note:** `IoMessage_locals_performOn_` redirects to the iterative eval loop +when `currentFrame` is set. All evaluation—including CFunction argument +evaluation and block activation—goes through the iterative path. A bootstrap- +only recursive fallback remains for VM initialization before the first eval +loop starts. + +### Phase 3: Argument Evaluation - DEFERRED +Current approach: Let CFunctions handle their own argument evaluation. +Control flow primitives receive unevaluated messages. + +Future work could pre-evaluate args in the loop for full iterative evaluation, +but this requires modifying all CFunctions. + +### Phase 4: Io Standard Library ✅ COMPLETE +- [x] `if` works as C primitive (not needed in Io) +- [x] `forEach` on List: C primitive with iterative eval (frame state machine) +- [x] `map`, `select`: pure Io methods in `List_bootstrap.io` using `foreach` internally +- [x] `reverseForeach`, `foreachLine`, `foreachSlot`: iterative via `IoMessage_locals_performOn_` redirect +- [x] Test control flow constructs (21/21 tests pass including callcc, exceptions, coros) + +**Note:** Original plan was to rewrite forEach as pure Io. Instead, forEach was +implemented as a C frame state machine (FRAME_STATE_FOREACH_*), which is better +because it avoids the overhead of Io-level loop management. map/select were +already Io-level methods from the start and work correctly through the iterative +path since forEach is iterative. + +### Phase 5: Continuation API ✅ COMPLETE +- [x] Design continuation capture API (`IoContinuation.h`) +- [x] Implement frame stack copy (`IoContinuation_copyFrameStack_`) +- [x] Implement `callcc` primitive with frame state machine +- [x] Implement `Continuation invoke` method (frame stack replacement) +- [x] Test continuation invoke (13/13 tests pass) +- [x] Add continuation introspection methods (`frameCount`, `frameStates`, `frameMessages`) +- [x] Implement serialization/deserialization (`asMap`/`fromMap`) + +**Implementation Notes:** +- `Continuation` object type created with capture and invoke methods +- `callcc(block)` captures frame stack before evaluating block +- Frame captured in `CALLCC_EVAL_BLOCK` state so restoration continues correctly +- `cont invoke(value)` replaces frame stack, sets `continuationInvoked` flag +- Eval loop checks flag after CFunction returns, restarts with new frame stack +- No setjmp/longjmp needed - frame state machine handles unwinding + +**Tail Call Optimization (TCO):** +Two mechanisms work together to keep frame stacks flat for recursive patterns: +1. **Direct TCO** (`IoState_activateBlockTCO_`): When a Block call is the last message + in a block body frame (`frame->blockLocals && !next`), reuse the frame instead of + pushing a new one. Handles `factorial(n-1, acc)` directly. +2. **TCO through if branches** (`IF_EVAL_BRANCH`): When `if()` is the last message in + the chain, evaluate the selected branch in-place instead of pushing a child frame. + This enables TCO for `if(n <= 0, acc, recurse(n-1, acc))`. +3. **savedCall preservation**: In-place if optimization saves `frame->call` in + `frame->savedCall` before repurposing the frame. The RETURN handler checks both + `frame->call` and `frame->savedCall` for Call stop status. This prevents the + `relayStopStatus`/`?` operator from losing its RETURN status when TCO replaces + `frame->call` after an in-place if optimization. + +### Phase 6: Coroutine Replacement ✅ COMPLETE +- [x] Implement coroutine as frame stack wrapper (IoCoroutineData with frameStack) +- [x] Implement save/restore state functions +- [x] Rewrite rawRun for frame-based operation +- [x] Rewrite rawReturnToParent for frame-based operation +- [x] Fix error handling without longjmp (helper functions return early) +- [x] REPL starts successfully! +- [x] Fix recursive/iterative evaluator mixing (added `state->inRecursiveEval` flag) +- [x] Make IoState_error_ lightweight (no coro swap from C stack) +- [x] Integrate Io-level exceptions with eval loop (Exception raise/pass) +- [x] Replace libcoroutine build dependency (deleted `libs/coroutine/`, cleaned CMakeLists) +- [x] Remove platform-specific assembly (all in deleted `libs/coroutine/`) +- [x] Test yield/resume (C tests: resume, yield, @@; CLI smoke tests all pass) +- [x] Test portable coroutines (21/21 C tests, 23/23 Io correctness test files pass) + +**Error Safety (return-and-check pattern):** +On master, `IoState_error_` did a longjmp (never returned). On this branch it returns +normally after setting `errorRaised = 1`. Every C function that calls a helper which +might raise an error must check `IOSTATE->errorRaised` and return early. Fixed sites: +- `IOASSERT` macro: added `return IONIL(self)` after `IoState_error_` +- `IoObject_rawClonePrimitive`: must set `isActivatable` from proto (prevents stale flags on recycled markers) +- `IoList_checkIndex`: changed to return int (0=ok, 1=error), callers check +- `IoList_sortInPlaceBy`: check `errorRaised` after `blockArgAt_` +- `IoFile_assertOpen/assertWrite`: return IONIL on error, all callers check +- `IoDirectory` opendir: `return IONIL(self)` after error (3 sites) +- `IoObject_self` (thisContext): skip arg pre-evaluation (prevents `ifNil` body executing on non-nil) +- `IoMessage_assertArgCount_receiver_`: all callers check `errorRaised` after +- `IoSeq_mutable.c IO_ASSERT_NOT_SYMBOL`: macro checks `errorRaised` after +- `IoCFunction_activate`: return after type mismatch error +- All `listArgAt_`/`mapArgAt_`/`blockArgAt_` callers: check `errorRaised` +- Various: `IoSeq_asJSON`, `IoSeq_findAnyOf`, `IoSeq_translate`, `IoBlock_code_`, etc. +- Nested eval loops: clear `inRecursiveEval` on entry (for correct control flow path) + +**IoState_error_ Made Lightweight:** +`IoState_error_` no longer calls `IoCoroutine_raiseError` → `rawReturnToParent`. +Instead it creates the Exception inline and sets `errorRaised = 1`. The eval loop +handles all frame unwinding: (1) the ACTIVATE handler catches errors during CFunction +calls and pops the retain pool, (2) a top-of-loop generic handler catches errors from +other contexts (e.g., `forward` in LOOKUP_SLOT). After unwinding, the `frame=NULL` +handler takes over for coro switching / nested eval exit. This eliminates the SIGSEGV +that occurred when `rawReturnToParent` tried to unwind frames from deep inside a +CFunction's C call stack. + +**Io-Level Exception Integration (FIXED):** +Io-level exceptions now work via `rawSignalException` which sets `errorRaised = 1`, +bridging Io-level exceptions to the eval loop's error handling. `Exception raise` and +`Exception raiseFrom` use `raiseException` which calls `rawSignalException` on the +current coro when there's no parent to resume. The eval loop then unwinds frames just +like C-level errors. `try()` catches both C-level and Io-level exceptions. + +**Critical Bug Fixed (recursive/iterative mixing):** +When CFunctions (like `doString`) call `IoMessage_locals_performOn_` (recursive evaluator) +while the iterative eval loop is running, control flow primitives (if, while, for, loop) +would see `state->currentFrame != NULL` and incorrectly try to use the iterative approach, +modifying the wrong frame. Fixed by adding `state->inRecursiveEval` flag that's set when +entering the recursive evaluator, allowing control flow primitives to use the correct path. + +### Phase 7: GC-Managed Frames ✅ COMPLETE +- [x] Phase 0: Mechanical rename (`frame->` → `fd->` via FRAME_DATA macro) +- [x] Phase 1: IoEvalFrame as IoObject (`typedef IoObject IoEvalFrame`, data behind `IoObject_dataPointer`) +- [x] Phase 2: Performance recovery (IoObject-level frame pool, 256 entries) +- [x] Phase 3: Io-level frame introspection (expose frame chain to Io code) + +**What changed:** +- `IoEvalFrame` is now `typedef IoObject IoEvalFrame` — frames are GC-managed +- `FRAME_DATA(frame)` dereferences `IoObject_dataPointer(frame)` (no longer identity cast) +- `pushFrame_` reuses pooled frames; `popFrame_` returns to pool (GC reclaims overflow) +- Continuations use grab-pointer capture (no deep copy); `copy` method for explicit snapshots +- `IoCoroutine_mark` simplified: single `IoObject_shouldMarkIfNonNull(frame)` marks entire chain +- `IoState_iterative_fast.c` excluded from build (dead code) +- 30/30 C tests, 239 Io tests (0 failures) +- Frame pool gives 6x speedup (23.85s → 3.65s on 1M for-loop) +- Block activation retain pool bracketing fixes WeakLink GC regression +- IoList atInsert/removeAt errorRaised checks fix pre-existing test failures + +**FRAME_DATA NULL safety:** Must check for NULL before calling `FRAME_DATA()` since it +dereferences a pointer. Pattern: `fd = frame ? FRAME_DATA(frame) : NULL;` + +## Files + +### Implementation +- `libs/iovm/source/IoEvalFrame.h` - Frame structure (IoObject typedef + IoEvalFrameData) +- `libs/iovm/source/IoEvalFrame.c` - Frame proto, tag, mark, free, reset, state names +- `libs/iovm/source/IoState_iterative.c` - Iterative eval loop with pooled push/pop +- `libs/iovm/source/IoObject_flow.c` - Control flow primitives (if, while, for, loop) +- `libs/iovm/source/IoContinuation.h` - Continuation object header +- `libs/iovm/source/IoContinuation.c` - Continuation capture (grab-pointer), invoke, copy, callcc, asMap/fromMap +- `libs/iovm/source/IoCoroutine.c` - Coroutine with simplified GC marking + +### Documentation +- `VM_EVAL_LOOP.md` - Detailed eval loop design +- `CONTINUATIONS_TODO.md` - This file + +### Tests +- `libs/iovm/tests/test_iterative_eval.c` - 30 tests for iterative evaluator (coro, TCO, continuations, ? operator, asMap) +- `libs/iovm/tests/debug_if.c` - Debug test for if primitive + +## Performance Notes + +**Frame pool recovery:** With GC-managed frames, the raw allocation path (IOCLONE per frame) +was 6x slower than the old C struct pool. Adding an IoObject-level pool (256 entries) in IoState +that reuses allocated-but-detached frames recovered the performance. Pooled frames are marked +by `IoCoroutine_mark` to prevent GC collection while parked. + +**Future optimizations:** +- Inline caching for slot lookup +- Bytecode compilation +- JIT (if really needed) diff --git a/agents/C_STACK_ELIMINATION_PLAN.md b/agents/C_STACK_ELIMINATION_PLAN.md new file mode 100644 index 000000000..b264c91fd --- /dev/null +++ b/agents/C_STACK_ELIMINATION_PLAN.md @@ -0,0 +1,419 @@ +# C Stack Elimination Plan + +## Problem Statement + +For continuations to work correctly, ALL execution state must be in heap-allocated frames. +Currently, C stack is used in many places during evaluation: + +1. **Recursive Evaluator** (`IoMessage_locals_performOn_`) - uses C stack for call chain +2. **Argument Evaluation** (`IoMessage_locals_valueArgAt_`) - calls recursive evaluator +3. **Collection Iteration** (`foreach`, `each`, etc.) - loops call evaluator per-element +4. **`doString`/`doMessage`** - explicitly call recursive evaluator + +When a continuation is captured while C code is on the stack: +- The C stack frames are NOT captured +- Invoking the continuation restores heap frames but C stack is gone +- Results in broken/incorrect execution + +## Current State + +### What Works (Iterative) +- Control flow primitives (`if`, `while`, `for`, `loop`) have iterative paths +- Block activation via `IoState_activateBlock_` pushes frames +- The `inRecursiveEval` flag prevents control flow from corrupting frames + +### What's Broken (Uses C Stack) +- `doString`/`doMessage`/`doFile` call recursive evaluator +- All CFunction argument evaluation via `IoMessage_locals_valueArgAt_` +- Collection iteration methods (`foreach`, `each`, `map`, etc.) +- ~100+ CFunctions that evaluate arguments + +## Solution Architecture + +### Core Insight + +The fundamental issue is that CFunctions expect **synchronous** argument evaluation, +but the iterative evaluator is **asynchronous** (pushes frames, returns to eval loop). + +**Solution**: Pre-evaluate arguments in the eval loop before calling CFunctions. + +### Key Changes + +1. **Argument Pre-evaluation**: Eval loop evaluates CFunction arguments before calling +2. **doString Pattern**: Convert evaluation-calling CFunctions to frame state machines +3. **Collection Iteration**: Convert foreach/each/etc to frame state machines +4. **Remove Fallbacks**: Eventually remove recursive evaluator calls entirely + +--- + +## Phase 1: Convert doString/doMessage/doFile + +**Goal**: Make code evaluation non-reentrant. + +### New Frame States +```c +FRAME_STATE_DO_COMPILE, // Compiling string to message (if needed) +FRAME_STATE_DO_EVAL, // Pushing frame to evaluate code +FRAME_STATE_DO_WAIT, // Waiting for evaluation result +``` + +### New Control Flow Info +```c +struct { + IoMessage *codeMessage; // The parsed/compiled message to evaluate + IoObject *evalTarget; // Target for evaluation + IoObject *evalLocals; // Locals for evaluation +} doInfo; +``` + +### Implementation Pattern + +**doString CFunction:** +```c +IO_METHOD(IoObject, doString) { + IoState *state = IOSTATE; + + if (state->currentFrame != NULL && !state->inRecursiveEval) { + // Iterative path + IoEvalFrame *frame = state->currentFrame; + + // Get the string argument (synchronously - it's usually a literal) + IoSymbol *string = IoMessage_locals_seqArgAt_(m, locals, 0); + IoSymbol *label = (IoMessage_argCount(m) > 1) + ? IoMessage_locals_symbolArgAt_(m, locals, 1) + : IOSYMBOL("doString"); + + // Compile string to message + IoMessage *codeMsg = IoMessage_newFromText_label_(state, CSTRING(string), CSTRING(label)); + + // Set up frame for evaluation + frame->controlFlow.doInfo.codeMessage = codeMsg; + frame->controlFlow.doInfo.evalTarget = self; + frame->controlFlow.doInfo.evalLocals = self; + frame->state = FRAME_STATE_DO_EVAL; + + state->needsControlFlowHandling = 1; + return state->ioNil; // Placeholder, real result comes from frame + } else { + // Recursive fallback (existing code) + return IoObject_rawDoString_label_(self, string, label); + } +} +``` + +**Eval Loop Handlers:** +```c +case FRAME_STATE_DO_EVAL: { + // Push frame to evaluate the code + IoEvalFrame *childFrame = IoState_pushFrame_(state); + childFrame->message = frame->controlFlow.doInfo.codeMessage; + childFrame->target = frame->controlFlow.doInfo.evalTarget; + childFrame->locals = frame->controlFlow.doInfo.evalLocals; + childFrame->cachedTarget = frame->controlFlow.doInfo.evalTarget; + childFrame->state = FRAME_STATE_START; + + frame->state = FRAME_STATE_DO_WAIT; + break; +} + +case FRAME_STATE_DO_WAIT: { + // Child frame has returned, result is in frame->result + // Continue to next message in chain + frame->state = FRAME_STATE_CONTINUE_CHAIN; + break; +} +``` + +### Files to Modify +- `IoEvalFrame.h` - Add new states and doInfo union member +- `IoState_iterative.c` - Add case handlers +- `IoObject.c` - Modify doString, doMessage, doFile + +### Testing +- `doString("1+1")` should return 2 +- Continuation captured in doString code should work correctly +- `doFile` should work for loading scripts + +--- + +## Phase 2: Pre-evaluate CFunction Arguments + +**Goal**: Eliminate `IoMessage_locals_valueArgAt_` re-entrancy. + +### Strategy + +After slot lookup, if the slot is a CFunction (not a block/method): +1. Count arguments +2. Push frames to evaluate each argument +3. Store results in `frame->argValues` +4. Then call the CFunction + +### Lazy vs Eager Arguments + +Some methods need unevaluated arguments: +- `and(a, b)` - don't evaluate `b` if `a` is false +- `or(a, b)` - don't evaluate `b` if `a` is true +- Macro-style methods + +**Solution**: Check if the CFunction has an "eager" tag. Default to eager. +Lazy methods (and/or/etc.) are marked specially. + +### Implementation + +**Modified LOOKUP_SLOT → ACTIVATE transition:** +```c +case FRAME_STATE_LOOKUP_SLOT: { + // ... existing slot lookup code ... + + if (slotValue) { + frame->slotValue = slotValue; + frame->slotContext = slotContext; + + // Check if we need to pre-evaluate arguments + if (ISCFUNCTION(slotValue) && !IoCFunction_isLazy(slotValue)) { + int argCount = IoMessage_argCount(frame->message); + if (argCount > 0 && !state->inRecursiveEval) { + // Set up argument evaluation + frame->argCount = argCount; + frame->argValues = io_calloc(argCount, sizeof(IoObject*)); + frame->currentArgIndex = 0; + frame->state = FRAME_STATE_EVAL_ARGS; + break; + } + } + + frame->state = FRAME_STATE_ACTIVATE; + } + // ... +} +``` + +**EVAL_ARGS state (already partially exists):** +```c +case FRAME_STATE_EVAL_ARGS: { + if (frame->currentArgIndex >= frame->argCount) { + // All arguments evaluated + frame->state = FRAME_STATE_ACTIVATE; + break; + } + + IoMessage *argMsg = IoMessage_rawArgAt_(frame->message, frame->currentArgIndex); + + // Check for cached result (literal) + if (IOMESSAGEDATA(argMsg)->cachedResult && !IOMESSAGEDATA(argMsg)->next) { + frame->argValues[frame->currentArgIndex] = IOMESSAGEDATA(argMsg)->cachedResult; + frame->currentArgIndex++; + break; + } + + // Push frame to evaluate argument + IoEvalFrame *argFrame = IoState_pushFrame_(state); + argFrame->message = argMsg; + argFrame->target = frame->locals; + argFrame->locals = frame->locals; + argFrame->cachedTarget = frame->locals; + argFrame->state = FRAME_STATE_START; + + // Result will be captured when argFrame returns + break; +} +``` + +**Modified RETURN to capture arg results:** +```c +case FRAME_STATE_RETURN: { + // ... existing code ... + + if (parent && parent->state == FRAME_STATE_EVAL_ARGS) { + parent->argValues[parent->currentArgIndex] = result; + parent->currentArgIndex++; + } + // ... +} +``` + +**Modified IoMessage_locals_valueArgAt_:** +```c +IoObject *IoMessage_locals_valueArgAt_(IoMessage *m, IoObject *locals, int n) { + IoState *state = IOSTATE; + + // Check for pre-evaluated arguments + if (state->currentFrame && state->currentFrame->argValues) { + if (n < state->currentFrame->argCount) { + IoObject *preEval = state->currentFrame->argValues[n]; + if (preEval) return preEval; + } + } + + // Fallback to recursive evaluation + IoMessage *argMessage = IoMessage_rawArgAt_(m, n); + return argMessage ? IoMessage_locals_performOn_(argMessage, locals, locals) + : state->ioNil; +} +``` + +### Files to Modify +- `IoState_iterative.c` - LOOKUP_SLOT, EVAL_ARGS, RETURN cases +- `IoMessage.c` - IoMessage_locals_valueArgAt_ and variants +- `IoCFunction.h/c` - Add isLazy flag (optional, for and/or) + +### Testing +- All existing tests should pass +- CFunction calls should work correctly +- Continuations captured during CFunction calls should work + +--- + +## Phase 3: Convert Collection Iteration + +**Goal**: Make foreach/each/map/etc non-reentrant. + +### New Frame States +```c +FRAME_STATE_FOREACH_INIT, // Initialize iteration +FRAME_STATE_FOREACH_EVAL_BODY, // Evaluating body expression +FRAME_STATE_FOREACH_NEXT, // Moving to next element +``` + +### Control Flow Info +```c +struct { + IoObject *collection; // List/Map/Sequence being iterated + IoMessage *bodyMessage; // Body to evaluate each iteration + IoSymbol *indexName; // Index variable name (optional) + IoSymbol *valueName; // Value variable name + int currentIndex; // Current iteration index + int collectionSize; // Total size + IoObject *lastResult; // Result of last body evaluation +} foreachInfo; +``` + +### Implementation Pattern + +This is similar to how `for` loop already works, but iterates over a collection. + +**List foreach CFunction:** +```c +IO_METHOD(IoList, foreach) { + IoState *state = IOSTATE; + + if (state->currentFrame != NULL && !state->inRecursiveEval) { + IoEvalFrame *frame = state->currentFrame; + + // Parse arguments (variable names, body message) + IoSymbol *indexName = NULL, *valueName = NULL; + IoMessage *bodyMsg = NULL; + // ... parse based on arg count ... + + frame->controlFlow.foreachInfo.collection = self; + frame->controlFlow.foreachInfo.bodyMessage = bodyMsg; + frame->controlFlow.foreachInfo.indexName = indexName; + frame->controlFlow.foreachInfo.valueName = valueName; + frame->controlFlow.foreachInfo.currentIndex = 0; + frame->controlFlow.foreachInfo.collectionSize = IoList_rawSize(self); + frame->controlFlow.foreachInfo.lastResult = NULL; + + frame->state = FRAME_STATE_FOREACH_EVAL_BODY; + state->needsControlFlowHandling = 1; + return state->ioNil; + } else { + // Recursive fallback (existing code) + // ... + } +} +``` + +### Files to Modify +- `IoEvalFrame.h` - Add new states and foreachInfo +- `IoState_iterative.c` - Add iteration state handlers +- `IoList.c` - foreach, each, reverseForeach +- `IoMap.c` - foreach, each +- `IoSeq_immutable.c` - foreach, each + +### Testing +- `list(1,2,3) foreach(i, v, v println)` should print 1, 2, 3 +- Continuation captured during iteration should work + +--- + +## Phase 4: Remove Recursive Fallbacks + +**Goal**: Eliminate the `inRecursiveEval` flag and recursive code paths. + +### Steps + +1. Ensure all control flow works without fallback +2. Ensure doString/doMessage work without fallback +3. Ensure collection iteration works without fallback +4. Remove `inRecursiveEval` flag from IoState +5. Remove recursive code paths from control flow primitives +6. Remove `IoMessage_locals_performOn_` or keep only for legacy/bootstrap + +### Testing +- All tests pass without recursive fallback +- Continuations work in all contexts + +--- + +## Phase 5: Audit Remaining Re-entrancy + +### Known Remaining Issues + +1. **Block activation from recursive context** - Blocks called via `IoBlock_activate` + when not in iterative context still use recursive eval + +2. **Dynamic method calls** - `perform`, `resend`, `super` may need conversion + +3. **Bootstrap** - Initial loading of Io files uses recursive eval + - This is acceptable since continuations aren't used during bootstrap + +### Action Items + +- Audit all calls to `IoMessage_locals_performOn_` +- Convert or mark as "bootstrap only" +- Document any remaining limitations + +--- + +## Implementation Order + +| Phase | Priority | Effort | Impact | +|-------|----------|--------|--------| +| Phase 1: doString | HIGH | Medium | Enables REPL continuation capture | +| Phase 2: Arg Pre-eval | HIGH | High | Enables most CFunctions to be safe | +| Phase 3: Collection | MEDIUM | Medium | Enables foreach/map continuation capture | +| Phase 4: Remove Fallbacks | LOW | Low | Cleanup, not functional | +| Phase 5: Audit | LOW | Low | Documentation | + +--- + +## Success Criteria + +1. **Continuations work in REPL**: Can capture continuation in doString code +2. **Continuations work in loops**: Can capture continuation inside foreach +3. **No C stack during normal execution**: All evaluation uses heap frames +4. **All tests pass**: 13/13 iterative evaluator tests +5. **Performance acceptable**: Not more than 2-3x slower than current + +--- + +## Files Summary + +### Must Modify +- `IoEvalFrame.h` - New states, control flow info +- `IoState_iterative.c` - New state handlers +- `IoObject.c` - doString, doMessage, doFile +- `IoMessage.c` - IoMessage_locals_valueArgAt_ +- `IoList.c` - foreach, each +- `IoMap.c` - foreach +- `IoObject_flow.c` - Remove recursive fallbacks (Phase 4) + +### May Modify +- `IoSeq_immutable.c` - foreach, each +- `IoFile.c` - foreachLine, foreach +- `IoNumber.c` - repeat +- `IoCFunction.h/c` - isLazy flag + +### Delete Eventually +- Recursive fallback code in control flow primitives +- Possibly `inRecursiveEval` flag diff --git a/agents/VM_EVAL_LOOP.md b/agents/VM_EVAL_LOOP.md new file mode 100644 index 000000000..b544a702d --- /dev/null +++ b/agents/VM_EVAL_LOOP.md @@ -0,0 +1,1668 @@ +# Io VM Eval Loop Design + +## Core Data Structures + +### Frame + +```c +typedef struct Frame { + IoMessage *message; // current message (instruction pointer) + struct Frame *caller; // return address (calling frame) + IoObject *locals; // context for slot lookup + IoObject *target; // current receiver + IoObject *result; // evaluation result + bool tailCall; // if true, reuse frame instead of push +} Frame; +``` + +### Sentinel Value + +```c +#define SKIP_NORMAL_DISPATCH ((IoObject *)-1) +``` + +When a primitive returns this, it has already manipulated the frame directly. + +## Main Eval Loop + +This is the conceptual loop. See "Design Review" section below for identified issues and corrections. + +```c +IoObject *IoState_eval(IoState *state) { + Frame *frame = state->currentFrame; + + while (frame) { + // === STOP STATUS CHECK === + if (state->stopStatus != STOP_STATUS_NORMAL) { + // Handle break/continue/return/exception - see detailed design below + IoState_handleStopStatus(state, &frame); + if (!frame) break; + continue; + } + + IoMessage *m = frame->message; + + // === END OF MESSAGE CHAIN === + if (!m) { + IoObject *result = frame->result; + Frame *caller = frame->caller; + if (caller) { + caller->result = result; + caller->target = result; + frame = caller; + } else { + state->result = result; // Save final result + frame = NULL; + } + state->currentFrame = frame; + continue; + } + + // === SEMICOLON - RESET TARGET === + IoSymbol *name = IoMessage_name(m); + if (name == state->semicolonSymbol) { + frame->target = frame->originalTarget; + frame->message = IoMessage_next(m); + continue; + } + + // === LITERAL (CACHED RESULT) === + if (IoMessage_cachedResult(m)) { + frame->result = IoMessage_cachedResult(m); + frame->target = frame->result; + frame->message = IoMessage_next(m); + continue; + } + + // === SLOT LOOKUP === + IoObject *context; + IoObject *slotValue = IoObject_getSlot_context_(frame->target, name, &context); + + // === SLOT NOT FOUND - TRY FORWARD === + if (!slotValue) { + IoObject *fwdContext; + IoObject *fwdSlot = IoObject_getSlot_context_(frame->target, + state->forwardSymbol, + &fwdContext); + if (fwdSlot && IoObject_isActivatable(fwdSlot)) { + IoObject *result = IoCFunction_activate(fwdSlot, frame->target, + frame, m, fwdContext); + if (result == SKIP_NORMAL_DISPATCH) { + continue; + } + frame->result = result; + frame->target = result; + frame->message = IoMessage_next(m); + continue; + } else { + IoState_raise(state, IoError_new(state, "Slot '%s' not found", CSTRING(name))); + continue; + } + } + + // === ACTIVATE SLOT VALUE === + if (IoObject_isActivatable(slotValue)) { + + if (ISCFUNCTION(slotValue)) { + // See "Argument Evaluation: Complete Solution" for full handling + IoObject *result = IoCFunction_activate(slotValue, frame->target, + frame, m, context); + if (result == SKIP_NORMAL_DISPATCH) { + continue; + } + frame->result = result; + frame->target = result; + frame->message = IoMessage_next(m); + } + else if (ISBLOCK(slotValue)) { + IoBlock *block = slotValue; + IoObject *newLocals = IoObject_createLocals(block, frame, m); + + // TCO: reuse frame if in tail position and this is a block frame + if (m->isTailPosition && frame->isBlockActivation) { + frame->message = IoBlock_message(block); + frame->locals = newLocals; + frame->target = newLocals; + frame->originalTarget = newLocals; + } else { + // Push new frame + Frame *newFrame = Frame_new(); + newFrame->message = IoBlock_message(block); + newFrame->locals = newLocals; + newFrame->target = newLocals; + newFrame->originalTarget = newLocals; + newFrame->caller = frame; + newFrame->isBlockActivation = true; + newFrame->passStops = IoBlock_passStops(block); + frame = newFrame; + state->currentFrame = frame; + } + } + } + else { + // === DATA SLOT - RETURN VALUE === + frame->result = slotValue; + frame->target = slotValue; + frame->message = IoMessage_next(m); + } + } + + return state->result; +} +``` + +## Primitive Styles + +### Normal Primitives + +Receive evaluated args, return a value: + +```c +IO_METHOD(IoNumber, add) { + double a = IoNumber_asDouble(self); + double b = IoNumber_asDouble(args[0]); + return IoNumber_new(state, a + b); +} + +IO_METHOD(IoList, size) { + return IoNumber_new(state, List_size(DATA(self))); +} + +IO_METHOD(IoString, at) { + int index = IoNumber_asInt(args[0]); + return IoString_charAt(self, index); +} +``` + +### Control Flow Primitives + +Receive frame + unevaluated message, manipulate frame directly. + +**NOTE:** The simple implementation below is WRONG - see "Issue 1" in Design Review for the corrected version that properly pushes frames. + +```c +// WRONG - naive implementation (doesn't handle message chains in args correctly) +// On true object +IO_METHOD(IoTrue, ifTrue) { + frame->message = IoMessage_argAt(m, 0); // evaluate arg + return SKIP_NORMAL_DISPATCH; +} + +IO_METHOD(IoTrue, ifFalse) { + frame->result = self; // skip arg + frame->target = self; + frame->message = IoMessage_next(m); + return SKIP_NORMAL_DISPATCH; +} + +// On false object +IO_METHOD(IoFalse, ifTrue) { + frame->result = self; // skip arg + frame->target = self; + frame->message = IoMessage_next(m); + return SKIP_NORMAL_DISPATCH; +} + +IO_METHOD(IoFalse, ifFalse) { + frame->message = IoMessage_argAt(m, 0); // evaluate arg + return SKIP_NORMAL_DISPATCH; +} +``` + +### Forward Implementation + +```c +// Default forward - raises error +IO_METHOD(IoObject, forward) { + IoSymbol *name = IoMessage_name(m); + return IoState_error(state, "Object does not respond to '%s'", CSTRING(name)); +} + +// Delegating forward - redirects to another object +IO_METHOD(IoProxyObject, forward) { + IoObject *delegate = IoObject_getSlot_(self, state->delegateSymbol); + frame->target = delegate; + // frame->message stays the same - retry on delegate + return SKIP_NORMAL_DISPATCH; +} +``` + +## Control Flow in Io + +Built on ifTrue/ifFalse + recursion + TCO: + +```io +if := method(cond, then, else, + cond ifTrue(return then) + else +) + +while := method(cond, body, + cond ifTrue( + body + while(cond, body) + ) +) + +for := method(counter, start, end, body, + counter := start + while(counter <= end, + body + counter = counter + 1 + ) +) + +loop := method(body, + body + loop(body) +) +``` + +## Argument Evaluation + +For normal primitives, arguments must be evaluated before calling. + +**Option A: Eager evaluation in eval loop** + +Before calling CFunction, eval loop evaluates all args: + +```c +if (ISCFUNCTION(slotValue)) { + // Evaluate args first + int argc = IoMessage_argCount(m); + IoObject **args = alloca(argc * sizeof(IoObject *)); + + for (int i = 0; i < argc; i++) { + // Push frame to evaluate arg[i] + // ... this needs thought ... + } + + IoObject *result = IoCFunction_activate(slotValue, frame->target, + frame, m, context, args, argc); +} +``` + +**Option B: Lazy evaluation via frame manipulation** + +Primitives that need evaluated args set up frames: + +```c +IO_METHOD(IoNumber, add) { + if (!frame->argsEvaluated) { + // Request arg evaluation + frame->argsEvaluated = true; + // Set up frame to evaluate arg 0, then return here + // ... complex ... + return SKIP_NORMAL_DISPATCH; + } + // Args ready + double a = IoNumber_asDouble(self); + double b = IoNumber_asDouble(frame->evaluatedArgs[0]); + return IoNumber_new(state, a + b); +} +``` + +**Option C: Synchronous arg evaluation (current Io)** + +Args evaluated synchronously, but only for non-control-flow primitives. +This requires careful design to avoid re-entrancy. + +## Deep Analysis: Potential Issues + +### 1. Argument Evaluation Without Re-entrancy + +**Problem:** Normal primitives need evaluated args, but evaluating args requires the eval loop. + +**Solution:** Hybrid approach: + +```c +IoObject *Frame_evaluateArgSync(Frame *frame, IoMessage *m, int i) { + IoMessage *argMsg = IoMessage_argAt(m, i); + + // Fast path: literal + if (IoMessage_cachedResult(argMsg)) { + return IoMessage_cachedResult(argMsg); + } + + // Fast path: simple slot lookup (no chain, no args) + if (!IoMessage_next(argMsg) && IoMessage_argCount(argMsg) == 0) { + IoObject *slotValue = IoObject_getSlot_(frame->locals, IoMessage_name(argMsg)); + if (slotValue && !IoObject_isActivatable(slotValue)) { + return slotValue; + } + } + + // Slow path: need full frame-based evaluation + return NEEDS_FRAME_EVAL; +} +``` + +Most args (literals, variable lookups) hit the fast path. Complex expressions fall back to frame-based evaluation, which requires the primitive to be written as a state machine or for the eval loop to pre-evaluate. + +**Alternative:** Pre-evaluate ALL args in eval loop before calling primitives: + +```c +if (ISCFUNCTION(slotValue) && !isControlFlowPrimitive(slotValue)) { + // Push frames to evaluate all args + // After all args evaluated, call primitive + // This requires tracking "pending primitive call" state +} +``` + +### 2. ifTrue/ifFalse Must Push Frames + +**Issue discovered:** Simply setting `frame->message` to the arg message doesn't work correctly for return flow. + +**Partially corrected implementation** (still has a bug - see Issue 1 in Design Review): + +```c +IO_METHOD(IoTrue, ifTrue) { + // Push frame to evaluate the argument + Frame *argFrame = Frame_new(); + argFrame->message = IoMessage_argAt(m, 0); + argFrame->locals = frame->locals; + argFrame->target = frame->target; // BUG: should be frame->locals + argFrame->caller = frame; + + state->currentFrame = argFrame; + + // When argFrame completes, current frame continues to next message + frame->message = IoMessage_next(m); + + return SKIP_NORMAL_DISPATCH; +} + +IO_METHOD(IoFalse, ifTrue) { + // Don't evaluate arg, just continue + frame->result = self; + frame->target = self; + frame->message = IoMessage_next(m); + return SKIP_NORMAL_DISPATCH; +} +``` + +**Trace:** `true ifTrue(x) println` +1. Eval `true` → result=true, message=`ifTrue(x)` +2. Lookup ifTrue on true, call IoTrue_ifTrue +3. Push argFrame for `x`, set frame->message to `println` +4. argFrame evaluates `x`, returns result +5. Pop to frame: result=x_result, target=x_result +6. Eval `println` on x_result + +This correctly chains the result. **But** the argFrame->target is wrong - see Issue 1 for the fully corrected version. + +### 3. TCO Detection + +**Problem:** When is a call in tail position? + +**Answer:** A call is in tail position when: +- It's the last message in a chain (no `next`) +- AND we're about to return from the current block + +**Detection:** + +```c +bool isTailPosition(Frame *frame, IoMessage *m) { + return IoMessage_next(m) == NULL && + frame->message == m; // not in middle of evaluating something +} +``` + +**When to apply:** +- Block activation when in tail position → reuse frame +- Must be careful with control flow (ifTrue pushes frames, not TCO) + +### 4. Return Statement + +**Problem:** `return x` needs to exit current block and return x. + +**Solution:** Set a flag and unwind: + +```c +typedef struct Frame { + // ... existing fields ... + bool isBlockBoundary; // true for block/method activations +} Frame; + +IO_METHOD(IoObject, return) { + // Evaluate the return value (need frame for this) + // Then set stop status + state->stopStatus = STOP_RETURN; + state->returnValue = evaluatedArg; + + return SKIP_NORMAL_DISPATCH; +} +``` + +In eval loop: +```c +// At start of loop iteration +if (state->stopStatus == STOP_RETURN) { + // Unwind to block boundary + while (frame && !frame->isBlockBoundary) { + frame = frame->caller; + } + if (frame) { + frame->result = state->returnValue; + frame->message = NULL; // signal return + } + state->stopStatus = STOP_NONE; + state->currentFrame = frame; + continue; +} +``` + +### 5. Break/Continue in Loops + +**Problem:** With `while` as recursive Io code, how do break/continue work? + +**Option A:** Use stop status flags + +```c +IO_METHOD(IoObject, break) { + state->stopStatus = STOP_BREAK; + return SKIP_NORMAL_DISPATCH; +} +``` + +In while (Io code), check stop status: +```io +while := method(cond, body, + cond ifTrue( + body + _checkBreak ifFalse( + while(cond, body) + ) + ) +) +``` + +But `_checkBreak` would need to be a primitive... + +**Option B:** Implement while/for/loop as C primitives + +These control structures manipulate frames directly and handle break/continue: + +```c +IO_METHOD(IoObject, while) { + // Mark frame as loop boundary + frame->isLoopBoundary = true; + frame->loopCondition = IoMessage_argAt(m, 0); + frame->loopBody = IoMessage_argAt(m, 1); + + // Set up to evaluate condition + Frame *condFrame = Frame_new(); + condFrame->message = frame->loopCondition; + // ... set up frame ... + + // After condition, check result and either: + // - evaluate body then loop back + // - or exit + + return SKIP_NORMAL_DISPATCH; +} +``` + +**Recommendation:** Option B is cleaner. while/for/loop as C primitives that manage their own looping via frame manipulation. Only `ifTrue`/`ifFalse` need to be minimal. + +### 6. Semicolon / Message Chain Boundaries + +**Problem:** In `a; b`, semicolon resets target to original. + +**Solution:** Track original target in frame: + +```c +typedef struct Frame { + // ... existing fields ... + IoObject *originalTarget; // reset point for semicolon +} Frame; +``` + +When evaluating semicolon message: +```c +if (name == state->semicolonSymbol) { + frame->target = frame->originalTarget; + frame->message = IoMessage_next(m); + continue; +} +``` + +### 7. The `call` Object + +**Problem:** Io's `call` provides introspection: sender, message, target, etc. + +**Solution:** Create Call object during block activation: + +```c +// In block activation +IoCall *callObj = IoCall_new(state); +IoCall_setSender_(callObj, frame->caller->locals); +IoCall_setMessage_(callObj, m); +IoCall_setTarget_(callObj, frame->target); +IoCall_setActivated_(callObj, block); + +// Add to new locals +IoObject_setSlot_(newLocals, state->callSymbol, callObj); +``` + +This is already how current Io works. Just need to preserve it. + +### 8. Garbage Collection + +**Problem:** Frames reference IoObjects. GC must mark them. + +**Solution:** Walk frame stack during GC mark phase: + +```c +void IoState_markFrames(IoState *state) { + Frame *frame = state->currentFrame; + while (frame) { + IoObject_mark(frame->locals); + IoObject_mark(frame->target); + IoObject_mark(frame->result); + // Mark messages if they're IoObjects + frame = frame->caller; + } +} +``` + +### 9. Coroutine Capture/Restore + +**Capture:** +```c +IoCoroutine *IoCoroutine_capture(IoState *state) { + IoCoroutine *coro = IoCoroutine_new(state); + + // Deep copy frame stack + coro->frameStack = copyFrameStack(state->currentFrame); + + return coro; +} +``` + +**Restore:** +```c +void IoCoroutine_resume(IoCoroutine *coro, IoState *state) { + // Save current frame stack (for the calling coroutine) + state->currentCoroutine->frameStack = state->currentFrame; + + // Restore this coroutine's frame stack + state->currentFrame = coro->frameStack; + state->currentCoroutine = coro; +} +``` + +Locals objects are heap-allocated, so they survive capture automatically. + +### 10. Slot Assignment (:= and =) + +**Problem:** `x := 1` and `x = 1` are special forms. + +**Solution:** Handled by parser or as primitives: + +```c +IO_METHOD(IoObject, setSlot) { + // args: name (symbol), value + IoSymbol *name = args[0]; // or get from message + IoObject *value = args[1]; + IoObject_setSlot_(frame->target, name, value); + return value; +} +``` + +The parser transforms `x := 1` into `setSlot("x", 1)`. + +### 11. Lazy Argument Evaluation + +**Current Io behavior:** Args only evaluated when accessed. + +**Question:** Do we preserve this? + +**Options:** +- A: Pre-evaluate all args (loses lazy semantics) +- B: Keep lazy, primitives request evaluation (complex) +- C: Lazy for control flow, eager for normal primitives (pragmatic) + +**Recommendation:** Option C. Control flow primitives receive unevaluated messages. Normal primitives get evaluated args. This matches user expectations - side effects in args happen in predictable order. + +## Revised Design Decisions + +1. **ifTrue/ifFalse** push frames (not just pointer manipulation) +2. **while/for/loop** implemented as C primitives for clean break/continue +3. **Normal primitives** receive evaluated args (eager) +4. **Control flow primitives** receive frame + message (lazy) +5. **break/continue/return** use stop status flags + frame unwinding +6. **TCO** applied at block activation when in tail position +7. **Frame has** originalTarget for semicolon reset +8. **call object** created during block activation + +## Minimal Primitive Set + +After analysis, the true minimal set of C primitives: + +**Must be C (frame manipulation):** +- `ifTrue` on true/false +- `ifFalse` on true/false +- `while` (for break/continue) +- `for` (for break/continue) +- `loop` (for break/continue) +- `return` +- `break` +- `continue` +- `try`/`catch` (exception handling) + +**Should be C (performance):** +- Number arithmetic +- String operations +- List/Array operations +- Slot operations (setSlot, getSlot, etc.) + +**Can be Io:** +- `if` (built on ifTrue/ifFalse) +- Higher-level iteration (forEach, map, select - built on while/for) +- Most object methods + +--- + +# Design Review: Identified Issues and Solutions + +## Issue 1: ifTrue argFrame has wrong initial target + +**Problem:** In section 2, the corrected ifTrue pushes an argFrame with: +```c +argFrame->target = frame->target; // BUG: target is `true`, not locals +``` + +When evaluating `true ifTrue(x)`, the argument `x` should be looked up starting from **locals**, not from `true`. Io's slot lookup searches target → target's protos, and locals are NOT automatically searched unless target IS locals. + +**Solution:** +```c +IO_METHOD(IoTrue, ifTrue) { + Frame *argFrame = Frame_new(); + argFrame->message = IoMessage_argAt(m, 0); + argFrame->locals = frame->locals; + argFrame->target = frame->locals; // FIXED: start lookup from locals + argFrame->originalTarget = frame->locals; + argFrame->caller = frame; + + state->currentFrame = argFrame; + frame->message = IoMessage_next(m); + + return SKIP_NORMAL_DISPATCH; +} +``` + +## Issue 2: Forward passes undefined context + +**Problem:** In the main eval loop, when a slot isn't found, `context` is undefined: +```c +IoObject *slotValue = IoObject_getSlot_context_(frame->target, name, &context); +if (!slotValue) { + IoObject *forwardSlot = IoObject_getSlot_(frame->target, state->forwardSymbol); + // ... + IoCFunction_activate(forwardSlot, frame->target, frame, m, context); // STALE +``` + +**Solution:** Get context when looking up forward: +```c +if (!slotValue) { + IoObject *forwardContext; + IoObject *forwardSlot = IoObject_getSlot_context_(frame->target, + state->forwardSymbol, + &forwardContext); + if (forwardSlot && IoObject_isActivatable(forwardSlot)) { + IoObject *result = IoCFunction_activate(forwardSlot, frame->target, + frame, m, forwardContext); + // ... + } +} +``` + +## Issue 3: Final result not saved to state + +**Problem:** At eval loop exit, `state->result` is never set. When the last frame pops, there's no caller to receive the result. + +**Solution:** Save result when popping the last frame: +```c +if (!m) { + IoObject *result = frame->result; + Frame *caller = frame->caller; + if (caller) { + caller->result = result; + caller->target = result; + frame = caller; + } else { + state->result = result; // SAVE FINAL RESULT + frame = NULL; + } + state->currentFrame = frame; + continue; +} +``` + +## Issue 4: originalTarget never initialized + +**Problem:** The Frame has `originalTarget` for semicolon reset, but block activation never sets it. + +**Solution:** Initialize in block activation: +```c +// Push new frame for block +Frame *newFrame = Frame_new(); +newFrame->message = IoBlock_message(block); +newFrame->locals = newLocals; +newFrame->target = newLocals; +newFrame->originalTarget = newLocals; // ADD THIS +newFrame->caller = frame; +``` + +--- + +# Break/Continue: The passStops Approach + +After reviewing current Io's implementation, the `passStops` mechanism is cleaner than `isLoopBoundary`. Here's how it works: + +## Stop Status Constants + +```c +#define STOP_STATUS_NORMAL 0 +#define STOP_STATUS_BREAK 1 +#define STOP_STATUS_CONTINUE 2 +#define STOP_STATUS_RETURN 4 +``` + +## Block passStops Flag + +Blocks have a `passStops` flag (default false). When false, the block **catches** break/continue/return. When true, they propagate through. + +```c +typedef struct Frame { + IoMessage *message; + struct Frame *caller; + IoObject *locals; + IoObject *target; + IoObject *result; + IoObject *originalTarget; + + // Control flow + bool isBlockActivation; // true for block/method frames + bool passStops; // if false, catches break/continue/return +} Frame; +``` + +## Break and Continue Primitives + +```c +IO_METHOD(IoObject, break) { + IoObject *value = IONIL(self); + + // Break can take an optional return value + if (IoMessage_argCount(m) > 0) { + // Must evaluate arg before setting stop status + // Use fast path if possible, otherwise push frame + IoMessage *argMsg = IoMessage_argAt(m, 0); + + if (IoMessage_cachedResult(argMsg)) { + value = IoMessage_cachedResult(argMsg); + } else if (!IoMessage_next(argMsg) && IoMessage_argCount(argMsg) == 0) { + // Simple slot lookup + IoObject *v = IoObject_getSlot_(frame->locals, IoMessage_name(argMsg)); + if (v && !IoObject_isActivatable(v)) { + value = v; + } else { + // Need frame-based eval - push special frame + Frame *argFrame = Frame_new(); + argFrame->message = argMsg; + argFrame->locals = frame->locals; + argFrame->target = frame->locals; + argFrame->caller = frame; + argFrame->isBreakArg = true; // Special marker + state->currentFrame = argFrame; + frame->message = NULL; + return SKIP_NORMAL_DISPATCH; + } + } + } + + state->stopStatus = STOP_STATUS_BREAK; + state->returnValue = value; + return SKIP_NORMAL_DISPATCH; +} + +IO_METHOD(IoObject, continue) { + state->stopStatus = STOP_STATUS_CONTINUE; + return SKIP_NORMAL_DISPATCH; +} +``` + +## Stop Status Handling in Eval Loop + +```c +// After popping a frame that was evaluating break's arg +if (frame && frame->isBreakArg) { + state->stopStatus = STOP_STATUS_BREAK; + state->returnValue = frame->result; + frame = frame->caller; + state->currentFrame = frame; +} + +// Handle stop status +if (state->stopStatus != STOP_STATUS_NORMAL) { + // Unwind to a frame that catches this stop + while (frame) { + // Block activations without passStops catch the stop + if (frame->isBlockActivation && !frame->passStops) { + frame->result = state->returnValue; + frame->message = NULL; // Signal return from this frame + state->stopStatus = STOP_STATUS_NORMAL; + break; + } + frame = frame->caller; + } + state->currentFrame = frame; + continue; +} +``` + +## C-Level Loops Check Status After Body + +```c +IO_METHOD(IoObject, while) { + IoState_resetStopStatus(state); + + for (;;) { + // Evaluate condition + IoObject *cond = IoMessage_locals_valueArgAt_(m, locals, 0); + if (!ISTRUE(cond)) break; + + // Evaluate body + IoObject *result = IoMessage_locals_valueArgAt_(m, locals, 1); + + // Check for break/continue + int status = state->stopStatus; + if (status == STOP_STATUS_BREAK) { + IoState_resetStopStatus(state); + return state->returnValue; // break(value) returns this + } + if (status == STOP_STATUS_CONTINUE) { + IoState_resetStopStatus(state); + // Continue to next iteration + } + if (status == STOP_STATUS_RETURN) { + // Don't reset - let it propagate + return result; + } + } + return result; +} +``` + +## Io-Level Iteration with passStops + +For Io code that implements iteration, the body block needs `passStops = true`: + +```io +List myForEach := method(body, + body setPassStops(true) // Let break/continue propagate to caller's loop + i := 0 + while(i < self size, + body call(self at(i)) + i = i + 1 + ) +) + +// Usage - break works correctly +list(1, 2, 3, 4, 5) myForEach(x, + if(x == 3, break) + x println +) +``` + +Without `setPassStops(true)`, the break would be caught by the body block and not propagate to the while loop. + +## Frame-Based While (Iterative, No Re-entrancy) + +For the fully iterative eval loop, while needs to be a state machine: + +```c +typedef enum { + WHILE_EVAL_COND, + WHILE_CHECK_COND, + WHILE_EVAL_BODY, + WHILE_DONE +} WhileState; + +typedef struct WhileFrame { + Frame base; + WhileState state; + IoMessage *condMsg; + IoMessage *bodyMsg; + IoObject *lastResult; +} WhileFrame; + +// In eval loop, handle WhileFrame specially: +if (frame->type == FRAME_WHILE) { + WhileFrame *wf = (WhileFrame *)frame; + + switch (wf->state) { + case WHILE_EVAL_COND: + // Push frame to evaluate condition + { + Frame *condFrame = Frame_new(); + condFrame->message = wf->condMsg; + condFrame->locals = frame->locals; + condFrame->target = frame->locals; + condFrame->caller = frame; + state->currentFrame = condFrame; + wf->state = WHILE_CHECK_COND; + } + continue; + + case WHILE_CHECK_COND: + // Condition result is in frame->result (from popped condFrame) + if (!ISTRUE(frame->result)) { + wf->state = WHILE_DONE; + frame->result = wf->lastResult; + frame->message = NULL; // Signal done + } else { + // Push frame to evaluate body + Frame *bodyFrame = Frame_new(); + bodyFrame->message = wf->bodyMsg; + bodyFrame->locals = frame->locals; + bodyFrame->target = frame->locals; + bodyFrame->caller = frame; + bodyFrame->passStops = true; // Let break/continue come back to us + state->currentFrame = bodyFrame; + wf->state = WHILE_EVAL_BODY; + } + continue; + + case WHILE_EVAL_BODY: + // Body result is in frame->result + wf->lastResult = frame->result; + + // Check for break/continue + if (state->stopStatus == STOP_STATUS_BREAK) { + state->stopStatus = STOP_STATUS_NORMAL; + frame->result = state->returnValue; + frame->message = NULL; + continue; + } + if (state->stopStatus == STOP_STATUS_CONTINUE) { + state->stopStatus = STOP_STATUS_NORMAL; + // Fall through to re-evaluate condition + } + if (state->stopStatus == STOP_STATUS_RETURN) { + // Don't catch return - let it propagate + frame->message = NULL; + continue; + } + + // Loop back to condition + wf->state = WHILE_EVAL_COND; + continue; + + case WHILE_DONE: + // Handled by normal frame pop + break; + } +} +``` + +--- + +# Argument Evaluation: Complete Solution + +## The Problem + +Normal primitives need evaluated args, but we can't recursively call eval. + +## Solution: ArgEval Frames + +When a CFunction needs evaluated args, push an ArgEvalFrame: + +```c +typedef struct ArgEvalFrame { + Frame base; + IoCFunction *pendingPrimitive; + IoObject *receiver; + IoObject *context; + IoMessage *originalMessage; + int argCount; + int nextArg; + IoObject **evaluatedArgs; +} ArgEvalFrame; +``` + +## Eval Loop Integration + +```c +// When activating a CFunction +if (ISCFUNCTION(slotValue)) { + IoCFunction *cf = (IoCFunction *)slotValue; + + // Control flow primitives handle their own args + if (cf->isControlFlow) { + IoObject *result = IoCFunction_activate(cf, frame->target, frame, m, context); + if (result == SKIP_NORMAL_DISPATCH) continue; + frame->result = result; + frame->target = result; + frame->message = IoMessage_next(m); + continue; + } + + // Normal primitives need evaluated args + int argc = IoMessage_argCount(m); + if (argc == 0) { + // No args - call directly + IoObject *result = IoCFunction_activate(cf, frame->target, frame, m, context); + frame->result = result; + frame->target = result; + frame->message = IoMessage_next(m); + continue; + } + + // Try fast path for all args + IoObject **args = alloca(argc * sizeof(IoObject *)); + bool allFast = true; + + for (int i = 0; i < argc && allFast; i++) { + IoMessage *argMsg = IoMessage_argAt(m, i); + + if (IoMessage_cachedResult(argMsg)) { + args[i] = IoMessage_cachedResult(argMsg); + } else if (!IoMessage_next(argMsg) && IoMessage_argCount(argMsg) == 0) { + IoObject *v = IoObject_getSlot_(frame->locals, IoMessage_name(argMsg)); + if (v && !IoObject_isActivatable(v)) { + args[i] = v; + } else { + allFast = false; + } + } else { + allFast = false; + } + } + + if (allFast) { + // All args evaluated via fast path - call primitive + IoObject *result = IoCFunction_activateWithArgs(cf, frame->target, + frame, m, context, args, argc); + frame->result = result; + frame->target = result; + frame->message = IoMessage_next(m); + continue; + } + + // Slow path: push ArgEvalFrame + ArgEvalFrame *aef = ArgEvalFrame_new(); + aef->base.type = FRAME_ARG_EVAL; + aef->base.caller = frame; + aef->base.locals = frame->locals; + aef->pendingPrimitive = cf; + aef->receiver = frame->target; + aef->context = context; + aef->originalMessage = m; + aef->argCount = argc; + aef->nextArg = 0; + aef->evaluatedArgs = IoState_alloc(state, argc * sizeof(IoObject *)); + + // Copy any args we already evaluated + for (int i = 0; i < argc; i++) { + if (args[i]) aef->evaluatedArgs[i] = args[i]; + } + + // Find first arg that needs frame eval + while (aef->nextArg < argc && aef->evaluatedArgs[aef->nextArg]) { + aef->nextArg++; + } + + // Push frame to evaluate that arg + Frame *argFrame = Frame_new(); + argFrame->message = IoMessage_argAt(m, aef->nextArg); + argFrame->locals = frame->locals; + argFrame->target = frame->locals; + argFrame->caller = (Frame *)aef; + + frame->message = IoMessage_next(m); // After primitive, continue here + state->currentFrame = argFrame; + continue; +} + +// Handle ArgEvalFrame receiving a result +if (frame->type == FRAME_ARG_EVAL) { + ArgEvalFrame *aef = (ArgEvalFrame *)frame; + + // Store the just-evaluated arg + aef->evaluatedArgs[aef->nextArg] = aef->base.result; + aef->nextArg++; + + // Find next arg needing eval + while (aef->nextArg < aef->argCount && aef->evaluatedArgs[aef->nextArg]) { + aef->nextArg++; + } + + if (aef->nextArg < aef->argCount) { + // More args to evaluate + Frame *argFrame = Frame_new(); + argFrame->message = IoMessage_argAt(aef->originalMessage, aef->nextArg); + argFrame->locals = aef->base.locals; + argFrame->target = aef->base.locals; + argFrame->caller = (Frame *)aef; + state->currentFrame = argFrame; + continue; + } + + // All args ready - call the primitive + IoObject *result = IoCFunction_activateWithArgs( + aef->pendingPrimitive, aef->receiver, + (Frame *)aef, aef->originalMessage, aef->context, + aef->evaluatedArgs, aef->argCount); + + // Pop ArgEvalFrame, store result in caller + Frame *caller = aef->base.caller; + caller->result = result; + caller->target = result; + state->currentFrame = caller; + + // Free the ArgEvalFrame + ArgEvalFrame_free(aef); + continue; +} +``` + +--- + +# Exception Handling: try/catch + +## Frame Structure + +```c +typedef struct TryFrame { + Frame base; + IoMessage *catchMsg; + IoSymbol *exceptionSlotName; + bool inCatch; +} TryFrame; +``` + +## try Primitive + +```c +IO_METHOD(IoObject, try) { + // try(tryBody, catch(e, catchBody)) + // or: try(tryBody) catch(e, catchBody) + + TryFrame *tf = TryFrame_new(); + tf->base.type = FRAME_TRY; + tf->base.caller = frame; + tf->base.locals = frame->locals; + tf->catchMsg = IoMessage_argAt(m, 1); // The catch clause + tf->exceptionSlotName = /* extract from catch */; + tf->inCatch = false; + + // Push frame to evaluate try body + Frame *tryFrame = Frame_new(); + tryFrame->message = IoMessage_argAt(m, 0); + tryFrame->locals = frame->locals; + tryFrame->target = frame->locals; + tryFrame->caller = (Frame *)tf; + tryFrame->passStops = true; // Let exceptions propagate to TryFrame + + state->currentFrame = tryFrame; + frame->message = IoMessage_next(m); + + return SKIP_NORMAL_DISPATCH; +} +``` + +## Exception Handling in Eval Loop + +```c +#define STOP_STATUS_EXCEPTION 8 + +// Raising an exception +void IoState_raise(IoState *state, IoObject *exception) { + state->stopStatus = STOP_STATUS_EXCEPTION; + state->exception = exception; +} + +// In eval loop, handle exceptions +if (state->stopStatus == STOP_STATUS_EXCEPTION) { + // Unwind looking for TryFrame + while (frame) { + if (frame->type == FRAME_TRY) { + TryFrame *tf = (TryFrame *)frame; + if (!tf->inCatch && tf->catchMsg) { + // Found a try block - execute catch + tf->inCatch = true; + + // Bind exception to slot name + IoObject_setSlot_(frame->locals, tf->exceptionSlotName, state->exception); + state->stopStatus = STOP_STATUS_NORMAL; + state->exception = NULL; + + // Push frame for catch body + Frame *catchFrame = Frame_new(); + catchFrame->message = IoMessage_argAt(tf->catchMsg, 1); + catchFrame->locals = frame->locals; + catchFrame->target = frame->locals; + catchFrame->caller = frame; + state->currentFrame = catchFrame; + break; + } + } + frame = frame->caller; + } + + if (!frame) { + // Uncaught exception - exit eval loop with error + state->currentFrame = NULL; + return; // Or handle top-level exception + } + continue; +} +``` + +--- + +# TCO: Refined Detection + +## The Challenge + +TCO should apply when a block call is in tail position. But "tail position" is subtle: + +| Code | Tail position? | +|------|---------------| +| `method(foo)` | foo: yes | +| `method(foo bar)` | foo: no, bar: yes | +| `method(if(c, a, b))` | a and b: yes (inside if) | +| `method(x := foo)` | foo: no (assignment after) | +| `method(a; foo)` | a: no, foo: yes | +| `method(return foo)` | foo: no (return handles it) | + +## Solution: Mark at Parse Time + +The parser can mark messages that are in tail position: + +```c +typedef struct IoMessage { + // ... existing fields ... + bool isTailPosition; +} IoMessage; + +// Parser sets this when: +// - Message is last in chain (no next) +// - AND not inside an assignment RHS +// - AND not inside return's argument +``` + +## TCO in Block Activation + +```c +if (ISBLOCK(slotValue)) { + IoBlock *block = slotValue; + IoObject *newLocals = IoObject_createLocals(block, frame, m); + + // TCO: if in tail position AND current frame is a block activation + if (m->isTailPosition && frame->isBlockActivation) { + // Reuse frame + frame->message = IoBlock_message(block); + frame->locals = newLocals; + frame->target = newLocals; + frame->originalTarget = newLocals; + // Keep frame->caller, passStops, etc. + } else { + // Push new frame + Frame *newFrame = Frame_new(); + newFrame->message = IoBlock_message(block); + newFrame->locals = newLocals; + newFrame->target = newLocals; + newFrame->originalTarget = newLocals; + newFrame->caller = frame; + newFrame->isBlockActivation = true; + newFrame->passStops = IoBlock_passStops(block); + frame = newFrame; + state->currentFrame = frame; + } +} +``` + +## TCO for ifTrue/ifFalse Branches + +When ifTrue/ifFalse is itself in tail position, the branch can inherit that: + +```c +IO_METHOD(IoTrue, ifTrue) { + Frame *argFrame = Frame_new(); + argFrame->message = IoMessage_argAt(m, 0); + argFrame->locals = frame->locals; + argFrame->target = frame->locals; + argFrame->originalTarget = frame->locals; + argFrame->caller = frame; + argFrame->passStops = true; + + // If ifTrue is in tail position, mark the arg frame for TCO eligibility + argFrame->inheritTailPosition = m->isTailPosition; + + state->currentFrame = argFrame; + frame->message = IoMessage_next(m); + + return SKIP_NORMAL_DISPATCH; +} +``` + +--- + +# Updated Frame Structure + +```c +typedef enum { + FRAME_NORMAL, + FRAME_WHILE, + FRAME_FOR, + FRAME_ARG_EVAL, + FRAME_TRY +} FrameType; + +typedef struct Frame { + FrameType type; + IoMessage *message; + struct Frame *caller; + IoObject *locals; + IoObject *target; + IoObject *result; + IoObject *originalTarget; + + // Block activation info + bool isBlockActivation; + bool passStops; + + // TCO + bool inheritTailPosition; + + // Special markers + bool isBreakArg; +} Frame; +``` + +--- + +# Updated Minimal Primitive Set + +**Must be C (frame/state manipulation):** +- `ifTrue` / `ifFalse` on true/false objects +- `while` / `for` / `loop` (state machine frames) +- `return` / `break` / `continue` (stop status) +- `try` / `catch` / `raise` (exception handling) +- `setPassStops` on Block + +**Should be C (performance + arg eval):** +- Number arithmetic +- String operations +- List/Map operations +- Slot operations + +**Can be Io:** +- `if` (built on ifTrue/ifFalse) +- `forEach` / `map` / `select` (use setPassStops for break/continue support) +- Most object methods + +--- + +# Remaining Edge Cases + +## 1. Break/Continue Outside Loop + +What if `break` is called with no enclosing loop? + +```io +break // No loop! +``` + +The stop status propagates up. If no frame catches it (no block without passStops, or all blocks have passStops=true), it reaches the top of the frame stack. + +**Solution:** Check at top level: + +```c +// After unwinding completes +if (!frame && state->stopStatus != STOP_STATUS_NORMAL) { + if (state->stopStatus == STOP_STATUS_BREAK) { + IoState_error(state, "break called outside of loop"); + } else if (state->stopStatus == STOP_STATUS_CONTINUE) { + IoState_error(state, "continue called outside of loop"); + } + state->stopStatus = STOP_STATUS_NORMAL; +} +``` + +## 2. Return from Top Level + +```io +return 42 // Outside any method +``` + +**Behavior:** Return at top level should probably just end evaluation with that value: + +```c +// In stop status handling +if (state->stopStatus == STOP_STATUS_RETURN) { + while (frame && frame->passStops) { + frame = frame->caller; + } + if (frame) { + frame->result = state->returnValue; + frame->message = NULL; + state->stopStatus = STOP_STATUS_NORMAL; + } else { + // Top-level return - end evaluation + state->result = state->returnValue; + state->stopStatus = STOP_STATUS_NORMAL; + state->currentFrame = NULL; + } +} +``` + +## 3. Exception in Catch Block + +```io +try( + raise("first") +) catch(e, + raise("second") // Exception while handling exception +) +``` + +When `raise("second")` executes, we're in the TryFrame's catch phase (`inCatch = true`). The exception should propagate to an outer try block, not be caught by the same try. + +The current design handles this because we check `!tf->inCatch`: + +```c +if (frame->type == FRAME_TRY) { + TryFrame *tf = (TryFrame *)frame; + if (!tf->inCatch && tf->catchMsg) { // Won't catch if already in catch + // ... + } +} +``` + +## 4. GC and Special Frames + +ArgEvalFrame, WhileFrame, TryFrame have extra heap-allocated data. GC must mark them: + +```c +void IoState_markFrames(IoState *state) { + Frame *frame = state->currentFrame; + while (frame) { + // Mark common fields + IoObject_mark(frame->locals); + IoObject_mark(frame->target); + IoObject_mark(frame->result); + IoObject_mark(frame->originalTarget); + + // Mark type-specific fields + switch (frame->type) { + case FRAME_ARG_EVAL: { + ArgEvalFrame *aef = (ArgEvalFrame *)frame; + IoObject_mark(aef->receiver); + for (int i = 0; i < aef->argCount; i++) { + if (aef->evaluatedArgs[i]) { + IoObject_mark(aef->evaluatedArgs[i]); + } + } + break; + } + case FRAME_WHILE: { + WhileFrame *wf = (WhileFrame *)frame; + IoObject_mark(wf->lastResult); + break; + } + case FRAME_TRY: { + TryFrame *tf = (TryFrame *)frame; + // catchMsg is IoMessage, mark if needed + break; + } + default: + break; + } + + frame = frame->caller; + } +} +``` + +## 5. Coroutine Capture with Special Frames + +When capturing a coroutine, we shallow-copy frame structs. Special frames need their extra data copied too: + +```c +Frame *copyFrameStack(Frame *frame) { + if (!frame) return NULL; + + Frame *copy; + switch (frame->type) { + case FRAME_ARG_EVAL: + copy = (Frame *)ArgEvalFrame_clone((ArgEvalFrame *)frame); + break; + case FRAME_WHILE: + copy = (Frame *)WhileFrame_clone((WhileFrame *)frame); + break; + case FRAME_TRY: + copy = (Frame *)TryFrame_clone((TryFrame *)frame); + break; + default: + copy = Frame_clone(frame); + break; + } + + copy->caller = copyFrameStack(frame->caller); + return copy; +} +``` + +**Note:** IoObjects are NOT copied - they're shared between coroutine stacks. This is intentional for coroutines (shared mutable state). For full call/cc semantics, this would need consideration. + +## 6. Stop Status and Coroutines + +Each coroutine must have its own stopStatus: + +```c +typedef struct IoCoroutine { + IoObject base; + Frame *frameStack; + int stopStatus; // Per-coroutine + IoObject *returnValue; // Per-coroutine + IoObject *exception; // Per-coroutine +} IoCoroutine; +``` + +When switching coroutines: + +```c +void IoCoroutine_switch(IoState *state, IoCoroutine *from, IoCoroutine *to) { + // Save current state + from->frameStack = state->currentFrame; + from->stopStatus = state->stopStatus; + from->returnValue = state->returnValue; + from->exception = state->exception; + + // Restore target state + state->currentFrame = to->frameStack; + state->stopStatus = to->stopStatus; + state->returnValue = to->returnValue; + state->exception = to->exception; + + state->currentCoroutine = to; +} +``` + +This ensures break/continue/return/exceptions don't leak between coroutines. + +## 7. Deeply Nested Arg Evaluation + +What if an arg itself calls a function that needs arg evaluation? + +```io +foo(bar(baz(x))) +``` + +This creates nested ArgEvalFrames: +``` +[original frame] + [ArgEvalFrame for foo] + [arg frame evaluating bar(baz(x))] + [ArgEvalFrame for bar] + [arg frame evaluating baz(x)] + [ArgEvalFrame for baz] + [arg frame evaluating x] +``` + +This works correctly because each ArgEvalFrame maintains its own state and they nest naturally through the caller chain. + +## 8. Primitive Calling Another Primitive + +What if a C primitive needs to call Io code internally? + +```c +IO_METHOD(IoList, sort) { + // Need to call user-provided comparison block + // But we're in a primitive - can't re-enter eval! +} +``` + +**Solution:** The primitive must set up frames and return SKIP_NORMAL_DISPATCH. It becomes a state machine: + +```c +typedef struct SortFrame { + Frame base; + IoList *list; + IoBlock *compareBlock; + int sortState; + // ... sorting algorithm state ... +} SortFrame; +``` + +This is complex. Alternative: mark such primitives as "yields" and have them push continuation frames. Or: for complex operations like sort, implement in Io using lower-level primitives. + +--- + +# Summary: Complete Frame Lifecycle + +``` +1. Eval loop running +2. Message lookup finds a block or CFunction +3. For block: + - If tail position + block frame: reuse frame (TCO) + - Otherwise: push new Frame with isBlockActivation=true +4. For CFunction: + - If control flow: call directly, may manipulate frames + - If no args: call directly + - If all args fast-path: evaluate inline, call + - Otherwise: push ArgEvalFrame, start evaluating args +5. ArgEvalFrame evaluates args one by one +6. When all args ready: call primitive, pop ArgEvalFrame +7. When frame->message is NULL: pop frame, pass result to caller +8. Stop status checked after each iteration: + - break/continue: unwind to catching block + - return: unwind to block boundary + - exception: unwind to try block +9. When frame stack empty: eval complete, return state->result +``` diff --git a/docs/Exceptions.md b/docs/Exceptions.md new file mode 100644 index 000000000..9e27cc43e --- /dev/null +++ b/docs/Exceptions.md @@ -0,0 +1,139 @@ +# Exception System Design Notes + +## Current Implementation + +### try/catch (non-resumable) + +Standard exception handling. `try(code)` runs code in a child coroutine. If an exception is raised, it's captured on the coroutine and returned. `catch` filters by exception type. `pass` re-raises. + +```io +e := try(Exception raise("boom")) +e catch(Exception, writeln(e error)) +``` + +### signal/withHandler (resumable) + +Added in the `stackless` branch. `withHandler` installs a handler on the coroutine's handler stack, evaluates the body, then removes the handler. `signal` walks the handler stack to find a matching handler and calls it as a subroutine — the signaler's frames remain on the stack. + +```io +result := withHandler(Exception, block(exc, resume, + "default value" +), + Exception signal("something went wrong") +) +// result is "default value" +``` + +The handler runs as a normal block activation with its own frame. Whatever the handler returns becomes the return value of `signal()` at the call site. No coroutine switch, no frame manipulation, no `callcc`. + +`_Resumption` is a placeholder object whose `invoke(v)` returns `v`. It exists for API consistency but is functionally the identity. + +## Comparison with Other Languages + +### Common Lisp Condition System + +CL separates three concerns: + +1. **Signaling** — low-level code detects a problem and signals a condition +2. **Handling** — high-level code decides on a recovery policy +3. **Restarting** — low-level code offers named recovery strategies + +```lisp +;; Low-level: offers restarts, doesn't choose +(defun parse-entry (line) + (restart-case (actually-parse line) + (skip-entry () nil) + (use-value (v) v))) + +;; High-level: chooses policy, doesn't know recovery details +(handler-bind ((malformed-entry + (lambda (c) (invoke-restart 'skip-entry)))) + (parse-log-file stream)) +``` + +Key features Io lacks: +- **Restarts**: named recovery points established by the signaler. The handler picks one without knowing recovery details. This decouples policy from mechanism. +- **Decline**: a `handler-bind` handler can return normally without invoking a restart, and the search continues to the next handler. In Io, the handler's return value is always used. +- **Interactive debugging**: unhandled conditions present available restarts to the developer in the debugger. SLIME's debugger lets you inspect the live stack, fix data, and resume — the program continues as if the error never happened. + +### Smalltalk (on:do:) + +Handler receives the exception object with a rich protocol: +- `ex resume: value` — resume at signal site with value (non-local return from handler) +- `ex return: value` — return from the protected block +- `ex retry` — re-run the protected block +- `ex pass` — delegate to outer handler +- `ex outer` — invoke outer handler, then resume + +More expressive than Io's current system but lacks CL's restart separation. + +## Possible Directions + +### 1. Add Restarts (CL-style) + +The most impactful addition. Restarts let the signaler offer recovery strategies without choosing one, and the handler choose a strategy without knowing recovery details. + +Possible Io API: + +```io +// Low-level code establishes restarts +parseEntry := method(line, + withRestarts( + list( + Restart clone setName("skipEntry") setAction(block(nil)), + Restart clone setName("useValue") setAction(block(v, v)) + ), + actuallyParse(line) + ) +) + +// High-level code picks a policy +withHandler(MalformedEntry, block(exc, resume, + invokeRestart("skipEntry") +), + parseLogFile(stream) +) +``` + +Implementation: a restart registry (List) on the Coroutine, similar to `handlerStack`. `withRestarts` pushes entries, `invokeRestart` walks the registry. Pure Io-level code, no VM changes needed. + +### 2. Handler Decline + +Allow a handler to decline (pass to next handler) instead of always producing a value. Could use a sentinel: + +```io +withHandler(Exception, block(exc, resume, + if(exc error containsSeq("fatal"), + decline // search continues to next handler + , + "recovered" + ) +), + body +) +``` + +### 3. Richer Exception Protocol (Smalltalk-style) + +Add methods to the exception object for `retry`, `return(value)`, `outer`. These would use the eval loop's existing stop-status mechanism or handler stack walking. + +### 4. Interactive Restart Selection in REPL + +When an unhandled signal reaches the top level with available restarts, present them to the user: + +``` +Error: Malformed entry at line 42 +Available restarts: + 0: [skipEntry] Skip this entry + 1: [useValue] Supply a replacement value + 2: [abort] Abort +Pick a restart: +``` + +This requires restarts (direction 1) and REPL integration. + +### Recommendation + +Start with **restarts** (direction 1). They provide the biggest leverage — decoupling error policy from recovery mechanism — and map naturally to Io's prototype model. A Restart is just an Object with `name` and `action` slots. The handler/restart registries are Lists on the Coroutine. No VM changes, no new primitives, just Io-level code building on `withHandler`. + +Decline (direction 2) is a small addition on top. Interactive selection (direction 4) is the long-term payoff but requires restarts first. diff --git a/docs/IoStacklessExamples.md b/docs/IoStacklessExamples.md new file mode 100644 index 000000000..943fef116 --- /dev/null +++ b/docs/IoStacklessExamples.md @@ -0,0 +1,321 @@ +# Io Stackless VM Examples + +The stackless branch replaces Io's recursive C-stack evaluator with a heap-allocated frame-based iterative evaluator. This enables portable coroutines, tail call optimization, frame introspection, and robust exception handling — all without platform-specific assembly, `setjmp`/`longjmp`, or `ucontext`. + +--- + +## Tail Call Optimization + +The stackless evaluator has two TCO mechanisms that keep frame stacks flat for recursive patterns. + +### Direct Tail Recursion + +When a block call is the last message in a block body, the frame is reused instead of pushing a new one: + +```io +countdown := method(n, + if(n <= 0, return n) + countdown(n - 1) +) +countdown(100000) // no stack overflow +``` + +### TCO Through If Branches + +When `if()` is the last message in a chain, the selected branch evaluates in-place. This is the idiomatic Io recursion pattern: + +```io +factorial := method(n, acc, + if(n <= 1, acc, factorial(n - 1, n * acc)) +) +factorial(100000, 1) // no stack overflow + +sumAcc := method(n, acc, + if(n <= 0, return acc) + sumAcc(n - 1, acc + n) +) +sumAcc(100000, 0) // => 5000050000 +``` + +### Non-Tail Recursion + +Even without TCO, recursion is bounded by heap rather than C stack depth: + +```io +sumTo := method(n, if(n <= 0, 0, n + sumTo(n - 1))) +sumTo(100) // => 5050, uses heap frames +``` + +--- + +## Coroutines + +Coroutines work by saving and restoring the frame pointer — no C stack switching. A suspended coroutine's entire state is a single pointer to its saved frame chain. Switching is O(1). + +### Async Dispatch (@@) + +The `@@` operator dispatches a message to a new coroutine: + +```io +o := Object clone +o work := method( + for(i, 1, 5, i println; yield) +) +o @@work +for(i, 1, 5, yield) +``` + +### Futures (@) + +The `@` operator returns a future that resolves when the coroutine completes: + +```io +obj := Object clone +obj double := method(v, v * 2) +future := obj @double(2) +future println // => 4 +``` + +### Manual Coroutine Control + +Create and resume coroutines directly: + +```io +c := Coroutine clone +c setRunTarget(Lobby) +c setRunLocals(Lobby) +c setRunMessage(message("from coro" println)) +c resume +``` + +### Cooperative Scheduling + +Coroutines yield control explicitly with `yield` and `pause`: + +```io +o := Object clone +o s := Sequence clone +o l := method( + j := 1 + loop( + s appendSeq("a", j asString, ".") + if(j % 2 == 0, pause) + j = j + 1 + ) +) + +o @@l +for(i, 1, 4, + yield + o s appendSeq("b", i asString, ".") + if(i == 2, o actorCoroutine recentInChain resumeLater) +) +// o s => "a1.a2.b1.b2.a3.a4.b3.b4." +``` + +### Scheduler + +The `Scheduler` manages a single shared queue called `yieldingCoros` — the list of coroutines that are ready to run. All coroutine switching goes through this queue. + +**yield** appends the current coroutine to the back of the queue, pops the first one off, and resumes it. If the queue is empty, yield is a no-op. If the current coroutine is the only one in the queue, it's also a no-op: + +```io +// Coroutine yield (simplified): +yield := method( + if(yieldingCoros isEmpty, return) + yieldingCoros append(self) // put ourselves at the back + next := yieldingCoros removeFirst // take the next one off the front + if(next == self, return) // we're the only one, nothing to do + next resume // switch to it +) +``` + +**pause** removes the current coroutine from the queue entirely and resumes the next one. The paused coroutine won't run again until something calls `resume` or `resumeLater` on it: + +```io +// Coroutine pause (simplified): +pause := method( + yieldingCoros remove(self) + next := yieldingCoros removeFirst + if(next, next resume, + Exception raise("Scheduler: nothing left to resume") + ) +) +``` + +**resumeLater** puts a coroutine at the front of the queue without switching to it — it will be the next one to run when the current coroutine yields: + +```io +someCoroutine resumeLater // insert at front of yieldingCoros +``` + +The scheduler loop waits for all coroutines to finish: + +```io +Scheduler waitForCorosToComplete // yields until yieldingCoros is empty +``` + +### Waiting + +`Object wait(s)` is a cooperative sleep — if other coroutines are in the queue, it yields in a loop until the deadline passes. If no other coroutines exist, it falls back to `System sleep`: + +```io +// cooperative wait — other coroutines run while we wait +wait(0.5) + +// equivalent to: +endDate := Date clone now + Duration clone setSeconds(0.5) +loop(endDate isPast ifTrue(break); yield) +``` + +### Futures and Waiting on Results + +The `@` operator returns a `FutureProxy`. When you send any message to the proxy, the calling coroutine pauses until the result is ready. The actor's coroutine calls `setResult` when done, which `resumeLater`s all waiting coroutines: + +```io +obj := Object clone do( + compute := method(n, n * n) +) + +result := obj @compute(7) +// result is a FutureProxy — calling coroutine keeps running +doOtherWork +result println // pauses here until compute finishes, then prints 49 +``` + +Under the hood: + +```io +// Future waitOnResult (simplified): +waitOnResult := method( + waitingCoros append(Scheduler currentCoroutine) + Scheduler currentCoroutine pause // removes us from yieldingCoros +) + +// Future setResult — called when the actor finishes: +setResult := method(r, + proxy _become(r) // proxy becomes the real value + waitingCoros foreach(resumeLater) // wake up everyone who was waiting +) +``` + +This is the general pattern for all async operations in Io. A coroutine waiting on an async operation (socket read, file I/O, timer, etc.) is **not in the `yieldingCoros` queue at all** — `pause` removed it. The coroutine only exists as a reference in the operation's waiting list. It won't be scheduled until the operation completes and calls `resumeLater`, which puts it back at the front of `yieldingCoros`. + +For example, a socket read looks like: + +```io +// Inside a Socket read method (conceptual): +streamReadNextChunk := method( + waitingCoros append(Scheduler currentCoroutine) + Scheduler currentCoroutine pause // off the scheduler entirely + // ... execution resumes here when data arrives ... + readBuffer +) + +// When the event loop detects data ready on the socket: +onReadable := method( + waitingCoros foreach(resumeLater) // back on the scheduler + waitingCoros empty +) +``` + +The VM's `IoState_activeCoroCallback` hook (in `IoState_callbacks.c`) lets an external event loop (libevent, kqueue, epoll, etc.) drive the scheduler — it gets notified when the coroutine count changes so it can integrate polling with coroutine switching. + +### Actor Pattern + +Objects become actors with `@` and `@@`. Each actor gets its own coroutine and message queue, processing messages one at a time with `yield` between each: + +```io +Database := Object clone do( + store := method(key, value, + // ... store data ... + "stored #{key}" interpolate println + ) +) + +// Fire-and-forget (@@) — returns nil +Database @@store("a", 1) +Database @@store("b", 2) + +// Future (@) — returns a proxy that blocks on access +result := Database @store("c", 3) +result println // pauses here until store completes +``` + +--- + +## Frame Introspection + +Live execution frames are exposed to Io code for debugging and metaprogramming. + +### Walking the Frame Stack + +```io +f := Coroutine currentCoroutine currentFrame +while(f != nil, + f description println + f = f parent +) +``` + +### Inspecting Frame Properties + +Each frame exposes its execution state: + +```io +f := Coroutine currentCoroutine currentFrame +f state println // frame state machine state (e.g., "activate") +f depth println // distance from bottom of frame stack +f target println // receiver object (self) +f locals println // enclosing scope +f result println // accumulated result +f message println // current message being evaluated +f call println // call introspection object (in methods) +f blockLocals println // block's local scope (in block activations) +``` + +### Programmatic Stack Traces + +```io +stackTrace := method( + frames := list + f := Coroutine currentCoroutine currentFrame + while(f != nil, + frames append(f description) + f = f parent + ) + frames +) +``` + +--- + +## Exception Handling + +Exceptions use the frame unwinding mechanism — no `longjmp`/`setjmp`. Both C-level errors and Io-level `Exception raise` set `errorRaised` and the eval loop unwinds frames. + +### Try / Catch + +```io +e := try(Exception raise("boom")) +e error println // => "boom" +"execution continues" println +``` + +### Exception Pass (Re-raise) + +```io +e := try( + try(Exception raise("inner")) pass +) +e error println // => "inner" +``` + +### C-Level Errors + +C-level errors (type mismatches, index out of bounds, etc.) are caught the same way: + +```io +e := try(1 unknownMethod) +e error println // => "Object does not respond to 'unknownMethod'" +``` diff --git a/docs/StacklessReport.md b/docs/StacklessReport.md new file mode 100644 index 000000000..4b581c75a --- /dev/null +++ b/docs/StacklessReport.md @@ -0,0 +1,298 @@ +# Stackless Evaluator Report + +## Overview + +The `stackless` branch replaces Io's recursive C-stack-based evaluator with a heap-allocated frame-based iterative evaluator. Every piece of execution state — message pointer, target, locals, arguments, control flow — lives in GC-managed frame objects on the heap rather than in C stack frames. + +### Goals + +1. **First-class continuations** — `callcc` captures the entire execution state as a serializable, network-transmittable object. Impossible with C stack recursion. **Note:** `callcc` is disabled by default (behind `#ifdef IO_CALLCC`) because undelimited continuations can rewind past cleanup code and corrupt handler stacks. Enable with `-DIO_CALLCC` at build time. +2. **Portable coroutines** — No platform-specific assembly, no `setjmp`/`longjmp`, no `ucontext`, no fibers. Coroutine switching is just swapping a frame pointer. +3. **No stack overflow** — Tail call optimization reuses frames. Deep recursion is bounded by heap, not C stack depth. +4. **Performance** — The iterative evaluator with object pooling and inline caching is 1.1x–4x faster than the recursive evaluator across all benchmarks. + +### Non-Goals + +- Changing the Io language semantics. All existing Io code runs unmodified. +- JIT compilation or bytecode. The evaluator still interprets the message tree directly. + +--- + +## Architecture + +### The Eval Loop + +A single C function, `IoState_evalLoop_()`, runs a `while(1)` loop that processes frames from a linked stack. Each frame has a `state` field (a state machine enum) that tells the loop what to do next. There is no C recursion for message evaluation — argument evaluation, block activation, and control flow are all driven by pushing/popping frames and transitioning states. + +``` +┌─────────────────────────────────────────────────┐ +│ IoState_evalLoop_ │ +│ │ +│ while(1) { │ +│ frame = state->currentFrame │ +│ switch(frame->state) { │ +│ START → LOOKUP_SLOT → ACTIVATE │ +│ ├── CFunction: call directly │ +│ └── Block: push child frame │ +│ CONTINUE_CHAIN → next message or RETURN │ +│ RETURN → pop frame, store result │ +│ IF_*, WHILE_*, FOR_*, ... (control flow)│ +│ } │ +│ } │ +└─────────────────────────────────────────────────┘ +``` + +### Frame Structure + +Frames are full IoObjects managed by the garbage collector (`typedef IoObject IoEvalFrame`). Their data payload is `IoEvalFrameData`: + +| Field | Purpose | +|-------|---------| +| `message` | Current message being evaluated (instruction pointer) | +| `target` | Receiver object (`self`) | +| `locals` | Enclosing scope for slot lookup | +| `result` | Accumulated result of this frame | +| `state` | Current state machine state | +| `parent` | Parent frame (for returning results) | +| `argValues` / `inlineArgs[4]` | Pre-evaluated argument values | +| `blockLocals` | Block's local scope (if block activation) | +| `call` / `savedCall` | Call introspection objects | +| `controlFlow` | Union of per-primitive state (for/while/if/foreach/etc.) | + +### Frame State Machine + +The evaluator has 24 states organized by function: + +**Core evaluation:** `START` → `LOOKUP_SLOT` → `EVAL_ARGS` → `ACTIVATE` → `CONTINUE_CHAIN` → `RETURN` + +**Control flow (each primitive has its own states):** +- **if:** `IF_EVAL_CONDITION` → `IF_CONVERT_BOOLEAN` → `IF_EVAL_BRANCH` +- **while:** `WHILE_EVAL_CONDITION` → `WHILE_CHECK_CONDITION` → `WHILE_DECIDE` → `WHILE_EVAL_BODY` +- **for:** `FOR_EVAL_SETUP` → `FOR_EVAL_BODY` → `FOR_AFTER_BODY` +- **loop:** `LOOP_EVAL_BODY` → `LOOP_AFTER_BODY` +- **foreach:** `FOREACH_EVAL_BODY` → `FOREACH_AFTER_BODY` +- **callcc:** `CALLCC_EVAL_BLOCK` +- **coroutines:** `CORO_WAIT_CHILD`, `CORO_YIELDED` +- **do/doString/doFile:** `DO_EVAL`, `DO_WAIT` + +### No C Stack Manipulation + +The entire design avoids platform-specific stack tricks: + +- No `setjmp`/`longjmp` — errors set `state->errorRaised = 1` and return normally. The eval loop unwinds frames. +- No `ucontext` or fibers — coroutines save/restore a frame pointer. +- No assembly — continuations copy frame chains on the heap. + +This makes the VM fully portable to any platform with a C99 compiler. + +--- + +## New Language Features + +### First-Class Continuations + +`callcc(block)` captures the current execution state into a Continuation object: + +```io +result := callcc(block(escape, + list(1, 2, 3) foreach(v, + if(v == 3, escape invoke("found it")) + ) + "not found" +)) +result println // => "found it" +``` + +**Continuation API:** + +| Method | Description | +|--------|-------------| +| `invoke(value)` | Restore captured frames, return value at callcc site | +| `copy` | Deep-copy the frame chain (enables multi-shot use) | +| `isInvoked` | Returns true if this continuation has been invoked | +| `frameCount` | Number of captured frames | +| `frameStates` | List of state names per frame | +| `frameMessages` | List of current messages per frame | +| `asMap` / `fromMap` | Serialize/deserialize continuation state | + +Continuations are one-shot by default. Use `copy` to create a fresh continuation for multi-shot patterns (generators, backtracking). See `docs/IoContinuationsExamples.md` for detailed examples. + +### Frame Introspection + +Live execution frames are exposed to Io code for debugging and metaprogramming: + +```io +f := Coroutine currentCoroutine currentFrame +while(f != nil, + f description println + f = f parent +) +``` + +**EvalFrame methods:** `message`, `target`, `locals`, `state`, `parent`, `result`, `depth`, `call`, `blockLocals`, `description` + +### Resumable Exceptions + +`signal` is the resumable counterpart to `raise`. A handler installed with `withHandler` can inspect an exception and supply a replacement value, resuming execution at the signal site: + +```io +result := withHandler(Exception, + block(e, resume, resume invoke(42)), + Exception signal("need value") + 1 +) +result println // => 43 +``` + +**How it works:** + +- `withHandler(proto, handler, body)` pushes a handler entry onto the current coroutine's `handlerStack`, evaluates the body, then pops the handler. +- `signal(error)` clones the exception, walks `handlerStack` from the current coroutine up through `parentCoroutine` (matching by `isKindOf`), and calls the first matching handler as a regular function. The handler's return value becomes signal's return value. +- `raise(error)` is unchanged — non-resumable, unwinds frames via `errorRaised`. +- If `signal` finds no matching handler, it falls back to `raise`. + +**Implementation:** Entirely in Io (`libs/iovm/io/Exception.io`), no C changes. The body is evaluated directly via `doMessage` rather than in a child coroutine, which avoids a known interaction between continuation capture and nested C eval loops. Handler lookup walks the `parentCoroutine` chain, so handlers installed outside a `try` are visible inside it. See `docs/IoContinuationsExamples.md` for detailed examples. + +### Portable Coroutines + +Coroutines work by saving and restoring the frame pointer — no C stack switching: + +```io +o := Object clone +o work := method( + for(i, 1, 5, i println; yield) +) +o @@work +for(i, 1, 5, yield) +``` + +A suspended coroutine's entire state is a single pointer to its saved frame chain. Switching coroutines is O(1). + +--- + +## Performance Optimizations + +### Object Pooling + +Four pools eliminate allocation overhead in hot paths: + +| Pool | Size | What's Reused | +|------|------|---------------| +| Frame pool | 256 | GC-managed EvalFrame objects | +| Block locals pool | 8 | PHash-allocated locals for block activation | +| Call pool | 8 | IoCallData-allocated Call objects | +| Number data freelist | 512 | IoObjectData allocations for Numbers | + +All pooled objects are GC-marked through `IoCoroutine_mark()` to prevent premature collection. + +### Inline Argument Buffer + +95% of method calls have 4 or fewer arguments. These use a stack-allocated `inlineArgs[4]` buffer instead of heap-allocating an argument array. + +### Monomorphic Inline Cache + +Each IoMessage has a one-entry cache for slot lookups: + +```c +if (tag matches && slotVersion matches && no local shadow) + → return cached value (skip proto chain walk) +``` + +The cache stores `(tag, slotValue, context, version)` and only caches proto-chain hits. A local-slot shadow guard prevents stale results when different objects of the same type have overriding local slots (e.g., `false.isTrue` shadowing `Object.isTrue`). + +### Special Form Detection + +Each CFunction that needs lazy argument evaluation carries an `isLazyArgs` flag, set at VM init time. This includes control flow primitives (`if`, `while`, `for`, `loop`, `callcc`), block constructors (`method`, `block`), iteration (`foreach`, `foreachSlot`), and others. Since Io's `getSlot` returns the same CFunction object, aliases automatically inherit the flag (e.g., `false.elseif := Object getSlot("if")`). The result is cached per-message-site for fast subsequent lookups. + +### Cached Literal Fast Paths + +When a control flow body is a single cached literal (nil, number, or string), the evaluator skips frame allocation entirely and uses the cached value directly. For-loops with literal bodies (`for(i, 1, 1000000, nil)`) run as tight C loops. + +### Tail Call Optimization + +Two complementary mechanisms keep frame stacks flat: + +1. **Direct TCO:** When a Block call is the last message in a block body, `activateBlockTCO_()` reuses the current frame instead of pushing a new one. + +2. **TCO through if:** When `if()` is the last message in a chain, the selected branch evaluates in-place without a child frame. This enables idiomatic Io recursion: + +```io +factorial := method(n, acc, + if(n <= 1, acc, factorial(n - 1, acc * n)) +) +factorial(100000, 1) // no stack overflow +``` + +### Boolean Singleton Fast Path + +`if` and `while` conditions that are `true`, `false`, or `nil` skip the `asBoolean` frame push — the singleton is used directly. + +### Number Cache + +Pre-allocated Number objects for the range [-10, 1024] eliminate allocation for most loop counters and arithmetic. + +--- + +## Hybrid Reference Counting (Optional) + +An optional RC layer (disabled by default, enable with `#define COLLECTOR_USE_REFCOUNT 1` in `CollectorMarker.h`) promptly reclaims short-lived objects. The existing mark/sweep GC remains as backup for cycles. + +When enabled, for-loop counter Numbers are reclaimed immediately via RC drain, keeping the freed list populated and avoiding `calloc`. This gives a 1.5x speedup on `for(i, 1, 1000000, i + 1)` at the cost of ~7% regression on method-heavy workloads from the refcount increment on every `IOREF`. + +--- + +## Benchmarks + +All benchmarks on macOS, Release build. Times are best-of-3 wall clock. + +### Stackless vs Master (Recursive Evaluator) + +| Benchmark | Master | Stackless | Speedup | +|-----------|--------|-----------|---------| +| `for(i, 1, 1M, nil)` | 0.32s | 0.08s | **4.0x** | +| `for(i, 1, 1M, i+1)` | 0.69s | 0.42s | **1.6x** | +| `x = x + 1` (500K) | 0.41s | 0.29s | **1.4x** | +| 500K `method(y, y+1)` | 0.74s | 0.28s | **2.6x** | +| `while(i < 1M)` | 0.70s | 0.64s | **1.1x** | +| `fib(30)` recursive | 3.10s | 1.77s | **1.8x** | +| List ops (100K) | 1.74s | 1.14s | **1.5x** | +| Test suite (239 tests) | 0.93s | 0.83s | **1.1x** | + +Stackless is faster on every benchmark. Method calls and tight for-loops benefit most from the iterative eval loop, frame pooling, and inline caches. Recursive workloads like `fib(30)` benefit from reduced frame allocation overhead. + +### With Optional RC Enabled + +| Benchmark | Without RC | With RC | Change | +|-----------|-----------|---------|--------| +| `for(i, 1, 1M, i+1)` | 0.42s | 0.35s | 1.2x faster | +| 500K `method(y, y+1)` | 0.28s | 0.32s | 12% slower | +| `fib(30)` | 1.77s | 1.96s | 11% slower | + +RC is a targeted optimization for allocation-heavy for-loops. It trades ~10% overhead on general workloads for prompt reclamation of loop temporaries. + +--- + +## Test Results + +- **30/30** C tests (TCO, continuations, exceptions, coroutines, `?` operator, `asMap`) +- **249/249** Io tests via `run.io` (230 original + 9 EvalFrame introspection + 10 resumable exception tests) +- SwitchTest: 6 pre-existing failures (same on master, not in run.io suite) + +--- + +## Key Files + +| File | Purpose | +|------|---------| +| `libs/iovm/source/IoState_iterative.c` | Iterative eval loop, state machine, control flow | +| `libs/iovm/source/IoEvalFrame.h` / `.c` | Frame structure, state enum, introspection methods | +| `libs/iovm/source/IoObject_flow.c` | Control flow primitives (if, while, for, loop, etc.) | +| `libs/iovm/source/IoContinuation.h` / `.c` | Continuation capture, invoke, copy, serialization | +| `libs/iovm/source/IoCoroutine.c` | Frame-based coroutine switching | +| `libs/iovm/source/IoState_eval.c` | Entry points (doCString, runCLI) | +| `libs/iovm/source/IoState_inline.h` | Inline helpers, pre-eval arg access | +| `libs/iovm/source/IoState.h` | VM state, pools, cached symbols | +| `libs/iovm/tests/correctness/EvalFrameTest.io` | Frame introspection tests | +| `libs/iovm/tests/correctness/ResumableExceptionTest.io` | Resumable exception tests | +| `libs/garbagecollector/source/Collector_inline.h` | RC increment/decrement (optional) | +| `agents/C_STACK_ELIMINATION_PLAN.md` | Architecture design document | +| `agents/CONTINUATIONS_TODO.md` | Phase tracker and implementation notes | diff --git a/extras/win32vc10/io/iovm/iovm.vcxproj b/extras/win32vc10/io/iovm/iovm.vcxproj index 9196138af..2da83cb4a 100755 --- a/extras/win32vc10/io/iovm/iovm.vcxproj +++ b/extras/win32vc10/io/iovm/iovm.vcxproj @@ -314,14 +314,14 @@ xcopy /Y /Q "$(SolutionDir)..\..\..\libs\iovm\source\*.h" "$(OutDir)headers\" - - - - - + + + + + - + @@ -339,9 +339,9 @@ xcopy /Y /Q "$(SolutionDir)..\..\..\libs\iovm\source\*.h" "$(OutDir)headers\" - - - + + + diff --git a/extras/win32vc10/io/iovm/iovm.vcxproj.filters b/extras/win32vc10/io/iovm/iovm.vcxproj.filters index ddf71002d..f354ce0b1 100755 --- a/extras/win32vc10/io/iovm/iovm.vcxproj.filters +++ b/extras/win32vc10/io/iovm/iovm.vcxproj.filters @@ -307,19 +307,19 @@ - + io - + io - + io - + io - + io @@ -328,7 +328,7 @@ io - + io @@ -382,13 +382,13 @@ io - + io - + io - + io diff --git a/libs/CMakeLists.txt b/libs/CMakeLists.txt index fadd0ac29..fab09ca06 100644 --- a/libs/CMakeLists.txt +++ b/libs/CMakeLists.txt @@ -13,14 +13,14 @@ set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/_build/lib) # Our Io source files to be "compiled" into a C source file. #file(GLOB IO_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/iovm/io/*.io") set(IO_SRCS - ${CMAKE_CURRENT_SOURCE_DIR}/io/A0_List.io - ${CMAKE_CURRENT_SOURCE_DIR}/io/A1_OperatorTable.io - ${CMAKE_CURRENT_SOURCE_DIR}/io/A2_Object.io - ${CMAKE_CURRENT_SOURCE_DIR}/io/A3_List.io - ${CMAKE_CURRENT_SOURCE_DIR}/io/A4_Exception.io + ${CMAKE_CURRENT_SOURCE_DIR}/io/List_bootstrap.io + ${CMAKE_CURRENT_SOURCE_DIR}/io/OperatorTable.io + ${CMAKE_CURRENT_SOURCE_DIR}/io/Object.io + ${CMAKE_CURRENT_SOURCE_DIR}/io/List.io + ${CMAKE_CURRENT_SOURCE_DIR}/io/Exception.io ${CMAKE_CURRENT_SOURCE_DIR}/io/Actor.io ${CMAKE_CURRENT_SOURCE_DIR}/io/AddonLoader.io - ${CMAKE_CURRENT_SOURCE_DIR}/io/B_Sequence.io + ${CMAKE_CURRENT_SOURCE_DIR}/io/Sequence.io ${CMAKE_CURRENT_SOURCE_DIR}/io/Block.io ${CMAKE_CURRENT_SOURCE_DIR}/io/CFunction.io ${CMAKE_CURRENT_SOURCE_DIR}/io/Date.io @@ -39,31 +39,22 @@ set(IO_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/io/System.io ${CMAKE_CURRENT_SOURCE_DIR}/io/UnitTest.io ${CMAKE_CURRENT_SOURCE_DIR}/io/Vector.io - ${CMAKE_CURRENT_SOURCE_DIR}/io/Y_Path.io - ${CMAKE_CURRENT_SOURCE_DIR}/io/Z_CLI.io - ${CMAKE_CURRENT_SOURCE_DIR}/io/Z_Importer.io + ${CMAKE_CURRENT_SOURCE_DIR}/io/Path.io + ${CMAKE_CURRENT_SOURCE_DIR}/io/CLI.io + ${CMAKE_CURRENT_SOURCE_DIR}/io/Importer.io ) -# Hackery for CMake's horrible ASM support -if(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Windows") - # Always include asm.S for all architectures - set(ASM_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/coroutine/source/asm.S) - set_source_files_properties(${ASM_SOURCES} PROPERTIES LANGUAGE C) -endif() - # Object files from every lib. Used to create iovmall static library. -file(GLOB COROUTINE_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/coroutine/source/*.c") file(GLOB BASEKIT_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/basekit/source/*.c") file(GLOB GARBAGECOLLECTOR_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/garbagecollector/source/*.c") file(GLOB IOVM_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/iovm/source/*.c") +list(REMOVE_ITEM IOVM_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/iovm/source/IoState_iterative_fast.c") list(APPEND IOVM_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/../deps/parson/parson.c) # Marvelous flags, likely compiler dependent. #add_definitions(-DBUILDING_IOVM_DLL)# -DINSTALL_PREFIX="${CMAKE_INSTALL_PREFIX}") set(IOVMALL_STATIC_SRCS - ${COROUTINE_SRCS} - ${ASM_SOURCES} ${BASEKIT_SRCS} ${GARBAGECOLLECTOR_SRCS} ${IOVM_SRCS} @@ -83,7 +74,6 @@ include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/../deps/parson ${CMAKE_CURRENT_SOURCE_DIR}/basekit/source ${CMAKE_CURRENT_SOURCE_DIR}/basekit/source/simd_cph/include - ${CMAKE_CURRENT_SOURCE_DIR}/coroutine/source ${CMAKE_CURRENT_SOURCE_DIR}/garbagecollector/source ) @@ -93,11 +83,10 @@ list(APPEND IOVMALL_STATIC_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/iovm/source/IoVMInit # ...And the static library. Refer to IOVMALL_STATIC_SRCS definition # up top. add_library(iovmall_static STATIC ${IOVMALL_STATIC_SRCS}) -add_dependencies(iovmall_static io2c basekit coroutine garbagecollector iovmall) +add_dependencies(iovmall_static io2c basekit garbagecollector iovmall) # Define the subdirectories we can reach from here that we want # to go into and build stuff. -add_subdirectory(coroutine) add_subdirectory(basekit) add_subdirectory(garbagecollector) add_subdirectory(iovm) diff --git a/libs/coroutine/.gitignore b/libs/coroutine/.gitignore deleted file mode 100644 index f19a83b0c..000000000 --- a/libs/coroutine/.gitignore +++ /dev/null @@ -1,86 +0,0 @@ -*.lib -*.dll -*.manifest -*.obj -*.exp -*.ilk -._* -*.pdb -*.hi -*.o -*.so -*.dylib -*.cmd -.mod.c* -*.tmp_versions -*CVS -*RCS -*IoVMCode.c* -*Io*Init.c* -~* -*_darcs -*_build -*_ioCodeProcessed -*errors -*.bak* -*.BAK* -*.orig* -*vssver.scc* -*.swp* -*MT -*\{arch\} -*.arch-ids -*, -*.class* -*.prof* -*.DS_Store* -*.FBCLockFolder -*BitKeeper -*ChangeSet -*.svn -.cvsignore* -.gdb_history* -*Thumbs.db* -.DS_Store -.libs -.deps* -*.a -*.la -*.lo -*.so -*.dylib -*.exe -*.Po -*.Tlo -*.Plo -_objs -_includes -*_libs -*Io.*Code.c* -io2c -*Io.*Init.c* -*pngtest* -*steve.model* -*skipdbtest* -*config.log* -*config.status* -.rej* -*autom4te.cache* -*.cache -*_include -*tags* -./addons/SGML/source/libsgml/Makefile* -./addons/SGML/source/libsgml/examples/Makefile* -./addons/SGML/source/libsgml/src/Makefile* -tools/editlib_test/editlib_test -extras/IoPlayers/MSWindows/ioplayer/ioplayer.ncb -extras/IoPlayers/MSWindows/ioplayer/ioplayer.suo -extras/IoPlayers/MSWindows/ioplayer/ioplayer/Debug -extras/IoPlayers/MSWindows/ioplayer/ioplayer/Release -extras/IoPlayers/MSWindows/ioplayer/ioplayer/ioplayer.vcproj.CUSTOMER2007.Customer.user -extras/IoPlayers/MSWindows/ioplayer/pingme.txt -extras/IoPlayers/MSWindows/ioplayer/smysrv -libs/iovm/docs/docs.txt -addons/*/docs/docs.txt -*.mode1 -*.pbxuser diff --git a/libs/coroutine/CMakeLists.txt b/libs/coroutine/CMakeLists.txt deleted file mode 100644 index 446a2184c..000000000 --- a/libs/coroutine/CMakeLists.txt +++ /dev/null @@ -1,47 +0,0 @@ -# Base Io build system -# Written by Jeremy Tregunna -# -# Build the coroutine library. - -# Output our dynamic library to the top-level _build hierarchy -set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/_build/dll) - -# Definitions, love them. -add_definitions("-DBUILDING_CORO_DLL") - -# Check if we're on FreeBSD, if so, use ucontext. -if(${CMAKE_SYSTEM_NAME} MATCHES "FreeBSD") - add_definitions("-DUSE_UCONTEXT") -endif(${CMAKE_SYSTEM_NAME} MATCHES "FreeBSD") - -# Set up Coroutine include dirs, -I args to compiler. -include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../basekit/source) - -# Sources... in all their wonderous glory! -file(GLOB SRCS "source/*.c") -file(GLOB HEADERS "source/*.h") - -# Hackery for CMake's horrible ASM support... but not on Windows -if(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Windows") - set(ASM_SOURCES source/asm.S) - set_source_files_properties(${ASM_SOURCES} PROPERTIES LANGUAGE C) - list(APPEND SRCS ${ASM_SOURCES}) -endif(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Windows") - -# Now build the shared library -add_library(coroutine SHARED ${SRCS}) -set_target_properties(coroutine PROPERTIES PUBLIC_HEADER "${HEADERS}") - -# The following add the install target, so we put libcoroutine.* in -# our install prefix. -if(WIN32) - install(TARGETS coroutine - RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX} - PUBLIC_HEADER DESTINATION include/io - ) -else() - install(TARGETS coroutine - LIBRARY DESTINATION lib - PUBLIC_HEADER DESTINATION include/io - ) -endif(WIN32) diff --git a/libs/coroutine/LICENSE.txt b/libs/coroutine/LICENSE.txt deleted file mode 100644 index 4e0822242..000000000 --- a/libs/coroutine/LICENSE.txt +++ /dev/null @@ -1,16 +0,0 @@ -(This is the 3-clause BSD License) - -Copyright (c) 2002, 2003 Steve Dekorte -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - diff --git a/libs/coroutine/depends b/libs/coroutine/depends deleted file mode 100644 index a7ad6c46f..000000000 --- a/libs/coroutine/depends +++ /dev/null @@ -1 +0,0 @@ -basekit \ No newline at end of file diff --git a/libs/coroutine/docs/index.html b/libs/coroutine/docs/index.html deleted file mode 100755 index 8f62b3040..000000000 --- a/libs/coroutine/docs/index.html +++ /dev/null @@ -1,89 +0,0 @@ - - - diff --git a/libs/coroutine/samples/Makefile b/libs/coroutine/samples/Makefile deleted file mode 100644 index a130136a4..000000000 --- a/libs/coroutine/samples/Makefile +++ /dev/null @@ -1,3 +0,0 @@ -twoCoroTest: twoCoroTest.c - $(CC) -I../../basekit/_build/headers -I../_build/headers -L../_build/lib -o $@ $< -lcoroutine - diff --git a/libs/coroutine/samples/twoCoroTest.c b/libs/coroutine/samples/twoCoroTest.c deleted file mode 100644 index 03ba53ea8..000000000 --- a/libs/coroutine/samples/twoCoroTest.c +++ /dev/null @@ -1,41 +0,0 @@ -#include "Coro.h" -#include - -Coro *firstCoro, *secondCoro; - -void secondTask(void *context) { - int num = 0; - - printf("secondTask created with value %d\n", *(int *)context); - - while (1) { - printf("secondTask: %d %d\n", (int)Coro_bytesLeftOnStack(secondCoro), - num++); - Coro_switchTo_(secondCoro, firstCoro); - } -} - -void firstTask(void *context) { - int value = 2; - int num = 0; - - printf("firstTask created with value %d\n", *(int *)context); - secondCoro = Coro_new(); - Coro_startCoro_(firstCoro, secondCoro, (void *)&value, secondTask); - - while (1) { - printf("firstTask: %d %d\n", (int)Coro_bytesLeftOnStack(firstCoro), - num++); - Coro_switchTo_(firstCoro, secondCoro); - } -} - -int main() { - Coro *mainCoro = Coro_new(); - int value = 1; - - Coro_initializeMainCoro(mainCoro); - - firstCoro = Coro_new(); - Coro_startCoro_(mainCoro, firstCoro, (void *)&value, firstTask); -} diff --git a/libs/coroutine/source/386-ucontext.h b/libs/coroutine/source/386-ucontext.h deleted file mode 100644 index ae48c6f73..000000000 --- a/libs/coroutine/source/386-ucontext.h +++ /dev/null @@ -1,120 +0,0 @@ -#define setcontext(u) setmcontext(&(u)->uc_mcontext) -#define getcontext(u) getmcontext(&(u)->uc_mcontext) -typedef struct mcontext mcontext_t; -typedef struct ucontext ucontext_t; - -extern int swapcontext(ucontext_t *, const ucontext_t *); -extern void makecontext(ucontext_t *, void (*)(void), int, ...); -extern int getmcontext(mcontext_t *); -extern void setmcontext(const mcontext_t *); - -/*- - * Copyright (c) 1999 Marcel Moolenaar - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer - * in this position and unchanged. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. The name of the author may not be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * $FreeBSD: src/sys/sys/ucontext.h,v 1.4 1999/10/11 20:33:17 luoqi Exp $ - */ - -/* #include */ - -/*- - * Copyright (c) 1999 Marcel Moolenaar - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer - * in this position and unchanged. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. The name of the author may not be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * $FreeBSD: src/sys/i386/include/ucontext.h,v 1.4 1999/10/11 20:33:09 luoqi Exp - * $ - */ - -struct mcontext { - /* - * The first 20 fields must match the definition of - * sigcontext. So that we can support sigcontext - * and ucontext_t at the same time. - */ - int mc_onstack; /* XXX - sigcontext compat. */ - int mc_gs; - int mc_fs; - int mc_es; - int mc_ds; - int mc_edi; - int mc_esi; - int mc_ebp; - int mc_isp; - int mc_ebx; - int mc_edx; - int mc_ecx; - int mc_eax; - int mc_trapno; - int mc_err; - int mc_eip; - int mc_cs; - int mc_eflags; - int mc_esp; /* machine state */ - int mc_ss; - - int mc_fpregs[28]; /* env87 + fpacc87 + u_long */ - int __spare__[17]; -}; - -struct ucontext { - /* - * Keep the order of the first two fields. Also, - * keep them the first two fields in the structure. - * This way we can have a union with struct - * sigcontext and ucontext_t. This allows us to - * support them both at the same time. - * note: the union is not defined, though. - */ - sigset_t uc_sigmask; - mcontext_t uc_mcontext; - - struct __ucontext *uc_link; - stack_t uc_stack; - int __spare__[8]; -}; diff --git a/libs/coroutine/source/Coro.c b/libs/coroutine/source/Coro.c deleted file mode 100644 index e06d13bfa..000000000 --- a/libs/coroutine/source/Coro.c +++ /dev/null @@ -1,821 +0,0 @@ -/* - Credits - - Originally based on Edgar Toernig's Minimalistic cooperative - multitasking http://www.goron.de/~froese/ reorg by Steve Dekorte and Chis - Double Symbian and Cygwin support by Chis Double Linux/PCC, Linux/Opteron, Irix - and FreeBSD/Alpha, ucontext support by Austin Kurahone FreeBSD/Intel support by - Faried Nawaz Mingw support by Pit Capitain Visual C support by Daniel Vollmer - Solaris support by Manpreet Singh - Fibers support by Jonas Eschenburg - Ucontext arg support by Olivier Ansaldi - Ucontext x86-64 support by James Burgess and Jonathan Wright - Russ Cox for the newer portable ucontext implementions. - Mac OS X support by Jorge Acereda - Guessed setjmp support (Android/Mac OS X/others?) by Jorge Acereda - - Notes - - This is the system dependent coro code. - Setup a jmp_buf so when we longjmp, it will invoke 'func' using 'stack'. - Important: 'func' must not return! - - Usually done by setting the program counter and stack pointer of a new, - empty stack. If you're adding a new platform, look in the setjmp.h for PC and - SP members of the stack structure - - If you don't see those members, Kentaro suggests writting a simple - test app that calls setjmp and dumps out the contents of the jmp_buf. - (The PC and SP should be in jmp_buf->__jmpbuf). - - Using something like GDB to be able to peek into register contents right - before the setjmp occurs would be helpful also. - */ - -#include "Base.h" -#include "Coro.h" -#include -#include -#include -#include -#include "taskimpl.h" - -#ifdef USE_VALGRIND -#include -#define STACK_REGISTER(coro) \ - { \ - Coro *c = (coro); \ - c->valgrindStackId = VALGRIND_STACK_REGISTER( \ - c->stack, (char *)c->stack + c->requestedStackSize); \ - } - -#define STACK_DEREGISTER(coro) \ - VALGRIND_STACK_DEREGISTER((coro)->valgrindStackId) - -#else -#define STACK_REGISTER(coro) -#define STACK_DEREGISTER(coro) -#endif - -typedef struct CallbackBlock { - void *context; - CoroStartCallback *func; -#ifdef USE_FIBERS - Coro *associatedCoro; -#endif -} CallbackBlock; - -#if !defined(__arm64__) && !defined(__aarch64__) -Coro *Coro_new(void) { - Coro *self = (Coro *)io_calloc(1, sizeof(Coro)); - self->requestedStackSize = CORO_DEFAULT_STACK_SIZE; - self->allocatedStackSize = 0; - -#ifdef USE_FIBERS - self->fiber = NULL; -#else - self->stack = NULL; -#endif - return self; -} - -#ifndef USE_FIBERS -void Coro_allocStackIfNeeded(Coro *self) { - if (self->stack && self->requestedStackSize < self->allocatedStackSize) { - io_free(self->stack); - self->stack = NULL; - self->requestedStackSize = 0; - } - - if (!self->stack) { - self->stack = (void *)io_calloc(1, self->requestedStackSize + 16); - self->allocatedStackSize = self->requestedStackSize; - // printf("Coro_%p allocating stack size %i\n", (void *)self, - // self->requestedStackSize); - STACK_REGISTER(self); - } -} -#endif - -void Coro_free(Coro *self) { -#ifdef USE_FIBERS - // If this coro has a fiber, delete it. - // Don't delete the main fiber. We don't want to commit suicide. - if (self->fiber && !self->isMain) { - DeleteFiber(self->fiber); - } -#else - STACK_DEREGISTER(self); - if (self->stack) { - io_free(self->stack); - } -#endif - - // printf("Coro_%p io_free\n", (void *)self); - - io_free(self); -} - -// stack - -void *Coro_stack(Coro *self) { return self->stack; } - -size_t Coro_stackSize(Coro *self) { return self->requestedStackSize; } - -void Coro_setStackSize_(Coro *self, size_t sizeInBytes) { - self->requestedStackSize = sizeInBytes; - // self->stack = (void *)io_realloc(self->stack, sizeInBytes); - // printf("Coro_%p io_reallocating stack size %i\n", (void *)self, - // sizeInBytes); -} - -#if __GNUC__ >= 4 -ptrdiff_t *Coro_CurrentStackPointer(void) __attribute__((noinline)); -#endif - -ptrdiff_t *Coro_CurrentStackPointer(void) { - ptrdiff_t a; - ptrdiff_t *b = &a; // to avoid compiler warning about unused variables - // ptrdiff_t *c = a ^ (b ^ a); // to avoid - return b; -} - -size_t Coro_bytesLeftOnStack(Coro *self) { - unsigned char dummy; - ptrdiff_t p1 = (ptrdiff_t)(&dummy); - ptrdiff_t p2 = (ptrdiff_t)Coro_CurrentStackPointer(); - int stackMovesUp = p2 > p1; - ptrdiff_t start = ((ptrdiff_t)self->stack); - ptrdiff_t end = start + self->requestedStackSize; - - if (stackMovesUp) // like PPC - { - return end - p1; - } else // like x86 - { - return p1 - start; - } -} - -int Coro_stackSpaceAlmostGone(Coro *self) { - return Coro_bytesLeftOnStack(self) < CORO_STACK_SIZE_MIN; -} - -void Coro_initializeMainCoro(Coro *self) { - self->isMain = 1; -#ifdef USE_FIBERS - // We must convert the current thread into a fiber if it hasn't already been - // done. - if ((LPVOID)0x1e00 == GetCurrentFiber()) // value returned when not a fiber - { - // Make this thread a fiber and set its data field to the main coro's - // address - ConvertThreadToFiber(self); - } - // Make the main coro represent the current fiber - self->fiber = GetCurrentFiber(); -#endif -} - -void Coro_startCoro_(Coro *self, Coro *other, void *context, - CoroStartCallback *callback) { - CallbackBlock sblock; - CallbackBlock *block = &sblock; - // CallbackBlock *block = malloc(sizeof(CallbackBlock)); // memory leak - block->context = context; - block->func = callback; - -#ifdef USE_FIBERS - block->associatedCoro = other; -#else - Coro_allocStackIfNeeded(other); -#endif - Coro_setup(other, block); - Coro_switchTo_(self, other); -} - -/* -void Coro_startCoro_(Coro *self, Coro *other, void *context, CoroStartCallback -*callback) -{ - globalCallbackBlock.context = context; - globalCallbackBlock.func = callback; -#ifndef USE_FIBERS - Coro_allocStackIfNeeded(other); -#endif - Coro_setup(other, &globalCallbackBlock); - Coro_switchTo_(self, other); -} -*/ - -#if defined(USE_UCONTEXT) && defined(__x86_64__) -void Coro_StartWithArg(unsigned int hiArg, unsigned int loArg) { - CallbackBlock *block = - (CallbackBlock *)(((long long)hiArg << 32) | (long long)loArg); - (block->func)(block->context); - printf("Scheduler error: returned from coro start function\n"); - exit(-1); -} - -/* -void Coro_Start(void) -{ - CallbackBlock block = globalCallbackBlock; - unsigned int hiArg = (unsigned int)(((long long)&block) >> 32); - unsigned int loArg = (unsigned int)(((long long)&block) & 0xFFFFFFFF); - Coro_StartWithArg(hiArg, loArg); -} -*/ -#else -void Coro_StartWithArg(CallbackBlock *block) { -#ifdef USE_FIBERS - MEMORY_BASIC_INFORMATION meminfo; - if (block->associatedCoro->fiber != GetCurrentFiber()) - abort(); - // Set the start of the stack for future comparaison. According to - // http://msdn.microsoft.com/en-us/library/ms686774(VS.85).aspx, - // some part of the stack is reserved for running an handler if - // the fiber exhaust its stack, but we have no way of retrieving - // this information (SetThreadStackGuarantee() is not supported - // on WindowsXP), so we have to assume that it is the default - // 64kB. - - // Look at the descriptors of the meminfo structure, which is - // conveniently located on the stack we are interested into. - VirtualQuery(&meminfo, &meminfo, sizeof meminfo); - block->associatedCoro->stack = (char *)meminfo.AllocationBase + 64 * 1024; -#endif - (block->func)(block->context); - printf("Scheduler error: returned from coro start function\n"); - exit(-1); -} - -static CallbackBlock globalCallbackBlock; - -void Coro_Start(void) { - CallbackBlock block = globalCallbackBlock; - Coro_StartWithArg(&block); -} - -#endif - -// -------------------------------------------------------------------- - -void Coro_UnsupportedPlatformError(void) { - printf( - "Io Scheduler error: no Coro_setupJmpbuf entry for this platform\n."); - exit(1); -} - -void Coro_switchTo_(Coro *self, Coro *next) { -#if defined(__SYMBIAN32__) - ProcessUIEvent(); -#elif defined(USE_FIBERS) - SwitchToFiber(next->fiber); -#elif defined(USE_UCONTEXT) - swapcontext(&self->env, &next->env); -#elif defined(USE_SETJMP) || defined USE_GUESSED_SETJMP - if (setjmp(self->env) == 0) { - longjmp(next->env, 1); - } -#endif -} - -// ---- setup ------------------------------------------ -#if defined(USE_SETJMP) && defined(__APPLE__) - -void Coro_setup(Coro *self, void *arg) { - uintptr_t stackend = Coro_stackSize(self) + (uintptr_t)Coro_stack(self); - uintptr_t start = (uintptr_t)Coro_Start; - /* since ucontext seems to be broken on amd64 */ - globalCallbackBlock.context = ((CallbackBlock *)arg)->context; - globalCallbackBlock.func = ((CallbackBlock *)arg)->func; - - setjmp(self->env); - -#if defined(__x86_64__) - *(uintptr_t *)(self->env + 4) = stackend - 8; - *(uintptr_t *)(self->env + 14) = start; -#else - *(uintptr_t *)(self->env + 9) = stackend - 4; - *(uintptr_t *)(self->env + 12) = start; -#endif -} - -#elif defined(USE_SETJMP) && defined(__linux__) -void Coro_setup(Coro *self, void *arg) { - uintptr_t stackend = Coro_stackSize(self) + (uintptr_t)Coro_stack(self); - uintptr_t start = (uintptr_t)Coro_Start; - /* since ucontext seems to be broken on amd64 */ - globalCallbackBlock.context = ((CallbackBlock *)arg)->context; - globalCallbackBlock.func = ((CallbackBlock *)arg)->func; - - setjmp(self->env); - /* This is probably not nice in that it deals directly with - * something with __ in front of it. - * - * Anyhow, Coro.h makes the member env of a struct Coro a - * jmp_buf. A jmp_buf, as defined in the amd64 setjmp.h - * is an array of one struct that wraps the actual __jmp_buf type - * which is the array of longs (on a 64 bit machine) that - * the programmer below expected. This struct begins with - * the __jmp_buf array of longs, so I think it was supposed - * to work like he originally had it, but for some reason - * it didn't. I don't know why. - * - Bryce Schroeder, 16 December 2006 - * - * Explaination of `magic' numbers: 6 is the stack pointer - * (RSP, the 64 bit equivalent of ESP), 7 is the program counter. - * This information came from this file on my Gentoo linux - * amd64 computer: - * /usr/include/gento-multilib/amd64/bits/setjmp.h - * Which was ultimatly included from setjmp.h in /usr/include. */ - - self->env[0].__jmpbuf[6] = ((unsigned long)(Coro_stack(self))); - self->env[0].__jmpbuf[7] = ((long)Coro_Start); -} - -#elif defined(HAS_UCONTEXT_ON_PRE_SOLARIS_10) - -typedef void (*makecontext_func)(void); - -void Coro_setup(Coro *self, void *arg) { - ucontext_t *ucp = (ucontext_t *)&self->env; - - getcontext(ucp); - - ucp->uc_stack.ss_sp = Coro_stack(self) + Coro_stackSize(self) - 8; - ucp->uc_stack.ss_size = Coro_stackSize(self); - ucp->uc_stack.ss_flags = 0; - ucp->uc_link = NULL; - - makecontext(ucp, (makecontext_func)Coro_StartWithArg, 1, arg); -} - -#elif defined(USE_UCONTEXT) - -typedef void (*makecontext_func)(void); - -void Coro_setup(Coro *self, void *arg) { - ucontext_t *ucp = (ucontext_t *)&self->env; - - getcontext(ucp); - - ucp->uc_stack.ss_sp = Coro_stack(self); - ucp->uc_stack.ss_size = Coro_stackSize(self); -#if !defined(__APPLE__) - ucp->uc_stack.ss_flags = 0; - ucp->uc_link = NULL; -#endif - -#if defined(__x86_64__) - unsigned int hiArg = (unsigned int)((long long)arg >> 32); - unsigned int loArg = (unsigned int)((long long)arg & 0xFFFFFFFF); - makecontext(ucp, (makecontext_func)Coro_StartWithArg, 2, hiArg, loArg); -#else - makecontext(ucp, (makecontext_func)Coro_StartWithArg, 1, arg); -#endif -} - -#elif defined(USE_FIBERS) - -void Coro_setup(Coro *self, void *arg) { - // If this coro was recycled and already has a fiber, delete it. - // Don't delete the main fiber. We don't want to commit suicide. - - if (self->fiber && !self->isMain) { - DeleteFiber(self->fiber); - } - - self->fiber = - CreateFiber(Coro_stackSize(self), - (LPFIBER_START_ROUTINE)Coro_StartWithArg, (LPVOID)arg); - if (!self->fiber) { - DWORD err = GetLastError(); - exit(err); - } -} - -#elif defined(__CYGWIN__) - -#define buf (self->env) - -void Coro_setup(Coro *self, void *arg) { - setjmp(buf); - buf[7] = (long)(Coro_stack(self) + Coro_stackSize(self) - 16); - buf[8] = (long)Coro_Start; - globalCallbackBlock.context = ((CallbackBlock *)arg)->context; - globalCallbackBlock.func = ((CallbackBlock *)arg)->func; -} - -#elif defined(__SYMBIAN32__) - -void Coro_setup(Coro *self, void *arg) { - /* - setjmp/longjmp is flakey under Symbian. - If the setjmp is done inside the call then a crash occurs. - Inlining it here solves the problem - */ - - setjmp(self->env); - self->env[0] = 0; - self->env[1] = 0; - self->env[2] = 0; - self->env[3] = - (unsigned long)(Coro_stack(self)) + Coro_stackSize(self) - 64; - self->env[9] = (long)Coro_Start; - self->env[8] = self->env[3] + 32; -} - -#elif defined(_BSD_PPC_SETJMP_H_) - -#define buf (self->env) -#define setjmp _setjmp -#define longjmp _longjmp - -void Coro_setup(Coro *self, void *arg) { - size_t *sp = (size_t *)(((intptr_t)Coro_stack(self) + Coro_stackSize(self) - - 64 + 15) & - ~15); - - setjmp(buf); - - // printf("self = %p\n", self); - // printf("sp = %p\n", sp); - buf[0] = (long)sp; - buf[21] = (long)Coro_Start; - globalCallbackBlock.context = ((CallbackBlock *)arg)->context; - globalCallbackBlock.func = ((CallbackBlock *)arg)->func; - // sp[-4] = (size_t)self; // for G5 10.3 - // sp[-6] = (size_t)self; // for G4 10.4 - - // printf("self = %p\n", (void *)self); - // printf("sp = %p\n", sp); -} - -/* -void Coro_setup(Coro *self, void *arg) -{ - size_t *sp = (size_t *)(((intptr_t)Coro_stack(self) - + Coro_stackSize(self) - 64 + -15) & ~15); - - setjmp(buf); - - //printf("self = %p\n", self); - //printf("sp = %p\n", sp); - buf[0] = (long)sp; - buf[21] = (long)Coro_Start; - //sp[-4] = (size_t)self; // for G5 10.3 - //sp[-6] = (size_t)self; // for G4 10.4 - - //printf("self = %p\n", (void *)self); - //printf("sp = %p\n", sp); -} -*/ - -#elif defined(__DragonFly__) - -#define buf (self->env) - -void Coro_setup(Coro *self, void *arg) { - void *stack = Coro_stack(self); - size_t stacksize = Coro_stackSize(self); - void *func = (void *)Coro_Start; - - setjmp(buf); - - buf->_jb[2] = (long)(stack + stacksize); - buf->_jb[0] = (long)func; - return; -} - -#elif defined(__arm__) -// contributed by Peter van Hardenberg - -#define buf (self->env) - -void Coro_setup(Coro *self, void *arg) { - setjmp(buf); - buf[8] = (int)Coro_stack(self) + (int)Coro_stackSize(self) - 16; - buf[9] = (int)Coro_Start; -} - -// NLB ... guessing. Check data from here -// http://state-threads.sourceforge.net/docs/notes.html -// It appears 2 is the SP and probably 0 is the PC just like on DragonFly -// /usr/src/lib/libc/arch/i386/gen/setjmp.S appears to confirm this... -// but I'm segfaulting to an address of 0000, so obviously I'm a but wrong -// somewhere... -// Current problem looks like "func" is off by 143 bytes/whatever, and I'm -// landing in the -// wrong spot after the return! So my structure is right, but somehow I have -// the wrong *func - -#elif defined(__OpenBSD__) && defined(__i386__) || \ - defined(__NetBSD__) && defined(__i386__) - -#define buf (self->env) - -void Coro_setup(Coro *self, void *arg) { - void *stack = Coro_stack(self); - size_t stacksize = Coro_stackSize(self); - void *func = (void *)Coro_Start; - - setjmp(buf); - - buf[2] = (long)(stack + stacksize); - buf[0] = (long)Coro_Start; - // it would seem this needs to have some value?? - globalCallbackBlock.context = ((CallbackBlock *)arg)->context; - globalCallbackBlock.func = ((CallbackBlock *)arg)->func; - return; -} - -#else - -/* Use POSIX ucontext by default */ -void Coro_setup(Coro *self, void *arg) { - uintptr_t stackend = Coro_stackSize(self) + (uintptr_t)Coro_stack(self); - uintptr_t start = (uintptr_t)Coro_Start; - /* since ucontext seems to be broken on amd64 */ - globalCallbackBlock.context = ((CallbackBlock *)arg)->context; - globalCallbackBlock.func = ((CallbackBlock *)arg)->func; - setjmp(self->env); -end : { - uintptr_t i; - uintptr_t *sav = (uintptr_t *)self->env; - size_t sz = sizeof(self->env) / sizeof(sav[0]); - - // Try to guess PC index - i = sz; - while (i--) - if (sav[i] == (uintptr_t) && end) - break; - assert(i < sz); - sav[i] = start; - - // Try to guess SP index - i = sz; - while (i--) - if (64 > (-sav[i] + (uintptr_t)&i)) - break; - assert(i < sz); - sav[i] = stackend - sizeof(uintptr_t) * 2 - 128; -} -} - -#endif - -#endif -// old code - -/* - // APPLE coros are handled by PortableUContext now -#elif defined(_BSD_PPC_SETJMP_H_) - -#define buf (self->env) -#define setjmp _setjmp -#define longjmp _longjmp - - void Coro_setup(Coro *self, void *arg) - { - size_t *sp = (size_t *)(((intptr_t)Coro_stack(self) + -Coro_stackSize(self) - 64 + 15) & ~15); - - setjmp(buf); - - //printf("self = %p\n", self); - //printf("sp = %p\n", sp); - buf[0] = (int)sp; - buf[21] = (int)Coro_Start; - //sp[-4] = (size_t)self; // for G5 10.3 - //sp[-6] = (size_t)self; // for G4 10.4 - - //printf("self = %p\n", (void *)self); - //printf("sp = %p\n", sp); - } - -#elif defined(_BSD_I386_SETJMP_H) - -#define buf (self->env) - - void Coro_setup(Coro *self, void *arg) - { - size_t *sp = (size_t *)((intptr_t)Coro_stack(self) + -Coro_stackSize(self)); - - setjmp(buf); - - buf[9] = (int)(sp); // esp - buf[12] = (int)Coro_Start; // eip - //buf[8] = 0; // ebp - } - */ - -/* Solaris supports ucontext - so we don't need this stuff anymore - -void Coro_setup(Coro *self, void *arg) -{ - // this bit goes before the setjmp call - // Solaris 9 Sparc with GCC -#if defined(__SVR4) && defined (__sun) -#if defined(_JBLEN) && (_JBLEN == 12) && defined(__sparc) -#if defined(_LP64) || defined(_I32LPx) -#define JBTYPE long - JBTYPE x; -#else -#define JBTYPE int - JBTYPE x; - asm("ta 3"); // flush register window -#endif - -#define SUN_STACK_END_INDEX 1 -#define SUN_PROGRAM_COUNTER 2 -#define SUN_STACK_START_INDEX 3 - - // Solaris 9 i386 with GCC -#elif defined(_JBLEN) && (_JBLEN == 10) && defined(__i386) -#if defined(_LP64) || defined(_I32LPx) -#define JBTYPE long - JBTYPE x; -#else -#define JBTYPE int - JBTYPE x; -#endif -#define SUN_PROGRAM_COUNTER 5 -#define SUN_STACK_START_INDEX 3 -#define SUN_STACK_END_INDEX 4 -#endif -#endif - */ - -/* Irix supports ucontext - so we don't need this stuff anymore - -#elif defined(sgi) && defined(_IRIX4_SIGJBLEN) // Irix/SGI - -void Coro_setup(Coro *self, void *arg) -{ - setjmp(buf); - buf[JB_SP] = (__uint64_t)((char *)stack + stacksize - 8); - buf[JB_PC] = (__uint64_t)Coro_Start; -} -*/ - -/* Linux supports ucontext - so we don't need this stuff anymore - -#elif defined(linux) -// Various flavors of Linux. -#if defined(JB_GPR1) -// Linux/PPC -buf->__jmpbuf[JB_GPR1] = ((int) stack + stacksize - 64 + 15) & ~15; -buf->__jmpbuf[JB_LR] = (int) Coro_Start; -return; - -#elif defined(JB_RBX) -// Linux/Opteron -buf->__jmpbuf[JB_RSP] = (long int )stack + stacksize; -buf->__jmpbuf[JB_PC] = Coro_Start; -return; - -#elif defined(JB_SP) - -// Linux/x86 with glibc2 -buf->__jmpbuf[JB_SP] = (int)stack + stacksize; -buf->__jmpbuf[JB_PC] = (int)Coro_StartWithArg; -// Push the argument on the stack (stack grows downwards) -// note: stack is stacksize + 16 bytes long -((int *)stack)[stacksize/sizeof(int) + 1] = (int)self; -return; - -#elif defined(_I386_JMP_BUF_H) -// x86-linux with libc5 -buf->__sp = (int)stack + stacksize; -buf->__pc = Coro_Start; -return; - -#elif defined(__JMP_BUF_SP) -// arm-linux on the sharp zauras -buf->__jmpbuf[__JMP_BUF_SP] = (int)stack + stacksize; -buf->__jmpbuf[__JMP_BUF_SP+1] = (int)Coro_Start; -return; - -#else - -*/ - -/* Windows supports fibers - so we don't need this stuff anymore - -#elif defined(__MINGW32__) - -void Coro_setup(Coro *self, void *arg) -{ - setjmp(buf); - buf[4] = (int)((unsigned char *)stack + stacksize - 16); // esp - buf[5] = (int)Coro_Start; // eip -} - -#elif defined(_MSC_VER) - -void Coro_setup(Coro *self, void *arg) -{ - setjmp(buf); - // win32 visual c - // should this be the same as __MINGW32__? - buf[4] = (int)((unsigned char *)stack + stacksize - 16); // esp - buf[5] = (int)Coro_Start; // eip -} -*/ - -/* FreeBSD supports ucontext - so we don't need this stuff anymore - -#elif defined(__FreeBSD__) && defined(__i386__) -// FreeBSD. -#if defined(_JBLEN) && (_JBLEN == 81) -// FreeBSD/Alpha -buf->_jb[2] = (long)Coro_Start; // sc_pc -buf->_jb[26+4] = (long)Coro_Start; // sc_regs[R_RA] -buf->_jb[27+4] = (long)Coro_Start; // sc_regs[R_T12] -buf->_jb[30+4] = (long)(stack + stacksize); // sc_regs[R_SP] -return; - -#elif defined(_JBLEN) -// FreeBSD on IA32 -buf->_jb[2] = (long)(stack + stacksize); -buf->_jb[0] = (long)Coro_Start; -return; - -#else -Coro_UnsupportedPlatformError(); -#endif -*/ - -/* NetBSD supports ucontext - so we don't need this stuff anymore - -#elif defined(__NetBSD__) && defined(__i386__) - -void Coro_setup(Coro *self, void *arg) -{ - setjmp(buf); -#if defined(_JB_ATTRIBUTES) - // NetBSD i386 - buf[2] = (long)(stack + stacksize); - buf[0] = (long)Coro_Start; -#else - Coro_UnsupportedPlatformError(); -#endif -} -*/ - -/* Sun supports ucontext - so we don't need this stuff anymore - -// Solaris supports ucontext - so we don't need this stuff anymore - -void Coro_setup(Coro *self, void *arg) -{ - // this bit goes before the setjmp call - // Solaris 9 Sparc with GCC -#if defined(__SVR4) && defined (__sun) -#if defined(_JBLEN) && (_JBLEN == 12) && defined(__sparc) -#if defined(_LP64) || defined(_I32LPx) -#define JBTYPE long - JBTYPE x; -#else -#define JBTYPE int - JBTYPE x; - asm("ta 3"); // flush register window -#endif - -#define SUN_STACK_END_INDEX 1 -#define SUN_PROGRAM_COUNTER 2 -#define SUN_STACK_START_INDEX 3 - - // Solaris 9 i386 with GCC -#elif defined(_JBLEN) && (_JBLEN == 10) && defined(__i386) -#if defined(_LP64) || defined(_I32LPx) -#define JBTYPE long - JBTYPE x; -#else -#define JBTYPE int - JBTYPE x; -#endif -#define SUN_PROGRAM_COUNTER 5 -#define SUN_STACK_START_INDEX 3 -#define SUN_STACK_END_INDEX 4 -#endif -#endif - -#elif defined(__SVR4) && defined(__sun) - // Solaris -#if defined(SUN_PROGRAM_COUNTER) - // SunOS 9 - buf[SUN_PROGRAM_COUNTER] = -(JBTYPE)Coro_Start; - - x = (JBTYPE)stack; - while ((x % 8) != 0) x --; // align on -an even boundary buf[SUN_STACK_START_INDEX] = (JBTYPE)x; x = -(JBTYPE)((JBTYPE)stack-stacksize / 2 + 15); while ((x % 8) != 0) x ++; // align -on an even boundary buf[SUN_STACK_END_INDEX] = (JBTYPE)x; - - */ diff --git a/libs/coroutine/source/Coro.h b/libs/coroutine/source/Coro.h deleted file mode 100644 index d517438e2..000000000 --- a/libs/coroutine/source/Coro.h +++ /dev/null @@ -1,136 +0,0 @@ -#ifndef CORO_DEFINED -#define CORO_DEFINED 1 - -#if defined(__linux__) -#define HAS_UCONTEXT 1 -#endif - -#if defined(__APPLE__) && defined(__i386__) -#define USE_UCONTEXT 1 -#endif - -#if defined(__APPLE__) && (defined(__arm64__) || defined(__aarch64__)) -#include "arm64-ucontext.h" -#define USE_UCONTEXT 1 -#endif - -#if defined(__FreeBSD__) -#define HAS_UCONTEXT 1 -#endif - -#if defined(__OpenBSD__) -#undef HAS_UCONTEXT -#undef USE_UCONTEXT -#undef USE_FIBERS -#endif - -#if defined(__amd64__) && !defined(__x86_64__) -#define __x86_64__ 1 -#endif - -#include "Common.h" -//#include "PortableUContext.h" -#include "taskimpl.h" - -#if defined(__SYMBIAN32__) -#define CORO_STACK_SIZE 8192 -#define CORO_STACK_SIZE_MIN 1024 -#else -//#define CORO_DEFAULT_STACK_SIZE (65536/2) -//#define CORO_DEFAULT_STACK_SIZE (65536*4) - -// 128k needed on PPC due to parser -#define CORO_DEFAULT_STACK_SIZE (128 * 1024) -//#define CORO_DEFAULT_STACK_SIZE (256*1024) -#define CORO_STACK_SIZE_MIN 8192 -#endif - -#if defined(WIN32) -#if defined(BUILDING_CORO_DLL) || defined(BUILDING_IOVMALL_DLL) -#define CORO_API __declspec(dllexport) -#else -#define CORO_API __declspec(dllimport) -#endif - -#else -#define CORO_API -#endif - -// Pick which coro implementation to use -// The make file can set -DUSE_FIBERS, -DUSE_UCONTEXT or -DUSE_SETJMP to force -// this choice. -#if !defined(USE_FIBERS) && !defined(USE_UCONTEXT) && !defined(USE_SETJMP) - -#if defined(WIN32) && defined(HAS_FIBERS) -#define USE_FIBERS -#elif defined(HAS_UCONTEXT) -//#elif defined(HAS_UCONTEXT) && !defined(__x86_64__) -#if !defined(USE_UCONTEXT) -#define USE_UCONTEXT -#endif -#else -#define USE_SETJMP -#endif - -#endif - -#if defined(USE_FIBERS) -#define CORO_IMPLEMENTATION "fibers" -#elif defined(USE_UCONTEXT) -#include -#define CORO_IMPLEMENTATION "ucontext" -#elif defined(USE_SETJMP) -#include -#define CORO_IMPLEMENTATION "setjmp" -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -typedef struct Coro Coro; - -struct Coro { - size_t requestedStackSize; - size_t allocatedStackSize; - void *stack; - -#ifdef USE_VALGRIND - unsigned int valgrindStackId; -#endif - -#if defined(USE_FIBERS) - void *fiber; -#elif defined(USE_UCONTEXT) - ucontext_t env; -#elif defined(USE_SETJMP) - jmp_buf env; -#endif - - unsigned char isMain; -}; - -CORO_API Coro *Coro_new(void); -CORO_API void Coro_free(Coro *self); - -// stack - -CORO_API void *Coro_stack(Coro *self); -CORO_API size_t Coro_stackSize(Coro *self); -CORO_API void Coro_setStackSize_(Coro *self, size_t sizeInBytes); -CORO_API size_t Coro_bytesLeftOnStack(Coro *self); -CORO_API int Coro_stackSpaceAlmostGone(Coro *self); - -CORO_API void Coro_initializeMainCoro(Coro *self); - -typedef void(CoroStartCallback)(void *); - -CORO_API void Coro_startCoro_(Coro *self, Coro *other, void *context, - CoroStartCallback *callback); -CORO_API void Coro_switchTo_(Coro *self, Coro *next); -CORO_API void Coro_setup(Coro *self, void *arg); // private - -#ifdef __cplusplus -} -#endif -#endif diff --git a/libs/coroutine/source/Coro_arm64.c b/libs/coroutine/source/Coro_arm64.c deleted file mode 100644 index 64472da69..000000000 --- a/libs/coroutine/source/Coro_arm64.c +++ /dev/null @@ -1,136 +0,0 @@ -#if defined(__arm64__) || defined(__aarch64__) -#include "Coro.h" -#include -#include -#include - -// External functions implemented in asm.S -extern int coro_arm64_getcontext(void *context) __asm("coro_arm64_getcontext"); -extern int coro_arm64_setcontext(void *context) __asm("coro_arm64_setcontext"); - -// Custom context implementation for ARM64 -typedef struct { - unsigned long x19_x20[2]; // x19, x20 - unsigned long x21_x22[2]; // x21, x22 - unsigned long x23_x24[2]; // x23, x24 - unsigned long x25_x26[2]; // x25, x26 - unsigned long x27_x28[2]; // x27, x28 - unsigned long fp_lr[2]; // x29 (fp), x30 (lr) - unsigned long sp; // stack pointer -} arm64_context_t; - -typedef struct CallbackBlock { - void *context; - CoroStartCallback *func; -} CallbackBlock; - -static CallbackBlock globalCallbackBlock; - -static void Coro_StartWithArg(void) { - //fprintf(stderr, "Coro_StartWithArg called\n"); - CallbackBlock *block = &globalCallbackBlock; - //fprintf(stderr, "Function pointer: %p\n", block->func); - block->func(block->context); - fprintf(stderr, "Scheduler error: returned from coro start function\n"); - exit(-1); -} - -void Coro_free(Coro *self) { - if (self->stack) { - free(self->stack); - } - free(self); -} - -void Coro_initializeMainCoro(Coro *self) { - self->isMain = 1; -} - -Coro *Coro_new(void) { - Coro *c = (Coro *)calloc(1, sizeof(Coro)); - if (c) { - c->requestedStackSize = CORO_DEFAULT_STACK_SIZE; - c->allocatedStackSize = 0; - c->stack = NULL; - } - return c; -} - -void Coro_setStackSize_(Coro *self, size_t size) { - self->requestedStackSize = size; -} - -static void Coro_allocStackIfNeeded(Coro *self) { - if (self->stack && self->requestedStackSize < self->allocatedStackSize) { - free(self->stack); - self->stack = NULL; - self->allocatedStackSize = 0; - } - if (!self->stack) { - // Make sure stack is 16-byte aligned for ARM64 - self->stack = calloc(1, self->requestedStackSize + 16); - self->allocatedStackSize = self->requestedStackSize; - } -} - -// This function initializes the context with a new stack and entry point -void Coro_setup(Coro *self, void *arg) { - arm64_context_t *context = (arm64_context_t *)&self->env; - memset(context, 0, sizeof(*context)); - - Coro_allocStackIfNeeded(self); - - // Initialize stack pointer to top of stack (ARM64 full descending stack) - unsigned long sp = (unsigned long)self->stack + self->allocatedStackSize - 16; - // Ensure 16-byte alignment - sp &= ~15UL; - - // Store stack pointer in context - context->sp = sp; - - // Store entry point in link register (x30) - context->fp_lr[1] = (unsigned long)Coro_StartWithArg; -} - -int Coro_stackSpaceAlmostGone(Coro *self) { - arm64_context_t *context = (arm64_context_t *)&self->env; - unsigned long sp = context->sp; - unsigned long stack_base = (unsigned long)self->stack; - - // Check if we have less than 1KB of stack space left - return (sp - stack_base) < 1024; -} - -size_t Coro_bytesLeftOnStack(Coro *self) { - arm64_context_t *context = (arm64_context_t *)&self->env; - unsigned long sp = context->sp; - unsigned long stack_base = (unsigned long)self->stack; - - // Return number of bytes between stack pointer and stack base - if (sp > stack_base) { - return sp - stack_base; - } - - // Fallback if stack info not available - return 1024 * 1024; -} - -void Coro_startCoro_(Coro *self, Coro *other, void *context, CoroStartCallback *callback) { - globalCallbackBlock.context = context; - globalCallbackBlock.func = callback; - Coro_setup(other, &globalCallbackBlock); - Coro_switchTo_(self, other); -} - -void Coro_switchTo_(Coro *self, Coro *next) { - // Get the context pointers - arm64_context_t *from_context = (arm64_context_t *)&self->env; - arm64_context_t *to_context = (arm64_context_t *)&next->env; - - // Save current context, if successful (returns 0), then restore the next context - if (coro_arm64_getcontext((void*)from_context) == 0) { - coro_arm64_setcontext((void*)to_context); - } - -} -#endif diff --git a/libs/coroutine/source/amd64-ucontext.h b/libs/coroutine/source/amd64-ucontext.h deleted file mode 100644 index 15853ae21..000000000 --- a/libs/coroutine/source/amd64-ucontext.h +++ /dev/null @@ -1,136 +0,0 @@ -#define setcontext(u) setmcontext(&(u)->uc_mcontext) -#define getcontext(u) getmcontext(&(u)->uc_mcontext) -typedef struct mcontext mcontext_t; -typedef struct ucontext ucontext_t; - -extern int swapcontext(ucontext_t *, const ucontext_t *); -extern void makecontext(ucontext_t *, void (*)(void), int, ...); -extern int getmcontext(mcontext_t *); -extern void setmcontext(const mcontext_t *); - -/*- - * Copyright (c) 1999 Marcel Moolenaar - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer - * in this position and unchanged. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. The name of the author may not be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * $FreeBSD: src/sys/sys/ucontext.h,v 1.4 1999/10/11 20:33:17 luoqi Exp $ - */ - -/* #include */ - -/*- - * Copyright (c) 1999 Marcel Moolenaar - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer - * in this position and unchanged. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. The name of the author may not be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * $FreeBSD: src/sys/i386/include/ucontext.h,v 1.4 1999/10/11 20:33:09 luoqi Exp - * $ - */ - -struct mcontext { - /* - * The first 20 fields must match the definition of - * sigcontext. So that we can support sigcontext - * and ucontext_t at the same time. - */ - long mc_onstack; /* XXX - sigcontext compat. */ - long mc_rdi; /* machine state (struct trapframe) */ - long mc_rsi; - long mc_rdx; - long mc_rcx; - long mc_r8; - long mc_r9; - long mc_rax; - long mc_rbx; - long mc_rbp; - long mc_r10; - long mc_r11; - long mc_r12; - long mc_r13; - long mc_r14; - long mc_r15; - long mc_trapno; - long mc_addr; - long mc_flags; - long mc_err; - long mc_rip; - long mc_cs; - long mc_rflags; - long mc_rsp; - long mc_ss; - - long mc_len; /* sizeof(mcontext_t) */ -#define _MC_FPFMT_NODEV 0x10000 /* device not present or configured */ -#define _MC_FPFMT_XMM 0x10002 - long mc_fpformat; -#define _MC_FPOWNED_NONE 0x20000 /* FP state not used */ -#define _MC_FPOWNED_FPU 0x20001 /* FP state came from FPU */ -#define _MC_FPOWNED_PCB 0x20002 /* FP state came from PCB */ - long mc_ownedfp; - /* - * See for the internals of mc_fpstate[]. - */ - long mc_fpstate[64]; - long mc_spare[8]; -}; - -struct ucontext { - /* - * Keep the order of the first two fields. Also, - * keep them the first two fields in the structure. - * This way we can have a union with struct - * sigcontext and ucontext_t. This allows us to - * support them both at the same time. - * note: the union is not defined, though. - */ - sigset_t uc_sigmask; - mcontext_t uc_mcontext; - - struct __ucontext *uc_link; - stack_t uc_stack; - int __spare__[8]; -}; diff --git a/libs/coroutine/source/arm64-ucontext.h b/libs/coroutine/source/arm64-ucontext.h deleted file mode 100644 index e9e4f9b18..000000000 --- a/libs/coroutine/source/arm64-ucontext.h +++ /dev/null @@ -1,14 +0,0 @@ -// arm64-ucontext.h: Apple Silicon (ARM64) macOS context switching using ucontext -#ifndef ARM64_UCONTEXT_H -#define ARM64_UCONTEXT_H - -// Required for ucontext on modern macOS -#ifndef _XOPEN_SOURCE -#define _XOPEN_SOURCE 700 -#endif - -#include - -typedef ucontext_t arm64_ucontext_t; - -#endif // ARM64_UCONTEXT_H diff --git a/libs/coroutine/source/asm.S b/libs/coroutine/source/asm.S deleted file mode 100644 index 74126de7c..000000000 --- a/libs/coroutine/source/asm.S +++ /dev/null @@ -1,297 +0,0 @@ -/* Copyright (c) 2005-2006 Russ Cox, MIT; see COPYRIGHT */ - -#if defined(__FreeBSD__) && defined(__i386__) && __FreeBSD__ < 5 -#define NEEDX86CONTEXT 1 -#define SET setmcontext -#define GET getmcontext -#endif - -#if defined(__OpenBSD__) && defined(__i386__) -#define NEEDX86CONTEXT 1 -#define SET setmcontext -#define GET getmcontext -#endif - -#if defined(__APPLE__) -#if defined(__i386__) -#define NEEDX86CONTEXT 1 -#define SET _setmcontext -#define GET _getmcontext -#elif defined(__x86_64__) -#define NEEDAMD64CONTEXT 1 -#define SET _setmcontext -#define GET _getmcontext -#elif defined(__arm64__) || defined(__aarch64__) || defined(__ARM64_ARCH_8__) -#define NEEDARM64CONTEXT 1 -#define SET coro_arm64_setcontext -#define GET coro_arm64_getcontext -#else -#define NEEDPOWERCONTEXT 1 -#define SET __setmcontext -#define GET __getmcontext -#endif -#endif - -#if defined(__linux__) && defined(__arm__) -#define NEEDARMCONTEXT 1 -#define SET setmcontext -#define GET getmcontext -#endif - -#if defined(__linux__) && defined(__ELF__) -.section .note.GNU-stack,"",%progbits -#endif - -#ifdef NEEDX86CONTEXT -.globl SET -SET: - movl 4(%esp), %eax - - movl 8(%eax), %fs - movl 12(%eax), %es - movl 16(%eax), %ds - movl 76(%eax), %ss - movl 20(%eax), %edi - movl 24(%eax), %esi - movl 28(%eax), %ebp - movl 36(%eax), %ebx - movl 40(%eax), %edx - movl 44(%eax), %ecx - - movl 72(%eax), %esp - pushl 60(%eax) /* new %eip */ - movl 48(%eax), %eax - ret - -.globl GET -GET: - movl 4(%esp), %eax - - movl %fs, 8(%eax) - movl %es, 12(%eax) - movl %ds, 16(%eax) - movl %ss, 76(%eax) - movl %edi, 20(%eax) - movl %esi, 24(%eax) - movl %ebp, 28(%eax) - movl %ebx, 36(%eax) - movl %edx, 40(%eax) - movl %ecx, 44(%eax) - - movl $1, 48(%eax) /* %eax */ - movl (%esp), %ecx /* %eip */ - movl %ecx, 60(%eax) - leal 4(%esp), %ecx /* %esp */ - movl %ecx, 72(%eax) - - movl 44(%eax), %ecx /* restore %ecx */ - movl $0, %eax - ret -#endif - -#ifdef NEEDAMD64CONTEXT -.globl SET -SET: - movq 16(%rdi), %rsi - movq 24(%rdi), %rdx - movq 32(%rdi), %rcx - movq 40(%rdi), %r8 - movq 48(%rdi), %r9 - movq 56(%rdi), %rax - movq 64(%rdi), %rbx - movq 72(%rdi), %rbp - movq 80(%rdi), %r10 - movq 88(%rdi), %r11 - movq 96(%rdi), %r12 - movq 104(%rdi), %r13 - movq 112(%rdi), %r14 - movq 120(%rdi), %r15 - movq 184(%rdi), %rsp - pushq 160(%rdi) /* new %eip */ - movq 8(%rdi), %rdi - ret - -.globl GET -GET: - movq %rdi, 8(%rdi) - movq %rsi, 16(%rdi) - movq %rdx, 24(%rdi) - movq %rcx, 32(%rdi) - movq %r8, 40(%rdi) - movq %r9, 48(%rdi) - movq $1, 56(%rdi) /* %rax */ - movq %rbx, 64(%rdi) - movq %rbp, 72(%rdi) - movq %r10, 80(%rdi) - movq %r11, 88(%rdi) - movq %r12, 96(%rdi) - movq %r13, 104(%rdi) - movq %r14, 112(%rdi) - movq %r15, 120(%rdi) - - movq (%rsp), %rcx /* %rip */ - movq %rcx, 160(%rdi) - leaq 8(%rsp), %rcx /* %rsp */ - movq %rcx, 184(%rdi) - - movq 32(%rdi), %rcx /* restore %rcx */ - movq $0, %rax - ret -#endif - -#ifdef NEEDPOWERCONTEXT -/* get FPR and VR use flags with sc 0x7FF3 */ -/* get vsave with mfspr reg, 256 */ - -.text -.align 2 - -.globl GET -GET: /* xxx: instruction scheduling */ - mflr r0 - mfcr r5 - mfctr r6 - mfxer r7 - stw r0, 0*4(r3) - stw r5, 1*4(r3) - stw r6, 2*4(r3) - stw r7, 3*4(r3) - - stw r1, 4*4(r3) - stw r2, 5*4(r3) - li r5, 1 /* return value for setmcontext */ - stw r5, 6*4(r3) - - stw r13, (0+7)*4(r3) /* callee-save GPRs */ - stw r14, (1+7)*4(r3) /* xxx: block move */ - stw r15, (2+7)*4(r3) - stw r16, (3+7)*4(r3) - stw r17, (4+7)*4(r3) - stw r18, (5+7)*4(r3) - stw r19, (6+7)*4(r3) - stw r20, (7+7)*4(r3) - stw r21, (8+7)*4(r3) - stw r22, (9+7)*4(r3) - stw r23, (10+7)*4(r3) - stw r24, (11+7)*4(r3) - stw r25, (12+7)*4(r3) - stw r26, (13+7)*4(r3) - stw r27, (14+7)*4(r3) - stw r28, (15+7)*4(r3) - stw r29, (16+7)*4(r3) - stw r30, (17+7)*4(r3) - stw r31, (18+7)*4(r3) - - li r3, 0 /* return */ - blr - -.globl SET -SET: - lwz r13, (0+7)*4(r3) /* callee-save GPRs */ - lwz r14, (1+7)*4(r3) /* xxx: block move */ - lwz r15, (2+7)*4(r3) - lwz r16, (3+7)*4(r3) - lwz r17, (4+7)*4(r3) - lwz r18, (5+7)*4(r3) - lwz r19, (6+7)*4(r3) - lwz r20, (7+7)*4(r3) - lwz r21, (8+7)*4(r3) - lwz r22, (9+7)*4(r3) - lwz r23, (10+7)*4(r3) - lwz r24, (11+7)*4(r3) - lwz r25, (12+7)*4(r3) - lwz r26, (13+7)*4(r3) - lwz r27, (14+7)*4(r3) - lwz r28, (15+7)*4(r3) - lwz r29, (16+7)*4(r3) - lwz r30, (17+7)*4(r3) - lwz r31, (18+7)*4(r3) - - lwz r1, 4*4(r3) - lwz r2, 5*4(r3) - - lwz r0, 0*4(r3) - mtlr r0 - lwz r0, 1*4(r3) - mtcr r0 /* mtcrf 0xFF, r0 */ - lwz r0, 2*4(r3) - mtctr r0 - lwz r0, 3*4(r3) - mtxer r0 - - lwz r3, 6*4(r3) - blr -#endif - -#ifdef NEEDARMCONTEXT -.globl GET -GET: - str r1, [r0,#4] - str r2, [r0,#8] - str r3, [r0,#12] - str r4, [r0,#16] - str r5, [r0,#20] - str r6, [r0,#24] - str r7, [r0,#28] - str r8, [r0,#32] - str r9, [r0,#36] - str r10, [r0,#40] - str r11, [r0,#44] - str r12, [r0,#48] - str r13, [r0,#52] - str r14, [r0,#56] - /* store 1 as r0-to-restore */ - mov r1, #1 - str r1, [r0] - /* return 0 */ - mov r0, #0 - mov pc, lr - -.globl SET -SET: - ldr r1, [r0,#4] - ldr r2, [r0,#8] - ldr r3, [r0,#12] - ldr r4, [r0,#16] - ldr r5, [r0,#20] - ldr r6, [r0,#24] - ldr r7, [r0,#28] - ldr r8, [r0,#32] - ldr r9, [r0,#36] - ldr r10, [r0,#40] - ldr r11, [r0,#44] - ldr r12, [r0,#48] - ldr r13, [r0,#52] - ldr r14, [r0,#56] - ldr r0, [r0] - mov pc, lr -#endif - -#ifdef NEEDARM64CONTEXT -.globl coro_arm64_getcontext -coro_arm64_getcontext: - // x0 contains the context pointer - stp x19, x20, [x0, #0] - stp x21, x22, [x0, #16] - stp x23, x24, [x0, #32] - stp x25, x26, [x0, #48] - stp x27, x28, [x0, #64] - stp x29, x30, [x0, #80] - mov x2, sp - str x2, [x0, #96] - mov x0, #0 - ret - -.globl coro_arm64_setcontext -coro_arm64_setcontext: - ldp x19, x20, [x0, #0] - ldp x21, x22, [x0, #16] - ldp x23, x24, [x0, #32] - ldp x25, x26, [x0, #48] - ldp x27, x28, [x0, #64] - ldp x29, x30, [x0, #80] - ldr x2, [x0, #96] - mov sp, x2 - mov x0, #1 - ret -#endif diff --git a/libs/coroutine/source/context.c b/libs/coroutine/source/context.c deleted file mode 100644 index 1ea7dc35b..000000000 --- a/libs/coroutine/source/context.c +++ /dev/null @@ -1,130 +0,0 @@ -/* Copyright (c) 2005-2006 Russ Cox, MIT; see COPYRIGHT */ - -#include -#include "taskimpl.h" - -#if defined(__APPLE__) -#if defined(__i386__) -#define NEEDX86MAKECONTEXT -#define NEEDSWAPCONTEXT -#elif defined(__x86_64__) -#define NEEDAMD64MAKECONTEXT -#define NEEDSWAPCONTEXT -#elif defined(__arm64__) || defined(__aarch64__) -#define NEEDARM64MAKECONTEXT -#define NEEDSWAPCONTEXT -#else -#define NEEDPOWERMAKECONTEXT -#define NEEDSWAPCONTEXT -#endif -#endif - -#if defined(__FreeBSD__) && defined(__i386__) && __FreeBSD__ < 5 -#define NEEDX86MAKECONTEXT -#define NEEDSWAPCONTEXT -#endif - -#if defined(__OpenBSD__) && defined(__i386__) -#define NEEDX86MAKECONTEXT -#define NEEDSWAPCONTEXT -#endif - -#if defined(__linux__) && defined(__arm__) -#define NEEDSWAPCONTEXT -#define NEEDARMMAKECONTEXT -#endif - -#ifdef NEEDPOWERMAKECONTEXT -void makecontext(ucontext_t *ucp, void (*func)(void), int argc, ...) { - ulong *sp, *tos; - va_list arg; - - tos = (ulong *)ucp->uc_stack.ss_sp + ucp->uc_stack.ss_size / sizeof(ulong); - sp = tos - 16; - ucp->mc.pc = (long)func; - ucp->mc.sp = (long)sp; - va_start(arg, argc); - ucp->mc.r3 = va_arg(arg, long); - va_end(arg); -} -#endif - -#ifdef NEEDX86MAKECONTEXT -void makecontext(ucontext_t *ucp, void (*func)(void), int argc, ...) { - int *sp; - int i; - va_list arg; - - /* Set sp to end of allocated stack area */ - sp = (int *)ucp->uc_stack.ss_sp + ucp->uc_stack.ss_size / 4; - /* reserve space for argc ints */ - sp -= argc; - sp = (void *)((uintptr_t)sp - (uintptr_t)sp % 16); /* 16-align for OS X */ - - /* Copy arguments into reserved space on stack */ - va_start(arg, argc); - for (i = 0; i < argc; i++) - sp[i] = va_arg(arg, int); - va_end(arg); - - *--sp = 0; /* return address */ - ucp->uc_mcontext.mc_eip = (long)func; - ucp->uc_mcontext.mc_esp = (int)sp; -} -#endif - -#ifdef NEEDAMD64MAKECONTEXT -void makecontext(ucontext_t *ucp, void (*func)(void), int argc, ...) { - long *sp; - va_list va; - - memset(&ucp->uc_mcontext, 0, sizeof ucp->uc_mcontext); - if (argc != 2) { - // printf("%s", "makecontext missing argument\n"); - //*(int*)0 = 0; - exit(-1); - } - va_start(va, argc); - ucp->uc_mcontext.mc_rdi = va_arg(va, int); - ucp->uc_mcontext.mc_rsi = va_arg(va, int); - va_end(va); - sp = (long *)ucp->uc_stack.ss_sp + ucp->uc_stack.ss_size / sizeof(long); - sp -= argc; - sp = (void *)((uintptr_t)sp - (uintptr_t)sp % 16); /* 16-align for OS X */ - *--sp = 0; /* return address */ - ucp->uc_mcontext.mc_rip = (long)func; - ucp->uc_mcontext.mc_rsp = (long)sp; -} -#endif - -#ifdef NEEDARMMAKECONTEXT -void makecontext(ucontext_t *uc, void (*fn)(void), int argc, ...) { - int i, *sp; - va_list arg; - - sp = (int *)uc->uc_stack.ss_sp + uc->uc_stack.ss_size / 4; - va_start(arg, argc); - - if (argc-- > 0) - uc->uc_mcontext.arm_r0 = va_arg(arg, uint); - if (argc-- > 0) - uc->uc_mcontext.arm_r1 = va_arg(arg, uint); - if (argc-- > 0) - uc->uc_mcontext.arm_r2 = va_arg(arg, uint); - if (argc-- > 0) - uc->uc_mcontext.arm_r3 = va_arg(arg, uint); - - va_end(arg); - uc->uc_mcontext.arm_sp = (uint)sp; - uc->uc_mcontext.arm_lr = (uint)fn; -} -#endif - - -#ifdef NEEDSWAPCONTEXT -int swapcontext(ucontext_t *oucp, const ucontext_t *ucp) { - if (getcontext(oucp) == 0) - setcontext(ucp); - return 0; -} -#endif diff --git a/libs/coroutine/source/power-ucontext.h b/libs/coroutine/source/power-ucontext.h deleted file mode 100644 index 3ee1b167d..000000000 --- a/libs/coroutine/source/power-ucontext.h +++ /dev/null @@ -1,36 +0,0 @@ -typedef unsigned long ulong; - -#define setcontext(u) _setmcontext(&(u)->mc) -#define getcontext(u) _getmcontext(&(u)->mc) -typedef struct mcontext mcontext_t; -typedef struct ucontext ucontext_t; -struct mcontext { - ulong pc; /* lr */ - ulong cr; /* mfcr */ - ulong ctr; /* mfcr */ - ulong xer; /* mfcr */ - ulong sp; /* callee saved: r1 */ - ulong toc; /* callee saved: r2 */ - ulong r3; /* first arg to function, return register: r3 */ - ulong gpr[19]; /* callee saved: r13-r31 */ - /* - // XXX: currently do not save vector registers or floating-point state - // ulong pad; - // uvlong fpr[18]; / * callee saved: f14-f31 * / - // ulong vr[4*12]; / * callee saved: v20-v31, 256-bits each * / - */ -}; - -struct ucontext { - struct { - void *ss_sp; - uint ss_size; - } uc_stack; - sigset_t uc_sigmask; - mcontext_t mc; -}; - -void makecontext(ucontext_t *, void (*)(void), int, ...); -int swapcontext(ucontext_t *, const ucontext_t *); -int _getmcontext(mcontext_t *); -void _setmcontext(const mcontext_t *); diff --git a/libs/coroutine/source/taskimpl.h b/libs/coroutine/source/taskimpl.h deleted file mode 100644 index af97014bf..000000000 --- a/libs/coroutine/source/taskimpl.h +++ /dev/null @@ -1,209 +0,0 @@ -#ifndef TASKIMPL_DEFINED -#define TASKIMPL_DEFINED 1 - -#include - -/* Copyright (c) 2005-2006 Russ Cox, MIT; see COPYRIGHT */ - -#if defined __sun__ -#define __EXTENSIONS__ 1 /* SunOS */ -#if defined __SunOS5_6__ || defined __SunOS5_7__ || defined __SunOS5_8__ -/* NOT USING #define __MAKECONTEXT_V2_SOURCE 1 / * SunOS */ -#else -#define __MAKECONTEXT_V2_SOURCE 1 -#endif -#endif - -//#define USE_UCONTEXT 1 - -#if defined __OpenBSD__ -#undef USE_UCONTEXT -#endif - -#if defined __APPLE__ -#include -#if defined MAC_OS_X_VERSION_10_5 -#undef USE_UCONTEXT -#define USE_UCONTEXT 0 -#endif -#endif - -#include -#include -#include -#include -#include - -#ifndef _WIN32 -#include -#include -#include -#ifndef __MINGW32__ -#include -#endif -#include -#include -#if USE_UCONTEXT -#include -#endif -#ifndef __MINGW32__ -#include -#endif -#include -#endif - -#if defined(__linux__) -#include -#endif - -//#include "task.h" - -#ifndef nil -#define nil ((void *)0) -#endif - -#define nelem(x) (sizeof(x) / sizeof((x)[0])) - -/* -#define ulong task_ulong -#define uint task_uint -#define uchar task_uchar -#define ushort task_ushort -#define uvlong task_uvlong -#define vlong task_vlong - -typedef unsigned long ulong; -typedef unsigned int uint; -typedef unsigned char uchar; -typedef unsigned short ushort; -typedef unsigned long long uvlong; -typedef long long vlong; - -#define print task_print -#define fprint task_fprint -#define snprint task_snprint -#define seprint task_seprint -#define vprint task_vprint -#define vfprint task_vfprint -#define vsnprint task_vsnprint -#define vseprint task_vseprint -#define strecpy task_strecpy - -int print(char*, ...); -int fprint(int, char*, ...); -char *snprint(char*, uint, char*, ...); -char *seprint(char*, char*, char*, ...); -int vprint(char*, va_list); -int vfprint(int, char*, va_list); -char *vsnprint(char*, uint, char*, va_list); -char *vseprint(char*, char*, char*, va_list); -char *strecpy(char*, char*, char*); -*/ - -#if defined(__FreeBSD__) && __FreeBSD__ < 5 -extern int getmcontext(mcontext_t *); -extern void setmcontext(const mcontext_t *); -#define setcontext(u) setmcontext(&(u)->uc_mcontext) -#define getcontext(u) getmcontext(&(u)->uc_mcontext) -extern int swapcontext(ucontext_t *, const ucontext_t *); -extern void makecontext(ucontext_t *, void (*)(), int, ...); -#endif - -#if defined(__APPLE__) -#if defined(__i386__) -#define mcontext libthread_mcontext -#define mcontext_t libthread_mcontext_t -#define ucontext libthread_ucontext -#define ucontext_t libthread_ucontext_t -#include "386-ucontext.h" -#elif defined(__x86_64__) -#define mcontext libthread_mcontext -#define mcontext_t libthread_mcontext_t -#define ucontext libthread_ucontext -#define ucontext_t libthread_ucontext_t -#include "amd64-ucontext.h" -#elif defined(__arm64__) || defined(__aarch64__) -#include "arm64-ucontext.h" -#else -#define mcontext libthread_mcontext -#define mcontext_t libthread_mcontext_t -#define ucontext libthread_ucontext -#define ucontext_t libthread_ucontext_t -#include "power-ucontext.h" -#endif -#endif - -#if defined(__OpenBSD__) -#define mcontext libthread_mcontext -#define mcontext_t libthread_mcontext_t -#define ucontext libthread_ucontext -#define ucontext_t libthread_ucontext_t -#if defined __i386__ -#include "386-ucontext.h" -#else -#include "power-ucontext.h" -#endif -extern pid_t rfork_thread(int, void *, int (*)(void *), void *); -#endif - -#if 0 && defined(__sun__) -#define mcontext libthread_mcontext -#define mcontext_t libthread_mcontext_t -#define ucontext libthread_ucontext -#define ucontext_t libthread_ucontext_t -#include "sparc-ucontext.h" -#endif - -#if defined(__arm__) -int getmcontext(mcontext_t *); -void setmcontext(const mcontext_t *); -#define setcontext(u) setmcontext((void *)&((u)->uc_mcontext.arm_r0)) -#define getcontext(u) getmcontext((void *)&((u)->uc_mcontext.arm_r0)) -#endif - -/* -typedef struct Context Context; - -enum -{ - STACK = 8192 -}; - -struct Context -{ - ucontext_t uc; -}; - -struct Task -{ - char name[256]; // offset known to acid - char state[256]; - Task *next; - Task *prev; - Task *allnext; - Task *allprev; - Context context; - uvlong alarmtime; - uint id; - uchar *stk; - uint stksize; - int exiting; - int alltaskslot; - int system; - int ready; - void (*startfn)(void*); - void *startarg; - void *udata; -}; - -void taskready(Task*); -void taskswitch(void); - -void addtask(Tasklist*, Task*); -void deltask(Tasklist*, Task*); - -extern Task *taskrunning; -extern int taskcount; -*/ - -#endif diff --git a/libs/garbagecollector/source/Collector.c b/libs/garbagecollector/source/Collector.c index b5a977132..11eed045a 100644 --- a/libs/garbagecollector/source/Collector.c +++ b/libs/garbagecollector/source/Collector.c @@ -47,6 +47,13 @@ Collector *Collector_new(void) { self->clocksUsed = 0; +#ifdef COLLECTOR_USE_REFCOUNT + self->rcFreeList = NULL; + self->rcFreeCount = 0; + self->rcFreeCapacity = 0; + self->inSweep = 0; +#endif + Collector_check(self); return self; @@ -107,6 +114,11 @@ void Collector_free(Collector *self) { CollectorMarker_free(self->grays); CollectorMarker_free(self->blacks); CollectorMarker_free(self->freed); +#ifdef COLLECTOR_USE_REFCOUNT + if (self->rcFreeList) { + io_free(self->rcFreeList); + } +#endif io_free(self); } @@ -210,6 +222,20 @@ float Collector_allocsPerSweep(Collector *self) { return self->allocsPerSweep; } CollectorMarker *Collector_newMarker(Collector *self) { CollectorMarker *m; BEGIN_TIMER + + // Paths that bypass pushPause/popPause (e.g. IoState_numberWithDouble_) + // never trigger GC via popPause, so dead objects can accumulate without + // bound. Collect here before allocating the new marker to bound memory + // growth. The 50x multiplier amortizes the O(live_set) sweep cost: + // ~1 sweep per 500K allocations, <40ms overhead per cycle. +#ifdef COLLECTOR_USE_NONINCREMENTAL_MARK_SWEEP + if (self->pauseCount == 0 && + self->newMarkerCount > self->allocsPerSweep * 50) { + self->newMarkerCount = 0; + Collector_collect(self); + } +#endif + m = self->freed->next; if (m->color != self->freed->color) { @@ -217,6 +243,9 @@ CollectorMarker *Collector_newMarker(Collector *self) { // printf("new marker\n"); } else { // printf("using recycled marker\n"); +#ifdef COLLECTOR_USE_REFCOUNT + m->refCount = 0; +#endif } self->allocated++; @@ -233,20 +262,6 @@ void Collector_addValue_(Collector *self, void *v) { #ifdef COLLECTOR_USE_NONINCREMENTAL_MARK_SWEEP self->newMarkerCount++; #endif - // pauseCount is never zero here... - /* - if (self->pauseCount == 0) - { - if(self->allocated > self->allocatedSweepLevel) - { - Collector_sweep(self); - } - else if (self->queuedMarks > 1.0) - { - Collector_markPhase(self); - } - } - */ } // collection ------------------------------------------------ @@ -373,7 +388,13 @@ size_t Collector_sweepPhase(Collector *self) { Collector_sendWillFreeCallbacks(self); } +#ifdef COLLECTOR_USE_REFCOUNT + self->inSweep = 1; +#endif freedCount = Collector_freeWhites(self); +#ifdef COLLECTOR_USE_REFCOUNT + self->inSweep = 0; +#endif self->sweepCount++; // printf("whites freed %i\n", (int)freedCount); @@ -440,3 +461,28 @@ char *Collector_colorNameFor_(Collector *self, void *v) { double Collector_timeUsed(Collector *self) { return (double)self->clocksUsed / (double)CLOCKS_PER_SEC; } + +#ifdef COLLECTOR_USE_REFCOUNT + +void Collector_rcEnqueue_(Collector *self, CollectorMarker *m) { + if (self->rcFreeCount >= self->rcFreeCapacity) { + self->rcFreeCapacity = self->rcFreeCapacity ? self->rcFreeCapacity * 2 : 64; + self->rcFreeList = (CollectorMarker **)io_realloc(self->rcFreeList, + self->rcFreeCapacity * sizeof(CollectorMarker *)); + } + self->rcFreeList[self->rcFreeCount++] = m; +} + +void Collector_rcDrainFreeList_(Collector *self) { + while (self->rcFreeCount > 0) { + CollectorMarker *m = self->rcFreeList[--self->rcFreeCount]; + if (m->refCount != 0) continue; + if (m->color == self->freed->color) continue; + if (m->color != self->whites->color) continue; + if (self->freeFunc) self->freeFunc(m); + Collector_makeFree_(self, m); + self->allocated--; + } +} + +#endif diff --git a/libs/garbagecollector/source/Collector.h b/libs/garbagecollector/source/Collector.h index a68030153..365a720e6 100644 --- a/libs/garbagecollector/source/Collector.h +++ b/libs/garbagecollector/source/Collector.h @@ -16,7 +16,7 @@ extern "C" { COLLECTMARKER_FOREACH(self->grays, v, code;); \ COLLECTMARKER_FOREACH(self->blacks, v, code;); -//#define COLLECTOR_RECYCLE_FREED 1 +#define COLLECTOR_RECYCLE_FREED 1 #define COLLECTOR_USE_NONINCREMENTAL_MARK_SWEEP 1 typedef enum { @@ -62,6 +62,13 @@ typedef struct { int newMarkerCount; int allocsPerSweep; #endif + +#ifdef COLLECTOR_USE_REFCOUNT + CollectorMarker **rcFreeList; + size_t rcFreeCount; + size_t rcFreeCapacity; + int inSweep; +#endif } Collector; COLLECTOR_API Collector *Collector_new(void); @@ -149,6 +156,11 @@ COLLECTOR_API void Collector_popPause(Collector *self); COLLECTOR_API int Collector_isPaused(Collector *self); COLLECTOR_API double Collector_timeUsed(Collector *self); +#ifdef COLLECTOR_USE_REFCOUNT +COLLECTOR_API void Collector_rcEnqueue_(Collector *self, CollectorMarker *m); +COLLECTOR_API void Collector_rcDrainFreeList_(Collector *self); +#endif + #include "Collector_inline.h" #ifdef __cplusplus diff --git a/libs/garbagecollector/source/CollectorMarker.h b/libs/garbagecollector/source/CollectorMarker.h index 9e7cab2a2..6fe692fce 100644 --- a/libs/garbagecollector/source/CollectorMarker.h +++ b/libs/garbagecollector/source/CollectorMarker.h @@ -8,6 +8,8 @@ #include "Common.h" #include "List.h" +// #define COLLECTOR_USE_REFCOUNT 1 + #ifdef __cplusplus extern "C" { #endif @@ -27,12 +29,19 @@ typedef void(CollectorDoFunc)(void *); typedef struct CollectorMarker CollectorMarker; +#ifdef COLLECTOR_USE_REFCOUNT +#define COLLECTOR_REFCOUNT_FIELD unsigned int refCount; +#else +#define COLLECTOR_REFCOUNT_FIELD +#endif + #define CollectorMarkerSansPointer \ CollectorMarker *prev; \ CollectorMarker *next; \ unsigned int color : 2; \ unsigned int hash1; \ - unsigned int hash2; + unsigned int hash2; \ + COLLECTOR_REFCOUNT_FIELD /* #if !defined(COLLECTOROBJECTTYPE) diff --git a/libs/garbagecollector/source/Collector_inline.h b/libs/garbagecollector/source/Collector_inline.h index c5d81cac3..8b393bbfe 100644 --- a/libs/garbagecollector/source/Collector_inline.h +++ b/libs/garbagecollector/source/Collector_inline.h @@ -56,9 +56,32 @@ IOINLINE void Collector_makeFreed_(Collector *self, void *v) CollectorMarker_removeAndInsertAfter_(v, self->freed); } */ -#ifdef COLLECTOR_USE_NONINCREMENTAL_MARK_SWEEP +#ifdef COLLECTOR_USE_REFCOUNT + +IOINLINE void Collector_value_addingRefTo_(Collector *self, void *v, + void *ref) { + (void)self; + (void)v; + if (ref) { + ((CollectorMarker *)ref)->refCount++; + } +} + +IOINLINE void Collector_value_removingRefTo_(Collector *self, void *ref) { + if (!ref) return; + CollectorMarker *m = (CollectorMarker *)ref; + if (m->refCount > 0) { + m->refCount--; + if (m->refCount == 0 && !self->inSweep) { + Collector_rcEnqueue_(self, m); + } + } +} + +#elif defined(COLLECTOR_USE_NONINCREMENTAL_MARK_SWEEP) #define Collector_value_addingRefTo_(self, v, ref) +#define Collector_value_removingRefTo_(self, ref) #else @@ -66,8 +89,6 @@ IOINLINE void *Collector_value_addingRefTo_(Collector *self, void *v, void *ref) { if (Collector_markerIsBlack_(self, (CollectorMarker *)v) && Collector_markerIsWhite_(self, (CollectorMarker *)ref)) - // if (self->safeMode || (Collector_markerIsBlack_(self, (CollectorMarker - // *)v) && Collector_markerIsWhite_(self, (CollectorMarker *)ref))) { Collector_makeGray_(self, (CollectorMarker *)ref); } @@ -75,6 +96,8 @@ IOINLINE void *Collector_value_addingRefTo_(Collector *self, void *v, return ref; } +#define Collector_value_removingRefTo_(self, ref) + #endif #undef IO_IN_C_FILE diff --git a/libs/iovm/CMakeLists.txt b/libs/iovm/CMakeLists.txt index d0ad150e7..162b1b323 100644 --- a/libs/iovm/CMakeLists.txt +++ b/libs/iovm/CMakeLists.txt @@ -12,15 +12,15 @@ set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/_build/dll) # Our Io source files to be "compiled" into a C source file. #file(GLOB IO_SRCS "io/*.io") set(IO_SRCS - ${CMAKE_CURRENT_SOURCE_DIR}/io/A0_List.io - ${CMAKE_CURRENT_SOURCE_DIR}/io/A0_Object.io - ${CMAKE_CURRENT_SOURCE_DIR}/io/A1_OperatorTable.io - ${CMAKE_CURRENT_SOURCE_DIR}/io/A2_Object.io - ${CMAKE_CURRENT_SOURCE_DIR}/io/A3_List.io - ${CMAKE_CURRENT_SOURCE_DIR}/io/A4_Exception.io + ${CMAKE_CURRENT_SOURCE_DIR}/io/List_bootstrap.io + ${CMAKE_CURRENT_SOURCE_DIR}/io/Object_bootstrap.io + ${CMAKE_CURRENT_SOURCE_DIR}/io/OperatorTable.io + ${CMAKE_CURRENT_SOURCE_DIR}/io/Object.io + ${CMAKE_CURRENT_SOURCE_DIR}/io/List.io + ${CMAKE_CURRENT_SOURCE_DIR}/io/Exception.io ${CMAKE_CURRENT_SOURCE_DIR}/io/Actor.io ${CMAKE_CURRENT_SOURCE_DIR}/io/AddonLoader.io - ${CMAKE_CURRENT_SOURCE_DIR}/io/B_Sequence.io + ${CMAKE_CURRENT_SOURCE_DIR}/io/Sequence.io ${CMAKE_CURRENT_SOURCE_DIR}/io/Block.io ${CMAKE_CURRENT_SOURCE_DIR}/io/CFunction.io ${CMAKE_CURRENT_SOURCE_DIR}/io/Date.io @@ -39,23 +39,11 @@ set(IO_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/io/System.io ${CMAKE_CURRENT_SOURCE_DIR}/io/UnitTest.io ${CMAKE_CURRENT_SOURCE_DIR}/io/Vector.io - ${CMAKE_CURRENT_SOURCE_DIR}/io/Y_Path.io - ${CMAKE_CURRENT_SOURCE_DIR}/io/Z_CLI.io - ${CMAKE_CURRENT_SOURCE_DIR}/io/Z_Importer.io + ${CMAKE_CURRENT_SOURCE_DIR}/io/Path.io + ${CMAKE_CURRENT_SOURCE_DIR}/io/CLI.io + ${CMAKE_CURRENT_SOURCE_DIR}/io/Importer.io ) -# Object files from every lib. Used to create iovmall static library. -# file(GLOB COROUTINE_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/../coroutine/source/*.[cS]") -# file(GLOB BASEKIT_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/../basekit/source/*.c") -# file(GLOB GARBAGECOLLECTOR_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/../garbagecollector/source/*.c") -# file(GLOB IOVM_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/../source/*.c") -# set(IOVMALL_STATIC_SRCS -# ${COROUTINE_SRCS} -# ${BASEKIT_SRCS} -# ${GARBAGECOLLECTOR_SRCS} -# ${IOVM_SRCS} -# ) - # Create a header file which defines our install prefix. This is # only needed because GCC is batshit insane. file(WRITE ${CMAKE_CURRENT_SOURCE_DIR}/source/IoInstallPrefix.h "#define INSTALL_PREFIX \"${CMAKE_INSTALL_PREFIX}\"") @@ -93,32 +81,39 @@ add_definitions("-DBUILDING_IOVM_DLL")# -DINSTALL_PREFIX="${CMAKE_INSTALL_PREFIX include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/../../deps/parson ${CMAKE_CURRENT_SOURCE_DIR}/../basekit/source - ${CMAKE_CURRENT_SOURCE_DIR}/../coroutine/source ${CMAKE_CURRENT_SOURCE_DIR}/../garbagecollector/source ) # Our library sources. file(GLOB SRCS "source/*.c") +list(REMOVE_ITEM SRCS "${CMAKE_CURRENT_SOURCE_DIR}/source/IoState_iterative_fast.c") list(APPEND SRCS source/IoVMInit.c) list(APPEND SRCS ../../deps/parson/parson.c) file(GLOB HEADERS "source/*.h") # Now build the shared library add_library(iovmall SHARED ${SRCS}) -add_dependencies(iovmall io2c basekit coroutine garbagecollector) +add_dependencies(iovmall io2c basekit garbagecollector) set_target_properties(iovmall PROPERTIES PUBLIC_HEADER "${HEADERS}") -target_link_libraries(iovmall basekit coroutine garbagecollector) +target_link_libraries(iovmall basekit garbagecollector) # The following add the install target, so we put libvmall.* in our # install prefix. if(WIN32) - install(TARGETS iovmall + install(TARGETS iovmall RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX} PUBLIC_HEADER DESTINATION include/io ) else() - install(TARGETS iovmall + install(TARGETS iovmall LIBRARY DESTINATION lib PUBLIC_HEADER DESTINATION include/io ) endif(WIN32) + +# Test for iterative evaluator +add_executable(test_iterative_eval tests/test_iterative_eval.c) +target_link_libraries(test_iterative_eval iovmall) +set_target_properties(test_iterative_eval PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/_build/binaries +) diff --git a/libs/iovm/io/Actor.io b/libs/iovm/io/Actor.io index 9361722f4..e03411dad 100644 --- a/libs/iovm/io/Actor.io +++ b/libs/iovm/io/Actor.io @@ -110,9 +110,9 @@ Object do( // need to yield in coroDo to allow future to be added to queue //self actorCoroutine := self coroDo(yield; actorProcessQueue) // coroDo refs stack! self actorCoroutine := Coroutine clone //setStackSize(20000) - actorCoroutine setRunTarget(self) - actorCoroutine setRunLocals(self) - actorCoroutine setRunMessage(message(actorProcessQueue)) + actorCoroutine setSlot("runTarget", self) + actorCoroutine setSlot("runLocals", self) + actorCoroutine setSlot("runMessage", message(actorProcessQueue)) Coroutine yieldingCoros atInsert(0, actorCoroutine) //Coroutine yieldingCoros append(actorCoroutine) ) @@ -171,14 +171,16 @@ Object do( setSlot("@", method( //writeln("@ ", call argAt(0)) m := call argAt(0) asMessageWithEvaluatedArgs(call sender) - f := Future clone setRunTarget(self) setRunMessage(m) + f := Future clone + f setSlot("runTarget", self) + f setSlot("runMessage", m) self actorRun self actorQueue append(f) f futureProxy )) - + futureSend := getSlot("@") - + /*doc Object @@ Same as Object @, but returns nil instead of FutureProxy.
@@ -188,7 +190,9 @@ Object do( setSlot("@@", method( //writeln(self type , "_", self uniqueId, " @@", call argAt(0)) //, " ", call argAt(0) label) m := call argAt(0) asMessageWithEvaluatedArgs(call sender) - f := Future clone setRunTarget(self) setRunMessage(m) + f := Future clone + f setSlot("runTarget", self) + f setSlot("runMessage", m) self actorRun self actorQueue append(f) nil diff --git a/libs/iovm/io/Z_CLI.io b/libs/iovm/io/CLI.io similarity index 100% rename from libs/iovm/io/Z_CLI.io rename to libs/iovm/io/CLI.io diff --git a/libs/iovm/io/A4_Exception.io b/libs/iovm/io/Exception.io similarity index 84% rename from libs/iovm/io/A4_Exception.io rename to libs/iovm/io/Exception.io index 13110d9b1..e3e07c93c 100644 --- a/libs/iovm/io/A4_Exception.io +++ b/libs/iovm/io/Exception.io @@ -65,6 +65,10 @@ Coroutine do( //metadoc Coroutine category Core //metadoc Coroutine description Coroutine is an primitive for Io's lightweight cooperative C-stack based threads. + //doc Coroutine handlerStack The list of resumable exception handler entries for this coroutine, or nil. + //doc Coroutine setHandlerStack + handlerStack ::= nil + init := method( recentInChain = nil ) @@ -287,9 +291,9 @@ Coroutine do( ) raiseException := method(e, - //doc Coroutine raiseException Sets exception in the receiver and resumes parent coroutine. + //doc Coroutine raiseException Sets exception in the receiver and signals the eval loop to unwind. self setException(e) - resumeParentCoroutine + rawSignalException ) ) @@ -337,23 +341,38 @@ Object do( See also documentation for Exception catch and pass. */ coro := Coroutine clone - coro setParentCoroutine(Scheduler currentCoroutine) - coro setRunTarget(call sender) - coro setRunLocals(call sender) - coro setRunMessage(call argAt(0)) + coro setSlot("parentCoroutine", Scheduler currentCoroutine) + coro setSlot("runTarget", call sender) + coro setSlot("runLocals", call sender) + coro setSlot("runMessage", call argAt(0)) coro run if(coro exception, coro exception, nil) ) + withHandler := method(exceptionProto, handlerBlock, + /*doc Object withHandler(exceptionProto, handlerBlock, body) + Installs a resumable exception handler for exceptions matching exceptionProto, + runs body, then removes the handler. The handlerBlock receives (exception, resume). + To resume at the signal site, call resume invoke(value) or simply return a value. + */ + coro := Scheduler currentCoroutine + if(coro handlerStack isNil, coro setHandlerStack(list)) + entry := list(exceptionProto, handlerBlock) + coro handlerStack append(entry) + result := call sender doMessage(call argAt(2), call sender) + coro handlerStack pop + result + ) + coroFor := method( /*doc Object coroFor(code) Returns a new coro to be run in a context of sender. */ coro := Coroutine clone - coro setRunTarget(call sender) - coro setRunLocals(call sender) - coro setRunMessage(call argAt(0)) + coro setSlot("runTarget", call sender) + coro setSlot("runLocals", call sender) + coro setSlot("runMessage", call argAt(0)) coro ) @@ -363,9 +382,9 @@ Object do( Returns a coro. */ coro := Coroutine clone - coro setRunTarget(call sender) - coro setRunLocals(call sender) - coro setRunMessage(call argAt(0)) + coro setSlot("runTarget", call sender) + coro setSlot("runLocals", call sender) + coro setSlot("runMessage", call argAt(0)) Coroutine yieldingCoros atInsert(0, Scheduler currentCoroutine) coro run coro @@ -381,9 +400,9 @@ Object do( Note: run target is self (i.e. receiver), not call sender as in coroDo. */ coro := Coroutine clone - coro setRunTarget(self) - coro setRunLocals(call sender) - coro setRunMessage(call argAt(0)) + coro setSlot("runTarget", self) + coro setSlot("runLocals", call sender) + coro setSlot("runMessage", call argAt(0)) Coroutine yieldingCoros atInsert(0, coro) coro ) @@ -392,9 +411,9 @@ Object do( coroWith := method( //doc Object coroWith(code) Returns a new coro to be run in a context of receiver. coro := Coroutine clone - coro setRunTarget(self) - coro setRunLocals(call sender) - coro setRunMessage(call argAt(0)) + coro setSlot("runTarget", self) + coro setSlot("runLocals", call sender) + coro setSlot("runMessage", call argAt(0)) coro ) @@ -513,6 +532,48 @@ MyErrorType := Error clone nestedException showStack ) ) + + _findHandler := method( + /*doc Exception _findHandler + Walks handler stacks from the current coroutine up through the parentCoroutine chain. + Returns the first matching handler block for this exception type, or nil. + */ + coro := Scheduler currentCoroutine + while(coro isNil not, + if(coro handlerStack isNil not, + coro handlerStack reverseForeach(entry, + if(self isKindOf(entry at(0)), + return entry at(1) + ) + ) + ) + coro = coro parentCoroutine + ) + nil + ) + + _Resumption := Object clone do( + //doc Exception _Resumption A simple object whose invoke(v) returns v. + // Passed to handlers as the resume argument for API compatibility. + invoke := method(v, v) + ) + + signal := method(error, nestedException, + /*doc Exception signal(error, optionalNestedException) + Raises a resumable exception. If a matching handler is installed via withHandler, + the handler is called with (exception, resume). The handler can resume + execution at the signal site by calling resume invoke(value), or simply returning + a value (which auto-resumes). If no handler is found, falls back to non-resumable raise. + */ + coro := Scheduler currentCoroutine + exception := self clone setError(error) setCoroutine(coro) setNestedException(nestedException) + handler := exception _findHandler + if(handler isNil, + coro raiseException(exception) + return + ) + handler call(exception, _Resumption) + ) ) System userInterruptHandler := method( diff --git a/libs/iovm/io/Z_Importer.io b/libs/iovm/io/Importer.io similarity index 100% rename from libs/iovm/io/Z_Importer.io rename to libs/iovm/io/Importer.io diff --git a/libs/iovm/io/A3_List.io b/libs/iovm/io/List.io similarity index 100% rename from libs/iovm/io/A3_List.io rename to libs/iovm/io/List.io diff --git a/libs/iovm/io/A0_List.io b/libs/iovm/io/List_bootstrap.io similarity index 100% rename from libs/iovm/io/A0_List.io rename to libs/iovm/io/List_bootstrap.io diff --git a/libs/iovm/io/A2_Object.io b/libs/iovm/io/Object.io similarity index 99% rename from libs/iovm/io/A2_Object.io rename to libs/iovm/io/Object.io index 0d2dbbaec..2dd611e0c 100644 --- a/libs/iovm/io/A2_Object.io +++ b/libs/iovm/io/Object.io @@ -365,6 +365,7 @@ Object do( setSlot("?", method( m := call argAt(0) + if(m isNil, return nil) if (self getSlot(m name) != nil, call relayStopStatus(m doInContext(self, call sender)) , diff --git a/libs/iovm/io/A0_Object.io b/libs/iovm/io/Object_bootstrap.io similarity index 100% rename from libs/iovm/io/A0_Object.io rename to libs/iovm/io/Object_bootstrap.io diff --git a/libs/iovm/io/A1_OperatorTable.io b/libs/iovm/io/OperatorTable.io similarity index 100% rename from libs/iovm/io/A1_OperatorTable.io rename to libs/iovm/io/OperatorTable.io diff --git a/libs/iovm/io/Y_Path.io b/libs/iovm/io/Path.io similarity index 100% rename from libs/iovm/io/Y_Path.io rename to libs/iovm/io/Path.io diff --git a/libs/iovm/io/B_Sequence.io b/libs/iovm/io/Sequence.io similarity index 100% rename from libs/iovm/io/B_Sequence.io rename to libs/iovm/io/Sequence.io diff --git a/libs/iovm/io/System.io b/libs/iovm/io/System.io index 272169cf3..04e3d2bfd 100644 --- a/libs/iovm/io/System.io +++ b/libs/iovm/io/System.io @@ -74,7 +74,7 @@ System do( self exit ) - //doc System runCommand Calls system and redirects stdout/err to tmp files. Returns object with exitStatus, stdout and stderr slots. + //doc System runCommand(cmd, successStatus) Calls system and redirects stdout/err to tmp files. Returns object with exitStatus, stdout and stderr slots. runCommand := method(cmd, successStatus, successStatus := if(successStatus, successStatus, 0) tmpDirPath := System getEnvironmentVariable("TMPDIR") diff --git a/libs/iovm/source/IoBlock.c b/libs/iovm/source/IoBlock.c index 6917b5725..203297452 100644 --- a/libs/iovm/source/IoBlock.c +++ b/libs/iovm/source/IoBlock.c @@ -80,6 +80,7 @@ IoState_lobby(IOSTATE), UArray_asCString(ba), "Block readFromStore"); { IoState_error_(IOSTATE, NULL, "Store found bad block code: %s", (char *)UArray_bytes(ba)); + return; } IoBlock_copy_(self, newBlock); @@ -240,26 +241,10 @@ IoObject *IoBlock_activate(IoBlock *self, IoObject *target, IoObject *locals, // gc may kick in while evaling locals, so we need to be safe IoObject_setSlot_to_(blockLocals, name, arg);); - if (Coro_stackSpaceAlmostGone(IoCoroutine_cid(state->currentCoroutine))) { - /* - IoCoroutine *currentCoroutine = state->currentCoroutine; - Coro *coro = IoCoroutine_cid(currentCoroutine); - - printf("%p-%p block overflow %i/%i\n", - (void *)currentCoroutine, (void *)coro, - Coro_bytesLeftOnStack(coro), Coro_stackSize(coro)); printf("message = - %s\n", CSTRING(IoMessage_name(selfData->message))); - */ - { - IoCoroutine *newCoro = IoCoroutine_new(state); - IoCoroutine_try(newCoro, blockLocals, blockLocals, - selfData->message); - result = IoCoroutine_rawResult(newCoro); - } - } else { - result = IoMessage_locals_performOn_(selfData->message, blockLocals, - blockLocals); - } + // With frame-based coroutines, no stack overflow check is needed + // (frames are heap-allocated, not using C stack) + result = IoMessage_locals_performOn_(selfData->message, blockLocals, + blockLocals); if (DATA(self)->passStops == 0) { state->returnValue = result; @@ -399,6 +384,7 @@ IO_METHOD(IoBlock, code_) { DATA(self)->message = IOREF(newM); } else { IoState_error_(IOSTATE, m, "no messages found in compile string"); + return IONIL(self); } return self; @@ -442,6 +428,7 @@ IO_METHOD(IoBlock, argumentNames_) { */ IoList *newArgNames = IoMessage_locals_listArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); List *rawNewArgNames = IoList_rawList(newArgNames); LIST_FOREACH( diff --git a/libs/iovm/source/IoBlock.h b/libs/iovm/source/IoBlock.h index 3d15f5bfb..0564ea0a5 100644 --- a/libs/iovm/source/IoBlock.h +++ b/libs/iovm/source/IoBlock.h @@ -9,6 +9,7 @@ #include "IoObject.h" #include "IoState.h" #include "IoCall.h" +#include #ifdef __cplusplus extern "C" { diff --git a/libs/iovm/source/IoCFunction.c b/libs/iovm/source/IoCFunction.c index e69bce0cc..4171834dd 100644 --- a/libs/iovm/source/IoCFunction.c +++ b/libs/iovm/source/IoCFunction.c @@ -181,6 +181,7 @@ IoObject *IoCFunction_activate(IoCFunction *self, IoObject *target, IoState_error_(IOSTATE, m, "CFunction defined for type %s but called on type %s", a, b); + return IOSTATE->ioNil; } // IoState_pushRetainPool(state); diff --git a/libs/iovm/source/IoCFunction.h b/libs/iovm/source/IoCFunction.h index 33ba4a97f..51853506b 100644 --- a/libs/iovm/source/IoCFunction.h +++ b/libs/iovm/source/IoCFunction.h @@ -9,6 +9,7 @@ #include "Common.h" #include "IoObject.h" +#include #ifdef __cplusplus extern "C" { @@ -30,6 +31,7 @@ typedef struct { IoUserFunction *func; IoSymbol *uniqueName; clock_t profilerTime; + unsigned int isLazyArgs; // 1 = special form, args must not be pre-evaluated } IoCFunctionData; IOVM_API IoCFunction *IoCFunction_proto(void *state); diff --git a/libs/iovm/source/IoContinuation.c b/libs/iovm/source/IoContinuation.c new file mode 100644 index 000000000..0c127f422 --- /dev/null +++ b/libs/iovm/source/IoContinuation.c @@ -0,0 +1,929 @@ + +// metadoc Continuation copyright Steve Dekorte 2002, 2025 +// metadoc Continuation license BSD revised + +#ifdef IO_CALLCC + +#include "IoContinuation.h" +#include "IoState.h" +#include "IoState_eval.h" +#include "IoObject.h" +#include "IoMessage.h" +#include "IoBlock.h" +#include "IoList.h" +#include "IoMap.h" +#include "IoNumber.h" +#include "IoSeq.h" +#include "IoEvalFrame.h" +#include "IoMessage_parser.h" +#include +#include + +#define DATA(self) ((IoContinuationData *)IoObject_dataPointer(self)) + +static const char *protoId = "Continuation"; + +IoTag *IoContinuation_newTag(void *state) { + IoTag *tag = IoTag_newWithName_(protoId); + IoTag_state_(tag, state); + IoTag_freeFunc_(tag, (IoTagFreeFunc *)IoContinuation_free); + IoTag_cloneFunc_(tag, (IoTagCloneFunc *)IoContinuation_rawClone); + IoTag_markFunc_(tag, (IoTagMarkFunc *)IoContinuation_mark); + return tag; +} + +IoContinuation *IoContinuation_proto(void *state) { + IoObject *self = IoObject_new(state); + IoObject_tag_(self, IoContinuation_newTag(state)); + + IoObject_setDataPointer_(self, io_calloc(1, sizeof(IoContinuationData))); + DATA(self)->capturedFrame = NULL; + DATA(self)->capturedLocals = NULL; + DATA(self)->invoked = 0; + + IoState_registerProtoWithId_(state, self, protoId); + + // Methods + { + IoMethodTable methodTable[] = { + {"invoke", IoContinuation_invoke}, + {"isInvoked", IoContinuation_isInvoked}, + {"copy", IoContinuation_copy}, + {"frameCount", IoContinuation_frameCount}, + {"frameStates", IoContinuation_frameStates}, + {"frameMessages", IoContinuation_frameMessages}, + {"asMap", IoContinuation_asMap}, + {"fromMap", IoContinuation_fromMap}, + {NULL, NULL}, + }; + IoObject_addMethodTable_(self, methodTable); + } + + return self; +} + +IoContinuation *IoContinuation_rawClone(IoContinuation *proto) { + IoObject *self = IoObject_rawClonePrimitive(proto); + IoObject_setDataPointer_(self, io_calloc(1, sizeof(IoContinuationData))); + DATA(self)->capturedFrame = NULL; + DATA(self)->capturedLocals = NULL; + DATA(self)->invoked = 0; + return self; +} + +IoContinuation *IoContinuation_new(void *state) { + IoObject *proto = IoState_protoWithId_(state, protoId); + return IOCLONE(proto); +} + +void IoContinuation_free(IoContinuation *self) { + // Frames are GC-managed, just null the pointer + DATA(self)->capturedFrame = NULL; + io_free(IoObject_dataPointer(self)); +} + +void IoContinuation_mark(IoContinuation *self) { + // Mark captured locals + IoObject_shouldMarkIfNonNull(DATA(self)->capturedLocals); + + // Mark top captured frame — GC follows the parent chain + // via IoEvalFrame's own markFunc + IoObject_shouldMarkIfNonNull(DATA(self)->capturedFrame); +} + +static IoEvalFrame *copyFrameChain_(IoState *state, IoEvalFrame *src); + +// Capture the current frame stack into this continuation. +// Deep copy the frame chain at capture time. This is necessary because +// popFrame_ zeroes frame data when frames are popped, so a grab-pointer +// capture would lose the frame state after normal return from callcc. +// The deep copy ensures deferred and multi-shot invocations work correctly. +void IoContinuation_captureFrameStack_(IoContinuation *self, + IoEvalFrame *frame, + IoObject *locals) { + IoState *state = IOSTATE; + DATA(self)->capturedFrame = copyFrameChain_(state, frame); + DATA(self)->capturedLocals = locals; + DATA(self)->invoked = 0; +} + +// Deep copy a frame chain (iterative). Used by the copy method. +static IoEvalFrame *copyFrameChain_(IoState *state, IoEvalFrame *src) { + if (!src) return NULL; + + IoEvalFrame *newTop = NULL; + IoEvalFrame *prevCopy = NULL; + + while (src) { + IoEvalFrameData *srcFd = FRAME_DATA(src); + + IoEvalFrame *copy = IoEvalFrame_newWithState(state); + IoEvalFrameData *copyFd = FRAME_DATA(copy); + + // Copy all data fields + memcpy(copyFd, srcFd, sizeof(IoEvalFrameData)); + + // Deep copy argValues array + if (srcFd->argValues && srcFd->argCount > 0) { + copyFd->argValues = io_calloc(srcFd->argCount, sizeof(IoObject *)); + memcpy(copyFd->argValues, srcFd->argValues, + srcFd->argCount * sizeof(IoObject *)); + } else { + copyFd->argValues = NULL; + } + + copyFd->parent = NULL; + + if (prevCopy) { + FRAME_DATA(prevCopy)->parent = copy; + } else { + newTop = copy; + } + + prevCopy = copy; + src = srcFd->parent; + } + + return newTop; +} + +// ============================================================ +// Methods +// ============================================================ + +IO_METHOD(IoContinuation, invoke) { + /*doc Continuation invoke(value) + Invokes the continuation, restoring the captured execution state. + The value argument becomes the result of the original callcc call. + Continuations are one-shot: they can only be invoked once. + Use copy to create a fresh continuation for multiple invocations. + */ + + IoState *state = IOSTATE; + + if (DATA(self)->invoked) { + IoState_error_(state, m, + "Continuation has already been invoked. " + "Use copy to create a fresh continuation for multiple invocations."); + return IONIL(self); + } + + IoObject *value = IoMessage_locals_valueArgAt_(m, locals, 0); + + if (state->currentFrame == NULL) { + IoState_error_(state, m, + "Continuation invoke requires iterative evaluation mode"); + return IONIL(self); + } + + if (!DATA(self)->capturedFrame) { + IoState_error_(state, m, + "Continuation has no captured state"); + return IONIL(self); + } + + DATA(self)->invoked = 1; + + // Replace the entire frame stack. + // Old frames become GC garbage (nothing references them). + state->currentFrame = DATA(self)->capturedFrame; + DATA(self)->capturedFrame = NULL; // one-shot transfer + + // Count the restored frames + state->frameDepth = 0; + IoEvalFrame *f = state->currentFrame; + while (f) { + state->frameDepth++; + f = FRAME_DATA(f)->parent; + } + + // Set the result of the callcc to be the invoked value + if (state->currentFrame) { + FRAME_DATA(state->currentFrame)->result = value; + } + + // Signal to the eval loop that the frame stack was replaced + state->continuationInvoked = 1; + + return value; +} + +IO_METHOD(IoContinuation, isInvoked) { + /*doc Continuation isInvoked + Returns true if this continuation has been invoked. + */ + return IOBOOL(self, DATA(self)->invoked); +} + +IO_METHOD(IoContinuation, copy) { + /*doc Continuation copy + Returns a deep copy of this continuation. The copy has an independent + frame chain that can be invoked separately. Use this for multi-shot + or delayed invocation patterns. + + Example: + saved := nil + callcc(block(cont, + saved = cont copy // deep copy while frames are live + "normal" + )) + // Later: saved invoke("delayed") + */ + IoState *state = IOSTATE; + IoContinuation *newCont = IoContinuation_new(state); + + if (DATA(self)->capturedFrame) { + DATA(newCont)->capturedFrame = + copyFrameChain_(state, DATA(self)->capturedFrame); + } + DATA(newCont)->capturedLocals = DATA(self)->capturedLocals; + DATA(newCont)->invoked = 0; + + return newCont; +} + +IO_METHOD(IoContinuation, frameCount) { + /*doc Continuation frameCount + Returns the number of frames in the captured continuation stack. + Returns 0 if no state has been captured. + */ + int count = 0; + IoEvalFrame *frame = DATA(self)->capturedFrame; + while (frame) { + count++; + frame = FRAME_DATA(frame)->parent; + } + return IONUMBER(count); +} + +IO_METHOD(IoContinuation, frameStates) { + /*doc Continuation frameStates + Returns a list of strings describing the state of each frame + in the captured continuation stack (from top to bottom). + */ + IoState *state = IOSTATE; + IoList *list = IoList_new(state); + IoEvalFrame *frame = DATA(self)->capturedFrame; + while (frame) { + IoEvalFrameData *fd = FRAME_DATA(frame); + const char *name = IoEvalFrame_stateName(fd->state); + IoObject *str = IOSYMBOL(name); + IoList_rawAppend_(list, str); + frame = fd->parent; + } + return list; +} + +IO_METHOD(IoContinuation, frameMessages) { + /*doc Continuation frameMessages + Returns a list of strings showing the current message at each + frame in the captured continuation stack (from top to bottom). + */ + IoState *state = IOSTATE; + IoList *list = IoList_new(state); + IoEvalFrame *frame = DATA(self)->capturedFrame; + while (frame) { + IoEvalFrameData *fd = FRAME_DATA(frame); + if (fd->message) { + IoObject *str = IOSYMBOL(CSTRING(IoMessage_name(fd->message))); + IoList_rawAppend_(list, str); + } else { + IoList_rawAppend_(list, state->ioNil); + } + frame = fd->parent; + } + return list; +} + +// ============================================================ +// Serialization helpers +// ============================================================ + +#define SYM(s) IoState_symbolWithCString_(state, (s)) +#define NUM(n) IoNumber_newWithDouble_(state, (double)(n)) + +// Helper: serialize an IoObject to a portable representation +static IoObject *serializeObject_(IoState *state, IoObject *obj) { + if (!obj) return state->ioNil; + if (obj == state->ioNil) return state->ioNil; + if (obj == state->ioTrue) return state->ioTrue; + if (obj == state->ioFalse) return state->ioFalse; + if (ISNUMBER(obj)) return obj; + if (ISSEQ(obj)) return obj; + + IoMap *map = IoMap_new(state); + const char *tagName = IoObject_tag(obj)->name; + IoMap_rawAtPut(map, SYM("_type"), SYM(tagName)); + + if (IoObject_slots(obj)) { + IoList *slotNames = IoList_new(state); + PHASH_FOREACH(IoObject_slots(obj), k, v, + IoList_rawAppend_(slotNames, k); + ); + IoMap_rawAtPut(map, SYM("_slotNames"), slotNames); + } + return map; +} + +// Helper: serialize a message tree to a Map +static IoObject *serializeMessage_(IoState *state, IoMessage *msg) { + if (!msg) return state->ioNil; + + IoMap *map = IoMap_new(state); + + IoMap_rawAtPut(map, SYM("name"), + SYM(CSTRING(IoMessage_name(msg)))); + + UArray *codeUA = IoMessage_descriptionJustSelfAndArgs(msg); + IoSeq *code = IoSeq_newWithUArray_copy_(state, codeUA, 0); + IoMap_rawAtPut(map, SYM("code"), code); + + UArray *chainUA = IoMessage_description(msg); + IoSeq *chainCode = IoSeq_newWithUArray_copy_(state, chainUA, 0); + IoMap_rawAtPut(map, SYM("chainCode"), chainCode); + + IoObject *cached = IoMessage_rawCachedResult(msg); + if (cached) { + IoMap_rawAtPut(map, SYM("cachedResult"), + serializeObject_(state, cached)); + } + + IoMap_rawAtPut(map, SYM("lineNumber"), + NUM(IoMessage_rawLineNumber(msg))); + IoSymbol *label = IoMessage_rawLabel(msg); + if (label) { + IoMap_rawAtPut(map, SYM("label"), label); + } + + IoMap_rawAtPut(map, SYM("argCount"), + NUM(IoMessage_argCount(msg))); + + return map; +} + +// Helper: serialize control flow state for a frame +static void serializeControlFlow_(IoState *state, IoEvalFrame *frame, + IoMap *map) { + IoEvalFrameData *fd = FRAME_DATA(frame); + switch (fd->state) { + case FRAME_STATE_IF_EVAL_CONDITION: + case FRAME_STATE_IF_CONVERT_BOOLEAN: + case FRAME_STATE_IF_EVAL_BRANCH: { + IoMap_rawAtPut(map, SYM("conditionResult"), + NUM(fd->controlFlow.ifInfo.conditionResult)); + if (fd->controlFlow.ifInfo.conditionMsg) + IoMap_rawAtPut(map, SYM("conditionMsg"), + serializeMessage_(state, fd->controlFlow.ifInfo.conditionMsg)); + if (fd->controlFlow.ifInfo.trueBranch) + IoMap_rawAtPut(map, SYM("trueBranch"), + serializeMessage_(state, fd->controlFlow.ifInfo.trueBranch)); + if (fd->controlFlow.ifInfo.falseBranch) + IoMap_rawAtPut(map, SYM("falseBranch"), + serializeMessage_(state, fd->controlFlow.ifInfo.falseBranch)); + break; + } + + case FRAME_STATE_WHILE_EVAL_CONDITION: + case FRAME_STATE_WHILE_CHECK_CONDITION: + case FRAME_STATE_WHILE_DECIDE: + case FRAME_STATE_WHILE_EVAL_BODY: { + IoMap_rawAtPut(map, SYM("conditionResult"), + NUM(fd->controlFlow.whileInfo.conditionResult)); + if (fd->controlFlow.whileInfo.conditionMsg) + IoMap_rawAtPut(map, SYM("conditionMsg"), + serializeMessage_(state, fd->controlFlow.whileInfo.conditionMsg)); + if (fd->controlFlow.whileInfo.bodyMsg) + IoMap_rawAtPut(map, SYM("bodyMsg"), + serializeMessage_(state, fd->controlFlow.whileInfo.bodyMsg)); + break; + } + + case FRAME_STATE_LOOP_EVAL_BODY: + case FRAME_STATE_LOOP_AFTER_BODY: { + if (fd->controlFlow.loopInfo.bodyMsg) + IoMap_rawAtPut(map, SYM("bodyMsg"), + serializeMessage_(state, fd->controlFlow.loopInfo.bodyMsg)); + break; + } + + case FRAME_STATE_FOR_EVAL_SETUP: + case FRAME_STATE_FOR_EVAL_BODY: + case FRAME_STATE_FOR_AFTER_BODY: { + IoMap_rawAtPut(map, SYM("startValue"), + NUM(fd->controlFlow.forInfo.startValue)); + IoMap_rawAtPut(map, SYM("endValue"), + NUM(fd->controlFlow.forInfo.endValue)); + IoMap_rawAtPut(map, SYM("increment"), + NUM(fd->controlFlow.forInfo.increment)); + IoMap_rawAtPut(map, SYM("currentValue"), + NUM(fd->controlFlow.forInfo.currentValue)); + IoMap_rawAtPut(map, SYM("initialized"), + NUM(fd->controlFlow.forInfo.initialized)); + if (fd->controlFlow.forInfo.counterName) + IoMap_rawAtPut(map, SYM("counterName"), + fd->controlFlow.forInfo.counterName); + if (fd->controlFlow.forInfo.bodyMsg) + IoMap_rawAtPut(map, SYM("bodyMsg"), + serializeMessage_(state, fd->controlFlow.forInfo.bodyMsg)); + break; + } + + case FRAME_STATE_FOREACH_EVAL_BODY: + case FRAME_STATE_FOREACH_AFTER_BODY: { + IoMap_rawAtPut(map, SYM("currentIndex"), + NUM(fd->controlFlow.foreachInfo.currentIndex)); + IoMap_rawAtPut(map, SYM("collectionSize"), + NUM(fd->controlFlow.foreachInfo.collectionSize)); + IoMap_rawAtPut(map, SYM("direction"), + NUM(fd->controlFlow.foreachInfo.direction)); + IoMap_rawAtPut(map, SYM("isEach"), + NUM(fd->controlFlow.foreachInfo.isEach)); + if (fd->controlFlow.foreachInfo.indexName) + IoMap_rawAtPut(map, SYM("indexName"), + fd->controlFlow.foreachInfo.indexName); + if (fd->controlFlow.foreachInfo.valueName) + IoMap_rawAtPut(map, SYM("valueName"), + fd->controlFlow.foreachInfo.valueName); + if (fd->controlFlow.foreachInfo.bodyMsg) + IoMap_rawAtPut(map, SYM("bodyMsg"), + serializeMessage_(state, fd->controlFlow.foreachInfo.bodyMsg)); + break; + } + + case FRAME_STATE_CALLCC_EVAL_BLOCK: { + if (fd->controlFlow.callccInfo.continuation) + IoMap_rawAtPut(map, SYM("hasContinuation"), state->ioTrue); + break; + } + + default: + break; + } +} + +// Serialize a single frame to a Map +static IoMap *serializeFrame_(IoState *state, IoEvalFrame *frame) { + IoEvalFrameData *fd = FRAME_DATA(frame); + IoMap *map = IoMap_new(state); + + IoMap_rawAtPut(map, SYM("state"), + SYM(IoEvalFrame_stateName(fd->state))); + + if (fd->message) + IoMap_rawAtPut(map, SYM("message"), + serializeMessage_(state, fd->message)); + + IoMap_rawAtPut(map, SYM("passStops"), NUM(fd->passStops)); + IoMap_rawAtPut(map, SYM("isNestedEvalRoot"), + NUM(fd->isNestedEvalRoot)); + + IoMap_rawAtPut(map, SYM("argCount"), NUM(fd->argCount)); + IoMap_rawAtPut(map, SYM("currentArgIndex"), + NUM(fd->currentArgIndex)); + + if (fd->argValues && fd->currentArgIndex > 0) { + IoList *args = IoList_new(state); + for (int i = 0; i < fd->currentArgIndex; i++) { + IoList_rawAppend_(args, + serializeObject_(state, fd->argValues[i])); + } + IoMap_rawAtPut(map, SYM("argValues"), args); + } + + if (fd->target) + IoMap_rawAtPut(map, SYM("targetType"), + SYM(IoObject_tag(fd->target)->name)); + if (fd->locals) + IoMap_rawAtPut(map, SYM("localsType"), + SYM(IoObject_tag(fd->locals)->name)); + if (fd->result) + IoMap_rawAtPut(map, SYM("result"), + serializeObject_(state, fd->result)); + if (fd->slotValue) + IoMap_rawAtPut(map, SYM("slotValue"), + serializeObject_(state, fd->slotValue)); + + if (fd->blockLocals) { + IoMap_rawAtPut(map, SYM("hasBlockLocals"), state->ioTrue); + if (IoObject_slots(fd->blockLocals)) { + IoMap *slotsMap = IoMap_new(state); + PHASH_FOREACH(IoObject_slots(fd->blockLocals), k, v, + IoMap_rawAtPut(slotsMap, k, serializeObject_(state, v)); + ); + IoMap_rawAtPut(map, SYM("blockLocalsSlots"), slotsMap); + } + } + + if (fd->call) { + int stopStatus = IoCall_rawStopStatus((IoCall *)fd->call); + IoMap_rawAtPut(map, SYM("callStopStatus"), NUM(stopStatus)); + } + if (fd->savedCall) { + int stopStatus = IoCall_rawStopStatus((IoCall *)fd->savedCall); + IoMap_rawAtPut(map, SYM("savedCallStopStatus"), NUM(stopStatus)); + } + + serializeControlFlow_(state, frame, map); + + return map; +} + +IO_METHOD(IoContinuation, asMap) { + /*doc Continuation asMap + Returns a Map representation of the captured continuation state. + */ + IoState *state = IOSTATE; + IoMap *result = IoMap_new(state); + + IoMap_rawAtPut(result, IOSYMBOL("invoked"), + IOBOOL(self, DATA(self)->invoked)); + + int count = 0; + IoEvalFrame *frame = DATA(self)->capturedFrame; + while (frame) { + count++; + frame = FRAME_DATA(frame)->parent; + } + IoMap_rawAtPut(result, IOSYMBOL("frameCount"), IONUMBER(count)); + + IoList *frames = IoList_new(state); + frame = DATA(self)->capturedFrame; + while (frame) { + IoList_rawAppend_(frames, serializeFrame_(state, frame)); + frame = FRAME_DATA(frame)->parent; + } + IoMap_rawAtPut(result, IOSYMBOL("frames"), frames); + + return result; +} + +#undef SYM +#undef NUM + +// ============================================================ +// Deserialization helpers +// ============================================================ + +#define SYM(s) IoState_symbolWithCString_(state, (s)) +#define NUM(n) IoNumber_newWithDouble_(state, (double)(n)) + +static double mapNumberAt_(IoState *state, IoMap *map, const char *key, double defaultVal) { + IoObject *val = IoMap_rawAt(map, SYM(key)); + if (!val || val == state->ioNil) return defaultVal; + if (ISNUMBER(val)) return CNUMBER(val); + return defaultVal; +} + +static const char *mapStringAt_(IoState *state, IoMap *map, const char *key) { + IoObject *val = IoMap_rawAt(map, SYM(key)); + if (!val || val == state->ioNil) return NULL; + if (ISSEQ(val)) return CSTRING(val); + return NULL; +} + +static IoMessage *deserializeMessage_(IoState *state, IoObject *msgMap) { + if (!msgMap || msgMap == state->ioNil) return NULL; + if (!ISMAP(msgMap)) return NULL; + + const char *chainCode = mapStringAt_(state, (IoMap *)msgMap, "chainCode"); + if (!chainCode) { + chainCode = mapStringAt_(state, (IoMap *)msgMap, "code"); + } + if (!chainCode) return NULL; + + const char *label = mapStringAt_(state, (IoMap *)msgMap, "label"); + if (!label) label = "fromMap"; + + return IoMessage_newFromText_label_(state, chainCode, label); +} + +static void deserializeControlFlow_(IoState *state, IoEvalFrame *frame, + IoMap *map) { + IoEvalFrameData *fd = FRAME_DATA(frame); + switch (fd->state) { + case FRAME_STATE_IF_EVAL_CONDITION: + case FRAME_STATE_IF_CONVERT_BOOLEAN: + case FRAME_STATE_IF_EVAL_BRANCH: { + fd->controlFlow.ifInfo.conditionResult = + (int)mapNumberAt_(state, map, "conditionResult", 0); + IoObject *condMap = IoMap_rawAt(map, SYM("conditionMsg")); + fd->controlFlow.ifInfo.conditionMsg = + deserializeMessage_(state, condMap); + IoObject *trueMap = IoMap_rawAt(map, SYM("trueBranch")); + fd->controlFlow.ifInfo.trueBranch = + deserializeMessage_(state, trueMap); + IoObject *falseMap = IoMap_rawAt(map, SYM("falseBranch")); + fd->controlFlow.ifInfo.falseBranch = + deserializeMessage_(state, falseMap); + break; + } + + case FRAME_STATE_WHILE_EVAL_CONDITION: + case FRAME_STATE_WHILE_CHECK_CONDITION: + case FRAME_STATE_WHILE_DECIDE: + case FRAME_STATE_WHILE_EVAL_BODY: { + fd->controlFlow.whileInfo.conditionResult = + (int)mapNumberAt_(state, map, "conditionResult", 0); + IoObject *condMap = IoMap_rawAt(map, SYM("conditionMsg")); + fd->controlFlow.whileInfo.conditionMsg = + deserializeMessage_(state, condMap); + IoObject *bodyMap = IoMap_rawAt(map, SYM("bodyMsg")); + fd->controlFlow.whileInfo.bodyMsg = + deserializeMessage_(state, bodyMap); + break; + } + + case FRAME_STATE_LOOP_EVAL_BODY: + case FRAME_STATE_LOOP_AFTER_BODY: { + IoObject *bodyMap = IoMap_rawAt(map, SYM("bodyMsg")); + fd->controlFlow.loopInfo.bodyMsg = + deserializeMessage_(state, bodyMap); + break; + } + + case FRAME_STATE_FOR_EVAL_SETUP: + case FRAME_STATE_FOR_EVAL_BODY: + case FRAME_STATE_FOR_AFTER_BODY: { + fd->controlFlow.forInfo.startValue = + mapNumberAt_(state, map, "startValue", 0); + fd->controlFlow.forInfo.endValue = + mapNumberAt_(state, map, "endValue", 0); + fd->controlFlow.forInfo.increment = + mapNumberAt_(state, map, "increment", 1); + fd->controlFlow.forInfo.currentValue = + mapNumberAt_(state, map, "currentValue", 0); + fd->controlFlow.forInfo.initialized = + (int)mapNumberAt_(state, map, "initialized", 0); + const char *counterName = mapStringAt_(state, map, "counterName"); + if (counterName) + fd->controlFlow.forInfo.counterName = SYM(counterName); + IoObject *bodyMap = IoMap_rawAt(map, SYM("bodyMsg")); + fd->controlFlow.forInfo.bodyMsg = + deserializeMessage_(state, bodyMap); + break; + } + + case FRAME_STATE_FOREACH_EVAL_BODY: + case FRAME_STATE_FOREACH_AFTER_BODY: { + fd->controlFlow.foreachInfo.currentIndex = + (int)mapNumberAt_(state, map, "currentIndex", 0); + fd->controlFlow.foreachInfo.collectionSize = + (int)mapNumberAt_(state, map, "collectionSize", 0); + fd->controlFlow.foreachInfo.direction = + (int)mapNumberAt_(state, map, "direction", 1); + fd->controlFlow.foreachInfo.isEach = + (int)mapNumberAt_(state, map, "isEach", 0); + const char *indexName = mapStringAt_(state, map, "indexName"); + if (indexName) + fd->controlFlow.foreachInfo.indexName = SYM(indexName); + const char *valueName = mapStringAt_(state, map, "valueName"); + if (valueName) + fd->controlFlow.foreachInfo.valueName = SYM(valueName); + IoObject *bodyMap = IoMap_rawAt(map, SYM("bodyMsg")); + fd->controlFlow.foreachInfo.bodyMsg = + deserializeMessage_(state, bodyMap); + break; + } + + case FRAME_STATE_CALLCC_EVAL_BLOCK: + break; + + default: + break; + } +} + +// Helper: deserialize a single frame from a Map +static IoEvalFrame *deserializeFrame_(IoState *state, IoObject *frameMap) { + if (!frameMap || frameMap == state->ioNil || !ISMAP(frameMap)) { + return NULL; + } + + IoMap *map = (IoMap *)frameMap; + IoEvalFrame *frame = IoEvalFrame_newWithState(state); + IoEvalFrameData *fd = FRAME_DATA(frame); + + const char *stateName = mapStringAt_(state, map, "state"); + fd->state = IoEvalFrame_stateFromName(stateName); + + IoObject *msgMap = IoMap_rawAt(map, SYM("message")); + if (msgMap && msgMap != state->ioNil) { + fd->message = deserializeMessage_(state, msgMap); + } + + fd->passStops = (int)mapNumberAt_(state, map, "passStops", 0); + fd->isNestedEvalRoot = (int)mapNumberAt_(state, map, "isNestedEvalRoot", 0); + + fd->argCount = (int)mapNumberAt_(state, map, "argCount", 0); + fd->currentArgIndex = (int)mapNumberAt_(state, map, "currentArgIndex", 0); + + IoObject *argList = IoMap_rawAt(map, SYM("argValues")); + if (argList && argList != state->ioNil && ISLIST(argList)) { + int argCount = (int)IoList_rawSize((IoList *)argList); + if (argCount > 0) { + fd->argValues = (IoObject **)io_calloc(argCount, sizeof(IoObject *)); + for (int i = 0; i < argCount; i++) { + fd->argValues[i] = IoList_rawAt_((IoList *)argList, i); + } + fd->currentArgIndex = argCount; + } + } + + fd->target = state->lobby; + fd->locals = state->lobby; + fd->cachedTarget = fd->target; + + IoObject *result = IoMap_rawAt(map, SYM("result")); + if (result && result != state->ioNil) { + fd->result = result; + } + + IoObject *slotValue = IoMap_rawAt(map, SYM("slotValue")); + if (slotValue && slotValue != state->ioNil) { + fd->slotValue = slotValue; + } + + IoObject *hasBlockLocals = IoMap_rawAt(map, SYM("hasBlockLocals")); + if (hasBlockLocals && hasBlockLocals == state->ioTrue) { + IoObject *blockLocals = IOCLONE(state->localsProto); + IoObject_isLocals_(blockLocals, 1); + + IoObject *slotsMap = IoMap_rawAt(map, SYM("blockLocalsSlots")); + if (slotsMap && slotsMap != state->ioNil && ISMAP(slotsMap)) { + IoObject_createSlotsIfNeeded(blockLocals); + PHash *bslots = IoObject_slots(blockLocals); + PHASH_FOREACH(IoObject_slots((IoObject *)slotsMap), k, v, + PHash_at_put_(bslots, k, v); + ); + } + + fd->blockLocals = blockLocals; + } + + deserializeControlFlow_(state, frame, map); + + return frame; +} + +IO_METHOD(IoContinuation, fromMap) { + /*doc Continuation fromMap(aMap) + Restores a Continuation from a Map representation produced by asMap. + Returns self with the restored continuation state. + */ + IoState *state = IOSTATE; + + IoObject *mapArg = IoMessage_locals_valueArgAt_(m, locals, 0); + if (state->errorRaised) return IONIL(self); + + if (!ISMAP(mapArg)) { + IoState_error_(state, m, "fromMap requires a Map argument"); + return IONIL(self); + } + IoMap *map = (IoMap *)mapArg; + + // Clear existing captured frames (GC handles old frames) + DATA(self)->capturedFrame = NULL; + + // Restore metadata + IoObject *invokedVal = IoMap_rawAt(map, SYM("invoked")); + DATA(self)->invoked = (invokedVal && invokedVal == state->ioTrue) ? 1 : 0; + + // Get frames list + IoObject *framesList = IoMap_rawAt(map, SYM("frames")); + if (!framesList || framesList == state->ioNil || !ISLIST(framesList)) { + DATA(self)->capturedFrame = NULL; + DATA(self)->capturedLocals = NULL; + return self; + } + + int frameCount = (int)IoList_rawSize((IoList *)framesList); + if (frameCount == 0) { + DATA(self)->capturedFrame = NULL; + DATA(self)->capturedLocals = NULL; + return self; + } + + // Build from the end (oldest) to front (newest) + IoEvalFrame *childFrame = NULL; + + for (int i = frameCount - 1; i >= 0; i--) { + IoObject *fMap = IoList_rawAt_((IoList *)framesList, i); + IoEvalFrame *frame = deserializeFrame_(state, fMap); + if (!frame) continue; + + FRAME_DATA(frame)->parent = childFrame; + childFrame = frame; + } + + DATA(self)->capturedFrame = childFrame; + + if (childFrame) { + DATA(self)->capturedLocals = FRAME_DATA(childFrame)->locals; + } + + return self; +} + +#undef SYM +#undef NUM + +// ============================================================ +// callcc - Call With Current Continuation +// ============================================================ + +IO_METHOD(IoObject, callcc) { + /*doc Object callcc(aBlock) + Calls aBlock with the current continuation as its argument. + The continuation captures the execution state at the point of + the callcc call. If the block returns normally, callcc returns + the block's return value. If the continuation is invoked with + a value, callcc returns that value instead. + + Example: + result := callcc(block(cont, + // This is the "escape" pattern + if(someCondition, + cont invoke("early exit") + ) + "normal return" + )) + // result is either "early exit" or "normal return" + */ + + IoState *state = IOSTATE; + IoEvalFrame *frame = state->currentFrame; + IoEvalFrameData *fd = FRAME_DATA(frame); + + if (frame == NULL) { + IoState_error_(state, m, + "callcc requires iterative evaluation mode."); + return IONIL(self); + } + + IoMessage *blockMsg = IoMessage_rawArgAt_(m, 0); + if (!blockMsg) { + IoState_error_(state, m, "callcc requires a block argument"); + return IONIL(self); + } + + IoObject *blockArg = IoMessage_locals_performOn_(blockMsg, locals, locals); + + if (!ISBLOCK(blockArg)) { + IoState_error_(state, m, "callcc requires a block argument"); + return IONIL(self); + } + + IoBlock *block = (IoBlock *)blockArg; + IoBlockData *blockData = (IoBlockData *)IoObject_dataPointer(block); + + IoObject *blockLocals = IOCLONE(state->localsProto); + IoObject_isLocals_(blockLocals, 1); + + IoObject *scope = blockData->scope ? blockData->scope : self; + + IoCall *callObject = IoCall_with( + state, locals, self, m, self, block, state->currentCoroutine + ); + + IoObject_createSlotsIfNeeded(blockLocals); + PHash *bslots = IoObject_slots(blockLocals); + PHash_at_put_(bslots, state->callSymbol, callObject); + PHash_at_put_(bslots, state->selfSymbol, scope); + PHash_at_put_(bslots, state->updateSlotSymbol, state->localsUpdateSlotCFunc); + + IoObject_isReferenced_(blockLocals, 0); + IoObject_isReferenced_(callObject, 0); + + // Set state BEFORE capturing so restored frame is in correct state + fd->state = FRAME_STATE_CALLCC_EVAL_BLOCK; + + // Capture frame stack — just grabs the pointer (no deep copy) + IoContinuation *cont = IoContinuation_new(state); + IoContinuation_captureFrameStack_(cont, frame, locals); + + fd->controlFlow.callccInfo.continuation = cont; + fd->controlFlow.callccInfo.blockLocals = blockLocals; + + List *argNames = blockData->argNames; + if (argNames && List_size(argNames) > 0) { + IoSymbol *paramName = List_at_(argNames, 0); + IoObject_setSlot_to_(blockLocals, paramName, cont); + } + + IoEvalFrame *blockFrame = IoState_pushFrame_(state); + IoEvalFrameData *blockFd = FRAME_DATA(blockFrame); + blockFd->message = blockData->message; + blockFd->target = blockLocals; + blockFd->locals = blockLocals; + blockFd->cachedTarget = blockLocals; + blockFd->state = FRAME_STATE_START; + blockFd->call = callObject; + blockFd->blockLocals = blockLocals; + blockFd->passStops = blockData->passStops; + + state->needsControlFlowHandling = 1; + + return state->ioNil; +} + +#endif /* IO_CALLCC */ diff --git a/libs/iovm/source/IoContinuation.h b/libs/iovm/source/IoContinuation.h new file mode 100644 index 000000000..d2e426327 --- /dev/null +++ b/libs/iovm/source/IoContinuation.h @@ -0,0 +1,74 @@ + +// metadoc Continuation copyright Steve Dekorte 2002, 2025 +// metadoc Continuation license BSD revised +// metadoc Continuation category Core +/*metadoc Continuation description +A first-class continuation that captures the execution state +(frame stack) at a point in time. When invoked, it restores +that state and continues execution from where it was captured. + +Continuations are one-shot by default. The escape pattern +(invoking from within the callcc block) works without copying. +For multi-shot or delayed invocation, use the copy method to +create an independent deep copy of the continuation first. +*/ + +#ifndef IoContinuation_DEFINED +#define IoContinuation_DEFINED 1 + +// callcc / first-class continuations are disabled by default. +// They deep-copy the entire frame stack and can rewind execution past +// cleanup code, corrupt handler stacks, and violate single-return +// assumptions. Enable with -DIO_CALLCC at build time if needed. +#ifdef IO_CALLCC + +#include "IoVMApi.h" +#include "IoObject.h" +#include "IoEvalFrame.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define ISCONTINUATION(self) \ + IoObject_hasCloneFunc_(self, (IoTagCloneFunc *)IoContinuation_rawClone) + +typedef IoObject IoContinuation; + +typedef struct { + IoEvalFrame *capturedFrame; // Captured frame stack (GC-managed, not deep copied) + IoObject *capturedLocals; // The locals where callcc was invoked + int invoked; // Has this continuation been invoked? +} IoContinuationData; + +// Proto and lifecycle +IOVM_API IoContinuation *IoContinuation_proto(void *state); +IOVM_API IoContinuation *IoContinuation_rawClone(IoContinuation *self); +IOVM_API IoContinuation *IoContinuation_new(void *state); +IOVM_API void IoContinuation_free(IoContinuation *self); +IOVM_API void IoContinuation_mark(IoContinuation *self); + +// Internal +IOVM_API void IoContinuation_captureFrameStack_(IoContinuation *self, + IoEvalFrame *frame, + IoObject *locals); + +// Methods +IOVM_API IO_METHOD(IoContinuation, invoke); +IOVM_API IO_METHOD(IoContinuation, isInvoked); +IOVM_API IO_METHOD(IoContinuation, copy); +IOVM_API IO_METHOD(IoContinuation, frameCount); +IOVM_API IO_METHOD(IoContinuation, frameStates); +IOVM_API IO_METHOD(IoContinuation, frameMessages); +IOVM_API IO_METHOD(IoContinuation, asMap); +IOVM_API IO_METHOD(IoContinuation, fromMap); + +// callcc primitive (on Object) +IOVM_API IO_METHOD(IoObject, callcc); + +#ifdef __cplusplus +} +#endif + +#endif /* IO_CALLCC */ +#endif diff --git a/libs/iovm/source/IoCoroutine.c b/libs/iovm/source/IoCoroutine.c index e246c7262..5d9744d6a 100644 --- a/libs/iovm/source/IoCoroutine.c +++ b/libs/iovm/source/IoCoroutine.c @@ -1,9 +1,10 @@ // metadoc Coroutine category Core -// metadoc Coroutine copyright Steve Dekorte 2002 +// metadoc Coroutine copyright Steve Dekorte 2002, 2025 // metadoc Coroutine license BSD revised /*metadoc Coroutine description Object wrapper for an Io coroutine. +Now implemented using frame-based evaluation (no platform-specific assembly). */ #include "IoCoroutine.h" @@ -14,9 +15,17 @@ Object wrapper for an Io coroutine. #include "IoNumber.h" #include "IoList.h" #include "IoBlock.h" +#include "IoEvalFrame.h" +#ifdef IO_CALLCC +#include "IoContinuation.h" +#endif +#include //#define DEBUG +// Define DEBUG_CORO_EVAL to enable verbose debug output +// #define DEBUG_CORO_EVAL 1 + static const char *protoId = "Coroutine"; #define DATA(self) ((IoCoroutineData *)IoObject_dataPointer(self)) @@ -50,12 +59,11 @@ IoCoroutine *IoCoroutine_proto(void *state) { #endif IoState_registerProtoWithId_((IoState *)state, self, protoId); - /* init Coroutine proto's coro as the main one */ - { - Coro *coro = Coro_new(); - DATA(self)->cid = coro; - Coro_initializeMainCoro(coro); - } + // Main coroutine: frameStack is NULL (its frames live in state->currentFrame) + DATA(self)->frameStack = NULL; + DATA(self)->stopStatus = MESSAGE_STOP_STATUS_NORMAL; + DATA(self)->returnValue = NULL; + DATA(self)->frameDepth = 0; return self; } @@ -72,6 +80,8 @@ void IoCoroutine_protoFinish(IoCoroutine *self) { {"setMessageDebugging", IoCoroutine_setMessageDebugging}, {"freeStack", IoCoroutine_freeStack}, {"setRecentInChain", IoCoroutine_setRecentInChain}, + {"rawSignalException", IoCoroutine_rawSignalException}, + {"currentFrame", IoCoroutine_currentFrame}, {NULL, NULL}, }; @@ -85,7 +95,10 @@ IoCoroutine *IoCoroutine_rawClone(IoCoroutine *proto) { #ifdef STACK_POP_CALLBACK Stack_popCallback_(DATA(self)->ioStack, IoObject_freeIfUnreferenced); #endif - DATA(self)->cid = (Coro *)NULL; + DATA(self)->frameStack = NULL; + DATA(self)->stopStatus = MESSAGE_STOP_STATUS_NORMAL; + DATA(self)->returnValue = NULL; + DATA(self)->frameDepth = 0; return self; } @@ -96,15 +109,43 @@ IoCoroutine *IoCoroutine_new(void *state) { } void IoCoroutine_free(IoCoroutine *self) { - Coro *coro = DATA(self)->cid; - if (coro) - Coro_free(coro); + // Frame stack is GC-managed, just null the pointer + DATA(self)->frameStack = NULL; Stack_free(DATA(self)->ioStack); io_free(DATA(self)); } void IoCoroutine_mark(IoCoroutine *self) { + IoState *state = IOSTATE; + + // Mark the ioStack (retain stack) Stack_do_(DATA(self)->ioStack, (ListDoCallback *)IoObject_shouldMark); + + // Mark the frame stack. + // Just mark the top frame — GC follows the parent chain + // via IoEvalFrame's own markFunc. + IoEvalFrame *frame = DATA(self)->frameStack; + if (self == state->currentCoroutine && state->currentFrame) { + frame = state->currentFrame; + } + IoObject_shouldMarkIfNonNull(frame); + + // Mark pooled frames so GC doesn't collect them while parked. + // Only do this when marking the current coroutine (avoids redundant work). + if (self == state->currentCoroutine) { + for (int i = 0; i < state->framePoolCount; i++) { + IoObject_shouldMarkIfNonNull(state->framePool[i]); + } + // Mark pooled blockLocals (they have PHash slots already allocated) + for (int i = 0; i < state->blockLocalsPoolSize; i++) { + IoObject_shouldMarkIfNonNull(state->blockLocalsPool[i]); + } + // Mark pooled Call objects (their IoCallData has pointer fields) + for (int i = 0; i < state->callPoolSize; i++) { + IoObject_shouldMarkIfNonNull(state->callPool[i]); + } + } + } // raw @@ -116,7 +157,27 @@ void IoCoroutine_rawShow(IoCoroutine *self) { printf("\n"); } -void *IoCoroutine_cid(IoCoroutine *self) { return DATA(self)->cid; } +IoEvalFrame *IoCoroutine_rawFrameStack(IoCoroutine *self) { + return DATA(self)->frameStack; +} + +// Save coroutine state from IoState +void IoCoroutine_saveState_(IoCoroutine *self, IoState *state) { + DATA(self)->frameStack = state->currentFrame; + DATA(self)->stopStatus = state->stopStatus; + DATA(self)->returnValue = state->returnValue; + DATA(self)->frameDepth = state->frameDepth; + // ioStack is already per-coroutine +} + +// Restore coroutine state to IoState +void IoCoroutine_restoreState_(IoCoroutine *self, IoState *state) { + state->currentFrame = DATA(self)->frameStack; + state->stopStatus = DATA(self)->stopStatus; + state->returnValue = DATA(self)->returnValue; + state->currentIoStack = DATA(self)->ioStack; + state->frameDepth = DATA(self)->frameDepth; +} /* // runTarget @@ -270,7 +331,19 @@ void IoCoroutine_rawSetResult_(IoCoroutine *self, IoObject *v) { } IoObject *IoCoroutine_rawResult(IoCoroutine *self) { - return IoObject_getSlot_(self, IOSYMBOL("result")); +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_rawResult: self=%p\n", (void*)self); + fflush(stderr); +#endif + + IoObject *result = IoObject_getSlot_(self, IOSYMBOL("result")); + +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_rawResult: result=%p\n", (void*)result); + fflush(stderr); +#endif + + return result; } // exception @@ -297,56 +370,170 @@ IO_METHOD(IoCoroutine, ioStack) { return IoList_newWithList_(IOSTATE, Stack_asList(DATA(self)->ioStack)); } +/* + * rawReturnToParent - Handle returning from a coroutine to its parent. + * + * In the frame-based model, this is called when we want to abort the current + * coroutine (usually due to an exception). It sets up the state so that when + * the current CFunction returns, the eval loop will see the parent's frames. + * + * NOTE: This does NOT involve any C stack manipulation. It just swaps frame + * stacks. The eval loop continues on the same C stack, processing parent's frames. + */ void IoCoroutine_rawReturnToParent(IoCoroutine *self) { - IoCoroutine *parent = IoCoroutine_rawParentCoroutine(self); + IoState *state = IOSTATE; - if (parent && ISCOROUTINE(parent)) { - IoCoroutine_rawResume(parent); - } else { - if (self == IOSTATE->mainCoroutine) { - printf("IoCoroutine error: attempt to return from main coro\n"); - exit(-1); + IoObject *exc = IoCoroutine_rawException(self); +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_rawReturnToParent: exception=%p, ioNil=%p\n", + (void*)exc, (void*)state->ioNil); + fflush(stderr); +#endif + + if (!ISNIL(exc)) { + // Only print backtrace when NOT in a nested eval. + // In nested evals (IoCoroutine_try), the caller (tryToPerform) + // checks for exceptions and handles backtrace printing via + // IoState_exception_. Printing here would cause infinite + // recursion: rawPrintBackTrace → tryToPerform → nested eval → + // error → rawReturnToParent → rawPrintBackTrace → ... + if (state->nestedEvalDepth == 0) { +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_rawReturnToParent: printing backtrace\n"); + fflush(stderr); +#endif + IoCoroutine_rawPrintBackTrace(self); +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_rawReturnToParent: backtrace done\n"); + fflush(stderr); +#endif } } - if (!ISNIL(IoCoroutine_rawException(self))) { - IoCoroutine_rawPrintBackTrace(self); + // In a nested eval (IoCoroutine_try), we need to handle two cases: + // 1. This coro was started via coro swap (parent has CORO_WAIT_CHILD + // frame with us as the child) — must restore parent's saved frames + // so the eval loop continues with the parent's frame stack. + // 2. This coro was started via needOwnEvalLoop (rawRun handles cleanup + // after evalLoop_ returns) — just pop all frames and return. + if (state->nestedEvalDepth > 0) { +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "rawReturnToParent: nestedEvalDepth=%d>0, self=%p, popping all frames\n", + state->nestedEvalDepth, (void*)self); + fflush(stderr); +#endif + while (state->currentFrame) { + IoState_popFrame_(state); + } + + // Check if we were started via coro swap: parent's saved frameStack + // should have a CORO_WAIT_CHILD frame with us as the child. + IoCoroutine *parent = IoCoroutine_rawParentCoroutine(self); + if (parent && ISCOROUTINE(parent)) { + IoEvalFrame *parentTopFrame = DATA(parent)->frameStack; + IoEvalFrameData *parentTopFd = FRAME_DATA(parentTopFrame); + if (parentTopFrame && + parentTopFd->state == FRAME_STATE_CORO_WAIT_CHILD && + parentTopFd->controlFlow.coroInfo.childCoroutine == (IoObject *)self) { +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "rawReturnToParent: CORO SWAP RESTORE — restoring parent coro\n"); + fflush(stderr); +#endif + // Save child state (for exception inspection by tryToPerform) + IoCoroutine_saveState_(self, state); + + // Restore parent's frames + IoState_setCurrentCoroutine_(state, parent); + IoCoroutine_restoreState_(parent, state); + + // Transition parent's CORO_WAIT_CHILD to CONTINUE_CHAIN + IoEvalFrame *parentFrame = state->currentFrame; + IoEvalFrameData *parentFd = FRAME_DATA(parentFrame); + if (parentFrame && parentFd->state == FRAME_STATE_CORO_WAIT_CHILD) { + parentFd->result = DATA(self)->returnValue + ? DATA(self)->returnValue + : state->ioNil; + parentFd->state = FRAME_STATE_CONTINUE_CHAIN; + } + // Don't return — let the eval loop continue with parent's frames + // But we DO need to return here because we're inside a CFunction + // that was called from the eval loop. The eval loop will pick up + // the restored parent frames on its next iteration. + return; + } + } + return; } - printf("IoCoroutine error: unable to auto abort coro %p by resuming parent " - "coro %s_%p\n", - (void *)self, IoObject_name(parent), (void *)parent); - exit(-1); -} + // Save our state +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_rawReturnToParent: saving state\n"); + fflush(stderr); +#endif + IoCoroutine_saveState_(self, state); -void IoCoroutine_coroStart(void *context) // Called by Coro_Start() -{ - IoCoroutine *self = (IoCoroutine *)context; - IoObject *result; + // Switch to parent coroutine + IoCoroutine *parent = IoCoroutine_rawParentCoroutine(self); +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_rawReturnToParent: parent=%p\n", (void*)parent); + fflush(stderr); +#endif - IoState_setCurrentCoroutine_(IOSTATE, self); - // printf("%p-%p start\n", (void *)self, (void *)DATA(self)->cid); - result = IoMessage_locals_performOn_(IOSTATE->mainMessage, self, self); + if (parent && ISCOROUTINE(parent)) { +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_rawReturnToParent: switching to parent\n"); + fflush(stderr); +#endif + IoState_setCurrentCoroutine_(state, parent); + IoCoroutine_restoreState_(parent, state); + + // If parent has a frame waiting for us, give it the result/exception + IoEvalFrame *parentFrame = state->currentFrame; + IoEvalFrameData *parentFd = FRAME_DATA(parentFrame); +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_rawReturnToParent: parentFrame=%p, state=%d\n", + (void*)parentFrame, parentFrame ? parentFd->state : -1); + fflush(stderr); +#endif - IoCoroutine_rawSetResult_(self, result); - IoCoroutine_rawReturnToParent(self); -} + if (parentFrame && parentFd->state == FRAME_STATE_CORO_WAIT_CHILD) { + parentFd->result = DATA(self)->returnValue + ? DATA(self)->returnValue + : state->ioNil; + parentFd->state = FRAME_STATE_CONTINUE_CHAIN; + } -/* -void IoCoroutine_coroStartWithContextAndCFunction(void *context, -CoroStartCallback *func) -{ - IoCoroutine *self = (IoCoroutine *)context; - IoObject *result; + // Signal to eval loop that frame stack changed - don't process stale frame + state->needsControlFlowHandling = 1; + +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_rawReturnToParent: done, needsControlFlowHandling=1\n"); + fflush(stderr); +#endif + } else { + if (self == state->mainCoroutine) { + printf("IoCoroutine error: attempt to return from main coro\n"); + exit(-1); + } + printf("IoCoroutine error: unable to return to parent coro from %p\n", + (void *)self); + exit(-1); + } +} - IoState_setCurrentCoroutine_(IOSTATE, self); - //printf("%p-%p start\n", (void *)self, (void *)DATA(self)->cid); - result = IoMessage_locals_performOn_(IOSTATE->mainMessage, self, self); +IO_METHOD(IoCoroutine, rawSignalException) { + /*doc Coroutine rawSignalException + Bridges an Io-level exception to the eval loop by setting errorRaised. + Called by raiseException when there is no parent coroutine to resume. + The exception should already be set on this coroutine via setException. + */ - IoCoroutine_rawSetResult_(self, result); - IoCoroutine_rawReturnToParent(self); + IoState *state = IOSTATE; + // The exception is already set on this coroutine (by raiseException). + // Signal the eval loop to unwind frames. + state->errorRaised = 1; + return self; } -*/ IO_METHOD(IoCoroutine, freeStack) { /*doc Coroutine freeStack @@ -355,9 +542,9 @@ IO_METHOD(IoCoroutine, freeStack) { IoCoroutine *current = IoState_currentCoroutine(IOSTATE); - if (current != self && DATA(self)->cid) { - Coro_free(DATA(self)->cid); - DATA(self)->cid = NULL; + if (current != self && DATA(self)->frameStack) { + // Frames are GC-managed, just drop the reference + DATA(self)->frameStack = NULL; } return self; @@ -377,38 +564,193 @@ IO_METHOD(IoCoroutine, main) { return IONIL(self); } -Coro *IoCoroutine_rawCoro(IoCoroutine *self) { return DATA(self)->cid; } - void IoCoroutine_clearStack(IoCoroutine *self) { Stack_clear(DATA(self)->ioStack); } +/* + * rawRun - Start running a coroutine. + * + * FRAME-BASED ARCHITECTURE: + * There is ONE eval loop running on the main C stack. This function does NOT + * call evalLoop_ for child coroutines - it just sets up their frame stack + * and returns. The existing eval loop will continue processing. + * + * For the MAIN coroutine (called at startup), we DO call evalLoop_ since + * there's no existing loop to return to. + * + * For CHILD coroutines (called from Io code), we: + * 1. Mark caller's frame as waiting for child + * 2. Save caller's frame stack + * 3. Push child's initial frame + * 4. Return - the eval loop continues with child's frames + * 5. When child's stack empties, eval loop returns to parent (see IoState_evalLoop_) + */ void IoCoroutine_rawRun(IoCoroutine *self) { + IoState *state = IOSTATE; + +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_rawRun: self=%p, mainCoro=%p\n", + (void*)self, (void*)state->mainCoroutine); + fflush(stderr); +#endif + IoCoroutine_rawSetRecentInChain_(self, self); - Coro *coro = DATA(self)->cid; + // Get the run parameters + IoObject *runTarget = IoCoroutine_rawRunTarget(self); + IoObject *runLocals = IoCoroutine_rawRunLocals(self); + IoMessage *runMessage = IoCoroutine_rawRunMessage(self); + +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_rawRun: target=%p, locals=%p, msg=%p\n", + (void*)runTarget, (void*)runLocals, (void*)runMessage); + fflush(stderr); +#endif - if (!coro) { - coro = Coro_new(); - DATA(self)->cid = coro; + if (!runTarget || !runLocals || !runMessage) { + printf("IoCoroutine_rawRun: missing runTarget, runLocals, or runMessage\n"); + return; } - { - IoObject *stackSize = IoObject_getSlot_(self, IOSTATE->stackSizeSymbol); + // Validate that runMessage is actually a Message + if (!ISMESSAGE(runMessage)) { + fprintf(stderr, "BUG: IoCoroutine_rawRun: runMessage is NOT a Message!\n"); + fprintf(stderr, " runMessage=%p, tag=%s\n", + (void*)runMessage, + IoObject_tag((IoObject*)runMessage) ? IoObject_tag((IoObject*)runMessage)->name : "NULL"); + fprintf(stderr, " runTarget=%p, runLocals=%p\n", + (void*)runTarget, (void*)runLocals); + fprintf(stderr, " self=%p (coro)\n", (void*)self); + fflush(stderr); + abort(); + } + + IoCoroutine *current = IoState_currentCoroutine(state); + +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_rawRun: current=%p, currentFrame=%p\n", + (void*)current, (void*)state->currentFrame); + fflush(stderr); +#endif + + // Check if we need to run our own eval loop. + // This happens when: + // - This is the main coroutine being started + // - No coroutine is current + // - The current coroutine has no active frames (no eval loop running) + int needOwnEvalLoop = (self == state->mainCoroutine) || + (current == NULL) || + (state->currentFrame == NULL); + +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_rawRun: needOwnEvalLoop=%d\n", needOwnEvalLoop); + fflush(stderr); +#endif + + if (needOwnEvalLoop) { + // No eval loop is running - we need to run one ourselves + IoCoroutine *previousCoro = current; + + DATA(self)->frameStack = NULL; + DATA(self)->stopStatus = MESSAGE_STOP_STATUS_NORMAL; + DATA(self)->returnValue = NULL; + + IoState_setCurrentCoroutine_(state, self); + state->currentFrame = NULL; + state->frameDepth = 0; + state->currentIoStack = DATA(self)->ioStack; + + // Set parent if we have one + if (previousCoro && previousCoro != self) { + IoCoroutine_rawSetParentCoroutine_(self, previousCoro); + } + + // Push initial frame + IoEvalFrame *frame = IoState_pushFrame_(state); + IoEvalFrameData *fd = FRAME_DATA(frame); + fd->message = runMessage; + fd->target = runTarget; + fd->locals = runLocals; + fd->cachedTarget = runTarget; + fd->state = FRAME_STATE_START; + + // Run the eval loop + state->nestedEvalDepth++; + IoObject *result = IoState_evalLoop_(state); + state->nestedEvalDepth--; + IoCoroutine_rawSetResult_(self, result); - if (ISNUMBER(stackSize)) { - Coro_setStackSize_(coro, CNUMBER(stackSize)); + // Restore previous coroutine if any + if (previousCoro && previousCoro != self) { + IoState_setCurrentCoroutine_(state, previousCoro); + IoCoroutine_restoreState_(previousCoro, state); } + return; } - { - IoCoroutine *current = IoState_currentCoroutine(IOSTATE); - Coro *currentCoro = IoCoroutine_rawCoro(current); - // IoState_stackRetain_(IOSTATE, self); - Coro_startCoro_(currentCoro, coro, self, - (CoroStartCallback *)IoCoroutine_coroStart); - // IoState_setCurrentCoroutine_(IOSTATE, current); + // Child coroutine - set up frame stack swap +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_rawRun: setting up child coro swap\n"); + fflush(stderr); +#endif + + // Mark caller's frame as waiting for this child to complete + IoEvalFrame *callerFrame = state->currentFrame; + IoEvalFrameData *callerFd = FRAME_DATA(callerFrame); +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_rawRun: callerFrame=%p\n", (void*)callerFrame); + fflush(stderr); +#endif + + if (callerFrame) { + callerFd->state = FRAME_STATE_CORO_WAIT_CHILD; + callerFd->controlFlow.coroInfo.childCoroutine = self; } + + // Save current coroutine's frame stack +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_rawRun: saving current coro state\n"); + fflush(stderr); +#endif + IoCoroutine_saveState_(current, state); + + // Set up child coroutine + IoCoroutine_rawSetParentCoroutine_(self, current); + DATA(self)->frameStack = NULL; + DATA(self)->stopStatus = MESSAGE_STOP_STATUS_NORMAL; + DATA(self)->returnValue = NULL; + + // Switch to child coroutine +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_rawRun: switching to child\n"); + fflush(stderr); +#endif + IoState_setCurrentCoroutine_(state, self); + state->currentFrame = NULL; + state->frameDepth = 0; + state->currentIoStack = DATA(self)->ioStack; + + // Push child's initial frame + IoEvalFrame *frame = IoState_pushFrame_(state); + IoEvalFrameData *fd = FRAME_DATA(frame); + fd->message = runMessage; + fd->target = runTarget; + fd->locals = runLocals; + fd->cachedTarget = runTarget; + fd->state = FRAME_STATE_START; + +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "rawRun CHILD ROOT FRAME SET: frame=%p, msg=%p, target=%p, locals=%p\n", + (void*)frame, (void*)runMessage, (void*)runTarget, (void*)runLocals); + fflush(stderr); +#endif + + // Signal that we set up control flow - don't process return value normally + state->needsControlFlowHandling = 1; + + // Return - the eval loop will continue with child's frame stack + // When child's stack empties, eval loop will switch back to parent } IO_METHOD(IoCoroutine, run) { @@ -416,19 +758,133 @@ IO_METHOD(IoCoroutine, run) { Runs receiver and returns self. */ + IoState *state = IOSTATE; +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_run: calling rawRun, currentFrame=%p\n", + (void*)state->currentFrame); + fflush(stderr); +#endif IoCoroutine_rawRun(self); - return IoCoroutine_rawResult(self); +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_run: rawRun returned, getting result\n"); + fflush(stderr); +#endif + IoObject *result = IoCoroutine_rawResult(self); +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_run: got result=%p, returning\n", (void*)result); + fflush(stderr); +#endif + return result; } void IoCoroutine_try(IoCoroutine *self, IoObject *target, IoObject *locals, IoMessage *message) { - IoCoroutine *currentCoro = - (IoCoroutine *)IoState_currentCoroutine((IoState *)IOSTATE); + IoState *state = IOSTATE; + +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_try: self=%p, entering\n", (void*)self); + fflush(stderr); +#endif + + IoCoroutine *currentCoro = (IoCoroutine *)IoState_currentCoroutine(state); + + // Validate message parameter + if (!ISMESSAGE(message)) { + fprintf(stderr, "BUG: IoCoroutine_try: message is NOT a Message!\n"); + fprintf(stderr, " message=%p, tag=%s\n", + (void*)message, + IoObject_tag((IoObject*)message) ? IoObject_tag((IoObject*)message)->name : "NULL"); + fflush(stderr); + abort(); + } + IoCoroutine_rawSetRunTarget_(self, target); IoCoroutine_rawSetRunLocals_(self, locals); IoCoroutine_rawSetRunMessage_(self, message); IoCoroutine_rawSetParentCoroutine_(self, currentCoro); - IoCoroutine_rawRun(self); + + // Verify runMessage was stored correctly + IoMessage *storedMsg = IoCoroutine_rawRunMessage(self); + if (storedMsg != message) { + fprintf(stderr, "BUG: IoCoroutine_try: stored runMessage differs!\n"); + fprintf(stderr, " expected=%p, got=%p\n", (void*)message, (void*)storedMsg); + if (storedMsg && !ISMESSAGE(storedMsg)) { + fprintf(stderr, " stored tag=%s\n", + IoObject_tag((IoObject*)storedMsg) ? IoObject_tag((IoObject*)storedMsg)->name : "NULL"); + } + fflush(stderr); + abort(); + } + + // Check if we're inside an existing eval loop. + // If so, we need to run a nested loop to ensure synchronous completion. + // IoCoroutine_try semantics require the coroutine to be done when this returns. + int needNestedLoop = (state->currentFrame != NULL); + + if (needNestedLoop) { +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_try: running nested eval for synchronous completion\n"); + fflush(stderr); +#endif + + // Save current coroutine's state + IoCoroutine_saveState_(currentCoro, state); + + // Initialize the try coroutine + DATA(self)->frameStack = NULL; + DATA(self)->stopStatus = MESSAGE_STOP_STATUS_NORMAL; + DATA(self)->returnValue = NULL; + + // Switch to try coroutine + IoState_setCurrentCoroutine_(state, self); + state->currentFrame = NULL; + state->frameDepth = 0; + state->currentIoStack = DATA(self)->ioStack; + + // Push initial frame + IoEvalFrame *frame = IoState_pushFrame_(state); + IoEvalFrameData *fd = FRAME_DATA(frame); + fd->message = message; + fd->target = target; + fd->locals = locals; + fd->cachedTarget = target; + fd->state = FRAME_STATE_START; + +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "try NESTED ROOT FRAME SET: frame=%p, msg=%p, target=%p, locals=%p\n", + (void*)frame, (void*)message, (void*)target, (void*)locals); + fflush(stderr); +#endif + + // Mark that we're in a nested eval - so evalLoop knows not to do coro switching. + state->nestedEvalDepth++; + + // Run nested eval loop - this will complete the try coroutine + IoObject *result = IoState_evalLoop_(state); + IoCoroutine_rawSetResult_(self, result); + + // Done with nested eval + state->nestedEvalDepth--; + + // Restore parent coroutine's state + IoState_setCurrentCoroutine_(state, currentCoro); + IoCoroutine_restoreState_(currentCoro, state); + +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_try: nested eval completed, result=%p\n", (void*)result); + fprintf(stderr, "IoCoroutine_try: restored to coro=%p, frame=%p\n", + (void*)currentCoro, (void*)state->currentFrame); + fflush(stderr); +#endif + } else { + // Must increment nestedEvalDepth so rawReturnToParent pops frames + // instead of doing a full parent-coro switch (which corrupts state + // while CFunction is still on C stack). rawRun's own eval loop will + // exit naturally, and rawRun handles coro restoration itself. + state->nestedEvalDepth++; + IoCoroutine_rawRun(self); + state->nestedEvalDepth--; + } } IoCoroutine *IoCoroutine_newWithTry(void *state, IoObject *target, @@ -440,6 +896,14 @@ IoCoroutine *IoCoroutine_newWithTry(void *state, IoObject *target, void IoCoroutine_raiseError(IoCoroutine *self, IoSymbol *description, IoMessage *m) { +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoCoroutine_raiseError: self=%p, error=%s\n", + (void*)self, CSTRING(description)); + fflush(stderr); +#endif + + // Just create the exception — no unwinding. + // The eval loop handles frame unwinding when it sees errorRaised. IoObject *e = IoObject_rawGetSlot_(self, IOSYMBOL("Exception")); if (e) { @@ -450,26 +914,82 @@ void IoCoroutine_raiseError(IoCoroutine *self, IoSymbol *description, IoObject_setSlot_to_(e, IOSYMBOL("coroutine"), self); IoCoroutine_rawSetException_(self, e); } - - IoCoroutine_rawReturnToParent(self); } // methods +/* + * rawResume - Resume a suspended coroutine. + * + * This swaps frame stacks: the current coroutine is suspended, and the + * target coroutine is resumed. The eval loop continues processing + * whichever frames are now in state->currentFrame. + */ IoObject *IoCoroutine_rawResume(IoCoroutine *self) { + IoState *state = IOSTATE; + IoCoroutine *current = IoState_currentCoroutine(state); + IoCoroutine_rawSetRecentInChain_(self, self); - if (DATA(self)->cid) { - IoCoroutine *current = IoState_currentCoroutine(IOSTATE); - IoState_setCurrentCoroutine_(IOSTATE, self); - // printf("IoCoroutine resuming %p\n", (void *)self); - Coro_switchTo_(IoCoroutine_rawCoro(current), IoCoroutine_rawCoro(self)); + // Can't resume self + if (self == current) { + return self; + } - // IoState_setCurrentCoroutine_(IOSTATE, current); - } else { - // printf("IoCoroutine_rawResume: can't resume coro that hasn't been run - // - so running it\n"); - IoCoroutine_rawRun(self); + if (DATA(self)->frameStack || DATA(self)->frameStack == NULL) { + // Either resuming a suspended coro OR starting a fresh one + + // Mark current frame as yielded (so we resume here when switched back) + IoEvalFrame *callerFrame = state->currentFrame; + IoEvalFrameData *callerFd = FRAME_DATA(callerFrame); + if (callerFrame) { + callerFd->state = FRAME_STATE_CORO_YIELDED; + } + + // Save current coroutine's state + IoCoroutine_saveState_(current, state); + + // Restore target coroutine's state + IoState_setCurrentCoroutine_(state, self); + IoCoroutine_restoreState_(self, state); + + // If target has no frames (never run), start it + if (state->currentFrame == NULL) { + IoObject *runTarget = IoCoroutine_rawRunTarget(self); + IoObject *runLocals = IoCoroutine_rawRunLocals(self); + IoMessage *runMessage = IoCoroutine_rawRunMessage(self); + + if (runTarget && runLocals && runMessage) { + if (!ISMESSAGE(runMessage)) { + // Can't start this coro - no valid runMessage. + // This can happen if 'resume' is called on the Coroutine proto + // (which has nil run parameters from ::= declarations). + // Just restore the caller and skip. +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "WARNING: rawResume: can't start coro %p (runMessage not a Message, is %s). Restoring caller.\n", + (void*)self, + IoObject_tag((IoObject*)runMessage) ? IoObject_tag((IoObject*)runMessage)->name : "NULL"); + fflush(stderr); +#endif + // Restore calling coroutine + IoState_setCurrentCoroutine_(state, current); + IoCoroutine_restoreState_(current, state); + state->needsControlFlowHandling = 0; + return self; + } + IoCoroutine_rawSetParentCoroutine_(self, current); + IoEvalFrame *frame = IoState_pushFrame_(state); + IoEvalFrameData *fd = FRAME_DATA(frame); + fd->message = runMessage; + fd->target = runTarget; + fd->locals = runLocals; + fd->cachedTarget = runTarget; + fd->state = FRAME_STATE_START; + } + } + + // Signal control flow handling + state->needsControlFlowHandling = 1; } return self; @@ -486,10 +1006,10 @@ IO_METHOD(IoCoroutine, resume) { IO_METHOD(IoCoroutine, implementation) { /*doc Coroutine implementation - Returns coroutine implementation type: "fibers", "ucontext" or "setjmp" + Returns coroutine implementation type: "frame-based" (portable, no assembly) */ - return IOSYMBOL(CORO_IMPLEMENTATION); + return IOSYMBOL("frame-based"); } IO_METHOD(IoCoroutine, isCurrent) { @@ -516,12 +1036,14 @@ int IoCoroutine_rawIoStackSize(IoCoroutine *self) { } void IoCoroutine_rawPrint(IoCoroutine *self) { - Coro *coro = DATA(self)->cid; - - if (coro) { - printf("Coroutine_%p with cid %p ioStackSize %i\n", (void *)self, - (void *)coro, (int)Stack_count(DATA(self)->ioStack)); + int frameCount = 0; + IoEvalFrame *f = DATA(self)->frameStack; + while (f) { + frameCount++; + f = FRAME_DATA(f)->parent; } + printf("Coroutine_%p frameDepth %d ioStackSize %i\n", (void *)self, + frameCount, (int)Stack_count(DATA(self)->ioStack)); } // debugging @@ -546,6 +1068,21 @@ IO_METHOD(IoCoroutine, setMessageDebugging) { return self; } +IO_METHOD(IoCoroutine, currentFrame) { + /*doc Coroutine currentFrame + Returns the current (topmost) EvalFrame of this coroutine, + or nil if no frames are active. Only works for the currently + executing coroutine. + */ + IoState *state = IOSTATE; + if (self == state->currentCoroutine && state->currentFrame) { + return state->currentFrame; + } + // For non-current coroutines, the frame stack is saved in the coro data + IoEvalFrame *f = DATA(self)->frameStack; + return f ? (IoObject *)f : IONIL(self); +} + IoObject *IoObject_performWithDebugger(IoCoroutine *self, IoObject *locals, IoMessage *m) { IoState *state = IOSTATE; @@ -575,13 +1112,14 @@ IoObject *IoObject_performWithDebugger(IoCoroutine *self, IoObject *locals, } void IoCoroutine_rawPrintBackTrace(IoCoroutine *self) { + IoState *state = IOSTATE; IoObject *e = IoCoroutine_rawException(self); IoMessage *caughtMessage = IoObject_rawGetSlot_(e, IOSYMBOL("caughtMessage")); if (IoObject_rawGetSlot_(e, IOSYMBOL("showStack"))) // sanity check { - IoState_on_doCString_withLabel_(IOSTATE, e, "showStack", "[Coroutine]"); + IoState_on_doCString_withLabel_(state, e, "showStack", "[Coroutine]"); } else { IoSymbol *error = IoObject_rawGetSlot_(e, IOSYMBOL("error")); diff --git a/libs/iovm/source/IoCoroutine.h b/libs/iovm/source/IoCoroutine.h index b53f1f8d6..99563ce13 100644 --- a/libs/iovm/source/IoCoroutine.h +++ b/libs/iovm/source/IoCoroutine.h @@ -6,23 +6,50 @@ #define IoCoroutine_DEFINED 1 #include "IoVMApi.h" -#include "IoState.h" - #include "Common.h" -#include "Coro.h" +#include "Stack.h" +#include "IoObject_struct.h" +#include "IoMessage.h" +#include "IoSeq.h" #ifdef __cplusplus extern "C" { #endif +// Forward declaration to avoid circular includes +struct IoState; + #define ISCOROUTINE(self) \ IoObject_hasCloneFunc_(self, (IoTagCloneFunc *)IoCoroutine_rawClone) typedef IoObject IoCoroutine; +/* + * Frame-based coroutine data structure. + * + * ARCHITECTURE NOTE: + * The iterative evaluator runs a SINGLE eval loop on the main C stack. + * Coroutines are implemented by swapping which heap-allocated frame stack + * the eval loop processes. There is NO C stack switching (no setjmp/longjmp, + * no ucontext, no fibers). + * + * When a coroutine is RUNNING: + * - frameStack is NULL + * - Its frames are in state->currentFrame + * + * When a coroutine is SUSPENDED: + * - frameStack points to its saved frame stack + * - state->currentFrame belongs to whoever is running + * + * Switching coroutines = save current frames, restore target frames, return. + * The eval loop continues processing whatever frames are now current. + */ typedef struct { - Coro *cid; - Stack *ioStack; + IoObject *frameStack; // Suspended frame stack (IoEvalFrame, NULL if running) + Stack *ioStack; // Retain stack for GC + int stopStatus; // Per-coroutine stop status + IoObject *returnValue; // Per-coroutine return value + int frameDepth; // Saved frame depth (avoids O(n) recalc on switch) int debuggingOn; } IoCoroutineData; @@ -39,7 +66,10 @@ IOVM_API void IoCoroutine_rawShow(IoCoroutine *self); IOVM_API IO_METHOD(IoCoroutine, main); IOVM_API IO_METHOD(IoCoroutine, freeStack); -IOVM_API void *IoCoroutine_cid(IoCoroutine *self); +// Frame-based coroutine state management +IOVM_API void IoCoroutine_saveState_(IoCoroutine *self, struct IoState *state); +IOVM_API void IoCoroutine_restoreState_(IoCoroutine *self, struct IoState *state); +IOVM_API IoObject *IoCoroutine_rawFrameStack(IoCoroutine *self); // label @@ -150,6 +180,9 @@ IOVM_API IoObject *IoObject_performWithDebugger(IoCoroutine *self, IOVM_API IO_METHOD(IoCoroutine, callStack); IOVM_API void IoCoroutine_rawPrintBackTrace(IoCoroutine *self); +IOVM_API IO_METHOD(IoCoroutine, rawSignalException); +IOVM_API IO_METHOD(IoCoroutine, currentFrame); + #ifdef __cplusplus } #endif diff --git a/libs/iovm/source/IoDate.c b/libs/iovm/source/IoDate.c index 792081a03..cc7062d02 100644 --- a/libs/iovm/source/IoDate.c +++ b/libs/iovm/source/IoDate.c @@ -157,6 +157,8 @@ IoDate *IoDate_fromSerialization(IoDate *self, IoObject *locals, IoMessage *m) { IoState_error_( IOSTATE, self, "Expected a serialization sequence comprising 4 int32 items."); + UArray_free(serialization); + return self; } Date_fromSerialization(DATA(self), serialization); @@ -202,6 +204,7 @@ IO_METHOD(IoDate, cpuSecondsToRun) { */ IoMessage_assertArgCount_receiver_(m, 1, self); + if (IOSTATE->errorRaised) return IONUMBER(0); { double t2, t1 = clock(); @@ -636,6 +639,7 @@ IO_METHOD(IoDate, fromString) { */ IoMessage_assertArgCount_receiver_(m, 2, self); + if (IOSTATE->errorRaised) return self; { IoSymbol *date_input = IoMessage_locals_seqArgAt_(m, locals, 0); IoSymbol *format = IoMessage_locals_seqArgAt_(m, locals, 1); diff --git a/libs/iovm/source/IoDirectory.c b/libs/iovm/source/IoDirectory.c index a141bc4ce..19320e0db 100644 --- a/libs/iovm/source/IoDirectory.c +++ b/libs/iovm/source/IoDirectory.c @@ -356,6 +356,7 @@ IO_METHOD(IoDirectory, items) { if (!dirp) { IoState_error_(IOSTATE, m, "Unable to open directory %s", UTF8CSTRING(DATA(self)->path)); + return IONIL(self); } while ((dp = readdir(dirp)) != NULL) { @@ -436,6 +437,7 @@ IO_METHOD(IoDirectory, itemNamed) { IoState_error_(IOSTATE, m, "Unable to open directory %s", UTF8CSTRING(DATA(self)->path)); + return IONIL(self); } while ((dp = readdir(dirp)) != NULL) @@ -510,6 +512,7 @@ IO_METHOD(IoDirectory, size) { if (!dirp) { IoState_error_(IOSTATE, m, "Unable to open directory %s", UTF8CSTRING(DATA(self)->path)); + return IONIL(self); } while ((dp = readdir(dirp)) != NULL) { diff --git a/libs/iovm/source/IoEvalFrame.c b/libs/iovm/source/IoEvalFrame.c new file mode 100644 index 000000000..b2cc2bb32 --- /dev/null +++ b/libs/iovm/source/IoEvalFrame.c @@ -0,0 +1,363 @@ + +// metadoc EvalFrame copyright Steve Dekorte 2002, 2025 +// metadoc EvalFrame license BSD revised +// metadoc EvalFrame category Core + +#include "IoEvalFrame.h" +#include "IoState.h" +#include "IoNumber.h" +#include +#include + +static const char *protoId = "EvalFrame"; + +IoTag *IoEvalFrame_newTag(void *state) { + IoTag *tag = IoTag_newWithName_(protoId); + IoTag_state_(tag, state); + IoTag_cloneFunc_(tag, (IoTagCloneFunc *)IoEvalFrame_rawClone); + IoTag_markFunc_(tag, (IoTagMarkFunc *)IoEvalFrame_mark); + IoTag_freeFunc_(tag, (IoTagFreeFunc *)IoEvalFrame_free); + return tag; +} + +IoEvalFrame *IoEvalFrame_proto(void *vState) { + IoState *state = (IoState *)vState; + IoObject *self = IoObject_new(state); + + IoObject_setDataPointer_(self, io_calloc(1, sizeof(IoEvalFrameData))); + IoObject_tag_(self, IoEvalFrame_newTag(state)); + + IoEvalFrameData *fd = FRAME_DATA(self); + memset(fd, 0, sizeof(IoEvalFrameData)); + + IoState_registerProtoWithId_(state, self, protoId); + + { + IoMethodTable methodTable[] = { + {"message", IoEvalFrame_message}, + {"target", IoEvalFrame_target}, + {"locals", IoEvalFrame_localContext}, + {"state", IoEvalFrame_state}, + {"parent", IoEvalFrame_parent}, + {"result", IoEvalFrame_result}, + {"depth", IoEvalFrame_depth}, + {"call", IoEvalFrame_call}, + {"blockLocals", IoEvalFrame_blockLocals}, + {"description", IoEvalFrame_description}, + {NULL, NULL}, + }; + IoObject_addMethodTable_(self, methodTable); + } + + return self; +} + +IoEvalFrame *IoEvalFrame_rawClone(IoEvalFrame *proto) { + IoObject *self = IoObject_rawClonePrimitive(proto); + IoObject_setDataPointer_(self, io_calloc(1, sizeof(IoEvalFrameData))); + + IoEvalFrameData *fd = FRAME_DATA(self); + memset(fd, 0, sizeof(IoEvalFrameData)); + + return self; +} + +IoEvalFrame *IoEvalFrame_newWithState(void *vState) { + IoObject *proto = IoState_protoWithId_((IoState *)vState, protoId); + return IOCLONE(proto); +} + +// Free an evaluation frame's data (tag freeFunc) +void IoEvalFrame_free(IoEvalFrame *self) { + IoEvalFrameData *fd = (IoEvalFrameData *)IoObject_dataPointer(self); + if (fd) { + if (fd->argValues && fd->argValues != fd->inlineArgs) { + io_free(fd->argValues); + } + io_free(fd); + } +} + +// Mark a frame's contents for garbage collection (tag markFunc) +// Marks the parent frame as an IoObject — GC follows the chain transitively. +void IoEvalFrame_mark(IoEvalFrame *self) { + IoEvalFrameData *fd = (IoEvalFrameData *)IoObject_dataPointer(self); + if (!fd) return; + + // Mark parent frame (GC follows the chain via parent's own markFunc) + IoObject_shouldMarkIfNonNull(fd->parent); + + // Mark all IoObject pointers + IoObject_shouldMarkIfNonNull(fd->message); + IoObject_shouldMarkIfNonNull(fd->target); + IoObject_shouldMarkIfNonNull(fd->locals); + IoObject_shouldMarkIfNonNull(fd->cachedTarget); + IoObject_shouldMarkIfNonNull(fd->result); + IoObject_shouldMarkIfNonNull(fd->slotValue); + IoObject_shouldMarkIfNonNull(fd->slotContext); + IoObject_shouldMarkIfNonNull(fd->call); + IoObject_shouldMarkIfNonNull(fd->savedCall); + IoObject_shouldMarkIfNonNull(fd->blockLocals); + + // Mark evaluated arguments + if (fd->argValues) { + int i; + for (i = 0; i < fd->currentArgIndex; i++) { + IoObject_shouldMarkIfNonNull(fd->argValues[i]); + } + } + + // Mark control flow continuation info based on current state + switch (fd->state) { + case FRAME_STATE_IF_EVAL_CONDITION: + case FRAME_STATE_IF_CONVERT_BOOLEAN: + case FRAME_STATE_IF_EVAL_BRANCH: + IoObject_shouldMarkIfNonNull(fd->controlFlow.ifInfo.conditionMsg); + IoObject_shouldMarkIfNonNull(fd->controlFlow.ifInfo.trueBranch); + IoObject_shouldMarkIfNonNull(fd->controlFlow.ifInfo.falseBranch); + break; + + case FRAME_STATE_WHILE_EVAL_CONDITION: + case FRAME_STATE_WHILE_CHECK_CONDITION: + case FRAME_STATE_WHILE_DECIDE: + case FRAME_STATE_WHILE_EVAL_BODY: + IoObject_shouldMarkIfNonNull(fd->controlFlow.whileInfo.conditionMsg); + IoObject_shouldMarkIfNonNull(fd->controlFlow.whileInfo.bodyMsg); + IoObject_shouldMarkIfNonNull(fd->controlFlow.whileInfo.lastResult); + break; + + case FRAME_STATE_LOOP_EVAL_BODY: + case FRAME_STATE_LOOP_AFTER_BODY: + IoObject_shouldMarkIfNonNull(fd->controlFlow.loopInfo.bodyMsg); + IoObject_shouldMarkIfNonNull(fd->controlFlow.loopInfo.lastResult); + break; + + case FRAME_STATE_FOR_EVAL_SETUP: + case FRAME_STATE_FOR_EVAL_BODY: + case FRAME_STATE_FOR_AFTER_BODY: + IoObject_shouldMarkIfNonNull(fd->controlFlow.forInfo.bodyMsg); + IoObject_shouldMarkIfNonNull(fd->controlFlow.forInfo.counterName); + IoObject_shouldMarkIfNonNull(fd->controlFlow.forInfo.lastResult); + break; + + case FRAME_STATE_FOREACH_EVAL_BODY: + case FRAME_STATE_FOREACH_AFTER_BODY: + IoObject_shouldMarkIfNonNull(fd->controlFlow.foreachInfo.collection); + IoObject_shouldMarkIfNonNull(fd->controlFlow.foreachInfo.bodyMsg); + IoObject_shouldMarkIfNonNull(fd->controlFlow.foreachInfo.indexName); + IoObject_shouldMarkIfNonNull(fd->controlFlow.foreachInfo.valueName); + IoObject_shouldMarkIfNonNull(fd->controlFlow.foreachInfo.lastResult); + IoObject_shouldMarkIfNonNull(fd->controlFlow.foreachInfo.mapSource); + break; + +#ifdef IO_CALLCC + case FRAME_STATE_CALLCC_EVAL_BLOCK: + IoObject_shouldMarkIfNonNull(fd->controlFlow.callccInfo.continuation); + IoObject_shouldMarkIfNonNull(fd->controlFlow.callccInfo.blockLocals); + break; +#endif + + case FRAME_STATE_CORO_WAIT_CHILD: + case FRAME_STATE_CORO_YIELDED: + IoObject_shouldMarkIfNonNull(fd->controlFlow.coroInfo.childCoroutine); + break; + + case FRAME_STATE_DO_EVAL: + case FRAME_STATE_DO_WAIT: + IoObject_shouldMarkIfNonNull(fd->controlFlow.doInfo.codeMessage); + IoObject_shouldMarkIfNonNull(fd->controlFlow.doInfo.evalTarget); + IoObject_shouldMarkIfNonNull(fd->controlFlow.doInfo.evalLocals); + break; + + default: + break; + } +} + +// Return a human-readable name for a frame state +const char *IoEvalFrame_stateName(IoFrameState state) { + switch (state) { + case FRAME_STATE_START: return "start"; + case FRAME_STATE_EVAL_ARGS: return "evalArgs"; + case FRAME_STATE_LOOKUP_SLOT: return "lookupSlot"; + case FRAME_STATE_ACTIVATE: return "activate"; + case FRAME_STATE_CONTINUE_CHAIN: return "continueChain"; + case FRAME_STATE_RETURN: return "return"; + case FRAME_STATE_IF_EVAL_CONDITION: return "if:evalCondition"; + case FRAME_STATE_IF_CONVERT_BOOLEAN: return "if:convertBoolean"; + case FRAME_STATE_IF_EVAL_BRANCH: return "if:evalBranch"; + case FRAME_STATE_WHILE_EVAL_CONDITION: return "while:evalCondition"; + case FRAME_STATE_WHILE_CHECK_CONDITION: return "while:checkCondition"; + case FRAME_STATE_WHILE_DECIDE: return "while:decide"; + case FRAME_STATE_WHILE_EVAL_BODY: return "while:evalBody"; + case FRAME_STATE_LOOP_EVAL_BODY: return "loop:evalBody"; + case FRAME_STATE_LOOP_AFTER_BODY: return "loop:afterBody"; + case FRAME_STATE_FOR_EVAL_SETUP: return "for:evalSetup"; + case FRAME_STATE_FOR_EVAL_BODY: return "for:evalBody"; + case FRAME_STATE_FOR_AFTER_BODY: return "for:afterBody"; + case FRAME_STATE_FOREACH_EVAL_BODY: return "foreach:evalBody"; + case FRAME_STATE_FOREACH_AFTER_BODY: return "foreach:afterBody"; +#ifdef IO_CALLCC + case FRAME_STATE_CALLCC_EVAL_BLOCK: return "callcc:evalBlock"; +#endif + case FRAME_STATE_CORO_WAIT_CHILD: return "coro:waitChild"; + case FRAME_STATE_CORO_YIELDED: return "coro:yielded"; + case FRAME_STATE_DO_EVAL: return "do:eval"; + case FRAME_STATE_DO_WAIT: return "do:wait"; + default: return "unknown"; + } +} + +// Return a frame state enum from its string name (reverse of IoEvalFrame_stateName) +IoFrameState IoEvalFrame_stateFromName(const char *name) { + if (!name) return FRAME_STATE_START; + if (strcmp(name, "start") == 0) return FRAME_STATE_START; + if (strcmp(name, "evalArgs") == 0) return FRAME_STATE_EVAL_ARGS; + if (strcmp(name, "lookupSlot") == 0) return FRAME_STATE_LOOKUP_SLOT; + if (strcmp(name, "activate") == 0) return FRAME_STATE_ACTIVATE; + if (strcmp(name, "continueChain") == 0) return FRAME_STATE_CONTINUE_CHAIN; + if (strcmp(name, "return") == 0) return FRAME_STATE_RETURN; + if (strcmp(name, "if:evalCondition") == 0) return FRAME_STATE_IF_EVAL_CONDITION; + if (strcmp(name, "if:convertBoolean") == 0) return FRAME_STATE_IF_CONVERT_BOOLEAN; + if (strcmp(name, "if:evalBranch") == 0) return FRAME_STATE_IF_EVAL_BRANCH; + if (strcmp(name, "while:evalCondition") == 0) return FRAME_STATE_WHILE_EVAL_CONDITION; + if (strcmp(name, "while:checkCondition") == 0) return FRAME_STATE_WHILE_CHECK_CONDITION; + if (strcmp(name, "while:decide") == 0) return FRAME_STATE_WHILE_DECIDE; + if (strcmp(name, "while:evalBody") == 0) return FRAME_STATE_WHILE_EVAL_BODY; + if (strcmp(name, "loop:evalBody") == 0) return FRAME_STATE_LOOP_EVAL_BODY; + if (strcmp(name, "loop:afterBody") == 0) return FRAME_STATE_LOOP_AFTER_BODY; + if (strcmp(name, "for:evalSetup") == 0) return FRAME_STATE_FOR_EVAL_SETUP; + if (strcmp(name, "for:evalBody") == 0) return FRAME_STATE_FOR_EVAL_BODY; + if (strcmp(name, "for:afterBody") == 0) return FRAME_STATE_FOR_AFTER_BODY; + if (strcmp(name, "foreach:evalBody") == 0) return FRAME_STATE_FOREACH_EVAL_BODY; + if (strcmp(name, "foreach:afterBody") == 0) return FRAME_STATE_FOREACH_AFTER_BODY; +#ifdef IO_CALLCC + if (strcmp(name, "callcc:evalBlock") == 0) return FRAME_STATE_CALLCC_EVAL_BLOCK; +#endif + if (strcmp(name, "coro:waitChild") == 0) return FRAME_STATE_CORO_WAIT_CHILD; + if (strcmp(name, "coro:yielded") == 0) return FRAME_STATE_CORO_YIELDED; + if (strcmp(name, "do:eval") == 0) return FRAME_STATE_DO_EVAL; + if (strcmp(name, "do:wait") == 0) return FRAME_STATE_DO_WAIT; + return FRAME_STATE_START; +} + +// Reset frame data to initial state (for reuse) +void IoEvalFrame_reset(IoEvalFrame *self) { + if (!self) return; + + IoEvalFrameData *fd = FRAME_DATA(self); + if (!fd) return; + + // Free argument values if allocated + if (fd->argValues) { + io_free(fd->argValues); + } + + // Zero all fields + memset(fd, 0, sizeof(IoEvalFrameData)); +} + +// ------- Io-visible methods ------- + +IO_METHOD(IoEvalFrame, message) { + /*doc EvalFrame message + Returns the Message being evaluated in this frame. + */ + IoEvalFrameData *fd = FRAME_DATA(self); + return fd->message ? (IoObject *)fd->message : IONIL(self); +} + +IO_METHOD(IoEvalFrame, target) { + /*doc EvalFrame target + Returns the target (receiver) of the message in this frame. + */ + IoEvalFrameData *fd = FRAME_DATA(self); + return fd->target ? fd->target : IONIL(self); +} + +IO_METHOD(IoEvalFrame, localContext) { + /*doc EvalFrame locals + Returns the locals object (sender context) for this frame. + */ + IoEvalFrameData *fd = FRAME_DATA(self); + return fd->locals ? fd->locals : IONIL(self); +} + +IO_METHOD(IoEvalFrame, state) { + /*doc EvalFrame state + Returns the current state of this frame as a Symbol + (e.g. "start", "activate", "if:evalBranch"). + */ + IoEvalFrameData *fd = FRAME_DATA(self); + return IOSYMBOL(IoEvalFrame_stateName(fd->state)); +} + +IO_METHOD(IoEvalFrame, parent) { + /*doc EvalFrame parent + Returns the parent frame, or nil if this is the bottom frame. + */ + IoEvalFrameData *fd = FRAME_DATA(self); + return fd->parent ? (IoObject *)fd->parent : IONIL(self); +} + +IO_METHOD(IoEvalFrame, result) { + /*doc EvalFrame result + Returns the current result value of this frame, or nil. + */ + IoEvalFrameData *fd = FRAME_DATA(self); + return fd->result ? fd->result : IONIL(self); +} + +IO_METHOD(IoEvalFrame, depth) { + /*doc EvalFrame depth + Returns the number of frames from this frame to the bottom + of the stack (inclusive). The bottom frame has depth 1. + */ + int count = 0; + IoEvalFrame *f = self; + while (f) { + count++; + f = FRAME_DATA(f)->parent; + } + return IONUMBER(count); +} + +IO_METHOD(IoEvalFrame, call) { + /*doc EvalFrame call + Returns the Call object for this frame (created during block/method + activation), or nil if this is not a block activation frame. + */ + IoEvalFrameData *fd = FRAME_DATA(self); + return fd->call ? (IoObject *)fd->call : IONIL(self); +} + +IO_METHOD(IoEvalFrame, blockLocals) { + /*doc EvalFrame blockLocals + Returns the block's local context object, or nil if this frame + is not a block activation. + */ + IoEvalFrameData *fd = FRAME_DATA(self); + return fd->blockLocals ? fd->blockLocals : IONIL(self); +} + +IO_METHOD(IoEvalFrame, description) { + /*doc EvalFrame description + Returns a human-readable description of this frame, including + the message name, state, and source location. + */ + IoEvalFrameData *fd = FRAME_DATA(self); + const char *stateName = IoEvalFrame_stateName(fd->state); + + if (fd->message) { + const char *msgName = CSTRING(IoMessage_name(fd->message)); + const char *label = IoMessage_rawLabel(fd->message) + ? CSTRING(IoMessage_rawLabel(fd->message)) + : "?"; + int line = IoMessage_rawLineNumber(fd->message); + char buf[512]; + snprintf(buf, sizeof(buf), "%s %s:%d [%s]", msgName, label, line, stateName); + return IOSYMBOL(buf); + } + + return IOSYMBOL(stateName); +} diff --git a/libs/iovm/source/IoEvalFrame.h b/libs/iovm/source/IoEvalFrame.h new file mode 100644 index 000000000..8c50a57d6 --- /dev/null +++ b/libs/iovm/source/IoEvalFrame.h @@ -0,0 +1,215 @@ + +// metadoc EvalFrame copyright Steve Dekorte 2002, 2025 +// metadoc EvalFrame license BSD revised + +#ifndef IOEVALFRAME_DEFINED +#define IOEVALFRAME_DEFINED 1 + +#include "Common.h" +#include "IoObject.h" +#include "IoMessage.h" +#include "IoCall.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// IoEvalFrame is a GC-managed IoObject. +// Frames are allocated by the collector and freed by GC when unreferenced. +typedef IoObject IoEvalFrame; + +// Frame states for the evaluation state machine +typedef enum { + FRAME_STATE_START, // Just pushed, need to begin evaluation + FRAME_STATE_EVAL_ARGS, // Evaluating arguments lazily + FRAME_STATE_LOOKUP_SLOT, // Ready to do slot lookup + FRAME_STATE_ACTIVATE, // Activating a block/method + FRAME_STATE_CONTINUE_CHAIN, // Continue to next message in chain + FRAME_STATE_RETURN, // Returning result to parent + + // Control flow states (for non-reentrant primitives) + FRAME_STATE_IF_EVAL_CONDITION, // Evaluating 'if' condition + FRAME_STATE_IF_CONVERT_BOOLEAN, // Converting condition result to boolean + FRAME_STATE_IF_EVAL_BRANCH, // Evaluating 'if' true/false branch + + // While loop states + FRAME_STATE_WHILE_EVAL_CONDITION, // Evaluating 'while' condition + FRAME_STATE_WHILE_CHECK_CONDITION,// Converting condition to boolean + FRAME_STATE_WHILE_DECIDE, // Deciding whether to continue or exit + FRAME_STATE_WHILE_EVAL_BODY, // Evaluating 'while' body + + // Loop (infinite) states + FRAME_STATE_LOOP_EVAL_BODY, // Starting 'loop' body + FRAME_STATE_LOOP_AFTER_BODY, // After 'loop' body, check break/continue + + // For loop states + FRAME_STATE_FOR_EVAL_SETUP, // Evaluating 'for' setup (range/collection) + FRAME_STATE_FOR_EVAL_BODY, // Starting 'for' body + FRAME_STATE_FOR_AFTER_BODY, // After 'for' body, increment and check + + // Foreach states (collection iteration) + FRAME_STATE_FOREACH_EVAL_BODY, // Set slots and push body frame + FRAME_STATE_FOREACH_AFTER_BODY, // After body, increment and check + + // Callcc states +#ifdef IO_CALLCC + FRAME_STATE_CALLCC_EVAL_BLOCK, // Evaluating callcc block body +#endif + + // Coroutine states + FRAME_STATE_CORO_WAIT_CHILD, // Waiting for child coroutine to complete + FRAME_STATE_CORO_YIELDED, // Yielded, will resume here when switched back + + // doString/doMessage/doFile states + FRAME_STATE_DO_EVAL, // Ready to evaluate compiled code + FRAME_STATE_DO_WAIT // Waiting for evaluation result +} IoFrameState; + +// Frame data payload — stored in IoObject's dataPointer +typedef struct IoEvalFrameData { + // Message chain being evaluated + IoMessage *message; // Current message in chain + + // Evaluation context + IoObject *target; // Current target (self for this message) + IoObject *locals; // Current locals (sender context) + IoObject *cachedTarget; // Original target (for semicolon reset) + + // Parent frame (for returning) — also an IoEvalFrame (IoObject) + IoEvalFrame *parent; + + // Current state in the evaluation state machine + IoFrameState state; + + // Argument evaluation state (for lazy evaluation) + int argCount; // Total number of args + int currentArgIndex; // Which arg we're currently evaluating + IoObject **argValues; // Evaluated argument values (array or inlineArgs) + IoObject *inlineArgs[4]; // Inline buffer for ≤4 args (avoids heap alloc) + + // Result + IoObject *result; // Result of this frame + + // For slot lookup and activation + IoObject *slotValue; // Result of slot lookup + IoObject *slotContext; // Context where slot was found + + // Call object (for introspection - created during block activation) + IoCall *call; // Call object for this activation (or NULL) + IoCall *savedCall; // Original Call preserved across in-place if optimization + + // For block activation + IoObject *blockLocals; // Block's local context (NULL if not a block) + int passStops; // Whether to pass stop status (break/continue/return) + int isNestedEvalRoot; // True if this is a nested eval boundary + uintptr_t retainPoolMark; // Retain pool mark for block activation (0 if none) + + // Control flow continuation info (only one active at a time) + union { + struct { + IoMessage *conditionMsg; // Condition message to evaluate + IoMessage *trueBranch; // True branch message + IoMessage *falseBranch; // False branch message (may be NULL) + int conditionResult; // Result of condition evaluation + } ifInfo; + + struct { + IoMessage *conditionMsg; // Condition message to evaluate + IoMessage *bodyMsg; // Body message to execute + IoObject *lastResult; // Last body result (for return value) + int conditionResult; // Result of condition evaluation + } whileInfo; + + struct { + IoMessage *bodyMsg; // Body message to execute + IoObject *lastResult; // Last body result (for break value) + } loopInfo; + + struct { + IoMessage *bodyMsg; // Body message to execute + IoSymbol *counterName; // Name of counter variable + double startValue; // Start value + double endValue; // End value + double increment; // Increment value + double currentValue; // Current counter value + IoObject *lastResult; // Last body result + int initialized; // Whether loop has started + } forInfo; + + struct { + IoObject *collection; // The collection object (List/Map/Seq) + IoMessage *bodyMsg; // Body message to execute + IoSymbol *indexName; // Index variable name (NULL if not used) + IoSymbol *valueName; // Value variable name (NULL for each) + int currentIndex; // Current iteration index + int collectionSize; // Total size (may change if list mutated) + IoObject *lastResult; // Last body result + int direction; // 1 for forward, -1 for reverse + int isEach; // 1 if "each" (send to element as target) + IoObject *mapSource; // Original Map (when collection is a keys list) + } foreachInfo; + +#ifdef IO_CALLCC + struct { + IoObject *continuation; // The Continuation object + IoObject *blockLocals; // Block's local context + } callccInfo; +#endif + + struct { + IoObject *childCoroutine; // Child coroutine we're waiting for + } coroInfo; + + struct { + IoMessage *codeMessage; // The parsed/compiled message to evaluate + IoObject *evalTarget; // Target for evaluation (self) + IoObject *evalLocals; // Locals for evaluation (context) + } doInfo; + } controlFlow; +} IoEvalFrameData; + +// Access the frame's data payload via IoObject's dataPointer +#define FRAME_DATA(frame) ((IoEvalFrameData *)IoObject_dataPointer(frame)) + +// Maximum args that fit in the inline buffer (avoid heap allocation) +#define FRAME_INLINE_ARG_MAX 4 + +// Fast path: check if a body message is a cached literal with no chain +// (e.g., nil, a number, or a string). These can be resolved without +// pushing a child frame — just use the cached result directly. +#define BODY_IS_CACHED_LITERAL(bodyMsg) \ + (IOMESSAGEDATA(bodyMsg)->cachedResult && !IOMESSAGEDATA(bodyMsg)->next) + +#define CACHED_LITERAL_RESULT(bodyMsg) \ + (IOMESSAGEDATA(bodyMsg)->cachedResult) + +// Type check macro +#define ISEVALFRAME(self) \ + IoObject_hasCloneFunc_(self, (IoTagCloneFunc *)IoEvalFrame_rawClone) + +// Proto and lifecycle +IOVM_API IoEvalFrame *IoEvalFrame_proto(void *state); +IOVM_API IoEvalFrame *IoEvalFrame_rawClone(IoEvalFrame *proto); +IOVM_API IoEvalFrame *IoEvalFrame_newWithState(void *state); +IOVM_API void IoEvalFrame_free(IoEvalFrame *self); +IOVM_API void IoEvalFrame_mark(IoEvalFrame *self); +IOVM_API void IoEvalFrame_reset(IoEvalFrame *self); +IOVM_API const char *IoEvalFrame_stateName(IoFrameState state); +IOVM_API IoFrameState IoEvalFrame_stateFromName(const char *name); + +// Io-visible methods +IOVM_API IO_METHOD(IoEvalFrame, message); +IOVM_API IO_METHOD(IoEvalFrame, target); +IOVM_API IO_METHOD(IoEvalFrame, localContext); +IOVM_API IO_METHOD(IoEvalFrame, state); +IOVM_API IO_METHOD(IoEvalFrame, parent); +IOVM_API IO_METHOD(IoEvalFrame, result); +IOVM_API IO_METHOD(IoEvalFrame, depth); +IOVM_API IO_METHOD(IoEvalFrame, call); +IOVM_API IO_METHOD(IoEvalFrame, blockLocals); +IOVM_API IO_METHOD(IoEvalFrame, description); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/libs/iovm/source/IoFile.c b/libs/iovm/source/IoFile.c index d28f49747..c61aa064f 100644 --- a/libs/iovm/source/IoFile.c +++ b/libs/iovm/source/IoFile.c @@ -438,6 +438,7 @@ IO_METHOD(IoFile, open) { if (!IoFile_justExists(self)) { IoState_error_(IOSTATE, m, "unable to create file '%s': %s", UTF8CSTRING(DATA(self)->path), strerror(errno)); + return IONIL(self); } } @@ -447,6 +448,7 @@ IO_METHOD(IoFile, open) { if (DATA(self)->stream == NULL) { IoState_error_(IOSTATE, m, "unable to open file path '%s': %s", UTF8CSTRING(DATA(self)->path), strerror(errno)); + return IONIL(self); } return self; @@ -465,6 +467,7 @@ IO_METHOD(IoFile, reopen) { DATA(self)->flags = IOFILE_FLAGS_NONE; IoMessage_assertArgCount_receiver_(m, 1, self); + if (IOSTATE->errorRaised) return IONIL(self); otherFile = IoMessage_locals_valueArgAt_(m, locals, 0); IOASSERT(ISFILE(otherFile), "arg must be a File"); @@ -488,7 +491,7 @@ IO_METHOD(IoFile, reopen) { IoState_error_( IOSTATE, m, "unable to reopen to file '%s' with mode %s.", UTF8CSTRING(DATA(self)->path), CSTRING(DATA(self)->mode)); - fclose(fp); + return IONIL(self); } } @@ -710,7 +713,9 @@ IO_METHOD(IoFile, write) { int i; IoFile_assertOpen(self, locals, m); + if (IOSTATE->errorRaised) return IONIL(self); IoFile_assertWrite(self, locals, m); + if (IOSTATE->errorRaised) return IONIL(self); for (i = 0; i < IoMessage_argCount(m); i++) { IoSymbol *string = IoMessage_locals_seqArgAt_(m, locals, i); @@ -737,6 +742,7 @@ IO_METHOD(IoFile, readLines) { } IoFile_assertOpen(self, locals, m); + if (IOSTATE->errorRaised) return IONIL(self); { IoList *lines = IoList_new(state); @@ -770,6 +776,7 @@ IO_METHOD(IoFile, readLine) { // char *path = UTF8CSTRING(DATA(self)->path); // tmp for debugging IoFile_assertOpen(self, locals, m); + if (IOSTATE->errorRaised) return IONIL(self); if (feof(DATA(self)->stream) != 0) { clearerr(DATA(self)->stream); @@ -805,6 +812,7 @@ UArray *IoFile_readUArrayOfLength_(IoFile *self, IoObject *locals, size_t length = IoMessage_locals_sizetArgAt_(m, locals, 0); UArray *ba = UArray_new(); IoFile_assertOpen(self, locals, m); + if (IOSTATE->errorRaised) return IONIL(self); UArray_readNumberOfItems_fromCStream_(ba, length, DATA(self)->stream); @@ -873,6 +881,7 @@ IO_METHOD(IoFile, rewind) { */ IoFile_assertOpen(self, locals, m); + if (IOSTATE->errorRaised) return IONIL(self); if (DATA(self)->stream) { rewind(DATA(self)->stream); @@ -889,6 +898,7 @@ IO_METHOD(IoFile, position_) { long pos = IoMessage_locals_longArgAt_(m, locals, 0); IoFile_assertOpen(self, locals, m); + if (IOSTATE->errorRaised) return IONIL(self); if (fseek(DATA(self)->stream, pos, 0) != 0) { IoState_error_(IOSTATE, m, "unable to set position %i file path '%s'", @@ -904,6 +914,7 @@ IO_METHOD(IoFile, position) { */ IoFile_assertOpen(self, locals, m); + if (IOSTATE->errorRaised) return IONIL(self); return IONUMBER(ftell(DATA(self)->stream)); } @@ -913,6 +924,7 @@ IO_METHOD(IoFile, positionAtEnd) { */ IoFile_assertOpen(self, locals, m); + if (IOSTATE->errorRaised) return IONIL(self); if (DATA(self)->stream) { fseek(DATA(self)->stream, 0, SEEK_END); @@ -927,6 +939,7 @@ IO_METHOD(IoFile, isAtEnd) { */ IoFile_assertOpen(self, locals, m); + if (IOSTATE->errorRaised) return IONIL(self); return IOBOOL(self, feof(DATA(self)->stream) != 0); } @@ -963,6 +976,7 @@ IO_METHOD(IoFile, assertOpen) { if (!DATA(self)->stream) { IoState_error_(IOSTATE, m, "file '%s' not yet open", UTF8CSTRING(DATA(self)->path)); + return IONIL(self); } return self; } @@ -973,6 +987,7 @@ IO_METHOD(IoFile, assertWrite) { if ((strcmp(mode, "r+")) && (strcmp(mode, "a+")) && (strcmp(mode, "w"))) { IoState_error_(IOSTATE, m, "file '%s' not open for write", UTF8CSTRING(DATA(self)->path)); + return IONIL(self); } return self; @@ -987,6 +1002,7 @@ IO_METHOD(IoFile, at) { int byte; IoFile_assertOpen(self, locals, m); + if (IOSTATE->errorRaised) return IONIL(self); IoFile_position_(self, locals, m); /* works since first arg is the same */ byte = fgetc(DATA(self)->stream); @@ -1006,7 +1022,9 @@ IO_METHOD(IoFile, atPut) { int c = IoMessage_locals_intArgAt_(m, locals, 1); IoFile_assertOpen(self, locals, m); + if (IOSTATE->errorRaised) return IONIL(self); IoFile_assertWrite(self, locals, m); + if (IOSTATE->errorRaised) return IONIL(self); IoFile_position_(self, locals, m); // works since first arg is the same if (fputc(c, DATA(self)->stream) == EOF) { @@ -1037,11 +1055,13 @@ aFile foreach(v, writeln("byte ", v)) int i = 0; IoFile_assertOpen(self, locals, m); + if (IOSTATE->errorRaised) return IONIL(self); result = IONIL(self); IoMessage_foreachArgs(m, self, &indexSlotName, &characterSlotName, &doMessage); + if (IOSTATE->errorRaised) return IONIL(self); for (;;) { int c = getc(DATA(self)->stream); @@ -1087,8 +1107,10 @@ aFile foreach(v, writeln("Line: ", v)) IoState *state; IoFile_assertOpen(self, locals, m); + if (IOSTATE->errorRaised) return IONIL(self); IoMessage_foreachArgs(m, self, &indexSlotName, &lineSlotName, &doMessage); + if (IOSTATE->errorRaised) return IONIL(self); result = IONIL(self); state = IOSTATE; diff --git a/libs/iovm/source/IoList.c b/libs/iovm/source/IoList.c index b6a6dbb2b..b62b25809 100644 --- a/libs/iovm/source/IoList.c +++ b/libs/iovm/source/IoList.c @@ -13,6 +13,7 @@ A mutable array of values. The first index is 0. #include "IoSeq.h" #include "IoState.h" #include "IoNumber.h" +#include "IoEvalFrame.h" #include "IoBlock.h" static const char *protoId = "List"; @@ -223,17 +224,19 @@ long IoList_rawIndexOf_(IoList *self, IoObject *v) { return -1; } -void IoList_checkIndex(IoList *self, IoMessage *m, char allowsExtending, - int index, const char *methodName) { +int IoList_checkIndex(IoList *self, IoMessage *m, char allowsExtending, + int index, const char *methodName) { size_t max = List_size(DATA(self)); if (allowsExtending) { max += 1; } - if (index < 0 || index >= max) { + if (index < 0 || (size_t)index >= max) { IoState_error_(IOSTATE, m, "index out of bounds\n"); + return 1; } + return 0; } // immutable -------------------------------------------------------- @@ -362,7 +365,10 @@ void IoList_sliceArguments(IoList *self, IoObject *locals, IoMessage *m, *step = (IoMessage_argCount(m) == 3) ? IoMessage_locals_intArgAt_(m, locals, 2) : 1; - IOASSERT(step != 0, "step cannot be equal to zero"); + if (*step == 0) { + IoState_error_(IOSTATE, m, "Io Assertion 'step cannot be equal to zero'"); + return; + } *start = IoMessage_locals_intArgAt_(m, locals, 0); *end = (IoMessage_argCount(m) >= 2) @@ -421,8 +427,32 @@ Step argument is also optional and defaults to 1. IO_METHOD(IoList, each) { IoState *state = IOSTATE; - IoObject *result = IONIL(self); + IoMessage *doMessage = IoMessage_rawArgAt_(m, 0); + + // Iterative path: use foreach frame state machine + if (state->currentFrame != NULL) { + IoEvalFrame *frame = state->currentFrame; + IoEvalFrameData *fd = FRAME_DATA(frame); + + fd->controlFlow.foreachInfo.collection = self; + fd->controlFlow.foreachInfo.bodyMsg = doMessage; + fd->controlFlow.foreachInfo.indexName = NULL; + fd->controlFlow.foreachInfo.valueName = NULL; + fd->controlFlow.foreachInfo.currentIndex = 0; + fd->controlFlow.foreachInfo.collectionSize = (int)List_size(DATA(self)); + fd->controlFlow.foreachInfo.lastResult = NULL; + fd->controlFlow.foreachInfo.direction = 1; + fd->controlFlow.foreachInfo.isEach = 1; + fd->controlFlow.foreachInfo.mapSource = NULL; + + fd->state = FRAME_STATE_FOREACH_EVAL_BODY; + state->needsControlFlowHandling = 1; + return state->ioNil; + } + + // Recursive fallback + IoObject *result = IONIL(self); List *list = DATA(self); IoState_pushRetainPool(state); @@ -449,17 +479,42 @@ list(1, 2, 3) foreach(v, writeln(v)) */ IoState *state = IOSTATE; - IoObject *result = IONIL(self); - IoSymbol *slotName = NULL; - IoSymbol *valueName; - IoMessage *doMessage; - List *list = DATA(self); if (IoMessage_argCount(m) == 1) { return IoList_each(self, locals, m); } + IoSymbol *slotName = NULL; + IoSymbol *valueName; + IoMessage *doMessage; + IoMessage_foreachArgs(m, self, &slotName, &valueName, &doMessage); + if (state->errorRaised) return IONIL(self); + + // Iterative path + if (state->currentFrame != NULL) { + IoEvalFrame *frame = state->currentFrame; + IoEvalFrameData *fd = FRAME_DATA(frame); + + fd->controlFlow.foreachInfo.collection = self; + fd->controlFlow.foreachInfo.bodyMsg = doMessage; + fd->controlFlow.foreachInfo.indexName = slotName; + fd->controlFlow.foreachInfo.valueName = valueName; + fd->controlFlow.foreachInfo.currentIndex = 0; + fd->controlFlow.foreachInfo.collectionSize = (int)List_size(DATA(self)); + fd->controlFlow.foreachInfo.lastResult = NULL; + fd->controlFlow.foreachInfo.direction = 1; + fd->controlFlow.foreachInfo.isEach = 0; + fd->controlFlow.foreachInfo.mapSource = NULL; + + fd->state = FRAME_STATE_FOREACH_EVAL_BODY; + state->needsControlFlowHandling = 1; + return state->ioNil; + } + + // Recursive fallback + IoObject *result = IONIL(self); + List *list = DATA(self); IoState_pushRetainPool(state); @@ -489,12 +544,37 @@ IO_METHOD(IoList, reverseForeach) { */ IoState *state = IOSTATE; - IoObject *result = IONIL(self); IoSymbol *slotName, *valueName; IoMessage *doMessage; - long i; IoMessage_foreachArgs(m, self, &slotName, &valueName, &doMessage); + if (state->errorRaised) return IONIL(self); + + // Iterative path + if (state->currentFrame != NULL) { + IoEvalFrame *frame = state->currentFrame; + IoEvalFrameData *fd = FRAME_DATA(frame); + int size = (int)List_size(DATA(self)); + + fd->controlFlow.foreachInfo.collection = self; + fd->controlFlow.foreachInfo.bodyMsg = doMessage; + fd->controlFlow.foreachInfo.indexName = slotName; + fd->controlFlow.foreachInfo.valueName = valueName; + fd->controlFlow.foreachInfo.currentIndex = size - 1; + fd->controlFlow.foreachInfo.collectionSize = size; + fd->controlFlow.foreachInfo.lastResult = NULL; + fd->controlFlow.foreachInfo.direction = -1; + fd->controlFlow.foreachInfo.isEach = 0; + fd->controlFlow.foreachInfo.mapSource = NULL; + + fd->state = FRAME_STATE_FOREACH_EVAL_BODY; + state->needsControlFlowHandling = 1; + return state->ioNil; + } + + // Recursive fallback + IoObject *result = IONIL(self); + long i; IoState_pushRetainPool(state); @@ -559,6 +639,7 @@ IO_METHOD(IoList, appendSeq) { if (other == self) { IoState_error_(IOSTATE, m, "can't add a list to itself\n"); + return IONIL(self); } else { List *selfList = DATA(self); List *otherList = DATA(other); @@ -668,9 +749,12 @@ IO_METHOD(IoList, atInsert) { */ int index = IoMessage_locals_intArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); IoObject *v = IoMessage_locals_valueArgAt_(m, locals, 1); + if (IOSTATE->errorRaised) return IONIL(self); - IoList_checkIndex(self, m, 1, index, "List atInsert"); + if (IoList_checkIndex(self, m, 1, index, "List atInsert")) + return IONIL(self); List_at_insert_(DATA(self), index, IOREF(v)); IoObject_isDirty_(self, 1); return self; @@ -683,9 +767,11 @@ IO_METHOD(IoList, removeAt) { */ int index = IoMessage_locals_intArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); IoObject *v = List_at_(DATA(self), index); - IoList_checkIndex(self, m, 0, index, "Io List atInsert"); + if (IoList_checkIndex(self, m, 0, index, "Io List atInsert")) + return IONIL(self); List_removeIndex_(DATA(self), index); IoObject_isDirty_(self, 1); return (v) ? v : IONIL(self); @@ -710,7 +796,8 @@ IO_METHOD(IoList, atPut) { int index = IoMessage_locals_intArgAt_(m, locals, 0); IoObject *v = IoMessage_locals_valueArgAt_(m, locals, 1); - IoList_checkIndex(self, m, 0, index, "Io List atPut"); + if (IoList_checkIndex(self, m, 0, index, "Io List atPut")) + return IONIL(self); IoList_rawAtPut(self, index, v); IoObject_isDirty_(self, 1); return self; @@ -760,8 +847,10 @@ IO_METHOD(IoList, swapIndices) { int i = IoMessage_locals_intArgAt_(m, locals, 0); int j = IoMessage_locals_intArgAt_(m, locals, 1); - IoList_checkIndex(self, m, 0, i, "List swapIndices"); - IoList_checkIndex(self, m, 0, j, "List swapIndices"); + if (IoList_checkIndex(self, m, 0, i, "List swapIndices")) + return IONIL(self); + if (IoList_checkIndex(self, m, 0, j, "List swapIndices")) + return IONIL(self); List_swap_with_(DATA(self), i, j); IoObject_isDirty_(self, 1); return self; @@ -869,6 +958,7 @@ IO_METHOD(IoList, sortInPlaceBy) { sc.state = IOSTATE; sc.locals = locals; sc.block = IoMessage_locals_blockArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); sc.blockMsg = IoMessage_new(IOSTATE); sc.argMsg1 = IoMessage_new(IOSTATE); sc.argMsg2 = IoMessage_new(IOSTATE); diff --git a/libs/iovm/source/IoMap.c b/libs/iovm/source/IoMap.c index 72b124519..2cf1c771b 100644 --- a/libs/iovm/source/IoMap.c +++ b/libs/iovm/source/IoMap.c @@ -13,6 +13,7 @@ A key/value dictionary appropriate for holding large key/value collections. #include "IoSeq.h" #include "IoState.h" #include "IoNumber.h" +#include "IoEvalFrame.h" #include "IoList.h" #include "IoBlock.h" @@ -133,6 +134,7 @@ IO_METHOD(IoMap, at) { */ IoSymbol *k = IoMessage_locals_symbolArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); void *result = PHash_at_(DATA(self), k); if (!result && IoMessage_argCount(m) > 1) { @@ -148,7 +150,9 @@ IO_METHOD(IoMap, atPut) { */ IoSymbol *k = IoMessage_locals_symbolArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); IoObject *v = IoMessage_locals_valueArgAt_(m, locals, 1); + if (IOSTATE->errorRaised) return IONIL(self); IoMap_rawAtPut(self, k, v); return self; } @@ -160,6 +164,7 @@ IO_METHOD(IoMap, atIfAbsentPut) { */ IoSymbol *k = IoMessage_locals_symbolArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); if (PHash_at_(DATA(self), k) == NULL) { IoObject *v = IoMessage_locals_valueArgAt_(m, locals, 1); @@ -248,10 +253,37 @@ aMap foreach(k, v, myBlock(k, v)) IoState *state = IOSTATE; IoSymbol *keyName, *valueName; IoMessage *doMessage; - PHash *p = DATA(self); - IoObject *result = IONIL(self); IoMessage_foreachArgs(m, self, &keyName, &valueName, &doMessage); + if (state->errorRaised) return IONIL(self); + + // Iterative path: build keys list for index-based iteration + if (state->currentFrame != NULL) { + IoEvalFrame *frame = state->currentFrame; + IoEvalFrameData *fd = FRAME_DATA(frame); + IoList *keysList = IoMap_rawKeys(self); + int keyCount = (int)List_size(IoList_rawList(keysList)); + + // Use keys list as collection, store original map for value lookup + fd->controlFlow.foreachInfo.collection = (IoObject *)keysList; + fd->controlFlow.foreachInfo.mapSource = self; + fd->controlFlow.foreachInfo.bodyMsg = doMessage; + fd->controlFlow.foreachInfo.indexName = keyName; + fd->controlFlow.foreachInfo.valueName = valueName; + fd->controlFlow.foreachInfo.currentIndex = 0; + fd->controlFlow.foreachInfo.collectionSize = keyCount; + fd->controlFlow.foreachInfo.lastResult = NULL; + fd->controlFlow.foreachInfo.direction = 1; + fd->controlFlow.foreachInfo.isEach = 0; + + fd->state = FRAME_STATE_FOREACH_EVAL_BODY; + state->needsControlFlowHandling = 1; + return state->ioNil; + } + + // Recursive fallback + PHash *p = DATA(self); + IoObject *result = IONIL(self); IoState_pushRetainPool(state); PHASH_FOREACH( diff --git a/libs/iovm/source/IoMessage.c b/libs/iovm/source/IoMessage.c index 760e48c87..46f66867c 100644 --- a/libs/iovm/source/IoMessage.c +++ b/libs/iovm/source/IoMessage.c @@ -265,6 +265,10 @@ void IoMessage_mark(IoMessage *self) { IoObject_shouldMarkIfNonNull((IoObject *)DATA(self)->next); IoObject_shouldMarkIfNonNull((IoObject *)DATA(self)->label); + + // Mark inline cache entries (prevent cached slot values from being collected) + IoObject_shouldMarkIfNonNull(DATA(self)->inlineCacheValue); + IoObject_shouldMarkIfNonNull(DATA(self)->inlineCacheContext); } void IoMessage_free(IoMessage *self) { @@ -285,6 +289,7 @@ void IoMessage_rawSetCachedResult_(IoMessage *self, IoObject *v) { void IoMessage_rawSetName_(IoMessage *self, IoObject *v) { DATA(self)->name = v ? IOREF(v) : NULL; + DATA(self)->isSpecialForm = 0; // Reset cached flag when name changes } void IoMessage_rawSetLabel_(IoMessage *self, IoObject *v) { @@ -449,11 +454,20 @@ IO_METHOD(IoMessage, doInContext) { IoObject *IoMessage_locals_performOn_(IoMessage *self, IoObject *locals, IoObject *target) { IoState *state = IOSTATE; + + // Use the iterative eval loop for message evaluation. + // This eliminates C stack recursion for message chains — + // only bounded recursion remains (for CFunction argument evaluation). + if (state->currentFrame) { + return IoMessage_locals_performOn_iterative(self, locals, target); + } + + // No eval loop running (bootstrap only). Use a simple recursive + // evaluator as a fallback. This is only reached during VM + // initialization before the first eval loop is started. IoMessage *m = self; IoObject *result = target; IoObject *cachedTarget = target; - // IoObject *semicolonSymbol = state->semicolonSymbol; - // IoMessageData *md; IoMessageData *md; if (state->receivedSignal) { @@ -461,10 +475,6 @@ IoObject *IoMessage_locals_performOn_(IoMessage *self, IoObject *locals, } do { - // md = DATA(m); - - // printf("%s %i\n", CSTRING(IoMessage_name(m)), state->stopStatus); - // printf(" %s\n", CSTRING(IoMessage_name(m))); if (state->showAllMessages) { printf("M:%s:%s:%i\n", CSTRING(IoMessage_name(m)), CSTRING(IoMessage_rawLabel(m)), IoMessage_rawLineNumber(m)); @@ -475,18 +485,7 @@ IoObject *IoMessage_locals_performOn_(IoMessage *self, IoObject *locals, if (md->name == state->semicolonSymbol) { target = cachedTarget; } else { - result = md->cachedResult; // put it on the stack? - /* - if(state->debugOn) - { - char *s = CSTRING(DATA(m)->name); - printf("%s\n", s); - if (strcmp(s, "clone") == 0) - { - printf("found '%s'\n", s); - } - } - */ + result = md->cachedResult; if (!result) { IoState_pushRetainPool(state); @@ -501,24 +500,16 @@ IoObject *IoMessage_locals_performOn_(IoMessage *self, IoObject *locals, result = IoObject_tag(target)->performFunc(target, locals, m); #endif IoState_popRetainPoolExceptFor_(state, result); + + if (state->errorRaised) { + return state->ioNil; + } } - // IoObject_freeIfUnreferenced(target); target = result; if (state->stopStatus != MESSAGE_STOP_STATUS_NORMAL) { return state->returnValue; - /* - result = state->returnValue; - - if (result) - { - //IoState_stackRetain_(state, result); - return result; - } - printf("IoBlock no result!\n"); - return state->ioNil; - */ } } } while ((m = md->next)); @@ -555,6 +546,7 @@ IoObject *IoMessage_locals_numberArgAt_(IoMessage *self, IoObject *locals, if (!ISNUMBER(v)) { IoMessage_locals_numberArgAt_errorForType_(self, locals, n, "Number"); + return IOSTATE->ioNil; // Return early after error } return v; @@ -606,6 +598,7 @@ IoObject *IoMessage_locals_seqArgAt_(IoMessage *self, IoObject *locals, int n) { if (!ISSEQ(v)) { IoMessage_locals_numberArgAt_errorForType_(self, locals, n, "Sequence"); + return IOSTATE->ioNil; // Return early after error } return v; @@ -623,6 +616,7 @@ IoObject *IoMessage_locals_symbolArgAt_(IoMessage *self, IoObject *locals, if (!ISSEQ(v)) { IoMessage_locals_numberArgAt_errorForType_(self, locals, n, "Sequence"); + return IOSTATE->ioNil; // Return early after error } return IoSeq_rawAsSymbol(v); @@ -635,6 +629,7 @@ IoObject *IoMessage_locals_mutableSeqArgAt_(IoMessage *self, IoObject *locals, if (!ISMUTABLESEQ(v)) { IoMessage_locals_numberArgAt_errorForType_(self, locals, n, "mutable Sequence"); + return IOSTATE->ioNil; // Return early after error } return v; @@ -643,39 +638,49 @@ IoObject *IoMessage_locals_mutableSeqArgAt_(IoMessage *self, IoObject *locals, IoObject *IoMessage_locals_blockArgAt_(IoMessage *self, IoObject *locals, int n) { IoObject *v = IoMessage_locals_valueArgAt_(self, locals, n); - if (!ISBLOCK(v)) + if (!ISBLOCK(v)) { IoMessage_locals_numberArgAt_errorForType_(self, locals, n, "Block"); + return IOSTATE->ioNil; // Return early after error + } return v; } IoObject *IoMessage_locals_dateArgAt_(IoMessage *self, IoObject *locals, int n) { IoObject *v = IoMessage_locals_valueArgAt_(self, locals, n); - if (!ISDATE(v)) + if (!ISDATE(v)) { IoMessage_locals_numberArgAt_errorForType_(self, locals, n, "Date"); + return IOSTATE->ioNil; // Return early after error + } return v; } IoObject *IoMessage_locals_messageArgAt_(IoMessage *self, IoObject *locals, int n) { IoObject *v = IoMessage_locals_valueArgAt_(self, locals, n); - if (!ISMESSAGE(v)) + if (!ISMESSAGE(v)) { IoMessage_locals_numberArgAt_errorForType_(self, locals, n, "Message"); + return IOSTATE->ioNil; // Return early after error + } return v; } IoObject *IoMessage_locals_listArgAt_(IoMessage *self, IoObject *locals, int n) { IoObject *v = IoMessage_locals_valueArgAt_(self, locals, n); - if (!ISLIST(v)) + if (!ISLIST(v)) { IoMessage_locals_numberArgAt_errorForType_(self, locals, n, "List"); + return IOSTATE->ioNil; // Return early after error + } return v; } IoObject *IoMessage_locals_mapArgAt_(IoMessage *self, IoObject *locals, int n) { IoObject *v = IoMessage_locals_valueArgAt_(self, locals, n); - if (!ISMAP(v)) + if (!ISMAP(v)) { IoMessage_locals_numberArgAt_errorForType_(self, locals, n, "Map"); + return IOSTATE->ioNil; // Return early after error + } return v; } @@ -986,6 +991,7 @@ IO_METHOD(IoMessage, setArguments) { */ IoList *ioList = IoMessage_locals_listArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); List *newArgs = IoList_rawList(ioList); List_removeAll(DATA(self)->args); @@ -1167,6 +1173,15 @@ void IoMessage_foreachArgs(IoMessage *self, IoObject *receiver, IoMessage_assertArgCount_receiver_(self, 2, receiver); + // IoState_error_ no longer longjmps — it returns normally. + // Must bail out before accessing args that may not exist. + if (IOSTATE->errorRaised) { + *indexSlotName = NULL; + *valueSlotName = NULL; + *doMessage = NULL; + return; + } + if (IoMessage_argCount(self) > 2) { *indexSlotName = IoMessage_name(IoMessage_rawArgAt_(self, 0)); offset = 1; diff --git a/libs/iovm/source/IoMessage.h b/libs/iovm/source/IoMessage.h index d8a759384..a4a1c07f6 100644 --- a/libs/iovm/source/IoMessage.h +++ b/libs/iovm/source/IoMessage.h @@ -46,6 +46,16 @@ typedef struct { // debugging info // int charNumber; int lineNumber; + uint8_t isSpecialForm; // 0=unchecked, 1=normal, 2=special form + + // Inline cache for monomorphic slot lookups. + // Caches the last (tag, value, context) result so repeated sends + // to objects of the same type skip the proto chain walk. + IoTag *inlineCacheTag; // Tag of target at cache time (NULL = empty) + IoObject *inlineCacheValue; // Cached slot value + IoObject *inlineCacheContext; // Cached slot context + unsigned int inlineCacheVersion; // slotVersion at cache time + IoSymbol *label; } IoMessageData; diff --git a/libs/iovm/source/IoMessage_parser.c b/libs/iovm/source/IoMessage_parser.c index dc5dbbec6..abf166e91 100644 --- a/libs/iovm/source/IoMessage_parser.c +++ b/libs/iovm/source/IoMessage_parser.c @@ -124,46 +124,7 @@ IoMessage *IoMessage_newParse(void *state, IoLexer *lexer) { ((IoState *)state)->ioNil); } -/* -typedef struct -{ - void *state; - IoLexer *lexer; - Coro *coro; - Coro *continuation; - void *result; -} ParseContext; - -IoMessage *IoMessage_coroNewParseNextMessageChain(ParseContext *context) -{ - context->result = IoMessage_newParseNextMessageChain(context->state, -context->lexer); Coro_switchTo_(context->coro, context->continuation); -} -*/ - IoMessage *IoMessage_newParseNextMessageChain(void *state, IoLexer *lexer) { - /* - IoCoroutine *current = IoState_currentCoroutine(state); - Coro *coro = IoCoroutine_cid(current); - if (Coro_stackSpaceAlmostGone(coro)) - { - // need to make Coroutine support a stack of Coros which it frees - when released - // return IoCoroutine_internallyChain(current, context, - IoMessage_...); - - Coro *newCoro = Coro_new(); - ParseContext p = {state, lexer, newCoro, coro, NULL}; - size_t left = Coro_bytesLeftOnStack(coro); - printf("Warning IoMessage_newParseNextMessageChain doing callc with - %i bytes left to avoid stack overflow\n", left); - - Coro_startCoro_(coro, newCoro, &p, (CoroStartCallback - *)IoMessage_coroNewParseNextMessageChain); Coro_free(newCoro); return - p.result; - } - */ - IoMessage *self = IoMessage_new(state); if (IoTokenType_isValidMessageName(IoLexer_topType(lexer))) { diff --git a/libs/iovm/source/IoNumber.c b/libs/iovm/source/IoNumber.c index d807c7f57..7c0a8f94f 100644 --- a/libs/iovm/source/IoNumber.c +++ b/libs/iovm/source/IoNumber.c @@ -512,6 +512,7 @@ IO_METHOD(IoNumber, asString) { if (IoMessage_argCount(m) >= 1) { int whole = IoMessage_locals_intArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); int part = 6; char *s; size_t length; @@ -519,6 +520,7 @@ IO_METHOD(IoNumber, asString) { if (IoMessage_argCount(m) >= 2) { part = abs(IoMessage_locals_intArgAt_(m, locals, 1)); + if (IOSTATE->errorRaised) return IONIL(self); } part = abs(part); @@ -1060,7 +1062,9 @@ IO_METHOD(IoNumber, between) { */ double a = IoMessage_locals_doubleArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); double b = IoMessage_locals_doubleArgAt_(m, locals, 1); + if (IOSTATE->errorRaised) return IONIL(self); double n = DATA(self); return IOBOOL(self, ((n >= a) && (n <= b)) || (n <= a && (n >= b))); @@ -1074,7 +1078,9 @@ IO_METHOD(IoNumber, clip) { */ double a = IoMessage_locals_doubleArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); double b = IoMessage_locals_doubleArgAt_(m, locals, 1); + if (IOSTATE->errorRaised) return IONIL(self); double n = DATA(self); if (n < a) @@ -1232,6 +1238,7 @@ IO_METHOD(IoNumber, repeat) { */ IoMessage_assertArgCount_receiver_(m, 1, self); + if (IOSTATE->errorRaised) return IONIL(self); { IoState *state = IOSTATE; diff --git a/libs/iovm/source/IoObject.c b/libs/iovm/source/IoObject.c index 4b7eb5b04..ada1b3d5e 100644 --- a/libs/iovm/source/IoObject.c +++ b/libs/iovm/source/IoObject.c @@ -13,6 +13,10 @@ objects. When cloned, an Object will call its init slot (with no arguments). #include "IoObject.h" #undef IOOBJECT_C #include "IoCoroutine.h" +#ifdef IO_CALLCC +#include "IoContinuation.h" +#endif +#include "IoEvalFrame.h" #include "IoTag.h" #include "IoCFunction.h" #include "IoSeq.h" @@ -45,8 +49,12 @@ IoTag *IoObject_newTag(void *state) { IoObject *IoObject_justAlloc(IoState *state) { IoObject *child = Collector_newMarker(state->collector); - CollectorMarker_setObject_(child, io_calloc(1, sizeof(IoObjectData))); - IoObject_protos_(child, (IoObject **)io_calloc(2, sizeof(IoObject *))); + // Allocate IoObjectData + 2-pointer protos array in a single block. + // The protos array is placed immediately after the IoObjectData struct. + IoObjectData *data = + io_calloc(1, sizeof(IoObjectData) + 2 * sizeof(IoObject *)); + CollectorMarker_setObject_(child, data); + IoObject_protos_(child, (IoObject **)(data + 1)); return child; } @@ -166,6 +174,9 @@ IoObject *IoObject_protoFinish(void *state) { {"break", IoObject_break}, {"continue", IoObject_continue}, {"stopStatus", IoObject_stopStatus}, +#ifdef IO_CALLCC + {"callcc", IoObject_callcc}, +#endif // utility @@ -356,6 +367,7 @@ IoObject *IoObject_rawClonePrimitive(IoObject *proto) { IoObject_tag_(self, IoObject_tag(proto)); IoObject_setProtoTo_(self, proto); IoObject_setDataPointer_(self, NULL); + IoObject_isActivatable_(self, IoObject_isActivatable(proto)); IoObject_isDirty_(self, 1); return self; } @@ -383,8 +395,19 @@ int IoObject_rawProtosCount(IoObject *self) { void IoObject_rawAppendProto_(IoObject *self, IoObject *p) { int count = IoObject_rawProtosCount(self); - IoObject_protos_(self, io_realloc(IoObject_protos(self), - (count + 2) * sizeof(IoObject *))); + IoObject **oldProtos = IoObject_protos(self); + IoObjectData *data = IoObject_deref(self); + + if ((void *)oldProtos == (void *)(data + 1)) { + // Protos are inline (part of data block) - can't realloc + IoObject **newProtos = + (IoObject **)io_calloc(count + 2, sizeof(IoObject *)); + memcpy(newProtos, oldProtos, (count + 1) * sizeof(IoObject *)); + IoObject_protos_(self, newProtos); + } else { + IoObject_protos_(self, io_realloc(oldProtos, + (count + 2) * sizeof(IoObject *))); + } IoObject_protos(self)[count] = IOREF(p); IoObject_protos(self)[count + 1] = NULL; } @@ -393,10 +416,17 @@ void IoObject_rawPrependProto_(IoObject *self, IoObject *p) { int count = IoObject_rawProtosCount(self); int oldSize = (count + 1) * sizeof(IoObject *); int newSize = oldSize + sizeof(IoObject *); - - IoObject_protos_(self, io_realloc(IoObject_protos(self), newSize)); - - { + IoObject **oldProtos = IoObject_protos(self); + IoObjectData *data = IoObject_deref(self); + + if ((void *)oldProtos == (void *)(data + 1)) { + // Protos are inline - allocate new array + IoObject **newProtos = + (IoObject **)io_calloc(1, newSize); + memcpy(newProtos + 1, oldProtos, oldSize); + IoObject_protos_(self, newProtos); + } else { + IoObject_protos_(self, io_realloc(oldProtos, newSize)); void *src = IoObject_protos(self); void *dst = IoObject_protos(self) + 1; memmove(dst, src, oldSize); @@ -482,6 +512,7 @@ IO_METHOD(IoObject, appendProto) { IoObject *proto = IoMessage_locals_valueArgAt_(m, locals, 0); IoObject_rawAppendProto_(self, proto); + IOSTATE->slotVersion++; return self; } @@ -492,6 +523,7 @@ IO_METHOD(IoObject, prependProto) { IoObject *proto = IoMessage_locals_valueArgAt_(m, locals, 0); IoObject_rawPrependProto_(self, proto); + IOSTATE->slotVersion++; return self; } @@ -503,6 +535,7 @@ IO_METHOD(IoObject, removeProto) { IoObject *proto = IoMessage_locals_valueArgAt_(m, locals, 0); IoObject_rawRemoveProto_(self, proto); + IOSTATE->slotVersion++; return self; } @@ -512,6 +545,7 @@ IO_METHOD(IoObject, removeAllProtos) { */ IoObject_rawRemoveAllProtos(self); + IOSTATE->slotVersion++; return self; } @@ -521,9 +555,11 @@ IO_METHOD(IoObject, setProtos) { */ IoList *ioList = IoMessage_locals_listArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); IoObject_rawRemoveAllProtos(self); LIST_FOREACH(IoList_rawList(ioList), i, v, IoObject_rawAppendProto_(self, (IoObject *)v)); + IOSTATE->slotVersion++; return self; } @@ -554,25 +590,16 @@ void IoObject_freeSlots( } void IoObject_willFree(IoObject *self) { - /* - // disabled until we keep a list of coros and can make sure their stacks are - marked after the - // willFree gc stage - if (IoObject_sentWillFree(self) == 0) - { - IoObject *context; - IoMessage *m = IOSTATE->willFreeMessage; - IoObject *finalizeSlotValue = IoObject_rawGetSlot_context_(self, - IoMessage_name(m), &context); - - if (finalizeSlotValue) - { - IoObject_perform(self, self, m); - IoObject_sentWillFree_(self, 1); - //IoObject_makeGray(self); - } +#ifdef COLLECTOR_USE_REFCOUNT + if (IoObject_ownsSlots(self)) { + PHASH_FOREACH(IoObject_slots(self), k, v, + (void)k; + Collector_value_removingRefTo_(IOCOLLECTOR, v); + ); } - */ +#else + (void)self; +#endif } void IoObject_free(IoObject *self) // prepare for io_free and possibly recycle @@ -632,9 +659,27 @@ void IoObject_dealloc(IoObject *self) // really io_free it PHash_free(IoObject_slots(self)); } - io_free(IoObject_protos(self)); - // memset(self, 0, sizeof(IoObjectData)); - io_free(self->object); + { + IoObjectData *objData = self->object; + IoObject **protos = objData->protos; + int protosInline = ((void *)protos == (void *)(objData + 1)); + + // Try to recycle data+protos block for Number allocation + IoState *st = objData->tag ? (IoState *)objData->tag->state : NULL; + if (st && objData->tag == st->numberTag && + protosInline && + st->numberDataFreeListSize < NUMBER_DATA_POOL_MAX) { + // Put on freelist (will be zeroed on reuse) + objData->data.ptr = st->numberDataFreeList; + st->numberDataFreeList = objData; + st->numberDataFreeListSize++; + } else { + if (!protosInline) { + io_free(protos); + } + io_free(objData); + } + } } else { // printf("IoObject_decrementMarkerCount(%p)\n", (void *)self); IoObject_decrementMarkerCount(self) @@ -785,6 +830,7 @@ void IoObject_setSlot_to_(IoObject *self, IoSymbol *slotName, IoObject *value) { void IoObject_removeSlot_(IoObject *self, IoSymbol *slotName) { IoObject_createSlotsIfNeeded(self); PHash_removeKey_(IoObject_slots(self), slotName); + IOSTATE->slotVersion++; } IoObject *IoObject_rawGetSlot_target_(IoObject *self, IoSymbol *slotName, @@ -915,7 +961,9 @@ IO_METHOD(IoObject, protoPerformWithArgList) { */ IoSymbol *slotName = IoMessage_locals_symbolArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); IoList *args = IoMessage_locals_listArgAt_(m, locals, 1); + if (IOSTATE->errorRaised) return IONIL(self); List *argList = IoList_rawList(args); IoObject *context; IoObject *v = IoObject_rawGetSlot_context_(self, slotName, &context); @@ -1028,8 +1076,11 @@ IO_METHOD(IoObject, protoSet_to_) { */ IoSymbol *slotName = IoMessage_locals_symbolArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); IoObject *slotValue = IoMessage_locals_valueArgAt_(m, locals, 1); + if (IOSTATE->errorRaised) return IONIL(self); IoObject_inlineSetSlot_to_(self, slotName, slotValue); + IOSTATE->slotVersion++; return slotValue; } @@ -1041,12 +1092,15 @@ IO_METHOD(IoObject, protoSetSlotWithType) { */ IoSymbol *slotName = IoMessage_locals_symbolArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); IoObject *slotValue = IoMessage_locals_valueArgAt_(m, locals, 1); + if (IOSTATE->errorRaised) return IONIL(self); IoObject_inlineSetSlot_to_(self, slotName, slotValue); IoObject_createSlotsIfNeeded(slotValue); if (PHash_at_(IoObject_slots(slotValue), IOSTATE->typeSymbol) == NULL) { IoObject_inlineSetSlot_to_(slotValue, IOSTATE->typeSymbol, slotName); } + IOSTATE->slotVersion++; return slotValue; } @@ -1092,6 +1146,7 @@ IO_METHOD(IoObject, protoUpdateSlot_to_) { if (obj) { IoObject_inlineSetSlot_to_(self, slotName, slotValue); + IOSTATE->slotVersion++; } else { IoState_error_(IOSTATE, m, "Slot %s not found. Must define slot using := operator " @@ -1300,13 +1355,34 @@ IO_METHOD(IoObject, doMessage) { context in which the message is evaluated. */ + IoState *state = IOSTATE; + + // Get the message argument (unevaluated - it's a message literal) IoMessage *aMessage = IoMessage_locals_messageArgAt_(m, locals, 0); IoObject *context = self; + // Get optional context argument (may need evaluation) if (IoMessage_argCount(m) >= 2) { context = IoMessage_locals_valueArgAt_(m, locals, 1); } + // Use iterative path if eval loop is active, otherwise recursive fallback + if (state->currentFrame != NULL) { + if (FRAME_DATA(state->currentFrame)->message == m) { + // Called directly from eval loop - use frame-state (zero C stack growth) + IoEvalFrame *frame = state->currentFrame; + IoEvalFrameData *fd = FRAME_DATA(frame); + fd->controlFlow.doInfo.codeMessage = aMessage; + fd->controlFlow.doInfo.evalTarget = self; + fd->controlFlow.doInfo.evalLocals = context; + fd->state = FRAME_STATE_DO_EVAL; + state->needsControlFlowHandling = 1; + return state->ioNil; + } + // Called indirectly (e.g. from interpolate) - use nested eval + return IoMessage_locals_performOn_iterative(aMessage, context, self); + } + return IoMessage_locals_performOn_(aMessage, context, self); } @@ -1315,9 +1391,11 @@ IO_METHOD(IoObject, doString) { Evaluates the string in the context of the receiver. Returns the result. */ + IoState *state = IOSTATE; + + // Get the string argument (may need evaluation if not a literal) IoSymbol *string = IoMessage_locals_seqArgAt_(m, locals, 0); IoSymbol *label; - IoObject *result; if (IoMessage_argCount(m) > 1) { label = IoMessage_locals_symbolArgAt_(m, locals, 1); @@ -1325,9 +1403,37 @@ IO_METHOD(IoObject, doString) { label = IOSYMBOL("doString"); } - IoState_pushRetainPool(IOSTATE); + // Use iterative path if eval loop is active, otherwise recursive fallback + if (state->currentFrame != NULL) { + IoState_pushCollectorPause(state); + IoMessage *codeMsg = IoMessage_newFromText_labelSymbol_(state, + CSTRING(string), label); + IoState_popCollectorPause(state); + + if (!codeMsg) { + IoState_error_(state, m, "doString: failed to compile string"); + return state->ioNil; + } + + if (FRAME_DATA(state->currentFrame)->message == m) { + // Called directly from eval loop - use frame-state (zero C stack growth) + IoEvalFrame *frame = state->currentFrame; + IoEvalFrameData *fd = FRAME_DATA(frame); + fd->controlFlow.doInfo.codeMessage = codeMsg; + fd->controlFlow.doInfo.evalTarget = self; + fd->controlFlow.doInfo.evalLocals = self; + fd->state = FRAME_STATE_DO_EVAL; + state->needsControlFlowHandling = 1; + return state->ioNil; + } + // Called indirectly (e.g. from interpolate) - use nested eval + return IoMessage_locals_performOn_iterative(codeMsg, self, self); + } + + IoObject *result; + IoState_pushRetainPool(state); result = IoObject_rawDoString_label_(self, string, label); - IoState_popRetainPoolExceptFor_(IOSTATE, result); + IoState_popRetainPoolExceptFor_(state, result); return result; } @@ -1337,16 +1443,50 @@ IO_METHOD(IoObject, doFile) { pathString is relative to the current working directory. */ + IoState *state = IOSTATE; + IoSymbol *path = IoMessage_locals_symbolArgAt_(m, locals, 0); - IoFile *file = IoFile_newWithPath_(IOSTATE, path); - IoSymbol *string = - (IoSymbol *)IoSeq_rawAsSymbol(IoFile_contents(file, locals, m)); + if (state->errorRaised) return state->ioNil; - if (IoSeq_rawSize(string)) { - return IoObject_rawDoString_label_(self, string, path); - } else { + IoFile *file = IoFile_newWithPath_(state, path); + IoObject *contents = IoFile_contents(file, locals, m); + if (state->errorRaised) return state->ioNil; + + IoSymbol *string = (IoSymbol *)IoSeq_rawAsSymbol(contents); + + if (!IoSeq_rawSize(string)) { return IONIL(self); } + + // Use iterative path if eval loop is active, otherwise recursive fallback + if (state->currentFrame != NULL) { + IoState_pushCollectorPause(state); + IoMessage *codeMsg = IoMessage_newFromText_labelSymbol_(state, + CSTRING(string), path); + IoState_popCollectorPause(state); + + if (!codeMsg) { + IoState_error_(state, m, "doFile: failed to compile file %s", + CSTRING(path)); + return state->ioNil; + } + + if (FRAME_DATA(state->currentFrame)->message == m) { + // Called directly from eval loop - use frame-state (zero C stack growth) + IoEvalFrame *frame = state->currentFrame; + IoEvalFrameData *fd = FRAME_DATA(frame); + fd->controlFlow.doInfo.codeMessage = codeMsg; + fd->controlFlow.doInfo.evalTarget = self; + fd->controlFlow.doInfo.evalLocals = self; + fd->state = FRAME_STATE_DO_EVAL; + state->needsControlFlowHandling = 1; + return state->ioNil; + } + // Called indirectly - use nested eval + return IoMessage_locals_performOn_iterative(codeMsg, self, self); + } + + return IoObject_rawDoString_label_(self, string, path); } IO_METHOD(IoObject, isIdenticalTo) { @@ -1403,6 +1543,10 @@ IO_METHOD(IoObject, foreachSlot) { IoState_pushRetainPool(IOSTATE); IoMessage_foreachArgs(m, self, &keyName, &valueName, &doMessage); + if (IOSTATE->errorRaised) { + IoState_popRetainPool(IOSTATE); + return IONIL(self); + } PHASH_FOREACH( IoObject_slots(self), key, value, IoState_clearTopPool(IOSTATE); diff --git a/libs/iovm/source/IoObject.h b/libs/iovm/source/IoObject.h index e5ed7bef3..9a589556d 100644 --- a/libs/iovm/source/IoObject.h +++ b/libs/iovm/source/IoObject.h @@ -18,6 +18,12 @@ extern "C" { #define IOREF(value) IoObject_addingRef_((IoObject *)self, (IoObject *)value) //#define IOALLOCREF(value) IoObject_isReferenced_(value, 1) +#ifdef COLLECTOR_USE_REFCOUNT +#define IOUNREF(value) Collector_value_removingRefTo_(IOCOLLECTOR, (void *)(value)) +#else +#define IOUNREF(value) +#endif + #define IOOBJECT_ISTYPE(self, typeName) \ IoObject_hasCloneFunc_(self, (IoTagCloneFunc *)Io##typeName##_rawClone) @@ -107,6 +113,10 @@ IOVM_API void IoObject_compact(IoObject *self); IOVM_API char *IoObject_markColorName(IoObject *self); IOVM_API void IoObject_show(IoObject *self); +// identity / thisContext + +IOVM_API IO_METHOD(IoObject, self); + // proto IOVM_API IO_METHOD(IoObject, clone); diff --git a/libs/iovm/source/IoObject_flow.c b/libs/iovm/source/IoObject_flow.c index e426feaca..7a21accba 100644 --- a/libs/iovm/source/IoObject_flow.c +++ b/libs/iovm/source/IoObject_flow.c @@ -1,6 +1,7 @@ #include "IoObject.h" #include "IoNumber.h" +#include "IoEvalFrame.h" // loops --------------------------------------- @@ -12,37 +13,48 @@ IO_METHOD(IoObject, while) { */ IoMessage_assertArgCount_receiver_(m, 2, self); + if (IOSTATE->errorRaised) return IONIL(self); - { - IoObject *result = IONIL(self); - IoState *state = IOSTATE; - unsigned char c; + IoState *state = IOSTATE; + // Bootstrap fallback: no eval loop running + if (!state->currentFrame) { + IoObject *result = IONIL(self); IoState_resetStopStatus(state); IoState_pushRetainPool(state); - for (;;) { IoObject *v; IoState_clearTopPool(state); IoState_stackRetain_(state, result); v = IoMessage_locals_valueArgAt_(m, locals, 0); - c = ISTRUE( - IoMessage_locals_performOn_(IOSTATE->asBooleanMessage, v, v)); - - if (!c) { + if (!ISTRUE(IoMessage_locals_performOn_(state->asBooleanMessage, v, v))) break; - } - - result = (IoObject *)IoMessage_locals_valueArgAt_(m, locals, 1); - - if (IoState_handleStatus(state)) { - goto done; - } + result = IoMessage_locals_valueArgAt_(m, locals, 1); + if (IoState_handleStatus(state)) break; } - done: IoState_popRetainPoolExceptFor_(state, result); return result; } + + IoEvalFrame *frame = state->currentFrame; + IoEvalFrameData *fd = FRAME_DATA(frame); + + // Get the argument messages (not evaluated yet) + IoMessage *conditionMsg = IoMessage_rawArgAt_(m, 0); + IoMessage *bodyMsg = IoMessage_rawArgAt_(m, 1); + + // Set up control flow info + fd->controlFlow.whileInfo.conditionMsg = conditionMsg; + fd->controlFlow.whileInfo.bodyMsg = bodyMsg; + fd->controlFlow.whileInfo.lastResult = NULL; + + // Change frame state to start WHILE evaluation + fd->state = FRAME_STATE_WHILE_EVAL_CONDITION; + + // Signal that we've set up control flow handling + state->needsControlFlowHandling = 1; + + return state->ioNil; } IO_METHOD(IoObject, loop) { @@ -51,26 +63,41 @@ IO_METHOD(IoObject, loop) { */ IoMessage_assertArgCount_receiver_(m, 1, self); - { - IoState *state = IOSTATE; - IoObject *result; + if (IOSTATE->errorRaised) return IONIL(self); - IoState_resetStopStatus(IOSTATE); - IoState_pushRetainPool(state); + IoState *state = IOSTATE; + // Bootstrap fallback: no eval loop running + if (!state->currentFrame) { + IoObject *result; + IoState_resetStopStatus(state); + IoState_pushRetainPool(state); for (;;) { IoState_clearTopPool(state); - result = IoMessage_locals_valueArgAt_(m, locals, 0); - - if (IoState_handleStatus(IOSTATE)) { - goto done; - } + if (IoState_handleStatus(state)) break; } - done: IoState_popRetainPoolExceptFor_(state, result); return result; } + + IoEvalFrame *frame = state->currentFrame; + IoEvalFrameData *fd = FRAME_DATA(frame); + + // Get the body message (not evaluated yet) + IoMessage *bodyMsg = IoMessage_rawArgAt_(m, 0); + + // Set up control flow info + fd->controlFlow.loopInfo.bodyMsg = bodyMsg; + fd->controlFlow.loopInfo.lastResult = NULL; + + // Change frame state to start LOOP evaluation + fd->state = FRAME_STATE_LOOP_EVAL_BODY; + + // Signal that we've set up control flow handling + state->needsControlFlowHandling = 1; + + return state->ioNil; } IO_METHOD(IoObject, for) @@ -81,9 +108,12 @@ IO_METHOD(IoObject, for) */ IoMessage_assertArgCount_receiver_(m, 4, self); + if (IOSTATE->errorRaised) return IONIL(self); - { - IoState *state = IOSTATE; + IoState *state = IOSTATE; + + // Bootstrap fallback: no eval loop running + if (!state->currentFrame) { IoMessage *indexMessage = IoMessage_rawArgAt_(m, 0); IoMessage *doMessage; IoObject *result = IONIL(self); @@ -92,55 +122,68 @@ IO_METHOD(IoObject, for) double startValue = IoMessage_locals_doubleArgAt_(m, locals, 1); double endValue = IoMessage_locals_doubleArgAt_(m, locals, 2); double increment = 1; - IoNumber *num = NULL; if (IoMessage_argCount(m) > 4) { increment = IoMessage_locals_doubleArgAt_(m, locals, 3); doMessage = IoMessage_rawArgAt_(m, 4); } else { doMessage = IoMessage_rawArgAt_(m, 3); - // if (startValue > endValue) - // { - // increment = -1; - // } } IoState_resetStopStatus(state); IoState_pushRetainPool(state); for (i = startValue;; i += increment) { - if (increment > 0) { - if (i > endValue) - break; - } else { - if (i < endValue) - break; - } - - /*if (result != locals && result != self) - * IoState_immediatelyFreeIfUnreferenced_(state, result);*/ + if (increment > 0) { if (i > endValue) break; } + else { if (i < endValue) break; } IoState_clearTopPool(state); - - { - num = IONUMBER(i); - IoObject_addingRef_(locals, num); - PHash_at_put_(IoObject_slots(locals), slotName, num); - - // IoObject_setSlot_to_(self, slotName, num); - } - - /*IoObject_setSlot_to_(locals, slotName, IONUMBER(i));*/ + IoObject_addingRef_(locals, IONUMBER(i)); + PHash_at_put_(IoObject_slots(locals), slotName, IONUMBER(i)); result = IoMessage_locals_performOn_(doMessage, locals, self); - - if (IoState_handleStatus(IOSTATE)) { - goto done; - } + if (IoState_handleStatus(state)) break; } - - done: IoState_popRetainPoolExceptFor_(state, result); return result; } + + IoEvalFrame *frame = state->currentFrame; + IoEvalFrameData *fd = FRAME_DATA(frame); + + IoMessage *indexMessage = IoMessage_rawArgAt_(m, 0); + IoSymbol *counterName = IoMessage_name(indexMessage); + + double startValue = IoMessage_locals_doubleArgAt_(m, locals, 1); + if (state->errorRaised) return IONIL(self); + double endValue = IoMessage_locals_doubleArgAt_(m, locals, 2); + if (state->errorRaised) return IONIL(self); + double increment = 1; + IoMessage *doMessage; + + if (IoMessage_argCount(m) > 4) { + increment = IoMessage_locals_doubleArgAt_(m, locals, 3); + if (state->errorRaised) return IONIL(self); + doMessage = IoMessage_rawArgAt_(m, 4); + } else { + doMessage = IoMessage_rawArgAt_(m, 3); + } + + // Set up control flow info + fd->controlFlow.forInfo.bodyMsg = doMessage; + fd->controlFlow.forInfo.counterName = counterName; + fd->controlFlow.forInfo.startValue = startValue; + fd->controlFlow.forInfo.endValue = endValue; + fd->controlFlow.forInfo.increment = increment; + fd->controlFlow.forInfo.currentValue = startValue; + fd->controlFlow.forInfo.lastResult = NULL; + fd->controlFlow.forInfo.initialized = 0; + + // Change frame state to start FOR evaluation + fd->state = FRAME_STATE_FOR_EVAL_BODY; + + // Signal that we've set up control flow handling + state->needsControlFlowHandling = 1; + + return state->ioNil; } IO_METHOD(IoObject, return ) { @@ -213,13 +256,47 @@ IO_METHOD(IoObject, if) { Returns the result of the evaluated message or Nil if none was evaluated. */ - IoObject *r = IoMessage_locals_valueArgAt_(m, locals, 0); - const int condition = - ISTRUE(IoMessage_locals_performOn_(IOSTATE->asBooleanMessage, r, r)); - const int index = condition ? 1 : 2; + IoState *state = IOSTATE; + + // Bootstrap fallback: if no eval loop is running (currentFrame == NULL), + // evaluate inline. This only happens during VM initialization. + if (!state->currentFrame) { + IoObject *r = IoMessage_locals_valueArgAt_(m, locals, 0); + const int condition = + ISTRUE(IoMessage_locals_performOn_(state->asBooleanMessage, r, r)); + const int index = condition ? 1 : 2; + if (index < IoMessage_argCount(m)) + return IoMessage_locals_valueArgAt_(m, locals, index); + return IOBOOL(self, condition); + } + + IoEvalFrame *frame = state->currentFrame; + IoEvalFrameData *fd = FRAME_DATA(frame); + + // Get the argument messages (not evaluated yet) + IoMessage *conditionMsg = IoMessage_rawArgAt_(m, 0); + IoMessage *trueBranch = IoMessage_rawArgAt_(m, 1); + IoMessage *falseBranch = IoMessage_argCount(m) > 2 ? IoMessage_rawArgAt_(m, 2) : NULL; + + // DEBUG + if (state->showAllMessages) { + printf("IF primitive: condition=%s, true=%s, false=%s\n", + conditionMsg ? CSTRING(IoMessage_name(conditionMsg)) : "NULL", + trueBranch ? CSTRING(IoMessage_name(trueBranch)) : "NULL", + falseBranch ? CSTRING(IoMessage_name(falseBranch)) : "NULL"); + } + + // Set up control flow info + fd->controlFlow.ifInfo.conditionMsg = conditionMsg; + fd->controlFlow.ifInfo.trueBranch = trueBranch; + fd->controlFlow.ifInfo.falseBranch = falseBranch; + + // Change frame state to start IF evaluation + fd->state = FRAME_STATE_IF_EVAL_CONDITION; - if (index < IoMessage_argCount(m)) - return IoMessage_locals_valueArgAt_(m, locals, index); + // Signal that we've set up control flow handling + state->needsControlFlowHandling = 1; - return IOBOOL(self, condition); + // Return placeholder (will be replaced by actual result) + return state->ioNil; } diff --git a/libs/iovm/source/IoObject_inline.h b/libs/iovm/source/IoObject_inline.h index 0701ab2cc..4298e0b5b 100644 --- a/libs/iovm/source/IoObject_inline.h +++ b/libs/iovm/source/IoObject_inline.h @@ -232,23 +232,23 @@ IOINLINE IoObject *IoObject_addingRef_(IoObject *self, IoObject *ref) { IOINLINE void IoObject_inlineSetSlot_to_(IoObject *self, IoSymbol *slotName, IoObject *value) { IoObject_createSlotsIfNeeded(self); - /* - if (!slotName->isSymbol) - { - printf("Io System Error: setSlot slotName not symbol\n"); - exit(1); - } - */ + +#ifdef COLLECTOR_USE_REFCOUNT + IoObject *oldValue = (IoObject *)PHash_at_(IoObject_slots(self), slotName); +#endif PHash_at_put_(IoObject_slots(self), IOREF(slotName), IOREF(value)); - IoObject_isDirty_(self, 1); - /* - if(PHash_at_put_(IoObject_slots(self), IOREF(slotName), IOREF(value))) - { - IoObject_isDirty_(self, 1); +#ifdef COLLECTOR_USE_REFCOUNT + if (oldValue && oldValue != value) { + Collector_value_removingRefTo_(IOCOLLECTOR, oldValue); + // Don't drain here — the caller may still be using 'self' and + // draining could free objects unsafely. The for-loop fast paths + // drain explicitly where it's safe. } - */ +#endif + + IoObject_isDirty_(self, 1); } IOINLINE_RECURSIVE IoObject *IoObject_rawGetSlot_context_(IoObject *self, diff --git a/libs/iovm/source/IoSeq_immutable.c b/libs/iovm/source/IoSeq_immutable.c index 11ab192c7..2cb7a580b 100644 --- a/libs/iovm/source/IoSeq_immutable.c +++ b/libs/iovm/source/IoSeq_immutable.c @@ -39,6 +39,7 @@ IO_METHOD(IoSeq, with) { for (n = 0; n < argCount; n++) { IoSeq *v = IoMessage_locals_seqArgAt_(m, locals, n); + if (IOSTATE->errorRaised) return IONIL(self); UArray_append_(ba, DATA(v)); } @@ -248,13 +249,17 @@ IOVM_API IO_METHOD(IoSeq, parseJson) { JSON_Value *output; char *value = IoSeq_asCString(self); - if (IoSeq_rawSizeInBytes(self) == 0) + if (IoSeq_rawSizeInBytes(self) == 0) { IoState_error_(IOSTATE, m, "Can't parse empty string."); + return IONIL(self); + } output = json_parse_string_with_comments(value); - if (!output) + if (!output) { IoState_error_(IOSTATE, m, "Can't parse JSON."); + return IONIL(self); + } result = parse_json_object(self, json_object(output)); @@ -380,11 +385,13 @@ IO_METHOD(IoSeq, exclusiveSlice) { */ long fromIndex = IoMessage_locals_longArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); long last = UArray_size(DATA(self)); UArray *ba; if (IoMessage_argCount(m) > 1) { last = IoMessage_locals_longArgAt_(m, locals, 1); + if (IOSTATE->errorRaised) return IONIL(self); } ba = UArray_slice(DATA(self), fromIndex, last); @@ -405,11 +412,13 @@ IO_METHOD(IoSeq, inclusiveSlice) { */ long fromIndex = IoMessage_locals_longArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); long last = UArray_size(DATA(self)); UArray *ba; if (IoMessage_argCount(m) > 1) { last = IoMessage_locals_longArgAt_(m, locals, 1); + if (IOSTATE->errorRaised) return IONIL(self); } if (last == -1) { @@ -441,6 +450,7 @@ IO_METHOD(IoSeq, between) { IoSeq *fromSeq, *toSeq; fromSeq = (IoSeq *)IoMessage_locals_valueArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); if (ISSEQ(fromSeq)) { if (IoSeq_rawSize(fromSeq) == 0) { @@ -463,6 +473,7 @@ IO_METHOD(IoSeq, between) { } toSeq = (IoSeq *)IoMessage_locals_valueArgAt_(m, locals, 1); + if (IOSTATE->errorRaised) return IONIL(self); if (ISSEQ(toSeq)) { end = UArray_find_from_(DATA(self), DATA(toSeq), start); @@ -497,6 +508,7 @@ IO_METHOD(IoSeq, findSeqs) { */ IoList *others = IoMessage_locals_listArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); List *delims = IoList_rawList(others); long f = 0; long firstIndex = -1; @@ -515,6 +527,7 @@ IO_METHOD(IoSeq, findSeqs) { IoState_error_(IOSTATE, m, "requires Sequences as arguments, not %ss", IoObject_name((IoSeq *)s)); + return IONIL(self); } index = UArray_find_from_(DATA(self), DATA(((IoSeq *)s)), f); @@ -548,8 +561,11 @@ IO_METHOD(IoSeq, findSeq) { long f = 0; long index; + if (IOSTATE->errorRaised) return IONIL(self); + if (IoMessage_argCount(m) > 1) { f = IoMessage_locals_longArgAt_(m, locals, 1); + if (IOSTATE->errorRaised) return IONIL(self); } index = UArray_find_from_(DATA(self), DATA(otherSequence), f); @@ -569,8 +585,11 @@ IO_METHOD(IoSeq, reverseFindSeq) { long from = UArray_size(DATA(self)); long index; + if (IOSTATE->errorRaised) return IONIL(self); + if (IoMessage_argCount(m) > 1) { from = IoMessage_locals_intArgAt_(m, locals, 1); + if (IOSTATE->errorRaised) return IONIL(self); } index = UArray_rFind_from_(DATA(self), DATA(other), from); @@ -588,6 +607,7 @@ IO_METHOD(IoSeq, beginsWithSeq) { */ IoSeq *other = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); return IOBOOL(self, UArray_beginsWith_(DATA(self), DATA(other))); } @@ -598,6 +618,7 @@ IO_METHOD(IoSeq, endsWithSeq) { */ IoSeq *other = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); return IOBOOL(self, UArray_endsWith_(DATA(self), DATA(other))); } @@ -610,6 +631,7 @@ IO_METHOD(IoSeq, contains) { // will make this more efficient when Numbers are Arrays IoNumber *n = IoMessage_locals_numberArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); UArray tmp = IoNumber_asStackUArray(n); return IOBOOL(self, UArray_contains_(DATA(self), &tmp)); @@ -622,6 +644,7 @@ IO_METHOD(IoSeq, containsSeq) { */ IoSeq *other = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); return IOBOOL(self, UArray_contains_(DATA(self), DATA(other))); } @@ -633,6 +656,7 @@ IO_METHOD(IoSeq, containsAnyCaseSeq) { */ IoSeq *other = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); return IOBOOL(self, UArray_containsAnyCase_(DATA(self), DATA(other))); } @@ -659,6 +683,7 @@ IO_METHOD(IoSeq, isEqualAnyCase) { */ IoSeq *other = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); return IOBOOL(self, UArray_equalsAnyCase_(DATA(self), DATA(other))); } @@ -745,6 +770,7 @@ List *IoSeq_byteArrayListForSeqList(IoSeq *self, IoObject *locals, IoMessage *m, IoState_error_(IOSTATE, m, "requires Sequences as arguments, not %ss", IoObject_name((IoSeq *)s)); + return NULL; } List_append_(list, DATA(((IoSeq *)s)));); @@ -755,13 +781,22 @@ List *IoSeq_byteArrayListForSeqList(IoSeq *self, IoObject *locals, IoMessage *m, IoObject *IoSeq_splitToFunction(IoSeq *self, IoObject *locals, IoMessage *m, IoSplitFunction *func) { IoList *output = IoList_new(IOSTATE); - List *others = IoSeq_byteArrayListForSeqList( - self, locals, m, IoSeq_stringListForArgs(self, locals, m)); + IoList *seqs = IoSeq_stringListForArgs(self, locals, m); + List *others; int i; + if (IOSTATE->errorRaised) return output; + + others = IoSeq_byteArrayListForSeqList(self, locals, m, seqs); + if (IOSTATE->errorRaised || others == NULL) { + return output; + } + for (i = 0; i < List_size(others); i++) { if (UArray_size(List_at_(others, i)) == 0) { IoState_error_(IOSTATE, m, "empty string argument"); + List_free(others); + return output; } } @@ -940,6 +975,7 @@ IO_METHOD(IoSeq, foreach) { IoMessage_foreachArgs(m, self, &indexSlotName, &characterSlotName, &doMessage); + if (IOSTATE->errorRaised) return IONIL(self); IoState_pushRetainPool(IOSTATE); @@ -1000,6 +1036,7 @@ IO_METHOD(IoSeq, cloneAppendSeq) { IOSTATE, m, "argument 0 to method '%s' must be a number or string, not a '%s'", CSTRING(IoMessage_name(m)), IoObject_name(other)); + return IONIL(self); } if (UArray_size(DATA(other)) == 0) { @@ -1078,6 +1115,7 @@ IO_METHOD(IoSeq, cloneAppendPath) { */ IoSeq *component = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); UArray *ba = UArray_clone(DATA(self)); UArray_appendPath_(ba, DATA(component)); return IoState_symbolWithUArray_copy_(IOSTATE, ba, 0); @@ -1114,7 +1152,10 @@ IO_METHOD(IoSeq, beforeSeq) { */ IoSeq *other = IoMessage_locals_seqArgAt_(m, locals, 0); - long pos = UArray_find_(DATA(self), DATA(other)); + long pos; + if (IOSTATE->errorRaised) return IONIL(self); + + pos = UArray_find_(DATA(self), DATA(other)); if (pos != -1) { UArray *ba = UArray_slice(DATA(self), 0, pos); @@ -1141,7 +1182,9 @@ IO_METHOD(IoSeq, afterSeq) { */ IoSeq *other = IoMessage_locals_seqArgAt_(m, locals, 0); - long pos = UArray_find_(DATA(self), DATA(other)); + long pos; + if (IOSTATE->errorRaised) return IONIL(self); + pos = UArray_find_(DATA(self), DATA(other)); if (pos != -1) { UArray *ba = UArray_slice(DATA(self), pos + UArray_size(DATA(other)), @@ -1187,6 +1230,7 @@ IO_METHOD(IoSeq, occurrencesOfSeq) { */ IoSeq *other = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); size_t count = UArray_count_(DATA(self), DATA(other)); return IONUMBER(count); } @@ -1235,6 +1279,7 @@ IO_METHOD(IoSeq, distanceTo) { */ IoSeq *other = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); double d; d = UArray_distanceTo_(DATA(self), DATA(other)); @@ -1247,6 +1292,7 @@ IO_METHOD(IoSeq, greaterThan_) { */ IoSeq *other = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); return IOBOOL(self, UArray_greaterThan_(DATA(self), DATA(other))); } @@ -1256,6 +1302,7 @@ IO_METHOD(IoSeq, lessThan_) { */ IoSeq *other = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); return IOBOOL(self, UArray_lessThan_(DATA(self), DATA(other))); } @@ -1266,6 +1313,7 @@ IO_METHOD(IoSeq, greaterThanOrEqualTo_) { */ IoSeq *other = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); return IOBOOL(self, UArray_greaterThanOrEqualTo_(DATA(self), DATA(other))); } @@ -1275,6 +1323,7 @@ IO_METHOD(IoSeq, lessThanOrEqualTo_) { */ IoSeq *other = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); return IOBOOL(self, UArray_lessThanOrEqualTo_(DATA(self), DATA(other))); } @@ -1311,7 +1360,9 @@ IO_METHOD(IoSeq, asStruct) { const unsigned char *data = UArray_bytes(DATA(self)); size_t size = UArray_sizeInBytes(DATA(self)); size_t offset = 0; - List *members = IoList_rawList(IoMessage_locals_listArgAt_(m, locals, 0)); + IoList *membersList = IoMessage_locals_listArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); + List *members = IoList_rawList(membersList); int memberIndex; IOASSERT(List_size(members) % 2 == 0, "members list must be even number"); @@ -1373,7 +1424,9 @@ IO_METHOD(IoSeq, withStruct) { The output pointStructSeq would contain 2 raw 32 bit floats. */ - List *members = IoList_rawList(IoMessage_locals_listArgAt_(m, locals, 0)); + IoList *membersList = IoMessage_locals_listArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); + List *members = IoList_rawList(membersList); int memberIndex; size_t maxSize = List_size(members) * 8; IoSeq *s = IoSeq_newWithData_length_( diff --git a/libs/iovm/source/IoSeq_mutable.c b/libs/iovm/source/IoSeq_mutable.c index 195c50fe6..1b91ad44a 100644 --- a/libs/iovm/source/IoSeq_mutable.c +++ b/libs/iovm/source/IoSeq_mutable.c @@ -25,7 +25,10 @@ character encoding #define DATA(self) ((UArray *)IoObject_dataPointer(self)) -#define IO_ASSERT_NOT_SYMBOL(self) IoAssertNotSymbol(self, m) +#define IO_ASSERT_NOT_SYMBOL(self) do { \ + IoAssertNotSymbol(self, m); \ + if (IOSTATE->errorRaised) return IONIL(self); \ +} while(0) #define IO_ASSERT_NUMBER_ENCODING(self) \ IOASSERT(DATA(self)->encoding == CENCODING_NUMBER, \ "operation not valid on non-number encodings") @@ -124,7 +127,9 @@ IO_METHOD(IoSeq, copy) { IO_ASSERT_NOT_SYMBOL(self); - IoSeq_rawCopy_(self, IoMessage_locals_seqArgAt_(m, locals, 0)); + IoSeq *other = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); + IoSeq_rawCopy_(self, other); return self; } @@ -171,7 +176,9 @@ IO_METHOD(IoSeq, atInsertSeq) { */ size_t n = IoMessage_locals_sizetArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); IoSeq *otherSeq = IoMessage_locals_valueAsStringArgAt_(m, locals, 1); + if (IOSTATE->errorRaised) return IONIL(self); IO_ASSERT_NOT_SYMBOL(self); @@ -189,7 +196,9 @@ IO_METHOD(IoSeq, insertSeqEvery) { */ IoSeq *otherSeq = IoMessage_locals_valueAsStringArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); size_t itemCount = IoMessage_locals_sizetArgAt_(m, locals, 1); + if (IOSTATE->errorRaised) return IONIL(self); IO_ASSERT_NOT_SYMBOL(self); @@ -243,7 +252,9 @@ IO_METHOD(IoSeq, removeSlice) { */ long start = IoMessage_locals_longArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); long end = IoMessage_locals_longArgAt_(m, locals, 1); + if (IOSTATE->errorRaised) return IONIL(self); IO_ASSERT_NOT_SYMBOL(self); @@ -271,7 +282,9 @@ IO_METHOD(IoSeq, leaveThenRemove) { */ size_t itemsToLeave = IoMessage_locals_sizetArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); size_t itemsToRemove = IoMessage_locals_sizetArgAt_(m, locals, 1); + if (IOSTATE->errorRaised) return IONIL(self); IO_ASSERT_NOT_SYMBOL(self); @@ -298,6 +311,7 @@ void IoSeq_rawPio_reallocateToSize_(IoSeq *self, size_t size) { if (ISSYMBOL(self)) { IoState_error_(IOSTATE, NULL, "attempt to resize an immutable Sequence"); + return; } UArray_sizeTo_(DATA(self), size); @@ -325,7 +339,9 @@ IO_METHOD(IoSeq, replaceSeq) { */ IoSeq *subSeq = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); IoSeq *otherSeq = IoMessage_locals_seqArgAt_(m, locals, 1); + if (IOSTATE->errorRaised) return IONIL(self); IO_ASSERT_NOT_SYMBOL(self); UArray_replace_with_(DATA(self), DATA(subSeq), DATA(otherSeq)); return self; @@ -337,6 +353,7 @@ IO_METHOD(IoSeq, removeSeq) { */ IoSeq *subSeq = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); IO_ASSERT_NOT_SYMBOL(self); UArray_remove_(DATA(self), DATA(subSeq)); return self; @@ -351,11 +368,14 @@ IO_METHOD(IoSeq, replaceFirstSeq) { */ IoSeq *subSeq = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); IoSeq *otherSeq = IoMessage_locals_seqArgAt_(m, locals, 1); + if (IOSTATE->errorRaised) return IONIL(self); size_t startIndex = 0; if (IoMessage_argCount(m) > 2) { startIndex = IoMessage_locals_longArgAt_(m, locals, 2); + if (IOSTATE->errorRaised) return IONIL(self); } IO_ASSERT_NOT_SYMBOL(self); @@ -380,6 +400,7 @@ IO_METHOD(IoSeq, atPut) { */ size_t i = IoMessage_locals_longArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); UArray *a = DATA(self); IO_ASSERT_NOT_SYMBOL(self); @@ -423,6 +444,7 @@ IO_METHOD(IoSeq, clipBeforeSeq) { */ IoSeq *otherSeq = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); IO_ASSERT_NOT_SYMBOL(self); UArray_clipBefore_(DATA(self), DATA(otherSeq)); return self; @@ -436,6 +458,7 @@ IO_METHOD(IoSeq, clipAfterSeq) { */ IoSeq *otherSeq = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); IO_ASSERT_NOT_SYMBOL(self); UArray_clipAfter_(DATA(self), DATA(otherSeq)); @@ -450,6 +473,7 @@ IO_METHOD(IoSeq, clipBeforeEndOfSeq) { */ IoSeq *otherSeq = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); IO_ASSERT_NOT_SYMBOL(self); UArray_clipBeforeEndOf_(DATA(self), DATA(otherSeq)); return self; @@ -463,6 +487,7 @@ IO_METHOD(IoSeq, clipAfterStartOfSeq) { */ IoSeq *otherSeq = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); IO_ASSERT_NOT_SYMBOL(self); UArray_clipAfterStartOf_(DATA(self), DATA(otherSeq)); return self; @@ -519,6 +544,7 @@ IO_METHOD(IoSeq, replaceMap) { */ IoMap *map = IoMessage_locals_mapArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); UArray *ba = DATA(self); IO_ASSERT_NOT_SYMBOL(self); @@ -548,15 +574,21 @@ IO_METHOD(IoSeq, translate) { same positions in toChars. Returns self. */ + IoSeq *fcSeq = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); + IoSeq *tcSeq = IoMessage_locals_seqArgAt_(m, locals, 1); + if (IOSTATE->errorRaised) return IONIL(self); + UArray *ba = DATA(self); - UArray *fc = DATA(IoMessage_locals_seqArgAt_(m, locals, 0)); - UArray *tc = DATA(IoMessage_locals_seqArgAt_(m, locals, 1)); + UArray *fc = DATA(fcSeq); + UArray *tc = DATA(tcSeq); IO_ASSERT_NOT_SYMBOL(self); if (UArray_size(tc) != UArray_size(fc)) { IoState_error_(IOSTATE, m, "translation strings must be of the same length"); + return IONIL(self); } UArray_translate(ba, fc, tc); @@ -593,6 +625,7 @@ IO_METHOD(IoSeq, strip) { if (IoMessage_argCount(m) > 0) { IoSeq *other = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); UArray_strip_(DATA(self), DATA(other)); } else { UArray space = UArray_stackAllocedWithCString_(WHITESPACE); @@ -617,6 +650,7 @@ IO_METHOD(IoSeq, lstrip) { if (IoMessage_argCount(m) > 0) { IoSeq *other = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); UArray_lstrip_(DATA(self), DATA(other)); } else { UArray space = UArray_stackAllocedWithCString_(WHITESPACE); @@ -640,6 +674,7 @@ IO_METHOD(IoSeq, rstrip) { if (IoMessage_argCount(m) > 0) { IoSeq *other = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); UArray_rstrip_(DATA(self), DATA(other)); } else { UArray space = UArray_stackAllocedWithCString_(WHITESPACE); @@ -679,6 +714,7 @@ IO_METHOD(IoSeq, removePrefix) { */ IoSeq *other = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); IO_ASSERT_NOT_SYMBOL(self); @@ -695,6 +731,7 @@ IO_METHOD(IoSeq, removeSuffix) { */ IoSeq *other = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); IO_ASSERT_NOT_SYMBOL(self); @@ -726,6 +763,7 @@ IO_METHOD(IoSeq, appendPathSeq) { */ IoSeq *component = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); IO_ASSERT_NOT_SYMBOL(self); UArray_appendPath_(DATA(self), DATA(component)); @@ -1001,6 +1039,7 @@ IO_METHOD(IoSeq, dotProduct) { */ IoSeq *other = IoMessage_locals_seqArgAt_(m, locals, 0); + if (IOSTATE->errorRaised) return IONIL(self); IO_ASSERT_NOT_SYMBOL(self); return IONUMBER(UArray_dotProduct_(DATA(self), DATA(other))); } @@ -1215,8 +1254,9 @@ IoSeqMutateNoArgNoResultOp(ceil) #define IoSeqLongArgNumberResultOp(name) \ IoObject *IoSeq_##name(IoSeq *self, IoObject *locals, IoMessage *m) { \ - return IONUMBER(UArray_##name( \ - DATA(self), IoMessage_locals_longArgAt_(m, locals, 0))); \ + long arg = IoMessage_locals_longArgAt_(m, locals, 0); \ + if (IOSTATE->errorRaised) return IONIL(self); \ + return IONUMBER(UArray_##name(DATA(self), arg)); \ } // IoSeqLongArgNumberResultOp(setAllBitsTo_) @@ -1239,8 +1279,9 @@ IoSeqMutateNoArgNoResultOp(ceil) #define IoSeqSeqArgNoResultOp(name) \ IoObject *IoSeq_##name(IoSeq *self, IoObject *locals, IoMessage *m) { \ IO_ASSERT_NOT_SYMBOL(self); \ - UArray_##name(DATA(self), \ - DATA(IoMessage_locals_seqArgAt_(m, locals, 0))); \ + IoSeq *arg = IoMessage_locals_seqArgAt_(m, locals, 0); \ + if (IOSTATE->errorRaised) return IONIL(self); \ + UArray_##name(DATA(self), DATA(arg)); \ return self; \ } diff --git a/libs/iovm/source/IoSeq_vector.c b/libs/iovm/source/IoSeq_vector.c index dfe9a7c1b..bae03ec44 100644 --- a/libs/iovm/source/IoSeq_vector.c +++ b/libs/iovm/source/IoSeq_vector.c @@ -27,7 +27,9 @@ void *IoMessage_locals_pointArgAt_(IoMessage *m, void *locals, int n) { } void IoSeq_assertIsVector(IoSeq *self, IoObject *locals, IoMessage *m) { - IOASSERT(ISVECTOR(self), "Seq needs to be of type float32"); + if (!(ISVECTOR(self))) { + IoState_error_(IOSTATE, m, "Io Assertion 'Seq needs to be of type float32'"); + } } // --------------------------------------------- diff --git a/libs/iovm/source/IoState.c b/libs/iovm/source/IoState.c index 3f23df492..03b49095e 100644 --- a/libs/iovm/source/IoState.c +++ b/libs/iovm/source/IoState.c @@ -9,6 +9,10 @@ #include "IoObject.h" #include "IoCall.h" #include "IoCoroutine.h" +#ifdef IO_CALLCC +#include "IoContinuation.h" +#endif +#include "IoEvalFrame.h" #include "IoSeq.h" #include "IoNumber.h" #include "IoCFunction.h" @@ -38,6 +42,54 @@ void IoVMCodeInit(IoObject *context); +// Mark CFunction objects whose arguments must not be pre-evaluated. +// Called once during init after all protos are registered. +// Aliases (e.g., false.elseif := Object getSlot("if")) automatically +// inherit the flag since they reference the same CFunction object. +static void IoState_markSlotLazyArgs_(IoState *self, const char *protoId, + const char *slotName) { + IoObject *proto = IoState_protoWithId_(self, protoId); + if (!proto) return; + IoObject *f = IoObject_rawGetSlot_(proto, SIOSYMBOL(slotName)); + if (f && ISCFUNCTION(f)) { + ((IoCFunctionData *)IoObject_dataPointer(f))->isLazyArgs = 1; + } +} + +static void IoState_markLazyArgsCFunctions_(IoState *self) { + // Control flow + IoState_markSlotLazyArgs_(self, "Object", "if"); + IoState_markSlotLazyArgs_(self, "Object", "while"); + IoState_markSlotLazyArgs_(self, "Object", "for"); + IoState_markSlotLazyArgs_(self, "Object", "loop"); +#ifdef IO_CALLCC + IoState_markSlotLazyArgs_(self, "Object", "callcc"); +#endif + // Block/method construction + IoState_markSlotLazyArgs_(self, "Object", "method"); + IoState_markSlotLazyArgs_(self, "Object", "block"); + // Evaluation (body is lazy) + IoState_markSlotLazyArgs_(self, "Object", "do"); + IoState_markSlotLazyArgs_(self, "Object", "lexicalDo"); + IoState_markSlotLazyArgs_(self, "Object", "message"); + IoState_markSlotLazyArgs_(self, "Object", "foreachSlot"); + // List + IoState_markSlotLazyArgs_(self, "List", "foreach"); + IoState_markSlotLazyArgs_(self, "List", "reverseForeach"); + IoState_markSlotLazyArgs_(self, "List", "sortInPlace"); + // Number + IoState_markSlotLazyArgs_(self, "Number", "repeat"); + // Date + IoState_markSlotLazyArgs_(self, "Date", "cpuSecondsToRun"); + // Sequence + IoState_markSlotLazyArgs_(self, "Sequence", "foreach"); + // Map + IoState_markSlotLazyArgs_(self, "Map", "foreach"); + // File + IoState_markSlotLazyArgs_(self, "File", "foreach"); + IoState_markSlotLazyArgs_(self, "File", "foreachLine"); +} + void IoState_new_atAddress(void *address) { IoState *self = (IoState *)address; IoCFunction *cFunctionProto; @@ -183,6 +235,12 @@ void IoState_new_atAddress(void *address) { IoObject_setSlot_to_(core, SIOSYMBOL("Map"), IoMap_proto(self)); // IoObject_setSlot_to_(core, SIOSYMBOL("Range"), IoRange_proto(self)); IoObject_setSlot_to_(core, SIOSYMBOL("Coroutine"), self->mainCoroutine); +#ifdef IO_CALLCC + IoObject_setSlot_to_(core, SIOSYMBOL("Continuation"), + IoContinuation_proto(self)); +#endif + IoObject_setSlot_to_(core, SIOSYMBOL("EvalFrame"), + IoEvalFrame_proto(self)); IoObject_setSlot_to_(core, SIOSYMBOL("Error"), IoError_proto(self)); IoObject_setSlot_to_(core, SIOSYMBOL("File"), IoFile_proto(self)); IoObject_setSlot_to_(core, SIOSYMBOL("Directory"), @@ -212,6 +270,20 @@ void IoState_new_atAddress(void *address) { self->stopStatus = MESSAGE_STOP_STATUS_NORMAL; self->returnValue = self->ioNil; + // Initialize iterative evaluation frame stack + self->currentFrame = NULL; + self->frameDepth = 0; + self->maxFrameDepth = 10000; // Default max depth + self->framePoolCount = 0; + memset(self->framePool, 0, sizeof(self->framePool)); + self->needsControlFlowHandling = 0; +#ifdef IO_CALLCC + self->continuationInvoked = 0; +#endif + self->nestedEvalDepth = 0; + self->errorRaised = 0; + self->slotVersion = 0; + IoState_clearRetainStack(self); IoState_popCollectorPause(self); @@ -232,6 +304,7 @@ void IoState_new_atAddress(void *address) { Collector_collect(self->collector); // io_show_mem("after IoState_clearRetainStack and Collector_collect"); IoState_setupUserInterruptHandler(self); + IoState_markLazyArgsCFunctions_(self); } } @@ -261,6 +334,9 @@ void IoState_setupQuickAccessSymbols(IoState *self) { self->stackSizeSymbol = IoState_retainedSymbol(self, "stackSize"); self->typeSymbol = IoState_retainedSymbol(self, "type"); self->updateSlotSymbol = IoState_retainedSymbol(self, "updateSlot"); + + + self->runTargetSymbol = IoState_retainedSymbol(self, "runTarget"); self->runMessageSymbol = IoState_retainedSymbol(self, "runMessage"); self->runLocalsSymbol = IoState_retainedSymbol(self, "runLocals"); @@ -280,6 +356,12 @@ void IoState_setupSingletons(IoState *self) { IoObject_setSlot_to_(core, SIOSYMBOL("Message"), IoMessage_proto(self)); IoObject_setSlot_to_(core, SIOSYMBOL("Call"), IoCall_proto(self)); + // Cache Call tag/proto for inline allocation in block activation + self->callProto = IoState_protoWithId_(self, "Call"); + self->callTag = IoObject_tag(self->callProto); + self->blockLocalsPoolSize = 0; + self->callPoolSize = 0; + self->nilMessage = IoMessage_newWithName_(self, SIOSYMBOL("nil")); IoMessage_rawSetCachedResult_(self->nilMessage, self->ioNil); IoState_retain_(self, self->nilMessage); diff --git a/libs/iovm/source/IoState.h b/libs/iovm/source/IoState.h index d2936962a..6acf5ffcb 100644 --- a/libs/iovm/source/IoState.h +++ b/libs/iovm/source/IoState.h @@ -65,6 +65,8 @@ struct IoState { IoSymbol *typeSymbol; IoSymbol *updateSlotSymbol; + + IoSymbol *runTargetSymbol; IoSymbol *runMessageSymbol; IoSymbol *runLocalsSymbol; @@ -95,6 +97,28 @@ struct IoState { List *cachedNumbers; + // Fast Number allocation: cached tag/proto + data block freelist + IoTag *numberTag; + IoObject *numberProto; + #define NUMBER_DATA_POOL_MAX 512 + void *numberDataFreeList; + int numberDataFreeListSize; + + // Block activation pools: pre-built blockLocals and Call objects + // retained so GC won't collect them. Returned to pool on block return. + #define BLOCK_LOCALS_POOL_MAX 8 + IoObject *blockLocalsPool[BLOCK_LOCALS_POOL_MAX]; + int blockLocalsPoolSize; + + // Cached Call tag/proto for inline allocation + IoTag *callTag; + IoObject *callProto; + + // Call object pool: reuses GC-managed Call objects across block activations + #define CALL_POOL_MAX 8 + IoObject *callPool[CALL_POOL_MAX]; + int callPoolSize; + // singletons IoObject *ioNil; @@ -129,6 +153,36 @@ struct IoState { int stopStatus; void *returnValue; + // iterative evaluation frame stack (for continuations) + // IoEvalFrame is typedef IoObject, so this is IoObject * + IoObject *currentFrame; // Top of the evaluation frame stack + int frameDepth; // Current frame depth + int maxFrameDepth; // Maximum allowed frame depth + + // Frame object pool — reuses GC-managed IoEvalFrame objects + // Pooled frames remain valid collector objects, just parked for reuse. + #define FRAME_POOL_SIZE 256 + IoObject *framePool[FRAME_POOL_SIZE]; + int framePoolCount; + + // Control flow handling flag (for non-reentrant primitives) + int needsControlFlowHandling; // Set by primitives that modify frame state +#ifdef IO_CALLCC + int continuationInvoked; // Set when a continuation replaces the frame stack +#endif + int nestedEvalDepth; // Depth of nested eval loops (for IoCoroutine_try) + + // Slot mutation counter for inline cache invalidation. + // Incremented on every setSlot/updateSlot/removeSlot. + // Inline caches on IoMessageData store the version at cache time; + // a mismatch means the cache may be stale and needs re-lookup. + unsigned int slotVersion; + + // Error handling flag - set when IoState_error_ is called. + // Helper functions check this and return early. The eval loop + // checks it after CFunction returns and unwinds frames. + int errorRaised; + // embedding void *callbackContext; @@ -202,6 +256,11 @@ IOVM_API void IoState_runCLI(IoState *self); IOVM_API IoObject *IoState_objectWithPid_(IoState *self, PID_TYPE pid); IOVM_API int IoState_exitResult(IoState *self); +// Check for pre-evaluated argument in the current eval frame. +// Returns the pre-evaluated value if available, NULL otherwise. +// Defined in IoState_iterative.c to avoid circular IoEvalFrame.h include. +IOVM_API IoObject *IoState_preEvalArgAt_(IoState *self, IoMessage *msg, int n); + #include "IoState_coros.h" #include "IoState_debug.h" #include "IoState_eval.h" diff --git a/libs/iovm/source/IoState_callbacks.c b/libs/iovm/source/IoState_callbacks.c index 67781a834..7e69c7019 100644 --- a/libs/iovm/source/IoState_callbacks.c +++ b/libs/iovm/source/IoState_callbacks.c @@ -1,6 +1,7 @@ #include "IoState.h" #include "IoObject.h" +#include #include "IoCoroutine.h" #include "IoSeq.h" #include "IoNumber.h" diff --git a/libs/iovm/source/IoState_eval.c b/libs/iovm/source/IoState_eval.c index a50e21fea..742bf3d65 100644 --- a/libs/iovm/source/IoState_eval.c +++ b/libs/iovm/source/IoState_eval.c @@ -5,17 +5,24 @@ #include "IoState.h" #include "IoObject.h" #include +#include #include "PortableGettimeofday.h" +// Define DEBUG_CORO_EVAL to enable verbose debug output +// #define DEBUG_CORO_EVAL 1 + IoObject *IoState_tryToPerform(IoState *self, IoObject *target, IoObject *locals, IoMessage *m) { IoCoroutine *tryCoro = IoCoroutine_newWithTry(self, target, locals, m); - if (IoCoroutine_rawException(tryCoro) != self->ioNil) { + IoObject *exc = IoCoroutine_rawException(tryCoro); + + if (exc != self->ioNil) { IoState_exception_(self, tryCoro); } - return IoCoroutine_rawResult(tryCoro); + IoObject *result = IoCoroutine_rawResult(tryCoro); + return result; } void IoState_zeroSandboxCounts(IoState *self) { @@ -39,6 +46,18 @@ void IoState_resetSandboxCounts(IoState *self) { IoObject *IoState_on_doCString_withLabel_(IoState *self, IoObject *target, const char *s, const char *label) { +#ifdef DEBUG_CORO_EVAL + static int callDepth = 0; + Stack *ioStackBefore; + Stack *ioStackAfter; + callDepth++; + fprintf(stderr, ">>> on_doCString ENTER (depth=%d, label=%s)\n", callDepth, label ? label : "NULL"); + fflush(stderr); + ioStackBefore = self->currentIoStack; + fprintf(stderr, "on_doCString: ioStack before push = %p\n", (void*)ioStackBefore); + fflush(stderr); +#endif + IoObject *result; IoState_pushRetainPool(self); @@ -56,8 +75,26 @@ IoObject *IoState_on_doCString_withLabel_(IoState *self, IoObject *target, result = IoState_tryToPerform(self, target, target, m); } +#ifdef DEBUG_CORO_EVAL + ioStackAfter = self->currentIoStack; + fprintf(stderr, "on_doCString: ioStack after tryToPerform = %p (was %p)\n", + (void*)ioStackAfter, (void*)ioStackBefore); + fflush(stderr); + + if (ioStackBefore != ioStackAfter) { + fprintf(stderr, "WARNING: ioStack changed during tryToPerform!\n"); + fflush(stderr); + } +#endif + IoState_popRetainPoolExceptFor_(self, result); +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "<<< on_doCString EXIT (depth=%d, result=%p)\n", callDepth, (void*)result); + callDepth--; + fflush(stderr); +#endif + return result; } diff --git a/libs/iovm/source/IoState_eval.h b/libs/iovm/source/IoState_eval.h index e497e0a63..88930cb66 100644 --- a/libs/iovm/source/IoState_eval.h +++ b/libs/iovm/source/IoState_eval.h @@ -19,6 +19,18 @@ IOVM_API IoObject *IoState_on_doCString_withLabel_(IoState *self, const char *s, const char *label); +// iterative (non-recursive) evaluation + +IOVM_API IoObject *IoMessage_locals_performOn_iterative(IoMessage *self, + IoObject *locals, + IoObject *target); +IOVM_API IoObject *IoMessage_locals_performOn_fast(IoMessage *self, + IoObject *locals, + IoObject *target); +IOVM_API IoObject *IoState_evalLoop_(IoState *self); +IOVM_API IoObject *IoState_pushFrame_(IoState *self); +IOVM_API void IoState_popFrame_(IoState *self); + // sandbox IOVM_API void IoState_zeroSandboxCounts(IoState *self); diff --git a/libs/iovm/source/IoState_exceptions.c b/libs/iovm/source/IoState_exceptions.c index c9924097b..a56c23cc1 100644 --- a/libs/iovm/source/IoState_exceptions.c +++ b/libs/iovm/source/IoState_exceptions.c @@ -6,9 +6,14 @@ #include "IoObject.h" #include "IoCoroutine.h" #include "IoSeq.h" +#include "IoEvalFrame.h" +#include //#define IOSTATE_SHOW_ERRORS 1 +// Define DEBUG_CORO_EVAL to enable verbose debug output +// #define DEBUG_CORO_EVAL 1 + void IoState_fatalError_(IoState *self, char *error) { fputs(error, stderr); fputs("\n", stderr); @@ -30,12 +35,68 @@ void IoState_error_(IoState *self, IoMessage *m, const char *format, ...) { fputs("\n\n", stderr); #endif +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoState_error_: entering, msg=%s\n", CSTRING(description)); + // Print frame stack trace + fprintf(stderr, " Frame stack trace:\n"); + IoEvalFrame *frame = self->currentFrame; + IoEvalFrameData *fd = FRAME_DATA(frame); + int frameNum = 0; + while (frame && frameNum < 30) { + fd = FRAME_DATA(frame); + if (fd->message) { + IoSymbol *label = IoMessage_rawLabel(fd->message); + int line = IoMessage_rawLineNumber(fd->message); + IoSymbol *name = IoMessage_name(fd->message); + const char *targetType = fd->target ? IoObject_name(fd->target) : "(null)"; + fprintf(stderr, " [%d] %s at %s:%d (state=%d, target=%s)\n", + frameNum, + name ? CSTRING(name) : "(null)", + label ? CSTRING(label) : "(null)", + line, + fd->state, + targetType); + } else { + fprintf(stderr, " [%d] (null message)\n", frameNum); + } + frame = fd->parent; + frameNum++; + } + fflush(stderr); +#endif + while (Collector_isPaused(self->collector)) { Collector_popPause(self->collector); } + // Create exception on current coroutine (lightweight — no unwinding). + // The eval loop handles frame unwinding when it sees errorRaised. + // Note: Can't use IOSYMBOL() here — it expands IOSTATE via IoObject_tag(self) + // but self is IoState*, not IoObject*. { IoCoroutine *coroutine = IoState_currentCoroutine(self); - IoCoroutine_raiseError(coroutine, description, m); + IoObject *e = IoObject_rawGetSlot_(coroutine, + IoState_symbolWithCString_(self, "Exception")); + if (e) { + e = IOCLONE(e); + IoObject_setSlot_to_(e, + IoState_symbolWithCString_(self, "error"), description); + if (m) IoObject_setSlot_to_(e, + IoState_symbolWithCString_(self, "caughtMessage"), m); + IoObject_setSlot_to_(e, + IoState_symbolWithCString_(self, "coroutine"), coroutine); + IoCoroutine_rawSetException_(coroutine, e); + } } + + self->errorRaised = 1; + +#ifdef DEBUG_CORO_EVAL + fprintf(stderr, "IoState_error_: errorRaised=1\n"); + fflush(stderr); +#endif + // IoState_error_ returns normally. Callers must check + // state->errorRaised and return early to avoid continuing + // with invalid state. The eval loop checks errorRaised after + // each CFunction call and unwinds frames. } diff --git a/libs/iovm/source/IoState_inline.h b/libs/iovm/source/IoState_inline.h index 280db7ed3..b10bd7ba9 100644 --- a/libs/iovm/source/IoState_inline.h +++ b/libs/iovm/source/IoState_inline.h @@ -16,6 +16,7 @@ #define IOASSERT(value, message) \ if (!(value)) { \ IoState_error_(IOSTATE, m, "Io Assertion '%s'", message); \ + return IONIL(self); \ } #define IOCOLLECTOR (IOSTATE->collector) @@ -144,8 +145,27 @@ IOINLINE void IoState_popRetainPoolExceptFor_(void *state, void *obj) { #define IOMESSAGEDATA(self) ((IoMessageData *)IoObject_dataPointer(self)) +/* + * Argument evaluation checks for pre-evaluated args first (from the + * iterative eval loop's arg pre-evaluation in FRAME_STATE_ACTIVATE). + * If pre-evaluated args are available, they're returned directly without + * any C stack recursion. Falls back to the recursive evaluator only + * when pre-evaluated args aren't available (bootstrap, legacy paths). + */ + IOINLINE IoObject *IoMessage_locals_quickValueArgAt_(IoMessage *self, IoObject *locals, int n) { + IoState *state = IOSTATE; + + // Check for pre-evaluated arguments from the iterative eval loop. + // The eval loop pre-evaluates args before calling CFunctions/blocks, + // so this avoids all C stack re-entrancy for argument evaluation. + if (state->currentFrame) { + IoObject *preEvaled = IoState_preEvalArgAt_(state, self, n); + if (preEvaled) return preEvaled; + } + + // Fall back to direct evaluation (cached literals or recursive eval) IoMessage *m = (IoMessage *)List_at_(IOMESSAGEDATA(self)->args, n); if (m) { @@ -156,26 +176,16 @@ IOINLINE IoObject *IoMessage_locals_quickValueArgAt_(IoMessage *self, return v; } + // Use recursive evaluator for argument evaluation return IoMessage_locals_performOn_(m, locals, locals); } - return IOSTATE->ioNil; + return state->ioNil; } IOINLINE IoObject *IoMessage_locals_valueArgAt_(IoMessage *self, IoObject *locals, int n) { return IoMessage_locals_quickValueArgAt_(self, locals, n); - /* - List *args = IOMESSAGEDATA(self)->args; - IoMessage *m = (IoMessage *)List_at_(args, n); - - if (m) - { - return IoMessage_locals_performOn_(m, locals, locals); - } - - return IOSTATE->ioNil; - */ } IOINLINE IoObject *IoMessage_locals_firstStringArg(IoMessage *self, diff --git a/libs/iovm/source/IoState_iterative.c b/libs/iovm/source/IoState_iterative.c new file mode 100644 index 000000000..4f6cd1a36 --- /dev/null +++ b/libs/iovm/source/IoState_iterative.c @@ -0,0 +1,2103 @@ + +// metadoc State copyright Steve Dekorte 2002, 2025 +// metadoc State license BSD revised +// metadoc State category Core +/*metadoc State description +Iterative (non-recursive) message evaluation for Io. +This provides a C-stack-independent evaluator that enables +first-class continuations, serializable execution state, +and network-portable coroutines. +*/ + +#include "IoState.h" +#include "IoEvalFrame.h" +#include "IoMessage.h" +#include "IoObject.h" +#include "IoBlock.h" +#include "IoCall.h" +#include "IoCFunction.h" +#include "IoNumber.h" +#include "IoCoroutine.h" +#include "IoMap.h" +#include "IoList.h" +#include +#include + +// Forward declarations +static void IoState_activateBlock_(IoState *state, IoEvalFrame *callerFrame); +static void IoState_activateBlockTCO_(IoState *state, IoEvalFrame *blockFrame); + + +// Debug: Validate that a frame's message field is a Message. +// This should be called after every frame setup where message is assigned. +#define VALIDATE_FRAME(f, location) do { \ + IoEvalFrameData *_vfd = FRAME_DATA(f); \ + if (_vfd->message && !ISMESSAGE(_vfd->message)) { \ + fprintf(stderr, "FRAME VALIDATION FAILED at %s\n", location); \ + fprintf(stderr, " frame=%p, message=%p (tag=%s), target=%p, locals=%p\n", \ + (void*)(f), (void*)_vfd->message, \ + IoObject_tag((IoObject*)_vfd->message) ? IoObject_tag((IoObject*)_vfd->message)->name : "NULL", \ + (void*)_vfd->target, (void*)_vfd->locals); \ + fprintf(stderr, " parent=%p, state=%d\n", (void*)_vfd->parent, _vfd->state); \ + fflush(stderr); \ + abort(); \ + } \ +} while(0) + +// Check for pre-evaluated argument in the current eval frame. +// Called from IoMessage_locals_quickValueArgAt_ (IoState_inline.h). +// Separated into a function to avoid circular IoEvalFrame.h includes. +IoObject *IoState_preEvalArgAt_(IoState *self, IoMessage *msg, int n) { + IoEvalFrame *frame = self->currentFrame; + if (!frame) return NULL; + IoEvalFrameData *fd = FRAME_DATA(frame); + if ( + fd->argValues && + msg == fd->message && + n < fd->argCount && + fd->argValues[n] != NULL) { + return fd->argValues[n]; + } + return NULL; +} + +// Push a new frame onto the evaluation stack. +// Reuses pooled frames when available; allocates new IoObject otherwise. +IoEvalFrame *IoState_pushFrame_(IoState *state) { + IoEvalFrame *frame; + IoEvalFrameData *fd; + + if (state->framePoolCount > 0) { + // Reuse a pooled frame (already a valid collector object) + // Pointer fields were cleared on pool return (GC safety). + // Only initialize the fields we actually use. + frame = state->framePool[--state->framePoolCount]; + fd = FRAME_DATA(frame); + } else { + frame = IoEvalFrame_newWithState(state); + fd = FRAME_DATA(frame); + } + + // Selective initialization: set only the fields that matter. + // The controlFlow union is left uninitialized — it's set by + // control flow primitives before use. + fd->message = NULL; + fd->target = NULL; + fd->locals = NULL; + fd->cachedTarget = NULL; + fd->parent = NULL; + fd->state = FRAME_STATE_START; + fd->argCount = 0; + fd->currentArgIndex = 0; + fd->argValues = NULL; + // inlineArgs left uninitialized — set when used + fd->result = NULL; + fd->slotValue = NULL; + fd->slotContext = NULL; + fd->call = NULL; + fd->savedCall = NULL; + fd->blockLocals = NULL; + fd->passStops = 0; + fd->isNestedEvalRoot = 0; + fd->retainPoolMark = 0; + + fd->parent = state->currentFrame; + state->currentFrame = frame; + state->frameDepth++; + + if (state->frameDepth > state->maxFrameDepth) { + IoState_error_(state, NULL, "Stack overflow: frame depth exceeded %d", + state->maxFrameDepth); + } + + return frame; +} + +// Pop a frame from the evaluation stack. +// Returns the frame to the pool if space; otherwise lets GC reclaim it. +void IoState_popFrame_(IoState *state) { + IoEvalFrame *frame = state->currentFrame; + if (frame) { + IoEvalFrameData *fd = FRAME_DATA(frame); + state->currentFrame = fd->parent; + state->frameDepth--; + + // Free heap-allocated argValues (not inline buffer) + if (fd->argValues && fd->argValues != fd->inlineArgs) { + io_free(fd->argValues); + } + fd->argValues = NULL; + + // Clear only pointer fields so pooled frames don't hold stale + // GC references. Without this, GC marking of pooled frames keeps + // dead objects alive (e.g. WeakLink targets that should be collected). + fd->message = NULL; + fd->target = NULL; + fd->locals = NULL; + fd->cachedTarget = NULL; + fd->parent = NULL; + fd->inlineArgs[0] = NULL; + fd->inlineArgs[1] = NULL; + fd->inlineArgs[2] = NULL; + fd->inlineArgs[3] = NULL; + fd->result = NULL; + fd->slotValue = NULL; + fd->slotContext = NULL; + fd->call = NULL; + fd->savedCall = NULL; + fd->blockLocals = NULL; + // Reset state so GC mark function won't walk stale controlFlow pointers. + // This is cheaper than memset of the ~80-byte controlFlow union. + fd->state = FRAME_STATE_START; + + // Return to pool if space available + if (state->framePoolCount < FRAME_POOL_SIZE) { + state->framePool[state->framePoolCount++] = frame; + } + // else: frame stays in collector, GC reclaims when unreferenced + } +} + +// Unwind frames for error handling, respecting nested eval boundaries. +// +// When IoMessage_locals_performOn_iterative creates a nested eval loop, +// it marks its root frame with isNestedEvalRoot. If an error occurs +// inside the nested eval, we must NOT pop frames beyond the boundary — +// doing so would corrupt the outer eval loop's frame pointers, especially +// when coroutine switches are involved. +// +// Returns 1 if a nested eval boundary was found (caller should return +// from the eval loop with errorRaised re-set). +// Returns 0 if all frames were popped (caller should fall through to +// the frame=NULL handler for coro switching / exit). +static int IoState_unwindFramesForError_(IoState *state) { + while (state->currentFrame) { + IoEvalFrameData *ufd = FRAME_DATA(state->currentFrame); + int isRoot = ufd->isNestedEvalRoot; + if (ufd->retainPoolMark) { + IoState_popRetainPool(state); + } + IoState_popFrame_(state); + if (isRoot) { + // Hit a nested eval boundary. Re-set errorRaised so the + // C caller (IoMessage_locals_performOn_iterative) propagates + // the error to the outer eval loop. + state->errorRaised = 1; + return 1; + } + } + return 0; // All frames popped +} + +// Main iterative evaluation loop +// +// COROUTINE ARCHITECTURE: +// This is the ONE eval loop that runs on the main C stack. It processes +// whatever frames are in state->currentFrame. Coroutine "switching" just +// changes which frame stack is current - the loop keeps running. +// +// When a coroutine's frame stack becomes empty: +// - If it has a parent, switch back to parent and continue +// - If no parent (main coro), exit the loop +// +// Define DEBUG_EVAL_LOOP to enable verbose debug output +// #define DEBUG_EVAL_LOOP 1 + +IoObject *IoState_evalLoop_(IoState *state) { + IoEvalFrame *frame; + IoEvalFrameData *fd; + IoObject *result = state->ioNil; + +#ifdef DEBUG_EVAL_LOOP + static int loopIter = 0; +#endif + + while (1) { + frame = state->currentFrame; + fd = frame ? FRAME_DATA(frame) : NULL; + + // Check if System exit was called + if (state->shouldExit) { + while (state->currentFrame) IoState_popFrame_(state); + return result; + } + +#ifdef DEBUG_EVAL_LOOP + loopIter++; + // Always print to trace coro issues + fprintf(stderr, "evalLoop iter %d: frame=%p, coro=%p, msg=%s, state=%d, nestedDepth=%d\n", + loopIter, (void*)frame, (void*)state->currentCoroutine, + (frame && fd->message) ? CSTRING(IoMessage_name(fd->message)) : "NULL", + frame ? fd->state : -1, + state->nestedEvalDepth); + fflush(stderr); +#endif + + // Check if current coroutine's frame stack is empty + if (!frame) { +#ifdef DEBUG_EVAL_LOOP + fprintf(stderr, "evalLoop: frame=NULL, coro=%p, nestedDepth=%d, result=%p\n", + (void*)state->currentCoroutine, state->nestedEvalDepth, (void*)result); + fflush(stderr); +#endif + + IoCoroutine *current = state->currentCoroutine; + IoCoroutine *parent = IoCoroutine_rawParentCoroutine(current); + + // Check if this is a child coro started via coro swap that has + // finished. The parent's saved frameStack will have a + // CORO_WAIT_CHILD or CORO_YIELDED frame. We must check + // this BEFORE the nestedEvalDepth check, because a coro swap + // child can finish within a nested eval loop. + if (parent && ISCOROUTINE(parent)) { + IoEvalFrame *parentTopFrame = ((IoCoroutineData *)IoObject_dataPointer(parent))->frameStack; + if (parentTopFrame) { + IoEvalFrameData *parentTopFd = FRAME_DATA(parentTopFrame); + if (parentTopFd->state == FRAME_STATE_CORO_WAIT_CHILD || + parentTopFd->state == FRAME_STATE_CORO_YIELDED) { +#ifdef DEBUG_EVAL_LOOP + fprintf(stderr, "evalLoop: coro finished, returning to parent coro (parent state=%d)\n", + parentTopFd->state); + fflush(stderr); +#endif + // Child coro finished - restore parent + IoCoroutine_rawSetResult_(current, result); + IoCoroutine_saveState_(current, state); + + IoCoroutine_restoreState_(parent, state); + IoState_setCurrentCoroutine_(state, parent); + + // Parent's top frame: transition to CONTINUE_CHAIN + frame = state->currentFrame; + fd = FRAME_DATA(frame); + if (frame && (fd->state == FRAME_STATE_CORO_WAIT_CHILD || + fd->state == FRAME_STATE_CORO_YIELDED)) { + fd->result = result; + fd->state = FRAME_STATE_CONTINUE_CHAIN; + } + continue; // Continue with parent's frames + } + } + } + + // If we're in a nested eval (e.g., from IoCoroutine_try), return + // immediately. The C caller (IoCoroutine_try or rawRun) handles + // cleanup and coroutine state restoration. + if (state->nestedEvalDepth > 0) { + return result; + } + + // Not nested, not a coro swap child — check for regular parent + if (parent && ISCOROUTINE(parent)) { +#ifdef DEBUG_EVAL_LOOP + fprintf(stderr, "evalLoop: returning to parent coro\n"); + fflush(stderr); +#endif + + // Child coroutine finished - return to parent + IoCoroutine_rawSetResult_(current, result); + + // Restore parent's frame stack + IoCoroutine_restoreState_(parent, state); + IoState_setCurrentCoroutine_(state, parent); + + // Parent's top frame should be waiting for us + frame = state->currentFrame; + fd = frame ? FRAME_DATA(frame) : NULL; +#ifdef DEBUG_EVAL_LOOP + fprintf(stderr, "evalLoop: parent frame=%p, state=%d\n", + (void*)frame, fd ? fd->state : -1); + fflush(stderr); +#endif + + if (frame && fd && fd->state == FRAME_STATE_CORO_WAIT_CHILD) { + fd->result = result; + fd->state = FRAME_STATE_CONTINUE_CHAIN; + } + continue; // Continue with parent's frames + } + +#ifdef DEBUG_EVAL_LOOP + fprintf(stderr, "evalLoop: exiting (no parent)\n"); + fflush(stderr); +#endif + // Main coroutine done - check for uncaught exception + { + IoObject *exc = IoCoroutine_rawException(current); + if (exc != state->ioNil) { + IoCoroutine_rawPrintBackTrace(current); + } + } + return result; + } + + IoMessage *m; + IoMessageData *md; + +#ifdef DEBUG_FRAME_VALIDATION + // Validate frame integrity (enabled by -DDEBUG_FRAME_VALIDATION) + if (!ISEVALFRAME(frame)) { + fprintf(stderr, "CORRUPTION: currentFrame is not an EvalFrame! frame=%p, tag=%s\n", + (void*)frame, + IoObject_tag(frame) ? IoObject_tag(frame)->name : "NULL"); + fflush(stderr); + abort(); + } + + if (fd->message && !ISMESSAGE(fd->message)) { + fprintf(stderr, "CORRUPTION: fd->message is not a Message!\n"); + fprintf(stderr, " frame=%p, state=%d, message=%p, tag=%s\n", + (void*)frame, fd->state, + (void*)fd->message, + IoObject_tag(fd->message) ? IoObject_tag(fd->message)->name : "NULL"); + fprintf(stderr, " target=%p, locals=%p, result=%p, cachedTarget=%p\n", + (void*)fd->target, (void*)fd->locals, + (void*)fd->result, (void*)fd->cachedTarget); + fprintf(stderr, " slotValue=%p, slotContext=%p, blockLocals=%p\n", + (void*)fd->slotValue, (void*)fd->slotContext, + (void*)fd->blockLocals); + fprintf(stderr, " argValues=%p, argCount=%d, currentArgIndex=%d\n", + (void*)fd->argValues, fd->argCount, fd->currentArgIndex); + fprintf(stderr, " parent=%p\n", (void*)fd->parent); + // Walk parent chain + IoEvalFrame *p = fd->parent; + int depth = 0; + while (p && depth < 10) { + IoEvalFrameData *pd = FRAME_DATA(p); + fprintf(stderr, " parent[%d]: frame=%p, state=%d, msg=%p (%s)\n", + depth, (void*)p, pd->state, (void*)pd->message, + (pd->message && ISMESSAGE(pd->message)) + ? CSTRING(IoMessage_name(pd->message)) : "INVALID/UNKNOWN"); + fprintf(stderr, " target=%p, locals=%p, blockLocals=%p\n", + (void*)pd->target, (void*)pd->locals, (void*)pd->blockLocals); + p = pd->parent; + depth++; + } + fflush(stderr); + abort(); + } +#endif + + // Check for signals (Ctrl-C, etc.) + if (state->receivedSignal) { + IoState_callUserInterruptHandler(state); + } + + // Stop status (return/break/continue) propagates naturally through + // CONTINUE_CHAIN → RETURN at each frame level. Method frames + // (blockLocals && !passStops) catch all stop statuses in + // CONTINUE_CHAIN. This matches the recursive evaluator where + // performOn_ checks stopStatus after each message. + // + // NOTE: We intentionally do NOT eagerly intercept 'return' here. + // Eager interception would bypass frames that need to observe + // state->stopStatus (e.g., IoObject_stopStatus used by + // relayStopStatus). Instead, return propagates frame-by-frame + // like break/continue. + + // Generic errorRaised check: catches errors raised outside of ACTIVATE + // (e.g., from forward handlers in LOOKUP_SLOT, or other CFunction calls). + // The ACTIVATE case has its own errorRaised handler that also pops the + // retain pool — errors raised during ACTIVATE are caught there first. + if (state->errorRaised) { +#ifdef DEBUG_EVAL_LOOP + fprintf(stderr, "evalLoop: top-level errorRaised, unwinding frames\n"); + fflush(stderr); +#endif + state->errorRaised = 0; + if (IoState_unwindFramesForError_(state)) { + return state->ioNil; // Hit nested eval boundary + } + continue; // frame=NULL handler takes over + } + + // Show message if debugging + if (state->showAllMessages && fd->message) { + printf("M:%s:%s:%i\n", CSTRING(IoMessage_name(fd->message)), + CSTRING(IoMessage_rawLabel(fd->message)), + IoMessage_rawLineNumber(fd->message)); + } + + switch (fd->state) { + + start_message: + case FRAME_STATE_START: { + // Starting evaluation of a message + m = fd->message; + if (!m) { + // No message to evaluate, return nil + fd->result = state->ioNil; + fd->state = FRAME_STATE_RETURN; + break; + } + +#ifdef DEBUG_EVAL_LOOP + { + // Print full message chain for debugging + IoMessage *dbgMsg = m; + fprintf(stderr, " START chain: "); + while (dbgMsg) { + IoSymbol *dbgLabel = IoMessage_rawLabel(dbgMsg); + fprintf(stderr, "%s@%s:%d -> ", + CSTRING(IoMessage_name(dbgMsg)), + dbgLabel ? CSTRING(dbgLabel) : "(null)", + IoMessage_rawLineNumber(dbgMsg)); + dbgMsg = IOMESSAGEDATA(dbgMsg)->next; + if (!dbgMsg) fprintf(stderr, "(end)"); + } + fprintf(stderr, "\n"); + fflush(stderr); + } +#endif + + md = IOMESSAGEDATA(m); + + if (!md) { + fprintf(stderr, "FATAL: NULL message data for message %p, frame=%p, state=%d\n", + (void*)m, (void*)frame, fd->state); + fprintf(stderr, " fd->target=%p, fd->locals=%p\n", + (void*)fd->target, (void*)fd->locals); + fprintf(stderr, " fd->argValues=%p, fd->argCount=%d, fd->currentArgIndex=%d\n", + (void*)fd->argValues, fd->argCount, fd->currentArgIndex); + fprintf(stderr, " IoObject_tag(m)->name=%s\n", + IoObject_tag(m) ? IoObject_tag(m)->name : "NULL"); + IoEvalFrame *p = fd->parent; + int depth = 0; + while (p && depth < 10) { + IoEvalFrameData *pd = FRAME_DATA(p); + fprintf(stderr, " parent[%d]: state=%d, msg=%p", depth, + pd->state, (void*)pd->message); + if (pd->message) { + IoMessageData *pmd = IOMESSAGEDATA(pd->message); + if (pmd) { + fprintf(stderr, " name=%s", CSTRING(IoMessage_name(pd->message))); + } else { + fprintf(stderr, " (no msg data, tag=%s)", + IoObject_tag(pd->message) ? IoObject_tag(pd->message)->name : "NULL"); + } + } + fprintf(stderr, " argValues=%p argCount=%d\n", + (void*)pd->argValues, pd->argCount); + p = pd->parent; + depth++; + } + fprintf(stderr, " (total parent depth: searched %d)\n", depth); + fflush(stderr); + fd->result = state->ioNil; + fd->state = FRAME_STATE_RETURN; + break; + } + + // Check if this is a semicolon (resets target) + if (md->name == state->semicolonSymbol) { + fd->target = fd->cachedTarget; + fd->message = md->next; + if (md->next) { + // Continue with next message + fd->state = FRAME_STATE_START; + } else { + // End of chain after semicolon + fd->result = fd->target; + fd->state = FRAME_STATE_RETURN; + } + break; + } + + // Check if message has a cached result (literal) + if (md->cachedResult) { + fd->result = md->cachedResult; + // If there's no next message, we can return immediately + if (!md->next) { + fd->state = FRAME_STATE_RETURN; + break; + } else { + fd->state = FRAME_STATE_CONTINUE_CHAIN; + goto continue_chain; // Fast path: skip loop restart + } + } + + // Skip to slot lookup — special form detection is done in + // ACTIVATE where it actually matters for arg pre-evaluation. + fd->state = FRAME_STATE_LOOKUP_SLOT; + goto lookup_slot; // Fast path: skip loop restart + } + + case FRAME_STATE_EVAL_ARGS: { + // Evaluate arguments one at a time iteratively (no C recursion). + // Entered from FRAME_STATE_ACTIVATE when args need pre-evaluation. + IoMessage *argMsg; + + if (fd->currentArgIndex >= fd->argCount) { + // All arguments evaluated, return to ACTIVATE to call the function + fd->state = FRAME_STATE_ACTIVATE; + break; + } + + // Get the next argument message + argMsg = IoMessage_rawArgAt_(fd->message, fd->currentArgIndex); + + if (!argMsg) { + // Shouldn't happen, but handle gracefully + fd->argValues[fd->currentArgIndex] = state->ioNil; + fd->currentArgIndex++; + break; + } + + // Check if argument has a cached result + if (IOMESSAGEDATA(argMsg)->cachedResult && + !IOMESSAGEDATA(argMsg)->next) { + // Use cached value, no need to push frame + fd->argValues[fd->currentArgIndex] = + IOMESSAGEDATA(argMsg)->cachedResult; + fd->currentArgIndex++; + break; + } + + // Need to evaluate this argument - push a new frame + IoEvalFrame *argFrame = IoState_pushFrame_(state); + IoEvalFrameData *argFd = FRAME_DATA(argFrame); + argFd->message = argMsg; + argFd->target = fd->locals; // Args eval in sender context + argFd->locals = fd->locals; + argFd->cachedTarget = fd->locals; + argFd->state = FRAME_STATE_START; + + // When argFrame returns, we'll resume at EVAL_ARGS state + // and currentArgIndex will be incremented + break; + } + + lookup_slot: + case FRAME_STATE_LOOKUP_SLOT: { + // Perform slot lookup on target + IoMessageData *lookupMd = IOMESSAGEDATA(fd->message); + IoSymbol *messageName = lookupMd->name; + IoObject *slotValue; + IoObject *slotContext; + + // Inline cache: if target has the same tag and global slot + // version hasn't changed since we cached, reuse the result. + // Only caches proto-chain hits (not direct slot hits on target). + // Guard: a local slot on target shadows the cached proto value + // (e.g. false.isTrue overrides Object.isTrue). + if (lookupMd->inlineCacheTag == IoObject_tag(fd->target) && + lookupMd->inlineCacheVersion == state->slotVersion && + !(IoObject_ownsSlots(fd->target) && + PHash_at_(IoObject_slots(fd->target), messageName))) { + slotValue = lookupMd->inlineCacheValue; + slotContext = lookupMd->inlineCacheContext; + } else { + slotValue = IoObject_rawGetSlot_context_(fd->target, messageName, + &slotContext); + // Cache only proto-chain hits (method lookups). + // Direct hits (local vars) change frequently and aren't worth caching. + if (slotValue && slotContext != fd->target) { + lookupMd->inlineCacheTag = IoObject_tag(fd->target); + lookupMd->inlineCacheValue = slotValue; + lookupMd->inlineCacheContext = slotContext; + lookupMd->inlineCacheVersion = state->slotVersion; + } + } + + if (slotValue) { + fd->slotValue = slotValue; + fd->slotContext = slotContext; + fd->state = FRAME_STATE_ACTIVATE; + goto activate; // Fast path: skip loop restart + } else if (IoObject_isLocals(fd->target)) { + // Slot not found on block locals — look up 'self' (the scope) + // and re-do the lookup there. This is the iterative equivalent + // of IoObject_localsForward which would call performOn_ + // recursively. By retargeting here, we avoid C stack growth. + IoObject *scope = IoObject_rawGetSlot_(fd->target, + state->selfSymbol); + if (scope) { + fd->target = scope; + // Retry lookup on the scope (stays in LOOKUP_SLOT) + } else { + // No scope — use regular forward + fd->result = IoObject_forward(fd->target, fd->locals, + fd->message); + if (state->errorRaised) { + state->errorRaised = 0; + if (IoState_unwindFramesForError_(state)) { + return state->ioNil; + } + break; + } + fd->state = FRAME_STATE_CONTINUE_CHAIN; + } + } else { + // Slot not found on non-locals target + fd->result = IoObject_forward(fd->target, fd->locals, + fd->message); + if (state->errorRaised) { + state->errorRaised = 0; + if (IoState_unwindFramesForError_(state)) { + return state->ioNil; + } + break; + } + fd->state = FRAME_STATE_CONTINUE_CHAIN; + } + break; + } + + activate: + case FRAME_STATE_ACTIVATE: { + // Activate the slot value (if activatable) + IoObject *slotValue = fd->slotValue; + + if (IoObject_isActivatable(slotValue)) { + // ============================================================ + // ARG PRE-EVALUATION + // Before calling any Block or CFunction, pre-evaluate all + // arguments iteratively. This eliminates C stack re-entrancy + // for argument evaluation (~95% of all re-entrant call sites). + // + // Skip pre-eval for CFunctions with isLazyArgs flag set. + // The flag is set at init time on all special-form CFunctions + // (if, while, for, foreach, method, block, etc.) and + // automatically covers aliases (e.g., elseif := getSlot("if")) + // since they share the same CFunction object. + // + // Also skip for CFunctions using IoObject_self (thisContext, + // ifNil, etc.) which ignore their arguments entirely. + // ============================================================ + // Check cached flag (0=unchecked, 1=normal, 2=special) + IoMessageData *activateMd = IOMESSAGEDATA(fd->message); + int isSpecialForm; + if (activateMd->isSpecialForm == 0) { + isSpecialForm = 0; + if (ISCFUNCTION(slotValue)) { + IoCFunctionData *cfData = + (IoCFunctionData *)IoObject_dataPointer(slotValue); + if (cfData->isLazyArgs || + cfData->func == (IoUserFunction *)IoObject_self) { + isSpecialForm = 1; + } + } + activateMd->isSpecialForm = isSpecialForm ? 2 : 1; + } else { + isSpecialForm = (activateMd->isSpecialForm == 2); + } + + if (!isSpecialForm && !fd->argValues) { + // Pre-evaluate arguments iteratively (no C stack re-entrancy). + // + // For CFunctions: pre-evaluate ALL args. + // For Blocks: pre-evaluate only the named formal parameters. + // Extra args beyond named params remain unevaluated for + // lazy access via call argAt() / call evalArgAt() + // (e.g., map(asUTF8) passes the message, not a value). + int msgArgCount = IoMessage_argCount(fd->message); + int preEvalCount = msgArgCount; + + if (ISBLOCK(slotValue) && msgArgCount > 0) { + IoBlockData *bd = (IoBlockData *)IoObject_dataPointer(slotValue); + int namedCount = (int)List_size(bd->argNames); + if (namedCount < preEvalCount) { + preEvalCount = namedCount; + } + } + + if (preEvalCount > 0) { + fd->argCount = preEvalCount; + fd->currentArgIndex = 0; + // Use inline buffer for small arg counts (avoids heap alloc) + if (preEvalCount <= FRAME_INLINE_ARG_MAX) { + fd->argValues = fd->inlineArgs; + memset(fd->inlineArgs, 0, preEvalCount * sizeof(IoObject *)); + } else { + fd->argValues = (IoObject **)io_calloc(preEvalCount, sizeof(IoObject *)); + } + + // Fast path: check if ALL args are simple cached literals + int allCached = 1; + int i; + for (i = 0; i < msgArgCount; i++) { + IoMessage *argMsg = IoMessage_rawArgAt_(fd->message, i); + if (argMsg) { + IoMessageData *argMd = IOMESSAGEDATA(argMsg); + if (argMd->cachedResult && !argMd->next) { + fd->argValues[i] = argMd->cachedResult; + } else { + allCached = 0; + break; + } + } else { + fd->argValues[i] = state->ioNil; + } + } + + if (allCached) { + // All args were cached literals - done + fd->currentArgIndex = msgArgCount; + } else { + // Slow path: evaluate args iteratively via EVAL_ARGS + // Reset to evaluate from the first non-cached arg + fd->currentArgIndex = 0; + // Clear fast-path partial results + memset(fd->argValues, 0, msgArgCount * sizeof(IoObject *)); + fd->state = FRAME_STATE_EVAL_ARGS; + break; + } + } + } + + // ============================================================ + // ACTIVATION (args are pre-evaluated or not needed) + // ============================================================ + if (ISBLOCK(slotValue)) { + // Fast path: trivial method body (single cached literal). + // Skip entire block activation (locals clone, Call object, + // PHash creation, frame push/pop) and return directly. + { + IoBlockData *blockData = + (IoBlockData *)IoObject_dataPointer(slotValue); + IoMessage *bodyMsg = blockData->message; + if (bodyMsg && BODY_IS_CACHED_LITERAL(bodyMsg)) { + fd->result = CACHED_LITERAL_RESULT(bodyMsg); + fd->state = FRAME_STATE_CONTINUE_CHAIN; + goto continue_chain; + } + } + + // Tail Call Optimization: if this is the last message + // in a block body frame, reuse the frame instead of + // pushing a new one. This prevents stack overflow for + // recursive methods like factorial. + if (fd->blockLocals && + !IOMESSAGEDATA(fd->message)->next) { + IoState_activateBlockTCO_(state, frame); + } else { + IoState_activateBlock_(state, frame); + } + } else { + // CFunction - call directly + IoTagActivateFunc *activateFunc = + IoObject_tag(slotValue)->activateFunc; + + // Save ioStack pointer before CFunction call. + // Coroutine operations (rawRun, rawReturnToParent, + // rawResume) switch state->currentIoStack to a + // different coroutine's stack. We must NOT pop the + // retain pool from the wrong stack - that causes + // buffer underflow and ioStack corruption. + Stack *savedIoStack = state->currentIoStack; + + IoState_pushRetainPool(state); + fd->result = + activateFunc(slotValue, fd->target, fd->locals, + fd->message, fd->slotContext); + + +#ifdef DEBUG_EVAL_LOOP + fprintf(stderr, "evalLoop ACTIVATE: CFunction %s returned, result=%p, frame=%p\n", + CSTRING(IoMessage_name(fd->message)), + (void*)fd->result, (void*)frame); + fflush(stderr); +#endif + + // Check if an error was raised during CFunction execution. + // IoState_error_ creates the Exception and sets errorRaised. + // If the error was raised outside the recursive evaluator, + // we got here via longjmp. If inside, we got here via the + // recursive evaluator's error check returning ioNil. + if (state->errorRaised) { +#ifdef DEBUG_EVAL_LOOP + fprintf(stderr, "evalLoop ACTIVATE: errorRaised, unwinding frames. frame=%p, currentFrame=%p, coro=%p\n", + (void*)frame, (void*)state->currentFrame, (void*)state->currentCoroutine); + fflush(stderr); +#endif + state->errorRaised = 0; + + // Pop retain pool if ioStack hasn't been switched + if (state->currentIoStack == savedIoStack) { + IoState_popRetainPoolExceptFor_(state, state->ioNil); + } + + // Unwind frames, respecting nested eval boundaries. + // If we hit an isNestedEvalRoot, return from this eval + // loop with errorRaised re-set. The C caller will + // propagate the error to the outer eval loop. + if (IoState_unwindFramesForError_(state)) { + return state->ioNil; + } + break; // loop restarts, frame=NULL handler takes over + } + +#ifdef IO_CALLCC + // Check if a continuation was invoked (frame stack replaced) + if (state->continuationInvoked) { +#ifdef DEBUG_EVAL_LOOP + fprintf(stderr, "evalLoop ACTIVATE: continuationInvoked, breaking\n"); + fflush(stderr); +#endif + state->continuationInvoked = 0; + break; + } +#endif + + // Only pop the retain pool if the ioStack hasn't been + // switched by a coroutine operation. If it HAS been + // switched, the retain pool mark is on the OLD stack + // (saved coroutine) and popping from the NEW stack + // would corrupt it (Stack_popMark underflow). + if (state->currentIoStack == savedIoStack) { + IoState_popRetainPoolExceptFor_(state, fd->result); + } + + // Check if primitive set up control flow handling + if (state->needsControlFlowHandling) { +#ifdef DEBUG_EVAL_LOOP + fprintf(stderr, "evalLoop ACTIVATE: needsControlFlowHandling, breaking\n"); + fprintf(stderr, " old frame=%p, state->currentFrame=%p, coro=%p\n", + (void*)frame, (void*)state->currentFrame, + (void*)state->currentCoroutine); + fflush(stderr); +#endif + state->needsControlFlowHandling = 0; + break; + } + +#ifdef DEBUG_EVAL_LOOP + fprintf(stderr, "evalLoop ACTIVATE: normal return\n"); + fflush(stderr); +#endif + + // Normal return, continue chain + fd->state = FRAME_STATE_CONTINUE_CHAIN; + goto continue_chain; // Fast path: skip loop restart + } + } else { + // Not activatable - just return the value + fd->result = slotValue; + fd->state = FRAME_STATE_CONTINUE_CHAIN; + goto continue_chain; // Fast path: skip loop restart + } +#ifdef DEBUG_EVAL_LOOP + fprintf(stderr, "evalLoop ACTIVATE: case done, frame=%p, state=%d\n", + (void*)frame, fd->state); + fflush(stderr); +#endif + break; + } + + continue_chain: + case FRAME_STATE_CONTINUE_CHAIN: { + // Check for non-normal stop status (break, continue, return). + // Stop status can be set by CFunctions like break/continue/return + // that were called in a child frame (e.g., break inside if inside loop). + if (state->stopStatus != MESSAGE_STOP_STATUS_NORMAL) { + // If this frame is a block activation with passStops=false, + // it catches the stop status here (return is contained within the block). + if (fd->blockLocals && !fd->passStops) { + fd->result = state->returnValue; + state->stopStatus = MESSAGE_STOP_STATUS_NORMAL; + } else { + // For non-catching frames, set result to returnValue. + // This matches the recursive evaluator behavior where + // IoMessage_locals_performOn_ returns state->returnValue + // when stopStatus is non-normal. This ensures nested eval + // roots and intermediate frames propagate the correct value. + fd->result = state->returnValue; + } + // Propagate upward. + fd->state = FRAME_STATE_RETURN; + break; + } + + // Move to next message in chain + IoMessage *next = IOMESSAGEDATA(fd->message)->next; + +#ifdef DEBUG_EVAL_LOOP + fprintf(stderr, " CONTINUE_CHAIN: current=%s, next=%s\n", + CSTRING(IoMessage_name(fd->message)), + next ? CSTRING(IoMessage_name(next)) : "(null)"); + fflush(stderr); +#endif + + if (next) { + // Update target to be the result + fd->target = fd->result; + fd->message = next; + fd->state = FRAME_STATE_START; + + // Reset arg state + if (fd->argValues) { + if (fd->argValues != fd->inlineArgs) { + io_free(fd->argValues); + } + fd->argValues = NULL; + } + fd->argCount = 0; + fd->currentArgIndex = 0; + goto start_message; // Fast path: skip loop restart + } else { + // End of chain - return + fd->state = FRAME_STATE_RETURN; + } + break; + } + + case FRAME_STATE_RETURN: { + // Pop this frame and return result to parent + result = fd->result; + IoEvalFrame *parent = fd->parent; + int isNestedRoot = fd->isNestedEvalRoot; + +#ifdef DEBUG_EVAL_LOOP + fprintf(stderr, " RETURN: msg=%s, parent=%p, isNestedRoot=%d\n", + fd->message ? CSTRING(IoMessage_name(fd->message)) : "(null)", + (void*)parent, isNestedRoot); + if (parent) { + IoEvalFrameData *pd = FRAME_DATA(parent); + fprintf(stderr, " RETURN: parent msg=%s, parent state=%d\n", + pd->message ? CSTRING(IoMessage_name(pd->message)) : "(null)", + pd->state); + } + fflush(stderr); +#endif + + if (parent && !isNestedRoot) { + IoEvalFrameData *pd = FRAME_DATA(parent); + // Store result in parent's current arg slot + if (pd->state == FRAME_STATE_EVAL_ARGS) { + pd->argValues[pd->currentArgIndex] = result; + pd->currentArgIndex++; + } + // If parent is waiting for a block to return, store result + else if (pd->state == FRAME_STATE_ACTIVATE) { + pd->result = result; + pd->state = FRAME_STATE_CONTINUE_CHAIN; + } + // If parent is in a control flow state or waiting for result, store it + else if (pd->state == FRAME_STATE_IF_CONVERT_BOOLEAN || + pd->state == FRAME_STATE_IF_EVAL_BRANCH || + pd->state == FRAME_STATE_WHILE_EVAL_CONDITION || + pd->state == FRAME_STATE_WHILE_CHECK_CONDITION || + pd->state == FRAME_STATE_WHILE_DECIDE || + pd->state == FRAME_STATE_WHILE_EVAL_BODY || + pd->state == FRAME_STATE_LOOP_EVAL_BODY || + pd->state == FRAME_STATE_LOOP_AFTER_BODY || + pd->state == FRAME_STATE_FOR_EVAL_SETUP || + pd->state == FRAME_STATE_FOR_EVAL_BODY || + pd->state == FRAME_STATE_FOR_AFTER_BODY || + pd->state == FRAME_STATE_FOREACH_AFTER_BODY || +#ifdef IO_CALLCC + pd->state == FRAME_STATE_CALLCC_EVAL_BLOCK || +#endif + pd->state == FRAME_STATE_DO_WAIT || + pd->state == FRAME_STATE_CONTINUE_CHAIN) { + pd->result = result; + // State already set by parent before pushing child, don't change it + } + } + + // Mirror IoBlock_activate lines 249-252: when a block frame + // with passStops=0 finishes, propagate per-Call stopStatus + // back to state->stopStatus. This is how Io-level code like + // relayStopStatus (which does call setStopStatus(ss)) gets + // its stop status propagated to the eval loop. + if (fd->blockLocals && !fd->passStops) { + int callStopStatus = MESSAGE_STOP_STATUS_NORMAL; + if (fd->call) { + callStopStatus = IoCall_rawStopStatus((IoCall *)fd->call); + } + // Also check savedCall: when in-place if optimization + // fires and then TCO replaces fd->call, the original + // Call (with stop status set by relayStopStatus) is here. + if (callStopStatus == MESSAGE_STOP_STATUS_NORMAL && fd->savedCall) { + callStopStatus = IoCall_rawStopStatus((IoCall *)fd->savedCall); + } + if (callStopStatus != MESSAGE_STOP_STATUS_NORMAL) { + state->stopStatus = callStopStatus; + state->returnValue = result; + } + } + + // Return blockLocals to pool for reuse (before retain pool pop) + if (fd->blockLocals && + state->blockLocalsPoolSize < BLOCK_LOCALS_POOL_MAX) { + state->blockLocalsPool[state->blockLocalsPoolSize++] = + fd->blockLocals; + } + + // Return Call object to pool for reuse + if (fd->call && + state->callPoolSize < CALL_POOL_MAX) { + // Clear pointer fields for GC safety (pooled objects + // are marked, so stale pointers would keep dead objects alive) + IoCallData *cd = (IoCallData *)IoObject_dataPointer(fd->call); + cd->sender = state->ioNil; + cd->target = state->ioNil; + cd->message = state->ioNil; + cd->slotContext = state->ioNil; + cd->activated = state->ioNil; + cd->coroutine = state->ioNil; + state->callPool[state->callPoolSize++] = fd->call; + } + + if (fd->retainPoolMark) { + IoState_popRetainPoolExceptFor_(state, result); + } + + IoState_popFrame_(state); + + // If this was a nested eval root, exit the eval loop + if (isNestedRoot) { + return result; + } + + // Fast path: if parent is a hot loop state, jump directly + // to it instead of restarting the eval loop (saves frame + // fetch, safety checks, and switch dispatch per iteration). + frame = state->currentFrame; + if (frame) { + fd = FRAME_DATA(frame); + if (fd->state == FRAME_STATE_FOR_AFTER_BODY) { + goto for_after_body; + } + if (fd->state == FRAME_STATE_FOREACH_AFTER_BODY) { + goto foreach_after_body; + } + if (fd->state == FRAME_STATE_LOOP_AFTER_BODY) { + goto loop_after_body; + } + if (fd->state == FRAME_STATE_WHILE_EVAL_BODY) { + goto while_eval_body; + } + } + break; + } + + case FRAME_STATE_IF_EVAL_CONDITION: { + // Push a frame to evaluate the condition + IoEvalFrame *condFrame = IoState_pushFrame_(state); + IoEvalFrameData *condFd = FRAME_DATA(condFrame); + condFd->message = fd->controlFlow.ifInfo.conditionMsg; + condFd->target = fd->locals; + condFd->locals = fd->locals; + condFd->cachedTarget = fd->locals; + condFd->state = FRAME_STATE_START; + + // When condition returns, convert it to boolean + fd->state = FRAME_STATE_IF_CONVERT_BOOLEAN; + break; + } + + case FRAME_STATE_IF_CONVERT_BOOLEAN: { + // Condition has been evaluated, now convert to boolean + IoObject *condResult = fd->result; + + // Fast path: ioTrue, ioFalse, ioNil already are booleans. + // Skip the asBoolean frame push for these common cases + // (comparisons like <=, ==, != always return ioTrue/ioFalse). + if (condResult == state->ioTrue || + condResult == state->ioFalse || + condResult == state->ioNil) { + fd->state = FRAME_STATE_IF_EVAL_BRANCH; + break; + } + + // Push frame to send 'asBoolean' message to result + IoEvalFrame *boolFrame = IoState_pushFrame_(state); + IoEvalFrameData *boolFd = FRAME_DATA(boolFrame); + boolFd->message = state->asBooleanMessage; + boolFd->target = condResult; + boolFd->locals = condResult; + boolFd->cachedTarget = condResult; + boolFd->state = FRAME_STATE_START; + + // When boolean conversion returns, evaluate branch + fd->state = FRAME_STATE_IF_EVAL_BRANCH; + break; + } + + case FRAME_STATE_IF_EVAL_BRANCH: { + // Boolean result is in fd->result + int condition = ISTRUE(fd->result); + + // Store the condition result for potential use + fd->controlFlow.ifInfo.conditionResult = condition; + + // DEBUG + if (state->showAllMessages) { + IoMessage *trueBr = fd->controlFlow.ifInfo.trueBranch; + IoMessage *falseBr = fd->controlFlow.ifInfo.falseBranch; + printf("IF_EVAL_BRANCH: condition=%d, evaluating %s branch\n", + condition, + condition ? "TRUE" : "FALSE"); + printf(" trueBranch=%s, falseBranch=%s\n", + trueBr ? CSTRING(IoMessage_name(trueBr)) : "NULL", + falseBr ? CSTRING(IoMessage_name(falseBr)) : "NULL"); + } + + // Determine which branch to take + IoMessage *branch = condition ? fd->controlFlow.ifInfo.trueBranch + : fd->controlFlow.ifInfo.falseBranch; + + if (branch) { + // Tail position optimization: if the if() is the last + // message in the chain, evaluate the branch directly in + // this frame instead of pushing a child. This enables TCO + // for the common pattern: + // method(n, if(n <= 0, acc, recurse(n - 1, acc))) + if (!IOMESSAGEDATA(fd->message)->next) { + // Preserve original Call so RETURN can check its + // stop status even after TCO replaces fd->call + // (needed for relayStopStatus / ? operator) + if (fd->call) { + fd->savedCall = fd->call; + } + fd->message = branch; + fd->target = fd->locals; + fd->cachedTarget = fd->locals; + fd->state = FRAME_STATE_START; + if (fd->argValues) { + if (fd->argValues != fd->inlineArgs) { + io_free(fd->argValues); + } + fd->argValues = NULL; + } + fd->argCount = 0; + fd->currentArgIndex = 0; + } else { + // Not tail position: push branch frame + IoEvalFrame *branchFrame = IoState_pushFrame_(state); + IoEvalFrameData *branchFd = FRAME_DATA(branchFrame); + branchFd->message = branch; + branchFd->target = fd->locals; + branchFd->locals = fd->locals; + branchFd->cachedTarget = fd->locals; + branchFd->state = FRAME_STATE_START; + fd->state = FRAME_STATE_CONTINUE_CHAIN; + } + } else { + // No branch for this condition, return boolean + fd->result = IOBOOL(fd->target, condition); + fd->state = FRAME_STATE_CONTINUE_CHAIN; + } + break; + } + + // ============================================================ + // WHILE LOOP STATE MACHINE + // ============================================================ + + case FRAME_STATE_WHILE_EVAL_CONDITION: { + // Push a frame to evaluate the condition + IoEvalFrame *condFrame = IoState_pushFrame_(state); + IoEvalFrameData *condFd = FRAME_DATA(condFrame); + condFd->message = fd->controlFlow.whileInfo.conditionMsg; + condFd->target = fd->locals; + condFd->locals = fd->locals; + condFd->cachedTarget = fd->locals; + condFd->state = FRAME_STATE_START; + + // When condition returns, we need to convert it to boolean + fd->state = FRAME_STATE_WHILE_CHECK_CONDITION; + break; + } + + case FRAME_STATE_WHILE_CHECK_CONDITION: { + // Condition has been evaluated, result is in fd->result + // Now convert to boolean by sending asBoolean message + IoObject *condResult = fd->result; + + // Fast path: skip asBoolean for true/false/nil singletons + if (condResult == state->ioTrue || + condResult == state->ioFalse || + condResult == state->ioNil) { + fd->state = FRAME_STATE_WHILE_DECIDE; + break; + } + + IoEvalFrame *boolFrame = IoState_pushFrame_(state); + IoEvalFrameData *boolFd = FRAME_DATA(boolFrame); + boolFd->message = state->asBooleanMessage; + boolFd->target = condResult; + boolFd->locals = condResult; + boolFd->cachedTarget = condResult; + boolFd->state = FRAME_STATE_START; + + // When boolean conversion returns, check result and maybe eval body + fd->state = FRAME_STATE_WHILE_DECIDE; + break; + } + + case FRAME_STATE_WHILE_DECIDE: { + // Boolean result is in fd->result + int condition = ISTRUE(fd->result); + + if (state->showAllMessages) { + printf("WHILE_DECIDE: condition=%d\n", condition); + } + + if (!condition) { + // Condition is false - exit loop + // Return the last body result (stored in whileInfo) or nil + fd->result = fd->controlFlow.whileInfo.lastResult + ? fd->controlFlow.whileInfo.lastResult + : state->ioNil; + fd->state = FRAME_STATE_CONTINUE_CHAIN; + } else { + // Fast path: body is a cached literal + if (BODY_IS_CACHED_LITERAL(fd->controlFlow.whileInfo.bodyMsg)) { + fd->result = CACHED_LITERAL_RESULT(fd->controlFlow.whileInfo.bodyMsg); + fd->state = FRAME_STATE_WHILE_EVAL_BODY; + break; + } + + // Condition is true - evaluate body + IoEvalFrame *bodyFrame = IoState_pushFrame_(state); + IoEvalFrameData *bodyFd = FRAME_DATA(bodyFrame); + bodyFd->message = fd->controlFlow.whileInfo.bodyMsg; + bodyFd->target = fd->locals; + bodyFd->locals = fd->locals; + bodyFd->cachedTarget = fd->locals; + bodyFd->state = FRAME_STATE_START; + bodyFd->passStops = 1; // Let break/continue propagate to us + + fd->state = FRAME_STATE_WHILE_EVAL_BODY; + } + break; + } + + while_eval_body: + case FRAME_STATE_WHILE_EVAL_BODY: { + // Body has been evaluated, result is in fd->result + // Store the result for potential return + fd->controlFlow.whileInfo.lastResult = fd->result; + + // Check for break/continue + if (state->stopStatus == MESSAGE_STOP_STATUS_BREAK) { + // Break - exit loop with break value + IoState_resetStopStatus(state); + fd->result = state->returnValue ? state->returnValue : state->ioNil; + fd->state = FRAME_STATE_CONTINUE_CHAIN; + break; + } + + if (state->stopStatus == MESSAGE_STOP_STATUS_CONTINUE) { + // Continue - go back to condition + IoState_resetStopStatus(state); + fd->state = FRAME_STATE_WHILE_EVAL_CONDITION; + break; + } + + if (state->stopStatus == MESSAGE_STOP_STATUS_RETURN) { + // Return - route through CONTINUE_CHAIN so method + // frames can catch it (sets result = returnValue) + fd->state = FRAME_STATE_CONTINUE_CHAIN; + break; + } + + // Normal - loop back to condition + fd->state = FRAME_STATE_WHILE_EVAL_CONDITION; + break; + } + + // ============================================================ + // LOOP (infinite) STATE MACHINE + // ============================================================ + + case FRAME_STATE_LOOP_EVAL_BODY: { + // For first iteration, just push body frame + // For subsequent iterations, we arrive here after body returns + + // Check if this is first iteration (lastResult is NULL) + if (fd->controlFlow.loopInfo.lastResult == NULL) { + // Mark that we've started (use a non-null value) + fd->controlFlow.loopInfo.lastResult = state->ioNil; + + // Fast path: body is a cached literal + if (BODY_IS_CACHED_LITERAL(fd->controlFlow.loopInfo.bodyMsg)) { + fd->result = CACHED_LITERAL_RESULT(fd->controlFlow.loopInfo.bodyMsg); + fd->state = FRAME_STATE_LOOP_AFTER_BODY; + break; + } + + // First iteration - push body frame + IoEvalFrame *bodyFrame = IoState_pushFrame_(state); + IoEvalFrameData *bodyFd = FRAME_DATA(bodyFrame); + bodyFd->message = fd->controlFlow.loopInfo.bodyMsg; + bodyFd->target = fd->locals; + bodyFd->locals = fd->locals; + bodyFd->cachedTarget = fd->locals; + bodyFd->state = FRAME_STATE_START; + bodyFd->passStops = 1; + + fd->state = FRAME_STATE_LOOP_AFTER_BODY; + break; + } + + // Should not reach here - use LOOP_AFTER_BODY instead + fd->state = FRAME_STATE_LOOP_AFTER_BODY; + break; + } + + loop_after_body: + case FRAME_STATE_LOOP_AFTER_BODY: { + // Body has been evaluated + fd->controlFlow.loopInfo.lastResult = fd->result; + + // Check for break + if (state->stopStatus == MESSAGE_STOP_STATUS_BREAK) { + IoState_resetStopStatus(state); + fd->result = state->returnValue ? state->returnValue : state->ioNil; + fd->state = FRAME_STATE_CONTINUE_CHAIN; + break; + } + + // Check for continue (just loop again) + if (state->stopStatus == MESSAGE_STOP_STATUS_CONTINUE) { + IoState_resetStopStatus(state); + } + + // Check for return (propagate through CONTINUE_CHAIN) + if (state->stopStatus == MESSAGE_STOP_STATUS_RETURN) { + fd->state = FRAME_STATE_CONTINUE_CHAIN; + break; + } + + // Fast path: body is a cached literal + if (BODY_IS_CACHED_LITERAL(fd->controlFlow.loopInfo.bodyMsg)) { + fd->result = CACHED_LITERAL_RESULT(fd->controlFlow.loopInfo.bodyMsg); + // Stay in LOOP_AFTER_BODY + break; + } + + // Loop again - push another body frame + IoEvalFrame *bodyFrame = IoState_pushFrame_(state); + IoEvalFrameData *bodyFd = FRAME_DATA(bodyFrame); + bodyFd->message = fd->controlFlow.loopInfo.bodyMsg; + bodyFd->target = fd->locals; + bodyFd->locals = fd->locals; + bodyFd->cachedTarget = fd->locals; + bodyFd->state = FRAME_STATE_START; + bodyFd->passStops = 1; + + // Stay in LOOP_AFTER_BODY to handle next iteration + break; + } + + // ============================================================ + // FOR LOOP STATE MACHINE + // ============================================================ + + case FRAME_STATE_FOR_EVAL_SETUP: + // Note: Currently unused - for primitive evaluates setup synchronously + // and jumps directly to FOR_EVAL_BODY. This case exists for potential + // future async setup evaluation. + fd->state = FRAME_STATE_FOR_EVAL_BODY; + break; + + case FRAME_STATE_FOR_EVAL_BODY: { + // Check if we need to initialize or continue + if (!fd->controlFlow.forInfo.initialized) { + // First time - just evaluate body for first iteration + fd->controlFlow.forInfo.initialized = 1; + + // Check if we should even start + double i = fd->controlFlow.forInfo.currentValue; + double end = fd->controlFlow.forInfo.endValue; + double incr = fd->controlFlow.forInfo.increment; + + if ((incr > 0 && i > end) || (incr < 0 && i < end)) { + // Range is empty, return nil + fd->result = state->ioNil; + fd->state = FRAME_STATE_CONTINUE_CHAIN; + break; + } + + // Set the counter variable + IoObject *num = IoState_numberWithDouble_(state, i); +#ifdef COLLECTOR_USE_REFCOUNT + // Opt-in to RC tracking for for-loop counter Numbers + ((CollectorMarker *)num)->refCount = 1; +#endif + IoObject_setSlot_to_(fd->locals, + fd->controlFlow.forInfo.counterName, num); + // Counter is now reachable via PHash slot; pop retain stack + Stack_pop(state->currentIoStack); + + // Fast path: body is a cached literal — run entire loop inline + if (BODY_IS_CACHED_LITERAL(fd->controlFlow.forInfo.bodyMsg)) { + IoObject *cachedBody = CACHED_LITERAL_RESULT(fd->controlFlow.forInfo.bodyMsg); + double loopIncr = fd->controlFlow.forInfo.increment; + double loopEnd = fd->controlFlow.forInfo.endValue; + IoSymbol *ctrName = fd->controlFlow.forInfo.counterName; + PHash *localSlots = IoObject_slots(fd->locals); + fd->controlFlow.forInfo.currentValue += loopIncr; + for (;;) { + double li = fd->controlFlow.forInfo.currentValue; + if ((loopIncr > 0 && li > loopEnd) || (loopIncr < 0 && li < loopEnd)) { + break; + } +#ifdef COLLECTOR_USE_REFCOUNT + IoObject *oldCtr1 = (IoObject *)PHash_at_(localSlots, ctrName); +#endif + { + IoObject *newCtr1 = IoState_numberWithDouble_(state, li); +#ifdef COLLECTOR_USE_REFCOUNT + ((CollectorMarker *)newCtr1)->refCount = 1; +#endif + PHash_at_put_(localSlots, ctrName, newCtr1); + } + // Counter is in PHash slot; pop retain to prevent + // unbounded ioStack growth + Stack_pop(state->currentIoStack); +#ifdef COLLECTOR_USE_REFCOUNT + if (oldCtr1) { + Collector_value_removingRefTo_(state->collector, oldCtr1); + Collector_rcDrainFreeList_(state->collector); + } +#endif + fd->controlFlow.forInfo.currentValue += loopIncr; + } + fd->result = cachedBody; + fd->state = FRAME_STATE_CONTINUE_CHAIN; + break; + } + + // Push body frame + IoEvalFrame *bodyFrame = IoState_pushFrame_(state); + IoEvalFrameData *bodyFd = FRAME_DATA(bodyFrame); + bodyFd->message = fd->controlFlow.forInfo.bodyMsg; + bodyFd->target = fd->locals; + bodyFd->locals = fd->locals; + bodyFd->cachedTarget = fd->locals; + bodyFd->state = FRAME_STATE_START; + bodyFd->passStops = 1; + + fd->state = FRAME_STATE_FOR_AFTER_BODY; + break; + } + + // Should not reach here + fd->state = FRAME_STATE_FOR_AFTER_BODY; + break; + } + + for_after_body: + case FRAME_STATE_FOR_AFTER_BODY: { + // Body has been evaluated + fd->controlFlow.forInfo.lastResult = fd->result; + + // Check for break + if (state->stopStatus == MESSAGE_STOP_STATUS_BREAK) { + IoState_resetStopStatus(state); + fd->result = state->returnValue ? state->returnValue : state->ioNil; + fd->state = FRAME_STATE_CONTINUE_CHAIN; + break; + } + + // Check for continue + if (state->stopStatus == MESSAGE_STOP_STATUS_CONTINUE) { + IoState_resetStopStatus(state); + // Fall through to increment and continue + } + + // Check for return (propagate through CONTINUE_CHAIN) + if (state->stopStatus == MESSAGE_STOP_STATUS_RETURN) { + fd->state = FRAME_STATE_CONTINUE_CHAIN; + break; + } + + // Increment counter + fd->controlFlow.forInfo.currentValue += fd->controlFlow.forInfo.increment; + + // Fast path: body is a cached literal — run remaining iterations + // in a tight C loop, bypassing the eval loop overhead entirely. + // This matches master's tight C for-loop performance. + if (BODY_IS_CACHED_LITERAL(fd->controlFlow.forInfo.bodyMsg)) { + IoObject *cachedBody = CACHED_LITERAL_RESULT(fd->controlFlow.forInfo.bodyMsg); + double incr = fd->controlFlow.forInfo.increment; + double end = fd->controlFlow.forInfo.endValue; + IoSymbol *counterName = fd->controlFlow.forInfo.counterName; + PHash *localSlots = IoObject_slots(fd->locals); + + for (;;) { + double i = fd->controlFlow.forInfo.currentValue; + if ((incr > 0 && i > end) || (incr < 0 && i < end)) { + fd->result = cachedBody; + fd->state = FRAME_STATE_CONTINUE_CHAIN; + goto for_after_body_done; + } +#ifdef COLLECTOR_USE_REFCOUNT + IoObject *oldCtr2 = (IoObject *)PHash_at_(localSlots, counterName); +#endif + { + IoObject *newCtr2 = IoState_numberWithDouble_(state, i); +#ifdef COLLECTOR_USE_REFCOUNT + ((CollectorMarker *)newCtr2)->refCount = 1; +#endif + PHash_at_put_(localSlots, counterName, newCtr2); + } + Stack_pop(state->currentIoStack); +#ifdef COLLECTOR_USE_REFCOUNT + if (oldCtr2) { + Collector_value_removingRefTo_(state->collector, oldCtr2); + Collector_rcDrainFreeList_(state->collector); + } +#endif + fd->controlFlow.forInfo.currentValue += incr; + } + } + + { + double i = fd->controlFlow.forInfo.currentValue; + double end = fd->controlFlow.forInfo.endValue; + double incr = fd->controlFlow.forInfo.increment; + + // Check if we should continue + if ((incr > 0 && i > end) || (incr < 0 && i < end)) { + // Done - return last result + fd->result = fd->controlFlow.forInfo.lastResult + ? fd->controlFlow.forInfo.lastResult + : state->ioNil; + fd->state = FRAME_STATE_CONTINUE_CHAIN; + break; + } + + // Set the counter variable (direct PHash access since + // slots are guaranteed to exist from for-loop setup) + IoObject *num = IoState_numberWithDouble_(state, i); +#ifdef COLLECTOR_USE_REFCOUNT + ((CollectorMarker *)num)->refCount = 1; + IoObject *oldCtr3 = (IoObject *)PHash_at_(IoObject_slots(fd->locals), + fd->controlFlow.forInfo.counterName); +#endif + PHash_at_put_(IoObject_slots(fd->locals), + fd->controlFlow.forInfo.counterName, num); + // Counter is now reachable via PHash slot; pop retain stack + Stack_pop(state->currentIoStack); +#ifdef COLLECTOR_USE_REFCOUNT + if (oldCtr3) { + Collector_value_removingRefTo_(state->collector, oldCtr3); + Collector_rcDrainFreeList_(state->collector); + } +#endif + + // Push body frame for next iteration + IoEvalFrame *bodyFrame = IoState_pushFrame_(state); + IoEvalFrameData *bodyFd = FRAME_DATA(bodyFrame); + bodyFd->message = fd->controlFlow.forInfo.bodyMsg; + bodyFd->target = fd->locals; + bodyFd->locals = fd->locals; + bodyFd->cachedTarget = fd->locals; + bodyFd->state = FRAME_STATE_START; + bodyFd->passStops = 1; + } + + // Stay in FOR_AFTER_BODY + for_after_body_done: + break; + } + + // ============================================================ + // FOREACH STATE MACHINE (collection iteration) + // ============================================================ + + case FRAME_STATE_FOREACH_EVAL_BODY: { + int idx = fd->controlFlow.foreachInfo.currentIndex; + int size = fd->controlFlow.foreachInfo.collectionSize; + int dir = fd->controlFlow.foreachInfo.direction; + + // Check bounds + if (dir > 0 ? (idx >= size) : (idx < 0)) { + // Done iterating + fd->result = fd->controlFlow.foreachInfo.lastResult + ? fd->controlFlow.foreachInfo.lastResult + : state->ioNil; + fd->state = FRAME_STATE_CONTINUE_CHAIN; + break; + } + + // Get element from collection + IoObject *collection = fd->controlFlow.foreachInfo.collection; + IoObject *mapSource = fd->controlFlow.foreachInfo.mapSource; + IoObject *element = NULL; + + if (ISLIST(collection)) { + List *list = IoList_rawList(collection); + // Re-check size (list may have been mutated) + int currentSize = (int)List_size(list); + if (dir > 0 ? (idx >= currentSize) : (idx < 0)) { + fd->result = fd->controlFlow.foreachInfo.lastResult + ? fd->controlFlow.foreachInfo.lastResult + : state->ioNil; + fd->state = FRAME_STATE_CONTINUE_CHAIN; + break; + } + fd->controlFlow.foreachInfo.collectionSize = currentSize; + + if (mapSource) { + // Map iteration: keys list, look up value from map + IoSymbol *key = (IoSymbol *)List_at_(list, idx); + if (fd->controlFlow.foreachInfo.indexName) { + IoObject_setSlot_to_(fd->locals, + fd->controlFlow.foreachInfo.indexName, key); + } + element = IoMap_rawAt(mapSource, key); + if (!element) element = state->ioNil; + } else { + element = (IoObject *)List_at_(list, idx); + } + } + + if (!element) element = state->ioNil; + + // Set slot values + if (fd->controlFlow.foreachInfo.isEach) { + // "each" mode: send body message to element as target + } else if (!mapSource) { + // List/Seq iteration: set index and value slots + if (fd->controlFlow.foreachInfo.indexName) { + IoObject_setSlot_to_(fd->locals, + fd->controlFlow.foreachInfo.indexName, + IoState_numberWithDouble_(state, idx)); + Stack_pop(state->currentIoStack); + } + if (fd->controlFlow.foreachInfo.valueName) { + IoObject_setSlot_to_(fd->locals, + fd->controlFlow.foreachInfo.valueName, element); + } + } else { + // Map: value slot (indexName/key already set above) + if (fd->controlFlow.foreachInfo.valueName) { + IoObject_setSlot_to_(fd->locals, + fd->controlFlow.foreachInfo.valueName, element); + } + } + + // Fast path: body is a cached literal — run remaining iterations + // in a tight C loop for forward List iteration (not "each", not map). + if (BODY_IS_CACHED_LITERAL(fd->controlFlow.foreachInfo.bodyMsg) && + ISLIST(collection) && !mapSource && + !fd->controlFlow.foreachInfo.isEach && dir > 0) { + IoObject *cachedBody = CACHED_LITERAL_RESULT(fd->controlFlow.foreachInfo.bodyMsg); + IoSymbol *indexName = fd->controlFlow.foreachInfo.indexName; + IoSymbol *valueName = fd->controlFlow.foreachInfo.valueName; + List *list = IoList_rawList(collection); + + // First iteration slots already set above, advance to next + idx += dir; + for (;;) { + int currentSize = (int)List_size(list); + if (idx >= currentSize) break; + IoObject *el = (IoObject *)List_at_(list, idx); + if (!el) el = state->ioNil; + + if (indexName) { + IoObject_setSlot_to_(fd->locals, indexName, + IoState_numberWithDouble_(state, idx)); + Stack_pop(state->currentIoStack); + } + if (valueName) { + IoObject_setSlot_to_(fd->locals, valueName, el); + } + idx++; + } + fd->controlFlow.foreachInfo.currentIndex = idx; + fd->result = cachedBody; + fd->state = FRAME_STATE_CONTINUE_CHAIN; + break; + } + + // Slower cached literal fallback (map, reverse, each) + if (BODY_IS_CACHED_LITERAL(fd->controlFlow.foreachInfo.bodyMsg)) { + fd->result = CACHED_LITERAL_RESULT(fd->controlFlow.foreachInfo.bodyMsg); + fd->state = FRAME_STATE_FOREACH_AFTER_BODY; + break; + } + + // Push body frame + IoEvalFrame *bodyFrame = IoState_pushFrame_(state); + IoEvalFrameData *bodyFd = FRAME_DATA(bodyFrame); + bodyFd->message = fd->controlFlow.foreachInfo.bodyMsg; + if (fd->controlFlow.foreachInfo.isEach) { + // "each": body runs with element as target + bodyFd->target = element; + bodyFd->locals = fd->locals; + bodyFd->cachedTarget = element; + } else { + bodyFd->target = fd->locals; + bodyFd->locals = fd->locals; + bodyFd->cachedTarget = fd->locals; + } + bodyFd->state = FRAME_STATE_START; + bodyFd->passStops = 1; + + fd->state = FRAME_STATE_FOREACH_AFTER_BODY; + break; + } + + foreach_after_body: + case FRAME_STATE_FOREACH_AFTER_BODY: { + fd->controlFlow.foreachInfo.lastResult = fd->result; + + // Check for break + if (state->stopStatus == MESSAGE_STOP_STATUS_BREAK) { + IoState_resetStopStatus(state); + fd->result = state->returnValue ? state->returnValue : state->ioNil; + fd->state = FRAME_STATE_CONTINUE_CHAIN; + break; + } + + // Check for continue + if (state->stopStatus == MESSAGE_STOP_STATUS_CONTINUE) { + IoState_resetStopStatus(state); + // Fall through to increment + } + + // Check for return (propagate through CONTINUE_CHAIN) + if (state->stopStatus == MESSAGE_STOP_STATUS_RETURN) { + fd->state = FRAME_STATE_CONTINUE_CHAIN; + break; + } + + // Increment/decrement index + fd->controlFlow.foreachInfo.currentIndex += + fd->controlFlow.foreachInfo.direction; + + // Go back to EVAL_BODY (checks bounds at top) + fd->state = FRAME_STATE_FOREACH_EVAL_BODY; + break; + } + +#ifdef IO_CALLCC + case FRAME_STATE_CALLCC_EVAL_BLOCK: { + // Block body has finished evaluating + // The result is in fd->result (set by RETURN handler) + // This is the "normal return" path - continuation was not invoked + + // Move to continue chain + fd->state = FRAME_STATE_CONTINUE_CHAIN; + break; + } +#endif + + // ============================================================ + // COROUTINE STATES + // ============================================================ + + case FRAME_STATE_CORO_WAIT_CHILD: { + // Waiting for a child coroutine to complete. + // When we get here, the child has finished and set fd->result. + // Just continue the chain. + fd->state = FRAME_STATE_CONTINUE_CHAIN; + break; + } + + case FRAME_STATE_CORO_YIELDED: { + // Resumed after yield/resume. Continue where we left off. + fd->state = FRAME_STATE_CONTINUE_CHAIN; + break; + } + + // ============================================================ + // DO STRING/MESSAGE/FILE STATE MACHINE (Phase 1: C stack elimination) + // ============================================================ + + case FRAME_STATE_DO_EVAL: { + // Push frame to evaluate the compiled code + IoEvalFrame *childFrame = IoState_pushFrame_(state); + IoEvalFrameData *childFd = FRAME_DATA(childFrame); + childFd->message = fd->controlFlow.doInfo.codeMessage; + childFd->target = fd->controlFlow.doInfo.evalTarget; + childFd->locals = fd->controlFlow.doInfo.evalLocals; + childFd->cachedTarget = fd->controlFlow.doInfo.evalTarget; + childFd->state = FRAME_STATE_START; + + fd->state = FRAME_STATE_DO_WAIT; + break; + } + + case FRAME_STATE_DO_WAIT: { + // Child frame has returned, result is in fd->result + // (set by RETURN handler when child completes) + // Continue to next message in chain + fd->state = FRAME_STATE_CONTINUE_CHAIN; + break; + } + + } // end switch + } // end while (unreachable due to while(1) and returns) + + return result; +} + +// Helper to activate a block without C recursion +static void IoState_activateBlock_(IoState *state, IoEvalFrame *callerFrame) { + IoEvalFrameData *callerFd = FRAME_DATA(callerFrame); + IoBlock *block = (IoBlock *)callerFd->slotValue; + IoBlockData *blockData = (IoBlockData *)IoObject_dataPointer(block); + + // DEBUG + if (state->showAllMessages) { + printf("ACTIVATING BLOCK (method call), body message: %s\n", + blockData->message ? CSTRING(IoMessage_name(blockData->message)) : "NULL"); + if (blockData->message && IOMESSAGEDATA(blockData->message)->next) { + printf(" ... with next: %s\n", CSTRING(IoMessage_name(IOMESSAGEDATA(blockData->message)->next))); + } + } + + // Push retain pool BEFORE creating block locals/call objects. + // Everything created during block activation and body execution + // lands above this mark. When the block returns, the pool is + // popped (keeping only the result), releasing temporaries from + // the ioStack so GC can collect unreferenced objects. + uintptr_t retainPoolMark = IoState_pushRetainPool(state); + + // Create or reuse block locals + IoObject *blockLocals; + PHash *bslots; + if (state->blockLocalsPoolSize > 0) { + // Reuse pooled blockLocals (already has PHash allocated) + blockLocals = state->blockLocalsPool[--state->blockLocalsPoolSize]; + // Push onto ioStack so GC can reach it. Without this, + // the blockLocals is unreachable between pool removal and + // frame attachment — any GC triggered by IoCall_with or + // argument evaluation would sweep it. + IoState_stackRetain_(state, blockLocals); + bslots = IoObject_slots(blockLocals); + PHash_clean(bslots); + } else { + blockLocals = IOCLONE(state->localsProto); + IoObject_isLocals_(blockLocals, 1); + IoObject_createSlotsIfNeeded(blockLocals); + bslots = IoObject_slots(blockLocals); + } + + + // Determine scope + IoObject *scope = + blockData->scope ? blockData->scope : callerFd->target; + + // Create or reuse Call object + IoCall *callObject; + if (state->callPoolSize > 0) { + // Reuse pooled Call (already a valid collector object with + // allocated IoCallData). Just reset the fields. + callObject = state->callPool[--state->callPoolSize]; + IoState_stackRetain_(state, callObject); + } else { + // Allocate new Call via IOCLONE + callObject = IOCLONE(state->callProto); + } + { + IoCallData *cd = (IoCallData *)IoObject_dataPointer(callObject); + cd->sender = callerFd->locals; + cd->target = callerFd->target; + cd->message = callerFd->message; + cd->slotContext = callerFd->slotContext; + cd->activated = block; + cd->coroutine = state->currentCoroutine; + cd->stopStatus = MESSAGE_STOP_STATUS_NORMAL; + } + + PHash_at_put_(bslots, state->callSymbol, callObject); + PHash_at_put_(bslots, state->selfSymbol, scope); + PHash_at_put_(bslots, state->updateSlotSymbol, + state->localsUpdateSlotCFunc); + + + // Bind arguments + // Named formal parameters are pre-evaluated by the eval loop + // (stored in callerFd->argValues). This eliminates C stack + // re-entrancy for the common case. + List *argNames = blockData->argNames; + IoMessage *m = callerFd->message; + int argCount = IoMessage_argCount(m); + + LIST_FOREACH(argNames, i, name, + IoObject *arg; + if ((int)i < argCount) { + // Use pre-evaluated value if available + if (callerFd->argValues && + (int)i < callerFd->argCount && + callerFd->argValues[(int)i] != NULL) { + arg = callerFd->argValues[(int)i]; + } else { + // Fallback: evaluate in sender's context (recursive) + arg = IoMessage_locals_valueArgAt_( + m, callerFd->locals, (int)i); + } + } else { + // Unbound params (fewer args than params) default to nil + arg = state->ioNil; + } + IoObject_setSlot_to_(blockLocals, name, arg);); + + // Mark these as unreferenced for potential recycling + IoObject_isReferenced_(blockLocals, 0); + IoObject_isReferenced_(callObject, 0); + + // Push new frame for block body evaluation + IoEvalFrame *blockFrame = IoState_pushFrame_(state); + IoEvalFrameData *blockFd = FRAME_DATA(blockFrame); + blockFd->message = blockData->message; + blockFd->target = blockLocals; + blockFd->locals = blockLocals; + blockFd->cachedTarget = blockLocals; + blockFd->state = FRAME_STATE_START; + blockFd->call = callObject; + blockFd->blockLocals = blockLocals; + blockFd->passStops = blockData->passStops; + blockFd->retainPoolMark = retainPoolMark; + + // Caller frame will wait for block frame to return + // Keep callerFrame in ACTIVATE state - when blockFrame returns, + // the RETURN handler will move callerFrame to CONTINUE_CHAIN +} + +// Tail Call Optimization: reuse the current block body frame for a tail call. +// Instead of pushing a new frame on top of the current one, we replace the +// current frame's context with the new block's context. This keeps the frame +// stack flat for recursive calls (e.g., factorial(n-1, n*acc)). +// +// blockFrame is the CURRENT block body frame being reused. +// blockFd->slotValue contains the Block to tail-call. +// blockFd->message, ->target, ->locals, ->slotContext have the call info. +static void IoState_activateBlockTCO_(IoState *state, IoEvalFrame *blockFrame) { + IoEvalFrameData *blockFd = FRAME_DATA(blockFrame); + IoBlock *block = (IoBlock *)blockFd->slotValue; + IoBlockData *blockData = (IoBlockData *)IoObject_dataPointer(block); + + // NOTE: We do NOT return old blockLocals to the pool here. + // The new block's Call object references the old blockLocals + // as call sender/target (via blockFd->locals/target passed to + // IoCall_with below). Pooling the old blockLocals would clean + // its PHash, destroying slots that call sender still needs + // (e.g., the ? operator's "m" slot used by relayStopStatus). + // Only the RETURN handler pools blockLocals, when the block + // has fully completed and its locals are no longer referenced. + + // Create or reuse block locals for the tail-called block + IoObject *blockLocals; + PHash *bslots; + if (state->blockLocalsPoolSize > 0) { + blockLocals = state->blockLocalsPool[--state->blockLocalsPoolSize]; + IoState_stackRetain_(state, blockLocals); + bslots = IoObject_slots(blockLocals); + PHash_clean(bslots); + } else { + blockLocals = IOCLONE(state->localsProto); + IoObject_isLocals_(blockLocals, 1); + IoObject_createSlotsIfNeeded(blockLocals); + bslots = IoObject_slots(blockLocals); + } + + // Determine scope + IoObject *scope = + blockData->scope ? blockData->scope : blockFd->target; + + // Create or reuse Call object + IoCall *callObject; + if (state->callPoolSize > 0) { + callObject = state->callPool[--state->callPoolSize]; + IoState_stackRetain_(state, callObject); + } else { + callObject = IOCLONE(state->callProto); + } + { + IoCallData *cd = (IoCallData *)IoObject_dataPointer(callObject); + cd->sender = blockFd->locals; + cd->target = blockFd->target; + cd->message = blockFd->message; + cd->slotContext = blockFd->slotContext; + cd->activated = block; + cd->coroutine = state->currentCoroutine; + cd->stopStatus = MESSAGE_STOP_STATUS_NORMAL; + } + + PHash_at_put_(bslots, state->callSymbol, callObject); + PHash_at_put_(bslots, state->selfSymbol, scope); + PHash_at_put_(bslots, state->updateSlotSymbol, + state->localsUpdateSlotCFunc); + + // Bind arguments (same as activateBlock_) + List *argNames = blockData->argNames; + IoMessage *m = blockFd->message; + int argCount = IoMessage_argCount(m); + + LIST_FOREACH(argNames, i, name, + IoObject *arg; + if ((int)i < argCount) { + if (blockFd->argValues && + (int)i < blockFd->argCount && + blockFd->argValues[(int)i] != NULL) { + arg = blockFd->argValues[(int)i]; + } else { + arg = IoMessage_locals_valueArgAt_( + m, blockFd->locals, (int)i); + } + } else { + // Unbound params (fewer args than params) default to nil + arg = state->ioNil; + } + IoObject_setSlot_to_(blockLocals, name, arg);); + + IoObject_isReferenced_(blockLocals, 0); + IoObject_isReferenced_(callObject, 0); + + // REUSE the current frame: replace context with the tail-called block + blockFd->message = blockData->message; + blockFd->target = blockLocals; + blockFd->locals = blockLocals; + blockFd->cachedTarget = blockLocals; + blockFd->state = FRAME_STATE_START; + blockFd->call = callObject; + blockFd->blockLocals = blockLocals; + blockFd->passStops = blockData->passStops; + + // Free pre-evaluated args from the previous call + if (blockFd->argValues) { + if (blockFd->argValues != blockFd->inlineArgs) { + io_free(blockFd->argValues); + } + blockFd->argValues = NULL; + } + blockFd->argCount = 0; + blockFd->currentArgIndex = 0; + blockFd->result = NULL; + blockFd->slotValue = NULL; + blockFd->slotContext = NULL; + + // Signal control flow handling so the eval loop restarts + // with the updated frame (don't process stale ACTIVATE state) + state->needsControlFlowHandling = 1; +} + +// Entry point for iterative evaluation from C code +// This is called when a CFunction needs to evaluate an argument. +// It pushes a frame and runs the eval loop until that frame returns. +IoObject *IoMessage_locals_performOn_iterative(IoMessage *self, + IoObject *locals, + IoObject *target) { + IoState *state = IOSTATE; + + // Push initial frame + IoEvalFrame *frame = IoState_pushFrame_(state); + IoEvalFrameData *fd = FRAME_DATA(frame); + fd->message = self; + fd->target = target; + fd->locals = locals; + fd->cachedTarget = target; + fd->state = FRAME_STATE_START; + fd->isNestedEvalRoot = 1; // Mark this frame as a nested eval boundary + + // Track nested eval depth so the eval loop knows to return when + // hitting an isNestedEvalRoot boundary during stop-status unwinding + state->nestedEvalDepth++; + + // Run evaluation loop - it will return when this frame (or the coroutine) completes + IoObject *result = IoState_evalLoop_(state); + + state->nestedEvalDepth--; + + return result; +} diff --git a/libs/iovm/source/IoState_iterative_fast.c b/libs/iovm/source/IoState_iterative_fast.c new file mode 100644 index 000000000..7c4a57857 --- /dev/null +++ b/libs/iovm/source/IoState_iterative_fast.c @@ -0,0 +1,372 @@ + +// metadoc State copyright Steve Dekorte 2002, 2025 +// metadoc State license BSD revised +/*metadoc State description +High-performance iterative evaluator with aggressive optimizations: +- Frame pooling (no malloc/free in hot path) +- Computed gotos (faster than switch) +- Inline fast paths for common cases +- Local variable caching +*/ + +#include "IoState.h" +#include "IoEvalFrame.h" +#include "IoMessage.h" +#include "IoObject.h" +#include "IoBlock.h" +#include "IoCall.h" + +// Frame pool for fast allocation +#define FRAME_POOL_SIZE 256 + +typedef struct IoFramePool { + IoEvalFrame frames[FRAME_POOL_SIZE]; + int freeList[FRAME_POOL_SIZE]; + int freeCount; +} IoFramePool; + +// Initialize frame pool +static void IoFramePool_init(IoFramePool *pool) { + pool->freeCount = FRAME_POOL_SIZE; + for (int i = 0; i < FRAME_POOL_SIZE; i++) { + pool->freeList[i] = i; + IoEvalFrame_reset(&pool->frames[i]); + } +} + +// Allocate frame from pool (inline for speed) +static inline IoEvalFrame *IoFramePool_alloc(IoFramePool *pool) { + if (pool->freeCount > 0) { + int idx = pool->freeList[--pool->freeCount]; + return &pool->frames[idx]; + } + // Pool exhausted - fall back to malloc + return IoEvalFrame_new(); +} + +// Return frame to pool (inline for speed) +static inline void IoFramePool_free(IoFramePool *pool, IoEvalFrame *frame) { + // Check if frame is from pool + if (frame >= &pool->frames[0] && + frame < &pool->frames[FRAME_POOL_SIZE]) { + int idx = frame - &pool->frames[0]; + IoEvalFrame_reset(frame); + pool->freeList[pool->freeCount++] = idx; + } else { + IoEvalFrame_free(frame); + } +} + +// Push frame using pool +static inline IoEvalFrame *IoState_pushFrameFast_(IoState *state, IoFramePool *pool) { + IoEvalFrame *frame = IoFramePool_alloc(pool); + frame->parent = state->currentFrame; + state->currentFrame = frame; + state->frameDepth++; + + if (state->frameDepth > state->maxFrameDepth) { + IoState_error_(state, NULL, "Stack overflow: frame depth exceeded %d", + state->maxFrameDepth); + } + + return frame; +} + +// Pop frame using pool +static inline void IoState_popFrameFast_(IoState *state, IoFramePool *pool) { + IoEvalFrame *frame = state->currentFrame; + if (frame) { + state->currentFrame = frame->parent; + state->frameDepth--; + IoFramePool_free(pool, frame); + } +} + +// Fast activation for blocks +static inline void IoState_activateBlockFast_(IoState *state, IoEvalFrame *callerFrame, IoFramePool *pool) { + IoBlock *block = (IoBlock *)callerFrame->slotValue; + IoBlockData *blockData = (IoBlockData *)IoObject_dataPointer(block); + + IoObject *blockLocals = IOCLONE(state->localsProto); + IoObject_isLocals_(blockLocals, 1); + + IoObject *scope = blockData->scope ? blockData->scope : callerFrame->target; + + IoCall *callObject = IoCall_with( + state, callerFrame->locals, callerFrame->target, + callerFrame->message, callerFrame->slotContext, + block, state->currentCoroutine + ); + + IoObject_createSlotsIfNeeded(blockLocals); + PHash *bslots = IoObject_slots(blockLocals); + PHash_at_put_(bslots, state->callSymbol, callObject); + PHash_at_put_(bslots, state->selfSymbol, scope); + PHash_at_put_(bslots, state->updateSlotSymbol, state->localsUpdateSlotCFunc); + + List *argNames = blockData->argNames; + if (callerFrame->argValues) { + LIST_FOREACH(argNames, i, name, + if ((int)i < callerFrame->argCount) { + IoObject *arg = callerFrame->argValues[i]; + IoObject_setSlot_to_(blockLocals, name, arg); + }); + } + + IoObject_isReferenced_(blockLocals, 0); + IoObject_isReferenced_(callObject, 0); + + IoEvalFrame *blockFrame = IoState_pushFrameFast_(state, pool); + blockFrame->message = blockData->message; + blockFrame->target = blockLocals; + blockFrame->locals = blockLocals; + blockFrame->cachedTarget = blockLocals; + blockFrame->state = FRAME_STATE_START; + blockFrame->call = callObject; + blockFrame->blockLocals = blockLocals; + blockFrame->passStops = blockData->passStops; +} + +// Ultra-fast evaluation loop with computed gotos +IoObject *IoState_evalLoopFast_(IoState *state, IoFramePool *pool) { + IoEvalFrame *frame; + IoObject *result = state->ioNil; + + // Local cache to reduce indirection + IoMessage *m; + IoMessageData *md; + IoObject *target; + IoObject *slotValue; + IoObject *slotContext; + IoSymbol *messageName; + +#ifdef __GNUC__ + // Computed goto labels (GCC extension - much faster than switch) + static void *dispatch_table[] = { + &&STATE_START, + &&STATE_EVAL_ARGS, + &&STATE_LOOKUP_SLOT, + &&STATE_ACTIVATE, + &&STATE_CONTINUE_CHAIN, + &&STATE_RETURN + }; + + #define DISPATCH() goto *dispatch_table[frame->state] + #define CASE(label) label: +#else + // Fallback to switch for non-GCC compilers + #define DISPATCH() goto dispatch_switch; dispatch_switch: switch(frame->state) + #define CASE(label) case label: +#endif + + while ((frame = state->currentFrame) != NULL) { + if (state->receivedSignal) { + IoState_callUserInterruptHandler(state); + } + + if (state->stopStatus != MESSAGE_STOP_STATUS_NORMAL) { + // Handle stop status... + IoEvalFrame *f = frame; + while (f) { + if (f->blockLocals && !f->passStops) { + f->result = state->returnValue; + f->state = FRAME_STATE_RETURN; + state->stopStatus = MESSAGE_STOP_STATUS_NORMAL; + break; + } + f = f->parent; + } + continue; + } + + DISPATCH(); + + CASE(STATE_START) { + m = frame->message; + if (!m) { + frame->result = state->ioNil; + frame->state = FRAME_STATE_RETURN; + DISPATCH(); + } + + md = IOMESSAGEDATA(m); + + // FAST PATH: Semicolon + if (md->name == state->semicolonSymbol) { + frame->target = frame->cachedTarget; + frame->message = md->next; + if (md->next) { + frame->state = FRAME_STATE_START; + } else { + frame->result = frame->target; + frame->state = FRAME_STATE_RETURN; + } + DISPATCH(); + } + + // FAST PATH: Cached result (literals) + if (md->cachedResult) { + frame->result = md->cachedResult; + if (!md->next) { + frame->state = FRAME_STATE_RETURN; + } else { + frame->state = FRAME_STATE_CONTINUE_CHAIN; + } + DISPATCH(); + } + + // Need argument evaluation + frame->argCount = List_size(md->args); + frame->currentArgIndex = 0; + + if (frame->argCount > 0) { + frame->argValues = io_calloc(frame->argCount, sizeof(IoObject *)); + frame->state = FRAME_STATE_EVAL_ARGS; + } else { + frame->state = FRAME_STATE_LOOKUP_SLOT; + } + DISPATCH(); + } + + CASE(STATE_EVAL_ARGS) { + if (frame->currentArgIndex >= frame->argCount) { + frame->state = FRAME_STATE_LOOKUP_SLOT; + DISPATCH(); + } + + IoMessage *argMsg = List_at_(IOMESSAGEDATA(frame->message)->args, + frame->currentArgIndex); + + if (!argMsg) { + frame->argValues[frame->currentArgIndex] = state->ioNil; + frame->currentArgIndex++; + DISPATCH(); + } + + // FAST PATH: Cached argument + IoMessageData *argMd = IOMESSAGEDATA(argMsg); + if (argMd->cachedResult && !argMd->next) { + frame->argValues[frame->currentArgIndex] = argMd->cachedResult; + frame->currentArgIndex++; + DISPATCH(); + } + + // Need to evaluate - push frame + IoEvalFrame *argFrame = IoState_pushFrameFast_(state, pool); + argFrame->message = argMsg; + argFrame->target = frame->locals; + argFrame->locals = frame->locals; + argFrame->cachedTarget = frame->locals; + argFrame->state = FRAME_STATE_START; + DISPATCH(); + } + + CASE(STATE_LOOKUP_SLOT) { + messageName = IoMessage_name(frame->message); + slotValue = IoObject_rawGetSlot_context_(frame->target, messageName, &slotContext); + + if (slotValue) { + frame->slotValue = slotValue; + frame->slotContext = slotContext; + frame->state = FRAME_STATE_ACTIVATE; + } else { + if (IoObject_isLocals(frame->target)) { + frame->result = IoObject_localsForward(frame->target, frame->locals, frame->message); + } else { + frame->result = IoObject_forward(frame->target, frame->locals, frame->message); + } + frame->state = FRAME_STATE_CONTINUE_CHAIN; + } + DISPATCH(); + } + + CASE(STATE_ACTIVATE) { + slotValue = frame->slotValue; + + if (IoObject_isActivatable(slotValue)) { + if (ISBLOCK(slotValue)) { + IoState_activateBlockFast_(state, frame, pool); + DISPATCH(); + } else { + // CFunction + IoTagActivateFunc *activateFunc = IoObject_tag(slotValue)->activateFunc; + IoState_pushRetainPool(state); + frame->result = activateFunc(slotValue, frame->target, frame->locals, + frame->message, frame->slotContext); + IoState_popRetainPoolExceptFor_(state, frame->result); + frame->state = FRAME_STATE_CONTINUE_CHAIN; + } + } else { + frame->result = slotValue; + frame->state = FRAME_STATE_CONTINUE_CHAIN; + } + DISPATCH(); + } + + CASE(STATE_CONTINUE_CHAIN) { + IoMessage *next = IOMESSAGEDATA(frame->message)->next; + + if (next) { + frame->target = frame->result; + frame->message = next; + frame->state = FRAME_STATE_START; + + if (frame->argValues) { + io_free(frame->argValues); + frame->argValues = NULL; + } + frame->argCount = 0; + frame->currentArgIndex = 0; + } else { + frame->state = FRAME_STATE_RETURN; + } + DISPATCH(); + } + + CASE(STATE_RETURN) { + result = frame->result; + IoEvalFrame *parent = frame->parent; + + if (parent) { + if (parent->state == FRAME_STATE_EVAL_ARGS) { + parent->argValues[parent->currentArgIndex] = result; + parent->currentArgIndex++; + } else if (parent->state == FRAME_STATE_ACTIVATE) { + parent->result = result; + parent->state = FRAME_STATE_CONTINUE_CHAIN; + } + } + + IoState_popFrameFast_(state, pool); + DISPATCH(); + } + } + + return result; +} + +// Fast entry point +IoObject *IoMessage_locals_performOn_fast(IoMessage *self, IoObject *locals, IoObject *target) { + IoState *state = IOSTATE; + + // Initialize thread-local frame pool + static __thread IoFramePool pool; + static __thread int pool_initialized = 0; + if (!pool_initialized) { + IoFramePool_init(&pool); + pool_initialized = 1; + } + + // Push initial frame + IoEvalFrame *frame = IoState_pushFrameFast_(state, &pool); + frame->message = self; + frame->target = target; + frame->locals = locals; + frame->cachedTarget = target; + frame->state = FRAME_STATE_START; + + // Run optimized evaluation loop + IoObject *result = IoState_evalLoopFast_(state, &pool); + + return result; +} diff --git a/libs/iovm/source/IoState_symbols.c b/libs/iovm/source/IoState_symbols.c index 1904cc244..413c08cd9 100644 --- a/libs/iovm/source/IoState_symbols.c +++ b/libs/iovm/source/IoState_symbols.c @@ -8,7 +8,7 @@ #include "IoNumber.h" #define MIN_CACHED_NUMBER -10 -#define MAX_CACHED_NUMBER 256 +#define MAX_CACHED_NUMBER 1024 // numbers ---------------------------------- @@ -22,6 +22,12 @@ void IoState_setupCachedNumbers(IoState *self) { List_append_(self->cachedNumbers, number); IoState_retain_(self, number); } + + // Cache Number proto and tag for fast inline allocation + self->numberProto = IoState_protoWithId_(self, "Number"); + self->numberTag = IoObject_tag(self->numberProto); + self->numberDataFreeList = NULL; + self->numberDataFreeListSize = 0; } IoObject *IoState_numberWithDouble_(IoState *self, double n) { @@ -32,7 +38,40 @@ IoObject *IoState_numberWithDouble_(IoState *self, double n) { return List_at_(self->cachedNumbers, i - MIN_CACHED_NUMBER); } - return IoNumber_newWithDouble_(self, n); + // Inline Number allocation: bypass IOCLONE overhead + // (avoids pushCollectorPause/popCollectorPause which can trigger GC, + // tag function dispatch, double Collector_addValue_, redundant field setup) + + // 1. Get a CollectorMarker (recycled from freed list or fresh) + IoObject *child = Collector_newMarker(self->collector); + + // 2. Get data+protos block from freelist or allocate new + IoObjectData *data; + if (self->numberDataFreeList) { + data = (IoObjectData *)self->numberDataFreeList; + self->numberDataFreeList = data->data.ptr; + self->numberDataFreeListSize--; + // Zero for correctness (flags, listeners, etc. must be 0) + memset(data, 0, sizeof(IoObjectData) + 2 * sizeof(IoObject *)); + } else { + data = (IoObjectData *)io_calloc( + 1, sizeof(IoObjectData) + 2 * sizeof(IoObject *)); + } + + // 3. Set up the Number object + CollectorMarker_setObject_(child, data); + data->tag = self->numberTag; + data->data.d = n; + data->slots = IoObject_slots(self->numberProto); + // Protos array is inline (immediately after IoObjectData) + IoObject **protos = (IoObject **)(data + 1); + protos[0] = self->numberProto; + data->protos = protos; + + // 4. Stack retain for GC safety (Collector_newMarker already added to whites) + IoState_unreferencedStackRetain_(self, child); + + return child; } // strings ---------------------------------- diff --git a/libs/iovm/source/IoSystem.c b/libs/iovm/source/IoSystem.c index 6768c2365..67798faf0 100644 --- a/libs/iovm/source/IoSystem.c +++ b/libs/iovm/source/IoSystem.c @@ -13,7 +13,7 @@ Contains methods related to the IoVM. #include "IoInstallPrefix.h" #endif -#if defined(linux) || defined(__MINGW64__) +#if defined(linux) || defined(__MINGW64__) || defined(__APPLE__) #include #endif diff --git a/libs/iovm/source/IoVersion.h b/libs/iovm/source/IoVersion.h index 43b060a4d..f7f6cbecf 100644 --- a/libs/iovm/source/IoVersion.h +++ b/libs/iovm/source/IoVersion.h @@ -1,3 +1,3 @@ #ifndef IO_VERSION_STRING -#define IO_VERSION_STRING "20170906" +#define IO_VERSION_STRING "20260302" #endif diff --git a/libs/iovm/tests/benchmark_iterative.c b/libs/iovm/tests/benchmark_iterative.c new file mode 100644 index 000000000..a38e14ba6 --- /dev/null +++ b/libs/iovm/tests/benchmark_iterative.c @@ -0,0 +1,102 @@ + +// Benchmark comparing recursive vs iterative evaluators + +#include "IoState.h" +#include "IoObject.h" +#include "IoMessage.h" +#include "IoSeq.h" +#include "IoNumber.h" +#include "IoState_eval.h" +#include +#include + +#define ITERATIONS 100000 + +// Helper to parse code +IoMessage *parseCode(IoState *state, const char *code) { + IoSymbol *label = IoState_symbolWithCString_(state, "[benchmark]"); + return IoMessage_newFromText_labelSymbol_(state, (char *)code, label); +} + +// Benchmark recursive evaluator +double benchmarkRecursive(IoState *state, const char *code, int iterations) { + IoMessage *msg = parseCode(state, code); + clock_t start = clock(); + + for (int i = 0; i < iterations; i++) { + IoMessage_locals_performOn_(msg, state->lobby, state->lobby); + } + + clock_t end = clock(); + return ((double)(end - start)) / CLOCKS_PER_SEC; +} + +// Benchmark iterative evaluator +double benchmarkIterative(IoState *state, const char *code, int iterations) { + IoMessage *msg = parseCode(state, code); + clock_t start = clock(); + + for (int i = 0; i < iterations; i++) { + IoMessage_locals_performOn_iterative(msg, state->lobby, state->lobby); + } + + clock_t end = clock(); + return ((double)(end - start)) / CLOCKS_PER_SEC; +} + +void runBenchmark(IoState *state, const char *name, const char *code, int iterations) { + printf("\n%s:\n", name); + printf(" Code: %s\n", code); + + // Warm up + parseCode(state, code); + + // Benchmark recursive + double recursiveTime = benchmarkRecursive(state, code, iterations); + double recursiveOpsPerSec = iterations / recursiveTime; + + // Benchmark iterative (now with frame pooling!) + double iterativeTime = benchmarkIterative(state, code, iterations); + double iterativeOpsPerSec = iterations / iterativeTime; + + printf(" Recursive: %.3f sec (%.2f M ops/sec)\n", + recursiveTime, recursiveOpsPerSec / 1000000.0); + printf(" Pooled: %.3f sec (%.2f M ops/sec) - %.2fx overhead\n", + iterativeTime, iterativeOpsPerSec / 1000000.0, iterativeTime / recursiveTime); + + double speedup = recursiveOpsPerSec / iterativeOpsPerSec; + printf(" Target: Need %.2fx faster to match Ruby goal\n", speedup * 10.0); +} + +int main(int argc, char **argv) { + printf("=== Io Evaluator Performance Benchmark ===\n"); + printf("Iterations: %d\n", ITERATIONS); + + IoState *state = IoState_new(); + IoState_init(state); + + // Set up some test data + IoMessage *msg = parseCode(state, "x := 42; y := 100"); + IoMessage_locals_performOn_(msg, state->lobby, state->lobby); + + msg = parseCode(state, "add := method(a, b, a + b)"); + IoMessage_locals_performOn_(msg, state->lobby, state->lobby); + + // Run benchmarks + runBenchmark(state, "Local Access", "x", ITERATIONS); + runBenchmark(state, "Local Set", "x = 123", ITERATIONS); + runBenchmark(state, "Slot Access", "List", ITERATIONS); + runBenchmark(state, "Simple Message", "2 + 3", ITERATIONS); + runBenchmark(state, "Message Chain", "2 + 3 + 4", ITERATIONS); + runBenchmark(state, "Block Activation", "block(42) call", ITERATIONS); + runBenchmark(state, "Method Call", "add(10, 20)", ITERATIONS / 10); + + printf("\n=== Summary ===\n"); + printf("The iterative evaluator trades some performance for:\n"); + printf(" - No C stack dependence (enables continuations)\n"); + printf(" - Serializable execution state\n"); + printf(" - Better stack overflow handling\n"); + printf(" - Network-portable coroutines\n"); + + return 0; +} diff --git a/libs/iovm/tests/correctness/EvalFrameTest.io b/libs/iovm/tests/correctness/EvalFrameTest.io new file mode 100644 index 000000000..fb45b5e90 --- /dev/null +++ b/libs/iovm/tests/correctness/EvalFrameTest.io @@ -0,0 +1,84 @@ +// EvalFrame introspection tests — requires stackless branch (currentFrame method) +if(Coroutine getSlot("currentFrame") == nil, + EvalFrameTest := UnitTest clone +, + EvalFrameTest := UnitTest clone do( + testCurrentFrame := method( + f := Coroutine currentCoroutine currentFrame + assertNotNil(f) + assertTrue(f depth > 0) + ) + + testFrameState := method( + f := Coroutine currentCoroutine currentFrame + assertTrue(f state isKindOf(Sequence)) + ) + + testFrameParent := method( + f := Coroutine currentCoroutine currentFrame + // There should always be a parent frame (we're inside a method) + assertNotNil(f parent) + assertTrue(f parent != nil) + ) + + testFrameDepth := method( + f := Coroutine currentCoroutine currentFrame + d := f depth + assertTrue(d >= 3) // at least: doString, method activation, this chain + ) + + testFrameDescription := method( + f := Coroutine currentCoroutine currentFrame + desc := f description + assertTrue(desc isKindOf(Sequence)) + assertTrue(desc size > 0) + ) + + testFrameWalk := method( + // Walk the full frame stack and verify it terminates + f := Coroutine currentCoroutine currentFrame + count := 0 + while(f != nil, + count = count + 1 + f = f parent + ) + assertTrue(count > 0) + ) + + testFrameCallInMethod := method( + helper := method( + f := Coroutine currentCoroutine currentFrame + // Walk up to find the frame with a call object + while(f != nil, + if(f call != nil, return f call) + f = f parent + ) + return nil + ) + c := helper + assertNotNil(c) + assertEquals("Call", c type) + ) + + testFrameLocals := method( + f := Coroutine currentCoroutine currentFrame + // Walk to find a frame with non-nil locals + while(f != nil, + if(f locals != nil, break) + f = f parent + ) + assertNotNil(f) + assertNotNil(f locals) + ) + + testFrameTarget := method( + f := Coroutine currentCoroutine currentFrame + while(f != nil, + if(f target != nil, break) + f = f parent + ) + assertNotNil(f) + assertNotNil(f target) + ) + ) +) diff --git a/libs/iovm/tests/correctness/ResumableExceptionTest.io b/libs/iovm/tests/correctness/ResumableExceptionTest.io new file mode 100644 index 000000000..d12656001 --- /dev/null +++ b/libs/iovm/tests/correctness/ResumableExceptionTest.io @@ -0,0 +1,101 @@ +ResumableExceptionTest := UnitTest clone do( + testBasicSignalAndResume := method( + result := withHandler(Exception, + block(e, resume, resume invoke(42)), + Exception signal("need value") + 1 + ) + assertEquals(result, 43) + ) + + testAutoResume := method( + // Handler returns value without explicit resume invoke + result := withHandler(Exception, + block(e, resume, 42), + Exception signal("need value") + 1 + ) + assertEquals(result, 43) + ) + + testNoHandler := method( + // signal with no handler falls back to raise + e := try(Exception signal("unhandled")) + assertEquals(e error, "unhandled") + ) + + testHandlerReRaises := method( + e := try( + withHandler(Exception, + block(exc, resume, exc pass), + Exception signal("will re-raise") + ) + ) + assertEquals(e error, "will re-raise") + ) + + testCustomExceptionType := method( + MyError := Exception clone do(type := "MyError") + result := withHandler(MyError, + block(exc, resume, resume invoke("handled")), + MyError signal("custom error") + ) + assertEquals(result, "handled") + ) + + testHandlerMatchesByProto := method( + // Handler for OtherType doesn't match Exception signal + OtherType := Exception clone do(type := "OtherType") + result := withHandler(OtherType, + block(exc, resume, resume invoke("wrong handler")), + withHandler(Exception, + block(exc, resume, resume invoke("right handler")), + Exception signal("test") + ) + ) + assertEquals(result, "right handler") + ) + + testNestedHandlers := method( + // Inner handler takes priority + result := withHandler(Exception, + block(e, resume, resume invoke("outer")), + withHandler(Exception, + block(e, resume, resume invoke("inner")), + Exception signal("test") + ) + ) + assertEquals(result, "inner") + ) + + testMultipleSignals := method( + // Handler handles multiple signals from same body + count := 0 + result := withHandler(Exception, + block(e, resume, + count = count + 1 + resume invoke(count) + ), + a := Exception signal("first") + b := Exception signal("second") + list(a, b) + ) + assertEquals(result at(0), 1) + assertEquals(result at(1), 2) + ) + + testSignalInsideTry := method( + // Handler installed outside try is visible inside + result := withHandler(Exception, + block(e, resume, resume invoke(99)), + e := try(Exception signal("inside try")) + // Signal was handled (resumed), no exception + if(e, "exception", "no exception") + ) + assertEquals(result, "no exception") + ) + + testRaiseUnchanged := method( + // raise still works as before (non-resumable) + e := try(Exception raise("fatal")) + assertEquals(e error, "fatal") + ) +) diff --git a/libs/iovm/tests/debug_if.c b/libs/iovm/tests/debug_if.c new file mode 100644 index 000000000..bb93b09ca --- /dev/null +++ b/libs/iovm/tests/debug_if.c @@ -0,0 +1,46 @@ +// Debug test for 'if' primitive + +#include "IoState.h" +#include "IoObject.h" +#include "IoMessage.h" +#include "IoSeq.h" +#include "IoNumber.h" +#include "IoState_eval.h" +#include + +IoMessage *parseCode(IoState *state, const char *code) { + IoSymbol *label = IoState_symbolWithCString_(state, "[debug]"); + return IoMessage_newFromText_labelSymbol_(state, (char *)code, label); +} + +IoObject *evalCode(IoState *state, const char *code) { + IoMessage *msg = parseCode(state, code); + return IoMessage_locals_performOn_iterative(msg, state->lobby, state->lobby); +} + +int main(int argc, char **argv) { + printf("=== IF Primitive Debug Test ===\n"); + + IoState *state = IoState_new(); + IoState_init(state); + + // Enable debug output + state->showAllMessages = 1; + + printf("\n1. Setting counter to 0\n"); + evalCode(state, "counter := 0"); + + printf("\n2. Defining increment method\n"); + evalCode(state, "increment := method(counter = counter + 1)"); + + printf("\n3. Calling if(true, increment, increment)\n"); + evalCode(state, "if(true, increment, increment)"); + + printf("\n4. Getting counter value\n"); + IoObject *result = evalCode(state, "counter"); + + printf("\n=== Result ===\n"); + printf("counter = %d (expected 1)\n", ISNUMBER(result) ? IoNumber_asInt(result) : -999); + + return 0; +} diff --git a/libs/iovm/tests/test_fast.c b/libs/iovm/tests/test_fast.c new file mode 100644 index 000000000..acd9cab63 --- /dev/null +++ b/libs/iovm/tests/test_fast.c @@ -0,0 +1,31 @@ + +// Quick test of fast evaluator + +#include "IoState.h" +#include "IoObject.h" +#include "IoMessage.h" +#include "IoNumber.h" +#include "IoState_eval.h" +#include + +int main() { + printf("Testing fast evaluator...\n"); + + IoState *state = IoState_new(); + IoState_init(state); + + IoSymbol *label = IoState_symbolWithCString_(state, "[test]"); + IoMessage *msg = IoMessage_newFromText_labelSymbol_(state, "2 + 3", label); + + printf("Calling fast evaluator...\n"); + IoObject *result = IoMessage_locals_performOn_fast(msg, state->lobby, state->lobby); + + if (ISNUMBER(result)) { + printf("Result: %d\n", IoNumber_asInt(result)); + printf("SUCCESS!\n"); + return 0; + } else { + printf("FAILED - not a number\n"); + return 1; + } +} diff --git a/libs/iovm/tests/test_iterative_eval.c b/libs/iovm/tests/test_iterative_eval.c new file mode 100644 index 000000000..2202d7854 --- /dev/null +++ b/libs/iovm/tests/test_iterative_eval.c @@ -0,0 +1,826 @@ + +// Test file for iterative (non-recursive) evaluation + +#include "IoState.h" +#include "IoObject.h" +#include "IoMessage.h" +#include "IoSeq.h" +#include "IoNumber.h" +#include "IoList.h" +#include "IoState_eval.h" +#include +#include + +// Helper to create a test message from code +IoMessage *testParseCode(IoState *state, const char *code) { + IoSymbol *label = IoState_symbolWithCString_(state, "[test]"); + return IoMessage_newFromText_labelSymbol_(state, (char *)code, label); +} + +// Helper to evaluate code iteratively +IoObject *testEvalCode(IoState *state, const char *code) { + IoMessage *msg = testParseCode(state, code); + return IoMessage_locals_performOn_iterative(msg, state->lobby, state->lobby); +} + +// Test simple literal evaluation +int test_literal(IoState *state) { + printf("Test 1: Literal evaluation... "); + + IoObject *result = testEvalCode(state, "42"); + if (!ISNUMBER(result) || IoNumber_asInt(result) != 42) { + printf("FAILED\n"); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test simple message send +int test_message_send(IoState *state) { + printf("Test 2: Simple message send... "); + + IoObject *result = testEvalCode(state, "2 + 3"); + if (!ISNUMBER(result) || IoNumber_asInt(result) != 5) { + printf("FAILED (got %d)\n", ISNUMBER(result) ? IoNumber_asInt(result) : -1); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test message chain +int test_message_chain(IoState *state) { + printf("Test 3: Message chain... "); + + // Simple chain without operator precedence complexity + IoObject *result = testEvalCode(state, "2 + 3 + 4"); + if (!ISNUMBER(result) || IoNumber_asInt(result) != 9) { + printf("FAILED (got %d, expected 9)\n", + ISNUMBER(result) ? IoNumber_asInt(result) : -1); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test slot assignment +int test_slot_assignment(IoState *state) { + printf("Test 4: Slot assignment... "); + + testEvalCode(state, "testValue := 123"); + IoObject *result = testEvalCode(state, "testValue"); + + if (!ISNUMBER(result) || IoNumber_asInt(result) != 123) { + printf("FAILED\n"); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test block evaluation +int test_block_eval(IoState *state) { + printf("Test 5: Block evaluation... "); + + IoObject *result = testEvalCode(state, "block(42) call"); + if (!ISNUMBER(result) || IoNumber_asInt(result) != 42) { + printf("FAILED\n"); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test block with arguments +int test_block_args(IoState *state) { + printf("Test 6: Block with arguments... "); + + testEvalCode(state, "addFunc := method(a, b, a + b)"); + IoObject *result = testEvalCode(state, "addFunc(10, 20)"); + + if (!ISNUMBER(result) || IoNumber_asInt(result) != 30) { + printf("FAILED (got %d)\n", ISNUMBER(result) ? IoNumber_asInt(result) : -1); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test semicolon (statement separator) +int test_semicolon(IoState *state) { + printf("Test 7: Semicolon statement separator... "); + + testEvalCode(state, "x := 5"); + IoObject *result = testEvalCode(state, "x := 10; x := 20; x"); + + if (!ISNUMBER(result) || IoNumber_asInt(result) != 20) { + printf("FAILED\n"); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test nested blocks +int test_nested_blocks(IoState *state) { + printf("Test 8: Nested blocks... "); + + // Simpler nested block test + const char *code = "outer := method(x, x + 5); outer(10)"; + IoObject *result = testEvalCode(state, code); + + if (!ISNUMBER(result) || IoNumber_asInt(result) != 15) { + printf("FAILED (got %d, expected 15)\n", + ISNUMBER(result) ? IoNumber_asInt(result) : -1); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test lazy argument evaluation +int test_lazy_args(IoState *state) { + printf("Test 9: Lazy argument evaluation... "); + + // Disable verbose debug for this test + int oldShowAll = state->showAllMessages; + state->showAllMessages = 0; + + // if() is a special form that doesn't evaluate all arguments + printf("\n>>> Setting up counter and increment method\n"); + testEvalCode(state, "counter := 0"); + testEvalCode(state, "increment := method(counter = counter + 1)"); + + printf(">>> Calling if(true, increment, increment)\n"); + state->showAllMessages = 1; // Enable for just the if call + testEvalCode(state, "if(true, increment, increment)"); + state->showAllMessages = 0; + + printf(">>> Getting counter value\n"); + IoObject *result = testEvalCode(state, "counter"); + + // Restore debug setting + state->showAllMessages = oldShowAll; + + // Should only increment once (true branch) + if (!ISNUMBER(result) || IoNumber_asInt(result) != 1) { + printf("FAILED (counter = %d, expected 1)\n", + ISNUMBER(result) ? IoNumber_asInt(result) : -1); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test list operations +int test_list(IoState *state) { + printf("Test 10: List operations... "); + + testEvalCode(state, "myList := list(1, 2, 3)"); + IoObject *result = testEvalCode(state, "myList size"); + + if (!ISNUMBER(result) || IoNumber_asInt(result) != 3) { + printf("FAILED\n"); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +#ifdef IO_CALLCC +// Test callcc (call with current continuation) +int test_callcc_normal(IoState *state) { + printf("Test 11: callcc normal return... "); + + // callcc with a block that returns normally + // The result should be the block's return value + IoObject *result = testEvalCode(state, + "callcc(block(cont, \"normal return\"))"); + + if (!ISSEQ(result)) { + printf("FAILED (not a sequence)\n"); + return 0; + } + + if (strcmp(CSTRING(result), "normal return") != 0) { + printf("FAILED (got '%s', expected 'normal return')\n", CSTRING(result)); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test callcc with continuation invocation +int test_callcc_invoke(IoState *state) { + printf("Test 12: callcc continuation invoke... "); + + // callcc with a block that invokes the continuation + // The result should be the value passed to invoke + IoObject *result = testEvalCode(state, + "callcc(block(cont, cont invoke(42); 999))"); + + if (!ISNUMBER(result)) { + printf("FAILED (not a number, got type %s)\n", + IoObject_name(result)); + return 0; + } + + if (IoNumber_asInt(result) != 42) { + printf("FAILED (got %d, expected 42)\n", IoNumber_asInt(result)); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test continuation as escape mechanism +int test_callcc_escape(IoState *state) { + printf("Test 13: callcc escape pattern... "); + + // Use continuation to escape from nested evaluation + // The "normal" value should never be reached + IoObject *result = testEvalCode(state, + "callcc(block(escape, if(true, escape invoke(\"escaped\")); \"normal\"))"); + + if (!ISSEQ(result)) { + printf("FAILED (not a sequence, got type %s)\n", + IoObject_name(result)); + return 0; + } + + if (strcmp(CSTRING(result), "escaped") != 0) { + printf("FAILED (got '%s', expected 'escaped')\n", CSTRING(result)); + return 0; + } + + printf("PASSED\n"); + return 1; +} +#endif /* IO_CALLCC */ + +// Test try/catch with Exception raise +int test_try_catch(IoState *state) { + printf("Test 14: try/catch Exception raise... "); + + IoObject *result = testEvalCode(state, + "e := try(Exception raise(\"test\")); e error"); + + if (!ISSEQ(result)) { + printf("FAILED (not a sequence, got type %s)\n", + IoObject_name(result)); + return 0; + } + + if (strcmp(CSTRING(result), "test") != 0) { + printf("FAILED (got '%s', expected 'test')\n", CSTRING(result)); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test try with no exception +int test_try_no_exception(IoState *state) { + printf("Test 15: try with no exception... "); + + IoObject *result = testEvalCode(state, "try(1 + 1)"); + + if (result != state->ioNil) { + printf("FAILED (expected nil, got type %s)\n", + IoObject_name(result)); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test that execution continues after try captures exception +int test_try_continues(IoState *state) { + printf("Test 16: execution continues after try... "); + + IoObject *result = testEvalCode(state, + "e := try(Exception raise(\"boom\")); \"continued\""); + + if (!ISSEQ(result)) { + printf("FAILED (not a sequence, got type %s)\n", + IoObject_name(result)); + return 0; + } + + if (strcmp(CSTRING(result), "continued") != 0) { + printf("FAILED (got '%s', expected 'continued')\n", CSTRING(result)); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test exception pass (re-raise) +int test_exception_pass(IoState *state) { + printf("Test 17: exception pass... "); + + IoObject *result = testEvalCode(state, + "e := try(try(Exception raise(\"inner\")) pass); e error"); + + if (!ISSEQ(result)) { + printf("FAILED (not a sequence, got type %s)\n", + IoObject_name(result)); + return 0; + } + + if (strcmp(CSTRING(result), "inner") != 0) { + printf("FAILED (got '%s', expected 'inner')\n", CSTRING(result)); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test uncaught C-level exception (unknown method) +int test_uncaught_exception(IoState *state) { + printf("Test 18: uncaught exception (unknown method)... "); + + testEvalCode(state, "e := try(1 unknownMethod)"); + IoObject *result = testEvalCode(state, "e error"); + + if (!ISSEQ(result)) { + printf("FAILED (not a sequence, got type %s)\n", + IoObject_name(result)); + return 0; + } + + if (strstr(CSTRING(result), "does not respond to") == NULL) { + printf("FAILED (got '%s', expected to contain 'does not respond to')\n", + CSTRING(result)); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test basic coroutine resume +int test_coro_resume_basic(IoState *state) { + printf("Test 19: basic coroutine resume... "); + + // Create a coro, set it up to run code, resume it + // After the coro finishes, control should return to main + // Note: both runTarget and runLocals must be Lobby so that + // updateSlot (=) can find the coroResult slot defined on Lobby + IoObject *result = testEvalCode(state, + "coroResult := nil; " + "c := Coroutine clone; " + "c setRunTarget(Lobby); " + "c setRunLocals(Lobby); " + "c setRunMessage(message(coroResult = \"from coro\")); " + "c resume; " + "coroResult"); + + if (!ISSEQ(result)) { + printf("FAILED (not a sequence, got type %s)\n", + IoObject_name(result)); + return 0; + } + + if (strcmp(CSTRING(result), "from coro") != 0) { + printf("FAILED (got '%s', expected 'from coro')\n", CSTRING(result)); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test yield to queued coroutine +int test_coro_yield(IoState *state) { + printf("Test 20: yield to queued coroutine... "); + + // yield with no queued coros should return nil and continue + IoObject *result = testEvalCode(state, "yield; \"after yield\""); + + if (!ISSEQ(result)) { + printf("FAILED (not a sequence, got type %s)\n", + IoObject_name(result)); + return 0; + } + + if (strcmp(CSTRING(result), "after yield") != 0) { + printf("FAILED (got '%s', expected 'after yield')\n", CSTRING(result)); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test @@ async dispatch (actor pattern) +int test_coro_async(IoState *state) { + printf("Test 21: @@ async dispatch... "); + + // @@ creates a future and runs in an actor coroutine + // Use Lobby setSlot since the method's locals don't inherit from Lobby + IoObject *result = testEvalCode(state, + "asyncResult := nil; " + "o := Object clone; " + "o test := method(Lobby setSlot(\"asyncResult\", \"async done\")); " + "o @@test; yield; " + "asyncResult"); + + if (!ISSEQ(result)) { + printf("FAILED (not a sequence, got type %s)\n", + IoObject_name(result)); + return 0; + } + + if (strcmp(CSTRING(result), "async done") != 0) { + printf("FAILED (got '%s', expected 'async done')\n", CSTRING(result)); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test tail call optimization (direct tail recursion) +int test_tco_direct(IoState *state) { + printf("Test 22: TCO direct tail recursion... "); + + // Direct tail recursion: last expression in method body is a self-call + // Without TCO this would use 10000+ frames; with TCO it stays flat + testEvalCode(state, + "countdown := method(n, if(n <= 0, return n); countdown(n - 1))"); + IoObject *result = testEvalCode(state, "countdown(10000)"); + + if (!ISNUMBER(result) || IoNumber_asInt(result) != 0) { + printf("FAILED (got %d, expected 0)\n", + ISNUMBER(result) ? IoNumber_asInt(result) : -1); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test deep recursion through if (heap frames, no C stack overflow) +int test_deep_recursion(IoState *state) { + printf("Test 23: deep recursion through if... "); + + // This recursion goes through if branches, so TCO may not apply, + // but heap-allocated frames prevent C stack overflow + testEvalCode(state, + "sumTo := method(n, if(n <= 0, 0, n + sumTo(n - 1)))"); + IoObject *result = testEvalCode(state, "sumTo(100)"); + + // sum of 1..100 = 5050 + if (!ISNUMBER(result) || IoNumber_asInt(result) != 5050) { + printf("FAILED (got %d, expected 5050)\n", + ISNUMBER(result) ? IoNumber_asInt(result) : -1); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test tail-recursive accumulator pattern +int test_tco_accumulator(IoState *state) { + printf("Test 24: TCO accumulator pattern... "); + + // Tail-recursive sum with accumulator + // The recursive call is the last expression in the method + testEvalCode(state, + "sumAcc := method(n, acc, if(n <= 0, return acc); sumAcc(n - 1, acc + n))"); + IoObject *result = testEvalCode(state, "sumAcc(10000, 0)"); + + // sum of 1..10000 = 50005000 + if (!ISNUMBER(result)) { + printf("FAILED (not a number)\n"); + return 0; + } + + double expected = 50005000.0; + double got = IoNumber_asDouble(result); + if (got != expected) { + printf("FAILED (got %.0f, expected %.0f)\n", got, expected); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test TCO through if branches (the most important TCO pattern) +int test_tco_through_if(IoState *state) { + printf("Test 25: TCO through if branches... "); + + // Classic tail-recursive pattern: the recursive call is inside + // an if branch, which is the last expression in the method. + // This tests the tail position optimization in IF_EVAL_BRANCH. + testEvalCode(state, + "factorial := method(n, acc, if(n <= 1, acc, factorial(n - 1, n * acc)))"); + IoObject *result = testEvalCode(state, "factorial(20, 1)"); + + if (!ISNUMBER(result)) { + printf("FAILED (not a number)\n"); + return 0; + } + + // 20! = 2432902008176640000 (fits in double) + double expected = 2432902008176640000.0; + double got = IoNumber_asDouble(result); + if (got != expected) { + printf("FAILED (got %.0f, expected %.0f)\n", got, expected); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test TCO through if with large recursion depth +int test_tco_if_deep(IoState *state) { + printf("Test 26: TCO through if (deep recursion)... "); + + // Without TCO through if, this would use 100000+ frames. + // With TCO through if, the frame stack stays bounded. + testEvalCode(state, + "countDown := method(n, acc, if(n <= 0, acc, countDown(n - 1, acc + 1)))"); + IoObject *result = testEvalCode(state, "countDown(100000, 0)"); + + if (!ISNUMBER(result)) { + printf("FAILED (not a number)\n"); + return 0; + } + + double expected = 100000.0; + double got = IoNumber_asDouble(result); + if (got != expected) { + printf("FAILED (got %.0f, expected %.0f)\n", got, expected); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +#ifdef IO_CALLCC +// Test continuation introspection methods +int test_continuation_introspection(IoState *state) { + printf("Test 27: Continuation introspection... "); + + // Capture a continuation and inspect it. + // Use copy to snapshot the frame chain while it's live — + // with grab-pointer capture, the original frame evolves after callcc returns. + testEvalCode(state, + "captured := nil\n" + "callcc(block(cont, captured = cont copy))"); + + IoObject *frameCount = testEvalCode(state, "captured frameCount"); + if (!ISNUMBER(frameCount) || IoNumber_asInt(frameCount) < 1) { + printf("FAILED (frameCount=%d, expected >= 1)\n", + ISNUMBER(frameCount) ? (int)IoNumber_asInt(frameCount) : -1); + return 0; + } + + IoObject *states = testEvalCode(state, "captured frameStates"); + if (!ISLIST(states)) { + printf("FAILED (frameStates not a list)\n"); + return 0; + } + + IoObject *messages = testEvalCode(state, "captured frameMessages"); + if (!ISLIST(messages)) { + printf("FAILED (frameMessages not a list)\n"); + return 0; + } + + // First frame state should be callcc:evalBlock + IoObject *firstState = testEvalCode(state, "captured frameStates first"); + if (!ISSEQ(firstState)) { + printf("FAILED (first state not a string)\n"); + return 0; + } + const char *stateName = CSTRING(firstState); + if (strcmp(stateName, "callcc:evalBlock") != 0) { + printf("FAILED (first state='%s', expected 'callcc:evalBlock')\n", stateName); + return 0; + } + + printf("PASSED\n"); + return 1; +} +#endif /* IO_CALLCC */ + +// Test TCO through if with relayStopStatus (? operator) +int test_tco_if_stop_status(IoState *state) { + printf("Test 28: TCO through if + stop status (? operator)... "); + + // This tests the savedCall mechanism: the ? operator uses + // relayStopStatus which sets Call stop status. With the in-place + // if optimization, TCO could replace frame->call and lose the + // stop status. The savedCall field preserves it. + // + // Pattern from ObjectTest: x ?return "first" sends return to x (nil), + // which triggers RETURN stop status. The ? method must propagate + // this via relayStopStatus. + testEvalCode(state, + "a := method(x, x return \"first\"; \"second\")\n" + "b := method(x, x ?return \"first\"; \"second\")"); + + IoObject *resultA = testEvalCode(state, "a"); + IoObject *resultB = testEvalCode(state, "b"); + + if (!ISSEQ(resultA)) { + printf("FAILED (a not a string, got %s)\n", IoObject_name(resultA)); + return 0; + } + if (strcmp(CSTRING(resultA), "first") != 0) { + printf("FAILED (a='%s', expected 'first')\n", CSTRING(resultA)); + return 0; + } + if (!ISSEQ(resultB)) { + printf("FAILED (b not a string, got %s)\n", IoObject_name(resultB)); + return 0; + } + if (strcmp(CSTRING(resultA), CSTRING(resultB)) != 0) { + printf("FAILED (a='%s' != b='%s')\n", CSTRING(resultA), CSTRING(resultB)); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +// Test ? operator with continue in foreach (stop status propagation) +int test_question_mark_continue(IoState *state) { + printf("Test 29: ? operator with continue (stop status)... "); + + // From ObjectTest: ?continue in foreach should behave like continue + testEvalCode(state, + "a := method(\n" + " r := list\n" + " list(1,2,3,4,5) foreach(x,\n" + " if(x > 3, continue)\n" + " r append(x)\n" + " )\n" + " r\n" + ")\n" + "b := method(\n" + " r := list\n" + " list(1,2,3,4,5) foreach(x,\n" + " if(x > 3, ?continue)\n" + " r append(x)\n" + " )\n" + " r\n" + ")"); + + IoObject *resultA = testEvalCode(state, "a"); + IoObject *resultB = testEvalCode(state, "b"); + + if (!ISLIST(resultA) || !ISLIST(resultB)) { + printf("FAILED (not lists)\n"); + return 0; + } + + // Both should be list(1, 2, 3) - items <= 3 + IoObject *sizeA = testEvalCode(state, "a size"); + IoObject *sizeB = testEvalCode(state, "b size"); + if (IoNumber_asInt(sizeA) != 3 || IoNumber_asInt(sizeB) != 3) { + printf("FAILED (sizes: a=%d, b=%d, expected 3)\n", + (int)IoNumber_asInt(sizeA), (int)IoNumber_asInt(sizeB)); + return 0; + } + + printf("PASSED\n"); + return 1; +} + +#ifdef IO_CALLCC +// Test Continuation asMap serialization +int test_continuation_asMap(IoState *state) { + printf("Test 30: Continuation asMap... "); + + // Capture a continuation inside a for loop. + // Use copy to snapshot while live (grab-pointer frames evolve after return). + testEvalCode(state, + "captured2 := nil\n" + "for(i, 1, 5, if(i == 3, callcc(block(cont, captured2 = cont copy))))"); + + + // Check asMap returns a Map + IoObject *map = testEvalCode(state, "captured2 asMap"); + if (!IoObject_slots(map)) { + printf("FAILED (asMap didn't return a Map)\n"); + return 0; + } + + // Check frameCount + IoObject *fc = testEvalCode(state, "captured2 asMap at(\"frameCount\")"); + if (!ISNUMBER(fc) || IoNumber_asInt(fc) < 1) { + printf("FAILED (frameCount < 1)\n"); + return 0; + } + + // Check first frame is callcc:evalBlock + IoObject *firstState = testEvalCode(state, + "captured2 asMap at(\"frames\") first at(\"state\")"); + if (!ISSEQ(firstState) || + strcmp(CSTRING(firstState), "callcc:evalBlock") != 0) { + printf("FAILED (first frame state != callcc:evalBlock)\n"); + return 0; + } + + // Check for loop state was captured (currentValue should be 3) + IoObject *forValue = testEvalCode(state, + "captured2 asMap at(\"frames\") detect(at(\"state\") beginsWithSeq(\"for\")) at(\"currentValue\")"); + if (!ISNUMBER(forValue) || IoNumber_asDouble(forValue) != 3.0) { + printf("FAILED (for currentValue != 3, got %s)\n", + ISNUMBER(forValue) ? "wrong number" : IoObject_name(forValue)); + return 0; + } + + printf("PASSED\n"); + return 1; +} +#endif /* IO_CALLCC */ + +int main(int argc, char **argv) { + printf("=== Iterative Evaluator Tests ===\n\n"); + + // Initialize Io state + IoState *state = IoState_new(); + IoState_init(state); + + int passed = 0; + int total = 0; + + // Enable debug output for lazy args test + int enableDebug = (argc > 1 && strcmp(argv[1], "-debug") == 0); + + // Run tests + total++; passed += test_literal(state); + total++; passed += test_message_send(state); + total++; passed += test_message_chain(state); + total++; passed += test_slot_assignment(state); + total++; passed += test_block_eval(state); + total++; passed += test_block_args(state); + total++; passed += test_semicolon(state); + total++; passed += test_nested_blocks(state); + total++; passed += test_lazy_args(state); + total++; passed += test_list(state); +#ifdef IO_CALLCC + total++; passed += test_callcc_normal(state); + total++; passed += test_callcc_invoke(state); + total++; passed += test_callcc_escape(state); +#endif + total++; passed += test_try_catch(state); + total++; passed += test_try_no_exception(state); + total++; passed += test_try_continues(state); + total++; passed += test_exception_pass(state); + total++; passed += test_uncaught_exception(state); + total++; passed += test_coro_resume_basic(state); + total++; passed += test_coro_yield(state); + total++; passed += test_coro_async(state); + total++; passed += test_tco_direct(state); + total++; passed += test_deep_recursion(state); + total++; passed += test_tco_accumulator(state); + total++; passed += test_tco_through_if(state); + total++; passed += test_tco_if_deep(state); +#ifdef IO_CALLCC + total++; passed += test_continuation_introspection(state); +#endif + total++; passed += test_tco_if_stop_status(state); + total++; passed += test_question_mark_continue(state); +#ifdef IO_CALLCC + total++; passed += test_continuation_asMap(state); +#endif + + // Print summary + printf("\n=== Results ===\n"); + printf("Passed: %d/%d\n", passed, total); + + if (passed == total) { + printf("\nAll tests PASSED!\n"); + return 0; + } else { + printf("\nSome tests FAILED.\n"); + return 1; + } +} diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 24fc49ede..746fd7936 100755 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -26,7 +26,6 @@ set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/_build/binaries) # Include locations include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/../libs/basekit/source - ${CMAKE_CURRENT_SOURCE_DIR}/../libs/coroutine/source ${CMAKE_CURRENT_SOURCE_DIR}/../libs/garbagecollector/source ${CMAKE_CURRENT_SOURCE_DIR}/../libs/iovm/source ) @@ -62,8 +61,8 @@ target_link_libraries(io_static iovmall_static ${LIBS}) add_executable(io ${SRCS}) -add_dependencies(io basekit coroutine garbagecollector iovmall) -target_link_libraries(io basekit coroutine garbagecollector iovmall ${LIBS}) +add_dependencies(io basekit garbagecollector iovmall) +target_link_libraries(io basekit garbagecollector iovmall ${LIBS}) # The following add the install target, so we put io and io_static in our # install prefix. diff --git a/tools/source/main.c b/tools/source/main.c index 0ff38639a..08addc41f 100755 --- a/tools/source/main.c +++ b/tools/source/main.c @@ -1,4 +1,5 @@ #include "IoState.h" +#include void IoAddonsInit(IoObject *context); @@ -33,7 +34,6 @@ int main(int argc, const char *argv[]) size_t sweepCount; #endif - self = IoState_new(); #ifdef IOBINDINGS IoState_setBindingsInitCallback(self, (IoStateBindingsInitCallback *)IoAddonsInit);