Skip to content

Releases: mgilbir/ktecma262

ktecma262 0.3.0

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.

ktecma262 0.2.0

Choose a tag to compare

@github-actions github-actions released this 16 Aug 16:12

The library stops being only a regular expression engine.

Four more corners of ECMA-262 arrive, each one something no Kotlin target
reproduces on its own: JavaScript's number formatting and parsing, the four URI
escaping functions, Unicode normalisation, and identifier validation — plus the
handful of places Kotlin returns a different answer rather than an error.

Nothing in the regular expression engine changed behaviour, so upgrading from
0.1.4 is safe; the version is 0.2.0 because the surface grew rather than
because anything moved. The JVM artifact grows from 176 KiB to about 269 KiB,
almost all of it the normalisation tables. Shrinkers remove what you do not
call — the tables are a single leaf class with one inbound reference — and on
plain JVM an unused class is never even loaded.

Added

  • String.isEcmaIdentifierName(), isEcmaIdentifier() and
    isEcmaReservedWord() in io.github.mgilbir.ecma262.text - ECMA-262 12.7,
    reusing the Unicode tables already compiled in.

    The two questions are different and both are useful: a property key needs an
    IdentifierName, a binding needs an Identifier, and keywords are the first
    but not the second. Words reserved only in some contexts - let and the
    strict-mode set, await in a module, yield in a generator - are parameters
    rather than assumptions.

    Verified at two levels: every code point against the composed rule from 12.7,
    and a sample against what node's parser actually accepts. The second caught a
    mistake in the first oracle: o.a-b parses as (o.a) - b, so testing member
    access reported a-b as a valid name. An object literal key is the correct
    probe.

    Five planted bugs are caught. One more showed that an explicit lone-surrogate
    branch was dead code, since a surrogate is in neither ID_Start nor
    ID_Continue; it has been removed rather than left as decoration.

  • String.ecmaTrim(), ecmaTrimStart(), ecmaTrimEnd() and EcmaMath in
    io.github.mgilbir.ecma262.text and .number. These are the places Kotlin
    disagrees with JavaScript by returning a different answer rather than an
    error, on every target including Kotlin/JS.

    Trimming differs on five characters, measured rather than assumed: Kotlin
    strips U+001C to U+001F where JavaScript keeps them, and keeps U+FEFF where
    JavaScript strips it. U+00A0 is stripped by both, contrary to a first guess.
    Which characters count is verified over the whole BMP.

    EcmaMath covers only the exactly specified functions — round, trunc,
    sign, clz32, imul, fround — since the rest of Math is
    implementation-approximated and there is nothing to be correct against.
    kotlin.math.round rounds ties to even; JavaScript rounds them toward
    positive infinity.

    Two implementations that look obvious are wrong: floor(x + 0.5) answers 1
    for 0.49999999999999994, and toFloat().toDouble() is a no-op on
    Kotlin/JS, where a Float is a JavaScript number. The second was caught by
    the JS target failing while JVM and native passed.

    The whitespace predicate is shared with the number parser, so the two cannot
    drift apart. Five planted bugs are caught.

  • String.normalize() in io.github.mgilbir.ecma262.text — ECMA-262 22.1.3.15,
    which defers to UAX #15, in all four forms. java.text.Normalizer is JVM-only
    and Kotlin/Native has nothing, so multiplatform code comparing user-entered
    text has been comparing sequences that look identical and are not.

    Tables are generated from the UCD, with decompositions stored fully expanded
    so normalising is a lookup rather than a recursion, and Hangul left out
    because its mappings are arithmetic.

    Checked against Unicode's own NormalizationTest.txt: 20,034 rows whose
    expectations no implementation produced, each verified against the invariants
    the file states — all five columns must agree under each form. Generation
    fails if node disagrees with any row. Every one of the 1,112,064 code points
    is then checked individually in all four forms against node.

    Six planted bugs are caught: unstable canonical ordering, ignoring the
    composition blocking rule, dropping a Hangul trailing jamo, NFKC using
    canonical decomposition, and — in the generator — ignoring
    Full_Composition_Exclusion and storing decompositions one step deep instead
    of fully expanded.

  • String.encodeUriComponent(), encodeUri(), decodeUriComponent() and
    decodeUri() in io.github.mgilbir.ecma262.uri — ECMA-262 19.2.6. Common
    Kotlin has no equivalent, and java.net.URLEncoder is a different algorithm
    (application/x-www-form-urlencoded) that is wrong for URIs.

    Encoding is verified over every one of the 1,114,112 code points against
    node, not a sample: escaping is what stops untrusted text from changing a
    URI's structure. Unpaired surrogates throw UriError.

    Decoding rejects what UTF-8 forbids — overlong forms, encoded surrogates,
    code points past U+10FFFF, truncated escapes — since accepting them turns a
    decoder into a filter bypass. Its input is text and cannot be enumerated, so
    ./gradlew uriFuzz covers it: 600,000 cases clean, about half of them
    rejections, and it runs nightly on three seeds.

    Five planted bugs are caught: a wrong unescaped set, accepting overlong
    encodings, accepting encoded surrogates, decoding reserved escapes in
    decodeUri, and substituting U+FFFD for a lone surrogate instead of throwing.

  • Double.toEcmaString() in io.github.mgilbir.ecma262.number — JavaScript's
    Number::toString, ECMA-262 6.1.6.1.20. No Kotlin target produces it:
    Kotlin/JS does because it is JavaScript, but Kotlin/JVM and Kotlin/Native
    print 1.0, 1.0E21 and 4.9E-324 where JavaScript prints 1, 1e+21 and
    5e-324. Over 200,000 random doubles the JVM's string differs 98.4% of the
    time.

    Almost all of that is layout — JavaScript stays positional out to 10^21 and in
    to 10^-6 — but not all of it: a JDK 21 Double.toString is not shortest for
    the smallest subnormals, so reusing the platform digits and re-laying them out
    would be wrong in exactly the cases hardest to notice.

    The specification defines the result rather than an algorithm, which makes two
    properties equivalent to it: the output round-trips, and no shorter decimal
    does. Both are checked over random doubles, every power of two, the subnormal
    range and short decimals, so the tests hold any implementation to the
    specification rather than to this one. Four planted bugs — an extra digit, a
    missing round-up, ignoring round-half-to-even, and dropping the asymmetric gap
    below powers of two — are each caught by the property that should catch them.

    Ties escape both properties, since both candidates round-trip and are equally
    short. Rounding them down instead of to the even significand costs 48 values
    in 231,948; the differential fixture and an explicit test carry that rule.

    Implemented with the exact rational method of Steele & White as presented by
    Burger & Dybvig: big integers, no lookup tables. Correctness first — it is 63x
    slower than java.lang.Double.toString at the extremes of the exponent range
    and 6.8x slower for everyday values. A table-driven method would close that.

  • String.toEcmaDouble()StringToNumber, ECMA-262 7.1.4.1.1, the
    conversion Number("…") performs. The whole string must be a numeric
    literal, so this is not parseFloat; 0x/0o/0b literals are accepted but
    take no sign, and an empty or all-whitespace string is +0. All 68 grammar
    and rounding cases are taken from node.

    Correctly rounded, which can turn on the 767th significant digit. Bounded
    against the inputs that have historically hung decimal parsers: significant
    digits are capped with the rest folded into a sticky flag, the exponent is
    clamped as it is read, and out-of-range magnitudes resolve before any big
    integer is built.

    Three planted bugs are caught — ties rounding up rather than to even,
    accepting trailing garbage, and ignoring the sticky flag. The third initially
    was not: no test exercised a value truncated at the cap that was also an
    exact tie, because a genuine tie never needs more than 767 digits.
    digitsBeyondTheCapBreakTies constructs one — the exact midpoint between 1
    and the next double, padded past the cap, with a single digit beyond it that
    decides the result.

  • Formatting and parsing are now checked as inverses against each other, so the
    round-trip property no longer leans on the host's decimal parser.

  • Double.toEcmaString(radix) for radices 2 to 36. Unlike everything else
    here this is compatibility rather than conformance: ECMA-262 calls the result
    for a radix other than 10 implementation-approximated and defines nothing
    further, so V8's behaviour is what is implemented and the 21,280 recorded
    strings are the contract rather than a check on one. Radix 10 delegates to the
    specified path. Four planted bugs are caught — dropping the round-half-to-even
    step, removing the floor on the error term, skipping the zero fill above 2^53,
    and stopping the fraction a digit early.

  • Double.toEcmaFixed(), Double.toEcmaExponential() and
    Double.toEcmaPrecision()Number.prototype.toFixed, toExponential and
    toPrecision (21.1.3.3, 21.1.3.2, 21.1.3.5). Checked against node over a grid
    of 4,054 values crossed with arguments from 0 to 100: 97,296 strings.

    All three round ties up, where toString rounds them to even. The
    specification asks for exactly that difference, and a planted swap to
    round-half-even is caught. So is losing the rounding carry — (99.995)
    .toFixed(2) is "100.00", and taking the decimal exponent from a rounded
    first digit instead of an unrounded one gave "999.95" until the grid caught
    it — and getting the toPrecision exponential threshold off by one.

  • Grisu3 as the fast path for Double.toEcmaString(), with the exact method
    kept as the fall...

