Skip to content

docs(emitters): state Kotlin's reserved sets for units - #1335

Merged
OmarAlJarrah merged 84 commits into
mainfrom
docs/emitters-kotlin-reserved-sets
Nov 4, 2026
Merged

OmarAlJarrah merged 84 commits into
mainfrom
docs/emitters-kotlin-reserved-sets

Conversation

@OmarAlJarrah

@OmarAlJarrah OmarAlJarrah commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Summary

docs/emitters/refine/reserved-names.md leaves three scopes to each target. Kotlin's naming document stated only the type scope, so a Kotlin emitter had nothing to reserve for a unit's module or package, or for what the SDK declares beside the spec's names in a unit, and T-19 had no fixture to name. Each answer below comes from a build, not from documentation.

Kotlin's modules sit in modules/<name>, not at the build's root. A module at the root meets every name Gradle and its plugins keep there, and that set has no fixed end:

  • build is the root project's output: the module builds, but gradle clean on the root deletes its sources. On a case-insensitive file system such as macOS's the same happens to a module named Build.
  • buildsrc fails the build on macOS, where Gradle finds it as buildSrc, its build-logic build, and runs :buildSrc:compileKotlin.
  • gradlew makes gradle wrapper fail ("Cannot write a file to a location pointing at a directory").
  • api shares its directory with the root project's ABI dump, which the Kotlin Gradle plugin's ABI validation writes to api/ and surface verification reads. gradle and src share theirs with the wrapper's files and the root's sources.

A Kotlin package name does not depend on its directory, so nesting the modules renames nothing. Under modules/, modules named build, buildsrc, BuildSrc, gradle, gradlew, src and api all build, and clean and wrapper keep their sources.

The units: buildSrc, the root project's name, and Windows' device names. These are what a module's project still meets:

  • Gradle refuses buildSrc as a project's name wherever its directory is ("'buildSrc' cannot be used as a project name as it is a reserved name"). Its check is "buildSrc".equals(name), so buildsrc and BuildSrc pass, and it is the only reserved project name in Gradle 9.8.2.
  • A module named as the root project, under the root's Maven group, has the root's coordinates, so Gradle resolves the root's dependency on it to the root itself ("Circular dependency between the following tasks"). This happens at the root and under modules/, and with a distinct group the same build passes.
  • Windows refuses con, prn, aux, nul, and com or lpt with a digit as a file or directory name, in any letter case and whatever the extension. So neither a module named aux nor the directory kotlinc writes its package's classes to could be made there. These come from Windows' file-naming rules, not from a build on Windows, and the document says so. Go's own module checks refuse the same list on any host.

A unit's package segment is its words in lowercase, joined with no separator (useraccounts), as Go's package names are. No document said how Kotlin cases one, and every units comparison turns on it. A camel-cased segment would let customHostname and customhostname be two units that a case-insensitive file system makes one directory. Lowercase makes them render alike and collide, and the collision is resolved by ID.

Not units names: hard keywords, kotlin and java.

  • Every one of Kotlin's 28 hard keywords, backticked, compiles as a package segment in a package and an import on the JVM. fun, object, when, is, in and typealias also compile for JS and macOS Native and link into an Objective-C framework. All 49 soft and modifier keywords (value, data, import, internal…) compile there unescaped. Keywords take the existing escape.
  • kotlinc refuses kotlin as a package's first segment ("Only the Kotlin standard library is allowed to use the 'kotlin' package"). The JVM refuses java there: kotlinc compiles package java.probe, and loading the class throws SecurityException: Prohibited package name: java.probe. Only a name the options give can begin a package, so a root beginning with either is now an options error.

A unit: Client, each sub-client, what a runtime template declares beside the spec's names, the default imports the generated code writes, and the Windows device names. The template guards already hold a template to the names it declares, so the set comes from the templates. ApiFailure, the exception tree, Response, EnvelopedResponse, RawBody, Optional, Transport and the rest are listed as examples, not kept as a second list. No document places the runtime, and the root cannot hold it: the root holds the client, which reaches every group's module, so a runtime there closes a cycle under SplitByGroup or Partition that Gradle and Go both refuse. That choice is #1489's. The set says so and does not depend on it. The device names are reserved here as well, since the JVM writes a top-level class to a file of its name (Aux.class).

