Skip to content

Commit 3dbe592

Browse files
committed
Split up newline token
There are actually 3 tokens in the Ruby grammar: the newline in a whitespace insensitive position that is ignored, a newline that is in a whitespace sensitive position that acts as a statement terminator, and a newline in a whitespace sensisitive position that acts as the terminator for an expression. The third one doesn't exist in our grammar at the moment, but I want to add it, because it makes working with translating the lex output easier.
1 parent b8c5a88 commit 3dbe592

6 files changed

Lines changed: 68 additions & 48 deletions

File tree

config.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,8 @@ tokens:
359359
comment: "when"
360360
- name: NEWLINE
361361
comment: "a newline character outside of other tokens"
362+
- name: NEWLINE_TERMINATOR
363+
comment: "a newline that terminates a construct where a newline is otherwise insignificant"
362364
- name: PARENTHESIS_RIGHT
363365
comment: ")"
364366
- name: PIPE

lib/prism/lex_compat.rb

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ def deconstruct_keys(keys) # :nodoc:
191191
MINUS_EQUAL: :on_op,
192192
MINUS_GREATER: :on_tlambda,
193193
NEWLINE: :on_nl,
194+
NEWLINE_TERMINATOR: :on_ignored_nl,
194195
NUMBERED_REFERENCE: :on_backref,
195196
PARENTHESIS_LEFT: :on_lparen,
196197
PARENTHESIS_LEFT_GROUPING: :on_lparen,
@@ -617,6 +618,9 @@ def result
617618

618619
bom = source.slice(0, 3) == "\xEF\xBB\xBF"
619620

621+
last_comment_token = nil #: lex_compat_token?
622+
last_comment_end = nil #: Integer?
623+
620624
result_value.each_with_index do |(prism_token, prism_state), index|
621625
lineno = prism_token.location.start_line
622626
column = prism_token.location.start_column
@@ -625,6 +629,16 @@ def result
625629
value = prism_token.value
626630
lex_state = Translation::Ripper::Lexer::State[prism_state]
627631

632+
# A comment token does not include its terminating newline, but
633+
# ripper's comment value does, so the newline token that directly
634+
# follows a comment is folded back into it.
635+
if last_comment_token && last_comment_end == prism_token.location.start_offset && (event == :on_nl || event == :on_ignored_nl)
636+
last_comment_token[2] += value
637+
last_comment_token = nil
638+
last_comment_end = nil
639+
next
640+
end
641+
628642
# If there's a UTF-8 byte-order mark as the start of the file, then for
629643
# certain tokens ripper sets the first token back by 3 bytes. It also
630644
# keeps the byte order mark in the first token's value. This is weird,
@@ -714,11 +728,16 @@ def result
714728
eof_token = prism_token
715729
previous_token = result_value[index - 1][0]
716730

731+
# A newline that was folded back into a comment still marks the
732+
# comment boundary for the check below.
733+
comment_boundary = previous_token.type == :COMMENT ||
734+
(index >= 2 && %i[NEWLINE NEWLINE_TERMINATOR IGNORED_NEWLINE].include?(previous_token.type) && result_value[index - 2][0].type == :COMMENT && result_value[index - 2][0].location.end_offset == previous_token.location.start_offset)
735+
717736
# If we're at the end of the file and the previous token was a
718737
# comment and there is still whitespace after the comment, then
719738
# Ripper will append a on_nl token (even though there isn't
720739
# necessarily a newline). We mirror that here.
721-
if previous_token.type == :COMMENT
740+
if comment_boundary
722741
# If the comment is at the start of a heredoc: <<HEREDOC # comment
723742
# then the comment's end_offset is up near the heredoc_beg.
724743
# This is not the correct offset to use for figuring out if
@@ -745,6 +764,11 @@ def result
745764

746765
previous_state = lex_state
747766

767+
if event == :on_comment
768+
last_comment_token = lex_compat_token
769+
last_comment_end = prism_token.location.end_offset
770+
end
771+
748772
# The order in which tokens appear in our lexer is different from the
749773
# order that they appear in Ripper. When we hit the declaration of a
750774
# heredoc in prism, we skip forward and lex the rest of the content of

