├── .editorconfig ├── .gitignore ├── .gitmodules ├── .travis.yml ├── LICENSE ├── README.md ├── SPEC.md ├── bin ├── kou └── kouc ├── examples ├── factorial.kou └── index.html ├── package-lock.json ├── package.json ├── scripts └── wabt.sh ├── src ├── codegen │ ├── context.ts │ └── index.ts ├── desugarer │ └── index.ts ├── kou.ts ├── kouc.ts ├── lexer │ ├── error.ts │ ├── index.ts │ └── token.ts ├── parser │ ├── ast.ts │ ├── error.ts │ └── index.ts ├── report-error.ts ├── stdlib.ts ├── typechecker │ ├── context.ts │ ├── error.ts │ └── index.ts ├── util.ts └── wasm.ts ├── test ├── codegen.spec.ts ├── desugarer.spec.ts ├── index.ts ├── lexer.spec.ts ├── parser.spec.ts └── typechecker.spec.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # Built result 61 | dist 62 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hatashiro/kou/90d90fa7188d003197b18a7e10c97101e190b018/.gitmodules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "lts/*" 4 | script: 5 | - npm run format:dry 6 | - npm run build 7 | - npm test 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 Hyunje Jun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kou 2 | 3 | A minimal language compiled into wasm bytecode 4 | 5 | [![npm](https://img.shields.io/npm/v/kou.svg?style=flat-square)](https://www.npmjs.com/package/kou) 6 | [![Travis](https://img.shields.io/travis/utatti/kou.svg?style=flat-square)](https://travis-ci.org/utatti/kou) 7 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 8 | 9 | ## Language specification 10 | 11 | Please refer to [SPEC.md](SPEC.md). 12 | 13 | ## Demonstration 14 | 15 | [![asciicast](https://asciinema.org/a/tP2sldS271HxxsKwWJ2RJdTHL.png)](https://asciinema.org/a/tP2sldS271HxxsKwWJ2RJdTHL) 16 | 17 | ## Milestones 18 | 19 | - [x] Tokenizer 20 | - [x] Parser 21 | - [x] Desugarer 22 | - [x] Type checker 23 | - [x] Code generator for wasm 24 | - [x] Basic codegen 25 | - [x] Complex types and expressions 26 | - [ ] [Codegen for string](https://github.com/utatti/kou/issues/1) 27 | - [ ] Module system 28 | - [ ] JS interop 29 | - [ ] IO 30 | - [ ] Bootstrapping 31 | 32 | ## Install 33 | 34 | ``` shell 35 | npm i -g kou 36 | ``` 37 | 38 | ## Usage 39 | 40 | Compile: 41 | 42 | ``` shell 43 | kouc hello.kou -o hello.wasm 44 | 45 | # For the detailed usage 46 | kouc --help 47 | ``` 48 | 49 | Run in CLI: 50 | 51 | ``` shell 52 | kou hello.wasm 53 | 54 | # For the detailed usage 55 | kou --help 56 | ``` 57 | 58 | ## Reference 59 | 60 | - [WebAssembly Specification](https://webassembly.github.io/spec/core/index.html) 61 | - [WebAssembly Reference Manual](https://github.com/sunfishcode/wasm-reference-manual/blob/master/WebAssembly.md) 62 | - [The Go Programming Language Specification](https://golang.org/ref/spec) 63 | 64 | ## License 65 | 66 | [MIT](LICENSE) 67 | -------------------------------------------------------------------------------- /SPEC.md: -------------------------------------------------------------------------------- 1 | # kou Language Specification 2 | 3 | * [Introduction](#introduction) 4 | * [Notation](#notation) 5 | * [Lexical elements](#lexical-elements) 6 | + [Punctuation](#punctuation) 7 | + [Operators](#operators) 8 | + [Keywords](#keywords) 9 | + [Literals](#literals) 10 | + [Identifier](#identifier) 11 | * [Types](#types) 12 | + [Primary types](#primary-types) 13 | + [Function type](#function-type) 14 | + [Tuple type](#tuple-type) 15 | + [Array type](#array-type) 16 | + [Void type](#void-type) 17 | * [Module](#module) 18 | + [Import](#import) 19 | * [Declaration](#declaration) 20 | * [Expressions](#expressions) 21 | + [LitExpr](#litexpr) 22 | + [IdentExpr](#identexpr) 23 | + [TupleExpr](#tupleexpr) 24 | + [ArrayExpr](#arrayexpr) 25 | + [CallExpr](#callexpr) 26 | + [IndexExpr](#indexexpr) 27 | + [FuncExpr](#funcexpr) 28 | + [CondExpr](#condexpr) 29 | + [LoopExpr](#loopexpr) 30 | + [NewExpr](#newexpr) 31 | * [Assignment](#assignment) 32 | * [Break](#break) 33 | * [Block](#block) 34 | 35 | ## Introduction 36 | 37 | This document is a language specification (yet informal) of the kou programming 38 | language. 39 | 40 | ## Notation 41 | 42 | The syntax is specified using Extended Backus-Naur Form (EBNF). 43 | 44 | ``` 45 | | alternation 46 | () grouping 47 | [] option (0 or 1 times) 48 | {} repetition (0 to n times) 49 | ``` 50 | 51 | Lower-case production names are used to identify lexical tokens. Non-terminals 52 | are in CamelCase. Lexical tokens are enclosed in double quotes "". 53 | 54 | ## Lexical elements 55 | 56 | ### Punctuation 57 | 58 | ``` 59 | -> , ( ) [ ] { } : = ; 60 | ``` 61 | 62 | ### Operators 63 | 64 | Unary: 65 | 66 | ``` 67 | unary_op = "+" | "-" | "!" . 68 | ``` 69 | 70 | Binary: 71 | 72 | ``` 73 | binary_op = rel_op | add_op | mul_op | bool_op . 74 | rel_op = "==" | "!=" | "<" | "<=" | ">" | ">=" . 75 | add_op = "+" | "-" | "|" | "^" . 76 | mul_op = "*" | "/" | "%" | "&" . 77 | bool_op = "||" | "&&" . 78 | ``` 79 | 80 | ### Keywords 81 | 82 | ``` 83 | import as let fn if else for in new while break 84 | ``` 85 | 86 | ### Literals 87 | 88 | Integer: 89 | 90 | ``` 91 | decimal_digit = "0" … "9" . 92 | int_lit = decimal_digit { decimal_digit } . 93 | ``` 94 | 95 | Float: 96 | 97 | ``` 98 | decimals = decimal_digit { decimal_digit } . 99 | float_lit = decimals "." [ decimals ] 100 | | "." decimals 101 | ``` 102 | 103 | Char: 104 | 105 | ``` 106 | escaped_char = "\" ( "n" | "r" | "t" | "\" | "'" | """ ) . 107 | char = unicode_char | escaped_char . 108 | char_lit = "'" ( char ) "'" 109 | ``` 110 | 111 | String: 112 | 113 | ``` 114 | string_lit = """ { char } """ . 115 | ``` 116 | 117 | Boolean: 118 | 119 | ``` 120 | bool_lit = "true" | "false" 121 | ``` 122 | 123 | ### Identifier 124 | 125 | ``` 126 | lower_letter = "a" … "z" . 127 | letter = lower_letter | "_" . 128 | ident = letter { letter | decimal_digit } . 129 | ``` 130 | 131 | ## Types 132 | 133 | ``` 134 | Type = PrimType | FuncType | TupleType | ArrayType | VoidType . 135 | ``` 136 | 137 | ### Primary types 138 | 139 | ``` 140 | PrimType = "int" | "float" | "str" | "bool" | "char" . 141 | ``` 142 | 143 | ### Function type 144 | 145 | ``` 146 | FuncType = Type "->" Type . 147 | ``` 148 | 149 | ### Tuple type 150 | 151 | ``` 152 | TupleType = "(" [ Type { "," Type } ] ")" . 153 | ``` 154 | 155 | Semantically, 1-tuple is the same with its inner type, or 1-tuple is desugared 156 | into its inner type. 157 | 158 | Related: [TupleExpr](#tupleexpr) 159 | 160 | ### Array type 161 | 162 | ``` 163 | ArrayType = "[" Type "]" . 164 | ``` 165 | 166 | Related: [ArrayExpr](#arrayexpr) 167 | 168 | ### Void type 169 | 170 | ``` 171 | VoidType = "void" . 172 | ``` 173 | 174 | Void type does not have a value. Any actual value in the type of `"void"` 175 | should result in a semantic error. 176 | 177 | ## Module 178 | 179 | Each file in kou is represented as a module. 180 | 181 | ``` 182 | Module = { Import } { Decl } . 183 | ``` 184 | 185 | ### Import 186 | 187 | ``` 188 | Import = "import" ImportPath 189 | "(" ImportElem { "," ImportElem } ")" . 190 | ImportPath = string_lit . 191 | ImportElem = ident [ "as" ident ] . 192 | ``` 193 | 194 | ## Declaration 195 | 196 | ``` 197 | Decl = "let" ident [ ":" Type ] "=" Expr . 198 | ``` 199 | 200 | ## Expressions 201 | 202 | ``` 203 | Expr = PrimUnaryExpr | BinaryExpr . 204 | BinaryExpr = Expr binary_op Expr . 205 | PrimUnaryExpr = PrimExpr | UnaryExpr . 206 | UnaryExpr = unary_op PrimUnaryExpr 207 | PrimExpr = LitExpr 208 | | IdentExpr 209 | | TupleExpr 210 | | ArrayExpr 211 | | CallExpr 212 | | FuncExpr 213 | | CondExpr 214 | | LoopExpr 215 | | NewExpr. 216 | ``` 217 | 218 | `Expr` stands for *Expression*. 219 | 220 | ### LitExpr 221 | 222 | The name stands for *Literal Expression*. 223 | 224 | ``` 225 | LitExpr = int_lit | float_lit | string_lit | bool_lit | char_lit . 226 | ``` 227 | 228 | ### IdentExpr 229 | 230 | The name stands for *Identifier Expression*. 231 | 232 | ``` 233 | IdentExpr = ident . 234 | ``` 235 | 236 | ### TupleExpr 237 | 238 | ``` 239 | TupleExpr = "(" [ Expr { "," Expr } ] ")" . 240 | ``` 241 | 242 | Semantically, 1-tuple is the same with its inner value, or 1-tuple is desugared 243 | into its inner value. 244 | 245 | Related: [Tuple type](#tuple-type) 246 | 247 | ### ArrayExpr 248 | 249 | ``` 250 | ArrayExpr = "[" Expr { "," Expr } "]" 251 | ``` 252 | 253 | Related: [Array type](#array-type) 254 | 255 | ### CallExpr 256 | 257 | ``` 258 | CallExpr = PrimExpr TupleExpr . 259 | ``` 260 | 261 | Related: [TupleExpr](#tupleexpr) 262 | 263 | ### IndexExpr 264 | 265 | ``` 266 | IndexExpr = PrimExpr "[" Expr "]" . 267 | ``` 268 | 269 | It can be used to retrieve an element from an array or a tuple. 270 | 271 | For the tuple case, the index should be a `LitExpr` having `int_lit`, with a 272 | value in the tuple's size range. 273 | 274 | Related: [Literals](#literals) 275 | 276 | ### FuncExpr 277 | 278 | ``` 279 | FuncExpr = "fn" ParamTuple Type Block . 280 | ParamTuple = "(" [ Param { "," Param } ] ")" . 281 | Param = ident Type . 282 | ``` 283 | 284 | Related: [Block](#block) 285 | 286 | ### CondExpr 287 | 288 | ``` 289 | CondExpr = "if" Expr Block "else" Block . 290 | ``` 291 | 292 | Related: [Block](#block) 293 | 294 | ### LoopExpr 295 | 296 | ``` 297 | LoopExpr = "while" Expr Block . 298 | ``` 299 | 300 | Related: 301 | 302 | - [Block](#block) 303 | - [Break](#break) 304 | 305 | ### NewExpr 306 | 307 | ``` 308 | NewExpr = "new" Type "[" Expr "]" . 309 | ``` 310 | 311 | It creates an array with a specified size. 312 | 313 | Related: 314 | 315 | - [Array type](#array-type) 316 | - [ArrayExpr](#arrayexpr) 317 | 318 | ## Assignment 319 | 320 | ``` 321 | Assign = LVal "=" Expr . 322 | ``` 323 | 324 | ### LVal 325 | 326 | ``` 327 | LVal = IdentExpr 328 | | IndexExpr . 329 | ``` 330 | 331 | Related: 332 | 333 | - [IdentExpr](#identexpr) 334 | - [IndexExpr](#indexexpr) 335 | 336 | ## Break 337 | 338 | ``` 339 | Break = "break" . 340 | ``` 341 | 342 | Break only works in LoopExpr. 343 | 344 | Related: [LoopExpr](#loopexpr) 345 | 346 | ## Block 347 | 348 | ``` 349 | Block = "{" { ( Expr | Decl | Assign | Break ) ";" } [ Expr ] "}" . 350 | ``` 351 | 352 | A block ending without `Expr` (no `";"`) has its return type as `void`, and it 353 | is the only way to express `void` type in kou. 354 | 355 | Related: [Void type](#void-type) 356 | -------------------------------------------------------------------------------- /bin/kou: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../dist/kou'); 3 | -------------------------------------------------------------------------------- /bin/kouc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../dist/kouc'); 3 | -------------------------------------------------------------------------------- /examples/factorial.kou: -------------------------------------------------------------------------------- 1 | let fac = fn (n int) int { 2 | if (n == 1) { 3 | 1 4 | } else { 5 | n * fac(n - 1) 6 | } 7 | } 8 | 9 | let main = fn () int { 10 | fac(10) 11 | } 12 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | kou 5 | 6 | 12 | 13 | 14 |

kou

15 | 16 |

Upload *.wasm

17 |

18 |

()

19 |

20 | 21 |

Result

22 |

23 | 24 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kou", 3 | "version": "0.3.0", 4 | "description": "A minimal language compiled into wasm bytecode", 5 | "main": "dist/kouc", 6 | "bin": { 7 | "kou": "bin/kou", 8 | "kouc": "bin/kouc" 9 | }, 10 | "scripts": { 11 | "build": "tsc", 12 | "prettier": "prettier --parser typescript --single-quote --trailing-comma all '{src,test}/**/*.ts'", 13 | "format": "npm run prettier -- --write", 14 | "format:dry": "npm run prettier -- -l", 15 | "test": "ts-node --no-cache --type-check test", 16 | "release": "npm run build && npm publish" 17 | }, 18 | "husky": { 19 | "hooks": { 20 | "pre-commit": "npm run format:dry", 21 | "pre-push": "npm run format:dry && npm run build && npm test" 22 | } 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/utatti/kou.git" 27 | }, 28 | "author": "Hyunjae Jun ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/utatti/kou/issues" 32 | }, 33 | "homepage": "https://github.com/utatti/kou", 34 | "devDependencies": { 35 | "husky": "^2.2.0", 36 | "prettier": "1.15.2", 37 | "ts-node": "7.0.1", 38 | "typescript": "3.1.6" 39 | }, 40 | "dependencies": { 41 | "@types/node": "10.12.6", 42 | "@types/tmp": "0.0.33", 43 | "@types/webassembly-js-api": "0.0.1", 44 | "@types/yargs": "12.0.1", 45 | "chalk": "2.4.1", 46 | "hexy": "0.2.11", 47 | "previewable-iterator": "0.1.1", 48 | "s-exify": "^0.1.0", 49 | "wabt": "^1.0.10", 50 | "yargs": "12.0.2" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /scripts/wabt.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | git clone --branch 1.0.10 --depth 1 https://github.com/WebAssembly/wabt.git 3 | cd wabt 4 | git submodule update --init 5 | make gcc-release 6 | -------------------------------------------------------------------------------- /src/codegen/context.ts: -------------------------------------------------------------------------------- 1 | import * as a from '../parser/ast'; 2 | import { StdFunc, defaultStdFuncs } from '../stdlib'; 3 | 4 | export class CodegenContext { 5 | private globalNameMap: Map = new Map(); 6 | private localNameMaps: Array> = []; 7 | private aliasMaps: Array> = [new Map()]; 8 | 9 | private scopeIDStack: Array = []; 10 | private incrScopeID: number = 0; 11 | 12 | public globalInitializers: Array<{ watName: string; expr: a.Expr }> = []; 13 | public tupleConstructors: Map< 14 | string, 15 | { types: Array; sizes: Array } 16 | > = new Map(); 17 | 18 | private stdFuncMap: Map; 19 | 20 | constructor(stdFuncs: Array = defaultStdFuncs()) { 21 | this.stdFuncMap = new Map( 22 | stdFuncs.map<[string, StdFunc]>(func => [func.name, func]), 23 | ); 24 | } 25 | 26 | private enterScope() { 27 | this.localNameMaps.unshift(new Map()); 28 | this.aliasMaps.unshift(new Map()); 29 | } 30 | 31 | private leaveScope() { 32 | this.localNameMaps.shift(); 33 | this.aliasMaps.shift(); 34 | } 35 | 36 | enterFunction() { 37 | this.enterScope(); 38 | this.resetScopeID(); 39 | } 40 | 41 | leaveFunction() { 42 | this.leaveScope(); 43 | } 44 | 45 | enterBlock() { 46 | this.enterScope(); 47 | this.incrScopeID++; 48 | this.scopeIDStack.unshift(this.incrScopeID); 49 | } 50 | 51 | leaveBlock() { 52 | this.leaveScope(); 53 | this.scopeIDStack.shift(); 54 | } 55 | 56 | resetScopeID() { 57 | this.scopeIDStack = []; 58 | this.incrScopeID = 0; 59 | } 60 | 61 | private withScopeID(name: string): string { 62 | if (this.scopeIDStack.length === 0) { 63 | return name; 64 | } else { 65 | return `${name}/${this.scopeIDStack[0]}`; 66 | } 67 | } 68 | 69 | convertLocalName(origName: string): string { 70 | return this.withScopeID(origName); 71 | } 72 | 73 | pushName(origName: string): string { 74 | const nameMap = this.localNameMaps[0] || this.globalNameMap; 75 | const watName = this.convertLocalName(origName); 76 | nameMap.set(origName, watName); 77 | return watName; 78 | } 79 | 80 | pushAlias(fromName: string, toName: string) { 81 | this.aliasMaps[0]!.set(fromName, toName); 82 | } 83 | 84 | pushInitializer(watName: string, expr: a.Expr) { 85 | // name here is a WAT name 86 | this.globalInitializers.push({ watName, expr }); 87 | } 88 | 89 | getLocalWATName(origName: string): string | null { 90 | for (const map of this.localNameMaps) { 91 | const name = map.get(origName); 92 | if (name) { 93 | return name; 94 | } 95 | } 96 | 97 | return null; 98 | } 99 | 100 | getGlobalWATName(origName: string): string | null { 101 | let aliasedName = null; 102 | for (const map of this.aliasMaps) { 103 | aliasedName = map.get(origName); 104 | if (aliasedName) { 105 | break; 106 | } 107 | } 108 | return this.globalNameMap.get(aliasedName || origName) || null; 109 | } 110 | 111 | useTupleConstructor(types: Array, sizes: Array): string { 112 | const name = `create_tuple/${types.join('/')}`; 113 | 114 | if (!this.tupleConstructors.has(name)) { 115 | this.tupleConstructors.set(name, { types, sizes }); 116 | } 117 | 118 | return name; 119 | } 120 | 121 | public getStdFunc(name: string): StdFunc | null { 122 | return this.stdFuncMap.get(name) || null; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/codegen/index.ts: -------------------------------------------------------------------------------- 1 | import * as a from '../parser/ast'; 2 | import { wat2wasm, convertMiBToPage } from '../wasm'; 3 | import { CodegenContext } from './context'; 4 | import { SExp, beautify } from 's-exify'; 5 | 6 | export function genWASM( 7 | mod: a.Module, 8 | opts: { exports: Array; memorySize: number }, 9 | ): Buffer { 10 | return wat2wasm(genWAT(mod, opts)); 11 | } 12 | 13 | export function genWAT( 14 | mod: a.Module, 15 | opts: { exports: Array; memorySize: number }, 16 | ): string { 17 | return beautify( 18 | codegenModule(mod, new CodegenContext(), { 19 | exports: opts.exports, 20 | pageCount: convertMiBToPage(opts.memorySize), 21 | }), 22 | ); 23 | } 24 | 25 | const exp = (...nodes: Array): SExp => nodes; 26 | const str = (raw: string) => `"${raw.replace('"', '\\"')}"`; 27 | const wat = (name: string) => `$${name}`; 28 | const sys = (name: string) => wat(`/${name}`); 29 | 30 | function codegenModule( 31 | mod: a.Module, 32 | ctx: CodegenContext, 33 | opts: { 34 | exports: Array; 35 | pageCount: number; 36 | }, 37 | ): SExp { 38 | const modE = exp('module'); 39 | 40 | // imports 41 | modE.push( 42 | exp( 43 | 'import', 44 | str('js'), 45 | str('memory'), 46 | exp('memory', String(opts.pageCount)), 47 | ), 48 | ); 49 | 50 | // system registries for language implementation 51 | modE.push(...codegenRegistry(ctx)); 52 | 53 | for (const decl of mod.value.decls) { 54 | // register global names first 55 | ctx.pushName(decl.value.name.value); 56 | } 57 | 58 | for (const decl of mod.value.decls) { 59 | // actual codegen for global decls 60 | const declE = codegenGlobalDecl(decl, ctx); 61 | if (declE) modE.push(declE); 62 | } 63 | 64 | modE.push(...codegenStartFunc(ctx)); 65 | 66 | // used tuple constructors 67 | for (const [name, { types, sizes }] of ctx.tupleConstructors.entries()) { 68 | modE.push(codegenTupleConstructor(name, types, sizes)); 69 | } 70 | 71 | for (const exportName of opts.exports) { 72 | const watName = ctx.getGlobalWATName(exportName)!; 73 | modE.push(exp('export', str(exportName), exp('func', wat(watName)))); 74 | } 75 | 76 | return modE; 77 | } 78 | 79 | function* codegenRegistry(ctx: CodegenContext): Iterable { 80 | const reg = (name: string, ty: string) => 81 | exp('global', sys(name), exp('mut', ty), exp(`${ty}.const`, '0')); 82 | 83 | yield reg('reg/addr', 'i32'); 84 | yield reg('reg/i32/1', 'i32'); 85 | yield reg('reg/i32/2', 'i32'); 86 | yield reg('reg/f64/1', 'f64'); 87 | yield reg('reg/f64/2', 'f64'); 88 | } 89 | 90 | function* codegenStartFunc(ctx: CodegenContext): Iterable { 91 | if (ctx.globalInitializers.length === 0) { 92 | return; 93 | } 94 | 95 | const funcE = exp('func', sys('start')); 96 | 97 | for (const { watName, expr } of ctx.globalInitializers) { 98 | funcE.push(...codegenExpr(expr, ctx)); 99 | funcE.push(exp('set_global', wat(watName))); 100 | } 101 | 102 | yield funcE; 103 | yield exp('start', sys('start')); 104 | } 105 | 106 | function codegenGlobalDecl(decl: a.Decl, ctx: CodegenContext): SExp | null { 107 | const expr = decl.value.expr; 108 | if (expr instanceof a.FuncExpr) { 109 | return codegenFunction(decl.value.name.value, decl.value.expr, ctx); 110 | } else if (expr instanceof a.IdentExpr && expr.type instanceof a.FuncType) { 111 | // function name alias 112 | ctx.pushAlias(decl.value.name.value, expr.value.value); 113 | return null; 114 | } else { 115 | return codegenGlobalVar(decl, ctx); 116 | } 117 | } 118 | 119 | function codegenFunction( 120 | origName: string, 121 | func: a.FuncExpr, 122 | ctx: CodegenContext, 123 | ): SExp { 124 | const name = ctx.pushName(origName); 125 | 126 | const funcE = exp('func', wat(name)); 127 | 128 | ctx.enterFunction(); 129 | 130 | for (const param of func.value.params.items) { 131 | funcE.push( 132 | exp( 133 | 'param', 134 | wat(ctx.pushName(param.name.value)), 135 | codegenType(param.type, ctx), 136 | ), 137 | ); 138 | } 139 | 140 | funcE.push(exp('result', codegenType(func.value.returnType, ctx))); 141 | 142 | funcE.push(...codegenBlock(func.value.body, true, ctx)); 143 | 144 | ctx.leaveFunction(); 145 | 146 | return funcE; 147 | } 148 | 149 | function codegenType(ty: a.Type, ctx: CodegenContext): string { 150 | if (ty instanceof a.IntType) { 151 | return 'i32'; 152 | } else if (ty instanceof a.FloatType) { 153 | return 'f64'; 154 | } else if (ty instanceof a.BoolType) { 155 | return 'i32'; // 0 or 1 156 | } else if (ty instanceof a.CharType) { 157 | return 'i32'; // ascii 158 | } else if (ty instanceof a.VoidType) { 159 | return ''; 160 | } else { 161 | return 'i32'; // memory offset 162 | } 163 | } 164 | 165 | function getByteSizeOfType(ty: a.Type): number { 166 | if (ty instanceof a.FloatType) { 167 | return 8; 168 | } else if (ty instanceof a.VoidType) { 169 | return 0; 170 | } else { 171 | return 4; 172 | } 173 | } 174 | 175 | function* codegenBlock( 176 | block: a.Block, 177 | isFunction: boolean, 178 | ctx: CodegenContext, 179 | ): Iterable { 180 | if (isFunction) { 181 | yield* codegenLocalVarDef(block, ctx); 182 | ctx.resetScopeID(); 183 | } else { 184 | ctx.enterBlock(); 185 | } 186 | 187 | for (const body of block.value.bodies) { 188 | if (body instanceof a.Expr) { 189 | // expr 190 | yield* codegenExpr(body, ctx); 191 | } else if (body instanceof a.Decl) { 192 | // local decl 193 | yield* codegenLocalVarDecl(body, ctx); 194 | } else if (body instanceof a.Assign) { 195 | // assignment 196 | yield* codegenAssign(body, ctx); 197 | } else { 198 | // break 199 | yield codegenBreak(body, ctx); 200 | } 201 | } 202 | 203 | if (isFunction) { 204 | yield exp('return'); 205 | } else { 206 | ctx.leaveBlock(); 207 | } 208 | } 209 | 210 | function codegenBlockType(block: a.Block, ctx: CodegenContext): string { 211 | if (block.value.returnVoid) { 212 | return ''; 213 | } else { 214 | // the last body should be an expr; 215 | const lastExpr: a.Expr = block.value.bodies[ 216 | block.value.bodies.length - 1 217 | ] as any; 218 | return codegenType(lastExpr.type!, ctx); 219 | } 220 | } 221 | 222 | function* codegenExpr(expr: a.Expr, ctx: CodegenContext): Iterable { 223 | if (expr instanceof a.LitExpr) { 224 | yield codegenLiteral(expr.value, ctx); 225 | } else if (expr instanceof a.IdentExpr) { 226 | yield codegenIdent(expr.value, ctx); 227 | } else if (expr instanceof a.CallExpr) { 228 | yield* codegenCallExpr(expr, ctx); 229 | } else if (expr instanceof a.UnaryExpr) { 230 | yield* codegenUnaryExpr(expr, ctx); 231 | } else if (expr instanceof a.BinaryExpr) { 232 | yield* codegenBinaryExpr(expr, ctx); 233 | } else if (expr instanceof a.CondExpr) { 234 | yield* codegenCondExpr(expr, ctx); 235 | } else if (expr instanceof a.TupleExpr) { 236 | yield* codegenTupleExpr(expr, ctx); 237 | } else if (expr instanceof a.ArrayExpr) { 238 | yield* codegenArrayExpr(expr, ctx); 239 | } else if (expr instanceof a.IndexExpr) { 240 | yield* codegenIndexExpr(expr, ctx); 241 | } else if (expr instanceof a.NewExpr) { 242 | yield* codegenNewExpr(expr, ctx); 243 | } else if (expr instanceof a.LoopExpr) { 244 | yield codegenLoopExpr(expr, ctx); 245 | } 246 | } 247 | 248 | function codegenLiteral(lit: a.Literal, ctx: CodegenContext): SExp { 249 | if (lit instanceof a.IntLit) { 250 | return exp('i32.const', String(lit.value)); 251 | } else if (lit instanceof a.FloatLit) { 252 | const rep = lit.value.startsWith('.') ? '0' + lit.value : lit.value; 253 | return exp('f64.const', rep); 254 | } else if (lit instanceof a.StrLit) { 255 | // TODO: string literal 256 | return exp('i32.const', '0'); 257 | } else if (lit instanceof a.CharLit) { 258 | return exp('i32.const', String(lit.parsedValue.codePointAt(0))); 259 | } else if (lit instanceof a.BoolLit) { 260 | return exp('i32.const', lit.parsedValue ? '1' : '0'); 261 | } else { 262 | return exp('unreachable'); 263 | } 264 | } 265 | 266 | function codegenInitialValForType(lit: a.Type, ctx: CodegenContext): SExp { 267 | if (lit instanceof a.IntType) { 268 | return exp('i32.const', '0'); 269 | } else if (lit instanceof a.FloatType) { 270 | return exp('f64.const', '0'); 271 | } else if (lit instanceof a.CharType) { 272 | return exp('i32.const', '0'); 273 | } else if (lit instanceof a.BoolType) { 274 | return exp('i32.const', '0'); 275 | } else { 276 | // memory address 277 | return exp('i32.const', '0'); 278 | } 279 | } 280 | 281 | function codegenIdent(ident: a.Ident, ctx: CodegenContext): SExp { 282 | let name = ctx.getLocalWATName(ident.value); 283 | if (name) { 284 | return exp('get_local', wat(name)); 285 | } else { 286 | name = ctx.getGlobalWATName(ident.value)!; 287 | return exp('get_global', wat(name)); 288 | } 289 | } 290 | 291 | function* codegenCallExpr( 292 | call: a.CallExpr, 293 | ctx: CodegenContext, 294 | ): Iterable { 295 | if (!(call.value.func instanceof a.IdentExpr)) { 296 | // do not support 297 | return; 298 | } 299 | 300 | if (call.value.args instanceof a.TupleExpr) { 301 | for (const arg of call.value.args.value.items) { 302 | yield* codegenExpr(arg, ctx); 303 | } 304 | } else { 305 | yield* codegenExpr(call.value.args, ctx); 306 | } 307 | 308 | const funcName = ctx.getGlobalWATName(call.value.func.value.value); 309 | 310 | if (typeof funcName === 'string') { 311 | yield exp('call', wat(funcName)); 312 | } else { 313 | // must be stdlib 314 | const stdFunc = ctx.getStdFunc(call.value.func.value.value)!; 315 | if (stdFunc.expr) { 316 | yield* stdFunc.expr; 317 | } 318 | } 319 | } 320 | 321 | function* codegenUnaryExpr( 322 | unary: a.UnaryExpr, 323 | ctx: CodegenContext, 324 | ): Iterable { 325 | const op = unary.value.op; 326 | const right = unary.value.right; 327 | 328 | // used for '-' 329 | let ty = codegenType(right.type!, ctx); 330 | 331 | if (op.value === '-') { 332 | yield exp(`${ty}.const`, '0'); 333 | } 334 | 335 | yield* codegenExpr(right, ctx); 336 | 337 | if (op.value === '-') { 338 | yield exp(`${ty}.sub`); 339 | } else if (op.value === '!') { 340 | yield exp('i32.eqz'); 341 | } 342 | 343 | // '+' should be removed already in desugarer, no need to handle 344 | } 345 | 346 | function* codegenBinaryExpr( 347 | binary: a.BinaryExpr, 348 | ctx: CodegenContext, 349 | ): Iterable { 350 | const op = binary.value.op; 351 | const left = binary.value.left; 352 | const right = binary.value.right; 353 | 354 | const ty = codegenType(right.type!, ctx); 355 | const signed = ty === 'i32' ? '_s' : ''; 356 | 357 | switch (op.value) { 358 | case '==': 359 | yield* codegenExpr(left, ctx); 360 | yield* codegenExpr(right, ctx); 361 | yield exp(`${ty}.eq`); 362 | break; 363 | case '!=': 364 | yield* codegenExpr(left, ctx); 365 | yield* codegenExpr(right, ctx); 366 | yield exp(`${ty}.ne`); 367 | break; 368 | case '<': 369 | yield* codegenExpr(left, ctx); 370 | yield* codegenExpr(right, ctx); 371 | yield exp(`${ty}.lt${signed}`); 372 | break; 373 | case '<=': 374 | yield* codegenExpr(left, ctx); 375 | yield* codegenExpr(right, ctx); 376 | yield exp(`${ty}.le${signed}`); 377 | break; 378 | case '>': 379 | yield* codegenExpr(left, ctx); 380 | yield* codegenExpr(right, ctx); 381 | yield exp(`${ty}.gt${signed}`); 382 | break; 383 | case '>=': 384 | yield* codegenExpr(left, ctx); 385 | yield* codegenExpr(right, ctx); 386 | yield exp(`${ty}.ge${signed}`); 387 | break; 388 | case '+': 389 | yield* codegenExpr(left, ctx); 390 | yield* codegenExpr(right, ctx); 391 | yield exp(`${ty}.add`); 392 | break; 393 | case '-': 394 | yield* codegenExpr(left, ctx); 395 | yield* codegenExpr(right, ctx); 396 | yield exp(`${ty}.sub`); 397 | break; 398 | case '^': 399 | yield* codegenExpr(left, ctx); 400 | yield* codegenExpr(right, ctx); 401 | yield exp('i32.xor'); 402 | break; 403 | case '&': 404 | yield* codegenExpr(left, ctx); 405 | yield* codegenExpr(right, ctx); 406 | yield exp('i32.and'); 407 | break; 408 | case '|': 409 | yield* codegenExpr(left, ctx); 410 | yield* codegenExpr(right, ctx); 411 | yield exp('i32.or'); 412 | break; 413 | case '*': 414 | yield* codegenExpr(left, ctx); 415 | yield* codegenExpr(right, ctx); 416 | yield exp(`${ty}.mul`); 417 | break; 418 | case '/': 419 | yield* codegenExpr(left, ctx); 420 | yield* codegenExpr(right, ctx); 421 | yield exp(`${ty}.div${signed}`); 422 | break; 423 | case '%': 424 | yield* codegenExpr(left, ctx); 425 | yield* codegenExpr(right, ctx); 426 | yield exp('i32.rem_s'); 427 | break; 428 | case '&&': 429 | // for short circuit evaluation 430 | yield* codegenExpr(left, ctx); 431 | yield exp( 432 | 'if', 433 | exp('result', 'i32'), 434 | exp('then', ...codegenExpr(right, ctx)), 435 | exp('else', exp('i32.const', '0')), 436 | ); 437 | break; 438 | case '||': 439 | // for short circuit evaluation 440 | yield* codegenExpr(left, ctx); 441 | yield exp( 442 | 'if', 443 | exp('result', 'i32'), 444 | exp('then', exp('i32.const', '1')), 445 | exp('else', ...codegenExpr(right, ctx)), 446 | ); 447 | break; 448 | } 449 | } 450 | 451 | function* codegenCondExpr( 452 | cond: a.CondExpr, 453 | ctx: CodegenContext, 454 | ): Iterable { 455 | yield* codegenExpr(cond.value.if, ctx); 456 | 457 | yield exp( 458 | 'if', 459 | exp('result', codegenBlockType(cond.value.then, ctx)), 460 | exp('then', ...codegenBlock(cond.value.then, false, ctx)), 461 | exp('else', ...codegenBlock(cond.value.else, false, ctx)), 462 | ); 463 | } 464 | 465 | function* codegenGetCurrentHeapPointer(): Iterable { 466 | yield exp('i32.const', '0'); 467 | yield exp('i32.load'); 468 | } 469 | 470 | function* codegenSetCurrentHeapPointer(): Iterable { 471 | yield exp('i32.const', '0'); 472 | yield* codegenSwapStackTop('i32'); 473 | yield exp('i32.store'); 474 | } 475 | 476 | function* codegenSwapStackTop(ty1: string, ty2: string = ty1): Iterable { 477 | yield exp('set_global', sys(`reg/${ty1}/1`)); 478 | yield exp('set_global', sys(`reg/${ty2}/2`)); 479 | yield exp('get_global', sys(`reg/${ty1}/1`)); 480 | yield exp('get_global', sys(`reg/${ty2}/2`)); 481 | } 482 | 483 | function* codegenMemoryAllocation(size?: number): Iterable { 484 | if (typeof size === 'number') { 485 | yield exp('i32.const', String(size)); 486 | } 487 | 488 | yield* codegenGetCurrentHeapPointer(); 489 | yield exp('set_global', sys('reg/addr')); 490 | yield exp('get_global', sys('reg/addr')); 491 | yield exp('i32.add'); 492 | yield* codegenSetCurrentHeapPointer(); 493 | yield exp('get_global', sys('reg/addr')); 494 | } 495 | 496 | function* codegenTupleExpr( 497 | tuple: a.TupleExpr, 498 | ctx: CodegenContext, 499 | ): Iterable { 500 | if (tuple.value.size === 0) { 501 | yield exp('i32.const', '0'); 502 | return; 503 | } 504 | 505 | for (let i = 0; i < tuple.value.size; i++) { 506 | const expr = tuple.value.items[i]; 507 | yield* codegenExpr(expr, ctx); 508 | } 509 | 510 | const tupleTy: a.TupleType = tuple.type as any; 511 | const types = tupleTy.value.items.map(ty => codegenType(ty, ctx)); 512 | const sizes = tupleTy.value.items.map(getByteSizeOfType); 513 | 514 | const constName = ctx.useTupleConstructor(types, sizes); 515 | yield exp('call', sys(constName)); 516 | } 517 | 518 | function* codegenArrayExpr( 519 | array: a.ArrayExpr, 520 | ctx: CodegenContext, 521 | ): Iterable { 522 | const arrTy = array.type! as a.ArrayType; 523 | 524 | const ty = codegenType(arrTy.value, ctx); 525 | const size = getByteSizeOfType(arrTy.value); 526 | const len = array.value.length; 527 | yield* codegenMemoryAllocation(4 + size * len); 528 | 529 | yield exp('set_global', sys('reg/i32/1')); 530 | for (let i = 0; i < len + 2; i++) { 531 | // prepare for set 532 | // +2: +1 to store length, +1 to return 533 | yield exp('get_global', sys('reg/i32/1')); 534 | } 535 | 536 | // store length 537 | let offset = 4; 538 | yield exp('i32.const', String(len)); 539 | yield exp('i32.store'); 540 | yield exp('i32.const', String(offset)); 541 | yield exp('i32.add'); 542 | 543 | // store values 544 | for (let i = 0; i < len; i++) { 545 | yield* codegenExpr(array.value[i], ctx); 546 | yield exp(`${ty}.store`); 547 | 548 | if (i < len - 1) { 549 | offset += size; 550 | yield exp('i32.const', String(offset)); 551 | yield exp('i32.add'); 552 | } 553 | } 554 | } 555 | 556 | function codegenTupleConstructor( 557 | name: string, 558 | types: Array, 559 | sizes: Array, 560 | ): SExp { 561 | const funcE = exp('func', sys(name)); 562 | 563 | for (let i = 0; i < types.length; i++) { 564 | funcE.push(exp('param', types[i])); 565 | } 566 | funcE.push(exp('result', 'i32')); 567 | 568 | const offset = wat('offset'); 569 | funcE.push(exp('local', wat('offset'), 'i32')); 570 | 571 | funcE.push(...codegenMemoryAllocation(sizes.reduce((x, y) => x + y))); 572 | funcE.push(exp('set_local', offset)); 573 | funcE.push(exp('get_local', offset)); // this becomes return value 574 | 575 | for (let i = 0; i < types.length; i++) { 576 | funcE.push(exp('get_local', offset)); 577 | funcE.push(exp('get_local', String(i))); 578 | funcE.push(exp(`${types[i]}.store`)); 579 | 580 | // calculate next offset 581 | funcE.push(exp('get_local', offset)); 582 | funcE.push(exp('i32.const', String(sizes[i]))); 583 | funcE.push(exp('i32.add')); 584 | funcE.push(exp('set_local', offset)); 585 | } 586 | 587 | return funcE; 588 | } 589 | 590 | function getTupleIdx(expr: a.IndexExpr): number { 591 | // The index of a tuple expr should be an int literal 592 | const idxLit: a.IntLit = expr.value.index.value as any; 593 | return idxLit.parsedValue; 594 | } 595 | 596 | function* codegenTupleAddr( 597 | target: a.Expr, 598 | idx: number, 599 | ctx: CodegenContext, 600 | ): Iterable { 601 | yield* codegenExpr(target, ctx); 602 | 603 | const tupleTy = target.type! as a.TupleType; 604 | 605 | let offset = 0; 606 | for (let i = 0; i < idx; i++) { 607 | offset += getByteSizeOfType(tupleTy.value.items[i]); 608 | } 609 | yield exp('i32.const', String(offset)); 610 | yield exp('i32.add'); 611 | } 612 | 613 | function* codegenArrayAddr( 614 | target: a.Expr, 615 | index: a.Expr, 616 | byteSize: number, 617 | ctx: CodegenContext, 618 | ): Iterable { 619 | yield* codegenExpr(target, ctx); 620 | yield exp('i32.const', '4'); 621 | yield exp('i32.add'); 622 | yield* codegenExpr(index, ctx); 623 | yield exp('i32.const', String(byteSize)); 624 | yield exp('i32.mul'); 625 | yield exp('i32.add'); 626 | } 627 | 628 | function* codegenIndexExpr( 629 | expr: a.IndexExpr, 630 | ctx: CodegenContext, 631 | ): Iterable { 632 | const target = expr.value.target; 633 | if (target.type instanceof a.ArrayType) { 634 | const byteSize = getByteSizeOfType(target.type.value); 635 | yield* codegenArrayAddr(target, expr.value.index, byteSize, ctx); 636 | const ty = codegenType(target.type.value, ctx); 637 | yield exp(`${ty}.load`); 638 | } else if (target.type instanceof a.TupleType) { 639 | const idx = getTupleIdx(expr); 640 | yield* codegenTupleAddr(target, idx, ctx); 641 | const ty = codegenType(target.type.value.items[idx], ctx); 642 | yield exp(`${ty}.load`); 643 | } 644 | } 645 | 646 | function* codegenNewExpr(expr: a.NewExpr, ctx: CodegenContext): Iterable { 647 | const size = getByteSizeOfType(expr.value.type); 648 | yield* codegenExpr(expr.value.length, ctx); 649 | yield exp('set_global', sys('reg/i32/1')); 650 | yield exp('get_global', sys('reg/i32/1')); 651 | yield exp('get_global', sys('reg/i32/1')); 652 | yield exp('i32.const', String(size)); 653 | yield exp('i32.mul'); 654 | yield exp('i32.const', '4'); 655 | yield exp('i32.add'); 656 | 657 | yield* codegenMemoryAllocation(); 658 | yield exp('set_global', sys('reg/addr')); 659 | yield exp('get_global', sys('reg/addr')); 660 | yield* codegenSwapStackTop('i32'); 661 | yield exp('i32.store'); 662 | 663 | yield exp('get_global', sys('reg/addr')); 664 | } 665 | 666 | function codegenLoopExpr(expr: a.LoopExpr, ctx: CodegenContext): SExp { 667 | return exp( 668 | 'block', 669 | exp( 670 | 'loop', 671 | ...codegenExpr(expr.value.while, ctx), 672 | exp( 673 | 'if', 674 | exp( 675 | 'then', 676 | ...codegenBlock(expr.value.body, false, ctx), 677 | exp('br', '1'), 678 | ), 679 | ), 680 | ), 681 | ); 682 | } 683 | 684 | function* codegenLocalVarDef( 685 | block: a.Block, 686 | ctx: CodegenContext, 687 | ): Iterable { 688 | for (const body of block.value.bodies) { 689 | if (body instanceof a.Decl) { 690 | const origName = body.value.name.value; 691 | const expr = body.value.expr; 692 | 693 | // ignore function alias 694 | if (expr instanceof a.IdentExpr && expr.type instanceof a.FuncType) { 695 | continue; 696 | } 697 | 698 | const name = ctx.convertLocalName(origName); 699 | yield exp('local', wat(name), codegenType(expr.type!, ctx)); 700 | } else if (body instanceof a.CondExpr) { 701 | ctx.enterBlock(); 702 | yield* codegenLocalVarDef(body.value.then, ctx); 703 | ctx.leaveBlock(); 704 | ctx.enterBlock(); 705 | yield* codegenLocalVarDef(body.value.else, ctx); 706 | ctx.leaveBlock(); 707 | } else if (body instanceof a.LoopExpr) { 708 | ctx.enterBlock(); 709 | yield* codegenLocalVarDef(body.value.body, ctx); 710 | ctx.leaveBlock(); 711 | } 712 | } 713 | } 714 | 715 | function* codegenLocalVarDecl( 716 | decl: a.Decl, 717 | ctx: CodegenContext, 718 | ): Iterable { 719 | const origName = decl.value.name.value; 720 | const expr = decl.value.expr; 721 | 722 | if (expr instanceof a.IdentExpr && expr.type instanceof a.FuncType) { 723 | ctx.pushAlias(origName, ctx.getGlobalWATName(expr.value.value)!); 724 | } else { 725 | yield* codegenExpr(expr, ctx); 726 | const name = ctx.pushName(origName); 727 | yield exp('set_local', wat(name)); 728 | } 729 | } 730 | 731 | function codegenGlobalVar(decl: a.Decl, ctx: CodegenContext): SExp { 732 | const name = ctx.getGlobalWATName(decl.value.name.value)!; 733 | 734 | const varE = exp('global', wat(name)); 735 | const expr = decl.value.expr; 736 | varE.push(exp('mut', codegenType(expr.type!, ctx))); 737 | if (expr instanceof a.LitExpr) { 738 | varE.push(...codegenLiteral(expr.value, ctx)); 739 | } else { 740 | varE.push(codegenInitialValForType(expr.type!, ctx)); 741 | ctx.pushInitializer(name, expr); 742 | } 743 | 744 | return varE; 745 | } 746 | 747 | function* codegenAssign(assign: a.Assign, ctx: CodegenContext): Iterable { 748 | yield* codegenExpr(assign.value.expr, ctx); 749 | 750 | const lVal = assign.value.lVal; 751 | if (lVal instanceof a.IdentExpr) { 752 | // ident 753 | const ident = lVal.value; 754 | let name = ctx.getLocalWATName(ident.value); 755 | if (name) { 756 | yield exp('set_local', wat(name)); 757 | } else { 758 | name = ctx.getGlobalWATName(ident.value)!; 759 | yield exp('set_global', wat(name)); 760 | } 761 | } else { 762 | // index expr 763 | const target = lVal.value.target; 764 | if (target.type instanceof a.ArrayType) { 765 | const byteSize = getByteSizeOfType(target.type.value); 766 | yield* codegenArrayAddr(target, lVal.value.index, byteSize, ctx); 767 | const ty = codegenType(target.type.value, ctx); 768 | yield* codegenSwapStackTop('i32', ty); 769 | yield exp(`${ty}.store`); 770 | } else if (target.type instanceof a.TupleType) { 771 | const idx = getTupleIdx(lVal); 772 | yield* codegenTupleAddr(target, idx, ctx); 773 | const ty = codegenType(target.type.value.items[idx], ctx); 774 | yield* codegenSwapStackTop('i32', ty); 775 | yield exp(`${ty}.store`); 776 | } 777 | } 778 | } 779 | 780 | function codegenBreak(break_: a.Break, ctx: CodegenContext): SExp { 781 | return exp('br', '1'); 782 | } 783 | -------------------------------------------------------------------------------- /src/desugarer/index.ts: -------------------------------------------------------------------------------- 1 | import * as a from '../parser/ast'; 2 | import { Arity1 } from '../util'; 3 | 4 | class Desugarer { 5 | constructor( 6 | private replacer: { 7 | replaceModule: Arity1; 8 | replaceImport: Arity1; 9 | replaceBlock: Arity1; 10 | replaceLiteral: Arity1>; 11 | replaceExpr: Arity1>; 12 | replaceDecl: Arity1; 13 | replaceAssign: Arity1; 14 | replaceBreak: Arity1; 15 | replaceIdent: Arity1; 16 | replaceBinaryOp: Arity1>; 17 | replaceUnaryOp: Arity1; 18 | replaceType: Arity1>; 19 | }, 20 | ) {} 21 | 22 | desugar(node: a.Module): a.Module { 23 | node.value.imports = node.value.imports.map(node => 24 | this.desugarImport(node), 25 | ); 26 | node.value.decls = node.value.decls.map(node => this.desugarDecl(node)); 27 | return this.replacer.replaceModule(node); 28 | } 29 | 30 | desugarImport(node: a.Import): a.Import { 31 | node.value.path = this.desugarLiteral(node.value.path); 32 | node.value.elems = node.value.elems.map(elem => ({ 33 | name: this.desugarIdent(elem.name), 34 | as: elem.as ? this.desugarIdent(elem.as) : elem.as, 35 | })); 36 | return this.replacer.replaceImport(node); 37 | } 38 | 39 | desugarBlock(node: a.Block): a.Block { 40 | node.value.bodies = node.value.bodies.map(body => { 41 | if (body instanceof a.Expr) { 42 | return this.desugarExpr(body); 43 | } else if (body instanceof a.Decl) { 44 | return this.desugarDecl(body); 45 | } else if (body instanceof a.Assign) { 46 | return this.desugarAssign(body); 47 | } else { 48 | return this.desugarBreak(body); 49 | } 50 | }); 51 | return this.replacer.replaceBlock(node); 52 | } 53 | 54 | desugarLiteral(node: a.Literal): a.Literal { 55 | return this.replacer.replaceLiteral(node); 56 | } 57 | 58 | desugarExpr(node: a.Expr): a.Expr { 59 | if (node instanceof a.BinaryExpr) { 60 | node.value.op = this.desugarBinaryOp(node.value.op); 61 | node.value.left = this.desugarExpr(node.value.left); 62 | node.value.right = this.desugarExpr(node.value.right); 63 | } else if (node instanceof a.UnaryExpr) { 64 | node.value.op = this.desugarUnaryOp(node.value.op); 65 | node.value.right = this.desugarExpr(node.value.right); 66 | } else if (node instanceof a.LitExpr) { 67 | node.value = this.desugarLiteral(node.value); 68 | } else if (node instanceof a.IdentExpr) { 69 | node.value = this.desugarIdent(node.value); 70 | } else if (node instanceof a.TupleExpr) { 71 | node.value.items = node.value.items.map(item => this.desugarExpr(item)); 72 | } else if (node instanceof a.ArrayExpr) { 73 | node.value = node.value.map(item => this.desugarExpr(item)); 74 | } else if (node instanceof a.CallExpr) { 75 | node.value.func = this.desugarExpr(node.value.func); 76 | node.value.args = this.desugarExpr(node.value.args); 77 | } else if (node instanceof a.IndexExpr) { 78 | node.value.target = this.desugarExpr(node.value.target); 79 | node.value.index = this.desugarExpr(node.value.index); 80 | } else if (node instanceof a.FuncExpr) { 81 | node.value.params.items = node.value.params.items.map(item => { 82 | const param = { 83 | name: this.desugarIdent(item.name), 84 | type: this.desugarType(item.type), 85 | }; 86 | return param; 87 | }); 88 | node.value.returnType = this.desugarType(node.value.returnType); 89 | node.value.body = this.desugarBlock(node.value.body); 90 | } else if (node instanceof a.CondExpr) { 91 | node.value.if = this.desugarExpr(node.value.if); 92 | node.value.then = this.desugarBlock(node.value.then); 93 | node.value.else = this.desugarBlock(node.value.else); 94 | } else if (node instanceof a.LoopExpr) { 95 | node.value.while = this.desugarExpr(node.value.while); 96 | node.value.body = this.desugarBlock(node.value.body); 97 | } else if (node instanceof a.NewExpr) { 98 | node.value.type = this.desugarType(node.value.type); 99 | node.value.length = this.desugarExpr(node.value.length); 100 | } 101 | return this.replacer.replaceExpr(node); 102 | } 103 | 104 | desugarDecl(node: a.Decl): a.Decl { 105 | node.value.name = this.desugarIdent(node.value.name); 106 | if (node.value.type) { 107 | node.value.type = this.desugarType(node.value.type); 108 | } 109 | node.value.expr = this.desugarExpr(node.value.expr); 110 | return this.replacer.replaceDecl(node); 111 | } 112 | 113 | desugarAssign(node: a.Assign): a.Assign { 114 | node.value.lVal = this.desugarExpr(node.value.lVal); 115 | node.value.expr = this.desugarExpr(node.value.expr); 116 | return this.replacer.replaceAssign(node); 117 | } 118 | 119 | desugarBreak(node: a.Break): a.Break { 120 | return this.replacer.replaceBreak(node); 121 | } 122 | 123 | desugarIdent(node: a.Ident): a.Ident { 124 | return this.replacer.replaceIdent(node); 125 | } 126 | 127 | desugarBinaryOp(node: a.BinaryOp): a.BinaryOp { 128 | return this.replacer.replaceBinaryOp(node); 129 | } 130 | 131 | desugarUnaryOp(node: a.UnaryOp): a.UnaryOp { 132 | return this.replacer.replaceUnaryOp(node); 133 | } 134 | 135 | desugarType(node: a.Type): a.Type { 136 | if (node instanceof a.FuncType) { 137 | node.value.param = this.desugarType(node.value.param); 138 | node.value.return = this.desugarType(node.value.return); 139 | } else if (node instanceof a.TupleType) { 140 | node.value.items = node.value.items.map(item => this.desugarType(item)); 141 | } else if (node instanceof a.ArrayType) { 142 | node.value = this.desugarType(node.value); 143 | } 144 | return this.replacer.replaceType(node); 145 | } 146 | } 147 | 148 | const id = (x: T) => x; 149 | 150 | export function desugarBefore(mod: a.Module): a.Module { 151 | const desugarer = new Desugarer({ 152 | replaceModule: id, 153 | replaceImport: id, 154 | replaceBlock: id, 155 | replaceLiteral: id, 156 | replaceExpr: unwrap1TupleExpr, 157 | replaceDecl: id, 158 | replaceAssign: id, 159 | replaceBreak: id, 160 | replaceIdent: id, 161 | replaceBinaryOp: id, 162 | replaceUnaryOp: id, 163 | replaceType: unwrap1TupleType, 164 | }); 165 | 166 | return desugarer.desugar(mod); 167 | } 168 | 169 | export function desugarAfter(mod: a.Module): a.Module { 170 | const desugarer = new Desugarer({ 171 | replaceModule: id, 172 | replaceImport: id, 173 | replaceBlock: id, 174 | replaceLiteral: id, 175 | replaceExpr: removeUnaryPlus, 176 | replaceDecl: id, 177 | replaceAssign: id, 178 | replaceBreak: id, 179 | replaceIdent: id, 180 | replaceBinaryOp: id, 181 | replaceUnaryOp: id, 182 | replaceType: id, 183 | }); 184 | 185 | return desugarer.desugar(mod); 186 | } 187 | 188 | function unwrap1TupleExpr(node: a.Expr): a.Expr { 189 | if (node instanceof a.TupleExpr && node.value.size === 1) { 190 | return node.value.items[0]; 191 | } 192 | return node; 193 | } 194 | 195 | function unwrap1TupleType(node: a.Type): a.Type { 196 | if (node instanceof a.TupleType && node.value.size === 1) { 197 | return node.value.items[0]; 198 | } 199 | return node; 200 | } 201 | 202 | function removeUnaryPlus(node: a.Expr): a.Expr { 203 | // Remove unary '+' operator 204 | if (node instanceof a.UnaryExpr && node.value.op.value === '+') { 205 | return node.value.right; 206 | } 207 | return node; 208 | } 209 | -------------------------------------------------------------------------------- /src/kou.ts: -------------------------------------------------------------------------------- 1 | import * as yargs from 'yargs'; 2 | import { readFileSync } from 'fs'; 3 | import { resolve } from 'path'; 4 | import { runWASM, magicNumber } from './wasm'; 5 | import { reportCompileError } from './report-error'; 6 | import { Compose } from './util'; 7 | import { tokenize } from './lexer'; 8 | import { parse } from './parser'; 9 | import { desugarBefore, desugarAfter } from './desugarer'; 10 | import { typeCheck } from './typechecker'; 11 | import { TypeContext } from './typechecker/context'; 12 | import { genWASM } from './codegen'; 13 | 14 | interface Argv { 15 | source: string; 16 | main: string; 17 | memory: number; 18 | } 19 | 20 | async function main(argv: Argv) { 21 | const sourcePath = resolve(argv.source); 22 | const buffer = readFileSync(sourcePath); 23 | 24 | let bytecode: Buffer; 25 | if (buffer.slice(0, 4).equals(magicNumber)) { 26 | // wasm bytecode 27 | bytecode = buffer; 28 | } else { 29 | // maybe kou code 30 | const compile = Compose.then(tokenize) 31 | .then(parse) 32 | .then(desugarBefore) 33 | .then(mod => typeCheck(mod, new TypeContext())) 34 | .then(desugarAfter) 35 | .then(mod => 36 | genWASM(mod, { exports: [argv.main], memorySize: argv.memory }), 37 | ).f; 38 | 39 | const input = buffer.toString(); 40 | try { 41 | bytecode = compile(input); 42 | } catch (err) { 43 | return reportCompileError(input, err); 44 | } 45 | } 46 | 47 | const result = await runWASM(bytecode, { 48 | main: argv.main, 49 | memorySize: argv.memory, 50 | }); 51 | console.log(result.value); 52 | } 53 | 54 | main(yargs 55 | .usage('$0 [args] ', 'kou Programming Language CLI') 56 | .help() 57 | .option('main', { 58 | desc: 'An exported function to run', 59 | type: 'string', 60 | default: 'main', 61 | }) 62 | .option('memory', { 63 | alias: 'm', 64 | desc: 'Memory size in MiB', 65 | type: 'number', 66 | default: 128, 67 | }).argv as any).catch(err => { 68 | console.error(err); 69 | process.exit(1); 70 | }); 71 | -------------------------------------------------------------------------------- /src/kouc.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import * as yargs from 'yargs'; 3 | import { readFileSync, writeFileSync } from 'fs'; 4 | import { resolve } from 'path'; 5 | 6 | import { tokenize } from './lexer'; 7 | import { parse } from './parser'; 8 | import { desugarBefore, desugarAfter } from './desugarer'; 9 | import { typeCheck } from './typechecker'; 10 | import { TypeContext } from './typechecker/context'; 11 | import { Compose } from './util'; 12 | import { genWAT, genWASM } from './codegen'; 13 | import { exitWithErrors, reportCompileError } from './report-error'; 14 | 15 | interface Argv { 16 | wat: boolean; 17 | out: string | null; 18 | export: string; 19 | source: string; 20 | memory: number; 21 | } 22 | 23 | function main(argv: Argv) { 24 | const exports = argv.export.split(',').map(e => e.trim()); 25 | const compile = Compose.then(tokenize) 26 | .then(parse) 27 | .then(desugarBefore) 28 | .then(mod => typeCheck(mod, new TypeContext())) 29 | .then(desugarAfter) 30 | .then(mod => 31 | (argv.wat ? genWAT : genWASM)(mod, { exports, memorySize: argv.memory }), 32 | ).f; 33 | 34 | const sourcePath = resolve(argv.source); 35 | 36 | let input: string; 37 | try { 38 | input = readFileSync(sourcePath, 'utf-8'); 39 | } catch (err) { 40 | return exitWithErrors([ 41 | `Cannot open input file: ${sourcePath}`, 42 | '', 43 | chalk.red(err.message), 44 | ]); 45 | } 46 | 47 | let output: string | Buffer; 48 | try { 49 | output = compile(input); 50 | } catch (err) { 51 | return reportCompileError(input, err); 52 | } 53 | 54 | if (argv.out) { 55 | writeFileSync(argv.out, output); 56 | console.log(chalk.green('Build succeeded!')); 57 | } else { 58 | console.log(); 59 | console.log( 60 | Buffer.isBuffer(output) ? require('hexy').hexy(output) : output, 61 | ); 62 | } 63 | } 64 | 65 | main(yargs 66 | .usage('$0 [args] ', 'kou Programming Language Compiler') 67 | .help() 68 | .option('wat', { 69 | desc: 'Output wat insteadof wasm', 70 | type: 'boolean', 71 | default: false, 72 | }) 73 | .option('out', { 74 | alias: 'o', 75 | desc: 'Output file name, stdout if null', 76 | type: 'string', 77 | default: null, 78 | }) 79 | .option('export', { 80 | alias: 'e', 81 | desc: 'Comma-separated list of export names', 82 | type: 'string', 83 | default: 'main', 84 | }) 85 | .option('memory', { 86 | alias: 'm', 87 | desc: 'Memory size in MiB', 88 | type: 'number', 89 | default: 128, 90 | }).argv as any); 91 | -------------------------------------------------------------------------------- /src/lexer/error.ts: -------------------------------------------------------------------------------- 1 | export class LexError extends Error { 2 | name: string = 'LexError'; 3 | 4 | constructor( 5 | public row: number, 6 | public column: number, 7 | public unexpected: string, 8 | ) { 9 | super(`Unexpected ${unexpected} at ${row}:${column}`); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/lexer/index.ts: -------------------------------------------------------------------------------- 1 | import { PreviewableIterable } from 'previewable-iterator'; 2 | import { 3 | Token, 4 | TokenConstructor, 5 | Punctuation, 6 | Operator, 7 | Keyword, 8 | IntLit, 9 | FloatLit, 10 | BoolLit, 11 | CharLit, 12 | StrLit, 13 | Ident, 14 | EOF, 15 | RepType, 16 | } from './token'; 17 | import { LexError } from './error'; 18 | import { match, isDigit, isAlphabet, isAlphanumeric } from '../util'; 19 | 20 | class LexerInput extends PreviewableIterable { 21 | public row: number = 1; 22 | public column: number = 1; 23 | private nextRow: number = 1; 24 | private nextColumn: number = 1; 25 | 26 | constructor(iterable: Iterable) { 27 | super(iterable[Symbol.iterator]()); 28 | } 29 | 30 | next(): IteratorResult { 31 | const { done, value } = super.next(); 32 | this.row = this.nextRow; 33 | this.column = this.nextColumn; 34 | if (value === '\n') { 35 | this.nextRow += 1; 36 | this.nextColumn = 1; 37 | } else { 38 | this.nextColumn += 1; 39 | } 40 | return { done, value }; 41 | } 42 | } 43 | 44 | export function* tokenize(raw: Iterable): Iterable> { 45 | // make input previewable 46 | const input = new LexerInput(raw); 47 | 48 | skipSpaces(input); 49 | while (!input.preview().done) { 50 | yield parseToken(input); 51 | skipSpaces(input); 52 | } 53 | 54 | return new EOF(input.row, input.column, null); 55 | } 56 | 57 | function skipSpaces(input: LexerInput) { 58 | while (/^\s$/.test(input.preview().value)) { 59 | input.next(); 60 | } 61 | } 62 | 63 | function parseToken(input: LexerInput): Token { 64 | const { done, value } = input.next(); 65 | if (done) { 66 | throw new LexError(input.row, input.column, 'end of input'); 67 | } 68 | 69 | let pos: [number, number] | null = null; 70 | 71 | function savePos() { 72 | pos = [input.row, input.column]; 73 | } 74 | 75 | function token>( 76 | Con: TokenConstructor, Tk>, 77 | rep: RepType, 78 | ): Tk { 79 | if (pos) { 80 | const t = new Con(pos[0], pos[1], rep); 81 | pos = null; 82 | return t; 83 | } else { 84 | return new Con(input.row, input.column, rep); 85 | } 86 | } 87 | 88 | function withPreview, Tk2 extends Token>( 89 | rep: any, 90 | A: TokenConstructor, 91 | B: TokenConstructor = A as any, 92 | ): Tk1 | Tk2 { 93 | savePos(); 94 | if (input.preview().value === rep[1]) { 95 | input.next(); 96 | return token(A, rep); 97 | } else { 98 | return token(B, rep[0]); 99 | } 100 | } 101 | 102 | function digits(): string { 103 | let lit = ''; 104 | while (isDigit(input.preview().value)) { 105 | lit += input.next().value; 106 | } 107 | return lit; 108 | } 109 | 110 | function char(): string { 111 | let { done, value: lit } = input.next(); 112 | 113 | if (done) { 114 | throw new LexError(input.row, input.column, 'end of input'); 115 | } 116 | if (lit === '\n') { 117 | throw new LexError(input.row, input.column, 'newline'); 118 | } 119 | 120 | if ( 121 | lit === '\\' && 122 | ['n', 'r', 't', '\\', "'", '"'].includes(input.preview().value) 123 | ) { 124 | lit += input.next().value; 125 | } 126 | return lit; 127 | } 128 | 129 | return match( 130 | value, 131 | [ 132 | // single char punctuation 133 | [',', () => token(Punctuation, ',')], 134 | ['(', () => token(Punctuation, '(')], 135 | [')', () => token(Punctuation, ')')], 136 | ['[', () => token(Punctuation, '[')], 137 | [']', () => token(Punctuation, ']')], 138 | ['{', () => token(Punctuation, '{')], 139 | ['}', () => token(Punctuation, '}')], 140 | [':', () => token(Punctuation, ':')], 141 | [';', () => token(Punctuation, ';')], 142 | 143 | // single char operator 144 | ['+', () => token(Operator, '+')], 145 | ['^', () => token(Operator, '^')], 146 | ['*', () => token(Operator, '*')], 147 | ['/', () => token(Operator, '/')], 148 | ['%', () => token(Operator, '%')], 149 | 150 | // punctuation or operator 151 | ['-', () => withPreview('->', Punctuation, Operator)], 152 | ['=', () => withPreview('==', Operator, Punctuation)], 153 | ['!', () => withPreview('!=', Operator)], 154 | ['<', () => withPreview('<=', Operator)], 155 | ['>', () => withPreview('>=', Operator)], 156 | ['|', () => withPreview('||', Operator)], 157 | ['&', () => withPreview('&&', Operator)], 158 | 159 | // number literal 160 | [ 161 | isDigit, 162 | () => { 163 | savePos(); 164 | let lit = value + digits(); 165 | if (input.preview().value === '.') { 166 | lit += input.next().value + digits(); 167 | return token(FloatLit, lit); 168 | } else { 169 | return token(IntLit, lit); 170 | } 171 | }, 172 | ], 173 | [ 174 | '.', 175 | () => { 176 | savePos(); 177 | return token(FloatLit, '.' + digits()); 178 | }, 179 | ], 180 | 181 | // keyword, bool literal, or identifier 182 | [ 183 | c => c === '_' || isAlphabet(c), 184 | () => { 185 | savePos(); 186 | let rep = value; 187 | while ( 188 | input.preview().value === '_' || 189 | isAlphanumeric(input.preview().value) 190 | ) { 191 | rep += input.next().value; 192 | } 193 | switch (rep) { 194 | case 'import': 195 | case 'as': 196 | case 'let': 197 | case 'fn': 198 | case 'if': 199 | case 'else': 200 | case 'for': 201 | case 'in': 202 | case 'new': 203 | case 'while': 204 | case 'break': 205 | return token(Keyword, rep); 206 | case 'true': 207 | case 'false': 208 | return token(BoolLit, rep); 209 | default: 210 | return token(Ident, rep); 211 | } 212 | }, 213 | ], 214 | 215 | // char literal 216 | [ 217 | "'", 218 | () => { 219 | savePos(); 220 | let rep = value; 221 | rep += char(); 222 | const closing = input.next().value; 223 | if (closing !== "'") { 224 | throw new LexError(input.row, input.column, closing); 225 | } 226 | rep += closing; 227 | return token(CharLit, rep); 228 | }, 229 | ], 230 | 231 | // string literal 232 | [ 233 | '"', 234 | () => { 235 | savePos(); 236 | let rep = value; 237 | while (true) { 238 | const c = char(); 239 | rep += c; 240 | if (c === '"') { 241 | break; 242 | } 243 | } 244 | return token(StrLit, rep); 245 | }, 246 | ], 247 | ], 248 | () => { 249 | throw new LexError(input.row, input.column, value); 250 | }, 251 | ); 252 | } 253 | -------------------------------------------------------------------------------- /src/lexer/token.ts: -------------------------------------------------------------------------------- 1 | export class Token { 2 | constructor(public row: number, public column: number, public rep: T) {} 3 | 4 | is>(Con: TokenConstructor, rep?: T): this is Tk { 5 | return ( 6 | this instanceof Con && (typeof rep === 'undefined' || this.rep === rep) 7 | ); 8 | } 9 | } 10 | 11 | export type RepType> = Tk extends Token 12 | ? T 13 | : never; 14 | 15 | export interface TokenConstructor> { 16 | new (row: number, column: number, rep: T): Tk; 17 | } 18 | 19 | export class Punctuation extends Token< 20 | '->' | ',' | '(' | ')' | '[' | ']' | '{' | '}' | ':' | '=' | ';' 21 | > {} 22 | 23 | export class Operator extends Token< 24 | | '+' 25 | | '-' 26 | | '!' 27 | | '==' 28 | | '!=' 29 | | '<' 30 | | '<=' 31 | | '>' 32 | | '>=' 33 | | '|' 34 | | '^' 35 | | '*' 36 | | '/' 37 | | '%' 38 | | '&' 39 | | '||' 40 | | '&&' 41 | > {} 42 | 43 | export class Keyword extends Token< 44 | | 'import' 45 | | 'as' 46 | | 'let' 47 | | 'fn' 48 | | 'if' 49 | | 'else' 50 | | 'for' 51 | | 'in' 52 | | 'new' 53 | | 'while' 54 | | 'break' 55 | > {} 56 | 57 | export abstract class Literal extends Token {} 58 | 59 | export class IntLit extends Literal {} 60 | 61 | export class FloatLit extends Literal {} 62 | 63 | export class CharLit extends Literal {} 64 | 65 | export class StrLit extends Literal {} 66 | 67 | export class BoolLit extends Literal<'true' | 'false'> {} 68 | 69 | export class Ident extends Token {} 70 | 71 | export class EOF extends Token {} 72 | -------------------------------------------------------------------------------- /src/parser/ast.ts: -------------------------------------------------------------------------------- 1 | import * as t from '../lexer/token'; 2 | import { unescape } from '../util'; 3 | 4 | export abstract class Node { 5 | constructor(public value: T, public row: number, public column: number) {} 6 | 7 | get name() { 8 | return this.constructor.name; 9 | } 10 | } 11 | 12 | export type ValType> = N extends Node ? V : never; 13 | 14 | export interface NodeConstructor> { 15 | new (value: T, row: number, column: number): N; 16 | } 17 | 18 | export abstract class Literal extends Node { 19 | parsedValue: T; 20 | abstract parse(rep: string): T; 21 | 22 | constructor(value: string, row: number, column: number) { 23 | super(value, row, column); 24 | this.parsedValue = this.parse(value); 25 | } 26 | } 27 | 28 | export class IntLit extends Literal { 29 | parse(rep: string): number { 30 | return parseInt(rep, 10); 31 | } 32 | 33 | static from(token: t.IntLit): IntLit { 34 | return new IntLit(token.rep, token.row, token.column); 35 | } 36 | } 37 | 38 | export class FloatLit extends Literal { 39 | parse(rep: string): number { 40 | return parseFloat(rep); 41 | } 42 | 43 | static from(token: t.FloatLit): FloatLit { 44 | return new FloatLit(token.rep, token.row, token.column); 45 | } 46 | } 47 | 48 | export class CharLit extends Literal { 49 | parse(rep: string): string { 50 | return unescape(rep.slice(1, -1)); 51 | } 52 | 53 | static from(token: t.CharLit): CharLit { 54 | return new CharLit(token.rep, token.row, token.column); 55 | } 56 | } 57 | 58 | export class StrLit extends Literal { 59 | parse(rep: string): string { 60 | return unescape(rep.slice(1, -1)); 61 | } 62 | 63 | static from(token: t.StrLit): StrLit { 64 | return new StrLit(token.rep, token.row, token.column); 65 | } 66 | } 67 | 68 | export class BoolLit extends Literal { 69 | parse(rep: string): boolean { 70 | return rep === 'true'; 71 | } 72 | 73 | static from(token: t.BoolLit): BoolLit { 74 | return new BoolLit(token.rep, token.row, token.column); 75 | } 76 | } 77 | 78 | export class Ident extends Node {} 79 | 80 | export abstract class Operator extends Node {} 81 | 82 | export type UnaryOperandType = { right: Type; ret: Type }; 83 | 84 | export class UnaryOp extends Operator<'+' | '-' | '!'> { 85 | static isUnaryOp(token: t.Token) { 86 | return token.is(t.Operator) && ['+', '-', '!'].includes(token.rep); 87 | } 88 | 89 | getOperandTypes(): Array { 90 | // helper for op with same operand/return types 91 | const res = (ty: Type) => ({ right: ty, ret: ty }); 92 | 93 | switch (this.value) { 94 | case '+': 95 | return [res(IntType.instance), res(FloatType.instance)]; 96 | case '-': 97 | return [res(IntType.instance), res(FloatType.instance)]; 98 | case '!': 99 | return [res(BoolType.instance)]; 100 | } 101 | } 102 | } 103 | 104 | export type BinaryOperandType = { 105 | left: Type; 106 | right: Type; 107 | ret: Type; 108 | }; 109 | 110 | export abstract class BinaryOp extends Operator { 111 | abstract precedence: number; 112 | 113 | static isBinaryOp(token: t.Token) { 114 | return ( 115 | token.is(t.Operator) && 116 | [ 117 | '+', 118 | '-', 119 | '==', 120 | '!=', 121 | '<', 122 | '<=', 123 | '>', 124 | '>=', 125 | '|', 126 | '^', 127 | '*', 128 | '/', 129 | '%', 130 | '&', 131 | '||', 132 | '&&', 133 | ].includes(token.rep) 134 | ); 135 | } 136 | 137 | abstract getOperandTypes(left: Type): Array; 138 | } 139 | 140 | // helper for op with same operand/return types 141 | const binaryOperand = (ty: Type, ret: Type = ty) => ({ 142 | left: ty, 143 | right: ty, 144 | ret: ret, 145 | }); 146 | 147 | export class EqOp extends BinaryOp<'==' | '!='> { 148 | precedence = 0; 149 | 150 | getOperandTypes(left: Type): Array { 151 | return [binaryOperand(left, BoolType.instance)]; 152 | } 153 | } 154 | 155 | export class CompOp extends BinaryOp<'<' | '<=' | '>' | '>='> { 156 | precedence = 0; 157 | 158 | getOperandTypes(left: Type): Array { 159 | return [ 160 | binaryOperand(IntType.instance, BoolType.instance), 161 | binaryOperand(FloatType.instance, BoolType.instance), 162 | binaryOperand(BoolType.instance, BoolType.instance), 163 | binaryOperand(CharType.instance, BoolType.instance), 164 | binaryOperand(StrType.instance, BoolType.instance), 165 | ]; 166 | } 167 | } 168 | 169 | export class AddOp extends BinaryOp<'+' | '-' | '|' | '^'> { 170 | precedence = 1; 171 | 172 | getOperandTypes(left: Type): Array { 173 | switch (this.value) { 174 | case '+': 175 | case '-': 176 | return [ 177 | binaryOperand(IntType.instance), 178 | binaryOperand(FloatType.instance), 179 | ]; 180 | case '|': 181 | case '^': 182 | return [binaryOperand(IntType.instance)]; 183 | } 184 | } 185 | } 186 | 187 | export class MulOp extends BinaryOp<'*' | '/' | '%' | '&'> { 188 | precedence = 2; 189 | 190 | getOperandTypes(left: Type): Array { 191 | switch (this.value) { 192 | case '*': 193 | case '/': 194 | return [ 195 | binaryOperand(IntType.instance), 196 | binaryOperand(FloatType.instance), 197 | ]; 198 | case '%': 199 | case '&': 200 | return [binaryOperand(IntType.instance)]; 201 | } 202 | } 203 | } 204 | 205 | export class BoolOp extends BinaryOp<'||' | '&&'> { 206 | precedence = 3; 207 | 208 | getOperandTypes(left: Type): Array { 209 | return [binaryOperand(BoolType.instance)]; 210 | } 211 | } 212 | 213 | export class Module extends Node<{ 214 | imports: Array; 215 | decls: Array; 216 | }> {} 217 | 218 | export class Import extends Node<{ path: StrLit; elems: Array }> {} 219 | 220 | export type ImportElem = { name: Ident; as: Ident | null }; 221 | 222 | export class Decl extends Node<{ 223 | name: Ident; 224 | type: Type | null; 225 | expr: Expr; 226 | }> {} 227 | 228 | export class Assign extends Node<{ 229 | lVal: LVal; 230 | expr: Expr; 231 | }> {} 232 | 233 | export type LVal = IdentExpr | IndexExpr; 234 | 235 | export class Break extends Node {} 236 | 237 | export class Block extends Node<{ 238 | bodies: Array | Decl | Assign | Break>; 239 | returnVoid: boolean; 240 | }> {} 241 | 242 | export abstract class Expr extends Node { 243 | type: Type | null = null; // 'type' is set in typechecker 244 | } 245 | 246 | export class BinaryExpr extends Expr<{ 247 | left: Expr; 248 | op: BinaryOp; 249 | right: Expr; 250 | }> {} 251 | 252 | export abstract class PrimUnaryExpr extends Expr {} 253 | 254 | export class UnaryExpr extends PrimUnaryExpr<{ 255 | op: UnaryOp; 256 | right: PrimUnaryExpr; 257 | }> {} 258 | 259 | export abstract class PrimExpr extends PrimUnaryExpr {} 260 | 261 | export class LitExpr extends PrimExpr> {} 262 | 263 | export class IdentExpr extends PrimExpr {} 264 | 265 | export type Tuple = { size: number; items: Array }; 266 | 267 | export class TupleExpr extends PrimExpr>> {} 268 | 269 | export class ArrayExpr extends PrimExpr>> {} 270 | 271 | export class CallExpr extends PrimExpr<{ 272 | func: Expr; // Syntactically PrimExpr, but Expr for desugar 273 | args: Expr; // Syntactically TupleExpr, but Expr for desugar 274 | }> {} 275 | 276 | export class IndexExpr extends PrimExpr<{ 277 | target: Expr; // Syntactically PrimExpr, but Expr for desugar 278 | index: Expr; 279 | }> {} 280 | 281 | export class FuncExpr extends PrimExpr<{ 282 | params: Tuple; 283 | returnType: Type; 284 | body: Block; 285 | }> {} 286 | 287 | export type Param = { name: Ident; type: Type }; 288 | 289 | export class CondExpr extends PrimExpr<{ 290 | if: Expr; 291 | then: Block; 292 | else: Block; 293 | }> {} 294 | 295 | export class LoopExpr extends PrimExpr<{ 296 | while: Expr; 297 | body: Block; 298 | }> {} 299 | 300 | export class NewExpr extends PrimExpr<{ 301 | type: Type; 302 | length: Expr; 303 | }> {} 304 | 305 | export abstract class Type extends Node { 306 | constructor(value: T, row: number, column: number) { 307 | super(value, row, column); 308 | } 309 | } 310 | 311 | // type without nested type 312 | export abstract class SimpleType extends Type { 313 | constructor(row: number, column: number); 314 | constructor(value: null, row: number, column: number); 315 | 316 | constructor() { 317 | if (arguments.length === 3) { 318 | super(arguments[0], arguments[1], arguments[2]); 319 | } else { 320 | super(null, arguments[0], arguments[1]); 321 | } 322 | } 323 | } 324 | 325 | export abstract class PrimType extends SimpleType {} 326 | 327 | export class IntType extends PrimType { 328 | get name() { 329 | return 'int'; 330 | } 331 | 332 | static instance: IntType = new IntType(-1, -1); 333 | } 334 | 335 | export class FloatType extends PrimType { 336 | get name() { 337 | return 'float'; 338 | } 339 | 340 | static instance: FloatType = new FloatType(-1, -1); 341 | } 342 | 343 | export class StrType extends PrimType { 344 | get name() { 345 | return 'str'; 346 | } 347 | 348 | static instance: StrType = new StrType(-1, -1); 349 | } 350 | 351 | export class BoolType extends PrimType { 352 | get name() { 353 | return 'bool'; 354 | } 355 | 356 | static instance: BoolType = new BoolType(-1, -1); 357 | } 358 | 359 | export class CharType extends PrimType { 360 | get name() { 361 | return 'char'; 362 | } 363 | 364 | static instance: CharType = new CharType(-1, -1); 365 | } 366 | 367 | export class FuncType extends Type<{ param: Type; return: Type }> { 368 | get name() { 369 | let paramName = this.value.param.name; 370 | if (this.value.param instanceof FuncType) { 371 | paramName = `(${paramName})`; 372 | } 373 | return `${paramName} -> ${this.value.return.name}`; 374 | } 375 | } 376 | 377 | export class TupleType extends Type>> { 378 | get name() { 379 | return `(${this.value.items.map(item => item.name).join(', ')})`; 380 | } 381 | } 382 | 383 | export class ArrayType extends Type> { 384 | get name() { 385 | return `[${this.value.name}]`; 386 | } 387 | } 388 | 389 | export class VoidType extends SimpleType { 390 | get name() { 391 | return 'void'; 392 | } 393 | 394 | static instance: VoidType = new VoidType(-1, -1); 395 | } 396 | 397 | // AnyType should be used only when it's really needed, e.g. empty array 398 | export class AnyType extends SimpleType { 399 | static instance: AnyType = new AnyType(-1, -1); 400 | } 401 | -------------------------------------------------------------------------------- /src/parser/error.ts: -------------------------------------------------------------------------------- 1 | export class ParseError extends Error { 2 | name: string = 'ParseError'; 3 | 4 | constructor( 5 | public row: number, 6 | public column: number, 7 | public unexpected: { name: string; rep?: string }, 8 | public expected?: { name: string; rep?: string }, 9 | ) { 10 | super(); 11 | 12 | const str = ({ name, rep }: { name: string; rep?: string }) => 13 | name + (rep ? ` ${rep}` : ''); 14 | 15 | let message = `Unexpected ${str(unexpected)} at ${row}:${column}`; 16 | if (expected) { 17 | message += `, expected ${str(expected)}`; 18 | } 19 | this.message = message; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/parser/index.ts: -------------------------------------------------------------------------------- 1 | import { previewable, PreviewableIterable } from 'previewable-iterator'; 2 | import { match } from '../util'; 3 | import * as t from '../lexer/token'; 4 | import { 5 | Node, 6 | NodeConstructor, 7 | Literal, 8 | IntLit, 9 | FloatLit, 10 | StrLit, 11 | BoolLit, 12 | CharLit, 13 | Module, 14 | Import, 15 | Ident, 16 | Decl, 17 | ImportElem, 18 | Type, 19 | SimpleType, 20 | IntType, 21 | FloatType, 22 | StrType, 23 | BoolType, 24 | CharType, 25 | VoidType, 26 | Expr, 27 | LitExpr, 28 | ArrayType, 29 | TupleType, 30 | FuncType, 31 | PrimUnaryExpr, 32 | UnaryOp, 33 | PrimExpr, 34 | UnaryExpr, 35 | IdentExpr, 36 | TupleExpr, 37 | ArrayExpr, 38 | FuncExpr, 39 | Param, 40 | Block, 41 | BinaryOp, 42 | BinaryExpr, 43 | EqOp, 44 | CompOp, 45 | AddOp, 46 | MulOp, 47 | BoolOp, 48 | CallExpr, 49 | IndexExpr, 50 | CondExpr, 51 | LoopExpr, 52 | Assign, 53 | LVal, 54 | NewExpr, 55 | Break, 56 | } from './ast'; 57 | import { ParseError } from './error'; 58 | 59 | type ParserInput = PreviewableIterable>; 60 | type Parser = (input: ParserInput) => T; 61 | 62 | export function parse(tokens: Iterable>): Module { 63 | const input = previewable(tokens); 64 | return parseModule(input); 65 | } 66 | 67 | function nextToken(input: ParserInput, consume: boolean = false): t.Token { 68 | if (!input.preview().value) { 69 | throw new ParseError(-1, -1, { name: 'end of token stream' }); 70 | } 71 | return consume ? input.next().value : input.preview().value; 72 | } 73 | 74 | function parseNode>( 75 | Cons: NodeConstructor, 76 | parser: Parser, 77 | ): Parser { 78 | return (input: ParserInput) => { 79 | const { row, column } = nextToken(input); 80 | return new Cons(parser(input), row, column); 81 | }; 82 | } 83 | 84 | function consume>( 85 | input: ParserInput, 86 | TokenCon: t.TokenConstructor, Tk>, 87 | rep?: t.RepType, 88 | ): Tk { 89 | const token = nextToken(input, true); 90 | 91 | if (token.is(TokenCon, rep)) { 92 | return token; 93 | } 94 | 95 | throw new ParseError( 96 | token.row, 97 | token.column, 98 | { name: token.constructor.name, rep: token.rep }, 99 | { name: TokenCon.name, rep: rep as any }, 100 | ); 101 | } 102 | 103 | function manyWhile( 104 | input: ParserInput, 105 | parser: Parser, 106 | whilst: (token: t.Token) => boolean, 107 | ): Array { 108 | const results: Array = []; 109 | while (whilst(nextToken(input))) { 110 | results.push(parser(input)); 111 | } 112 | return results; 113 | } 114 | 115 | function commaSeparated(input: ParserInput, parser: Parser): Array { 116 | return [parser(input)].concat( 117 | manyWhile( 118 | input, 119 | input => consume(input, t.Punctuation, ',') && parser(input), 120 | token => token.is(t.Punctuation, ','), 121 | ), 122 | ); 123 | } 124 | 125 | const parseModule: Parser = parseNode(Module, input => { 126 | const imports = manyWhile(input, parseImport, token => 127 | token.is(t.Keyword, 'import'), 128 | ); 129 | 130 | const decls = manyWhile(input, parseDecl, token => 131 | token.is(t.Keyword, 'let'), 132 | ); 133 | 134 | // should be the end 135 | consume(input, t.EOF); 136 | 137 | return { imports, decls }; 138 | }); 139 | 140 | const parseImport: Parser = parseNode(Import, input => { 141 | consume(input, t.Keyword, 'import'); 142 | 143 | const path = StrLit.from(consume(input, t.StrLit) as t.StrLit); 144 | 145 | consume(input, t.Punctuation, '('); 146 | 147 | const elems: Array = commaSeparated(input, input => { 148 | const name = parseIdent(input); 149 | let as_: Ident | null = null; 150 | if (nextToken(input).is(t.Keyword, 'as')) { 151 | consume(input, t.Keyword, 'as'); 152 | as_ = parseIdent(input); 153 | } 154 | return { name, as: as_ }; 155 | }); 156 | 157 | consume(input, t.Punctuation, ')'); 158 | 159 | return { path, elems }; 160 | }); 161 | 162 | const parseIdent: Parser = parseNode( 163 | Ident, 164 | input => consume(input, t.Ident).rep, 165 | ); 166 | 167 | const parseDecl: Parser = parseNode(Decl, input => { 168 | consume(input, t.Keyword, 'let'); 169 | 170 | const name = parseIdent(input); 171 | 172 | let type_: Type | null = null; 173 | if (nextToken(input).is(t.Punctuation, ':')) { 174 | consume(input, t.Punctuation, ':'); 175 | type_ = parseType(input); 176 | } 177 | 178 | consume(input, t.Punctuation, '='); 179 | 180 | const expr = parseExpr(input); 181 | 182 | return { name, type: type_, expr }; 183 | }); 184 | 185 | function parseType(input: ParserInput): Type { 186 | let type_: Type; 187 | 188 | const token = nextToken(input); 189 | if (token.is(t.Punctuation, '[')) { 190 | type_ = parseArrayType(input); 191 | } else if (token.is(t.Punctuation, '(')) { 192 | type_ = parseTupleType(input); 193 | } else if (token.is(t.Ident)) { 194 | type_ = parseSimpleType(input); 195 | } else { 196 | throw new ParseError( 197 | token.row, 198 | token.column, 199 | { 200 | name: token.constructor.name, 201 | rep: token.rep, 202 | }, 203 | { 204 | name: 'Type', 205 | }, 206 | ); 207 | } 208 | 209 | if (nextToken(input).is(t.Punctuation, '->')) { 210 | return parseFuncType(input, type_); 211 | } else { 212 | return type_; 213 | } 214 | } 215 | 216 | const parseArrayType: Parser = parseNode(ArrayType, input => { 217 | consume(input, t.Punctuation, '['); 218 | const elementType = parseType(input); 219 | consume(input, t.Punctuation, ']'); 220 | return elementType; 221 | }); 222 | 223 | const parseTupleType: Parser = parseNode(TupleType, input => { 224 | consume(input, t.Punctuation, '('); 225 | 226 | let items: Array> = []; 227 | if (!nextToken(input).is(t.Punctuation, ')')) { 228 | items = commaSeparated(input, parseType); 229 | } 230 | 231 | consume(input, t.Punctuation, ')'); 232 | 233 | return { size: items.length, items }; 234 | }); 235 | 236 | function parseFuncType(input: ParserInput, lho: Type): FuncType { 237 | consume(input, t.Punctuation, '->'); 238 | return new FuncType( 239 | { 240 | param: lho, 241 | return: parseType(input), 242 | }, 243 | lho.row, 244 | lho.column, 245 | ); 246 | } 247 | 248 | function parseSimpleType(input: ParserInput): SimpleType { 249 | const ident = consume(input, t.Ident); 250 | return match( 251 | ident.rep, 252 | [ 253 | ['int', () => new IntType(null, ident.row, ident.column)], 254 | ['float', () => new FloatType(null, ident.row, ident.column)], 255 | ['str', () => new StrType(null, ident.row, ident.column)], 256 | ['bool', () => new BoolType(null, ident.row, ident.column)], 257 | ['char', () => new CharType(null, ident.row, ident.column)], 258 | ['void', () => new VoidType(null, ident.row, ident.column)], 259 | ], 260 | () => { 261 | throw new ParseError(ident.row, ident.column, { 262 | name: 'unknown type', 263 | rep: ident.rep, 264 | }); 265 | }, 266 | ); 267 | } 268 | 269 | function parseLiteral(input: ParserInput): Literal { 270 | const token = nextToken(input, true); 271 | return match, Literal>( 272 | token, 273 | [ 274 | [token => token.is(t.IntLit), () => IntLit.from(token)], 275 | [token => token.is(t.FloatLit), () => FloatLit.from(token)], 276 | [token => token.is(t.StrLit), () => StrLit.from(token)], 277 | [token => token.is(t.BoolLit), () => BoolLit.from(token)], 278 | [token => token.is(t.CharLit), () => CharLit.from(token)], 279 | ], 280 | () => { 281 | throw new ParseError( 282 | token.row, 283 | token.column, 284 | { 285 | name: token.constructor.name, 286 | rep: token.rep, 287 | }, 288 | { 289 | name: 'Literal', 290 | }, 291 | ); 292 | }, 293 | ); 294 | } 295 | 296 | function parseExpr(input: ParserInput, precedence: number = -1): Expr { 297 | let left = parsePrimUnaryExpr(input); 298 | 299 | while ( 300 | BinaryOp.isBinaryOp(nextToken(input)) && 301 | tokenToBinaryOp(nextToken(input)).precedence > precedence 302 | ) { 303 | left = parseBinaryExpr(input, left, precedence); 304 | } 305 | 306 | return left; 307 | } 308 | 309 | function parsePrimUnaryExpr(input: ParserInput): PrimUnaryExpr { 310 | const next = nextToken(input); 311 | if (UnaryOp.isUnaryOp(next)) { 312 | return parseUnaryExpr(input); 313 | } else { 314 | return parsePrimExpr(input); 315 | } 316 | } 317 | 318 | function parseBinaryExpr( 319 | input: ParserInput, 320 | left: Expr, 321 | precedence: number = -1, 322 | ): BinaryExpr { 323 | const op = parseBinaryOp(input); 324 | const right = parseExpr(input, op.precedence); 325 | return new BinaryExpr({ op, left, right }, left.row, left.column); 326 | } 327 | 328 | function parseBinaryOp(input: ParserInput): BinaryOp { 329 | const op = consume(input, t.Operator); 330 | return tokenToBinaryOp(op); 331 | } 332 | 333 | function tokenToBinaryOp(op: t.Operator): BinaryOp { 334 | switch (op.rep) { 335 | case '==': 336 | case '!=': 337 | return new EqOp(op.rep, op.row, op.column); 338 | case '<': 339 | case '<=': 340 | case '>': 341 | case '>=': 342 | return new CompOp(op.rep, op.row, op.column); 343 | case '+': 344 | case '-': 345 | case '|': 346 | case '^': 347 | return new AddOp(op.rep, op.row, op.column); 348 | case '*': 349 | case '/': 350 | case '%': 351 | case '&': 352 | return new MulOp(op.rep, op.row, op.column); 353 | case '||': 354 | case '&&': 355 | return new BoolOp(op.rep, op.row, op.column); 356 | default: 357 | throw new ParseError( 358 | op.row, 359 | op.column, 360 | { 361 | name: 'non-binary operator', 362 | rep: op.rep, 363 | }, 364 | { 365 | name: 'binary operator', 366 | }, 367 | ); 368 | } 369 | } 370 | 371 | const parseUnaryExpr: Parser = parseNode(UnaryExpr, input => { 372 | const op = parseUnaryOp(input); 373 | const right = parsePrimUnaryExpr(input); 374 | return { op, right }; 375 | }); 376 | 377 | const parseUnaryOp: Parser = parseNode(UnaryOp, input => { 378 | const op = consume(input, t.Operator); 379 | if (op.rep === '+' || op.rep === '-' || op.rep === '!') { 380 | return op.rep; 381 | } else { 382 | throw new ParseError( 383 | op.row, 384 | op.column, 385 | { 386 | name: 'non-unary operator', 387 | rep: op.rep, 388 | }, 389 | { name: 'unary operator' }, 390 | ); 391 | } 392 | }); 393 | 394 | function parsePrimExpr(input: ParserInput): PrimExpr { 395 | const token = nextToken(input); 396 | let expr = match, PrimExpr>( 397 | token, 398 | [ 399 | [token => token instanceof t.Literal, () => parseLitExpr(input)], 400 | [token => token.is(t.Ident), () => parseIdentExpr(input)], 401 | [token => token.is(t.Punctuation, '('), () => parseTupleExpr(input)], 402 | [token => token.is(t.Punctuation, '['), () => parseArrayExpr(input)], 403 | [token => token.is(t.Keyword, 'fn'), () => parseFuncExpr(input)], 404 | [token => token.is(t.Keyword, 'if'), () => parseCondExpr(input)], 405 | [token => token.is(t.Keyword, 'while'), () => parseLoopExpr(input)], 406 | [token => token.is(t.Keyword, 'new'), () => parseNewExpr(input)], 407 | ], 408 | () => { 409 | throw new ParseError(token.row, token.column, { 410 | name: token.constructor.name, 411 | rep: token.rep, 412 | }); 413 | }, 414 | ); 415 | 416 | while (nextToken(input).is(t.Punctuation)) { 417 | if (nextToken(input).is(t.Punctuation, '(')) { 418 | expr = parseCallExpr(input, expr); 419 | } else if (nextToken(input).is(t.Punctuation, '[')) { 420 | expr = parseIndexExpr(input, expr); 421 | } else { 422 | break; 423 | } 424 | } 425 | 426 | return expr; 427 | } 428 | 429 | const parseLitExpr: Parser = parseNode(LitExpr, parseLiteral); 430 | 431 | const parseIdentExpr: Parser = parseNode(IdentExpr, parseIdent); 432 | 433 | const parseTupleExpr: Parser = parseNode(TupleExpr, input => { 434 | consume(input, t.Punctuation, '('); 435 | let items: Array> = []; 436 | if (!nextToken(input).is(t.Punctuation, ')')) { 437 | items = commaSeparated(input, parseExpr); 438 | } 439 | consume(input, t.Punctuation, ')'); 440 | return { size: items.length, items }; 441 | }); 442 | 443 | const parseArrayExpr: Parser = parseNode(ArrayExpr, input => { 444 | consume(input, t.Punctuation, '['); 445 | let elems: Array> = []; 446 | if (!nextToken(input).is(t.Punctuation, ']')) { 447 | elems = commaSeparated(input, parseExpr); 448 | } 449 | consume(input, t.Punctuation, ']'); 450 | return elems; 451 | }); 452 | 453 | function parseCallExpr(input: ParserInput, func: PrimExpr): CallExpr { 454 | const args = parseTupleExpr(input); 455 | return new CallExpr({ func, args }, func.row, func.column); 456 | } 457 | 458 | function parseIndexExpr(input: ParserInput, target: PrimExpr): IndexExpr { 459 | consume(input, t.Punctuation, '['); 460 | const index = parseExpr(input); 461 | consume(input, t.Punctuation, ']'); 462 | return new IndexExpr({ target, index }, target.row, target.column); 463 | } 464 | 465 | const parseFuncExpr: Parser = parseNode(FuncExpr, input => { 466 | consume(input, t.Keyword, 'fn'); 467 | 468 | consume(input, t.Punctuation, '('); 469 | let params: Array = []; 470 | if (!nextToken(input).is(t.Punctuation, ')')) { 471 | params = commaSeparated(input, input => { 472 | const name = parseIdent(input); 473 | const type_ = parseType(input); 474 | return { name, type: type_ }; 475 | }); 476 | } 477 | consume(input, t.Punctuation, ')'); 478 | 479 | const returnType = parseType(input); 480 | const body = parseBlock(input); 481 | 482 | return { 483 | params: { 484 | size: params.length, 485 | items: params, 486 | }, 487 | returnType, 488 | body, 489 | }; 490 | }); 491 | 492 | const parseCondExpr: Parser = parseNode(CondExpr, input => { 493 | consume(input, t.Keyword, 'if'); 494 | const if_ = parseExpr(input); 495 | const then = parseBlock(input); 496 | consume(input, t.Keyword, 'else'); 497 | const else_ = parseBlock(input); 498 | return { if: if_, then, else: else_ }; 499 | }); 500 | 501 | const parseLoopExpr: Parser = parseNode(LoopExpr, input => { 502 | consume(input, t.Keyword, 'while'); 503 | const while_ = parseExpr(input); 504 | const body = parseBlock(input); 505 | return { while: while_, body }; 506 | }); 507 | 508 | const parseNewExpr: Parser = parseNode(NewExpr, input => { 509 | consume(input, t.Keyword, 'new'); 510 | const ty = parseType(input); 511 | consume(input, t.Punctuation, '['); 512 | const length = parseExpr(input); 513 | consume(input, t.Punctuation, ']'); 514 | return { type: ty, length }; 515 | }); 516 | 517 | const parseBlock: Parser = parseNode(Block, input => { 518 | consume(input, t.Punctuation, '{'); 519 | 520 | let bodies: Block['value']['bodies'] = []; 521 | let returnVoid = true; 522 | 523 | while (!nextToken(input).is(t.Punctuation, '}')) { 524 | if (nextToken(input).is(t.Keyword, 'let')) { 525 | bodies.push(parseDecl(input)); 526 | } else if (nextToken(input).is(t.Keyword, 'break')) { 527 | bodies.push(parseBreak(input)); 528 | } else { 529 | const expr = parseExpr(input); 530 | if (nextToken(input).is(t.Punctuation, '=')) { 531 | if (expr instanceof IdentExpr || expr instanceof IndexExpr) { 532 | bodies.push(parseAssign(input, expr)); 533 | } else { 534 | throw new ParseError(expr.row, expr.column, expr, { 535 | name: 'LVal (IdentExpr or IndexExpr)', 536 | }); 537 | } 538 | } else { 539 | bodies.push(expr); 540 | } 541 | } 542 | 543 | if (nextToken(input).is(t.Punctuation, ';')) { 544 | consume(input, t.Punctuation, ';'); 545 | returnVoid = true; 546 | } else { 547 | returnVoid = false; 548 | break; 549 | } 550 | } 551 | 552 | consume(input, t.Punctuation, '}'); 553 | 554 | return { bodies, returnVoid }; 555 | }); 556 | 557 | function parseAssign(input: ParserInput, lVal: LVal): Assign { 558 | consume(input, t.Punctuation, '='); 559 | const expr = parseExpr(input); 560 | return new Assign({ lVal, expr }, lVal.row, lVal.column); 561 | } 562 | 563 | const parseBreak: Parser = parseNode(Break, input => { 564 | consume(input, t.Keyword, 'break'); 565 | return null; 566 | }); 567 | -------------------------------------------------------------------------------- /src/report-error.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { LexError } from './lexer/error'; 3 | import { ParseError } from './parser/error'; 4 | import { TypeError } from './typechecker/error'; 5 | 6 | export function exitWithErrors(errors: Array) { 7 | errors.forEach(err => console.error(err)); 8 | process.exit(1); 9 | } 10 | 11 | export function reportCompileError( 12 | input: string, 13 | err: LexError | ParseError | TypeError | any, 14 | ) { 15 | if ( 16 | !( 17 | err instanceof LexError || 18 | err instanceof ParseError || 19 | err instanceof TypeError 20 | ) 21 | ) { 22 | throw err; 23 | } 24 | 25 | const errors: Array = []; 26 | 27 | errors.push(`${err.name}: ${err.message}\n`); 28 | 29 | const lineIdx = err.row - 1; 30 | const fromIdx = lineIdx < 1 ? 0 : lineIdx - 1; 31 | const toIdx = lineIdx + 2; 32 | const targetIdx = lineIdx - fromIdx; 33 | 34 | const lineNoDigitLen = toIdx.toString().length; 35 | 36 | input 37 | .split('\n') 38 | .slice(fromIdx, toIdx) 39 | .forEach((line, idx) => { 40 | const lineNo = fromIdx + idx + 1; 41 | errors.push( 42 | `${' '.repeat(lineNoDigitLen - lineNo.toString().length)}${chalk.grey( 43 | lineNo + '|', 44 | )} ${line}`, 45 | ); 46 | 47 | if (targetIdx === idx) { 48 | errors.push( 49 | ` ${' '.repeat(lineNoDigitLen + err.column - 1)}${chalk.red('^')}`, 50 | ); 51 | } 52 | }); 53 | 54 | exitWithErrors(errors); 55 | } 56 | -------------------------------------------------------------------------------- /src/stdlib.ts: -------------------------------------------------------------------------------- 1 | import * as a from './parser/ast'; 2 | import { SExp } from 's-exify'; 3 | 4 | export type StdFunc = { 5 | name: string; 6 | type: a.Type; 7 | init?: Array; 8 | expr?: Array; 9 | }; 10 | 11 | export const defaultStdFuncs = (): Array => [stdInt, stdFloat, stdLen]; 12 | 13 | const funcTy = (val: { param: a.Type; return: a.Type }) => 14 | new a.FuncType(val, -1, -1); 15 | const arrTy = (val: a.Type) => new a.ArrayType(val, -1, -1); 16 | 17 | export const stdInt: StdFunc = { 18 | name: 'int', 19 | type: funcTy({ param: a.FloatType.instance, return: a.IntType.instance }), 20 | expr: [['i32.trunc_s/f64']], 21 | }; 22 | 23 | export const stdFloat: StdFunc = { 24 | name: 'float', 25 | type: funcTy({ param: a.IntType.instance, return: a.FloatType.instance }), 26 | expr: [['f64.convert_s/i32']], 27 | }; 28 | 29 | export const stdLen: StdFunc = { 30 | name: 'len', 31 | type: funcTy({ 32 | param: arrTy(a.AnyType.instance), 33 | return: a.IntType.instance, 34 | }), 35 | expr: [['i32.load']], 36 | }; 37 | -------------------------------------------------------------------------------- /src/typechecker/context.ts: -------------------------------------------------------------------------------- 1 | import * as a from '../parser/ast'; 2 | import { TypeError } from './error'; 3 | import { StdFunc, defaultStdFuncs } from '../stdlib'; 4 | 5 | export type IdentTypeDef = { 6 | ident: a.Ident; 7 | type: a.Type; 8 | }; 9 | 10 | export class TypeContext { 11 | private scopes: Array>>; 12 | private loops: Array = []; 13 | 14 | constructor(public stdFuncs: Array = defaultStdFuncs()) { 15 | this.scopes = [ 16 | new Map( 17 | stdFuncs.map<[string, a.Type]>(({ name, type }) => [name, type]), 18 | ), 19 | ]; 20 | } 21 | 22 | get currentScope() { 23 | return this.scopes[0]; 24 | } 25 | 26 | get currentLoop(): a.LoopExpr | null { 27 | return this.loops[0] || null; 28 | } 29 | 30 | enterScope() { 31 | this.scopes.unshift(new Map()); 32 | } 33 | 34 | leaveScope() { 35 | this.scopes.shift(); 36 | } 37 | 38 | enterLoop(loop: a.LoopExpr) { 39 | this.loops.unshift(loop); 40 | } 41 | 42 | leaveLoop() { 43 | this.loops.shift(); 44 | } 45 | 46 | push({ ident, type: ty }: IdentTypeDef) { 47 | const name = ident.value; 48 | if (this.currentScope.has(name)) { 49 | throw new TypeError( 50 | ident, 51 | undefined, 52 | `identifier '${name}' has already been declared`, 53 | 'SemanticError', 54 | ); 55 | } else { 56 | this.currentScope.set(name, ty); 57 | } 58 | } 59 | 60 | getTypeOf(ident: a.Ident): a.Type | null { 61 | for (const scope of this.scopes) { 62 | const ty = scope.get(ident.value); 63 | if (ty) { 64 | return ty; 65 | } 66 | } 67 | return null; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/typechecker/error.ts: -------------------------------------------------------------------------------- 1 | export class TypeError extends Error { 2 | name: 'TypeError' | 'SemanticError'; 3 | row: number; 4 | column: number; 5 | 6 | constructor( 7 | public actual: { row: number; column: number; name: string }, 8 | public expected?: { name: string }, 9 | message: string = 'Type mismatch', 10 | name: 'TypeError' | 'SemanticError' = 'TypeError', 11 | ) { 12 | super( 13 | `${message}: ${expected ? `expected ${expected.name}, ` : ''}found ${ 14 | actual.name 15 | } at ${actual.row}:${actual.column}`, 16 | ); 17 | 18 | this.name = name; 19 | this.row = actual.row; 20 | this.column = actual.column; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/typechecker/index.ts: -------------------------------------------------------------------------------- 1 | import * as a from '../parser/ast'; 2 | import { orStr } from '../util'; 3 | import { TypeContext, IdentTypeDef } from './context'; 4 | import { TypeError } from './error'; 5 | 6 | export function typeCheck(mod: a.Module, ctx: TypeContext) { 7 | ctx.enterScope(); 8 | 9 | // FIXME: process imports 10 | 11 | // register global decls 12 | mod.value.decls.forEach(decl => registerDeclType(decl, ctx)); 13 | 14 | // actual type checks for exprs (including function body) 15 | mod.value.decls.forEach(decl => checkDeclType(decl, ctx)); 16 | 17 | ctx.leaveScope(); 18 | 19 | return mod; 20 | } 21 | 22 | function cloneType>( 23 | orig: TY, 24 | { row, column }: { row: number; column: number }, 25 | ): TY { 26 | return new (orig.constructor as any)(orig.value, row, column); 27 | } 28 | 29 | export function checkExprType( 30 | expr: a.Expr, 31 | ctx: TypeContext, 32 | checkFuncBody: boolean = true, 33 | ): a.Type { 34 | if (expr.type) { 35 | return expr.type; 36 | } 37 | 38 | const ty = checkExprTypeWithoutCache(expr, ctx, checkFuncBody); 39 | 40 | // do not cache the result for FuncExpr without body check 41 | if (!(expr instanceof a.FuncExpr && !checkFuncBody)) { 42 | expr.type = ty; 43 | } 44 | 45 | return ty; 46 | } 47 | 48 | function checkExprTypeWithoutCache( 49 | expr: a.Expr, 50 | ctx: TypeContext, 51 | checkFuncBody: boolean = true, 52 | ): a.Type { 53 | if (expr instanceof a.LitExpr) { 54 | if (expr.value instanceof a.IntLit) { 55 | return new a.IntType(expr.row, expr.column); 56 | } else if (expr.value instanceof a.FloatLit) { 57 | return new a.FloatType(expr.row, expr.column); 58 | } else if (expr.value instanceof a.CharLit) { 59 | return new a.CharType(expr.row, expr.column); 60 | } else if (expr.value instanceof a.BoolLit) { 61 | return new a.BoolType(expr.row, expr.column); 62 | } else if (expr.value instanceof a.StrLit) { 63 | return new a.StrType(expr.row, expr.column); 64 | } 65 | } else if (expr instanceof a.IdentExpr) { 66 | const ty = ctx.getTypeOf(expr.value); 67 | if (ty) { 68 | return cloneType(ty, expr); 69 | } else { 70 | throw new TypeError( 71 | { 72 | row: expr.row, 73 | column: expr.column, 74 | name: expr.value.value, 75 | }, 76 | undefined, 77 | 'undefined identifier', 78 | 'SemanticError', 79 | ); 80 | } 81 | } else if (expr instanceof a.TupleExpr) { 82 | return new a.TupleType( 83 | { 84 | size: expr.value.size, 85 | items: expr.value.items.map(item => checkExprType(item, ctx)), 86 | }, 87 | expr.row, 88 | expr.column, 89 | ); 90 | } else if (expr instanceof a.ArrayExpr) { 91 | if (expr.value.length === 0) { 92 | return new a.ArrayType(a.AnyType.instance, expr.row, expr.column); 93 | } 94 | const ty = checkExprType(expr.value[0], ctx); 95 | for (let i = 1; i < expr.value.length; i++) { 96 | assertType(checkExprType(expr.value[i], ctx), ty); 97 | } 98 | return new a.ArrayType(ty, expr.row, expr.column); 99 | } else if (expr instanceof a.FuncExpr) { 100 | let param: a.Type; 101 | if (expr.value.params.size === 1) { 102 | param = expr.value.params.items[0].type; 103 | } else { 104 | param = new a.TupleType( 105 | { 106 | size: expr.value.params.size, 107 | items: expr.value.params.items.map(item => item.type), 108 | }, 109 | expr.row, 110 | expr.column, 111 | ); 112 | } 113 | 114 | if (checkFuncBody) { 115 | const bodyType = checkBlockType( 116 | expr.value.body, 117 | ctx, 118 | expr.value.params.items.map(({ name, type: ty }) => ({ 119 | ident: name, 120 | type: ty, 121 | })), 122 | ); 123 | 124 | try { 125 | assertType(bodyType, expr.value.returnType); 126 | } catch { 127 | let message = 'Function return type mismatch'; 128 | 129 | if (expr.value.returnType instanceof a.VoidType) { 130 | message += ", ';' may be missing"; 131 | } 132 | 133 | throw new TypeError(bodyType, expr.value.returnType, message); 134 | } 135 | } 136 | 137 | return new a.FuncType( 138 | { param, return: expr.value.returnType }, 139 | expr.row, 140 | expr.column, 141 | ); 142 | } else if (expr instanceof a.CallExpr) { 143 | const funcType = checkExprType(expr.value.func, ctx); 144 | const argsType = checkExprType(expr.value.args, ctx); 145 | 146 | if (!(funcType instanceof a.FuncType)) { 147 | throw new TypeError( 148 | funcType, 149 | { name: 'function' }, 150 | 'non-callable target', 151 | 'SemanticError', 152 | ); 153 | } 154 | 155 | try { 156 | assertType(argsType, funcType.value.param); 157 | } catch { 158 | throw new TypeError( 159 | argsType, 160 | funcType.value.param, 161 | 'Function parameter type mismatch', 162 | ); 163 | } 164 | 165 | return cloneType(funcType.value.return, expr); 166 | } else if (expr instanceof a.IndexExpr) { 167 | const targetType = checkExprType(expr.value.target, ctx); 168 | if (targetType instanceof a.ArrayType) { 169 | const indexType = checkExprType(expr.value.index, ctx); 170 | if (indexType instanceof a.IntType) { 171 | return cloneType(targetType.value, expr); 172 | } else { 173 | throw new TypeError( 174 | indexType, 175 | a.IntType.instance, 176 | 'Index type mismatch', 177 | ); 178 | } 179 | } else if (targetType instanceof a.StrType) { 180 | const indexType = checkExprType(expr.value.index, ctx); 181 | if (indexType instanceof a.IntType) { 182 | return new a.CharType(expr.row, expr.column); 183 | } else { 184 | throw new TypeError( 185 | indexType, 186 | a.IntType.instance, 187 | 'Index type mismatch', 188 | ); 189 | } 190 | } else if (targetType instanceof a.TupleType) { 191 | const index = expr.value.index; 192 | if (index instanceof a.LitExpr && index.value instanceof a.IntLit) { 193 | const lit = index.value; 194 | if (lit.parsedValue < targetType.value.size) { 195 | return cloneType(targetType.value.items[lit.parsedValue], expr); 196 | } else { 197 | throw new TypeError( 198 | { row: lit.row, column: lit.column, name: lit.value }, 199 | { name: `int < ${targetType.value.size}` }, 200 | 'Tuple index out of range', 201 | ); 202 | } 203 | } else { 204 | throw new TypeError( 205 | { row: index.row, column: index.column, name: 'expr' }, 206 | undefined, 207 | 'Invalid tuple index: only int literal is allowed for tuple index', 208 | ); 209 | } 210 | } else { 211 | throw new TypeError( 212 | targetType, 213 | { name: 'array, str or tuple' }, 214 | 'Indexable type mismatch', 215 | ); 216 | } 217 | } else if (expr instanceof a.CondExpr) { 218 | assertType( 219 | checkExprType(expr.value.if, ctx), 220 | new a.BoolType(expr.row, expr.column), 221 | ); 222 | 223 | const ifBlockType = checkBlockType(expr.value.then, ctx); 224 | const elseBlockType = checkBlockType(expr.value.else, ctx); 225 | try { 226 | assertType(ifBlockType, elseBlockType); 227 | } catch { 228 | let message = "'else' block should have the same type as 'if' block"; 229 | 230 | if (ifBlockType instanceof a.VoidType) { 231 | message += ", ';' may be missing"; 232 | } 233 | 234 | throw new TypeError(elseBlockType, ifBlockType, message); 235 | } 236 | 237 | return ifBlockType; 238 | } else if (expr instanceof a.LoopExpr) { 239 | const condType = checkExprType(expr.value.while, ctx); 240 | 241 | if (condType instanceof a.BoolType) { 242 | ctx.enterLoop(expr); 243 | checkBlockType(expr.value.body, ctx); 244 | ctx.leaveLoop(); 245 | return a.VoidType.instance; 246 | } else { 247 | throw new TypeError( 248 | condType, 249 | undefined, 250 | 'Loop condition should be a boolean', 251 | ); 252 | } 253 | } else if (expr instanceof a.NewExpr) { 254 | const lengthType = checkExprType(expr.value.length, ctx); 255 | if (lengthType instanceof a.IntType) { 256 | return new a.ArrayType(expr.value.type, expr.row, expr.column); 257 | } else { 258 | throw new TypeError( 259 | lengthType, 260 | a.IntType.instance, 261 | 'Length type mismatch', 262 | ); 263 | } 264 | } else if (expr instanceof a.UnaryExpr) { 265 | const opTypes = expr.value.op.getOperandTypes(); 266 | const rightActualTy = checkExprType(expr.value.right, ctx); 267 | for (const { right, ret } of opTypes) { 268 | try { 269 | assertType(rightActualTy, right); 270 | 271 | return cloneType(ret, expr); 272 | } catch { 273 | // ignore, try the next 274 | } 275 | } 276 | // fails for all the operand types 277 | throw new TypeError( 278 | rightActualTy, 279 | { 280 | name: orStr(opTypes.map(x => x.right.name)), 281 | }, 282 | `Operand type mismatch for '${expr.value.op.value}'`, 283 | ); 284 | } else if (expr instanceof a.BinaryExpr) { 285 | const leftActualTy = checkExprType(expr.value.left, ctx); 286 | const opTypes = expr.value.op.getOperandTypes(leftActualTy); 287 | const rightActualTy = checkExprType(expr.value.right, ctx); 288 | for (const { left, right, ret } of opTypes) { 289 | try { 290 | assertType(leftActualTy, left); 291 | } catch { 292 | // ignore, try the next 293 | continue; 294 | } 295 | 296 | try { 297 | assertType(rightActualTy, right); 298 | 299 | return cloneType(ret, expr); 300 | } catch { 301 | throw new TypeError( 302 | rightActualTy, 303 | right, 304 | `Right-hand operand type mismatch for '${expr.value.op.value}'`, 305 | ); 306 | } 307 | } 308 | // fails for all the operand types 309 | throw new TypeError( 310 | leftActualTy, 311 | { 312 | name: orStr(opTypes.map(x => x.left.name)), 313 | }, 314 | `Left-hand operand type mismatch for '${expr.value.op.value}'`, 315 | ); 316 | } 317 | 318 | throw new TypeError({ 319 | row: expr.row, 320 | column: expr.column, 321 | name: 'invalid type', 322 | }); 323 | } 324 | 325 | function registerDeclType(decl: a.Decl, ctx: TypeContext) { 326 | if (!decl.value.type) { 327 | // ensure type property 328 | decl.value.type = checkExprType(decl.value.expr, ctx, false); 329 | } 330 | 331 | ctx.push({ ident: decl.value.name, type: decl.value.type }); 332 | } 333 | 334 | function checkDeclType(decl: a.Decl, ctx: TypeContext) { 335 | if (!decl.value.type) { 336 | throw new TypeError( 337 | decl, 338 | undefined, 339 | 'Type of decl should be tagged before being checked', 340 | ); 341 | } 342 | 343 | if (containsVoidType(decl.value.type)) { 344 | throw new TypeError( 345 | { name: decl.value.type.name, row: decl.row, column: decl.column }, 346 | undefined, 347 | 'A decl type cannot contain void', 348 | 'SemanticError', 349 | ); 350 | } 351 | 352 | assertType(checkExprType(decl.value.expr, ctx), decl.value.type); 353 | } 354 | 355 | function checkAssignType(assign: a.Assign, ctx: TypeContext) { 356 | assertType( 357 | checkExprType(assign.value.expr, ctx), 358 | checkExprType(assign.value.lVal, ctx), 359 | ); 360 | } 361 | 362 | function checkBreakType(break_: a.Break, ctx: TypeContext) { 363 | if (!ctx.currentLoop) { 364 | throw new TypeError( 365 | { name: 'unexpected break', row: break_.row, column: break_.column }, 366 | undefined, 367 | 'break can be only used in a loop', 368 | 'SemanticError', 369 | ); 370 | } 371 | } 372 | 373 | function containsVoidType(ty: a.Type): boolean { 374 | if (ty instanceof a.VoidType) { 375 | return true; 376 | } else if (ty instanceof a.TupleType) { 377 | return ty.value.items.some(containsVoidType); 378 | } else if (ty instanceof a.ArrayType) { 379 | return containsVoidType(ty.value); 380 | } else { 381 | return false; 382 | } 383 | } 384 | 385 | export function checkBlockType( 386 | block: a.Block, 387 | ctx: TypeContext, 388 | initialDefs: Array = [], 389 | ): a.Type { 390 | ctx.enterScope(); 391 | initialDefs.forEach(def => ctx.push(def)); 392 | 393 | let exprType: a.Type = new a.VoidType(block.row, block.column); 394 | block.value.bodies.forEach(body => { 395 | if (body instanceof a.Decl) { 396 | registerDeclType(body, ctx); 397 | checkDeclType(body, ctx); 398 | } else if (body instanceof a.Expr) { 399 | exprType = checkExprType(body, ctx); 400 | } else if (body instanceof a.Assign) { 401 | checkAssignType(body, ctx); 402 | } else { 403 | checkBreakType(body, ctx); 404 | } 405 | }); 406 | 407 | const ty: a.Type = block.value.returnVoid 408 | ? new a.VoidType(block.row, block.column) 409 | : exprType; 410 | 411 | ctx.leaveScope(); 412 | 413 | return ty; 414 | } 415 | 416 | export function assertType(actual: a.Type, expected: a.Type) { 417 | // if any of them is AnyType, it always succeeds 418 | if (actual instanceof a.AnyType || expected instanceof a.AnyType) { 419 | return; 420 | } 421 | 422 | // simple types 423 | if ( 424 | (actual instanceof a.IntType && expected instanceof a.IntType) || 425 | (actual instanceof a.FloatType && expected instanceof a.FloatType) || 426 | (actual instanceof a.StrType && expected instanceof a.StrType) || 427 | (actual instanceof a.BoolType && expected instanceof a.BoolType) || 428 | (actual instanceof a.CharType && expected instanceof a.CharType) || 429 | (actual instanceof a.VoidType && expected instanceof a.VoidType) 430 | ) { 431 | return; 432 | } 433 | 434 | // func type 435 | if (actual instanceof a.FuncType && expected instanceof a.FuncType) { 436 | try { 437 | assertType(actual.value.param, expected.value.param); 438 | assertType(actual.value.return, expected.value.return); 439 | } catch { 440 | throw new TypeError(actual, expected); 441 | } 442 | return; 443 | } 444 | 445 | // tuple type 446 | if (actual instanceof a.TupleType && expected instanceof a.TupleType) { 447 | if (expected.value.size !== actual.value.size) { 448 | throw new TypeError(actual, expected, 'Tuple length mismatch'); 449 | } 450 | 451 | for (let i = 0; i < expected.value.size; i++) { 452 | try { 453 | assertType(actual.value.items[i], expected.value.items[i]); 454 | } catch { 455 | throw new TypeError(actual, expected); 456 | } 457 | } 458 | 459 | return; 460 | } 461 | 462 | // array type 463 | if (actual instanceof a.ArrayType && expected instanceof a.ArrayType) { 464 | try { 465 | assertType(actual.value, expected.value); 466 | } catch { 467 | throw new TypeError(actual, expected); 468 | } 469 | return; 470 | } 471 | 472 | throw new TypeError(actual, expected); 473 | } 474 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | type Condition = T | ((val: T) => boolean); 2 | 3 | function predicate(val: T, cond: Condition): boolean { 4 | if (cond instanceof Function) { 5 | return cond(val); 6 | } else { 7 | return val === cond; 8 | } 9 | } 10 | 11 | export function match( 12 | val: V, 13 | matches: Array<[Condition, () => T]>, 14 | fallback: () => T, 15 | ): T { 16 | for (const [p, f] of matches) { 17 | if (predicate(val, p)) { 18 | return f(); 19 | } 20 | } 21 | return fallback(); 22 | } 23 | 24 | export function isDigit(c: string): boolean { 25 | return /^[0-9]$/.test(c); 26 | } 27 | 28 | export function isAlphabet(c: string): boolean { 29 | return /^[a-zA-Z]$/.test(c); 30 | } 31 | 32 | export function isAlphanumeric(c: string): boolean { 33 | return isDigit(c) || isAlphabet(c); 34 | } 35 | 36 | export function unescape(str: string): string { 37 | return str 38 | .replace(/\\n/g, '\n') 39 | .replace(/\\r/g, '\r') 40 | .replace(/\\t/g, '\t') 41 | .replace(/\\\\/g, '\\') 42 | .replace(/\\'/g, "'") 43 | .replace(/\\"/g, '"'); 44 | } 45 | 46 | export function orStr(words: Array): string { 47 | const last = words.slice(-1)[0]; 48 | const rests = words.slice(0, -1).join(', '); 49 | return rests ? `${rests} or ${last}` : last; 50 | } 51 | 52 | export type Arity1 = (x: T) => U; 53 | 54 | export class Compose { 55 | constructor(public f: Arity1) {} 56 | 57 | static then(f: Arity1): Compose { 58 | return new Compose(f); 59 | } 60 | 61 | then(g: Arity1): Compose { 62 | return new Compose((x: T) => g(this.f(x))); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/wasm.ts: -------------------------------------------------------------------------------- 1 | const wabt = require('wabt'); 2 | 3 | const { parseWat } = wabt(); 4 | 5 | const FILENAME = 'main.wat'; 6 | 7 | export function wat2wasm(wat: string): Buffer { 8 | const module = parseWat(FILENAME, wat); 9 | const binary = module.toBinary({ 10 | log: false, 11 | canonicalize_lebs: false, 12 | relocatable: false, 13 | write_debug_names: false, 14 | }); 15 | return Buffer.from(binary.buffer); 16 | } 17 | 18 | export const magicNumber = Buffer.from([0x00, 0x61, 0x73, 0x6d]); 19 | 20 | export function convertMiBToPage(mib: number): number { 21 | // 1 page in WASM is 64KiB 22 | return (mib * 1024) / 64; 23 | } 24 | 25 | export type WASMResult = { 26 | value: any; 27 | memory: WebAssembly.Memory; 28 | }; 29 | 30 | export async function runWASM( 31 | wasmModule: Buffer, 32 | opts: { 33 | main: string; 34 | memorySize: number; 35 | }, 36 | ): Promise { 37 | // FIXME: stdlib 38 | 39 | const memory = new WebAssembly.Memory({ 40 | initial: convertMiBToPage(opts.memorySize), 41 | }); 42 | 43 | const uint32arr = new Uint32Array(memory.buffer); 44 | uint32arr[0] = 4; // set current heap pointer, the first 4 bytes are used for the pointer 45 | 46 | const imports = { 47 | js: { 48 | memory, 49 | }, 50 | }; 51 | 52 | const { instance } = await WebAssembly.instantiate(wasmModule, imports); 53 | const value = instance.exports[opts.main](); 54 | 55 | return { value, memory }; 56 | } 57 | -------------------------------------------------------------------------------- /test/codegen.spec.ts: -------------------------------------------------------------------------------- 1 | import { deepEqual } from 'assert'; 2 | import chalk from 'chalk'; 3 | import { tokenize } from '../src/lexer/'; 4 | import { parse } from '../src/parser/'; 5 | import { desugarBefore, desugarAfter } from '../src/desugarer/'; 6 | import { typeCheck } from '../src/typechecker/'; 7 | import { TypeContext } from '../src/typechecker/context'; 8 | import { genWASM } from '../src/codegen/'; 9 | import { Compose } from '../src/util'; 10 | import { runWASM, WASMResult } from '../src/wasm'; 11 | 12 | console.log(chalk.bold('Running codegen tests...')); 13 | 14 | const memorySize = 4; // 4MiB 15 | 16 | const compile = Compose.then(tokenize) 17 | .then(parse) 18 | .then(desugarBefore) 19 | .then(mod => typeCheck(mod, new TypeContext())) 20 | .then(desugarAfter) 21 | .then(mod => genWASM(mod, { exports: ['test'], memorySize })).f; 22 | 23 | async function moduleRunTest( 24 | moduleStr: string, 25 | expected: any, 26 | resultHandler: (result: WASMResult) => any = result => result.value, 27 | ): Promise { 28 | try { 29 | const wasmModule = compile(moduleStr); 30 | const result = await runWASM(wasmModule, { main: 'test', memorySize }); 31 | deepEqual(resultHandler(result), expected); 32 | } catch (err) { 33 | console.error(chalk.blue.bold('Test:')); 34 | console.error(moduleStr); 35 | console.error(); 36 | console.error(chalk.red.bold('Error:')); 37 | console.error(err.message); 38 | process.exit(1); 39 | } 40 | } 41 | 42 | (async function() { 43 | await moduleRunTest( 44 | ` 45 | let test = fn () float { 46 | 1234. 47 | } 48 | `, 49 | 1234, 50 | ); 51 | 52 | await moduleRunTest( 53 | ` 54 | let test = fn () void { 55 | 1234.; 56 | } 57 | `, 58 | undefined, 59 | ); 60 | 61 | await moduleRunTest( 62 | ` 63 | let test = fn () int { 64 | 1234 65 | } 66 | `, 67 | 1234, 68 | ); 69 | 70 | await moduleRunTest( 71 | ` 72 | let test = fn () bool { 73 | true 74 | } 75 | `, 76 | 1, 77 | ); 78 | 79 | await moduleRunTest( 80 | ` 81 | let test = fn () char { 82 | '\\n' 83 | } 84 | `, 85 | '\n'.codePointAt(0), 86 | ); 87 | 88 | await moduleRunTest( 89 | ` 90 | let test = fn () int { 91 | let x = 1234; 92 | x 93 | } 94 | `, 95 | 1234, 96 | ); 97 | 98 | await moduleRunTest( 99 | ` 100 | let test = fn () int { 101 | let x = 1234; 102 | x; 103 | let y = 123; 104 | y 105 | } 106 | `, 107 | 123, 108 | ); 109 | 110 | await moduleRunTest( 111 | ` 112 | let x = 1234 113 | let y = 123 114 | let test = fn () int { 115 | x; 116 | y 117 | } 118 | `, 119 | 123, 120 | ); 121 | 122 | await moduleRunTest( 123 | ` 124 | let test_impl = fn () int { 125 | let x = 1234; 126 | x; 127 | let y = 123; 128 | y 129 | } 130 | 131 | let test = test_impl 132 | `, 133 | 123, 134 | ); 135 | 136 | await moduleRunTest( 137 | ` 138 | let test_impl = fn (x float) float { 139 | 1234; 140 | x 141 | } 142 | 143 | let test = fn () float { 144 | test_impl(123.) 145 | } 146 | `, 147 | 123, 148 | ); 149 | 150 | await moduleRunTest( 151 | ` 152 | let test_impl = fn (x float, y int) int { 153 | x; 154 | y 155 | } 156 | 157 | let test = fn () int { 158 | test_impl(123., test_impl(123., 1234)) 159 | } 160 | `, 161 | 1234, 162 | ); 163 | 164 | await moduleRunTest( 165 | ` 166 | let test_impl = fn (x float, y int) int { 167 | x; 168 | y 169 | } 170 | 171 | let test = fn () int { 172 | let f = test_impl; 173 | let g = f; 174 | g(123., f(123., 1234)) 175 | } 176 | `, 177 | 1234, 178 | ); 179 | 180 | await moduleRunTest( 181 | ` 182 | let test_impl = fn (x float, y int) int { 183 | x; 184 | y 185 | } 186 | 187 | let x = test_impl(123., 1234) 188 | 189 | let test = fn () int { 190 | x 191 | } 192 | `, 193 | 1234, 194 | ); 195 | 196 | await moduleRunTest( 197 | ` 198 | let test = fn () int { 199 | -100 200 | } 201 | `, 202 | -100, 203 | ); 204 | 205 | await moduleRunTest( 206 | ` 207 | let test = fn () float { 208 | +100.123 209 | } 210 | `, 211 | 100.123, 212 | ); 213 | 214 | await moduleRunTest( 215 | ` 216 | let test = fn () float { 217 | -100.123 218 | } 219 | `, 220 | -100.123, 221 | ); 222 | 223 | await moduleRunTest( 224 | ` 225 | let test = fn () bool { 226 | !false 227 | } 228 | `, 229 | 1, 230 | ); 231 | 232 | await moduleRunTest( 233 | ` 234 | let test = fn () bool { 235 | !true 236 | } 237 | `, 238 | 0, 239 | ); 240 | 241 | await moduleRunTest( 242 | ` 243 | let a = 1234. 244 | let f = fn () float { 245 | a; 246 | 123. 247 | } 248 | let id = fn (x float) float { x } 249 | let c = fn (b bool) float { 250 | let x = 1.23; 251 | if (!b) { 252 | let x = a; 253 | x 254 | } else { 255 | let y = x; 256 | let x = y; 257 | let f = id; 258 | f(x) 259 | } 260 | } 261 | let test = fn () float { 262 | c(false); 263 | c(true) 264 | } 265 | `, 266 | 1.23, 267 | ); 268 | 269 | await moduleRunTest( 270 | ` 271 | let test = fn () bool { 272 | 1234 == 1234 273 | } 274 | `, 275 | 1, 276 | ); 277 | 278 | await moduleRunTest( 279 | ` 280 | let test = fn () bool { 281 | 1234. == 1234. 282 | } 283 | `, 284 | 1, 285 | ); 286 | 287 | await moduleRunTest( 288 | ` 289 | let test = fn () bool { 290 | 'a' != 'a' 291 | } 292 | `, 293 | 0, 294 | ); 295 | 296 | await moduleRunTest( 297 | ` 298 | let test = fn () bool { 299 | 'a' < 'b' 300 | } 301 | `, 302 | 1, 303 | ); 304 | 305 | await moduleRunTest( 306 | ` 307 | let test = fn () bool { 308 | 'a' <= 'a' 309 | } 310 | `, 311 | 1, 312 | ); 313 | 314 | await moduleRunTest( 315 | ` 316 | let test = fn () bool { 317 | 1.234 > 12.34 318 | } 319 | `, 320 | 0, 321 | ); 322 | 323 | await moduleRunTest( 324 | ` 325 | let test = fn () bool { 326 | 1234 >= 1235 327 | } 328 | `, 329 | 0, 330 | ); 331 | 332 | await moduleRunTest( 333 | ` 334 | let test = fn () int { 335 | 1234 + 5678 336 | } 337 | `, 338 | 6912, 339 | ); 340 | 341 | await moduleRunTest( 342 | ` 343 | let test = fn () float { 344 | 1234. + .5678 345 | } 346 | `, 347 | 1234.5678, 348 | ); 349 | 350 | await moduleRunTest( 351 | ` 352 | let test = fn () int { 353 | 1234 - 5678 + 1 354 | } 355 | `, 356 | -4443, 357 | ); 358 | 359 | await moduleRunTest( 360 | ` 361 | let test = fn () float { 362 | 1234. - 5678. + 1. 363 | } 364 | `, 365 | -4443, 366 | ); 367 | 368 | await moduleRunTest( 369 | ` 370 | let test = fn () int { 371 | 1234 ^ 5678 372 | } 373 | `, 374 | 4860, 375 | ); 376 | 377 | await moduleRunTest( 378 | ` 379 | let test = fn () int { 380 | 1234 | 5678 381 | } 382 | `, 383 | 5886, 384 | ); 385 | 386 | await moduleRunTest( 387 | ` 388 | let test = fn () int { 389 | 1234 & 5678 390 | } 391 | `, 392 | 1026, 393 | ); 394 | 395 | await moduleRunTest( 396 | ` 397 | let test = fn () int { 398 | 1234 * 5678 399 | } 400 | `, 401 | 7006652, 402 | ); 403 | 404 | await moduleRunTest( 405 | ` 406 | let test = fn () float { 407 | 1234. * 5678. 408 | } 409 | `, 410 | 7006652, 411 | ); 412 | 413 | await moduleRunTest( 414 | ` 415 | let test = fn () int { 416 | -5678 / 1234 417 | } 418 | `, 419 | -4, 420 | ); 421 | 422 | await moduleRunTest( 423 | ` 424 | let test = fn () float { 425 | 4. / -2. 426 | } 427 | `, 428 | -2, 429 | ); 430 | 431 | await moduleRunTest( 432 | ` 433 | let test = fn () int { 434 | 1234 % 123 435 | } 436 | `, 437 | 4, 438 | ); 439 | 440 | await moduleRunTest( 441 | ` 442 | let test = fn () int { 443 | -1234 % -123 444 | } 445 | `, 446 | -4, 447 | ); 448 | 449 | await moduleRunTest( 450 | ` 451 | let test = fn () bool { 452 | false && true 453 | } 454 | `, 455 | 0, 456 | ); 457 | 458 | await moduleRunTest( 459 | ` 460 | let test = fn () bool { 461 | true && (1 == 1) 462 | } 463 | `, 464 | 1, 465 | ); 466 | 467 | await moduleRunTest( 468 | ` 469 | let test = fn () bool { 470 | true || false 471 | } 472 | `, 473 | 1, 474 | ); 475 | 476 | await moduleRunTest( 477 | ` 478 | let fac = fn (n int) int { 479 | if n == 1 { 480 | 1 481 | } else { 482 | n * fac(n - 1) 483 | } 484 | } 485 | 486 | let test = fn () int { 487 | fac(10) 488 | } 489 | `, 490 | 3628800, 491 | ); 492 | 493 | const s2i = (slice: ArrayBuffer) => new Int32Array(slice)[0]; 494 | const s2f = (slice: ArrayBuffer) => new Float64Array(slice)[0]; 495 | 496 | const tuple = (...sizes: Array) => ({ 497 | value, 498 | memory, 499 | }: WASMResult) => { 500 | let offset = value; 501 | return sizes.map(size => { 502 | const slice = memory.buffer.slice(offset, offset + size); 503 | let value; 504 | if (size === 8) { 505 | // must be float 506 | value = s2f(slice); 507 | } else { 508 | // must be 4, read as int32 509 | value = s2i(slice); 510 | } 511 | offset += size; 512 | return value; 513 | }); 514 | }; 515 | 516 | await moduleRunTest( 517 | ` 518 | let test = fn () (int, float, bool) { 519 | (1, 1.5, true) 520 | } 521 | `, 522 | [1, 1.5, 1], 523 | tuple(4, 8, 4), 524 | ); 525 | 526 | await moduleRunTest( 527 | ` 528 | let test = fn () () { 529 | () 530 | } 531 | `, 532 | 0, 533 | ); 534 | 535 | await moduleRunTest( 536 | ` 537 | let f1 = fn () () { 538 | () 539 | } 540 | 541 | let f2 = fn (x bool) bool { 542 | !x 543 | } 544 | 545 | let test = fn () ((), bool) { 546 | (); 547 | (); 548 | (1, 2); 549 | (f1(), f2(false)) 550 | } 551 | `, 552 | [0, 1], 553 | tuple(4, 4), 554 | ); 555 | 556 | await moduleRunTest( 557 | ` 558 | let test = fn () float { 559 | (1, 1.5, true)[1] 560 | } 561 | `, 562 | 1.5, 563 | ); 564 | 565 | await moduleRunTest( 566 | ` 567 | let test = fn () int { 568 | int((1, 3.5, true)[1]) 569 | } 570 | `, 571 | 3, 572 | ); 573 | 574 | await moduleRunTest( 575 | ` 576 | let test = fn () int { 577 | let x = (1, (2, (3, 4), 5), 6, (7, 8)); 578 | x[1][1][1] 579 | } 580 | `, 581 | 4, 582 | ); 583 | 584 | await moduleRunTest( 585 | ` 586 | let test = fn () (float, float) { 587 | let x = (1, 1.5, (2.5, false, (), 0), 3.5); 588 | let y = (2.5, false); 589 | (x[2][0] - float(x[0]), y[0] + float(x[2][3])) 590 | } 591 | `, 592 | [1.5, 2.5], 593 | tuple(8, 8), 594 | ); 595 | 596 | await moduleRunTest( 597 | ` 598 | let test = fn () int { 599 | let x = 1234.; 600 | int(x) 601 | } 602 | `, 603 | 1234, 604 | ); 605 | 606 | await moduleRunTest( 607 | ` 608 | let test = fn () float { 609 | let x = 1234; 610 | float(x) 611 | } 612 | `, 613 | 1234, 614 | ); 615 | 616 | await moduleRunTest( 617 | ` 618 | let test = fn () int { 619 | let x = 1; 620 | x = x + 10; 621 | let y = 2; 622 | x = x + 2 * y; 623 | x 624 | } 625 | `, 626 | 15, 627 | ); 628 | 629 | await moduleRunTest( 630 | ` 631 | let g = 10 632 | 633 | let test = fn () int { 634 | let x = 1; 635 | x = x + 10; 636 | let y = 2; 637 | x = x + 2 * y; 638 | g = x + g; 639 | g 640 | } 641 | `, 642 | 25, 643 | ); 644 | 645 | await moduleRunTest( 646 | ` 647 | let g = (10, 20., 30) 648 | 649 | let test = fn () int { 650 | g[0] = 0; 651 | g[1] = float(g[0]) + float(g[2]); 652 | int(g[1]) + g[2] 653 | } 654 | `, 655 | 60, 656 | ); 657 | 658 | const array = (size: number) => ({ value, memory }: WASMResult) => { 659 | let offset = value; 660 | 661 | const len = s2i(memory.buffer.slice(offset, offset + 4)); 662 | if (size === 8) { 663 | // must be float 664 | return Array.from( 665 | new Float64Array(memory.buffer.slice(offset + 4, offset + 4 + 8 * len)), 666 | ); 667 | } else { 668 | // must be 4, read as int32 669 | return Array.from( 670 | new Int32Array(memory.buffer.slice(offset + 4, offset + 4 + 4 * len)), 671 | ); 672 | } 673 | }; 674 | 675 | await moduleRunTest( 676 | ` 677 | let test = fn () [int] { 678 | [1, 2, 3] 679 | } 680 | `, 681 | [1, 2, 3], 682 | array(4), 683 | ); 684 | 685 | await moduleRunTest( 686 | ` 687 | let test = fn () [bool] { 688 | [true, false, true, true] 689 | } 690 | `, 691 | [1, 0, 1, 1], 692 | array(4), 693 | ); 694 | 695 | await moduleRunTest( 696 | ` 697 | let test = fn () [float] { 698 | [1.5, 2.4, 3.3, 4.2, 5.1] 699 | } 700 | `, 701 | [1.5, 2.4, 3.3, 4.2, 5.1], 702 | array(8), 703 | ); 704 | 705 | await moduleRunTest( 706 | ` 707 | let test = fn () float { 708 | [1.5, 2.4, 3.3, 4.2, 5.1][2] 709 | } 710 | `, 711 | 3.3, 712 | ); 713 | 714 | await moduleRunTest( 715 | ` 716 | let test = fn () float { 717 | [[1.5, 2.4], [3.3, 4.2, 5.1]][1][2] 718 | } 719 | `, 720 | 5.1, 721 | ); 722 | 723 | await moduleRunTest( 724 | ` 725 | let test = fn () float { 726 | let x = [1.5, 2.4, 3.3, 4.2, 5.1]; 727 | x[2] = x[1] + x[3]; 728 | x[2] 729 | } 730 | `, 731 | 6.6, 732 | ); 733 | 734 | await moduleRunTest( 735 | ` 736 | let test = fn () [float] { 737 | let x = [[1.5, 2.4], [3.3, 4.2, 5.1]]; 738 | x[1] = x[0]; 739 | x[1] 740 | } 741 | `, 742 | [1.5, 2.4], 743 | array(8), 744 | ); 745 | 746 | // new expr 747 | await moduleRunTest( 748 | ` 749 | let test = fn () [float] { 750 | let y = (1, 2, 3); 751 | let x = new float[y[2]]; 752 | x[0] = 1.5; 753 | x[1] = 2.5; 754 | x[2] = 3.5; 755 | x 756 | } 757 | `, 758 | [1.5, 2.5, 3.5], 759 | array(8), 760 | ); 761 | 762 | // len 763 | await moduleRunTest( 764 | ` 765 | let test = fn () int { 766 | len([true, false, true, false, false]) 767 | } 768 | `, 769 | 5, 770 | ); 771 | 772 | await moduleRunTest( 773 | ` 774 | let test = fn () int { 775 | let y = (1, 2, 3); 776 | let x = new float[y[2]]; 777 | len(x) 778 | } 779 | `, 780 | 3, 781 | ); 782 | 783 | // loop 784 | await moduleRunTest( 785 | ` 786 | let test = fn () int { 787 | let x = 0; 788 | while x < 100 { 789 | x = x + 1; 790 | }; 791 | x 792 | } 793 | `, 794 | 100, 795 | ); 796 | await moduleRunTest( 797 | ` 798 | let test = fn () int { 799 | let x = 0; 800 | while x < 100 { 801 | if (x >= 80) { 802 | break; 803 | } else { 804 | x = x + 1; 805 | } 806 | }; 807 | x 808 | } 809 | `, 810 | 80, 811 | ); 812 | await moduleRunTest( 813 | ` 814 | let test = fn () [float] { 815 | let y = (1, 2, 3); 816 | let x = new float[y[2]]; 817 | let i = 0; 818 | while i < len(x) { 819 | x[i] = float(i) + 0.5; 820 | i = i + 1; 821 | }; 822 | x 823 | } 824 | `, 825 | [0.5, 1.5, 2.5], 826 | array(8), 827 | ); 828 | await moduleRunTest( 829 | ` 830 | let test = fn () [int] { 831 | let res = new int[5]; 832 | let i = 1; 833 | while i <= len(res) { 834 | let j = 1; 835 | let sum = 0; 836 | while (j <= i) { 837 | sum = sum + j; 838 | j = j + 1; 839 | }; 840 | res[i - 1] = sum; 841 | i = i + 1; 842 | }; 843 | res 844 | } 845 | `, 846 | [1, 3, 6, 10, 15], 847 | array(4), 848 | ); 849 | 850 | await moduleRunTest( 851 | ` 852 | let test = fn () int { 853 | other() 854 | } 855 | 856 | let other = fn () int { 857 | 1234 858 | } 859 | `, 860 | 1234, 861 | ); 862 | 863 | console.log(chalk.green.bold('Passed!')); 864 | })(); 865 | -------------------------------------------------------------------------------- /test/desugarer.spec.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import * as a from '../src/parser/ast'; 3 | import { desugarBefore, desugarAfter } from '../src/desugarer'; 4 | 5 | console.log(chalk.bold('Running desugarer tests...')); 6 | 7 | function n(Cons: any, value: any): any { 8 | return new Cons(value, -1, -1); 9 | } 10 | 11 | function valueEqual(actual: any, expected: any): boolean { 12 | if (actual instanceof a.Node) { 13 | astEqual(actual, expected); 14 | return true; 15 | } else if (Array.isArray(actual) && Array.isArray(expected)) { 16 | return ( 17 | actual.length === expected.length && 18 | actual.every((el, idx) => valueEqual(el, expected[idx])) 19 | ); 20 | } else if (actual instanceof Object && expected instanceof Object) { 21 | return Object.entries(expected).every(([key, val]) => 22 | valueEqual(actual[key], val), 23 | ); 24 | } else { 25 | return actual === expected; 26 | } 27 | } 28 | 29 | function astEqual(actual: a.Node, expected?: a.Node) { 30 | if ( 31 | expected && 32 | actual.constructor === expected.constructor && 33 | valueEqual(actual.value, expected.value) 34 | ) { 35 | return; 36 | } 37 | 38 | throw new Error( 39 | `Expected ${ 40 | expected 41 | ? `${expected.constructor.name}(${JSON.stringify(expected.value)})` 42 | : 'undefined' 43 | }, found ${actual.constructor.name}(${JSON.stringify(actual.value)})`, 44 | ); 45 | } 46 | 47 | type DesugarPath = 'before' | 'after' | 'both'; 48 | 49 | function moduleDesugarTest( 50 | path: DesugarPath, 51 | description: string, 52 | input: a.Module, 53 | expected: a.Module, 54 | ) { 55 | let desugar: (input: a.Module) => a.Module; 56 | switch (path) { 57 | case 'before': 58 | desugar = desugarBefore; 59 | break; 60 | case 'after': 61 | desugar = desugarAfter; 62 | break; 63 | case 'both': 64 | desugar = x => desugarAfter(desugarBefore(x)); 65 | break; 66 | default: 67 | throw new Error('unreachable'); 68 | } 69 | 70 | try { 71 | astEqual(desugar(input), expected); 72 | } catch (err) { 73 | console.error(chalk.blue.bold('Test:')); 74 | console.error(description); 75 | console.error(); 76 | console.error(chalk.red.bold('Error:')); 77 | console.error(err); 78 | process.exit(1); 79 | } 80 | } 81 | 82 | moduleDesugarTest( 83 | 'both', 84 | 'No desugar', 85 | n(a.Module, { 86 | imports: [], 87 | decls: [], 88 | }), 89 | n(a.Module, { 90 | imports: [], 91 | decls: [], 92 | }), 93 | ); 94 | 95 | function declDesugarTest( 96 | path: DesugarPath, 97 | description: string, 98 | input: a.Decl, 99 | expected: a.Decl, 100 | ) { 101 | moduleDesugarTest( 102 | path, 103 | description, 104 | n(a.Module, { 105 | imports: [], 106 | decls: [input], 107 | }), 108 | n(a.Module, { 109 | imports: [], 110 | decls: [expected], 111 | }), 112 | ); 113 | } 114 | 115 | declDesugarTest( 116 | 'both', 117 | 'No desugar', 118 | n(a.Decl, { 119 | name: n(a.Ident, 'x'), 120 | type: null, 121 | expr: n(a.LitExpr, n(a.IntLit, '1')), 122 | }), 123 | n(a.Decl, { 124 | name: n(a.Ident, 'x'), 125 | type: null, 126 | expr: n(a.LitExpr, n(a.IntLit, '1')), 127 | }), 128 | ); 129 | 130 | function exprDesugarTest( 131 | path: DesugarPath, 132 | description: string, 133 | input: a.Expr, 134 | expected: a.Expr, 135 | ) { 136 | declDesugarTest( 137 | path, 138 | description, 139 | n(a.Decl, { 140 | name: n(a.Ident, 'x'), 141 | type: null, 142 | expr: input, 143 | }), 144 | n(a.Decl, { 145 | name: n(a.Ident, 'x'), 146 | type: null, 147 | expr: expected, 148 | }), 149 | ); 150 | } 151 | 152 | exprDesugarTest( 153 | 'both', 154 | 'No desugar', 155 | n(a.LitExpr, n(a.IntLit, '1')), 156 | n(a.LitExpr, n(a.IntLit, '1')), 157 | ); 158 | 159 | exprDesugarTest( 160 | 'before', 161 | 'Unwrap 1-tuple', 162 | n(a.TupleExpr, { size: 1, items: [n(a.LitExpr, n(a.IntLit, '123'))] }), 163 | n(a.LitExpr, n(a.IntLit, '123')), 164 | ); 165 | 166 | exprDesugarTest( 167 | 'before', 168 | 'Unwrap 1-tuple multiple times', 169 | n(a.TupleExpr, { 170 | size: 1, 171 | items: [ 172 | n(a.TupleExpr, { 173 | size: 1, 174 | items: [ 175 | n(a.TupleExpr, { 176 | size: 1, 177 | items: [n(a.LitExpr, n(a.IntLit, '123'))], 178 | }), 179 | ], 180 | }), 181 | ], 182 | }), 183 | n(a.LitExpr, n(a.IntLit, '123')), 184 | ); 185 | 186 | exprDesugarTest( 187 | 'before', 188 | 'Unwrap 1-tuple with binary operator 1', 189 | n(a.BinaryExpr, { 190 | op: n(a.MulOp, '*'), 191 | left: n(a.LitExpr, n(a.IntLit, '1')), 192 | right: n(a.TupleExpr, { 193 | size: 1, 194 | items: [ 195 | n(a.BinaryExpr, { 196 | op: n(a.AddOp, '+'), 197 | left: n(a.LitExpr, n(a.IntLit, '2')), 198 | right: n(a.LitExpr, n(a.IntLit, '3')), 199 | }), 200 | ], 201 | }), 202 | }), 203 | n(a.BinaryExpr, { 204 | op: n(a.MulOp, '*'), 205 | left: n(a.LitExpr, n(a.IntLit, '1')), 206 | right: n(a.BinaryExpr, { 207 | op: n(a.AddOp, '+'), 208 | left: n(a.LitExpr, n(a.IntLit, '2')), 209 | right: n(a.LitExpr, n(a.IntLit, '3')), 210 | }), 211 | }), 212 | ); 213 | 214 | exprDesugarTest( 215 | 'before', 216 | 'Unwrap 1-tuple with binary operator 2', 217 | n(a.BinaryExpr, { 218 | op: n(a.AddOp, '+'), 219 | left: n(a.LitExpr, n(a.IntLit, '1')), 220 | right: n(a.TupleExpr, { 221 | size: 1, 222 | items: [ 223 | n(a.BinaryExpr, { 224 | op: n(a.MulOp, '*'), 225 | left: n(a.LitExpr, n(a.IntLit, '2')), 226 | right: n(a.LitExpr, n(a.IntLit, '3')), 227 | }), 228 | ], 229 | }), 230 | }), 231 | n(a.BinaryExpr, { 232 | op: n(a.AddOp, '+'), 233 | left: n(a.LitExpr, n(a.IntLit, '1')), 234 | right: n(a.BinaryExpr, { 235 | op: n(a.MulOp, '*'), 236 | left: n(a.LitExpr, n(a.IntLit, '2')), 237 | right: n(a.LitExpr, n(a.IntLit, '3')), 238 | }), 239 | }), 240 | ); 241 | 242 | exprDesugarTest( 243 | 'after', 244 | 'Unwrap unary + operator', 245 | n(a.UnaryExpr, { 246 | op: n(a.UnaryOp, '+'), 247 | right: n(a.LitExpr, n(a.IntLit, '1')), 248 | }), 249 | n(a.LitExpr, n(a.IntLit, '1')), 250 | ); 251 | exprDesugarTest( 252 | 'after', 253 | 'Unwrap multiple + operator', 254 | n(a.UnaryExpr, { 255 | op: n(a.UnaryOp, '+'), 256 | right: n(a.UnaryExpr, { 257 | op: n(a.UnaryOp, '-'), 258 | right: n(a.UnaryExpr, { 259 | op: n(a.UnaryOp, '+'), 260 | right: n(a.UnaryExpr, { 261 | op: n(a.UnaryOp, '+'), 262 | right: n(a.UnaryExpr, { 263 | op: n(a.UnaryOp, '-'), 264 | right: n(a.LitExpr, n(a.IntLit, '1')), 265 | }), 266 | }), 267 | }), 268 | }), 269 | }), 270 | n(a.UnaryExpr, { 271 | op: n(a.UnaryOp, '-'), 272 | right: n(a.UnaryExpr, { 273 | op: n(a.UnaryOp, '-'), 274 | right: n(a.LitExpr, n(a.IntLit, '1')), 275 | }), 276 | }), 277 | ); 278 | 279 | function typeDesugarTest( 280 | path: DesugarPath, 281 | description: string, 282 | input: a.Type, 283 | expected: a.Type, 284 | ) { 285 | declDesugarTest( 286 | path, 287 | description, 288 | n(a.Decl, { 289 | name: n(a.Ident, 'x'), 290 | type: input, 291 | expr: n(a.Ident, 'y'), 292 | }), 293 | n(a.Decl, { 294 | name: n(a.Ident, 'x'), 295 | type: expected, 296 | expr: n(a.Ident, 'y'), 297 | }), 298 | ); 299 | } 300 | 301 | typeDesugarTest('before', 'No desugar', n(a.IntType, null), n(a.IntType, null)); 302 | 303 | typeDesugarTest( 304 | 'before', 305 | 'Unwrap 1-tuple type', 306 | n(a.TupleType, { size: 1, items: [n(a.IntType, null)] }), 307 | n(a.IntType, null), 308 | ); 309 | 310 | typeDesugarTest( 311 | 'before', 312 | 'Unwrap 1-tuple type multiple times', 313 | n(a.TupleType, { 314 | size: 1, 315 | items: [ 316 | n(a.TupleType, { 317 | size: 1, 318 | items: [ 319 | n(a.TupleType, { 320 | size: 1, 321 | items: [n(a.IntType, null)], 322 | }), 323 | ], 324 | }), 325 | ], 326 | }), 327 | n(a.IntType, null), 328 | ); 329 | 330 | declDesugarTest( 331 | 'both', 332 | 'Complex case', 333 | n(a.Decl, { 334 | name: n(a.Ident, 'x'), 335 | type: n(a.FuncType, { 336 | param: n(a.TupleType, { 337 | size: 3, 338 | items: [ 339 | n(a.StrType, null), 340 | n( 341 | a.ArrayType, 342 | n(a.TupleType, { 343 | size: 1, 344 | items: [ 345 | n(a.TupleType, { 346 | size: 1, 347 | items: [n(a.BoolType, null)], 348 | }), 349 | ], 350 | }), 351 | ), 352 | n(a.FuncType, { 353 | param: n(a.TupleType, { 354 | size: 1, 355 | items: [n(a.CharType, null)], 356 | }), 357 | return: n(a.StrType, null), 358 | }), 359 | ], 360 | }), 361 | return: n(a.FuncType, { 362 | param: n(a.IntType, null), 363 | return: n(a.TupleType, { 364 | size: 2, 365 | items: [ 366 | n(a.TupleType, { size: 1, items: [n(a.IntType, null)] }), 367 | n(a.TupleType, { size: 1, items: [n(a.IntType, null)] }), 368 | ], 369 | }), 370 | }), 371 | }), 372 | expr: n(a.FuncExpr, { 373 | params: { 374 | size: 2, 375 | items: [ 376 | { 377 | name: n(a.Ident, 'x'), 378 | type: n(a.TupleType, { size: 1, items: [n(a.IntType, null)] }), 379 | }, 380 | { name: n(a.Ident, 'y'), type: n(a.IntType, null) }, 381 | ], 382 | }, 383 | returnType: n(a.TupleType, { 384 | size: 1, 385 | items: [ 386 | n(a.TupleType, { 387 | size: 1, 388 | items: [n(a.TupleType, { size: 1, items: [n(a.IntType, null)] })], 389 | }), 390 | ], 391 | }), 392 | body: n(a.Block, { 393 | bodies: [ 394 | n(a.BinaryExpr, { 395 | op: n(a.MulOp, '*'), 396 | left: n(a.UnaryExpr, { 397 | op: n(a.UnaryOp, '+'), 398 | right: n(a.LitExpr, n(a.IntLit, '1')), 399 | }), 400 | right: n(a.TupleExpr, { 401 | size: 1, 402 | items: [ 403 | n(a.BinaryExpr, { 404 | op: n(a.AddOp, '+'), 405 | left: n(a.IdentExpr, n(a.Ident, 'x')), 406 | right: n(a.IdentExpr, n(a.Ident, 'y')), 407 | }), 408 | ], 409 | }), 410 | }), 411 | ], 412 | returnVoid: false, 413 | }), 414 | }), 415 | }), 416 | n(a.Decl, { 417 | name: n(a.Ident, 'x'), 418 | type: n(a.FuncType, { 419 | param: n(a.TupleType, { 420 | size: 3, 421 | items: [ 422 | n(a.StrType, null), 423 | n(a.ArrayType, n(a.BoolType, null)), 424 | n(a.FuncType, { 425 | param: n(a.CharType, null), 426 | return: n(a.StrType, null), 427 | }), 428 | ], 429 | }), 430 | return: n(a.FuncType, { 431 | param: n(a.IntType, null), 432 | return: n(a.TupleType, { 433 | size: 2, 434 | items: [n(a.IntType, null), n(a.IntType, null)], 435 | }), 436 | }), 437 | }), 438 | expr: n(a.FuncExpr, { 439 | params: { 440 | size: 2, 441 | items: [ 442 | { 443 | name: n(a.Ident, 'x'), 444 | type: n(a.IntType, null), 445 | }, 446 | { name: n(a.Ident, 'y'), type: n(a.IntType, null) }, 447 | ], 448 | }, 449 | returnType: n(a.IntType, null), 450 | body: n(a.Block, { 451 | bodies: [ 452 | n(a.BinaryExpr, { 453 | op: n(a.MulOp, '*'), 454 | left: n(a.LitExpr, n(a.IntLit, '1')), 455 | right: n(a.BinaryExpr, { 456 | op: n(a.AddOp, '+'), 457 | left: n(a.IdentExpr, n(a.Ident, 'x')), 458 | right: n(a.IdentExpr, n(a.Ident, 'y')), 459 | }), 460 | }), 461 | ], 462 | returnVoid: false, 463 | }), 464 | }), 465 | }), 466 | ); 467 | 468 | console.log(chalk.green.bold('Passed!')); 469 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import './lexer.spec'; 2 | import './parser.spec'; 3 | import './desugarer.spec'; 4 | import './typechecker.spec'; 5 | import './codegen.spec'; 6 | -------------------------------------------------------------------------------- /test/lexer.spec.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { tokenize } from '../src/lexer'; 3 | import * as t from '../src/lexer/token'; 4 | 5 | console.log(chalk.bold('Running lexer tests...')); 6 | 7 | type TokenExpectation = t.Token> = [ 8 | t.TokenConstructor, Tk>, 9 | t.RepType, 10 | number | undefined, 11 | number | undefined 12 | ]; 13 | 14 | const exp = >( 15 | Cons: t.TokenConstructor, Tk>, 16 | rep: t.RepType, 17 | row?: number, 18 | column?: number, 19 | ): TokenExpectation => [Cons, rep, row, column]; 20 | 21 | function tokenEqual>( 22 | token: Tk, 23 | expected?: TokenExpectation, 24 | ) { 25 | if (!expected) { 26 | throw new Error( 27 | `Expected undefined, found ${token.constructor['name']}(${token.row}, ${ 28 | token.column 29 | }, ${token.rep})`, 30 | ); 31 | } 32 | 33 | let [Con, rep, row, column] = expected; 34 | 35 | if ( 36 | expected && 37 | token instanceof Con && 38 | token.rep === rep && 39 | (typeof row === 'undefined' || token.row === row) && 40 | (typeof column === 'undefined' || token.column === column) 41 | ) { 42 | // success 43 | return; 44 | } 45 | 46 | throw new Error( 47 | `Expected ${Con['name']}(${row}, ${column}, ${rep}), found ${ 48 | token.constructor['name'] 49 | }(${token.row}, ${token.column}, ${token.rep})`, 50 | ); 51 | } 52 | 53 | function tokenizeTest(input: string, expectations: Array) { 54 | expectations.push(exp(t.EOF, null)); 55 | try { 56 | for (const token of tokenize(input)) { 57 | tokenEqual(token, expectations.shift()); 58 | } 59 | } catch (err) { 60 | console.error(chalk.blue.bold('Source:')); 61 | console.error(input); 62 | console.error(); 63 | console.error(chalk.red.bold('Error:')); 64 | console.error(err); 65 | process.exit(1); 66 | } 67 | } 68 | 69 | // single token test 70 | tokenizeTest('->', [exp(t.Punctuation, '->', 1, 1)]); 71 | tokenizeTest(',', [exp(t.Punctuation, ',', 1, 1)]); 72 | tokenizeTest('(', [exp(t.Punctuation, '(', 1, 1)]); 73 | tokenizeTest(')', [exp(t.Punctuation, ')', 1, 1)]); 74 | tokenizeTest('[', [exp(t.Punctuation, '[', 1, 1)]); 75 | tokenizeTest(']', [exp(t.Punctuation, ']', 1, 1)]); 76 | tokenizeTest('{', [exp(t.Punctuation, '{', 1, 1)]); 77 | tokenizeTest('}', [exp(t.Punctuation, '}', 1, 1)]); 78 | tokenizeTest(':', [exp(t.Punctuation, ':', 1, 1)]); 79 | tokenizeTest('=', [exp(t.Punctuation, '=', 1, 1)]); 80 | tokenizeTest(';', [exp(t.Punctuation, ';', 1, 1)]); 81 | 82 | tokenizeTest('+', [exp(t.Operator, '+', 1, 1)]); 83 | tokenizeTest('-', [exp(t.Operator, '-', 1, 1)]); 84 | tokenizeTest('!', [exp(t.Operator, '!', 1, 1)]); 85 | tokenizeTest('==', [exp(t.Operator, '==', 1, 1)]); 86 | tokenizeTest('!=', [exp(t.Operator, '!=', 1, 1)]); 87 | tokenizeTest('<', [exp(t.Operator, '<', 1, 1)]); 88 | tokenizeTest('<=', [exp(t.Operator, '<=', 1, 1)]); 89 | tokenizeTest('>', [exp(t.Operator, '>', 1, 1)]); 90 | tokenizeTest('>=', [exp(t.Operator, '>=', 1, 1)]); 91 | tokenizeTest('|', [exp(t.Operator, '|', 1, 1)]); 92 | tokenizeTest('^', [exp(t.Operator, '^', 1, 1)]); 93 | tokenizeTest('*', [exp(t.Operator, '*', 1, 1)]); 94 | tokenizeTest('/', [exp(t.Operator, '/', 1, 1)]); 95 | tokenizeTest('%', [exp(t.Operator, '%', 1, 1)]); 96 | tokenizeTest('&', [exp(t.Operator, '&', 1, 1)]); 97 | tokenizeTest('||', [exp(t.Operator, '||', 1, 1)]); 98 | tokenizeTest('&&', [exp(t.Operator, '&&', 1, 1)]); 99 | 100 | tokenizeTest('1', [exp(t.IntLit, '1', 1, 1)]); 101 | tokenizeTest('123', [exp(t.IntLit, '123', 1, 1)]); 102 | tokenizeTest('1234567890', [exp(t.IntLit, '1234567890', 1, 1)]); 103 | tokenizeTest('1010101010', [exp(t.IntLit, '1010101010', 1, 1)]); 104 | tokenizeTest('01234', [exp(t.IntLit, '01234', 1, 1)]); 105 | tokenizeTest('00000', [exp(t.IntLit, '00000', 1, 1)]); 106 | tokenizeTest('00100', [exp(t.IntLit, '00100', 1, 1)]); 107 | 108 | tokenizeTest('1.123', [exp(t.FloatLit, '1.123', 1, 1)]); 109 | tokenizeTest('123.1', [exp(t.FloatLit, '123.1', 1, 1)]); 110 | tokenizeTest('01234.1234', [exp(t.FloatLit, '01234.1234', 1, 1)]); 111 | tokenizeTest('00000.00000', [exp(t.FloatLit, '00000.00000', 1, 1)]); 112 | tokenizeTest('00100.10000', [exp(t.FloatLit, '00100.10000', 1, 1)]); 113 | tokenizeTest('.1234', [exp(t.FloatLit, '.1234', 1, 1)]); 114 | tokenizeTest('.00000', [exp(t.FloatLit, '.00000', 1, 1)]); 115 | tokenizeTest('.10000', [exp(t.FloatLit, '.10000', 1, 1)]); 116 | 117 | tokenizeTest('true', [exp(t.BoolLit, 'true', 1, 1)]); 118 | tokenizeTest('false', [exp(t.BoolLit, 'false', 1, 1)]); 119 | 120 | tokenizeTest('import', [exp(t.Keyword, 'import', 1, 1)]); 121 | tokenizeTest('as', [exp(t.Keyword, 'as', 1, 1)]); 122 | tokenizeTest('let', [exp(t.Keyword, 'let', 1, 1)]); 123 | tokenizeTest('fn', [exp(t.Keyword, 'fn', 1, 1)]); 124 | tokenizeTest('if', [exp(t.Keyword, 'if', 1, 1)]); 125 | tokenizeTest('else', [exp(t.Keyword, 'else', 1, 1)]); 126 | tokenizeTest('for', [exp(t.Keyword, 'for', 1, 1)]); 127 | tokenizeTest('in', [exp(t.Keyword, 'in', 1, 1)]); 128 | tokenizeTest('new', [exp(t.Keyword, 'new', 1, 1)]); 129 | tokenizeTest('while', [exp(t.Keyword, 'while', 1, 1)]); 130 | tokenizeTest('break', [exp(t.Keyword, 'break', 1, 1)]); 131 | 132 | tokenizeTest('hello', [exp(t.Ident, 'hello', 1, 1)]); 133 | tokenizeTest('hello1', [exp(t.Ident, 'hello1', 1, 1)]); 134 | tokenizeTest('_hello', [exp(t.Ident, '_hello', 1, 1)]); 135 | tokenizeTest('_1he2ll3o', [exp(t.Ident, '_1he2ll3o', 1, 1)]); 136 | tokenizeTest('___', [exp(t.Ident, '___', 1, 1)]); 137 | 138 | tokenizeTest("'a'", [exp(t.CharLit, "'a'", 1, 1)]); 139 | tokenizeTest("'1'", [exp(t.CharLit, "'1'", 1, 1)]); 140 | tokenizeTest("'*'", [exp(t.CharLit, "'*'", 1, 1)]); 141 | tokenizeTest("'\\n'", [exp(t.CharLit, "'\\n'", 1, 1)]); 142 | tokenizeTest("'\\r'", [exp(t.CharLit, "'\\r'", 1, 1)]); 143 | tokenizeTest("'\\t'", [exp(t.CharLit, "'\\t'", 1, 1)]); 144 | tokenizeTest("'\\\\'", [exp(t.CharLit, "'\\\\'", 1, 1)]); 145 | tokenizeTest("'\\\"'", [exp(t.CharLit, "'\\\"'", 1, 1)]); 146 | tokenizeTest("'\\''", [exp(t.CharLit, "'\\''", 1, 1)]); 147 | 148 | tokenizeTest('"hello, world 123!"', [ 149 | exp(t.StrLit, '"hello, world 123!"', 1, 1), 150 | ]); 151 | tokenizeTest('"!@#$\'%^&*()"', [exp(t.StrLit, '"!@#$\'%^&*()"', 1, 1)]); 152 | tokenizeTest('"hello,\\nworld!"', [exp(t.StrLit, '"hello,\\nworld!"', 1, 1)]); 153 | tokenizeTest('"hello,\\rworld!"', [exp(t.StrLit, '"hello,\\rworld!"', 1, 1)]); 154 | tokenizeTest('"hello,\\tworld!"', [exp(t.StrLit, '"hello,\\tworld!"', 1, 1)]); 155 | tokenizeTest('"hello,\\\\world!"', [exp(t.StrLit, '"hello,\\\\world!"', 1, 1)]); 156 | tokenizeTest('"hello,\\"world!"', [exp(t.StrLit, '"hello,\\"world!"', 1, 1)]); 157 | tokenizeTest('"hello,\\\'rworld!"', [ 158 | exp(t.StrLit, '"hello,\\\'rworld!"', 1, 1), 159 | ]); 160 | 161 | // token position test 162 | tokenizeTest('123hello', [ 163 | exp(t.IntLit, '123', 1, 1), 164 | exp(t.Ident, 'hello', 1, 4), 165 | ]); 166 | tokenizeTest('123 hello', [ 167 | exp(t.IntLit, '123', 1, 1), 168 | exp(t.Ident, 'hello', 1, 9), 169 | ]); 170 | tokenizeTest('123\n\nhello', [ 171 | exp(t.IntLit, '123', 1, 1), 172 | exp(t.Ident, 'hello', 3, 1), 173 | ]); 174 | tokenizeTest('123\n\n hello', [ 175 | exp(t.IntLit, '123', 1, 1), 176 | exp(t.Ident, 'hello', 3, 4), 177 | ]); 178 | 179 | // empty program test 180 | tokenizeTest('', []); 181 | 182 | // actual program test 183 | tokenizeTest( 184 | ` 185 | import "/some/x.kou" (x) 186 | import "/some/xy.kou" (x, y) 187 | import "/some/as.kou" (orig_one as new_one) 188 | import "/some/asas.kou" (orig_one2 as new_one2, orig3 as new3) 189 | 190 | let main: () -> void = fn () void { 191 | let x: (int, string, char) = (123, "hello, world", '\\n'); 192 | let y: [float] = [1.3, .0, 0.4, .12345]; 193 | if fst(x) == 123 { 194 | println(add(1, 2)); 195 | } else { 196 | let z = y[1] - .1; 197 | } 198 | } 199 | 200 | let add = fn (x: int, y: int) int { x + y } 201 | `, 202 | [ 203 | exp(t.Keyword, 'import'), 204 | exp(t.StrLit, '"/some/x.kou"'), 205 | exp(t.Punctuation, '('), 206 | exp(t.Ident, 'x'), 207 | exp(t.Punctuation, ')'), 208 | exp(t.Keyword, 'import'), 209 | exp(t.StrLit, '"/some/xy.kou"'), 210 | exp(t.Punctuation, '('), 211 | exp(t.Ident, 'x'), 212 | exp(t.Punctuation, ','), 213 | exp(t.Ident, 'y'), 214 | exp(t.Punctuation, ')'), 215 | exp(t.Keyword, 'import'), 216 | exp(t.StrLit, '"/some/as.kou"'), 217 | exp(t.Punctuation, '('), 218 | exp(t.Ident, 'orig_one'), 219 | exp(t.Keyword, 'as'), 220 | exp(t.Ident, 'new_one'), 221 | exp(t.Punctuation, ')'), 222 | exp(t.Keyword, 'import'), 223 | exp(t.StrLit, '"/some/asas.kou"'), 224 | exp(t.Punctuation, '('), 225 | exp(t.Ident, 'orig_one2'), 226 | exp(t.Keyword, 'as'), 227 | exp(t.Ident, 'new_one2'), 228 | exp(t.Punctuation, ','), 229 | exp(t.Ident, 'orig3'), 230 | exp(t.Keyword, 'as'), 231 | exp(t.Ident, 'new3'), 232 | exp(t.Punctuation, ')'), 233 | exp(t.Keyword, 'let'), 234 | exp(t.Ident, 'main'), 235 | exp(t.Punctuation, ':'), 236 | exp(t.Punctuation, '('), 237 | exp(t.Punctuation, ')'), 238 | exp(t.Punctuation, '->'), 239 | exp(t.Ident, 'void'), 240 | exp(t.Punctuation, '='), 241 | exp(t.Keyword, 'fn'), 242 | exp(t.Punctuation, '('), 243 | exp(t.Punctuation, ')'), 244 | exp(t.Ident, 'void'), 245 | exp(t.Punctuation, '{'), 246 | exp(t.Keyword, 'let'), 247 | exp(t.Ident, 'x'), 248 | exp(t.Punctuation, ':'), 249 | exp(t.Punctuation, '('), 250 | exp(t.Ident, 'int'), 251 | exp(t.Punctuation, ','), 252 | exp(t.Ident, 'string'), 253 | exp(t.Punctuation, ','), 254 | exp(t.Ident, 'char'), 255 | exp(t.Punctuation, ')'), 256 | exp(t.Punctuation, '='), 257 | exp(t.Punctuation, '('), 258 | exp(t.IntLit, '123'), 259 | exp(t.Punctuation, ','), 260 | exp(t.StrLit, '"hello, world"'), 261 | exp(t.Punctuation, ','), 262 | exp(t.CharLit, "'\\n'"), 263 | exp(t.Punctuation, ')'), 264 | exp(t.Punctuation, ';'), 265 | exp(t.Keyword, 'let'), 266 | exp(t.Ident, 'y'), 267 | exp(t.Punctuation, ':'), 268 | exp(t.Punctuation, '['), 269 | exp(t.Ident, 'float'), 270 | exp(t.Punctuation, ']'), 271 | exp(t.Punctuation, '='), 272 | exp(t.Punctuation, '['), 273 | exp(t.FloatLit, '1.3'), 274 | exp(t.Punctuation, ','), 275 | exp(t.FloatLit, '.0'), 276 | exp(t.Punctuation, ','), 277 | exp(t.FloatLit, '0.4'), 278 | exp(t.Punctuation, ','), 279 | exp(t.FloatLit, '.12345'), 280 | exp(t.Punctuation, ']'), 281 | exp(t.Punctuation, ';'), 282 | exp(t.Keyword, 'if'), 283 | exp(t.Ident, 'fst'), 284 | exp(t.Punctuation, '('), 285 | exp(t.Ident, 'x'), 286 | exp(t.Punctuation, ')'), 287 | exp(t.Operator, '=='), 288 | exp(t.IntLit, '123'), 289 | exp(t.Punctuation, '{'), 290 | exp(t.Ident, 'println'), 291 | exp(t.Punctuation, '('), 292 | exp(t.Ident, 'add'), 293 | exp(t.Punctuation, '('), 294 | exp(t.IntLit, '1'), 295 | exp(t.Punctuation, ','), 296 | exp(t.IntLit, '2'), 297 | exp(t.Punctuation, ')'), 298 | exp(t.Punctuation, ')'), 299 | exp(t.Punctuation, ';'), 300 | exp(t.Punctuation, '}'), 301 | exp(t.Keyword, 'else'), 302 | exp(t.Punctuation, '{'), 303 | exp(t.Keyword, 'let'), 304 | exp(t.Ident, 'z'), 305 | exp(t.Punctuation, '='), 306 | exp(t.Ident, 'y'), 307 | exp(t.Punctuation, '['), 308 | exp(t.IntLit, '1'), 309 | exp(t.Punctuation, ']'), 310 | exp(t.Operator, '-'), 311 | exp(t.FloatLit, '.1'), 312 | exp(t.Punctuation, ';'), 313 | exp(t.Punctuation, '}'), 314 | exp(t.Punctuation, '}'), 315 | exp(t.Keyword, 'let'), 316 | exp(t.Ident, 'add'), 317 | exp(t.Punctuation, '='), 318 | exp(t.Keyword, 'fn'), 319 | exp(t.Punctuation, '('), 320 | exp(t.Ident, 'x'), 321 | exp(t.Punctuation, ':'), 322 | exp(t.Ident, 'int'), 323 | exp(t.Punctuation, ','), 324 | exp(t.Ident, 'y'), 325 | exp(t.Punctuation, ':'), 326 | exp(t.Ident, 'int'), 327 | exp(t.Punctuation, ')'), 328 | exp(t.Ident, 'int'), 329 | exp(t.Punctuation, '{'), 330 | exp(t.Ident, 'x'), 331 | exp(t.Operator, '+'), 332 | exp(t.Ident, 'y'), 333 | exp(t.Punctuation, '}'), 334 | ], 335 | ); 336 | 337 | console.log(chalk.green.bold('Passed!')); 338 | -------------------------------------------------------------------------------- /test/parser.spec.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { tokenize } from '../src/lexer'; 3 | import { parse } from '../src/parser'; 4 | import * as a from '../src/parser/ast'; 5 | import { Compose } from '../src/util'; 6 | 7 | console.log(chalk.bold('Running parser tests...')); 8 | 9 | type NodeExpectation = a.Node> = [ 10 | a.NodeConstructor, N>, 11 | any, 12 | number | undefined, 13 | number | undefined 14 | ]; 15 | 16 | const exp = >( 17 | Cons: a.NodeConstructor, N>, 18 | value: any = null, 19 | row?: number, 20 | column?: number, 21 | ): NodeExpectation => [Cons, value, row, column]; 22 | 23 | function valueEqual(actual: any, expected: any): boolean { 24 | if (actual instanceof a.Node) { 25 | astEqual(actual, expected); 26 | return true; 27 | } else if (Array.isArray(actual) && Array.isArray(expected)) { 28 | return ( 29 | actual.length === expected.length && 30 | actual.every((el, idx) => valueEqual(el, expected[idx])) 31 | ); 32 | } else if (actual instanceof Object && expected instanceof Object) { 33 | return Object.entries(expected).every(([key, val]) => 34 | valueEqual(actual[key], val), 35 | ); 36 | } else { 37 | return actual === expected; 38 | } 39 | } 40 | 41 | function astEqual(actual: a.Node, expected?: NodeExpectation) { 42 | if (!expected) { 43 | throw new Error( 44 | `Expected undefined, found ${actual.constructor['name']}(${actual.row}, ${ 45 | actual.column 46 | }, ${JSON.stringify(actual.value)})`, 47 | ); 48 | } 49 | 50 | let [Con, value, row, column] = expected; 51 | 52 | if ( 53 | expected && 54 | actual instanceof Con && 55 | (typeof row === 'undefined' || actual.row === row) && 56 | (typeof column === 'undefined' || actual.column === column) && 57 | valueEqual(actual.value, value) 58 | ) { 59 | return; 60 | } 61 | 62 | let expectedPos = ''; 63 | let actualPos = ''; 64 | if (row && column) { 65 | expectedPos = `${row}, ${column}, `; 66 | actualPos = `${actual.row}, ${actual.column}, `; 67 | } 68 | throw new Error( 69 | `Expected ${Con['name']}(${expectedPos}${JSON.stringify(value)}), found ${ 70 | actual.constructor['name'] 71 | }(${actualPos}${JSON.stringify(actual.value)})`, 72 | ); 73 | } 74 | 75 | const compile = Compose.then(tokenize).then(parse).f; 76 | 77 | function programTest( 78 | input: string, 79 | expected: any, 80 | sourceToShow: string = input, 81 | ) { 82 | try { 83 | astEqual(compile(input), exp(a.Module, expected)); 84 | } catch (err) { 85 | console.error(chalk.blue.bold('Source:')); 86 | console.error(sourceToShow); 87 | console.error(); 88 | console.error(chalk.red.bold('Error:')); 89 | console.error(err); 90 | process.exit(1); 91 | } 92 | } 93 | 94 | function importTest(source: string, expected: Array) { 95 | programTest(source, { imports: expected, decls: [] }); 96 | } 97 | 98 | function declTest(source: string, expected: Array) { 99 | programTest(source, { imports: [], decls: expected }); 100 | } 101 | 102 | function typeTest(source: string, expected: NodeExpectation) { 103 | programTest( 104 | `let x: ${source} = true`, 105 | { 106 | imports: [], 107 | decls: [ 108 | exp(a.Decl, { 109 | name: exp(a.Ident, 'x'), 110 | type: expected, 111 | expr: exp(a.LitExpr, exp(a.BoolLit, 'true')), 112 | }), 113 | ], 114 | }, 115 | source, 116 | ); 117 | } 118 | 119 | function exprTest(source: string, expected: NodeExpectation) { 120 | programTest( 121 | `let x = ${source}`, 122 | { 123 | imports: [], 124 | decls: [ 125 | exp(a.Decl, { 126 | name: exp(a.Ident, 'x'), 127 | type: null, 128 | expr: expected, 129 | }), 130 | ], 131 | }, 132 | source, 133 | ); 134 | } 135 | 136 | importTest('import "test.kou" (test_name)', [ 137 | exp(a.Import, { 138 | path: exp(a.StrLit, '"test.kou"'), 139 | elems: [ 140 | { 141 | name: exp(a.Ident, 'test_name'), 142 | as: null, 143 | }, 144 | ], 145 | }), 146 | ]); 147 | 148 | importTest('import "test.kou" (test_name as test_alias)', [ 149 | exp(a.Import, { 150 | path: exp(a.StrLit, '"test.kou"'), 151 | elems: [ 152 | { 153 | name: exp(a.Ident, 'test_name'), 154 | as: exp(a.Ident, 'test_alias'), 155 | }, 156 | ], 157 | }), 158 | ]); 159 | 160 | importTest('import "test.kou" (test_name as test_alias, hoge, foo as bar)', [ 161 | exp(a.Import, { 162 | path: exp(a.StrLit, '"test.kou"'), 163 | elems: [ 164 | { 165 | name: exp(a.Ident, 'test_name'), 166 | as: exp(a.Ident, 'test_alias'), 167 | }, 168 | { 169 | name: exp(a.Ident, 'hoge'), 170 | as: null, 171 | }, 172 | { 173 | name: exp(a.Ident, 'foo'), 174 | as: exp(a.Ident, 'bar'), 175 | }, 176 | ], 177 | }), 178 | ]); 179 | 180 | importTest( 181 | ` 182 | import "file1.kou" (test_name as test_alias) 183 | import "file2.kou" (test_name as test_alias, hoge, foo as bar) 184 | `, 185 | [ 186 | exp(a.Import, { 187 | path: exp(a.StrLit, '"file1.kou"'), 188 | elems: [ 189 | { 190 | name: exp(a.Ident, 'test_name'), 191 | as: exp(a.Ident, 'test_alias'), 192 | }, 193 | ], 194 | }), 195 | exp(a.Import, { 196 | path: exp(a.StrLit, '"file2.kou"'), 197 | elems: [ 198 | { 199 | name: exp(a.Ident, 'test_name'), 200 | as: exp(a.Ident, 'test_alias'), 201 | }, 202 | { 203 | name: exp(a.Ident, 'hoge'), 204 | as: null, 205 | }, 206 | { 207 | name: exp(a.Ident, 'foo'), 208 | as: exp(a.Ident, 'bar'), 209 | }, 210 | ], 211 | }), 212 | ], 213 | ); 214 | 215 | declTest( 216 | ` 217 | let simple = 10 218 | let typed: str = "hello, world" 219 | `, 220 | [ 221 | exp(a.Decl, { 222 | name: exp(a.Ident, 'simple'), 223 | type: null, 224 | expr: exp(a.LitExpr, exp(a.IntLit, '10')), 225 | }), 226 | exp(a.Decl, { 227 | name: exp(a.Ident, 'typed'), 228 | type: exp(a.StrType), 229 | expr: exp(a.LitExpr, exp(a.StrLit, '"hello, world"')), 230 | }), 231 | ], 232 | ); 233 | 234 | typeTest('int', exp(a.IntType)); 235 | typeTest('float', exp(a.FloatType)); 236 | typeTest('str', exp(a.StrType)); 237 | typeTest('bool', exp(a.BoolType)); 238 | typeTest('char', exp(a.CharType)); 239 | typeTest('void', exp(a.VoidType)); 240 | typeTest('[int]', exp(a.ArrayType, exp(a.IntType))); 241 | typeTest('[[str]]', exp(a.ArrayType, exp(a.ArrayType, exp(a.StrType)))); 242 | typeTest( 243 | '[[[bool]]]', 244 | exp(a.ArrayType, exp(a.ArrayType, exp(a.ArrayType, exp(a.BoolType)))), 245 | ); 246 | typeTest( 247 | '(int, float)', 248 | exp(a.TupleType, { 249 | size: 2, 250 | items: [exp(a.IntType), exp(a.FloatType)], 251 | }), 252 | ); 253 | typeTest( 254 | '(int, float, str)', 255 | exp(a.TupleType, { 256 | size: 3, 257 | items: [exp(a.IntType), exp(a.FloatType), exp(a.StrType)], 258 | }), 259 | ); 260 | typeTest( 261 | '[(int, str)]', 262 | exp( 263 | a.ArrayType, 264 | exp(a.TupleType, { 265 | size: 2, 266 | items: [exp(a.IntType), exp(a.StrType)], 267 | }), 268 | ), 269 | ); 270 | typeTest( 271 | '(int, [float], (str, bool, char))', 272 | exp(a.TupleType, { 273 | size: 3, 274 | items: [ 275 | exp(a.IntType), 276 | exp(a.ArrayType, exp(a.FloatType)), 277 | exp(a.TupleType, { 278 | size: 3, 279 | items: [exp(a.StrType), exp(a.BoolType), exp(a.CharType)], 280 | }), 281 | ], 282 | }), 283 | ); 284 | typeTest('(int)', exp(a.TupleType, { size: 1, items: [exp(a.IntType)] })); 285 | typeTest('()', exp(a.TupleType, { size: 0, items: [] })); 286 | typeTest( 287 | 'int -> bool', 288 | exp(a.FuncType, { param: exp(a.IntType), return: exp(a.BoolType) }), 289 | ); 290 | typeTest( 291 | '(str, char) -> (str, int, (char))', 292 | exp(a.FuncType, { 293 | param: exp(a.TupleType, { 294 | size: 2, 295 | items: [exp(a.StrType), exp(a.CharType)], 296 | }), 297 | return: exp(a.TupleType, { 298 | size: 3, 299 | items: [ 300 | exp(a.StrType), 301 | exp(a.IntType), 302 | exp(a.TupleType, { size: 1, items: [exp(a.CharType)] }), 303 | ], 304 | }), 305 | }), 306 | ); 307 | typeTest( 308 | 'bool -> [str] -> str', 309 | exp(a.FuncType, { 310 | param: exp(a.BoolType), 311 | return: exp(a.FuncType, { 312 | param: exp(a.ArrayType, exp(a.StrType)), 313 | return: exp(a.StrType), 314 | }), 315 | }), 316 | ); 317 | typeTest( 318 | 'str -> str -> str -> str', 319 | exp(a.FuncType, { 320 | param: exp(a.StrType), 321 | return: exp(a.FuncType, { 322 | param: exp(a.StrType), 323 | return: exp(a.FuncType, { 324 | param: exp(a.StrType), 325 | return: exp(a.StrType), 326 | }), 327 | }), 328 | }), 329 | ); 330 | typeTest( 331 | '() -> void', 332 | exp(a.FuncType, { 333 | param: exp(a.TupleType, { size: 0, items: [] }), 334 | return: exp(a.VoidType), 335 | }), 336 | ); 337 | 338 | exprTest('1234', exp(a.LitExpr, exp(a.IntLit, '1234'))); 339 | exprTest('1.234', exp(a.LitExpr, exp(a.FloatLit, '1.234'))); 340 | exprTest('"hello, world"', exp(a.LitExpr, exp(a.StrLit, '"hello, world"'))); 341 | exprTest('true', exp(a.LitExpr, exp(a.BoolLit, 'true'))); 342 | exprTest('false', exp(a.LitExpr, exp(a.BoolLit, 'false'))); 343 | exprTest("'c'", exp(a.LitExpr, exp(a.CharLit, "'c'"))); 344 | 345 | exprTest('some_var', exp(a.IdentExpr, exp(a.Ident, 'some_var'))); 346 | 347 | exprTest( 348 | '-1234', 349 | exp(a.UnaryExpr, { 350 | op: exp(a.UnaryOp, '-'), 351 | right: exp(a.LitExpr, exp(a.IntLit, '1234')), 352 | }), 353 | ); 354 | exprTest( 355 | '+1234', 356 | exp(a.UnaryExpr, { 357 | op: exp(a.UnaryOp, '+'), 358 | right: exp(a.LitExpr, exp(a.IntLit, '1234')), 359 | }), 360 | ); 361 | exprTest( 362 | '-+1234', 363 | exp(a.UnaryExpr, { 364 | op: exp(a.UnaryOp, '-'), 365 | right: exp(a.UnaryExpr, { 366 | op: exp(a.UnaryOp, '+'), 367 | right: exp(a.LitExpr, exp(a.IntLit, '1234')), 368 | }), 369 | }), 370 | ); 371 | exprTest( 372 | '!true', 373 | exp(a.UnaryExpr, { 374 | op: exp(a.UnaryOp, '!'), 375 | right: exp(a.LitExpr, exp(a.BoolLit, 'true')), 376 | }), 377 | ); 378 | exprTest( 379 | '!!!!false', 380 | exp(a.UnaryExpr, { 381 | op: exp(a.UnaryOp, '!'), 382 | right: exp(a.UnaryExpr, { 383 | op: exp(a.UnaryOp, '!'), 384 | right: exp(a.UnaryExpr, { 385 | op: exp(a.UnaryOp, '!'), 386 | right: exp(a.UnaryExpr, { 387 | op: exp(a.UnaryOp, '!'), 388 | right: exp(a.LitExpr, exp(a.BoolLit, 'false')), 389 | }), 390 | }), 391 | }), 392 | }), 393 | ); 394 | exprTest( 395 | '+some_int', 396 | exp(a.UnaryExpr, { 397 | op: exp(a.UnaryOp, '+'), 398 | right: exp(a.IdentExpr, exp(a.Ident, 'some_int')), 399 | }), 400 | ); 401 | 402 | exprTest( 403 | '1 + 2', 404 | exp(a.BinaryExpr, { 405 | op: exp(a.AddOp, '+'), 406 | left: exp(a.LitExpr, exp(a.IntLit, '1')), 407 | right: exp(a.LitExpr, exp(a.IntLit, '2')), 408 | }), 409 | ); 410 | exprTest( 411 | '1 + 2 * 3', 412 | exp(a.BinaryExpr, { 413 | op: exp(a.AddOp, '+'), 414 | left: exp(a.LitExpr, exp(a.IntLit, '1')), 415 | right: exp(a.BinaryExpr, { 416 | op: exp(a.MulOp, '*'), 417 | left: exp(a.LitExpr, exp(a.IntLit, '2')), 418 | right: exp(a.LitExpr, exp(a.IntLit, '3')), 419 | }), 420 | }), 421 | ); 422 | exprTest( 423 | '1 * 2 + 3', 424 | exp(a.BinaryExpr, { 425 | op: exp(a.AddOp, '+'), 426 | left: exp(a.BinaryExpr, { 427 | op: exp(a.MulOp, '*'), 428 | left: exp(a.LitExpr, exp(a.IntLit, '1')), 429 | right: exp(a.LitExpr, exp(a.IntLit, '2')), 430 | }), 431 | right: exp(a.LitExpr, exp(a.IntLit, '3')), 432 | }), 433 | ); 434 | exprTest( 435 | '(1 + 2) * 3', 436 | exp(a.BinaryExpr, { 437 | op: exp(a.MulOp, '*'), 438 | left: exp(a.TupleExpr, { 439 | size: 1, 440 | items: [ 441 | exp(a.BinaryExpr, { 442 | op: exp(a.AddOp, '+'), 443 | left: exp(a.LitExpr, exp(a.IntLit, '1')), 444 | right: exp(a.LitExpr, exp(a.IntLit, '2')), 445 | }), 446 | ], 447 | }), 448 | right: exp(a.LitExpr, exp(a.IntLit, '3')), 449 | }), 450 | ); 451 | exprTest( 452 | '1 * (2 + 3)', 453 | exp(a.BinaryExpr, { 454 | op: exp(a.MulOp, '*'), 455 | left: exp(a.LitExpr, exp(a.IntLit, '1')), 456 | right: exp(a.TupleExpr, { 457 | size: 1, 458 | items: [ 459 | exp(a.BinaryExpr, { 460 | op: exp(a.AddOp, '+'), 461 | left: exp(a.LitExpr, exp(a.IntLit, '2')), 462 | right: exp(a.LitExpr, exp(a.IntLit, '3')), 463 | }), 464 | ], 465 | }), 466 | }), 467 | ); 468 | exprTest( 469 | '(1 + 2 * 3 + 4 > 5) || (true && false == false || true)', 470 | exp(a.BinaryExpr, { 471 | op: exp(a.BoolOp, '||'), 472 | left: exp(a.TupleExpr, { 473 | size: 1, 474 | items: [ 475 | exp(a.BinaryExpr, { 476 | op: exp(a.CompOp, '>'), 477 | left: exp(a.BinaryExpr, { 478 | op: exp(a.AddOp, '+'), 479 | left: exp(a.BinaryExpr, { 480 | op: exp(a.AddOp, '+'), 481 | left: exp(a.LitExpr, exp(a.IntLit, '1')), 482 | right: exp(a.BinaryExpr, { 483 | op: exp(a.MulOp, '*'), 484 | left: exp(a.LitExpr, exp(a.IntLit, '2')), 485 | right: exp(a.LitExpr, exp(a.IntLit, '3')), 486 | }), 487 | }), 488 | right: exp(a.LitExpr, exp(a.IntLit, '4')), 489 | }), 490 | right: exp(a.LitExpr, exp(a.IntLit, '5')), 491 | }), 492 | ], 493 | }), 494 | right: exp(a.TupleExpr, { 495 | size: 1, 496 | items: [ 497 | exp(a.BinaryExpr, { 498 | op: exp(a.EqOp, '=='), 499 | left: exp(a.BinaryExpr, { 500 | op: exp(a.BoolOp, '&&'), 501 | left: exp(a.LitExpr, exp(a.BoolLit, 'true')), 502 | right: exp(a.LitExpr, exp(a.BoolLit, 'false')), 503 | }), 504 | right: exp(a.BinaryExpr, { 505 | op: exp(a.BoolOp, '||'), 506 | left: exp(a.LitExpr, exp(a.BoolLit, 'false')), 507 | right: exp(a.LitExpr, exp(a.BoolLit, 'true')), 508 | }), 509 | }), 510 | ], 511 | }), 512 | }), 513 | ); 514 | 515 | exprTest('()', exp(a.TupleExpr, { size: 0, items: [] })); 516 | exprTest( 517 | '(1)', 518 | exp(a.TupleExpr, { size: 1, items: [exp(a.LitExpr, exp(a.IntLit, '1'))] }), 519 | ); 520 | exprTest( 521 | '(-1234, !!x, ("hello", true))', 522 | exp(a.TupleExpr, { 523 | size: 3, 524 | items: [ 525 | exp(a.UnaryExpr, { 526 | op: exp(a.UnaryOp, '-'), 527 | right: exp(a.LitExpr, exp(a.IntLit, '1234')), 528 | }), 529 | exp(a.UnaryExpr, { 530 | op: exp(a.UnaryOp, '!'), 531 | right: exp(a.UnaryExpr, { 532 | op: exp(a.UnaryOp, '!'), 533 | right: exp(a.IdentExpr, exp(a.Ident, 'x')), 534 | }), 535 | }), 536 | exp(a.TupleExpr, { 537 | size: 2, 538 | items: [ 539 | exp(a.LitExpr, exp(a.StrLit, '"hello"')), 540 | exp(a.LitExpr, exp(a.BoolLit, 'true')), 541 | ], 542 | }), 543 | ], 544 | }), 545 | ); 546 | 547 | exprTest('[]', exp(a.ArrayExpr, [])); 548 | exprTest( 549 | '[1, 2, 3]', 550 | exp(a.ArrayExpr, [ 551 | exp(a.LitExpr, exp(a.IntLit, '1')), 552 | exp(a.LitExpr, exp(a.IntLit, '2')), 553 | exp(a.LitExpr, exp(a.IntLit, '3')), 554 | ]), 555 | ); 556 | exprTest( 557 | '[a, b, c]', 558 | exp(a.ArrayExpr, [ 559 | exp(a.IdentExpr, exp(a.Ident, 'a')), 560 | exp(a.IdentExpr, exp(a.Ident, 'b')), 561 | exp(a.IdentExpr, exp(a.Ident, 'c')), 562 | ]), 563 | ); 564 | 565 | exprTest( 566 | 'fn () void {}', 567 | exp(a.FuncExpr, { 568 | params: { 569 | size: 0, 570 | items: [], 571 | }, 572 | returnType: exp(a.VoidType), 573 | body: exp(a.Block, { 574 | bodies: [], 575 | returnVoid: true, 576 | }), 577 | }), 578 | ); 579 | exprTest( 580 | ` 581 | fn (x int, y int) int { 582 | let result = x + y; 583 | print(result); 584 | result 585 | } 586 | `, 587 | exp(a.FuncExpr, { 588 | params: { 589 | size: 2, 590 | items: [ 591 | { name: exp(a.Ident, 'x'), type: exp(a.IntType) }, 592 | { name: exp(a.Ident, 'y'), type: exp(a.IntType) }, 593 | ], 594 | }, 595 | returnType: exp(a.IntType), 596 | body: exp(a.Block, { 597 | bodies: [ 598 | exp(a.Decl, { 599 | name: exp(a.Ident, 'result'), 600 | type: null, 601 | expr: exp(a.BinaryExpr, { 602 | op: exp(a.AddOp, '+'), 603 | left: exp(a.IdentExpr, exp(a.Ident, 'x')), 604 | right: exp(a.IdentExpr, exp(a.Ident, 'y')), 605 | }), 606 | }), 607 | exp(a.CallExpr, { 608 | func: exp(a.IdentExpr, exp(a.Ident, 'print')), 609 | args: exp(a.TupleExpr, { 610 | size: 1, 611 | items: [exp(a.IdentExpr, exp(a.Ident, 'result'))], 612 | }), 613 | }), 614 | exp(a.IdentExpr, exp(a.Ident, 'result')), 615 | ], 616 | returnVoid: false, 617 | }), 618 | }), 619 | ); 620 | exprTest( 621 | ` 622 | fn (x int, y int) int { 623 | let result = x + y; 624 | print(result); 625 | result; 626 | } 627 | `, 628 | exp(a.FuncExpr, { 629 | params: { 630 | size: 2, 631 | items: [ 632 | { name: exp(a.Ident, 'x'), type: exp(a.IntType) }, 633 | { name: exp(a.Ident, 'y'), type: exp(a.IntType) }, 634 | ], 635 | }, 636 | returnType: exp(a.IntType), 637 | body: exp(a.Block, { 638 | bodies: [ 639 | exp(a.Decl, { 640 | name: exp(a.Ident, 'result'), 641 | type: null, 642 | expr: exp(a.BinaryExpr, { 643 | op: exp(a.AddOp, '+'), 644 | left: exp(a.IdentExpr, exp(a.Ident, 'x')), 645 | right: exp(a.IdentExpr, exp(a.Ident, 'y')), 646 | }), 647 | }), 648 | exp(a.CallExpr, { 649 | func: exp(a.IdentExpr, exp(a.Ident, 'print')), 650 | args: exp(a.TupleExpr, { 651 | size: 1, 652 | items: [exp(a.IdentExpr, exp(a.Ident, 'result'))], 653 | }), 654 | }), 655 | exp(a.IdentExpr, exp(a.Ident, 'result')), 656 | ], 657 | returnVoid: true, 658 | }), 659 | }), 660 | ); 661 | exprTest( 662 | 'fn (ignored [bool], negated int) int { -negated }', 663 | exp(a.FuncExpr, { 664 | params: { 665 | size: 2, 666 | items: [ 667 | { 668 | name: exp(a.Ident, 'ignored'), 669 | type: exp(a.ArrayType, exp(a.BoolType)), 670 | }, 671 | { name: exp(a.Ident, 'negated'), type: exp(a.IntType) }, 672 | ], 673 | }, 674 | returnType: exp(a.IntType), 675 | body: exp(a.Block, { 676 | bodies: [ 677 | exp(a.UnaryExpr, { 678 | op: exp(a.UnaryOp, '-'), 679 | right: exp(a.IdentExpr, exp(a.Ident, 'negated')), 680 | }), 681 | ], 682 | returnVoid: false, 683 | }), 684 | }), 685 | ); 686 | exprTest( 687 | ` 688 | fn (i int) () -> int { 689 | fn () int { i } 690 | }`, 691 | exp(a.FuncExpr, { 692 | params: { 693 | size: 1, 694 | items: [{ name: exp(a.Ident, 'i'), type: exp(a.IntType) }], 695 | }, 696 | returnType: exp(a.FuncType, { 697 | param: exp(a.TupleType, { 698 | size: 0, 699 | items: [], 700 | }), 701 | return: exp(a.IntType), 702 | }), 703 | body: exp(a.Block, { 704 | bodies: [ 705 | exp(a.FuncExpr, { 706 | params: { 707 | size: 0, 708 | items: [], 709 | }, 710 | returnType: exp(a.IntType), 711 | body: exp(a.Block, { 712 | bodies: [exp(a.IdentExpr, exp(a.Ident, 'i'))], 713 | returnVoid: false, 714 | }), 715 | }), 716 | ], 717 | returnVoid: false, 718 | }), 719 | }), 720 | ); 721 | 722 | exprTest( 723 | 'func()', 724 | exp(a.CallExpr, { 725 | func: exp(a.IdentExpr, exp(a.Ident, 'func')), 726 | args: exp(a.TupleExpr, { 727 | size: 0, 728 | items: [], 729 | }), 730 | }), 731 | ); 732 | exprTest( 733 | 'func2("hello", 1 + 2, true)', 734 | exp(a.CallExpr, { 735 | func: exp(a.IdentExpr, exp(a.Ident, 'func2')), 736 | args: exp(a.TupleExpr, { 737 | size: 3, 738 | items: [ 739 | exp(a.LitExpr, exp(a.StrLit, '"hello"')), 740 | exp(a.BinaryExpr, { 741 | op: exp(a.AddOp, '+'), 742 | left: exp(a.LitExpr, exp(a.IntLit, '1')), 743 | right: exp(a.LitExpr, exp(a.IntLit, '2')), 744 | }), 745 | exp(a.LitExpr, exp(a.BoolLit, 'true')), 746 | ], 747 | }), 748 | }), 749 | ); 750 | exprTest( 751 | 'func3("hello")(1 + 2, true)', 752 | exp(a.CallExpr, { 753 | func: exp(a.CallExpr, { 754 | func: exp(a.IdentExpr, exp(a.Ident, 'func3')), 755 | args: exp(a.TupleExpr, { 756 | size: 1, 757 | items: [exp(a.LitExpr, exp(a.StrLit, '"hello"'))], 758 | }), 759 | }), 760 | args: exp(a.TupleExpr, { 761 | size: 2, 762 | items: [ 763 | exp(a.BinaryExpr, { 764 | op: exp(a.AddOp, '+'), 765 | left: exp(a.LitExpr, exp(a.IntLit, '1')), 766 | right: exp(a.LitExpr, exp(a.IntLit, '2')), 767 | }), 768 | exp(a.LitExpr, exp(a.BoolLit, 'true')), 769 | ], 770 | }), 771 | }), 772 | ); 773 | exprTest( 774 | '(fn (x int, y int) int { x + y })(10, 20)', 775 | exp(a.CallExpr, { 776 | func: exp(a.TupleExpr, { 777 | size: 1, 778 | items: [ 779 | exp(a.FuncExpr, { 780 | params: { 781 | size: 2, 782 | items: [ 783 | { 784 | name: exp(a.Ident, 'x'), 785 | type: exp(a.IntType), 786 | }, 787 | { 788 | name: exp(a.Ident, 'y'), 789 | type: exp(a.IntType), 790 | }, 791 | ], 792 | }, 793 | returnType: exp(a.IntType), 794 | body: exp(a.Block, { 795 | bodies: [ 796 | exp(a.BinaryExpr, { 797 | op: exp(a.AddOp, '+'), 798 | left: exp(a.IdentExpr, exp(a.Ident, 'x')), 799 | right: exp(a.IdentExpr, exp(a.Ident, 'y')), 800 | }), 801 | ], 802 | returnVoid: false, 803 | }), 804 | }), 805 | ], 806 | }), 807 | args: exp(a.TupleExpr, { 808 | size: 2, 809 | items: [ 810 | exp(a.LitExpr, exp(a.IntLit, '10')), 811 | exp(a.LitExpr, exp(a.IntLit, '20')), 812 | ], 813 | }), 814 | }), 815 | ); 816 | exprTest( 817 | 'fn (x int, y int) int { x + y }(10, 20)', 818 | exp(a.CallExpr, { 819 | func: exp(a.FuncExpr, { 820 | params: { 821 | size: 2, 822 | items: [ 823 | { 824 | name: exp(a.Ident, 'x'), 825 | type: exp(a.IntType), 826 | }, 827 | { 828 | name: exp(a.Ident, 'y'), 829 | type: exp(a.IntType), 830 | }, 831 | ], 832 | }, 833 | returnType: exp(a.IntType), 834 | body: exp(a.Block, { 835 | bodies: [ 836 | exp(a.BinaryExpr, { 837 | op: exp(a.AddOp, '+'), 838 | left: exp(a.IdentExpr, exp(a.Ident, 'x')), 839 | right: exp(a.IdentExpr, exp(a.Ident, 'y')), 840 | }), 841 | ], 842 | returnVoid: false, 843 | }), 844 | }), 845 | args: exp(a.TupleExpr, { 846 | size: 2, 847 | items: [ 848 | exp(a.LitExpr, exp(a.IntLit, '10')), 849 | exp(a.LitExpr, exp(a.IntLit, '20')), 850 | ], 851 | }), 852 | }), 853 | ); 854 | 855 | exprTest( 856 | 'arr[idx]', 857 | exp(a.IndexExpr, { 858 | target: exp(a.IdentExpr, exp(a.Ident, 'arr')), 859 | index: exp(a.IdentExpr, exp(a.Ident, 'idx')), 860 | }), 861 | ); 862 | exprTest( 863 | '[1, 2, 3][2]', 864 | exp(a.IndexExpr, { 865 | target: exp(a.ArrayExpr, [ 866 | exp(a.LitExpr, exp(a.IntLit, '1')), 867 | exp(a.LitExpr, exp(a.IntLit, '2')), 868 | exp(a.LitExpr, exp(a.IntLit, '3')), 869 | ]), 870 | index: exp(a.LitExpr, exp(a.IntLit, '2')), 871 | }), 872 | ); 873 | exprTest( 874 | 'func()[idx % 3]("hello")[2][1]', 875 | exp(a.IndexExpr, { 876 | target: exp(a.IndexExpr, { 877 | target: exp(a.CallExpr, { 878 | func: exp(a.IndexExpr, { 879 | target: exp(a.CallExpr, { 880 | func: exp(a.IdentExpr, exp(a.Ident, 'func')), 881 | args: exp(a.TupleExpr, { 882 | size: 0, 883 | items: [], 884 | }), 885 | }), 886 | index: exp(a.BinaryExpr, { 887 | op: exp(a.MulOp, '%'), 888 | left: exp(a.IdentExpr, exp(a.Ident, 'idx')), 889 | right: exp(a.LitExpr, exp(a.IntLit, '3')), 890 | }), 891 | }), 892 | args: exp(a.TupleExpr, { 893 | size: 1, 894 | items: [exp(a.LitExpr, exp(a.StrLit, '"hello"'))], 895 | }), 896 | }), 897 | index: exp(a.LitExpr, exp(a.IntLit, '2')), 898 | }), 899 | index: exp(a.LitExpr, exp(a.IntLit, '1')), 900 | }), 901 | ); 902 | 903 | exprTest( 904 | ` 905 | if 1 + 2 > 3 { 906 | "hello" 907 | } else { 908 | "world" 909 | } 910 | `, 911 | exp(a.CondExpr, { 912 | if: exp(a.BinaryExpr, { 913 | op: exp(a.CompOp, '>'), 914 | left: exp(a.BinaryExpr, { 915 | op: exp(a.AddOp, '+'), 916 | left: exp(a.LitExpr, exp(a.IntLit, '1')), 917 | right: exp(a.LitExpr, exp(a.IntLit, '2')), 918 | }), 919 | right: exp(a.LitExpr, exp(a.IntLit, '3')), 920 | }), 921 | then: exp(a.Block, { 922 | bodies: [exp(a.LitExpr, exp(a.StrLit, '"hello"'))], 923 | returnVoid: false, 924 | }), 925 | else: exp(a.Block, { 926 | bodies: [exp(a.LitExpr, exp(a.StrLit, '"world"'))], 927 | returnVoid: false, 928 | }), 929 | }), 930 | ); 931 | exprTest( 932 | ` 933 | if 1 + 2 > 3 { 934 | print("hello, world"); 935 | } else { 936 | } 937 | `, 938 | exp(a.CondExpr, { 939 | if: exp(a.BinaryExpr, { 940 | op: exp(a.CompOp, '>'), 941 | left: exp(a.BinaryExpr, { 942 | op: exp(a.AddOp, '+'), 943 | left: exp(a.LitExpr, exp(a.IntLit, '1')), 944 | right: exp(a.LitExpr, exp(a.IntLit, '2')), 945 | }), 946 | right: exp(a.LitExpr, exp(a.IntLit, '3')), 947 | }), 948 | then: exp(a.Block, { 949 | bodies: [ 950 | exp(a.CallExpr, { 951 | func: exp(a.IdentExpr, exp(a.Ident, 'print')), 952 | args: exp(a.TupleExpr, { 953 | size: 1, 954 | items: [exp(a.LitExpr, exp(a.StrLit, '"hello, world"'))], 955 | }), 956 | }), 957 | ], 958 | returnVoid: true, 959 | }), 960 | else: exp(a.Block, { bodies: [], returnVoid: true }), 961 | }), 962 | ); 963 | 964 | exprTest( 965 | 'while x { x * 2 }', 966 | exp(a.LoopExpr, { 967 | while: exp(a.IdentExpr, exp(a.Ident, 'x')), 968 | body: exp(a.Block, { 969 | bodies: [ 970 | exp(a.BinaryExpr, { 971 | op: exp(a.MulOp, '*'), 972 | left: exp(a.IdentExpr, exp(a.Ident, 'x')), 973 | right: exp(a.LitExpr, exp(a.IntLit, '2')), 974 | }), 975 | ], 976 | returnVoid: false, 977 | }), 978 | }), 979 | ); 980 | exprTest( 981 | ` 982 | while true { 983 | if x > 100 { 984 | break; 985 | } else { 986 | x = x + 1; 987 | } 988 | } 989 | `, 990 | exp(a.LoopExpr, { 991 | while: exp(a.LitExpr, exp(a.BoolLit, 'true')), 992 | body: exp(a.Block, { 993 | bodies: [ 994 | exp(a.CondExpr, { 995 | if: exp(a.BinaryExpr, { 996 | op: exp(a.CompOp, '>'), 997 | left: exp(a.IdentExpr, exp(a.Ident, 'x')), 998 | right: exp(a.LitExpr, exp(a.IntLit, '100')), 999 | }), 1000 | then: exp(a.Block, { 1001 | bodies: [exp(a.Break)], 1002 | returnVoid: true, 1003 | }), 1004 | else: exp(a.Block, { 1005 | bodies: [ 1006 | exp(a.Assign, { 1007 | lVal: exp(a.IdentExpr, exp(a.Ident, 'x')), 1008 | expr: exp(a.BinaryExpr, { 1009 | op: exp(a.AddOp, '+'), 1010 | left: exp(a.IdentExpr, exp(a.Ident, 'x')), 1011 | right: exp(a.LitExpr, exp(a.IntLit, '1')), 1012 | }), 1013 | }), 1014 | ], 1015 | returnVoid: true, 1016 | }), 1017 | }), 1018 | ], 1019 | returnVoid: false, 1020 | }), 1021 | }), 1022 | ); 1023 | 1024 | exprTest( 1025 | ` 1026 | fn (x int, y int) int { 1027 | let result = x + y; 1028 | result = result + 3; 1029 | result 1030 | } 1031 | `, 1032 | exp(a.FuncExpr, { 1033 | params: { 1034 | size: 2, 1035 | items: [ 1036 | { name: exp(a.Ident, 'x'), type: exp(a.IntType) }, 1037 | { name: exp(a.Ident, 'y'), type: exp(a.IntType) }, 1038 | ], 1039 | }, 1040 | returnType: exp(a.IntType), 1041 | body: exp(a.Block, { 1042 | bodies: [ 1043 | exp(a.Decl, { 1044 | name: exp(a.Ident, 'result'), 1045 | type: null, 1046 | expr: exp(a.BinaryExpr, { 1047 | op: exp(a.AddOp, '+'), 1048 | left: exp(a.IdentExpr, exp(a.Ident, 'x')), 1049 | right: exp(a.IdentExpr, exp(a.Ident, 'y')), 1050 | }), 1051 | }), 1052 | exp(a.Assign, { 1053 | lVal: exp(a.IdentExpr, exp(a.Ident, 'result')), 1054 | expr: exp(a.BinaryExpr, { 1055 | op: exp(a.AddOp, '+'), 1056 | left: exp(a.IdentExpr, exp(a.Ident, 'result')), 1057 | right: exp(a.LitExpr, exp(a.IntLit, '3')), 1058 | }), 1059 | }), 1060 | exp(a.IdentExpr, exp(a.Ident, 'result')), 1061 | ], 1062 | returnVoid: false, 1063 | }), 1064 | }), 1065 | ); 1066 | 1067 | exprTest( 1068 | ` 1069 | fn () (bool, int) { 1070 | let result = (true, 1); 1071 | result[1] = 1234; 1072 | result 1073 | } 1074 | `, 1075 | exp(a.FuncExpr, { 1076 | params: { 1077 | size: 0, 1078 | items: [], 1079 | }, 1080 | returnType: exp(a.TupleType, { 1081 | size: 2, 1082 | items: [exp(a.BoolType), exp(a.IntType)], 1083 | }), 1084 | body: exp(a.Block, { 1085 | bodies: [ 1086 | exp(a.Decl, { 1087 | name: exp(a.Ident, 'result'), 1088 | type: null, 1089 | expr: exp(a.TupleExpr, { 1090 | size: 2, 1091 | items: [ 1092 | exp(a.LitExpr, exp(a.BoolLit, 'true')), 1093 | exp(a.LitExpr, exp(a.IntLit, '1')), 1094 | ], 1095 | }), 1096 | }), 1097 | exp(a.Assign, { 1098 | lVal: exp(a.IndexExpr, { 1099 | target: exp(a.IdentExpr, exp(a.Ident, 'result')), 1100 | index: exp(a.LitExpr, exp(a.IntLit, '1')), 1101 | }), 1102 | expr: exp(a.LitExpr, exp(a.IntLit, '1234')), 1103 | }), 1104 | exp(a.IdentExpr, exp(a.Ident, 'result')), 1105 | ], 1106 | returnVoid: false, 1107 | }), 1108 | }), 1109 | ); 1110 | 1111 | // new expr 1112 | exprTest( 1113 | 'new int[10]', 1114 | exp(a.NewExpr, { 1115 | type: exp(a.IntType), 1116 | length: exp(a.LitExpr, exp(a.IntLit, '10')), 1117 | }), 1118 | ); 1119 | 1120 | console.log(chalk.green.bold('Passed!')); 1121 | -------------------------------------------------------------------------------- /test/typechecker.spec.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import * as a from '../src/parser/ast'; 3 | import { tokenize } from '../src/lexer/'; 4 | import { parse } from '../src/parser/'; 5 | import { desugarBefore } from '../src/desugarer/'; 6 | import { Compose } from '../src/util'; 7 | import { 8 | checkExprType, 9 | checkBlockType, 10 | assertType, 11 | typeCheck, 12 | } from '../src/typechecker/'; 13 | import { TypeContext } from '../src/typechecker/context'; 14 | import { TypeError } from '../src/typechecker/error'; 15 | 16 | console.log(chalk.bold('Running typechecker tests...')); 17 | 18 | // complex type constructors 19 | const tupleType = (v: a.Tuple>) => new a.TupleType(v, -1, -1); 20 | const arrayType = (v: a.Type) => new a.ArrayType(v, -1, -1); 21 | const funcType = (v: { param: a.Type; return: a.Type }) => 22 | new a.FuncType(v, -1, -1); 23 | 24 | const compileAST = Compose.then(tokenize) 25 | .then(parse) 26 | .then(desugarBefore).f; 27 | 28 | function exprTypeTest( 29 | exprStr: string, 30 | ctx: TypeContext, 31 | expectedType: a.Type, 32 | shouldThrow?: string, 33 | ) { 34 | const moduleStr = `let x = ${exprStr}`; 35 | 36 | function failWith(errMsg: string) { 37 | console.error(chalk.blue.bold('Test:')); 38 | console.error(exprStr); 39 | console.error(); 40 | console.error(chalk.red.bold('Error:')); 41 | console.error(errMsg); 42 | process.exit(1); 43 | } 44 | 45 | try { 46 | const mod = compileAST(moduleStr); 47 | const actualType = checkExprType(mod.value.decls[0].value.expr, ctx); 48 | assertType(actualType, expectedType); 49 | } catch (err) { 50 | if ( 51 | shouldThrow && 52 | err instanceof TypeError && 53 | err.message.includes(shouldThrow) 54 | ) { 55 | return; 56 | } 57 | 58 | failWith(err); 59 | } 60 | 61 | if (shouldThrow) { 62 | failWith(`No error was thrown for '${shouldThrow}'`); 63 | } 64 | } 65 | 66 | function blockTypeTest( 67 | blockStr: string, 68 | ctx: TypeContext, 69 | expectedType: a.Type, 70 | ) { 71 | const moduleStr = `let x = fn () ${expectedType.name} ${blockStr}`; 72 | try { 73 | const mod = compileAST(moduleStr); 74 | const fn = mod.value.decls[0].value.expr as a.FuncExpr; 75 | const actualType = checkBlockType(fn.value.body, ctx); 76 | assertType(actualType, expectedType); 77 | } catch (err) { 78 | console.error(chalk.blue.bold('Test:')); 79 | console.error(blockStr); 80 | console.error(); 81 | console.error(chalk.red.bold('Error:')); 82 | console.error(err); 83 | process.exit(1); 84 | } 85 | } 86 | 87 | function ctx(obj: Array<{ [name: string]: a.Type }> = []): TypeContext { 88 | const ctx = new TypeContext(); 89 | for (const scopeObj of obj) { 90 | Object.keys(scopeObj).forEach(name => 91 | ctx.push({ ident: new a.Ident(name, -1, -1), type: scopeObj[name] }), 92 | ); 93 | } 94 | return ctx; 95 | } 96 | 97 | // literal 98 | exprTypeTest('123', ctx(), new a.IntType(0, 0)); 99 | exprTypeTest('.123', ctx(), new a.FloatType(0, 0)); 100 | exprTypeTest('"hello, world"', ctx(), new a.StrType(0, 0)); 101 | exprTypeTest('true', ctx(), new a.BoolType(0, 0)); 102 | exprTypeTest('false', ctx(), new a.BoolType(0, 0)); 103 | exprTypeTest("'\\n'", ctx(), new a.CharType(0, 0)); 104 | 105 | // ident 106 | exprTypeTest( 107 | 'some_ident', 108 | ctx([{ some_ident: a.IntType.instance }]), 109 | a.IntType.instance, 110 | ); 111 | exprTypeTest( 112 | 'some_ident', 113 | ctx([ 114 | {}, 115 | { other_ident: a.FloatType.instance }, 116 | { some_ident: a.IntType.instance }, 117 | {}, 118 | ]), 119 | a.IntType.instance, 120 | ); 121 | exprTypeTest( 122 | 'some_ident', 123 | ctx([ 124 | {}, 125 | { one_ident: a.IntType.instance }, 126 | { some_ident: a.StrType.instance }, 127 | {}, 128 | ]), 129 | a.StrType.instance, 130 | ); 131 | exprTypeTest( 132 | 'invalid_ident', 133 | ctx([ 134 | {}, 135 | { one_ident: a.IntType.instance }, 136 | { some_ident: a.StrType.instance }, 137 | {}, 138 | ]), 139 | a.StrType.instance, 140 | 'undefined identifier: found invalid_ident', 141 | ); 142 | 143 | // tuple 144 | exprTypeTest( 145 | '(123, hello, true)', 146 | ctx([{ hello: a.StrType.instance }]), 147 | tupleType({ 148 | size: 3, 149 | items: [a.IntType.instance, a.StrType.instance, a.BoolType.instance], 150 | }), 151 | ); 152 | exprTypeTest( 153 | '(123, hello, false)', 154 | ctx([{ hello: a.StrType.instance }]), 155 | tupleType({ 156 | size: 4, 157 | items: [ 158 | a.IntType.instance, 159 | a.StrType.instance, 160 | a.BoolType.instance, 161 | a.CharType.instance, 162 | ], 163 | }), 164 | 'Tuple length mismatch: expected (int, str, bool, char), found (int, str, bool)', 165 | ); 166 | exprTypeTest( 167 | '(1234, hello, true)', 168 | ctx([{ hello: a.StrType.instance }]), 169 | tupleType({ 170 | size: 3, 171 | items: [a.IntType.instance, a.CharType.instance, a.BoolType.instance], 172 | }), 173 | 'Type mismatch: expected (int, char, bool), found (int, str, bool)', 174 | ); 175 | 176 | // array 177 | exprTypeTest('[1, 2, 3, 4]', ctx(), arrayType(a.IntType.instance)); 178 | exprTypeTest('[]', ctx(), arrayType(a.IntType.instance)); 179 | exprTypeTest('[]', ctx(), arrayType(a.StrType.instance)); 180 | exprTypeTest( 181 | '[[1], [2, 3, 4], []]', 182 | ctx(), 183 | arrayType(arrayType(a.IntType.instance)), 184 | ); 185 | exprTypeTest( 186 | '[some_ident, 4]', 187 | ctx([{ some_ident: a.IntType.instance }]), 188 | arrayType(a.IntType.instance), 189 | ); 190 | exprTypeTest( 191 | '[some_ident, 4]', 192 | ctx([{ some_ident: a.IntType.instance }]), 193 | arrayType(a.StrType.instance), 194 | 'Type mismatch: expected [str], found [int]', 195 | ); 196 | exprTypeTest( 197 | '[some_ident, "str", 4]', 198 | ctx([{ some_ident: a.IntType.instance }]), 199 | arrayType(a.IntType.instance), 200 | 'Type mismatch: expected int, found str', 201 | ); 202 | 203 | // function 204 | exprTypeTest( 205 | 'fn (a int) bool { true }', 206 | ctx(), 207 | funcType({ 208 | param: a.IntType.instance, 209 | return: a.BoolType.instance, 210 | }), 211 | ); 212 | exprTypeTest( 213 | 'fn (a int, b str) bool { true }', 214 | ctx(), 215 | funcType({ 216 | param: tupleType({ 217 | size: 2, 218 | items: [a.IntType.instance, a.StrType.instance], 219 | }), 220 | return: a.BoolType.instance, 221 | }), 222 | ); 223 | exprTypeTest( 224 | "fn (a int, b str) bool -> char { fn (c bool) char { 'a' } }", 225 | ctx(), 226 | funcType({ 227 | param: tupleType({ 228 | size: 2, 229 | items: [a.IntType.instance, a.StrType.instance], 230 | }), 231 | return: funcType({ 232 | param: a.BoolType.instance, 233 | return: a.CharType.instance, 234 | }), 235 | }), 236 | ); 237 | exprTypeTest( 238 | "fn (a str -> int) bool -> char { fn (c bool) char { 'a' } }", 239 | ctx(), 240 | funcType({ 241 | param: funcType({ 242 | param: a.StrType.instance, 243 | return: a.IntType.instance, 244 | }), 245 | return: funcType({ 246 | param: a.BoolType.instance, 247 | return: a.CharType.instance, 248 | }), 249 | }), 250 | ); 251 | exprTypeTest( 252 | "fn (a float, b str -> int) bool -> char { fn (c bool) char { 'a' } }", 253 | ctx(), 254 | funcType({ 255 | param: tupleType({ 256 | size: 2, 257 | items: [ 258 | a.FloatType.instance, 259 | funcType({ 260 | param: a.StrType.instance, 261 | return: a.IntType.instance, 262 | }), 263 | ], 264 | }), 265 | return: funcType({ 266 | param: a.BoolType.instance, 267 | return: a.CharType.instance, 268 | }), 269 | }), 270 | ); 271 | exprTypeTest( 272 | 'fn (a int, b str) bool { false }', 273 | ctx(), 274 | funcType({ 275 | param: tupleType({ 276 | size: 2, 277 | items: [a.CharType.instance, a.StrType.instance], 278 | }), 279 | return: a.BoolType.instance, 280 | }), 281 | 'Type mismatch: expected (char, str) -> bool, found (int, str) -> bool', 282 | ); 283 | exprTypeTest( 284 | "fn (a int, b str) bool -> char { fn (c bool) char { 'a' } }", 285 | ctx(), 286 | funcType({ 287 | param: tupleType({ 288 | size: 2, 289 | items: [a.IntType.instance, a.StrType.instance], 290 | }), 291 | return: funcType({ 292 | param: a.BoolType.instance, 293 | return: a.BoolType.instance, 294 | }), 295 | }), 296 | 'Type mismatch: expected (int, str) -> bool -> bool, found (int, str) -> bool -> char', 297 | ); 298 | exprTypeTest( 299 | "fn (a str -> int) bool -> char { fn (c bool) char { 'a' } }", 300 | ctx(), 301 | funcType({ 302 | param: funcType({ 303 | param: a.StrType.instance, 304 | return: a.BoolType.instance, 305 | }), 306 | return: funcType({ 307 | param: a.BoolType.instance, 308 | return: a.CharType.instance, 309 | }), 310 | }), 311 | 'Type mismatch: expected (str -> bool) -> bool -> char, found (str -> int) -> bool -> char', 312 | ); 313 | exprTypeTest( 314 | 'fn (a int) bool {}', 315 | ctx(), 316 | funcType({ 317 | param: a.IntType.instance, 318 | return: a.BoolType.instance, 319 | }), 320 | 'Function return type mismatch: expected bool, found void', 321 | ); 322 | exprTypeTest( 323 | 'fn (a int) bool { a }', 324 | ctx(), 325 | funcType({ 326 | param: a.IntType.instance, 327 | return: a.BoolType.instance, 328 | }), 329 | 'Function return type mismatch: expected bool, found int', 330 | ); 331 | exprTypeTest( 332 | 'fn (a int) void { a }', 333 | ctx(), 334 | funcType({ 335 | param: a.IntType.instance, 336 | return: a.VoidType.instance, 337 | }), 338 | "Function return type mismatch, ';' may be missing: expected void, found int", 339 | ); 340 | exprTypeTest( 341 | 'fn (a int) void { a; }', 342 | ctx(), 343 | funcType({ 344 | param: a.IntType.instance, 345 | return: a.VoidType.instance, 346 | }), 347 | ); 348 | 349 | // call expr 350 | exprTypeTest('fn (a int, b int) int { a } (1, 2)', ctx(), a.IntType.instance); 351 | exprTypeTest('fn (a str) char { \'a\' } ("hello")', ctx(), a.CharType.instance); 352 | exprTypeTest( 353 | "fn (a str -> int) bool -> char { fn (c bool) char { 'a' } } (fn (a str) int { 1 })", 354 | ctx(), 355 | funcType({ 356 | param: a.BoolType.instance, 357 | return: a.CharType.instance, 358 | }), 359 | ); 360 | exprTypeTest( 361 | 'f1(f2)', 362 | ctx([ 363 | { 364 | f1: funcType({ 365 | param: funcType({ 366 | param: a.StrType.instance, 367 | return: a.BoolType.instance, 368 | }), 369 | return: funcType({ 370 | param: a.BoolType.instance, 371 | return: a.CharType.instance, 372 | }), 373 | }), 374 | f2: funcType({ 375 | param: a.StrType.instance, 376 | return: a.BoolType.instance, 377 | }), 378 | }, 379 | ]), 380 | funcType({ 381 | param: a.BoolType.instance, 382 | return: a.CharType.instance, 383 | }), 384 | ); 385 | exprTypeTest( 386 | '"i am not callable"(1, \'c\')', 387 | ctx(), 388 | a.VoidType.instance, 389 | 'non-callable target: expected function, found str', 390 | ); 391 | exprTypeTest( 392 | "fn (a int, b int) int { a } (1, 'c')", 393 | ctx(), 394 | a.IntType.instance, 395 | 'Function parameter type mismatch: expected (int, int), found (int, char)', 396 | ); 397 | exprTypeTest( 398 | "fn (a str) char { 'a' } (.123)", 399 | ctx(), 400 | a.CharType.instance, 401 | 'Function parameter type mismatch: expected str, found float', 402 | ); 403 | 404 | // block 405 | blockTypeTest('{}', ctx(), a.VoidType.instance); 406 | blockTypeTest( 407 | ` 408 | { 409 | let x = fn () int { x() }; 410 | x() 411 | } 412 | `, 413 | ctx(), 414 | a.IntType.instance, 415 | ); 416 | blockTypeTest( 417 | ` 418 | { 419 | f(123); 420 | let y = f(g); 421 | h(y) 422 | } 423 | `, 424 | ctx([ 425 | { 426 | f: funcType({ 427 | param: a.IntType.instance, 428 | return: a.BoolType.instance, 429 | }), 430 | }, 431 | { g: a.IntType.instance }, 432 | { 433 | h: funcType({ 434 | param: a.BoolType.instance, 435 | return: a.CharType.instance, 436 | }), 437 | }, 438 | ]), 439 | a.CharType.instance, 440 | ); 441 | blockTypeTest( 442 | ` 443 | { 444 | f(123); 445 | let y = f(g); 446 | h(y); 447 | } 448 | `, 449 | ctx([ 450 | { 451 | f: funcType({ 452 | param: a.IntType.instance, 453 | return: a.BoolType.instance, 454 | }), 455 | }, 456 | { g: a.IntType.instance }, 457 | { 458 | h: funcType({ 459 | param: a.BoolType.instance, 460 | return: a.CharType.instance, 461 | }), 462 | }, 463 | ]), 464 | a.VoidType.instance, 465 | ); 466 | 467 | // index expr 468 | exprTypeTest( 469 | 'arr[3]', 470 | ctx([{ arr: arrayType(a.IntType.instance) }]), 471 | a.IntType.instance, 472 | ); 473 | exprTypeTest('"hello"[3]', ctx(), a.CharType.instance); 474 | exprTypeTest('("hello", false, 123)[0]', ctx(), a.StrType.instance); 475 | exprTypeTest('("hello", false, 123)[1]', ctx(), a.BoolType.instance); 476 | exprTypeTest('("hello", false, 123)[2]', ctx(), a.IntType.instance); 477 | exprTypeTest( 478 | '("hello", false, 123)[3]', 479 | ctx(), 480 | a.VoidType.instance, 481 | 'Tuple index out of range: expected int < 3, found 3', 482 | ); 483 | exprTypeTest( 484 | 'arr[no_int]', 485 | ctx([ 486 | { 487 | arr: arrayType(a.IntType.instance), 488 | no_int: a.CharType.instance, 489 | }, 490 | ]), 491 | a.IntType.instance, 492 | 'Index type mismatch: expected int, found char', 493 | ); 494 | exprTypeTest( 495 | '"hello"[no_int]', 496 | ctx([{ no_int: a.CharType.instance }]), 497 | a.CharType.instance, 498 | 'Index type mismatch: expected int, found char', 499 | ); 500 | exprTypeTest( 501 | '("hello", false, 123)[i]', 502 | ctx([{ i: a.IntType.instance }]), 503 | a.VoidType.instance, 504 | 'Invalid tuple index: only int literal is allowed for tuple index: found expr', 505 | ); 506 | exprTypeTest( 507 | '("hello", false, 123)[no_int]', 508 | ctx([{ no_int: a.CharType.instance }]), 509 | a.VoidType.instance, 510 | 'Invalid tuple index: only int literal is allowed for tuple index: found expr', 511 | ); 512 | exprTypeTest( 513 | '3[0]', 514 | ctx(), 515 | a.VoidType.instance, 516 | 'Indexable type mismatch: expected array, str or tuple, found int', 517 | ); 518 | 519 | // cond expr 520 | exprTypeTest( 521 | 'if some_bool { 10 } else { 20 }', 522 | ctx([{ some_bool: a.BoolType.instance }]), 523 | a.IntType.instance, 524 | ); 525 | exprTypeTest( 526 | 'if f(123) { "hello" } else { "world" }', 527 | ctx([ 528 | { 529 | f: funcType({ param: a.IntType.instance, return: a.BoolType.instance }), 530 | }, 531 | ]), 532 | a.StrType.instance, 533 | ); 534 | exprTypeTest( 535 | 'if some_char { 10 } else { 20 }', 536 | ctx([{ some_char: a.CharType.instance }]), 537 | a.IntType.instance, 538 | 'Type mismatch: expected bool, found char', 539 | ); 540 | exprTypeTest( 541 | 'if some_bool { 10 } else { "hello" }', 542 | ctx([{ some_bool: a.BoolType.instance }]), 543 | a.IntType.instance, 544 | "'else' block should have the same type as 'if' block: expected int, found str", 545 | ); 546 | exprTypeTest( 547 | 'if some_bool { } else { "hello" }', 548 | ctx([{ some_bool: a.BoolType.instance }]), 549 | a.VoidType.instance, 550 | "'else' block should have the same type as 'if' block, ';' may be missing: expected void, found str", 551 | ); 552 | exprTypeTest( 553 | 'if some_bool { } else { "hello"; }', 554 | ctx([{ some_bool: a.BoolType.instance }]), 555 | a.VoidType.instance, 556 | ); 557 | 558 | // loop expr 559 | exprTypeTest( 560 | 'while true { f(123) }', 561 | ctx([ 562 | { 563 | f: funcType({ param: a.IntType.instance, return: a.BoolType.instance }), 564 | }, 565 | ]), 566 | a.VoidType.instance, 567 | ); 568 | exprTypeTest( 569 | 'while cond { f(123); }', 570 | ctx([ 571 | { 572 | cond: a.BoolType.instance, 573 | f: funcType({ param: a.IntType.instance, return: a.BoolType.instance }), 574 | }, 575 | ]), 576 | a.VoidType.instance, 577 | ); 578 | exprTypeTest( 579 | 'while true { if i > 10 { break } else { i = i + 1 } }', 580 | ctx([ 581 | { 582 | i: a.IntType.instance, 583 | }, 584 | ]), 585 | a.VoidType.instance, 586 | ); 587 | exprTypeTest( 588 | 'while i { if i > 10 { break } else { i = i + 1 } }', 589 | ctx([ 590 | { 591 | i: a.IntType.instance, 592 | }, 593 | ]), 594 | a.VoidType.instance, 595 | 'Loop condition should be a boolean: found int', 596 | ); 597 | exprTypeTest( 598 | 'if true { break } else { }', 599 | ctx([]), 600 | a.VoidType.instance, 601 | 'break can be only used in a loop: found unexpected break', 602 | ); 603 | 604 | // unary expr 605 | exprTypeTest( 606 | '+x', 607 | ctx([ 608 | { 609 | x: a.IntType.instance, 610 | }, 611 | ]), 612 | a.IntType.instance, 613 | ); 614 | exprTypeTest( 615 | '-x', 616 | ctx([ 617 | { 618 | x: a.IntType.instance, 619 | }, 620 | ]), 621 | a.IntType.instance, 622 | ); 623 | exprTypeTest( 624 | '+x', 625 | ctx([ 626 | { 627 | x: a.FloatType.instance, 628 | }, 629 | ]), 630 | a.FloatType.instance, 631 | ); 632 | exprTypeTest( 633 | '-x', 634 | ctx([ 635 | { 636 | x: a.FloatType.instance, 637 | }, 638 | ]), 639 | a.FloatType.instance, 640 | ); 641 | exprTypeTest( 642 | '!x', 643 | ctx([ 644 | { 645 | x: a.BoolType.instance, 646 | }, 647 | ]), 648 | a.BoolType.instance, 649 | ); 650 | exprTypeTest( 651 | '-x', 652 | ctx([ 653 | { 654 | x: a.BoolType.instance, 655 | }, 656 | ]), 657 | a.BoolType.instance, 658 | "Operand type mismatch for '-': expected int or float, found bool", 659 | ); 660 | exprTypeTest( 661 | '!x', 662 | ctx([ 663 | { 664 | x: a.IntType.instance, 665 | }, 666 | ]), 667 | a.IntType.instance, 668 | "Operand type mismatch for '!': expected bool, found int", 669 | ); 670 | 671 | // binary expr 672 | // eq op 673 | exprTypeTest('1 == 1', ctx(), a.BoolType.instance); 674 | exprTypeTest('"hello" != "hello"', ctx(), a.BoolType.instance); 675 | exprTypeTest( 676 | '"hello" == 3', 677 | ctx(), 678 | a.BoolType.instance, 679 | "Right-hand operand type mismatch for '==': expected str, found int", 680 | ); 681 | // comp op 682 | exprTypeTest('3.5 > .0', ctx(), a.BoolType.instance); 683 | exprTypeTest("'c' > 'a'", ctx(), a.BoolType.instance); 684 | exprTypeTest( 685 | "'c' < 3", 686 | ctx(), 687 | a.BoolType.instance, 688 | "Right-hand operand type mismatch for '<': expected char, found int", 689 | ); 690 | exprTypeTest( 691 | 'fn () void {} <= 3', 692 | ctx(), 693 | a.BoolType.instance, 694 | "Left-hand operand type mismatch for '<=': expected int, float, bool, char or str, found () -> void", 695 | ); 696 | // add & mul op 697 | exprTypeTest('3 + 0', ctx(), a.IntType.instance); 698 | exprTypeTest('3 * 123 / 13', ctx(), a.IntType.instance); 699 | exprTypeTest('3.5 + .0', ctx(), a.FloatType.instance); 700 | exprTypeTest('3.5 * .0 / 1.0', ctx(), a.FloatType.instance); 701 | exprTypeTest( 702 | '3.5 * 1 / 1.0', 703 | ctx(), 704 | a.FloatType.instance, 705 | "Right-hand operand type mismatch for '*': expected float, found int", 706 | ); 707 | exprTypeTest( 708 | '"4" | 1', 709 | ctx(), 710 | a.IntType.instance, 711 | "Left-hand operand type mismatch for '|': expected int, found str", 712 | ); 713 | // bool op 714 | exprTypeTest('true && false', ctx(), a.BoolType.instance); 715 | exprTypeTest('true || false', ctx(), a.BoolType.instance); 716 | exprTypeTest( 717 | '.1 || false', 718 | ctx(), 719 | a.BoolType.instance, 720 | "Left-hand operand type mismatch for '||': expected bool, found float", 721 | ); 722 | exprTypeTest( 723 | 'true && 1', 724 | ctx(), 725 | a.BoolType.instance, 726 | "Right-hand operand type mismatch for '&&': expected bool, found int", 727 | ); 728 | 729 | function typeCheckTest( 730 | program: string, 731 | context: TypeContext, 732 | shouldThrow?: string, 733 | ) { 734 | function failWith(errMsg: string) { 735 | console.error(chalk.blue.bold('Test:')); 736 | console.error(program); 737 | console.error(); 738 | console.error(chalk.red.bold('Error:')); 739 | console.error(errMsg); 740 | process.exit(1); 741 | } 742 | 743 | try { 744 | typeCheck(compileAST(program), context); 745 | } catch (err) { 746 | if ( 747 | shouldThrow && 748 | err instanceof TypeError && 749 | err.message.includes(shouldThrow) 750 | ) { 751 | return; 752 | } 753 | 754 | failWith(err); 755 | } 756 | 757 | if (shouldThrow) { 758 | failWith(`No error was thrown for '${shouldThrow}'`); 759 | } 760 | } 761 | 762 | typeCheckTest( 763 | ` 764 | let main = fn () void { 765 | print("hello, world!"); 766 | } 767 | `, 768 | ctx([ 769 | { 770 | print: funcType({ 771 | param: a.StrType.instance, 772 | return: a.VoidType.instance, 773 | }), 774 | }, 775 | ]), 776 | ); 777 | 778 | typeCheckTest( 779 | ` 780 | let fac = fn (n int) int { 781 | if (n == 1) { 782 | 1 783 | } else { 784 | n * fac(n - 1) 785 | } 786 | } 787 | 788 | let main = fn () void { 789 | print(i2s(fac(10))); 790 | } 791 | `, 792 | ctx([ 793 | { 794 | print: funcType({ 795 | param: a.StrType.instance, 796 | return: a.VoidType.instance, 797 | }), 798 | i2s: funcType({ param: a.IntType.instance, return: a.StrType.instance }), 799 | }, 800 | ]), 801 | ); 802 | 803 | typeCheckTest( 804 | ` 805 | let fac = fn (n int) int { 806 | if (n == 1) { 807 | 1 808 | } else { 809 | n * fac(n - 1) 810 | } 811 | } 812 | 813 | let print_int = fn (n int) void { 814 | print(i2s(n)) 815 | } 816 | 817 | let main = fn () void { 818 | print_int(fac(10)) 819 | } 820 | `, 821 | ctx([ 822 | { 823 | print: funcType({ 824 | param: a.StrType.instance, 825 | return: a.VoidType.instance, 826 | }), 827 | i2s: funcType({ param: a.IntType.instance, return: a.StrType.instance }), 828 | }, 829 | ]), 830 | ); 831 | 832 | typeCheckTest( 833 | ` 834 | let fac = fn (n int) int { 835 | if (n == 1) { 836 | 1 837 | } else { 838 | n * fac(n - 1) 839 | } 840 | } 841 | 842 | let print_int = fn (n int, blah str) void { 843 | print(i2s(n)) 844 | } 845 | 846 | let main = fn () void { 847 | print_int(fac(10)) 848 | } 849 | `, 850 | ctx([ 851 | { 852 | print: funcType({ 853 | param: a.StrType.instance, 854 | return: a.VoidType.instance, 855 | }), 856 | i2s: funcType({ param: a.IntType.instance, return: a.StrType.instance }), 857 | }, 858 | ]), 859 | 'Function parameter type mismatch: expected (int, str), found int at 15:13', 860 | ); 861 | 862 | // no void decl tests 863 | typeCheckTest( 864 | ` 865 | let f = fn () void {} 866 | let x: void = f() 867 | `, 868 | ctx(), 869 | 'A decl type cannot contain void: found void at 3:1', 870 | ); 871 | typeCheckTest( 872 | ` 873 | let f = fn () void {} 874 | let x = f() 875 | `, 876 | ctx(), 877 | 'A decl type cannot contain void: found void at 3:1', 878 | ); 879 | typeCheckTest( 880 | ` 881 | let f = fn () void {} 882 | let x = (1, f()) 883 | `, 884 | ctx(), 885 | 'A decl type cannot contain void: found (int, void) at 3:1', 886 | ); 887 | typeCheckTest( 888 | ` 889 | let f = fn () void {} 890 | let x = (1, ("hello", f(), false)) 891 | `, 892 | ctx(), 893 | 'A decl type cannot contain void: found (int, (str, void, bool)) at 3:1', 894 | ); 895 | typeCheckTest( 896 | ` 897 | let f = fn () void {} 898 | let x = [f()] 899 | `, 900 | ctx(), 901 | 'A decl type cannot contain void: found [void] at 3:1', 902 | ); 903 | typeCheckTest( 904 | ` 905 | let f = fn () void { 906 | let x: void = f() 907 | } 908 | `, 909 | ctx(), 910 | 'A decl type cannot contain void: found void at 3:3', 911 | ); 912 | typeCheckTest( 913 | ` 914 | let f = fn () void { 915 | let x = f() 916 | } 917 | `, 918 | ctx(), 919 | 'A decl type cannot contain void: found void at 3:3', 920 | ); 921 | typeCheckTest( 922 | ` 923 | let f = fn () void { 924 | let x = (1, f()) 925 | } 926 | `, 927 | ctx(), 928 | 'A decl type cannot contain void: found (int, void) at 3:3', 929 | ); 930 | typeCheckTest( 931 | ` 932 | let f = fn () void { 933 | let x = (1, ("hello", f(), false)) 934 | } 935 | `, 936 | ctx(), 937 | 'A decl type cannot contain void: found (int, (str, void, bool)) at 3:3', 938 | ); 939 | typeCheckTest( 940 | ` 941 | let f = fn () void { 942 | let x = [f()] 943 | } 944 | `, 945 | ctx(), 946 | 'A decl type cannot contain void: found [void] at 3:3', 947 | ); 948 | 949 | // Assignments 950 | typeCheckTest( 951 | ` 952 | let f = fn () void { 953 | let x = 10; 954 | x = 2; 955 | } 956 | `, 957 | ctx(), 958 | ); 959 | typeCheckTest( 960 | ` 961 | let f = fn () void { 962 | let x = 10; 963 | x = 1.0; 964 | } 965 | `, 966 | ctx(), 967 | 'Type mismatch: expected int, found float', 968 | ); 969 | typeCheckTest( 970 | ` 971 | let f = fn () void { 972 | let x = (10, true); 973 | x[1] = false; 974 | } 975 | `, 976 | ctx(), 977 | ); 978 | typeCheckTest( 979 | ` 980 | let f = fn () void { 981 | let x = (10, true); 982 | x[1] = "hi"; 983 | } 984 | `, 985 | ctx(), 986 | 'Type mismatch: expected bool, found str', 987 | ); 988 | typeCheckTest( 989 | ` 990 | let f = fn () void { 991 | let x = (10, true); 992 | let y = 1; 993 | x[y] = "hi"; 994 | } 995 | `, 996 | ctx(), 997 | 'Invalid tuple index: only int literal is allowed for tuple index: found expr', 998 | ); 999 | typeCheckTest( 1000 | ` 1001 | let f = fn () void { 1002 | let x = [1, 2, 3]; 1003 | x[1] = 1234; 1004 | } 1005 | `, 1006 | ctx(), 1007 | ); 1008 | typeCheckTest( 1009 | ` 1010 | let f = fn () void { 1011 | let x = [1, 2, 3]; 1012 | x[1] = true; 1013 | } 1014 | `, 1015 | ctx(), 1016 | 'Type mismatch: expected int, found bool', 1017 | ); 1018 | 1019 | // new expr 1020 | exprTypeTest('new int[10]', ctx(), arrayType(a.IntType.instance)); 1021 | exprTypeTest( 1022 | 'new int[l]', 1023 | ctx([{ l: a.IntType.instance }]), 1024 | arrayType(a.IntType.instance), 1025 | ); 1026 | exprTypeTest( 1027 | 'new int[int(f)]', 1028 | ctx([{ f: a.FloatType.instance }]), 1029 | arrayType(a.IntType.instance), 1030 | ); 1031 | exprTypeTest( 1032 | 'new int[c]', 1033 | ctx([{ c: a.CharType.instance }]), 1034 | arrayType(a.IntType.instance), 1035 | 'Length type mismatch: expected int, found char', 1036 | ); 1037 | 1038 | console.log(chalk.green.bold('Passed!')); 1039 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "outDir": "dist", 5 | "strict": true, 6 | "target": "es2017" 7 | }, 8 | "include": [ 9 | "src/**/*" 10 | ] 11 | } 12 | --------------------------------------------------------------------------------