A top-level declaration also shadows a Kotlin default import for its whole package. With a model List in a package, List<String> there failed to compile ("No type arguments expected for 'class List : Any'"), as did Map<String, Long> beside a model Map, Result<Int> beside a model Result, and a class extending Exception beside a model Exception. The unit set now reserves each default-imported name any refine step or template can write. Each declares those it writes, and the template lint holds a template to them. The set is their union for the target's version, so adding an operation renames no model.

On Multiplatform the target's names are reserved in the Objective-C namespace of every unit. The Objective-C export drops a declaration's package and joins a nested class's name to its outer's. So a framework gives every unit's declarations, nested ones among them, one namespace, and of two sharing a name it appends _ to one:

  • With the runtime's ApiException in p.runtime beside a model p.accounts.ApiException, the export named the runtime's exception ApiException_, so a Swift caller catching ApiException would catch the model.
  • A model PetDog exported to Swift as PetDog_ beside the class Dog that a sealed interface Pet nests, which Swift also calls PetDog. A union's fallback, Payment.Unknown, is such a class.
  • A model UserBuilder exported to Objective-C as UserBuilder_ beside User.Builder, whose Swift name keeps its nesting.

So the joined names of the public classes the target nests in a type (UserBuilder, UserCompanion, PaymentUnknown) are reserved too. The flattening holds across Gradle modules: with the runtime in its own module under modules/ and a group module declaring a model ApiException, both exported into one framework, the runtime's class was again the one renamed. The remedy, an explicit @ObjCName, is experimental in Kotlin 2.4.0 and fails to compile without an opt-in. With @file:OptIn(ExperimentalObjCName::class) the spec's class exported as AccountsApiException and the runtime kept ApiException. A caller in another package compiled without opting in, so swift.md now says each such file opts in.

Swift's and Foundation's own names are reserved for the Swift names a framework exports. A Swift caller has both in scope, and the export renames neither side. Typechecked with Swift 6.2.3 against a Kotlin 2.4.0 framework:

  • A class exported as Optional, Result or Duration shadowed Swift's own in the caller's file: cannot specialize non-generic type 'Optional', type 'Duration' has no member 'seconds'.
  • One exported as Decimal, Date, Data or URL made the name ambiguous with Foundation's: 'Decimal' is ambiguous for type lookup in this context.

Importing the SDK broke code that never touched it. The runtime itself declares Optional, Duration and, on Multiplatform, Decimal, so every Multiplatform SDK did this. Those three now take Swift names of their own, and the names are a table of the options version.

A caller's module qualification keeps working. A Swift caller settles an ambiguity by naming the module (Probe.Image, SwiftUI.Image), and that fails once a type takes a module's name. A class Probe in the framework Probe turned Probe.Client() into type 'Probe' has no member 'Client', and a class Foundation turned Foundation.Date into 'Date' is not a member type of class 'Probe.Foundation'. The framework's own name, Swift and Foundation are now reserved among the Swift names. A module a caller imports by choice is left to that qualification, which the rule now states as its boundary. In a SwiftUI file, let i: Image was ambiguous beside a framework class Image, while SwiftUI.Image and Probe.Image resolved and SwiftUI's own Image(systemName:) and Color.red compiled.

The export's own Objective-C names are reserved. The export declares classes under the framework's prefix: ProbeBase, ProbeMutableSet, ProbeMutableDictionary, ProbeNumber and a box per Kotlin number. A spec class Base, MutableSet or MutableDictionary was renamed beside them (ProbeBase_). One named Number, Boolean, Byte, Short, Int, Long, Float, Double or an unsigned number's took the same Objective-C name, and the framework failed to link: Linking globals named 'OBJC_METACLASS_$_ProbeNumber': symbol multiply defined!. Removing each in turn named all twelve, and an explicit @ObjCName linked. T-19 now requires that the framework links. A declaration that meets one only in that namespace keeps its Kotlin name and takes an explicit Objective-C and Swift name, so no JVM name moves for Swift's sake. swift.md now counts a name another declaration exports among the names the export would mangle, and output-units.md no longer says two units' declarations never collide.

A unit name the options give that cannot yield is refused. A partition key mapping to a name the units' scope reserves (shared, a Kotlin module's buildSrc, a Go package's internal) was neither escaped nor refused. It is now an options error, which T-18's partition fixtures draw.

The shared scopes now admit what a file system refuses and what a default import supplies. reserved-names.md defined the units' scope as what "a target's toolchain treats apart", and a unit's as the names the target declares there. Neither admitted Windows' device names or a shadowed default import, nor did the inputs EscapeReservedWords lists in steps.md. They now do.

