Note (2026-04-12): Shipped with one revision from this proposal — bare @@:self is not accepted as a system-instance reference; it is a compile error (E603). @@:self is a syntactic prefix, always chained with a member (@@:self.method(args)). The same rule applies to @@:system (E604) — only @@:system.state is currently defined. See frame_language.md for the normative spec.

Note (2026-04-20): Erlang support landed with full semantics. @@:self.method(args) expands to bare self.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 existing data_gen machinery so self.field reads and self.field = value assignments after the call see whatever state the called handler set. Each dispatch site is wrapped in a case ...#data.frame_current_state of that short-circuits the rest of the caller’s body if the called handler transitioned — matching Python/TS’s if ctx._transitioned: return; behavior. See framepiler_design.md § Erlang — @@:self via frame_dispatch__ + transition guards for the emitted pattern. 22_self_calibrating_sensor.ferl now 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:

    1. 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.
    2. 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.
    3. Inconsistency with @@: grammar. @@:self is the natural accessor for the system instance. @@:return, @@:event, @@:params.x are 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.
    4. 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.
    5. 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 accessor
  • self — 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:

  1. A new FrameContext is created and pushed onto _context_stack.
  2. A FrameEvent is constructed with the interface method name and parameters.
  3. The FrameEvent is dispatched through the kernel.
  4. The kernel routes through the router → state dispatch → handler chain.
  5. If a transition occurs, the kernel processes exit/switch/enter.
  6. 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

  1. 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.

  2. @@:self in 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. The balanced_paren_end() infrastructure should handle this, but it needs verification for the call-with-arguments case.

  3. 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 @@:event is “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 @@:return set to “outer”. Self-call sets @@:return to “inner”. After self-call, outer’s @@:return is 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) when process takes 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 @@:self accessor grammar, context stack)
  • Frame V4 Language Reference — frame_language.md
  • Frame Runtime Architecture — frame_runtime.md
  • Framepiler Design — framepiler_design.md