Ruby 4.1.0dev (2026-11-10 revision 09658e85dc51184db6df11e142dc3eeabfdbdcd0)
comments.c
1#include "prism/internal/comments.h"
2
3#include "prism/internal/arena.h"
4#include "prism/internal/assert.h"
5#include "prism/internal/char.h"
6#include "prism/internal/node.h"
7#include "prism/internal/parser.h"
8
9#include <stdlib.h>
10#include <string.h>
11
12/* The state used while walking the tree to find the owner of each comment. */
13typedef struct {
14 /*
15 * The arena for memory that is only needed during the walk, which is the
16 * scratch arena of the parser.
17 */
18 pm_arena_t *arena;
19
20 /* The start of the source that was parsed. */
21 const uint8_t *source;
22
23 /* The comments found while parsing, sorted by start offset. */
24 const pm_comment_t **comments;
25
26 /* The number of comments. */
27 uint32_t size;
28
29 /* The start offset of each comment. */
30 uint32_t *starts;
31
32 /*
33 * The result of the most recent search for the first comment after the end
34 * of a node. This is a hint to the search to avoid fully binary searching
35 * the comments list if possible.
36 */
37 uint32_t cursor;
38
39 /* The node each comment is attached to. */
40 const pm_node_t **attached;
41
42 /*
43 * A disjoint set over comment indices that maps each index to the next
44 * index at or after it that has not yet been claimed. It has one extra
45 * entry at the end that is never claimed, so that every lookup ends. This
46 * allows quickly finding the next unclaimed comment without having to check
47 * the state of each comment individually.
48 */
49 uint32_t *unclaimed;
50
51 /*
52 * A stack of nodes whose children are being visited. Each level of the
53 * walk pushes its children, visits them, then pops back to where it
54 * started.
55 */
56 struct {
57 const pm_node_t **nodes;
58 size_t size;
59 size_t capacity;
60 } stack;
62
63/* Compare two comments by start offset for qsort. */
64static int
65pm_comments_compare(const void *left, const void *right) {
66 uint32_t left_start = (*(const pm_comment_t *const *) left)->location.start;
67 uint32_t right_start = (*(const pm_comment_t *const *) right)->location.start;
68 return (left_start > right_start) - (left_start < right_start);
69}
70
71/*
72 * Copy the linked list of comments from the parser sorted by start offset, and
73 * relink the list in that order. Usually comments arrive in source order, but
74 * unfortunately because of heredocs they can occasionally be out of order.
75 */
76static const pm_comment_t **
77pm_comments_sort(pm_parser_t *parser, uint32_t size) {
78 pm_comment_t **comments = (pm_comment_t **) pm_arena_alloc(&parser->metadata_arena, sizeof(pm_comment_t *) * size, PRISM_ALIGNOF(pm_comment_t *));
79
80 uint32_t index = 0;
81 for (pm_list_node_t *node = parser->comment_list.head; node != NULL; node = node->next) {
82 comments[index++] = (pm_comment_t *) node;
83 }
84
85 qsort((void *) comments, size, sizeof(pm_comment_t *), pm_comments_compare);
86
87 for (index = 0; index + 1 < size; index++) {
88 comments[index]->node.next = &comments[index + 1]->node;
89 }
90
91 comments[size - 1]->node.next = NULL;
92 parser->comment_list.head = &comments[0]->node;
93 parser->comment_list.tail = &comments[size - 1]->node;
94
95 return (const pm_comment_t **) comments;
96}
97
98/*
99 * Return the index of the first comment in [left, right) that starts at or
100 * after the given offset, or right if there is none.
101 */
102static uint32_t
103pm_comments_lower_bound(const pm_comments_walk_t *walk, uint32_t left, uint32_t right, uint32_t offset) {
104 while (left < right) {
105 uint32_t middle = left + (right - left) / 2;
106
107 if (walk->starts[middle] < offset) {
108 left = middle + 1;
109 } else {
110 right = middle;
111 }
112 }
113
114 return left;
115}
116
117/*
118 * Return the index of the first comment that starts at or after the given
119 * offset, or the number of comments if there is none. This gallops outward
120 * from the previous result to find a range to search, so that a run of nearby
121 * offsets costs a constant number of comparisons each.
122 */
123static uint32_t
124pm_comments_seek(pm_comments_walk_t *walk, uint32_t offset) {
125 const uint32_t *starts = walk->starts;
126
127 uint32_t size = walk->size;
128 uint32_t cursor = walk->cursor;
129 uint32_t left;
130 uint32_t right;
131 uint32_t step = 1;
132
133 if (cursor == size || starts[cursor] >= offset) {
134 /*
135 * The answer is at or before the cursor. Widen the range backward
136 * until the comment before it starts before the offset.
137 */
138 right = cursor;
139 left = cursor;
140
141 while (left > 0 && starts[left - 1] >= offset) {
142 right = left - 1;
143 left = (left - 1 > step) ? left - 1 - step : 0;
144 step *= 2;
145 }
146 } else {
147 /*
148 * The answer is after the cursor. Widen the range forward until it
149 * ends at a comment that starts at or after the offset.
150 */
151 left = cursor + 1;
152 right = left;
153
154 while (right < size && starts[right] < offset) {
155 left = right + 1;
156 right = (size - left > step) ? left + step : size;
157 step *= 2;
158 }
159 }
160
161 walk->cursor = pm_comments_lower_bound(walk, left, right, offset);
162 return walk->cursor;
163}
164
165/*
166 * The first comment index at or after the given index that has not yet been
167 * claimed, compressing the path along the way.
168 */
169static uint32_t
170pm_comments_unclaimed(pm_comments_walk_t *walk, uint32_t index) {
171 uint32_t *unclaimed = walk->unclaimed;
172
173 while (unclaimed[index] != index) {
174 unclaimed[index] = unclaimed[unclaimed[index]];
175 index = unclaimed[index];
176 }
177
178 return index;
179}
180
181/*
182 * Return the earliest offset before the given one from which the source up to
183 * it holds only whitespace and comments. Any comment that starts at or after
184 * the returned offset is separated from the given offset by only whitespace and
185 * other comments.
186 */
187static uint32_t
188pm_comments_blank_before(const pm_comments_walk_t *walk, uint32_t offset) {
189 uint32_t index = pm_comments_lower_bound(walk, 0, walk->size, offset);
190
191 while (true) {
192 offset -= (uint32_t) pm_strrspn_whitespace(walk->source, (ptrdiff_t) offset);
193 if (index == 0) return offset;
194
195 /* An embedded document's location includes its trailing newline, so
196 * the whitespace that was just skipped can end inside a comment. */
197 const pm_location_t *location = &walk->comments[index - 1]->location;
198 if (location->start >= offset || PM_LOCATION_END(location) < offset) return offset;
199
200 offset = location->start;
201 index--;
202 }
203}
204
205/*
206 * Return the latest offset after the given one, and no later than the given
207 * end, up to which the source from the given offset holds only whitespace and
208 * comments. Any comment that ends at or before the returned offset is
209 * separated from the given offset by only whitespace and other comments.
210 */
211static uint32_t
212pm_comments_blank_after(const pm_comments_walk_t *walk, uint32_t offset, uint32_t end) {
213 uint32_t index = pm_comments_lower_bound(walk, 0, walk->size, offset);
214
215 while (true) {
216 offset += (uint32_t) pm_strspn_whitespace(walk->source + offset, (ptrdiff_t) end - (ptrdiff_t) offset);
217 if (index == walk->size || walk->starts[index] != offset) return offset;
218
219 offset = PM_LOCATION_END(&walk->comments[index]->location);
220 index++;
221 }
222}
223
224/*
225 * Push a child node onto the walk stack without descending into it. The walk
226 * descends itself, so that it can visit children in source order and handle
227 * each node after all of its descendants.
228 */
230pm_comments_push(const pm_node_t *node, void *data) {
231 pm_comments_walk_t *walk = (pm_comments_walk_t *) data;
232
233 if (walk->stack.size == walk->stack.capacity) {
234 size_t capacity = walk->stack.capacity == 0 ? 64 : walk->stack.capacity * 2;
235 const pm_node_t **nodes = (const pm_node_t **) pm_arena_alloc(walk->arena, sizeof(pm_node_t *) * capacity, PRISM_ALIGNOF(pm_node_t *));
236 if (walk->stack.size > 0) memcpy((void *) nodes, (const void *) walk->stack.nodes, sizeof(pm_node_t *) * walk->stack.size);
237
238 walk->stack.nodes = nodes;
239 walk->stack.capacity = capacity;
240 }
241
242 walk->stack.nodes[walk->stack.size++] = node;
243 return PM_VISIT_SKIP;
244}
245
246/*
247 * Visit the given node and its descendants in post-order, attaching comments as
248 * the nodes are visited.
249 */
250static void
251pm_comments_visit(pm_comments_walk_t *walk, const pm_node_t *node) {
252 /* First, push all of the children of this node onto the stack. */
253 size_t base = walk->stack.size;
254 pm_visit_child_nodes(node, pm_comments_push, walk);
255 size_t top = walk->stack.size;
256
257 /* Next, in-place sort the children by their starting location. */
258 for (size_t index = base + 1; index < top; index++) {
259 const pm_node_t *child = walk->stack.nodes[index];
260 size_t insert = index;
261
262 while (insert > base && walk->stack.nodes[insert - 1]->location.start > child->location.start) {
263 walk->stack.nodes[insert] = walk->stack.nodes[insert - 1];
264 insert--;
265 }
266
267 walk->stack.nodes[insert] = child;
268 }
269
270 /* Next, visit all of the children so that they can claim comments. */
271 for (size_t index = base; index < top; index++) {
272 pm_comments_visit(walk, walk->stack.nodes[index]);
273 }
274
275 uint32_t start = node->location.start;
276 uint32_t end = PM_LOCATION_END(&node->location);
277 uint32_t after = pm_comments_seek(walk, end);
278
279 /* If this node is followed by a comment on the same line, attach it if
280 * appropriate. Appropriate in this case means if no other node has claimed
281 * it or if we are closer in distance or equal in distance to the current
282 * claimant. */
283 if (
284 after < walk->size &&
285 !PM_NODE_TYPE_P(node, PM_PROGRAM_NODE) &&
286 !PM_NODE_TYPE_P(node, PM_STATEMENTS_NODE) &&
287 (node->location.length == 0 || walk->source[end - 1] != '\n')
288 ) {
289 uint32_t comment_start = walk->starts[after];
290
291 if (memchr(walk->source + end, '\n', comment_start - end) == NULL) {
292 const pm_node_t *attached = walk->attached[after];
293 uint32_t attached_end = attached == NULL ? 0 : PM_LOCATION_END(&attached->location);
294
295 if (end >= attached_end || attached_end > comment_start) {
296 walk->attached[after] = node;
297 }
298 }
299 }
300
301 /* Grab up the remaining comments within the range of this node that were
302 * not claimed by any of its descendants. A comment that is separated by
303 * only whitespace and other comments from a statements node child, either
304 * the child after it or the child before it, is attached to the statements
305 * instead, so that the comments before the first statement and after the
306 * last statement in a body belong to the body. The claimed comments are in
307 * order, so the children around each one are found by moving forward
308 * through the sorted children. */
309 if (after > 0 && walk->starts[after - 1] >= start) {
310 uint32_t index = pm_comments_unclaimed(walk, pm_comments_lower_bound(walk, 0, after, start));
311 size_t child_index = base;
312
313 /* The boundaries around the statements children are each found once,
314 * when the first comment that needs them is reached, since the
315 * comments that follow reach the same children. */
316 const pm_node_t *leading = NULL;
317 uint32_t leading_boundary = 0;
318 const pm_node_t *trailing = NULL;
319 uint32_t trailing_boundary = 0;
320
321 while (index < after && PM_LOCATION_END(&walk->comments[index]->location) <= end) {
322 if (walk->attached[index] == NULL) {
323 uint32_t comment_end = PM_LOCATION_END(&walk->comments[index]->location);
324 const pm_node_t *attached = node;
325
326 while (child_index < top && walk->stack.nodes[child_index]->location.start < comment_end) child_index++;
327
328 if (child_index < top) {
329 const pm_node_t *child = walk->stack.nodes[child_index];
330
331 if (PM_NODE_TYPE_P(child, PM_STATEMENTS_NODE)) {
332 if (child != leading) {
333 leading = child;
334 leading_boundary = pm_comments_blank_before(walk, child->location.start);
335 }
336
337 if (walk->starts[index] >= leading_boundary) attached = child;
338 }
339 }
340
341 if (attached == node && child_index > base) {
342 const pm_node_t *child = walk->stack.nodes[child_index - 1];
343
344 /* A rescue clause ends at its last statement rather than at
345 * the keyword that follows it, and the first clause in a
346 * body contains the clauses after it. So a comment after
347 * the statements of the last clause follows the first
348 * clause, and belongs to the statements of the last. */
349 if (PM_NODE_TYPE_P(child, PM_RESCUE_NODE)) {
350 const pm_rescue_node_t *rescue = (const pm_rescue_node_t *) child;
351 while (rescue->subsequent != NULL) rescue = rescue->subsequent;
352 child = (const pm_node_t *) rescue->statements;
353 }
354
355 if (child != NULL && PM_NODE_TYPE_P(child, PM_STATEMENTS_NODE) && PM_LOCATION_END(&child->location) <= walk->starts[index]) {
356 if (child != trailing) {
357 trailing = child;
358 trailing_boundary = pm_comments_blank_after(walk, PM_LOCATION_END(&child->location), end);
359 }
360
361 if (comment_end <= trailing_boundary) attached = child;
362 }
363 }
364
365 walk->attached[index] = attached;
366 }
367
368 walk->unclaimed[index] = index + 1;
369 index = pm_comments_unclaimed(walk, index + 1);
370 }
371 }
372
373 /* Truncate the stack back to the base size. */
374 walk->stack.size = base;
375}
376
377/*
378 * Sort the parser's comment list in place by start offset, then assign every
379 * comment to the node that owns it. A comment is owned by:
380 *
381 * 1. the node that ends latest on the comment's line before the comment starts,
382 * with ties going to the node that is higher in the tree;
383 * 2. otherwise, the innermost node whose location contains the comment, or the
384 * statements node child of that node if only whitespace and other comments
385 * come between the statements and the comment, on either side;
386 * 3. otherwise, the top-level statements node of the program.
387 */
388void
389pm_comments_attach(pm_parser_t *parser, const pm_node_t *root) {
390 PRISM_ASSERT(PM_NODE_TYPE_P(root, PM_PROGRAM_NODE));
391
392 uint32_t size = (uint32_t) parser->comment_list.size;
393 if (size == 0) return;
394
395 pm_arena_t *arena = &parser->scratch_arena;
396 pm_comments_walk_t walk = {
397 .arena = arena,
398 .source = parser->start,
399 .comments = pm_comments_sort(parser, size),
400 .size = size,
401 .starts = (uint32_t *) pm_arena_alloc(arena, sizeof(uint32_t) * size, PRISM_ALIGNOF(uint32_t)),
402 .attached = (const pm_node_t **) pm_arena_zalloc(arena, sizeof(pm_node_t *) * size, PRISM_ALIGNOF(pm_node_t *)),
403 .unclaimed = (uint32_t *) pm_arena_alloc(arena, sizeof(uint32_t) * (size + 1), PRISM_ALIGNOF(uint32_t))
404 };
405
406 for (uint32_t index = 0; index < size; index++) walk.starts[index] = walk.comments[index]->location.start;
407 for (uint32_t index = 0; index <= size; index++) walk.unclaimed[index] = index;
408
409 pm_comments_visit(&walk, root);
410
411 /*
412 * Attach comments that no node attached to during the walk to the
413 * top-level statements, then group the comments by node with a counting
414 * sort. Comments are placed in sorted order, so each node's comments stay
415 * sorted by start offset.
416 */
417 const pm_node_t *statements = (const pm_node_t *) ((const pm_program_node_t *) root)->statements;
418 uint32_t ranges_size = 0;
419
420 for (uint32_t index = 0; index < size; index++) {
421 if (walk.attached[index] == NULL) walk.attached[index] = statements;
422
423 uint32_t node_id = walk.attached[index]->node_id;
424 PRISM_ASSERT(node_id <= parser->node_id);
425 if (node_id >= ranges_size) ranges_size = node_id + 1;
426 }
427
428 /* The ranges only extend to the largest node id that owns a comment, since
429 * none of the nodes after it own any. */
430 pm_comment_range_t *ranges = (pm_comment_range_t *) pm_arena_zalloc(&parser->metadata_arena, sizeof(pm_comment_range_t) * ranges_size, PRISM_ALIGNOF(pm_comment_range_t));
431 uint32_t *attachments = (uint32_t *) pm_arena_alloc(&parser->metadata_arena, sizeof(uint32_t) * size, PRISM_ALIGNOF(uint32_t));
432
433 for (uint32_t index = 0; index < size; index++) {
434 ranges[walk.attached[index]->node_id].length++;
435 }
436
437 uint32_t offset = 0;
438 for (uint32_t index = 0; index < ranges_size; index++) {
439 ranges[index].index = offset;
440 offset += ranges[index].length;
441 ranges[index].length = 0;
442 }
443
444 for (uint32_t index = 0; index < size; index++) {
445 pm_comment_range_t *range = &ranges[walk.attached[index]->node_id];
446 attachments[range->index + range->length++] = index;
447 }
448
449 parser->comment_attachments = (pm_comment_attachments_t) {
450 .comments = walk.comments,
451 .attachments = attachments,
452 .ranges = ranges,
453 .ranges_size = ranges_size
454 };
455}
#define PRISM_ALIGNOF
Get the alignment requirement of a type.
Definition align.h:15
comment_range comments(const pm_parser_t *parser PRISM_LIFETIMEBOUND) noexcept
The comments found while parsing, in source order.
Definition parser.hpp:257
pm_visit_result_t
The value returned by a visitor callback, which determines where the walk goes next.
Definition node.h:49
@ PM_VISIT_SKIP
Continue the walk without visiting this node's children.
Definition node.h:51
The comments owned by a node, as a slice of the comment attachments of the parser that produced it.
Definition comments.h:34
uint32_t index
The index of the first attachment owned by the node.
Definition comments.h:36
uint32_t length
The number of attachments owned by the node.
Definition comments.h:39
This struct represents a slice in the source code, defined by an offset and a length.
Definition ast.h:579
uint32_t start
The offset of the location from the start of the source.
Definition ast.h:581
uint32_t length
The length of the location.
Definition ast.h:584
This is the base structure that represents a node in the syntax tree.
Definition ast.h:1090
uint32_t node_id
The unique identifier for this node, which is deterministic based on the source.
Definition ast.h:1109
pm_location_t location
This is the location of the node in the source.
Definition ast.h:1115
ProgramNode.
Definition ast.h:6848
RescueNode.
Definition ast.h:7134
PM_NODE_ALIGNAS struct pm_statements_node * statements
RescueNode::statements.
Definition ast.h:7166
PM_NODE_ALIGNAS struct pm_rescue_node * subsequent
RescueNode::subsequent.
Definition ast.h:7171