Skip to content

Commit 02574ca

Browse files
committed
Preprocessor: Add synthetic token for trailing disabled branch trivia;
This was breaking cst json round trip when ifdefs were present. Added another round trip test for that case. This also puts all of the disabled content back under disabledTokens, which is generally easier to work with.
1 parent d69e890 commit 02574ca

8 files changed

Lines changed: 194 additions & 36 deletions

File tree

include/slang/syntax/CSTSerializer.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
#include "slang/syntax/SyntaxNode.h"
1111
#include "slang/text/Json.h"
1212

13+
namespace slang {
14+
class SourceManager;
15+
}
16+
1317
namespace slang::syntax {
1418

1519
class SyntaxTree;
@@ -23,6 +27,16 @@ class SLANG_EXPORT CSTSerializer {
2327
public:
2428
explicit CSTSerializer(JsonWriter& writer, CSTJsonMode mode = CSTJsonMode::Full);
2529

30+
/// Sets a source manager used to annotate tokens that originate from a macro expansion
31+
/// or an included file with a `"fromExpansion": true` property. Such tokens are
32+
/// serialized in addition to the directive that produced them (the macro usage or
33+
/// `include, which occupies the same textual position); this flag lets consumers (e.g.
34+
/// source reconstruction) tell the two apart and avoid double-counting the text.
35+
///
36+
/// When serializing a whole SyntaxTree this is set automatically from the tree; it only
37+
/// needs to be called explicitly when serializing a bare SyntaxNode.
38+
void setSourceManager(const SourceManager* sm) { sourceManager = sm; }
39+
2640
/// Serialize a syntax tree to JSON
2741
void serialize(const SyntaxTree& tree);
2842

@@ -32,6 +46,7 @@ class SLANG_EXPORT CSTSerializer {
3246
private:
3347
JsonWriter& writer;
3448
CSTJsonMode mode;
49+
const SourceManager* sourceManager = nullptr;
3550
};
3651

3752
} // namespace slang::syntax

scripts/reconstruct_from_json.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,30 @@ def extract_tokens(node, tokens):
2121
if node is None:
2222
return
2323

24+
# Text that comes from a macro expansion or an included file is serialized in addition
25+
# to the directive that produced it (which occupies the same source position), so skip
26+
# it to reconstruct the original, unexpanded source. This flag is set on both tokens
27+
# and their plain-text trivia. Note directive trivia carry no "text" of their own, so
28+
# even when they sit inside expanded content they are still recursed into below (the
29+
# producing `include / macro usage lives there).
30+
expanded = isinstance(node, dict) and node.get("fromExpansion")
31+
2432
if isinstance(node, dict):
2533
# Check if this is a token node
2634
if "kind" in node and "text" in node:
27-
# This is a token - add its text and trivia
35+
# Always process trivia first (it may itself contain directive syntax, e.g.
36+
# `ifdef branches whose disabled tokens hang off the directive, or the
37+
# `include / macro usage that produced an expansion) followed by the token's
38+
# own text.
2839
if "trivia" in node:
2940
for trivia in node["trivia"]:
30-
if "text" in trivia:
31-
tokens.append(trivia["text"])
32-
tokens.append(node["text"])
41+
extract_tokens(trivia, tokens)
42+
if not expanded:
43+
tokens.append(node["text"])
3344
else:
34-
# This is a syntax node - recurse through all properties
45+
# This is a syntax node (or a piece of trivia) - recurse through all
46+
# properties. This covers directive trivia, whose content lives under a
47+
# nested "syntax" node rather than a flat "text" field.
3548
for key, value in node.items():
3649
if key in ["kind"]: # Skip metadata
3750
continue

source/parsing/Preprocessor.cpp

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
#include "slang/diagnostics/LexerDiags.h"
1111
#include "slang/diagnostics/PreprocessorDiags.h"
12+
#include "slang/parsing/TokenKind.h"
1213
#include "slang/syntax/AllSyntax.h"
1314
#include "slang/text/Glob.h"
1415
#include "slang/text/SourceManager.h"
@@ -988,25 +989,14 @@ Trivia Preprocessor::parseBranchDirective(Token directive,
988989
}
989990

990991
if (done) {
991-
// Put the token back so that we'll look at it next, but with
992-
// its trivia rewritten to change comments and whitespace to disabled
993-
// text, since this branch was not taken and we want the comments to
994-
// disappear in the preprocessed output.
995-
SmallVector<Trivia, 2> trivia(token.trivia());
996-
for (auto& t : trivia) {
997-
switch (t.kind) {
998-
case TriviaKind::LineComment:
999-
case TriviaKind::BlockComment:
1000-
case TriviaKind::Whitespace:
1001-
case TriviaKind::EndOfLine:
1002-
t.kind = TriviaKind::DisabledText;
1003-
break;
1004-
default:
1005-
break;
1006-
}
1007-
}
1008992

1009-
currentToken = token.withTrivia(alloc, trivia);
993+
// Make a synthetic token for holding the trailing trivia, to keep it still
994+
// classified as disabled text.
995+
scratchTokenBuffer.push_back(
996+
Token::createMissing(alloc, TokenKind::Placeholder, token.location())
997+
.withTrivia(alloc, token.trivia()));
998+
999+
currentToken = token.withTrivia(alloc, {});
10101000
break;
10111001
}
10121002
scratchTokenBuffer.push_back(token);

source/syntax/CSTSerializer.cpp

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "slang/syntax/AllSyntax.h"
1616
#include "slang/syntax/SyntaxTree.h"
1717
#include "slang/syntax/SyntaxVisitor.h"
18+
#include "slang/text/SourceManager.h"
1819
#include "slang/util/Util.h"
1920

2021
namespace slang::syntax {
@@ -23,6 +24,11 @@ CSTSerializer::CSTSerializer(JsonWriter& writer, CSTJsonMode mode) : writer(writ
2324
}
2425

2526
void CSTSerializer::serialize(const SyntaxTree& tree) {
27+
// A tree always knows its source manager, so use it (unless one was set explicitly)
28+
// to annotate macro-expanded tokens.
29+
if (!sourceManager)
30+
sourceManager = &tree.sourceManager();
31+
2632
writer.startObject();
2733
writer.writeProperty("kind");
2834
writer.writeValue("SyntaxTree"sv);
@@ -37,8 +43,10 @@ struct always_false : std::false_type {};
3743
struct CSTJsonVisitor {
3844
JsonWriter& writer;
3945
CSTJsonMode mode;
46+
const SourceManager* sourceManager;
4047

41-
CSTJsonVisitor(JsonWriter& w, CSTJsonMode m) : writer(w), mode(m) {}
48+
CSTJsonVisitor(JsonWriter& w, CSTJsonMode m, const SourceManager* sm) :
49+
writer(w), mode(m), sourceManager(sm) {}
4250

4351
template<std::derived_from<SyntaxNode> T>
4452
void visit(const T& node) {
@@ -143,10 +151,33 @@ struct CSTJsonVisitor {
143151
writer.endArray();
144152
}
145153

146-
void writeTrivia(parsing::Trivia trivia) {
154+
// Returns true if the given source location comes from a macro expansion or an
155+
// included file, i.e. text that is serialized in addition to the directive that
156+
// produced it and so should not be double-counted when reconstructing source.
157+
bool isExpandedLoc(SourceLocation loc) const {
158+
return sourceManager &&
159+
(sourceManager->isMacroLoc(loc) || sourceManager->isIncludedFileLoc(loc));
160+
}
161+
162+
// @a parentExpanded is the expansion state of the token this trivia belongs to; plain
163+
// text trivia has no location of its own and so inherits it.
164+
void writeTrivia(parsing::Trivia trivia, bool parentExpanded) {
147165
writer.startObject();
148166
writer.writeProperty("kind");
149167
writer.writeValue(toString(trivia.kind));
168+
169+
// Flag trivia that is itself expanded text. Directive/skipped trivia carry their
170+
// own location; plain text trivia inherits the parent token's expansion state.
171+
bool expanded = parentExpanded;
172+
if (sourceManager) {
173+
if (auto loc = trivia.getExplicitLocation())
174+
expanded = isExpandedLoc(*loc);
175+
}
176+
if (expanded) {
177+
writer.writeProperty("fromExpansion");
178+
writer.writeValue(true);
179+
}
180+
150181
switch (trivia.kind) {
151182
case parsing::TriviaKind::Directive:
152183
case parsing::TriviaKind::SkippedSyntax:
@@ -181,14 +212,26 @@ struct CSTJsonVisitor {
181212
writer.writeProperty("text");
182213
writer.writeValue(token.rawText());
183214

215+
// Flag tokens that come from a macro expansion or an included file. Such tokens
216+
// are serialized in addition to the directive that produced them -- the macro
217+
// usage, or the `include -- which occupies the same textual position, so consumers
218+
// reconstructing the original source can skip them to avoid double-counting. The
219+
// same flag is propagated to the token's plain-text trivia, which has no location
220+
// of its own.
221+
bool expanded = isExpandedLoc(token.location());
222+
if (expanded) {
223+
writer.writeProperty("fromExpansion");
224+
writer.writeValue(true);
225+
}
226+
184227
// Handle trivia based on mode
185228
if (!token.trivia().empty()) {
186229
switch (mode) {
187230
case CSTJsonMode::Full:
188231
writer.writeProperty("trivia");
189232
writer.startArray();
190233
for (auto& t : token.trivia())
191-
writeTrivia(t);
234+
writeTrivia(t, expanded);
192235
writer.endArray();
193236
break;
194237
case CSTJsonMode::NoWhitespace: {
@@ -211,7 +254,7 @@ struct CSTJsonVisitor {
211254
writer.writeProperty("trivia");
212255
writer.startArray();
213256
for (auto& t : filtered)
214-
writeTrivia(t);
257+
writeTrivia(t, expanded);
215258
writer.endArray();
216259
}
217260
break;
@@ -238,7 +281,7 @@ struct CSTJsonVisitor {
238281
};
239282

240283
void CSTSerializer::serialize(const SyntaxNode& node) {
241-
CSTJsonVisitor visitor(writer, mode);
284+
CSTJsonVisitor visitor(writer, mode, sourceManager);
242285
node.visit(visitor);
243286
}
244287

tests/regression/CMakeLists.txt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,48 @@ add_test(
2424
set_tests_properties(regression_cst_json_roundtrip
2525
PROPERTIES DEPENDS regression_cst_json_gen)
2626

27+
# CST JSON round trip test focused on preprocessor conditional directives, whose disabled
28+
# branches exercise the trivia handling that plain source (all.sv) doesn't cover.
29+
add_test(
30+
NAME regression_cst_json_conditionals_gen
31+
COMMAND slang::driver --cst-json ${CMAKE_CURRENT_BINARY_DIR}/conditionals_cst.json
32+
${CMAKE_CURRENT_LIST_DIR}/conditionals.sv "--std=1800-2023")
33+
34+
add_test(
35+
NAME regression_cst_json_conditionals_roundtrip
36+
COMMAND
37+
${Python_EXECUTABLE} ${PROJECT_SOURCE_DIR}/scripts/reconstruct_from_json.py
38+
${CMAKE_CURRENT_BINARY_DIR}/conditionals_cst.json --compare
39+
${CMAKE_CURRENT_LIST_DIR}/conditionals.sv)
40+
41+
set_tests_properties(regression_cst_json_conditionals_roundtrip
42+
PROPERTIES DEPENDS regression_cst_json_conditionals_gen)
43+
44+
# Exercise the same source with different conditional branches enabled. Each variant writes
45+
# to a separate file so that the generation tests can run concurrently.
46+
function(add_cst_json_conditionals_variant name)
47+
set(json_file ${CMAKE_CURRENT_BINARY_DIR}/conditionals_${name}_cst.json)
48+
49+
add_test(
50+
NAME regression_cst_json_conditionals_${name}_gen
51+
COMMAND slang::driver --cst-json ${json_file}
52+
${CMAKE_CURRENT_LIST_DIR}/conditionals.sv "--std=1800-2023" ${ARGN})
53+
54+
add_test(
55+
NAME regression_cst_json_conditionals_${name}_roundtrip
56+
COMMAND
57+
${Python_EXECUTABLE} ${PROJECT_SOURCE_DIR}/scripts/reconstruct_from_json.py
58+
${json_file} --compare ${CMAKE_CURRENT_LIST_DIR}/conditionals.sv)
59+
60+
set_tests_properties(
61+
regression_cst_json_conditionals_${name}_roundtrip
62+
PROPERTIES DEPENDS regression_cst_json_conditionals_${name}_gen)
63+
endfunction()
64+
65+
add_cst_json_conditionals_variant(foo -DFOO)
66+
add_cst_json_conditionals_variant(bar_baz -DBAR -DBAZ)
67+
add_cst_json_conditionals_variant(foo_inner -DFOO -DINNER)
68+
2769
add_subdirectory(driver)
2870

2971
if(SLANG_INCLUDE_UVM_TEST)

tests/regression/conditionals.sv

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Exercises preprocessor conditional directives so the CST JSON round-trip covers the
2+
// trivia handling of disabled branches (line breaks, indentation, and comments that end
3+
// a skipped branch must still round-trip faithfully).
4+
5+
module m;
6+
`ifdef FOO
7+
logic disabled_simple;
8+
`endif
9+
10+
`ifndef FOO
11+
logic taken_simple;
12+
`else
13+
logic disabled_else;
14+
`endif
15+
16+
`ifdef FOO
17+
logic a;
18+
`elsif BAR
19+
logic b;
20+
`else
21+
logic c;
22+
`endif
23+
24+
`ifndef FOO
25+
// taken branch with a nested disabled block
26+
`ifdef BAZ
27+
logic nested_disabled;
28+
`else
29+
logic nested_taken;
30+
`endif
31+
`endif
32+
33+
`ifdef FOO
34+
// disabled branch containing comments and a nested pair
35+
logic x; // trailing comment
36+
`ifdef INNER
37+
logic y;
38+
`endif
39+
// dangling comment before endif
40+
`endif
41+
42+
// A macro whose body itself contains conditional directives. In the FOO-defined test
43+
// variants its invocation produces expanded directive trivia that must not be
44+
// double-counted when reconstructing the original source.
45+
`define GUARD(name) \
46+
`ifndef name \
47+
`define name \
48+
`endif
49+
50+
`ifdef FOO
51+
`GUARD(SOMETHING)
52+
`endif
53+
54+
endmodule

tests/regression/driver/cst-json-modes.sv

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,24 @@
1616
// RUN: echo CST-MODE-simple-tokens && %slang --cst-json - --cst-json-mode simple-tokens --parse-only %data/cst_serializer_modes.sv || true
1717

1818
// "full" mode keeps every trivia kind verbatim, including whitespace, the
19-
// disabled `ifdef text, comments, directives and skipped tokens.
19+
// disabled `ifdef branch (whose tokens are carried as disabled tokens ending in
20+
// a Placeholder), comments, directives and skipped tokens.
2021
// CHECK-LABEL: CST-MODE-full
2122
// CHECK-DAG: "kind": "Whitespace"
22-
// CHECK-DAG: "kind": "DisabledText"
23+
// CHECK-DAG: "kind": "Placeholder"
2324
// CHECK-DAG: "kind": "Directive"
2425
// CHECK-DAG: "kind": "LineComment"
2526
// CHECK-DAG: "kind": "BlockComment"
2627
// CHECK-DAG: "kind": "SkippedTokens"
2728

28-
// "no-whitespace" mode drops Whitespace/EndOfLine (and whitespace-only
29-
// DisabledText) trivia but preserves comments, directives and skipped tokens.
29+
// "no-whitespace" mode drops Whitespace/EndOfLine trivia but preserves comments,
30+
// directives and skipped tokens.
3031
// CHECK-LABEL: CST-MODE-no-whitespace
3132
// CHECK-DAG: "kind": "Directive"
3233
// CHECK-DAG: "kind": "LineComment"
3334
// CHECK-DAG: "kind": "BlockComment"
3435
// CHECK-DAG: "kind": "SkippedTokens"
3536
// CHECK-NOT: "kind": "Whitespace"
36-
// CHECK-NOT: "kind": "DisabledText"
3737

3838
// "simple-trivia" mode emits the trivia of each token as a single concatenated
3939
// string value instead of an array of structured trivia objects.

tests/unittests/data/cst_serializer_modes.sv

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33

44
// Fixture for tests/regression/driver/cst-json-modes.sv. It is serialized via
55
// `--cst-json` in every CSTJsonMode. It intentionally contains a macro
6-
// definition, an untaken `ifdef branch (which produces DisabledText trivia),
7-
// line and block comments, and a stray token after `endmodule` (which becomes
8-
// SkippedTokens trivia) so that the directive, disabled-text and skipped-token
6+
// definition, an untaken `ifdef branch (whose skipped tokens are carried as the
7+
// directive's disabled tokens, terminated by a Placeholder token), line and
8+
// block comments, and a stray token after `endmodule` (which becomes
9+
// SkippedTokens trivia) so that the directive, disabled-token and skipped-token
910
// serialization paths all run.
1011

1112
`define WIDTH 8

0 commit comments

Comments
 (0)