Codewars Tiny Three-Pass Compiler JS堆内存溢出问题求助
问题现象
完成Tiny Three-Pass Compiler练习时,在Node.js v10环境下出现以下错误:
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory Aborted (core dumped) <--- Last few GCs --->
[19:0x5578df6e1f80] 6093 ms: Mark-sweep 580.2 (592.5) -> 580.2 (584.5) MB, 1501.0 / 0.0 ms (average mu = 0.282, current mu = 0.000) last resort GC in old space requested [19:0x5578df6e1f80] 6285 ms: Mark-sweep 580.2 (584.5) -> 580.2 (584.5) MB, 191.9 / 0.0 ms (average mu = 0.243, current mu = 0.000) last resort GC in old space requested
<--- JS stacktrace --->
==== JS stack trace =========================================
0: ExitFrame [pc: 0x3dbb90e5be1d] Security context: 0x3dd3dcdf3419
1: /* anonymous / [0x3d354a868c89 ,program=0x1344e3ade909 <String[47]: [ x y z ] ( 23x +5y -3z ) / (1 +3 +22)>]
2: /* anonymous */ [0x3d354a8463c9] [/home/codewarrior/node/test.js:214] [bytecode=0x3dd3dcdce859 offset=90](this=0x3d354...`
在Node.js v8环境下则出现:
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
Aborted (core dumped)
当前代码
function isNumber (token){ return !isNaN(token); } function isOperator(token){ return "*/+-"indexOf(token)!== -1; } function Compiler () {}; Compiler.prototype.compile = function (program) { return this.pass3(this.pass2(this.pass1(program))); }; Compiler.prototype.tokenize = function (program) { // Turn a program string into an array of tokens. Each token // is either '[', ']', '(', ')', '+', '-', '*', '/', a variable // name or a number (as a string) var regex = /\s*([-+*/\(\)\[\]]|[A-Za-z]+|[0-9]+)\s*/g; return program.replace(regex, ":$1").substring(1).split(':').map( function (tok) { return isNaN(tok) ? tok : tok|0; }); }; Compiler.prototype.pass1 = function (program) { var tokens = this.tokenize(program); function getNextToken(){ token = tokens.shift(); } function precedenceIsNotGreater(o1, o2){ var precedences = { '/' :4, '*' :3, '+' :2, '-' :1, } return precedences[o1] <= precedences[o2]; } var token ; var outputQueue = []; var operatorStack = []; var args = []; do{ getNextToken() if(token === '['){ for(getNextToken(); token !== ']'; getNextToken()){ args.push(token); } }else if(isNumber(token) || args.includes(token)){ outputQueue.push(token); }else if(isOperator(token)){ var o1 = token; for(var o2 = operatorStack[operatorStack.length-1]; operatorStack.length && isOperator(o2) && precedenceIsNotGreater(o1, o2); o2 = operatorStack[operatorStack.length -1]){ outputQueue.push(token); } operatorStack.push(o1); }else if(token === '('){ operatorStack.push(token); } else if(token === ')'){ for(var nextOperator = operatorStack[operatorStack.length -1]; operatorStack.length && nextOperator !== '('; nextOperator = operatorStack[operatorStack.length-1]){ outputQueue.push(operatorStack.pop()) } operatorStack.pop() } } while(tokens.length); while(operatorStack.length){ outputQueue.push(operatorStack.pop()) } var output; function getNextOutput(){ output = outputQueue.pop(); } function buildAst(outputQueue) { getNextOutput(); var node = {}; if(isNumber(output)){ node.op = 'imm'; node.n = output; } else if(args.includes(output)){ node.op = 'arg'; node.n = args.indexOf(output); } else if(isOperator(output)){ node.op = output; var b = buildAst(outputQueue); var a = buildAst(outputQueue); node.a = a; node.b = b; } return node; } return buildAst(outputQueue); }; Compiler.prototype.pass2 = function (ast) { function reduceTree(ast) { if(ast.op === 'imm' || ast.op === 'arg') { return ast; } ast.a = reduceTree(ast.a); ast.b = reduceTree(ast.b); if(ast.a.op === 'imm' && ast.b.op === 'imm'){ var n = Function("return " + ''+ast.a.n+ast.op+ast.b.n)(); return{ op: 'imm', n: n} } return ast; } return reduceTree(ast) }; Compiler.prototype.pass3 = function (ast) { var operatorMap = { '+' : 'AD', '-' : 'SU', '*' : 'MU', '/' : 'DI', } var operationDepths = {}; var maxDepth = -Infinity; function markDepth (ast, depth =0){ if(ast.a && ast.b){ maxDepth = Math.max(maxDepth, depth); if(!operationDepths[depth]){ operationDepths[depth] = []; } operationDepths[depth].push(ast); markDepth(ast.a, depth+1); markDepth(ast.b, depth+1); } } markDepth(ast); var asm = []; var currentDepth = maxDepth; while(currentDepth >=0){ currentDepthOperations = operationDepths[currentDepth]; while(currentDepthOperations.length){ var currentOperation = currentDepthOperations.shift(); if(currentOperation.b.op === 'imm'){ asm.push('IM ' + currentOperation.b.n); }else if(currentOperation.b.op === 'arg'){ asm.push('AR ' + currentOperation.b.n) }else{ asm.push('PO') } asm.push('SW') if(currentOperation.a.aop === 'imm'){ asm.push('IM ' + currentOperation.a.n) }else if(currentOperation.a.op === 'arg') { asm.push('AR ' + currentOperation.a.n) }else{ asm.push('PO') } asm.push(operatorMap[currentOperation.op]); asm.push('PU') } currentDepth--; } return asm; };
问题分析与修复
内存溢出的核心原因是pass1中处理运算符时的死循环,同时代码还有几处语法和逻辑错误,逐一修复如下:
1. 修复isOperator函数的语法错误
原代码中字符串调用indexOf时缺少点运算符,导致语法错误:
// 错误 return "*/+-"indexOf(token)!== -1; // 修复后 return "*/+-".indexOf(token)!== -1;
2. 修复pass1中的运算符处理死循环
在处理运算符的for循环里,原代码错误地将当前运算符token(即o1)反复加入outputQueue,而没有弹出栈顶的运算符o2,导致队列无限增长,最终内存溢出:
// 错误 outputQueue.push(token); // 修复后:弹出栈顶运算符并加入队列 outputQueue.push(operatorStack.pop());
3. 修复pass3中的笔误
原代码中currentOperation.a.aop是笔误,应为currentOperation.a.op,否则会导致判断逻辑错误:
// 错误 if(currentOperation.a.aop === 'imm'){ // 修复后 if(currentOperation.a.op === 'imm'){
修复后的完整代码
function isNumber(token) { return !isNaN(token); } function isOperator(token) { return "*/+-".indexOf(token)!== -1; } function Compiler() {}; Compiler.prototype.compile = function(program) { return this.pass3(this.pass2(this.pass1(program))); }; Compiler.prototype.tokenize = function(program) { var regex = /\s*([-+*/\(\)\[\]]|[A-Za-z]+|[0-9]+)\s*/g; return program.replace(regex, ":$1").substring(1).split(':').map(function(tok) { return isNaN(tok)? tok : tok | 0; }); }; Compiler.prototype.pass1 = function(program) { var tokens = this.tokenize(program); function getNextToken() { token = tokens.shift(); } function precedenceIsNotGreater(o1, o2) { var precedences = { '/' :4, '*' :3, '+' :2, '-' :1, } return precedences[o1] <= precedences[o2]; } var token; var outputQueue = []; var operatorStack = []; var args = []; do { getNextToken(); if(token === '[') { for(getNextToken(); token!== ']'; getNextToken()) { args.push(token); } } else if(isNumber(token) || args.includes(token)) { outputQueue.push(token); } else if(isOperator(token)) { var o1 = token; for(var o2 = operatorStack[operatorStack.length-1]; operatorStack.length && isOperator(o2) && precedenceIsNotGreater(o1, o2); o2 = operatorStack[operatorStack.length -1]) { outputQueue.push(operatorStack.pop()); } operatorStack.push(o1); } else if(token === '(') { operatorStack.push(token); } else if(token === ')') { for(var nextOperator = operatorStack[operatorStack.length -1]; operatorStack.length && nextOperator!== '('; nextOperator = operatorStack[operatorStack.length-1]) { outputQueue.push(operatorStack.pop()) } operatorStack.pop(); } } while(tokens.length); while(operatorStack.length) { outputQueue.push(operatorStack.pop()) } var output; function getNextOutput() { output = outputQueue.pop(); } function buildAst(outputQueue) { getNextOutput(); var node = {}; if(isNumber(output)) { node.op = 'imm'; node.n = output; } else if(args.includes(output)) { node.op = 'arg'; node.n = args.indexOf(output); } else if(isOperator(output)) { node.op = output; var b = buildAst(outputQueue); var a = buildAst(outputQueue); node.a = a; node.b = b; } return node; } return buildAst(outputQueue); }; Compiler.prototype.pass2 = function(ast) { function reduceTree(ast) { if(ast.op === 'imm' || ast.op === 'arg') { return ast; } ast.a = reduceTree(ast.a); ast.b = reduceTree(ast.b); if(ast.a.op === 'imm' && ast.b.op === 'imm') { var n = Function("return " + ast.a.n + ast.op + ast.b.n)(); return { op: 'imm', n: n }; } return ast; } return reduceTree(ast); }; Compiler.prototype.pass3 = function(ast) { var operatorMap = { '+' : 'AD', '-' : 'SU', '*' : 'MU', '/' : 'DI', } var operationDepths = {}; var maxDepth = -Infinity; function markDepth(ast, depth = 0) { if(ast.a && ast.b) { maxDepth = Math.max(maxDepth, depth); if(!operationDepths[depth]) { operationDepths[depth] = []; } operationDepths[depth].push(ast); markDepth(ast.a, depth + 1); markDepth(ast.b, depth + 1); } } markDepth(ast); var asm = []; var currentDepth = maxDepth; while(currentDepth >= 0) { var currentDepthOperations = operationDepths[currentDepth]; while(currentDepthOperations.length) { var currentOperation = currentDepthOperations.shift(); if(currentOperation.b.op === 'imm') { asm.push('IM ' + currentOperation.b.n); } else if(currentOperation.b.op === 'arg') { asm.push('AR ' + currentOperation.b.n); } else { asm.push('PO'); } asm.push('SW'); if(currentOperation.a.op === 'imm') { asm.push('IM ' + currentOperation.a.n); } else if(currentOperation.a.op === 'arg') { asm.push('AR ' + currentOperation.a.n); } else { asm.push('PO'); } asm.push(operatorMap[currentOperation.op]); asm.push('PU'); } currentDepth--; } return asm; };
内容的提问来源于stack exchange,提问作者Victor Malhotra

