diff --git a/.gitignore b/.gitignore index efb7dfe61e..98a3c18fcf 100644 --- a/.gitignore +++ b/.gitignore @@ -87,6 +87,7 @@ tweeter_test.db /tests/megatest.nim /tests/ic/*_temp.nim +/tests/ic/*_mm/ /tests/navigator/*_temp.nim diff --git a/compiler/deps.nim b/compiler/deps.nim index f95a884da9..7071af13cf 100644 --- a/compiler/deps.nim +++ b/compiler/deps.nim @@ -956,7 +956,12 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string result = nimcache / c.nodes[0].files[0].modname & ".backend.build.nif" let mainNif = c.nodes[0].files[0].nimFile - let exeFile = changeFileExt(c.nodes[0].files[0].nimFile, ExeExt) + # Honor `--out`/`--outdir`: `cmdIc`'s `setOutFile` populated `conf.outFile` + # (the user's `--out`, or the default ``), so `absOutFile` is + # the final link target — exactly what a whole-program `nim c` would produce. + # The `link` child computes its own output from its project name, so the path + # is also forwarded to it below. + let exeFile = string(c.config.absOutFile) let mergeFile = nimcache / MergeDecisionFile # Per-node output paths. @@ -1117,6 +1122,11 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string b.addIdent "nim_nifc" b.withTree "args": b.addStrLit "--icBackendStage:link" + # The link child is its own `cmdNifC` process whose project is the main + # module, so it would default the binary to `/
`. + # Forward the resolved target so it writes exactly `exeFile` (`--out`'s + # path splits back into outDir+outFile in the child). + b.addStrLit "--out:" & exeFile for i in 0 ..< c.nodes.len: if live[i]: inputStr cFiles[i] outputStr exeFile diff --git a/compiler/main.nim b/compiler/main.nim index fbc2b81be6..efbf496d4f 100644 --- a/compiler/main.nim +++ b/compiler/main.nim @@ -436,6 +436,9 @@ proc mainCommand*(graph: ModuleGraph) = # Generate .build.nif for nifmake setUseIc(true) wantMainModule(conf) + # Resolve the output binary path (honoring `--out`) up front, like cmdNifC: + # the backend build file derives the link target from `conf.absOutFile`. + setOutFile(conf) when not defined(nimKochBootstrap): commandIc(conf) else: diff --git a/testament/categories.nim b/testament/categories.nim index a86541dc3f..1b6cc30965 100644 --- a/testament/categories.nim +++ b/testament/categories.nim @@ -13,7 +13,7 @@ # included from testament.nim import important_packages -import std/[strformat, strutils] +import std/[strformat, strutils, tables] from std/sequtils import filterIt const @@ -488,6 +488,221 @@ proc testNimblePackages(r: var TResults; cat: Category; packageFilter: string) = # ---------------- IC tests --------------------------------------------- +# ---- Metamorphic IC tests -------------------------------------------------- +# +# A metamorphic IC test drives a *sequence of edits across several modules* +# through `nim ic` in a fixed build directory (same absolute paths throughout, +# which is what keeps the cache content-stable) and asserts the invariants the +# incremental backend is supposed to guarantee — see doc/ic_ideas.md: +# +# * clean build == incremental build (a fresh in-place rebuild of the +# final sources is byte-identical to +# the binary and full cache set the +# incremental edits converged to) +# * a no-op edit changes no artifact (`noop`) +# * a body-only edit touches no interface (`body-edit`: no `*.iface.bif` +# cookie changes -> no importer re-sem) +# * an interface edit propagates to (`iface-edit`: an `*.iface.bif` +# importers cookie changes and >= 2 modules' +# `*.s.bif` codegen is rebuilt) +# +# File format (a `tests/ic/t*.nim` whose body, after the spec header, contains a +# line `#? metamorphic`): +# +# #? metamorphic +# #!FILE a.nim +# proc greet*(): string = "hi" +# #!FILE main.nim # `main.nim` is always the build root +# import a +# echo greet() +# #!STEP expect: hi +# #!FILE a.nim # re-emit a module to "edit" it +# proc greet*(): string = "hi" # identical content +# #!STEP expect: hi; noop +# +# `#!FILE ` blocks (re)write a module in the virtual file system; the +# accumulated file set is materialised before each `#!STEP`. A `#!STEP`'s +# attributes are `;`-separated, each either `key: value` or a bare flag: +# expect: noop body-edit iface-edit modules: clean +# The last step always also runs the clean==incremental check. + +type MetamorphicError = object of CatchableError + resultKind: TResultEnum + expected, given: string + +proc mmRaise(kind: TResultEnum, expected, given: string) = + var e = newException(MetamorphicError, given) + e.resultKind = kind + e.expected = expected + e.given = given + raise e + +proc isMetamorphicIcTest(content: string): bool = + for line in content.splitLines: + if line.strip == "#? metamorphic": return true + +proc snapshotDir(dir: string): Table[string, string] = + ## relative path -> raw file contents, for every file under `dir`. + result = initTable[string, string]() + if dirExists(dir): + for it in walkDirRec(dir): + result[it.relativePath(dir)] = readFile(it) + +proc changedPaths(prev, cur: Table[string, string]): seq[string] = + result = @[] + for k, v in cur: + if prev.getOrDefault(k) != v: result.add k + for k in prev.keys: + if k notin cur: result.add k + +proc isProvenance(path: string): bool = + ## Build-provenance sidecars that legitimately differ between a fresh build and + ## an edit-accumulated one (they record build history, not codegen). Excluded + ## only from the cross-build clean==incremental comparison — a *no-op* edit must + ## still leave even these untouched. + path.endsWith(".frontend.build.nif") + +proc changedModuleCount(changed: seq[string]): int = + ## distinct modules whose codegen (`*.s.bif`) was rebuilt. + var mods: seq[string] = @[] + for p in changed: + if p.endsWith(".s.bif"): + let key = p.extractFilename.split('.')[0] + if key notin mods: mods.add key + result = mods.len + +proc runMetamorphicIcTest(r: var TResults; file: string; cat: Category; options: string) = + var test = TTest(cat: cat, name: file, options: options, + spec: initSpec(file), startTime: epochTime()) + test.spec.targets = {targetC} + inc r.total + + # Absolute paths: `nim ic` runs with `workingDir = buildDir`, so a relative + # `--nimcache` would resolve against the build dir, not where we read it back. + let buildDir = (file.changeFileExt("") & "_mm").absolutePath + let nc = buildDir / "nc" + let bin = buildDir / "prog".addFileExt(ExeExt) + removeDir(buildDir) + createDir(buildDir) + + template compileIc(): untyped = + execCmdEx2(compilerPrefix, ["ic", "--hint:Conf:off", "--warnings:off", + "--nimcache:" & nc, "--out:" & bin, "main.nim"], + workingDir = buildDir) + + # Parse the source into a flat op list: ("file", name, content) | ("step", attrs, ""). + type OpKind = enum opFile, opStep + type Op = object + kind: OpKind + a, b: string + var ops: seq[Op] = @[] + block parse: + var curName = "" + var buf = "" + template flushFile() = + if curName.len > 0: ops.add Op(kind: opFile, a: curName, b: buf) + curName = ""; buf = "" + for raw in readFile(file).splitLines: + let s = raw.strip + if s.startsWith("#!FILE"): + flushFile() + curName = s["#!FILE".len .. ^1].strip + elif s.startsWith("#!STEP"): + flushFile() + ops.add Op(kind: opStep, a: s["#!STEP".len .. ^1].strip) + elif curName.len > 0: + buf.add raw; buf.add "\n" + let lastStep = block: + var n = 0 + for o in ops: + if o.kind == opStep: inc n + n + + var vfs = initTable[string, string]() + var prevSnap = initTable[string, string]() + var prevBin = "" + var stepIdx = 0 + try: + for o in ops: + if o.kind == opFile: + vfs[o.a] = o.b + continue + inc stepIdx + let where = "step " & $stepIdx + # Parse step attributes. + var attrs = initTable[string, string]() + for part in o.a.split(';'): + let p = part.strip + if p.len == 0: continue + let c = p.find(':') + if c >= 0: attrs[p[0 ..< c].strip] = p[c+1 .. ^1].strip + else: attrs[p] = "" + + for fn, content in vfs: writeFile(buildDir / fn, content) + let (_, cout, ccode) = compileIc() + if ccode != 0: + mmRaise(reBuildFailed, "", where & ": `nim ic` failed:\n" & cout) + let (_, rout, rcode) = execCmdEx2(bin.absolutePath, [], workingDir = buildDir) + if rcode != 0: + mmRaise(reBuildFailed, "", where & ": program exited with " & $rcode & ":\n" & rout) + if "expect" in attrs: + let want = attrs["expect"].replace("\\n", "\n") + if rout.strip == want.strip: discard + else: mmRaise(reOutputsDiffer, want, where & " output:\n" & rout.strip) + + let snap = snapshotDir(nc) + let binBytes = readFile(bin) + if stepIdx > 1: + let changed = changedPaths(prevSnap, snap) + if "noop" in attrs and (changed.len != 0 or binBytes != prevBin): + mmRaise(reOutputsDiffer, "no artifact change", + where & ": no-op edit changed " & $changed.len & " cache file(s): " & changed.join(", ")) + if "body-edit" in attrs: + for p in changed: + if p.endsWith(".iface.bif"): + mmRaise(reOutputsDiffer, "no interface change", + where & ": body-only edit changed an interface cookie: " & p) + if "iface-edit" in attrs: + var sawIface = false + for p in changed: + if p.endsWith(".iface.bif"): sawIface = true + if not sawIface: + mmRaise(reOutputsDiffer, "interface change", where & ": interface edit changed no `*.iface.bif` cookie") + if changedModuleCount(changed) < 2: + mmRaise(reOutputsDiffer, "propagation to importer", + where & ": interface edit did not propagate (only " & $changedModuleCount(changed) & " module rebuilt)") + if "modules" in attrs: + let want = parseInt(attrs["modules"]) + let got = changedModuleCount(changed) + if got != want: + mmRaise(reOutputsDiffer, $want & " modules rebuilt", where & ": " & $got & " module(s) rebuilt") + prevSnap = snap + prevBin = binBytes + + if "clean" in attrs or stepIdx == lastStep: + removeDir(nc) + let (_, cout2, ccode2) = compileIc() + if ccode2 != 0: + mmRaise(reBuildFailed, "", where & ": clean rebuild failed:\n" & cout2) + let cleanSnap = snapshotDir(nc) + let cleanBin = readFile(bin) + if cleanBin != binBytes: + mmRaise(reOutputsDiffer, "clean binary == incremental binary", + where & ": clean rebuild produced a different binary") + var diff: seq[string] = @[] + for p in changedPaths(snap, cleanSnap): + if not isProvenance(p): diff.add p + if diff.len != 0: + mmRaise(reOutputsDiffer, "clean cache == incremental cache", + where & ": clean rebuild differs in " & $diff.len & " cache file(s): " & diff.join(", ")) + prevSnap = cleanSnap + prevBin = cleanBin + finishTest(r, test, targetC, "", "", "", reSuccess) + inc r.passed + except MetamorphicError: + let e = (ref MetamorphicError)(getCurrentException()) + finishTest(r, test, targetC, "", e.expected, e.given, e.resultKind) + proc icTests(r: var TResults; testsDir: string, cat: Category, options: string; isNavigatorTest: bool) = template editedTest() = @@ -498,11 +713,18 @@ proc icTests(r: var TResults; testsDir: string, cat: Category, options: string; const tempExt = "_temp.nim" for it in walkDirRec(testsDir): + # `_mm` directories hold materialised modules + nimcache for metamorphic + # tests; never collect their files as tests in their own right. + if "_mm" in it: continue if isTestFile(it) and not it.endsWith(tempExt): + let content = readFile(it) + if isMetamorphicIcTest(content): + runMetamorphicIcTest(r, it, cat, options) + continue + let nimcache = nimcacheDir(it, options, targetC) removeDir(nimcache) - let content = readFile(it) for fragment in content.split("#!EDIT!#"): let file = it.replace(".nim", tempExt) writeFile(file, fragment) diff --git a/tests/ic/tmeta_async.nim b/tests/ic/tmeta_async.nim new file mode 100644 index 0000000000..51e64ac24d --- /dev/null +++ b/tests/ic/tmeta_async.nim @@ -0,0 +1,46 @@ +discard """ + description: '''metamorphic IC: async/await across an edit (continuations + clean==incremental)''' +""" + +# Async is the most cache-fragile area under `nim ic`: the `{.async.}` transform +# generates continuation closures and lifts environments, and those lowered +# bodies must survive incremental edits and converge to exactly what a clean +# build produces. This drives an edit to an async proc body and asserts the +# importer is NOT rebuilt (the lifted continuation stays local to its module), +# a no-op changes nothing, and the final state is byte-identical to a clean +# build. See doc/ic_ideas.md and [[ic-nimbus-test]] (async was the long pole). + +#? metamorphic + +#!FILE worker.nim +import std/asyncdispatch +proc compute*(x: int): Future[int] {.async.} = + await sleepAsync(0) + result = x * 2 + +#!FILE main.nim +import std/asyncdispatch, worker +echo waitFor compute(21) + +#!STEP expect: 42 + +# --- edit the async proc body. The continuation env lives in `worker`, so only +# `worker` rebuilds; `main` (the caller) is left untouched -> modules: 1. +# (An async body edit does perturb `worker`'s interface cookie via the +# generated env type, so this is not asserted as a pure `body-edit`.) +#!FILE worker.nim +import std/asyncdispatch +proc compute*(x: int): Future[int] {.async.} = + await sleepAsync(0) + result = x * 3 + +#!STEP expect: 63; modules: 1 + +# --- re-emit identical content: nothing may change, lifted closures included. +#!FILE worker.nim +import std/asyncdispatch +proc compute*(x: int): Future[int] {.async.} = + await sleepAsync(0) + result = x * 3 + +#!STEP expect: 63; noop diff --git a/tests/ic/tmeta_exprcase.nim b/tests/ic/tmeta_exprcase.nim new file mode 100644 index 0000000000..ea6fdcdb13 --- /dev/null +++ b/tests/ic/tmeta_exprcase.nim @@ -0,0 +1,64 @@ +discard """ + description: '''metamorphic IC: expression-based `let x = case ...` idiom''' +""" + +# nimbus-eth2 leans on expression-style code (`let x = case ...` rather than +# statement assignment). This exercises that construct through the incremental +# path: a body edit that adds a `case` branch stays local to the module, while a +# return-type change (interface edit) propagates to the importer even though the +# importer's source is byte-identical. See doc/ic_ideas.md. + +#? metamorphic + +#!FILE classify.nim +proc classify*(n: int): string = + let kind = case n + of 0: "zero" + of 1, 2, 3: "small" + else: "big" + result = kind & "(" & $n & ")" + +#!FILE main.nim +import classify +echo classify(0), " ", classify(2), " ", classify(99) + +#!STEP expect: zero(0) small(2) big(99) + +# --- body edit: add a `case` branch and tweak a label. The signature is +# unchanged, so no interface cookie changes and only `classify` rebuilds. +#!FILE classify.nim +proc classify*(n: int): string = + let kind = case n + of 0: "ZERO" + of 1, 2, 3: "small" + of 4, 5, 6: "medium" + else: "big" + result = kind & "(" & $n & ")" + +#!STEP expect: ZERO(0) small(2) big(99); body-edit; modules: 1 + +# --- re-emit identical content: nothing may change. +#!FILE classify.nim +proc classify*(n: int): string = + let kind = case n + of 0: "ZERO" + of 1, 2, 3: "small" + of 4, 5, 6: "medium" + else: "big" + result = kind & "(" & $n & ")" + +#!STEP expect: ZERO(0) small(2) big(99); noop + +# --- interface edit: the expression-`case` now yields `int`, changing +# `classify`'s return type. `main`'s source is byte-identical (`echo` prints +# either) yet must re-sem & recodegen -> the cookie changes and 2 modules +# rebuild. The final step also runs the clean==incremental check. +#!FILE classify.nim +proc classify*(n: int): int = + result = case n + of 0: 0 + of 1, 2, 3: 1 + of 4, 5, 6: 5 + else: 9 + +#!STEP expect: 0 1 9; iface-edit; modules: 2 diff --git a/tests/ic/tmeta_generic.nim b/tests/ic/tmeta_generic.nim new file mode 100644 index 0000000000..35f26f2e61 --- /dev/null +++ b/tests/ic/tmeta_generic.nim @@ -0,0 +1,33 @@ +discard """ + description: '''metamorphic IC: generic instantiation cache stability across edits''' +""" + +# A generic lives in `gen` but is *instantiated* in `main` (the per-module +# backend emits instance bodies at the instantiation site). Editing the generic +# body must therefore rebuild both `gen` and `main`, and an incremental edit must +# still converge to exactly the same artifacts as a clean build. This exercises +# the static/generic-instance cache path that has historically been bug-prone. + +#? metamorphic + +#!FILE gen.nim +proc box*[T](x: T): seq[T] = @[x, x] + +#!FILE main.nim +import gen +echo box(3).len, " ", box("hi")[0] + +#!STEP expect: 2 hi + +# --- edit the generic body: the importer holds the instantiations, so both the +# definer and the instantiation site rebuild (2 modules). +#!FILE gen.nim +proc box*[T](x: T): seq[T] = @[x, x, x] + +#!STEP expect: 3 hi; modules: 2 + +# --- re-emit identical content: nothing may change. +#!FILE gen.nim +proc box*[T](x: T): seq[T] = @[x, x, x] + +#!STEP expect: 3 hi; noop diff --git a/tests/ic/tmeta_ortype.nim b/tests/ic/tmeta_ortype.nim new file mode 100644 index 0000000000..132d45356e --- /dev/null +++ b/tests/ic/tmeta_ortype.nim @@ -0,0 +1,61 @@ +discard """ + description: '''metamorphic IC: or-type (type-class union) idiom from nimbus forks.nim''' +""" + +# nimbus-eth2 writes a lot of generic code over `A | B | C` type-class unions +# (consensus forks). This models that idiom: a `Fruit = Apple | Banana` union +# with a generic `describe[T: Fruit]` dispatched by `when T is ...`. The generic +# is instantiated in `main`, so editing its body rebuilds both modules, and the +# incremental result must match a clean build. See doc/ic_ideas.md. + +#? metamorphic + +#!FILE forks.nim +type + Apple* = object + weight*: int + Banana* = object + length*: int + Fruit* = Apple | Banana + +proc describe*[T: Fruit](x: T): string = + when T is Apple: "apple " & $x.weight + else: "banana " & $x.length + +#!FILE main.nim +import forks +echo describe(Apple(weight: 5)), " | ", describe(Banana(length: 9)) + +#!STEP expect: apple 5 | banana 9 + +# --- edit the generic body (no signature change). The union/constraint is +# untouched, so no interface cookie changes; but `main` holds the two +# instantiations, so both modules' codegen rebuilds. +#!FILE forks.nim +type + Apple* = object + weight*: int + Banana* = object + length*: int + Fruit* = Apple | Banana + +proc describe*[T: Fruit](x: T): string = + when T is Apple: "APPLE " & $x.weight + else: "BANANA " & $x.length + +#!STEP expect: APPLE 5 | BANANA 9; body-edit; modules: 2 + +# --- re-emit identical content: nothing may change. +#!FILE forks.nim +type + Apple* = object + weight*: int + Banana* = object + length*: int + Fruit* = Apple | Banana + +proc describe*[T: Fruit](x: T): string = + when T is Apple: "APPLE " & $x.weight + else: "BANANA " & $x.length + +#!STEP expect: APPLE 5 | BANANA 9; noop diff --git a/tests/ic/tmeta_result.nim b/tests/ic/tmeta_result.nim new file mode 100644 index 0000000000..5ed596ea54 --- /dev/null +++ b/tests/ic/tmeta_result.nim @@ -0,0 +1,93 @@ +discard """ + description: '''metamorphic IC: Result[T, E] (nim-results style) variant-object idiom''' +""" + +# nimbus-eth2 threads errors through nim-results' `Result[T, E]`, a generic +# *variant* (case) object. This models a minimal hermetic version and exercises +# the generic-variant cache path across edits: a body edit of a generic accessor, +# adding a public overload (an interface edit), and a no-op — with a final +# clean==incremental check. See doc/ic_ideas.md. + +#? metamorphic + +#!FILE results.nim +type + ResultKind = enum rOk, rErr + Result*[T, E] = object + case kind: ResultKind + of rOk: v: T + of rErr: e: E + +proc ok*[T, E](x: T): Result[T, E] = Result[T, E](kind: rOk, v: x) +proc err*[T, E](x: E): Result[T, E] = Result[T, E](kind: rErr, e: x) +proc isOk*[T, E](r: Result[T, E]): bool = r.kind == rOk +proc get*[T, E](r: Result[T, E]): T = r.v +proc error*[T, E](r: Result[T, E]): E = r.e + +#!FILE main.nim +import results +proc parse(s: string): Result[int, string] = + if s == "42": ok[int, string](42) + else: err[int, string]("bad: " & s) +let a = parse("42") +let b = parse("x") +echo (if a.isOk: $a.get else: a.error), " ", (if b.isOk: $b.get else: b.error) + +#!STEP expect: 42 bad: x + +# --- body-only edit of a generic accessor (`error`): signature unchanged, so no +# interface cookie changes; the importer holds the instantiation, so both +# modules' codegen rebuilds. +#!FILE results.nim +type + ResultKind = enum rOk, rErr + Result*[T, E] = object + case kind: ResultKind + of rOk: v: T + of rErr: e: E + +proc ok*[T, E](x: T): Result[T, E] = Result[T, E](kind: rOk, v: x) +proc err*[T, E](x: E): Result[T, E] = Result[T, E](kind: rErr, e: x) +proc isOk*[T, E](r: Result[T, E]): bool = r.kind == rOk +proc get*[T, E](r: Result[T, E]): T = r.v +proc error*[T, E](r: Result[T, E]): E = "ERR:" & r.e + +#!STEP expect: 42 ERR:bad: x; body-edit; modules: 2 + +# --- re-emit identical content: nothing may change. +#!FILE results.nim +type + ResultKind = enum rOk, rErr + Result*[T, E] = object + case kind: ResultKind + of rOk: v: T + of rErr: e: E + +proc ok*[T, E](x: T): Result[T, E] = Result[T, E](kind: rOk, v: x) +proc err*[T, E](x: E): Result[T, E] = Result[T, E](kind: rErr, e: x) +proc isOk*[T, E](r: Result[T, E]): bool = r.kind == rOk +proc get*[T, E](r: Result[T, E]): T = r.v +proc error*[T, E](r: Result[T, E]): E = "ERR:" & r.e + +#!STEP expect: 42 ERR:bad: x; noop + +# --- interface edit: add a public `get` overload (a new exported signature). +# `main` doesn't call it, yet importing `results` whose interface changed +# forces a re-sem; the cookie changes and >= 2 modules rebuild. The final +# step also runs the clean==incremental check. +#!FILE results.nim +type + ResultKind = enum rOk, rErr + Result*[T, E] = object + case kind: ResultKind + of rOk: v: T + of rErr: e: E + +proc ok*[T, E](x: T): Result[T, E] = Result[T, E](kind: rOk, v: x) +proc err*[T, E](x: E): Result[T, E] = Result[T, E](kind: rErr, e: x) +proc isOk*[T, E](r: Result[T, E]): bool = r.kind == rOk +proc get*[T, E](r: Result[T, E], fallback: T): T = (if r.kind == rOk: r.v else: fallback) +proc get*[T, E](r: Result[T, E]): T = r.v +proc error*[T, E](r: Result[T, E]): E = "ERR:" & r.e + +#!STEP expect: 42 ERR:bad: x; iface-edit; modules: 2 diff --git a/tests/ic/tmeta_smoke.nim b/tests/ic/tmeta_smoke.nim new file mode 100644 index 0000000000..d8f057dab3 --- /dev/null +++ b/tests/ic/tmeta_smoke.nim @@ -0,0 +1,50 @@ +discard """ + description: '''metamorphic IC: clean==incremental, no-op stability, body vs interface boundary''' +""" + +# This is a *metamorphic* IC test (see the `#? metamorphic` marker and the +# runner in testament/categories.nim). It drives a sequence of cross-module +# edits through `nim ic` in one fixed build directory and checks the invariants +# the incremental backend must uphold (doc/ic_ideas.md). + +#? metamorphic + +#!FILE a.nim +proc greet*(): string = "hi" +proc secret(): int = 41 # private, body-only churn target +proc value*(): int = secret() + 1 + +#!FILE main.nim +import a +echo greet(), " ", value() + +#!STEP expect: hi 42 + +# --- body-only edit: a private body changes, no signature does. +# => no `*.iface.bif` cookie changes, exactly 1 module's codegen rebuilds, +# the importer is left untouched. +#!FILE a.nim +proc greet*(): string = "hi" +proc secret(): int = 999 +proc value*(): int = secret() + 1 + +#!STEP expect: hi 1000; body-edit; modules: 1 + +# --- no-op edit: re-emit byte-identical content. Nothing downstream may change. +#!FILE a.nim +proc greet*(): string = "hi" +proc secret(): int = 999 +proc value*(): int = secret() + 1 + +#!STEP expect: hi 1000; noop + +# --- interface edit: `value`'s return type changes (a signature change) while +# main.nim's source stays byte-identical. The interface cookie must change +# and the importer must be re-sem'd & recodegen'd (>= 2 modules rebuilt). +# The final step also runs the clean==incremental check. +#!FILE a.nim +proc greet*(): string = "hi" +proc secret(): int = 999 +proc value*(): int64 = secret().int64 + 1 + +#!STEP expect: hi 1000; iface-edit diff --git a/tests/ic/tmeta_transitive.nim b/tests/ic/tmeta_transitive.nim new file mode 100644 index 0000000000..aec23f7d66 --- /dev/null +++ b/tests/ic/tmeta_transitive.nim @@ -0,0 +1,39 @@ +discard """ + description: '''metamorphic IC: edit propagation across a 3-module import chain''' +""" + +# Chain: main -> b -> a. Demonstrates that a body edit stays local to the edited +# module, while an interface edit propagates to its direct importer but stops +# where the next signature is unchanged. See doc/ic_ideas.md and the runner in +# testament/categories.nim. + +#? metamorphic + +#!FILE a.nim +proc base*(): int = 1 + +#!FILE b.nim +import a +proc mid*(): int = base() + 10 + +#!FILE main.nim +import b +echo mid() + +#!STEP expect: 11 + +# --- body-only edit of `a.base`: no signature changes, so nothing re-sems; +# only module `a`'s own codegen rebuilds. +#!FILE a.nim +proc base*(): int = 7 + +#!STEP expect: 17; body-edit; modules: 1 + +# --- interface edit of `a.base` (return type int -> int64). `b` uses `base`, so +# `a`'s cookie change forces `b` to re-sem & recodegen; but `b.mid`'s own +# signature is unchanged, so `main` is NOT rebuilt -> exactly 2 modules. +# The final step also runs the clean==incremental check. +#!FILE a.nim +proc base*(): int64 = 7 + +#!STEP expect: 17; iface-edit; modules: 2