T-19 now names types after the reserved names, in the root and in a group's unit, since only a type meets the unit scope. It names groups after every units name and every name a Kotlin build keeps at its root. It requires that clean and wrapper keep every module's sources, and that no path a Kotlin build writes has a Windows device name for a segment, which a build on macOS or Linux would otherwise pass. It also requires that the framework's header give each declaration the Swift name the naming policy chose, none of them a Swift or Foundation type, so that a Swift file importing the framework and Foundation compiles. Every template's declared names are held to the default-imported names it writes.

On the golden spec (spec @70c631aa, morphic @71e08b61), the 595 groups by first tag normalize to 593 names. Of those, shared meets the shared reservation, and none meets buildSrc or a Windows device name, nor does any type. No named type meets a runtime name, a Kotlin default import, a Swift or Foundation type, an export's own class name or a module's name, and none meets a nested class's joined name: the one name ending in Builder, email-sending_EmailBuilder, has no model email-sending_Email beside it. One group, api (4 operations, among them POST /v1/verify), would have shared the root's ABI dump directory had modules sat at the root.

Closes #1187

Out of scope

Test plan

  • Probes with Kotlin 2.4.0, Gradle 9.8.2 and Corretto 25 on darwin/arm64, all offline:
    • JVM: all 28 hard keywords, backticked, and all 49 soft and modifier keywords, unescaped, as package segments imported from another package, compile and run. package probe.kotlin compiles, package kotlin.probe fails with the error above, and package java.probe compiles and throws SecurityException when loaded.
    • Multiplatform: the 6 keyword packages pass compileKotlinJvm, compileKotlinJs, compileKotlinMacosArm64 and linkDebugFrameworkMacosArm64, and the header declares all 6 classes. Same-named classes in two packages export as ApiException/ApiException_ and User/User_. With the runtime's class in p.runtime and a model in p.accounts, the runtime's was the one renamed.
    • Modules at the root: gradle, src, shared and fun build. build and Build build, and clean removes their sources. buildsrc fails, with and without a plugin version in its build file. gradlew builds, and wrapper fails. api builds, and the root's ABI dump lands at api/<root>.api.
    • Modules under modules/: build, buildsrc, BuildSrc, gradle, gradlew, src and api build, and clean and wrapper keep every module's sources. buildSrc is refused as a project name.
    • A module named as the root project, in the root's group, fails on a cycle, both at the root and under modules/, and passes with a group of its own. A module named modules builds under modules/.
    • The Objective-C header of a framework holding User.Builder, User.Companion and the sealed Pet.Dog, beside top-level UserBuilder, PetDog and p.accounts.UserCompanion, names them as above.
    • Kotlin/Multiplatform with the runtime and a group as Gradle modules under modules/, both exported into one framework: compileKotlinJvm and linkDebugFrameworkMacosArm64 pass, and the runtime's ApiException exports as ApiException_. @ObjCName without an opt-in fails to compile, and with a file-level @OptIn the explicit name holds and a caller elsewhere needs none.
    • Models named List, Map, Exception and Result in a package break List<String>, Map<String, Long>, a subclass of Exception and Result<Int> there. A property named class compiles and runs, its getter String getClass() apart from Object.getClass().
    • A Swift 6.2.3 file importing Foundation and a framework exporting Optional, Result, Duration, Decimal, Date, Data and URL fails as quoted above in a type annotation of each of the seven, while Date(), Data() and URL(string:) as expressions still meant Foundation's.
    • A framework holding classes Number, Boolean, Byte, Short, Int, Long, UByte, UShort, UInt, ULong, Float and Double fails to link, each named in turn by the linker, and links once they are gone, while Base, MutableSet and MutableDictionary export as ProbeBase_, ProbeMutableSet_ and ProbeMutableDictionary_. @ObjCName on Number links. A Swift file qualifying Probe.Client() and Foundation.Date fails beside classes Probe and Foundation, and classes Type and Protocol cause nothing.
    • Under modules/, modules named kotlin, java, test, main, clean, help, tasks, init, dependencies, projects, properties, wrapper, assemble, check and jar build, and clean and wrapper succeed.
    • golang.org/x/mod/module refuses aux.go, Aux.go, con.go and lpt1.go and the import path example.com/sdk/aux, and accepts aux_test.go. Go 1.27.3 refuses to build a module importing example.com/sdk/aux on darwin. A root project holding Client and the runtime, with a module throwing the runtime's exception, fails on a cycle. Go 1.27.3 refuses the same layout (import cycle not allowed).
  • CI: the documents' tests.