Read more

ktecma262 0.1.4

Choose a tag to compare

@github-actions github-actions released this 15 Aug 09:39

Republishes 0.1.3 complete. The library is unchanged; 0.1.3 reached Maven
Central with four of its seven modules.

Fixed (publishing)

  • Publishing no longer uploads each publication separately to the OSSRH
    Staging API for the server to assemble into a deployment. That assembly is
    what failed: all seven publications uploaded without error, into a single
    staging repository, and the deployment the Portal built from it contained
    four. Nothing on the upload side reported a problem.

    The build now stages every publication into one directory, zips it, and
    uploads that single bundle to the Central Portal API. What is uploaded is
    what is published, with no server-side assembly step in between.

  • ./gradlew verifyCentralBundle checks the bundle before it is uploaded:
    every publication present, each with a POM, Gradle module metadata and a
    detached signature.

  • After uploading, the release workflow compares the modules it bundled
    against the components the Portal reports for the deployment, and fails if
    any are missing. Run against 0.1.3's actual deployment, this reports
    exactly ktecma262-iosarm64, ktecma262-iossimulatorarm64, ktecma262-js.

Available from Maven Central as io.github.mgilbir:ktecma262:0.1.4.

ktecma262 0.1.3

Choose a tag to compare

@github-actions github-actions released this 15 Aug 09:02

