Note (2026-04-12): Shipped with one revision from this proposal — bare
@@:selfis not accepted as a system-instance reference; it is a compile error (E603).@@:selfis a syntactic prefix, always chained with a member (@@:self.method(args)). The same rule applies to@@:system(E604) — only@@:system.stateis currently defined. Seeframe_language.mdfor the normative spec.Note (2026-04-20): Erlang support landed with full semantics.
@@:self.method(args)expands to bareself.method(args)which the Erlang handler post-pass (erlang_system.rs) recognizes and rewrites to{DataN, Result} = frame_dispatch__(method, [args], DataPrev). Data is threaded forward via the existingdata_genmachinery soself.fieldreads andself.field = valueassignments after the call see whatever state the called handler set. Each dispatch site is wrapped in acase ...#data.frame_current_state ofthat short-circuits the rest of the caller’s body if the called handler transitioned — matching Python/TS’sif ctx._transitioned: return;behavior. Seeframepiler_design.md § Erlang — @@:self via frame_dispatch__ + transition guardsfor the emitted pattern.22_self_calibrating_sensor.ferlnow exercises the transition-guard via a pattern-match assertion in$Shutdown::assert_trace().
Summary
Introduce @@:self.<iface_method>(args) as a Frame-managed construct for a system
to call its own interface methods from within event handlers, enter/exit handlers,
and actions. This replaces the current practice of using native self-calls
(self.method() in Python, this.method() in TypeScript, etc.) with a
Frame-recognized construct that participates in context management, return value
handling, and the full kernel dispatch pipeline.
The transpiler recognizes @@:self.<iface_name>(args) as a Frame statement and
generates the appropriate reentrant interface call with correct context push/pop
semantics.
Motivation / Problem
-
Systems today call their own interface methods using native target-language syntax (
self.method()in Python,this.method()in TypeScript,self.method()in Rust). This works mechanically — the generated interface method constructs a FrameEvent and dispatches through the kernel — but Frame has no visibility into it. The transpiler treats the call as opaque native code. -
This opacity creates several problems:
- No static validation. The transpiler cannot verify that the interface method exists, that the argument count matches, or that the return type is used correctly. Typos in native self-calls are caught at compile time by the target language, not by Frame.
- No tracing/debugging integration. The VSCode debugger extension traces events through the kernel. A native self-call enters the kernel but the debugger has no Frame-level annotation to correlate it with the call site. It appears as an anonymous reentrant dispatch.
- Inconsistency with
@@:grammar.@@:selfis the natural accessor for the system instance.@@:return,@@:event,@@:params.xare all Frame-managed constructs. But calling an interface method on the system requires dropping into native syntax. The accessor grammar has a hole where method invocation should be. - Cross-backend portability. Native self-call syntax differs per language:
self.method(),this.method(),self->method(),this->method(). A Frame construct eliminates this variance from the source. - Context awareness. When the transpiler knows a self-call is happening, it can generate instrumentation: pre/post hooks for tracing, assertions on return stack depth, guard checks, or policy evaluation points.
Design
1. Syntax
@@:self.<interface_method_name>(arg1, arg2, ...)
The construct follows the @@: accessor grammar:
@@:— Frame namespace accessorself— resolves to the persistent system instance.<method>()— dot-accessor invokes an interface method on that instance
With return value
result = @@:self.getStatus()
Fire-and-forget (no return capture)
@@:self.reset()
With arguments
@@:self.process(data, "high")
2. Semantics
@@:self.iface(args) is semantically identical to a native self-call on the
generated interface method. The difference is that the transpiler owns the
expansion and can inject additional behavior.
2.1 Dispatch Pipeline
The call follows the same pipeline as an external interface call:
- A new FrameContext is created and pushed onto
_context_stack. - A FrameEvent is constructed with the interface method name and parameters.
- The FrameEvent is dispatched through the kernel.
- The kernel routes through the router → state dispatch → handler chain.
- If a transition occurs, the kernel processes exit/switch/enter.
- On return, the context is popped and the return value is available to the caller.
2.2 Context Stack Behavior
A self-interface-call is a reentrant call. The context stack grows by one:
Handler processing event "increment":
context_stack: [ctx_increment]
@@:self.getStatus() // reentrant self-call
context_stack: [ctx_increment, ctx_getStatus]
// handler for getStatus executes
// @@:event == "getStatus"
// @@:params == {} (no params)
context_stack: [ctx_increment] // ctx_getStatus popped
// back in increment handler
// @@:event == "increment" (original context restored)
The context stack isolates each dispatch. @@:event, @@:params, @@:return,
and @@:data all refer to the topmost context. A self-call does not corrupt
the calling context.
2.3 State Sensitivity
Because the call goes through the full kernel dispatch pipeline, the handler that executes depends on the current state at the time of the call, not the state at the time the outer handler began. If a transition occurred before the self-call, the self-call dispatches to a handler in the new state.
@@system Workflow {
interface:
advance()
status(): str = ""
machine:
$Draft {
advance() {
-> $Review
// After transition, we're in $Review
s = @@:self.status() // dispatches to $Review's handler
print(f"Now in: {s}") // prints "Now in: pending_review"
}
status(): str {
@@:("draft")
}
}
$Review {
status(): str {
@@:("pending_review")
}
}
}
3. Codegen Expansion
3.1 Python
// Frame source
@@:self.process(data, "high")
# Generated
self.process(data, "high")
3.2 TypeScript
// Generated
this.process(data, "high");
3.3 Rust
// Generated
self.process(data, "high");
3.4 C
// Generated
SystemName_process(self, data, "high");
3.5 With Return Value
status = @@:self.getStatus()
# Generated
status = self.getStatus()
4. Validation Rules
The transpiler validates @@:self.<method>(args) at transpile time:
| Code | Check | Severity |
|---|---|---|
| E601 | <method> does not exist in the system’s interface: block |
Error |
| E602 | Argument count does not match interface method parameter count | Error |
| W601 | Interface method has a return type but return value is not captured | Warning |
These validations are impossible with native self-calls because the transpiler treats them as opaque native code.
5. Scanner / Parser Impact
5.1 Token Recognition
The NativeRegionScanner recognizes @@:self as a Frame token. The extension
recognizes the pattern:
@@:self . <identifier> ( <args> )
as a self-interface-call statement rather than a simple accessor.
The distinguishing rule:
@@:self— bare accessor (expand to system instance reference)@@:self.method()— parentheses present → call (expand to interface call)@@:self.method(a, b)— parentheses with args → call with arguments
5.2 Expression Context
@@:self.method() can appear anywhere a native method call can appear:
- As a standalone statement
- On the right side of an assignment
- As an argument to an action call:
doSomething(@@:self.getStatus()) - In a native expression:
if @@:self.isReady(): ...
In all cases, the transpiler expands the @@:self.method() portion into the
target-language self-call and leaves the surrounding native code intact.
6. Interaction with Other Features
6.1 Transitions
A self-interface-call dispatches through the kernel. If the handler for the called method triggers a transition, that transition executes fully (exit → state change → enter) before control returns to the calling handler.
This means the calling handler may find itself in a different state after the self-call returns. This is already the behavior with native self-calls. Making it explicit as a Frame construct does not change the semantics but makes the reentrancy visible in the Frame source.
6.2 Event Forwarding (=>)
A self-call creates a new FrameEvent and dispatches it. It does not participate
in the forwarding chain of the currently-processing event. If the calling handler
later executes => $^, that forward uses the original event, not the
self-call’s event.
6.3 @@:return
Within the called handler, @@:return = value and @@:(value) affect the
self-call’s return context on the context stack. They do not affect the calling
handler’s @@:return — the context stack isolates them.
7. Restrictions
7.1 Interface Methods Only
@@:self.<n>() is specifically for interface methods. Actions and operations
are called directly using native syntax because they do not go through the
kernel dispatch pipeline. If <n> resolves to an action or operation rather
than an interface method, the transpiler emits error E601.
7.2 No Constructor Calls
@@:self cannot call the system constructor. The constructor is not an interface
method — it runs once during system creation and is not part of the dispatch
pipeline.
7.3 No Chaining
@@:self.getA().doB() is not valid Frame syntax. The return value of getA()
is a native value, not a Frame system. Only the first call is a Frame construct.
The scanner recognizes @@:self.<ident>(<args>) as the complete Frame token and
does not parse further dots.
Examples
Basic Self-Call
@@[target("python_3")]
@@system TrafficLight {
interface:
next()
getColor(): str = ""
machine:
$Green {
next() {
color = @@:self.getColor()
print(f"Leaving {color}")
-> $Yellow
}
getColor(): str {
@@:("green")
}
}
$Yellow {
next() { -> $Red }
getColor(): str { @@:("yellow") }
}
$Red {
next() { -> $Green }
getColor(): str { @@:("red") }
}
}
Self-Call in Action
@@[target("python_3")]
@@system Sensor {
interface:
calibrate()
reading(): float = 0.0
machine:
$Active {
calibrate() {
doCalibration()
}
reading(): float {
@@:(self.raw_value + self.offset)
}
}
actions:
doCalibration() {
baseline = @@:self.reading()
self.offset = baseline * -1
}
domain:
raw_value: float = 0.0
offset: float = 0.0
}
Self-Call in Enter Handler
@@[target("python_3")]
@@system Connection {
interface:
connect(host: str)
disconnect()
isAlive(): bool = False
machine:
$Disconnected {
connect(host: str) {
self.host = host
-> $Connected
}
}
$Connected {
$>() {
alive = @@:self.isAlive()
if not alive:
-> $Disconnected
}
disconnect() { -> $Disconnected }
isAlive(): bool {
@@:(ping(self.host))
}
}
actions:
ping(host) {
return True
}
domain:
host: str = ""
}
Open Issues
-
Recursive self-call depth. A self-call in a handler triggered by a self-call creates unbounded recursion potential. Should the transpiler emit a recursion depth check? The context stack would grow without bound. A maximum depth (configurable, default 100) could be enforced at runtime.
-
@@:selfin complex native expressions. When@@:self.method()appears inside a complex native expression (e.g.,x = foo(@@:self.bar(), baz())), the scanner must extract and expand only the@@:self.bar()portion. This requires the scanner to handle nested parentheses correctly to find the end of the argument list. Thebalanced_paren_end()infrastructure should handle this, but it needs verification for the call-with-arguments case. -
Guard interaction (RFC-0003). If interface guards are enabled, should self-calls bypass guards or go through them? Internal calls might be considered trusted. Proposal: guards apply by default; a pragma could disable guard evaluation for self-calls.
Test Plan
Unit Fixtures
- Basic self-call: Handler calls
@@:self.method(). Verify method dispatches through kernel and handler executes. - Self-call with return:
result = @@:self.getStatus(). Verify return value propagates correctly. - Self-call with args:
@@:self.process(a, b). Verify parameters arrive in handler. - Context isolation: Outer handler’s
@@:eventis “increment”. Self-call dispatches “getStatus”. Inside getStatus handler,@@:event== “getStatus”. After self-call returns, outer handler’s@@:event== “increment”. - Return value isolation: Outer call has
@@:returnset to “outer”. Self-call sets@@:returnto “inner”. After self-call, outer’s@@:returnis still “outer”. - Self-call after transition: Transition occurs, then self-call. Verify self-call dispatches to the new state’s handler.
- Self-call in enter handler: Enter handler calls
@@:self.method(). Verify correct dispatch and context management. - Self-call in action: Action calls
@@:self.method(). Verify dispatch and return value. - Invalid method name:
@@:self.nonExistent(). Verify transpiler emits E601. - Arity mismatch:
@@:self.process(a)whenprocesstakes two params. Verify transpiler emits E602. - Self-call triggers transition: Called method’s handler transitions. Verify transition completes before control returns to caller.
Per-Backend Validation
All fixtures must pass for every target language backend. Self-call expansion
is backend-specific (self., this., self->, function-style) so each backend
must be tested independently.
References
- RFC-0003 — Interface Guards (guard interaction with self-calls)
- RFC-0005 — System OS / SOS (defines
@@:selfaccessor grammar, context stack) - Frame V4 Language Reference —
frame_language.md - Frame Runtime Architecture —
frame_runtime.md - Framepiler Design —
framepiler_design.md