STUDIO HAZE · topaz
Topaz source
Open by house policy. Topaz is our own language.
Topaz 5.20 · 56 files
compiler/crates/topaz_self_frontend/topaz/resolver/graph.tpz
Topaz · 24608 B · Original file
1
// Entry admission, module loading, dependency order, and cycle diagnosis.2
// Host queries are derived from explicit facts; scope tables and initializer3
// validation are left to sibling resolver leaves.5
import layout { normalize }6
import parser { parse }7
import raw { lex }8
import types {9
PreviewGeneratedStdModule,10
PreviewHostQuery,11
PreviewImportEdge,12
PreviewModuleResult,13
PreviewResolvedDiagnostic,14
ResolveResult,15
}16
import unicode { caseFold, nfd }17
import resolver.model_facts {18
DiscoveredGraph,19
EdgeDraft,20
ImportDraft,21
ImportPathDisposition,22
ModuleDependencyCatalog,23
ModuleDraft,24
PreparedImport,25
ResolverHostFactCatalog,26
ResolverInputs,27
addQuery,28
appendModuleDiagnostics,29
contains,30
diagnostic,31
hasParentPathSegment,32
pathJoin,33
sortQueries,34
sourceFact,35
stringJoin,36
}37
import resolver.modules_tables { importDrafts }39
function generatedStdModule(40
modules: Array<PreviewGeneratedStdModule>,41
identity: string,42
) -> Option<PreviewGeneratedStdModule> {43
for module in modules {44
if module.identity == identity {45
return Some(module)46
}47
}48
None49
}51
function modulePath(root: string, segments: Array<string>) -> string {52
pathJoin(root, "{stringJoin(segments, "/")}.tpz")53
}55
export function moduleIdentity(entry: string, root: string) -> string {56
let mut relative = entry57
if root != "" && entry.startsWith("{root}/") {58
let bytes = Bytes.encodeUtf8(entry)59
match bytes.slice(Bytes.encodeUtf8(root).length() + 1, bytes.length()).decodeUtf8() {60
case Ok(value) => {61
relative = value62
}63
case Err(_) => ()64
}65
}66
if relative.endsWith(".tpz") {67
let bytes = Bytes.encodeUtf8(relative)68
match bytes.slice(0, bytes.length() - 4).decodeUtf8() {69
case Ok(value) => {70
relative = value71
}72
case Err(_) => ()73
}74
}75
relative.replace("/", ".")76
}78
export function parseModule(79
sourceOrdinal: int,80
identity: string,81
path: string,82
source: string,83
entry: bool,84
externModule: bool,85
generatedStd: bool,86
maxAstNodes: int,87
maxAstDepth: int,88
) -> Result<ModuleDraft, string> {89
let raw = lex(source)90
let layout = normalize(raw.tokens, source)91
let parsed = parse(layout.tokens, source, maxAstNodes, maxAstDepth)?92
let mut diagnostics = raw.diagnostics93
for found in layout.diagnostics {94
diagnostics.push(found)95
}96
for found in parsed.diagnostics {97
diagnostics.push(found)98
}99
let sourceBytes = Bytes.encodeUtf8(source)100
Ok(ModuleDraft {101
result: PreviewModuleResult {102
sourceOrdinal: sourceOrdinal,103
identity: identity,104
path: path,105
source: source,106
entry: entry,107
extern: externModule,108
generatedStd: generatedStd,109
raw: raw.tokens,110
layout: layout.tokens,111
ast: parsed.nodes,112
},113
source: sourceBytes,114
root: parsed.root,115
imports: importDrafts(parsed.root, sourceBytes),116
diagnostics: diagnostics,117
})118
}120
function isExtern(externModules: Array<string>, identity: string) -> bool {121
contains(externModules, identity)122
}124
function isExternNamespace(externModules: Array<string>, identity: string) -> bool {125
let root = match identity.split(".").get(0) {126
case Some(value) => value127
case None => identity128
}129
for module in externModules {130
if module == root || module.startsWith("{root}.") {131
return true132
}133
}134
false135
}137
function validateSegment(138
facts: ResolverHostFactCatalog,139
dir: string,140
name: string,141
directory: bool,142
) -> ImportPathDisposition {143
let fact = match sourceFact(facts, "list-directory", dir) {144
case Some(value) => value145
case None => return ImportPathDisposition { kind: "pending", collisionDetail: "" }146
}147
if fact.status == "unreadable" {148
return ImportPathDisposition {149
kind: "unreadable",150
collisionDetail: fact.reasonCode,151
}152
}153
if fact.status != "present" {154
return ImportPathDisposition { kind: "missing", collisionDetail: "" }155
}156
let mut exact = false157
let mut matches: Array<string> = []158
let normalized = nfd(name)159
let folded = caseFold(normalized)160
for entry in fact.entries {161
let sameKind = (directory && entry.kind == "directory") ||162
(!directory && entry.kind == "file")163
if sameKind {164
if entry.name == name {165
exact = true166
}167
let candidate = nfd(entry.name)168
if candidate == normalized || caseFold(candidate) == folded {169
matches.push(entry.name)170
}171
}172
}173
if matches.length > 1 {174
ImportPathDisposition {175
kind: "collision",176
collisionDetail: ": {stringJoin(matches, " / ")}",177
}178
} else if exact {179
ImportPathDisposition { kind: "ok", collisionDetail: "" }180
} else {181
ImportPathDisposition { kind: "missing", collisionDetail: "" }182
}183
}185
function prepareImport(186
inputs: ResolverInputs,187
import: ImportDraft,188
generated: Option<PreviewGeneratedStdModule>,189
queries: Array<PreviewHostQuery>,190
) -> Result<PreparedImport, ImportPathDisposition> {191
match generated {192
case Some(value) => return Ok(PreparedImport {193
path: value.path,194
source: value.source,195
physicalAlias: "",196
generatedStd: true,197
})198
case None => ()199
}200
let first = match import.segments.get(0) {201
case Some(value) => value202
case None => return Err(ImportPathDisposition {203
kind: "missing",204
collisionDetail: "",205
})206
}207
let path = modulePath(if first == "std" { "" } else { inputs.root }, import.segments)208
if first == "std" {209
addQuery(queries, inputs.hostFacts, "read-source", path)210
return match sourceFact(inputs.hostFacts, "read-source", path) {211
case Some(value) => if value.status == "present" {212
Ok(PreparedImport {213
path: path,214
source: value.source,215
physicalAlias: "",216
generatedStd: false,217
})218
} else if value.status == "unreadable" {219
Err(ImportPathDisposition {220
kind: "source-unreadable",221
collisionDetail: value.reasonCode,222
})223
} else if value.status == "invalid-utf8" {224
Err(ImportPathDisposition {225
kind: "source-invalid-utf8",226
collisionDetail: "",227
})228
} else {229
Err(ImportPathDisposition { kind: "missing", collisionDetail: "" })230
}231
case None => Err(ImportPathDisposition {232
kind: "pending",233
collisionDetail: "",234
})235
}236
}237
addQuery(queries, inputs.hostFacts, "physical-containment", path)238
let containment = match sourceFact(inputs.hostFacts, "physical-containment", path) {239
case Some(value) => if value.status == "outside" {240
return Err(ImportPathDisposition { kind: "outside", collisionDetail: "" })241
} else {242
value243
}244
case None => return Err(ImportPathDisposition {245
kind: "pending",246
collisionDetail: "",247
})248
}249
let mut dir = inputs.root250
let mut index = 0251
for segment in import.segments {252
addQuery(queries, inputs.hostFacts, "list-directory", dir)253
let name = if index + 1 == import.segments.length {254
"{segment}.tpz"255
} else {256
segment257
}258
let disposition = validateSegment(259
inputs.hostFacts,260
dir,261
name,262
index + 1 < import.segments.length,263
)264
if disposition.kind != "ok" {265
return Err(disposition)266
}267
if index + 1 < import.segments.length {268
dir = pathJoin(dir, segment)269
}270
index += 1271
}272
addQuery(queries, inputs.hostFacts, "read-source", path)273
match sourceFact(inputs.hostFacts, "read-source", path) {274
case Some(value) => if value.status == "present" {275
Ok(PreparedImport {276
path: path,277
source: value.source,278
physicalAlias: if containment.status == "inside" {279
containment.aliasClass280
} else {281
""282
},283
generatedStd: false,284
})285
} else if value.status == "unreadable" {286
Err(ImportPathDisposition {287
kind: "source-unreadable",288
collisionDetail: value.reasonCode,289
})290
} else if value.status == "invalid-utf8" {291
Err(ImportPathDisposition {292
kind: "source-invalid-utf8",293
collisionDetail: "",294
})295
} else {296
Err(ImportPathDisposition { kind: "missing", collisionDetail: "" })297
}298
case None => Err(ImportPathDisposition {299
kind: "pending",300
collisionDetail: "",301
})302
}303
}305
export function dependencyOrder(306
modules: Array<ModuleDraft>,307
catalog: ModuleDependencyCatalog,308
) -> Array<ModuleDraft> {309
let mut remaining: Set<string> = set {}310
for identity in catalog.identities {311
remaining.add(identity)312
}313
let mut done: Set<string> = set {}314
let mut output: Array<ModuleDraft> = []315
while output.length < modules.length {316
let mut ready: Option<int> = None317
for identity in catalog.identities {318
if !remaining.contains(identity) {319
continue320
}321
let mut blocked = false322
match catalog.dependencies.get(identity) {323
case Some(targets) => for target in targets {324
if !done.contains(target) {325
blocked = true326
}327
}328
case None => ()329
}330
if !blocked {331
ready = catalog.moduleIndexes.get(identity)332
break333
}334
}335
let selected = match ready {336
case Some(index) => modules[index]337
case None => return modules338
}339
remaining.remove(selected.result.identity)340
output.push(selected)341
done.add(selected.result.identity)342
}343
output344
}346
export function moduleDependencyCatalog(347
modules: Array<ModuleDraft>,348
edges: Array<EdgeDraft>,349
) -> ModuleDependencyCatalog {350
let mut identities: Array<string> = []351
let mut moduleIndexes: Map<string, int> = Map.new()352
let mut dependencies: Map<string, Set<string>> = Map.new()353
let mut dependents: Map<string, Set<string>> = Map.new()354
let mut publicEdges: Array<PreviewImportEdge> = []355
let mut moduleIndexValue = 0356
for module in modules {357
identities.push(module.result.identity)358
moduleIndexes.insert(module.result.identity, moduleIndexValue)359
dependencies.insert(module.result.identity, set {})360
dependents.insert(module.result.identity, set {})361
moduleIndexValue += 1362
}363
identities.sort()364
for edge in edges {365
if moduleIndexes.containsKey(edge.from) && moduleIndexes.containsKey(edge.to) {366
publicEdges.push(PreviewImportEdge { from: edge.from, to: edge.to })367
match dependencies.get(edge.from) {368
case Some(found) => {369
let mut targets = found370
targets.add(edge.to)371
}372
case None => ()373
}374
match dependents.get(edge.to) {375
case Some(found) => {376
let mut sources = found377
sources.add(edge.from)378
}379
case None => ()380
}381
}382
}383
ModuleDependencyCatalog {384
identities: identities,385
moduleIndexes: moduleIndexes,386
dependencies: dependencies,387
dependents: dependents,388
publicEdges: publicEdges,389
}390
}392
function cycleFrom(393
start: string,394
current: string,395
catalog: ModuleDependencyCatalog,396
path: Array<string>,397
visited: Set<string>,398
) -> Option<Array<string>> {399
let mut targets = match catalog.dependencies.get(current) {400
case Some(found) => found.toArray()401
case None => []402
}403
targets.sort()404
let mut activePath = path405
let mut activeVisited = visited406
for target in targets {407
if target == start {408
if path.length == 1 {409
continue410
}411
let mut result: Array<string> = []412
for item in path {413
result.push(item)414
}415
result.push(start)416
return Some(result)417
}418
if activeVisited.contains(target) {419
continue420
}421
activePath.push(target)422
activeVisited.add(target)423
let found = cycleFrom(start, target, catalog, activePath, activeVisited)424
activePath.pop()425
activeVisited.remove(target)426
match found {427
case Some(result) => return Some(result)428
case None => ()429
}430
}431
None432
}434
function collectReachable(435
current: string,436
graph: Map<string, Set<string>>,437
visited: Set<string>,438
) -> () {439
let targets = match graph.get(current) {440
case Some(found) => found441
case None => return ()442
}443
let mut activeVisited = visited444
for next in targets {445
if !activeVisited.contains(next) {446
activeVisited.add(next)447
let _ = collectReachable(next, graph, activeVisited)448
}449
}450
()451
}453
function reachableFrom(454
start: string,455
graph: Map<string, Set<string>>,456
) -> Set<string> {457
let mut reachable: Set<string> = set {}458
reachable.add(start)459
collectReachable(start, graph, reachable)460
reachable461
}463
export function diagnoseCycles(464
catalog: ModuleDependencyCatalog,465
edges: Array<EdgeDraft>,466
diagnostics: Array<PreviewResolvedDiagnostic>,467
) -> () {468
let mut covered: Set<string> = set {}469
for identity in catalog.identities {470
if covered.contains(identity) {471
continue472
}473
let mut pathVisited: Set<string> = set {}474
pathVisited.add(identity)475
match cycleFrom(identity, identity, catalog, [identity], pathVisited) {476
case Some(path) => {477
let next = path[1]478
let cycleText = stringJoin(path, " -> ")479
for edge in edges {480
if edge.from == identity && edge.to == next {481
diagnostic(482
diagnostics,483
edge.moduleIndex,484
"TPZ3006",485
"import cycle: {cycleText}",486
edge.lo,487
edge.hi,488
)489
break490
}491
()492
}493
let forward = reachableFrom(identity, catalog.dependencies)494
let reverse = reachableFrom(identity, catalog.dependents)495
for candidate in catalog.identities {496
if forward.contains(candidate) && reverse.contains(candidate) {497
covered.add(candidate)498
}499
}500
()501
}502
case None => {503
for edge in edges {504
if edge.from == identity && edge.to == identity {505
diagnostic(506
diagnostics,507
edge.moduleIndex,508
"TPZ3006",509
"module imports itself: `{identity}`",510
edge.lo,511
edge.hi,512
)513
covered.add(identity)514
break515
}516
()517
}518
()519
}520
}521
()522
}523
()524
}526
export function pendingResult(queries: Array<PreviewHostQuery>) -> ResolveResult {527
ResolveResult {528
queries: sortQueries(queries),529
modules: [],530
edges: [],531
scopes: [],532
declarations: [],533
references: [],534
exports: [],535
diagnostics: [],536
}537
}539
export function validateEntryPath(540
entry: string,541
root: string,542
diagnostics: Array<PreviewResolvedDiagnostic>,543
) -> bool {544
let parentSegment = hasParentPathSegment(entry) || hasParentPathSegment(root)545
let rootIsEntry = root != "" && entry == root546
if parentSegment ||547
rootIsEntry ||548
(root != "" && !entry.startsWith("{root}/")) {549
diagnostic(550
diagnostics,551
0,552
"TPZ3002",553
if parentSegment {554
"entry and root paths must not contain parent (`..`) segments"555
} else if rootIsEntry {556
"the source root `{root}` must be a directory containing the entry, not the entry file itself"557
} else {558
"the root `{root}` does not contain the entry `{entry}`"559
},560
0,561
0,562
)563
return false564
}565
true566
}568
export function validateEntryPhysicalContainment(569
entry: string,570
facts: ResolverHostFactCatalog,571
diagnostics: Array<PreviewResolvedDiagnostic>,572
) -> bool {573
match sourceFact(facts, "physical-containment", entry) {574
case Some(value) => if value.status == "outside" {575
diagnostic(576
diagnostics,577
0,578
"TPZ3005",579
"the entry `{entry}` resolves outside the root (symlink/alias containment)",580
0,581
0,582
)583
false584
} else {585
true586
}587
case None => true588
}589
}591
export function readEntrySource(592
entry: string,593
facts: ResolverHostFactCatalog,594
diagnostics: Array<PreviewResolvedDiagnostic>,595
) -> Option<string> {596
match sourceFact(facts, "read-source", entry) {597
case Some(value) => if value.status == "present" {598
Some(value.source)599
} else if value.status == "unreadable" {600
diagnostic(601
diagnostics,602
0,603
"TPZ3003",604
"cannot load entry `{entry}`: {value.reasonCode}",605
0,606
0,607
)608
None609
} else if value.status == "invalid-utf8" {610
diagnostic(611
diagnostics,612
0,613
"TPZ3003",614
"cannot load entry `{entry}`: source is not valid UTF-8",615
0,616
0,617
)618
None619
} else {620
diagnostic(621
diagnostics,622
0,623
"TPZ3001",624
"entry file `{entry}` does not exist",625
0,626
0,627
)628
None629
}630
case None => {631
diagnostic(632
diagnostics,633
0,634
"TPZ3001",635
"entry file `{entry}` does not exist",636
0,637
0,638
)639
None640
}641
}642
}644
function validateImportedTopLevel(645
module: ModuleDraft,646
target: string,647
moduleIndexValue: int,648
diagnostics: Array<PreviewResolvedDiagnostic>,649
) -> () {650
for statement in module.root.children {651
let allowed = statement.kind == "statement/import" ||652
statement.kind == "statement/export" ||653
statement.kind == "statement/function" ||654
statement.kind == "statement/impl" ||655
statement.kind == "statement/protocol" ||656
statement.kind == "statement/type-alias" ||657
statement.kind == "statement/enum" ||658
statement.kind == "statement/record" ||659
statement.kind == "statement/newtype" ||660
statement.kind == "statement/let" ||661
statement.kind == "statement/const"662
if !allowed {663
diagnostic(664
diagnostics,665
moduleIndexValue,666
"TPZ3007",667
"imported module `{target}` contains a top-level free statement; free statements are entry-only (the same file is valid as an entry)",668
statement.lo,669
statement.hi,670
)671
}672
}673
()674
}676
function recordImportEdge(677
module: ModuleDraft,678
moduleIndexValue: int,679
import: ImportDraft,680
importedHere: Array<string>,681
edges: Array<EdgeDraft>,682
diagnostics: Array<PreviewResolvedDiagnostic>,683
) -> bool {684
let firstSegment = match import.segments.get(0) {685
case Some(value) => value686
case None => ""687
}688
if firstSegment == "topaz" {689
diagnostic(690
diagnostics,691
moduleIndexValue,692
"TPZ3016",693
"the module path root `topaz` is reserved; user modules cannot live under it",694
import.pathLo,695
import.pathHi,696
)697
return false698
}699
let mut graphEdges = edges700
graphEdges.push(EdgeDraft {701
from: module.result.identity,702
to: import.target,703
moduleIndex: moduleIndexValue,704
lo: import.pathLo,705
hi: import.pathHi,706
})707
if contains(importedHere, import.target) {708
diagnostic(709
diagnostics,710
moduleIndexValue,711
"TPZ3017",712
"`{import.target}` is already imported by this module; a logical module may appear in at most one import item",713
import.pathLo,714
import.pathHi,715
)716
return false717
}718
let mut admitted = importedHere719
admitted.push(import.target)720
true721
}723
function reportImportDisposition(724
inputs: ResolverInputs,725
import: ImportDraft,726
disposition: ImportPathDisposition,727
moduleIndexValue: int,728
diagnostics: Array<PreviewResolvedDiagnostic>,729
) -> () {730
if disposition.kind == "pending" {731
return ()732
}733
if disposition.kind == "outside" {734
diagnostic(735
diagnostics,736
moduleIndexValue,737
"TPZ3005",738
"`{import.target}` resolves outside the root (symlink/alias containment)",739
import.pathLo,740
import.pathHi,741
)742
return ()743
}744
if disposition.kind == "collision" {745
diagnostic(746
diagnostics,747
moduleIndexValue,748
"TPZ3004",749
"the path `{import.target}` collides with another candidate under the module name keys{disposition.collisionDetail}",750
import.pathLo,751
import.pathHi,752
)753
return ()754
}755
if disposition.kind == "unreadable" {756
diagnostic(757
diagnostics,758
moduleIndexValue,759
"TPZ3003",760
"cannot inspect module path for `{import.target}`: {disposition.collisionDetail}",761
import.pathLo,762
import.pathHi,763
)764
return ()765
}766
if disposition.kind == "source-unreadable" ||767
disposition.kind == "source-invalid-utf8" {768
diagnostic(769
diagnostics,770
moduleIndexValue,771
"TPZ3003",772
if disposition.kind == "source-invalid-utf8" {773
"cannot load module `{import.target}`: source is not valid UTF-8"774
} else {775
"cannot load module `{import.target}`: {disposition.collisionDetail}"776
},777
import.pathLo,778
import.pathHi,779
)780
return ()781
}782
if disposition.kind == "missing" {783
let standard = match import.segments.get(0) {784
case Some(value) => value == "std"785
case None => false786
}787
let externNamespace = !standard &&788
isExternNamespace(inputs.externModules, import.target)789
diagnostic(790
diagnostics,791
moduleIndexValue,792
if externNamespace { "TPZ5030" } else { "TPZ3001" },793
if externNamespace {794
"extern module `{import.target}` is not declared in topaz.toml"795
} else if standard {796
"no std module for `{import.target}` (expected virtual `{stringJoin(import.segments, "/")}.tpz`)"797
} else {798
"no module file for `{import.target}` (expected `{modulePath(inputs.root, import.segments)}` by exact scalars)"799
},800
import.pathLo,801
import.pathHi,802
)803
}804
()805
}807
function loadImportedModule(808
inputs: ResolverInputs,809
import: ImportDraft,810
prepared: PreparedImport,811
moduleIndexValue: int,812
) -> Result<ModuleDraft, string> {813
parseModule(814
moduleIndexValue,815
import.target,816
prepared.path,817
prepared.source,818
false,819
isExtern(inputs.externModules, import.target),820
prepared.generatedStd,821
inputs.maxAstNodes,822
inputs.maxAstDepth,823
)824
}826
export function discoverModuleGraph(827
entryDraft: ModuleDraft,828
inputs: ResolverInputs,829
queries: Array<PreviewHostQuery>,830
diagnostics: Array<PreviewResolvedDiagnostic>,831
) -> Result<DiscoveredGraph, string> {832
let mut discovered: Array<ModuleDraft> = [entryDraft]833
let mut queued: Set<string> = set { entryDraft.result.identity }834
let mut physicalIdentities: Map<string, string> = Map.new()835
match sourceFact(inputs.hostFacts, "physical-containment", entryDraft.result.path) {836
case Some(fact) => if fact.status == "inside" && fact.aliasClass != "" {837
physicalIdentities.insert(fact.aliasClass, entryDraft.result.identity)838
}839
case None => ()840
}841
appendModuleDiagnostics(diagnostics, 0, entryDraft.diagnostics)842
let mut edges: Array<EdgeDraft> = []843
let mut next = 0844
while next < discovered.length {845
let module = discovered[next]846
let mut importedHere: Array<string> = []847
for import in module.imports {848
if !recordImportEdge(849
module,850
next,851
import,852
importedHere,853
edges,854
diagnostics,855
) {856
continue857
}858
if queued.contains(import.target) {859
continue860
}861
queued.add(import.target)862
let generated = generatedStdModule(inputs.generatedStdModules, import.target)863
match prepareImport(inputs, import, generated, queries) {864
case Ok(prepared) => {865
if prepared.physicalAlias != "" {866
match physicalIdentities.get(prepared.physicalAlias) {867
case Some(existing) => {868
diagnostic(869
diagnostics,870
next,871
"TPZ3004",872
"the modules `{existing}` and `{import.target}` resolve to the same physical file; one physical file cannot have two module identities",873
import.pathLo,874
import.pathHi,875
)876
continue877
}878
case None => physicalIdentities.insert(prepared.physicalAlias, import.target)879
}880
}881
let imported = loadImportedModule(882
inputs,883
import,884
prepared,885
discovered.length,886
)?887
let importedIndex = discovered.length888
appendModuleDiagnostics(diagnostics, importedIndex, imported.diagnostics)889
validateImportedTopLevel(imported, import.target, importedIndex, diagnostics)890
discovered.push(imported)891
}892
case Err(disposition) => {893
reportImportDisposition(894
inputs,895
import,896
disposition,897
next,898
diagnostics,899
)900
}901
}902
}903
next += 1904
}905
Ok(DiscoveredGraph {906
modules: discovered,907
edges: edges,908
})909
}SHA256 b0dc672402bff2c4df9937c2486a12ea4a2caeb264db9aefe31efdea7ea1c5e1