Caution

Deprecated — incompletely published.

Only four of seven modules reached Maven Central: ktecma262, ktecma262-jvm, ktecma262-linuxx64 and ktecma262-macosarm64. ktecma262-js, ktecma262-iosarm64 and ktecma262-iossimulatorarm64 are absent, while the root module still declares variants pointing at them — so resolving 0.1.3 for JS or either iOS target fails outright.

JVM, Linux and macOS consumers are unaffected. The cause was in publishing, not in the build: all seven publications uploaded without error and the portal assembled a deployment containing four. The library code is identical to 0.1.4.

Use 0.1.4 instead.


Adds native targets and fixes a parser bug found by the nightly fuzzer.

Added

  • macosArm64, iosArm64, iosSimulatorArm64 and linuxX64 targets. The
    engine is pure commonMain Kotlin with no expect/actual, so the sources
    are unchanged; all 153 tests run on Kotlin/Native exactly as they do on JVM
    and JS, including the ~42,750-case recorded differential suite.

Fixed

  • Annex B's fallback for an invalid \c consumed the c. Both
    ExtendedAtom :: \ [lookahead = c] and ClassAtomNoDash :: \ [lookahead = c]
    denote the backslash alone, leaving the c to be parsed as the next
    atom, so anything binding to it bound to the wrong thing:

    • a quantifier covered both characters, making them jointly optional —
      /a\c*/ matched a bare "a", where the pattern is a, \, c* and
      requires a literal backslash;
    • in a class the c could not open a range, so [\c-z] was the three
      characters \, c, - rather than \ plus c-z.

    Found by the nightly differential fuzzer on /a\c*{?/ig, one case in
    500,000. The class-range half was not reached by the fuzzer and came out of
    reading the grammar while fixing the first.

Testing

  • A second direction of the known V8 modifier-scoping defect is now recognised
    and skipped. The presence of a modifier group makes V8 drop the case
    extension from a negated word class — /(?-i:a)?[^\w]/vi matches the long
    s, which folds to "s" and so belongs to \w — while a bare \w in the same
    pattern keeps the extension, so V8 contradicts itself. Previously only an
    added i was recognised, on the belief that (?-i:…) scoped correctly.
    This engine's behaviour is unchanged and matches V8's own answer once the
    modifier group is removed; ModifierGroupTest now pins both directions.
    Broadening the skip costs 180 of ~42,750 recorded cases.
  • The recorded corpus covers the \c fallback in all three modes, and grew
    from 41,271 to 42,753 cases.

Release process

None of this changes the library, but all of it is why 0.1.2 was unusable for
native consumers.

  • Releases are now built and published from macOS. It is the only host that
    can compile every target: Apple targets need Xcode, and Kotlin/Native
    cross-compiles the Linux target from macOS.
  • ./gradlew verifyPublishedVariants fails when a declared target would not
    actually be published. Kotlin creates a publication only for targets the
    host can build, while the root module lists a variant for every declared
    target — so publishing from the wrong host uploads a module referencing
    artifacts that do not exist.
  • CI builds every target on macOS on each push, rather than discovering Apple
    breakage on release day.
  • The GitHub release page is created by the release workflow, with notes taken
    from this file and the jars attached. v0.1.1 and v0.1.2 were tagged and
    published without one. The workflow fails early if the changelog has no
    section for the version being released, rather than after the artifacts are
    already immutable.
  • The Central deployment is released automatically (publishing_type=automatic)
    instead of waiting for a manual Publish. Pushing a tag is now the point of no
    return.

On Maven Central as io.github.mgilbir:ktecma262:0.1.3, but only the common, JVM, Linux and macOS modules — see the notice at the top.

ktecma262 0.1.2 (no native variants)

Choose a tag to compare

@mgilbir mgilbir released this 15 Aug 09:42

Caution

Deprecated — no native variants.

Published with the common, JVM and JS variants only. A Kotlin Multiplatform build that declares any native target cannot resolve it.

It is also actively harmful to work around locally: because Gradle takes the first repository holding a coordinate, a 0.1.2 on Maven Central shadows a 0.1.2 you published to mavenLocal, silently replacing your build with one that has fewer variants.

Use 0.1.4 instead.


No library changes: the compiled artifacts are byte-for-byte identical to
the 0.1.1 tag. This version exists only because the 0.1.1 release never
completed.

Its release run hung for over an hour in the differential fuzz step. The
fuzzer deliberately generates patterns that are catastrophic for a
backtracker; this engine bounds them with a step budget, but the node
process used as the oracle has no such limit and cannot be interrupted
from JavaScript, so V8 ran at full CPU until the run was cancelled.

Fixed (test infrastructure only)

  • Cases that exceed this engine's step budget are now screened out before
    being sent to the oracle. The comparison already skipped them, so no
    coverage is lost, and the failing seed now completes in 37 seconds.
  • A watchdog kills the oracle after two minutes without output and names
    the case it stopped on.
  • The oracle streams results instead of buffering them until close, so
    progress is no longer lost when it is killed.
  • The oracle's stderr is inherited rather than left in an undrained pipe,
    which could have blocked it.
  • The oracle process is destroyed on shutdown; a cancelled run previously
    left node spinning indefinitely.

v0.1.1 remains as a tag but was never published.

0.1.2 has JVM, JS and common variants only. A Kotlin Multiplatform build
that declares a native target cannot resolve it, and — because Gradle takes the
first repository holding a coordinate — it will shadow a locally published
build of the same version. Use 0.1.3.

ktecma262 0.1.1 (never published)

Choose a tag to compare

@mgilbir mgilbir released this 15 Aug 09:42

Caution

Deprecated — never published.

This tag exists, but nothing was ever released to Maven Central under it: the release run hung in the differential fuzz step and was cancelled. There is no 0.1.1 artifact to depend on.

Its content — the \0 character-class fix — shipped in 0.1.2 and later.

Use 0.1.4 instead.


Fixes a parser bug found by the differential fuzzer immediately after 0.1.0
was tagged.

Fixed

  • \0 inside a character class is the NUL escape in every mode. Both
    Unicode-mode class paths rejected it: the u path rejected every digit
    escape outright, where CharacterEscape :: 0 [lookahead ∉ DecimalDigit]
    keeps \0 valid, and the v class-set path had no digit branch at all.
    [\0] and [+\0d] now match NUL under u and v as they already did
    under Annex B, while [\01], [\1] and [\9] remain SyntaxErrors in
    Unicode mode.

    Found on /st[+\0d]&+/vs by the fuzzer's unstructured-pattern mode — a
    shape the grammar-driven generator cannot produce. The recorded corpus
    now covers digit escapes inside classes in all three modes.

0.1.0 is affected by this bug and should not be used.

ktecma262 0.1.0

Choose a tag to compare

@mgilbir mgilbir released this 15 Aug 01:11

Caution

Deprecated — parser bug.

\0 inside a character class was rejected as a SyntaxError under the u and v flags, where ECMA-262 keeps it valid as the NUL escape. Patterns such as [\0] and [+\0d] failed to compile. Fixed in 0.1.1.

Use 0.1.4 instead.


Warning

Superseded by 0.1.1.
This release rejects \0 inside a character class under the u and v
flags, where it is the NUL escape in every mode — so patterns such as
[\0] and [+\0d] fail to compile. Use 0.1.1.

An ECMA-262 (JavaScript) regular expression engine in pure Kotlin, for Kotlin
Multiplatform. Patterns and match results behave as they do in JavaScript: the
same flags, Annex B syntax, capture semantics and UTF-16 offsets.

Highlights

  • Flags i g m s u v y d; Annex B by default, strict ECMA-262 opt-in
  • Named groups including ES2022 duplicates; backreferences including forward
    references; lookahead and lookbehind, including variable-length lookbehind
    with right-to-left capture semantics
  • v (UnicodeSets): nested classes, && and --, \q{…} string literals, and
    properties of strings (\p{RGI_Emoji} and its constituents)
  • Regexp modifiers (?i:…) and RegExp.escape (ES2025)
  • Unicode 17.0.0 compiled in, so \p{…} does not vary with the host JDK
  • JVM output is Java 17 bytecode, checked by reading the emitted class files

Safety and performance

Backtracking runs on an explicit stack with an undo log, so deep backtracking
cannot overflow the call stack — java.util.regex throws StackOverflowError
on inputs this engine handles. Matching is bounded by RegExp.maxSteps
(default 1,000,000, as PCRE's backtrack limit), and the budget spans a whole
operation rather than each match, so cost cannot scale with a match count the
input controls.

On a Ryzen 9 6900HX: literal scan over 100k chars 41 µs (vs 81 µs for
java.util.regex), a three-capture date match 174 ns (vs 170 ns).

How correctness was established

Differential testing against a real JavaScript engine rather than readings of
the specification:

  • ~40,000 recorded node results replayed on JVM and JS on every build
  • Millions of live fuzz cases covering exec, findAll, replace and split,
    including unstructured patterns the generator's own grammar cannot produce
  • Test262's generated v-flag conformance cases
  • All 439 Unicode properties verified against node code point by code point, and
    RegExp.escape verified over all 1,114,112 code points

Deliberate divergences from V8

Three, each documented in the README with a test pinning the specified
behaviour: match positions that split a surrogate pair under /u,
single-character \q{} folding under /vi, and modifier scoping leaking into
\w. Each was confirmed as a V8 inconsistency rather than a reading of the
grammar.

Installing

Maven Central publication is pending namespace verification. Until then, build
from source:

git clone https://github.com/mgilbir/ktecma262 && cd ktecma262
./gradlew publishToMavenLocal

then add mavenLocal() to your repositories and
implementation("io.github.mgilbir:ktecma262:0.1.0") — or take the jars below.

The -sources jars carry the KDoc; the artifacts here are the same ones
publishToMavenLocal produces.