{"_id":"@blizzhackers/nip-compiler","_rev":"2-a4a605d7ada54a99ee6499ca84fa0f8f","name":"@blizzhackers/nip-compiler","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@blizzhackers/nip-compiler","version":"0.1.0","keywords":["diablo2","d2","nip","jip","item-filter","compiler","kolbot","d2bs","pickit","ast","transpiler"],"license":"MIT","_id":"@blizzhackers/nip-compiler@0.1.0","maintainers":[{"name":"nishimura-katsuo","email":"jwebbb01@hotmail.com"},{"name":"jaenster","email":"github@stoots.info"}],"homepage":"https://blizzhackers.github.io/nip-compiler/","bugs":{"url":"https://github.com/blizzhackers/nip-compiler/issues"},"bin":{"nip-compile":"dist/cli.js"},"dist":{"shasum":"f2064da32c9458e099c9631969ce9c9c719071d0","tarball":"https://registry.npmjs.org/@blizzhackers/nip-compiler/-/nip-compiler-0.1.0.tgz","fileCount":120,"integrity":"sha512-+SnBRMufUkVqu4Of0yhFE++6QUk15CmLKXUPvW30jC6tf+irDuF+rBW/d+NxLDtR/DieL6PIg9dfPuR/C+7t1A==","signatures":[{"sig":"MEQCICz4cRcbu9cFSWJ43IXgX6P/fLB2OgDlb5M9Dumj5ezFAiB7Sa8zzpLwKVJJI79QrCoPbhIOCZTYJ1GwvRqdkTVMkw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1886174},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./aliases":{"types":"./dist/emitter/d2-aliases.d.ts","import":"./dist/emitter/d2-aliases.js","default":"./dist/emitter/d2-aliases.js"},"./d2-data":{"types":"./dist/emitter/d2-type-map.d.ts","import":"./dist/emitter/d2-type-map.js","default":"./dist/emitter/d2-type-map.js"},"./set-items":{"types":"./dist/emitter/d2-set-items.d.ts","import":"./dist/emitter/d2-set-items.js","default":"./dist/emitter/d2-set-items.js"},"./unique-items":{"types":"./dist/emitter/d2-unique-items.d.ts","import":"./dist/emitter/d2-unique-items.js","default":"./dist/emitter/d2-unique-items.js"},"./discriminator":{"types":"./dist/emitter/d2-discriminator.d.ts","import":"./dist/emitter/d2-discriminator.js","default":"./dist/emitter/d2-discriminator.js"}},"gitHead":"373e1e34301b4b9d813ff3044196a97c280b7792","scripts":{"test":"node --import tsx --test 'src/**/*.test.ts'","build":"tsc","start":"tsx src/index.ts","compile":"tsx src/cli.ts"},"_npmUser":{"name":"jaenster","email":"github@stoots.info"},"repository":{"url":"git+https://github.com/blizzhackers/nip-compiler.git","type":"git","directory":"packages/compiler"},"_npmVersion":"10.8.1","description":"NIP/JIP compiler for Diablo 2 item filtering — lexer, parser, binder, and optimizing code generator","directories":{},"_nodeVersion":"24.12.0","dependencies":{"escodegen":"^2.1.0"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.0","playwright":"^1.59.1","typescript":"^5.7.0","@types/node":"^25.5.0","@types/estree":"^1.0.8","@types/escodegen":"^0.0.10"},"_npmOperationalInternal":{"tmp":"tmp/nip-compiler_0.1.0_1775157202993_0.9003129405572017","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@blizzhackers/nip-compiler","version":"0.1.1","type":"module","description":"NIP/JIP compiler for Diablo 2 item filtering — lexer, parser, binder, and optimizing code generator","license":"MIT","homepage":"https://blizzhackers.github.io/nip-compiler/","bugs":{"url":"https://github.com/blizzhackers/nip-compiler/issues"},"repository":{"type":"git","url":"git+https://github.com/blizzhackers/nip-compiler.git","directory":"packages/compiler"},"keywords":["diablo2","d2","nip","jip","item-filter","compiler","kolbot","d2bs","pickit","ast","transpiler"],"main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./aliases":{"types":"./dist/emitter/d2-aliases.d.ts","import":"./dist/emitter/d2-aliases.js","default":"./dist/emitter/d2-aliases.js"},"./d2-data":{"types":"./dist/emitter/d2-type-map.d.ts","import":"./dist/emitter/d2-type-map.js","default":"./dist/emitter/d2-type-map.js"},"./unique-items":{"types":"./dist/emitter/d2-unique-items.d.ts","import":"./dist/emitter/d2-unique-items.js","default":"./dist/emitter/d2-unique-items.js"},"./set-items":{"types":"./dist/emitter/d2-set-items.d.ts","import":"./dist/emitter/d2-set-items.js","default":"./dist/emitter/d2-set-items.js"},"./discriminator":{"types":"./dist/emitter/d2-discriminator.d.ts","import":"./dist/emitter/d2-discriminator.js","default":"./dist/emitter/d2-discriminator.js"}},"bin":{"nip-compile":"dist/cli.js"},"scripts":{"build":"tsc","test":"node --import tsx --test 'src/**/*.test.ts'","start":"tsx src/index.ts","compile":"tsx src/cli.ts"},"devDependencies":{"@types/escodegen":"^0.0.10","@types/estree":"^1.0.8","@types/node":"^25.5.0","playwright":"^1.59.1","tsx":"^4.19.0","typescript":"^5.7.0"},"dependencies":{"escodegen":"^2.1.0"},"gitHead":"168b921a30244c4209138f28b18b4d02847ac170","_id":"@blizzhackers/nip-compiler@0.1.1","_nodeVersion":"24.14.0","_npmVersion":"11.9.0","dist":{"integrity":"sha512-BydsLcyFy5yaSe7ZhojDfAUBnbbgGrQA6MuET+zypnrEkB1b7hZKAlimkSfQIFkDBHldoUArPiCuKWnXmJKLFQ==","shasum":"73e9fa7508a28a902a20ee9a1feb0265f14c1a9b","tarball":"https://registry.npmjs.org/@blizzhackers/nip-compiler/-/nip-compiler-0.1.1.tgz","fileCount":113,"unpackedSize":1851201,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@blizzhackers%2fnip-compiler@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDYLiJ01A0MhT+4xUDYFBq9tFQsfTAJMiCv+TEKZeQ4VQIhAMEsT4HKEJRLa/61nB7k83bi0MHmwaqAj2BS84+vWtyI"}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:63dca22f-5ab0-466f-89d0-e5582703380b"}},"directories":{},"maintainers":[{"name":"nishimura-katsuo","email":"jwebbb01@hotmail.com"},{"name":"jaenster","email":"github@stoots.info"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/nip-compiler_0.1.1_1775157568706_0.8249830630409034"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-02T19:13:22.916Z","modified":"2026-04-02T19:19:29.166Z","0.1.0":"2026-04-02T19:13:23.150Z","0.1.1":"2026-04-02T19:19:28.861Z"},"bugs":{"url":"https://github.com/blizzhackers/nip-compiler/issues"},"license":"MIT","homepage":"https://blizzhackers.github.io/nip-compiler/","keywords":["diablo2","d2","nip","jip","item-filter","compiler","kolbot","d2bs","pickit","ast","transpiler"],"repository":{"type":"git","url":"git+https://github.com/blizzhackers/nip-compiler.git","directory":"packages/compiler"},"description":"NIP/JIP compiler for Diablo 2 item filtering — lexer, parser, binder, and optimizing code generator","maintainers":[{"name":"nishimura-katsuo","email":"jwebbb01@hotmail.com"},{"name":"jaenster","email":"github@stoots.info"}],"readme":"# @blizzhackers/nip-compiler\n\nNIP file lexer, parser, binder, and code emitter for Diablo 2 item filtering. Compiles `.nip` pickit rules into optimized JavaScript via ESTree AST + escodegen — **157x faster** than kolbot's runtime `NTItemParser` on SpiderMonkey (Firefox), **219x** on V8 (Chromium).\n\n## Install\n\n```bash\nnpm install @blizzhackers/nip-compiler\n```\n\n## CLI\n\n```bash\n# Basic compilation\nnip-compile pickit/*.nip -o checkItem.js\n\n# Kolbot mode (CJS, self-registers via NTIP.addCompiled)\nnip-compile --kolbot -o pickit/compiled/checkItem.js pickit/*.nip\n\n# Pretty-print\nnip-compile --pretty -o checkItem.js pickit/*.nip\n\n# Minified\nnip-compile --minify -o checkItem.min.js pickit/*.nip\n\n# With source maps\nnip-compile --pretty --sourcemap -o checkItem.js pickit/*.nip\n```\n\n### Options\n\n| Flag | Description |\n|-|-|\n| `-o, --output <file>` | Output file (default: stdout) |\n| `--kolbot` | Emit CJS with kolbot-compatible format |\n| `--pretty` | Pretty-print output |\n| `--minify` | Strip comments and whitespace |\n| `--no-comments` | Omit source comments |\n| `--format <type>` | Output format: `iife` (default), `esm`, `cjs` |\n| `--sourcemap` | Generate `.map` source map file |\n\n## API\n\n```ts\nimport { Parser, Binder, Emitter, d2Aliases, OutputFormat } from '@blizzhackers/nip-compiler';\n\nconst parser = new Parser();\nconst binder = new Binder();\n\n// Parse\nconst ast = parser.parseFile('[name] == berrune', 'runes.nip');\nconst { diagnostics } = binder.bindFile(ast);\n\n// Emit\nconst emitter = new Emitter({\n  aliases: d2Aliases,\n  kolbotCompat: true,\n  prettyPrint: true,\n  outputFormat: OutputFormat.CJS,\n});\nconst js = emitter.emit([ast]);\n```\n\n## Architecture\n\n```\n.nip source\n    |\n    v\n  Lexer     -> Token stream\n    |\n    v\n  Parser    -> AST (NipFileNode -> NipLineNode -> ExprNode)\n    |\n    v\n  Binder    -> Resolves aliases, validates keywords/values, reports diagnostics\n    |\n    v\n  Analyzer  -> Extracts dispatch keys, expands [type] to classids\n    |\n    v\n  Grouper   -> Groups rules by classid + quality\n    |\n    v\n  Emitter   -> ESTree AST via emitter-ast.ts + codegen-ast.ts\n    |\n    v\n  escodegen -> JavaScript code + source maps from AST node .loc\n```\n\n## Why it's fast\n\nThe original `NTItemParser` (kolbot/d2bs) checks items by iterating **all rules** for every item. With 807 rules, that's ~1000 property reads per item — even for junk items nobody wants.\n\nWe compile rules into a dispatch table indexed by `classid | (quality << 10)`. Checking an item is:\n\n1. **One Uint16Array read** — `_mi[classid | (quality << 10)]` (raw memory, no type checks)\n2. **One switch jump** — handler index to direct function call\n3. **5-20 stat checks** in the handler (or zero for no-match)\n\nFor junk items (the vast majority in-game): step 1 returns 0, step 2 hits `case 0: break`, done. **Zero `getStatEx` calls, zero `getFlag` calls.** In d2bs, where `getStatEx` crosses the JS-to-C++ boundary, this means zero native calls for items we don't care about.\n\n### Per-item cost comparison\n\n| | Original NTIP | Compiled |\n|-|-|-|\n| Junk item (no match) | ~1000 property reads | **1 array read** |\n| Matching item (e.g. SoJ) | ~430 reads + stat calls | **15 reads** (29x fewer) |\n| Unid unique ring | ~1020 reads | **9 reads** (113x fewer) |\n\n### Benchmarks (Playwright, 215 items, 9 nip files)\n\n| Engine | Original NTIP | Switch dispatch | Object lookup | Object vs Original |\n|-|-|-|-|-|\n| Firefox 137 (SpiderMonkey) | 127K ops/s | 5.7M ops/s | **19.9M ops/s** | **157x** |\n| Firefox 148 | 120K ops/s | 7.3M ops/s | **22.5M ops/s** | **189x** |\n| Chromium (V8) | 102K ops/s | 16.7M ops/s | **22.2M ops/s** | **219x** |\n\nSpiderMonkey is the target engine (d2bs uses it). Object lookup is **3.5x faster than switch** on SpiderMonkey because the `Uint16Array` read + switch jump table avoids the overhead of SpiderMonkey's sparse array dispatch.\n\n## Optimizations in emitted code\n\n### Dispatch\n\n- **Uint16Array + switch jump table** — `_mi[classid|(quality<<10)]` returns a handler index; `switch(_ix)` dispatches via jump table. SpiderMonkey compiles dense integer switches to jump tables. The Uint16Array read is a raw memory access with no type checks or hole checks.\n- **Type-to-classid expansion** — `[type] == armor` is expanded to all 45 armor classids at compile time, eliminating the runtime type switch entirely. Handler dedup across classids ensures no code bloat.\n- **Quality range expansion** — `[quality] <= superior` becomes `case 1: case 2: case 3:` switch labels instead of `if (_q <= 3)`.\n- **Impossible quality filtering** — uses D2 item type data (`d2-type-map.ts`) to skip handlers for impossible quality combos. Charms can only be magic/unique, runes/gems/potions are always normal, jewels are magic/rare/unique only. The OG NTIP would blindly match a \"rare charm\" — we don't even emit code for it.\n\n### Stat evaluation\n\n- **Group-level stat hoisting** — `getStatEx` calls used 2+ times across rules in the same quality group are lifted to `const _hN` declarations, avoiding redundant native calls.\n- **Per-rule local hoisting** — stats used 2+ times within a single rule's expression get their own `_lN` variable.\n- **Selectivity reordering** — AND conditions sorted by selectivity (`==` first, then `!=`, ranges, OR) for faster short-circuit failure.\n- **Complementary if/else** — `flag`/`!flag` and `<`/`>=` pairs chained as if/else instead of separate if blocks, avoiding redundant `getFlag` calls.\n\n### Unidentified item handling\n\n- **Two-pass base/magical split** — base stats (defense, damage, durability) are always readable, even on unidentified items. Base-stat rules run first. Only magical-stat rules trigger the unid bail (`return -1` = \"maybe, ID this item\").\n- **Per-flag-group unid bail** — flag checks (ethereal, runeword) are property conditions that must pass before unid bail fires. Each flag group gets its own bail inside the condition, matching original NTIP behavior.\n- **Quality-aware base stats** — sockets (stat 194) are readable on normal/superior quality items. Quality <= 3 items are always identified in-game, so _id checks are skipped entirely.\n\n### Code structure\n\n- **Handler dedup** — classids sharing identical rule sets (e.g., all 45 armor classids from type expansion) share one handler function. Source signature-based dedup across classids.\n- **Dead code elimination** — unconditional matches cut unreachable rules. Dead returns after early returns are pruned. Unused `_c`/`_q`/`_t` declarations skipped per handler.\n- **Packed source table** — verbose mode decodes file and line from a packed `_s` array: `(line << _b) | fileId`. Dynamic bit width based on file count. No array-of-arrays.\n- **ESTree AST** — code generated via proper AST construction + escodegen, enabling source maps, formatting control, and structural optimizations that string concatenation can't do.\n\n### SpiderMonkey-specific\n\n- **Dense Uint16Array** — dispatch index stored in a `Uint16Array` (raw typed memory). SpiderMonkey marks regular sparse arrays as \"holey\" which adds per-access checks. Typed arrays avoid this entirely — **57% faster** than dense `Array.fill(0)` on Firefox.\n- **`|0` coercion** — signals int32 type to SpiderMonkey's type inference system, avoiding double-to-int conversions.\n\n## Kolbot integration\n\nThe `--kolbot` flag emits a module that self-registers:\n\n```js\n// In Pickit.init() — one line:\nrequire(\"pickit/compiled/checkItem.js\");\n```\n\nThe compiled module calls `NTIP.addCompiled()` on load. `CheckItem`, `GetTier`, and `GetMercTier` check compiled rules first, then fall through to `NTIP_CheckList` for inline rules. Custom `entryList` callers (Runewords, ShopBot) bypass compiled automatically.\n\n## Tests\n\n```bash\nnpm test\n```\n\n- 223 cross-validation tests against the original `NTItemParser.js` with 215 items covering quality ranges, eth/non-eth complement chains, unidentified items with base stats, rune ranges, runewords, prefix/suffix, tier calculations, maxquantity, junk items, and edge cases — **0 mismatches**.\n- 6 tests verify we *intentionally differ* from the OG NTIP by rejecting impossible quality combinations (rare charms, magic runes, unique gold) that the original would blindly match.\n- Playwright benchmark suite runs on Firefox (SpiderMonkey) and Chromium (V8) for cross-engine performance validation.\n","readmeFilename":"README.md"}