Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .chronus/changes/realm-as-program-2026-5-14.md
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an intresting approach, I had some branch where I experimented with something that I believe also solve the same issue you are trying to solve here but introducing the concept of type graph. A program can then have multiple type graphs which can have realms. But the idea unlike realm which are mixed a type graph is a completely independent version. It felt like it worked well for what my pr was doing(adding typespec emitter options instead of json schema) but also was opening the door for having those type graph mutator that would take a type graph and return a whole new one

main...timotheeguerin:typespec:emitter-options-typespec

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@timotheeguerin ooh I like this! Do you have any plans to land the TypeGraph approach? As you mentioned, the completely independent TypeGraphs within a program would be super useful for how we'd like to use mutators.

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
---

Add `Realm.asProgram()` returning a `Program`-shaped facade backed by the realm. The facade delegates pass-through members to the parent program and overrides `getGlobalNamespaceType()`, `stateMap`/`stateSet`, and the aggregate state collections so they reflect realm-local state. Together with namespace-rooted realm tracking via `Realm.setGlobalNamespace()` / `Realm.globalNamespace`, this lets the output of `mutateSubgraphWithNamespace` be consumed by the next stage as a `Program` — enabling chained `Program → Program` mutation pipelines without re-parsing TSP files. State-map reads on a clone fall back to the parent's state on the original type via a back-link recorded at clone time, preserving decorator state across the realm boundary.
11 changes: 10 additions & 1 deletion packages/compiler/src/experimental/mutators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,10 +393,19 @@ function createMutatorEngine(
if (options.mutateNamespaces) {
preparingNamespace = true;
// Prepare namespaces first
mutateSubgraphWorker(program.getGlobalNamespaceType(), muts);
const globalNs = program.getGlobalNamespaceType();
const mutatedGlobal = mutateSubgraphWorker(globalNs, muts);
preparingNamespace = false;

postVisits.forEach((visit) => visit());

// Record the cloned global namespace on the realm so realm.asProgram() can
// return a Program-shaped view rooted at the mutated tree. If no mutator
// touched the global namespace, mutatedGlobal === globalNs and we leave the
// realm's globalNamespace unset so the facade falls through to the parent.
if (mutatedGlobal !== globalNs && mutatedGlobal.kind === "Namespace") {
realm.setGlobalNamespace(mutatedGlobal);
}
}

return {
Expand Down
305 changes: 302 additions & 3 deletions packages/compiler/src/experimental/realm.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { compilerAssert } from "../core/diagnostics.js";
import { Program } from "../core/program.js";
import { Type } from "../core/types.js";
import { Namespace, Type } from "../core/types.js";
import type { Typekit } from "../typekit/index.js";
import { createTypekit } from "./typekit/index.js";

/**
* Symbol used to attach a back-link from a cloned type to its original.
*
* This is used by {@link StateMapRealmView} so that state-map reads on a clone
* fall back to the original's state in the parent program when the realm has
* not set its own value for the clone.
*
* @experimental
*/
const ORIGINAL_TYPE = Symbol.for("TypeSpec.Realm.originalType");

/**
* A Realm's view of a Program's state map for a given state key.
*
Expand All @@ -24,7 +35,15 @@ class StateMapRealmView<V> implements Map<Type, V> {
}

has(t: Type) {
return this.#select(t).has(t) ?? false;
if (this.#select(t).has(t)) return true;
// Fall back to the original type's state in the parent program. This lets
// state set on the original type (e.g. by decorators on the source program)
// remain visible when callers ask about the clone.
const original = (t as any)[ORIGINAL_TYPE] as Type | undefined;
if (original && this.#realm.hasType(t) && !this.#realmState.has(t)) {
return this.#parentState.has(original);
}
return false;
}

set(t: Type, v: any) {
Expand All @@ -33,7 +52,14 @@ class StateMapRealmView<V> implements Map<Type, V> {
}

get(t: Type) {
return this.#select(t).get(t);
const target = this.#select(t);
if (target.has(t)) return target.get(t);
// Fall back to the original type's state in the parent program.
const original = (t as any)[ORIGINAL_TYPE] as Type | undefined;
if (original && this.#realm.hasType(t)) {
return this.#parentState.get(original);
}
return undefined;
}

delete(t: Type) {
Expand Down Expand Up @@ -99,6 +125,85 @@ class StateMapRealmView<V> implements Map<Type, V> {
}
}

/**
* A Realm's view of a Program's state set for a given state key.
*
* Mirrors {@link StateMapRealmView} for Set-shaped state. Membership writes go
* to whichever side owns the type (realm vs parent). Reads on a clone fall back
* to the original type's membership in the parent set.
*
* @experimental
*/
class StateSetRealmView implements Set<Type> {
#realm: Realm;
#parentState: Set<Type>;
#realmState: Set<Type>;

public constructor(realm: Realm, realmState: Set<Type>, parentState: Set<Type>) {
this.#realm = realm;
this.#parentState = parentState;
this.#realmState = realmState;
}

add(t: Type): this {
this.#select(t).add(t);
return this;
}

has(t: Type): boolean {
if (this.#select(t).has(t)) return true;
const original = (t as any)[ORIGINAL_TYPE] as Type | undefined;
if (original && this.#realm.hasType(t) && !this.#realmState.has(t)) {
return this.#parentState.has(original);
}
return false;
}

delete(t: Type): boolean {
return this.#select(t).delete(t);
}

clear(): void {
this.#realmState.clear();
}

get size(): number {
return this.#realmState.size + this.#parentState.size;
}

forEach(cb: (value: Type, value2: Type, set: Set<Type>) => void, thisArg?: any): void {
for (const item of this.values()) {
cb.call(thisArg, item, item, this);
}
}

*values(): SetIterator<Type> {
for (const item of this.#realmState) yield item;
for (const item of this.#parentState) yield item;
}

*keys(): SetIterator<Type> {
yield* this.values();
}

*entries(): SetIterator<[Type, Type]> {
for (const item of this.values()) yield [item, item];
}

[Symbol.iterator](): SetIterator<Type> {
return this.values();
}

[Symbol.toStringTag] = "StateSet";

#select(keyType: Type): Set<Type> {
if (this.#realm.hasType(keyType)) {
return this.#realmState;
}
return this.#parentState;
}
}

/**
* A Realm is an alternate view of a Program where types can be cloned, deleted, and modified without affecting the
* original types in the Program.
Expand All @@ -123,6 +228,16 @@ export class Realm {
#deletedTypes = new WeakSet<Type>();

#stateMaps = new Map<symbol, Map<Type, any>>();
#stateSets = new Map<symbol, Set<Type>>();

/**
* The cloned global namespace for this realm, if the realm was produced by a
* namespace-rooted mutation. This is set by the mutator engine when it clones
* the global namespace and is exposed via {@link asProgram} so downstream
* stages can traverse the mutated type graph.
*/
#globalNamespace: Namespace | undefined;

public key!: symbol;

/**
Expand Down Expand Up @@ -222,10 +337,194 @@ export class Realm {
Realm.realmForType.set(type, this);
}

/**
* Gets a state set for the given state key symbol.
*
* Mirrors {@link Realm.stateMap} but for state sets. Returns a view layered
* over the parent program's state set: membership writes go to the realm,
* reads fall through to the parent for non-realm types.
*
* @param stateKey - The symbol to use as the state key.
* @returns The realm's state set for the given state key.
*
* @experimental
*/
stateSet(stateKey: symbol): Set<Type> {
let s = this.#stateSets.get(stateKey);
if (!s) {
s = new Set();
this.#stateSets.set(stateKey, s);
}
return new StateSetRealmView(this, s, this.#program.stateSet(stateKey));
}

/**
* The cloned global namespace for this realm.
*
* Set by the mutator engine when a namespace-rooted mutation runs and the
* global namespace is cloned. Undefined when the mutation did not touch the
* global namespace.
*
* @experimental
*/
get globalNamespace(): Namespace | undefined {
return this.#globalNamespace;
}

/**
* Records the cloned global namespace for this realm.
*
* Intended to be called by the mutator engine, not by user code.
*
* @internal
*/
setGlobalNamespace(ns: Namespace): void {
this.#globalNamespace = ns;
}

/**
* Returns a {@link Program}-shaped view of this realm.
*
* The returned program delegates everything that has not changed
* (compiler options, host, source files, checker, resolver, project root, ...)
* to the parent program, and overrides the things the realm owns:
*
* - {@link Program.getGlobalNamespaceType} returns the cloned global namespace
* if one was recorded, otherwise the parent's global namespace.
* - {@link Program.stateMap} and {@link Program.stateSet} return realm-layered
* views (writes are realm-local; reads fall back to the parent and to
* original types via the clone back-link).
*
* This makes a mutated realm consumable as input to a subsequent mutation
* stage or to an emitter that takes a `Program`.
*
* @experimental
*/
asProgram(): Program {
const realm = this;
const parent = this.#program;
const globalNs = this.#globalNamespace;

// Layered state map / set caches so repeated calls return the same view.
const layeredMaps = new Map<symbol, Map<Type, any>>();
const layeredSets = new Map<symbol, Set<Type>>();
const stateMap = (key: symbol): Map<Type, any> => {
let m = layeredMaps.get(key);
if (!m) {
m = realm.stateMap(key);
layeredMaps.set(key, m);
}
return m;
};
const stateSet = (key: symbol): Set<Type> => {
let s = layeredSets.get(key);
if (!s) {
s = realm.stateSet(key);
layeredSets.set(key, s);
}
return s;
};

const facade: Program = {
// Pure pass-through to the parent program.
get compilerOptions() {
return parent.compilerOptions;
},
get mainFile() {
return parent.mainFile;
},
get sourceFiles() {
return parent.sourceFiles;
},
get jsSourceFiles() {
return parent.jsSourceFiles;
},
get literalTypes() {
return parent.literalTypes;
},
get host() {
return parent.host;
},
get tracer() {
return parent.tracer;
},
trace: (area, message) => parent.trace(area, message),
get checker() {
return parent.checker;
},
get resolver() {
return parent.resolver;
},
get emitters() {
return parent.emitters;
},
get diagnostics() {
return parent.diagnostics;
},
loadTypeSpecScript: (sf) => parent.loadTypeSpecScript(sf),
onValidate: (cb, lib) => parent.onValidate(cb, lib),
getOption: (key) => parent.getOption(key),
get stats() {
return parent.stats;
},
hasError: () => parent.hasError(),
reportDiagnostic: (d) => parent.reportDiagnostic(d),
reportDiagnostics: (ds) => parent.reportDiagnostics(ds),
reportDuplicateSymbols: (s) => parent.reportDuplicateSymbols(s),
resolveTypeReference: (ref) => parent.resolveTypeReference(ref),
resolveTypeOrValueReference: (ref) => parent.resolveTypeOrValueReference(ref),
getSourceFileLocationContext: (sf) => parent.getSourceFileLocationContext(sf),
get projectRoot() {
return parent.projectRoot;
},

// Realm-aware overrides.
getGlobalNamespaceType: () => globalNs ?? parent.getGlobalNamespaceType(),
stateMap,
stateSet,
// The aggregate state-map and state-set collections are exposed as
// proxies that resolve each lookup through the layered view. Most callers
// iterate over a specific key (via stateMap(key)/stateSet(key)) so this
// is mainly here to satisfy the Program shape.
get stateMaps() {
return new Proxy(parent.stateMaps, {
get: (target, prop) => {
if (prop === "get") {
return (key: symbol) => stateMap(key);
}
return Reflect.get(target, prop);
},
}) as Map<symbol, Map<Type, unknown>>;
},
get stateSets() {
return new Proxy(parent.stateSets, {
get: (target, prop) => {
if (prop === "get") {
return (key: symbol) => stateSet(key);
}
return Reflect.get(target, prop);
},
}) as Map<symbol, Set<Type>>;
},
};

return facade;
}

#cloneIntoRealm<T extends Type>(type: T): T {
const clone = this.typekit.type.clone(type);
this.#types.add(clone);
Realm.realmForType.set(clone, this);
// Record a back-link so state-map reads against the clone can fall back to
// state set on the original type in the parent program. This is what
// preserves decorator state (e.g. @doc, @visibility) across the realm
// boundary without the engine having to eagerly copy every state map.
Object.defineProperty(clone, ORIGINAL_TYPE, {
value: type,
enumerable: false,
configurable: false,
writable: false,
});
return clone;
}

Expand Down
Loading