Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## Unreleased
### Added
- script: expose `all_headers` to templates — the fine-grained, transitive, deduplicated set of header files Slang resolved via `` `include `` for the kept trees. Populated whenever the Slang pass runs (`--top`/`--trim-incdirs`/`--broken`/`--encrypted`).

## 0.32.1 - 2026-07-07
### Added
Expand Down
53 changes: 40 additions & 13 deletions src/cmd/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ pub fn run(sess: &Session, args: &ScriptArgs) -> Result<()> {
// unused include directories (per `--trim-incdirs`), with per-class policies for files
// slang couldn't fully parse (`--broken`, `--encrypted`).
#[cfg(feature = "slang")]
let (srcs, unparseable_paths) = {
let (srcs, unparseable_paths, resolved_headers) = {
let trim_incdirs = match args.trim_incdirs {
TrimIncdirs::Always => true,
TrimIncdirs::Never => false,
Expand All @@ -400,7 +400,11 @@ pub fn run(sess: &Session, args: &ScriptArgs) -> Result<()> {
Some(ParsePolicy::Error) | Some(ParsePolicy::Drop)
);
if args.top.is_empty() && !trim_incdirs && !policies_need_slang {
(srcs, std::collections::HashSet::<PathBuf>::new())
(
srcs,
std::collections::HashSet::<PathBuf>::new(),
Vec::new(),
)
} else {
apply_slang_filters(
srcs,
Expand All @@ -413,6 +417,8 @@ pub fn run(sess: &Session, args: &ScriptArgs) -> Result<()> {
};
#[cfg(not(feature = "slang"))]
let unparseable_paths = std::collections::HashSet::<PathBuf>::new();
#[cfg(not(feature = "slang"))]
let resolved_headers: Vec<PathBuf> = Vec::new();

let mut tera_context = Context::new();
let mut only_args = OnlyArgs {
Expand Down Expand Up @@ -503,6 +509,7 @@ pub fn run(sess: &Session, args: &ScriptArgs) -> Result<()> {
only_args,
srcs,
&unparseable_paths,
&resolved_headers,
)
}

Expand Down Expand Up @@ -556,14 +563,23 @@ where
///
/// Returns the filtered groups plus the set of unparseable file paths that survived filtering,
/// so the caller can annotate them in `source_annotations` output.
///
/// Also returns the fine-grained set of header files slang actually resolved via `` `include ``
/// (transitively) while parsing the kept trees, so the caller can expose them to templates as
/// `all_headers`. This is the same list used for `--trim-incdirs`, but kept in full (files, not
/// just their directories).
#[cfg(feature = "slang")]
fn apply_slang_filters<'a>(
srcs: Vec<SourceGroup<'a>>,
top: &[String],
trim_incdirs: bool,
broken_policy: ParsePolicy,
encrypted_policy: ParsePolicy,
) -> Result<(Vec<SourceGroup<'a>>, std::collections::HashSet<PathBuf>)> {
) -> Result<(
Vec<SourceGroup<'a>>,
std::collections::HashSet<PathBuf>,
Vec<PathBuf>,
)> {
use std::collections::HashSet;

let mut session = SlangSession::new();
Expand Down Expand Up @@ -674,18 +690,19 @@ fn apply_slang_filters<'a>(
HashSet::new()
};

// The header files slang actually resolved via `include (transitively, deduped, canonical)
// while parsing the kept trees. Used both for strict include-dir trimming below and returned
// to the caller to expose as `all_headers`. Computed unconditionally: the cost is a cheap walk
// over already-parsed trees, and both consumers want it.
let resolved_includes: Vec<PathBuf> = session
.resolved_include_paths(&kept_trees)
.into_iter()
.map(PathBuf::from)
.collect();

