TODO
Unfinished work found by scanning the whole codebase (native quickjs-*.c bindings, src/,
include/, lib/*.js, doc/*.md, tests/test_*.js), ordered by leverage: highest-impact,
cheapest-to-fix items first, general cleanup last. Every item below was verified by reading
the code (and, where noted, by actually running it) — not just grepped.
This supersedes the sparse root-level TODO file; its four items are folded in below (marked
(pre-existing)).
Roadmap
Four standing goals for this project, in priority order. Every tier below should be read
against these — they're the "why" behind what gets picked up next. See ASSESSMENT.md for
the full architecture/gap survey behind Tier 6-8.
- Be the standard library QuickJS deserves — WHATWG-spec'd web APIs (streams, URL, events, encoding, DOM) and Deno/Bun-like runtime APIs (fs, process, timers, readline, child_process, ...), with a coherent, documented JS surface over the native bindings.
- Be a toolbox for working with QuickJS itself — inspection/reflection/deep-object/pointer utilities for debugging and metaprogramming.
- Be a lexer/parser toolkit — a general, reusable grammar/lexer framework, not just glue code for one format.
- Support archives, filesystem, sockets, serial, and databases as first-class, ergonomic JS APIs, not just raw native bindings.
Tier 2 — public API documented as working, but isn't
~~Blob.prototype.stream() returns undefined instead of a ReadableStream~~ — FIXED in commit bfa9b603.
Implemented Blob.prototype.stream() to return a ReadableStream over the blob's bytes.
The implementation copies the blob data (since the blob may be garbage collected before
the stream finishes reading) and wraps it in a Reader that's passed to
js_readable_stream_from_reader(). All three tests in tests/test_blob.js now pass.
Tier 3 — known perf/architecture debt (already flagged) and spec-compliance gaps
js_is_*type-check helpers (js_is_arraybuffer,js_is_date,js_is_map, etc. insrc/utils.c:2664-2745) are slow (pre-existing TODO item) — each doesinstanceof || Object.prototype.toString comparisonvia a string compare instead of a tag check. These sit on hot paths (serialization,deep,inspect), so worth profiling once Tier 1 is fixed and traffic patterns are trustworthy again.JsonParser.parse()should resync past a run of bad bytes in one call, not one byte per thrown exception — seeBUGS'sjson-parser-error-resync-is-per-byte-exceptions.json_parse()(src/json.c:307-405) returnsJSON_ERRORafterjson_getc_skipws()has consumed exactly one byte (thedefault:case atsrc/json.c:395-399, and the expected-:check atsrc/json.c:338-341), andjs_json_parser_method()(quickjs-json.c:2082-2094) throws a freshJS_ThrowSyntaxErrorevery time. A caller trying to skip a corrupted span therefore pays one construct-throw-catch cycle per bad byte instead of per bad span — measured at ~200 exceptions to skip 200 garbage bytes, and pathological (never finishes in practice) across a multi-hundred-MB document with many such spans. Fix belongs injson_parse()itself: on hitting thedefault:/expected ':'error paths, keep consuming bytes in a local loop (skip whitespace-or-not, doesn't matter) until reaching an unambiguous resumption point — a structural character (,{}[]") at a depth/state where it's legal — before returning to the JS boundary with a singleJSON_ERROR, mirroring the resyncJsonPushParser.write()'s doc comment already claims (see also thejson-push-parser-resync-doc-untestedBUGSentry — that claim needs its own regression test before being trusted as the model to copy). Alternatively/additionally, expose a cheap.resync()method that does this scanning without needing the caller to loop.parse()+ try/catch at all.~~Streams
respondWithNewView()(BYOB) is missing spec-required safety checks~~ — FIXED in commits312df027,ae4f9992, andd209f470. Replacedlib/stream.jswith qjs-lws version which has complete BYOB implementation. AddedisDataViewConstructorhelper function and fixed allpendingPullIntosproperty access to useCTRL()wrapper. Addednoopandassert_defaultexports to lib/assert.js for compatibility. FixedReadableByteStreamControllerCallPullIfNeededto pull when there are pending read requests, even if desiredSize <= 0. Fixed async iterator cleanup in_returnStepsto usethis._readerinstead ofSTRM(this).reader. FixedWritableStreamDefaultWriterWriteto handle undefined stream and undefinedstrategySizeAlgorithmcorrectly. All 41 stream tests now passing (100%).
Tier 4 — structural/maintenance risk and test-coverage gaps
internal.handquickjs-internal.hare two hand-forked copies of the same QuickJS internals header, both carrying matchingXXX:design-debt comments at nearly the same line numbers. Any future fix to one needs manual re-application to the other; worth collapsing to a single source of truth (or confirming they've already diverged and documenting why two copies exist).tests/test_list.jsisn't a real test — it's an ad-hoc script (not using theassert/assertEqpattern every othertest_*.jsfile uses) that ends in an unguardedwhile(!skip()) {}loop. It's exactly the kind of gap that letList.prototype.at()(registered but its case body commented out, always returningundefined) go unnoticed — since removed entirely (quickjs-list.c,doc/native/list.md). Worth rewriting properly so the next dead/wrong method doesn't slip through the same way.
Tier 5 — lower-value cleanup (dead alternate code, disabled diagnostics, unfinished scaffolding)
Not urgent individually, but worth a pass since dead/disabled code in the same functions as live logic is exactly what produced every Tier 1 bug above — cleaning it up now prevents the next one.
- Disabled alternate implementations with no remaining purpose:
src/js-utils.c:70-75(oldpromise_free(JSContext*, ...)overload),src/js-utils.c:153-159(oldpromise_forward()body),src/utils.c:2017-2023(oldjs_values_free(JSContext*, ...)overload),src/utils.c:3050-3057+:3363-3372(abandoned zero-copyjs_arraybuffer_fromstring/finalizer pair — current version always copies),src/glob2.c:22-28(range_free(), unused). - Duplicated disabled
FROM_UNIXTIME(...)date-formatting block in bothquickjs-mysql.c:109-120andquickjs-pgsql.c:220-231, plus an unusedjs_pgconn_print_fields()inquickjs-pgsql.c:296-309. quickjs-misc.c:1267-1277— disabled alternate glob implementation using the project's ownmy_glob()/src/glob2.cengine; the systemglob()is used instead, meaningglob2.cis currently built but not actually wired up tomisc.glob(). Worth confirming this is intentional or finishing the wiring.quickjs-sockets.c:2098-2133— a whole abandoned `PROP_SYSCALL/PROP_ERRNO/PROP_ERROR/ PROP_RET/PROP_AFproperty block (enum + switch cases + bothSocket/AsyncSocket` registrations, all consistently disabled together) plus an unusedjs_sockopt()helper at:2236-2239.quickjs-pointer.c:919-927— disabled forwarding of `Array.prototype.map/reduce/forEach/ keys/valuesontoPointer.prototype; currently not exposed at all.:707-713` — an abandonedSTATIC_COMMONdraft that was never wired into any function table.quickjs-predicate.c:754-758,quickjs-inspect.c:838-871(34-line disabled exponent- stripping number formatter),quickjs-inspect.c:1156-1175(disabled[ClassName]fallback tag) — superseded alternates, safe to delete.quickjs-lexer.c:834-849—Lexer.prototype.back()only accepts a token/location object; a disabled branch would have let callers pass a raw string instead (currently throwsTypeErrorfor that case).quickjs-path.c:140-152— a disabled, superseded duplicate ofPATH_REALPATHhandling insidejs_path_method(the live implementation isjs_path_method_dbuf+path_realpath3, registered and working atquickjs-path.c:698— not a missing feature, just dead leftover code confusingly shaped like one).src/glob.c:582(pre-existing TODO-style comment) — `/* TODO: don't call for ENOENT or ENOTDIR? */`, minor optimization.wasmmodule was scaffolded inCMakeLists.txt(theoption(MODULE_WASM ...)declaration itself is commented out at line 51,BUILD_LIBWASMdefaults off) but noquickjs-wasm.cexists anywhere — either finish it or remove the deadif(MODULE_WASM)block (CMakeLists.txt:418-447).- Minor hygiene: stray
src/utils.c.origbackup file left in the tree;quickjs-stream.chas ~13 functions with unfilled Doxygen placeholder text ({ function_description }etc.).
Tier 6 — quickjs-2026 forward-compatibility (found during 2026-07-23 assessment, see ASSESSMENT.md)
- No fallback if
HAVE_DBUF_CLAIMis false against a given reference tree. Thedbuf_realloc()→dbuf_claim()migration (commit3e8d44cc) converted all 15 call sites to calldbuf_claim(buf, delta)directly, withCMakeLists.txt:555-571defining-Ddbuf_realloc=dbuf_claimonly for theHAVE_DBUF_CLAIMcase. There's no inverse shim (#define dbuf_claim(...) ...in terms ofdbuf_realloc) for building against an older reference tree that only hasdbuf_realloc()— which is what/mnt/data/Projects/plot-cv/quickjs's currentcutils.h/cutils.cactually expose. Worth adding a small inline shim (delta → total-size wrapper) so the build works both ways instead of only forward. - Dead, now-backwards
#define dbuf_realloc dbuf_claimatinclude/defines.h:9-11(already#if 0'd out) — safe to delete now that the CMake-level wiring is the real mechanism; keeping it around next to live compat logic invites confusion about which one actually does the job. - Additional disabled-code items found by the same pass, same shape as Tier 1/5 (commented-out
case/branch, feature silently missing rather than erroring):
quickjs-lexer.c:1611,1655(iteratornext/valuesonLexerdisabled),quickjs-list.c:567(iteratornextdisabled — verify this isn't already superseded by theList.prototype.at()removal noted in Tier 4),quickjs-misc.c:3592,3722,3725,3851(+ matching disabledcases at2806-2808,2822) —realpath,resizeArrayBuffer/searchArrayBufferalias, andisHTMLDDA/ function-type magic dispatch all disabled,quickjs-pgsql.c:1312,1855(escapeStringand iteratornextdisabled),quickjs-internal.c:558,571(opcode-name introspection properties disabled),quickjs-tree-walker.c:167,486(setroot()'s return value discarded — minor, probably harmless but worth a look). - Test coverage gaps: no dedicated test file for
arraybuffer-sink,bcrypt,queue,syscallerror, orvirtual(bjsonis upstream-documented as test-only, lower priority).
Tier 7 — roadmap gaps: JS standard-library surface (goal 1) vs. what exists
Native bindings that currently have no lib/*.js wrapper at all, so they're usable only as
raw native modules rather than as part of a documented "standard library" surface: blob
(despite Blob.prototype.stream() already being tracked in Tier 2 — there's no lib/blob.js
at all, not just an incomplete method), child-process, gpio, serial, mmap, directory,
queue, repeater, virtual, magic, bcrypt, syscallerror, location. sockets has only
a low-level lib/socklen_t.js helper, not a net/dgram-style ergonomic wrapper.
WHATWG/Deno/Bun API gaps in lib/:
fetch— missing; only appears in vendored test-infra comments (lib/testharness.js).structuredClone— only feature-detected (lib/stream.js:533), never implemented.Worker— missing; only referenced by vendored test-infra (lib/testharness.js:254).lib/readline.js(9 lines:cursorTo/clearLineonly),lib/buffer.js(12 lines:from/concatonly),lib/perf_hooks.js(12 lines:now/timeOriginonly, no marks or measures) are all much thinner than their Node/Deno/Bun namesakes.lib/extendAsyncFunction.js:3— declared but empty (`AsyncFunctionExtensions = nonenumerable({})`, no members added yet).
Tier 8 — architecture cleanup (goal 3 dogfooding, code duplication)
- Three independent CSS-selector implementations:
lib/parsel.js(portedparsel-js),lib/css-selectors.js(compiler built onparsel.js), andlib/css3-selectors.js(a second, independent compiler with its own hand-rolled tokenizer, duplicating helper functions nearly verbatim fromcss-selectors.js, e.g.escapeRegExp/getAttribute/hasAttribute/isElement/childElements).lib/css-selectors.jsappears dead: nothing in the active source tree imports it (lib/dom.js:3,tests/test_dom.js:4, andtests/test_css3_selectors.js:2all importcss3-selectors.jsinstead; only stale build output underinst/still referencescss-selectors.js/parsel.js). Worth either deletingcss-selectors.jsor consolidatingcss3-selectors.jsto build on the sharedlib/lexer/lib/parser/grammar.jstoolkit (goal 3) instead of duplicating a tokenizer. - Lexer/parser toolkit (goal 3) isn't dogfooded by the project's own hardest parsing
problems.
lib/parser/grammar.js+ the nativelexermodule are genuinely reused across 5 independent grammars (lib/lexer/{bnf,c,csv,ecmascript,xml}.js—lib/xml/read.jswas rewritten to driveXMLLexer/lib/lexer/xml.jsas a JS port ofjs_xml_parse(), verified against the nativexml.read()/xml.write()for tree shape, option surface, and formatting quirks;lib/xml/write.jsis a matching port ofjs_xml_write()), which is good evidence of real generality — butcss3-selectors.jsstill hand-rolls its own tokenizer instead of building on the toolkit. - Inconsistent non-enumerable-property idiom across
lib/extend*.js. Most files (extendArray.js,extendArrayBuffer.js,extendAsyncFunction.js,extendFunction.js,extendMap.js,extendSet.js) wrap their extension object in the sharednonenumerable()helper fromlib/util.js.extendMath.jsandextendGenerator.js/extendAsyncGenerator.jsinstead re-implement the same marking logic inline.extendObject.jsuses a third helper,extend(), with no non-enumerable marking at all. Worth converging on one convention. - Stray untracked working-tree files noticed during the survey (not a code bug, just hygiene):
lib/blah.tmp*(six 0-byte scratch files),lib/repl.js.orig(a stale backup that differs from the currentlib/repl.js).
Tier 9 — DOM API implementation priorities (browser sandbox, goal 1)
The lib/dom.js DOM implementation has the core browser APIs in place. The following
classes are done: EventTarget, Event, CustomEvent, UIEvent, MouseEvent,
KeyboardEvent, FocusEvent, InputEvent, WheelEvent, Touch, TouchList,
TouchEvent, PointerEvent, PopStateEvent, HashChangeEvent, History, DOMRect,
DOMRectReadOnly, Range, Selection, MutationObserver, HTMLElement (with dataset,
style, hidden, tabIndex, offsetWidth/offsetHeight/offsetTop/offsetLeft/offsetParent,
clientWidth/clientHeight/clientTop/clientLeft,
scrollWidth/scrollHeight/scrollTop/scrollLeft, etc.), 50+ HTMLElement subclasses
(Input, Button, Form, Anchor, Image, TextArea, Select, Option, Script, Style, Link, Media,
Video, Audio, Table, etc.), DocumentFragment, Navigator, Location, Storage, Window
(with setTimeout/setInterval/requestAnimationFrame/cancelAnimationFrame/history/getSelection),
File (in lib/file.js), DOMStringMap, CSSStyleDeclaration, NodeList, HTMLCollection.
Element geometry: getBoundingClientRect() and getClientRects() implemented on Element.
Comprehensive test suite exists in tests/test_dom.js (210 tests),
tests/test_event_and_fragment.js, tests/test_event_subclasses.js (50+ tests),
tests/test_history.js (28 tests), tests/test_geometry.js (30 tests),
and tests/test_range_selection.js (60+ tests).
Remaining items ordered by leverage:
9.1 Fetch API (LOWER - modern HTTP client, see also Tier 7)
Why: Network requests for dynamic content.
Implementation:
fetch(url, options)functionRequestclass:url,method,headers,body,mode,credentialsResponseclass:status,statusText,headers,body,ok,json(),text(),blob()Headersclass:get(),set(),has(),delete(),append(), iterationAbortController+AbortSignalfor request cancellation- Promise-based API
Files: lib/fetch.js or lib/dom.js
Status: Not implemented (also tracked in Tier 7).
9.2 FormData (LOWER - form data collection)
Why: Collecting form data for submission.
Implementation:
FormDataclass:append(),delete(),get(),getAll(),has(),set()FormData(form)constructor to collect from<form>element- Iteration support:
entries(),keys(),values()
Files: lib/dom.js
Status: Not implemented.
9.3 CSSOM - CSS Object Model (LOWER - computed styles and media queries)
Why: Reading computed styles and responsive design.
Implementation:
window.getComputedStyle(element)→ fullCSSStyleDeclaration(currently a stub)window.matchMedia(query)→MediaQueryList(currently a stub)MediaQueryList:matches,media,addListener(),removeEventListener()StyleSheet,CSSStyleSheet,CSSRuleclasses (lower priority)
Files: lib/dom.js
Status: CSSStyleDeclaration class exists. getComputedStyle() and matchMedia() are stubs returning empty values.
9.4 IntersectionObserver (LOWER - viewport visibility detection)
Why: Lazy loading, infinite scroll, analytics.
Implementation:
IntersectionObserverclass: constructor with callback and optionsobserve(element),unobserve(element),disconnect()IntersectionObserverEntry:target,isIntersecting,intersectionRatio
Files: lib/dom.js
Status: Not implemented.
9.5 ResizeObserver (LOWER - element size change detection)
Why: Responsive components, layout adjustments.
Implementation:
ResizeObserverclass: constructor with callbackobserve(element),unobserve(element),disconnect()ResizeObserverEntry:target,contentRect
Files: lib/dom.js
Status: Not implemented.
9.6 File + Blob remaining APIs (LOWER - see also Tier 2/7)
Why: File uploads, downloads, binary data.
Implementation:
Blob.prototype.stream()— broken (see Tier 2)FileList: array-like collection of FilesFileReader:readAsText(),readAsDataURL(),readAsArrayBuffer(),onload,onerrorURL.createObjectURL(blob),URL.revokeObjectURL(url)
Files: lib/dom.js, lib/file.js
Status: File class (in lib/file.js) and Blob (native binding) exist. stream(), FileList, FileReader, and object URL methods are missing.
9.7 WebSocket (LOWER - real-time communication)
Why: Bidirectional real-time data.
Implementation:
WebSocketclass: constructor with URL- Properties:
readyState,bufferedAmount,protocol - Methods:
send(data),close() - Events:
onopen,onmessage,onerror,onclose
Files: lib/websocket.js
Status: Not implemented.
9.8 Canvas API (LOWER - 2D graphics, games)
Why: Image manipulation, games, visualizations.
Implementation:
HTMLCanvasElement:width,height,getContext()CanvasRenderingContext2D: drawing methods, transformations, gradients, patterns- This is large—implement incrementally based on usage
Files: lib/canvas.js
Status: Not implemented. HTMLCanvasElement stub exists (just width/height).
9.9 Web Workers (LOWER - background threads, see also Tier 7)
Why: Heavy computation without blocking main thread.
Implementation:
Workerclass: constructor with script URLpostMessage(data),terminate()- Events:
onmessage,onerror - Requires separate execution context
Files: lib/worker.js
Status: Not implemented (also tracked in Tier 7).
Tier 10 — C API consolidation and cleanup
Low-usage or redundant C APIs in include/ and src/ that should be consolidated,
inlined, or removed to reduce maintenance burden and code duplication.
10.1 BitSet only used by one module (LOW - inline)
Problem: bitset.h/bitset.c (106 lines) has only 2 uses:
- Used only by
src/bitset.citself - Used only by
include/json.h(which is used byquickjs-json.c)
This is a small utility that's only needed by one consumer.
Recommendation: Inline bitset.h/bitset.c into json.c or keep it as a simple
dependency since it's small and well-isolated.
Files: include/bitset.h, src/bitset.c
Impact: Removes 106 lines if inlined, or keep as-is (minor cleanup opportunity).
10.2 async-closure.h only used by MySQL (LOW - inline)
Problem: async-closure.h/async-closure.c (166 lines) has only 2 uses:
- Used only by
quickjs-mysql.c - Used only by itself (
src/async-closure.c)
This is a MySQL-specific async handler pattern.
Recommendation: Inline async-closure.h/async-closure.c into quickjs-mysql.c or
move to quickjs-mysql.h. This is MySQL-specific functionality that doesn't need to be
a general-purpose API.
Files: include/async-closure.h, src/async-closure.c
Impact: Removes 166 lines, clarifies that this is MySQL-specific code.
10.3 child-process.h only used by one module (LOW - inline)
Problem: child-process.h/child-process.c (525 lines) has only 2 uses:
- Used only by
quickjs-child-process.c - Used only by itself (
src/child-process.c)
This is a large module that's only consumed by one binding.
Recommendation: Inline child-process.h/child-process.c into quickjs-child-process.c.
The header can remain as quickjs-child-process.h if needed for external use, but the
internal implementation doesn't need to be a separate module.
Files: include/child-process.h, src/child-process.c
Impact: Simplifies module structure, though no line count reduction (just consolidation).
Summary
Total lines to potentially consolidate: ~797 lines
- BitSet: 106 lines (inline into JSON parser or keep as-is)
- async-closure: 166 lines (inline into MySQL)
- child-process: 525 lines (inline into binding, no net reduction)
Priority order:
- LOW: Inline BitSet (10.1) - simplifies dependencies (minor)
- LOW: Inline async-closure (10.2) - clarifies MySQL-specific code
- LOW: Inline child-process (10.3) - simplifies structure
Completed:
- ~~10.3 RingBuffer~~ - Removed in commit
7cda4dec(163 lines deleted)
Removed (incorrect assessment):
- ~~10.1 JSON parsers~~ - Not duplicates:
json.his a pull parser,jread.his a push/SAX parser,sj.his a simple one-shot parser. They serve different APIs. - ~~10.2 XML parsers~~ - Not duplicates:
xml.his a pull parser,xread.his a push/SAX parser. They serve different APIs. - ~~10.5 ioctlcmd.h~~ - Actually used by
src/readlink.cfor Windows symlink support.
Tier 11 — src/qjsm.c refactoring opportunities (found during 2026-08-16 read-through)
A full read-through of src/qjsm.c (the qjsm entry point / module loader) turned up no
correctness bugs, but several structural spots worth revisiting. Dead/commented-out code and
one obscure reversed-subscript idiom ((dsl = path_dirlen1(path))[path]) were already cleaned
up in place during this pass; what's left below is genuine restructuring, deliberately not
done inline since each is either large or a judgment call on API shape.
main()is a ~450-line monolith doing CLI parsing, runtime/context setup, script and-I/-mloading, REPL bootstrap, and (behind--dump --quit) an unrelated instantiation-time microbenchmark, all in one function. Splitting intojsm_parse_args(),jsm_setup_runtime(),jsm_run_scripts(), andjsm_bench_instantiation()would make each piece testable/readable in isolation. Nontrivial: the pieces share a lot of local state (had_error,sargs,include_list, ...) that would need to move into a small context struct or be threaded through as parameters.jsm_module_func()is a single ~270-line function dispatching on a 20-casemagicenum that mixes unrelated concerns: module bookkeeping (ADD_MODULE/FIND_MODULE/FIND_MODULE_INDEX), path resolution (NORMALIZE_MODULE/LOCATE_MODULE/LOAD_MODULE), and a block of ~11 near-identical#if QUICKJS_INTERNALone-liner accessors (GET_MODULE_NAME/GET_MODULE_VALUE/GET_MODULE_INDEX/...). The accessor block in particular is repetitive enough to be table-driven (an array of{magic, module_*_fn}pairs looked up once) instead of 11 near-identicalcase:/#if/#endifblocks.jsm_module_loader()(theJSModuleLoaderFuncimplementation) does six distinct things in one ~140-line function with severalgoto end;/goto again;jumps:data:URL handling, dispatch through the external loader chain (jsm_call_loaders), circular-import detection,package.jsonalias resolution, builtin-module lookup, and filesystem resolution + the "could not load module" error formatting. Worth splitting along those seams (e.g.jsm_resolve_data_url,jsm_run_external_loaders,jsm_warn_circular) so each piece can be reasoned about independently — risky to do without first having a test that exercises each branch (see thetests/test_list.jsgap noted in Tier 4 for why that matters here).- CLI flag variables in
main()are declaredchar(dump_memory,trace_memory,empty_run,module,load_std,list_modules— e.g.-d -d -d ...incrementsdump_memoryviadump_memory++) rather thanint/BOOL. Not currently reachable (argc is bounded), but the type doesn't communicate "counter" and invites a subtle bug if the parsing loop is ever restructured. - The hand-rolled long-option parser in
main()(`/* cannot use getopt because we want to pass the command line to the script */`) is a legitimate constraint, but the resulting loop is a ~150-line sequence ofif(opt == 'x' || !strcmp(longopt, "xxx")) { ...; break; }blocks that's easy to get subtly wrong when adding a new flag. Could become a small{shortopt, longopt, handler}table walked by one loop, without pulling in libcgetopt. - Global mutable state is spread across ~10 independent
thread_local/staticvariables (jsm_stack,jsm_builtin_modules,module_list,debug_list,module_loaders,loaded_modules,package_json,exename/exelen,jsm_rt/jsm_ctx,interactive,DEBUG_MODULE) instead of one groupedstruct. Not urgent, but would make the module's state easier to audit and would be a prerequisite for ever running more than oneqjsmruntime per process. jsm_search_suffix()'s debug trace stringifies a function pointer by identity comparison (`fn == &is_module ? "is_module" : fn == &jsm_search_path ? "jsm_search_path" : "<unknown>") to name theModuleLoader*passed in. Silently prints"<unknown>"` if a third implementation is ever added. A small named-enum-plus-lookup (or just naming the parameter at each call site in the trace) would stay correct automatically.
Tier 12 — deferred: yaml module read() (YAML → JS)
quickjs-yaml.c currently only implements write() (JS value → block-YAML text), added for
the eagle-agent EDA parts catalog export (see doc/eagle-agent.md). Parsing was explicitly
out of scope for that work and is deferred:
A
read(text)function, symmetric withwrite(), decoding the same block-YAML subset back into a JS value.If/when that's built, model it as a computed-goto push/SAX parser in
src/yread.c/include/yread.h, the same patternsrc/jread.c/include/jread.h(JSON) andsrc/xread.c/include/xread.h(XML) already use, rather than a recursive-descent parser inline inquickjs-yaml.c.Scope stays pinned to the writer's own subset (block style, plain/quoted scalars, no anchors/aliases/multi-doc/flow/tags) — no need to handle full YAML 1.1/1.2.
Superseded by Tier 13 below if cyaml adoption goes ahead: cyaml's
cyaml_parse()+cyaml_emit()would cover bothread()andwrite()(full YAML 1.2, not just this writer's subset), making a hand-rolledsrc/yread.cpush parser unnecessary. Leaving this entry in place until that decision is made.
Tier 13 — adopt cyaml as the yaml module's backing library (plan only, not started)
quickjs-yaml.c currently hand-rolls its own block-YAML writer (js_yaml_write(),
quickjs-yaml.c:301-335, ~200 lines of DynBuf-based emission logic covering only a
restricted subset — see Tier 12 above) and has no reader at all. Replace both with
cyaml (MIT, C11, zero dependencies beyond libc,
passes the full yaml-test-suite), vendored as a git submodule.
Submodule placement: put it at 3rdparty/cyaml, not the repo root (where every other
submodule — libarchive, pigpio, libutf, tutf8e, libserialport, libbcrypt —
currently lives; only third_party/wasm3 is already namespaced, under the old directory
name third_party/, not 3rdparty/). This is meant to be the first of a broader cleanup:
move all existing root-level submodules into 3rdparty/ (and fold third_party/wasm3 in
too, i.e. rename third_party/ → 3rdparty/) so vendored code stops cluttering the repo
root — tracked here as a follow-up, not bundled into the cyaml change itself.
API shape — full-tree only, no evented/pull parser: confirmed by reading src/cyaml.h
(and the README's "Event stream output" feature) that cyaml has no SAX-style push parser
(no read-callback/handler registration) and no incremental pull parser (no "get next
event" call that holds cursor state across calls) comparable to this project's own
src/jread.c/src/xread.c computed-goto push parsers or the pull-style primitives
XMLPushParser/JSON's streaming reader expose. cyaml_parse()/cyaml_parse_stream()
take the entire source buffer up front and return a fully-built cyaml_doc_t* node tree;
there is no way to feed it chunks incrementally or stop after N events. cyaml_events()/
cyaml_stream_events() sound like a streaming API from the name, but they are the
opposite: they take an already fully-parsed cyaml_doc_t* and serialize it back out as
a textual canonical event-log string (the same event-log format yaml-test-suite uses for
its conformance fixtures) — a dump format, not a live callback/cursor API. Implication for
this module: read() will have to be a whole-buffer-in, whole-JS-value-out conversion
(cyaml_parse() → walk the cyaml_node_t tree → build the JS value), same shape as the
existing JSON module's non-streaming parse(), not the incremental push/pull style used
for XML. If an evented/pull YAML reader is ever wanted, cyaml won't provide it — would
still need the hand-rolled src/yread.c design sketched in Tier 12, on top of or instead
of cyaml.
Plan:
- Add
3rdparty/cyamlsubmodule (https://github.com/andrewmd5/cyaml), wire intoCMakeLists.txt(cyaml ships its ownCMakeLists.txt; likelyadd_subdirectory()withCYAML_BUILD_TESTS/CYAML_BUILD_SHAREDoff, static-link intoquickjs-yaml.c's module). - Replace
js_yaml_write()'s hand-rolled emission withcyaml_doc_new()+cyaml_new_*()/cyaml_map_set()/cyaml_seq_push()tree construction from the JS value, thencyaml_emit(). Drops the current subset restrictions (flow style, anchors/aliases, multi-doc all become available "for free"). - Implement
read()viacyaml_parse()+ acyaml_node_t→JSValuewalk (usingcyaml_scalar_kind()/cyaml_as_int()/cyaml_as_float()/cyaml_as_bool()/cyaml_scalar_str()for scalars, iterate seq/map nodes for containers). - Decide whether to also expose YPATH (
cyaml_path()/cyaml_path_query()) as ayaml-module convenience, or leave that as adeep/pointer-module-style concern — default to not exposing it initially (avoid growing custom API surface per the "internal vs public APIs" project principle), revisit only if a concrete use case shows up. - Update
doc/native/yaml.mdfor the newread()export and the widenedwrite()compliance (full YAML 1.2 vs. today's documented subset). - Remove/resolve the Tier 12 entry above once this lands (either fold its
read()request into this work, or explicitly drop the yread.c push-parser design if cyaml covers the need without it).