Skip to content

Commit 22c9fd5

Browse files
committed
fixes
1 parent 14c2d7b commit 22c9fd5

3 files changed

Lines changed: 99 additions & 9 deletions

File tree

‎LMLExposition/Collect.lean‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,25 @@ def declPageHrefMap (decls : Array DeclInfo) : Std.HashMap Name String :=
949949
def declByNameMap (decls : Array DeclInfo) : Std.HashMap Name DeclInfo :=
950950
decls.foldl (fun acc decl => acc.insert decl.name decl) {}
951951

952+
/-- True if `n` survives a round trip through `Name`'s JSON codec (`toString` then `toName`).
953+
This is lossy for names containing characters outside a normal identifier — notably auto-generated
954+
helper names from *external* libraries, which can embed hygiene markers like `_@.Mathlib.Foo.123`
955+
that aren't valid surface syntax and so don't parse back to the original name. -/
956+
def isJsonSafeName (n : Name) : Bool :=
957+
n.toString.toName == n
958+
959+
/-- Drops names that don't round-trip through JSON (see `isJsonSafeName`) from `deps`/`typeDeps`,
960+
so that serializing `decls` for the `collect` subcommand can't fail. Safe to do unconditionally:
961+
every consumer of `deps`/`typeDeps` (graph edges, "Type/Body uses" links, the extraction closure)
962+
already discards anything that isn't itself an exposed project declaration, and a name needing
963+
this escape hatch is never one (`shouldExpose` already excludes internal/auto-generated names from
964+
being exposed), so this can only drop names that were already inert. -/
965+
def dropUnsafeDeps (decls : Array DeclInfo) : Array DeclInfo :=
966+
decls.map fun d => { d with
967+
deps := d.deps.filter isJsonSafeName
968+
typeDeps := d.typeDeps.filter isJsonSafeName
969+
}
970+
952971
/-- Helper for runCoreIO. -/
953972
def runCoreIO {α : Type} (env : Environment) (x : CoreM α) : IO α := do
954973
x.toIO'

‎LMLExposition/Extract.lean‎

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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. -/
337390
def 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

‎LMLExposition/Site.lean‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -633,7 +633,7 @@ show today, and writes `excluded-declarations.txt` under `cfg.outputDir` when gi
633633
private def collectData (cfg : Cli) (projectDir : System.FilePath) (ws : Lake.Workspace)
634634
(rootPrefix : Name) (env : Environment) : IO CollectedData := do
635635
let decls ← collectDecls projectDir rootPrefix ws.root env
636-
let decls := decls |> attachReverseDeps |> attachTransitiveDeps |> attachDependsOnSorry
636+
let decls := decls |> dropUnsafeDeps |> attachReverseDeps |> attachTransitiveDeps |> attachDependsOnSorry
637637
let excludedNames :=
638638
(projectConstants env rootPrefix).filterMap fun (name, _, info) =>
639639
if shouldExpose env rootPrefix name info then none else some name
@@ -668,9 +668,9 @@ private def loadCollectedData (path : String) : IO CollectedData := do
668668
let text ← IO.FS.readFile path
669669
let .ok json := Json.parse text
670670
| throw <| IO.userError s!"Failed to parse JSON from {path}"
671-
let .ok (data : CollectedData) := FromJson.fromJson? json
672-
| throw <| IO.userError s!"Failed to decode collected data from {path}"
673-
pure data
671+
match FromJson.fromJson? json with
672+
| .ok (data : CollectedData) => pure data
673+
| .error err => throw <| IO.userError s!"Failed to decode collected data from {path}: {err}"
674674

675675
/-- Builds and renders the Verso site from already-collected data. Needs no Lean environment
676676
and no access to the target project's source tree: `data` and `cfg`'s render-time flags

0 commit comments

Comments
 (0)