Skip to content

Instantly share code, notes, and snippets.

@qpwo
Created August 20, 2026 13:24
Show Gist options
  • Select an option

  • Save qpwo/b971a1c901bd398764b9b463e6fd6c45 to your computer and use it in GitHub Desktop.

Select an option

Save qpwo/b971a1c901bd398764b9b463e6fd6c45 to your computer and use it in GitHub Desktop.
how-to-split-ts-tsx-mjs.c
//bin/sh -c 'sum=$(cksum < "$0" | tr " " -); o=${TMPDIR:-/tmp}/how-to-split-ts-tsx-mjs-$sum; [ -x "$o" ] || ${CC:-cc} -O3 -Wall -Wextra $(pkg-config --cflags libgit2) "$0" -o "$o" $(pkg-config --libs libgit2) || exit; exec "$o" "$@"' "$0" "$@"; exit
#include <git2.h>
#include <ctype.h>
#include <errno.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
typedef struct {
const char *text;
size_t len;
size_t start;
size_t end;
unsigned line;
unsigned kind;
int mate;
} Token;
typedef struct {
char *key;
char *ident;
uint64_t hash;
size_t start;
size_t end;
unsigned line;
unsigned end_line;
char **refs;
size_t nrefs;
} Symbol;
typedef struct {
char *path;
char *source;
size_t len;
Symbol *symbols;
size_t count;
} File;
typedef struct {
char *name;
char *ident;
unsigned edits;
int module;
int current;
size_t symbols;
size_t bytes;
} Entity;
typedef struct {
uint32_t left;
uint32_t right;
unsigned shared;
double weight;
unsigned used;
} PairSlot;
typedef struct {
char *key;
uint32_t entity;
unsigned used;
} NameSlot;
typedef struct {
uint32_t other;
double weight;
unsigned shared;
} Edge;
typedef struct {
Edge *items;
size_t count;
size_t cap;
} Edges;
typedef struct {
git_repository *repo;
File *files;
size_t nfiles;
size_t filecap;
Entity *entities;
size_t nentities;
size_t entitycap;
NameSlot *names;
size_t namecap;
PairSlot *pairs;
size_t paircap;
size_t npairs;
size_t commits;
size_t mapped;
size_t multi;
size_t merges;
size_t parsed_blobs;
size_t changed_files;
size_t changed_symbols;
size_t broadest;
size_t broad_skipped;
char head[65];
char *subject;
double repulsion;
size_t median_bytes;
} Analysis;
static const char *source_path_context = "<self-test>";
static char source_blob_context[1024];
static _Noreturn void fail(const char *format, ...) {
va_list args;
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
fputc('\n', stderr);
exit(1);
}
static void git_fail(const char *operation, int code) {
const git_error *error = git_error_last();
fail("%s failed (%d): %s", operation, code, error ? error->message : "unknown libgit2 error");
}
static void *resize(void *old, size_t size) {
void *out = realloc(old, size);
if (!out && size) fail("out of memory allocating %zu bytes", size);
return out;
}
static char *copy_text(const char *text, size_t len) {
char *out = resize(NULL, len + 1);
memcpy(out, text, len);
out[len] = 0;
return out;
}
static int text_is(const char *text, size_t len, const char *wanted) {
return len == strlen(wanted) && !memcmp(text, wanted, len);
}
static int token_is(Token token, const char *wanted) {
return text_is(token.text, token.len, wanted);
}
static int ident_start(unsigned char c) {
return isalpha(c) || c == '_' || c == '$' || c >= 128;
}
static int ident_part(unsigned char c) {
return ident_start(c) || isdigit(c);
}
static size_t quote_end(const char *source, size_t len, size_t i, char quote) {
i++;
while (i < len && source[i] != '\n') {
if (source[i] == '\\' && i + 1 < len) i += 2;
else if (source[i++] == quote) return i;
}
return 0;
}
static size_t regex_end(const char *source, size_t len, size_t i);
static size_t skip_template(const char *source, size_t len, size_t i);
static size_t skip_template_expression(const char *source, size_t len, size_t i) {
size_t depth = 1;
int prefix = 1;
while (i < len) {
if (isspace((unsigned char)source[i])) {
i++;
continue;
}
if (i + 1 < len && source[i] == '/' && source[i + 1] == '/') {
while (i < len && source[i] != '\n') i++;
continue;
}
if (i + 1 < len && source[i] == '/' && source[i + 1] == '*') {
i += 2;
while (i + 1 < len && !(source[i] == '*' && source[i + 1] == '/')) i++;
if (i + 1 == len) return 0;
i += 2;
continue;
}
if (source[i] == '\'' || source[i] == '"') {
size_t end = quote_end(source, len, i, source[i]);
i = end ? end : i + 1;
prefix = 0;
continue;
}
if (source[i] == '`') {
i = skip_template(source, len, i);
prefix = 0;
continue;
}
if (source[i] == '/' && prefix) {
size_t end = regex_end(source, len, i);
if (end) {
i = end;
prefix = 0;
continue;
}
}
if (ident_start((unsigned char)source[i])) {
size_t start = i++;
while (i < len && ident_part((unsigned char)source[i])) i++;
prefix = text_is(source + start, i - start, "return") ||
text_is(source + start, i - start, "throw") ||
text_is(source + start, i - start, "case") ||
text_is(source + start, i - start, "yield") ||
text_is(source + start, i - start, "await");
continue;
}
if (source[i] == '{') {
depth++;
prefix = 1;
} else if (source[i] == '}' && !--depth) {
return i + 1;
} else {
prefix = strchr("([,;:?=!!&|+-*%<>", source[i]) != NULL;
}
i++;
}
return 0;
}
static size_t skip_template(const char *source, size_t len, size_t i) {
size_t start = i;
i++;
while (i < len) {
if (source[i] == '\\' && i + 1 < len) {
i += 2;
continue;
}
if (source[i] == '`') return i + 1;
if (i + 1 < len && source[i] == '$' && source[i + 1] == '{') {
i = skip_template_expression(source, len, i + 2);
if (!i) break;
continue;
}
i++;
}
unsigned line = 1;
for (size_t j = 0; j < start; j++) line += source[j] == '\n';
fail(
"unclosed template literal in %s at line %u: %.120s",
source_path_context,
line,
source + start
);
return 0;
}
static int regex_follows(Token *tokens, size_t count) {
if (!count) return 1;
Token token = tokens[count - 1];
if (!token.kind) return !token_is(token, ")") && !token_is(token, "]") &&
!token_is(token, "}") && !token_is(token, "++") &&
!token_is(token, "--") && !token_is(token, "<");
if (token.kind != 1) return 0;
return token_is(token, "return") || token_is(token, "throw") ||
token_is(token, "case") || token_is(token, "delete") ||
token_is(token, "typeof") || token_is(token, "void") ||
token_is(token, "new") || token_is(token, "in") ||
token_is(token, "of") || token_is(token, "yield") ||
token_is(token, "await");
}
static size_t regex_end(const char *source, size_t len, size_t i) {
int class = 0;
i++;
while (i < len && source[i] != '\n') {
if (source[i] == '\\' && i + 1 < len) {
i += 2;
continue;
}
if (source[i] == '[') class = 1;
if (source[i] == ']') class = 0;
if (source[i++] == '/' && !class) {
while (i < len && isalpha((unsigned char)source[i])) i++;
return i;
}
}
return 0;
}
static void add_token(Token **tokens, size_t *count, size_t *cap, Token token) {
if (*count == *cap) {
*cap = *cap ? *cap * 2 : 4096;
*tokens = resize(*tokens, *cap * sizeof **tokens);
}
(*tokens)[(*count)++] = token;
}
static Token *lex_ts(const char *source, size_t len, size_t *count) {
Token *tokens = NULL;
size_t used = 0;
size_t cap = 0;
unsigned line = 1;
for (size_t i = 0; i < len;) {
if (isspace((unsigned char)source[i])) {
line += source[i++] == '\n';
continue;
}
if (i + 1 < len && source[i] == '/' && source[i + 1] == '/') {
i += 2;
while (i < len && source[i] != '\n') i++;
continue;
}
if (i + 1 < len && source[i] == '/' && source[i + 1] == '*') {
i += 2;
while (i + 1 < len && !(source[i] == '*' && source[i + 1] == '/')) {
line += source[i++] == '\n';
}
if (i + 1 == len) fail("unclosed block comment");
i += 2;
continue;
}
size_t start = i;
unsigned start_line = line;
unsigned kind = 0;
size_t regex = source[i] == '/' && regex_follows(tokens, used) ?
regex_end(source, len, i) : 0;
size_t quote = source[i] == '\'' || source[i] == '"' ?
quote_end(source, len, i, source[i]) : 0;
if (regex) {
i = regex;
kind = 2;
} else if (quote) {
i = quote;
kind = 2;
} else if (source[i] == '`') {
i = skip_template(source, len, i);
kind = 2;
} else if (ident_start((unsigned char)source[i])) {
i++;
while (i < len && ident_part((unsigned char)source[i])) i++;
kind = 1;
} else if (isdigit((unsigned char)source[i])) {
i++;
while (i < len && (isalnum((unsigned char)source[i]) || strchr("._", source[i]))) i++;
kind = 2;
} else {
static const char *const punct[] = {
">>>=", "===", "!==", "=>", "?.", "??", "**", "++", "--", "&&", "||",
"==", "!=", "<=", ">=", "<<", ">>", "+=", "-=", "*=", "/=", "%=", "&=",
"|=", "^=", "::", "...", NULL
};
size_t width = 1;
for (size_t p = 0; punct[p]; p++) {
size_t n = strlen(punct[p]);
if (i + n <= len && !memcmp(source + i, punct[p], n)) {
width = n;
break;
}
}
i += width;
}
for (size_t p = start; p < i; p++) line += source[p] == '\n';
add_token(&tokens, &used, &cap, (Token){
source + start, i - start, start, i, start_line, kind, -1
});
}
int *stack = resize(NULL, used * sizeof *stack);
size_t depth = 0;
for (size_t i = 0; i < used; i++) {
if (token_is(tokens[i], "(") || token_is(tokens[i], "[") || token_is(tokens[i], "{")) {
stack[depth++] = (int)i;
continue;
}
if (!token_is(tokens[i], ")") && !token_is(tokens[i], "]") && !token_is(tokens[i], "}")) continue;
if (!depth) fail("unmatched closing token on line %u", tokens[i].line);
size_t left = (size_t)stack[--depth];
int valid = (token_is(tokens[left], "(") && token_is(tokens[i], ")")) ||
(token_is(tokens[left], "[") && token_is(tokens[i], "]")) ||
(token_is(tokens[left], "{") && token_is(tokens[i], "}"));
if (!valid) {
fprintf(stderr, "%s: tokens before mismatch:", source_path_context);
for (size_t j = i > 30 ? i - 30 : 0; j <= i; j++)
fprintf(stderr, " %.*s@%u", (int)tokens[j].len, tokens[j].text, tokens[j].line);
fprintf(stderr, "\n%s: open stack:", source_path_context);
for (size_t j = 0; j < depth; j++) {
Token open = tokens[stack[j]];
fprintf(stderr, " %.*s@%u", (int)open.len, open.text, open.line);
}
fputc('\n', stderr);
fail(
"%s: mismatched %.*s on line %u with %.*s on line %u",
source_path_context,
(int)tokens[left].len,
tokens[left].text,
tokens[left].line,
(int)tokens[i].len,
tokens[i].text,
tokens[i].line
);
}
tokens[left].mate = (int)i;
tokens[i].mate = (int)left;
}
if (depth) fail("unclosed token on line %u", tokens[stack[depth - 1]].line);
free(stack);
*count = used;
return tokens;
}
static uint64_t hash_tokens(Token *tokens, size_t start, size_t end) {
uint64_t hash = 1469598103934665603ULL;
for (size_t i = start; i < end; i++) {
hash ^= tokens[i].len;
hash *= 1099511628211ULL;
for (size_t j = 0; j < tokens[i].len; j++) {
hash ^= (unsigned char)tokens[i].text[j];
hash *= 1099511628211ULL;
}
}
return hash;
}
static size_t statement_end(Token *tokens, size_t count, size_t start) {
for (size_t i = start; i < count; i++) {
if (tokens[i].mate > (int)i) {
i = (size_t)tokens[i].mate;
continue;
}
if (token_is(tokens[i], ";")) return i + 1;
}
return count;
}
static size_t declaration_end(Token *tokens, size_t count, size_t start, int function) {
for (size_t i = start; i < count; i++) {
if (token_is(tokens[i], "{") && tokens[i].mate > (int)i) {
int signature_type =
function && i &&
(token_is(tokens[i - 1], ":") || token_is(tokens[i - 1], "<") ||
token_is(tokens[i - 1], "|") || token_is(tokens[i - 1], "&") ||
token_is(tokens[i - 1], "=>") || token_is(tokens[i - 1], "extends"));
if (!signature_type) return (size_t)tokens[i].mate + 1;
i = (size_t)tokens[i].mate;
continue;
}
if (token_is(tokens[i], ";")) return i + 1;
if (tokens[i].mate > (int)i) i = (size_t)tokens[i].mate;
}
return count;
}
static int has_name(char **names, size_t count, Token token) {
for (size_t i = 0; i < count; i++)
if (text_is(token.text, token.len, names[i])) return 1;
return 0;
}
static void add_name(char ***names, size_t *count, Token token) {
if (has_name(*names, *count, token)) return;
*names = resize(*names, (*count + 1) * sizeof **names);
(*names)[(*count)++] = copy_text(token.text, token.len);
}
static void add_ref(Symbol *symbol, Token token) {
if (has_name(symbol->refs, symbol->nrefs, token)) return;
symbol->refs = resize(symbol->refs, (symbol->nrefs + 1) * sizeof *symbol->refs);
symbol->refs[symbol->nrefs++] = copy_text(token.text, token.len);
}
static void collect_parameter_names(
char ***bound,
size_t *nbound,
Token *tokens,
size_t open,
size_t close
) {
size_t segment = open + 1;
for (size_t i = segment; i <= close; i++) {
if (i < close && tokens[i].mate > (int)i) {
i = (size_t)tokens[i].mate;
continue;
}
if (i < close && !token_is(tokens[i], ",")) continue;
for (size_t j = segment; j < i; j++)
if (tokens[j].kind == 1) {
add_name(bound, nbound, tokens[j]);
break;
}
segment = i + 1;
}
}
static void add_template_refs(Symbol *symbol, char **bound, size_t nbound, Token token) {
for (size_t i = 0; i + 2 < token.len; i++) {
if (token.text[i] != '$' || token.text[i + 1] != '{') continue;
size_t end = skip_template_expression(token.text, token.len, i + 2);
if (!end || end <= i + 3) continue;
size_t count = 0;
Token *part = lex_ts(token.text + i + 2, end - i - 3, &count);
for (size_t j = 0; j < count; j++) {
if (part[j].kind == 2 && part[j].len && part[j].text[0] == '`') {
add_template_refs(symbol, bound, nbound, part[j]);
continue;
}
if (part[j].kind != 1 || has_name(bound, nbound, part[j])) continue;
if (j && (token_is(part[j - 1], ".") || token_is(part[j - 1], "?."))) continue;
if (j + 1 < count && token_is(part[j + 1], ":")) continue;
add_ref(symbol, part[j]);
}
free(part);
i = end - 1;
}
}
static void add_symbol(
Symbol **symbols,
size_t *count,
size_t *cap,
const char *kind,
Token name,
Token *tokens,
size_t start,
size_t end,
int references
) {
if (*count == *cap) {
*cap = *cap ? *cap * 2 : 128;
*symbols = resize(*symbols, *cap * sizeof **symbols);
}
Symbol *symbol = &(*symbols)[(*count)++];
memset(symbol, 0, sizeof *symbol);
symbol->ident = copy_text(name.text, name.len);
symbol->key = resize(NULL, strlen(kind) + name.len + 2);
snprintf(symbol->key, strlen(kind) + name.len + 2, "%s %.*s", kind, (int)name.len, name.text);
symbol->hash = hash_tokens(tokens, start, end);
symbol->start = tokens[start].start;
symbol->end = tokens[end - 1].end;
symbol->line = tokens[start].line;
symbol->end_line = tokens[end - 1].line;
if (!references) return;
char **bound = NULL;
size_t nbound = 0;
add_name(&bound, &nbound, name);
for (size_t i = start; i < end; i++) {
if ((token_is(tokens[i], "const") || token_is(tokens[i], "let") ||
token_is(tokens[i], "var") || token_is(tokens[i], "catch")) &&
i + 1 < end) {
size_t next = i + 1;
if (token_is(tokens[next], "(")) next++;
if (next < end && tokens[next].kind == 1) add_name(&bound, &nbound, tokens[next]);
}
if (token_is(tokens[i], "(") && tokens[i].mate > (int)i) {
size_t close = (size_t)tokens[i].mate;
int function_params =
i > start && (token_is(tokens[i - 1], symbol->ident) ||
token_is(tokens[i - 1], "function"));
int arrow_params = close + 1 < end && token_is(tokens[close + 1], "=>");
if (function_params || arrow_params)
collect_parameter_names(&bound, &nbound, tokens, i, close);
}
if (tokens[i].kind == 1 && i + 1 < end && token_is(tokens[i + 1], "=>"))
add_name(&bound, &nbound, tokens[i]);
}
for (size_t i = start; i < end; i++) {
if (tokens[i].kind == 2 && tokens[i].len && tokens[i].text[0] == '`') {
add_template_refs(symbol, bound, nbound, tokens[i]);
continue;
}
if (tokens[i].kind != 1 || has_name(bound, nbound, tokens[i])) continue;
if (i > start && (token_is(tokens[i - 1], ".") || token_is(tokens[i - 1], "?.")))
continue;
if (i + 1 < end && token_is(tokens[i + 1], ":")) continue;
add_ref(symbol, tokens[i]);
}
for (size_t i = 0; i < nbound; i++) free(bound[i]);
free(bound);
}
static int declaration_keyword(Token token) {
static const char *const words[] = {
"function", "class", "interface", "type", "enum", "namespace", "module", NULL
};
for (size_t i = 0; words[i]; i++)
if (token_is(token, words[i])) return (int)i + 1;
return 0;
}
static Token default_name(Token keyword) {
return (Token){"default", 7, keyword.start, keyword.end, keyword.line, 1, -1};
}
static size_t extract_declaration(
Symbol **symbols,
size_t *used,
size_t *cap,
Token *tokens,
size_t count,
size_t keyword,
int references
) {
static const char *const kinds[] = {
"function", "class", "interface", "type", "enum", "namespace", "module"
};
int declaration = declaration_keyword(tokens[keyword]);
size_t name = keyword + 1;
while (name < count && (token_is(tokens[name], "*") || token_is(tokens[name], "const"))) name++;
Token symbol_name = name < count && tokens[name].kind == 1 ? tokens[name] : default_name(tokens[keyword]);
size_t start = keyword;
while (start && (token_is(tokens[start - 1], "export") ||
token_is(tokens[start - 1], "default") ||
token_is(tokens[start - 1], "declare") ||
token_is(tokens[start - 1], "abstract") ||
token_is(tokens[start - 1], "async")))
start--;
size_t end = declaration == 4
? statement_end(tokens, count, keyword)
: declaration_end(tokens, count, keyword, declaration == 1);
add_symbol(symbols, used, cap, kinds[declaration - 1], symbol_name,
tokens, start, end, references);
return end;
}
static size_t extract_variables(
Symbol **symbols,
size_t *used,
size_t *cap,
Token *tokens,
size_t count,
size_t keyword,
int references
) {
size_t end = statement_end(tokens, count, keyword);
size_t start = keyword;
while (start && (token_is(tokens[start - 1], "export") ||
token_is(tokens[start - 1], "declare")))
start--;
size_t segment = keyword + 1;
size_t type_angles = 0;
int type_annotation = 0;
for (size_t i = segment; i <= end; i++) {
if (i < end && tokens[i].mate > (int)i) {
i = (size_t)tokens[i].mate;
continue;
}
if (i < end && (token_is(tokens[i], ":") ||
token_is(tokens[i], "as") ||
token_is(tokens[i], "satisfies")))
type_annotation = 1;
if (i < end && type_annotation && token_is(tokens[i], "<")) type_angles++;
if (i < end && type_annotation && token_is(tokens[i], ">") && type_angles) type_angles--;
if (i < end && type_annotation && token_is(tokens[i], ">>")) {
type_angles = type_angles > 1 ? type_angles - 2 : 0;
}
if (i < end && token_is(tokens[i], "=") && !type_angles) type_annotation = 0;
if (i < end && (!token_is(tokens[i], ",") || type_angles)) continue;
size_t name = segment;
while (name < i && tokens[name].kind != 1) name++;
if (name < i)
add_symbol(symbols, used, cap,
token_is(tokens[keyword], "const") ? "const" :
token_is(tokens[keyword], "let") ? "let" : "var",
tokens[name], tokens, start, end, references);
segment = i + 1;
type_annotation = 0;
type_angles = 0;
}
return end;
}
static Symbol *extract_symbols(const char *source, size_t len, size_t *count, int references) {
size_t ntokens = 0;
Token *tokens = lex_ts(source, len, &ntokens);
Symbol *symbols = NULL;
size_t used = 0;
size_t cap = 0;
for (size_t i = 0; i < ntokens;) {
if (token_is(tokens[i], "import")) {
i = statement_end(tokens, ntokens, i);
continue;
}
if (declaration_keyword(tokens[i])) {
i = extract_declaration(&symbols, &used, &cap, tokens, ntokens, i, references);
continue;
}
if (token_is(tokens[i], "const") || token_is(tokens[i], "let") || token_is(tokens[i], "var")) {
i = extract_variables(&symbols, &used, &cap, tokens, ntokens, i, references);
continue;
}
if (token_is(tokens[i], "{") && tokens[i].mate > (int)i) {
i = (size_t)tokens[i].mate + 1;
continue;
}
i++;
}
free(tokens);
*count = used;
return symbols;
}
static void free_symbol(Symbol *symbol) {
free(symbol->key);
free(symbol->ident);
for (size_t i = 0; i < symbol->nrefs; i++) free(symbol->refs[i]);
free(symbol->refs);
}
static int symbol_order(const void *left, const void *right) {
const Symbol *a = left;
const Symbol *b = right;
int order = strcmp(a->key, b->key);
if (order) return order;
return a->hash < b->hash ? -1 : a->hash != b->hash;
}
static int symbol_has_ref(Symbol *symbol, const char *name) {
for (size_t i = 0; i < symbol->nrefs; i++)
if (!strcmp(symbol->refs[i], name)) return 1;
return 0;
}
static void test_extract(void) {
const char source[] =
"const alpha = { brace: /[{}]/g }, beta = (x) => x + 1;\n"
"const nested = (flag) => `x ${flag ? `yes ${alpha}` : `no`}`;\n"
"const slug = 'top';\n"
"const codes: Record<string, string> = { curp: 'invalid' };\n"
"export const mapped = codes as Record<string, unknown>;\n"
"export function Panel(id: string) { const slug = id; return <span data={{ slug: true }}>{document.getElementById(`output-${id}`)?.textContent}</span>; }\n"
"export async function run(x: number) { return alpha.brace.test(`${x}`); }\n"
"export interface Shape { x: number }\n"
"export default function () { return beta(2) }\n";
size_t count = 0;
Symbol *symbols = extract_symbols(source, sizeof source - 1, &count, 1);
qsort(symbols, count, sizeof *symbols, symbol_order);
const char *expected[] = {
"const alpha", "const beta", "const codes", "const mapped", "const nested", "const slug",
"function Panel", "function default", "function run", "interface Shape"
};
if (count != sizeof expected / sizeof *expected)
fail("self-test symbol count: got %zu, expected %zu", count, sizeof expected / sizeof *expected);
for (size_t i = 0; i < count; i++)
if (strcmp(symbols[i].key, expected[i]))
fail("self-test symbol %zu: got %s, expected %s", i, symbols[i].key, expected[i]);
if (!symbol_has_ref(&symbols[4], "alpha") || symbol_has_ref(&symbols[4], "flag"))
fail("self-test nested template references");
if (!symbol_has_ref(&symbols[6], "document") || symbol_has_ref(&symbols[6], "slug") ||
symbol_has_ref(&symbols[6], "getElementById"))
fail("self-test scoped TSX references");
for (size_t i = 0; i < count; i++) free_symbol(&symbols[i]);
free(symbols);
puts("self-test: extraction ok");
}
static void test_symbol_bags(void);
static void test_split_helpers(void);
static void run_repository(const char *path, int split);
static double build_graph(Analysis *analysis, Edges **graph_out, double **degree_out);
static size_t cluster_graph(Analysis *analysis, Edges *graph, double *degree, double graph_weight);
static void report_modules(Analysis *analysis, size_t modules);
static void report_split_candidates(Analysis *analysis, size_t modules);
static void report_extractable_communities(Analysis *analysis, size_t modules, int apply);
static void split_repository(Analysis *analysis, size_t modules);
static size_t module_prefix_end(const char *source, size_t len);
int main(int argc, char **argv) {
if (argc == 2 && (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help"))) {
printf("usage: %s [--report | --split] [REPOSITORY]\n", argv[0]);
puts("Report or apply a path-independent TS/TSX/MJS split from Git co-edit history.");
puts("--split replaces all repository TS/TSX/MJS sources with 50 named root TSX files.");
return 0;
}
if (argc == 2 && !strcmp(argv[1], "--self-test")) {
test_extract();
test_symbol_bags();
test_split_helpers();
return 0;
}
int split = argc > 1 && !strcmp(argv[1], "--split");
int report = argc > 1 && !strcmp(argv[1], "--report");
int path_arg = split || report ? 2 : 1;
if (argc > path_arg + 1 || (argc > 1 && argv[1][0] == '-' && !split && !report)) {
fprintf(stderr, "usage: %s [--report | --split] [REPOSITORY]\n", argv[0]);
return 2;
}
run_repository(argc > path_arg ? argv[path_arg] : ".", split);
return 0;
}
static int source_path(const char *path) {
size_t len = strlen(path);
return (len > 3 && !strcmp(path + len - 3, ".ts")) ||
(len > 4 && !strcmp(path + len - 4, ".tsx")) ||
(len > 4 && !strcmp(path + len - 4, ".mjs"));
}
static size_t module_root_stem_len(const char *path, size_t stem_len) {
if (stem_len < 11) return stem_len;
const char *suffix = path + stem_len - 11;
if (memcmp(suffix, ".module-", 8) ||
!isdigit((unsigned char)suffix[8]) ||
!isdigit((unsigned char)suffix[9]) ||
!isdigit((unsigned char)suffix[10]))
return stem_len;
return stem_len - 11;
}
static uint64_t hash_string(const char *text) {
uint64_t hash = 1469598103934665603ULL;
while (*text) {
hash ^= (unsigned char)*text++;
hash *= 1099511628211ULL;
}
return hash;
}
static void rebuild_names(Analysis *analysis, size_t capacity) {
NameSlot *old = analysis->names;
size_t oldcap = analysis->namecap;
analysis->names = calloc(capacity, sizeof *analysis->names);
if (!analysis->names) fail("out of memory building symbol index");
analysis->namecap = capacity;
for (size_t i = 0; i < oldcap; i++) {
if (!old[i].used) continue;
size_t slot = hash_string(old[i].key) & (capacity - 1);
while (analysis->names[slot].used) slot = (slot + 1) & (capacity - 1);
analysis->names[slot] = old[i];
}
free(old);
}
static uint32_t entity_index(Analysis *analysis, const char *key) {
if (!analysis->namecap) rebuild_names(analysis, 1024);
if ((analysis->nentities + 1) * 10 > analysis->namecap * 7)
rebuild_names(analysis, analysis->namecap * 2);
size_t slot = hash_string(key) & (analysis->namecap - 1);
while (analysis->names[slot].used) {
if (!strcmp(analysis->names[slot].key, key)) return analysis->names[slot].entity;
slot = (slot + 1) & (analysis->namecap - 1);
}
if (analysis->nentities == analysis->entitycap) {
analysis->entitycap = analysis->entitycap ? analysis->entitycap * 2 : 1024;
analysis->entities = resize(analysis->entities, analysis->entitycap * sizeof *analysis->entities);
}
uint32_t entity = (uint32_t)analysis->nentities++;
if ((size_t)entity != analysis->nentities - 1) fail("too many symbols");
char *name = copy_text(key, strlen(key));
char *space = strchr(name, ' ');
analysis->entities[entity] = (Entity){
name, copy_text(space ? space + 1 : name, strlen(space ? space + 1 : name)), 0, -1, 0, 0, 0
};
analysis->names[slot] = (NameSlot){name, entity, 1};
return entity;
}
static uint64_t hash_pair(uint32_t left, uint32_t right) {
uint64_t value = ((uint64_t)left << 32) | right;
value ^= value >> 33;
value *= 0xff51afd7ed558ccdULL;
value ^= value >> 33;
value *= 0xc4ceb9fe1a85ec53ULL;
return value ^ (value >> 33);
}
static void rebuild_pairs(Analysis *analysis, size_t capacity) {
PairSlot *old = analysis->pairs;
size_t oldcap = analysis->paircap;
analysis->pairs = calloc(capacity, sizeof *analysis->pairs);
if (!analysis->pairs) fail("out of memory building co-edit graph");
analysis->paircap = capacity;
for (size_t i = 0; i < oldcap; i++) {
if (!old[i].used) continue;
size_t slot = hash_pair(old[i].left, old[i].right) & (capacity - 1);
while (analysis->pairs[slot].used) slot = (slot + 1) & (capacity - 1);
analysis->pairs[slot] = old[i];
}
free(old);
}
static PairSlot *pair_slot(Analysis *analysis, uint32_t left, uint32_t right) {
if (left > right) {
uint32_t swap = left;
left = right;
right = swap;
}
if (!analysis->paircap) rebuild_pairs(analysis, 2048);
if ((analysis->npairs + 1) * 10 > analysis->paircap * 7)
rebuild_pairs(analysis, analysis->paircap * 2);
size_t slot = hash_pair(left, right) & (analysis->paircap - 1);
while (analysis->pairs[slot].used) {
if (analysis->pairs[slot].left == left && analysis->pairs[slot].right == right)
return &analysis->pairs[slot];
slot = (slot + 1) & (analysis->paircap - 1);
}
analysis->npairs++;
analysis->pairs[slot] = (PairSlot){left, right, 0, 0, 1};
return &analysis->pairs[slot];
}
typedef struct {
Symbol *items;
size_t count;
size_t cap;
} Symbols;
static void append_blob_symbols(
Analysis *analysis,
Symbols *out,
const git_oid *oid,
const char *path
) {
if (!source_path(path) || git_oid_is_zero(oid)) return;
git_blob *blob = NULL;
int code = git_blob_lookup(&blob, analysis->repo, oid);
if (code) git_fail("git_blob_lookup", code);
size_t count = 0;
char oid_text[GIT_OID_MAX_HEXSIZE + 1];
git_oid_tostr(oid_text, sizeof oid_text, oid);
snprintf(source_blob_context, sizeof source_blob_context, "%s@%s", path, oid_text);
source_path_context = source_blob_context;
Symbol *symbols = extract_symbols(git_blob_rawcontent(blob), git_blob_rawsize(blob), &count, 0);
git_blob_free(blob);
if (out->count + count > out->cap) {
out->cap = out->cap ? out->cap * 2 : 128;
while (out->cap < out->count + count) out->cap *= 2;
out->items = resize(out->items, out->cap * sizeof *out->items);
}
if (count) {
memcpy(out->items + out->count, symbols, count * sizeof *symbols);
out->count += count;
}
analysis->parsed_blobs++;
free(symbols);
}
static void free_symbols(Symbols *symbols) {
for (size_t i = 0; i < symbols->count; i++) free_symbol(&symbols->items[i]);
free(symbols->items);
}
static void record_changed(Analysis *analysis, uint32_t *changed, size_t count) {
if (!count) return;
analysis->mapped++;
analysis->multi += count > 1;
analysis->changed_symbols += count;
if (count > analysis->broadest) analysis->broadest = count;
for (size_t i = 0; i < count; i++) analysis->entities[changed[i]].edits++;
if (count < 2) return;
if (count > 512) {
analysis->broad_skipped++;
return;
}
double weight = 1.0 / (double)(count - 1);
for (size_t i = 0; i < count; i++)
for (size_t j = i + 1; j < count; j++) {
PairSlot *pair = pair_slot(analysis, changed[i], changed[j]);
pair->shared++;
pair->weight += weight;
}
}
static void compare_symbol_bags(Analysis *analysis, Symbols *old, Symbols *newer) {
qsort(old->items, old->count, sizeof *old->items, symbol_order);
qsort(newer->items, newer->count, sizeof *newer->items, symbol_order);
uint32_t *changed = resize(NULL, (old->count + newer->count) * sizeof *changed);
size_t used = 0;
size_t left = 0;
size_t right = 0;
while (left < old->count || right < newer->count) {
const char *key = left < old->count ? old->items[left].key : newer->items[right].key;
if (right < newer->count &&
(left == old->count || strcmp(newer->items[right].key, key) < 0))
key = newer->items[right].key;
size_t left_end = left;
size_t right_end = right;
while (left_end < old->count && !strcmp(old->items[left_end].key, key)) left_end++;
while (right_end < newer->count && !strcmp(newer->items[right_end].key, key)) right_end++;
int differs = left_end - left != right_end - right;
for (size_t i = 0; !differs && i < left_end - left; i++)
differs = old->items[left + i].hash != newer->items[right + i].hash;
if (differs) changed[used++] = entity_index(analysis, key);
left = left_end;
right = right_end;
}
record_changed(analysis, changed, used);
free(changed);
}
static void append_test_source(Symbols *bag, const char *source) {
size_t count = 0;
Symbol *symbols = extract_symbols(source, strlen(source), &count, 0);
bag->items = resize(bag->items, (bag->count + count) * sizeof *bag->items);
if (count) memcpy(bag->items + bag->count, symbols, count * sizeof *symbols);
bag->count += count;
bag->cap = bag->count;
free(symbols);
}
static void test_symbol_bags(void) {
Symbols old = {0};
Symbols moved = {0};
Symbols edited = {0};
append_test_source(&old, "export const alpha = () => 1;");
append_test_source(&old, "export const beta = () => 2;");
append_test_source(&moved, "export const beta = () => 2;");
append_test_source(&moved, "export const alpha = () => 1;");
append_test_source(&edited, "export const beta = () => 3;");
append_test_source(&edited, "export const alpha = () => 1;");
Analysis analysis = {0};
compare_symbol_bags(&analysis, &old, &moved);
if (analysis.mapped || analysis.changed_symbols)
fail("self-test path-independent symbol move");
compare_symbol_bags(&analysis, &old, &edited);
if (analysis.mapped != 1 || analysis.changed_symbols != 1 ||
analysis.nentities != 1 || strcmp(analysis.entities[0].name, "const beta"))
fail("self-test path-independent symbol edit");
free_symbols(&old);
free_symbols(&moved);
free_symbols(&edited);
for (size_t i = 0; i < analysis.nentities; i++) {
free(analysis.entities[i].name);
free(analysis.entities[i].ident);
}
free(analysis.entities);
free(analysis.names);
free(analysis.pairs);
puts("self-test: path-independent bags ok");
}
static void analyze_commit(Analysis *analysis, git_commit *commit) {
if (git_commit_parentcount(commit) != 1) {
analysis->merges += git_commit_parentcount(commit) > 1;
return;
}
git_commit *parent = NULL;
git_tree *old_tree = NULL;
git_tree *new_tree = NULL;
git_diff *diff = NULL;
int code = git_commit_parent(&parent, commit, 0);
if (code) git_fail("git_commit_parent", code);
if ((code = git_commit_tree(&old_tree, parent))) git_fail("git_commit_tree(parent)", code);
if ((code = git_commit_tree(&new_tree, commit))) git_fail("git_commit_tree(commit)", code);
if ((code = git_diff_tree_to_tree(&diff, analysis->repo, old_tree, new_tree, NULL)))
git_fail("git_diff_tree_to_tree", code);
Symbols old = {0};
Symbols newer = {0};
size_t deltas = git_diff_num_deltas(diff);
for (size_t i = 0; i < deltas; i++) {
const git_diff_delta *delta = git_diff_get_delta(diff, i);
int relevant = source_path(delta->old_file.path) || source_path(delta->new_file.path);
if (!relevant) continue;
analysis->changed_files++;
append_blob_symbols(analysis, &old, &delta->old_file.id, delta->old_file.path);
append_blob_symbols(analysis, &newer, &delta->new_file.id, delta->new_file.path);
}
compare_symbol_bags(analysis, &old, &newer);
free_symbols(&old);
free_symbols(&newer);
git_diff_free(diff);
git_tree_free(old_tree);
git_tree_free(new_tree);
git_commit_free(parent);
}
static int load_current_file(
const char *root,
const git_tree_entry *entry,
void *payload
) {
Analysis *analysis = payload;
if (git_tree_entry_type(entry) != GIT_OBJECT_BLOB) return 0;
size_t len = strlen(root) + strlen(git_tree_entry_name(entry));
char *path = resize(NULL, len + 1);
snprintf(path, len + 1, "%s%s", root, git_tree_entry_name(entry));
if (!source_path(path)) {
free(path);
return 0;
}
git_blob *blob = NULL;
int code = git_blob_lookup(&blob, analysis->repo, git_tree_entry_id(entry));
if (code) git_fail("git_blob_lookup(HEAD)", code);
size_t count = 0;
size_t source_len = git_blob_rawsize(blob);
char *source = copy_text(git_blob_rawcontent(blob), source_len);
source_path_context = path;
Symbol *symbols = extract_symbols(source, source_len, &count, 1);
git_blob_free(blob);
if (analysis->nfiles == analysis->filecap) {
analysis->filecap = analysis->filecap ? analysis->filecap * 2 : 1024;
analysis->files = resize(analysis->files, analysis->filecap * sizeof *analysis->files);
}
analysis->files[analysis->nfiles++] = (File){path, source, source_len, symbols, count};
for (size_t i = 0; i < count; i++) {
uint32_t entity = entity_index(analysis, symbols[i].key);
analysis->entities[entity].current = 1;
analysis->entities[entity].symbols++;
analysis->entities[entity].bytes += symbols[i].end - symbols[i].start;
}
return 0;
}
static void load_current(Analysis *analysis, git_commit *head) {
git_tree *tree = NULL;
int code = git_commit_tree(&tree, head);
if (code) git_fail("git_commit_tree(HEAD)", code);
code = git_tree_walk(tree, GIT_TREEWALK_PRE, load_current_file, analysis);
if (code) git_fail("git_tree_walk", code);
git_tree_free(tree);
}
static void scan_history(Analysis *analysis) {
git_revwalk *walk = NULL;
int code = git_revwalk_new(&walk, analysis->repo);
if (code) git_fail("git_revwalk_new", code);
git_revwalk_sorting(walk, GIT_SORT_TOPOLOGICAL | GIT_SORT_TIME | GIT_SORT_REVERSE);
if ((code = git_revwalk_push_head(walk))) git_fail("git_revwalk_push_head", code);
git_oid oid;
time_t last_status = time(NULL);
while ((code = git_revwalk_next(&oid, walk)) == 0) {
git_commit *commit = NULL;
if ((code = git_commit_lookup(&commit, analysis->repo, &oid)))
git_fail("git_commit_lookup", code);
analysis->commits++;
analyze_commit(analysis, commit);
git_commit_free(commit);
if (time(NULL) - last_status >= 10) {
fprintf(
stderr,
"scan: commits=%zu mapped=%zu blobs=%zu pairs=%zu\n",
analysis->commits,
analysis->mapped,
analysis->parsed_blobs,
analysis->npairs
);
last_status = time(NULL);
}
}
if (code != GIT_ITEROVER) git_fail("git_revwalk_next", code);
git_revwalk_free(walk);
}
static void free_analysis(Analysis *analysis, Edges *graph, double *degree) {
for (size_t i = 0; i < analysis->nentities; i++) free(graph[i].items);
free(graph);
free(degree);
for (size_t i = 0; i < analysis->nfiles; i++) {
File *file = &analysis->files[i];
free(file->path);
free(file->source);
for (size_t j = 0; j < file->count; j++) free_symbol(&file->symbols[j]);
free(file->symbols);
}
free(analysis->files);
for (size_t i = 0; i < analysis->nentities; i++) {
free(analysis->entities[i].name);
free(analysis->entities[i].ident);
}
free(analysis->entities);
free(analysis->names);
free(analysis->pairs);
free(analysis->subject);
}
static void run_repository(const char *path, int split) {
git_libgit2_init();
Analysis analysis = {0};
int code = git_repository_open_ext(&analysis.repo, path, 0, NULL);
if (code) git_fail("git_repository_open_ext", code);
git_oid head_id;
if ((code = git_reference_name_to_id(&head_id, analysis.repo, "HEAD")))
git_fail("git_reference_name_to_id(HEAD)", code);
git_commit *head = NULL;
if ((code = git_commit_lookup(&head, analysis.repo, &head_id)))
git_fail("git_commit_lookup(HEAD)", code);
git_oid_tostr(analysis.head, sizeof analysis.head, &head_id);
analysis.subject = copy_text(git_commit_summary(head), strlen(git_commit_summary(head)));
scan_history(&analysis);
load_current(&analysis, head);
Edges *graph = NULL;
double *degree = NULL;
double graph_weight = build_graph(&analysis, &graph, &degree);
size_t modules = cluster_graph(&analysis, graph, degree, graph_weight);
printf("TS/TSX/MJS HISTORY SPLIT REPORT\n");
printf("repository=%s endpoint=%.12s subject=%s\n", path, analysis.head, analysis.subject);
printf(
"commits=%zu merges_skipped=%zu mapped=%zu multi_symbol=%zu broad_skipped=%zu\n",
analysis.commits,
analysis.merges,
analysis.mapped,
analysis.multi,
analysis.broad_skipped
);
printf(
"current_files=%zu historical_symbols=%zu changed_files=%zu parsed_blobs=%zu coedit_pairs=%zu broadest=%zu\n",
analysis.nfiles,
analysis.nentities,
analysis.changed_files,
analysis.parsed_blobs,
analysis.npairs,
analysis.broadest
);
printf(
"graph_weight=%.2f repulsion=%.9g modules=%zu median_module_bytes=%zu target_bytes=%u\n",
graph_weight,
analysis.repulsion,
modules,
analysis.median_bytes,
20 * 1024
);
if (!split) report_modules(&analysis, modules);
if (split) split_repository(&analysis, modules);
if (getenv("HOW_SPLIT_VERBOSE")) {
report_split_candidates(&analysis, modules);
report_extractable_communities(&analysis, modules, 0);
}
free_analysis(&analysis, graph, degree);
git_commit_free(head);
git_repository_free(analysis.repo);
git_libgit2_shutdown();
}
static double build_graph(Analysis *analysis, Edges **graph_out, double **degree_out) {
Edges *graph = calloc(analysis->nentities, sizeof *graph);
double *degree = calloc(analysis->nentities, sizeof *degree);
if (!graph || !degree) fail("out of memory building sparse graph");
double total = 0;
for (size_t i = 0; i < analysis->paircap; i++) {
PairSlot pair = analysis->pairs[i];
if (!pair.used) continue;
Entity *left = &analysis->entities[pair.left];
Entity *right = &analysis->entities[pair.right];
if (!left->current || !right->current || left->symbols != 1 || right->symbols != 1)
continue;
Edges *a = &graph[pair.left];
Edges *b = &graph[pair.right];
if (a->count == a->cap) {
a->cap = a->cap ? a->cap * 2 : 8;
a->items = resize(a->items, a->cap * sizeof *a->items);
}
if (b->count == b->cap) {
b->cap = b->cap ? b->cap * 2 : 8;
b->items = resize(b->items, b->cap * sizeof *b->items);
}
a->items[a->count++] = (Edge){pair.right, pair.weight, pair.shared};
b->items[b->count++] = (Edge){pair.left, pair.weight, pair.shared};
degree[pair.left] += pair.weight;
degree[pair.right] += pair.weight;
total += pair.weight;
}
if (total == 0.0) fail("not enough current-symbol co-edits to propose a refactor");
*graph_out = graph;
*degree_out = degree;
return total;
}
static PairSlot *merge_cross_slot(
PairSlot *cross,
size_t capacity,
uint32_t left,
uint32_t right
) {
if (left > right) {
uint32_t swap = left;
left = right;
right = swap;
}
size_t slot = hash_pair(left, right) & (capacity - 1);
while (cross[slot].used) {
if (cross[slot].left == left && cross[slot].right == right)
return &cross[slot];
slot = (slot + 1) & (capacity - 1);
}
cross[slot] = (PairSlot){left, right, 0, 0, 1};
return &cross[slot];
}
static void build_merge_cross(
Analysis *analysis,
Edges *graph,
PairSlot *cross,
size_t capacity
) {
memset(cross, 0, capacity * sizeof *cross);
for (size_t i = 0; i < analysis->nentities; i++)
for (size_t e = 0; e < graph[i].count; e++) {
uint32_t other = graph[i].items[e].other;
if (i >= other) continue;
int left = analysis->entities[i].module;
int right = analysis->entities[other].module;
if (left < 0 || right < 0 || left == right) continue;
PairSlot *pair = merge_cross_slot(
cross,
capacity,
(uint32_t)left,
(uint32_t)right
);
pair->weight += graph[i].items[e].weight;
pair->shared++;
}
}
static size_t merge_modules(
Analysis *analysis,
Edges *graph,
size_t *members,
size_t *bytes,
double repulsion
) {
size_t capacity = analysis->paircap ? analysis->paircap : 16;
PairSlot *cross = calloc(capacity, sizeof *cross);
if (!cross) fail("out of memory merging modules");
size_t merged = 0;
for (;;) {
build_merge_cross(analysis, graph, cross, capacity);
int best_left = -1;
int best_right = -1;
double best_gain = 1e-12;
for (size_t i = 0; i < capacity; i++) {
if (!cross[i].used || !members[cross[i].left] || !members[cross[i].right])
continue;
double pairs =
(double)members[cross[i].left] * members[cross[i].right];
double gain = cross[i].weight - repulsion * pairs;
if (gain <= best_gain) continue;
best_gain = gain;
best_left = (int)cross[i].left;
best_right = (int)cross[i].right;
}
if (best_left < 0) break;
for (size_t i = 0; i < analysis->nentities; i++)
if (analysis->entities[i].module == best_right)
analysis->entities[i].module = best_left;
members[best_left] += members[best_right];
members[best_right] = 0;
bytes[best_left] += bytes[best_right];
bytes[best_right] = 0;
merged++;
}
free(cross);
return merged;
}
static int size_order(const void *left, const void *right) {
size_t a = *(const size_t *)left;
size_t b = *(const size_t *)right;
return a < b ? -1 : a != b;
}
static size_t cluster_once(
Analysis *analysis,
Edges *graph,
double repulsion,
size_t *median_bytes
) {
size_t n = analysis->nentities;
size_t *members = calloc(n, sizeof *members);
size_t *bytes = calloc(n, sizeof *bytes);
size_t *linked = calloc(n, sizeof *linked);
double *toward = calloc(n, sizeof *toward);
uint32_t *touched = resize(NULL, n * sizeof *touched);
if (!members || !bytes || !linked || !toward)
fail("out of memory clustering graph");
for (size_t i = 0; i < n; i++) {
analysis->entities[i].module = -1;
if (!graph[i].count) continue;
analysis->entities[i].module = (int)i;
members[i] = 1;
bytes[i] = analysis->entities[i].bytes;
}
for (size_t pass = 0; pass < 100; pass++) {
size_t moved = 0;
for (size_t i = 0; i < n; i++) {
if (analysis->entities[i].module < 0) continue;
int old = analysis->entities[i].module;
members[old]--;
bytes[old] -= analysis->entities[i].bytes;
size_t used = 0;
for (size_t e = 0; e < graph[i].count; e++) {
int module = analysis->entities[graph[i].items[e].other].module;
if (module < 0) continue;
if (!linked[module]) touched[used++] = (uint32_t)module;
linked[module]++;
toward[module] += graph[i].items[e].weight;
}
int best = old;
double best_score = toward[old] - repulsion * members[old];
for (size_t j = 0; j < used; j++) {
int module = (int)touched[j];
double score = toward[module] - repulsion * members[module];
if (score > best_score + 1e-12 ||
(score > best_score - 1e-12 && module < best)) {
best = module;
best_score = score;
}
}
for (size_t j = 0; j < used; j++) {
toward[touched[j]] = 0;
linked[touched[j]] = 0;
}
analysis->entities[i].module = best;
members[best]++;
bytes[best] += analysis->entities[i].bytes;
moved += best != old;
}
if (!moved) break;
}
merge_modules(analysis, graph, members, bytes, repulsion);
int *dense = resize(NULL, n * sizeof *dense);
size_t *sizes = resize(NULL, n * sizeof *sizes);
size_t modules = 0;
for (size_t i = 0; i < n; i++) {
dense[i] = members[i] ? (int)modules++ : -1;
if (members[i]) sizes[dense[i]] = bytes[i];
}
for (size_t i = 0; i < n; i++)
if (analysis->entities[i].module >= 0)
analysis->entities[i].module = dense[analysis->entities[i].module];
qsort(sizes, modules, sizeof *sizes, size_order);
size_t total_bytes = 0;
for (size_t i = 0; i < modules; i++) total_bytes += sizes[i];
size_t cumulative_bytes = 0;
*median_bytes = 0;
for (size_t i = 0; i < modules; i++) {
cumulative_bytes += sizes[i];
if (cumulative_bytes * 2 < total_bytes) continue;
*median_bytes = sizes[i];
break;
}
free(sizes);
free(dense);
free(members);
free(bytes);
free(linked);
free(toward);
free(touched);
return modules;
}
static size_t cluster_graph(
Analysis *analysis,
Edges *graph,
double *degree,
double graph_weight
) {
(void)degree;
(void)graph_weight;
const size_t target = 20 * 1024;
int *best = resize(NULL, analysis->nentities * sizeof *best);
size_t best_median = 0;
size_t best_modules = cluster_once(analysis, graph, 0, &best_median);
double best_repulsion = 0;
for (size_t i = 0; i < analysis->nentities; i++)
best[i] = analysis->entities[i].module;
if (best_median > target) {
double low = 0;
double high = 0.000001;
size_t median = best_median;
for (size_t i = 0; i < 60 && median > target; i++) {
low = high;
high *= 2;
size_t modules = cluster_once(analysis, graph, high, &median);
if (getenv("HOW_SPLIT_TRACE"))
fprintf(stderr, "repulsion=%.9g modules=%zu weighted_median_bytes=%zu\n", high, modules, median);
size_t distance = median > target ? median - target : target - median;
size_t best_distance =
best_median > target ? best_median - target : target - best_median;
if (distance < best_distance) {
best_median = median;
best_modules = modules;
best_repulsion = high;
for (size_t j = 0; j < analysis->nentities; j++)
best[j] = analysis->entities[j].module;
}
}
for (size_t i = 0; i < 18; i++) {
double middle = (low + high) / 2;
size_t median;
size_t modules = cluster_once(analysis, graph, middle, &median);
if (getenv("HOW_SPLIT_TRACE"))
fprintf(stderr, "repulsion=%.9g modules=%zu weighted_median_bytes=%zu\n", middle, modules, median);
size_t distance = median > target ? median - target : target - median;
size_t best_distance =
best_median > target ? best_median - target : target - best_median;
if (distance < best_distance) {
best_median = median;
best_modules = modules;
best_repulsion = middle;
for (size_t j = 0; j < analysis->nentities; j++)
best[j] = analysis->entities[j].module;
}
if (median > target) low = middle;
else high = middle;
}
}
for (size_t i = 0; i < analysis->nentities; i++)
analysis->entities[i].module = best[i];
analysis->repulsion = best_repulsion;
analysis->median_bytes = best_median;
free(best);
return best_modules;
}
typedef struct {
size_t module;
size_t symbols;
unsigned edits;
} ModuleRow;
static int module_row_order(const void *left, const void *right) {
const ModuleRow *a = left;
const ModuleRow *b = right;
if (a->edits != b->edits) return a->edits < b->edits ? 1 : -1;
if (a->symbols != b->symbols) return a->symbols < b->symbols ? 1 : -1;
return a->module < b->module ? -1 : a->module != b->module;
}
static void print_module_chunk(
Analysis *analysis,
ModuleRow row,
size_t *files,
size_t part,
size_t first,
size_t count,
size_t bytes
) {
printf(
"\nfile_%03zu community=%zu part=%zu symbols=%zu bytes=%zu edits=%u%s\n",
++*files, row.module + 1, part, count, bytes, row.edits,
bytes > 20 * 1024 ? " oversized_symbol=1" : ""
);
size_t ordinal = 0;
for (size_t i = 0; i < analysis->nentities; i++) {
Entity *entity = &analysis->entities[i];
if (entity->module != (int)row.module ||
!entity->current || entity->symbols != 1)
continue;
if (ordinal >= first && ordinal < first + count)
printf(" %s\n", entity->name);
ordinal++;
}
}
static void report_modules(Analysis *analysis, size_t modules) {
if (!modules) return;
ModuleRow *rows = calloc(modules, sizeof *rows);
if (!rows) fail("out of memory reporting modules");
for (size_t i = 0; i < modules; i++) rows[i].module = i;
for (size_t i = 0; i < analysis->nentities; i++) {
Entity *entity = &analysis->entities[i];
if (entity->module < 0 || !entity->current || entity->symbols != 1) continue;
rows[entity->module].symbols++;
rows[entity->module].edits += entity->edits;
}
qsort(rows, modules, sizeof *rows, module_row_order);
printf("\nPROPOSED SYMBOL FILES\n");
size_t files = 0;
size_t unclustered = 0;
for (size_t row = 0; row < modules; row++) {
if (rows[row].symbols < 2) {
unclustered += rows[row].symbols;
continue;
}
size_t first = 0;
size_t count = 0;
size_t bytes = 0;
size_t part = 1;
for (size_t i = 0; i < analysis->nentities; i++) {
Entity *entity = &analysis->entities[i];
if (entity->module != (int)rows[row].module ||
!entity->current || entity->symbols != 1)
continue;
if (count && bytes + entity->bytes > 20 * 1024) {
print_module_chunk(&analysis[0], rows[row], &files, part++, first, count, bytes);
first += count;
count = 0;
bytes = 0;
}
count++;
bytes += entity->bytes;
}
if (count)
print_module_chunk(&analysis[0], rows[row], &files, part, first, count, bytes);
}
printf("\nproposed_files=%zu unclustered_symbols=%zu\n", files, unclustered);
free(rows);
}
typedef struct {
size_t file;
size_t placed;
size_t distinct;
size_t first;
size_t second;
unsigned first_edits;
unsigned second_edits;
uint64_t score;
} FileRow;
static int file_row_order(const void *left, const void *right) {
const FileRow *a = left;
const FileRow *b = right;
if (a->score != b->score) return a->score < b->score ? 1 : -1;
if (a->placed != b->placed) return a->placed < b->placed ? 1 : -1;
return a->file < b->file ? -1 : a->file != b->file;
}
static void report_split_candidates(Analysis *analysis, size_t modules) {
if (!modules) return;
FileRow *rows = resize(NULL, analysis->nfiles * sizeof *rows);
size_t used = 0;
size_t *counts = calloc(modules, sizeof *counts);
unsigned *edits = calloc(modules, sizeof *edits);
if (!counts || !edits) fail("out of memory ranking split candidates");
for (size_t f = 0; f < analysis->nfiles; f++) {
memset(counts, 0, modules * sizeof *counts);
memset(edits, 0, modules * sizeof *edits);
size_t placed = 0;
for (size_t s = 0; s < analysis->files[f].count; s++) {
uint32_t entity = entity_index(analysis, analysis->files[f].symbols[s].key);
int module = analysis->entities[entity].module;
if (module < 0 || analysis->entities[entity].symbols != 1) continue;
counts[module]++;
edits[module] += analysis->entities[entity].edits;
placed++;
}
size_t first = SIZE_MAX;
size_t second = SIZE_MAX;
size_t distinct = 0;
for (size_t module = 0; module < modules; module++) {
if (!counts[module]) continue;
distinct++;
if (first == SIZE_MAX || edits[module] > edits[first]) {
second = first;
first = module;
} else if (second == SIZE_MAX || edits[module] > edits[second]) {
second = module;
}
}
if (second == SIZE_MAX || counts[first] < 2 || counts[second] < 2 ||
edits[first] < 4 || edits[second] < 4) continue;
rows[used++] = (FileRow){
f,
placed,
distinct,
first,
second,
edits[first],
edits[second],
(uint64_t)edits[second] * counts[second] * placed
};
}
qsort(rows, used, sizeof *rows, file_row_order);
printf("\nPROPOSED FILE SPLITS\n");
for (size_t r = 0; r < used && r < 20; r++) {
FileRow row = rows[r];
File *file = &analysis->files[row.file];
printf(
"\n%s symbols=%zu placed=%zu communities=%zu split_score=%llu\n",
file->path,
file->count,
row.placed,
row.distinct,
(unsigned long long)row.score
);
for (size_t module = 0; module < modules; module++) {
size_t count = 0;
unsigned body_edits = 0;
for (size_t s = 0; s < file->count; s++) {
uint32_t entity = entity_index(analysis, file->symbols[s].key);
if (analysis->entities[entity].module != (int)module) continue;
count++;
body_edits += analysis->entities[entity].edits;
}
if (!count) continue;
printf(" module_%03zu symbols=%zu edits=%u\n", module + 1, count, body_edits);
for (size_t s = 0; s < file->count; s++) {
Symbol *symbol = &file->symbols[s];
uint32_t entity = entity_index(analysis, symbol->key);
if (analysis->entities[entity].module != (int)module) continue;
printf(
" %3u line=%u %s\n",
analysis->entities[entity].edits,
symbol->line,
symbol->key
);
}
}
}
printf("\nshowing %zu of %zu files with two substantial history communities\n", used < 20 ? used : 20, used);
free(counts);
free(edits);
free(rows);
}
typedef struct {
size_t file;
size_t module;
size_t members;
size_t span_symbols;
unsigned edits;
size_t internal_refs;
size_t cut_refs;
size_t first;
size_t last;
uint64_t score;
} ExtractRow;
static int extract_row_order(const void *left, const void *right) {
const ExtractRow *a = left;
const ExtractRow *b = right;
if (a->score != b->score) return a->score < b->score ? 1 : -1;
if (a->edits != b->edits) return a->edits < b->edits ? 1 : -1;
return a->file < b->file ? -1 : a->file != b->file;
}
static int symbol_has_ident(Symbol *symbol, const char *ident) {
return !strcmp(symbol->ident, ident);
}
static void apply_extract(Analysis *analysis, ExtractRow row);
static void report_extractable_communities(Analysis *analysis, size_t modules, int apply) {
ExtractRow *rows = NULL;
size_t used = 0;
size_t cap = 0;
for (size_t f = 0; f < analysis->nfiles; f++) {
File *file = &analysis->files[f];
for (size_t module = 0; module < modules; module++) {
size_t members = 0;
size_t first = SIZE_MAX;
size_t last = 0;
unsigned edits = 0;
for (size_t s = 0; s < file->count; s++) {
uint32_t entity = entity_index(analysis, file->symbols[s].key);
if (analysis->entities[entity].module != (int)module) continue;
members++;
edits += analysis->entities[entity].edits;
if (first == SIZE_MAX || file->symbols[s].start < file->symbols[first].start) first = s;
if (members == 1 || file->symbols[s].end > file->symbols[last].end) last = s;
}
if (members < 3 || edits < 8) continue;
int expanded;
do {
expanded = 0;
size_t begin = file->symbols[first].start;
size_t end = file->symbols[last].end;
for (size_t s = 0; s < file->count; s++) {
Symbol *from = &file->symbols[s];
if (from->start < begin || from->end > end) continue;
for (size_t r = 0; r < from->nrefs; r++)
for (size_t t = 0; t < file->count; t++) {
if (!symbol_has_ident(&file->symbols[t], from->refs[r])) continue;
if (file->symbols[t].start < file->symbols[first].start) {
first = t;
expanded = 1;
}
if (file->symbols[t].end > file->symbols[last].end) {
last = t;
expanded = 1;
}
break;
}
}
} while (expanded);
size_t span_symbols = 0;
size_t internal_refs = 0;
size_t cut_refs = 0;
size_t begin = file->symbols[first].start;
size_t end = file->symbols[last].end;
size_t move_first = SIZE_MAX;
for (size_t s = 0; s < file->count; s++) {
Symbol *from = &file->symbols[s];
int from_span = from->start >= begin && from->end <= end;
if (from_span) {
span_symbols++;
if (move_first == SIZE_MAX) move_first = s;
}
for (size_t r = 0; r < from->nrefs; r++) {
int target_local = 0;
int target_span = 0;
for (size_t t = 0; t < file->count; t++) {
if (!symbol_has_ident(&file->symbols[t], from->refs[r])) continue;
target_local = 1;
target_span =
file->symbols[t].start >= begin &&
file->symbols[t].end <= end;
break;
}
if (!target_local) continue;
if (from_span && target_span) internal_refs++;
else if (from_span != target_span) cut_refs++;
}
}
if (move_first == SIZE_MAX ||
file->count - span_symbols < 3 ||
members * 2 < span_symbols)
continue;
size_t span_lines =
file->symbols[last].end_line -
file->symbols[move_first].line + 1;
uint64_t score =
(uint64_t)edits * members * members *
(internal_refs + 1) * (internal_refs + 1) * 1000000 /
(span_symbols * span_symbols *
(cut_refs + 1) * (cut_refs + 1) * span_lines);
if (used == cap) {
cap = cap ? cap * 2 : 128;
rows = resize(rows, cap * sizeof *rows);
}
rows[used++] = (ExtractRow){
f, module, members, span_symbols, edits, internal_refs, cut_refs,
move_first, last, score
};
}
}
qsort(rows, used, sizeof *rows, extract_row_order);
printf("\nEXTRACTABLE HISTORY COMMUNITIES\n");
for (size_t i = 0; i < used && i < 10; i++) {
ExtractRow row = rows[i];
File *file = &analysis->files[row.file];
printf(
"\n%s module_%03zu members=%zu span_symbols=%zu edits=%u internal_refs=%zu cut_refs=%zu score=%llu lines=%u-%u\n",
file->path,
row.module + 1,
row.members,
row.span_symbols,
row.edits,
row.internal_refs,
row.cut_refs,
(unsigned long long)row.score,
file->symbols[row.first].line,
file->symbols[row.last].end_line
);
size_t begin = file->symbols[row.first].start;
size_t end = file->symbols[row.last].end;
for (size_t s = 0; s < file->count; s++) {
Symbol *symbol = &file->symbols[s];
if (symbol->start < begin || symbol->end > end) continue;
uint32_t entity = entity_index(analysis, symbol->key);
printf(
" %c %3u line=%u %s\n",
analysis->entities[entity].module == (int)row.module ? '*' : '-',
analysis->entities[entity].edits,
symbol->line,
symbol->key
);
}
}
printf("\nshowing %zu of %zu cohesive file/community spans\n", used < 10 ? used : 10, used);
if (apply && used) {
unsigned char *done = calloc(analysis->nfiles, 1);
if (!done) fail("out of memory tracking applied files");
for (size_t i = 0; i < used; i++) {
if (done[rows[i].file]) continue;
done[rows[i].file] = 1;
apply_extract(analysis, rows[i]);
}
free(done);
}
free(rows);
}
static void write_file(const char *path, const char *data, size_t len) {
FILE *out = fopen(path, "wb");
if (!out) fail("fopen %s failed: %s", path, strerror(errno));
if (fwrite(data, 1, len, out) != len) {
int code = errno;
fclose(out);
fail("fwrite %s failed: %s", path, strerror(code));
}
if (fclose(out)) fail("fclose %s failed: %s", path, strerror(errno));
}
typedef struct {
size_t file;
size_t start;
size_t end;
size_t symbols;
size_t chunk;
int module;
uint32_t leader;
} SplitUnit;
typedef struct {
char *data;
char *path;
size_t len;
size_t cap;
size_t symbols;
size_t source_bytes;
uint32_t leaders[8];
size_t nleaders;
} SplitChunk;
typedef struct {
int module;
size_t unit;
size_t bytes;
uint32_t leader;
} SplitGroup;
static int find_entity(Analysis *analysis, const char *key) {
if (!analysis->namecap) return -1;
size_t slot = hash_string(key) & (analysis->namecap - 1);
while (analysis->names[slot].used) {
if (!strcmp(analysis->names[slot].key, key))
return (int)analysis->names[slot].entity;
slot = (slot + 1) & (analysis->namecap - 1);
}
return -1;
}
static char *read_file(const char *path, size_t *len) {
FILE *in = fopen(path, "rb");
if (!in) fail("fopen %s failed: %s", path, strerror(errno));
if (fseek(in, 0, SEEK_END)) fail("fseek %s failed: %s", path, strerror(errno));
long size = ftell(in);
if (size < 0) fail("ftell %s failed: %s", path, strerror(errno));
rewind(in);
char *data = resize(NULL, (size_t)size + 1);
if (fread(data, 1, (size_t)size, in) != (size_t)size)
fail("fread %s failed: %s", path, strerror(errno));
if (fclose(in)) fail("fclose %s failed: %s", path, strerror(errno));
data[size] = 0;
*len = (size_t)size;
return data;
}
static void append_chunk(SplitChunk *chunk, const char *data, size_t len) {
if (chunk->len + len + 1 > chunk->cap) {
chunk->cap = chunk->cap ? chunk->cap * 2 : 32768;
while (chunk->cap < chunk->len + len + 1) chunk->cap *= 2;
chunk->data = resize(chunk->data, chunk->cap);
}
memcpy(chunk->data + chunk->len, data, len);
chunk->len += len;
chunk->data[chunk->len] = 0;
}
static int split_group_order(const void *left, const void *right) {
const SplitGroup *a = left;
const SplitGroup *b = right;
if (a->bytes != b->bytes) return a->bytes < b->bytes ? 1 : -1;
if (a->module != b->module) return a->module < b->module ? -1 : 1;
return a->unit < b->unit ? -1 : a->unit != b->unit;
}
static void add_chunk_leader(SplitChunk *chunk, Analysis *analysis, uint32_t leader) {
if (leader == UINT32_MAX) return;
for (size_t i = 0; i < chunk->nleaders; i++)
if (chunk->leaders[i] == leader) return;
size_t at = chunk->nleaders;
if (at < 8) chunk->nleaders++;
else at = 7;
while (at && analysis->entities[chunk->leaders[at - 1]].edits <
analysis->entities[leader].edits) {
if (at < 8) chunk->leaders[at] = chunk->leaders[at - 1];
at--;
}
chunk->leaders[at] = leader;
}
static void validate_split_inputs(Analysis *analysis) {
const char *workdir = git_repository_workdir(analysis->repo);
if (!workdir) fail("cannot split a bare repository");
for (size_t i = 0; i < analysis->nfiles; i++) {
File *file = &analysis->files[i];
char *path = resize(NULL, strlen(workdir) + strlen(file->path) + 1);
sprintf(path, "%s%s", workdir, file->path);
size_t len = 0;
char *data = read_file(path, &len);
if (len != file->len || memcmp(data, file->source, len))
fail("refusing to replace modified source: %s", file->path);
free(data);
free(path);
}
git_status_options options;
int code = git_status_options_init(&options, GIT_STATUS_OPTIONS_VERSION);
if (code) git_fail("git_status_options_init", code);
options.flags =
GIT_STATUS_OPT_INCLUDE_UNTRACKED |
GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS;
git_status_list *statuses = NULL;
code = git_status_list_new(&statuses, analysis->repo, &options);
if (code) git_fail("git_status_list_new", code);
size_t count = git_status_list_entrycount(statuses);
for (size_t i = 0; i < count; i++) {
const git_status_entry *entry = git_status_byindex(statuses, i);
const char *path = entry->index_to_workdir
? entry->index_to_workdir->new_file.path
: entry->head_to_index ? entry->head_to_index->new_file.path : NULL;
if (path && source_path(path))
fail("refusing to replace changed or untracked source: %s status=%u", path, entry->status);
}
git_status_list_free(statuses);
}
static void add_split_unit(
SplitUnit **units,
size_t *count,
size_t *capacity,
SplitUnit unit
) {
if (*count == *capacity) {
*capacity = *capacity ? *capacity * 2 : 4096;
*units = resize(*units, *capacity * sizeof **units);
}
(*units)[(*count)++] = unit;
}
static SplitUnit *collect_split_units(
Analysis *analysis,
size_t *count,
size_t *source_bytes,
size_t *source_symbols
) {
SplitUnit *units = NULL;
size_t capacity = 0;
for (size_t f = 0; f < analysis->nfiles; f++) {
File *file = &analysis->files[f];
*source_bytes += file->len;
*source_symbols += file->count;
size_t cursor = 0;
for (size_t i = 0; i < file->count;) {
size_t next = i + 1;
while (next < file->count &&
file->symbols[next].start == file->symbols[i].start &&
file->symbols[next].end == file->symbols[i].end)
next++;
if (file->symbols[i].start < cursor)
fail("overlapping symbol spans at byte %zu in %s", file->symbols[i].start, file->path);
int module = -2;
uint32_t leader = UINT32_MAX;
for (size_t j = i; j < next; j++) {
int entity = find_entity(analysis, file->symbols[j].key);
int candidate =
entity >= 0 &&
analysis->entities[entity].current &&
analysis->entities[entity].symbols == 1
? analysis->entities[entity].module
: -1;
if (module == -2) module = candidate;
else if (module != candidate) module = -1;
if (entity >= 0 &&
analysis->entities[entity].symbols == 1 &&
(leader == UINT32_MAX ||
analysis->entities[entity].edits > analysis->entities[leader].edits))
leader = (uint32_t)entity;
}
add_split_unit(
&units,
count,
&capacity,
(SplitUnit){
f, cursor, file->symbols[i].end, next - i,
SIZE_MAX, module < 0 ? -1 : module, leader
}
);
cursor = file->symbols[i].end;
i = next;
}
if (cursor < file->len)
add_split_unit(
&units,
count,
&capacity,
(SplitUnit){f, cursor, file->len, 0, SIZE_MAX, -1, UINT32_MAX}
);
}
return units;
}
static SplitGroup *build_split_groups(
SplitUnit *units,
size_t nunits,
size_t modules,
size_t max_bytes,
size_t *count
) {
size_t *module_bytes = calloc(modules, sizeof *module_bytes);
if (!module_bytes) fail("out of memory sizing split communities");
for (size_t i = 0; i < nunits; i++)
if (units[i].module >= 0)
module_bytes[units[i].module] += units[i].end - units[i].start + 1;
SplitGroup *groups = resize(NULL, (modules + nunits) * sizeof *groups);
for (size_t i = 0; i < modules; i++)
if (module_bytes[i] && module_bytes[i] <= max_bytes)
groups[(*count)++] = (SplitGroup){(int)i, SIZE_MAX, module_bytes[i], UINT32_MAX};
for (size_t i = 0; i < nunits; i++) {
if (units[i].module >= 0 && module_bytes[units[i].module] <= max_bytes)
continue;
size_t bytes = units[i].end - units[i].start + 1;
if (bytes > max_bytes)
fail("natural declaration unit exceeds one-tenth limit: bytes=%zu max=%zu", bytes, max_bytes);
groups[(*count)++] = (SplitGroup){-1, i, bytes, units[i].leader};
}
free(module_bytes);
qsort(groups, *count, sizeof *groups, split_group_order);
return groups;
}
static void pack_split_groups(
SplitUnit *units,
size_t nunits,
SplitGroup *groups,
size_t ngroups,
size_t modules,
SplitChunk *chunks,
size_t nchunks,
size_t max_bytes
) {
size_t *module_chunk = resize(NULL, modules * sizeof *module_chunk);
for (size_t i = 0; i < modules; i++) module_chunk[i] = SIZE_MAX;
for (size_t i = 0; i < ngroups; i++) {
size_t chunk = SIZE_MAX;
for (size_t j = 0; j < nchunks; j++) {
if (chunks[j].len + groups[i].bytes > max_bytes) continue;
if (chunk == SIZE_MAX || chunks[j].len < chunks[chunk].len) chunk = j;
}
if (chunk == SIZE_MAX)
fail("cannot fit split group: bytes=%zu max=%zu", groups[i].bytes, max_bytes);
chunks[chunk].len += groups[i].bytes;
if (groups[i].module >= 0) module_chunk[groups[i].module] = chunk;
else units[groups[i].unit].chunk = chunk;
}
for (size_t i = 0; i < nunits; i++)
if (units[i].chunk == SIZE_MAX && units[i].module >= 0)
units[i].chunk = module_chunk[units[i].module];
for (size_t i = 0; i < nunits; i++)
if (units[i].chunk == SIZE_MAX)
fail("split unit was not assigned: unit=%zu module=%d", i, units[i].module);
for (size_t i = 0; i < nchunks; i++) {
if (!chunks[i].len) fail("split produced empty output: chunk=%zu", i);
chunks[i].len = 0;
}
free(module_chunk);
}
static void build_split_chunks(
Analysis *analysis,
SplitUnit *units,
size_t nunits,
SplitChunk *chunks
) {
for (size_t i = 0; i < nunits; i++) {
SplitUnit *unit = &units[i];
File *file = &analysis->files[unit->file];
SplitChunk *chunk = &chunks[unit->chunk];
append_chunk(chunk, file->source + unit->start, unit->end - unit->start);
append_chunk(chunk, "\n", 1);
chunk->source_bytes += unit->end - unit->start;
chunk->symbols += unit->symbols;
add_chunk_leader(chunk, analysis, unit->leader);
}
}
static size_t title_words(const char *ident, char words[][32], size_t count) {
for (size_t i = 0; ident[i] && count < 32;) {
while (ident[i] && !isalnum((unsigned char)ident[i])) i++;
if (!ident[i]) break;
size_t start = i++;
while (ident[i] && isalnum((unsigned char)ident[i])) {
int lower_upper =
islower((unsigned char)ident[i - 1]) &&
isupper((unsigned char)ident[i]);
int acronym_word =
i > start + 1 &&
isupper((unsigned char)ident[i - 1]) &&
isupper((unsigned char)ident[i]) &&
islower((unsigned char)ident[i + 1]);
if (lower_upper || acronym_word) break;
i++;
}
size_t len = i - start;
if (len > 31) len = 31;
words[count][0] = (char)toupper((unsigned char)ident[start]);
for (size_t j = 1; j < len; j++)
words[count][j] = (char)tolower((unsigned char)ident[start + j]);
words[count][len] = 0;
int duplicate = 0;
for (size_t j = 0; j < count; j++)
duplicate |= !strcmp(words[j], words[count]);
if (!duplicate) count++;
}
return count;
}
static int split_path_available(
SplitChunk *chunks,
size_t current,
const char *path
) {
for (size_t i = 0; i < current; i++)
if (!strcmp(chunks[i].path, path)) return 0;
FILE *existing = fopen(path, "rb");
if (!existing) {
if (errno == ENOENT) return 1;
fail("fopen %s failed: %s", path, strerror(errno));
}
fclose(existing);
return 0;
}
static void split_repository(Analysis *analysis, size_t modules) {
validate_split_inputs(analysis);
size_t nunits = 0;
size_t source_bytes = 0;
size_t source_symbols = 0;
SplitUnit *units = collect_split_units(
analysis,
&nunits,
&source_bytes,
&source_symbols
);
if (!nunits || !source_bytes) fail("repository has no source bytes to split");
const size_t nchunks = 50;
size_t output_bytes = source_bytes + nunits;
size_t max_bytes = output_bytes / 10;
SplitChunk *chunks = calloc(nchunks, sizeof *chunks);
if (!chunks) fail("out of memory allocating split files");
size_t ngroups = 0;
SplitGroup *groups = build_split_groups(
units,
nunits,
modules,
max_bytes,
&ngroups
);
pack_split_groups(
units,
nunits,
groups,
ngroups,
modules,
chunks,
nchunks,
max_bytes
);
build_split_chunks(analysis, units, nunits, chunks);
size_t covered_bytes = 0;
size_t covered_symbols = 0;
size_t written_bytes = 0;
for (size_t i = 0; i < nchunks; i++) {
covered_bytes += chunks[i].source_bytes;
covered_symbols += chunks[i].symbols;
written_bytes += chunks[i].len;
if (chunks[i].len * 10 > output_bytes)
fail(
"split size limit exceeded: chunk=%zu bytes=%zu total=%zu",
i + 1,
chunks[i].len,
output_bytes
);
}
if (covered_bytes != source_bytes ||
covered_symbols != source_symbols ||
written_bytes != output_bytes)
fail(
"split coverage mismatch: bytes=%zu/%zu symbols=%zu/%zu output=%zu/%zu",
covered_bytes,
source_bytes,
covered_symbols,
source_symbols,
written_bytes,
output_bytes
);
static const char *const fallback[] = {
"Core", "Runtime", "Service", "System", "Engine", "Model", "Store", "Flow",
"Tools", "Views", "Rules", "Types", "Tasks", "Events", "Inputs", "Outputs",
"Records", "Queries", "Actions", "Routes", "Workers", "Clients", "Servers",
"Reports", "Reviews", "Search", "Graph", "Auth", "Billing", "Pilots",
"Cases", "Subjects", "Evidence", "Sources", "Documents", "Sessions",
"Metrics", "Registry", "Pipeline", "Studio", "Console", "Network",
"Storage", "Images", "Emails", "Teams", "Access", "Config", "Schema", "Batch"
};
const char *workdir = git_repository_workdir(analysis->repo);
for (size_t i = 0; i < nchunks; i++) {
char words[32][32] = {{0}};
size_t nwords = 0;
for (size_t j = 0; j < chunks[i].nleaders; j++)
nwords = title_words(
analysis->entities[chunks[i].leaders[j]].ident,
words,
nwords
);
char path[4096];
for (size_t a = 0; a < nwords && !chunks[i].path; a++)
for (size_t b = a + 1; b < nwords && !chunks[i].path; b++) {
snprintf(path, sizeof path, "%s%s%s.tsx", workdir, words[a], words[b]);
if (split_path_available(chunks, i, path))
chunks[i].path = copy_text(path, strlen(path));
}
for (size_t j = 0; j < nchunks && !chunks[i].path; j++) {
const char *first = nwords ? words[0] : "Source";
const char *second = fallback[(i + j) % nchunks];
if (!strcmp(first, second)) continue;
snprintf(path, sizeof path, "%s%s%s.tsx", workdir, first, second);
if (split_path_available(chunks, i, path))
chunks[i].path = copy_text(path, strlen(path));
}
if (!chunks[i].path) fail("cannot derive unique two-word name for chunk %zu", i + 1);
}
for (size_t i = 0; i < nchunks; i++) {
write_file(chunks[i].path, chunks[i].data, chunks[i].len);
const char *base = strrchr(chunks[i].path, '/');
printf(
"split ./%s symbols=%zu source_bytes=%zu output_bytes=%zu\n",
base ? base + 1 : chunks[i].path,
chunks[i].symbols,
chunks[i].source_bytes,
chunks[i].len
);
}
for (size_t i = 0; i < analysis->nfiles; i++) {
char path[4096];
snprintf(path, sizeof path, "%s%s", workdir, analysis->files[i].path);
if (unlink(path))
fail("unlink %s failed: %s", path, strerror(errno));
}
printf(
"split_complete files=%zu removed_sources=%zu source_bytes=%zu output_bytes=%zu max_bytes=%zu\n",
nchunks,
analysis->nfiles,
source_bytes,
output_bytes,
max_bytes
);
for (size_t i = 0; i < nchunks; i++) {
free(chunks[i].data);
free(chunks[i].path);
}
free(groups);
free(chunks);
free(units);
}
static size_t module_prefix_end(const char *source, size_t len) {
size_t count = 0;
Token *tokens = lex_ts(source, len, &count);
size_t i = 0;
while (i < count) {
if (token_is(tokens[i], "import") ||
(tokens[i].kind == 2 &&
(tokens[i].text[0] == '\'' || tokens[i].text[0] == '"'))) {
i = statement_end(tokens, count, i);
continue;
}
size_t end = tokens[i].start;
free(tokens);
return end;
}
free(tokens);
return len;
}
static void test_split_helpers(void) {
const char *plain = "scripts/foo.ts";
const char *generated = "scripts/foo.module-001.ts";
const char source[] =
"import x from 'x';\n"
"import type { Y } from 'y';\n"
"const value: Y = x;\n";
if (module_root_stem_len(plain, strlen(plain) - 3) != strlen("scripts/foo") ||
module_root_stem_len(generated, strlen(generated) - 3) != strlen("scripts/foo"))
fail("self-test flat module sibling stem");
if (module_prefix_end(source, sizeof source - 1) !=
(size_t)(strstr(source, "const value") - source))
fail("self-test physical extraction prefix");
puts("self-test: split helpers ok");
}
static int token_list_has(Token *tokens, size_t count, const char *ident) {
Symbol refs = {0};
int found = 0;
for (size_t i = 0; i < count; i++) {
if (tokens[i].kind == 1 && token_is(tokens[i], ident) &&
(!i || (!token_is(tokens[i - 1], ".") && !token_is(tokens[i - 1], "?."))) &&
(i + 1 == count || !token_is(tokens[i + 1], ":")))
found = 1;
if (tokens[i].kind == 2 && tokens[i].len && tokens[i].text[0] == '`')
add_template_refs(&refs, NULL, 0, tokens[i]);
}
found = found || symbol_has_ref(&refs, ident);
for (size_t i = 0; i < refs.nrefs; i++) free(refs.refs[i]);
free(refs.refs);
return found;
}
typedef struct {
char *name;
int type_only;
int export_kind;
int imported;
} MoveBinding;
static int symbol_type_only(Symbol *symbol);
static void add_move_binding(
MoveBinding **items,
size_t *count,
Symbol *symbol,
int export_kind
) {
for (size_t i = 0; i < *count; i++) {
if (strcmp((*items)[i].name, symbol->ident)) continue;
(*items)[i].type_only &= symbol_type_only(symbol);
if (export_kind > (*items)[i].export_kind)
(*items)[i].export_kind = export_kind;
return;
}
*items = resize(*items, (*count + 1) * sizeof **items);
(*items)[*count] = (MoveBinding){
copy_text(symbol->ident, strlen(symbol->ident)),
symbol_type_only(symbol),
export_kind,
0
};
(*count)++;
}
static int symbol_type_only(Symbol *symbol) {
return !strncmp(symbol->key, "type ", 5) || !strncmp(symbol->key, "interface ", 10);
}
static int symbol_export_kind(File *file, Symbol *symbol) {
size_t count = 0;
Token *tokens = lex_ts(
file->source + symbol->start,
symbol->end - symbol->start,
&count
);
int kind = count && token_is(tokens[0], "export");
if (kind && count > 1 && token_is(tokens[1], "default")) kind = 2;
free(tokens);
return kind;
}
static void append_text(char **out, size_t *len, const char *text, size_t size) {
*out = resize(*out, *len + size + 1);
memcpy(*out + *len, text, size);
*len += size;
(*out)[*len] = 0;
}
static void apply_extract(Analysis *analysis, ExtractRow row) {
File *file = &analysis->files[row.file];
size_t begin = file->symbols[row.first].start;
size_t end = file->symbols[row.last].end;
size_t prefix_end = module_prefix_end(file->source, file->len);
const char *workdir = git_repository_workdir(analysis->repo);
if (!workdir) fail("cannot apply in a bare repository");
char *original = resize(NULL, strlen(workdir) + strlen(file->path) + 1);
sprintf(original, "%s%s", workdir, file->path);
const char *slash = strrchr(file->path, '/');
const char *base = slash ? slash + 1 : file->path;
const char *extension = strrchr(base, '.');
if (!extension) fail("source has no extension: %s", file->path);
size_t stem_len = module_root_stem_len(
file->path,
(size_t)(extension - file->path)
);
size_t sibling_len = stem_len + 32 + strlen(extension);
char *sibling_rel = resize(NULL, sibling_len);
char *sibling = resize(NULL, strlen(workdir) + sibling_len + 1);
unsigned sibling_number = 0;
for (;;) {
snprintf(
sibling_rel,
sibling_len,
"%.*s.module-%03u%s",
(int)stem_len,
file->path,
++sibling_number,
extension
);
sprintf(sibling, "%s%s", workdir, sibling_rel);
FILE *existing = fopen(sibling, "rb");
if (!existing) {
if (errno != ENOENT)
fail("fopen %s failed: %s", sibling, strerror(errno));
break;
}
fclose(existing);
if (sibling_number == 999)
fail("no available module sibling for %s", file->path);
}
size_t retained_prefix_count = 0;
Token *retained_prefix = lex_ts(
file->source + prefix_end,
begin - prefix_end,
&retained_prefix_count
);
size_t suffix_count = 0;
Token *suffix = lex_ts(file->source + end, file->len - end, &suffix_count);
MoveBinding *bindings = NULL;
size_t binding_count = 0;
for (size_t s = 0; s < file->count; s++) {
Symbol *symbol = &file->symbols[s];
if (symbol->start < begin || symbol->end > end) continue;
if (!strcmp(symbol->ident, "default"))
fail("cannot extract anonymous default declaration from %s", file->path);
int export_kind = symbol_export_kind(file, symbol);
if (export_kind == 2)
fail("cannot extract default export from %s", file->path);
add_move_binding(&bindings, &binding_count, symbol, export_kind);
}
if (!binding_count) fail("candidate has no movable bindings in %s", file->path);
for (size_t i = 0; i < binding_count; i++)
bindings[i].imported =
token_list_has(retained_prefix, retained_prefix_count, bindings[i].name) ||
token_list_has(suffix, suffix_count, bindings[i].name);
const char *sibling_base = strrchr(sibling_rel, '/');
sibling_base = sibling_base ? sibling_base + 1 : sibling_rel;
char spec[1024];
snprintf(
spec,
sizeof spec,
"./%.*s%s",
(int)(strlen(sibling_base) - strlen(extension)),
sibling_base,
!strcmp(extension, ".mjs") ? extension : ""
);
char *module = NULL;
size_t module_len = 0;
size_t module_exports = 0;
append_text(&module, &module_len, file->source, prefix_end);
append_text(&module, &module_len, file->source + begin, end - begin);
for (size_t i = 0; i < binding_count; i++) {
if (!bindings[i].imported || bindings[i].export_kind) continue;
append_text(
&module,
&module_len,
module_exports ? ", " : "\nexport { ",
module_exports ? 2 : 10
);
if (bindings[i].type_only)
append_text(&module, &module_len, "type ", 5);
append_text(
&module,
&module_len,
bindings[i].name,
strlen(bindings[i].name)
);
module_exports++;
}
if (module_exports) append_text(&module, &module_len, " };\n", 4);
char *replacement = NULL;
size_t replacement_len = 0;
size_t imported = 0;
append_text(
&replacement,
&replacement_len,
file->source,
prefix_end
);
for (size_t i = 0; i < binding_count; i++) {
if (!bindings[i].imported) continue;
append_text(
&replacement,
&replacement_len,
imported ? ", " : "import { ",
imported ? 2 : 9
);
if (bindings[i].type_only)
append_text(&replacement, &replacement_len, "type ", 5);
append_text(
&replacement,
&replacement_len,
bindings[i].name,
strlen(bindings[i].name)
);
imported++;
}
if (imported) {
append_text(&replacement, &replacement_len, " } from '", 9);
append_text(&replacement, &replacement_len, spec, strlen(spec));
append_text(&replacement, &replacement_len, "';\n", 3);
} else {
append_text(&replacement, &replacement_len, "import '", 8);
append_text(&replacement, &replacement_len, spec, strlen(spec));
append_text(&replacement, &replacement_len, "';\n", 3);
}
size_t reexported = 0;
for (size_t i = 0; i < binding_count; i++) {
if (!bindings[i].export_kind) continue;
append_text(
&replacement,
&replacement_len,
reexported ? ", " : "export { ",
reexported ? 2 : 9
);
if (bindings[i].type_only)
append_text(&replacement, &replacement_len, "type ", 5);
append_text(
&replacement,
&replacement_len,
bindings[i].name,
strlen(bindings[i].name)
);
reexported++;
}
if (reexported) {
append_text(&replacement, &replacement_len, " } from '", 9);
append_text(&replacement, &replacement_len, spec, strlen(spec));
append_text(&replacement, &replacement_len, "';\n", 3);
}
append_text(
&replacement,
&replacement_len,
file->source + prefix_end,
begin - prefix_end
);
append_text(
&replacement,
&replacement_len,
file->source + end,
file->len - end
);
while (replacement_len &&
(replacement[replacement_len - 1] == ' ' ||
replacement[replacement_len - 1] == '\t' ||
replacement[replacement_len - 1] == '\r' ||
replacement[replacement_len - 1] == '\n'))
replacement_len--;
append_text(&replacement, &replacement_len, "\n", 1);
write_file(sibling, module, module_len);
write_file(original, replacement, replacement_len);
printf(
"\napplied split: %s -> %s bytes=%zu bindings=%zu imported=%zu reexported=%zu\n",
file->path,
sibling_rel,
end - begin,
binding_count,
imported,
reexported
);
for (size_t i = 0; i < binding_count; i++) free(bindings[i].name);
free(bindings);
free(retained_prefix);
free(suffix);
free(module);
free(replacement);
free(original);
free(sibling_rel);
free(sibling);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment