@jsleekr/graft 5.7.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 +235 -0
- package/dist/analyzer/estimator.d.ts +33 -0
- package/dist/analyzer/estimator.js +273 -0
- package/dist/analyzer/graph-checker.d.ts +13 -0
- package/dist/analyzer/graph-checker.js +153 -0
- package/dist/analyzer/scope.d.ts +21 -0
- package/dist/analyzer/scope.js +324 -0
- package/dist/analyzer/types.d.ts +17 -0
- package/dist/analyzer/types.js +323 -0
- package/dist/codegen/agents.d.ts +2 -0
- package/dist/codegen/agents.js +109 -0
- package/dist/codegen/backend.d.ts +16 -0
- package/dist/codegen/backend.js +1 -0
- package/dist/codegen/claude-backend.d.ts +9 -0
- package/dist/codegen/claude-backend.js +47 -0
- package/dist/codegen/codegen.d.ts +10 -0
- package/dist/codegen/codegen.js +57 -0
- package/dist/codegen/hooks.d.ts +2 -0
- package/dist/codegen/hooks.js +165 -0
- package/dist/codegen/orchestration.d.ts +3 -0
- package/dist/codegen/orchestration.js +250 -0
- package/dist/codegen/settings.d.ts +36 -0
- package/dist/codegen/settings.js +87 -0
- package/dist/compiler.d.ts +21 -0
- package/dist/compiler.js +101 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +13 -0
- package/dist/errors/diagnostics.d.ts +21 -0
- package/dist/errors/diagnostics.js +25 -0
- package/dist/format.d.ts +12 -0
- package/dist/format.js +46 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +181 -0
- package/dist/lexer/lexer.d.ts +23 -0
- package/dist/lexer/lexer.js +268 -0
- package/dist/lexer/tokens.d.ts +96 -0
- package/dist/lexer/tokens.js +150 -0
- package/dist/lsp/features/code-actions.d.ts +7 -0
- package/dist/lsp/features/code-actions.js +58 -0
- package/dist/lsp/features/completions.d.ts +7 -0
- package/dist/lsp/features/completions.js +271 -0
- package/dist/lsp/features/definition.d.ts +3 -0
- package/dist/lsp/features/definition.js +32 -0
- package/dist/lsp/features/diagnostics.d.ts +4 -0
- package/dist/lsp/features/diagnostics.js +33 -0
- package/dist/lsp/features/hover.d.ts +7 -0
- package/dist/lsp/features/hover.js +88 -0
- package/dist/lsp/features/index.d.ts +9 -0
- package/dist/lsp/features/index.js +9 -0
- package/dist/lsp/features/references.d.ts +7 -0
- package/dist/lsp/features/references.js +53 -0
- package/dist/lsp/features/rename.d.ts +17 -0
- package/dist/lsp/features/rename.js +198 -0
- package/dist/lsp/features/symbols.d.ts +7 -0
- package/dist/lsp/features/symbols.js +74 -0
- package/dist/lsp/features/utils.d.ts +3 -0
- package/dist/lsp/features/utils.js +65 -0
- package/dist/lsp/features.d.ts +20 -0
- package/dist/lsp/features.js +513 -0
- package/dist/lsp/server.d.ts +2 -0
- package/dist/lsp/server.js +327 -0
- package/dist/parser/ast.d.ts +244 -0
- package/dist/parser/ast.js +10 -0
- package/dist/parser/parser.d.ts +95 -0
- package/dist/parser/parser.js +1175 -0
- package/dist/program-index.d.ts +21 -0
- package/dist/program-index.js +74 -0
- package/dist/resolver/resolver.d.ts +9 -0
- package/dist/resolver/resolver.js +136 -0
- package/dist/runner.d.ts +13 -0
- package/dist/runner.js +41 -0
- package/dist/runtime/executor.d.ts +56 -0
- package/dist/runtime/executor.js +285 -0
- package/dist/runtime/expr-eval.d.ts +3 -0
- package/dist/runtime/expr-eval.js +138 -0
- package/dist/runtime/flow-runner.d.ts +21 -0
- package/dist/runtime/flow-runner.js +230 -0
- package/dist/runtime/memory.d.ts +5 -0
- package/dist/runtime/memory.js +41 -0
- package/dist/runtime/prompt-builder.d.ts +12 -0
- package/dist/runtime/prompt-builder.js +66 -0
- package/dist/runtime/subprocess.d.ts +20 -0
- package/dist/runtime/subprocess.js +99 -0
- package/dist/runtime/token-tracker.d.ts +36 -0
- package/dist/runtime/token-tracker.js +56 -0
- package/dist/runtime/transforms.d.ts +2 -0
- package/dist/runtime/transforms.js +104 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +35 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +11 -0
- package/package.json +70 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const MODEL_MAP = {
|
|
2
|
+
sonnet: 'claude-sonnet-4-20250514',
|
|
3
|
+
opus: 'claude-opus-4-20250514',
|
|
4
|
+
haiku: 'claude-haiku-4-5-20251001',
|
|
5
|
+
};
|
|
6
|
+
/** Fraction of tokens estimated for a single-field read (vs full context). */
|
|
7
|
+
export const PARTIAL_FIELD_FACTOR = 0.3;
|
|
8
|
+
/** Budget fraction at which to emit a warning. */
|
|
9
|
+
export const BUDGET_WARNING_THRESHOLD = 0.8;
|
|
10
|
+
/** Budget fraction at which to emit a critical warning. */
|
|
11
|
+
export const BUDGET_CRITICAL_THRESHOLD = 0.9;
|
|
12
|
+
/** Maximum depth for conditional chain traversal in both estimation and runtime. */
|
|
13
|
+
export const MAX_CONDITIONAL_HOPS = 10;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface SourceLocation {
|
|
2
|
+
line: number;
|
|
3
|
+
column: number;
|
|
4
|
+
offset: number;
|
|
5
|
+
length?: number;
|
|
6
|
+
}
|
|
7
|
+
export type ParseErrorCode = 'PARSE_UNEXPECTED_TOKEN' | 'PARSE_MISSING_FIELD';
|
|
8
|
+
export type ScopeErrorCode = 'SCOPE_DUPLICATE_NAME' | 'SCOPE_UNDEFINED_REF' | 'SCOPE_FIELD_NOT_FOUND' | 'SCOPE_INVALID_WRITES' | 'SCOPE_INVALID_FOREACH' | 'SCOPE_PARALLEL_WRITES' | 'SCOPE_MAX_TOKENS_INVALID' | 'SCOPE_INVALID_FALLBACK' | 'SCOPE_FALLBACK_CYCLE' | 'SCOPE_BINDING_COLLISION' | 'SCOPE_VAR_COLLISION' | 'SCOPE_VAR_UNDECLARED' | 'SCOPE_VAR_ORDER' | 'SCOPE_GRAPH_RECURSION' | 'SCOPE_GRAPH_PARAM_MISSING' | 'SCOPE_GRAPH_PARAM_TYPE' | 'SCOPE_UNKNOWN_FUNCTION';
|
|
9
|
+
export type TypeErrorCode = 'TYPE_FIELD_NOT_FOUND' | 'TYPE_SCHEMA_MISMATCH' | 'TYPE_WRITE_FIELD_OVERLAP' | 'TYPE_CONDITION_MISMATCH' | 'TYPE_EXPR_MISMATCH' | 'TYPE_VAR_CONDITION' | 'TYPE_FUNC_ARITY' | 'TYPE_CONDITIONAL_MISMATCH';
|
|
10
|
+
export type BudgetErrorCode = 'BUDGET_EXCEEDED' | 'BUDGET_NODE_EXCEEDED' | 'BUDGET_CHAIN_CYCLE' | 'BUDGET_CHAIN_DEPTH';
|
|
11
|
+
export type ImportErrorCode = 'IMPORT_CIRCULAR' | 'IMPORT_NOT_FOUND' | 'IMPORT_NAME_NOT_FOUND' | 'IMPORT_DUPLICATE_NAME' | 'IMPORT_INVALID_PATH' | 'IMPORT_PARSE_ERROR';
|
|
12
|
+
export type GraphErrorCode = 'GRAPH_MISSING' | 'GRAPH_MULTIPLE';
|
|
13
|
+
export type ConfigErrorCode = 'CONFIG_UNKNOWN_BACKEND';
|
|
14
|
+
export type GraftErrorCode = ParseErrorCode | ScopeErrorCode | TypeErrorCode | BudgetErrorCode | ImportErrorCode | GraphErrorCode | ConfigErrorCode;
|
|
15
|
+
export declare class GraftError extends Error {
|
|
16
|
+
readonly location: SourceLocation;
|
|
17
|
+
readonly severity: 'error' | 'warning';
|
|
18
|
+
readonly code?: GraftErrorCode | undefined;
|
|
19
|
+
constructor(message: string, location: SourceLocation, severity?: 'error' | 'warning', code?: GraftErrorCode | undefined);
|
|
20
|
+
format(source: string): string;
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export class GraftError extends Error {
|
|
2
|
+
location;
|
|
3
|
+
severity;
|
|
4
|
+
code;
|
|
5
|
+
constructor(message, location, severity = 'error', code) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.location = location;
|
|
8
|
+
this.severity = severity;
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.name = 'GraftError';
|
|
11
|
+
}
|
|
12
|
+
format(source) {
|
|
13
|
+
const lines = source.split('\n');
|
|
14
|
+
const lineIdx = this.location.line - 1;
|
|
15
|
+
const line = (lineIdx >= 0 && lineIdx < lines.length) ? lines[lineIdx] : '';
|
|
16
|
+
const col = Math.max(0, this.location.column - 1);
|
|
17
|
+
const pointer = ' '.repeat(col) + '^';
|
|
18
|
+
return [
|
|
19
|
+
`Error at line ${this.location.line}:${this.location.column}:`,
|
|
20
|
+
` ${line}`,
|
|
21
|
+
` ${pointer}`,
|
|
22
|
+
` ${this.message}`,
|
|
23
|
+
].join('\n');
|
|
24
|
+
}
|
|
25
|
+
}
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { TokenReport } from './analyzer/estimator.js';
|
|
2
|
+
import { Expr } from './parser/ast.js';
|
|
3
|
+
export interface FormatOptions {
|
|
4
|
+
/** Include budget comparison on best/worst path lines */
|
|
5
|
+
showBudget?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Format a TokenReport into human-readable lines for CLI output.
|
|
9
|
+
* Returns a single string with newlines.
|
|
10
|
+
*/
|
|
11
|
+
export declare function formatTokenReport(report: TokenReport, options?: FormatOptions): string;
|
|
12
|
+
export declare function formatExpr(expr: Expr): string;
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format a TokenReport into human-readable lines for CLI output.
|
|
3
|
+
* Returns a single string with newlines.
|
|
4
|
+
*/
|
|
5
|
+
export function formatTokenReport(report, options) {
|
|
6
|
+
const lines = [];
|
|
7
|
+
const showBudget = options?.showBudget ?? false;
|
|
8
|
+
for (const node of report.nodes) {
|
|
9
|
+
lines.push(` ${node.name.padEnd(20)} in ~${node.estimatedIn.toLocaleString('en-US').padStart(6)} out ~${node.estimatedOut.toLocaleString('en-US').padStart(6)}`);
|
|
10
|
+
}
|
|
11
|
+
if (showBudget) {
|
|
12
|
+
const bestOk = report.bestCase <= report.budget;
|
|
13
|
+
const worstOk = report.worstCase <= report.budget;
|
|
14
|
+
lines.push(` Best path: ${report.bestCase.toLocaleString('en-US').padStart(8)} tokens ${bestOk ? '\u2713' : '\u2717'} ${bestOk ? 'within' : 'exceeds'} budget (${report.budget.toLocaleString('en-US')})`);
|
|
15
|
+
lines.push(` Worst path: ${report.worstCase.toLocaleString('en-US').padStart(8)} tokens ${worstOk ? '\u2713' : '\u26A0'} ${worstOk ? 'within' : 'exceeds'} budget (${report.budget.toLocaleString('en-US')})`);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
lines.push(` Best path: ${report.bestCase.toLocaleString('en-US').padStart(8)} tokens`);
|
|
19
|
+
lines.push(` Worst path: ${report.worstCase.toLocaleString('en-US').padStart(8)} tokens`);
|
|
20
|
+
}
|
|
21
|
+
return lines.join('\n');
|
|
22
|
+
}
|
|
23
|
+
export function formatExpr(expr) {
|
|
24
|
+
switch (expr.kind) {
|
|
25
|
+
case 'literal':
|
|
26
|
+
return typeof expr.value === 'string' ? `"${expr.value}"` : String(expr.value);
|
|
27
|
+
case 'field_access':
|
|
28
|
+
return expr.segments.join('.');
|
|
29
|
+
case 'binary':
|
|
30
|
+
return `${formatExpr(expr.left)} ${expr.op} ${formatExpr(expr.right)}`;
|
|
31
|
+
case 'unary':
|
|
32
|
+
return `${expr.op}${formatExpr(expr.operand)}`;
|
|
33
|
+
case 'group':
|
|
34
|
+
return `(${formatExpr(expr.inner)})`;
|
|
35
|
+
case 'call':
|
|
36
|
+
return `${expr.name}(${expr.args.map(formatExpr).join(', ')})`;
|
|
37
|
+
case 'template':
|
|
38
|
+
return '"' + expr.parts.map(p => p.kind === 'text' ? p.value : `\${${formatExpr(p.value)}}`).join('') + '"';
|
|
39
|
+
case 'conditional':
|
|
40
|
+
return `if ${formatExpr(expr.condition)} then ${formatExpr(expr.consequent)} else ${formatExpr(expr.alternate)}`;
|
|
41
|
+
default: {
|
|
42
|
+
const _exhaustive = expr;
|
|
43
|
+
return _exhaustive;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { compile, compileAndWrite } from './compiler.js';
|
|
6
|
+
import { VERSION } from './version.js';
|
|
7
|
+
import { formatTokenReport } from './format.js';
|
|
8
|
+
const KNOWN_BACKENDS = new Set(['claude']);
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name('graft')
|
|
12
|
+
.description('Graft compiler — graph-native language for AI agent harness engineering')
|
|
13
|
+
.version(VERSION);
|
|
14
|
+
program
|
|
15
|
+
.command('compile')
|
|
16
|
+
.description('Compile .gft source to Claude Code harness structure')
|
|
17
|
+
.argument('<file>', '.gft source file')
|
|
18
|
+
.option('--out-dir <dir>', 'output directory', '.')
|
|
19
|
+
.option('--backend <name>', 'codegen backend', 'claude')
|
|
20
|
+
.action((file, opts) => {
|
|
21
|
+
if (!KNOWN_BACKENDS.has(opts.backend)) {
|
|
22
|
+
console.error(`Error: unknown backend '${opts.backend}'. Available: ${[...KNOWN_BACKENDS].join(', ')}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const source = readSource(file);
|
|
26
|
+
let result;
|
|
27
|
+
try {
|
|
28
|
+
result = compileAndWrite(source, path.resolve(file), path.resolve(opts.outDir));
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
if (!result.success) {
|
|
35
|
+
console.error('\n✗ Compilation failed:\n');
|
|
36
|
+
for (const err of result.errors) {
|
|
37
|
+
console.error(err.format(source));
|
|
38
|
+
console.error('');
|
|
39
|
+
}
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
console.log('\n✓ Parse OK');
|
|
43
|
+
console.log('✓ Scope check OK');
|
|
44
|
+
console.log('✓ Type check OK');
|
|
45
|
+
if (result.report) {
|
|
46
|
+
console.log('✓ Token analysis:');
|
|
47
|
+
console.log(formatTokenReport(result.report, { showBudget: true }));
|
|
48
|
+
}
|
|
49
|
+
for (const w of result.warnings) {
|
|
50
|
+
console.log(`\n⚠ ${w.message}`);
|
|
51
|
+
}
|
|
52
|
+
if (result.files) {
|
|
53
|
+
console.log('\nGenerated:');
|
|
54
|
+
for (const f of result.files) {
|
|
55
|
+
console.log(` ${f.path}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
console.log('');
|
|
59
|
+
});
|
|
60
|
+
program
|
|
61
|
+
.command('check')
|
|
62
|
+
.description('Validate .gft source without writing files')
|
|
63
|
+
.argument('<file>', '.gft source file')
|
|
64
|
+
.action((file) => {
|
|
65
|
+
const source = readSource(file);
|
|
66
|
+
const result = compile(source, path.resolve(file));
|
|
67
|
+
if (!result.success) {
|
|
68
|
+
console.error('\n✗ Check failed:\n');
|
|
69
|
+
for (const err of result.errors) {
|
|
70
|
+
console.error(err.format(source));
|
|
71
|
+
console.error('');
|
|
72
|
+
}
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
console.log('\n✓ Parse OK');
|
|
76
|
+
console.log('✓ Scope check OK');
|
|
77
|
+
console.log('✓ Type check OK');
|
|
78
|
+
if (result.report) {
|
|
79
|
+
console.log('✓ Token analysis:');
|
|
80
|
+
console.log(formatTokenReport(result.report));
|
|
81
|
+
}
|
|
82
|
+
for (const w of result.warnings) {
|
|
83
|
+
console.log(`\n⚠ ${w.message}`);
|
|
84
|
+
}
|
|
85
|
+
console.log('');
|
|
86
|
+
});
|
|
87
|
+
program
|
|
88
|
+
.command('run')
|
|
89
|
+
.description('Compile and execute a .gft pipeline')
|
|
90
|
+
.argument('<file>', '.gft source file')
|
|
91
|
+
.option('--input <file>', 'input JSON file')
|
|
92
|
+
.option('--dry-run', 'simulate execution without spawning subprocesses')
|
|
93
|
+
.option('--verbose', 'print execution details')
|
|
94
|
+
.option('--timeout <seconds>', 'subprocess timeout in seconds', '300')
|
|
95
|
+
.option('--work-dir <dir>', 'working directory for execution')
|
|
96
|
+
.action(async (file, opts) => {
|
|
97
|
+
const { run } = await import('./runner.js');
|
|
98
|
+
const result = await run({
|
|
99
|
+
sourceFile: file,
|
|
100
|
+
inputFile: opts.input,
|
|
101
|
+
workDir: opts.workDir,
|
|
102
|
+
dryRun: opts.dryRun,
|
|
103
|
+
verbose: opts.verbose,
|
|
104
|
+
timeoutMs: parseInt(opts.timeout, 10) * 1000,
|
|
105
|
+
});
|
|
106
|
+
if (!result.success) {
|
|
107
|
+
console.error('\nExecution failed:');
|
|
108
|
+
for (const err of result.errors)
|
|
109
|
+
console.error(` ${err}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
console.log(`\nGraph '${result.graph}' completed in ${result.totalDurationMs}ms`);
|
|
113
|
+
console.log(`Nodes executed: ${result.nodeResults.length}`);
|
|
114
|
+
for (const nr of result.nodeResults) {
|
|
115
|
+
const status = nr.success ? 'OK' : 'FAILED';
|
|
116
|
+
console.log(` ${nr.node.padEnd(20)} ${status.padEnd(8)} ${nr.durationMs}ms`);
|
|
117
|
+
}
|
|
118
|
+
if (result.finalOutput !== null) {
|
|
119
|
+
console.log('\nFinal output:');
|
|
120
|
+
console.log(JSON.stringify(result.finalOutput, null, 2));
|
|
121
|
+
}
|
|
122
|
+
console.log('');
|
|
123
|
+
});
|
|
124
|
+
program
|
|
125
|
+
.command('init')
|
|
126
|
+
.description('Scaffold a new Graft project')
|
|
127
|
+
.argument('<name>', 'project name')
|
|
128
|
+
.action((name) => {
|
|
129
|
+
const dir = path.resolve(name);
|
|
130
|
+
if (fs.existsSync(dir)) {
|
|
131
|
+
console.error(`Error: directory '${name}' already exists`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
135
|
+
const baseName = path.basename(name);
|
|
136
|
+
const safeName = baseName.replace(/[^a-zA-Z0-9]/g, '_').replace(/^_+|_+$/g, '') || 'pipeline';
|
|
137
|
+
fs.writeFileSync(path.join(dir, 'pipeline.gft'), `// ${safeName} — a simple two-node pipeline
|
|
138
|
+
|
|
139
|
+
context Input(max_tokens: 500) {
|
|
140
|
+
question: String
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
node Analyst(model: sonnet, budget: 4k/2k) {
|
|
144
|
+
reads: [Input]
|
|
145
|
+
produces Analysis {
|
|
146
|
+
answer: String
|
|
147
|
+
confidence: Float(0..1)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
node Reviewer(model: haiku, budget: 2k/1k) {
|
|
152
|
+
reads: [Analysis]
|
|
153
|
+
produces Output {
|
|
154
|
+
final_answer: String
|
|
155
|
+
approved: Bool
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
edge Analyst -> Reviewer | select(answer, confidence) | compact
|
|
160
|
+
|
|
161
|
+
graph ${safeName}(input: Input, output: Output, budget: 10k) {
|
|
162
|
+
Analyst -> Reviewer -> done
|
|
163
|
+
}
|
|
164
|
+
`);
|
|
165
|
+
console.log(`\nCreated ${name}/`);
|
|
166
|
+
console.log(` pipeline.gft`);
|
|
167
|
+
console.log(`\nNext steps:`);
|
|
168
|
+
console.log(` cd ${name}`);
|
|
169
|
+
console.log(` graft compile pipeline.gft`);
|
|
170
|
+
console.log(` # Open in Claude Code to run the pipeline`);
|
|
171
|
+
console.log('');
|
|
172
|
+
});
|
|
173
|
+
function readSource(file) {
|
|
174
|
+
const resolved = path.resolve(file);
|
|
175
|
+
if (!fs.existsSync(resolved)) {
|
|
176
|
+
console.error(`Error: file not found: ${resolved}`);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
return fs.readFileSync(resolved, 'utf-8');
|
|
180
|
+
}
|
|
181
|
+
program.parse();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Token } from './tokens.js';
|
|
2
|
+
export declare class Lexer {
|
|
3
|
+
private readonly source;
|
|
4
|
+
private pos;
|
|
5
|
+
private line;
|
|
6
|
+
private column;
|
|
7
|
+
private tokens;
|
|
8
|
+
constructor(source: string);
|
|
9
|
+
tokenize(): Token[];
|
|
10
|
+
private skipWhitespace;
|
|
11
|
+
private skipLineComment;
|
|
12
|
+
private skipBlockComment;
|
|
13
|
+
private readString;
|
|
14
|
+
private readNumber;
|
|
15
|
+
private readIdentifierOrKeyword;
|
|
16
|
+
private readSymbol;
|
|
17
|
+
private matchTwoChar;
|
|
18
|
+
private peek;
|
|
19
|
+
private location;
|
|
20
|
+
private isDigit;
|
|
21
|
+
private isAlpha;
|
|
22
|
+
private isAlphaNumeric;
|
|
23
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { TokenType, KEYWORDS } from './tokens.js';
|
|
2
|
+
import { GraftError } from '../errors/diagnostics.js';
|
|
3
|
+
const SINGLE_CHAR = {
|
|
4
|
+
'{': TokenType.LBrace,
|
|
5
|
+
'}': TokenType.RBrace,
|
|
6
|
+
'(': TokenType.LParen,
|
|
7
|
+
')': TokenType.RParen,
|
|
8
|
+
'[': TokenType.LBracket,
|
|
9
|
+
']': TokenType.RBracket,
|
|
10
|
+
':': TokenType.Colon,
|
|
11
|
+
',': TokenType.Comma,
|
|
12
|
+
'.': TokenType.Dot,
|
|
13
|
+
'|': TokenType.Pipe,
|
|
14
|
+
'/': TokenType.Slash,
|
|
15
|
+
'*': TokenType.Star,
|
|
16
|
+
'%': TokenType.Percent,
|
|
17
|
+
'+': TokenType.Plus,
|
|
18
|
+
'-': TokenType.Minus,
|
|
19
|
+
'!': TokenType.Bang,
|
|
20
|
+
'=': TokenType.Equals,
|
|
21
|
+
'>': TokenType.Greater,
|
|
22
|
+
'<': TokenType.Less,
|
|
23
|
+
};
|
|
24
|
+
export class Lexer {
|
|
25
|
+
source;
|
|
26
|
+
pos = 0;
|
|
27
|
+
line = 1;
|
|
28
|
+
column = 1;
|
|
29
|
+
tokens = [];
|
|
30
|
+
constructor(source) {
|
|
31
|
+
this.source = source;
|
|
32
|
+
}
|
|
33
|
+
tokenize() {
|
|
34
|
+
this.tokens = [];
|
|
35
|
+
while (this.pos < this.source.length) {
|
|
36
|
+
this.skipWhitespace();
|
|
37
|
+
if (this.pos >= this.source.length)
|
|
38
|
+
break;
|
|
39
|
+
const ch = this.source[this.pos];
|
|
40
|
+
// Comments
|
|
41
|
+
if (ch === '/' && this.peek(1) === '/') {
|
|
42
|
+
this.skipLineComment();
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (ch === '/' && this.peek(1) === '*') {
|
|
46
|
+
this.skipBlockComment();
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// String literals
|
|
50
|
+
if (ch === '"') {
|
|
51
|
+
this.readString();
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// Numbers (integer, k-integer, float)
|
|
55
|
+
if (this.isDigit(ch)) {
|
|
56
|
+
this.readNumber();
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// Identifiers and keywords
|
|
60
|
+
if (this.isAlpha(ch)) {
|
|
61
|
+
this.readIdentifierOrKeyword();
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Symbols
|
|
65
|
+
if (this.readSymbol()) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
throw new GraftError(`Unexpected character '${ch}'`, this.location());
|
|
69
|
+
}
|
|
70
|
+
this.tokens.push({ type: TokenType.EOF, value: '', location: this.location() });
|
|
71
|
+
return this.tokens;
|
|
72
|
+
}
|
|
73
|
+
skipWhitespace() {
|
|
74
|
+
while (this.pos < this.source.length) {
|
|
75
|
+
const ch = this.source[this.pos];
|
|
76
|
+
if (ch === '\n') {
|
|
77
|
+
this.pos++;
|
|
78
|
+
this.line++;
|
|
79
|
+
this.column = 1;
|
|
80
|
+
}
|
|
81
|
+
else if (ch === '\r') {
|
|
82
|
+
this.pos++;
|
|
83
|
+
if (this.pos < this.source.length && this.source[this.pos] === '\n') {
|
|
84
|
+
this.pos++;
|
|
85
|
+
}
|
|
86
|
+
this.line++;
|
|
87
|
+
this.column = 1;
|
|
88
|
+
}
|
|
89
|
+
else if (ch === ' ' || ch === '\t') {
|
|
90
|
+
this.pos++;
|
|
91
|
+
this.column++;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
skipLineComment() {
|
|
99
|
+
this.pos += 2;
|
|
100
|
+
this.column += 2;
|
|
101
|
+
while (this.pos < this.source.length && this.source[this.pos] !== '\n') {
|
|
102
|
+
this.pos++;
|
|
103
|
+
this.column++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
skipBlockComment() {
|
|
107
|
+
const loc = this.location();
|
|
108
|
+
this.pos += 2;
|
|
109
|
+
this.column += 2;
|
|
110
|
+
while (this.pos < this.source.length) {
|
|
111
|
+
if (this.source[this.pos] === '*' && this.peek(1) === '/') {
|
|
112
|
+
this.pos += 2;
|
|
113
|
+
this.column += 2;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (this.source[this.pos] === '\n') {
|
|
117
|
+
this.line++;
|
|
118
|
+
this.column = 1;
|
|
119
|
+
this.pos++;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
this.pos++;
|
|
123
|
+
this.column++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
throw new GraftError('Unterminated block comment', loc);
|
|
127
|
+
}
|
|
128
|
+
readString() {
|
|
129
|
+
const loc = this.location();
|
|
130
|
+
this.pos++;
|
|
131
|
+
this.column++;
|
|
132
|
+
let value = '';
|
|
133
|
+
let hasInterpolation = false;
|
|
134
|
+
while (this.pos < this.source.length && this.source[this.pos] !== '"') {
|
|
135
|
+
if (this.source[this.pos] === '\n') {
|
|
136
|
+
throw new GraftError('Unterminated string literal', loc);
|
|
137
|
+
}
|
|
138
|
+
// Check for escape: \${ produces literal ${
|
|
139
|
+
if (this.source[this.pos] === '\\' && this.pos + 1 < this.source.length && this.source[this.pos + 1] === '$' && this.pos + 2 < this.source.length && this.source[this.pos + 2] === '{') {
|
|
140
|
+
value += '${';
|
|
141
|
+
this.pos += 3;
|
|
142
|
+
this.column += 3;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
// Check for interpolation: ${ marks this as a template string
|
|
146
|
+
if (this.source[this.pos] === '$' && this.pos + 1 < this.source.length && this.source[this.pos + 1] === '{') {
|
|
147
|
+
hasInterpolation = true;
|
|
148
|
+
}
|
|
149
|
+
value += this.source[this.pos];
|
|
150
|
+
this.pos++;
|
|
151
|
+
this.column++;
|
|
152
|
+
}
|
|
153
|
+
if (this.pos >= this.source.length) {
|
|
154
|
+
throw new GraftError('Unterminated string literal', loc);
|
|
155
|
+
}
|
|
156
|
+
this.pos++;
|
|
157
|
+
this.column++;
|
|
158
|
+
const type = hasInterpolation ? TokenType.TemplateString : TokenType.StringLiteral;
|
|
159
|
+
this.tokens.push({ type, value, location: { ...loc, length: value.length + 2 } });
|
|
160
|
+
}
|
|
161
|
+
readNumber() {
|
|
162
|
+
const loc = this.location();
|
|
163
|
+
let value = '';
|
|
164
|
+
while (this.pos < this.source.length && this.isDigit(this.source[this.pos])) {
|
|
165
|
+
value += this.source[this.pos];
|
|
166
|
+
this.pos++;
|
|
167
|
+
this.column++;
|
|
168
|
+
}
|
|
169
|
+
// Check for k-suffix
|
|
170
|
+
if (this.pos < this.source.length && this.source[this.pos] === 'k') {
|
|
171
|
+
value += 'k';
|
|
172
|
+
this.pos++;
|
|
173
|
+
this.column++;
|
|
174
|
+
this.tokens.push({ type: TokenType.KIntegerLiteral, value, location: { ...loc, length: value.length } });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// Check for float (requires digit after dot; dot-dot is range operator)
|
|
178
|
+
if (this.pos < this.source.length &&
|
|
179
|
+
this.source[this.pos] === '.' &&
|
|
180
|
+
this.peek(1) !== undefined &&
|
|
181
|
+
this.isDigit(this.peek(1))) {
|
|
182
|
+
value += '.';
|
|
183
|
+
this.pos++;
|
|
184
|
+
this.column++;
|
|
185
|
+
while (this.pos < this.source.length && this.isDigit(this.source[this.pos])) {
|
|
186
|
+
value += this.source[this.pos];
|
|
187
|
+
this.pos++;
|
|
188
|
+
this.column++;
|
|
189
|
+
}
|
|
190
|
+
this.tokens.push({ type: TokenType.FloatLiteral, value, location: { ...loc, length: value.length } });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this.tokens.push({ type: TokenType.IntegerLiteral, value, location: { ...loc, length: value.length } });
|
|
194
|
+
}
|
|
195
|
+
readIdentifierOrKeyword() {
|
|
196
|
+
const loc = this.location();
|
|
197
|
+
let value = '';
|
|
198
|
+
while (this.pos < this.source.length && this.isAlphaNumeric(this.source[this.pos])) {
|
|
199
|
+
value += this.source[this.pos];
|
|
200
|
+
this.pos++;
|
|
201
|
+
this.column++;
|
|
202
|
+
}
|
|
203
|
+
const keywordType = KEYWORDS[value];
|
|
204
|
+
this.tokens.push({
|
|
205
|
+
type: keywordType ?? TokenType.Identifier,
|
|
206
|
+
value,
|
|
207
|
+
location: { ...loc, length: value.length },
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
readSymbol() {
|
|
211
|
+
const loc = this.location();
|
|
212
|
+
const ch = this.source[this.pos];
|
|
213
|
+
const next = this.peek(1);
|
|
214
|
+
// Two-character symbols first (maximal munch)
|
|
215
|
+
const twoChar = this.matchTwoChar(ch, next);
|
|
216
|
+
if (twoChar) {
|
|
217
|
+
this.tokens.push({ type: twoChar[0], value: twoChar[1], location: { ...loc, length: 2 } });
|
|
218
|
+
this.pos += 2;
|
|
219
|
+
this.column += 2;
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
// Single-character symbols
|
|
223
|
+
const singleType = SINGLE_CHAR[ch];
|
|
224
|
+
if (singleType !== undefined) {
|
|
225
|
+
this.tokens.push({ type: singleType, value: ch, location: { ...loc, length: 1 } });
|
|
226
|
+
this.pos++;
|
|
227
|
+
this.column++;
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
matchTwoChar(ch, next) {
|
|
233
|
+
if (ch === '-' && next === '>')
|
|
234
|
+
return [TokenType.Arrow, '->'];
|
|
235
|
+
if (ch === '.' && next === '.')
|
|
236
|
+
return [TokenType.DotDot, '..'];
|
|
237
|
+
if (ch === '>' && next === '=')
|
|
238
|
+
return [TokenType.GreaterEqual, '>='];
|
|
239
|
+
if (ch === '<' && next === '=')
|
|
240
|
+
return [TokenType.LessEqual, '<='];
|
|
241
|
+
if (ch === '=' && next === '=')
|
|
242
|
+
return [TokenType.EqualEqual, '=='];
|
|
243
|
+
if (ch === '!' && next === '=')
|
|
244
|
+
return [TokenType.BangEqual, '!='];
|
|
245
|
+
if (ch === '&' && next === '&')
|
|
246
|
+
return [TokenType.AmpAmp, '&&'];
|
|
247
|
+
if (ch === '|' && next === '|')
|
|
248
|
+
return [TokenType.PipePipe, '||'];
|
|
249
|
+
if (ch === '?' && next === '?')
|
|
250
|
+
return [TokenType.QuestionQuestion, '??'];
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
peek(offset) {
|
|
254
|
+
return this.source[this.pos + offset];
|
|
255
|
+
}
|
|
256
|
+
location() {
|
|
257
|
+
return { line: this.line, column: this.column, offset: this.pos };
|
|
258
|
+
}
|
|
259
|
+
isDigit(ch) {
|
|
260
|
+
return ch >= '0' && ch <= '9';
|
|
261
|
+
}
|
|
262
|
+
isAlpha(ch) {
|
|
263
|
+
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_';
|
|
264
|
+
}
|
|
265
|
+
isAlphaNumeric(ch) {
|
|
266
|
+
return this.isAlpha(ch) || this.isDigit(ch);
|
|
267
|
+
}
|
|
268
|
+
}
|