Skip to content
Merged
42 changes: 36 additions & 6 deletions .agents/skills/uloop-hot-reload/references/introduced-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ When an edited body in the same run names a refused type, its shim compile fails
CS0234, or CS0426, or with CS0103 or CS0117 when the body reads a static member of it. That `Failed` row's `Reason` then ends with a note that quotes the refusal
and says `uloop compile` clears it.

Three conditions produce a `Failed` row in `IntroducedTypes` instead, and a `Failed` row makes
These conditions produce a `Failed` row in `IntroducedTypes` instead, and a `Failed` row makes
`Success` false and leaves every file that shares an assembly with the refused declaration
unapplied — no method body of those files is patched in that run, files in other assemblies still
apply, and patches from earlier reloads stay active:
Expand All @@ -72,6 +72,7 @@ apply, and patches from earlier reloads stay active:
| A member body of an already-introduced type changed and is neither an ordinary method body nor a getter-only property body | `Changed member body of introduced type requires a compile:` |
| Two files of the reload declare the same type | `Introduced type <type> is declared in more than one file of the group:` |
| The artifact assembly did not compile | `Introduced-type compilation failed:` |
| This reload did not patch a body the artifact stubs (see "Calling members hot reload adds") | `Not introduced:` |

## Reading the response

Expand Down Expand Up @@ -139,17 +140,46 @@ applied as `Added` rows. Constructor, setter, init, indexer and event accessor b
initializer bodies, member removals, signature changes, and added constructors, operators,
events, indexers or nested types still require a compile.

## Calling members hot reload adds

A new type's ordinary methods and get-only properties can call a method, field or property that
hot reload adds, in the same reload or an earlier one, to a compiled type of the same assembly or
to a type an earlier reload introduced. The artifact compiles each such body as a stub that
throws, and the same reload activates the type only when it holds a patch for every stub, then
patches the real bodies in, so the response shows the type as `Introduced` and those bodies as
`Patched` rows. Later reloads that
include the file keep the type `AlreadyActive` and patch the body again. The file declaring the
addition has to be in the reload: passed, or unchanged since it was last applied, which the
reload pulls back in on its own.

Constructors, initializers, setters, indexers, operators, event accessors and subscriptions to an
added event cannot be patched, so a call from them still fails the artifact compile (CS1061 or
CS0117) with a hint saying where such a call works. So does a call to an addition in another
assembly, or in a file that changed since it was last applied and is not passed.

