Skip to content

Commit 41c16a2

Browse files
committed
Manage stdlib in toolchain
1 parent d9b301c commit 41c16a2

22 files changed

+321
-385
lines changed

crates/pcb-docgen/README.md

Lines changed: 10 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,123 +1,28 @@
1-
# stdlib-docgen
1+
# pcb-docgen
22

3-
Automated documentation generator for the Zener standard library (`@stdlib`).
3+
Generates stdlib docs (`stdlib.mdx`) from `.zen` files.
44

5-
## Overview
5+
## Notes
66

7-
This crate parses `.zen` files from the stdlib directory and generates a single `stdlib.mdx`
8-
documentation file that is used by the `pcb doc` command.
7+
- Input stdlib path is typically `<workspace>/.pcb/stdlib`.
8+
- Output is deterministic and used by `pcb doc`.
99

10-
**Important**: The stdlib version is re-exported from `pcb-zen-core::STDLIB_VERSION`, so it
11-
stays automatically in sync when the stdlib version is bumped.
12-
13-
## How it works
14-
15-
1. **File Discovery**: Walks the cached stdlib at `~/.pcb/cache/github.com/diodeinc/stdlib/<version>`,
16-
excluding `test/` and `kicad/` directories
17-
2. **Classification**: Distinguishes between library files (functions/types) and module files (instantiable components)
18-
3. **Library Parsing**: Extracts docstrings, functions, types, and constants using regex-based parsing
19-
4. **Module Signatures**: Runs `pcb build <file.zen> --netlist` to get the `__signature` JSON
20-
5. **MDX Generation**: Produces deterministic `docs/pages/stdlib.mdx`
21-
22-
## Usage
23-
24-
### CLI
10+
## CLI
2511

2612
```bash
27-
# Generate stdlib docs using cached stdlib (default)
28-
cargo run -p stdlib-docgen
29-
30-
# Generate with custom paths (for development/testing)
31-
cargo run -p stdlib-docgen -- <stdlib_path> <docs_dir> <pcb_cli_path>
13+
cargo run -p pcb-docgen
14+
cargo run -p pcb-docgen -- <stdlib_path> <docs_dir> <pcb_cli_path>
3215
```
3316

34-
The tool will use the stdlib from `~/.pcb/cache/github.com/diodeinc/stdlib/<version>` by default.
35-
If the cache is empty, run `pcb build` on any project first to populate it.
36-
37-
### As a library
17+
## Library
3818

3919
```rust
40-
use stdlib_docgen::generate_stdlib_mdx;
20+
use pcb_docgen::generate_stdlib_mdx;
4121
use std::path::Path;
4222

4323
let result = generate_stdlib_mdx(
4424
Path::new("../stdlib"),
4525
Path::new("docs/pages"),
4626
Path::new("target/debug/pcb"),
4727
)?;
48-
49-
println!("Generated {} libraries and {} modules",
50-
result.library_count, result.module_count);
51-
```
52-
53-
## Docstring Conventions
54-
55-
### File-level docstrings
56-
57-
Put a triple-quoted docstring at the top of the file (after `load` statements):
58-
59-
```python
60-
"""Short summary of the module.
61-
62-
Longer description with examples if needed.
63-
"""
64-
65-
load("@stdlib/units.zen", "Voltage")
66-
```
67-
68-
### Function docstrings
69-
70-
Put a triple-quoted docstring as the first statement in the function body:
71-
72-
```python
73-
def my_function(arg1, arg2):
74-
"""Short summary of what the function does.
75-
76-
Args:
77-
arg1: Description of arg1
78-
arg2: Description of arg2
79-
80-
Returns:
81-
Description of return value
82-
"""
83-
...
84-
```
85-
86-
### Module files
87-
88-
Module files are detected by the presence of `config()` or `io()` declarations.
89-
Their signatures are automatically extracted from `pcb build --netlist` output.
90-
91-
Add a file-level docstring to describe the component's purpose:
92-
93-
```python
94-
"""
95-
Pin Header Connector Component
96-
97-
A configurable pin header component that supports:
98-
- Single and dual row configurations
99-
- Multiple pitch options
100-
- Various mount types
101-
102-
Example usage:
103-
PinHeader(name="J1", pins=4, P1=vcc, P2=gnd)
104-
"""
105-
106-
package = config("package", Package, default=Package("0603"))
107-
P1 = io("P1", Net)
108-
```
109-
110-
## Integration with build.rs
111-
112-
To regenerate stdlib docs during `cargo build`, add to `pcb-docs/build.rs`:
113-
114-
```rust
115-
use stdlib_docgen::generate_stdlib_mdx;
116-
117-
// In main():
118-
stdlib_docgen::generate_stdlib_mdx(
119-
&stdlib_root,
120-
&docs_dir,
121-
&pcb_cli,
122-
)?;
12328
```

