spl-lang 0.0.0-stage → 0.1.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.
@@ -0,0 +1,350 @@
1
+ import { compareValues, equals, fromJson, hasKey, isDict, isList, newDict, runtimeError, setKey, str, toJson, truthy, typeName, } from './values.js';
2
+ function arity(name, args, min, max = min) {
3
+ if (args.length < min || args.length > max) {
4
+ const expected = min === max ? `${min}` : `${min}-${max}`;
5
+ throw runtimeError(`${name}() takes ${expected} argument(s) but ${args.length} were given`);
6
+ }
7
+ }
8
+ function needNumber(name, v) {
9
+ if (typeof v !== 'number')
10
+ throw runtimeError(`${name}() expects a number, got ${typeName(v)}`);
11
+ return v;
12
+ }
13
+ function needInt(name, v) {
14
+ const n = needNumber(name, v);
15
+ if (!Number.isInteger(n))
16
+ throw runtimeError(`${name}() expects an integer, got ${n}`);
17
+ return n;
18
+ }
19
+ function needString(name, v) {
20
+ if (typeof v !== 'string')
21
+ throw runtimeError(`${name}() expects a string, got ${typeName(v)}`);
22
+ return v;
23
+ }
24
+ function needList(name, v) {
25
+ if (!isList(v))
26
+ throw runtimeError(`${name}() expects a list, got ${typeName(v)}`);
27
+ return v;
28
+ }
29
+ /** Values that can be iterated by `for`, `list()`, `sorted()` etc. */
30
+ export function iterate(v) {
31
+ if (isList(v))
32
+ return v;
33
+ if (typeof v === 'string')
34
+ return Array.from(v);
35
+ if (isDict(v))
36
+ return Object.keys(v);
37
+ throw runtimeError(`${typeName(v)} is not iterable`);
38
+ }
39
+ export function makeBuiltins(ctx) {
40
+ const fns = new Map();
41
+ const def = (name, call) => fns.set(name, { __fn: 'builtin', name, call });
42
+ def('len', (a) => {
43
+ arity('len', a, 1);
44
+ const v = a[0];
45
+ if (typeof v === 'string')
46
+ return Array.from(v).length;
47
+ if (isList(v))
48
+ return v.length;
49
+ if (isDict(v))
50
+ return Object.keys(v).length;
51
+ throw runtimeError(`len() does not support ${typeName(v)}`);
52
+ });
53
+ def('str', (a) => {
54
+ arity('str', a, 1);
55
+ return str(a[0]);
56
+ });
57
+ def('int', (a) => {
58
+ arity('int', a, 1);
59
+ const v = a[0];
60
+ if (typeof v === 'number')
61
+ return Math.trunc(v);
62
+ if (typeof v === 'boolean')
63
+ return v ? 1 : 0;
64
+ if (typeof v === 'string' && /^\s*[+-]?\d+\s*$/.test(v))
65
+ return Number.parseInt(v, 10);
66
+ throw runtimeError(`int() cannot convert ${typeof v === 'string' ? JSON.stringify(v) : typeName(v)}`);
67
+ });
68
+ def('float', (a) => {
69
+ arity('float', a, 1);
70
+ const v = a[0];
71
+ if (typeof v === 'number')
72
+ return v;
73
+ if (typeof v === 'boolean')
74
+ return v ? 1 : 0;
75
+ if (typeof v === 'string' && v.trim() !== '' && Number.isFinite(Number(v)))
76
+ return Number(v);
77
+ throw runtimeError(`float() cannot convert ${typeof v === 'string' ? JSON.stringify(v) : typeName(v)}`);
78
+ });
79
+ def('bool', (a) => {
80
+ arity('bool', a, 1);
81
+ return truthy(a[0]);
82
+ });
83
+ def('type', (a) => {
84
+ arity('type', a, 1);
85
+ return typeName(a[0]);
86
+ });
87
+ def('list', (a) => {
88
+ arity('list', a, 1);
89
+ return [...iterate(a[0])];
90
+ });
91
+ def('range', (a) => {
92
+ arity('range', a, 1, 3);
93
+ const nums = a.map((x) => needInt('range', x));
94
+ const [start, stop, step] = nums.length === 1 ? [0, nums[0], 1] : [nums[0], nums[1], nums[2] ?? 1];
95
+ if (step === 0)
96
+ throw runtimeError('range() step must not be zero');
97
+ const out = [];
98
+ for (let i = start; step > 0 ? i < stop : i > stop; i += step) {
99
+ out.push(i);
100
+ if (out.length > 10_000_000)
101
+ throw runtimeError('range() is too large');
102
+ }
103
+ return out;
104
+ });
105
+ def('abs', (a) => {
106
+ arity('abs', a, 1);
107
+ return Math.abs(needNumber('abs', a[0]));
108
+ });
109
+ def('round', (a) => {
110
+ arity('round', a, 1, 2);
111
+ const n = needNumber('round', a[0]);
112
+ if (a.length === 1) {
113
+ const r = Math.round(Math.abs(n));
114
+ const even = Math.abs(n) % 1 === 0.5 && r % 2 === 1 ? r - 1 : r;
115
+ return n < 0 ? -even : even;
116
+ }
117
+ const f = 10 ** needInt('round', a[1]);
118
+ return Math.round(n * f) / f;
119
+ });
120
+ const extremum = (name, pick) => def(name, (a) => {
121
+ const items = a.length === 1 ? iterate(a[0]) : a;
122
+ if (items.length === 0)
123
+ throw runtimeError(`${name}() of an empty sequence`);
124
+ return items.reduce((best, x) => (pick(compareValues(x, best)) ? x : best));
125
+ });
126
+ extremum('min', (c) => c < 0);
127
+ extremum('max', (c) => c > 0);
128
+ def('sum', (a) => {
129
+ arity('sum', a, 1);
130
+ return iterate(a[0]).reduce((acc, x) => acc + needNumber('sum', x), 0);
131
+ });
132
+ def('sorted', (a) => {
133
+ arity('sorted', a, 1, 2);
134
+ const out = [...iterate(a[0])].sort(compareValues);
135
+ return a[1] !== undefined && truthy(a[1]) ? out.reverse() : out;
136
+ });
137
+ def('reversed', (a) => {
138
+ arity('reversed', a, 1);
139
+ return [...iterate(a[0])].reverse();
140
+ });
141
+ def('enumerate', (a) => {
142
+ arity('enumerate', a, 1);
143
+ return iterate(a[0]).map((x, i) => [i, x]);
144
+ });
145
+ def('zip', (a) => {
146
+ if (a.length === 0)
147
+ return [];
148
+ const lists = a.map(iterate);
149
+ const n = Math.min(...lists.map((l) => l.length));
150
+ return Array.from({ length: n }, (_, i) => lists.map((l) => l[i]));
151
+ });
152
+ def('any', (a) => {
153
+ arity('any', a, 1);
154
+ return iterate(a[0]).some(truthy);
155
+ });
156
+ def('all', (a) => {
157
+ arity('all', a, 1);
158
+ return iterate(a[0]).every(truthy);
159
+ });
160
+ def('keys', (a) => {
161
+ arity('keys', a, 1);
162
+ if (!isDict(a[0]))
163
+ throw runtimeError(`keys() expects a dict, got ${typeName(a[0])}`);
164
+ return Object.keys(a[0]);
165
+ });
166
+ def('values', (a) => {
167
+ arity('values', a, 1);
168
+ if (!isDict(a[0]))
169
+ throw runtimeError(`values() expects a dict, got ${typeName(a[0])}`);
170
+ return Object.values(a[0]);
171
+ });
172
+ def('items', (a) => {
173
+ arity('items', a, 1);
174
+ if (!isDict(a[0]))
175
+ throw runtimeError(`items() expects a dict, got ${typeName(a[0])}`);
176
+ return Object.entries(a[0]);
177
+ });
178
+ def('json_parse', (a) => {
179
+ arity('json_parse', a, 1);
180
+ try {
181
+ return fromJson(JSON.parse(needString('json_parse', a[0])));
182
+ }
183
+ catch (e) {
184
+ if (e instanceof SyntaxError)
185
+ throw runtimeError(`json_parse(): ${e.message}`);
186
+ throw e;
187
+ }
188
+ });
189
+ def('json_stringify', (a) => {
190
+ arity('json_stringify', a, 1, 2);
191
+ const indent = a[1] === undefined ? undefined : needInt('json_stringify', a[1]);
192
+ return JSON.stringify(toJson(a[0]), null, indent);
193
+ });
194
+ def('log', (a) => {
195
+ ctx.log(a.map(str).join(' '));
196
+ return null;
197
+ });
198
+ return fns;
199
+ }
200
+ const stringMethods = {
201
+ upper: (s) => s.toUpperCase(),
202
+ lower: (s) => s.toLowerCase(),
203
+ strip: (s) => s.trim(),
204
+ lstrip: (s) => s.trimStart(),
205
+ rstrip: (s) => s.trimEnd(),
206
+ title: (s) => s.toLowerCase().replace(/(^|\s)\S/g, (m) => m.toUpperCase()),
207
+ capitalize: (s) => (s ? s[0].toUpperCase() + s.slice(1).toLowerCase() : s),
208
+ isdigit: (s) => /^\d+$/.test(s),
209
+ split: (s, a) => {
210
+ arity('split', a, 0, 1);
211
+ if (a.length === 0 || a[0] === null)
212
+ return s.trim() === '' ? [] : s.trim().split(/\s+/);
213
+ const sep = needString('split', a[0]);
214
+ if (sep === '')
215
+ throw runtimeError('split() separator must not be empty');
216
+ return s.split(sep);
217
+ },
218
+ splitlines: (s) => (s === '' ? [] : s.split(/\r?\n/)),
219
+ join: (s, a) => {
220
+ arity('join', a, 1);
221
+ return iterate(a[0]).map((x) => {
222
+ if (typeof x !== 'string')
223
+ throw runtimeError(`join() expects strings, got ${typeName(x)}`);
224
+ return x;
225
+ }).join(s);
226
+ },
227
+ replace: (s, a) => {
228
+ arity('replace', a, 2);
229
+ return s.split(needString('replace', a[0])).join(needString('replace', a[1]));
230
+ },
231
+ startswith: (s, a) => {
232
+ arity('startswith', a, 1);
233
+ return s.startsWith(needString('startswith', a[0]));
234
+ },
235
+ endswith: (s, a) => {
236
+ arity('endswith', a, 1);
237
+ return s.endsWith(needString('endswith', a[0]));
238
+ },
239
+ find: (s, a) => {
240
+ arity('find', a, 1);
241
+ return s.indexOf(needString('find', a[0]));
242
+ },
243
+ count: (s, a) => {
244
+ arity('count', a, 1);
245
+ const sub = needString('count', a[0]);
246
+ return sub === '' ? s.length + 1 : s.split(sub).length - 1;
247
+ },
248
+ };
249
+ const listMethods = {
250
+ append: (l, a) => {
251
+ arity('append', a, 1);
252
+ l.push(a[0]);
253
+ return null;
254
+ },
255
+ extend: (l, a) => {
256
+ arity('extend', a, 1);
257
+ l.push(...iterate(a[0]));
258
+ return null;
259
+ },
260
+ insert: (l, a) => {
261
+ arity('insert', a, 2);
262
+ l.splice(needInt('insert', a[0]), 0, a[1]);
263
+ return null;
264
+ },
265
+ pop: (l, a) => {
266
+ arity('pop', a, 0, 1);
267
+ if (l.length === 0)
268
+ throw runtimeError('pop() from an empty list');
269
+ const i = a.length ? needInt('pop', a[0]) : -1;
270
+ const idx = i < 0 ? l.length + i : i;
271
+ if (idx < 0 || idx >= l.length)
272
+ throw runtimeError('pop() index out of range');
273
+ return l.splice(idx, 1)[0];
274
+ },
275
+ remove: (l, a) => {
276
+ arity('remove', a, 1);
277
+ const i = l.findIndex((x) => equals(x, a[0]));
278
+ if (i < 0)
279
+ throw runtimeError('remove(): value not in list');
280
+ l.splice(i, 1);
281
+ return null;
282
+ },
283
+ index: (l, a) => {
284
+ arity('index', a, 1);
285
+ return l.findIndex((x) => equals(x, a[0]));
286
+ },
287
+ count: (l, a) => {
288
+ arity('count', a, 1);
289
+ return l.filter((x) => equals(x, a[0])).length;
290
+ },
291
+ reverse: (l) => {
292
+ l.reverse();
293
+ return null;
294
+ },
295
+ sort: (l, a) => {
296
+ arity('sort', a, 0, 1);
297
+ l.sort(compareValues);
298
+ if (a.length && truthy(a[0]))
299
+ l.reverse();
300
+ return null;
301
+ },
302
+ copy: (l) => [...l],
303
+ clear: (l) => {
304
+ l.length = 0;
305
+ return null;
306
+ },
307
+ };
308
+ const dictMethods = {
309
+ keys: (d) => Object.keys(d),
310
+ values: (d) => Object.values(d),
311
+ items: (d) => Object.entries(d),
312
+ get: (d, a) => {
313
+ arity('get', a, 1, 2);
314
+ const k = needString('get', a[0]);
315
+ return hasKey(d, k) ? d[k] : (a[1] ?? null);
316
+ },
317
+ pop: (d, a) => {
318
+ arity('pop', a, 1, 2);
319
+ const k = needString('pop', a[0]);
320
+ if (!hasKey(d, k)) {
321
+ if (a.length === 2)
322
+ return a[1];
323
+ throw runtimeError(`key '${k}' not found`);
324
+ }
325
+ const v = d[k];
326
+ delete d[k];
327
+ return v;
328
+ },
329
+ update: (d, a) => {
330
+ arity('update', a, 1);
331
+ if (!isDict(a[0]))
332
+ throw runtimeError(`update() expects a dict, got ${typeName(a[0])}`);
333
+ for (const [k, v] of Object.entries(a[0]))
334
+ setKey(d, k, v);
335
+ return null;
336
+ },
337
+ copy: (d) => newDict(Object.entries(d)),
338
+ clear: (d) => {
339
+ for (const k of Object.keys(d))
340
+ delete d[k];
341
+ return null;
342
+ },
343
+ };
344
+ export function callMethod(self, name, args) {
345
+ const table = typeof self === 'string' ? stringMethods : isList(self) ? listMethods : isDict(self) ? dictMethods : undefined;
346
+ if (!table || !Object.prototype.hasOwnProperty.call(table, name)) {
347
+ throw runtimeError(`${typeName(self)} has no method '${name}'`);
348
+ }
349
+ return table[name](self, args);
350
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { parseArgs } from 'node:util';
5
+ import { CopilotBackend } from './copilot.js';
6
+ import { AuthError, login, logout } from './auth.js';
7
+ import { SplSyntaxError } from './errors.js';
8
+ import { Logger } from './logger.js';
9
+ import { parse } from './parser.js';
10
+ import { run } from './interpreter.js';
11
+ import { JsonFileStore, MemoryStore, loadStoreModule } from './store.js';
12
+ import { toJson } from './values.js';
13
+ const HELP = `spl - Semantic Prompt Language interpreter
14
+
15
+ Usage: spl <script.spl> [options]
16
+ spl login authenticate with GitHub Copilot (device flow in the browser)
17
+ spl logout remove the credentials saved by 'spl login'
18
+
19
+ Options:
20
+ --log-level <level> error | info | debug (default: info). Logs go to stderr.
21
+ --silent suppress all logs; only the result is printed
22
+ --model <id> model override (takes precedence over @{model=...})
23
+ --store <file.json> persist top-level variables in a JSON file
24
+ --store-module <path> custom variable store: module exporting createStore() -> { load(), save(vars) }
25
+ --timeout <seconds> per LLM call timeout (default: 300)
26
+ --cwd <dir> base directory for relative paths and restrictions (default: current directory)
27
+ --check parse the script and exit
28
+ -v, --version print version
29
+ -h, --help print this help
30
+
31
+ The script result (the value of 'success' or 'return', or the failure payload) is written to stdout.
32
+ Exit codes: 0 success, 1 uncaught script failure, 2 syntax or usage error.`;
33
+ function version() {
34
+ const pkg = new URL('../package.json', import.meta.url);
35
+ return JSON.parse(fs.readFileSync(pkg, 'utf8')).version;
36
+ }
37
+ function usageError(msg) {
38
+ process.stderr.write(`spl: ${msg}\nTry 'spl --help'.\n`);
39
+ process.exit(2);
40
+ }
41
+ async function loginCommand() {
42
+ try {
43
+ const status = await login((line) => process.stdout.write(line + '\n'));
44
+ process.stdout.write(`Authenticated with GitHub Copilot${status.login ? ` as ${status.login}` : ''}.\n`);
45
+ return 0;
46
+ }
47
+ catch (e) {
48
+ if (!(e instanceof AuthError))
49
+ throw e;
50
+ process.stderr.write(`spl: login failed: ${e.message}\n`);
51
+ return 1;
52
+ }
53
+ }
54
+ function logoutCommand() {
55
+ try {
56
+ process.stdout.write(logout() ? 'Saved credentials removed.\n' : 'No saved credentials.\n');
57
+ return 0;
58
+ }
59
+ catch (e) {
60
+ if (!(e instanceof AuthError))
61
+ throw e;
62
+ process.stderr.write(`spl: ${e.message}\n`);
63
+ return 1;
64
+ }
65
+ }
66
+ async function main() {
67
+ let parsed;
68
+ try {
69
+ parsed = parseArgs({
70
+ allowPositionals: true,
71
+ options: {
72
+ 'log-level': { type: 'string' },
73
+ silent: { type: 'boolean' },
74
+ model: { type: 'string' },
75
+ store: { type: 'string' },
76
+ 'store-module': { type: 'string' },
77
+ timeout: { type: 'string' },
78
+ cwd: { type: 'string' },
79
+ check: { type: 'boolean' },
80
+ version: { type: 'boolean', short: 'v' },
81
+ help: { type: 'boolean', short: 'h' },
82
+ },
83
+ });
84
+ }
85
+ catch (e) {
86
+ return usageError(e.message);
87
+ }
88
+ const { values, positionals } = parsed;
89
+ if (values.help) {
90
+ process.stdout.write(HELP + '\n');
91
+ return 0;
92
+ }
93
+ if (values.version) {
94
+ process.stdout.write(version() + '\n');
95
+ return 0;
96
+ }
97
+ if (positionals[0] === 'login' || positionals[0] === 'logout') {
98
+ if (positionals.length !== 1)
99
+ usageError(`'${positionals[0]}' takes no arguments`);
100
+ return positionals[0] === 'login' ? loginCommand() : logoutCommand();
101
+ }
102
+ if (positionals.length !== 1)
103
+ usageError('expected exactly one script file');
104
+ const level = values['log-level'] ?? 'info';
105
+ if (!['error', 'info', 'debug'].includes(level))
106
+ usageError(`invalid --log-level '${level}'`);
107
+ const logger = new Logger(values.silent ? 'silent' : level);
108
+ const timeoutSec = Number(values.timeout ?? 300);
109
+ if (!Number.isFinite(timeoutSec) || timeoutSec <= 0)
110
+ usageError('--timeout must be a positive number');
111
+ if (values.store && values['store-module'])
112
+ usageError('--store and --store-module are mutually exclusive');
113
+ const cwd = path.resolve(values.cwd ?? process.cwd());
114
+ const file = positionals[0];
115
+ let source;
116
+ try {
117
+ source = fs.readFileSync(file, 'utf8');
118
+ }
119
+ catch (e) {
120
+ return usageError(`cannot read '${file}': ${e.message}`);
121
+ }
122
+ let program;
123
+ try {
124
+ program = parse(source);
125
+ }
126
+ catch (e) {
127
+ if (e instanceof SplSyntaxError) {
128
+ process.stderr.write(e.format(file) + '\n');
129
+ return 2;
130
+ }
131
+ throw e;
132
+ }
133
+ if (values.check)
134
+ return 0;
135
+ let store;
136
+ try {
137
+ store = values.store ? new JsonFileStore(values.store) : values['store-module'] ? await loadStoreModule(values['store-module']) : new MemoryStore();
138
+ }
139
+ catch (e) {
140
+ return usageError(e.message);
141
+ }
142
+ const backend = new CopilotBackend({ logger, cwd, timeoutMs: timeoutSec * 1000 });
143
+ try {
144
+ const result = await run(program, { backend, logger, store, cwd, model: values.model });
145
+ if (result.status === 'failure') {
146
+ process.stdout.write(JSON.stringify(result.failure, null, 4) + '\n');
147
+ return 1;
148
+ }
149
+ if (result.hasValue) {
150
+ const v = result.value;
151
+ process.stdout.write((typeof v === 'string' ? v : JSON.stringify(toJson(v), null, 2)) + '\n');
152
+ }
153
+ return 0;
154
+ }
155
+ finally {
156
+ await backend.close();
157
+ }
158
+ }
159
+ main().then((code) => process.exit(code), (e) => {
160
+ process.stderr.write(`spl: internal error: ${e.stack ?? e}\n`);
161
+ process.exit(70);
162
+ });
@@ -0,0 +1,29 @@
1
+ import type { PermissionRequest, PermissionRequestResult } from '@github/copilot-sdk';
2
+ import { Failure } from './errors.js';
3
+ import { type LlmBackend, type LlmRequest, type LlmResponse } from './llm.js';
4
+ import type { Logger } from './logger.js';
5
+ import type { Restrictions } from './restrict.js';
6
+ export interface CopilotBackendOptions {
7
+ logger: Logger;
8
+ cwd: string;
9
+ timeoutMs: number;
10
+ }
11
+ export declare class CopilotBackend implements LlmBackend {
12
+ private readonly opts;
13
+ private client?;
14
+ private models?;
15
+ constructor(opts: CopilotBackendOptions);
16
+ private getClient;
17
+ private listModels;
18
+ /** Maps the `think` level to the SDK reasoning effort, warning when the model cannot honor it. */
19
+ private resolveEffort;
20
+ /** Fails on a model id the runtime does not offer; it would otherwise silently fall back to the default model. */
21
+ private checkModel;
22
+ call(req: LlmRequest): Promise<LlmResponse>;
23
+ close(): Promise<void>;
24
+ }
25
+ /**
26
+ * Hard enforcement at the permission layer. Only the five runtime tools are exposed to the model, so any
27
+ * other request is denied; requests that would violate a restriction are additionally reported as violations.
28
+ */
29
+ export declare function decidePermission(r: PermissionRequest, restrictions: Restrictions, block: (f: Failure) => void): PermissionRequestResult;