Skip to content

[AS-SDK] Phase 5 — @view/@mutating/@payable intent transform - #23

Merged
zarah-s merged 1 commit into
mainfrom
as-sdk/phase-5-intent-transform
Aug 6, 2026
Merged

[AS-SDK] Phase 5 — @view/@mutating/@payable intent transform#23
zarah-s merged 1 commit into
mainfrom
as-sdk/phase-5-intent-transform

Conversation

@manoahLinks

Copy link
Copy Markdown
Member

Closes #10.

Adds the hybrid's ergonomic top layer: authors mark what an entry point does, next to the logic that does it, and an asc plugin holds those markers up against [functions.*] in otigen.toml.

@view      export function __get_impl(): u64 {  }
@mutating  export function __increment_impl(): u64 {  }
@payable   export function __deposit_impl(): void {  }

Where it lives

The issue called for a separate assemblyscript-transform/ package scoped @pyde-net/asc-transform. It ships instead inside @pyde-net/host, on the /transform subpath — one package per language directory, matching the go/ consolidation in #18. Two things fall out of that: the scaffold already depends on the SDK, so wiring it costs one asconfig.json line and no second install; and there is no publish-ordering dance between two packages that must move together.

{ "options": { "transform": ["@pyde-net/host/transform"] } }

The manifest stays authoritative

The plugin injects no schema, reads no [state] / [events], and feeds nothing back into the compile. It collects the markers, strips them from the AST, and either agrees with the manifest or fails the build:

FAILURE IntentMismatch: function intent in AssemblyScript disagrees with otigen.toml

  1. assembly/index.ts:106 — `increment`
       source      : @view — declares this entry never writes state
       otigen.toml : …/otigen.toml:46: attributes = ["entry"] — no "view", so the ABI marks it mutating
       fix         : drop @view, or add "view" to [functions.increment].attributes

Annotation is opt-in per function, so an unannotated contract — every contract written before this — is not checked at all.

Marker Required in attributes Attribute present, marker absent
@view view not an error — silence claims nothing
@mutating no view not an error
@payable payable error
@entry entry not an error

@payable is the one symmetric rule: on an annotated function, saying nothing about value reads as "this entry refuses value" — a safety claim the manifest may not be making.

Acceptance criteria

  • An annotated contract builds via otigen build; the ABI is untouched because it never comes from the markers.
  • @view on a fn the manifest marks mutating fails otigen build (exit 2) citing both sides — verified on a freshly-generated scaffold, not just in tests.
  • Removing the transform from asconfig.json still yields a correct, deployable contract — asserted byte-for-byte: annotated-with-plugin and unannotated-without both produce 634b5a85… on examples/counter-as.
  • Payable accept/reject on devnet. Not run here: those flags come from [functions.*] via otigen build, which this plugin cannot influence in either direction.

Two bugs worth reading

Both were found by building a real contract, not by the unit tests, and both fail silently — the worst failure mode a drift checker can have, because every mismatch sails through unchecked:

  1. NodeKind is a const enum. tsc inlines one compiler's numeric ids at build time; FunctionDeclaration moved 55 → 56 between asc 0.27 and 0.28, so the walk matched nothing.
  2. instanceof assumes a single assemblyscript install. It goes quietly false whenever the project resolves a different copy than the plugin does — exactly what a linked or unhoisted SDK produces, which is what this repo's own examples use.

The walk is now structural: no node kinds, no class identity. Regressions for both: a plain-object AST fixture that is not an assemblyscript instance and carries no kind ids, plus a CI step that re-runs the suite against the far end of the supported range.

Version policy

Supported: asc >= 0.27.30, < 0.29.0, checked against the compiler actually running the build. Outside it the plugin warns and stands down rather than reading an AST it cannot vouch for — an asc bump can cost the sugar, never the substrate.

