From cc2afc616f3c0d7553dad0375b0f23768b475e10 Mon Sep 17 00:00:00 2001 From: Araq Date: Wed, 10 Jun 2026 13:41:10 +0200 Subject: [PATCH] IC: don't mutate loaded types in handleGenericInvocation When a generic's body is computed by a macro (Bar[T, U] = makeBar(T, U)), `newbody` after replaceTypeVarsT can be a type loaded from a dep module - even a builtin like `int` - which is Sealed under IC: - the in-place flag accumulation `newbody.flags = newbody.flags + ...` asserted (and under non-IC silently pollutes the shared type's flags, e.g. the global `int`); compute the flags into a local, skip the in-place store for Sealed types and feed `result.flags` from the local - value-identical for the instance. - `newbody.typeInst = result` likewise; a loaded body keeps whatever its defining module serialized (the field was first-wins anyway). Both changes are no-ops for non-IC (types are never Sealed there). Fixes tmacrogenerics. Macro sweep 93/95 - the two remaining fails are tmacro7 (disabled test, fails identically under non-IC) and tmacrogetimpl (needs a design decision on getImplTransformed sym sharing). tests/ic 5/5, koch boot -d:release and clean koch bootic both reach bit-identical fixed points. Co-Authored-By: Claude Fable 5 --- compiler/semtypinst.nim | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/compiler/semtypinst.nim b/compiler/semtypinst.nim index 621ef14a1a..1680f38127 100644 --- a/compiler/semtypinst.nim +++ b/compiler/semtypinst.nim @@ -503,8 +503,14 @@ proc handleGenericInvocation(cl: var TReplTypeVars, t: PType): PType = let bbody = last body var newbody = replaceTypeVarsT(cl, bbody, isInstValue = true) cl.skipTypedesc = oldSkipTypedesc - newbody.flags = newbody.flags + (t.flags + body.flags - tfInstClearedFlags) - result.flags = result.flags + newbody.flags - tfInstClearedFlags + let newbodyFlags = newbody.flags + (t.flags + body.flags - tfInstClearedFlags) + if newbody.state != Sealed: + newbody.flags = newbodyFlags + # else: `newbody` is a type loaded from a dep module (it can even be a + # builtin like `int` when the generic's body is computed by a macro) and is + # immutable under IC. Skip the in-place flag accumulation on the shared + # type; the instance `result` still receives the flags below. + result.flags = result.flags + newbodyFlags - tfInstClearedFlags setToPreviousLayer(cl.typeMap) @@ -524,8 +530,11 @@ proc handleGenericInvocation(cl: var TReplTypeVars, t: PType): PType = # generics *when the type is constructed*: cl.c.graph.setAttachedOp(cl.c.module.position, newbody, attachedDeepCopy, cl.c.instTypeBoundOp(cl.c, dc, result, cl.info, attachedDeepCopy, 1)) - if newbody.typeInst == nil: + if newbody.typeInst == nil and newbody.state != Sealed: # doAssert newbody.typeInst == nil + # An IC-loaded (Sealed) `newbody` keeps whatever `typeInst` its defining + # module serialized; recording this process's first instantiation on the + # shared type is not possible (and was always first-wins anyway). newbody.typeInst = result if tfRefsAnonObj in newbody.flags and newbody.kind != tyGenericInst: # can come here for tyGenericInst too, see tests/metatype/ttypeor.nim