@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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +235 -0
  3. package/dist/analyzer/estimator.d.ts +33 -0
  4. package/dist/analyzer/estimator.js +273 -0
  5. package/dist/analyzer/graph-checker.d.ts +13 -0
  6. package/dist/analyzer/graph-checker.js +153 -0
  7. package/dist/analyzer/scope.d.ts +21 -0
  8. package/dist/analyzer/scope.js +324 -0
  9. package/dist/analyzer/types.d.ts +17 -0
  10. package/dist/analyzer/types.js +323 -0
  11. package/dist/codegen/agents.d.ts +2 -0
  12. package/dist/codegen/agents.js +109 -0
  13. package/dist/codegen/backend.d.ts +16 -0
  14. package/dist/codegen/backend.js +1 -0
  15. package/dist/codegen/claude-backend.d.ts +9 -0
  16. package/dist/codegen/claude-backend.js +47 -0
  17. package/dist/codegen/codegen.d.ts +10 -0
  18. package/dist/codegen/codegen.js +57 -0
  19. package/dist/codegen/hooks.d.ts +2 -0
  20. package/dist/codegen/hooks.js +165 -0
  21. package/dist/codegen/orchestration.d.ts +3 -0
  22. package/dist/codegen/orchestration.js +250 -0
  23. package/dist/codegen/settings.d.ts +36 -0
  24. package/dist/codegen/settings.js +87 -0
  25. package/dist/compiler.d.ts +21 -0
  26. package/dist/compiler.js +101 -0
  27. package/dist/constants.d.ts +9 -0
  28. package/dist/constants.js +13 -0
  29. package/dist/errors/diagnostics.d.ts +21 -0
  30. package/dist/errors/diagnostics.js +25 -0
  31. package/dist/format.d.ts +12 -0
  32. package/dist/format.js +46 -0
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.js +181 -0
  35. package/dist/lexer/lexer.d.ts +23 -0
  36. package/dist/lexer/lexer.js +268 -0
  37. package/dist/lexer/tokens.d.ts +96 -0
  38. package/dist/lexer/tokens.js +150 -0
  39. package/dist/lsp/features/code-actions.d.ts +7 -0
  40. package/dist/lsp/features/code-actions.js +58 -0
  41. package/dist/lsp/features/completions.d.ts +7 -0
  42. package/dist/lsp/features/completions.js +271 -0
  43. package/dist/lsp/features/definition.d.ts +3 -0
  44. package/dist/lsp/features/definition.js +32 -0
  45. package/dist/lsp/features/diagnostics.d.ts +4 -0
  46. package/dist/lsp/features/diagnostics.js +33 -0
  47. package/dist/lsp/features/hover.d.ts +7 -0
  48. package/dist/lsp/features/hover.js +88 -0
  49. package/dist/lsp/features/index.d.ts +9 -0
  50. package/dist/lsp/features/index.js +9 -0
  51. package/dist/lsp/features/references.d.ts +7 -0
  52. package/dist/lsp/features/references.js +53 -0
  53. package/dist/lsp/features/rename.d.ts +17 -0
  54. package/dist/lsp/features/rename.js +198 -0
  55. package/dist/lsp/features/symbols.d.ts +7 -0
  56. package/dist/lsp/features/symbols.js +74 -0
  57. package/dist/lsp/features/utils.d.ts +3 -0
  58. package/dist/lsp/features/utils.js +65 -0
  59. package/dist/lsp/features.d.ts +20 -0
  60. package/dist/lsp/features.js +513 -0
  61. package/dist/lsp/server.d.ts +2 -0
  62. package/dist/lsp/server.js +327 -0
  63. package/dist/parser/ast.d.ts +244 -0
  64. package/dist/parser/ast.js +10 -0
  65. package/dist/parser/parser.d.ts +95 -0
  66. package/dist/parser/parser.js +1175 -0
  67. package/dist/program-index.d.ts +21 -0
  68. package/dist/program-index.js +74 -0
  69. package/dist/resolver/resolver.d.ts +9 -0
  70. package/dist/resolver/resolver.js +136 -0
  71. package/dist/runner.d.ts +13 -0
  72. package/dist/runner.js +41 -0
  73. package/dist/runtime/executor.d.ts +56 -0
  74. package/dist/runtime/executor.js +285 -0
  75. package/dist/runtime/expr-eval.d.ts +3 -0
  76. package/dist/runtime/expr-eval.js +138 -0
  77. package/dist/runtime/flow-runner.d.ts +21 -0
  78. package/dist/runtime/flow-runner.js +230 -0
  79. package/dist/runtime/memory.d.ts +5 -0
  80. package/dist/runtime/memory.js +41 -0
  81. package/dist/runtime/prompt-builder.d.ts +12 -0
  82. package/dist/runtime/prompt-builder.js +66 -0
  83. package/dist/runtime/subprocess.d.ts +20 -0
  84. package/dist/runtime/subprocess.js +99 -0
  85. package/dist/runtime/token-tracker.d.ts +36 -0
  86. package/dist/runtime/token-tracker.js +56 -0
  87. package/dist/runtime/transforms.d.ts +2 -0
  88. package/dist/runtime/transforms.js +104 -0
  89. package/dist/types.d.ts +10 -0
  90. package/dist/types.js +1 -0
  91. package/dist/utils.d.ts +3 -0
  92. package/dist/utils.js +35 -0
  93. package/dist/version.d.ts +1 -0
  94. package/dist/version.js +11 -0
  95. 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
+ }
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
+ }