From 510d25d2b1845f9ffd1823b67bee97784e6dc3a1 Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Sun, 27 Oct 2024 05:12:19 +0000 Subject: [PATCH] router v2 proof of concept implemented --- benchmark/compiler/utils.mjs | 8 +- benchmark/const.js | 15 +- benchmark/ifs.mjs | 148 ++++++++++++ benchmark/ifs_compile.mjs | 85 +++++++ benchmark/map_query.mjs | 106 +++++++++ benchmark/router_flaska_v2.mjs | 96 ++++++++ benchmark/test.mjs | 33 +++ router_v2.mjs | 420 +++++++++++++++++++++++++++++++++ 8 files changed, 908 insertions(+), 3 deletions(-) create mode 100644 benchmark/ifs.mjs create mode 100644 benchmark/ifs_compile.mjs create mode 100644 benchmark/map_query.mjs create mode 100644 benchmark/router_flaska_v2.mjs create mode 100644 benchmark/test.mjs create mode 100644 router_v2.mjs diff --git a/benchmark/compiler/utils.mjs b/benchmark/compiler/utils.mjs index cbaf750..8ac58d8 100644 --- a/benchmark/compiler/utils.mjs +++ b/benchmark/compiler/utils.mjs @@ -7,9 +7,15 @@ export function printTree(children, indent = 0) { } } +export function printIfNotEightyOne(fn) { + let opt = %GetOptimizationStatus(fn) + if (opt === 81) return + printCurrentStatus() +} + export function printCurrentStatus(fn) { let opt = %GetOptimizationStatus(fn) - console.log(`${opt.toString(2).padStart(17, '0').split('').join(' ')}`) + console.log(`${opt.toString(2).padStart(17, '0').split('').join(' ')} (${opt})`) } export function printStatusHelperText() { console.log(`┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ diff --git a/benchmark/const.js b/benchmark/const.js index 06873e4..2539830 100644 --- a/benchmark/const.js +++ b/benchmark/const.js @@ -6,6 +6,15 @@ export const overrideDummy = function(override) { export const dummy = function() { callItem() } +export const testRoutes = [ + // '/api/articles/:id/file/:fileId', + '/', + // '/api/articles', + // '/api/articles/:id/file', + // '/api/articles/:id', + '/::rest', +] + export const allRoutes = [ '/', '/api/articles', @@ -23,6 +32,7 @@ export const allRoutes = [ '/api/pages/:id/articles/public', '/api/staff', '/api/staff/:id', + '/::rest', ] export const allManyRoutes = [ @@ -37,7 +47,7 @@ export const allManyRoutes = [ '/api/categories/:categoryId/properties', '/api/categories/:categoryId/values/:props', '/api/categories/:categoryId', - '/api/categories/:categoryId/products/:productId', + //'/api/categories/:categoryId/products/:productId', '/api/categories/:categoryId/products/:productId', '/api/customers', '/api/customers/:id', @@ -61,7 +71,7 @@ export const allManyRoutes = [ '/api/products/:id', '/api/products/:id/movement', '/api/products/:id/sub_products/:productId', - '/api/products/:id/sub_products/:productId', + //'/api/products/:id/sub_products/:productId', '/api/products/code/:code', '/api/products/property/:propertyId', '/api/properties', @@ -84,4 +94,5 @@ export const allManyRoutes = [ '/api/works/public', '/api/staff', '/api/staff/:id', + '/::rest', ] diff --git a/benchmark/ifs.mjs b/benchmark/ifs.mjs new file mode 100644 index 0000000..f5666c3 --- /dev/null +++ b/benchmark/ifs.mjs @@ -0,0 +1,148 @@ +import { summary, run, bench } from 'mitata'; + +function printCurrentStatus(fn) { + let opt = %GetOptimizationStatus(fn) + console.log(`${opt.toString(2).padStart(17, '0').split('').join(' ')} (${opt}) ${fn.name}`) +} +function printStatusHelperText() { + console.log(`┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ +│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─╸ is function +│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └───╸ is never optimized +│ │ │ │ │ │ │ │ │ │ │ │ │ │ └─────╸ is always optimized +│ │ │ │ │ │ │ │ │ │ │ │ │ └───────╸ is maybe deoptimized +│ │ │ │ │ │ │ │ │ │ │ │ └─────────╸ is optimized +│ │ │ │ │ │ │ │ │ │ │ └───────────╸ is optimized by TurboFan +│ │ │ │ │ │ │ │ │ │ └─────────────╸ is interpreted +│ │ │ │ │ │ │ │ │ └───────────────╸ is marked for optimization +│ │ │ │ │ │ │ │ └─────────────────╸ is marked for concurrent optimization +│ │ │ │ │ │ │ └───────────────────╸ is optimizing concurrently +│ │ │ │ │ │ └─────────────────────╸ is executing +│ │ │ │ │ └───────────────────────╸ topmost frame is turbo fanned +│ │ │ │ └─────────────────────────╸ lite mode +│ │ │ └───────────────────────────╸ marked for deoptimization +│ │ └─────────────────────────────╸ baseline +│ └───────────────────────────────╸ topmost frame is interpreted +└─────────────────────────────────╸ topmost frame is baseline`) +} + +// Warmup (de-optimize `bench()` calls) +bench('noop', () => { }); +bench('noop2', () => { }); + +function ifSingle(a) { + if (a[0] === 97 && a[1] === 97 && a[2] === 97 && a[3] === 97 && a.length === 4) { + return 100 + } + return 10 +} + +function ifChain(a) { + if (a[0] === 97) + if (a[1] === 97) + if (a[2] === 97) + if (a[3] === 97) + if (a.length === 4) { + return 100 + } + return 10 +} + +function ifSingleOpt(a) { + if (a.length >= 4 && a[0] === 97 && a[1] === 97 && a[2] === 97 && a[3] === 97 && a.length === 4) { + return 100 + } + return 10 +} + +function ifChainOpt(a) { + if (a.length >= 4) + if (a[0] === 97) + if (a[1] === 97) + if (a[2] === 97) + if (a[3] === 97) + if (a.length === 4) { + return 100 + } + return 10 +} + +function ifSingleOptAlt(a) { + if (a.length >= 4 && a[0] == 97 && a[1] == 97 && a[2] == 97 && a[3] == 97 && a.length == 4) { + return 100 + } + return 10 +} + +function ifChainOptAlt(a) { + if (a.length >= 4) + if (a[0] == 97) + if (a[1] == 97) + if (a[2] == 97) + if (a[3] == 97) + if (a.length == 4) { + return 100 + } + return 10 +} + +let paths = [ + [97, 97, 97, 97], + [97, 97, 97, 97, 97], + [97, 97, 96, 97], + [97, 96, 97, 97], + [96, 97, 97, 97], + [], + [98], + [97, 97, 97], + [97, 97], + [97], + [97, 97, 96], + [97, 96], + [96], +] + +let paths2 = [ + [97, 97, 97, 97], + [97, 97, 97, 97, 97], + [97, 97, 96, 97], + [97, 96, 97, 97], + [96, 97, 97, 97], + [97, 97, 97, 97], + [97, 97, 97, 97, 97], + [97, 97, 96, 97], + [97, 96, 97, 97], + [96, 97, 97, 97], + [97, 97, 97, 97, 97, 97], + [97, 97, 97, 97, 96, 97], + [97, 97, 97, 97, 97, 96], +] + +let func1 = [ifSingle, ifChain, ifSingleOpt, ifChainOpt, ifSingleOptAlt, ifChainOptAlt]; +for (let fun of func1) { + console.log('-- begin', fun.name) + for (var i = 0; i < 1000000; i++) { + paths.map(fun) + } + printCurrentStatus(fun); +} +printStatusHelperText() + + +summary(() => { + func1.forEach(function(fun) { + bench(fun.name + ' first', function() { + return paths.map(fun) + }) + }) + func1.forEach(function(fun) { + bench(fun.name + ' second', function() { + return paths2.map(fun) + }) + }) +}) +run().then(function() { + for (let fun of func1) { + printCurrentStatus(fun); + } + printStatusHelperText() +}); diff --git a/benchmark/ifs_compile.mjs b/benchmark/ifs_compile.mjs new file mode 100644 index 0000000..b6553a0 --- /dev/null +++ b/benchmark/ifs_compile.mjs @@ -0,0 +1,85 @@ +import { printCurrentStatus, printStatusHelperText } from "./compiler/utils.mjs"; + +function ifSingle(a) { + if (a[0] === 97 && a[1] === 97 && a[2] === 97 && a[3] === 97 && a.length === 4) { + return 100 + } + return 10 +} + +function ifChain(a) { + if (a[0] === 97) + if (a[1] === 97) + if (a[2] === 97) + if (a[3] === 97) + if (a.length === 4) { + return 100 + } + return 10 +} + +function ifSingleOptimized(a) { + if (a.length >= 4 && a[0] === 97 && a[1] === 97 && a[2] === 97 && a[3] === 97 && a.length === 4) { + return 100 + } + return 10 +} + +function ifChainOptimized(a) { + if (a.length >= 4) + if (a[0] === 97) + if (a[1] === 97) + if (a[2] === 97) + if (a[3] === 97) + if (a.length === 4) { + return 100 + } + return 10 +} + +function ifSingleOptimizedAlt(a) { + if (a.length >= 4 && a[0] == 97 && a[1] == 97 && a[2] == 97 && a[3] == 97 && a.length == 4) { + return 100 + } + return 10 +} + +function ifChainOptimizedAlt(a) { + if (a.length >= 4) + if (a[0] == 97) + if (a[1] == 97) + if (a[2] == 97) + if (a[3] == 97) + if (a.length == 4) { + return 100 + } + return 10 +} + +let paths = [ + [97, 97, 97, 97], + [97, 97, 97, 97, 97], + [97, 97, 96, 97], + [97, 96, 97, 97], + [96, 97, 97, 97], + [], + [98], + [97, 97, 97], + [97, 97], + [97], + [97, 97, 96], + [97, 96], + [96], +] + +let func1 = [ifSingle, ifChain, ifSingleOptimized, ifChainOptimized, ifSingleOptimizedAlt, ifChainOptimizedAlt]; +for (let fun of func1) { + console.log('-- begin', fun.name) + for (var i = 0; i < 1000000; i++) { + paths.map(x => fun(x)) + } + printCurrentStatus(fun); +} +printStatusHelperText() + + diff --git a/benchmark/map_query.mjs b/benchmark/map_query.mjs new file mode 100644 index 0000000..4a458f5 --- /dev/null +++ b/benchmark/map_query.mjs @@ -0,0 +1,106 @@ +import { summary, run, bench } from 'mitata'; + +function printCurrentStatus(fn) { + let opt = %GetOptimizationStatus(fn) + console.log(`${opt.toString(2).padStart(17, '0').split('').join(' ')} (${opt}) ${fn.name}`) +} +function printStatusHelperText() { + console.log(`┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ +│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─╸ is function +│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └───╸ is never optimized +│ │ │ │ │ │ │ │ │ │ │ │ │ │ └─────╸ is always optimized +│ │ │ │ │ │ │ │ │ │ │ │ │ └───────╸ is maybe deoptimized +│ │ │ │ │ │ │ │ │ │ │ │ └─────────╸ is optimized +│ │ │ │ │ │ │ │ │ │ │ └───────────╸ is optimized by TurboFan +│ │ │ │ │ │ │ │ │ │ └─────────────╸ is interpreted +│ │ │ │ │ │ │ │ │ └───────────────╸ is marked for optimization +│ │ │ │ │ │ │ │ └─────────────────╸ is marked for concurrent optimization +│ │ │ │ │ │ │ └───────────────────╸ is optimizing concurrently +│ │ │ │ │ │ └─────────────────────╸ is executing +│ │ │ │ │ └───────────────────────╸ topmost frame is turbo fanned +│ │ │ │ └─────────────────────────╸ lite mode +│ │ │ └───────────────────────────╸ marked for deoptimization +│ │ └─────────────────────────────╸ baseline +│ └───────────────────────────────╸ topmost frame is interpreted +└─────────────────────────────────╸ topmost frame is baseline`) +} + +// Warmup (de-optimize `bench()` calls) +bench('noop', () => { }); +bench('noop2', () => { }); + +function mapGetAndCheck(map, t) { + let x = map.get(t) + if (x) return x + return t +} + +function mapCheckBeforeGet(map, t) { + if (map.has(t)) { + return map.get(t) + } + return t +} + +let paths = [ + 'a', + 'aa', + 'aaa', + 'aaaa', + 'aaaaa', + 'aaaaaa', + 'aaaaaaa', + 'aaaaaaaa', +] +let tests = [ + 'a', + 'aa', + 'aaa', + 'aaaa', + 'aaaaa', + 'aaaaaa', + 'aaaaaaa', + 'aaaaaaaa', + 'b', + 'bb', + 'bbb', + 'bbbb', + 'bbbbb', + 'bbbbbb', + 'bbbbbbb', + 'bbbbbbbb', + 'c', + 'cc', + 'ccc', + 'cccc', + 'ccccc', + 'cccccc', + 'ccccccc', + 'cccccccc', +] + +let map = new Map(paths.map(x => [x, { a: x }])) + +let func1 = [mapGetAndCheck, mapCheckBeforeGet]; +for (let fun of func1) { + console.log('-- begin', fun.name) + for (var i = 0; i < 1000000; i++) { + tests.map(t => fun(map, t)) + } + printCurrentStatus(fun); +} +printStatusHelperText() + +summary(() => { + func1.forEach(function(fun) { + bench(fun.name, function() { + return paths.map(t => fun(map, t)) + }) + }) +}) +run().then(function() { + for (let fun of func1) { + printCurrentStatus(fun); + } + printStatusHelperText() +}); diff --git a/benchmark/router_flaska_v2.mjs b/benchmark/router_flaska_v2.mjs new file mode 100644 index 0000000..439b0a7 --- /dev/null +++ b/benchmark/router_flaska_v2.mjs @@ -0,0 +1,96 @@ +import { summary, run, bench } from 'mitata'; +import assert from 'assert' +import { compilePaths } from "../router_v2.mjs" +import * as consts from './const.js' + +function printCurrentStatus(fn) { + // console.log(`--- checking optimizations status on ${fn.name} ---`) + // let opt = %GetOptimizationStatus(fn) + // console.log(`${opt.toString(2).padStart(17, '0').split('').join(' ')} (${opt}) ${fn.name}`) +} +function printStatusHelperText() { + console.log(`┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ +│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─╸ is function +│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └───╸ is never optimized +│ │ │ │ │ │ │ │ │ │ │ │ │ │ └─────╸ is always optimized +│ │ │ │ │ │ │ │ │ │ │ │ │ └───────╸ is maybe deoptimized +│ │ │ │ │ │ │ │ │ │ │ │ └─────────╸ is optimized +│ │ │ │ │ │ │ │ │ │ │ └───────────╸ is optimized by TurboFan +│ │ │ │ │ │ │ │ │ │ └─────────────╸ is interpreted +│ │ │ │ │ │ │ │ │ └───────────────╸ is marked for optimization +│ │ │ │ │ │ │ │ └─────────────────╸ is marked for concurrent optimization +│ │ │ │ │ │ │ └───────────────────╸ is optimizing concurrently +│ │ │ │ │ │ └─────────────────────╸ is executing +│ │ │ │ │ └───────────────────────╸ topmost frame is turbo fanned +│ │ │ │ └─────────────────────────╸ lite mode +│ │ │ └───────────────────────────╸ marked for deoptimization +│ │ └─────────────────────────────╸ baseline +│ └───────────────────────────────╸ topmost frame is interpreted +└─────────────────────────────────╸ topmost frame is baseline`) +} + +// Warmup (de-optimize `bench()` calls) +bench('noop', () => { }); +bench('noop2', () => { }); + +let paths = [ + { path: '/aa/aa', }, + { path: '/aa/:blabla', }, + { path: '/aa/:blabla/aa', }, + { path: '/aa/:blabla/ab', }, + { path: '/aa/:blabla/bb', }, + { path: '/::rest', }, +] + +paths = consts.allManyRoutes.map(x => ({ path: x })) + +let tests = [ + ['/', paths[5]], + ['/aa', paths[5]], + ['/aa/aa', paths[0]], + ['/aa/_', paths[1]], + ['/aa/_/aa', paths[2]], + ['/aa/_/ab', paths[3]], + ['/aa/_/bb', paths[4]], +] + +tests = paths.map(p => ([p.path.replace(/:[^/]+/g, '_'), p])) +let testStrings = tests.map(x => x[0]) + +let func = compilePaths(paths) +for (let [_, fun] of func) { + console.log(`--- Sanity checking ${fun.name} ---`) + for (let test of tests) { + let check = fun(test[0]) + // console.log(test[0], check) + assert.strictEqual(check.path, test[1]) + } +} + +for (let [_, fun] of func) { + console.log(`--- warming up ${fun.name} ---`) + for (var i = 0; i < 10000; i++) { + testStrings.forEach(fun) + } +} +console.log('--- sleeping ---') +await new Promise(res => setTimeout(res, 1000)) +for (let [org] of func) { + printCurrentStatus(org); +} +printStatusHelperText() + + +summary(() => { + func.forEach(function([_, fun]) { + bench(fun.name.slice(6), function() { + return testStrings.map(fun) + }) + }) +}) +run().then(function() { + for (let [check] of func) { + printCurrentStatus(check); + } + printStatusHelperText() +}); \ No newline at end of file diff --git a/benchmark/test.mjs b/benchmark/test.mjs new file mode 100644 index 0000000..7227824 --- /dev/null +++ b/benchmark/test.mjs @@ -0,0 +1,33 @@ +import assert from 'assert' +import { compilePaths } from "../router_v2.mjs" +import * as consts from './const.js' + +let paths = [ + { path: '/aa/aa', }, + { path: '/aa/:blabla', }, + { path: '/::rest', }, +] + +// paths = consts.allManyRoutes.map(x => ({ path: x })) + +let tests = [ + ['/', paths[5]], + ['/aa', paths[5]], + ['/aa/aa', paths[0]], + ['/aa/_', paths[1]], + ['/aa/_/aa', paths[2]], + ['/aa/_/ab', paths[3]], + ['/aa/_/bb', paths[4]], +] + +tests = paths.map(p => ([p.path.replace(/:[^/]+/g, '_'), p])) + +let func = compilePaths(paths) +for (let [_, fun] of func) { + console.log(`--- ${fun.name} ---`) + for (let test of tests) { + let check = fun(test[0]) + console.log(test[0], check) + assert.strictEqual(check.path, test[1]) + } +} diff --git a/router_v2.mjs b/router_v2.mjs new file mode 100644 index 0000000..014eb40 --- /dev/null +++ b/router_v2.mjs @@ -0,0 +1,420 @@ +const Debug = false + +export function printTree(children, indent = 0) { + if (!children.length || !Debug) return + + for (let child of children) { + console.log(''.padStart(indent * 2) + (child.char || '*') + + ' ' + (child.isParams ? `{ params = ${child.isParams} }` : '') + + (child.isFullParams ? `{ fullParams = ${child.isFullParams} }` : '') + + ' ' + (child.path ? `{ handler = ${child.path.path} }` : '')) + printTree(child.children, indent + 1) + } +} + +class RouterError extends Error { + constructor(route1, route2, ...params) { + // Pass remaining arguments (including vendor specific ones) to parent constructor + super(...params); + + // Maintains proper stack trace for where our error was thrown (only available on V8) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, RouterError); + } + + this.name = "RouterError"; + this.routeA = route1 + this.routeB = route2 + } +} + +function Child(split, x, i) { + this.path = null + this.isParams = split[x].isParams ? split[x].word : null + this.isFullParams = split[x].isFullParams ? split[x].word : null + this.paramVarName = split[x].paramVarName ?? null + this.char = !this.isParams && !this.isFullParams ? split[x].word[i] || '/' : null + this.count = 0 + this.children = [] +} + +function buildChild(x, i, splitPaths) { + let splitPath = splitPaths[0] + let letter = new Child(splitPath.split, x, i) + + let consume = [] + if (splitPath.split.length === x + 1 + && (splitPath.split[x].isParams + || splitPath.split[x].isFullParams + || splitPath.split[x].word.length === i + 1)) { + letter.path = splitPath.entry + letter.count += 1 + } else { + consume = [splitPath] + } + + for (let y = 1; y < splitPaths.length; y++) { + let checkPath = splitPaths[y] + if (!checkPath.split[x] + || checkPath.split[x].isParams !== splitPath.split[x].isParams + || checkPath.split[x].isFullParams !== splitPath.split[x].isFullParams + || !checkPath.split[x].isParams + && !checkPath.split[x].isFullParams + && (checkPath.split[x].word[i] || '/') !== letter.char) break + consume.push(checkPath) + } + + letter.count += consume.length + if (splitPath.split[x].word.length === i || splitPath.split[x].isParams || splitPath.split[x].isFullParams) { + x++ + i = -1 + } + while (consume.length) { + letter.children.push(buildChild(x, i + 1, consume)) + consume.splice(0, letter.children[letter.children.length - 1].count) + } + return letter +} + +export function buildTree(splitPaths) { + let builder = [] + while (splitPaths.length) { + builder.push(buildChild(0, 0, splitPaths)) + splitPaths.splice(0, builder[builder.length - 1].count) + } + return builder +} + +const regParamPrefix = /^::?/ +const regCleanNonAschii = /(?![a-zA-Z_])./g +const regCleanRest = /_+/g + +function splitAndSortPaths(paths, separateStatic = true) { + let staticPaths = new Map() + let paramsPaths = [] + let collator = new Intl.Collator('en', { sensitivity: 'accent' }); + + paths.forEach(function(entry) { + if (entry.path[0] !== '/') throw new RouterError(entry, null, 'Specified route was missing forward slash at start') + + // Collect static paths separately + if (entry.path.indexOf('/:') < 0 && separateStatic) { + return staticPaths.set(entry.path, { + path: entry, + params: {} + }) + } + + // Collect params path separately + paramsPaths.push({ + split: entry.path.slice(1).split(/\//g).map(function(word) { + let actualWord = word.replace(regParamPrefix, '') + return { + word: actualWord, + isParams: word[0] === ':' && word[1] !== ':', + isFullParams: word[0] === ':' && word[1] === ':', + paramVarName: word[0] === ':' + ? actualWord.replace(regCleanNonAschii, '_').replace(regCleanRest, '_') + : null + } + }), + entry, + }) + }) + + paramsPaths.sort(function(aGroup, bGroup) { + let length = Math.max(aGroup.split.length, bGroup.split.length) + for (let x = 0; x < length; x++) { + let a = aGroup.split[x] + let b = bGroup.split[x] + if (!a) return -1 + if (!b) return 1 + // Full params go last + if (a.isFullParams && b.isFullParams) throw new RouterError(aGroup.entry, bGroup.entry, 'Two full path routes found on same level') + if (a.isFullParams) return 1 + if (b.isFullParams) return -1 + // Params go second last + if (a.isParams && !b.isParams) return 1 + if (!a.isParams && b.isParams) return -1 + // otherwise sort alphabetically if not identical + if (a.word !== b.word) return collator.compare(a.word, b.word) + } + throw new RouterError(aGroup, bGroup, 'Two identical paths were found') + }) + + return { + staticPaths, + paramsPaths, + } +} + +const SlashCode = '/'.charCodeAt(0) + +function getIndex(offset, additions, params) { + return (offset + additions) + + (params.length + ? ' + ' + params.map(a => `offset${a[1]}`).join(' + ') + : '') +} + +function treeIntoCompiledTreeBufferReturnPath(indentString, paths, branch, params, alt) { + let pathIndex = paths.indexOf(branch.path) + if (pathIndex < 0) { + throw new RouterError(branch.path, null, 'InternalError: Specified path was not found in paths') + } + let output = '' + output += '\n' + indentString + `return {` + output += '\n' + indentString + ` path: paths[${pathIndex}],` + if (params.length) { + output += '\n' + indentString + ` params: {` + for (let param of params) { + if (alt === 0) { + output += '\n' + indentString + ` ${param[0]}: s${param[1]}.toString(),` + } else if (alt === 1 || alt === 3) { + output += '\n' + indentString + ` ${param[0]}: s${param[1]},` + } else if (alt === 2) { + output += '\n' + indentString + ` ${param[0]}: textDecoder.decode(s${param[1]}),` + } + } + output += '\n' + indentString + ` },` + } else { + output += '\n' + indentString + ` params: {},` + } + output += '\n' + indentString + `}` + return output +} + +function treeIntoCompiledTreeBufferBranch(paths, branches, indent = 0, params = [], alt = 0) { + let output = '' + let indentation = ''.padStart((indent - params.length) * 2) + let addEndBracket = true + + for (let i = 0; i < branches.length; i++) { + let branch = branches[i] + if (i > 0) { + if (!branch.isParams && !branch.isFullParams) { + output += ' else ' + } else { + // output += '} //' + output += '\n' + indentation + } + } + + if (!branch.isParams && !branch.isFullParams) { + output += `if (buf[${getIndex(indent, 0, params)}] === ${branch.char.charCodeAt(0)}) { // ${branch.char}` + + if (branch.path) { + output += '\n' + indentation + ` if (buf.length === ${getIndex(indent, 1, params)}) {` + output += treeIntoCompiledTreeBufferReturnPath(indentation + ' ', paths, branch, params, alt) + output += '\n' + indentation + ` }` + } + } else { + addEndBracket = false + let paramVarName = (params.length + 1) + '_' + branch.paramVarName + if (alt === 1 || alt === 3) { + output += `let s${paramVarName} = str.slice(${getIndex(indent, 0, params)}${branch.isFullParams ? '' : `, buf.indexOf(${SlashCode}, ${getIndex(indent, 0, params)}) >>> 0`})` + } else { + output += `let s${paramVarName} = buf.slice(${getIndex(indent, 0, params)}${branch.isFullParams ? '' : `, buf.indexOf(${SlashCode}, ${getIndex(indent, 0, params)}) >>> 0`})` + } + output += '\n' + indentation + `let offset${paramVarName} = s${paramVarName}.length` + output += '\n' + indentation + params.push([branch.isParams || branch.isFullParams, paramVarName]) + + if (branch.isFullParams) { + output += treeIntoCompiledTreeBufferReturnPath(indentation, paths, branch, params, alt) + } else if (branch.path) { + output += '\n' + indentation + `if (buf.length === ${getIndex(indent, 0, params)}) {` + output += treeIntoCompiledTreeBufferReturnPath(indentation + ' ', paths, branch, params, alt) + output += '\n' + indentation + `}` + } + } + + if (branch.children.length) { + if (branch.path) { + output += ' else ' + } else { + output += '\n' + indentation + ' ' + } + output += treeIntoCompiledTreeBufferBranch(paths, branch.children, indent + 1, params.slice()) + } + if (addEndBracket) { + output += '\n' + indentation + '} ' + } + } + return output +} + +export function treeIntoCompiledTreeBuffer(paths, tree) { + let output = 'return function RBuffer(paths, static, str) {' + output += '\n let checkStatic = static.get(str)' + output += '\n if(checkStatic) {' + output += '\n return checkStatic' + output += '\n }' + output += '\n var buf = Buffer.from(str)' + output += '\n ' + treeIntoCompiledTreeBufferBranch(paths, tree, 1) + output += '\n return null' + output += '\n}' + if (Debug) { + console.log(output) + } + return new Function(output)() +} + + +function treeIntoCompiledTreeStringReturnPath(indentString, paths, branch, params) { + let pathIndex = paths.indexOf(branch.path) + if (pathIndex < 0) { + throw new RouterError(branch.path, null, 'InternalError: Specified path was not found in paths') + } + let output = '' + output += '\n' + indentString + `return {` + output += '\n' + indentString + ` path: paths[${pathIndex}],` + if (params.length) { + output += '\n' + indentString + ` params: {` + for (let param of params) { + output += '\n' + indentString + ` ${param[0]}: s${param[1]},` + } + output += '\n' + indentString + ` },` + } else { + output += '\n' + indentString + ` params: {},` + } + output += '\n' + indentString + `}` + return output +} + +function treeIntoCompiledTreeStringBranch(paths, branches, indent = 0, params = []) { + let output = '' + let indentation = ''.padStart((indent - params.length) * 2) + let addEndBracket = true + + for (let i = 0; i < branches.length; i++) { + let branch = branches[i] + if (i > 0) { + if (!branch.isParams && !branch.isFullParams) { + output += ' else ' + } else { + // output += '} //' + output += '\n' + indentation + } + } + + if (!branch.isParams && !branch.isFullParams) { + output += `if (str.charCodeAt(${getIndex(indent, 0, params)}) === ${branch.char.charCodeAt(0)}) { // ${branch.char}` + + if (branch.path) { + output += '\n' + indentation + ` if (str.length === ${getIndex(indent, 1, params)}) {` + output += treeIntoCompiledTreeStringReturnPath(indentation + ' ', paths, branch, params) + output += '\n' + indentation + ` }` + } + } else { + addEndBracket = false + let paramVarName = (params.length + 1) + '_' + branch.paramVarName + output += `let s${paramVarName} = str.slice(${getIndex(indent, 0, params)}${branch.isFullParams ? '' : `, str.indexOf('/', ${getIndex(indent, 0, params)}) >>> 0`})` + output += '\n' + indentation + `let offset${paramVarName} = s${paramVarName}.length` + output += '\n' + indentation + params.push([branch.isParams || branch.isFullParams, paramVarName]) + + if (branch.isFullParams) { + output += treeIntoCompiledTreeStringReturnPath(indentation, paths, branch, params) + } else if (branch.path) { + output += '\n' + indentation + `if (str.length === ${getIndex(indent, 0, params)}) {` + output += treeIntoCompiledTreeStringReturnPath(indentation + ' ', paths, branch, params) + output += '\n' + indentation + `}` + } + } + + if (branch.children.length) { + if (branch.path) { + output += ' else ' + } else { + output += '\n' + indentation + ' ' + } + output += treeIntoCompiledTreeStringBranch(paths, branch.children, indent + 1, params.slice()) + } + if (addEndBracket) { + output += '\n' + indentation + '} ' + } + } + return output +} + +export function treeIntoCompiledTreeString(paths, tree) { + let output = 'return function RString(paths, static, str) {' + output += '\n let checkStatic = static.get(str)' + output += '\n if(checkStatic) {' + output += '\n return checkStatic' + output += '\n }' + output += '\n ' + treeIntoCompiledTreeStringBranch(paths, tree, 1) + output += '\n return null' + output += '\n}' + if (Debug) { + console.log(output) + } + return new Function(output)() +} + +export function treeIntoCompiledTreeBufferAltOne(paths, tree) { + let output = 'return function RBufferStrSlice(paths, static, str) {' + output += '\n let checkStatic = static.get(str)' + output += '\n if(checkStatic) {' + output += '\n return checkStatic' + output += '\n }' + output += '\n var buf = Buffer.from(str)' + output += '\n ' + treeIntoCompiledTreeBufferBranch(paths, tree, 1, [], 1) + output += '\n return null' + output += '\n}' + if (Debug) { + console.log(output) + } + return new Function(output)() +} + +export function treeIntoCompiledTreeBufferAltTwo(paths, tree) { + let output = 'return function RTextEncoderDecoder(textEncoder, textDecoder, paths, static, str) {' + output += '\n let checkStatic = static.get(str)' + output += '\n if(checkStatic) {' + output += '\n return checkStatic' + output += '\n }' + output += '\n var buf = textEncoder.encode(str)' + output += '\n ' + treeIntoCompiledTreeBufferBranch(paths, tree, 1, [], 2) + output += '\n return null' + output += '\n}' + if (Debug) { + console.log(output) + } + return new Function(output)() +} + +export function treeIntoCompiledTreeBufferAltThree(paths, tree) { + let output = 'return function RTextEncoderStrSlice(textEncoder, paths, static, str) {' + output += '\n let checkStatic = static.get(str)' + output += '\n if(checkStatic) {' + output += '\n return checkStatic' + output += '\n }' + output += '\n var buf = textEncoder.encode(str)' + output += '\n ' + treeIntoCompiledTreeBufferBranch(paths, tree, 1, [], 3) + output += '\n return null' + output += '\n}' + if (Debug) { + console.log(output) + } + return new Function(output)() +} + +export function compilePaths(paths) { + let splitPaths = splitAndSortPaths(paths) + let tree = buildTree(splitPaths.paramsPaths.slice()) + printTree(tree, 0) + let compiled = treeIntoCompiledTreeBuffer(paths, tree) + let compiledString = treeIntoCompiledTreeString(paths, tree) + let compiledTextOne = treeIntoCompiledTreeBufferAltOne(paths, tree) + let compiledTextTwo = treeIntoCompiledTreeBufferAltTwo(paths, tree) + let compiledTextThree = treeIntoCompiledTreeBufferAltThree(paths, tree) + return [ + [compiled, compiled.bind(null, paths, splitPaths.staticPaths)], + [compiledTextOne, compiledTextOne.bind(null, paths, splitPaths.staticPaths)], + [compiledTextTwo, compiledTextTwo.bind(null, new TextEncoder(), new TextDecoder(), paths, splitPaths.staticPaths)], + [compiledTextThree, compiledTextThree.bind(null, new TextEncoder(), paths, splitPaths.staticPaths)], + [compiledString, compiledString.bind(null, paths, splitPaths.staticPaths)], + ] +} \ No newline at end of file