From a0a684109fe7c5b84ae79a45f6bdcc674b57a8c9 Mon Sep 17 00:00:00 2001 From: Yilou Wang Date: Thu, 19 Mar 2026 00:20:34 +0100 Subject: [PATCH] Support modport export/import task prototypes and out-of-block definitions (#7277) --- src/V3AstNodeOther.h | 3 + src/V3LinkDot.cpp | 119 +++++++++++++++++- src/V3ParseGrammar.h | 2 + src/verilog.y | 32 +++-- test_regress/t/t_interface_modport_export.out | 3 - test_regress/t/t_modport_export_bad.out | 11 ++ test_regress/t/t_modport_export_bad.py | 16 +++ test_regress/t/t_modport_export_bad.v | 52 ++++++++ test_regress/t/t_modport_export_task.py | 18 +++ test_regress/t/t_modport_export_task.v | 63 ++++++++++ 10 files changed, 308 insertions(+), 11 deletions(-) create mode 100644 test_regress/t/t_modport_export_bad.out create mode 100755 test_regress/t/t_modport_export_bad.py create mode 100644 test_regress/t/t_modport_export_bad.v create mode 100755 test_regress/t/t_modport_export_task.py create mode 100644 test_regress/t/t_modport_export_task.v diff --git a/src/V3AstNodeOther.h b/src/V3AstNodeOther.h index 29ee46408..d161d572e 100644 --- a/src/V3AstNodeOther.h +++ b/src/V3AstNodeOther.h @@ -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; } diff --git a/src/V3LinkDot.cpp b/src/V3LinkDot.cpp index f9c5abbf2..5fd0366f9 100644 --- a/src/V3LinkDot.cpp +++ b/src/V3LinkDot.cpp @@ -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()); diff --git a/src/V3ParseGrammar.h b/src/V3ParseGrammar.h index 373402133..276ed2ffb 100644 --- a/src/V3ParseGrammar.h +++ b/src/V3ParseGrammar.h @@ -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 m_pinStack; // Queue of pin numbers being parsed diff --git a/src/verilog.y b/src/verilog.y index 421e301a9..39684d8f0 100644 --- a/src/verilog.y +++ b/src/verilog.y @@ -1690,8 +1690,16 @@ modport_itemList: // IEEE: part of modport_declaration modport_item: // ==IEEE: modport_item idAny/*new-modport*/ '(' - /*mid*/ { VARRESET_NONLIST(UNKNOWN); VARIO(INOUT); } - /*cont*/ modportPortsDeclList ')' { $$ = new AstModport{$1, *$1, $4}; } + /*mid*/ { VARRESET_NONLIST(UNKNOWN); VARIO(INOUT); + GRAMMARP->m_modportProtoTasksp = nullptr; } + /*cont*/ modportPortsDeclList ')' + { $$ = new AstModport{$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: @@ -1720,9 +1728,19 @@ modportPortsDecl: GRAMMARP->m_modportImpExpActive = true; GRAMMARP->m_modportImpExpLastIsExport = true; } | yIMPORT method_prototype - { $$ = nullptr; BBUNSUP($1, "Unsupported: Modport import with prototype"); DEL($2); } + { $$ = new AstModportFTaskRef{$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($1, "Unsupported: Modport export with prototype"); DEL($2); } + { $$ = new AstModportFTaskRef{$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: | id/*interface_identifier*/ '.' idAny { $$ = new AstTask{$$, *$3, nullptr}; $$->verilogTask(true); - BBUNSUP($2, "Unsupported: Out of block function declaration"); } + $$->ifacePortName(*$1); } // | packageClassScope id { $$ = new AstTask{$$, *$2, nullptr}; @@ -4745,9 +4763,9 @@ fIdScoped: // IEEE: part of function_body_declaration/task_ // | id/*interface_identifier*/ '.' idAny { $$ = $1; - $$ = new AstFunc{$$, *$1, nullptr, nullptr}; + $$ = new AstFunc{$$, *$3, nullptr, nullptr}; $$->verilogFunction(true); - BBUNSUP($2, "Unsupported: Out of block function declaration"); } + $$->ifacePortName(*$1); } // | packageClassScope id { $$ = $1; diff --git a/test_regress/t/t_interface_modport_export.out b/test_regress/t/t_interface_modport_export.out index f138ab349..6b24ba0a9 100644 --- a/test_regress/t/t_interface_modport_export.out +++ b/test_regress/t/t_interface_modport_export.out @@ -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 diff --git a/test_regress/t/t_modport_export_bad.out b/test_regress/t/t_modport_export_bad.out new file mode 100644 index 000000000..6cffcf93d --- /dev/null +++ b/test_regress/t/t_modport_export_bad.out @@ -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 diff --git a/test_regress/t/t_modport_export_bad.py b/test_regress/t/t_modport_export_bad.py new file mode 100755 index 000000000..0d60466ab --- /dev/null +++ b/test_regress/t/t_modport_export_bad.py @@ -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() diff --git a/test_regress/t/t_modport_export_bad.v b/test_regress/t/t_modport_export_bad.v new file mode 100644 index 000000000..b8b338139 --- /dev/null +++ b/test_regress/t/t_modport_export_bad.v @@ -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 diff --git a/test_regress/t/t_modport_export_task.py b/test_regress/t/t_modport_export_task.py new file mode 100755 index 000000000..8a938befd --- /dev/null +++ b/test_regress/t/t_modport_export_task.py @@ -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() diff --git a/test_regress/t/t_modport_export_task.v b/test_regress/t/t_modport_export_task.v new file mode 100644 index 000000000..6a8cf3484 --- /dev/null +++ b/test_regress/t/t_modport_export_task.v @@ -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