// Strict include-dir trimming: a directory survives only if slang actually resolved at least
// one `include directive through it. Canonicalize both sides so symlinks / `.` / `..` don't
// cause spurious mismatches.
let resolved_includes: Vec<PathBuf> = if trim_incdirs {
session
.resolved_include_paths(&kept_trees)
.into_iter()
.map(PathBuf::from)
.collect()
} else {
Vec::new()
};
let dir_is_used = |dir: &Path| -> bool {
let canon = canonicalize(dir).unwrap_or_else(|_| dir.to_path_buf());
resolved_includes.iter().any(|f| f.starts_with(&canon))
Expand Down Expand Up @@ -759,7 +776,7 @@ fn apply_slang_filters<'a>(
class_summary("encrypted", &encrypted_paths, encrypted_policy);
class_summary("broken", &broken_paths, broken_policy);

Ok((filtered, kept_unparseable))
Ok((filtered, kept_unparseable, resolved_includes))
}

static HEADER_AUTOGEN: &str = "This script was generated automatically by bender.";
Expand All @@ -783,6 +800,7 @@ fn emit_template(
only: OnlyArgs,
srcs: Vec<SourceGroup>,
unparseable_paths: &std::collections::HashSet<PathBuf>,
resolved_headers: &[PathBuf],
) -> Result<()> {
// Helper for annotating FileEntry.comment on files that survived filtering despite slang
// failing to parse them; visible to users with `--source-annotations`.
Expand Down Expand Up @@ -852,6 +870,15 @@ fn emit_template(
};
tera_context.insert("all_incdirs", &all_incdirs);

// Fine-grained header files slang resolved via `include (transitively) for the kept trees.
// Empty unless the slang pass ran (i.e. `--top`/`--trim-incdirs`/a parse policy triggered it).
// Exposed so `template`/`template-json` can emit dependency lists (e.g. a Makefile `.d`) that
// track header edits precisely, without listing whole include dirs. Sorted+deduped; paths are
// absolute, consistent with `all_files`/`all_incdirs` (relativize via `root` in templates).
let mut all_headers: IndexSet<PathBuf> = resolved_headers.iter().cloned().collect();
all_headers.sort();
tera_context.insert("all_headers", &all_headers);

// replace files in all_files with override files
let override_map = all_override_files
.iter()
Expand Down
54 changes: 54 additions & 0 deletions tests/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,4 +435,58 @@ mod tests {
);
}
}

/// `all_headers` exposes the fine-grained set of headers slang actually resolved via `include
/// (see `resolved_include_paths`), so templates can build dependency lists that track header
/// edits. `top.sv` does `` `include "macros.svh" ``; `include_unused/unused_header.svh` is
/// never included. `macros.svh` only ever appears through `all_headers` (it is not a compiled
/// source), so a plain substring check is unambiguous.
#[test]
fn script_all_headers_lists_resolved_headers() {
let out = run_script(&["--target", "top", "--top", "top", "template-json"]);
assert!(
out.contains("\"all_headers\""),
"template-json must expose an all_headers key:\n{out}"
);
assert!(
out.contains("macros.svh"),
"all_headers should list the resolved header macros.svh:\n{out}"
);
assert!(
!out.contains("unused_header.svh"),
"all_headers must not list a header that was never `included:\n{out}"
);
}

/// Without a slang trigger (`--top`/`--trim-incdirs`/parse policy) the pass does not run, so
/// no includes are resolved and `all_headers` is empty.
#[test]
fn script_all_headers_empty_without_slang_pass() {
let out = run_script(&["--target", "top", "template-json"]);
assert!(
out.contains("\"all_headers\""),
"all_headers key must exist even when empty:\n{out}"
);
assert!(
!out.contains("macros.svh"),
"all_headers must be empty when the slang pass does not run:\n{out}"
);
}

/// `all_headers` does not require `--top`: any slang trigger populates it. `--trim-incdirs
/// always` runs the pass over all trees, so the resolved header is reported.
#[test]
fn script_all_headers_populated_without_top_when_slang_runs() {
let out = run_script(&[
"--target",
"top",
"--trim-incdirs",
"always",
"template-json",
]);
assert!(
out.contains("macros.svh"),
"all_headers should be populated whenever the slang pass runs:\n{out}"
);
}
}
Loading