Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 125 additions & 35 deletions source/compiler/qsc_doc_gen/src/generate_docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
#[cfg(test)]
mod tests;

use crate::display::{CodeDisplay, Lookup};
use crate::display::{increase_header_level, parse_doc_for_summary};
use crate::display::{CodeDisplay, Lookup, increase_header_level, parse_doc_for_summary};
use crate::table_of_contents::table_of_contents;
use qsc_ast::ast;
use qsc_data_structures::language_features::LanguageFeatures;
Expand All @@ -15,6 +14,8 @@ use qsc_frontend::resolve;
use qsc_hir::hir::{CallableKind, Item, ItemKind, Package, PackageId, Res, Visibility};
use qsc_hir::{hir, ty};
use rustc_hash::FxHashMap;
use std::collections::BTreeMap;
use std::fmt::Write;
use std::fmt::{Display, Formatter, Result};
use std::rc::Rc;
use std::sync::Arc;
Expand Down Expand Up @@ -276,46 +277,46 @@ impl Lookup for Compilation {
}
}

/// Generates and returns documentation files for the standard library
/// and additional sources (if specified.)
#[must_use]
pub fn generate_docs(
additional_sources: Option<(PackageStore, &Dependencies, SourceMap)>,
capabilities: Option<TargetCapabilityFlags>,
language_features: Option<LanguageFeatures>,
) -> Files {
// Capabilities should default to all capabilities for documentation generation.
let capabilities = Some(capabilities.unwrap_or(TargetCapabilityFlags::all()));
let compilation = Compilation::new(additional_sources, capabilities, language_features);
let mut files: FilesWithMetadata = vec![];
/// Determines the package kind for a given package in the compilation context
fn determine_package_kind(package_id: PackageId, compilation: &Compilation) -> Option<PackageKind> {
let is_current_package = compilation.current_package_id == Some(package_id);

if package_id == PackageId::CORE {
// Core package is always included in the compilation.
Some(PackageKind::Core)
} else if package_id == 1.into() {
// Standard package is currently always included, but this isn't enforced by the compiler.
Some(PackageKind::StandardLibrary)
} else if is_current_package {
// This package could be user code if current package is specified.
Some(PackageKind::UserCode)
} else {
// This is a either a direct dependency of the user code or
// is not a package user can access (an indirect dependency).
compilation
.dependencies
.get(&package_id)
.map(|alias| PackageKind::AliasedPackage(alias.to_string()))
}
}

let display = &CodeDisplay {
compilation: &compilation,
};
/// Processes all packages in a compilation and builds a table-of-contents structure
fn build_toc_from_compilation(
compilation: &Compilation,
mut files: Option<&mut FilesWithMetadata>,
) -> ToC {
let display = &CodeDisplay { compilation };

let mut toc: ToC = FxHashMap::default();

for (package_id, unit) in &compilation.package_store {
let is_current_package = compilation.current_package_id == Some(package_id);
let package_kind;
if package_id == PackageId::CORE {
// Core package is always included in the compilation.
package_kind = PackageKind::Core;
} else if package_id == 1.into() {
// Standard package is currently always included, but this isn't enforced by the compiler.
package_kind = PackageKind::StandardLibrary;
} else if is_current_package {
// This package could be user code if current package is specified.
package_kind = PackageKind::UserCode;
} else if let Some(alias) = compilation.dependencies.get(&package_id) {
// This is a direct dependency of the user code.
package_kind = PackageKind::AliasedPackage(alias.to_string());
} else {
// This is not a package user can access (an indirect dependency).
let Some(package_kind) = determine_package_kind(package_id, compilation) else {
continue;
}
};

let is_current_package = compilation.current_package_id == Some(package_id);
let package = &unit.package;

for (_, item) in &package.items {
if let Some((ns, metadata)) = generate_doc_for_item(
package_id,
Expand All @@ -324,13 +325,31 @@ pub fn generate_docs(
is_current_package,
item,
display,
&mut files,
files.as_deref_mut().unwrap_or(&mut vec![]),
) {
toc.entry(ns).or_default().push(metadata);
}
}
}

toc
}

/// Generates and returns documentation files for the standard library
/// and additional sources (if specified.)
#[must_use]
pub fn generate_docs(
additional_sources: Option<(PackageStore, &Dependencies, SourceMap)>,
capabilities: Option<TargetCapabilityFlags>,
language_features: Option<LanguageFeatures>,
) -> Files {
// Capabilities should default to all capabilities for documentation generation.
let capabilities = Some(capabilities.unwrap_or(TargetCapabilityFlags::all()));
let compilation = Compilation::new(additional_sources, capabilities, language_features);
let mut files: FilesWithMetadata = vec![];

let mut toc = build_toc_from_compilation(&compilation, Some(&mut files));

// Generate Overview files for each namespace
for (ns, items) in &mut toc {
generate_index_file(&mut files, ns, items);
Expand Down Expand Up @@ -709,3 +728,74 @@ fn get_metadata(
signature,
})
}

/// Generates summary documentation organized by namespace.
/// Returns a map of namespace -> metadata items for easier testing and manipulation.
fn generate_summaries_map() -> BTreeMap<String, Vec<Rc<Metadata>>> {
let compilation = Compilation::new(None, None, None);

// Use the shared logic to build ToC structure
let toc = build_toc_from_compilation(&compilation, None);

// Convert ToC to BTreeMap, filtering out table of contents entries
let mut result = BTreeMap::new();

for (ns, items) in toc {
let mut summaries = Vec::new();

for item in items {
// Skip table of contents entries
if item.kind == MetadataKind::TableOfContents {
continue;
}

summaries.push(item);
}

if !summaries.is_empty() {
// Sort items within namespace
summaries.sort_by_key(|item| item.name.clone());
result.insert(ns.to_string(), summaries);
}
}

result
}
/// Converts a Metadata item to its markdown representation
fn metadata_to_markdown(item: &Metadata) -> String {
let mut result = format!("## {}\n\n", item.name);
let _ = write!(result, "```qsharp\n{}\n```\n\n", item.signature);
if !item.summary.is_empty() {
let _ = write!(result, "{}\n\n", item.summary);
}
result
}

/// Generates markdown summary for a single namespace
fn generate_namespace_summary(namespace: &str, items: &[Rc<Metadata>]) -> String {
let mut result = format!("# {namespace}\n\n");

for item in items {
result.push_str(&metadata_to_markdown(item));
}

result
}

/// Generates summary documentation organized by namespace.
/// Returns a single markdown string with namespace headers and minimal item documentation
/// containing just function signatures and summaries for efficient consumption by language models.
#[must_use]
pub fn generate_summaries() -> String {
let summaries_map = generate_summaries_map();

// Generate markdown output organized by namespace
let mut result = String::new();

// Sort namespaces for consistent output (BTreeMap already sorts keys)
for (ns, items) in &summaries_map {
result.push_str(&generate_namespace_summary(ns, items));
}

result
}
106 changes: 105 additions & 1 deletion source/compiler/qsc_doc_gen/src/generate_docs/tests.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,31 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use super::generate_docs;
use crate::generate_docs::{generate_docs, generate_summaries_map, metadata_to_markdown};
use expect_test::expect;
use std::collections::BTreeMap;

/// Testing helper function that returns summaries as a structured map
/// for easier test validation. Returns a map where each namespace maps to
/// a vector of markdown strings, one per item.
pub fn generate_summaries_for_testing() -> BTreeMap<String, Vec<String>> {
let summaries_map = generate_summaries_map();

let mut result = BTreeMap::new();

for (ns, items) in summaries_map {
let mut item_markdowns = Vec::new();

for item in items {
let markdown = metadata_to_markdown(&item);
item_markdowns.push(markdown);
}

result.insert(ns, item_markdowns);
}

result
}

#[test]
fn generates_standard_item() {
Expand Down Expand Up @@ -207,3 +230,84 @@ fn top_index_file_generation() {
"#]]
.assert_eq(full_contents.as_str());
}

#[test]
fn generates_standard_item_summary() {
let summaries = generate_summaries_for_testing();
// Find a summary for a known item, e.g., Std.Core.Length
let core_summaries = summaries
.get("Std.Core")
.expect("Could not find Std.Core namespace");
let length_summary = core_summaries
.iter()
.find(|item| item.contains("## Length"))
.expect("Could not find summary for Length");

expect![[r#"
## Length

```qsharp
function Length<'T>(a : 'T[]) : Int
```

Returns the number of elements in the input array `a`.

"#]]
.assert_eq(length_summary);
}

#[test]
fn generates_std_core_summary() {
let summaries = generate_summaries_for_testing();
let core_summaries = summaries
.get("Std.Core")
.expect("Could not find Std.Core namespace");

// Combine all summaries for the namespace
let combined_summary = core_summaries.join("\n\n");

expect![[r#"
## Length

```qsharp
function Length<'T>(a : 'T[]) : Int
```

Returns the number of elements in the input array `a`.



## Repeated

```qsharp
function Repeated<'T>(value : 'T, length : Int) : 'T[]
```

Creates an array of given `length` with all elements equal to given `value`. `length` must be a non-negative integer.

"#]]
.assert_eq(&combined_summary);
}

#[test]
fn generates_summary_for_reexport() {
let summaries = generate_summaries_for_testing();
let length_summary = summaries
.get("Microsoft.Quantum.Core")
.expect("Could not find Microsoft.Quantum.Core namespace")
.iter()
.find(|item| item.contains("## Length"))
.expect("Could not find summary for Length");

expect![[r#"
## Length

```qsharp

```

This is an exported item. The actual definition is found here: [Std.Core.Length](xref:Qdk.Std.Core.Length)

"#]]
.assert_eq(length_summary);
}
7 changes: 7 additions & 0 deletions source/npm/qsharp/src/compiler/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ export interface ICompiler {

getDocumentation(additionalProgram?: ProgramConfig): Promise<IDocFile[]>;

getLibrarySummaries(): Promise<string>;

checkExerciseSolution(
userCode: string,
exerciseSources: string[],
Expand Down Expand Up @@ -249,6 +251,10 @@ export class Compiler implements ICompiler {
);
}

async getLibrarySummaries(): Promise<string> {
return this.wasm.get_library_summaries();
}

async checkExerciseSolution(
userCode: string,
exerciseSources: string[],
Expand Down Expand Up @@ -348,6 +354,7 @@ export const compilerProtocol: ServiceProtocol<ICompiler, QscEventData> = {
getEstimates: "request",
getCircuit: "request",
getDocumentation: "request",
getLibrarySummaries: "request",
run: "requestWithProgress",
runWithNoise: "requestWithProgress",
checkExerciseSolution: "requestWithProgress",
Expand Down
26 changes: 26 additions & 0 deletions source/npm/qsharp/test/basics.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,32 @@ test("autogenerated documentation", async () => {
);
});

test("library summaries slim docs", async () => {
const compiler = getCompiler();
const summaries = await compiler.getLibrarySummaries();
assert(typeof summaries === "string", "Summaries should be a string");
assert(summaries.length > 0, "Summaries should not be empty");

// Check that it contains namespace headers (markdown format)
assert(
summaries.includes("# Microsoft.Quantum"),
"Should contain standard library namespaces",
);

// Check that it contains function signatures in code blocks
assert(summaries.includes("```qsharp"), "Should contain Q# code blocks");
assert(summaries.includes("## "), "Should contain function headers");

// Check that it's organized by namespace
const lines = summaries.split("\n");
const namespaceHeaders = lines.filter((line) => line.startsWith("# "));
assert(namespaceHeaders.length > 0, "Should have namespace headers");

console.log(
`Generated ${summaries.length} characters of summaries with ${namespaceHeaders.length} namespaces`,
);
});

test("basic eval", async () => {
let code = `namespace Test {
function Answer() : Int {
Expand Down
Loading
Loading