lib/prism/translation/parser/lexer.rb

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ class Lexer # :nodoc:
141141
MINUS_EQUAL: :tOP_ASGN,
142142
MINUS_GREATER: :tLAMBDA,
143143
NEWLINE: :tNL,
144+
NEWLINE_TERMINATOR: :tNL,
144145
NUMBERED_REFERENCE: :tNTH_REF,
145146
PARENTHESIS_LEFT: :tLPAREN2,
146147
PARENTHESIS_LEFT_GROUPING: :tLPAREN,
@@ -187,11 +188,6 @@ class Lexer # :nodoc:
187188
XSTRING_BEGIN: :tXSTRING_BEG
188189
}
189190

190-
# Types of tokens that are allowed to continue a method call with comments in-between.
191-
# For these, the parser gem doesn't emit a newline token after the last comment.
192-
COMMENT_CONTINUATION_TYPES = Set.new([:COMMENT, :AMPERSAND_DOT, :DOT])
193-
private_constant :COMMENT_CONTINUATION_TYPES
194-
195191
# Heredocs are complex and require us to keep track of a bit of info to refer to later
196192
HeredocData = Struct.new(:identifier, :common_whitespace, keyword_init: true)
197193

@@ -242,6 +238,13 @@ def to_a
242238
value = token.value
243239
location = range(token.location.start_offset, token.location.end_offset)
244240

241+
# A newline deferred past a run of comments is emitted before the
242+
# token that follows the last comment.
243+
if comment_newline_location && type != :tCOMMENT
244+
tokens << [:tNL, [nil, comment_newline_location]]
245+
comment_newline_location = nil
246+
end
247+
245248
case type
246249
when :tCHARACTER
247250
value.delete_prefix!("?")
@@ -259,27 +262,9 @@ def to_a
259262
location = range(token.location.start_offset, next_token.location.end_offset)
260263
index += 1
261264
else
262-
is_at_eol = value.chomp!.nil?
263-
location = range(token.location.start_offset, token.location.end_offset + (is_at_eol ? 0 : -1))
264-
265-
prev_token, _ = lexed[index - 2] if index - 2 >= 0
266-
next_token, _ = lexed[index]
267-
268-
is_inline_comment = prev_token&.location&.start_line == token.location.start_line
269-
if is_inline_comment && !is_at_eol && !COMMENT_CONTINUATION_TYPES.include?(next_token&.type)
270-
tokens << [:tCOMMENT, [value, location]]
271-
272-
nl_location = range(token.location.end_offset - 1, token.location.end_offset)
273-
tokens << [:tNL, [nil, nl_location]]
274-
next
275-
elsif is_inline_comment && next_token&.type == :COMMENT
276-
comment_newline_location = range(token.location.end_offset - 1, token.location.end_offset)
277-
elsif comment_newline_location && !COMMENT_CONTINUATION_TYPES.include?(next_token&.type)
278-
tokens << [:tCOMMENT, [value, location]]
279-
tokens << [:tNL, [nil, comment_newline_location]]
280-
comment_newline_location = nil
281-
next
282-
end
265+
# A carriage return before the terminating newline is part of
266+
# the comment token but not of the comment's value.
267+
location = range(token.location.start_offset, token.location.end_offset - 1) if value.chomp!
283268
end
284269
when :tNL
285270
next_token, _ = lexed[index]
@@ -501,6 +486,10 @@ def to_a
501486
end
502487
end
503488

489+
if comment_newline_location
490+
tokens << [:tNL, [nil, comment_newline_location]]
491+
end
492+
504493
tokens
505494
end
506495

src/prism.c

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10212,7 +10212,6 @@ parser_lex(pm_parser_t *parser) {
1021210212
pm_comment_t *comment = parser_comment(parser, PM_COMMENT_INLINE);
1021310213
pm_list_append(&parser->comment_list, (pm_list_node_t *) comment);
1021410214

10215-
if (ending) parser->current.end++;
1021610215
parser->current.type = PM_TOKEN_COMMENT;
1021710216
parser_lex_callback(parser);
1021810217

@@ -10230,7 +10229,16 @@ parser_lex(pm_parser_t *parser) {
1023010229
}
1023110230
}
1023210231

