Module: MilkTea::Lexer::Strings
- Included in:
- MilkTea::Lexer
- Defined in:
- lib/milk_tea/core/lexer/strings.rb
Overview
String literal lexing (including adjacent-literal continuation), character literal lexing, and escape-sequence decoding.
Instance Method Summary collapse
- #decode_escape(char) ⇒ Object
- #lex_char_literal(line, index, line_number, line_offset:) ⇒ Object
- #lex_string(lines, line_index, line, index, line_number, line_offset:, cstring: false) ⇒ Object
- #scan_string_segment(line, index, line_number, cstring: false, recover: false) ⇒ Object
Instance Method Details
#decode_escape(char) ⇒ Object
151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/milk_tea/core/lexer/strings.rb', line 151 def decode_escape(char) case char when "n" then "\n" when "r" then "\r" when "t" then "\t" when "0" then "\0" when '"' then '"' when "'" then "'" when "\\" then "\\" else char end end |
#lex_char_literal(line, index, line_number, line_offset:) ⇒ Object
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/milk_tea/core/lexer/strings.rb', line 111 def lex_char_literal(line, index, line_number, line_offset:) start = index index += 1 if index >= line.length raise LexError.new("unterminated character literal", line: line_number, column: start + 1, path: @path) end char = line[index] if char == "\\" index += 1 if index >= line.length raise LexError.new("unterminated escape in character literal", line: line_number, column: start + 1, path: @path) end escape_char = line[index] if escape_char == "x" hex = line[index + 1, 2] unless hex&.match?(/\A[0-9a-fA-F]{2}\z/) raise LexError.new("invalid hex escape in character literal", line: line_number, column: index + 1, path: @path) end value = hex.to_i(16) index += 3 else value = decode_escape(escape_char).ord index += 1 end else value = char.ord index += 1 end if index >= line.length || line[index] != "'" raise LexError.new("expected closing ' in character literal", line: line_number, column: index + 1, path: @path) end index += 1 lexeme = line[start...index] @tokens << token(:char_literal, lexeme, value, line_number, start + 1, start_offset: line_offset + start, end_offset: line_offset + index) index end |
#lex_string(lines, line_index, line, index, line_number, line_offset:, cstring: false) ⇒ Object
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/milk_tea/core/lexer/strings.rb', line 8 def lex_string(lines, line_index, line, index, line_number, line_offset:, cstring: false) segment = scan_string_segment(line, index, line_number, cstring:, recover: @recovery_errors) consumed_lines = 1 value = +segment.value last_line = line last_line_number = line_number last_line_offset = line_offset last_segment_end = segment.next_index last_line_has_newline = lines.fetch(line_index).end_with?("\n") remainder = line[segment.next_index..] || "" line_indent = leading_space_count(line) recovered = segment.recovered if remainder.strip.empty? && !recovered scan_line_index = line_index + 1 scan_line_number = line_number + 1 scan_line_offset = line_offset + lines.fetch(line_index).bytesize while scan_line_index < lines.length raw_line = lines.fetch(scan_line_index) scan_line = raw_line.delete_suffix("\n").b segment_start = leading_space_count(scan_line) prefix = cstring ? 'c"' : '"' break unless segment_start > line_indent break if scan_line[segment_start].nil? break if scan_line[segment_start] == "#" break unless scan_line[segment_start, prefix.length] == prefix continued_segment = scan_string_segment(scan_line, segment_start, scan_line_number, cstring:, recover: @recovery_errors) continued_remainder = scan_line[continued_segment.next_index..] || "" break unless continued_remainder.strip.empty? value << continued_segment.value consumed_lines += 1 last_line = scan_line last_line_number = scan_line_number last_line_offset = scan_line_offset last_segment_end = continued_segment.next_index last_line_has_newline = raw_line.end_with?("\n") recovered ||= continued_segment.recovered scan_line_offset += raw_line.bytesize scan_line_number += 1 scan_line_index += 1 break if continued_segment.recovered end end start_offset = line_offset + index if consumed_lines == 1 lexeme = line[index...segment.next_index] @tokens << token(cstring ? :cstring : :string, lexeme, value, line_number, index + 1, start_offset:, end_offset: line_offset + segment.next_index) return StringLexResult.new(consumed_lines:, next_index: segment.next_index) end end_offset = last_line_offset + last_segment_end lexeme = @source.byteslice(start_offset, end_offset - start_offset) @tokens << token(cstring ? :cstring : :string, lexeme, value, line_number, index + 1, start_offset:, end_offset:) emit_line_newline(last_line, last_line_number, last_line_offset, last_line_has_newline) StringLexResult.new(consumed_lines:, next_index: last_segment_end) end |
#scan_string_segment(line, index, line_number, cstring: false, recover: false) ⇒ Object
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/milk_tea/core/lexer/strings.rb', line 72 def scan_string_segment(line, index, line_number, cstring: false, recover: false) start = index index += cstring ? 2 : 1 value = +"" while index < line.length char = line[index] if char == '"' return StringSegment.new(next_index: index + 1, value:) end if char == "\\" escape = line[index + 1] if escape.nil? if recover @recovery_errors << LexError.new("unterminated string literal", line: line_number, column: start + 1, path: @path) if @recovery_errors return StringSegment.new(next_index: line.length, value:, recovered: true) end raise LexError.new("unterminated string literal", line: line_number, column: start + 1, path: @path) end value << decode_escape(escape) index += 2 next end value << char index += 1 end if recover @recovery_errors << LexError.new("unterminated string literal", line: line_number, column: start + 1, path: @path) if @recovery_errors return StringSegment.new(next_index: line.length, value:, recovered: true) end raise LexError.new("unterminated string literal", line: line_number, column: start + 1, path: @path) end |