An anyOf lets one value satisfy several variants, and the emitter design
lowered every untagged union to a closed sum that holds exactly one.
Where two variants admit the same value, holding the first misnames it
and drops the members only the others declare.

The plan now proves, once per union, whether two variants of an anyOf
can admit one value (TypePlan.Inclusive), by JSON kind, by finite value
sets and by a member one model requires that the other cannot hold. A
union it proves disjoint lowers as a closed sum, as a oneOf does. One it
cannot is inclusive: it holds an optional member per variant, set for
each variant the value admits, decodes into every candidate variant, and
encodes the merge, a disagreement being an encode error. A null a
nullable variant admits is held by the reference, never by a variant.

Go lowers an inclusive union to a struct with a pointer per variant and
Kotlin to a class with a nullable property per variant. T-29 pins the
round trip of a value two open models admit.
A positional model is an array on the wire, so the kinds rule now says
so and the member rule reads only models laid out by name. A stream
that ends on a terminal variant now ends on an element holding one,
among the several an inclusive union holds. A null literal variant now
lowers as a nullable variant does, so either spelling of a null member
reaches the reference. Inclusive joins the plan fields the plan-or-refine
test keeps, and the docs emitter says when a value may be several
variants at once.
plan.Policy held a function, PrimaryResponse, and Plan.Version hashed the
policy. A function has no stable bytes: json/v2 refuses it, its printed
form is an address that changes every run, and skipping it gives two
policies that pick different responses one version. The policy is now
data from a closed vocabulary: PrimaryResponse is an ordered list of
rules and DefaultFails says whether an unpicked Default is the catch-all
failure. Version hashes the document's digest without sources, a new
plan.Revision bumped with every change to Build's output, and the
policy's deterministic JSON.

EnvelopeDetect named a type nothing defined. plan.Policy.Envelopes now
lists envelopes a configuration names, none by default. Each matches a
body by the bases it reaches or by the members it declares, and says
where the value sits and which flag value means the call failed. A
matched success returns that member, keeps the envelope reachable
through WithResponse, and turns a 2xx whose flag says it failed into a
failure with no fault or kind, decoded as the operation's matching error
body. Failures and pages are read whole.

T-30 pins the plan's identity and T-31 the envelope reads.
…into docs/emitters-envelopes-policy-data

# Conflicts:
#	docs/emitters/docs-and-mock.md
#	docs/emitters/testing/mandatory-tests.md
Runtime policy was one tree per project, so an operation needing its own
timeout or retry had nowhere to say so, and a document's own SDK
configuration, kept verbatim as vendor extensions, would have been
parsed by each emitter its own way.

Policy and hints are now layers, each holding a document-wide override
and overrides by operation: what a document embeds, the project's, the
emitter's. A setting takes the highest layer that sets it, an
operation's value over its layer's document-wide one, and a declared IR
fact beats every layer. A new emitters/embed reader turns vendor
extensions a configuration maps into the lowest layer of each, decoding
each value strictly and reporting what it cannot read.

The vocabulary gains connection-error retries, Retry-After, a throttling
backoff and redirects. Whether an operation is safe or idempotent is a
plan fact, OpPlan.Effects: declared, else from its HTTP method. An enum
a client reads keeps a value it does not list, as a union does, and a
oneOf value two variants admit decodes as plan.Policy.ExclusiveMatch
says. A service hint makes a parameter a client setting; nothing is
hoisted by inference, since a shared parameter stops being shared as an
API grows, and the plan only reports candidates.
An enum whose value type has no Go constant form, a date, a uuid or a
decimal, had no Go lowering, and the type table built an enum value by
decoding its wire form, which no constant can be. Kotlin's enum class
writes every member as its @SerialName string, so a boolean or numeric
enum went out as a string its schema does not admit.

A member is now written as the JSON kind its value type has, and
decoded by comparing as JSON compares values, numbers by value. Go
declares an enum over its value type's row, and over the wire text where
the row has no constant, the codec writing the value type's JSON kind and
a Value method giving the row's type. Kotlin generates a KSerializer for
every enum that is not a string enum, and an entry of a mapped type holds
its value as that type. T-36 pins every value type on every target.
Kotlin's naming document stated only the members it gives every type,
so a Kotlin emitter had no names to reserve for a group's module or for
the declarations a unit holds beside the spec's, and T-19 no fixture.