10233-
lexed_comment = true;
10232+
/* The comment does not include its terminating newline,
10233+
* which lexes through the newline handling below as its
10234+
* own token. A comment that ends the file has no newline,
10235+
* so the newline handling runs without one to emit. */
10236+
if (ending == NULL) {
10237+
lexed_comment = true;
10238+
} else {
10239+
parser->current.start = ending;
10240+
parser->current.end = ending + 1;
10241+
}
1023410242
}
1023510243
PRISM_FALLTHROUGH
1023610244
case '\r':
@@ -10268,7 +10276,11 @@ parser_lex(pm_parser_t *parser) {
1026810276
break;
1026910277
case PM_IGNORED_NEWLINE_PATTERN:
1027010278
if (parser->pattern_matching_newlines || parser->in_keyword_arg) {
10271-
if (!lexed_comment) parser_lex_ignored_newline(parser);
10279+
if (!lexed_comment) {
10280+
parser->current.type = PM_TOKEN_NEWLINE_TERMINATOR;
10281+
parser_lex_callback(parser);
10282+
}
10283+
1027210284
lex_state_set(parser, PM_LEX_STATE_BEG);
1027310285
parser->command_start = true;
1027410286
parser->current.type = PM_TOKEN_NEWLINE;
@@ -10365,11 +10377,15 @@ parser_lex(pm_parser_t *parser) {
1036510377
// If we hit a . after a newline, then we're in a call chain and
1036610378
// we need to return the call operator.
1036710379
if (next_content[0] == '.') {
10368-
// To match ripper, we need to emit an ignored newline even though
10369-
// it's a real newline in the case that we have a beginless range
10370-
// on a subsequent line.
10380+
/* A beginless range on the next line means this
10381+
* newline terminates the statement rather than
10382+
* continuing a method chain. */
1037110383
if (peek_at(parser, next_content + 1) == '.') {
10372-
if (!lexed_comment) parser_lex_ignored_newline(parser);
10384+
if (!lexed_comment) {
10385+
parser->current.type = PM_TOKEN_NEWLINE_TERMINATOR;
10386+
parser_lex_callback(parser);
10387+
}
10388+
1037310389
lex_state_set(parser, PM_LEX_STATE_BEG);
1037410390
parser->command_start = true;
1037510391
parser->current.type = PM_TOKEN_NEWLINE;

templates/src/tokens.c.erb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,8 @@ pm_token_str(pm_token_type_t token_type) {
275275
return "'->'";
276276
case PM_TOKEN_NEWLINE:
277277
return "newline";
278+
case PM_TOKEN_NEWLINE_TERMINATOR:
279+
return "newline";
278280
case PM_TOKEN_NUMBERED_REFERENCE:
279281
return "numbered reference";
280282
case PM_TOKEN_PARENTHESIS_LEFT:

test/prism/ruby/parser_test.rb

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -109,26 +109,13 @@ class ParserTest < TestCase
109109
# These files are failing to translate their lexer output into the lexer
110110
# output expected by the parser gem, so we'll skip them for now.
111111
skip_tokens = [
112-
"dash_heredocs.txt",
113112
"embdoc_no_newline_at_end.txt",
114-
"seattlerb/case_in.txt",
115-
"seattlerb/difficult4__leading_dots2.txt",
116113
"seattlerb/heredoc_unicode.txt",
117114
"seattlerb/parse_line_heredoc.txt",
118115
"seattlerb/pct_w_heredoc_interp_nested.txt",
119-
"seattlerb/required_kwarg_no_value.txt",
120-
"seattlerb/TestRubyParserShared.txt",
121116
"unparser/corpus/literal/assignment.txt",
122117
"unparser/corpus/literal/literal.txt",
123-
"whitequark/args.txt",
124-
"whitequark/beginless_erange_after_newline.txt",
125-
"whitequark/beginless_irange_after_newline.txt",
126-
"whitequark/forward_arg_with_open_args.txt",
127-
"whitequark/kwarg_no_paren.txt",
128-
"whitequark/multiple_pattern_matches.txt",
129-
"whitequark/newline_in_hash_argument.txt",
130-
"whitequark/pattern_matching_hash.txt",
131-
"whitequark/ruby_bug_9669.txt"
118+
"whitequark/forward_arg_with_open_args.txt"
132119
]
133120

134121
Fixture.each_for_version(except: skip_syntax_error, version: "3.3") do |fixture|

0 commit comments

Comments
 (0)