Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-mcp

Model Context Protocol server for Pudu language documentation and compiler tools

0.1.1Apache-2.01

InstallClose

Reference.pudu

Pudu156 lines5.9 KB

GitHub ↗
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 }2122/// Where the standard library's modules live under the library root.23const STD_DIRECTORY: Str = "Std"2425/// Where a workspace keeps its installed packages.26const DEPS_DIRECTORY: Str = "deps"2728/// How deep a directory walk goes before it stops.29const MAX_WALK_DEPTH: Int = 123031/// Every module file of the standard library and the workspace's installed packages.32export 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}5556/// The file of one module.57export fn fileOf(files: &Array[ModuleFile], moduleName: Str) -> Option[ModuleFile] {58  List.find(files, |file: ModuleFile| file.moduleName == moduleName)59}6061/// The public declarations of every file, from the compiler's reference output.62export 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}9091/// The public declarations of one module's file.92export 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}99100/// The public declarations of one chunk of files.101fn 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}109110/// Every `.pudu` file under a directory, without following links to directories.111fn 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}123124/// Files named by their module, relative to the source root they sit under.125fn 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}136137/// Whether a path is a directory that is not a symbolic link.138fn isDirectory(path: Str) -> Bool {139  match Fs.metadata(path) {140    case Ok(found) => found.isDirectory && !found.isSymbolicLink141    case Err(_) => false142  }143}144145/// Items in consecutive groups of at most `size`.146fn 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