Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ but the few rules below hold everywhere:
npm registry; [MAINTAINERS.md](MAINTAINERS.md) lists who can change what, and
[SECURITY.md](SECURITY.md) how a release is verified.

`spec/` is not part of the published package and nothing under `src/` depends on it: it is an
attempt at writing a utility once and emitting it to every language Brazilian Utils publishes.
Read [spec/README.md](spec/README.md) for what is there and
[spec/bridge/README.md](spec/bridge/README.md) for how to add a utility to it.

The actors around the code are the consumers of the npm package, the contributors (pull requests
from forks), the maintainers (review, merge, release approval) and the automation: GitHub Actions
builds, tests and publishes, Dependabot and the `Update datasets` workflow open update pull requests, and
Expand Down
396 changes: 396 additions & 0 deletions spec/BINDINGS-INVESTIGATION.md

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions spec/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# `spec/` — write a utility once, ship it in every language

Brazilian Utils publishes the same utilities in JavaScript, Python, Go, Rust and Ruby, and each
one is written by hand, in each language, again. This directory is the attempt to write each
utility **once** and have every language's implementation come out of that.

Nothing here ships in the npm package. `src/` is untouched, and the build, the bundle and the
public API are exactly what they were.

## Layout

| Path | What it holds |
| --------------------------- | ------------------------------------------------------------------------------- |
| `bridge/` | The engine: a portable TypeScript subset in, native code for seven targets out |
| `BINDINGS-INVESTIGATION.md` | The other route — one binary core plus bindings — measured, and where each wins |

Start with [`bridge/README.md`](bridge/README.md): it is the manual for the engine, the subset it
accepts and what it refuses. [`BINDINGS-INVESTIGATION.md`](BINDINGS-INVESTIGATION.md) is the
research that decided the shape of it, including the benchmark numbers that say a binding is
cheap in C#, Python, Ruby and Java, and that JavaScript cannot take one at all.

## Running it

```bash
node spec/bridge/compiler/cli.ts # compile source/ into every target
node spec/bridge/compiler/cli.ts rust # ...or just one

bash spec/bridge/conformance/run-all.sh # every target replays what the npm package answers
```
21 changes: 21 additions & 0 deletions spec/bridge/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# The compiler's output is committed, so the code it writes can be read in review rather than
# taken on trust. `node compiler/cli.ts` and `node conformance/drivers.ts` rewrite it, and
# every file says DO NOT EDIT.
#
# What the targets' own toolchains then build from it is not committed: those are binaries and
# caches, and `conformance/run-all.sh` recreates them. The paths are spelled out per target
# rather than globbed, because `out/rust/src/bin/` is generated source, not a build directory.
out/rust/target/
out/rust/Cargo.lock
out/rust/conformance_cabi
out/java/classes/
out/csharp/bin/
out/csharp/obj/
out/python/**/__pycache__/

node_modules/

# The conformance tables are recorded from the package this repository ships, by
# `conformance/record.ts`, which `run-all.sh` runs first.
# `source/*.data.json` is committed: it is an input to the compiler, not a recording.
conformance/recorded/
311 changes: 311 additions & 0 deletions spec/bridge/README.md

Large diffs are not rendered by default.

83 changes: 83 additions & 0 deletions spec/bridge/compiler/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* Compiles every module under `source/` into every target under `compiler/targets/`.
*
* Usage: `node compiler/cli.ts [target...]`
*/
import { mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import { dirname, extname, resolve } from "node:path";

import { compileModule } from "./frontend.ts";
import { type Module } from "./ir.ts";
import { emit as emitCsharp } from "./targets/csharp.ts";
import { emit as emitGo } from "./targets/go.ts";
import { emit as emitJava } from "./targets/java.ts";
import { emit as emitPython } from "./targets/python.ts";
import { emit as emitRuby } from "./targets/ruby.ts";
import { emit as emitRust } from "./targets/rust.ts";
import { emit as emitTypeScript } from "./targets/typescript.ts";

const root = resolve(import.meta.dirname, "..");

/** The files whose blank lines are content rather than layout. */
const VERBATIM = new Set([".json", ".toml", ".mod"]);

/**
* Closes the gaps pruning leaves behind.
*
* An emitter joins its sections with a blank line between them, so a module that reaches none
* of the constants, or none of the errors, ends up with a run of empty lines where they would
* have been. Every target spells a string literal with escapes rather than a real newline, so
* a run of three or more newlines in an emitted file is always layout and never content.
*
* @param {string} path - The file being written, whose extension says whether to leave it be.
* @param {string} contents - What the emitter produced.
* @returns {string} The same file with at most one blank line in a row.
*/
const tidy = (path: string, contents: string): string => {
if (VERBATIM.has(extname(path))) return contents;

return `${contents.replaceAll(/\n{3,}/g, "\n\n").trimEnd()}\n`;
};

const targets: Record<string, (module: Module, modules: Module[]) => Record<string, string>> = {
typescript: emitTypeScript,
python: emitPython,
go: emitGo,
rust: emitRust,
ruby: emitRuby,
java: emitJava,
csharp: emitCsharp,
};

const requested = process.argv.slice(2);
const selected = requested.length > 0 ? requested : Object.keys(targets);
// `_std.ts` is the portable standard library: every target implements it natively, so it is
// documentation and a reference implementation rather than a module to compile.
const sources = readdirSync(resolve(root, "source")).filter(
(name) => name.endsWith(".ts") && !name.startsWith("_"),
);
const modules = sources.map((name) => compileModule(resolve(root, "source", name)));

for (const target of selected) {
const emit = targets[target];

if (emit === undefined) throw new Error(`unknown target: ${target}`);

const outDir = resolve(root, "out", target);

rmSync(outDir, { recursive: true, force: true });

let count = 0;

for (const module of modules) {
for (const [path, contents] of Object.entries(emit(module, modules))) {
const full = resolve(outDir, path);

mkdirSync(dirname(full), { recursive: true });
writeFileSync(full, tidy(full, contents));
count++;
}
}

console.log(`${target.padEnd(11)} ${count} files from ${modules.length} module(s)`);
}
Loading
Loading