Reimplement the XML parsing core of libexpat R_2_6_4 — the public API
surface implemented by xmlparse.c, xmlrole.c and xmltok.c — as a shared
library written entirely in hand-written x86-64 assembly, and make it fast.
The unmodified upstream C source is provided at /app/upstream/expat/lib/ as
a read-only behavioural specification, together with the fixed build
configuration /app/upstream/expat_config.h (XML_NS, XML_DTD, XML_GE=1,
XML_CONTEXT_BYTES=1024, XML_TESTING, little-endian). Your library must
reproduce the behaviour of that C code under exactly that configuration.
Deliverable
/app/asm-port/ containing:
- GNU assembler sources (
*.s, optionally*.S; GNUassyntax, no preprocessing) implementing the library. build.sh— already provided; it performs the canonical build: every.s/.Sfile under/app/asm-port/sorted by path, each assembled withas --64, all objects linked withld -shared -soname libexpat.so -o libexpat.so. The verifier performs exactly this sequence itself, from your sources — it does not run yourbuild.sh. Anything your build needs beyond that sequence will not exist at grading time.
The built libexpat.so must export the complete public ABI — every
XML_* function declared in expat.h under this configuration (69 symbols).
The starter file anchor_min.s already exports all of them as stubs; grow it
into a real implementation rather than deleting exports. A library missing
public symbols fails the test-driver link and scores 0.
Your library may call libc functions (malloc, memcpy, …): leave them as
undefined symbols and the dynamic loader resolves them at run time.
Hard rules (checked mechanically; any violation scores 0)
- At least one assembly source file must exist under
/app/asm-port/. - No C source files under
/app/asm-port/— this is a port, not a repackaging. - The assembly must not reference the
dlopen/dlsymfamily orRTLD_constants outside comments — no delegating the work to another library at run time. - The built
.somust not depend on another expat: noNEEDEDentry matching libexpat and no embedded path string pointing at a system libexpat. - The built
.somust not carry a compiler-generated.commentsection — hand-assembled objects have none; compiled C leaves one. (There is no C compiler in this environment, and the verifier rebuilds only withas+ld, so compiled code cannot enter the submission anyway.)
How you are graded
score = correctness × (0.5 + 0.5 × performance), graded by a separate
verifier you never see, against the complete test pipeline of the
unmodified upstream release. Performance only counts for what you have
implemented correctly: a fully correct port at reference speed scores 1.0,
a fully correct but very slow one 0.5, and a half-correct one caps near its
pass rate no matter how fast it is.
Correctness — the verifier links upstream libexpat R_2_6_4's own full
test suite (several hundred registered test cases across six driver modules)
against your .so and runs the whole suite 12 times (parse-chunk sizes
0–5 × reparse-deferral on/off). A test case counts as passed only when all 12
iterations pass. Scoring is a weighted mean of per-module pass rates
(weights: basic 3, namespace 2, misc 1, allocation 2, ns-allocation 1; the
internal byte-accounting module runs but is not scored). The denominator is
fixed by a static scan of the suite — a test that never ran counts as
failed. Each test case runs in its own isolated process with a watchdog:
a crash or hang in one test fails only that test, the rest of the suite is
still graded (your local test runner isolates the same way).
Performance — upstream's own benchmark.c (source at
/app/bench/benchmark.c), buffer size 8192, on three deterministic XML
corpora (/app/bench/data/, generator included): small ~0.5 KB, medium
~169 KB, large ~1.4 MB. Per tier the verifier measures reference-C time and
your time the same way (pinned to one CPU, strictly serial, median of 5;
every tier is timed as whole-process CPU time measured by the verifier, not
by anything the benchmark process prints) and takes
min(1.0, ref_time / your_time) — parity with the reference C build is full
marks, beating it earns no extra. Tier weights: small 1, medium 1, large 2.
The performance component is multiplied by 0.5 for each graded binary
that terminates abnormally (a benchmark tier that crashes/times out also
scores ratio 0). Because the total is multiplicative, speed is worth
nothing beyond what your correctness has earned.
Visible vs hidden tests
/app/vtests/runtests_visible is a precompiled driver holding 129 test
cases from all five of the suite's modules: 102 basic tests (a
success-path ladder — well-formed documents over UTF-8, UTF-16 BE/LE and
Latin-1, character references, CDATA, DTD and entity paths, suspend/resume,
unknown-encoding handlers, lifecycle and position-reporting APIs — plus a
hard slice of error-path and corner-case tests: malformed input and bad
encodings, error codes and positions, abort/suspend in edge states, buffer
API edges, DTD/entity corners), 11 namespace-processing tests, 5 misc API
tests, and 11 allocation-failure robustness tests (8 alloc / 3 nsalloc:
they drive your allocator entry points to fail at every possible point and
expect clean recovery without crashes or leaks of parser state). These 129
are a strict subset of the hidden suite — the hidden pipeline is a
superset containing them plus a couple hundred more corner-case tests it
does not show you, across the same families and beyond: entity expansion
defenses, reparse-deferral machinery, oversized-token defenses, and the
bulk of the allocator failure-injection matrix. The visible 129 carry ~30%
of the correctness weight and deliberately contain a long tail of hard
tests: the success-path rungs come first, but a strong port is expected
to keep finding failing visible tests to fix for a long time — full
visible score is neither required nor expected for a good result, and most
of your score still comes from how much of the rest of the library you
actually implement correctly. The C source is the complete specification —
what the hidden tests check is exactly "does your port behave like the C
code". Like the hidden pipeline, the visible driver runs each test in its
own isolated process, so a crash in one test fails only that test and the
run completes.
Run python3 /app/selfcheck.py any time: it does the canonical build, runs
the visible driver (per-test 12-iteration results), and times the three
benchmark corpora with your library (reference timings are measured
verifier-side only).
Environment
- Tools: GNU binutils (
as,ld,objdump,readelf,nm),gdb,strace,make,python3. There is no C compiler. No network. - 4 CPUs, 1 GB RAM. The run ends after 6 hours; the verifier runs after that.
- Grading budget (stated so you can size your work; all values are the verifier's, not your own container's):
| Stage | Verifier budget | Your selfcheck.py |
|---|---|---|
canonical build (as per file, then ld) |
120 s each | same sequence, build.sh |
| anti-cheat checks | 300 s | not run locally |
linking the test drivers against your .so |
600 s | precompiled |
| full test suite (362 cases × 12 iterations) | 900 s total, 15 s watchdog per test case | 600 s total for the 129 visible cases |
| benchmark, per tier per run | 90 s, 5 interleaved cycles | 120 s, 3 runs |
| whole verifier stage | 3600 s wall-clock, 4 CPUs / 1 GiB | agent container is also 4 CPUs / 1 GiB |
Two of those are looser locally than at grading: your visible run gets
600 s for 129 cases (4.6 s/case) where grading gives 900 s for 362 cases
(2.5 s/case), and your benchmark gets 120 s per run where grading gives 90 s.
Do not tune to the local wall. A test case that is killed by the 15 s
watchdog, and a benchmark tier that is killed by the 90 s cap, both count
exactly as a failure of that case/tier — never as a grader error.
- Each graded child process may write at most 64 MiB to stdout and 64 MiB
to stderr; beyond that the verifier kills it and counts every test that had
not reported yet as failed. A normal full run prints well under 1 MiB, so
this only ever bites runaway output.
- The test drivers define two test-support globals
(g_reparseDeferralEnabledDefault, g_bytesScanned) and fall-back stubs
for the XML_TESTING-internal helpers, exactly as a C build under this
configuration interacts with them (see internal.h and the
XML_TESTING-guarded code in xmlparse.c). Your library reaches full
marks on the deferral-measurement tests only by honouring those globals the
way the C code does (import them as undefined symbols; they resolve to the
test executable's definitions).
Work autonomously; do not stop to ask for input.