Everything a consumer needs to tokenize and to compute with dates, and the
last of the issues filed against 0.2.0.
Added
-
ECMA-262 21.4 time value arithmetic, in
io.github.mgilbir.ecma262.date
(#7).makeDay,makeTime,makeDate,timeClipandmakeFullYearare the
specification's own decomposition, kept as separate steps because the rolling
rule is where aggregate arithmetic goes wrong: month 12 is the thirteenth
month, day 0 is the day before the first, hour 24 is the next midnight, and
nothing is clipped untiltimeClip, which is what lets the rolling work.
makeFullYearis the two-digit rule, where 99 means 1999 and 100 means 100. -
parseDateTimeString(text, zone)- the Date Time String Format, 21.4.1.32.
Its one asymmetry decides which day a value lands in: a date-only string is
UTC, a date-time string with no offset is local time. -
EcmaTimeZone, the seam that keeps a time zone database out of this library.
It is consulted for exactly one case - a date-time string with no offset - and
it takes a time value carrying the wall clock rather than an offset, because
the offset depends on the date and the instant is what the caller is trying to
compute. That is the specification'sLocalTZA(t, isUTC = false), and it is
java.time'sZoneRules.getOffset(LocalDateTime)one for one, so a JVM
implementation is a three-line delegation. The spring-forward gap and the
autumn overlap both resolve with the pre-transition offset, as JavaScript
does; a JVM test pins that against node across both 2024 transitions.Anything outside the grammar returns NaN.
Date.parsemay fall back to an
implementation-specific parser and V8 readsMarch 1, 2024,2024/03/01, a
lowercasezand four fractional digits; none of that is portable, and
guessing at it would agree with one engine and disagree with the next.Fourteen planted bugs are caught, and a fifteenth exposed a gap in the fuzzer
rather than in the library: the generator could not produce24:30, so a
missing end-of-day check survived it. The generator now reaches that case. -
scanRegExpLiteral(text, from)inio.github.mgilbir.ecma262.lexer- finds
where a regular expression literal ends (#6). Not a search for the next/:
a backslash escapes what follows, a/inside[...]is an ordinary
character, and a line terminator may not appear in the body at all, which is
what stops an unterminated literal swallowing the rest of a file. An empty
body or a leading star is a comment rather than a literal.It deliberately does not decide whether a
/starts a literal - that depends
on the preceding token, so it belongs to the host grammar - and it does not
validate flags, whichRegExp.compilealready reports properly. -
decodeEscapeSequence(source, backslashAt)in the same package - one
EscapeSequenceorLineContinuation, offset in and offset out (#8). The
three rules worth having a table for: a zero escape is NUL only when no digit
follows, a braced unicode escape may need a surrogate pair, and a line
continuation consumes CR LF as one break and produces nothing. Legacy octal is
rejected rather than guessed at, matching strict mode. -
Double.toEcmaInt32()andtoEcmaUint32()-ToInt32andToUint32,
ECMA-262 7.1.6 and 7.1.7 (#9). These were already implemented privately for
clz32andimul; they are what every bitwise operator coerces its operands
with, so a consumer implementing&,|,^,<<,>>or>>>needs them
first. Not a cast: NaN and the infinities become zero, the value truncates
toward zero, and the rest wraps modulo 2^32. -
isEcmaWhiteSpace(Char)andisEcmaLineTerminator(Char)are public, in
io.github.mgilbir.ecma262.text. The first was internal and the second did
not exist, so a consumer lexing a JavaScript subset was hand-copying a table
this library already holds (#5).They are kept as the two disjoint productions the grammar defines rather
than one predicate for both. A tokenizer has to tell them apart: a line
terminator may not appear in a regular expression literal's body, and it ends
a single-line comment, where whitespace does neither. Trimming and the numeric
literal parser use the union, which now lives in one place so those two cannot
drift apart.Both sets are checked over the whole BMP - 21 and 4 characters, disjoint, and
together exactly whattrimremoves. The sets came from asking node rather
than reading the table: a line terminator ends a single-line comment, and
whitespace separates tokens in a declaration. The declaration form matters, as
an arithmetic probe reports-as whitespace,1 - + - 1being 2.
Fixed
-
\cXinside a character class was rejected under thevflag. The
class-set path had no branch for it at all, so[\cf_]was a SyntaxError
while every engine accepts it and matches U+0006.uand Annex B classes
were unaffected, as was\cXoutside a class.Found by the nightly fuzzer on
/=XwXd[\cf_]bX/vi, one case in 500,000.This is the second bug of exactly this shape -
\0was the first, in 0.1.1 -
so rather than fix it and move on, the two class paths were compared branch by
branch. They now handle the same escapes,vadditionally handling\q,
which is the only one that belongs to it alone. The recorded corpus covers
control escapes inside classes in all three modes.
Available from Maven Central as io.github.mgilbir:ktecma262:0.3.0.