google / google/xls

[DSLX] type inference v2: "No constexpr value found for node `T`" for a proc with an explicit `type` parametric

Open
#4,932 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
C++
Stars
1.9k
Forks
283
Avg merge
2d 10h
Merged PRs (30d)
135

Description

**Describe the bug**

Under type inference v2, IR conversion / typechecking fails with

```
NOT_FOUND: No constexpr value found for node `T` (NameDef) @ producer.x:5:19-5:20 in TypeInfo root
=== Source Location Trace: ===
xls/dslx/type_system/type_info.cc:297
xls/dslx/type_system_v2/constant_collector.cc:987
xls/dslx/type_system_v2/constant_collector.cc:1044
xls/dslx/type_system_v2/inference_table_converter_impl.cc:1320
```

when a `proc` has an explicitly-supplied `T: type` parametric and its
initializer (the impl-style `new`, i.e. a proc `StructInstance`) is built.
Value parametrics — and *defaulted* type parametrics — have a constexpr noted
on the binding, but an explicitly supplied `T: type` does not, so the
unconditional `GetConstExpr(binding->name_def())` in the constant collector
fails.

The failure only shows up in a specific (but common) shape: two parametric
procs in **separate modules** connected by a channel whose payload is a
parametric struct, where one proc is instantiated with `T` bound to that
struct type and the other reaches the same struct through its own `T: type`
parametric.

**To Reproduce**

Three files.

`producer.x`
```rust
#![feature(generics)]
#![feature(explicit_state_access)]

// A proc generic over the element type `T` it sends.
pub proc Producer {
sink: chan out,
state: uN[WIDTH],
}

impl Producer {
pub fn new(sink: chan out) -> Self {
Producer { sink, state: uN[WIDTH]:0 }
}

fn next(self) { }
}
```

`consumer.x`
```rust
#![feature(generics)]
#![feature(explicit_state_access)]

pub struct Request {
reg: T[DEGREE + 1],
}

impl Request {
fn default() -> Self { zero!>() }
}

pub proc Consumer {
request: chan> in,
sample_out: chan out,
state: Request,
}

impl Consumer {
fn new(request: chan> in, sample_out: chan out) -> Self {
Consumer {
request, sample_out, state: Request::default(),
}
}
fn next(self) { }
}
```

`main.x`
```rust
#![feature(generics)]
#![feature(explicit_state_access)]

import producer;
import consumer;

type Elem = s16;
const DEGREE = u32:3;
type Req = consumer::Request;

pub proc Top {}
impl Top {
fn new() -> Self {
let (req_s, req_r) = chan("req");
// Producer is instantiated with T = the parametric struct type.
producer::Producer()}>::new(req_s).spawn();

let (res_s, res_r) = chan("res");
// Consumer reaches the same struct through its own type parametric.
consumer::Consumer::new(req_r, res_s).spawn();
Top {}
}
}
```

```
ir_converter_main --top=Top --type_inference_v2=true \
--dslx_path= main.x
```

fails with the `No constexpr value found for node `T`` error above.
Collapsing the two procs into a single module makes the error disappear,
which is why it is easy to miss in small tests.

**Expected behavior**

Conversion succeeds; the explicitly-supplied `T: type` parametric is resolved
when the proc initializer is built.

**Root cause / suggested fix**

In `xls/dslx/type_system_v2/constant_collector.cc`, when `HandleStructInstance`
builds a proc initializer it collects the proc's parametrics with an
unconditional `GetConstExpr(binding->name_def())`. That call has no value for
an explicitly supplied `type` parametric. Looking the value up in the
parametric env first, and falling back to an opaque type reference for a
`GenericTypeAnnotation`, fixes it (positions must be preserved, since proc
initializers are compared element-wise):

```diff
--- a/xls/dslx/type_system_v2/constant_collector.cc
+++ b/xls/dslx/type_system_v2/constant_collector.cc
@@ -981,11 +981,27 @@ class Visitor : public AstNodeVisitorWithDefault {

std::vector parametrics;
parametrics.reserve(type.struct_def_base().parametric_bindings().size());
+ const ParametricEnv env = table_.GetParametricEnv(parametric_context_);
for (const ParametricBinding* binding :
type.struct_def_base().parametric_bindings()) {
- XLS_ASSIGN_OR_RETURN(InterpValue value,
- ti_->GetConstExpr(binding->name_def()));
- parametrics.push_back(value);
+ // Value parametrics (and defaulted type parametrics) have a constexpr
+ // noted on the binding. An explicitly supplied `T: type` parametric does
+ // not, so fall back to the parametric env, then to an opaque type
+ // reference. Positions must be preserved: proc initializers are compared
+ // element-wise by index.
+ std::optional value =
+ ti_->GetConstExprOption(binding->name_def());
+ if (!value.has_value()) {
+ value = env.GetValue(binding->name_def());
+ }
+ if (!value.has_value() &&
+ binding->type_annotation()->IsAnnotation()) {
+ value = InterpValue::MakeTypeReference(binding->type_annotation());
+ }
+ if (!value.has_value()) {
+ XLS_ASSIGN_OR_RETURN(value, ti_->GetConstExpr(binding->name_def()));
+ }
+ parametrics.push_back(*value);
}
```

With this change the reproduction above converts successfully.

**Environment**
- Built from `main` (google/xls @ 1b53da61b).
- Linux x86_64, type inference v2.

---
_Disclaimer: I hit this while working on a hardware project of mine. I used
Claude to locate the root cause in the codebase and to draft the patch above;
I have verified both the reproduction and the fix and am filing it._

Contributor guide

Open the contributing guide

Research direction

Start in xls/dslx/type_system_v2/constant_collector.cc at HandleStructInstance and the parametric collection path around the reported constant-collector locations. Run the provided three-file reproduction with ir_converter_main and type inference v2; done means conversion succeeds for the separate-module proc case without the missing constexpr error.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
74/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.