slang ASTVisitor silently skips Invalid nodes: add VisitFlags::Bad to find writes in blocks slang couldn't bind
With slang 12.x as a library, an ASTVisitor<..., Statements|Expressions> never enters a statement or expression that is bad(): visit() returns early unless VisitFlags::Bad is set, and InvalidStatement / InvalidExpression have no visitStmts/visitExprs, so even with the flag the default does nothing. A procedural block containing one unbindable statement (e.g. a call to a task whose file wasn't given) becomes one InvalidStatement whose child still holds the whole block, so a write-collector sees nothing in it.
Fix: declare the visitor with VisitFlags::Bad and add handlers: void handle(const InvalidStatement& s) { if (s.child) s.child->visit(*this); } void handle(const InvalidExpression& e) { if (e.child) e.child->visit(*this); } (slang's own handle() lookup is CRTP, so no using-declaration is needed.)
Related gotchas: - SyntaxNode::toString() includes the first token's leading trivia, so a statement or expression text can start with the comment on the line above. Strip it with: size_t lead = 0; for (auto& t : node->getFirstToken().trivia()) lead += t.getRawText().size(); then take substr(lead). - Implicit assignments slang makes up (e.g. the output argument of $value$plusargs is an AssignmentExpression) have no syntax, so their text is empty: skip or fall back to the enclosing statement's text. - Unknown simulator system tasks ($fsdbDumpvars etc.) are better handled before elaboration by registering a stub SystemSubroutine for each unknown $name; then the block isn't Invalid at all.