Unescape HTML · WAT 🤝 Fable 5.2 · #3 Smallest
SHA-256
bea3ba1914a0cd74506507a07e9b1b2318db19d32bb38485ee755e8b78a35d71
View source (62.5 KiB of WAT)
;; unescape_html in hand-written WebAssembly text with SIMD: decodes every HTML character
;; reference in UTF-8 text the way Python's html.unescape does, and copies all other bytes
;; unchanged.
;;
;; The design is the one from my C entry, written directly so that nothing a compiler adds
;; is left: no stack, no stack pointer, every table at a fixed address.
;; - 64 zero bytes are written after the input, so no load or loop needs a bounds check.
;; - Two stages per 4 KiB window. Stage 1 turns each 64-byte block into a bitmask of its `&`
;; and appends their positions to a list. Stage 2 walks the list: copy the text since the
;; last reference (32 bytes unconditionally, more in a loop), then decode.
;; - Inputs without `&` are returned where they are; nothing is copied.
;; - A name of up to 7 bytes ending in `;` is looked up without measuring it: the first `;`
;; in the next 8 bytes is found with a SWAR zero-byte test, the bytes up to it are the key,
;; and one multiply indexes an open-addressed table of 16-byte entries (key, value).
;; - Longer names go through a second table and one masked 32-byte vector compare.
;; - Without `;` only the 106 legacy names match. A filter on the first two bytes gives the
;; longest legacy name that could start there, usually none.
;; - Numeric references of up to 7 digits, decimal or hexadecimal, share one branch-free
;; SWAR path.
;; - The entity list is stored packed (6.4 KB for 2,125 names) and unpacked on the first
;; call: see $sym and $init. pack.py, at the end of this file, is the matching packer.
;;
;; Memory map (bytes):
;; 0 packed entity list, then the Windows-1252 table, model rates, UTF-8 marks
;; 6592 ctx: the contexts of the symbol being unpacked; 6608 slot: their model entries
;; 6624 name: the name being unpacked, zero padded
;; 6672 leg_max: longest legacy name starting with two bytes (hashed), or 0
;; 10768 long_slots: hash of a long name -> its number in `longs`, or 0
;; 14864 last: match model, where two characters were last seen
;; 23056 pos: positions of `&` in the current window
;; 39504 hist: every name character unpacked so far
;; 55648 longs: 48-byte records (name[32], value, length) for names of 8 bytes or more
;; 79936 fast: (key, value) entries for names of up to 7 bytes; empty key = all ones
;; 145984 leg: the same for legacy names, keyed with the length in the top byte
;; 150336 len_mask: 32 bytes of 0xFF, then 32 of 0
;; 150400 output, and 262144 bytes into the same buffer:
;; 412544 input, 1048576 bytes and padding
;; Output and input share one buffer, a technique from my earlier entries (4a6e243a and
;; others): a reference grows by at most 1.2x (`≫⃒` is 5 bytes in, 6 out), so the write
;; position stays at least 52429 bytes behind the read position and decoding in
;; place is safe. The bytes before the input also hold the unpacker's model on the first call.
;; A value is its UTF-8 bytes in the low bytes of an i64 and its length in the top byte.
;;
;; Build: wat2wasm unescape.wat -o unescape-wat.wasm
(module
(memory (export "memory") 23 23)
;; Range coder state: the first four bytes of the list are folded into $code here.
(global $range (mut i32) (i32.const -1))
(global $code (mut i32) (i32.const 0xDEE11AE6))
(global $in (mut i32) (i32.const 4))
(global $ulen (mut i32) (i32.const 0))
(data (i32.const 0) "\de\e1\1a\e6\73\31\90\be\3a\8c\cc\da\91\d3\67\29\3c\e4\d7\59\c6\46\88\1f\78\6b\a1\3c\91\75\17\e0\3b\32\52\99\a6\ba\b9\e7\64\5b\5c\85\19\9e\a6\8d\dd\b5\df\5e\54\50\90\2c\6d\0d\64\c2\b1\5f\42\be\ac\b0\b7\b1\0e\d9\5a\7b\7a\89\43\e7\7a\43\ae\25\35\c1\68\c5\9c\c0\91\85\7c\c9\cb\96\45\97\54\78\45\f9\2f\c0\b1\94\59\f4\de\de\57\85\56\55\fb\be\cf\4a\20\69\a2\64\ae\04\9c\b6\20\68\65\29\aa\fc\fe\14\ca\02\4a\df\2a\4d\38\9f\47\01\49\35\5a\9b\32\55\af\3c\9b\a8\c4\9c\ca\20\ad\05\6d\51\1a\65\33\34\95\a3\cb\9c\bb\e9\7f\bf\8e\2c\5b\4c\4f\1b\e9\22\9e\cf\0a\79\3b\92\4b\21\de\e1\52\2a\aa\a3\10\14\f2\c0\28\e8\5c\7d\00\91\2d\ee\1c\46\59\1a\3d\1a\59\1f\e9\ec\3b\3e\92\7a\8b\e4\0f\47\d0\54\a6\62\b7\00\44\12\b6\b3\7e\67\02\bf\59\02\d9\b6\d3\a1\87\4b\19\43\bd\ac\25\07\37\8b\6f\00\bc\69\46\b3\8d\77\e8\38\c7\fa\96\a6\3d\d9\a0\2f\4e\4c\ac\66\f3\01\1e\f8\18\c0\e7\1a\6e\bf\48\c0\61\b3\3d\49\23\4a\93\93\db\bc\8d\73\20\d5\dd\87\15\d8\26\ad\50\4e\af\b5\be\51\ce\a8\8f\a2\46\41\6c\62\7a\79\53\6f\69\28\ad\eb\91\ca\cc\60\3e\d6\de\91\1e\f7\5a\df\fd\2c\2b\24\02\74\40\c3\c3\c2\90\39\f4\9f\b5\23\07\c5\18\b7\cd\47\46\f0\e1\4f\99\06\ea\f5\66\f3\aa\d9\3e\e8\12\27\f3\d6\b5\be\f7\47\1a\dc\cc\58\a1\d7\9a\f2\6e\fc\f7\32\84\da\5f\77\34\da\ed\cd\0f\15\a3\d7\41\36\02\48\7e\e6\ff\89\cc\5c\b6\4a\1a\c6\62\f6\10\95\78\3e\f4\b8\6d\59\b0\a1\6f\25\a2\14\ee\ac\77\98\df\b2\09\09\45\b9\10\82\bf\6e\27\2e\50\ff\ab\d7\f7\a7\1b\c7\8b\d6\a7\7c\98\79\07\0e\df\4f\53\c3\e7\61\30\81\8d\af\25\7d\87\1b\5d\f0\dc\f9\db\36\4d\3c\24\54\fa\21\7a\48\0d\d1\7a\0a\e9\5a\20\4d\95\49\1e\62\e3\0c\c6\b8\eb\b9\24\16\7b\6e\41\bd\6c\0e\0c\11\09\91\a2\70\60\2e\ce\12\43\41\9f\2b\c9\53\84\d3\54\06\05\bc\7d\37\ff\9e\30\fe\b6\ca\8d\e3\c2\ad\d3\79\1a\94\91\72\a8\54\26\79\0e\b5\9a\46\9d\cc\2f\82\56\a2\10\76\7a\46\e6\b0\1b\4e\c3\c3\4c\b1\ad\1d\7e\02\d9\b4\fb\11\62\35\5a\e8\47\3e\de\f2\93\c3\55\19\af\bf\2e\5a\f7\d4\ee\75\2e\3b\99\7d\92\1c\ae\ca\ea\23\3b\ac\39\87\d5\22\ba\12\4f\ff\74\0a\36\03\da\09\eb\24\86\a2\a4\17\46\ec\6c\8b\48\70\86\67\dc\bc\81\eb\7a\06\df\69\96\0b\28\c8\0d\7d\f6\b3\6a\34\e8\02\c0\21\43\52\98\05\c3\64\24\3b\95\62\ec\a2\83\78\7e\85\34\46\f8\74\ae\39\77\43\8b\ea\12\88\97\19\e7\78\e3\56\62\ed\b5\76\4d\a7\ad\dd\bd\84\e0\a5\99\bb\17\32\1e\77\a9\41\99\97\88\93\b9\7d\3d\f6\a3\e4\f2\b8\da\14\96\cc\ca\fd\d8\cf\f6\7d\fb\22\cb\15\83\53\80\f0\57\ed\93\95\59\2a\5f\a9\6d\d9\55\80\43\ab\24\c5\48\de\10\70\ca\09\56\bf\7a\28\59\45\76\20\fd\98\f4\53\3a\a1\65\f5\6e\38\5a\2a\37\39\2d\76\39\1f\87\0b\92\6d\05\49\69\dc\10\20\ef\cc\f3\ba\fd\85\ab\eb\97\f9\d5\1e\1a\b8\1a\4d\6f\fd\a2\b8\65\96\de\5b\2a\25\01\b1\b4\1e\97\d9\83\32\86\8c\8b\36\52\3d\2c\00\e1\b1\8f\47\ad\df\f5\ff\dd\b3\b4\1a\46\34\96\c1\8b\e7\86\75\5f\14\4f\c8\00\b1\18\ce\7f\42\f7\cc\a0\59\a2\13\5a\1e\3f\4c\29\98\7b\61\80\66\02\8c\58\9e\9f\e0\a0\0d\b6\1e\be\54\80\fd\c5\6d\72\05\e0\54\84\73\47\fc\24\42\c7\5e\d8\bd\51\67\1b\3e\66\08\f0\66\0c\15\4c\24\fc\24\6d\6f\ff\3b\74\52\0a\4e\75\99\35\17\d2\66\24\65\b6\9f\11\76\0f\1e\09\ab\d4\40\5c\e1\11\1e\e7\bf\e1\78\c1\9d\d2\ef\bd\a2\03\f0\c9\a0\e6\18\53\b7\0c\a3\76\9a\2b\e7\0d\9d\fb\6a\0b\77\8c\28\5d\0c\60\84\eb\85\12\c7\8b\cd\b6\7f\d7\e4\d0\d7\80\32\e1\cb\34\96\0f\83\e7\db\12\07\14\95\18\b7\2c\14\33\93\2b\a2\95\43\77\3e\79\12\76\59\d8\10\14\e8\23\d8\0a\b4\71\14\60\f2\2f\74\e5\33\ed\0f\11\a4\98\4e\56\f5\b5\90\9a\61\d5\52\9e\0c\b2\aa\0d\4d\f2\98\2a\b6\1d\b6\1a\d6\ea\33\0a\c5\24\4e\ef\ba\fd\9e\92\a4\e0\18\24\78\da\07\c0\bb\78\6f\6d\b2\1b\f4\04\07\88\c5\49\40\f0\ed\8c\f3\76\1d\43\56\1d\55\d5\88\85\77\7a\c3\e2\90\34\63\e0\9a\d8\9b\22\a2\ef\fd\fd\cd\af\62\0f\a6\34\e9\ab\fe\b6\4a\89\c8\d6\5f\d9\b7\9f\da\1f\5c\ac\62\6d\47\73\5f\5c\40\ef\80\fe\39\70\15\41\d7\db\e4\99\bf\76\27\98\70\c8\9b\7d\7a\38\71\1b\a8\4e\d8\f9\27\86\bd\8a\25\98\55\74\39\0d\23\6a\fb\f6\18\08\3e\ec\77\a6\fe\01\d4\31\90\5c\fe\fd\3a\8e\10\85\86\48\ea\2d\a0\3b\d0\99\52\a1\1a\29\d3\76\b7\d6\84\7b\87\ea\bc\9b\3b\3f\8d\65\08\d8\6d\86\68\10\74\f3\4b\73\c1\05\27\36\7a\7c\c9\3a\83\fc\f7\0b\98\1d\fe\7a\e5\19\ee\08\3a\3b\6e\5e\1f\2e\63\36\37\18\0f\19\99\55\06\51\b8\6f\ab\7c\18\e2\39\03\3f\8b\23\c3\6d\13\1b\cd\9a\10\44\7c\07\52\64\6a\8a\23\58\63\c7\8f\4f\72\e6\d7\91\58\a0\be\6a\31\e7\67\e8\ac\5c\47\1d\fc\b4\cc\e0\50\61\2c\16\66\16\66\c5\07\5a\38\f8\32\bb\88\95\76\fa\25\5c\d1\a0\de\70\c7\9a\73\c1\47\8f\36\ec\3f\a2\28\14\96\d9\a3\6d\a0\7b\29\9b\4f\d2\63\b2\c2\ea\f3\93\b5\0d\03\c3\30\6a\36\ec\70\4e\70\8d\93\2e\2b\39\23\37\f2\e4\c9\3c\ad\8f\99\f6\fe\88\5f\e5\40\2b\78\19\c1\21\58\79\09\75\d4\06\1e\3d\f7\26\14\cd\67\5e\c6\da\80\d6\b4\e8\66\ff\b1\1a\b3\06\b7\01\22\f6\e8\ce\7d\b2\fc\25\a4\82\0d\96\25\92\9f\60\8b\c4\19\eb\d7\7d\83\c5\ef\48\55\cb\f9\ac\64\31\ce\0f\a2\99\da\18\39\54\16\7b\3e\b6\92\3f\09\90\e1\46\ca\82\19\84\71\89\2a\ce\73\54\cc\bc\c5\d0\d6\b7\5b\a2\6a\04\25\59\05\4d\fa\d1\08\ea\93\1e\4a\88\2a\b7\ad\8e\c9\92\a9\20\0d\95\e5\21\25\ae\51\f9\6f\63\54\a7\da\45\e6\72\4f\ba\af\98\8b\4f\ff\80\43\04\e6\3f\5e\c2\4d\99\15\88\87\a7\ca\e1\42\eb\1a\a1\0b\7b\3a\5a\5b\91\fc\93\50\12\93\a3\a1\47\7c\d7\19\4b\0e\90\a5\3f\de\be\50\12\99\51\95\8d\9d\98\6c\48\32\24\86\b5\d9\e9\5f\50\b0\2d\8c\84\6d\a4\4e\ee\3c\d2\a5\9e\98\40\5e\8c\8c\b2\7b\ac\45\db\78\17\10\a9\a0\ae\50\88\4f\d8\f5\df\69\eb\a3\8b\f2\ba\a9\55\9e\73\21\b3\61\14\ac\5b\69\b0\68\12\65\94\99\0a\24\fc\9d\0a\9f\fc\aa\22\fb\5b\c8\ec\8f\4d\da\75\7e\f3\d1\4f\47\76\d9\74\e2\e6\8d\00\9c\dd\9e\38\46\1b\24\d1\dd\61\c3\ea\81\84\2a\2a\88\a1\f1\2e\a3\88\25\89\bb\97\5f\d4\24\e7\66\e8\1b\ec\ea\5d\96\4a\f5\4f\7c\43\6a\5a\e6\4d\a8\59\27\7b\5a\86\4d\ad\54\f3\ab\6a\f1\56\7d\c8\7b\4f\df\50\c9\73\3b\2c\85\53\8c\39\c5\6c\75\54\b6\78\71\79\a4\4a\fd\19\fb\b8\1d\de\c9\6e\41\c7\1d\4d\4e\11\7a\43\88\a2\ef\42\d6\28\ba\b7\82\a7\e0\61\88\d7\9d\03\4f\da\df\be\9f\37\5c\7f\fa\b3\1f\b4\ff\2e\0b\11\2f\f5\5b\3a\07\4d\6d\4d\3b\5a\20\d9\a6\34\26\4b\dd\e4\53\3a\e6\d6\e3\86\dd\31\5d\8a\5f\ef\c5\8d\be\48\f8\c3\07\57\33\31\b3\f8\50\3c\d2\bd\63\90\df\63\e4\5e\b1\52\ed\2d\75\d0\d5\b7\cb\a6\ce\bb\d3\46\3d\32\1b\0d\3f\87\89\0f\fa\c3\f0\34\04\d2\ca\8b\b0\fa\ee\a8\c6\7d\d5\18\a3\55\55\06\3b\7f\ce\a2\8b\f5\eb\ed\ba\02\53\f6\f0\2c\ca\75\55\07\d6\9b\9b\c6\97\2b\32\53\58\86\9d\b8\31\32\a9\b0\ac\bd\f6\92\43\b1\d1\2c\b0\7d\5e\d1\28\cd\9a\34\4b\fe\f9\c4\d6\d8\8c\95\4f\5a\a0\45\c0\2d\ce\ee\6f\c1\6f\df\da\51\78\ff\33\10\fb\0b\fc\ca\a9\10\39\f9\ff\3b\a0\98\0a\46\ef\68\94\f4\d2\69\4f\b1\8b\17\fe\90\d1\eb\b5\74\98\29\6d\33\b8\e6\a5\88\d7\97\92\90\bd\67\ac\65\ca\1f\d0\10\0b\23\73\b7\7e\b6\d5\a4\7b\96\58\0e\31\ae\1c\29\87\47\6b\c1\5b\f5\3b\96\dc\97\9f\0c\86\37\d7\fc\f4\52\60\0b\27\64\1b\6c\85\90\85\7e\63\8b\85\fa\04\97\a4\33\ee\c2\0f\28\4f\7a\fe\4c\d9\a3\8e\cb\bd\d2\d7\b7\cc\4d\58\de\49\25\5d\69\49\02\15\59\6b\04\eb\42\d4\f8\24\e6\17\8e\4b\d1\66\96\19\a2\9f\a2\2f\a5\42\a1\5d\96\d1\ed\68\61\25\38\68\fe\50\60\ac\24\09\21\db\7c\cc\06\36\bc\16\5b\f0\e2\e5\c0\12\19\6c\2a\87\6d\9d\1e\a6\82\86\5c\c8\9d\64\67\5f\2b\d3\01\c8\60\b7\94\17\4c\90\3f\2e\ec\b2\61\39\dc\ef\9c\3f\49\2a\fc\9d\e7\b8\17\e4\8e\58\8a\1d\cc\6b\fe\6a\ea\0a\a5\cb\40\b4\57\65\02\67\76\a1\8e\13\fd\3a\ee\d0\af\53\57\66\1f\60\51\36\be\af\35\a2\2e\e2\73\10\46\16\12\92\b6\4c\3c\c2\b8\54\62\de\8b\82\0b\bc\08\82\18\d4\05\ce\14\6f\58\74\1f\dd\7a\5b\e7\93\d5\4e\f3\5c\5e\56\b9\a2\c1\56\65\45\6f\97\f8\e9\bd\15\18\d0\cf\4d\5c\9b\f4\1e\25\06\3d\61\14\8a\7c\75\53\d3\3a\b4\42\d5\1b\12\25\1c\d7\d1\04\3b\be\c7\1d\1f\85\2e\06\bf\57\96\4b\2d\68\e3\7d\f3\27\e4\70\d0\6c\7a\a5\f8\70\99\59\d0\23\e1\85\13\00\e4\92\9b\80\ad\dd\86\1d\4b\00\fe\11\7e\0f\96\e1\25\ef\92\85\db\95\1d\cc\cb\ea\04\00\23\fd\2f\36\f7\a0\08\d3\88\36\f8\c1\bb\b3\9e\bb\e7\38\5b\10\e6\7a\b7\48\87\0b\b1\67\50\6a\98\43\13\ad\95\b2\9a\fe\6a\3d\dc\6a\2c\e8\7a\d3\13\74\af\3f\41\7b\bc\92\92\ed\cc\23\2a\e8\e3\17\16\84\af\34\72\a3\f4\e7\cc\3d\d2\18\39\89\aa\23\93\09\da\5f\c9\e3\8a\45\2c\1c\14\8c\29\4e\2f\ff\56\04\2e\38\c8\8e\4f\f4\5a\41\92\98\c8\f1\8a\af\bd\b3\2c\be\b7\ca\2f\ea\f0\95\55\55\5a\32\93\48\bf\22\62\65\70\12\ae\0c\fc\67\7d\88\90\d0\e9\39\50\e0\49\f1\5c\bf\5f\48\fc\1d\07\e6\77\df\cd\73\80\c1\90\a2\12\8c\c7\00\56\f7\75\1d\98\9e\9c\95\5c\40\4a\af\e3\af\c7\a9\88\29\3c\14\2c\a8\b8\d2\e7\14\b3\1b\3e\b5\dd\64\35\f2\77\ac\28\6d\33\5f\aa\23\23\03\d0\0f\98\a8\50\21\44\37\2a\14\27\0e\cd\eb\a8\91\18\79\b2\0a\ea\5c\26\15\f7\36\cd\9f\a7\38\93\c1\94\78\c8\b2\a0\a5\04\93\9e\ab\0a\bb\5b\f0\5e\ec\84\8e\7c\d2\d9\74\41\7b\8e\36\72\83\75\a0\b8\7a\98\87\1d\1a\7b\d3\9a\29\90\fa\50\28\de\bf\a7\c6\3d\de\d9\99\b2\2b\37\90\97\ee\85\95\8f\ff\fb\97\ba\86\ae\a0\99\44\1e\06\93\30\d1\bf\61\39\c4\2b\56\0b\0f\54\0e\d1\50\8a\8e\52\40\ce\25\24\a8\ed\9f\f0\99\59\24\e4\fc\2f\21\cf\eb\b3\55\b1\f4\22\5b\2d\41\64\08\b8\ae\d4\fb\a5\12\8c\1c\3d\77\9f\e9\a0\26\13\e7\a3\d4\89\5e\87\f2\e3\c1\1c\2c\51\0e\7d\c3\95\2e\5c\1b\02\f3\0b\92\b3\df\39\17\3d\61\8e\00\8e\d4\31\8d\5a\a5\f3\3b\4a\0b\4d\2a\87\1e\22\78\1a\ad\1a\9f\d8\71\cf\4f\11\16\a0\33\4c\4a\ab\00\d6\17\4b\c0\55\3b\0d\8f\e4\8c\fc\ce\dc\9f\a4\fc\ca\97\53\02\51\ad\83\78\49\86\aa\e2\5d\f8\e4\6d\6b\25\9d\19\ee\98\8f\b5\df\0c\d1\49\c4\76\b4\84\d0\48\e4\43\ac\0b\6a\0d\7f\b4\df\72\f2\3f\91\f2\54\55\ea\95\a6\4f\9b\8c\b8\de\05\36\6f\bf\33\26\8a\d0\e9\8b\c8\23\7b\36\fd\6b\f5\e4\59\e8\95\65\a1\4f\30\91\5a\7a\9f\d7\6e\8a\a5\38\8f\0b\e5\fe\d5\31\e7\1a\e0\8a\96\7f\19\01\e7\ec\19\59\52\8a\75\8c\c7\ef\fe\8c\2c\69\c2\72\cc\f9\c7\36\8e\33\71\1a\1c\69\ef\b1\87\7f\9f\92\14\81\0e\fa\dc\13\15\d0\1f\30\e7\61\c2\7b\49\37\8c\0c\37\79\20\02\37\6c\44\bf\ce\bb\03\c1\84\68\e6\3d\a9\96\a4\ea\94\4f\4f\4a\f6\50\7a\3d\3c\1a\8e\73\7e\5d\3e\5c\4b\7f\3f\f7\b9\b8\ad\58\9f\9b\e0\9b\56\50\72\36\7b\96\eb\76\e3\ea\de\11\60\8a\26\1b\9f\17\ea\aa\df\b3\60\80\a2\df\28\0e\bc\fe\46\2c\21\2d\ab\a8\34\bb\49\46\7b\09\c0\36\d4\3c\66\b1\c2\48\85\dd\21\bb\05\72\05\a0\9f\2c\49\c2\aa\1e\8b\45\eb\b2\53\f3\39\bd\92\5e\06\1e\b7\21\4c\f0\a3\84\50\9b\ee\3e\4d\f2\02\bd\4e\9e\59\88\77\e5\cb\c7\3a\0b\2a\59\78\cf\b3\59\5b\7d\6d\bd\c8\8c\73\73\49\1c\5a\ee\11\66\78\6f\0b\41\7f\44\5d\83\74\3b\5c\ec\17\fc\30\69\29\0d\ce\cc\0a\81\37\c9\6f\ac\50\eb\4f\85\3f\19\df\19\86\1b\45\8d\9e\75\c9\9e\c3\ab\f0\6a\e4\be\56\a1\21\63\71\7f\fc\67\91\0f\db\2b\74\1b\20\e1\ff\60\5f\6b\47\59\25\73\2d\cc\72\cf\ed\a8\de\64\c3\fd\42\0b\f3\2f\a5\d1\be\72\6e\2d\b1\c6\ef\48\20\05\ec\db\c4\87\22\d6\a2\92\dd\90\cd\fc\d9\4a\47\00\32\11\58\27\1a\74\e0\92\77\89\6e\32\34\fe\93\a3\08\b7\8c\23\be\7a\a6\fa\9e\c4\f2\b5\53\bb\0e\92\2a\69\a8\cf\93\fe\12\e1\77\da\5f\f2\9f\d2\a1\7e\4b\44\02\91\11\a7\90\55\b5\1c\b7\d6\04\56\f3\e3\ea\96\e4\21\d4\8c\c6\76\cd\06\ee\c0\fa\08\e9\89\6b\04\9c\47\4a\60\db\67\27\09\90\e3\3e\cc\15\cf\85\7d\e6\8e\54\18\db\97\5f\85\4a\34\dc\a3\f0\5c\0e\e2\4a\7f\84\72\be\fd\c3\d7\bf\64\2a\cf\b3\50\77\8d\eb\5d\6d\97\61\22\a4\e8\9d\c2\67\1c\b1\45\21\69\bc\ff\0d\f2\0b\2b\34\eb\b2\cb\20\1b\73\9b\a4\00\36\43\d0\79\52\f3\68\de\d8\79\fd\0b\bb\de\90\4e\2b\ea\a4\5f\18\dd\f8\52\7b\f7\d8\9b\08\13\7e\9c\e2\cf\83\bf\cc\8c\f0\88\b0\cb\0d\cd\6c\9a\38\29\55\57\bf\74\17\93\4e\b2\8f\52\37\68\71\79\f2\b9\6a\48\57\6a\82\bf\58\16\cc\36\02\12\ec\0e\1d\25\b9\95\ea\91\cb\e5\64\97\8d\9e\4d\57\0d\8c\5a\10\77\eb\34\23\72\c7\83\a4\34\ee\09\0a\f4\b4\75\5f\72\64\74\09\cf\22\f4\a4\8d\73\03\ca\de\3e\d1\aa\ab\92\99\95\7d\61\6a\56\09\b5\01\85\c8\d9\58\b0\ca\23\7c\b8\a1\15\4c\80\f3\15\ad\c8\0c\01\f8\a7\77\a4\05\b1\69\91\4f\b4\e6\2c\d2\b9\c3\e8\b5\31\39\6a\d5\e8\f9\0a\8a\e7\90\81\7c\4f\ae\52\ba\27\c0\60\f8\06\ca\b2\b2\de\ab\3d\e4\09\bf\4b\13\c6\b4\ec\09\7a\50\45\f9\ba\95\75\05\c0\f7\4a\43\b5\fe\c9\98\ea\45\4c\e6\52\59\c8\4d\4c\04\17\88\5f\5c\b9\4c\e5\98\40\b5\02\f9\68\31\e6\39\99\26\fd\57\76\31\53\67\41\02\5a\81\07\65\a4\08\93\a2\9a\56\0e\e5\3f\8e\ae\60\ee\d5\1d\33\55\09\e4\dd\eb\84\ea\11\8c\d3\0a\d2\0b\71\cf\aa\6f\ea\2a\84\9a\47\76\3a\5f\6c\1b\11\92\aa\49\68\ad\b4\d7\47\f3\c2\96\e7\fa\22\74\d6\ed\c7\6f\1f\c7\cc\3b\e5\24\f2\43\5e\14\ae\3c\7c\40\73\93\1c\5c\7d\6c\60\1b\ea\fa\2f\d1\83\c1\07\b1\01\be\56\cf\1f\7d\ab\41\e2\71\88\bc\b1\93\6e\dd\3e\71\07\be\66\83\81\81\16\fa\a8\a5\73\23\da\91\20\c6\4f\af\ca\b8\c8\12\7b\ff\53\74\a6\e4\6f\c5\4e\50\45\d0\59\26\14\73\a8\82\30\d0\e5\df\4f\83\2b\9a\bb\a7\ac\08\cd\4a\f2\6a\9d\6c\e5\34\74\2e\f9\e5\fb\36\46\8e\f6\10\44\9f\8c\7c\78\4b\f8\52\91\6c\8e\6a\28\f1\44\b6\10\4c\fc\64\f1\39\6c\42\05\38\bf\5b\5f\27\3e\6e\7a\92\9a\cd\8e\26\46\34\53\db\fe\de\0f\b7\58\97\16\23\aa\b2\2a\36\0a\cd\5c\11\31\a9\1d\d0\32\32\fa\60\cc\8a\69\de\57\dd\9e\08\6c\f0\8e\bb\83\4c\54\ea\12\2c\7e\2a\10\f3\4a\db\b6\ab\50\e4\15\91\10\f4\7d\ac\0b\26\d9\12\10\fb\96\68\02\10\87\29\60\60\bb\63\7b\66\1b\8b\ab\fb\db\9f\f7\d7\cc\0c\85\eb\03\df\11\28\08\7b\06\67\b7\30\88\7e\94\69\26\04\b4\aa\05\a9\89\de\3a\6f\68\4c\a2\6d\c1\d3\19\62\4c\5d\2e\6a\6d\60\41\44\42\e4\b6\69\5e\d6\54\68\3f\63\76\c7\09\a0\e6\c6\92\a3\58\63\d8\ba\22\9a\e7\44\6c\67\a2\88\77\06\ec\b5\b3\47\ab\da\4c\9a\a6\7f\1d\8a\7c\c4\71\59\2e\70\d0\50\a0\b1\75\ba\d6\56\6f\62\c3\82\aa\d7\2c\ef\cb\cb\02\81\e8\83\0e\a7\25\6d\56\61\d8\4d\d2\58\9f\ed\ce\e3\95\5d\81\db\b3\af\32\2a\a7\0d\ca\1b\04\9c\a6\6c\8b\05\cc\db\70\0a\26\6d\c0\b2\57\2a\9e\54\61\cf\06\e9\a8\70\33\57\7c\75\f7\51\0d\3a\1b\f8\17\3a\57\e0\c8\4b\5f\98\f4\15\78\1f\a2\cb\44\e5\a1\19\76\1e\2c\dd\76\9c\5f\ef\e8\9e\f7\c8\4a\bc\dd\75\1d\10\65\85\17\d5\8b\c0\88\0f\ea\5e\61\89\d5\90\01\83\8c\7e\ec\be\03\03\d0\7f\49\0a\4d\e2\f3\c3\ba\b4\e6\92\fa\c6\27\09\22\1e\d3\e3\f6\39\11\62\1a\96\9a\18\a1\75\72\f0\72\6d\cd\d3\34\f8\03\e8\1c\43\3d\16\c3\bb\0c\81\b6\e6\7e\6d\89\c2\22\c5\39\68\db\99\ea\1b\58\7a\e9\08\d3\c8\67\51\6b\49\34\37\aa\7c\d8\71\b4\8d\3b\ac\8e\6c\84\07\e4\c2\16\c9\98\80\39\03\d4\2d\fe\8f\2d\af\2d\10\c7\52\de\bc\0b\2b\75\2e\31\c3\d0\9f\75\b3\c2\ab\d3\b9\c5\fe\62\3e\fd\13\2e\b4\2f\96\09\d8\8d\3e\fc\e5\b1\bd\8e\ab\f7\7e\94\65\6e\01\fa\cf\95\f9\76\d7\5f\35\32\ee\82\19\3a\e8\3a\0f\db\a9\c9\f6\f3\34\b9\e9\2b\0a\8f\bd\a5\e0\d2\69\f7\0b\d0\1d\2d\67\56\d2\cf\9f\0b\97\a1\4f\13\40\70\44\50\fa\5f\77\d1\73\56\5c\7e\ac\17\e3\6f\20\97\54\c4\e1\62\fa\41\e3\82\f6\77\24\23\11\9c\84\0c\6a\82\a6\4e\d4\5e\5f\69\82\d1\1e\30\87\fd\ac\85\b7\d5\40\44\bb\ae\b2\71\f2\98\23\7a\1b\49\86\2b\64\86\b8\07\26\41\2c\f4\38\cd\cb\23\71\5a\cb\c9\78\83\a1\ef\55\2b\29\67\1e\9b\69\52\94\ad\72\21\e8\67\56\32\ff\b1\f3\29\61\2c\1d\55\e8\75\9c\0b\91\0b\9d\04\c2\42\60\a3\2a\ba\67\fd\2d\c4\8e\e3\33\cc\fa\df\25\08\e1\06\bb\f9\45\38\d1\2a\ab\99\56\db\73\07\81\ba\51\66\12\af\af\68\eb\a8\8c\e9\af\2a\45\14\07\23\b8\b0\c3\e4\1e\a9\8f\02\5f\98\13\c6\19\68\b7\a6\0d\b0\41\da\2f\65\88\7a\81\b8\a1\2d\5a\90\4b\b4\e9\86\41\70\62\df\f3\f4\49\e1\12\0b\d9\bc\fe\c6\7f\ee\53\70\a1\b3\e7\bb\5b\99\11\84\a3\1b\12\d2\bd\96\e2\f3\b2\a4\a0\2e\c0\ce\8d\a3\ea\36\39\ff\55\e8\7b\7a\3a\16\3c\73\7b\ee\c7\74\90\68\00\80\6f\80\c4\fb\7d\42\1e\ca\90\ff\fc\03\cc\73\bb\d5\f9\e8\49\1a\9f\05\d0\59\75\c3\83\56\ff\65\4e\e2\b3\6e\8f\ab\39\ef\41\eb\c3\b2\81\d1\26\ed\74\01\0b\a7\9a\07\34\bc\aa\54\1f\7f\9d\a8\f4\7a\21\0e\43\fd\c0\b7\da\18\58\7d\2d\7b\9e\74\1d\69\18\20\93\bc\9b\18\c0\e1\7a\3b\9a\fd\0f\5a\8e\a3\82\d2\e9\b4\df\40\ca\3d\68\0a\7d\ed\46\7c\fc\b9\3d\8c\ac\c5\2e\ac\ba\38\98\14\96\88\3d\52\a7\4a\1d\f4\7c\9a\04\2b\c7\61\15\69\6c\a6\67\38\09\46\1d\22\78\21\44\50\3e\ee\f4\59\85\f7\24\6c\01\ca\8b\c1\7d\c2\65\9a\d6\e0\b1\df\b9\3c\0e\43\04\4c\e6\69\f5\fa\12\98\d1\b1\b7\c9\92\ab\c7\13\11\3c\f4\48\68\d8\34\a2\2f\64\0b\89\a9\0e\97\38\16\7f\89\94\bc\ee\93\96\a8\b3\e4\0f\26\44\93\1e\99\e8\f7\5f\50\dd\da\13\24\ae\6b\53\19\16\83\80\7d\ad\1f\8a\80\40\16\eb\de\b3\4c\a0\25\4f\a3\bb\6e\7f\83\36\e5\04\cf\bc\05\b5\13\0b\2b\c4\e2\fc\06\c4\75\4b\a8\76\04\46\9b\c7\aa\bc\ee\c5\79\05\82\64\68\4d\5e\97\b3\25\f5\d3\8a\86\40\8c\b5\a3\79\3c\da\e2\a9\65\dc\4c\dc\34\6d\7a\de\26\a2\a1\e0\02\9d\dd\c2\d1\77\05\09\bf\51\fd\83\5c\40\06\a2\65\05\53\5e\a1\33\c4\24\73\63\92\00\cd\4c\7a\ae\de\b4\e6\22\22\18\38\88\27\74\23\c4\9b\29\80\ab\be\3f\11\97\6e\9a\53\b4\3d\16\a8\83\45\ea\7e\b4\de\39\3d\42\e2\69\af\be\24\4d\61\87\f2\a4\93\62\cf\c8\eb\15\25\aa\0b\bf\5f\9c\c2\65\77\b1\2d\62\63\81\1a\ab\c2\ea\2a\35\4a\21\0a\ef\aa\cd\09\95\b3\dc\13\f4\51\26\cc\28\0c\d4\1b\77\36\cc\13\72\5e\5b\ec\8c\b7\30\5c\6b\cc\89\73\e4\0f\41\cf\78\1e\75\ad\f1\8c\5c\23\26\00\ae\c8\55\3c\b9\f0\18\29\8e\d7\30\be\86\28\bb\52\6e\2e\e0\09\8c\93\b2\8e\6c\0f\a6\fd\51\d7\20\d4\16\a0\c4\c1\62\01\51\6d\e4\2f\41\33\3b\d0\9a\51\7e\4b\bb\30\b5\a9\5a\af\75\1f\bc\3f\f9\fb\f1\4c\43\21\27\89\24\20\d3\a3\1c\44\21\98\74\e3\79\3d\d9\29\69\68\17\d9\0a\45\6a\a1\f4\e1\87\78\fe\76\10\a2\ea\7e\23\50\35\01\12\74\61\59\2d\ab\da\83\e0\ca\73\c8\3a\12\8f\a0\7b\3c\eb\1e\5d\2d\6c\1a\28\5c\9d\49\dd\8e\d1\d6\75\a8\49\11\92\44\d5\87\54\64\c9\0c\2a\a3\6d\2c\ce\51\d4\b5\99\3e\af\be\ac\a7\f2\21\38\75\46\a0\ef\9f\97\dd\be\54\ea\bb\21\9e\21\40\f6\1a\5f\6d\3a\a5\6f\fe\fb\2d\81\62\36\20\bc\b7\92\16\5d\ca\5a\b8\ec\df\e8\78\16\d3\54\f6\d6\d0\d2\8d\48\bb\fb\57\2b\50\d1\9d\a2\31\fb\cb\56\97\d6\53\44\86\7d\f5\8d\95\1b\ee\fd\c1\1e\7d\1f\68\bb\9d\21\f4\c7\2e\8d\a8\97\b3\71\54\f8\4e\08\b7\86\57\78\c7\6e\0d\69\e6\3b\84\e2\ad\c3\fc\2f\76\8e\d1\4f\fb\cd\60\a8\42\fe\91\49\fc\f2\82\28\b7\96\25\70\23\69\7b\17\da\ba\23\ad\b9\be\9a\b4\e9\60\bc\a7\36\41\9b\cd\6f\1b\67\fb\ab\76\f6\86\80\f7\db\fe\84\8f\eb\d4\6c\9b\06\c3\86\d2\b5\43\0a\cf\e6\a8\97\3d\e0\31\20\d7\88\68\2a\30\c3\48\4d\d2\8b\b7\43\9c\f7\21\c5\e3\af\7e\ef\76\b2\f7\bf\ed\a8\7b\69\34\4e\e5\43\df\e4\3e\87\fa\10\5d\c5\c9\14\ab\b1\d2\1b\83\2f\3e\ff\51\a4\94\4f\c2\bd\0f\d0\fa\6a\ba\de\a4\fd\4f\b7\2e\ed\fe\8c\dc\bb\90\fd\53\85\e2\3e\d5\78\63\78\57\9f\f4\50\c8\65\cb\2b\50\3d\4d\52\8b\ba\c5\d5\02\bf\ff\af\f6\90\6c\03\d7\11\68\19\68\3f\30\b6\08\bb\f5\cc\ae\50\0f\46\1f\83\d4\22\03\67\e1\60\57\93\f8\a8\d5\9d\c4\7d\3a\8f\5e\b0\f0\e4\e4\a4\e2\ba\d5\ce\59\7a\39\10\be\da\64\e1\9f\c5\d7\9f\c4\3e\39\82\8f\36\41\fc\04\aa\d1\03\e2\80\88\ed\08\3c\62\17\29\1a\cd\93\02\8f\a2\54\8f\4c\2f\93\d4\57\73\6c\2c\fd\d5\12\a5\e8\4c\39\02\0d\35\e5\20\fb\9a\c4\8a\9d\54\95\64\0e\f9\3c\a2\f6\c2\28\42\01\a5\a2\d3\5a\2f\e7\eb\be\39\5e\eb\d9\6d\00\8d\60\07\4d\5e\cc\97\cc\02\44\33\ea\9a\6b\7f\af\e7\71\49\3f\6c\09\30\c5\e5\02\80\c7\af\a9\cb\ae\00\e0\93\cc\b5\e6\d9\d4\c2\f7\cf\91\4c\56\bd\bd\9d\cb\54\83\05\db\1a\60\92\dc\b4\df\67\6f\91\53\12\25\16\0d\1b\29\a9\97\96\c2\7b\8b\20\3e\62\3f\05\58\81\44\39\51\72\20\f6\b6\86\ab\76\08\71\73\b0\fd\17\12\d7\90\8f\66\77\33\ad\df\f1\a9\84\38\3f\dc\7d\7d\a2\9f\84\07\ab\b9\e0\75\88\74\6f\7e\08\af\2f\34\8b\ec\9d\51\1d\28\14\9f\c5\cd\22\a8\51\5e\2b\79\1c\da\0c\51\f3\15\69\d8\e7\d8\ab\0d\a2\3e\90\24\47\a0\de\10\25\5a\a4\c0\fc\6a\b8\34\80\e1\f1\35\f1\cc\99\09\2e\21\0e\f5\77\60\b3\3d\56\8a\93\da\b3\79\e8\02\fc\60\4e\db\56\01\ce\50\f8\1c\7c\76\42\1d\40\cf\5a\20\25\62\88\7d\69\87\ec\fb\a9\cc\94\41\e6\18\dc\87\5c\00\2f\46\97\be\4b\de\24\31\1e\5a\75\ba\ed\3b\80\f4\b3\35\57\7a\2a\56\87\aa\3a\69\4a\6f\a7\38\18\3e\fa\6b\d1\a3\1d\53\8d\28\6e\34\30\15\bb\8a\1f\35\ef\7f\a3\f8\82\08\f2\b5\d2\bc\74\d7\b5\94\29\d9\a7\a9\70\cf\71\04\40\e2\2e\b5\f4\af\75\75\a4\f6\0f\1a\b0\0c\ca\fc\40\15\9c\9e\87\26\6c\83\b0\60\f1\41\43\ab\46\34\0a\6d\f6\5b\78\27\19\02\b8\5c\40\94\fe\ad\65\cc\ba\0e\e5\10\6d\55\5d\4e\8f\85\43\fe\51\d2\d5\83\08\61\9a\0e\a6\92\70\58\d6\09\c7\bd\9e\71\a7\c7\8c\36\c9\08\92\ca\3a\1a\99\2d\02\4c\a4\64\21\ba\b1\3c\59\d0\62\18\ae\f6\97\7d\c7\7e\d0\4f\b2\74\f4\51\50\b1\67\f3\fc\45\c0\88\97\b1\13\b2\58\0b\04\e2\b9\27\70\47\f3\a7\46\4b\e9\90\50\1c\b2\c1\2c\2c\54\6e\b2\70\50\df\bc\88\fe\c0\b9\58\b5\1d\2e\82\61\a5\9c\0b\e0\4e\f2\30\c7\ba\05\ae\89\b6\99\b5\6e\b4\06\09\bc\fd\c9\fe\00\22\39\04\92\52\11\60\c5\27\1e\e8\75\a8\55\67\56\8d\1b\d4\40\44\7d\4e\bf\c2\d7\0f\e5\16\7a\6e\37\93\ec\89\bd\dd\2f\8f\7f\31\f4\34\ed\ca\92\95\59\f1\f3\4e\ee\57\48\1b\55\49\61\da\71\01\50\ea\09\16\f2\96\51\3b\24\ae\5b\f2\2e\46\ba\cd\58\19\54\cf\93\bf\c7\47\c6\4b\ed\a8\59\08\c9\71\55\7e\af\84\7c\15\a9\54\01\07\ad\ba\ca\2d\80\aa\b5\bc\7a\b2\8a\04\d9\29\b7\e7\1e\ce\a7\06\2c\d9\6f\b5\2b\4a\10\19\47\f2\45\b9\a5\08\89\b3\1d\f2\ff\aa\6e\24\6d\51\95\e9\d6\13\79\bf\bf\5f\24\0f\08\8a\1e\00\ac\20\81\00\1a\20\92\01\1e\20\26\20\20\20\21\20\c6\02\30\20\60\01\39\20\52\01\8d\00\7d\01\8f\00\90\00\18\20\19\20\1c\20\1d\20\22\20\13\20\14\20\dc\02\22\21\61\01\3a\20\53\01\9d\00\7e\01\78\01\aa\aa\66\66\24\49\e3\38\8b\2e\62\27\c0\80\00\00\e0\80\80\00\f0\80\80\80")
(func (export "input_ptr") (result i32) (i32.const 412544))
(func (export "input_utf8_cap") (result i32) (i32.const 1048576))
(func (export "output_utf8_cap") (result i32) (i32.const 1258298))
;; A code point as UTF-8 bytes in a word, low byte first; their count goes to $ulen.
;; The bits are laid out as four bytes, then the unused leading ones are shifted out.
(func $utf8 (param $v i32) (result i32)
(local $n i32)
(global.set $ulen (local.tee $n
(i32.add (i32.add (i32.add (i32.const 1)
(i32.gt_u (local.get $v) (i32.const 0x7F)))
(i32.gt_u (local.get $v) (i32.const 0x7FF)))
(i32.gt_u (local.get $v) (i32.const 0xFFFF)))))
(if (result i32) (i32.eq (local.get $n) (i32.const 1))
(then (local.get $v))
(else (i32.or
(i32.shr_u
(i32.or (i32.or (i32.or
(i32.shr_u (local.get $v) (i32.const 18))
(i32.shl (i32.and (i32.shr_u (local.get $v) (i32.const 12)) (i32.const 0x3F)) (i32.const 8)))
(i32.shl (i32.and (i32.shr_u (local.get $v) (i32.const 6)) (i32.const 0x3F)) (i32.const 16)))
(i32.shl (i32.and (local.get $v) (i32.const 0x3F)) (i32.const 24)))
(i32.sub (i32.const 32) (i32.shl (local.get $n) (i32.const 3))))
(i32.load offset=6564 (i32.shl (local.get $n) (i32.const 2)))))))
;; Unpacks one symbol of $nbits bits with an adaptive binary range coder (the LZMA one).
;; ctx holds $nctx4 / 4 contexts; each bit is predicted by mixing their model entries.
;; An entry is 16 bits: a 12-bit probability (stored XOR 0x800, so zeroed memory means
;; one half) above a 4-bit count of how often it was used.
(func $sym (param $nctx4 i32) (param $nbits i32) (result i32)
(local $node i32) (local $top i32) (local $k i32) (local $s i32) (local $e i32)
(local $r i32) (local $num i32) (local $den i32) (local $bit i32)
(local.set $node (i32.const 1))
(local.set $top (i32.shl (i32.const 1) (local.get $nbits)))
(loop $bits
(local.set $num (i32.const 0))
(local.set $den (i32.const 0))
(local.set $k (i32.const 0))
(loop $vote
(i32.store offset=6608 (local.get $k) (local.tee $s
(i32.and (i32.shr_u
(i32.mul
(i32.add (i32.load offset=6592 (local.get $k)) (i32.mul (local.get $node) (i32.const 0x2545F491)))
(i32.const 0x9E3779B1))
(i32.const 14)) (i32.const -2))))
(local.set $r (i32.shr_u (local.tee $e (i32.load16_u offset=150400 (local.get $s))) (i32.const 4)))
;; Weigh a context by its order, how often it was seen and how sure it is.
(local.set $e (i32.shl
(i32.mul
(i32.and (local.get $e) (i32.const 15))
(i32.add (i32.const 1) (i32.shr_u
(select (local.get $r) (i32.sub (i32.const 0x1000) (local.get $r))
(i32.lt_u (local.get $r) (i32.const 0x800)))
(i32.const 8))))
(i32.shr_u (local.get $k) (i32.const 2))))
(local.set $num (i32.add (local.get $num)
(i32.mul (i32.xor (local.get $r) (i32.const 0x800)) (local.get $e))))
(local.set $den (i32.add (local.get $den) (local.get $e)))
(br_if $vote (i32.lt_u (local.tee $k (i32.add (local.get $k) (i32.const 4))) (local.get $nctx4))))
;; $r: 12-bit probability that the bit is 1, never 0.
(local.set $r (if (result i32) (local.get $den)
(then (i32.div_u (local.get $num) (local.get $den)))
(else (i32.const 0x800))))
(local.set $r (i32.mul (i32.shr_u (global.get $range) (i32.const 12))
(select (local.get $r) (i32.const 1) (local.get $r))))
(if (local.tee $bit (i32.lt_u (global.get $code) (local.get $r)))
(then (global.set $range (local.get $r)))
(else
(global.set $code (i32.sub (global.get $code) (local.get $r)))
(global.set $range (i32.sub (global.get $range) (local.get $r)))))
(loop $norm
(if (i32.lt_u (global.get $range) (i32.const 0x1000000)) (then
(global.set $range (i32.shl (global.get $range) (i32.const 8)))
(global.set $code (i32.or (i32.shl (global.get $code) (i32.const 8)) (i32.load8_u (global.get $in))))
(global.set $in (i32.add (global.get $in) (i32.const 1)))
(br $norm))))
;; Move each entry toward the bit just seen, by 2 / (2n + 3) after n uses.
(local.set $k (i32.const 0))
(loop $learn
(local.set $r (i32.xor (i32.shr_u
(local.tee $e (i32.load16_u offset=150400 (local.tee $s (i32.load offset=6608 (local.get $k)))))
(i32.const 4)) (i32.const 0x800)))
(local.set $e (i32.and (local.get $e) (i32.const 15)))
(i32.store16 offset=150400 (local.get $s) (i32.or
(i32.shl
(i32.xor
(i32.add (local.get $r) (i32.shr_s
(i32.mul
(i32.sub (i32.shl (local.get $bit) (i32.const 12)) (local.get $r))
(i32.load16_u offset=6560 (i32.shl (local.get $e) (i32.const 1))))
(i32.const 16)))
(i32.const 0x800))
(i32.const 4))
(i32.add (local.get $e) (i32.lt_u (local.get $e) (i32.const 5)))))
(br_if $learn (i32.lt_u (local.tee $k (i32.add (local.get $k) (i32.const 4))) (local.get $nctx4))))
(br_if $bits (i32.lt_u
(local.tee $node (i32.add (i32.shl (local.get $node) (i32.const 1)) (local.get $bit)))
(local.get $top))))
(i32.sub (local.get $node) (local.get $top)))
;; Unpacks the entity list into the lookup tables. Entities are ordered by code point, so
;; a value is a small step from the one before. A name is coded character by character
;; from four contexts: the previous one, two and three characters, and a match model that
;; predicts the character that followed the last place the previous two were seen.
(func $init
(local $r i32) (local $cp i32) (local $nl i32) (local $hn i32) (local $d i32) (local $vn i32)
(local $legacy i32) (local $n i32) (local $h i32) (local $mp i32) (local $ml i32) (local $c i32)
(local $ep i32) (local $val i64) (local $key i64)
;; Empty keys in fast and leg, and the 0xFF half of len_mask, in one fill.
(memory.fill (i32.const 79936) (i32.const 255) (i32.const 70432))
(local.set $r (i32.const 2125))
(local.set $hn (i32.const 1))
(loop $entity
;; Step from the previous code point: its bit length, then the bits below the top one.
(i32.store (i32.const 6592) (i32.const 1048576))
(if (i32.gt_u (local.tee $d (call $sym (i32.const 4) (i32.const 5))) (i32.const 1)) (then
(i32.store (i32.const 6592) (i32.add (i32.const 2097152) (local.get $d)))
(local.set $d (i32.add
(i32.shl (i32.const 1) (local.tee $d (i32.sub (local.get $d) (i32.const 1))))
(call $sym (i32.const 4) (local.get $d))))))
(local.set $val (i64.extend_i32_u (call $utf8
(local.tee $cp (i32.add (local.get $cp) (local.get $d))))))
(local.set $vn (global.get $ulen))
;; A few values have a second code point.
(i32.store (i32.const 6592) (i32.add (i32.const 3145728) (i32.eqz (local.get $d))))
(if (call $sym (i32.const 4) (i32.const 1)) (then
(i32.store (i32.const 6592) (i32.const 4194304))
(local.set $val (i64.or (local.get $val) (i64.shl
(i64.extend_i32_u (call $utf8 (call $sym (i32.const 4) (i32.const 16))))
(i64.extend_i32_u (i32.shl (local.get $vn) (i32.const 3))))))
(local.set $vn (i32.add (local.get $vn) (global.get $ulen)))))
(local.set $val (i64.or (local.get $val)
(i64.shl (i64.extend_i32_u (local.get $vn)) (i64.const 56))))
(i32.store (i32.const 6592) (i32.add (i32.const 5242880) (i32.lt_u (local.get $cp) (i32.const 256))))
(local.set $legacy (call $sym (i32.const 4) (i32.const 1)))
;; Names use a 6-bit alphabet: 0 ends the name, then digits, capitals, small letters.
(memory.fill (i32.const 6624) (i32.const 0) (i32.const 40))
(local.set $n (i32.const 0))
(local.set $h (i32.const 0))
(local.set $ml (i32.const 0))
(loop $char
(i32.store (i32.const 6592) (i32.add (i32.const 8388608) (i32.and (local.get $h) (i32.const 63))))
(i32.store (i32.const 6596) (i32.add (i32.const 9437184) (i32.and (local.get $h) (i32.const 4095))))
(i32.store (i32.const 6600) (i32.add (i32.add (i32.const 13631488)
(i32.shl (select (i32.load8_u offset=39504 (local.get $mp)) (i32.const 0) (local.get $ml)) (i32.const 4)))
(select (local.get $ml) (i32.const 3) (i32.lt_u (local.get $ml) (i32.const 3)))))
(i32.store (i32.const 6604) (i32.add (i32.const 10485760) (i32.and (local.get $h) (i32.const 0x3FFFF))))
(local.set $c (call $sym (i32.const 16) (i32.const 6)))
(if (i32.and (i32.ne (local.get $ml) (i32.const 0))
(i32.eq (i32.load8_u offset=39504 (local.get $mp)) (local.get $c)))
(then
(local.set $ml (i32.add (local.get $ml) (i32.const 1)))
(local.set $mp (i32.add (local.get $mp) (i32.const 1))))
(else (local.set $ml (i32.const 0))))
(i32.store8 offset=39504 (local.get $hn) (local.get $c))
(local.set $hn (i32.add (local.get $hn) (i32.const 1)))
(if (local.get $c) (then
(i32.store8 offset=6624 (local.get $n) (i32.add (local.get $c)
(select (i32.const 47) (select (i32.const 54) (i32.const 60) (i32.lt_u (local.get $c) (i32.const 37)))
(i32.lt_u (local.get $c) (i32.const 11)))))
(local.set $h (i32.or (i32.shl (local.get $h) (i32.const 6)) (local.get $c)))
(if (i32.ge_u (local.tee $n (i32.add (local.get $n) (i32.const 1))) (i32.const 2)) (then
(local.set $c (i32.load16_u offset=14864
(local.tee $ep (i32.shl (i32.and (local.get $h) (i32.const 4095)) (i32.const 1)))))
(if (i32.and (i32.eqz (local.get $ml)) (i32.ne (local.get $c) (i32.const 0))) (then
(local.set $mp (local.get $c))
(local.set $ml (i32.const 1))))
(i32.store16 offset=14864 (local.get $ep) (local.get $hn))))
(br $char))))
(if (i32.le_u (local.get $n) (i32.const 7))
(then
;; fast: the key is the name and its `;`.
(local.set $key (i64.or (i64.load (i32.const 6624))
(i64.shl (i64.const 59) (i64.extend_i32_u (i32.shl (local.get $n) (i32.const 3))))))
(local.set $ep (i32.shl (i32.wrap_i64 (i64.shr_u
(i64.mul (local.get $key) (i64.const 0xA59D054B12F5B905)) (i64.const 52))) (i32.const 4)))
(loop $probe
(if (i64.ne (i64.load offset=79936 (local.get $ep)) (i64.const -1)) (then
(local.set $ep (i32.add (local.get $ep) (i32.const 16)))
(br $probe))))
(i64.store offset=79936 (local.get $ep) (local.get $key))
(i64.store offset=79944 (local.get $ep) (local.get $val))
(if (local.get $legacy) (then
;; leg: the length is part of the key, so a NUL after a shorter name cannot match.
(local.set $key (i64.or (i64.load (i32.const 6624))
(i64.shl (i64.extend_i32_u (local.get $n)) (i64.const 56))))
(local.set $ep (i32.shl (i32.wrap_i64 (i64.shr_u
(i64.mul (local.get $key) (i64.const 0xF880D97D1854E551)) (i64.const 56))) (i32.const 4)))
(loop $probe
(if (i64.ne (i64.load offset=145984 (local.get $ep)) (i64.const -1)) (then
(local.set $ep (i32.add (local.get $ep) (i32.const 16)))
(br $probe))))
(i64.store offset=145984 (local.get $ep) (local.get $key))
(i64.store offset=145992 (local.get $ep) (local.get $val))
(if (i32.lt_u
(i32.load8_u offset=6672 (local.tee $ep (i32.shr_u
(i32.mul (i32.load16_u (i32.const 6624)) (i32.const 0x9E3779B1)) (i32.const 20))))
(local.get $n))
(then (i32.store8 offset=6672 (local.get $ep) (local.get $n)))))))
(else
;; longs: record number $nl (from 1) is at 55600 + 48 * $nl.
(memory.copy
(i32.add (i32.const 55600) (local.tee $ep
(i32.mul (local.tee $nl (i32.add (local.get $nl) (i32.const 1))) (i32.const 48))))
(i32.const 6624) (i32.const 32))
(i64.store offset=55632 (local.get $ep) (local.get $val))
(i32.store offset=55640 (local.get $ep) (local.get $n))
(local.set $ep (i32.wrap_i64 (i64.shr_u (i64.mul
(i64.add
(i64.xor (i64.load (i32.const 6624))
(i64.rotl (i64.load offset=6616 (local.get $n)) (i64.const 29)))
(i64.extend_i32_u (local.get $n)))
(i64.const 0xA0C2995F40498CB3)) (i64.const 53))))
(loop $probe
(if (i32.load16_u offset=10768 (i32.shl (local.get $ep) (i32.const 1))) (then
(local.set $ep (i32.and (i32.add (local.get $ep) (i32.const 1)) (i32.const 2047)))
(br $probe))))
(i32.store16 offset=10768 (i32.shl (local.get $ep) (i32.const 1)) (local.get $nl))))
(br_if $entity (local.tee $r (i32.sub (local.get $r) (i32.const 1))))))
(func (export "render") (param $n i32) (result i64)
(local $size i32) (local $e i32) (local $o i32) (local $base i32) (local $lim i32)
(local $out i32) (local $q i32) (local $i i32) (local $len i32) (local $j i32)
(local $hex i32) (local $p i32) (local $v i32) (local $k i32) (local $ep i32) (local $h i32)
(local $w i64) (local $mm i64) (local $d i64) (local $t i64) (local $letter i64)
(local $x i64) (local $key i64) (local $val i64) (local $kb i64) (local $high i64)
(local $zero v128) (local $amp v128) (local $s0 v128) (local $s1 v128) (local $m0 v128) (local $m1 v128)
;; len_mask is filled by $init, so its first byte says whether the tables exist yet.
(if (i32.eqz (i32.load8_u (i32.const 150336))) (then (call $init)))
(local.set $size (select (local.get $n) (i32.const 1048576) (i32.lt_u (local.get $n) (i32.const 1048576))))
;; 64 zero bytes after the input: nothing stale can be mistaken for part of a reference.
(v128.store offset=412544 (local.get $size) (local.get $zero))
(v128.store offset=412560 (local.get $size) (local.get $zero))
(v128.store offset=412576 (local.get $size) (local.get $zero))
(v128.store offset=412592 (local.get $size) (local.get $zero))
(local.set $amp (i8x16.splat (i32.const 38)))
(local.set $high (i64.const 0x8080808080808080))
;; $e: end of the last reference in the input. $o: bytes of output so far.
(block $done
(loop $window
(br_if $done (i32.ge_u (local.get $base) (local.get $size)))
(local.set $lim (select (i32.add (local.get $base) (i32.const 4096)) (local.get $size)
(i32.gt_u (i32.sub (local.get $size) (local.get $base)) (i32.const 4096))))
;; Stage 1: list the position of every `&` in the window, 64 bytes at a time. The
;; first four positions of a block are written without looking at how many there
;; are; entries past the count are overwritten or ignored.
(local.set $out (i32.const 0))
(loop $scan
(local.set $mm (i64.or (i64.or (i64.or
(i64.extend_i32_u (i8x16.bitmask (i8x16.eq (v128.load offset=412544 (local.get $base)) (local.get $amp))))
(i64.shl (i64.extend_i32_u (i8x16.bitmask (i8x16.eq (v128.load offset=412560 (local.get $base)) (local.get $amp)))) (i64.const 16)))
(i64.shl (i64.extend_i32_u (i8x16.bitmask (i8x16.eq (v128.load offset=412576 (local.get $base)) (local.get $amp)))) (i64.const 32)))
(i64.shl (i64.extend_i32_u (i8x16.bitmask (i8x16.eq (v128.load offset=412592 (local.get $base)) (local.get $amp)))) (i64.const 48))))
(local.set $k (i32.add (local.get $out) (i32.shl (i32.wrap_i64 (i64.popcnt (local.get $mm))) (i32.const 2))))
(i32.store offset=23056 (local.get $out) (i32.add (local.get $base) (i32.wrap_i64 (i64.ctz (local.get $mm)))))
(local.set $mm (i64.and (local.get $mm) (i64.sub (local.get $mm) (i64.const 1))))
(i32.store offset=23060 (local.get $out) (i32.add (local.get $base) (i32.wrap_i64 (i64.ctz (local.get $mm)))))
(local.set $mm (i64.and (local.get $mm) (i64.sub (local.get $mm) (i64.const 1))))
(i32.store offset=23064 (local.get $out) (i32.add (local.get $base) (i32.wrap_i64 (i64.ctz (local.get $mm)))))
(local.set $mm (i64.and (local.get $mm) (i64.sub (local.get $mm) (i64.const 1))))
(i32.store offset=23068 (local.get $out) (i32.add (local.get $base) (i32.wrap_i64 (i64.ctz (local.get $mm)))))
(if (i64.ne (local.tee $mm (i64.and (local.get $mm) (i64.sub (local.get $mm) (i64.const 1)))) (i64.const 0)) (then
(loop $more
(i32.store offset=23072 (local.get $out) (i32.add (local.get $base) (i32.wrap_i64 (i64.ctz (local.get $mm)))))
(local.set $out (i32.add (local.get $out) (i32.const 4)))
(br_if $more (i64.ne (local.tee $mm (i64.and (local.get $mm) (i64.sub (local.get $mm) (i64.const 1)))) (i64.const 0))))))
(local.set $out (local.get $k))
(br_if $scan (i32.lt_u (local.tee $base (i32.add (local.get $base) (i32.const 64))) (local.get $lim))))
;; Stage 2: for each position, copy the text before it and decode the reference.
(local.set $q (i32.const 0))
(if (local.get $out) (then
(loop $ref
;; $i: position of this `&`; it becomes the end of the reference.
(local.set $len (i32.sub (local.tee $i (i32.load offset=23056 (local.get $q))) (local.get $e)))
(local.set $q (i32.add (local.get $q) (i32.const 4)))
(v128.store offset=150400 (local.get $o) (v128.load offset=412544 (local.get $e)))
(v128.store offset=150416 (local.get $o) (v128.load offset=412560 (local.get $e)))
(if (i32.gt_u (local.get $len) (i32.const 32)) (then
(local.set $j (i32.const 32))
(loop $copy
(v128.store offset=150400 (i32.add (local.get $o) (local.get $j))
(v128.load offset=412544 (i32.add (local.get $e) (local.get $j))))
(br_if $copy (i32.lt_u (local.tee $j (i32.add (local.get $j) (i32.const 16))) (local.get $len))))))
(local.set $o (i32.add (local.get $o) (local.get $len)))
;; $w: the 8 bytes after the `&`.
(local.set $w (i64.load offset=412545 (local.get $i)))
(block $next
(block $bare
(block $legacy
(if (i32.eq (i32.and (i32.wrap_i64 (local.get $w)) (i32.const 255)) (i32.const 35)) (then
;; Numeric. Decimal and hexadecimal share one path: classify 8 bytes,
;; take the digits before the first other byte, and fold them pairwise.
(local.set $hex (i32.eq
(i32.or (i32.and (i32.wrap_i64 (i64.shr_u (local.get $w) (i64.const 8))) (i32.const 255)) (i32.const 32))
(i32.const 120)))
(local.set $t (i64.xor
(local.tee $d (i64.load offset=412544
(local.tee $p (i32.add (i32.add (local.get $i) (i32.const 2)) (local.get $hex)))))
(i64.const 0x3030303030303030)))
;; $x: 0x80 in each byte that is a letter a-f or A-F.
(local.set $x (i64.and
(local.tee $key (i64.or (local.get $d) (i64.const 0x2020202020202020)))
(i64.xor (local.get $high) (i64.const -1))))
(local.set $letter (i64.and
(i64.and
(i64.add (local.get $x) (i64.const 0x1F1F1F1F1F1F1F1F))
(i64.xor
(i64.or (i64.add (local.get $x) (i64.const 0x1919191919191919)) (local.get $key))
(i64.const -1)))
(local.get $high)))
;; $t: 0x80 in each byte that is not a digit of this reference.
(if (i64.ne
(local.tee $t (i64.and
(i64.and
(i64.or
(i64.add (i64.and (local.get $t) (i64.xor (local.get $high) (i64.const -1)))
(i64.const 0x7676767676767676))
(local.get $t))
(local.get $high))
(i64.xor
(select (local.get $letter) (i64.const 0) (local.get $hex))
(i64.const -1))))
(i64.const 0))
(then
;; $kb: bits of digits. None: the `&` stands for itself.
(br_if $bare (i64.eqz (local.tee $kb (i64.and (i64.ctz (local.get $t)) (i64.const 56)))))
(local.set $x (i64.shl
(i64.add (i64.and (local.get $d) (i64.const 0x0F0F0F0F0F0F0F0F))
(i64.mul (i64.shr_u (local.get $letter) (i64.const 7)) (i64.const 9)))
(i64.sub (i64.const 64) (local.get $kb))))
(local.set $x (i64.and
(i64.add (i64.mul (local.get $x) (select (i64.const 16) (i64.const 10) (local.get $hex)))
(i64.shr_u (local.get $x) (i64.const 8)))
(i64.const 0x00FF00FF00FF00FF)))
(local.set $x (i64.and
(i64.add (i64.mul (local.get $x) (select (i64.const 256) (i64.const 100) (local.get $hex)))
(i64.shr_u (local.get $x) (i64.const 16)))
(i64.const 0x0000FFFF0000FFFF)))
(local.set $v (i32.wrap_i64
(i64.add (i64.mul (local.get $x) (select (i64.const 65536) (i64.const 10000) (local.get $hex)))
(i64.shr_u (local.get $x) (i64.const 32)))))
(local.set $p (i32.add (local.get $p) (i32.wrap_i64 (i64.shr_u (local.get $kb) (i64.const 3))))))
(else
;; Eight or more digits: one at a time, saturating past U+10FFFF.
(local.set $v (i32.const 0))
(local.set $k (select (i32.const 16) (i32.const 10) (local.get $hex)))
(block $end
(loop $digit
(if (i32.gt_u
(local.tee $h (i32.sub (i32.load8_u offset=412544 (local.get $p)) (i32.const 48)))
(i32.const 9))
(then
(if (i32.lt_u
(local.tee $h (i32.sub (i32.or (i32.load8_u offset=412544 (local.get $p)) (i32.const 32)) (i32.const 87)))
(i32.const 10))
(then (local.set $h (i32.const 16))))))
(br_if $end (i32.ge_u (local.get $h) (local.get $k)))
(if (i32.gt_u
(local.tee $v (i32.add (i32.mul (local.get $v) (local.get $k)) (local.get $h)))
(i32.const 0x10FFFF))
(then (local.set $v (i32.const 0x110000))))
(local.set $p (i32.add (local.get $p) (i32.const 1)))
(br $digit)))))
(local.set $i (i32.add (local.get $p)
(i32.eq (i32.load8_u offset=412544 (local.get $p)) (i32.const 59))))
;; 0x80-0x9F use the Windows-1252 table; 0, surrogates and values past
;; U+10FFFF become U+FFFD.
(if (i32.lt_u (i32.sub (local.get $v) (i32.const 0x80)) (i32.const 0x20))
(then (local.set $v (i32.load16_u offset=6240 (i32.shl (local.get $v) (i32.const 1)))))
(else
(if (i32.or
(i32.ge_u (i32.sub (local.get $v) (i32.const 1)) (i32.const 0x10FFFF))
(i32.lt_u (i32.sub (local.get $v) (i32.const 0xD800)) (i32.const 0x800)))
(then (local.set $v (i32.const 0xFFFD))))))
;; UTF-8, as in $utf8 but in line: a call here would cost the loop its registers.
(local.set $k
(i32.add (i32.add (i32.add (i32.const 1)
(i32.gt_u (local.get $v) (i32.const 0x7F)))
(i32.gt_u (local.get $v) (i32.const 0x7FF)))
(i32.gt_u (local.get $v) (i32.const 0xFFFF))))
(i32.store offset=150400 (local.get $o)
(select
(local.get $v)
(i32.or
(i32.shr_u
(i32.or (i32.or (i32.or
(i32.shr_u (local.get $v) (i32.const 18))
(i32.shl (i32.and (i32.shr_u (local.get $v) (i32.const 12)) (i32.const 0x3F)) (i32.const 8)))
(i32.shl (i32.and (i32.shr_u (local.get $v) (i32.const 6)) (i32.const 0x3F)) (i32.const 16)))
(i32.shl (i32.and (local.get $v) (i32.const 0x3F)) (i32.const 24)))
(i32.sub (i32.const 32) (i32.shl (local.get $k) (i32.const 3))))
(i32.load offset=6564 (i32.shl (local.get $k) (i32.const 2))))
(i32.eq (local.get $k) (i32.const 1))))
(local.set $o (i32.add (local.get $o) (local.get $k)))
(br $next)))
;; Named. $t: 0x80 in the first byte of $w that is `;`.
(local.set $t (i64.xor (local.get $w) (i64.const 0x3B3B3B3B3B3B3B3B)))
(if (i64.ne
(local.tee $t (i64.and
(i64.and (i64.sub (local.get $t) (i64.const 0x0101010101010101)) (i64.xor (local.get $t) (i64.const -1)))
(local.get $high)))
(i64.const 0))
(then
;; Up to 7 bytes: the name and its `;` are the key into fast.
(local.set $key (i64.and (local.get $w)
(i64.sub
(i64.shl (i64.const 2) (i64.add (local.tee $kb (i64.and (i64.ctz (local.get $t)) (i64.const 56))) (i64.const 7)))
(i64.const 1))))
(local.set $ep (i32.shl (i32.wrap_i64 (i64.shr_u
(i64.mul (local.get $key) (i64.const 0xA59D054B12F5B905)) (i64.const 52))) (i32.const 4)))
(loop $probe
(if (i64.eq (local.tee $val (i64.load offset=79936 (local.get $ep))) (local.get $key)) (then
(i64.store offset=150400 (local.get $o) (local.tee $val (i64.load offset=79944 (local.get $ep))))
(local.set $o (i32.add (local.get $o) (i32.wrap_i64 (i64.shr_u (local.get $val) (i64.const 56)))))
(local.set $i (i32.add (i32.add (local.get $i) (i32.const 2))
(i32.wrap_i64 (i64.shr_u (local.get $kb) (i64.const 3)))))
(br $next)))
(br_if $legacy (i64.eq (local.get $val) (i64.const -1)))
(local.set $ep (i32.add (local.get $ep) (i32.const 16)))
(br $probe)))
(else
;; No `;` in 8 bytes: a name of 8 to 31 bytes needs its `;` in the next 24.
(local.set $s0 (v128.load offset=412545 (local.get $i)))
(local.set $s1 (v128.load offset=412561 (local.get $i)))
(if (local.tee $h (i32.or
(i8x16.bitmask (i8x16.eq (local.get $s0) (local.tee $m0 (i8x16.splat (i32.const 59)))))
(i32.shl (i8x16.bitmask (i8x16.eq (local.get $s1) (local.get $m0))) (i32.const 16))))
(then
(local.set $len (i32.ctz (local.get $h)))
(local.set $h (i32.wrap_i64 (i64.shr_u (i64.mul
(i64.add
(i64.xor (local.get $w)
(i64.rotl (i64.load offset=412537 (i32.add (local.get $i) (local.get $len))) (i64.const 29)))
(i64.extend_i32_u (local.get $len)))
(i64.const 0xA0C2995F40498CB3)) (i64.const 53))))
;; Byte j of the 32 loaded at len_mask + 32 - len is 0xFF when j < len.
(local.set $m0 (v128.load offset=150336 (local.tee $k (i32.sub (i32.const 32) (local.get $len)))))
(local.set $m1 (v128.load offset=150352 (local.get $k)))
(block $miss
(loop $slot
(br_if $miss (i32.eqz (local.tee $ep
(i32.load16_u offset=10768 (i32.shl (local.get $h) (i32.const 1))))))
(if (i32.and
(i32.eq
(i32.load offset=55640 (local.tee $ep (i32.mul (local.get $ep) (i32.const 48))))
(local.get $len))
(i32.eqz (v128.any_true (v128.or
(v128.xor (v128.and (local.get $s0) (local.get $m0)) (v128.load offset=55600 (local.get $ep)))
(v128.xor (v128.and (local.get $s1) (local.get $m1)) (v128.load offset=55616 (local.get $ep)))))))
(then
(i64.store offset=150400 (local.get $o) (local.tee $val (i64.load offset=55632 (local.get $ep))))
(local.set $o (i32.add (local.get $o) (i32.wrap_i64 (i64.shr_u (local.get $val) (i64.const 56)))))
(local.set $i (i32.add (i32.add (local.get $i) (i32.const 2)) (local.get $len)))
(br $next)))
(local.set $h (i32.and (i32.add (local.get $h) (i32.const 1)) (i32.const 2047)))
(br $slot))))))))
;; Without `;`: the longest legacy name that is a prefix, longest first.
(local.set $k (i32.load8_u offset=6672 (i32.shr_u
(i32.mul (i32.and (i32.wrap_i64 (local.get $w)) (i32.const 0xFFFF)) (i32.const 0x9E3779B1))
(i32.const 20))))
(loop $try
(br_if $bare (i32.lt_u (local.get $k) (i32.const 2)))
(local.set $key (i64.or
(i64.and (local.get $w)
(i64.sub (i64.shl (i64.const 1) (i64.extend_i32_u (i32.shl (local.get $k) (i32.const 3)))) (i64.const 1)))
(i64.shl (i64.extend_i32_u (local.get $k)) (i64.const 56))))
(local.set $ep (i32.shl (i32.wrap_i64 (i64.shr_u
(i64.mul (local.get $key) (i64.const 0xF880D97D1854E551)) (i64.const 56))) (i32.const 4)))
(loop $probe
(if (i64.eq (local.tee $val (i64.load offset=145984 (local.get $ep))) (local.get $key)) (then
(i64.store offset=150400 (local.get $o) (local.tee $val (i64.load offset=145992 (local.get $ep))))
(local.set $o (i32.add (local.get $o) (i32.wrap_i64 (i64.shr_u (local.get $val) (i64.const 56)))))
(local.set $i (i32.add (i32.add (local.get $i) (i32.const 1)) (local.get $k)))
(br $next)))
(if (i64.ne (local.get $val) (i64.const -1)) (then
(local.set $ep (i32.add (local.get $ep) (i32.const 16)))
(br $probe))))
(local.set $k (i32.sub (local.get $k) (i32.const 1)))
(br $try)))
;; Not a reference: the `&` is copied.
(i32.store8 offset=150400 (local.get $o) (i32.const 38))
(local.set $o (i32.add (local.get $o) (i32.const 1)))
(local.set $i (i32.add (local.get $i) (i32.const 1))))
(local.set $e (local.get $i))
(br_if $ref (i32.lt_u (local.get $q) (local.get $out))))))
(br $window)))
;; Without a reference the input is the output.
(if (i32.eqz (local.get $e)) (then
(return (i64.or (i64.const 1771862988161024) (i64.extend_i32_u (local.get $size))))))
;; Copy the text after the last reference.
(local.set $o (i32.sub (local.get $o) (local.get $e)))
(loop $tail
(if (i32.lt_u (local.get $e) (local.get $size)) (then
(v128.store offset=150400 (i32.add (local.get $o) (local.get $e)) (v128.load offset=412544 (local.get $e)))
(local.set $e (i32.add (local.get $e) (i32.const 16)))
(br $tail))))
(i64.or (i64.const 645963081318400) (i64.extend_i32_u (i32.add (local.get $o) (local.get $size))))))
(; pack.py, which produced the packed entity list:
"""Packs the entity list for unescape-small: entities ordered by code point, coded with an
adaptive binary range coder whose contexts are the previous one, two and three name
characters and a match model. The C unpacker in unescape-small.in.c mirrors Model and Decoder exactly.
"""
import html.entities
M32 = 0xFFFFFFFF
K = 1 << 20
HASH_A, HASH_B = 0x2545F491, 0x9E3779B1
# Step toward the bit just seen, as a 16-bit fraction: 2 / (2n + 3) after n uses.
RATE = [43690, 26214, 18724, 14563, 11915, 10082]
html5 = html.entities.html5
NAMES = {k[:-1]: v for k, v in html5.items() if k.endswith(';')}
LEGACY = {k for k in html5 if not k.endswith(';')}
ALPHA = '\0' + '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
CODE = {c: i for i, c in enumerate(ALPHA)}
ORDER = sorted(NAMES, key=lambda n: ([ord(c) for c in NAMES[n]], n))
def tdiv(a, b):
"""Division truncating toward zero, as in C."""
q = abs(a) // abs(b)
return q if (a < 0) == (b < 0) else -q
class Model:
def __init__(self, bits=16, limit=6, narrow=False):
# narrow: 16-bit entries holding a 12-bit probability and a 4-bit count,
# instead of 32-bit entries holding 16 bits of each.
self.t = [0] * (1 << bits)
self.bits, self.limit, self.narrow = bits, limit, narrow
def load(self, s):
e = self.t[s]
return ((e >> 4) ^ 0x800, e & 15) if self.narrow else ((e >> 16) ^ 0x8000, e & 0xFFFF)
def slots(self, ctxs, node):
return [(((c + node * HASH_A) * HASH_B) & M32) >> (32 - self.bits) for c in ctxs]
def predict(self, slots):
"""12-bit probability that the bit is 1."""
num = den = 0
for k, s in enumerate(slots):
p, n = self.load(s)
half, shift = (0x800, 8) if self.narrow else (0x8000, 12)
w = (n * (1 + (abs(p - half) >> shift))) << k
num += p * w
den += w
if self.narrow:
p = num // den if den else 0x800
else:
p = (num // den if den else 0x8000) >> 4
return min(max(p, 1), 4095)
def update(self, slots, bit):
for s in slots:
p, n = self.load(s)
one = 0x1000 if self.narrow else 0x10000
p += (((one if bit else 0) - p) * RATE[n]) >> 16
if n < self.limit:
n += 1
self.t[s] = (p ^ 0x800) << 4 | n if self.narrow else (p ^ 0x8000) << 16 | n
class Encoder:
def __init__(self):
self.low, self.range, self.cache, self.pending, self.out = 0, M32, 0, 1, bytearray()
def shift_low(self):
if self.low < 0xFF000000 or self.low > M32:
carry = self.low >> 32
byte = self.cache
while self.pending:
self.out.append((byte + carry) & 0xFF)
byte = 0xFF
self.pending -= 1
self.cache = self.low >> 24 & 0xFF
self.pending += 1
self.low = (self.low << 8) & M32
def bit(self, p, bit):
bound = (self.range >> 12) * p
if bit:
self.range = bound
else:
self.low += bound
self.range -= bound
while self.range < 1 << 24:
self.range = (self.range << 8) & M32
self.shift_low()
def finish(self):
for _ in range(5):
self.shift_low()
assert self.out[0] == 0
return bytes(self.out[1:])
class Decoder:
def __init__(self, data):
self.data, self.pos, self.range = data + bytes(8), 4, M32
self.code = int.from_bytes(data[:4], 'big')
def bit(self, p):
bound = (self.range >> 12) * p
if self.code < bound:
self.range = bound
bit = 1
else:
self.code -= bound
self.range -= bound
bit = 0
while self.range < 1 << 24:
self.range = (self.range << 8) & M32
self.code = (self.code << 8 | self.data[self.pos]) & M32
self.pos += 1
return bit
class Coder:
"""Runs the same field sequence for packing (values given) and unpacking (values None)."""
def __init__(self, data=None, **kw):
self.m = Model(**kw)
self.enc = Encoder() if data is None else None
self.dec = Decoder(data) if data is not None else None
self.hist, self.last = [0], [0] * 4096
def sym(self, ctxs, nbits, value=None):
node = 1
for i in range(nbits - 1, -1, -1):
slots = self.m.slots(ctxs, node)
p = self.m.predict(slots)
if self.enc:
b = value >> i & 1
self.enc.bit(p, b)
else:
b = self.dec.bit(p)
self.m.update(slots, b)
node = node * 2 + b
return node - (1 << nbits)
def entity(self, cp, rec=None):
"""rec = (name, [code points], legacy) when packing. Returns the same when unpacking."""
name, cps, legacy = rec if rec else (None, None, None)
d = cps[0] - cp if rec else None
nb = self.sym([1 * K], 5, d.bit_length() if rec else None)
if nb > 1:
d = (1 << (nb - 1)) + self.sym([2 * K + nb], nb - 1, d - (1 << (nb - 1)) if rec else None)
else:
d = nb
cp += d
second = 0
if self.sym([3 * K + (d == 0)], 1, len(cps) - 1 if rec else None):
second = self.sym([4 * K], 16, cps[1] if rec else None)
leg = self.sym([5 * K + (cp < 256)], 1, int(legacy) if rec else None)
# The match model predicts the character that followed the last place the previous
# two characters were seen, and how many predictions in a row have been right.
h, out, mp, ml = 0, '', 0, 0
for i in range(64):
ctxs = [8 * K + (h & 63), 9 * K + (h & 4095),
13 * K + (self.hist[mp] if ml else 0) * 16 + min(ml, 3), 10 * K + (h & 0x3FFFF)]
c = self.sym(ctxs, 6, CODE[(name + '\0')[i]] if rec else None)
if ml and self.hist[mp] == c:
ml += 1
mp += 1
else:
ml = 0
self.hist.append(c)
if c == 0:
break
out += ALPHA[c]
h = h << 6 | c
if i >= 1:
if ml == 0 and self.last[h & 4095]:
mp, ml = self.last[h & 4095], 1
self.last[h & 4095] = len(self.hist)
return out, [cp, second] if second else [cp], bool(leg)
def records():
return [(n, [ord(c) for c in NAMES[n]], n in LEGACY) for n in ORDER]
def pack(**kw):
c = Coder(**kw)
cp = 0
for rec in records():
assert len(rec[1]) <= 2 and rec[1][0] >= cp and (len(rec[1]) == 1 or rec[1][1] < 0x10000)
c.entity(cp, rec)
cp = rec[1][0]
return c.enc.finish()
def unpack(data, count, **kw):
c = Coder(data, **kw)
cp, out = 0, []
for _ in range(count):
rec = c.entity(cp)
cp = rec[1][0]
out.append(rec)
return out
if __name__ == '__main__':
import sys
blob = pack(bits=18, limit=5)
assert unpack(blob, len(ORDER), bits=18, limit=5) == records()
print('round trip ok', len(blob))
# This file: pack(bits=17, limit=5, narrow=True)
;)
Download source MIT licensed — build on it
Run it on the example input with qipdo (Node.js 22 or newer):
printf '%s' 'Fish & chips — café <3 😀 © 2026 ¬it;' | npx qipdo calculated.world components/bea3ba1914a0cd74506507a07e9b1b2318db19d32bb38485ee755e8b78a35d71.wasm
Hand-written WebAssembly text with SIMD: the earlier version of my entry 147c6682. Same decoder and in-place single buffer (23 pages at the full 1 MiB input); the packed entity list uses fixed mixing weights, which is 375 bytes larger but unpacks faster on the first call.
- Builds on
- Decoder design from my C entry 882dd447; the single in-place buffer follows my earlier entry 4a6e243a. The range coder is the LZMA one.
Try it
Step through this Wasm running on the input above. Focus the debugger, then press Space to run, N to step over or ? for all keys.
To debug it in your terminal with qiptui (Node.js 22 or newer):
First download the Wasm and the example input:
curl -o unescape_html.wasm https://calculated.world/components/bea3ba1914a0cd74506507a07e9b1b2318db19d32bb38485ee755e8b78a35d71.wasm
printf '%s' 'Fish & chips — café <3 😀 © 2026 ¬it;' > input.txt
Then step through it in the qipdb debugger:
npx qiptui \
-F component=@unescape_html.wasm \
-F 'input=<input.txt' \
qip.dev/tui/qipdb.wasm
The debugger command fetches qipdb from qip.dev and keeps it in memory.
Build details
Compiler, size, memory and checksum
- Submitted
- 11 November 2026 at 06:48 UTC
- Language
- WAT
- Compiler
-
wat2wasm 1.0.42 (WABT)wat2wasm unescape.wat -o unescape-wat.wasm - Size
- 9.6 KiB, gzip 8.7 KiB
- Download
- ~150 ms on Slow 4G (Lighthouse’s 1.6 Mbps, 150 ms round trips)
- Memory
- 23 pages (1.4 MiB)
- SIMD
- yes
- AI models
- claude-fable-5-2
Functions, loops, branches and other static counts
- Functions
- 7
- Instructions
- 1,529
- Loops
- 20
- Conditional branches
- 49
- Indirect calls
- 0
- Memory loads
- 37
- Memory stores
- 32
- SIMD instructions
- 42
- Potentially trapping instructions
- 95
Instruction sites in the binary, not executions, counted by qip.dev’s wasm-counts.