Skip to content

docs: document function/method modifiers and compiler annotations#1340

Merged
mbouaziz merged 4 commits into
mainfrom
docs-modifiers-annotations
Jul 16, 2026
Merged

docs: document function/method modifiers and compiler annotations#1340
mbouaziz merged 4 commits into
mainfrom
docs-modifiers-annotations

Conversation

@mbouaziz

@mbouaziz mbouaziz commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Adds skiplang/docs/modifiers-and-annotations.md. There was no reference for either, and the two are easy to conflate:

  • A modifier is a keyword. The grammar and the type system check it, and reject what they don't understand.
  • An annotation is a free-form string. The compiler never validates it — the parser takes @ plus any identifier, and every consumer does an exact string match.

That distinction has teeth. This compiles clean, with no error and no warning:

@totally_bogus_annotation
@debbug                    // typo for @debug — silently does nothing
fun helper(): Int { 42 }

which for @debug means quietly restoring CSE.

Method

Every claim is derived from the compiler source rather than from the declaration syntax, and the surprising ones were checked by compiling a program. A fact-checking pass against the source caught six real errors in my first draft — including the overridable rule and two rows of the receiver table — which are fixed here.

Notes a reader is most likely to get wrong

These are the ones worth reviewing closely:

  • @debug only suppresses CSE, and is non-transitive — so a plain wrapper around a @debug call can still be merged. untracked is the transitive one, because it lives in the function's type. (This is the distinction behind Mark side-effecting stdlib native funs @debug so they aren't CSE'd #1337.)
  • @may_alloc / @no_throw / @no_return are read only for bodiless functions and are silently dead otherwise. The prelude has live examples: print_string carries @no_throw and skipExit carries @no_return, both over real bodies. @may_alloc's absent-default is the unsafe direction — a native function without it is assumed to allocate nothing.
  • Receiver modality: the unmodified default is not the permissive option — readonly is. frozen additionally demands a deeply frozen receiver and fails as a constraint, not at the modality check.
  • Methods are final by default, but only when they have a body. Abstract members must be overridden, and marking one overridable is an error. There is no final funfinal is a constructor modifier.
  • Module-level private parses but is not enforced (the check is dead code), so it's flagged as an intent marker rather than a guarantee. private at global scope is rejected.
  • async type-checks but does not codegen — awaiting one fails with an ICE, and the async tests are disabled — so it's marked unusable rather than documented as if it worked.

@anything and @param are deliberately excluded: both look like annotations but are internal artifacts that cannot be written.

Also worth knowing

Unknown annotations aren't an oversight — they're the extension point. The compiler indexes functions by annotation, and #forEachFunction (@foo, ...) enumerates them. sktest's @test is exactly this and is not known to the compiler at all.

Note for reviewers

const.sk:363-364 has a comment saying @no_inline/@debug mean "don't discard calls as dead code either". That comment is stalealwaysLive in deadcode.sk makes every call unconditionally live, so no call is ever DCE'd and @debug has nothing to do with it. The doc says CSE-only, which is correct; please don't let that comment prompt a "correction".

🤖 Generated with Claude Code

— Claude Opus 4.8 (1M context)

mbouaziz and others added 4 commits July 16, 2026 10:30
There was no reference for either, and the two are easy to conflate: a
modifier is a keyword that the grammar and the type system check, while an
annotation is a free-form string that the compiler never validates. That
distinction has teeth — a misspelled annotation is silently accepted and
does nothing, which for @debug means quietly restoring CSE.

Every claim was derived from the compiler source rather than from the
declaration syntax, and the surprising ones were checked by compiling a
program. The notes that survived that pass are the ones a reader is most
likely to get wrong:

- @debug only suppresses CSE, and is non-transitive, so a plain wrapper
  around a @debug call can still be merged; untracked is the transitive
  one, because it lives in the function's type.
- @may_alloc/@no_throw/@no_return are read only for bodiless functions and
  are silently dead otherwise — print_string and skipExit both carry one
  over a real body. @may_alloc's absent-default is the unsafe direction.
- Receiver modality: the unmodified default is not the permissive option,
  readonly is; frozen additionally demands a deeply frozen receiver, and
  fails as a constraint rather than at the modality check.
- Methods are final by default, but only when they have a body: abstract
  members must be overridden, and marking one overridable is an error.
  There is no `final fun` — final is a constructor modifier.
- Module-level private parses but is not enforced, so it is flagged as an
  intent marker rather than a guarantee.
- async type-checks but does not codegen, so it is marked unusable rather
  than documented as if it worked.

@anything and @param are excluded: both look like annotations but are
internal artifacts that cannot be written.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The collection primitives are disabled in the runtime: obstack.c has a
"Collection primitive (disabled)" section, SKIP_Obstack_collect is empty,
and the preamble's inline entrypoints return without doing anything. The
compiler still runs the local-GC pass and emits the calls, so the
annotations do tell the compiler something, but nothing collects.

Without this a reader of @gc would reasonably conclude that collection
happens. Raised as a question on #1349, since the source says the state is
deliberate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The table entry for @cpp_runtime named the registration but not what it is
for or when a native declaration should prefer it. Adds a subsection: the
two are identical at the call site, the one behavioural difference is that
a native, non-generic @cpp_runtime function is registered so a later
compiler pass can synthesise a call to it by name (the coroutine and const
lowering hooks depend on this), and by convention @cpp_runtime also marks
Skip-runtime functions whose body is a portable fallback the native back
end replaces — where, being generic, they get only the convention.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The GC caveat said the annotations have no runtime effect. That is true of
the automatic path, but @gc is worse: it selects out-of-line collection
entrypoints, and SKIP_Obstack_collect0 is defined nowhere, so any use of
@gc fails to link. Corrected and linked to #1355.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mbouaziz
mbouaziz force-pushed the docs-modifiers-annotations branch from a6c126b to 25c57e4 Compare July 16, 2026 08:30
@mbouaziz
mbouaziz enabled auto-merge July 16, 2026 08:31
@mbouaziz
mbouaziz merged commit 28d4dad into main Jul 16, 2026
3 checks passed
@mbouaziz
mbouaziz deleted the docs-modifiers-annotations branch July 16, 2026 08:31
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.

1 participant