gradient-script 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 +515 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +136 -0
- package/dist/dsl/AST.d.ts +123 -0
- package/dist/dsl/AST.js +23 -0
- package/dist/dsl/BuiltIns.d.ts +58 -0
- package/dist/dsl/BuiltIns.js +181 -0
- package/dist/dsl/CSE.d.ts +21 -0
- package/dist/dsl/CSE.js +194 -0
- package/dist/dsl/CodeGen.d.ts +60 -0
- package/dist/dsl/CodeGen.js +474 -0
- package/dist/dsl/Differentiation.d.ts +45 -0
- package/dist/dsl/Differentiation.js +421 -0
- package/dist/dsl/DiscontinuityAnalyzer.d.ts +18 -0
- package/dist/dsl/DiscontinuityAnalyzer.js +75 -0
- package/dist/dsl/Errors.d.ts +22 -0
- package/dist/dsl/Errors.js +49 -0
- package/dist/dsl/Expander.d.ts +13 -0
- package/dist/dsl/Expander.js +220 -0
- package/dist/dsl/ExpressionTransformer.d.ts +54 -0
- package/dist/dsl/ExpressionTransformer.js +102 -0
- package/dist/dsl/ExpressionUtils.d.ts +55 -0
- package/dist/dsl/ExpressionUtils.js +175 -0
- package/dist/dsl/GradientChecker.d.ts +71 -0
- package/dist/dsl/GradientChecker.js +258 -0
- package/dist/dsl/Guards.d.ts +27 -0
- package/dist/dsl/Guards.js +206 -0
- package/dist/dsl/Inliner.d.ts +10 -0
- package/dist/dsl/Inliner.js +40 -0
- package/dist/dsl/Lexer.d.ts +63 -0
- package/dist/dsl/Lexer.js +243 -0
- package/dist/dsl/Parser.d.ts +92 -0
- package/dist/dsl/Parser.js +328 -0
- package/dist/dsl/Simplify.d.ts +17 -0
- package/dist/dsl/Simplify.js +276 -0
- package/dist/dsl/TypeInference.d.ts +39 -0
- package/dist/dsl/TypeInference.js +147 -0
- package/dist/dsl/Types.d.ts +58 -0
- package/dist/dsl/Types.js +114 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +11 -0
- package/dist/symbolic/AST.d.ts +113 -0
- package/dist/symbolic/AST.js +128 -0
- package/dist/symbolic/CodeGen.d.ts +35 -0
- package/dist/symbolic/CodeGen.js +280 -0
- package/dist/symbolic/Parser.d.ts +64 -0
- package/dist/symbolic/Parser.js +329 -0
- package/dist/symbolic/Simplify.d.ts +10 -0
- package/dist/symbolic/Simplify.js +244 -0
- package/dist/symbolic/SymbolicDiff.d.ts +35 -0
- package/dist/symbolic/SymbolicDiff.js +339 -0
- package/package.json +56 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lexer for GradientScript DSL
|
|
3
|
+
* Tokenizes input with support for ∇, ^, **, and structured types
|
|
4
|
+
*/
|
|
5
|
+
export var TokenType;
|
|
6
|
+
(function (TokenType) {
|
|
7
|
+
// Literals
|
|
8
|
+
TokenType["NUMBER"] = "NUMBER";
|
|
9
|
+
TokenType["IDENTIFIER"] = "IDENTIFIER";
|
|
10
|
+
// Keywords
|
|
11
|
+
TokenType["FUNCTION"] = "FUNCTION";
|
|
12
|
+
TokenType["RETURN"] = "RETURN";
|
|
13
|
+
// Operators
|
|
14
|
+
TokenType["PLUS"] = "PLUS";
|
|
15
|
+
TokenType["MINUS"] = "MINUS";
|
|
16
|
+
TokenType["MULTIPLY"] = "MULTIPLY";
|
|
17
|
+
TokenType["DIVIDE"] = "DIVIDE";
|
|
18
|
+
TokenType["POWER"] = "POWER";
|
|
19
|
+
TokenType["POWER_ALT"] = "POWER_ALT";
|
|
20
|
+
TokenType["NABLA"] = "NABLA";
|
|
21
|
+
// Delimiters
|
|
22
|
+
TokenType["LPAREN"] = "LPAREN";
|
|
23
|
+
TokenType["RPAREN"] = "RPAREN";
|
|
24
|
+
TokenType["LBRACE"] = "LBRACE";
|
|
25
|
+
TokenType["RBRACE"] = "RBRACE";
|
|
26
|
+
TokenType["COMMA"] = "COMMA";
|
|
27
|
+
TokenType["COLON"] = "COLON";
|
|
28
|
+
TokenType["DOT"] = "DOT";
|
|
29
|
+
TokenType["EQUALS"] = "EQUALS";
|
|
30
|
+
// Special
|
|
31
|
+
TokenType["NEWLINE"] = "NEWLINE";
|
|
32
|
+
TokenType["EOF"] = "EOF";
|
|
33
|
+
})(TokenType || (TokenType = {}));
|
|
34
|
+
export class Lexer {
|
|
35
|
+
input;
|
|
36
|
+
position = 0;
|
|
37
|
+
line = 1;
|
|
38
|
+
column = 1;
|
|
39
|
+
constructor(input) {
|
|
40
|
+
this.input = input;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get all tokens
|
|
44
|
+
*/
|
|
45
|
+
tokenize() {
|
|
46
|
+
const tokens = [];
|
|
47
|
+
let token = this.nextToken();
|
|
48
|
+
while (token.type !== TokenType.EOF) {
|
|
49
|
+
if (token.type !== TokenType.NEWLINE) {
|
|
50
|
+
// Skip newlines for now (treat as whitespace)
|
|
51
|
+
tokens.push(token);
|
|
52
|
+
}
|
|
53
|
+
token = this.nextToken();
|
|
54
|
+
}
|
|
55
|
+
tokens.push(token); // EOF token
|
|
56
|
+
return tokens;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get next token
|
|
60
|
+
*/
|
|
61
|
+
nextToken() {
|
|
62
|
+
this.skipWhitespace();
|
|
63
|
+
if (this.isAtEnd()) {
|
|
64
|
+
return this.makeToken(TokenType.EOF, '');
|
|
65
|
+
}
|
|
66
|
+
const char = this.peek();
|
|
67
|
+
const line = this.line;
|
|
68
|
+
const column = this.column;
|
|
69
|
+
// Numbers
|
|
70
|
+
if (this.isDigit(char)) {
|
|
71
|
+
return this.number();
|
|
72
|
+
}
|
|
73
|
+
// Identifiers and keywords
|
|
74
|
+
if (this.isAlpha(char)) {
|
|
75
|
+
return this.identifier();
|
|
76
|
+
}
|
|
77
|
+
// Single character tokens
|
|
78
|
+
switch (char) {
|
|
79
|
+
case '+':
|
|
80
|
+
this.advance();
|
|
81
|
+
return { type: TokenType.PLUS, value: '+', line, column };
|
|
82
|
+
case '-':
|
|
83
|
+
this.advance();
|
|
84
|
+
return { type: TokenType.MINUS, value: '-', line, column };
|
|
85
|
+
case '/':
|
|
86
|
+
this.advance();
|
|
87
|
+
return { type: TokenType.DIVIDE, value: '/', line, column };
|
|
88
|
+
case '(':
|
|
89
|
+
this.advance();
|
|
90
|
+
return { type: TokenType.LPAREN, value: '(', line, column };
|
|
91
|
+
case ')':
|
|
92
|
+
this.advance();
|
|
93
|
+
return { type: TokenType.RPAREN, value: ')', line, column };
|
|
94
|
+
case '{':
|
|
95
|
+
this.advance();
|
|
96
|
+
return { type: TokenType.LBRACE, value: '{', line, column };
|
|
97
|
+
case '}':
|
|
98
|
+
this.advance();
|
|
99
|
+
return { type: TokenType.RBRACE, value: '}', line, column };
|
|
100
|
+
case ',':
|
|
101
|
+
this.advance();
|
|
102
|
+
return { type: TokenType.COMMA, value: ',', line, column };
|
|
103
|
+
case ':':
|
|
104
|
+
this.advance();
|
|
105
|
+
return { type: TokenType.COLON, value: ':', line, column };
|
|
106
|
+
case '.':
|
|
107
|
+
this.advance();
|
|
108
|
+
return { type: TokenType.DOT, value: '.', line, column };
|
|
109
|
+
case '=':
|
|
110
|
+
this.advance();
|
|
111
|
+
return { type: TokenType.EQUALS, value: '=', line, column };
|
|
112
|
+
case '^':
|
|
113
|
+
this.advance();
|
|
114
|
+
return { type: TokenType.POWER, value: '^', line, column };
|
|
115
|
+
case '∇':
|
|
116
|
+
this.advance();
|
|
117
|
+
return { type: TokenType.NABLA, value: '∇', line, column };
|
|
118
|
+
case '*':
|
|
119
|
+
this.advance();
|
|
120
|
+
if (this.peek() === '*') {
|
|
121
|
+
this.advance();
|
|
122
|
+
return { type: TokenType.POWER_ALT, value: '**', line, column };
|
|
123
|
+
}
|
|
124
|
+
return { type: TokenType.MULTIPLY, value: '*', line, column };
|
|
125
|
+
case '\n':
|
|
126
|
+
this.advance();
|
|
127
|
+
return { type: TokenType.NEWLINE, value: '\n', line, column };
|
|
128
|
+
}
|
|
129
|
+
throw new Error(`Unexpected character '${char}' at line ${line}, column ${column}`);
|
|
130
|
+
}
|
|
131
|
+
number() {
|
|
132
|
+
const line = this.line;
|
|
133
|
+
const column = this.column;
|
|
134
|
+
let value = '';
|
|
135
|
+
while (this.isDigit(this.peek())) {
|
|
136
|
+
value += this.advance();
|
|
137
|
+
}
|
|
138
|
+
// Handle decimal point
|
|
139
|
+
if (this.peek() === '.' && this.isDigit(this.peekNext())) {
|
|
140
|
+
value += this.advance(); // consume '.'
|
|
141
|
+
while (this.isDigit(this.peek())) {
|
|
142
|
+
value += this.advance();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Handle scientific notation
|
|
146
|
+
if (this.peek() === 'e' || this.peek() === 'E') {
|
|
147
|
+
value += this.advance(); // consume 'e'
|
|
148
|
+
if (this.peek() === '+' || this.peek() === '-') {
|
|
149
|
+
value += this.advance();
|
|
150
|
+
}
|
|
151
|
+
while (this.isDigit(this.peek())) {
|
|
152
|
+
value += this.advance();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { type: TokenType.NUMBER, value, line, column };
|
|
156
|
+
}
|
|
157
|
+
identifier() {
|
|
158
|
+
const line = this.line;
|
|
159
|
+
const column = this.column;
|
|
160
|
+
let value = '';
|
|
161
|
+
while (this.isAlphaNumeric(this.peek())) {
|
|
162
|
+
value += this.advance();
|
|
163
|
+
}
|
|
164
|
+
// Check for keywords
|
|
165
|
+
let type = TokenType.IDENTIFIER;
|
|
166
|
+
if (value === 'function') {
|
|
167
|
+
type = TokenType.FUNCTION;
|
|
168
|
+
}
|
|
169
|
+
else if (value === 'return') {
|
|
170
|
+
type = TokenType.RETURN;
|
|
171
|
+
}
|
|
172
|
+
return { type, value, line, column };
|
|
173
|
+
}
|
|
174
|
+
skipWhitespace() {
|
|
175
|
+
while (!this.isAtEnd()) {
|
|
176
|
+
const char = this.peek();
|
|
177
|
+
if (char === ' ' || char === '\r' || char === '\t') {
|
|
178
|
+
this.advance();
|
|
179
|
+
}
|
|
180
|
+
else if (char === '/' && this.peekNext() === '/') {
|
|
181
|
+
// Skip single-line comment
|
|
182
|
+
while (this.peek() !== '\n' && !this.isAtEnd()) {
|
|
183
|
+
this.advance();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else if (char === '\n') {
|
|
187
|
+
// Don't skip newlines here - they're tokens
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
peek() {
|
|
196
|
+
if (this.isAtEnd())
|
|
197
|
+
return '\0';
|
|
198
|
+
return this.input[this.position];
|
|
199
|
+
}
|
|
200
|
+
peekNext() {
|
|
201
|
+
if (this.position + 1 >= this.input.length)
|
|
202
|
+
return '\0';
|
|
203
|
+
return this.input[this.position + 1];
|
|
204
|
+
}
|
|
205
|
+
advance() {
|
|
206
|
+
const char = this.input[this.position++];
|
|
207
|
+
if (char === '\n') {
|
|
208
|
+
this.line++;
|
|
209
|
+
this.column = 1;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
this.column++;
|
|
213
|
+
}
|
|
214
|
+
return char;
|
|
215
|
+
}
|
|
216
|
+
isAtEnd() {
|
|
217
|
+
return this.position >= this.input.length;
|
|
218
|
+
}
|
|
219
|
+
isDigit(char) {
|
|
220
|
+
return char >= '0' && char <= '9';
|
|
221
|
+
}
|
|
222
|
+
isAlpha(char) {
|
|
223
|
+
return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || char === '_';
|
|
224
|
+
}
|
|
225
|
+
isAlphaNumeric(char) {
|
|
226
|
+
return this.isAlpha(char) || this.isDigit(char);
|
|
227
|
+
}
|
|
228
|
+
makeToken(type, value) {
|
|
229
|
+
return {
|
|
230
|
+
type,
|
|
231
|
+
value,
|
|
232
|
+
line: this.line,
|
|
233
|
+
column: this.column
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Convenience function to tokenize input
|
|
239
|
+
*/
|
|
240
|
+
export function tokenize(input) {
|
|
241
|
+
const lexer = new Lexer(input);
|
|
242
|
+
return lexer.tokenize();
|
|
243
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser for GradientScript DSL
|
|
3
|
+
* Parses function definitions with structured types
|
|
4
|
+
*/
|
|
5
|
+
import { Program } from './AST.js';
|
|
6
|
+
export declare class Parser {
|
|
7
|
+
private tokens;
|
|
8
|
+
private current;
|
|
9
|
+
constructor(input: string);
|
|
10
|
+
/**
|
|
11
|
+
* Parse the entire program
|
|
12
|
+
*/
|
|
13
|
+
parse(): Program;
|
|
14
|
+
/**
|
|
15
|
+
* Parse function definition
|
|
16
|
+
* function name(param1∇: {x, y}, param2) { ... }
|
|
17
|
+
*/
|
|
18
|
+
private functionDef;
|
|
19
|
+
/**
|
|
20
|
+
* Parse parameter list
|
|
21
|
+
* param1∇: {x, y}, param2, param3∇
|
|
22
|
+
*/
|
|
23
|
+
private parameterList;
|
|
24
|
+
/**
|
|
25
|
+
* Parse single parameter
|
|
26
|
+
* name∇: {x, y} or name or name∇
|
|
27
|
+
*/
|
|
28
|
+
private parameter;
|
|
29
|
+
/**
|
|
30
|
+
* Parse struct type annotation
|
|
31
|
+
* {x, y} or {x, y, z}
|
|
32
|
+
*/
|
|
33
|
+
private structTypeAnnotation;
|
|
34
|
+
/**
|
|
35
|
+
* Parse function body
|
|
36
|
+
* sequence of assignments followed by return
|
|
37
|
+
*/
|
|
38
|
+
private functionBody;
|
|
39
|
+
/**
|
|
40
|
+
* Parse statement (currently only assignment)
|
|
41
|
+
*/
|
|
42
|
+
private statement;
|
|
43
|
+
/**
|
|
44
|
+
* Parse assignment
|
|
45
|
+
* variable = expression
|
|
46
|
+
*/
|
|
47
|
+
private assignment;
|
|
48
|
+
/**
|
|
49
|
+
* Parse expression
|
|
50
|
+
*/
|
|
51
|
+
private expression;
|
|
52
|
+
/**
|
|
53
|
+
* Parse additive expression (+ and -)
|
|
54
|
+
*/
|
|
55
|
+
private additive;
|
|
56
|
+
/**
|
|
57
|
+
* Parse multiplicative expression (* and /)
|
|
58
|
+
*/
|
|
59
|
+
private multiplicative;
|
|
60
|
+
/**
|
|
61
|
+
* Parse power expression (^ and **)
|
|
62
|
+
*/
|
|
63
|
+
private power;
|
|
64
|
+
/**
|
|
65
|
+
* Parse unary expression (- and +)
|
|
66
|
+
*/
|
|
67
|
+
private unary;
|
|
68
|
+
/**
|
|
69
|
+
* Parse postfix expression (function calls and component access)
|
|
70
|
+
*/
|
|
71
|
+
private postfix;
|
|
72
|
+
/**
|
|
73
|
+
* Parse argument list for function call
|
|
74
|
+
*/
|
|
75
|
+
private argumentList;
|
|
76
|
+
/**
|
|
77
|
+
* Parse primary expression
|
|
78
|
+
*/
|
|
79
|
+
private primary;
|
|
80
|
+
private match;
|
|
81
|
+
private check;
|
|
82
|
+
private advance;
|
|
83
|
+
private isAtEnd;
|
|
84
|
+
private peek;
|
|
85
|
+
private previous;
|
|
86
|
+
private consume;
|
|
87
|
+
private error;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Convenience function to parse input
|
|
91
|
+
*/
|
|
92
|
+
export declare function parse(input: string): Program;
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser for GradientScript DSL
|
|
3
|
+
* Parses function definitions with structured types
|
|
4
|
+
*/
|
|
5
|
+
import { TokenType, Lexer } from './Lexer.js';
|
|
6
|
+
import { ParseError } from './Errors.js';
|
|
7
|
+
export class Parser {
|
|
8
|
+
tokens;
|
|
9
|
+
current = 0;
|
|
10
|
+
constructor(input) {
|
|
11
|
+
const lexer = new Lexer(input);
|
|
12
|
+
this.tokens = lexer.tokenize();
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Parse the entire program
|
|
16
|
+
*/
|
|
17
|
+
parse() {
|
|
18
|
+
const functions = [];
|
|
19
|
+
while (!this.isAtEnd()) {
|
|
20
|
+
functions.push(this.functionDef());
|
|
21
|
+
}
|
|
22
|
+
if (functions.length === 0) {
|
|
23
|
+
const token = this.peek();
|
|
24
|
+
throw new ParseError('Expected at least one function definition', token.line, token.column);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
kind: 'program',
|
|
28
|
+
functions
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Parse function definition
|
|
33
|
+
* function name(param1∇: {x, y}, param2) { ... }
|
|
34
|
+
*/
|
|
35
|
+
functionDef() {
|
|
36
|
+
this.consume(TokenType.FUNCTION, "Expected 'function'");
|
|
37
|
+
const name = this.consume(TokenType.IDENTIFIER, 'Expected function name').value;
|
|
38
|
+
this.consume(TokenType.LPAREN, "Expected '(' after function name");
|
|
39
|
+
const parameters = this.parameterList();
|
|
40
|
+
this.consume(TokenType.RPAREN, "Expected ')' after parameters");
|
|
41
|
+
this.consume(TokenType.LBRACE, "Expected '{' before function body");
|
|
42
|
+
const { body, returnExpr } = this.functionBody();
|
|
43
|
+
this.consume(TokenType.RBRACE, "Expected '}' after function body");
|
|
44
|
+
return {
|
|
45
|
+
kind: 'function',
|
|
46
|
+
name,
|
|
47
|
+
parameters,
|
|
48
|
+
body,
|
|
49
|
+
returnExpr
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Parse parameter list
|
|
54
|
+
* param1∇: {x, y}, param2, param3∇
|
|
55
|
+
*/
|
|
56
|
+
parameterList() {
|
|
57
|
+
const params = [];
|
|
58
|
+
if (this.check(TokenType.RPAREN)) {
|
|
59
|
+
return params; // Empty parameter list
|
|
60
|
+
}
|
|
61
|
+
do {
|
|
62
|
+
params.push(this.parameter());
|
|
63
|
+
} while (this.match(TokenType.COMMA));
|
|
64
|
+
return params;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Parse single parameter
|
|
68
|
+
* name∇: {x, y} or name or name∇
|
|
69
|
+
*/
|
|
70
|
+
parameter() {
|
|
71
|
+
const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected parameter name');
|
|
72
|
+
const name = nameToken.value;
|
|
73
|
+
// Check for ∇ (gradient annotation)
|
|
74
|
+
const requiresGrad = this.match(TokenType.NABLA);
|
|
75
|
+
// Check for type annotation : {x, y}
|
|
76
|
+
let paramType;
|
|
77
|
+
if (this.match(TokenType.COLON)) {
|
|
78
|
+
paramType = this.structTypeAnnotation();
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
name,
|
|
82
|
+
requiresGrad,
|
|
83
|
+
paramType
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Parse struct type annotation
|
|
88
|
+
* {x, y} or {x, y, z}
|
|
89
|
+
*/
|
|
90
|
+
structTypeAnnotation() {
|
|
91
|
+
this.consume(TokenType.LBRACE, "Expected '{'");
|
|
92
|
+
const components = [];
|
|
93
|
+
do {
|
|
94
|
+
const comp = this.consume(TokenType.IDENTIFIER, 'Expected component name');
|
|
95
|
+
components.push(comp.value);
|
|
96
|
+
} while (this.match(TokenType.COMMA));
|
|
97
|
+
this.consume(TokenType.RBRACE, "Expected '}'");
|
|
98
|
+
return { components };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Parse function body
|
|
102
|
+
* sequence of assignments followed by return
|
|
103
|
+
*/
|
|
104
|
+
functionBody() {
|
|
105
|
+
const body = [];
|
|
106
|
+
// Parse statements until we hit return
|
|
107
|
+
while (!this.check(TokenType.RETURN) && !this.check(TokenType.RBRACE)) {
|
|
108
|
+
body.push(this.statement());
|
|
109
|
+
}
|
|
110
|
+
// Parse return statement
|
|
111
|
+
this.consume(TokenType.RETURN, "Expected 'return' statement");
|
|
112
|
+
const returnExpr = this.expression();
|
|
113
|
+
return { body, returnExpr };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Parse statement (currently only assignment)
|
|
117
|
+
*/
|
|
118
|
+
statement() {
|
|
119
|
+
return this.assignment();
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Parse assignment
|
|
123
|
+
* variable = expression
|
|
124
|
+
*/
|
|
125
|
+
assignment() {
|
|
126
|
+
const variable = this.consume(TokenType.IDENTIFIER, 'Expected variable name').value;
|
|
127
|
+
this.consume(TokenType.EQUALS, "Expected '=' in assignment");
|
|
128
|
+
const expression = this.expression();
|
|
129
|
+
return {
|
|
130
|
+
kind: 'assignment',
|
|
131
|
+
variable,
|
|
132
|
+
expression
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Parse expression
|
|
137
|
+
*/
|
|
138
|
+
expression() {
|
|
139
|
+
return this.additive();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Parse additive expression (+ and -)
|
|
143
|
+
*/
|
|
144
|
+
additive() {
|
|
145
|
+
let expr = this.multiplicative();
|
|
146
|
+
while (this.match(TokenType.PLUS, TokenType.MINUS)) {
|
|
147
|
+
const operator = this.previous().value;
|
|
148
|
+
const right = this.multiplicative();
|
|
149
|
+
expr = {
|
|
150
|
+
kind: 'binary',
|
|
151
|
+
operator,
|
|
152
|
+
left: expr,
|
|
153
|
+
right
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return expr;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Parse multiplicative expression (* and /)
|
|
160
|
+
*/
|
|
161
|
+
multiplicative() {
|
|
162
|
+
let expr = this.power();
|
|
163
|
+
while (this.match(TokenType.MULTIPLY, TokenType.DIVIDE)) {
|
|
164
|
+
const operator = this.previous().value;
|
|
165
|
+
const right = this.power();
|
|
166
|
+
expr = {
|
|
167
|
+
kind: 'binary',
|
|
168
|
+
operator,
|
|
169
|
+
left: expr,
|
|
170
|
+
right
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return expr;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Parse power expression (^ and **)
|
|
177
|
+
*/
|
|
178
|
+
power() {
|
|
179
|
+
let expr = this.unary();
|
|
180
|
+
// Right-associative
|
|
181
|
+
if (this.match(TokenType.POWER, TokenType.POWER_ALT)) {
|
|
182
|
+
const operator = this.previous().type === TokenType.POWER ? '^' : '**';
|
|
183
|
+
const right = this.power(); // Right-associative recursion
|
|
184
|
+
expr = {
|
|
185
|
+
kind: 'binary',
|
|
186
|
+
operator,
|
|
187
|
+
left: expr,
|
|
188
|
+
right
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return expr;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Parse unary expression (- and +)
|
|
195
|
+
*/
|
|
196
|
+
unary() {
|
|
197
|
+
if (this.match(TokenType.MINUS, TokenType.PLUS)) {
|
|
198
|
+
const operator = this.previous().value;
|
|
199
|
+
const operand = this.unary();
|
|
200
|
+
return {
|
|
201
|
+
kind: 'unary',
|
|
202
|
+
operator,
|
|
203
|
+
operand
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return this.postfix();
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Parse postfix expression (function calls and component access)
|
|
210
|
+
*/
|
|
211
|
+
postfix() {
|
|
212
|
+
let expr = this.primary();
|
|
213
|
+
while (true) {
|
|
214
|
+
if (this.match(TokenType.LPAREN)) {
|
|
215
|
+
// Function call
|
|
216
|
+
const args = this.argumentList();
|
|
217
|
+
this.consume(TokenType.RPAREN, "Expected ')' after arguments");
|
|
218
|
+
if (expr.kind !== 'variable') {
|
|
219
|
+
const token = this.previous();
|
|
220
|
+
throw new ParseError('Only simple function names are supported', token.line, token.column);
|
|
221
|
+
}
|
|
222
|
+
expr = {
|
|
223
|
+
kind: 'call',
|
|
224
|
+
name: expr.name,
|
|
225
|
+
args
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
else if (this.match(TokenType.DOT)) {
|
|
229
|
+
// Component access
|
|
230
|
+
const component = this.consume(TokenType.IDENTIFIER, 'Expected component name after "."').value;
|
|
231
|
+
expr = {
|
|
232
|
+
kind: 'component',
|
|
233
|
+
object: expr,
|
|
234
|
+
component
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return expr;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Parse argument list for function call
|
|
245
|
+
*/
|
|
246
|
+
argumentList() {
|
|
247
|
+
const args = [];
|
|
248
|
+
if (this.check(TokenType.RPAREN)) {
|
|
249
|
+
return args; // Empty argument list
|
|
250
|
+
}
|
|
251
|
+
do {
|
|
252
|
+
args.push(this.expression());
|
|
253
|
+
} while (this.match(TokenType.COMMA));
|
|
254
|
+
return args;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Parse primary expression
|
|
258
|
+
*/
|
|
259
|
+
primary() {
|
|
260
|
+
// Number
|
|
261
|
+
if (this.match(TokenType.NUMBER)) {
|
|
262
|
+
const value = parseFloat(this.previous().value);
|
|
263
|
+
return {
|
|
264
|
+
kind: 'number',
|
|
265
|
+
value
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
// Variable
|
|
269
|
+
if (this.match(TokenType.IDENTIFIER)) {
|
|
270
|
+
const name = this.previous().value;
|
|
271
|
+
return {
|
|
272
|
+
kind: 'variable',
|
|
273
|
+
name
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
// Parenthesized expression
|
|
277
|
+
if (this.match(TokenType.LPAREN)) {
|
|
278
|
+
const expr = this.expression();
|
|
279
|
+
this.consume(TokenType.RPAREN, "Expected ')' after expression");
|
|
280
|
+
return expr;
|
|
281
|
+
}
|
|
282
|
+
throw this.error(this.peek(), 'Expected expression');
|
|
283
|
+
}
|
|
284
|
+
// Helper methods
|
|
285
|
+
match(...types) {
|
|
286
|
+
for (const type of types) {
|
|
287
|
+
if (this.check(type)) {
|
|
288
|
+
this.advance();
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
check(type) {
|
|
295
|
+
if (this.isAtEnd())
|
|
296
|
+
return false;
|
|
297
|
+
return this.peek().type === type;
|
|
298
|
+
}
|
|
299
|
+
advance() {
|
|
300
|
+
if (!this.isAtEnd())
|
|
301
|
+
this.current++;
|
|
302
|
+
return this.previous();
|
|
303
|
+
}
|
|
304
|
+
isAtEnd() {
|
|
305
|
+
return this.peek().type === TokenType.EOF;
|
|
306
|
+
}
|
|
307
|
+
peek() {
|
|
308
|
+
return this.tokens[this.current];
|
|
309
|
+
}
|
|
310
|
+
previous() {
|
|
311
|
+
return this.tokens[this.current - 1];
|
|
312
|
+
}
|
|
313
|
+
consume(type, message) {
|
|
314
|
+
if (this.check(type))
|
|
315
|
+
return this.advance();
|
|
316
|
+
throw this.error(this.peek(), message);
|
|
317
|
+
}
|
|
318
|
+
error(token, message) {
|
|
319
|
+
return new ParseError(message, token.line, token.column, token.value);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Convenience function to parse input
|
|
324
|
+
*/
|
|
325
|
+
export function parse(input) {
|
|
326
|
+
const parser = new Parser(input);
|
|
327
|
+
return parser.parse();
|
|
328
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expression simplification for GradientScript DSL
|
|
3
|
+
* Applies algebraic simplification rules
|
|
4
|
+
*/
|
|
5
|
+
import { Expression } from './AST.js';
|
|
6
|
+
/**
|
|
7
|
+
* Simplify an expression using algebraic rules
|
|
8
|
+
*/
|
|
9
|
+
export declare function simplify(expr: Expression): Expression;
|
|
10
|
+
/**
|
|
11
|
+
* Simplify all gradients in a map
|
|
12
|
+
*/
|
|
13
|
+
export declare function simplifyGradients(gradients: Map<string, Expression | {
|
|
14
|
+
components: Map<string, Expression>;
|
|
15
|
+
}>): Map<string, Expression | {
|
|
16
|
+
components: Map<string, Expression>;
|
|
17
|
+
}>;
|