Measured with Kotlin 2.4.0 and Gradle 9.8.2: a module named build sits
in the root project's output, which clean deletes with the module's
sources; one named buildsrc is taken for Gradle's buildSrc on a
case-insensitive file system and fails the build; gradle and src share
the wrapper's and the root sources' directories. Those four are the
units set, and a group's module is placed at the build root so the set
applies. Hard keywords need no reservation: backticked as package
segments they compile on the JVM, JS and Native and link into a
framework, and kotlin is refused only as the first segment, which the
options name. The unit set is Client, a group's sub-client and the
runtime names the root unit declares.
EmitterInput.Policy became policy.Resolved when policy gained layers and
per-operation overrides, but the refiner context still held a single
policy.Policy, so a step had no way to read an operation's own settings.
Ctx.Policy now holds the resolved policy the emitter is given.
The rule saying what a oneOf decodes a value two variants admit to
landed at the end of the section on a variant's null, which it has
nothing to do with. It belongs with how a variant is recognized, which
is also where T-35 links it.
A group's module sat at the build's root, so the units set had to hold
every name Gradle and its plugins keep there. The set as written missed
two names and a letter case. A module named api shares its directory
with the root project's ABI dump, which the Kotlin Gradle plugin writes
to api/. One named gradlew makes `gradle wrapper` fail. And a
case-insensitive file system meets build in any letter case: `clean`
deletes a module named Build, sources and all. Any plugin may claim
another name there, so no list closes the set.

Each module now sits in modules/<name>. A Kotlin package name does not
depend on its directory, so moving modules there renames nothing. Under
modules/, modules named build, buildsrc, BuildSrc, gradle, gradlew, src
and api all build, and `clean` and `wrapper` keep their sources.

