4343# # - `intraModuleInline` runs inside hexer, pre-DCE, on the tree that becomes
4444# # this module's `.x.nif`. Its own module only (`xnifDir` unset).
4545# # - `runInterModuleInliner` (shoggoth) runs after DCE, on the `.c.nif`s, and
46- # # is the one that crosses module borders: `loadForeign` lazy-loads the
47- # # callee's `.c.nif`.
46+ # # is the one that crosses module borders: `foreignProc` parses just the
47+ # # callee's decl out of its `.c.nif`, via the file's embedded index .
4848# #
4949# # Either way the decision is derived from the same file the body comes out
50- # # of: `indexProcBodies` measures each proc right after the module is parsed,
51- # # so what is scored is exactly what would be spliced (hexer's flattening of
50+ # # of: `computeInlineInfo` measures the very decl a splice would copy (for the
51+ # # own module in `indexProcBodies`, for a callee in `lookupInlineInfo`), so
52+ # # what is scored is exactly what would be spliced (hexer's flattening of
5253# # tiny bodies happens *before* the `.c.nif` is written — a proc that grows
5354# # past the bound by having its own callees spliced into it is re-measured,
5455# # and demoted, by every importer).
5859# # the inlined body to fresh symbols, and drops the trailing `(ret X)`
5960# # (its result is discarded at statement position).
6061
61- import std / [tables, assertions, os, sets, hashes]
62- when defined (inlinerTrace) or defined (inlinerDeny): import std / [syncio, strutils]
62+ import std / [tables, assertions, os, sets, hashes, syncio ]
63+ when defined (inlinerTrace) or defined (inlinerDeny): import std / [strutils]
6364include " .." / lib / nifprelude
6465include " .." / lib / compat2
6566import " .." / lib / symparser
67+ import " .." / lib / foreignmodules
6668import " .." / lengc / [leng_model]
6769
6870type
@@ -268,11 +270,6 @@ proc computeInlineInfo*(procDecl: Cursor): InlineInfo =
268270 result .inlinable = true
269271
270272type
271- ForeignModule * = object
272- buf* : TokenBuf
273- bodies* : Table [SymId , int ] # sym → offset of its (proc …) in `buf`
274- inlineInfo* : Table [SymId , InlineInfo ] # sym → its `(inline …)` annotation
275-
276273 InlinerCtx * = object
277274 moduleSuffix* : string
278275 counter: int # fresh-name suffix
@@ -304,9 +301,14 @@ type
304301 # interleaved runs; the pre-hint policy is 75 ms), which is not worth 4x
305302 # the program — and the native boot pays for it twice, in 55.4s against
306303 # 28.7s.
307- foreign: Table [string , ref ForeignModule ]
308- # Cached cross-module bodies. `ref` so growing the table doesn't
309- # invalidate cursors that point into a previously-fetched buffer.
304+ foreign: Table [string , ForeignModule ]
305+ # Opened callee modules, by module suffix. A `ForeignModule` is a `ref`
306+ # that owns each decl it parsed, so the cursors `lookupBody` hands out
307+ # stay valid as this grows.
308+ noForeign: HashSet [string ]
309+ # Module suffixes with no `.c.nif` to open (asked once, not per call).
310+ foreignInfo: Table [SymId , InlineInfo ]
311+ # Cross-module policy verdicts, computed on first ask per callee.
310312 inProgress* : HashSet [SymId ]
311313 # Currently-being-spliced procs. Recursive `.inline` (direct or
312314 # mutual) would otherwise cause the splice + re-tr loop in dce2 to
@@ -333,7 +335,9 @@ proc initInlinerCtx*(moduleSuffix: string; src: ptr TokenBuf;
333335 maxDepth: maxDepth,
334336 growthLeft: high (int ),
335337 counterPrefix: counterPrefix,
336- foreign: initTable [string , ref ForeignModule ](),
338+ foreign: initTable [string , ForeignModule ](),
339+ noForeign: initHashSet [string ](),
340+ foreignInfo: initTable [SymId , InlineInfo ](),
337341 inProgress: initHashSet [SymId ]())
338342
339343proc growthBudget * (bodySize: int ): int =
@@ -371,14 +375,14 @@ proc applySmallTotalRule(buf: var TokenBuf; infos: var seq[(SymId, InlineInfo)])
371375 info.inlinable = true
372376
373377proc indexProcBodies (buf: var TokenBuf ; bodies: var Table [SymId , int ];
374- infos: var Table [SymId , InlineInfo ]; ownModule: bool ) =
375- # # Walks the top-level `(stmts …)` and records `(proc :sym …)` decls
376- # # by sym → byte offset into `buf`, along with each proc's `InlineInfo`,
377- # # computed right here from the body we are indexing (`computeInlineInfo`
378- # # walks it once — a linear pass over a buffer we just parsed anyway). No
379- # # pragma transport is involved, so own-module and foreign bodies go
380- # # through the identical policy, and the size that is scored is the size
381- # # of the exact body a splice would copy.
378+ infos: var Table [SymId , InlineInfo ]) =
379+ # # Walks the module's own top-level `(stmts …)` and records `(proc :sym …)`
380+ # # decls by sym → byte offset into `buf`, along with each proc's
381+ # # `InlineInfo`, computed right here from the body we are indexing
382+ # # (`computeInlineInfo` walks it once — a linear pass over a buffer we just
383+ # # parsed anyway). No pragma transport is involved: a foreign callee goes
384+ # # through the same `computeInlineInfo` (`lookupInlineInfo`), so the size
385+ # # that is scored is the size of the exact body a splice would copy.
382386 var n = beginRead (buf)
383387 var found: seq [(SymId , InlineInfo )] = @ []
384388 if n.stmtKind == StmtsS :
@@ -394,13 +398,13 @@ proc indexProcBodies(buf: var TokenBuf; bodies: var Table[SymId, int];
394398 # being rewritten has them all in view: a foreign module's internal count says
395399 # nothing about how often ITS callers reach for the proc (measured: applied to
396400 # foreign modules it turned every once-called 400-token helper into an
397- # always-splice for the whole program).
398- if ownModule: applySmallTotalRule (buf, found)
401+ # always-splice for the whole program). Hence foreign procs never get it.
402+ applySmallTotalRule (buf, found)
399403 for (sym, info) in found:
400404 if info.inlinable: infos[sym] = info
401405
402406proc collectProcBodies * (c: var InlinerCtx ) =
403- indexProcBodies (c.src[], c.bodies, c.ownInfo, ownModule = true )
407+ indexProcBodies (c.src[], c.bodies, c.ownInfo)
404408
405409proc findForeignFile (c: InlinerCtx ; modul, ext: string ): string =
406410 # # Search the caller's dir first, then the parent — system modules
@@ -415,48 +419,62 @@ proc findForeignFile(c: InlinerCtx; modul, ext: string): string =
415419 if fileExists (parent): return parent
416420 return " "
417421
418- proc loadForeign (c: var InlinerCtx ; modul: string ): bool =
419- # # Lazy-load a foreign module: its proc bodies *and* their inline
420- # # annotations come out of the same parse, so `lookupInlineInfo` and
421- # # `lookupBody` share one file per module.
422+ proc openedForeign (c: var InlinerCtx ; modul: string ): bool =
423+ # # Opens the callee module `modul` into `c.foreign` on first use; false when
424+ # # there is no `.c.nif` for it.
422425 # #
423426 # # The `.c.nif`, because only this pass runs that late: it is post-DCE, so
424427 # # its generic instances already name the module that won the merge and a
425428 # # body copies into any other module unchanged. The `.x.nif` still names the
426429 # # callee's own module for instances the merge later moves elsewhere; that
427430 # # is the intra-module pass's world, not this one's.
428- if modul == c.moduleSuffix: return true
429431 if modul in c.foreign: return true
432+ if modul in c.noForeign: return false
430433 let xpath = findForeignFile (c, modul, " .c.nif" )
431- if xpath.len == 0 : return false
432- var fm: ref ForeignModule
433- new fm
434- fm.buf = parseFromFile (xpath)
435- indexProcBodies (fm.buf, fm.bodies, fm.inlineInfo, ownModule = false )
434+ if xpath.len == 0 :
435+ c.noForeign.incl modul
436+ return false
437+ let fm = openForeignModule (xpath)
438+ if not fm.hasEmbeddedIndex:
439+ quit " inter-module inliner: " & xpath & " carries no embedded index"
436440 c.foreign[modul] = fm
437441 result = true
438442
443+ proc foreignProc (c: var InlinerCtx ; calleeSym: SymId ; decl: var Cursor ): bool =
444+ # # Parses the ONE `(proc …)` decl of `calleeSym` out of its module, through
445+ # # the module's embedded index. This used to parse the whole callee module
446+ # # and score every proc in it — including just to learn that the callee is
447+ # # not inlinable. Measured on a native release build of nimsem: 2,620
448+ # # whole-module parses, 393 MB, 3.83 s of 10.5 s of optimizer time, for at
449+ # # most 11.8 % of the tokens ever being used; `std/syncio` alone was parsed
450+ # # 88 times for 51 body fetches.
451+ let modul = pool.symModule (calleeSym)
452+ if not openedForeign (c, modul): return false
453+ let fm = c.foreign.getOrQuit (modul)
454+ let key = pool.symString (calleeSym)
455+ if not fm.hasDecl (key): return false
456+ # Interned into the global pool/tags so the SymIds and tag ids line up with
457+ # the module being rewritten; dense, seeded line info because a splice copies
458+ # it into the output.
459+ decl = fm.getDecl (key, globalTags, pool, withLineInfo = true )
460+ result = decl.stmtKind == ProcS
461+
439462proc lookupBody (c: var InlinerCtx ; calleeSym: SymId ; outCur: var Cursor ): bool =
440463 # # Resolves a callee sym to a cursor pointing at its `(proc …)` decl.
441464 # # The cursor's refcount keeps the underlying buffer alive for as
442- # # long as the cursor is held — `c.foreign` stores `ref
443- # # ForeignModule`, so subsequent table growth can't move the
444- # # TokenBuf out from under us. Returns false when we don't have a
445- # # body for `calleeSym` (extern decl, missing `.x.nif`, etc.).
465+ # # long as the cursor is held; a foreign decl's buffer is owned by its
466+ # # `ForeignModule` besides. Returns false when we don't have a body for
467+ # # `calleeSym` (extern decl, missing `.c.nif`, etc.).
446468 let modul = pool.symModule (calleeSym)
447469 if modul == c.moduleSuffix:
448470 if calleeSym in c.bodies:
449471 outCur = cursorAt (c.src[], c.bodies.getOrQuit (calleeSym))
450472 return true
451473 return false
452- if not loadForeign (c, modul): return false
453- let fm = c.foreign.getOrQuit (modul)
454- if calleeSym notin fm.bodies: return false
455474 # No further vetting here: the only bodies that reach this point already
456475 # passed `shouldInlineCall`, i.e. the size-driven policy in
457476 # `computeInlineInfo` (tiny → always, big → scored, `.noinline` → never).
458- outCur = cursorAt (fm.buf, fm.bodies.getOrQuit (calleeSym))
459- result = true
477+ result = foreignProc (c, calleeSym, outCur)
460478
461479proc freshSym (c: var InlinerCtx ; orig: SymId ): SymId =
462480 # # Mint a fresh local sym for an inlined body's local. The name must carry
@@ -487,9 +505,15 @@ proc lookupInlineInfo(c: var InlinerCtx; calleeSym: SymId): InlineInfo =
487505 let modul = pool.symModule (calleeSym)
488506 if modul == c.moduleSuffix:
489507 return c.ownInfo.getOrDefault (calleeSym, DefaultInlineInfo )
490- if not loadForeign (c, modul): return DefaultInlineInfo
491- result = c.foreign.getOrQuit (modul).inlineInfo.getOrDefault (calleeSym,
492- DefaultInlineInfo )
508+ if calleeSym in c.foreignInfo: return c.foreignInfo.getOrQuit (calleeSym)
509+ var decl = default (Cursor )
510+ result = DefaultInlineInfo
511+ if foreignProc (c, calleeSym, decl):
512+ # Scored on its own body only — never the small-total rule, see
513+ # `indexProcBodies`.
514+ let info = computeInlineInfo (decl)
515+ if info.inlinable: result = info
516+ c.foreignInfo[calleeSym] = result
493517
494518proc argContainsConstructor (callNode: Cursor ): bool =
495519 # # `(oconstr/aconstr …)` anywhere in an argument. The C backend renders an
0 commit comments