npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gcu/adder

v0.3.0

Published

Python-in-JavaScript. AST parser + tree-walking evaluator in pure JS, no Wasm. Standalone library — Python values are JS values (no FFI boundary). Used by Auditable as a cell type.

Downloads

434

Readme

@gcu/adder

Python-in-JavaScript. AST parser + tree-walking evaluator in pure JS, no WASM. A full Python dialect (classes, decorators, generators, try/except, comprehensions, async/await, import machinery) implemented as a regular JS library — values pass between Python and JS without any FFI boundary.

Ships with an optional AIR-transpilation fast path (@gcu/adder/air) that compiles adder source to V8-hinted JavaScript via @gcu/air, typically 15–300× faster than the tree-walker on numeric workloads.

Originally built for Auditable notebooks; usable as a standalone library anywhere ESM runs — browsers, Node, workers, Deno.

Pre-1.0 — APIs may change on minor version bumps.

Install

npm install @gcu/adder

# optional: install @gcu/air for the fast-path transpiler
npm install @gcu/air

Quick start

import { run } from '@gcu/adder';

const scope = await run(`
  def fib(n):
      return n if n < 2 else fib(n-1) + fib(n-2)

  answer = fib(10)
`);

console.log(scope.answer);   // 55
console.log(scope.fib);      // [AsyncFunction: fib]

For 15–300× faster execution, the same program through the AIR transpile path:

import { run } from '@gcu/adder/air';

const scope = await run(code);  // identical API

API

All four functions share the same options shape and exist in both @gcu/adder (tree-walker) and @gcu/adder/air (AIR-transpile).

run(code, opts?): Promise<scope>

Execute adder source as a module. Returns an object mapping bound names to values.

evalExpr(code, opts?): Promise<value>

Evaluate a single adder expression and return its value. Useful for REPL expression-mode or embedding Python expressions in other workflows.

await evalExpr('sum(range(10))')         // 45
await evalExpr('"hello".upper()')        // "HELLO"

compile(code)

Parse the source once, reuse for many executions. The AIR path additionally caches the emitted JavaScript.

import { compile } from '@gcu/adder/air';

const m = await compile(`result = base ** exp`);
m.imports;                                       // ['base', 'exp']
(await m.run({ globals: { base: 2, exp: 10 } })).result;    // 1024
(await m.run({ globals: { base: 3, exp: 5 } })).result;     // 243

isIncomplete(code)

True if the parser would want more input (unclosed bracket, missing block body, trailing comma). REPLs use this to decide whether to keep reading.

isIncomplete('def foo():')            // true  — body is missing
isIncomplete('def foo():\n  pass')    // false
isIncomplete('(1 + 2')                // true  — unclosed paren
isIncomplete('1 + 2')                 // false

Options

{
  // Bindings (Python exec() style)
  globals?: Record<string, unknown>    // lowest precedence
  locals?:  Record<string, unknown>    // higher precedence than globals

  // IO hooks
  print?:   (...args) => void           // override print() (gets raw args)
  stdout?:  (s: string) => void         // where default print writes
  stderr?:  (s: string) => void
  stdin?:   () => string | null | Promise<string | null>   // next line for input()
  input?:   (prompt?) => string | Promise<string>          // override input() entirely

  // Filesystem
  vfs?:     VFSLike                     // @gcu/vfs-shaped object for open(), os, pathlib
}

Name resolution order: localsglobals → adder builtins (print, input, len, range, int, str, etc.) → NameError.

Usage patterns

Script runner

import { run } from '@gcu/adder/air';
import { readFile } from 'node:fs/promises';
import readline from 'node:readline';

// Line-by-line stdin reader, returns null on EOF.
const rl = readline.createInterface({ input: process.stdin });
const buffered = [];
let resolveNext = null;
let closed = false;
rl.on('line',  line => resolveNext ? (resolveNext(line), resolveNext = null) : buffered.push(line));
rl.on('close', ()   => { closed = true; if (resolveNext) { resolveNext(null); resolveNext = null; } });
const nextLine = () => buffered.length ? Promise.resolve(buffered.shift())
                     : closed         ? Promise.resolve(null)
                                      : new Promise(r => { resolveNext = r; });

const script = await readFile(process.argv[2], 'utf8');
await run(script, {
  stdin:  nextLine,
  stdout: s => process.stdout.write(s),
  stderr: s => process.stderr.write(s),
  globals: {
    sys: { argv: process.argv.slice(2) },
  },
});
rl.close();

