mirror of
https://github.com/nim-lang/Nim.git
synced 2026-09-01 03:13:41 +00:00
Compare commits
76 Commits
version-2-
...
araq-ic-fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39b2f1830f | ||
|
|
c6f70374bc | ||
|
|
55b244efff | ||
|
|
f48efa4b1f | ||
|
|
3f014bbd37 | ||
|
|
d680599038 | ||
|
|
087c82f985 | ||
|
|
bff0bc45fd | ||
|
|
8299401888 | ||
|
|
5d77a71043 | ||
|
|
e5bba5a0fe | ||
|
|
bad9d3b2bc | ||
|
|
98b1e0ab50 | ||
|
|
82b1b9a3c9 | ||
|
|
b6248a0b80 | ||
|
|
4c6d7d2d20 | ||
|
|
1a70426a5b | ||
|
|
20a3375571 | ||
|
|
2605cd3213 | ||
|
|
dbaed3d38a | ||
|
|
727d70d2ba | ||
|
|
2db27d4826 | ||
|
|
d27e1e2712 | ||
|
|
db47363784 | ||
|
|
8afd306b0d | ||
|
|
603953376d | ||
|
|
31a7b6bc85 | ||
|
|
709fd00861 | ||
|
|
e5bafa48c5 | ||
|
|
9f34f5e42b | ||
|
|
e988e0366c | ||
|
|
47144c7962 | ||
|
|
bca07265a4 | ||
|
|
c1a815fa07 | ||
|
|
fe39ad5fba | ||
|
|
06b1bf8f9a | ||
|
|
837082eb89 | ||
|
|
512d2a8f26 | ||
|
|
5799220c98 | ||
|
|
dcec8e1cd1 | ||
|
|
8cb406cd7a | ||
|
|
802bcf5a2d | ||
|
|
2447dfdc7d | ||
|
|
437876efbd | ||
|
|
20b70c8b1e | ||
|
|
9e076d74c0 | ||
|
|
8d4ddd5516 | ||
|
|
f84f53bff4 | ||
|
|
e0c0724b62 | ||
|
|
d81e764f98 | ||
|
|
f897fe8c29 | ||
|
|
33ee586913 | ||
|
|
0be9b4f3f6 | ||
|
|
c36c527db3 | ||
|
|
3c53629164 | ||
|
|
1f504865ed | ||
|
|
0c0cc1e496 | ||
|
|
1387093f99 | ||
|
|
546a518b22 | ||
|
|
c87926dadf | ||
|
|
bd95f88f74 | ||
|
|
cce17461de | ||
|
|
f3bdc6c5f2 | ||
|
|
7ddfc44c0f | ||
|
|
dc242e9027 | ||
|
|
467c911dc5 | ||
|
|
c01e58c146 | ||
|
|
8ca7b75b8b | ||
|
|
7f120229e8 | ||
|
|
31215b3856 | ||
|
|
2d1412a2ea | ||
|
|
37223d2ea9 | ||
|
|
6f1e6fdd06 | ||
|
|
f1256ddcf4 | ||
|
|
901ca7905a | ||
|
|
81325d0745 |
@@ -99,6 +99,7 @@ parameter and result types, not just their source-level shape. Use
|
||||
works without single-quoting.
|
||||
- `std/uri`: The `?` operator now appends query parameters to an existing query
|
||||
string instead of replacing it. Fixes [#19782](https://github.com/nim-lang/Nim/issues/19782).
|
||||
- `std/jsonutils`: `fromJson` now throws an exception when converting to `array`/`seq` if the JSON isn't an array instead of silently failing
|
||||
|
||||
## Language changes
|
||||
|
||||
@@ -149,6 +150,13 @@ parameter and result types, not just their source-level shape. Use
|
||||
The issue was that `hasValuelessStatics` in `semtypinst.nim` didn't recognize
|
||||
`tyTypeDesc(tyGenericParam)` as an unresolved generic parameter.
|
||||
|
||||
- The JS backend now implements write-through for `var openArray` parameters that
|
||||
receive a `toOpenArray` view (bug #15952): mutations reach the caller's storage
|
||||
instead of silently writing to a copy. Fixed homogeneous numeric arrays
|
||||
(`array[N, T]`, JS typed arrays) slice via `subarray`; `seq` and non-numeric
|
||||
arrays slice via a `{base, off, len}` view. This also covers seq/non-numeric-array
|
||||
write-through, pass-through, re-slicing and `@` (openArray-to-seq) of such views.
|
||||
|
||||
## Tool changes
|
||||
|
||||
- Added `--raw` flag when generating JSON docs to not render markup.
|
||||
|
||||
@@ -8,7 +8,7 @@ const
|
||||
nkBracketExpr, nkDerefExpr, nkHiddenDeref,
|
||||
nkAddr, nkHiddenAddr,
|
||||
nkObjDownConv, nkObjUpConv}
|
||||
PathKinds1* = {nkHiddenStdConv, nkHiddenSubConv}
|
||||
PathKinds1* = {nkHiddenStdConv, nkHiddenSubConv, nkCast}
|
||||
|
||||
proc skipConvDfa*(n: PNode): PNode =
|
||||
result = n
|
||||
@@ -125,4 +125,3 @@ proc aliases*(obj, field: PNode): AliasKind =
|
||||
else:
|
||||
result = maybe
|
||||
else: assert false # unreachable
|
||||
|
||||
|
||||
262
compiler/ast.nim
262
compiler/ast.nim
@@ -359,6 +359,18 @@ proc `flags=`*(t: PType, val: TTypeFlags) {.inline.} =
|
||||
t.flagsImpl = val
|
||||
|
||||
proc sons*(t: PType): var TTypeSeq {.inline.} =
|
||||
## The RAW child seq. Despite the name this is NOT the counterpart of the
|
||||
## `sons` ITERATOR over a `PNode`, and it is not the way to walk a type's
|
||||
## children — use `kids` / `ikids` / `paramTypes` / `signature`, or the named
|
||||
## accessors (`returnType`, `baseClass`, `elementType`, `indexType`,
|
||||
## `genericHead`, ...), which say WHICH child they mean.
|
||||
##
|
||||
## The difference is not cosmetic. A `tyProc` keeps its parameter types in
|
||||
## `n`, not here — `setSons` asserts `sonsImpl.len <= 1` for one — so `[]`,
|
||||
## `len` and every iterator built on them route parameters through
|
||||
## `n[i].sym.typ`, while this seq holds only the return type. `for x in
|
||||
## t.sons` therefore compiles, looks like the `PNode` idiom, and silently
|
||||
## visits a different set of types.
|
||||
if t.state == Partial: loadType(t)
|
||||
result = t.sonsImpl
|
||||
|
||||
@@ -509,7 +521,8 @@ proc getPIdent*(a: PNode): PIdent {.inline.} =
|
||||
of nkOpenSymChoice, nkClosedSymChoice, nkOpenSym: a.sons[0].sym.name
|
||||
else: nil
|
||||
|
||||
template id*(a: PType | PSym): int = toId(a.itemId)
|
||||
template id*(a: PSym): int = toId(a.itemId)
|
||||
template id*(a: PType): int = toId(a.bindingId)
|
||||
|
||||
type
|
||||
IdGenerator* = ref object # unfortunately, we really need the 'shared mutable' aspect here.
|
||||
@@ -701,6 +714,10 @@ proc extractPragma*(s: PSym): PNode =
|
||||
proc skipPragmaExpr*(n: PNode): PNode =
|
||||
## if pragma expr, give the node the pragmas are applied to,
|
||||
## otherwise give node itself
|
||||
##
|
||||
## `bnode` carries the `BNode` spelling. It is a separate one-liner rather
|
||||
## than a shared template because this sits above the point in this module
|
||||
## where `firstSon` for a `PNode` exists.
|
||||
if n.kind == nkPragmaExpr:
|
||||
result = n[0]
|
||||
else:
|
||||
@@ -764,10 +781,28 @@ when false:
|
||||
echo k
|
||||
echo v
|
||||
|
||||
when defined(icSymCount):
|
||||
import std / [syncio, exitprocs, tables as symCountTables]
|
||||
var symMints*: symCountTables.CountTable[string]
|
||||
var symMintTotal*: int
|
||||
var symCountHooked = false
|
||||
|
||||
proc newSym*(symKind: TSymKind, name: PIdent, idgen: IdGenerator; owner: PSym,
|
||||
info: TLineInfo; options: TOptions = {}): PSym =
|
||||
# generates a symbol and initializes the hash field too
|
||||
assert not name.isNil
|
||||
when defined(icSymCount):
|
||||
# Counting symbol MINTS, not their names in the output: a gensym's number is
|
||||
# its item id, so one extra symbol anywhere shifts every later name. A count
|
||||
# is therefore far more sensitive than diffing generated C, and it localises
|
||||
# the extra mint by kind instead of by whatever file happened to show it.
|
||||
inc symMintTotal
|
||||
symMints.inc $symKind
|
||||
if not symCountHooked:
|
||||
symCountHooked = true
|
||||
addExitProc proc () =
|
||||
stderr.writeLine "SYMMINT total=" & $symMintTotal
|
||||
for k, v in symMints: stderr.writeLine "SYMMINT " & k & "=" & $v
|
||||
let id = nextSymId idgen
|
||||
result = PSym(name: name, kindImpl: symKind, flagsImpl: {}, infoImpl: info, itemId: id,
|
||||
optionsImpl: options, ownerFieldImpl: owner, offsetImpl: defaultOffset,
|
||||
@@ -1097,7 +1132,7 @@ proc newType*(kind: TTypeKind; idgen: IdGenerator; owner: PSym; son: sink PType
|
||||
let id = nextTypeId idgen
|
||||
result = PType(kind: kind, ownerFieldImpl: owner, sizeImpl: defaultSize,
|
||||
alignImpl: defaultAlignment, itemId: id,
|
||||
uniqueId: id, sonsImpl: @[])
|
||||
bindingId: id, sonsImpl: @[])
|
||||
if son != nil:
|
||||
assert kind != tyProc
|
||||
result.sonsImpl.add son
|
||||
@@ -1173,18 +1208,23 @@ proc copyType*(t: PType, idgen: IdGenerator, owner: PSym): PType =
|
||||
result.symImpl = t.sym # backend-info should not be copied
|
||||
|
||||
proc exactReplica*(t: PType; idgen: IdGenerator): PType =
|
||||
## Replica that KEEPS `itemId` — the generic-param binding tables
|
||||
## (`LayeredIdTable`) key on it, so the copy must keep matching its
|
||||
## original — but mints a FRESH `uniqueId`: uniqueId is the SERIALIZATION
|
||||
## identity (NIF type names key on it) and must be unique per instance.
|
||||
## Replicas sharing the original's uniqueId serialized as duplicate defs
|
||||
## under one NIF name; the loader collapsed them into a single type,
|
||||
## Copy that INHERITS `bindingId` — the generic-param binding tables
|
||||
## (`LayeredIdTable`) key on it, so the copy must keep matching its original
|
||||
## there — while getting its own `itemId`, like every other type. The two
|
||||
## remaining callers are `semtypinst.instCopyType` (a partially instantiated
|
||||
## meta type must still bind in the next instantiation round) and the
|
||||
## `tfUnresolved` typedesc replica in `semtypes.semTypeIdent`; everything
|
||||
## else that used to come through here is a plain `copyType`.
|
||||
##
|
||||
## Do not "simplify" this to share `itemId` as well: `itemId` is the
|
||||
## serialization identity, and replicas sharing it serialized as duplicate
|
||||
## defs under one NIF name, which the loader collapsed into a single type —
|
||||
## losing their flag differences (use-site `tfUnresolved` typedescs) or
|
||||
## their structure (meta instance bodies shadowing a generic's canonical
|
||||
## body).
|
||||
result = PType(kind: t.kind, ownerFieldImpl: t.owner, sizeImpl: defaultSize,
|
||||
alignImpl: defaultAlignment, itemId: t.itemId,
|
||||
uniqueId: nextTypeId(idgen))
|
||||
alignImpl: defaultAlignment, itemId: nextTypeId(idgen),
|
||||
bindingId: t.bindingId)
|
||||
assignType(result, t)
|
||||
result.symImpl = t.sym # backend-info should not be copied
|
||||
|
||||
@@ -1446,18 +1486,28 @@ proc hasSubnodeWith*(n: PNode, kind: TNodeKind): bool =
|
||||
return true
|
||||
result = false
|
||||
|
||||
proc getInt*(a: PNode): Int128 =
|
||||
case a.kind
|
||||
of nkCharLit, nkUIntLit..nkUInt64Lit:
|
||||
result = toInt128(cast[uint64](a.intVal))
|
||||
of nkInt8Lit..nkInt64Lit:
|
||||
result = toInt128(a.intVal)
|
||||
of nkIntLit:
|
||||
# XXX: enable this assert
|
||||
# assert a.typ.kind notin {tyChar, tyUint..tyUInt64}
|
||||
result = toInt128(a.intVal)
|
||||
else:
|
||||
raiseRecoverableError("cannot extract number from invalid AST node")
|
||||
template getIntImpl*(aArg: typed): Int128 =
|
||||
## The body of `getInt`, in a form `bnode.nim` can instantiate for a `BNode`
|
||||
## too — same reason as `canRaiseImpl`: `BNode` is defined there and that
|
||||
## module imports this one, so the shared logic has to live in a template
|
||||
## rather than an `AnyNode` proc. There is no second copy.
|
||||
block:
|
||||
let a = aArg
|
||||
var res: Int128
|
||||
case a.kind
|
||||
of nkCharLit, nkUIntLit..nkUInt64Lit:
|
||||
res = toInt128(cast[uint64](a.intVal))
|
||||
of nkInt8Lit..nkInt64Lit:
|
||||
res = toInt128(a.intVal)
|
||||
of nkIntLit:
|
||||
# XXX: enable this assert
|
||||
# assert a.typ.kind notin {tyChar, tyUint..tyUInt64}
|
||||
res = toInt128(a.intVal)
|
||||
else:
|
||||
raiseRecoverableError("cannot extract number from invalid AST node")
|
||||
res
|
||||
|
||||
proc getInt*(a: PNode): Int128 = getIntImpl(a)
|
||||
|
||||
proc getInt64*(a: PNode): int64 {.deprecated: "use getInt".} =
|
||||
case a.kind
|
||||
@@ -1477,14 +1527,21 @@ proc getFloat*(a: PNode): BiggestFloat =
|
||||
#internalError(a.info, "getFloat")
|
||||
#result = 0.0
|
||||
|
||||
proc getStr*(a: PNode): string =
|
||||
case a.kind
|
||||
of nkStrLit..nkTripleStrLit: result = a.strVal
|
||||
of nkNilLit:
|
||||
# let's hope this fixes more problems than it creates:
|
||||
result = ""
|
||||
else:
|
||||
raiseRecoverableError("cannot extract string from invalid AST node")
|
||||
template getStrImpl*(aArg: typed): string =
|
||||
## Body shared with `bnode`'s `BNode` spelling — see `canRaiseImpl`.
|
||||
block:
|
||||
let gs = aArg
|
||||
var res = ""
|
||||
case gs.kind
|
||||
of nkStrLit..nkTripleStrLit: res = gs.strVal
|
||||
of nkNilLit:
|
||||
# let's hope this fixes more problems than it creates:
|
||||
res = ""
|
||||
else:
|
||||
raiseRecoverableError("cannot extract string from invalid AST node")
|
||||
res
|
||||
|
||||
proc getStr*(a: PNode): string = getStrImpl(a)
|
||||
#doAssert false, "getStr"
|
||||
#internalError(a.info, "getStr")
|
||||
#result = ""
|
||||
@@ -1627,8 +1684,14 @@ proc isImportedException*(t: PType; conf: ConfigRef): bool =
|
||||
let base = t.skipTypes({tyAlias, tyPtr, tyDistinct, tyGenericInst})
|
||||
result = base.sym != nil and {sfCompileToCpp, sfImportc} * base.sym.flags != {}
|
||||
|
||||
proc isInfixAs*(n: PNode): bool =
|
||||
return n.kind == nkInfix and n[0].kind == nkIdent and n[0].ident.id == ord(wAs)
|
||||
template isInfixAsImpl*(nArg: typed): bool =
|
||||
## Body shared with `bnode`'s `BNode` spelling — see `canRaiseImpl`.
|
||||
block:
|
||||
let ia = nArg
|
||||
ia.kind == nkInfix and ia.firstSon.kind == nkIdent and
|
||||
ia.firstSon.ident.id == ord(wAs)
|
||||
|
||||
proc isInfixAs*(n: PNode): bool = isInfixAsImpl(n)
|
||||
|
||||
proc skipColon*(n: PNode): PNode =
|
||||
result = n
|
||||
@@ -1705,36 +1768,110 @@ proc addParam*(procType: PType; param: PSym) =
|
||||
procType.n.add newSymNode(param)
|
||||
rawAddSon(procType, param.typ)
|
||||
|
||||
const magicsThatCanRaise = {
|
||||
const magicsThatCanRaise* = {
|
||||
mNone, mSlurp, mStaticExec, mParseExprToAst, mParseStmtToAst, mEcho}
|
||||
|
||||
proc canRaiseConservative*(fn: PNode): bool =
|
||||
if fn.kind == nkSym and fn.sym.magic notin magicsThatCanRaise:
|
||||
result = false
|
||||
else:
|
||||
result = true
|
||||
# `canRaise` and `canRaiseConservative` are asked by the C backend, which is
|
||||
# migrating to reading routine bodies straight off a `.bif` `Cursor` rather than
|
||||
# off a materialised `PNode` tree (see `compiler/bnode.nim`). Both predicates
|
||||
# only ever look at a node's `kind`, `sym` and `typ`, so ONE body serves either
|
||||
# spelling -- but `BNode` is defined in `bnode.nim`, which imports this module,
|
||||
# so the `BNode` overloads cannot live here. The bodies therefore live in
|
||||
# templates and `bnode.nim` instantiates them for its own node type: one source
|
||||
# of truth, no import cycle, and no second copy to keep in sync.
|
||||
#
|
||||
# The effect list is reached through `effectsOf` / `raisesNothing` rather than
|
||||
# by subscripting `fn.typ.n`, so the templates below contain no knowledge of the
|
||||
# layout and the `BNode` instantiation inherits none. `fn.typ` stays a `PType`
|
||||
# in both spellings -- there is deliberately no `BType` (see `bnode.nim`) -- so
|
||||
# what "works on a `.bif`" means for these two is that the type the decoder
|
||||
# materialises must carry the same effect list the from-source one did. That is
|
||||
# a claim about the WRITER, not about the vocabulary, and it is checked
|
||||
# separately: `-d:icCanRaiseLog` logs every answer, and the same program built
|
||||
# with and without `--ic:on` must produce the same verdicts.
|
||||
|
||||
proc canRaise*(fn: PNode): bool =
|
||||
if fn.kind == nkSym and (fn.sym.magic notin magicsThatCanRaise or
|
||||
{sfImportc, sfInfixCall} * fn.sym.flags == {sfImportc} or
|
||||
sfGeneratedOp in fn.sym.flags):
|
||||
result = false
|
||||
elif fn.kind == nkSym and fn.sym.magic == mEcho:
|
||||
result = true
|
||||
elif fn.typ != nil and fn.typ.kind == tyProc and fn.typ.n != nil:
|
||||
# TODO check for n having sons? or just return false for now if not
|
||||
if fn.typ.n[0].kind == nkSym:
|
||||
result = false
|
||||
when defined(icCanRaiseLog):
|
||||
var canRaiseBranch* = 0
|
||||
## Which branch decided the last answer: 1 = the symbol's magic/flags,
|
||||
## 2 = `mEcho`, 3 = the EFFECT LIST reached through `effectsOf`, 4 = the
|
||||
## conservative predicate, 5 = short-circuited in `canRaiseDisp` before
|
||||
## either predicate ran, 0 = fell through. Only branch 3 reads anything
|
||||
## that had to survive a `.bif` round trip, so a differential in which no
|
||||
## callee reaches it would prove nothing about the writer — which is the
|
||||
## whole point of running the differential. See `-d:icCanRaiseLog`.
|
||||
|
||||
template markCanRaiseBranch*(n: int) =
|
||||
when defined(icCanRaiseLog): canRaiseBranch = n
|
||||
|
||||
template canRaiseConservativeImpl*(fnArg: typed): bool =
|
||||
block:
|
||||
let fn = fnArg
|
||||
markCanRaiseBranch 4
|
||||
not (fn.kind == nkSym and fn.sym.magic notin magicsThatCanRaise)
|
||||
|
||||
proc effectsOf*(t: PType): PNode {.inline.} =
|
||||
## The `nkEffectList` a proc type carries as child 0 of its formal-params
|
||||
## node, with the parameters following from index 1 (`newProcType` builds it
|
||||
## that way; `cgen` reads the params back with `sonsFrom(prc.typ.n, 1)`).
|
||||
##
|
||||
## Named rather than subscripted so that the layout is written down in ONE
|
||||
## place. `.n` here is a TYPE's node, never a routine body, so it is always
|
||||
## fully materialised and `firstSon` is safe — the `nfLazyBody` hazard that
|
||||
## makes raw child access dangerous elsewhere (see `astdef.sons`) cannot reach
|
||||
## it. A proc type always has this child; `t.n` with no children is not a
|
||||
## shape the writer or sem produces, and this deliberately does not paper over
|
||||
## one appearing.
|
||||
result = if t.n == nil: nil else: t.n.firstSon
|
||||
|
||||
proc raisesNothing*(effects: PNode): bool =
|
||||
## Whether an effect list says DEFINITIVELY that nothing is raised: it is long
|
||||
## enough to have a raises slot at all, the slot is present, and it is empty.
|
||||
##
|
||||
## Every other shape — a list too short to carry the slot, an absent slot, a
|
||||
## non-empty one — means the effects are unspecified or non-empty, and a
|
||||
## caller must assume a raise. Stating it as the NEGATIVE is the point: the
|
||||
## safe default has to be "can raise", so the one narrow case that licenses
|
||||
## dropping an exception check is the one spelled out here, and a shape nobody
|
||||
## anticipated falls on the conservative side by construction rather than by
|
||||
## luck.
|
||||
result = effects != nil and effects.len >= effectListLen and
|
||||
effects[exceptionEffects] != nil and
|
||||
effects[exceptionEffects].safeLen == 0
|
||||
|
||||
template canRaiseImpl*(fnArg: typed): bool =
|
||||
block:
|
||||
let fn = fnArg
|
||||
var res: bool
|
||||
if fn.kind == nkSym and (fn.sym.magic notin magicsThatCanRaise or
|
||||
{sfImportc, sfInfixCall} * fn.sym.flags == {sfImportc} or
|
||||
sfGeneratedOp in fn.sym.flags):
|
||||
markCanRaiseBranch 1
|
||||
res = false
|
||||
elif fn.kind == nkSym and fn.sym.magic == mEcho:
|
||||
markCanRaiseBranch 2
|
||||
res = true
|
||||
elif fn.typ != nil and fn.typ.kind == tyProc and fn.typ.n != nil:
|
||||
markCanRaiseBranch 3
|
||||
let effects = effectsOf(fn.typ)
|
||||
if effects.kind == nkSym:
|
||||
# The historical shape: slot 0 used to be an `nkType` before the effects
|
||||
# moved in (see `newProcType`). Nothing to read, so nothing licenses a
|
||||
# raise.
|
||||
res = false
|
||||
else:
|
||||
# A proc-typed value with no explicit raises slot still has
|
||||
# unspecified effects, which sempass2 treats conservatively.
|
||||
# Codegen needs to do the same in order to keep goto-exception
|
||||
# checks after indirect/closure calls.
|
||||
res = not raisesNothing(effects)
|
||||
else:
|
||||
# A proc-typed value with no explicit raises slot still has
|
||||
# unspecified effects, which sempass2 treats conservatively.
|
||||
# Codegen needs to do the same in order to keep goto-exception
|
||||
# checks after indirect/closure calls.
|
||||
result = ((fn.typ.n[0].len < effectListLen) or
|
||||
fn.typ.n[0][exceptionEffects] == nil or
|
||||
fn.typ.n[0][exceptionEffects].safeLen > 0)
|
||||
else:
|
||||
result = false
|
||||
markCanRaiseBranch 0
|
||||
res = false
|
||||
res
|
||||
|
||||
proc canRaiseConservative*(fn: PNode): bool = canRaiseConservativeImpl(fn)
|
||||
|
||||
proc canRaise*(fn: PNode): bool = canRaiseImpl(fn)
|
||||
|
||||
proc toHumanStrImpl[T](kind: T, num: static int): string =
|
||||
result = $kind
|
||||
@@ -1749,8 +1886,13 @@ proc toHumanStr*(kind: TTypeKind): string =
|
||||
## strips leading `tk`
|
||||
result = toHumanStrImpl(kind, 2)
|
||||
|
||||
proc skipHiddenAddr*(n: PNode): PNode {.inline.} =
|
||||
(if n.kind == nkHiddenAddr: n[0] else: n)
|
||||
template skipHiddenAddrImpl*(nArg: typed): untyped =
|
||||
## Body shared with `bnode`'s `BNode` spelling — see `canRaiseImpl`.
|
||||
block:
|
||||
let sha = nArg
|
||||
(if sha.kind == nkHiddenAddr: sha.firstSon else: sha)
|
||||
|
||||
proc skipHiddenAddr*(n: PNode): PNode {.inline.} = skipHiddenAddrImpl(n)
|
||||
|
||||
proc isNewStyleConcept*(n: PNode): bool {.inline.} =
|
||||
assert n.kind == nkTypeClassTy
|
||||
|
||||
1299
compiler/ast2nif.nim
1299
compiler/ast2nif.nim
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@
|
||||
|
||||
import
|
||||
ast, astyaml, options, lineinfos, idents, rodutils,
|
||||
msgs
|
||||
msgs, bnode
|
||||
|
||||
import std/[hashes, intsets]
|
||||
import std/strutils except addf
|
||||
@@ -100,7 +100,7 @@ proc skipConvCastAndClosure*(n: PNode): PNode =
|
||||
result = result[1]
|
||||
else: break
|
||||
|
||||
proc sameValue*(a, b: PNode): bool =
|
||||
proc sameValue*[T: AnyNode](a, b: T): bool =
|
||||
result = false
|
||||
case a.kind
|
||||
of nkCharLit..nkUInt64Lit:
|
||||
@@ -639,9 +639,14 @@ proc getOrDefault*[T](t: TIdTable[T], key: ItemId): T =
|
||||
if index >= 0: result = t.data[index].val
|
||||
else: result = default(T)
|
||||
|
||||
template idTableGet*[T](t: TIdTable[T], key: PType | PSym): T =
|
||||
template idTableGet*[T](t: TIdTable[T], key: PSym): T =
|
||||
getOrDefault(t, key.itemId)
|
||||
|
||||
template idTableGet*[T](t: TIdTable[T], key: PType): T =
|
||||
## Type-keyed tables are BINDING tables: an `exactReplica` must find what its
|
||||
## original bound, hence `bindingId` and not the type's own identity.
|
||||
getOrDefault(t, key.bindingId)
|
||||
|
||||
proc idTableRawInsert[T](data: var TIdPairSeq[T], key: ItemId, val: T) =
|
||||
var h: Hash
|
||||
let keyId = toId(key)
|
||||
@@ -672,9 +677,12 @@ proc `[]=`*[T](t: var TIdTable[T], key: ItemId, val: T) =
|
||||
idTableRawInsert(t.data, key, val)
|
||||
inc(t.counter)
|
||||
|
||||
template idTablePut*[T](t: var TIdTable[T], key: PType | PSym, val: T) =
|
||||
template idTablePut*[T](t: var TIdTable[T], key: PSym, val: T) =
|
||||
t[key.itemId] = val
|
||||
|
||||
template idTablePut*[T](t: var TIdTable[T], key: PType, val: T) =
|
||||
t[key.bindingId] = val
|
||||
|
||||
iterator idTablePairs*[T](t: TIdTable[T]): tuple[key: ItemId, val: T] =
|
||||
for i in 0..high(t.data):
|
||||
if not isNil(t.data[i].key):
|
||||
@@ -732,7 +740,7 @@ proc listSymbolNames*(symbols: openArray[PSym]): string =
|
||||
result.add ", "
|
||||
result.add sym.name.s
|
||||
|
||||
proc isDiscriminantField*(n: PNode): bool =
|
||||
if n.kind == nkCheckedFieldExpr: sfDiscriminant in n[0][1].sym.flags
|
||||
elif n.kind == nkDotExpr: sfDiscriminant in n[1].sym.flags
|
||||
proc isDiscriminantField*(n: AnyNode): bool =
|
||||
if n.kind == nkCheckedFieldExpr: sfDiscriminant in n.firstSon.secondSon.sym.flags
|
||||
elif n.kind == nkDotExpr: sfDiscriminant in n.secondSon.sym.flags
|
||||
else: false
|
||||
|
||||
@@ -784,11 +784,16 @@ type
|
||||
# same id; there may be multiple copies of a type
|
||||
# in memory!
|
||||
# Keep in sync with PackedType
|
||||
itemId*: ItemId
|
||||
itemId*: ItemId # THE identity of this type: unique per instance, forever.
|
||||
# Names the type in the NIF cache and decides which
|
||||
# module owns its definition.
|
||||
kind*: TTypeKind # kind of type
|
||||
state*: ItemState
|
||||
uniqueId*: ItemId # due to a design mistake, we need to keep the real ID here as it
|
||||
# is required by the --incremental:on mode.
|
||||
bindingId*: ItemId # the id of the type this one is a REPLICA of (its own
|
||||
# `itemId` when it is not a replica). Only the generic
|
||||
# binding tables (`LayeredIdTable` & friends) key on it:
|
||||
# `exactReplica` produces a copy that must keep matching
|
||||
# its original in those tables. Never an identity.
|
||||
callConvImpl*: TCallingConvention # for procs
|
||||
flagsImpl*: TTypeFlags # flags of the type
|
||||
sonsImpl*: TTypeSeq # base types, etc.
|
||||
@@ -952,13 +957,45 @@ iterator items*(n: PNode): PNode =
|
||||
|
||||
iterator sons*(n: PNode): PNode =
|
||||
## Iterates over the children of `n`. Preferred over `for i in 0..<n.len: n[i]`
|
||||
## as it does not rely on random indexed access (see doc/ic_backend_nif_native.md).
|
||||
## as it does not rely on random indexed access, and over `for x in n.sons`,
|
||||
## which reads the raw FIELD and so skips the `len` hook that materialises a
|
||||
## deferred `nfLazyBody` body — over such a body that loop silently visits
|
||||
## nothing. See `compiler/bnode.nim` for the backend vocabulary this feeds.
|
||||
for i in 0..<n.safeLen: yield n[i]
|
||||
|
||||
iterator isons*(n: PNode): tuple[i: int, n: PNode] =
|
||||
## Like `sons` but also yields the child index. Replaces
|
||||
## `for i in 0..<n.len: ... n[i] ...` when `i` itself is still needed.
|
||||
for i in 0..<n.safeLen: yield (i, n[i])
|
||||
iterator isons*(n: PNode; start = 0): tuple[i: int, n: PNode] =
|
||||
## Like `sons` but also yields the child index, and optionally skips the first
|
||||
## `start` children. Replaces `for i in start..<n.len: ... n[i] ...` when `i`
|
||||
## itself is still needed — for a parameter position, a `needTmp[i-1]` lookup,
|
||||
## a parallel index into the routine's `PType`, and so on. `start` is almost
|
||||
## always 1, to step over a call's callee or a case statement's selector.
|
||||
##
|
||||
## Use `sonsFrom` instead when the index is only ever used to subscript `n`.
|
||||
for i in start..<n.safeLen: yield (i, n[i])
|
||||
|
||||
iterator sonsFrom*(n: PNode; start: int): PNode =
|
||||
## `sons` skipping the first `start` children. Replaces
|
||||
## `for i in start..<n.len: ... n[i] ...`, which is by far the commonest
|
||||
## indexed shape in the code generator — `start` is almost always 1, to step
|
||||
## over a case/try statement's selector or a call's callee.
|
||||
for i in start..<n.safeLen: yield n[i]
|
||||
|
||||
iterator sonsButLast*(n: PNode; count = 1): PNode =
|
||||
## `sons` without the last `count` children. Replaces `for i in 0..<n.len-1:
|
||||
## ... n[i] ...`, which is what an `nkOfBranch`/`nkExceptBranch` walk looks
|
||||
## like: the last child is the branch BODY, the ones before it are the labels
|
||||
## it matches. `count = 2` is the `nkVarTuple`/`nkIdentDefs` shape, whose last
|
||||
## two children are the type and the value. A `Cursor` can serve this with a
|
||||
## single pass and `count` nodes of lookahead; the indexed form has to re-walk
|
||||
## the children for every label.
|
||||
##
|
||||
## Use `isonsButLast` instead when the index is still needed.
|
||||
for i in 0 ..< n.safeLen - count: yield n[i]
|
||||
|
||||
iterator isonsButLast*(n: PNode; count = 1): tuple[i: int, n: PNode] =
|
||||
## Like `sonsButLast` but also yields the child index — for a tuple field
|
||||
## position, a parallel index into the tuple's `PType`, and so on.
|
||||
for i in 0 ..< n.safeLen - count: yield (i, n[i])
|
||||
|
||||
when defined(useNodeIds):
|
||||
const nodeIdToDebug* = -1 # 2322968
|
||||
@@ -1041,10 +1078,56 @@ proc newStrNode*(strVal: string; info: TLineInfo): PNode =
|
||||
# handling for IC, they end up in IC indexes etc. Thus we "log" them in the module graph
|
||||
# and to pass them around to the NIF writer. This is not very elegant but it works.
|
||||
|
||||
const
|
||||
InstanceDisambBit* = 0x4000_0000'i32
|
||||
## Set in the `disamb` of routine instances whose value is content-derived
|
||||
## (see `modulegraphs.setInstanceDisamb`); keeps them disjoint from the
|
||||
## small counter range ordinary symbols draw from, so the NIF name
|
||||
## `name.disamb.module` stays collision-free within a module.
|
||||
HookDisambBit* = 0x2000_0000'i32
|
||||
## Set in the `disamb` of synthesized type-bound operators and `$enum`
|
||||
## procs whose value is content-derived (see `modulegraphs.setHookDisamb`);
|
||||
## disjoint from both the small counter range and `InstanceDisambBit`.
|
||||
##
|
||||
## Both live here rather than in `modulegraphs` because `ast2nif` — which
|
||||
## cannot import that module — names symbols by them.
|
||||
|
||||
proc backendMintedDisamb*(s: PSym): int32 {.inline.} =
|
||||
## The integer that identifies a BACKEND-MINTED symbol (`isBackendMinted`) in
|
||||
## every name derived from it: its NIF name (`ast2nif.toNifSymName`) and its C
|
||||
## name (`mangleutils.mangleProcNameExt`, `ccgutils.makeUnique`).
|
||||
##
|
||||
## Two cases, and the whole point of having ONE function is that all three
|
||||
## sites take the same one:
|
||||
##
|
||||
## * A lifted HOOK's `disamb` is CONTENT-derived (`modulegraphs.setHookDisamb`),
|
||||
## so it is identical in every process. Such a hook really does cross process
|
||||
## boundaries — `lower` mints the env hooks of nested routines while `cg`
|
||||
## mints those of the module's top level, and both land in the same
|
||||
## translation unit — and its C name is also baked into emit-everywhere RTTI
|
||||
## tables. `itemId.item` would differ per process, so two unrelated hooks
|
||||
## collided on one `_c<item>` and the merge stage kept a single body for both
|
||||
## (C accepted the mistyped call, C++ rejected it).
|
||||
## * Otherwise `itemId.item` — the writer's dedup identity, unique per `@bk`
|
||||
## sym. `disamb` cannot serve here: a module's `:env` syms are minted from TWO
|
||||
## id spaces (the backend `lower` stage's idgen and sem's `vmTransfIdgen`)
|
||||
## whose `disambTable`s each start `:env` at the same low count, so a
|
||||
## macro-lowered and a backend-lowered `:env` collide on `:env.2.<mod>@bk`.
|
||||
##
|
||||
## The loader copies the name's numeric component back into `disamb`, so after a
|
||||
## round trip `disamb` equals this value and `ast2nif.globalName` — which always
|
||||
## reads `disamb` — agrees with the name the writer produced.
|
||||
##
|
||||
## This rule used to be written out at each of the three sites. They drifted:
|
||||
## `toNifSymName` lacked the hook exception, so a content-derived value was
|
||||
## overwritten by the loader and two backend hooks merged into one C function.
|
||||
if (s.disamb and HookDisambBit) != 0'i32: s.disamb
|
||||
else: s.itemId.item
|
||||
|
||||
type
|
||||
LogEntryKind* = enum
|
||||
HookEntry, ConverterEntry, MethodEntry, EnumToStrEntry, GenericInstEntry,
|
||||
PureEnumEntry
|
||||
PureEnumEntry, CppMemberEntry
|
||||
LogEntry* = object
|
||||
kind*: LogEntryKind
|
||||
op*: TTypeAttachedOp
|
||||
@@ -1091,7 +1174,7 @@ proc forcePartial*(s: PSym) =
|
||||
proc forcePartial*(t: PType) =
|
||||
## Resets all impl-fields to their default values and sets state to Partial.
|
||||
## This is useful for creating a stub type that can be lazily loaded later.
|
||||
## The fields itemId, kind, uniqueId are preserved.
|
||||
## The fields itemId, kind, bindingId are preserved.
|
||||
t.state = Partial
|
||||
t.callConvImpl = ccNimCall
|
||||
t.flagsImpl = {}
|
||||
|
||||
1132
compiler/bnode.nim
Normal file
1132
compiler/bnode.nim
Normal file
File diff suppressed because it is too large
Load Diff
335
compiler/bodynav.nim
Normal file
335
compiler/bodynav.nim
Normal file
@@ -0,0 +1,335 @@
|
||||
#
|
||||
#
|
||||
# The Nim Compiler
|
||||
# (c) Copyright 2026 Andreas Rumpf
|
||||
#
|
||||
# See the file "copying.txt", included in this
|
||||
# distribution, for details about the copyright.
|
||||
#
|
||||
|
||||
## `BodyNav` — a scope-chained navigator over a `.bif` routine body.
|
||||
##
|
||||
## Ported from Nimony's `nimony/typenav.nim` (`TypeCache` / `TypeScope`). The
|
||||
## idea being stolen is not the type algebra — we do not need it, `typ` returns
|
||||
## a fully materialized `PType` — but the SHAPE of the resolution context:
|
||||
##
|
||||
## * a chain of scope frames, each a small table, linked to its parent;
|
||||
## * `openScope` / `closeScope` / `registerLocal`, called BY THE TRAVERSAL as it
|
||||
## descends and as it walks past each definition;
|
||||
## * a lookup that consults the chain and, on a miss, falls through to the
|
||||
## module index (`typenav`'s `tryLoadSym`; here the decoder's own
|
||||
## `symFromCursor`).
|
||||
##
|
||||
## The consequence is the point: the scope is a PRODUCT OF THE WALK. Nothing is
|
||||
## snapshotted, so nothing can be stale, and a reader that starts at the top of
|
||||
## a body and descends always has exactly the definitions it has already passed.
|
||||
##
|
||||
## WHAT THIS REPLACES. `ast2nif.PendingBody` stashes `localSyms` — a COPY of the
|
||||
## enclosing sym def's local symbols, taken when the body was deferred — and
|
||||
## `bnode`'s `BodyScope` then copies it again. `materializeLazyBody` loads the
|
||||
## body with its own `var pb`, so every definition the load creates lands in a
|
||||
## table that is discarded on return. A cursor-side reader holding the earlier
|
||||
## copy therefore cannot see them, and would mint its own `PSym` for the same
|
||||
## name: two objects, one symbol.
|
||||
##
|
||||
## HOW BIG THAT PROBLEM ACTUALLY IS, measured rather than assumed. Build with
|
||||
## `-d:icLocalSymStats` and every process reports its `localSyms` traffic on
|
||||
## exit. Over a full `--ic:on` build of the standard-library closure (104
|
||||
## backend processes):
|
||||
##
|
||||
## localHit=0 fieldStub=2 miss=0 sdReg=5902 extractReg=45
|
||||
##
|
||||
## Definitions register constantly and NOT ONE use ever resolves through the
|
||||
## table. The reason is `ast2nif.isLocalSym`, which returns a hardwired `false`:
|
||||
## every symbol is emitted with a module suffix and resolves through the
|
||||
## decoder's global `syms` memo, so both spellings get the same `PSym` whatever
|
||||
## either one has cached. The 5902 registrations are object FIELDS, whose uses
|
||||
## deliberately go to `loadFieldStub` instead.
|
||||
##
|
||||
## So the stale snapshot is a LATENT hazard, not a live bug, and this module is
|
||||
## not a bug fix — it is the mechanism that keeps it latent once `isLocalSym`
|
||||
## stops being `false`, or once a body-local name appears for any other reason.
|
||||
## Said plainly so nobody has to re-derive it: today the nav changes no answers,
|
||||
## and the grinder in `cgen` proves that by requiring the navigated symbol to be
|
||||
## the same object the `PNode` loader produced, at every node of every body.
|
||||
##
|
||||
## It is not decorative either, and that also has a number. Over the same build,
|
||||
## the grinder's traversal reports `navHits=42236 navFallbacks=12658
|
||||
## navRegistered=311`: the chain answers 77% of lookups, and 311 definitions are
|
||||
## registered by the walk rather than read from a table someone filled in
|
||||
## earlier. Sabotaging the key (truncating it to three characters, so
|
||||
## `c_fwrite` and `c_fflush` collide) makes the grinder fail on the first body
|
||||
## it reaches — so a clean run means the resolution is right, not that the
|
||||
## lookup never happened.
|
||||
##
|
||||
## FIELDS ARE NOT REGISTERED, and that is deliberate. `loadFieldStub` mints a
|
||||
## fresh stub per use because two distinct fields can share a name (and a
|
||||
## position) across types — `a.x` and `b.x` in one body are two different
|
||||
## symbols. Caching a field by its bare name would hand the second use the first
|
||||
## one's stub, and its type. The nav skips field names entirely and leaves that
|
||||
## path exactly as it was.
|
||||
|
||||
import std / tables
|
||||
import ast, ast2nif
|
||||
|
||||
when defined(nimPreviewSlimSystem):
|
||||
import std / assertions
|
||||
|
||||
import "../dist/nimony/src/lib/nifcore" except pool
|
||||
|
||||
type
|
||||
NavScopeKind* = enum
|
||||
nsBlock, ## an ordinary nested scope
|
||||
nsRoutine ## a routine boundary — see `crossedRoutines`
|
||||
|
||||
NavScope {.acyclic.} = ref object
|
||||
locals: Table[string, PSym]
|
||||
parent: NavScope
|
||||
kind: NavScopeKind
|
||||
|
||||
BridgeTables* = ref object
|
||||
## The side tables of an IN-PROCESS bridged buffer (`nodebridge.nim`).
|
||||
## A `.bif` names its symbols because the reader is a different process; a
|
||||
## buffer built and read inside ONE process does not have to, and paying the
|
||||
## name round trip anyway would be worse than pointless — it is what makes
|
||||
## the file path unable to give a field a stable identity (`loadFieldStub`
|
||||
## mints per use). Here a symbol reference is an index and resolution hands
|
||||
## back the very same object, so `symAt` is exact and idempotent for every
|
||||
## symbol kind, fields included.
|
||||
syms*: seq[PSym]
|
||||
types*: seq[PType]
|
||||
origins*: Table[int, PNode]
|
||||
## Token position -> the `PNode` encoded there, so a cursor can name the
|
||||
## node it came from. Lives here rather than in `BridgeBuf` because the
|
||||
## lookup has to be reachable from wherever a location is built, which is
|
||||
## everywhere in the generator — the same reason `syms` is here.
|
||||
buf*: ptr TokenBuf
|
||||
## The buffer `origins` is keyed against; `cursorToPosition` needs it.
|
||||
## Borrowed, not owned: it points into the `BridgeBuf` that a scoped
|
||||
## `withBridge` is currently reading, and never outlives it.
|
||||
|
||||
BodyNav* = object
|
||||
## The resolution context for ONE routine body. `base` is what the decoder
|
||||
## itself needs (the owning module plus a table `loadSymStub` can write
|
||||
## into); the frame chain on top of it is this module's contribution.
|
||||
##
|
||||
## `bridge` is non-nil only while reading a bridged buffer. It is consulted
|
||||
## FIRST and, when it answers, it answers exactly — there is no fallback,
|
||||
## because a `(bsym …)` index that the tables cannot resolve is a corrupt
|
||||
## buffer, not a cache miss.
|
||||
base*: BodyScope
|
||||
bridge*: BridgeTables
|
||||
current: NavScope
|
||||
hits*: int ## resolved from the chain
|
||||
fallbacks*: int ## resolved through the decoder
|
||||
registered*: int ## definitions the walk registered
|
||||
|
||||
proc originAt*(t: BridgeTables; c: Cursor): PNode =
|
||||
## The source node a cursor was encoded from, or nil when there is none (a
|
||||
## `DotToken`, or a cursor that is not at a node head).
|
||||
if t == nil or t.buf == nil: return nil
|
||||
result = t.origins.getOrDefault(cursorToPosition(t.buf[], c), nil)
|
||||
|
||||
proc initBodyNav*(base: sink BodyScope): BodyNav =
|
||||
## A nav over a body, seeded with whatever resolution context the decoder
|
||||
## handed out. The root frame is a routine frame: a body IS one.
|
||||
result = BodyNav(base: base,
|
||||
current: NavScope(locals: initTable[string, PSym](),
|
||||
parent: nil, kind: nsRoutine))
|
||||
|
||||
proc initBridgeNav*(tables: BridgeTables): BodyNav =
|
||||
## A nav over an in-process bridged buffer. `base` stays empty — a bridged
|
||||
## buffer names nothing, so there is nothing for the decoder to resolve — but
|
||||
## the ROOT FRAME still has to exist: a walk brackets its descent with
|
||||
## `openScope`/`closeScope`, and a nav without a root frame makes the first
|
||||
## `closeScope` pop past the bottom.
|
||||
result = BodyNav(bridge: tables,
|
||||
current: NavScope(locals: initTable[string, PSym](),
|
||||
parent: nil, kind: nsRoutine))
|
||||
|
||||
proc openScope*(nav: var BodyNav; kind = nsBlock) {.inline.} =
|
||||
nav.current = NavScope(locals: initTable[string, PSym](),
|
||||
parent: nav.current, kind: kind)
|
||||
|
||||
proc closeScope*(nav: var BodyNav) {.inline.} =
|
||||
doAssert nav.current.parent != nil, "closeScope past the root frame"
|
||||
nav.current = nav.current.parent
|
||||
|
||||
template withScope*(nav: var BodyNav; kind: NavScopeKind; body: untyped) =
|
||||
openScope(nav, kind)
|
||||
try:
|
||||
body
|
||||
finally:
|
||||
closeScope(nav)
|
||||
|
||||
proc registerLocal*(nav: var BodyNav; name: string; s: PSym) {.inline.} =
|
||||
## Record a definition the walk has just passed, in the innermost frame.
|
||||
nav.current.locals[name] = s
|
||||
inc nav.registered
|
||||
|
||||
proc lookupLocal*(nav: BodyNav; name: string): PSym =
|
||||
## The chain only. `nil` when nothing in scope carries this name.
|
||||
var it {.cursor.} = nav.current
|
||||
while it != nil:
|
||||
let s = it.locals.getOrDefault(name)
|
||||
if s != nil: return s
|
||||
it = it.parent
|
||||
result = nil
|
||||
|
||||
proc crossedRoutines*(nav: BodyNav; name: string): int =
|
||||
## How many routine frames separate the use from the definition — 0 when the
|
||||
## definition is in the current routine. `typenav` computes the same thing as
|
||||
## `LocalInfo.crossedProc`, and it is what tells a closure pass that a name is
|
||||
## captured rather than local. Nothing consumes it here yet; it is the reason
|
||||
## the frames carry a kind at all, and dropping the kind would make it
|
||||
## unrecoverable later.
|
||||
var it {.cursor.} = nav.current
|
||||
var crossed = 0
|
||||
while it != nil:
|
||||
if it.locals.getOrDefault(name) != nil: return crossed
|
||||
if it.kind == nsRoutine: inc crossed
|
||||
it = it.parent
|
||||
result = -1
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Names
|
||||
#
|
||||
# A symbol reaches the reader in four shapes and they all NAME the same thing;
|
||||
# `navName` is the one place that knows which token holds the name, so the
|
||||
# lookup key is derived identically no matter which wrapper the writer chose.
|
||||
|
||||
proc navName*(n: Cursor): string =
|
||||
## The NIF name a token denotes, or `""` when the token names no symbol.
|
||||
case nifcore.kind(n)
|
||||
of Symbol, SymbolDef:
|
||||
result = symName(n)
|
||||
of TagLit:
|
||||
let tag = n.tags.tagName(cursorTagId(n))
|
||||
if tag == symDefTagName:
|
||||
let name = childCursor(n)
|
||||
result = if nifcore.kind(name) in {Symbol, SymbolDef}: symName(name) else: ""
|
||||
elif tag == hiddenTypeTagName:
|
||||
# `(ht <type> <sym>)`
|
||||
var inner = childCursor(n)
|
||||
skip inner
|
||||
result = navName(inner)
|
||||
elif tag == symNodeFlagsTagName:
|
||||
# `(nflags <flags> <symnode>)`
|
||||
var inner = childCursor(n)
|
||||
skip inner
|
||||
result = navName(inner)
|
||||
else:
|
||||
result = ""
|
||||
else:
|
||||
result = ""
|
||||
|
||||
proc symToken*(n: Cursor): Cursor =
|
||||
## The token that actually NAMES the symbol, with the wrappers stripped.
|
||||
## `loadSymStub` accepts a `Symbol`, a `SymbolDef` or an `(sd ...)` and
|
||||
## rejects everything else, so the `(ht ...)` / `(nflags ...)` forms have to be
|
||||
## peeled here rather than at each call site — the same peeling `navName` does
|
||||
## for the key, kept beside it so the two cannot drift apart.
|
||||
result = n
|
||||
while nifcore.kind(result) == TagLit:
|
||||
let tag = result.tags.tagName(cursorTagId(result))
|
||||
if tag == hiddenTypeTagName or tag == symNodeFlagsTagName:
|
||||
var inner = childCursor(result)
|
||||
skip inner # the explicit type / the node flags
|
||||
result = inner
|
||||
else:
|
||||
break
|
||||
|
||||
proc cacheFrame(nav: var BodyNav): NavScope =
|
||||
## Where a decoder-resolved name is remembered: the nearest ROUTINE frame.
|
||||
## Not the innermost frame — a `.bif` name is unique within its module (see
|
||||
## `isLocalSym`), so its meaning cannot change between frames, and caching it
|
||||
## deeper would only throw it away sooner. Not the root either, so that a
|
||||
## nested routine's names die with the nested routine.
|
||||
result = nav.current
|
||||
while result.kind != nsRoutine and result.parent != nil:
|
||||
result = result.parent
|
||||
|
||||
proc bridgeIndex(n: Cursor; tag: string): int =
|
||||
## The `<intlit>` payload of a `(bsym …)` / `(btyp …)` token, or -1 when `n`
|
||||
## is not that shape.
|
||||
result = -1
|
||||
if nifcore.kind(n) == TagLit and n.tags.tagName(cursorTagId(n)) == tag:
|
||||
let payload = childCursor(n)
|
||||
if nifcore.kind(payload) == IntLit:
|
||||
result = int(nifcore.intVal(payload))
|
||||
|
||||
proc symAt*(nav: var BodyNav; n: Cursor): PSym =
|
||||
## The symbol a token names: the bridge first (exact), then the chain, then
|
||||
## the decoder.
|
||||
if nav.bridge != nil:
|
||||
let idx = bridgeIndex(symToken(n), bridgeSymTagName)
|
||||
if idx >= 0:
|
||||
doAssert idx < nav.bridge.syms.len,
|
||||
"bridged sym index out of range: " & $idx
|
||||
inc nav.hits
|
||||
return nav.bridge.syms[idx]
|
||||
let name = navName(n)
|
||||
if name.len > 0:
|
||||
let cached = lookupLocal(nav, name)
|
||||
if cached != nil:
|
||||
inc nav.hits
|
||||
return cached
|
||||
inc nav.fallbacks
|
||||
result = symFromCursor(program, symToken(n), nav.base)
|
||||
if result != nil and name.len > 0 and not isFieldNifName(name):
|
||||
cacheFrame(nav).locals[name] = result
|
||||
|
||||
proc typeAt*(nav: var BodyNav; n: Cursor): PType =
|
||||
## Types are not navigated: `ast2nif` already materializes them lazily from
|
||||
## the module's type index, keyed by name, so there is no per-body state to
|
||||
## keep and nothing a frame could cache that the decoder does not already.
|
||||
if nav.bridge != nil:
|
||||
if nifcore.kind(n) == DotToken: return nil
|
||||
let idx = bridgeIndex(n, bridgeTypeTagName)
|
||||
if idx >= 0:
|
||||
doAssert idx < nav.bridge.types.len,
|
||||
"bridged type index out of range: " & $idx
|
||||
return nav.bridge.types[idx]
|
||||
result = typeFromCursor(program, n, nav.base)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Registration during a walk
|
||||
|
||||
proc registerDefHere*(nav: var BodyNav; n: Cursor): bool {.discardable.} =
|
||||
## Register `n` if `n` ITSELF is a definition; do not descend. This is the
|
||||
## incremental half: a walk calls it on each child before recursing into it,
|
||||
## so a use can only resolve from the chain to a definition the walk has
|
||||
## already passed. A use that precedes its definition simply misses and falls
|
||||
## through to the decoder, which is the behaviour there was before — the nav
|
||||
## degrades to the old path rather than answering wrongly.
|
||||
result = false
|
||||
if nifcore.kind(n) == TagLit and
|
||||
n.tags.tagName(cursorTagId(n)) == symDefTagName:
|
||||
let name = navName(n)
|
||||
if name.len > 0 and not isFieldNifName(name):
|
||||
let s = symFromCursor(program, n, nav.base)
|
||||
if s != nil:
|
||||
registerLocal(nav, name, s)
|
||||
result = true
|
||||
|
||||
proc registerDefs*(nav: var BodyNav; n: Cursor) =
|
||||
## Register every definition in the SUBTREE at `n` — `typenav.registerLocals`
|
||||
## with the recursion left in, because a Nim body puts `nkIdentDefs` under an
|
||||
## `nkVarSection` under the statement list rather than declaring at one level.
|
||||
##
|
||||
## Call it on entering a scope to get the eager behaviour (every definition
|
||||
## known before any use is resolved, which is what a RANDOM-ACCESS reader
|
||||
## needs), or per statement to get the incremental one (only definitions
|
||||
## already walked past are visible, which is what a real pass wants and what
|
||||
## makes use-before-def detectable rather than silently working).
|
||||
if nifcore.kind(n) == TagLit and
|
||||
n.tags.tagName(cursorTagId(n)) == symDefTagName:
|
||||
let name = navName(n)
|
||||
if name.len > 0 and not isFieldNifName(name):
|
||||
let s = symFromCursor(program, n, nav.base)
|
||||
if s != nil: registerLocal(nav, name, s) # `(sd ...)` needs no peeling
|
||||
return
|
||||
var c = childCursor(n)
|
||||
while c.hasMore:
|
||||
registerDefs(nav, c)
|
||||
skip c
|
||||
@@ -326,7 +326,7 @@ proc startStruct(obj: var Builder; m: BModule; t: PType; name: string; baseType:
|
||||
# rest of the options add a field or don't need it due to inheritance,
|
||||
# we need to add the dummy field for uncheckedarray ahead of time
|
||||
# so that it remains trailing
|
||||
if t.itemId notin m.g.graph.memberProcsPerType and
|
||||
if t.bindingId notin m.g.graph.memberProcsPerType and
|
||||
t.n != nil and t.n.len == 1 and t.n[0].kind == nkSym and
|
||||
t.n[0].sym.typ.skipTypes(abstractInst).kind == tyUncheckedArray:
|
||||
# only consists of flexible array field, add *initial* dummy field
|
||||
@@ -341,7 +341,7 @@ proc startStruct(obj: var Builder; m: BModule; t: PType; name: string; baseType:
|
||||
|
||||
proc finishStruct(obj: var Builder; m: BModule; t: PType; info: StructBuilderInfo) =
|
||||
if info.baseKind == bcNone and info.preFieldsLen == obj.buf.len and
|
||||
t.itemId notin m.g.graph.memberProcsPerType:
|
||||
t.bindingId notin m.g.graph.memberProcsPerType:
|
||||
# no fields were added, add dummy field
|
||||
obj.addField(name = "dummy", typ = CChar)
|
||||
if info.named:
|
||||
|
||||
@@ -9,9 +9,19 @@
|
||||
#
|
||||
# included from cgen.nim
|
||||
|
||||
proc canRaiseDisp(p: BProc; n: PNode): bool =
|
||||
proc canRaiseDisp(p: BProc; n: AnyNode): bool =
|
||||
# we assume things like sysFatal cannot raise themselves
|
||||
if n.kind == nkSym and {sfNeverRaises, sfImportc, sfCompilerProc} * n.sym.flags != {}:
|
||||
# 5 = "decided here, neither predicate ran". Without resetting, the marker
|
||||
# keeps whatever the PREVIOUS call left in it and the early return below
|
||||
# attributes this answer to a branch that did not execute — which is how the
|
||||
# first run of this differential came to claim effect-list coverage it did
|
||||
# not have. Both short-circuits below leave it at 5.
|
||||
markCanRaiseBranch 5
|
||||
if n.kind == nkSym and n.sym.kind == skMethod:
|
||||
# A base method may be overridden by a branch with a wider exception set.
|
||||
# Its inferred effects describe only the base body, not every vtable target.
|
||||
result = true
|
||||
elif n.kind == nkSym and {sfNeverRaises, sfImportc, sfCompilerProc} * n.sym.flags != {}:
|
||||
result = false
|
||||
elif optPanics in p.config.globalOptions or
|
||||
(n.kind == nkSym and sfSystemModule in getModule(n.sym).flags and
|
||||
@@ -21,8 +31,22 @@ proc canRaiseDisp(p: BProc; n: PNode): bool =
|
||||
else:
|
||||
# we have to be *very* conservative:
|
||||
result = canRaiseConservative(n)
|
||||
when defined(icCanRaiseLog):
|
||||
# `canRaise` reads the raises spec off `fn.typ.n`, and under `--ic:on` that
|
||||
# node came back from a `.bif`. Whether it came back INTACT is not something
|
||||
# the `BNode`/`PNode` grinder can answer — both spellings ask the same
|
||||
# `PType` and so agree however wrong it is. The only oracle is the same
|
||||
# program built without IC. Log the verdict per callee; the two builds must
|
||||
# produce the same one.
|
||||
if n.kind == nkSym:
|
||||
logCanRaise(n.sym, result)
|
||||
|
||||
proc preventNrvo(p: BProc; dest, le, ri: PNode): bool =
|
||||
proc preventNrvo(p: BProc; dest, le: PNode; ri: AnyNode): bool =
|
||||
## `dest` and `le` stay `PNode`s: they are DESTINATIONS, which the whole call
|
||||
## family keeps as `PNode`s so they can be nil and so they can be handed to
|
||||
## the alias analysis, and it is also what keeps the `warnObservableStores`
|
||||
## message able to RENDER `le` — rendering being a capability the cursor seam
|
||||
## does not have at all. `ri`, the call being generated, is a cursor.
|
||||
proc locationEscapes(p: BProc; le: PNode; inTryStmt: bool): bool =
|
||||
result = false
|
||||
var n = le
|
||||
@@ -42,16 +66,17 @@ proc preventNrvo(p: BProc; dest, le, ri: PNode): bool =
|
||||
nkCheckedFieldExpr:
|
||||
n = n.firstSon
|
||||
of nkHiddenStdConv, nkHiddenSubConv, nkConv:
|
||||
n = n[1]
|
||||
n = n.secondSon
|
||||
else:
|
||||
# cannot analyse the location; assume the worst
|
||||
return true
|
||||
|
||||
result = false
|
||||
if le != nil:
|
||||
for i in 1..<ri.len:
|
||||
let r = ri[i]
|
||||
if isPartOf(le, r, {pfStructural}) != arNo: return true
|
||||
for r in sonsFrom(ri, 1):
|
||||
# `isPartOf` compares field symbols by identity and so has not moved to
|
||||
# the seam; `origin` hands it the same nodes it always compared.
|
||||
if isPartOf(le, origin(r), {pfStructural}) != arNo: return true
|
||||
# we use the weaker 'canRaise' here in order to prevent too many
|
||||
# annoying warnings, see #14514
|
||||
if canRaise(ri.firstSon) and
|
||||
@@ -59,11 +84,10 @@ proc preventNrvo(p: BProc; dest, le, ri: PNode): bool =
|
||||
message(p.config, le.info, warnObservableStores, $le)
|
||||
# bug #19613 prevent dangerous aliasing too:
|
||||
if dest != nil and dest != le:
|
||||
for i in 1..<ri.len:
|
||||
let r = ri[i]
|
||||
if isPartOf(dest, r, {pfStructural}) != arNo: return true
|
||||
for r in sonsFrom(ri, 1):
|
||||
if isPartOf(dest, origin(r), {pfStructural}) != arNo: return true
|
||||
|
||||
proc hasNoInit(call: PNode): bool {.inline.} =
|
||||
proc hasNoInit(call: AnyNode): bool {.inline.} =
|
||||
result = call.firstSon.kind == nkSym and sfNoInit in call.firstSon.sym.flags
|
||||
|
||||
proc isHarmlessStore(p: BProc; canRaise: bool; d: TLoc): bool =
|
||||
@@ -95,7 +119,11 @@ proc cleanupTemp(p: BProc; returnType: PType, tmp: TLoc): bool =
|
||||
else:
|
||||
result = false
|
||||
|
||||
proc fixupCall(p: BProc, le, ri: PNode, d: var TLoc,
|
||||
# `le` — the assignment DESTINATION — stays a `PNode` throughout this family.
|
||||
# It is nilable (`genCall` passes nil, and a cursor has no standalone nil), and
|
||||
# it is what `preventNrvo` and `isPartOf` are handed, both of which are still
|
||||
# `PNode`-typed. `ri`, the expression being generated, is the part that moves.
|
||||
proc fixupCall(p: BProc, le: PNode, ri: AnyNode, d: var TLoc,
|
||||
result: var Builder, call: var CallBuilder) =
|
||||
let canRaise = p.config.exc == excGoto and canRaiseDisp(p, ri.firstSon)
|
||||
genLineDir(p, ri)
|
||||
@@ -179,14 +207,14 @@ proc fixupCall(p: BProc, le, ri: PNode, d: var TLoc,
|
||||
|
||||
proc genBoundsCheck(p: BProc; arr, a, b: TLoc; arrTyp: PType)
|
||||
|
||||
proc reifiedOpenArray(n: PNode): bool {.inline.} =
|
||||
proc reifiedOpenArray(n: AnyNode): bool {.inline.} =
|
||||
var x = n
|
||||
while true:
|
||||
case x.kind
|
||||
of {nkAddr, nkHiddenAddr, nkHiddenDeref}:
|
||||
x = x.firstSon
|
||||
of nkHiddenStdConv:
|
||||
x = x[1]
|
||||
x = x.secondSon
|
||||
else:
|
||||
break
|
||||
if x.kind == nkSym and x.sym.kind == skParam:
|
||||
@@ -194,10 +222,10 @@ proc reifiedOpenArray(n: PNode): bool {.inline.} =
|
||||
else:
|
||||
result = true
|
||||
|
||||
proc genOpenArraySlice(p: BProc; q: PNode; formalType, destType: PType; prepareForMutation = false): (Rope, Rope) =
|
||||
var a = initLocExpr(p, q[1])
|
||||
var b = initLocExpr(p, q[2])
|
||||
var c = initLocExpr(p, q[3])
|
||||
proc genOpenArraySlice(p: BProc; q: AnyNode; formalType, destType: PType; prepareForMutation = false): (Rope, Rope) =
|
||||
var a = initLocExpr(p, q.secondSon)
|
||||
var b = initLocExpr(p, son(q, 2))
|
||||
var c = initLocExpr(p, son(q, 3))
|
||||
# bug #23321: In the function mapType, ptrs (tyPtr, tyVar, tyLent, tyRef)
|
||||
# are mapped into ctPtrToArray, the dereference of which is skipped
|
||||
# in the `genDeref`. We need to skip these ptrs here
|
||||
@@ -223,7 +251,7 @@ proc genOpenArraySlice(p: BProc; q: PNode; formalType, destType: PType; prepareF
|
||||
let lit = cIntLiteral(first)
|
||||
result = (cCast(ptrType(dest), cOp(Add, NimInt, ra, cOp(Sub, NimInt, rb, lit))), lengthExpr)
|
||||
of tyOpenArray, tyVarargs:
|
||||
let data = if reifiedOpenArray(q[1]): dotField(ra, "Field0") else: ra
|
||||
let data = if reifiedOpenArray(q.secondSon): dotField(ra, "Field0") else: ra
|
||||
result = (cCast(ptrType(dest), cOp(Add, NimInt, data, rb)), lengthExpr)
|
||||
of tyUncheckedArray, tyCstring:
|
||||
result = (cCast(ptrType(dest), cOp(Add, NimInt, ra, rb)), lengthExpr)
|
||||
@@ -257,26 +285,26 @@ proc genOpenArraySlice(p: BProc; q: PNode; formalType, destType: PType; prepareF
|
||||
result = ("", "")
|
||||
internalError(p.config, "openArrayLoc: " & typeToString(a.t))
|
||||
|
||||
proc openArrayLoc(p: BProc, formalType: PType, n: PNode; result: var Builder) =
|
||||
proc openArrayLoc(p: BProc, formalType: PType, n: AnyNode; result: var Builder) =
|
||||
var q = skipConv(n)
|
||||
var skipped = false
|
||||
while q.kind == nkStmtListExpr and q.len > 0:
|
||||
while q.kind == nkStmtListExpr and q.hasSons:
|
||||
skipped = true
|
||||
q = q.lastSon
|
||||
if getMagic(q) == mSlice:
|
||||
# magic: pass slice to openArray:
|
||||
if skipped:
|
||||
q = skipConv(n)
|
||||
while q.kind == nkStmtListExpr and q.len > 0:
|
||||
for i in 0..<q.len-1:
|
||||
genStmts(p, q[i])
|
||||
while q.kind == nkStmtListExpr and q.hasSons:
|
||||
for it in sonsButLast(q):
|
||||
genStmts(p, it)
|
||||
q = q.lastSon
|
||||
let (x, y) = genOpenArraySlice(p, q, formalType, n.typ.elementType)
|
||||
result.add(x)
|
||||
result.addArgumentSeparator()
|
||||
result.add(y)
|
||||
else:
|
||||
var a = initLocExpr(p, if n.kind == nkHiddenStdConv: n[1] else: n)
|
||||
var a = initLocExpr(p, if n.kind == nkHiddenStdConv: n.secondSon else: n)
|
||||
case skipTypes(a.t, abstractVar+{tyStatic}).kind
|
||||
of tyOpenArray, tyVarargs:
|
||||
let ra = rdLoc(a)
|
||||
@@ -367,13 +395,13 @@ proc expressionsNeedsTmp(p: BProc, a: TLoc): TLoc =
|
||||
result = getTemp(p, a.lode.typ, needsInit=false)
|
||||
genAssignment(p, result, a, {})
|
||||
|
||||
proc genArgStringToCString(p: BProc, n: PNode; result: var Builder; needsTmp: bool) {.inline.} =
|
||||
proc genArgStringToCString(p: BProc, n: AnyNode; result: var Builder; needsTmp: bool) {.inline.} =
|
||||
var a = initLocExpr(p, n.firstSon)
|
||||
let tmp = withTmpIfNeeded(p, a, needsTmp)
|
||||
let ra = if p.config.usesSso(): byRefLoc(p, tmp) else: tmp.rdLoc
|
||||
result.addCall(cgsymValue(p.module, "nimToCStringConv"), ra)
|
||||
|
||||
proc genArg(p: BProc, n: PNode, param: PSym; call: PNode; result: var Builder; needsTmp = false) =
|
||||
proc genArg(p: BProc, n: AnyNode, param: PSym; call: AnyNode; result: var Builder; needsTmp = false) =
|
||||
var a: TLoc
|
||||
if n.kind == nkStringToCString:
|
||||
genArgStringToCString(p, n, result, needsTmp)
|
||||
@@ -393,10 +421,16 @@ proc genArg(p: BProc, n: PNode, param: PSym; call: PNode; result: var Builder; n
|
||||
# will be a reference in C++ and we cannot create a temporary reference
|
||||
# variable. Thus, we create a temporary pointer variable instead.
|
||||
let needsIndirect = mapType(p.config, n.firstSon.typ, mapTypeChooser(n.firstSon) == skParam) != ctArray
|
||||
# A REWRITE, and one that has to be followed. The node's type is replaced in
|
||||
# place, and a cursor would keep reading the type slot as it was ENCODED —
|
||||
# the buffer does not see the mutation. So from here this site works on the
|
||||
# origin, which is the node being mutated and therefore the one that has the
|
||||
# new type.
|
||||
let nn = origin(n)
|
||||
if needsIndirect:
|
||||
n.typ = n.typ.exactReplica(p.module.idgen)
|
||||
n.typ.incl tfVarIsPtr
|
||||
a = initLocExprSingleUse(p, n)
|
||||
nn.typ = copyType(nn.typ, p.module.idgen, nn.typ.owner)
|
||||
nn.typ.incl tfVarIsPtr
|
||||
a = initLocExprSingleUse(p, nn)
|
||||
a = withTmpIfNeeded(p, a, needsTmp)
|
||||
if needsIndirect: a.flags.incl lfIndirect
|
||||
# if the proc is 'importc'ed but not 'importcpp'ed then 'var T' still
|
||||
@@ -418,7 +452,7 @@ proc genArg(p: BProc, n: PNode, param: PSym; call: PNode; result: var Builder; n
|
||||
addRdLoc(withTmpIfNeeded(p, a, needsTmp), result)
|
||||
#assert result != nil
|
||||
|
||||
proc genArgNoParam(p: BProc, n: PNode; result: var Builder; needsTmp = false) =
|
||||
proc genArgNoParam(p: BProc, n: AnyNode; result: var Builder; needsTmp = false) =
|
||||
var a: TLoc
|
||||
if n.kind == nkStringToCString:
|
||||
genArgStringToCString(p, n, result, needsTmp)
|
||||
@@ -428,65 +462,81 @@ proc genArgNoParam(p: BProc, n: PNode; result: var Builder; needsTmp = false) =
|
||||
|
||||
import aliasanalysis
|
||||
|
||||
proc potentialAlias(n: PNode, potentialWrites: seq[PNode]): bool =
|
||||
proc potentialAlias(n: AnyNode, potentialWrites: seq[PNode]): bool =
|
||||
result = false
|
||||
for p in potentialWrites:
|
||||
if p.aliases(n) != no or n.aliases(p) != no:
|
||||
return true
|
||||
|
||||
proc skipTrivialIndirections(n: PNode): PNode =
|
||||
proc skipTrivialIndirections[T: AnyNode](n: T): T =
|
||||
## Explicitly generic rather than `(n: AnyNode): AnyNode`: two occurrences of
|
||||
## a type class in one signature are two INDEPENDENT parameters, so that
|
||||
## spelling would let the result type drift from the argument's.
|
||||
result = n
|
||||
while true:
|
||||
case result.kind
|
||||
of nkDerefExpr, nkHiddenDeref, nkAddr, nkHiddenAddr, nkObjDownConv, nkObjUpConv:
|
||||
result = result.firstSon
|
||||
of nkHiddenStdConv, nkHiddenSubConv:
|
||||
result = result[1]
|
||||
result = result.secondSon
|
||||
else: break
|
||||
|
||||
proc getPotentialReads(n: PNode; result: var seq[PNode]) =
|
||||
proc getPotentialReads(n: AnyNode; result: var seq[PNode]) =
|
||||
case n.kind:
|
||||
of nkLiterals, nkIdent, nkFormalParams: discard
|
||||
of nkSym: result.add n
|
||||
else:
|
||||
for s in n:
|
||||
for s in sons(n):
|
||||
getPotentialReads(s, result)
|
||||
|
||||
proc genParams(p: BProc, ri: PNode, typ: PType; result: var Builder, argBuilder: var CallBuilder) =
|
||||
proc genParams(p: BProc, ri: AnyNode, typ: PType; result: var Builder, argBuilder: var CallBuilder) =
|
||||
# We must generate temporaries in cases like #14396
|
||||
# to keep the strict Left-To-Right evaluation
|
||||
var needTmp = newSeq[bool](ri.len - 1)
|
||||
# The arguments are walked BACKWARDS below, which a `Cursor` cannot do and
|
||||
# which costs a re-walk per step even on a `PNode`. Materialize them in one
|
||||
# forward pass and index that; `needTmp` already allocates per call, so this
|
||||
# is the same order of work.
|
||||
#
|
||||
# The arguments are materialized as `PNode`s, not cursors, because the alias
|
||||
# analysis below (`potentialAlias`, `getPotentialReads`) carries a
|
||||
# `seq[PNode]` beside the node and has not moved to the seam — see the
|
||||
# mixed-representation blocker in `bnode`'s module doc. `origin` gives the
|
||||
# same objects the tree-driven build used, so this is the argument list it
|
||||
# always was; when that analysis moves, this becomes `seq[AnyNode]`.
|
||||
var args: seq[PNode] = @[]
|
||||
for it in sonsFrom(ri, 1): args.add origin(it)
|
||||
var needTmp = newSeq[bool](args.len)
|
||||
var potentialWrites: seq[PNode] = @[]
|
||||
for i in countdown(ri.len - 1, 1):
|
||||
if ri[i].skipTrivialIndirections.kind == nkSym:
|
||||
needTmp[i - 1] = potentialAlias(ri[i], potentialWrites)
|
||||
for i in countdown(args.high, 0):
|
||||
if args[i].skipTrivialIndirections.kind == nkSym:
|
||||
needTmp[i] = potentialAlias(args[i], potentialWrites)
|
||||
else:
|
||||
#if not ri[i].typ.isCompileTimeOnly:
|
||||
#if not args[i].typ.isCompileTimeOnly:
|
||||
var potentialReads: seq[PNode] = @[]
|
||||
getPotentialReads(ri[i], potentialReads)
|
||||
getPotentialReads(args[i], potentialReads)
|
||||
for n in potentialReads:
|
||||
if not needTmp[i - 1]:
|
||||
needTmp[i - 1] = potentialAlias(n, potentialWrites)
|
||||
getPotentialWrites(ri[i], false, potentialWrites)
|
||||
if not needTmp[i]:
|
||||
needTmp[i] = potentialAlias(n, potentialWrites)
|
||||
getPotentialWrites(args[i], false, potentialWrites)
|
||||
when false:
|
||||
# this optimization is wrong, see bug #23748
|
||||
if ri[i].kind in {nkHiddenAddr, nkAddr}:
|
||||
if args[i].kind in {nkHiddenAddr, nkAddr}:
|
||||
# Optimization: don't use a temp, if we would only take the address anyway
|
||||
needTmp[i - 1] = false
|
||||
needTmp[i] = false
|
||||
|
||||
for i in 1..<ri.len:
|
||||
for i, it in isons(ri, 1):
|
||||
if i < typ.n.len:
|
||||
assert(typ.n[i].kind == nkSym)
|
||||
let paramType = typ.n[i]
|
||||
assert(son(typ.n, i).kind == nkSym)
|
||||
let paramType = son(typ.n, i)
|
||||
if not paramType.typ.isCompileTimeOnly:
|
||||
var arg = newBuilder("")
|
||||
genArg(p, ri[i], paramType.sym, ri, arg, needTmp[i-1])
|
||||
genArg(p, it, paramType.sym, ri, arg, needTmp[i-1])
|
||||
if arg.buf.len != 0:
|
||||
result.addArgument(argBuilder):
|
||||
result.add(extract(arg))
|
||||
else:
|
||||
var arg = newBuilder("")
|
||||
genArgNoParam(p, ri[i], arg, needTmp[i-1])
|
||||
genArgNoParam(p, it, arg, needTmp[i-1])
|
||||
if arg.buf.len != 0:
|
||||
result.addArgument(argBuilder):
|
||||
result.add(extract(arg))
|
||||
@@ -496,7 +546,7 @@ proc addActualSuffixForHCR(res: var Rope, module: PSym, sym: PSym) =
|
||||
(sym.typ.callConv == ccInline or sym.owner.id == module.id):
|
||||
res = res & "_actual".rope
|
||||
|
||||
proc genPrefixCall(p: BProc, le, ri: PNode, d: var TLoc) =
|
||||
proc genPrefixCall(p: BProc, le: PNode, ri: AnyNode, d: var TLoc) =
|
||||
# this is a hotspot in the compiler
|
||||
var op = initLocExpr(p, ri.firstSon)
|
||||
# getUniqueType() is too expensive here:
|
||||
@@ -512,7 +562,7 @@ proc genPrefixCall(p: BProc, le, ri: PNode, d: var TLoc) =
|
||||
genParams(p, ri, typ, res, call)
|
||||
fixupCall(p, le, ri, d, res, call)
|
||||
|
||||
proc genClosureCall(p: BProc, le, ri: PNode, d: var TLoc) =
|
||||
proc genClosureCall(p: BProc, le: PNode, ri: AnyNode, d: var TLoc) =
|
||||
|
||||
template callProc(rp, params, pTyp: Snippet): Snippet =
|
||||
let e = dotField(rp, "ClE_0")
|
||||
@@ -547,6 +597,12 @@ proc genClosureCall(p: BProc, le, ri: PNode, d: var TLoc) =
|
||||
var argBuilder = default(CallBuilder) # not initCallBuilder, we just want the params
|
||||
genParams(p, ri, typ, params, argBuilder)
|
||||
|
||||
# `rawProc` is bound BEFORE the `{.dirty.}` template that uses it. Inside a
|
||||
# generic proc a dirty template's identifiers resolve at instantiation, and a
|
||||
# local declared after the template loses to the module-level `rawProc` proc
|
||||
# — which type-checks as a completely different thing.
|
||||
let rawProc = getClosureType(p.module, typ, clHalf)
|
||||
|
||||
template genCallPattern {.dirty.} =
|
||||
let rp = rdLoc(op)
|
||||
let pars = extract(params)
|
||||
@@ -555,8 +611,6 @@ proc genClosureCall(p: BProc, le, ri: PNode, d: var TLoc) =
|
||||
p.s(cpsStmts).add(callIter(rp, pars))
|
||||
else:
|
||||
p.s(cpsStmts).add(callProc(rp, pars, rawProc))
|
||||
|
||||
let rawProc = getClosureType(p.module, typ, clHalf)
|
||||
let canRaise = p.config.exc == excGoto and canRaiseDisp(p, ri.firstSon)
|
||||
if typ.returnType != nil:
|
||||
if isInvalidReturnType(p.config, typ):
|
||||
@@ -608,27 +662,27 @@ proc genClosureCall(p: BProc, le, ri: PNode, d: var TLoc) =
|
||||
genCallPattern()
|
||||
if canRaise: raiseExit(p)
|
||||
|
||||
proc genOtherArg(p: BProc; ri: PNode; i: int; typ: PType; result: var Builder;
|
||||
proc genOtherArg(p: BProc; ri: AnyNode; i: int; typ: PType; result: var Builder;
|
||||
argBuilder: var CallBuilder) =
|
||||
if i < typ.n.len:
|
||||
# 'var T' is 'T&' in C++. This means we ignore the request of
|
||||
# any nkHiddenAddr when it's a 'var T'.
|
||||
let paramType = typ.n[i]
|
||||
let paramType = son(typ.n, i)
|
||||
assert(paramType.kind == nkSym)
|
||||
if paramType.typ.isCompileTimeOnly:
|
||||
discard
|
||||
elif paramType.typ.kind in {tyVar} and ri[i].kind == nkHiddenAddr:
|
||||
elif paramType.typ.kind in {tyVar} and son(ri, i).kind == nkHiddenAddr:
|
||||
result.addArgument(argBuilder):
|
||||
genArgNoParam(p, ri[i].firstSon, result)
|
||||
genArgNoParam(p, son(ri, i).firstSon, result)
|
||||
else:
|
||||
result.addArgument(argBuilder):
|
||||
genArgNoParam(p, ri[i], result) #, typ.n[i].sym)
|
||||
genArgNoParam(p, son(ri, i), result) #, son(typ.n, i).sym)
|
||||
else:
|
||||
if tfVarargs notin typ.flags:
|
||||
localError(p.config, ri.info, "wrong argument count")
|
||||
else:
|
||||
result.addArgument(argBuilder):
|
||||
genArgNoParam(p, ri[i], result)
|
||||
genArgNoParam(p, son(ri, i), result)
|
||||
|
||||
discard """
|
||||
Dot call syntax in C++
|
||||
@@ -667,7 +721,7 @@ y.v() --> y.v() is correct
|
||||
|
||||
"""
|
||||
|
||||
proc skipAddrDeref(node: PNode): PNode =
|
||||
proc skipAddrDeref[T: AnyNode](node: T): T =
|
||||
var n = node
|
||||
var isAddr = false
|
||||
case n.kind
|
||||
@@ -685,15 +739,15 @@ proc skipAddrDeref(node: PNode): PNode =
|
||||
else:
|
||||
result = node
|
||||
|
||||
proc genThisArg(p: BProc; ri: PNode; i: int; typ: PType; result: var Builder) =
|
||||
proc genThisArg(p: BProc; ri: AnyNode; i: int; typ: PType; result: var Builder) =
|
||||
# for better or worse c2nim translates the 'this' argument to a 'var T'.
|
||||
# However manual wrappers may also use 'ptr T'. In any case we support both
|
||||
# for convenience.
|
||||
internalAssert p.config, i < typ.n.len
|
||||
assert(typ.n[i].kind == nkSym)
|
||||
assert(son(typ.n, i).kind == nkSym)
|
||||
# if the parameter is lying (tyVar) and thus we required an additional deref,
|
||||
# skip the deref:
|
||||
var ri = ri[i]
|
||||
var ri = son(ri, i)
|
||||
while ri.kind == nkObjDownConv: ri = ri.firstSon
|
||||
let t = typ[i].skipTypes({tyGenericInst, tyAlias, tySink})
|
||||
if t.kind in {tyVar}:
|
||||
@@ -717,22 +771,22 @@ proc genThisArg(p: BProc; ri: PNode; i: int; typ: PType; result: var Builder) =
|
||||
else:
|
||||
ri = skipAddrDeref(ri)
|
||||
if ri.kind in {nkAddr, nkHiddenAddr}: ri = ri.firstSon
|
||||
genArgNoParam(p, ri, result) #, typ.n[i].sym)
|
||||
genArgNoParam(p, ri, result) #, son(typ.n, i).sym)
|
||||
result.add(".")
|
||||
|
||||
proc genPatternCall(p: BProc; ri: PNode; pat: string; typ: PType; result: var Builder) =
|
||||
proc genPatternCall(p: BProc; ri: AnyNode; pat: string; typ: PType; result: var Builder) =
|
||||
var i = 0
|
||||
var j = 1
|
||||
while i < pat.len:
|
||||
case pat[i]
|
||||
of '@':
|
||||
var callBuilder = default(CallBuilder) # not init call builder
|
||||
for k in j..<ri.len:
|
||||
for k, _ in isons(ri, j):
|
||||
genOtherArg(p, ri, k, typ, result, callBuilder)
|
||||
inc i
|
||||
of '#':
|
||||
if i+1 < pat.len and pat[i+1] in {'+', '@'}:
|
||||
let ri = ri[j]
|
||||
let ri = son(ri, j)
|
||||
if ri.kind in nkCallKinds:
|
||||
let typ = skipTypes(ri.firstSon.typ, abstractInst)
|
||||
if pat[i+1] == '+': genArgNoParam(p, ri.firstSon, result)
|
||||
@@ -740,7 +794,7 @@ proc genPatternCall(p: BProc; ri: PNode; pat: string; typ: PType; result: var Bu
|
||||
if 1 < ri.len:
|
||||
var callBuilder: CallBuilder = default(CallBuilder)
|
||||
genOtherArg(p, ri, 1, typ, result, callBuilder)
|
||||
for k in j+1..<ri.len:
|
||||
for k, _ in isons(ri, j+1):
|
||||
var callBuilder: CallBuilder = default(CallBuilder)
|
||||
genOtherArg(p, ri, k, typ, result, callBuilder)
|
||||
result.add(")")
|
||||
@@ -751,7 +805,7 @@ proc genPatternCall(p: BProc; ri: PNode; pat: string; typ: PType; result: var Bu
|
||||
genThisArg(p, ri, j, typ, result)
|
||||
inc i
|
||||
elif i+1 < pat.len and pat[i+1] == '[':
|
||||
var arg = ri[j].skipAddrDeref
|
||||
var arg = son(ri, j).skipAddrDeref
|
||||
while arg.kind in {nkAddr, nkHiddenAddr, nkObjDownConv}: arg = arg.firstSon
|
||||
genArgNoParam(p, arg, result)
|
||||
#result.add debugTree(arg, 0, 10)
|
||||
@@ -774,7 +828,7 @@ proc genPatternCall(p: BProc; ri: PNode; pat: string; typ: PType; result: var Bu
|
||||
if i - 1 >= start:
|
||||
result.add(substr(pat, start, i - 1))
|
||||
|
||||
proc genInfixCall(p: BProc, le, ri: PNode, d: var TLoc) =
|
||||
proc genInfixCall(p: BProc, le: PNode, ri: AnyNode, d: var TLoc) =
|
||||
var op = initLocExpr(p, ri.firstSon)
|
||||
# getUniqueType() is too expensive here:
|
||||
var typ = skipTypes(ri.firstSon.typ, abstractInst)
|
||||
@@ -811,11 +865,11 @@ proc genInfixCall(p: BProc, le, ri: PNode, d: var TLoc) =
|
||||
pl.add(op.snippet)
|
||||
var res = newBuilder("")
|
||||
var call = initCallBuilder(res, extract(pl))
|
||||
for i in 2..<ri.len:
|
||||
for i, _ in isons(ri, 2):
|
||||
genOtherArg(p, ri, i, typ, res, call)
|
||||
fixupCall(p, le, ri, d, res, call)
|
||||
|
||||
proc genNamedParamCall(p: BProc, ri: PNode, d: var TLoc) =
|
||||
proc genNamedParamCall(p: BProc, ri: AnyNode, d: var TLoc) =
|
||||
# generates a crappy ObjC call
|
||||
var op = initLocExpr(p, ri.firstSon)
|
||||
var pl = newBuilder("[")
|
||||
@@ -832,25 +886,25 @@ proc genNamedParamCall(p: BProc, ri: PNode, d: var TLoc) =
|
||||
pl.add(op.snippet)
|
||||
if ri.len > 1:
|
||||
pl.add(": ")
|
||||
genArg(p, ri[1], typ.n[1].sym, ri, pl)
|
||||
genArg(p, ri.secondSon, typ.n.secondSon.sym, ri, pl)
|
||||
start = 2
|
||||
else:
|
||||
if ri.len > 1:
|
||||
genArg(p, ri[1], typ.n[1].sym, ri, pl)
|
||||
genArg(p, ri.secondSon, typ.n.secondSon.sym, ri, pl)
|
||||
pl.add(" ")
|
||||
pl.add(op.snippet)
|
||||
if ri.len > 2:
|
||||
pl.add(": ")
|
||||
genArg(p, ri[2], typ.n[2].sym, ri, pl)
|
||||
for i in start..<ri.len:
|
||||
genArg(p, son(ri, 2), son(typ.n, 2).sym, ri, pl)
|
||||
for i, it in isons(ri, start):
|
||||
if i >= typ.n.len:
|
||||
internalError(p.config, ri.info, "varargs for objective C method?")
|
||||
assert(typ.n[i].kind == nkSym)
|
||||
var param = typ.n[i].sym
|
||||
assert(son(typ.n, i).kind == nkSym)
|
||||
var param = son(typ.n, i).sym
|
||||
pl.add(" ")
|
||||
pl.add(param.name.s)
|
||||
pl.add(": ")
|
||||
genArg(p, ri[i], param, ri, pl)
|
||||
genArg(p, it, param, ri, pl)
|
||||
if typ.returnType != nil:
|
||||
if isInvalidReturnType(p.config, typ):
|
||||
if ri.len > 1: pl.add(" ")
|
||||
@@ -882,11 +936,11 @@ proc genNamedParamCall(p: BProc, ri: PNode, d: var TLoc) =
|
||||
p.s(cpsStmts).addStmt():
|
||||
p.s(cpsStmts).add(extract(pl))
|
||||
|
||||
proc notYetAlive(n: PNode): bool {.inline.} =
|
||||
proc notYetAlive(n: AnyNode): bool {.inline.} =
|
||||
let r = getRoot(n)
|
||||
result = r != nil and r.loc.lode == nil
|
||||
|
||||
proc isInactiveDestructorCall(p: BProc, e: PNode): bool =
|
||||
proc isInactiveDestructorCall(p: BProc, e: AnyNode): bool =
|
||||
#[ Consider this example.
|
||||
|
||||
var :tmpD_3281815
|
||||
@@ -903,10 +957,10 @@ proc isInactiveDestructorCall(p: BProc, e: PNode): bool =
|
||||
We want to return early but the 'finally' section is traversed before
|
||||
the 'let args = ...' statement. We exploit this to generate better
|
||||
code for 'return'. ]#
|
||||
result = e.len == 2 and e.firstSon.kind == nkSym and
|
||||
e.firstSon.sym.name.s == "=destroy" and notYetAlive(e[1].skipAddr)
|
||||
result = e.safeLen == 2 and e.firstSon.kind == nkSym and
|
||||
e.firstSon.sym.name.s == "=destroy" and notYetAlive(e.secondSon.skipAddr)
|
||||
|
||||
proc genAsgnCall(p: BProc, le, ri: PNode, d: var TLoc) =
|
||||
proc genAsgnCall(p: BProc, le: PNode, ri: AnyNode, d: var TLoc) =
|
||||
if p.withinBlockLeaveActions > 0 and isInactiveDestructorCall(p, ri):
|
||||
return
|
||||
when defined(icDbgHash):
|
||||
@@ -928,4 +982,4 @@ proc genAsgnCall(p: BProc, le, ri: PNode, d: var TLoc) =
|
||||
else:
|
||||
genPrefixCall(p, le, ri, d)
|
||||
|
||||
proc genCall(p: BProc, e: PNode, d: var TLoc) = genAsgnCall(p, nil, e, d)
|
||||
proc genCall(p: BProc, e: AnyNode, d: var TLoc) = genAsgnCall(p, nil, e, d)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -53,11 +53,11 @@ proc genStringLiteralDataOnlyV1(m: BModule, s: string; result: var Rope) =
|
||||
res.add(makeCString(s))
|
||||
m.s[cfsStrData].add(extract(res))
|
||||
|
||||
proc genStringLiteralV1(m: BModule; n: PNode; result: var Builder) =
|
||||
proc genStringLiteralV1(m: BModule; n: AnyNode; result: var Builder) =
|
||||
if s.isNil:
|
||||
result.add(cCast(ptrType(cgsymValue(m, "NimStringDesc")), NimNil))
|
||||
else:
|
||||
let id = nodeTableTestOrSet(m.dataCache, n, m.labels)
|
||||
let id = nodeTableTestOrSet(m.dataCache, origin(n), m.labels)
|
||||
var name: string = ""
|
||||
if id == m.labels:
|
||||
# string literal not found in the cache:
|
||||
@@ -85,8 +85,8 @@ proc genStringLiteralDataOnlyV2(m: BModule, s: string; result: Rope; isConst: bo
|
||||
res.add(makeCString(s))
|
||||
m.s[cfsStrData].add(extract(res))
|
||||
|
||||
proc genStringLiteralV2(m: BModule; n: PNode; isConst: bool; result: var Builder) =
|
||||
let id = nodeTableTestOrSet(m.dataCache, n, m.labels)
|
||||
proc genStringLiteralV2(m: BModule; n: AnyNode; isConst: bool; result: var Builder) =
|
||||
let id = nodeTableTestOrSet(m.dataCache, origin(n), m.labels)
|
||||
var litName: string
|
||||
if id == m.labels:
|
||||
cgsym(m, "NimStrPayload")
|
||||
@@ -111,8 +111,8 @@ proc genStringLiteralV2(m: BModule; n: PNode; isConst: bool; result: var Builder
|
||||
res.add(cCast(ptrType("NimStrPayload"), cAddr(litName)))
|
||||
m.s[cfsStrData].add(extract(res))
|
||||
|
||||
proc genStringLiteralV2Const(m: BModule; n: PNode; isConst: bool; result: var Builder) =
|
||||
let id = nodeTableTestOrSet(m.dataCache, n, m.labels)
|
||||
proc genStringLiteralV2Const(m: BModule; n: AnyNode; isConst: bool; result: var Builder) =
|
||||
let id = nodeTableTestOrSet(m.dataCache, origin(n), m.labels)
|
||||
var pureLit: Rope
|
||||
if id == m.labels:
|
||||
pureLit = getTempName(m)
|
||||
@@ -164,7 +164,7 @@ proc ssoMoreLit(m: BModule; s: string): string =
|
||||
val = val or (ch shl (uint(ptrSize - 1 - i) * 8))
|
||||
result = cCast(ptrType("LongString"), "(uintptr_t)" & $val)
|
||||
|
||||
proc genStringLiteralV3Const(m: BModule; n: PNode; isConst: bool; result: var Builder) =
|
||||
proc genStringLiteralV3Const(m: BModule; n: AnyNode; isConst: bool; result: var Builder) =
|
||||
# Inline SmallString struct initializer for use inside const aggregate types.
|
||||
# Layout: {bytes: NimUint, more: ptr LongString}
|
||||
# bytes = slen (low byte) | char[0]<<8 | char[1]<<16 | ... | char[6]<<56
|
||||
@@ -220,7 +220,7 @@ proc genStringLiteralV3Const(m: BModule; n: PNode; isConst: bool; result: var Bu
|
||||
|
||||
# ------ Version 3: SmallString (SSO) strings --------------------------------
|
||||
|
||||
proc genStringLiteralV3(m: BModule; n: PNode; isConst: bool; result: var Builder) =
|
||||
proc genStringLiteralV3(m: BModule; n: AnyNode; isConst: bool; result: var Builder) =
|
||||
# SmallString literal. Always generate a fresh SmallString variable (like v2
|
||||
# always generates a fresh outer NimStringV2). For long strings, cache the
|
||||
# LongString payload to avoid duplicates within a module.
|
||||
@@ -259,7 +259,7 @@ proc genStringLiteralV3(m: BModule; n: PNode; isConst: bool; result: var Builder
|
||||
else:
|
||||
# Long: cache the LongString block to emit it only once per module per string.
|
||||
# Always generate a fresh SmallString pointing at the (possibly cached) block.
|
||||
let id = nodeTableTestOrSet(m.dataCache, n, m.labels)
|
||||
let id = nodeTableTestOrSet(m.dataCache, origin(n), m.labels)
|
||||
var dataName: string
|
||||
if id == m.labels:
|
||||
dataName = getTempName(m)
|
||||
@@ -301,7 +301,7 @@ proc genStringLiteralV3(m: BModule; n: PNode; isConst: bool; result: var Builder
|
||||
proc genNilStringLiteral(m: BModule; info: TLineInfo; result: var Builder) =
|
||||
result.add(cCast(ptrType(cgsymValue(m, "NimStringDesc")), NimNil))
|
||||
|
||||
proc genStringLiteral(m: BModule; n: PNode; result: var Builder) =
|
||||
proc genStringLiteral(m: BModule; n: AnyNode; result: var Builder) =
|
||||
case detectStrVersion(m)
|
||||
of 0, 1: genStringLiteralV1(m, n, result)
|
||||
of 2: genStringLiteralV2(m, n, isConst = true, result)
|
||||
|
||||
@@ -19,18 +19,17 @@ proc specializeResetN(p: BProc, accessor: Rope, n: PNode;
|
||||
if n == nil: return
|
||||
case n.kind
|
||||
of nkRecList:
|
||||
for i in 0..<n.len:
|
||||
specializeResetN(p, accessor, n[i], typ)
|
||||
for it in sons(n):
|
||||
specializeResetN(p, accessor, it, typ)
|
||||
of nkRecCase:
|
||||
if (n[0].kind != nkSym): internalError(p.config, n.info, "specializeResetN")
|
||||
let disc = n[0].sym
|
||||
if (n.firstSon.kind != nkSym): internalError(p.config, n.info, "specializeResetN")
|
||||
let disc = n.firstSon.sym
|
||||
if disc.loc.snippet == "": fillObjectFields(p.module, typ)
|
||||
if disc.loc.t == nil:
|
||||
internalError(p.config, n.info, "specializeResetN()")
|
||||
let discField = dotField(accessor, disc.loc.snippet)
|
||||
p.s(cpsStmts).addSwitchStmt(discField):
|
||||
for i in 1..<n.len:
|
||||
let branch = n[i]
|
||||
for branch in sonsFrom(n, 1):
|
||||
assert branch.kind in {nkOfBranch, nkElse}
|
||||
var caseBuilder: SwitchCaseBuilder
|
||||
p.s(cpsStmts).addSwitchCase(caseBuilder):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ type
|
||||
|
||||
|
||||
proc genTraverseProc(c: TTraversalClosure, accessor: Rope, typ: PType)
|
||||
proc genCaseRange(p: BProc, branch: PNode, info: var SwitchCaseBuilder)
|
||||
proc genCaseRange(p: BProc, branch: AnyNode, info: var SwitchCaseBuilder)
|
||||
proc getTemp(p: BProc, t: PType, needsInit=false): TLoc
|
||||
|
||||
proc visit(p: BProc, data, visitor: Snippet) =
|
||||
@@ -31,19 +31,18 @@ proc genTraverseProc(c: TTraversalClosure, accessor: Rope, n: PNode;
|
||||
if n == nil: return
|
||||
case n.kind
|
||||
of nkRecList:
|
||||
for i in 0..<n.len:
|
||||
genTraverseProc(c, accessor, n[i], typ)
|
||||
for it in sons(n):
|
||||
genTraverseProc(c, accessor, it, typ)
|
||||
of nkRecCase:
|
||||
if (n[0].kind != nkSym): internalError(c.p.config, n.info, "genTraverseProc")
|
||||
if (n.firstSon.kind != nkSym): internalError(c.p.config, n.info, "genTraverseProc")
|
||||
var p = c.p
|
||||
let disc = n[0].sym
|
||||
let disc = n.firstSon.sym
|
||||
if disc.loc.snippet == "": fillObjectFields(c.p.module, typ)
|
||||
if disc.loc.t == nil:
|
||||
internalError(c.p.config, n.info, "genTraverseProc()")
|
||||
let discField = dotField(accessor, disc.loc.snippet)
|
||||
p.s(cpsStmts).addSwitchStmt(discField):
|
||||
for i in 1..<n.len:
|
||||
let branch = n[i]
|
||||
for branch in sonsFrom(n, 1):
|
||||
assert branch.kind in {nkOfBranch, nkElse}
|
||||
var caseBuilder: SwitchCaseBuilder
|
||||
p.s(cpsStmts).addSwitchCase(caseBuilder):
|
||||
|
||||
@@ -59,10 +59,10 @@ proc mangleProc(m: BModule; s: PSym; makeUnique: bool): string =
|
||||
result = "_Z" # Common prefix in Itanium ABI
|
||||
var params = ""
|
||||
var staticLists = ""
|
||||
if s.typ.len > 1: #we dont care about the return param
|
||||
for i in 1..<s.typ.len:
|
||||
if s.typ[i].isNil: continue
|
||||
params.add encodeType(m, s.typ[i], staticLists)
|
||||
if s.typ.paramsLen > 0: # we dont care about the return param
|
||||
for _, pt in paramTypes(s.typ):
|
||||
if pt.isNil: continue
|
||||
params.add encodeType(m, pt, staticLists)
|
||||
|
||||
result.add encodeSym(m, s, makeUnique, staticLists)
|
||||
result.add params
|
||||
@@ -311,7 +311,7 @@ proc isInvalidReturnType(conf: ConfigRef; typ: PType, isProc = true): bool =
|
||||
var rettype = typ
|
||||
var isAllowedCall = true
|
||||
if isProc:
|
||||
rettype = rettype[0]
|
||||
rettype = rettype.returnType
|
||||
isAllowedCall = typ.callConv in {ccClosure, ccInline, ccNimCall}
|
||||
if rettype == nil or (isAllowedCall and
|
||||
getSize(conf, rettype) > conf.target.floatSize*3):
|
||||
@@ -480,7 +480,7 @@ proc getTypeDescWeak(m: BModule; t: PType; check: var IntSet; kind: TypeDescKind
|
||||
of tySequence:
|
||||
let sig = hashType(t, m.config)
|
||||
if optSeqDestructors in m.config.globalOptions:
|
||||
if skipTypes(etB[0], typedescInst).kind == tyEmpty:
|
||||
if skipTypes(etB.elementType, typedescInst).kind == tyEmpty:
|
||||
internalError(m.config, "cannot map the empty seq type to a C type")
|
||||
|
||||
result = cacheGetType(m.forwTypeCache, sig)
|
||||
@@ -524,7 +524,7 @@ proc seqV2ContentType(m: BModule; t: PType; check: var IntSet) =
|
||||
if result == "":
|
||||
discard getTypeDescAux(m, t, check, dkVar)
|
||||
else:
|
||||
let dataTyp = getTypeDescAux(m, t.skipTypes(abstractInst)[0], check, dkVar)
|
||||
let dataTyp = getTypeDescAux(m, t.skipTypes(abstractInst).elementType, check, dkVar)
|
||||
m.s[cfsTypes].addSimpleStruct(m, name = result & "_Content", baseType = ""):
|
||||
m.s[cfsTypes].addField(name = "cap", typ = NimInt)
|
||||
m.s[cfsTypes].addField(name = "data",
|
||||
@@ -598,10 +598,10 @@ proc genMemberProcParams(m: BModule; prc: PSym, superCall, rettype, name, params
|
||||
rettype = runtimeFormat(rettype.replace("'0", "$1"), [getTypeDescAux(m, t.returnType, check, dkResult)])
|
||||
var types, names, args: seq[string] = @[]
|
||||
if not isCtor:
|
||||
var this = t.n[1].sym
|
||||
var this = t.n.secondSon.sym
|
||||
backendEnsureMutable this
|
||||
fillParamName(m, this)
|
||||
fillLoc(this.locImpl, locParam, t.n[1],
|
||||
fillLoc(this.locImpl, locParam, t.n.secondSon,
|
||||
this.paramStorageLoc)
|
||||
if this.typ.kind == tyPtr:
|
||||
this.locImpl.snippet = "this"
|
||||
@@ -611,9 +611,9 @@ proc genMemberProcParams(m: BModule; prc: PSym, superCall, rettype, name, params
|
||||
types.add getTypeDescWeak(m, this.typ, check, dkParam)
|
||||
|
||||
let firstParam = if isCtor: 1 else: 2
|
||||
for i in firstParam..<t.n.len:
|
||||
if t.n[i].kind != nkSym: internalError(m.config, t.n.info, "genMemberProcParams")
|
||||
var param = t.n[i].sym
|
||||
for it in sonsFrom(t.n, firstParam):
|
||||
if it.kind != nkSym: internalError(m.config, t.n.info, "genMemberProcParams")
|
||||
var param = it.sym
|
||||
var descKind = dkParam
|
||||
if optByRef in param.options:
|
||||
if param.typ.kind == tyGenericInst:
|
||||
@@ -623,7 +623,7 @@ proc genMemberProcParams(m: BModule; prc: PSym, superCall, rettype, name, params
|
||||
var typ, name: string
|
||||
backendEnsureMutable param
|
||||
fillParamName(m, param)
|
||||
fillLoc(param.locImpl, locParam, t.n[i],
|
||||
fillLoc(param.locImpl, locParam, it,
|
||||
param.paramStorageLoc)
|
||||
if ccgIntroducedPtr(m.config, param, t.returnType) and descKind == dkParam:
|
||||
typ = getTypeDescWeak(m, param.typ, check, descKind) & "*"
|
||||
@@ -668,9 +668,9 @@ proc genProcParams(m: BModule; t: PType, rettype: var Rope, params: var Builder,
|
||||
rettype = getTypeDescWeak(m, t.returnType, check, dkResult)
|
||||
var paramBuilder: ProcParamBuilder
|
||||
params.addProcParams(paramBuilder):
|
||||
for i in 1..<t.n.len:
|
||||
if t.n[i].kind != nkSym: internalError(m.config, t.n.info, "genProcParams")
|
||||
var param = t.n[i].sym
|
||||
for child in sonsFrom(t.n, 1):
|
||||
if child.kind != nkSym: internalError(m.config, t.n.info, "genProcParams")
|
||||
var param = child.sym
|
||||
# The hidden closure environment param (`:envP`) is not a real C parameter:
|
||||
# the environment is passed via the trailing `ClE_0` (added below) and
|
||||
# `closureSetup` materialises `:envP` as a local cast of it. In a from-source
|
||||
@@ -692,7 +692,7 @@ proc genProcParams(m: BModule; t: PType, rettype: var Rope, params: var Builder,
|
||||
if isCompileTimeOnly(param.typ): continue
|
||||
backendEnsureMutable param
|
||||
fillParamName(m, param)
|
||||
fillLoc(param.locImpl, locParam, t.n[i],
|
||||
fillLoc(param.locImpl, locParam, child,
|
||||
param.paramStorageLoc)
|
||||
if isClosureEnv: continue # name/loc filled, but not part of the C signature
|
||||
var typ: Rope
|
||||
@@ -715,7 +715,7 @@ proc genProcParams(m: BModule; t: PType, rettype: var Rope, params: var Builder,
|
||||
# need to pass hidden parameter:
|
||||
params.addParam(paramBuilder, name = param.locImpl.snippet & "Len_" & $j, typ = NimInt)
|
||||
inc(j)
|
||||
arr = arr[0].skipTypes({tySink})
|
||||
arr = arr.elementType.skipTypes({tySink})
|
||||
if t.returnType != nil and isInvalidReturnType(m.config, t):
|
||||
var arr = t.returnType
|
||||
var typ: Snippet
|
||||
@@ -742,18 +742,18 @@ proc mangleRecFieldName(m: BModule; field: PSym): Rope =
|
||||
|
||||
proc hasCppCtor(m: BModule; typ: PType): bool =
|
||||
result = false
|
||||
if m.compileToCpp and typ != nil and typ.itemId in m.g.graph.memberProcsPerType:
|
||||
for prc in m.g.graph.memberProcsPerType[typ.itemId]:
|
||||
if m.compileToCpp and typ != nil and typ.bindingId in m.g.graph.memberProcsPerType:
|
||||
for prc in m.g.graph.memberProcsPerType[typ.bindingId]:
|
||||
if sfConstructor in prc.flags:
|
||||
return true
|
||||
|
||||
proc genCppParamsForCtor(p: BProc; call: PNode; didGenTemp: var bool): string
|
||||
proc genCppParamsForCtor(p: BProc; call: AnyNode; didGenTemp: var bool): string
|
||||
|
||||
proc genCppInitializer(m: BModule, prc: BProc; typ: PType; didGenTemp: var bool): string =
|
||||
#To avoid creating a BProc per test when called inside a struct nil BProc is allowed
|
||||
result = "{}"
|
||||
if typ.itemId in m.g.graph.initializersPerType:
|
||||
let call = m.g.graph.initializersPerType[typ.itemId]
|
||||
if typ.bindingId in m.g.graph.initializersPerType:
|
||||
let call = m.g.graph.initializersPerType[typ.bindingId]
|
||||
if call != nil:
|
||||
var p = prc
|
||||
if p == nil:
|
||||
@@ -767,7 +767,7 @@ proc genRecordFieldsAux(m: BModule; n: PNode,
|
||||
check: var IntSet; result: var Builder; unionPrefix = "") =
|
||||
case n.kind
|
||||
of nkRecList:
|
||||
for ni in n.sons:
|
||||
for ni in sons(n):
|
||||
genRecordFieldsAux(m, ni, rectype, check, result, unionPrefix)
|
||||
of nkRecCase:
|
||||
if n.firstSon.kind != nkSym: internalError(m.config, n.info, "genRecordFieldsAux")
|
||||
@@ -775,10 +775,10 @@ proc genRecordFieldsAux(m: BModule; n: PNode,
|
||||
# prefix mangled name with "_U" to avoid clashes with other field names,
|
||||
# since identifiers are not allowed to start with '_'
|
||||
var unionBody = newBuilder("")
|
||||
for i in 1..<n.len:
|
||||
case n[i].kind
|
||||
for i, it in isons(n, 1):
|
||||
case it.kind
|
||||
of nkOfBranch, nkElse:
|
||||
let k = lastSon(n[i])
|
||||
let k = lastSon(it)
|
||||
if k.kind != nkSym:
|
||||
let structName = "_" & mangleRecFieldName(m, n.firstSon.sym) & "_" & $i
|
||||
var a = newBuilder("")
|
||||
@@ -833,8 +833,8 @@ proc genMemberProcHeader(m: BModule; prc: PSym; result: var Builder; asPtr: bool
|
||||
|
||||
proc addRecordFields(result: var Builder; m: BModule; typ: PType, check: var IntSet) =
|
||||
genRecordFieldsAux(m, typ.n, typ, check, result)
|
||||
if typ.itemId in m.g.graph.memberProcsPerType:
|
||||
let procs = m.g.graph.memberProcsPerType[typ.itemId]
|
||||
if typ.bindingId in m.g.graph.memberProcsPerType:
|
||||
let procs = m.g.graph.memberProcsPerType[typ.bindingId]
|
||||
var isDefaultCtorGen, isCtorGen: bool = false
|
||||
for prc in procs:
|
||||
if sfConstructor in prc.flags:
|
||||
@@ -915,7 +915,7 @@ proc resolveStarsInCppType(typ: PType, idx, stars: int): PType =
|
||||
result = typ[idx]
|
||||
for i in 1..stars:
|
||||
if result != nil and result.kidsLen > 0:
|
||||
result = if result.kind == tyGenericInst: result[FirstGenericParamAt]
|
||||
result = if result.kind == tyGenericInst: result.firstGenericParam
|
||||
else: result.elemType
|
||||
|
||||
proc getOpenArrayDesc(m: BModule; t: PType, check: var IntSet; kind: TypeDescKind): Rope =
|
||||
@@ -1075,7 +1075,7 @@ proc getTypeDescAux(m: BModule; origTyp: PType, check: var IntSet; kind: TypeDes
|
||||
let owner = hashOwner(t.sym)
|
||||
if not gDebugInfo.hasEnum(t.sym.name.s, t.sym.info.line, owner):
|
||||
var vals: seq[(string, int)] = @[]
|
||||
for son in t.n.sons:
|
||||
for son in sons(t.n):
|
||||
assert(son.kind == nkSym)
|
||||
let field = son.sym
|
||||
vals.add((field.name.s, field.position.int))
|
||||
@@ -1267,7 +1267,7 @@ proc genMemberProcHeader(m: BModule; prc: PSym; result: var Builder; asPtr: bool
|
||||
var check = initIntSet()
|
||||
fillBackendName(m, prc)
|
||||
backendEnsureMutable prc
|
||||
fillLoc(prc.locImpl, locProc, prc.ast[namePos], OnUnknown)
|
||||
fillLoc(prc.locImpl, locProc, son(prc.ast, namePos), OnUnknown)
|
||||
var memberOp = "#." #only virtual
|
||||
var typ: PType
|
||||
if isCtor:
|
||||
@@ -1289,6 +1289,14 @@ proc genMemberProcHeader(m: BModule; prc: PSym; result: var Builder; asPtr: bool
|
||||
name = typDesc
|
||||
if isFnConst:
|
||||
fnConst = " const"
|
||||
if not isCtor:
|
||||
# The call-site form (`x->salute(@)`), not the mangled Nim name. Set it on
|
||||
# BOTH paths: whole-program cgen always emitted the out-of-class definition
|
||||
# (the `else` branch) before any caller, but the per-module backend emits a
|
||||
# foreign member proc's body in ITS OWN module, so the caller's TU only ever
|
||||
# reaches the in-class declaration below — and called the member by the
|
||||
# mangled name (`loo->salute_u0__vireouyks1()`, "struct Loo has no member").
|
||||
prc.locImpl.snippet = "$1$2(@)" % [memberOp, name]
|
||||
if isFwdDecl:
|
||||
if isStatic:
|
||||
result.add "static "
|
||||
@@ -1298,9 +1306,7 @@ proc genMemberProcHeader(m: BModule; prc: PSym; result: var Builder; asPtr: bool
|
||||
override = " override"
|
||||
superCall = ""
|
||||
else:
|
||||
if not isCtor:
|
||||
prc.locImpl.snippet = "$1$2(@)" % [memberOp, name]
|
||||
elif superCall != "":
|
||||
if isCtor and superCall != "":
|
||||
superCall = " : " & superCall
|
||||
|
||||
name = "$1::$2" % [typDesc, name]
|
||||
@@ -1315,7 +1321,7 @@ proc genProcHeader(m: BModule; prc: PSym; result: var Builder; visibility: var D
|
||||
var check = initIntSet()
|
||||
fillBackendName(m, prc)
|
||||
backendEnsureMutable prc
|
||||
fillLoc(prc.locImpl, locProc, prc.ast[namePos], OnUnknown)
|
||||
fillLoc(prc.locImpl, locProc, son(prc.ast, namePos), OnUnknown)
|
||||
var rettype: Snippet = ""
|
||||
var desc = newBuilder("")
|
||||
genProcParams(m, prc.typ, rettype, desc, check, true, false)
|
||||
@@ -1456,7 +1462,7 @@ proc discriminatorTableName(m: BModule; objtype: PType, d: PSym): Rope =
|
||||
# bugfix: we need to search the type that contains the discriminator:
|
||||
var objtype = objtype.skipTypes(abstractPtrs)
|
||||
while lookupInRecord(objtype.n, d.name) == nil:
|
||||
objtype = objtype[0].skipTypes(abstractPtrs)
|
||||
objtype = objtype.baseClass.skipTypes(abstractPtrs)
|
||||
if objtype.sym == nil:
|
||||
internalError(m.config, d.info, "anonymous obj with discriminator")
|
||||
result = "NimDT_$1_$2" % [rope($hashType(objtype, m.config)), rope(d.name.s.mangle)]
|
||||
@@ -1546,23 +1552,22 @@ proc genObjectFields(m: BModule; typ, origType: PType, n: PNode, expr: Rope;
|
||||
else:
|
||||
m.s[cfsData].addArrayVar(kind = Local, name = tmp,
|
||||
elementType = ptrType("TNimNode"), len = toInt(L)+1)
|
||||
for i in 1..<n.len:
|
||||
var b = n[i] # branch
|
||||
for b in sonsFrom(n, 1):
|
||||
var tmp2 = getNimNode(m)
|
||||
genObjectFields(m, typ, origType, lastSon(b), tmp2, info)
|
||||
case b.kind
|
||||
of nkOfBranch:
|
||||
if b.len < 2:
|
||||
internalError(m.config, b.info, "genObjectFields; nkOfBranch broken")
|
||||
for j in 0..<b.len - 1:
|
||||
if b[j].kind == nkRange:
|
||||
var x = toInt(getOrdValue(b[j].firstSon))
|
||||
var y = toInt(getOrdValue(b[j][1]))
|
||||
for label in sonsButLast(b):
|
||||
if label.kind == nkRange:
|
||||
var x = toInt(getOrdValue(label.firstSon))
|
||||
var y = toInt(getOrdValue(label.secondSon))
|
||||
while x <= y:
|
||||
m.s[cfsTypeInit3].addSubscriptAssignment(tmp, cIntValue(x), cAddr(tmp2))
|
||||
inc(x)
|
||||
else:
|
||||
m.s[cfsTypeInit3].addSubscriptAssignment(tmp, cIntValue(getOrdValue(b[j])), cAddr(tmp2))
|
||||
m.s[cfsTypeInit3].addSubscriptAssignment(tmp, cIntValue(getOrdValue(label)), cAddr(tmp2))
|
||||
of nkElse:
|
||||
m.s[cfsTypeInit3].addSubscriptAssignment(tmp, cIntValue(L), cAddr(tmp2))
|
||||
else: internalError(m.config, n.info, "genObjectFields(nkRecCase)")
|
||||
@@ -1780,7 +1785,7 @@ proc generateRttiDestructor(g: ModuleGraph; typ: PType; owner: PSym; kind: TType
|
||||
|
||||
dest.typ = getSysType(g, info, tyPointer)
|
||||
|
||||
result.typ = newProcType(info, idgen, owner)
|
||||
result.typ = newProcType(info, idgen, result)
|
||||
result.typ.addParam dest
|
||||
|
||||
var n = newNodeI(nkProcDef, info, bodyPos+1)
|
||||
@@ -1856,7 +1861,7 @@ proc getObjDepth(t: PType): int16 =
|
||||
result = -1
|
||||
while x != nil:
|
||||
x = skipTypes(x, skipPtrs)
|
||||
x = x[0]
|
||||
x = x.baseClass
|
||||
inc(result)
|
||||
|
||||
proc genDisplayElem(d: MD5Digest): uint32 =
|
||||
@@ -1872,7 +1877,7 @@ proc genDisplay(result: var Builder, m: BModule; t: PType, depth: int) =
|
||||
while x != nil:
|
||||
x = skipTypes(x, skipPtrs)
|
||||
seqs[i] = cIntValue(genDisplayElem(MD5Digest(hashType(x, m.config))))
|
||||
x = x[0]
|
||||
x = x.baseClass
|
||||
inc i
|
||||
|
||||
var arr: StructInitializer
|
||||
@@ -1891,11 +1896,30 @@ proc genVTable(result: var Builder, seqs: seq[PSym]) =
|
||||
result.add(cCast(CPointer, seqs[i].loc.snippet))
|
||||
|
||||
proc genTypeInfoV2OldImpl(m: BModule; t, origType: PType, name: Rope; info: TLineInfo) =
|
||||
## The C++/HCR flavour: C++ has no designated initializers, so the RTTI record
|
||||
## is a bare variable that the module's `DatInit` fills field by field.
|
||||
cgsym(m, "TNimTypeV2")
|
||||
m.s[cfsStrData].addDeclWithVisibility(Private):
|
||||
m.s[cfsStrData].addVar(kind = Local, name = name, typ = "TNimTypeV2")
|
||||
if m.config.cmd == cmdNifC:
|
||||
# Same emit-everywhere split as `genTypeInfoV2Impl`: every `cg` process that
|
||||
# demands this type declares it `extern`, and the DEFINITION is a droppable
|
||||
# `'d'` unit the merge stage gives a single owner. Without the split the bare
|
||||
# `TNimTypeV2 x;` in each TU is a tentative definition — which C's linker
|
||||
# merges but C++'s does not, so `nim cpp --ic:on` died at link with
|
||||
# "multiple definition of NTIv2__…". The field ASSIGNMENTS stay in every
|
||||
# TU's `DatInit`: they are top-level code, not a definition, and every module
|
||||
# computes the same values.
|
||||
m.s[cfsStrData].addDeclWithVisibility(Extern):
|
||||
m.s[cfsStrData].addVar(kind = Local, name = name, typ = "TNimTypeV2")
|
||||
m.s[cfsVars].add(cnifDefDirective(name, "d", icNifName(m, origType)))
|
||||
var def = newBuilder("")
|
||||
def.addDeclWithVisibility(Private):
|
||||
def.addVar(kind = Local, name = name, typ = "TNimTypeV2")
|
||||
m.s[cfsVars].add extract(def)
|
||||
m.s[cfsVars].add(cnifEndDefs())
|
||||
m.icDataDefs.add (name, icNifName(m, origType))
|
||||
else:
|
||||
m.s[cfsStrData].addDeclWithVisibility(Private):
|
||||
m.s[cfsStrData].addVar(kind = Local, name = name, typ = "TNimTypeV2")
|
||||
|
||||
var flags = 0
|
||||
if not canFormAcycle(m.g.graph, t): flags = flags or 1
|
||||
@@ -2070,7 +2094,7 @@ proc genTypeInfoV2(m: BModule; t: PType; info: TLineInfo): Rope =
|
||||
result = "NTIv2$1_" % [rope($sig)]
|
||||
m.typeInfoMarkerV2[sig] = result
|
||||
|
||||
let owner = t.skipTypes(typedescPtrs).itemId.module
|
||||
let owner = t.skipTypes(typedescPtrs).bindingId.module
|
||||
# In the per-module backend (`cg`) RTTI is emit-everywhere like procs and
|
||||
# consts: every demanding module emits the `'d'` definition (deduped to one
|
||||
# owner by the merge stage). The owner-routing below would instead push the
|
||||
@@ -2173,7 +2197,7 @@ proc genTypeInfoV1(m: BModule; t: PType; info: TLineInfo): Rope =
|
||||
declareNimType(m, "TNimType", result, old.int)
|
||||
return prefixTI(result)
|
||||
|
||||
var owner = t.skipTypes(typedescPtrs).itemId.module
|
||||
var owner = t.skipTypes(typedescPtrs).bindingId.module
|
||||
# In the per-module backend (`cg`) V1 RTTI is emit-everywhere like procs,
|
||||
# consts and V2 type info: every demanding module emits the `'d'` definition
|
||||
# (deduped to one owner by the merge stage). The owner-routing below would
|
||||
@@ -2265,7 +2289,7 @@ proc genTypeInfo*(config: ConfigRef, m: BModule; t: PType; info: TLineInfo): Rop
|
||||
|
||||
proc retrieveSym(n: PNode): PSym =
|
||||
case n.kind
|
||||
of nkPostfix: result = retrieveSym(n[1])
|
||||
of nkPostfix: result = retrieveSym(n.secondSon)
|
||||
of nkPragmaExpr, nkTypeDef: result = retrieveSym(n.firstSon)
|
||||
of nkSym: result = n.sym
|
||||
else: result = nil
|
||||
@@ -2289,8 +2313,8 @@ proc genTypeSection(m: BModule, n: PNode) =
|
||||
# declarations where the type is already written separately before the initializer.
|
||||
proc genCppConstructorExpr(m: BModule, prc: BProc; typ: PType; didGenTemp: var bool): Snippet =
|
||||
var params = ""
|
||||
if typ.itemId in m.g.graph.initializersPerType:
|
||||
let call = m.g.graph.initializersPerType[typ.itemId]
|
||||
if typ.bindingId in m.g.graph.initializersPerType:
|
||||
let call = m.g.graph.initializersPerType[typ.bindingId]
|
||||
if call != nil:
|
||||
var p = prc
|
||||
if p == nil:
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
import
|
||||
ast, types, msgs, wordrecg,
|
||||
platform, trees, options, cgendata, mangleutils, renderer, modulegraphs
|
||||
platform, trees, options, cgendata, mangleutils, renderer, modulegraphs, bnode
|
||||
|
||||
import std/[hashes, strutils, formatfloat]
|
||||
|
||||
@@ -22,18 +22,38 @@ proc getPragmaStmt*(n: PNode, w: TSpecialWord): PNode =
|
||||
case n.kind
|
||||
of nkStmtList:
|
||||
result = nil
|
||||
for i in 0..<n.len:
|
||||
result = getPragmaStmt(n[i], w)
|
||||
for it in sons(n):
|
||||
result = getPragmaStmt(it, w)
|
||||
if result != nil: break
|
||||
of nkPragma:
|
||||
result = nil
|
||||
for i in 0..<n.len:
|
||||
if whichPragma(n[i]) == w: return n[i]
|
||||
for it in sons(n):
|
||||
if whichPragma(it) == w: return it
|
||||
else:
|
||||
result = nil
|
||||
|
||||
proc stmtsContainPragma*(n: PNode, w: TSpecialWord): bool =
|
||||
result = getPragmaStmt(n, w) != nil
|
||||
proc stmtsContainPragma*(n: AnyNode, w: TSpecialWord): bool =
|
||||
## Deliberately NOT `getPragmaStmt(n, w) != nil`, and the reason is the one
|
||||
## shape the `AnyNode` seam cannot serve: a proc that returns a node OR nil.
|
||||
## `.bif` spells a missing child as a `DotToken` *inside* a tree, so there is
|
||||
## no nil token to hand back as a return value, and a `Cursor` is not nilable.
|
||||
## Predicates split out from such a proc are the way across.
|
||||
##
|
||||
## The duplicated traversal is the cost, and it is checked rather than
|
||||
## trusted: `grindPredicates` asserts this answers exactly
|
||||
## `getPragmaStmt(n, w) != nil` at every node, so the two cannot drift apart
|
||||
## silently.
|
||||
case n.kind
|
||||
of nkStmtList:
|
||||
result = false
|
||||
for it in sons(n):
|
||||
if stmtsContainPragma(it, w): return true
|
||||
of nkPragma:
|
||||
result = false
|
||||
for it in sons(n):
|
||||
if whichPragma(it) == w: return true
|
||||
else:
|
||||
result = false
|
||||
|
||||
proc hashString*(conf: ConfigRef; s: string): BiggestInt =
|
||||
# has to be the same algorithm as strmantle.hashString!
|
||||
@@ -92,7 +112,7 @@ proc ccgIntroducedPtr*(conf: ConfigRef; s: PSym, retType: PType): bool =
|
||||
result = true
|
||||
elif (optByRef in s.options) or (getSize(conf, pt) > conf.target.floatSize * 3):
|
||||
result = true # requested anyway
|
||||
elif (tfFinal in pt.flags) and (pt[0] == nil):
|
||||
elif (tfFinal in pt.flags) and (pt.baseClass == nil):
|
||||
result = false # no need, because no subtyping possible
|
||||
else:
|
||||
result = true # ordinary objects are always passed by reference,
|
||||
@@ -113,20 +133,12 @@ proc encodeName*(name: string): string =
|
||||
proc makeUnique(m: BModule; s: PSym, name: string = ""): string =
|
||||
result = if name == "": s.name.s else: name
|
||||
# keep backend-minted ids out of the `_u` namespace; their item counter
|
||||
# restarts at 0 and would collide with loaded symbols' ids
|
||||
# restarts at 0 and would collide with loaded symbols' ids. Which integer
|
||||
# identifies such a symbol is decided ONCE, in `astdef.backendMintedDisamb`,
|
||||
# shared with `mangleProcNameExt` and `ast2nif.toNifSymName`.
|
||||
if s.itemId.isBackendMinted:
|
||||
result.add "_c"
|
||||
if (s.disamb and HookDisambBit) != 0'i32:
|
||||
# A backend-minted sym whose `disamb` is content-derived (setHookDisamb gave
|
||||
# it HookDisambBit) — e.g. the `rttiDestroy` wrapper. Its `itemId.item` is a
|
||||
# PER-PROCESS backend counter, so using it makes the C name diverge across
|
||||
# the emit-everywhere processes: the type's RTTI table (emit-everywhere,
|
||||
# merge-deduped) ends up referencing one process's `_c<item>` while the
|
||||
# wrapper is defined with another's -> undefined at link (`rttiDestroy_c23`).
|
||||
# The content-derived disamb is stable across processes, so use it.
|
||||
result.add $s.disamb
|
||||
else:
|
||||
result.add $s.itemId.item
|
||||
result.add $backendMintedDisamb(s)
|
||||
else:
|
||||
result.add "_u"
|
||||
# Mirror `mangleProcNameExt`: use the per-(module,name) `disamb`, NOT
|
||||
@@ -156,7 +168,7 @@ proc encodeType*(m: BModule; t: PType; staticLists: var string): string =
|
||||
of tyObject, tyEnum, tyDistinct, tyUserTypeClass, tyGenericParam:
|
||||
result = encodeSym(m, t.sym)
|
||||
of tyGenericInst, tyUserTypeClassInst, tyGenericBody:
|
||||
result = encodeName(t[0].sym.name.s)
|
||||
result = encodeName(t.genericHead.sym.name.s)
|
||||
result.add "I"
|
||||
for i in 1..<t.len - 1:
|
||||
result.add encodeType(m, t[i], staticLists)
|
||||
@@ -168,8 +180,7 @@ proc encodeType*(m: BModule; t: PType; staticLists: var string): string =
|
||||
of tySequence: encodeName("seq")
|
||||
else: encodeName(kindName)
|
||||
result.add "I"
|
||||
for i in 0..<t.len:
|
||||
let s = t[i]
|
||||
for s in kids(t):
|
||||
if s.isNil: continue
|
||||
result.add encodeType(m, s, staticLists)
|
||||
result.add "E"
|
||||
@@ -180,12 +191,12 @@ proc encodeType*(m: BModule; t: PType; staticLists: var string): string =
|
||||
raiseAssert "unreachable"
|
||||
of tyRange:
|
||||
var val = "range_"
|
||||
if t.n[0].typ.kind in {tyFloat..tyFloat128}:
|
||||
val.addFloat t.n[0].floatVal
|
||||
if t.n.firstSon.typ.kind in {tyFloat..tyFloat128}:
|
||||
val.addFloat t.n.firstSon.floatVal
|
||||
val.add "_"
|
||||
val.addFloat t.n[1].floatVal
|
||||
val.addFloat t.n.secondSon.floatVal
|
||||
else:
|
||||
val.add $t.n[0].intVal & "_" & $t.n[1].intVal
|
||||
val.add $t.n.firstSon.intVal & "_" & $t.n.secondSon.intVal
|
||||
result = encodeName(val)
|
||||
of tyString..tyUInt64, tyPointer, tyBool, tyChar, tyVoid, tyAnything, tyNil, tyEmpty:
|
||||
result = encodeName(kindName)
|
||||
|
||||
1139
compiler/cgen.nim
1139
compiler/cgen.nim
File diff suppressed because it is too large
Load Diff
@@ -142,6 +142,13 @@ type
|
||||
# not a list of IDs nor can it be made to be one.
|
||||
mangledPrcs*: HashSet[string]
|
||||
|
||||
icEmitted*: IntSet
|
||||
## Under `--icBackendStage:cg`: the positions of the modules THIS process
|
||||
## writes a translation unit for. `cgen.findPendingModule` consults it to
|
||||
## decide where a demanded definition goes — see the comment there. Empty
|
||||
## outside that stage, which is why every other backend keeps the ordinary
|
||||
## whole-program routing.
|
||||
|
||||
TCGen = object of PPassContext # represents a C source file
|
||||
s*: TCFileSections # sections of the C file
|
||||
flags*: set[CodegenFlag]
|
||||
@@ -186,6 +193,10 @@ type
|
||||
# embeds (redirected defs, shared instances,
|
||||
# hooks); recorded as the artifact's cdeps so
|
||||
# the reuse gate can check their impl cookies
|
||||
icGlobalDtorName*: string # per-module backend: the C name of this
|
||||
# module's global-destructor proc, recorded in
|
||||
# the artifact's meta head so the main module's
|
||||
# `cg` — a different process — can call it
|
||||
icDataDefs*: seq[tuple[cname, nifname: string]]
|
||||
# C names of data definitions (consts, globals,
|
||||
# RTTI) this TU embeds plus their NIF symbol
|
||||
@@ -234,7 +245,8 @@ proc newProc*(prc: PSym, module: BModule): BProc =
|
||||
|
||||
proc newModuleList*(g: ModuleGraph): BModuleList =
|
||||
BModuleList(typeInfoMarker: initTable[SigHash, tuple[str: Rope, owner: int32]](),
|
||||
config: g.config, graph: g, nimtvDeclared: initIntSet())
|
||||
config: g.config, graph: g, nimtvDeclared: initIntSet(),
|
||||
icEmitted: initIntSet())
|
||||
|
||||
iterator cgenModules*(g: BModuleList): BModule =
|
||||
for m in g.modulesClosed:
|
||||
|
||||
@@ -197,10 +197,10 @@ proc methodDef*(g: ModuleGraph; idgen: IdGenerator; s: PSym) =
|
||||
if witness.isNil: witness = g.methods[i].methods[0]
|
||||
# create a new dispatcher:
|
||||
# stores the id and the position
|
||||
if s.typ.firstParamType.skipTypes(skipPtrs).itemId notin g.bucketTable:
|
||||
g.bucketTable[s.typ.firstParamType.skipTypes(skipPtrs).itemId] = 1
|
||||
if s.typ.firstParamType.skipTypes(skipPtrs).bindingId notin g.bucketTable:
|
||||
g.bucketTable[s.typ.firstParamType.skipTypes(skipPtrs).bindingId] = 1
|
||||
else:
|
||||
g.bucketTable.inc(s.typ.firstParamType.skipTypes(skipPtrs).itemId)
|
||||
g.bucketTable.inc(s.typ.firstParamType.skipTypes(skipPtrs).bindingId)
|
||||
g.methods.add((methods: @[s], dispatcher: createDispatcher(s, g, idgen)))
|
||||
logMethodDef(g, s)
|
||||
#echo "adding ", s.info
|
||||
|
||||
@@ -73,14 +73,15 @@ proc stripCnifMarks*(s: string): string =
|
||||
inc i
|
||||
|
||||
const
|
||||
CnifVersion* = "4"
|
||||
CnifVersion* = "5"
|
||||
## Artifact format version, stored in the meta head. Artifacts written
|
||||
## by an older compiler lack the NIF names and the cref group the
|
||||
## def-retention check needs (v2), the cdeps group the fine-grained
|
||||
## reuse gate needs (v3), or the type NIF names and cnif-marked extern
|
||||
## reuse gate needs (v3), the type NIF names and cnif-marked extern
|
||||
## RTTI references the typeinfo flavor of the def-retention check
|
||||
## needs (v4); `readCnifHeads` reports them as invalid so their TUs
|
||||
## simply regenerate once.
|
||||
## needs (v4), or the global-destructor name the main module's `cg`
|
||||
## calls at teardown (v5); `readCnifHeads` reports them as invalid so
|
||||
## their TUs simply regenerate once.
|
||||
|
||||
proc cnifDefDirective*(name, flags, nifName: string): string =
|
||||
CnifDefStart & name & CnifDefSep & flags & CnifDefSep & nifName & CnifDefEnd
|
||||
@@ -91,15 +92,17 @@ proc cnifEndDefs*(): string =
|
||||
proc writeCnifArtifact*(code: string; outfile: string;
|
||||
initRequired = false; datInitRequired = false;
|
||||
dataDefs: openArray[tuple[cname, nifname: string]] = [];
|
||||
semmedNif = ""; moduleBase = "";
|
||||
semmedNif = ""; moduleBase = ""; globalDtor = "";
|
||||
implDeps: openArray[string] = []) =
|
||||
## Splits the marked module text into the `.c.nif` artifact.
|
||||
## The artifact starts with a `(meta <flags> "semmedNif" "moduleBase"
|
||||
## "version")` head — whether the module has an init/datInit proc
|
||||
## ('i'/'d'), which semmed NIF it was generated from and the module's
|
||||
## "version" "globalDtor")` head — whether the module has an init/datInit
|
||||
## proc ('i'/'d'), which semmed NIF it was generated from, the module's
|
||||
## mangled base name (what `registerModuleToMain` and the reuse decision
|
||||
## need when the TU is reused in a later run, possibly without the module
|
||||
## ever being loaded again) — a `(cdata (SymbolDef StrLit)*)` group naming
|
||||
## ever being loaded again) and the C name of the module's global-destructor
|
||||
## proc, if any (what the main module's `cg` calls at program teardown; see
|
||||
## `cgen.genIcModuleDestroyGlobals`) — a `(cdata (SymbolDef StrLit)*)` group naming
|
||||
## the data definitions (consts, globals, RTTI) the TU embeds together
|
||||
## with their NIF names, a `(cref Ident*)` group naming every C name
|
||||
## the TU references but does not define itself (what the def-retention
|
||||
@@ -153,6 +156,7 @@ proc writeCnifArtifact*(code: string; outfile: string;
|
||||
b.addStrLit semmedNif
|
||||
b.addStrLit moduleBase
|
||||
b.addStrLit CnifVersion
|
||||
b.addStrLit globalDtor
|
||||
b.withTree "cdata":
|
||||
for d in dataDefs:
|
||||
b.addSymbolDef d.cname
|
||||
@@ -261,6 +265,8 @@ type
|
||||
datInitRequired*: bool
|
||||
semmedNif*: string ## the semmed NIF this TU was generated from
|
||||
moduleBase*: string ## the module's mangled base name
|
||||
globalDtor*: string ## C name of the module's global-destructor proc
|
||||
## ("" when the module has no global destructors)
|
||||
cdefs*: seq[tuple[cname, nifname: string]] ## the proc definitions
|
||||
cdata*: seq[tuple[cname, nifname: string]] ## the data definitions
|
||||
crefs*: seq[string] ## C names referenced but not defined here
|
||||
@@ -303,6 +309,7 @@ proc readCnifHeads*(f: string): CnifHeads =
|
||||
if strIdx == 0: result.semmedNif = strVal(c)
|
||||
elif strIdx == 1: result.moduleBase = strVal(c)
|
||||
elif strIdx == 2: version = strVal(c)
|
||||
elif strIdx == 3: result.globalDtor = strVal(c)
|
||||
inc strIdx
|
||||
inc c
|
||||
else:
|
||||
@@ -585,6 +592,13 @@ proc computeMergeDecision*(files: openArray[string]): MergeDecision =
|
||||
if d in result.live: inc result.liveDefs
|
||||
|
||||
const MergeDecisionFile* = "ic.backend.merge.nif"
|
||||
const LiveModulesFile* = "ic.backend.live.txt"
|
||||
## One `.c.nif` path per line: exactly the artifacts of the modules the CURRENT
|
||||
## build graph considers live. The `merge` stage reads this instead of globbing
|
||||
## `*.c.nif` off the nimcache, so a leftover artifact from an unrelated build
|
||||
## that happens to share the cache directory cannot be merged in (which is what
|
||||
## made a shared prebuilt cache unusable: merge picked owners in modules the
|
||||
## program does not import, and the link then wanted their objects).
|
||||
## Fixed name of the merge stage's output in the nimcache, read by `emit`.
|
||||
|
||||
proc writeMergeDecision*(outfile: string; d: MergeDecision) =
|
||||
|
||||
@@ -627,7 +627,11 @@ proc processMemoryManagementOption(switch, arg: string, pass: TCmdLinePass,
|
||||
conf.selectedGC = gcHooks
|
||||
defineSymbol(conf.symbols, "gchooks")
|
||||
incl conf.globalOptions, optSeqDestructors
|
||||
processOnOffSwitchG(conf, {optSeqDestructors}, arg, pass, info)
|
||||
# (The `arg` here is the mm MODE — "hooks" — so feeding it to an on/off
|
||||
# switch made `--mm:hooks` fail outright with "'on' or 'off' expected, but
|
||||
# 'hooks' found". The `incl` above is what that call was meant to do.
|
||||
# Reachable only via the explicit switch: `--newruntime` sets
|
||||
# `selectedGC` directly, which is why this stayed hidden.)
|
||||
if pass in {passCmd2, passPP}:
|
||||
defineSymbol(conf.symbols, "nimSeqsV2")
|
||||
of "go":
|
||||
@@ -985,12 +989,16 @@ proc processSwitch*(switch, arg: string, pass: TCmdLinePass, info: TLineInfo;
|
||||
expectArg(conf, switch, arg, pass, info)
|
||||
if pass in {passCmd2, passPP}:
|
||||
conf.icBackendStage = arg
|
||||
of "icbackendmodule":
|
||||
# `nim nifc` only: the NIF module suffix the cg/emit stage operates on (see
|
||||
# options.icBackendModule).
|
||||
of "icbackendmodule", "icbackendmodules":
|
||||
# `nim nifc` only: the NIF module suffixes the lower/cg/emit stage operates
|
||||
# on, comma-separated — the invocation's batch (see
|
||||
# options.icBackendModules). The singular spelling is the same switch: a
|
||||
# one-module batch is what the per-module fan-out passes.
|
||||
expectArg(conf, switch, arg, pass, info)
|
||||
if pass in {passCmd2, passPP}:
|
||||
conf.icBackendModule = arg
|
||||
conf.icBackendModules = @[]
|
||||
for suffix in arg.split(','):
|
||||
if suffix.len > 0: conf.icBackendModules.add suffix
|
||||
of "import":
|
||||
expectArg(conf, switch, arg, pass, info)
|
||||
if pass in {passCmd2, passPP}:
|
||||
@@ -1088,9 +1096,14 @@ proc processSwitch*(switch, arg: string, pass: TCmdLinePass, info: TLineInfo;
|
||||
expectNoArg(conf, switch, arg, pass, info)
|
||||
helpOnError(conf, pass)
|
||||
of "symbolfiles", "incremental", "ic":
|
||||
if switch.normalize == "symbolfiles": deprecatedAlias(switch, "incremental")
|
||||
if pass in {passCmd2, passPP} and switch.normalize == "symbolfiles":
|
||||
deprecatedAlias(switch, "incremental")
|
||||
# xxx maybe also ic, since not in help?
|
||||
if pass in {passCmd2, passPP}:
|
||||
# `--ic:on` is read in passCmd1 too: `nim.nim` decides BEFORE config loading
|
||||
# whether this run is an IC driver (`ensureIcConfig` must produce the
|
||||
# precompiled config the driver itself then replays), and passCmd1 is the
|
||||
# only pass that has run by then.
|
||||
if pass in {passCmd1, passCmd2, passPP}:
|
||||
case arg.normalize
|
||||
of "on": conf.ic = true
|
||||
of "legacy": conf.symbolFiles = v2Sf
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
## for details. Note this is a first implementation and only the "Concept matching"
|
||||
## section has been implemented.
|
||||
|
||||
import ast, semdata, lookups, lineinfos, idents, msgs, renderer, types, layeredtable
|
||||
import ast, semdata, lookups, lineinfos, idents, msgs, renderer, types,
|
||||
layeredtable, semtypinst
|
||||
|
||||
import std/sets
|
||||
|
||||
@@ -71,7 +72,8 @@ proc semConceptDeclaration*(c: PContext; n: PNode): PNode =
|
||||
|
||||
type
|
||||
MatchFlags* = enum
|
||||
mfDontBind # Do not bind generic parameters
|
||||
mfDontBind # Do not export bindings from the concept match
|
||||
mfBindGenericParam # Export inferred invocation parameters despite mfDontBind
|
||||
mfCheckGeneric # formal <- formal comparison as opposed to formal <- operand
|
||||
|
||||
ConceptTypePair = tuple[conceptId, typeId: ItemId]
|
||||
@@ -205,7 +207,7 @@ proc matchConceptToImpl(c: PContext, f, potentialImpl: PType; m: var MatchCon):
|
||||
|
||||
# Cycle detection: track (concept, type) pairs to prevent infinite recursion.
|
||||
# Returns true on cycle (coinductive semantics) to support co-dependent concepts.
|
||||
let pair: ConceptTypePair = (concpt.itemId, potentialImpl.itemId)
|
||||
let pair: ConceptTypePair = (concpt.bindingId, potentialImpl.bindingId)
|
||||
if pair in m.marker:
|
||||
return true
|
||||
m.marker.incl pair
|
||||
@@ -573,7 +575,17 @@ proc conceptMatchNode(c: PContext; n: PNode; m: var MatchCon): bool =
|
||||
# error was reported earlier.
|
||||
result = false
|
||||
|
||||
proc fixBindings(bindings: var LayeredIdTable; concpt: PType; invocation: PType; m: var MatchCon) =
|
||||
proc resolvedBinding(c: PContext; t: PType; m: MatchCon): PType =
|
||||
## An inferred concept parameter can refer to an implementation-local
|
||||
## generic parameter, for example `Elem[Impl.T]`. Resolve it while the
|
||||
## matcher's private bindings (`Impl.T -> int`) are still available.
|
||||
if t.containsUnresolvedType:
|
||||
prepareMetatypeForSigmatch(c, m.bindings, m.concpt.sym.info, t)
|
||||
else:
|
||||
t
|
||||
|
||||
proc fixBindings(c: PContext; bindings: var LayeredIdTable; concpt: PType;
|
||||
invocation: PType; m: var MatchCon) =
|
||||
# invocation != nil means we have a non-atomic concept:
|
||||
if invocation != nil and invocation.kind == tyGenericInvocation:
|
||||
assert concpt.sym.typ.kind == tyGenericBody
|
||||
@@ -585,8 +597,9 @@ proc fixBindings(bindings: var LayeredIdTable; concpt: PType; invocation: PType;
|
||||
continue
|
||||
let found = m.bindings.lookup(thisSym)
|
||||
if found != nil:
|
||||
when logBindings: echo "Invocation bind: ", thisSym, " ", found
|
||||
bindings.put(thisSym, found)
|
||||
let resolved = resolvedBinding(c, found, m)
|
||||
when logBindings: echo "Invocation bind: ", thisSym, " ", resolved
|
||||
bindings.put(thisSym, resolved)
|
||||
|
||||
# bind even more generic parameters
|
||||
let genBody = invocation.base
|
||||
@@ -602,6 +615,20 @@ proc fixBindings(bindings: var LayeredIdTable; concpt: PType; invocation: PType;
|
||||
bindings.put(invocation[i], boundV)
|
||||
bindings.put(concpt, m.potentialImplementation)
|
||||
|
||||
proc fixConstraintBindings(c: PContext; bindings: var LayeredIdTable;
|
||||
invocation: PType; m: MatchCon) =
|
||||
## Propagates only the dependent parameters of a concept constraint. The
|
||||
## concept itself and its private matcher bindings must remain unbound so
|
||||
## that independent constraints using the same concept don't get coupled.
|
||||
if invocation != nil and invocation.kind == tyGenericInvocation:
|
||||
let genBody = invocation.base
|
||||
assert genBody.kind == tyGenericBody
|
||||
for i in FirstGenericParamAt ..< invocation.kidsLen:
|
||||
if lookup(bindings, invocation[i]) == nil:
|
||||
let boundValue = m.bindings.lookup(genBody[i - 1])
|
||||
if boundValue != nil:
|
||||
bindings.put(invocation[i], resolvedBinding(c, boundValue, m))
|
||||
|
||||
proc processConcept(c: PContext; concpt, invocation: PType, bindings: var LayeredIdTable; m: var MatchCon): bool =
|
||||
m.bindings = m.bindings.newTypeMapLayer()
|
||||
if invocation != nil and invocation.kind == tyGenericInst:
|
||||
@@ -611,8 +638,11 @@ proc processConcept(c: PContext; concpt, invocation: PType, bindings: var Layere
|
||||
if invocation[i].kind != tyVoid:
|
||||
bindParam(c, m, genericBody[i-1], invocation[i])
|
||||
result = conceptMatchNode(c, concpt.conceptBody, m)
|
||||
if result and mfDontBind notin m.flags:
|
||||
fixBindings(bindings, concpt, invocation, m)
|
||||
if result:
|
||||
if mfDontBind notin m.flags:
|
||||
fixBindings(c, bindings, concpt, invocation, m)
|
||||
elif mfBindGenericParam in m.flags:
|
||||
fixConstraintBindings(c, bindings, invocation, m)
|
||||
|
||||
proc conceptMatch*(c: PContext; concpt, arg: PType; bindings: var LayeredIdTable; invocation: PType, flags: set[MatchFlags] = {}): bool =
|
||||
## Entry point from sigmatch. 'concpt' is the concept we try to match (here still a PType but
|
||||
|
||||
@@ -11,12 +11,15 @@
|
||||
## This enables incremental and parallel compilation using the `m` switch.
|
||||
|
||||
import std / [os, tables, sets, times, osproc, algorithm, strtabs, strutils, syncio]
|
||||
from std/sha1 import secureHash, `$`
|
||||
import options, msgs, lineinfos, pathutils, condsyms,
|
||||
modulepaths, extccomp, cnif, platform
|
||||
|
||||
import "../dist/nimony/src/lib" / [nifstreams, bitabs, nifreader, nifbuilder]
|
||||
import nifstreams
|
||||
import "../dist/nimony/src/lib" / [bitabs, nifreader, nifbuilder]
|
||||
import icmodnames
|
||||
import icnifcore
|
||||
from ic/replayer import BackendActionsExt
|
||||
|
||||
type
|
||||
FilePair = object
|
||||
@@ -26,6 +29,14 @@ type
|
||||
Node = ref object
|
||||
files: seq[FilePair] # main file + includes
|
||||
deps: seq[int] # indices into DepContext.nodes
|
||||
specDeps: seq[int] # the subset of `deps` reached ONLY through a `when`
|
||||
# condition the scanner could not evaluate
|
||||
missingImport: string # an `import` path this module's source names, under a
|
||||
# `when` the scanner could not decide, that does not
|
||||
# exist on disk (empty when all resolved)
|
||||
missingHardImport: string ## ditto but NOT under any undecidable `when`: the
|
||||
## real compile would reach this `import`, so it is
|
||||
## a genuine "cannot open file" error
|
||||
id: int
|
||||
|
||||
DepContext = object
|
||||
@@ -41,6 +52,9 @@ type
|
||||
scanningMain: bool # currently scanning the project main module's deps;
|
||||
# makes `when isMainModule` conditions evaluate true
|
||||
# only there (every other module is imported)
|
||||
speculating: int # nesting depth of `when` guards the scanner could not
|
||||
# decide; every import edge added while this is > 0 is
|
||||
# recorded as speculative (see pruneDeadSpeculative)
|
||||
|
||||
proc toPair(c: DepContext; f: string): FilePair =
|
||||
FilePair(nimFile: f, modname: moduleSuffix(f, cast[seq[string]](c.config.searchPaths)))
|
||||
@@ -51,6 +65,13 @@ proc depsFile(c: DepContext; f: FilePair): string =
|
||||
proc parsedFile(c: DepContext; f: FilePair): string =
|
||||
getNimcacheDir(c.config).string / f.modname & ".p.nif"
|
||||
|
||||
proc parsedDepsFile(c: DepContext; f: FilePair): string =
|
||||
## The deps sidecar `nifler parse --deps <src> <out>.p.nif` actually writes: it
|
||||
## appends `.deps.nif` to the OUTPUT path, giving `<mod>.p.deps.nif`. Not to be
|
||||
## confused with `depsFile` (`<mod>.deps.nif`), which the driver's own
|
||||
## `nifler deps` pre-scan writes.
|
||||
parsedFile(c, f).changeFileExt("") & ".deps.nif"
|
||||
|
||||
proc semmedFile(c: DepContext; f: FilePair): string =
|
||||
getNimcacheDir(c.config).string / f.modname & ".s.bif"
|
||||
|
||||
@@ -206,6 +227,18 @@ proc getsImplicitImports(c: DepContext; nimFile: string): bool =
|
||||
## system.nim and never reaches them). Stdlib == under conf.libpath.
|
||||
not isRelativeTo(nimFile, c.config.libpath.string)
|
||||
|
||||
proc addDepEdge(c: DepContext; current: Node; depId: int) =
|
||||
## Record `current -> depId`. While the scanner is inside a `when` guard it
|
||||
## could not evaluate (`c.speculating > 0`) the edge is *speculative*: it may
|
||||
## not exist in the real compile at all. An edge seen at least once outside
|
||||
## such a guard is hard and stays hard.
|
||||
if depId notin current.deps: current.deps.add depId
|
||||
if c.speculating > 0:
|
||||
if depId notin current.specDeps: current.specDeps.add depId
|
||||
else:
|
||||
let i = current.specDeps.find(depId)
|
||||
if i >= 0: current.specDeps.delete i
|
||||
|
||||
proc processImport(c: var DepContext; importPath: string; current: Node; origin: string) =
|
||||
# `origin` = the file the `import` literally appears in. Crucial for imports
|
||||
# inside `include`d files: e.g. `system.nim` includes `system/excpt.nim`, which
|
||||
@@ -217,6 +250,14 @@ proc processImport(c: var DepContext; importPath: string; current: Node; origin:
|
||||
# only after the post-sem `.s.deps` revealed the edge.
|
||||
let resolved = resolveImport(c, origin, importPath)
|
||||
if resolved.len == 0 or not fileExists(resolved):
|
||||
# The module does not exist on disk. Silently ignoring this is right for the
|
||||
# scanner (the `import` may sit in a dead `when` branch and the real compile
|
||||
# never looks at it), but remember it: `pruneDeadSpeculative` uses it to tell
|
||||
# a module that is merely unused apart from one that cannot compile at all.
|
||||
if c.speculating > 0:
|
||||
if current.missingImport.len == 0: current.missingImport = importPath
|
||||
elif current.missingHardImport.len == 0:
|
||||
current.missingHardImport = importPath
|
||||
return
|
||||
|
||||
let pair = c.toPair(resolved)
|
||||
@@ -225,7 +266,7 @@ proc processImport(c: var DepContext; importPath: string; current: Node; origin:
|
||||
if existingIdx == -1:
|
||||
# New module - create node and process it
|
||||
let newNode = Node(files: @[pair], id: c.nodes.len)
|
||||
current.deps.add newNode.id
|
||||
addDepEdge(c, current, newNode.id)
|
||||
# Every module depends on system.nim
|
||||
if c.systemNodeId >= 0:
|
||||
newNode.deps.add c.systemNodeId
|
||||
@@ -243,8 +284,7 @@ proc processImport(c: var DepContext; importPath: string; current: Node; origin:
|
||||
traverseDeps(c, pair, newNode)
|
||||
else:
|
||||
# Already processed - just add dependency
|
||||
if existingIdx notin current.deps:
|
||||
current.deps.add existingIdx
|
||||
addDepEdge(c, current, existingIdx)
|
||||
|
||||
proc skipSubtree(s: var Stream; first: PackedToken) =
|
||||
## Consume tokens until the ParLe at `first` is balanced. Caller has
|
||||
@@ -482,6 +522,18 @@ proc parseImportPath(s: var Stream; t: var PackedToken): seq[string] =
|
||||
for r in parseImportPath(s, t):
|
||||
result.add op & r
|
||||
if t.kind == ParRi: t = next(s) # skip closing ')'
|
||||
elif tag == "pragmax":
|
||||
# `import x {.all.}` serialises as `(pragmax x (pragmas all))`. Without
|
||||
# this it fell into the unknown-subtree skip below and the import was
|
||||
# DROPPED from the static graph: the build only learned about it from the
|
||||
# `.s.deps` sidecar a round later, after a round that failed with
|
||||
# "requires precompiled NIF for import". Correct, but a wasted round and
|
||||
# an alarming error line for an ordinary import.
|
||||
t = next(s) # skip 'pragmax' tag
|
||||
result = parseImportPath(s, t) # the path is the first child
|
||||
while t.kind != ParRi and t.kind != EofToken:
|
||||
discard parseImportPath(s, t) # the pragma list; consumed, not a path
|
||||
if t.kind == ParRi: t = next(s) # skip closing ')'
|
||||
elif tag == "bracket":
|
||||
t = next(s) # skip 'bracket' tag
|
||||
while t.kind != ParRi and t.kind != EofToken:
|
||||
@@ -533,14 +585,19 @@ proc readDepsFile(c: var DepContext; pair: FilePair; current: Node) =
|
||||
# entirely. Otherwise advance past the marker and parse the path.
|
||||
t = next(s)
|
||||
var live = true
|
||||
var speculative = false
|
||||
if t.kind == ParLe and pool.tags[t.tagId] == "when":
|
||||
# whenMarkerHolds consumes everything up to and including the
|
||||
# closing `)` of the `(when ...)` subtree. Drop the import only when
|
||||
# the condition is PROVABLY false; a `cvUnknown` condition (e.g. an
|
||||
# `else:` branch guarded by `not <unevaluatable call>`, as in
|
||||
# `when tryImport x: ... else: import x`) keeps the dependency so the
|
||||
# static graph never misses a real import.
|
||||
live = whenMarkerHolds(c, s) != cvFalse
|
||||
# static graph never misses a real import — but marks every edge it
|
||||
# creates speculative, so `pruneDeadSpeculative` can still drop a
|
||||
# subtree that provably cannot compile in this configuration.
|
||||
let cond = whenMarkerHolds(c, s)
|
||||
live = cond != cvFalse
|
||||
speculative = cond == cvUnknown
|
||||
t = next(s)
|
||||
if not live:
|
||||
# Drain the rest of this import/include node.
|
||||
@@ -558,6 +615,7 @@ proc readDepsFile(c: var DepContext; pair: FilePair; current: Node) =
|
||||
# that expand to several imports. A plain `import a, b, c` lists several
|
||||
# modules as siblings; a `fromimport` has a single path followed by the
|
||||
# imported symbol list, which must not be treated as modules.
|
||||
if speculative: inc c.speculating
|
||||
if tag == "fromimport" or tag == "importexcept":
|
||||
# `from m import syms` / `import m except syms`: the first child is the
|
||||
# module path; the rest is the (in/ex)cluded symbol list, which must not
|
||||
@@ -573,6 +631,7 @@ proc readDepsFile(c: var DepContext; pair: FilePair; current: Node) =
|
||||
processInclude(c, importPath, current, pair.nimFile)
|
||||
else:
|
||||
processImport(c, importPath, current, pair.nimFile)
|
||||
if speculative: dec c.speculating
|
||||
# Drain any remaining tokens of this node (e.g. the symbol list of a
|
||||
# `fromimport`), up to and including the node's closing ')'.
|
||||
var depth = 1
|
||||
@@ -689,6 +748,148 @@ proc traverseDeps(c: var DepContext; pair: FilePair; current: Node) =
|
||||
return
|
||||
readDepsFile(c, pair, current)
|
||||
|
||||
proc pruneDeadSpeculative(c: var DepContext) =
|
||||
## Drop modules that are reachable only through a `when` guard the scanner
|
||||
## cannot evaluate AND that cannot possibly compile because they import a
|
||||
## module which does not exist on disk.
|
||||
##
|
||||
## The motivating shape is the ordinary `{.strdefine.}` backend switch:
|
||||
##
|
||||
## const figdrawTextBackend* {.strdefine.} = "pixie"
|
||||
## when figdrawTextBackend == "harfbuzzy":
|
||||
## import ./textrasters/glyphid_raster # imports `pkg/harfbuzzy`
|
||||
##
|
||||
## The value of that const needs sem, so `evalCondCmp` answers `cvUnknown` and
|
||||
## the conservative rule keeps the import — the right call for an edge, but it
|
||||
## also gives `glyphid_raster` its own `nim m` rule. The classic compiler never
|
||||
## looks at that file; IC compiles it, cannot find `pkg/harfbuzzy`, and the
|
||||
## whole build dies on a package the user never installed because they never
|
||||
## selected that backend.
|
||||
##
|
||||
## Dropping is safe: if the guard *was* live, the importer's own `nim m` fails
|
||||
## on the missing NIF, records the import in its `.s.deps` sidecar, and the
|
||||
## discovery fixpoint re-adds the node — this time reporting the honest
|
||||
## `cannot open file: pkg/harfbuzzy/raw` instead of a cascade of
|
||||
## `undeclared identifier` noise.
|
||||
let n = c.nodes.len
|
||||
if n == 0: return
|
||||
|
||||
var roots = @[0]
|
||||
if c.systemNodeId >= 0: roots.add c.systemNodeId
|
||||
for i in c.implicitNodeIds: roots.add i
|
||||
|
||||
# Reachability through NON-speculative edges only: these modules are compiled
|
||||
# for certain, so a missing import in them is a genuine user error to report.
|
||||
var hard = newSeq[bool](n)
|
||||
var stack = roots
|
||||
while stack.len > 0:
|
||||
let v = stack.pop()
|
||||
if hard[v]: continue
|
||||
hard[v] = true
|
||||
for d in c.nodes[v].deps:
|
||||
if d notin c.nodes[v].specDeps and not hard[d]: stack.add d
|
||||
|
||||
# A module the real compile DOES reach, naming an import that is not on disk,
|
||||
# is a plain user error — and one nifmake cannot notice on its own: deleting
|
||||
# `effects.nim` moves no mtime, so the importer's `nim m` never re-fires and
|
||||
# `nim ic` happily relinked a stale binary while `nim c` said "cannot open
|
||||
# file". Report it here, where the graph scan is the only thing that looks at
|
||||
# import paths at all.
|
||||
var reported = false
|
||||
for i in 0 ..< n:
|
||||
if hard[i] and c.nodes[i].missingHardImport.len > 0:
|
||||
rawMessage(c.config, errGenerated,
|
||||
c.nodes[i].files[0].nimFile & ": cannot open file: " &
|
||||
c.nodes[i].missingHardImport)
|
||||
reported = true
|
||||
if reported: return
|
||||
|
||||
var dead = newSeq[bool](n)
|
||||
var anyDead = false
|
||||
for i in 0 ..< n:
|
||||
if not hard[i] and c.nodes[i].missingImport.len > 0:
|
||||
dead[i] = true
|
||||
anyDead = true
|
||||
if not anyDead: return
|
||||
|
||||
# Anything left reachable only through a dead node is dead too.
|
||||
var alive = newSeq[bool](n)
|
||||
stack = @[]
|
||||
for r in roots:
|
||||
if not dead[r]: stack.add r
|
||||
while stack.len > 0:
|
||||
let v = stack.pop()
|
||||
if alive[v]: continue
|
||||
alive[v] = true
|
||||
for d in c.nodes[v].deps:
|
||||
if not dead[d] and not alive[d]: stack.add d
|
||||
|
||||
# Drop the scan artifacts of a module that just left the graph, so an
|
||||
# edit-accumulated cache does not differ from a clean one for no reason
|
||||
# (`tests/ic/tdead_when_import` pins that). Re-running nifler if it ever comes
|
||||
# back costs a single parse.
|
||||
#
|
||||
# But a FILE can belong to several nodes, and only the NODE is dead.
|
||||
# `lib/system/inclrtl.nim` is `include`d by dozens of live stdlib modules and
|
||||
# also sits in the file set of a dead-speculative one; a clean build therefore
|
||||
# has its `.p.nif`, and deleting it here does not tidy the cache, it corrupts
|
||||
# it. The consequences compound: the missing output re-fires that file's
|
||||
# `nifler` rule, which rewrites the parsed file with a fresh mtime, which
|
||||
# re-fires every `nim_m` rule listing it as an input — 16 full module re-sems
|
||||
# (system, os, times, strutils, macros, unicode, ...) on every warm build, for
|
||||
# ever, because the scanner is stateless and rediscovers the dead node each
|
||||
# run. Measured on a 219-module program: an 11 s NO-OP build. So delete only
|
||||
# what no live node claims.
|
||||
var liveFiles = initHashSet[string]()
|
||||
for i in 0 ..< n:
|
||||
if alive[i]:
|
||||
for f in c.nodes[i].files: liveFiles.incl f.nimFile
|
||||
|
||||
var cascaded = 0
|
||||
for i in 0 ..< n:
|
||||
if not alive[i]:
|
||||
for f in c.nodes[i].files:
|
||||
if f.nimFile in liveFiles: continue
|
||||
removeFile(c.parsedFile(f))
|
||||
removeFile(c.depsFile(f))
|
||||
removeFile(c.parsedDepsFile(f))
|
||||
if c.nodes[i].missingImport.len > 0:
|
||||
rawMessage(c.config, hintSuccess,
|
||||
"ic: skipping " & c.nodes[i].files[0].nimFile &
|
||||
" (reached only under an undecidable `when`, and imports " &
|
||||
c.nodes[i].missingImport & ", which is not installed)")
|
||||
else:
|
||||
inc cascaded
|
||||
if cascaded > 0:
|
||||
rawMessage(c.config, hintSuccess,
|
||||
"ic: " & $cascaded & " further module(s) skipped, reachable only through those")
|
||||
|
||||
# Compact `c.nodes`; node ids ARE indices everywhere, so remap them all.
|
||||
var remap = newSeq[int](n)
|
||||
var newNodes: seq[Node] = @[]
|
||||
for i in 0 ..< n:
|
||||
if alive[i]:
|
||||
remap[i] = newNodes.len
|
||||
newNodes.add c.nodes[i]
|
||||
else:
|
||||
remap[i] = -1
|
||||
proc remapped(remap: seq[int]; src: seq[int]): seq[int] =
|
||||
result = @[]
|
||||
for x in src:
|
||||
if remap[x] >= 0 and remap[x] notin result: result.add remap[x]
|
||||
for node in newNodes:
|
||||
node.id = remap[node.id]
|
||||
node.deps = remapped(remap, node.deps)
|
||||
node.specDeps = remapped(remap, node.specDeps)
|
||||
c.nodes = newNodes
|
||||
|
||||
var pm = initTable[string, int]()
|
||||
for name, idx in c.processedModules:
|
||||
if idx >= 0 and idx < n and remap[idx] >= 0: pm[name] = remap[idx]
|
||||
c.processedModules = pm
|
||||
if c.systemNodeId >= 0: c.systemNodeId = remap[c.systemNodeId]
|
||||
c.implicitNodeIds = remapped(remap, c.implicitNodeIds)
|
||||
|
||||
proc computeSCCs(c: DepContext): seq[seq[int]] =
|
||||
## Tarjan's strongly-connected-components over the module dependency graph
|
||||
## (`node.deps`). Each returned component is a list of node indices; a module
|
||||
@@ -771,6 +972,19 @@ proc computeForwardedArgs(c: DepContext): seq[string] =
|
||||
# them — phantom outputs that re-fire the build on every rerun).
|
||||
if c.config.selectedGC != gcUnselected:
|
||||
result.add "--mm:" & $c.config.selectedGC
|
||||
# The children are invoked as `nim m` / `nim nifc`, so the driver's own command
|
||||
# token (`c`, `cpp`, `ic`) is gone and with it the backend it selected. Name it
|
||||
# explicitly — `nim cpp --ic:on` must not have its stdlib sem'd and its TUs
|
||||
# emitted as C. The exception model rides along for the same reason: `nim cpp`
|
||||
# defaults to `--exceptions:cpp`, which changes both codegen and sem.
|
||||
if c.config.backend != backendInvalid:
|
||||
result.add "--backend:" & $c.config.backend
|
||||
if c.config.exc != excNone:
|
||||
result.add "--exceptions:" & (case c.config.exc
|
||||
of excGoto: "goto"
|
||||
of excCpp: "cpp"
|
||||
of excQuirky: "quirky"
|
||||
else: "setjmp")
|
||||
# method dispatch semantics must match across the child processes:
|
||||
# a child compiled without --multimethods:on builds different dispatch
|
||||
# buckets (and rejects calls as ambiguous that multi-dispatch accepts)
|
||||
@@ -798,6 +1012,71 @@ proc computeForwardedArgs(c: DepContext): seq[string] =
|
||||
# replayed (`conf.icPreparsedConfig`); `commandIc` has already guaranteed it
|
||||
# exists, else it bailed.
|
||||
result.add "--icPreparsedConfig:" & c.config.icPreparsedConfig
|
||||
# Everything else the user typed on the `nim ic` command line. The children
|
||||
# replay the project's CONFIG FILES (ic_config.cfg.nif), never the driver's
|
||||
# argv, so a switch that exists only there — `--opt:speed`, `--panics:on`,
|
||||
# `--experimental:…`, `--passC:…` — silently did not reach them: `nim ic
|
||||
# --opt:speed` produced a byte-identical debug binary. Forward the switches
|
||||
# verbatim, minus the ones that MUST differ per child (the output/cache paths,
|
||||
# the command itself, and IC's own per-rule switches, which each rule sets).
|
||||
const notForwarded = [
|
||||
"nimcache", "out", "o", "outdir", "usenimcache", "run", "r",
|
||||
"incremental", "ic", "symbolfiles", "genbif",
|
||||
"icproject", "icpreparsedconfig", "icconfigout", "icgroup",
|
||||
"icbackendstage", "icbackendmodule", "ismainmodule",
|
||||
"help", "h", "fullhelp", "version", "v", "advanced"]
|
||||
for a in commandLineParams():
|
||||
if a.len < 2 or a[0] != '-': continue
|
||||
var i = 1
|
||||
if i < a.len and a[i] == '-': inc i
|
||||
var name = ""
|
||||
while i < a.len and a[i] notin {':', '='}:
|
||||
name.add a[i]
|
||||
inc i
|
||||
if normalize(name) notin notForwarded and a notin result:
|
||||
result.add a
|
||||
|
||||
proc configSignatureFile(c: DepContext; forwardedArgs: seq[string]): string =
|
||||
## nifmake decides staleness from file mtimes alone — it never looks at a
|
||||
## rule's command line. So changing `-d:someDefine`, `--mm:` or `--threads:`
|
||||
## between two `nim ic` runs re-generated the build file with the new switches
|
||||
## but re-fired nothing: the user got a silently stale binary built with the
|
||||
## OLD configuration. Reify the configuration as a FILE and make every rule
|
||||
## that consumes it an input, so a config change moves an mtime like any edit.
|
||||
## Written `OnlyIfChanged` so a genuine no-op run stays a no-op.
|
||||
##
|
||||
## Deliberately EXCLUDES the two per-build path switches (`--icproject:`,
|
||||
## `--icPreparsedConfig:`): they name where this build lives, not what it
|
||||
## produces, so including them made the signature differ between two caches
|
||||
## holding byte-identical artifacts — which defeats prefilling a test's cache
|
||||
## from a shared warm one (every rule would re-fire on the rewritten
|
||||
## signature). The precompiled config still counts, by CONTENT: a `nim.cfg`
|
||||
## edit changes the artifact, hence the hash, hence every rule.
|
||||
result = getNimcacheDir(c.config).string / "ic_build_args.txt"
|
||||
var content = ""
|
||||
for p in c.config.searchPaths:
|
||||
content.add "--path:" & p.string & "\n"
|
||||
for a in forwardedArgs:
|
||||
if a.startsWith("--icproject:") or a.startsWith("--icPreparsedConfig:"):
|
||||
continue
|
||||
content.add a & "\n"
|
||||
if c.config.icPreparsedConfig.len > 0 and fileExists(c.config.icPreparsedConfig):
|
||||
# Hash the precompiled config MINUS its `(nimcache "...")` entry — the one
|
||||
# line in the artifact that records where this build's cache lives rather
|
||||
# than what the config says. Everything else is genuinely config-derived, so
|
||||
# two builds with the same `nim.cfg`/`config.nims` hash the same no matter
|
||||
# which directory they run in.
|
||||
var normalized = ""
|
||||
try:
|
||||
for line in lines(c.config.icPreparsedConfig):
|
||||
if "(nimcache " in line: continue
|
||||
normalized.add line
|
||||
normalized.add '\n'
|
||||
except IOError, OSError:
|
||||
normalized = c.config.icPreparsedConfig
|
||||
content.add "config:" & $secureHash(normalized) & "\n"
|
||||
if not fileExists(result) or readFile(result) != content:
|
||||
writeFile(result, content)
|
||||
|
||||
proc generateFrontendBuildFile(c: DepContext; forwardedArgs: seq[string]): string =
|
||||
## Frontend build file: the nifler (parse) and `nim m` (sem) rules only. The
|
||||
@@ -861,8 +1140,13 @@ proc generateFrontendBuildFile(c: DepContext; forwardedArgs: seq[string]): strin
|
||||
b.addTree "output"
|
||||
b.addStrLit parsed
|
||||
b.endTree()
|
||||
# The deps sidecar this command really produces is `<mod>.p.deps.nif`,
|
||||
# not `<mod>.deps.nif` (which only the driver's `nifler deps` pre-scan
|
||||
# writes). Declaring the latter made the rule permanently stale — a
|
||||
# missing output is nifmake's strongest rebuild trigger — for every
|
||||
# module the pre-scan does not also cover.
|
||||
b.addTree "output"
|
||||
b.addStrLit c.depsFile(pair)
|
||||
b.addStrLit c.parsedDepsFile(pair)
|
||||
b.endTree()
|
||||
b.endTree()
|
||||
|
||||
@@ -878,6 +1162,7 @@ proc generateFrontendBuildFile(c: DepContext; forwardedArgs: seq[string]): strin
|
||||
# a NIF for each. Only dependencies *outside* the component become build-graph
|
||||
# inputs — intra-component edges are produced by this very rule and listing
|
||||
# them would reintroduce the cycle nifmake just rejected.
|
||||
let argsFile = configSignatureFile(c, forwardedArgs)
|
||||
let sccs = computeSCCs(c)
|
||||
var sccOf = newSeq[int](c.nodes.len)
|
||||
for sccId, comp in sccs:
|
||||
@@ -906,6 +1191,10 @@ proc generateFrontendBuildFile(c: DepContext; forwardedArgs: seq[string]): strin
|
||||
# Input 0 (the project file passed to `nim m`): the representative's .nim.
|
||||
b.withTree "input":
|
||||
b.addStrLit repPair.nimFile
|
||||
# The configuration this child is invoked with (see configSignatureFile).
|
||||
b.addTree "input"
|
||||
b.addStrLit argsFile
|
||||
b.endTree()
|
||||
# All parsed files of every member (nifler outputs this group consumes).
|
||||
for m in members:
|
||||
for f in c.nodes[m].files:
|
||||
@@ -999,7 +1288,7 @@ proc backendCFile(c: DepContext; node: Node): string =
|
||||
if node.id == 0: AbsoluteFile node.files[0].nimFile
|
||||
else: AbsoluteFile node.files[0].modname
|
||||
result = changeFileExt(completeCfilePath(c.config,
|
||||
mangleModuleName(c.config, cfilename).AbsoluteFile), ".nim.c").string
|
||||
mangleModuleName(c.config, cfilename).AbsoluteFile), icCFileExt(c.config)).string
|
||||
|
||||
proc computeLiveBackendNodes(c: DepContext): seq[bool] =
|
||||
## Which nodes the backend must code-generate: the closure reachable from the
|
||||
@@ -1031,6 +1320,76 @@ proc computeLiveBackendNodes(c: DepContext): seq[bool] =
|
||||
let idx = c.processedModules.getOrDefault(c.toPair(p).modname, -1)
|
||||
if idx >= 0: stack.add idx
|
||||
|
||||
proc intDefine(conf: ConfigRef; name: string; fallback: int): int =
|
||||
## `-d:<name>:N` as an int, or `fallback` when unset or unparsable.
|
||||
result = fallback
|
||||
if isDefined(conf, name):
|
||||
try: result = parseInt(conf.symbols[name])
|
||||
except ValueError: result = fallback
|
||||
|
||||
proc backendBatchSize(conf: ConfigRef; liveCount: int): int =
|
||||
## How many modules share one backend process. 1 is the historical per-module
|
||||
## fan-out; larger batches amortise the process floor and the dependency
|
||||
## closure load (measured on a 67-module program: 7.6 ms of process startup
|
||||
## and ~10 ms of closure loading per child, against 3.5 ms of actual codegen).
|
||||
##
|
||||
## `-d:icBatchSize:N` pins it. The default is 1 — the plumbing is in place but
|
||||
## the policy is not yet validated. `-d:icBatchSize:0` means "one batch per
|
||||
## job", which is the shape a tuned default will take: enough batches to keep
|
||||
## every core busy and no more, since a batch beyond that only buys
|
||||
## amortisation at the price of parallelism.
|
||||
if not isDefined(conf, "icBatchSize"): return 1
|
||||
result = intDefine(conf, "icBatchSize", 1)
|
||||
if result == 0:
|
||||
let jobs =
|
||||
if isDefined(conf, "icNoParallel"): 1
|
||||
elif isDefined(conf, "icJobs"): max(1, intDefine(conf, "icJobs", 1))
|
||||
elif conf.numberOfProcessors > 0: conf.numberOfProcessors
|
||||
else: 1
|
||||
result = (liveCount + jobs - 1) div jobs
|
||||
result = max(1, result)
|
||||
|
||||
proc emitBatches(c: DepContext; live: seq[bool];
|
||||
shared: seq[seq[int]]): seq[seq[int]] =
|
||||
## emit's partition. Unlike `lower`/`cg` it takes the MAIN module too and, by
|
||||
## default, puts every live node in one batch: emit owns no decisions, so
|
||||
## there is nothing for a grouping to get wrong (see the rule that uses this).
|
||||
## An explicit `-d:icBatchSize` reuses the shared partition instead, plus main,
|
||||
## so the fan-out remains available to compare against.
|
||||
if isDefined(c.config, "icBatchSize"):
|
||||
result = shared
|
||||
if live.len > 0 and live[0]: result.add @[0]
|
||||
else:
|
||||
var all: seq[int] = @[]
|
||||
for i in 0 ..< c.nodes.len:
|
||||
if live[i]: all.add i
|
||||
result = if all.len > 0: @[all] else: @[]
|
||||
|
||||
proc backendBatches(c: DepContext; live: seq[bool]): seq[seq[int]] =
|
||||
## Partition the live non-main nodes into batches of node indices. The main
|
||||
## module is never in one: it loads the whole program, so batching it with
|
||||
## anything defeats the memory bound the per-module split exists to give.
|
||||
##
|
||||
## Contiguous runs of `c.nodes`, which is import-traversal order, so a batch's
|
||||
## members tend to share dependencies and its union closure stays close to one
|
||||
## member's. A smarter partition (by closure overlap, or by the dirty set on an
|
||||
## incremental build) belongs here and nowhere else — every stage already takes
|
||||
## whatever grouping this returns.
|
||||
var liveIdx: seq[int] = @[]
|
||||
for i in 0 ..< c.nodes.len:
|
||||
if live[i] and c.nodes[i].id != 0: liveIdx.add i
|
||||
let size = backendBatchSize(c.config, liveIdx.len)
|
||||
result = @[]
|
||||
var i = 0
|
||||
while i < liveIdx.len:
|
||||
var batch: seq[int] = @[]
|
||||
var j = i
|
||||
while j < liveIdx.len and batch.len < size:
|
||||
batch.add liveIdx[j]
|
||||
inc j
|
||||
result.add batch
|
||||
i = j
|
||||
|
||||
proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string =
|
||||
## Per-module backend build file. One `nim_nifc` command template (the actual
|
||||
## stage/module switches ride in each rule's `(args …)`), then the stages of
|
||||
@@ -1086,6 +1445,8 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string
|
||||
if fileExists(cnifFiles[i]) or fileExists(cFiles[i]): prunedStale = true
|
||||
removeFile(cnifFiles[i])
|
||||
removeFile(cFiles[i])
|
||||
removeFile(cFiles[i] & ".stamp")
|
||||
removeFile(cFiles[i] & BackendActionsExt)
|
||||
# The merge decision is a pure function of the set of `.c.nif`s present; if we
|
||||
# just removed an over-approximated module's artifacts, a decision computed
|
||||
# while they were present is stale — it can name a now-absent module as a
|
||||
@@ -1096,6 +1457,8 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string
|
||||
if prunedStale:
|
||||
removeFile(mergeFile)
|
||||
|
||||
let argsFile = configSignatureFile(c, forwardedArgs)
|
||||
|
||||
var b = nifbuilder.open(result)
|
||||
defer: b.close()
|
||||
|
||||
@@ -1145,16 +1508,39 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string
|
||||
# frontend writes `.s.nif`s content-stably, so an interface change to a
|
||||
# dependency re-sems (and re-emits the `.s.nif` of) every transitive importer;
|
||||
# a module whose own `.s.nif` is unchanged genuinely needs no re-lowering.
|
||||
for i, node in c.nodes:
|
||||
if not live[i]: continue
|
||||
let batches = backendBatches(c, live)
|
||||
template suffixList(batch: seq[int]): string =
|
||||
var acc = ""
|
||||
for k, idx in batch:
|
||||
if k > 0: acc.add ","
|
||||
acc.add c.nodes[idx].files[0].modname
|
||||
acc
|
||||
|
||||
for batch in batches:
|
||||
b.addTree "do"
|
||||
b.addIdent "nim_nifc"
|
||||
b.withTree "args":
|
||||
b.addStrLit "--icBackendStage:lower"
|
||||
b.addStrLit "--icBackendModule:" & node.files[0].modname
|
||||
inputStr c.semmedFile(node.files[0])
|
||||
outputStr tFiles[i]
|
||||
b.addStrLit "--icBackendModules:" & suffixList(batch)
|
||||
for idx in batch:
|
||||
inputStr c.semmedFile(c.nodes[idx].files[0])
|
||||
inputStr argsFile
|
||||
for idx in batch:
|
||||
outputStr tFiles[idx]
|
||||
b.endTree()
|
||||
# The main module is its own rule in every stage: it loads the whole program.
|
||||
block:
|
||||
let i = 0
|
||||
if live[i]:
|
||||
b.addTree "do"
|
||||
b.addIdent "nim_nifc"
|
||||
b.withTree "args":
|
||||
b.addStrLit "--icBackendStage:lower"
|
||||
b.addStrLit "--icBackendModules:" & c.nodes[i].files[0].modname
|
||||
inputStr c.semmedFile(c.nodes[i].files[0])
|
||||
inputStr argsFile
|
||||
outputStr tFiles[i]
|
||||
b.endTree()
|
||||
|
||||
# cg: one rule per module. Input is this module's OWN `.t.nif`. cg DOES read
|
||||
# its dependencies' `.t.nif`s at runtime (loadDepClosure), but ordering is
|
||||
@@ -1166,47 +1552,97 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string
|
||||
# emit-everywhere'd but does not own is dropped by `emit` regardless, so a
|
||||
# stale copy here is harmless. The main module additionally depends on every
|
||||
# other `.c.nif` (it reads their init/datInit metas to wire up NimMain).
|
||||
for i, node in c.nodes:
|
||||
if not live[i]: continue
|
||||
for batch in batches:
|
||||
b.addTree "do"
|
||||
b.addIdent "nim_nifc"
|
||||
b.withTree "args":
|
||||
b.addStrLit "--icBackendStage:cg"
|
||||
b.addStrLit "--icBackendModule:" & node.files[0].modname
|
||||
inputStr tFiles[i]
|
||||
if node.id == 0:
|
||||
b.addStrLit "--icBackendModules:" & suffixList(batch)
|
||||
for idx in batch:
|
||||
inputStr tFiles[idx]
|
||||
inputStr argsFile
|
||||
for idx in batch:
|
||||
outputStr cnifFiles[idx]
|
||||
# The module's C compile/link directives (`{.passL.}` etc.), recorded so
|
||||
# the `link` stage recovers them without loading the module graph. See
|
||||
# `replayer.writeBackendActions`.
|
||||
outputStr cFiles[idx] & BackendActionsExt
|
||||
b.endTree()
|
||||
block:
|
||||
let i = 0
|
||||
if live[i]:
|
||||
b.addTree "do"
|
||||
b.addIdent "nim_nifc"
|
||||
b.withTree "args":
|
||||
b.addStrLit "--icBackendStage:cg"
|
||||
b.addStrLit "--icBackendModules:" & c.nodes[i].files[0].modname
|
||||
inputStr tFiles[i]
|
||||
inputStr argsFile
|
||||
for j in 0 ..< c.nodes.len:
|
||||
if c.nodes[j].id != 0 and live[j]:
|
||||
inputStr cnifFiles[j]
|
||||
outputStr cnifFiles[i]
|
||||
b.endTree()
|
||||
outputStr cnifFiles[i]
|
||||
outputStr cFiles[i] & BackendActionsExt
|
||||
b.endTree()
|
||||
|
||||
# merge: read every `.c.nif`, write the ownership/liveness decision.
|
||||
# merge: read the live modules' `.c.nif`, write the ownership/liveness
|
||||
# decision. The list is handed over as a FILE (`LiveModulesFile`) because the
|
||||
# merge child is a separate process that never sees the build file: without it
|
||||
# merge globbed `*.c.nif` off the nimcache and so silently absorbed artifacts
|
||||
# belonging to some other program that shares the directory.
|
||||
let liveFile = nimcache / LiveModulesFile
|
||||
block:
|
||||
var manifest = ""
|
||||
for i in 0 ..< c.nodes.len:
|
||||
if live[i]:
|
||||
manifest.add cnifFiles[i]
|
||||
manifest.add "\n"
|
||||
# OnlyIfChanged: its mtime is a merge input, so rewriting it every run would
|
||||
# re-fire merge (and, through the decision, every `emit`) on a no-op build.
|
||||
if not fileExists(liveFile) or readFile(liveFile) != manifest:
|
||||
writeFile(liveFile, manifest)
|
||||
b.addTree "do"
|
||||
b.addIdent "nim_nifc"
|
||||
b.withTree "args":
|
||||
b.addStrLit "--icBackendStage:merge"
|
||||
for i in 0 ..< c.nodes.len:
|
||||
if live[i]: inputStr cnifFiles[i]
|
||||
inputStr liveFile
|
||||
outputStr mergeFile
|
||||
b.endTree()
|
||||
|
||||
# emit: render each module's `.c` from its `.c.nif` + the merge decision.
|
||||
for i, node in c.nodes:
|
||||
if not live[i]: continue
|
||||
#
|
||||
# ONE rule for everything, main included. emit is a pure function of a
|
||||
# `.c.nif` and the merge decision — `renderCFromArtifact` filters text and
|
||||
# touches no AST, and the stage loads no module graph at all — so batching it
|
||||
# cannot change what it produces, and measurement agrees: 67 processes and one
|
||||
# process give byte-identical `.c`, in 0.502 s versus 0.041 s. What that buys
|
||||
# is not the cold build (where 0.5 s serial is ~0.05 s across cores) but the
|
||||
# fire-all: every `emit` re-fires whenever `merge` rewrites the decision, which
|
||||
# is every edit that reaches the backend. That now costs one process start.
|
||||
#
|
||||
# `-d:icBatchSize:N` still splits it, for A/B-ing against the fan-out.
|
||||
for batch in emitBatches(c, live, batches):
|
||||
b.addTree "do"
|
||||
b.addIdent "nim_nifc"
|
||||
b.withTree "args":
|
||||
b.addStrLit "--icBackendStage:emit"
|
||||
b.addStrLit "--icBackendModule:" & node.files[0].modname
|
||||
# Inputs: this module's OWN `.c.nif` and the global merge decision. emit also
|
||||
# loads `.t.nif`s at runtime (getCFile/type resolution), but those are depth 1
|
||||
# and emit is past the merge barrier, so they always exist — no need to list
|
||||
# them. (emit still re-fires for every module whenever `merge` rewrites the
|
||||
# decision file; making that incremental is a separate concern.)
|
||||
inputStr cnifFiles[i]
|
||||
b.addStrLit "--icBackendModules:" & suffixList(batch)
|
||||
# Inputs: each member's OWN `.c.nif` and the global merge decision. emit
|
||||
# reads nothing else — it derives its output paths rather than loading a
|
||||
# module graph. (It still re-fires for every module whenever `merge` rewrites
|
||||
# the decision file; making that incremental is a separate concern — though
|
||||
# batching is what makes the re-fire cheap.)
|
||||
for idx in batch:
|
||||
inputStr cnifFiles[idx]
|
||||
inputStr mergeFile
|
||||
outputStr cFiles[i]
|
||||
for idx in batch:
|
||||
outputStr cFiles[idx]
|
||||
# The freshness proof for this rule; see nifbackend.generateEmitStage. The
|
||||
# `.c` alone cannot serve: it is written OnlyIfChanged, so a rule that ran
|
||||
# and produced identical bytes looks exactly like a rule that never ran.
|
||||
outputStr cFiles[idx] & ".stamp"
|
||||
b.endTree()
|
||||
|
||||
# link: compile + link every emitted `.c` in one process.
|
||||
@@ -1220,12 +1656,62 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string
|
||||
# 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]
|
||||
if live[i]:
|
||||
inputStr cFiles[i]
|
||||
inputStr cFiles[i] & BackendActionsExt
|
||||
inputStr argsFile
|
||||
outputStr exeFile
|
||||
b.endTree()
|
||||
|
||||
b.endTree() # stmts
|
||||
|
||||
proc deriveFromSemDeps(c: var DepContext): bool =
|
||||
## Fold every already-compiled module's `.s.deps` sidecar (its REAL post-sem
|
||||
## imports, macro-generated ones included) back into the graph. Returns true
|
||||
## if anything new was added.
|
||||
##
|
||||
## Run BEFORE the first nifmake pass as well as after a failure. The static
|
||||
## scanner cannot see `parseStmt("import dyn")`, so on the run that first hits
|
||||
## it the frontend fails, this recovers the node, and the retry succeeds. But
|
||||
## the graph is rebuilt from scratch on every `nim ic`, so on the NEXT run the
|
||||
## frontend succeeds on round one — with `dyn` absent from the graph again,
|
||||
## hence with no nifler/`nim m` rule of its own and no edge into its importer.
|
||||
## Editing `dyn.nim` then changed nothing at all: the build silently reused the
|
||||
## `.s.bif` from the run that discovered it. Seeding from the sidecars makes
|
||||
## the discovery stick across runs.
|
||||
##
|
||||
## The edges are recorded SPECULATIVELY: a sidecar says what the module
|
||||
## imported the last time it was semmed, which is a statement about the past.
|
||||
## Flip a `when`, or delete an `import`, and a module that is no longer reached
|
||||
## would otherwise linger in the graph forever (and fail to build, if what it
|
||||
## imports is gone). Marking the edge speculative lets `pruneDeadSpeculative`
|
||||
## drop such a leftover, while a genuinely-needed macro import — which compiles
|
||||
## fine — stays.
|
||||
result = false
|
||||
inc c.speculating
|
||||
defer: dec c.speculating
|
||||
let n0 = c.nodes.len # snapshot: new nodes are traversed as they're added
|
||||
for ni in 0 ..< n0:
|
||||
for p in readSemDeps(c, c.nodes[ni].files[0]):
|
||||
let pair = c.toPair(p)
|
||||
var idx = c.processedModules.getOrDefault(pair.modname, -1)
|
||||
if idx == -1:
|
||||
if not fileExists(pair.nimFile): continue
|
||||
let newNode = Node(files: @[pair], id: c.nodes.len)
|
||||
if c.systemNodeId >= 0:
|
||||
newNode.deps.add c.systemNodeId
|
||||
if getsImplicitImports(c, pair.nimFile):
|
||||
for impId in c.implicitNodeIds:
|
||||
if impId != newNode.id: newNode.deps.add impId
|
||||
c.processedModules[pair.modname] = newNode.id
|
||||
c.nodes.add newNode
|
||||
idx = newNode.id
|
||||
traverseDeps(c, pair, newNode)
|
||||
result = true
|
||||
if idx != ni and idx notin c.nodes[ni].deps:
|
||||
addDepEdge(c, c.nodes[ni], idx)
|
||||
result = true
|
||||
|
||||
proc commandIc*(conf: ConfigRef; frontendOnly = false) =
|
||||
## Main entry point for `nim ic`. With `frontendOnly` (used by `nim track` for
|
||||
## IDE queries) it runs only Phase 1 — the incremental nifler + `nim m`
|
||||
@@ -1323,6 +1809,17 @@ proc commandIc*(conf: ConfigRef; frontendOnly = false) =
|
||||
# Process dependencies
|
||||
traverseDeps(c, rootPair, rootNode)
|
||||
|
||||
# Re-apply what earlier runs discovered post-sem (macro-generated imports),
|
||||
# so those modules keep their rules on a warm build instead of vanishing from
|
||||
# the graph until the next failure. No-op on a cold cache. Runs BEFORE the
|
||||
# prune so a sidecar entry that has since gone stale is prunable too.
|
||||
discard deriveFromSemDeps(c)
|
||||
|
||||
# Modules that only a `when` the scanner cannot decide pulls in, and that
|
||||
# import something not installed, are dead in this configuration; scheduling
|
||||
# them would fail the build over code the classic compiler never reads.
|
||||
pruneDeadSpeculative(c)
|
||||
|
||||
# Discovery via `.s.deps`: imports GENERATED by macros (chronicles builds
|
||||
# `import chronicles/textlines` via parseStmt from the chronicles_sinks
|
||||
# define) are invisible to the static scanner. Each `nim m` records the
|
||||
@@ -1393,28 +1890,20 @@ proc commandIc*(conf: ConfigRef; frontendOnly = false) =
|
||||
var discovered = false
|
||||
inc rounds
|
||||
if rounds <= 20:
|
||||
let n0 = c.nodes.len # snapshot: new nodes are traversed as they're added
|
||||
for ni in 0 ..< n0:
|
||||
for p in readSemDeps(c, c.nodes[ni].files[0]):
|
||||
let pair = c.toPair(p)
|
||||
var idx = c.processedModules.getOrDefault(pair.modname, -1)
|
||||
if idx == -1:
|
||||
let newNode = Node(files: @[pair], id: c.nodes.len)
|
||||
if c.systemNodeId >= 0:
|
||||
newNode.deps.add c.systemNodeId
|
||||
if getsImplicitImports(c, pair.nimFile):
|
||||
for impId in c.implicitNodeIds:
|
||||
if impId != newNode.id: newNode.deps.add impId
|
||||
c.processedModules[pair.modname] = newNode.id
|
||||
c.nodes.add newNode
|
||||
idx = newNode.id
|
||||
traverseDeps(c, pair, newNode)
|
||||
discovered = true
|
||||
if idx != ni and idx notin c.nodes[ni].deps:
|
||||
c.nodes[ni].deps.add idx
|
||||
discovered = true
|
||||
discovered = deriveFromSemDeps(c)
|
||||
if not discovered:
|
||||
rawMessage(conf, errGenerated, "nifmake failed with exit code: " & $exitCode)
|
||||
# The children have already printed the real diagnostics. Adding an
|
||||
# `Error:` line of our own here made a build-system status the LAST error
|
||||
# in the stream, hiding the compiler's own message from anything that
|
||||
# reads the final error (testament's `errormsg:`, editors, CI log
|
||||
# scrapers) — every `reject`-style test under `nim ic` reported
|
||||
# "nifmake failed with exit code: 1" instead of what the compiler said.
|
||||
# The non-zero exit is what signals failure; this line is context.
|
||||
rawMessage(conf, hintExecuting,
|
||||
"nifmake reported failures (exit code " & $exitCode & ")")
|
||||
# Fail the run without printing an `Error:` of our own (see above): the
|
||||
# exit code is derived from `errorCounter`.
|
||||
inc conf.errorCounter
|
||||
break
|
||||
|
||||
# Phase 2 — backend (whole-program `nim nifc`), run once over the now-final
|
||||
@@ -1429,6 +1918,8 @@ proc commandIc*(conf: ConfigRef; frontendOnly = false) =
|
||||
rawMessage(conf, hintExecuting, cmd)
|
||||
let exitCode = execShellCmd(cmd)
|
||||
if exitCode != 0:
|
||||
rawMessage(conf, errGenerated, "nifmake (backend) failed with exit code: " & $exitCode)
|
||||
rawMessage(conf, hintExecuting,
|
||||
"nifmake reported backend failures (exit code " & $exitCode & ")")
|
||||
inc conf.errorCounter
|
||||
else:
|
||||
rawMessage(conf, errGenerated, "nim ic not available in bootstrap build")
|
||||
|
||||
@@ -454,7 +454,7 @@ proc gen(c: var Con; n: PNode) =
|
||||
of nkPragmaBlock: gen(c, n.lastSon)
|
||||
of nkDiscardStmt, nkObjDownConv, nkObjUpConv, nkStringToCString, nkCStringToString:
|
||||
gen(c, n[0])
|
||||
of nkConv, nkExprColonExpr, nkExprEqExpr, nkCast, PathKinds1:
|
||||
of nkConv, nkExprColonExpr, nkExprEqExpr, PathKinds1:
|
||||
gen(c, n[1])
|
||||
of nkVarSection, nkLetSection: genVarSection(c, n)
|
||||
of nkDefer: raiseAssert "dfa construction pass requires the elimination of 'defer'"
|
||||
|
||||
@@ -14,7 +14,7 @@ proc genEnumToStrProc*(t: PType; info: TLineInfo; g: ModuleGraph; idgen: IdGener
|
||||
let res = newSym(skResult, getIdent(g.cache, "result"), idgen, result, info)
|
||||
res.typ = getSysType(g, info, tyString)
|
||||
|
||||
result.typ = newType(tyProc, idgen, t.owner)
|
||||
result.typ = newType(tyProc, idgen, result)
|
||||
result.typ.n = newNodeI(nkFormalParams, info)
|
||||
rawAddSon(result.typ, res.typ)
|
||||
result.typ.n.add newNodeI(nkEffectList, info)
|
||||
|
||||
@@ -14,11 +14,77 @@
|
||||
import ".." / [ast, modulegraphs, trees, extccomp, btrees,
|
||||
msgs, lineinfos, pathutils, options, cgmeth]
|
||||
|
||||
import std/tables
|
||||
import std/[tables, os, strutils, syncio]
|
||||
|
||||
when defined(nimPreviewSlimSystem):
|
||||
import std/assertions
|
||||
|
||||
const BackendActionsExt* = ".cflags"
|
||||
## Sidecar written by a module's `cg` stage next to its `.c`, carrying the C
|
||||
## compile/link directives that module's `{.passL.}`/`{.compile.}`/… pragmas
|
||||
## recorded. See `writeBackendActions`.
|
||||
|
||||
proc writeBackendActions*(g: ModuleGraph; module: PSym; list: PNode;
|
||||
outfile: string) =
|
||||
## Serialize the backend-relevant replay actions of ONE module to `outfile`,
|
||||
## one tab-separated action per line.
|
||||
##
|
||||
## The `link` stage used to recover these by loading the whole import closure
|
||||
## as `PrecompiledModule`s and re-running `replayBackendActions` over each —
|
||||
## a 3.7s whole-program graph load, per link, purely to recover a handful of
|
||||
## strings and the modules' `.c` paths. The producing `cg` process already has
|
||||
## them in hand, so it writes them down instead and `link` reads them back
|
||||
## (`applyBackendActions`). Written unconditionally, even when empty: it is a
|
||||
## declared nifmake output of the `cg` rule, and a missing output re-fires the
|
||||
## rule for ever.
|
||||
##
|
||||
## `localpassc` needs the module's own source path, which only the writer can
|
||||
## resolve, so it is baked in here as a third field.
|
||||
var content = ""
|
||||
if list != nil:
|
||||
for n in list:
|
||||
if n.kind == nkReplayAction and n.len >= 2 and
|
||||
n[0].kind == nkStrLit and n[1].kind == nkStrLit:
|
||||
case n[0].strVal
|
||||
of "compile":
|
||||
if n.len == 4 and n[2].kind == nkStrLit and n[3].kind == nkStrLit:
|
||||
content.add "compile\t" & n[1].strVal & "\t" & n[2].strVal & "\t" &
|
||||
n[3].strVal & "\n"
|
||||
of "link", "passl", "passc", "cppdefine":
|
||||
content.add n[0].strVal & "\t" & n[1].strVal & "\n"
|
||||
of "localpassc":
|
||||
content.add "localpassc\t" & n[1].strVal & "\t" &
|
||||
toFullPathConsiderDirty(g.config, module.info.fileIndex).string & "\n"
|
||||
else: discard
|
||||
writeFile(outfile, content)
|
||||
|
||||
proc applyBackendActions*(g: ModuleGraph; infile: string) =
|
||||
## Apply one module's recorded C directives (see `writeBackendActions`). The
|
||||
## `link` stage's replacement for loading that module and replaying its AST.
|
||||
if not fileExists(infile): return
|
||||
for line in lines(infile):
|
||||
if line.len == 0: continue
|
||||
let f = line.split('\t')
|
||||
case f[0]
|
||||
of "compile":
|
||||
if f.len == 4:
|
||||
let cname = AbsoluteFile f[1]
|
||||
var cf = Cfile(nimname: splitFile(cname).name, cname: cname,
|
||||
obj: AbsoluteFile f[2],
|
||||
flags: {CfileFlag.External}, customArgs: f[3])
|
||||
extccomp.addExternalFileToCompile(g.config, cf)
|
||||
of "link":
|
||||
if f.len == 2: extccomp.addExternalFileToLink(g.config, AbsoluteFile f[1])
|
||||
of "passl":
|
||||
if f.len == 2: extccomp.addLinkOption(g.config, f[1])
|
||||
of "passc":
|
||||
if f.len == 2: extccomp.addCompileOption(g.config, f[1])
|
||||
of "localpassc":
|
||||
if f.len == 3: extccomp.addLocalCompileOption(g.config, f[1], AbsoluteFile f[2])
|
||||
of "cppdefine":
|
||||
if f.len == 2: options.cppDefine(g.config, f[1])
|
||||
else: discard
|
||||
|
||||
proc replayStateChanges*(module: PSym; g: ModuleGraph; list: PNode) =
|
||||
## `list` is an `nkStmtList` of `nkReplayAction` nodes (macro-cache puts/incs/
|
||||
## adds/incls and a few pragmas) recorded for `module`. Under the NIF backend a
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
## would misresolve.
|
||||
|
||||
import options, commands, lineinfos, pathutils, msgs
|
||||
import std/[algorithm, os, sets, osproc, times, streams, syncio]
|
||||
import std/[algorithm, os, sets, osproc, times, streams, syncio, strutils]
|
||||
import "../dist/nimony/src/lib" / [nifbuilder, nifcoreparse]
|
||||
|
||||
const
|
||||
@@ -269,11 +269,26 @@ proc ensureIcConfig*(conf: ConfigRef) =
|
||||
# verbatim: all `-`-prefixed switches first (in encounter order), then the
|
||||
# non-switch project token(s). The producer re-reads `nim.cfg` itself.
|
||||
var pargs = @["icconfig", "--icConfigOut:" & outPath]
|
||||
# The command token is dropped below, so `nim cpp --ic:on` would hand the
|
||||
# producer a C-backend config: name the backend explicitly. (`nim ic
|
||||
# --backend:cpp` already carries the switch; the duplicate is harmless.)
|
||||
if conf.backend != backendInvalid:
|
||||
pargs.add "--backend:" & $conf.backend
|
||||
var rest: seq[string] = @[]
|
||||
var droppedCmd = false
|
||||
for a in commandLineParams():
|
||||
if a.len == 0: continue
|
||||
if a[0] == '-':
|
||||
# `--run`/`-r` must not reach the producer: it only serialises the
|
||||
# resolved config, has no output binary, and `nim.nim`'s run step asserts
|
||||
# on the empty `outFile` (`nim cpp --ic:on -r foo.nim`).
|
||||
var name = ""
|
||||
var i = 1
|
||||
if i < a.len and a[i] == '-': inc i
|
||||
while i < a.len and a[i] notin {':', '='}:
|
||||
name.add a[i]
|
||||
inc i
|
||||
if normalize(name) in ["r", "run"]: continue
|
||||
pargs.add a
|
||||
elif not droppedCmd:
|
||||
droppedCmd = true # drop the original command token (`ic`/`track`)
|
||||
|
||||
114
compiler/icprof.nim
Normal file
114
compiler/icprof.nim
Normal file
@@ -0,0 +1,114 @@
|
||||
#
|
||||
#
|
||||
# The Nim Compiler
|
||||
# (c) Copyright 2026 Andreas Rumpf
|
||||
#
|
||||
# See the file "copying.txt", included in this
|
||||
# distribution, for details about the copyright.
|
||||
#
|
||||
|
||||
## Opt-in instrumentation for the IC backend, enabled with `-d:icBNodeProf`.
|
||||
## Off, every template below is `discard` and nothing is linked in.
|
||||
##
|
||||
## It lives in its own module with NO compiler imports so that any stage can
|
||||
## use it without creating a cycle — `bnode` needs it for the accessors,
|
||||
## `nifbackend` for the stage phases, `cgen` for what happens per routine.
|
||||
##
|
||||
## Each backend process appends ONE line to `$NIM_IC_BNODE_PROF` at exit (or to
|
||||
## stderr when that is unset), because a `--ic:on` build fans out a process per
|
||||
## module per stage and interleaved writes would tear. Use `-d:icNoParallel`
|
||||
## when the numbers need to be attributable to a particular module.
|
||||
##
|
||||
## Counts are for volume, timings for cost, and the two answer different
|
||||
## questions: the accessors turned out to be 700k calls worth 8ms, while `info`
|
||||
## was 259k calls worth 1.36s. Neither number alone would have found that.
|
||||
|
||||
when defined(icBNodeProf):
|
||||
import std / [envvars, exitprocs, syncio, monotimes]
|
||||
from std / times import inNanoseconds
|
||||
|
||||
type
|
||||
ProfSlot* = enum
|
||||
pKind, pTagKindHit, pTagKindMiss, pAstChildren, pSkip, pSon, pLen,
|
||||
pLastSon, pIterYield, pSym, pTyp, pTypTagLit, pOrigin, pNilType,
|
||||
pGenBodyCalls, pInfo, pIfaceExported, pIfaceHidden, pIfaceModules,
|
||||
pTopNodes, pExportSyms, pPeekKind, pPeekFallback, pPeekLoaded,
|
||||
pTopToolingSkip
|
||||
TimeSlot* = enum
|
||||
tLoadClosure, tModuleId, tBifLoad, tPosIndex, tTopLevel, tInterfTables,
|
||||
tTransform, tHandOff, tGenBody, tAnalyses,
|
||||
tSym, tTyp, tInfo, tOrigin, tExportBranch, tResolveSym, tEnumFields,
|
||||
# Coarse phases, added to find where a backend process spends the time
|
||||
# that none of the slots above account for. `tStage` is the whole stage
|
||||
# body, so `Process - tStage` is everything before it: exec, the Nim
|
||||
# runtime, config replay, `registerNifSuffix`/graph setup.
|
||||
tStage,
|
||||
tLowerOwned, tLowerHooks, tLowerWrite,
|
||||
tCgGen, tCgInit, tCgFinish, tCgWrite,
|
||||
tMergeStage, tEmitRender, tLinkStage,
|
||||
# `nim m` (the frontend): the sem pass as a whole, and writing the module's
|
||||
# `.s.bif`. `Stage - WriteNif - <the loading slots>` is then sem proper.
|
||||
tWriteNif,
|
||||
# `processTopLevel`'s branches: which part of a module HEADER costs what.
|
||||
tTopReplay, tTopLogOps, tTopOffers, tTopStmts
|
||||
|
||||
let procStart = getMonoTime()
|
||||
## Set when this module initialises, i.e. essentially at process start, so
|
||||
## the dump can report total process wall time and the startup share can be
|
||||
## derived as `Process - Stage`.
|
||||
|
||||
var profStageName* = "frontend"
|
||||
## Which invocation this is: the backend stage name, or "frontend" for a
|
||||
## `nim m` process, which arms the profiler through ast2nif but never enters
|
||||
## a backend stage. Without it the `Process - Stage` startup figure is
|
||||
## meaningless — 204 frontend processes' whole runtime lands in it.
|
||||
|
||||
var profCounts: array[ProfSlot, int]
|
||||
var profNanos: array[TimeSlot, int64]
|
||||
var profStart: array[TimeSlot, MonoTime]
|
||||
var profArmed = false
|
||||
|
||||
proc profDump() =
|
||||
var line = "BNODEPROF stage=" & profStageName
|
||||
for s in ProfSlot: line.add " " & ($s)[1..^1] & "=" & $profCounts[s]
|
||||
for s in TimeSlot: line.add " " & ($s)[1..^1] & "ms=" & $(profNanos[s] div 1_000_000)
|
||||
line.add " Processms=" & $((getMonoTime() - procStart).inNanoseconds div 1_000_000)
|
||||
let f = getEnv("NIM_IC_BNODE_PROF")
|
||||
if f.len > 0:
|
||||
let h = open(f, fmAppend)
|
||||
h.writeLine line
|
||||
h.close()
|
||||
else:
|
||||
stderr.writeLine line
|
||||
|
||||
template armProf() =
|
||||
if not profArmed:
|
||||
profArmed = true
|
||||
addExitProc profDump
|
||||
|
||||
template prof*(s: ProfSlot; n = 1) =
|
||||
armProf()
|
||||
inc profCounts[s], n
|
||||
template icProfStart*(s: TimeSlot) =
|
||||
armProf()
|
||||
profStart[s] = getMonoTime()
|
||||
template icProfStop*(s: TimeSlot) =
|
||||
profNanos[s] += (getMonoTime() - profStart[s]).inNanoseconds
|
||||
|
||||
template timed*(s: TimeSlot; body: untyped) =
|
||||
## Leaf timing. NOT re-entrant, and the phase slots are not disjoint —
|
||||
## `tTransform` contains body materialization, `tTyp` reaches `tSym`. Read
|
||||
## them as nested, not additive.
|
||||
##
|
||||
## Arms the dump like `prof`/`icProfStart` do. It did not, and so a process
|
||||
## whose ONLY instrumentation is a `timed` never reported at all: the
|
||||
## `merge`, `emit` and `link` stages were silently absent from every profile.
|
||||
armProf()
|
||||
let t0 = getMonoTime()
|
||||
body
|
||||
profNanos[s] += (getMonoTime() - t0).inNanoseconds
|
||||
else:
|
||||
template prof*(s: untyped; n = 1) = discard
|
||||
template icProfStart*(s: untyped) = discard
|
||||
template icProfStop*(s: untyped) = discard
|
||||
template timed*(s: untyped; body: untyped) = body
|
||||
@@ -423,6 +423,20 @@ proc genDefaultCall(t: PType; c: Con; info: TLineInfo): PNode =
|
||||
result.add(newSymNode(createMagic(c.graph, c.idgen, "default", mDefault)))
|
||||
result.typ = t
|
||||
|
||||
proc stabilizeBracketIndex(n: PNode; c: var Con; body: var PNode): PNode =
|
||||
## Evaluate a side-effecting index once and return the stable access.
|
||||
doAssert n.kind == nkBracketExpr and not isAtom(n[1])
|
||||
let temp = newSym(skLet, getIdent(c.graph.cache, "bracketTmp"), c.idgen,
|
||||
c.owner, n[1].info)
|
||||
temp.typ = n[1].typ
|
||||
let tempAsNode = newSymNode(temp)
|
||||
body.add newTree(nkLetSection, n[1].info,
|
||||
newTree(nkIdentDefs, tempAsNode,
|
||||
newNodeI(nkEmpty, tempAsNode.info), n[1]))
|
||||
result = copyNode(n)
|
||||
result.add n[0]
|
||||
result.add tempAsNode
|
||||
|
||||
proc destructiveMoveVar(n: PNode; c: var Con; s: var Scope): PNode =
|
||||
# generate: (let tmp = v; reset(v); tmp)
|
||||
if (not hasDestructor(c, n.typ)) and c.inEnsureMove == 0:
|
||||
@@ -434,6 +448,10 @@ proc destructiveMoveVar(n: PNode; c: var Con; s: var Scope): PNode =
|
||||
else:
|
||||
result = newNodeIT(nkStmtListExpr, n.info, n.typ)
|
||||
|
||||
var n = n
|
||||
if n.kind == nkBracketExpr and not isAtom(n[1]):
|
||||
n = stabilizeBracketIndex(n, c, result)
|
||||
|
||||
var temp = newSym(skLet, getIdent(c.graph.cache, "blitTmp"), c.idgen, c.owner, n.info)
|
||||
temp.typ = n.typ
|
||||
var v = newNodeI(nkLetSection, n.info)
|
||||
@@ -1119,6 +1137,11 @@ proc p(n: PNode; c: var Con; s: var Scope; mode: ProcessMode; tmpFlags = {sfSing
|
||||
result[i] = n[i]
|
||||
of nkGotoState, nkState, nkAsmStmt:
|
||||
result = n
|
||||
of nkReplayAction:
|
||||
# A `.rod`/NIF replay record. It only ever appears in a NIF-loaded
|
||||
# module's TOP-LEVEL statements (the loader prepends the `(replay ...)`
|
||||
# entries there); cgen discards it, so pass it through untouched.
|
||||
result = n
|
||||
else:
|
||||
result = nil
|
||||
internalError(c.graph.config, n.info, "cannot inject destructors to node kind: " & $n.kind)
|
||||
@@ -1150,24 +1173,11 @@ proc sameLocation*(a, b: PNode): bool =
|
||||
else: false
|
||||
|
||||
proc genFieldAccessSideEffects(c: var Con; s: var Scope; dest, ri: PNode; flags: set[MoveOrCopyFlag] = {}): PNode =
|
||||
# with side effects
|
||||
var temp = newSym(skLet, getIdent(c.graph.cache, "bracketTmp"), c.idgen, c.owner, ri[1].info)
|
||||
temp.typ = ri[1].typ
|
||||
var v = newNodeI(nkLetSection, ri[1].info)
|
||||
let tempAsNode = newSymNode(temp)
|
||||
|
||||
var vpart = newNodeI(nkIdentDefs, tempAsNode.info, 3)
|
||||
vpart[0] = tempAsNode
|
||||
vpart[1] = newNodeI(nkEmpty, tempAsNode.info)
|
||||
vpart[2] = ri[1]
|
||||
v.add(vpart)
|
||||
|
||||
var newAccess = copyNode(ri)
|
||||
newAccess.add ri[0]
|
||||
newAccess.add tempAsNode
|
||||
|
||||
var snk = c.genSink(s, dest, newAccess, flags)
|
||||
result = newTree(nkStmtList, v, snk, c.genWasMoved(newAccess))
|
||||
result = newNodeI(nkStmtList, ri.info)
|
||||
let newAccess = stabilizeBracketIndex(ri, c, result)
|
||||
let snk = c.genSink(s, dest, newAccess, flags)
|
||||
result.add snk
|
||||
result.add c.genWasMoved(newAccess)
|
||||
|
||||
proc ownsData(c: var Con; s: var Scope; orig: PNode; flags: set[MoveOrCopyFlag]): PNode =
|
||||
var n = orig
|
||||
|
||||
@@ -1450,6 +1450,20 @@ proc genCheckedFieldOp(p: PProc, n: PNode, addrTyp: PType, r: var TCompRes) =
|
||||
r.res = "$1.$2" % [tmp, field.loc.snippet]
|
||||
r.kind = resExpr
|
||||
|
||||
proc isVarOpenArrayParam(n: PNode): bool =
|
||||
## True if `n` resolves to a `var openArray` parameter. The JS backend
|
||||
## represents such parameters as a `{base, off, len}` slice view so that
|
||||
## writes through a `toOpenArray` view reach the caller's storage (bug #15952).
|
||||
var it = n
|
||||
while true:
|
||||
case it.kind
|
||||
of nkHiddenDeref, nkDerefExpr, nkHiddenAddr, nkAddr: it = it[0]
|
||||
of nkHiddenStdConv, nkConv, nkObjDownConv, nkObjUpConv: it = it[1]
|
||||
else: break
|
||||
result = it.kind == nkSym and it.sym.kind == skParam and
|
||||
it.sym.typ != nil and it.sym.typ.kind == tyVar and
|
||||
it.sym.typ.len > 0 and it.sym.typ[0].kind == tyOpenArray
|
||||
|
||||
proc genArrayAddr(p: PProc, n: PNode, r: var TCompRes) =
|
||||
var
|
||||
a, b: TCompRes = default(TCompRes)
|
||||
@@ -1458,6 +1472,19 @@ proc genArrayAddr(p: PProc, n: PNode, r: var TCompRes) =
|
||||
let m = if n.kind == nkHiddenAddr: n[0] else: n
|
||||
gen(p, m[0], a)
|
||||
gen(p, m[1], b)
|
||||
if isVarOpenArrayParam(m[0]):
|
||||
# `var openArray` param is a `{base, off, len}` view; index the base with
|
||||
# the offset applied. `m[0]` is a plain param name, safe to reference
|
||||
# repeatedly (no side effects, so no temp needed).
|
||||
let pn = a.rdLoc
|
||||
r.address = "($1).base" % [pn]
|
||||
if optBoundsCheck in p.options:
|
||||
useMagic(p, "chckIndx")
|
||||
r.res = "($1).off + chckIndx($2, 0, ($1).len - 1)" % [pn, b.rdLoc]
|
||||
else:
|
||||
r.res = "($1).off + ($2)" % [pn, b.rdLoc]
|
||||
r.kind = resExpr
|
||||
return
|
||||
#internalAssert p.config, a.typ != etyBaseIndex and b.typ != etyBaseIndex
|
||||
let (x, tmp) = maybeMakeTemp(p, m[0], a)
|
||||
r.address = x
|
||||
@@ -1726,8 +1753,47 @@ proc genArgNoParam(p: PProc, n: PNode, r: var TCompRes) =
|
||||
else:
|
||||
r.res.add(a.res)
|
||||
|
||||
proc genVarOpenArrayArg(p: PProc, n: PNode, r: var TCompRes) =
|
||||
## Emit a `{base, off, len}` slice view for an argument to a `var openArray`
|
||||
## parameter (bug #15952). The view always aliases the base storage, so writes
|
||||
## through the callee's `openArray` reach the caller's array/seq/typed array.
|
||||
var b, lo, hi, v: TCompRes = default(TCompRes)
|
||||
# the argument reaches codegen as `addr(toOpenArray(x, lo, hi))` (possibly
|
||||
# under conversions); unwrap to the actual `toOpenArray` call.
|
||||
var sl = n
|
||||
while true:
|
||||
case sl.kind
|
||||
of nkHiddenAddr, nkAddr, nkHiddenDeref, nkDerefExpr: sl = sl[0]
|
||||
of nkHiddenStdConv, nkConv, nkObjDownConv, nkObjUpConv: sl = sl[1]
|
||||
else: break
|
||||
if sl.kind in nkCallKinds and getMagic(sl) == mSlice:
|
||||
gen(p, sl[1], b)
|
||||
gen(p, sl[2], lo)
|
||||
gen(p, sl[3], hi)
|
||||
if isVarOpenArrayParam(sl[1]):
|
||||
# slicing a `var openArray` view: rebase onto the same underlying storage
|
||||
r.res = "{base: ($1).base, off: ($1).off + $2, len: $3 - $2 + 1}" % [
|
||||
b.rdLoc, lo.rdLoc, hi.rdLoc]
|
||||
else:
|
||||
r.res = "{base: $1, off: $2, len: $3 - $2 + 1}" % [
|
||||
b.rdLoc, lo.rdLoc, hi.rdLoc]
|
||||
elif isVarOpenArrayParam(sl):
|
||||
# already a view from another `var openArray` param: forward it unchanged
|
||||
gen(p, sl, b)
|
||||
r.res = b.rdLoc
|
||||
else:
|
||||
# a whole array/seq/typed-array value: wrap with a zero offset
|
||||
gen(p, n, v)
|
||||
r.res = "{base: $1, off: 0, len: ($1).length}" % [v.rdLoc]
|
||||
r.kind = resExpr
|
||||
|
||||
proc genArg(p: PProc, n: PNode, param: PSym, r: var TCompRes; emitted: ptr int = nil) =
|
||||
var a: TCompRes = default(TCompRes)
|
||||
if param.typ != nil and param.typ.kind == tyVar and param.typ[0].kind == tyOpenArray:
|
||||
# `var openArray` params are passed as a `{base, off, len}` slice view.
|
||||
genVarOpenArrayArg(p, n, a)
|
||||
r.res.add(a.rdLoc)
|
||||
return
|
||||
gen(p, n, a)
|
||||
if skipTypes(param.typ, abstractVar).kind in {tyOpenArray, tyVarargs} and
|
||||
a.typ == etyBaseIndex:
|
||||
@@ -1737,6 +1803,13 @@ proc genArg(p: PProc, n: PNode, param: PSym, r: var TCompRes; emitted: ptr int =
|
||||
r.res.add(", ")
|
||||
r.res.add(a.res)
|
||||
if emitted != nil: inc emitted[]
|
||||
elif skipTypes(param.typ, abstractVar).kind == tyOpenArray and
|
||||
isVarOpenArrayParam(n):
|
||||
# a `var openArray` view passed to a read-only `openArray` param: materialize
|
||||
# a snapshot so the callee sees a plain array.
|
||||
var w: TCompRes = default(TCompRes)
|
||||
gen(p, n, w)
|
||||
r.res.add("(($1).base).slice(($1).off, ($1).off + ($1).len)" % [w.rdLoc])
|
||||
elif n.typ.kind in {tyVar, tyPtr, tyRef, tyLent, tyOwned} and
|
||||
n.kind in nkCallKinds and mapType(param.typ) == etyBaseIndex:
|
||||
# this fixes bug #5608:
|
||||
@@ -2371,13 +2444,21 @@ proc genMagic(p: PProc, n: PNode, r: var TCompRes) =
|
||||
useMagic(p, "nimCopy")
|
||||
r.res = "nimCopy(null, $1, $2)" % [x.rdLoc, genTypeInfo(p, n.typ)]
|
||||
of mOpenArrayToSeq:
|
||||
genCall(p, n, r)
|
||||
if isVarOpenArrayParam(n[1]):
|
||||
var x: TCompRes = default(TCompRes)
|
||||
gen(p, n[1], x)
|
||||
r.res = "(($1).base).slice(($1).off, ($1).off + ($1).len)" % [x.rdLoc]
|
||||
r.kind = resExpr
|
||||
else:
|
||||
genCall(p, n, r)
|
||||
of mDestroy, mTrace: discard "ignore calls to the default destructor"
|
||||
of mOrd: genOrd(p, n, r)
|
||||
of mLengthStr, mLengthSeq, mLengthOpenArray, mLengthArray:
|
||||
var x: TCompRes = default(TCompRes)
|
||||
gen(p, n[1], x)
|
||||
if skipTypes(n[1].typ, abstractInst).kind == tyCstring:
|
||||
if isVarOpenArrayParam(n[1]):
|
||||
r.res = "($1).len" % [x.rdLoc]
|
||||
elif skipTypes(n[1].typ, abstractInst).kind == tyCstring:
|
||||
let (a, tmp) = maybeMakeTemp(p, n[1], x)
|
||||
r.res = "(($1) == null ? 0 : ($2).length)" % [a, tmp]
|
||||
else:
|
||||
@@ -2386,7 +2467,9 @@ proc genMagic(p: PProc, n: PNode, r: var TCompRes) =
|
||||
of mHigh:
|
||||
var x: TCompRes = default(TCompRes)
|
||||
gen(p, n[1], x)
|
||||
if skipTypes(n[1].typ, abstractInst).kind == tyCstring:
|
||||
if isVarOpenArrayParam(n[1]):
|
||||
r.res = "($1).len - 1" % [x.rdLoc]
|
||||
elif skipTypes(n[1].typ, abstractInst).kind == tyCstring:
|
||||
let (a, tmp) = maybeMakeTemp(p, n[1], x)
|
||||
r.res = "(($1) == null ? -1 : ($2).length - 1)" % [a, tmp]
|
||||
else:
|
||||
@@ -2469,11 +2552,24 @@ proc genMagic(p: PProc, n: PNode, r: var TCompRes) =
|
||||
genCall(p, n, r)
|
||||
of mSlice:
|
||||
# arr.slice([begin[, end]]): 'end' is exclusive
|
||||
# Fixed homogeneous numeric arrays lower to JS typed arrays; `slice`
|
||||
# copies, which silently breaks `var openArray` write-through (bug #15952).
|
||||
# `subarray` returns a live shared-buffer view with the same
|
||||
# exclusive-end signature, so use it there; keep `slice` for seqs/strings.
|
||||
var x, y, z: TCompRes = default(TCompRes)
|
||||
gen(p, n[1], x)
|
||||
gen(p, n[2], y)
|
||||
gen(p, n[3], z)
|
||||
r.res = "($1.slice($2, $3 + 1))" % [x.rdLoc, y.rdLoc, z.rdLoc]
|
||||
if isVarOpenArrayParam(n[1]):
|
||||
# re-slicing a `var openArray` view: materialize from the view's base/offset
|
||||
r.res = "(($1).base).slice(($1).off + $2, ($1).off + $3 + 1)" % [
|
||||
x.rdLoc, y.rdLoc, z.rdLoc]
|
||||
else:
|
||||
let baseTy = skipTypes(n[1].typ, abstractVarRange + {tyLent})
|
||||
if baseTy.kind == tyArray and arrayTypeForElemType(p.config, elemType(baseTy)).len > 0:
|
||||
r.res = "($1.subarray($2, $3 + 1))" % [x.rdLoc, y.rdLoc, z.rdLoc]
|
||||
else:
|
||||
r.res = "($1.slice($2, $3 + 1))" % [x.rdLoc, y.rdLoc, z.rdLoc]
|
||||
r.kind = resExpr
|
||||
of mMove:
|
||||
genMove(p, n, r)
|
||||
|
||||
@@ -675,6 +675,17 @@ proc rawClosureCreation(owner: PSym;
|
||||
if up != nil and upField.typ.skipTypes({tyOwned, tyRef, tyPtr}) == up.typ.skipTypes({tyOwned, tyRef, tyPtr}):
|
||||
result.add(newAsgnStmt(rawIndirectAccess(env, upField, env.info),
|
||||
up, env.info))
|
||||
# That assignment stores a real `ref`, so `injectDestructorCalls` has to
|
||||
# find the up-field type's ops — otherwise it stays a raw pointer store,
|
||||
# the enclosing env's refcount is one too low, and at teardown the two
|
||||
# envs' mutually recursive `=destroy`s each believe they hold the last
|
||||
# reference and recurse until the stack is gone. Whole-program cgen never
|
||||
# noticed: some LATER lifting pass creates this very ref type's ops, and it
|
||||
# runs before any routine's destructor injection. The per-module backend
|
||||
# injects a routine right after lifting it (the `lower` stage), long before
|
||||
# the module's top level is transformed at all (that is `cg`).
|
||||
if up.typ != nil and up.typ.kind == tyRef and up.typ.elementType != nil:
|
||||
createTypeBoundOpsLL(d.graph, up.typ, env.info, d.idgen, owner)
|
||||
#elif oldenv != nil and oldenv.typ == upField.typ:
|
||||
# result.add(newAsgnStmt(rawIndirectAccess(env, upField, env.info),
|
||||
# oldenv, env.info))
|
||||
@@ -732,6 +743,10 @@ proc closureCreationForIter(owner: PSym, iter: PNode;
|
||||
if u != nil and u.typ.skipTypes({tyOwned, tyRef, tyPtr}) == expectedUpTyp:
|
||||
result.add(newAsgnStmt(rawIndirectAccess(vnode, upField, iter.info),
|
||||
u, iter.info))
|
||||
# See the identical call in `rawClosureCreation`: the up-field's ops must
|
||||
# exist by the time this assignment is destructor-injected.
|
||||
if u.typ != nil and u.typ.kind == tyRef and u.typ.elementType != nil:
|
||||
createTypeBoundOpsLL(d.graph, u.typ, iter.info, d.idgen, owner)
|
||||
else:
|
||||
localError(d.graph.config, iter.info, "internal error: cannot create up reference for iter")
|
||||
result.add makeClosure(d.graph, d.idgen, iter.sym, vnode, iter.info)
|
||||
|
||||
@@ -82,7 +82,7 @@ proc lookup(typeMap: ref LayeredIdTableObj, key: ItemId): PType =
|
||||
|
||||
template lookup*(typeMap: ref LayeredIdTableObj, key: PType): PType =
|
||||
## recursively looks up binding of `key` in all parent layers
|
||||
lookup(typeMap, key.itemId)
|
||||
lookup(typeMap, key.bindingId)
|
||||
|
||||
when not useRef:
|
||||
proc lookup(typeMap: LayeredIdTableObj, key: ItemId): PType {.inline.} =
|
||||
@@ -91,11 +91,11 @@ when not useRef:
|
||||
result = lookup(typeMap.nextLayer, key)
|
||||
|
||||
template lookup*(typeMap: LayeredIdTableObj, key: PType): PType =
|
||||
lookup(typeMap, key.itemId)
|
||||
lookup(typeMap, key.bindingId)
|
||||
|
||||
proc put(typeMap: var LayeredIdTable, key: ItemId, value: PType) {.inline.} =
|
||||
typeMap.topLayer[key] = value
|
||||
|
||||
template put*(typeMap: var LayeredIdTable, key, value: PType) =
|
||||
## binds `key` to `value` only in current layer
|
||||
put(typeMap, key.itemId, value)
|
||||
put(typeMap, key.bindingId, value)
|
||||
|
||||
@@ -718,7 +718,7 @@ proc useSeqOrStrOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
|
||||
when defined(icDbg):
|
||||
if t.destructor == nil:
|
||||
echo "MISSING destructor: ", typeToString(t), " kind=", t.kind,
|
||||
" itemId=", t.itemId, " uniqueId=", t.uniqueId, " state=", t.state,
|
||||
" itemId=", t.itemId, " bindingId=", t.bindingId, " state=", t.state,
|
||||
" owner=", (if t.owner != nil: t.owner.name.s else: "nil")
|
||||
doAssert t.destructor != nil
|
||||
body.add destructorCall(c, t.destructor, x)
|
||||
@@ -1233,7 +1233,7 @@ proc symDupPrototype(g: ModuleGraph; typ: PType; owner: PSym; kind: TTypeAttache
|
||||
res.typ = typ
|
||||
src.typ = typ
|
||||
|
||||
result.typ = newType(tyProc, idgen, owner)
|
||||
result.typ = newType(tyProc, idgen, result)
|
||||
result.typ.n = newNodeI(nkFormalParams, info)
|
||||
rawAddSon(result.typ, res.typ)
|
||||
result.typ.n.add newNodeI(nkEffectList, info)
|
||||
@@ -1279,7 +1279,8 @@ proc symPrototype(g: ModuleGraph; typ: PType; owner: PSym; kind: TTypeAttachedOp
|
||||
else:
|
||||
src.typ = typ
|
||||
|
||||
result.typ = newProcType(info, idgen, owner)
|
||||
# the hook OWNS its signature, like any routine sem'd from source
|
||||
result.typ = newProcType(info, idgen, result)
|
||||
result.typ.addParam dest
|
||||
if kind notin {attachedDestructor, attachedWasMoved}:
|
||||
result.typ.addParam src
|
||||
|
||||
@@ -29,7 +29,8 @@ when defined(nimPreviewSlimSystem):
|
||||
import ../dist/checksums/src/checksums/sha1
|
||||
|
||||
import pipelines
|
||||
from icconfig import produceIcConfig
|
||||
import icprof
|
||||
from icconfig import produceIcConfig, ensureIcConfig
|
||||
|
||||
when not defined(nimKochBootstrap):
|
||||
import nifbackend
|
||||
@@ -269,6 +270,28 @@ proc mainCommand*(graph: ModuleGraph) =
|
||||
|
||||
proc compileToBackend() =
|
||||
customizeForBackend(conf.backend)
|
||||
if isIcDriver(conf):
|
||||
# `nim c --ic:on` / `nim cpp --ic:on`: same driver as `nim ic`, entered
|
||||
# through the ordinary compile command so every backend switch the user
|
||||
# already knows keeps working (`nim cpp`, `--exceptions:`, `-d:`, ...).
|
||||
# `customizeForBackend` above has already defined the backend symbol and
|
||||
# picked the exception model, which is exactly what the per-module
|
||||
# children must inherit — `computeForwardedArgs` forwards both.
|
||||
setUseIc(true)
|
||||
wantMainModule(conf)
|
||||
setOutFile(conf)
|
||||
when not defined(nimKochBootstrap):
|
||||
if conf.icPreparsedConfig.len == 0:
|
||||
# `--ic:on` came from a `nim.cfg`/`config.nims` rather than the command
|
||||
# line, so `nim.nim` could not see it before config loading and the
|
||||
# precompiled config the children replay does not exist yet. Produce it
|
||||
# now. (The driver then keeps the config IT parsed instead of replaying
|
||||
# the artifact; both come from the same files.)
|
||||
ensureIcConfig(conf)
|
||||
commandIc(conf)
|
||||
else:
|
||||
rawMessage(conf, errGenerated, "--ic:on not available in bootstrap build")
|
||||
return
|
||||
setOutFile(conf)
|
||||
case conf.backend
|
||||
of backendC: commandCompileToC(graph)
|
||||
@@ -423,7 +446,9 @@ proc mainCommand*(graph: ModuleGraph) =
|
||||
# per-module compilation model cannot provide (yet); methods dispatch
|
||||
# through the classic if-chain dispatchers instead
|
||||
excl conf.features, Feature.vtables
|
||||
commandCheck(graph)
|
||||
# `tStage` for a `nim m` process, so `Process - Stage` is its real startup
|
||||
# (exec, runtime init, config replay) rather than its whole runtime.
|
||||
timed tStage: commandCheck(graph)
|
||||
of cmdNifC:
|
||||
setUseIc(true)
|
||||
excl conf.features, Feature.vtables
|
||||
|
||||
@@ -61,22 +61,12 @@ proc mangleProcNameExt*(graph: ModuleGraph, s: PSym): string =
|
||||
# starts with an EMPTY per-name disamb table, so its `disamb` restarts at 0
|
||||
# and collides with same-named sem-time symbols loaded from NIFs (two
|
||||
# `=destroy` hooks both mangling to `_u2` → "conflicting types for ..." in
|
||||
# the generated C). Most such symbols never cross a process boundary (nifc
|
||||
# lifts, emits and compiles them in one run), so the per-module-unique
|
||||
# item id is a safe and deterministic discriminator; the `_c` marker keeps
|
||||
# the namespace disjoint from `_u<disamb>`.
|
||||
# the generated C). The `_c` marker keeps the namespace disjoint from
|
||||
# `_u<disamb>`; `backendMintedDisamb` (astdef) is the ONE definition of which
|
||||
# integer identifies such a symbol, shared with `ccgutils.makeUnique` and
|
||||
# `ast2nif.toNifSymName` so the C name and the NIF name cannot drift apart.
|
||||
result = "_c"
|
||||
if (s.disamb and HookDisambBit) != 0'i32:
|
||||
# EXCEPTION: a backend-minted sym whose `disamb` is content-derived
|
||||
# (setHookDisamb gave it HookDisambBit) — e.g. the `rttiDestroy` wrapper —
|
||||
# DOES cross process boundaries: its C name is baked into the type's RTTI
|
||||
# table, which is emit-everywhere and merge-deduped, so one process's
|
||||
# `_c<item>` (a per-process backend counter) ends up referenced while the
|
||||
# wrapper is defined with another's → undefined at link (`rttiDestroy_c23`).
|
||||
# The content-derived disamb is stable across processes; use it.
|
||||
result.addInt s.disamb
|
||||
else:
|
||||
result.addInt s.itemId.item
|
||||
result.addInt backendMintedDisamb(s)
|
||||
else:
|
||||
result = "_u"
|
||||
# Use `disamb` rather than `itemId.item`: under incremental compilation a
|
||||
|
||||
@@ -17,7 +17,8 @@ import ast, astalgo, options, lineinfos,idents, btrees, ropes, msgs, pathutils,
|
||||
|
||||
when not defined(nimKochBootstrap):
|
||||
import ast2nif
|
||||
import "../dist/nimony/src/lib" / [nifstreams, bitabs]
|
||||
import nifstreams
|
||||
import "../dist/nimony/src/lib" / bitabs
|
||||
|
||||
import typekeys
|
||||
|
||||
@@ -35,6 +36,10 @@ type
|
||||
pureEnums*: seq[PSym]
|
||||
interf: TStrTable
|
||||
interfHidden: TStrTable
|
||||
hiddenPending: bool ## `interfHidden` holds only the exported half so far;
|
||||
## `ensureHiddenIface` materialises the hidden-only
|
||||
## symbols on first use. See
|
||||
## `ast2nif.buildHiddenInterface`.
|
||||
uniqueName*: Rope
|
||||
|
||||
Operators* = object
|
||||
@@ -136,6 +141,10 @@ type
|
||||
systemModule*: PSym
|
||||
sysTypes*: array[TTypeKind, PType]
|
||||
compilerprocs*: TStrTable
|
||||
missingCompilerProcs*: HashSet[string]
|
||||
# `nim nifc` only: compilerproc names no
|
||||
# loaded module defines, so the whole-program
|
||||
# index scan in `loadCompilerProc` runs once
|
||||
exposed*: TStrTable
|
||||
packageTypes*: TStrTable
|
||||
emptyNode*: PNode
|
||||
@@ -165,6 +174,11 @@ type
|
||||
onDefinitionResolveForward*: proc (graph: ModuleGraph; s: PSym; info: TLineInfo) {.nimcall.}
|
||||
onUsage*: proc (graph: ModuleGraph; s: PSym; info: TLineInfo) {.nimcall.}
|
||||
globalDestructors*: seq[PNode]
|
||||
icModuleDtors*: seq[string] # per-module backend: the C names of the
|
||||
# other modules' global-destructor procs
|
||||
# (`genIcModuleDestroyGlobals`), already in
|
||||
# call order; only the main module's `cg`
|
||||
# fills this, from the `.c.nif` meta heads
|
||||
strongSemCheck*: proc (graph: ModuleGraph; owner: PSym; body: PNode) {.nimcall.}
|
||||
compatibleProps*: proc (graph: ModuleGraph; formal, actual: PType): bool {.nimcall.}
|
||||
idgen*: IdGenerator
|
||||
@@ -247,6 +261,25 @@ proc toBase64a(s: cstring, len: int): string =
|
||||
result.add cb64[a shr 2]
|
||||
result.add cb64[(a and 3) shl 4]
|
||||
|
||||
proc ensureHiddenIface(g: ModuleGraph; pos: int) =
|
||||
## Materialise a loaded module's hidden-only interface the first time anything
|
||||
## asks for it. Every READ of `interfHidden` goes through `interfSelect`, so
|
||||
## guarding those sites is complete.
|
||||
if g.ifaces[pos].hiddenPending:
|
||||
when not defined(nimKochBootstrap):
|
||||
# By SUFFIX: `c.mods` and `g.ifaces` use different FileIndexes for the
|
||||
# same module (see `buildHiddenInterface`). Into a LOCAL table, because
|
||||
# loading symbols can grow `g.ifaces` and a `var` alias into it would then
|
||||
# point at the freed buffer. Cleared only on success, so an import whose
|
||||
# `.s.bif` does not exist yet is retried rather than written off.
|
||||
var tab = g.ifaces[pos].interfHidden
|
||||
if buildHiddenInterface(ast.program,
|
||||
cachedModuleSuffix(g.config, FileIndex pos), tab):
|
||||
g.ifaces[pos].interfHidden = tab
|
||||
g.ifaces[pos].hiddenPending = false
|
||||
else:
|
||||
g.ifaces[pos].hiddenPending = false
|
||||
|
||||
template interfSelect(iface: Iface, importHidden: bool): TStrTable =
|
||||
var ret = iface.interf.addr # without intermediate ptr, it creates a copy and compiler becomes 15x slower!
|
||||
if importHidden: ret = iface.interfHidden.addr
|
||||
@@ -282,6 +315,7 @@ proc initModuleIter*(mi: var ModuleIter; g: ModuleGraph; m: PSym; name: PIdent):
|
||||
assert m.kind == skModule
|
||||
mi.modIndex = m.position
|
||||
mi.importHidden = optImportHidden in m.options
|
||||
if mi.importHidden: ensureHiddenIface(g, mi.modIndex)
|
||||
result = initIdentIter(mi.ti, g.ifaces[mi.modIndex].interfSelect(mi.importHidden), name)
|
||||
|
||||
proc nextModuleIter*(mi: var ModuleIter; g: ModuleGraph): PSym =
|
||||
@@ -289,6 +323,7 @@ proc nextModuleIter*(mi: var ModuleIter; g: ModuleGraph): PSym =
|
||||
|
||||
iterator allSyms*(g: ModuleGraph; m: PSym): PSym =
|
||||
let importHidden = optImportHidden in m.options
|
||||
if importHidden: ensureHiddenIface(g, m.position)
|
||||
for s in g.ifaces[m.position].interfSelect(importHidden).data:
|
||||
if s != nil:
|
||||
yield s
|
||||
@@ -305,12 +340,31 @@ proc reexportedModuleSyms*(g: ModuleGraph; m: PSym): seq[(string, string)] =
|
||||
not seen.containsOrIncl(s.position):
|
||||
result.add (s.name.s, cachedModuleSuffix(g.config, FileIndex s.position))
|
||||
|
||||
proc reexportedLocalSyms*(g: ModuleGraph; m: PSym): seq[ItemId] =
|
||||
## Symbols DEFINED in `m` that reached `m`'s interface through an explicit
|
||||
## `export s` rather than through a `*` marker on their declaration.
|
||||
##
|
||||
## `semExport` re-exports by `reexportSym`, which adds to the interface table
|
||||
## and does NOT set `sfExported` — so a symbol can be importable while its
|
||||
## declaration says otherwise. The NIF writer decides importability from
|
||||
## `sfExported` alone and therefore missed exactly these. `std/random` does it
|
||||
## (`proc initRand(): Rand` private, then `since (1, 5, 1): export initRand`),
|
||||
## which is why `--ic:on` could not compile anything that reached
|
||||
## `std/tempfiles` — `initRand()` was undeclared in the importer.
|
||||
result = @[]
|
||||
for s in g.ifaces[m.position].interf.data:
|
||||
if s != nil and s.kind != skModule and sfExported notin s.flags and
|
||||
s.itemId.module == m.position:
|
||||
result.add s.itemId
|
||||
|
||||
proc someSym*(g: ModuleGraph; m: PSym; name: PIdent): PSym =
|
||||
let importHidden = optImportHidden in m.options
|
||||
if importHidden: ensureHiddenIface(g, m.position)
|
||||
result = strTableGet(g.ifaces[m.position].interfSelect(importHidden), name)
|
||||
|
||||
proc someSymAmb*(g: ModuleGraph; m: PSym; name: PIdent; amb: var bool): PSym =
|
||||
let importHidden = optImportHidden in m.options
|
||||
if importHidden: ensureHiddenIface(g, m.position)
|
||||
var ti: TIdentIter = default(TIdentIter)
|
||||
result = initIdentIter(ti, g.ifaces[m.position].interfSelect(importHidden), name)
|
||||
if result != nil and nextIdentIter(ti, g.ifaces[m.position].interfSelect(importHidden)) != nil:
|
||||
@@ -343,8 +397,8 @@ iterator procInstCacheItems*(g: ModuleGraph; s: PSym): PInstantiation =
|
||||
proc getAttachedOp*(g: ModuleGraph; t: PType; op: TTypeAttachedOp): PSym =
|
||||
## returns the requested attached operation for type `t`. Can return nil
|
||||
## if no such operation exists.
|
||||
if g.attachedOps[op].contains(t.itemId):
|
||||
result = g.attachedOps[op][t.itemId]
|
||||
if g.attachedOps[op].contains(t.bindingId):
|
||||
result = g.attachedOps[op][t.bindingId]
|
||||
elif g.config.cmd in {cmdNifC, cmdM}:
|
||||
# Fall back to key-based lookup for NIF-loaded hooks
|
||||
let key = typeKey(t, g.config, loadTypeCallback, loadSymCallback)
|
||||
@@ -373,7 +427,7 @@ proc setAttachedOp*(g: ModuleGraph; module: int; t: PType; op: TTypeAttachedOp;
|
||||
# references derived env-field syms that no module's NIF defines
|
||||
if g.loadedOps[op].getOrDefault(key) == nil:
|
||||
g.loadedOps[op][key] = value
|
||||
g.attachedOps[op][t.itemId] = value
|
||||
g.attachedOps[op][t.bindingId] = value
|
||||
return
|
||||
let existing = g.loadedOps[op].getOrDefault(key)
|
||||
if existing == nil:
|
||||
@@ -411,7 +465,7 @@ proc setAttachedOp*(g: ModuleGraph; module: int; t: PType; op: TTypeAttachedOp;
|
||||
break
|
||||
if not updated:
|
||||
g.opsLog.add LogEntry(kind: HookEntry, op: op, module: module, key: key, sym: value)
|
||||
g.attachedOps[op][t.itemId] = value
|
||||
g.attachedOps[op][t.bindingId] = value
|
||||
|
||||
proc setAttachedOp*(g: ModuleGraph; module: int; typeId: ItemId; op: TTypeAttachedOp; value: PSym) =
|
||||
## Overload that takes ItemId directly, useful for registering hooks from NIF index.
|
||||
@@ -419,7 +473,7 @@ proc setAttachedOp*(g: ModuleGraph; module: int; typeId: ItemId; op: TTypeAttach
|
||||
|
||||
proc setAttachedOpPartial*(g: ModuleGraph; module: int; t: PType; op: TTypeAttachedOp; value: PSym) =
|
||||
## we also need to record this to the packed module.
|
||||
g.attachedOps[op][t.itemId] = value
|
||||
g.attachedOps[op][t.bindingId] = value
|
||||
|
||||
proc completePartialOp*(g: ModuleGraph; module: int; t: PType; op: TTypeAttachedOp; value: PSym) {.inline.} =
|
||||
discard
|
||||
@@ -441,19 +495,19 @@ proc addNifReplayAction*(g: ModuleGraph; module: int32; n: PNode) =
|
||||
g.nifReplayActions.mgetOrPut(module, @[]).add n
|
||||
|
||||
iterator getMethodsPerType*(g: ModuleGraph; t: PType): PSym =
|
||||
if g.methodsPerType.contains(t.itemId):
|
||||
for it in mitems g.methodsPerType[t.itemId]:
|
||||
if g.methodsPerType.contains(t.bindingId):
|
||||
for it in mitems g.methodsPerType[t.bindingId]:
|
||||
yield it
|
||||
|
||||
proc getToStringProc*(g: ModuleGraph; t: PType): PSym =
|
||||
result = g.enumToStringProcs.getOrDefault(t.itemId)
|
||||
result = g.enumToStringProcs.getOrDefault(t.bindingId)
|
||||
if result == nil and g.config.cmd in {cmdNifC, cmdM}:
|
||||
let key = typeKey(t, g.config, loadTypeCallback, loadSymCallback)
|
||||
result = g.loadedEnumToStringProcs.getOrDefault(key)
|
||||
assert result != nil
|
||||
|
||||
proc setToStringProc*(g: ModuleGraph; t: PType; value: PSym) =
|
||||
g.enumToStringProcs[t.itemId] = value
|
||||
g.enumToStringProcs[t.bindingId] = value
|
||||
let key = typeKey(t, g.config, loadTypeCallback, loadSymCallback)
|
||||
# Stamp with the module that owns the generated proc, not the enum's def
|
||||
# module: the def module's process may never have generated it (same
|
||||
@@ -461,12 +515,12 @@ proc setToStringProc*(g: ModuleGraph; t: PType; value: PSym) =
|
||||
g.opsLog.add LogEntry(kind: EnumToStrEntry, module: value.itemId.module.int, key: key, sym: value)
|
||||
|
||||
iterator methodsForGeneric*(g: ModuleGraph; t: PType): (int, PSym) =
|
||||
if g.methodsPerGenericType.contains(t.itemId):
|
||||
for it in mitems g.methodsPerGenericType[t.itemId]:
|
||||
if g.methodsPerGenericType.contains(t.bindingId):
|
||||
for it in mitems g.methodsPerGenericType[t.bindingId]:
|
||||
yield (it[0], it[1])
|
||||
|
||||
proc addMethodToGeneric*(g: ModuleGraph; module: int; t: PType; col: int; m: PSym) =
|
||||
g.methodsPerGenericType.mgetOrPut(t.itemId, @[]).add (col, m)
|
||||
g.methodsPerGenericType.mgetOrPut(t.bindingId, @[]).add (col, m)
|
||||
let key = typeKey(t, g.config, loadTypeCallback, loadSymCallback)
|
||||
let ownerModule = if t.sym != nil: t.sym.itemId.module.int else: module
|
||||
g.opsLog.add LogEntry(kind: MethodEntry, module: ownerModule, key: key, sym: m)
|
||||
@@ -481,6 +535,49 @@ proc logMethodDef*(g: ModuleGraph; s: PSym) =
|
||||
g.opsLog.add LogEntry(kind: MethodEntry, module: s.itemId.module.int,
|
||||
key: "", sym: s)
|
||||
|
||||
proc logCppMember*(g: ModuleGraph; s: PSym) =
|
||||
## Log a C++ `{.member.}`/`{.virtual.}`/`{.constructor.}` registration (and the
|
||||
## `importcpp` default-initializer flavour) so the NIF backend can rebuild
|
||||
## `memberProcsPerType`/`initializersPerType`, which live only in the sem
|
||||
## process. Without them the per-module backend emitted the struct WITHOUT its
|
||||
## in-class member declarations and the out-of-class definitions did not match
|
||||
## ("no declaration matches 'void Doo::memberProc()'").
|
||||
##
|
||||
## No type key: `replayCppMember` re-derives the type from the routine's
|
||||
## signature exactly as `semCppMember` does, so nothing has to survive the
|
||||
## round trip except the routine itself.
|
||||
if g.config.cmd in {cmdNifC, cmdM}:
|
||||
g.opsLog.add LogEntry(kind: CppMemberEntry, module: s.itemId.module.int,
|
||||
key: "", sym: s)
|
||||
|
||||
proc replayCppMember*(g: ModuleGraph; s: PSym) =
|
||||
## Inverse of `logCppMember`, mirroring `semstmts.semCppMember`'s derivation.
|
||||
if s == nil or s.typ == nil: return
|
||||
if sfImportc notin s.flags:
|
||||
var typ = if sfConstructor in s.flags: s.typ.returnType else: s.typ.firstParamType
|
||||
if typ != nil and typ.kind == tyPtr and sfConstructor notin s.flags:
|
||||
typ = typ.elementType
|
||||
if typ != nil and typ.kind == tyObject:
|
||||
let procs = addr g.memberProcsPerType.mgetOrPut(typ.bindingId, @[])
|
||||
for prc in procs[]:
|
||||
if prc == s: return
|
||||
procs[].add s
|
||||
else:
|
||||
let typ = s.typ.returnType
|
||||
if typ != nil and typ.kind == tyObject and
|
||||
typ.bindingId notin g.initializersPerType and s.typ.n != nil:
|
||||
# The default values sem read off the `nkIdentDefs` live on the param syms.
|
||||
var call = newTree(nkCall, newSymNode(s))
|
||||
var isInitializer = s.typ.n.len > 1
|
||||
for i in 1 ..< s.typ.n.len:
|
||||
let p = s.typ.n[i]
|
||||
if p.kind != nkSym or p.sym.ast == nil or p.sym.ast.kind == nkEmpty:
|
||||
isInitializer = false
|
||||
break
|
||||
call.add p.sym.ast
|
||||
if isInitializer:
|
||||
g.initializersPerType[typ.bindingId] = call
|
||||
|
||||
proc registerLoadedMethod*(g: ModuleGraph; m: PSym) =
|
||||
## Rebuild the dispatch buckets from a serialized method registration.
|
||||
## Buckets group the methods sharing a dispatcher; the dispatcher's BODY
|
||||
@@ -522,12 +619,6 @@ proc logGenericInstance*(g: ModuleGraph; inst: PSym) =
|
||||
let ownerModule = inst.itemId.module.int
|
||||
g.opsLog.add LogEntry(kind: GenericInstEntry, module: ownerModule, sym: inst)
|
||||
|
||||
const
|
||||
InstanceDisambBit* = 0x4000_0000'i32
|
||||
## Set in the `disamb` of routine instances whose value is content-derived
|
||||
## (see `setInstanceDisamb`); keeps them disjoint from the small counter
|
||||
## range ordinary symbols draw from, so the NIF name `name.disamb.module`
|
||||
## stays collision-free within a module.
|
||||
|
||||
proc setInstanceDisamb*(g: ModuleGraph; inst, generic: PSym;
|
||||
concreteTypes: openArray[PType]) =
|
||||
@@ -566,12 +657,6 @@ proc setInstanceDisamb*(g: ModuleGraph; inst, generic: PSym;
|
||||
break
|
||||
inst.disamb = h
|
||||
|
||||
const
|
||||
HookDisambBit* = 0x2000_0000'i32
|
||||
## Set in the `disamb` of synthesized type-bound operators and `$enum`
|
||||
## procs whose value is content-derived (see `setHookDisamb`); disjoint
|
||||
## from both the small counter range and the `InstanceDisambBit` range.
|
||||
|
||||
proc setHookDisamb*(g: ModuleGraph; hook: PSym; opName: string; typ: PType) =
|
||||
## Under IC, replace a synthesized hook's counter-based `disamb` with a
|
||||
## content-derived one: a hash of the operation name plus the `typeKey` of
|
||||
@@ -638,6 +723,29 @@ proc loadCompilerProc*(g: ModuleGraph; name: string): PSym =
|
||||
strTableAdd(g.compilerprocs, result)
|
||||
return result
|
||||
|
||||
# `nim nifc`: a module loaded from a NIF is named by its mangled suffix
|
||||
# (`thrkxstl4`), not by its source name, and its file index resolves to
|
||||
# that suffix too — so the `"threadpool"` match below can never fire and
|
||||
# `spawn`, expanded at codegen time, died on `system module needs:
|
||||
# nimArgsPassingDone`. The backend loads the WHOLE program before
|
||||
# codegen starts, so just consult every loaded module's index; a miss is
|
||||
# final for the rest of the process (nothing more gets loaded) and is
|
||||
# remembered, because `getCompilerProc` is also used as a mere presence
|
||||
# probe and would otherwise rescan every index on every call.
|
||||
if g.config.cmd == cmdNifC:
|
||||
if name in g.missingCompilerProcs: return nil
|
||||
for moduleIdx in 0..<g.ifaces.len:
|
||||
let module = g.ifaces[moduleIdx].module
|
||||
if module == nil or module.position.FileIndex == systemFileIdx: continue
|
||||
if not fileExists(toNifFilename(g.config, module.position.FileIndex)):
|
||||
continue
|
||||
result = tryResolveCompilerProc(ast.program, name, module.position.FileIndex)
|
||||
if result != nil:
|
||||
strTableAdd(g.compilerprocs, result)
|
||||
return result
|
||||
g.missingCompilerProcs.incl name
|
||||
return nil
|
||||
|
||||
# Try threadpool module (some compilerprocs like FlowVar are there)
|
||||
# Find threadpool module by searching loaded modules
|
||||
for moduleIdx in 0..<g.ifaces.len:
|
||||
@@ -940,6 +1048,8 @@ when not defined(nimKochBootstrap):
|
||||
g.loadedOps[x.op][x.key] = x.sym
|
||||
of EnumToStrEntry:
|
||||
g.loadedEnumToStringProcs[x.key] = x.sym
|
||||
of CppMemberEntry:
|
||||
replayCppMember(g, x.sym)
|
||||
of MethodEntry:
|
||||
# only `methodDef` registrations (empty key) rebuild dispatch
|
||||
# buckets; the `addMethodToGeneric` flavor (typeKey key) announces
|
||||
@@ -970,7 +1080,13 @@ when not defined(nimKochBootstrap):
|
||||
var isKnownFile = false
|
||||
let fileIdx = g.config.registerNifSuffix(string suffix, isKnownFile)
|
||||
if not g.hookClosure.containsOrIncl(fileIdx.int):
|
||||
let precomp = loadNifModule(ast.program, suffix, interf, interfHidden, {})
|
||||
# `SkipInterfaceTables`: `interf`/`interfHidden` here are scratch tables
|
||||
# shared by every iteration and never read — this module is a
|
||||
# dep-of-a-dep, so none of its symbols are visible to the module being
|
||||
# semchecked. Building them called `loadSymFromIndexEntry` for every
|
||||
# index entry of every closure member.
|
||||
let precomp = loadNifModule(ast.program, suffix, interf, interfHidden,
|
||||
{SkipInterfaceTables})
|
||||
registerLoadedHooks(g, precomp.logOps)
|
||||
# Record this transitively-loaded module so the sem driver applies its
|
||||
# VM-level load effects (macro-cache replay + `{.compileTime.}` global init)
|
||||
@@ -1033,6 +1149,7 @@ when not defined(nimKochBootstrap):
|
||||
strTableAdd(interf, inner)
|
||||
g.ifaces[fIdx.int].interf = interf
|
||||
g.ifaces[fIdx.int].interfHidden = interfHidden
|
||||
g.ifaces[fIdx.int].hiddenPending = true
|
||||
|
||||
proc moduleFromNifFile*(g: ModuleGraph; fileIdx: FileIndex;
|
||||
flags: set[LoadFlag] = {}): PrecompiledModule =
|
||||
@@ -1065,11 +1182,26 @@ when not defined(nimKochBootstrap):
|
||||
setOwner(m, getPackage(g.config, g.cache, fileIdx))
|
||||
# Register module in graph
|
||||
registerModule(g, m)
|
||||
# ... and, in the BACKEND, bind its NIF name to THIS symbol before anything
|
||||
# in the file is decoded, so the loader never mints a second `skModule` for
|
||||
# it (see `registerModuleSelfSym`). Backend-only: under `nim m` a module is
|
||||
# loaded for its INTERFACE, and re-pointing the owner slot of every loaded
|
||||
# symbol at the freshly built module sym changes what sem sees for an
|
||||
# imported routine — `times.toDateTimeByWeek` then lost its inferred
|
||||
# `raises` and the importer failed with "can raise an unlisted exception".
|
||||
if g.config.cmd == cmdNifC:
|
||||
registerModuleSelfSym(ast.program, cachedModuleSuffix(g.config, fileIdx), m)
|
||||
|
||||
result = loadNifModule(ast.program, fileIdx,
|
||||
g.ifaces[fileIdx.int].interf,
|
||||
g.ifaces[fileIdx.int].interfHidden, flags)
|
||||
# The hidden-only half was not built; `ensureHiddenIface` will, if asked.
|
||||
g.ifaces[fileIdx.int].hiddenPending = true
|
||||
result.module = m
|
||||
# Restore the module symbol's persisted flags (see ast2nif `(modflags)`);
|
||||
# `cgen.genTopLevelStmt` gates the destructor pass on `sfInjectDestructors`.
|
||||
if (result.moduleFlags and ModFlagInjectDestructors) != 0:
|
||||
m.incl sfInjectDestructors
|
||||
for (mname, msuffix) in result.reexportedModules:
|
||||
let ms = materializeReexportedModule(g, mname, msuffix)
|
||||
if ms != nil:
|
||||
@@ -1117,7 +1249,7 @@ when not defined(nimKochBootstrap):
|
||||
discard "dispatch buckets already rebuilt by registerLoadedHooks"
|
||||
of GenericInstEntry:
|
||||
raiseAssert "GenericInstEntry should not be in the NIF index"
|
||||
of HookEntry, EnumToStrEntry:
|
||||
of HookEntry, EnumToStrEntry, CppMemberEntry:
|
||||
discard "already done by registerLoadedHooks"
|
||||
# Register methods per type from NIF index
|
||||
discard "todo"
|
||||
|
||||
@@ -28,6 +28,7 @@ import ast, options, lineinfos, modulegraphs, cgendata, cgen,
|
||||
from cgmeth import generateIfMethodDispatchers
|
||||
from transf import transformBody
|
||||
from injectdestructors import injectDestructorCalls
|
||||
import icprof
|
||||
import ic / replayer
|
||||
|
||||
proc systemNifSuffix(conf: ConfigRef): string =
|
||||
@@ -134,91 +135,6 @@ proc emitMethodDispatchers(g: ModuleGraph) =
|
||||
if not containsOrIncl(mainMod.declaredThings, disp.id):
|
||||
genProcLvl3(mainMod, disp)
|
||||
|
||||
proc signatureHasMetaType(t: PType; depth: int = 0): bool =
|
||||
## Whether a routine signature mentions a compile-time/meta element type
|
||||
## (`typed`/`untyped` — e.g. `echo`'s `varargs[typed]` — typedesc, static,
|
||||
## generic param). Such routines are expanded at their call sites and never
|
||||
## emitted standalone, so the per-module owned-routine seeding must skip them
|
||||
## (`getTypeDescAux(tyTyped)` otherwise). `tfHasMeta` alone misses the varargs
|
||||
## element case, hence the explicit scan.
|
||||
result = false
|
||||
if t == nil or depth > 8: return false
|
||||
if t.kind == tyGenericBody:
|
||||
# The uninstantiated template carried as a `tyGenericInst`'s first child
|
||||
# always mentions its `tyGenericParam` placeholders, but the instance
|
||||
# itself is fully concrete (e.g. `var CountTable[SigHash]`). Descending
|
||||
# here would wrongly flag every routine with a generic-instance parameter
|
||||
# as meta and drop it from the owned-routine seeding -> undefined symbols
|
||||
# at link (its only definer never emits it).
|
||||
return false
|
||||
if t.kind == tyStatic:
|
||||
# A RESOLVED static value (the `256` in `MDigest[256]`, the `N` in
|
||||
# `HashList[T, N]`, …) is carried as a `tyStatic` node inside the otherwise
|
||||
# fully-concrete `tyGenericInst`, but it is NOT meta: the routine is a normal
|
||||
# runtime routine the owner must emit. Only an UNRESOLVED `static T` parameter
|
||||
# (no bound value, `t.n == nil`) is meta. Without this, every routine whose
|
||||
# signature touches a `static`-parameterized generic instance (the bulk of
|
||||
# the SSZ/`MDigest` API) is dropped from the owned-routine seeding and ends up
|
||||
# an undefined reference at link (mirrors the tyGenericBody case above).
|
||||
return t.n == nil
|
||||
if t.kind in {tyTyped, tyUntyped, tyTypeDesc, tyGenericParam,
|
||||
tyAnything, tyFromExpr, tyError}:
|
||||
return true
|
||||
for k in t.kids:
|
||||
if signatureHasMetaType(k, depth + 1): return true
|
||||
|
||||
proc ownsRuntimeRoutine(s: PSym; modPos: int): bool =
|
||||
## A concrete, non-generic, runtime routine with a real body, OWNED by the
|
||||
## module at `modPos`. Shared by the `cg` stage's owned-routine seeding (so a
|
||||
## routine called only from other modules is still emitted by somebody) and
|
||||
## the `lower` stage's owned-routine enumeration, so both stages see exactly
|
||||
## the same set. The exclusions:
|
||||
## - nested/closure procs (owner is a proc, not a module): emitted via their
|
||||
## enclosing routine's lambda-lifting, never standalone;
|
||||
## - generic instances (`sfFromGeneric`): emitted by demand, deduped by merge;
|
||||
## - `importc`/`compileTime`/`error`/forward sentinels and meta signatures:
|
||||
## not real codegen targets.
|
||||
## - method DISPATCHERS (`sfDispatcher`): their bodies are (re)synthesized into
|
||||
## the main TU by `emitMethodDispatchers`/`generateIfMethodDispatchers`, never
|
||||
## per module. A dispatcher is a `copySym` clone of the method that shares the
|
||||
## method's body sub-tree (incl. its closure iterator); transforming it here
|
||||
## would lambda-lift that SHARED iterator a SECOND time under a different owner
|
||||
## identity, baking a conflicting `up` field → "up references do not agree"
|
||||
## (the divergence is impossible in non-IC, where the dispatcher body is empty
|
||||
## at lift time). So a dispatcher is never an owned runtime routine.
|
||||
## A `{.closure.}` iterator IS a standalone runtime routine (unlike an inline
|
||||
## iterator, which is expanded at each call site) and must be emitted by its
|
||||
## owner — else a cross-module `for` over it links to nothing.
|
||||
##
|
||||
## Generic INSTANCES (`sfFromGeneric`) are NEVER an owned runtime routine — not
|
||||
## in `cg` and not in the `lower` stage. They are demanded by the backend's
|
||||
## emit-everywhere path and deduped by `merge` (content C name); the frontend
|
||||
## materialises them through the `(offer)` mechanism. The `lower` stage must
|
||||
## not transform an instance: a not-fully-concrete instance (a closure factory
|
||||
## over a `static` param, or a `$`/`=` op instance whose body resolves only at
|
||||
## its further-specialised use sites) still carries unresolved overload choices
|
||||
## and crashes `transformBody` (empty-`namePos` lambda, nil-typed const-fold).
|
||||
s.itemId.module == modPos and
|
||||
(s.kind in {skProc, skFunc, skConverter, skMethod} or
|
||||
(s.kind == skIterator and s.typ != nil and s.typ.callConv == ccClosure)) and
|
||||
s.skipGenericOwner != nil and s.skipGenericOwner.kind == skModule and
|
||||
s.magic == mNone and
|
||||
sfFromGeneric notin s.flags and
|
||||
sfDispatcher notin s.flags and
|
||||
{sfForward, sfImportc, sfCompileTime, sfError} * s.flags == {} and
|
||||
s.typ != nil and not signatureHasMetaType(s.typ) and
|
||||
s.ast != nil and s.ast.safeLen > bodyPos and
|
||||
s.ast[genericParamsPos].kind == nkEmpty
|
||||
# NOTE: an `nkEmpty` body is NOT a disqualifier. A concrete, owned, non-
|
||||
# forward/-importc/-magic routine whose body folds to nothing is still a real
|
||||
# definition the owner must emit (`void f(void){}`), exactly as whole-program
|
||||
# cgen does — else a cross-module caller links to nothing. This bites e.g.
|
||||
# Nimbus' `extras.incInternalErrors`, a plain `proc` whose sole statement is a
|
||||
# metrics-counter `.inc()` that the `metrics` library expands to a no-op when
|
||||
# the importing tool (ncli) builds with `-u:metrics`; the body is then a bare
|
||||
# `nkEmpty`, but `state_transition_epoch` still calls it. Forward declarations
|
||||
# (the other empty-body case) carry `sfForward` and are excluded above.
|
||||
|
||||
proc generateCodeForModule(g: ModuleGraph; precomp: PrecompiledModule) =
|
||||
## Generate C code for a single module.
|
||||
let moduleId = precomp.module.position
|
||||
@@ -309,24 +225,30 @@ proc loadBackendModules(g: ModuleGraph; mainFileIdx: FileIndex):
|
||||
discard setupNifBackendModule(g, precompSys.module)
|
||||
result = (modules, precompSys, nifFiles)
|
||||
|
||||
proc loadDepClosure(g: ModuleGraph; targetSuffix: string):
|
||||
proc loadDepClosure(g: ModuleGraph; targetSuffixes: seq[string]):
|
||||
tuple[modules: seq[PrecompiledModule], precompSys: PrecompiledModule,
|
||||
target: PrecompiledModule] =
|
||||
## Per-module `cg`/`emit` for a NON-main target: load system + the target
|
||||
## module + the target's transitive import closure ONLY — not the whole
|
||||
## program. This is the "process the one file it is passed" model (à la
|
||||
## Nimony's `hexer c file.nif`): the foreign symbols the target's codegen
|
||||
## demands are loaded lazily by `ast2nif.moduleId`, which opens any referenced
|
||||
## module's NIF index on first touch, so a body in a not-loaded module still
|
||||
## resolves. The closure is loaded as full `BModule`s only so that the
|
||||
## incidental `g.mods[pos]` accesses during codegen resolve; system's own
|
||||
## internal closure (allocators, locks, …) is included because a target's
|
||||
## emit-everywhere codegen can demand those without importing them directly.
|
||||
targets: seq[PrecompiledModule]] =
|
||||
## Per-module `lower`/`cg`/`emit` for a NON-main batch: load system + every
|
||||
## module in the batch + their transitive import closure ONLY — not the whole
|
||||
## program. This is the "process the files it is passed" model (à la Nimony's
|
||||
## `hexer c file.nif`): the foreign symbols a target's codegen demands are
|
||||
## loaded lazily by `ast2nif.moduleId`, which opens any referenced module's NIF
|
||||
## index on first touch, so a body in a not-loaded module still resolves. The
|
||||
## closure is loaded as full `BModule`s only so that the incidental
|
||||
## `g.mods[pos]` accesses during codegen resolve; system's own internal closure
|
||||
## (allocators, locks, …) is included because a target's emit-everywhere
|
||||
## codegen can demand those without importing them directly.
|
||||
##
|
||||
## The whole program is no longer loaded in this process, which is what bounds
|
||||
## per-process memory under nifmake's parallel fan-out (the main module's `cg`,
|
||||
## which still loads everything for NimMain's init list and the method
|
||||
## dispatchers, runs essentially alone since every other `.c.nif` precedes it).
|
||||
##
|
||||
## The batch is loaded as ONE closure: `resetForBackend`, the system load and
|
||||
## the closure walk happen once no matter how many targets share the process,
|
||||
## and a module in two targets' closures is loaded once. That amortization is
|
||||
## the reason batches exist — a per-module process spends far more time here
|
||||
## than it spends generating code.
|
||||
resetForBackend(g)
|
||||
var isKnownFile = false
|
||||
let systemFileIdx = registerNifSuffix(g.config, systemNifSuffix(g.config), isKnownFile)
|
||||
@@ -338,18 +260,30 @@ proc loadDepClosure(g: ModuleGraph; targetSuffix: string):
|
||||
var visited = initHashSet[string]()
|
||||
visited.incl systemNifSuffix(g.config)
|
||||
|
||||
# Only the target is codegen'd, so only it needs its full AST; the closure is
|
||||
# loaded interface-only (demanded bodies come lazily from the kept-open
|
||||
# streams), which is what keeps a per-module process light under parallel fan-out.
|
||||
var isKnown = false
|
||||
let targetIdx = registerNifSuffix(g.config, targetSuffix, isKnown)
|
||||
let target = moduleFromNifFile(g, targetIdx, {LoadFullAst})
|
||||
visited.incl targetSuffix
|
||||
|
||||
# Only the batch is codegen'd, so only it needs full ASTs; the surrounding
|
||||
# closure is loaded interface-only (demanded bodies come lazily from the
|
||||
# kept-open streams), which is what keeps the process light under fan-out.
|
||||
var targets: seq[PrecompiledModule] = @[]
|
||||
var stack: seq[ModuleSuffix] = @[]
|
||||
if target.module != nil:
|
||||
modules.add target
|
||||
for dep in target.deps: stack.add dep
|
||||
# Separate from `visited`, which exists to keep the closure walk off modules
|
||||
# already loaded. System is in `visited` from the start yet can perfectly well
|
||||
# BE a batch member — it is a live node with its own `.t.bif` and `.c.nif` —
|
||||
# and then it needs the full-AST load like any other member, on top of the
|
||||
# interface-only load above. Reusing `visited` to deduplicate members skipped
|
||||
# it and produced a batch with nothing in it.
|
||||
var claimed = initHashSet[string]()
|
||||
for targetSuffix in targetSuffixes:
|
||||
if claimed.containsOrIncl(targetSuffix): continue
|
||||
var isKnown = false
|
||||
let targetIdx = registerNifSuffix(g.config, targetSuffix, isKnown)
|
||||
let target = moduleFromNifFile(g, targetIdx, {LoadFullAst})
|
||||
targets.add target
|
||||
# A member that is also another member's dependency must keep its full AST,
|
||||
# so claim it before the closure walk can load it interface-only.
|
||||
visited.incl targetSuffix
|
||||
if target.module != nil:
|
||||
modules.add target
|
||||
for dep in target.deps: stack.add dep
|
||||
if precompSys.module != nil:
|
||||
for dep in precompSys.deps: stack.add dep
|
||||
while stack.len > 0:
|
||||
@@ -366,7 +300,7 @@ proc loadDepClosure(g: ModuleGraph; targetSuffix: string):
|
||||
discard setupNifBackendModule(g, m.module)
|
||||
if precompSys.module != nil:
|
||||
discard setupNifBackendModule(g, precompSys.module)
|
||||
result = (modules, precompSys, target)
|
||||
result = (modules, precompSys, targets)
|
||||
|
||||
proc findTargetModule(g: ModuleGraph; modules: seq[PrecompiledModule];
|
||||
precompSys: PrecompiledModule; suffix: string): PrecompiledModule =
|
||||
@@ -380,6 +314,18 @@ proc findTargetModule(g: ModuleGraph; modules: seq[PrecompiledModule];
|
||||
cachedModuleSuffix(g.config, FileIndex precompSys.module.position) == suffix:
|
||||
return precompSys
|
||||
|
||||
proc backendBatch(conf: ConfigRef; mainSuffix: string):
|
||||
tuple[members: seq[string], isMain: bool] =
|
||||
## The module suffixes this invocation processes, and whether it is the
|
||||
## main-module invocation. Main is never batched with anything else: it loads
|
||||
## the WHOLE program (NimMain's init list and the method dispatchers are
|
||||
## whole-program facts), so putting another module in with it would defeat the
|
||||
## bound on per-process memory that the per-module split exists to provide.
|
||||
let members = conf.icBackendModules
|
||||
result = (members: members,
|
||||
isMain: members.len == 0 or
|
||||
(members.len == 1 and members[0] == mainSuffix))
|
||||
|
||||
proc setNestedClosureBodies(g: ModuleGraph; idgen: IdGenerator; n: PNode;
|
||||
owner: PSym; seen: var IntSet) =
|
||||
## A closure routine nested in `owner` (the `:anonymous` proc lambda-lifting
|
||||
@@ -444,8 +390,12 @@ proc reownFromTwin(n: PNode; twin, s: PSym) =
|
||||
for i in 0 ..< n.safeLen:
|
||||
reownFromTwin(n[i], twin, s)
|
||||
|
||||
proc lowerOneModule(g: ModuleGraph; target: PrecompiledModule;
|
||||
seenNested: var IntSet)
|
||||
|
||||
proc generateLowerStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
## Per-module backend lowering (`--icBackendStage:lower --icBackendModule:<suffix>`):
|
||||
## Backend lowering for this invocation's batch
|
||||
## (`--icBackendStage:lower --icBackendModules:<a,b,c>`):
|
||||
## enumerate the routines this module OWNS and write them to `<module>.t.nif`.
|
||||
## Eventually this transforms each owned routine once, in the owner's id space,
|
||||
## so `cg` reads the result instead of re-deriving it (re-derivation per
|
||||
@@ -458,30 +408,46 @@ proc generateLowerStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
## stage does.
|
||||
nifcBackendActive = true
|
||||
let mainSuffix = cachedModuleSuffix(g.config, mainFileIdx)
|
||||
let targetIsMain = g.config.icBackendModule.len == 0 or
|
||||
g.config.icBackendModule == mainSuffix
|
||||
let batch = backendBatch(g.config, mainSuffix)
|
||||
var modules: seq[PrecompiledModule]
|
||||
var precompSys: PrecompiledModule
|
||||
var target: PrecompiledModule
|
||||
if targetIsMain:
|
||||
var targets: seq[PrecompiledModule]
|
||||
if batch.isMain:
|
||||
var nifFiles: seq[string]
|
||||
(modules, precompSys, nifFiles) = loadBackendModules(g, mainFileIdx)
|
||||
if modules.len == 0:
|
||||
rawMessage(g.config, errGenerated,
|
||||
"Cannot load NIF file for main module: " & toFullPath(g.config, mainFileIdx))
|
||||
return
|
||||
target = findTargetModule(g, modules, precompSys, g.config.icBackendModule)
|
||||
targets = @[findTargetModule(g, modules, precompSys, mainSuffix)]
|
||||
else:
|
||||
(modules, precompSys, target) = loadDepClosure(g, g.config.icBackendModule)
|
||||
(modules, precompSys, targets) = block:
|
||||
icProfStart(tLoadClosure)
|
||||
let r = loadDepClosure(g, batch.members)
|
||||
icProfStop(tLoadClosure)
|
||||
r
|
||||
# ONE PSym graph for the whole batch, so the guard against transforming a
|
||||
# nested routine twice has to span it: two members reaching the same nested
|
||||
# closure would otherwise inject its destructors twice into the same `PSym`.
|
||||
# (In the one-module-per-process fan-out the two members are two processes
|
||||
# with two copies, and each injects once.)
|
||||
var seenNested = initIntSet()
|
||||
for target in targets:
|
||||
lowerOneModule(g, target, seenNested)
|
||||
|
||||
proc lowerOneModule(g: ModuleGraph; target: PrecompiledModule;
|
||||
seenNested: var IntSet) =
|
||||
## Lower the routines `target` OWNS and write its `.t.bif`. One batch member.
|
||||
if target.module == nil:
|
||||
rawMessage(g.config, errGenerated,
|
||||
"per-module lowering: module not found for suffix: " & g.config.icBackendModule)
|
||||
"per-module lowering: module not found for suffix")
|
||||
return
|
||||
let modPos = target.module.position
|
||||
let tb = BModuleList(g.backend).mods[modPos]
|
||||
if tb == nil:
|
||||
rawMessage(g.config, errGenerated,
|
||||
"per-module lowering: no backend module for suffix: " & g.config.icBackendModule)
|
||||
"per-module lowering: no backend module for suffix: " &
|
||||
cachedModuleSuffix(g.config, FileIndex modPos))
|
||||
return
|
||||
# Transform every owned routine ONCE in this single process's id space and
|
||||
# re-serialize the ENTIRE module as a proper indexed NIF (`writeLoweredModule`)
|
||||
@@ -497,11 +463,14 @@ proc generateLowerStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
# `transformBody`/lambda-lifting LIFTS the closure env's type-bound ops
|
||||
# (`=destroy` etc.) into `g.opsLog`; snapshot its length so we serialize exactly
|
||||
# the ops THIS stage created (not those loaded from `.s.nif`).
|
||||
# Per MEMBER, not per batch: each member's `.t.bif` must carry exactly the ops
|
||||
# ITS lowering lifted, the way its own process would have written them.
|
||||
let opsLogStart = g.opsLog.len
|
||||
# Shared across the owned loop so a nested routine reachable from more than one
|
||||
# owner is transformed + destructor-injected EXACTLY once (double injection
|
||||
# would emit two `=destroy`/`=copy` runs).
|
||||
var seenNested = initIntSet()
|
||||
# `seenNested` comes from the caller and spans the whole batch — see the
|
||||
# comment at its declaration. Within one module it already served to transform
|
||||
# + destructor-inject a nested routine reachable from more than one owner
|
||||
# EXACTLY once (double injection would emit two `=destroy`/`=copy` runs).
|
||||
icProfStart(tLowerOwned)
|
||||
for s in moduleSymbolStubs(ast.program, FileIndex modPos):
|
||||
if ownsRuntimeRoutine(s, modPos):
|
||||
# REUSE path (`icReuseSemLowering` ON): a routine already transformed during
|
||||
@@ -545,6 +514,8 @@ proc generateLowerStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
# into the `.t.nif`; `cg` re-attaches them so `injectDestructorCalls` resolves
|
||||
# the loaded env's `=destroy`. Iterate to a fixpoint: a hook body can lift
|
||||
# further hooks (a field's `=destroy`).
|
||||
icProfStop(tLowerOwned)
|
||||
icProfStart(tLowerHooks)
|
||||
var hooks: seq[LogEntry] = @[]
|
||||
var i = opsLogStart
|
||||
while i < g.opsLog.len:
|
||||
@@ -564,9 +535,11 @@ proc generateLowerStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
# Re-serialize the whole module to its suffix-based `.t.nif` (the path
|
||||
# `toNifFilename` resolves for the cg/emit stages). `writeLoweredModule` seals
|
||||
# routines itself.
|
||||
icProfStop(tLowerHooks)
|
||||
let suffix = cachedModuleSuffix(g.config, FileIndex modPos)
|
||||
let wholeArtifact = toGeneratedFile(g.config, AbsoluteFile(suffix), ".t.bif").string
|
||||
writeLoweredModule(ast.program, g.config, target, hooks, wholeArtifact)
|
||||
timed tLowerWrite:
|
||||
writeLoweredModule(ast.program, g.config, target, hooks, wholeArtifact)
|
||||
if isDefined(g.config, "icDceCheck"):
|
||||
stderr.writeLine "[icLower] " & extractFilename(wholeArtifact) & " " &
|
||||
$hooks.len & " hooks"
|
||||
@@ -587,13 +560,19 @@ proc visitDep(suffix: string;
|
||||
let bm = bl.mods[pm.module.position]
|
||||
if bm != nil: ordered.add bm
|
||||
|
||||
proc cgGenerateModule(g: ModuleGraph; target: PrecompiledModule)
|
||||
proc cgFinishModule(g: ModuleGraph; target: PrecompiledModule;
|
||||
modules: seq[PrecompiledModule];
|
||||
precompSys: PrecompiledModule)
|
||||
|
||||
proc generateCgStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
## Per-module backend codegen (`--icBackendStage:cg --icBackendModule:<suffix>`):
|
||||
## generate C for the single module named by `icBackendModule` and write only
|
||||
## its `.c.nif` artifact (no merge, no `.c` render, no cc/link — those are
|
||||
## separate nifmake rules).
|
||||
## Backend codegen for this invocation's batch
|
||||
## (`--icBackendStage:cg --icBackendModules:<a,b,c>`): generate C for each
|
||||
## member and write its `.c.nif` artifact (no merge, no `.c` render, no
|
||||
## cc/link — those are separate nifmake rules).
|
||||
##
|
||||
## `findPendingModule` routes every demand into the target (emit-everywhere).
|
||||
## `findPendingModule` routes a demand to its owner when the owner is in the
|
||||
## batch and into the demanding TU otherwise (emit-everywhere).
|
||||
##
|
||||
## A NON-main target loads only its own import closure (`loadDepClosure`); the
|
||||
## whole program is no longer pulled into every parallel `cg` process. The main
|
||||
@@ -603,12 +582,11 @@ proc generateCgStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
# gate `newSymNode`'s lazy-type marking to this stage only (see astdef)
|
||||
nifcBackendActive = true
|
||||
let mainSuffix = cachedModuleSuffix(g.config, mainFileIdx)
|
||||
let targetIsMain = g.config.icBackendModule.len == 0 or
|
||||
g.config.icBackendModule == mainSuffix
|
||||
let batch = backendBatch(g.config, mainSuffix)
|
||||
var modules: seq[PrecompiledModule]
|
||||
var precompSys: PrecompiledModule
|
||||
var target: PrecompiledModule
|
||||
if targetIsMain:
|
||||
var targets: seq[PrecompiledModule]
|
||||
if batch.isMain:
|
||||
var nifFiles: seq[string]
|
||||
(modules, precompSys, nifFiles) = loadBackendModules(g, mainFileIdx)
|
||||
if modules.len == 0:
|
||||
@@ -619,24 +597,90 @@ proc generateCgStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
# MERGE stage recomputes the one program-wide live set across all `.c.nif`s.
|
||||
# Running a whole-program liveness pass over all ~260 NIFs in the main `cg`
|
||||
# would cost ~900 MB for a result the merge stage throws away.
|
||||
target = findTargetModule(g, modules, precompSys, g.config.icBackendModule)
|
||||
targets = @[findTargetModule(g, modules, precompSys, mainSuffix)]
|
||||
else:
|
||||
# No whole-program load, hence no whole-program DCE: the target emits its
|
||||
# No whole-program load, hence no whole-program DCE: each member emits its
|
||||
# full demanded closure and the merge stage drops what is globally dead.
|
||||
(modules, precompSys, target) = loadDepClosure(g, g.config.icBackendModule)
|
||||
if target.module == nil:
|
||||
rawMessage(g.config, errGenerated,
|
||||
"per-module codegen: module not found for suffix: " & g.config.icBackendModule)
|
||||
return
|
||||
(modules, precompSys, targets) = block:
|
||||
icProfStart(tLoadClosure)
|
||||
let r = loadDepClosure(g, batch.members)
|
||||
icProfStop(tLoadClosure)
|
||||
r
|
||||
for i, target in targets:
|
||||
if target.module == nil:
|
||||
rawMessage(g.config, errGenerated,
|
||||
"per-module codegen: module not found for suffix: " &
|
||||
(if i < batch.members.len: batch.members[i] else: mainSuffix))
|
||||
return
|
||||
|
||||
let bl = BModuleList(g.backend)
|
||||
# Declare which modules this process writes a TU for, BEFORE any code is
|
||||
# generated: `findPendingModule` consults the set on the very first demand, so
|
||||
# a member added later would have its definitions routed into whichever TU
|
||||
# asked first — which is precisely what the set exists to prevent.
|
||||
for target in targets:
|
||||
bl.icEmitted.incl target.module.position
|
||||
|
||||
# Generate EVERY member before finishing ANY of them. `finishModule` closes a
|
||||
# TU (`finalCodegenActions` puts it in `modulesClosed`), and a later member's
|
||||
# codegen routes definitions it does not own INTO an earlier member's TU — see
|
||||
# `findPendingModule`. Finishing as we went closed those TUs first, and the
|
||||
# definitions that arrived afterwards were silently dropped: 18 undefined
|
||||
# symbols at link, all of them `_u`-flagged uniques whose owner happened to
|
||||
# sort earlier in its batch.
|
||||
timed tCgGen:
|
||||
for target in targets:
|
||||
cgGenerateModule(g, target)
|
||||
timed tCgFinish:
|
||||
for target in targets:
|
||||
cgFinishModule(g, target, modules, precompSys)
|
||||
|
||||
# Writes each batch member's `.c.nif` (every other loaded module's TU is empty,
|
||||
# so `cgenWriteModules` emits no artifact for it). cc/link are NOT run here.
|
||||
timed tCgWrite:
|
||||
cgenWriteModules(g.backend, g.config)
|
||||
|
||||
# Always leave a `.c.nif` for every member, even one whose module has no code
|
||||
# (a leaf library whose procs all emit into their users): the nifmake graph
|
||||
# declares a `.c.nif` output per member, so a missing one would re-fire the
|
||||
# rule forever. An empty artifact renders to an empty `.c`.
|
||||
for target in targets:
|
||||
let tb = bl.mods[target.module.position]
|
||||
if tb != nil:
|
||||
let artifact = getCFile(tb).string & ".nif"
|
||||
if not fileExists(artifact):
|
||||
writeCnifArtifact("", artifact,
|
||||
semmedNif = toNifFilename(g.config, FileIndex target.module.position),
|
||||
moduleBase = $getSomeNameForModule(tb))
|
||||
|
||||
proc cgGenerateModule(g: ModuleGraph; target: PrecompiledModule) =
|
||||
## Generate ONE batch member's code. Does NOT finish its TU — see the caller.
|
||||
# The `lower` stage already wrote each module's transformed bodies + lifted
|
||||
# hooks into its `.t.nif`, which the loaders above read directly (toNifFilename
|
||||
# resolves the `.t.nif`); transformed bodies arrive via loadSymFromCursor and
|
||||
# lifted hooks via moduleFromNifFile's registerLoadedHooks. Nothing to apply.
|
||||
generateCodeForModule(g, target)
|
||||
let bl = BModuleList(g.backend)
|
||||
if sfMainModule notin target.module.flags:
|
||||
# This module's top-level `var`s with a `=destroy` registered their teardown
|
||||
# in `graph.globalDestructors` during `genTopLevelStmt` above. Main's `cg` is
|
||||
# a different process and never sees them, so emit them as this TU's own
|
||||
# exported proc and announce the name in the meta head. Stays HERE, in the
|
||||
# generate pass: it consumes the destructors this module just registered.
|
||||
let tbm = bl.mods[target.module.position]
|
||||
if tbm != nil:
|
||||
tbm.icGlobalDtorName = genIcModuleDestroyGlobals(g, tbm)
|
||||
|
||||
proc cgFinishModule(g: ModuleGraph; target: PrecompiledModule;
|
||||
modules: seq[PrecompiledModule];
|
||||
precompSys: PrecompiledModule) =
|
||||
## Close ONE batch member's translation unit, once every member of the batch
|
||||
## has generated. The artifact write is not here: `cgenWriteModules` is a
|
||||
## single whole-list operation the caller runs after the whole batch.
|
||||
let bl = BModuleList(g.backend)
|
||||
# The main module also owns the whole-program method dispatchers + NimMain.
|
||||
if sfMainModule in target.module.flags:
|
||||
icProfStart(tCgInit)
|
||||
emitMethodDispatchers(g)
|
||||
# NimMain (generated when the main module is finished) must call every other
|
||||
# module's init/datInit. Those translation units are produced by their own
|
||||
@@ -695,24 +739,22 @@ proc generateCgStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
for m in ordered:
|
||||
let heads = readCnifHeads(getCFile(m).string & ".nif")
|
||||
registerReusedModuleToMain(bl, m, heads.initRequired, heads.datInitRequired)
|
||||
if heads.globalDtor.len > 0: g.icModuleDtors.add heads.globalDtor
|
||||
# `ordered` is dependency (post-order) init order; teardown runs in reverse,
|
||||
# so an importer's globals are destroyed before the ones it may still point
|
||||
# at. This mirrors whole-program cgen, which walks its single accumulated
|
||||
# `globalDestructors` list backwards. Main's own destructors come first and
|
||||
# are added by `finalCodegenActions` itself.
|
||||
reverse g.icModuleDtors
|
||||
icProfStop(tCgInit)
|
||||
let tb = bl.mods[target.module.position]
|
||||
if tb != nil:
|
||||
finishModule(g, tb)
|
||||
|
||||
# Writes only the target's `.c.nif` (every other loaded module's TU is empty,
|
||||
# so `cgenWriteModules` emits no artifact for it). cc/link are NOT run here.
|
||||
cgenWriteModules(g.backend, g.config)
|
||||
|
||||
# Always leave a `.c.nif` for the target, even when the module has no code
|
||||
# (a leaf library whose procs all emit into their users): the per-module
|
||||
# nifmake graph declares one `.c.nif` output per `cg` rule, so a missing one
|
||||
# would re-fire the rule forever. An empty artifact renders to an empty `.c`.
|
||||
if tb != nil:
|
||||
let artifact = getCFile(tb).string & ".nif"
|
||||
if not fileExists(artifact):
|
||||
writeCnifArtifact("", artifact,
|
||||
semmedNif = toNifFilename(g.config, FileIndex target.module.position),
|
||||
moduleBase = $getSomeNameForModule(tb))
|
||||
# Record this module's C compile/link directives next to its `.c` so the
|
||||
# `link` stage can recover them without loading the module graph. See
|
||||
# `replayer.writeBackendActions`.
|
||||
writeBackendActions(g, target.module, target.topLevel,
|
||||
getCFile(tb).string & BackendActionsExt)
|
||||
|
||||
proc generateMergeStage(g: ModuleGraph) =
|
||||
## Per-module backend merge (`--icBackendStage:merge`): a pure artifact
|
||||
@@ -724,8 +766,19 @@ proc generateMergeStage(g: ModuleGraph) =
|
||||
## in-process first-claimant/DCE coordination.
|
||||
let nimcache = getNimcacheDir(g.config).string
|
||||
var files: seq[string] = @[]
|
||||
for artifact in walkFiles(nimcache / "*.c.nif"):
|
||||
files.add artifact
|
||||
# The driver lists the live modules' artifacts explicitly (deps.nim's
|
||||
# `writeLiveModules`); only fall back to globbing when that manifest is
|
||||
# absent (a cache written by an older compiler). Globbing merges whatever
|
||||
# `.c.nif` happens to sit in the directory, which is wrong the moment the
|
||||
# cache is shared with another program — see `LiveModulesFile`.
|
||||
let manifest = nimcache / LiveModulesFile
|
||||
if fileExists(manifest):
|
||||
for line in lines(manifest):
|
||||
let p = line.strip()
|
||||
if p.len > 0: files.add p
|
||||
else:
|
||||
for artifact in walkFiles(nimcache / ("*" & icCFileExt(g.config) & ".nif")):
|
||||
files.add artifact
|
||||
sort files
|
||||
let decision = computeMergeDecision(files)
|
||||
if decision.broken:
|
||||
@@ -738,16 +791,19 @@ proc generateMergeStage(g: ModuleGraph) =
|
||||
" live: " & $decision.live.len & " defs: " & $decision.defs &
|
||||
" liveDefs: " & $decision.liveDefs & " owned: " & $decision.owners.len
|
||||
|
||||
proc emitOneModule(g: ModuleGraph; mainFileIdx: FileIndex; member: string;
|
||||
isMain: bool; decision: MergeDecision)
|
||||
|
||||
proc generateEmitStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
## Per-module backend emit (`--icBackendStage:emit --icBackendModule:<suffix>`):
|
||||
## Backend emit for this invocation's batch
|
||||
## (`--icBackendStage:emit --icBackendModules:<a,b,c>`):
|
||||
## render the target module's final `.c` from its `.c.nif` and the merge
|
||||
## decision. Loads the target the same way `cg` does so `getCFile` returns the
|
||||
## identical path `cg` wrote to (the main module's source-vs-suffix aliasing in
|
||||
## particular); no codegen runs. A non-main target loads only its own closure
|
||||
## (`loadDepClosure`) so emit, like `cg`, stays bounded under parallel fan-out.
|
||||
let mainSuffix = cachedModuleSuffix(g.config, mainFileIdx)
|
||||
let targetIsMain = g.config.icBackendModule.len == 0 or
|
||||
g.config.icBackendModule == mainSuffix
|
||||
let batch = backendBatch(g.config, mainSuffix)
|
||||
# emit renders a module's final `.c` PURELY from its own `.c.nif` and the merge
|
||||
# decision (see `renderCFromArtifact` — text filtering, no AST is touched). It
|
||||
# used to load the target's whole transitive import closure as BModules solely
|
||||
@@ -760,21 +816,38 @@ proc generateEmitStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
# path directly instead — the SAME pure computation `deps.nim.backendCFile`
|
||||
# uses to DECLARE this stage's output (`getCFile` == that formula) — so an emit
|
||||
# process loads nothing and the fire-all costs process-startup, not a graph load.
|
||||
let cfilename =
|
||||
if targetIsMain: AbsoluteFile toFullPath(g.config, mainFileIdx)
|
||||
else: AbsoluteFile g.config.icBackendModule
|
||||
let cfile = changeFileExt(completeCfilePath(g.config,
|
||||
mangleModuleName(g.config, cfilename).AbsoluteFile), ".nim.c").string
|
||||
let artifact = cfile & ".nif"
|
||||
if not fileExists(artifact):
|
||||
rawMessage(g.config, errGenerated,
|
||||
"per-module emit: missing .c.nif artifact for suffix: " & g.config.icBackendModule)
|
||||
return
|
||||
# The decision is read ONCE for the batch: it is a whole-program artifact, and
|
||||
# re-reading it per member was a per-process cost the batch exists to remove.
|
||||
let decision = readMergeDecision(getNimcacheDir(g.config).string / MergeDecisionFile)
|
||||
if decision.broken:
|
||||
rawMessage(g.config, errGenerated,
|
||||
"per-module emit: missing or unparsable merge decision " & MergeDecisionFile)
|
||||
return
|
||||
let members = if batch.members.len == 0: @[mainSuffix] else: batch.members
|
||||
for member in members:
|
||||
# Per MEMBER, not per batch. `backendBatch.isMain` answers "is this
|
||||
# invocation the main-module invocation", which is the right question for
|
||||
# `lower`/`cg` (main loads the whole program, so it is never batched with
|
||||
# anything). emit has no such constraint and batches freely, so main can sit
|
||||
# in a batch with others — and then the batch-wide flag sent main's `.c` to
|
||||
# the path derived from its SUFFIX rather than from its source file, and its
|
||||
# `.c` was never written.
|
||||
emitOneModule(g, mainFileIdx, member, member == mainSuffix, decision)
|
||||
|
||||
proc emitOneModule(g: ModuleGraph; mainFileIdx: FileIndex; member: string;
|
||||
isMain: bool; decision: MergeDecision) =
|
||||
## Render ONE batch member's final `.c` from its `.c.nif` and the batch's
|
||||
## merge decision.
|
||||
let cfilename =
|
||||
if isMain: AbsoluteFile toFullPath(g.config, mainFileIdx)
|
||||
else: AbsoluteFile member
|
||||
let cfile = changeFileExt(completeCfilePath(g.config,
|
||||
mangleModuleName(g.config, cfilename).AbsoluteFile), icCFileExt(g.config)).string
|
||||
let artifact = cfile & ".nif"
|
||||
if not fileExists(artifact):
|
||||
rawMessage(g.config, errGenerated,
|
||||
"per-module emit: missing .c.nif artifact for suffix: " & member)
|
||||
return
|
||||
var dropped = 0
|
||||
let code = renderCFromArtifact(artifact, decision, extractFilename(artifact), dropped)
|
||||
# Write the `.c` content-stably. `merge` re-runs on any edit and bumps the
|
||||
@@ -788,6 +861,15 @@ proc generateEmitStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
# up-to-date check, not a shared prerequisite in nifmake's mtime ordering.
|
||||
if not fileExists(cfile) or readFile(cfile) != code:
|
||||
writeFile(cfile, code)
|
||||
# ... but nifmake needs SOME output whose mtime proves "this rule ran since its
|
||||
# inputs last moved". With the `.c` as the only output, the content-stable write
|
||||
# above is indistinguishable from not having run: `merge` rewrites the decision
|
||||
# file unconditionally, so every `emit` whose `.c` came out byte-identical stays
|
||||
# older than a declared input and re-fires on every warm build from then on
|
||||
# (measured: all 218 emit rules of a 219-module program, on a NO-OP build).
|
||||
# The stamp is written unconditionally and is the rule's freshness proof; the
|
||||
# `.c` keeps its content-stable mtime so `callCCompiler` still reuses the `.o`.
|
||||
writeFile(cfile & ".stamp", $code.len & " " & $dropped & "\n")
|
||||
if isDefined(g.config, "icDceCheck"):
|
||||
stderr.writeLine "[icEmit] " & extractFilename(cfile) & " dropped " &
|
||||
$dropped & " bodies (" & $code.len & " bytes)"
|
||||
@@ -796,64 +878,74 @@ proc generateLinkStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
## Per-module backend link (`--icBackendStage:link`): the `emit` stages have
|
||||
## written every module's `.c`; register them and run the C compiler + linker
|
||||
## once via `extccomp.callCCompiler` (which parallelizes the per-file cc and
|
||||
## skips up-to-date objects itself). No codegen runs — the graph is loaded only
|
||||
## so `getCFile` yields each module's emitted `.c` path.
|
||||
let (modules, precompSys, _) = loadBackendModules(g, mainFileIdx)
|
||||
if modules.len == 0:
|
||||
rawMessage(g.config, errGenerated,
|
||||
"Cannot load NIF file for main module: " & toFullPath(g.config, mainFileIdx))
|
||||
return
|
||||
# The per-module `cg` processes each collect their module's C compile/link
|
||||
# directives (`{.passL: "-lm".}` etc.) via `replayBackendActions`, but those
|
||||
# live in the cg process and never reach this separate link process. Re-collect
|
||||
# every loaded module's directives here so the final `callCCompiler` sees them
|
||||
# (without this, math's `-lm` is lost → undefined `floor`/`pow`/… at link).
|
||||
for m in modules:
|
||||
replayBackendActions(g, m.module, m.topLevel)
|
||||
if precompSys.module != nil:
|
||||
replayBackendActions(g, precompSys.module, precompSys.topLevel)
|
||||
let bl = BModuleList(g.backend)
|
||||
## skips up-to-date objects itself). No codegen runs and NO MODULE GRAPH IS
|
||||
## LOADED.
|
||||
##
|
||||
## It used to load the whole import closure (`loadBackendModules`) for two
|
||||
## things only: each module's `.c` path via `getCFile`, and its recorded C
|
||||
## directives via `replayBackendActions`. That was 3.7s of the ~11s serial
|
||||
## backend critical path on a 219-module program — a whole-program
|
||||
## deserialization to recover a list of paths and a handful of strings. Both
|
||||
## are now read from artifacts the earlier stages already produce:
|
||||
## * the driver's `LiveModulesFile` manifest lists every live module's
|
||||
## `.c.nif`, and the `.c` sits beside it (`emit`'s output);
|
||||
## * each module's `cg` wrote its directives to a `.cflags` sidecar.
|
||||
let nimcache = getNimcacheDir(g.config).string
|
||||
var cfiles: seq[string] = @[]
|
||||
let manifest = nimcache / LiveModulesFile
|
||||
if fileExists(manifest):
|
||||
for line in lines(manifest):
|
||||
let p = line.strip()
|
||||
if p.len > 0 and p.endsWith(".nif"): cfiles.add p[0 ..< p.len - ".nif".len]
|
||||
else:
|
||||
# A cache written by an older compiler has no manifest; fall back to the
|
||||
# `.c` files sitting next to the artifacts.
|
||||
for artifact in walkFiles(nimcache / ("*" & icCFileExt(g.config) & ".nif")):
|
||||
cfiles.add artifact[0 ..< artifact.len - ".nif".len]
|
||||
sort cfiles
|
||||
|
||||
var addedCFiles = initHashSet[string]()
|
||||
for m in bl.mods:
|
||||
if m != nil:
|
||||
let cfile = getCFile(m)
|
||||
# Only modules that are their own cg/emit target produced a `.c`; the rest
|
||||
# (extra members of system's closure that no build rule targets) had their
|
||||
# code emit-everywhere'd into the targets, so they have no file to compile.
|
||||
if not fileExists(cfile.string): continue
|
||||
addedCFiles.incl extractFilename(cfile.string)
|
||||
var cf = Cfile(nimname: m.module.name.s, cname: cfile,
|
||||
obj: completeCfilePath(g.config, toObjFile(g.config, cfile)),
|
||||
flags: {})
|
||||
# `addExternalFileToCompile` (not `addFileToCompile`) gates each `.c` on its
|
||||
# SHA1 footprint: an unchanged `.c` keeps its `.o` and is flagged Cached, so
|
||||
# `callCCompiler` skips its compile but still links the existing object. This
|
||||
# is what makes a localized edit recompile only the handful of `.c`s the
|
||||
# `emit` stage actually rewrote, instead of every object every time — the
|
||||
# final piece of per-module backend incrementality after the merge barrier.
|
||||
addExternalFileToCompile(g.config, cf)
|
||||
for cpath in cfiles:
|
||||
# Only modules that are their own cg/emit target produced a `.c`; the rest
|
||||
# had their code emit-everywhere'd into the targets, so there is nothing to
|
||||
# compile for them.
|
||||
if not fileExists(cpath): continue
|
||||
addedCFiles.incl extractFilename(cpath)
|
||||
# The directives this module recorded (`{.passL: "-lm".}` etc.); without
|
||||
# them math's `-lm` is lost -> undefined `floor`/`pow`/… at link.
|
||||
applyBackendActions(g, cpath & BackendActionsExt)
|
||||
let cfile = AbsoluteFile cpath
|
||||
var cf = Cfile(nimname: splitFile(cfile).name, cname: cfile,
|
||||
obj: completeCfilePath(g.config, toObjFile(g.config, cfile)),
|
||||
flags: {})
|
||||
# `addExternalFileToCompile` (not `addFileToCompile`) gates each `.c` on its
|
||||
# SHA1 footprint: an unchanged `.c` keeps its `.o` and is flagged Cached, so
|
||||
# `callCCompiler` skips its compile but still links the existing object. This
|
||||
# is what makes a localized edit recompile only the handful of `.c`s the
|
||||
# `emit` stage actually rewrote, instead of every object every time.
|
||||
addExternalFileToCompile(g.config, cf)
|
||||
|
||||
# deps.nim's static scanner can keep a CONDITIONALLY-imported module as a build
|
||||
# node (e.g. `net`'s `when defineSsl: import openssl`, or a `when defined(os)`
|
||||
# import) that the NIF-`deps` walk above never reaches because the condition is
|
||||
# off. Such a node still emitted a `.c`, and it can OWN a live generic instance
|
||||
# that a REACHABLE module reuses (openssl owns `toHex[uint8]`, reused by
|
||||
# `strutils.escape`) — so its body must be at link or that reference is
|
||||
# node (e.g. `net`'s `when defineSsl: import openssl`) that the manifest above
|
||||
# may not cover. Such a node still emitted a `.c`, and it can OWN a live generic
|
||||
# instance that a REACHABLE module reuses (openssl owns `toHex[uint8]`, reused
|
||||
# by `strutils.escape`) — so its body must be at link or that reference is
|
||||
# undefined. Link every emitted `.c` the merge decision says OWNS a LIVE symbol;
|
||||
# a node that owns nothing live (a Windows-only winsock node on Linux) is
|
||||
# correctly skipped.
|
||||
block:
|
||||
let nimcache = getNimcacheDir(g.config).string
|
||||
let decision = readMergeDecision(nimcache / MergeDecisionFile)
|
||||
if not decision.broken:
|
||||
var liveOwners = initHashSet[string]()
|
||||
for cname, owner in decision.owners:
|
||||
if owner.endsWith(".c.nif") and cname in decision.live:
|
||||
if owner.endsWith(icCFileExt(g.config) & ".nif") and cname in decision.live:
|
||||
liveOwners.incl owner
|
||||
for owner in liveOwners:
|
||||
let cbase = owner[0 ..< owner.len - ".nif".len] # "@m….nim.c.nif" -> ".c"
|
||||
if addedCFiles.containsOrIncl(cbase): continue
|
||||
let cfile = AbsoluteFile(nimcache / cbase)
|
||||
if not fileExists(cfile.string): continue
|
||||
applyBackendActions(g, cfile.string & BackendActionsExt)
|
||||
var cf = Cfile(nimname: cbase, cname: cfile,
|
||||
obj: completeCfilePath(g.config, toObjFile(g.config, cfile)),
|
||||
flags: {})
|
||||
@@ -864,20 +956,27 @@ proc generateLinkStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
proc generateCode*(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
## Main entry point for NIF-based C code generation.
|
||||
## Traverses the module dependency graph and generates C code.
|
||||
when defined(icBNodeProf): profStageName = g.config.icBackendStage
|
||||
if g.config.icBackendStage == "lower":
|
||||
generateLowerStage(g, mainFileIdx)
|
||||
timed tStage: generateLowerStage(g, mainFileIdx)
|
||||
return
|
||||
elif g.config.icBackendStage == "cg":
|
||||
generateCgStage(g, mainFileIdx)
|
||||
timed tStage: generateCgStage(g, mainFileIdx)
|
||||
return
|
||||
elif g.config.icBackendStage == "merge":
|
||||
generateMergeStage(g)
|
||||
timed tStage:
|
||||
timed tMergeStage:
|
||||
generateMergeStage(g)
|
||||
return
|
||||
elif g.config.icBackendStage == "emit":
|
||||
generateEmitStage(g, mainFileIdx)
|
||||
timed tStage:
|
||||
timed tEmitRender:
|
||||
generateEmitStage(g, mainFileIdx)
|
||||
return
|
||||
elif g.config.icBackendStage == "link":
|
||||
generateLinkStage(g, mainFileIdx)
|
||||
timed tStage:
|
||||
timed tLinkStage:
|
||||
generateLinkStage(g, mainFileIdx)
|
||||
return
|
||||
else:
|
||||
rawMessage(g.config, errGenerated,
|
||||
|
||||
247
compiler/nifstreams.nim
Normal file
247
compiler/nifstreams.nim
Normal file
@@ -0,0 +1,247 @@
|
||||
## nifstreams — the classic NIF streaming surface, used ONLY by this compiler's
|
||||
## IC modules: ast2nif, deps, modulegraphs and pipelines import it and must keep
|
||||
## compiling unchanged across nimony's own refactorings.
|
||||
##
|
||||
## It used to live in `dist/nimony/src/lib`, which is where the rest of the NIF
|
||||
## stack still is. It does not belong there: nimony's own code imports nifpools
|
||||
## (via nifprelude) and is under standing orders never to import this file, so
|
||||
## nothing over there ever exercised it — which is exactly how it came to hand
|
||||
## out `TagLit` where every caller here tests for `ParLe` (see `next`), silently
|
||||
## emptying the IC build graph. A compatibility shim with exactly one consumer
|
||||
## belongs in the consumer's repo, where its tests run and its contract is
|
||||
## somebody's problem.
|
||||
##
|
||||
## Everything it adapts (`nifpools`, `nifreader`, `lineinfos`) still comes from
|
||||
## `dist/nimony`; only the adapter moved.
|
||||
##
|
||||
## Everything here is an honest adapter, not a fake:
|
||||
## * Floats get a REAL interning pool: `pool.floats.getOrIncl` returns a
|
||||
## `FloatId` index, `floatToken` packs it into a genuine `FloatLit` NifToken
|
||||
## (transit-only: it must never enter a TokenBuf, whose float encoding is
|
||||
## inline multi-token), and `pool.floats[t.floatId]` decodes it — lossless.
|
||||
## * `Stream`/`next` wrap the textual nifreader; the unified NifKind has real
|
||||
## `ParLe`/`ParRi`/`EofToken` members, so structural scanners (deps.nim)
|
||||
## see the exact classic kinds. Ident/StringLit/Symbol payloads are interned
|
||||
## into the global `pool`, so `pool.strings[t.litId]` works as before.
|
||||
## Number tokens keep their KIND only (a 4-byte token cannot always carry
|
||||
## the value); classic scanners never read those payloads.
|
||||
|
||||
import std / tables
|
||||
import "../dist/nimony/src/lib" / nifpools
|
||||
# `except`: the frontend went all-NifLineInfo; the classic side keeps speaking
|
||||
# PackedLineInfo, so nifpools' same-name/same-params variants must not leak
|
||||
# through (`info(n: NifToken)` differs only in return type, `NoLineInfo` is a
|
||||
# same-name const of a different type — either would be ambiguous or wrong for
|
||||
# ast2nif). The classic replacements are defined below / come from lineinfos.
|
||||
# `tagId` is excluded for a different reason: nifpools decodes the 9-bit field
|
||||
# of a real `TagLit`, but this surface hands out `ParLe` tokens whose tag id
|
||||
# fills the whole 28-bit payload (see `next`), so the decode below is the only
|
||||
# correct one here.
|
||||
export nifpools except info, NoLineInfo, tagId
|
||||
import "../dist/nimony/src/lib" / lineinfos
|
||||
export lineinfos
|
||||
|
||||
from "../dist/nimony/src/lib" / nifreader import Reader, ExpandedToken, decodeStr
|
||||
|
||||
# ── Classic names the Nim compiler side still uses ───────────────────────
|
||||
|
||||
type
|
||||
PackedToken* = NifToken ## ast2nif still says PackedToken
|
||||
|
||||
# Raw payload decodes, sound ONLY on this surface. Every token here comes from
|
||||
# `next` or the classic `symToken`/`strToken`/`identToken` constructors, which
|
||||
# intern EVERY literal — including names of at most `StrInlineMaxLen` bytes,
|
||||
# which the nifcore builders would instead store inside the token. On such an
|
||||
# inline token the payload is packed bytes, not an id, so nifpools (nimony's own
|
||||
# surface, where buffers come from the builders) deliberately has no equivalent:
|
||||
# there it must go through a `Cursor`, which handles both encodings.
|
||||
proc tagId*(n: NifToken): TagId {.inline.} = TagId(uoperand(n))
|
||||
## Classic `ParLe` tokens (see `next`) keep the tag id in the full 28-bit
|
||||
## payload rather than in `TagLit`'s 9-bit field: `globalTags` already holds
|
||||
## 355 tags before the Nim compiler registers its own dialect, so a 512-tag
|
||||
## ceiling is not a ceiling this surface can live under.
|
||||
proc litId*(n: NifToken): StrId {.inline.} = StrId(uoperand(n) shr 1)
|
||||
proc symId*(n: NifToken): SymId {.inline.} = SymId(uoperand(n) shr 1)
|
||||
proc litId*(c: Cursor): StrId {.inline.} = strId(c)
|
||||
proc firstSon*(n: Cursor): Cursor {.inline.} = childCursor(n)
|
||||
|
||||
var lineMan*: LineInfoManager
|
||||
## The classic packed line-info side channel (`pool.man`). Frontend code no
|
||||
## longer uses it — it lives here purely for ast2nif's writer, which packs
|
||||
## `TLineInfo` into `PackedLineInfo` and unpacks on emit.
|
||||
|
||||
template files*(p: Pool): untyped = p.filenames
|
||||
template tags*(p: Pool): untyped = globalTags.tags
|
||||
template man*(p: Pool): untyped = lineMan
|
||||
|
||||
proc info*(n: NifToken): PackedLineInfo {.inline.} = lineinfos.NoLineInfo
|
||||
## Classic tokens carried their line info inline; a bare 4-byte nifcore
|
||||
## token cannot, so reading it back yields `NoLineInfo` (ast2nif's
|
||||
## `emitInfo(t.info)` then emits nothing — matching the writer, which
|
||||
## attaches real positions at the builder level instead).
|
||||
|
||||
proc info*(c: Cursor): PackedLineInfo {.inline.} =
|
||||
## Classic packed view of a cursor's line info (ast2nif shadows this with
|
||||
## its own NifLineInfo template; kept for any other classic reader).
|
||||
let li = rawLineInfo(c)
|
||||
if li.file.isValid: pack(lineMan, li.file, li.line, li.col)
|
||||
else: lineinfos.NoLineInfo
|
||||
|
||||
type
|
||||
IntId* = distinct int64 ## value carriers (nifcore stores inline)
|
||||
UIntId* = distinct uint64
|
||||
|
||||
## Identity proxies: the id already carries the value, `[]` returns it.
|
||||
IntegersProxy* = object
|
||||
UIntegersProxy* = object
|
||||
|
||||
func `==`*(a, b: IntId): bool {.borrow.}
|
||||
func `==`*(a, b: UIntId): bool {.borrow.}
|
||||
|
||||
template integers*(p: Pool): IntegersProxy = IntegersProxy()
|
||||
template uintegers*(p: Pool): UIntegersProxy = UIntegersProxy()
|
||||
|
||||
template `[]`*(x: IntegersProxy; id: IntId): int64 = int64(id)
|
||||
template `[]`*(x: UIntegersProxy; id: UIntId): uint64 = uint64(id)
|
||||
|
||||
# nifcore stores integers inline: the "id" is the value itself.
|
||||
template getOrIncl*(x: IntegersProxy; v: int64): IntId = IntId(v)
|
||||
template getOrIncl*(x: UIntegersProxy; v: uint64): UIntId = UIntId(v)
|
||||
|
||||
proc intId*(n: NifToken): IntId {.inline.} = IntId(n.soperand)
|
||||
proc uintId*(n: NifToken): UIntId {.inline.} = UIntId(uoperand(n))
|
||||
proc intId*(c: Cursor): IntId {.inline.} = IntId(intVal(c))
|
||||
proc uintId*(c: Cursor): UIntId {.inline.} = UIntId(uintVal(c))
|
||||
|
||||
proc addIntLit*(dest: var TokenBuf; id: IntId; info: PackedLineInfo) =
|
||||
addIntLit(dest, int64(id))
|
||||
if info.isValid:
|
||||
let u = unpack(lineMan, info)
|
||||
appendLineInfo(dest, u.file, u.line, u.col)
|
||||
|
||||
# Classic single-token constructors with a (dropped) line-info argument.
|
||||
proc strToken*(s: StrId; info: PackedLineInfo): NifToken {.inline.} = strLitToken(s)
|
||||
proc symToken*(id: SymId; info: PackedLineInfo): NifToken {.inline.} = symToken(id)
|
||||
proc identToken*(id: StrId; info: PackedLineInfo): NifToken {.inline.} = identToken(id)
|
||||
proc dotToken*(info: PackedLineInfo): NifToken {.inline.} = dotToken()
|
||||
proc charToken*(ch: char; info: PackedLineInfo): NifToken {.inline.} = charToken(ch)
|
||||
|
||||
# ── Classic interned float literals (ast2nif) ────────────────────────────
|
||||
|
||||
type
|
||||
FloatId* = distinct uint32 ## 1-based index into the global float pool
|
||||
FloatPool* = object
|
||||
values: seq[float64]
|
||||
lookup: Table[uint64, uint32] # bit pattern -> 1-based id
|
||||
|
||||
func `==`*(a, b: FloatId): bool {.borrow.}
|
||||
|
||||
var globalFloats*: FloatPool
|
||||
|
||||
template floats*(p: Pool): var FloatPool = globalFloats
|
||||
|
||||
proc getOrIncl*(fp: var FloatPool; v: float64): FloatId =
|
||||
let bits = cast[uint64](v)
|
||||
let existing = fp.lookup.getOrDefault(bits, 0'u32)
|
||||
if existing != 0'u32:
|
||||
result = FloatId(existing)
|
||||
else:
|
||||
fp.values.add v
|
||||
let id = uint32(fp.values.len)
|
||||
fp.lookup[bits] = id
|
||||
result = FloatId(id)
|
||||
|
||||
proc `[]`*(fp: FloatPool; id: FloatId): float64 {.inline.} =
|
||||
fp.values[int(uint32(id)) - 1]
|
||||
|
||||
proc floatToken*(id: FloatId; info: PackedLineInfo): NifToken {.inline.} =
|
||||
## Transit-only token: carries the pool index so the receiver can decode it
|
||||
## via `pool.floats[t.floatId]`. It must never be appended to a TokenBuf
|
||||
## (nifcore stores floats inline as a multi-token encoding); the line info
|
||||
## is dropped like in the other classic token constructors.
|
||||
NifToken((uint32(id) shl KindBits) or uint32(FloatLit))
|
||||
|
||||
proc floatId*(n: NifToken): FloatId {.inline.} = FloatId(uoperand(n))
|
||||
|
||||
# ── Classic streaming text reader (deps.nim) ─────────────────────────────
|
||||
|
||||
type
|
||||
Stream* = object
|
||||
r*: Reader
|
||||
|
||||
proc parLeToken*(t: TagId): NifToken {.inline.} =
|
||||
## The classic surface's opening-tag token: kind `ParLe`, tag id in the
|
||||
## payload. Transit-only, like `floatToken` — a `ParLe` never appears in a
|
||||
## binary token stream, so this must not be appended to a TokenBuf.
|
||||
NifToken((uint32(t) shl KindBits) or uint32(ParLe))
|
||||
|
||||
proc open*(filename: string): Stream =
|
||||
Stream(r: nifreader.open(filename))
|
||||
|
||||
proc close*(s: var Stream) =
|
||||
nifreader.close(s.r)
|
||||
|
||||
proc next*(s: var Stream): NifToken =
|
||||
## One classic packed token per call. Pool-referencing kinds are interned
|
||||
## into the global `pool`/`globalTags`, so `.litId`/`.tagId` accessors and
|
||||
## `pool.strings[...]`/`pool.tags[...]` lookups behave exactly as classic
|
||||
## nifstreams did. Kinds without a pool payload come back kind-only.
|
||||
var t = default(ExpandedToken)
|
||||
nifreader.next(s.r, t)
|
||||
case t.tk
|
||||
of ParLe:
|
||||
# NOT `tagLitToken`: that would set the kind to `TagLit`, and every classic
|
||||
# structural scanner tests for `ParLe` (deps.nim walks the import graph that
|
||||
# way). Emitting `TagLit` here made every one of those tests silently fail —
|
||||
# the scanner saw an unknown token, skipped the subtree, and the Nim
|
||||
# compiler's IC build graph came out missing most of its edges.
|
||||
result = parLeToken(registerTag(globalTags, decodeStr(s.r, t)))
|
||||
of Ident:
|
||||
result = identToken(pool.strings.getOrIncl(decodeStr(s.r, t)))
|
||||
of StrLit:
|
||||
result = strLitToken(pool.strings.getOrIncl(decodeStr(s.r, t)))
|
||||
of Symbol:
|
||||
result = symToken(pool.syms.getOrIncl(decodeStr(s.r, t)))
|
||||
of SymbolDef:
|
||||
result = symdefToken(pool.syms.getOrIncl(decodeStr(s.r, t)))
|
||||
else:
|
||||
# ParRi/EofToken/DotToken/CharLit/numbers: correct kind, no payload.
|
||||
result = NifToken(uint32(t.tk))
|
||||
|
||||
when isMainModule:
|
||||
# `nim c -r compiler/nifstreams.nim`.
|
||||
#
|
||||
# The promise this checks: structural scanners see the CLASSIC kinds. Nim's deps.nim walks
|
||||
# the import graph by testing `t.kind == ParLe` and then reading
|
||||
# `pool.tags[t.tagId]`. Hand out nifcore's own `TagLit` instead and every one
|
||||
# of those tests falls through silently — the scanner treats the opener as an
|
||||
# unknown token, skips the subtree, and Nim's IC build graph comes out missing
|
||||
# most of its edges while each individual file still "parses" fine.
|
||||
import std / [os, syncio]
|
||||
from "../dist/nimony/src/lib" / nifreader import processDirectives
|
||||
from std / assertions import assert
|
||||
|
||||
let f = getTempDir() / "nifstreams_selftest.nif"
|
||||
syncio.writeFile f, "(.nif27)\n(stmts (import (infix / std (bracket os osproc))) (x \"s\" y))\n"
|
||||
|
||||
var kinds: seq[NifKind] = @[]
|
||||
var tagNames: seq[string] = @[]
|
||||
var lits: seq[string] = @[]
|
||||
var s = nifstreams.open(f)
|
||||
discard processDirectives(s.r)
|
||||
while true:
|
||||
let t = next(s)
|
||||
if t.kind == EofToken: break
|
||||
kinds.add t.kind
|
||||
case t.kind
|
||||
of ParLe: tagNames.add pool.tags[t.tagId]
|
||||
of Ident, StrLit: lits.add pool.strings[t.litId]
|
||||
else: discard
|
||||
nifstreams.close(s)
|
||||
removeFile f
|
||||
|
||||
assert tagNames == @["stmts", "import", "infix", "bracket", "x"], $tagNames
|
||||
assert lits == @["/", "std", "os", "osproc", "s", "y"], $lits
|
||||
assert ParRi in kinds, "closers must stay classic too"
|
||||
assert TagLit notin kinds, "an opener must arrive as ParLe, not TagLit"
|
||||
echo "success"
|
||||
@@ -120,7 +120,7 @@ proc handleCmdLine(cache: IdentCache; conf: ConfigRef) =
|
||||
# so `loadConfigs` replays it instead of re-parsing the `nim.cfg` chain — the
|
||||
# driver runs on the exact same config its children will. See icconfig.nim.
|
||||
when not defined(nimKochBootstrap):
|
||||
if conf.cmd in {cmdIc, cmdTrack}:
|
||||
if conf.cmd in {cmdIc, cmdTrack} or isIcDriver(conf):
|
||||
ensureIcConfig(conf)
|
||||
|
||||
var graph = newModuleGraph(cache, conf)
|
||||
|
||||
340
compiler/nodebridge.nim
Normal file
340
compiler/nodebridge.nim
Normal file
@@ -0,0 +1,340 @@
|
||||
#
|
||||
#
|
||||
# The Nim Compiler
|
||||
# (c) Copyright 2026 Andreas Rumpf
|
||||
#
|
||||
# See the file "copying.txt", included in this
|
||||
# distribution, for details about the copyright.
|
||||
#
|
||||
|
||||
## `PNode` <-> `TokenBuf`, in one process.
|
||||
##
|
||||
## WHY THIS EXISTS. The backend splits in two along a line that is not the one
|
||||
## the migration to `BNode` was drawn along. Passes that REWRITE — transf,
|
||||
## destructor injection, closure lifting, the tree the code generator builds as
|
||||
## it goes — construct new nodes, and a `Cursor` is a read cursor into a shared
|
||||
## token buffer, so they cannot be expressed against it and there is no reason
|
||||
## to try. Passes that READ want the cursor. The bridge is the seam between
|
||||
## them: a rewriting pass keeps producing a `PNode`, and anything that only
|
||||
## reads gets a `TokenBuf`, from which a `Cursor` — and so a `BNode` — is a
|
||||
## pointer.
|
||||
##
|
||||
## HOW IT DIFFERS FROM THE `.bif` FORMAT, and why that is the point. A `.bif`
|
||||
## is read by a DIFFERENT PROCESS, so every symbol and type has to be written as
|
||||
## a NAME the reader can look up again. A bridged buffer is read by the process
|
||||
## that built it, so it does not: a symbol reference is `(bsym <idx>)`, an index
|
||||
## into a side table holding the very `PSym` the encoder was handed, and the
|
||||
## type slot is `(btyp <idx>)` the same way.
|
||||
##
|
||||
## Three consequences, and the middle one is the reason to prefer this over
|
||||
## routing rewrites back through the file format:
|
||||
##
|
||||
## * It is LOSSLESS. No name mangling, no module index, no stubs, so nothing can
|
||||
## be lost or renamed on the way through. `toPNode(toTokenBuf(n))` is `n`
|
||||
## again, and `cgen`'s grinder checks the stronger property — that the cursor
|
||||
## answers identically to the ORIGINAL `PNode` at every node, with no
|
||||
## tolerated differences at all, unlike the file path which needs two.
|
||||
## * `sym` IS IDEMPOTENT HERE, FIELDS INCLUDED. On the file path it is not, and
|
||||
## cannot be: a cross-context field reference has no index entry, so
|
||||
## `loadFieldStub` mints a fresh `skField` stub per use because two distinct
|
||||
## fields can share a name and a position across types. That is what blocks
|
||||
## `aliases.isPartOf` from moving to the seam (see `bnode.sym`). A bridged
|
||||
## buffer hands back the same object every time, so code that compares field
|
||||
## identity is correct on it.
|
||||
## * The ENCODER is cheap, and that part is measured: no string formatting, no
|
||||
## pool lookups for names, no index seeks, just a tree walk and two `seq.add`s.
|
||||
## Building a buffer for every routine and NOT reading it costs 6.79s against a
|
||||
## 6.75s baseline on a 50-module target — inside the noise.
|
||||
##
|
||||
## READING is not free, and that is where the cost of the whole seam sits.
|
||||
## Driving the generator off cursors takes the same target from 6.75s to 8.85s,
|
||||
## **+31%**, stable across interleaved runs. Since a compile is mostly frontend,
|
||||
## codegen itself is slowed by considerably more than 31%. The suspects are the
|
||||
## per-access costs a `PNode` does not have: `son(n, i)` is O(i) because it skips
|
||||
## from the first child, `kind` checks the tag pool and indexes a memo on every
|
||||
## call, `sym`/`typ` go through the nav, and `origin` is a hash lookup on every
|
||||
## location built. None of that is inherent — `son` could cache, `origin` could
|
||||
## key on something cheaper — but none of it has been optimised, and the number
|
||||
## is here so nobody has to rediscover it before deciding whether to.
|
||||
##
|
||||
## WHAT IT IS NOT. The buffer is transient and process-local: `(bsym …)` means
|
||||
## nothing without the tables beside it, so a bridged buffer must never be
|
||||
## written to a file. The `.bif` writer in `ast2nif` is still the only thing
|
||||
## that serializes, and it is a different job — it has to name things precisely
|
||||
## because the reader cannot see this process's heap.
|
||||
##
|
||||
## USE:
|
||||
##
|
||||
## var b = toTokenBuf(n, conf)
|
||||
## withBridge(b.tables):
|
||||
## let root = BNode(b.rootCursor) # read it like any other `BNode`
|
||||
## ...
|
||||
## let back = toPNode(b) # a fresh `PNode` tree, if a rewrite needs one
|
||||
##
|
||||
## `withBridge` and `BNode` live in `bnode.nim` and exist only under
|
||||
## `-d:newIcBackend`; this module is below that seam and does not depend on it,
|
||||
## so the encoder and the round trip are usable either way.
|
||||
|
||||
import std / tables
|
||||
|
||||
import ast, astdef, idents, options, msgs, lineinfos
|
||||
import icnifcore, ast2nif
|
||||
import ic / enum2nif
|
||||
|
||||
import "../dist/nimony/src/lib/nifcore" except pool
|
||||
|
||||
import bodynav
|
||||
|
||||
when defined(nimPreviewSlimSystem):
|
||||
import std / assertions
|
||||
|
||||
type
|
||||
BridgeBuf* = object
|
||||
## An encoded tree plus everything needed to read it back. Not copyable —
|
||||
## it owns a `TokenBuf`.
|
||||
bld*: IcBuilder
|
||||
tables*: BridgeTables
|
||||
conf: ConfigRef
|
||||
symIdx: Table[int, int] ## PSym identity -> index into `tables.syms`
|
||||
typeIdx: Table[int, int] ## PType identity -> index into `tables.types`
|
||||
|
||||
proc initBridgeBuf*(conf: ConfigRef; cap = 64): BridgeBuf =
|
||||
BridgeBuf(bld: newIcBuilder(cap), tables: BridgeTables(), conf: conf,
|
||||
symIdx: initTable[int, int](), typeIdx: initTable[int, int]())
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Encode
|
||||
#
|
||||
# The shape mirrors the `.bif` node encoding exactly — `(<kind> <flags> <type>
|
||||
# <child|payload>…)` — so `bnode` reads a bridged buffer with the accessors it
|
||||
# already has. Only the two leaves that would have been NAMES differ.
|
||||
|
||||
proc symIndex(b: var BridgeBuf; s: PSym): int =
|
||||
## Symbols are deduplicated by identity, so the same `PSym` referenced twenty
|
||||
## times costs one table slot and twenty equal indices — which is also what
|
||||
## makes `sym` idempotent on the way back.
|
||||
let key = cast[int](s)
|
||||
result = b.symIdx.getOrDefault(key, -1)
|
||||
if result < 0:
|
||||
result = b.tables.syms.len
|
||||
b.tables.syms.add s
|
||||
b.symIdx[key] = result
|
||||
|
||||
proc typeIndex(b: var BridgeBuf; t: PType): int =
|
||||
let key = cast[int](t)
|
||||
result = b.typeIdx.getOrDefault(key, -1)
|
||||
if result < 0:
|
||||
result = b.tables.types.len
|
||||
b.tables.types.add t
|
||||
b.typeIdx[key] = result
|
||||
|
||||
proc emitInfo(b: var BridgeBuf; info: TLineInfo) =
|
||||
## Line info goes through the SAME filename pool the `.bif` writer uses
|
||||
## (`icPool.filenames`, keyed by full path), so `bnode.info` — which resolves
|
||||
## through the decoder's `oldLineInfo` — needs no bridge-specific path.
|
||||
if info == unknownLineInfo: return
|
||||
b.bld.lineInfo(msgs.toFullPath(b.conf, info.fileIndex),
|
||||
info.line.int32, info.col.int32)
|
||||
|
||||
proc emitFlags(b: var BridgeBuf; flags: TNodeFlags) =
|
||||
var asIdent = ""
|
||||
genFlags(flags, asIdent)
|
||||
if asIdent.len > 0: b.bld.addIdent asIdent
|
||||
else: b.bld.addDotToken()
|
||||
|
||||
proc emitTypeSlot(b: var BridgeBuf; t: PType) =
|
||||
if t == nil:
|
||||
b.bld.addDotToken()
|
||||
else:
|
||||
b.bld.openTag bridgeTypeTagName
|
||||
b.bld.addIntLit typeIndex(b, t).int64
|
||||
b.bld.closeTag()
|
||||
|
||||
proc encodeNode(b: var BridgeBuf; n: PNode)
|
||||
|
||||
proc encodeSym(b: var BridgeBuf; n: PNode) =
|
||||
## `(nflags <flags> (ht <type> (bsym <idx>)))`, always the full chain.
|
||||
##
|
||||
## The wrappers are unconditional on purpose. The `.bif` writer emits them
|
||||
## only when the node differs from its symbol, which is what creates the
|
||||
## `(ht . <sym>)` shape whose nil is load-bearing and whose meaning depends on
|
||||
## whether the symbol was loaded yet — a real ambiguity that cost a reverted
|
||||
## commit on this branch. A bridge has no reason to inherit it: spelling the
|
||||
## node's own type and flags out every time costs four tokens and makes the
|
||||
## answer exact by construction.
|
||||
b.bld.openTag symNodeFlagsTagName
|
||||
b.emitInfo(n.info)
|
||||
b.emitFlags(n.flags)
|
||||
b.bld.openTag hiddenTypeTagName
|
||||
b.emitTypeSlot(n.typ) # the LAZY-AWARE accessor: what `ast.typ` says
|
||||
b.bld.openTag bridgeSymTagName
|
||||
b.bld.addIntLit symIndex(b, n.sym).int64
|
||||
b.bld.closeTag() # bsym
|
||||
b.bld.closeTag() # ht
|
||||
b.bld.closeTag() # nflags
|
||||
|
||||
proc encodeNode(b: var BridgeBuf; n: PNode) =
|
||||
if n == nil:
|
||||
# A nil child is a `DotToken` and has no origin: there is no node to
|
||||
# remember, and `originOf` answering nil for it is the right answer.
|
||||
b.bld.addDotToken()
|
||||
return
|
||||
# ORIGIN TRACKING. `len` is where this node's head token is about to land, and
|
||||
# `cursorToPosition` is its inverse — nifcore documents that index as a stable
|
||||
# key for exactly this. Recording it is what keeps `TLoc.lode` a `PNode`: a
|
||||
# cursor-driven generator can still put the ORIGINAL node in a location, so
|
||||
# the identity comparisons that already exist (`preventNrvo`'s `dest != le`,
|
||||
# `isPartOf(d.lode, …)`) keep meaning what they meant. Without this the
|
||||
# generator could not migrate without `TLoc` itself changing representation —
|
||||
# and `TLoc` lives in `astdef`, at the bottom of the module graph, so that
|
||||
# would push the seam far below the backend.
|
||||
b.tables.origins[b.bld.buf.len] = n
|
||||
if n.kind == nkSym and n.sym != nil:
|
||||
encodeSym(b, n)
|
||||
return
|
||||
b.bld.openTag toNifTag(n.kind)
|
||||
b.emitInfo(n.info)
|
||||
b.emitFlags(n.flags)
|
||||
b.emitTypeSlot(n.typ)
|
||||
case n.kind
|
||||
of nkCharLit:
|
||||
b.bld.addCharLit char(n.intVal)
|
||||
of nkIntLit..nkInt64Lit:
|
||||
b.bld.addIntLit n.intVal
|
||||
of nkUIntLit..nkUInt64Lit:
|
||||
b.bld.addUIntLit cast[uint64](n.intVal)
|
||||
of nkFloatLit..nkFloat128Lit:
|
||||
b.bld.addFloatLit n.floatVal
|
||||
of nkStrLit..nkTripleStrLit:
|
||||
b.bld.addStrLit n.strVal
|
||||
of nkIdent:
|
||||
b.bld.addIdent n.ident.s
|
||||
of nkSym:
|
||||
# `n.sym == nil`, which `encodeSym` cannot express. It is a broken node
|
||||
# either way; encode it as a childless `nkSym` so the walk stays total.
|
||||
discard
|
||||
of nkNone, nkEmpty, nkNilLit, nkType, nkCommentStmt:
|
||||
discard
|
||||
else:
|
||||
for child in sons(n): encodeNode(b, child)
|
||||
b.bld.closeTag()
|
||||
|
||||
proc toTokenBuf*(n: PNode; conf: ConfigRef): BridgeBuf =
|
||||
## Encode a whole tree. `n` is not modified and not retained: the buffer holds
|
||||
## tokens, and the tables hold the `PSym`/`PType` objects the tree pointed at.
|
||||
result = initBridgeBuf(conf)
|
||||
encodeNode(result, n)
|
||||
# The tables carry a BORROWED pointer to the buffer so `originAt` can key
|
||||
# against it. Set once, here, after encoding is finished and the buffer will
|
||||
# not be reallocated out from under it.
|
||||
result.tables.buf = addr result.bld.buf
|
||||
|
||||
proc originOf*(b: var BridgeBuf; c: Cursor): PNode {.inline.} =
|
||||
## The `PNode` that was encoded at `c`, or nil when `c` is a `DotToken` (a nil
|
||||
## child) or does not point at a node head. Identity-preserving: this is the
|
||||
## very object the encoder was handed, not a copy, which is the whole point.
|
||||
b.tables.buf = addr b.bld.buf
|
||||
originAt(b.tables, c)
|
||||
|
||||
proc rootCursor*(b: var BridgeBuf): Cursor {.inline.} =
|
||||
## A read cursor at the encoded root. `beginRead` asserts every tag was
|
||||
## closed, so a mis-nested encode is caught here rather than as nonsense
|
||||
## further along.
|
||||
beginRead(b.bld.buf)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Decode
|
||||
#
|
||||
# The other direction, for a rewriting pass that has a cursor and needs a tree
|
||||
# it can mutate. Deliberately NOT written against `bnode`: this module is below
|
||||
# it (`bnode` reads through a nav, which is exactly the state a decoder should
|
||||
# not need), and the shape is the encoder's, right here, so the two stay
|
||||
# legible as a pair.
|
||||
|
||||
proc decodeNode(b: BridgeBuf; c: var Cursor): PNode
|
||||
|
||||
proc decodeTypeSlot(b: BridgeBuf; c: var Cursor): PType =
|
||||
if nifcore.kind(c) == DotToken:
|
||||
result = nil
|
||||
skip c
|
||||
else:
|
||||
doAssert nifcore.kind(c) == TagLit and
|
||||
c.tags.tagName(cursorTagId(c)) == bridgeTypeTagName,
|
||||
"bridge: type slot expected"
|
||||
let payload = childCursor(c)
|
||||
doAssert nifcore.kind(payload) == IntLit, "bridge: (btyp) payload expected"
|
||||
let idx = int(nifcore.intVal(payload))
|
||||
doAssert idx < b.tables.types.len, "bridge: type index out of range"
|
||||
result = b.tables.types[idx]
|
||||
skip c
|
||||
|
||||
proc decodeFlags(c: var Cursor): TNodeFlags =
|
||||
result = nodeFlagsFromCursor(c)
|
||||
skip c
|
||||
|
||||
proc decodeSym(b: BridgeBuf; c: var Cursor): PNode =
|
||||
## Unwinds exactly what `encodeSym` wrote.
|
||||
var outer = childCursor(c) # inside (nflags
|
||||
let flags = decodeFlags(outer)
|
||||
doAssert nifcore.kind(outer) == TagLit and
|
||||
outer.tags.tagName(cursorTagId(outer)) == hiddenTypeTagName,
|
||||
"bridge: (ht) expected inside (nflags)"
|
||||
var ht = childCursor(outer) # inside (ht
|
||||
let typ = decodeTypeSlot(b, ht)
|
||||
doAssert nifcore.kind(ht) == TagLit and
|
||||
ht.tags.tagName(cursorTagId(ht)) == bridgeSymTagName,
|
||||
"bridge: (bsym) expected inside (ht)"
|
||||
let payload = childCursor(ht)
|
||||
doAssert nifcore.kind(payload) == IntLit, "bridge: (bsym) payload expected"
|
||||
let idx = int(nifcore.intVal(payload))
|
||||
doAssert idx < b.tables.syms.len, "bridge: sym index out of range"
|
||||
result = newSymNode(b.tables.syms[idx], lineInfoFromCursor(program, c))
|
||||
result.typField = typ
|
||||
result.flags = flags
|
||||
skip c
|
||||
|
||||
proc decodeNode(b: BridgeBuf; c: var Cursor): PNode =
|
||||
case nifcore.kind(c)
|
||||
of DotToken:
|
||||
result = nil
|
||||
skip c
|
||||
of TagLit:
|
||||
let tag = c.tags.tagName(cursorTagId(c))
|
||||
if tag == symNodeFlagsTagName:
|
||||
return decodeSym(b, c)
|
||||
let kind = parse(TNodeKind, tag)
|
||||
let info = lineInfoFromCursor(program, c)
|
||||
var inner = childCursor(c)
|
||||
let flags = decodeFlags(inner)
|
||||
let typ = decodeTypeSlot(b, inner)
|
||||
result = newNodeI(kind, info)
|
||||
result.flags = flags
|
||||
result.typField = typ
|
||||
case kind
|
||||
of nkCharLit..nkUInt64Lit:
|
||||
result.intVal =
|
||||
case nifcore.kind(inner)
|
||||
of CharLit: BiggestInt(ord(charLit(inner)))
|
||||
of UIntLit: cast[BiggestInt](nifcore.uintVal(inner))
|
||||
else: BiggestInt(nifcore.intVal(inner))
|
||||
of nkFloatLit..nkFloat128Lit:
|
||||
result.floatVal = nifcore.floatVal(inner)
|
||||
of nkStrLit..nkTripleStrLit:
|
||||
result.strVal = strVal(inner)
|
||||
of nkIdent:
|
||||
result.ident = identFromCursor(program, inner)
|
||||
else:
|
||||
while inner.hasMore:
|
||||
result.sons.add decodeNode(b, inner)
|
||||
skip c
|
||||
else:
|
||||
raiseAssert "bridge: unexpected token " & $nifcore.kind(c)
|
||||
|
||||
proc toPNode*(b: var BridgeBuf): PNode =
|
||||
## The tree the buffer encodes, as fresh `PNode`s sharing the ORIGINAL
|
||||
## `PSym`s and `PType`s. Round-tripping is therefore identity-preserving for
|
||||
## symbols and types and structure-preserving for everything else, which is
|
||||
## what a rewriting pass needs: it can rebuild a subtree without the symbols
|
||||
## underneath it changing identity.
|
||||
var c = rootCursor(b)
|
||||
result = decodeNode(b, c)
|
||||
@@ -29,7 +29,7 @@ const
|
||||
|
||||
nimEnableCovariance* = defined(nimEnableCovariance)
|
||||
|
||||
icFormatVersion* = "30"
|
||||
icFormatVersion* = "38"
|
||||
## Version of the IC cache format (the sem-NIF module layout written by
|
||||
## ast2nif.nim plus the iface/impl/edges side files). Bump it whenever
|
||||
## that layout changes: `commandIc` wipes a nimcache whose `ic.version`
|
||||
@@ -54,6 +54,16 @@ const
|
||||
## id, so its hash is stable across the NIF boundary (was breaking
|
||||
## nim-serialization's auto-serialization lookup under IC). The sem-NIF
|
||||
## macrocache entries and baked generic-instance bodies hold the old hashes.
|
||||
## v7 (=31): anonymous wrapper types (`var T`, `lent T`, `sink T`, tuples)
|
||||
## are named by their CONTENT instead of `itemId.item`, the module-wide
|
||||
## type-mint counter (see ast2nif.CanonTypeKinds). Old caches name the same
|
||||
## type differently, so every `.s.bif` reference would dangle.
|
||||
## v8 (=32): the same for `tyProc`, except that a proc type which is a
|
||||
## routine's SIGNATURE is named after that routine rather than by content
|
||||
## (see ast2nif.sigRoutineOf). Renames types, so old caches dangle again.
|
||||
## v9 (=33): and for the per-module `int`/`float` LITERAL COPIES (see
|
||||
## ast2nif.CanonLitCopyKinds), the last mover that broke a build outright
|
||||
## (`symbol has no offset` out of a cached `.t.bif`). Renames types again.
|
||||
|
||||
type # please make sure we have under 32 options
|
||||
# (improves code efficiency a lot!)
|
||||
@@ -458,10 +468,16 @@ type
|
||||
# codegen+DCE+cc+link in one process). The stages
|
||||
# are wired as nifmake rules by `deps.nim`'s backend
|
||||
# build file. See `compiler/nifbackend.nim`.
|
||||
icBackendModule*: string # under `nim nifc` with icBackendStage in {cg,emit}:
|
||||
# the NIF module suffix this invocation codegens or
|
||||
# emits. The other modules are loaded only so types
|
||||
# resolve; their definitions are referenced extern.
|
||||
icBackendModules*: seq[string]
|
||||
# under `nim nifc` with icBackendStage in
|
||||
# {lower,cg,emit}: the NIF module suffixes this
|
||||
# invocation processes — its BATCH. One entry is
|
||||
# the per-module fan-out; several share one process
|
||||
# and therefore ONE dependency-closure load between
|
||||
# them, which is the whole point (see
|
||||
# `nifbackend.loadDepClosure`). Every other module
|
||||
# is loaded only so types resolve; its definitions
|
||||
# are referenced extern. Empty = the main module.
|
||||
spellSuggestMax*: int # max number of spelling suggestions for typos
|
||||
|
||||
cppDefines*: HashSet[string] # (*)
|
||||
@@ -930,6 +946,24 @@ proc getOsCacheDir(): string =
|
||||
else:
|
||||
result = getHomeDir() / genSubDir.string
|
||||
|
||||
proc isIcDriver*(conf: ConfigRef): bool =
|
||||
## True for `nim c --ic:on` / `nim cpp --ic:on`: this process is the `nim ic`
|
||||
## DRIVER (it builds the nifmake graph and spawns the per-module children),
|
||||
## not a compilation. `nim ic` itself keeps its own `cmdIc` branch.
|
||||
conf.ic and conf.cmd in {cmdCompileToC, cmdCompileToCpp, cmdCompileToOC}
|
||||
|
||||
proc icCFileExt*(conf: ConfigRef): string =
|
||||
## The extension the per-module backend gives a module's translation unit.
|
||||
## Mirrors `cgen.getCFile` at BACKEND granularity, which is all the `nim ic`
|
||||
## driver can know: it DECLARES every module's `.c`/`.cpp` output to nifmake
|
||||
## without loading a single module, so a per-module `{.compile: cpp.}`
|
||||
## (`sfCompileToCpp`) is out of reach — and `nim cpp` selects the backend for
|
||||
## the whole program anyway.
|
||||
case conf.backend
|
||||
of backendCpp: ".nim.cpp"
|
||||
of backendObjc: ".nim.m"
|
||||
else: ".nim.c"
|
||||
|
||||
proc getNimcacheDir*(conf: ConfigRef): AbsoluteDir =
|
||||
proc nimcacheSuffix(conf: ConfigRef): string =
|
||||
if conf.ideActive: "_nimsuggest" # dedicated cache, never shared with `nim c`
|
||||
|
||||
@@ -6,9 +6,11 @@ import sem, cgen, modulegraphs, ast, llstream, parser, msgs,
|
||||
when not defined(nimKochBootstrap):
|
||||
import vmdef
|
||||
import ast2nif
|
||||
import "../dist/nimony/src/lib" / [nifstreams, bitabs]
|
||||
import nifstreams
|
||||
import "../dist/nimony/src/lib" / bitabs
|
||||
|
||||
import pipelineutils
|
||||
import icprof
|
||||
|
||||
import ../dist/checksums/src/checksums/sha1
|
||||
|
||||
@@ -248,7 +250,15 @@ proc processPipelineModule*(graph: ModuleGraph; module: PSym; idgen: IdGenerator
|
||||
# current strongly-connected import group (`--icGroup`) are the exception:
|
||||
# they are compiled from source here, so each must write its own NIF.
|
||||
let shouldWriteNif =
|
||||
if graph.config.ideActive:
|
||||
if graph.config.errorCounter > 0:
|
||||
# Never persist an artifact built from erroneous AST. `nim m` does exit
|
||||
# non-zero, but its outputs would still land on disk NEWER than their
|
||||
# inputs, so nifmake sees the rule as satisfied on the next run: the
|
||||
# build then "succeeds" from a poisoned NIF — a silently wrong binary,
|
||||
# or an internal error once codegen meets an `nkError` body. Leaving the
|
||||
# outputs missing keeps the rule dirty so it re-fires and re-reports.
|
||||
false
|
||||
elif graph.config.ideActive:
|
||||
# nimsuggest (cmdM): persist NIF for cleanly-compiled, SAVED modules so
|
||||
# later queries load them instead of recompiling. Never persist the
|
||||
# actively edited buffer (it may hold unsaved/incomplete code) nor a
|
||||
@@ -305,7 +315,7 @@ proc processPipelineModule*(graph: ModuleGraph; module: PSym; idgen: IdGenerator
|
||||
var typeOffers: seq[tuple[generic: PSym; inst: PType]] = @[]
|
||||
for genItemId, instList in graph.typeInstCache:
|
||||
for inst in instList:
|
||||
if inst != nil and inst.uniqueId.module == module.position and
|
||||
if inst != nil and inst.itemId.module == module.position and
|
||||
inst.kidsLen > 0 and inst[0] != nil and
|
||||
inst[0].kind == tyGenericBody and inst[0].sym != nil:
|
||||
typeOffers.add (inst[0].sym, inst)
|
||||
@@ -320,10 +330,19 @@ proc processPipelineModule*(graph: ModuleGraph; module: PSym; idgen: IdGenerator
|
||||
let firstUnusedId = max(idgen.symId, idgen.typeId)
|
||||
var expansions: seq[(PSym, TLineInfo)] = @[]
|
||||
discard graph.nifExpansions.take(module.position.int32, expansions)
|
||||
writeNifModule(graph.config, module.position.int32, topLevelStmts, graph.opsLog,
|
||||
replayActions, implDeps, reexportedModuleSyms(graph, module),
|
||||
genericOffers, typeOffers, resolvedImportDeps, firstUnusedId,
|
||||
expansions)
|
||||
# The module symbol's own backend-relevant flags. `sfInjectDestructors` is
|
||||
# set by sempass2 when the module's TOP-LEVEL statements need the
|
||||
# destructor pass; `moduleFromNifFile` builds a fresh module PSym, so
|
||||
# without persisting it `cgen.genTopLevelStmt` skipped
|
||||
# `injectDestructorCalls` and top-level locals were never destroyed.
|
||||
let moduleFlags =
|
||||
if sfInjectDestructors in module.flags: ModFlagInjectDestructors else: 0'i32
|
||||
timed tWriteNif:
|
||||
writeNifModule(graph.config, module.position.int32, topLevelStmts, graph.opsLog,
|
||||
replayActions, implDeps, reexportedModuleSyms(graph, module),
|
||||
genericOffers, typeOffers, resolvedImportDeps, firstUnusedId,
|
||||
expansions, moduleFlags,
|
||||
reexportedLocalSyms(graph, module))
|
||||
# The module's REAL direct imports (incl. macro-generated) for `nim ic`'s
|
||||
# graph re-derivation; see ast2nif.writeSemDeps / semdata.addImportFileDep.
|
||||
var semDepPaths: seq[string] = @[]
|
||||
|
||||
@@ -77,7 +77,7 @@ proc isAttachableRoutineTo(prc: PSym, arg: PType): bool =
|
||||
# has default value, parameter is not considered in type attachment
|
||||
continue
|
||||
let t = nominalRoot(prc.typ[i])
|
||||
if t != nil and t.itemId == arg.itemId:
|
||||
if t != nil and t.bindingId == arg.bindingId:
|
||||
# parameter `i` is a nominal type in this module
|
||||
# attachable if the nominal root `t` has the same id as `arg`
|
||||
return true
|
||||
@@ -735,10 +735,10 @@ proc indexTypesMatch(c: PContext, f, a: PType, arg: PNode): PNode =
|
||||
when defined(icDbg):
|
||||
if result == nil and f != nil and a != nil and f.kind == tyEnum:
|
||||
echo "INDEXMISMATCH f=", typeToString(f), " itemId=", f.itemId,
|
||||
" uniqueId=", f.uniqueId, " mod=", toFullPath(c.config, f.itemId.module.FileIndex),
|
||||
" bindingId=", f.bindingId, " mod=", toFullPath(c.config, f.itemId.module.FileIndex),
|
||||
" sym=", (if f.sym != nil: $f.sym.itemId else: "nil"), " state=", f.state
|
||||
let a2 = a.skipTypes({tyRange})
|
||||
echo " a=", typeToString(a), " itemId=", a2.itemId, " uniqueId=", a2.uniqueId,
|
||||
echo " a=", typeToString(a), " itemId=", a2.itemId, " bindingId=", a2.bindingId,
|
||||
" mod=", toFullPath(c.config, a2.itemId.module.FileIndex),
|
||||
" sym=", (if a2.sym != nil: $a2.sym.itemId else: "nil"), " state=", a2.state
|
||||
|
||||
|
||||
@@ -1931,7 +1931,7 @@ proc borrowCheck(c: PContext, n, le, ri: PNode) =
|
||||
PathKinds0 = {nkDotExpr, nkCheckedFieldExpr,
|
||||
nkBracketExpr, nkAddr, nkHiddenAddr,
|
||||
nkObjDownConv, nkObjUpConv}
|
||||
PathKinds1 = {nkHiddenStdConv, nkHiddenSubConv}
|
||||
PathKinds1 = {nkHiddenStdConv, nkHiddenSubConv, nkCast}
|
||||
|
||||
proc getRoot(n: PNode; followDeref: bool): PNode =
|
||||
result = n
|
||||
@@ -2187,7 +2187,7 @@ proc semProcBody(c: PContext, n: PNode; expectedType: PType = nil): PNode =
|
||||
echo "[icMetaRet] meta result type for ", c.p.owner.name.s, ": ",
|
||||
typeToString(c.p.resultSym.typ), " kind=", c.p.resultSym.typ.kind,
|
||||
" flags=", c.p.resultSym.typ.flags,
|
||||
" uid=", c.p.resultSym.typ.uniqueId.module, ".", c.p.resultSym.typ.uniqueId.item,
|
||||
" itemId=", c.p.resultSym.typ.itemId.module, ".", c.p.resultSym.typ.itemId.item,
|
||||
" state=", c.p.resultSym.typ.state
|
||||
if isEmptyType(result.typ):
|
||||
# we inferred a 'void' return type:
|
||||
|
||||
@@ -129,7 +129,12 @@ proc semGenericStmtSymbol(c: PContext, n: PNode, s: PSym,
|
||||
result.typ = nil
|
||||
onUse(n.info, s)
|
||||
of skParam:
|
||||
if s.owner == c.p.owner:
|
||||
if s.typ != nil and s.typ.kind == tyStatic and s.typ.n != nil:
|
||||
# The enclosing routine gives this static parameter a concrete value.
|
||||
# Keep that value so the nested generic can fold it as a compile-time
|
||||
# expression instead of generating a runtime parameter reference.
|
||||
result = s.typ.n
|
||||
elif s.owner == c.p.owner:
|
||||
# Parameters of the routine currently being semchecked stay as local
|
||||
# identifiers
|
||||
result = n
|
||||
@@ -681,4 +686,3 @@ proc semConceptBody(c: PContext, n: PNode): PNode =
|
||||
)
|
||||
result = semGenericStmt(c, n, {withinConcept}, ctx)
|
||||
semIdeForTemplateOrGeneric(c, result, ctx.cursorInBody)
|
||||
|
||||
|
||||
@@ -349,7 +349,7 @@ proc instantiateProcType(c: PContext, pt: LayeredIdTable,
|
||||
when defined(icDbgRefc):
|
||||
echo "[icInst] ", prc.name.s, " param ", oldParam.name.s,
|
||||
": ", typeToString(resulti), " (kind=", resulti.kind,
|
||||
" uid=", resulti.uniqueId.module, ".", resulti.uniqueId.item,
|
||||
" itemId=", resulti.itemId.module, ".", resulti.itemId.item,
|
||||
" flags=", resulti.flags, ") -> ", typeToString(paramType),
|
||||
" (kind=", paramType.kind, ")"
|
||||
|
||||
@@ -407,6 +407,10 @@ proc instantiateProcType(c: PContext, pt: LayeredIdTable,
|
||||
eraseVoidParams(result)
|
||||
skipIntLiteralParams(result, c.idgen)
|
||||
|
||||
# The signature belongs to the INSTANCE, not to the generic it was copied
|
||||
# from: `instCopyType` above kept the generic's owner, and every parameter has
|
||||
# already been re-owned with `setOwner(param, prc)`.
|
||||
setOwner(result, prc)
|
||||
prc.typ = result
|
||||
popInfoContext(c.config)
|
||||
|
||||
|
||||
@@ -478,6 +478,15 @@ proc turnFinalizerIntoDestructor(c: PContext; orig: PSym; info: TLineInfo): PSym
|
||||
# proc signature:
|
||||
result.typ = newProcType(result.info, c.idgen, result)
|
||||
result.typ.addParam newParam
|
||||
# `transform` only rewrites the PARAMETER, so the copied AST still names `orig`
|
||||
# at `namePos`. Make the definition name itself, the invariant every other
|
||||
# routine AST keeps: the NIF writer re-derives a routine's serialized AST from
|
||||
# `ast[namePos].sym.ast` (ast2nif's `nkProcDef` branch), so a stale name node
|
||||
# made this proc serialize `orig`'s body — whose parameter belongs to `orig`.
|
||||
# Lambda lifting then saw the body's parameter as a variable captured from
|
||||
# another proc and aborted with "internal error: environment misses: x".
|
||||
if result.ast != nil and result.ast.safeLen > namePos:
|
||||
result.ast[namePos] = newSymNode(result, result.info)
|
||||
|
||||
proc semQuantifier(c: PContext; n: PNode): PNode =
|
||||
checkSonsLen(n, 2, c.config)
|
||||
|
||||
@@ -109,7 +109,7 @@ proc getObjDepth(t: PType): (int, ItemId) =
|
||||
x = skipTypes(x, skipPtrs)
|
||||
if x.kind != tyObject:
|
||||
return (-3, default(ItemId))
|
||||
stack.add x.itemId
|
||||
stack.add x.bindingId
|
||||
x = x.baseClass
|
||||
inc(result[0])
|
||||
result[1] = stack[^2]
|
||||
|
||||
@@ -1819,7 +1819,7 @@ proc typeSectionFinalPass(c: PContext, n: PNode) =
|
||||
var reified = semTypeNode(c, typeNode, nil)
|
||||
assert reified != nil
|
||||
assignType(typ, reified)
|
||||
typ.itemId = reified.itemId # same id
|
||||
typ.bindingId = reified.bindingId # same id
|
||||
if containsForwardType(typ):
|
||||
c.forwardTypeUpdates.add (owner, typ, typeNode)
|
||||
elif not remainingOwners.missingOrExcl(owner.id):
|
||||
@@ -2160,47 +2160,54 @@ proc checkedForDestructor(t: PType): bool =
|
||||
return true
|
||||
result = false
|
||||
|
||||
proc whereToBindTypeHook(c: PContext; t: PType): PType =
|
||||
proc normalizeTypeHook(t: PType; markAsgn = false): PType =
|
||||
result = t
|
||||
while true:
|
||||
if result.kind in {tyGenericBody, tyGenericInst}: result = result.skipModifier
|
||||
elif result.kind == tyGenericInvocation: result = result[0]
|
||||
else: break
|
||||
if markAsgn:
|
||||
incl(result, tfHasAsgn)
|
||||
if result.kind == tyCompositeTypeClass and result.base.kind == tyGenericBody:
|
||||
result = result.base
|
||||
elif result.kind in {tyGenericBody, tyGenericInst}:
|
||||
result = result.skipModifier
|
||||
elif result.kind == tyGenericInvocation:
|
||||
result = result.genericHead
|
||||
else:
|
||||
break
|
||||
|
||||
proc whereToBindTypeHook(c: PContext; t: PType): PType =
|
||||
result = normalizeTypeHook(t)
|
||||
if result.kind in {tyObject, tyDistinct, tySequence, tyString}:
|
||||
result = canonType(c, result)
|
||||
|
||||
proc bindHookToType(c: PContext; s: PSym; n: PNode; op: TTypeAttachedOp;
|
||||
typeToBind: PType): bool =
|
||||
var obj = typeToBind
|
||||
if obj.kind notin {tyObject, tyDistinct, tySequence, tyString}:
|
||||
return false
|
||||
obj = canonType(c, obj)
|
||||
let ao = getAttachedOp(c.graph, obj, op)
|
||||
if ao == s:
|
||||
discard "forward declared hook"
|
||||
elif ao.isNil and not checkedForDestructor(obj):
|
||||
setAttachedOp(c.graph, c.module.position, obj, op, s)
|
||||
else:
|
||||
prevDestructor(c, op, ao, obj, n.info)
|
||||
if obj.owner.getModule != s.getModule:
|
||||
localError(c.config, n.info, errGenerated,
|
||||
"type bound operation `" & s.name.s & "` can be defined only in the same module with its type (" & obj.typeToString() & ")")
|
||||
result = true
|
||||
|
||||
proc bindDupHook(c: PContext; s: PSym; n: PNode; op: TTypeAttachedOp) =
|
||||
let t = s.typ
|
||||
var noError = false
|
||||
let cond = t.len == 2 and t.returnType != nil
|
||||
|
||||
if cond:
|
||||
var obj = t.firstParamType
|
||||
while true:
|
||||
incl(obj, tfHasAsgn)
|
||||
if obj.kind in {tyGenericBody, tyGenericInst}: obj = obj.skipModifier
|
||||
elif obj.kind == tyGenericInvocation: obj = obj.genericHead
|
||||
else: break
|
||||
var obj = normalizeTypeHook(t.firstParamType, markAsgn = true)
|
||||
let res = normalizeTypeHook(t.returnType)
|
||||
|
||||
var res = t.returnType
|
||||
while true:
|
||||
if res.kind in {tyGenericBody, tyGenericInst}: res = res.skipModifier
|
||||
elif res.kind == tyGenericInvocation: res = res.genericHead
|
||||
else: break
|
||||
|
||||
if obj.kind in {tyObject, tyDistinct, tySequence, tyString} and sameType(obj, res):
|
||||
obj = canonType(c, obj)
|
||||
let ao = getAttachedOp(c.graph, obj, op)
|
||||
if ao == s:
|
||||
discard "forward declared destructor"
|
||||
elif ao.isNil and not checkedForDestructor(obj):
|
||||
setAttachedOp(c.graph, c.module.position, obj, op, s)
|
||||
else:
|
||||
prevDestructor(c, op, ao, obj, n.info)
|
||||
noError = true
|
||||
if obj.owner.getModule != s.getModule:
|
||||
localError(c.config, n.info, errGenerated,
|
||||
"type bound operation `" & s.name.s & "` can be defined only in the same module with its type (" & obj.typeToString() & ")")
|
||||
if sameType(obj, res):
|
||||
noError = bindHookToType(c, s, n, op, obj)
|
||||
|
||||
if not noError and sfSystemModule notin s.owner.flags:
|
||||
localError(c.config, n.info, errGenerated,
|
||||
@@ -2230,25 +2237,8 @@ proc bindTypeHook(c: PContext; s: PSym; n: PNode; op: TTypeAttachedOp) =
|
||||
t.len >= 2 and t.returnType == nil
|
||||
|
||||
if cond:
|
||||
var obj = t.firstParamType.skipTypes({tyVar})
|
||||
while true:
|
||||
incl(obj, tfHasAsgn)
|
||||
if obj.kind in {tyGenericBody, tyGenericInst}: obj = obj.skipModifier
|
||||
elif obj.kind == tyGenericInvocation: obj = obj.genericHead
|
||||
else: break
|
||||
if obj.kind in {tyObject, tyDistinct, tySequence, tyString}:
|
||||
obj = canonType(c, obj)
|
||||
let ao = getAttachedOp(c.graph, obj, op)
|
||||
if ao == s:
|
||||
discard "forward declared destructor"
|
||||
elif ao.isNil and not checkedForDestructor(obj):
|
||||
setAttachedOp(c.graph, c.module.position, obj, op, s)
|
||||
else:
|
||||
prevDestructor(c, op, ao, obj, n.info)
|
||||
noError = true
|
||||
if obj.owner.getModule != s.getModule:
|
||||
localError(c.config, n.info, errGenerated,
|
||||
"type bound operation `" & s.name.s & "` can be defined only in the same module with its type (" & obj.typeToString() & ")")
|
||||
var obj = normalizeTypeHook(t.firstParamType.skipTypes({tyVar}), markAsgn = true)
|
||||
noError = bindHookToType(c, s, n, op, obj)
|
||||
if not noError and sfSystemModule notin s.owner.flags:
|
||||
case op
|
||||
of attachedTrace:
|
||||
@@ -2315,35 +2305,12 @@ proc semOverride(c: PContext, s: PSym, n: PNode) =
|
||||
message(c.config, n.info, warnDeprecated, "Overriding `=` hook is deprecated; Override `=copy` hook instead")
|
||||
let t = s.typ
|
||||
if t.len == 3 and t.returnType == nil and t.firstParamType.kind == tyVar:
|
||||
var obj = t.firstParamType.elementType
|
||||
while true:
|
||||
incl(obj, tfHasAsgn)
|
||||
if obj.kind == tyGenericBody: obj = obj.skipModifier
|
||||
elif obj.kind == tyGenericInvocation: obj = obj.genericHead
|
||||
else: break
|
||||
var objB = t[2]
|
||||
while true:
|
||||
if objB.kind == tyGenericBody: objB = objB.skipModifier
|
||||
elif objB.kind in {tyGenericInvocation, tyGenericInst}:
|
||||
objB = objB.genericHead
|
||||
else: break
|
||||
if obj.kind in {tyObject, tyDistinct, tySequence, tyString} and sameType(obj, objB):
|
||||
var obj = normalizeTypeHook(t.firstParamType.elementType, markAsgn = true)
|
||||
let objB = normalizeTypeHook(t[2])
|
||||
if sameType(obj, objB):
|
||||
# attach these ops to the canonical tySequence
|
||||
obj = canonType(c, obj)
|
||||
#echo "ATTACHING TO ", obj.id, " ", s.name.s, " ", cast[int](obj)
|
||||
let k = if name == "=" or name == "=copy": attachedAsgn else: attachedSink
|
||||
let ao = getAttachedOp(c.graph, obj, k)
|
||||
if ao == s:
|
||||
discard "forward declared op"
|
||||
elif ao.isNil and not checkedForDestructor(obj):
|
||||
setAttachedOp(c.graph, c.module.position, obj, k, s)
|
||||
else:
|
||||
prevDestructor(c, k, ao, obj, n.info)
|
||||
if obj.owner.getModule != s.getModule:
|
||||
localError(c.config, n.info, errGenerated,
|
||||
"type bound operation `" & name & "` can be defined only in the same module with its type (" & obj.typeToString() & ")")
|
||||
|
||||
return
|
||||
if bindHookToType(c, s, n, k, obj): return
|
||||
if sfSystemModule notin s.owner.flags:
|
||||
localError(c.config, n.info, errGenerated,
|
||||
"signature for '" & s.name.s & "' must be proc[T: object](x: var T; y: T)")
|
||||
@@ -2409,7 +2376,8 @@ proc semCppMember(c: PContext; s: PSym; n: PNode) =
|
||||
if typ.kind != tyObject:
|
||||
localError(c.config, n.info, pragmaName & " must be either ptr to object or object type.")
|
||||
if sameOwners(typ.owner, s.owner) and sameOwners(c.module, s.owner):
|
||||
c.graph.memberProcsPerType.mgetOrPut(typ.itemId, @[]).add s
|
||||
c.graph.memberProcsPerType.mgetOrPut(typ.bindingId, @[]).add s
|
||||
logCppMember(c.graph, s)
|
||||
else:
|
||||
localError(c.config, n.info,
|
||||
pragmaName & " procs must be defined in the same scope as the type they are virtual for and it must be a top level scope")
|
||||
@@ -2417,7 +2385,7 @@ proc semCppMember(c: PContext; s: PSym; n: PNode) =
|
||||
localError(c.config, n.info, pragmaName & " procs are only supported in C++")
|
||||
else:
|
||||
var typ = s.typ.returnType
|
||||
if typ != nil and typ.kind == tyObject and typ.itemId notin c.graph.initializersPerType:
|
||||
if typ != nil and typ.kind == tyObject and typ.bindingId notin c.graph.initializersPerType:
|
||||
var initializerCall = newTree(nkCall, newSymNode(s))
|
||||
var isInitializer = n[paramsPos].len > 1
|
||||
for i in 1..<n[paramsPos].len:
|
||||
@@ -2431,7 +2399,8 @@ proc semCppMember(c: PContext; s: PSym; n: PNode) =
|
||||
initializerCall.add val
|
||||
inc j
|
||||
if isInitializer:
|
||||
c.graph.initializersPerType[typ.itemId] = initializerCall
|
||||
c.graph.initializersPerType[typ.bindingId] = initializerCall
|
||||
logCppMember(c.graph, s)
|
||||
|
||||
proc semMethodPrototype(c: PContext; s: PSym; n: PNode) =
|
||||
if s.isGenericRoutine:
|
||||
|
||||
@@ -1379,7 +1379,7 @@ proc liftParamType(c: PContext, procKind: TSymKind, genericParams: PNode,
|
||||
|
||||
for i in 0..<paramType.len - 1:
|
||||
if paramType[i].kind == tyStatic:
|
||||
var staticCopy = paramType[i].exactReplica(c.idgen)
|
||||
var staticCopy = copyType(paramType[i], c.idgen, paramType[i].owner)
|
||||
staticCopy.incl tfInferrableStatic
|
||||
result.rawAddSon staticCopy
|
||||
else:
|
||||
@@ -2481,7 +2481,7 @@ proc semTypeNode(c: PContext, n: PNode, prev: PType): PType =
|
||||
# bugfix: keep the fresh id for aliases to integral types:
|
||||
if s.typ.kind notin {tyBool, tyChar, tyInt..tyInt64, tyFloat..tyFloat128,
|
||||
tyUInt..tyUInt64}:
|
||||
prev.itemId = s.typ.itemId
|
||||
prev.bindingId = s.typ.bindingId
|
||||
result = prev
|
||||
of nkSym:
|
||||
let s = getGenSym(c, n.sym)
|
||||
|
||||
@@ -376,8 +376,8 @@ proc lookupTypeVar(cl: var TReplTypeVars, t: PType): PType =
|
||||
result = cl.typeMap.lookup(t)
|
||||
when defined(icDbgRefc):
|
||||
if t.kind in {tyGenericParam, tyTypeDesc}:
|
||||
echo "[icBind] lookup ", t.kind, " ", typeToString(t), " uid=", t.uniqueId.module, ".",
|
||||
t.uniqueId.item, " itemId=", t.itemId.module, ".", t.itemId.item,
|
||||
echo "[icBind] lookup ", t.kind, " ", typeToString(t), " itemId=", t.itemId.module, ".",
|
||||
t.itemId.item, " bindingId=", t.bindingId.module, ".", t.bindingId.item,
|
||||
" state=", t.state, " flags=", t.flags, " -> ",
|
||||
(if result != nil: typeToString(result) else: "MISS"),
|
||||
" allowMeta=", cl.allowMetaTypes
|
||||
@@ -423,7 +423,7 @@ proc handleGenericInvocation(cl: var TReplTypeVars, t: PType): PType =
|
||||
var header = t
|
||||
# search for some instantiation here:
|
||||
if cl.allowMetaTypes:
|
||||
result = getOrDefault(cl.localCache, t.itemId)
|
||||
result = getOrDefault(cl.localCache, t.bindingId)
|
||||
else:
|
||||
result = searchInstTypes(cl.c.graph, t)
|
||||
|
||||
@@ -473,7 +473,7 @@ proc handleGenericInvocation(cl: var TReplTypeVars, t: PType): PType =
|
||||
if not cl.allowMetaTypes:
|
||||
cacheTypeInst(cl.c, result)
|
||||
else:
|
||||
cl.localCache[t.itemId] = result
|
||||
cl.localCache[t.bindingId] = result
|
||||
|
||||
let oldSkipTypedesc = cl.skipTypedesc
|
||||
cl.skipTypedesc = true
|
||||
@@ -647,7 +647,7 @@ proc replaceTypeVarsTAux(cl: var TReplTypeVars, t: PType, isInstValue = false):
|
||||
# type
|
||||
# Vector[N: static[int]] = array[N, float64]
|
||||
# TwoVectors[Na, Nb: static[int]] = (Vector[Na], Vector[Nb])
|
||||
result = getOrDefault(cl.localCache, t.itemId)
|
||||
result = getOrDefault(cl.localCache, t.bindingId)
|
||||
if result != nil: return result
|
||||
inc cl.recursionLimit
|
||||
|
||||
@@ -739,7 +739,7 @@ proc replaceTypeVarsTAux(cl: var TReplTypeVars, t: PType, isInstValue = false):
|
||||
return
|
||||
bailout()
|
||||
result = instCopyType(cl, t)
|
||||
cl.localCache[t.itemId] = result
|
||||
cl.localCache[t.bindingId] = result
|
||||
for i in FirstGenericParamAt..<result.kidsLen:
|
||||
var r = result[i]
|
||||
if r != nil:
|
||||
@@ -755,7 +755,7 @@ proc replaceTypeVarsTAux(cl: var TReplTypeVars, t: PType, isInstValue = false):
|
||||
of tyGenericInst, tyUserTypeClassInst:
|
||||
bailout()
|
||||
result = instCopyType(cl, t)
|
||||
cl.localCache[t.itemId] = result
|
||||
cl.localCache[t.bindingId] = result
|
||||
for i in FirstGenericParamAt..<result.kidsLen:
|
||||
result[i] = replaceTypeVarsT(cl, result[i])
|
||||
propagateToOwner(result, result.last)
|
||||
@@ -770,7 +770,7 @@ proc replaceTypeVarsTAux(cl: var TReplTypeVars, t: PType, isInstValue = false):
|
||||
result = instCopyType(cl, t)
|
||||
result.size = -1 # needs to be recomputed
|
||||
#if not cl.allowMetaTypes:
|
||||
cl.localCache[t.itemId] = result
|
||||
cl.localCache[t.bindingId] = result
|
||||
let propagateInstValue = isInstValue and isRefPtrObject(t)
|
||||
|
||||
for i, resulti in result.ikids:
|
||||
@@ -819,7 +819,7 @@ proc replaceTypeVarsTAux(cl: var TReplTypeVars, t: PType, isInstValue = false):
|
||||
result = t
|
||||
|
||||
# Slow path, we have some work to do. CRUCIAL: only ever mutate a type that
|
||||
# is LOCAL to the module we are instantiating in (`uniqueId.module ==
|
||||
# is LOCAL to the module we are instantiating in (`itemId.module ==
|
||||
# idgen.module`). A type loaded from another module's NIF (foreign) already
|
||||
# had its object branches resolved when it was originally compiled; mutating
|
||||
# it in place here is an old→new heap write that re-homes the loaded type to
|
||||
@@ -828,11 +828,11 @@ proc replaceTypeVarsTAux(cl: var TReplTypeVars, t: PType, isInstValue = false):
|
||||
# prior `state != Sealed` guard was insufficient: a freshly-LOADED type is
|
||||
# `Complete`, not `Sealed` (`Sealed` only means "already re-written to a NIF").
|
||||
if t.kind == tyRef and t.hasElementType and t.elementType.kind == tyObject and
|
||||
t.elementType.n != nil and t.elementType.uniqueId.module == cl.c.idgen.module.int:
|
||||
t.elementType.n != nil and t.elementType.itemId.module == cl.c.idgen.module.int:
|
||||
discard replaceObjBranches(cl, t.elementType.n)
|
||||
|
||||
elif result.n != nil and t.kind == tyObject and result.state != Sealed and
|
||||
result.uniqueId.module == cl.c.idgen.module.int:
|
||||
result.itemId.module == cl.c.idgen.module.int:
|
||||
# Invalidate the type size as we may alter its structure
|
||||
result.size = -1
|
||||
result.n = replaceObjBranches(cl, result.n)
|
||||
|
||||
@@ -150,7 +150,7 @@ proc hashType(c: var MD5Context, t: PType; flags: set[ConsiderFlag]; conf: Confi
|
||||
if hashDepth > hashMaxDepth: hashMaxDepth = hashDepth
|
||||
if hashCalls >= 500_000_000 and hashCalls <= 500_000_300:
|
||||
echo "HASHLOOP n=", hashCalls, " d=", hashDepth, " kind=", t.kind, " id=", t.itemId,
|
||||
" uniq=", t.uniqueId, " sym=", (if t.sym != nil: t.sym.name.s else: "NIL"),
|
||||
" bindingId=", t.bindingId, " sym=", (if t.sym != nil: t.sym.name.s else: "NIL"),
|
||||
" state=", t.state, " owner=", (if t.owner != nil: t.owner.name.s else: "NIL")
|
||||
elif hashCalls == 500_000_301:
|
||||
echo "HASHLOOP maxDepth=", hashMaxDepth
|
||||
@@ -209,7 +209,11 @@ proc hashType(c: var MD5Context, t: PType; flags: set[ConsiderFlag]; conf: Confi
|
||||
# backend spelling instead of collapsing into the generic Nim builtin:
|
||||
c &= char(t.kind)
|
||||
if t.sym != nil and {sfImportc, sfExportc} * t.sym.flags != {}:
|
||||
c.hashSym(t.sym)
|
||||
# Aliases inherit the external name, but have a different symbol.
|
||||
if t.sym.loc.snippet != "":
|
||||
c &= t.sym.loc.snippet
|
||||
else:
|
||||
c.hashSym(t.sym)
|
||||
of tyObject, tyEnum:
|
||||
if t.typeInstImpl != nil:
|
||||
# prevent against infinite recursions here, see bug #8883:
|
||||
|
||||
@@ -137,8 +137,8 @@ proc put(c: var TCandidate, key, val: PType) {.inline.} =
|
||||
echo "binding ", key, " -> ", val
|
||||
when defined(icDbgRefc):
|
||||
if key.kind in {tyGenericParam, tyTypeDesc}:
|
||||
echo "[icBind] put ", key.kind, " ", typeToString(key), " uid=", key.uniqueId.module, ".",
|
||||
key.uniqueId.item, " itemId=", key.itemId.module, ".", key.itemId.item,
|
||||
echo "[icBind] put ", key.kind, " ", typeToString(key), " itemId=", key.itemId.module, ".",
|
||||
key.itemId.item, " bindingId=", key.bindingId.module, ".", key.bindingId.item,
|
||||
" state=", key.state, " -> ", typeToString(val)
|
||||
put(c.bindings, key, val.skipIntLit(c.c.idgen))
|
||||
|
||||
@@ -913,16 +913,14 @@ proc matchUserTypeClass*(m: var TCandidate; ff, a: PType): PType =
|
||||
case typ.kind
|
||||
of tyStatic:
|
||||
param = paramSym skConst
|
||||
param.typ = typ.exactReplica(m.c.idgen)
|
||||
#copyType(typ, c.idgen, typ.owner)
|
||||
param.typ = copyType(typ, m.c.idgen, typ.owner)
|
||||
if typ.n == nil:
|
||||
param.typ.incl tfInferrableStatic
|
||||
else:
|
||||
param.ast = typ.n
|
||||
of tyFromExpr:
|
||||
param = paramSym skVar
|
||||
param.typ = typ.exactReplica(m.c.idgen)
|
||||
#copyType(typ, c.idgen, typ.owner)
|
||||
param.typ = copyType(typ, m.c.idgen, typ.owner)
|
||||
else:
|
||||
param = paramSym skType
|
||||
param.typ = if typ.isMetaType:
|
||||
@@ -974,8 +972,7 @@ proc matchUserTypeClass*(m: var TCandidate; ff, a: PType): PType =
|
||||
if ff.kind == tyUserTypeClassInst:
|
||||
result = generateTypeInstance(c, m.bindings, typeClass.sym.info, ff)
|
||||
else:
|
||||
result = ff.exactReplica(m.c.idgen)
|
||||
#copyType(ff, c.idgen, ff.owner)
|
||||
result = copyType(ff, m.c.idgen, ff.owner)
|
||||
|
||||
result.n = checkedBody
|
||||
|
||||
@@ -1169,6 +1166,8 @@ proc enterConceptMatch(c: var TCandidate; f,a: PType, flags: TTypeRelFlags): TTy
|
||||
return typeRel(c, prev, a, flags)
|
||||
if trDontBind in flags:
|
||||
conceptFlags.incl mfDontBind
|
||||
if trBindGenericParam in flags:
|
||||
conceptFlags.incl mfBindGenericParam
|
||||
if trCheckGeneric in flags:
|
||||
conceptFlags.incl mfCheckGeneric
|
||||
let mres = concepts.conceptMatch(c.c, concpt, a, c.bindings, container, flags = conceptFlags)
|
||||
@@ -1239,11 +1238,17 @@ proc typeRel(c: var TCandidate, f, aOrig: PType,
|
||||
tfConceptMatchedTypeSym notin aOrig.flags
|
||||
|
||||
template skipTypeCursor(it, kinds: untyped) =
|
||||
# `ast.last`, not a hand-inlined copy of it. What this replaces was `last`'s
|
||||
# body verbatim MINUS its `if state == Partial: loadType` line -- and that
|
||||
# line is the whole point: a NIF-loaded stub answers `kind` off its NIF name
|
||||
# while `sonsImpl` is still EMPTY, so `sonsImpl[^1]` raised IndexDefect.
|
||||
# nimbus-eth2 died on it in the very first `nim ic` pass, inside the `x is T`
|
||||
# under a chronos `{.async.}` iterator's `when`. The second call site below
|
||||
# is unguarded and runs on EVERY `typeRel`, so this is not a concept-only
|
||||
# corner: a probe counts 195 Partial `tyVar`/`tyLent` arrivals across one
|
||||
# nimbus frontend, each of which was an IndexDefect waiting for its turn.
|
||||
while it.kind in kinds:
|
||||
if it.kind == tyProc and it.nImpl.len > 1:
|
||||
it = it.nImpl[^1].sym.typ
|
||||
else:
|
||||
it = it.sonsImpl[^1]
|
||||
it = it.last
|
||||
|
||||
var aOrig {.cursor.} = aOrig
|
||||
if useTypeLoweringRuleInTypeClass:
|
||||
@@ -2689,7 +2694,7 @@ proc staticAwareTypeRel(m: var TCandidate, f: PType, arg: var PNode): TTypeRelat
|
||||
# The ast of the type does not point to the symbol.
|
||||
# Without this we will never resolve a `static proc` with overloads
|
||||
let copiedNode = copyNode(arg)
|
||||
copiedNode.typ = exactReplica(copiedNode.typ, m.c.idgen)
|
||||
copiedNode.typ = copyType(copiedNode.typ, m.c.idgen, copiedNode.typ.owner)
|
||||
copiedNode.typ.n = arg
|
||||
arg = copiedNode
|
||||
typeRel(m, f, arg.typ)
|
||||
|
||||
@@ -38,6 +38,15 @@ proc transformBody*(g: ModuleGraph; idgen: IdGenerator; prc: PSym; flags: Transf
|
||||
|
||||
import closureiters, lambdalifting
|
||||
|
||||
when not defined(nimKochBootstrap):
|
||||
# The `PNode` -> `TokenBuf` bridge, and through it `bodynav`, which resolves
|
||||
# names against `ast.program`. `program` does not EXIST under
|
||||
# `-d:nimKochBootstrap` — that define disables the whole IC subsystem (see
|
||||
# `ast.nim` and `koch.bootic`) — so the bridge has to be out of that build
|
||||
# too, not merely unused by it. `handOffBody` below is guarded for the same
|
||||
# reason; its only caller is `cgen`, under `-d:newIcBackend`.
|
||||
import nodebridge
|
||||
|
||||
type
|
||||
PTransCon = ref object # part of TContext; stackable
|
||||
mapping: TIdTable[PNode] # mapping from symbols to nodes
|
||||
@@ -1436,6 +1445,25 @@ proc transformBody*(g: ModuleGraph; idgen: IdGenerator; prc: PSym; flags: Transf
|
||||
#if prc.name.s == "main":
|
||||
# echo "transformed into ", renderTree(result, {renderIds})
|
||||
|
||||
when not defined(nimKochBootstrap):
|
||||
proc handOffBody*(body: PNode; conf: ConfigRef): BridgeBuf =
|
||||
## THE HANDOFF from the rewriting stage to the reading stage: the transformed
|
||||
## body, as a `TokenBuf` a reader can cursor over (`nodebridge`).
|
||||
##
|
||||
## It lives here because the invariant it carries is this module's: a bridged
|
||||
## buffer is a SNAPSHOT, so it must be taken after the LAST rewrite the body
|
||||
## will receive. Anything that mutates a node afterwards — `cgen.easyResultAsgn`
|
||||
## setting `nfPreventCg` is the one that does — leaves the buffer describing a
|
||||
## tree that no longer exists.
|
||||
##
|
||||
## The call site is in `cgen` rather than at the end of `transformBody` for
|
||||
## exactly that reason: destructor injection runs *after* `transformBody`
|
||||
## returns and is another rewrite, so transforming is not the last step and a
|
||||
## buffer taken here would be stale before it was read. `transformBody` returns
|
||||
## a `PNode` on purpose; this is the point where a caller that has finished
|
||||
## rewriting says so.
|
||||
result = toTokenBuf(body, conf)
|
||||
|
||||
proc transformStmt*(g: ModuleGraph; idgen: IdGenerator; module: PSym, n: PNode; flags: TransformFlags = {}): PNode =
|
||||
if nfTransf in n.flags:
|
||||
result = n
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
# tree helper routines
|
||||
|
||||
import
|
||||
ast, wordrecg, idents
|
||||
ast, wordrecg, idents, bnode
|
||||
|
||||
proc cyclicTreeAux(n: PNode, visited: var seq[PNode]): bool =
|
||||
result = false
|
||||
@@ -83,16 +83,17 @@ proc sameTree*(a, b: PNode): bool =
|
||||
if not sameTree(a[i], b[i]): return
|
||||
result = true
|
||||
|
||||
proc getMagic*(op: PNode): TMagic =
|
||||
if op == nil: return mNone
|
||||
proc getMagic*(op: AnyNode): TMagic =
|
||||
if op.isNilNode: return mNone
|
||||
case op.kind
|
||||
of nkCallKinds:
|
||||
case op[0].kind
|
||||
of nkSym: result = op[0].sym.magic
|
||||
let callee = op.firstSon
|
||||
case callee.kind
|
||||
of nkSym: result = callee.sym.magic
|
||||
else: result = mNone
|
||||
else: result = mNone
|
||||
|
||||
proc isConstExpr*(n: PNode): bool =
|
||||
proc isConstExpr*(n: AnyNode): bool =
|
||||
const atomKinds = {nkCharLit..nkNilLit} # Char, Int, UInt, Str, Float and Nil literals
|
||||
n.kind in atomKinds or nfAllConst in n.flags
|
||||
|
||||
@@ -102,15 +103,16 @@ proc isCaseObj*(n: PNode): bool =
|
||||
for i in 0..<n.safeLen:
|
||||
if n[i].isCaseObj: return true
|
||||
|
||||
proc isDeepConstExpr*(n: PNode; preventInheritance = false): bool =
|
||||
proc isDeepConstExpr*(n: AnyNode; preventInheritance = false): bool =
|
||||
case n.kind
|
||||
of nkCharLit..nkNilLit:
|
||||
result = true
|
||||
of nkExprEqExpr, nkExprColonExpr, nkHiddenStdConv, nkHiddenSubConv:
|
||||
result = isDeepConstExpr(n[1], preventInheritance)
|
||||
result = isDeepConstExpr(n.secondSon, preventInheritance)
|
||||
of nkCurly, nkBracket, nkPar, nkTupleConstr, nkObjConstr, nkClosure, nkRange:
|
||||
for i in ord(n.kind == nkObjConstr)..<n.len:
|
||||
if not isDeepConstExpr(n[i], preventInheritance): return false
|
||||
# `nkObjConstr` carries its TYPE as child 0 and its fields from 1.
|
||||
for it in sonsFrom(n, ord(n.kind == nkObjConstr)):
|
||||
if not isDeepConstExpr(it, preventInheritance): return false
|
||||
if n.typ.isNil: result = true
|
||||
else:
|
||||
let t = n.typ.skipTypes({tyGenericInst, tyDistinct, tyAlias, tySink, tyOwned})
|
||||
@@ -139,17 +141,17 @@ proc isRange*(n: PNode): bool {.inline.} =
|
||||
else:
|
||||
result = false
|
||||
|
||||
proc whichPragma*(n: PNode): TSpecialWord =
|
||||
let key = if n.kind in nkPragmaCallKinds and n.len > 0: n[0] else: n
|
||||
proc whichPragma*(n: AnyNode): TSpecialWord =
|
||||
let key = if n.kind in nkPragmaCallKinds and n.hasSons: n.firstSon else: n
|
||||
case key.kind
|
||||
of nkIdent: result = whichKeyword(key.ident)
|
||||
of nkSym: result = whichKeyword(key.sym.name)
|
||||
of nkCast: return wCast
|
||||
of nkClosedSymChoice, nkOpenSymChoice, nkOpenSym:
|
||||
return whichPragma(key[0])
|
||||
return whichPragma(key.firstSon)
|
||||
of nkBracketExpr:
|
||||
if n.kind notin nkPragmaCallKinds: return wInvalid
|
||||
result = whichPragma(key[0])
|
||||
result = whichPragma(key.firstSon)
|
||||
if result notin {wHint, wHintAsError, wWarning, wWarningAsError}:
|
||||
# note bracket pragmas, see processNote
|
||||
result = wInvalid
|
||||
@@ -205,7 +207,7 @@ proc extractRange*(k: TNodeKind, n: PNode, a, b: int): PNode =
|
||||
result = newNodeI(k, n.info, b-a+1)
|
||||
for i in 0..b-a: result[i] = n[i+a]
|
||||
|
||||
proc getRoot*(n: PNode): PSym =
|
||||
proc getRoot*(n: AnyNode): PSym =
|
||||
## ``getRoot`` takes a *path* ``n``. A path is an lvalue expression
|
||||
## like ``obj.x[i].y``. The *root* of a path is the symbol that can be
|
||||
## determined as the owner; ``obj`` in the example.
|
||||
@@ -217,11 +219,11 @@ proc getRoot*(n: PNode): PSym =
|
||||
result = nil
|
||||
of nkDotExpr, nkBracketExpr, nkHiddenDeref, nkDerefExpr,
|
||||
nkObjUpConv, nkObjDownConv, nkCheckedFieldExpr, nkHiddenAddr, nkAddr:
|
||||
result = getRoot(n[0])
|
||||
result = getRoot(n.firstSon)
|
||||
of nkHiddenStdConv, nkHiddenSubConv, nkConv:
|
||||
result = getRoot(n[1])
|
||||
result = getRoot(n.secondSon)
|
||||
of nkCallKinds:
|
||||
if getMagic(n) == mSlice: result = getRoot(n[1])
|
||||
if getMagic(n) == mSlice: result = getRoot(n.secondSon)
|
||||
else: result = nil
|
||||
else: result = nil
|
||||
|
||||
@@ -252,8 +254,8 @@ proc isRunnableExamples*(n: PNode): bool =
|
||||
result = n.kind == nkSym and n.sym.magic == mRunnableExamples or
|
||||
n.kind == nkIdent and n.ident.id == ord(wRunnableExamples)
|
||||
|
||||
proc skipAddr*(n: PNode): PNode {.inline.} =
|
||||
result = if n.kind in {nkAddr, nkHiddenAddr}: n[0] else: n
|
||||
proc skipAddr*[T: AnyNode](n: T): T {.inline.} =
|
||||
result = if n.kind in {nkAddr, nkHiddenAddr}: n.firstSon else: n
|
||||
|
||||
proc getPotentialWrites*(n: PNode; mutate: bool; result: var seq[PNode]) =
|
||||
case n.kind:
|
||||
|
||||
@@ -27,7 +27,7 @@ proc hashTree*(n: PNode): Hash =
|
||||
of nkCharLit..nkUInt64Lit: result = result !& hash(n.intVal)
|
||||
of nkFloatLit..nkFloat64Lit: result = result !& hash(cast[uint64](n.floatVal))
|
||||
of nkStrLit..nkTripleStrLit: result = result !& hash(n.strVal)
|
||||
of nkType, nkNilLit: result = result !& hash(n.typ.itemId)
|
||||
of nkType, nkNilLit: result = result !& hash(n.typ.bindingId)
|
||||
else:
|
||||
for i in 0..<n.len:
|
||||
result = result !& hashTree(n[i])
|
||||
|
||||
@@ -172,9 +172,9 @@ proc backendTypeName(t: PType; conf: ConfigRef): string =
|
||||
result = "`t"
|
||||
result.addInt ord(t.kind)
|
||||
result.add '.'
|
||||
result.addInt t.uniqueId.item
|
||||
result.addInt t.itemId.item
|
||||
result.add '.'
|
||||
result.add modname(t.uniqueId.module, conf)
|
||||
result.add modname(t.itemId.module, conf)
|
||||
result.add "@bk"
|
||||
|
||||
proc typeKey(c: var Context; t: PType; flags: set[ConsiderFlag]; conf: ConfigRef) =
|
||||
@@ -186,7 +186,7 @@ proc typeKey(c: var Context; t: PType; flags: set[ConsiderFlag]; conf: ConfigRef
|
||||
assert c.tl != nil
|
||||
c.tl(t)
|
||||
|
||||
if t.uniqueId.isBackendMinted:
|
||||
if t.itemId.isBackendMinted:
|
||||
# Backend-minted (lower-stage) closure-env types key by their stable NIF name,
|
||||
# never by structure (which diverges across the NIF boundary). An env `ref`
|
||||
# that is itself NOT backend-minted still keys stably: it recurses here and
|
||||
@@ -335,9 +335,9 @@ proc typeKey(c: var Context; t: PType; flags: set[ConsiderFlag]; conf: ConfigRef
|
||||
# mutation that an assertion deeper in `treeKey` left unrestored would
|
||||
# corrupt the type. `symKey` above already emitted the type's identity,
|
||||
# so on a back-reference we simply stop.
|
||||
if not containsOrIncl(c.visited, t.itemId):
|
||||
if not containsOrIncl(c.visited, t.bindingId):
|
||||
c.treeKey(t.nImpl, flags + {CoHashTypeInsideNode}, conf)
|
||||
c.visited.excl t.itemId
|
||||
c.visited.excl t.bindingId
|
||||
else:
|
||||
c.m.addIdent "´empty"
|
||||
# Object inheritance is part of identity: key the base class too.
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
import
|
||||
ast, astalgo, trees, msgs, platform, renderer, options,
|
||||
lineinfos, int128, modulegraphs, astmsgs
|
||||
lineinfos, int128, modulegraphs, astmsgs, bnode
|
||||
|
||||
import std/[intsets, strutils]
|
||||
|
||||
@@ -102,7 +102,7 @@ proc isPureObject*(typ: PType): bool =
|
||||
proc isUnsigned*(t: PType): bool =
|
||||
t.skipTypes(abstractInst).kind in {tyChar, tyUInt..tyUInt64}
|
||||
|
||||
proc getOrdValueAux*(n: PNode, err: var bool): Int128 =
|
||||
proc getOrdValueAux*(n: AnyNode, err: var bool): Int128 =
|
||||
var k = n.kind
|
||||
if n.typ != nil and n.typ.skipTypes(abstractInst).kind in {tyChar, tyUInt..tyUInt64}:
|
||||
k = nkUIntLit
|
||||
@@ -119,17 +119,17 @@ proc getOrdValueAux*(n: PNode, err: var bool): Int128 =
|
||||
of nkNilLit:
|
||||
int128.Zero
|
||||
of nkHiddenStdConv:
|
||||
getOrdValueAux(n[1], err)
|
||||
getOrdValueAux(n.secondSon, err)
|
||||
else:
|
||||
err = true
|
||||
int128.Zero
|
||||
|
||||
proc getOrdValue*(n: PNode): Int128 =
|
||||
proc getOrdValue*(n: AnyNode): Int128 =
|
||||
var err: bool = false
|
||||
result = getOrdValueAux(n, err)
|
||||
#assert err == false
|
||||
|
||||
proc getOrdValue*(n: PNode, onError: Int128): Int128 =
|
||||
proc getOrdValue*(n: AnyNode, onError: Int128): Int128 =
|
||||
var err = false
|
||||
result = getOrdValueAux(n, err)
|
||||
if err:
|
||||
@@ -1392,17 +1392,17 @@ proc classify*(t: PType): OrdinalType =
|
||||
result = IntLike
|
||||
else: result = NoneLike
|
||||
|
||||
proc skipConv*(n: PNode): PNode =
|
||||
proc skipConv*[T: AnyNode](n: T): T =
|
||||
result = n
|
||||
case n.kind
|
||||
of nkObjUpConv, nkObjDownConv, nkChckRange, nkChckRangeF, nkChckRange64:
|
||||
# only skip the conversion if it doesn't lose too important information
|
||||
# (see bug #1334)
|
||||
if n[0].typ.classify == n.typ.classify:
|
||||
result = n[0]
|
||||
if n.firstSon.typ.classify == n.typ.classify:
|
||||
result = n.firstSon
|
||||
of nkHiddenStdConv, nkHiddenSubConv, nkConv:
|
||||
if n[1].typ.classify == n.typ.classify:
|
||||
result = n[1]
|
||||
if n.secondSon.typ.classify == n.typ.classify:
|
||||
result = n.secondSon
|
||||
else: discard
|
||||
|
||||
proc skipHidden*(n: PNode): PNode =
|
||||
|
||||
@@ -90,30 +90,30 @@ proc collectVTableDispatchers*(g: ModuleGraph) =
|
||||
sortBucket(g.methods[bucket].methods, relevantCols)
|
||||
let base = g.methods[bucket].methods[^1]
|
||||
let baseType = base.typ.firstParamType.skipTypes(skipPtrs-{tyTypeDesc})
|
||||
if baseType.itemId in g.objectTree and not containGenerics(baseType, g.objectTree[baseType.itemId]):
|
||||
let methodIndexLen = g.bucketTable[baseType.itemId]
|
||||
if baseType.itemId notin itemTable: # once is enough
|
||||
if baseType.bindingId in g.objectTree and not containGenerics(baseType, g.objectTree[baseType.bindingId]):
|
||||
let methodIndexLen = g.bucketTable[baseType.bindingId]
|
||||
if baseType.bindingId notin itemTable: # once is enough
|
||||
rootTypeSeq.add baseType
|
||||
itemTable[baseType.itemId] = newSeq[PSym](methodIndexLen)
|
||||
itemTable[baseType.bindingId] = newSeq[PSym](methodIndexLen)
|
||||
|
||||
sort(g.objectTree[baseType.itemId], cmp = proc (x, y: tuple[depth: int, value: PType]): int =
|
||||
sort(g.objectTree[baseType.bindingId], cmp = proc (x, y: tuple[depth: int, value: PType]): int =
|
||||
if x.depth >= y.depth: 1
|
||||
else: -1
|
||||
)
|
||||
|
||||
for item in g.objectTree[baseType.itemId]:
|
||||
if item.value.itemId notin itemTable:
|
||||
itemTable[item.value.itemId] = newSeq[PSym](methodIndexLen)
|
||||
for item in g.objectTree[baseType.bindingId]:
|
||||
if item.value.bindingId notin itemTable:
|
||||
itemTable[item.value.bindingId] = newSeq[PSym](methodIndexLen)
|
||||
|
||||
var mIndex = 0 # here is the correpsonding index
|
||||
if baseType.itemId notin rootItemIdCount:
|
||||
rootItemIdCount[baseType.itemId] = 1
|
||||
if baseType.bindingId notin rootItemIdCount:
|
||||
rootItemIdCount[baseType.bindingId] = 1
|
||||
else:
|
||||
mIndex = rootItemIdCount[baseType.itemId]
|
||||
rootItemIdCount.inc(baseType.itemId)
|
||||
mIndex = rootItemIdCount[baseType.bindingId]
|
||||
rootItemIdCount.inc(baseType.bindingId)
|
||||
for idx in 0..<g.methods[bucket].methods.len:
|
||||
let obj = g.methods[bucket].methods[idx].typ.firstParamType.skipTypes(skipPtrs)
|
||||
itemTable[obj.itemId][mIndex] = g.methods[bucket].methods[idx]
|
||||
itemTable[obj.bindingId][mIndex] = g.methods[bucket].methods[idx]
|
||||
g.addDispatchers genVTableDispatcher(g, g.methods[bucket].methods, mIndex)
|
||||
else: # if the base object doesn't have this method
|
||||
g.addDispatchers genIfDispatcher(g, g.methods[bucket].methods, relevantCols, g.idgen)
|
||||
@@ -128,40 +128,40 @@ proc sortVTableDispatchers*(g: ModuleGraph) =
|
||||
sortBucket(g.methods[bucket].methods, relevantCols)
|
||||
let base = g.methods[bucket].methods[^1]
|
||||
let baseType = base.typ.firstParamType.skipTypes(skipPtrs-{tyTypeDesc})
|
||||
if baseType.itemId in g.objectTree and not containGenerics(baseType, g.objectTree[baseType.itemId]):
|
||||
let methodIndexLen = g.bucketTable[baseType.itemId]
|
||||
if baseType.itemId notin itemTable: # once is enough
|
||||
rootTypeSeq.add baseType.itemId
|
||||
itemTable[baseType.itemId] = newSeq[PSym](methodIndexLen)
|
||||
if baseType.bindingId in g.objectTree and not containGenerics(baseType, g.objectTree[baseType.bindingId]):
|
||||
let methodIndexLen = g.bucketTable[baseType.bindingId]
|
||||
if baseType.bindingId notin itemTable: # once is enough
|
||||
rootTypeSeq.add baseType.bindingId
|
||||
itemTable[baseType.bindingId] = newSeq[PSym](methodIndexLen)
|
||||
|
||||
sort(g.objectTree[baseType.itemId], cmp = proc (x, y: tuple[depth: int, value: PType]): int =
|
||||
sort(g.objectTree[baseType.bindingId], cmp = proc (x, y: tuple[depth: int, value: PType]): int =
|
||||
if x.depth >= y.depth: 1
|
||||
else: -1
|
||||
)
|
||||
|
||||
for item in g.objectTree[baseType.itemId]:
|
||||
if item.value.itemId notin itemTable:
|
||||
itemTable[item.value.itemId] = newSeq[PSym](methodIndexLen)
|
||||
for item in g.objectTree[baseType.bindingId]:
|
||||
if item.value.bindingId notin itemTable:
|
||||
itemTable[item.value.bindingId] = newSeq[PSym](methodIndexLen)
|
||||
|
||||
var mIndex = 0 # here is the correpsonding index
|
||||
if baseType.itemId notin rootItemIdCount:
|
||||
rootItemIdCount[baseType.itemId] = 1
|
||||
if baseType.bindingId notin rootItemIdCount:
|
||||
rootItemIdCount[baseType.bindingId] = 1
|
||||
else:
|
||||
mIndex = rootItemIdCount[baseType.itemId]
|
||||
rootItemIdCount.inc(baseType.itemId)
|
||||
mIndex = rootItemIdCount[baseType.bindingId]
|
||||
rootItemIdCount.inc(baseType.bindingId)
|
||||
for idx in 0..<g.methods[bucket].methods.len:
|
||||
let obj = g.methods[bucket].methods[idx].typ.firstParamType.skipTypes(skipPtrs)
|
||||
if obj.itemId notin itemTable:
|
||||
itemTable[obj.itemId] = newSeq[PSym](methodIndexLen)
|
||||
itemTable[obj.itemId][mIndex] = g.methods[bucket].methods[idx]
|
||||
if obj.bindingId notin itemTable:
|
||||
itemTable[obj.bindingId] = newSeq[PSym](methodIndexLen)
|
||||
itemTable[obj.bindingId][mIndex] = g.methods[bucket].methods[idx]
|
||||
|
||||
for baseType in rootTypeSeq:
|
||||
g.setMethodsPerType(baseType, itemTable[baseType])
|
||||
for item in g.objectTree[baseType]:
|
||||
let typ = item.value.skipTypes(skipPtrs)
|
||||
let idx = typ.itemId
|
||||
let idx = typ.bindingId
|
||||
for mIndex in 0..<itemTable[idx].len:
|
||||
if itemTable[idx][mIndex] == nil:
|
||||
let parentIndex = typ.baseClass.skipTypes(skipPtrs).itemId
|
||||
let parentIndex = typ.baseClass.skipTypes(skipPtrs).bindingId
|
||||
itemTable[idx][mIndex] = itemTable[parentIndex][mIndex]
|
||||
g.setMethodsPerType(idx, itemTable[idx])
|
||||
|
||||
133
doc/ic.md
133
doc/ic.md
@@ -2,12 +2,23 @@
|
||||
Incremental Compilation (IC)
|
||||
======================================
|
||||
|
||||
The ``nim ic`` command provides incremental compilation for Nim projects. It
|
||||
decomposes compilation into per-module steps whose results are cached as NIF
|
||||
files, and uses the external ``nifmake`` build tool to re-run only the steps
|
||||
whose inputs changed.
|
||||
``--ic:on`` turns an ordinary compile into an incremental one. It decomposes
|
||||
compilation into per-module steps whose results are cached as NIF files, and
|
||||
uses the external ``nifmake`` build tool to re-run only the steps whose inputs
|
||||
changed.
|
||||
|
||||
This document describes **how `nim ic` works today**, including the edge cases
|
||||
.. code-block:: cmd
|
||||
|
||||
nim c --ic:on myproject.nim
|
||||
nim cpp --ic:on myproject.nim
|
||||
|
||||
It is a switch on the normal compile commands, not a command of its own, so
|
||||
everything else keeps working unchanged: ``cpp`` and ``objc`` backends, ``-r``,
|
||||
``-d:release``, ``--exceptions:``, and a project-wide opt-in from ``nim.cfg`` /
|
||||
``config.nims``. The older spelling ``nim ic`` still works and drives the same
|
||||
code, but it is the C backend only and cannot run the binary it built.
|
||||
|
||||
This document describes **how IC works today**, including the edge cases
|
||||
that shaped the current design. The per-module backend rewrite that earlier
|
||||
editions of this document listed as a *Plan* has **landed**: the whole-program,
|
||||
reuse/redirect/def-retention backend is gone and codegen is now a set of
|
||||
@@ -16,7 +27,7 @@ reuse/redirect/def-retention backend is gone and codegen is now a set of
|
||||
Overview
|
||||
========
|
||||
|
||||
The pipeline has two halves driven by one process (`nim ic`, `commandIc` in
|
||||
The pipeline has two halves driven by one process (the *driver*, `commandIc` in
|
||||
``compiler/deps.nim``) that constructs a dependency graph, writes a build file,
|
||||
and hands it to ``nifmake``:
|
||||
|
||||
@@ -210,15 +221,17 @@ Edge cases (and why the machinery exists)
|
||||
- **`nil` sons of loaded ASTs.** NIF dot-tokens load as `nil` where from-source
|
||||
ASTs have `nkEmpty`; several passes gained `nil` guards.
|
||||
- **Sealed loaded types.** Loaded types are `Sealed`; sem/transform mutate via
|
||||
`unsealForTransform`/`exactReplica(idgen)` (the latter mints a fresh `uniqueId`
|
||||
so serialized replicas don't collapse).
|
||||
`unsealForTransform`/`copyType`, or -- where the copy must still answer to the
|
||||
original in the generic binding tables -- `exactReplica(idgen)`, which gives the
|
||||
copy its own `itemId` (so serialized replicas don't collapse) while inheriting
|
||||
the original's `bindingId`.
|
||||
- **Methods/RTTI ownership.** RTTI and type-bound hooks are emit-everywhere at
|
||||
`cg` and deduplicated by the `merge` stage, like generic instances; the main
|
||||
module's `cg` owns the whole-program method dispatchers.
|
||||
- **Config cost.** Each child re-parsing `nim.cfg` + re-running `config.nims` in
|
||||
the VM was ~80 ms; replaced by a precompiled `ic_config.cfg.nif` replayed in
|
||||
`loadConfigs` (`compiler/icconfig.nim`).
|
||||
- **`koch bootic`** bootstraps the compiler through `nim ic` (a 3-iteration
|
||||
- **`koch bootic`** bootstraps the compiler through `--ic:on` (a 3-iteration
|
||||
fixed-point check). It writes its binary to ``bin/nim_ic`` and never clobbers
|
||||
``bin/nim``.
|
||||
|
||||
@@ -245,7 +258,7 @@ Known residual hack
|
||||
Status and performance
|
||||
======================
|
||||
|
||||
`nim ic` self-builds the compiler (`koch bootic`'s byte-identical fixed-point
|
||||
IC self-builds the compiler (`koch bootic`'s byte-identical fixed-point
|
||||
check) under both `orc` and `--mm:refc`, and passes the external-package CI set.
|
||||
|
||||
Cold full bootstrap on a 32-core box (`-d:release`, **no edits** — IC's worst
|
||||
@@ -254,7 +267,7 @@ case, since incremental reuse is not exercised):
|
||||
| | wall | notes |
|
||||
| - | ---- | ----- |
|
||||
| `koch boot` (classic) | ~1m00s | reference |
|
||||
| `koch bootic` (`nim ic`) | ~1m39s | **~1.66×** |
|
||||
| `koch bootic` (`--ic:on`) | ~1m39s | **~1.66×** |
|
||||
|
||||
This is down from ~7.5× in the whole-program-backend era. IC does modestly more
|
||||
aggregate work (more processes, NIF re-parsing of imports per process), but on a
|
||||
@@ -403,3 +416,101 @@ See also
|
||||
|
||||
- NIF format spec: [nifspec/doc/nif-spec.md](../nifspec/doc/nif-spec.md)
|
||||
- NIFC (C-like target) spec: dist/nimony/doc/nifc-spec.md
|
||||
|
||||
Testing IC
|
||||
==========
|
||||
|
||||
Two mechanisms, at very different scales.
|
||||
|
||||
**`tests/ic` — metamorphic tests.** A `t*.nim` whose body contains `#? metamorphic`
|
||||
drives a sequence of cross-module edits through the IC driver in one fixed build
|
||||
directory (see `testament/categories.nim`, `runMetamorphicIcTest`). Directives:
|
||||
|
||||
| directive | effect |
|
||||
| --------- | ------ |
|
||||
| ``#!FILE <name>`` | (re)write a module in the virtual file system |
|
||||
| ``#!DELETE <name>`` | remove a module, from the vfs and from disk |
|
||||
| ``#!FLAGS <switches>`` | change the compiler switches from here on |
|
||||
| ``#!STEP <attrs>`` | materialise the files, build, run, check |
|
||||
|
||||
Step attributes: ``expect: <stdout>``, ``fails: <substring>`` (BOTH compilers must
|
||||
reject it, with that text), ``noop``, ``body-edit``, ``iface-edit``,
|
||||
``modules: <n>``, ``clean``, ``no-oracle``.
|
||||
|
||||
Every successful step is **also compiled with `nim c` and run, and the two
|
||||
outputs must agree**. That oracle is the only check in the suite that is not
|
||||
IC-against-IC: `clean == incremental`, `noop changes nothing` and the cookie
|
||||
invariants are all satisfied by an IC that is *consistently* wrong, which is how
|
||||
two silent miscompilations survived (a NIF-loaded module's `sfInjectDestructors`
|
||||
was lost, so top-level destructors were never injected; `nfFirstWrite`/`nfLastRead`
|
||||
had nowhere to live on a serialized sym node, so every first assignment to a
|
||||
destructor-bearing local became `=sink` over zeroed memory). `koch bootic` has the
|
||||
same blind spot — it proves the compiler reproduces *itself*.
|
||||
|
||||
**`testament --ic` — the whole corpus.** Appends `--ic:on` to every C and C++
|
||||
test compile, so IC inherits the existing ~10k programs and their expected
|
||||
output instead of the handful written for it by hand. Because it is a switch and
|
||||
not a command, a test that overrides the command wholesale (`cmd: "nim cpp -r
|
||||
$file"`) simply gains the switch — no verb rewriting, and the C++ corpus comes
|
||||
along for free. Each also gets a private nimcache; without one they would share
|
||||
a cache and thrash it.
|
||||
|
||||
To keep that affordable, testament borrows nimony's hastur model
|
||||
(`warmupSharedCache` + `prefillFromWarmup`): a generated warmup program pulling in
|
||||
`system` and the most-imported stdlib modules is compiled once per distinct
|
||||
compile configuration into `nimcache/ic_warmup_<hash>`, and each test's empty
|
||||
cache is seeded from it with **mtimes preserved** (nifmake compares
|
||||
output-mtime > input-mtime, so stamping the copies "now" would re-fire the whole
|
||||
graph). Only program-independent artifacts are copied — the frontend NIFs and
|
||||
cookies plus the per-module `lower`/`cg` outputs. The `.c`/`.o` are deliberately
|
||||
left behind: the merge decision (which module owns each emit-everywhere
|
||||
definition) is whole-program, so those are re-rendered for every program anyway.
|
||||
|
||||
Measured on `tests/destructor` (97 test runs, 32-core box):
|
||||
|
||||
| | cold | warm |
|
||||
| - | ---- | ---- |
|
||||
| `nim c` | 35s | 32s |
|
||||
| `--ic:on` | ~3m30 | **9.8s** |
|
||||
|
||||
The warm number is the developer loop and it is 3.2x faster than the classic
|
||||
backend; the cold number is paid once per configuration and then cached on disk.
|
||||
The disk cost is real and worth knowing: ~3.4 GB of nimcache for that one
|
||||
category.
|
||||
|
||||
One property of an incremental compiler is worth spelling out because it looks
|
||||
like a test bug: **a cached stage emits no diagnostics**. `--expandArc` output, a
|
||||
hint, a warning — all of it is produced by the process that actually runs, so a
|
||||
build that reuses every artifact prints nothing. Tests that check `nimout` (and
|
||||
anything you are debugging by eye) therefore need a cold cache; running the same
|
||||
test twice in a row makes the second run's `nimout` empty.
|
||||
|
||||
The C++ backend
|
||||
===============
|
||||
|
||||
``nim cpp --ic:on`` works, and `tests/cpp` passes under it. Three things had to
|
||||
change for that, and they are worth knowing because they are the shape of every
|
||||
"C++ needs the whole program" problem the per-module backend has:
|
||||
|
||||
* **The driver must name the right file.** ``deps.nim`` DECLARES each module's
|
||||
translation unit to ``nifmake`` without loading a single module, so it cannot
|
||||
ask ``cgen.getCFile``; ``options.icCFileExt`` mirrors that formula at backend
|
||||
granularity (``.nim.cpp`` / ``.nim.m`` / ``.nim.c``).
|
||||
|
||||
* **C++ has no designated initializers**, so the RTTI record is a bare variable
|
||||
that ``DatInit`` fills field by field. That bare ``TNimTypeV2 x;`` is a
|
||||
tentative definition, which C's linker merges and C++'s does not — every TU
|
||||
that demanded the type defined it. It now gets the same extern-declaration +
|
||||
owned-``'d'``-definition split the C flavour has.
|
||||
|
||||
* **A C++ member is declared inside its class.** ``memberProcsPerType`` and
|
||||
``initializersPerType`` live only in the sem process, so the backend emitted
|
||||
the struct WITHOUT its member declarations; they are replayed from a
|
||||
``(repcppmember …)`` log entry now (``modulegraphs.replayCppMember`` re-derives
|
||||
the type from the routine's signature, exactly as ``semCppMember`` does).
|
||||
Two follow-on details: a member's ``loc.snippet`` is a CALL PATTERN
|
||||
(``#->salute(@)``), so it must be computed even in the TU that only *calls* the
|
||||
member (whole-program cgen got that for free by generating the defining module
|
||||
first), and it is not a linker name — every ``salute`` member in every class
|
||||
mints the same one, so definitions are keyed by their NIF name in the merge
|
||||
stage instead.
|
||||
|
||||
42
koch.nim
42
koch.nim
@@ -16,11 +16,12 @@ const
|
||||
ChecksumsStableCommit = "5c132cd332cce5d64a0da9ac3e4c9664313dccb4" # 0.2.2
|
||||
SatStableCommit = "9d52513b3c68bfb929dbd687d4fb2836cfee6936"
|
||||
|
||||
NimonyStableCommit = "f831b953d7c21d9a4b11d0042039e7f84d7c8dc9" # unversioned \
|
||||
NimonyStableCommit = "1721aab3cad18663da92c2b85508b1f2ff73e3df" # unversioned \
|
||||
# Note that Nimony uses Nim as a git submodule but we don't want to install
|
||||
# Nimony's dependency to Nim as we are Nim. So a `git clone` without --recursive
|
||||
# is **required** here.
|
||||
# Commit from 2026-07-10 -- stable .bif file format
|
||||
# Commit from 2026-08-31 -- nifcore-based lib; `bif.load` fills pools with
|
||||
# `addOrdered` instead of hashing every entry it just read back in order.
|
||||
|
||||
# examples of possible values for fusion: #head, #ea82b54, 1.2.3
|
||||
FusionStableHash = "#562467452b32cb7a97410ea177f083e6d8405734"
|
||||
@@ -76,7 +77,7 @@ Options:
|
||||
--skipIntegrityCheck skips integrity check when booting the compiler
|
||||
Possible Commands:
|
||||
boot [options] bootstraps with given command line options
|
||||
bootic [options] bootstraps via the incremental compiler (`nim ic`)
|
||||
bootic [options] bootstraps via the incremental compiler (`--ic:on`)
|
||||
distrohelper [bindir] helper for distro packagers
|
||||
tools builds Nim related tools
|
||||
toolsNoExternal builds Nim related tools (except external tools,
|
||||
@@ -196,10 +197,31 @@ proc bundleChecksums(latest: bool) =
|
||||
# to `koch boot`, but `nimCompileFold` spawns a fresh `nim c` that would
|
||||
# otherwise inherit the ambient configuration.
|
||||
const nifOptions = "-d:release --noNimblePath --skipUserCfg --skipParentCfg"
|
||||
if not fileExists("bin/nifler".exe):
|
||||
nimCompileFold("Compile nifler", "dist/nimony/src/nifler/nifler.nim", options = nifOptions)
|
||||
if not fileExists("bin/nifmake".exe):
|
||||
nimCompileFold("Compile nifmake", "dist/nimony/src/nifmake/nifmake.nim", options = nifOptions)
|
||||
|
||||
# Rebuilding these only when the binary is ABSENT silently keeps the tools of
|
||||
# the PREVIOUS pin: bump `NimonyStableCommit` in a checkout that already has
|
||||
# `bin/nifler`, and the compiler links the new `dist/nimony/src/lib` while
|
||||
# `nifler`/`nifmake` still speak the old one. A fresh CI checkout has no
|
||||
# `bin/`, so it builds them and looks green — only the working tree that
|
||||
# already has them breaks, which is the worst way round to find out. So stamp
|
||||
# each tool with the nimony commit it came from and rebuild on a mismatch.
|
||||
# If the commit cannot be determined (a bundled `dist` with no `.git`), fall
|
||||
# back to the old build-if-absent rule rather than rebuilding every time.
|
||||
let nimonyHead = block:
|
||||
let (outp, status) = osproc.execCmdEx(
|
||||
"git -C " & quoteShell(distDir / "nimony") & " rev-parse HEAD")
|
||||
if status == 0: outp.strip else: ""
|
||||
|
||||
proc bundleNifTool(name, src: string) =
|
||||
let stamp = "bin" / ("." & name & ".nimony-commit")
|
||||
let builtFrom = if fileExists(stamp): readFile(stamp).strip else: ""
|
||||
if not fileExists(("bin" / name).exe) or
|
||||
(nimonyHead.len > 0 and builtFrom != nimonyHead):
|
||||
nimCompileFold("Compile " & name, src, options = nifOptions)
|
||||
if nimonyHead.len > 0: writeFile(stamp, nimonyHead)
|
||||
|
||||
bundleNifTool("nifler", "dist/nimony/src/nifler/nifler.nim")
|
||||
bundleNifTool("nifmake", "dist/nimony/src/nifmake/nifmake.nim")
|
||||
|
||||
proc bundleNimsuggest(args: string) =
|
||||
bundleChecksums(false)
|
||||
@@ -450,7 +472,7 @@ proc bootic(args: string, skipIntegrityCheck: bool) =
|
||||
# everything.
|
||||
if i > 0: removeDir smartNimcache
|
||||
let nimi = if i == 0: nimStart else: i.thVersion
|
||||
exec "$# ic --nimcache:$# $# compiler" / "nim.nim" %
|
||||
exec "$# c --ic:on --nimcache:$# $# compiler" / "nim.nim" %
|
||||
[nimi, smartNimcache, args]
|
||||
if sameFileContent(output, i.thVersion):
|
||||
copyExe(output, finalDest)
|
||||
@@ -615,7 +637,7 @@ proc runIcTestFile(inp: string) =
|
||||
for fragment in content.split("#!EDIT!#"):
|
||||
let file = inp.replace(".nim", "_temp.nim")
|
||||
writeFile(file, fragment)
|
||||
var cmd = nimExe & " ic --hint:Conf:off --warnings:off "
|
||||
var cmd = nimExe & " c --ic:on --hint:Conf:off --warnings:off "
|
||||
cmd.add quoteShell(file)
|
||||
exec(cmd)
|
||||
|
||||
@@ -625,7 +647,7 @@ proc runIcTestFile(inp: string) =
|
||||
const icSuite = ["thallo", "tconverter", "timp", "tmiscs", "tparseutils",
|
||||
"tcompiletimeglobal", "tsighashstable", "tpureenum", "tgenericoffer",
|
||||
"tconverterreexport", "ttypeoffer", "ttransitiveoffer",
|
||||
"tmodsymref", "tmethupref", "temit", "ttraitparam"]
|
||||
"tmodsymref", "tmethupref", "temit", "ttraitparam", "tnestasgn"]
|
||||
|
||||
proc icTest(args: string) =
|
||||
temp("")
|
||||
|
||||
@@ -17,6 +17,7 @@ __AVR__
|
||||
__arm__
|
||||
__riscv
|
||||
__EMSCRIPTEN__
|
||||
__unix__
|
||||
*/
|
||||
|
||||
|
||||
@@ -597,7 +598,7 @@ NIM_STATIC_ASSERT(sizeof(NI) == sizeof(void*) && NIM_INTBITS == sizeof(NI)*8, "P
|
||||
#define nimMulInt64(a, b, res) __builtin_smulll_overflow(a, b, (long long int*)res)
|
||||
|
||||
#if NIM_INTBITS == 32
|
||||
#if (defined(__arm__) || defined(__riscv)) && defined(__GNUC__)
|
||||
#if ((defined(__arm__) && !defined(__unix__)) || defined(__riscv)) && defined(__GNUC__)
|
||||
/* arm-none-eabi-gcc and riscv32-unknown-elf-gcc targets define int32_t as long int */
|
||||
#define nimAddInt(a, b, res) __builtin_saddl_overflow(a, b, res)
|
||||
#define nimSubInt(a, b, res) __builtin_ssubl_overflow(a, b, res)
|
||||
|
||||
@@ -3304,13 +3304,21 @@ proc dirInclude(p: var RstParser): PRstNode =
|
||||
## Only the content before the first occurrence of the specified
|
||||
## text (but after any after text) will be included. If text is
|
||||
## not found inclusion will happen until the end of the file.
|
||||
#literal : flag (empty)
|
||||
# The entire included text is inserted into the document as a single
|
||||
# literal block (useful for program listings).
|
||||
#encoding : name of text encoding
|
||||
# The text encoding of the external data file. Defaults to the document's
|
||||
# encoding (if specified).
|
||||
#
|
||||
##
|
||||
## :literal: flag (empty)
|
||||
##
|
||||
## The entire included text is inserted into the document as a single
|
||||
## literal block (useful for program listings).
|
||||
##
|
||||
## :code: language (if empty, `nim` is assumed by default)
|
||||
##
|
||||
## The argument and the included content are passed to the code directive
|
||||
## (useful for program listings).
|
||||
##
|
||||
## :encoding: name of text encoding
|
||||
##
|
||||
## The text encoding of the external data file. Defaults to the document's
|
||||
## encoding (if specified).
|
||||
result = nil
|
||||
var n = parseDirective(p, rnDirective, {hasArg, argIsFile, hasOptions}, nil)
|
||||
var filename = strip(addNodes(n.sons[0]))
|
||||
@@ -3319,31 +3327,44 @@ proc dirInclude(p: var RstParser): PRstNode =
|
||||
rstMessage(p, meCannotOpenFile, filename)
|
||||
else:
|
||||
# XXX: error handling; recursive file inclusion!
|
||||
let inputString = readFile(path)
|
||||
let startPosition =
|
||||
block:
|
||||
let searchFor = n.getFieldValue("start-after").strip()
|
||||
if searchFor != "":
|
||||
let pos = inputString.find(searchFor)
|
||||
if pos != -1: pos + searchFor.len
|
||||
else: 0
|
||||
else:
|
||||
0
|
||||
|
||||
let endPosition =
|
||||
block:
|
||||
let searchFor = n.getFieldValue("end-before").strip()
|
||||
if searchFor != "":
|
||||
let pos = inputString.find(searchFor, start = startPosition)
|
||||
if pos != -1: pos - 1
|
||||
else: 0
|
||||
else:
|
||||
inputString.len - 1
|
||||
|
||||
if getFieldValue(n, "literal") != "":
|
||||
result = newRstNode(rnLiteralBlock)
|
||||
result.add newLeaf(readFile(path))
|
||||
result.add newLeaf(inputString[startPosition..endPosition])
|
||||
elif getFieldValue(n, "code") != "":
|
||||
result = newRstNode(rnCodeBlock)
|
||||
result.sons.setLen(3)
|
||||
let lang = getFieldValue(n, "code").strip()
|
||||
if lang notin ["", "\x01\x01"]:
|
||||
var codeArg = newRstNode(rnDirArg)
|
||||
codeArg.add(newLeaf(lang))
|
||||
result.sons[0] = codeArg
|
||||
result.sons[1] = newRstNode(rnFieldList)
|
||||
defaultCodeLangNim(p, result)
|
||||
var litBlock = newRstNode(rnLiteralBlock)
|
||||
litBlock.add newLeaf(inputString[startPosition..endPosition])
|
||||
result.sons[2] = litBlock
|
||||
else:
|
||||
let inputString = readFile(path)
|
||||
let startPosition =
|
||||
block:
|
||||
let searchFor = n.getFieldValue("start-after").strip()
|
||||
if searchFor != "":
|
||||
let pos = inputString.find(searchFor)
|
||||
if pos != -1: pos + searchFor.len
|
||||
else: 0
|
||||
else:
|
||||
0
|
||||
|
||||
let endPosition =
|
||||
block:
|
||||
let searchFor = n.getFieldValue("end-before").strip()
|
||||
if searchFor != "":
|
||||
let pos = inputString.find(searchFor, start = startPosition)
|
||||
if pos != -1: pos - 1
|
||||
else: 0
|
||||
else:
|
||||
inputString.len - 1
|
||||
|
||||
var q: RstParser
|
||||
initParser(q, p.s)
|
||||
let saveFileIdx = p.s.currFileIdx
|
||||
|
||||
@@ -152,9 +152,11 @@ proc parseProtocol(protocol: string): tuple[orig: string, major, minor: int] =
|
||||
raise newException(ValueError, "Invalid request protocol. Got: " &
|
||||
protocol)
|
||||
result.orig = protocol
|
||||
i.inc protocol.parseSaturatedNatural(result.major, i)
|
||||
if i < protocol.len: inc i # Skip .
|
||||
i.inc protocol.parseSaturatedNatural(result.minor, i)
|
||||
var n = protocol.parseSaturatedNatural(result.major, i)
|
||||
i.inc n
|
||||
if i < protocol.len and protocol[i] == '.':
|
||||
inc i
|
||||
n = protocol.parseSaturatedNatural(result.minor, i)
|
||||
|
||||
proc sendStatus(client: AsyncSocket, status: string): Future[void] =
|
||||
client.send("HTTP/1.1 " & status & "\c\L\c\L")
|
||||
|
||||
@@ -238,6 +238,7 @@ proc fromJson*[T](a: var T, b: JsonNode, opt = Joptions()) =
|
||||
a = T()
|
||||
fromJson(a[], b, opt)
|
||||
elif T is array:
|
||||
checkJson b.kind == JArray
|
||||
checkJson a.len == b.len, "Json array size doesn't match for " & $T
|
||||
var i = 0
|
||||
for ai in mitems(a):
|
||||
@@ -248,6 +249,7 @@ proc fromJson*[T](a: var T, b: JsonNode, opt = Joptions()) =
|
||||
for val in b.getElems:
|
||||
incl a, jsonTo(val, E)
|
||||
elif T is seq:
|
||||
checkJson b.kind == JArray
|
||||
a.setLen b.len
|
||||
for i, val in b.getElems:
|
||||
fromJson(a[i], val, opt)
|
||||
|
||||
@@ -1254,7 +1254,9 @@ proc del*[T](x: var seq[T], i: Natural) {.noSideEffect.} =
|
||||
a.del(2)
|
||||
assert a == @[10, 11, 14, 13]
|
||||
let xl = x.len - 1
|
||||
movingCopy(x[i], x[xl])
|
||||
# Avoid moving the element onto itself when deleting the last item.
|
||||
if i != xl:
|
||||
movingCopy(x[i], x[xl])
|
||||
setLen(x, xl)
|
||||
|
||||
proc insert*[T](x: var seq[T], item: sink T, i = 0.Natural) {.noSideEffect.} =
|
||||
|
||||
@@ -155,15 +155,17 @@ type
|
||||
|
||||
MemRegion = object
|
||||
when usesRegionHandles:
|
||||
# Keeping the handle here does change the layout, but until proven otherwise
|
||||
# this layout is more readable and shouldn't regress performance.
|
||||
regionHandle: ptr RegionHandle
|
||||
when not defined(gcDestructors):
|
||||
minLargeObj, maxLargeObj: int
|
||||
freeSmallChunks: array[0..max(1, SmallChunkSize div MemAlign-1), PSmallChunk]
|
||||
# List of available chunks per size class. Only one is expected to be active per class.
|
||||
when defined(gcDestructors):
|
||||
when defined(gcDestructors) and not usesRegionHandles:
|
||||
sharedFreeLists: SharedFreeLists
|
||||
# Used directly without threads. Threaded builds use RegionHandle but
|
||||
# retain this 2 KiB spacer: removing it regresses 2-4 KiB allocations.
|
||||
# Remote-free buckets live on the MemRegion when there is no
|
||||
# RegionHandle. Threaded memory managers with handles keep them on the handle instead.
|
||||
flBitmap: uint32
|
||||
slBitmap: array[RealFli, uint32]
|
||||
matrix: array[RealFli, array[MaxSli, PBigChunk]]
|
||||
@@ -963,13 +965,19 @@ proc bigChunkAlignOffset(alignment: int): int {.inline.} =
|
||||
else:
|
||||
result = align(sizeof(BigChunk) + sizeof(FreeCell), alignment) - sizeof(BigChunk) - sizeof(FreeCell)
|
||||
|
||||
proc rawAlloc(a: var MemRegion, requestedSize: int, alignment: int = 0): pointer =
|
||||
template rawAllocAux(aligned: static bool) {.dirty.} =
|
||||
when defined(nimTypeNames):
|
||||
inc(a.allocCounter)
|
||||
sysAssert(allocInv(a), "rawAlloc: begin")
|
||||
sysAssert(roundup(65, 8) == 72, "rawAlloc: roundup broken")
|
||||
var size = roundup(requestedSize, max(MemAlign, alignment))
|
||||
let alignOff = smallChunkAlignOffset(alignment)
|
||||
when aligned:
|
||||
var size = roundup(requestedSize, max(MemAlign, alignment))
|
||||
let alignOff = smallChunkAlignOffset(alignment)
|
||||
else:
|
||||
# Common `alloc` path: no custom alignment. Keep this a separate
|
||||
# instantiation so clang does not emit `smallChunkAlignOffset(0)`.
|
||||
var size = (requestedSize + (MemAlign - 1)) and not (MemAlign - 1)
|
||||
const alignOff = 0
|
||||
sysAssert(size >= sizeof(FreeCell), "rawAlloc: requested size too small")
|
||||
sysAssert(size >= requestedSize, "insufficient allocated size!")
|
||||
#c_fprintf(stdout, "alloc; size: %ld; %ld\n", requestedSize, size)
|
||||
@@ -986,11 +994,13 @@ proc rawAlloc(a: var MemRegion, requestedSize: int, alignment: int = 0): pointer
|
||||
if atomicLoadN(sharedHead, ATOMIC_RELAXED) != nil:
|
||||
tc.freeList = atomicExchangeN(sharedHead, nil, ATOMIC_ACQUIRE)
|
||||
else:
|
||||
tc.freeList = a.sharedFreeLists[s]
|
||||
a.sharedFreeLists[s] = nil
|
||||
# If `tc.freeList` isn't nil, `tc` gains capacity. Calculate how
|
||||
# much it gained and how many foreign cells are included.
|
||||
compensateCounters(a, tc, size)
|
||||
let sharedHead = addr a.sharedFreeLists[s]
|
||||
tc.freeList = sharedHead[]
|
||||
sharedHead[] = nil
|
||||
# Empty peeks are the common local case; skip the walk and the
|
||||
# `free += 0` / `occ -= 0` stores clang would otherwise keep.
|
||||
if tc.freeList != nil:
|
||||
compensateCounters(a, tc, size)
|
||||
|
||||
# allocate a small block: for small chunks, we use only its next pointer
|
||||
let s = size div MemAlign
|
||||
@@ -1071,7 +1081,7 @@ proc rawAlloc(a: var MemRegion, requestedSize: int, alignment: int = 0): pointer
|
||||
# For big chunks with custom alignment, allocate extra space.
|
||||
# Since chunks are page-aligned, the needed padding is a compile-time
|
||||
# deterministic value rather than a worst-case estimate.
|
||||
let alignPad = bigChunkAlignOffset(alignment)
|
||||
let alignPad = when aligned: bigChunkAlignOffset(alignment) else: 0
|
||||
size = requestedSize + bigChunkOverhead() + alignPad
|
||||
# allocate a large block
|
||||
var c = if size >= HugeChunkSize: getHugeChunk(a, size)
|
||||
@@ -1096,6 +1106,12 @@ proc rawAlloc(a: var MemRegion, requestedSize: int, alignment: int = 0): pointer
|
||||
when defined(heaptrack):
|
||||
heaptrack_malloc(result, requestedSize)
|
||||
|
||||
proc rawAlloc(a: var MemRegion, requestedSize: int): pointer =
|
||||
rawAllocAux(false)
|
||||
|
||||
proc rawAlloc(a: var MemRegion, requestedSize: int, alignment: int): pointer =
|
||||
rawAllocAux(true)
|
||||
|
||||
proc rawAlloc0(a: var MemRegion, requestedSize: int): pointer =
|
||||
result = rawAlloc(a, requestedSize)
|
||||
zeroMem(result, requestedSize)
|
||||
|
||||
@@ -516,7 +516,15 @@ proc testNimblePackages(r: var TResults; cat: Category; packageFilter: string) =
|
||||
# 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
|
||||
# fails: <substring> no-oracle
|
||||
# The last step always also runs the clean==incremental check.
|
||||
#
|
||||
# Every successful step is ALSO compiled with `nim c` and run, and the two
|
||||
# outputs must agree (`no-oracle` opts out). This is the only check in the suite
|
||||
# that is not IC-against-IC; without it a consistently wrong IC passes
|
||||
# everything. `#!DELETE <file>` removes a module, `#!FLAGS <switches>` changes
|
||||
# the compiler switches from that point on, and `fails: <text>` asserts that
|
||||
# BOTH compilers reject the program with that text.
|
||||
|
||||
type MetamorphicError = object of CatchableError
|
||||
resultKind: TResultEnum
|
||||
@@ -590,16 +598,34 @@ proc runMetamorphicIcTest(r: var TResults; file: string; cat: Category; options:
|
||||
let buildDir = (file.changeFileExt("") & "_mm").absolutePath
|
||||
let nc = buildDir / "nc"
|
||||
let bin = buildDir / "prog".addFileExt(ExeExt)
|
||||
# The ORACLE: the same sources compiled by the classic backend. Every
|
||||
# invariant this runner checked before was IC-against-IC (clean == incremental,
|
||||
# no-op changes nothing, ...), which a *consistently* wrong IC satisfies
|
||||
# perfectly — that is how a whole class of silent miscompilations (top-level
|
||||
# destructors never injected; `nfFirstWrite`/`nfLastRead` dropped by the
|
||||
# serializer, so every first assignment to a destructor-bearing local became
|
||||
# `=sink` over zeroed memory) stayed invisible. `nim c` is the reference the
|
||||
# suite was missing.
|
||||
let ncRef = buildDir / "ncref"
|
||||
let binRef = buildDir / "progref".addFileExt(ExeExt)
|
||||
removeDir(buildDir)
|
||||
createDir(buildDir)
|
||||
|
||||
# Extra switches for both compilers, settable per step via `#!FLAGS`.
|
||||
var extraFlags: seq[string] = @[]
|
||||
|
||||
template compileIc(): untyped =
|
||||
execCmdEx2(compilerPrefix, ["ic", "--hint:Conf:off", "--warnings:off",
|
||||
"--nimcache:" & nc, "--out:" & bin, "main.nim"],
|
||||
execCmdEx2(compilerPrefix, @["ic", "--hint:Conf:off", "--warnings:off",
|
||||
"--nimcache:" & nc, "--out:" & bin] & extraFlags & @["main.nim"],
|
||||
workingDir = buildDir)
|
||||
|
||||
template compileRef(): untyped =
|
||||
execCmdEx2(compilerPrefix, @["c", "--hint:Conf:off", "--warnings:off",
|
||||
"--nimcache:" & ncRef, "--out:" & binRef] & extraFlags & @["main.nim"],
|
||||
workingDir = buildDir)
|
||||
|
||||
# Parse the source into a flat op list: ("file", name, content) | ("step", attrs, "").
|
||||
type OpKind = enum opFile, opStep
|
||||
type OpKind = enum opFile, opStep, opDelete, opFlags
|
||||
type Op = object
|
||||
kind: OpKind
|
||||
a, b: string
|
||||
@@ -615,6 +641,18 @@ proc runMetamorphicIcTest(r: var TResults; file: string; cat: Category; options:
|
||||
if s.startsWith("#!FILE"):
|
||||
flushFile()
|
||||
curName = s["#!FILE".len .. ^1].strip
|
||||
elif s.startsWith("#!DELETE"):
|
||||
# Remove a module from the virtual file system AND from disk. Deleting a
|
||||
# still-imported file moves no mtime, so nothing in an mtime-keyed build
|
||||
# re-fires: `nim ic` used to relink a stale binary where `nim c` reports
|
||||
# `cannot open file`. Untestable until the format could express it.
|
||||
flushFile()
|
||||
ops.add Op(kind: opDelete, a: s["#!DELETE".len .. ^1].strip)
|
||||
elif s.startsWith("#!FLAGS"):
|
||||
# Change the compiler switches for the following steps. Config changes
|
||||
# are not files, so an mtime-keyed build cannot see them either.
|
||||
flushFile()
|
||||
ops.add Op(kind: opFlags, a: s["#!FLAGS".len .. ^1].strip)
|
||||
elif s.startsWith("#!STEP"):
|
||||
flushFile()
|
||||
ops.add Op(kind: opStep, a: s["#!STEP".len .. ^1].strip)
|
||||
@@ -630,11 +668,21 @@ proc runMetamorphicIcTest(r: var TResults; file: string; cat: Category; options:
|
||||
var prevSnap = initTable[string, string]()
|
||||
var prevBin = ""
|
||||
var stepIdx = 0
|
||||
var deleted: seq[string] = @[]
|
||||
try:
|
||||
for o in ops:
|
||||
if o.kind == opFile:
|
||||
case o.kind
|
||||
of opFile:
|
||||
vfs[o.a] = o.b
|
||||
continue
|
||||
of opDelete:
|
||||
vfs.del o.a
|
||||
deleted.add o.a
|
||||
continue
|
||||
of opFlags:
|
||||
extraFlags = o.a.splitWhitespace()
|
||||
continue
|
||||
of opStep: discard
|
||||
inc stepIdx
|
||||
let where = "step " & $stepIdx
|
||||
# Parse step attributes.
|
||||
@@ -646,8 +694,36 @@ proc runMetamorphicIcTest(r: var TResults; file: string; cat: Category; options:
|
||||
if c >= 0: attrs[p[0 ..< c].strip] = p[c+1 .. ^1].strip
|
||||
else: attrs[p] = ""
|
||||
|
||||
for fn in deleted:
|
||||
removeFile(buildDir / fn)
|
||||
deleted.setLen 0
|
||||
for fn, content in vfs: writeFile(buildDir / fn, content)
|
||||
let (_, cout, ccode) = compileIc()
|
||||
|
||||
# `fails: <substring>` — the build MUST fail, with that text in its output.
|
||||
# Without this every step had to succeed, so the whole error path was
|
||||
# untested: a `nim m` that errored still wrote its `.s.bif`, nifmake then
|
||||
# saw the rule satisfied, and the NEXT run reported success for a program
|
||||
# that does not compile.
|
||||
if "fails" in attrs:
|
||||
if ccode == 0:
|
||||
mmRaise(reBuildFailed, "a failed build", where & ": `nim ic` unexpectedly succeeded")
|
||||
let want = attrs["fails"]
|
||||
if want.len > 0 and want notin cout:
|
||||
mmRaise(reOutputsDiffer, want, where & ": error text did not contain it:\n" & cout)
|
||||
# The oracle must reject it too, else the test is asserting an IC-only
|
||||
# error rather than a real one.
|
||||
let (_, refOut, refCode) = compileRef()
|
||||
if refCode == 0:
|
||||
mmRaise(reBuildFailed, "`nim c` to fail too",
|
||||
where & ": `nim ic` failed but `nim c` accepted the program:\n" & cout)
|
||||
if want.len > 0 and want notin refOut:
|
||||
mmRaise(reOutputsDiffer, want,
|
||||
where & ": `nim c` failed differently:\n" & refOut)
|
||||
prevSnap = snapshotDir(nc)
|
||||
prevBin = ""
|
||||
continue
|
||||
|
||||
if ccode != 0:
|
||||
mmRaise(reBuildFailed, "", where & ": `nim ic` failed:\n" & cout)
|
||||
let (_, rout, rcode) = execCmdEx2(bin.absolutePath, [], workingDir = buildDir)
|
||||
@@ -658,6 +734,22 @@ proc runMetamorphicIcTest(r: var TResults; file: string; cat: Category; options:
|
||||
if rout.strip == want.strip: discard
|
||||
else: mmRaise(reOutputsDiffer, want, where & " output:\n" & rout.strip)
|
||||
|
||||
# ORACLE: same sources through the classic backend, same observable
|
||||
# behaviour. Unlike `expect:` this needs no foresight from the test author —
|
||||
# it compares everything the program does, not only what someone thought to
|
||||
# print, which is exactly what a silently-skipped destructor evades.
|
||||
block oracle:
|
||||
if "no-oracle" in attrs: break oracle
|
||||
let (_, refCout, refCcode) = compileRef()
|
||||
if refCcode != 0:
|
||||
mmRaise(reBuildFailed, "", where & ": `nim c` (oracle) failed:\n" & refCout)
|
||||
let (_, refRout, refRcode) = execCmdEx2(binRef.absolutePath, [],
|
||||
workingDir = buildDir)
|
||||
if refRout.strip != rout.strip or refRcode != rcode:
|
||||
mmRaise(reOutputsDiffer, "`nim c` output:\n" & refRout.strip,
|
||||
where & ": `nim ic` disagrees with `nim c`\n ic (exit " & $rcode &
|
||||
"):\n" & rout.strip & "\n c (exit " & $refRcode & "):\n" & refRout.strip)
|
||||
|
||||
let snap = snapshotDir(nc)
|
||||
let binBytes = stableBinary(bin)
|
||||
if stepIdx > 1:
|
||||
@@ -755,6 +847,12 @@ proc processSingleTest(r: var TResults, cat: Category, options, test: string, ta
|
||||
let target = if cat.string.normalize == "js": targetJS else: targetC
|
||||
targets = {target}
|
||||
doAssert fileExists(test), test & " test does not exist"
|
||||
# `testament r <file>` must dispatch metamorphic IC tests the same way
|
||||
# `testament cat ic` does, otherwise a single-test run tries to parse the
|
||||
# header as an ordinary spec and rejects it.
|
||||
if isMetamorphicIcTest(readFile(test)):
|
||||
runMetamorphicIcTest(r, test, cat, options)
|
||||
return
|
||||
testSpec r, makeTest(test, options, cat), targets
|
||||
|
||||
proc isJoinableSpec(spec: TSpec): bool =
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import
|
||||
std/[strutils, pegs, os, osproc, streams, json,
|
||||
parseopt, browsers, terminal, exitprocs,
|
||||
algorithm, times, intsets, macros]
|
||||
algorithm, times, intsets, macros, tables]
|
||||
|
||||
import backend, specs, azure, htmlgen
|
||||
|
||||
@@ -35,6 +35,12 @@ var simulate = false
|
||||
var optVerbose = false
|
||||
var useMegatest = true
|
||||
var valgrindEnabled = true
|
||||
var useIc = false
|
||||
## `--ic`: compile every C-target test with `nim ic` instead of `nim c`, so the
|
||||
## incremental compiler inherits the whole existing corpus (~10k programs with
|
||||
## expected output) instead of the handful of tests written for it by hand.
|
||||
## Every invariant the `tests/ic` suite checks is IC-against-IC; this is the
|
||||
## part that compares IC against the reference backend at scale.
|
||||
|
||||
proc verboseCmd(cmd: string) =
|
||||
if optVerbose:
|
||||
@@ -58,6 +64,7 @@ Arguments:
|
||||
Options:
|
||||
--print print results to the console
|
||||
--verbose print commands (compiling and running tests)
|
||||
--ic compile C-target tests with `nim ic` (incremental)
|
||||
--simulate see what tests would be run but don't run them (for debugging)
|
||||
--failing only show failing/ignored tests
|
||||
--targets:"c cpp js objc" run tests for specified targets (default: c)
|
||||
@@ -155,11 +162,40 @@ proc execCmdEx2(command: string, args: openArray[string]; workingDir: string = "
|
||||
if result.exitCode != -1: break
|
||||
close(p)
|
||||
|
||||
proc nimcacheDir(filename, options: string, target: TTarget): string =
|
||||
proc nimcacheDir(filename, options: string, target: TTarget,
|
||||
extraOptions = ""): string =
|
||||
## Give each test a private nimcache dir so they don't clobber each other's.
|
||||
let hashInput = options & $target
|
||||
## `extraOptions` (a `matrix:` entry) is part of the key: two matrix variants
|
||||
## of one file are two different compilations, and sharing a cache between them
|
||||
## means each run invalidates what the previous left. Harmless for the classic
|
||||
## backend, which caches only object files, but it makes an incremental cache
|
||||
## useless — every variant re-sems the world every time.
|
||||
let hashInput = options & extraOptions & $target
|
||||
result = "nimcache" / (filename & '_' & hashInput.getMD5)
|
||||
|
||||
const icWarmupSource = """
|
||||
# Generated by testament for `--ic`. Compiling this once fills a shared IC cache
|
||||
# with `system` and the stdlib modules the test corpus imports most, so each
|
||||
# test's own cold build starts from precompiled NIFs instead of re-semming the
|
||||
# world. Mirrors nimony's hastur `tools/warmup.nim` + `prefillFromWarmup`.
|
||||
import std/[assertions, macros, strutils, tables, os, typetraits, sequtils,
|
||||
sugar, math, options, times, json, sets, algorithm, hashes,
|
||||
strformat, parseutils, streams, unicode]
|
||||
|
||||
proc icWarmupAnchor*(): int =
|
||||
# Reference a few generic instantiations the corpus leans on so their
|
||||
# `.c.nif` artifacts are precompiled too, not just the modules' interfaces.
|
||||
var t = initTable[string, int]()
|
||||
t["a"] = 1
|
||||
var s = @[1, 2, 3]
|
||||
s.sort()
|
||||
result = s.len + t.len + "x".repeat(2).len
|
||||
"""
|
||||
|
||||
var icWarmupCaches: Table[string, string]
|
||||
## Compile-config key -> shared warm IC cache (or "" when unavailable).
|
||||
var buildingIcWarmup = false
|
||||
|
||||
proc prepareTestCmd(cmdTemplate, filename, options, nimcache: string,
|
||||
target: TTarget, extraOptions = ""): string =
|
||||
var options = target.defaultOptions & ' ' & options
|
||||
@@ -169,9 +205,103 @@ proc prepareTestCmd(cmdTemplate, filename, options, nimcache: string,
|
||||
result = cmdTemplate % ["target", targetToCmd[target],
|
||||
"options", options, "file", filename.quoteShell,
|
||||
"filedir", filename.getFileDir(), "nim", compilerPrefix]
|
||||
if useIc and target in {targetC, targetCpp}:
|
||||
# `--ic:on` turns the ordinary compile command into the IC driver, so the
|
||||
# verb is left alone: roughly half the corpus overrides the command wholesale
|
||||
# (`cmd: "nim c --gc:arc $file"`), which neither goes through `$target` nor
|
||||
# picks up `$options`, and such a test now simply gains the switch. Each also
|
||||
# gets a private nimcache, which is what makes it incremental at all.
|
||||
#
|
||||
# Switches must land BEFORE the project file: anything after it is swallowed
|
||||
# into `config.arguments`, and a non-empty `arguments` without `--run` is a
|
||||
# hard error ("arguments can only be given if the '--run' option is
|
||||
# selected").
|
||||
var switches = "--ic:on "
|
||||
if nimcache.len > 0 and "--nimCache:" notin result and "--nimcache:" notin result:
|
||||
switches.add "--nimCache:" & nimcache.quoteShell & " "
|
||||
# `rfind`, not `find`: the private nimcache path embeds the test's file name
|
||||
# (`nimcache/tests/destructor/tmove.nim_<hash>`), so the FIRST occurrence is
|
||||
# inside a switch's value. The project file is the last one.
|
||||
let fileArg = filename.quoteShell
|
||||
let at = result.rfind(fileArg)
|
||||
if at >= 0: result = result[0 ..< at] & switches & result[at .. ^1]
|
||||
else: result.add " " & switches
|
||||
|
||||
proc icWarmupCache(cmdTemplate, filename, options: string, target: TTarget,
|
||||
extraOptions: string): string =
|
||||
## The shared warm cache for this test's exact compile configuration, built on
|
||||
## first use and kept in `nimcache/` across runs. Keyed by the switches AND the
|
||||
## test's directory, because both decide what the artifacts contain: the
|
||||
## switches through `-d:`/`--mm:` etc., the directory through the `nim.cfg` /
|
||||
## `config.nims` it inherits. A cache built under a different configuration
|
||||
## would just be invalidated wholesale on first use, which is worse than none.
|
||||
if buildingIcWarmup: return ""
|
||||
let dir = filename.getFileDir()
|
||||
let key = options & extraOptions & $target & dir
|
||||
if icWarmupCaches.hasKey(key): return icWarmupCaches[key]
|
||||
result = "nimcache" / ("ic_warmup_" & key.getMD5)
|
||||
icWarmupCaches[key] = result
|
||||
if dirExists(result / "ic.version"): return # already built by an earlier run
|
||||
if fileExists(result / "ic.version"): return
|
||||
# The warmup must live in the test's own directory so it inherits the same
|
||||
# config files; a stray `.nim` there is not picked up as a test (testament
|
||||
# only collects `t*.nim`). The name must be a valid Nim identifier.
|
||||
let src = dir / "icwarmup_generated.nim"
|
||||
try:
|
||||
writeFile(src, icWarmupSource)
|
||||
except IOError, OSError:
|
||||
icWarmupCaches[key] = ""
|
||||
return ""
|
||||
buildingIcWarmup = true
|
||||
let cmd = prepareTestCmd(cmdTemplate, src, options, result, target, extraOptions)
|
||||
let (outp, code) = execCmdEx(cmd)
|
||||
buildingIcWarmup = false
|
||||
try: removeFile(src)
|
||||
except OSError: discard
|
||||
if code != 0:
|
||||
# Non-fatal: without a warm cache every test just pays its own cold build.
|
||||
if optVerbose: echo "ic warmup failed: ", cmd, "\n", outp
|
||||
icWarmupCaches[key] = ""
|
||||
return ""
|
||||
|
||||
proc prefillIcCache(warmup, nimcache: string) =
|
||||
## Seed a test's empty cache from the shared warm one. Only the artifacts that
|
||||
## do NOT depend on which program is being built are copied: the frontend NIFs
|
||||
## and cookies, plus the per-module `lower`/`cg` outputs. The `.c`/`.o` are
|
||||
## deliberately left out — the merge decision (who owns each emit-everywhere
|
||||
## definition) is whole-program, so those get re-rendered for every program
|
||||
## anyway and copying them is pure I/O.
|
||||
##
|
||||
## Mtimes are preserved, and that is load-bearing: nifmake decides staleness by
|
||||
## output-mtime > input-mtime, so stamping every prefilled file with "now"
|
||||
## would scramble the DAG ordering the warmup established and re-fire the
|
||||
## whole graph — exactly what the copy is meant to avoid.
|
||||
if warmup.len == 0 or not dirExists(warmup): return
|
||||
if dirExists(nimcache): return # the test already has its own cache
|
||||
const wanted = [".p.nif", ".p.deps.nif", ".deps.nif", ".s.bif", ".iface.bif",
|
||||
".impl.bif", ".edges.bif", ".s.deps.bif", ".t.bif",
|
||||
".c.nif", ".cpp.nif"]
|
||||
try:
|
||||
createDir(nimcache)
|
||||
for path in walkFiles(warmup / "*"):
|
||||
let name = path.extractFilename
|
||||
var take = name == "ic.version" or name == "ic_build_args.txt"
|
||||
if not take:
|
||||
for ext in wanted:
|
||||
if name.endsWith(ext): take = true; break
|
||||
if not take: continue
|
||||
let dst = nimcache / name
|
||||
copyFile(path, dst)
|
||||
try: setLastModificationTime(dst, getLastModificationTime(path))
|
||||
except OSError, IOError: discard
|
||||
except OSError, IOError:
|
||||
discard # best effort; a cold build still works
|
||||
|
||||
proc callNimCompiler(cmdTemplate, filename, options, nimcache: string,
|
||||
target: TTarget, extraOptions = ""): TSpec =
|
||||
if useIc and target in {targetC, targetCpp} and nimcache.len > 0 and not buildingIcWarmup:
|
||||
prefillIcCache(icWarmupCache(cmdTemplate, filename, options, target, extraOptions),
|
||||
nimcache)
|
||||
result = TSpec(cmd: prepareTestCmd(cmdTemplate, filename, options, nimcache, target,
|
||||
extraOptions))
|
||||
verboseCmd(result.cmd)
|
||||
@@ -415,21 +545,28 @@ proc cmpMsgs(r: var TResults, expected, given: TSpec, test: TTest,
|
||||
result = r.finishTestRetryable(test, target, extraOptions, expected.msg, given.msg, reSuccess)
|
||||
inc(r.passed)
|
||||
|
||||
proc generatedFile(test: TTest, target: TTarget): string =
|
||||
proc generatedFile(test: TTest, target: TTarget, extraOptions: string): string =
|
||||
if target == targetJS:
|
||||
result = test.name.changeFileExt("js")
|
||||
else:
|
||||
let (_, name, _) = test.name.splitFile
|
||||
let ext = targetToExt[target]
|
||||
result = nimcacheDir(test.name, test.options, target) / "@m" & name.changeFileExt(ext)
|
||||
# `extraOptions` must match what `testSpecWithNimcache` passed to the
|
||||
# compiler — the matrix entry is part of the nimcache key, so leaving it out
|
||||
# here looks for the `.c` of a DIFFERENT variant's cache (which does not
|
||||
# exist) and every `ccodeCheck` test with a `matrix:` failed as
|
||||
# `reCodeNotFound`.
|
||||
result = nimcacheDir(test.name, test.options, target, extraOptions) /
|
||||
"@m" & name.changeFileExt(ext)
|
||||
|
||||
proc needsCodegenCheck(spec: TSpec): bool =
|
||||
result = spec.maxCodeSize > 0 or spec.ccodeCheck.len > 0
|
||||
|
||||
proc codegenCheck(test: TTest, target: TTarget, spec: TSpec, expectedMsg: var string,
|
||||
proc codegenCheck(test: TTest, target: TTarget, extraOptions: string,
|
||||
spec: TSpec, expectedMsg: var string,
|
||||
given: var TSpec) =
|
||||
try:
|
||||
let genFile = generatedFile(test, target)
|
||||
let genFile = generatedFile(test, target, extraOptions)
|
||||
let contents = readFile(genFile)
|
||||
for check in spec.ccodeCheck:
|
||||
if check.len > 0 and check[0] == '\\':
|
||||
@@ -457,7 +594,7 @@ proc compilerOutputTests(test: TTest, target: TTarget, extraOptions: string,
|
||||
var givenmsg: string = ""
|
||||
if given.err == reSuccess:
|
||||
if expected.needsCodegenCheck:
|
||||
codegenCheck(test, target, expected, expectedmsg, given)
|
||||
codegenCheck(test, target, extraOptions, expected, expectedmsg, given)
|
||||
givenmsg = given.msg
|
||||
if not nimoutCheck(expected, given) or
|
||||
not checkForInlineErrors(expected, given):
|
||||
@@ -590,7 +727,7 @@ proc targetHelper(r: var TResults, test: TTest, expected: TSpec, extraOptions: s
|
||||
inc count
|
||||
echo "testSpec count: ", count, " expected: ", expected
|
||||
else:
|
||||
let nimcache = nimcacheDir(test.name, test.options, target)
|
||||
let nimcache = nimcacheDir(test.name, test.options, target, extraOptions)
|
||||
var testClone = test
|
||||
let target = changeTarget(extraOptions, target)
|
||||
testSpecHelper(r, testClone, expected, target, extraOptions, nimcache)
|
||||
@@ -691,6 +828,7 @@ proc main() =
|
||||
case p.key.normalize
|
||||
of "print": optPrintResults = true
|
||||
of "verbose": optVerbose = true
|
||||
of "ic": useIc = true
|
||||
of "failing": optFailing = true
|
||||
of "pedantic": discard # deadcode refs https://github.com/nim-lang/Nim/issues/16731
|
||||
of "targets":
|
||||
|
||||
@@ -35,6 +35,20 @@ proc bug20303() =
|
||||
|
||||
bug20303()
|
||||
|
||||
block: # bug #26143
|
||||
var indexCalls = 0
|
||||
|
||||
proc nextIndex(): int =
|
||||
result = indexCalls
|
||||
inc indexCalls
|
||||
|
||||
proc consume(value: sink string) =
|
||||
doAssert value == "A"
|
||||
|
||||
var values = @["A", "B"]
|
||||
consume(values[nextIndex()])
|
||||
doAssert indexCalls == 1
|
||||
|
||||
proc main() = # todo bug with templates
|
||||
block: # bug #11267
|
||||
var a: seq[char] = block: @[]
|
||||
|
||||
4
tests/ccgbugs/mcodegendeclglobal.nim
Normal file
4
tests/ccgbugs/mcodegendeclglobal.nim
Normal file
@@ -0,0 +1,4 @@
|
||||
var codegenDeclGlobal* {.codegenDecl: "$# /* custom declaration */ $#".} = 123
|
||||
|
||||
proc readCodegenDeclGlobal*(): int {.inline.} =
|
||||
codegenDeclGlobal
|
||||
5
tests/ccgbugs/mseq_importc_alias.nim
Normal file
5
tests/ccgbugs/mseq_importc_alias.nim
Normal file
@@ -0,0 +1,5 @@
|
||||
proc resizeCints*(s: var seq[cint], n: int) =
|
||||
s.setLen(n)
|
||||
|
||||
proc cintLen*(s: seq[cint]): int =
|
||||
result = s.len
|
||||
13
tests/ccgbugs/tcodegendeclglobal.nim
Normal file
13
tests/ccgbugs/tcodegendeclglobal.nim
Normal file
@@ -0,0 +1,13 @@
|
||||
discard """
|
||||
output: '''
|
||||
123
|
||||
123
|
||||
'''
|
||||
ccodecheck: "'extern NI /* custom declaration */ codegenDeclGlobal'"
|
||||
targets: "c cpp"
|
||||
"""
|
||||
|
||||
import ./mcodegendeclglobal
|
||||
|
||||
echo codegenDeclGlobal
|
||||
echo readCodegenDeclGlobal()
|
||||
15
tests/ccgbugs/tseq_importc_alias_crossmod.nim
Normal file
15
tests/ccgbugs/tseq_importc_alias_crossmod.nim
Normal file
@@ -0,0 +1,15 @@
|
||||
discard """
|
||||
action: run
|
||||
targets: "c cpp"
|
||||
"""
|
||||
|
||||
import mseq_importc_alias
|
||||
|
||||
type CIntAlias = cint
|
||||
|
||||
var fds: seq[CIntAlias]
|
||||
doAssert cintLen(@[1.cint, 2.cint]) == 2
|
||||
doAssert cintLen(fds) == 0
|
||||
resizeCints(fds, 3)
|
||||
fds[1] = CIntAlias(7)
|
||||
doAssert cintLen(fds) == 3
|
||||
20
tests/ccgbugs/tseq_importc_alias_mangle.nim
Normal file
20
tests/ccgbugs/tseq_importc_alias_mangle.nim
Normal file
@@ -0,0 +1,20 @@
|
||||
discard """
|
||||
action: run
|
||||
targets: "c cpp"
|
||||
"""
|
||||
|
||||
type CIntAlias = cint
|
||||
|
||||
var x: (cint,) = (1.cint,)
|
||||
var y: (CIntAlias,) = x
|
||||
x = y
|
||||
doAssert x[0] == 1.cint
|
||||
|
||||
var a: seq[cint]
|
||||
var b: seq[CIntAlias]
|
||||
a.add 1.cint
|
||||
a.add 2.cint
|
||||
b = a
|
||||
a = b
|
||||
doAssert a[0] == 1.cint
|
||||
doAssert b[1] == CIntAlias(2)
|
||||
75
tests/concepts/t26147.nim
Normal file
75
tests/concepts/t26147.nim
Normal file
@@ -0,0 +1,75 @@
|
||||
discard """
|
||||
action: run
|
||||
"""
|
||||
|
||||
type Indexable[T] = concept
|
||||
proc `[]`(a: Self; index: int): T
|
||||
proc len(a: Self): int
|
||||
|
||||
iterator items[T; I: Indexable[T]](indexable: I): T =
|
||||
for index in 0 ..< indexable.len:
|
||||
yield indexable[index]
|
||||
|
||||
type Dummy[T] = distinct seq[T]
|
||||
|
||||
proc `[]`[T](d: Dummy[T], i: int): T = seq[T](d)[i]
|
||||
proc len[T](d: Dummy[T]): int = seq[T](d).len
|
||||
|
||||
var acc = 0
|
||||
for x in Dummy(@[1, 2, 3]):
|
||||
acc += x
|
||||
doAssert acc == 6
|
||||
|
||||
# Inferred concept parameters are resolved through the implementation's own
|
||||
# generic bindings before being exported to the surrounding routine.
|
||||
type
|
||||
Elem[T] = object
|
||||
value: T
|
||||
NestedDummy[T] = ref object
|
||||
data: seq[T]
|
||||
|
||||
proc `[]`[T](d: NestedDummy[T], i: int): Elem[T] =
|
||||
Elem[T](value: d.data[i])
|
||||
proc len[T](d: NestedDummy[T]): int = d.data.len
|
||||
|
||||
iterator directItems[T](indexable: Indexable[T]): T =
|
||||
for index in 0 ..< indexable.len:
|
||||
yield indexable[index]
|
||||
|
||||
var nestedAcc = 0
|
||||
for x in NestedDummy[int](data: @[4, 5, 6]):
|
||||
nestedAcc += x.value
|
||||
doAssert nestedAcc == 15
|
||||
|
||||
var directNestedAcc = 0
|
||||
for x in directItems(NestedDummy[int](data: @[7, 8, 9])):
|
||||
directNestedAcc += x.value
|
||||
doAssert directNestedAcc == 24
|
||||
|
||||
# All dependent parameters inferred while checking a concept constraint must
|
||||
# be propagated to the constrained routine.
|
||||
type
|
||||
KeyValue[K, V] = concept
|
||||
proc key(x: Self): K
|
||||
proc value(x: Self): V
|
||||
Pair[K, V] = object
|
||||
k: K
|
||||
v: V
|
||||
|
||||
proc key[K, V](x: Pair[K, V]): K = x.k
|
||||
proc value[K, V](x: Pair[K, V]): V = x.v
|
||||
|
||||
proc unpack[K, V; P: KeyValue[K, V]](x: P): (K, V) =
|
||||
(x.key, x.value)
|
||||
|
||||
let pair = Pair[int, string](k: 7, v: "seven")
|
||||
doAssert unpack(pair) == (7, "seven")
|
||||
doAssert not compiles(unpack[string, int](pair))
|
||||
|
||||
proc unpackBoth[K1, V1, K2, V2;
|
||||
P1: KeyValue[K1, V1]; P2: KeyValue[K2, V2]](
|
||||
x: P1; y: P2): ((K1, V1), (K2, V2)) =
|
||||
(unpack(x), unpack(y))
|
||||
|
||||
let otherPair = Pair[string, float](k: "eight", v: 8.0)
|
||||
doAssert unpackBoth(pair, otherPair) == ((7, "seven"), ("eight", 8.0))
|
||||
28
tests/destructor/t26123.nim
Normal file
28
tests/destructor/t26123.nim
Normal file
@@ -0,0 +1,28 @@
|
||||
discard """
|
||||
matrix: "--mm:orc"
|
||||
output: "destroy b"
|
||||
"""
|
||||
|
||||
# bug #26123
|
||||
|
||||
type
|
||||
A = ptr AObj
|
||||
|
||||
AObj = object
|
||||
b: B
|
||||
|
||||
B = distinct ptr BObj
|
||||
|
||||
BObj = object
|
||||
a: A
|
||||
|
||||
proc `=destroy`(r: var B) =
|
||||
echo "destroy b"
|
||||
|
||||
proc main() =
|
||||
var a = create(AObj)
|
||||
var b = B(create(BObj))
|
||||
a.b = b
|
||||
cast[ptr BObj](b).a = a
|
||||
|
||||
main()
|
||||
@@ -166,3 +166,99 @@ type Vector*[T] = object
|
||||
# proc `=destroy`*(x: var Vector[int]) = discard # this will remove error
|
||||
proc `=destroy`*[T](x: var Vector[T]) = discard
|
||||
var a: Vector[int] # Error: unresolved generic parameter
|
||||
|
||||
# issue #26132
|
||||
|
||||
block:
|
||||
type UnparameterizedGeneric[T] = object
|
||||
|
||||
proc `=destroy`(x: var UnparameterizedGeneric) = discard
|
||||
proc `=wasMoved`(x: var UnparameterizedGeneric) = discard
|
||||
proc `=trace`(x: var UnparameterizedGeneric; env: pointer) = discard
|
||||
|
||||
var x: UnparameterizedGeneric[int]
|
||||
discard x
|
||||
|
||||
# Exercise every type-bound hook with the generic parameter omitted.
|
||||
block:
|
||||
type
|
||||
Generic[T] = object
|
||||
value: T
|
||||
|
||||
var destroys, moves, traces, copies, sinks, dups: int
|
||||
|
||||
proc `=destroy`(x: var Generic) = inc destroys
|
||||
proc `=wasMoved`(x: var Generic) =
|
||||
inc moves
|
||||
x.value = default(typeof(x.value))
|
||||
proc `=trace`(x: var Generic; env: pointer) = inc traces
|
||||
proc `=copy`(dest: var Generic; src: Generic) =
|
||||
inc copies
|
||||
dest.value = src.value
|
||||
proc `=sink`(dest: var Generic; src: Generic) =
|
||||
inc sinks
|
||||
dest.value = src.value
|
||||
proc `=dup`(src: Generic): Generic =
|
||||
inc dups
|
||||
Generic(value: src.value)
|
||||
proc deepCopy(src: ref Generic): ref Generic = src
|
||||
|
||||
proc exercise[T]() =
|
||||
var first = Generic[T](value: default(T))
|
||||
var second = Generic[T](value: default(T))
|
||||
second = first
|
||||
doAssert second.value == first.value
|
||||
second = Generic[T](value: default(T))
|
||||
doAssert second.value == default(T)
|
||||
`=trace`(first, nil)
|
||||
`=wasMoved`(first)
|
||||
let implicitDuplicate = first
|
||||
discard implicitDuplicate
|
||||
let duplicate = `=dup`(first)
|
||||
discard duplicate
|
||||
let original = new(Generic[T])
|
||||
doAssert deepCopy(original) == original
|
||||
|
||||
exercise[string]()
|
||||
exercise[int]()
|
||||
exercise[seq[int]]()
|
||||
|
||||
doAssert copies > 0
|
||||
doAssert sinks > 0
|
||||
doAssert dups > 0
|
||||
doAssert moves > 0
|
||||
doAssert traces > 0
|
||||
doAssert destroys > 0
|
||||
|
||||
block:
|
||||
type GenericDistinct[T] = distinct Generic[T]
|
||||
|
||||
proc `=destroy`(x: var GenericDistinct) = discard
|
||||
proc `=wasMoved`(x: var GenericDistinct) = discard
|
||||
proc `=trace`(x: var GenericDistinct; env: pointer) = discard
|
||||
proc `=copy`(dest: var GenericDistinct; src: GenericDistinct) = discard
|
||||
proc `=sink`(dest: var GenericDistinct; src: GenericDistinct) = discard
|
||||
proc `=dup`(src: GenericDistinct): GenericDistinct = src
|
||||
proc deepCopy(src: ref GenericDistinct): ref GenericDistinct = src
|
||||
|
||||
var first = GenericDistinct[string](Generic[string](value: "first"))
|
||||
var second = GenericDistinct[string](Generic[string](value: "second"))
|
||||
second = first
|
||||
second = GenericDistinct[string](Generic[string](value: "third"))
|
||||
`=trace`(first, nil)
|
||||
`=wasMoved`(first)
|
||||
let moved = move(first)
|
||||
let duplicate = `=dup`(moved)
|
||||
discard duplicate
|
||||
let original = new(GenericDistinct[string])
|
||||
doAssert deepCopy(original) == original
|
||||
|
||||
block:
|
||||
type GenericPair[A, B] = object
|
||||
left: A
|
||||
right: B
|
||||
|
||||
proc `=destroy`(x: var GenericPair) = discard
|
||||
|
||||
var pair = GenericPair[int, string](left: 42, right: "pair")
|
||||
discard pair
|
||||
|
||||
7
tests/generics/t26124.nim
Normal file
7
tests/generics/t26124.nim
Normal file
@@ -0,0 +1,7 @@
|
||||
proc u(k: static int) =
|
||||
proc r(_: static int) =
|
||||
while k > 0:
|
||||
discard
|
||||
r(0)
|
||||
|
||||
u(0)
|
||||
4
tests/ic/mexportprivate.nim
Normal file
4
tests/ic/mexportprivate.nim
Normal file
@@ -0,0 +1,4 @@
|
||||
proc pub*(x: int): int = x + 1
|
||||
|
||||
proc hidden(): int = 42 # no `*` ...
|
||||
export hidden # ... but explicitly re-exported
|
||||
4
tests/ic/mimporthidden.nim
Normal file
4
tests/ic/mimporthidden.nim
Normal file
@@ -0,0 +1,4 @@
|
||||
proc pub*(x: int): int = x + 1
|
||||
|
||||
proc secret(): int = 7 # no `*`
|
||||
proc hiddenToo(x: int): int = x
|
||||
11
tests/ic/mnestasgn.nim
Normal file
11
tests/ic/mnestasgn.nim
Normal file
@@ -0,0 +1,11 @@
|
||||
# Helper for tnestasgn.nim: a `sink`-param routine containing a nested proc
|
||||
# whose ENTIRE body is a single assignment, so the body node is a bare `nkAsgn`
|
||||
# rather than an `nkStmtList` — the shape that used to be deferred behind a
|
||||
# childless placeholder of that same kind.
|
||||
|
||||
proc consume*(s: sink string) =
|
||||
var x = ""
|
||||
proc setIt() =
|
||||
x = s
|
||||
setIt()
|
||||
echo x
|
||||
48
tests/ic/readme.md
Normal file
48
tests/ic/readme.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Running `tests/ic`
|
||||
|
||||
./bin/testament --nim:<your compiler> cat ic
|
||||
|
||||
## The metamorphic tests are expensive, and look hung when they are not
|
||||
|
||||
16 of the tests carry `#? metamorphic`. Each has 3–4 `#!STEP` directives, and
|
||||
every step compiles the program **twice** — once under `nim ic`, once with
|
||||
`nim c` as the reference oracle. That is 100+ full compilations for the
|
||||
category. Under `--ic:on` each compilation additionally fans out one backend
|
||||
process per module per stage, and each of those is a compiler holding its own
|
||||
module graph (~800MB peak).
|
||||
|
||||
**A `nim ic` parent sitting at 0% CPU is normal.** It is waiting on its
|
||||
children. It is not a deadlock, and neither is a metamorphic test that occupies
|
||||
the runner for many minutes. Before concluding anything is stuck, check that the
|
||||
test NAME changes over a few minutes — that is the difference between slow and
|
||||
hung, and it is easy to get wrong.
|
||||
|
||||
On a memory-constrained machine the fan-out will swap. The symptoms are exactly
|
||||
the ones that read as a deadlock: several processes at 0% CPU, no output, a
|
||||
different test "stuck" on every run, and the same compilation finishing in
|
||||
seconds when run on its own. Check `vm_stat` (page-ins per second) and
|
||||
`sysctl vm.swapusage` before looking for a bug. This was diagnosed as a
|
||||
testament/`nim ic` interaction more than once before anyone measured.
|
||||
|
||||
Cap the fan-out to fit the machine — precedence documented at `deps.nim`'s
|
||||
`let parallel`:
|
||||
|
||||
--parallelBuild:N # standard flag, given meaning under IC
|
||||
-d:icJobs:N # same cap, legacy define
|
||||
-d:icNoParallel # serial, and non-interleaved child output
|
||||
|
||||
Serial output matters for a second reason: the parallel backend processes share
|
||||
one stderr, so any per-process diagnostic printing (`NIM_IC_BNODE_GRIND`,
|
||||
`-d:icCanRaiseLog`) interleaves and produces torn lines. Either use
|
||||
`-d:icNoParallel` or parse defensively and count what you dropped.
|
||||
|
||||
## Running a single test
|
||||
|
||||
`testament r tests/ic/<file>.nim` works for the ordinary tests. It does NOT work
|
||||
for the metamorphic ones — the multi-step files carry several `discard """`
|
||||
spec blocks and the single-test path rejects them with "duplicate `specStart`".
|
||||
Those only run through `cat ic`.
|
||||
|
||||
Files matching `tests/ic/*_temp.nim` are ignored by git (see `.gitignore`) and
|
||||
are scratch, not tests: several import helper modules that do not exist and fail
|
||||
for that reason alone.
|
||||
101
tests/ic/tclosure_hooks.nim
Normal file
101
tests/ic/tclosure_hooks.nim
Normal file
@@ -0,0 +1,101 @@
|
||||
discard """
|
||||
description: '''IC vs `nim c`: closure environments, their hooks and their owners'''
|
||||
"""
|
||||
|
||||
#? metamorphic
|
||||
|
||||
# A closure's environment type — and the `=destroy`/`=copy` the compiler lifts
|
||||
# for it — is minted by the BACKEND, during the `lower` stage, and exists in no
|
||||
# module's semmed NIF. The per-module backend has to decide which translation
|
||||
# unit emits such a routine, and the owner walk it uses lands on the module of
|
||||
# the ORIGINAL generic: for a generic closure iterator defined in one module and
|
||||
# instantiated in another, that is a module which never sees the instance, so the
|
||||
# env's `=destroy` was emitted by nobody (`undefined reference to
|
||||
# eqdestroy__c485__…`). Every referencing TU emits it now.
|
||||
#
|
||||
# The steps then move the captured state around, because the env's LAYOUT is what
|
||||
# decides whether those hooks are trivial: a body-only edit that adds a capture
|
||||
# changes the env type of a routine whose importers do not re-sem.
|
||||
|
||||
#!FILE clleaf.nim
|
||||
type Ev* = proc (s: string): string {.closure.}
|
||||
|
||||
proc leafMaker*(tag: string): Ev =
|
||||
var n = 0
|
||||
proc outer(s: string): string =
|
||||
proc inner(t: string): string =
|
||||
inc n
|
||||
tag & ":" & t & ":" & $n
|
||||
inner(s)
|
||||
result = outer
|
||||
|
||||
iterator leafIter*[T](xs: seq[T]): T {.closure.} =
|
||||
for x in xs: yield x
|
||||
|
||||
#!FILE clmid.nim
|
||||
import clleaf
|
||||
|
||||
proc midMaker*(tag: string): Ev =
|
||||
let base = leafMaker(tag & "/mid")
|
||||
var calls = 0
|
||||
result = proc (s: string): string =
|
||||
inc calls
|
||||
base(s) & "#" & $calls
|
||||
|
||||
proc midIter*(): seq[string] =
|
||||
# instantiates `leafIter[string]` HERE, not where it is defined
|
||||
result = @[]
|
||||
for x in leafIter(@["p", "q"]): result.add x
|
||||
|
||||
#!FILE main.nim
|
||||
import clleaf, clmid
|
||||
|
||||
let t = midMaker("top")
|
||||
echo t("Alpha")
|
||||
echo t("Beta")
|
||||
echo midIter()
|
||||
|
||||
# an instance only the main module has
|
||||
var fs: seq[float] = @[]
|
||||
for x in leafIter(@[1.5, 2.5]): fs.add x
|
||||
echo fs
|
||||
#!STEP
|
||||
|
||||
# body-only edit that GROWS the environment: a second captured local
|
||||
#!FILE clleaf.nim
|
||||
type Ev* = proc (s: string): string {.closure.}
|
||||
|
||||
proc leafMaker*(tag: string): Ev =
|
||||
var n = 0
|
||||
var seen: seq[string] = @[]
|
||||
proc outer(s: string): string =
|
||||
proc inner(t: string): string =
|
||||
inc n
|
||||
seen.add t
|
||||
tag & ":" & t & ":" & $n & ":" & $seen.len
|
||||
inner(s)
|
||||
result = outer
|
||||
|
||||
iterator leafIter*[T](xs: seq[T]): T {.closure.} =
|
||||
var i = 0
|
||||
for x in xs:
|
||||
inc i
|
||||
yield x
|
||||
#!STEP
|
||||
|
||||
# and shrink it again
|
||||
#!FILE clleaf.nim
|
||||
type Ev* = proc (s: string): string {.closure.}
|
||||
|
||||
proc leafMaker*(tag: string): Ev =
|
||||
var n = 0
|
||||
proc outer(s: string): string =
|
||||
proc inner(t: string): string =
|
||||
inc n
|
||||
tag & ":" & t & ":" & $n
|
||||
inner(s)
|
||||
result = outer
|
||||
|
||||
iterator leafIter*[T](xs: seq[T]): T {.closure.} =
|
||||
for x in xs: yield x
|
||||
#!STEP
|
||||
90
tests/ic/tclosure_nested_iter.nim
Normal file
90
tests/ic/tclosure_nested_iter.nim
Normal file
@@ -0,0 +1,90 @@
|
||||
discard """
|
||||
description: '''IC vs `nim c`: a closure iterator nested in a closure iterator'''
|
||||
"""
|
||||
|
||||
#? metamorphic
|
||||
|
||||
# `env.:up = enclosingEnv` links a nested routine's environment to its parent,
|
||||
# and the two environments then reference each other. That assignment has to go
|
||||
# through `=copy` (with the cyclic increment) or the parent's refcount is one too
|
||||
# low, and at teardown both `=destroy`s believe they hold the last reference and
|
||||
# recurse until the stack is gone — a SIGSEGV, after the program's own output has
|
||||
# already been printed. (`tests/iter/tnestedclosures.nim`, "Test 3".)
|
||||
#
|
||||
# Whether it becomes a `=copy` depends on the up-field type's hooks existing when
|
||||
# the routine is destructor-injected. Whole-program cgen got that for free: a
|
||||
# LATER lifting pass creates them, and it runs before any routine's injection.
|
||||
# The per-module backend injects a routine right after lifting it (the `lower`
|
||||
# stage), long before the module's top level is transformed at all (that is
|
||||
# `cg`) — so the hooks are created at the assignment site now.
|
||||
|
||||
#!FILE main.nim
|
||||
iterator foo(): int {.closure.} =
|
||||
let x = 34
|
||||
proc bar() = echo "bar sees ", x
|
||||
iterator bar2(): int {.closure.} =
|
||||
bar()
|
||||
yield x
|
||||
for y in bar2():
|
||||
yield y
|
||||
|
||||
for v in foo(): echo v
|
||||
|
||||
# a closure iterator nested in a closure iterator, inside a proc
|
||||
proc factory() =
|
||||
iterator outerIt(): int {.closure.} =
|
||||
iterator innerIt(): int {.closure.} =
|
||||
yield 0
|
||||
yield 1
|
||||
yield 2
|
||||
for x in innerIt(): yield x
|
||||
for x in outerIt(): echo x
|
||||
factory()
|
||||
|
||||
# the iterator's env outlives the proc that made it
|
||||
proc keep(): iterator (): string =
|
||||
let held = "kept"
|
||||
result = iterator (): string =
|
||||
yield held
|
||||
yield held & "!"
|
||||
for s in keep()(): echo s
|
||||
#!STEP
|
||||
|
||||
# growing the captured state changes both env layouts
|
||||
#!FILE main.nim
|
||||
iterator foo(): int {.closure.} =
|
||||
let x = 34
|
||||
var log: seq[string] = @[]
|
||||
proc bar() =
|
||||
log.add "bar"
|
||||
echo "bar sees ", x, " ", log.len
|
||||
iterator bar2(): int {.closure.} =
|
||||
bar()
|
||||
bar()
|
||||
yield x
|
||||
for y in bar2():
|
||||
yield y
|
||||
|
||||
for v in foo(): echo v
|
||||
|
||||
proc factory() =
|
||||
iterator outerIt(): int {.closure.} =
|
||||
var emitted = 0
|
||||
iterator innerIt(): int {.closure.} =
|
||||
yield 0
|
||||
yield 1
|
||||
yield 2
|
||||
for x in innerIt():
|
||||
inc emitted
|
||||
yield x * emitted
|
||||
for x in outerIt(): echo x
|
||||
factory()
|
||||
|
||||
proc keep(): iterator (): string =
|
||||
let held = "kept"
|
||||
let extra = "+"
|
||||
result = iterator (): string =
|
||||
yield held & extra
|
||||
yield held & "!" & extra
|
||||
for s in keep()(): echo s
|
||||
#!STEP
|
||||
34
tests/ic/tconfig_invalidation.nim
Normal file
34
tests/ic/tconfig_invalidation.nim
Normal file
@@ -0,0 +1,34 @@
|
||||
discard """
|
||||
description: '''IC: changing the compiler switches must invalidate the cache'''
|
||||
"""
|
||||
|
||||
#? metamorphic
|
||||
|
||||
# nifmake decides staleness from file mtimes and never looks at a rule's command
|
||||
# line, so `-d:` / `--mm:` / `--opt:` changes re-generated the build file with
|
||||
# the new switches and re-fired nothing: a silently stale binary built with the
|
||||
# PREVIOUS configuration. And switches given only on the driver's command line
|
||||
# never reached the per-module children at all, because they replay the
|
||||
# project's config files rather than the driver's argv.
|
||||
|
||||
#!FILE cfg.nim
|
||||
const Mode* {.strdefine.} = "plain"
|
||||
|
||||
proc describe*(): string =
|
||||
when Mode == "loud": "LOUD"
|
||||
elif Mode == "quiet": "quiet"
|
||||
else: "plain"
|
||||
|
||||
#!FILE main.nim
|
||||
import cfg
|
||||
echo describe()
|
||||
#!STEP expect: plain
|
||||
|
||||
#!FLAGS -d:Mode=loud
|
||||
#!STEP expect: LOUD
|
||||
|
||||
#!FLAGS -d:Mode=quiet
|
||||
#!STEP expect: quiet
|
||||
|
||||
#!FLAGS
|
||||
#!STEP expect: plain
|
||||
37
tests/ic/tdead_when_import.nim
Normal file
37
tests/ic/tdead_when_import.nim
Normal file
@@ -0,0 +1,37 @@
|
||||
discard """
|
||||
description: '''IC: an import under an undecidable `when` must not be compiled'''
|
||||
"""
|
||||
|
||||
#? metamorphic
|
||||
|
||||
# `when SomeStrdefineConst == "x": import y` is `cvUnknown` to the dependency
|
||||
# scanner, which conservatively keeps the edge — right for an edge, but it also
|
||||
# gave `y` its own `nim m` rule. `nim c` never looks at that file, so a build
|
||||
# died on a package the user never installed because they never selected that
|
||||
# backend. Selecting it must still produce the honest error.
|
||||
|
||||
#!FILE needsmissing.nim
|
||||
import pkg/definitely_not_an_installed_package
|
||||
proc unreachable*(): string = "never"
|
||||
|
||||
#!FILE guarded.nim
|
||||
const Backend* {.strdefine.} = "plain"
|
||||
|
||||
when Backend == "fancy":
|
||||
import ./needsmissing
|
||||
|
||||
proc pick*(): string =
|
||||
when Backend == "fancy": unreachable()
|
||||
else: "plain"
|
||||
|
||||
#!FILE main.nim
|
||||
import guarded
|
||||
echo pick()
|
||||
#!STEP expect: plain
|
||||
|
||||
# selecting the branch that really does need the missing package must report it
|
||||
#!FLAGS -d:Backend=fancy
|
||||
#!STEP fails: cannot open file
|
||||
|
||||
#!FLAGS
|
||||
#!STEP expect: plain
|
||||
26
tests/ic/tdeleted_module.nim
Normal file
26
tests/ic/tdeleted_module.nim
Normal file
@@ -0,0 +1,26 @@
|
||||
discard """
|
||||
description: '''IC: deleting a still-imported module must be an error'''
|
||||
"""
|
||||
|
||||
#? metamorphic
|
||||
|
||||
# Deleting a file moves no mtime, so nothing in an mtime-keyed build re-fires:
|
||||
# `nim ic` relinked a stale binary while `nim c` reported `cannot open file`.
|
||||
# The dependency scan is the only part of the pipeline that looks at import
|
||||
# paths at all, so that is where the vanished module has to be noticed.
|
||||
|
||||
#!FILE helper.nim
|
||||
proc help*(): string = "helped"
|
||||
|
||||
#!FILE main.nim
|
||||
import helper
|
||||
echo help()
|
||||
#!STEP expect: helped
|
||||
|
||||
#!DELETE helper.nim
|
||||
#!STEP fails: cannot open file
|
||||
|
||||
# putting it back recovers
|
||||
#!FILE helper.nim
|
||||
proc help*(): string = "back"
|
||||
#!STEP expect: back
|
||||
68
tests/ic/tdestructor_fidelity.nim
Normal file
68
tests/ic/tdestructor_fidelity.nim
Normal file
@@ -0,0 +1,68 @@
|
||||
discard """
|
||||
description: '''IC vs `nim c`: destructor injection and move analysis must agree'''
|
||||
"""
|
||||
|
||||
#? metamorphic
|
||||
|
||||
# Two whole classes of IC miscompilation are invisible to any IC-vs-IC check,
|
||||
# because IC was *consistently* wrong: warm == cold == not what `nim c` does.
|
||||
# The oracle is what catches them.
|
||||
#
|
||||
# * `sfInjectDestructors` lives on the MODULE symbol, which the NIF loader
|
||||
# rebuilds from scratch — so `genTopLevelStmt` skipped the destructor pass
|
||||
# entirely and a module-level `block: let h = ...` never ran `=destroy`.
|
||||
# * `nfFirstWrite`/`nfLastRead` sit on `nkSym` nodes, which serialize as bare
|
||||
# NIF `SymUse` tokens with nowhere to put node flags — so the frontend's move
|
||||
# analysis never reached the backend and EVERY first assignment to a
|
||||
# destructor-bearing local became `=sink` over still-zeroed memory.
|
||||
|
||||
#!FILE res.nim
|
||||
var log*: seq[string]
|
||||
|
||||
type R* = object
|
||||
tag*: string
|
||||
|
||||
proc `=destroy`*(r: R) = log.add "d(" & r.tag & ")"
|
||||
proc `=copy`*(d: var R, s: R) = (log.add "c(" & s.tag & ")"; d.tag = s.tag)
|
||||
|
||||
proc mk*(t: string): R = R(tag: t)
|
||||
proc mkVia*(t: string): R = (result = R(tag: t))
|
||||
proc consume*(r: sink R): string = "u:" & r.tag
|
||||
|
||||
#!FILE main.nim
|
||||
import res
|
||||
|
||||
# in a proc: worked before
|
||||
proc inProc() =
|
||||
let a = mk("proc")
|
||||
discard a
|
||||
inProc()
|
||||
|
||||
# module top level: the pass was skipped wholesale
|
||||
block:
|
||||
let t = mk("toplevel")
|
||||
discard t
|
||||
|
||||
for i in 0 .. 1:
|
||||
let l = mk("loop" & $i)
|
||||
discard l
|
||||
|
||||
# every `result` shape: each must construct in place, not `=sink` over zeroes
|
||||
block:
|
||||
let x = mk("direct")
|
||||
let y = mkVia("via")
|
||||
discard x
|
||||
discard y
|
||||
|
||||
# last read is a move, a re-read is a copy
|
||||
proc moves(): string =
|
||||
var m = mk("moved")
|
||||
result = consume(m)
|
||||
proc copies(): string =
|
||||
var k = mk("kept")
|
||||
result = consume(k) & "/" & k.tag
|
||||
discard moves()
|
||||
discard copies()
|
||||
|
||||
echo log
|
||||
#!STEP expect: @["d(proc)", "d(toplevel)", "d(loop0)", "d(loop1)", "d(via)", "d(direct)", "d(moved)", "c(kept)", "d(kept)", "d(kept)"]
|
||||
38
tests/ic/tdiscovered_import.nim
Normal file
38
tests/ic/tdiscovered_import.nim
Normal file
@@ -0,0 +1,38 @@
|
||||
discard """
|
||||
description: '''IC: a macro-generated import stays in the graph across runs'''
|
||||
"""
|
||||
|
||||
#? metamorphic
|
||||
|
||||
# The static scanner cannot see `parseStmt("import dyn")`. The discovery
|
||||
# fixpoint recovers it — but only ran AFTER a failure, and the graph is
|
||||
# re-derived statically on every run, so on a warm build the discovered module
|
||||
# had no nifler/`nim m` rule at all: editing it changed nothing, forever.
|
||||
|
||||
#!FILE dyn.nim
|
||||
proc hidden*(): string = "first"
|
||||
|
||||
#!FILE gen.nim
|
||||
import std/macros
|
||||
|
||||
macro generatedImport(): untyped =
|
||||
parseStmt("import dyn")
|
||||
|
||||
generatedImport()
|
||||
|
||||
proc reveal*(): string = hidden()
|
||||
|
||||
#!FILE main.nim
|
||||
import gen
|
||||
echo reveal()
|
||||
#!STEP expect: first
|
||||
|
||||
# the warm build must see this edit
|
||||
#!FILE dyn.nim
|
||||
proc hidden*(): string = "second"
|
||||
#!STEP expect: second
|
||||
|
||||
# and again, to prove it is not a one-shot recovery
|
||||
#!FILE dyn.nim
|
||||
proc hidden*(): string = "third"
|
||||
#!STEP expect: third
|
||||
32
tests/ic/terror_recovery.nim
Normal file
32
tests/ic/terror_recovery.nim
Normal file
@@ -0,0 +1,32 @@
|
||||
discard """
|
||||
description: '''IC: a failed `nim m` must not poison the cache'''
|
||||
"""
|
||||
|
||||
#? metamorphic
|
||||
|
||||
# A `nim m` that errored still wrote its `.s.bif` and cookie sidecars. nifmake
|
||||
# then saw the rule satisfied (outputs newer than inputs) and the NEXT run
|
||||
# reported success for a program that does not compile — linking a binary
|
||||
# generated from error-bearing AST, or crashing codegen outright. Expressing
|
||||
# this needs a step that is allowed to FAIL and a following step that recovers.
|
||||
|
||||
#!FILE dep.nim
|
||||
proc value*(): int = 41
|
||||
|
||||
#!FILE main.nim
|
||||
import dep
|
||||
echo value() + 1
|
||||
#!STEP expect: 42
|
||||
|
||||
# introduce a real error
|
||||
#!FILE dep.nim
|
||||
proc value*(): int = undefinedThing() + 1
|
||||
#!STEP fails: undeclared identifier: 'undefinedThing'
|
||||
|
||||
# ... and again: the second run must NOT decide the rule is up to date.
|
||||
#!STEP fails: undeclared identifier: 'undefinedThing'
|
||||
|
||||
# fixing it must rebuild rather than serve the poisoned artifact
|
||||
#!FILE dep.nim
|
||||
proc value*(): int = 100
|
||||
#!STEP expect: 101
|
||||
14
tests/ic/texportprivate.nim
Normal file
14
tests/ic/texportprivate.nim
Normal file
@@ -0,0 +1,14 @@
|
||||
discard """
|
||||
output: '''42'''
|
||||
"""
|
||||
|
||||
# `export s` re-exports a symbol whose declaration has no `*`. It reaches the
|
||||
# module interface through `reexportSym` alone, so a NIF writer that decides
|
||||
# importability from `sfExported` ships it as private and the importer reports
|
||||
# "undeclared identifier". `std/random` does exactly this
|
||||
# (`proc initRand(): Rand` + `since (1, 5, 1): export initRand`), which made
|
||||
# `--ic:on` unable to compile anything reaching `std/tempfiles`.
|
||||
|
||||
import mexportprivate
|
||||
|
||||
echo hidden()
|
||||
49
tests/ic/tglobal_dtors.nim
Normal file
49
tests/ic/tglobal_dtors.nim
Normal file
@@ -0,0 +1,49 @@
|
||||
discard """
|
||||
description: '''IC vs `nim c`: module-level globals must be destroyed at exit'''
|
||||
"""
|
||||
|
||||
#? metamorphic
|
||||
|
||||
# `graph.globalDestructors` is filled while a module's top level goes through
|
||||
# `injectDestructorCalls`, and whole-program cgen empties the list into the main
|
||||
# module's init proc — which IS the program body, so the calls land at program
|
||||
# exit. Under `nim ic` every module's `cg` is a separate process, so the main
|
||||
# module's `cg` only ever saw its OWN entries and a module-level `var` with a
|
||||
# `=destroy` in any imported module was simply never destroyed.
|
||||
#
|
||||
# The teardown ORDER is the other half: it must be the reverse of the init order
|
||||
# (importers before their dependencies), which is what the oracle pins down here
|
||||
# — three modules in a chain plus main, each with a global of its own.
|
||||
|
||||
#!FILE gdlog.nim
|
||||
type G* = object
|
||||
tag*: string
|
||||
|
||||
proc `=destroy`*(g: G) = echo "destroy ", g.tag
|
||||
proc mk*(t: string): G = G(tag: t)
|
||||
|
||||
#!FILE gda.nim
|
||||
import gdlog
|
||||
var ga* = mk("a")
|
||||
|
||||
#!FILE gdb.nim
|
||||
import gdlog, gda
|
||||
var gb* = mk("b:" & ga.tag)
|
||||
|
||||
#!FILE gdc.nim
|
||||
import gdlog, gdb
|
||||
var gcv* = mk("c:" & gb.tag)
|
||||
|
||||
#!FILE main.nim
|
||||
import gdlog, gda, gdb, gdc
|
||||
|
||||
var gmain = mk("main")
|
||||
|
||||
echo "body ", ga.tag, " ", gb.tag, " ", gcv.tag, " ", gmain.tag
|
||||
#!STEP
|
||||
|
||||
# touching a leaf module must not lose anyone's teardown
|
||||
#!FILE gda.nim
|
||||
import gdlog
|
||||
var ga* = mk("a2")
|
||||
#!STEP
|
||||
7
tests/ic/timportcalias.nim
Normal file
7
tests/ic/timportcalias.nim
Normal file
@@ -0,0 +1,7 @@
|
||||
import ../ccgbugs/mseq_importc_alias
|
||||
|
||||
type CIntAlias = cint
|
||||
|
||||
var values: seq[CIntAlias]
|
||||
resizeCints(values, 2)
|
||||
doAssert cintLen(values) == 2
|
||||
17
tests/ic/timporthidden.nim
Normal file
17
tests/ic/timporthidden.nim
Normal file
@@ -0,0 +1,17 @@
|
||||
discard """
|
||||
output: '''42'''
|
||||
"""
|
||||
|
||||
# `import x {.all.}` makes x's PRIVATE symbols visible. Under IC that means the
|
||||
# hidden half of a loaded module's interface has to be there — and it is now
|
||||
# built on demand rather than at load time, because almost nothing ever reads it
|
||||
# (1.70M hidden stubs against 0.29M exported ones on a cold Atlas build).
|
||||
#
|
||||
# The trap the first attempt fell into: a module has TWO FileIndexes. `c.mods`
|
||||
# in the decode context is keyed by the one `registerNifSuffix` mints for the
|
||||
# NIF suffix; `g.ifaces` is indexed by the module's source file. Asking one with
|
||||
# the other misses silently, and this test is what says so.
|
||||
|
||||
import mimporthidden {.all.}
|
||||
|
||||
echo secret() + hiddenToo(35)
|
||||
18
tests/ic/tnestasgn.nim
Normal file
18
tests/ic/tnestasgn.nim
Normal file
@@ -0,0 +1,18 @@
|
||||
discard """
|
||||
output: '''hi'''
|
||||
"""
|
||||
|
||||
# Regression test, minimized from a nimbus-eth2 `nim ic` crash by
|
||||
# https://github.com/nim-lang/Nim/pull/26106 (the only one of that PR's eight
|
||||
# repros that reproduces on its own base).
|
||||
#
|
||||
# A NIF-loaded routine's body is installed as a `nfLazyBody` placeholder. The
|
||||
# placeholder used to carry the REAL body kind while holding no children, which
|
||||
# breaks the compiler's most basic invariant — a node's kind implies its arity.
|
||||
# `trees.getPotentialWrites` walks the outer routine because it has a `sink`
|
||||
# parameter, reaches the nested proc's body under `of nkAsgn`, and reads
|
||||
# `n[0]`/`n[1]` as every such reader is entitled to: "index out of bounds, the
|
||||
# container is empty [IndexDefect]".
|
||||
|
||||
import mnestasgn
|
||||
consume("hi")
|
||||
20
tests/method/tmethod_virtual_raise.nim
Normal file
20
tests/method/tmethod_virtual_raise.nim
Normal file
@@ -0,0 +1,20 @@
|
||||
discard """
|
||||
output: '''caught'''
|
||||
"""
|
||||
|
||||
type
|
||||
Base = ref object of RootObj
|
||||
Child = ref object of Base
|
||||
|
||||
method run(value: Base): string {.base.} =
|
||||
result = "base"
|
||||
|
||||
method run(value: Child): string =
|
||||
raise newException(ValueError, "child")
|
||||
|
||||
let value: Base = Child()
|
||||
try:
|
||||
discard value.run()
|
||||
quit "virtual method did not raise"
|
||||
except ValueError:
|
||||
echo "caught"
|
||||
@@ -11,6 +11,9 @@ proc fn2[T](a: var openArray[T]): seq[T] =
|
||||
proc fn3[T](a: var openArray[T]) =
|
||||
for i, ai in mpairs(a): ai = i * 10
|
||||
|
||||
proc wr[T](a: var openArray[T]; v: T) =
|
||||
a[0] = v
|
||||
|
||||
proc main =
|
||||
var a = [1,2,3,4,5]
|
||||
|
||||
@@ -20,8 +23,22 @@ proc main =
|
||||
doAssert fn2(a.toOpenArray(1,3)) == @[2,3,4]
|
||||
|
||||
fn3(a.toOpenArray(1,3))
|
||||
when defined(js): discard # xxx bug #15952: `a` left unchanged
|
||||
else: doAssert a == [1, 0, 10, 20, 5]
|
||||
doAssert a == [1, 0, 10, 20, 5]
|
||||
|
||||
block: # bug #15952: `toOpenArray` slices are live views on JS
|
||||
# Fixed homogeneous numeric arrays lower to JS typed arrays; seqs and
|
||||
# non-numeric fixed arrays lower to plain JS arrays. In all cases a slice
|
||||
# passed to a `var openArray` must alias the source so writes propagate
|
||||
# (JS: subarray view for typed arrays, {base,off,len} view otherwise).
|
||||
var si = @[1, 2, 3, 4, 5]
|
||||
fn3(si.toOpenArray(1, 3))
|
||||
doAssert si == @[1, 0, 10, 20, 5]
|
||||
var ss = ["a", "b", "c", "d", "e"]
|
||||
wr(ss.toOpenArray(1, 3), "Z")
|
||||
doAssert ss == ["a", "Z", "c", "d", "e"]
|
||||
# read-only slicing must still work and never throw, on every backend.
|
||||
doAssert fn1(@[1, 2, 3, 4, 5].toOpenArray(1, 3)) == @[2, 3, 4]
|
||||
doAssert fn1(["a", "b", "c", "d", "e"].toOpenArray(1, 3)) == @["b", "c", "d"]
|
||||
|
||||
block: # bug #12521
|
||||
block:
|
||||
|
||||
24
tests/stdlib/t26134.nim
Normal file
24
tests/stdlib/t26134.nim
Normal file
@@ -0,0 +1,24 @@
|
||||
discard """
|
||||
matrix: "--mm:orc --undef:nimPreviewNonVarDestructor"
|
||||
output: "hello"
|
||||
"""
|
||||
|
||||
# bug #26134
|
||||
|
||||
type MyObject = object
|
||||
|
||||
proc `=destroy`(v: var MyObject) =
|
||||
echo "hello"
|
||||
|
||||
proc remove(v: var seq[MyObject]) =
|
||||
v.del(0)
|
||||
|
||||
proc aaa(v: var seq[MyObject], i: sink MyObject) =
|
||||
v.add(i)
|
||||
|
||||
proc main =
|
||||
var v: seq[MyObject]
|
||||
v.aaa(MyObject())
|
||||
v.remove()
|
||||
|
||||
main()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user