1 | const {Expr, Token, Expression, SpliceSetterExpression, SourceTag, TokenTypeData} = require('./lang');
|
2 | const _ = require('lodash');
|
3 | const {splitSettersGetters, topologicalSortGetters, tagAllExpressions, tagToSimpleFilename} = require('./expr-tagging');
|
4 | const objectHash = require('object-hash');
|
5 |
|
6 | const nativeOps = {
|
7 | eq: '===',
|
8 | plus: '+',
|
9 | minus: '-',
|
10 | mult: '*',
|
11 | div: '/',
|
12 | gt: '>',
|
13 | gte: '>=',
|
14 | lt: '<',
|
15 | lte: '<=',
|
16 | mod: '%'
|
17 | };
|
18 |
|
19 | const typeOfChecks = {
|
20 | isUndefined: 'undefined',
|
21 | isBoolean: 'boolean',
|
22 | isString: 'string',
|
23 | isNumber: 'number'
|
24 | }
|
25 |
|
26 | class NaiveCompiler {
|
27 | constructor(model, options) {
|
28 | const {getters, setters} = splitSettersGetters(model);
|
29 | tagAllExpressions(getters);
|
30 | this.getters = getters;
|
31 | this.setters = setters;
|
32 |
|
33 | this.options = options;
|
34 | }
|
35 |
|
36 | get template() {
|
37 | return require('./templates/naive.js');
|
38 | }
|
39 |
|
40 | getNativeMathFunction(name, source) {
|
41 | return this.options.debug ? `mathFunction('${name}', '${source}')` : `Math.${name}`
|
42 | }
|
43 |
|
44 | getNativeStringFunction(name, source) {
|
45 | return `String.prototype.${name}`
|
46 | }
|
47 |
|
48 | generateExpr(expr) {
|
49 | const currentToken = expr instanceof Expression ? expr[0] : expr;
|
50 | const source = currentToken[SourceTag]
|
51 |
|
52 | const tokenType = currentToken.$type;
|
53 | switch (tokenType) {
|
54 | case 'quote': return this.generateExpr(expr[1])
|
55 | case 'breakpoint': return `((() => {debugger; return ${this.generateExpr(expr[1])}}) ())`
|
56 | case 'trace': {
|
57 | const label = expr.length === 3 && this.generateExpr(expr[1])
|
58 | const inner = expr.length === 3 ? expr[2] : expr[1]
|
59 | const nextToken = inner instanceof Expression ? inner[0] : inner
|
60 | const innerSrc = nextToken[SourceTag] || source
|
61 |
|
62 | return `((() => {
|
63 | const value = (${this.generateExpr(inner)});
|
64 | console.log(${label ? `${label}, ` : ''}{value, token: '${nextToken.$type}', source: '${this.shortSource(innerSrc)}'})
|
65 | return value;
|
66 | }) ())`
|
67 | }
|
68 | case 'and':
|
69 | return (
|
70 | `(${
|
71 | expr
|
72 | .slice(1)
|
73 | .map(e => this.generateExpr(e))
|
74 | .map(part => `(${part})`)
|
75 | .join('&&')
|
76 | })`
|
77 | );
|
78 | case 'or':
|
79 | return (
|
80 | `(${
|
81 | expr
|
82 | .slice(1)
|
83 | .map(e => this.generateExpr(e))
|
84 | .map(part => `(${part})`)
|
85 | .join('||')
|
86 | })`
|
87 | );
|
88 | case 'not':
|
89 | return `!(${this.generateExpr(expr[1])})`;
|
90 | case 'ternary':
|
91 | return `((${this.generateExpr(expr[1])})?(${this.generateExpr(expr[2])}):(${this.generateExpr(expr[3])}))`;
|
92 | case 'array':
|
93 | return `[${expr
|
94 | .slice(1)
|
95 | .map(t => this.generateExpr(t))
|
96 | .join(',')}]`;
|
97 | case 'object':
|
98 | return `{${_.range(1, expr.length, 2)
|
99 | .map(idx => `"${expr[idx]}": ${this.generateExpr(expr[idx + 1])}`)
|
100 | .join(',')}}`;
|
101 | case 'range':
|
102 | return `range(${this.generateExpr(expr[1])}, ${expr.length > 2 ? this.generateExpr(expr[2]) : '0'}, ${
|
103 | expr.length > 3 ? this.generateExpr(expr[3]) : '1'
|
104 | })`;
|
105 | case 'keys':
|
106 | case 'values':
|
107 | case 'assign':
|
108 | case 'defaults':
|
109 | case 'size':
|
110 | case 'sum':
|
111 | case 'flatten':
|
112 | return `${tokenType}(${this.generateExpr(expr[1])})`;
|
113 | case 'isArray':
|
114 | return `Array.isArray(${this.generateExpr(expr[1])})`
|
115 | case 'isBoolean':
|
116 | case 'isNumber':
|
117 | case 'isString':
|
118 | case 'isUndefined':
|
119 | return `(typeof (${this.generateExpr(expr[1])}) === '${typeOfChecks[tokenType]}')`
|
120 | case 'toUpperCase':
|
121 | case 'toLowerCase':
|
122 | return `(${this.getNativeStringFunction(tokenType, source)}).call(${this.generateExpr(expr[1])})`;
|
123 | case 'stringLength':
|
124 | return `(${this.generateExpr(expr[1])}).length`
|
125 | case 'floor':
|
126 | case 'ceil':
|
127 | case 'round':
|
128 | return `(${this.getNativeMathFunction(tokenType, source)})(${this.generateExpr(expr[1])})`;
|
129 | case 'parseInt':
|
130 | return `parseInt(${this.generateExpr(expr[1])}, ${expr.length > 2 ? expr[2] : 10})`;
|
131 | case 'eq':
|
132 | case 'lt':
|
133 | case 'lte':
|
134 | case 'gt':
|
135 | case 'gte':
|
136 | case 'plus':
|
137 | case 'minus':
|
138 | case 'mult':
|
139 | case 'div':
|
140 | case 'mod':
|
141 | return `(${this.generateExpr(expr[1])}) ${nativeOps[tokenType]} (${this.generateExpr(expr[2])})`;
|
142 | case 'startsWith':
|
143 | case 'endsWith':
|
144 | case 'split':
|
145 | return `(${this.getNativeStringFunction(tokenType, source)}).call(${this.generateExpr(expr[1])}, ${this.generateExpr(expr[2])})`;
|
146 | case 'substring':
|
147 | return `(${this.getNativeStringFunction(tokenType, source)}).call(${this.generateExpr(expr[1])}, ${this.generateExpr(expr[2])}, ${this.generateExpr(expr[3])})`;
|
148 | case 'get':
|
149 | return `${this.generateExpr(expr[2])}[${this.generateExpr(expr[1])}]`;
|
150 | case 'mapValues':
|
151 | case 'filterBy':
|
152 | case 'groupBy':
|
153 | case 'mapKeys':
|
154 | case 'map':
|
155 | case 'any':
|
156 | case 'filter':
|
157 | case 'keyBy':
|
158 | case 'anyValues':
|
159 | case 'recursiveMap':
|
160 | case 'recursiveMapValues':
|
161 | return `${tokenType}(${this.generateExpr(expr[1])}, ${this.generateExpr(expr[2])}, ${
|
162 | typeof expr[3] === 'undefined' ? null : this.generateExpr(expr[3])
|
163 | })`;
|
164 | case 'loop':
|
165 | return 'loop';
|
166 | case 'recur':
|
167 | return `${this.generateExpr(expr[1])}(${this.generateExpr(expr[2])})`;
|
168 | case 'func':
|
169 | return currentToken.$funcId;
|
170 | case 'root':
|
171 | return '$model';
|
172 | case 'null':
|
173 | case 'val':
|
174 | case 'key':
|
175 | case 'arg0':
|
176 | case 'arg1':
|
177 | case 'arg2':
|
178 | case 'arg3':
|
179 | case 'arg4':
|
180 | case 'arg5':
|
181 | case 'arg6':
|
182 | case 'arg7':
|
183 | case 'arg8':
|
184 | case 'arg9':
|
185 | case 'context':
|
186 | return tokenType;
|
187 | case 'topLevel':
|
188 | return '$res';
|
189 | case 'cond':
|
190 | return `$cond_${this.generateExpr(expr[1])}`
|
191 | case 'effect':
|
192 | case 'call':
|
193 | return `($funcLib[${this.generateExpr(expr[1])}].call($res${expr
|
194 | .slice(2)
|
195 | .map(subExpr => `,${this.generateExpr(subExpr)}`)
|
196 | .join('')}) ${tokenType === 'effect' ? ' && void 0' : ''})`;
|
197 | case 'bind':
|
198 | return `($funcLibRaw[${this.generateExpr(expr[1])}] || $res[${this.generateExpr(expr[1])}]).bind($res${expr
|
199 | .slice(2)
|
200 | .map(subExpr => `,${this.generateExpr(subExpr)}`)
|
201 | .join('')})`;
|
202 | case 'invoke':
|
203 | return `(${expr[1]}(${expr.slice(2).map(t => t.$type).join(',')}))`
|
204 | case 'abstract':
|
205 | throw expr[2]
|
206 | default:
|
207 | return JSON.stringify(currentToken);
|
208 | }
|
209 | }
|
210 |
|
211 | buildDerived(name) {
|
212 | const prefix = name.indexOf('$') === 0 ? '' : `$res.${name} = `;
|
213 | return `${prefix} $${name}();`;
|
214 | }
|
215 |
|
216 | buildSetter(setter, name) {
|
217 | const setterType = setter.setterType()
|
218 | const numTokens = setter.filter(part => part instanceof Token).length - 1
|
219 | const pathExpr =
|
220 | [...setter.slice(1)].map(token => {
|
221 | if (!(token instanceof Token)) {
|
222 | return JSON.stringify(token)
|
223 | }
|
224 |
|
225 | if (setterType === 'splice' && token.$type === 'key') {
|
226 | return `arg${numTokens - 1}`
|
227 | }
|
228 |
|
229 | return token.$type
|
230 | }).join(',')
|
231 | return `${name}: $setter.bind(null, (${Array(numTokens).fill(null).map((a, i) => `arg${i}`).concat('...additionalArgs').join(',')}) => ${setterType}([${pathExpr}], ...additionalArgs))`
|
232 | }
|
233 |
|
234 | pathToString(path, n = 0) {
|
235 | this.disableTypeChecking = true
|
236 | const res = this.generateExpr(
|
237 | path.slice(1, path.length - n).reduce((acc, token) => Expr(new Token('get'), token, acc), path[0])
|
238 | );
|
239 | this.disableTypeChecking = false
|
240 | return res
|
241 | }
|
242 |
|
243 | shortSource(src) {
|
244 | return require('path').relative(this.options.cwd || '.', src)
|
245 | }
|
246 |
|
247 | exprTemplatePlaceholders(expr, funcName) {
|
248 | const currentToken = expr instanceof Expression ? expr[0] : expr;
|
249 | const tokenType = currentToken.$type;
|
250 | return {
|
251 | ROOTNAME: expr[0].$rootName,
|
252 | FUNCNAME: funcName,
|
253 | EXPR1: () => expr.length > 1 ? this.generateExpr(expr[1]) : '',
|
254 | EXPR: () => this.generateExpr(expr),
|
255 | TYPE_CHECK: () => {
|
256 | const typeData = TokenTypeData[tokenType]
|
257 |
|
258 | if (!this.options.debug || !typeData || !_.size(typeData.expectedTypes)) {
|
259 | return ''
|
260 | }
|
261 |
|
262 | const input = expr[typeData.chainIndex] instanceof Expression || expr[typeData.chainIndex] instanceof Token ? this.generateExpr(expr[typeData.chainIndex]) : expr[typeData.chainIndex]
|
263 | const name = currentToken.$rootName
|
264 | const source = this.shortSource(currentToken[SourceTag])
|
265 | return `checkTypes(${input}, '${name}', ${JSON.stringify(typeData.expectedTypes)}, '${tokenType}', '${source}')`
|
266 | },
|
267 | ID: () => expr[0].$id,
|
268 | FN_ARGS: () => ` ${expr[0].$type === 'func' ? expr.slice(2).map(t => t.$type).join(',') : ''}`
|
269 | };
|
270 | }
|
271 |
|
272 | appendExpr(acc, type, expr, funcName) {
|
273 | acc.push(
|
274 | this.mergeTemplate(
|
275 | expr[0].$type === 'func' && this.template[expr[0].$funcType] ?
|
276 | this.template[expr[0].$funcType] :
|
277 | this.template[type],
|
278 | this.exprTemplatePlaceholders(expr, funcName)
|
279 | )
|
280 | );
|
281 | }
|
282 |
|
283 | buildExprFunctionsByTokenType(acc, expr) {
|
284 | const tokenType = expr[0].$type;
|
285 | switch (tokenType) {
|
286 | case 'func':
|
287 | this.appendExpr(acc, tokenType, expr, expr[0].$funcId);
|
288 | break;
|
289 | }
|
290 | }
|
291 |
|
292 | buildExprFunctions(acc, expr, name) {
|
293 | if (!(expr instanceof Expression) || !expr[0]) {
|
294 | return acc;
|
295 | }
|
296 | _.forEach(expr.slice(1), this.buildExprFunctions.bind(this, acc));
|
297 | this.buildExprFunctionsByTokenType(acc, expr);
|
298 | if (typeof name === 'string') {
|
299 |
|
300 | if (expr[0].$type !== 'func') {
|
301 | this.appendExpr(acc, 'topLevel', expr, name);
|
302 | }
|
303 | }
|
304 | return acc;
|
305 | }
|
306 |
|
307 | mergeTemplate(template, placeHolders) {
|
308 | return Object.keys(placeHolders)
|
309 | .reduce((result, name) => {
|
310 | const replaceFunc = typeof placeHolders[name] === 'function' ? placeHolders[name]() : () => placeHolders[name];
|
311 | const commentRegex = new RegExp(`/\\*\\s*${name}\\s*([\\s\\S]*?)\\*/`, 'mg');
|
312 | const dollarRegex = new RegExp(`\\$${name}`, 'g');
|
313 | const inCommentRegex = new RegExp(
|
314 | `/\\*\\s*${name}\\s*\\*/([\\s\\S]*?)/\\*\\s*${name}\\-END\\s*\\*/`,
|
315 | 'mg'
|
316 | );
|
317 | return result
|
318 | .replace(inCommentRegex, replaceFunc)
|
319 | .replace(commentRegex, replaceFunc)
|
320 | .replace(dollarRegex, replaceFunc);
|
321 | }, template.toString())
|
322 | .replace(/function\s*\w*\(\)\s*\{\s*([\s\S]+)\}/, (m, i) => i);
|
323 | }
|
324 |
|
325 |
|
326 | allExpressions() {
|
327 | return _.reduce(this.getters, this.buildExprFunctions.bind(this), []).join('\n')
|
328 | }
|
329 |
|
330 | topLevelOverrides() {
|
331 | return {
|
332 | NAME: this.options.name,
|
333 |
|
334 | AST: () => JSON.stringify(this.getters, null, 2),
|
335 | DEBUG_MODE: () => `/* DEBUG */${!!this.options.debug}`,
|
336 | SOURCE_FILES: () => () => this.options.debug ? JSON.stringify(Object.values(this.getters).reduce((acc, getter) => {
|
337 | const tag = getter instanceof Expression && getter[0][SourceTag];
|
338 | const simpleFileName = tag && tagToSimpleFilename(tag);
|
339 | if (simpleFileName && !acc[simpleFileName]) {
|
340 | const fileName = getter[0][SourceTag].split(':')[0]
|
341 | acc[simpleFileName] = require('fs').readFileSync(fileName).toString();
|
342 | }
|
343 | return acc;
|
344 | }, {})) : '',
|
345 | LIBRARY: () => this.mergeTemplate(this.template.library, {}),
|
346 | ALL_EXPRESSIONS: () => this.allExpressions(),
|
347 | DERIVED: () =>
|
348 | topologicalSortGetters(this.getters)
|
349 | .filter(name => this.getters[name][0].$type !== 'func')
|
350 | .map(this.buildDerived.bind(this))
|
351 | .join('\n'),
|
352 | SETTERS: () => _.map(this.setters, this.buildSetter.bind(this)).join(',')
|
353 | };
|
354 | }
|
355 |
|
356 | compile() {
|
357 | return this.mergeTemplate(this.template.base, this.topLevelOverrides());
|
358 | }
|
359 |
|
360 | hash() {
|
361 | return objectHash({getters: this.getters, setters: this.setters});
|
362 | }
|
363 |
|
364 | get lang() {
|
365 | return 'js';
|
366 | }
|
367 | }
|
368 |
|
369 | module.exports = NaiveCompiler;
|