@@ -222,16 +222,18 @@ def parse_object_pair(object)
222222 # Parses the key of an object, including the special logic for merging dangling arrays.
223223 # Returns [key, was_array_merged_flag]
224224 def parse_object_key ( object )
225+ char = peek_char
226+
225227 # First, check for and handle the dangling array merge logic.
226- if try_to_merge_dangling_array ( object )
228+ if char == '[' && try_to_merge_dangling_array ( object )
227229 return [ nil , true , false ] # Signal that an array was merged.
228230 end
229231
230232 # If no merge happened, proceed with standard key parsing.
231233 @context . push ( :object_key )
232234 is_bracketed = false
233235
234- if peek_char == '['
236+ if char == '['
235237 @scanner . getch # Consume '['
236238 arr = parse_array
237239 key = arr . first . to_s
@@ -509,6 +511,16 @@ def check_unmatched_delimiters(
509511 unmatched_delimiter = false
510512 # --- Main Parsing Loop ---
511513 while !@scanner . eos? && char != rstring_delimiter
514+ # Fast-path for unquoted keys (e.g. { key: val })
515+ # consumes a chunk of valid identifier characters at once.
516+ if missing_quotes && current_context? ( :object_key )
517+ chunk = @scanner . scan ( /[a-zA-Z0-9_$-]+/ )
518+ if chunk
519+ string_parts << chunk
520+ char = peek_char
521+ end
522+ end
523+
512524 break if context_termination_reached? (
513525 char :,
514526 missing_quotes :
@@ -979,7 +991,8 @@ def parse_number
979991
980992 # Handle cases where the number ends with an invalid character.
981993 if !scanned_str . empty? && INVALID_NUMBER_TRAILERS . include? ( scanned_str [ -1 ] )
982- # Do not rewind scanner, simply discard the invalid trailing char (garbage)
994+ # Rewind scanner for the invalid char so it can be handled by the main loop (e.g. as a separator)
995+ @scanner . pos -= 1
983996 scanned_str = scanned_str [ 0 ...-1 ]
984997 # Handle cases where what looked like a number is actually a string.
985998 # e.g. "123-abc"
@@ -1170,16 +1183,29 @@ def skip_whitespaces
11701183
11711184 # Peeks the next character without advancing the scanner
11721185 def peek_char ( offset = 0 )
1173- return @scanner . check ( /./m ) if offset . zero?
1186+ # Handle the common 0-offset case
1187+ if offset . zero?
1188+ # peek(1) returns the next BYTE, not character
1189+ byte_str = @scanner . peek ( 1 )
1190+ return nil if byte_str . empty?
1191+
1192+ # Fast path: If it's a standard ASCII char (0-127), return it directly.
1193+ # This avoids the regex overhead for standard JSON characters ({, [, ", etc).
1194+ return byte_str if byte_str . getbyte ( 0 ) < 128
1195+
1196+ # Slow path: If it's a multibyte char (e.g. “), use regex to match the full character.
1197+ return @scanner . check ( /./m )
1198+ end
11741199
1200+ # For offsets > 0, we must scan to skip correctly (as characters can be variable width)
11751201 saved_pos = @scanner . pos
1176- c = nil
1202+ res = nil
11771203 ( offset + 1 ) . times do
1178- c = @scanner . getch
1179- break if c . nil?
1204+ res = @scanner . getch
1205+ break if res . nil?
11801206 end
11811207 @scanner . pos = saved_pos
1182- c
1208+ res
11831209 end
11841210
11851211 def current_context? ( value )
0 commit comments