spl-lang 0.1.1 → 0.3.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/README.md +73 -129
- package/dist/ast.d.ts +12 -19
- package/dist/cli.js +58 -13
- package/dist/copilot.d.ts +6 -3
- package/dist/copilot.js +23 -10
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/interpreter.js +106 -73
- package/dist/llm.d.ts +5 -10
- package/dist/llm.js +17 -26
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +7 -0
- package/dist/mcp.d.ts +7 -0
- package/dist/mcp.js +108 -0
- package/dist/parser.d.ts +2 -12
- package/dist/parser.js +36 -105
- package/dist/recording.d.ts +17 -0
- package/dist/recording.js +93 -0
- package/dist/restrict.d.ts +3 -0
- package/dist/restrict.js +27 -1
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +40 -0
- package/dist/tools.d.ts +9 -1
- package/dist/tools.js +223 -28
- package/dist/values.d.ts +1 -1
- package/docs/syntax.ebnf +53 -69
- package/package.json +3 -2
package/dist/tools.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import * as fs from 'node:fs/promises';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
4
5
|
import { Failure } from './errors.js';
|
|
5
6
|
const MAX_TEXT = 200_000;
|
|
6
7
|
const SHELL_TIMEOUT_MS = 120_000;
|
|
7
8
|
const FETCH_TIMEOUT_MS = 30_000;
|
|
8
9
|
const MAX_REDIRECTS = 5;
|
|
9
|
-
/** Characters that let a command chain, redirect or substitute; rejected while any restriction is active. */
|
|
10
|
-
const SHELL_META = /[&|;<>`$()\r\n^%!]/;
|
|
11
10
|
export const TOOL_SPECS = {
|
|
12
11
|
read_file: {
|
|
13
12
|
name: 'read_file',
|
|
@@ -86,6 +85,127 @@ export function splitCommand(command) {
|
|
|
86
85
|
args.push(cur);
|
|
87
86
|
return args;
|
|
88
87
|
}
|
|
88
|
+
function findMatchingParen(str, openIndex) {
|
|
89
|
+
let depth = 0;
|
|
90
|
+
for (let i = openIndex; i < str.length; i++) {
|
|
91
|
+
if (str[i] === '(')
|
|
92
|
+
depth++;
|
|
93
|
+
else if (str[i] === ')') {
|
|
94
|
+
depth--;
|
|
95
|
+
if (depth === 0)
|
|
96
|
+
return i;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return -1;
|
|
100
|
+
}
|
|
101
|
+
/** Splits a command string by chain/pipeline separators (&&, ||, |, ;, &, \n) outside quotes. */
|
|
102
|
+
export function splitChainedCommands(command) {
|
|
103
|
+
const subcmds = [];
|
|
104
|
+
let cur = '';
|
|
105
|
+
let quote;
|
|
106
|
+
for (let i = 0; i < command.length; i++) {
|
|
107
|
+
const ch = command[i];
|
|
108
|
+
if (quote) {
|
|
109
|
+
if (ch === quote)
|
|
110
|
+
quote = undefined;
|
|
111
|
+
cur += ch;
|
|
112
|
+
}
|
|
113
|
+
else if (ch === '"' || ch === "'") {
|
|
114
|
+
quote = ch;
|
|
115
|
+
cur += ch;
|
|
116
|
+
}
|
|
117
|
+
else if (ch === '\n' || ch === ';') {
|
|
118
|
+
if (cur.trim())
|
|
119
|
+
subcmds.push(cur.trim());
|
|
120
|
+
cur = '';
|
|
121
|
+
}
|
|
122
|
+
else if (ch === '&' && command[i + 1] === '&') {
|
|
123
|
+
if (cur.trim())
|
|
124
|
+
subcmds.push(cur.trim());
|
|
125
|
+
cur = '';
|
|
126
|
+
i++;
|
|
127
|
+
}
|
|
128
|
+
else if (ch === '|' && command[i + 1] === '|') {
|
|
129
|
+
if (cur.trim())
|
|
130
|
+
subcmds.push(cur.trim());
|
|
131
|
+
cur = '';
|
|
132
|
+
i++;
|
|
133
|
+
}
|
|
134
|
+
else if (ch === '|') {
|
|
135
|
+
if (cur.trim())
|
|
136
|
+
subcmds.push(cur.trim());
|
|
137
|
+
cur = '';
|
|
138
|
+
}
|
|
139
|
+
else if (ch === '&') {
|
|
140
|
+
const prevNonSpace = cur.trimEnd().slice(-1);
|
|
141
|
+
if (prevNonSpace === '>') {
|
|
142
|
+
cur += ch;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
if (cur.trim())
|
|
146
|
+
subcmds.push(cur.trim());
|
|
147
|
+
cur = '';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
cur += ch;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (quote)
|
|
155
|
+
throw new Failure('restriction_violation', 'shell command rejected: unterminated quote');
|
|
156
|
+
if (cur.trim())
|
|
157
|
+
subcmds.push(cur.trim());
|
|
158
|
+
return subcmds;
|
|
159
|
+
}
|
|
160
|
+
/** Finds nested $(...) or `...` command substitutions outside single quotes. */
|
|
161
|
+
export function extractSubstitutions(command) {
|
|
162
|
+
const subs = [];
|
|
163
|
+
let quote;
|
|
164
|
+
for (let i = 0; i < command.length; i++) {
|
|
165
|
+
const ch = command[i];
|
|
166
|
+
if (quote === "'") {
|
|
167
|
+
if (ch === "'")
|
|
168
|
+
quote = undefined;
|
|
169
|
+
}
|
|
170
|
+
else if (quote === '"') {
|
|
171
|
+
if (ch === '"') {
|
|
172
|
+
quote = undefined;
|
|
173
|
+
}
|
|
174
|
+
else if (ch === '`') {
|
|
175
|
+
const end = command.indexOf('`', i + 1);
|
|
176
|
+
if (end !== -1) {
|
|
177
|
+
subs.push(command.slice(i + 1, end));
|
|
178
|
+
i = end;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else if (ch === '$' && command[i + 1] === '(') {
|
|
182
|
+
const end = findMatchingParen(command, i + 1);
|
|
183
|
+
if (end !== -1) {
|
|
184
|
+
subs.push(command.slice(i + 2, end));
|
|
185
|
+
i = end;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else if (ch === "'" || ch === '"') {
|
|
190
|
+
quote = ch;
|
|
191
|
+
}
|
|
192
|
+
else if (ch === '`') {
|
|
193
|
+
const end = command.indexOf('`', i + 1);
|
|
194
|
+
if (end !== -1) {
|
|
195
|
+
subs.push(command.slice(i + 1, end));
|
|
196
|
+
i = end;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else if (ch === '$' && command[i + 1] === '(') {
|
|
200
|
+
const end = findMatchingParen(command, i + 1);
|
|
201
|
+
if (end !== -1) {
|
|
202
|
+
subs.push(command.slice(i + 2, end));
|
|
203
|
+
i = end;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return subs;
|
|
208
|
+
}
|
|
89
209
|
const SEARCH_ENDPOINT = 'https://html.duckduckgo.com/html/';
|
|
90
210
|
const MAX_SEARCH_RESULTS = 10;
|
|
91
211
|
const decodeEntities = (s) => s
|
|
@@ -121,6 +241,10 @@ export function parseSearchResults(html) {
|
|
|
121
241
|
}
|
|
122
242
|
const looksLikeUrl = (s) => /^[a-z][a-z0-9+.-]*:\/\//i.test(s);
|
|
123
243
|
const looksLikePath = (s) => /[\\/]/.test(s) || s.startsWith('.') || s.startsWith('~') || /^[a-z]:/i.test(s);
|
|
244
|
+
function isDirSwitch(executable, arg) {
|
|
245
|
+
return process.platform === 'win32' && executable.toLowerCase() === 'dir'
|
|
246
|
+
&& /^\/(?:-?[bcdlnpqrswx4]|[aot](?::[a-z-]+)?)$/i.test(arg);
|
|
247
|
+
}
|
|
124
248
|
/**
|
|
125
249
|
* Executes the built-in tools. Every call is checked against the active restrictions,
|
|
126
250
|
* whether it comes from the script itself or from the LLM.
|
|
@@ -128,11 +252,17 @@ const looksLikePath = (s) => /[\\/]/.test(s) || s.startsWith('.') || s.startsWit
|
|
|
128
252
|
export class ToolHost {
|
|
129
253
|
restrictions;
|
|
130
254
|
cwd;
|
|
131
|
-
|
|
255
|
+
replay;
|
|
256
|
+
raw;
|
|
257
|
+
constructor(restrictions, cwd, replay = false, raw = false) {
|
|
132
258
|
this.restrictions = restrictions;
|
|
133
259
|
this.cwd = cwd;
|
|
260
|
+
this.replay = replay;
|
|
261
|
+
this.raw = raw;
|
|
134
262
|
}
|
|
135
263
|
async invoke(name, params) {
|
|
264
|
+
if (this.replay && name !== 'read_file')
|
|
265
|
+
throw new Failure('restriction_violation', `replay blocks ${name}; only local reads and recorded generation are allowed`);
|
|
136
266
|
switch (name) {
|
|
137
267
|
case 'read_file': return this.readFile(needString(name, params, 'path'));
|
|
138
268
|
case 'write_file': {
|
|
@@ -149,7 +279,8 @@ export class ToolHost {
|
|
|
149
279
|
async readFile(p) {
|
|
150
280
|
const canonical = this.restrictions.checkPath(p, 'read');
|
|
151
281
|
try {
|
|
152
|
-
|
|
282
|
+
const text = await fs.readFile(canonical, 'utf8');
|
|
283
|
+
return this.raw ? text : truncate(text);
|
|
153
284
|
}
|
|
154
285
|
catch (e) {
|
|
155
286
|
throw new Failure('tool_error', `read_file '${p}': ${e.message}`);
|
|
@@ -180,7 +311,16 @@ export class ToolHost {
|
|
|
180
311
|
current = this.restrictions.checkUrl(new URL(res.headers.get('location'), current).href);
|
|
181
312
|
continue;
|
|
182
313
|
}
|
|
183
|
-
|
|
314
|
+
let text;
|
|
315
|
+
try {
|
|
316
|
+
text = await res.text();
|
|
317
|
+
}
|
|
318
|
+
catch (e) {
|
|
319
|
+
if (!(e instanceof Error))
|
|
320
|
+
throw e;
|
|
321
|
+
throw new Failure('tool_error', `web_fetch '${current.href}': ${e.message}`);
|
|
322
|
+
}
|
|
323
|
+
const body = this.raw ? text : truncate(text);
|
|
184
324
|
if (!res.ok)
|
|
185
325
|
throw new Failure('tool_error', `web_fetch '${current.href}': HTTP ${res.status}\n${body}`);
|
|
186
326
|
return body;
|
|
@@ -209,17 +349,77 @@ export class ToolHost {
|
|
|
209
349
|
return results.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}${r.snippet ? `\n ${r.snippet}` : ''}`).join('\n');
|
|
210
350
|
}
|
|
211
351
|
async shell(command) {
|
|
212
|
-
if (!
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
352
|
+
if (!command || command.trim() === '')
|
|
353
|
+
throw new Failure('tool_error', 'shell: empty command');
|
|
354
|
+
if (this.restrictions.active) {
|
|
355
|
+
try {
|
|
356
|
+
this.checkCommandRestrictions(command);
|
|
357
|
+
}
|
|
358
|
+
catch (e) {
|
|
359
|
+
if (e instanceof Failure && e.statusCode === 'restriction_violation') {
|
|
360
|
+
throw new Failure('restriction_violation', `${e.reason} (shell command: ${JSON.stringify(command)})`);
|
|
361
|
+
}
|
|
362
|
+
throw e;
|
|
363
|
+
}
|
|
217
364
|
}
|
|
218
|
-
|
|
219
|
-
|
|
365
|
+
return this.run(command, { shell: true });
|
|
366
|
+
}
|
|
367
|
+
checkCommandRestrictions(command) {
|
|
368
|
+
const subcmds = splitChainedCommands(command);
|
|
369
|
+
if (subcmds.length === 0)
|
|
220
370
|
throw new Failure('tool_error', 'shell: empty command');
|
|
221
|
-
|
|
222
|
-
|
|
371
|
+
for (const sub of subcmds) {
|
|
372
|
+
this.checkSubcommand(sub);
|
|
373
|
+
}
|
|
374
|
+
for (const sub of extractSubstitutions(command)) {
|
|
375
|
+
this.checkCommandRestrictions(sub);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
checkSubcommand(subcmd) {
|
|
379
|
+
const argv = splitCommand(subcmd);
|
|
380
|
+
if (argv.length === 0)
|
|
381
|
+
return;
|
|
382
|
+
let execIdx = 0;
|
|
383
|
+
while (execIdx < argv.length && /^[a-zA-Z_][a-zA-Z0-9_]*=/.test(argv[execIdx])) {
|
|
384
|
+
execIdx++;
|
|
385
|
+
}
|
|
386
|
+
if (execIdx >= argv.length)
|
|
387
|
+
return;
|
|
388
|
+
const exe = argv[execIdx].replace(/^[()]+/, '').replace(/[()]+$/, '');
|
|
389
|
+
if (exe)
|
|
390
|
+
this.restrictions.checkExec(exe);
|
|
391
|
+
for (let i = execIdx + 1; i < argv.length; i++) {
|
|
392
|
+
const arg = argv[i];
|
|
393
|
+
if (arg === '>' || arg === '>>' || arg === '1>' || arg === '2>') {
|
|
394
|
+
const target = argv[++i];
|
|
395
|
+
if (target && !target.startsWith('&') && this.restrictions.has('fs')) {
|
|
396
|
+
this.restrictions.checkPath(target, 'write');
|
|
397
|
+
}
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (arg === '<' || arg === '0<') {
|
|
401
|
+
const target = argv[++i];
|
|
402
|
+
if (target && this.restrictions.has('fs')) {
|
|
403
|
+
this.restrictions.checkPath(target, 'read');
|
|
404
|
+
}
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (/^[0-9]?>+/.test(arg)) {
|
|
408
|
+
const target = arg.replace(/^[0-9]?>+/, '');
|
|
409
|
+
if (target && !target.startsWith('&') && this.restrictions.has('fs')) {
|
|
410
|
+
this.restrictions.checkPath(target, 'write');
|
|
411
|
+
}
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (/^[0-9]?<+/.test(arg)) {
|
|
415
|
+
const target = arg.replace(/^[0-9]?<+/, '');
|
|
416
|
+
if (target && this.restrictions.has('fs')) {
|
|
417
|
+
this.restrictions.checkPath(target, 'read');
|
|
418
|
+
}
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (isDirSwitch(exe, arg))
|
|
422
|
+
continue;
|
|
223
423
|
const value = arg.startsWith('-') && arg.includes('=') ? arg.slice(arg.indexOf('=') + 1) : arg;
|
|
224
424
|
if (looksLikeUrl(value)) {
|
|
225
425
|
if (this.restrictions.has('net'))
|
|
@@ -229,25 +429,20 @@ export class ToolHost {
|
|
|
229
429
|
this.restrictions.checkPath(value, 'read');
|
|
230
430
|
}
|
|
231
431
|
}
|
|
232
|
-
if (argv.some((a) => a.includes('"'))) {
|
|
233
|
-
throw new Failure('restriction_violation', 'shell command rejected: embedded double quotes are not allowed inside a restrict block');
|
|
234
|
-
}
|
|
235
|
-
if (process.platform === 'win32') {
|
|
236
|
-
// .cmd shims (npm, ...) need a shell on Windows; metacharacters were already rejected.
|
|
237
|
-
return this.run(argv.map((a) => (/\s/.test(a) || a === '' ? `"${a}"` : a)).join(' '), { shell: true });
|
|
238
|
-
}
|
|
239
|
-
return this.run(argv[0], { shell: false, args: argv.slice(1) });
|
|
240
432
|
}
|
|
241
433
|
run(command, opts) {
|
|
242
434
|
return new Promise((resolve, reject) => {
|
|
243
435
|
const child = spawn(command, opts.args ?? [], { cwd: this.cwd, shell: opts.shell, windowsHide: true });
|
|
244
436
|
let out = '';
|
|
245
|
-
const collect = (
|
|
246
|
-
if (out.length < MAX_TEXT * 2)
|
|
247
|
-
out +=
|
|
437
|
+
const collect = (text) => {
|
|
438
|
+
if (this.raw || out.length < MAX_TEXT * 2)
|
|
439
|
+
out += text;
|
|
248
440
|
};
|
|
249
|
-
child.stdout
|
|
250
|
-
|
|
441
|
+
for (const stream of [child.stdout, child.stderr]) {
|
|
442
|
+
const decoder = new StringDecoder('utf8');
|
|
443
|
+
stream.on('data', (data) => collect(decoder.write(data)));
|
|
444
|
+
stream.on('end', () => collect(decoder.end()));
|
|
445
|
+
}
|
|
251
446
|
const timer = setTimeout(() => child.kill(), SHELL_TIMEOUT_MS);
|
|
252
447
|
child.on('error', (e) => {
|
|
253
448
|
clearTimeout(timer);
|
|
@@ -255,7 +450,7 @@ export class ToolHost {
|
|
|
255
450
|
});
|
|
256
451
|
child.on('close', (code, signal) => {
|
|
257
452
|
clearTimeout(timer);
|
|
258
|
-
const text = truncate(out);
|
|
453
|
+
const text = this.raw ? out : truncate(out);
|
|
259
454
|
if (code === 0)
|
|
260
455
|
resolve(text);
|
|
261
456
|
else
|
package/dist/values.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export interface UserFunction {
|
|
|
17
17
|
export interface BuiltinFunction {
|
|
18
18
|
__fn: 'builtin';
|
|
19
19
|
name: string;
|
|
20
|
-
call: (args: Value[]) => Value
|
|
20
|
+
call: (args: Value[]) => Value | Promise<Value>;
|
|
21
21
|
}
|
|
22
22
|
export type FunctionValue = UserFunction | BuiltinFunction;
|
|
23
23
|
export declare const runtimeError: (msg: string) => Failure;
|
package/docs/syntax.ebnf
CHANGED
|
@@ -1,97 +1,81 @@
|
|
|
1
|
-
(*
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
'#' starts a comment that runs to the end of the line. *)
|
|
1
|
+
(* SPL 0.3. { x } means repetition; [ x ] means optional.
|
|
2
|
+
Statements end at newline, ';', '}' or EOF. Newlines inside () and [] are ignored.
|
|
3
|
+
'#' starts a comment. Opening block braces must be on the keyword's line. *)
|
|
5
4
|
|
|
6
|
-
program = { directive ,
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
program = { directive , separator } , { statement , separator } ;
|
|
6
|
+
separator = NL | ";" | end_of_block | EOF ;
|
|
7
|
+
directive = "@" , "{" , "model" , "=" , plain_string , [ "," ] , "}" ;
|
|
8
|
+
block = "{" , { statement , separator } , "}" ;
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
| "
|
|
14
|
-
| "break" | "continue" | "pass"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Scoping: 'let NAME = ...' binds in the current scope. A bare block { ... }, a function body and its parameters
|
|
20
|
-
open a new scope, so 'let' there shadows an outer variable. if/elif/else, for, while, try/except, enforce, forbid
|
|
21
|
-
and restrict bodies do NOT open a scope: 'let' inside them updates the enclosing variable, which is how results
|
|
22
|
-
of LLM commands leave an enforce/forbid/restrict block. Reading a name searches the scopes outwards.
|
|
23
|
-
Compound forms (let x += 1) read the visible value and bind the result in the current scope. *)
|
|
24
|
-
let_stmt = "let" , target , ( "=" | "+=" | "-=" | "*=" | "/=" | "//=" | "%=" ) , expr ;
|
|
25
|
-
target = NAME | postfix "." NAME | postfix "[" expr "]" ;
|
|
26
|
-
|
|
27
|
-
if_stmt = "if" , expr , block , { NL* "elif" , expr , block } , [ NL* "else" , block ] ;
|
|
10
|
+
statement = declaration | assignment | if_stmt | for_stmt | while_stmt
|
|
11
|
+
| def_stmt | try_stmt | guidance_stmt | restrict_stmt | block
|
|
12
|
+
| "return" , [ expr ] | "success" , [ expr ]
|
|
13
|
+
| "assert" , expr , [ expr ] | "fail" , expr
|
|
14
|
+
| "break" | "continue" | "pass" | expr ;
|
|
15
|
+
declaration = "let" , NAME , "=" , expr ;
|
|
16
|
+
assignment = target , ( "=" | "+=" | "-=" | "*=" | "/=" | "//=" | "%=" ) , expr ;
|
|
17
|
+
target = NAME | postfix , "." , NAME | postfix , "[" , expr , "]" ;
|
|
18
|
+
if_stmt = "if" , expr , block , { NL* , "elif" , expr , block } , [ NL* , "else" , block ] ;
|
|
28
19
|
for_stmt = "for" , NAME , { "," , NAME } , "in" , expr , block ;
|
|
29
20
|
while_stmt = "while" , expr , block ;
|
|
30
|
-
def_stmt = "def" , NAME , "(" , [ NAME , { "," , NAME } ] , ")" , block ;
|
|
21
|
+
def_stmt = "def" , NAME , "(" , [ NAME , { "," , NAME } , [ "," ] ] , ")" , block ;
|
|
31
22
|
try_stmt = "try" , block , NL* , "except" , NAME , block ;
|
|
23
|
+
guidance_stmt = ( "instruct" | "avoid" ) , expr , block ;
|
|
24
|
+
restrict_stmt = "restrict" , restrict_clause , { "," , restrict_clause } , block ;
|
|
25
|
+
restrict_clause = [ "fs" | "net" | "exec" | "mcp" ] , expr ;
|
|
32
26
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
27
|
+
(* let declares/redeclares in the current function or script. All blocks share that scope.
|
|
28
|
+
Name assignment requires a declared local; it cannot rebind an enclosing function/script.
|
|
29
|
+
Functions read lexical enclosing scopes; collection mutations can share state.
|
|
30
|
+
Loop variables and caught failure names are local bindings.
|
|
31
|
+
return exits the innermost function, or the script at top level.
|
|
32
|
+
success exits the script, including when used in a function. *)
|
|
39
33
|
|
|
40
|
-
(* Expressions, lowest to highest precedence *)
|
|
41
34
|
expr = or_expr , [ "if" , or_expr , "else" , expr ] ;
|
|
42
35
|
or_expr = and_expr , { "or" , and_expr } ;
|
|
43
36
|
and_expr = not_expr , { "and" , not_expr } ;
|
|
44
37
|
not_expr = "not" , not_expr | comparison ;
|
|
45
|
-
comparison = sum , { ( "==" | "!=" | "<" | "<=" | ">" | ">=" | "in" | "not" "in" ) , sum } ;
|
|
38
|
+
comparison = sum , { ( "==" | "!=" | "<" | "<=" | ">" | ">=" | "in" | "not" , "in" ) , sum } ;
|
|
46
39
|
sum = term , { ( "+" | "-" ) , term } ;
|
|
47
40
|
term = unary , { ( "*" | "/" | "//" | "%" ) , unary } ;
|
|
48
|
-
unary = ( "-" | "+" ) , unary |
|
|
41
|
+
unary = ( "-" | "+" ) , unary | "?" , [ modifiers ] , postfix | power ;
|
|
49
42
|
power = postfix , [ "**" , unary ] ;
|
|
50
43
|
postfix = primary , { call | index | slice | member } ;
|
|
51
|
-
call = "(" , [ expr , { "," , expr } ] , ")" ;
|
|
44
|
+
call = "(" , [ expr , { "," , expr } , [ "," ] ] , ")" ;
|
|
52
45
|
index = "[" , expr , "]" ;
|
|
53
46
|
slice = "[" , [ expr ] , ":" , [ expr ] , "]" ;
|
|
54
47
|
member = "." , NAME ;
|
|
55
|
-
|
|
56
48
|
primary = NUMBER | string | "True" | "False" | "None" | NAME
|
|
57
49
|
| "(" , expr , ")" | list | dict
|
|
58
|
-
|
|
|
50
|
+
| ( "think" | "agent" ) , [ modifiers ] , postfix ;
|
|
59
51
|
list = "[" , [ expr , { "," , expr } , [ "," ] ] , "]" ;
|
|
60
52
|
dict = "{" , [ expr , ":" , expr , { "," , expr , ":" , expr } , [ "," ] ] , "}" ;
|
|
53
|
+
modifiers = "{" , [ modifier , { "," , modifier } , [ "," ] ] , "}" ;
|
|
54
|
+
modifier = ( "len" | "fmt" | "reasoning" | "show" | "schema" | "repairs" | "model" ) , "=" , expr ;
|
|
61
55
|
|
|
62
|
-
(*
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
think_mod = "level" , "=" , ( "'low'" | "'medium'" | "'high'" )
|
|
74
|
-
| "show" , "=" , ( "True" | "False" ) ; (* each modifier at most once *)
|
|
75
|
-
|
|
76
|
-
tool_name = "read_file" | "write_file" | "web_fetch" | "web_search" | "shell" ;
|
|
77
|
-
tool_params = "{" , [ tool_param , { "," , tool_param } ] , "}" ;
|
|
78
|
-
(* When every parameter the tool needs is given, the runtime runs the tool and passes its output to the instructions.
|
|
79
|
-
When one is missing, the LLM calls the tool itself; the parameters that were given stay fixed. *)
|
|
80
|
-
tool_param = "path" , "=" , expr (* read_file, write_file *)
|
|
81
|
-
| "content" , "=" , expr (* write_file; the LLM writes the file when omitted *)
|
|
82
|
-
| "url" , "=" , expr (* web_fetch *)
|
|
83
|
-
| "query" , "=" , expr (* web_search *)
|
|
84
|
-
| "command" , "=" , expr ; (* shell *)
|
|
56
|
+
(* Unknown/duplicate modifier names are syntax errors. Values are expressions, validated at runtime:
|
|
57
|
+
len: word/sentence/paragraph/long (guidance only)
|
|
58
|
+
fmt: text/json/object; schema implies json unless fmt is explicit
|
|
59
|
+
reasoning: low/medium/high; think and agent default to medium
|
|
60
|
+
show: True/False; schema: synchronous strict JSON Schema draft-07 object or boolean
|
|
61
|
+
repairs: integer 0..5 (additional generation attempts, default 0); agents require 0
|
|
62
|
+
model: non-empty string; CLI override > per-call model > header > SDK default
|
|
63
|
+
Generation (? and think) has no tools. agent explicitly enables restricted runtime/MCP tools.
|
|
64
|
+
Raw tools are ordinary positional functions: read_file(path), write_file(path, content),
|
|
65
|
+
web_fetch(url), web_search(query), shell(command). They never invoke a model.
|
|
66
|
+
instruct/avoid are prompt guidance; schema/assert validate data; restrict checks actions. *)
|
|
85
67
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
plain_string =
|
|
68
|
+
string = [ "r" ] , quote , { char | escape | interpolation } , closing_quote ;
|
|
69
|
+
quote = "'" | '"' | "'''" | '"""' ;
|
|
70
|
+
plain_string = string_without_interpolation ;
|
|
89
71
|
escape = "\" , ( "n" | "t" | "r" | "0" | "\" | "'" | '"' | "{" | "}" ) ;
|
|
90
|
-
interpolation = "{" , expr , "}" ;
|
|
72
|
+
interpolation = "{" , expr , "}" ;
|
|
91
73
|
NUMBER = digit+ , [ "." , digit+ ] , [ ( "e" | "E" ) , [ "+" | "-" ] , digit+ ] ;
|
|
92
|
-
NAME = ( letter | "_" ) , { letter | digit | "_" } ;
|
|
74
|
+
NAME = ( letter | "_" ) , { letter | digit | "_" } ;
|
|
93
75
|
NL = newline ;
|
|
94
76
|
|
|
95
|
-
(*
|
|
96
|
-
let if elif else for in while def return try except assert fail success
|
|
97
|
-
think break continue pass and or not True False None
|
|
77
|
+
(* "{{"/"}}" are literal braces; raw strings have no interpolation or escapes.
|
|
78
|
+
Reserved words: let if elif else for in while def return try except assert fail success
|
|
79
|
+
instruct avoid restrict think agent break continue pass and or not True False None.
|
|
80
|
+
Removed syntax: enforce/forbid, think{level=...}, tool{param=...} 'instructions',
|
|
81
|
+
let x += ... and let d.key = ... . *)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spl-lang",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Semantic Prompt Language: a Python-like language with LLM calls built into the syntax",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
],
|
|
30
30
|
"license": "ISC",
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@github/copilot-sdk": "^1.0.22"
|
|
32
|
+
"@github/copilot-sdk": "^1.0.22",
|
|
33
|
+
"ajv": "^8.17.1"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
36
|
"@types/node": "^26.6.5",
|