crates/pcb-docgen/src/render.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ pub fn render_docs(
2121

2222
// Add package URL as h1 header if provided
2323
if let Some(url) = package_url {
24-
// Special case: display github.com/diodeinc/stdlib as @stdlib
25-
let display_url = if url == "github.com/diodeinc/stdlib" {
24+
// Special case: display virtual stdlib package as @stdlib
25+
let display_url = if pcb_zen_core::is_stdlib_module_path(url) {
2626
"@stdlib"
2727
} else {
2828
url
@@ -300,7 +300,7 @@ mod tests {
300300
#[test]
301301
fn test_render_docs_stdlib_alias() {
302302
let files = vec![];
303-
let output = render_docs(&files, Some("github.com/diodeinc/stdlib"), None);
303+
let output = render_docs(&files, Some(pcb_zen_core::STDLIB_MODULE_PATH), None);
304304
assert!(output.contains("# @stdlib\n"));
305305
}
306306
}

crates/pcb-test-utils/src/sandbox.rs

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -532,13 +532,12 @@ impl Sandbox {
532532
.replace_all(&result, r#""net_id": Number(<ID>)"#)
533533
.to_string();
534534

535-
// Sanitize stdlib version in both paths and package IDs:
536-
// - stdlib/0.5.1 -> stdlib/<STDLIB_VERSION>
537-
// - stdlib@0.5.1 -> stdlib@<STDLIB_VERSION>
535+
// Normalize legacy versioned stdlib paths:
536+
// - stdlib/0.5.1 -> stdlib
538537
let stdlib_version_pattern =
539538
Regex::new(r"stdlib([/@])\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?").unwrap();
540539
result = stdlib_version_pattern
541-
.replace_all(&result, "stdlib$1<STDLIB_VERSION>")
540+
.replace_all(&result, "stdlib")
542541
.to_string();
543542

544543
result
@@ -677,16 +676,13 @@ impl Sandbox {
677676
std::os::windows::fs::symlink_dir(&global_dir, &sandbox_dir).unwrap();
678677
}
679678

680-
/// Seed stdlib and common asset dependency repos for dep resolution tests.
679+
/// Seed common asset dependency repos for dep resolution tests.
681680
///
682-
/// Uses the global cache if present; otherwise fetches via network and caches locally.
681+
/// Embedded stdlib is materialized per-workspace by the toolchain, so only external
682+
/// asset repos need seeding here.
683683
pub fn seed_stdlib(&mut self) -> &mut Self {
684-
let stdlib_version = pcb_zen_core::STDLIB_VERSION;
685684
let kicad_version = "9.0.3";
686685

687-
// cache (~/.pcb/cache) - seed stdlib + common asset deps
688-
self.seed_cache_repo("github.com/diodeinc/stdlib", stdlib_version, true);
689-
690686
for repo in [
691687
"gitlab.com/kicad/libraries/kicad-symbols",
692688
"gitlab.com/kicad/libraries/kicad-footprints",
@@ -1151,23 +1147,19 @@ mod tests {
11511147
}
11521148

11531149
#[test]
1154-
fn test_sanitize_stdlib_version_in_path_and_package_id() {
1150+
fn test_sanitize_stdlib_version_in_path() {
11551151
let sb = Sandbox::new();
11561152
let input = r#"{
11571153
"package_roots": {
1158-
"github.com/diodeinc/stdlib@0.5.8": "/tmp/vendor/github.com/diodeinc/stdlib/0.5.8",
1159-
"github.com/diodeinc/stdlib@0.5.8-beta.1": "/tmp/vendor/github.com/diodeinc/stdlib/0.5.8-beta.1"
1154+
"stdlib": "/tmp/.pcb/cache/stdlib/0.5.8",
1155+
"workspace": "/tmp/workspace"
11601156
}
11611157
}"#;
11621158

11631159
let output = sb.sanitize_output(input);
11641160

1165-
assert!(output.contains("github.com/diodeinc/stdlib@<STDLIB_VERSION>"));
1166-
assert!(output.contains("/tmp/vendor/github.com/diodeinc/stdlib/<STDLIB_VERSION>"));
1167-
assert!(!output.contains("stdlib@0.5.8"));
1161+
assert!(output.contains("\"stdlib\": \"/tmp/.pcb/cache/stdlib\""));
11681162
assert!(!output.contains("stdlib/0.5.8"));
1169-
assert!(!output.contains("stdlib@0.5.8-beta.1"));
1170-
assert!(!output.contains("stdlib/0.5.8-beta.1"));
11711163
}
11721164

11731165
#[test]

crates/pcb-zen-core/src/lang/eval.rs

Lines changed: 37 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -505,18 +505,9 @@ impl EvalContextConfig {
505505
/// Expand alias using the resolution map.
506506
fn expand_alias(&self, context: &ResolveContext, alias: &str) -> Result<String, anyhow::Error> {
507507
let package_root = self.find_package_root_for_file(&context.current_file)?;
508-
let resolved_map = self
509-
.resolution
510-
.package_resolutions
511-
.get(&package_root)
512-
.ok_or_else(|| {
513-
anyhow::anyhow!(
514-
"Dependency map not loaded for package '{}'",
515-
package_root.display()
516-
)
517-
})?;
518-
519-
if let Some(url) = Self::find_alias_in_map(resolved_map, alias) {
508+
if let Some(resolved_map) = self.resolution.package_resolutions.get(&package_root)
509+
&& let Some(url) = Self::find_alias_in_map(resolved_map, alias)
510+
{
520511
return Ok(url);
521512
}
522513

@@ -529,24 +520,31 @@ impl EvalContextConfig {
529520
context: &ResolveContext,
530521
package_root: &Path,
531522
) -> Result<PathBuf, anyhow::Error> {
532-
let spec = context.latest_spec();
533-
let full_url = spec
534-
.to_full_url()
535-
.expect("try_resolve_workspace called with non-URL spec");
536-
537-
let resolved_map = self
538-
.resolution
539-
.package_resolutions
540-
.get(package_root)
541-
.ok_or_else(|| {
542-
anyhow::anyhow!(
523+
let full_url = if let LoadSpec::Stdlib { path } = context.latest_spec() {
524+
let stdlib_root = self.resolution.workspace_info.workspace_stdlib_dir();
525+
return Ok(if path.as_os_str().is_empty() {
526+
stdlib_root
527+
} else {
528+
stdlib_root.join(path)
529+
});
530+
} else {
531+
context
532+
.latest_spec()
533+
.to_full_url()
534+
.expect("try_resolve_workspace called with non-URL spec")
535+
};
536+
537+
let resolved_map = self.resolution.package_resolutions.get(package_root);
538+
let best_match = resolved_map.and_then(|map| Self::find_best_dep_match(map, &full_url));
539+
540+
let Some((matched_dep, root_path)) = best_match else {
541+
if resolved_map.is_none() {
542+
anyhow::bail!(
543543
"Dependency map not loaded for package '{}'",
544544
package_root.display()
545-
)
546-
})?;
547-
let best_match = Self::find_best_dep_match(resolved_map, &full_url);
545+
);
546+
}
548547

549-
let Some((matched_dep, root_path)) = best_match else {
550548
anyhow::bail!(
551549
"No declared dependency matches '{}'\n \
552550
Add a dependency to [dependencies] in pcb.toml that covers this path",
@@ -584,22 +582,13 @@ impl EvalContextConfig {
584582
}
585583

586584
/// Find the package URL for a given canonical package root path by scanning
587-
/// workspace members and resolution maps.
585+
/// known package roots.
588586
// TODO: if this becomes a bottleneck, pre-build a reverse map (PathBuf -> URL) at init time.
589587
fn find_url_for_package_root(&self, canonical_root: &Path) -> Option<String> {
590-
let ws = &self.resolution.workspace_info;
591-
for (url, member) in &ws.packages {
592-
let dir = member.dir(&ws.root);
593-
let canon = self.file_provider.canonicalize(&dir).unwrap_or(dir);
594-
if canon == canonical_root {
595-
return Some(url.clone());
596-
}
597-
}
598-
for resolved_map in self.resolution.package_resolutions.values() {
599-
for (dep_url, dep_path) in resolved_map {
600-
if dep_path == canonical_root {
601-
return Some(dep_url.clone());
602-
}
588+
for (url, root) in self.resolution.package_roots() {
589+
let canonical = self.file_provider.canonicalize(&root).unwrap_or(root);
590+
if canonical == canonical_root {
591+
return Some(url);
603592
}
604593
}
605594
None
@@ -694,22 +683,18 @@ impl EvalContextConfig {
694683
// Expand aliases
695684
if let LoadSpec::Package { package, path, .. } = context.latest_spec() {
696685
let expanded_url = self.expand_alias(context, package)?;
697-
let full_url = if path.as_os_str().is_empty() {
698-
expanded_url
699-
} else {
700-
format!("{}/{}", expanded_url, path.display())
686+
let expanded_spec = LoadSpec::Package {
687+
package: expanded_url,
688+
path: path.clone(),
701689
};
702-
703-
let expanded_spec = LoadSpec::parse(&full_url)
704-
.ok_or_else(|| anyhow::anyhow!("Failed to parse expanded alias: {}", full_url))?;
705-
context.push_spec(expanded_spec)?;
690+
if &expanded_spec != context.latest_spec() {
691+
context.push_spec(expanded_spec)?;
692+
}
706693
}
707694

708695
let resolved_path = match context.latest_spec() {
709-
LoadSpec::Github { .. } | LoadSpec::Gitlab { .. } => self.resolve_url(context)?,
710696
LoadSpec::Path { .. } => self.resolve_relative(context)?,
711-
LoadSpec::Package { .. } => unreachable!("Package checked above"),
712-
LoadSpec::PackageUri { .. } => unreachable!("PackageUri resolved in resolve_context"),
697+
_ => self.resolve_url(context)?,
713698
};
714699

715700
if !context.file_provider.exists(&resolved_path)

0 commit comments

Comments
 (0)