Notes for review

  • The manifest reader is dependency-free on purpose. It runs inside asc on the build path of every opted-in contract; a TOML dependency would put a third-party package inside the trust boundary of a check whose whole job is to tell the author the truth about their manifest. It throws — rather than guessing — on an attributes value it cannot parse, since silently ignoring one turns a real mismatch into a pass.
  • assemblyscript/package.json gains "type": "module" and an exports map. asc resolves AS sources by file path and never consults exports, so @pyde-net/host/assembly/* is unaffected; the map exists for Node, which is what loads the plugin.
  • 29 tests, green against both asc 0.27.37 (locked) and 0.28.20 (what scaffolds pin).

Follow-ups

  • pyde-net/otigen#TBD wires this into the scaffold. It pins ^0.1.0-alpha.15, so this must publish first — npm is on alpha.14.
  • pyde-net/otigen-templates holds the actual otigen init --lang as contract starter and needs the same wiring; it also carries stale /assembly host-fn imports that no longer compile against alpha.14, independent of this work.

…otigen.toml

Adds the ergonomic top layer for AssemblyScript contracts: authors mark
what an entry point DOES, next to the logic that does it, and an `asc`
plugin holds those markers up against `[functions.*]`.

    @view      export function __get_impl(): u64 { ... }
    @mutating  export function __increment_impl(): u64 { ... }
    @payable   export function __deposit_impl(): void { ... }

It ships inside `@pyde-net/host` on the `/transform` subpath rather than
as a second npm package — one package per language directory, like go/,
and the scaffold already depends on the SDK, so wiring it costs an
asconfig line and no new install.

The manifest stays authoritative. The plugin injects no schema, reads no
[state] / [events], and feeds nothing back into the compile: it collects
the markers, strips them from the AST, and either agrees with the
manifest or fails the build citing both sides and the line each claim
sits on. Annotation is opt-in per function, so an unannotated contract —
every contract written before this — is not checked at all.

Two properties are asserted byte-for-byte in the suite, because they are
the design rather than a nice-to-have:

  * Annotating a contract produces identical wasm.
  * Deleting the transform from asconfig.json still builds that same
    deployable wasm from the Phase 2-4 substrate. Only the cross-check
    is lost, which is why an `asc` bump can at worst disable the sugar:
    outside the supported range (>=0.27.30, <0.29.0) the plugin warns
    and stands down instead of reading an AST it cannot vouch for.

The AST walk is deliberately structural — no `NodeKind`, no
`instanceof`. Both alternatives fail SILENTLY, which is the one failure
mode a drift checker must not have: `NodeKind` is a const enum, so tsc
bakes in one compiler's numeric ids (FunctionDeclaration moved 55 -> 56
between 0.27 and 0.28) and the walk then matches nothing; `instanceof`
goes quietly false whenever the project resolves a different
`assemblyscript` install than the plugin does, which is exactly what a
linked or unhoisted SDK produces. Both were real, both were caught by
building an actual contract rather than by the unit tests, and both now
have regressions: a plain-object AST fixture, and a CI step that re-runs
the suite against the far end of the supported range.

The manifest reader is dependency-free on purpose. It runs inside `asc`
on the build path of every contract that opts in, and a TOML dependency
would put a third-party package inside the trust boundary of a check
whose whole job is to tell the author the truth about their manifest. It
reads the slice of TOML a manifest can express and throws — rather than
guessing — on an `attributes` value it cannot parse, since silently
ignoring one would turn a real mismatch into a pass.

Closes #10
@zarah-s
zarah-s merged commit 40f64b0 into main Aug 6, 2026
0 of 5 checks passed
zarah-s added a commit that referenced this pull request Aug 6, 2026
)

Two follow-ups from the #23 review.

F1 — a decorator one edit away from a marker (@veiw, @mutatng, @payble)
was silently left on the AST and the function it meant to constrain
compiled unchecked — the one silent-miss that most undermines the
'checked against otigen.toml' promise. partitionDecorators now takes an
optional warn callback and, for a non-marker decorator within one edit
(transposition included) of a real marker, tells the author it is being
left unchecked and what they likely meant. Purely diagnostic — the wasm
is unaffected, and a real marker or an asc built-in (@inline) never trips
it. Covered by two collectIntents tests.

F2 — the asc dep floor (^0.27.0 / peer >=0.27.0) sat below the range the
transform actually enforces (>=0.27.30). A clean install resolving
0.27.0-0.27.29 would stand the transform down while the tests expect it
to run. Both the devDep and the peer range now start at 0.27.30 and cap
below 0.29.0, matching SUPPORTED_ASC_RANGE.

F3 (unconventional manifest shapes) is left as-is: the parser reads only
[functions.NAME] headers by design — a tested choice — it is fail-closed
with an actionable error, and otigen only ever emits headers.

Suite: 31/31 on asc 0.27.37 and 0.28.20.
zarah-s added a commit that referenced this pull request Aug 6, 2026
First npm release carrying the intent transform (#23) and its follow-ups
(#28): @view/@mutating/@payable checked against otigen.toml, plus the
near-miss marker warning and the tightened asc floor. assembly/ (raw.*
escape hatch, ctx/exit/hash wrappers) ships unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[AS-SDK] Phase 5 — Intent transform (@view/@mutating/@payable)

2 participants