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.
- package/README.md +147 -3
- package/dist/ast.d.ts +177 -0
- package/dist/ast.js +1 -0
- package/dist/auth.d.ts +19 -0
- package/dist/auth.js +110 -0
- package/dist/builtins.d.ts +8 -0
- package/dist/builtins.js +350 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +162 -0
- package/dist/copilot.d.ts +29 -0
- package/dist/copilot.js +192 -0
- package/dist/errors.d.ts +31 -0
- package/dist/errors.js +52 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +7 -0
- package/dist/interpreter.d.ts +25 -0
- package/dist/interpreter.js +546 -0
- package/dist/lexer.d.ts +43 -0
- package/dist/lexer.js +208 -0
- package/dist/llm.d.ts +46 -0
- package/dist/llm.js +54 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +27 -0
- package/dist/parser.d.ts +13 -0
- package/dist/parser.js +709 -0
- package/dist/restrict.d.ts +27 -0
- package/dist/restrict.js +158 -0
- package/dist/store.d.ts +20 -0
- package/dist/store.js +52 -0
- package/dist/tools.d.ts +38 -0
- package/dist/tools.js +266 -0
- package/dist/values.d.ts +49 -0
- package/dist/values.js +131 -0
- package/docs/syntax.ebnf +97 -0
- package/package.json +39 -6
package/dist/values.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Failure } from './errors.js';
|
|
2
|
+
export const runtimeError = (msg) => new Failure('runtime_error', msg);
|
|
3
|
+
export function isFunction(v) {
|
|
4
|
+
return typeof v === 'object' && v !== null && '__fn' in v && (v.__fn === 'user' || v.__fn === 'builtin');
|
|
5
|
+
}
|
|
6
|
+
export function isList(v) {
|
|
7
|
+
return Array.isArray(v);
|
|
8
|
+
}
|
|
9
|
+
export function isDict(v) {
|
|
10
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v) && !isFunction(v);
|
|
11
|
+
}
|
|
12
|
+
export function typeName(v) {
|
|
13
|
+
if (v === null)
|
|
14
|
+
return 'None';
|
|
15
|
+
if (typeof v === 'boolean')
|
|
16
|
+
return 'bool';
|
|
17
|
+
if (typeof v === 'number')
|
|
18
|
+
return 'number';
|
|
19
|
+
if (typeof v === 'string')
|
|
20
|
+
return 'str';
|
|
21
|
+
if (Array.isArray(v))
|
|
22
|
+
return 'list';
|
|
23
|
+
if (isFunction(v))
|
|
24
|
+
return 'function';
|
|
25
|
+
return 'dict';
|
|
26
|
+
}
|
|
27
|
+
export function truthy(v) {
|
|
28
|
+
if (v === null || v === false)
|
|
29
|
+
return false;
|
|
30
|
+
if (v === true)
|
|
31
|
+
return true;
|
|
32
|
+
if (typeof v === 'number')
|
|
33
|
+
return v !== 0;
|
|
34
|
+
if (typeof v === 'string')
|
|
35
|
+
return v.length > 0;
|
|
36
|
+
if (Array.isArray(v))
|
|
37
|
+
return v.length > 0;
|
|
38
|
+
if (isFunction(v))
|
|
39
|
+
return true;
|
|
40
|
+
return Object.keys(v).length > 0;
|
|
41
|
+
}
|
|
42
|
+
export function hasKey(o, k) {
|
|
43
|
+
return Object.prototype.hasOwnProperty.call(o, k);
|
|
44
|
+
}
|
|
45
|
+
/** Assigns a dict key without ever touching the prototype chain. */
|
|
46
|
+
export function setKey(o, k, v) {
|
|
47
|
+
Object.defineProperty(o, k, { value: v, writable: true, enumerable: true, configurable: true });
|
|
48
|
+
}
|
|
49
|
+
export function newDict(entries = []) {
|
|
50
|
+
const d = {};
|
|
51
|
+
for (const [k, v] of entries)
|
|
52
|
+
setKey(d, k, v);
|
|
53
|
+
return d;
|
|
54
|
+
}
|
|
55
|
+
function formatNumber(n) {
|
|
56
|
+
if (Number.isNaN(n))
|
|
57
|
+
return 'nan';
|
|
58
|
+
if (n === Infinity)
|
|
59
|
+
return 'inf';
|
|
60
|
+
if (n === -Infinity)
|
|
61
|
+
return '-inf';
|
|
62
|
+
return String(n);
|
|
63
|
+
}
|
|
64
|
+
/** Display form used for string interpolation and str(). */
|
|
65
|
+
export function str(v) {
|
|
66
|
+
if (v === null)
|
|
67
|
+
return 'None';
|
|
68
|
+
if (v === true)
|
|
69
|
+
return 'True';
|
|
70
|
+
if (v === false)
|
|
71
|
+
return 'False';
|
|
72
|
+
if (typeof v === 'number')
|
|
73
|
+
return formatNumber(v);
|
|
74
|
+
if (typeof v === 'string')
|
|
75
|
+
return v;
|
|
76
|
+
return JSON.stringify(toJson(v));
|
|
77
|
+
}
|
|
78
|
+
/** Converts to a plain JSON-compatible value (functions become descriptive strings). */
|
|
79
|
+
export function toJson(v) {
|
|
80
|
+
if (v === null || typeof v === 'boolean' || typeof v === 'string')
|
|
81
|
+
return v;
|
|
82
|
+
if (typeof v === 'number')
|
|
83
|
+
return Number.isFinite(v) ? v : formatNumber(v);
|
|
84
|
+
if (Array.isArray(v))
|
|
85
|
+
return v.map(toJson);
|
|
86
|
+
if (isFunction(v))
|
|
87
|
+
return `<function ${v.name}>`;
|
|
88
|
+
const out = {};
|
|
89
|
+
for (const [k, x] of Object.entries(v))
|
|
90
|
+
Object.defineProperty(out, k, { value: toJson(x), enumerable: true, writable: true, configurable: true });
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
/** Converts parsed JSON into a Value (JSON objects are already safe dicts). */
|
|
94
|
+
export function fromJson(j) {
|
|
95
|
+
if (j === null || typeof j === 'boolean' || typeof j === 'number' || typeof j === 'string')
|
|
96
|
+
return j;
|
|
97
|
+
if (Array.isArray(j))
|
|
98
|
+
return j.map(fromJson);
|
|
99
|
+
if (typeof j === 'object')
|
|
100
|
+
return newDict(Object.entries(j).map(([k, x]) => [k, fromJson(x)]));
|
|
101
|
+
throw runtimeError(`cannot convert ${typeof j} to a value`);
|
|
102
|
+
}
|
|
103
|
+
export function equals(a, b) {
|
|
104
|
+
if (a === b)
|
|
105
|
+
return true;
|
|
106
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
107
|
+
return a.length === b.length && a.every((x, i) => equals(x, b[i]));
|
|
108
|
+
}
|
|
109
|
+
if (isDict(a) && isDict(b)) {
|
|
110
|
+
const ka = Object.keys(a);
|
|
111
|
+
return ka.length === Object.keys(b).length && ka.every((k) => hasKey(b, k) && equals(a[k], b[k]));
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
/** Ordering for numbers, strings and (lexicographically) lists. */
|
|
116
|
+
export function compareValues(a, b) {
|
|
117
|
+
if (typeof a === 'number' && typeof b === 'number')
|
|
118
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
119
|
+
if (typeof a === 'string' && typeof b === 'string')
|
|
120
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
121
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
122
|
+
const n = Math.min(a.length, b.length);
|
|
123
|
+
for (let i = 0; i < n; i++) {
|
|
124
|
+
const c = compareValues(a[i], b[i]);
|
|
125
|
+
if (c !== 0)
|
|
126
|
+
return c;
|
|
127
|
+
}
|
|
128
|
+
return a.length - b.length;
|
|
129
|
+
}
|
|
130
|
+
throw runtimeError(`cannot order ${typeName(a)} and ${typeName(b)}`);
|
|
131
|
+
}
|
package/docs/syntax.ebnf
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
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. *)
|
|
5
|
+
|
|
6
|
+
program = { directive , NL } , { statement , ( NL | ";" ) } ;
|
|
7
|
+
directive = "@" , "{" , directive_item , { "," , directive_item } , "}" ;
|
|
8
|
+
directive_item = "model" , "=" , plain_string ; (* only allowed before the first statement *)
|
|
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 ] ;
|
|
28
|
+
for_stmt = "for" , NAME , { "," , NAME } , "in" , expr , block ;
|
|
29
|
+
while_stmt = "while" , expr , block ;
|
|
30
|
+
def_stmt = "def" , NAME , "(" , [ NAME , { "," , NAME } ] , ")" , block ;
|
|
31
|
+
try_stmt = "try" , block , NL* , "except" , NAME , block ;
|
|
32
|
+
|
|
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 *)
|
|
39
|
+
|
|
40
|
+
(* Expressions, lowest to highest precedence *)
|
|
41
|
+
expr = or_expr , [ "if" , or_expr , "else" , expr ] ;
|
|
42
|
+
or_expr = and_expr , { "or" , and_expr } ;
|
|
43
|
+
and_expr = not_expr , { "and" , not_expr } ;
|
|
44
|
+
not_expr = "not" , not_expr | comparison ;
|
|
45
|
+
comparison = sum , { ( "==" | "!=" | "<" | "<=" | ">" | ">=" | "in" | "not" "in" ) , sum } ;
|
|
46
|
+
sum = term , { ( "+" | "-" ) , term } ;
|
|
47
|
+
term = unary , { ( "*" | "/" | "//" | "%" ) , unary } ;
|
|
48
|
+
unary = ( "-" | "+" ) , unary | llm_call | power ;
|
|
49
|
+
power = postfix , [ "**" , unary ] ;
|
|
50
|
+
postfix = primary , { call | index | slice | member } ;
|
|
51
|
+
call = "(" , [ expr , { "," , expr } ] , ")" ;
|
|
52
|
+
index = "[" , expr , "]" ;
|
|
53
|
+
slice = "[" , [ expr ] , ":" , [ expr ] , "]" ;
|
|
54
|
+
member = "." , NAME ;
|
|
55
|
+
|
|
56
|
+
primary = NUMBER | string | "True" | "False" | "None" | NAME
|
|
57
|
+
| "(" , expr , ")" | list | dict
|
|
58
|
+
| think_call | tool_call ;
|
|
59
|
+
list = "[" , [ expr , { "," , expr } , [ "," ] ] , "]" ;
|
|
60
|
+
dict = "{" , [ expr , ":" , expr , { "," , expr , ":" , expr } , [ "," ] ] , "}" ;
|
|
61
|
+
|
|
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 *)
|
|
85
|
+
|
|
86
|
+
(* Lexical rules *)
|
|
87
|
+
string = [ "r" ] , ( "'" | '"' | "'''" | '"""' ) , { char | escape | interpolation } , closing_quote ;
|
|
88
|
+
plain_string = string without interpolation ;
|
|
89
|
+
escape = "\" , ( "n" | "t" | "r" | "0" | "\" | "'" | '"' | "{" | "}" ) ;
|
|
90
|
+
interpolation = "{" , expr , "}" ; (* "{{" and "}}" are literal braces; raw strings (r'...') have no escapes or interpolation *)
|
|
91
|
+
NUMBER = digit+ , [ "." , digit+ ] , [ ( "e" | "E" ) , [ "+" | "-" ] , digit+ ] ;
|
|
92
|
+
NAME = ( letter | "_" ) , { letter | digit | "_" } ; (* not a reserved word *)
|
|
93
|
+
NL = newline ;
|
|
94
|
+
|
|
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 *)
|
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "spl-lang",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "spl-lang",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Semantic Prompt Language: a Python-like language with LLM calls built into the syntax",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"spl": "dist/cli.js",
|
|
10
|
+
"spl-lang": "dist/cli.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"docs"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
|
|
21
|
+
"prepare": "npm run build",
|
|
22
|
+
"test": "npm run build && vitest run",
|
|
23
|
+
"start": "node dist/cli.js"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"llm",
|
|
27
|
+
"language",
|
|
28
|
+
"copilot"
|
|
29
|
+
],
|
|
30
|
+
"license": "ISC",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@github/copilot-sdk": "^1.0.22"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^26.6.5",
|
|
36
|
+
"typescript": "^7.0.2",
|
|
37
|
+
"vitest": "^5.0.5"
|
|
38
|
+
}
|
|
39
|
+
}
|