spl-lang 0.4.1 → 0.5.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 +7 -0
- package/dist/ast.d.ts +3 -0
- package/dist/builtins.js +6 -1
- package/dist/cli.js +3 -1
- package/dist/interpreter.d.ts +2 -0
- package/dist/interpreter.js +18 -4
- package/dist/lexer.js +4 -2
- package/dist/parser.js +9 -1
- package/dist/values.d.ts +1 -0
- package/dist/values.js +5 -0
- package/docs/syntax.ebnf +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,6 +42,10 @@ under `~/.spl` (override with `SPL_HOME`); `spl logout` removes those saved cred
|
|
|
42
42
|
`{}` remains an empty dict. Functions still require explicit `return`.
|
|
43
43
|
- **Logging:** `log(...)` defaults to info. `log{level=debug}(...)` selects error/info/debug;
|
|
44
44
|
bare level names, strings and expressions are accepted and checked at runtime.
|
|
45
|
+
- **Runtime inputs:** `$env.NAME` / `$env['NAME']` reads process environment strings;
|
|
46
|
+
`$args[i]` reads repeated CLI `--arg` strings in order (empty list by default).
|
|
47
|
+
Missing environment names and out-of-range argument indexes return `None`.
|
|
48
|
+
Both are read-only snapshots, including through aliases, and are not automatically persisted.
|
|
45
49
|
|
|
46
50
|
Common generation modifiers are `len`, `fmt`, `reasoning`, `show`, `schema`, `repairs`,
|
|
47
51
|
and `model`. Values are evaluated once, then validated before calling the model.
|
|
@@ -67,6 +71,7 @@ spl script.spl [--model id] [--mcp file.json] [--cwd dir] [--timeout seconds]
|
|
|
67
71
|
[--store file.json | --store-module module]
|
|
68
72
|
[--record file.jsonl | --replay file.jsonl]
|
|
69
73
|
[--log-level error|info|debug] [--silent] [--check]
|
|
74
|
+
[--arg value ...]
|
|
70
75
|
```
|
|
71
76
|
|
|
72
77
|
Results are printed on stdout, logs on stderr. Full prompts/responses and statement traces
|
|
@@ -87,6 +92,8 @@ The package exports `parse`, `run`, `Logger`, `CopilotBackend`, `RecordingBacken
|
|
|
87
92
|
`ReplayBackend`, variable stores, authentication helpers, and related types.
|
|
88
93
|
Call `close()` on the backend after running; replay also checks for unused entries there.
|
|
89
94
|
Custom backends must honor the request kind: only `agent` may execute tools.
|
|
95
|
+
Embedding callers can pass `args: ['first', 'second']` in `RunOptions`; omitted
|
|
96
|
+
arguments default to `[]`, not the host's `process.argv`. Each run snapshots `process.env`.
|
|
90
97
|
See [embedding](../docs/internals.html#embedding).
|
|
91
98
|
|
|
92
99
|
## Migration
|
package/dist/ast.d.ts
CHANGED
package/dist/builtins.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { compareValues, equals, fromJson, hasKey, isDict, isList, newDict, runtimeError, setKey, str, toJson, truthy, typeName, } from './values.js';
|
|
1
|
+
import { assertMutable, compareValues, equals, fromJson, hasKey, isDict, isList, newDict, runtimeError, setKey, str, toJson, truthy, typeName, } from './values.js';
|
|
2
2
|
function arity(name, args, min, max = min) {
|
|
3
3
|
if (args.length < min || args.length > max) {
|
|
4
4
|
const expected = min === max ? `${min}` : `${min}-${max}`;
|
|
@@ -346,5 +346,10 @@ export function callMethod(self, name, args) {
|
|
|
346
346
|
if (!table || !Object.prototype.hasOwnProperty.call(table, name)) {
|
|
347
347
|
throw runtimeError(`${typeName(self)} has no method '${name}'`);
|
|
348
348
|
}
|
|
349
|
+
const mutating = isList(self)
|
|
350
|
+
? ['append', 'extend', 'insert', 'pop', 'remove', 'reverse', 'sort', 'clear']
|
|
351
|
+
: isDict(self) ? ['pop', 'update', 'clear'] : [];
|
|
352
|
+
if (mutating.includes(name))
|
|
353
|
+
assertMutable(self);
|
|
349
354
|
return table[name](self, args);
|
|
350
355
|
}
|
package/dist/cli.js
CHANGED
|
@@ -24,6 +24,7 @@ Options:
|
|
|
24
24
|
All levels include LLM transcripts and [deterministic] statement traces.
|
|
25
25
|
--silent suppress all logs; only the result is printed
|
|
26
26
|
--model <id> model override (takes precedence over @{model=...})
|
|
27
|
+
--arg <value> script argument exposed in $args (repeatable; also --arg=value)
|
|
27
28
|
--mcp <file.json> MCP servers in VS Code mcp.json format
|
|
28
29
|
--store <file.json> persist top-level variables in a JSON file
|
|
29
30
|
--store-module <path> custom variable store: module exporting createStore() -> { load(), save(vars) }
|
|
@@ -79,6 +80,7 @@ async function main() {
|
|
|
79
80
|
'log-level': { type: 'string' },
|
|
80
81
|
silent: { type: 'boolean' },
|
|
81
82
|
model: { type: 'string' },
|
|
83
|
+
arg: { type: 'string', multiple: true },
|
|
82
84
|
mcp: { type: 'string' },
|
|
83
85
|
store: { type: 'string' },
|
|
84
86
|
'store-module': { type: 'string' },
|
|
@@ -177,7 +179,7 @@ async function main() {
|
|
|
177
179
|
}
|
|
178
180
|
let result;
|
|
179
181
|
try {
|
|
180
|
-
result = await run(program, { backend, logger, store: values.replay ? undefined : store, cwd, model: values.model });
|
|
182
|
+
result = await run(program, { backend, logger, store: values.replay ? undefined : store, cwd, model: values.model, args: values.arg });
|
|
181
183
|
}
|
|
182
184
|
finally {
|
|
183
185
|
try {
|
package/dist/interpreter.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface RunOptions {
|
|
|
12
12
|
cwd?: string;
|
|
13
13
|
/** Overrides the `@{model=...}` directive. */
|
|
14
14
|
model?: string;
|
|
15
|
+
/** Script arguments exposed as read-only `$args`; defaults to an empty list. */
|
|
16
|
+
args?: readonly string[];
|
|
15
17
|
maxCallDepth?: number;
|
|
16
18
|
}
|
|
17
19
|
export type RunResult = {
|
package/dist/interpreter.js
CHANGED
|
@@ -5,7 +5,7 @@ import { compileSchema } from './schema.js';
|
|
|
5
5
|
import { Restrictions, makeRestriction } from './restrict.js';
|
|
6
6
|
import { MemoryStore } from './store.js';
|
|
7
7
|
import { ToolHost, TOOL_SPECS } from './tools.js';
|
|
8
|
-
import { compareValues, equals, hasKey, isDict, isFunction, isList, newDict, runtimeError, setKey, str, toJson, truthy, typeName, } from './values.js';
|
|
8
|
+
import { assertMutable, compareValues, equals, hasKey, isDict, isFunction, isList, newDict, runtimeError, setKey, str, toJson, truthy, typeName, } from './values.js';
|
|
9
9
|
class ExpressionSignal {
|
|
10
10
|
signal;
|
|
11
11
|
constructor(signal) {
|
|
@@ -24,6 +24,8 @@ class Interpreter {
|
|
|
24
24
|
cwd;
|
|
25
25
|
model;
|
|
26
26
|
maxDepth;
|
|
27
|
+
runtimeEnv = newDict();
|
|
28
|
+
runtimeArgs;
|
|
27
29
|
constraints = [];
|
|
28
30
|
restrictions;
|
|
29
31
|
depth = 0;
|
|
@@ -34,6 +36,13 @@ class Interpreter {
|
|
|
34
36
|
this.store = opts.store ?? new MemoryStore();
|
|
35
37
|
this.model = opts.model ?? program.directives.model;
|
|
36
38
|
this.maxDepth = opts.maxCallDepth ?? 200;
|
|
39
|
+
for (const [name, value] of Object.entries(process.env)) {
|
|
40
|
+
if (value !== undefined)
|
|
41
|
+
setKey(this.runtimeEnv, name, value);
|
|
42
|
+
}
|
|
43
|
+
this.runtimeArgs = [...(opts.args ?? [])];
|
|
44
|
+
Object.freeze(this.runtimeEnv);
|
|
45
|
+
Object.freeze(this.runtimeArgs);
|
|
37
46
|
this.restrictions = new Restrictions(this.cwd);
|
|
38
47
|
this.builtins = makeBuiltins({ log: (m) => opts.logger.info(`log: ${m}`) });
|
|
39
48
|
for (const tool of Object.keys(TOOL_SPECS)) {
|
|
@@ -263,6 +272,7 @@ class Interpreter {
|
|
|
263
272
|
return;
|
|
264
273
|
}
|
|
265
274
|
const obj = await this.eval(target.obj, env);
|
|
275
|
+
assertMutable(obj);
|
|
266
276
|
if (target.t === 'member') {
|
|
267
277
|
if (!isDict(obj))
|
|
268
278
|
throw runtimeError(`cannot set member '${target.name}' on ${typeName(obj)}`);
|
|
@@ -314,6 +324,7 @@ class Interpreter {
|
|
|
314
324
|
return null;
|
|
315
325
|
}
|
|
316
326
|
case 'name': return this.lookup(e.name, env);
|
|
327
|
+
case 'special': return e.name === 'env' ? this.runtimeEnv : this.runtimeArgs;
|
|
317
328
|
case 'list': {
|
|
318
329
|
const out = [];
|
|
319
330
|
for (const it of e.items)
|
|
@@ -379,8 +390,11 @@ class Interpreter {
|
|
|
379
390
|
}
|
|
380
391
|
}
|
|
381
392
|
getKey(d, k) {
|
|
382
|
-
if (!hasKey(d, k))
|
|
393
|
+
if (!hasKey(d, k)) {
|
|
394
|
+
if (d === this.runtimeEnv)
|
|
395
|
+
return null;
|
|
383
396
|
throw runtimeError(`key '${k}' not found`);
|
|
397
|
+
}
|
|
384
398
|
return d[k];
|
|
385
399
|
}
|
|
386
400
|
async evalCall(callee, argExprs, env) {
|
|
@@ -493,13 +507,13 @@ class Interpreter {
|
|
|
493
507
|
if (typeof index !== 'number' || !Number.isInteger(index))
|
|
494
508
|
throw runtimeError(`indices must be integers, got ${typeName(index)}`);
|
|
495
509
|
const i = index < 0 ? list.length + index : index;
|
|
496
|
-
if (i < 0 || i >= list.length)
|
|
510
|
+
if ((i < 0 || i >= list.length) && list !== this.runtimeArgs)
|
|
497
511
|
throw runtimeError(`index ${index} out of range`);
|
|
498
512
|
return i;
|
|
499
513
|
}
|
|
500
514
|
index(obj, index) {
|
|
501
515
|
if (isList(obj))
|
|
502
|
-
return obj[this.listIndex(obj, index)];
|
|
516
|
+
return obj[this.listIndex(obj, index)] ?? null;
|
|
503
517
|
if (typeof obj === 'string') {
|
|
504
518
|
const chars = Array.from(obj);
|
|
505
519
|
return chars[this.listIndex(chars, index)];
|
package/dist/lexer.js
CHANGED
|
@@ -71,8 +71,10 @@ export function tokenize(src, opts = {}) {
|
|
|
71
71
|
tokens.push(readString(raw));
|
|
72
72
|
continue;
|
|
73
73
|
}
|
|
74
|
-
if (isNameStart(c)) {
|
|
75
|
-
|
|
74
|
+
if (isNameStart(c) || c === '$') {
|
|
75
|
+
if (c === '$' && (i + 1 >= src.length || !isNameStart(src[i + 1])))
|
|
76
|
+
err("unexpected character '$'");
|
|
77
|
+
let j = i + (c === '$' ? 1 : 0);
|
|
76
78
|
while (j < src.length && isNameChar(src[j]))
|
|
77
79
|
j++;
|
|
78
80
|
const value = src.slice(i, j);
|
package/dist/parser.js
CHANGED
|
@@ -69,6 +69,8 @@ class Parser {
|
|
|
69
69
|
if (t.type !== 'name')
|
|
70
70
|
this.fail(`expected ${what} but found ${this.describe(t)}`);
|
|
71
71
|
const name = t.value;
|
|
72
|
+
if (name.startsWith('$'))
|
|
73
|
+
this.fail(`special variable '${name}' cannot be used as ${what}`);
|
|
72
74
|
if (KEYWORDS.has(name))
|
|
73
75
|
this.fail(`'${name}' is a reserved word and cannot be used as ${what}`);
|
|
74
76
|
this.next();
|
|
@@ -476,7 +478,7 @@ class Parser {
|
|
|
476
478
|
else if (this.isOp('.')) {
|
|
477
479
|
this.next();
|
|
478
480
|
const t = this.tok;
|
|
479
|
-
if (t.type !== 'name')
|
|
481
|
+
if (t.type !== 'name' || t.value.startsWith('$'))
|
|
480
482
|
this.fail("expected a member name after '.'");
|
|
481
483
|
this.next();
|
|
482
484
|
e = { t: 'member', obj: e, name: t.value };
|
|
@@ -585,6 +587,12 @@ class Parser {
|
|
|
585
587
|
return this.fail(`unexpected ${this.describe(t)}`);
|
|
586
588
|
}
|
|
587
589
|
parseNamePrimary(t) {
|
|
590
|
+
if (t.value.startsWith('$')) {
|
|
591
|
+
if (t.value !== '$env' && t.value !== '$args')
|
|
592
|
+
this.fail(`unknown special variable '${t.value}'`);
|
|
593
|
+
this.next();
|
|
594
|
+
return { t: 'special', name: t.value === '$env' ? 'env' : 'args' };
|
|
595
|
+
}
|
|
588
596
|
switch (t.value) {
|
|
589
597
|
case 'True':
|
|
590
598
|
this.next();
|
package/dist/values.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface BuiltinFunction {
|
|
|
21
21
|
}
|
|
22
22
|
export type FunctionValue = UserFunction | BuiltinFunction;
|
|
23
23
|
export declare const runtimeError: (msg: string) => Failure;
|
|
24
|
+
export declare function assertMutable(v: Value): void;
|
|
24
25
|
export declare function isFunction(v: unknown): v is FunctionValue;
|
|
25
26
|
export declare function isList(v: unknown): v is Value[];
|
|
26
27
|
export declare function isDict(v: unknown): v is {
|
package/dist/values.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { Failure } from './errors.js';
|
|
2
2
|
export const runtimeError = (msg) => new Failure('runtime_error', msg);
|
|
3
|
+
export function assertMutable(v) {
|
|
4
|
+
if (typeof v === 'object' && v !== null && Object.isFrozen(v)) {
|
|
5
|
+
throw runtimeError('cannot modify a read-only value');
|
|
6
|
+
}
|
|
7
|
+
}
|
|
3
8
|
export function isFunction(v) {
|
|
4
9
|
return typeof v === 'object' && v !== null && '__fn' in v && (v.__fn === 'user' || v.__fn === 'builtin');
|
|
5
10
|
}
|
package/docs/syntax.ebnf
CHANGED
|
@@ -46,9 +46,10 @@ call = "(" , [ expr , { "," , expr } , [ "," ] ] , ")" ;
|
|
|
46
46
|
index = "[" , expr , "]" ;
|
|
47
47
|
slice = "[" , [ expr ] , ":" , [ expr ] , "]" ;
|
|
48
48
|
member = "." , NAME ;
|
|
49
|
-
primary = NUMBER | string | "True" | "False" | "None" | NAME
|
|
49
|
+
primary = NUMBER | string | "True" | "False" | "None" | NAME | special_var
|
|
50
50
|
| "(" , expr , ")" | list | dict | block | if_stmt | log_expr
|
|
51
51
|
| ( "think" | "agent" ) , [ modifiers ] , postfix ;
|
|
52
|
+
special_var = "$env" | "$args" ;
|
|
52
53
|
list = "[" , [ expr , { "," , expr } , [ "," ] ] , "]" ;
|
|
53
54
|
dict = "{" , [ expr , ":" , expr , { "," , expr , ":" , expr } , [ "," ] ] , "}" ;
|
|
54
55
|
modifiers = "{" , [ modifier , { "," , modifier } , [ "," ] ] , "}" ;
|
|
@@ -84,6 +85,12 @@ NAME = ( letter | "_" ) , { letter | digit | "_" } ;
|
|
|
84
85
|
NL = newline ;
|
|
85
86
|
|
|
86
87
|
(* "{{"/"}}" are literal braces; raw strings have no interpolation or escapes.
|
|
88
|
+
$env is a read-only process-environment dict snapshot; missing keys return None.
|
|
89
|
+
$args is a read-only list of repeated CLI --arg strings (empty by default);
|
|
90
|
+
out-of-range integer indexes return None, and negative indexes count from the end.
|
|
91
|
+
Both are available in every scope and interpolation, cannot be rebound or mutated
|
|
92
|
+
(including through aliases), and are not automatically persisted.
|
|
93
|
+
Other $-prefixed names are syntax errors. Ordinary dict/list missing reads still fail.
|
|
87
94
|
Reserved words: let if elif else for in while def return try except assert fail success
|
|
88
95
|
enforce forbid restrict think agent break continue pass and or not True False None.
|
|
89
96
|
Removed syntax: think{level=...}, tool{param=...} 'instructions',
|