Skip to content

ktecma262 0.3.0

Latest

Choose a tag to compare

@github-actions github-actions released this 02 Sep 04:21

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, timeClip and makeFullYear are 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 until timeClip, which is what lets the rolling work.
    makeFullYear is 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's LocalTZA(t, isUTC = false), and it is
    java.time's ZoneRules.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.parse may fall back to an
    implementation-specific parser and V8 reads March 1, 2024, 2024/03/01, a
    lowercase z and 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 produce 24:30, so a
    missing end-of-day check survived it. The generator now reaches that case.

  • scanRegExpLiteral(text, from) in io.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, which RegExp.compile already reports properly.

  • decodeEscapeSequence(source, backslashAt) in the same package - one
    EscapeSequence or LineContinuation, 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() and toEcmaUint32() - ToInt32 and ToUint32,
    ECMA-262 7.1.6 and 7.1.7 (#9). These were already implemented privately for
    clz32 and imul; 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) and isEcmaLineTerminator(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 what trim removes. 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 - + - 1 being 2.

Fixed

  • \cX inside a character class was rejected under the v flag. 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. u and Annex B classes
    were unaffected, as was \cX outside 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 - \0 was 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, v additionally 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.