The following was produced by Claude.
Summary
Since #121 (Emit hw.typealias for named types in CIRCT IR output), the CIRCT typescope is
emitted inline into every generated .sv file behind an include guard named after the
scope:
`ifndef _TYPESCOPE_CoreModuleTypeScope
`define _TYPESCOPE_CoreModuleTypeScope
typedef logic _hdk_lqd__LqdControlOp;
typedef struct packed {logic [63:0] address; logic [15:0] valid_bytes; logic [63:0] data; }
_hdk_esi_lqd__EsiLqdWriteReq___64__;
...
`endif // _TYPESCOPE_CoreModuleTypeScope
The guard macro is identical in every file, but the contents differ per file — each file
declares only the named types it uses. When more than one exported class is compiled into the
same design, the first file processed defines the guard and every later file's typedefs are
silently skipped, while its modules still reference them.
The result is unsimulatable/unsynthesizable output from an otherwise successful compile.
Reproducer
Two exports of a template that instantiate a named type with different parameters is enough.
Concretely, with EsiLqdToHost<8> / EsiLqdToHost<64> / … (each producing
EsiLqdWriteReq<MessageBytes*8>):
$ kanagawa exports.k -O3 --backend=sv --target-device=mock --output=r_
$ verilator --lint-only -sv r_*.sv
%Error: r_LqdToHost_8.sv:12344:30: Can't find typedef/interface: '_hdk_esi_lqd__EsiLqdWriteReq___64__'
%Error: r_LqdToHost_64.sv:12345:30: Can't find typedef/interface: '_hdk_esi_lqd__EsiLqdWriteReq___512__'
%Error: r_LqdToHost_512.sv:12349:30: Can't find typedef/interface: '_hdk_esi_lqd__EsiLqdWriteReq___4096__'
%Error: r_LqdToHost_8.sv:12332:30: Can't find typedef/interface: '_hdk_lqd__LqdControlOp'
... (12 errors total)
r_LqdToHost_8.sv wins the guard, so it is the only file whose _TYPESCOPE_CoreModuleTypeScope
body survives. _hdk_esi_lqd__EsiLqdWriteReq___512__ (needed by r_LqdToHost_64.sv) is never
declared anywhere in the compiled sources.
The dangling reference is on the ESI wrapper port itself:
output wire struct packed {_hdk_esi_lqd__EsiLqdWriteReq___64__ req; } WriteMem,
Regression range
Same sources, same flags, same Verilator (5.048):
| KanagawaOSS |
Can't find typedef errors |
1.2.0-main.240 |
0 |
1.2.0-main.249 |
12 |
On main.240 the _TYPESCOPE_CoreModuleTypeScope block is emitted but empty, so the shared
guard was harmless. #121 populated it without making the guard granular.
Impact
Any design that compiles two or more exported classes using named struct/union/enum types on
ESI-visible ports. Because the named types reach the ports via hw.typealias, this is exactly
the case #121 was intended to improve.
Suggested fix
Guard each declaration on its own name rather than the scope, so the blocks compose:
`ifndef _TYPEDECL_CoreModuleTypeScope___hdk_esi_lqd__EsiLqdWriteReq___64__
`define _TYPEDECL_CoreModuleTypeScope___hdk_esi_lqd__EsiLqdWriteReq___64__
typedef struct packed {...} _hdk_esi_lqd__EsiLqdWriteReq___64__;
`endif
Every declaration then survives and is still defined exactly once (the mangled names already
encode template arguments, so equal names imply equal definitions). We are carrying this rewrite
as a post-processing pass over the generated .sv locally and it resolves all 12 errors.
Note that a consumer which re-emits the imported typescope (e.g. PyCDE emitting its own modules
from the imported IR) must apply the same scheme, otherwise it both suppresses the Kanagawa
blocks and redeclares their contents.
Related, from the same upgrade
Also worth being aware of for hw.typealias consumers: each generated file declares a typescope
under the same symbol with different members, so tools that import several Kanagawa MLIR
files and de-duplicate typescopes by symbol name must merge the hw.typedecl entries rather
than keep the first typescope, or the aliases from later files end up unresolvable.
The following was produced by Claude.
Summary
Since #121 (
Emit hw.typealias for named types in CIRCT IR output), the CIRCT typescope isemitted inline into every generated
.svfile behind an include guard named after thescope:
The guard macro is identical in every file, but the contents differ per file — each file
declares only the named types it uses. When more than one exported class is compiled into the
same design, the first file processed defines the guard and every later file's typedefs are
silently skipped, while its modules still reference them.
The result is unsimulatable/unsynthesizable output from an otherwise successful compile.
Reproducer
Two exports of a template that instantiate a named type with different parameters is enough.
Concretely, with
EsiLqdToHost<8>/EsiLqdToHost<64>/ … (each producingEsiLqdWriteReq<MessageBytes*8>):r_LqdToHost_8.svwins the guard, so it is the only file whose_TYPESCOPE_CoreModuleTypeScopebody survives.
_hdk_esi_lqd__EsiLqdWriteReq___512__(needed byr_LqdToHost_64.sv) is neverdeclared anywhere in the compiled sources.
The dangling reference is on the ESI wrapper port itself:
Regression range
Same sources, same flags, same Verilator (5.048):
Can't find typedeferrors1.2.0-main.2401.2.0-main.249On
main.240the_TYPESCOPE_CoreModuleTypeScopeblock is emitted but empty, so the sharedguard was harmless. #121 populated it without making the guard granular.
Impact
Any design that compiles two or more exported classes using named struct/union/enum types on
ESI-visible ports. Because the named types reach the ports via
hw.typealias, this is exactlythe case #121 was intended to improve.
Suggested fix
Guard each declaration on its own name rather than the scope, so the blocks compose:
Every declaration then survives and is still defined exactly once (the mangled names already
encode template arguments, so equal names imply equal definitions). We are carrying this rewrite
as a post-processing pass over the generated
.svlocally and it resolves all 12 errors.Note that a consumer which re-emits the imported typescope (e.g. PyCDE emitting its own modules
from the imported IR) must apply the same scheme, otherwise it both suppresses the Kanagawa
blocks and redeclares their contents.
Related, from the same upgrade
Also worth being aware of for
hw.typealiasconsumers: each generated file declares a typescopeunder the same symbol with different members, so tools that import several Kanagawa MLIR
files and de-duplicate typescopes by symbol name must merge the
hw.typedeclentries ratherthan keep the first typescope, or the aliases from later files end up unresolvable.