Skip to content

[compiler] Add Realm.asProgram() facade for chained mutation pipelines#10693

Draft
FionaBronwen wants to merge 1 commit into
microsoft:mainfrom
FionaBronwen:fionabronwen/realm-as-program-prototype
Draft

[compiler] Add Realm.asProgram() facade for chained mutation pipelines#10693
FionaBronwen wants to merge 1 commit into
microsoft:mainfrom
FionaBronwen:fionabronwen/realm-as-program-prototype

Conversation

@FionaBronwen
Copy link
Copy Markdown
Contributor

@FionaBronwen FionaBronwen commented May 14, 2026

Overview

Today the mutator framework returns a Realm. Realm holds cloned types and a layered state map but does not implement Program, so it cannot be fed back into a subsequent mutation stage or consumed by emitters that expect a Program.

This change makes a mutated realm consumable as a Program:

  • Realm gains an optional cloned global namespace slot (#globalNamespace) with a public getter and an internal setter (setGlobalNamespace). The mutator engine records the cloned global on the realm when it runs a namespace-rooted mutation.

  • Realm.asProgram() returns a Program-shaped facade. Pass-through members (compilerOptions, host, checker, resolver, sourceFiles, etc.) come from the parent program. Realm-aware overrides reflect realm-local state:

    • getGlobalNamespaceType -> the cloned global if recorded, otherwise the parent's global
    • stateMap, stateSet -> realm-layered views (writes are realm-local; reads fall back to the parent and to original types via the clone back-link)
    • stateMaps, stateSets -> proxy views that route per-key lookups through the layered stateMap/stateSet helpers
  • StateMapRealmView and the new StateSetRealmView fall back to the parent's state keyed by the original type when reading on a clone. This is achieved through a non-enumerable Symbol-for back-link recorded at clone time (Realm.cloneIntoRealm), so decorator state set on the original remains visible under the clone without an eager copy.

Multiple mutation stages can now be chained as:

const stage1 = mutateSubgraphWithNamespace(program, ms1, program.getGlobalNamespaceType());
const p1 = stage1.realm?.asProgram() ?? program;
const stage2 = mutateSubgraphWithNamespace(p1, ms2, p1.getGlobalNamespaceType());
...

A new test file (test/experimental/realm-as-program.test.ts) exercises the facade end-to-end: getGlobalNamespaceType returns the cloned tree, navigateProgram on the facade walks the mutated tree, and a two-stage chain sees both stage 1 and stage 2 mutations. All existing experimental tests continue to pass.

Today the mutator framework returns a Realm. Realm holds cloned types and a
layered state map but does not implement Program, so it cannot be fed back
into the next mutation stage or consumed by emitters that expect a Program.

This change makes a mutated realm consumable as a Program:

- Realm gains an optional cloned global namespace slot (#globalNamespace)
  with a public getter and an internal setter (setGlobalNamespace). The
  mutator engine records the cloned global on the realm when it runs a
  namespace-rooted mutation.

- Realm.asProgram() returns a Program-shaped facade. Pass-through members
  (compilerOptions, host, checker, resolver, sourceFiles, etc.) come from
  the parent program. Realm-aware overrides reflect realm-local state:
    * getGlobalNamespaceType -> the cloned global if recorded, otherwise
      the parent's global
    * stateMap, stateSet -> realm-layered views (writes are realm-local;
      reads fall back to the parent and to original types via the clone
      back-link)
    * stateMaps, stateSets -> proxy views that route per-key lookups
      through the layered stateMap/stateSet helpers

- StateMapRealmView and the new StateSetRealmView fall back to the parent's
  state keyed by the original type when reading on a clone. This is
  achieved through a non-enumerable Symbol-for back-link recorded at clone
  time (Realm.cloneIntoRealm), so decorator state set on the original
  remains visible under the clone without an eager copy.

Multiple stages can now be chained as:

    const stage1 = mutateSubgraphWithNamespace(program, ms1, program.getGlobalNamespaceType());
    const p1 = stage1.realm?.asProgram() ?? program;
    const stage2 = mutateSubgraphWithNamespace(p1, ms2, p1.getGlobalNamespaceType());
    ...

A new test file (test/experimental/realm-as-program.test.ts) exercises the
facade end-to-end: getGlobalNamespaceType returns the cloned tree,
navigateProgram on the facade walks the mutated tree, and a two-stage chain
sees both stage 1 and stage 2 mutations. All existing experimental tests
continue to pass.
@microsoft-github-policy-service microsoft-github-policy-service Bot added the compiler:core Issues for @typespec/compiler label May 14, 2026
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

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants