Skip to content
Merged
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
42 changes: 35 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,18 @@ jobs:
libzstd-dev zlib1g-dev libsqlite3-dev libcurl4-openssl-dev libpq-dev
sudo ln -sf /usr/bin/clang-21 /usr/bin/clang
sudo ln -sf /usr/bin/llvm-config-21 /usr/bin/llvm-config
# Rust toolchain for vendor build of librure (rust-lang/regex C ABI).
# Build-time dep only — end users installing the release tarball
# receive a prebuilt librure.a and never need rustc.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- run: npm install
- uses: actions/cache@v4
with:
path: |
vendor/
c_bridges/*.o
key: vendor-${{ runner.os }}-${{ hashFiles('scripts/build-vendor.sh', 'scripts/vendor-pins.sh', 'c_bridges/*.c', 'c_bridges/*.cpp') }}
key: vendor-rure1-${{ runner.os }}-${{ hashFiles('scripts/build-vendor.sh', 'scripts/vendor-pins.sh', 'c_bridges/*.c', 'c_bridges/*.cpp') }}
- run: bash scripts/build-vendor.sh
- run: npm run build
- name: Build tree-sitter TSX objects
Expand Down Expand Up @@ -121,7 +126,15 @@ jobs:
return f' (\u2191{pct:.0f}%)'
compute = {k: b for k, b in d['benchmarks'].items() if b.get('category') != 'cli'}
cli = {k: b for k, b in d['benchmarks'].items() if b.get('category') == 'cli'}
for k, b in sorted(compute.items(), key=lambda x: x[1]['name']):
def rank_sort_key(b):
r = b['results']
lower = b.get('lower_is_better', True)
chad_v = r.get('chadscript', {}).get('value')
if chad_v is None:
return (99, b['name'])
vals = sorted([v['value'] for v in r.values()], reverse=not lower)
return (vals.index(chad_v) + 1, b['name'])
for k, b in sorted(compute.items(), key=lambda x: rank_sort_key(x[1])):
r = b['results']
def g(lang):
v = r.get(lang, {}).get('label', '—')
Expand All @@ -132,7 +145,7 @@ jobs:
cli_names = {'grep': 'grep', 'ripgrep': 'ripgrep', 'node': 'Node.js', 'xxd': 'xxd'}
lines.append('')
lines.append('### CLI Tool Benchmarks\n')
for k, b in sorted(cli.items(), key=lambda x: x[1]['name']):
for k, b in sorted(cli.items(), key=lambda x: rank_sort_key(x[1])):
r = b['results']
competitors = [l for l in r if l != 'chadscript']
chad_v = r.get('chadscript', {}).get('label', '—')
Expand Down Expand Up @@ -203,6 +216,11 @@ jobs:
libzstd-dev zlib1g-dev libsqlite3-dev libcurl4-openssl-dev libpq-dev
sudo ln -sf /usr/bin/clang-21 /usr/bin/clang
sudo ln -sf /usr/bin/llvm-config-21 /usr/bin/llvm-config
# Rust toolchain for vendor build of librure (rust-lang/regex C ABI).
# Build-time dep only — end users installing the release tarball
# receive a prebuilt librure.a and never need rustc.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
clang --version 2>&1 | head -2

- name: Install npm dependencies
Expand All @@ -214,7 +232,7 @@ jobs:
path: |
vendor/
c_bridges/*.o
key: vendor-${{ runner.os }}-${{ hashFiles('scripts/build-vendor.sh', 'scripts/vendor-pins.sh', 'c_bridges/*.c', 'c_bridges/*.cpp') }}
key: vendor-rure1-${{ runner.os }}-${{ hashFiles('scripts/build-vendor.sh', 'scripts/vendor-pins.sh', 'c_bridges/*.c', 'c_bridges/*.cpp') }}

- name: Build vendor libraries
run: bash scripts/build-vendor.sh
Expand All @@ -225,7 +243,7 @@ jobs:
- name: Verify vendor libraries
run: |
fail=0
for lib in vendor/bdwgc/libgc.a vendor/yyjson/libyyjson.a vendor/libuv/build/libuv.a vendor/picohttpparser/picohttpparser.o c_bridges/lws-bridge.o c_bridges/multipart-bridge.o c_bridges/regex-bridge.o c_bridges/child-process-bridge.o c_bridges/child-process-spawn.o c_bridges/trampoline-bridge.o c_bridges/os-bridge.o c_bridges/strlen-cache.o c_bridges/time-bridge.o c_bridges/base64-bridge.o c_bridges/url-bridge.o c_bridges/uri-bridge.o c_bridges/dotenv-bridge.o c_bridges/watch-bridge.o c_bridges/arena-bridge.o c_bridges/curl-bridge.o c_bridges/pg-bridge.o c_bridges/compress-bridge.o c_bridges/yaml-bridge.o c_bridges/string-ops-bridge.o c_bridges/llvm-bridge.o c_bridges/llvm-builder-bridge.o c_bridges/lld-bridge.o; do
for lib in vendor/bdwgc/libgc.a vendor/yyjson/libyyjson.a vendor/libuv/build/libuv.a vendor/picohttpparser/picohttpparser.o vendor/rure/librure.a c_bridges/lws-bridge.o c_bridges/multipart-bridge.o c_bridges/regex-bridge.o c_bridges/child-process-bridge.o c_bridges/child-process-spawn.o c_bridges/trampoline-bridge.o c_bridges/os-bridge.o c_bridges/strlen-cache.o c_bridges/time-bridge.o c_bridges/base64-bridge.o c_bridges/url-bridge.o c_bridges/uri-bridge.o c_bridges/dotenv-bridge.o c_bridges/watch-bridge.o c_bridges/arena-bridge.o c_bridges/curl-bridge.o c_bridges/pg-bridge.o c_bridges/compress-bridge.o c_bridges/yaml-bridge.o c_bridges/string-ops-bridge.o c_bridges/llvm-bridge.o c_bridges/llvm-builder-bridge.o c_bridges/lld-bridge.o; do
if [ ! -f "$lib" ]; then
echo "MISSING: $lib"
fail=1
Expand Down Expand Up @@ -289,6 +307,7 @@ jobs:
cp c_bridges/lws-bridge.o release/lib/
cp c_bridges/multipart-bridge.o release/lib/
cp c_bridges/regex-bridge.o release/lib/
cp vendor/rure/librure.a release/lib/
cp c_bridges/child-process-bridge.o release/lib/
cp c_bridges/child-process-spawn.o release/lib/
cp c_bridges/trampoline-bridge.o release/lib/
Expand Down Expand Up @@ -330,6 +349,13 @@ jobs:
echo "/opt/homebrew/opt/llvm/bin" >> $GITHUB_PATH
echo "/opt/homebrew/opt/postgresql@16/bin" >> $GITHUB_PATH
echo "/usr/local/opt/llvm/bin" >> $GITHUB_PATH
# Rust toolchain for vendor build of librure (rust-lang/regex C ABI).
# Build-time dep only — end users get a prebuilt librure.a in the
# release tarball and never need rustc.
if ! command -v cargo >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
fi

- name: Start and provision postgres
run: |
Expand All @@ -356,7 +382,7 @@ jobs:
path: |
vendor/
c_bridges/*.o
key: vendor-${{ runner.os }}-${{ hashFiles('scripts/build-vendor.sh', 'scripts/vendor-pins.sh', 'c_bridges/*.c', 'c_bridges/*.cpp') }}
key: vendor-rure1-${{ runner.os }}-${{ hashFiles('scripts/build-vendor.sh', 'scripts/vendor-pins.sh', 'c_bridges/*.c', 'c_bridges/*.cpp') }}

- name: Build vendor libraries
run: bash scripts/build-vendor.sh
Expand All @@ -367,7 +393,7 @@ jobs:
- name: Verify vendor libraries
run: |
fail=0
for lib in vendor/bdwgc/libgc.a vendor/yyjson/libyyjson.a vendor/libuv/build/libuv.a vendor/picohttpparser/picohttpparser.o c_bridges/lws-bridge.o c_bridges/multipart-bridge.o c_bridges/regex-bridge.o c_bridges/child-process-bridge.o c_bridges/child-process-spawn.o c_bridges/trampoline-bridge.o c_bridges/os-bridge.o c_bridges/strlen-cache.o c_bridges/time-bridge.o c_bridges/base64-bridge.o c_bridges/url-bridge.o c_bridges/uri-bridge.o c_bridges/dotenv-bridge.o c_bridges/watch-bridge.o c_bridges/arena-bridge.o c_bridges/curl-bridge.o c_bridges/pg-bridge.o c_bridges/compress-bridge.o c_bridges/yaml-bridge.o c_bridges/string-ops-bridge.o c_bridges/llvm-bridge.o c_bridges/llvm-builder-bridge.o c_bridges/lld-bridge.o; do
for lib in vendor/bdwgc/libgc.a vendor/yyjson/libyyjson.a vendor/libuv/build/libuv.a vendor/picohttpparser/picohttpparser.o vendor/rure/librure.a c_bridges/lws-bridge.o c_bridges/multipart-bridge.o c_bridges/regex-bridge.o c_bridges/child-process-bridge.o c_bridges/child-process-spawn.o c_bridges/trampoline-bridge.o c_bridges/os-bridge.o c_bridges/strlen-cache.o c_bridges/time-bridge.o c_bridges/base64-bridge.o c_bridges/url-bridge.o c_bridges/uri-bridge.o c_bridges/dotenv-bridge.o c_bridges/watch-bridge.o c_bridges/arena-bridge.o c_bridges/curl-bridge.o c_bridges/pg-bridge.o c_bridges/compress-bridge.o c_bridges/yaml-bridge.o c_bridges/string-ops-bridge.o c_bridges/llvm-bridge.o c_bridges/llvm-builder-bridge.o c_bridges/lld-bridge.o; do
if [ ! -f "$lib" ]; then
echo "MISSING: $lib"
fail=1
Expand Down Expand Up @@ -435,6 +461,7 @@ jobs:
cp c_bridges/lws-bridge.o release/lib/
cp c_bridges/multipart-bridge.o release/lib/
cp c_bridges/regex-bridge.o release/lib/
cp vendor/rure/librure.a release/lib/
cp c_bridges/child-process-bridge.o release/lib/
cp c_bridges/child-process-spawn.o release/lib/
cp c_bridges/trampoline-bridge.o release/lib/
Expand Down Expand Up @@ -512,6 +539,7 @@ jobs:
lws-bridge.o \
multipart-bridge.o \
regex-bridge.o \
librure.a \
child-process-bridge.o \
child-process-spawn.o \
trampoline-bridge.o \
Expand Down
15 changes: 13 additions & 2 deletions .github/workflows/cross-compile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ jobs:
sudo apt-get install -y llvm clang lld cmake make gcc g++ \
autoconf automake libtool linux-headers-generic git \
libzstd-dev zlib1g-dev libsqlite3-dev libcurl4-openssl-dev file
# Rust for librure (rust-lang/regex C ABI). Build-time dep only.
# NOTE: this builds librure for the SDK's host arch (linux-x64).
# Cross-arch SDKs (e.g. linux-arm64) will need `rustup target add`
# — tracked as a follow-up; current cross-compile path is x64-only.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
echo "$HOME/.cargo/bin" >> $GITHUB_PATH

- name: Install npm dependencies
run: npm install
Expand All @@ -35,7 +41,7 @@ jobs:
path: |
vendor/
c_bridges/*.o
key: vendor-${{ runner.os }}-${{ hashFiles('scripts/build-vendor.sh', 'scripts/vendor-pins.sh', 'c_bridges/*.c') }}
key: vendor-rure1-${{ runner.os }}-${{ hashFiles('scripts/build-vendor.sh', 'scripts/vendor-pins.sh', 'c_bridges/*.c') }}

- name: Build vendor libraries
run: bash scripts/build-vendor.sh
Expand Down Expand Up @@ -81,6 +87,11 @@ jobs:
echo "/usr/local/opt/llvm/bin" >> $GITHUB_PATH
echo "/opt/homebrew/opt/lld/bin" >> $GITHUB_PATH
echo "/usr/local/opt/lld/bin" >> $GITHUB_PATH
# Rust for librure (rust-lang/regex C ABI). Build-time dep only.
if ! command -v cargo >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
fi

- name: Install npm dependencies
run: npm install
Expand All @@ -91,7 +102,7 @@ jobs:
path: |
vendor/
c_bridges/*.o
key: vendor-${{ runner.os }}-${{ hashFiles('scripts/build-vendor.sh', 'scripts/vendor-pins.sh', 'c_bridges/*.c') }}
key: vendor-rure1-${{ runner.os }}-${{ hashFiles('scripts/build-vendor.sh', 'scripts/vendor-pins.sh', 'c_bridges/*.c') }}

- name: Build vendor libraries
run: bash scripts/build-vendor.sh
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/update-benchmarks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,16 @@ jobs:
libzstd-dev zlib1g-dev libsqlite3-dev libcurl4-openssl-dev
sudo ln -sf /usr/bin/clang-21 /usr/bin/clang
sudo ln -sf /usr/bin/llvm-config-21 /usr/bin/llvm-config
# rustc/cargo for librure (rust-lang/regex C ABI) vendor build.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- run: npm install
- uses: actions/cache@v4
with:
path: |
vendor/
c_bridges/*.o
key: vendor-${{ runner.os }}-${{ hashFiles('scripts/build-vendor.sh', 'scripts/vendor-pins.sh', 'c_bridges/*.c', 'c_bridges/*.cpp') }}
key: vendor-rure1-${{ runner.os }}-${{ hashFiles('scripts/build-vendor.sh', 'scripts/vendor-pins.sh', 'c_bridges/*.c', 'c_bridges/*.cpp') }}
- run: bash scripts/build-vendor.sh
- run: npm run build
- name: Build tree-sitter TSX objects
Expand Down
14 changes: 13 additions & 1 deletion BUILDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ sudo dnf install cmake autoconf automake libtool nodejs npm
brew install cmake autoconf automake libtool node
```

**Rust toolchain** (only needed when (re)building vendor libraries):

ChadScript's regex engine is `librure` — the C ABI for Rust's `regex`
crate. Vendor builds compile it from source via cargo. End users
installing via the release tarball receive a prebuilt `librure.a` and
do **not** need Rust.

```bash
# Any OS (one-time, ~60s):
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
```

## Build

```bash
Expand All @@ -38,7 +50,7 @@ bash scripts/build-vendor.sh
npm run build
```

`scripts/build-vendor.sh` clones and builds static archives for libgc, cJSON, libuv, tree-sitter, and libwebsockets into `vendor/`. It's idempotent — re-running skips already-built libraries.
`scripts/build-vendor.sh` clones and builds static archives for libgc, cJSON, libuv, tree-sitter, libwebsockets, and `librure` (Rust regex C ABI) into `vendor/`. It's idempotent — re-running skips already-built libraries.

## Run

Expand Down
4 changes: 3 additions & 1 deletion benchmarks/assemble_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@
"stringops": {"name": "String Manipulation", "desc": "Concatenate 100K strings, split, toUpperCase, join.", "metric": "s", "lower_is_better": True},
"fileio": {"name": "File I/O", "desc": "Write and read ~100MB to /tmp.", "metric": "s", "lower_is_better": True},
"binarytrees": {"name": "Binary Trees", "desc": "Build/check/discard binary trees of depth 18.", "metric": "s", "lower_is_better": True},
"json": {"name": "JSON Parse/Stringify", "desc": "Parse 10K JSON objects, stringify back.", "metric": "s", "lower_is_better": True},
"json": {"name": "JSON Parse/Stringify", "desc": "Parse 100K JSON objects, stringify back.", "metric": "s", "lower_is_better": True},
"stringsearch": {"name": "String Search", "desc": "Recursive directory search for 'console.log' in src/.", "metric": "s", "lower_is_better": True},
"regex_match": {"name": "Regex Match", "desc": "100K matches of an anchored pattern with one capture group.", "metric": "s", "lower_is_better": True},
"map_lookup": {"name": "Hash Map Lookup", "desc": "100K-entry Map<string,number>, 1M random .get() lookups.", "metric": "s", "lower_is_better": True},
"cligrep": {"name": "Recursive Grep", "desc": "cgrep vs grep — search for 'function' across 5x copies of src/.", "metric": "s", "lower_is_better": True, "category": "cli"},
"clihex": {"name": "Hex Dump", "desc": "chex vs xxd — hex dump a 5MB binary file.", "metric": "s", "lower_is_better": True, "category": "cli"},
}
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/json/bench.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#include <time.h>
#include "yyjson.h"

#define COUNT 10000
#define COUNT 100000

typedef struct {
int id;
Expand Down
6 changes: 5 additions & 1 deletion benchmarks/json/chadscript.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
const COUNT = 10000;
// Parse 100k JSON objects (~10MB total throughput), then stringify them
// all back. Exercises the parser/serializer hot paths at a scale where
// SIMD-friendly engines (yyjson, V8) actually pull away from naive ones.

const COUNT = 100000;

interface Item {
id: number;
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/json/json_bench.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"time"
)

const COUNT = 10000
const COUNT = 100000

type Item struct {
ID int `json:"id"`
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/json/node.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const COUNT = 10000;
const COUNT = 100000;

const jsonStrings = [];
for (let i = 0; i < COUNT; i++) {
Expand Down
72 changes: 72 additions & 0 deletions benchmarks/map_lookup/bench.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// C reference: small open-addressing hash map (FNV-1a, linear probe).
// String keys, int values. Same N/Q as the other implementations.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#define N 100000
#define Q 1000000
#define CAP (N * 2)

typedef struct {
char *key;
int val;
int used;
} Slot;

static Slot table[CAP];

static unsigned long fnv1a(const char *s) {
unsigned long h = 1469598103934665603UL;
while (*s) {
h ^= (unsigned char)*s++;
h *= 1099511628211UL;
}
return h;
}

static void map_set(const char *k, int v) {
unsigned long h = fnv1a(k) % CAP;
while (table[h].used) {
if (strcmp(table[h].key, k) == 0) { table[h].val = v; return; }
h = (h + 1) % CAP;
}
table[h].key = strdup(k);
table[h].val = v;
table[h].used = 1;
}

static int map_get(const char *k, int *found) {
unsigned long h = fnv1a(k) % CAP;
while (table[h].used) {
if (strcmp(table[h].key, k) == 0) { *found = 1; return table[h].val; }
h = (h + 1) % CAP;
}
*found = 0;
return 0;
}

int main(void) {
char buf[32];
for (int i = 0; i < N; i++) {
snprintf(buf, sizeof(buf), "key%d", i);
map_set(buf, i);
}

struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
long long sum = 0;
for (int q = 0; q < Q; q++) {
snprintf(buf, sizeof(buf), "key%d", q % N);
int found;
int v = map_get(buf, &found);
if (found) sum += v;
}
clock_gettime(CLOCK_MONOTONIC, &t1);
double elapsed = (t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) / 1e9;

printf("Sum: %lld\n", sum);
printf("Time: %.6fs\n", elapsed);
return 0;
}
22 changes: 22 additions & 0 deletions benchmarks/map_lookup/chadscript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const N = 100000;
const Q = 1000000;

const m = new Map<string, number>();
let i = 0;
while (i < N) {
m.set("key" + i, i);
i = i + 1;
}

const start = Date.now();
let sum = 0;
let q = 0;
while (q < Q) {
const v = m.get("key" + (q % N));
if (v !== undefined) sum = sum + v;
q = q + 1;
}
const elapsed = (Date.now() - start) / 1000;

console.log("Sum: " + sum);
console.log("Time: " + elapsed + "s");
30 changes: 30 additions & 0 deletions benchmarks/map_lookup/map_lookup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package main

import (
"fmt"
"strconv"
"time"
)

const N = 100000
const Q = 1000000

func main() {
m := make(map[string]int, N)
for i := 0; i < N; i++ {
m["key"+strconv.Itoa(i)] = i
}

start := time.Now()
sum := 0
for q := 0; q < Q; q++ {
v, ok := m["key"+strconv.Itoa(q%N)]
if ok {
sum += v
}
}
elapsed := time.Since(start).Seconds()

fmt.Printf("Sum: %d\n", sum)
fmt.Printf("Time: %.6fs\n", elapsed)
}
Loading
Loading