diff --git a/dist/index.min.js b/dist/index.min.js index 2a7fc1d..a61ba1d 100644 --- a/dist/index.min.js +++ b/dist/index.min.js @@ -1 +1 @@ -(()=>{"use strict";var e={404:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.default=class{constructor(e=null){this.superEnvironment=e,this.table=new Map}get(e){const t=this.table.get(e);return void 0!==t?t:null===this.superEnvironment?null:this.superEnvironment.get(e)}set(e,t){this.table.set(e,t)}}},290:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.wrapReturnValue=t.makeEvaluatedEmpty=t.makeEvaluatedFunction=t.makeEvaluatedBoolean=t.makeEvaluatedString=t.makeEvaluatedNumber=void 0,t.makeEvaluatedNumber=e=>({type:"number",value:e,get representation(){return`${e}`}}),t.makeEvaluatedString=e=>({type:"string",value:e,get representation(){return`'${e}'`}}),t.makeEvaluatedBoolean=e=>({type:"boolean",value:e,get representation(){return e?"참":"거짓"}}),t.makeEvaluatedFunction=(e,t,r)=>({type:"function",parameters:e,body:t,environment:r,get representation(){return"(함수)"}}),t.makeEvaluatedEmpty=()=>({type:"empty",get representation(){return"(비어있음)"}}),t.wrapReturnValue=e=>({type:"return value",value:e})},673:function(e,t,r){var a=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.Environment=void 0;const n=r(290),i=a(r(404));t.default=class{evaluate(e,t){return this.evaluateProgram(e,t)}evaluateProgram(e,t){const{statements:r}=e;if(0===r.length)return(0,n.makeEvaluatedEmpty)();for(let e=0;e"===r)return(0,n.makeEvaluatedBoolean)(a.value>i.value);if("<"===r)return(0,n.makeEvaluatedBoolean)(a.value="===r)return(0,n.makeEvaluatedBoolean)(a.value>=i.value);if("<="===r)return(0,n.makeEvaluatedBoolean)(a.value<=i.value)}if("number"===a.type&&"number"===i.type){if("+"===r)return(0,n.makeEvaluatedNumber)(a.value+i.value);if("-"===r)return(0,n.makeEvaluatedNumber)(a.value-i.value);if("*"===r)return(0,n.makeEvaluatedNumber)(a.value*i.value);if("/"===r)return(0,n.makeEvaluatedNumber)(a.value/i.value);throw new Error(`bad infix ${r} for number operands`)}throw new Error(`bad infix ${r}, with left '${a}' and right '${i}'`)}parseCallArguments(e,t){const r=[];for(const a of e){const e=this.evaluateExpression(a,t);r.push(e)}return r}evaluateFunctionCall(e,t){const r=new i.default(e.environment);for(let a=0;a{const t=new u.default(e),r=new o.default(t).parseProgram(),a=new l.default,n=new l.Environment,i=a.evaluate(r,n);return String(i.representation)}},197:function(e,t,r){var a=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const n=a(r(13));class i{constructor(e){this.reader=new n.default(e,i.END_OF_INPUT)}pop(){const e=this.reader.read();return this.reader.next(),e}peek(){return this.reader.read()}}i.END_OF_INPUT="\0",t.default=i},13:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.default=class{constructor(e,t){this.chars=e,this.fallbackChar=t,this.index=0}read(){return this.index===this.chars.length?this.fallbackChar:this.chars[this.index]}next(){this.index!==this.chars.length&&this.index++}}},439:function(e,t,r){var a=this&&this.__createBinding||(Object.create?function(e,t,r,a){void 0===a&&(a=r);var n=Object.getOwnPropertyDescriptor(t,r);n&&!("get"in n?!t.__esModule:n.writable||n.configurable)||(n={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,a,n)}:function(e,t,r,a){void 0===a&&(a=r),e[a]=t[r]}),n=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),i=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r in e)"default"!==r&&Object.prototype.hasOwnProperty.call(e,r)&&a(t,e,r);return n(t,e),t},s=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const u=s(r(197)),o=i(r(632)),l=i(r(352));t.default=class{constructor(e){this.charBuffer=new u.default(e)}getToken(){this.skipWhitespaces();const e=this.charBuffer.peek();switch(e){case"+":case"-":case"*":case"/":{const e=this.charBuffer.pop();return o.operator(e)}case"(":case")":{const e=this.charBuffer.pop();return o.groupDelimiter(e)}case"{":case"}":{const e=this.charBuffer.pop();return o.blockDelimiter(e)}case",":{const e=this.charBuffer.pop();return o.separator(e)}case"!":{this.charBuffer.pop();const e=this.readOperatorStartingWithBang();return o.operator(e)}case"=":{this.charBuffer.pop();const e=this.readOperatorStartingWithEqual();return o.operator(e)}case">":{this.charBuffer.pop();const e=this.readOperatorStartingWithGreaterThan();return o.operator(e)}case"<":{this.charBuffer.pop();const e=this.readOperatorStartingWithLessThan();return o.operator(e)}case"'":{this.charBuffer.pop();const[e,t]=this.readStringLiteral();return t?o.stringLiteral(e):o.illegal("'"+e)}case u.default.END_OF_INPUT:return o.end;default:if(l.isDigit(e)){const e=this.readNumberLiteral();return o.numberLiteral(e)}if(l.isLetter(e)){const e=this.readLettersAndDigits();return"참"===e||"거짓"===e?o.booleanLiteral(e):"만약"===e||"아니면"===e||"함수"===e||"결과"===e?o.keyword(e):o.identifier(e)}return this.charBuffer.pop(),o.illegal(e)}}skipWhitespaces(){for(;l.isWhitespace(this.charBuffer.peek());)this.charBuffer.pop()}readOperatorStartingWithBang(){return"="===this.charBuffer.peek()?(this.charBuffer.pop(),"!="):"!"}readOperatorStartingWithEqual(){return"="===this.charBuffer.peek()?(this.charBuffer.pop(),"=="):"="}readOperatorStartingWithGreaterThan(){return"="===this.charBuffer.peek()?(this.charBuffer.pop(),">="):">"}readOperatorStartingWithLessThan(){return"="===this.charBuffer.peek()?(this.charBuffer.pop(),"<="):"<"}readStringLiteral(){const e=[];for(;;){const t=this.charBuffer.pop();if("'"===t||t===u.default.END_OF_INPUT)return[e.join(""),"'"===t];e.push(t)}}readNumberLiteral(){return this.readDigits()+this.readDecimalPart()}readDigits(){const e=[];for(;l.isDigit(this.charBuffer.peek());)e.push(this.charBuffer.pop());return e.join("")}readDecimalPart(){return"."!==this.charBuffer.peek()?"":this.charBuffer.pop()+this.readDigits()}readLettersAndDigits(){const e=[];for(;l.isLetter(this.charBuffer.peek())||l.isDigit(this.charBuffer.peek());)e.push(this.charBuffer.pop());return e.join("")}}},632:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.end=t.illegal=t.keyword=t.separator=t.blockDelimiter=t.groupDelimiter=t.stringLiteral=t.booleanLiteral=t.numberLiteral=t.identifier=t.operator=t.END_VALUE=void 0,t.END_VALUE="$end",t.operator=e=>({type:"operator",value:e}),t.identifier=e=>({type:"identifier",value:e}),t.numberLiteral=e=>({type:"number literal",value:e}),t.booleanLiteral=e=>({type:"boolean literal",value:e}),t.stringLiteral=e=>({type:"string literal",value:e}),t.groupDelimiter=e=>({type:"group delimiter",value:e}),t.blockDelimiter=e=>({type:"block delimiter",value:e}),t.separator=e=>({type:"separator",value:e}),t.keyword=e=>({type:"keyword",value:e}),t.illegal=e=>({type:"illegal",value:e}),t.end={type:"end",value:t.END_VALUE}},352:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.isWhitespace=t.isDigit=t.isLetter=void 0,t.isLetter=e=>1===e.length&&/^[a-zA-Z가-힣_]$/.test(e),t.isDigit=e=>1===e.length&&/^[0-9]$/.test(e),t.isWhitespace=e=>1===e.length&&/^[ \t\r\n]$/.test(e)},522:function(e,t,r){var a=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const n=r(763),i=a(r(461)),s=e=>{switch(e){case"=":return 30;case"==":case"!=":case">":case"<":case">=":case"<=":return 40;case"+":case"-":return 50;case"*":case"/":return 60;case"(":return 80;default:return 0}};t.default=class{constructor(e){this.buffer=new i.default(e)}parseProgram(){const e=(0,n.makeProgram)();for(;!this.buffer.isEnd();){const t=this.parseStatement();null!==t&&e.statements.push(t)}return e}parseBlock(){const e=this.buffer.read();if("block delimiter"!==e.type||"{"!==e.value)throw new Error(`expected { but received ${e.type}`);this.buffer.next();const t=[];for(;;){const e=this.buffer.read();if("block delimiter"===e.type&&"}"===e.value){this.buffer.next();break}const r=this.parseStatement();null!==r&&t.push(r)}return(0,n.makeBlock)(t)}parseStatement(){const e=this.buffer.read();return"keyword"===e.type&&"만약"===e.value?(this.buffer.next(),this.parseBranchStatement()):"keyword"===e.type&&"결과"===e.value?(this.buffer.next(),this.parseReturnStatement()):this.parseExpressionStatement()}parseBranchStatement(){const e=this.parseExpression(0),t=this.parseBlock(),r=this.buffer.read();if("keyword"!==r.type||"아니면"!==r.value)return(0,n.makeBranchStatement)(e,t);this.buffer.next();const a=this.parseBlock();return(0,n.makeBranchStatement)(e,t,a)}parseReturnStatement(){const e=this.parseExpression(0);return(0,n.makeReturnStatement)(e)}parseExpressionStatement(){const e=this.parseExpression(0);return(0,n.makeExpressionStatement)(e)}parseExpression(e){let t=this.parsePrefixExpression();for(;!(s(this.buffer.read().value)<=e);){const e=this.parseInfixExpression(t);if(null===e)break;t=e}return t}parsePrefixExpression(){const e=this.buffer.read();if(this.buffer.next(),"number literal"===e.type)return this.parseNumberLiteral(e.value);if("boolean literal"===e.type)return this.parseBooleanLiteral(e.value);if("string literal"===e.type)return this.parseStringLiteral(e.value);if("identifier"===e.type)return(0,n.makeIdentifier)(e.value);if("operator"===e.type&&("+"===e.value||"-"===e.value||"!"===e.value)){const t=this.parseExpression(70),r=e.value;return(0,n.makePrefixExpression)(r,t)}if("keyword"===e.type&&"함수"===e.value){const e=this.parseParameters(),t=this.parseBlock();return(0,n.makeFunctionExpression)(t,e)}if("group delimiter"===e.type&&"("===e.value){const e=this.parseExpression(0),t=this.buffer.read();if(this.buffer.next(),"group delimiter"!==t.type||")"!==t.value)throw new Error(`expected ) but received ${t.type}`);return e}throw new Error(`bad token type ${e.type} (${e.value}) for prefix expression`)}parseParameters(){const e=[],t=this.buffer.read();if("group delimiter"!==t.type||"("!==t.value)throw new Error(`expected ( but received ${t.type}`);this.buffer.next();const r=this.buffer.read();if(this.buffer.next(),"group delimiter"===r.type&&")"===r.value)return[];const a=r;if("identifier"!==a.type)throw new Error(`expected identifier but received ${a}`);const n=a;for(e.push(n);;){const t=this.buffer.read();if(this.buffer.next(),"group delimiter"===t.type&&")"===t.value)break;const r=t;if("separator"!==r.type)throw new Error(`expected comma but received ${r}`);const a=this.buffer.read();if(this.buffer.next(),"identifier"!==a.type)throw new Error(`expected identifier but received ${a}`);const n=a;e.push(n)}return e}parseInfixExpression(e){const t=this.buffer.read();if("group delimiter"===t.type&&"("===t.value)return"function expression"!==e.type&&"identifier"!==e.type?null:(this.buffer.next(),this.parseCall(e));if("operator"!==t.type)return null;const r=t.value;return"="===r&&"identifier"===e.type?(this.buffer.next(),this.parseAssignment(e)):"+"===r||"-"===r||"*"===r||"/"===r||"!="===r||"=="===r||">"===r||"<"===r||">="===r||"<="===r?(this.buffer.next(),this.parseArithmeticInfixExpression(e,r)):null}parseCall(e){const t=this.parseCallArguments();return(0,n.makeCall)(e,t)}parseCallArguments(){const e=this.buffer.read();if("group delimiter"===e.type&&")"===e.value)return this.buffer.next(),[];const t=[this.parseExpression(0)];for(;"separator"===this.buffer.read().type;){this.buffer.next();const e=this.parseExpression(0);t.push(e)}const r=this.buffer.read();if(this.buffer.next(),"group delimiter"!==r.type||")"!==r.value)throw new Error(`expect ) but received ${r.type}`);return t}parseAssignment(e){const t=s("="),r=this.parseExpression(t);return(0,n.makeAssignment)(e,r)}parseArithmeticInfixExpression(e,t){const r=s(t),a=this.parseExpression(r);return(0,n.makeInfixExpression)(t,e,a)}parseNumberLiteral(e){const t=Number(e);if(Number.isNaN(t))throw new Error(`expected non-NaN number, but received '${e}'`);return(0,n.makeNumberNode)(t)}parseBooleanLiteral(e){const t="참"===e;return(0,n.makeBooleanNode)(t)}parseStringLiteral(e){return(0,n.makeStringNode)(e)}}},813:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.makeAssignment=t.makeCall=t.makeFunctionExpression=t.makeInfixExpression=t.makePrefixExpression=t.makeStringNode=t.makeBooleanNode=t.makeNumberNode=t.makeIdentifier=void 0,t.makeIdentifier=e=>({type:"identifier",value:e}),t.makeNumberNode=e=>({type:"number node",value:e}),t.makeBooleanNode=e=>({type:"boolean node",value:e}),t.makeStringNode=e=>({type:"string node",value:e}),t.makePrefixExpression=(e,t)=>({type:"prefix expression",prefix:e,expression:t}),t.makeInfixExpression=(e,t,r)=>({type:"infix expression",infix:e,left:t,right:r}),t.makeFunctionExpression=(e,t=[])=>({type:"function expression",parameter:t,body:e}),t.makeCall=(e,t)=>({type:"call",functionToCall:e,callArguments:t}),t.makeAssignment=(e,t)=>({type:"assignment",left:e,right:t})},54:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.makeBlock=t.makeProgram=void 0,t.makeProgram=(e=[])=>({type:"program",statements:e}),t.makeBlock=(e=[])=>({type:"block",statements:e})},763:function(e,t,r){var a=this&&this.__createBinding||(Object.create?function(e,t,r,a){void 0===a&&(a=r);var n=Object.getOwnPropertyDescriptor(t,r);n&&!("get"in n?!t.__esModule:n.writable||n.configurable)||(n={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,a,n)}:function(e,t,r,a){void 0===a&&(a=r),e[a]=t[r]}),n=this&&this.__exportStar||function(e,t){for(var r in e)"default"===r||Object.prototype.hasOwnProperty.call(t,r)||a(t,e,r)};Object.defineProperty(t,"__esModule",{value:!0}),n(r(813),t),n(r(602),t),n(r(54),t)},602:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.makeExpressionStatement=t.makeReturnStatement=t.makeBranchStatement=void 0,t.makeBranchStatement=(e,t,r)=>({type:"branch statement",predicate:e,consequence:t,alternative:r}),t.makeReturnStatement=e=>({type:"return statement",expression:e}),t.makeExpressionStatement=e=>({type:"expression statement",expression:e})},461:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.default=class{constructor(e){this.lexer=e,this.token=e.getToken()}isEnd(){return"end"===this.token.type}read(){return this.token}next(){this.token=this.lexer.getToken()}}}},t={},r=function r(a){var n=t[a];if(void 0!==n)return n.exports;var i=t[a]={exports:{}};return e[a].call(i.exports,i,i.exports,r),i.exports}(436);window.kal=r})(); \ No newline at end of file +(()=>{"use strict";var e={404:(e,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.default=class{constructor(e=null){this.superEnvironment=e,this.table=new Map}get(e){const r=this.table.get(e);return void 0!==r?r:null===this.superEnvironment?null:this.superEnvironment.get(e)}set(e,r){this.table.set(e,r)}}},673:function(e,r,t){var a=this&&this.__createBinding||(Object.create?function(e,r,t,a){void 0===a&&(a=t);var n=Object.getOwnPropertyDescriptor(r,t);n&&!("get"in n?!r.__esModule:n.writable||n.configurable)||(n={enumerable:!0,get:function(){return r[t]}}),Object.defineProperty(e,a,n)}:function(e,r,t,a){void 0===a&&(a=t),e[a]=r[t]}),n=this&&this.__setModuleDefault||(Object.create?function(e,r){Object.defineProperty(e,"default",{enumerable:!0,value:r})}:function(e,r){e.default=r}),i=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var r={};if(null!=e)for(var t in e)"default"!==t&&Object.prototype.hasOwnProperty.call(e,t)&&a(r,e,t);return n(r,e),r},o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(r,"__esModule",{value:!0}),r.Environment=r.BadIdentifierError=r.BadInfixExpressionError=r.BadPrefixExpressionError=r.BadAssignmentLeftError=r.BadPredicateError=r.TopLevelReturnError=r.EvalError=void 0;const s=i(t(994)),c=o(t(404));class u extends Error{constructor(e,r){super(),this.range=e,this.received=r}}r.EvalError=u;class l extends u{}r.TopLevelReturnError=l;class d extends u{}r.BadPredicateError=d;class p extends u{}r.BadAssignmentLeftError=p;class h extends u{}r.BadPrefixExpressionError=h;class f extends u{}r.BadInfixExpressionError=f;class g extends u{}r.BadIdentifierError=g,r.default=class{evaluate(e,r){return this.evaluateProgram(e,r)}evaluateProgram(e,r){const{statements:t}=e;let a=null;for(let n=0;nthis.evaluateExpression(e,r)))}evaluateFunctionCall(e,r){const t=this.createExtendedEnvironment(e.environment,e.parameters,r),a=this.evaluateBlock(e.body,t);if("return"!==a.type)throw new Error("expected return value in function but it didn't");return a.value}getBooleanComparisonInfixOperationValue(e,r,t){return this.getComparisonInfixOperationValue(e,r,t)}getNumericComparisonInfixOperationValue(e,r,t){return this.getComparisonInfixOperationValue(e,r,t)}getStringComparisonInfixOperationValue(e,r,t){return this.getComparisonInfixOperationValue(e,r,t)}getComparisonInfixOperationValue(e,r,t){return"=="===t?e===r:"!="===t?e!==r:">"===t?e>r:"<"===t?e="===t?e>=r:"<="===t?e<=r:t}getArithmeticInfixOperationValue(e,r,t){return"+"===t?e+r:"-"===t?e-r:"*"===t?e*r:"/"===t?e/r:t}evaluatePrefixNumberExpression(e,r){return"+"===e?this.createNumberValue(r.value,r.range):"-"===e?this.createNumberValue(-r.value,r.range):e}evaluatePrefixBooleanExpression(e,r){return"!"===e?this.createBooleanValue(!r.value,r.range):e}createExtendedEnvironment(e,r,t){const a=new c.default(e);for(let e=0;er===e))}isComparisonInfixOperator(e){return["==","!=",">","<",">=","<="].some((r=>r===e))}};var v=t(404);Object.defineProperty(r,"Environment",{enumerable:!0,get:function(){return o(v).default}})},994:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createReturnValue=r.createFunctionValue=r.createEmptyValue=r.createStringValue=r.createBooleanValue=r.createNumberValue=void 0;const a=t(548),n=e=>(r,t,n)=>Object.assign({type:e,range:(0,a.copyRange)(n.begin,n.end),representation:t},r);r.createNumberValue=n("number"),r.createBooleanValue=n("boolean"),r.createStringValue=n("string"),r.createEmptyValue=n("empty"),r.createFunctionValue=n("function"),r.createReturnValue=e=>({type:"return",value:e})},436:function(e,r,t){var a=this&&this.__createBinding||(Object.create?function(e,r,t,a){void 0===a&&(a=t);var n=Object.getOwnPropertyDescriptor(r,t);n&&!("get"in n?!r.__esModule:n.writable||n.configurable)||(n={enumerable:!0,get:function(){return r[t]}}),Object.defineProperty(e,a,n)}:function(e,r,t,a){void 0===a&&(a=t),e[a]=r[t]}),n=this&&this.__setModuleDefault||(Object.create?function(e,r){Object.defineProperty(e,"default",{enumerable:!0,value:r})}:function(e,r){e.default=r}),i=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var r={};if(null!=e)for(var t in e)"default"!==t&&Object.prototype.hasOwnProperty.call(e,t)&&a(r,e,t);return n(r,e),r},o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(r,"__esModule",{value:!0}),r.execute=void 0;const s=o(t(439)),c=o(t(522)),u=i(t(673));r.execute=e=>{const r=new s.default(e),t=new c.default(r).parseSource(),a=new u.default,n=new u.Environment,i=a.evaluate(t,n);return String(i.representation)}},545:(e,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.default=class{constructor(e,r){this.chars=e,this.fallbackChar=r,this.index=0,this.row=0,this.col=0}readChar(){if(this.index===this.chars.length)return{value:this.fallbackChar,position:{row:this.row,col:this.col}};const e=this.peekNewLine();return null!==e?{value:e,position:{row:this.row,col:this.col}}:{value:this.chars[this.index],position:{row:this.row,col:this.col}}}advance(){if(this.index===this.chars.length)return;const e=this.peekNewLine();if(null!==e)return this.index+=e.length,++this.row,void(this.col=0);++this.index,++this.col}peekNewLine(){if(this.index===this.chars.length)return null;const e=this.chars[this.index];if("\r"!==e&&"\n"!==e)return null;if(this.index+1===this.chars.length)return e;const r=this.chars[this.index+1];return"\r"!==r&&"\n"!==r?e:e+r}}},197:function(e,r,t){var a=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(r,"__esModule",{value:!0});const n=a(t(545));class i{constructor(e){this.reader=new n.default(e,i.END_OF_INPUT)}popChar(){const e=this.reader.readChar();return this.reader.advance(),e}peekChar(){return this.reader.readChar()}}i.END_OF_INPUT="\0",r.default=i},439:function(e,r,t){var a=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(r,"__esModule",{value:!0});const n=a(t(197)),i=t(562),o=t(352);r.default=class{constructor(e){this.charBuffer=new n.default(e)}getSourceToken(){this.skipWhitespaceChars();const e=this.charBuffer.peekChar();switch(e.value){case"+":case"-":case"*":case"/":{const{position:r}=this.charBuffer.popChar(),t=e.value;return(0,i.createOperatorToken)(t,r,r)}case"(":case")":{const{position:r}=this.charBuffer.popChar(),t=e.value;return(0,i.createGroupDelimiterToken)(t,r,r)}case"{":case"}":{const{position:r}=this.charBuffer.popChar(),t=e.value;return(0,i.createBlockDelimiterToken)(t,r,r)}case",":{const{position:r}=this.charBuffer.popChar(),t=e.value;return(0,i.createSeparatorToken)(t,r,r)}case"!":{const{position:e}=this.charBuffer.popChar();return this.lexCharsStartingWithBang(e)}case"=":{const{position:e}=this.charBuffer.popChar();return this.lexCharsStartingWithEqual(e)}case">":{const{position:e}=this.charBuffer.popChar();return this.lexCharsStartingWithGreaterThan(e)}case"<":{const{position:e}=this.charBuffer.popChar();return this.lexCharsStartingWithLessThan(e)}case"'":{const{position:e}=this.charBuffer.popChar();return this.lexCharsStartingWithSingleQuote(e)}case n.default.END_OF_INPUT:{const{position:e}=this.charBuffer.popChar();return(0,i.createEndToken)("$end",e,e)}default:{if((0,o.isDigit)(e.value))return this.lexNumberLiteral();if((0,o.isLetter)(e.value))return this.lexLetters();const{position:r}=this.charBuffer.popChar();return(0,i.createIllegalToken)(e.value,r,r)}}}skipWhitespaceChars(){for(;;){const e=this.charBuffer.peekChar();if(!(0,o.isWhitespace)(e.value))break;this.charBuffer.popChar()}}lexCharsStartingWithBang(e){if("="===this.charBuffer.peekChar().value){const{position:r}=this.charBuffer.popChar();return(0,i.createOperatorToken)("!=",e,r)}return(0,i.createOperatorToken)("!",e,e)}lexCharsStartingWithEqual(e){if("="===this.charBuffer.peekChar().value){const{position:r}=this.charBuffer.popChar();return(0,i.createOperatorToken)("==",e,r)}return(0,i.createOperatorToken)("=",e,e)}lexCharsStartingWithGreaterThan(e){if("="===this.charBuffer.peekChar().value){const{position:r}=this.charBuffer.popChar();return(0,i.createOperatorToken)(">=",e,r)}return(0,i.createOperatorToken)(">",e,e)}lexCharsStartingWithLessThan(e){if("="===this.charBuffer.peekChar().value){const{position:r}=this.charBuffer.popChar();return(0,i.createOperatorToken)("<=",e,r)}return(0,i.createOperatorToken)("<",e,e)}lexCharsStartingWithSingleQuote(e){const r=[];for(;;){const t=this.charBuffer.popChar(),a=r.map((e=>e.value)).join(""),o=e,s=t.position;if("'"===t.value)return(0,i.createStringLiteralToken)(a,o,s);if(t.value===n.default.END_OF_INPUT)return(0,i.createIllegalStringLiteralToken)(a,o,s);r.push(t)}}lexNumberLiteral(){const e=this.readDigitChars(),r=this.readDecimalChars(),t=e.concat(r),a=t.map((e=>e.value)).join(""),n=t[0].position,o=t[t.length-1].position;return(0,i.createNumberLiteralToken)(a,n,o)}lexLetters(){const e=this.readLetterChars(),r=e.map((e=>e.value)).join(""),t=e[0].position,a=e[e.length-1].position;switch(r){case"참":case"거짓":return(0,i.createBooleanLiteralToken)(r,t,a);case"만약":case"아니면":case"함수":case"결과":return(0,i.createKeywordToken)(r,t,a);default:return(0,i.createIdentifierToken)(r,t,a)}}readDigitChars(){const e=[];for(;;){const r=this.charBuffer.peekChar();if(!(0,o.isDigit)(r.value))break;e.push(this.charBuffer.popChar())}return e}readDecimalChars(){if("."!==this.charBuffer.peekChar().value)return[];const e=this.charBuffer.popChar(),r=this.readDigitChars();return[e].concat(r)}readLetterChars(){const e=[];for(;;){const r=this.charBuffer.peekChar();if(!(0,o.isLetter)(r.value)&&!(0,o.isDigit)(r.value))break;e.push(this.charBuffer.popChar())}return e}}},854:(e,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createTokenCreator=void 0,r.createTokenCreator=function(e){return function(r,t,a){return void 0!==a?{type:e,value:r,range:{begin:t,end:a}}:{type:e,value:r,range:t}}}},287:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createSeparatorToken=r.createBlockDelimiterToken=r.createGroupDelimiterToken=void 0;const a=t(854);r.createGroupDelimiterToken=(0,a.createTokenCreator)("group delimiter"),r.createBlockDelimiterToken=(0,a.createTokenCreator)("block delimiter"),r.createSeparatorToken=(0,a.createTokenCreator)("separator")},762:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createKeywordToken=r.createIdentifierToken=void 0;const a=t(854);r.createIdentifierToken=(0,a.createTokenCreator)("identifier"),r.createKeywordToken=(0,a.createTokenCreator)("keyword")},562:function(e,r,t){var a=this&&this.__createBinding||(Object.create?function(e,r,t,a){void 0===a&&(a=t);var n=Object.getOwnPropertyDescriptor(r,t);n&&!("get"in n?!r.__esModule:n.writable||n.configurable)||(n={enumerable:!0,get:function(){return r[t]}}),Object.defineProperty(e,a,n)}:function(e,r,t,a){void 0===a&&(a=t),e[a]=r[t]}),n=this&&this.__exportStar||function(e,r){for(var t in e)"default"===t||Object.prototype.hasOwnProperty.call(r,t)||a(r,e,t)};Object.defineProperty(r,"__esModule",{value:!0}),n(t(547),r),n(t(762),r),n(t(768),r),n(t(287),r),n(t(763),r)},768:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createStringLiteralToken=r.createBooleanLiteralToken=r.createNumberLiteralToken=void 0;const a=t(854);r.createNumberLiteralToken=(0,a.createTokenCreator)("number literal"),r.createBooleanLiteralToken=(0,a.createTokenCreator)("boolean literal"),r.createStringLiteralToken=(0,a.createTokenCreator)("string literal")},547:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createOperatorToken=void 0;const a=t(854);r.createOperatorToken=(0,a.createTokenCreator)("operator")},763:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createEndToken=r.createIllegalStringLiteralToken=r.createIllegalToken=r.END_VALUE=void 0;const a=t(854);r.END_VALUE="$end",r.createIllegalToken=(0,a.createTokenCreator)("illegal"),r.createIllegalStringLiteralToken=(0,a.createTokenCreator)("illegal string"),r.createEndToken=(0,a.createTokenCreator)("end")},352:(e,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.isWhitespace=r.isDigit=r.isLetter=void 0,r.isLetter=e=>1===e.length&&/^[a-zA-Z가-힣_]$/.test(e),r.isDigit=e=>1===e.length&&/^[0-9]$/.test(e),r.isWhitespace=e=>!(e.length>2)&&/^(\r\n|[ \t\r\n])$/.test(e)},817:(e,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.getInfixBindingPower=r.bindingPowers=void 0,r.bindingPowers={lowest:{left:0,right:1},assignment:{left:31,right:30},comparison:{left:41,right:40},summative:{left:50,right:51},productive:{left:60,right:61},prefix:{left:70,right:71},call:{left:80,right:81}},r.getInfixBindingPower=e=>{switch(e){case"=":return r.bindingPowers.assignment;case"==":case"!=":case">":case"<":case">=":case"<=":return r.bindingPowers.comparison;case"+":case"-":return r.bindingPowers.summative;case"*":case"/":return r.bindingPowers.productive;case"(":return r.bindingPowers.call;default:return r.bindingPowers.lowest}}},522:function(e,r,t){var a=this&&this.__createBinding||(Object.create?function(e,r,t,a){void 0===a&&(a=t);var n=Object.getOwnPropertyDescriptor(r,t);n&&!("get"in n?!r.__esModule:n.writable||n.configurable)||(n={enumerable:!0,get:function(){return r[t]}}),Object.defineProperty(e,a,n)}:function(e,r,t,a){void 0===a&&(a=t),e[a]=r[t]}),n=this&&this.__setModuleDefault||(Object.create?function(e,r){Object.defineProperty(e,"default",{enumerable:!0,value:r})}:function(e,r){e.default=r}),i=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var r={};if(null!=e)for(var t in e)"default"!==t&&Object.prototype.hasOwnProperty.call(e,t)&&a(r,e,t);return n(r,e),r},o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(r,"__esModule",{value:!0}),r.BadSeparatorError=r.BadIdentifierError=r.BadFunctionKeywordError=r.BadAssignmentError=r.BadBlockDelimiterError=r.BadGroupDelimiterError=r.BadExpressionError=r.BadInfixError=r.BadPrefixError=r.BadBooleanLiteralError=r.BadNumberLiteralError=r.ParserError=void 0;const s=i(t(39)),c=t(817),u=t(548);class l extends Error{constructor(e,r,t){super(),this.received=e,this.expected=r,this.range=t}}r.ParserError=l;class d extends l{}r.BadNumberLiteralError=d;class p extends l{}r.BadBooleanLiteralError=p;class h extends l{}r.BadPrefixError=h;class f extends l{}r.BadInfixError=f;class g extends l{}r.BadExpressionError=g;class v extends l{}r.BadGroupDelimiterError=v;class x extends l{}r.BadBlockDelimiterError=x;class m extends l{}r.BadAssignmentError=m;class b extends l{}r.BadFunctionKeywordError=b;class y extends l{}r.BadIdentifierError=y;class E extends l{}r.BadSeparatorError=E;const w=o(t(405));class _{constructor(e){this.reader=new w.default(e)}parseSource(){const e=[];for(;!this.reader.isEnd();)e.push(this.parseStatement());const r={row:0,col:0},t=e.length>0?e[0].range.begin:r,a=e.length>0?e[e.length-1].range.end:r;return s.createProgramNode({statements:e},t,a)}parseBlock(){const e=this.reader.read();this.advanceOrThrow("block delimiter","{",x);const r=[];for(;;){const t=this.reader.read();if("block delimiter"===t.type&&"}"===t.value){this.reader.advance();const a=(0,u.copyRange)(e.range.begin,t.range.end);return s.createBlockNode({statements:r},a)}const a=this.parseStatement();r.push(a)}}parseStatement(){const e=this.reader.read(),{type:r,value:t}=e;return"keyword"===r&&"만약"===t?this.parseBranchStatement():"keyword"===r&&"결과"===t?this.parseReturnStatement():this.parseExpressionStatement()}parseBranchStatement(){const e=this.reader.read();this.reader.advance();const r=this.parseExpression(c.bindingPowers.lowest),t=this.parseBlock(),a=this.reader.read();if("keyword"!==a.type||"아니면"!==a.value){const a={begin:e.range.begin,end:t.range.end};return s.createBranchNode({predicate:r,consequence:t},a)}this.reader.advance();const n=this.parseBlock(),i={begin:e.range.begin,end:n.range.end};return s.createBranchNode({predicate:r,consequence:t,alternative:n},i)}parseReturnStatement(){const e=this.reader.read();this.reader.advance();const r=this.parseExpression(c.bindingPowers.lowest),t={begin:e.range.begin,end:r.range.end};return s.createReturnNode({expression:r},t)}parseExpressionStatement(){const e=this.parseExpression(c.bindingPowers.lowest),r=e.range;return s.createExpressionStatementNode({expression:e},r)}parseExpression(e){let r=this.parseExpressionStart();for(;!((0,c.getInfixBindingPower)(this.reader.read().value).left<=e.right);){const e=this.parseExpressionMiddle(r);if(null===e)break;r=e}return r}parseExpressionStart(){const{type:e,value:r,range:t}=this.reader.read();if("number literal"===e)return this.parseNumberLiteral();if("boolean literal"===e)return this.parseBooleanLiteral();if("string literal"===e)return this.parseStringLiteral();if("identifier"===e)return this.parseIdentifier();if("operator"===e&&this.isPrefixOperator(r))return this.parsePrefix();if("keyword"===e&&"함수"===r)return this.parseFunction();if("group delimiter"===e&&"("===r)return this.parseGroupedExpression();throw new g(e,"expression",t)}parseExpressionMiddle(e){const{type:r,value:t}=this.reader.read();return"group delimiter"===r&&"("===t?"function"!==e.type&&"identifier"!==e.type?null:this.parseCall(e):"operator"===r&&this.isInfixOperator(t)?this.parseInfix(e):"operator"===r&&"="===t&&"identifier"===e.type?this.parseAssignment(e):null}parseCall(e){this.advanceOrThrow("group delimiter","(",v);const r=this.reader.read();if("group delimiter"===r.type&&")"===r.value){this.reader.advance();const t=(0,u.copyRange)(e.range.begin,r.range.end);return s.createCallNode({func:e,args:[]},t)}const t=[this.parseExpression(c.bindingPowers.lowest)];for(;"separator"===this.reader.read().type;)this.reader.advance(),t.push(this.parseExpression(c.bindingPowers.lowest));const a=this.reader.read();this.advanceOrThrow("group delimiter",")",v);const n=(0,u.copyRange)(e.range.begin,a.range.end);return s.createCallNode({func:e,args:t},n)}parseAssignment(e){const{value:r,range:t}=this.reader.read();if(this.reader.advance(),"="!==r)throw new m(r,"=",t);const a=r,n=(0,c.getInfixBindingPower)(a),i=this.parseExpression(n),o={begin:e.range.begin,end:i.range.end};return s.createAssignmentNode({left:e,right:i},o)}parseNumberLiteral(){const{value:e,range:r}=this.reader.read();this.reader.advance();const t=Number(e);if(Number.isNaN(t))throw new d(e,"non NaN",r);return s.createNumberNode({value:t},r)}parseBooleanLiteral(){const{value:e,range:r}=this.reader.read();let t;if(this.reader.advance(),"참"===e)t=!0;else{if("거짓"!==e)throw new p(e,"참, 거짓",r);t=!1}return s.createBooleanNode({value:t},r)}parseStringLiteral(){const{value:e,range:r}=this.reader.read();return this.reader.advance(),s.createStringNode({value:e},r)}parseIdentifier(){const{type:e,value:r,range:t}=this.reader.read();if(this.reader.advance(),"identifier"!==e)throw new y(e,"identifier",t);return s.createIdentifierNode({value:r},t)}parsePrefix(){const{value:e,range:r}=this.reader.read();if(this.reader.advance(),!this.isPrefixOperator(e))throw new h(e,"prefix operator",r);const t=e,a=this.parseExpression(c.bindingPowers.prefix);return s.createPrefixNode({prefix:t,right:a},r)}parseInfix(e){const{value:r,range:t}=this.reader.read();if(this.reader.advance(),!this.isInfixOperator(r))throw new f(r,"infix operator",t);const a=r,n=(0,c.getInfixBindingPower)(a),i=this.parseExpression(n),o=(0,u.copyRange)(e.range.begin,i.range.end);return s.createInfixNode({infix:a,left:e,right:i},o)}parseFunction(){const e=this.reader.read();this.advanceOrThrow("keyword","함수",b);const r=this.parseParameters(),t=this.parseBlock(),a=(0,u.copyRange)(e.range.begin,t.range.end);return s.createFunctionNode({parameters:r,body:t},a)}parseParameters(){this.advanceOrThrow("group delimiter","(",v);const e=this.reader.read();if("group delimiter"===e.type&&")"===e.value)return this.reader.advance(),[];const r=[this.parseIdentifier()];for(;;){const e=this.reader.read();if(this.reader.advance(),"group delimiter"===e.type&&")"===e.value)return r;if("separator"!==e.type)throw new E(e.type,",",e.range);r.push(this.parseIdentifier())}}parseGroupedExpression(){this.advanceOrThrow("group delimiter","(",v);const e=this.parseExpression(c.bindingPowers.lowest);this.advanceOrThrow("group delimiter",")",v);const r=(0,u.copyRange)(e.range.begin,e.range.end,{begin:{row:0,col:-1},end:{row:0,col:1}});return Object.assign(Object.assign({},e),{range:r})}advanceOrThrow(e,r,t){const a=this.reader.read();if(this.reader.advance(),a.type!==e||a.value!==r)throw new t(a.value,r,a.range)}isPrefixOperator(e){return _.PREFIX_OPERATORS.some((r=>r===e))}isInfixOperator(e){return _.INFIX_OPERATORS.some((r=>r===e))}}_.PREFIX_OPERATORS=["+","-","!"],_.INFIX_OPERATORS=["+","-","*","/","!=","==",">","<",">=","<="],r.default=_},405:(e,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.default=class{constructor(e){this.lexer=e,this.token=e.getSourceToken()}read(){return this.token}advance(){this.token=this.lexer.getSourceToken()}isEnd(){return"end"===this.token.type}}},662:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createNodeCreator=void 0;const a=t(548);r.createNodeCreator=function(e){return function(r,t,n){if(void 0!==n)return Object.assign({type:e,range:(0,a.copyRange)(t,n)},r);const i=t;return Object.assign({type:e,range:(0,a.copyRange)(i.begin,i.end)},r)}}},878:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createAssignmentNode=r.createCallNode=r.createFunctionNode=r.createInfixNode=r.createPrefixNode=r.createStringNode=r.createBooleanNode=r.createNumberNode=r.createIdentifierNode=void 0;const a=t(662);r.createIdentifierNode=(0,a.createNodeCreator)("identifier"),r.createNumberNode=(0,a.createNodeCreator)("number"),r.createBooleanNode=(0,a.createNodeCreator)("boolean"),r.createStringNode=(0,a.createNodeCreator)("string"),r.createPrefixNode=(0,a.createNodeCreator)("prefix"),r.createInfixNode=(0,a.createNodeCreator)("infix"),r.createFunctionNode=(0,a.createNodeCreator)("function"),r.createCallNode=(0,a.createNodeCreator)("call"),r.createAssignmentNode=(0,a.createNodeCreator)("assignment")},701:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createBlockNode=r.createProgramNode=void 0;const a=t(662);r.createProgramNode=(0,a.createNodeCreator)("program"),r.createBlockNode=(0,a.createNodeCreator)("block")},39:function(e,r,t){var a=this&&this.__createBinding||(Object.create?function(e,r,t,a){void 0===a&&(a=t);var n=Object.getOwnPropertyDescriptor(r,t);n&&!("get"in n?!r.__esModule:n.writable||n.configurable)||(n={enumerable:!0,get:function(){return r[t]}}),Object.defineProperty(e,a,n)}:function(e,r,t,a){void 0===a&&(a=t),e[a]=r[t]}),n=this&&this.__exportStar||function(e,r){for(var t in e)"default"===t||Object.prototype.hasOwnProperty.call(r,t)||a(r,e,t)};Object.defineProperty(r,"__esModule",{value:!0}),n(t(701),r),n(t(234),r),n(t(878),r)},234:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createExpressionStatementNode=r.createReturnNode=r.createBranchNode=void 0;const a=t(662);r.createBranchNode=(0,a.createNodeCreator)("branch"),r.createReturnNode=(0,a.createNodeCreator)("return"),r.createExpressionStatementNode=(0,a.createNodeCreator)("expression statement")},548:(e,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.copyRange=r.copyPosition=void 0,r.copyPosition=(e,r)=>{var t,a;return{row:e.row+(null!==(t=null==r?void 0:r.row)&&void 0!==t?t:0),col:e.col+(null!==(a=null==r?void 0:r.col)&&void 0!==a?a:0)}},r.copyRange=(e,t,a)=>({begin:(0,r.copyPosition)(e,null==a?void 0:a.begin),end:(0,r.copyPosition)(t,null==a?void 0:a.end)})}},r={},t=function t(a){var n=r[a];if(void 0!==n)return n.exports;var i=r[a]={exports:{}};return e[a].call(i.exports,i,i.exports,t),i.exports}(436);window.kal=t})(); \ No newline at end of file diff --git a/package.json b/package.json index 0372888..f7064cb 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ ], "devDependencies": { "@types/jest": "^29.5.10", + "@types/node": "^20.11.6", "jest": "^29.7.0", "ts-jest": "^29.1.1", "typescript": "^5.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0087520..285005f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,12 @@ devDependencies: '@types/jest': specifier: ^29.5.10 version: 29.5.10 + '@types/node': + specifier: ^20.11.6 + version: 20.11.6 jest: specifier: ^29.7.0 - version: 29.7.0 + version: 29.7.0(@types/node@20.11.6) ts-jest: specifier: ^29.1.1 version: 29.1.1(@babel/core@7.23.5)(jest@29.7.0)(typescript@5.3.2) @@ -389,7 +392,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.10.3 + '@types/node': 20.11.6 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -410,14 +413,14 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.3 + '@types/node': 20.11.6 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.10.3) + jest-config: 29.7.0(@types/node@20.11.6) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -445,7 +448,7 @@ packages: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.3 + '@types/node': 20.11.6 jest-mock: 29.7.0 dev: true @@ -472,7 +475,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.10.3 + '@types/node': 20.11.6 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -505,7 +508,7 @@ packages: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.20 - '@types/node': 20.10.3 + '@types/node': 20.11.6 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -593,7 +596,7 @@ packages: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.10.3 + '@types/node': 20.11.6 '@types/yargs': 17.0.32 chalk: 4.1.2 dev: true @@ -701,7 +704,7 @@ packages: /@types/graceful-fs@4.1.9: resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} dependencies: - '@types/node': 20.10.3 + '@types/node': 20.11.6 dev: true /@types/istanbul-lib-coverage@2.0.6: @@ -731,8 +734,8 @@ packages: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true - /@types/node@20.10.3: - resolution: {integrity: sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==} + /@types/node@20.11.6: + resolution: {integrity: sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==} dependencies: undici-types: 5.26.5 dev: true @@ -1220,7 +1223,7 @@ packages: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} dev: true - /create-jest@29.7.0: + /create-jest@29.7.0(@types/node@20.11.6): resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -1229,7 +1232,7 @@ packages: chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.10.3) + jest-config: 29.7.0(@types/node@20.11.6) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -1680,7 +1683,7 @@ packages: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.3 + '@types/node': 20.11.6 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.1 @@ -1701,7 +1704,7 @@ packages: - supports-color dev: true - /jest-cli@29.7.0: + /jest-cli@29.7.0(@types/node@20.11.6): resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -1715,10 +1718,10 @@ packages: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0 + create-jest: 29.7.0(@types/node@20.11.6) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.10.3) + jest-config: 29.7.0(@types/node@20.11.6) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -1729,7 +1732,7 @@ packages: - ts-node dev: true - /jest-config@29.7.0(@types/node@20.10.3): + /jest-config@29.7.0(@types/node@20.11.6): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -1744,7 +1747,7 @@ packages: '@babel/core': 7.23.5 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.3 + '@types/node': 20.11.6 babel-jest: 29.7.0(@babel/core@7.23.5) chalk: 4.1.2 ci-info: 3.9.0 @@ -1804,7 +1807,7 @@ packages: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.3 + '@types/node': 20.11.6 jest-mock: 29.7.0 jest-util: 29.7.0 dev: true @@ -1820,7 +1823,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 20.10.3 + '@types/node': 20.11.6 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -1871,7 +1874,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.10.3 + '@types/node': 20.11.6 jest-util: 29.7.0 dev: true @@ -1926,7 +1929,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.3 + '@types/node': 20.11.6 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -1957,7 +1960,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.3 + '@types/node': 20.11.6 chalk: 4.1.2 cjs-module-lexer: 1.2.3 collect-v8-coverage: 1.0.2 @@ -2009,7 +2012,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.10.3 + '@types/node': 20.11.6 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -2034,7 +2037,7 @@ packages: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.3 + '@types/node': 20.11.6 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -2046,7 +2049,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.10.3 + '@types/node': 20.11.6 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true @@ -2055,13 +2058,13 @@ packages: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 20.10.3 + '@types/node': 20.11.6 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true - /jest@29.7.0: + /jest@29.7.0(@types/node@20.11.6): resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -2074,7 +2077,7 @@ packages: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0 + jest-cli: 29.7.0(@types/node@20.11.6) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -2657,7 +2660,7 @@ packages: '@babel/core': 7.23.5 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0 + jest: 29.7.0(@types/node@20.11.6) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 diff --git a/src/evaluator/environment/index.test.ts b/src/evaluator/environment/index.test.ts index 91741c3..bd145a8 100644 --- a/src/evaluator/environment/index.test.ts +++ b/src/evaluator/environment/index.test.ts @@ -1,11 +1,11 @@ -import type { Evaluated } from "../evaluated"; +import type { Value } from "../value"; import Environment from "./"; describe("set()", () => { it("set name and value", () => { const env = new Environment(); const varName = "foo"; - const varValue = {} as Evaluated; + const varValue = {} as Value; expect(() => env.set(varName, varValue)).not.toThrow(); }); @@ -15,7 +15,7 @@ describe("get()", () => { it("get value after setting the value", () => { const env = new Environment(); const varName = "foo"; - const varValue = {} as Evaluated; + const varValue = {} as Value; env.set(varName, varValue); @@ -33,7 +33,7 @@ describe("get()", () => { describe("linked environment", () => { it("set super environment and get via sub environment", () => { const varNameInSuper = "foo"; - const varValueInSuper = {} as Evaluated; + const varValueInSuper = {} as Value; const superEnv = new Environment(); superEnv.set(varNameInSuper, varValueInSuper); diff --git a/src/evaluator/environment/index.ts b/src/evaluator/environment/index.ts index a27a929..5fd9a4d 100644 --- a/src/evaluator/environment/index.ts +++ b/src/evaluator/environment/index.ts @@ -1,8 +1,8 @@ -import type { Evaluated } from "../evaluated"; +import type { Value } from "../value"; export interface EnvironmentType { - get: (name: string) => Evaluated | null; - set: (name: string, value: Evaluated) => void; + get: (name: string) => Value | null; + set: (name: string, value: Value) => void; } export default class Environment implements EnvironmentType { @@ -14,7 +14,7 @@ export default class Environment implements EnvironmentType { this.table = new Map; } - get(name: string): Evaluated | null { + get(name: string): Value | null { // return if found in current environment const fetched = this.table.get(name); if (fetched !== undefined) { @@ -28,7 +28,7 @@ export default class Environment implements EnvironmentType { return this.superEnvironment.get(name); } - set(name: string, value: Evaluated): void { + set(name: string, value: Value): void { this.table.set(name, value); } } diff --git a/src/evaluator/evaluated/index.test.ts b/src/evaluator/evaluated/index.test.ts deleted file mode 100644 index c2ee547..0000000 --- a/src/evaluator/evaluated/index.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { - makeEvaluatedNumber, - makeEvaluatedString, - makeEvaluatedBoolean, - makeEvaluatedFunction, - makeEvaluatedEmpty, - wrapReturnValue, -} from "./"; -import type { Evaluated } from "./"; -import type { - FunctionExpression -} from "../../parser/syntax-tree"; -import type Environment from "../environment"; - -describe("makeEvaluatedNumber()", () => { - it("make number value", () => { - const evaluated = makeEvaluatedNumber(42); - - expect(evaluated.type).toBe("number"); - expect(evaluated.value).toBe(42); - expect(evaluated.representation).toBe("42"); - }); -}); - -describe("makeEvaluatedString()", () => { - it("make nonempty string value", () => { - const evaluated = makeEvaluatedString("foo bar"); - - expect(evaluated.type).toBe("string"); - expect(evaluated.value).toBe("foo bar"); - expect(evaluated.representation).toBe("'foo bar'"); - }); - - it("make empty string value", () => { - const evaluated = makeEvaluatedString(""); - - expect(evaluated.type).toBe("string"); - expect(evaluated.value).toBe(""); - expect(evaluated.representation).toBe("''"); - }); -}); - -describe("makeEvaluatedBoolean()", () => { - it("make true boolean value", () => { - const evaluated = makeEvaluatedBoolean(true); - - expect(evaluated.type).toBe("boolean"); - expect(evaluated.value).toBe(true); - expect(evaluated.representation).toBe("참"); - }); - - it("make false boolean value", () => { - const evaluated = makeEvaluatedBoolean(false); - - expect(evaluated.type).toBe("boolean"); - expect(evaluated.value).toBe(false); - expect(evaluated.representation).toBe("거짓"); - }); -}); - -describe("makeEvaluatedFunction()", () => { - it("make function value", () => { - const parametersMock = [] as FunctionExpression["parameter"]; - const bodyMock = {} as FunctionExpression["body"]; - const environmentMock = {} as Environment; - - const evaluated = makeEvaluatedFunction(parametersMock, bodyMock, environmentMock); - - expect(evaluated.type).toBe("function"); - expect(evaluated.parameters).toBe(parametersMock); - expect(evaluated.body).toBe(bodyMock); - expect(evaluated.environment).toBe(environmentMock); - expect(evaluated.representation).toBe("(함수)"); - }); -}); - -describe("makeEvaluatedEmpty()", () => { - it("make empty value", () => { - const evaluated = makeEvaluatedEmpty(); - - expect(evaluated.type).toBe("empty"); - expect(evaluated.representation).toBe("(비어있음)"); - }); -}); - -describe("wrapReturnValue()", () => { - it("wrap return value", () => { - const valueMock = {} as Evaluated; - - const wrapped = wrapReturnValue(valueMock); - - expect(wrapped.type).toBe("return value"); - }); -}); diff --git a/src/evaluator/evaluated/index.ts b/src/evaluator/evaluated/index.ts deleted file mode 100644 index b534735..0000000 --- a/src/evaluator/evaluated/index.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { - FunctionExpression, -} from "../../parser/syntax-tree"; -import type Environment from "../environment"; - -interface EvaluatedBase { - readonly type: string; - readonly representation: string; -} - -export interface EvaluatedNumber extends EvaluatedBase { - readonly type: "number"; - readonly value: number; -} - -export interface EvaluatedString extends EvaluatedBase { - readonly type: "string"; - readonly value: string; -} - -export interface EvaluatedBoolean extends EvaluatedBase { - readonly type: "boolean"; - readonly value: boolean; -} - -export interface EvaluatedFunction extends EvaluatedBase { - readonly type: "function"; - readonly parameters: FunctionExpression["parameter"]; - readonly body: FunctionExpression["body"]; - readonly environment: Environment; -} - -// 'empty' represents the result of running statement (e.g., branching) in REPL -export interface EvaluatedEmpty extends EvaluatedBase { - readonly type: "empty"; -} - -export interface ReturnValue { - readonly type: "return value"; - readonly value: Evaluated; -} - -export type Evaluated = - EvaluatedPrimitive | - EvaluatedFunction | - EvaluatedEmpty; -export type EvaluatedPrimitive = - EvaluatedNumber | - EvaluatedString | - EvaluatedBoolean; - -export type MakeEvaluatedNumber = (value: number) => EvaluatedNumber; -export const makeEvaluatedNumber: MakeEvaluatedNumber = value => ({ - type: "number", - value, - get representation() { - return `${value}`; - }, -}); - -export type MakeEvaluatedString = (value: string) => EvaluatedString; -export const makeEvaluatedString: MakeEvaluatedString = value => ({ - type: "string", - value, - get representation() { - return `'${value}'`; - }, -}); - -export type MakeEvaluatedBoolean = (value: boolean) => EvaluatedBoolean; -export const makeEvaluatedBoolean: MakeEvaluatedBoolean = value => ({ - type: "boolean", - value, - get representation() { - return value ? "참" : "거짓"; - }, -}); - -export type MakeEvaluatedFunction = ( - parameters: FunctionExpression["parameter"], - body: FunctionExpression["body"], - environment: Environment -) => EvaluatedFunction; -export const makeEvaluatedFunction: MakeEvaluatedFunction = (parameters, body, environment) => ({ - type: "function", - parameters, - body, - environment, - get representation() { - return "(함수)"; - }, -}); - -export type MakeEvaluatedEmpty = () => EvaluatedEmpty; -export const makeEvaluatedEmpty: MakeEvaluatedEmpty = () => ({ - type: "empty", - get representation() { - return "(비어있음)"; - }, -}); - -export type WrapReturnValue = (value: Evaluated) => ReturnValue; -export const wrapReturnValue: WrapReturnValue = value => ({ - type: "return value", - value, -}); diff --git a/src/evaluator/index.test.ts b/src/evaluator/index.test.ts index 6118bf8..09daf46 100644 --- a/src/evaluator/index.test.ts +++ b/src/evaluator/index.test.ts @@ -1,29 +1,33 @@ import Lexer from "../lexer"; import Parser from "../parser"; -import type { EvaluatedPrimitive, EvaluatedFunction, EvaluatedEmpty } from "./evaluated"; -import Evaluator from "./"; +import Evaluator, * as Eval from "./"; import Environment from "./environment"; const evaluateInput = (input: string) => { const lexer = new Lexer(input); const parser = new Parser(lexer); - const program = parser.parseProgram(); + const parsed = parser.parseSource(); const evaluator = new Evaluator(); - const environment = new Environment(); - const evaluated = evaluator.evaluate(program, environment); - + const env = new Environment(); + const evaluated = evaluator.evaluate(parsed, env); return evaluated; }; const testEvaluatingPrimitive = ({ input, expected }: { input: string, expected: any }): void => { - const evaluated = evaluateInput(input) as EvaluatedPrimitive; + const evaluated = evaluateInput(input) as any; expect(evaluated.value).toBe(expected); }; +const testEvaluatingEmpty = ({ input }: { input: string }): void => { + const evaluated = evaluateInput(input) as any; + + expect(evaluated.value).toBe(null); +}; + const testEvaluatingFunction = ({ input, expectedParamsLength }: { input: string, expectedParamsLength: number }): void => { - const evaluated = evaluateInput(input) as EvaluatedFunction; + const evaluated = evaluateInput(input) as any; expect(evaluated).toHaveProperty("parameters"); expect(evaluated.parameters.length).toBe(expectedParamsLength); @@ -31,12 +35,6 @@ const testEvaluatingFunction = ({ input, expectedParamsLength }: { input: string expect(evaluated).toHaveProperty("environment"); }; -const testEvaluatingEmpty = ({ input }: { input: string }): void => { - const evaluated = evaluateInput(input) as EvaluatedEmpty; - - expect(evaluated.type).toBe("empty"); -}; - describe("evaluate()", () => { describe("single numbers", () => { const cases = [ @@ -358,4 +356,43 @@ describe("evaluate()", () => { it.each(cases)("evaluate $name", testEvaluatingPrimitive); }); + + describe("errors", () => { + const cases = [ + { + name: "top level return error", + input: "결과 11", + expected: Eval.TopLevelReturnError, + range: { begin: { row: 0, col: 0 }, end: { row: 0, col: 4 } }, + }, + { + name: "bad predicate error", + input: "만약 11 {\n 22\n}", + expected: Eval.BadPredicateError, + range: { begin: { row: 0, col: 3 }, end: { row: 0, col: 4 } }, + received: "11", + }, + { + name: "bad identifier error", + input: "사과", + expected: Eval.BadIdentifierError, + range: { begin: { row: 0, col: 0 }, end: { row: 0, col: 1 } }, + received: "사과", + }, + ]; + + it.each(cases)("$name", ({ input, expected, range, received }) => { + expect(() => evaluateInput(input)).toThrow(expected); + try { + evaluateInput(input); + } catch (err) { + const e = err as typeof expected; + + expect(e).toMatchObject({ range }); + if (received !== undefined) { + expect(e).toMatchObject({ received }); + } + } + }); + }); }); diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index 71fed9d..3064b69 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -1,182 +1,193 @@ -import type { - Program, - Block, - Statement, - BranchStatement, - ExpressionStatement, - Expression, - Identifier, - PrefixExpression, - InfixExpression, - FunctionExpression, - Call, - Assignment, -} from "../parser"; -import { - makeEvaluatedNumber, - makeEvaluatedBoolean, - makeEvaluatedString, - makeEvaluatedFunction, - makeEvaluatedEmpty, - wrapReturnValue, -} from "./evaluated"; -import type { - Evaluated, - EvaluatedNumber, - EvaluatedBoolean, - EvaluatedFunction, - ReturnValue, -} from "./evaluated"; +import type * as Node from "../parser"; +import type * as Value from "./value"; +import * as value from "./value"; import Environment from "./environment"; +import type { Range } from "../util/position"; + +export class EvalError extends Error { + public range: Range; + public received?: string; + + constructor(range: Range, received?: string) { + super(); + this.range = range; + this.received = received; + } +} + +export class TopLevelReturnError extends EvalError {}; +export class BadPredicateError extends EvalError {}; +export class BadAssignmentLeftError extends EvalError {}; +export class BadPrefixExpressionError extends EvalError {}; +export class BadInfixExpressionError extends EvalError {}; +export class BadIdentifierError extends EvalError {}; + +type ComparisonOperator = "==" | "!=" | ">" | "<" | ">=" | "<="; export default class Evaluator { - evaluate(node: Program, env: Environment): Evaluated { + evaluate(node: Node.ProgramNode, env: Environment): Value.Value { return this.evaluateProgram(node, env); } - private evaluateProgram(node: Program, env: Environment): Evaluated { + private evaluateProgram(node: Node.ProgramNode, env: Environment): Value.Value { const { statements } = node; - if (statements.length === 0) { - return makeEvaluatedEmpty(); - } - // loop except the last statement + let lastEvaluated: Value.Value | null = null; for (let i = 0; i < statements.length; ++i) { - const statement = statements[i]; - const evaluated = this.evaluateStatement(statement, env); - if (evaluated.type === "return value") { - throw new Error(`return value cannot appear in top level scope`); + const evaluated = this.evaluateStatement(statements[i], env); + + if (evaluated.type === "return") { + throw new TopLevelReturnError(node.range); } - } - // return the last evaluated value - const lastStatement = statements[statements.length-1]; - const evaluated = this.evaluateStatement(lastStatement, env); - if (evaluated.type === "return value") { - throw new Error(`return value cannot appear in top level scope`); + lastEvaluated = evaluated; } - return evaluated; + + return lastEvaluated ?? this.createEmptyValue(node.range); } - private evaluateBlock(node: Block, env: Environment): Evaluated | ReturnValue { - const { statements } = node; - if (statements.length === 0) { - throw new Error(`block cannot be empty`); + private evaluateStatement(node: Node.StatementNode, env: Environment): Value.Value | Value.ReturnValue { + if (node.type === "branch") { + return this.evaluateBranchStatement(node, env); } - - // loop except the last statement - for (let i = 0; i < statements.length; ++i) { - const statement = statements[i]; - const evaluated = this.evaluateStatement(statement, env); - if (evaluated.type === "return value") { // early return if return statement encoutered - return evaluated; - } + if (node.type === "expression statement") { + return this.evaluateExpressionStatement(node, env); + } + if (node.type === "return") { + const val = this.evaluateExpression(node.expression, env); + return value.createReturnValue(val); } - const lastStatement = statements[statements.length-1]; - const evaluated = this.evaluateStatement(lastStatement, env); - return evaluated; + const nothing: never = node; + return nothing; } - private evaluatePrefixExpression(node: PrefixExpression, env: Environment): Evaluated { - const subExpression = this.evaluateExpression(node.expression, env); + private evaluateBranchStatement(node: Node.BranchNode, env: Environment): Value.Value | Value.ReturnValue { + const pred = this.evaluateExpression(node.predicate, env); + if (pred.type !== "boolean") { + throw new BadPredicateError(pred.range, pred.representation); + } - if ( - (node.prefix === "+" || node.prefix === "-") && - subExpression.type == "number" - ) { - return this.evaluatePrefixNumberExpression(node.prefix, subExpression); + if (pred.value) { + return this.evaluateBlock(node.consequence, env); } - if (node.prefix === "!" && subExpression.type === "boolean") { - return this.evaluatePrefixBooleanExpression(node.prefix, subExpression); + + if (node.alternative === undefined) { + return this.createEmptyValue(node.range); } - throw new Error(`bad prefix expression: prefix: '${node.prefix}' with type: '${typeof subExpression}'`); + return this.evaluateBlock(node.alternative, env); } - private evaluatePrefixNumberExpression(prefix: string, operand: EvaluatedNumber): EvaluatedNumber { - if (prefix === "+") { - return operand; - } - if (prefix === "-") { - return makeEvaluatedNumber(-operand.value); + private evaluateBlock(node: Node.BlockNode, env: Environment): Value.Value | Value.ReturnValue { + let lastEvaluated: Value.Value | null = null; + + for (let i = 0; i < node.statements.length; ++i) { + const evaluated = this.evaluateStatement(node.statements[i], env); + + if (evaluated.type === "return") { + return evaluated; + } + + lastEvaluated = evaluated; } - throw new Error(`bad prefix ${prefix}`); + return lastEvaluated ?? this.createEmptyValue(node.range); } - private evaluatePrefixBooleanExpression(prefix: string, operand: EvaluatedBoolean): EvaluatedBoolean { - if (prefix === "!") { - return makeEvaluatedBoolean(!operand.value); + private evaluateExpressionStatement(node: Node.ExpressionStatementNode, env: Environment): Value.Value { + return this.evaluateExpression(node.expression, env); + } + + private evaluateExpression(node: Node.ExpressionNode, env: Environment): Value.Value { + if (node.type === "number") { + return this.createNumberValue(node.value, node.range); + } + if (node.type === "boolean") { + return this.createBooleanValue(node.value, node.range); + } + if (node.type === "string") { + return this.createStringValue(node.value, node.range); + } + if (node.type === "prefix") { + return this.evaluatePrefixExpression(node, env); + } + if (node.type === "infix") { + return this.evaluateInfixExpression(node, env); + } + if (node.type === "assignment") { + return this.evaluateAssignment(node, env); + } + if (node.type === "identifier") { + return this.evaluateIdentifier(node, env); + } + if (node.type === "function") { + return this.evaluateFunctionExpression(node, env); + } + if (node.type === "call") { + return this.evaluateCall(node, env); } - throw new Error(`bad prefix ${prefix}`); + const _never: never = node; + return _never; } - private evaluateStatement(node: Statement, env: Environment): Evaluated | ReturnValue { - if (node.type === "branch statement") { - return this.evaluateBranchStatement(node, env); - } + private evaluatePrefixExpression(node: Node.PrefixNode, env: Environment): Value.Value { + const right = this.evaluateExpression(node.right, env); - if (node.type === "expression statement") { - return this.evaluateExpressionStatement(node, env); + if ((node.prefix === "+" || node.prefix === "-") && right.type == "number") { + return this.evaluatePrefixNumberExpression(node.prefix, right); } - - if (node.type === "return statement") { - const value = this.evaluateExpression(node.expression, env); - return wrapReturnValue(value); + if (node.prefix === "!" && right.type === "boolean") { + return this.evaluatePrefixBooleanExpression(node.prefix, right); } - const nothing: never = node; - return nothing; + throw new BadPrefixExpressionError(node.range); } - private evaluateBranchStatement(node: BranchStatement, env: Environment): Evaluated | ReturnValue { - const predicate = this.evaluateExpression(node.predicate, env); - if (predicate.type !== "boolean") { - throw new Error(`expected boolean expression predicate, but received ${predicate.type}`); + private evaluateInfixExpression(node: Node.InfixNode, env: Environment): Value.Value { + const left = this.evaluateExpression(node.left, env); + const right = this.evaluateExpression(node.right, env); + + if (left.type === "number" && right.type === "number" && this.isArithmeticInfixOperator(node.infix)) { + const value = this.getArithmeticInfixOperationValue(left.value, right.value, node.infix); + return this.createNumberValue(value, node.range); } - if (predicate.value) { - const consequence = this.evaluateBlock(node.consequence, env); - return consequence; + if (left.type === "number" && right.type === "number" && this.isComparisonInfixOperator(node.infix)) { + const value = this.getNumericComparisonInfixOperationValue(left.value, right.value, node.infix); + return this.createBooleanValue(value, node.range); } - // early return if no else block - if (typeof node.alternative === "undefined") { - return makeEvaluatedEmpty(); + if (left.type === "boolean" && right.type === "boolean" && this.isComparisonInfixOperator(node.infix)) { + const value = this.getBooleanComparisonInfixOperationValue(left.value, right.value, node.infix); + return this.createBooleanValue(value, node.range); } - const alternative = this.evaluateBlock(node.alternative, env); - return alternative; - } + if (left.type === "string" && right.type === "string" && this.isComparisonInfixOperator(node.infix)) { + const value = this.getStringComparisonInfixOperationValue(left.value, right.value, node.infix); + return this.createBooleanValue(value, node.range); + } - private evaluateExpressionStatement(node: ExpressionStatement, env: Environment): Evaluated { - return this.evaluateExpression(node.expression, env); + throw new BadInfixExpressionError(node.range); } - private evaluateFunctionExpression(node: FunctionExpression, env: Environment): Evaluated { - const parameters = node.parameter; - const body = node.body; - return makeEvaluatedFunction(parameters, body, env); - } + private evaluateIdentifier(node: Node.IdentifierNode, env: Environment): Value.Value { + const varName = node.value; + const value = env.get(varName); - private evaluateCall(node: Call, env: Environment): Evaluated { - const functionToCall = this.evaluateExpression(node.functionToCall, env); - if (functionToCall.type !== "function") { - throw new Error(`expected function but received ${functionToCall.type}`); + if (value === null) { + throw new BadIdentifierError(node.range, varName); } - const callArguments = this.parseCallArguments(node.callArguments, env); - - const value = this.evaluateFunctionCall(functionToCall, callArguments); return value; } - private evaluateAssignment(node: Assignment, env: Environment): Evaluated { + private evaluateAssignment(node: Node.AssignmentNode, env: Environment): Value.Value { if (node.left.type !== "identifier") { - throw new Error(`expected identifier on left value, but received ${typeof node.left.type}`); + throw new BadAssignmentLeftError(node.range); } + const varName = node.left.value; const varValue = this.evaluateExpression(node.right, env); @@ -185,135 +196,156 @@ export default class Evaluator { return varValue; // evaluated value of assignment is the evaluated value of variable } - private evaluateIdentifier(node: Identifier, env: Environment): Evaluated { - const varName = node.value; - const value = env.get(varName); + private evaluateFunctionExpression(node: Node.FunctionNode, env: Environment): Value.Value { + return this.createFunctionValue(node.parameters, node.body, env, node.range); + } - if (value === null) { - throw new Error(`identifier '${varName}' not found`); + private evaluateCall(node: Node.CallNode, env: Environment): Value.Value { + const func = this.evaluateExpression(node.func, env); + if (func.type !== "function") { + throw new Error(`expected function but received ${func.type}`); } + const callArguments = this.evaluateCallArguments(node.args, env); + + const value = this.evaluateFunctionCall(func, callArguments); return value; } - private evaluateExpression(node: Expression, env: Environment): Evaluated { - if (node.type === "number node") { - return makeEvaluatedNumber(node.value); - } + private evaluateCallArguments(args: Node.ExpressionNode[], env: Environment): Value.Value[] { + return args.map(arg => this.evaluateExpression(arg, env)); + } - if (node.type === "boolean node") { - return makeEvaluatedBoolean(node.value); - } + private evaluateFunctionCall(func: Value.FunctionValue, callArguments: Value.Value[]): Value.Value { + const env = this.createExtendedEnvironment(func.environment, func.parameters, callArguments); - if (node.type === "string node") { - return makeEvaluatedString(node.value); + const blockValue = this.evaluateBlock(func.body, env); + if (blockValue.type !== "return") { + // TODO: better error with range + throw new Error(`expected return value in function but it didn't`); } - if (node.type === "infix expression") { - return this.evaluateInfixExpression(node, env); - } + const returnValue = blockValue.value; + return returnValue; + } - if (node.type === "prefix expression") { - return this.evaluatePrefixExpression(node, env); - } + private getBooleanComparisonInfixOperationValue(left: boolean, right: boolean, operator: ComparisonOperator): boolean { + return this.getComparisonInfixOperationValue(left, right, operator); + } - if (node.type === "function expression") { - return this.evaluateFunctionExpression(node, env); - } + private getNumericComparisonInfixOperationValue(left: number, right: number, operator: ComparisonOperator): boolean { + return this.getComparisonInfixOperationValue(left, right, operator); + } - if (node.type === "call") { - return this.evaluateCall(node, env); - } + private getStringComparisonInfixOperationValue(left: string, right: string, operator: ComparisonOperator): boolean { + return this.getComparisonInfixOperationValue(left, right, operator); + } - if (node.type === "assignment") { - return this.evaluateAssignment(node, env); + private getComparisonInfixOperationValue(left: T, right: T, operator: ComparisonOperator): boolean { + if (operator === "==") { + return left === right; } - - if (node.type === "identifier") { - return this.evaluateIdentifier(node, env); + if (operator === "!=") { + return left !== right; + } + if (operator === ">") { + return left > right; + } + if (operator === "<") { + return left < right; + } + if (operator === ">=") { + return left >= right; + } + if (operator === "<=") { + return left <= right; } - const nothing: never = node; - return nothing; + const _never: never = operator; + return _never; } - private evaluateInfixExpression(node: InfixExpression, env: Environment): Evaluated { - const infix = node.infix; - const left = this.evaluateExpression(node.left, env); - const right = this.evaluateExpression(node.right, env); + private getArithmeticInfixOperationValue(left: number, right: number, operator: "+" | "-" | "*" | "/"): number { + if (operator === "+") { + return left + right; + } + if (operator === "-") { + return left - right; + } + if (operator === "*") { + return left * right; + } + if (operator === "/") { + return left / right; + } - // type matching order is important: more inclusive case first + const _never: never = operator; + return _never; + } - if ( - (left.type === "boolean" && right.type === "boolean") || - (left.type === "number" && right.type === "number") || - (left.type === "string" && right.type === "string") - ) { - if (infix === "==") { - return makeEvaluatedBoolean(left.value == right.value); - } - if (infix === "!=") { - return makeEvaluatedBoolean(left.value != right.value); - } - if (infix === ">") { - return makeEvaluatedBoolean(left.value > right.value); - } - if (infix === "<") { - return makeEvaluatedBoolean(left.value < right.value); - } - if (infix === ">=") { - return makeEvaluatedBoolean(left.value >= right.value); - } - if (infix === "<=") { - return makeEvaluatedBoolean(left.value <= right.value); - } + private evaluatePrefixNumberExpression(prefix: "+" | "-", node: Node.NumberNode): Value.NumberValue { + if (prefix === "+") { + return this.createNumberValue(node.value, node.range); + } + if (prefix === "-") { + return this.createNumberValue(-node.value, node.range); } - if (left.type === "number" && right.type === "number") { - if (infix === "+") { - return makeEvaluatedNumber(left.value + right.value); - } - if (infix === "-") { - return makeEvaluatedNumber(left.value - right.value); - } - if (infix === "*") { - return makeEvaluatedNumber(left.value * right.value); - } - if (infix === "/") { - // TODO: guard division by zero - return makeEvaluatedNumber(left.value / right.value); - } + const _never: never = prefix; + return _never; + } - throw new Error(`bad infix ${infix} for number operands`); + private evaluatePrefixBooleanExpression(prefix: "!", node: Node.BooleanNode): Value.BooleanValue { + if (prefix === "!") { + return this.createBooleanValue(!node.value, node.range); } - throw new Error(`bad infix ${infix}, with left '${left}' and right '${right}'`); + const _never: never = prefix; + return _never; } - private parseCallArguments(callArguments: Expression[], env: Environment): Evaluated[] { - const values = []; - for (const arg of callArguments) { - const value = this.evaluateExpression(arg, env); - values.push(value); + private createExtendedEnvironment(oldEnv: Environment, identifiers: Node.IdentifierNode[], values: Value.Value[]): Environment { + const newEnv = new Environment(oldEnv); + + for (let i = 0; i < identifiers.length; ++i) { + const name = identifiers[i].value; + const value = values[i]; + newEnv.set(name, value); } - return values; + + return newEnv; } - private evaluateFunctionCall(functionToCall: EvaluatedFunction, callArguments: Evaluated[]): Evaluated { - const functionEnv = new Environment(functionToCall.environment); - for (let i = 0; i < functionToCall.parameters.length; ++i) { - const name = functionToCall.parameters[i].value; - const value = callArguments[i]; - functionEnv.set(name, value); - } + // create value functions: wrappers for consistent representation - const evaluated = this.evaluateBlock(functionToCall.body, functionEnv); - if (evaluated.type !== "return value") { - throw new Error(`expected return value in function but it didn't`); - } + private createNumberValue(val: number, range: Range): Value.NumberValue { + return value.createNumberValue({ value: val }, String(val), range); + } - const returnValue = evaluated.value; - return returnValue; + private createBooleanValue(val: boolean, range: Range): Value.BooleanValue { + return value.createBooleanValue({ value: val }, val ? "참" : "거짓", range); + } + + private createStringValue(val: string, range: Range): Value.StringValue { + return value.createStringValue({ value: val }, val, range); + } + + private createEmptyValue(range: Range): Value.EmptyValue { + return value.createEmptyValue({ value: null }, "(없음)", range); + } + + private createFunctionValue(parameters: Node.FunctionNode["parameters"], body: Node.FunctionNode["body"], environment: Environment, range: Range): Value.FunctionValue { + return value.createFunctionValue({ parameters, body, environment }, "(함수)", range); + } + + // util predicate functions + + private isArithmeticInfixOperator(operator: string): operator is "+" | "-" | "*" | "/" { + return ["+", "-", "*", "/"].some(infix => infix === operator); + } + + private isComparisonInfixOperator(operator: string): operator is ComparisonOperator { + return ["==", "!=", ">", "<", ">=", "<="].some(infix => infix === operator); } } diff --git a/src/evaluator/value/index.ts b/src/evaluator/value/index.ts new file mode 100644 index 0000000..1261beb --- /dev/null +++ b/src/evaluator/value/index.ts @@ -0,0 +1,61 @@ +import { copyRange, type Range } from "../../util/position"; +import type { FunctionNode } from "../../parser"; +import Environment from "../environment"; + +export interface ValueBase { + readonly type: T, + readonly representation: string, + readonly range: Range, +} + +export type Value = PrimitiveValue + | EmptyValue + +export type PrimitiveValue = NumberValue + | StringValue + | BooleanValue + | FunctionValue + +export interface NumberValue extends ValueBase<"number"> { + readonly value: number, +} +export interface StringValue extends ValueBase<"string"> { + readonly value: string, +} +export interface BooleanValue extends ValueBase<"boolean"> { + readonly value: boolean, +} +export interface FunctionValue extends ValueBase<"function"> { + readonly parameters: FunctionNode["parameters"], + readonly body: FunctionNode["body"], + readonly environment: Environment, +} +export interface EmptyValue extends ValueBase<"empty"> { + readonly value: null, +} + +export interface ReturnValue { + readonly type: "return", + readonly value: Value, +} + +const createValueCreator = >(type: T) => { + return (fields: Omit>, representation: string, range: Range) => ({ + type, + range: copyRange(range.begin, range.end), + representation, + ...fields, + }); +}; +type CreateValue> = (fields: Omit>, representation: string, range: Range) => V; + +export const createNumberValue: CreateValue<"number", NumberValue> = createValueCreator<"number", NumberValue>("number"); +export const createBooleanValue: CreateValue<"boolean", BooleanValue> = createValueCreator<"boolean", BooleanValue>("boolean"); +export const createStringValue: CreateValue<"string", StringValue> = createValueCreator<"string", StringValue>("string"); +export const createEmptyValue: CreateValue<"empty", EmptyValue> = createValueCreator<"empty", EmptyValue>("empty"); +export const createFunctionValue: CreateValue<"function", FunctionValue> = createValueCreator<"function", FunctionValue>("function"); + +export const createReturnValue = (value: Value): ReturnValue => ({ + type: "return", + value, +}); diff --git a/src/index.test.ts b/src/index.test.ts index f227f53..f19e464 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -24,12 +24,12 @@ it("execute 1 == 2", () => { expect(execute("1 == 2")).toBe("거짓"); }); -it("execute 2 > 1 == 참", () => { - expect(execute("2 > 1 == 참")).toBe("참"); +it("execute 참 == 2 > 1", () => { + expect(execute("참 == 2 > 1")).toBe("참"); }); -it("execute 1 != 1 == 거짓", () => { // note that comparison is left associative - expect(execute("1 != 1 == 거짓")).toBe("참"); +it("execute 거짓 == 1 != 1", () => { // note that comparison is left associative + expect(execute("거짓 == 1 != 1")).toBe("참"); }); it("execute 거짓 == (1 < 1+1)", () => { diff --git a/src/index.ts b/src/index.ts index 278f9ac..c52c336 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import Evaluator, { Environment } from "./evaluator"; export const execute = (input: string): string => { const lexer = new Lexer(input); const parser = new Parser(lexer); - const parsed = parser.parseProgram(); + const parsed = parser.parseSource(); const evaluator = new Evaluator(); const environment = new Environment(); diff --git a/src/lexer/char-buffer/char-reader/index.test.ts b/src/lexer/char-buffer/char-reader/index.test.ts new file mode 100644 index 0000000..c011c26 --- /dev/null +++ b/src/lexer/char-buffer/char-reader/index.test.ts @@ -0,0 +1,74 @@ +import Reader from "./"; + +describe("readSource()", () => { + it("read a character", () => { + const reader = new Reader("a", "\0"); + const expected = { value: "a", position: { row: 0, col: 0 }}; + + const char = reader.readChar(); + + expect(char).toEqual(expected); + }); + + it("read the same character twice if not advanced", () => { + const reader = new Reader("a", "\0"); + const expected = { value: "a", position: { row: 0, col: 0 }}; + + const char1 = reader.readChar(); + const char2 = reader.readChar(); + + expect(char1).toEqual(expected); + expect(char2).toEqual(expected); + }); + + it("read the fallback character if end of input", () => { + const reader = new Reader("", "\0"); + const expected = { value: "\0", position: { row: 0, col: 0 }}; + + const char1 = reader.readChar(); + const char2 = reader.readChar(); + + expect(char1).toEqual(expected); + expect(char2).toEqual(expected); + }); +}); + +describe("advance()", () => { + it("advance to next character", () => { + const reader = new Reader("ab", "\0"); + const expected = { value: "b", position: { row: 0, col: 1 }}; + + reader.advance(); + const char = reader.readChar(); + + expect(char).toEqual(expected); + }); + + it("advance to next line if new line (LF)", () => { + const reader = new Reader("a\nb", "\0"); + const expected1 = { value: "\n", position: { row: 0, col: 1 }}; + const expected2 = { value: "b", position: { row: 1, col: 0 }}; + + reader.advance(); + const char1 = reader.readChar(); + expect(char1).toEqual(expected1); + + reader.advance(); + const char2 = reader.readChar(); + expect(char2).toEqual(expected2); + }); + + it("advance to next line if new line (CR LF)", () => { + const reader = new Reader("a\r\nb", "\0"); + const expected1 = { value: "\r\n", position: { row: 0, col: 1 }}; + const expected2 = { value: "b", position: { row: 1, col: 0 }}; + + reader.advance(); + const char1 = reader.readChar(); + expect(char1).toEqual(expected1); + + reader.advance(); + const char2 = reader.readChar(); + expect(char2).toEqual(expected2); + }); +}); diff --git a/src/lexer/char-buffer/char-reader/index.ts b/src/lexer/char-buffer/char-reader/index.ts new file mode 100644 index 0000000..5f24dce --- /dev/null +++ b/src/lexer/char-buffer/char-reader/index.ts @@ -0,0 +1,110 @@ +import type SourceChar from "./source-char"; + +export default class CharReader { + private readonly chars: string; + private readonly fallbackChar: string; + private index: number; // next index to read + private row: number; // current row + private col: number; // current col + + /** + * @params chars - Characters to initialize + * @params fallbackChar - A character to return on underflow + */ + constructor(chars: string, fallbackChar: string) { + this.chars = chars; + this.fallbackChar = fallbackChar; + + this.index = 0; // first character to read is at index 0 + this.row = 0; + this.col = 0; + } + + /** return character at current position */ + readChar(): SourceChar { + // return fallback character if end of input + if (this.index === this.chars.length) { + const fallbackChar: SourceChar = { + value: this.fallbackChar, + position: { + row: this.row, + col: this.col, + }, + }; + return fallbackChar; + } + + // return new line character(s) if any + const newLine = this.peekNewLine(); + if (newLine !== null) { + const newLineChar = { + value: newLine, + position: { + row: this.row, + col: this.col, + }, + }; + return newLineChar; + } + + // return single character + const char = this.chars[this.index]; + const sourceChar: SourceChar = { + value: char, + position: { + row: this.row, + col: this.col, + }, + }; + return sourceChar; + } + + /** advance to next character */ + advance(): void { + if (this.index === this.chars.length) { + return; + } + + // advance position past new line character(s) if any + const newLine = this.peekNewLine(); + if (newLine !== null) { + this.index += newLine.length; + ++this.row; + this.col = 0; + return; + } + + // advance + ++this.index; + ++this.col; + } + + private peekNewLine(): string | null { + // return if end of input + if (this.index === this.chars.length) { + return null; + } + + // return if no new line + const char = this.chars[this.index]; + if (char !== "\r" && char !== "\n") { + return null; + } + + // return if the last character + if (this.index+1 === this.chars.length) { + return char; + } + + // return if single-character new line + const nextChar = this.chars[this.index+1]; + if (nextChar !== "\r" && nextChar !== "\n") { + return char; + } + + // return two-character new line + return char + nextChar; + } +} + +export type { SourceChar }; diff --git a/src/lexer/char-buffer/char-reader/source-char/index.ts b/src/lexer/char-buffer/char-reader/source-char/index.ts new file mode 100644 index 0000000..2a372cf --- /dev/null +++ b/src/lexer/char-buffer/char-reader/source-char/index.ts @@ -0,0 +1,6 @@ +import type { Position } from "../../../../util/position"; + +export default interface SourceChar { + value: string, + position: Position, +}; diff --git a/src/lexer/char-buffer/index.test.ts b/src/lexer/char-buffer/index.test.ts index 7a85e49..4fa5b1b 100644 --- a/src/lexer/char-buffer/index.test.ts +++ b/src/lexer/char-buffer/index.test.ts @@ -1,51 +1,41 @@ -import Buffer from "./"; +import CharBuffer from "./"; -describe("pop()", () => { +describe("popChar()", () => { it("pop characters", () => { - const buffer = new Buffer("ab"); + const buffer = new CharBuffer("ab"); + const expected1 = { value: "a", position: { row: 0, col: 0 }}; + const expected2 = { value: "b", position: { row: 0, col: 1 }}; - expect(buffer.pop()).toBe("a"); - expect(buffer.pop()).toBe("b"); + expect(buffer.popChar()).toEqual(expected1); + expect(buffer.popChar()).toEqual(expected2); }); - it("pop null character if nothing to pop", () => { - const buffer = new Buffer(""); + it("pop null characters if nothing to pop", () => { + const buffer = new CharBuffer(""); + const expected = { value: "\0", position: { row: 0, col: 0 }}; - expect(buffer.pop()).toBe("\0"); - }); - - it("pop null character more than once if nothing to pop", () => { - const buffer = new Buffer(""); - - expect(buffer.pop()).toBe("\0"); - expect(buffer.pop()).toBe("\0"); + // pop the same null character more than once + expect(buffer.popChar()).toEqual(expected); + expect(buffer.popChar()).toEqual(expected); }); }); -describe("peek()", () => { +describe("peekChar()", () => { it("peek character", () => { - const buffer = new Buffer("a"); - - expect(buffer.peek()).toBe("a"); - }); - - it("peek the same character twice", () => { - const buffer = new Buffer("a"); - - expect(buffer.peek()).toBe("a"); - expect(buffer.peek()).toBe("a"); - }); - - it("peek null character if nothing to pop", () => { - const buffer = new Buffer(""); + const buffer = new CharBuffer("a"); + const expected = { value: "a", position: { row: 0, col: 0 }}; - expect(buffer.peek()).toBe("\0"); + // peek the same character if not popped + expect(buffer.peekChar()).toEqual(expected); + expect(buffer.peekChar()).toEqual(expected); }); - it("peek null character more than once if nothing to pop", () => { - const buffer = new Buffer(""); + it("peek null characters if nothing to pop", () => { + const buffer = new CharBuffer(""); + const expected = { value: "\0", position: { row: 0, col: 0 }}; - expect(buffer.peek()).toBe("\0"); - expect(buffer.peek()).toBe("\0"); + // peek the same null character more than once + expect(buffer.peekChar()).toEqual(expected); + expect(buffer.peekChar()).toEqual(expected); }); }); diff --git a/src/lexer/char-buffer/index.ts b/src/lexer/char-buffer/index.ts index f6a32c7..bcea770 100644 --- a/src/lexer/char-buffer/index.ts +++ b/src/lexer/char-buffer/index.ts @@ -1,24 +1,26 @@ -import Reader from "./reader"; +import CharReader, { type SourceChar } from "./char-reader"; export default class CharBuffer { static readonly END_OF_INPUT = "\0"; - private readonly reader: Reader; + private readonly reader: CharReader; constructor(input: string) { - this.reader = new Reader(input, CharBuffer.END_OF_INPUT); + this.reader = new CharReader(input, CharBuffer.END_OF_INPUT); } - pop(): string { - const char = this.reader.read(); - this.reader.next(); + popChar(): SourceChar { + const char = this.reader.readChar(); + this.reader.advance(); return char; } - peek(): string { - const char = this.reader.read(); + peekChar(): SourceChar { + const char = this.reader.readChar(); return char; } } + +export type { SourceChar } from "./char-reader"; diff --git a/src/lexer/char-buffer/reader/index.test.ts b/src/lexer/char-buffer/reader/index.test.ts deleted file mode 100644 index c53cc98..0000000 --- a/src/lexer/char-buffer/reader/index.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Reader from "./"; - -describe("read()", () => { - it("read a character", () => { - const reader = new Reader("a", "\0"); - - expect(reader.read()).toBe("a"); - }); - - it("read the same character twice", () => { - const reader = new Reader("a", "\0"); - - expect(reader.read()).toBe("a"); - expect(reader.read()).toBe("a"); - }); - - it("read fallback character if end of input", () => { - const reader = new Reader("", "\0"); - - expect(reader.read()).toBe("\0"); - expect(reader.read()).toBe("\0"); - }); -}); - -describe("next()", () => { - it("increment index and read next character", () => { - const reader = new Reader("ab", "\0"); - - reader.next(); - expect(reader.read()).toBe("b"); - }); - - it("not increment index if end of input", () => { - const reader = new Reader("a", "\0"); - - reader.next(); - reader.next(); - expect(reader.read()).toBe("\0"); - }); -}); diff --git a/src/lexer/char-buffer/reader/index.ts b/src/lexer/char-buffer/reader/index.ts deleted file mode 100644 index 66d6bd1..0000000 --- a/src/lexer/char-buffer/reader/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -export default class Reader { - private readonly chars: string; - private readonly fallbackChar: string; - private index: number; - - /** - * @params chars - Characters to initialize - * @params fallbackChar - A character to return on underflow - */ - constructor(chars: string, fallbackChar: string) { - this.chars = chars; - this.fallbackChar = fallbackChar; - this.index = 0; // first character to read is at index 0 - } - - /** Returns current character; if end of input, return fallback character */ - read(): string { - if (this.index === this.chars.length) { - return this.fallbackChar; - } - - return this.chars[this.index]; - } - - /** Increment index to get next character with get() */ - next(): void { - if (this.index === this.chars.length) { - return; - } - - this.index++; - } -} diff --git a/src/lexer/index.test.ts b/src/lexer/index.test.ts index 27f0597..a069362 100644 --- a/src/lexer/index.test.ts +++ b/src/lexer/index.test.ts @@ -1,249 +1,561 @@ import Lexer from "./"; -import { - operator, - identifier, - numberLiteral, - booleanLiteral, - stringLiteral, - groupDelimiter, - blockDelimiter, - keyword, - separator, - illegal, - end, -} from "./token"; import type { - TokenType, - Operator, - Identifier, - NumberLiteral, - BooleanLiteral, - StringLiteral, - GroupDelimiter, - BlockDelimiter, - Keyword, - Separator, - Illegal, - End, -} from "./token"; - -describe("getToken()", () => { + SourceToken, +} from "./source-token"; + +describe("getSourceToken()", () => { describe("single token", () => { - const testLexing = ({ input, expected }: { input: string, expected: TokenType }) => { + const testLex = ({ input, expected }: { input: string, expected: SourceToken }) => { const lexer = new Lexer(input); - const token = lexer.getToken(); + const token = lexer.getSourceToken(); expect(token).toEqual(expected); }; describe("operators", () => { - const cases: { input: string, expected: Operator }[] = [ - { input: "+", expected: operator("+") }, - { input: "-", expected: operator("-") }, - { input: "*", expected: operator("*") }, - { input: "/", expected: operator("/") }, - { input: "=", expected: operator("=") }, - { input: "!", expected: operator("!") }, - { input: "==", expected: operator("==") }, - { input: "!=", expected: operator("!=") }, - { input: ">", expected: operator(">") }, - { input: "<", expected: operator("<") }, - { input: ">=", expected: operator(">=") }, - { input: "<=", expected: operator("<=") }, + const cases: { input: string, expected: SourceToken }[] = [ + { + input: "+", + expected: { + type: "operator", + value: "+", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, + }, + }, + { + input: "-", + expected: { + type: "operator", + value: "-", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, + }, + }, + { + input: "*", + expected: { + type: "operator", + value: "*", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, + }, + }, + { + input: "/", + expected: { + type: "operator", + value: "/", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, + }, + }, + { + input: "=", + expected: { + type: "operator", + value: "=", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, + }, + }, + { + input: "==", + expected: { + type: "operator", + value: "==", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, + }, + }, + { + input: "!", + expected: { + type: "operator", + value: "!", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, + }, + }, + { + input: "!=", + expected: { + type: "operator", + value: "!=", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, + }, + }, + { + input: ">", + expected: { + type: "operator", + value: ">", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, + }, + }, + { + input: ">=", + expected: { + type: "operator", + value: ">=", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, + }, + }, + { + input: "<", + expected: { + type: "operator", + value: "<", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, + }, + }, + { + input: "<=", + expected: { + type: "operator", + value: "<=", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, + }, + }, ]; - it.each(cases)("get operator token '$input'", testLexing); + it.each(cases)("lex operator '$input'", testLex); }); - describe("identifiers", () => { - const cases: { input: string, expected: Identifier }[] = [ - { input: "foo", expected: identifier("foo") }, - { input: "이름", expected: identifier("이름") }, - { input: "foo이름", expected: identifier("foo이름") }, - { input: "foo123", expected: identifier("foo123") }, - { input: "이름foo", expected: identifier("이름foo") }, - { input: "_foo이름", expected: identifier("_foo이름") }, + describe("delimiters", () => { + const cases: { input: string, expected: SourceToken }[] = [ + { + input: "(", + expected: { + type: "group delimiter", + value: "(", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, + }, + }, + { + input: ")", + expected: { + type: "group delimiter", + value: ")", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, + }, + }, + { + input: "{", + expected: { + type: "block delimiter", + value: "{", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, + }, + }, + { + input: "}", + expected: { + type: "block delimiter", + value: "}", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, + }, + }, + { + input: ",", + expected: { + type: "separator", + value: ",", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, + }, + }, ]; - it.each(cases)("get identifier token '$input'", testLexing); + it.each(cases)("lex delimiter '$input'", testLex); }); describe("number literals", () => { - const cases: { input: string, expected: NumberLiteral }[] = [ - { input: "0", expected: numberLiteral("0") }, - { input: "123", expected: numberLiteral("123") }, - { input: "12.75", expected: numberLiteral("12.75") }, - { input: "0.875", expected: numberLiteral("0.875") }, - { input: "2.00", expected: numberLiteral("2.00") }, + const cases: { input: string, expected: SourceToken }[] = [ + { + input: "0", + expected: { + type: "number literal", + value: "0", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, + }, + }, + { + input: "1", + expected: { + type: "number literal", + value: "1", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, + }, + }, + { + input: "1234", + expected: { + type: "number literal", + value: "1234", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 3 }, + }, + }, + }, + { + input: "12.34", + expected: { + type: "number literal", + value: "12.34", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 4 }, + }, + }, + }, ]; - it.each(cases)("get number literal token '$input'", testLexing); + it.each(cases)("lex number literal '$input'", testLex); }); describe("boolean literals", () => { - const cases: { input: string, expected: BooleanLiteral }[] = [ - { input: "참", expected: booleanLiteral("참") }, - { input: "거짓", expected: booleanLiteral("거짓") }, + const cases: { input: string, expected: SourceToken }[] = [ + { + input: "참", + expected: { + type: "boolean literal", + value: "참", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, + }, + }, + { + input: "거짓", + expected: { + type: "boolean literal", + value: "거짓", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, + }, + }, ]; - it.each(cases)("get boolean literal token '$input'", testLexing); + it.each(cases)("lex boolean literal '$input'", testLex); }); describe("string literals", () => { - const cases: { input: string, expected: StringLiteral }[] = [ - { input: "'foo bar'", expected: stringLiteral("foo bar") }, - { input: "'123'", expected: stringLiteral("123") }, - { input: "'!@#$'", expected: stringLiteral("!@#$") }, - { input: "' '", expected: stringLiteral(" ") }, - { input: "'참'", expected: stringLiteral("참") }, - ]; - - it.each(cases)("get string literal token '$input'", testLexing); - }); - - describe("group delimiters", () => { - const cases: { input: string, expected: GroupDelimiter }[] = [ - { input: "(", expected: groupDelimiter("(") }, - { input: ")", expected: groupDelimiter(")") }, - ]; - - it.each(cases)("get group delimiter token '$input'", testLexing); - }); - - describe("block delimiters", () => { - const cases: { input: string, expected: BlockDelimiter }[] = [ - { input: "{", expected: blockDelimiter("{") }, - { input: "}", expected: blockDelimiter("}") }, + const cases: { input: string, expected: SourceToken }[] = [ + { + input: "'foo bar 123 !@# 참'", + expected: { + type: "string literal", + value: "foo bar 123 !@# 참", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 18 }, + }, + }, + }, + { + input: "''", + expected: { + type: "string literal", + value: "", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, + }, + }, ]; - it.each(cases)("get group delimiter token '$input'", testLexing); + it.each(cases)("lex string literal '$input'", testLex); }); describe("keywords", () => { - const cases: { input: string, expected: Keyword }[] = [ - { input: "만약", expected: keyword("만약") }, - { input: "아니면", expected: keyword("아니면") }, - { input: "함수", expected: keyword("함수") }, - { input: "결과", expected: keyword("결과") }, - ]; - - it.each(cases)("get group delimiter token '$input'", testLexing); - }); - - describe("separator", () => { - const cases: { input: string, expected: Separator }[] = [ - { input: ",", expected: separator(",") }, + const cases: { input: string, expected: SourceToken }[] = [ + { + input: "만약", + expected: { + type: "keyword", + value: "만약", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, + }, + }, + { + input: "아니면", + expected: { + type: "keyword", + value: "아니면", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 2 }, + }, + }, + }, + { + input: "함수", + expected: { + type: "keyword", + value: "함수", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, + }, + }, + { + input: "결과", + expected: { + type: "keyword", + value: "결과", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, + }, + }, ]; - it.each(cases)("get separator token '$input'", testLexing); + it.each(cases)("lex keyword '$input'", testLex); }); - describe("illegal", () => { - const cases: { input: string, expected: Illegal }[] = [ - { input: "$", expected: illegal("$") }, - { input: "'foo", expected: illegal("'foo") }, + describe("identifiers", () => { + const cases: { input: string, expected: SourceToken }[] = [ + { + input: "Foo이름123_", + expected: { + type: "identifier", + value: "Foo이름123_", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 8 }, + }, + }, + }, + { + input: "이름Foo123_", + expected: { + type: "identifier", + value: "이름Foo123_", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 8 }, + }, + }, + }, + { + input: "_이름Foo123", + expected: { + type: "identifier", + value: "_이름Foo123", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 8 }, + }, + }, + }, ]; - it.each(cases)("get illegal token '$input'", testLexing); + it.each(cases)("lex identifier '$input'", testLex); }); - describe("end", () => { - const cases: { input: string, expected: End }[] = [ - { input: "", expected: end }, + describe("special", () => { + const cases: { input: string, expected: SourceToken }[] = [ + { + input: "$", + expected: { + type: "illegal", + value: "$", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, + }, + }, + { + input: "'foo", + expected: { + type: "illegal string", + value: "foo", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 4 }, + }, + }, + }, + { + input: "", + expected: { + type: "end", + value: "$end", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, + }, + }, ]; - it.each(cases)("get end token '$input'", testLexing); + it.each(cases)("lex special '$input'", testLex); }); }); describe("multiple tokens", () => { - const cases: { input: string, expectedTokens: TokenType[] }[] = [ - { - input: "12 + 34 * 5 / 67 - 89", - expectedTokens: [ - numberLiteral("12"), - operator("+"), - numberLiteral("34"), - operator("*"), - numberLiteral("5"), - operator("/"), - numberLiteral("67"), - operator("-"), - numberLiteral("89"), - end, - ] - }, - { - input: "_이름 = foo123", - expectedTokens: [ - identifier("_이름"), - operator("="), - identifier("foo123"), - end, - ] - }, + const cases: { input: string, expectedTokens: SourceToken[] }[] = [ { - input: "'foo' 'bar'", + input: "12 + 34", expectedTokens: [ - stringLiteral("foo"), - stringLiteral("bar"), - end, + { + type: "number literal", + value: "12", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, + }, + { + type: "operator", + value: "+", + range: { + begin: { row: 0, col: 3 }, + end: { row: 0, col: 3 }, + }, + }, + { + type: "number literal", + value: "34", + range: { + begin: { row: 0, col: 5 }, + end: { row: 0, col: 6 }, + }, + }, + { + type: "end", + value: "$end", + range: { + begin: { row: 0, col: 7 }, + end: { row: 0, col: 7 }, + }, + }, ] }, { - input: "만약 참 { \n 12 \n } 아니면 { \n 34 \n}", + input: "만약 참 {\n 12\r\n}", expectedTokens: [ - keyword("만약"), - booleanLiteral("참"), - blockDelimiter("{"), - numberLiteral("12"), - blockDelimiter("}"), - keyword("아니면"), - blockDelimiter("{"), - numberLiteral("34"), - blockDelimiter("}"), - end, + { + type: "keyword", + value: "만약", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, + }, + { + type: "boolean literal", + value: "참", + range: { + begin: { row: 0, col: 3 }, + end: { row: 0, col: 3 }, + }, + }, + { + type: "block delimiter", + value: "{", + range: { + begin: { row: 0, col: 5 }, + end: { row: 0, col: 5 }, + }, + }, + { + type: "number literal", + value: "12", + range: { + begin: { row: 1, col: 2 }, + end: { row: 1, col: 3 }, + }, + }, + { + type: "block delimiter", + value: "}", + range: { + begin: { row: 2, col: 0 }, + end: { row: 2, col: 0 }, + }, + }, + { + type: "end", + value: "$end", + range: { + begin: { row: 2, col: 1 }, + end: { row: 2, col: 1 }, + }, + }, ] }, - { - input: "함수(사과, 바나나) { 결과 사과 + 바나나 }", - expectedTokens:[ - keyword("함수"), - groupDelimiter("("), - identifier("사과"), - separator(","), - identifier("바나나"), - groupDelimiter(")"), - blockDelimiter("{"), - keyword("결과"), - identifier("사과"), - operator("+"), - identifier("바나나"), - blockDelimiter("}"), - ], - }, ]; it.each(cases)("get tokens from input '$input'", ({ input, expectedTokens }) => { const lexer = new Lexer(input); for (const expected of expectedTokens) { - const token = lexer.getToken(); + const token = lexer.getSourceToken(); expect(token).toEqual(expected); } }); }); - - describe("no token", () => { - it("get only end token", () => { - const input = " \r\r\n\n\t\t"; - const expected = end; - - const lexer = new Lexer(input); - - const token = lexer.getToken(); - expect(token).toEqual(expected); - }); - }); }); diff --git a/src/lexer/index.ts b/src/lexer/index.ts index c464bbc..4ccccb8 100644 --- a/src/lexer/index.ts +++ b/src/lexer/index.ts @@ -1,6 +1,28 @@ -import CharBuffer from "./char-buffer"; -import * as Token from "./token"; -import * as Util from "./util"; +import CharBuffer, { type SourceChar } from "./char-buffer"; + +import { + createOperatorToken, + createGroupDelimiterToken, + createBlockDelimiterToken, + createSeparatorToken, + createIllegalToken, + createIllegalStringLiteralToken, + createNumberLiteralToken, + createBooleanLiteralToken, + createStringLiteralToken, + createKeywordToken, + createIdentifierToken, + createEndToken, +} from "./source-token"; +import type { + SourceToken, + OperatorToken, + NumberLiteralToken, + StringLiteralToken, + IllegalStringLiteralToken, +} from "./source-token"; +import type { Position } from "../util/position"; +import { isDigit, isLetter, isWhitespace } from "./util"; export default class Lexer { private readonly charBuffer: CharBuffer; @@ -9,233 +31,300 @@ export default class Lexer { this.charBuffer = new CharBuffer(input); } - getToken(): Token.TokenType { - this.skipWhitespaces(); + getSourceToken(): SourceToken { + this.skipWhitespaceChars(); - const char = this.charBuffer.peek(); - switch (char) { + const char = this.charBuffer.peekChar(); + switch (char.value) { case "+": case "-": case "*": case "/": { - const operator = this.charBuffer.pop() as typeof char; - return Token.operator(operator); + const { position } = this.charBuffer.popChar(); + const value = char.value; + + const token = createOperatorToken(value, position, position); + return token; } case "(": case ")": { - const delimiter = this.charBuffer.pop() as typeof char; - return Token.groupDelimiter(delimiter); + const { position } = this.charBuffer.popChar(); + const value = char.value; + + const token = createGroupDelimiterToken(value, position, position); + return token; } case "{": case "}": { - const delimiter = this.charBuffer.pop() as typeof char; - return Token.blockDelimiter(delimiter); + const { position } = this.charBuffer.popChar(); + const value = char.value; + + const token = createBlockDelimiterToken(value, position, position); + return token; } case ",": { - const separator = this.charBuffer.pop() as typeof char; - return Token.separator(separator); + const { position } = this.charBuffer.popChar(); + const value = char.value; + + const token = createSeparatorToken(value, position, position); + return token; } case "!": { - this.charBuffer.pop(); + const { position: pos } = this.charBuffer.popChar(); - const operator = this.readOperatorStartingWithBang(); - return Token.operator(operator); + const token = this.lexCharsStartingWithBang(pos); + return token; } case "=": { - this.charBuffer.pop(); + const { position: pos } = this.charBuffer.popChar(); - const operator: "=" | "==" = this.readOperatorStartingWithEqual(); - return Token.operator(operator); + const token = this.lexCharsStartingWithEqual(pos); + return token; } case ">": { - this.charBuffer.pop(); + const { position: pos } = this.charBuffer.popChar(); - const operator: ">" | ">=" = this.readOperatorStartingWithGreaterThan(); - return Token.operator(operator); + const token = this.lexCharsStartingWithGreaterThan(pos); + return token; } case "<": { - this.charBuffer.pop(); + const { position: pos } = this.charBuffer.popChar(); - const operator: "<" | "<=" = this.readOperatorStartingWithLessThan(); - return Token.operator(operator); + const token = this.lexCharsStartingWithLessThan(pos); + return token; } case "'": { - this.charBuffer.pop(); + const { position: pos } = this.charBuffer.popChar(); - const [str, ok] = this.readStringLiteral(); - return ok ? Token.stringLiteral(str) : Token.illegal("'" + str); + const token = this.lexCharsStartingWithSingleQuote(pos); + return token; } case CharBuffer.END_OF_INPUT: - return Token.end; - - default: - if (Util.isDigit(char)) { - const number = this.readNumberLiteral(); + { + const { position: pos } = this.charBuffer.popChar(); - return Token.numberLiteral(number); + const token = createEndToken("$end", pos, pos); + return token; } - if (Util.isLetter(char)) { - const read = this.readLettersAndDigits(); - - // order is important: match keywords first, before identifier - if (read === "참" || read === "거짓") { - return Token.booleanLiteral(read); + default: + { + if (isDigit(char.value)) { + const token = this.lexNumberLiteral(); + return token; } - if ( - read === "만약" || - read === "아니면" || - read === "함수" || - read === "결과" - ) { - return Token.keyword(read); + if (isLetter(char.value)) { + const token = this.lexLetters(); + return token; } - return Token.identifier(read); + const { position } = this.charBuffer.popChar(); + const token = createIllegalToken(char.value, position, position); + return token; } - - this.charBuffer.pop(); - return Token.illegal(char); } } - private skipWhitespaces(): void { - while (Util.isWhitespace(this.charBuffer.peek())) { - this.charBuffer.pop(); + private skipWhitespaceChars(): void { + while (true) { + const char = this.charBuffer.peekChar(); + if (!isWhitespace(char.value)) { + break; + } + + this.charBuffer.popChar(); } } - /** assume the bang character popped */ - private readOperatorStartingWithBang(): "!" | "!=" { - switch (this.charBuffer.peek()) { + /** assumes the bang character popped */ + private lexCharsStartingWithBang(bangPos: Position): OperatorToken { + const peek = this.charBuffer.peekChar(); + switch (peek.value) { case "=": - this.charBuffer.pop(); - return "!="; + { + const { position: posEnd } = this.charBuffer.popChar(); + return createOperatorToken("!=", bangPos, posEnd); + } default: - return "!"; + return createOperatorToken("!", bangPos, bangPos); } } - /** assume the equal character popped */ - private readOperatorStartingWithEqual(): "=" | "==" { - switch (this.charBuffer.peek()) { + /** assumes the equal character popped */ + private lexCharsStartingWithEqual(equalPos: Position): OperatorToken { + const peek = this.charBuffer.peekChar(); + switch (peek.value) { case "=": - this.charBuffer.pop(); - return "=="; + { + const { position: posEnd } = this.charBuffer.popChar(); + return createOperatorToken("==", equalPos, posEnd); + } default: - return "="; + return createOperatorToken("=", equalPos, equalPos); } } /** assume the greater-than character popped */ - private readOperatorStartingWithGreaterThan(): ">" | ">=" { - switch (this.charBuffer.peek()) { + private lexCharsStartingWithGreaterThan(greaterThanPos: Position): OperatorToken { + const peek = this.charBuffer.peekChar(); + switch (peek.value) { case "=": - this.charBuffer.pop(); - return ">="; + { + const { position: posEnd } = this.charBuffer.popChar(); + return createOperatorToken(">=", greaterThanPos, posEnd); + } default: - return ">"; + return createOperatorToken(">", greaterThanPos, greaterThanPos); } } /** assume the less-than character popped */ - private readOperatorStartingWithLessThan(): "<" | "<=" { - switch (this.charBuffer.peek()) { + private lexCharsStartingWithLessThan(lessThanPos: Position): OperatorToken { + const peek = this.charBuffer.peekChar(); + switch (peek.value) { case "=": - this.charBuffer.pop(); - return "<="; + { + const { position: posEnd } = this.charBuffer.popChar(); + return createOperatorToken("<=", lessThanPos, posEnd); + } default: - return "<"; + return createOperatorToken("<", lessThanPos, lessThanPos); } } - /** return [string-literal, true] if ok; otherwise [string-read-so-far, false] */ - private readStringLiteral(): [string, boolean] { - const read: string[] = []; + /** assume the single quote character popped */ + private lexCharsStartingWithSingleQuote(quotePos: Position): StringLiteralToken | IllegalStringLiteralToken { + const chars: SourceChar[] = []; - // read string until string closing symbol (') while (true) { - const char = this.charBuffer.pop(); + const char = this.charBuffer.popChar(); - if (char === "'" || char === CharBuffer.END_OF_INPUT) { - const str = read.join(""); - const legalString = char === "'"; // true if closing quote encoutered + const value = chars.map(char => char.value).join(""); + const posBegin = quotePos; + const posEnd = char.position; - return [str, legalString]; + if (char.value === "'") { + return createStringLiteralToken(value, posBegin, posEnd); } - read.push(char); + if (char.value === CharBuffer.END_OF_INPUT) { + return createIllegalStringLiteralToken(value, posBegin, posEnd); + } + + chars.push(char); } } - private readNumberLiteral(): string { - const wholeNumberPart = this.readDigits(); - const decimalPart = this.readDecimalPart(); + private lexNumberLiteral(): NumberLiteralToken { + const wholeNumberPart = this.readDigitChars(); + const decimalPart = this.readDecimalChars(); + const numberChars = wholeNumberPart.concat(decimalPart); + + const value = numberChars.map(char => char.value).join(""); + const posBegin = numberChars[0].position; + const posEnd = numberChars[numberChars.length-1].position; + + const token = createNumberLiteralToken(value, posBegin, posEnd); + return token; + } + + private lexLetters(): any { + const letterChars = this.readLetterChars(); - const number = wholeNumberPart + decimalPart; + const value = letterChars.map(char => char.value).join(""); + const posBegin = letterChars[0].position; + const posEnd = letterChars[letterChars.length-1].position; + + // order is important; match keywords first, then identifier + switch (value) { + case "참": + case "거짓": + { + const token = createBooleanLiteralToken(value, posBegin, posEnd); + return token; + } + + case "만약": + case "아니면": + case "함수": + case "결과": + { + const token = createKeywordToken(value, posBegin, posEnd); + return token; + } - return number; + default: + { + const token = createIdentifierToken(value, posBegin, posEnd); + return token; + } + } } - private readDigits(): string { - const read: string[] = []; - while (Util.isDigit(this.charBuffer.peek())) { - read.push(this.charBuffer.pop()); + private readDigitChars(): SourceChar[] { + const chars: SourceChar[] = []; + while (true) { + const peek = this.charBuffer.peekChar(); + if (!isDigit(peek.value)) { + break; + } + + chars.push(this.charBuffer.popChar()); } - const digits = read.join(""); - return digits; + return chars; } - /** helper function for readNumberLiteral() method */ - private readDecimalPart(): string { + private readDecimalChars(): SourceChar[] { // read decimal point; if not, early return - const maybeDecimalPoint = this.charBuffer.peek(); - if (maybeDecimalPoint !== ".") { - return ""; + const maybeDot = this.charBuffer.peekChar(); + if (maybeDot.value !== ".") { + return []; } - const decimalPoint = this.charBuffer.pop(); + const dot = this.charBuffer.popChar(); // read and return decimal part - const digits = this.readDigits(); - const decimalPart = decimalPoint + digits; - return decimalPart; + const digits = this.readDigitChars(); + const decimalChars = [dot].concat(digits); + return decimalChars; } - private readLettersAndDigits(): string { - const read = []; - while ( - Util.isLetter(this.charBuffer.peek()) || - Util.isDigit(this.charBuffer.peek()) - ) { - read.push(this.charBuffer.pop()); + private readLetterChars(): SourceChar[] { + const chars: SourceChar[] = []; + while (true) { + const peek = this.charBuffer.peekChar(); + if (!isLetter(peek.value) && !isDigit(peek.value)) { + break; + } + + chars.push(this.charBuffer.popChar()); } - return read.join(""); + return chars; } } -export type { TokenType } from "./token"; +export type { SourceToken } from "./source-token"; diff --git a/src/lexer/source-token/base/index.ts b/src/lexer/source-token/base/index.ts new file mode 100644 index 0000000..79f4abe --- /dev/null +++ b/src/lexer/source-token/base/index.ts @@ -0,0 +1,30 @@ +import type { Position, Range } from "../../../util/position"; + +export interface SourceTokenBase { + readonly type: T, + readonly value: V, + readonly range: Range, +}; + +/** returns overloaded token creator function */ +export function createTokenCreator(type: T["type"]) { + // explicitly specify the return type since the overloaded function cannot infer it + type Token = { type: T["type"], value: T["value"], range: Range }; + + function createToken(value: T["value"], range: Range): Token; + function createToken(value: T["value"], pos1: Position, pos2: Position): Token; + function createToken(value: T["value"], arg1: Position | Range, pos2?: Position): Token { + if (pos2 !== undefined) { + const range = { begin: arg1 as Position, end: pos2 as Position }; + return { type, value, range }; + } + + return { type, value, range: arg1 as { begin: Position, end: Position } }; + }; + + return createToken; +}; + +declare function createToken(value: T["value"], range: Range): T; +declare function createToken(value: T["value"], pos1: Position, pos2: Position): T; +export type CreateToken = typeof createToken; diff --git a/src/lexer/source-token/delimiter/index.test.ts b/src/lexer/source-token/delimiter/index.test.ts new file mode 100644 index 0000000..0aeff2c --- /dev/null +++ b/src/lexer/source-token/delimiter/index.test.ts @@ -0,0 +1,96 @@ +import { + createGroupDelimiterToken, + createBlockDelimiterToken, + createSeparatorToken, +} from "./"; +import { fakePos } from "../testing/fixtures"; + +describe("create token with begin and end position", () => { + const cases = [ + { + name: "group delimiter", + token: createGroupDelimiterToken("(", fakePos, fakePos), + expected: { + type: "group delimiter", + value: "(", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "block delimiter", + token: createBlockDelimiterToken("{", fakePos, fakePos), + expected: { + type: "block delimiter", + value: "{", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "separator", + token: createSeparatorToken(",", fakePos, fakePos), + expected: { + type: "separator", + value: ",", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + ]; + + it.each(cases)("create $name token", ({ token, expected }) => { + expect(token).toEqual(expected); + }); +}); + +describe("create token with range", () => { + const cases = [ + { + name: "group delimiter", + token: createGroupDelimiterToken("(", { begin: fakePos, end: fakePos }), + expected: { + type: "group delimiter", + value: "(", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "block delimiter", + token: createBlockDelimiterToken("{", { begin: fakePos, end: fakePos }), + expected: { + type: "block delimiter", + value: "{", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "separator", + token: createSeparatorToken(",", { begin: fakePos, end: fakePos }), + expected: { + type: "separator", + value: ",", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + ]; + + it.each(cases)("create $name token", ({ token, expected }) => { + expect(token).toEqual(expected); + }); +}); diff --git a/src/lexer/source-token/delimiter/index.ts b/src/lexer/source-token/delimiter/index.ts new file mode 100644 index 0000000..79109b3 --- /dev/null +++ b/src/lexer/source-token/delimiter/index.ts @@ -0,0 +1,14 @@ +import type { SourceTokenBase, CreateToken } from "./../base"; +import { createTokenCreator } from "../base"; + +export type GroupDelimiterValue = "(" | ")"; +export type BlockDelimiterValue = "{" | "}"; +export type SeparatorValue = ","; + +export type GroupDelimiterToken = SourceTokenBase<"group delimiter", GroupDelimiterValue>; +export type BlockDelimiterToken = SourceTokenBase<"block delimiter", BlockDelimiterValue>; +export type SeparatorToken = SourceTokenBase<"separator", SeparatorValue>; + +export const createGroupDelimiterToken: CreateToken = createTokenCreator("group delimiter"); +export const createBlockDelimiterToken: CreateToken = createTokenCreator("block delimiter"); +export const createSeparatorToken: CreateToken = createTokenCreator("separator"); diff --git a/src/lexer/source-token/identifier/index.test.ts b/src/lexer/source-token/identifier/index.test.ts new file mode 100644 index 0000000..e86d0a3 --- /dev/null +++ b/src/lexer/source-token/identifier/index.test.ts @@ -0,0 +1,71 @@ +import { + createIdentifierToken, + createKeywordToken, +} from "./"; +import { fakePos } from "../testing/fixtures"; + +describe("create token with begin and end position", () => { + const cases = [ + { + name: "identifier", + token: createIdentifierToken("foo", fakePos, fakePos), + expected: { + type: "identifier", + value: "foo", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "keyword", + token: createKeywordToken("만약", fakePos, fakePos), + expected: { + type: "keyword", + value: "만약", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + ]; + + it.each(cases)("create $name token", ({ token, expected }) => { + expect(token).toEqual(expected); + }); +}); + +describe("create token with range", () => { + const cases = [ + { + name: "identifier", + token: createIdentifierToken("foo", { begin: fakePos, end: fakePos }), + expected: { + type: "identifier", + value: "foo", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "keyword", + token: createKeywordToken("만약", { begin: fakePos, end: fakePos }), + expected: { + type: "keyword", + value: "만약", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + ]; + + it.each(cases)("create $name token", ({ token, expected }) => { + expect(token).toEqual(expected); + }); +}); diff --git a/src/lexer/source-token/identifier/index.ts b/src/lexer/source-token/identifier/index.ts new file mode 100644 index 0000000..2fb4943 --- /dev/null +++ b/src/lexer/source-token/identifier/index.ts @@ -0,0 +1,13 @@ +import type { SourceTokenBase, CreateToken } from "./../base"; +import { createTokenCreator } from "../base"; + +export type KeywordValue = BranchKeywordValue | FunctionKeywordValue | ReturnKeywordValue; +export type BranchKeywordValue = "만약" | "아니면"; +export type FunctionKeywordValue = "함수"; +export type ReturnKeywordValue = "결과"; + +export type IdentifierToken = SourceTokenBase<"identifier", string>; +export type KeywordToken = SourceTokenBase<"keyword", KeywordValue>; + +export const createIdentifierToken: CreateToken = createTokenCreator("identifier"); +export const createKeywordToken: CreateToken = createTokenCreator("keyword"); diff --git a/src/lexer/source-token/index.ts b/src/lexer/source-token/index.ts new file mode 100644 index 0000000..e3f71a2 --- /dev/null +++ b/src/lexer/source-token/index.ts @@ -0,0 +1,29 @@ +import type { OperatorToken } from "./operator"; +import type { IdentifierToken, KeywordToken } from "./identifier"; +import type { NumberLiteralToken, BooleanLiteralToken, StringLiteralToken } from "./literal"; +import type { GroupDelimiterToken, BlockDelimiterToken, SeparatorToken } from "./delimiter"; +import type { IllegalToken, IllegalStringLiteralToken, EndToken } from "./special"; + +export type SourceToken = OperatorToken + | IdentifierToken + | KeywordToken + | NumberLiteralToken + | BooleanLiteralToken + | StringLiteralToken + | GroupDelimiterToken + | BlockDelimiterToken + | SeparatorToken + | IllegalToken + | IllegalStringLiteralToken + | EndToken + +export * from "./operator"; +export type * from "./operator"; +export * from "./identifier"; +export type * from "./identifier"; +export * from "./literal"; +export type * from "./literal"; +export * from "./delimiter"; +export type * from "./delimiter"; +export * from "./special"; +export type * from "./special"; diff --git a/src/lexer/source-token/literal/index.test.ts b/src/lexer/source-token/literal/index.test.ts new file mode 100644 index 0000000..43509ec --- /dev/null +++ b/src/lexer/source-token/literal/index.test.ts @@ -0,0 +1,96 @@ +import { + createNumberLiteralToken, + createBooleanLiteralToken, + createStringLiteralToken, +} from "./"; +import { fakePos } from "../testing/fixtures"; + +describe("create token with begin and end position", () => { + const cases = [ + { + name: "number literal", + token: createNumberLiteralToken("0", fakePos, fakePos), + expected: { + type: "number literal", + value: "0", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "boolean literal", + token: createBooleanLiteralToken("참", fakePos, fakePos), + expected: { + type: "boolean literal", + value: "참", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "string literal", + token: createStringLiteralToken("foo", fakePos, fakePos), + expected: { + type: "string literal", + value: "foo", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + ]; + + it.each(cases)("create $name token", ({ token, expected }) => { + expect(token).toEqual(expected); + }); +}); + +describe("create token with range", () => { + const cases = [ + { + name: "number literal", + token: createNumberLiteralToken("0", { begin: fakePos, end: fakePos }), + expected: { + type: "number literal", + value: "0", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "boolean literal", + token: createBooleanLiteralToken("참", { begin: fakePos, end: fakePos }), + expected: { + type: "boolean literal", + value: "참", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "string literal", + token: createStringLiteralToken("foo", { begin: fakePos, end: fakePos }), + expected: { + type: "string literal", + value: "foo", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + ]; + + it.each(cases)("create $name token", ({ token, expected }) => { + expect(token).toEqual(expected); + }); +}); diff --git a/src/lexer/source-token/literal/index.ts b/src/lexer/source-token/literal/index.ts new file mode 100644 index 0000000..4377e42 --- /dev/null +++ b/src/lexer/source-token/literal/index.ts @@ -0,0 +1,12 @@ +import type { SourceTokenBase, CreateToken } from "./../base"; +import { createTokenCreator } from "../base"; + +export type BooleanLiteralValue = "참" | "거짓"; + +export type NumberLiteralToken = SourceTokenBase<"number literal", string>; +export type BooleanLiteralToken = SourceTokenBase<"boolean literal", BooleanLiteralValue>; +export type StringLiteralToken = SourceTokenBase<"string literal", string>; + +export const createNumberLiteralToken: CreateToken = createTokenCreator("number literal"); +export const createBooleanLiteralToken: CreateToken = createTokenCreator("boolean literal"); +export const createStringLiteralToken: CreateToken = createTokenCreator("string literal"); diff --git a/src/lexer/source-token/operator/index.test.ts b/src/lexer/source-token/operator/index.test.ts new file mode 100644 index 0000000..33a68d9 --- /dev/null +++ b/src/lexer/source-token/operator/index.test.ts @@ -0,0 +1,46 @@ +import { + createOperatorToken, +} from "./"; +import { fakePos } from "../testing/fixtures"; + +describe("create token with begin and end position", () => { + const cases = [ + { + name: "operator", + token: createOperatorToken("+", fakePos, fakePos), + expected: { + type: "operator", + value: "+", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + ]; + + it.each(cases)("create $name token", ({ token, expected }) => { + expect(token).toEqual(expected); + }); +}); + +describe("create token with range", () => { + const cases = [ + { + name: "operator", + token: createOperatorToken("+", { begin: fakePos, end: fakePos }), + expected: { + type: "operator", + value: "+", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + ]; + + it.each(cases)("create $name token", ({ token, expected }) => { + expect(token).toEqual(expected); + }); +}); diff --git a/src/lexer/source-token/operator/index.ts b/src/lexer/source-token/operator/index.ts new file mode 100644 index 0000000..97d02ff --- /dev/null +++ b/src/lexer/source-token/operator/index.ts @@ -0,0 +1,11 @@ +import type { SourceTokenBase, CreateToken } from "./../base"; +import { createTokenCreator } from "../base"; + +export type OperatorValue = ArithmeticOperatorValue | AssignmentOperatorValue | LogicalOperatorValue; +export type ArithmeticOperatorValue = "+" | "-" | "*" | "/"; +export type AssignmentOperatorValue = "="; +export type LogicalOperatorValue = "!" | "!=" | "==" | ">" | "<" | ">=" | "<="; + +export type OperatorToken = SourceTokenBase<"operator", OperatorValue>; + +export const createOperatorToken: CreateToken = createTokenCreator("operator"); diff --git a/src/lexer/source-token/special/index.test.ts b/src/lexer/source-token/special/index.test.ts new file mode 100644 index 0000000..6aa7a1b --- /dev/null +++ b/src/lexer/source-token/special/index.test.ts @@ -0,0 +1,96 @@ +import { + createIllegalToken, + createIllegalStringLiteralToken, + createEndToken, +} from "./"; +import { fakePos } from "../testing/fixtures"; + +describe("create token with begin and end position", () => { + const cases = [ + { + name: "illegal", + token: createIllegalToken("$", fakePos, fakePos), + expected: { + type: "illegal", + value: "$", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "illegal string", + token: createIllegalStringLiteralToken("foo", fakePos, fakePos), + expected: { + type: "illegal string", + value: "foo", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "end", + token: createEndToken("$end", fakePos, fakePos), + expected: { + type: "end", + value: "$end", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + ]; + + it.each(cases)("create $name token", ({ token, expected }) => { + expect(token).toEqual(expected); + }); +}); + +describe("create token with range", () => { + const cases = [ + { + name: "illegal", + token: createIllegalToken("$end", { begin: fakePos, end: fakePos }), + expected: { + type: "illegal", + value: "$end", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "illegal string", + token: createIllegalStringLiteralToken("foo", { begin: fakePos, end: fakePos }), + expected: { + type: "illegal string", + value: "foo", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "end", + token: createEndToken("$end", { begin: fakePos, end: fakePos }), + expected: { + type: "end", + value: "$end", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + ]; + + it.each(cases)("create $name token", ({ token, expected }) => { + expect(token).toEqual(expected); + }); +}); diff --git a/src/lexer/source-token/special/index.ts b/src/lexer/source-token/special/index.ts new file mode 100644 index 0000000..6e50e57 --- /dev/null +++ b/src/lexer/source-token/special/index.ts @@ -0,0 +1,13 @@ +import type { SourceTokenBase, CreateToken } from "./../base"; +import { createTokenCreator } from "../base"; + +export const END_VALUE = "$end"; // unreadable character '$' used to avoid other token values +type EndValue = typeof END_VALUE; + +export type IllegalToken = SourceTokenBase<"illegal", string>; +export type IllegalStringLiteralToken = SourceTokenBase<"illegal string", string>; +export type EndToken = SourceTokenBase<"end", EndValue>; + +export const createIllegalToken: CreateToken = createTokenCreator("illegal"); +export const createIllegalStringLiteralToken: CreateToken = createTokenCreator("illegal string"); +export const createEndToken: CreateToken = createTokenCreator("end"); diff --git a/src/lexer/source-token/testing/fixtures/index.ts b/src/lexer/source-token/testing/fixtures/index.ts new file mode 100644 index 0000000..441a3d0 --- /dev/null +++ b/src/lexer/source-token/testing/fixtures/index.ts @@ -0,0 +1 @@ +export const fakePos = { row: 0, col: 0 }; diff --git a/src/lexer/token/index.test.ts b/src/lexer/token/index.test.ts deleted file mode 100644 index 70e4927..0000000 --- a/src/lexer/token/index.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { - operator, - identifier, - numberLiteral, - booleanLiteral, - stringLiteral, - groupDelimiter, - separator, - keyword, -} from "./"; -import type { - Operator, - Identifier, - NumberLiteral, - BooleanLiteral, - StringLiteral, - GroupDelimiter, - Separator, - Keyword, -} from "./"; - -describe("operator", () => { - const cases: { input: Operator["value"], expected: Operator }[] = [ - { input: "+", expected: operator("+") }, - { input: "-", expected: operator("-") }, - { input: "*", expected: operator("*") }, - { input: "/", expected: operator("/") }, - { input: "=", expected: operator("=") }, - { input: "!", expected: operator("!") }, - { input: "!=", expected: operator("!=") }, - { input: "==", expected: operator("==") }, - { input: ">", expected: operator(">") }, - { input: "<", expected: operator("<") }, - { input: ">=", expected: operator(">=") }, - { input: "<=", expected: operator("<=") }, - ]; - - it.each(cases)("make operator token for '$input'", ({ input, expected }) => { - const token = operator(input); - - expect(token).toEqual(expected); - }); -}); - -describe("identifier", () => { - const cases: { input: Identifier["value"], expected: Identifier }[] = [ - { input: "foo", expected: identifier("foo") }, - { input: "이름", expected: identifier("이름") }, - { input: "_foo이름123", expected: identifier("_foo이름123") }, - ]; - - it.each(cases)("make identifier token for '$input'", ({ input, expected }) => { - const token = identifier(input); - - expect(token).toEqual(expected); - }); -}); - -describe("number literal", () => { - const cases: { input: NumberLiteral["value"], expected: NumberLiteral }[] = [ - { input: "0", expected: numberLiteral("0") }, - { input: "123", expected: numberLiteral("123") }, - ]; - - it.each(cases)("make number literal token for '$input'", ({ input, expected }) => { - const token = numberLiteral(input); - - expect(token).toEqual(expected); - }); -}); - -describe("boolean literal", () => { - const cases: { input: BooleanLiteral["value"], expected: BooleanLiteral }[] = [ - { input: "참", expected: booleanLiteral("참") }, - { input: "거짓", expected: booleanLiteral("거짓") }, - ]; - - it.each(cases)("make boolean literal token for '$input'", ({ input, expected }) => { - const token = booleanLiteral(input); - - expect(token).toEqual(expected); - }); -}); - -describe("string literal", () => { - const cases: { input: StringLiteral["value"], expected: StringLiteral }[] = [ - { input: "foo bar", expected: stringLiteral("foo bar") }, - { input: " ", expected: stringLiteral(" ") }, - { input: "123", expected: stringLiteral("123") }, - { input: "참", expected: stringLiteral("참") }, - ]; - - it.each(cases)("make string literal token for '$input'", ({ input, expected }) => { - const token = stringLiteral(input); - - expect(token).toEqual(expected); - }); -}); - -describe("group delimiter", () => { - const cases: { input: GroupDelimiter["value"], expected: GroupDelimiter }[] = [ - { input: "(", expected: groupDelimiter("(") }, - { input: ")", expected: groupDelimiter(")") }, - ]; - - it.each(cases)("make group delimiter token for '$input'", ({ input, expected }) => { - const token = groupDelimiter(input); - - expect(token).toEqual(expected); - }); -}); - -describe("separator", () => { - const cases: { input: Separator["value"], expected: Separator }[] = [ - { input: ",", expected: separator(",") }, - ]; - - it.each(cases)("make separator token for '$input'", ({ input, expected }) => { - const token = separator(input); - - expect(token).toEqual(expected); - }); -}); - -describe("keywords", () => { - const cases: { input: Keyword["value"], expected: Keyword }[] = [ - { input: "만약", expected: keyword("만약") }, - { input: "아니면", expected: keyword("아니면") }, - { input: "함수", expected: keyword("함수") }, - { input: "결과", expected: keyword("결과") }, - ]; - - it.each(cases)("make keyword token for '$input'", ({ input, expected }) => { - const token = keyword(input); - - expect(token).toEqual(expected); - }); -}); diff --git a/src/lexer/token/index.ts b/src/lexer/token/index.ts deleted file mode 100644 index ee587de..0000000 --- a/src/lexer/token/index.ts +++ /dev/null @@ -1,135 +0,0 @@ -export type TokenType = - Operator | - Identifier | - NumberLiteral | - BooleanLiteral | - StringLiteral | - GroupDelimiter | - BlockDelimiter | - Separator | - Keyword | - Illegal | - End; - -export const END_VALUE = "$end"; // unreadable character '$' used to avoid other token values - -type OperatorValue = ArithmeticOperatorValue | AssignmentOperatorValue | LogicalOperatorValue; -type ArithmeticOperatorValue = "+" | "-" | "*" | "/"; -type AssignmentOperatorValue = "="; -type LogicalOperatorValue = "!" | "!=" | "==" | ">" | "<" | ">=" | "<="; -type BooleanLiteralValue = "참" | "거짓"; -type GroupDelimiterValue = "(" | ")"; -type BlockDelimiterValue = "{" | "}"; -type SeparatorValue = ","; -type KeywordValue = BranchKeywordValue | FunctionKeywordValue | ReturnKeywordValue; -type BranchKeywordValue = "만약" | "아니면"; -type FunctionKeywordValue = "함수"; -type ReturnKeywordValue = "결과"; -type EndValue = typeof END_VALUE; - -export interface Operator { - type: "operator"; - value: OperatorValue; -} - -export interface Identifier { - type: "identifier"; - value: string; -} - -export interface NumberLiteral { - type: "number literal"; - value: string; -} - -export interface BooleanLiteral { - type: "boolean literal"; - value: BooleanLiteralValue; -} - -export interface StringLiteral { - type: "string literal"; - value: string; -} - -export interface GroupDelimiter { - type: "group delimiter"; - value: GroupDelimiterValue; -} - -export interface BlockDelimiter { - type: "block delimiter"; - value: BlockDelimiterValue; -} - -export interface Separator { - type: "separator", - value: SeparatorValue; -} - -export interface Keyword { - type: "keyword"; - value: KeywordValue; -} - -export interface Illegal { - type: "illegal"; - value: string; -} - -export interface End { - type: "end"; - value: EndValue -} - -export const operator = (value: Operator["value"]): Operator => ({ - type: "operator", - value, -}); - -export const identifier = (value: Identifier["value"]): Identifier => ({ - type: "identifier", - value, -}); - -export const numberLiteral = (value: NumberLiteral["value"]): NumberLiteral => ({ - type: "number literal", - value, -}); - -export const booleanLiteral = (value: BooleanLiteral["value"]): BooleanLiteral => ({ - type: "boolean literal", - value, -}); - -export const stringLiteral = (value: StringLiteral["value"]): StringLiteral => ({ - type: "string literal", - value, -}); - -export const groupDelimiter = (value: GroupDelimiter["value"]): GroupDelimiter => ({ - type: "group delimiter", - value, -}); - -export const blockDelimiter = (value: BlockDelimiter["value"]): BlockDelimiter => ({ - type: "block delimiter", - value, -}); - -export const separator = (value: Separator["value"]): Separator => ({ - type: "separator", - value, -}); - -export const keyword = (value: Keyword["value"]): Keyword => ({ - type: "keyword", - value, -}); - -export const illegal = (value: Illegal["value"]): Illegal => ({ - type: "illegal", - value, -}); - -export const end: End = { type: "end", value: END_VALUE }; diff --git a/src/lexer/util/index.ts b/src/lexer/util/index.ts index 485d6d6..3d2276f 100644 --- a/src/lexer/util/index.ts +++ b/src/lexer/util/index.ts @@ -15,9 +15,9 @@ export const isDigit = (char: string): boolean => { }; export const isWhitespace = (char: string): boolean => { - if (char.length !== 1) { + if (char.length > 2) { return false; } - return /^[ \t\r\n]$/.test(char); + return /^(\r\n|[ \t\r\n])$/.test(char); } diff --git a/src/parser/binding-power.ts b/src/parser/binding-power.ts new file mode 100644 index 0000000..3f4eb64 --- /dev/null +++ b/src/parser/binding-power.ts @@ -0,0 +1,45 @@ +export type BindingPower = number; + +export type BindingPowerEntry = { left: BindingPower, right: BindingPower }; +export type BindingPowers = { [key: string]: BindingPowerEntry }; + +export const bindingPowers: BindingPowers = { + lowest: { left: 0, right: 1 }, + assignment: { left: 31, right: 30 }, + comparison: { left: 41, right: 40 }, + summative: { left: 50, right: 51 }, + productive: { left: 60, right: 61 }, + prefix: { left: 70, right: 71 }, + call: { left: 80, right: 81 }, +}; + +export const getInfixBindingPower = (infix: string): BindingPowerEntry => { + switch (infix) { + case "=": + return bindingPowers.assignment; + + case "==": + case "!=": + case ">": + case "<": + case ">=": + case "<=": + return bindingPowers.comparison; + + case "+": + case "-": + return bindingPowers.summative; + + case "*": + case "/": + return bindingPowers.productive; + + // for function call, it behaves an infix operator which lies between + // function expression and parameter list, e.g, print("hello") + case "(": + return bindingPowers.call; + + default: + return bindingPowers.lowest; + } +}; diff --git a/src/parser/index.test.ts b/src/parser/index.test.ts index 581cec1..e8a788c 100644 --- a/src/parser/index.test.ts +++ b/src/parser/index.test.ts @@ -1,1126 +1,1046 @@ import Lexer from "../lexer"; import Parser from "./"; -import type { Program } from "./syntax-tree"; +import { + ParserError, + BadExpressionError, +} from "./"; +import type { + ProgramNode, +} from "./syntax-node"; -describe("parseProgram()", () => { - const testParsing = ({ input, expected }: { input: string, expected: Program }) => { +type SuccessTestCase = { name: string, input: string, expected: E }; +type FailureTestCase = { name: string, input: string, expected: E }; + +describe("parseSource()", () => { + const createParser = (input: string) => { const lexer = new Lexer(input); const parser = new Parser(lexer); - const node = parser.parseProgram(); + return parser; + }; + + const testSuccess = ({ input, expected }: { input: string, expected: ProgramNode }) => { + const parser = createParser(input); - expect(node).toEqual(expected); + const node = parser.parseSource(); + + expect(node).toMatchObject(expected); }; - describe("assignment", () => { - const cases: { name: string, input: string, expected: Program }[] = [ - { - name: "a single assignment statement", - input: "x = 42", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "assignment", - left: { type: "identifier", value: "x" }, - right: { type: "number node", value: 42 }, - }, - }, - ], - }, - }, - { - name: "multiple assignment statements", - input: "x = 42 한 = 9 _123 = 123", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "assignment", - left: { type: "identifier", value: "x" }, - right: { type: "number node", value: 42 }, - }, - }, - { - type: "expression statement", - expression: { - type: "assignment", - left: { type: "identifier", value: "한" }, - right: { type: "number node", value: 9 }, - }, - }, - { - type: "expression statement", - expression: { - type: "assignment", - left: { type: "identifier", value: "_123" }, - right: { type: "number node", value: 123 }, - }, - }, - ], - }, - }, - ]; + const testFailure = ({ input, expected }: { input: string, expected: typeof ParserError }) => { + const parser = createParser(input); - it.each(cases)("parse $name", testParsing); - }); + expect(() => parser.parseSource()).toThrow(expected); + }; - describe("logical expression", () => { - const cases: { name: string, input: string, expected: Program }[] = [ - { - name: "not operator", - input: "!x", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "prefix expression", - prefix: "!", - expression: { type: "identifier", value: "x" }, - }, - }, - ], - }, - }, - { - name: "double not operator", - input: "!!x", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "prefix expression", - prefix: "!", + describe("creating nodes", () => { + describe("literal expressions", () => { + const cases: SuccessTestCase[] = [ + { + name: "a number literal", + input: "42", + expected: { + type: "program", + statements: [ + { + type: "expression statement", expression: { - type: "prefix expression", - prefix: "!", - expression: { type: "identifier", value: "x" }, - }, - }, - }, - ], - }, - }, - { - name: "equal-to comparison", - input: "x == y", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "==", - left: { type: "identifier", value: "x" }, - right: { type: "identifier", value: "y" }, - }, - }, - ], - }, - }, - { - name: "not-equal-to comparison", - input: "x != y", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "!=", - left: { type: "identifier", value: "x" }, - right: { type: "identifier", value: "y" }, - }, - }, - ], - }, - }, - { - name: "greater-than comparison", - input: "x > y", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: ">", - left: { type: "identifier", value: "x" }, - right: { type: "identifier", value: "y" }, - }, - }, - ], - }, - }, - { - name: "less-than comparison", - input: "x < y", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "<", - left: { type: "identifier", value: "x" }, - right: { type: "identifier", value: "y" }, - }, - }, - ], - }, - }, - { - name: "greater-than-or-equal-to comparison", - input: "x >= y", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: ">=", - left: { type: "identifier", value: "x" }, - right: { type: "identifier", value: "y" }, + type: "number", + value: 42, + } }, - }, - ], + ], + }, }, - }, - { - name: "less-than-or-equal-to comparison", - input: "x <= y", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "<=", - left: { type: "identifier", value: "x" }, - right: { type: "identifier", value: "y" }, + { + name: "a boolean literal", + input: "참", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "boolean", + value: true, + } }, - }, - ], + ], + }, }, - }, - { - name: "left associative comparison", - input: "x <= y == z", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "==", - left: { - type: "infix expression", - infix: "<=", - left: { type: "identifier", value: "x" }, - right: { type: "identifier", value: "y" }, + { + name: "a string literal", + input: "'foo bar'", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "string", + value: "foo bar", }, - right: { type: "identifier", value: "z" }, }, - }, - ], + ], + }, }, - }, - { - name: "complex grouped comparison", - input: "x == (y >= z)", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "==", - left: { type: "identifier", value: "x" }, - right: { - type: "infix expression", - infix: ">=", - left: { type: "identifier", value: "y" }, - right: { type: "identifier", value: "z" }, - }, + { + name: "a identifer literal", + input: "foo", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "identifier", + value: "foo", + } }, - }, - ], + ], + }, }, - }, - ]; + ]; - it.each(cases)("parse $name", testParsing); - }); + it.each(cases)("$name", testSuccess); + }); - describe("return statement", () => { - const cases: { name: string, input: string, expected: Program }[] = [ - { - name: "return number literal", - input: "결과 42", - expected: { - type: "program", - statements: [ - { - type: "return statement", - expression: { type: "number node", value: 42 }, + describe("arithmetic expressions", () => { + describe("single number", () => { + const cases: SuccessTestCase[] = [ + { + name: "positive number", + input: "+42", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "prefix", + prefix: "+", + right: { + type: "number", + value: 42, + }, + } + }, + ], }, - ], - }, - }, - { - name: "return arithmetic expression", - input: "결과 사과 + 바나나", - expected: { - type: "program", - statements: [ - { - type: "return statement", - expression: { - type: "infix expression", - infix: "+", - left: { type: "identifier", value: "사과" }, - right: { type: "identifier", value: "바나나" }, - }, + }, + { + name: "negative number", + input: "-42", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "prefix", + prefix: "-", + right: { + type: "number", + value: 42, + }, + } + }, + ], }, - ], - }, - }, - { - name: "return function", - input: "결과 함수(사과) { 결과 사과 + 1 }", - expected: { - type: "program", - statements: [ - { - type: "return statement", - expression: { - type: "function expression", - parameter: [ - { type: "identifier", value: "사과" }, - ], - body: { - type: "block", - statements: [ - { - type: "return statement", - expression: { - type: "infix expression", - infix: "+", - left: { type: "identifier", value: "사과" }, - right: { type: "number node", value: 1 }, + }, + { + name: "doubly negative number", + input: "--42", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "prefix", + prefix: "-", + right: { + type: "prefix", + prefix: "-", + right: { + type: "number", + value: 42, }, }, - ], + }, }, - }, + ], }, - ], - }, - }, - ]; + }, + ]; - it.each(cases)("parse $name", testParsing); - }); + it.each(cases)("$name", testSuccess); + }); - describe("simple expression", () => { - const cases: { name: string, input: string, expected: Program }[] = [ - { - name: "an identifier", - input: "x", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { type: "identifier", value: "x" }, - }, - ], - }, - }, - { - name: "a number", - input: "123", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { type: "number node", value: 123 }, - }, - ], - }, - }, - { - name: "a negative number", - input: "-42", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "prefix expression", - prefix: "-", - expression: { type: "number node", value: 42 }, - }, - }, - ], - }, - }, - { - name: "a doubly negative number", - input: "--42", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "prefix expression", - prefix: "-", + describe("left associativity", () => { + const leftAssocCases = [ + { infix: "+", name: "left associative addition" }, + { infix: "-", name: "left associative subtraction" }, + { infix: "*", name: "left associative multiplication" }, + { infix: "/", name: "left associative division" }, + ]; + const leftAssocTestCases: SuccessTestCase[] = leftAssocCases.map(({ infix, name }) => ({ + name, + input: `11 ${infix} 22 ${infix} 33`, + expected: { + type: "program", + statements: [ + { + type: "expression statement", expression: { - type: "prefix expression", - prefix: "-", - expression: { type: "number node", value: 42 }, + type: "infix", + infix, + left: { + type: "infix", + infix, + left: { + type: "number", + value: 11, + }, + right: { + type: "number", + value: 22, + }, + }, + right: { + type: "number", + value: 33, + }, }, }, - }, - ], - }, - }, - { - name: "a positive number", - input: "+42", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "prefix expression", - prefix: "+", - expression: { type: "number node", value: 42 }, - }, - }, - ], - }, - }, - { - name: "an addition of two numbers", - input: "42+99", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 42 }, - right: { type: "number node", value: 99 }, - }, - }, - ], - }, - }, - { - name: "an addition with the first negative number", - input: "-42+99", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { - type: "prefix expression", - prefix: "-", - expression: { type: "number node", value: 42 }, + ], + }, + })); + + it.each(leftAssocTestCases)("$name", testSuccess); + }); + + describe("associativity among different operations", () => { + const cases: SuccessTestCase[] = [ + { + name: "four operations", + input: "11+22*33/44-55", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "infix", + infix: "-", + left: { + type: "infix", + infix: "+", + left: { + type: "number", + value: 11, + }, + right: { + type: "infix", + infix: "/", + left: { + type: "infix", + infix: "*", + left: { + type: "number", + value: 22 + }, + right: { + type: "number", + value: 33 + }, + }, + right: { + type: "number", + value: 44, + }, + }, + }, + right: { + type: "number", + value: 55, + }, + }, }, - right: { type: "number node", value: 99 }, - }, + ], }, - ], - }, - }, - { - name: "an addition with the second negative number", - input: "42+-99", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 42 }, - right: { - type: "prefix expression", - prefix: "-", - expression: { type: "number node", value: 99 }, + }, + { + name: "with grouped", + input: "11+(22+33)", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "infix", + infix: "+", + left: { + type: "number", + value: 11, + }, + right: { + type: "infix", + infix: "+", + left: { + type: "number", + value: 22, + }, + right: { + type: "number", + value: 33, + }, + }, + }, }, - }, + ], }, - ], - }, - }, - { - name: "an addition of two negative numbers", - input: "-42+-99", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { - type: "prefix expression", - prefix: "-", - expression: { type: "number node", value: 42 }, - }, - right: { - type: "prefix expression", - prefix: "-", - expression: { type: "number node", value: 99 }, + }, + ]; + + it.each(cases)("$name", testSuccess); + }); + + }); + + describe("logical expressions", () => { + describe("unary operation", () => { + const cases: SuccessTestCase[] = [ + { + name: "negation expression", + input: "!foo", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "prefix", + prefix: "!", + right: { + type: "identifier", + value: "foo", + }, + }, }, - }, + ], }, - ], - }, - }, - { - name: "an addition of two positive numbers", - input: "+42++99", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { - type: "prefix expression", - prefix: "+", - expression: { type: "number node", value: 42 }, - }, - right: { - type: "prefix expression", - prefix: "+", - expression: { type: "number node", value: 99 }, + }, + { + name: "double negation expression", + input: "!!foo", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "prefix", + prefix: "!", + right: { + type: "prefix", + prefix: "!", + right: { + type: "identifier", + value: "foo", + }, + }, + }, }, - }, - }, - ], - }, - }, - { - name: "a subtraction of two numbers", - input: "42-99", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "-", - left: { type: "number node", value: 42 }, - right: { type: "number node", value: 99 }, - }, - }, - ], - }, - }, - { - name: "a multiplication of two numbers", - input: "42*99", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "*", - left: { type: "number node", value: 42 }, - right: { type: "number node", value: 99 }, - }, - }, - ], - }, - }, - { - name: "a division of two numbers", - input: "42/99", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "/", - left: { type: "number node", value: 42 }, - right: { type: "number node", value: 99 }, - }, + ], }, - ], - }, - }, - { - name: "an addition of three numbers, left associative", - input: "42+99+12", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 42 }, - right: { type: "number node", value: 99 }, + }, + ]; + + it.each(cases)("$name", testSuccess); + }); + + describe("binary operation", () => { + const infixCases = [ + { name: "equal-to expression", infix: "==" }, + { name: "not-equal-to expression", infix: "!=" }, + { name: "greater-than expression", infix: ">" }, + { name: "less-than expression", infix: "<" }, + { name: "greater-than-or-equal-to expression", infix: ">=" }, + { name: "less-than-or-equal-to expression", infix: "<=" }, + ]; + const infixTestCases: SuccessTestCase[] = infixCases.map(({ name, infix }) => ({ + name, + input: `foo ${infix} bar`, + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "infix", + infix, + left: { + type: "identifier", + value: "foo", + }, + right: { + type: "identifier", + value: "bar", + }, }, - right: { type: "number node", value: 12 }, }, - }, - ], - }, - }, - { - name: "addition and multiplication", - input: "42+99*12", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 42 }, - right: { - type: "infix expression", - infix: "*", - left: { type: "number node", value: 99 }, - right: { type: "number node", value: 12 }, + ], + }, + })); + + it.each(infixTestCases)("$name", testSuccess); + }); + + describe("right associativity", () => { + const infixCases = [ + { name: "right associative equal-to expression", infix: "==" }, + { name: "right associative not-equal-to expression", infix: "!=" }, + ]; + const infixTestCases: SuccessTestCase[] = infixCases.map(({ name, infix }) => ({ + name, + input: `foo ${infix} bar ${infix} baz`, + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "infix", + infix, + left: { + type: "identifier", + value: "foo", + }, + right: { + type: "infix", + infix, + left: { + type: "identifier", + value: "bar", + }, + right: { + type: "identifier", + value: "baz", + }, + }, }, }, - }, - ], - }, - }, - { - name: "multiplication and addition", - input: "42*99+12", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { - type: "infix expression", - infix: "*", - left: { type: "number node", value: 42 }, - right: { type: "number node", value: 99 }, + ], + }, + })); + + it.each(infixTestCases)("$name", testSuccess); + }); + + describe("grouped expression", () => { + const cases: SuccessTestCase[] = [ + { + name: "equal-to and not-equal-to", + input: "(foo == bar) != baz", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "infix", + infix: "!=", + left: { + type: "infix", + infix: "==", + left: { + type: "identifier", + value: "foo", + }, + right: { + type: "identifier", + value: "bar", + }, + }, + right: { + type: "identifier", + value: "baz", + }, + }, }, - right: { type: "number node", value: 12 }, - }, + ], }, - ], - }, - }, - { - name: "an addition with grouped expression", - input: "12+(34+56)", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 12 }, - right: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 34 }, - right: { type: "number node", value: 56 }, + }, + ]; + + it.each(cases)("$name", testSuccess); + }); + }); + + describe("assignment", () => { + const cases: SuccessTestCase[] = [ + { + name: "a single assignment statement", + input: "x = 42", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "assignment", + left: { + type: "identifier", + value: "x", + }, + right: { + type: "number", + value: 42, + }, }, }, - }, - ], + ], + }, }, - }, - { - name: "an addition with grouped more than once", - input: "12+(34+(56+(78+9)))", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 12 }, - right: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 34 }, + { + name: "right associative assignment", + input: "x = y = 42", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "assignment", + left: { + type: "identifier", + value: "x", + }, right: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 56 }, + type: "assignment", + left: { + type: "identifier", + value: "y", + }, right: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 78 }, - right: { type: "number node", value: 9 }, + type: "number", + value: 42, }, }, }, }, - }, - ], + ], + }, }, - }, - { - name: "arithmetic expression with grouped more than once", - input: "(12*(34/56))+(7-((8+9)*10))", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { - type: "infix expression", - infix: "*", - left: { type: "number node", value: 12 }, - right: { - type: "infix expression", - infix: "/", - left: { type: "number node", value: 34 }, - right: { type: "number node", value: 56 }, + ]; + + it.each(cases)("$name", testSuccess); + }); + + describe("call expressions", () => { + const cases: SuccessTestCase[] = [ + { + name: "call function with identifier", + input: "foo(bar, 42)", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "call", + func: { + type: "identifier", + value: "foo", }, - }, - right: { - type: "infix expression", - infix: "-", - left: { type: "number node", value: 7 }, - right: { - type: "infix expression", - infix: "*", - left: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 8 }, - right: { type: "number node", value: 9 }, + args: [ + { + type: "identifier", + value: "bar", }, - right: { type: "number node", value: 10 }, - }, + { + type: "number", + value: 42, + }, + ], }, }, - }, - ], + ], + }, }, - }, - { - name: "arithmetic expression with floating point numbers", - input: "0.75 + 1.25", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 0.75 }, - right: { type: "number node", value: 1.25 }, + { + name: "call function with function literal", + input: "함수(foo){ foo }(42)", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "call", + func: { + type: "function", + parameters: {}, // omit + body: {}, // omit + }, + args: [ + { + type: "number", + value: 42, + }, + ], + }, }, - }, - ], - }, - }, - { - name: "true boolean literal", - input: "참", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { type: "boolean node", value: true }, - }, - ], + ], + }, }, - }, - { - name: "false boolean literal", - input: "거짓", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { type: "boolean node", value: false }, - }, - ], + ]; + + it.each(cases)("$name", testSuccess); + }); + + describe("function expressions", () => { + const cases: SuccessTestCase[] = [ + { + name: "function expression with parameters", + input: "함수 (foo, bar) { foo + bar }", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "function", + parameters: [ + { + type: "identifier", + value: "foo", + }, + { + type: "identifier", + value: "bar", + }, + ], + body: { + type: "block", + statements: [ + { + type: "expression statement", + expression: { + type: "infix", + }, + }, + ], + }, + }, + }, + ], + }, }, - }, - { - name: "string literal", - input: "'foo bar'", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { type: "string node", value: "foo bar" }, - }, - ], + ]; + + it.each(cases)("$name", testSuccess); + }); + + describe("return statement", () => { + const cases: SuccessTestCase[] = [ + { + name: "return number literal", + input: "결과 42", + expected: { + type: "program", + statements: [ + { + type: "return", + expression: { + type: "number", + value: 42, + }, + }, + ], + }, }, - }, - ]; + ]; - it.each(cases)("parse $name", testParsing); - }); + it.each(cases)("$name", testSuccess); + }); - describe("functions", () => { - const cases: { name: string, input: string, expected: Program }[] = [ - { - name: "function expression with parameters", - input: "함수 (사과, 바나나) { 사과 + 바나나 }", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "function expression", - parameter: [ - { type: "identifier", value: "사과" }, - { type: "identifier", value: "바나나" }, - ], - body: { + describe("branch statement", () => { + const cases: SuccessTestCase[] = [ + { + name: "predicate and consequence", + input: "만약 foo { bar }", + expected: { + type: "program", + statements: [ + { + type: "branch", + predicate: { + type: "identifier", + value: "foo", + }, + consequence: { type: "block", statements: [ { type: "expression statement", expression: { - type: "infix expression", - infix: "+", - left: { type: "identifier", value: "사과" }, - right: { type: "identifier", value: "바나나" }, + type: "identifier", + value: "bar", }, }, ], }, }, - }, - ], + ], + }, }, - }, - { - name: "function expression with no parameters", - input: "함수 () { 사과 + 바나나 }", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "function expression", - parameter: [], - body: { + { + name: "predicate and consequence with alternative", + input: "만약 foo { bar } 아니면 { baz }", + expected: { + type: "program", + statements: [ + { + type: "branch", + predicate: { + type: "identifier", + value: "foo", + }, + consequence: { + type: "block", + statements: [ + { + type: "expression statement", + expression: { + type: "identifier", + value: "bar", + }, + }, + ], + }, + alternative: { type: "block", statements: [ { type: "expression statement", expression: { - type: "infix expression", - infix: "+", - left: { type: "identifier", value: "사과" }, - right: { type: "identifier", value: "바나나" }, + type: "identifier", + value: "baz", }, }, ], }, }, - }, - ], + ], + }, }, - }, - ]; + ]; - it.each(cases)("parse $name", testParsing); + it.each(cases)("$name", testSuccess); + }); }); - describe("calls", () => { - const cases: { name: string, input: string, expected: Program }[] = [ - { - name: "call function without arguments", - input: "과일()", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "call", - functionToCall: { type: "identifier", value: "과일" }, - callArguments: [], - }, + describe("marking positions", () => { + describe("single statements", () => { + describe("literal expressions", () => { + const literalCases = [ + { + name: "number literal", + type: "number", + input: "12345", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 4 }, }, - ], - }, - }, - { - name: "call function with identifier arguments", - input: "과일(사과, 바나나, 포도)", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "call", - functionToCall: { type: "identifier", value: "과일" }, - callArguments: [ - { type: "identifier", value: "사과" }, - { type: "identifier", value: "바나나" }, - { type: "identifier", value: "포도" }, - ], - }, + }, + { + name: "string literal", + type: "string", + input: "'foo bar'", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 8 }, }, - ], - }, - }, - { - name: "call function with expression arguments", - input: "과일(1, 2+3)", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "call", - functionToCall: { type: "identifier", value: "과일" }, - callArguments: [ - { type: "number node", value: 1 }, - { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 2 }, - right: { type: "number node", value: 3 }, - }, - ], - }, + }, + { + name: "boolean literal", + type: "boolean", + input: "거짓", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, }, - ], - }, - }, - { - name: "call function with function literal", - input: "함수(사과, 바나나){사과 + 바나나}(1, 2)", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "call", - functionToCall: { - type: "function expression", - parameter: [ - { type: "identifier", value: "사과" }, - { type: "identifier", value: "바나나" }, - ], - body: { - type: "block", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { type: "identifier", value: "사과" }, - right: { type: "identifier", value: "바나나" }, - }, - }, - ], - }, + }, + { + name: "identifier literal", + type: "identifier", + input: "foo", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 2 }, + }, + }, + ]; + const cases: SuccessTestCase[] = literalCases.map(({ name, input, range, type }) => ({ + name, + input, + expected: { + type: "program", + range, + statements: [ + { + type: "expression statement", + range, + expression: { + type, + range, }, - callArguments: [ - { type: "number node", value: 1 }, - { type: "number node", value: 2 }, - ], }, - }, - ], - }, - }, - ]; + ], + }, + })); - it.each(cases)("parse $name", testParsing); - }); + it.each(cases)("$name", testSuccess); + }); - describe("branch statements", () => { - const cases: { name: string, input: string, expected: Program }[] = [ - { - name: "simple if statement with boolean predicate", - input: "만약 참 { 1 }", - expected: { - type: "program", - statements: [ - { - type: "branch statement", - predicate: { type: "boolean node", value: true }, - consequence: { - type: "block", - statements: [ - { - type: "expression statement", - expression: { type: "number node", value: 1 }, - }, - ], + describe("single expressions", () => { + const cases: SuccessTestCase[] = [ + { + name: "assignment", + input: "x = 42", + expected: { + type: "program", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 5 }, }, + statements: [ + { + type: "expression statement", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 5 }, + }, + expression: { + type: "assignment", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 5 }, + }, + left: { + type: "identifier", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, + }, + right: { + type: "number", + range: { + begin: { row: 0, col: 4 }, + end: { row: 0, col: 5 }, + }, + }, + }, + }, + ], }, - ], - }, - }, - { - name: "simple if statement with expression predicate", - input: "만약 사과 == 1 { 2 }", - expected: { - type: "program", - statements: [ - { - type: "branch statement", - predicate: { - type: "infix expression", - infix: "==", - left: { type: "identifier", value: "사과" }, - right: { type: "number node", value: 1 }, + }, + { + name: "arithmetic expression", + input: "11 + 22", + expected: { + type: "program", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 6 }, }, - consequence: { - type: "block", - statements: [ - { - type: "expression statement", - expression: { type: "number node", value: 2 }, + statements: [ + { + type: "expression statement", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 6 }, }, - ], - }, + expression: { + type: "infix", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 6 }, + }, + left: { + type: "number", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, + }, + right: { + type: "number", + range: { + begin: { row: 0, col: 5 }, + end: { row: 0, col: 6 }, + }, + }, + }, + }, + ], }, - ], - }, - }, - { - name: "simple if-else statement with boolean predicate", - input: "만약 참 { 3 } 아니면 { 4 }", - expected: { - type: "program", - statements: [ - { - type: "branch statement", - predicate: { type: "boolean node", value: true }, - consequence: { - type: "block", - statements: [ - { - type: "expression statement", - expression: { type: "number node", value: 3 }, + }, + { + name: "grouped expression", + input: "(11 + 22)", + expected: { + type: "program", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 8 }, + }, + statements: [ + { + type: "expression statement", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 8 }, + }, + expression: { + type: "infix", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 8 }, + }, + left: { + type: "number", + range: { + begin: { row: 0, col: 1 }, + end: { row: 0, col: 2 }, + }, + }, + right: { + type: "number", + range: { + begin: { row: 0, col: 6 }, + end: { row: 0, col: 7 }, + }, + }, }, - ], + }, + ], + }, + }, + { + name: "function expression", + input: "함수(foo) {\n foo\n}", + expected: { + type: "program", + range: { + begin: { row: 0, col: 0, }, + end: { row: 2, col: 0, }, }, - alternative: { - type: "block", - statements: [ - { - type: "expression statement", - expression: { type: "number node", value: 4 }, + statements: [ + { + type: "expression statement", + expression: { + type: "function", + range: { + begin: { row: 0, col: 0, }, + end: { row: 2, col: 0, }, + }, + body: { + type: "block", + range: { + begin: { row: 0, col: 8, }, + end: { row: 2, col: 0, }, + }, + }, }, - ], + }, + ], + }, + }, + { + name: "call expression", + input: "foo(bar, baz)", + expected: { + type: "program", + range: { }, + statements: [ + { + type: "expression statement", + expression: { + type: "call", + range: { + begin: { row: 0, col: 0, }, + end: { row: 0, col: 12, }, + }, + }, + }, + ], }, - ], - }, - }, - ]; + }, + ]; - it.each(cases)("parse $name", testParsing); - }); + it.each(cases)("$name", testSuccess); + }); - describe("complex expression", () => { - const cases: { name: string, input: string, expected: Program }[] = [ - { - name: "assignment and arithmetic expression", - input: "변수1 = 1 ((변수1 + 변수1) * 변수1)", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "assignment", - left: { type: "identifier", value: "변수1" }, - right: { type: "number node", value: 1 }, + describe("single statements", () => { + const cases: SuccessTestCase[] = [ + { + name: "branch statement", + input: "만약 foo {\n 11\n} 아니면 {\n 22\n}", + expected: { + type: "program", + range: { }, - }, - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "*", - left: { - type: "infix expression", - infix: "+", - left: { type: "identifier", value: "변수1" }, - right: { type: "identifier", value: "변수1" }, + statements: [ + { + type: "branch", + range: { + }, + predicate: { + range: { + begin: { row: 0, col: 3, }, + end: { row: 0, col: 5, }, + }, + }, + consequence: { + type: "block", + range: { + begin: { row: 0, col: 7, }, + end: { row: 2, col: 0, }, + }, + }, + alternative: { + type: "block", + range: { + begin: { row: 2, col: 6, }, + end: { row: 4, col: 0, }, + }, + }, }, - right: { type: "identifier", value: "변수1" }, - }, + ], }, - ], - }, - }, + }, + ]; + + it.each(cases)("$name", testSuccess); + }); + }); + }); + + describe("error handling", () => { + const cases: FailureTestCase[] = [ + { + name: "not parsable expression start", + input: "*3", + expected: BadExpressionError, + } ]; - it.each(cases)("parse $name", testParsing); + it.each(cases)("$name", testFailure); }); }); diff --git a/src/parser/index.ts b/src/parser/index.ts index 78ea614..ab4a025 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -1,410 +1,400 @@ -import { - makeProgram, - makeBlock, - makeIdentifier, - makeAssignment, - makeNumberNode, - makeBooleanNode, - makeStringNode, - makeBranchStatement, - makeReturnStatement, - makeExpressionStatement, - makePrefixExpression, - makeInfixExpression, - makeFunctionExpression, - makeCall, -} from "./syntax-tree"; -import type { - Program, - Block, - Statement, - NumberNode, - BooleanNode, - StringNode, - BranchStatement, - ReturnStatement, - ExpressionStatement, - Expression, - FunctionExpression, - Call, - Identifier, - InfixExpression, -} from "./syntax-tree"; -import Lexer from "../lexer"; -import TokenReader from "./token-reader"; - -type BindingPower = number; -const bindingPower = { - lowest: 0, - assignment: 30, - comparison: 40, - summative: 50, - productive: 60, - prefix: 70, - call: 80, -}; -const getBindingPower = (infix: string): BindingPower => { - switch (infix) { - case "=": - return bindingPower.assignment; - case "==": - case "!=": - case ">": - case "<": - case ">=": - case "<=": - return bindingPower.comparison; - case "+": - case "-": - return bindingPower.summative; - case "*": - case "/": - return bindingPower.productive; - case "(": // when '(' is used infix operator, it behaves as call operator - return bindingPower.call; - default: - return bindingPower.lowest; +import type * as Node from "./syntax-node"; +import * as node from "./syntax-node"; +import { getInfixBindingPower, bindingPowers, type BindingPowerEntry } from "./binding-power"; + +import { copyRange, type Range } from "../util/position"; + +export class ParserError extends Error { + public received: string; + public expected: string; + public range: Range; + + constructor(received: string, expected: string, range: Range) { + super(); + this.received = received; + this.expected = expected; + this.range = range; } }; +export class BadNumberLiteralError extends ParserError {}; +export class BadBooleanLiteralError extends ParserError {}; +export class BadPrefixError extends ParserError {}; +export class BadInfixError extends ParserError {}; +export class BadExpressionError extends ParserError {}; +export class BadGroupDelimiterError extends ParserError {}; +export class BadBlockDelimiterError extends ParserError {}; +export class BadAssignmentError extends ParserError {}; +export class BadFunctionKeywordError extends ParserError {}; +export class BadIdentifierError extends ParserError {}; +export class BadSeparatorError extends ParserError {}; + +import Lexer from "../lexer"; +import SourceTokenReader from "./source-token-reader"; + +type PrefixOperator = "+" | "-" | "!"; +type InfixOperator = "+" | "-" | "*" | "/" | "!=" | "==" | ">" | "<" | ">=" | "<="; + export default class Parser { - private buffer: TokenReader; + private static readonly PREFIX_OPERATORS = ["+", "-", "!"] as const; + private static readonly INFIX_OPERATORS = ["+", "-", "*", "/", "!=", "==", ">", "<", ">=", "<="] as const; + + private reader: SourceTokenReader; constructor(lexer: Lexer) { - this.buffer = new TokenReader(lexer); + this.reader = new SourceTokenReader(lexer); } - parseProgram(): Program { - const program = makeProgram(); + parseSource(): Node.ProgramNode { + const statements: Node.StatementNode[] = []; - while (!this.buffer.isEnd()) { - const statement = this.parseStatement(); - if (statement !== null) { - program.statements.push(statement); - } + while (!this.reader.isEnd()) { + statements.push(this.parseStatement()); } + const firstPos = { row: 0, col: 0 }; + const posBegin = statements.length > 0 ? statements[0].range.begin : firstPos; + const posEnd = statements.length > 0 ? statements[statements.length-1].range.end : firstPos; + + const program = node.createProgramNode({ statements }, posBegin, posEnd); + return program; } - private parseBlock(): Block { - // eat token if block start delimiter; otherwise throw error - const maybeBlockStart = this.buffer.read(); - if (maybeBlockStart.type !== "block delimiter" || maybeBlockStart.value !== "{") { - throw new Error(`expected { but received ${maybeBlockStart.type}`); - } - this.buffer.next(); + private parseBlock(): Node.BlockNode { + const firstToken = this.reader.read(); + this.advanceOrThrow("block delimiter", "{", BadBlockDelimiterError); - // populate statements in block - const statements: Statement[] = []; + const statements: Node.StatementNode[] = []; while (true) { - const token = this.buffer.read(); - - // end block delimiter token and break loop if end of block + const token = this.reader.read(); if (token.type === "block delimiter" && token.value === "}") { - this.buffer.next(); - break; + this.reader.advance(); + + const range = copyRange(firstToken.range.begin, token.range.end); + return node.createBlockNode({ statements }, range); } - // append statement to block const statement = this.parseStatement(); - if (statement !== null) { - statements.push(statement); - } + statements.push(statement); } - - // make and return block - const block = makeBlock(statements); - return block; } - private parseStatement(): Statement { - const token = this.buffer.read(); - - if (token.type === "keyword" && token.value === "만약") { - this.buffer.next(); + private parseStatement(): Node.StatementNode { + const token = this.reader.read(); + const { type, value } = token; + if (type === "keyword" && value === "만약") { return this.parseBranchStatement(); } - if (token.type === "keyword" && token.value === "결과") { - this.buffer.next(); - + if (type === "keyword" && value === "결과") { return this.parseReturnStatement(); } return this.parseExpressionStatement(); } - private parseBranchStatement(): BranchStatement { - const predicate = this.parseExpression(bindingPower.lowest); + private parseBranchStatement(): Node.BranchNode { + const firstToken = this.reader.read(); + this.reader.advance(); + + const predicate = this.parseExpression(bindingPowers.lowest); const consequence = this.parseBlock(); - // eat token if else token; otherwise early return without else block - const maybeElseToken = this.buffer.read(); + const maybeElseToken = this.reader.read(); if (maybeElseToken.type !== "keyword" || maybeElseToken.value !== "아니면") { - const branchStatement = makeBranchStatement(predicate, consequence); - return branchStatement; + const range = { begin: firstToken.range.begin, end: consequence.range.end }; + return node.createBranchNode({ predicate, consequence }, range); } - this.buffer.next(); + this.reader.advance(); - // return statement with else block const alternative = this.parseBlock(); - const branchStatement = makeBranchStatement(predicate, consequence, alternative); - return branchStatement; + const range = { begin: firstToken.range.begin, end: alternative.range.end }; + return node.createBranchNode({ predicate, consequence, alternative }, range); } - private parseReturnStatement(): ReturnStatement { - const expression = this.parseExpression(bindingPower.lowest); + private parseReturnStatement(): Node.ReturnNode { + const firstToken = this.reader.read(); + this.reader.advance(); - return makeReturnStatement(expression); + const expression = this.parseExpression(bindingPowers.lowest); + const range = { begin: firstToken.range.begin, end: expression.range.end }; + return node.createReturnNode({ expression }, range); } - private parseExpressionStatement(): ExpressionStatement { - const expression = this.parseExpression(bindingPower.lowest); + /** return expression statement node, which is just a statement wrapper for an expression */ + private parseExpressionStatement(): Node.ExpressionStatementNode { + const expression = this.parseExpression(bindingPowers.lowest); - return makeExpressionStatement(expression); + const range = expression.range; + return node.createExpressionStatementNode({ expression }, range); } - private parseExpression(threshold: BindingPower): Expression { - let expression = this.parsePrefixExpression(); + private parseExpression(threshold: BindingPowerEntry): Node.ExpressionNode { + let topNode = this.parseExpressionStart(); while (true) { - const nextBindingPower = getBindingPower(this.buffer.read().value); - if (nextBindingPower <= threshold) { + const nextBindingPower = getInfixBindingPower(this.reader.read().value); + if (nextBindingPower.left <= threshold.right) { break; } - const infixExpression = this.parseInfixExpression(expression); + const infixExpression = this.parseExpressionMiddle(topNode); if (infixExpression === null) { break; } - expression = infixExpression; + topNode = infixExpression; } - return expression; + return topNode; } - private parsePrefixExpression(): Expression { - const token = this.buffer.read(); - this.buffer.next(); // eat token + private parseExpressionStart(): Node.ExpressionNode { + const { type, value, range } = this.reader.read(); - if (token.type === "number literal") { - const numberNode = this.parseNumberLiteral(token.value); - return numberNode; + if (type === "number literal") { + return this.parseNumberLiteral(); } - if (token.type === "boolean literal") { - const booleanNode = this.parseBooleanLiteral(token.value); - return booleanNode; + if (type === "boolean literal") { + return this.parseBooleanLiteral(); } - if (token.type === "string literal") { - const stringNode = this.parseStringLiteral(token.value); - return stringNode; + if (type === "string literal") { + return this.parseStringLiteral(); } - if (token.type === "identifier") { - const identifier = makeIdentifier(token.value); - return identifier; + if (type === "identifier") { + return this.parseIdentifier(); } - if ( - token.type === "operator" && - (token.value === "+" || token.value === "-" || token.value === "!") - ) { - const subExpression = this.parseExpression(bindingPower.prefix); - const prefix = token.value; - const expression = makePrefixExpression(prefix, subExpression); - return expression; + if (type === "operator" && this.isPrefixOperator(value)) { + return this.parsePrefix(); } - if (token.type === "keyword" && token.value === "함수") { - const parameters = this.parseParameters(); - const body = this.parseBlock(); - - const functionExpression = makeFunctionExpression(body, parameters); - return functionExpression; + if (type === "keyword" && value === "함수") { + return this.parseFunction(); } - if (token.type === "group delimiter" && token.value === "(") { - const groupedExpression = this.parseExpression(bindingPower.lowest); - - const nextToken = this.buffer.read(); - this.buffer.next(); // eat token - if (nextToken.type !== "group delimiter" || nextToken.value !== ")") { - throw new Error(`expected ) but received ${nextToken.type}`); - } - - return groupedExpression; + if (type === "group delimiter" && value === "(") { + return this.parseGroupedExpression(); } - throw new Error(`bad token type ${token.type} (${token.value}) for prefix expression`); + throw new BadExpressionError(type, "expression", range); } - private parseParameters(): Identifier[] { - const parameters: Identifier[] = []; + /** return node if parsable; null otherwise **/ + private parseExpressionMiddle(left: Node.ExpressionNode): Node.ExpressionNode | null { + const { type, value } = this.reader.read(); + + if (type === "group delimiter" && value === "(") { + if (left.type !== "function" && left.type !== "identifier") { + return null; + } - const maybeGroupStart = this.buffer.read(); - if (maybeGroupStart.type !== "group delimiter" || maybeGroupStart.value !== "(") { - throw new Error(`expected ( but received ${maybeGroupStart.type}`); + return this.parseCall(left); } - this.buffer.next(); - // early return empty parameters if end of parameter list - const maybeIdentifierOrGroupEnd = this.buffer.read(); - this.buffer.next(); - if (maybeIdentifierOrGroupEnd.type === "group delimiter" && maybeIdentifierOrGroupEnd.value === ")") { - return []; + if (type === "operator" && this.isInfixOperator(value)) { + return this.parseInfix(left); } - const maybeIdentifier = maybeIdentifierOrGroupEnd; - // read first parameter - if (maybeIdentifier.type !== "identifier") { - throw new Error(`expected identifier but received ${maybeIdentifier}`); + if (type === "operator" && value === "=" && left.type === "identifier") { + return this.parseAssignment(left); } - const identifier = maybeIdentifier; - parameters.push(identifier); - // read the rest parameters - while (true) { - const maybeCommaOrGroupEnd = this.buffer.read(); - this.buffer.next(); + return null; + } - // break if end of parameter list - if (maybeCommaOrGroupEnd.type === "group delimiter" && maybeCommaOrGroupEnd.value === ")") { - break; - } - const maybeComma = maybeCommaOrGroupEnd; + private parseCall(left: Node.FunctionNode | Node.IdentifierNode): Node.CallNode { + this.advanceOrThrow("group delimiter", "(", BadGroupDelimiterError); - // read comma - if (maybeComma.type !== "separator") { - throw new Error(`expected comma but received ${maybeComma}`); - } + const secondToken = this.reader.read(); + if (secondToken.type === "group delimiter" && secondToken.value === ")") { + this.reader.advance(); // eat delimiter - // read next identifier - const maybeIdentifier = this.buffer.read(); - this.buffer.next(); - if (maybeIdentifier.type !== "identifier") { - throw new Error(`expected identifier but received ${maybeIdentifier}`); + const range = copyRange(left.range.begin, secondToken.range.end); + return node.createCallNode({ func: left, args: [] }, range); + } + + const args = [this.parseExpression(bindingPowers.lowest)]; + while (true) { + const token = this.reader.read(); + if (token.type !== "separator") { + break; } - const identifier = maybeIdentifier; + this.reader.advance(); // eat comma - parameters.push(identifier); + args.push(this.parseExpression(bindingPowers.lowest)); } - return parameters; - } + const lastToken = this.reader.read(); + this.advanceOrThrow("group delimiter", ")", BadGroupDelimiterError); - private parseInfixExpression(left: Expression): Expression | null { - // note: do not eat token and just return null if not parsable - const token = this.buffer.read(); + const range = copyRange(left.range.begin, lastToken.range.end); - if (token.type === "group delimiter" && token.value === "(") { - if (left.type !== "function expression" && left.type !== "identifier") { - return null; - } + return node.createCallNode({ func: left, args }, range); + } - this.buffer.next(); // eat infix token - return this.parseCall(left); - } + private parseAssignment(left: Node.IdentifierNode): Node.ExpressionNode { + const { value, range } = this.reader.read(); + this.reader.advance(); - if (token.type !== "operator") { - return null; + if (value !== "=") { + throw new BadAssignmentError(value, "=", range); } + const infix = value; + + const infixBindingPower = getInfixBindingPower(infix); + const right = this.parseExpression(infixBindingPower); + const assignmentRange = { begin: left.range.begin, end: right.range.end }; + + return node.createAssignmentNode({ left, right }, assignmentRange); + } - const infix = token.value; - if (infix === "=" && left.type === "identifier") { - this.buffer.next(); // eat infix token - const a= this.parseAssignment(left); - return a; + private parseNumberLiteral(): Node.NumberNode { + const { value, range } = this.reader.read(); + this.reader.advance(); + + const parsed = Number(value); + if (Number.isNaN(parsed)) { + throw new BadNumberLiteralError(value, "non NaN", range); } - if ( - infix === "+" || - infix === "-" || - infix === "*" || - infix === "/" || - infix === "!=" || - infix === "==" || - infix === ">" || - infix === "<" || - infix === ">=" || - infix === "<=" - ) { - this.buffer.next(); // eat infix token - return this.parseArithmeticInfixExpression(left, infix); + + return node.createNumberNode({ value: parsed }, range); + } + + private parseBooleanLiteral(): Node.BooleanNode { + const { value, range } = this.reader.read(); + this.reader.advance(); + + let parsed: boolean; + if (value === "참") { + parsed = true; + } else if (value === "거짓") { + parsed = false; + } else { + throw new BadBooleanLiteralError(value, "참, 거짓", range); } - return null; + + return node.createBooleanNode({ value: parsed }, range); } - private parseCall(functionToCall: Identifier | FunctionExpression): Call { - const callArguments = this.parseCallArguments(); + private parseStringLiteral(): Node.StringNode { + const { value, range } = this.reader.read(); + this.reader.advance(); - return makeCall(functionToCall, callArguments); + return node.createStringNode({ value }, range); } - private parseCallArguments(): Expression[] { - const maybeExpressionOrGroupEnd = this.buffer.read(); - if (maybeExpressionOrGroupEnd.type === "group delimiter" && maybeExpressionOrGroupEnd.value === ")") { - this.buffer.next(); + private parseIdentifier(): Node.IdentifierNode { + const { type, value, range } = this.reader.read(); + this.reader.advance(); - return []; + if (type !== "identifier") { + throw new BadIdentifierError(type, "identifier", range); } - const firstArgument = this.parseExpression(bindingPower.lowest); + return node.createIdentifierNode({ value }, range); + } - const callArguments = [firstArgument]; - while (true) { - const maybeComma = this.buffer.read(); - if (maybeComma.type !== "separator") { - break; - } - this.buffer.next(); + private parsePrefix(): Node.PrefixNode { + const { value, range } = this.reader.read(); + this.reader.advance(); - const argument = this.parseExpression(bindingPower.lowest); - callArguments.push(argument); + if (!this.isPrefixOperator(value)) { + throw new BadPrefixError(value, "prefix operator", range); } - // expect ')' - const maybeGroupEnd = this.buffer.read(); - this.buffer.next(); - if (maybeGroupEnd.type !== "group delimiter" || maybeGroupEnd.value !== ")") { - throw new Error(`expect ) but received ${maybeGroupEnd.type}`); - } + const prefix = value; + const right = this.parseExpression(bindingPowers.prefix); - return callArguments; + return node.createPrefixNode({ prefix, right }, range); } - private parseAssignment(left: Identifier): Expression { - const infix = "="; - const infixBindingPower = getBindingPower(infix); + private parseInfix(left: Node.ExpressionNode): Node.InfixNode { + const { value, range } = this.reader.read(); + this.reader.advance(); + + if (!this.isInfixOperator(value)) { + throw new BadInfixError(value, "infix operator", range); + } + const infix = value; + const infixBindingPower = getInfixBindingPower(infix); const right = this.parseExpression(infixBindingPower); + const infixRange = copyRange(left.range.begin, right.range.end); - return makeAssignment(left, right); + return node.createInfixNode({ infix, left, right }, infixRange); } - private parseArithmeticInfixExpression(left: Expression, infix: InfixExpression["infix"]): Expression { - const infixBindingPower = getBindingPower(infix); + private parseFunction(): Node.FunctionNode { + const firstToken = this.reader.read(); - const right = this.parseExpression(infixBindingPower); + this.advanceOrThrow("keyword", "함수", BadFunctionKeywordError); + + const parameters = this.parseParameters(); + const body = this.parseBlock(); - return makeInfixExpression(infix, left, right); + const range = copyRange(firstToken.range.begin, body.range.end); + return node.createFunctionNode({ parameters, body }, range); } - private parseNumberLiteral(literal: string): NumberNode { - const parsedNumber = Number(literal); - if (Number.isNaN(parsedNumber)) { - throw new Error(`expected non-NaN number, but received '${literal}'`); + private parseParameters(): Node.IdentifierNode[] { + this.advanceOrThrow("group delimiter", "(", BadGroupDelimiterError); + + const groupEndOrIdentifier = this.reader.read(); + + // early return if empty parameter list + if (groupEndOrIdentifier.type === "group delimiter" && groupEndOrIdentifier.value === ")") { + this.reader.advance(); + return []; } - return makeNumberNode(parsedNumber); + const parameters = [this.parseIdentifier()]; + + while (true) { + const commaOrGroupEnd = this.reader.read(); + this.reader.advance(); + + if (commaOrGroupEnd.type === "group delimiter" && commaOrGroupEnd.value === ")") { + return parameters; + } + + if (commaOrGroupEnd.type !== "separator") { + throw new BadSeparatorError(commaOrGroupEnd.type, ",", commaOrGroupEnd.range); + } + + parameters.push(this.parseIdentifier()); + } } - private parseBooleanLiteral(literal: "참" | "거짓"): BooleanNode { - const parsedValue = literal === "참"; + private parseGroupedExpression(): Node.ExpressionNode { + this.advanceOrThrow("group delimiter", "(", BadGroupDelimiterError); - return makeBooleanNode(parsedValue); + const expression = this.parseExpression(bindingPowers.lowest); + + this.advanceOrThrow("group delimiter", ")", BadGroupDelimiterError); + + // range change due to group delimiters + const offset = { begin: { row: 0, col: -1 }, end: { row: 0, col: 1 } }; + const range = copyRange(expression.range.begin, expression.range.end, offset); + + return { ...expression, range }; + } + + private advanceOrThrow(type: string, value: string, ErrorClass: typeof ParserError): void { + const token = this.reader.read(); + this.reader.advance(); + + if (token.type !== type || token.value !== value) { + throw new ErrorClass(token.value, value, token.range); + } } - private parseStringLiteral(literal: string): StringNode { - return makeStringNode(literal); + private isPrefixOperator(operator: string): operator is PrefixOperator { + return Parser.PREFIX_OPERATORS.some(prefix => prefix === operator); } -} -export type * from "./syntax-tree"; + private isInfixOperator(operator: string): operator is InfixOperator { + return Parser.INFIX_OPERATORS.some(infix => infix === operator); + } +}; + +export type * from "./syntax-node"; diff --git a/src/parser/source-token-reader/index.test.ts b/src/parser/source-token-reader/index.test.ts new file mode 100644 index 0000000..7e222fd --- /dev/null +++ b/src/parser/source-token-reader/index.test.ts @@ -0,0 +1,76 @@ +import Lexer from "../../lexer"; +import SourceTokenReader from "./"; + +const createReader = (input: string) => { + const lexer = new Lexer(input); + + return new SourceTokenReader(lexer); +}; + +describe("read()", () => { + it("read a token", () => { + const input = "42"; + const reader = createReader(input); + const expected = { + type: "number literal", + value: "42", + range: { + begin: { col: 0, row: 0 }, + end: { col: 1, row: 0 }, + }, + }; + + expect(reader.read()).toEqual(expected); + }); + + it("read the end token if nothing to read", () => { + const input = ""; + const reader = createReader(input); + const expected = { + type: "end", + value: "$end", + range: { + begin: { col: 0, row: 0 }, + end: { col: 0, row: 0 }, + }, + }; + + expect(reader.read()).toEqual(expected); + }); +}); + +describe("advance()", () => { + it("advance to next token", () => { + const input = "42 99"; + const reader = createReader(input); + const expected = { + type: "number literal", + value: "99", + range: { + begin: { col: 3, row: 0 }, + end: { col: 4, row: 0 }, + }, + }; + + reader.advance(); + expect(reader.read()).toEqual(expected); + }); +}); + +describe("isEnd()", () => { + it("return true if end", () => { + const input = ""; + const reader = createReader(input); + const expected = true; + + expect(reader.isEnd()).toEqual(expected); + }); + + it("return false if not end", () => { + const input = "42"; + const reader = createReader(input); + const expected = false; + + expect(reader.isEnd()).toEqual(expected); + }); +}); diff --git a/src/parser/source-token-reader/index.ts b/src/parser/source-token-reader/index.ts new file mode 100644 index 0000000..341117b --- /dev/null +++ b/src/parser/source-token-reader/index.ts @@ -0,0 +1,24 @@ +import Lexer from "../../lexer"; +import type { SourceToken } from "../../lexer"; + +export default class SourceTokenReader { + private readonly lexer: Lexer; + private token: SourceToken; + + constructor(lexer: Lexer) { + this.lexer = lexer; + this.token = lexer.getSourceToken(); + } + + read(): SourceToken { + return this.token; + } + + advance(): void { + this.token = this.lexer.getSourceToken(); + } + + isEnd(): boolean { + return this.token.type === "end"; + } +} diff --git a/src/parser/syntax-node/base/index.ts b/src/parser/syntax-node/base/index.ts new file mode 100644 index 0000000..5b6ec81 --- /dev/null +++ b/src/parser/syntax-node/base/index.ts @@ -0,0 +1,30 @@ +import type { Position, Range } from "../../../util/position"; +import { copyRange } from "../../../util/position"; + +export interface SyntaxNodeBase { + readonly type: T, + readonly range: Range, +}; + +type AdditionalFields> = Omit>; + +export function createNodeCreator>(type: T) { + type Node = { type: T, range: Range } & AdditionalFields; + + function createNode(fields: AdditionalFields, range: Range): Node; + function createNode(fields: AdditionalFields, rangeBegin: Position, rangeEnd: Position): Node; + function createNode(fields: AdditionalFields, arg1: Range | Position, rangeEnd?: Position): Node { + if (rangeEnd !== undefined) { + return { type, range: copyRange(arg1 as Position, rangeEnd), ...fields }; + } + + const range = arg1 as Range; + return { type, range: copyRange(range.begin, range.end), ...fields }; + }; + + return createNode; +}; + +declare function createNode>(fields: AdditionalFields, range: Range): N; +declare function createNode>(fields: AdditionalFields, rangeBegin: Position, rangeEnd: Position): N; +export type CreateNode> = typeof createNode; diff --git a/src/parser/syntax-node/expression/index.test.ts b/src/parser/syntax-node/expression/index.test.ts new file mode 100644 index 0000000..03a520c --- /dev/null +++ b/src/parser/syntax-node/expression/index.test.ts @@ -0,0 +1,103 @@ +import { + createIdentifierNode, + createNumberNode, + createStringNode, + createPrefixNode, + createInfixNode, + createFunctionNode, + createCallNode, + createAssignmentNode, +} from "./"; +import type { + IdentifierNode, + ExpressionNode, +} from "./"; +import type { + BlockNode, +} from "../group"; +import { fakePos } from "../testing/fixtures"; + +const cases = [ + { + name: "identifier", + node: createIdentifierNode({ value: "foo" }, fakePos, fakePos), + expected: { + type: "identifier", + value: "foo", + range: { begin: fakePos, end: fakePos }, + }, + }, + { + name: "number", + node: createNumberNode({ value: 42 }, fakePos, fakePos), + expected: { + type: "number", + value: 42, + range: { begin: fakePos, end: fakePos }, + }, + }, + { + name: "string", + node: createStringNode({ value: "foo" }, fakePos, fakePos), + expected: { + type: "string", + value: "foo", + range: { begin: fakePos, end: fakePos }, + }, + }, + { + name: "prefix", + node: createPrefixNode({ prefix: "+", right: {} as ExpressionNode }, fakePos, fakePos), + expected: { + type: "prefix", + prefix: "+", + right: {}, + range: { begin: fakePos, end: fakePos }, + }, + }, + { + name: "infix", + node: createInfixNode({ infix: "+", left: {} as ExpressionNode, right: {} as ExpressionNode }, fakePos, fakePos), + expected: { + type: "infix", + infix: "+", + left: {}, + right: {}, + range: { begin: fakePos, end: fakePos }, + }, + }, + { + name: "function", + node: createFunctionNode({ parameters: [] as IdentifierNode[], body: {} as BlockNode }, fakePos, fakePos), + expected: { + type: "function", + parameters: [], + body: {}, + range: { begin: fakePos, end: fakePos }, + }, + }, + { + name: "call", + node: createCallNode({ func: {} as IdentifierNode, args: [] as ExpressionNode[] }, fakePos, fakePos), + expected: { + type: "call", + func: {}, + args: [], + range: { begin: fakePos, end: fakePos }, + }, + }, + { + name: "assignment", + node: createAssignmentNode({ left: {} as IdentifierNode, right: {} as ExpressionNode }, fakePos, fakePos), + expected: { + type: "assignment", + left: {}, + right: {}, + range: { begin: fakePos, end: fakePos }, + }, + }, +]; + +it.each(cases)("create $name node", ({ node, expected }) => { + expect(node).toEqual(expected); +}); diff --git a/src/parser/syntax-node/expression/index.ts b/src/parser/syntax-node/expression/index.ts new file mode 100644 index 0000000..9bfb3d9 --- /dev/null +++ b/src/parser/syntax-node/expression/index.ts @@ -0,0 +1,60 @@ +import type { SyntaxNodeBase, CreateNode } from "../base"; +import { createNodeCreator } from "../base"; +import type { BlockNode } from "../group"; + +export type Prefix = "+" | "-" | "!"; +export type Infix = "+" | "-" | "*" | "/" | "=" | "==" | "!=" | ">" | "<" | ">=" | "<="; + +export type ExpressionNode = IdentifierNode + | NumberNode + | BooleanNode + | StringNode + | PrefixNode + | InfixNode + | FunctionNode + | CallNode + | AssignmentNode; + +export interface IdentifierNode extends SyntaxNodeBase<"identifier"> { + value: string, +}; +export interface NumberNode extends SyntaxNodeBase<"number"> { + value: number, +}; +export interface BooleanNode extends SyntaxNodeBase<"boolean"> { + value: boolean, +}; +export interface StringNode extends SyntaxNodeBase<"string"> { + value: string, +}; +export interface PrefixNode extends SyntaxNodeBase<"prefix"> { + prefix: Prefix, + right: ExpressionNode, +}; +export interface InfixNode extends SyntaxNodeBase<"infix"> { + infix: Infix, + left: ExpressionNode, + right: ExpressionNode, +}; +export interface FunctionNode extends SyntaxNodeBase<"function"> { + parameters: IdentifierNode[], + body: BlockNode, +}; +export interface CallNode extends SyntaxNodeBase<"call"> { + func: IdentifierNode | FunctionNode, + args: ExpressionNode[], +}; +export interface AssignmentNode extends SyntaxNodeBase<"assignment"> { + left: ExpressionNode, + right: ExpressionNode, +}; + +export const createIdentifierNode: CreateNode<"identifier", IdentifierNode> = createNodeCreator<"identifier", IdentifierNode>("identifier"); +export const createNumberNode: CreateNode<"number", NumberNode> = createNodeCreator<"number", NumberNode>("number"); +export const createBooleanNode: CreateNode<"boolean", BooleanNode> = createNodeCreator<"boolean", BooleanNode>("boolean"); +export const createStringNode: CreateNode<"string", StringNode> = createNodeCreator<"string", StringNode>("string"); +export const createPrefixNode: CreateNode<"prefix", PrefixNode> = createNodeCreator<"prefix", PrefixNode>("prefix"); +export const createInfixNode: CreateNode<"infix", InfixNode> = createNodeCreator<"infix", InfixNode>("infix"); +export const createFunctionNode: CreateNode<"function", FunctionNode> = createNodeCreator<"function", FunctionNode>("function"); +export const createCallNode: CreateNode<"call", CallNode> = createNodeCreator<"call", CallNode>("call"); +export const createAssignmentNode: CreateNode<"assignment", AssignmentNode> = createNodeCreator<"assignment", AssignmentNode>("assignment"); diff --git a/src/parser/syntax-node/group/index.test.ts b/src/parser/syntax-node/group/index.test.ts new file mode 100644 index 0000000..8231d98 --- /dev/null +++ b/src/parser/syntax-node/group/index.test.ts @@ -0,0 +1,30 @@ +import { + createProgramNode, + createBlockNode, +} from "./"; +import { fakePos } from "../testing/fixtures"; + +const cases = [ + { + name: "program", + node: createProgramNode({ statements: [] }, fakePos, fakePos), + expected: { + type: "program", + statements: [], + range: { begin: fakePos, end: fakePos }, + }, + }, + { + name: "block", + node: createBlockNode({ statements: [] }, fakePos, fakePos), + expected: { + type: "block", + statements: [], + range: { begin: fakePos, end: fakePos }, + }, + }, +]; + +it.each(cases)("create $name node", ({ node, expected }) => { + expect(node).toEqual(expected); +}); diff --git a/src/parser/syntax-node/group/index.ts b/src/parser/syntax-node/group/index.ts new file mode 100644 index 0000000..099f83c --- /dev/null +++ b/src/parser/syntax-node/group/index.ts @@ -0,0 +1,17 @@ +import type { SyntaxNodeBase, CreateNode } from "../base"; +import { createNodeCreator } from "../base"; +import type { StatementNode } from "../statement"; + +export type GroupNode = ProgramNode | BlockNode; + +/** a root node for a syntax tree of a program */ +export interface ProgramNode extends SyntaxNodeBase<"program"> { + statements: StatementNode[], +}; +/** a group of statements */ +export interface BlockNode extends SyntaxNodeBase<"block"> { + statements: StatementNode[], +}; + +export const createProgramNode: CreateNode<"program", ProgramNode> = createNodeCreator<"program", ProgramNode>("program"); +export const createBlockNode: CreateNode<"block", BlockNode> = createNodeCreator<"block", BlockNode>("block"); diff --git a/src/parser/syntax-node/index.ts b/src/parser/syntax-node/index.ts new file mode 100644 index 0000000..cdf0e22 --- /dev/null +++ b/src/parser/syntax-node/index.ts @@ -0,0 +1,12 @@ +import type { GroupNode } from "./group"; +import type { StatementNode } from "./statement"; +import type { ExpressionNode } from "./expression"; + +export type SyntaxNode = GroupNode | StatementNode | ExpressionNode; + +export * from "./group"; +export type * from "./group"; +export * from "./statement"; +export type * from "./statement"; +export * from "./expression"; +export type * from "./expression"; diff --git a/src/parser/syntax-node/statement/index.test.ts b/src/parser/syntax-node/statement/index.test.ts new file mode 100644 index 0000000..2c824a5 --- /dev/null +++ b/src/parser/syntax-node/statement/index.test.ts @@ -0,0 +1,58 @@ +import { + createBranchNode, + createReturnNode, + createExpressionStatementNode, +} from "./"; +import type { + ExpressionNode, +} from "../expression"; +import type { + BlockNode, +} from "../group"; +import { fakePos } from "../testing/fixtures"; + +const cases = [ + { + name: "branch", + node: createBranchNode({ predicate: {} as ExpressionNode, consequence: {} as BlockNode }, fakePos, fakePos), + expected: { + type: "branch", + predicate: {}, + consequence: {}, + range: { begin: fakePos, end: fakePos }, + }, + }, + { + name: "branch with alternative", + node: createBranchNode({ predicate: {} as ExpressionNode, consequence: {} as BlockNode, alternative: {} as BlockNode }, fakePos, fakePos), + expected: { + type: "branch", + predicate: {}, + consequence: {}, + alternative: {}, + range: { begin: fakePos, end: fakePos }, + }, + }, + { + name: "return", + node: createReturnNode({ expression: {} as ExpressionNode }, fakePos, fakePos), + expected: { + type: "return", + expression: {}, + range: { begin: fakePos, end: fakePos }, + }, + }, + { + name: "expression statement", + node: createExpressionStatementNode({ expression: {} as ExpressionNode }, fakePos, fakePos), + expected: { + type: "expression statement", + expression: {}, + range: { begin: fakePos, end: fakePos }, + }, + }, +]; + +it.each(cases)("create $name node", ({ node, expected }) => { + expect(node).toEqual(expected); +}); diff --git a/src/parser/syntax-node/statement/index.ts b/src/parser/syntax-node/statement/index.ts new file mode 100644 index 0000000..72b19d8 --- /dev/null +++ b/src/parser/syntax-node/statement/index.ts @@ -0,0 +1,23 @@ +import type { SyntaxNodeBase, CreateNode } from "../base"; +import { createNodeCreator } from "../base"; +import type { BlockNode } from "../group"; +import type { ExpressionNode } from "../expression"; + +export type StatementNode = BranchNode | ReturnNode | ExpressionStatementNode; + +export interface BranchNode extends SyntaxNodeBase<"branch"> { + predicate: ExpressionNode, + consequence: BlockNode, + alternative?: BlockNode, +}; +export interface ReturnNode extends SyntaxNodeBase<"return"> { + expression: ExpressionNode +}; +/** A wrapper type to treat a single expression as a statement. */ +export interface ExpressionStatementNode extends SyntaxNodeBase<"expression statement"> { + expression: ExpressionNode +}; + +export const createBranchNode: CreateNode<"branch", BranchNode> = createNodeCreator<"branch", BranchNode>("branch"); +export const createReturnNode: CreateNode<"return", ReturnNode> = createNodeCreator<"return", ReturnNode>("return"); +export const createExpressionStatementNode: CreateNode<"expression statement", ExpressionStatementNode> = createNodeCreator<"expression statement", ExpressionStatementNode>("expression statement"); diff --git a/src/parser/syntax-node/testing/fixtures/index.ts b/src/parser/syntax-node/testing/fixtures/index.ts new file mode 100644 index 0000000..e6928c3 --- /dev/null +++ b/src/parser/syntax-node/testing/fixtures/index.ts @@ -0,0 +1 @@ +export const fakePos = { col: 0, row: 0 }; diff --git a/src/parser/syntax-tree/expression/index.ts b/src/parser/syntax-tree/expression/index.ts deleted file mode 100644 index e5ea74d..0000000 --- a/src/parser/syntax-tree/expression/index.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Block } from "../group"; - -export type Expression = - Identifier | - NumberNode | - BooleanNode | - StringNode | - PrefixExpression | - InfixExpression | - FunctionExpression | - Call | - Assignment; - -export interface Identifier { - type: "identifier"; - value: string; -} - -export interface NumberNode { - type: "number node"; - value: number; -} - -export interface BooleanNode { - type: "boolean node"; - value: boolean; -} - -export interface StringNode { - type: "string node"; - value: string; -} - -export interface PrefixExpression { - type: "prefix expression"; - prefix: "+" | "-" | "!"; - expression: Expression; -} - -export interface InfixExpression { - type: "infix expression"; - infix: "+" | "-" | "*" | "/" | "=" | "==" | "!=" | ">" | "<" | ">=" | "<="; - left: Expression; - right: Expression; -} - -export interface FunctionExpression { - type: "function expression"; - parameter: Identifier[], - body: Block; -} - -export interface Call { - type: "call"; - functionToCall: Identifier | FunctionExpression; - callArguments: Expression[]; -} - -export interface Assignment { - type: "assignment"; - left: Identifier; - right: Expression; -} - -export const makeIdentifier = (value: Identifier["value"]): Identifier => ({ - type: "identifier", - value, -}); - -export const makeNumberNode = (value: NumberNode["value"]): NumberNode => ({ - type: "number node", - value, -}); - -export const makeBooleanNode = (value: BooleanNode["value"]): BooleanNode => ({ - type: "boolean node", - value, -}); - -export const makeStringNode = (value: StringNode["value"]): StringNode => ({ - type: "string node", - value, -}); - -export const makePrefixExpression = (prefix: PrefixExpression["prefix"], expression: PrefixExpression["expression"]): PrefixExpression => ({ - type: "prefix expression", - prefix, - expression, -}); - -export const makeInfixExpression = (infix: InfixExpression["infix"], left: InfixExpression["left"], right: InfixExpression["right"]): InfixExpression => ({ - type: "infix expression", - infix, - left, - right, -}); - -export const makeFunctionExpression = (body: FunctionExpression["body"], parameter: FunctionExpression["parameter"] = []): FunctionExpression => ({ - type: "function expression", - parameter, - body, -}); - -export const makeCall = (functionToCall: Call["functionToCall"], callArguments: Call["callArguments"]): Call => ({ - type: "call", - functionToCall, - callArguments, -}); - -export const makeAssignment = (left: Assignment["left"], right: Assignment["right"]): Assignment => ({ - type: "assignment", - left, - right, -}); diff --git a/src/parser/syntax-tree/group/index.ts b/src/parser/syntax-tree/group/index.ts deleted file mode 100644 index 82b1170..0000000 --- a/src/parser/syntax-tree/group/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { Statement } from "../statement"; - -export type Group = Program | Block; - -/** a root node for a syntax tree of a program */ -export interface Program { - type: "program"; - statements: Statement[]; -} - -/** a group of statements */ -export interface Block { - type: "block"; - statements: Statement[]; -} - -export const makeProgram = (statements: Program["statements"] = []): Program => ({ - type: "program", - statements, -}); - -export const makeBlock = (statements: Block["statements"] = []): Block => ({ - type: "block", - statements, -}); diff --git a/src/parser/syntax-tree/index.ts b/src/parser/syntax-tree/index.ts deleted file mode 100644 index 38b9138..0000000 --- a/src/parser/syntax-tree/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Group } from "./group"; -import type { Statement } from "./statement"; -import type { Expression } from "./expression"; - -export type Node = Group | Statement | Expression; - -export * from "./expression"; -export * from "./statement"; -export * from "./group"; -export type * from "./expression"; -export type * from "./statement"; -export type * from "./group"; diff --git a/src/parser/syntax-tree/statement/index.ts b/src/parser/syntax-tree/statement/index.ts deleted file mode 100644 index 38603cb..0000000 --- a/src/parser/syntax-tree/statement/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { Block } from "../group"; -import type { Expression } from "../expression"; - -export type Statement = - BranchStatement | - ReturnStatement | - ExpressionStatement; - -export interface BranchStatement { - type: "branch statement"; - predicate: Expression; - consequence: Block; - alternative?: Block; -} - -export interface ReturnStatement { - type: "return statement"; - expression: Expression; -} - -/** A wrapper type to treat a single expression as a statement. */ -export interface ExpressionStatement { - type: "expression statement"; - expression: Expression; -} - -type MakeBranchStatement = ( - predicate: BranchStatement["predicate"], - consequence: BranchStatement["consequence"], - alternative?: BranchStatement["alternative"] -) => BranchStatement; -export const makeBranchStatement: MakeBranchStatement = (predicate, consequence, alternative) => ({ - type: "branch statement", - predicate, - consequence, - alternative, -}); - -type MakeReturnStatement = (expression: ReturnStatement["expression"]) => ReturnStatement; -export const makeReturnStatement: MakeReturnStatement = expression => ({ - type: "return statement", - expression, -}); - -export const makeExpressionStatement = (expression: ExpressionStatement["expression"]): ExpressionStatement => ({ - type: "expression statement", - expression, -}); diff --git a/src/parser/token-reader/index.test.ts b/src/parser/token-reader/index.test.ts deleted file mode 100644 index e345f9c..0000000 --- a/src/parser/token-reader/index.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import Lexer from "../../lexer"; -import TokenReader from "./"; - -describe("read()", () => { - it("read a token", () => { - const input = "42"; - const lexer = new Lexer(input); - const reader = new TokenReader(lexer); - - expect(reader.read()).toEqual({ type: "number literal", value: "42" }); - }); -}); - -describe("next()", () => { - it("read next token", () => { - const input = "42 99"; - const lexer = new Lexer(input); - const reader = new TokenReader(lexer); - - expect(reader.read()).toEqual({ type: "number literal", value: "42" }); - reader.next(); - expect(reader.read()).toEqual({ type: "number literal", value: "99" }); - }); - - it("read end token if end", () => { - const input = ""; - const lexer = new Lexer(input); - const reader = new TokenReader(lexer); - - expect(reader.read().type).toEqual("end"); - reader.next(); - expect(reader.read().type).toEqual("end"); - }); -}); - -describe("isEnd()", () => { - it("return true if end", () => { - const input = ""; - const lexer = new Lexer(input); - const reader = new TokenReader(lexer); - - expect(reader.isEnd()).toBe(true); - }); - - it("return false if not end", () => { - const input = "42"; - const lexer = new Lexer(input); - const reader = new TokenReader(lexer); - - expect(reader.isEnd()).toBe(false); - }); -}); diff --git a/src/parser/token-reader/index.ts b/src/parser/token-reader/index.ts deleted file mode 100644 index f1025a4..0000000 --- a/src/parser/token-reader/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import Lexer from "../../lexer"; -import type { TokenType } from "../../lexer"; - -export default class TokenReader { - private readonly lexer: Lexer; - private token: TokenType; - - constructor(lexer: Lexer) { - this.lexer = lexer; - this.token = lexer.getToken(); - } - - isEnd(): boolean { - return this.token.type === "end"; - } - - read(): TokenType { - return this.token; - } - - next(): void { - this.token = this.lexer.getToken(); - } -} diff --git a/src/util/position/index.ts b/src/util/position/index.ts new file mode 100644 index 0000000..18d8dcb --- /dev/null +++ b/src/util/position/index.ts @@ -0,0 +1,23 @@ +export interface Position { + readonly row: number, + readonly col: number, +} + +export interface Range { + readonly begin: Position, + readonly end: Position, +} + +export const copyPosition = (pos: Position, offset?: Position) => { + const row = pos.row + (offset?.row ?? 0); + const col = pos.col + (offset?.col ?? 0); + + return { row, col }; +} + +export const copyRange = (begin: Position, end: Position, offset?: Range) => { + const copiedBegin = copyPosition(begin, offset?.begin); + const copiedEnd = copyPosition(end, offset?.end); + + return { begin: copiedBegin, end: copiedEnd }; +} diff --git a/tsconfig.json b/tsconfig.json index 932814b..02de301 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "target": "es2015", "module": "commonjs", + "lib": ["es2022"], "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true,