├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── spec ├── calculator.spec.js ├── fuzz.spec.js ├── generator.js ├── manual-fuzz.js ├── parser.spec.js ├── support │ └── jasmine.json └── tokenizer.spec.js └── src ├── calculator.js ├── parser.js ├── tokenizer.js └── tokens.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Demo Fuzzing 2 | 3 | Repository ini berisi contoh demo untuk [teknik fuzzing](https://en.wikipedia.org/wiki/Fuzzing) yang dibahas di Hijra Engineering Talk tanggal 12 Maret 2022 yang silam (tonton [rekaman videonya di YouTube](https://www.youtube.com/watch?v=JkhvqDSa2Q4)). 4 | 5 | Yang dibutuhkan: [Node.js](https://nodejs.org/) versi 12 atau lebih baru. 6 | 7 | Langkah pertama, pasang semua dependensi terlebih dahulu: 8 | 9 | ``` 10 | npm install 11 | ``` 12 | 13 | Lalu coba jalankan fuzzing secara manual: 14 | 15 | ``` 16 | npm run fuzz 17 | ``` 18 | 19 | Seharusnya akan tampil pesan kesalahan, karena fuzzing menemukan sebuah bug! 20 | 21 | Silakan perbaiki dulu bug-nya (petunjuk: cari kata kunci `FIXME`). 22 | 23 | Sesudah bug tersebut diperbaiki, kembali ulangi langkah di atas dan mestinya tidak ada pesan kesalahan lagi. 24 | 25 | Untuk menjalankan unit tests secara lengkap, yang juga mencakup fuzzing (lihat `fuzz.spec.js`): 26 | 27 | ``` 28 | npm test 29 | ``` 30 | 31 | ## Tautan terkait 32 | 33 | * [OSS-Fuzz](https://github.com/google/oss-fuzz) continuous fuzzing for open source software 34 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-fuzzing", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "balanced-match": { 8 | "version": "1.0.2", 9 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 10 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 11 | "dev": true 12 | }, 13 | "brace-expansion": { 14 | "version": "1.1.11", 15 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 16 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 17 | "dev": true, 18 | "requires": { 19 | "balanced-match": "^1.0.0", 20 | "concat-map": "0.0.1" 21 | } 22 | }, 23 | "concat-map": { 24 | "version": "0.0.1", 25 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 26 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 27 | "dev": true 28 | }, 29 | "fs.realpath": { 30 | "version": "1.0.0", 31 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 32 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 33 | "dev": true 34 | }, 35 | "glob": { 36 | "version": "7.2.0", 37 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", 38 | "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", 39 | "dev": true, 40 | "requires": { 41 | "fs.realpath": "^1.0.0", 42 | "inflight": "^1.0.4", 43 | "inherits": "2", 44 | "minimatch": "^3.0.4", 45 | "once": "^1.3.0", 46 | "path-is-absolute": "^1.0.0" 47 | } 48 | }, 49 | "inflight": { 50 | "version": "1.0.6", 51 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 52 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 53 | "dev": true, 54 | "requires": { 55 | "once": "^1.3.0", 56 | "wrappy": "1" 57 | } 58 | }, 59 | "inherits": { 60 | "version": "2.0.4", 61 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 62 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 63 | "dev": true 64 | }, 65 | "isaac": { 66 | "version": "0.0.5", 67 | "resolved": "https://registry.npmjs.org/isaac/-/isaac-0.0.5.tgz", 68 | "integrity": "sha1-pPKSIpqL324zkAo+uLQgGlf/LYk=" 69 | }, 70 | "jasmine": { 71 | "version": "4.1.0", 72 | "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-4.1.0.tgz", 73 | "integrity": "sha512-4VhjbUgwfNS9CBnUMoSWr9tdNgOoOhNIjAD8YRxTn+PmOf4qTSC0Uqhk66dWGnz2vJxtNIU0uBjiwnsp4Ud9VA==", 74 | "dev": true, 75 | "requires": { 76 | "glob": "^7.1.6", 77 | "jasmine-core": "^4.1.0" 78 | } 79 | }, 80 | "jasmine-core": { 81 | "version": "4.1.0", 82 | "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.1.0.tgz", 83 | "integrity": "sha512-8E8BiffCL8sBwK1zU9cbavLe8xpJAgOduSJ6N8PJVv8VosQ/nxVTuXj2kUeHxTlZBVvh24G19ga7xdiaxlceKg==", 84 | "dev": true 85 | }, 86 | "minimatch": { 87 | "version": "3.1.2", 88 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 89 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 90 | "dev": true, 91 | "requires": { 92 | "brace-expansion": "^1.1.7" 93 | } 94 | }, 95 | "once": { 96 | "version": "1.4.0", 97 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 98 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 99 | "dev": true, 100 | "requires": { 101 | "wrappy": "1" 102 | } 103 | }, 104 | "path-is-absolute": { 105 | "version": "1.0.1", 106 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 107 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 108 | "dev": true 109 | }, 110 | "wrappy": { 111 | "version": "1.0.2", 112 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 113 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 114 | "dev": true 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-fuzzing", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jasmine", 8 | "fuzz": "node spec/manual-fuzz.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "jasmine": "4.1.0" 15 | }, 16 | "dependencies": { 17 | "isaac": "0.0.5" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /spec/calculator.spec.js: -------------------------------------------------------------------------------- 1 | const calc = require('../src/calculator'); 2 | 3 | describe('calc', () => { 4 | it('should evaluate negations', () => { 5 | expect(calc('-1')).toEqual(-1); 6 | //expect(calc('--2')).toEqual(2); 7 | //expect(calc('-+3')).toEqual(-3); 8 | //expect(calc('+-4')).toEqual(-4); 9 | }); 10 | 11 | it('should evaluate additions and substractions', () => { 12 | expect(calc('1 + 2')).toEqual(3); 13 | expect(calc('1 + 2 + 3')).toEqual(6); 14 | expect(calc('4 - 5')).toEqual(-1); 15 | expect(calc('4 - 5 - 6')).toEqual(-7); 16 | expect(calc('6 + 7 - 8')).toEqual(5); 17 | expect(calc('6 - 7 + 8')).toEqual(7); 18 | expect(calc('6 + 7 - 8 + 9')).toEqual(14); 19 | }); 20 | 21 | it('should evaluate multiplications and divisions', () => { 22 | expect(calc('1 * 2')).toEqual(2); 23 | expect(calc('1 * 2 * 3')).toEqual(6); 24 | expect(calc('4 / 5')).toEqual(0.8); 25 | expect(calc('6 / 3 / 2')).toEqual(1); 26 | expect(calc('6 * 2 / 3')).toEqual(4); 27 | expect(calc('6 * 2 / 3 * 2')).toEqual(8); 28 | }); 29 | 30 | it('should honor operator precedences', () => { 31 | expect(calc('1 + 2 * 3')).toEqual(7); 32 | expect(calc('1 + 2 - 3 / 4')).toEqual(2.25); 33 | expect(calc('4 - +12 / 6')).toEqual(2); 34 | expect(calc('6 * 7 - - 8')).toEqual(50); 35 | }); 36 | 37 | it('should tackle grouped expressions', () => { 38 | expect(calc('(1)')).toEqual(1); 39 | expect(calc('(((2)))')).toEqual(2); 40 | expect(calc('-(3)')).toEqual(-3); 41 | expect(calc('(1 + 2) * + 3')).toEqual(9); 42 | expect(calc('-(1 + 2) * - 3')).toEqual(9); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /spec/fuzz.spec.js: -------------------------------------------------------------------------------- 1 | const generateExpression = require('./generator'); 2 | const calc = require('../src/calculator'); 3 | 4 | describe('fuzzing', () => { 5 | for (let seed = 1; seed < 1e3; ++seed) { 6 | it(`should parse generated expression from seed ${seed}`, () => { 7 | const expression = generateExpression(seed); 8 | expect(() => calc(expression)).not.toThrow(); 9 | }); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /spec/generator.js: -------------------------------------------------------------------------------- 1 | const isaac = require('isaac'); 2 | 3 | function generateExpression(seed = 7, depth = 5) { 4 | isaac.seed(seed); 5 | 6 | const randomInt = (max) => Math.floor(isaac.random() * max); 7 | const randomItem = (items) => items[randomInt(items.length)]; 8 | 9 | const integer = () => randomInt(1000); 10 | 11 | const number = () => integer(); 12 | 13 | const positive = () => '+' + primary(); 14 | const negative = () => '-' + primary(); 15 | 16 | const group = () => '(' + primary() + ')'; 17 | 18 | const primary = () => { 19 | --depth; 20 | let result = number(); 21 | if (depth > 0) { 22 | result = randomItem([result, positive(), negative(), binary(), group()]); 23 | } 24 | ++depth; 25 | return String(result); 26 | }; 27 | 28 | const binary = () => { 29 | const left = primary(); 30 | const right = primary(); 31 | const op = randomItem(['+', '-', '*', '/']); 32 | return left + op + right; 33 | }; 34 | 35 | return primary(); 36 | } 37 | 38 | module.exports = generateExpression; -------------------------------------------------------------------------------- /spec/manual-fuzz.js: -------------------------------------------------------------------------------- 1 | const calc = require('../src/calculator'); 2 | const generateExpression = require('./generator'); 3 | 4 | for (let seed = 0; seed < 10; ++seed) { 5 | const expression = generateExpression(seed); 6 | try { 7 | const result = calc(expression); 8 | console.log('OK', { seed, expression, result }); 9 | } catch (e) { 10 | console.error('FAIL', { seed, expression }); 11 | process.exit(-1); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /spec/parser.spec.js: -------------------------------------------------------------------------------- 1 | const parse = require('../src/parser'); 2 | 3 | describe('parser', () => { 4 | it('should parse numbers', () => { 5 | expect(parse('0')).toEqual(0); 6 | expect(parse('1')).toEqual(1); 7 | expect(parse('3.14')).toEqual(3.14); 8 | }); 9 | 10 | it('should parse unary expressions', () => { 11 | expect(parse('-0')).toEqual(['-', 0]); 12 | expect(parse('+1')).toEqual(1); 13 | }); 14 | 15 | it('should parse additions and subtractions', () => { 16 | expect(parse('1 + 2')).toEqual(['+', 1, 2]); 17 | expect(parse('1 + 2 + 3')).toEqual(['+', ['+', 1, 2], 3]); 18 | expect(parse('4 - 5')).toEqual(['-', 4, 5]); 19 | expect(parse('4 - 5 - 6')).toEqual(['-', ['-', 4, 5], 6]); 20 | expect(parse('6 + 7 - 8')).toEqual(['-', ['+', 6, 7], 8]); 21 | expect(parse('6 - 7 + 8')).toEqual(['+', ['-', 6, 7], 8]); 22 | expect(parse('6 + 7 - 8 + 9')).toEqual(['+', ['-', ['+', 6, 7], 8], 9]); 23 | }); 24 | 25 | it('should parse multiplications and divisions', () => { 26 | expect(parse('1 * 2')).toEqual(['*', 1, 2]); 27 | expect(parse('1 * 2 * 3')).toEqual(['*', ['*', 1, 2], 3]); 28 | expect(parse('4 / 5')).toEqual(['/', 4, 5]); 29 | expect(parse('4 / 5 / 6')).toEqual(['/', ['/', 4, 5], 6]); 30 | expect(parse('6 * 7 / 8')).toEqual(['/', ['*', 6, 7], 8]); 31 | expect(parse('6 / 7 * 8')).toEqual(['*', ['/', 6, 7], 8]); 32 | expect(parse('6 * 7 / 8 * 9')).toEqual(['*', ['/', ['*', 6, 7], 8], 9]); 33 | }); 34 | 35 | it('should honor operator precedences', () => { 36 | expect(parse('1 + 2 * 3')).toEqual(['+', 1, ['*', 2, 3]]); 37 | expect(parse('1 + 2 - 3 / 4')).toEqual(['-', ['+', 1, 2], ['/', 3, 4]]); 38 | expect(parse('4 - +5 / 6')).toEqual(['-', 4, ['/', 5, 6]]); 39 | expect(parse('6 * 7 - - 8')).toEqual(['-', ['*', 6, 7], ['-', 8]]); 40 | }); 41 | 42 | it('should parse grouped expressions', () => { 43 | expect(parse('(1)')).toEqual(1); 44 | expect(parse('(((2)))')).toEqual(2); 45 | expect(parse('-(3)')).toEqual(['-', 3]); 46 | expect(parse('(1 + 2) * + 3')).toEqual(['*', ['+', 1, 2], 3]); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": true 11 | } 12 | -------------------------------------------------------------------------------- /spec/tokenizer.spec.js: -------------------------------------------------------------------------------- 1 | const T = require('../src/tokens'); 2 | const tokenize = require('../src/tokenizer'); 3 | 4 | describe('tokenizer', () => { 5 | const tokens = (expr) => tokenize(expr).map((t) => t.type); 6 | 7 | it('should return [] on empty expression', () => { 8 | expect(tokenize()).toEqual([]); 9 | }); 10 | 11 | it('should ignore whitespaces', () => { 12 | expect(tokenize(' ')).toEqual([]); 13 | expect(tokenize(' ')).toEqual([]); 14 | }); 15 | 16 | it('should recognize operators', () => { 17 | expect(tokens('(')).toEqual([T.OpenParenthesis]); 18 | expect(tokens(')')).toEqual([T.CloseParenthesis]); 19 | expect(tokens('()')).toEqual([T.OpenParenthesis, T.CloseParenthesis]); 20 | expect(tokens('+')).toEqual([T.Plus]); 21 | expect(tokens('-')).toEqual([T.Minus]); 22 | expect(tokens('*')).toEqual([T.Star]); 23 | expect(tokens('/')).toEqual([T.Slash]); 24 | }); 25 | 26 | it('should handle integer numbers', () => { 27 | expect(tokens('0')).toEqual([T.Number]); 28 | expect(tokens(' 42 ')).toEqual([T.Number]); 29 | }); 30 | 31 | it('should handle floating-point numbers', () => { 32 | expect(tokens('1.2')).toEqual([T.Number]); 33 | expect(tokens('3.14159')).toEqual([T.Number]); 34 | expect(tokens('0.1')).toEqual([T.Number]); 35 | expect(tokens('.1')).toEqual([T.Number]); 36 | expect(tokens('1.')).toEqual([T.Number]); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/calculator.js: -------------------------------------------------------------------------------- 1 | const parse = require('./parser'); 2 | 3 | function calc(expression) { 4 | const eval = (node) => { 5 | if (Array.isArray(node)) { 6 | const [op, ...operands] = node; 7 | const subs = operands.map(eval); 8 | const [left, right] = subs; 9 | switch (op) { 10 | case '+': 11 | return left + right; 12 | case '-': 13 | return node.length === 3 ? left - right : -left; 14 | case '*': 15 | return left * right; 16 | case '/': 17 | return left / right; 18 | default: 19 | throw new Error('Unknown operator ' + op); 20 | break; 21 | } 22 | } 23 | return node; 24 | }; 25 | 26 | return eval(parse(expression)); 27 | } 28 | 29 | module.exports = calc; 30 | -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | const TOKEN = require('./tokens'); 2 | const tokenize = require('./tokenizer'); 3 | 4 | function parse(expression) { 5 | const tokens = tokenize(expression); 6 | 7 | const next = () => tokens.shift(); 8 | 9 | const match = (type) => tokens.length > 0 && tokens[0].type === type; 10 | 11 | const expect = (type) => { 12 | if (!match(type)) { 13 | throw new Error('Unexpected token'); 14 | } 15 | next(); 16 | }; 17 | 18 | // Group ::= "(" Expression ")" 19 | const parseGroup = () => { 20 | expect(TOKEN.OpenParenthesis); 21 | const expr = parseExpression(); 22 | expect(TOKEN.CloseParenthesis); 23 | return expr; 24 | }; 25 | 26 | // Primary ::= Number | Group 27 | const parsePrimary = () => { 28 | if (match(TOKEN.OpenParenthesis)) { 29 | return parseGroup(); 30 | } 31 | const token = next(); 32 | if (!token) { 33 | throw new Error('Unexpected end of input'); 34 | } 35 | const { type, start, end } = token; 36 | if (type !== TOKEN.Number) { 37 | throw new Error('Unexpected end of input'); 38 | } 39 | const text = expression.substring(start, end); 40 | return parseFloat(text); 41 | }; 42 | 43 | // Unary ::= Primary | 44 | // "+" Unary | 45 | // "-" Unary 46 | const parseUnary = () => { 47 | if (match(TOKEN.Plus) || match(TOKEN.Minus)) { 48 | const { type } = next(); 49 | const expr = parsePrimary(); // FIXME BUG 50 | return type === TOKEN.Minus ? ['-', expr] : expr; 51 | } 52 | return parsePrimary(); 53 | }; 54 | 55 | // Multiplicative ::= Unary | 56 | // Multiplicative "*"" Unary | 57 | // Multiplicative "/" Unary 58 | const parseMultiplicative = () => { 59 | let expr = parseUnary(); 60 | while (match(TOKEN.Star) || match(TOKEN.Slash)) { 61 | const { type } = next(); 62 | const op = type === TOKEN.Star ? '*' : '/'; 63 | expr = [op, expr, parseUnary()]; 64 | } 65 | return expr; 66 | }; 67 | 68 | // Additive ::= Multiplicative | 69 | // Additive "+" Multiplicative 70 | // Additive "-" Multiplicative 71 | const parseAdditive = () => { 72 | let expr = parseMultiplicative(); 73 | while (match(TOKEN.Plus) || match(TOKEN.Minus)) { 74 | const { type } = next(); 75 | const op = type === TOKEN.Plus ? '+' : '-'; 76 | expr = [op, expr, parseMultiplicative()]; 77 | } 78 | return expr; 79 | }; 80 | 81 | const parseExpression = () => parseAdditive(); 82 | 83 | // console.log('parsing', expression, JSON.stringify(tokens)) 84 | 85 | return parseExpression(); 86 | } 87 | 88 | module.exports = parse; -------------------------------------------------------------------------------- /src/tokenizer.js: -------------------------------------------------------------------------------- 1 | const TOKEN = require('../src/tokens'); 2 | 3 | const isSpace = (code) => code === 9 || code === 10 || code === 13 || code === 32; 4 | const isDigit = (code) => code >= 0x30 && code <= 0x39; // 0..9 5 | 6 | function tokenize(expression = '') { 7 | const input = expression; 8 | const length = expression.length; 9 | let index = 0; 10 | 11 | const skipSpaces = () => { 12 | while (index < length) { 13 | const code = input.charCodeAt(index); 14 | if (!isSpace(code)) { 15 | break; 16 | } 17 | ++index; 18 | } 19 | }; 20 | 21 | const scanOperator = () => { 22 | const start = index; 23 | const ch = input[start]; 24 | let type = null; 25 | 26 | switch (ch) { 27 | case '(': 28 | type = TOKEN.OpenParenthesis; 29 | ++index; 30 | break; 31 | case ')': 32 | type = TOKEN.CloseParenthesis; 33 | ++index; 34 | break; 35 | case '+': 36 | type = TOKEN.Plus; 37 | ++index; 38 | break; 39 | case '-': 40 | type = TOKEN.Minus; 41 | ++index; 42 | break; 43 | case '*': 44 | type = TOKEN.Star; 45 | ++index; 46 | break; 47 | case '/': 48 | type = TOKEN.Slash; 49 | ++index; 50 | break; 51 | default: 52 | break; 53 | } 54 | const end = index; 55 | 56 | return type ? { type, start, end } : null; 57 | }; 58 | 59 | const scanNumber = () => { 60 | const start = index; 61 | while (index < length) { 62 | const code = input.charCodeAt(index); 63 | if (!isDigit(code)) { 64 | break; 65 | } 66 | ++index; 67 | } 68 | 69 | if (input[index] === '.') { 70 | ++index; 71 | while (index < length) { 72 | const code = input.charCodeAt(index); 73 | if (!isDigit(code)) { 74 | break; 75 | } 76 | ++index; 77 | } 78 | } else if (index <= start) { 79 | return null; 80 | } 81 | 82 | const type = TOKEN.Number; 83 | const end = index; 84 | return { type, start, end }; 85 | }; 86 | 87 | const main = () => { 88 | const tokens = []; 89 | while (index < length) { 90 | skipSpaces(); 91 | let token = scanOperator(); 92 | if (!token) { 93 | token = scanNumber(); 94 | } 95 | 96 | if (token) { 97 | tokens.push(token); 98 | } else { 99 | const char = input[index]; 100 | if (!char) { 101 | break; 102 | } 103 | throw new Error('Invalid character: ' + char); 104 | } 105 | } 106 | return tokens; 107 | }; 108 | 109 | return main(); 110 | } 111 | 112 | module.exports = tokenize; 113 | -------------------------------------------------------------------------------- /src/tokens.js: -------------------------------------------------------------------------------- 1 | const TOKEN = { 2 | EOF: 0, 3 | Number: 1, 4 | Identifier: 2, 5 | OpenParenthesis: 3, 6 | CloseParenthesis: 4, 7 | Plus: 5, 8 | Minus: 6, 9 | Star: 7, 10 | Slash: 8 11 | }; 12 | 13 | module.exports = TOKEN; 14 | --------------------------------------------------------------------------------