scrml.dev v0.7.1
Reference › Errors

W-ENGINE-SELF-WRITE-DETECTED

Engine self-write detected. Per §51.0.F.1, self-writes are runtime no-ops — informational lint surfacing the intentional or accidental no-op.

Info Compile-time Added v0.3 Option-d SPEC §51.0.F.1 (normative)

What it means

An engine self-write is @var = .CurrentVariant or @var.advance(.CurrentVariant) where the target equals the engine's currently-active variant. The v0.3 Option-d ratification (2026-05-12) carves out self-writes as runtime no-ops rather than E-ENGINE-INVALID-TRANSITION violations (the pre-Option-d behavior). The W- prefix is legacy naming; the severity is Info — informational only.

The lint surfaces two firing patterns:

  • STRICT (inside-state-child fire): .CurrentVariant matches the enclosing state-child tag. The static check is exact — the write target IS the surrounding state.
  • CONSERVATIVE (outside-state-child fire): the write site is outside any state-child body AND .CurrentVariant is a declared variant of the engine. The current state at runtime is dynamic, but the literal `.Variant` write target is statically known — the lint surfaces the pattern where it WILL be a no-op IF the engine happens to be in that variant.

What a self-write skips

Per §51.0.F.1, a self-write is a runtime no-op:

  • No <onTransition> handlers fire.
  • No effect= attribute fires.
  • No history capture per §51.0.N (the history attribute on composite state-children).
  • No <onTimeout> rearm (per §51.0.M, a self-write does NOT re-enter the state-child).
  • No <onIdle> watchdog reset (per §51.0.R).
  • No subscriber notification — downstream reactive cells that read the engine variable do NOT recompute.

Minimal reproducer

type Phase:enum = { Active, Idle }

function refresh() {
  @phase = .Active
  //          ^^^^^^^
  //          W-ENGINE-SELF-WRITE-DETECTED (CONSERVATIVE):
  //          .Active is a declared variant; if @phase is currently
  //          .Active, this write is a runtime no-op.
}

<engine for=Phase initial=.Active>
  <Active rule=.Idle>
    <button onclick=refresh()>I'm here</button>
  </>
  <Idle/>
</>

When the lint is signal vs noise

Intentional self-write (lint is noise)

A defensive set(.Current) reachable from multiple variants — e.g., a "reset to Active state" handler that runs from any state including the Active state. The no-op-when-already-Active behavior is correct. No action required; the lint is informational.

Accidental self-write (lint is signal)

A state change was expected unconditionally — but the write target equals the current variant by mistake. The adopter intended different state, but a copy-paste, typo, or stale code-shape produced a no-op. The lint surfaces "this write does nothing" so the bug is visible at compile time instead of as silent-runtime-no-effect.

Suppression

  1. Rephrase via a derived cell. Avoid the literal .Variant form by computing the target dynamically: @phase = computeNext(). The compile-time literal-match no longer fires.
  2. Remove the write if the no-op behavior is accidental and the surrounding logic doesn't actually need the assignment.
  3. Suppress per SPEC §28 if the lint is noise in this specific call site. Config key: lint.engine-self-write-detected.

Related

Specification

Normative text: SPEC §51.0.F.1 (idempotent self-write semantics — v0.3 Option-d synthesis), §51.0.F (rule= contract — the pre-Option-d error this lint replaces), §28 (lint suppression configs), §34 catalog row. Spec lives at compiler/SPEC.md .