spl-lang 0.0.0-stage → 0.1.1
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/README.md +147 -3
- package/dist/ast.d.ts +177 -0
- package/dist/ast.js +1 -0
- package/dist/auth.d.ts +19 -0
- package/dist/auth.js +110 -0
- package/dist/builtins.d.ts +8 -0
- package/dist/builtins.js +350 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +162 -0
- package/dist/copilot.d.ts +29 -0
- package/dist/copilot.js +192 -0
- package/dist/errors.d.ts +31 -0
- package/dist/errors.js +52 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +7 -0
- package/dist/interpreter.d.ts +25 -0
- package/dist/interpreter.js +546 -0
- package/dist/lexer.d.ts +43 -0
- package/dist/lexer.js +208 -0
- package/dist/llm.d.ts +46 -0
- package/dist/llm.js +54 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +27 -0
- package/dist/parser.d.ts +13 -0
- package/dist/parser.js +709 -0
- package/dist/restrict.d.ts +27 -0
- package/dist/restrict.js +158 -0
- package/dist/store.d.ts +20 -0
- package/dist/store.js +52 -0
- package/dist/tools.d.ts +38 -0
- package/dist/tools.js +266 -0
- package/dist/values.d.ts +49 -0
- package/dist/values.js +131 -0
- package/docs/syntax.ebnf +97 -0
- package/package.json +39 -6
package/dist/lexer.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { SplSyntaxError } from './errors.js';
|
|
2
|
+
const OPS = [
|
|
3
|
+
'//=', '**', '//', '==', '!=', '<=', '>=', '+=', '-=', '*=', '/=', '%=',
|
|
4
|
+
'+', '-', '*', '/', '%', '<', '>', '=', '(', ')', '[', ']', '{', '}', ',', ':', '.', '?', '@', ';',
|
|
5
|
+
];
|
|
6
|
+
const ESCAPES = {
|
|
7
|
+
n: '\n', t: '\t', r: '\r', '0': '\0', '\\': '\\', "'": "'", '"': '"', '{': '{', '}': '}',
|
|
8
|
+
};
|
|
9
|
+
const isDigit = (c) => c >= '0' && c <= '9';
|
|
10
|
+
const isNameStart = (c) => /[A-Za-z_]/.test(c);
|
|
11
|
+
const isNameChar = (c) => /[A-Za-z0-9_]/.test(c);
|
|
12
|
+
export function tokenize(src, opts = {}) {
|
|
13
|
+
const tokens = [];
|
|
14
|
+
let i = 0;
|
|
15
|
+
let line = opts.line ?? 1;
|
|
16
|
+
let col = opts.col ?? 1;
|
|
17
|
+
let depth = opts.inExpr ? 1 : 0;
|
|
18
|
+
const brackets = [];
|
|
19
|
+
const err = (msg, l = line, c = col) => {
|
|
20
|
+
throw new SplSyntaxError(msg, l, c);
|
|
21
|
+
};
|
|
22
|
+
const advance = (n = 1) => {
|
|
23
|
+
for (let k = 0; k < n; k++) {
|
|
24
|
+
if (src[i] === '\n') {
|
|
25
|
+
line++;
|
|
26
|
+
col = 1;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
col++;
|
|
30
|
+
}
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
while (i < src.length) {
|
|
35
|
+
const c = src[i];
|
|
36
|
+
if (c === ' ' || c === '\t' || c === '\r') {
|
|
37
|
+
advance();
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (c === '#') {
|
|
41
|
+
while (i < src.length && src[i] !== '\n')
|
|
42
|
+
advance();
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (c === '\\' && (src[i + 1] === '\n' || (src[i + 1] === '\r' && src[i + 2] === '\n'))) {
|
|
46
|
+
advance(src[i + 1] === '\r' ? 3 : 2);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (c === '\n') {
|
|
50
|
+
if (depth === 0)
|
|
51
|
+
tokens.push({ type: 'nl', line, col });
|
|
52
|
+
advance();
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const startLine = line;
|
|
56
|
+
const startCol = col;
|
|
57
|
+
if (isDigit(c)) {
|
|
58
|
+
const m = /^\d+(\.\d+)?([eE][+-]?\d+)?/.exec(src.slice(i));
|
|
59
|
+
advance(m[0].length);
|
|
60
|
+
if (i < src.length && isNameChar(src[i]))
|
|
61
|
+
err('invalid number literal', startLine, startCol);
|
|
62
|
+
tokens.push({ type: 'num', value: Number(m[0]), line: startLine, col: startCol });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const quoteAt = (k) => src[k] === "'" || src[k] === '"';
|
|
66
|
+
if (quoteAt(i) || (c === 'r' && quoteAt(i + 1))) {
|
|
67
|
+
const raw = c === 'r';
|
|
68
|
+
if (raw)
|
|
69
|
+
advance();
|
|
70
|
+
tokens.push(readString(raw));
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (isNameStart(c)) {
|
|
74
|
+
let j = i;
|
|
75
|
+
while (j < src.length && isNameChar(src[j]))
|
|
76
|
+
j++;
|
|
77
|
+
const value = src.slice(i, j);
|
|
78
|
+
advance(j - i);
|
|
79
|
+
tokens.push({ type: 'name', value, line: startLine, col: startCol });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const op = OPS.find((o) => src.startsWith(o, i));
|
|
83
|
+
if (!op)
|
|
84
|
+
err(`unexpected character '${c}'`);
|
|
85
|
+
advance(op.length);
|
|
86
|
+
if (op === '(' || op === '[') {
|
|
87
|
+
brackets.push(op);
|
|
88
|
+
depth++;
|
|
89
|
+
}
|
|
90
|
+
else if (op === ')' || op === ']') {
|
|
91
|
+
const open = brackets.pop();
|
|
92
|
+
if ((op === ')' && open !== '(') || (op === ']' && open !== '[')) {
|
|
93
|
+
err(`unbalanced '${op}'`, startLine, startCol);
|
|
94
|
+
}
|
|
95
|
+
depth--;
|
|
96
|
+
}
|
|
97
|
+
tokens.push({ type: 'op', value: op, line: startLine, col: startCol });
|
|
98
|
+
}
|
|
99
|
+
if (brackets.length)
|
|
100
|
+
err(`unclosed '${brackets[brackets.length - 1]}'`);
|
|
101
|
+
tokens.push({ type: 'eof', line, col });
|
|
102
|
+
return tokens;
|
|
103
|
+
function readString(raw) {
|
|
104
|
+
const startLine = line;
|
|
105
|
+
const startCol = col;
|
|
106
|
+
const quote = src[i];
|
|
107
|
+
const triple = src.startsWith(quote.repeat(3), i);
|
|
108
|
+
advance(triple ? 3 : 1);
|
|
109
|
+
const parts = [];
|
|
110
|
+
let text = '';
|
|
111
|
+
const flush = () => {
|
|
112
|
+
if (text)
|
|
113
|
+
parts.push({ text });
|
|
114
|
+
text = '';
|
|
115
|
+
};
|
|
116
|
+
for (;;) {
|
|
117
|
+
if (i >= src.length)
|
|
118
|
+
err('unterminated string', startLine, startCol);
|
|
119
|
+
const ch = src[i];
|
|
120
|
+
if (triple ? src.startsWith(quote.repeat(3), i) : ch === quote) {
|
|
121
|
+
advance(triple ? 3 : 1);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
if (ch === '\n' && !triple)
|
|
125
|
+
err('unterminated string', startLine, startCol);
|
|
126
|
+
if (raw) {
|
|
127
|
+
text += ch;
|
|
128
|
+
advance();
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (ch === '\\') {
|
|
132
|
+
const esc = src[i + 1];
|
|
133
|
+
if (esc === '\n') {
|
|
134
|
+
advance(2);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (esc === undefined || !(esc in ESCAPES))
|
|
138
|
+
err(`invalid escape sequence '\\${esc ?? ''}'`);
|
|
139
|
+
text += ESCAPES[esc];
|
|
140
|
+
advance(2);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (ch === '{') {
|
|
144
|
+
if (src[i + 1] === '{') {
|
|
145
|
+
text += '{';
|
|
146
|
+
advance(2);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
flush();
|
|
150
|
+
advance();
|
|
151
|
+
const exprLine = line;
|
|
152
|
+
const exprCol = col;
|
|
153
|
+
const exprSrc = readInterpolation();
|
|
154
|
+
if (!exprSrc.trim())
|
|
155
|
+
err('empty interpolation', exprLine, exprCol);
|
|
156
|
+
parts.push({ expr: exprSrc, line: exprLine, col: exprCol });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (ch === '}') {
|
|
160
|
+
if (src[i + 1] === '}') {
|
|
161
|
+
text += '}';
|
|
162
|
+
advance(2);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
err("single '}' in string; write '}}' or '\\}' for a literal brace");
|
|
166
|
+
}
|
|
167
|
+
text += ch;
|
|
168
|
+
advance();
|
|
169
|
+
}
|
|
170
|
+
flush();
|
|
171
|
+
return { type: 'str', parts, line: startLine, col: startCol };
|
|
172
|
+
}
|
|
173
|
+
/** Reads up to the matching '}' of an interpolation, returning the expression source. */
|
|
174
|
+
function readInterpolation() {
|
|
175
|
+
const start = i;
|
|
176
|
+
const startLine = line;
|
|
177
|
+
const startCol = col;
|
|
178
|
+
let nest = 0;
|
|
179
|
+
for (;;) {
|
|
180
|
+
if (i >= src.length)
|
|
181
|
+
err("unterminated interpolation, missing '}'", startLine, startCol);
|
|
182
|
+
const ch = src[i];
|
|
183
|
+
if (ch === '"' || ch === "'") {
|
|
184
|
+
advance();
|
|
185
|
+
while (i < src.length && src[i] !== ch && src[i] !== '\n') {
|
|
186
|
+
advance(src[i] === '\\' ? 2 : 1);
|
|
187
|
+
}
|
|
188
|
+
if (src[i] !== ch)
|
|
189
|
+
err('unterminated string in interpolation', startLine, startCol);
|
|
190
|
+
advance();
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (ch === '{' || ch === '[' || ch === '(')
|
|
194
|
+
nest++;
|
|
195
|
+
if (ch === '}' || ch === ']' || ch === ')') {
|
|
196
|
+
if (nest === 0) {
|
|
197
|
+
if (ch !== '}')
|
|
198
|
+
err(`unbalanced '${ch}' in interpolation`);
|
|
199
|
+
const body = src.slice(start, i);
|
|
200
|
+
advance();
|
|
201
|
+
return body;
|
|
202
|
+
}
|
|
203
|
+
nest--;
|
|
204
|
+
}
|
|
205
|
+
advance();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
package/dist/llm.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AskMods, ToolName } from './ast.js';
|
|
2
|
+
import type { Restrictions } from './restrict.js';
|
|
3
|
+
import type { ToolHost } from './tools.js';
|
|
4
|
+
import { type Value } from './values.js';
|
|
5
|
+
export type ReasoningLevel = 'low' | 'medium' | 'high';
|
|
6
|
+
export interface Constraint {
|
|
7
|
+
kind: 'enforce' | 'forbid';
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
export interface LlmRequest {
|
|
11
|
+
kind: 'ask' | 'think' | 'tool';
|
|
12
|
+
/** The fully assembled query: constraints, restriction guidance and hints are already prepended. */
|
|
13
|
+
prompt: string;
|
|
14
|
+
model?: string;
|
|
15
|
+
reasoningEffort?: ReasoningLevel;
|
|
16
|
+
showReasoning: boolean;
|
|
17
|
+
restrictions: Restrictions;
|
|
18
|
+
tools: ToolHost;
|
|
19
|
+
/** For tools the script left parameters out of: the LLM calls the tool itself, these params are fixed by the script. */
|
|
20
|
+
pinned?: {
|
|
21
|
+
tool: ToolName;
|
|
22
|
+
params: Record<string, string>;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export interface LlmResponse {
|
|
26
|
+
text: string;
|
|
27
|
+
reasoning?: string;
|
|
28
|
+
/** Effective model and parameters, recorded in the logs. */
|
|
29
|
+
effective: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
export interface LlmBackend {
|
|
32
|
+
call(req: LlmRequest): Promise<LlmResponse>;
|
|
33
|
+
close(): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
/** Raised by a backend for any non-script-level LLM problem; becomes `llm_error`. */
|
|
36
|
+
export declare class LlmError extends Error {
|
|
37
|
+
}
|
|
38
|
+
export declare function buildPrompt(opts: {
|
|
39
|
+
prompt: string;
|
|
40
|
+
mods?: AskMods;
|
|
41
|
+
thinking?: boolean;
|
|
42
|
+
constraints: readonly Constraint[];
|
|
43
|
+
restrictions: Restrictions;
|
|
44
|
+
}): string;
|
|
45
|
+
/** Parses an `fmt='object'` response; anything that is not a JSON object or array is `invalid_format`. */
|
|
46
|
+
export declare function parseObjectResponse(text: string): Value;
|
package/dist/llm.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Failure } from './errors.js';
|
|
2
|
+
import { fromJson, isDict, isList } from './values.js';
|
|
3
|
+
/** Raised by a backend for any non-script-level LLM problem; becomes `llm_error`. */
|
|
4
|
+
export class LlmError extends Error {
|
|
5
|
+
}
|
|
6
|
+
const LEN_HINTS = {
|
|
7
|
+
word: 'Answer with a single word.',
|
|
8
|
+
sentence: 'Answer in a single sentence.',
|
|
9
|
+
paragraph: 'Answer in a single paragraph.',
|
|
10
|
+
long: 'Give a long, detailed answer.',
|
|
11
|
+
};
|
|
12
|
+
const FMT_OBJECT_HINT = 'Respond with ONLY a valid JSON value (an object unless the task clearly asks for a list). No prose, no markdown, no code fences.';
|
|
13
|
+
export function buildPrompt(opts) {
|
|
14
|
+
const sections = [];
|
|
15
|
+
if (opts.constraints.length) {
|
|
16
|
+
sections.push('Rules you must follow strictly:\n' +
|
|
17
|
+
opts.constraints.map((c) => `- You ${c.kind === 'enforce' ? 'MUST' : 'MUST NOT'}: ${c.text}`).join('\n'));
|
|
18
|
+
}
|
|
19
|
+
const guidance = opts.restrictions.guidance();
|
|
20
|
+
if (guidance.length)
|
|
21
|
+
sections.push(`Active restrictions (enforced by the runtime):\n${guidance.map((g) => `- ${g}`).join('\n')}`);
|
|
22
|
+
if (opts.thinking)
|
|
23
|
+
sections.push('Think carefully about the problem before giving the final answer.');
|
|
24
|
+
if (opts.mods?.len)
|
|
25
|
+
sections.push(LEN_HINTS[opts.mods.len]);
|
|
26
|
+
if (opts.mods?.fmt === 'object')
|
|
27
|
+
sections.push(FMT_OBJECT_HINT);
|
|
28
|
+
sections.push(opts.prompt);
|
|
29
|
+
return sections.join('\n\n');
|
|
30
|
+
}
|
|
31
|
+
/** Parses an `fmt='object'` response; anything that is not a JSON object or array is `invalid_format`. */
|
|
32
|
+
export function parseObjectResponse(text) {
|
|
33
|
+
const candidates = [text.trim()];
|
|
34
|
+
const fenced = /^```[a-zA-Z]*\s*\n([\s\S]*?)\n?```$/.exec(text.trim());
|
|
35
|
+
if (fenced)
|
|
36
|
+
candidates.push(fenced[1].trim());
|
|
37
|
+
const open = text.search(/[{[]/);
|
|
38
|
+
const close = Math.max(text.lastIndexOf('}'), text.lastIndexOf(']'));
|
|
39
|
+
if (open >= 0 && close > open)
|
|
40
|
+
candidates.push(text.slice(open, close + 1));
|
|
41
|
+
let lastError = 'empty response';
|
|
42
|
+
for (const c of candidates) {
|
|
43
|
+
try {
|
|
44
|
+
const v = fromJson(JSON.parse(c));
|
|
45
|
+
if (isDict(v) || isList(v))
|
|
46
|
+
return v;
|
|
47
|
+
lastError = 'response is JSON but not an object or array';
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
lastError = e.message;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
throw new Failure('invalid_format', `LLM response is not valid JSON: ${lastError}`);
|
|
54
|
+
}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type LogLevel = 'error' | 'info' | 'debug';
|
|
2
|
+
/** Logs go to stderr so that stdout carries only the script result. */
|
|
3
|
+
export declare class Logger {
|
|
4
|
+
private level;
|
|
5
|
+
private readonly sink;
|
|
6
|
+
constructor(level?: LogLevel | 'silent', sink?: (line: string) => void);
|
|
7
|
+
private emit;
|
|
8
|
+
error(msg: string): void;
|
|
9
|
+
warn(msg: string): void;
|
|
10
|
+
info(msg: string): void;
|
|
11
|
+
debug(msg: string): void;
|
|
12
|
+
}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const RANK = { error: 0, info: 1, debug: 2 };
|
|
2
|
+
/** Logs go to stderr so that stdout carries only the script result. */
|
|
3
|
+
export class Logger {
|
|
4
|
+
level;
|
|
5
|
+
sink;
|
|
6
|
+
constructor(level = 'info', sink = (l) => process.stderr.write(l + '\n')) {
|
|
7
|
+
this.level = level;
|
|
8
|
+
this.sink = sink;
|
|
9
|
+
}
|
|
10
|
+
emit(min, tag, msg) {
|
|
11
|
+
if (this.level === 'silent' || RANK[this.level] < RANK[min])
|
|
12
|
+
return;
|
|
13
|
+
this.sink(`[${tag}] ${msg}`);
|
|
14
|
+
}
|
|
15
|
+
error(msg) {
|
|
16
|
+
this.emit('error', 'error', msg);
|
|
17
|
+
}
|
|
18
|
+
warn(msg) {
|
|
19
|
+
this.emit('info', 'warn', msg);
|
|
20
|
+
}
|
|
21
|
+
info(msg) {
|
|
22
|
+
this.emit('info', 'info', msg);
|
|
23
|
+
}
|
|
24
|
+
debug(msg) {
|
|
25
|
+
this.emit('debug', 'debug', msg);
|
|
26
|
+
}
|
|
27
|
+
}
|
package/dist/parser.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Program, ToolName } from './ast.js';
|
|
2
|
+
export declare const TOOL_NAMES: readonly ToolName[];
|
|
3
|
+
/** Fixed modifier schema per command; anything else is a syntax error. */
|
|
4
|
+
export declare const MODIFIER_SCHEMA: Record<'?' | 'think', Record<string, readonly (string | boolean)[]>>;
|
|
5
|
+
/**
|
|
6
|
+
* Tool parameter schema: the allowed names, and the `required` ones that let the runtime run the tool itself.
|
|
7
|
+
* Every parameter is optional in a script; the LLM supplies the missing required ones when it calls the tool.
|
|
8
|
+
*/
|
|
9
|
+
export declare const TOOL_SCHEMA: Record<ToolName, {
|
|
10
|
+
params: string[];
|
|
11
|
+
required: string[];
|
|
12
|
+
}>;
|
|
13
|
+
export declare function parse(source: string): Program;
|