xbintsc 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +191 -0
- package/bin/xbintsc.js +26 -0
- package/dist/src/ast/factory.d.ts +11 -0
- package/dist/src/ast/factory.js +36 -0
- package/dist/src/ast/factory.js.map +1 -0
- package/dist/src/ast/nodes.d.ts +848 -0
- package/dist/src/ast/nodes.js +247 -0
- package/dist/src/ast/nodes.js.map +1 -0
- package/dist/src/ast/visitor.d.ts +18 -0
- package/dist/src/ast/visitor.js +52 -0
- package/dist/src/ast/visitor.js.map +1 -0
- package/dist/src/binder/binder.d.ts +82 -0
- package/dist/src/binder/binder.js +443 -0
- package/dist/src/binder/binder.js.map +1 -0
- package/dist/src/cli/main.d.ts +16 -0
- package/dist/src/cli/main.js +191 -0
- package/dist/src/cli/main.js.map +1 -0
- package/dist/src/codegen/llvm.d.ts +31 -0
- package/dist/src/codegen/llvm.js +1178 -0
- package/dist/src/codegen/llvm.js.map +1 -0
- package/dist/src/codegen/values.d.ts +23 -0
- package/dist/src/codegen/values.js +32 -0
- package/dist/src/codegen/values.js.map +1 -0
- package/dist/src/diagnostics/diagnostic.d.ts +69 -0
- package/dist/src/diagnostics/diagnostic.js +104 -0
- package/dist/src/diagnostics/diagnostic.js.map +1 -0
- package/dist/src/diagnostics/source.d.ts +43 -0
- package/dist/src/diagnostics/source.js +73 -0
- package/dist/src/diagnostics/source.js.map +1 -0
- package/dist/src/driver/cache.d.ts +37 -0
- package/dist/src/driver/cache.js +82 -0
- package/dist/src/driver/cache.js.map +1 -0
- package/dist/src/driver/compiler.d.ts +41 -0
- package/dist/src/driver/compiler.js +128 -0
- package/dist/src/driver/compiler.js.map +1 -0
- package/dist/src/driver/index.d.ts +4 -0
- package/dist/src/driver/index.js +5 -0
- package/dist/src/driver/index.js.map +1 -0
- package/dist/src/driver/paths.d.ts +4 -0
- package/dist/src/driver/paths.js +22 -0
- package/dist/src/driver/paths.js.map +1 -0
- package/dist/src/driver/toolchain.d.ts +43 -0
- package/dist/src/driver/toolchain.js +76 -0
- package/dist/src/driver/toolchain.js.map +1 -0
- package/dist/src/extensions/node.d.ts +9 -0
- package/dist/src/extensions/node.js +20 -0
- package/dist/src/extensions/node.js.map +1 -0
- package/dist/src/extensions/registry.d.ts +45 -0
- package/dist/src/extensions/registry.js +76 -0
- package/dist/src/extensions/registry.js.map +1 -0
- package/dist/src/index.d.ts +18 -0
- package/dist/src/index.js +19 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lexer/scanner.d.ts +48 -0
- package/dist/src/lexer/scanner.js +650 -0
- package/dist/src/lexer/scanner.js.map +1 -0
- package/dist/src/lexer/token.d.ts +170 -0
- package/dist/src/lexer/token.js +250 -0
- package/dist/src/lexer/token.js.map +1 -0
- package/dist/src/parser/parser.d.ts +127 -0
- package/dist/src/parser/parser.js +2334 -0
- package/dist/src/parser/parser.js.map +1 -0
- package/dist/tests/binder/binder.test.d.ts +1 -0
- package/dist/tests/binder/binder.test.js +89 -0
- package/dist/tests/binder/binder.test.js.map +1 -0
- package/dist/tests/cli/main.test.d.ts +1 -0
- package/dist/tests/cli/main.test.js +68 -0
- package/dist/tests/cli/main.test.js.map +1 -0
- package/dist/tests/cli/process.test.d.ts +9 -0
- package/dist/tests/cli/process.test.js +57 -0
- package/dist/tests/cli/process.test.js.map +1 -0
- package/dist/tests/codegen/llvm.test.d.ts +1 -0
- package/dist/tests/codegen/llvm.test.js +93 -0
- package/dist/tests/codegen/llvm.test.js.map +1 -0
- package/dist/tests/driver/cache.test.d.ts +1 -0
- package/dist/tests/driver/cache.test.js +66 -0
- package/dist/tests/driver/cache.test.js.map +1 -0
- package/dist/tests/driver/compiler.test.d.ts +1 -0
- package/dist/tests/driver/compiler.test.js +80 -0
- package/dist/tests/driver/compiler.test.js.map +1 -0
- package/dist/tests/driver/toolchain.test.d.ts +1 -0
- package/dist/tests/driver/toolchain.test.js +60 -0
- package/dist/tests/driver/toolchain.test.js.map +1 -0
- package/dist/tests/e2e/compile.test.d.ts +5 -0
- package/dist/tests/e2e/compile.test.js +93 -0
- package/dist/tests/e2e/compile.test.js.map +1 -0
- package/dist/tests/extensions/registry.test.d.ts +1 -0
- package/dist/tests/extensions/registry.test.js +47 -0
- package/dist/tests/extensions/registry.test.js.map +1 -0
- package/dist/tests/helpers.d.ts +23 -0
- package/dist/tests/helpers.js +41 -0
- package/dist/tests/helpers.js.map +1 -0
- package/dist/tests/lexer/scanner.test.d.ts +1 -0
- package/dist/tests/lexer/scanner.test.js +95 -0
- package/dist/tests/lexer/scanner.test.js.map +1 -0
- package/dist/tests/parser/parser.test.d.ts +1 -0
- package/dist/tests/parser/parser.test.js +137 -0
- package/dist/tests/parser/parser.test.js.map +1 -0
- package/package.json +49 -0
- package/runtime/ext_node.c +42 -0
- package/runtime/rt.h +177 -0
- package/runtime/xt_runtime.c +682 -0
- package/scripts/build-runtime.mjs +34 -0
- package/src/ast/factory.ts +37 -0
- package/src/ast/nodes.ts +1111 -0
- package/src/ast/visitor.ts +60 -0
- package/src/binder/binder.ts +549 -0
- package/src/cli/main.ts +210 -0
- package/src/codegen/llvm.ts +1324 -0
- package/src/codegen/values.ts +36 -0
- package/src/diagnostics/diagnostic.ts +132 -0
- package/src/diagnostics/source.ts +97 -0
- package/src/driver/cache.ts +96 -0
- package/src/driver/compiler.ts +197 -0
- package/src/driver/index.ts +4 -0
- package/src/driver/paths.ts +24 -0
- package/src/driver/toolchain.ts +103 -0
- package/src/extensions/node.ts +23 -0
- package/src/extensions/registry.ts +101 -0
- package/src/index.ts +33 -0
- package/src/lexer/scanner.ts +650 -0
- package/src/lexer/token.ts +266 -0
- package/src/parser/parser.ts +2396 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The NaN-boxed value model shared by the compiler and the runtime.
|
|
3
|
+
*
|
|
4
|
+
* A JavaScript value is a 64-bit word. Doubles are stored unboxed; every other
|
|
5
|
+
* value uses a tag in the high 16 bits plus a 48-bit payload. The compiler
|
|
6
|
+
* emits these constants directly into LLVM IR, so this module (together with
|
|
7
|
+
* runtime/rt.h) is the single source of truth for the representation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const XT_TAG_MASK = 0xffff000000000000n;
|
|
11
|
+
export const XT_NUMBER_MASK = 0xfff8000000000000n;
|
|
12
|
+
|
|
13
|
+
export const XT_UNDEFINED = 0xfff8000000000000n;
|
|
14
|
+
export const XT_NULL = 0xfff9000000000000n;
|
|
15
|
+
export const XT_FALSE = 0xfffa000000000000n;
|
|
16
|
+
export const XT_TRUE = 0xfffb000000000000n;
|
|
17
|
+
export const XT_STRING = 0xfffc000000000000n;
|
|
18
|
+
export const XT_OBJECT = 0xfffd000000000000n;
|
|
19
|
+
export const XT_ARRAY = 0xfffe000000000000n;
|
|
20
|
+
export const XT_FUNCTION = 0xffff000000000000n;
|
|
21
|
+
|
|
22
|
+
/** Render a 64-bit constant the way LLVM expects signed i64 literals. */
|
|
23
|
+
export function i64(value: bigint): string {
|
|
24
|
+
return BigInt.asIntN(64, value).toString();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The raw 64 bits of an IEEE-754 double, as a signed i64 literal. */
|
|
28
|
+
export function numberLiteral(value: number): string {
|
|
29
|
+
const view = new DataView(new ArrayBuffer(8));
|
|
30
|
+
view.setFloat64(0, value);
|
|
31
|
+
return i64(view.getBigInt64(0));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function booleanLiteral(value: boolean): string {
|
|
35
|
+
return i64(value ? XT_TRUE : XT_FALSE);
|
|
36
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { SourceFile } from "./source.js";
|
|
2
|
+
|
|
3
|
+
export type DiagnosticCategory = "error" | "warning" | "info";
|
|
4
|
+
|
|
5
|
+
/** Stable machine readable codes, grouped by pipeline stage. */
|
|
6
|
+
export enum DiagnosticCode {
|
|
7
|
+
// Lexer 1xxx
|
|
8
|
+
UnterminatedString = 1001,
|
|
9
|
+
UnterminatedTemplate = 1002,
|
|
10
|
+
UnterminatedComment = 1003,
|
|
11
|
+
InvalidCharacter = 1004,
|
|
12
|
+
InvalidNumber = 1005,
|
|
13
|
+
InvalidEscape = 1006,
|
|
14
|
+
|
|
15
|
+
// Parser 2xxx
|
|
16
|
+
UnexpectedToken = 2001,
|
|
17
|
+
ExpectedToken = 2002,
|
|
18
|
+
ExpectedIdentifier = 2003,
|
|
19
|
+
UnexpectedEof = 2004,
|
|
20
|
+
InvalidAssignmentTarget = 2005,
|
|
21
|
+
DuplicateDefault = 2006,
|
|
22
|
+
InvalidTypeSyntax = 2007,
|
|
23
|
+
|
|
24
|
+
// Binder 3xxx
|
|
25
|
+
DuplicateIdentifier = 3001,
|
|
26
|
+
CannotFindName = 3002,
|
|
27
|
+
IllegalRedeclaration = 3003,
|
|
28
|
+
|
|
29
|
+
// Checker 4xxx
|
|
30
|
+
TypeMismatch = 4001,
|
|
31
|
+
NotCallable = 4002,
|
|
32
|
+
PropertyNotFound = 4003,
|
|
33
|
+
ArgumentCountMismatch = 4004,
|
|
34
|
+
UnsupportedFeature = 4005,
|
|
35
|
+
|
|
36
|
+
// Codegen 5xxx
|
|
37
|
+
CodegenError = 5001,
|
|
38
|
+
|
|
39
|
+
// Driver 6xxx
|
|
40
|
+
ModuleNotFound = 6001,
|
|
41
|
+
IOError = 6002,
|
|
42
|
+
ToolchainError = 6003,
|
|
43
|
+
IncrementalCacheError = 6004,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface Diagnostic {
|
|
47
|
+
readonly category: DiagnosticCategory;
|
|
48
|
+
readonly code: DiagnosticCode;
|
|
49
|
+
readonly message: string;
|
|
50
|
+
readonly fileName?: string;
|
|
51
|
+
readonly start?: number;
|
|
52
|
+
readonly end?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class DiagnosticError extends Error {
|
|
56
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
57
|
+
constructor(diagnostics: Diagnostic | readonly Diagnostic[]) {
|
|
58
|
+
const list = Array.isArray(diagnostics) ? diagnostics : [diagnostics];
|
|
59
|
+
super(list.map((d) => formatDiagnostic(d)).join("\n"));
|
|
60
|
+
this.name = "DiagnosticError";
|
|
61
|
+
this.diagnostics = list;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class DiagnosticBag {
|
|
66
|
+
private readonly items: Diagnostic[] = [];
|
|
67
|
+
|
|
68
|
+
get diagnostics(): readonly Diagnostic[] {
|
|
69
|
+
return this.items;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get hasErrors(): boolean {
|
|
73
|
+
return this.items.some((d) => d.category === "error");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Snapshot the current size so a speculative parse can roll back. */
|
|
77
|
+
mark(): number {
|
|
78
|
+
return this.items.length;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Discard every diagnostic added after `mark()` was taken. */
|
|
82
|
+
reset(mark: number): void {
|
|
83
|
+
this.items.length = mark;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
error(code: DiagnosticCode, message: string, range?: { start: number; end: number }, fileName?: string): void {
|
|
87
|
+
this.items.push({ category: "error", code, message, fileName, start: range?.start, end: range?.end });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
warning(code: DiagnosticCode, message: string, range?: { start: number; end: number }, fileName?: string): void {
|
|
91
|
+
this.items.push({ category: "warning", code, message, fileName, start: range?.start, end: range?.end });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
add(diagnostic: Diagnostic): void {
|
|
95
|
+
this.items.push(diagnostic);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
addAll(diagnostics: readonly Diagnostic[]): void {
|
|
99
|
+
this.items.push(...diagnostics);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
throwIfErrors(): void {
|
|
103
|
+
if (this.hasErrors) throw new DiagnosticError(this.items);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Render a diagnostic with a source excerpt and a caret underline. Falls back
|
|
109
|
+
* to a plain "file:line:col" form when no source file is available.
|
|
110
|
+
*/
|
|
111
|
+
export function formatDiagnostic(d: Diagnostic, source?: SourceFile): string {
|
|
112
|
+
const severity = d.category;
|
|
113
|
+
let location = d.fileName ?? "<unknown>";
|
|
114
|
+
let excerpt = "";
|
|
115
|
+
if (source && d.start !== undefined) {
|
|
116
|
+
const { line, column } = source.positionAt(d.start);
|
|
117
|
+
location = `${source.fileName}:${line}:${column}`;
|
|
118
|
+
const lineText = source.text.split("\n")[line - 1] ?? "";
|
|
119
|
+
const end = d.end ?? d.start;
|
|
120
|
+
const endCol = source.positionAt(end).column;
|
|
121
|
+
const caretPad = " ".repeat(Math.max(0, column - 1));
|
|
122
|
+
const caret = "^".repeat(Math.max(1, endCol - column));
|
|
123
|
+
excerpt = `\n${lineText}\n${caretPad}${caret}`;
|
|
124
|
+
}
|
|
125
|
+
return `${location} - ${severity} TS${d.code}: ${d.message}${excerpt}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function formatDiagnostics(diagnostics: readonly Diagnostic[], sources?: Map<string, SourceFile>): string {
|
|
129
|
+
return diagnostics
|
|
130
|
+
.map((d) => formatDiagnostic(d, d.fileName ? sources?.get(d.fileName) : undefined))
|
|
131
|
+
.join("\n");
|
|
132
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source file abstraction: owns the raw text plus a line-offset index so that
|
|
3
|
+
* positions (character offsets) can be mapped back to line/column pairs for
|
|
4
|
+
* diagnostics and for incremental-compilation fingerprints.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface LineAndColumn {
|
|
8
|
+
/** 1-based line number. */
|
|
9
|
+
readonly line: number;
|
|
10
|
+
/** 1-based column number (UTF-16 code units). */
|
|
11
|
+
readonly column: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A text position. `offset` is a UTF-16 code-unit index into the source text,
|
|
16
|
+
* matching how the JavaScript/TypeScript toolchain measures positions.
|
|
17
|
+
*/
|
|
18
|
+
export interface TextPosition {
|
|
19
|
+
readonly offset: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TextRange {
|
|
23
|
+
readonly start: number;
|
|
24
|
+
readonly end: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class SourceFile {
|
|
28
|
+
readonly fileName: string;
|
|
29
|
+
readonly text: string;
|
|
30
|
+
/** Content hash used by the incremental compiler. */
|
|
31
|
+
readonly hash: string;
|
|
32
|
+
private lineStarts: number[] | undefined;
|
|
33
|
+
|
|
34
|
+
constructor(fileName: string, text: string) {
|
|
35
|
+
this.fileName = fileName;
|
|
36
|
+
// Normalize BOM and line endings so downstream stages only deal with "\n".
|
|
37
|
+
const normalized = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
38
|
+
this.text = normalized.replace(/\r\n?/g, "\n");
|
|
39
|
+
this.hash = hashText(this.text);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get length(): number {
|
|
43
|
+
return this.text.length;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private getLineStarts(): number[] {
|
|
47
|
+
if (this.lineStarts === undefined) {
|
|
48
|
+
const starts = [0];
|
|
49
|
+
const text = this.text;
|
|
50
|
+
for (let i = 0; i < text.length; i++) {
|
|
51
|
+
if (text.charCodeAt(i) === 10 /* \n */) starts.push(i + 1);
|
|
52
|
+
}
|
|
53
|
+
this.lineStarts = starts;
|
|
54
|
+
}
|
|
55
|
+
return this.lineStarts;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
positionAt(offset: number): LineAndColumn {
|
|
59
|
+
const clamped = Math.max(0, Math.min(offset, this.text.length));
|
|
60
|
+
const starts = this.getLineStarts();
|
|
61
|
+
// Binary search for the greatest line start <= clamped.
|
|
62
|
+
let lo = 0;
|
|
63
|
+
let hi = starts.length - 1;
|
|
64
|
+
while (lo < hi) {
|
|
65
|
+
const mid = (lo + hi + 1) >> 1;
|
|
66
|
+
if (starts[mid]! <= clamped) lo = mid;
|
|
67
|
+
else hi = mid - 1;
|
|
68
|
+
}
|
|
69
|
+
return { line: lo + 1, column: clamped - starts[lo]! + 1 };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Extract the raw source text covered by a range. */
|
|
73
|
+
slice(range: TextRange): string {
|
|
74
|
+
return this.text.slice(range.start, range.end);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* FNV-1a 64-bit content hash rendered as a hex string. Small, dependency free
|
|
80
|
+
* and stable across platforms which is exactly what the incremental cache
|
|
81
|
+
* needs. Collision resistance is not a security property here.
|
|
82
|
+
*/
|
|
83
|
+
export function hashText(text: string, seed = 0xcbf29ce484222325n): string {
|
|
84
|
+
let hash = seed;
|
|
85
|
+
const prime = 0x100000001b3n;
|
|
86
|
+
const mask = 0xffffffffffffffffn;
|
|
87
|
+
for (let i = 0; i < text.length; i++) {
|
|
88
|
+
hash ^= BigInt(text.charCodeAt(i));
|
|
89
|
+
hash = (hash * prime) & mask;
|
|
90
|
+
}
|
|
91
|
+
return hash.toString(16).padStart(16, "0");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Combine several hashes/strings into one stable fingerprint. */
|
|
95
|
+
export function combineHashes(parts: readonly string[]): string {
|
|
96
|
+
return hashText(parts.join("\u0000"));
|
|
97
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-addressed incremental build cache.
|
|
3
|
+
*
|
|
4
|
+
* The cache keys a build on everything that can change the output: the entry
|
|
5
|
+
* source hash, the compiler version, the resolved options and the set of active
|
|
6
|
+
* extensions. If nothing changed and the recorded outputs still exist, the
|
|
7
|
+
* build is skipped entirely.
|
|
8
|
+
*
|
|
9
|
+
* It is deliberately a single JSON manifest so it is easy to inspect, diff and
|
|
10
|
+
* invalidate; a future content-addressed object store can replace it without
|
|
11
|
+
* touching callers.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
18
|
+
export interface CacheEntry {
|
|
19
|
+
readonly key: string;
|
|
20
|
+
readonly outputs: readonly string[];
|
|
21
|
+
readonly createdAt: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CacheManifest {
|
|
25
|
+
readonly version: 1;
|
|
26
|
+
readonly entries: Record<string, CacheEntry>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function hashString(text: string): string {
|
|
30
|
+
return createHash("sha256").update(text).digest("hex").slice(0, 32);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function hashParts(parts: readonly string[]): string {
|
|
34
|
+
const hash = createHash("sha256");
|
|
35
|
+
for (const part of parts) {
|
|
36
|
+
hash.update(String(part.length));
|
|
37
|
+
hash.update(":");
|
|
38
|
+
hash.update(part);
|
|
39
|
+
hash.update(";");
|
|
40
|
+
}
|
|
41
|
+
return hash.digest("hex").slice(0, 32);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class BuildCache {
|
|
45
|
+
private manifest: CacheManifest = { version: 1, entries: {} };
|
|
46
|
+
private dirty = false;
|
|
47
|
+
|
|
48
|
+
constructor(private readonly directory: string) {
|
|
49
|
+
this.load();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private get manifestPath(): string {
|
|
53
|
+
return join(this.directory, "build-cache.json");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private load(): void {
|
|
57
|
+
if (!existsSync(this.manifestPath)) return;
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(readFileSync(this.manifestPath, "utf8")) as CacheManifest;
|
|
60
|
+
if (parsed.version === 1 && parsed.entries) this.manifest = parsed;
|
|
61
|
+
} catch {
|
|
62
|
+
// A corrupt cache is never fatal; start from scratch.
|
|
63
|
+
this.manifest = { version: 1, entries: {} };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
save(): void {
|
|
68
|
+
if (!this.dirty) return;
|
|
69
|
+
mkdirSync(this.directory, { recursive: true });
|
|
70
|
+
writeFileSync(this.manifestPath, JSON.stringify(this.manifest, null, 2));
|
|
71
|
+
this.dirty = false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** True when `key` was built before and every recorded output still exists. */
|
|
75
|
+
isFresh(key: string, outputs: readonly string[]): boolean {
|
|
76
|
+
const entry = this.manifest.entries[key];
|
|
77
|
+
if (!entry) return false;
|
|
78
|
+
const recorded = outputs.length > 0 ? outputs : entry.outputs;
|
|
79
|
+
if (recorded.length === 0) return false;
|
|
80
|
+
return recorded.every((output) => existsSync(output));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
record(key: string, outputs: readonly string[]): void {
|
|
84
|
+
this.manifest.entries[key] = { key, outputs: [...outputs], createdAt: Date.now() };
|
|
85
|
+
this.dirty = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
clear(): void {
|
|
89
|
+
this.manifest = { version: 1, entries: {} };
|
|
90
|
+
this.dirty = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
entries(): readonly CacheEntry[] {
|
|
94
|
+
return Object.values(this.manifest.entries);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The build driver: source file in, native binary out.
|
|
3
|
+
*
|
|
4
|
+
* Pipeline: read -> parse -> bind/check -> LLVM IR -> object -> link.
|
|
5
|
+
* The runtime (and any registered extension sources) are compiled once and
|
|
6
|
+
* cached, and the whole build is skipped when the incremental cache says
|
|
7
|
+
* nothing relevant changed.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
12
|
+
import { DiagnosticBag, type Diagnostic } from "../diagnostics/diagnostic.js";
|
|
13
|
+
import { SourceFile } from "../diagnostics/source.js";
|
|
14
|
+
import { Parser } from "../parser/parser.js";
|
|
15
|
+
import { generate } from "../codegen/llvm.js";
|
|
16
|
+
import { createDefaultRegistry, type ExtensionRegistry } from "../extensions/registry.js";
|
|
17
|
+
import { BuildCache, hashParts } from "./cache.js";
|
|
18
|
+
import { findRuntimeDir } from "./paths.js";
|
|
19
|
+
import {
|
|
20
|
+
compileC,
|
|
21
|
+
compileIr,
|
|
22
|
+
findClang,
|
|
23
|
+
link,
|
|
24
|
+
realRunner,
|
|
25
|
+
type Runner,
|
|
26
|
+
} from "./toolchain.js";
|
|
27
|
+
|
|
28
|
+
export const COMPILER_VERSION = "0.1.0";
|
|
29
|
+
|
|
30
|
+
export type EmitKind = "exe" | "obj" | "ir";
|
|
31
|
+
|
|
32
|
+
export interface BuildOptions {
|
|
33
|
+
/** Explicit output path (defaults to the source base name in `outDir`). */
|
|
34
|
+
readonly output?: string;
|
|
35
|
+
readonly outDir?: string;
|
|
36
|
+
readonly optimize?: "0" | "1" | "2" | "3";
|
|
37
|
+
readonly emit?: EmitKind;
|
|
38
|
+
readonly force?: boolean;
|
|
39
|
+
readonly verbose?: boolean;
|
|
40
|
+
readonly cacheDir?: string;
|
|
41
|
+
readonly extensions?: ExtensionRegistry;
|
|
42
|
+
readonly runner?: Runner;
|
|
43
|
+
readonly clang?: string;
|
|
44
|
+
readonly write?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface BuildResult {
|
|
48
|
+
readonly outputPath: string;
|
|
49
|
+
readonly irPath?: string;
|
|
50
|
+
readonly cached: boolean;
|
|
51
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
52
|
+
readonly ir?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface CompileStringResult {
|
|
56
|
+
readonly ir: string;
|
|
57
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Compile TypeScript source text to LLVM IR without touching the toolchain. */
|
|
61
|
+
export function compileString(source: string, fileName = "input.ts", extensions?: ExtensionRegistry): CompileStringResult {
|
|
62
|
+
const file = new SourceFile(fileName, source);
|
|
63
|
+
const diagnostics = new DiagnosticBag();
|
|
64
|
+
const parser = new Parser(file, diagnostics);
|
|
65
|
+
const sourceFile = parser.parseSourceFile();
|
|
66
|
+
const registry = extensions ?? createDefaultRegistry();
|
|
67
|
+
const { ir } = generate(sourceFile, diagnostics, { builtins: registry.builtins() });
|
|
68
|
+
return { ir, diagnostics: diagnostics.diagnostics };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function executableName(entry: string, outDir: string, explicit: string | undefined): string {
|
|
72
|
+
if (explicit) return resolve(explicit);
|
|
73
|
+
const base = basename(entry, extname(entry));
|
|
74
|
+
const suffix = process.platform === "win32" ? ".exe" : "";
|
|
75
|
+
return resolve(outDir, base + suffix);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function build(entryPath: string, options: BuildOptions = {}): BuildResult {
|
|
79
|
+
const runner = options.runner ?? realRunner;
|
|
80
|
+
const registry = options.extensions ?? createDefaultRegistry();
|
|
81
|
+
const outDir = resolve(options.outDir ?? join(process.cwd(), "build"));
|
|
82
|
+
const cacheDir = resolve(options.cacheDir ?? join(process.cwd(), ".xbintsc"));
|
|
83
|
+
const emit: EmitKind = options.emit ?? "exe";
|
|
84
|
+
const optimize = options.optimize ?? "2";
|
|
85
|
+
|
|
86
|
+
const absoluteEntry = resolve(entryPath);
|
|
87
|
+
const sourceText = readFileSync(absoluteEntry, "utf8");
|
|
88
|
+
|
|
89
|
+
const baseName = basename(absoluteEntry, extname(absoluteEntry));
|
|
90
|
+
const outputPath =
|
|
91
|
+
emit === "ir"
|
|
92
|
+
? resolve(options.output ?? join(outDir, baseName + ".ll"))
|
|
93
|
+
: emit === "obj"
|
|
94
|
+
? resolve(options.output ?? join(outDir, baseName + ".o"))
|
|
95
|
+
: executableName(absoluteEntry, outDir, options.output);
|
|
96
|
+
|
|
97
|
+
const cacheKey = hashParts([
|
|
98
|
+
COMPILER_VERSION,
|
|
99
|
+
sourceText,
|
|
100
|
+
emit,
|
|
101
|
+
optimize,
|
|
102
|
+
process.platform,
|
|
103
|
+
registry.all().map((e) => e.name).join(","),
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
const cache = new BuildCache(cacheDir);
|
|
107
|
+
const outputs = [outputPath];
|
|
108
|
+
if (!options.force && cache.isFresh(cacheKey, outputs)) {
|
|
109
|
+
return { outputPath, cached: true, diagnostics: [] };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// -- front end -----------------------------------------------------------
|
|
113
|
+
const file = new SourceFile(absoluteEntry, sourceText);
|
|
114
|
+
const diagnostics = new DiagnosticBag();
|
|
115
|
+
const parser = new Parser(file, diagnostics);
|
|
116
|
+
const sourceFile = parser.parseSourceFile();
|
|
117
|
+
const { ir } = generate(sourceFile, diagnostics, { builtins: registry.builtins() });
|
|
118
|
+
|
|
119
|
+
if (options.verbose) {
|
|
120
|
+
process.stderr.write(`xbintsc: generated ${ir.length} bytes of LLVM IR\n`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (diagnostics.hasErrors) {
|
|
124
|
+
return { outputPath, cached: false, diagnostics: diagnostics.diagnostics, ir };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
mkdirSync(outDir, { recursive: true });
|
|
128
|
+
const irPath = resolve(outDir, basename(absoluteEntry, extname(absoluteEntry)) + ".ll");
|
|
129
|
+
writeFileSync(irPath, ir);
|
|
130
|
+
|
|
131
|
+
if (emit === "ir") {
|
|
132
|
+
cache.record(cacheKey, outputs);
|
|
133
|
+
cache.save();
|
|
134
|
+
return { outputPath, irPath, cached: false, diagnostics: [], ir };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// -- toolchain -----------------------------------------------------------
|
|
138
|
+
const clang = options.clang ?? findClang(runner);
|
|
139
|
+
const runtimeDir = findRuntimeDir();
|
|
140
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
141
|
+
|
|
142
|
+
const { runtimeObject, extensionObjects } = ensureRuntimeObjects(runner, clang, runtimeDir, cacheDir, registry);
|
|
143
|
+
|
|
144
|
+
const objectPath = emit === "obj" ? outputPath : resolve(outDir, baseName + ".o");
|
|
145
|
+
compileIr(runner, { clang, irPath, objectPath, optimize });
|
|
146
|
+
|
|
147
|
+
if (emit === "obj") {
|
|
148
|
+
cache.record(cacheKey, outputs);
|
|
149
|
+
cache.save();
|
|
150
|
+
return { outputPath, irPath, cached: false, diagnostics: [], ir };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
link(runner, {
|
|
154
|
+
clang,
|
|
155
|
+
objectPaths: [objectPath, runtimeObject, ...extensionObjects],
|
|
156
|
+
outputPath,
|
|
157
|
+
linkerFlags: [...(process.platform === "win32" ? [] : ["-lm"]), ...registry.linkerFlags()],
|
|
158
|
+
optimize,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
cache.record(cacheKey, outputs);
|
|
162
|
+
cache.save();
|
|
163
|
+
return { outputPath, irPath, cached: false, diagnostics: [], ir };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
interface RuntimeObjects {
|
|
167
|
+
readonly runtimeObject: string;
|
|
168
|
+
readonly extensionObjects: readonly string[];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Compile the core runtime and every extension source, reusing cached object
|
|
173
|
+
* files keyed on the C source hash. Returns the object paths to link.
|
|
174
|
+
*/
|
|
175
|
+
function ensureRuntimeObjects(
|
|
176
|
+
runner: Runner,
|
|
177
|
+
clang: string,
|
|
178
|
+
runtimeDir: string,
|
|
179
|
+
cacheDir: string,
|
|
180
|
+
registry: ExtensionRegistry,
|
|
181
|
+
): RuntimeObjects {
|
|
182
|
+
const compileOne = (sourcePath: string, tag: string): string => {
|
|
183
|
+
const body = readFileSync(sourcePath, "utf8");
|
|
184
|
+
const key = hashParts(["runtime", COMPILER_VERSION, tag, body, clang, process.platform]);
|
|
185
|
+
const objectPath = join(cacheDir, `${tag}-${key}.o`);
|
|
186
|
+
if (!existsSync(objectPath)) {
|
|
187
|
+
compileC(runner, clang, sourcePath, objectPath, runtimeDir);
|
|
188
|
+
}
|
|
189
|
+
return objectPath;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const runtimeObject = compileOne(join(runtimeDir, "xt_runtime.c"), "xt_runtime");
|
|
193
|
+
const extensionObjects = registry
|
|
194
|
+
.runtimeSources()
|
|
195
|
+
.map((sourcePath, index) => compileOne(sourcePath, `ext_${index}_${basename(sourcePath, ".c")}`));
|
|
196
|
+
return { runtimeObject, extensionObjects };
|
|
197
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { build, compileString, COMPILER_VERSION, type BuildOptions, type BuildResult, type EmitKind } from "./compiler.js";
|
|
2
|
+
export { BuildCache, hashParts, hashString } from "./cache.js";
|
|
3
|
+
export { findRuntimeDir, findPackageRoot } from "./paths.js";
|
|
4
|
+
export { findClang, realRunner, ToolchainError, type Runner } from "./toolchain.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Filesystem locations owned by the compiler package. */
|
|
2
|
+
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
/** Directory containing `rt.h` and the runtime/ext C sources. */
|
|
10
|
+
export function findRuntimeDir(): string {
|
|
11
|
+
const candidates = [
|
|
12
|
+
resolve(here, "../../runtime"),
|
|
13
|
+
resolve(here, "../../../runtime"),
|
|
14
|
+
resolve(process.cwd(), "runtime"),
|
|
15
|
+
];
|
|
16
|
+
for (const candidate of candidates) {
|
|
17
|
+
if (existsSync(join(candidate, "rt.h"))) return candidate;
|
|
18
|
+
}
|
|
19
|
+
throw new Error("Unable to locate the xbintsc runtime directory (missing runtime/rt.h)");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function findPackageRoot(): string {
|
|
23
|
+
return resolve(findRuntimeDir(), "..");
|
|
24
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin wrappers around the external toolchain (clang) and process execution.
|
|
3
|
+
*
|
|
4
|
+
* Everything that shells out lives here so the rest of the driver stays
|
|
5
|
+
* testable: unit tests can pass a fake `Runner` and never touch the host.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawnSync, type SpawnSyncOptions } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
export interface CommandResult {
|
|
11
|
+
readonly status: number;
|
|
12
|
+
readonly stdout: string;
|
|
13
|
+
readonly stderr: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Runner {
|
|
17
|
+
run(command: string, args: readonly string[], options?: { cwd?: string }): CommandResult;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const realRunner: Runner = {
|
|
21
|
+
run(command, args, options = {}) {
|
|
22
|
+
const spawnOptions: SpawnSyncOptions = {
|
|
23
|
+
cwd: options.cwd,
|
|
24
|
+
encoding: "utf8",
|
|
25
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
26
|
+
};
|
|
27
|
+
const result = spawnSync(command, [...args], spawnOptions);
|
|
28
|
+
return {
|
|
29
|
+
status: result.status ?? (result.error ? 1 : 0),
|
|
30
|
+
stdout: result.stdout?.toString() ?? "",
|
|
31
|
+
stderr: result.stderr?.toString() ?? (result.error ? String(result.error.message) : ""),
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export class ToolchainError extends Error {
|
|
37
|
+
readonly command: string;
|
|
38
|
+
readonly status: number;
|
|
39
|
+
readonly stderr: string;
|
|
40
|
+
constructor(command: string, status: number, stderr: string) {
|
|
41
|
+
super(`Command failed (${status}): ${command}\n${stderr}`);
|
|
42
|
+
this.name = "ToolchainError";
|
|
43
|
+
this.command = command;
|
|
44
|
+
this.status = status;
|
|
45
|
+
this.stderr = stderr;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Locate a clang-compatible C compiler, honouring `xbintsc_CLANG`. */
|
|
50
|
+
export function findClang(runner: Runner = realRunner): string {
|
|
51
|
+
const candidates = [
|
|
52
|
+
process.env.xbintsc_CLANG,
|
|
53
|
+
"clang",
|
|
54
|
+
"clang-18",
|
|
55
|
+
"clang-17",
|
|
56
|
+
"clang-16",
|
|
57
|
+
"cc",
|
|
58
|
+
].filter((candidate): candidate is string => Boolean(candidate));
|
|
59
|
+
for (const candidate of candidates) {
|
|
60
|
+
const result = runner.run(candidate, ["--version"]);
|
|
61
|
+
if (result.status === 0) return candidate;
|
|
62
|
+
}
|
|
63
|
+
throw new ToolchainError("clang --version", 1, "No C compiler found. Set xbintsc_CLANG to a clang binary.");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface CompileIrOptions {
|
|
67
|
+
readonly clang: string;
|
|
68
|
+
readonly irPath: string;
|
|
69
|
+
readonly objectPath: string;
|
|
70
|
+
readonly optimize: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Compile an LLVM IR file to a native object file. */
|
|
74
|
+
export function compileIr(runner: Runner, options: CompileIrOptions): void {
|
|
75
|
+
const result = runner.run(options.clang, [
|
|
76
|
+
`-O${options.optimize}`,
|
|
77
|
+
"-c",
|
|
78
|
+
options.irPath,
|
|
79
|
+
"-o",
|
|
80
|
+
options.objectPath,
|
|
81
|
+
]);
|
|
82
|
+
if (result.status !== 0) throw new ToolchainError(`${options.clang} ${options.irPath}`, result.status, result.stderr);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface LinkOptions {
|
|
86
|
+
readonly clang: string;
|
|
87
|
+
readonly objectPaths: readonly string[];
|
|
88
|
+
readonly outputPath: string;
|
|
89
|
+
readonly linkerFlags: readonly string[];
|
|
90
|
+
readonly optimize: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function link(runner: Runner, options: LinkOptions): void {
|
|
94
|
+
const args = [`-O${options.optimize}`, ...options.objectPaths, "-o", options.outputPath, ...options.linkerFlags];
|
|
95
|
+
const result = runner.run(options.clang, args);
|
|
96
|
+
if (result.status !== 0) throw new ToolchainError(`${options.clang} link`, result.status, result.stderr);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Compile a C source to an object file (used for the runtime and extensions). */
|
|
100
|
+
export function compileC(runner: Runner, clang: string, sourcePath: string, objectPath: string, includeDir: string): void {
|
|
101
|
+
const result = runner.run(clang, ["-O2", "-c", sourcePath, "-o", objectPath, `-I${includeDir}`]);
|
|
102
|
+
if (result.status !== 0) throw new ToolchainError(`${clang} ${sourcePath}`, result.status, result.stderr);
|
|
103
|
+
}
|