mooner / Resume KO/EN /

mooner:~/resume $ cat projects/neonjs/README.md

NeonJS

JavaScript engine for the JVM

A JavaScript engine for the JVM, written in Kotlin. It is built to run untrusted scripts inside Java/Kotlin applications, with a bytecode interpreter, a JIT to JVM bytecode, a sandbox with execution limits and Java/Kotlin interop. It also runs on Android (API 26+).

Source ↗Maven ↗

mooner:~/resume/projects/neonjs $ tree -L 1

Structure01

구조

07 parts

Eight Gradle modules. The engine runs on neonjs-core alone; the others are added when needed. About 58,000 lines of Kotlin in the main code and 7,400 in tests.

neonjs-core
Parser, bytecode compiler, interpreter, JIT, built-ins, a RegExp engine written from scratch, the sandbox, Java interop, and web globals such as URL, Blob and events.
neonjs-intl
Intl and the non-ISO Temporal calendars on ICU4J. On the class path, it is switched on.
neonjs-net
fetch and WebSocket over OkHttp, reaching only the hosts and address ranges allowed.
neonjs-node
node: modules, require and Buffer. Much of the JS side is ported from the Node.js v26.11.1 sources.
neonjs-android
Turns the classes the JIT generates into dex with dx, or with D8 through the -d8 module, and loads them from memory.
neonjs-cli
A command-line runner and REPL, with options for the Node APIs, a network allow list and .env files.
neonjs-test262
Runners for Test262, web-platform-tests and Node.js’s own tests, a mutation fuzzer, and an on-device check (AndroidCheck).

mooner:~/resume/projects/neonjs $ cat docs/architecture.md

Architecture02

아키텍처

Data flow

HOST APPctx.eval(source)PARSER · SCOPESAST · registersBYTECODECodeBlockINTERPRETERruns firstSANDBOXlimits · host accesscheckJIT QUEUEbackground · ×32hotJVM COMPILERASM · unboxed · inlineCODE DEFINERone per platformHOTSPOThidden classANDROIDdx / D8 → dex// the interpreter keeps running// until the compiled code is installed

Source goes parser → scope analysis → bytecode, and the interpreter runs it first. In the default adaptive mode, functions called more than 1,000 times, or loops that run long, are compiled to JVM bytecode on background threads and swapped in.

  • Three modes: interpreter only, compile everything, adaptive. In adaptive mode a function keeps running interpreted until its code is ready, and compilation goes in batches of up to 32.
  • A JIT class is named by a SHA-256 of everything the generator reads, so equal names mean equal class files: functions that differ only in constants or position, and the same code run in many contexts, share one class.
  • Unboxed numbers and inlining are not speculative. Types come only from what instructions produce; no check is removed because of them and nothing deoptimizes. A wrong type surfaces as an exception instead of reading as another number.
  • What differs per platform sits behind one CodeDefiner: hidden classes on HotSpot, dex from dx or D8 on Android, and the interpreter alone when neither is available.
  • The sandbox limits time, instructions, call depth, string length and memory (HotSpot only), and denies host access by default. A script’s catch and finally cannot intercept termination.
  • HotSpot does not JIT methods over 8,000 bytes, so the interpreter and runtime keep their methods under that, and a test checks it.
  • On StarLight’s nightly branch it sits next to Rhino as a second JavaScript language (0.1.3).

mooner:~/resume/projects/neonjs $ git log --grep=fix

Problem solving03

문제 해결

06 cases

  1. 01

    The runtime aborts on Android 8.0 and 8.1

    Problem
    The API 26 emulator CI died on a JIT thread with Check failed: self == thread_running_gc_. ART 8.x records the only implementation of an interface method and reads that record again even after the class loader behind it is gone, and the probe class loaded first was that only implementation.
    Approach
    Two implementations of CompiledCode are now linked in the engine’s loader up front, so ART stops tracking the method. On those versions Java.extend adapters stay loaded, and the device script prints the native backtrace from crash logs.
    Result
    The next Android CI run passed. Today the API 26 and 34 emulators pass all 7 device checks and a 15,789-test Test262 subset.
  2. 02

    More than 750,000 JIT classes

    Problem
    JIT class names hashed only the instructions and register count, so in Test262 78 names stood for genuinely different code. That ruled out putting several classes in one dex, and the same code got a new class for every block.
    Approach
    Names became a SHA-256 of everything the generator reads, so equal names mean equal class files. Generated classes hold no state, so blocks with the same input share one instance from a weakly held cache.
    Result
    As measured in the commit, Test262 in compiled mode went from 756,204 generated classes to 75,915, and from about 150 s to 35 s. 51,915 names match 51,915 distinct class files.
  3. 03

    The fixed cost of dex conversion adds up per function

    Problem
    On Android, JIT classes must be converted to dex, and every dx or D8 call has a large fixed cost. Converting each function on its own pays that cost once per function.
    Approach
    Compilation moved to background daemon threads that take up to 32 blocks at a time and turn each batch into one dex file and one class loader. Exactly one thread compiles a block, a rejected batch is defined class by class, and worker time is not charged to a script’s limits.
    Result
    As measured in the commit, compiling 5,000 functions at once on a desktop JVM went from 1,331 ms to 671 ms with dx and from 7,150 ms to 2,400 ms with D8. Through dx, all 48,747 Test262 tests still pass.
  4. 04

    Compiled code boxes a number at every step

    Problem
    Generated code held every value as an Object, so each arithmetic step allocated a new Double.
    Approach
    A forward type analysis gives registers and stack slots a kind (double, int32 or anything else), and registers that only hold numbers become JVM locals. Ints are used only for values that are int32 by definition, such as bitwise results, and arithmetic stays in doubles, so overflow and -0 behave as before.
    Result
    As measured in the commits, on an ART emulator an empty loop went from 9.22 to 0.70 ns per iteration and xorshift32 from 25.3 to 3.5 ns. 60,000 random numeric functions compared against the interpreter showed no mismatch, and Test262 passes with unboxing on and off.
  5. 05

    HotSpot and Android call different methods

    Problem
    Among overloads of equal conversion cost, the one reflection listed first won, and HotSpot and ART list methods in different orders: sb.append(null) called a different method depending on the VM. Long IDs beyond 2^53 also changed their last digits on the way to a JS number.
    Approach
    Ties now go to the most specific types, as in Java, then to a fixed order, and a signature can name the overload wanted. Longs beyond 2^53 − 1 cross over as BigInts.
    Result
    Both are pinned by the interop contract tests, and the Android device check covers the long boundaries.
  6. 06

    The sandbox does not do what its docs say

    Problem
    The security docs said waiting for a busy context was bounded by the time limit, but the wait had no bound and two threads could deadlock. The engine’s own API classes were missing from the deny list, and new URL('a:#' + 'x'.repeat(2e6)) ran a quadratic loop that no time limit could stop.
    Approach
    Lock waits are bounded by the time limit and end in a NeonTimeoutException; every engine package and API class is on the deny list; the URL parser is linear and checks for interrupts and limits every 4,096 steps.
    Result
    All three are pinned by tests. A new class in the engine packages must be denied or declared harmless in the test.

Accepting requestsAnsan --:-- KST

mooner:~/resume $ ./contact.sh

Contact→

Name
문민기 / Minki Moon
Handle
@mooner
Base
Ansan, KR

Happy to trade coffee for conversation.

If anything here catches your eye, feel free to reach out.

// ping me anytime — i read all.

© 2026 Minki Mooncrafted with monospace affection · rev 2026.10.18