initial commit

This commit is contained in:
i2p
2026-08-27 21:09:14 +00:00
commit a5b6d59437
12681 changed files with 3253832 additions and 0 deletions
+193
View File
@@ -0,0 +1,193 @@
// print how long each step took
#define VERBOSE
//
// This loads up a JavaScriptCore global object which only has a "write"
// global function and then calls eval
//
//
// Usage:
// ./cold-jsc-start <file>
// ./cold-jsc-start -e "write('hey')"
//
#include "root.h"
#include <wtf/FileSystem.h>
#include <JavaScriptCore/JSGlobalObject.h>
#include <JavaScriptCore/JSArrayBufferView.h>
#include <JavaScriptCore/JSArrayBufferViewInlines.h>
#include <JavaScriptCore/Completion.h>
#include <JavaScriptCore/InitializeThreading.h>
#include <unistd.h>
#include <wtf/Stopwatch.h>
using namespace JSC;
JSC_DEFINE_HOST_FUNCTION(jsFunctionWrite, (JSC::JSGlobalObject * globalObject,
JSC::CallFrame *callframe)) {
if (callframe->argumentCount() < 1)
return JSValue::encode(jsUndefined());
JSValue arg1 = callframe->argument(0);
JSValue toWriteArg = callframe->argument(1);
auto &vm = globalObject->vm();
auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
int32_t fd = STDOUT_FILENO;
if (callframe->argumentCount() > 1) {
fd = arg1.toInt32(globalObject);
RETURN_IF_EXCEPTION(scope, {});
} else {
toWriteArg = arg1;
}
if (auto *buffer = jsDynamicCast<JSC::JSArrayBufferView *>(toWriteArg)) {
auto *data = buffer->vector();
auto length = buffer->byteLength();
auto written = write(fd, data, length);
return JSValue::encode(jsNumber(written));
}
auto string = toWriteArg.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
auto utf8 = string.utf8();
auto length = utf8.length();
auto written = write(fd, utf8.data(), length);
return JSValue::encode(jsNumber(written));
}
int main(int argc, char **argv) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <file>\n", argv[0]);
return 1;
}
#ifdef VERBOSE
auto stopwatch = Stopwatch::create();
stopwatch->start();
#endif
{
WTF::initializeMainThread();
JSC::initialize();
{
JSC::Options::AllowUnfinalizedAccessScope scope;
JSC::Options::useConcurrentJIT() = true;
// JSC::Options::useSigillCrashAnalyzer() = true;
JSC::Options::useWebAssembly() = true;
JSC::Options::useSourceProviderCache() = true;
// JSC::Options::useUnlinkedCodeBlockJettisoning() = false;
JSC::Options::exposeInternalModuleLoader() = true;
JSC::Options::useSharedArrayBuffer() = true;
JSC::Options::useJIT() = true;
JSC::Options::useBBQJIT() = true;
JSC::Options::useJITCage() = false;
JSC::Options::useShadowRealm() = true;
JSC::Options::useResizableArrayBuffer() = true;
JSC::Options::showPrivateScriptsInStackTraces() = true;
JSC::Options::useSetMethods() = true;
JSC::Options::assertOptionsAreCoherent();
}
}
#ifdef VERBOSE
fprintf(stderr, "JSC::Initialize took %f ms\n",
stopwatch->elapsedTime().milliseconds());
stopwatch->reset();
stopwatch->start();
#endif
auto &vm = JSC::VM::create(JSC::HeapType::Large).leakRef();
vm.heap.acquireAccess();
#ifdef VERBOSE
fprintf(stderr, "JSC::VM::create took %f ms\n",
stopwatch->elapsedTime().milliseconds());
stopwatch->reset();
stopwatch->start();
#endif
JSC::JSLockHolder locker(vm);
auto *globalObject = JSC::JSGlobalObject::create(
vm, JSC::JSGlobalObject::createStructure(vm, JSC::jsNull()));
#ifdef VERBOSE
fprintf(stderr, "JSC::JSGlobalObject::create took %f ms\n",
stopwatch->elapsedTime().milliseconds());
stopwatch->reset();
stopwatch->start();
#endif
JSC::gcProtect(globalObject);
globalObject->putDirectNativeFunction(
vm, globalObject,
PropertyName(JSC::Identifier::fromString(vm, "write"_s)), 0,
jsFunctionWrite, ImplementationVisibility::Public, JSC::NoIntrinsic,
JSC::PropertyAttribute::ReadOnly | 0);
vm.ref();
if (argc > 2) {
auto source =
JSC::makeSource(WTF::String::fromUTF8(argv[argc - 1]),
SourceOrigin(WTF::URL("file://eval.js"_s)),
JSC::SourceTaintedOrigin::Untainted, "eval.js"_s);
NakedPtr<Exception> evaluationException;
JSValue returnValue =
JSC::profiledEvaluate(globalObject, ProfilingReason::API, source,
globalObject, evaluationException);
#ifdef VERBOSE
fprintf(stderr, "\neval took %f ms\n",
stopwatch->elapsedTime().milliseconds());
stopwatch->reset();
#endif
if (evaluationException) {
fprintf(
stderr, "Exception: %s\n",
evaluationException->value().toWTFString(globalObject).utf8().data());
return 1;
} else {
return 0;
}
}
WTF::String fileURLString = WTF::String::fromUTF8(argv[argc - 1]);
if (auto contents = WTF::FileSystemImpl::readEntireFile(fileURLString)) {
auto source =
JSC::makeSource(WTF::String::fromUTF8(contents.value()),
SourceOrigin(WTF::URL(fileURLString)),
JSC::SourceTaintedOrigin::Untainted, fileURLString);
NakedPtr<Exception> evaluationException;
JSValue returnValue =
JSC::profiledEvaluate(globalObject, ProfilingReason::API, source,
globalObject, evaluationException);
#ifdef VERBOSE
fprintf(stderr, "eval took %f ms\n",
stopwatch->elapsedTime().milliseconds());
stopwatch->reset();
#endif
if (evaluationException) {
fprintf(
stderr, "Exception: %s\n",
evaluationException->value().toWTFString(globalObject).utf8().data());
return 1;
} else {
return 0;
}
} else {
fprintf(stderr, "Could not read file %s\n", argv[argc - 1]);
return 1;
}
}
+108
View File
@@ -0,0 +1,108 @@
import { Generator, Context } from "./unicode-generator";
// Create sets for fast lookups
const idStartES5Set = new Set([
...require("@unicode/unicode-3.0.0/General_Category/Uppercase_Letter/code-points"),
...require("@unicode/unicode-3.0.0/General_Category/Lowercase_Letter/code-points"),
...require("@unicode/unicode-3.0.0/General_Category/Titlecase_Letter/code-points"),
...require("@unicode/unicode-3.0.0/General_Category/Modifier_Letter/code-points"),
...require("@unicode/unicode-3.0.0/General_Category/Other_Letter/code-points"),
]);
const idContinueES5Set = new Set([
...idStartES5Set,
...require("@unicode/unicode-3.0.0/General_Category/Nonspacing_Mark/code-points"),
...require("@unicode/unicode-3.0.0/General_Category/Spacing_Mark/code-points"),
...require("@unicode/unicode-3.0.0/General_Category/Decimal_Number/code-points"),
...require("@unicode/unicode-3.0.0/General_Category/Connector_Punctuation/code-points"),
]);
const idStartESNextSet = new Set(require("@unicode/unicode-15.1.0/Binary_Property/ID_Start/code-points"));
const idContinueESNextSet = new Set(require("@unicode/unicode-15.1.0/Binary_Property/ID_Continue/code-points"));
// Exclude known problematic codepoints
const ID_Continue_mistake = new Set([0x30fb, 0xff65]);
function bitsToU64Array(bits: number[]): bigint[] {
const result: bigint[] = [];
for (let i = 0; i < bits.length; i += 64) {
let value = 0n;
for (let j = 0; j < 64 && i + j < bits.length; j++) {
if (bits[i + j]) {
value |= 1n << BigInt(j);
}
}
result.push(value);
}
return result;
}
async function generateTable(table: string, name: string, checkFn: (cp: number) => boolean) {
const context: Context<boolean> = {
get: (cp: number) => checkFn(cp),
eql: (a: boolean, b: boolean) => a === b,
};
const generator = new Generator(context);
const tables = await generator.generate();
return `
pub fn ${name}(cp: u21) bool {
if (cp > 0x10FFFF) return false;
const high = cp >> 8;
const low = cp & 0xFF;
const stage2_idx = ${table}.stage1[high];
const bit_pos = stage2_idx + low;
const u64_idx = bit_pos >> 6;
const bit_idx = @as(u6, @intCast(bit_pos & 63));
return (${table}.stage2[u64_idx] & (@as(u64, 1) << bit_idx)) != 0;
}
const ${table} = struct {
pub const stage1 = [_]u16{${tables.stage1.join(",")}};
pub const stage2 = [_]u64{${bitsToU64Array(tables.stage2)
.map(n => n.toString())
.join(",")}};
};
`;
}
async function main() {
const functions = [
{
name: "isIDStartES5",
table: "idStartES5",
check: (cp: number) => idStartES5Set.has(cp),
},
{
name: "isIDContinueES5",
table: "idContinueES5",
check: (cp: number) => idContinueES5Set.has(cp),
},
{
name: "isIDStartESNext",
table: "idStartESNext",
check: (cp: number) => idStartESNextSet.has(cp),
},
{
name: "isIDContinueESNext",
table: "idContinueESNext",
check: (cp: number) => idContinueESNextSet.has(cp) && !ID_Continue_mistake.has(cp),
},
];
const results = await Promise.all(
functions.map(async ({ name, check, table }) => {
const code = await generateTable(table, name, check);
return `
/// ${name} checks if a codepoint is valid in the ${name} category
${code}`;
}),
);
console.log(`/// This file is auto-generated. Do not edit.
${results.join("\n\n")}`);
}
main();
+178
View File
@@ -0,0 +1,178 @@
import * as fs from "fs";
import path from "path";
interface LetterGroup {
offset: number;
length: number;
packages: string[];
}
const INPUT = path.join(__dirname, "..", "src", "runtime", "cli", "add_completions.txt");
const OUTPUT = path.join(__dirname, "..", "src", "runtime", "cli", "add_completions.rs");
// Read and parse input file
const content = fs.readFileSync(INPUT, "utf8");
const packages = content
.split("\n")
.map(line => line.trim())
.filter(line => line.length > 0)
.sort();
// Group packages by first letter
const letterGroups = new Map<string, LetterGroup>();
let currentOffset = 0;
let maxListSize = 0;
for (const pkg of packages) {
if (pkg.length === 0) continue;
const firstLetter = pkg[0].toLowerCase();
if (!letterGroups.has(firstLetter)) {
letterGroups.set(firstLetter, {
offset: currentOffset,
length: 0,
packages: [],
});
}
const group = letterGroups.get(firstLetter)!;
group.packages.push(pkg);
group.length++;
maxListSize = Math.max(maxListSize, group.length);
}
// Create a single buffer with all package data
const dataChunks: Buffer[] = [];
let totalUncompressed = 0;
// Store total package count first
const totalCountBuf = Buffer.alloc(4);
totalCountBuf.writeUInt32LE(packages.length, 0);
dataChunks.push(totalCountBuf);
totalUncompressed += 4;
// Then all packages with length prefixes
for (const pkg of packages) {
const lenBuf = Buffer.alloc(2);
lenBuf.writeUInt16LE(pkg.length, 0);
dataChunks.push(lenBuf);
dataChunks.push(Buffer.from(pkg, "utf8"));
totalUncompressed += 2 + pkg.length;
}
const uncompressedData = Buffer.concat(dataChunks);
// Compress with zstd (level 1, matching the old `zstd -1` CLI invocation)
const compressedData = Bun.zstdCompressSync(uncompressedData, { level: 1 });
// Calculate compression ratio
const totalCompressed = compressedData.length;
const ratio = ((totalCompressed / totalUncompressed) * 100).toFixed(1);
console.log("\nCompression statistics:");
console.log(`Uncompressed size: ${totalUncompressed} bytes`);
console.log(`Compressed size: ${totalCompressed} bytes`);
console.log(`Compression ratio: ${ratio}%`);
// Generate index entries
const letters = "abcdefghijklmnopqrstuvwxyz";
const indexEntries: string[] = [];
let offset = 0;
for (const letter of letters) {
const group = letterGroups.get(letter);
const len = group?.length ?? 0;
indexEntries.push(` (${offset}, ${len}), // ${letter}`);
offset += len;
}
// Split the compressed blob into 16-byte rows for readability
const blobLines: string[] = [];
for (let i = 0; i < compressedData.length; i += 16) {
const row = [...compressedData.slice(i, i + 16)].join(", ");
blobLines.push(` ${row},`);
}
const out = `// Auto-generated by misctools/generate-add-completions.ts. Do not edit.
//
// Regenerate after editing src/runtime/cli/add_completions.txt:
//
// bun misctools/generate-add-completions.ts
//
// Compressing the completions list saves about 100 KB of binary size.
use bun_core::Once;
#[repr(u8)]
#[derive(Copy, Clone, Eq, PartialEq)]
pub(crate) enum FirstLetter {
${[...letters].map(l => ` ${l.toUpperCase()} = b'${l}',`).join("\n")}
}
/// Largest per-letter package list length.
pub(crate) const BIGGEST_LIST: usize = ${maxListSize};
const UNCOMPRESSED_SIZE: usize = ${totalUncompressed};
/// (offset, length) into the decompressed package list for each letter a-z.
const INDEX: [(usize, usize); 26] = [
${indexEntries.join("\n")}
];
static COMPRESSED_DATA: &[u8] = &[
${blobLines.join("\n")}
];
struct Table {
_decompressed: Vec<u8>,
packages: Vec<&'static [u8]>,
}
static TABLE: Once<Table> = <Once<Table>>::new();
fn build_table() -> Table {
let decompressed = bun_zstd::decompress_alloc(COMPRESSED_DATA)
.expect("add_completions: zstd decompress failed");
debug_assert_eq!(decompressed.len(), UNCOMPRESSED_SIZE);
// SAFETY: the decompressed Vec is moved into TABLE (a process-lifetime
// Once) and never dropped or reallocated, so its storage is valid for
// 'static. The package slices below borrow this storage.
let data: &'static [u8] =
unsafe { core::slice::from_raw_parts(decompressed.as_ptr(), decompressed.len()) };
let total = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
let mut packages: Vec<&'static [u8]> = Vec::with_capacity(total);
let mut pos: usize = 4;
for _ in 0..total {
let len = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;
pos += 2;
packages.push(&data[pos..pos + len]);
pos += len;
}
Table { _decompressed: decompressed, packages }
}
/// Decompress the package-name table. Idempotent.
pub fn init() {
TABLE.get_or_init(build_table);
}
/// Returns the slice of package names beginning with \`letter\`.
pub(crate) fn get_packages(letter: FirstLetter) -> &'static [&'static [u8]] {
let table = TABLE.get_or_init(build_table);
let (offset, length) = INDEX[(letter as u8 - b'a') as usize];
if length == 0 {
return &[];
}
&table.packages[offset..offset + length]
}
`;
fs.writeFileSync(OUTPUT, out);
try {
const { execSync } = await import("child_process");
execSync(`rustfmt "${OUTPUT}"`, { stdio: "inherit" });
} catch {
// rustfmt not available; committed output is already formatted reasonably
}
console.log(`\nGenerated ${OUTPUT} for ${packages.length} packages`);
+728
View File
@@ -0,0 +1,728 @@
#!/usr/bin/env bun
/**
* CLI Flag Parser for Bun Commands
*
* This script reads the --help menu for every Bun command and generates JSON
* containing all flag information, descriptions, and whether they support
* positional or non-positional arguments.
*
* Handles complex cases like:
* - Nested subcommands (bun pm cache rm)
* - Command aliases (bun i = bun install, bun a = bun add)
* - Dynamic completions (scripts, packages, files)
* - Context-aware flags
* - Special cases like bare 'bun' vs 'bun run'
*
* Output is saved to completions/bun-cli.json for use in generating
* shell completions (fish, bash, zsh).
*/
import { spawn } from "bun";
import { mkdirSync, writeFileSync, mkdtempSync, rmSync } from "fs";
import { join } from "path";
interface FlagInfo {
name: string;
shortName?: string;
description: string;
hasValue: boolean;
valueType?: string;
defaultValue?: string;
choices?: string[];
required?: boolean;
multiple?: boolean;
}
interface SubcommandInfo {
name: string;
description: string;
flags?: FlagInfo[];
subcommands?: Record<string, SubcommandInfo>;
positionalArgs?: {
name: string;
description?: string;
required: boolean;
multiple: boolean;
type?: string;
completionType?: string;
}[];
examples?: string[];
}
interface CommandInfo {
name: string;
aliases?: string[];
description: string;
usage?: string;
flags: FlagInfo[];
positionalArgs: {
name: string;
description?: string;
required: boolean;
multiple: boolean;
type?: string;
completionType?: string;
}[];
examples: string[];
subcommands?: Record<string, SubcommandInfo>;
documentationUrl?: string;
dynamicCompletions?: {
scripts?: boolean;
packages?: boolean;
files?: boolean;
binaries?: boolean;
};
}
interface CompletionData {
version: string;
commands: Record<string, CommandInfo>;
globalFlags: FlagInfo[];
specialHandling: {
bareCommand: {
description: string;
canRunFiles: boolean;
dynamicCompletions: {
scripts: boolean;
files: boolean;
binaries: boolean;
};
};
};
bunGetCompletes: {
available: boolean;
commands: {
scripts: string; // "bun getcompletes s" or "bun getcompletes z"
binaries: string; // "bun getcompletes b"
packages: string; // "bun getcompletes a <prefix>"
files: string; // "bun getcompletes j"
};
};
}
const BUN_EXECUTABLE = process.env.BUN_DEBUG_BUILD || "bun";
/**
* Parse flag line from help output
*/
function parseFlag(line: string): FlagInfo | null {
// Match patterns like:
// -h, --help Display this menu and exit
// --timeout=<val> Set the per-test timeout in milliseconds, default is 5000.
// -r, --preload=<val> Import a module before other modules are loaded
// --watch Automatically restart the process on file change
const patterns = [
// Long flag with short flag and value: -r, --preload=<val>
/^\s*(-[a-zA-Z]),\s+(--[a-zA-Z-]+)=(<[^>]+>)\s+(.+)$/,
// Long flag with short flag: -h, --help
/^\s*(-[a-zA-Z]),\s+(--[a-zA-Z-]+)\s+(.+)$/,
// Long flag with value: --timeout=<val>
/^\s+(--[a-zA-Z-]+)=(<[^>]+>)\s+(.+)$/,
// Long flag without value: --watch
/^\s+(--[a-zA-Z-]+)\s+(.+)$/,
// Short flag only: -i
/^\s+(-[a-zA-Z])\s+(.+)$/,
];
for (const pattern of patterns) {
const match = line.match(pattern);
if (match) {
let shortName: string | undefined;
let longName: string;
let valueSpec: string | undefined;
let description: string;
if (match.length === 5) {
// Pattern with short flag, long flag, and value
[, shortName, longName, valueSpec, description] = match;
} else if (match.length === 4) {
if (match[1].startsWith("-") && match[1].length === 2) {
// Short flag with long flag
[, shortName, longName, description] = match;
} else if (match[2].startsWith("<")) {
// Long flag with value
[, longName, valueSpec, description] = match;
} else {
// Long flag without value
[, longName, description] = match;
}
} else if (match.length === 3) {
if (match[1].length === 2) {
// Short flag only
[, shortName, description] = match;
longName = shortName.replace("-", "--");
} else {
// Long flag without value
[, longName, description] = match;
}
} else {
continue;
}
// The help printer drops escaped <placeholder> tags, leaving doubled spaces behind
description = description.replace(/\s{2,}/g, " ").trim();
// Extract additional info from description
const hasValue = !!valueSpec;
let valueType: string | undefined;
let defaultValue: string | undefined;
let choices: string[] | undefined;
if (valueSpec) {
valueType = valueSpec.replace(/[<>]/g, "");
}
// Look for default values in description
const defaultMatch = description.match(/[Dd]efault(?:s?)\s*(?:is|to|:)\s*"?([^".\s,]+)"?/);
if (defaultMatch) {
defaultValue = defaultMatch[1];
}
// Look for choices/enums
const choicesMatch = description.match(/(?:One of|Valid (?:orders?|values?|options?)):?\s*"?([^"]+)"?/);
if (choicesMatch) {
choices = choicesMatch[1]
.split(/[,\s]+/)
.map(s => s.replace(/[",]/g, "").trim())
.filter(Boolean);
}
return {
name: longName.replace(/^--/, ""),
shortName: shortName?.replace(/^-/, ""),
description,
hasValue,
valueType,
defaultValue,
choices,
required: false, // We'll determine this from usage patterns
multiple: description.toLowerCase().includes("multiple") || description.includes("[]"),
};
}
}
return null;
}
/**
* Parse usage line to extract positional arguments
*/
function parseUsage(usage: string): {
name: string;
description?: string;
required: boolean;
multiple: boolean;
type?: string;
completionType?: string;
}[] {
const args: {
name: string;
description?: string;
required: boolean;
multiple: boolean;
type?: string;
completionType?: string;
}[] = [];
// Extract parts after command name
const parts = usage.split(/\s+/).slice(2); // Skip "Usage:" and command name
for (const part of parts) {
if (part.startsWith("[") || part.startsWith("<") || part.includes("...")) {
let name = part;
let required = false;
let multiple = false;
let completionType: string | undefined;
// Clean up the argument name
name = name.replace(/[\[\]<>]/g, "");
if (part.startsWith("<")) {
required = true;
}
if (part.includes("...") || name.includes("...")) {
multiple = true;
name = name.replace(/\.{3}/g, "");
}
// Skip flags
if (!name.startsWith("-") && name.length > 0) {
// Determine completion type based on argument name
if (name.toLowerCase().includes("package")) {
completionType = "package";
} else if (name.toLowerCase().includes("script")) {
completionType = "script";
} else if (name.toLowerCase().includes("file") || name.includes(".")) {
completionType = "file";
}
args.push({
name,
required,
multiple,
type: "string", // Default type
completionType,
});
}
}
}
return args;
}
const temppackagejson = mkdtempSync("package");
writeFileSync(
join(temppackagejson, "package.json"),
JSON.stringify({
name: "test",
version: "1.0.0",
scripts: {},
}),
);
process.once("beforeExit", () => {
rmSync(temppackagejson, { recursive: true });
});
/**
* Execute bun command and get help output
*/
async function getHelpOutput(command: string[]): Promise<string> {
try {
const proc = spawn({
cmd: [BUN_EXECUTABLE, ...command, "--help"],
stdout: "pipe",
stderr: "pipe",
cwd: temppackagejson,
});
const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
await proc.exited;
return stdout || stderr || "";
} catch (error) {
console.error(`Failed to get help for command: ${command.join(" ")}`, error);
return "";
}
}
/**
* Parse PM subcommands from help output
*/
function parsePmSubcommands(helpText: string): Record<string, SubcommandInfo> {
const lines = helpText.split("\n");
const subcommands: Record<string, SubcommandInfo> = {};
let inCommands = false;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed === "Commands:") {
inCommands = true;
continue;
}
if (inCommands && trimmed.startsWith("Learn more")) {
break;
}
if (inCommands && line.match(/^\s+bun pm \w+/)) {
// Parse lines like: "bun pm pack create a tarball of the current workspace"
const match = line.match(/^\s+bun pm (\S+)(?:\s+(.+))?$/);
if (match) {
const [, name, description = ""] = match;
subcommands[name] = {
name,
description: description.trim(),
flags: [],
positionalArgs: [],
};
// Special handling for subcommands with their own subcommands
if (name === "cache") {
subcommands[name].subcommands = {
rm: {
name: "rm",
description: "clear the cache",
},
};
} else if (name === "pkg") {
subcommands[name].subcommands = {
get: { name: "get", description: "get values from package.json" },
set: { name: "set", description: "set values in package.json" },
delete: { name: "delete", description: "delete keys from package.json" },
fix: { name: "fix", description: "auto-correct common package.json errors" },
};
}
}
}
}
return subcommands;
}
/**
* Parse help output into CommandInfo
*/
function parseHelpOutput(helpText: string, commandName: string): CommandInfo {
const lines = helpText.split("\n");
const command: CommandInfo = {
name: commandName,
description: "",
flags: [],
positionalArgs: [],
examples: [],
};
let currentSection = "";
let inFlags = false;
let inExamples = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmed = line.trim();
// Extract command description (usually the first non-usage line)
if (
!command.description &&
trimmed &&
!trimmed.startsWith("Usage:") &&
!trimmed.startsWith("Alias:") &&
currentSection === ""
) {
command.description = trimmed;
continue;
}
// Extract aliases
if (trimmed.startsWith("Alias:")) {
const aliasMatch = trimmed.match(/Alias:\s*(.+)/);
if (aliasMatch) {
command.aliases = aliasMatch[1]
.split(/[,\s]+/)
.map(a => a.trim())
.filter(Boolean);
}
continue;
}
// Extract usage and positional args
if (trimmed.startsWith("Usage:")) {
command.usage = trimmed;
command.positionalArgs = parseUsage(trimmed);
continue;
}
// Track sections
if (trimmed === "Flags:") {
inFlags = true;
currentSection = "flags";
continue;
} else if (trimmed === "Examples:") {
inExamples = true;
inFlags = false;
currentSection = "examples";
continue;
} else if (
trimmed.startsWith("Full documentation") ||
trimmed.startsWith("Learn more") ||
trimmed.startsWith("A full list")
) {
const urlMatch = trimmed.match(/https?:\/\/[^\s]+/);
if (urlMatch) {
command.documentationUrl = urlMatch[0];
}
inFlags = false;
inExamples = false;
continue;
}
// Parse flags
if (inFlags && line.match(/^\s+(-|\s+--)/)) {
const flag = parseFlag(line);
if (flag) {
command.flags.push(flag);
}
}
// Parse examples
if (inExamples && trimmed && !trimmed.startsWith("Full documentation")) {
if (trimmed.startsWith("bun ") || trimmed.startsWith("./") || trimmed.startsWith("Bundle")) {
command.examples.push(trimmed);
}
}
}
// Special case for pm command
if (commandName === "pm") {
command.subcommands = parsePmSubcommands(helpText);
}
// Add dynamic completion info based on command
command.dynamicCompletions = {};
if (commandName === "run") {
command.dynamicCompletions.scripts = true;
command.dynamicCompletions.files = true;
command.dynamicCompletions.binaries = true;
// Also add file type info for positional args
for (const arg of command.positionalArgs) {
if (arg.name.includes("file") || arg.name.includes("script")) {
arg.completionType = "javascript_files";
}
}
} else if (commandName === "add") {
command.dynamicCompletions.packages = true;
// Mark package args
for (const arg of command.positionalArgs) {
if (arg.name.includes("package") || arg.name === "name") {
arg.completionType = "package";
}
}
} else if (commandName === "remove") {
command.dynamicCompletions.packages = true; // installed packages
for (const arg of command.positionalArgs) {
if (arg.name.includes("package") || arg.name === "name") {
arg.completionType = "installed_package";
}
}
} else if (["test"].includes(commandName)) {
command.dynamicCompletions.files = true;
for (const arg of command.positionalArgs) {
if (arg.name.includes("pattern") || arg.name.includes("file")) {
arg.completionType = "test_files";
}
}
} else if (["build"].includes(commandName)) {
command.dynamicCompletions.files = true;
for (const arg of command.positionalArgs) {
if (arg.name === "entrypoint" || arg.name.includes("file")) {
arg.completionType = "javascript_files";
}
}
} else if (commandName === "create") {
// Create has special template completions
for (const arg of command.positionalArgs) {
if (arg.name.includes("template")) {
arg.completionType = "create_template";
}
}
}
return command;
}
/**
* Get list of main commands from bun --help
*/
async function getMainCommands(): Promise<string[]> {
const helpText = await getHelpOutput([]);
const lines = helpText.split("\n");
const commands: string[] = [];
let inCommands = false;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed === "Commands:") {
inCommands = true;
continue;
}
// Stop when we hit the "Flags:" section
if (inCommands && trimmed === "Flags:") {
break;
}
if (inCommands && line.match(/^\s+\w+/)) {
// Extract command name (first word after whitespace)
const match = line.match(/^\s+(\w+)/);
if (match) {
commands.push(match[1]);
}
}
}
const commandsToRemove = ["lint"];
return commands.filter(a => {
if (commandsToRemove.includes(a)) {
return false;
}
return true;
});
}
/**
* Extract global flags from main help
*/
function parseGlobalFlags(helpText: string): FlagInfo[] {
const lines = helpText.split("\n");
const flags: FlagInfo[] = [];
let inFlags = false;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed === "Flags:") {
inFlags = true;
continue;
}
if (inFlags && (trimmed === "" || trimmed.startsWith("("))) {
break;
}
if (inFlags && line.match(/^\s+(-|\s+--)/)) {
const flag = parseFlag(line);
if (flag) {
flags.push(flag);
}
}
}
return flags;
}
/**
* Add command aliases based on common patterns
*/
function addCommandAliases(commands: Record<string, CommandInfo>): void {
const aliasMap: Record<string, string[]> = {
"install": ["i"],
"update": ["up"],
"add": ["a"],
"remove": ["rm"],
"create": ["c"],
"x": ["bunx"], // bunx is an alias for bun x
};
for (const [command, aliases] of Object.entries(aliasMap)) {
if (commands[command]) {
commands[command].aliases = aliases;
}
}
}
/**
* Main function to generate completion data
*/
async function generateCompletions(): Promise<void> {
console.log("🔍 Discovering Bun commands...");
// Get main help and extract commands
const mainHelpText = await getHelpOutput([]);
const mainCommands = await getMainCommands();
const globalFlags = parseGlobalFlags(mainHelpText);
console.log(`📋 Found ${mainCommands.length} main commands: ${mainCommands.join(", ")}`);
const completionData: CompletionData = {
version: "1.1.0",
commands: {},
globalFlags,
specialHandling: {
bareCommand: {
description: "Run JavaScript/TypeScript files directly or access package scripts and binaries",
canRunFiles: true,
dynamicCompletions: {
scripts: true,
files: true,
binaries: true,
},
},
},
bunGetCompletes: {
available: true,
commands: {
scripts: "bun getcompletes s", // or "bun getcompletes z" for scripts with descriptions
binaries: "bun getcompletes b",
packages: "bun getcompletes a", // takes prefix as argument
files: "bun getcompletes j", // JavaScript/TypeScript files
},
},
};
// Parse each command
for (const commandName of mainCommands) {
console.log(`📖 Parsing help for: ${commandName}`);
try {
const helpText = await getHelpOutput([commandName]);
if (helpText.trim()) {
const commandInfo = parseHelpOutput(helpText, commandName);
completionData.commands[commandName] = commandInfo;
}
} catch (error) {
console.error(`❌ Failed to parse ${commandName}:`, error);
}
}
// Add common aliases
addCommandAliases(completionData.commands);
// Also check some common subcommands that might have their own help
const additionalCommands = ["pm"];
for (const commandName of additionalCommands) {
if (!completionData.commands[commandName]) {
console.log(`📖 Parsing help for additional command: ${commandName}`);
try {
const helpText = await getHelpOutput([commandName]);
if (helpText.trim() && !helpText.includes("error:") && !helpText.includes("Error:")) {
const commandInfo = parseHelpOutput(helpText, commandName);
completionData.commands[commandName] = commandInfo;
}
} catch (error) {
console.error(`❌ Failed to parse ${commandName}:`, error);
}
}
}
// Ensure completions directory exists
const completionsDir = join(process.cwd(), "completions");
try {
mkdirSync(completionsDir, { recursive: true });
} catch (error) {
// Directory might already exist
}
// Write the JSON file
const outputPath = join(completionsDir, "bun-cli.json");
const jsonData = JSON.stringify(completionData, null, 2);
writeFileSync(outputPath, jsonData, "utf8");
console.log(`✅ Generated CLI completion data at: ${outputPath}`);
console.log(`📊 Statistics:`);
console.log(` - Commands: ${Object.keys(completionData.commands).length}`);
console.log(` - Global flags: ${completionData.globalFlags.length}`);
let totalFlags = 0;
let totalExamples = 0;
let totalSubcommands = 0;
for (const [name, cmd] of Object.entries(completionData.commands)) {
totalFlags += cmd.flags.length;
totalExamples += cmd.examples.length;
const subcommandCount = cmd.subcommands ? Object.keys(cmd.subcommands).length : 0;
totalSubcommands += subcommandCount;
const aliasInfo = cmd.aliases ? ` (aliases: ${cmd.aliases.join(", ")})` : "";
const subcommandInfo = subcommandCount > 0 ? `, ${subcommandCount} subcommands` : "";
const dynamicInfo = cmd.dynamicCompletions ? ` [dynamic: ${Object.keys(cmd.dynamicCompletions).join(", ")}]` : "";
console.log(
` - ${name}${aliasInfo}: ${cmd.flags.length} flags, ${cmd.positionalArgs.length} positional args, ${cmd.examples.length} examples${subcommandInfo}${dynamicInfo}`,
);
}
console.log(` - Total command flags: ${totalFlags}`);
console.log(` - Total examples: ${totalExamples}`);
console.log(` - Total subcommands: ${totalSubcommands}`);
}
// Run the script
if (import.meta.main) {
generateCompletions().catch(console.error);
}
+120
View File
@@ -0,0 +1,120 @@
# LLDB Pretty Printers for Bun
This directory contains LLDB pretty printers for various Bun data structures to improve the debugging experience.
## Files
- `bun_pretty_printer.py` - Pretty printers for Bun-specific types (bun.String, WTFStringImpl, ZigString, BabyList, etc.)
- `lldb_webkit.py` - Pretty printers for WebKit/JavaScriptCore types
- `init.lldb` - LLDB initialization commands
## Supported Types
### bun.String Types
- `bun.String` (or just `String`) - The main Bun string type
- `WTFStringImpl` - WebKit string implementation (Latin1/UTF16)
- `ZigString` - Tagged string type used at the FFI boundary (UTF8/Latin1/UTF16 with pointer tagging)
### Display Format
The pretty printers show string content directly, with additional metadata:
```
# bun.String examples:
"Hello, World!" [latin1] # Regular ZigString
"UTF-8 String 🎉" [utf8] # UTF-8 encoded
"Static content" [latin1 static] # Static string
"" # Empty string
<dead> # Dead/invalid string
# WTFStringImpl examples:
"WebKit String" # Shows the actual string content
# ZigString examples:
"Some text" [utf16 global] # UTF16 globally allocated
"ASCII text" [latin1] # Latin1 encoded
```
## Usage
### Option 1: Manual Loading
In your LLDB session:
```lldb
command script import /path/to/bun/misctools/lldb/bun_pretty_printer.py
```
### Option 2: Add to ~/.lldbinit
Add the following line to your `~/.lldbinit` file to load automatically:
```lldb
command script import /path/to/bun/misctools/lldb/bun_pretty_printer.py
```
### Option 3: Use init.lldb
```lldb
command source /path/to/bun/misctools/lldb/init.lldb
```
## Testing
To test the pretty printers:
1. Build a debug version of Bun:
```bash
bun bd
```
2. Create a test file that uses bun.String types
3. Debug with LLDB:
```bash
lldb ./build/debug/bun-debug
(lldb) command script import misctools/lldb/bun_pretty_printer.py
(lldb) breakpoint set --file your_file.rs --line <line_number>
(lldb) run your_test.ts
(lldb) frame variable
```
## Implementation Details
### ZigString Pointer Tagging
ZigString uses pointer tagging in the upper bits:
- Bit 63: 1 = UTF16, 0 = UTF8/Latin1
- Bit 62: 1 = Globally allocated (mimalloc)
- Bit 61: 1 = UTF8 encoding
The pretty printer automatically detects and handles these tags.
### WTFStringImpl Encoding
WTFStringImpl uses flags in `m_hashAndFlags`:
- Bit 2 (s_hashFlag8BitBuffer): 1 = Latin1, 0 = UTF16
### bun.String Tag Union
bun.String is a tagged union with these variants:
- Dead (0): Invalid/freed string
- WTFStringImpl (1): WebKit string
- ZigString (2): Regular ZigString
- StaticZigString (3): Static/immortal string
- Empty (4): Empty string ""
## Troubleshooting
If the pretty printers don't work:
1. Verify the Python script loaded:
```lldb
(lldb) script print("Python works")
```
2. Check if the category is enabled:
```lldb
(lldb) type category list
```
3. Enable the Bun category manually:
```lldb
(lldb) type category enable bun
```
4. For debugging the pretty printer itself, check for exceptions:
- The pretty printers catch all exceptions and return `<error>`
- Modify the code to print exceptions for debugging
+338
View File
@@ -0,0 +1,338 @@
# Pretty printers for Bun data structures
import lldb
import re
class bun_BabyList_SynthProvider:
def __init__(self, value, _=None):
self.value = value
def update(self):
try:
self.ptr = self.value.GetChildMemberWithName('ptr')
self.len = self.value.GetChildMemberWithName('len').GetValueAsUnsigned()
self.cap = self.value.GetChildMemberWithName('cap').GetValueAsUnsigned()
self.elem_type = self.ptr.type.GetPointeeType()
self.elem_size = self.elem_type.size
except:
self.len = 0
self.cap = 0
pass
def has_children(self):
return True
def num_children(self):
return self.len or 0
def get_child_index(self, name):
try:
return int(name.removeprefix('[').removesuffix(']'))
except:
return -1
def get_child_at_index(self, index):
if index not in range(self.len):
return None
try:
return self.ptr.CreateChildAtOffset('[%d]' % index, index * self.elem_size, self.elem_type)
except:
return None
def bun_BabyList_SummaryProvider(value, _=None):
try:
# Get the non-synthetic value to access raw members
value = value.GetNonSyntheticValue()
len_val = value.GetChildMemberWithName('len')
cap_val = value.GetChildMemberWithName('cap')
return 'len=%d cap=%d' % (len_val.GetValueAsUnsigned(), cap_val.GetValueAsUnsigned())
except:
return 'len=? cap=?'
def add(debugger, *, category, regex=False, type, identifier=None, synth=False, inline_children=False, expand=False, summary=False):
prefix = '.'.join((__name__, (identifier or type).replace('.', '_').replace(':', '_')))
if summary:
debugger.HandleCommand('type summary add --category %s%s%s "%s"' % (
category,
' --inline-children' if inline_children else ''.join((' --expand' if expand else '', ' --python-function %s_SummaryProvider' % prefix if summary == True else ' --summary-string "%s"' % summary)),
' --regex' if regex else '',
type
))
if synth:
debugger.HandleCommand('type synthetic add --category %s%s --python-class %s_SynthProvider "%s"' % (
category,
' --regex' if regex else '',
prefix,
type
))
def WTFStringImpl_SummaryProvider(value, _=None):
try:
# Get the raw pointer (it's already a pointer type)
value = value.GetNonSyntheticValue()
# Check if it's a pointer type and dereference if needed
if value.type.IsPointerType():
struct = value.deref
else:
struct = value
m_length = struct.GetChildMemberWithName('m_length').GetValueAsUnsigned()
m_hashAndFlags = struct.GetChildMemberWithName('m_hashAndFlags').GetValueAsUnsigned()
m_ptr = struct.GetChildMemberWithName('m_ptr')
# Check if it's 8-bit (latin1) or 16-bit (utf16) string
s_hashFlag8BitBuffer = 1 << 2
is_8bit = (m_hashAndFlags & s_hashFlag8BitBuffer) != 0
if m_length == 0:
return '[%s] ""' % ('latin1' if is_8bit else 'utf16')
# Limit memory reads to 1MB for performance
MAX_BYTES = 1024 * 1024 # 1MB
MAX_DISPLAY_CHARS = 200 # Maximum characters to display
# Calculate how much to read
bytes_per_char = 1 if is_8bit else 2
total_bytes = m_length * bytes_per_char
truncated = False
if total_bytes > MAX_BYTES:
# Read only first part of very large strings
chars_to_read = MAX_BYTES // bytes_per_char
bytes_to_read = chars_to_read * bytes_per_char
truncated = True
else:
chars_to_read = m_length
bytes_to_read = total_bytes
if is_8bit:
# Latin1 string
latin1_ptr = m_ptr.GetChildMemberWithName('latin1')
process = value.process
error = lldb.SBError()
ptr_addr = latin1_ptr.GetValueAsUnsigned()
if ptr_addr:
byte_data = process.ReadMemory(ptr_addr, min(chars_to_read, m_length), error)
if error.Success():
string_val = byte_data.decode('latin1', errors='replace')
else:
return '[latin1] <read error: %s>' % error
else:
return '[latin1] <null ptr>'
else:
# UTF16 string
utf16_ptr = m_ptr.GetChildMemberWithName('utf16')
process = value.process
error = lldb.SBError()
ptr_addr = utf16_ptr.GetValueAsUnsigned()
if ptr_addr:
byte_data = process.ReadMemory(ptr_addr, bytes_to_read, error)
if error.Success():
# Properly decode UTF16LE to string
string_val = byte_data.decode('utf-16le', errors='replace')
else:
return '[utf16] <read error: %s>' % error
else:
return '[utf16] <null ptr>'
# Escape special characters
string_val = string_val.replace('\\', '\\\\')
string_val = string_val.replace('"', '\\"')
string_val = string_val.replace('\n', '\\n')
string_val = string_val.replace('\r', '\\r')
string_val = string_val.replace('\t', '\\t')
# Truncate display if too long
display_truncated = truncated or len(string_val) > MAX_DISPLAY_CHARS
if len(string_val) > MAX_DISPLAY_CHARS:
string_val = string_val[:MAX_DISPLAY_CHARS]
# Add encoding and size info at the beginning
encoding = 'latin1' if is_8bit else 'utf16'
if display_truncated:
size_info = ' %d chars' % m_length
if total_bytes >= 1024 * 1024:
size_info += ' (%.1fMB)' % (total_bytes / (1024.0 * 1024.0))
elif total_bytes >= 1024:
size_info += ' (%.1fKB)' % (total_bytes / 1024.0)
return '[%s%s] "%s..." <truncated>' % (encoding, size_info, string_val)
else:
return '[%s] "%s"' % (encoding, string_val)
except:
return '<error>'
def ZigString_SummaryProvider(value, _=None):
try:
value = value.GetNonSyntheticValue()
ptr = value.GetChildMemberWithName('_unsafe_ptr_do_not_use').GetValueAsUnsigned()
length = value.GetChildMemberWithName('len').GetValueAsUnsigned()
# Check encoding flags
is_16bit = (ptr & (1 << 63)) != 0
is_utf8 = (ptr & (1 << 61)) != 0
is_global = (ptr & (1 << 62)) != 0
# Determine encoding
encoding = 'utf16' if is_16bit else ('utf8' if is_utf8 else 'latin1')
flags = ' global' if is_global else ''
if length == 0:
return '[%s%s] ""' % (encoding, flags)
# Untag the pointer (keep only the lower 53 bits)
untagged_ptr = ptr & ((1 << 53) - 1)
# Limit memory reads to 1MB for performance
MAX_BYTES = 1024 * 1024 # 1MB
MAX_DISPLAY_CHARS = 200 # Maximum characters to display
# Calculate how much to read
bytes_per_char = 2 if is_16bit else 1
total_bytes = length * bytes_per_char
truncated = False
if total_bytes > MAX_BYTES:
# Read only first part of very large strings
chars_to_read = MAX_BYTES // bytes_per_char
bytes_to_read = chars_to_read * bytes_per_char
truncated = True
else:
bytes_to_read = total_bytes
# Read the string data
process = value.process
error = lldb.SBError()
byte_data = process.ReadMemory(untagged_ptr, bytes_to_read, error)
if not error.Success():
return '[%s%s] <read error>' % (encoding, flags)
# Decode based on encoding
if is_16bit:
string_val = byte_data.decode('utf-16le', errors='replace')
elif is_utf8:
string_val = byte_data.decode('utf-8', errors='replace')
else:
string_val = byte_data.decode('latin1', errors='replace')
# Escape special characters
string_val = string_val.replace('\\', '\\\\')
string_val = string_val.replace('"', '\\"')
string_val = string_val.replace('\n', '\\n')
string_val = string_val.replace('\r', '\\r')
string_val = string_val.replace('\t', '\\t')
# Truncate display if too long
display_truncated = truncated or len(string_val) > MAX_DISPLAY_CHARS
if len(string_val) > MAX_DISPLAY_CHARS:
string_val = string_val[:MAX_DISPLAY_CHARS]
# Build the output
if display_truncated:
size_info = ' %d chars' % length
if total_bytes >= 1024 * 1024:
size_info += ' (%.1fMB)' % (total_bytes / (1024.0 * 1024.0))
elif total_bytes >= 1024:
size_info += ' (%.1fKB)' % (total_bytes / 1024.0)
return '[%s%s%s] "%s..." <truncated>' % (encoding, flags, size_info, string_val)
else:
return '[%s%s] "%s"' % (encoding, flags, string_val)
except:
return '<error>'
def bun_String_SummaryProvider(value, _=None):
try:
value = value.GetNonSyntheticValue()
# Debug: Show the actual type name LLDB sees
type_name = value.GetTypeName()
tag = value.GetChildMemberWithName('tag')
if not tag or not tag.IsValid():
# Try alternate field names
tag = value.GetChildMemberWithName('Tag')
if not tag or not tag.IsValid():
# Show type name to help debug
return '<no tag field in type: %s>' % type_name
tag_value = tag.GetValueAsUnsigned()
# Map tag values to names
tag_names = {
0: 'Dead',
1: 'WTFStringImpl',
2: 'ZigString',
3: 'StaticZigString',
4: 'Empty'
}
tag_name = tag_names.get(tag_value, 'Unknown')
if tag_name == 'Empty':
return '""'
elif tag_name == 'Dead':
return '<dead>'
elif tag_name == 'WTFStringImpl':
value_union = value.GetChildMemberWithName('value')
if not value_union or not value_union.IsValid():
return '<no value field>'
impl_value = value_union.GetChildMemberWithName('WTFStringImpl')
if not impl_value or not impl_value.IsValid():
return '<no WTFStringImpl field>'
return WTFStringImpl_SummaryProvider(impl_value, _)
elif tag_name == 'ZigString' or tag_name == 'StaticZigString':
value_union = value.GetChildMemberWithName('value')
if not value_union or not value_union.IsValid():
return '<no value field>'
field_name = 'ZigString' if tag_name == 'ZigString' else 'StaticZigString'
zig_string_value = value_union.GetChildMemberWithName(field_name)
if not zig_string_value or not zig_string_value.IsValid():
return '<no %s field>' % field_name
result = ZigString_SummaryProvider(zig_string_value, _)
# Add static marker if needed
if tag_name == 'StaticZigString':
result = result.replace(']', ' static]')
return result
else:
return '<unknown tag %d>' % tag_value
except Exception as e:
return '<error: %s>' % str(e)
def __lldb_init_module(debugger, _=None):
# Initialize Bun Category
debugger.HandleCommand('type category define --language c99 bun')
# Initialize Bun Data Structures
add(debugger, category='bun', regex=True, type='^baby_list\\.BabyList\\(.*\\)$', identifier='bun_BabyList', synth=True, expand=True, summary=True)
# Add WTFStringImpl pretty printer - try multiple possible type names
add(debugger, category='bun', type='WTFStringImpl', identifier='WTFStringImpl', summary=True)
add(debugger, category='bun', type='*WTFStringImplStruct', identifier='WTFStringImpl', summary=True)
add(debugger, category='bun', type='string.WTFStringImpl', identifier='WTFStringImpl', summary=True)
add(debugger, category='bun', type='string.WTFStringImplStruct', identifier='WTFStringImpl', summary=True)
add(debugger, category='bun', type='*string.WTFStringImplStruct', identifier='WTFStringImpl', summary=True)
# Add ZigString pretty printer - try multiple possible type names
add(debugger, category='bun', type='ZigString', identifier='ZigString', summary=True)
add(debugger, category='bun', type='bun.js.bindings.ZigString', identifier='ZigString', summary=True)
add(debugger, category='bun', type='bindings.ZigString', identifier='ZigString', summary=True)
# Add bun.String pretty printer - try multiple possible type names
add(debugger, category='bun', type='String', identifier='bun_String', summary=True)
add(debugger, category='bun', type='bun.String', identifier='bun_String', summary=True)
add(debugger, category='bun', type='string.String', identifier='bun_String', summary=True)
add(debugger, category='bun', type='BunString', identifier='bun_String', summary=True)
add(debugger, category='bun', type='bun::String', identifier='bun_String', summary=True)
add(debugger, category='bun', type='bun::string::String', identifier='bun_String', summary=True)
# Try regex patterns for more flexible matching
add(debugger, category='bun', regex=True, type='.*String$', identifier='bun_String', summary=True)
add(debugger, category='bun', regex=True, type='.*WTFStringImpl.*', identifier='WTFStringImpl', summary=True)
add(debugger, category='bun', regex=True, type='.*ZigString.*', identifier='ZigString', summary=True)
# Enable the category
debugger.HandleCommand('type category enable bun')
+22
View File
@@ -0,0 +1,22 @@
# This file is separate from .lldbinit because it has to be in the same directory as the Python
# modules in order for the "attach" action to work.
# Tell LLDB what to do when the debugged process receives SIGPWR: pass it through to the process
# (-p), but do not stop the process (-s) or notify the user (-n).
#
# JSC's garbage collector sends this signal (as configured by Bun WebKit in
# Thread::initializePlatformThreading() in ThreadingPOSIX.cpp) to the JS thread to suspend or resume
# it. So stopping the process would just create noise when debugging any long-running script.
process handle -p true -s false -n false SIGPWR
process handle -p true -s false -n false SIGUSR1
process handle -p true -s false -n false SIGUSR2
command script import -c lldb_webkit.py
command script import -c bun_pretty_printer.py
command script delete btjs
command alias btjs p {printf("gathering btjs trace...\n");printf("%s\n", (char*)dumpBtjsTrace())}
# do not pass SIGHUP on to child process. it is often not the real error and the stop point will be nonsensical.
process handle -p false -s false -n true SIGHUP
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
{
"name": "misctools",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {
"@unicode/unicode-13.0.0": "^1.2.1",
"@unicode/unicode-3.0.0": "^1.6.5",
"semver": "^7.3.7"
},
"dependencies": {
"@unicode/unicode-15.1.0": "^1.6.5"
}
}
+138
View File
@@ -0,0 +1,138 @@
import crypto from "crypto";
// Types to mirror Zig's structures
interface Context<Elem> {
get(codepoint: number): Promise<Elem> | Elem;
eql(a: Elem, b: Elem): boolean;
}
interface Tables<Elem> {
stage1: number[];
stage2: number[];
stage3: Elem[];
}
class Generator<Elem> {
private static readonly BLOCK_SIZE = 256;
private readonly ctx: Context<Elem>;
private readonly blockMap = new Map<string, number>();
constructor(ctx: Context<Elem>) {
this.ctx = ctx;
}
private hashBlock(block: number[]): string {
const hash = crypto.createHash("sha256");
hash.update(Buffer.from(new Uint16Array(block).buffer));
return hash.digest("hex");
}
async generate(): Promise<Tables<Elem>> {
const stage1: number[] = [];
const stage2: number[] = [];
const stage3: Elem[] = [];
let block = new Array(Generator.BLOCK_SIZE).fill(0);
let blockLen = 0;
// Maximum Unicode codepoint is 0x10FFFF
for (let cp = 0; cp <= 0x10ffff; cp++) {
// Get the mapping for this codepoint
const elem = await this.ctx.get(cp);
// Find or add the element in stage3
let blockIdx = stage3.findIndex(item => this.ctx.eql(item, elem));
if (blockIdx === -1) {
blockIdx = stage3.length;
stage3.push(elem);
}
if (blockIdx > 0xffff) {
throw new Error("Block index too large");
}
// Add to current block
block[blockLen] = blockIdx;
blockLen++;
// Check if we need to finalize this block
if (blockLen < Generator.BLOCK_SIZE && cp !== 0x10ffff) {
continue;
}
// Fill remaining block space with zeros if needed
if (blockLen < Generator.BLOCK_SIZE) {
block.fill(0, blockLen);
}
// Get or create stage2 index for this block
const blockHash = this.hashBlock(block);
let stage2Idx = this.blockMap.get(blockHash);
if (stage2Idx === undefined) {
stage2Idx = stage2.length;
this.blockMap.set(blockHash, stage2Idx);
stage2.push(...block.slice(0, blockLen));
}
if (stage2Idx > 0xffff) {
throw new Error("Stage2 index too large");
}
// Add mapping to stage1
stage1.push(stage2Idx);
// Reset block
block = new Array(Generator.BLOCK_SIZE).fill(0);
blockLen = 0;
}
return { stage1, stage2, stage3 };
}
// Generates Zig code for the lookup tables
static writeZig<Elem>(tableName: string, tables: Tables<Elem>, elemToString: (elem: Elem) => string): string {
let output = `/// Auto-generated. Do not edit.\n`;
output += `fn ${tableName}(comptime Elem: type) type {\n`;
output += " return struct {\n";
// Stage 1
output += `pub const stage1: [${tables.stage1.length}]u16 = .{`;
output += tables.stage1.join(",");
output += "};\n\n";
// Stage 2
output += `pub const stage2: [${tables.stage2.length}]u8 = .{`;
output += tables.stage2.join(",");
output += "};\n\n";
// Stage 3
output += `pub const stage3: [${tables.stage3.length}]Elem = .{`;
output += tables.stage3.map(elemToString).join(",");
output += "};\n";
output += " };\n}\n";
return output;
}
}
// Example usage:
async function example() {
// Example context that maps codepoints to their category
const ctx: Context<string> = {
get: async (cp: number) => {
// This would normally look up the actual Unicode category
return "Lu";
},
eql: (a: string, b: string) => a === b,
};
const generator = new Generator(ctx);
const tables = await generator.generate();
// Generate Zig code
const zigCode = Generator.writeZig(tables, (elem: string) => `"${elem}"`);
console.log(zigCode);
}
export { Generator, type Context, type Tables };