mirror of
https://github.com/nim-lang/Nim.git
synced 2026-08-04 22:48:38 +00:00
IC: bugfixes and test cases
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -87,6 +87,7 @@ tweeter_test.db
|
||||
|
||||
/tests/megatest.nim
|
||||
/tests/ic/*_temp.nim
|
||||
/tests/ic/*_mm/
|
||||
/tests/navigator/*_temp.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 `<project><exeExt>`), 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 `<maindir>/<main><exeExt>`.
|
||||
# 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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 <name>` 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: <stdout> noop body-edit iface-edit modules: <n> 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)
|
||||
|
||||
46
tests/ic/tmeta_async.nim
Normal file
46
tests/ic/tmeta_async.nim
Normal file
@@ -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
|
||||
64
tests/ic/tmeta_exprcase.nim
Normal file
64
tests/ic/tmeta_exprcase.nim
Normal file
@@ -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
|
||||
33
tests/ic/tmeta_generic.nim
Normal file
33
tests/ic/tmeta_generic.nim
Normal file
@@ -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
|
||||
61
tests/ic/tmeta_ortype.nim
Normal file
61
tests/ic/tmeta_ortype.nim
Normal file
@@ -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
|
||||
93
tests/ic/tmeta_result.nim
Normal file
93
tests/ic/tmeta_result.nim
Normal file
@@ -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
|
||||
50
tests/ic/tmeta_smoke.nim
Normal file
50
tests/ic/tmeta_smoke.nim
Normal file
@@ -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
|
||||
39
tests/ic/tmeta_transitive.nim
Normal file
39
tests/ic/tmeta_transitive.nim
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user