UNPKG

12.3 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 '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 // console.log(name, expr[0])
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 // TODO: fix memory issue and reenable AST output
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
369module.exports = NaiveCompiler;