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 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
@@ -14,6 +14,9 @@ export type Expr = {
14
14
  t: 'name';
15
15
  name: string;
16
16
  line: number;
17
+ } | {
18
+ t: 'special';
19
+ name: 'env' | 'args';
17
20
  } | {
18
21
  t: 'list';
19
22
  items: Expr[];
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 {
@@ -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 = {
@@ -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
- let j = i;
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',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spl-lang",
3
- "version": "0.4.1",
3
+ "version": "0.5.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",