Skip to content

Add experimental when clauses on auto decorators - #11806

Draft
Timothee Guerin (timotheeguerin) wants to merge 2 commits into
microsoft:mainfrom
timotheeguerin:conditional-scopes
Draft

Add experimental when clauses on auto decorators#11806
Timothee Guerin (timotheeguerin) wants to merge 2 commits into
microsoft:mainfrom
timotheeguerin:conditional-scopes

Conversation

@timotheeguerin

Copy link
Copy Markdown
Member

Emitters targeting different languages routinely need different metadata for the same declaration. Today that is expressed by inventing a decorator per emitter, and the spec ends up carrying one @clientName-shaped decorator per target.

This adds a when suffix on auto decorator applications, so one declaration can carry per-scope metadata:

@clientName("Widget") when language("csharp") | language("java")
@clientName("widget") when language("python")
@clientName("Thing")
model Widget {}

Emitters read the value for their own scope. EmitContext.scope is prefilled with the emitter's package name; createScope({ language, target }) narrows it for emitters serving several languages from one package:

const name = getAutoDecoratorValue(program, "MyLib.clientName", model, context.scope);

Gated behind the scoped-decorators feature flag.

Why this isn't conditional compilation

That was the main risk, and two properties are load-bearing:

  • Validation is unconditional; only storage is conditioned. A typo inside a when-scoped decorator is an error even when no emitter will ever select that scope.
  • Conditioned values live in a parallel state map (dec-scoped:<fqn>). The unscoped getAutoDecoratorValue(program, fqn, target) returns exactly what it returned before, so the autoextern migration contract is untouched and scope-unaware emitters are unaffected.

when is a contextual keyword, so it remains usable as an identifier — no migration needed.

Drive-by fix: realm clone provenance

Realm clones had no back-pointer to the type they were cloned from (Realm.realmForType maps type→realm; getTypeAtVersion only goes forward). Because StateMapRealmView.#select() routes on realm.hasType(), any state-map read against a version-projected clone silently returned undefined — versioning only works today because it reads from original by hand.

Realm.sourceForType + Realm.sourceOf() fix this generally, and are what make scoped metadata compose with versioning.

Scope of this PR

This is Phase 1 onlywhen on auto dec applications. It deliberately never touches the symbol table or the type graph, which is why it's safe.

I spiked the structural phases separately before proposing this; findings are written up in #10551. The short version: Phase 2 as specified in the design doc is conditional compilation and should not be built as written, but a narrower present-or-absent form is viable. That discussion belongs on the issue, not here.

Known gap

Dimension overlap is unresolved: emitter("@typespec/http-client-csharp") and language("csharp") both match the C# emitter. The design doc says "error on overlapping conditions", but this overlap is inherent, not accidental — an error is the wrong answer. Today the topmost application wins. This needs a specificity rule before the flag comes off.

Breaking change

EmitContext gained required scope / createScope members, which breaks hand-rolled contexts (one in-repo call site in openapi3 is fixed here). Same class as the earlier perf addition, but calling it out.


Compiler suite 4223 passed (baseline 4172) with zero regressions; versioning 137 and openapi3 2583 also green.

Refs #10551

Emitters targeting different languages routinely need different metadata for
the same declaration, and today that is expressed by inventing a decorator per
emitter. This adds a `when` suffix on `auto` decorator applications so a single
spec can carry per-scope metadata:

    @clientName("Widget") when language("csharp") | language("java")
    @clientName("widget") when language("python")
    @clientName("Thing")
    model Widget {}

Emitters read the value for their own scope. `EmitContext.scope` is prefilled
with the emitter's package name and `createScope()` narrows it for emitters
that serve several languages from one package.

Two properties keep this out of conditional-compilation territory:

- Validation is unconditional; only storage is conditioned. A typo inside a
  `when`-scoped decorator is an error even when no emitter selects that scope.
- Conditioned values live in a parallel state map keyed `dec-scoped:<fqn>`, so
  the existing `getAutoDecoratorValue(program, fqn, target)` contract - and the
  `auto` <-> `extern` migration story - are untouched.

`when` is a contextual keyword, so it remains usable as an identifier.

Also fixes a pre-existing gap this depends on: realm clones had no back-pointer
to the type they were cloned from, so any state-map read against a
version-projected clone silently returned `undefined`. `Realm.sourceForType` /
`Realm.sourceOf()` make scoped metadata compose with versioning.

Gated behind the `scoped-decorators` feature flag.

Refs: microsoft#10551
@pkg-pr-new

pkg-pr-new Bot commented Sep 1, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@typespec/compiler@11806
npm i https://pkg.pr.new/@typespec/openapi3@11806

commit: 726ab0d

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

❌ There is undocummented changes. Run chronus add to add a changeset or click here.

The following packages have changes but are not documented.

  • @typespec/openapi3

The following packages have already been documented:

  • @typespec/compiler
Show changes

@typespec/compiler - feature ✏️

Add experimental when clauses on auto decorator applications, letting a single spec carry different metadata per emitter, language, or target. Enable with the scoped-decorators feature flag.,> ,> tsp,> @clientName("Widget") when language("csharp") | language("java"),> @clientName("widget") when language("python"),> @clientName("Thing"),> model Widget {},> ,> ,> Emitters read the value for their own scope; EmitContext.scope is prefilled with the emitter's package name and EmitContext.createScope() narrows it:,> ,> ts,> const value = getAutoDecoratorValue(program, "MyLib.clientName", model, context.scope);,> ,> ,> Decorator arguments are still validated in every scope — only the stored value is conditioned — and the unscoped getAutoDecoratorValue(program, fqn, target) overload is unchanged.

@azure-sdk-automation

azure-sdk-automation Bot commented Sep 1, 2026

Copy link
Copy Markdown

You can try these changes here

🛝 Playground 🌐 Website 🛝 VSCode Extension

@ArcturusZhang

Copy link
Copy Markdown
Member

Does this when clause support priority?
In real world scenarios, the C# provisioning library needs two scopes - we need it to just accept "csharp" scope to get the renamings from mgmt library, and we need its own scope to override this when something special happens. In this case, if we add csharp client name clause, it will eventually go into mgmt library unexpectedly.

@timotheeguerin

Copy link
Copy Markdown
Member Author

Does this when clause support priority? In real world scenarios, the C# provisioning library needs two scopes - we need it to just accept "csharp" scope to get the renamings from mgmt library, and we need its own scope to override this when something special happens. In this case, if we add csharp client name clause, it will eventually go into mgmt library unexpectedly.

This is still an experiment but yes there should be some priority, most likely specified by the emitter/library where you specify under which scope you want to see the type graph.
However how does that really affect you? An azure spec should only be for mgmt or data plane so the scope shouldn't need to differentiate no?

Also, while language() and emitter function might exists I don't think this will be the way we'll be using them in Azure, we'll probably have a shared enum of scope or something like that

enum AzureScopes {
  csharp,
  python,
  javascript,
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

compiler:core Issues for @typespec/compiler emitter:openapi3 Issues for @typespec/openapi3 emitter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants