@bardsballad/cadence 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/dist/index.d.ts +2 -0
- package/dist/index.js +39 -0
- package/dist/lexer/tokenize.d.ts +2 -0
- package/dist/lexer/tokenize.js +161 -0
- package/dist/parser/ast.d.ts +52 -0
- package/dist/parser/ast.js +2 -0
- package/dist/parser/parser.d.ts +20 -0
- package/dist/parser/parser.js +202 -0
- package/dist/runtime/context.d.ts +2 -0
- package/dist/runtime/context.js +14 -0
- package/dist/runtime/evaluator.d.ts +5 -0
- package/dist/runtime/evaluator.js +86 -0
- package/dist/runtime/functions.d.ts +3 -0
- package/dist/runtime/functions.js +69 -0
- package/dist/runtime/runCadence.d.ts +1 -0
- package/dist/runtime/runCadence.js +11 -0
- package/dist/types/statements.d.ts +1 -0
- package/dist/types/statements.js +2 -0
- package/dist/types/tokens.d.ts +6 -0
- package/dist/types/tokens.js +2 -0
- package/package.json +25 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CadenceHelpers = exports.runCadence = void 0;
|
|
37
|
+
var runCadence_1 = require("./runtime/runCadence");
|
|
38
|
+
Object.defineProperty(exports, "runCadence", { enumerable: true, get: function () { return runCadence_1.runCadence; } });
|
|
39
|
+
exports.CadenceHelpers = __importStar(require("./runtime/functions"));
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.tokenize = tokenize;
|
|
4
|
+
const isDigit = (c) => c >= '0' && c <= '9';
|
|
5
|
+
const isAlpha = (c) => /[A-Za-z_]/.test(c);
|
|
6
|
+
const isAlnum = (c) => /[A-Za-z0-9_]/.test(c);
|
|
7
|
+
function tokenize(source) {
|
|
8
|
+
const tokens = [];
|
|
9
|
+
let i = 0;
|
|
10
|
+
const push = (type, lexeme, position) => {
|
|
11
|
+
tokens.push({ type, lexeme, position });
|
|
12
|
+
};
|
|
13
|
+
while (i < source.length) {
|
|
14
|
+
const start = i;
|
|
15
|
+
const ch = source[i];
|
|
16
|
+
// whitespace and newlines
|
|
17
|
+
if (ch === ' ' || ch === '\t' || ch === '\r' || ch === '\n') {
|
|
18
|
+
i++;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
// numbers (integer and decimals)
|
|
22
|
+
if (isDigit(ch) || (ch === '.' && isDigit(source[i + 1]))) {
|
|
23
|
+
let num = '';
|
|
24
|
+
let dotSeen = false;
|
|
25
|
+
while (i < source.length) {
|
|
26
|
+
const c = source[i];
|
|
27
|
+
if (isDigit(c)) {
|
|
28
|
+
num += c;
|
|
29
|
+
i++;
|
|
30
|
+
}
|
|
31
|
+
else if (c === '.' && !dotSeen && isDigit(source[i + 1])) {
|
|
32
|
+
dotSeen = true;
|
|
33
|
+
num += c;
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
push('NUMBER', num, start);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
// strings: only support simple single or double quotes
|
|
44
|
+
if (ch === '"' || ch === '\'') {
|
|
45
|
+
const quote = ch;
|
|
46
|
+
i++; // skip opening quote
|
|
47
|
+
let str = '';
|
|
48
|
+
while (i < source.length && source[i] !== quote) {
|
|
49
|
+
const c = source[i];
|
|
50
|
+
if (c === '\\') {
|
|
51
|
+
const next = source[i + 1];
|
|
52
|
+
if (next === '"' || next === '\'' || next === '\\') {
|
|
53
|
+
str += next;
|
|
54
|
+
i += 2;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
str += c;
|
|
59
|
+
i++;
|
|
60
|
+
}
|
|
61
|
+
if (source[i] !== quote) {
|
|
62
|
+
throw new Error(`Unterminated string at ${start}`);
|
|
63
|
+
}
|
|
64
|
+
i++; // closing quote
|
|
65
|
+
tokens.push({ type: 'STRING', lexeme: str, position: start });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// identifiers
|
|
69
|
+
if (isAlpha(ch)) {
|
|
70
|
+
let ident = '';
|
|
71
|
+
while (i < source.length && isAlnum(source[i])) {
|
|
72
|
+
ident += source[i++];
|
|
73
|
+
}
|
|
74
|
+
push('IDENT', ident, start);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// multi-char operators
|
|
78
|
+
const two = source.slice(i, i + 2);
|
|
79
|
+
if (two === '<=' || two === '>=') {
|
|
80
|
+
push(two === '<=' ? 'LTE' : 'GTE', two, start);
|
|
81
|
+
i += 2;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (two === '==' || two === '!=') {
|
|
85
|
+
push(two === '==' ? 'EQ' : 'NEQ', two, start);
|
|
86
|
+
i += 2;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (two === '&&' || two === '||') {
|
|
90
|
+
push(two === '&&' ? 'AND' : 'OR', two, start);
|
|
91
|
+
i += 2;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// single-char tokens
|
|
95
|
+
switch (ch) {
|
|
96
|
+
case '+':
|
|
97
|
+
push('PLUS', ch, start);
|
|
98
|
+
i++;
|
|
99
|
+
continue;
|
|
100
|
+
case '-':
|
|
101
|
+
push('MINUS', ch, start);
|
|
102
|
+
i++;
|
|
103
|
+
continue;
|
|
104
|
+
case '*':
|
|
105
|
+
push('STAR', ch, start);
|
|
106
|
+
i++;
|
|
107
|
+
continue;
|
|
108
|
+
case '/':
|
|
109
|
+
push('SLASH', ch, start);
|
|
110
|
+
i++;
|
|
111
|
+
continue;
|
|
112
|
+
case '^':
|
|
113
|
+
push('CARET', ch, start);
|
|
114
|
+
i++;
|
|
115
|
+
continue;
|
|
116
|
+
case '<':
|
|
117
|
+
push('LT', ch, start);
|
|
118
|
+
i++;
|
|
119
|
+
continue;
|
|
120
|
+
case '>':
|
|
121
|
+
push('GT', ch, start);
|
|
122
|
+
i++;
|
|
123
|
+
continue;
|
|
124
|
+
case '(':
|
|
125
|
+
push('LPAREN', ch, start);
|
|
126
|
+
i++;
|
|
127
|
+
continue;
|
|
128
|
+
case ')':
|
|
129
|
+
push('RPAREN', ch, start);
|
|
130
|
+
i++;
|
|
131
|
+
continue;
|
|
132
|
+
case '[':
|
|
133
|
+
push('LBRACK', ch, start);
|
|
134
|
+
i++;
|
|
135
|
+
continue;
|
|
136
|
+
case ']':
|
|
137
|
+
push('RBRACK', ch, start);
|
|
138
|
+
i++;
|
|
139
|
+
continue;
|
|
140
|
+
case ',':
|
|
141
|
+
push('COMMA', ch, start);
|
|
142
|
+
i++;
|
|
143
|
+
continue;
|
|
144
|
+
case ';':
|
|
145
|
+
push('SEMICOLON', ch, start);
|
|
146
|
+
i++;
|
|
147
|
+
continue;
|
|
148
|
+
case '?':
|
|
149
|
+
push('QUESTION', ch, start);
|
|
150
|
+
i++;
|
|
151
|
+
continue;
|
|
152
|
+
case ':':
|
|
153
|
+
push('COLON', ch, start);
|
|
154
|
+
i++;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
throw new Error(`Unexpected character '${ch}' at ${i}`);
|
|
158
|
+
}
|
|
159
|
+
push('EOF', '', i);
|
|
160
|
+
return tokens;
|
|
161
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type NumberLiteral = {
|
|
2
|
+
type: 'NumberLiteral';
|
|
3
|
+
value: number;
|
|
4
|
+
};
|
|
5
|
+
export type StringLiteral = {
|
|
6
|
+
type: 'StringLiteral';
|
|
7
|
+
value: string;
|
|
8
|
+
};
|
|
9
|
+
export type Identifier = {
|
|
10
|
+
type: 'Identifier';
|
|
11
|
+
name: string;
|
|
12
|
+
};
|
|
13
|
+
export type BooleanLiteral = {
|
|
14
|
+
type: 'BooleanLiteral';
|
|
15
|
+
value: boolean;
|
|
16
|
+
};
|
|
17
|
+
export type UnaryExpression = {
|
|
18
|
+
type: 'UnaryExpression';
|
|
19
|
+
operator: '-' | '+';
|
|
20
|
+
argument: Expression;
|
|
21
|
+
};
|
|
22
|
+
export type BinaryExpression = {
|
|
23
|
+
type: 'BinaryExpression';
|
|
24
|
+
operator: '+' | '-' | '*' | '/' | '^' | '<' | '<=' | '>' | '>=' | '==' | '!=' | '&&' | '||';
|
|
25
|
+
left: Expression;
|
|
26
|
+
right: Expression;
|
|
27
|
+
};
|
|
28
|
+
export type ConditionalExpression = {
|
|
29
|
+
type: 'ConditionalExpression';
|
|
30
|
+
test: Expression;
|
|
31
|
+
consequent: Expression;
|
|
32
|
+
alternate: Expression;
|
|
33
|
+
};
|
|
34
|
+
export type CallExpression = {
|
|
35
|
+
type: 'CallExpression';
|
|
36
|
+
callee: Identifier;
|
|
37
|
+
args: Expression[];
|
|
38
|
+
};
|
|
39
|
+
export type Grouping = {
|
|
40
|
+
type: 'Grouping';
|
|
41
|
+
expression: Expression;
|
|
42
|
+
};
|
|
43
|
+
export type Expression = NumberLiteral | StringLiteral | BooleanLiteral | Identifier | UnaryExpression | BinaryExpression | ConditionalExpression | CallExpression | Grouping;
|
|
44
|
+
export type Statement = {
|
|
45
|
+
type: 'Statement';
|
|
46
|
+
expression: Expression;
|
|
47
|
+
assignTo?: string;
|
|
48
|
+
};
|
|
49
|
+
export type Program = {
|
|
50
|
+
type: 'Program';
|
|
51
|
+
body: Statement[];
|
|
52
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Program } from './ast';
|
|
2
|
+
export declare class Parser {
|
|
3
|
+
private tokens;
|
|
4
|
+
private pos;
|
|
5
|
+
constructor(source: string);
|
|
6
|
+
private peek;
|
|
7
|
+
private advance;
|
|
8
|
+
private expect;
|
|
9
|
+
parseProgram(): Program;
|
|
10
|
+
private parseStatement;
|
|
11
|
+
private parseExpression;
|
|
12
|
+
private nudFor;
|
|
13
|
+
private ledFor;
|
|
14
|
+
private lbpOf;
|
|
15
|
+
private prefixBindingPower;
|
|
16
|
+
private infixRightBindingPower;
|
|
17
|
+
private tokenToOperator;
|
|
18
|
+
private operatorToTokenType;
|
|
19
|
+
}
|
|
20
|
+
export declare function parse(source: string): Program;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Parser = void 0;
|
|
4
|
+
exports.parse = parse;
|
|
5
|
+
const tokenize_1 = require("../lexer/tokenize");
|
|
6
|
+
class Parser {
|
|
7
|
+
constructor(source) {
|
|
8
|
+
this.pos = 0;
|
|
9
|
+
this.tokens = (0, tokenize_1.tokenize)(source);
|
|
10
|
+
}
|
|
11
|
+
peek() {
|
|
12
|
+
return this.tokens[this.pos];
|
|
13
|
+
}
|
|
14
|
+
advance() {
|
|
15
|
+
return this.tokens[this.pos++];
|
|
16
|
+
}
|
|
17
|
+
expect(type, message) {
|
|
18
|
+
const t = this.peek();
|
|
19
|
+
if (t.type !== type)
|
|
20
|
+
throw new Error(message + ` at ${t.position}`);
|
|
21
|
+
return this.advance();
|
|
22
|
+
}
|
|
23
|
+
parseProgram() {
|
|
24
|
+
const body = [];
|
|
25
|
+
while (this.peek().type !== 'EOF') {
|
|
26
|
+
// allow stray semicolons
|
|
27
|
+
if (this.peek().type === 'SEMICOLON') {
|
|
28
|
+
this.advance();
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const stmt = this.parseStatement();
|
|
32
|
+
body.push(stmt);
|
|
33
|
+
if (this.peek().type === 'SEMICOLON')
|
|
34
|
+
this.advance();
|
|
35
|
+
}
|
|
36
|
+
return { type: 'Program', body };
|
|
37
|
+
}
|
|
38
|
+
parseStatement() {
|
|
39
|
+
const expr = this.parseExpression(0);
|
|
40
|
+
let assignTo;
|
|
41
|
+
if (this.peek().type === 'LBRACK') {
|
|
42
|
+
this.advance();
|
|
43
|
+
const ident = this.expect('IDENT', 'Expected identifier in brackets').lexeme;
|
|
44
|
+
this.expect('RBRACK', 'Expected closing ] after identifier');
|
|
45
|
+
assignTo = ident;
|
|
46
|
+
}
|
|
47
|
+
return { type: 'Statement', expression: expr, assignTo };
|
|
48
|
+
}
|
|
49
|
+
parseExpression(rbp) {
|
|
50
|
+
let t = this.advance();
|
|
51
|
+
let left = this.nudFor(t);
|
|
52
|
+
while (rbp < this.lbpOf(this.peek())) {
|
|
53
|
+
t = this.advance();
|
|
54
|
+
left = this.ledFor(t, left);
|
|
55
|
+
}
|
|
56
|
+
return left;
|
|
57
|
+
}
|
|
58
|
+
nudFor(token) {
|
|
59
|
+
switch (token.type) {
|
|
60
|
+
case 'NUMBER':
|
|
61
|
+
return { type: 'NumberLiteral', value: Number(token.lexeme) };
|
|
62
|
+
case 'STRING':
|
|
63
|
+
return { type: 'StringLiteral', value: token.lexeme };
|
|
64
|
+
case 'IDENT': {
|
|
65
|
+
// function call or identifier
|
|
66
|
+
if (this.peek().type === 'LPAREN') {
|
|
67
|
+
const name = token.lexeme;
|
|
68
|
+
this.advance(); // (
|
|
69
|
+
const args = [];
|
|
70
|
+
if (this.peek().type !== 'RPAREN') {
|
|
71
|
+
// parse comma-separated args
|
|
72
|
+
args.push(this.parseExpression(0));
|
|
73
|
+
while (this.peek().type === 'COMMA') {
|
|
74
|
+
this.advance();
|
|
75
|
+
args.push(this.parseExpression(0));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
this.expect('RPAREN', 'Expected ) after arguments');
|
|
79
|
+
const callee = { type: 'Identifier', name: name };
|
|
80
|
+
const call = { type: 'CallExpression', callee, args };
|
|
81
|
+
return call;
|
|
82
|
+
}
|
|
83
|
+
if (token.lexeme === 'true' || token.lexeme === 'false') {
|
|
84
|
+
return { type: 'BooleanLiteral', value: token.lexeme === 'true' };
|
|
85
|
+
}
|
|
86
|
+
return { type: 'Identifier', name: token.lexeme };
|
|
87
|
+
}
|
|
88
|
+
case 'LPAREN': {
|
|
89
|
+
const expr = this.parseExpression(0);
|
|
90
|
+
this.expect('RPAREN', 'Expected ) after expression');
|
|
91
|
+
return { type: 'Grouping', expression: expr };
|
|
92
|
+
}
|
|
93
|
+
case 'MINUS':
|
|
94
|
+
case 'PLUS': {
|
|
95
|
+
const op = token.type === 'MINUS' ? '-' : '+';
|
|
96
|
+
const argument = this.parseExpression(this.prefixBindingPower(op));
|
|
97
|
+
return { type: 'UnaryExpression', operator: op, argument };
|
|
98
|
+
}
|
|
99
|
+
default:
|
|
100
|
+
throw new Error(`Unexpected token ${token.type} at ${token.position}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
ledFor(token, left) {
|
|
104
|
+
switch (token.type) {
|
|
105
|
+
case 'PLUS':
|
|
106
|
+
case 'MINUS':
|
|
107
|
+
case 'STAR':
|
|
108
|
+
case 'SLASH':
|
|
109
|
+
case 'CARET':
|
|
110
|
+
case 'LT':
|
|
111
|
+
case 'LTE':
|
|
112
|
+
case 'GT':
|
|
113
|
+
case 'GTE':
|
|
114
|
+
case 'EQ':
|
|
115
|
+
case 'NEQ':
|
|
116
|
+
case 'AND':
|
|
117
|
+
case 'OR': {
|
|
118
|
+
const op = this.tokenToOperator(token.type);
|
|
119
|
+
const rbp = this.infixRightBindingPower(op);
|
|
120
|
+
const right = this.parseExpression(rbp);
|
|
121
|
+
return { type: 'BinaryExpression', operator: op, left, right };
|
|
122
|
+
}
|
|
123
|
+
case 'QUESTION': {
|
|
124
|
+
// parse ternary conditional: left ? x : y
|
|
125
|
+
const consequent = this.parseExpression(0);
|
|
126
|
+
this.expect('COLON', 'Expected : in conditional expression');
|
|
127
|
+
const alternate = this.parseExpression(2); // bind weaker than most ops
|
|
128
|
+
return { type: 'ConditionalExpression', test: left, consequent, alternate };
|
|
129
|
+
}
|
|
130
|
+
default:
|
|
131
|
+
throw new Error(`Unexpected token in led: ${token.type} at ${token.position}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
lbpOf(token) {
|
|
135
|
+
switch (token.type) {
|
|
136
|
+
case 'CARET': return 50; // right-associative handled via rbp
|
|
137
|
+
case 'STAR':
|
|
138
|
+
case 'SLASH': return 40;
|
|
139
|
+
case 'PLUS':
|
|
140
|
+
case 'MINUS': return 30;
|
|
141
|
+
case 'LT':
|
|
142
|
+
case 'LTE':
|
|
143
|
+
case 'GT':
|
|
144
|
+
case 'GTE': return 20;
|
|
145
|
+
case 'EQ':
|
|
146
|
+
case 'NEQ': return 15;
|
|
147
|
+
case 'AND': return 10;
|
|
148
|
+
case 'OR': return 9;
|
|
149
|
+
case 'QUESTION': return 2; // ternary
|
|
150
|
+
default: return 0;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
prefixBindingPower(op) {
|
|
154
|
+
return 60; // higher than any infix
|
|
155
|
+
}
|
|
156
|
+
infixRightBindingPower(op) {
|
|
157
|
+
switch (op) {
|
|
158
|
+
case '^': return 49; // right-assoc: less than lbp 50
|
|
159
|
+
default: return this.lbpOf({ type: this.operatorToTokenType(op), lexeme: '', position: 0 });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
tokenToOperator(type) {
|
|
163
|
+
switch (type) {
|
|
164
|
+
case 'PLUS': return '+';
|
|
165
|
+
case 'MINUS': return '-';
|
|
166
|
+
case 'STAR': return '*';
|
|
167
|
+
case 'SLASH': return '/';
|
|
168
|
+
case 'CARET': return '^';
|
|
169
|
+
case 'LT': return '<';
|
|
170
|
+
case 'LTE': return '<=';
|
|
171
|
+
case 'GT': return '>';
|
|
172
|
+
case 'GTE': return '>=';
|
|
173
|
+
case 'EQ': return '==';
|
|
174
|
+
case 'NEQ': return '!=';
|
|
175
|
+
case 'AND': return '&&';
|
|
176
|
+
case 'OR': return '||';
|
|
177
|
+
default: throw new Error('Unknown operator token ' + type);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
operatorToTokenType(op) {
|
|
181
|
+
switch (op) {
|
|
182
|
+
case '+': return 'PLUS';
|
|
183
|
+
case '-': return 'MINUS';
|
|
184
|
+
case '*': return 'STAR';
|
|
185
|
+
case '/': return 'SLASH';
|
|
186
|
+
case '^': return 'CARET';
|
|
187
|
+
case '<': return 'LT';
|
|
188
|
+
case '<=': return 'LTE';
|
|
189
|
+
case '>': return 'GT';
|
|
190
|
+
case '>=': return 'GTE';
|
|
191
|
+
case '==': return 'EQ';
|
|
192
|
+
case '!=': return 'NEQ';
|
|
193
|
+
case '&&': return 'AND';
|
|
194
|
+
case '||': return 'OR';
|
|
195
|
+
default: throw new Error('Unknown operator ' + op);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
exports.Parser = Parser;
|
|
200
|
+
function parse(source) {
|
|
201
|
+
return new Parser(source).parseProgram();
|
|
202
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createFrozenContext = createFrozenContext;
|
|
4
|
+
function createFrozenContext(input) {
|
|
5
|
+
return deepFreeze({ ...input });
|
|
6
|
+
}
|
|
7
|
+
function deepFreeze(obj) {
|
|
8
|
+
Object.getOwnPropertyNames(obj).forEach((prop) => {
|
|
9
|
+
const value = obj[prop];
|
|
10
|
+
if (value && typeof value === 'object')
|
|
11
|
+
deepFreeze(value);
|
|
12
|
+
});
|
|
13
|
+
return Object.freeze(obj);
|
|
14
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.evaluate = evaluate;
|
|
4
|
+
const functions_1 = require("./functions");
|
|
5
|
+
function evaluate(program, options) {
|
|
6
|
+
const scope = Object.create(null);
|
|
7
|
+
let last;
|
|
8
|
+
for (const stmt of program.body) {
|
|
9
|
+
last = evalExpression(stmt.expression, scope, options.input);
|
|
10
|
+
if (stmt.assignTo) {
|
|
11
|
+
scope[stmt.assignTo] = last;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return last;
|
|
15
|
+
}
|
|
16
|
+
function evalExpression(expr, scope, input) {
|
|
17
|
+
switch (expr.type) {
|
|
18
|
+
case 'NumberLiteral': return expr.value;
|
|
19
|
+
case 'StringLiteral': return expr.value;
|
|
20
|
+
case 'BooleanLiteral': return expr.value;
|
|
21
|
+
case 'Identifier': {
|
|
22
|
+
if (Object.prototype.hasOwnProperty.call(scope, expr.name))
|
|
23
|
+
return scope[expr.name];
|
|
24
|
+
if (Object.prototype.hasOwnProperty.call(input, expr.name))
|
|
25
|
+
return input[expr.name];
|
|
26
|
+
throw new Error(`Unknown identifier: ${expr.name}`);
|
|
27
|
+
}
|
|
28
|
+
case 'Grouping':
|
|
29
|
+
return evalExpression(expr.expression, scope, input);
|
|
30
|
+
case 'UnaryExpression': {
|
|
31
|
+
const v = evalExpression(expr.argument, scope, input);
|
|
32
|
+
switch (expr.operator) {
|
|
33
|
+
case '+': return toNumber(v);
|
|
34
|
+
case '-': return -toNumber(v);
|
|
35
|
+
}
|
|
36
|
+
throw new Error('Unknown unary operator');
|
|
37
|
+
}
|
|
38
|
+
case 'BinaryExpression': {
|
|
39
|
+
if (expr.operator === '&&') {
|
|
40
|
+
const l = evalExpression(expr.left, scope, input);
|
|
41
|
+
return l ? evalExpression(expr.right, scope, input) : l;
|
|
42
|
+
}
|
|
43
|
+
if (expr.operator === '||') {
|
|
44
|
+
const l = evalExpression(expr.left, scope, input);
|
|
45
|
+
return l ? l : evalExpression(expr.right, scope, input);
|
|
46
|
+
}
|
|
47
|
+
const left = evalExpression(expr.left, scope, input);
|
|
48
|
+
const right = evalExpression(expr.right, scope, input);
|
|
49
|
+
switch (expr.operator) {
|
|
50
|
+
case '+':
|
|
51
|
+
if (typeof left === 'string' || typeof right === 'string')
|
|
52
|
+
return String(left) + String(right);
|
|
53
|
+
return toNumber(left) + toNumber(right);
|
|
54
|
+
case '-': return toNumber(left) - toNumber(right);
|
|
55
|
+
case '*': return toNumber(left) * toNumber(right);
|
|
56
|
+
case '/': return toNumber(left) / toNumber(right);
|
|
57
|
+
case '^': return Math.pow(toNumber(left), toNumber(right));
|
|
58
|
+
case '<': return toNumber(left) < toNumber(right);
|
|
59
|
+
case '<=': return toNumber(left) <= toNumber(right);
|
|
60
|
+
case '>': return toNumber(left) > toNumber(right);
|
|
61
|
+
case '>=': return toNumber(left) >= toNumber(right);
|
|
62
|
+
case '==': return left === right;
|
|
63
|
+
case '!=': return left !== right;
|
|
64
|
+
}
|
|
65
|
+
throw new Error('Unknown binary operator');
|
|
66
|
+
}
|
|
67
|
+
case 'ConditionalExpression': {
|
|
68
|
+
const cond = evalExpression(expr.test, scope, input);
|
|
69
|
+
return cond ? evalExpression(expr.consequent, scope, input) : evalExpression(expr.alternate, scope, input);
|
|
70
|
+
}
|
|
71
|
+
case 'CallExpression': {
|
|
72
|
+
const fn = (0, functions_1.getHelper)(expr.callee.name);
|
|
73
|
+
if (expr.args.length !== 1)
|
|
74
|
+
throw new Error(`Function ${expr.callee.name} expects 1 argument`);
|
|
75
|
+
const argVal = evalExpression(expr.args[0], scope, input);
|
|
76
|
+
return fn(argVal);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function toNumber(v) {
|
|
81
|
+
if (typeof v === 'number')
|
|
82
|
+
return v;
|
|
83
|
+
if (typeof v === 'string' && v.trim() !== '' && !isNaN(Number(v)))
|
|
84
|
+
return Number(v);
|
|
85
|
+
throw new Error('Expected number');
|
|
86
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.helpers = void 0;
|
|
4
|
+
exports.getHelper = getHelper;
|
|
5
|
+
function assertArray(name, v) {
|
|
6
|
+
if (!Array.isArray(v))
|
|
7
|
+
throw new Error(`${name} expects array`);
|
|
8
|
+
}
|
|
9
|
+
function assertNumber(name, v) {
|
|
10
|
+
if (typeof v !== 'number' || Number.isNaN(v))
|
|
11
|
+
throw new Error(`${name} expects number`);
|
|
12
|
+
}
|
|
13
|
+
exports.helpers = {
|
|
14
|
+
sum(arg) {
|
|
15
|
+
assertArray('sum', arg);
|
|
16
|
+
return arg.reduce((a, b) => a + (typeof b === 'number' ? b : 0), 0);
|
|
17
|
+
},
|
|
18
|
+
count(arg) {
|
|
19
|
+
assertArray('count', arg);
|
|
20
|
+
return arg.length;
|
|
21
|
+
},
|
|
22
|
+
min(arg) {
|
|
23
|
+
assertArray('min', arg);
|
|
24
|
+
if (arg.length === 0)
|
|
25
|
+
return Infinity;
|
|
26
|
+
return arg.reduce((m, v) => (typeof v === 'number' && v < m ? v : m), Infinity);
|
|
27
|
+
},
|
|
28
|
+
max(arg) {
|
|
29
|
+
assertArray('max', arg);
|
|
30
|
+
if (arg.length === 0)
|
|
31
|
+
return -Infinity;
|
|
32
|
+
return arg.reduce((m, v) => (typeof v === 'number' && v > m ? v : m), -Infinity);
|
|
33
|
+
},
|
|
34
|
+
avg(arg) {
|
|
35
|
+
assertArray('avg', arg);
|
|
36
|
+
if (arg.length === 0)
|
|
37
|
+
return 0;
|
|
38
|
+
const nums = arg.filter((v) => typeof v === 'number');
|
|
39
|
+
if (nums.length === 0)
|
|
40
|
+
return 0;
|
|
41
|
+
return nums.reduce((a, b) => a + b, 0) / nums.length;
|
|
42
|
+
},
|
|
43
|
+
any(arg) {
|
|
44
|
+
assertArray('any', arg);
|
|
45
|
+
return arg.some((v) => Boolean(v));
|
|
46
|
+
},
|
|
47
|
+
all(arg) {
|
|
48
|
+
assertArray('all', arg);
|
|
49
|
+
return arg.every((v) => Boolean(v));
|
|
50
|
+
},
|
|
51
|
+
floor(arg) {
|
|
52
|
+
assertNumber('floor', arg);
|
|
53
|
+
return Math.floor(arg);
|
|
54
|
+
},
|
|
55
|
+
ceil(arg) {
|
|
56
|
+
assertNumber('ceil', arg);
|
|
57
|
+
return Math.ceil(arg);
|
|
58
|
+
},
|
|
59
|
+
abs(arg) {
|
|
60
|
+
assertNumber('abs', arg);
|
|
61
|
+
return Math.abs(arg);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
function getHelper(name) {
|
|
65
|
+
const fn = exports.helpers[name];
|
|
66
|
+
if (!fn)
|
|
67
|
+
throw new Error(`Unknown function: ${name}`);
|
|
68
|
+
return fn;
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runCadence(program: string, input: Record<string, any>): any;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runCadence = runCadence;
|
|
4
|
+
const parser_1 = require("../parser/parser");
|
|
5
|
+
const context_1 = require("./context");
|
|
6
|
+
const evaluator_1 = require("./evaluator");
|
|
7
|
+
function runCadence(program, input) {
|
|
8
|
+
const ast = (0, parser_1.parse)(program);
|
|
9
|
+
const frozen = (0, context_1.createFrozenContext)(input);
|
|
10
|
+
return (0, evaluator_1.evaluate)(ast, { input: frozen });
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { Statement } from '../parser/ast';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type TokenType = 'NUMBER' | 'STRING' | 'IDENT' | 'LPAREN' | 'RPAREN' | 'LBRACK' | 'RBRACK' | 'COMMA' | 'SEMICOLON' | 'QUESTION' | 'COLON' | 'PLUS' | 'MINUS' | 'STAR' | 'SLASH' | 'CARET' | 'LT' | 'LTE' | 'GT' | 'GTE' | 'EQ' | 'NEQ' | 'AND' | 'OR' | 'EOF';
|
|
2
|
+
export interface Token {
|
|
3
|
+
type: TokenType;
|
|
4
|
+
lexeme: string;
|
|
5
|
+
position: number;
|
|
6
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bardsballad/cadence",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cadence: a safe, deterministic expression engine for user-generated content.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": ["dist"],
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -p tsconfig.json",
|
|
10
|
+
"prepublishOnly": "npm run build",
|
|
11
|
+
"test": "jest"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["expression", "safe", "sandbox", "ttrpg"],
|
|
14
|
+
"author": "",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/jest": "^29.5.12",
|
|
21
|
+
"jest": "^29.7.0",
|
|
22
|
+
"ts-jest": "^29.2.5",
|
|
23
|
+
"typescript": "^5.6.3"
|
|
24
|
+
}
|
|
25
|
+
}
|