@@ -49,11 +49,30 @@ structure CommandEntry where
4949 src : String
5050 kind : SyntaxNodeKind
5151 declNames : Array Name := #[]
52- /-- For a `namespace` command, the namespace it opens (used to emit existence stubs). -/
52+ /-- For a `namespace` command, the namespace it opens exactly as spelled in the source (used by
53+ `activePrefixes` for `variable`-pruning; kept relative/possibly-unqualified on purpose, since
54+ that pruning logic is unaffected by whether a namespace was entered via its full dotted path or
55+ a name relative to an already-open ancestor). -/
5356 nsName? : Option Name := none
57+ /-- For a `namespace` command, the namespace it opens as a *fully qualified* name (i.e.
58+ including any enclosing `namespace`s it was nested inside), used only to emit existence stubs
59+ (`nsStubs` in `assembleTarget`). Two `namespace` commands for the same actual namespace, one
60+ spelled relative to an open ancestor and the other fully dotted, must be recognized as the same
61+ namespace here — otherwise the assembled file ends up with an extra empty stub for the relative
62+ spelling that is a distinct (and so ambiguous, once both are in scope) namespace from the real,
63+ populated one. -/
64+ qualifiedNsName? : Option Name := none
5465 /-- For a `variable` command, its binders decomposed as `(source text, identifiers referenced)`,
5566 so that binders mentioning declarations outside a target's closure can be dropped. -/
5667 binders : Array (String × Array String) := #[]
68+ /-- For an `open NS (a b c)` command (the explicit-list form, as opposed to a bare `open NS`),
69+ `NS` exactly as spelled in the source, and the listed identifiers (`a b c`). Used to drop names
70+ from the list (or the whole command, if none survive) that aren't part of a target's closure —
71+ keeping all of them unconditionally would otherwise reference a declaration that was dropped
72+ (or even `NS` itself, in a target where nothing causes `NS`, or a stub for it, to exist at all).
73+ `openOnlyIdents` is empty for every other form of `open` (and for every other command kind). -/
74+ openOnlyNamespace? : Option String := none
75+ openOnlyIdents : Array String := #[]
5776 /-- Extra commands to emit right after this one — used for `instance … := sorry` replacements of a
5877 definition's `deriving` clause (which can't be re-derived in the minimal file). -/
5978 appended : Array String := #[]
@@ -287,6 +306,16 @@ def processFile (env : Environment) (source : String) (filePath : String)
287306 let cmdState := Command.mkState env messages {}
288307 let s ← IO.processCommands inputCtx parserState cmdState
289308 let mut entries : Array CommandEntry := #[]
309+ -- Stack of the fully-qualified namespace prefix in effect *after* each currently-open
310+ -- `namespace`/`section` frame, so a `namespace` command nested inside another (rather than
311+ -- spelled with the full dotted path) still gets its true fully-qualified name as `nsName?`
312+ -- below — e.g. `namespace Learning` then later `namespace IsBayesAlgEnvSeq` must record
313+ -- `Learning.IsBayesAlgEnvSeq`, the same name a single `namespace Learning.IsBayesAlgEnvSeq`
314+ -- command would record, since both spellings denote the same namespace. Without this, the two
315+ -- spellings are treated as unrelated namespaces, and the assembled file ends up with both an
316+ -- empty stub for the bare name and the real (populated) one for the qualified name, which can
317+ -- make an unqualified reference to a member of the real one ambiguous.
318+ let mut nsPrefixStack : Array Name := #[Name.anonymous]
290319 for stx in s.commands do
291320 if stx.getKind == ``Parser.Module.header then continue
292321 let some cmdStart := stx.getPos? | continue
@@ -322,23 +351,56 @@ def processFile (env : Environment) (source : String) (filePath : String)
322351 let kind := stx.getKind
323352 let nsName? := if kind == ``Parser.Command.namespace && stx.getArgs.size ≥ 2 then
324353 some stx[1 ].getId else none
354+ let qualifiedNsName? := nsName?.map (nsPrefixStack.back! ++ ·)
355+ if let some ns := qualifiedNsName? then
356+ nsPrefixStack := nsPrefixStack.push ns
357+ else if kind == ``Parser.Command.«section » then
358+ nsPrefixStack := nsPrefixStack.push nsPrefixStack.back!
359+ else if kind == ``Parser.Command.«end » && nsPrefixStack.size > 1 then
360+ nsPrefixStack := nsPrefixStack.pop
325361 let binders := if kind == ``Parser.Command.«variable » then
326362 decomposeVariable source stx else #[]
363+ -- `openOnly` is `open NS (a b c)`, parsed as 4 children: the `NS` ident, the `(` token, a
364+ -- node wrapping the `a b c` idents, and the `)` token. Reading `NS` and the list from their
365+ -- own (3rd and 1st) children, rather than from a walk of the whole `openOnly` node, avoids
366+ -- needing to separate them by position — `collectIdents`'s stack-based walk doesn't visit
367+ -- children left-to-right, so naming isn't reliable from a whole-node walk.
368+ let (openOnlyNamespace?, openOnlyIdents) :=
369+ if kind == ``Parser.Command.«open » && stx.getArgs.size ≥ 2
370+ && stx[1 ].getKind == ``Parser.Command.openOnly && stx[1 ].getArgs.size ≥ 3 then
371+ (some stx[1 ][0 ].getId.toString, collectIdents stx[1 ][2 ])
372+ else (none, #[])
327373 entries := entries.push
328- { cls := .context, src := slice source cmdStart cmdEnd, kind, nsName?, binders }
374+ { cls := .context, src := slice source cmdStart cmdEnd, kind, nsName?, qualifiedNsName?,
375+ binders, openOnlyNamespace?, openOnlyIdents }
329376 else
330377 entries := entries.push { cls := .skip, src := slice source cmdStart cmdEnd, kind := stx.getKind }
331378 return entries
332379
333380/-! ## Phase 2: per-target filtering and section stripping -/
334381
335382/-- Restricts declaration entries to those defining a declaration in `keep`; the rest become `skip`.
336- Context entries are preserved. -/
383+ Context entries are preserved, except an `open NS (a b c)` is trimmed to just the listed
384+ identifiers that are the short name of something in `keep` (or dropped entirely if none are) —
385+ keeping a name unconditionally would otherwise reference a declaration this target dropped, or
386+ even `NS` itself, in a target where nothing causes `NS` (or a stub for it) to exist at all.
387+ Identifiers are matched by short name only (not full path), so this can only under-drop (never
388+ wrongly drop a name that is actually needed), since a false-positive match just means a harmless
389+ extra name is kept in the list rather than the more precise outcome of dropping it. -/
337390def restrictToTarget (entries : Array CommandEntry) (keep : Std.HashSet Name) : Array CommandEntry :=
391+ let keepShortNames : Std.HashSet String := keep.fold (init := {}) fun s n => s.insert n.getString!
338392 entries.map fun e =>
339393 match e.cls with
340394 | .decl => if e.declNames.any keep.contains then e else { e with cls := .skip }
341- | _ => e
395+ | .context =>
396+ match e.openOnlyNamespace? with
397+ | none => e
398+ | some ns =>
399+ let kept := e.openOnlyIdents.filter keepShortNames.contains
400+ if kept.isEmpty then { e with cls := .skip }
401+ else if kept.size == e.openOnlyIdents.size then e
402+ else { e with src := s! "open { ns} ({ String.intercalate " " kept.toList} )" }
403+ | .skip => e
342404
343405/-! ## Phase 3: assembly -/
344406
@@ -515,7 +577,7 @@ def assembleTarget (env : Environment) (rootPrefix : Name) (cache : Std.HashMap
515577 if seen.contains ns then (seen, acc) else (seen.insert ns, acc.push ns)
516578 for (_, entries) in involved do
517579 for e in entries do
518- if let some ns := e.nsName ? then
580+ if let some ns := e.qualifiedNsName ? then
519581 (seen, acc) := add ns seen acc
520582 else if e.kind == ``Parser.Command.«open » then
521583 -- Tokens after `open`/`scoped` that name a project namespace.
@@ -554,6 +616,14 @@ def assembleTarget (env : Environment) (rootPrefix : Name) (cache : Std.HashMap
554616 modName.toString.drop (rootPrefix.toString.length + 1 )
555617 else modName.toString
556618 items := items.push (.hard, s! "\n -- ═══ { shortName} ═══\n " )
619+ -- Wraps each module's replayed content in its own `section … end`, so its `open` commands
620+ -- (which, unlike `notation`/`def`/etc., are scoped by `section`) don't leak into later
621+ -- modules. Without this, each contributing module's `open`s pile up across the whole
622+ -- assembled file instead of each being local to its own file as in the original project,
623+ -- and repeating the same `open Foo` several times can make an unqualified name reachable
624+ -- through several redundant open-paths to the same declaration, which Lean then reports as
625+ -- ambiguous even though every path resolves to the exact same constant.
626+ items := items.push (.openSection, "section\n " )
557627 for e in entries do
558628 match e.cls with
559629 | .context =>
@@ -588,6 +658,7 @@ def assembleTarget (env : Environment) (rootPrefix : Name) (cache : Std.HashMap
588658 s := s ++ "\n "
589659 items := items.push (.hard, s)
590660 | .skip => pure ()
661+ items := items.push (.close, "end\n " )
591662 out := out ++ stripEmptyScopes items
592663 return (collapseBlankRuns out).trimAsciiEnd.toString ++ "\n "
593664
0 commit comments