UNPKG

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