Capturing output (tests, server-side rendering)

const lines = [];
await run(`
  print("hello")
  for i in range(3):
      print(i * i)
`, {
  stdout: s => lines.push(s),
});
// lines: ['hello\n', '0\n', '1\n', '4\n']

Custom print (raw args, not formatted string)

await run(`print("x =", x, "squared:", x*x)`, {
  globals: { x: 7 },
  print: (...args) => myLogger.info('py:', args),
});
// myLogger receives: ['x =', 7, 'squared:', 49]

Input / interactive

const lines = ['Arthur', '42'];
await run(`
  name = input("Name? ")
  age = int(input("Age? "))
  print(f"Hello {name}, age {age}")
`, {
  stdin:  () => lines.shift(),
  stdout: s => process.stdout.write(s),
});

Filesystem via VFS

import { VFS, MemoryBackend } from '@gcu/vfs';

const vfs = new VFS({ backend: new MemoryBackend() });
await vfs.writeFile('/hello.txt', new TextEncoder().encode('hi from vfs'));

await run(`
  from pathlib import Path
  text = Path("/hello.txt").read_text()
  print(text.upper())
`, { vfs, stdout: s => process.stdout.write(s) });
// → HI FROM VFS

Building a REPL

import { isIncomplete, evalExpr, run } from '@gcu/adder/air';
import readline from 'node:readline/promises';
import { stdin, stdout } from 'node:process';
import { inspect } from 'node:util';

const rl = readline.createInterface({ input: stdin, output: stdout });
const scope = {};
const opts = { locals: scope, stdout: s => stdout.write(s) };

let buffer = '';
while (true) {
  const line = await rl.question(buffer ? '... ' : '>>> ');
  buffer = buffer ? buffer + '\n' + line : line;
  if (isIncomplete(buffer)) continue;

  try {
    // Try as an expression first — if it parses, display its value
    const value = await evalExpr(buffer, opts);
    if (value !== undefined) stdout.write(inspect(value) + '\n');
  } catch {
    // Not a single expression — run as a statement block and absorb the scope
    Object.assign(scope, await run(buffer, opts));
  }
  buffer = '';
}

Tagged template (inline Python in JS)

import { adderTag } from '@gcu/adder';

const scale = 2.5;
const { result } = await adderTag`
  result = ${scale} * 10
`;
// result: 25

Data model

Python values ARE JavaScript values. No FFI boundary:

| Python type | JS representation | |----------------|---------------------------------------------------------| | int, float | number | | str | string | | bool | boolean | | None | null | | list, tuple| Array | | dict | plain Object (or Map for non-string keys) | | set | Set | | range | AdderRange (lazy, iterable) | | bytes | Uint8Array |

A list defined in a run() call is a regular Array — pass it to any JS function, return it from a Promise, store it in IndexedDB, it's just an array.

Built-in modules

Available via import inside adder code:

| module | what's there | |---------------|--------------| | math | constants + functions (pi, sin, sqrt, factorial, gcd, ...) | | json | dumps(), loads() (Map/Set aware) | | random | random(), randint(), choice(), shuffle(), gauss() (xoshiro128 PRNG) | | itertools | chain, product, combinations, permutations, accumulate, groupby, ... | | functools | reduce, partial, lru_cache | | collections | OrderedDict, defaultdict, Counter, namedtuple | | re | match, search, findall, sub, split, compile | | string | ascii_lowercase, digits, punctuation, etc. | | sys | version, platform, path, argv, exit | | js | proxy to globalThis — reach any host API | | this | the Zen of Python |

When opts.vfs is set, additional filesystem modules are available: os, os.path, pathlib, shutil, glob. The built-in open() also works with with context managers.

Using @gcu/adder inside Auditable

Load as a cell type from a notebook:

await install("@gcu/adder")   // or load() in dev

This fetches the full bundled build and registers adder as a cell type. Press n to convert a cell or use the cell-header button. See also @gcu/adder/register for the side-effect entry point.

@gcu/adder/cell exposes pythonParseNames, pythonFindUses, pythonExecute for custom DAG integration. @gcu/adder/highlight exposes tokenizePython and pythonCompletions. @gcu/adder/vfs exposes setAdderVFS/getAdderVFS for global VFS configuration (prefer the per-call opts.vfs above).

What's not supported

  • Generators / yield are parsed but not evaluated
  • Multiple inheritance
  • Metaclasses
  • match / case
  • exec() / eval()

See SPEC.md for language details.

License

MIT — see LICENSE.