
Reference.pudu
Pudu156 lines5.9 KB
1/** @Services.Reference.Indexer — builds the declaration index from the compiler */2module PuduLangMcp.Services.Reference34import Std.Concurrent as Concurrent5import Std.Fs as Fs6import Std.Io as Io7import Std.List as List8import Std.Math as Math9import Std.Option as Option10import Std.Path as Path11import Std.Result as Result12import PuduLangMcp.Constants.Server as Server13import PuduLangMcp.Domain.Code.SourceFile as SourceFile14import PuduLangMcp.Domain.Reference.Decode as Decode15import PuduLangMcp.Domain.Reference.Entry as Entry16import PuduLangMcp.Errors.ToolError as ToolError17import PuduLangMcp.Services.Toolchain as Toolchain1819/** @Services.Reference.ModuleFile — one importable module and its file */20export type ModuleFile = { moduleName: Str, path: Str, origin: Str }212223const STD_DIRECTORY: Str = "Std"242526const DEPS_DIRECTORY: Str = "deps"272829const MAX_WALK_DEPTH: Int = 12303132export fn moduleFiles(library: Option[Str], workspaceRoot: Str) -> Array[ModuleFile] {33 var files: Array[ModuleFile] = []34 if let Some(root) = library {35 files = files.concat(named(root, &walk(Path.join(root, STD_DIRECTORY), 0), STD_DIRECTORY))36 }37 let deps = Path.join(workspaceRoot, DEPS_DIRECTORY)38 for package in Result.unwrapOr(Io.listPaths(deps), []) {39 if isDirectory(package) {40 let source = Path.join(package, "src")41 let root = if isDirectory(source) { source } else { package }42 files = files.concat(named(root, &walk(root, 0), Path.nameOf(package)))43 }44 }45 var seen: Set[Str] = setOf([])46 var unique: Array[ModuleFile] = []47 for file in files {48 if !seen.contains(file.moduleName) {49 seen = seen.insert(file.moduleName)50 unique = unique.push(file)51 }52 }53 List.sortOn(&unique, |file: ModuleFile| file.moduleName)54}555657export fn fileOf(files: &Array[ModuleFile], moduleName: Str) -> Option[ModuleFile] {58 List.find(files, |file: ModuleFile| file.moduleName == moduleName)59}606162export fn build(toolchain: &Toolchain.Toolchain, files: &Array[ModuleFile]) -> Result[Array[Entry.Entry], ToolError.ToolFailure] {63 if toolchain.compiler == None { return Err(ToolError.ToolchainMissing) }64 let chunks = chunked(&files.map(|file: ModuleFile| file.path), Server.REFERENCE_CHUNK)65 let runner = *toolchain66 let decoded = match Concurrent.mapBounded(&chunks, Server.REFERENCE_WORKERS, fn(chunk: Array[Str]) -> Result[Array[Entry.Entry], Str] {67 indexChunk(&runner, &chunk)68 }) {69 case Ok(results) => results70 case Err(_) => { return Err(ToolError.Unavailable("The reference index could not be built in parallel.")) }71 }72 var seen: Set[Str] = setOf([])73 var entries: Array[Entry.Entry] = []74 for result in decoded {75 match result {76 case Ok(found) => {77 for entry in found {78 let key = entry.moduleName + "." + entry.name + " " + entry.kind + " " + entry.signature79 if !seen.contains(key) {80 seen = seen.insert(key)81 entries = entries.push(entry)82 }83 }84 }85 case Err(problem) => { return Err(ToolError.Unavailable("The reference index could not be built: " + problem)) }86 }87 }88 Ok(entries)89}909192export fn moduleEntries(toolchain: &Toolchain.Toolchain, file: &ModuleFile) -> Result[Array[Entry.Entry], ToolError.ToolFailure] {93 if toolchain.compiler == None { return Err(ToolError.ToolchainMissing) }94 match indexChunk(toolchain, &[file.path]) {95 case Ok(found) => Ok(found.filter(|entry: Entry.Entry| entry.moduleName == file.moduleName))96 case Err(problem) => Err(ToolError.Unavailable("The reference for " + file.moduleName + " could not be read: " + problem))97 }98}99100101fn indexChunk(toolchain: &Toolchain.Toolchain, chunk: &Array[Str]) -> Result[Array[Entry.Entry], Str] {102 let api = toolchain.run(["api", "--json"].concat(*chunk), "", "", Server.REFERENCE_TIMEOUT_MS, Server.REFERENCE_OUTPUT_CAP_BYTES) ?103 if api.timedOut { return Err("pudu api timed out") }104 let exports = Decode.exportsOf(api.output) ?105 let doc = toolchain.run(["doc", "--json"].concat(*chunk), "", "", Server.REFERENCE_TIMEOUT_MS, Server.REFERENCE_OUTPUT_CAP_BYTES) ?106 if doc.timedOut { return Err("pudu doc timed out") }107 Decode.entriesOf(doc.output, &exports)108}109110111fn walk(directory: Str, depth: Int) -> Array[Str] {112 if depth > MAX_WALK_DEPTH { return [] }113 var found: Array[Str] = []114 for path in Result.unwrapOr(Io.listPaths(directory), []) {115 if isDirectory(path) {116 found = found.concat(walk(path, depth + 1))117 } else if path.endsWith(".pudu") {118 found = found.push(path)119 }120 }121 found122}123124125fn named(root: Str, paths: &Array[Str], origin: Str) -> Array[ModuleFile] {126 var files: Array[ModuleFile] = []127 for path in *paths {128 if let Some(relative) = Path.relativeTo(root, path) {129 if let Some(moduleName) = SourceFile.moduleOfFile(relative) {130 files = files.push(ModuleFile{moduleName: moduleName, path: path, origin: origin})131 }132 }133 }134 files135}136137138fn isDirectory(path: Str) -> Bool {139 match Fs.metadata(path) {140 case Ok(found) => found.isDirectory && !found.isSymbolicLink141 case Err(_) => false142 }143}144145146fn chunked(items: &Array[Str], size: Int) -> Array[Array[Str]] {147 var groups: Array[Array[Str]] = []148 var start = 0149 while start < items.length() {150 let end = Math.min(start + size, items.length())151 groups = groups.push(items.slice(start, end))152 start = end153 }154 groups155}156