UCSC CSE 111 · Advanced Programming
CSE 111 — dc-bigint REPL (UCSC, Winter 2022)
The asg1 assignment for CSE 111 (Advanced Programming): a Unix-style desk calculator with arbitrary-precision integer arithmetic. Implements bignum +, −, ×, ÷, %, ^ from scratch over a vector<uint8_t> of decimal digits, then drives a stack machine on top. The bigint/ubigint classes from the original course homework are compiled to WebAssembly via Emscripten — a thin shim layer around the unmodified arithmetic exposes a JS-callable stack API for the live REPL. Several arithmetic bugs in the original course code were tracked down and fixed during the WASM port (see commit history for details: ubigint(unsigned long) constructor leaked size-N zero-prefix, operator+ never pushed the final carry, divide_by_2 walked the wrong direction, operator< returned wrong on size-equal cases).
Full Unix dc(1)-style stack-machine calculator over arbitrary-precision integers. Compute 2^1000 (302 digits) live in the browser, factorials, large divisions with remainders, signed arithmetic. The bigint/ubigint algorithms come straight from the course assignment — no fast paths to native long arithmetic; every digit goes through hand-written carry-aware decimal multiplication and binary shift-and-subtract long division.
Overview
README.md ↗This repo packages the dc-bigint assignment from CSE 111 (asg1): a Unix-style desk calculator (dc(1)) with arbitrary-precision integer arithmetic implemented from scratch in C++. The unsigned-bignum ubigint does carry-aware decimal arithmetic (+, −, ×, ÷, %, multiply/divide-by-2 primitives, binary long-division as the workhorse), and the signed bigint layers sign handling on top. The pow() function uses fast exponentiation (square-and-multiply) over the bigint type.
Live demo
https://codeseys.io/projects/cse-111-bigint
The browser-based REPL is the original course CLI's stack machine wired to a JavaScript text input: type 2 1000 ^ p and watch 302 digits of 2^1000 print back. The bigint/ubigint algorithms are unchanged from the course submission — only the I/O boundary is different.
Bugs fixed during the WASM port
The original course code had four arithmetic bugs that all needed fixing before the demo would produce correct answers:
ubigint(unsigned long)constructor: had: uvalue(that)in the member init list, which callsvector<uint8_t>(that)— i.e. constructs a vector ofthatzero-initialized elements — soubigint(1)ended up as[0, 1](a vector pre-populated with one zero, then'1'appended), which displays as"10"decimal. For any positive value the bug yieldedvalue × 10^value.ubigint{1}andubigint{2}andubigint{0}all stopped meaning what they're named, breaking every literal-from-int construction inpow()andudivide(). Fix: empty the vector and only append digits, then trim.ubigint::operator+: the final carry was never pushed onto the result. For5 + 8 = 13, after the digit-pair loop sum = 3 with carry = 1, but neither tail loop runs (both indices past end), so1was lost and5+8printed as3. Fix: push the residual carry after the three loops.ubigint::divide_by_2: walked low-to-high and tried to readuvalue[i+1]before that digit had been halved — plus accessed past the end of the vector. The correct algorithm walks high-to-low, carrying the remainder from each digit into the next. Fix: rewrite tofor (int i = size-1; i >= 0; --i)with a 0/1 carry from the previous iteration.ubigint::operator<: in the same-size case, the per-digit scan returnedfalseonthis[i] > that[i]even when higher-order digits had already establishedthis < that. For12 < 50: high digits 1 vs 5 → 1 > 5 false (so don't return), continue to low digits 2 vs 0 → 2 > 0 → return false, declaring12 < 50to be false. Many divisions were wrong because of this. Fix: at the first differing digit (high to low), return whetherthis[i] < that[i].ubigint::operator*(fixed earlier in the audit, before this attempt):temp[i+j] = temp[i+j] + this[i]*that[j] + carrywithout% 10left digit slots holding values like 25 from5*5, which printed as a literal"25"and made5*5display as"225".
After these fixes, the original pow() algorithm in libfns.cpp works correctly because it's algorithmically right — it was just being driven by broken primitives. Everything verified against Python: 2**1000, 30!, 2^31-1 all match byte-for-byte.
Layout
src/ — original asg1 sources (with the bugs fixed inline)
bigint.{cpp,h} — signed bigint
ubigint.{cpp,h} — unsigned bigint (the actual arithmetic)
iterstack.h — std::deque-backed stack with iteration
libfns.{cpp,h} — pow() over bigint
scanner.{cpp,h} — input tokenizer
util.{cpp,h} — error type, env helpers (CLI-only; not in WASM)
debug.{cpp,h} — DEBUGF macro
main.cpp — original CLI driver (kept for reference)
embeds-wasm/dc-bigint/ — Emscripten shim layer + REPL HTML harness
scripts/build-embeds.sh — CI build script
web.codeseys.json — Manifest for codeseys.io
Build pipeline
- Push to
mastertriggers.github/workflows/build-web-asset.yml, which calls the reusable workflow atbaladithyab/web-embed-workflows@main. - Manifest declares
build.tools: ["emsdk"]so the workflow installs Emscripten (cached after first run). scripts/build-embeds.shrunsemccoversrc/*.cpp(minusutil.cppandmain.cpp) plus the wasm shim, producingembeds/dc-bigint.{js,wasm,html}.- The workflow uploads everything in
embeds/to Cloudflare R2 atcse-111-bigint/<git-sha>/, authenticated via OIDC (no static API keys).
Running the original CLI
After the bug fixes, the CLI also works correctly:
g++ -std=gnu++2a -O2 -o ydc src/*.cpp
echo '2 100 ^ p' | ./ydc
# → 1267650600228229401496703205376
echo '1 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9 * 10 * p' | ./ydc
# → 3628800
Running locally
# Install emsdk (one-time)
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk && ./emsdk install latest && ./emsdk activate latest
source emsdk_env.sh
# Build
cd ../UCSC-CSE-111-BigInt
EMBED_SOURCE_DIR=embeds ./scripts/build-embeds.sh
# Serve
python3 -m http.server -d embeds 8080
# Open http://localhost:8080/dc-bigint.html