Support modport export/import task prototypes and out-of-block definitions (#7277)

This commit is contained in:
Yilou Wang 2026-03-19 00:20:34 +01:00 committed by GitHub
parent b8ca9292a4
commit a0a684109f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 308 additions and 11 deletions

View File

@ -87,6 +87,7 @@ class AstNodeFTask VL_NOT_FINAL : public AstNode {
// @astgen op4 := scopeNamep : Optional[AstScopeName]
string m_name; // Name of task
string m_cname; // Name of task if DPI import
string m_ifacePortName; // Interface port name for out-of-block definition (IEEE 25.8)
uint64_t m_dpiOpenParent = 0; // DPI import open array, if !=0, how many callees
bool m_taskPublic : 1; // Public task
bool m_attrIsolateAssign : 1; // User isolate_assignments attribute
@ -179,6 +180,8 @@ public:
void isExternProto(bool flag) { m_isExternProto = flag; }
bool isExternDef() const { return m_isExternDef; }
void isExternDef(bool flag) { m_isExternDef = flag; }
const string& ifacePortName() const { return m_ifacePortName; }
void ifacePortName(const string& name) { m_ifacePortName = name; }
bool prototype() const { return m_prototype; }
void prototype(bool flag) { m_prototype = flag; }
bool dpiExport() const { return m_dpiExport; }

View File

@ -1489,11 +1489,129 @@ class LinkDotFindVisitor final : public VNVisitor {
}
}
}
// Handle out-of-block interface method definition (IEEE 1800-2023 25.7)
// Move task body from module into the interface's prototype task,
// rewriting port-prefixed references in the body.
void moveIfaceExportBody(AstNodeFTask* nodep) {
const string& portName = nodep->ifacePortName();
UINFO(5, " moveIfaceExportBody: port=" << portName << " task=" << nodep->name() << endl);
// Look up the interface port variable in the module's symbol table
VSymEnt* const portSymp = m_modSymp->findIdFallback(portName);
if (!portSymp) {
nodep->v3error("Interface port not found for out-of-block definition: '"
<< portName << "' (IEEE 1800-2023 25.7)");
nodep->unlinkFrBack();
VL_DO_DANGLING(pushDeletep(nodep), nodep);
return;
}
AstVar* const portp = VN_CAST(portSymp->nodep(), Var);
if (!portp) {
nodep->v3fatalSrc("Out-of-block definition port is not a variable: " << portName);
return;
}
// Get the interface name from the port's type
// At this stage dtypep() may not be set yet; check childDTypep() too
AstIfaceRefDType* ifaceRefDtp = nullptr;
if (portp->dtypep()) { ifaceRefDtp = VN_CAST(portp->dtypep(), IfaceRefDType); }
if (!ifaceRefDtp && portp->childDTypep()) {
ifaceRefDtp = VN_CAST(portp->childDTypep(), IfaceRefDType);
}
if (!ifaceRefDtp) {
nodep->v3error("Out-of-block definition port is not an interface port: '"
<< portName << "' (IEEE 1800-2023 25.7)");
nodep->unlinkFrBack();
VL_DO_DANGLING(pushDeletep(nodep), nodep);
return;
}
const string ifaceName = ifaceRefDtp->ifaceName();
UINFO(5, " moveIfaceExportBody: iface=" << ifaceName << endl);
// Find the interface module by name
AstNodeModule* const ifaceModp = m_statep->findModuleSym(ifaceName);
if (!ifaceModp) {
nodep->v3fatalSrc("Interface not found for out-of-block definition: " << ifaceName);
return;
}
// Find the prototype task in the interface
AstNodeFTask* protoTaskp = nullptr;
for (AstNode* itemp = ifaceModp->stmtsp(); itemp; itemp = itemp->nextp()) {
if (AstNodeFTask* const ftaskp = VN_CAST(itemp, NodeFTask)) {
if (ftaskp->name() == nodep->name() && ftaskp->prototype()) {
protoTaskp = ftaskp;
break;
}
}
}
if (!protoTaskp) {
nodep->v3error("No matching export prototype found in interface "
<< ifaceName << " for task " << nodep->prettyNameQ()
<< " (IEEE 1800-2023 25.7)");
nodep->unlinkFrBack();
VL_DO_DANGLING(pushDeletep(nodep), nodep);
return;
}
// Move body statements (non-port declarations) from the out-of-block task
// to the prototype task. Rewrite port.X references to just X in the body.
for (AstNode* stmtp = nodep->stmtsp(); stmtp;) {
AstNode* const nextp = stmtp->nextp();
// Skip port declarations -- they already exist in the prototype
if (const AstVar* const varp = VN_CAST(stmtp, Var)) {
if (varp->isIO()) {
stmtp = nextp;
continue;
}
}
// Clone this statement and rewrite port references
AstNode* const newStmtp = stmtp->cloneTree(false);
rewriteIfacePortRefsSingle(newStmtp, portName);
protoTaskp->addStmtsp(newStmtp);
stmtp = nextp;
}
// Mark prototype as no longer prototype (it has a body now)
protoTaskp->prototype(false);
// Delete the out-of-block task from the module
nodep->unlinkFrBack();
VL_DO_DANGLING(pushDeletep(nodep), nodep);
}
// Rewrite AstDot nodes where lhsp is AstParseRef with the port name
// to just the rhsp (stripping the port prefix).
static void rewriteIfacePortRefsSingle(AstNode* nodep, const string& portName) {
if (!nodep) return;
// Check if this is a non-colon Dot with lhs matching portName
if (AstDot* const dotp = VN_CAST(nodep, Dot)) {
if (!dotp->colon()) {
const AstParseRef* const lhsRefp = VN_CAST(dotp->lhsp(), ParseRef);
if (lhsRefp && lhsRefp->name() == portName) {
UINFO(5,
" rewriteIfacePortRefs: stripping port prefix from " << dotp << endl);
AstNode* const rhsp = dotp->rhsp()->unlinkFrBack();
dotp->replaceWith(rhsp);
VL_DO_DANGLING(dotp->deleteTree(), dotp);
rewriteIfacePortRefsSingle(rhsp, portName);
return;
}
}
}
// Recurse into all children
for (AstNode* childp : {nodep->op1p(), nodep->op2p(), nodep->op3p(), nodep->op4p()}) {
while (childp) {
AstNode* const nextp = childp->nextp();
rewriteIfacePortRefsSingle(childp, portName);
childp = nextp;
}
}
}
void visit(AstNodeFTask* nodep) override { // FindVisitor::
// NodeTask: Remember its name for later resolution
UINFO(5, " " << nodep);
UASSERT_OBJ(m_curSymp && m_modSymp, nodep, "Function/Task not under module?");
if (nodep->name() == "new") m_explicitNew = true;
// Handle out-of-block interface method definition (IEEE 25.8)
if (!nodep->ifacePortName().empty() && m_statep->forPrimary()) {
moveIfaceExportBody(nodep);
return; // Task has been deleted
}
// Remember the existing symbol table scope
VL_RESTORER(m_classOrPackagep);
VL_RESTORER(m_curSymp);
@ -2659,7 +2777,6 @@ class LinkDotIfaceVisitor final : public VNVisitor {
void visit(AstModportFTaskRef* nodep) override { // IfaceVisitor::
UINFO(5, " fif: " << nodep);
iterateChildren(nodep);
if (nodep->isExport()) nodep->v3warn(E_UNSUPPORTED, "Unsupported: modport export");
VSymEnt* const symp = m_curSymp->findIdFallback(nodep->name());
if (!symp) {
nodep->v3error("Modport item not found: " << nodep->prettyNameQ());

View File

@ -50,6 +50,8 @@ public:
= false; // Standalone ID is a tf_identifier instead of port_identifier
bool m_modportImpExpLastIsExport
= false; // Last import_export statement in modportPortsDecl is an export
AstNode* m_modportProtoTasksp
= nullptr; // Prototype tasks from modport import/export with prototype
int m_pinNum = -1; // Pin number currently parsing
std::stack<int> m_pinStack; // Queue of pin numbers being parsed

View File

@ -1690,8 +1690,16 @@ modport_itemList<nodep>: // IEEE: part of modport_declaration
modport_item<nodep>: // ==IEEE: modport_item
idAny/*new-modport*/ '('
/*mid*/ { VARRESET_NONLIST(UNKNOWN); VARIO(INOUT); }
/*cont*/ modportPortsDeclList ')' { $$ = new AstModport{$<fl>1, *$1, $4}; }
/*mid*/ { VARRESET_NONLIST(UNKNOWN); VARIO(INOUT);
GRAMMARP->m_modportProtoTasksp = nullptr; }
/*cont*/ modportPortsDeclList ')'
{ $$ = new AstModport{$<fl>1, *$1, $4};
// Append any prototype tasks from import/export as interface siblings
if (GRAMMARP->m_modportProtoTasksp) {
$$ = AstNode::addNextNull(
GRAMMARP->m_modportProtoTasksp, $$);
GRAMMARP->m_modportProtoTasksp = nullptr;
} }
;
modportPortsDeclList<nodep>:
@ -1720,9 +1728,19 @@ modportPortsDecl<nodep>:
GRAMMARP->m_modportImpExpActive = true;
GRAMMARP->m_modportImpExpLastIsExport = true; }
| yIMPORT method_prototype
{ $$ = nullptr; BBUNSUP($<fl>1, "Unsupported: Modport import with prototype"); DEL($2); }
{ $$ = new AstModportFTaskRef{$<fl>2, $2->name(), false};
GRAMMARP->m_modportImpExpActive = true;
GRAMMARP->m_modportImpExpLastIsExport = false;
// Import prototype: task should already exist in interface
// (from export or direct declaration). Delete the prototype.
DEL($2); }
| yEXPORT method_prototype
{ $$ = nullptr; BBUNSUP($<fl>1, "Unsupported: Modport export with prototype"); DEL($2); }
{ $$ = new AstModportFTaskRef{$<fl>2, $2->name(), true};
GRAMMARP->m_modportImpExpActive = true;
GRAMMARP->m_modportImpExpLastIsExport = true;
// Export: add prototype task to interface (as sibling of modport)
GRAMMARP->m_modportProtoTasksp
= AstNode::addNextNull(GRAMMARP->m_modportProtoTasksp, $2); }
// Continuations of above after a comma.
// // IEEE: modport_simple_ports_declaration
| modportSimplePortOrTFPort { $$ = $1; }
@ -4679,7 +4697,7 @@ taskId<nodeFTaskp>:
| id/*interface_identifier*/ '.' idAny
{ $$ = new AstTask{$<fl>$, *$3, nullptr};
$$->verilogTask(true);
BBUNSUP($2, "Unsupported: Out of block function declaration"); }
$$->ifacePortName(*$1); }
//
| packageClassScope id
{ $$ = new AstTask{$<fl>$, *$2, nullptr};
@ -4745,9 +4763,9 @@ fIdScoped<funcp>: // IEEE: part of function_body_declaration/task_
//
| id/*interface_identifier*/ '.' idAny
{ $<fl>$ = $<fl>1;
$$ = new AstFunc{$<fl>$, *$1, nullptr, nullptr};
$$ = new AstFunc{$<fl>$, *$3, nullptr, nullptr};
$$->verilogFunction(true);
BBUNSUP($2, "Unsupported: Out of block function declaration"); }
$$->ifacePortName(*$1); }
//
| packageClassScope id
{ $<fl>$ = $<fl>1;

View File

@ -2,7 +2,4 @@
12 | extern function myfunc (input logic val);
| ^~~~~~
... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest
%Error-UNSUPPORTED: t/t_interface_modport_export.v:48:30: Unsupported: Out of block function declaration
48 | function automatic logic ie.myfunc (input logic val);
| ^
%Error: Exiting due to

View File

@ -0,0 +1,11 @@
%Error: t/t_modport_export_bad.v:18:8: Out-of-block definition port is not an interface port: 'clk' (IEEE 1800-2023 25.7)
18 | task clk.send(input logic [7:0] val);
| ^~~
... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance.
%Error: t/t_modport_export_bad.v:25:8: Interface port not found for out-of-block definition: 'nonexistent' (IEEE 1800-2023 25.7)
25 | task nonexistent.send(input logic [7:0] val);
| ^~~~~~~~~~~
%Error: t/t_modport_export_bad.v:40:8: No matching export prototype found in interface bus_if_noexport for task 'send' (IEEE 1800-2023 25.7)
40 | task port.send(input logic [7:0] val);
| ^~~~
%Error: Exiting due to

View File

@ -0,0 +1,16 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of either the GNU Lesser General Public License Version 3
# or the Perl Artistic License Version 2.0.
# SPDX-FileCopyrightText: 2026 Wilson Snyder
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
import vltest_bootstrap
test.scenarios('vlt')
test.compile(fails=test.vlt_all, expect_filename=test.golden_filename)
test.passes()

View File

@ -0,0 +1,52 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 PlanV GmbH
// SPDX-License-Identifier: CC0-1.0
interface bus_if;
logic [7:0] data;
modport provider(
output data,
export task send(input logic [7:0] val)
);
endinterface
// Error 1: 'clk' is a plain wire port, not an interface port
module driver1(bus_if.provider port, input logic clk);
task clk.send(input logic [7:0] val); // lint_off: UNUSED
port.data = val;
endtask
endmodule
// Error 2: 'nonexistent' is not a port on this module
module driver2(bus_if.provider port);
task nonexistent.send(input logic [7:0] val);
port.data = val;
endtask
endmodule
interface bus_if_noexport;
logic [7:0] data;
modport provider(
output data
);
endinterface
// Error 3: no export prototype for 'send' in interface
module driver3(bus_if_noexport.provider port);
task port.send(input logic [7:0] val);
port.data = val;
endtask
endmodule
module t;
bus_if bif();
logic clk;
driver1 drv1(.port(bif.provider), .clk(clk));
driver2 drv2(.port(bif.provider));
bus_if_noexport bif2();
driver3 drv3(.port(bif2.provider));
endmodule

View File

@ -0,0 +1,18 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of either the GNU Lesser General Public License Version 3
# or the Perl Artistic License Version 2.0.
# SPDX-FileCopyrightText: 2026 Wilson Snyder
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
import vltest_bootstrap
test.scenarios('simulator')
test.compile()
test.execute()
test.passes()

View File

@ -0,0 +1,63 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 PlanV GmbH
// SPDX-License-Identifier: CC0-1.0
`define checkh(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got='h%x exp='h%x\n", `__FILE__,`__LINE__, (gotv), (expv)); $stop; end while(0)
interface bus_if;
logic [7:0] data;
logic [7:0] result;
modport provider(
output data,
output result,
export task send(input logic [7:0] val),
export task accumulate(input logic [7:0] a, input logic [7:0] b)
);
modport consumer(
input data,
input result,
import task send(input logic [7:0] val),
import task accumulate(input logic [7:0] a, input logic [7:0] b)
);
endinterface
module driver(bus_if.provider port);
task port.send(input logic [7:0] val);
port.data = val;
port.result = val + 8'h01;
endtask
task port.accumulate(input logic [7:0] a, input logic [7:0] b);
port.data = a;
port.result = a + b;
endtask
endmodule
module t;
bus_if bif();
driver drv(.port(bif.provider));
initial begin
// Test 1: send -- multiple statements in task body
bif.consumer.send(8'hAB);
`checkh(bif.data, 8'hAB);
`checkh(bif.result, 8'hAC);
// Test 2: send again with different value
bif.consumer.send(8'h42);
`checkh(bif.data, 8'h42);
`checkh(bif.result, 8'h43);
// Test 3: accumulate -- multiple statements + two arguments
bif.consumer.accumulate(8'h10, 8'h20);
`checkh(bif.data, 8'h10);
`checkh(bif.result, 8'h30);
$write("*-* All Finished *-*\n");
$finish;
end
endmodule