spl-lang 0.3.1 → 0.4.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 +14 -6
- package/dist/ast.d.ts +17 -1
- package/dist/interpreter.js +70 -29
- package/dist/lexer.js +9 -1
- package/dist/llm.d.ts +3 -1
- package/dist/llm.js +13 -5
- package/dist/parser.d.ts +1 -0
- package/dist/parser.js +51 -20
- package/docs/syntax.ebnf +14 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,16 +26,22 @@ under `~/.spl` (override with `SPL_HOME`); `spl logout` removes those saved cred
|
|
|
26
26
|
- **Generation:** `?` and `think` have no tools or MCP access.
|
|
27
27
|
- **Agency:** `agent` explicitly enables model-directed runtime and configured MCP tools.
|
|
28
28
|
- **Validation:** expression-valued `schema` uses strict synchronous JSON Schema draft-07;
|
|
29
|
-
`fmt
|
|
30
|
-
|
|
29
|
+
`fmt` supports `integer`, `float`, `array`, `object`, `text`, and `boolean`.
|
|
30
|
+
Non-text formats require strict JSON of the selected type; `float` accepts any finite
|
|
31
|
+
number, including whole numbers, and `object` requires a dict, not a list.
|
|
32
|
+
`fmt='json'` still accepts any JSON value. Prose and code fences are not silently extracted.
|
|
31
33
|
- **Repairs:** `repairs=0..5` retries only malformed/invalid generation, not transport errors
|
|
32
34
|
or external actions. Agents require `repairs=0`. Use `assert` for business rules.
|
|
33
35
|
- **Guidance:** `enforce`/`forbid` and `len` influence prompts, not runtime guarantees.
|
|
34
36
|
- **Capabilities:** nested `restrict fs/net/exec/mcp` clauses intersect. Shell command
|
|
35
37
|
inspection is best effort; MCP servers are trusted services, not OS-sandboxed.
|
|
36
|
-
- **Bindings:**
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
- **Bindings:** every block has a lexical child scope. `let` declares/redeclares locally
|
|
39
|
+
and shadows outer names. Reads and assignments search outward for the first declaration;
|
|
40
|
+
child locals, loop variables and caught failures do not leak to parents.
|
|
41
|
+
- **Expressions:** blocks and conditionals yield their final expression, otherwise `None`.
|
|
42
|
+
`{}` remains an empty dict. Functions still require explicit `return`.
|
|
43
|
+
- **Logging:** `log(...)` defaults to info. `log{level=debug}(...)` selects error/info/debug;
|
|
44
|
+
bare level names, strings and expressions are accepted and checked at runtime.
|
|
39
45
|
|
|
40
46
|
Common generation modifiers are `len`, `fmt`, `reasoning`, `show`, `schema`, `repairs`,
|
|
41
47
|
and `model`. Values are evaluated once, then validated before calling the model.
|
|
@@ -88,4 +94,6 @@ See [embedding](../docs/internals.html#embedding).
|
|
|
88
94
|
Use `enforce` and `forbid` for scoped prompt guidance; `instruct` and `avoid` are not accepted.
|
|
89
95
|
0.3 removes `think{level=...}`, tool-command syntax, and `let` before
|
|
90
96
|
compound/member/index assignments. Use `reasoning`, ordinary raw functions, and explicit agents instead.
|
|
91
|
-
|
|
97
|
+
Blocks create lexical child scopes; replace inner `let` with assignment when an outer
|
|
98
|
+
binding must be updated. See the [migration table](../README.md#breaking-changes-in-03).
|
|
99
|
+
`fmt='object'` is now object-only; use `fmt='array'` for lists or `fmt='json'` for any JSON value.
|
package/dist/ast.d.ts
CHANGED
|
@@ -60,6 +60,22 @@ export type Expr = {
|
|
|
60
60
|
obj: Expr;
|
|
61
61
|
lo?: Expr;
|
|
62
62
|
hi?: Expr;
|
|
63
|
+
} | {
|
|
64
|
+
t: 'block';
|
|
65
|
+
body: Stmt[];
|
|
66
|
+
line: number;
|
|
67
|
+
} | {
|
|
68
|
+
t: 'if';
|
|
69
|
+
branches: {
|
|
70
|
+
cond: Expr;
|
|
71
|
+
body: Stmt[];
|
|
72
|
+
}[];
|
|
73
|
+
orelse?: Stmt[];
|
|
74
|
+
line: number;
|
|
75
|
+
} | {
|
|
76
|
+
t: 'log';
|
|
77
|
+
level: Expr;
|
|
78
|
+
args: Expr[];
|
|
63
79
|
} | {
|
|
64
80
|
t: 'ask' | 'think' | 'agent';
|
|
65
81
|
mods: Record<string, Expr>;
|
|
@@ -67,7 +83,7 @@ export type Expr = {
|
|
|
67
83
|
};
|
|
68
84
|
export interface AskMods {
|
|
69
85
|
len?: 'word' | 'sentence' | 'paragraph' | 'long';
|
|
70
|
-
fmt?: '
|
|
86
|
+
fmt?: 'integer' | 'float' | 'array' | 'object' | 'text' | 'boolean' | 'json';
|
|
71
87
|
}
|
|
72
88
|
export type ToolName = 'read_file' | 'write_file' | 'web_fetch' | 'web_search' | 'shell';
|
|
73
89
|
export type RestrictKind = 'fs' | 'net' | 'exec' | 'mcp';
|
package/dist/interpreter.js
CHANGED
|
@@ -6,6 +6,12 @@ import { Restrictions, makeRestriction } from './restrict.js';
|
|
|
6
6
|
import { MemoryStore } from './store.js';
|
|
7
7
|
import { ToolHost, TOOL_SPECS } from './tools.js';
|
|
8
8
|
import { compareValues, equals, hasKey, isDict, isFunction, isList, newDict, runtimeError, setKey, str, toJson, truthy, typeName, } from './values.js';
|
|
9
|
+
class ExpressionSignal {
|
|
10
|
+
signal;
|
|
11
|
+
constructor(signal) {
|
|
12
|
+
this.signal = signal;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
9
15
|
export async function run(program, opts) {
|
|
10
16
|
return new Interpreter(program, opts).run();
|
|
11
17
|
}
|
|
@@ -45,7 +51,7 @@ class Interpreter {
|
|
|
45
51
|
const signal = await this.execBlock(this.program.body, this.globals);
|
|
46
52
|
if (signal?.k === 'return')
|
|
47
53
|
return { status: 'success', value: signal.v, hasValue: signal.hasValue };
|
|
48
|
-
if (signal)
|
|
54
|
+
if (signal && signal.k !== 'value')
|
|
49
55
|
throw runtimeError(`'${signal.k}' outside of a loop`);
|
|
50
56
|
return { status: 'success', value: null, hasValue: false };
|
|
51
57
|
}
|
|
@@ -75,23 +81,33 @@ class Interpreter {
|
|
|
75
81
|
}
|
|
76
82
|
// ---- environments --------------------------------------------------
|
|
77
83
|
lookup(name, env) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
84
|
+
const scope = this.bindingScope(name, env);
|
|
85
|
+
if (scope)
|
|
86
|
+
return scope.vars.get(name);
|
|
82
87
|
const b = this.builtins.get(name);
|
|
83
88
|
if (b)
|
|
84
89
|
return b;
|
|
85
90
|
throw runtimeError(`name '${name}' is not defined`);
|
|
86
91
|
}
|
|
92
|
+
bindingScope(name, env) {
|
|
93
|
+
for (let scope = env; scope; scope = scope.parent) {
|
|
94
|
+
if (scope.vars.has(name))
|
|
95
|
+
return scope;
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
childScope(parent) {
|
|
100
|
+
return { vars: new Map(), parent };
|
|
101
|
+
}
|
|
87
102
|
// ---- statements ----------------------------------------------------
|
|
88
103
|
async execBlock(stmts, env) {
|
|
104
|
+
let completion;
|
|
89
105
|
for (const s of stmts) {
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
return
|
|
106
|
+
completion = await this.execStmt(s, env);
|
|
107
|
+
if (completion && completion.k !== 'value')
|
|
108
|
+
return completion;
|
|
93
109
|
}
|
|
94
|
-
return
|
|
110
|
+
return completion;
|
|
95
111
|
}
|
|
96
112
|
async execStmt(s, env) {
|
|
97
113
|
try {
|
|
@@ -99,6 +115,8 @@ class Interpreter {
|
|
|
99
115
|
return await this.execStmtInner(s, env);
|
|
100
116
|
}
|
|
101
117
|
catch (e) {
|
|
118
|
+
if (e instanceof ExpressionSignal)
|
|
119
|
+
return e.signal;
|
|
102
120
|
if (e instanceof Failure)
|
|
103
121
|
throw e.locate(s.line, this.program.lines[s.line - 1]?.trim() ?? '');
|
|
104
122
|
if (e instanceof RangeError) {
|
|
@@ -116,10 +134,9 @@ class Interpreter {
|
|
|
116
134
|
await this.execAssign(s.target, s.op, s.value, env);
|
|
117
135
|
return;
|
|
118
136
|
case 'block':
|
|
119
|
-
return this.execBlock(s.body, env);
|
|
137
|
+
return this.execBlock(s.body, this.childScope(env));
|
|
120
138
|
case 'expr':
|
|
121
|
-
await this.eval(s.e, env);
|
|
122
|
-
return;
|
|
139
|
+
return { k: 'value', v: await this.eval(s.e, env) };
|
|
123
140
|
case 'pass':
|
|
124
141
|
return;
|
|
125
142
|
case 'break':
|
|
@@ -131,13 +148,13 @@ class Interpreter {
|
|
|
131
148
|
case 'if': {
|
|
132
149
|
for (const b of s.branches) {
|
|
133
150
|
if (truthy(await this.eval(b.cond, env)))
|
|
134
|
-
return this.execBlock(b.body, env);
|
|
151
|
+
return this.execBlock(b.body, this.childScope(env));
|
|
135
152
|
}
|
|
136
|
-
return s.orelse ? this.execBlock(s.orelse, env) : undefined;
|
|
153
|
+
return s.orelse ? this.execBlock(s.orelse, this.childScope(env)) : undefined;
|
|
137
154
|
}
|
|
138
155
|
case 'while': {
|
|
139
156
|
while (truthy(await this.eval(s.cond, env))) {
|
|
140
|
-
const sig = await this.execBlock(s.body, env);
|
|
157
|
+
const sig = await this.execBlock(s.body, this.childScope(env));
|
|
141
158
|
if (sig?.k === 'break')
|
|
142
159
|
break;
|
|
143
160
|
if (sig?.k === 'return')
|
|
@@ -148,8 +165,9 @@ class Interpreter {
|
|
|
148
165
|
case 'for': {
|
|
149
166
|
const items = iterate(await this.eval(s.iter, env));
|
|
150
167
|
for (let i = 0; i < items.length; i++) {
|
|
151
|
-
this.
|
|
152
|
-
|
|
168
|
+
const scope = this.childScope(env);
|
|
169
|
+
this.bindLoopVars(s.vars, items[i], scope);
|
|
170
|
+
const sig = await this.execBlock(s.body, scope);
|
|
153
171
|
if (sig?.k === 'break')
|
|
154
172
|
break;
|
|
155
173
|
if (sig?.k === 'return')
|
|
@@ -167,7 +185,8 @@ class Interpreter {
|
|
|
167
185
|
const savedRestrictions = this.restrictions;
|
|
168
186
|
const savedDepth = this.depth;
|
|
169
187
|
try {
|
|
170
|
-
|
|
188
|
+
const completion = await this.execBlock(s.body, this.childScope(env));
|
|
189
|
+
return completion?.k === 'value' ? undefined : completion;
|
|
171
190
|
}
|
|
172
191
|
catch (e) {
|
|
173
192
|
if (!(e instanceof Failure))
|
|
@@ -176,8 +195,10 @@ class Interpreter {
|
|
|
176
195
|
this.restrictions = savedRestrictions;
|
|
177
196
|
this.depth = savedDepth;
|
|
178
197
|
this.opts.logger.debug(`caught ${e.statusCode}: ${e.reason}`);
|
|
179
|
-
|
|
180
|
-
|
|
198
|
+
const scope = this.childScope(env);
|
|
199
|
+
scope.vars.set(s.errName, newDict(Object.entries(e.payload())));
|
|
200
|
+
const completion = await this.execBlock(s.handler, scope);
|
|
201
|
+
return completion?.k === 'value' ? undefined : completion;
|
|
181
202
|
}
|
|
182
203
|
}
|
|
183
204
|
case 'assert': {
|
|
@@ -195,7 +216,8 @@ class Interpreter {
|
|
|
195
216
|
const saved = this.constraints;
|
|
196
217
|
this.constraints = [...saved, { kind: s.kind, text }];
|
|
197
218
|
try {
|
|
198
|
-
|
|
219
|
+
const completion = await this.execBlock(s.body, this.childScope(env));
|
|
220
|
+
return completion?.k === 'value' ? undefined : completion;
|
|
199
221
|
}
|
|
200
222
|
finally {
|
|
201
223
|
this.constraints = saved;
|
|
@@ -209,7 +231,8 @@ class Interpreter {
|
|
|
209
231
|
this.restrictions = this.restrictions.with(restriction);
|
|
210
232
|
this.opts.logger.debug(`restrict ${clause.kind}: ${restriction.values.join(', ')}`);
|
|
211
233
|
}
|
|
212
|
-
|
|
234
|
+
const completion = await this.execBlock(s.body, this.childScope(env));
|
|
235
|
+
return completion?.k === 'value' ? undefined : completion;
|
|
213
236
|
}
|
|
214
237
|
finally {
|
|
215
238
|
this.restrictions = saved;
|
|
@@ -233,9 +256,10 @@ class Interpreter {
|
|
|
233
256
|
return op === '=' ? rhs : this.binary(op.slice(0, -1), current(), rhs);
|
|
234
257
|
};
|
|
235
258
|
if (target.t === 'name') {
|
|
236
|
-
|
|
259
|
+
const scope = this.bindingScope(target.name, env);
|
|
260
|
+
if (!scope)
|
|
237
261
|
throw runtimeError(`cannot assign '${target.name}': declare a local with let first`);
|
|
238
|
-
|
|
262
|
+
scope.vars.set(target.name, await compute(() => this.lookup(target.name, env)));
|
|
239
263
|
return;
|
|
240
264
|
}
|
|
241
265
|
const obj = await this.eval(target.obj, env);
|
|
@@ -273,6 +297,22 @@ class Interpreter {
|
|
|
273
297
|
case 'str': return this.text(e.parts, env);
|
|
274
298
|
case 'bool': return e.v;
|
|
275
299
|
case 'none': return null;
|
|
300
|
+
case 'block':
|
|
301
|
+
case 'if': {
|
|
302
|
+
const completion = await this.execStmt(e, env);
|
|
303
|
+
if (completion && completion.k !== 'value')
|
|
304
|
+
throw new ExpressionSignal(completion);
|
|
305
|
+
return completion?.v ?? null;
|
|
306
|
+
}
|
|
307
|
+
case 'log': {
|
|
308
|
+
const level = await this.eval(e.level, env);
|
|
309
|
+
if (level !== 'error' && level !== 'info' && level !== 'debug') {
|
|
310
|
+
throw runtimeError("log level must be 'error', 'info' or 'debug'");
|
|
311
|
+
}
|
|
312
|
+
const args = await this.evalArgs(e.args, env);
|
|
313
|
+
this.opts.logger[level](`log: ${args.map(str).join(' ')}`);
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
276
316
|
case 'name': return this.lookup(e.name, env);
|
|
277
317
|
case 'list': {
|
|
278
318
|
const out = [];
|
|
@@ -373,7 +413,7 @@ class Interpreter {
|
|
|
373
413
|
this.depth++;
|
|
374
414
|
try {
|
|
375
415
|
const sig = await this.execBlock(fn.body, local);
|
|
376
|
-
if (sig && sig.k !== 'return')
|
|
416
|
+
if (sig && sig.k !== 'return' && sig.k !== 'value')
|
|
377
417
|
throw runtimeError(`'${sig.k}' outside of a loop`);
|
|
378
418
|
return sig?.k === 'return' ? sig.v : null;
|
|
379
419
|
}
|
|
@@ -527,8 +567,9 @@ class Interpreter {
|
|
|
527
567
|
const fmt = options.fmt ?? ('schema' in options ? 'json' : 'text');
|
|
528
568
|
if (len !== undefined && len !== 'word' && len !== 'sentence' && len !== 'paragraph' && len !== 'long')
|
|
529
569
|
throw runtimeError('len must be word, sentence, paragraph, or long');
|
|
530
|
-
if (fmt !== '
|
|
531
|
-
throw runtimeError('fmt must be text,
|
|
570
|
+
if (fmt !== 'integer' && fmt !== 'float' && fmt !== 'array' && fmt !== 'object' && fmt !== 'text' && fmt !== 'boolean' && fmt !== 'json') {
|
|
571
|
+
throw runtimeError('fmt must be integer, float, array, object, text, boolean, or json');
|
|
572
|
+
}
|
|
532
573
|
if (reasoning !== undefined && reasoning !== 'low' && reasoning !== 'medium' && reasoning !== 'high')
|
|
533
574
|
throw runtimeError('reasoning must be low, medium, or high');
|
|
534
575
|
if (typeof show !== 'boolean')
|
|
@@ -544,7 +585,7 @@ class Interpreter {
|
|
|
544
585
|
const validate = 'schema' in options ? compileSchema(options.schema) : undefined;
|
|
545
586
|
let prompt = buildPrompt({
|
|
546
587
|
prompt: str(await this.eval(promptExpr, env)),
|
|
547
|
-
mods: { len, fmt
|
|
588
|
+
mods: { len, fmt },
|
|
548
589
|
thinking: reasoning !== undefined,
|
|
549
590
|
constraints: this.constraints,
|
|
550
591
|
restrictions: this.restrictions,
|
|
@@ -564,7 +605,7 @@ class Interpreter {
|
|
|
564
605
|
tools: new ToolHost(this.restrictions, this.cwd, this.opts.backend.mode === 'replay'),
|
|
565
606
|
});
|
|
566
607
|
try {
|
|
567
|
-
const value = fmt === 'text' ? text : parseJsonResponse(text, fmt
|
|
608
|
+
const value = fmt === 'text' ? text : parseJsonResponse(text, fmt);
|
|
568
609
|
validate?.(value);
|
|
569
610
|
return value;
|
|
570
611
|
}
|
package/dist/lexer.js
CHANGED
|
@@ -16,6 +16,7 @@ export function tokenize(src, opts = {}) {
|
|
|
16
16
|
let col = opts.col ?? 1;
|
|
17
17
|
let depth = opts.inExpr ? 1 : 0;
|
|
18
18
|
const brackets = [];
|
|
19
|
+
const braceDepths = [];
|
|
19
20
|
const err = (msg, l = line, c = col) => {
|
|
20
21
|
throw new SplSyntaxError(msg, l, c);
|
|
21
22
|
};
|
|
@@ -83,7 +84,14 @@ export function tokenize(src, opts = {}) {
|
|
|
83
84
|
if (!op)
|
|
84
85
|
err(`unexpected character '${c}'`);
|
|
85
86
|
advance(op.length);
|
|
86
|
-
if (op === '
|
|
87
|
+
if (op === '{') {
|
|
88
|
+
braceDepths.push(depth);
|
|
89
|
+
depth = 0;
|
|
90
|
+
}
|
|
91
|
+
else if (op === '}') {
|
|
92
|
+
depth = braceDepths.pop() ?? depth;
|
|
93
|
+
}
|
|
94
|
+
else if (op === '(' || op === '[') {
|
|
87
95
|
brackets.push(op);
|
|
88
96
|
depth++;
|
|
89
97
|
}
|
package/dist/llm.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export interface LlmBackend {
|
|
|
31
31
|
/** Raised by a backend for any non-script-level LLM problem; becomes `llm_error`. */
|
|
32
32
|
export declare class LlmError extends Error {
|
|
33
33
|
}
|
|
34
|
+
type JsonFormat = Exclude<NonNullable<AskMods['fmt']>, 'text'>;
|
|
34
35
|
export declare function buildPrompt(opts: {
|
|
35
36
|
prompt: string;
|
|
36
37
|
mods?: AskMods;
|
|
@@ -38,4 +39,5 @@ export declare function buildPrompt(opts: {
|
|
|
38
39
|
constraints: readonly Constraint[];
|
|
39
40
|
restrictions: Restrictions;
|
|
40
41
|
}): string;
|
|
41
|
-
export declare function parseJsonResponse(text: string,
|
|
42
|
+
export declare function parseJsonResponse(text: string, format?: JsonFormat): Value;
|
|
43
|
+
export {};
|
package/dist/llm.js
CHANGED
|
@@ -10,6 +10,13 @@ const LEN_HINTS = {
|
|
|
10
10
|
long: 'Give a long, detailed answer.',
|
|
11
11
|
};
|
|
12
12
|
const JSON_HINT = 'Respond with ONLY valid JSON. No prose, no markdown, no code fences.';
|
|
13
|
+
const FORMAT_CONTRACTS = {
|
|
14
|
+
integer: { description: 'a JSON integer', accepts: (value) => typeof value === 'number' && Number.isInteger(value) },
|
|
15
|
+
float: { description: 'a finite JSON number', accepts: (value) => typeof value === 'number' && Number.isFinite(value) },
|
|
16
|
+
array: { description: 'a JSON array', accepts: isList },
|
|
17
|
+
object: { description: 'a JSON object', accepts: isDict },
|
|
18
|
+
boolean: { description: 'a JSON boolean (true or false)', accepts: (value) => typeof value === 'boolean' },
|
|
19
|
+
};
|
|
13
20
|
export function buildPrompt(opts) {
|
|
14
21
|
const sections = [];
|
|
15
22
|
if (opts.constraints.length) {
|
|
@@ -23,12 +30,13 @@ export function buildPrompt(opts) {
|
|
|
23
30
|
sections.push('Think carefully about the problem before giving the final answer.');
|
|
24
31
|
if (opts.mods?.len)
|
|
25
32
|
sections.push(LEN_HINTS[opts.mods.len]);
|
|
26
|
-
if (opts.mods?.fmt
|
|
27
|
-
sections.push(`${JSON_HINT} Return
|
|
33
|
+
if (opts.mods?.fmt && opts.mods.fmt !== 'text' && opts.mods.fmt !== 'json') {
|
|
34
|
+
sections.push(`${JSON_HINT} Return ${FORMAT_CONTRACTS[opts.mods.fmt].description}.`);
|
|
35
|
+
}
|
|
28
36
|
sections.push(opts.prompt);
|
|
29
37
|
return sections.join('\n\n');
|
|
30
38
|
}
|
|
31
|
-
export function parseJsonResponse(text,
|
|
39
|
+
export function parseJsonResponse(text, format = 'json') {
|
|
32
40
|
let value;
|
|
33
41
|
try {
|
|
34
42
|
value = fromJson(JSON.parse(text));
|
|
@@ -38,8 +46,8 @@ export function parseJsonResponse(text, objectOnly = false) {
|
|
|
38
46
|
throw e;
|
|
39
47
|
throw new Failure('invalid_format', `LLM response is not valid JSON: ${e.message}`);
|
|
40
48
|
}
|
|
41
|
-
if (
|
|
42
|
-
throw new Failure('invalid_format',
|
|
49
|
+
if (format !== 'json' && !FORMAT_CONTRACTS[format].accepts(value)) {
|
|
50
|
+
throw new Failure('invalid_format', `LLM response must be ${FORMAT_CONTRACTS[format].description} (fmt='${format}')`);
|
|
43
51
|
}
|
|
44
52
|
return value;
|
|
45
53
|
}
|
package/dist/parser.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { Program } from './ast.js';
|
|
2
2
|
export declare const MODIFIER_NAMES: readonly ['len', 'fmt', 'reasoning', 'show', 'schema', 'repairs', 'model'];
|
|
3
|
+
export declare const LOG_MODIFIER_NAMES: readonly ['level'];
|
|
3
4
|
export declare function parse(source: string): Program;
|
package/dist/parser.js
CHANGED
|
@@ -6,6 +6,7 @@ const KEYWORDS = new Set([
|
|
|
6
6
|
'True', 'False', 'None',
|
|
7
7
|
]);
|
|
8
8
|
export const MODIFIER_NAMES = ['len', 'fmt', 'reasoning', 'show', 'schema', 'repairs', 'model'];
|
|
9
|
+
export const LOG_MODIFIER_NAMES = ['level'];
|
|
9
10
|
const DIRECTIVE_SCHEMA = { model: 'string' };
|
|
10
11
|
const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '//=', '%=']);
|
|
11
12
|
export function parse(source) {
|
|
@@ -160,7 +161,11 @@ class Parser {
|
|
|
160
161
|
}
|
|
161
162
|
parseBlock() {
|
|
162
163
|
this.expectOp('{');
|
|
163
|
-
|
|
164
|
+
return this.finishBlock();
|
|
165
|
+
}
|
|
166
|
+
finishBlock(body = []) {
|
|
167
|
+
if (body.length)
|
|
168
|
+
this.endOfStatement();
|
|
164
169
|
this.skipSeparators();
|
|
165
170
|
while (!this.isOp('}')) {
|
|
166
171
|
if (this.tok.type === 'eof')
|
|
@@ -180,7 +185,6 @@ class Parser {
|
|
|
180
185
|
if (t.type === 'name') {
|
|
181
186
|
switch (t.value) {
|
|
182
187
|
case 'let': return this.parseLet();
|
|
183
|
-
case 'if': return this.parseIf();
|
|
184
188
|
case 'for': {
|
|
185
189
|
this.next();
|
|
186
190
|
const vars = [this.expectIdent('a loop variable')];
|
|
@@ -257,8 +261,6 @@ class Parser {
|
|
|
257
261
|
break;
|
|
258
262
|
}
|
|
259
263
|
}
|
|
260
|
-
if (t.type === 'op' && t.value === '{')
|
|
261
|
-
return { t: 'block', line, body: this.parseBlock() };
|
|
262
264
|
return this.parseSimpleStatement();
|
|
263
265
|
}
|
|
264
266
|
parseLet() {
|
|
@@ -353,7 +355,7 @@ class Parser {
|
|
|
353
355
|
this.next();
|
|
354
356
|
return { t: 'assign', line, target, op: t.value, value: this.parseExpr() };
|
|
355
357
|
}
|
|
356
|
-
return { t: 'expr', line, e };
|
|
358
|
+
return e.t === 'block' || e.t === 'if' ? e : { t: 'expr', line, e };
|
|
357
359
|
}
|
|
358
360
|
toTarget(e, at) {
|
|
359
361
|
if (e.t === 'name')
|
|
@@ -465,17 +467,7 @@ class Parser {
|
|
|
465
467
|
let e = this.parsePrimary();
|
|
466
468
|
for (;;) {
|
|
467
469
|
if (this.isOp('(')) {
|
|
468
|
-
this.
|
|
469
|
-
const args = [];
|
|
470
|
-
while (!this.isOp(')')) {
|
|
471
|
-
args.push(this.parseExpr());
|
|
472
|
-
if (this.isOp(','))
|
|
473
|
-
this.next();
|
|
474
|
-
else if (!this.isOp(')'))
|
|
475
|
-
this.fail("expected ',' or ')' in call");
|
|
476
|
-
}
|
|
477
|
-
this.next();
|
|
478
|
-
e = { t: 'call', callee: e, args };
|
|
470
|
+
e = { t: 'call', callee: e, args: this.parseArgs() };
|
|
479
471
|
}
|
|
480
472
|
else if (this.isOp('[')) {
|
|
481
473
|
this.next();
|
|
@@ -494,6 +486,19 @@ class Parser {
|
|
|
494
486
|
}
|
|
495
487
|
}
|
|
496
488
|
}
|
|
489
|
+
parseArgs() {
|
|
490
|
+
this.expectOp('(');
|
|
491
|
+
const args = [];
|
|
492
|
+
while (!this.isOp(')')) {
|
|
493
|
+
args.push(this.parseExpr());
|
|
494
|
+
if (this.isOp(','))
|
|
495
|
+
this.next();
|
|
496
|
+
else if (!this.isOp(')'))
|
|
497
|
+
this.fail("expected ',' or ')' in call");
|
|
498
|
+
}
|
|
499
|
+
this.next();
|
|
500
|
+
return args;
|
|
501
|
+
}
|
|
497
502
|
parseSubscript(obj) {
|
|
498
503
|
const lo = this.isOp(':') ? undefined : this.parseExpr();
|
|
499
504
|
if (lo && !this.isOp(':')) {
|
|
@@ -544,10 +549,19 @@ class Parser {
|
|
|
544
549
|
}
|
|
545
550
|
if (t.value === '{') {
|
|
546
551
|
this.next();
|
|
547
|
-
this.
|
|
552
|
+
this.skipSeparators();
|
|
553
|
+
if (this.isOp('}')) {
|
|
554
|
+
this.next();
|
|
555
|
+
return { t: 'dict', entries: [] };
|
|
556
|
+
}
|
|
557
|
+
const first = this.parseStatement();
|
|
558
|
+
const key = first.t === 'expr' ? first.e : first.t === 'if' || first.t === 'block' ? first : undefined;
|
|
559
|
+
if (!key || !this.isOp(':')) {
|
|
560
|
+
return { t: 'block', line: t.line, body: this.finishBlock([first]) };
|
|
561
|
+
}
|
|
548
562
|
const entries = [];
|
|
563
|
+
let k = key;
|
|
549
564
|
while (!this.isOp('}')) {
|
|
550
|
-
const k = this.parseExpr();
|
|
551
565
|
this.expectOp(':');
|
|
552
566
|
this.skipNl();
|
|
553
567
|
entries.push([k, this.parseExpr()]);
|
|
@@ -559,6 +573,8 @@ class Parser {
|
|
|
559
573
|
else if (!this.isOp('}')) {
|
|
560
574
|
this.fail("expected ',' or '}' in dict");
|
|
561
575
|
}
|
|
576
|
+
if (!this.isOp('}'))
|
|
577
|
+
k = this.parseExpr();
|
|
562
578
|
}
|
|
563
579
|
this.next();
|
|
564
580
|
return { t: 'dict', entries };
|
|
@@ -579,6 +595,16 @@ class Parser {
|
|
|
579
595
|
case 'None':
|
|
580
596
|
this.next();
|
|
581
597
|
return { t: 'none' };
|
|
598
|
+
case 'if': return this.parseIf();
|
|
599
|
+
case 'log':
|
|
600
|
+
if (this.isOp('{', this.peekAt(1))) {
|
|
601
|
+
this.next();
|
|
602
|
+
const mods = this.parseMods('log');
|
|
603
|
+
const level = mods.level ?? { t: 'str', parts: ['info'] };
|
|
604
|
+
return { t: 'log', level, args: this.parseArgs() };
|
|
605
|
+
}
|
|
606
|
+
this.next();
|
|
607
|
+
return { t: 'name', name: t.value, line: t.line };
|
|
582
608
|
case 'think':
|
|
583
609
|
case 'agent': {
|
|
584
610
|
this.next();
|
|
@@ -601,19 +627,24 @@ class Parser {
|
|
|
601
627
|
this.expectOp('{');
|
|
602
628
|
this.skipNl();
|
|
603
629
|
const out = {};
|
|
630
|
+
const allowed = cmd === 'log' ? LOG_MODIFIER_NAMES : MODIFIER_NAMES;
|
|
604
631
|
while (!this.isOp('}')) {
|
|
605
632
|
const nameTok = this.tok;
|
|
606
633
|
if (nameTok.type !== 'name')
|
|
607
634
|
this.fail('expected a modifier name');
|
|
608
635
|
const name = nameTok.value;
|
|
609
636
|
this.next();
|
|
610
|
-
if (!
|
|
611
|
-
this.fail(`unknown modifier '${name}' for '${cmd}' (allowed: ${
|
|
637
|
+
if (!allowed.includes(name)) {
|
|
638
|
+
this.fail(`unknown modifier '${name}' for '${cmd}' (allowed: ${allowed.join(', ')})`, nameTok);
|
|
612
639
|
}
|
|
613
640
|
if (name in out)
|
|
614
641
|
this.fail(`duplicate modifier '${name}'`, nameTok);
|
|
615
642
|
this.expectOp('=');
|
|
616
643
|
out[name] = this.parseExpr();
|
|
644
|
+
const value = out[name];
|
|
645
|
+
if (cmd === 'log' && value.t === 'name' && ['error', 'info', 'debug'].includes(value.name)) {
|
|
646
|
+
out[name] = { t: 'str', parts: [value.name] };
|
|
647
|
+
}
|
|
617
648
|
this.skipNl();
|
|
618
649
|
if (this.isOp(',')) {
|
|
619
650
|
this.next();
|
package/docs/syntax.ebnf
CHANGED
|
@@ -24,10 +24,11 @@ guidance_stmt = ( "enforce" | "forbid" ) , expr , block ;
|
|
|
24
24
|
restrict_stmt = "restrict" , restrict_clause , { "," , restrict_clause } , block ;
|
|
25
25
|
restrict_clause = [ "fs" | "net" | "exec" | "mcp" ] , expr ;
|
|
26
26
|
|
|
27
|
-
(*
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
(* Every block creates a lexical child scope. let declares/redeclares locally and shadows parents.
|
|
28
|
+
Reads and assignments search outward for the first declaration; undeclared names are errors.
|
|
29
|
+
Each loop iteration has fresh locals; caught failure names belong to the handler scope.
|
|
30
|
+
Blocks/if yield their final expression, otherwise None. An unmatched if yields None.
|
|
31
|
+
{} remains an empty dict; { pass } is a block yielding None. Functions need explicit return.
|
|
31
32
|
return exits the innermost function, or the script at top level.
|
|
32
33
|
success exits the script, including when used in a function. *)
|
|
33
34
|
|
|
@@ -46,16 +47,20 @@ index = "[" , expr , "]" ;
|
|
|
46
47
|
slice = "[" , [ expr ] , ":" , [ expr ] , "]" ;
|
|
47
48
|
member = "." , NAME ;
|
|
48
49
|
primary = NUMBER | string | "True" | "False" | "None" | NAME
|
|
49
|
-
| "(" , expr , ")" | list | dict
|
|
50
|
+
| "(" , expr , ")" | list | dict | block | if_stmt | log_expr
|
|
50
51
|
| ( "think" | "agent" ) , [ modifiers ] , postfix ;
|
|
51
52
|
list = "[" , [ expr , { "," , expr } , [ "," ] ] , "]" ;
|
|
52
53
|
dict = "{" , [ expr , ":" , expr , { "," , expr , ":" , expr } , [ "," ] ] , "}" ;
|
|
53
54
|
modifiers = "{" , [ modifier , { "," , modifier } , [ "," ] ] , "}" ;
|
|
54
55
|
modifier = ( "len" | "fmt" | "reasoning" | "show" | "schema" | "repairs" | "model" ) , "=" , expr ;
|
|
56
|
+
log_expr = "log" , "{" , [ "level" , "=" , log_level , [ "," ] ] , "}" , call ;
|
|
57
|
+
log_level = "error" | "info" | "debug" | expr ;
|
|
55
58
|
|
|
56
59
|
(* Unknown/duplicate modifier names are syntax errors. Values are expressions, validated at runtime:
|
|
57
60
|
len: word/sentence/paragraph/long (guidance only)
|
|
58
|
-
fmt: text/json
|
|
61
|
+
fmt: integer/float/array/object/text/boolean/json; schema implies json unless fmt is explicit
|
|
62
|
+
Non-text formats require strict JSON of the selected type; float accepts finite numbers,
|
|
63
|
+
object excludes arrays, and json accepts any JSON value.
|
|
59
64
|
reasoning: low/medium/high; think and agent default to medium
|
|
60
65
|
show: True/False; schema: synchronous strict JSON Schema draft-07 object or boolean
|
|
61
66
|
repairs: integer 0..5 (additional generation attempts, default 0); agents require 0
|
|
@@ -63,6 +68,9 @@ modifier = ( "len" | "fmt" | "reasoning" | "show" | "schema" | "repairs" |
|
|
|
63
68
|
Generation (? and think) has no tools. agent explicitly enables restricted runtime/MCP tools.
|
|
64
69
|
Raw tools are ordinary positional functions: read_file(path), write_file(path, content),
|
|
65
70
|
web_fetch(url), web_search(query), shell(command). They never invoke a model.
|
|
71
|
+
log{level=...}(args) selects error/info/debug severity; plain log(args) defaults to info.
|
|
72
|
+
Level values are bare names, strings or expressions, validated before evaluating arguments.
|
|
73
|
+
Debug messages are emitted only at debug log level; silent suppresses all logs.
|
|
66
74
|
enforce/forbid are scoped prompt guidance.
|
|
67
75
|
schema/assert validate data; restrict checks actions. *)
|
|
68
76
|
|