Module-Scoped configured_values in DSLX
Document contact: allight
Date: 2026-09-22
Relevant issues: #4995
- Module-Scoped configured_values in DSLX
- Overview
- Background & Problem Statement
- Alternatives Considered
- Detailed Design: Proposal 2 (Target/Module-Scoped configured_values)
- Detailed Design: Unused configured_value Warning (unused_configured_value)
- Summary of Files Modified
- Testing & Rollout Plan
Overview
DSLX provides the built-in function configured_value_or<T>("key", default) to
allow build-time configuration overrides of constants in hardware designs and
tests. Currently, configured_values can only be specified on terminal build
targets (xls_dslx_ir, xls_dslx_opt_ir, and xls_dslx_test) and are applied
exclusively to the top-level entry Module during parsing and typechecking. Any
configured_value_or calls located inside imported xls_dslx_library modules
silently ignore the configured overrides and fall back to their compile
defaults.
This document describes Proposal 2 (Target/Module-Scoped
configured_values) from #4995,
which introduces target-scoped configured_values on xls_dslx_library
targets, propagates module-scoped overrides through DslxInfo providers and CLI
flags (key@mod_a+mod_b:value), and adds a default-enabled compiler warning
(unused_configured_value) to catch misspelled or unreferenced configuration
keys across single- and multi-module scopes.
Background & Problem Statement
How configured_value_or Works Today
The DSLX built-in function has the signature:
fn configured_value_or<T: type, N: u32>(label: u8[N], default_value: T) -> T;
During constant collection and type deduction (in
NoteBuiltinInvocationConstExpr within xls/dslx/type_system/deduce_utils.cc),
the compiler inspects invocation->owner()->configured_values(), which is an
absl::flat_hash_map<std::string, std::string> stored on the enclosing AST
Module (xls/dslx/frontend/module.h):
-
If
keyis present ininvocation->owner()->configured_values(), the compiler parses the associated string override into anInterpValueof the explicit parametric typeTviaGetConfiguredValueAsInterpValueand records it inTypeInfo::NoteConstExpr. -
If
keyis absent frominvocation->owner()->configured_values(), the compiler evaluatesdefault_valueand binds the result toinvocation.
During bytecode emission (xls/dslx/bytecode/bytecode_emitter.cc) and IR
conversion (xls/dslx/ir_convert/function_converter.cc), the resolved constant
expression is emitted directly from TypeInfo.
Why Imported Library Modules Silently Fall Back to Defaults
Today, configured_values is only wired to the top-level entry module:
- Build Rules: Only
xls_dslx_ir,xls_dslx_opt_ir, andxls_dslx_testaccept aconfigured_valuesdictionary attribute.xls_dslx_librarydoes not acceptconfigured_values, andDslxInfo(xls/build_rules/xls_providers.bzl) does not carry any configuration metadata to downstream dependents.
- Top-Level Entry Point:
ParseAndTypecheck(xls/dslx/parse_and_typecheck.cc) andConvertFilesToPackage(xls/dslx/ir_convert/ir_converter.cc) callmodule->SetConfiguredValues(options.configured_values)solely on the entryModuleparsed from the primary input file.
- Imported Modules (
DoImport): When the entry module imports a dependency (import foo.bar;),DoImportandDslxPathToModuleInfo(xls/dslx/import_routines.cc) instantiate a freshModuleobject and invokeftypecheckwithout populatingmodule->SetConfiguredValues(...). BecauseImportData(xls/dslx/import_data.h) does not store or dispatch configured values to imported modules,invocation->owner()->configured_values()is always empty for every imported module.
Consequently, when a hardware designer extracts shared constants or
parameterized IP blocks into an xls_dslx_library, all configured_value_or
calls inside that library silently evaluate to default_value during both
xls_dslx_test execution and xls_dslx_ir synthesis.
Alternatives Considered
Proposal 1: Global Propagation via ImportData (Rejected)
An initial prototype (PR #4985)
stored a single flat list of configured_values on ImportData and copied the
entire map into every Module instantiated inside DslxPathToModuleInfo.
While simple to implement, Proposal 1 was rejected for the following reasons:
- Implicit Global Blast Radius: Any key specified on a top-level
xls_dslx_irorxls_dslx_testtarget globally mutates every transitive dependency in the import DAG that happens to query the same string key.
- Key Collisions Across Transitive Dependencies: Two unrelated libraries
in a large design (for example,
pcie_phyandnoc_router) cannot both use a common key name like"fifo_depth"or"use_fast_mode"without a single top-level override unintentionally overwriting both modules simultaneously.
- Action-at-a-Distance & Fragile Caching: Library authors have no
visibility in
BUILDfiles into which libraries are meant to be parameterized byconfigured_values, andxls_dslx_librarytargets cannot validate or encapsulate their own configuration parameters during standaloneparse_and_typecheck_dslx_mainactions.
Proposal 3: Source-Level Parameterized Module Imports (Deferred)
Proposal 3 envisions first-class DSLX language syntax for parameterizing modules
at the import statement site (similar to SystemVerilog parameterized
interfaces or ML functors):
import foo.bar with { FIFO_DEPTH: u32:16 };
While source-level module functors are expressive and enable multiple
instantiations of the same module with distinct parameters within a single
translation unit, they require substantial changes to the DSLX grammar, AST,
ImportTokens cache keys in ImportData, and IR name mangling. Furthermore,
they do not directly solve the build-system requirement of injecting
synthesis-time overrides from Bazel BUILD targets without editing .x source
files. Proposal 2 solves the build-system configuration requirement cleanly
today while remaining orthogonal to future language-level module functors.
Detailed Design: Proposal 2 (Target/Module-Scoped configured_values)
Proposal 2 scopes configured_values to the xls_dslx_library (or terminal
target) where they are declared, propagates them transitively via DslxInfo,
and binds each key exclusively to the DSLX module(s) belonging to that target's
scope.
1. Build Rules & Starlark Propagation
xls_dslx_library Attribute and Typechecking
We add "configured_values": attr.string_dict(...) to _xls_dslx_library_attrs
in xls/build_rules/xls_dslx_rules.bzl:
_xls_dslx_library_attrs = {
"configured_values": attr.string_dict(
doc = "Dictionary of overrides to use for overridable constants " +
"in DSLX processing for the modules in this library. " +
"Format is \"key\":\"value\" pairs.",
),
...
}
When _xls_dslx_library_impl invokes parse_and_typecheck_dslx_main
(_xls_dslx_parse_and_typecheck_tool) on each file in ctx.files.srcs, it
passes the merged --configured_values flag containing both the library's own
scoped entries and those inherited from ctx.attr.deps. This guarantees that
library-level typechecking validates the configured override strings (including
type compatibility and enum resolution) at the library build step.
DslxInfo Provider Extension
We extend DslxInfo in xls/build_rules/xls_providers.bzl with a new
configured_values depset field:
DslxInfo = provider(
doc = "...",
fields = {
"configured_values": "Depset: A depset of scoped configured_value " +
"strings ('key@mod_a+mod_b:value') for this " +
"target and its transitive xls_dslx_library " +
"dependencies.",
"dslx_placeholder_files": "...",
"dslx_source_files": "...",
"target_dslx_source_files": "...",
},
)
For an xls_dslx_library target with srcs = [f_1, ..., f_n] and
configured_values = {"k": "v"}, Starlark computes the normalized DSLX module
identifier mod_i for each file f_i in srcs and formats each entry as:
k@mod_1+mod_2+...+mod_n:v
These formatted entries are placed in the direct list of a depset whose
transitive children are [dep[DslxInfo].configured_values for dep in
ctx.attr.deps].
Propagation to Consumer Rules
All downstream DSLX rules that consume xls_dslx_library targets via deps or
library (xls_dslx_library, xls_dslx_test, xls_dslx_ir, and
xls_dslx_opt_ir) collect the transitive DslxInfo.configured_values depset
alongside their own direct ctx.attr.configured_values attribute and pass the
combined list via --configured_values=... to parse_and_typecheck_dslx_main,
interpreter_main, and ir_converter_main.
2. CLI Flag Encoding
To maintain full backwards compatibility with existing CLI invocations while
supporting module-scoped and multi-file library rules, --configured_values
accepts comma-separated entries in two formats:
| Format | Scope | Semantics |
|---|---|---|
key:value |
Unscoped (Entry Module) | Applies only to the top-level entry module being compiled or tested. Preserves 100% backwards compatibility with existing CLI callers and direct configured_values attributes. |
key@mod_1+...+mod_n:value |
Module-Scoped | Applies to any module in the +-delimited scope set {mod_1, ..., mod_n}. Used by xls_dslx_library to scope a configuration override to the .x files in its srcs. |
Grouping a multi-file xls_dslx_library's modules into a single
key@mod_1+...+mod_n:value entry (rather than emitting separate
key@mod_i:value entries per file) is required for accurate
unused_configured_value warning semantics: when a library has srcs =
["mod_a.x", "mod_b.x"], often only one module (mod_a.x) calls
configured_value_or while mod_b.x is a helper or wrapper module in the same
target. Encoding the library's srcs as a single +-delimited group allows the
compiler to verify that at least one module in the library consumes key,
avoiding false-positive unused_configured_value warnings on mod_b.x.
Note that the separator between the scope specification and the value is the
first : after the key@scope prefix; the value itself may contain colons
(such as DSLX typed literals u32:42, s32:-100, or enum colon-refs
MyEnum::C). Specifically:
- We split on the first
:to separate<lhs>(keyorkey@mod_1+...+mod_n) from<rhs>(value, e.g.MyEnum::C).
- If
<lhs>contains@, we split<lhs>on@intokeyand the+-separated list of target module identifiersmod_1, ..., mod_n.
- If
<lhs>does not contain@,keyis bound to the unscoped entry-module configuration map.
3. Module Identifier Normalization
A critical subtlety in Bazel and monorepo builds is ensuring that the module
identifier emitted by Starlark in key@<module_id>:value matches the module
identifier known to C++ during both ParseAndTypecheck (when a file is the
top-level entry module) and DoImport (when a file is imported via import
...;).
How C++ Names Modules
-
When Imported (
DoImportinimport_routines.cc):*
ImportTokensholds the dotted components of theimportstatement (for example,xls.examples.fooorxls.examples.foo), andmodule->name()is set tosubject.ToString().*
dslx_path.source_pathholds the resolved relative path to the.xfile (for example,xls/examples/foo.x). -
When Compiled as the Entry Module (
ParseAndTypecheck):*
PathToName(entry_module_path)derivesmodule_namefrom the stem of the filename (for example,"foo"), whileentry_module_pathholds the relative path passed on the CLI (for example,xls/examples/foo.x).
Canonical Module Identifier Matching
To ensure consistent matching across Starlark, generated build outputs, external workspaces, and standalone CLI usage without any build-system-specific or repository-specific hardcoded path filters:
-
Starlark Normalization (
get_dslx_module_scope_for_file): In Starlark, for eachFilefinctx.files.srcs, we derive its search-path-relative dotted module identifier by takingf.path, stripping the file's search path roots (f.root.pathandf.owner.workspace_root), removing the.xsuffix, stripping leading./or/, and replacing/with.(yielding e.g.xls.examples.foo). All corresponding search roots (ctx.genfiles_dir.path,ctx.bin_dir.path,f.root.path, andf.owner.workspace_root) are passed via--dslx_path. -
C++ Search-Path-Based Matching (
ImportData::ModuleMatchesScope): In C++,ImportData::ModuleMatchesScope(std::string_view scope_id, std::string_view module_name, const std::filesystem::path& module_path)normalizesmodule_pathstrictly against the configured search paths:* Strips any matching search path prefix (
additional_search_paths(),stdlib_path(), or the VFS current working directory) frommodule_path.* Converts the search-path-relative path (with
.xremoved and/replaced by.) to a dotted module identifier.* Matches if the normalized
scope_idequals the normalizedmodule_name, equals the dottedmodule_path, or equals the search-path-relative dottedmodule_path.
4. Precedence, Diamond Deduplication, and Conflict Detection
In realistic build graphs, a library may be reached along multiple dependency
paths (diamond dependencies), or a terminal target (xls_dslx_test,
xls_dslx_ir) with library = ":my_lib" may wish to override a default
configured value set on :my_lib.
We enforce the following deterministic resolution rules:
-
Diamond Deduplication (Identical Values): Because
DslxInfo.configured_valuesuses a Bazeldepset, identical strings from diamond dependencies are automatically deduplicated in Starlark. In C++, if the same(scope, key)pair is provided multiple times with the exact same value, it is silently deduplicated. -
Conflict Detection Across Scoped Entries (Different Values): If two scoped entries assign different values to the same
keyfor an overlapping moduleM(for example,key@M:val1andkey@M:val2), parser initialization immediately fails with anabsl::InvalidArgumentErrorreporting the conflicting key, module, and values. -
Consumer Target Precedence Over
library = ...: When a consumer target (xls_dslx_test,xls_dslx_ir, orxls_dslx_opt_ir) specifieslibrary = ":my_lib"and also specifies its ownconfigured_values = {"key": "override_val"}:* If
"key"in the consumer'sconfigured_valuesis unscoped, it applies to the entry module(s) being tested or converted and takes precedence over (overrides) any@-scoped value for"key"inherited fromlibrary[DslxInfo].configured_valueson that entry module.* Furthermore, overriding a library's
@-scoped key via an unscoped entry on the entry module marks both the unscoped entry and the library's scoped group as used so that no false-positive unused warning is emitted.* If the consumer explicitly specifies a scoped key
"key@other_mod": "override_val"inctx.attr.configured_values, Starlark filters out or supersedes any inheritedDslxInfo.configured_valuesentry for the same(key, scope)before constructing the CLI arguments.
Detailed Design: Unused configured_value Warning (unused_configured_value)
1. Motivation
Because configured_values keys are arbitrary strings matched against string
literals in configured_value_or<T>("key", default), a typo in a BUILD file
or CLI flag (for example, configured_values = {"fifi_depth": "u32:16"} instead
of "fifo_depth") previously went completely unnoticed: the module silently
fell back to default and synthesized hardware with the wrong parameter.
Similarly, refactoring DSLX code to remove a configured_value_or call left
dead configured_values entries in BUILD files.
To prevent silent misconfigurations, we introduce a new compiler warning,
unused_configured_value, enabled by default in kDefaultWarningsSet.
2. WarningKind::kUnusedConfiguredValue and Bitset Widening
In xls/dslx/warning_kind.h:
WarningKindIntis currentlyuint16_twithkWarningKindCount = 15(bits0..14). Adding a 16th warning (1 << 15) would overflow signed/16-bit shift expressions such as(WarningKindInt{1} << kWarningKindCount) - 1whenkWarningKindCount == 16.
- We widen
WarningKindIntfromuint16_ttouint32_t:using WarningKindInt = uint32_t; enum class WarningKind : WarningKindInt { ... kWidthSliceOutOfRange = 1 << 14, kUnusedConfiguredValue = 1 << 15, }; constexpr WarningKindInt kWarningKindCount = 16;
- We map
WarningKind::kUnusedConfiguredValueto the flag name"unused_configured_value"inxls/dslx/warning_kind.cc.
- Because
kDefaultWarningsSetis constructed fromkAllWarningsSetminus explicit opt-out warnings (kShouldUseAssertandkAlreadyExhaustiveMatch),WarningKind::kUnusedConfiguredValueis enabled by default (and treated as an error when--warnings_as_errors=true, which is the default in XLS tools and tests). Users can explicitly suppress it if needed via--disable_warnings=unused_configured_value.
3. Usage Tracking and Multi-File Scope Verification
Consider an xls_dslx_library with multiple source files:
xls_dslx_library(
name = "multi_file_lib",
srcs = [
"mod_a.x",
"mod_b.x",
],
configured_values = {
"shared_param": "u32:64",
},
)
Here, "shared_param" is encoded as shared_param@pkg.mod_a+pkg.mod_b:u32:64.
Frequently, only mod_a.x calls configured_value_or<u32>("shared_param",
...), while mod_b.x is a helper or test module in the same library that
imports mod_a.
If we naively warned whenever any single module in {mod_a, mod_b} failed to
call configured_value_or("shared_param", ...), then compiling or testing
mod_b.x would trigger a false-positive unused_configured_value warning if
mod_b.x did not touch mod_a, or if each module were checked in isolation!
Conversely, if neither mod_a.x nor mod_b.x calls
configured_value_or("shared_param", ...), we must emit a warning.
We solve this across both C++ and Starlark as follows:
Data Structures in ImportData and Module
-
ConfiguredValueGroupinImportData:ImportDatastores the parsed configuration rules as a list ofConfiguredValueGrouprecords:struct ConfiguredValueGroup { std::string key; std::string value; // Empty vector indicates an unscoped entry (applies to the entry module). // Non-empty vector indicates a '+'-delimited module scope set. std::vector<std::string> scope_modules; bool used = false; }; -
Populating Each
Module: When the entry module is initialized inParseAndTypecheck(orConvertFilesToPackage) and when any imported module is initialized inDslxPathToModuleInfo(import_routines.cc),ImportDataresolves all applicable(key, value)pairs for that module (applying entry-module precedence over scoped entries), records the(module_name, key)mapping for usage tracking, and populatesmodule->SetConfiguredValuesMap(...). -
Recording Key Usage During Pre-Typecheck Analysis and Type Deduction:
* Pre-Typecheck Analysis (Uninstantiated Parametrics): A library module may define
pub fn p<N: u32>() -> u32 { configured_value_or<u32>("key", 0) }or a parametric proc/impl without instantiatingpinside the library itself. BecauseNoteBuiltinInvocationConstExpronly executes when a function body is concretely instantiated, relying solely on type deduction would emit a false-positiveunused_configured_valuewarning when building thexls_dslx_librarytarget. Therefore, duringSemanticsAnalysis::RunPreTypeCheckPass(semantics_analysis.cc), the compiler walksInvocationAST nodes in the module whose callee isconfigured_value_orand extracts string literal keys to mark them as used onImportData.* Type Deduction (
NoteBuiltinInvocationConstExprindeduce_utils.cc): Wheneverconfigured_value_orlooks upkeyininvocation->owner(),import_data->NoteConfiguredValueUsed(...)marks any matchingConfiguredValueGroup(both unscoped entry overrides and any@-scoped group whosescope_modulescontains the module) asused = true. -
Propagating
configured_valuesAcrossinterpreter_main.ccModes: Inxls/dslx/interpreter_main.cc,--configured_valuesmust be propagated not only toparse_and_typecheck_options.configured_values, but also tooptions.convert_options.configured_values(used when--compare=jitor--compare=interpreterconverts modules to IR for comparison) andir_convert_options.configured_values(used when--lower_to_irvalidates IR lowering). -
Verifying Scope Satisfaction After Typechecking: At the completion of top-level
ParseAndTypecheck:* Unscoped Entries (
scope_modules.empty()): Must haveused == true(i.e., used by the entry module). Ifused == false, aWarningKind::kUnusedConfiguredValuewarning is added to the entry module'sWarningCollectorat a synthetic file-startSpan(Pos(fileno, 0, 0)):``` Configured value `foo` (with value `u32:42`) was provided but not used in module `my_entry`. ```* Scoped Entries (
!scope_modules.empty()): A scoped groupkey@mod_1+...+mod_n:valueis considered active in a compilation unit if at least one module in{mod_1, ..., mod_n}was loaded during the compilation session (either as the entry module or viaImportData).* For single-module scopes (`n == 1`), if `mod_1` was loaded (as the entry module or via import) and `used == false`, a `WarningKind::kUnusedConfiguredValue` warning is emitted. * For multi-module scopes (`n > 1`), if **all** modules in `{mod_1, ..., mod_n}` were loaded in the session (or when `parse_and_typecheck_dslx_main` is invoked across all `srcs` of the `xls_dslx_library`) and `used == false` across the entire scope set, a `WarningKind::kUnusedConfiguredValue` warning is emitted: ``` Configured value `shared_param` (with value `u32:64`) scoped to `pkg.mod_a+pkg.mod_b` was not used by any module in its scope. ``` * In `_xls_dslx_library_impl`, `parse_and_typecheck_dslx_main` is passed all `srcs` of the library in a single invocation so that every file in `srcs` is parsed and typechecked into the shared `ImportData` session before checking unused scoped `configured_values`. This guarantees that a key declared on a multi-file `xls_dslx_library` warns if and only if **none** of the files in that library's `srcs` use the key.
Summary of Files Modified
| File Path | Summary of Changes |
|---|---|
xls/dslx/warning_kind.h |
Widen WarningKindInt to uint32_t, add WarningKind::kUnusedConfiguredValue = 1 << 15, increment kWarningKindCount to 16. |
xls/dslx/warning_kind.cc |
Register "unused_configured_value" string mapping in WarningKindToString and WarningKindFromString. |
xls/dslx/frontend/module.h |
Replace SetConfiguredValues with SetConfiguredValuesMap accepting the resolved per-module map from ImportData. |
xls/dslx/frontend/semantics_analysis.cc |
Record configured_value_or key usage during SemanticsAnalysis::RunPreTypeCheckPass (including inside uninstantiated parametrics). |
xls/dslx/import_data.h / .cc |
Store ConfiguredValueGroup records on ImportData, resolve per-module configured values with conflict detection and entry-module override precedence, track usage across scopes, and check unused values. |
xls/dslx/import_routines.cc |
Populate module->SetConfiguredValuesMap(...) in imported modules inside DslxPathToModuleInfo using import_data. |
xls/dslx/parse_and_typecheck.cc |
Register options.configured_values on import_data, populate entry module configured values, preserve configured values across TestFunctionTransformer re-parses, and emit WarningKind::kUnusedConfiguredValue diagnostics. |
xls/dslx/parse_and_typecheck_dslx_main.cc |
Add --configured_values flag and support typechecking multiple input files in a shared ImportData session for multi-file xls_dslx_library verification. |
xls/dslx/interpreter_main.cc |
Propagate configured_values to options.convert_options (for --compare=jit) and ir_convert_options (for --lower_to_ir). |
xls/dslx/type_system/deduce_utils.cc |
Call NoteConfiguredValueUsed on ImportData when configured_value_or references a key. |
xls/build_rules/xls_providers.bzl |
Add configured_values depset field to DslxInfo. |
xls/build_rules/xls_common_rules.bzl |
Add collect_dslx_configured_values helper to collect transitive @-scoped entries and direct overrides. |
xls/build_rules/xls_dslx_rules.bzl |
Add configured_values attribute to xls_dslx_library, compute module-scoped key@mod_a+mod_b:value entries via _format_scoped_configured_values, propagate DslxInfo.configured_values, and pass --configured_values to parse_and_typecheck_dslx_main. |
xls/build_rules/xls_dslx_test.bzl |
Collect transitive DslxInfo.configured_values from library / deps / dep, merge with direct ctx.attr.configured_values, and pass to interpreter_main. |
xls/build_rules/xls_ir_rules.bzl |
Collect transitive DslxInfo.configured_values in _convert_to_ir, merge with direct ctx.attr.configured_values, and pass to ir_converter_main. |
Testing & Rollout Plan
-
Unit Tests (C++ Type System, Bytecode, & IR Converter):
* Verify unscoped
key:valueapplies only to the entry module and does not leak into imported modules (typecheck_module_test.cc,ir_converter_test.cc).* Verify scoped
key@import.path:valueandkey@mod_a+mod_b:valueapply to matching imported modules and do not affect unrelated modules.* Verify conflict detection raises an
INVALID_ARGUMENTerror when two scoped rules specify different values for the same key and module.* Verify unscoped entry-module override takes precedence over an
@-scoped rule targeting the entry module.* Verify
WarningKind::kUnusedConfiguredValuetriggers for unused unscoped and unused scoped keys, does not trigger when at least one module in amod_a+mod_bscope consumes the key, and can be suppressed via--disable_warnings=unused_configured_value. -
Build Rule Integration Tests (
xls/examples/&build_rules/tests/):* Add
xls_dslx_librarytargets withconfigured_values(both single-file and multi-filesrcs).* Add
xls_dslx_test,xls_dslx_ir, andxls_dslx_opt_irtargets that depend on configuredxls_dslx_librarytargets (including diamond dependencies and consumer overrides vialibrary = ...).