[preprocessor] Fix crash/hang on truncated callable macro invocation

GenerateBypassWhiteSpaces dereferences the stream iterator (**iterator) with no
end guard, relying on every stream ending in a kept EOF token. The streams
re-lexed for macro expansion in ExpandText, ExpandMacro, HandleInclude and the
standalone tool strip the EOF (loop stops on !isEOF()), so a callable macro
invocation truncated at end-of-stream (e.g. `define A(x) hello `A followed by
`A(1)) makes the streamer return the view's end() iterator and the deref reads
past the end -> SIGSEGV. An included file ending in a callable macro crashes the
same way; a '(' with no ')' spins forever.

Restore the kept-EOF sentinel on each re-lexed stream so the whitespace-skip
loop stops at EOF and callers return a diagnostic instead of dereferencing past
the end: append the EOF sentinel in all four stream builders; break on it in the
two token-pulling loops so it is not forwarded; skip it when splicing an included
child stream into the parent; and break on EOF in the argument-scanning loop
(the one caller that did not handle a mid-scan EOF, which otherwise hangs on a
'(' without ')'). Also record the "callable macro without ()" error in
preprocess_data_.errors instead of silently swallowing it (requires making
ConsumeAndParseMacroCall non-static).

Adds TruncatedCallableMacroDoesNotCrash. Existing preprocessor and analyzer test
suites pass. Unbounded macro self-recursion is a separate pre-existing bug and
is not addressed here.

Signed-off-by: Eylon Krause <eylon1909@gmail.com>
diff --git a/verible/verilog/preprocessor/verilog-preprocess.cc b/verible/verilog/preprocessor/verilog-preprocess.cc
index 2f77111..1d847fb 100644
--- a/verible/verilog/preprocessor/verilog-preprocess.cc
+++ b/verible/verilog/preprocessor/verilog-preprocess.cc
@@ -245,11 +245,15 @@
   if ((*token_iter)->text() == "(") {
     token_iter = GenerateBypassWhiteSpaces(generator);  // skip the "("
   } else {
+    preprocess_data_.errors.emplace_back(
+        **token_iter,
+        "Error it is illegal to call a callable macro without ().");
     return absl::InvalidArgumentError(
         "Error it is illegal to call a callable macro without ().");
   }
 
   while (parameters_size > 0) {
+    if ((*token_iter)->isEOF()) break;  // truncated call; stop scanning args
     if ((*token_iter)->token_enum() == MacroArg) {
       macro_call->positional_arguments.emplace_back(**token_iter);
       token_iter = GenerateBypassWhiteSpaces(generator);
@@ -342,6 +346,10 @@
        lexer.DoNextToken()) {
     lexed_sequence.push_back(lexer.GetLastToken());
   }
+  // Retain the EOF token as an end sentinel so a truncated callable-macro
+  // invocation stops at EOF in GenerateBypassWhiteSpaces instead of
+  // dereferencing past the end of the stream view.
+  lexed_sequence.push_back(lexer.GetLastToken());
   verible::TokenStreamView lexed_streamview;
   // Initializing the lexed token stream view.
   InitTokenStreamView(lexed_sequence, &lexed_streamview);
@@ -352,6 +360,7 @@
   // Token-pulling loop.
   for (auto iter = iter_generator(); iter != end; iter = iter_generator()) {
     auto &last_token = **iter;
+    if (last_token.isEOF()) break;  // end sentinel; nothing to forward
     // TODO: handle lexical error
     if (lexer.GetLastToken().token_enum() == TK_SPACE) {
       continue;  // don't forward spaces
@@ -396,6 +405,8 @@
        lexer.DoNextToken()) {
     lexed_sequence.push_back(lexer.GetLastToken());
   }
+  // Retain EOF end sentinel (see ExpandText).
+  lexed_sequence.push_back(lexer.GetLastToken());
   verible::TokenStreamView lexed_streamview;
   // Initializing the lexed token stream view.
   InitTokenStreamView(lexed_sequence, &lexed_streamview);
@@ -407,6 +418,7 @@
   for (auto iter = iter_generator(); iter != end; iter = iter_generator()) {
     // TODO: handle lexical error
     auto &last_token = **iter;
+    if (last_token.isEOF()) break;  // end sentinel; nothing to forward
     if (last_token.token_enum() == TK_SPACE) continue;  // don't forward spaces
     // If the expanded token is another macro identifier that needs to be
     // expanded.
@@ -635,6 +647,9 @@
        lexer.DoNextToken()) {
     included_sequence.push_back(lexer.GetLastToken());
   }
+  // Retain EOF end sentinel; the child ScanStream expects an EOF-terminated
+  // stream.
+  included_sequence.push_back(lexer.GetLastToken());
 
   // Preprocessing the included file tokens.
   verible::TokenStreamView lexed_streamview;
@@ -657,8 +672,11 @@
     preprocess_data_.included_text_structure.push_back(std::move(u));
   }
 
-  // Forwarding the included preprocessed view.
+  // Forwarding the included preprocessed view.  The EOF end sentinel appended
+  // above is consumed by the child ScanStream and must not be spliced into the
+  // middle of the parent's token stream.
   for (const auto &u : child_preprocessed_data.preprocessed_token_stream) {
+    if (u->isEOF()) continue;
     preprocess_data_.preprocessed_token_stream.push_back(u);
   }
 
diff --git a/verible/verilog/preprocessor/verilog-preprocess.h b/verible/verilog/preprocessor/verilog-preprocess.h
index 2d134ae..0d932f0 100644
--- a/verible/verilog/preprocessor/verilog-preprocess.h
+++ b/verible/verilog/preprocessor/verilog-preprocess.h
@@ -164,9 +164,11 @@
   absl::Status HandleElse(TokenStreamView::const_iterator else_pos);
   absl::Status HandleEndif(TokenStreamView::const_iterator endif_pos);
 
-  static absl::Status ConsumeAndParseMacroCall(
-      TokenStreamView::const_iterator, const StreamIteratorGenerator &,
-      verible::MacroCall *, const verible::MacroDefinition &);
+  // Non-static so it can record diagnostics into preprocess_data_.errors.
+  absl::Status ConsumeAndParseMacroCall(TokenStreamView::const_iterator,
+                                        const StreamIteratorGenerator &,
+                                        verible::MacroCall *,
+                                        const verible::MacroDefinition &);
 
   // The following functions return nullptr when there is no error:
   absl::Status ConsumeMacroDefinition(const StreamIteratorGenerator &,
diff --git a/verible/verilog/preprocessor/verilog-preprocess_test.cc b/verible/verilog/preprocessor/verilog-preprocess_test.cc
index ae33567..a8e41d9 100644
--- a/verible/verilog/preprocessor/verilog-preprocess_test.cc
+++ b/verible/verilog/preprocessor/verilog-preprocess_test.cc
@@ -1033,5 +1033,30 @@
       << error.error_message;
 }
 
+// Regression: a callable-macro invocation truncated at end-of-stream (no '(',
+// or '(' with no matching ')') must not crash or hang the preprocessor.  Before
+// the fix these inputs dereferenced past the end of the token stream view
+// (SIGSEGV) or spun forever scanning arguments.  With error-surfacing enabled
+// the no-'(' cases also report a preprocessor diagnostic.
+TEST(VerilogPreprocessTest, TruncatedCallableMacroDoesNotCrash) {
+  constexpr std::string_view kNoParenInputs[] = {
+      "`define A(x) hello `A\n`A(1)\n",  // truncated callable ref in macro body
+      "`define A(x) x\n`A\n",            // truncated callable ref at top level
+  };
+  for (std::string_view input : kNoParenInputs) {
+    PreprocessorTester tester(
+        input, VerilogPreprocess::Config({.expand_macros = true}));
+    EXPECT_FALSE(tester.Status().ok()) << input;
+    EXPECT_GE(tester.PreprocessorData().errors.size(), 1) << input;
+  }
+
+  // '(' with no matching ')': must terminate (was an infinite loop).  The
+  // residue is rejected downstream, so only assert non-OK here.
+  PreprocessorTester open_paren(
+      "`define C(z) z\n`define A(x) hello `C(\n`A(1)\n",
+      VerilogPreprocess::Config({.expand_macros = true}));
+  EXPECT_FALSE(open_paren.Status().ok());
+}
+
 }  // namespace
 }  // namespace verilog
diff --git a/verible/verilog/tools/preprocessor/verilog-preprocessor.cc b/verible/verilog/tools/preprocessor/verilog-preprocessor.cc
index c23fdf7..af781f3 100644
--- a/verible/verilog/tools/preprocessor/verilog-preprocessor.cc
+++ b/verible/verilog/tools/preprocessor/verilog-preprocessor.cc
@@ -122,6 +122,7 @@
     // source code just like it was, but with conditionals filtered.
     lexed_sequence.push_back(lexer.GetLastToken());
   }
+  lexed_sequence.push_back(lexer.GetLastToken());  // EOF end sentinel
   verible::TokenStreamView lexed_streamview;
   // Initializing the lexed token stream view.
   InitTokenStreamView(lexed_sequence, &lexed_streamview);