From 98408a0a87a972da7b9c96613b7c05ba443480d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Pim=C3=A1s?= Date: Tue, 5 May 2026 00:39:00 -0300 Subject: [PATCH 01/11] [kernel/process] use ProcessVMStack with explicit buffer init Process>>initializeStack now creates a ProcessVMStack instead of a plain ProcessStack and instances are built via `ProcessVMStack on:` so the per-process VM buffer is set up at the right time. Split basicInitialize into initializeWithNewBuffer and initializeWithActiveBuffer; pick one in ProcessVMStack>>process: based on the new isActive predicate on Process/ActiveProcess. ProcessStack now keeps the last environment in a dedicated `env` instvar instead of stuffing it past the top of the stack. Context>>copyAllTo: walks `caller` (not the obsolete `parent`). --- modules/Kernel/ActiveProcess.st | 5 +++++ modules/Kernel/Context.st | 4 ++-- modules/Kernel/Process.st | 7 ++++++- modules/Kernel/ProcessStack.st | 17 ++++++++++++++--- modules/Kernel/ProcessVMStack.st | 28 ++++++++++++++++++++++------ 5 files changed, 49 insertions(+), 12 deletions(-) diff --git a/modules/Kernel/ActiveProcess.st b/modules/Kernel/ActiveProcess.st index 8373bf6c..e986227d 100644 --- a/modules/Kernel/ActiveProcess.st +++ b/modules/Kernel/ActiveProcess.st @@ -19,6 +19,11 @@ ActiveProcess >> beInactive [ self changeClassTo: SuspendedProcess ] +{ #category : #testing } +ActiveProcess >> isActive [ + ^true +] + { #category : #converting } ActiveProcess >> canReturnTo: homeFrame [ diff --git a/modules/Kernel/Context.st b/modules/Kernel/Context.st index 75e3e353..cbcc63ad 100644 --- a/modules/Kernel/Context.st +++ b/modules/Kernel/Context.st @@ -66,8 +66,8 @@ Context >> code: anExecutableCode method: aCompiledMethod [ { #category : #services } Context >> copyAllTo: aProcessStack [ - | start offset | - start := parent copyAllTo: aProcessStack. + | start | + start := caller copyAllTo: aProcessStack. ^self copyTo: aProcessStack at: start ] diff --git a/modules/Kernel/Process.st b/modules/Kernel/Process.st index b074ae90..dda1b57f 100644 --- a/modules/Kernel/Process.st +++ b/modules/Kernel/Process.st @@ -84,7 +84,12 @@ Process >> findHandlerFor: anException [ { #category : #private } Process >> initializeStack [ - nativeStack := ProcessStack on: self + nativeStack := ProcessVMStack on: self +] + +{ #category : #testing } +Process >> isActive [ + ^false ] { #category : #private } diff --git a/modules/Kernel/ProcessStack.st b/modules/Kernel/ProcessStack.st index 76ed70c4..32e6a66e 100644 --- a/modules/Kernel/ProcessStack.st +++ b/modules/Kernel/ProcessStack.st @@ -9,7 +9,8 @@ Class { #instVars : [ 'process', 'sp', - 'bp' + 'bp', + 'env' ], #category : #Kernel } @@ -35,8 +36,18 @@ ProcessStack >> contextSwitchTo: next [ { #category : #services } ProcessStack >> fillFrom: aContext [ #CRITICAL. - sp := (aContext copyAllTo: self). - self at: sp put: aContext lastEnvironment + sp := aContext copyAllTo: self. + env := aContext lastEnvironment +] + +{ #category : #accessing } +ProcessStack >> env [ + ^env +] + +{ #category : #accessing } +ProcessStack >> env: anObject [ + env := anObject ] { #category : #accessing } diff --git a/modules/Kernel/ProcessVMStack.st b/modules/Kernel/ProcessVMStack.st index 67bb144f..3080a282 100644 --- a/modules/Kernel/ProcessVMStack.st +++ b/modules/Kernel/ProcessVMStack.st @@ -6,9 +6,18 @@ Class { #name : #ProcessVMStack, #superclass : #ProcessStack, + #instVars : [ + 'buffer', + 'bufferSize' + ], #category : #Kernel } +{ #category : 'instance creation' } +ProcessVMStack class >> on: aProcess [ + ^self new process: aProcess +] + { #category : 'Primitives' } ProcessVMStack >> at: anInteger [ @@ -20,8 +29,13 @@ ProcessVMStack >> at: anInteger put: anObject [ ] { #category : 'Primitives' } -ProcessVMStack >> basicInitialize [ - +ProcessVMStack >> initializeWithNewBuffer [ + +] + +{ #category : 'Primitives' } +ProcessVMStack >> initializeWithActiveBuffer [ + ] { #category : #primitives } @@ -49,10 +63,12 @@ ProcessVMStack >> contextSwitchTo: aProcessStack [ ] -{ #category : 'initializing' } -ProcessVMStack >> initialize [ - super initialize. - self basicInitialize +{ #category : 'accessing' } +ProcessVMStack >> process: aProcess [ + super process: aProcess. + aProcess isActive + ifTrue: [self initializeWithActiveBuffer] + ifFalse: [self initializeWithNewBuffer] ] { #category : 'Primitives' } From aa3a404cd31f6d40ffe481b6548867c50474df2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Pim=C3=A1s?= Date: Tue, 5 May 2026 00:58:26 -0300 Subject: [PATCH 02/11] [runtime/process] back ProcessVMStack with explicit per-process buffer Mirrors the kernel-side split between ProcessStack ivars (process/sp/bp/env) and the ProcessVMStack-only buffer + bufferSize. KnownConstants offsets and the Runtime accessors are renamed accordingly (processStack*_ for the shared ivars, processVMStack{Buffer,BufferSize}_ for the rest). EvaluationContext now tracks the size of its native stack and exposes bindToBuffer_size_/stackPointer_/environment_ so context switches can swap buffers in and out. Implements the previously stubbed-out primitives: ProcessVMStackInitializeWithNewBuffer / WithActiveBuffer, ProcessVMStackBufferSize/AtPut/BpAtPut/PcAtPut, ProcessVMStackContextSwitchTo, PrepareForExecution (no-op in this runtime). GarbageCollector: replace the placeholder scanFirstStackChunk_ with scanStack_sp_bp_ + scanSuspendedProcessStack_, and skip a suspended stack when its buffer is the one currently bound to the live evaluator context (already walked by scanCurrentContext). --- runtime/cpp/Allocator/GarbageCollector.cpp | 56 ++++---- runtime/cpp/Allocator/GarbageCollector.h | 3 +- runtime/cpp/Evaluator/EvaluationContext.cpp | 2 + runtime/cpp/Evaluator/EvaluationContext.h | 9 ++ runtime/cpp/Evaluator/Evaluator.cpp | 137 +++++++++++++++++++- runtime/cpp/Evaluator/Evaluator.h | 8 ++ runtime/cpp/Evaluator/Runtime.h | 53 +++++++- runtime/cpp/KnownConstants.h | 11 +- 8 files changed, 228 insertions(+), 51 deletions(-) diff --git a/runtime/cpp/Allocator/GarbageCollector.cpp b/runtime/cpp/Allocator/GarbageCollector.cpp index e1f018b0..9a452676 100644 --- a/runtime/cpp/Allocator/GarbageCollector.cpp +++ b/runtime/cpp/Allocator/GarbageCollector.cpp @@ -192,30 +192,23 @@ void GarbageCollector::nativeFramesStartingAt_bp_do_(uintptr_t **stack, uintptr_ } } -void GarbageCollector::scanFirstStackChunk_(HeapObject *aProcessVMStack) { - /** - * Scanning needs to fetch a chain of stack (frame) pointers. The head - * of the chain is either of two cases: - * - The active process. - * In that case, the top of the stack is a common frame (probably a call - * to a primitive). No special action needs to be done. - * - A sleeping (native) process. - * In that case, the top of the stack is that process' env, followed - * by a retaddr. The GC has to scan that addr and then continue normally. - * (TODO: make env an instvar of the ProcessVMStack) - */ - //if (aProcessVMStack != runtime->_activeProcessStack) - // this->scanTopSlot_(aProcessVMStack); - - auto firstSP = _runtime->processVMStackSP_(aProcessVMStack) + 2; - auto firstBP = _runtime->processVMStackBP_(aProcessVMStack); - auto stack = (uintptr_t**)nullptr; //_runtime->processVMStackContext_(aProcessVMStack)->stack(); - this->nativeFramesStartingAt_bp_do_(stack, firstSP, firstBP, +void GarbageCollector::scanStack_sp_bp_(uintptr_t **stack, uintptr_t sp, uintptr_t bp) { + this->nativeFramesStartingAt_bp_do_(stack, sp, bp, [this](uintptr_t *frame, uintptr_t size) { this->scanNativeStackFrame_sized_(frame, size); }); } +void GarbageCollector::scanSuspendedProcessStack_(HeapObject *aProcessVMStack) { + // A suspended process' topmost frame was laid out by ProcessStack>>fillFrom:. + // `sp` ivar is the slot just below the saved-bp slot of that topmost frame + // Start the frame walk at sp+1 (saved-bp slot) up to bp. + auto firstSP = _runtime->processStackSP_(aProcessVMStack) + 1; + auto firstBP = _runtime->processStackBP_(aProcessVMStack); + auto stack = (uintptr_t**)_runtime->processVMStackBuffer_(aProcessVMStack); + this->scanStack_sp_bp_(stack, firstSP, firstBP); +} + void GarbageCollector::scanPointer_(Object** pointer) { this->scanRoot_(pointer); @@ -230,27 +223,24 @@ void GarbageCollector::scanCurrentContext() { this->scanPointer_((Object**)&context->_regE); this->scanPointer_((Object**)&context->_regM); - - auto firstSP = context->stackPointer(); - auto firstBP = context->framePointer(); - auto stack = (uintptr_t**)context->stack(); - this->nativeFramesStartingAt_bp_do_(stack, firstSP, firstBP, - [this](uintptr_t *frame, uintptr_t size) { - this->scanNativeStackFrame_sized_(frame, size); - }); + this->scanStack_sp_bp_( + (uintptr_t**)context->stack(), + context->stackPointer(), + context->framePointer()); } void GarbageCollector::scanStack_(HeapObject *aProcessVMStack) { - //auto context = _runtime->processVMStackContext_(aProcessVMStack); - - // skip this stack if it corresponds to active process, which has already been scanned - //if (context == _runtime->_evaluator->context()) + // Skip this stack if it corresponds to the active process: its buffer is + // the one bound to the live evaluator context, which scanCurrentContext + // has already walked using the live SP/BP/regs. + auto buffer = _runtime->processVMStackBuffer_(aProcessVMStack); + if (buffer == _runtime->_evaluator->context()->stack()) return; - auto process = _runtime->processVMStackProcess_(aProcessVMStack); + auto process = _runtime->processStackProcess_(aProcessVMStack); if (_runtime->processStackIsValid_(process)) - this->scanFirstStackChunk_(aProcessVMStack); + this->scanSuspendedProcessStack_(aProcessVMStack); /* unimplemented GC in callbacks this->stackFramesBeneathCallbackIn_Do_(aProcessVMStack, diff --git a/runtime/cpp/Allocator/GarbageCollector.h b/runtime/cpp/Allocator/GarbageCollector.h index 2400c6dc..9441ea16 100644 --- a/runtime/cpp/Allocator/GarbageCollector.h +++ b/runtime/cpp/Allocator/GarbageCollector.h @@ -57,7 +57,8 @@ class GarbageCollector { void scanSpecialSlots_(HeapObject *special); void nativeFramesStartingAt_bp_do_(uintptr_t **stack, uintptr_t sp, uintptr_t bp, std::function block); - void scanFirstStackChunk_(HeapObject * aProcessVMStack); + void scanStack_sp_bp_(uintptr_t **stack, uintptr_t sp, uintptr_t bp); + void scanSuspendedProcessStack_(HeapObject * aProcessVMStack); void scanPointer_(Object **pointer); void scanCurrentContext(); void scanStack_(HeapObject *aProcessVMStack); diff --git a/runtime/cpp/Evaluator/EvaluationContext.cpp b/runtime/cpp/Evaluator/EvaluationContext.cpp index 34d89ac6..9115dc2d 100644 --- a/runtime/cpp/Evaluator/EvaluationContext.cpp +++ b/runtime/cpp/Evaluator/EvaluationContext.cpp @@ -2,6 +2,7 @@ #include "EvaluationContext.h" #include +#include #include #include "Runtime.h" @@ -24,6 +25,7 @@ EvaluationContext::EvaluationContext(Runtime *runtime) : _runtime(runtime) _regBP = _regPC = 0; _regS = nullptr; _stack = new Object*[STACK_SIZE]; + _stackSize = STACK_SIZE; } HeapObject* EvaluationContext::classBinding() diff --git a/runtime/cpp/Evaluator/EvaluationContext.h b/runtime/cpp/Evaluator/EvaluationContext.h index 765b750b..9b3b7dcb 100644 --- a/runtime/cpp/Evaluator/EvaluationContext.h +++ b/runtime/cpp/Evaluator/EvaluationContext.h @@ -23,6 +23,7 @@ class EvaluationContext { HeapObject *_regM, *_regE; uintptr_t _regSP, _regBP, _regPC; Object *_regS, **_stack; + uintptr_t _stackSize; Runtime *_runtime; const int FRAME_TO_RECEIVER_DELTA = 1; @@ -42,12 +43,20 @@ class EvaluationContext { Object* self() { return _regS; } HeapObject* environment() { return _regE; } + void environment_(HeapObject* env) { _regE = env; } HeapObject* compiledCode() { return _regM; } uintptr_t stackPointer() { return _regSP; } + void stackPointer_(uintptr_t sp) { _regSP = sp; } uintptr_t framePointer() { return _regBP; } void framePointer_(uintptr_t bp) { _regBP = bp; } Object** stack() { return _stack; } + uintptr_t stackSize() { return _stackSize; } + + void bindToBuffer_size_(Object **buffer, uintptr_t size) { + _stack = buffer; + _stackSize = size; + } HeapObject* classBinding(); diff --git a/runtime/cpp/Evaluator/Evaluator.cpp b/runtime/cpp/Evaluator/Evaluator.cpp index dbee7d13..06ff890e 100644 --- a/runtime/cpp/Evaluator/Evaluator.cpp +++ b/runtime/cpp/Evaluator/Evaluator.cpp @@ -194,15 +194,16 @@ void Evaluator::initializePrimitives() this->addPrimitive("HostLoadModuleFromPath", &Evaluator::primitiveHostLoadModuleFromPath); - //this->addPrimitive("PrepareForExecution", &Evaluator::primitivePrepareForExecution); - //this->addPrimitive("ProcessVMStackInitialize", &Evaluator::primitiveProcessVMStackInitialize); + this->addPrimitive("PrepareForExecution", &Evaluator::primitivePrepareForExecution); + this->addPrimitive("ProcessVMStackInitializeWithNewBuffer", &Evaluator::primitiveProcessVMStackInitializeWithNewBuffer); + this->addPrimitive("ProcessVMStackInitializeWithActiveBuffer", &Evaluator::primitiveProcessVMStackInitializeWithActiveBuffer); this->addPrimitive("ProcessVMStackAt", &Evaluator::primitiveProcessStackAt); - //this->addPrimitive("ProcessVMStackAtPut", &Evaluator::primitiveProcessVMStackAtPut); - //this->addPrimitive("ProcessVMStackBpAtPut", &Evaluator::primitiveProcessVMStackBpAtPut); - //this->addPrimitive("ProcessVMStackPcAtPut", &Evaluator::primitiveProcessVMStackPcAtPut); + this->addPrimitive("ProcessVMStackAtPut", &Evaluator::primitiveProcessVMStackAtPut); + this->addPrimitive("ProcessVMStackBpAtPut", &Evaluator::primitiveProcessVMStackBpAtPut); + this->addPrimitive("ProcessVMStackPcAtPut", &Evaluator::primitiveProcessVMStackPcAtPut); this->addPrimitive("ProcessVMStackBP", &Evaluator::primitiveProcessBP); - //this->addPrimitive("ProcessVMStackBufferSize", &Evaluator::primitiveProcessVMStackBufferSize); - //this->addPrimitive("ProcessVMStackContextSwitchTo", &Evaluator::primitiveProcessVMStackContextSwitchTo); + this->addPrimitive("ProcessVMStackBufferSize", &Evaluator::primitiveProcessVMStackBufferSize); + this->addPrimitive("ProcessVMStackContextSwitchTo", &Evaluator::primitiveProcessVMStackContextSwitchTo); _linearizer->primitives_(_primitives); } @@ -1010,6 +1011,128 @@ Object* Evaluator::primitiveProcessStackAt() return _context->stackAt_(this->_context->firstArgument()->asSmallInteger()->asNative()); } +Object* Evaluator::primitiveProcessVMStackInitializeWithNewBuffer() +{ + // Allocate the off-heap buffer that backs a (suspended) process's stack and + // stash it (tagged as a SmallInteger so the GC won't try to follow it) into + // the receiver. + // TODO: this buffer leaks when the wrapping ProcessVMStack is GC'd. + // Wiring a real finalizer requires GC-sweep hooks (see GarbageCollector:: + // rememberSpecial_ for the special-class machinery). For now the leak is + // bounded by the number of Process objects ever created (1 OS-page-sized + // buffer each) and reclaimed by the OS on exit. + auto pvm = this->_context->self()->asHeapObject(); + uintptr_t size = this->_context->stackSize(); + Object **buffer = new Object*[size]; + for (uintptr_t i = 0; i < size; ++i) buffer[i] = (Object*)_runtime->_nilObj; + _runtime->processVMStackBuffer_put_(pvm, buffer); + _runtime->processVMStackBufferSize_put_(pvm, size); + return (Object*)pvm; +} + +Object* Evaluator::primitiveProcessVMStackInitializeWithActiveBuffer() +{ + // Adopt the live C++ evaluator stack: the receiver becomes the wrapper + // around the buffer that the running native code is currently using. + // SP/BP/env are written by the suspending side (contextSwitchTo:) so we + // don't initialize them here. The GC distinguishes the active stack from + // suspended ones by comparing buffer pointers with the evaluator context. + auto pvm = this->_context->self()->asHeapObject(); + auto ctx = this->_context; + _runtime->processVMStackBuffer_put_(pvm, ctx->stack()); + _runtime->processVMStackBufferSize_put_(pvm, ctx->stackSize()); + return (Object*)pvm; +} + +Object* Evaluator::primitiveProcessVMStackBufferSize() +{ + auto receiver = this->_context->self()->asHeapObject(); + return receiver->slot(Offsets::ProcessVMStackBufferSize); +} + +Object* Evaluator::primitiveProcessVMStackAtPut() +{ + auto receiver = this->_context->self()->asHeapObject(); + auto buffer = this->_runtime->processVMStackBuffer_(receiver); + if (!buffer) return failPrimitive(); + auto index = this->_context->firstArgument()->asSmallInteger()->asNative(); + auto value = this->_context->secondArgument(); + auto size = this->_runtime->processVMStackBufferSize_(receiver); + if (index < 1 || (uintptr_t)index > size) return failPrimitive(); + buffer[index - 1] = value; + return value; +} + +Object* Evaluator::primitiveProcessVMStackBpAtPut() +{ + auto receiver = this->_context->self()->asHeapObject(); + auto buffer = this->_runtime->processVMStackBuffer_(receiver); + if (!buffer) return failPrimitive(); + auto index = this->_context->firstArgument()->asSmallInteger()->asNative(); + auto bp = this->_context->secondArgument()->asSmallInteger()->asNative(); + auto size = this->_runtime->processVMStackBufferSize_(receiver); + if (index < 1 || (uintptr_t)index > size) return failPrimitive(); + buffer[index - 1] = (Object*)(uintptr_t)bp; + return this->_context->secondArgument(); +} + +Object* Evaluator::primitiveProcessVMStackPcAtPut() +{ + auto receiver = this->_context->self()->asHeapObject(); + auto buffer = this->_runtime->processVMStackBuffer_(receiver); + if (!buffer) return failPrimitive(); + auto index = this->_context->firstArgument()->asSmallInteger()->asNative(); + auto pc = this->_context->secondArgument()->asSmallInteger()->asNative(); + // third argument is the code object; not stored at this slot in our model, + // but mirrors the Smalltalk signature `pcAt:put:of:`. + auto size = this->_runtime->processVMStackBufferSize_(receiver); + if (index < 1 || (uintptr_t)index > size) return failPrimitive(); + buffer[index - 1] = (Object*)(uintptr_t)pc; + return this->_context->secondArgument(); +} + +Object* Evaluator::primitiveProcessVMStackContextSwitchTo() +{ + auto outgoing = this->_context->self()->asHeapObject(); // current stack + auto incoming = this->_context->firstArgument()->asHeapObject(); // target stack + + // Snapshot live registers into the outgoing PVMStack ivars. + this->_runtime->processStackSP_put_(outgoing, this->_context->stackPointer()); + this->_runtime->processStackBP_put_(outgoing, this->_context->framePointer()); + this->_runtime->processStackEnv_put_(outgoing, (Object*)this->_context->environment()); + + // Bind the evaluator buffer to the incoming stack's buffer. + auto buffer = this->_runtime->processVMStackBuffer_(incoming); + auto size = this->_runtime->processVMStackBufferSize_(incoming); + this->_context->bindToBuffer_size_(buffer, size); + + // Restore SP from the incoming PVMStack. + // Just popFrame to load M/E/S/PC/BP from the topmost frame. + auto sp = this->_runtime->processStackSP_(incoming); + this->_context->stackPointer_(sp + 1); + this->_context->framePointer_(this->_context->stackPointer()); + + // popFrame reads the method out of the buffer; ensure that method has + // its executable code prepared (the Smalltalk-level prepareForExecution + // primitive is a no-op so that may not yet have happened). + this->_context->popFrame(); + auto method = this->_context->compiledCode(); + if (this->_runtime->methodExecutableCode_(method) == this->_runtime->_nilObj) + this->prepareForExecution_(method); + auto code = this->_runtime->methodExecutableCode_(method); + _work = this->_runtime->executableCodeWork_(code); + + + return (Object*)this->_runtime->_trueObj; +} + +Object* Evaluator::primitivePrepareForExecution() +{ + // CompiledMethod>>prepareForExecution. In this runtime methods are + // compiled lazily on first invoke, so this is a no-op. + return this->_context->self(); +} + Object* Evaluator::primitivePrimeFor() { return this->primitivePrimeFor_(this->_context->firstArgument()->asSmallInteger()->asNative()); } diff --git a/runtime/cpp/Evaluator/Evaluator.h b/runtime/cpp/Evaluator/Evaluator.h index 6b483236..4c40ccf7 100644 --- a/runtime/cpp/Evaluator/Evaluator.h +++ b/runtime/cpp/Evaluator/Evaluator.h @@ -238,6 +238,14 @@ class Evaluator : public SExpressionVisitor { Object* primitivePerformWithArguments(); Object* primitiveProcessBP(); Object* primitiveProcessStackAt(); + Object* primitiveProcessVMStackInitializeWithNewBuffer(); + Object* primitiveProcessVMStackInitializeWithActiveBuffer(); + Object* primitiveProcessVMStackBufferSize(); + Object* primitiveProcessVMStackAtPut(); + Object* primitiveProcessVMStackBpAtPut(); + Object* primitiveProcessVMStackPcAtPut(); + Object* primitiveProcessVMStackContextSwitchTo(); + Object* primitivePrepareForExecution(); Object* primitivePrimeFor(); Object* primitivePrimeFor_(auto anInteger); Object* primitiveSMIBitAnd(); diff --git a/runtime/cpp/Evaluator/Runtime.h b/runtime/cpp/Evaluator/Runtime.h index 7b67aced..c16cf6c6 100644 --- a/runtime/cpp/Evaluator/Runtime.h +++ b/runtime/cpp/Evaluator/Runtime.h @@ -476,18 +476,59 @@ class Runtime { return process->slot(Offsets::ProcessTopContext) != (Object*)KnownObjects::nil; } - HeapObject* processVMStackProcess_(HeapObject * processVMStack) { - return processVMStack->slot(Offsets::ProcessVMStackProcess)->asHeapObject(); + HeapObject* processStackProcess_(HeapObject * processStack) { + return processStack->slot(Offsets::ProcessStackProcess)->asHeapObject(); } - uintptr_t processVMStackSP_(HeapObject * processVMStack) { - return processVMStack->slot(Offsets::ProcessVMStackSP)->asSmallInteger()->asNative(); + void processStackProcess_put_(HeapObject * processStack, Object *process) { + processStack->slot(Offsets::ProcessStackProcess) = process; } - uintptr_t processVMStackBP_(HeapObject * processVMStack) { - return processVMStack->slot(Offsets::ProcessVMStackBP)->asSmallInteger()->asNative(); + uintptr_t processStackSP_(HeapObject * processStack) { + return processStack->slot(Offsets::ProcessStackSP)->asSmallInteger()->asNative(); } + void processStackSP_put_(HeapObject * processStack, uintptr_t sp) { + processStack->slot(Offsets::ProcessStackSP) = (Object*)newInteger_(sp); + } + + uintptr_t processStackBP_(HeapObject * processStack) { + return processStack->slot(Offsets::ProcessStackBP)->asSmallInteger()->asNative(); + } + + void processStackBP_put_(HeapObject * processStack, uintptr_t bp) { + processStack->slot(Offsets::ProcessStackBP) = (Object*)newInteger_(bp); + } + + Object* processStackEnv_(HeapObject * processStack) { + return processStack->slot(Offsets::ProcessStackEnv); + } + + void processStackEnv_put_(HeapObject * processStack, Object *env) { + processStack->slot(Offsets::ProcessStackEnv) = env; + } + + Object** processVMStackBuffer_(HeapObject * processVMStack) { + auto smi = processVMStack->slot(Offsets::ProcessVMStackBuffer); + if (smi == (Object*)_nilObj) return nullptr; + return reinterpret_cast(smi->asSmallInteger()->asNative()); + } + + void processVMStackBuffer_put_(HeapObject * processVMStack, Object **buffer) { + processVMStack->slot(Offsets::ProcessVMStackBuffer) = + (Object*)newInteger_(reinterpret_cast(buffer)); + } + + uintptr_t processVMStackBufferSize_(HeapObject * processVMStack) { + return processVMStack->slot(Offsets::ProcessVMStackBufferSize)->asSmallInteger()->asNative(); + } + + void processVMStackBufferSize_put_(HeapObject * processVMStack, uintptr_t size) { + processVMStack->slot(Offsets::ProcessVMStackBufferSize) = (Object*)newInteger_(size); + } + + + void initializeKernelObjects() { this->_falseObj = _kernel->_exports["false"]; diff --git a/runtime/cpp/KnownConstants.h b/runtime/cpp/KnownConstants.h index eacf7966..33ea64ae 100644 --- a/runtime/cpp/KnownConstants.h +++ b/runtime/cpp/KnownConstants.h @@ -93,10 +93,13 @@ enum Offsets { ProcessNativeStack = 1, ProcessTopContext = 2, - // inst vars of ProcessVMStack (the VM's internal stack object for processes) - ProcessVMStackProcess = 0, - ProcessVMStackSP = 1, - ProcessVMStackBP = 2, + // inst vars of ProcessStack (and its VM-internal subclass ProcessVMStack) + ProcessStackProcess = 0, + ProcessStackSP = 1, + ProcessStackBP = 2, + ProcessStackEnv = 3, + ProcessVMStackBuffer = 4, + ProcessVMStackBufferSize = 5, FFI_uint8 = 0, FFI_sint8 = 1, From e25cc6b1989529c587443602a0d92f0497fd550a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Pim=C3=A1s?= Date: Tue, 5 May 2026 01:01:44 -0300 Subject: [PATCH 03/11] [runtime/evaluator] add failPrimitiveWith_ helper Same as failPrimitive but also publishes an errorObject to the fallback Smalltalk code by writing it into the method's first temp (slot 1; temp indices are 1-based across the compiler/linearizer). The fallback method picks it up by declaring a | error | temp. --- runtime/cpp/Evaluator/Evaluator.cpp | 12 ++++++++++++ runtime/cpp/Evaluator/Evaluator.h | 1 + 2 files changed, 13 insertions(+) diff --git a/runtime/cpp/Evaluator/Evaluator.cpp b/runtime/cpp/Evaluator/Evaluator.cpp index 06ff890e..fb18de75 100644 --- a/runtime/cpp/Evaluator/Evaluator.cpp +++ b/runtime/cpp/Evaluator/Evaluator.cpp @@ -520,6 +520,18 @@ Object* Evaluator::failPrimitive() return this->_regR; } +Object* Evaluator::failPrimitiveWith_(Object* errorObject) +{ + // Same as failPrimitive, but additionally publishes `errorObject` to the + // Smalltalk fallback code by writing it into the method's first temp. + // The fallback method should declare a `| error |` temp to receive it. + // Temp indices are 1-based (matches compiler/linearizer convention). + auto method = this->_context->method(); + if (this->_runtime->methodTempCount_(method) > 0) + this->_context->stackTemporaryAt_put_(1, errorObject); + return this->failPrimitive(); +} + Object* Evaluator::primitiveAt() { auto receiver = this->_context->self(); diff --git a/runtime/cpp/Evaluator/Evaluator.h b/runtime/cpp/Evaluator/Evaluator.h index 4c40ccf7..f1ed7392 100644 --- a/runtime/cpp/Evaluator/Evaluator.h +++ b/runtime/cpp/Evaluator/Evaluator.h @@ -182,6 +182,7 @@ class Evaluator : public SExpressionVisitor { Object* boolObject(bool aBoolean); Object* failPrimitive(); + Object* failPrimitiveWith_(Object* errorObject); Object* primitiveAt(); Object* primitiveAtPut(); From e35698b3f13618a47559c6fa71969300f44dcaf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Pim=C3=A1s?= Date: Tue, 5 May 2026 01:04:52 -0300 Subject: [PATCH 04/11] [runtime/evaluator] dispatch DNU through _doesNotUnderstand:with: The VM pushes selector + Array(arguments) before invoking the DNU shim, so it must look up the 2-arg _doesNotUnderstand:with: method (provided on ProtoObject), not the 1-arg user-facing doesNotUnderstand:. Looking up the wrong selector left the call site with a mismatched stack and could crash; with the right shim, unhandled-message errors now surface cleanly via the standard exception path. Drop the dead active snapshot ifNil: [^self] comment in KernelModule>>suspendBecause: while we are nearby. --- modules/Kernel/KernelModule.st | 1 - runtime/cpp/Evaluator/Evaluator.cpp | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/Kernel/KernelModule.st b/modules/Kernel/KernelModule.st index 88cc1b1e..d38b7416 100644 --- a/modules/Kernel/KernelModule.st +++ b/modules/Kernel/KernelModule.st @@ -255,7 +255,6 @@ KernelModule >> suspendOnUnhandledExceptions [ KernelModule >> suspendBecause: anException [ | active process | active := Processor activeProcess. - "active snapshot ifNil: [^self]." process := Process sending: #suspended:because: to: host with: {active. anException}. process takeControl ] diff --git a/runtime/cpp/Evaluator/Evaluator.cpp b/runtime/cpp/Evaluator/Evaluator.cpp index fb18de75..141eea7f 100644 --- a/runtime/cpp/Evaluator/Evaluator.cpp +++ b/runtime/cpp/Evaluator/Evaluator.cpp @@ -317,14 +317,14 @@ void Egg::Evaluator::messageNotUnderstood_(SAbstractMessage *message) auto array = _runtime->newArray_(args); _context->push_(message->selector()); _context->push_((Object*)array); - auto symbol = _runtime->existingSymbolFrom_("doesNotUnderstand:"); + auto symbol = _runtime->existingSymbolFrom_("_doesNotUnderstand:with:"); auto behavior = _runtime->behaviorOf_(_regR); auto dnu = _runtime->lookup_startingAt_((Object*)symbol, behavior); if (!dnu) { std::string errmsg = std::string("Message not understood!\n") + this->_regR->printString() + " does not understand " + message->selector()->printString() + - "\nmethod #doesNotUnderstand: not found on receiver"; + "\nmethod #_doesNotUnderstand:with: not found on receiver"; error_(errmsg); } From c40a3858b6721ce742586ca57e962fac146e0f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Pim=C3=A1s?= Date: Tue, 5 May 2026 01:05:42 -0300 Subject: [PATCH 05/11] [runtime/evaluator] fail gracefully if module load throws (HostLoadModule/HostLoadModuleFromPath) Wrap both primitives in try/catch and use failPrimitiveWith_ to publish the error string to the fallback Smalltalk code. This ensures that parse errors and other exceptions during module load are surfaced as Smalltalk-level primitive failures, not C++ crashes. --- runtime/cpp/Evaluator/Evaluator.cpp | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/runtime/cpp/Evaluator/Evaluator.cpp b/runtime/cpp/Evaluator/Evaluator.cpp index 141eea7f..e01269fc 100644 --- a/runtime/cpp/Evaluator/Evaluator.cpp +++ b/runtime/cpp/Evaluator/Evaluator.cpp @@ -925,9 +925,13 @@ Object* Evaluator::primitiveHostLoadModule() { auto guard = this->_runtime->_heap->atGCUnsafepoint(); auto name = this->_context->firstArgument()->asHeapObject()->asLocalString(); std::cout << "loading " << name << "..." << std::endl; - auto module = (Object*)this->_runtime->loadModule_(this->_context->firstArgument()->asHeapObject()); - std::cout << " done loading " << name << std::endl; - return module; + try { + auto module = (Object*)this->_runtime->loadModule_(this->_context->firstArgument()->asHeapObject()); + std::cout << " done loading " << name << std::endl; + return module; + } catch (const std::exception& e) { + return this->failPrimitiveWith_((Object*)this->_runtime->newString_(e.what())); + } } Object* Evaluator::primitiveHostWriteFile() { @@ -977,9 +981,13 @@ Object* Evaluator::primitiveHostLoadModuleFromPath() { auto guard = this->_runtime->_heap->atGCUnsafepoint(); auto path = this->_context->firstArgument()->asHeapObject()->asLocalString(); std::cout << "loading from " << path << "..." << std::endl; - auto module = (Object*)this->_runtime->loadModuleFromPath_(path); - std::cout << " done loading " << path << std::endl; - return module; + try { + auto module = (Object*)this->_runtime->loadModuleFromPath_(path); + std::cout << " done loading " << path << std::endl; + return module; + } catch (const std::exception& e) { + return this->failPrimitiveWith_((Object*)this->_runtime->newString_(e.what())); + } } Object* Evaluator::primitiveNew() { From 1d492406e8609156491b3318d78143cb9e17298a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Pim=C3=A1s?= Date: Tue, 5 May 2026 01:07:00 -0300 Subject: [PATCH 06/11] [runtime/evaluator] add HostExit primitive (process exit from Smalltalk) Registers HostExit as a primitive. Implements primitiveHostExit to call std::exit with the given SmallInteger code (default 0). Returns nilObj for completeness, but this is unreachable. Allows Smalltalk code to terminate the VM process with a specific exit code. --- runtime/cpp/Evaluator/Evaluator.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/cpp/Evaluator/Evaluator.cpp b/runtime/cpp/Evaluator/Evaluator.cpp index e01269fc..1fe4584a 100644 --- a/runtime/cpp/Evaluator/Evaluator.cpp +++ b/runtime/cpp/Evaluator/Evaluator.cpp @@ -185,6 +185,7 @@ void Evaluator::initializePrimitives() this->addPrimitive("HostPlatformName", &Evaluator::primitiveHostPlatformName); this->addPrimitive("HostCurrentMilliseconds", &Evaluator::primitiveHostCurrentMilliseconds); this->addPrimitive("HostLog", &Evaluator::primitiveHostLog); + this->addPrimitive("HostExit", &Evaluator::primitiveHostExit); this->addPrimitive("HostReadFile", &Evaluator::primitiveHostReadFile); this->addPrimitive("HostWriteFile", &Evaluator::primitiveHostWriteFile); this->addPrimitive("HostCreateDirectory", &Evaluator::primitiveHostCreateDirectory); From 940217fd1f5cd828792e97acd8fee5f766d3a0be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Pim=C3=A1s?= Date: Tue, 5 May 2026 01:11:41 -0300 Subject: [PATCH 07/11] [loader/kernel] relative module search paths + throw on missing module Loader: reorder search prefixes so the closest match ('./') is tried first, matching the Smalltalk-side setupDefaultSearchPaths order. Loader: replace error() calls with throw std::runtime_error so the module-not-found case is caught by the new try/catch wrappers in primitiveHostLoadModule[FromPath] and surfaced as a Smalltalk error. HostSystem>>setupDefaultSearchPaths: drop the cwd prefix from each candidate and rely on relative paths only; this keeps the displayed search path short and readable regardless of where the binary is launched from. HostSystem>>primitiveLoad: / primitiveLoadModuleFromPath: add a | error | temp so failPrimitiveWith_ can publish the exception message, then re-signal it as an Error. HostSystem>>suspended:because: replace the unimplemented HostSuspendedBecause primitive with a direct log+exit sequence so unhandled errors produce a readable message before exiting. --- modules/Kernel/HostSystem.st | 13 +++++++++---- runtime/cpp/Loader.cpp | 9 ++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/modules/Kernel/HostSystem.st b/modules/Kernel/HostSystem.st index 3c94a35e..c13cfd9f 100644 --- a/modules/Kernel/HostSystem.st +++ b/modules/Kernel/HostSystem.st @@ -152,12 +152,16 @@ HostSystem >> prependSearchPath: aString type: aSymbol [ { #category : #loading } HostSystem >> primitiveLoad: aSymbol [ + | error | + ^Error signal: error ] { #category : #loading } HostSystem >> primitiveLoadModuleFromPath: aString [ + | error | + ^Error signal: error ] { #category : #services } @@ -177,16 +181,15 @@ HostSystem >> setupDefaultSearchPaths [ platform-native PATH separator) supplies extra source directories. We also walk a few levels up from the current directory so the runtime works both from the project root and from a build subdirectory." - | env home cwd prefixes | + | env home prefixes | env := self getEnv: 'EGG_MODULES_PATH'. env ifNotNil: [ (env substringsSplitBy: self pathSeparator) do: [:p | self addSearchPath: p type: #tonel. self addSearchPath: p type: #ems]]. - cwd := self currentDirectory. prefixes := #('' '../' '../../' '../../../' '../../../../'). prefixes do: [:p | | base | - base := cwd , '/' , p , 'modules'. + base := p , 'modules'. (self pathExists: base) ifTrue: [ self addSearchPath: base type: #tonel. self addSearchPath: base type: #ems]]. @@ -200,7 +203,9 @@ HostSystem >> setupDefaultSearchPaths [ { #category : #private } HostSystem >> suspended: aProcess because: anException [ - + self + logError: 'Error: ' , anException description; + exit: 1 ] { #category : #services } diff --git a/runtime/cpp/Loader.cpp b/runtime/cpp/Loader.cpp index e21b9e43..a618bdcd 100644 --- a/runtime/cpp/Loader.cpp +++ b/runtime/cpp/Loader.cpp @@ -9,6 +9,7 @@ #include "FileImageSegment.h" #include #include +#include namespace Egg { @@ -25,7 +26,7 @@ std::string Loader::findModulesDir_() { return _modulesDir; std::vector prefixes = { - "../../../../", "../../../", "../../", "../", "./" + "./", "../", "../../", "../../../", "../../../../" }; for (const auto& prefix : prefixes) { auto candidate = prefix + _modulesDir; @@ -96,8 +97,7 @@ HeapObject* Loader::loadModule_(const std::string& name) { module = sourceLoader.loadModuleFromSource(modPath); } else { - error(("Module not found: " + name).c_str()); - return nullptr; + throw std::runtime_error("Module not found: " + name); } _loadedModules[name] = module; @@ -143,8 +143,7 @@ HeapObject* Loader::loadModuleFromPath_(const std::string& path) { module = sourceLoader.loadModuleFromSource(path); } else { - error(("Module not found at path: " + path).c_str()); - return nullptr; + throw std::runtime_error("Module not found at path: " + path); } _loadedModules[name] = module; From aa39e6d99c98aac54a90399c39bca913bf5ac872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Pim=C3=A1s?= Date: Tue, 5 May 2026 01:15:46 -0300 Subject: [PATCH 08/11] [bootstrap] enrich parse/compile errors with file:line:col context TonelReader: add optional `filename` parameter to `parseFile`; store it in `_filename`. Replace bare `error_()` calls with `parseError_()`, which walks `_source` up to `_pos` to compute a 1-based line/column and throws `std::runtime_error("file:line:col: message")`. Bootstrapper / SourceModuleLoader: pass `entry.path().string()` as the filename so every parse error now names the .st file and position. SourceModuleLoader>>createMethodsOf_: wrap each `createNewMethod_` call in a try/catch that delegates to the new `rethrowWithContext_` static helper, which appends "while compiling ClassName >> " to the rethrown exception, giving a clear breadcrumb from a compile error back to the failing file and method. --- runtime/cpp/Bootstrap/Bootstrapper.cpp | 4 +-- runtime/cpp/Bootstrap/SourceModuleLoader.cpp | 37 +++++++++++++++----- runtime/cpp/Bootstrap/SourceModuleLoader.h | 4 +++ runtime/cpp/Bootstrap/TonelReader.cpp | 25 +++++++++++-- runtime/cpp/Bootstrap/TonelReader.h | 8 ++++- 5 files changed, 63 insertions(+), 15 deletions(-) diff --git a/runtime/cpp/Bootstrap/Bootstrapper.cpp b/runtime/cpp/Bootstrap/Bootstrapper.cpp index b73ea3f7..dd3c3689 100644 --- a/runtime/cpp/Bootstrap/Bootstrapper.cpp +++ b/runtime/cpp/Bootstrap/Bootstrapper.cpp @@ -88,7 +88,7 @@ void Bootstrapper::loadKernelSpecs() { if (className == "package") continue; std::string source = readSourceFile_(className); - ClassSpec* spec = reader.parseFile(source); + ClassSpec* spec = reader.parseFile(source, entry.path().string()); _moduleSpec.addClass(spec); } } @@ -107,7 +107,7 @@ void Bootstrapper::loadKernelSpecs() { if (!file.is_open()) continue; std::string source((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - ClassSpec* extensionSpec = reader.parseFile(source); + ClassSpec* extensionSpec = reader.parseFile(source, entry.path().string()); ClassSpec* existingSpec = _moduleSpec.resolveClass(className); if (!existingSpec) { std::cerr << " Warning: VM extension for unknown class " << className << std::endl; diff --git a/runtime/cpp/Bootstrap/SourceModuleLoader.cpp b/runtime/cpp/Bootstrap/SourceModuleLoader.cpp index 0557da69..e76857a2 100644 --- a/runtime/cpp/Bootstrap/SourceModuleLoader.cpp +++ b/runtime/cpp/Bootstrap/SourceModuleLoader.cpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include namespace Egg { @@ -59,7 +61,7 @@ HeapObject* SourceModuleLoader::loadModuleFromSource(const std::string& modulePa std::string source((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - ClassSpec* spec = reader.parseFile(source); + ClassSpec* spec = reader.parseFile(source, entry.path().string()); _moduleSpec.addClass(spec); if (spec->isExtension()) { extensionNames.push_back(spec->name()); @@ -302,17 +304,34 @@ Object* SourceModuleLoader::createNewClassFrom_(ClassSpec* spec, Object* module) void SourceModuleLoader::createMethodsOf_(Object* cls, ClassSpec* spec) { GCedRef clsRef(cls); - // Instance methods - for (const auto& method : spec->methods()) { - createNewMethod_(method.source(), clsRef.get(), method.category()); - } + auto runMethod = [&](const MethodSpec& method, Object* receiver, const std::string& side) { + try { + createNewMethod_(method.source(), receiver, method.category()); + } catch (const std::exception& e) { + rethrowWithContext_(e, spec->name(), side, method.source()); + } + }; + + for (const auto& method : spec->methods()) + runMethod(method, clsRef.get(), ""); - // Class methods auto metaclass = _runtime->sendLocal_to_("class", clsRef.get()); GCedRef metaRef(metaclass); - for (const auto& method : spec->metaclass()->methods()) { - createNewMethod_(method.source(), metaRef.get(), method.category()); - } + for (const auto& method : spec->metaclass()->methods()) + runMethod(method, metaRef.get(), " class"); +} + +[[noreturn]] void SourceModuleLoader::rethrowWithContext_(const std::exception& e, + const Egg::string& className, + const std::string& side, + const Egg::string& source) { + size_t nl = source.find(U'\n'); + Egg::string sig = nl == Egg::string::npos ? source : source.substr(0, nl); + std::stringstream ss; + ss << e.what() + << "\n while compiling " << className.toUtf8() << side + << " >> " << sig.toUtf8(); + throw std::runtime_error(ss.str()); } void SourceModuleLoader::createNewMethod_(const Egg::string& source, Object* species, const Egg::string& category) { diff --git a/runtime/cpp/Bootstrap/SourceModuleLoader.h b/runtime/cpp/Bootstrap/SourceModuleLoader.h index 496410e9..67e74c8f 100644 --- a/runtime/cpp/Bootstrap/SourceModuleLoader.h +++ b/runtime/cpp/Bootstrap/SourceModuleLoader.h @@ -35,6 +35,10 @@ class SourceModuleLoader { Object* createNewClassFrom_(ClassSpec* spec, Object* module); void createMethodsOf_(Object* cls, ClassSpec* spec); void createNewMethod_(const Egg::string& source, Object* species, const Egg::string& category = Egg::string("")); + [[noreturn]] static void rethrowWithContext_(const std::exception& e, + const Egg::string& className, + const std::string& side, + const Egg::string& source); Object* createExtensionMethod_(const Egg::string& source, Object* species); // Literal transfer helpers diff --git a/runtime/cpp/Bootstrap/TonelReader.cpp b/runtime/cpp/Bootstrap/TonelReader.cpp index 0ed103e8..428e1eb9 100644 --- a/runtime/cpp/Bootstrap/TonelReader.cpp +++ b/runtime/cpp/Bootstrap/TonelReader.cpp @@ -9,6 +9,9 @@ #include "CodeSpecs.h" #include "Egg.h" +#include +#include + namespace Egg { // ── Stream primitives ──────────────────────────────────────────────── @@ -38,9 +41,11 @@ void TonelReader::skipLine() { // ── Tonel structure ────────────────────────────────────────────────── -ClassSpec* TonelReader::parseFile(const std::string& utf8Source) { +ClassSpec* TonelReader::parseFile(const std::string& utf8Source, + const std::string& filename) { _source = Egg::string(utf8Source); _pos = 0; + _filename = filename; Egg::string comment = readComments(); Egg::string type = readType(); @@ -60,7 +65,7 @@ ClassSpec* TonelReader::parseFile(const std::string& utf8Source) { } else if (type == "Class") { spec->supername(fields.count("superclass") ? fields["superclass"] : Egg::string("")); } else { - error_("Unknown Tonel type: " + type.toUtf8()); + parseError_("Unknown Tonel type: " + type.toUtf8()); } // #type field: #variable (pointer-indexed), #bytes (byte-indexed) @@ -219,7 +224,7 @@ Egg::string TonelReader::nextBlock() { char32_t prev = 0; while (nested > 0) { if (atEnd()) - error_("unterminated method body"); + parseError_("unterminated method body"); char32_t ch = next(); if (prev == U'$') { // This character is a character literal value (e.g. $[ $] $' $" $$) @@ -264,6 +269,20 @@ void TonelReader::skipComment() { skipToMatch(U'"'); } +void TonelReader::parseError_(const std::string& message) const { + // Compute 1-based line/column from _pos in _source. + size_t line = 1, col = 1; + size_t limit = _pos < _source.length() ? _pos : _source.length(); + for (size_t i = 0; i < limit; ++i) { + if (_source[i] == U'\n') { line++; col = 1; } + else { col++; } + } + std::stringstream ss; + ss << (_filename.empty() ? "" : _filename) + << ":" << line << ":" << col << ": " << message; + throw std::runtime_error(ss.str()); +} + // Mirrors TonelReader >> skipToMatch: // Skips until a non-doubled occurrence of `ch` is found. void TonelReader::skipToMatch(char32_t ch) { diff --git a/runtime/cpp/Bootstrap/TonelReader.h b/runtime/cpp/Bootstrap/TonelReader.h index 2eb948df..db7d4764 100644 --- a/runtime/cpp/Bootstrap/TonelReader.h +++ b/runtime/cpp/Bootstrap/TonelReader.h @@ -21,7 +21,9 @@ class TonelReader { // Parse a complete Tonel-format .st file and return a ClassSpec. // The returned ClassSpec is heap-allocated; caller takes ownership. - ClassSpec* parseFile(const std::string& utf8Source); + // The optional filename is only used to enrich parse-error messages. + ClassSpec* parseFile(const std::string& utf8Source, + const std::string& filename = ""); private: // Stream-style helpers @@ -51,9 +53,13 @@ class TonelReader { Egg::string parseSTONString(); std::vector parseSTONArray(); + // Throws std::runtime_error with file:line:col context. + [[noreturn]] void parseError_(const std::string& message) const; + // State Egg::string _source; size_t _pos = 0; + std::string _filename; }; } // namespace Egg From 016776e6439d1ea1e240eb7ad5bd5be589356e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Pim=C3=A1s?= Date: Tue, 5 May 2026 01:20:09 -0300 Subject: [PATCH 09/11] [kernel] add String class >> new (delegates to new: 0) String>>new was missing; sending `new` to String would fall through to Object>>new, which allocates a fixed-size object rather than a byte object. Delegate to `new: 0` to create an empty, properly-formatted String instead. --- modules/Kernel/String.st | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/Kernel/String.st b/modules/Kernel/String.st index 733a11f5..f2c079eb 100644 --- a/modules/Kernel/String.st +++ b/modules/Kernel/String.st @@ -44,6 +44,11 @@ String class >> fromUTF8: aByteArray [ ifFalse: [UTF8 current decode: aByteArray] ] +{ #category : #'instance creation' } +String class >> new [ + ^self new: 0 +] + { #category : #'instance creation' } String class >> new: anInteger [ ^self primitiveNewBytes: anInteger + 1 From f7068f4a155d9c477e25c82936789d2757ef05c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Pim=C3=A1s?= Date: Tue, 5 May 2026 01:46:21 -0300 Subject: [PATCH 10/11] [runtime][pharo] update primitives - Add primitiveProcessVMStackInitializeWithActiveBuffer and primitiveProcessVMStackInitializeWithNewBuffer following kernel process changes (should check if they work as expected) - Fix kernel and kernel prim paths --- .../Powerlang-Core/EggBootstrapModule.class.st | 7 ++++--- .../pharo/Powerlang-Core/EggEvaluator.class.st | 18 ++++++++++++++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/runtime/pharo/Powerlang-Core/EggBootstrapModule.class.st b/runtime/pharo/Powerlang-Core/EggBootstrapModule.class.st index bbb21100..3eb22a2e 100644 --- a/runtime/pharo/Powerlang-Core/EggBootstrapModule.class.st +++ b/runtime/pharo/Powerlang-Core/EggBootstrapModule.class.st @@ -41,7 +41,7 @@ EggBootstrapModule class >> fromSpec [ EggBootstrapModule class >> kernelSpec [ KernelSpec ifNil: [ - KernelSpec := self readSpec: #Kernel at: '.'. + KernelSpec := self readSpec: #Kernel at: '../../modules'. KernelSpec initializeConstants ]. ^ KernelSpec ] @@ -57,7 +57,8 @@ EggBootstrapModule class >> new [ { #category : 'accessing' } EggBootstrapModule class >> readSpec: specName at: path [ | dir subdir | - dir := '../../modules' asFileReference. + + dir := '.' asFileReference. subdir := path isString ifTrue: [ path ] ifFalse: [ path asPath pathString ]. subdir notEmpty ifTrue: [ dir := dir / subdir]. @@ -95,7 +96,7 @@ EggBootstrapModule class >> resetSpecs [ { #category : 'accessing' } EggBootstrapModule class >> vmPrimitivesSpec [ ^ VMPrimitivesSpec - ifNil: [ VMPrimitivesSpec := self readSpec: #VM at: 'Kernel' ] + ifNil: [ VMPrimitivesSpec := self readSpec: #VM at: '../../modules/Kernel' ] ] { #category : 'initialization' } diff --git a/runtime/pharo/Powerlang-Core/EggEvaluator.class.st b/runtime/pharo/Powerlang-Core/EggEvaluator.class.st index d6dd1857..417f5971 100644 --- a/runtime/pharo/Powerlang-Core/EggEvaluator.class.st +++ b/runtime/pharo/Powerlang-Core/EggEvaluator.class.st @@ -215,8 +215,10 @@ EggEvaluator >> initializePrimitives [ at: #PrimeFor put: self primitivePrimeFor; at: #FlushFromCaches put: self primitiveFlushFromCaches; at: #PrepareForExecution put: self primitivePrepareForExecution; - at: #ProcessVMStackInitialize - put: self primitiveProcessVMStackInitialize; + at: #ProcessVMStackInitializeWithActiveBuffer + put: self primitiveProcessVMStackInitializeWithActiveBuffer; + at: #ProcessVMStackInitializeWithNewBuffer + put: self primitiveProcessVMStackInitializeWithNewBuffer; at: #ProcessVMStackAt put: self primitiveProcessVMStackAt; at: #ProcessVMStackAtPut put: self primitiveProcessVMStackAtPut; at: #ProcessVMStackBpAtPut put: self primitiveProcessVMStackBpAtPut; @@ -618,6 +620,18 @@ EggEvaluator >> primitiveProcessVMStackInitialize [ stacks at: context self put: new ] ] +{ #category : 'primitives' } +EggEvaluator >> primitiveProcessVMStackInitializeWithActiveBuffer [ + + ^ [ stacks at: context self put: context ] +] + +{ #category : 'primitives' } +EggEvaluator >> primitiveProcessVMStackInitializeWithNewBuffer [ + + ^ [ stacks at: context self put: self halt newEvaluationContext ] +] + { #category : 'primitives' } EggEvaluator >> primitiveProcessVMStackPcAtPut [ From 6e26032fe359b0d50f1e3179f056fbc45ba5741e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Pim=C3=A1s?= Date: Tue, 5 May 2026 01:49:01 -0300 Subject: [PATCH 11/11] [runtime/evaluator] add primitiveHostExit implementation Implements primitiveHostExit: reads the first argument, calls std::exit with its SmallInteger value (default 0), and returns nilObj for completeness (unreachable). Registers the primitive under the name 'HostExit'. Declares it in Evaluator.h. --- runtime/cpp/Evaluator/Evaluator.cpp | 9 +++++++++ runtime/cpp/Evaluator/Evaluator.h | 1 + 2 files changed, 10 insertions(+) diff --git a/runtime/cpp/Evaluator/Evaluator.cpp b/runtime/cpp/Evaluator/Evaluator.cpp index 1fe4584a..9ee917e0 100644 --- a/runtime/cpp/Evaluator/Evaluator.cpp +++ b/runtime/cpp/Evaluator/Evaluator.cpp @@ -899,6 +899,15 @@ Object* Evaluator::primitiveHostLog() { return this->_regR; } +Object* Evaluator::primitiveHostExit() { + auto arg = this->_context->firstArgument(); + int code = 0; + if (arg->isSmallInteger()) + code = (int)arg->asSmallInteger()->asNative(); + std::exit(code); + return (Object*)this->_runtime->_nilObj; +} + Object* Evaluator::primitiveHostReadFile() { auto filename = this->_context->firstArgument(); std::ifstream file(filename->asHeapObject()->asLocalString(), std::ios::binary); diff --git a/runtime/cpp/Evaluator/Evaluator.h b/runtime/cpp/Evaluator/Evaluator.h index f1ed7392..cf9bf69a 100644 --- a/runtime/cpp/Evaluator/Evaluator.h +++ b/runtime/cpp/Evaluator/Evaluator.h @@ -228,6 +228,7 @@ class Evaluator : public SExpressionVisitor { Object* primitiveHostLoadModule(); Object* primitiveHostLoadModuleFromPath(); Object* primitiveHostLog(); + Object* primitiveHostExit(); Object* primitiveHostPathExists(); Object* primitiveHostPlatformName(); Object* primitiveHostReadFile();