├── .babelrc ├── .gitignore ├── .prettierignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── patches ├── python32_debug.diff ├── smooth-scrollbar+8.3.1.patch └── subscribe-ui-event+2.0.4.patch ├── python_code ├── actual_dict_factory_test.py ├── build_autogenerated_chapter1_hash.py ├── build_autogenerated_chapter2.py ├── build_autogenerated_chapter3_chapter4.py ├── chapter1_linear_search_reimplementation_test.py ├── chapter4_probing_python_reimplementation_test.py ├── common.py ├── dict32_reimplementation_test_v2.py ├── dict_reimpl_common.py ├── dict_reimplementation.py ├── dictinfo.py ├── dictinfo32.py ├── dictinfo33.py ├── hash_chapter1_impl.py ├── hash_chapter1_reimpl_js.py ├── hash_chapter1_reimplementation_test.py ├── hash_chapter2_impl.py ├── hash_chapter2_impl_test.py ├── hash_chapter2_reimpl_js.py ├── hash_chapter2_reimplementation_test.py ├── hash_chapter3_class_impl.py ├── hash_chapter3_class_impl_test.py ├── interface_test.py ├── js_reimpl_common.py └── js_reimplementation_interface.py ├── scripts ├── extractPythonCode.js ├── pyReimplWrapper.js └── ssr.js ├── src ├── app.js ├── autogenerated │ ├── chapter1.html │ ├── chapter2.html │ ├── chapter3.html │ └── chapter4.html ├── chapter1_simplified_hash.js ├── chapter2_hash_table_functions.js ├── chapter3_and_4_common.js ├── chapter3_hash_class.js ├── chapter4_real_python_dict.js ├── code_blocks.js ├── common_formatters.js ├── hash_impl_common.js ├── hash_impl_common.test.js ├── index.js ├── inputs.js ├── mustache │ ├── chapter1.json │ ├── chapter2.json │ ├── chapter3.json │ └── chapter4.json ├── page.html.template ├── probing_visualization.js ├── py_obj_parsing.js ├── py_obj_parsing.test.js ├── store.js ├── styles.css └── util.js ├── ssr-all.sh ├── stress_test_python.sh ├── unittest_python.sh ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "@babel/preset-react" 10 | ], 11 | "plugins": [ 12 | [ 13 | "@babel/plugin-proposal-decorators", 14 | { 15 | "legacy": true 16 | } 17 | ], 18 | "@babel/plugin-proposal-class-properties", 19 | "@babel/plugin-syntax-dynamic-import", 20 | "@babel/plugin-syntax-import-meta", 21 | "@babel/plugin-proposal-json-strings", 22 | "@babel/plugin-proposal-function-sent", 23 | "@babel/plugin-proposal-export-namespace-from", 24 | "@babel/plugin-proposal-numeric-separator", 25 | "@babel/plugin-proposal-throw-expressions", 26 | "@babel/plugin-proposal-optional-chaining" 27 | ], 28 | "env": { 29 | "test": { 30 | "presets": [ 31 | [ 32 | "@babel/preset-env", 33 | { 34 | "modules": "auto" 35 | } 36 | ], 37 | "@babel/preset-react" 38 | ] 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### PYTHON 2 | # 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | .static_storage/ 58 | .media/ 59 | local_settings.py 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | 108 | 109 | ### NODE 110 | # Logs 111 | logs 112 | *.log 113 | npm-debug.log* 114 | yarn-debug.log* 115 | yarn-error.log* 116 | 117 | # Runtime data 118 | pids 119 | *.pid 120 | *.seed 121 | *.pid.lock 122 | 123 | # Directory for instrumented libs generated by jscoverage/JSCover 124 | lib-cov 125 | 126 | # Coverage directory used by tools like istanbul 127 | coverage 128 | 129 | # nyc test coverage 130 | .nyc_output 131 | 132 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 133 | .grunt 134 | 135 | # Bower dependency directory (https://bower.io/) 136 | bower_components 137 | 138 | # node-waf configuration 139 | .lock-wscript 140 | 141 | # Compiled binary addons (https://nodejs.org/api/addons.html) 142 | build/Release 143 | 144 | # Dependency directories 145 | node_modules/ 146 | jspm_packages/ 147 | 148 | # Typescript v1 declaration files 149 | typings/ 150 | 151 | # Optional npm cache directory 152 | .npm 153 | 154 | # Optional eslint cache 155 | .eslintcache 156 | 157 | # Optional REPL history 158 | .node_repl_history 159 | 160 | # Output of 'npm pack' 161 | *.tgz 162 | 163 | # Yarn Integrity file 164 | .yarn-integrity 165 | 166 | # dotenv environment variables file 167 | .env 168 | 169 | # next.js build output 170 | .next 171 | 172 | # vim custom added 173 | *.swp 174 | *.swo 175 | 176 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | package.json 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2018 Alexander Putilin 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 | # Inside Python Dict - an explorable explanation 2 | 3 | This repository contains the code for ["Inside Python Dict"](https://just-taking-a-ride.com/inside_python_dict/), a explorable explanation of python dicts. 4 | 5 | ## Code 6 | 7 | The code is quite messy, the build system works on my laptop but might not work on your machine. That's all fixable, but I am not sure if there is any interest from people in using the actual code. If so, let me know, I'll clean things up a bit and write an overview of the codebase. 8 | 9 | Meanwhile, try running `npm install && npm start` and see if if works. 10 | 11 | Bugfixes will be gladly accepted. 12 | 13 | ### Disclaimer 14 | 15 | I am providing code in the repository to you under an open source license. Because this is my personal repository, the license you receive to my code is from me and not my employer (Facebook) 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inside_python_dict", 3 | "version": "1.0.0", 4 | "description": "Inside python dict - an explorable explanation", 5 | "main": "index.js", 6 | "scripts": { 7 | "jest": "jest --env=node", 8 | "test:pystress": "npm run extractcode && ./stress_test_python.sh", 9 | "test:pyunit": "npm run extractcode && ./unittest_python.sh", 10 | "test": "npm run jest && npm run test:pyunit && npm run test:pystress", 11 | "build:ssr": "./ssr-all.sh", 12 | "update:html": "mkdir -p build && (for i in {chapter1,chapter2,chapter3,chapter4}; do mustache src/mustache/$i.json src/page.html.template > src/autogenerated/$i.html; done)", 13 | "start": "webpack-dev-server --config webpack.dev.js --host 0.0.0.0", 14 | "serve": "http-server -p 9090 dist/", 15 | "build": "npm run build:ssr && webpack --config webpack.prod.js", 16 | "babel-node": "npx babel-node --presets '@babel/env'", 17 | "extractcode": "mkdir -p build && npm run babel-node scripts/extractPythonCode.js", 18 | "dictserver": "rm pynode.sock ; npm run babel-node scripts/pyReimplWrapper.js; rm pynode.sock", 19 | "postinstall": "patch-package" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/eleweek/inside_python_dict.git" 24 | }, 25 | "author": "Alexander Putilin", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/eleweek/inside_python_dict/issues" 29 | }, 30 | "homepage": "https://github.com/eleweek/inside_python_dict#readme", 31 | "prettier": { 32 | "printWidth": 120, 33 | "tabWidth": 4, 34 | "useTabs": false, 35 | "singleQuote": true, 36 | "bracketSpacing": false, 37 | "semi": true, 38 | "trailingComma": "es5" 39 | }, 40 | "dependencies": { 41 | "@fortawesome/fontawesome-svg-core": "^1.2.10", 42 | "@fortawesome/free-brands-svg-icons": "^5.6.1", 43 | "@fortawesome/free-solid-svg-icons": "^5.6.1", 44 | "@fortawesome/react-fontawesome": "^0.1.3", 45 | "bignumber.js": "^8.0.1", 46 | "bootstrap": "^4.1.3", 47 | "bowser": "^2.0.0-beta.3", 48 | "classnames": "^2.2.6", 49 | "d3": "^5.7.0", 50 | "d3-selection-multi": "^1.0.1", 51 | "i": "^0.3.6", 52 | "immutable": "^4.0.0-rc.12", 53 | "lodash": "^4.17.11", 54 | "lowlight": "^1.11.0", 55 | "memoize-one": "^4.1.0", 56 | "mobx": "^5.8.0", 57 | "mobx-react": "^5.4.3", 58 | "rc-slider": "^8.6.4", 59 | "react": "^16.6.3", 60 | "react-css-transition-replace": "^3.0.3", 61 | "react-dom": "^16.6.3", 62 | "react-error-boundary": "^1.2.3", 63 | "react-input-autosize": "^2.2.1", 64 | "react-popper": "^1.3.2", 65 | "react-smooth-scrollbar": "^8.0.6", 66 | "react-stickynode": "^2.1.0", 67 | "rehype": "^7.0.0", 68 | "smooth-scrollbar": "8.3.1" 69 | }, 70 | "devDependencies": { 71 | "@babel/cli": "^7.2.0", 72 | "@babel/core": "^7.2.2", 73 | "@babel/node": "^7.2.2", 74 | "@babel/plugin-proposal-class-properties": "^7.2.1", 75 | "@babel/plugin-proposal-decorators": "^7.2.2", 76 | "@babel/plugin-proposal-export-namespace-from": "^7.2.0", 77 | "@babel/plugin-proposal-function-sent": "^7.2.0", 78 | "@babel/plugin-proposal-json-strings": "^7.2.0", 79 | "@babel/plugin-proposal-numeric-separator": "^7.2.0", 80 | "@babel/plugin-proposal-object-rest-spread": "^7.2.0", 81 | "@babel/plugin-proposal-optional-chaining": "^7.2.0", 82 | "@babel/plugin-proposal-throw-expressions": "^7.2.0", 83 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 84 | "@babel/plugin-syntax-import-meta": "^7.2.0", 85 | "@babel/plugin-transform-destructuring": "^7.2.0", 86 | "@babel/preset-env": "^7.2.0", 87 | "@babel/preset-react": "^7.0.0", 88 | "babel-core": "^7.0.0-bridge.0", 89 | "babel-jest": "^23.4.2", 90 | "babel-loader": "^8.0.0", 91 | "babel-plugin-lodash": "^3.3.4", 92 | "clean-webpack-plugin": "^1.0.0", 93 | "css-loader": "^2.0.1", 94 | "dotenv": "^6.2.0", 95 | "html-webpack-plugin": "^3.2.0", 96 | "http-server": "^0.11.1", 97 | "husky": "^1.2.1", 98 | "ignore-styles": "^5.0.1", 99 | "jest": "^23.6.0", 100 | "mini-css-extract-plugin": "^0.5.0", 101 | "mustache": "^3.0.1", 102 | "npm": "^6.5.0", 103 | "patch-package": "^5.1.1", 104 | "prettier": "^1.15.3", 105 | "pretty-quick": "^1.8.0", 106 | "split": "^1.0.1", 107 | "style-loader": "^0.23.1", 108 | "unminified-webpack-plugin": "^2.0.0", 109 | "webpack": "^4.27.1", 110 | "webpack-bundle-analyzer": "^3.0.3", 111 | "webpack-cli": "^3.1.2", 112 | "webpack-dev-server": "^3.1.10", 113 | "webpack-merge": "^4.1.5" 114 | }, 115 | "husky": { 116 | "hooks": { 117 | "pre-commit": "pretty-quick --staged" 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /patches/python32_debug.diff: -------------------------------------------------------------------------------- 1 | diff --git a/Objects/dictobject.c b/Objects/dictobject.c 2 | index c10bfccdce..3734a08281 100644 3 | --- a/Objects/dictobject.c 4 | +++ b/Objects/dictobject.c 5 | @@ -321,6 +321,8 @@ lookdict(PyDictObject *mp, PyObject *key, register Py_hash_t hash) 6 | PyObject *startkey; 7 | 8 | i = (size_t)hash & mask; 9 | + fprintf(stderr, "lookdict hash = %ld\n", hash); 10 | + fprintf(stderr, "initial i = %zu\n", i); 11 | ep = &ep0[i]; 12 | if (ep->me_key == NULL || ep->me_key == key) 13 | return ep; 14 | @@ -355,7 +357,9 @@ lookdict(PyDictObject *mp, PyObject *key, register Py_hash_t hash) 15 | least likely outcome, so test for that last. */ 16 | for (perturb = hash; ; perturb >>= PERTURB_SHIFT) { 17 | i = (i << 2) + i + perturb + 1; 18 | + fprintf(stderr, "next i = %zu perturb = %zu\n", i, perturb); 19 | ep = &ep0[i & mask]; 20 | + fprintf(stderr, "next i & mask = %zu perturb = %zu\n", i & mask, perturb); 21 | if (ep->me_key == NULL) 22 | return freeslot == NULL ? ep : freeslot; 23 | if (ep->me_key == key) 24 | @@ -648,6 +652,7 @@ dictresize(PyDictObject *mp, Py_ssize_t minused) 25 | } 26 | } 27 | else { 28 | + fprintf(stderr, "PyMem_NEW branch"); 29 | newtable = PyMem_NEW(PyDictEntry, newsize); 30 | if (newtable == NULL) { 31 | PyErr_NoMemory(); 32 | @@ -693,6 +698,7 @@ PyObject * 33 | _PyDict_NewPresized(Py_ssize_t minused) 34 | { 35 | PyObject *op = PyDict_New(); 36 | + fprintf(stderr, "_PyDict_NewPresized() %p %d\n", op, (int)minused); 37 | 38 | if (minused>5 && op != NULL && dictresize((PyDictObject *)op, minused) == -1) { 39 | Py_DECREF(op); 40 | diff --git a/Objects/longobject.c b/Objects/longobject.c 41 | index e2a4ef9c5e..7d72c88417 100644 42 | --- a/Objects/longobject.c 43 | +++ b/Objects/longobject.c 44 | @@ -2611,6 +2611,7 @@ long_hash(PyLongObject *v) 45 | sign = -1; 46 | i = -(i); 47 | } 48 | + fprintf(stderr, "i = %ld\n", i); 49 | while (--i >= 0) { 50 | /* Here x is a quantity in the range [0, _PyHASH_MODULUS); we 51 | want to compute x * 2**PyLong_SHIFT + v->ob_digit[i] modulo 52 | -------------------------------------------------------------------------------- /patches/smooth-scrollbar+8.3.1.patch: -------------------------------------------------------------------------------- 1 | patch-package 2 | --- a/node_modules/smooth-scrollbar/events/touch.js 3 | +++ b/node_modules/smooth-scrollbar/events/touch.js 4 | @@ -1,7 +1,7 @@ 5 | import { eventScope, TouchRecord, } from '../utils/'; 6 | var activeScrollbar; 7 | export function touchHandler(scrollbar) { 8 | - var MIN_EAING_MOMENTUM = 50; 9 | + var MIN_EAING_MOMENTUM = 3; 10 | var EASING_MULTIPLIER = /Android/.test(navigator.userAgent) ? 3 : 2; 11 | var target = scrollbar.options.delegateTo || scrollbar.containerEl; 12 | var touchRecord = new TouchRecord(); 13 | --- a/node_modules/smooth-scrollbar/geometry/update.js 14 | +++ b/node_modules/smooth-scrollbar/geometry/update.js 15 | @@ -4,6 +4,9 @@ export function update(scrollbar) { 16 | x: Math.max(newSize.content.width - newSize.container.width, 0), 17 | y: Math.max(newSize.content.height - newSize.container.height, 0), 18 | }; 19 | + // hack for a weird chrome on windows bug 20 | + if (limit.x <= 2) limit.x = 0; 21 | + if (limit.y <= 2) limit.y = 0; 22 | // metrics 23 | var containerBounding = scrollbar.containerEl.getBoundingClientRect(); 24 | var bounding = { 25 | --- a/node_modules/smooth-scrollbar/scrollbar.js 26 | +++ b/node_modules/smooth-scrollbar/scrollbar.js 27 | @@ -322,6 +322,10 @@ var Scrollbar = /** @class */ (function () { 28 | if (limit.x === 0 && limit.y === 0) { 29 | this._updateDebounced(); 30 | } 31 | + if (Math.abs(deltaY) > Math.abs(deltaX)) { 32 | + if (deltaY > 0 && offset.y === limit.y) return true; 33 | + if (deltaY < 0 && offset.y === 0) return true; 34 | + } 35 | var destX = clamp(deltaX + offset.x, 0, limit.x); 36 | var destY = clamp(deltaY + offset.y, 0, limit.y); 37 | var res = true; 38 | --- a/node_modules/smooth-scrollbar/track/track.js 39 | +++ b/node_modules/smooth-scrollbar/track/track.js 40 | @@ -41,8 +41,9 @@ var ScrollbarTrack = /** @class */ (function () { 41 | this.element.classList.remove('show'); 42 | }; 43 | ScrollbarTrack.prototype.update = function (scrollOffset, containerSize, pageSize) { 44 | + // -2 is a hack for a weird chrome on windows bug 45 | setStyle(this.element, { 46 | - display: pageSize <= containerSize ? 'none' : 'block', 47 | + display: pageSize - 2 <= containerSize ? 'none' : 'block', 48 | }); 49 | this.thumb.update(scrollOffset, containerSize, pageSize); 50 | }; 51 | deleted file mode 100644 52 | --- a/node_modules/smooth-scrollbar/track/track.js.map 53 | +++ /dev/null 54 | @@ -1 +0,0 @@ 55 | -{"version":3,"file":"track.js","sourceRoot":"","sources":["../src/track/track.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEzC,OAAO,EACL,QAAQ,GACT,MAAM,WAAW,CAAC;AAEnB;IAUE,wBACE,SAAyB,EACzB,YAAwB;QAAxB,6BAAA,EAAA,gBAAwB;QAT1B;;WAEG;QACM,YAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAEzC,aAAQ,GAAG,KAAK,CAAC;QAMvB,IAAI,CAAC,OAAO,CAAC,SAAS,GAAG,qCAAmC,SAAW,CAAC;QAExE,IAAI,CAAC,KAAK,GAAG,IAAI,cAAc,CAC7B,SAAS,EACT,YAAY,CACb,CAAC;QAEF,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACpC,CAAC;IAED;;;;OAIG;IACH,iCAAQ,GAAR,UAAS,kBAA+B;QACtC,kBAAkB,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC/C,CAAC;IAED;;OAEG;IACH,6BAAI,GAAJ;QACE,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;YAClB,MAAM,CAAC;QACT,CAAC;QAED,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACrC,CAAC;IAED;;OAEG;IACH,6BAAI,GAAJ;QACE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;YACnB,MAAM,CAAC;QACT,CAAC;QAED,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACtB,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACxC,CAAC;IAED,+BAAM,GAAN,UACE,YAAoB,EACpB,aAAqB,EACrB,QAAgB;QAEhB,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE;YACrB,OAAO,EAAE,QAAQ,IAAI,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO;SACtD,CAAC,CAAC;QAEH,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa,EAAE,QAAQ,CAAC,CAAC;IAC3D,CAAC;IACH,qBAAC;AAAD,CAAC,AApED,IAoEC"} 56 | \ No newline at end of file 57 | -------------------------------------------------------------------------------- /patches/subscribe-ui-event+2.0.4.patch: -------------------------------------------------------------------------------- 1 | patch-package 2 | new file mode 100644 3 | Binary files /dev/null and b/node_modules/subscribe-ui-event/.index.es.js.swp differ 4 | --- a/node_modules/subscribe-ui-event/index.es.js 5 | +++ b/node_modules/subscribe-ui-event/index.es.js 6 | @@ -7,7 +7,7 @@ import listenLib from './dist-es/lib/listen'; 7 | import subscribeLib from './dist-es/subscribe'; 8 | import unsubscribeLib from './dist-es/unsubscribe'; 9 | 10 | -const IS_CLIENT = typeof window !== 'undefined'; 11 | +var IS_CLIENT = typeof window !== 'undefined'; 12 | 13 | function warn() { 14 | if (process.env.NODE_ENV !== 'production') { 15 | @@ -15,6 +15,6 @@ function warn() { 16 | } 17 | } 18 | 19 | -export const listen = IS_CLIENT ? listenLib : warn; 20 | -export const subscribe = IS_CLIENT ? subscribeLib : warn; 21 | -export const unsubscribe = IS_CLIENT ? unsubscribeLib : warn; 22 | +export var listen = IS_CLIENT ? listenLib : warn; 23 | +export var subscribe = IS_CLIENT ? subscribeLib : warn; 24 | +export var unsubscribe = IS_CLIENT ? unsubscribeLib : warn; 25 | -------------------------------------------------------------------------------- /python_code/actual_dict_factory_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from dict32_reimplementation_test_v2 import dict_factory 4 | from dictinfo import dump_py_dict 5 | 6 | 7 | def table_size(d): 8 | return len(dump_py_dict(d)[0]) 9 | 10 | 11 | class TestDictFactory(unittest.TestCase): 12 | def test_dict_factory(self): 13 | self.assertEqual(table_size(dict_factory([])), 8) 14 | self.assertEqual(table_size(dict_factory([(1, 1)])), 8) 15 | self.assertEqual(table_size(dict_factory([(1, 1), (1, 2), (1, 3), (1, 4)])), 8) 16 | self.assertEqual(table_size(dict_factory([(1, 1), (1, 2), (1, 3), (1, 4), (1, 5)])), 8) 17 | self.assertEqual(table_size(dict_factory([(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8)])), 16) 18 | self.assertEqual(table_size({1: 1, 1: 2, 1: 3, 1: 4, 1: 5, 1: 6, 1: 7, 1: 8}), 16) 19 | self.assertEqual(table_size(dict([(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8)])), 8) 20 | 21 | self.assertEqual(table_size({"x": "y", "abde": 1, "cdef": 4, "world": 9, "hmmm": 16, "hello": 25, "xxx": 36, "ya": 49, "hello,world!": 64, "well": 81, "meh": 100}), 64) 22 | 23 | 24 | def main(): 25 | unittest.main() 26 | 27 | 28 | if __name__ == "__main__": 29 | main() 30 | -------------------------------------------------------------------------------- /python_code/build_autogenerated_chapter1_hash.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'build')) 4 | from hash_chapter1_extracted import * 5 | 6 | 7 | def create_new(numbers): 8 | return build_insert_all(numbers) 9 | 10 | 11 | def create_new_broken(numbers): 12 | return build_not_quite_what_we_want(numbers) 13 | 14 | 15 | def has_key(keys, key): 16 | return has_number(keys, key) 17 | 18 | 19 | def linear_search(numbers, number): 20 | return simple_search(numbers, number) 21 | -------------------------------------------------------------------------------- /python_code/build_autogenerated_chapter2.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'build')) 4 | from hash_chapter2_extracted import * 5 | -------------------------------------------------------------------------------- /python_code/build_autogenerated_chapter3_chapter4.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'build')) 4 | from dict32js_extracted import Dict32Extracted 5 | from hash_class_recycling_extracted import HashClassRecyclingExtracted 6 | from hash_class_no_recycling_extracted import HashClassNoRecyclingExtracted 7 | -------------------------------------------------------------------------------- /python_code/chapter1_linear_search_reimplementation_test.py: -------------------------------------------------------------------------------- 1 | import random 2 | import argparse 3 | 4 | import hash_chapter1_reimpl_js 5 | import hash_chapter1_impl 6 | import build_autogenerated_chapter1_hash 7 | 8 | IMPLEMENTATIONS = { 9 | 'ref': hash_chapter1_impl.linear_search, 10 | 'js': hash_chapter1_reimpl_js.linear_search, 11 | 'py_extracted': build_autogenerated_chapter1_hash.linear_search 12 | } 13 | 14 | 15 | def run(test_implementation, size): 16 | MAX_VAL = 5000 17 | ref_search = IMPLEMENTATIONS['ref'] 18 | test_search = IMPLEMENTATIONS[test_implementation] 19 | 20 | numbers = [random.randint(-MAX_VAL, MAX_VAL) for _ in range(size)] 21 | 22 | for number in numbers: 23 | assert ref_search(numbers, number) 24 | assert test_search(numbers, number) 25 | 26 | for i in range(size * 3): 27 | number = random.randint(-MAX_VAL, MAX_VAL) 28 | assert ref_search(numbers, number) == test_search(numbers, number) 29 | 30 | 31 | if __name__ == "__main__": 32 | parser = argparse.ArgumentParser(description='Stress-test chapter1 reimplementation') 33 | parser.add_argument('--test-implementation', choices=['py_extracted', 'js'], required=True) 34 | parser.add_argument('--size', type=int, default=100) 35 | args = parser.parse_args() 36 | 37 | run(test_implementation=args.test_implementation, 38 | size=args.size) 39 | -------------------------------------------------------------------------------- /python_code/chapter4_probing_python_reimplementation_test.py: -------------------------------------------------------------------------------- 1 | import json 2 | from common import AllKeyValueFactory 3 | from js_reimpl_common import _init_sock_stuff, dump_simple_py_obj 4 | from pprint import pprint 5 | 6 | sock, sockfile = _init_sock_stuff() 7 | 8 | 9 | def probe_all_js(key, slots_count): 10 | global sockfile 11 | global sock 12 | 13 | data = { 14 | "dict": "pythonProbing", 15 | "args": { 16 | 'key': dump_simple_py_obj(key), 17 | 'slotsCount': slots_count 18 | }, 19 | } 20 | 21 | sock.send(bytes(json.dumps(data) + "\n", 'UTF-8')) 22 | response = json.loads(sockfile.readline()) 23 | 24 | return response['result'] 25 | 26 | 27 | def probe_all(key, slots_count=8): 28 | PERTURB_SHIFT = 5 29 | links = [[] for _ in range(slots_count)] 30 | hash_code = hash(key) 31 | perturb = 2**64 + hash_code if hash_code < 0 else hash_code 32 | idx = hash_code % slots_count 33 | start_idx = idx 34 | visited = set() 35 | while len(visited) < slots_count: 36 | visited.add(idx) 37 | next_idx = (idx * 5 + perturb + 1) % slots_count 38 | links[idx].append({'nextIdx': next_idx, 'perturbLink': perturb != 0}) 39 | idx = next_idx 40 | perturb >>= PERTURB_SHIFT 41 | 42 | return {'startIdx': start_idx, 'links': links} 43 | 44 | 45 | def test(): 46 | factory = AllKeyValueFactory(100) 47 | for slots_count in [8, 16, 32]: 48 | for i in range(300): 49 | key = factory.generate_key() 50 | assert probe_all(key, slots_count) == probe_all_js(key, slots_count) 51 | 52 | 53 | if __name__ == "__main__": 54 | test() 55 | -------------------------------------------------------------------------------- /python_code/common.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | 5 | class EmptyValueClass(object): 6 | def __str__(self): 7 | return "EMPTY" 8 | 9 | def __repr__(self): 10 | return "" 11 | 12 | 13 | class DummyValueClass(object): 14 | def __str__(self): 15 | return "" 16 | 17 | def __repr__(self): 18 | return "" 19 | 20 | 21 | EMPTY = EmptyValueClass() 22 | DUMMY = DummyValueClass() 23 | 24 | 25 | def get_object_field_or_null(obj, field_name): 26 | try: 27 | return getattr(obj, field_name) 28 | except ValueError: 29 | return EMPTY 30 | 31 | 32 | def get_object_field_or_none(obj, field_name): 33 | try: 34 | return getattr(obj, field_name) 35 | except ValueError: 36 | return None 37 | 38 | 39 | def generate_random_string(str_len=5): 40 | # FROM: https://stackoverflow.com/questions/2257441/random-string-generation-with-upper-case-letters-and-digits-in-python 41 | return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(str_len)) 42 | 43 | 44 | _unicode_chars = string.ascii_uppercase + string.digits + "йцукенгшщзхъфывапролджэячсмитьбю" 45 | 46 | 47 | def generate_random_unicode(str_len): 48 | # FROM: https://stackoverflow.com/questions/2257441/random-string-generation-with-upper-case-letters-and-digits-in-python 49 | return ''.join(random.choice(_unicode_chars) for _ in range(str_len)) 50 | 51 | 52 | class IntKeyValueFactory(object): 53 | def __init__(self, n_inserts): 54 | self.n_inserts = n_inserts 55 | self._insert_count = 0 56 | self._key_range = list(range(n_inserts)) 57 | 58 | def generate_key(self): 59 | return random.choice(self._key_range) 60 | 61 | def generate_value(self): 62 | self._insert_count += 1 63 | return self._insert_count 64 | 65 | 66 | class AllKeyValueFactory(object): 67 | def __init__(self, n_inserts, int_chance=0.1, long_chance=0.1, len0_chance=0.01, len1_chance=0.1, len2_chance=0.3, len3_chance=0.2, len_random_chance=0.17): 68 | self.int_pbf = int_chance 69 | self.long_pbf = self.int_pbf + long_chance 70 | self.len0_pbf = self.int_pbf + len0_chance 71 | self.len1_pbf = self.len0_pbf + len1_chance 72 | self.len2_pbf = self.len1_pbf + len2_chance 73 | self.len3_pbf = self.len2_pbf + len3_chance 74 | self.len_random_pbf = self.len3_pbf + len_random_chance 75 | assert 0.0 <= self.len3_pbf <= 1.0 76 | 77 | half_range = int(n_inserts / 2) 78 | self._int_range = [i - half_range for i in range(2 * half_range)] 79 | 80 | def _generate_obj(self): 81 | r = random.random() 82 | if r <= self.int_pbf: 83 | return random.choice(self._int_range) 84 | if r <= self.long_pbf: 85 | sign = "-" if random.random() < 0.5 else "" 86 | first_digit = random.choice("123456789") 87 | return sign + first_digit + ''.join(random.choice("0123456789") for _ in range(random.randint(20, 50))) 88 | if r <= self.len0_pbf: 89 | return "" 90 | if r <= self.len1_pbf: 91 | return generate_random_unicode(1) 92 | if r <= self.len2_pbf: 93 | return generate_random_unicode(2) 94 | if r <= self.len3_pbf: 95 | return generate_random_unicode(3) 96 | if r <= self.len_random_pbf: 97 | return generate_random_unicode(random.randint(4, 25)) 98 | return None 99 | 100 | def generate_key(self): 101 | return self._generate_obj() 102 | 103 | def generate_value(self): 104 | return self._generate_obj() 105 | -------------------------------------------------------------------------------- /python_code/dict32_reimplementation_test_v2.py: -------------------------------------------------------------------------------- 1 | import random 2 | import argparse 3 | import json 4 | 5 | from common import EMPTY, AllKeyValueFactory, IntKeyValueFactory 6 | from dictinfo import dump_py_dict 7 | from dict_reimplementation import PyDictReimplementation32, dump_reimpl_dict 8 | from js_reimplementation_interface import Dict32JsImpl, AlmostPythonDictRecyclingJsImpl, AlmostPythonDictNoRecyclingJsImpl 9 | import hash_chapter3_class_impl 10 | import build_autogenerated_chapter3_chapter4 11 | 12 | 13 | def dict_factory(pairs=None): 14 | if not pairs: 15 | return {} 16 | 17 | # quick&dirty 18 | def to_string(x): 19 | return json.dumps(x) if x is not None else "None" 20 | d = eval("{" + ", ".join("{}:{}".format(to_string(k), to_string(v)) for [k, v] in pairs) + "}") 21 | return d 22 | 23 | 24 | IMPLEMENTATIONS = { 25 | "dict_actual": (dict_factory, dump_py_dict), 26 | "dict32_reimpl_py": (PyDictReimplementation32, dump_reimpl_dict), 27 | "dict32_reimpl_js": (Dict32JsImpl, dump_reimpl_dict), 28 | 29 | "dict32_reimpl_py_extracted": (build_autogenerated_chapter3_chapter4.Dict32Extracted, dump_reimpl_dict), 30 | 31 | "almost_python_dict_recycling_py": (hash_chapter3_class_impl.AlmostPythonDictImplementationRecycling, dump_reimpl_dict), 32 | "almost_python_dict_no_recycling_py": (hash_chapter3_class_impl.AlmostPythonDictImplementationNoRecycling, dump_reimpl_dict), 33 | "almost_python_dict_no_recycling_py_simpler": (hash_chapter3_class_impl.AlmostPythonDictImplementationNoRecyclingSimplerVersion, dump_reimpl_dict), 34 | "almost_python_dict_recycling_js": (AlmostPythonDictRecyclingJsImpl, dump_reimpl_dict), 35 | "almost_python_dict_no_recycling_js": (AlmostPythonDictNoRecyclingJsImpl, dump_reimpl_dict), 36 | 37 | "almost_python_dict_recycling_py_extracted": (build_autogenerated_chapter3_chapter4.HashClassRecyclingExtracted, dump_reimpl_dict), 38 | "almost_python_dict_no_recycling_py_extracted": (build_autogenerated_chapter3_chapter4.HashClassNoRecyclingExtracted, dump_reimpl_dict), 39 | } 40 | 41 | 42 | def verify_same(d, dump_d_func, dreimpl, dump_dreimpl_func): 43 | dump_d = dump_d_func(d) 44 | dump_reimpl = dump_dreimpl_func(dreimpl) 45 | 46 | if dump_d != dump_reimpl: 47 | hashes_orig, keys_orig, values_orig, fill_orig, used_orig = dump_d 48 | hashes_new, keys_new, values_new, fill_new, used_new = dump_reimpl 49 | print("ORIG SIZE", len(hashes_orig)) 50 | print("NEW SIZE", len(hashes_new)) 51 | print("ORIG fill/used: ", fill_orig, used_orig) 52 | print("NEW fill/used: ", fill_new, used_new) 53 | if len(hashes_orig) == len(hashes_new): 54 | size = len(hashes_orig) 55 | print("NEW | ORIG") 56 | for i in range(size): 57 | if hashes_new[i] is not EMPTY or hashes_orig[i] is not EMPTY: 58 | print(i, " " * 3, 59 | hashes_new[i], keys_new[i], values_new[i], " " * 3, 60 | hashes_orig[i], keys_orig[i], values_orig[i]) 61 | 62 | assert dump_d == dump_reimpl 63 | 64 | 65 | def run(ref_impl_factory, ref_impl_dump, test_impl_factory, test_impl_dump, n_inserts, extra_checks, key_value_factory, initial_state, verbose): 66 | SINGLE_REMOVE_CHANCE = 0.3 67 | MASS_REMOVE_CHANCE = 0.002 68 | MASS_REMOVE_COEFF = 0.8 69 | 70 | removed = set() 71 | 72 | if initial_state: 73 | d = ref_impl_factory(initial_state) 74 | else: 75 | d = ref_impl_factory() 76 | 77 | if initial_state: 78 | dreimpl = test_impl_factory(initial_state) 79 | else: 80 | dreimpl = test_impl_factory() 81 | 82 | if verbose: 83 | print("Starting test") 84 | 85 | for i in range(n_inserts): 86 | should_remove = (random.random() < SINGLE_REMOVE_CHANCE) 87 | if should_remove and d and d.keys(): # TODO: ugly, written while on a plane 88 | to_remove = random.choice(list(d.keys())) 89 | if verbose: 90 | print("Removing {}".format(to_remove)) 91 | del d[to_remove] 92 | del dreimpl[to_remove] 93 | if verbose: 94 | print(d) 95 | verify_same(d, ref_impl_dump, dreimpl, test_impl_dump) 96 | removed.add(to_remove) 97 | 98 | should_mass_remove = (random.random() < MASS_REMOVE_CHANCE) 99 | if should_mass_remove and len(d) > 10: 100 | to_remove_list = random.sample(list(d.keys()), int(MASS_REMOVE_COEFF * len(d))) 101 | if verbose: 102 | print("Mass-Removing {} elements".format(len(to_remove_list))) 103 | for k in to_remove_list: 104 | del d[k] 105 | del dreimpl[k] 106 | removed.add(k) 107 | 108 | if extra_checks: 109 | for k in d.keys(): 110 | assert d[k] == dreimpl[k] 111 | 112 | for r in removed: 113 | try: 114 | dreimpl[r] 115 | assert False 116 | except KeyError: 117 | pass 118 | 119 | key_to_insert = key_value_factory.generate_key() 120 | value_to_insert = key_value_factory.generate_value() 121 | _keys_set = getattr(d, '_keys_set', None) 122 | # TODO: ugly code written on a plane 123 | # TODO: properly implement in/not in when I land 124 | if _keys_set is not None: 125 | key_present = key_to_insert in _keys_set 126 | else: 127 | key_present = key_to_insert in d 128 | 129 | if not key_present: 130 | if verbose: 131 | print("Inserting ({key}, {value})".format(key=key_to_insert, value=value_to_insert)) 132 | try: 133 | dreimpl[key_to_insert] 134 | assert False 135 | except KeyError: 136 | pass 137 | else: 138 | if verbose: 139 | print("Replacing ({key}, {value1}) with ({key}, {value2})".format(key=key_to_insert, value1=d[key_to_insert], value2=value_to_insert)) 140 | removed.discard(key_to_insert) 141 | d[key_to_insert] = value_to_insert 142 | dreimpl[key_to_insert] = value_to_insert 143 | if verbose: 144 | print(d) 145 | verify_same(d, ref_impl_dump, dreimpl, test_impl_dump) 146 | assert dreimpl[key_to_insert] == value_to_insert 147 | 148 | 149 | if __name__ == "__main__": 150 | parser = argparse.ArgumentParser(description='Stress-test dict-like reimplementations') 151 | parser.add_argument('--reference-implementation', choices=IMPLEMENTATIONS.keys(), required=True) 152 | parser.add_argument('--test-implementation', choices=IMPLEMENTATIONS.keys(), required=True) 153 | parser.add_argument('--no-extra-getitem-checks', dest='extra_checks', action='store_false') 154 | parser.add_argument('--num-inserts', type=int, default=500) 155 | parser.add_argument('--forever', action='store_true') 156 | parser.add_argument('--kv', choices=["numbers", "all"], required=True) 157 | parser.add_argument('--initial-size', type=int, default=-1) 158 | parser.add_argument('--verbose', action='store_true') 159 | args = parser.parse_args() 160 | 161 | if args.kv == "numbers": 162 | kv_factory = IntKeyValueFactory(args.num_inserts) 163 | elif args.kv == "all": 164 | kv_factory = AllKeyValueFactory(args.num_inserts) 165 | 166 | ref_impl = IMPLEMENTATIONS[args.reference_implementation] 167 | test_impl = IMPLEMENTATIONS[args.test_implementation] 168 | 169 | def test_iteration(): 170 | initial_size = args.initial_size if args.initial_size >= 0 else random.randint(0, 100) 171 | initial_state = [(kv_factory.generate_key(), kv_factory.generate_value()) for _ in range(initial_size)] 172 | run(*(ref_impl + test_impl), 173 | n_inserts=args.num_inserts, 174 | extra_checks=args.extra_checks, 175 | key_value_factory=kv_factory, 176 | initial_state=initial_state, 177 | verbose=args.verbose) 178 | 179 | if args.forever: 180 | while True: 181 | test_iteration() 182 | else: 183 | test_iteration() 184 | -------------------------------------------------------------------------------- /python_code/dict_reimpl_common.py: -------------------------------------------------------------------------------- 1 | from common import EMPTY 2 | 3 | 4 | class Slot(object): 5 | def __init__(self, hash_code=EMPTY, key=EMPTY, value=EMPTY): 6 | self.hash_code = hash_code 7 | self.key = key 8 | self.value = value 9 | 10 | 11 | class BaseDictImpl(object): 12 | def __init__(self): 13 | self.slots = [Slot() for _ in range(self.START_SIZE)] 14 | self.fill = 0 15 | self.used = 0 16 | 17 | def find_nearest_size(self, minused): 18 | new_size = 8 19 | while new_size <= minused: 20 | new_size *= 2 21 | 22 | return new_size 23 | -------------------------------------------------------------------------------- /python_code/dict_reimplementation.py: -------------------------------------------------------------------------------- 1 | from common import DUMMY, EMPTY 2 | from dict_reimpl_common import BaseDictImpl, Slot 3 | from operator import attrgetter 4 | 5 | 6 | class PyDictReimplementationBase(BaseDictImpl): 7 | START_SIZE = 8 8 | PERTURB_SHIFT = 5 9 | 10 | def __init__(self, pairs=None): 11 | BaseDictImpl.__init__(self) 12 | start_size = self.find_nearest_size(len(pairs)) if pairs else self.START_SIZE 13 | self.slots = [Slot() for _ in range(start_size)] 14 | if pairs: 15 | for k, v in pairs: 16 | self[k] = v 17 | 18 | def __setitem__(self, key, value): 19 | hash_code = hash(key) 20 | perturb = self.signed_to_unsigned(hash_code) 21 | idx = hash_code % len(self.slots) 22 | target_idx = None 23 | while self.slots[idx].key is not EMPTY: 24 | if self.slots[idx].hash_code == hash_code and self.slots[idx].key == key: 25 | target_idx = idx 26 | break 27 | if target_idx is None and self.slots[idx].key is DUMMY: 28 | target_idx = idx 29 | 30 | idx = (idx * 5 + perturb + 1) % len(self.slots) 31 | perturb >>= self.PERTURB_SHIFT 32 | 33 | if target_idx is None: 34 | target_idx = idx 35 | 36 | if self.slots[target_idx].key is EMPTY: 37 | self.used += 1 38 | self.fill += 1 39 | elif self.slots[target_idx].key is DUMMY: 40 | self.used += 1 41 | 42 | self.slots[target_idx] = Slot(hash_code, key, value) 43 | if self.fill * 3 >= len(self.slots) * 2: 44 | self.resize() 45 | 46 | def __delitem__(self, key): 47 | idx = self.lookdict(key) 48 | 49 | self.used -= 1 50 | self.slots[idx].key = DUMMY 51 | self.slots[idx].value = EMPTY 52 | 53 | def __getitem__(self, key): 54 | idx = self.lookdict(key) 55 | 56 | return self.slots[idx].value 57 | 58 | @staticmethod 59 | def signed_to_unsigned(hash_code): 60 | return 2**64 + hash_code if hash_code < 0 else hash_code 61 | 62 | def lookdict(self, key): 63 | hash_code = hash(key) 64 | perturb = self.signed_to_unsigned(hash_code) 65 | 66 | idx = hash_code % len(self.slots) 67 | while self.slots[idx].key is not EMPTY: 68 | if self.slots[idx].hash_code == hash_code and self.slots[idx].key == key: 69 | return idx 70 | 71 | idx = (idx * 5 + perturb + 1) % len(self.slots) 72 | perturb >>= self.PERTURB_SHIFT 73 | 74 | raise KeyError() 75 | 76 | def resize(self): 77 | old_slots = self.slots 78 | new_size = self.find_nearest_size(self._next_size()) 79 | self.slots = [Slot() for _ in range(new_size)] 80 | self.fill = self.used 81 | for slot in old_slots: 82 | if slot.key is not EMPTY and slot.key is not DUMMY: 83 | perturb = self.signed_to_unsigned(slot.hash_code) 84 | idx = slot.hash_code % len(self.slots) 85 | while self.slots[idx].key is not EMPTY: 86 | idx = (idx * 5 + perturb + 1) % len(self.slots) 87 | perturb >>= self.PERTURB_SHIFT 88 | 89 | self.slots[idx] = Slot(slot.hash_code, slot.key, slot.value) 90 | 91 | 92 | class PyDictReimplementation32(PyDictReimplementationBase): 93 | def _next_size(self): 94 | return self.used * (4 if self.used <= 50000 else 2) 95 | 96 | 97 | def dump_reimpl_dict(d): 98 | def extract_fields(field_name): 99 | return list(map(attrgetter(field_name), d.slots)) 100 | return extract_fields('hash_code'), extract_fields('key'), extract_fields('value'), d.fill, d.used 101 | -------------------------------------------------------------------------------- /python_code/dictinfo.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | def dump_py_dict(d): 5 | vi = sys.version_info 6 | 7 | if vi.major != 3: 8 | raise Exception("Unsupported major version") 9 | 10 | if vi.minor < 2: 11 | raise Exception("Unsupported minor version (too old)") 12 | if vi.minor > 3: 13 | raise Exception("Unsupported minor version (too new)") 14 | 15 | if vi.minor == 2: 16 | import dictinfo32 17 | return dictinfo32.dump_py_dict(d) 18 | else: 19 | import dictinfo33 20 | return dictinfo33.dump_py_dict(d) 21 | -------------------------------------------------------------------------------- /python_code/dictinfo32.py: -------------------------------------------------------------------------------- 1 | from ctypes import Structure, c_ulong, POINTER, cast, py_object, c_long 2 | from common import get_object_field_or_null, EMPTY, DUMMY 3 | 4 | 5 | class PyDictEntry(Structure): 6 | _fields_ = [ 7 | ('me_hash', c_long), 8 | ('me_key', py_object), 9 | ('me_value', py_object), 10 | ] 11 | 12 | 13 | class PyDictObject(Structure): 14 | _fields_ = [ 15 | ('ob_refcnt', c_ulong), 16 | ('ob_type', c_ulong), 17 | ('ma_fill', c_ulong), 18 | ('ma_used', c_ulong), 19 | ('ma_mask', c_ulong), 20 | ('ma_table', POINTER(PyDictEntry)), 21 | ] 22 | 23 | 24 | def dictobject(d): 25 | return cast(id(d), POINTER(PyDictObject)).contents 26 | 27 | 28 | d = {0: 0} 29 | del d[0] 30 | dummy_internal = dictobject(d).ma_table[0].me_key 31 | del d 32 | 33 | 34 | def dump_py_dict(d): 35 | do = dictobject(d) 36 | 37 | keys = [] 38 | hashes = [] 39 | values = [] 40 | 41 | size = do.ma_mask + 1 42 | 43 | for i in range(size): 44 | key = get_object_field_or_null(do.ma_table[i], 'me_key') 45 | keys.append(key if key is not dummy_internal else DUMMY) 46 | 47 | for i, key in enumerate(keys): 48 | if key is EMPTY: 49 | hashes.append(EMPTY) 50 | values.append(EMPTY) 51 | else: 52 | hashes.append(do.ma_table[i].me_hash) 53 | values.append(get_object_field_or_null(do.ma_table[i], 'me_value')) 54 | 55 | return hashes, keys, values, do.ma_fill, do.ma_used 56 | -------------------------------------------------------------------------------- /python_code/dictinfo33.py: -------------------------------------------------------------------------------- 1 | from ctypes import Structure, c_ulong, POINTER, cast, addressof, py_object, c_long, c_void_p 2 | from common import get_object_field_or_null, EMPTY, DUMMY 3 | 4 | 5 | class PyDictKeyEntry(Structure): 6 | _fields_ = [ 7 | ('me_hash', c_long), 8 | ('me_key', py_object), 9 | ('me_value', py_object), 10 | ] 11 | 12 | 13 | class PyDictKeysObject(Structure): 14 | _fields_ = [ 15 | ('dk_refcnt', c_long), 16 | ('dk_size', c_long), 17 | ('dict_lookup_func', POINTER(c_void_p)), 18 | ('dk_usable', c_long), 19 | ('dk_entries', PyDictKeyEntry), 20 | ] 21 | 22 | 23 | class PyDictObject(Structure): 24 | _fields_ = [ 25 | ('ob_refcnt', c_ulong), 26 | ('ob_type', c_ulong), 27 | ('ma_used', c_long), 28 | ('ma_keys', POINTER(PyDictKeysObject)), 29 | 30 | # Not actually a void*, split tables are not supported right now 31 | ('ma_values', POINTER(c_void_p)) 32 | ] 33 | 34 | 35 | def dictobject(d): 36 | return cast(id(d), POINTER(PyDictObject)).contents 37 | 38 | 39 | d = {0: 0} 40 | del d[0] 41 | dummy_internal = dictobject(d).ma_keys.contents.dk_entries.me_key 42 | del d 43 | 44 | 45 | def usable_fraction(size): 46 | return (size * 2 + 1) // 3 47 | 48 | 49 | def dump_py_dict(d): 50 | do = dictobject(d) 51 | 52 | keys = [] 53 | hashes = [] 54 | values = [] 55 | 56 | size = do.ma_keys.contents.dk_size 57 | entries = cast(addressof(do.ma_keys.contents.dk_entries), POINTER(PyDictKeyEntry)) 58 | for i in range(size): 59 | key = get_object_field_or_null(entries[i], 'me_key') 60 | keys.append(key if key is not dummy_internal else DUMMY) 61 | 62 | for i, key in enumerate(keys): 63 | if key is EMPTY: 64 | hashes.append(EMPTY) 65 | values.append(EMPTY) 66 | else: 67 | hashes.append(entries[i].me_hash) 68 | values.append(get_object_field_or_null(entries[i], 'me_value')) 69 | 70 | return hashes, keys, values, usable_fraction(do.ma_keys.contents.dk_size) - do.ma_keys.contents.dk_usable, do.ma_used 71 | -------------------------------------------------------------------------------- /python_code/hash_chapter1_impl.py: -------------------------------------------------------------------------------- 1 | def create_new(numbers): 2 | n = len(numbers) 3 | keys = [None for i in range(2 * n)] 4 | 5 | for num in numbers: 6 | idx = num % len(keys) 7 | 8 | while keys[idx] is not None: 9 | idx = (idx + 1) % len(keys) 10 | 11 | keys[idx] = num 12 | 13 | return keys 14 | 15 | 16 | def create_new_broken(numbers): 17 | n = len(numbers) 18 | keys = [None for i in range(n)] 19 | 20 | for num in numbers: 21 | idx = num % len(keys) 22 | keys[idx] = num 23 | 24 | return keys 25 | 26 | 27 | def has_key(keys, key): 28 | idx = key % len(keys) 29 | while keys[idx] is not None: 30 | if keys[idx] == key: 31 | return True 32 | idx = (idx + 1) % len(keys) 33 | 34 | return False 35 | 36 | 37 | def linear_search(numbers, number): 38 | return number in numbers 39 | -------------------------------------------------------------------------------- /python_code/hash_chapter1_reimpl_js.py: -------------------------------------------------------------------------------- 1 | from js_reimpl_common import run_op_chapter1_chapter2 2 | 3 | 4 | def run_op(keys, op, **kwargs): 5 | return run_op_chapter1_chapter2("chapter1", None, keys, op, **kwargs) 6 | 7 | 8 | def create_new(numbers): 9 | return run_op(None, "create_new", array=numbers) 10 | 11 | 12 | def create_new_broken(numbers): 13 | return run_op(None, "create_new_broken", array=numbers) 14 | 15 | 16 | def has_key(keys, key): 17 | return run_op(keys, "has_key", key=key) 18 | 19 | 20 | def linear_search(numbers, key): 21 | return run_op(None, "linear_search", key=key, array=numbers) 22 | -------------------------------------------------------------------------------- /python_code/hash_chapter1_reimplementation_test.py: -------------------------------------------------------------------------------- 1 | import random 2 | import argparse 3 | 4 | import hash_chapter1_reimpl_js 5 | import hash_chapter1_impl 6 | import build_autogenerated_chapter1_hash 7 | 8 | 9 | def get_implementation(is_broken, impl): 10 | if impl == "js": 11 | module = hash_chapter1_reimpl_js 12 | elif impl == "py_ref": 13 | module = hash_chapter1_impl 14 | elif impl == "py_extracted": 15 | module = build_autogenerated_chapter1_hash 16 | else: 17 | assert False 18 | 19 | return (module.create_new_broken if is_broken else module.create_new, module.has_key) 20 | 21 | 22 | def run(test_implementation, is_broken, n_inserts): 23 | MAX_VAL = 5000 24 | ref_create_new, ref_has_key = get_implementation(is_broken, "py_ref") 25 | test_create_new, test_has_key = get_implementation(is_broken, test_implementation) 26 | 27 | numbers = list(set(random.randint(-MAX_VAL, MAX_VAL) for _ in range(n_inserts))) 28 | 29 | ref_keys = ref_create_new(numbers) 30 | test_keys = test_create_new(numbers) 31 | 32 | for number in numbers: 33 | if not is_broken: 34 | assert ref_has_key(ref_keys, number) 35 | assert test_has_key(test_keys, number) 36 | else: 37 | assert ref_has_key(ref_keys, number) == test_has_key(test_keys, number) 38 | 39 | for i in range(n_inserts * 3): 40 | number = random.randint(-MAX_VAL, MAX_VAL) 41 | assert ref_has_key(ref_keys, number) == test_has_key(test_keys, number) 42 | 43 | 44 | if __name__ == "__main__": 45 | parser = argparse.ArgumentParser(description='Stress-test chapter1 reimplementation') 46 | parser.add_argument('--is-broken', action='store_true') 47 | parser.add_argument('--test-implementation', choices=['py_extracted', 'js'], required=True) 48 | parser.add_argument('--num-inserts', type=int, default=500) 49 | args = parser.parse_args() 50 | 51 | run(test_implementation=args.test_implementation, 52 | is_broken=args.is_broken, 53 | n_inserts=args.num_inserts) 54 | -------------------------------------------------------------------------------- /python_code/hash_chapter2_impl.py: -------------------------------------------------------------------------------- 1 | from common import DUMMY, EMPTY 2 | 3 | 4 | def create_new(from_keys): 5 | n = len(from_keys) 6 | hash_codes = [EMPTY for i in range(2 * n)] 7 | keys = [EMPTY for i in range(2 * n)] 8 | 9 | for key in from_keys: 10 | hash_code = hash(key) 11 | idx = hash_code % len(keys) 12 | 13 | while keys[idx] is not EMPTY: 14 | if hash_codes[idx] == hash_code and keys[idx] == key: 15 | break 16 | idx = (idx + 1) % len(keys) 17 | 18 | hash_codes[idx] = hash_code 19 | keys[idx] = key 20 | 21 | return hash_codes, keys 22 | 23 | 24 | def insert(hash_codes, keys, key): 25 | hash_code = hash(key) 26 | idx = hash_code % len(keys) 27 | 28 | while hash_codes[idx] is not EMPTY: 29 | if hash_codes[idx] == hash_code and keys[idx] == key: 30 | return 31 | idx = (idx + 1) % len(keys) 32 | 33 | hash_codes[idx] = hash_code 34 | keys[idx] = key 35 | 36 | 37 | def remove(hash_codes, keys, key): 38 | hash_code = hash(key) 39 | idx = hash_code % len(keys) 40 | 41 | while hash_codes[idx] is not EMPTY: 42 | if hash_codes[idx] == hash_code and keys[idx] == key: 43 | keys[idx] = DUMMY 44 | return 45 | idx = (idx + 1) % len(keys) 46 | 47 | raise KeyError() 48 | 49 | 50 | def has_key(hash_codes, keys, key): 51 | hash_code = hash(key) 52 | idx = hash_code % len(keys) 53 | while hash_codes[idx] is not EMPTY: 54 | if hash_codes[idx] == hash_code and keys[idx] == key: 55 | return True 56 | idx = (idx + 1) % len(keys) 57 | return False 58 | 59 | 60 | def resize(hash_codes, keys): 61 | new_hash_codes = [EMPTY for i in range(len(hash_codes) * 2)] 62 | new_keys = [EMPTY for i in range(len(keys) * 2)] 63 | for hash_code, key in zip(hash_codes, keys): 64 | if key is EMPTY or key is DUMMY: 65 | continue 66 | idx = hash_code % len(new_keys) 67 | while new_hash_codes[idx] is not EMPTY: 68 | idx = (idx + 1) % len(new_keys) 69 | new_hash_codes[idx] = hash_code 70 | new_keys[idx] = key 71 | 72 | return new_hash_codes, new_keys 73 | -------------------------------------------------------------------------------- /python_code/hash_chapter2_impl_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from hash_chapter2_impl import create_new, has_key, insert, remove, resize, DUMMY 3 | from common import generate_random_string 4 | 5 | 6 | class MyHashTest(unittest.TestCase): 7 | def test_handcrafted(self): 8 | expected_len = 6 9 | hashes, keys = create_new([42, 43, 12]) 10 | 11 | self.assertEqual(len(hashes), expected_len) 12 | self.assertEqual(len(keys), expected_len) 13 | insert(hashes, keys, 42) 14 | 15 | self.assertEqual(hashes[42 % expected_len], 42) 16 | self.assertEqual(keys[42 % expected_len], 42) 17 | 18 | self.assertEqual(hashes[43 % expected_len], 43) 19 | self.assertEqual(keys[43 % expected_len], 43) 20 | 21 | self.assertEqual(hashes[42 % expected_len], 42) 22 | self.assertEqual(keys[42 % expected_len], 42) 23 | 24 | self.assertEqual(hashes[12 % expected_len], 42) 25 | self.assertEqual(keys[12 % expected_len], 42) 26 | self.assertEqual(hashes[12 % expected_len + 1], 43) 27 | self.assertEqual(keys[12 % expected_len + 1], 43) 28 | self.assertEqual(hashes[12 % expected_len + 2], 12) 29 | self.assertEqual(keys[12 % expected_len + 2], 12) 30 | 31 | self.assertTrue(has_key(hashes, keys, 42)) 32 | self.assertTrue(has_key(hashes, keys, 43)) 33 | self.assertTrue(has_key(hashes, keys, 12)) 34 | self.assertFalse(has_key(hashes, keys, 45)) 35 | 36 | # table: [42, 43, 12, None, None, None] 37 | insert(hashes, keys, "") # hash("") == 0 38 | self.assertEqual(hashes[3], 0) 39 | self.assertEqual(keys[3], "") 40 | 41 | self.assertTrue(has_key(hashes, keys, "")) 42 | self.assertTrue(has_key(hashes, keys, 42)) 43 | 44 | insert(hashes, keys, "aba") # hash("aba") % 6 == 5 45 | self.assertEqual(hashes[5], hash("aba")) 46 | self.assertEqual(keys[5], "aba") 47 | 48 | self.assertTrue(has_key(hashes, keys, 12)) 49 | remove(hashes, keys, 12) 50 | self.assertFalse(has_key(hashes, keys, 12)) 51 | 52 | self.assertEqual(hashes[12 % expected_len], 42) 53 | self.assertEqual(keys[12 % expected_len], 42) 54 | 55 | self.assertEqual(keys[12 % expected_len + 2], DUMMY) 56 | 57 | with self.assertRaises(KeyError): 58 | remove(hashes, keys, 12) 59 | with self.assertRaises(KeyError): 60 | remove(hashes, keys, 45) 61 | 62 | self.assertFalse(has_key(hashes, keys, 12)) 63 | self.assertFalse(has_key(hashes, keys, 45)) 64 | self.assertTrue(has_key(hashes, keys, 42)) 65 | self.assertTrue(has_key(hashes, keys, 43)) 66 | self.assertTrue(has_key(hashes, keys, "")) 67 | self.assertTrue(has_key(hashes, keys, "aba")) 68 | 69 | insert(hashes, keys, "abg") 70 | self.assertTrue(has_key(hashes, keys, "abg")) 71 | self.assertEqual(hashes[4], hash("abg")) 72 | self.assertEqual(keys[4], "abg") 73 | hashes, keys = resize(hashes, keys) 74 | 75 | self.assertTrue(has_key(hashes, keys, 42)) 76 | self.assertTrue(has_key(hashes, keys, 43)) 77 | self.assertTrue(has_key(hashes, keys, "")) 78 | self.assertTrue(has_key(hashes, keys, "aba")) 79 | self.assertTrue(has_key(hashes, keys, "abg")) 80 | 81 | self.assertFalse(has_key(hashes, keys, 12)) 82 | self.assertFalse(has_key(hashes, keys, 45)) 83 | 84 | self.assertEqual(hashes[6], 42) 85 | self.assertEqual(keys[6], 42) 86 | self.assertEqual(hashes[7], 43) 87 | self.assertEqual(keys[7], 43) 88 | 89 | self.assertEqual(hashes[0], 0) 90 | self.assertEqual(keys[0], "") 91 | for h in hashes: 92 | self.assertTrue(h != 12) 93 | 94 | self.assertEqual(hashes[5], hash("aba")) 95 | self.assertEqual(keys[5], "aba") 96 | 97 | self.assertEqual(hashes[11], hash("abg")) 98 | self.assertEqual(keys[11], "abg") 99 | 100 | def test_all(self): 101 | n = 10 102 | initial_keys = [generate_random_string() for _ in range(n)] 103 | more_keys = [generate_random_string() for _ in range(n // 3)] 104 | myhashes, mykeys = create_new(initial_keys) 105 | 106 | for key in more_keys: 107 | insert(myhashes, mykeys, key) 108 | insert(myhashes, mykeys, key) 109 | 110 | existing_keys = initial_keys + more_keys 111 | for key in existing_keys: 112 | self.assertTrue(has_key(myhashes, mykeys, key)) 113 | 114 | myhashes, mykeys = resize(myhashes, mykeys) 115 | 116 | for key in existing_keys: 117 | self.assertTrue(has_key(myhashes, mykeys, key)) 118 | 119 | missing_keys = [generate_random_string() for _ in range(3 * n)] 120 | for key in set(missing_keys) - set(existing_keys): 121 | self.assertFalse(has_key(myhashes, mykeys, key)) 122 | with self.assertRaises(KeyError): 123 | remove(myhashes, mykeys, key) 124 | 125 | for key in existing_keys: 126 | self.assertTrue(has_key(myhashes, mykeys, key)) 127 | remove(myhashes, mykeys, key) 128 | self.assertFalse(has_key(myhashes, mykeys, key)) 129 | 130 | for key in more_keys: 131 | self.assertFalse(has_key(myhashes, mykeys, key)) 132 | insert(myhashes, mykeys, key) 133 | self.assertTrue(has_key(myhashes, mykeys, key)) 134 | remove(myhashes, mykeys, key) 135 | self.assertFalse(has_key(myhashes, mykeys, key)) 136 | 137 | 138 | def main(): 139 | unittest.main() 140 | 141 | 142 | if __name__ == "__main__": 143 | main() 144 | -------------------------------------------------------------------------------- /python_code/hash_chapter2_reimpl_js.py: -------------------------------------------------------------------------------- 1 | from js_reimpl_common import run_op_chapter1_chapter2 2 | 3 | 4 | def run_op(hash_codes, keys, op, **kwargs): 5 | return run_op_chapter1_chapter2("chapter2", hash_codes, keys, op, **kwargs) 6 | 7 | 8 | def create_new(from_keys): 9 | return run_op(None, None, "create_new", array=from_keys) 10 | 11 | 12 | def insert(hash_codes, keys, key): 13 | new_hash_codes, new_keys = run_op(hash_codes, keys, "insert", key=key) 14 | hash_codes[:] = new_hash_codes 15 | keys[:] = new_keys 16 | 17 | 18 | def remove(hash_codes, keys, key): 19 | new_hash_codes, new_keys = run_op(hash_codes, keys, "remove", key=key) 20 | hash_codes[:] = new_hash_codes 21 | keys[:] = new_keys 22 | 23 | 24 | def has_key(hash_codes, keys, key): 25 | return run_op(hash_codes, keys, "has_key", key=key) 26 | 27 | 28 | def resize(hash_codes, keys): 29 | return run_op(hash_codes, keys, "resize") 30 | -------------------------------------------------------------------------------- /python_code/hash_chapter2_reimplementation_test.py: -------------------------------------------------------------------------------- 1 | import random 2 | import argparse 3 | 4 | from common import DUMMY, EMPTY, AllKeyValueFactory, IntKeyValueFactory 5 | 6 | import hash_chapter2_reimpl_js 7 | import hash_chapter2_impl 8 | import build_autogenerated_chapter2 9 | 10 | TEST_IMPLEMENTATIONS = { 11 | 'js_reimpl': hash_chapter2_reimpl_js, 12 | 'py_extracted': build_autogenerated_chapter2 13 | } 14 | 15 | 16 | def verify_same(ref_hash_codes, ref_keys, hash_codes, keys): 17 | if (ref_hash_codes, ref_keys) != (hash_codes, keys): 18 | print("ORIG SIZES", len(ref_hash_codes), len(ref_keys)) 19 | print("NEW SIZES", len(hash_codes), len(keys)) 20 | if len(ref_hash_codes) == len(hash_codes) == len(ref_keys) == len(keys): 21 | size = len(hash_codes) 22 | print("NEW | ORIG") 23 | for i in range(size): 24 | if ref_hash_codes[i] is not EMPTY or hash_codes[i] is not EMPTY: 25 | print(i, " " * 3, 26 | ref_hash_codes[i], ref_keys[i], " " * 3, 27 | hash_codes[i], keys[i], " " * 3) 28 | 29 | assert ref_hash_codes == hash_codes and ref_keys == keys 30 | 31 | 32 | def run(ref_impl, test_impl, n_inserts, key_value_factory, initial_state, extra_checks, verbose): 33 | SINGLE_REMOVE_CHANCE = 0.3 34 | 35 | ref_hash_codes, ref_keys = ref_impl.create_new(initial_state) 36 | test_hash_codes, test_keys = test_impl.create_new(initial_state) 37 | 38 | def vs(): 39 | verify_same(ref_hash_codes, ref_keys, test_hash_codes, test_keys) 40 | 41 | vs() 42 | 43 | if verbose: 44 | print("Starting test") 45 | 46 | for i in range(n_inserts): 47 | key_to_insert = key_value_factory.generate_key() 48 | 49 | existing_keys = set([k for k in ref_keys if k is not DUMMY and k is not EMPTY]) 50 | fill = sum(1 for k in ref_keys if k is not EMPTY) 51 | if existing_keys and random.random() < SINGLE_REMOVE_CHANCE: 52 | key_to_remove = random.choice(list(existing_keys)) 53 | assert ref_impl.has_key(ref_hash_codes, ref_keys, key_to_remove) 54 | assert test_impl.has_key(test_hash_codes, test_keys, key_to_remove) 55 | 56 | ref_impl.remove(ref_hash_codes, ref_keys, key_to_remove) 57 | test_impl.remove(test_hash_codes, test_keys, key_to_remove) 58 | existing_keys.remove(key_to_remove) 59 | 60 | assert not ref_impl.has_key(ref_hash_codes, ref_keys, key_to_remove) 61 | assert not test_impl.has_key(test_hash_codes, test_keys, key_to_remove) 62 | 63 | is_key_present = ref_impl.has_key(ref_hash_codes, ref_keys, key_to_insert) 64 | assert (key_to_insert in existing_keys) == is_key_present 65 | 66 | if not is_key_present: 67 | if verbose: 68 | print("Inserting {}".format(key_to_insert)) 69 | assert not test_impl.has_key(test_hash_codes, test_keys, key_to_insert) 70 | else: 71 | if verbose: 72 | print("Re-Inserting {}".format(key_to_insert)) 73 | 74 | ref_impl.insert(ref_hash_codes, ref_keys, key_to_insert) 75 | test_impl.insert(test_hash_codes, test_keys, key_to_insert) 76 | vs() 77 | assert test_impl.has_key(test_hash_codes, test_keys, key_to_insert) 78 | assert ref_impl.has_key(ref_hash_codes, ref_keys, key_to_insert) 79 | 80 | if fill / len(ref_keys) > 0.66: 81 | ref_hash_codes, ref_keys = ref_impl.resize(ref_hash_codes, ref_keys) 82 | test_hash_codes, test_keys = test_impl.resize(test_hash_codes, test_keys) 83 | vs() 84 | 85 | if extra_checks: 86 | for k in existing_keys: 87 | assert test_impl.has_key(test_hash_codes, test_keys, k) 88 | assert ref_impl.has_key(ref_hash_codes, ref_keys, k) 89 | 90 | 91 | if __name__ == "__main__": 92 | parser = argparse.ArgumentParser(description='Stress-test chapter2 reimplementation') 93 | parser.add_argument('--test-implementation', choices=TEST_IMPLEMENTATIONS.keys(), required=True) 94 | parser.add_argument('--num-inserts', type=int, default=500) 95 | parser.add_argument('--forever', action='store_true') 96 | parser.add_argument('--kv', choices=["numbers", "all"], required=True) 97 | parser.add_argument('--initial-size', type=int, default=-1) 98 | parser.add_argument('--extra-getitem-checks', action='store_true', default=False) 99 | parser.add_argument('--verbose', action='store_true', default=False) 100 | args = parser.parse_args() 101 | 102 | if args.kv == "numbers": 103 | kv_factory = IntKeyValueFactory(args.num_inserts) 104 | elif args.kv == "all": 105 | kv_factory = AllKeyValueFactory(args.num_inserts) 106 | 107 | def test_iteration(): 108 | initial_size = args.initial_size if args.initial_size >= 0 else random.randint(0, 100) 109 | initial_state = [kv_factory.generate_key() for _ in range(initial_size)] 110 | run(hash_chapter2_impl, 111 | TEST_IMPLEMENTATIONS[args.test_implementation], 112 | n_inserts=args.num_inserts, 113 | key_value_factory=kv_factory, 114 | initial_state=initial_state, 115 | extra_checks=args.extra_getitem_checks, 116 | verbose=args.verbose) 117 | 118 | if args.forever: 119 | while True: 120 | test_iteration() 121 | else: 122 | test_iteration() 123 | -------------------------------------------------------------------------------- /python_code/hash_chapter3_class_impl.py: -------------------------------------------------------------------------------- 1 | from common import DUMMY, EMPTY 2 | from dict_reimpl_common import BaseDictImpl, Slot 3 | 4 | 5 | class AlmostPythonDictBase(BaseDictImpl): 6 | START_SIZE = 8 7 | 8 | def __init__(self, pairs=None): 9 | BaseDictImpl.__init__(self) 10 | self._keys_set = set() 11 | if pairs: 12 | for k, v in pairs: 13 | self[k] = v 14 | 15 | def lookdict(self, key): 16 | hash_code = hash(key) 17 | 18 | idx = hash_code % len(self.slots) 19 | while self.slots[idx].key is not EMPTY: 20 | if self.slots[idx].hash_code == hash_code and self.slots[idx].key == key: 21 | return idx 22 | 23 | idx = (idx + 1) % len(self.slots) 24 | 25 | raise KeyError() 26 | 27 | def __getitem__(self, key): 28 | idx = self.lookdict(key) 29 | 30 | return self.slots[idx].value 31 | 32 | def __delitem__(self, key): 33 | idx = self.lookdict(key) 34 | 35 | self.used -= 1 36 | self.slots[idx].key = DUMMY 37 | self.slots[idx].value = EMPTY 38 | self._keys_set.remove(key) 39 | 40 | def resize(self): 41 | old_slots = self.slots 42 | new_size = self.find_nearest_size(2 * self.used) 43 | self.slots = [Slot() for _ in range(new_size)] 44 | 45 | for slot in old_slots: 46 | if slot.key is not EMPTY and slot.key is not DUMMY: 47 | idx = slot.hash_code % len(self.slots) 48 | while self.slots[idx].key is not EMPTY: 49 | idx = (idx + 1) % len(self.slots) 50 | 51 | self.slots[idx] = Slot(slot.hash_code, slot.key, slot.value) 52 | 53 | self.fill = self.used 54 | 55 | def keys(self): 56 | return self._keys_set 57 | 58 | def __len__(self): 59 | return len(self.keys()) 60 | 61 | 62 | class AlmostPythonDictImplementationRecycling(AlmostPythonDictBase): 63 | def __setitem__(self, key, value): 64 | hash_code = hash(key) 65 | idx = hash_code % len(self.slots) 66 | target_idx = None 67 | while self.slots[idx].key is not EMPTY: 68 | if self.slots[idx].hash_code == hash_code and self.slots[idx].key == key: 69 | target_idx = idx 70 | break 71 | if target_idx is None and self.slots[idx].key is DUMMY: 72 | target_idx = idx 73 | 74 | idx = (idx + 1) % len(self.slots) 75 | 76 | if target_idx is None: 77 | target_idx = idx 78 | 79 | if self.slots[target_idx].key is EMPTY: 80 | self.used += 1 81 | self.fill += 1 82 | elif self.slots[target_idx].key is DUMMY: 83 | self.used += 1 84 | 85 | self.slots[target_idx] = Slot(hash_code, key, value) 86 | 87 | if self.fill * 3 >= len(self.slots) * 2: 88 | self.resize() 89 | 90 | self._keys_set.add(key) 91 | 92 | 93 | class AlmostPythonDictImplementationNoRecycling(AlmostPythonDictBase): 94 | def __setitem__(self, key, value): 95 | hash_code = hash(key) 96 | idx = hash_code % len(self.slots) 97 | target_idx = None 98 | while self.slots[idx].key is not EMPTY: 99 | if self.slots[idx].hash_code == hash_code and\ 100 | self.slots[idx].key == key: 101 | target_idx = idx 102 | break 103 | idx = (idx + 1) % len(self.slots) 104 | 105 | if target_idx is None: 106 | target_idx = idx 107 | if self.slots[target_idx].key is EMPTY: 108 | self.used += 1 109 | self.fill += 1 110 | 111 | self.slots[target_idx] = Slot(hash_code, key, value) 112 | if self.fill * 3 >= len(self.slots) * 2: 113 | self.resize() 114 | 115 | self._keys_set.add(key) 116 | 117 | 118 | class AlmostPythonDictImplementationNoRecyclingSimplerVersion(AlmostPythonDictBase): 119 | def __setitem__(self, key, value): 120 | hash_code = hash(key) 121 | idx = hash_code % len(self.slots) 122 | while self.slots[idx].key is not EMPTY: 123 | if self.slots[idx].hash_code == hash_code and\ 124 | self.slots[idx].key == key: 125 | break 126 | idx = (idx + 1) % len(self.slots) 127 | 128 | if self.slots[idx].key is EMPTY: 129 | self.used += 1 130 | self.fill += 1 131 | 132 | self.slots[idx] = Slot(hash_code, key, value) 133 | if self.fill * 3 >= len(self.slots) * 2: 134 | self.resize() 135 | 136 | self._keys_set.add(key) 137 | -------------------------------------------------------------------------------- /python_code/hash_chapter3_class_impl_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from common import DUMMY, EMPTY 3 | from hash_chapter3_class_impl import AlmostPythonDictImplementationRecycling, AlmostPythonDictImplementationNoRecycling 4 | 5 | 6 | class HashDictImplementationTest(unittest.TestCase): 7 | def test_handcrafted(self): 8 | d = AlmostPythonDictImplementationRecycling() 9 | self.assertEqual(len(d.slots), 8) 10 | 11 | def assert_contains(i, h, k, v): 12 | self.assertEqual(d.slots[i].hash_code, h) 13 | self.assertEqual(d.slots[i].key, k) 14 | self.assertEqual(d.slots[i].value, v) 15 | 16 | d[""] = 1 17 | d[17] = 2 18 | d[18] = 3 19 | self.assertEqual(d[""], 1) 20 | self.assertEqual(d[17], 2) 21 | self.assertEqual(d[18], 3) 22 | 23 | assert_contains(0, 0, "", 1) 24 | assert_contains(1, 17, 17, 2) 25 | assert_contains(2, 18, 18, 3) 26 | 27 | self.assertEqual(d.fill, 3) 28 | self.assertEqual(d.used, 3) 29 | 30 | with self.assertRaises(KeyError): 31 | del d[1] 32 | 33 | del d[17] 34 | assert_contains(1, 17, DUMMY, EMPTY) 35 | 36 | self.assertEqual(d.fill, 3) 37 | self.assertEqual(d.used, 2) 38 | # hash("abcd") % 8 == 0 39 | 40 | # py 3.2 hash() 41 | d["abcd"] = 4 42 | self.assertEqual(d["abcd"], 4) 43 | assert_contains(1, -2835746963027601024, "abcd", 4) 44 | self.assertEqual(d.fill, 3) 45 | self.assertEqual(d.used, 3) 46 | 47 | d["abcd"] = 5 48 | self.assertEqual(d["abcd"], 5) 49 | assert_contains(1, -2835746963027601024, "abcd", 5) 50 | self.assertEqual(d.fill, 3) 51 | self.assertEqual(d.used, 3) 52 | 53 | del d["abcd"] 54 | with self.assertRaises(KeyError): 55 | d["abcd"] 56 | 57 | d[15] = 6 58 | d[14] = 7 59 | 60 | assert_contains(7, 15, 15, 6) 61 | assert_contains(6, 14, 14, 7) 62 | 63 | self.assertEqual(len(d.slots), 8) 64 | self.assertEqual(d.fill, 5) 65 | self.assertEqual(d.used, 4) 66 | d[13] = 8 67 | self.assertEqual(len(d.slots), 16) 68 | self.assertEqual(d.fill, 5) 69 | self.assertEqual(d.used, 5) 70 | 71 | assert_contains(0, 0, "", 1) 72 | assert_contains(2, 18, 18, 3) 73 | assert_contains(13, 13, 13, 8) 74 | assert_contains(14, 14, 14, 7) 75 | assert_contains(15, 15, 15, 6) 76 | 77 | def test_handcrafted_simple_setitem(self): 78 | d = AlmostPythonDictImplementationNoRecycling() 79 | self.assertEqual(len(d.slots), 8) 80 | 81 | def assert_contains(i, h, k, v): 82 | self.assertEqual(d.slots[i].hash_code, h) 83 | self.assertEqual(d.slots[i].key, k) 84 | self.assertEqual(d.slots[i].value, v) 85 | 86 | d[""] = 1 87 | d[17] = 2 88 | d[18] = 3 89 | self.assertEqual(d[""], 1) 90 | self.assertEqual(d[17], 2) 91 | self.assertEqual(d[18], 3) 92 | 93 | assert_contains(0, 0, "", 1) 94 | assert_contains(1, 17, 17, 2) 95 | assert_contains(2, 18, 18, 3) 96 | 97 | self.assertEqual(d.fill, 3) 98 | self.assertEqual(d.used, 3) 99 | 100 | with self.assertRaises(KeyError): 101 | del d[1] 102 | 103 | del d[17] 104 | assert_contains(1, 17, DUMMY, EMPTY) 105 | 106 | self.assertEqual(d.fill, 3) 107 | self.assertEqual(d.used, 2) 108 | # hash("abcd") % 8 == 0 109 | 110 | # py 3.2 hash() 111 | d["abcd"] = 4 112 | self.assertEqual(d["abcd"], 4) 113 | assert_contains(3, -2835746963027601024, "abcd", 4) 114 | self.assertEqual(d.fill, 4) 115 | self.assertEqual(d.used, 3) 116 | 117 | d["abcd"] = 5 118 | self.assertEqual(d["abcd"], 5) 119 | assert_contains(3, -2835746963027601024, "abcd", 5) 120 | self.assertEqual(d.fill, 4) 121 | self.assertEqual(d.used, 3) 122 | 123 | del d["abcd"] 124 | with self.assertRaises(KeyError): 125 | d["abcd"] 126 | 127 | self.assertEqual(len(d.slots), 8) 128 | self.assertEqual(d.fill, 4) 129 | self.assertEqual(d.used, 2) 130 | 131 | d[15] = 6 132 | self.assertEqual(len(d.slots), 8) 133 | self.assertEqual(d.fill, 5) 134 | self.assertEqual(d.used, 3) 135 | assert_contains(7, 15, 15, 6) 136 | 137 | d[13] = 8 138 | self.assertEqual(len(d.slots), 16) 139 | self.assertEqual(d.fill, 4) 140 | self.assertEqual(d.used, 4) 141 | 142 | assert_contains(0, 0, "", 1) 143 | assert_contains(2, 18, 18, 3) 144 | assert_contains(13, 13, 13, 8) 145 | assert_contains(15, 15, 15, 6) 146 | 147 | 148 | def main(): 149 | unittest.main() 150 | 151 | 152 | if __name__ == "__main__": 153 | main() 154 | -------------------------------------------------------------------------------- /python_code/interface_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from dict32_reimplementation import PyDictReimplementation 3 | from hash_chapter3_class_impl import AlmostPythonDictImplementationRecycling, AlmostPythonDictImplementationNoRecycling 4 | from js_reimplementation_interface import Dict32JsImpl, AlmostPythonDictRecyclingJsImpl, AlmostPythonDictNoRecyclingJsImpl 5 | 6 | 7 | class Interface(unittest.TestCase): 8 | def test_all(self): 9 | self.do_simple_test_single_class(PyDictReimplementation) 10 | self.do_simple_test_single_class(AlmostPythonDictImplementationRecycling) 11 | self.do_simple_test_single_class(AlmostPythonDictImplementationNoRecycling) 12 | 13 | self.do_simple_test_single_class(Dict32JsImpl) 14 | self.do_simple_test_single_class(AlmostPythonDictRecyclingJsImpl) 15 | self.do_simple_test_single_class(AlmostPythonDictNoRecyclingJsImpl) 16 | 17 | def do_simple_test_single_class(self, klass): 18 | d = klass() 19 | 20 | for i in range(100): 21 | d[i] = i 22 | self.assertEqual(d[i], i) 23 | 24 | for i in range(50): 25 | del d[i] 26 | with self.assertRaises(KeyError): 27 | d[i] 28 | 29 | for i in range(200): 30 | d[i] = i + 1 31 | self.assertEqual(d[i], i + 1) 32 | 33 | 34 | def main(): 35 | unittest.main() 36 | 37 | 38 | if __name__ == "__main__": 39 | main() 40 | -------------------------------------------------------------------------------- /python_code/js_reimpl_common.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import json 3 | 4 | from common import DUMMY, EMPTY 5 | 6 | none_info = { 7 | "type": "None", 8 | "hash": str(hash(None)) 9 | } 10 | 11 | 12 | def dump_simple_py_obj(obj): 13 | if obj is DUMMY: 14 | return { 15 | "type": "DUMMY" 16 | } 17 | elif obj is EMPTY: 18 | return None 19 | elif obj is None: 20 | return none_info 21 | elif isinstance(obj, int): 22 | return { 23 | 'type': 'int', 24 | 'value': str(obj) 25 | } 26 | return obj 27 | 28 | 29 | def dump_pairs(pairs): 30 | res = [] 31 | for k, v in pairs: 32 | res.append([dump_simple_py_obj(k), dump_simple_py_obj(v)]) 33 | 34 | return res 35 | 36 | 37 | def dump_array(array): 38 | return list(map(dump_simple_py_obj, array)) 39 | 40 | 41 | def parse_array(array): 42 | return list(map(parse_simple_py_obj, array)) 43 | 44 | 45 | def parse_simple_py_obj(obj): 46 | if isinstance(obj, dict): 47 | assert obj["type"] in ["DUMMY", "None", "int"] 48 | if obj["type"] == "DUMMY": 49 | return DUMMY 50 | if obj["type"] == "None": 51 | return None 52 | return int(obj["value"]) 53 | elif obj is None: 54 | return EMPTY 55 | return obj 56 | 57 | 58 | sock = None 59 | sockfile = None 60 | 61 | 62 | def _init_sock_stuff(): 63 | global sock 64 | global sockfile 65 | 66 | # TODO: unhardcode? 67 | SOCK_FILENAME = 'pynode.sock' 68 | 69 | if sock is None: 70 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 71 | sock.connect(SOCK_FILENAME) 72 | sockfile = sock.makefile('r') 73 | 74 | return sock, sockfile 75 | 76 | 77 | def run_op_chapter1_chapter2(chapter, hash_codes, keys, op, **kwargs): 78 | _init_sock_stuff() 79 | 80 | for name in kwargs: 81 | if name != 'array': 82 | kwargs[name] = dump_simple_py_obj(kwargs[name]) 83 | else: 84 | kwargs[name] = dump_array(kwargs[name]) 85 | 86 | data = { 87 | "dict": chapter, 88 | "op": op, 89 | "args": kwargs, 90 | "hashCodes": dump_array(hash_codes) if hash_codes is not None else None, 91 | "keys": dump_array(keys) if keys is not None else None, 92 | } 93 | 94 | sock.send(bytes(json.dumps(data) + "\n", 'UTF-8')) 95 | response = json.loads(sockfile.readline()) 96 | 97 | if "exception" in response and response["exception"]: 98 | raise KeyError() 99 | 100 | if 'result' in response and response['result'] is not None: 101 | # TODO: this is pretty hacky 102 | return response["result"] 103 | elif "hashCodes" in response: 104 | return parse_array(response["hashCodes"]), parse_array(response["keys"]) 105 | else: 106 | return parse_array(response["keys"]) 107 | -------------------------------------------------------------------------------- /python_code/js_reimplementation_interface.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import json 3 | 4 | from common import DUMMY, EMPTY 5 | from js_reimpl_common import dump_simple_py_obj, parse_simple_py_obj, dump_pairs 6 | from dict_reimpl_common import Slot 7 | 8 | 9 | class JsImplBase(object): 10 | # TODO: unhardcode? 11 | SOCK_FILENAME = 'pynode.sock' 12 | 13 | def __init__(self, pairs=None): 14 | pairs = pairs or [] 15 | 16 | self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 17 | self.sock.connect(self.SOCK_FILENAME) 18 | self.sockfile = self.sock.makefile('r') 19 | 20 | self.slots = None 21 | self.fill = None 22 | self.used = None 23 | 24 | self.run_op("__init__", pairs=pairs) 25 | 26 | def __del__(self): 27 | self.sock.close() 28 | 29 | def dump_slots(self): 30 | def dump_slot(slot): 31 | key = dump_simple_py_obj(slot.key) 32 | value = dump_simple_py_obj(slot.value) 33 | 34 | hash_code = slot.hash_code 35 | if hash_code is EMPTY: 36 | hash_code = None 37 | 38 | return { 39 | "hashCode": str(hash_code) if hash_code is not None else None, 40 | "key": key, 41 | "value": value, 42 | } 43 | 44 | if self.slots is None: 45 | return None 46 | 47 | return list(map(dump_slot, self.slots)) 48 | 49 | def restore_slots(self, slots): 50 | def restore_slot(slot): 51 | key = parse_simple_py_obj(slot["key"]) 52 | value = parse_simple_py_obj(slot["value"]) 53 | assert value is not DUMMY 54 | 55 | hash_code = int(slot["hashCode"]) if slot["hashCode"] is not None else None 56 | if hash_code is None: 57 | hash_code = EMPTY 58 | 59 | return Slot(hash_code, key, value) 60 | 61 | self.slots = list(map(restore_slot, slots)) 62 | 63 | def run_op(self, op, **kwargs): 64 | for name in kwargs: 65 | if name != 'pairs': 66 | kwargs[name] = dump_simple_py_obj(kwargs[name]) 67 | else: 68 | kwargs[name] = dump_pairs(kwargs[name]) 69 | 70 | data = { 71 | "dict": self.dict_type, 72 | "op": op, 73 | "args": kwargs, 74 | "self": { 75 | "slots": self.dump_slots(), 76 | "used": self.used, 77 | "fill": self.fill 78 | } 79 | } 80 | 81 | # pprint(("<< sending", data, op, kwargs)) 82 | self.sock.send(bytes(json.dumps(data) + "\n", 'UTF-8')) 83 | response = json.loads(self.sockfile.readline()) 84 | # pprint((">> receiving", response)) 85 | 86 | self.restore_slots(response["self"]["slots"]) 87 | self.fill = response["self"]["fill"] 88 | self.used = response["self"]["used"] 89 | if response["exception"]: 90 | raise KeyError("whatever") 91 | 92 | return parse_simple_py_obj(response["result"]) 93 | 94 | 95 | class Dict32JsImpl(JsImplBase): 96 | dict_type = "dict32" 97 | 98 | def __setitem__(self, key, value): 99 | return self.run_op("__setitem__", key=key, value=value) 100 | 101 | def __delitem__(self, key): 102 | return self.run_op("__delitem__", key=key) 103 | 104 | def __getitem__(self, key): 105 | return self.run_op("__getitem__", key=key) 106 | 107 | 108 | class AlmostPythonDictBaseJsImpl(JsImplBase): 109 | dict_type = "almost_python_dict" 110 | 111 | def __delitem__(self, key): 112 | return self.run_op("__delitem__", key=key) 113 | 114 | def __getitem__(self, key): 115 | return self.run_op("__getitem__", key=key) 116 | 117 | 118 | class AlmostPythonDictRecyclingJsImpl(AlmostPythonDictBaseJsImpl): 119 | def __setitem__(self, key, value): 120 | return self.run_op("__setitem__recycling", key=key, value=value) 121 | 122 | 123 | class AlmostPythonDictNoRecyclingJsImpl(AlmostPythonDictBaseJsImpl): 124 | def __setitem__(self, key, value): 125 | return self.run_op("__setitem__no_recycling", key=key, value=value) 126 | -------------------------------------------------------------------------------- /scripts/extractPythonCode.js: -------------------------------------------------------------------------------- 1 | import 'ignore-styles'; 2 | 3 | import { 4 | DICT32_INIT, 5 | DICT32_SETITEM, 6 | DICT32_RESIZE_CODE, 7 | _DICT32_GETITEM_ONLY, 8 | _DICT32_DELITEM_ONLY, 9 | DICT32_LOOKDICT, 10 | STATICMETHOD_SIGNED_TO_UNSIGNED, 11 | PROBING_PYTHON_CODE, 12 | } from '../src/chapter4_real_python_dict'; 13 | 14 | import { 15 | HASH_CLASS_INIT_CODE, 16 | HASH_CLASS_SETITEM_RECYCLING_CODE, 17 | HASH_CLASS_SETITEM_SIMPLIFIED_CODE, 18 | _HASH_CLASS_GETITEM_ONLY, 19 | _HASH_CLASS_DELITEM_ONLY, 20 | HASH_CLASS_LOOKDICT, 21 | HASH_CLASS_RESIZE_CODE, 22 | FIND_NEAREST_SIZE_CODE_STRING, 23 | SLOT_CLASS_CODE_STRING, 24 | } from '../src/chapter3_hash_class'; 25 | 26 | import { 27 | HASH_CREATE_NEW_CODE, 28 | HASH_SEARCH_CODE, 29 | HASH_REMOVE_CODE, 30 | HASH_RESIZE_CODE, 31 | HASH_INSERT_CODE, 32 | } from '../src/chapter2_hash_table_functions'; 33 | 34 | import { 35 | SIMPLIFIED_INSERT_ALL_BROKEN_CODE, 36 | SIMPLIFIED_INSERT_ALL_CODE, 37 | SIMPLIFIED_SEARCH_CODE, 38 | SIMPLE_LIST_SEARCH, 39 | } from '../src/chapter1_simplified_hash'; 40 | 41 | import fs from 'fs'; 42 | import * as path from 'path'; 43 | 44 | function extractCodeLines(codeWithBpAndLevels) { 45 | return codeWithBpAndLevels.map(([line, bp, level]) => line); 46 | } 47 | 48 | function outputCode(filename, headers, importedCode, indent4 = true) { 49 | let allLines = []; 50 | for (let part of importedCode) { 51 | let lines; 52 | if (typeof part !== 'string') { 53 | lines = extractCodeLines(part); 54 | } else { 55 | lines = part.split('\n'); 56 | } 57 | 58 | allLines.push(...lines); 59 | if (lines[lines.length - 1] !== '') { 60 | allLines.push(''); 61 | } 62 | } 63 | const joinedLines = allLines.map(line => (line.length > 0 && indent4 ? ' ' + line : line)).join('\n'); 64 | fs.writeFileSync(filename, headers.join('\n') + '\n' + joinedLines); 65 | } 66 | 67 | const commonImports = `import sys 68 | import os 69 | sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'python_code')) 70 | from common import DUMMY, EMPTY 71 | 72 | `; 73 | 74 | const dict32def = ` 75 | class Dict32Extracted(object):`; 76 | 77 | const DIR = 'build'; 78 | 79 | outputCode( 80 | path.join(DIR, 'dict32js_extracted.py'), 81 | [commonImports, SLOT_CLASS_CODE_STRING, dict32def], 82 | [ 83 | DICT32_INIT, 84 | FIND_NEAREST_SIZE_CODE_STRING, 85 | STATICMETHOD_SIGNED_TO_UNSIGNED, 86 | DICT32_SETITEM, 87 | DICT32_RESIZE_CODE, 88 | _DICT32_GETITEM_ONLY, 89 | _DICT32_DELITEM_ONLY, 90 | DICT32_LOOKDICT, 91 | ] 92 | ); 93 | 94 | const hashClassRecyclingDef = ` 95 | class HashClassRecyclingExtracted(object):`; 96 | 97 | outputCode( 98 | path.join(DIR, 'hash_class_recycling_extracted.py'), 99 | [commonImports, SLOT_CLASS_CODE_STRING, hashClassRecyclingDef], 100 | [ 101 | HASH_CLASS_INIT_CODE, 102 | FIND_NEAREST_SIZE_CODE_STRING, 103 | HASH_CLASS_SETITEM_RECYCLING_CODE, 104 | HASH_CLASS_RESIZE_CODE, 105 | _HASH_CLASS_GETITEM_ONLY, 106 | _HASH_CLASS_DELITEM_ONLY, 107 | HASH_CLASS_LOOKDICT, 108 | ] 109 | ); 110 | 111 | const hashClassNoRecyclingDef = ` 112 | class HashClassNoRecyclingExtracted(object):`; 113 | 114 | outputCode( 115 | path.join(DIR, 'hash_class_no_recycling_extracted.py'), 116 | [commonImports, SLOT_CLASS_CODE_STRING, hashClassNoRecyclingDef], 117 | [ 118 | HASH_CLASS_INIT_CODE, 119 | FIND_NEAREST_SIZE_CODE_STRING, 120 | HASH_CLASS_SETITEM_SIMPLIFIED_CODE, 121 | HASH_CLASS_RESIZE_CODE, 122 | _HASH_CLASS_GETITEM_ONLY, 123 | _HASH_CLASS_DELITEM_ONLY, 124 | HASH_CLASS_LOOKDICT, 125 | ] 126 | ); 127 | 128 | outputCode( 129 | path.join(DIR, 'hash_chapter2_extracted.py'), 130 | [commonImports], 131 | [HASH_CREATE_NEW_CODE, HASH_SEARCH_CODE, HASH_REMOVE_CODE, HASH_RESIZE_CODE, HASH_INSERT_CODE], 132 | false 133 | ); 134 | 135 | outputCode( 136 | path.join(DIR, 'hash_chapter1_extracted.py'), 137 | [commonImports], 138 | [SIMPLIFIED_INSERT_ALL_CODE, SIMPLIFIED_INSERT_ALL_BROKEN_CODE, SIMPLIFIED_SEARCH_CODE, SIMPLE_LIST_SEARCH], 139 | false 140 | ); 141 | 142 | outputCode(path.join(DIR, 'chapter4_probing_python_code.py'), [commonImports], [PROBING_PYTHON_CODE], false); 143 | -------------------------------------------------------------------------------- /scripts/pyReimplWrapper.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | const split = require('split'); 3 | import 'ignore-styles'; 4 | 5 | import {BigNumber} from 'bignumber.js'; 6 | import {DUMMY, EMPTY, None} from '../src/hash_impl_common'; 7 | import {Dict32} from '../src/chapter4_real_python_dict'; 8 | import {GenerateProbingLinks} from '../src/probing_visualization.js'; 9 | import {AlmostPythonDict} from '../src/chapter3_hash_class'; 10 | import {Ops as Chapter2Ops} from '../src/chapter2_hash_table_functions'; 11 | import {Ops as Chapter1Ops} from '../src/chapter1_simplified_hash'; 12 | import {Slot} from '../src/chapter3_and_4_common'; 13 | import {List as ImmutableList} from 'immutable'; 14 | 15 | function parseSimplePyObj(obj) { 16 | if (obj === null || typeof obj === 'string') { 17 | return obj; 18 | } else if (typeof obj === 'object' && obj.type === 'None') { 19 | let res = None; 20 | // TODO FIXME: this does not support multiple clients 21 | res._hashCode = obj.hash; 22 | return res; 23 | } else if (typeof obj === 'object' && obj.type === 'DUMMY') { 24 | return DUMMY; 25 | } else if (typeof obj === 'object' && obj.type === 'EMPTY') { 26 | return EMPTY; 27 | } else if (typeof obj === 'object' && obj.type === 'int') { 28 | return BigNumber(obj.value); 29 | } else { 30 | throw new Error(`Unknown obj ${JSON.stringify(obj)}`); 31 | } 32 | } 33 | 34 | function parseArray(array) { 35 | return array.map(parseSimplePyObj); 36 | } 37 | 38 | function dumpArray(array) { 39 | return array.map(dumpSimplePyObj); 40 | } 41 | 42 | function parsePairs(pairs) { 43 | return pairs.map(([k, v]) => [parseSimplePyObj(k), parseSimplePyObj(v)]); 44 | } 45 | 46 | function dumpSimplePyObj(obj) { 47 | if (obj === DUMMY) { 48 | return { 49 | type: 'DUMMY', 50 | }; 51 | } else if (obj === None) { 52 | return { 53 | type: 'None', 54 | }; 55 | } else if (BigNumber.isBigNumber(obj)) { 56 | return { 57 | type: 'int', 58 | value: obj.toString(), 59 | }; 60 | } else { 61 | return obj; 62 | } 63 | } 64 | 65 | function restorePyDictState(state) { 66 | let {pySelf} = Dict32.__init__(); 67 | if (state.slots != null) { 68 | pySelf = pySelf.set( 69 | 'slots', 70 | new ImmutableList( 71 | state.slots.map(slot => { 72 | let key = parseSimplePyObj(slot.key); 73 | let value = parseSimplePyObj(slot.value); 74 | 75 | return Slot({ 76 | pyHashCode: slot.hashCode ? new BigNumber(slot.hashCode) : null, 77 | key: key, 78 | value: value, 79 | }); 80 | }) 81 | ) 82 | ); 83 | } else { 84 | pySelf = pySelf.set('slots', null); 85 | } 86 | pySelf = pySelf.set('used', state.used); 87 | pySelf = pySelf.set('fill', state.fill); 88 | 89 | return pySelf; 90 | } 91 | 92 | function dumpPyDictState(pySelf) { 93 | let data = {}; 94 | 95 | data.slots = pySelf.get('slots').map(slot => { 96 | return { 97 | hashCode: slot.pyHashCode != null ? slot.pyHashCode.toString() : null, 98 | key: dumpSimplePyObj(slot.key), 99 | value: dumpSimplePyObj(slot.value), 100 | }; 101 | }); 102 | data.used = pySelf.get('used'); 103 | data.fill = pySelf.get('fill'); 104 | 105 | return data; 106 | } 107 | 108 | function dict32RunOp(pySelf, op, key, value, pairs) { 109 | switch (op) { 110 | case '__init__': 111 | pySelf = Dict32.__init__(pairs).pySelf; 112 | return {pySelf}; 113 | case '__getitem__': { 114 | const {result, isException} = Dict32.__getitem__(pySelf, key); 115 | return {pySelf, result, isException}; 116 | } 117 | case '__setitem__': { 118 | ({pySelf} = Dict32.__setitem__(pySelf, key, value)); 119 | return {pySelf}; 120 | } 121 | case '__delitem__': { 122 | let isException; 123 | ({pySelf, isException} = Dict32.__delitem__(pySelf, key)); 124 | return {pySelf, isException}; 125 | } 126 | default: 127 | throw new Error('Unknown op: ' + op); 128 | } 129 | } 130 | 131 | function almostPyDictRunOp(pySelf, op, key, value, pairs) { 132 | switch (op) { 133 | case '__init__': 134 | pySelf = AlmostPythonDict.__init__(pairs).pySelf; 135 | return {pySelf}; 136 | case '__getitem__': { 137 | const {result, isException} = AlmostPythonDict.__getitem__(pySelf, key); 138 | return {pySelf, result, isException}; 139 | } 140 | case '__setitem__recycling': { 141 | ({pySelf} = AlmostPythonDict.__setitem__recycling(pySelf, key, value)); 142 | return {pySelf}; 143 | } 144 | case '__setitem__no_recycling': { 145 | ({pySelf} = AlmostPythonDict.__setitem__no_recycling(pySelf, key, value)); 146 | return {pySelf}; 147 | } 148 | case '__delitem__': { 149 | let isException; 150 | ({pySelf, isException} = AlmostPythonDict.__delitem__(pySelf, key)); 151 | return {pySelf, isException}; 152 | } 153 | default: 154 | throw new Error('Unknown op: ' + op); 155 | } 156 | } 157 | 158 | function chapter1run(keys, op, key, numbers) { 159 | switch (op) { 160 | case 'create_new': 161 | ({keys} = Chapter1Ops.createNew(numbers)); 162 | return {keys}; 163 | case 'create_new_broken': 164 | ({keys} = Chapter1Ops.createNewBroken(numbers)); 165 | return {keys}; 166 | case 'has_key': { 167 | let result; 168 | ({keys, result} = Chapter1Ops.hasKey(keys, key)); 169 | return {keys, result}; 170 | } 171 | case 'linear_search': { 172 | let {result} = Chapter1Ops.linearSearch(numbers, key); 173 | return {result}; 174 | } 175 | default: 176 | throw new Error('Unknown op: ' + op); 177 | } 178 | } 179 | 180 | function chapter2run(hashCodes, keys, op, key, array) { 181 | switch (op) { 182 | case 'create_new': 183 | ({hashCodes, keys} = Chapter2Ops.createNew(array)); 184 | return {hashCodes, keys}; 185 | case 'insert': 186 | ({hashCodes, keys} = Chapter2Ops.insert(hashCodes, keys, key)); 187 | return {hashCodes, keys}; 188 | case 'remove': { 189 | let isException; 190 | ({hashCodes, keys, isException} = Chapter2Ops.remove(hashCodes, keys, key)); 191 | return {hashCodes, keys, isException}; 192 | } 193 | case 'has_key': { 194 | let result; 195 | ({hashCodes, keys, result} = Chapter2Ops.hasKey(hashCodes, keys, key)); 196 | return {hashCodes, keys, result}; 197 | } 198 | case 'resize': 199 | ({hashCodes, keys} = Chapter2Ops.resize(hashCodes, keys)); 200 | return {hashCodes, keys}; 201 | default: 202 | throw new Error('Unknown op: ' + op); 203 | } 204 | } 205 | 206 | const server = net.createServer(c => { 207 | console.log('Client connected'); 208 | 209 | c.on('end', () => { 210 | console.log('Client disconnected'); 211 | }); 212 | 213 | c.pipe(split()).on('data', line => { 214 | console.log('Received line of length ' + line.length); 215 | if (!line) return; 216 | 217 | const data = JSON.parse(line); 218 | const dictType = data.dict; 219 | const op = data.op; 220 | let {key, value, pairs, array} = data.args; 221 | if (key !== undefined) { 222 | key = parseSimplePyObj(key); 223 | } 224 | if (value !== undefined) { 225 | value = parseSimplePyObj(value); 226 | } 227 | if (pairs !== undefined) { 228 | pairs = parsePairs(pairs); 229 | } 230 | if (array !== undefined) { 231 | array = parseArray(array); 232 | } 233 | 234 | console.log(op, data.args); 235 | 236 | let isException, result; 237 | let response; 238 | 239 | if (dictType === 'dict32' || dictType === 'almost_python_dict') { 240 | let pySelf = restorePyDictState(data.self); 241 | if (dictType === 'dict32') { 242 | ({pySelf, isException, result} = dict32RunOp(pySelf, op, key, value, pairs)); 243 | } else if (dictType === 'almost_python_dict') { 244 | ({pySelf, isException, result} = almostPyDictRunOp(pySelf, op, key, value, pairs)); 245 | } else { 246 | throw new Error('Unknown dict type'); 247 | } 248 | 249 | response = { 250 | exception: isException || false, 251 | result: result !== undefined ? dumpSimplePyObj(result) : null, 252 | self: dumpPyDictState(pySelf), 253 | }; 254 | } else if (dictType === 'chapter2') { 255 | let hashCodes = data.hashCodes != null ? new ImmutableList(parseArray(data.hashCodes)) : undefined; 256 | let keys = data.keys != null ? new ImmutableList(parseArray(data.keys)) : undefined; 257 | ({hashCodes, keys, isException, result} = chapter2run(hashCodes, keys, op, key, array)); 258 | response = { 259 | exception: isException || false, 260 | result: result !== undefined ? result : null, 261 | hashCodes: dumpArray(hashCodes), 262 | keys: dumpArray(keys), 263 | }; 264 | } else if (dictType === 'chapter1') { 265 | let keys = data.keys != null ? new ImmutableList(parseArray(data.keys)) : undefined; 266 | ({keys, result} = chapter1run(keys, op, key, array)); 267 | response = { 268 | result: result !== undefined ? result : null, 269 | keys: keys !== undefined ? dumpArray(keys) : null, 270 | }; 271 | } else if (dictType === 'pythonProbing') { 272 | let g = new GenerateProbingLinks(); 273 | const result = g.run(data.args.slotsCount, key, 'python'); 274 | response = { 275 | result, 276 | }; 277 | } else { 278 | throw new Error('Unknown dict type'); 279 | } 280 | 281 | c.write(JSON.stringify(response) + '\n'); 282 | }); 283 | }); 284 | 285 | server.on('error', err => { 286 | throw err; 287 | }); 288 | 289 | server.on('listening', () => { 290 | console.log(`Listening`); 291 | }); 292 | 293 | server.listen('pynode.sock', () => { 294 | console.log('Starting listening...'); 295 | }); 296 | -------------------------------------------------------------------------------- /scripts/ssr.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | process.env.NODE_ENV = 'ssr'; 4 | 5 | console.log = () => {}; // Do not log to stdout 6 | global.performance = {now: () => 0}; 7 | import 'ignore-styles'; 8 | 9 | import * as React from 'react'; 10 | import ReactDOMServer from 'react-dom/server'; 11 | import {CHAPTER_ID_TO_COMPONENT} from '../src/index'; 12 | import {App} from '../src/app'; 13 | import fs from 'fs'; 14 | 15 | const filename = process.argv[2]; 16 | const chapterIds = JSON.parse(process.argv[3]); 17 | const chapters = chapterIds.map(id => CHAPTER_ID_TO_COMPONENT[id]); 18 | let selectedChapterId; 19 | if (chapterIds.length === 1) { 20 | selectedChapterId = chapterIds[0]; 21 | } 22 | 23 | fs.readFile(filename, 'utf8', function(err, file) { 24 | if (err) { 25 | throw new Error(`Cannot read source html: ${err}`); 26 | } 27 | const renderedComponent = ReactDOMServer.renderToString( 28 | 29 | ); 30 | let fullHtml = file.replace(/
<\/div>/, `
${renderedComponent}
`); 31 | const gaId = process.env.GA_ID; 32 | console.warn('Google analytics ID is', gaId); 33 | if (gaId) { 34 | let GA_SCRIPT = ` 35 | 36 | `; 43 | GA_SCRIPT = GA_SCRIPT.replace(/__GA_CODE_HERE__/g, gaId); 44 | fullHtml = fullHtml.replace('', `${GA_SCRIPT}`); 45 | } 46 | process.stdout.write(fullHtml); 47 | }); 48 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import Bootstrap from 'bootstrap/dist/css/bootstrap.min.css'; 3 | import stylesCss from './styles.css'; 4 | 5 | import * as React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | 8 | import {MyErrorBoundary, initUxSettings, getUxSettings, BootstrapAlert, doubleRAF} from './util'; 9 | import {win, globalSettings} from './store'; 10 | 11 | import {faDesktop} from '@fortawesome/free-solid-svg-icons/faDesktop'; 12 | import {faSpinner} from '@fortawesome/free-solid-svg-icons/faSpinner'; 13 | import {faSyncAlt} from '@fortawesome/free-solid-svg-icons/faSyncAlt'; 14 | import {faEnvelope} from '@fortawesome/free-solid-svg-icons/faEnvelope'; 15 | import {faChevronRight} from '@fortawesome/free-solid-svg-icons/faChevronRight'; 16 | import {faFirefox} from '@fortawesome/free-brands-svg-icons/faFirefox'; 17 | import {faGithub} from '@fortawesome/free-brands-svg-icons/faGithub'; 18 | import {faTwitter} from '@fortawesome/free-brands-svg-icons/faTwitter'; 19 | import {faMailchimp} from '@fortawesome/free-brands-svg-icons/faMailchimp'; 20 | 21 | import {library, config as fontAwesomeConfig} from '@fortawesome/fontawesome-svg-core'; 22 | fontAwesomeConfig.autoAddCss = false; 23 | 24 | library.add(faDesktop); 25 | library.add(faFirefox); 26 | library.add(faSpinner); 27 | library.add(faSyncAlt); 28 | library.add(faEnvelope); 29 | library.add(faChevronRight); 30 | library.add(faGithub); 31 | library.add(faMailchimp); 32 | library.add(faTwitter); 33 | 34 | import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; 35 | 36 | import '@fortawesome/fontawesome-svg-core/styles.css'; 37 | 38 | function getWindowDimensions() { 39 | const width = document.documentElement.clientWidth; 40 | const height = document.documentElement.clientHeight; 41 | return {width, height}; 42 | } 43 | 44 | function logViewportStats() { 45 | console.log(`DIMENSIONS: window inner: ${window.innerWidth}x${window.innerHeight}`); 46 | console.log( 47 | `DIMENSIONS: document.documentElement: ${document.documentElement.clientWidth}x${ 48 | document.documentElement.clientHeight 49 | }` 50 | ); 51 | const vv = window.visualViewport; 52 | console.log(`DIMENSIONS: visualViewport: ${vv != null ? vv.width + 'x' + vv.height : vv}`); 53 | 54 | const {width, height} = getWindowDimensions(); 55 | console.log(`DIMENSIONS: used: ${width}x${height}`); 56 | // TODO FIXME: this is for debugging only 57 | /*const url = `/viewports?wi=${window.innerWidth}x${window.innerHeight}&de=${document.documentElement.clientWidth}x${document.documentElement.clientHeight}&vv=${vv.width}x${vv.height}`; 58 | const Http = new XMLHttpRequest(); 59 | Http.open("GET", url); 60 | Http.send();*/ 61 | } 62 | 63 | const GITHUB_REPO_URL = 'https://github.com/eleweek/inside_python_dict'; 64 | const MAILCHIMP_URL = 'http://eepurl.com/gbzhvn'; 65 | const TWITTER_LINK = 'https://twitter.com/SashaPutilin'; 66 | const EMAIL = 'avp-13@yandex.ru'; 67 | 68 | function GithubRibbon() { 69 | return ( 70 | 71 | Fork me on GitHub 76 | 77 | ); 78 | } 79 | 80 | function GithubCorner() { 81 | // FROM: http://tholman.com/github-corners/ 82 | return ( 83 |
`, 86 | }} 87 | /> 88 | ); 89 | } 90 | 91 | function GithubForkMe({windowWidth}) { 92 | /*if (windowWidth != null && windowWidth > 1150) { 93 | return ; 94 | } else {*/ 95 | return ; 96 | /*}*/ 97 | } 98 | 99 | const CONTENTS_DATA = [ 100 | [1, 'chapter1.html', 'Searching efficiently in a list'], 101 | [2, 'chapter2.html', 'Why are hash tables called hash tables?'], 102 | [3, 'chapter3.html', 'Putting it all together to make an "almost"-python-dict'], 103 | [4, 'chapter4.html', 'How python dict *really* works internally'], 104 | ]; 105 | 106 | function chapterIdDotHtml(chapterId) { 107 | if (chapterId && !chapterId.endsWith('.html')) { 108 | return chapterId + '.html'; 109 | } else { 110 | return null; 111 | } 112 | } 113 | 114 | function NextPrev({selectedChapterId}) { 115 | const selectedChapter = chapterIdDotHtml(selectedChapterId); 116 | if (selectedChapter == null) { 117 | return null; 118 | } 119 | 120 | let prevHref, prevTitle; 121 | let nextHref, nextTitle; 122 | 123 | for (let i = 0; i < CONTENTS_DATA.length; ++i) { 124 | if (CONTENTS_DATA[i][1] === selectedChapter) { 125 | if (i > 0) { 126 | prevHref = CONTENTS_DATA[i - 1][1]; 127 | prevTitle = CONTENTS_DATA[i - 1][2]; 128 | } 129 | if (i < CONTENTS_DATA.length - 1) { 130 | nextHref = CONTENTS_DATA[i + 1][1]; 131 | nextTitle = CONTENTS_DATA[i + 1][2]; 132 | } 133 | break; 134 | } 135 | } 136 | 137 | if (nextHref) { 138 | return ( 139 | 146 | ); 147 | } else { 148 | return null; 149 | } 150 | } 151 | 152 | class Contents extends React.PureComponent { 153 | static EXTRA_ERROR_BOUNDARY = true; 154 | 155 | render() { 156 | const {selectedChapterId} = this.props; 157 | const selectedChapter = chapterIdDotHtml(selectedChapterId); 158 | const CIRCLE_SIZE = 30; 159 | return ( 160 |
161 |
162 | {CONTENTS_DATA.map(([i, href, title]) => { 163 | const contentRow = ( 164 | 165 |
179 | {i} 180 |
181 |
182 |
{title}
183 |
184 |
185 | ); 186 | return ( 187 |
192 | {selectedChapter === href ? ( 193 | contentRow 194 | ) : ( 195 | 201 | {contentRow} 202 | 203 | )} 204 |
205 | ); 206 | })} 207 |
208 |
209 | ); 210 | } 211 | } 212 | 213 | class LoadingAlert extends React.PureComponent { 214 | constructor() { 215 | super(); 216 | 217 | this.state = { 218 | loaded: false, 219 | }; 220 | } 221 | 222 | render() { 223 | return ( 224 | 232 | JavaScript code is loading... 233 | 234 | ); 235 | } 236 | 237 | componentDidMount() { 238 | this.setState({loaded: true}); 239 | } 240 | } 241 | 242 | class Alerts extends React.Component { 243 | constructor() { 244 | super(); 245 | 246 | this.state = { 247 | mounted: false, 248 | }; 249 | } 250 | 251 | render() { 252 | const alerts = []; 253 | const isRunningInBrowser = typeof window !== 'undefined'; 254 | alerts.push(); 255 | 256 | if (this.state.mounted) { 257 | const {browser, windowWidth, windowHeight} = this.props; 258 | if (browser) { 259 | if (browser.platform.type === 'mobile') { 260 | alerts.push( 261 | 262 | Mobile device detected. For the best 263 | experience use a desktop browser 264 | 265 | ); 266 | if (windowWidth < windowHeight) { 267 | alerts.push( 268 | 269 | Rotating your device is recommended{' '} 270 | - animations are better with a wider viewport 271 | 272 | ); 273 | } 274 | } else if (browser.browser.name === 'Firefox' && browser.os.name !== 'Linux') { 275 | alerts.push( 276 | 277 | Firefox detected. Heavy 278 | animations may lag sometimes. If this happens, Chrome or Safari is recommended. 279 | 280 | ); 281 | } 282 | } 283 | } 284 | 285 | return {alerts}; 286 | } 287 | 288 | componentDidMount() { 289 | this.setState({mounted: true}); 290 | } 291 | } 292 | 293 | function Footer() { 294 | return ( 295 | 326 | ); 327 | } 328 | 329 | // mainly to prevent addressbar stuff on mobile changing things excessively 330 | const SIGNIFICANT_HEIGHT_CHANGE = 100; 331 | export class App extends React.Component { 332 | constructor() { 333 | super(); 334 | this.state = { 335 | mounted: false, 336 | windowWidth: null, 337 | windowHeight: null, 338 | }; 339 | } 340 | 341 | windowSizeChangeHandle = () => { 342 | logViewportStats(); 343 | const dimensions = getWindowDimensions(); 344 | const windowWidth = dimensions.width; 345 | const windowHeight = dimensions.height; 346 | if (this.state.windowWidth !== windowWidth || this.state.windowHeight !== windowHeight) { 347 | console.log('Processing window size change', windowWidth, windowHeight); 348 | if ( 349 | this.state.windowWidth != windowWidth || 350 | this.state.windowHeight > windowHeight || 351 | windowHeight - this.state.windowHeight > SIGNIFICANT_HEIGHT_CHANGE 352 | ) { 353 | console.log('App size changed from', this.state); 354 | this.setState({ 355 | windowWidth, 356 | windowHeight, 357 | }); 358 | if (win.width !== windowWidth || win.height !== windowHeight) { 359 | win.setWH(windowWidth, windowHeight); 360 | } 361 | } 362 | fixStickyResize(windowWidth, windowHeight); 363 | } 364 | }; 365 | 366 | componentDidMount() { 367 | const MEANINGFUL_Y_DIFF = 50; // components that depend on scroll should allow some leeway 368 | let lastScrollY = null; 369 | const onScroll = _.throttle(() => { 370 | if (!lastScrollY || Math.abs(lastScrollY - window.scrollY) > MEANINGFUL_Y_DIFF) { 371 | console.log('onScroll triggered', window.scrollY); 372 | win.setScrollY(window.scrollY); 373 | lastScrollY = window.scrollY; 374 | } 375 | }, 100); 376 | window.addEventListener('scroll', onScroll); 377 | 378 | const dimensions = getWindowDimensions(); 379 | const windowWidth = dimensions.width; 380 | const windowHeight = dimensions.height; 381 | console.log('componentDidMount() window geometry', windowWidth, windowHeight); 382 | 383 | window.addEventListener('resize', _.throttle(this.windowSizeChangeHandle, 500)); 384 | globalSettings.maxCodePlaySpeed = getUxSettings().MAX_CODE_PLAY_SPEED; 385 | 386 | this.setState({ 387 | windowWidth, 388 | windowHeight, 389 | mounted: true, 390 | }); 391 | win.setAll(windowWidth, windowHeight, window.scrollY, true); 392 | } 393 | 394 | componentWillUnmount() { 395 | window.removeEventListener('resize', this.windowSizeChangeHandle); 396 | } 397 | 398 | render() { 399 | console.log('App.render()'); 400 | const contents = ; 401 | const independentContents = this.props.selectedChapterId === 'chapter1'; 402 | // Make sure SSR works 403 | const {windowWidth, windowHeight} = this.state.mounted ? this.state : {}; 404 | 405 | let chapters = []; 406 | for (let [i, Chapter] of this.props.chapters.entries()) { 407 | chapters.push( 408 | 409 | 410 | 411 | ); 412 | } 413 | return ( 414 | 415 |
416 | 417 | 418 | 419 |

Inside python dict — an explorable explanation

420 | 421 | 422 | 423 | {!independentContents && {contents}} 424 | {chapters} 425 | 426 | 427 | 428 |
429 |