Files
Kevin Newton 398c61b5eb [ruby/prism] Scratch arena
There are some constructs that only last for the duration of a
parse, and should no longer be held once the parse is through. For
those we should use a scratch arena, freed at the end of parsing.

https://github.com/ruby/prism/commit/30a255539e
2026-10-24 20:17:09 +00:00

456 lines
17 KiB
C

#include "prism/internal/comments.h"
#include "prism/internal/arena.h"
#include "prism/internal/char.h"
#include "prism/internal/node.h"
#include "prism/internal/parser.h"
#include <assert.h>
#include <stdlib.h>
#include <string.h>
/* The state used while walking the tree to find the owner of each comment. */
typedef struct {
/*
* The arena for memory that is only needed during the walk, which is the
* scratch arena of the parser.
*/
pm_arena_t *arena;
/* The start of the source that was parsed. */
const uint8_t *source;
/* The comments found while parsing, sorted by start offset. */
const pm_comment_t **comments;
/* The number of comments. */
uint32_t size;
/* The start offset of each comment. */
uint32_t *starts;
/*
* The result of the most recent search for the first comment after the end
* of a node. This is a hint to the search to avoid fully binary searching
* the comments list if possible.
*/
uint32_t cursor;
/* The node each comment is attached to. */
const pm_node_t **attached;
/*
* A disjoint set over comment indices that maps each index to the next
* index at or after it that has not yet been claimed. It has one extra
* entry at the end that is never claimed, so that every lookup ends. This
* allows quickly finding the next unclaimed comment without having to check
* the state of each comment individually.
*/
uint32_t *unclaimed;
/*
* A stack of nodes whose children are being visited. Each level of the
* walk pushes its children, visits them, then pops back to where it
* started.
*/
struct {
const pm_node_t **nodes;
size_t size;
size_t capacity;
} stack;
} pm_comments_walk_t;
/* Compare two comments by start offset for qsort. */
static int
pm_comments_compare(const void *left, const void *right) {
uint32_t left_start = (*(const pm_comment_t *const *) left)->location.start;
uint32_t right_start = (*(const pm_comment_t *const *) right)->location.start;
return (left_start > right_start) - (left_start < right_start);
}
/*
* Copy the linked list of comments from the parser sorted by start offset, and
* relink the list in that order. Usually comments arrive in source order, but
* unfortunately because of heredocs they can occasionally be out of order.
*/
static const pm_comment_t **
pm_comments_sort(pm_parser_t *parser, uint32_t size) {
pm_comment_t **comments = (pm_comment_t **) pm_arena_alloc(&parser->metadata_arena, sizeof(pm_comment_t *) * size, PRISM_ALIGNOF(pm_comment_t *));
uint32_t index = 0;
for (pm_list_node_t *node = parser->comment_list.head; node != NULL; node = node->next) {
comments[index++] = (pm_comment_t *) node;
}
qsort((void *) comments, size, sizeof(pm_comment_t *), pm_comments_compare);
for (index = 0; index + 1 < size; index++) {
comments[index]->node.next = &comments[index + 1]->node;
}
comments[size - 1]->node.next = NULL;
parser->comment_list.head = &comments[0]->node;
parser->comment_list.tail = &comments[size - 1]->node;
return (const pm_comment_t **) comments;
}
/*
* Return the index of the first comment in [left, right) that starts at or
* after the given offset, or right if there is none.
*/
static uint32_t
pm_comments_lower_bound(const pm_comments_walk_t *walk, uint32_t left, uint32_t right, uint32_t offset) {
while (left < right) {
uint32_t middle = left + (right - left) / 2;
if (walk->starts[middle] < offset) {
left = middle + 1;
} else {
right = middle;
}
}
return left;
}
/*
* Return the index of the first comment that starts at or after the given
* offset, or the number of comments if there is none. This gallops outward
* from the previous result to find a range to search, so that a run of nearby
* offsets costs a constant number of comparisons each.
*/
static uint32_t
pm_comments_seek(pm_comments_walk_t *walk, uint32_t offset) {
const uint32_t *starts = walk->starts;
uint32_t size = walk->size;
uint32_t cursor = walk->cursor;
uint32_t left;
uint32_t right;
uint32_t step = 1;
if (cursor == size || starts[cursor] >= offset) {
/*
* The answer is at or before the cursor. Widen the range backward
* until the comment before it starts before the offset.
*/
right = cursor;
left = cursor;
while (left > 0 && starts[left - 1] >= offset) {
right = left - 1;
left = (left - 1 > step) ? left - 1 - step : 0;
step *= 2;
}
} else {
/*
* The answer is after the cursor. Widen the range forward until it
* ends at a comment that starts at or after the offset.
*/
left = cursor + 1;
right = left;
while (right < size && starts[right] < offset) {
left = right + 1;
right = (size - left > step) ? left + step : size;
step *= 2;
}
}
walk->cursor = pm_comments_lower_bound(walk, left, right, offset);
return walk->cursor;
}
/*
* The first comment index at or after the given index that has not yet been
* claimed, compressing the path along the way.
*/
static uint32_t
pm_comments_unclaimed(pm_comments_walk_t *walk, uint32_t index) {
uint32_t *unclaimed = walk->unclaimed;
while (unclaimed[index] != index) {
unclaimed[index] = unclaimed[unclaimed[index]];
index = unclaimed[index];
}
return index;
}
/*
* Return the earliest offset before the given one from which the source up to
* it holds only whitespace and comments. Any comment that starts at or after
* the returned offset is separated from the given offset by only whitespace and
* other comments.
*/
static uint32_t
pm_comments_blank_before(const pm_comments_walk_t *walk, uint32_t offset) {
uint32_t index = pm_comments_lower_bound(walk, 0, walk->size, offset);
while (true) {
offset -= (uint32_t) pm_strrspn_whitespace(walk->source, (ptrdiff_t) offset);
if (index == 0) return offset;
/* An embedded document's location includes its trailing newline, so
* the whitespace that was just skipped can end inside a comment. */
const pm_location_t *location = &walk->comments[index - 1]->location;
if (location->start >= offset || PM_LOCATION_END(location) < offset) return offset;
offset = location->start;
index--;
}
}
/*
* Return the latest offset after the given one, and no later than the given
* end, up to which the source from the given offset holds only whitespace and
* comments. Any comment that ends at or before the returned offset is
* separated from the given offset by only whitespace and other comments.
*/
static uint32_t
pm_comments_blank_after(const pm_comments_walk_t *walk, uint32_t offset, uint32_t end) {
uint32_t index = pm_comments_lower_bound(walk, 0, walk->size, offset);
while (true) {
offset += (uint32_t) pm_strspn_whitespace(walk->source + offset, (ptrdiff_t) end - (ptrdiff_t) offset);
if (index == walk->size || walk->starts[index] != offset) return offset;
offset = PM_LOCATION_END(&walk->comments[index]->location);
index++;
}
}
/*
* Push a child node onto the walk stack without descending into it. The walk
* descends itself, so that it can visit children in source order and handle
* each node after all of its descendants.
*/
static pm_visit_result_t
pm_comments_push(const pm_node_t *node, void *data) {
pm_comments_walk_t *walk = (pm_comments_walk_t *) data;
if (walk->stack.size == walk->stack.capacity) {
size_t capacity = walk->stack.capacity == 0 ? 64 : walk->stack.capacity * 2;
const pm_node_t **nodes = (const pm_node_t **) pm_arena_alloc(walk->arena, sizeof(pm_node_t *) * capacity, PRISM_ALIGNOF(pm_node_t *));
if (walk->stack.size > 0) memcpy((void *) nodes, (const void *) walk->stack.nodes, sizeof(pm_node_t *) * walk->stack.size);
walk->stack.nodes = nodes;
walk->stack.capacity = capacity;
}
walk->stack.nodes[walk->stack.size++] = node;
return PM_VISIT_SKIP;
}
/*
* Visit the given node and its descendants in post-order, attaching comments as
* the nodes are visited.
*/
static void
pm_comments_visit(pm_comments_walk_t *walk, const pm_node_t *node) {
/* First, push all of the children of this node onto the stack. */
size_t base = walk->stack.size;
pm_visit_child_nodes(node, pm_comments_push, walk);
size_t top = walk->stack.size;
/* Next, in-place sort the children by their starting location. */
for (size_t index = base + 1; index < top; index++) {
const pm_node_t *child = walk->stack.nodes[index];
size_t insert = index;
while (insert > base && walk->stack.nodes[insert - 1]->location.start > child->location.start) {
walk->stack.nodes[insert] = walk->stack.nodes[insert - 1];
insert--;
}
walk->stack.nodes[insert] = child;
}
/* Next, visit all of the children so that they can claim comments. */
for (size_t index = base; index < top; index++) {
pm_comments_visit(walk, walk->stack.nodes[index]);
}
uint32_t start = node->location.start;
uint32_t end = PM_LOCATION_END(&node->location);
uint32_t after = pm_comments_seek(walk, end);
/* If this node is followed by a comment on the same line, attach it if
* appropriate. Appropriate in this case means if no other node has claimed
* it or if we are closer in distance or equal in distance to the current
* claimant. */
if (
after < walk->size &&
!PM_NODE_TYPE_P(node, PM_PROGRAM_NODE) &&
!PM_NODE_TYPE_P(node, PM_STATEMENTS_NODE) &&
(node->location.length == 0 || walk->source[end - 1] != '\n')
) {
uint32_t comment_start = walk->starts[after];
if (memchr(walk->source + end, '\n', comment_start - end) == NULL) {
const pm_node_t *attached = walk->attached[after];
uint32_t attached_end = attached == NULL ? 0 : PM_LOCATION_END(&attached->location);
if (end >= attached_end || attached_end > comment_start) {
walk->attached[after] = node;
}
}
}
/* Grab up the remaining comments within the range of this node that were
* not claimed by any of its descendants. A comment that is separated by
* only whitespace and other comments from a statements node child, either
* the child after it or the child before it, is attached to the statements
* instead, so that the comments before the first statement and after the
* last statement in a body belong to the body. The claimed comments are in
* order, so the children around each one are found by moving forward
* through the sorted children. */
if (after > 0 && walk->starts[after - 1] >= start) {
uint32_t index = pm_comments_unclaimed(walk, pm_comments_lower_bound(walk, 0, after, start));
size_t child_index = base;
/* The boundaries around the statements children are each found once,
* when the first comment that needs them is reached, since the
* comments that follow reach the same children. */
const pm_node_t *leading = NULL;
uint32_t leading_boundary = 0;
const pm_node_t *trailing = NULL;
uint32_t trailing_boundary = 0;
while (index < after && PM_LOCATION_END(&walk->comments[index]->location) <= end) {
if (walk->attached[index] == NULL) {
uint32_t comment_end = PM_LOCATION_END(&walk->comments[index]->location);
const pm_node_t *attached = node;
while (child_index < top && walk->stack.nodes[child_index]->location.start < comment_end) child_index++;
if (child_index < top) {
const pm_node_t *child = walk->stack.nodes[child_index];
if (PM_NODE_TYPE_P(child, PM_STATEMENTS_NODE)) {
if (child != leading) {
leading = child;
leading_boundary = pm_comments_blank_before(walk, child->location.start);
}
if (walk->starts[index] >= leading_boundary) attached = child;
}
}
if (attached == node && child_index > base) {
const pm_node_t *child = walk->stack.nodes[child_index - 1];
/* A rescue clause ends at its last statement rather than at
* the keyword that follows it, and the first clause in a
* body contains the clauses after it. So a comment after
* the statements of the last clause follows the first
* clause, and belongs to the statements of the last. */
if (PM_NODE_TYPE_P(child, PM_RESCUE_NODE)) {
const pm_rescue_node_t *rescue = (const pm_rescue_node_t *) child;
while (rescue->subsequent != NULL) rescue = rescue->subsequent;
child = (const pm_node_t *) rescue->statements;
}
if (child != NULL && PM_NODE_TYPE_P(child, PM_STATEMENTS_NODE) && PM_LOCATION_END(&child->location) <= walk->starts[index]) {
if (child != trailing) {
trailing = child;
trailing_boundary = pm_comments_blank_after(walk, PM_LOCATION_END(&child->location), end);
}
if (comment_end <= trailing_boundary) attached = child;
}
}
walk->attached[index] = attached;
}
walk->unclaimed[index] = index + 1;
index = pm_comments_unclaimed(walk, index + 1);
}
}
/* Truncate the stack back to the base size. */
walk->stack.size = base;
}
/*
* Sort the parser's comment list in place by start offset, then assign every
* comment to the node that owns it. A comment is owned by:
*
* 1. the node that ends latest on the comment's line before the comment starts,
* with ties going to the node that is higher in the tree;
* 2. otherwise, the innermost node whose location contains the comment, or the
* statements node child of that node if only whitespace and other comments
* come between the statements and the comment, on either side;
* 3. otherwise, the top-level statements node of the program.
*/
void
pm_comments_attach(pm_parser_t *parser, const pm_node_t *root) {
assert(PM_NODE_TYPE_P(root, PM_PROGRAM_NODE));
uint32_t size = (uint32_t) parser->comment_list.size;
if (size == 0) return;
pm_arena_t *arena = &parser->scratch_arena;
pm_comments_walk_t walk = {
.arena = arena,
.source = parser->start,
.comments = pm_comments_sort(parser, size),
.size = size,
.starts = (uint32_t *) pm_arena_alloc(arena, sizeof(uint32_t) * size, PRISM_ALIGNOF(uint32_t)),
.attached = (const pm_node_t **) pm_arena_zalloc(arena, sizeof(pm_node_t *) * size, PRISM_ALIGNOF(pm_node_t *)),
.unclaimed = (uint32_t *) pm_arena_alloc(arena, sizeof(uint32_t) * (size + 1), PRISM_ALIGNOF(uint32_t))
};
for (uint32_t index = 0; index < size; index++) walk.starts[index] = walk.comments[index]->location.start;
for (uint32_t index = 0; index <= size; index++) walk.unclaimed[index] = index;
pm_comments_visit(&walk, root);
/*
* Attach comments that no node attached to during the walk to the
* top-level statements, then group the comments by node with a counting
* sort. Comments are placed in sorted order, so each node's comments stay
* sorted by start offset.
*/
const pm_node_t *statements = (const pm_node_t *) ((const pm_program_node_t *) root)->statements;
uint32_t ranges_size = 0;
for (uint32_t index = 0; index < size; index++) {
if (walk.attached[index] == NULL) walk.attached[index] = statements;
uint32_t node_id = walk.attached[index]->node_id;
assert(node_id <= parser->node_id);
if (node_id >= ranges_size) ranges_size = node_id + 1;
}
/* The ranges only extend to the largest node id that owns a comment, since
* none of the nodes after it own any. */
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));
uint32_t *attachments = (uint32_t *) pm_arena_alloc(&parser->metadata_arena, sizeof(uint32_t) * size, PRISM_ALIGNOF(uint32_t));
for (uint32_t index = 0; index < size; index++) {
ranges[walk.attached[index]->node_id].length++;
}
uint32_t offset = 0;
for (uint32_t index = 0; index < ranges_size; index++) {
ranges[index].index = offset;
offset += ranges[index].length;
ranges[index].length = 0;
}
for (uint32_t index = 0; index < size; index++) {
pm_comment_range_t *range = &ranges[walk.attached[index]->node_id];
attachments[range->index + range->length++] = index;
}
parser->comment_attachments = (pm_comment_attachments_t) {
.comments = walk.comments,
.attachments = attachments,
.ranges = ranges,
.ranges_size = ranges_size
};
}