Add SystemVerilog chained calls a().b() and regression

Implement call_chain_expr in the parser, PECallFunction chain prefixes,
elaboration for class methods on returned handles, and prefix class resolution
for width checks on multi-hop chains. Place call_chain_expr before bare
hierarchy_identifier in expr_primary so id ( is not reduced as a lone ident.

Document behavior and developer notes in devel/sv_call_chain.md (including
that iverilog uses the installed lib/ivl/ivl binary).

Add ivtest sv_call_chain_method1 normal test with -g2012.

Made-with: Cursor
This commit is contained in:
mjoekhan 2026-04-14 00:07:21 +05:00
parent 1a10f9d57b
commit 24c1293801
11 changed files with 332 additions and 15 deletions

View File

@ -261,12 +261,28 @@ PECallFunction::PECallFunction(perm_string n, const list<named_pexpr_t> &parms)
{
}
PECallFunction::PECallFunction(PExpr* chain_prefix, const pform_name_t &method,
const vector<named_pexpr_t> &parms)
: path_(method), parms_(parms), chain_prefix_(chain_prefix), is_overridden_(false)
{
}
PECallFunction::PECallFunction(PExpr* chain_prefix, const pform_name_t &method,
const list<named_pexpr_t> &parms)
: path_(method), parms_(parms.begin(), parms.end()),
chain_prefix_(chain_prefix), is_overridden_(false)
{
}
PECallFunction::~PECallFunction()
{
delete chain_prefix_;
}
void PECallFunction::declare_implicit_nets(LexicalScope*scope, NetNet::Type type)
{
if (chain_prefix_)
chain_prefix_->declare_implicit_nets(scope, type);
for (const auto &parm : parms_) {
if (parm.parm)
parm.parm->declare_implicit_nets(scope, type);
@ -275,12 +291,13 @@ void PECallFunction::declare_implicit_nets(LexicalScope*scope, NetNet::Type type
bool PECallFunction::has_aa_term(Design*des, NetScope*scope) const
{
bool flag = false;
if (chain_prefix_ && chain_prefix_->has_aa_term(des, scope))
return true;
for (const auto &parm : parms_) {
if (parm.parm)
flag |= parm.parm->has_aa_term(des, scope);
if (parm.parm && parm.parm->has_aa_term(des, scope))
return true;
}
return flag;
return false;
}
PEConcat::PEConcat(const list<PExpr*>&p, PExpr*r)

22
PExpr.h
View File

@ -925,8 +925,18 @@ class PECallFunction : public PExpr {
explicit PECallFunction(const pform_name_t &n, const std::list<named_pexpr_t> &parms);
explicit PECallFunction(perm_string n, const std::list<named_pexpr_t> &parms);
// SystemVerilog: prefix().method(args) — prefix elaborates to a class handle.
explicit PECallFunction(PExpr* chain_prefix, const pform_name_t &method,
const std::vector<named_pexpr_t> &parms);
explicit PECallFunction(PExpr* chain_prefix, const pform_name_t &method,
const std::list<named_pexpr_t> &parms);
~PECallFunction() override;
// For chained-call resolution (path is only the final method name).
const pform_scoped_name_t& peek_path(void) const { return path_; }
const PExpr* peek_chain_prefix(void) const { return chain_prefix_; }
virtual void dump(std::ostream &) const override;
virtual void declare_implicit_nets(LexicalScope*scope, NetNet::Type type) override;
@ -945,6 +955,8 @@ class PECallFunction : public PExpr {
private:
pform_scoped_name_t path_;
std::vector<named_pexpr_t> parms_;
// If non-null, this call is prefix().tail_name(...) (SV method chain).
PExpr* chain_prefix_ = nullptr;
// For system functions.
bool is_overridden_;
@ -990,8 +1002,18 @@ class PECallFunction : public PExpr {
perm_string method_name,
const std::vector<named_pexpr_t>*src_parms) const;
NetExpr* elaborate_class_method_net_this_(Design*des, NetScope*scope,
NetExpr* this_expr,
const netclass_t*class_type,
perm_string method_name,
const std::vector<named_pexpr_t>*src_parms) const;
NetExpr* elaborate_expr_method_chained_(Design*des, NetScope*scope,
symbol_search_results&search_results) const;
NetExpr* elaborate_expr_chain_(Design*des, NetScope*scope, unsigned flags) const;
unsigned test_width_chain_(Design*des, NetScope*scope, width_mode_t&mode);
};
/*

72
devel/sv_call_chain.md Normal file
View File

@ -0,0 +1,72 @@
# SystemVerilog chained calls: `a().b()`
This note describes the parser and elaboration support for **chained calls**: a
function call whose value is a class handle, followed by one or more
`.method(args)` segments (e.g. `get_c().f()`, `a().b().c()`).
## Language shape
- **Parse:** A dedicated nonterminal `call_chain_expr` in `parse.y` builds a
left-associated chain:
- `hierarchy_identifier attribute_list_opt argument_list_parens` — first call;
- `call_chain_expr '.' hierarchy_identifier attribute_list_opt argument_list_parens` — each further segment.
- **`expr_primary`** includes `call_chain_expr` **before** the bare
`hierarchy_identifier` alternative so `id (` is parsed as a call, not as an
identifier plus a stray `(`.
## Parse tree (`PExpr`)
- **`PECallFunction`** (`PExpr.h` / `PExpr.cc`):
- Optional **`chain_prefix_`**: inner `PExpr` for the prefix (another
`PECallFunction` for longer chains).
- Constructors and accessors: `peek_path()`, `peek_chain_prefix()`.
- **`pform_make_chained_call_function`** (`pform.cc`) — requires SystemVerilog;
builds `PECallFunction(prefix, method_name, args)`.
## Elaboration (`elab_expr.cc`)
- **`PECallFunction::elaborate_expr_`** delegates to **`elaborate_expr_chain_`**
when `chain_prefix_` is set.
- **`elaborate_class_method_net_this_`** passes the elaborated prefix as the
implicit `this` argument (first parameter slot), including nested `NetEUFunc`
for inner calls — not only `NetESignal(net)`.
- **`resolve_call_chain_prefix_class`** (static helper) resolves the **class
type** of the prefix for multi-hop chains (e.g. width checks), walking the
chain prefix recursively instead of searching only the tail name.
## Dump / debug
- **`pform_dump.cc`** prints chained calls with a `prefix.` prefix before the
method path.
## Regression
- **`ivtest/ivltests/sv_call_chain_method1.v`**
- **`ivtest/vvp_tests/sv_call_chain_method1.json`** (`-g2012`)
- Listed in **`ivtest/regress-vvp.list`** as `sv_call_chain_method1`.
## Using a locally built `iverilog`
`iverilog` invokes the installed compiler under your prefix, typically
`$PREFIX/lib/ivl/ivl`, not the `ivl` binary in the build tree. After changing
the parser, **reinstall** or copy the new `ivl` into that lib directory so
`iverilog` picks up the change; otherwise chained-call syntax may still fail
with a **syntax error** while a direct `./ivl -C...` test from the build tree
succeeds.
```bash
# Example after building in-tree
make install
# or copy only the compiler binary to your existing install
cp ivl "$PREFIX/lib/ivl/ivl"
```
## Related files (non-exhaustive)
| Area | Files |
|------------|--------|
| Grammar | `parse.y` (`call_chain_expr`, `expr_primary`) |
| Parse form | `pform.cc`, `pform.h` |
| AST | `PExpr.h`, `PExpr.cc` |
| Elab | `elab_expr.cc` |
| Dump | `pform_dump.cc` |

View File

@ -55,6 +55,53 @@ bool type_is_vectorable(ivl_variable_type_t type)
}
}
/*
* For a().b() call chains, find the class type of the prefix expression
* (the value before the final ".method(args)").
*/
static const netclass_t* resolve_call_chain_prefix_class(Design*des, NetScope*scope,
PECallFunction* link)
{
if (link == 0)
return 0;
if (link->peek_chain_prefix() == 0) {
symbol_search_results sr;
if (!symbol_search(link, des, scope, link->peek_path(), UINT_MAX, &sr))
return 0;
if (!sr.is_scope() || sr.scope->type() != NetScope::FUNC)
return 0;
NetFuncDef* fundef = sr.scope->func_def();
ivl_assert(*link, fundef);
NetScope* dscope = fundef->scope();
NetNet* res = dscope->find_signal(dscope->basename());
if (res == 0)
return 0;
return dynamic_cast<const netclass_t*>(res->net_type());
}
PECallFunction* inner = dynamic_cast<PECallFunction*>(
const_cast<PExpr*>(link->peek_chain_prefix()));
if (inner == 0)
return 0;
const netclass_t* recv_class = resolve_call_chain_prefix_class(des, scope, inner);
if (recv_class == 0)
return 0;
perm_string mname = peek_tail_name(link->peek_path());
NetScope* mscope = recv_class->method_from_name(mname);
if (mscope == 0)
return 0;
NetFuncDef* mdef = mscope->func_def();
if (mdef == 0)
return 0;
NetNet* mres = mscope->find_signal(mscope->basename());
if (mres == 0)
return 0;
return dynamic_cast<const netclass_t*>(mres->net_type());
}
static ivl_nature_t find_access_function(const pform_scoped_name_t &path)
{
if (path.package || path.name.size() != 1)
@ -1742,9 +1789,53 @@ unsigned PECallFunction::test_width_method_(Design*, NetScope*,
return 0;
}
unsigned PECallFunction::test_width_chain_(Design*des, NetScope*scope,
width_mode_t&)
{
PECallFunction* inner_pf = dynamic_cast<PECallFunction*>(chain_prefix_);
if (inner_pf == 0) {
expr_width_ = 0;
return 0;
}
const netclass_t* cls = resolve_call_chain_prefix_class(des, scope, inner_pf);
if (cls == 0) {
expr_width_ = 0;
return 0;
}
perm_string mname = peek_tail_name(path_);
NetScope* mscope = cls->method_from_name(mname);
if (mscope == 0) {
expr_width_ = 0;
return 0;
}
NetFuncDef* mdef = mscope->func_def();
if (mdef == 0 || mdef->is_void()) {
expr_width_ = 0;
return 0;
}
NetNet* mres = mscope->find_signal(mscope->basename());
if (mres == 0) {
expr_width_ = 0;
return 0;
}
expr_type_ = mres->data_type();
expr_width_ = mres->vector_width();
min_width_ = expr_width_;
signed_flag_ = mres->get_signed();
return expr_width_;
}
unsigned PECallFunction::test_width(Design*des, NetScope*scope,
width_mode_t&mode)
{
if (chain_prefix_)
return test_width_chain_(des, scope, mode);
if (debug_elaborate) {
cerr << get_fileline() << ": PECallFunction::test_width: "
<< "path_: " << path_ << endl;
@ -2891,6 +2982,9 @@ NetExpr* PECallFunction::elaborate_expr_(Design*des, NetScope*scope,
{
flags &= ~SYS_TASK_ARG; // don't propagate the SYS_TASK_ARG flag
if (chain_prefix_)
return elaborate_expr_chain_(des, scope, flags);
// Search for the symbol. This should turn up a scope.
symbol_search_results search_results;
bool search_flag = symbol_search(this, des, scope, path_, UINT_MAX, &search_results);
@ -3256,6 +3350,18 @@ NetExpr* PECallFunction::elaborate_class_method_net_(Design*des, NetScope*scope,
NetNet*net, const netclass_t*class_type,
perm_string method_name,
const vector<named_pexpr_t>*src_parms) const
{
NetESignal*ethis = new NetESignal(net);
ethis->set_line(*this);
return elaborate_class_method_net_this_(des, scope, ethis, class_type,
method_name, src_parms);
}
NetExpr* PECallFunction::elaborate_class_method_net_this_(Design*des, NetScope*scope,
NetExpr* this_expr,
const netclass_t*class_type,
perm_string method_name,
const vector<named_pexpr_t>*src_parms) const
{
NetScope*method = class_type->method_from_name(method_name);
@ -3276,9 +3382,7 @@ NetExpr* PECallFunction::elaborate_class_method_net_(Design*des, NetScope*scope,
vector<NetExpr*> parms(def->port_count());
ivl_assert(*this, def->port_count() >= 1);
NetESignal*ethis = new NetESignal(net);
ethis->set_line(*this);
parms[0] = ethis;
parms[0] = this_expr;
elaborate_arguments_(des, scope, def, false, parms, 1, src_parms);
@ -3288,6 +3392,29 @@ NetExpr* PECallFunction::elaborate_class_method_net_(Design*des, NetScope*scope,
return call;
}
NetExpr* PECallFunction::elaborate_expr_chain_(Design*des, NetScope*scope,
unsigned flags) const
{
ivl_assert(*this, chain_prefix_);
NetExpr* inner = chain_prefix_->elaborate_expr(des, scope, -1, flags);
if (inner == 0)
return 0;
const netclass_t* cls = dynamic_cast<const netclass_t*>(inner->net_type());
if (cls == 0) {
cerr << get_fileline() << ": error: "
<< "The prefix of a chained call (a().b()) must be an expression "
<< "with class type." << endl;
des->errors += 1;
delete inner;
return 0;
}
perm_string method_name = peek_tail_name(path_);
return elaborate_class_method_net_this_(des, scope, inner, cls,
method_name, &parms_);
}
/*
* Handle obj.m1().m2(args): arguments apply only to the last call; intermediate
* methods must be class methods returning class handles.

View File

@ -0,0 +1,25 @@
// Chained call: function returns class handle, then method on result (a().b()).
module test;
class C;
function int f;
f = 7;
endfunction
endclass
function C get_c;
get_c = new;
endfunction
initial begin
int x;
x = get_c().f();
if (x !== 7) begin
$display("FAILED");
end else begin
$display("PASSED");
end
end
endmodule

View File

@ -222,6 +222,7 @@ sv_array_cassign6 vvp_tests/sv_array_cassign6.json
sv_array_cassign7 vvp_tests/sv_array_cassign7.json
sv_array_cassign8 vvp_tests/sv_array_cassign8.json
sv_automatic_2state vvp_tests/sv_automatic_2state.json
sv_call_chain_method1 vvp_tests/sv_call_chain_method1.json
sv_chained_constructor1 vvp_tests/sv_chained_constructor1.json
sv_chained_constructor2 vvp_tests/sv_chained_constructor2.json
sv_chained_constructor3 vvp_tests/sv_chained_constructor3.json

View File

@ -0,0 +1,9 @@
{
"type" : "normal",
"source" : "sv_call_chain_method1.v",
"iverilog-args" : [ "-g2012" ],
"vlog95" : {
"__comment" : "Classes are not supported",
"type" : "CE"
}
}

34
parse.y
View File

@ -756,7 +756,7 @@ Module::port_t *module_declare_port(const YYLTYPE&loc, char *id,
%type <spec_optional_args> timeskew_fullskew_opt_remain_active_flag
%type <expr> assignment_pattern expression expression_opt expr_mintypmax
%type <expr> expr_primary_or_typename expr_primary
%type <expr> expr_primary_or_typename expr_primary call_chain_expr
%type <expr> class_new dynamic_array_new
%type <expr> var_decl_initializer_opt initializer_opt
%type <expr> inc_or_dec_expression inside_expression lpvalue
@ -3869,6 +3869,26 @@ expr_primary_or_typename
;
/* SystemVerilog: a().b() — call a function, then invoke a method on the
returned class handle. Extends with further ".id(args)" as needed. */
call_chain_expr
: hierarchy_identifier attribute_list_opt argument_list_parens
{ PECallFunction*tmp = pform_make_call_function(@1, *$1, *$3);
delete $1;
delete $2;
delete $3;
$$ = tmp;
}
| call_chain_expr '.' hierarchy_identifier attribute_list_opt argument_list_parens
{ PECallFunction*tmp = pform_make_chained_call_function(@2, $1, *$3, *$5);
delete $3;
delete $4;
delete $5;
$$ = tmp;
}
;
expr_primary
: number
{ assert($1);
@ -3928,6 +3948,11 @@ expr_primary
/* The hierarchy_identifier rule matches simple identifiers as well as
indexed arrays and part selects */
/* SV call chains get_c1().f() — must come before bare hierarchy_identifier
so `id (` is not reduced as PEIdent + error. */
| call_chain_expr
{ $$ = $1;
}
| hierarchy_identifier
{ PEIdent*tmp = pform_new_ident(@1, *$1);
FILE_NAME(tmp, @1);
@ -3978,13 +4003,6 @@ expr_primary
function call. If a system identifier, then a system function
call. It can also be a call to a class method (function). */
| hierarchy_identifier attribute_list_opt argument_list_parens
{ PECallFunction*tmp = pform_make_call_function(@1, *$1, *$3);
delete $1;
delete $2;
delete $3;
$$ = tmp;
}
| class_hierarchy_identifier argument_list_parens
{ PECallFunction*tmp = pform_make_call_function(@1, *$1, *$2);
delete $1;

View File

@ -938,6 +938,24 @@ PECallFunction* pform_make_call_function(const struct vlltype&loc,
return tmp;
}
PECallFunction* pform_make_chained_call_function(const struct vlltype&loc,
PExpr*prefix,
const pform_name_t&method,
const list<named_pexpr_t> &parms)
{
if (!gn_system_verilog()) {
pform_requires_sv(loc, "Chained calls like a().b()");
delete prefix;
return new PECallFunction(method, parms);
}
check_potential_imports(loc, method.front().name, true);
PECallFunction*tmp = new PECallFunction(prefix, method, parms);
FILE_NAME(tmp, loc);
return tmp;
}
PCallTask* pform_make_call_task(const struct vlltype&loc,
const pform_name_t&name,
const list<named_pexpr_t> &parms)

View File

@ -324,6 +324,10 @@ extern void pform_set_type_referenced(const struct vlltype&loc, const char*name)
extern PECallFunction* pform_make_call_function(const struct vlltype&loc,
const pform_name_t&name,
const std::list<named_pexpr_t> &parms);
extern PECallFunction* pform_make_chained_call_function(const struct vlltype&loc,
PExpr*prefix,
const pform_name_t&method,
const std::list<named_pexpr_t> &parms);
extern PCallTask* pform_make_call_task(const struct vlltype&loc,
const pform_name_t&name,
const std::list<named_pexpr_t> &parms);

View File

@ -421,6 +421,10 @@ void PEConcat::dump(ostream&out) const
void PECallFunction::dump(ostream &out) const
{
if (peek_chain_prefix()) {
peek_chain_prefix()->dump(out);
out << ".";
}
out << path_ << "(" << parms_ << ")";
}