- When this reload does not patch a stubbed body (a generic method, or a method of a struct, is
`Skipped`), no type of that artifact is introduced: the stubbed type's row reads `Not
introduced: <method> calls members that a hot reload added, …`, the other types of the batch
fail with it, and nothing of that assembly's files is applied.
- Another type the same reload introduces cannot name such a type in its member signatures. The
run is refused with `Introduced type '<type>' calls members that a hot reload added, so its
method bodies run through hot reload patches, …`; run `uloop compile`, or name the type only
inside that other type's method bodies. Two types that both call additions this way may name
each other.
- After `--revert-all`, or when the reload that introduces the type fails to apply one of those
patches (that method's row is `Failed`), a stubbed body runs its stub, which throws
`InvalidOperationException` naming the file to reload; reloading that file patches the body in
again.

## Still needs `uloop compile`

Any refused shape above; use of the type from another assembly, from a file that is neither
passed to this reload nor already hot-reloaded; anything
that reaches the type through Unity (serialization, `[SerializeField]`, Inspector,
`AddComponent`, `CreateInstance`, message discovery); a method body edit of an introduced
struct, which is `Skipped` like any struct method; a call to a member an earlier or the same
reload *added* to a compiled type or to an earlier introduced type, because introduced types
compile against the compiled assemblies and retained artifacts only, so the compile fails naming
the missing member and says a hot reload addition shares its name (reloading the addition first
does not help); an added method that passes a type declared from source in this reload to a
struct, which is `Skipped` like any struct method; a call to a member hot reload *added* from a
new type's body that is not an ordinary method or get-only property, or to an addition this
reload does not hold (see "Calling members hot reload adds"); an added method that passes a type declared from source in this reload to a
member of an earlier introduced type whose signature was bound to the compiled copy, which is
`Skipped` naming both types; and any new or changed `.asmdef` / `.asmref`. A snippet run by `uloop execute-dynamic-code` is
the exception: every active artifact is referenced by that compilation, so the snippet can name an
Expand Down
11 changes: 5 additions & 6 deletions .agents/skills/uloop-hot-reload/references/scope-and-limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,11 @@ by name through the wiring entry point, which is how a value or scene reference
into an added `[SerializeField]` without a compile — see
[added-field-wiring.md](added-field-wiring.md).

An introduced type cannot use members hot reload added to a compiled type, whether they
were added in the same reload or an earlier one: its artifact compiles against the
compiled assemblies and earlier introduced types only, so reloading the addition first
does not help. Such a reference fails with CS1061/CS0117 on a `Failed` `IntroducedTypes`
row, whose `Reason` notes that the missing name matches an addition; run `uloop compile`,
then rerun (issue #2695).
A type a reload introduces can call added members of a compiled type from its ordinary
methods and get-only properties: its artifact compiles those bodies as stubs, and the same
reload patches the real bodies in. From a constructor, initializer, setter, indexer,
operator or event accessor such a reference still fails with CS1061/CS0117 and needs a
compile — see [introduced-types.md](introduced-types.md).

Added members are an Editor-session illusion. Any real compile or domain reload
drops them all: added methods disappear from the ledger and added-field values are
Expand Down
42 changes: 36 additions & 6 deletions .claude/skills/uloop-hot-reload/references/introduced-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ When an edited body in the same run names a refused type, its shim compile fails
CS0234, or CS0426, or with CS0103 or CS0117 when the body reads a static member of it. That `Failed` row's `Reason` then ends with a note that quotes the refusal
and says `uloop compile` clears it.

Three conditions produce a `Failed` row in `IntroducedTypes` instead, and a `Failed` row makes
These conditions produce a `Failed` row in `IntroducedTypes` instead, and a `Failed` row makes
`Success` false and leaves every file that shares an assembly with the refused declaration
unapplied — no method body of those files is patched in that run, files in other assemblies still
apply, and patches from earlier reloads stay active:
Expand All @@ -72,6 +72,7 @@ apply, and patches from earlier reloads stay active:
| A member body of an already-introduced type changed and is neither an ordinary method body nor a getter-only property body | `Changed member body of introduced type requires a compile:` |
| Two files of the reload declare the same type | `Introduced type <type> is declared in more than one file of the group:` |
| The artifact assembly did not compile | `Introduced-type compilation failed:` |
| This reload did not patch a body the artifact stubs (see "Calling members hot reload adds") | `Not introduced:` |

## Reading the response

Expand Down Expand Up @@ -139,17 +140,46 @@ applied as `Added` rows. Constructor, setter, init, indexer and event accessor b
initializer bodies, member removals, signature changes, and added constructors, operators,
events, indexers or nested types still require a compile.

## Calling members hot reload adds

A new type's ordinary methods and get-only properties can call a method, field or property that
hot reload adds, in the same reload or an earlier one, to a compiled type of the same assembly or
to a type an earlier reload introduced. The artifact compiles each such body as a stub that
throws, and the same reload activates the type only when it holds a patch for every stub, then
patches the real bodies in, so the response shows the type as `Introduced` and those bodies as
`Patched` rows. Later reloads that
include the file keep the type `AlreadyActive` and patch the body again. The file declaring the
addition has to be in the reload: passed, or unchanged since it was last applied, which the
reload pulls back in on its own.

Constructors, initializers, setters, indexers, operators, event accessors and subscriptions to an
added event cannot be patched, so a call from them still fails the artifact compile (CS1061 or
CS0117) with a hint saying where such a call works. So does a call to an addition in another
assembly, or in a file that changed since it was last applied and is not passed.

- When this reload does not patch a stubbed body (a generic method, or a method of a struct, is
`Skipped`), no type of that artifact is introduced: the stubbed type's row reads `Not
introduced: <method> calls members that a hot reload added, …`, the other types of the batch
fail with it, and nothing of that assembly's files is applied.
- Another type the same reload introduces cannot name such a type in its member signatures. The
run is refused with `Introduced type '<type>' calls members that a hot reload added, so its
method bodies run through hot reload patches, …`; run `uloop compile`, or name the type only
inside that other type's method bodies. Two types that both call additions this way may name
each other.
- After `--revert-all`, or when the reload that introduces the type fails to apply one of those
patches (that method's row is `Failed`), a stubbed body runs its stub, which throws
`InvalidOperationException` naming the file to reload; reloading that file patches the body in
again.

## Still needs `uloop compile`

Any refused shape above; use of the type from another assembly, from a file that is neither
passed to this reload nor already hot-reloaded; anything
that reaches the type through Unity (serialization, `[SerializeField]`, Inspector,
`AddComponent`, `CreateInstance`, message discovery); a method body edit of an introduced
struct, which is `Skipped` like any struct method; a call to a member an earlier or the same
reload *added* to a compiled type or to an earlier introduced type, because introduced types
compile against the compiled assemblies and retained artifacts only, so the compile fails naming
the missing member and says a hot reload addition shares its name (reloading the addition first
does not help); an added method that passes a type declared from source in this reload to a
struct, which is `Skipped` like any struct method; a call to a member hot reload *added* from a
new type's body that is not an ordinary method or get-only property, or to an addition this
reload does not hold (see "Calling members hot reload adds"); an added method that passes a type declared from source in this reload to a
member of an earlier introduced type whose signature was bound to the compiled copy, which is
`Skipped` naming both types; and any new or changed `.asmdef` / `.asmref`. A snippet run by `uloop execute-dynamic-code` is
the exception: every active artifact is referenced by that compilation, so the snippet can name an
Expand Down
11 changes: 5 additions & 6 deletions .claude/skills/uloop-hot-reload/references/scope-and-limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,11 @@ by name through the wiring entry point, which is how a value or scene reference
into an added `[SerializeField]` without a compile — see
[added-field-wiring.md](added-field-wiring.md).

An introduced type cannot use members hot reload added to a compiled type, whether they
were added in the same reload or an earlier one: its artifact compiles against the
compiled assemblies and earlier introduced types only, so reloading the addition first
does not help. Such a reference fails with CS1061/CS0117 on a `Failed` `IntroducedTypes`
row, whose `Reason` notes that the missing name matches an addition; run `uloop compile`,
then rerun (issue #2695).
A type a reload introduces can call added members of a compiled type from its ordinary
methods and get-only properties: its artifact compiles those bodies as stubs, and the same
reload patches the real bodies in. From a constructor, initializer, setter, indexer,
operator or event accessor such a reference still fails with CS1061/CS0117 and needs a
compile — see [introduced-types.md](introduced-types.md).

Added members are an Editor-session illusion. Any real compile or domain reload
drops them all: added methods disappear from the ledger and added-field values are
Expand Down
58 changes: 54 additions & 4 deletions Assets/Tests/Editor/HotReload/HotReloadEntryHomeResolverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,67 @@ public void Resolve_RowNamesAnAssemblyNoArtifactCarries_Throws()
() => resolver.Resolve(fileHome, "EntryHomeResolverUnknownAssembly"));
}

/// <summary>
/// What: a row that names the artifact this run prepared, which nothing has activated yet,
/// resolves to that artifact's home, so its body can be patched in before activation.
/// </summary>
[Test]
public void Resolve_RowNamesThePreparedArtifact_ReturnsThatArtifactHome()
{
HotReloadIntroducedTypeArtifact prepared = CreateArtifact();
_access.Domain.IntroducedTypes.RegisterPrepared(prepared);
HotReloadEntryHomeResolver resolver = new HotReloadEntryHomeResolver(
_access.Domain,
ResolveProjectRoot(),
prepared);
HotReloadTypeHome fileHome = HotReloadTypeHome.ScriptAssembliesUnderProject(
ResolveProjectRoot(),
ProjectAssemblyName);

HotReloadTypeHome home = resolver.Resolve(fileHome, prepared.Assembly.GetName().Name);

Assert.That(home.Kind, Is.EqualTo(HotReloadTypeHomeKind.RetainedArtifact));
Assert.That(home.DllPath, Is.EqualTo(prepared.DllPath));
HotReloadLoadedAssemblyResolution resolution = home.ResolveLoadedAssembly(
prepared.Assembly.ManifestModule.ModuleVersionId.ToString());
Assert.That(resolution.State, Is.EqualTo(HotReloadLoadedAssemblyState.Loaded));
Assert.That(resolution.Assembly, Is.SameAs(prepared.Assembly));
}

/// <summary>
/// What: without the prepared artifact handed in, a row naming it is refused like any
/// assembly this domain does not retain, because nothing has activated it.
/// </summary>
[Test]
public void Resolve_RowNamesAPreparedArtifactTheResolverWasNotGiven_Throws()
{
HotReloadIntroducedTypeArtifact prepared = CreateArtifact();
_access.Domain.IntroducedTypes.RegisterPrepared(prepared);
HotReloadEntryHomeResolver resolver = CreateResolver();
HotReloadTypeHome fileHome = HotReloadTypeHome.ScriptAssembliesUnderProject(
ResolveProjectRoot(),
ProjectAssemblyName);

Assert.Throws<InvalidOperationException>(
() => resolver.Resolve(fileHome, prepared.Assembly.GetName().Name));
}

private HotReloadEntryHomeResolver CreateResolver()
{
return new HotReloadEntryHomeResolver(_access.Domain, ResolveProjectRoot());
}

private HotReloadIntroducedTypeArtifact ActivateArtifact()
{
HotReloadIntroducedTypeArtifact artifact = new HotReloadIntroducedTypeArtifact(
HotReloadIntroducedTypeArtifact artifact = CreateArtifact();
_access.Domain.IntroducedTypes.RegisterPrepared(artifact);
_access.Domain.IntroducedTypes.Activate(artifact);
return artifact;
}

private static HotReloadIntroducedTypeArtifact CreateArtifact()
{
return new HotReloadIntroducedTypeArtifact(
CreateArtifactAssembly(),
ArtifactDllPath,
"entry-home-resolver-artifact.pdb",
Expand All @@ -115,9 +168,6 @@ private HotReloadIntroducedTypeArtifact ActivateArtifact()
"entry-home-resolver-fingerprint",
"public class Introduced { }")
});
_access.Domain.IntroducedTypes.RegisterPrepared(artifact);
_access.Domain.IntroducedTypes.Activate(artifact);
return artifact;
}

// Why a generated name: an artifact assembly is compiled under a name of its own, so a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ private HotReloadGroupFile CreateDefaultFile(string path)
_compilationAssembly,
HotReloadTypeHome.ScriptAssembliesUnderProject(_projectRoot, AssemblyName),
_projectRoot,
new HotReloadFileSinks(new List<string>(), null, null));
new HotReloadFileSinks(new List<string>(), null, new HotReloadRunStaleSignatureWarnings()));
file.IsDefaultSelected = true;
return file;
}
Expand All @@ -317,7 +317,7 @@ private static HotReloadGroupFile CreateSibling(HotReloadGroupFile template, str
template,
path,
WriteWorkerSource(path),
new HotReloadFileSinks(new List<string>(), null, null),
new HotReloadFileSinks(new List<string>(), null, new HotReloadRunStaleSignatureWarnings()),
null,
new HotReloadSiblingBaselineNotices());
}
Expand Down
Loading
Loading