What a module's project still meets is two Gradle rules on project
names. Gradle refuses buildSrc as a project's name wherever it sits. A
module named as the root project, under the root's Maven group, has the
root's coordinates, and the build fails on a cycle. Measured with Kotlin
2.4.0 and Gradle 9.8.2 on darwin/arm64.
The unit set put every runtime name in the root unit. Under SplitByGroup
or Partition no runtime can sit there: the root holds the client, which
reaches every group's module, and a group module using a runtime in the
root closes a cycle Gradle refuses ("Circular dependency between the
following tasks"). No document says where the runtime lands, so the set
now reserves what a runtime template declares in whichever unit it
renders into beside the spec's names, as the template guards already
hold it, and lists those names as examples, adding EnvelopedResponse,
which the list left out. Where the runtime lands is a decision of its
own.

The Objective-C export drops a top-level declaration's package, so a
Multiplatform framework gives every unit's declarations one namespace
and appends _ to one of two that share a name. With Kotlin 2.4.0 the
runtime's ApiException in p.runtime was exported as ApiException_ beside
a model p.accounts.ApiException, so a Swift caller catching ApiException
catches the model. On Multiplatform the target's own names are now
reserved in every unit, a top-level name two units declare is one the
export would mangle, and output units no longer says two units'
declarations never collide.

T-19 named no type after a reserved name, though only a type meets the
unit scope. It now names types in the root and in a group's unit, and
holds the framework's header to the Swift names the policy chose.
A name the options give a unit never yields, so where one meets a
reserved or refused name nothing escaped it and nothing refused it.

A partition key may map to a name the units' scope reserves: shared,
which the shared unit holds, a Kotlin module's buildSrc, or a Go package
named internal, which no caller outside the SDK may import. Such a key
is now an options error, beside the other partition keys the document
cannot honour, and T-18's fixtures for every partition error draw it.

The Kotlin root's first segment was left to the options to name, with
nothing to refuse what kotlinc refuses there ("Only the Kotlin standard
library is allowed to use the 'kotlin' package"). The JVM refuses a
first segment java the same way, though kotlinc compiles it: loading the
class throws "SecurityException: Prohibited package name". A root
beginning with either is now an options error, as an invalid value is.
The unit set reserves what a runtime template declares in whichever unit
it renders into, since no document places the runtime and the root
cannot hold it once groups or partitions split the SDK. Say where that
choice is tracked, so a reader of the set knows it is open.
The Swift rule said only a top-level name two units declare shares the
framework's namespace. The export also joins a nested class's name to
its outer's, and those joined names sit in the same namespace. With
Kotlin 2.4.0, a model PetDog was exported to Swift as PetDog_ beside the
class Dog that a sealed interface Pet nests, which Swift calls PetDog
too. A model UserBuilder was exported to Objective-C as UserBuilder_
beside User.Builder, whose Swift name keeps its nesting.

The target nests public classes of its own in types: a model's Builder
and Companion, a union's Unknown fallback. Their joined names
(UserBuilder, UserCompanion, PaymentUnknown) are now reserved in every
unit on Multiplatform, beside the top-level names already reserved, and
a name another declaration exports is one the export would mangle.
Windows refuses con, prn, aux, nul, and com or lpt with a digit, as the
name of a file or directory in any letter case and whatever the
extension. A Kotlin unit named aux could not be checked out or built
there: its module directory, and the directory kotlinc writes the
package's classes to, cannot be made. A top-level class named Aux fails
the same way, as the JVM writes it to Aux.class. Go's own module checks
refuse the same list on any host (aux.go and the import path
example.com/sdk/aux are both malformed to golang.org/x/mod), which is
the same rule seen from another toolchain.

Both sets now hold those names, a root the options give with one as a
segment is an options error, since an options name never yields, and
T-19 requires that no path a Kotlin build writes has one for a segment,
which a build on a host that allows them would otherwise pass. These
names come from Windows' file-naming rules, not from a build on Windows,
and the section now says so.
The unit set said the target's names are reserved in every unit on
Multiplatform. A reserved name takes Kotlin's escape, so a model
UserBuilder, which meets User.Builder only in the Objective-C export,
would have become UserBuilder_ for every caller, the JVM's included. The
Swift rule already gives such a declaration an explicit Objective-C and
Swift name instead. The set now reserves these names in the Objective-C
namespace alone, and says a declaration meeting one only there keeps its
Kotlin name.
The Multiplatform clause reserved "each of these" in the Objective-C
namespace, which since the previous change also took in the Windows
device names, a file-system rule with no meaning there. It now names
what it means: each name the target declares.
No document said how a Kotlin unit's name is cased, and every units
comparison turns on it. A camel-cased segment would let two units differ
in letter case alone, customHostname beside customhostname, which a
case-insensitive file system makes one module directory and one class
directory. A unit's package segment is now its words in lowercase,
joined with no separator, as Go's package names are, so two units whose
words break differently render alike and collide, and are told apart by
ID as output units already says.
The naming document said a hard keyword as a package segment takes the
backtick escape, from 18 of Kotlin's 28 hard keywords. The other ten
(break, continue, do, else, false, for, if, typeof, var, while),
backticked, also compile in a package and an import on the JVM. All 49
soft and modifier keywords (value, data, import, internal and the rest)
compile there unescaped. The paragraph now says every hard keyword was
measured, and that soft and modifier keywords need no escape.
The Swift rule gives a name the export would mangle an explicit
@ObjCName. In Kotlin 2.4.0 that annotation is still experimental, and a
file using it without an opt-in fails to compile ("This declaration
needs opt-in ... ExperimentalObjCName"). With
@file:OptIn(ExperimentalObjCName::class) it works: a model
p.accounts.ApiException named AccountsApiException left the runtime's
ApiException its own name. A caller in another package compiled without
opting in, and no signature carries the annotation, so the language
floor's rule against opt-in APIs in public signatures holds. The rule
now says so.

The flattening it guards against was measured in one module only. It
holds across modules too: with the runtime in its own Gradle module
under modules/ and a group module exporting a model of the same name,
both exported into one framework, the runtime's class was the one
renamed ApiException_. The rule now says it was measured both ways, and
the namespace facts sit in a bullet of their own.
Reserved names defined the units' scope as the names a target's
toolchain treats apart, and a unit's as the names the target declares
there. Kotlin's sets now hold Windows' device names in both: a unit
named aux, or a top-level class Aux written to Aux.class, is a path
Windows refuses. Neither definition admitted them, nor did the inputs
EscapeReservedWords lists. The units' scope now also covers what a file
system the target writes to treats apart, and a unit's what a top-level
declaration cannot take, and the step's inputs follow.
A top-level declaration shadows a Kotlin default import for its whole
package. With a model List in a package, List<String> there failed to
compile ("No type arguments expected for 'class List : Any'"), and with
a model Exception, a class extending Exception did, as did Map<String,
Long> beside a model Map and Result<Int> beside a model Result. Error
and Result are among the commonest schema names, and nothing reserved
them.

The unit set now reserves each name Kotlin's default imports give that
any refine step or template can write. Each declares those it writes as
it declares what it binds, and the template lint holds a template to
them. The set is their union for the target's version, never what one
unit's code happens to write, so adding an operation renames no model.
The shared definition of a unit's scope now names this case, and the
unit bullet is split by where each reserved name comes from.

A property named class, which Kotlin backticks, was probed too and needs
nothing: its getter is String getClass(), a descriptor apart from
Object.getClass(), and it compiles and runs.
A Swift caller has Swift's standard library and Foundation in scope
beside the framework, and the Objective-C export renames neither side.
Typechecked with Swift 6.2.3 against a Kotlin 2.4.0 framework, a class
the framework exports as Optional, Result or Duration shadowed Swift's
own in the caller's file: Optional<Int> gave "cannot specialize
non-generic type 'Optional'", and Duration.seconds(1) gave "type
'Duration' has no member 'seconds'". One exported as Decimal, Date, Data
or URL made the name ambiguous with Foundation's ("'Decimal' is
ambiguous for type lookup in this context"). Importing the SDK broke
code that never touched it, and the runtime itself declares Optional,
Duration and, on Multiplatform, Decimal, so every Multiplatform SDK did
this.

Every type name Swift's standard library and Foundation declare is now
reserved for the Swift names a framework exports, a table of the options
version as the naming tables are. A declaration whose Swift name would
be one of them takes an explicit one, the runtime's three included. The
lowering table's naming row says so, and T-19 compiles a Swift file that
imports the framework and Foundation.
The Objective-C export declares classes of its own under the framework's
prefix: ProbeBase, ProbeMutableSet, ProbeMutableDictionary, ProbeNumber
and a box per Kotlin number (KotlinInt in Swift, ProbeInt in
Objective-C). Measured with Kotlin 2.4.0, a spec class Base, MutableSet
or MutableDictionary was renamed beside them (ProbeBase_), but one named
Number, Boolean, Byte, Short, Int, Long, Float, Double or an unsigned
number's took the same Objective-C name, and the framework failed to
link: "Linking globals named 'OBJC_METACLASS_$_ProbeNumber': symbol
multiply defined!". Removing each in turn named all twelve. With an
explicit @ObjCName on the spec's class it linked.

These names are now reserved in the Objective-C namespace, the
Multiplatform part of the unit set is split into its Objective-C and
Swift halves, and T-19 requires that the framework links.
A Swift caller settles an ambiguity by naming the module, Probe.Image or
SwiftUI.Image, which the Swift rules lean on for every module not
reserved. That fails once a type takes a module's name. Typechecked with
Swift 6.2.3, a class Probe in the framework Probe turned Probe.Client()
into "type 'Probe' has no member 'Client'", and a class Foundation
turned Foundation.Date into "'Date' is not a member type of class
'Probe.Foundation'". The framework's own name, Swift and Foundation are
now reserved among the Swift names a caller sees.

A module a caller imports by choice is left to that qualification, and
the rule now says so where the next reader looks: in a SwiftUI file, let
i: Image was ambiguous beside a framework class Image, while
SwiftUI.Image and Probe.Image both resolved and SwiftUI's own Image and
Color expressions compiled. T-19's Swift file also qualifies a name by
each module.

The lowering table's naming row listed two of the cases that take an
explicit Swift name, and missed the rest, so it now points at the Swift
rules instead.
@OmarAlJarrah
OmarAlJarrah merged commit 039e5a9 into main Nov 4, 2026
4 checks passed
@OmarAlJarrah
OmarAlJarrah deleted the docs/emitters-kotlin-reserved-sets branch November 4, 2026 21:50
OmarAlJarrah added a commit that referenced this pull request Nov 4, 2026
…ft-keywords

main's tree at dfd9fe0, #1459's squash, is the tree of the head of
fix/openapi-declared-dialect the previous merge brought in, so what
main adds is #1335's docs/emitters alone. The conflicts the two
histories draw, both carrying #1459's changes from 71e08b6, resolve
to this branch's side: outside docs/emitters the merge's tree is the
branch's, and inside it main's.
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.

docs/emitters: Kotlin states no reserved set for the units or a unit

1 participant