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/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
- constructor(restrictions, cwd) {
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
- return truncate(await fs.readFile(canonical, 'utf8'));
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
- const body = truncate(await res.text());
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 (!this.restrictions.active)
213
- return this.run(command, { shell: true });
214
- // With any restriction active only plain commands are accepted, so the checks below are exact.
215
- if (SHELL_META.test(command)) {
216
- throw new Failure('restriction_violation', 'shell command rejected: chaining, redirection and substitution are not allowed inside a restrict block');
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
- const argv = splitCommand(command);
219
- if (argv.length === 0)
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
- this.restrictions.checkExec(argv[0]);
222
- for (const arg of argv.slice(1)) {
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 = (d) => {
246
- if (out.length < MAX_TEXT * 2)
247
- out += d.toString('utf8');
437
+ const collect = (text) => {
438
+ if (this.raw || out.length < MAX_TEXT * 2)
439
+ out += text;
248
440
  };
249
- child.stdout.on('data', collect);
250
- child.stderr.on('data', collect);
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
- (* Semantic Prompt Language (SPL) - syntax in EBNF.
2
- Notation: ISO-style. { x } = zero or more, [ x ] = optional, ( ) = grouping, | = alternative.
3
- Statements are separated by newlines or ';'. Newlines are insignificant inside ( ) and [ ].
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 , NL } , { statement , ( NL | ";" ) } ;
7
- directive = "@" , "{" , directive_item , { "," , directive_item } , "}" ;
8
- directive_item = "model" , "=" , plain_string ; (* only allowed before the first statement *)
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
- block = "{" , { statement , ( NL | ";" ) } , "}" ; (* '{' must be on the same line as its keyword; as a statement it is a nested scope *)
11
-
12
- statement = let_stmt | if_stmt | for_stmt | while_stmt | def_stmt | block
13
- | "return" , [ expr ] (* anywhere: ends the function, or the script when at the top level *)
14
- | "break" | "continue" | "pass" (* break/continue only inside loops *)
15
- | try_stmt | assert_stmt | fail_stmt | success_stmt
16
- | constraint_stmt | restrict_stmt | expr ;
17
-
18
- (* Every modification of a variable starts with 'let'; a bare x = 1 is a syntax error.
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
- assert_stmt = "assert" , expr , [ expr ] ; (* optional message *)
34
- fail_stmt = "fail" , expr ;
35
- success_stmt = "success" , [ expr ] ;
36
-
37
- constraint_stmt = ( "enforce" | "forbid" ) , expr , block ;
38
- restrict_stmt = "restrict" , [ "fs" | "net" | "exec" ] , expr , block ; (* default kind: fs *)
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 | llm_call | power ;
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
- | think_call | tool_call ;
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
- (* LLM commands. '?' is a prefix operator (see 'unary'). The prompt operand is a postfix expression,
63
- so ?'a' + b is (?'a') + b and -?'n' is -(?'n').
64
- Every modifier and parameter is optional: ?'q', think 'q' and read_file 'what to do' run with defaults. *)
65
- llm_call = "?" , [ ask_mods ] , postfix ;
66
- think_call = "think" , [ think_mods ] , postfix ;
67
- tool_call = tool_name , [ tool_params ] , postfix ; (* the last operand holds the instructions *)
68
-
69
- ask_mods = "{" , ask_mod , { "," , ask_mod } , "}" ;
70
- ask_mod = "len" , "=" , ( "'word'" | "'sentence'" | "'paragraph'" | "'long'" )
71
- | "fmt" , "=" , ( "'text'" | "'object'" ) ;
72
- think_mods = "{" , think_mod , { "," , think_mod } , "}" ;
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
- (* Lexical rules *)
87
- string = [ "r" ] , ( "'" | '"' | "'''" | '"""' ) , { char | escape | interpolation } , closing_quote ;
88
- plain_string = string without interpolation ;
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 , "}" ; (* "{{" and "}}" are literal braces; raw strings (r'...') have no escapes or interpolation *)
72
+ interpolation = "{" , expr , "}" ;
91
73
  NUMBER = digit+ , [ "." , digit+ ] , [ ( "e" | "E" ) , [ "+" | "-" ] , digit+ ] ;
92
- NAME = ( letter | "_" ) , { letter | digit | "_" } ; (* not a reserved word *)
74
+ NAME = ( letter | "_" ) , { letter | digit | "_" } ;
93
75
  NL = newline ;
94
76
 
95
- (* Reserved words:
96
- let if elif else for in while def return try except assert fail success enforce forbid restrict
97
- think break continue pass and or not True False None read_file write_file web_fetch web_search shell *)
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.1.1",
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",