docs: document function/method modifiers and compiler annotations#1340
Merged
Conversation
This was referenced Jul 15, 2026
Open
Open
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
force-pushed
the
docs-modifiers-annotations
branch
from
July 16, 2026 08:30
a6c126b to
25c57e4
Compare
mbouaziz
enabled auto-merge
July 16, 2026 08:31
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
skiplang/docs/modifiers-and-annotations.md. There was no reference for either, and the two are easy to conflate:@plus any identifier, and every consumer does an exact string match.That distinction has teeth. This compiles clean, with no error and no warning:
which for
@debugmeans 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
overridablerule 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:
@debugonly suppresses CSE, and is non-transitive — so a plain wrapper around a@debugcall can still be merged.untrackedis 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_returnare read only for bodiless functions and are silently dead otherwise. The prelude has live examples:print_stringcarries@no_throwandskipExitcarries@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.readonlyis.frozenadditionally demands a deeply frozen receiver and fails as a constraint, not at the modality check.overridableis an error. There is nofinal fun—finalis a constructor modifier.privateparses but is not enforced (the check is dead code), so it's flagged as an intent marker rather than a guarantee.privateat global scope is rejected.asynctype-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.@anythingand@paramare 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@testis exactly this and is not known to the compiler at all.Note for reviewers
const.sk:363-364has a comment saying@no_inline/@debugmean "don't discard calls as dead code either". That comment is stale —alwaysLiveindeadcode.skmakes every call unconditionally live, so no call is ever DCE'd and@debughas 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)