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