├── sympad ├── __init__.py └── __main__.py ├── bg.png ├── sympad.png ├── wait.webp ├── aliases.sh ├── .gitignore ├── LICENSE ├── setup.py ├── README.md ├── make_single_script.py ├── style.css ├── index.html ├── changelog.txt ├── make_parser_tables.py ├── lalr1.py ├── spatch.py ├── todo.txt ├── splot.py ├── script.js ├── sxlat.py ├── server.py └── test_sym.py /sympad/__init__.py: -------------------------------------------------------------------------------- 1 | from .sympad import main -------------------------------------------------------------------------------- /bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tom-pytel/sympad/HEAD/bg.png -------------------------------------------------------------------------------- /sympad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tom-pytel/sympad/HEAD/sympad.png -------------------------------------------------------------------------------- /wait.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tom-pytel/sympad/HEAD/wait.webp -------------------------------------------------------------------------------- /sympad/__main__.py: -------------------------------------------------------------------------------- 1 | if __name__ == '__main__': 2 | from sympad import main 3 | main () 4 | -------------------------------------------------------------------------------- /aliases.sh: -------------------------------------------------------------------------------- 1 | alias t="./test_sym.py" 2 | alias ti="./test_sym.py -i" 3 | alias te="./test_sym.py -e" 4 | alias tc="./test_sym_constant.bat" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .vscode 3 | .venv* 4 | parser.out 5 | test.txt 6 | build 7 | dist 8 | build 9 | dist 10 | sympad.egg-info 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Tomasz Pytel 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 17 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import setuptools 4 | 5 | setuptools.setup ( 6 | name = "sympad", 7 | version = "1.1.6", 8 | author = "Tomasz Pytel", 9 | author_email = "tom_pytel@yahoo.com", 10 | license = 'BSD', 11 | keywords = "Math CAS SymPy GUI", 12 | description = "Graphical symbolic math calculator / scratchpad using SymPy", 13 | long_description = "SymPad is a simple single script graphical symbolic calculator / scratchpad using SymPy for the math, MathJax for the display in a browser and matplotlib for plotting. " 14 | "User input is intended to be quick, easy and intuitive and is displayed in symbolic form as it is being entered. " 15 | "Sympad will accept Python expressions, LaTeX formatting, unicode math symbols and a native shorthand intended for quick entry, or a mix of all of these. " 16 | "The input will be evaluated symbolically or numerically with the results being copy/pasteable in Python or LaTeX formats, so it acts as a translator as well.", 17 | long_description_content_type = "text/plain", 18 | url = "https://github.com/Pristine-Cat/sympad", 19 | packages = ['sympad'], 20 | scripts = ['bin/sympad'], 21 | classifiers = [ 22 | 'Intended Audience :: Education', 23 | 'Intended Audience :: Science/Research', 24 | 'License :: OSI Approved :: BSD License', 25 | 'Operating System :: OS Independent', 26 | 'Programming Language :: Python', 27 | 'Programming Language :: Python :: 3', 28 | 'Programming Language :: Python :: 3 :: Only', 29 | 'Topic :: Scientific/Engineering', 30 | 'Topic :: Scientific/Engineering :: Mathematics', 31 | ], 32 | install_requires = ['sympy>=1.7.1'], 33 | python_requires = '>=3.6', 34 | ) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SymPad 2 | 3 | SymPad is a simple single script graphical symbolic calculator / scratchpad using SymPy for the math, MathJax for the display in a browser and matplotlib for plotting. 4 | User input is intended to be quick, easy and intuitive and is displayed in symbolic form as it is being entered. 5 | Sympad will accept Python expressions, LaTeX formatting, unicode math symbols and a native shorthand intended for quick entry, or a mix of all of these. 6 | The input will be evaluated symbolically or numerically with the results being copy/pasteable in Python or LaTeX formats, so it acts as a translator as well. 7 | 8 | The following are examples of valid inputs to SymPad: 9 | ``` 10 | cos -pi 11 | N cos**-1 -\log_2 sqrt[4] 16 12 | \lim_{x\to\infty} 1/x 13 | \sum_{n=0}**oo x^n / n! 14 | d**6 / dx dy**2 dz**3 x^3 y^3 z^3 15 | \int_0^\pi \int_0^{2pi} \int_0^1 rho**2 sin\phi drho dtheta dphi 16 | \[[1, 2], [3, 4]]**-1 17 | Matrix (4, 4, lambda r, c: c + r if c > r else 0) 18 | factor (x**3 + 3 y x**2 + 3 x y**2 + y**3) 19 | f (x, y) = sqrt (x**2 + y**2) 20 | solve (x**2 + y = 4, x) 21 | y = y (t) 22 | dsolve (y'' - 4y' - 12y = 3e**{5t}) 23 | pdsolve (x * d/dx u (x, y) - y * d/dy u (x, y) + y**2 u (x, y) - y**2) 24 | ``` 25 | 26 | And here are some examples of the visual output as you type: 27 | 28 | ![SymPad image example](https://raw.githubusercontent.com/Pristine-Cat/SymPad/master/sympad.png#1) 29 | 30 | ## Installation 31 | 32 | If you just want to use the program you only need the file **sympad.py**, and of course Python and the SymPy Python package. 33 | You can download **sympad.py** from here or install it directly from the PyPI repository. 34 | This version of SymPad works with SymPy 1.5, a version of SymPad which works correctly with SymPy 1.4 is available on the branch **SymPy1.4**. 35 | 36 | To install the latest version on Linux from PyPI: 37 | ``` 38 | pip3 install sympad 39 | ``` 40 | To install on Windows from PyPI: 41 | ``` 42 | pip3 install --user sympad 43 | ``` 44 | 45 | If you install SymPad from PyPI then SymPy will be installed automatically. 46 | 47 | If you want to install SymPy yourself: [https://sympy.org/](https://sympy.org/) 48 | ``` 49 | pip3 install sympy 50 | ``` 51 | 52 | In order to get the optional plotting functionality you must have the matplotlib package installed: [https://matplotlib.org/](https://matplotlib.org/) 53 | ``` 54 | pip3 install matplotlib 55 | ``` 56 | 57 | As said to use the program you just need **sympad.py**, this is an autogenerated Python script which contains all the modules and web resources in one handy easily copyable file. 58 | Otherwise if you want to play with the code then download everything and run the program using **server.py**. 59 | 60 | If you want to regenerate the parser tables you will need the PLY Python package: [https://www.dabeaz.com/ply/](https://www.dabeaz.com/ply/) 61 | ``` 62 | pip3 install ply 63 | ``` 64 | 65 | ## Running SymPad 66 | 67 | If you downloaded **sympad.py** from here and have SymPy installed then you can just run the SymPad script from anywhere. 68 | 69 | If you installed from PyPI then you can run SymPad on Linux or Windows via: 70 | ``` 71 | python -m sympad 72 | ``` 73 | 74 | On Linux a non-py extension script version is also installed so you can just run it as: 75 | ``` 76 | sympad 77 | ``` 78 | 79 | ## Open-Source License 80 | 81 | SymPad is made available under the BSD license, you may use it as you wish, as long as you copy the BSD statement if you redistribute it. See the LICENSE file for details. 82 | -------------------------------------------------------------------------------- /make_single_script.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # python 3.6+ 3 | 4 | # Collect all source and data files into single stand-alone sympad.py script file. 5 | 6 | import io 7 | import os 8 | 9 | _TEXT_FILES = ('style.css', 'script.js', 'index.html', 'help.html') 10 | _BIN_FILES = ('bg.png', 'wait.webp') 11 | _PY_FILES = ('lalr1.py', 'sast.py', 'sxlat.py', 'sym.py', 'sparser.py', 'spatch.py', 'splot.py', 'server.py') 12 | 13 | _HEADER = ''' 14 | #!/usr/bin/env python3 15 | # python 3.6+ 16 | 17 | # THIS SCRIPT WAS AUTOGENERATED FROM SOURCE FILES FOUND AT: 18 | # https://github.com/Pristine-Cat/SymPad 19 | 20 | # Copyright (c) 2019 Tomasz Pytel 21 | # All rights reserved. 22 | # 23 | # Redistribution and use in source and binary forms, with or without 24 | # modification, are permitted provided that the following conditions are met: 25 | # 26 | # * Redistributions of source code must retain the above copyright 27 | # notice, this list of conditions and the following disclaimer. 28 | # * Redistributions in binary form must reproduce the above copyright 29 | # notice, this list of conditions and the following disclaimer in the 30 | # documentation and/or other materials provided with the distribution. 31 | # 32 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 33 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 34 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 35 | # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 36 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 37 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 38 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 39 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 40 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 41 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 42 | 43 | _RUNNING_AS_SINGLE_SCRIPT = True 44 | 45 | import sys 46 | sys.path.insert (0, '') # allow importing from current directory first (for SymPy development version) 47 | '''.lstrip () 48 | 49 | if __name__ == '__main__': 50 | fdout = io.StringIO () 51 | 52 | fdout.write (_HEADER) 53 | fdout.write ('\n_FILES = {\n') 54 | 55 | for fnm in _TEXT_FILES: 56 | fdout.write (f'''\n\t'{fnm}': # {fnm}\n\nr"""''') 57 | 58 | for line in open (fnm, encoding="utf8"): 59 | fdout.write (line) 60 | 61 | fdout.write ('""".encode ("utf8"),\n') 62 | 63 | for fnm in _BIN_FILES: 64 | fdout.write (f"\n\t'{fnm}': # {fnm}\n\n") 65 | 66 | data = open (fnm, 'rb').read () 67 | 68 | for i in range (0, len (data), 64): 69 | fdout.write (f"\t\t{data [i : i + 64]!r}{',' if i + 64 >= len (data) else ''}\n") 70 | 71 | fdout.write ('}\n\n') 72 | 73 | for fnm in _PY_FILES: 74 | writing = True 75 | 76 | for line in open (fnm): 77 | liner = line.rstrip () 78 | 79 | if writing: 80 | if not liner.endswith ('# AUTO_REMOVE_IN_SINGLE_SCRIPT'): 81 | if liner.lstrip () == '# AUTO_REMOVE_IN_SINGLE_SCRIPT_BLOCK_START': 82 | writing = False 83 | else: 84 | fdout.write (line) 85 | 86 | else: 87 | if liner.lstrip () == '# AUTO_REMOVE_IN_SINGLE_SCRIPT_BLOCK_END': 88 | writing = True 89 | 90 | open ('sympad/sympad.py', 'w', newline = '', encoding="utf8").write (fdout.getvalue ()) 91 | open ('bin/sympad', 'w', newline = '', encoding="utf8").write (fdout.getvalue ()) 92 | 93 | os.chmod ('bin/sympad', 0o755) 94 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | body { 8 | margin-top: 1em; 9 | margin-bottom: 6em; 10 | cursor: default; 11 | background-image: url('/bg.png'); 12 | } 13 | 14 | i { 15 | color: #0008; 16 | } 17 | 18 | #Clipboard { 19 | position: fixed; 20 | bottom: -2em; 21 | color: transparent; 22 | border: 0px; 23 | } 24 | 25 | #Greeting { 26 | position: fixed; 27 | left: 50%; 28 | top: 50%; 29 | transform: translate(-50%, -50%); 30 | color: #0007; 31 | } 32 | 33 | .GreetingA { 34 | display: block; 35 | color: #0007; 36 | text-decoration: none; 37 | margin-bottom: 0.5em; 38 | } 39 | 40 | #InputCover { 41 | position: fixed; 42 | z-index: 2; 43 | height: 4em; 44 | bottom: 0; 45 | left: 0; 46 | right: 0; 47 | background-image: url('/bg.png'); 48 | } 49 | 50 | #InputCoverLeft { 51 | position: fixed; 52 | z-index: 5; 53 | height: 4em; 54 | bottom: 0; 55 | left: 0; 56 | background-image: url('/bg.png'); 57 | } 58 | 59 | #InputCoverRight { 60 | position: fixed; 61 | z-index: 5; 62 | height: 4em; 63 | bottom: 0; 64 | right: 0; 65 | background-image: url('/bg.png'); 66 | } 67 | 68 | #Input { 69 | position: fixed; 70 | z-index: 3; 71 | bottom: 2em; 72 | left: 4em; 73 | right: 1em; 74 | border-color: transparent; 75 | outline-color: transparent; 76 | background-color: transparent; 77 | } 78 | 79 | #InputOverlay { 80 | z-index: 4; 81 | pointer-events: none; 82 | } 83 | 84 | #OverlayGood { 85 | white-space: pre; 86 | -webkit-text-fill-color: transparent; 87 | } 88 | 89 | #OverlayError { 90 | position: absolute; 91 | white-space: pre; 92 | -webkit-text-fill-color: #f44; 93 | } 94 | 95 | #OverlayAutocomplete { 96 | position: absolute; 97 | white-space: pre; 98 | -webkit-text-fill-color: #999; 99 | } 100 | 101 | .LogEntry { 102 | width: 100%; 103 | margin-bottom: 1.5em; 104 | } 105 | 106 | .LogMargin { 107 | display: inline-block; 108 | height: 100%; 109 | width: 4em; 110 | vertical-align: top; 111 | text-align: right; 112 | padding-right: 0.5em; 113 | color: #0008; 114 | } 115 | 116 | .LogBody { 117 | display: inline-block; 118 | margin-right: -9999em; 119 | } 120 | 121 | .LogWait { 122 | vertical-align: top; 123 | } 124 | 125 | .LogInput { 126 | margin-bottom: 0.75em; 127 | width: fit-content; 128 | cursor: pointer; 129 | } 130 | 131 | .LogEval { 132 | position: relative; 133 | margin-bottom: 0.25em; 134 | cursor: pointer; 135 | } 136 | 137 | .LogMsg { 138 | margin-bottom: 0.25em; 139 | } 140 | 141 | .LogError { 142 | margin-bottom: 0.25em; 143 | color: red; 144 | } 145 | 146 | .LogErrorTriange { 147 | position: absolute; 148 | left: -1.25em; 149 | top: 0.25em; 150 | font-size: 0.7em; 151 | color: red; 152 | font-weight: bold; 153 | } 154 | 155 | #VarDiv { 156 | display: none; 157 | position: fixed; 158 | right: 1em; 159 | top: 0; 160 | box-shadow: 4px 4px 12px 2px #0002; 161 | } 162 | 163 | #VarTab { 164 | border: 1px solid #0004; 165 | border-top: 0; 166 | background-color: black; 167 | background-image: url('/bg.png'); 168 | color: #0008; 169 | } 170 | 171 | #VarTab:hover { 172 | color: black; 173 | } 174 | 175 | #VarTabShadow { 176 | z-index: -1; 177 | box-shadow: 4px 4px 12px 2px #0002; 178 | } 179 | 180 | #VarTabShadow, #VarTab { 181 | position: absolute; 182 | right: 0; 183 | bottom: -1.65em; 184 | padding: 0.25em; 185 | } 186 | 187 | #VarContent { 188 | /* display: none; */ 189 | min-width: 6.1em; 190 | padding: 0.25em 0.25em; 191 | border: 1px solid #0004; 192 | border-top: 0; 193 | background-image: url('/bg.png'); 194 | } 195 | 196 | #VarTable { 197 | margin-right: 0; 198 | margin-left: auto; 199 | border-spacing: 0; 200 | } 201 | 202 | table.VarTable td { 203 | padding: 0.25em; 204 | } 205 | 206 | .CopySpan { 207 | display: inline-block; 208 | } 209 | 210 | #ValidationError { 211 | color: #f99; 212 | } 213 | 214 | table.HelpTable td { 215 | vertical-align: top; 216 | padding: 0.25em 0; 217 | } 218 | 219 | table.HelpTable tr td:first-child { 220 | white-space: nowrap; 221 | } 222 | 223 | table.HelpTable tr td:nth-child(2) { 224 | padding: 0.25em 0.5em; 225 | } 226 | 227 | p span:hover { 228 | color: gray; 229 | } 230 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | SymPad 10 | 11 | 12 | 13 | 14 | 15 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 |
33 |

SymPad

34 |
35 |
on SymPy
36 | 37 |

38 | Type 'help' or '?' at any time for more information. 39 |
40 | - or - 41 |
42 | Type or click any of the following to get started: 43 |
44 |

45 | cos -pi 46 | N cos**-1 -\log_2 sqrt[4] 16 47 | expand ((1 + x)**4) 48 | factor (x^3 + 3y x^2 + 3x y^2 + y^3) 49 | series (e^x, x, 0, 5) 50 | Limit (\frac1x, x, 0, dir='-') 51 | \sum_{n=0}**oo x^n / n! 52 | d**6 / dx dy**2 dz**3 x^3 y^3 z^3 53 | Integral (e^{-x^2}, (x, 0, \infty)) 54 | \int_0^\pi \int_0^{2pi} \int_0^1 rho**2 sin\phi drho dtheta dphi 55 | \[[1, 2], [3, 4]]**-1 56 | Matrix (4, 4, lambda r, c: c + r if c > r else 0) 57 | f (x, y) = sqrt (x**2 + y**2) 58 | solve (x**2 + y = 4, x) 59 | dsolve (y(x)'' + 9y(x)) 60 | y = y(t); dsolve (y'' - 4y' - 12y = 3e**{5t}); del y 61 | pdsolve (x * d/dx u (x, y) - y * d/dy u (x, y) + y**2u (x, y) - y**2) 62 | (({1, 2, 3} && {2, 3, 4}) ^^ {3, 4, 5}) - \{4} || {7,} 63 | simplify (not (not a and not b) and not (not a or not c)) 64 | plotf (2pi, -2, 2, sin x, 'r=sin', cos x, 'g=cos', tan x, 'b=tan') 65 |
66 | More Examples... 67 | 72 | 73 |

74 |
75 | Copyright (c) 2019 Tomasz Pytel. SymPad on GitHub 76 |
77 |
78 | 79 |
80 | 81 |
82 |
83 |
84 | 85 |
86 | 87 |
88 |
89 |
Variables
90 |
Variables
91 |
92 | 93 | 94 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | v1.1.6: 2 | ------- 3 | * Updated for SymPy 1.7.1. 4 | 5 | v1.1.5: 6 | ------- 7 | * Updated for SymPy 1.5.1. 8 | 9 | v1.1.4: 10 | ------- 11 | * Made top level SymPy functions assignable to vars and usable as vars and function object arguments. 12 | * Improved parsing and error marking incomplete matrices. 13 | * Fixed variable mapping to allow vars mapped to vars mapped to lambdas to work as functions. 14 | 15 | v1.1.3: 16 | ------- 17 | * Updated matrix multiplication simplification. 18 | * Fix bug where an incomplete matrix during entry would make the rest of the expression disappear. 19 | 20 | v1.1.2: 21 | ------- 22 | * Upgraded to SymPy 1.5. 23 | * Added usage of SymPy SymmetricDifference. 24 | 25 | v1.1.1: 26 | ------- 27 | * Branched version for legacy SymPy 1.4 now that 1.5 is out. 28 | * Fixed Javascript bug where double spaces showed up as literal " " strings. 29 | * Fixed LaTeX display of undefined function keyword argument names with underscores. 30 | 31 | v1.1: 32 | ----- 33 | * Finally put sympad on PyPI. 34 | * Major grammar rework to remove redundant conflicts for speedup and to better handle tail elements (!, ', **, .attr, [idx], (x, y, ...)), \int, \sum and \lim among other things. 35 | * Added symbols with assumptions. 36 | * Added ability to 'call' derivatives of lambda functions. 37 | * Added parsing of 'not' as possible unary prefix to unparenthesized function arguments. 38 | * Added differentiation of parentheses grouping vs. call via curly enclosed parentheses. 39 | * Added mapping of undefined functions back to the variables they are assigned to. 40 | * Added partial differential equation support. 41 | * Added ability to select display of leading rational of products. 42 | * Added parsing of LaTeX subscripts in \operatorname. 43 | * Added ability to select strict or loose LaTeX formatting. 44 | * Made supported builtin functions accessible directly by name without need of escape character. 45 | * Made Eq returned from SymPy display as = when unambiguous in grammar. 46 | * Brought quick mode up to date with many recent changes. 47 | * Re-enabled unspaced quick mode function arguments. 48 | * Lots of tweaks to undefined functions for display and use in assignment to lambdas. 49 | * Removed recursive execution of script and relegated it to a command line switch for development. 50 | * Finally documented lots of changes. 51 | 52 | v1.0.19: 53 | -------- 54 | * Added parsing and writing of initial conditions of derivatives of undefined functions in function format (complicated). 55 | * Added proper formatting of exceptions which contain newlines in the exception text. 56 | * Added error hint text to web client. 57 | * Improved unparenthesized argument parsing for functions. 58 | * Improved error reporting from the parser. 59 | * Fixed "d / dx (a) b" and "\frac{d}{dx} (a) b" to work properly. 60 | 61 | v1.0.18: 62 | -------- 63 | * Added variable stack frames to variable mapping. 64 | * Added remapping of differentials in derivatives and integrals. 65 | * Changed lambdas to not automatically bind global variables if they have the same names as parameters, instead they can be accessed with @(). 66 | * Changed lambdas to not automatically doit() their body upon definition by default instead of needing the no-eval pseudo-function. 67 | * Improved assignment parsing to handle assignment to undefined functions better (creation of lambdas). 68 | * Moved SymPy .doit() from per-object level to end of whole evaluation since it can interfere with certain calculations. 69 | 70 | v1.0.17: 71 | -------- 72 | * Added explicit substitution shorthand and AST. 73 | * Added undefined function parsing and writing of immediate value arguments for initial conditions. 74 | * Changed to allow implicit undefined functions as exponents for grammatical consistency. 75 | * Changed some ordering for negatives to display simpler additive terms. 76 | * Fixed some annoying cases where fractions were displayed in the numerator of another fraction instead of being absorbed into it. 77 | * Fixed certain tokens which were not being gramatically rejected as they should have been with a trailing \w. 78 | 79 | v1.0.16: 80 | -------- 81 | * Added early out for successful parse if all remaining conflicts are trivial reductions. 82 | * Fixes, randomized testing no longer returning any errors. 83 | 84 | v1.0.15: 85 | -------- 86 | * Extended recognition of single unparenthesized arguments to functions - "N ln sin**2 3" is valid now. 87 | * Added union, intersection and symmetric difference unicode characters. 88 | * Added more thorough test cases to test_sym. 89 | * Changed And() parsing to more reliably recover original extended comparisons from mangled SymPy representation. 90 | * Tweaked lambda / slice / dict parsing and writing to be more comprehensive. 91 | * Fixed even more esoteric grammar inconsistencies. 92 | 93 | v1.0.14: 94 | -------- 95 | * Added translation from messy SymPy representation of symmetric set difference to internal simple one. 96 | * Changed test_sym to work better and test more cases. 97 | * Fixed many small grammatical inconsistencies. 98 | * Fixed matrix simplification error during integration for ODE solver. 99 | 100 | v1.0.13: 101 | -------- 102 | * Added differentiation now also parses and writes out forms dy/dx and df/dx. 103 | * Added parsing and representation of numerical subscripts. 104 | * Modified differential recognition in integral parsing to work better - \int dx/x, \int *lambda* dx, etc... 105 | * Fixed erroneous mapping of bound variables in lim, sum, diff and intg. 106 | 107 | v1.0.12: 108 | -------- 109 | * Added variables tab to browser client. 110 | * Added lambdas as expression in proper contexts (can add, multiply, differentiate, etc...). 111 | * Added S(num) is automatically converted to just num. 112 | * Changed lambda function parsing to be more robust and raised lambda keyword binding precedence. 113 | 114 | v1.0.11: 115 | ----- 116 | * Added definition of lambdas via assignment to undefined function - f(x) = x**2. 117 | * Modified undefined functions to be specifiable implicitly without '?' in most contexts. 118 | 119 | v1.0.10: 120 | ----- 121 | * Added undefined function initial conditions - ?y(x)(0) = something, ?y(x)'(0) = something 122 | * Added implicit undefined functions - y(x) -> ?y(x) 123 | 124 | v1.0.9: 125 | ------- 126 | * Added undefined functions (for ODEs). 127 | * Added prime ' differentiation operator. 128 | * Added semicolon separator to allow multiple expressions on single line. 129 | * Cleaned up internal negative object parsing and printing rules. 130 | * Fixed internal lambda variable representation so lambda defined variables doesn't get remapped. 131 | 132 | v1.0.8: 133 | ------- 134 | * Added extended comparisons: x < y < z >= w != ... 135 | * Added SymPy set operation Contains usage. 136 | * Added and expanded some function visual translations: summation(), .limit(), .diff(), .integrate() and more... 137 | * Added compacting of multiple Subs() and .subs(). 138 | * Changed lambdas to bind global variables on definition. 139 | * Cleaned up passing assignments as equations into functions. 140 | * Cleaned up internal handling of incomplete and empty matrices. 141 | * Fixed more obscure grammar cases. 142 | 143 | v1.0.7: 144 | ------- 145 | * Added Pythonic 'or', 'and' and 'not' operators using SymPy Or, And and Not. 146 | * Added slice() translation and lots of slice formatting fixes. 147 | * Changed comparisons to use SymPy comparison objects first and fall back to Python operators if that fails. 148 | * Patched in basic math operation on SymPy Booleans, x + (y > z) valid now. 149 | * Patched SymPy sets.Complement to sympify args. 150 | * Fixed lots of obscure corner case grammar things that should never happen anyway like dictionaries of slices. 151 | 152 | v1.0.6: 153 | ------- 154 | * Added walk over vector field plotting using matplotlib. 155 | 156 | v1.0.5: 157 | ------- 158 | * Added vector field plotting using matplotlib. 159 | 160 | v1.0.4: 161 | ------- 162 | * Added basic function plotting using matplotlib. 163 | 164 | v1.0.3: 165 | ------- 166 | * Extended post-evaluation simplification into basic Python containers. 167 | * dict_keys, dict_values and dict_items converted to lists. 168 | * Autocompletion of parentheses of member function calls and user lambdas. 169 | * Added Subs() and .subs() translation and latex parsing. 170 | 171 | v1.0.2: 172 | ------- 173 | * Changed function keyword argument recognition to be more friendly to equations. 174 | * Added automatic post-evaluation simplification (optional through env). 175 | * Added SymPy matrix multiplication intermediate simplification patch to control matrix expression blowup (optional through env). 176 | * Union of symmetric Complement sets converted back to symmetric difference after evaluation where applicable. 177 | 178 | v1.0.1: 179 | ------- 180 | * Changed native vector and matrix representation from {} to \[] to make grammatical room for sets and dictionaries. 181 | * Added basic sets and operations. 182 | * Added Python dictionaries. 183 | * Added LaTeX \Re and \Im functions and translation. 184 | * Integral now concatenates iterated integrals into one function call. 185 | -------------------------------------------------------------------------------- /make_parser_tables.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # python 3.6+ 3 | 4 | # Build parser tables for sparser.py using grammar dynamically generated from 5 | # class methods in that file. 6 | 7 | import sys 8 | 9 | try: 10 | import ply 11 | except ModuleNotFoundError: 12 | print ("This script requires the 'ply' python package to be installed.") 13 | sys.exit (-1) 14 | 15 | import getopt 16 | import importlib 17 | import importlib.util 18 | import os 19 | import re 20 | import types 21 | 22 | from lalr1 import LALR1 23 | 24 | #............................................................................................... 25 | def parse_rules (conflict_reduce_first = ()): # conflict_reduce_first = {'TOKEN', 'TOKEN', ...} 26 | def skipspace (): 27 | while lines and not lines [-1].strip (): 28 | lines.pop () 29 | 30 | if not lines: 31 | return True 32 | 33 | return False 34 | 35 | lines = list (reversed (open ('parser.out').readlines ())) 36 | rules = [] # [('production', ('symbol', ...)), ...] - index by rule num 37 | strules = [] # [[(rule, pos) or rule with pos = 0 implied, ...], ...] - index by state 38 | terms = {} # {'symbol': ([state, ...], [+shift or -reduce, ...], {state: conflict +shift or -reduce, ...} or not present if none), ...} 39 | nterms = {} # {'symbol': ([state, ...], [+shift or -reduce, ...]), ...} 40 | state = -1 41 | 42 | while lines [-1] [:5] != 'Rule ': 43 | lines.pop () 44 | 45 | while lines [-1] [:5] == 'Rule ': 46 | _, num, prod, _, subs = lines.pop ().split (maxsplit = 4) 47 | 48 | assert int (num) == len (rules) 49 | 50 | rules.append ((prod, tuple (subs.split ()) if subs != '\n' else ())) 51 | 52 | for _ in range (2): 53 | skipspace () 54 | lines.pop () 55 | skipspace () 56 | 57 | while lines [-1].strip (): 58 | t = lines.pop ().split () 59 | 60 | while lines [-1] [:6] != 'state ': 61 | lines.pop () 62 | 63 | while lines: 64 | if lines [-1] [:8] == 'WARNING:': 65 | break 66 | 67 | state += 1 68 | strules.append ([]) 69 | 70 | assert int (lines.pop ().split () [1]) == state 71 | 72 | skipspace () 73 | 74 | while lines [-1].strip (): # rules 75 | t = lines.pop ().split () 76 | rulei = int (t [0] [1 : -1]) 77 | pos = t.index ('.') - 3 78 | 79 | strules [-1].append ((rulei, pos) if pos else rulei) 80 | 81 | if skipspace (): 82 | break 83 | 84 | if lines [-1] [:6] == 'state ' or lines [-1] [:8] == 'WARNING:': 85 | continue 86 | 87 | while lines and lines [-1].strip (): # terminals 88 | t = lines.pop ().split () 89 | 90 | if t [0] == '!': 91 | continue 92 | 93 | sts, acts, _ = terms.setdefault (t [0], ([], [], {})) 94 | 95 | if t [1] == 'shift': 96 | sts.append (state) 97 | acts.append (int (t [-1])) 98 | else: # t [1] == 'reduce' 99 | sts.append (state) 100 | acts.append (-int (t [4])) 101 | 102 | if skipspace (): 103 | break 104 | 105 | if lines [-1].split () [0] == '!': 106 | while lines and lines [-1].strip (): 107 | t = lines.pop ().split () 108 | act = int (t [-2]) if t [3] == 'shift' else -int (t [6]) 109 | 110 | if t [1] in conflict_reduce_first: 111 | terms [t [1]] [2] [state] = terms [t [1]] [1] [-1] 112 | terms [t [1]] [1] [-1] = act 113 | else: 114 | terms [t [1]] [2] [state] = act 115 | 116 | if skipspace (): 117 | break 118 | 119 | if lines [-1] [:6] == 'state ' or lines [-1] [:8] == 'WARNING:': 120 | continue 121 | 122 | while lines and lines [-1].strip (): # non-terminals 123 | t = lines.pop ().split () 124 | 125 | assert t [1] == 'shift' 126 | 127 | sts, acts = nterms.setdefault (t [0], ([], [])) 128 | 129 | sts.append (state) 130 | acts.append (int (t [-1])) 131 | 132 | if skipspace (): 133 | break 134 | 135 | lterms = list (terms.keys ()) 136 | lnterms = list (nterms.keys ()) 137 | symbols = lterms + list (reversed (lnterms)) 138 | rules = [(-1 - lnterms.index (r [0]), tuple ((-1 - lnterms.index (s)) if s in nterms else lterms.index (s) \ 139 | for s in r [1])) for r in rules [1:]] 140 | rules = [r if len (r [1]) != 1 else (r [0], r [1] [0]) for r in rules] 141 | strules = [sr if len (sr) > 1 else sr [0] for sr in strules] 142 | terms = [(lterms.index (s),) + t if t [2] else (lterms.index (s),) + t [:2] for s, t in terms.items ()] 143 | nterms = [(-1 - lnterms.index (s),) + t for s, t in nterms.items ()] 144 | 145 | return symbols, rules, strules, terms, nterms 146 | 147 | #............................................................................................... 148 | def process (fnm, nodelete = False, compress = False, width = 512): 149 | parser_tables_rec = re.compile (r'^(\s*)_PARSER_TABLES\s*=') 150 | parser_tables_cont_rec = re.compile (r'\\\s*$') 151 | 152 | spec = importlib.util.spec_from_file_location (fnm, fnm + '.py') 153 | mod = importlib.util.module_from_spec (spec) 154 | 155 | spec.loader.exec_module (mod) 156 | 157 | # find and extract data from parser class 158 | for name, obj in mod.__dict__.items (): 159 | if isinstance (obj, object) and (hasattr (obj, '_PARSER_TABLES') and \ 160 | hasattr (obj, 'TOKENS') and hasattr (obj, '_PARSER_TOP')): 161 | 162 | pc_name = name 163 | pc_obj = obj 164 | pc_start = LALR1._rec_SYMBOL_NUMTAIL.match (obj._PARSER_TOP).group (1) 165 | pc_funcs = {} # {'prod': [('name', func, ('parm', ...)), ...], ...} - 'self' stripped from parms 166 | 167 | for name, obj in pc_obj.__dict__.items (): 168 | if name [0] != '_' and type (obj) is types.FunctionType and obj.__code__.co_argcount >= 1: # 2: allow empty productions 169 | name, num = LALR1._rec_SYMBOL_NUMTAIL.match (name).groups () 170 | num = 0 if num is None else int (num) 171 | 172 | pc_funcs.setdefault (name, []).append ((name, num, obj, obj.__code__.co_varnames [1 : obj.__code__.co_argcount])) 173 | 174 | if pc_start is None: 175 | pc_start = name 176 | 177 | if pc_start is None: 178 | raise RuntimeError ('start production not found') 179 | 180 | break 181 | 182 | else: 183 | raise RuntimeError ('parser class not found') 184 | 185 | # build tokens, rules and context for ply 186 | ply_dict = {'__file__': __file__, 'tokens': list (filter (lambda s: s != 'ignore', pc_obj.TOKENS.keys ())), \ 187 | 'start': pc_start, 'p_error': lambda p: None, 't_error': lambda t: None} 188 | prods = {} # {'prod': [('symbol', ...), ...], ...} 189 | stack = [pc_start] 190 | 191 | for tok, text in pc_obj.TOKENS.items (): 192 | if tok != 'ignore': 193 | ply_dict [f't_{tok}'] = text 194 | 195 | while stack: 196 | prod = stack.pop () 197 | 198 | if prod not in pc_funcs: 199 | raise NameError (f'production not found {prod!r}') 200 | 201 | rhss = prods.setdefault (prod, []) 202 | 203 | for _, _, _, parms in sorted (pc_funcs [prod]): # pc_funcs [prod]: 204 | parms = [p if p in pc_obj.TOKENS else LALR1._rec_SYMBOL_NUMTAIL.match (p).group (1) for p in parms] 205 | 206 | rhss.append (parms) 207 | stack.extend (filter (lambda p: p not in pc_obj.TOKENS and p not in prods and p not in stack, parms)) 208 | 209 | # add rule functions to context and build parse tables 210 | for prod, rhss in prods.items (): 211 | func = lambda p: None 212 | func.__doc__ = f'{prod} : ' + '\n| '.join (' '.join (rhs) for rhs in rhss) 213 | 214 | ply_dict [f'p_{prod}'] = func 215 | 216 | exec ('import ply.lex as lex', ply_dict) 217 | exec ('import ply.yacc as yacc', ply_dict) 218 | exec ('lex.lex ()', ply_dict) 219 | exec (f'yacc.yacc (outputdir = {os.getcwd ()!r})', ply_dict) 220 | 221 | qpdata = parse_rules (getattr (pc_obj, '_PARSER_CONFLICT_REDUCE', {})) 222 | text = repr (qpdata).replace (' ', '') 223 | 224 | if not nodelete: 225 | os.unlink ('parser.out') 226 | 227 | os.unlink ('parsetab.py') 228 | 229 | # write generated data into file 230 | class_rec = re.compile (fr'^\s*class\s+{pc_name}\s*[:(]') 231 | lines = open (f'{fnm}.py').readlines () 232 | 233 | for idx in range (len (lines)): 234 | if class_rec.match (lines [idx]): 235 | for idx in range (idx + 1, len (lines)): 236 | m = parser_tables_rec.match (lines [idx]) 237 | 238 | if m: 239 | for end in range (idx, len (lines)): 240 | if not parser_tables_cont_rec.search (lines [end]): 241 | break 242 | 243 | end += 1 244 | tab1 = m.group (1) 245 | tab2 = tab1 + '\t\t' 246 | lines_new = [f'{tab1}_PARSER_TABLES = \\\n'] 247 | 248 | if not compress: 249 | parser_tables_split_rec = re.compile (r'.{0,' + str (width) + r'}[,)}\]]') 250 | 251 | for line in parser_tables_split_rec.findall (text): 252 | lines_new.append (f'{tab2}{line} \\\n') 253 | 254 | else: 255 | import base64, zlib 256 | 257 | text = base64.b64encode (zlib.compress (text.encode ('utf8'))) 258 | 259 | for i in range (0, len (text), width): 260 | lines_new.append (f'{tab2}{text [i : i + width]!r} \\\n') 261 | 262 | lines_new [-1] = lines_new [-1] [:-2] + '\n' 263 | lines [idx : end] = lines_new 264 | 265 | open (f'{fnm}.py', 'w').writelines (lines) 266 | 267 | break 268 | 269 | break 270 | 271 | #............................................................................................... 272 | def cmdline (): 273 | nodelete = False 274 | width = 192 275 | compress = True 276 | opts, argv = getopt.getopt (sys.argv [1:], 'w:', ['nd', 'nodelete', 'nc', 'nocompress', 'width=']) 277 | 278 | for opt, arg in opts: 279 | if opt in ('--nd', '--nodelete'): 280 | nodelete = True 281 | elif opt in ('--nc', '--nocompress'): 282 | compress = False 283 | elif opt in ('-w', '--width'): 284 | width = int (arg) 285 | 286 | fnm = argv [0] if argv else 'sparser' 287 | 288 | process (fnm, nodelete = nodelete, compress = compress, width = width) 289 | 290 | if __name__ == '__main__': 291 | cmdline () 292 | -------------------------------------------------------------------------------- /lalr1.py: -------------------------------------------------------------------------------- 1 | # Parser for PLY generated LALR1 grammar. 2 | 3 | import re 4 | import types 5 | 6 | #............................................................................................... 7 | class Token (str): 8 | __slots__ = ['text', 'pos', 'grp'] 9 | 10 | def __new__ (cls, str_, text = None, pos = None, grps = None): 11 | self = str.__new__ (cls, str_) 12 | self.text = text or '' 13 | self.pos = pos 14 | self.grp = () if not grps else grps 15 | 16 | return self 17 | 18 | class State: 19 | __slots__ = ['idx', 'sym', 'pos', 'red'] 20 | 21 | def __init__ (self, idx, sym, pos = None, red = None): # idx = state index, sym = symbol (TOKEN or 'expression')[, pos = position in text, red = reduction] 22 | self.idx = idx 23 | self.sym = sym 24 | self.pos = sym.pos if pos is None else pos 25 | self.red = red 26 | 27 | def __repr__ (self): 28 | return f'({self.idx}, {self.sym}, {self.pos}{"" if self.red is None else f", {self.red}"})' 29 | 30 | class Conflict (tuple): 31 | def __new__ (cls, conf, pos, tokidx, stidx, tokens, stack, estate):#, keep = False): 32 | self = tuple.__new__ (cls, (conf, pos, tokidx, stidx, tokens, stack, estate)) 33 | self.conf = conf 34 | self.pos = pos 35 | # self.keep = keep # do not remove when popping conflicts 36 | 37 | return self 38 | 39 | # def __repr__ (self): 40 | # r = tuple.__repr__ (self) 41 | 42 | # return f'{r [:-1]}, keep)' if self.keep else r 43 | 44 | # class Incomplete (Exception): # parse is head of good statement but incomplete 45 | # __slots__ = ['red'] 46 | 47 | # def __init__ (self, red): 48 | # self.red = red 49 | 50 | # class KeepConf: 51 | # __slots__ = ['red'] 52 | 53 | # def __init__ (self, red): 54 | # self.red = red 55 | 56 | class PopConfs: 57 | __slots__ = ['red'] 58 | 59 | def __init__ (self, red): 60 | self.red = red 61 | 62 | class Reduce: # returned instantiated will try conflicted reduction before rule, returned as uninstantiated class will discard results of rule and just continue with last conflict 63 | __slots__ = ['then', 'keep'] 64 | 65 | def __init__ (self, then):#, keep = False): 66 | self.then = then 67 | # self.keep = keep 68 | 69 | Reduce.red = Reduce 70 | 71 | class LALR1: 72 | _rec_SYMBOL_NUMTAIL = re.compile (r'(.*[^_\d])_?(\d+)?') # symbol names in code have extra digits at end for uniqueness which are discarded 73 | 74 | _PARSER_CONFLICT_REDUCE = {} # set of tokens for which a reduction will always be tried before a shift 75 | 76 | def set_tokens (self, tokens): 77 | self.tokgrps = {} # {'token': (groups pos start, groups pos end), ...} 78 | tokpats = list (tokens.items ()) 79 | pos = 0 80 | 81 | for tok, pat in tokpats: 82 | l = re.compile (pat).groups + 1 83 | self.tokgrps [tok] = (pos, pos + l) 84 | pos += l 85 | 86 | self.tokre = '|'.join (f'(?P<{tok}>{pat})' for tok, pat in tokpats) 87 | self.tokrec = re.compile (self.tokre) 88 | 89 | def __init__ (self): 90 | if isinstance (self._PARSER_TABLES, bytes): 91 | import ast, base64, zlib 92 | symbols, rules, strules, terms, nterms = ast.literal_eval (zlib.decompress (base64.b64decode (self._PARSER_TABLES)).decode ('utf8')) 93 | else: 94 | symbols, rules, strules, terms, nterms = self._PARSER_TABLES 95 | 96 | self.set_tokens (self.TOKENS) 97 | 98 | self.rules = [(0, (symbols [-1]))] + [(symbols [r [0]], tuple (symbols [s] for s in (r [1] if isinstance (r [1], tuple) else (r [1],)))) for r in rules] 99 | self.strules = [[t if isinstance (t, tuple) else (t, 0) for t in (sr if isinstance (sr, list) else [sr])] for sr in strules] 100 | states = max (max (max (t [1]) for t in terms), max (max (t [1]) for t in nterms)) + 1 101 | self.terms = [{} for _ in range (states)] # [{'symbol': [+shift or -reduce, conflict +shift or -reduce or None], ...}] - index by state num then terminal 102 | self.nterms = [{} for _ in range (states)] # [{'symbol': +shift or -reduce, ...}] - index by state num then non-terminal 103 | self.rfuncs = [None] # first rule is always None 104 | 105 | for t in terms: 106 | sym, sts, acts, confs = t if len (t) == 4 else t + (None,) 107 | sym = symbols [sym] 108 | 109 | for st, act in zip (sts, acts): 110 | self.terms [st] [sym] = (act, None) 111 | 112 | if confs: 113 | for st, act in confs.items (): 114 | self.terms [st] [sym] = (self.terms [st] [sym] [0], act) 115 | 116 | for sym, sts, acts in nterms: 117 | for st, act in zip (sts, acts): 118 | self.nterms [st] [symbols [sym]] = act 119 | 120 | prods = {} # {('production', ('symbol', ...)): func, ...} 121 | 122 | for name in dir (self): 123 | obj = getattr (self, name) 124 | 125 | if name [0] != '_' and type (obj) is types.MethodType and obj.__code__.co_argcount >= 1: # 2: allow empty productions 126 | m = LALR1._rec_SYMBOL_NUMTAIL.match (name) 127 | 128 | if m: 129 | parms = tuple (p if p in self.TOKENS else LALR1._rec_SYMBOL_NUMTAIL.match (p).group (1) \ 130 | for p in obj.__code__.co_varnames [1 : obj.__code__.co_argcount]) 131 | prods [(m.group (1), parms)] = obj 132 | 133 | for irule in range (1, len (self.rules)): 134 | func = prods.get (self.rules [irule] [:2]) 135 | 136 | if not func: 137 | raise NameError (f"no method for rule '{self.rules [irule] [0]} -> {''' '''.join (self.rules [irule] [1])}'") 138 | 139 | self.rfuncs.append (func) 140 | 141 | def tokenize (self, text): 142 | tokens = [] 143 | end = len (text) 144 | pos = 0 145 | 146 | while pos < end: 147 | m = self.tokrec.match (text, pos) 148 | 149 | if m is None: 150 | tokens.append (Token ('$err', text [pos], pos)) 151 | 152 | break 153 | 154 | else: 155 | if m.lastgroup != 'ignore': 156 | tok = m.lastgroup 157 | s, e = self.tokgrps [tok] 158 | grps = m.groups () [s : e] 159 | 160 | tokens.append (Token (tok, grps [0], pos, grps [1:])) 161 | 162 | pos += len (m.group (0)) 163 | 164 | tokens.append (Token ('$end', '', pos)) 165 | 166 | return tokens 167 | 168 | #............................................................................................... 169 | def parse_getextrastate (self): 170 | return None 171 | 172 | def parse_setextrastate (self, state): 173 | pass 174 | 175 | def parse_error (self): 176 | return False # True if state fixed to continue parsing, False to fail 177 | 178 | def parse_success (self, red): 179 | 'NO PARSE_SUCCESS' 180 | return None # True to contunue checking conflict backtracks, False to stop and return 181 | 182 | def parse (self, src): 183 | has_parse_success = (self.parse_success.__doc__ != 'NO PARSE_SUCCESS') 184 | 185 | rules, terms, nterms, rfuncs = self.rules, self.terms, self.nterms, self.rfuncs 186 | 187 | tokens = self.tokenize (src) 188 | tokidx = 0 189 | confs = [] # [(action, tokidx, stack, stidx, extra state), ...] # conflict backtrack stack 190 | stack = self.stack = [State (0, None, 0, None)] # [(stidx, symbol, pos, reduction) or (stidx, token), ...] 191 | stidx = 0 192 | rederr = None # reduction function raised exception (SyntaxError or Incomplete usually) 193 | act = True 194 | pos = 0 195 | 196 | # if not hasattr (self, 'reds'): # DEBUG 197 | # self.reds = {} # DEBUG 198 | 199 | while 1: 200 | if not rederr and act is not None: 201 | tok = tokens [tokidx] 202 | act, conf = terms [stidx].get (tok, (None, None)) 203 | 204 | if rederr or act is None: 205 | if rederr is not Reduce: 206 | self.tokens, self.tokidx, self.confs, self.stidx, self.tok, self.rederr, self.pos = \ 207 | tokens, tokidx, confs, stidx, tok, rederr, pos 208 | 209 | rederr = None 210 | 211 | if tok == '$end' and stidx == 1 and len (stack) == 2 and stack [1].sym == rules [0] [1]: 212 | if not has_parse_success: 213 | return stack [1].red 214 | 215 | if not self.parse_success (stack [1].red) or not confs: 216 | return None 217 | 218 | elif self.parse_error (): 219 | tokidx, stidx = self.tokidx, self.stidx 220 | act = True 221 | rederr = None 222 | 223 | continue 224 | 225 | if not confs: 226 | if has_parse_success: # do not raise SyntaxError if parser relies on parse_success 227 | return None 228 | 229 | if rederr is not None: 230 | raise rederr # re-raise exception from last reduction function if present 231 | 232 | raise SyntaxError ( \ 233 | 'unexpected end of input' if tok == '$end' else \ 234 | f'invalid token {tok.text!r}' if tok == '$err' else \ 235 | f'invalid syntax {src [tok.pos : tok.pos + 16]!r}') 236 | 237 | act, _, tokidx, stidx, tokens, stack, estate = confs.pop () 238 | self.stack = stack 239 | tok = tokens [tokidx] 240 | conf = None 241 | rederr = None 242 | 243 | self.parse_setextrastate (estate) 244 | 245 | if act is None: 246 | continue 247 | 248 | if conf is not None: 249 | confs.append (Conflict (conf, tok.pos, tokidx, stidx, tokens [:], stack [:], self.parse_getextrastate ()))#, keep = act < 0 and tok in self._PARSER_CONFLICT_REDUCE)) 250 | 251 | # if conf < 0: # DEBUG 252 | # k = (act, rules [-conf]) 253 | # self.reds [k] = self.reds.get (k, 0) + 1 254 | 255 | if act > 0: 256 | tokidx += 1 257 | stidx = act 258 | 259 | stack.append (State (stidx, tok)) 260 | 261 | else: 262 | rule = rules [-act] 263 | rnlen = -len (rule [1]) 264 | prod = rule [0] 265 | pos = self.pos = stack [rnlen].pos 266 | 267 | try: 268 | red = rfuncs [-act] (*((t.sym if t.red is None else t.red for t in stack [rnlen:]) if rnlen else ())) 269 | 270 | except SyntaxError as e: 271 | rederr = e # or True 272 | 273 | continue 274 | 275 | # except Incomplete as e: 276 | # rederr = e 277 | # red = e.red 278 | 279 | else: 280 | # if isinstance (red, KeepConf): # mark this conflict to not be removed by PopConf 281 | # red = red.red 282 | # confs [-1].keep = True 283 | 284 | if isinstance (red, Reduce): # successful rule but request to follow conflicted reduction first putting results of rule on conf stack to be picked up later 285 | stidx = nterms [stack [rnlen - 1].idx] [prod] 286 | stack = stack [:rnlen] + [State (stidx, prod, pos, red.then)] 287 | tok = tokens [tokidx] 288 | act, conf = terms [stidx].get (tok, (None, None)) 289 | estate = self.parse_getextrastate () 290 | rederr = Reduce 291 | 292 | if conf is not None: 293 | confs.insert (-1, Conflict (conf, tok.pos, tokidx, stidx, tokens [:], stack [:], estate))#, keep = red.keep)) 294 | 295 | confs.insert (-1, Conflict (act, tok.pos, tokidx, stidx, tokens [:], stack [:], estate))#, keep = red.keep)) 296 | 297 | continue 298 | 299 | if red is Reduce or isinstance (red, PopConfs): # pop all conflicts generated from parsing this rule because parse is guaranteed good 300 | red = red.red 301 | start = stack [-1].pos if red is Reduce else pos 302 | i = 0 303 | 304 | for i in range (len (confs) - 1, -1, -1): 305 | if confs [i].pos <= start: 306 | break 307 | 308 | # if not confs [i].keep: # dont remove conflicts which are marked for keeping 309 | # del confs [i] 310 | del confs [i] 311 | 312 | if red is Reduce: # if reduction only requested then don't store rule result and fall back to previous conflicted reduction 313 | rederr = red 314 | 315 | continue 316 | 317 | if rnlen: 318 | del stack [rnlen:] 319 | 320 | stidx = nterms [stack [-1].idx] [prod] 321 | 322 | stack.append (State (stidx, prod, pos, red)) 323 | 324 | class lalr1: # for single script 325 | Token = Token 326 | State = State 327 | # Incomplete = Incomplete 328 | PopConfs = PopConfs 329 | Reduce = Reduce 330 | LALR1 = LALR1 331 | 332 | # print ('\n'.join (str (s) for s in confs + stack + [rule, pos])) -------------------------------------------------------------------------------- /spatch.py: -------------------------------------------------------------------------------- 1 | # Patch SymPy bugs and inconveniences. 2 | 3 | from collections import defaultdict 4 | 5 | #............................................................................................... 6 | def _Complement__new__ (cls, a, b, evaluate = True): # sets.Complement patched to sympify args 7 | if evaluate: 8 | return Complement.reduce (sympify (a), sympify (b)) 9 | 10 | return Basic.__new__ (cls, a, b) 11 | 12 | #............................................................................................... 13 | # matrix multiplication itermediate simplification routines 14 | 15 | def _dotprodsimp(expr, withsimp=False): 16 | def count_ops_alg(expr): 17 | ops = 0 18 | args = [expr] 19 | 20 | while args: 21 | a = args.pop() 22 | 23 | if not isinstance(a, Basic): 24 | continue 25 | 26 | if a.is_Rational: 27 | if a is not S.One: # -1/3 = NEG + DIV 28 | ops += bool (a.p < 0) + bool (a.q != 1) 29 | 30 | elif a.is_Mul: 31 | if _coeff_isneg(a): 32 | ops += 1 33 | if a.args[0] is S.NegativeOne: 34 | a = a.as_two_terms()[1] 35 | else: 36 | a = -a 37 | 38 | n, d = fraction(a) 39 | 40 | if n.is_Integer: 41 | ops += 1 + bool (n < 0) 42 | args.append(d) # won't be -Mul but could be Add 43 | 44 | elif d is not S.One: 45 | if not d.is_Integer: 46 | args.append(d) 47 | ops += 1 48 | args.append(n) # could be -Mul 49 | 50 | else: 51 | ops += len(a.args) - 1 52 | args.extend(a.args) 53 | 54 | elif a.is_Add: 55 | laargs = len(a.args) 56 | negs = 0 57 | 58 | for ai in a.args: 59 | if _coeff_isneg(ai): 60 | negs += 1 61 | ai = -ai 62 | args.append(ai) 63 | 64 | ops += laargs - (negs != laargs) # -x - y = NEG + SUB 65 | 66 | elif a.is_Pow: 67 | ops += 1 68 | args.append(a.base) 69 | 70 | return ops 71 | 72 | def nonalg_subs_dummies(expr, dummies): 73 | if not expr.args: 74 | return expr 75 | 76 | if expr.is_Add or expr.is_Mul or expr.is_Pow: 77 | args = None 78 | 79 | for i, a in enumerate(expr.args): 80 | c = nonalg_subs_dummies(a, dummies) 81 | 82 | if c is a: 83 | continue 84 | 85 | if args is None: 86 | args = list(expr.args) 87 | 88 | args[i] = c 89 | 90 | if args is None: 91 | return expr 92 | 93 | return expr.func(*args) 94 | 95 | return dummies.setdefault(expr, Dummy()) 96 | 97 | simplified = False # doesn't really mean simplified, rather "can simplify again" 98 | 99 | if isinstance(expr, Basic) and (expr.is_Add or expr.is_Mul or expr.is_Pow): 100 | expr2 = expr.expand(deep=True, modulus=None, power_base=False, 101 | power_exp=False, mul=True, log=False, multinomial=True, basic=False) 102 | 103 | if expr2 != expr: 104 | expr = expr2 105 | simplified = True 106 | 107 | exprops = count_ops_alg(expr) 108 | 109 | if exprops >= 6: # empirically tested cutoff for expensive simplification 110 | dummies = {} 111 | expr2 = nonalg_subs_dummies(expr, dummies) 112 | 113 | if expr2 is expr or count_ops_alg(expr2) >= 6: # check again after substitution 114 | expr3 = cancel(expr2) 115 | 116 | if expr3 != expr2: 117 | expr = expr3.subs([(d, e) for e, d in dummies.items()]) 118 | simplified = True 119 | 120 | # very special case: x/(x-1) - 1/(x-1) -> 1 121 | elif (exprops == 5 and expr.is_Add and expr.args [0].is_Mul and 122 | expr.args [1].is_Mul and expr.args [0].args [-1].is_Pow and 123 | expr.args [1].args [-1].is_Pow and 124 | expr.args [0].args [-1].exp is S.NegativeOne and 125 | expr.args [1].args [-1].exp is S.NegativeOne): 126 | 127 | expr2 = together (expr) 128 | expr2ops = count_ops_alg(expr2) 129 | 130 | if expr2ops < exprops: 131 | expr = expr2 132 | simplified = True 133 | 134 | else: 135 | simplified = True 136 | 137 | return (expr, simplified) if withsimp else expr 138 | 139 | def _MatrixArithmetic__mul__(self, other): 140 | other = _matrixify(other) 141 | # matrix-like objects can have shapes. This is 142 | # our first sanity check. 143 | if hasattr(other, 'shape') and len(other.shape) == 2: 144 | if self.shape[1] != other.shape[0]: 145 | raise ShapeError("Matrix size mismatch: %s * %s." % ( 146 | self.shape, other.shape)) 147 | 148 | # honest sympy matrices defer to their class's routine 149 | if getattr(other, 'is_Matrix', False): 150 | m = self._eval_matrix_mul(other) 151 | return m.applyfunc(_dotprodsimp) 152 | 153 | # Matrix-like objects can be passed to CommonMatrix routines directly. 154 | if getattr(other, 'is_MatrixLike', False): 155 | return MatrixArithmetic._eval_matrix_mul(self, other) 156 | 157 | # if 'other' is not iterable then scalar multiplication. 158 | if not isinstance(other, Iterable): 159 | try: 160 | return self._eval_scalar_mul(other) 161 | except TypeError: 162 | pass 163 | 164 | raise NotImplementedError() 165 | 166 | def _MatrixArithmetic_eval_pow_by_recursion(self, num, prevsimp=None): 167 | if prevsimp is None: 168 | prevsimp = [True]*len(self) 169 | 170 | if num == 1: 171 | return self 172 | 173 | if num % 2 == 1: 174 | a, b = self, self._eval_pow_by_recursion(num - 1, prevsimp=prevsimp) 175 | else: 176 | a = b = self._eval_pow_by_recursion(num // 2, prevsimp=prevsimp) 177 | 178 | m = a.multiply(b) 179 | lenm = len(m) 180 | elems = [None]*lenm 181 | 182 | for i in range(lenm): 183 | if prevsimp[i]: 184 | elems[i], prevsimp[i] = _dotprodsimp(m[i], withsimp=True) 185 | else: 186 | elems[i] = m[i] 187 | 188 | return m._new(m.rows, m.cols, elems) 189 | 190 | def _MatrixReductions_row_reduce(self, iszerofunc, simpfunc, normalize_last=True, 191 | normalize=True, zero_above=True): 192 | def get_col(i): 193 | return mat[i::cols] 194 | 195 | def row_swap(i, j): 196 | mat[i*cols:(i + 1)*cols], mat[j*cols:(j + 1)*cols] = \ 197 | mat[j*cols:(j + 1)*cols], mat[i*cols:(i + 1)*cols] 198 | 199 | def cross_cancel(a, i, b, j): 200 | """Does the row op row[i] = a*row[i] - b*row[j]""" 201 | q = (j - i)*cols 202 | for p in range(i*cols, (i + 1)*cols): 203 | mat[p] = _dotprodsimp(a*mat[p] - b*mat[p + q]) 204 | 205 | rows, cols = self.rows, self.cols 206 | mat = list(self) 207 | piv_row, piv_col = 0, 0 208 | pivot_cols = [] 209 | swaps = [] 210 | 211 | # use a fraction free method to zero above and below each pivot 212 | while piv_col < cols and piv_row < rows: 213 | pivot_offset, pivot_val, \ 214 | _, newly_determined = _find_reasonable_pivot( 215 | get_col(piv_col)[piv_row:], iszerofunc, simpfunc) 216 | 217 | # _find_reasonable_pivot may have simplified some things 218 | # in the process. Let's not let them go to waste 219 | for (offset, val) in newly_determined: 220 | offset += piv_row 221 | mat[offset*cols + piv_col] = val 222 | 223 | if pivot_offset is None: 224 | piv_col += 1 225 | continue 226 | 227 | pivot_cols.append(piv_col) 228 | if pivot_offset != 0: 229 | row_swap(piv_row, pivot_offset + piv_row) 230 | swaps.append((piv_row, pivot_offset + piv_row)) 231 | 232 | # if we aren't normalizing last, we normalize 233 | # before we zero the other rows 234 | if normalize_last is False: 235 | i, j = piv_row, piv_col 236 | mat[i*cols + j] = self.one 237 | for p in range(i*cols + j + 1, (i + 1)*cols): 238 | mat[p] = _dotprodsimp(mat[p] / pivot_val) 239 | # after normalizing, the pivot value is 1 240 | pivot_val = self.one 241 | 242 | # zero above and below the pivot 243 | for row in range(rows): 244 | # don't zero our current row 245 | if row == piv_row: 246 | continue 247 | # don't zero above the pivot unless we're told. 248 | if zero_above is False and row < piv_row: 249 | continue 250 | # if we're already a zero, don't do anything 251 | val = mat[row*cols + piv_col] 252 | if iszerofunc(val): 253 | continue 254 | 255 | cross_cancel(pivot_val, row, val, piv_row) 256 | piv_row += 1 257 | 258 | # normalize each row 259 | if normalize_last is True and normalize is True: 260 | for piv_i, piv_j in enumerate(pivot_cols): 261 | pivot_val = mat[piv_i*cols + piv_j] 262 | mat[piv_i*cols + piv_j] = self.one 263 | for p in range(piv_i*cols + piv_j + 1, (piv_i + 1)*cols): 264 | mat[p] = _dotprodsimp(mat[p] / pivot_val) 265 | 266 | return self._new(self.rows, self.cols, mat), tuple(pivot_cols), tuple(swaps) 267 | 268 | #............................................................................................... 269 | SPATCHED = False 270 | 271 | try: # try to patch and fail silently if sympy has changed too much since this was written 272 | from sympy import sympify, S, cancel, together, Basic, Complement, boolalg, Dummy 273 | from sympy.core.compatibility import Iterable 274 | from sympy.core.function import _coeff_isneg 275 | from sympy.matrices.common import MatrixArithmetic, ShapeError, _matrixify, classof 276 | from sympy.matrices.matrices import MatrixReductions, _find_reasonable_pivot 277 | from sympy.matrices.dense import DenseMatrix 278 | from sympy.matrices.sparse import SparseMatrix 279 | from sympy.simplify.radsimp import fraction 280 | 281 | Complement.__new__ = _Complement__new__ # sets.Complement sympify args fix 282 | 283 | # enable math on booleans 284 | boolalg.BooleanTrue.__int__ = lambda self: 1 285 | boolalg.BooleanTrue.__float__ = lambda self: 1.0 286 | boolalg.BooleanTrue.__complex__ = lambda self: 1+0j 287 | boolalg.BooleanTrue.as_coeff_Add = lambda self, *a, **kw: (S.Zero, S.One) 288 | boolalg.BooleanTrue.as_coeff_Mul = lambda self, *a, **kw: (S.One, S.One) 289 | boolalg.BooleanTrue._eval_evalf = lambda self, *a, **kw: S.One 290 | 291 | boolalg.BooleanFalse.__int__ = lambda self: 0 292 | boolalg.BooleanFalse.__float__ = lambda self: 0.0 293 | boolalg.BooleanFalse.__complex__ = lambda self: 0j 294 | boolalg.BooleanFalse.as_coeff_Mul = lambda self, *a, **kw: (S.Zero, S.Zero) 295 | boolalg.BooleanFalse.as_coeff_Add = lambda self, *a, **kw: (S.Zero, S.Zero) 296 | boolalg.BooleanFalse._eval_evalf = lambda self, *a, **kw: S.Zero 297 | 298 | boolalg.BooleanAtom.__add__ = lambda self, other: self.__int__ () + other 299 | boolalg.BooleanAtom.__radd__ = lambda self, other: other + self.__int__ () 300 | boolalg.BooleanAtom.__sub__ = lambda self, other: self.__int__ () - other 301 | boolalg.BooleanAtom.__rsub__ = lambda self, other: other - self.__int__ () 302 | boolalg.BooleanAtom.__mul__ = lambda self, other: self.__int__ () * other 303 | boolalg.BooleanAtom.__rmul__ = lambda self, other: other * self.__int__ () 304 | boolalg.BooleanAtom.__pow__ = lambda self, other: self.__int__ () ** other 305 | boolalg.BooleanAtom.__rpow__ = lambda self, other: other ** self.__int__ () 306 | boolalg.BooleanAtom.__div__ = lambda self, other: self.__int__ () / other 307 | boolalg.BooleanAtom.__rdiv__ = lambda self, other: other / self.__int__ () 308 | boolalg.BooleanAtom.__truediv__ = lambda self, other: self.__int__ () / other 309 | boolalg.BooleanAtom.__rtruediv__ = lambda self, other: other / self.__int__ () 310 | boolalg.BooleanAtom.__floordiv__ = lambda self, other: self.__int__ () // other 311 | boolalg.BooleanAtom.__rfloordiv__ = lambda self, other: other // self.__int__ () 312 | boolalg.BooleanAtom.__mod__ = lambda self, other: self.__int__ () % other 313 | boolalg.BooleanAtom.__rmod__ = lambda self, other: other % self.__int__ () 314 | 315 | # itermediate matrix multiplication simplification 316 | _SYMPY_MatrixArithmetic__mul__ = MatrixArithmetic.__mul__ 317 | _SYMPY_MatrixArithmetic_eval_pow_by_recursion = MatrixArithmetic._eval_pow_by_recursion 318 | _SYMPY_MatrixReductions_row_reduce = MatrixReductions._row_reduce 319 | MatrixArithmetic.__mul__ = _MatrixArithmetic__mul__ 320 | MatrixArithmetic._eval_pow_by_recursion = _MatrixArithmetic_eval_pow_by_recursion 321 | MatrixReductions._row_reduce = _MatrixReductions_row_reduce 322 | 323 | SPATCHED = True 324 | 325 | except: 326 | pass 327 | 328 | def set_matmulsimp (state): 329 | if SPATCHED: 330 | idx = bool (state) 331 | MatrixArithmetic.__mul__ = (_SYMPY_MatrixArithmetic__mul__, _MatrixArithmetic__mul__) [idx] 332 | MatrixArithmetic._eval_pow_by_recursion = (_SYMPY_MatrixArithmetic_eval_pow_by_recursion, _MatrixArithmetic_eval_pow_by_recursion) [idx] 333 | MatrixReductions._row_reduce = (_SYMPY_MatrixReductions_row_reduce, _MatrixReductions_row_reduce) [idx] 334 | 335 | class spatch: # for single script 336 | SPATCHED = SPATCHED 337 | set_matmulsimp = set_matmulsimp 338 | -------------------------------------------------------------------------------- /todo.txt: -------------------------------------------------------------------------------- 1 | > sin = ?(x) then sin(2)? 2 | 3 | > doc: hidden function change 4 | > doc: functions as variables and assignment change 5 | > doc: new == vs. = display behavior 6 | > doc: new integration behavior 7 | > doc: caveats with variable mapping and functions like integrate, summation due to their unfree vars being remapped 8 | 9 | 10 | > ERROR! assign to ufuncs from iterable? 11 | 12 | > \. ?F (x, 0)' |_{x = 0} ---> \. ?F (x, y) |_{x = 0, y = 0} 13 | > keep track of what undefined functions are created during processing? 14 | 15 | > somehow allow \. u (x, y) |_{u = v} when u and v are ufunc variables 16 | 17 | > subs autocomplete 18 | 19 | > subs_diff_any_ufunc and other ufunc implicit multiply with invalid call paren show as cdot 20 | 21 | > next (t) (2) at end of iterator t gives IndexError instead of StopIteration 22 | 23 | > "d / 2" is "(d) / 2", fix it 24 | > a / -{b \int x dx} * c -> py: a / (-(b*Integral(x, x))) * c 25 | 26 | > redo _expr_diff "a d/dx b"? - * Differentiation now has higher precedence than implicit multiplication ahead of it: "a d/dx b" -> "a * d/dx (b)", "\int a dx d / dx b" 27 | 28 | > test_server for semicolon stuff - multiple returns 29 | 30 | > display ufunc as \operatorname{func} in loose tex mode? 31 | > mark \operatorname funcs so they can't be rewound? doesn't seem to be necessary. 32 | 33 | 34 | > test_sym cases - call derivative of lambda 35 | 36 | > "d / dx (2)" vs. "d**2 / dx (2)" when dx is a lambda, intricate - would need to analyze parser stack to see if valid diff, but if someone is naming their lambdas "dx" that is their problem 37 | > better potential derivative test than self.stack_has_sym ('DIVIDE') 38 | 39 | > again redo spt2ast_Add? 40 | 41 | 42 | > \int doesn't need curlys for star multiply 43 | > subs clarity parentheses for tex display like d/dx (f), attr, etc...? 44 | > (a or b) or (a and b) - result parenthesizing for clarity 45 | > @ and % without parentheses consistency? 46 | > \sum and \lim parens for clarity 47 | > clean up safety parens from grammar tail element overhaul 48 | > clean up sym lambda keyword safety wrapping 49 | 50 | > test parse (ast2tex (spt2ast(spt))) vs. parse (latex (spt))? 51 | 52 | > "__" subscript shortcut with rules like "**"? 53 | 54 | > change AST._free_vars in sym to try to use ast2spt().free_symbols without .doit() and fall back to AST function if that fails? 55 | 56 | 57 | > allow funcs as vars for like symbols (..., cls = Functions)? 58 | 59 | > allow N**2 y? 60 | > N -N x -> N (-N (x)) 61 | 62 | > \sum, \lim accept only body of paren if paren follows directly 63 | > \sum, \lim and \intg to take expr_add for body 64 | 65 | > test_sym - test parsing sympy latex 66 | 67 | 68 | > logic xor operator 69 | 70 | > Boolean .could_extract_minus_sign() for calculations 71 | 72 | > relocate error hint text in web client to input area 73 | > improve error reporting with position of error returned from reduction functions if possible lalr1.Syntax (text, pos) 74 | > syntax error of diffp of multivar ufunc in sparser? 75 | > more informative syntax errors from ics functions 76 | 77 | > test_sym check for VarNulls and reject expression if detected 78 | > add slice() to test_sym 79 | > add back "forbidden" functions to test_sym (reject NullVars sxlats)? - remove or reject null vars after 80 | 81 | 82 | > patch dsolve to do: dsolve (y'' + 11 y' + 24 y, y (0) = 0, y'(0) = -7)? 83 | 84 | > CREATE SYMPY OBJECTS FOR PYTHON OBJECTS IN SYM?!? 85 | 86 | > double check all _ast2? (AST (...)) for parent issues 87 | 88 | > test_server multi maths from semicolon expressions 89 | 90 | 91 | > f (x) = x**2; f (x) 92 | 93 | > allow log_base without the \? 94 | > sum_{start <= x < stop} -> \sum_{x = start}^{stop - 1}? 95 | > do not write out parentheses for integration where not needed - \int {d / dx (x**2)} dx -> \int d / dx (x**2) dx? 96 | 97 | > verify all is_var -> is_var_nonconst where consts not allowed 98 | > worry about invalid varnames (reserved function names) or leave as convenience? 99 | > SymPy function aliases as vars? "D = Derivative"? 100 | 101 | > set full result of assignment to previous variable '_'? 102 | 103 | > make semicolon separated plots work 104 | 105 | > overhaul function call keyword arguments: expr -> ('kw', expr)? 106 | > overhaul lambda AST variable storage: ('@', 'var') -> 'var'? 107 | 108 | > allow leading underscore for vars and funcs and leading \_ for tex? 109 | > clean up sparser TOKENS and centralize repeated patterns 110 | 111 | > evil extra spacing before single-quote strings! 112 | > # comment end of line? 113 | 114 | > importing modules to allow custom code execution 115 | > module and function asts? 116 | 117 | > better plotting limits specification 118 | 119 | > test_python module 120 | 121 | > vector calculus 122 | > nabla - gradient, laplacian 123 | > Proper implementation of vectors with "\vec{x}" and "\hat{i}" variables? 124 | 125 | > random server port selection / fallback 126 | > ProductSet? 127 | > ImageSet? 128 | 129 | > formatting: no spacing for nums - 3 x -> 3x? 130 | > formatting: quick mode no spacing for vars x y z -> xyz? 131 | > formatting: non-quick mode explicit tex spacing for vars x y -> x\ y? 132 | 133 | > SYM.PY REFACTOR! 134 | 135 | > make sympad importable? 136 | 137 | > Custom Contains and other set operation fallbacks to allow symbols to stand in for sets? 138 | > 1 in x and other sets to accept symbols, how? 139 | > plots as objects? 140 | > flush sys.stdout to msg before sending exception? 141 | > xlat different types and initializations of matrices? 142 | > long variable name shortcut in quick mode? $? @? 143 | > rewrite sqrt during autocomplete to show radix? 144 | 145 | > floor / ceiling 146 | 147 | > break out state machine from server 148 | 149 | > server send back AST from validaiton to facilitate evaluation? 150 | 151 | > sequence (factorial (k), (k, 1, oo)) (ellipsis) 152 | > ImageSet (Lambda (n, 2 n pi + pi / 2), Integers) 153 | 154 | > more graphical plots 155 | > numpy support 156 | 157 | 158 | Slow parse - These are slow due to abuse of regex! 159 | -------------------------------------------------- 160 | 161 | 283.53125s: \[[ \arccot() or \[[ \lambda ,],[ w_{1} ,],[ 0 ,],] \cdot $zRF(real = True) \cdot d^{5} / dy^{3} dy^{2} \emptyset or x0 if 1e100 else None if 1.0 \cdot \Phi_{27} / \partialy \cdot False \cdot 1.0 \cdot 0.01466869603275454 ,\int \frac "s" eta / lambda_{4} ; .1 ; b ; 1. dCneFiF4u , oo || -4.383422266575117e-14 in \log_ \omega Pi in \int dz dh \cdot dx \cdot \psi_{84} \cdot True ' ,],[\[ \left| a \right|*\left| z20 \right| , w_{1} ! || a or 1 || Theta or 1. ,],\. \[ \partial x , 1. ,]* \emptyset ; dy ; xyzd ; \tilde\infty * None if 26320873337533.05 else 21134.155295076693 |_{\substack{ 1.2227127498338437e+19 ^^ "s" ^^ \partialy / \. \Xi |_{ c = y1 , eta = \Gamma_{51} , sigma = -.1 } = $QV(commutative = True) \\ None and \emptyset and partial^{4} / partialz^{2} partialx^{1} partialz^{1} -0.0008452535981829209 = difference_delta((())) and None && \xi && beta }},\sum_{x = {{{ 1e100 , c , 2 }}}** partial in 0 not in False not in \Lambda }^ -7.85992240533836e+20 [ None , b ]^ \infty zoo and \Psi_{93} { 0 , "s" }^\sum_{x = \partial x }^ oo dx ,],] 162 | 163.28125s: lambda: - xyzd if u(x, y, z) and ( : 62313.47619053531 : , dy ', 1.0 [ z20 , \partial x ]) && \left. 1e+100 or 1e+100 \right|_{\substack{ dx ! = \sqrt[ \phi_{29} ] -5.529339448748291e-09 }} and \sum_{x = \[ dx .bO0xI.z4rtZp4N() ,\int 0 dT ,\left. 0 \right|_{\substack{ None = partial \\ 1e+100 = 6.352354272671388e-16 }},]}^ \left| \partialy \right| and \partial || \partial x -1.0 && 1.0 && -1.5418373774276718e-05 && \tilde\infty \cdot \partial x \cdot 0 && \tilde\infty ; oo 163 | 28.453125s: {\.(:)|_{Sigma_{12}::'=-0>a,\left|\partial\right|&&chinotin-.1=lambdax,y,z:$qqq(real=True,commutative=True),Segment({})=\frac-88183020.84443538/-.1\s um_{x=0}^NONE\partialx}:\[\[[\emptyset,\emptyset,dz,],[\nu,\Lambda,dz,],]in\lim_{x\toz20}\partialyin-.1,]:\.4499484.467734945|_{\substack{1e+100=2067.693449705334\\y1=1e100}}\cdotxyzdif\partialelse-1.0ifcelse-5.542490885645353e-09if-1^\lim_{x\toz20^^1.}$B6uENQP()} 164 | 20.671875s: {{partial^{5}/partialx^{2}partialy^{3}xyzd!,\fracDelta'' 'dz**1e+100}}if\.0andband6.191305469846304e-09[\partialif0.0010151362451333347]|_{\substack{\gcd(({1,1.0},\int_-.1^-.1partialxdq))=\sqrtdxifFalse>y1else(dy,"s","s")if\partialx*c*-4.4653096888116064e-21elseNone==1e-100>=-2.0251011102347307e-18if\partial||2else1+1e+100+\partial}}elselambdax:\{x0,0,b,d/dy(u()(x,y))(0,1)}if\sqrt[d^{1}/dz^{1}{}]\{Falsein0,-1,1.,((b,-9.580303685500166e-07,phi))}else{True+x0:::Monomial((()))/\int\emptysetdP,notpartialx+\emptyset+2:::,(((1.))),\sigma[\partial]:dx=249.84211067914106and\left.\partialx\right|_{-2.3110629421441234e+20,1,\emptyset=c,c,False}and\sum_{x=1e100}^1.y1} 165 | 17.671875s: \int\{}dG*"s"'=dx**dxif\partial^\partialy&&\mu=\partialy.HZm7Ur.Xaiyk.OO1M()else\[[[[1.or\emptysetor\partial,nth_power_roots_poly((w_{1})),\sqrt[\partialy]oo]]],(not\partialy),\lim_{x\to\log_\partialy0}\[[-4.469111038573928e-16,],],]if{1,x0&&c[5.6422000714431704e+16,dx,"s"]&&RealField((-1,y1)):\sqrt[(7.668680606727133e-14::)]\left|oo\right|,d^{1}/dy^{1}iotaand1.0and.1:{$bd(commutative=True),1.0/\partial}}else\frac$mjVIF(commutative=True)\cdot3.2935527643376255e-06:-1sqrtlambda;b;2 166 | 14.71875s: {\lim_{x\to"s"'}\lim_{x\to\partialx}.1:\[[d^{4}/dy^{1}dy^{1}dz^{2}-1,notTrue,],[\fracdy\iota,\logdx,],[lambdax,y:.1,dx.SkjaV().xAaTvJE(),],]}^-\.g(x,y,z)|_{\.1e-100|_{\zeta,None,\Gamma_{47}=\emptyset,y1,\em ptyset}=\left.partialx\right|_{\substack{dx=tau\\\partialx=False}},\left|a\right|=\left|-9.408023550902053e+16\right|} 167 | 13.4375s: \left.-\emptyset**False/"s"'\right|_{\substack{\.lambdax,y,z:0*y1*dz|_{b^munotinlambda:y1notin\fracPix0notin[[False,-1.217383816320693e-12,a]]={{:w_{1}}:((a)),((oo,1e+100)):\sum_{x=False}^\pix0,{\partial:1.5165219116286896e-05}:\{w_{1},\xi,-.1}},\{((Complexes)),$nvQUW(real=True,commutative=True)}={},\frac\{y1,1310759352132.995}\left.\partialx\right|_{-1.0=2}=\partialx!andy1\cdot\partialxandnotw_{1}}={{{8.752331030243382e+16,oo}}}*:\left.1.0\right|_{True=False}>=sqrt_mod_iter(1e100)lnpartialx/Lambda_{63}oraor\xi}} 168 | 10.125s: {NONE:':1**3.480067056030083e-12\cdot36094604.0569791^2,\sum_{x=\sum_{x=\partialx}^dydz}^minpoly(((\emptyset)))\sqrt[-1.7454803239022647e-10]kappa:lambdax,y,z:b.teI5N().dH()},-1e+100^^\[\partialx,]^^$ouQVqO(real=True)+\.\left.\partialy\right|_{\tilde\infty=z20}|_{\substack{-2.335660686903354e-20;partial;2.9245005768789054e-13;1.8693689910922626e-21=\.z20|_{None=1e+100,1.458621 6274097632e-09=oo_{76},partial=0}\\w_{1}&&-3.357729778121166e-11&&x0=\upsilon,\partial\\-.1!=b+\partialy}} 169 | 5.859375s: \left.notb[-0.012450983090926753,partial,True]*\frac ()1e-100**\partialx*"s"'/partialx&&\iota\right|_{\int_partialx.xU5FHek()^1e+100>=dzd**3/dydxdx(h(x,y,real=True))dd&&\lim_{x\to1}1.0,?()(),\lim_{x\to-1.0}dz=-1.CvNb.IU().W5vX6rtI[\[[$tBZvdy(real=True,commutative=True),lambdax,y:\alpha,],[\partialx=2.745117662780435e-08,a&&w_{1},],[\lim_{x\topartial}xyzd,\tilde\infty,\partialx,],],\sum_{x=BlockDiagMatrix(1e100,c,\partialy)}^--11.0.dg.FcNr]=\{},{{1.0[1.,False,\inftyzoo]}}^[[[-7.141733656844623e-16,"s"]]]/\emptyset^^xyzd} 170 | 2.765625s: {lambda:{\tilde\infty:None,y1:\partialx}[partial^{2}/partialy^{2}\sqrt[\tilde\infty]\partial,Function('',)(x)(0),\sum_{x=0}^c1.*"s"'*\ln\partial] :\partialx\cdot1e100\cdot-.1/False,1.0if1.,-1,1e-100**1.0[-1],\lim_{x\to{{{oo}}}}\lim_{x\to0}\partialyand\left|\partialy\right|\cdot\tilde\infty^^"s"^^Lambda:d^{3}/dx^{1}dz^{2}\[\partialyifpartialxelse1e-100ifTrueelse-1,\partialyor1e-100,]} 171 | 172 | 173 | f = free variable for func name 174 | x = nonconst variable 175 | C = constant 176 | E = non-constant expression 177 | A = any of (x, C, E) not covered by rule yet 178 | 179 | f () = f () - _expr_varfunc () 180 | f (E) = f * (E) 181 | f (A) = f (A) 182 | ?f () = ?f () - _expr_ufunc () 183 | ?f (A) = ?f (A) 184 | ?f () (A) = ?f (A) - _expr_ufunc_ics () - from here on kw disallowed 185 | ?f (x) (A) = ?f (A) 186 | ?f (A) (A) = ?f (A) * (A) 187 | 188 | 189 | Quick Mode Errors: 190 | ------------------ 191 | 192 | 193 | text: \[[9.08830^1 e-100.LRPl().qtXX().ojbKXNA||-\int a dm20^1 e-100.LRPl().qtXX().ojbKXNA||-\int a dm9S,],] 194 | ast: ('-mat', ((('||', (('+', (('*', (('^', ('#', '9.08830'), ('#', '1')), ('@', 'e'))), ('-', ('*', (('#', '100.'), ('@', 'L'), ('@', 'R'), ('@', 'P'), ('.', ('.', ('-ufunc', 'l', ()), 'qtXX', ()), 'ojbKXNA')))))), ('+', (('*', (('-', ('^', ('-intg', ('@', 'a'), ('@', 'dm20')), ('#', '1'))), ('@', 'e'))), ('-', ('*', (('#', '100.'), ('@', 'L'), ('@', 'R'), ('@', 'P'), ('.', ('.', ('-ufunc', 'l', ()), 'qtXX', ()), 'ojbKXNA')))))), ('*', (('-', ('-intg', ('@', 'a'), ('@', 'dm9'))), ('@', 'S'))))),),)) 195 | ast: ('-mat', ((('||', (('+', (('*', (('^', ('#', '9.08830'), ('#', '1')), ('@', 'e'))), ('-', ('*', (('#', '100.'), ('@', 'L'), ('@', 'R'), ('@', 'P'), ('.', ('.', ('-ufunc', 'l', ()), 'qtXX', ()), 'ojbKXNA')), {1})))), ('+', (('*', (('-', ('^', ('(', ('-intg', ('@', 'a'), ('@', 'dm20'))), ('#', '1'))), ('@', 'e'))), ('-', ('*', (('#', '100.'), ('@', 'L'), ('@', 'R'), ('@', 'P'), ('.', ('.', ('-ufunc', 'l', ()), 'qtXX', ()), 'ojbKXNA')), {1})))), ('-', ('*', (('-intg', ('@', 'a'), ('@', 'dm9')), ('@', 'S')), {1})))),),)) 196 | tex1: \begin{bmatrix} 9.08830^1 e - 100. \cdot L\ R\ P\ l\left( \right).\operatorname{qtXX}\left( \right).ojbKXNA \cup -\left(\int a \ dm_{20} \right)^1 e - 100. \cdot L\ R\ P\ l\left( \right).\operatorname{qtXX}\left( \right).ojbKXNA \cup -\int a \ dm_{9} \cdot S \end{bmatrix} 197 | tex2: \begin{bmatrix} 9.08830^1 e - 100. \cdot L\ R\ P\ l\left( \right).\operatorname{qtXX}\left( \right).ojbKXNA \cup -\left(\int a \ dm_{20} \right)^1 e - 100. \cdot L\ R\ P\ l\left( \right).\operatorname{qtXX}\left( \right).ojbKXNA \cup -{\int a \ dm_{9} \cdot S} \end{bmatrix} 198 | 199 | 200 | text: h(r(r eal=True)True)() 201 | ast: ('*', (('@', 'h'), ('(', ('*', (('-ufunc', 'r', (), (('real', ('@', 'True')),)), ('@', 'True')))), ('(', (',', ())))) 202 | ast: ('*', (('@', 'h'), ('(', ('*', (('@', 'r'), ('(', ('=', ('*', (('-func', 're', (('@', 'a'),)), ('@', 'l'))), ('@', 'True'))), ('@', 'True')))), ('(', (',', ())))) 203 | tex1: {h}\left(r\left(real = True \right) True \right) \left( \right) 204 | tex2: {h}\left({r}\left(\Re{\left(a \right)} l = True \right) True \right) \left( \right) 205 | 206 | 207 | text: ?h(r eal=True,coh(r eal=True,commutative=True)(x,y,z)utative=True)(x,y,z)(0,1,2) 208 | ast: ('*', (('-ufunc', '?h', (('=', ('*', (('@', 'r'), ('@', 'e'), ('@', 'a'), ('@', 'l'))), ('@', 'True')), ('=', ('*', (('@', 'c'), ('@', 'o'), ('-ufunc', 'h', (), (('commutative', ('@', 'True')), ('real', ('@', 'True')))), ('(', (',', (('@', 'x'), ('@', 'y'), ('@', 'z')))), ('@', 'u'), ('@', 't'), ('@', 'a'), ('@', 't'), ('@', 'i'), ('@', 'v'), ('@', 'e'))), ('@', 'True')))), ('(', (',', (('@', 'x'), ('@', 'y'), ('@', 'z')))), ('(', (',', (('#', '0'), ('#', '1'), ('#', '2')))))) 209 | ast: ('*', (('-ufunc', '?h', (('=', ('*', (('@', 'r'), ('@', 'e'), ('@', 'a'), ('@', 'l'))), ('@', 'True')), ('=', ('*', (('@', 'c'), ('@', 'o'), ('@', 'h'), ('(', (',', (('=', ('*', (('@', 'c'), ('@', 'o'), ('@', 'm'), ('@', 'mu'), ('@', 't'), ('@', 'a'), ('@', 't'), ('@', 'i'), ('@', 'v'), ('@', 'e'))), ('@', 'True')), ('=', ('*', (('-func', 're', (('@', 'a'),)), ('@', 'l'))), ('@', 'True'))))), ('(', (',', (('@', 'x'), ('@', 'y'), ('@', 'z')))), ('@', 'u'), ('@', 't'), ('@', 'a'), ('@', 't'), ('@', 'i'), ('@', 'v'), ('@', 'e'))), ('@', 'True')))), ('(', (',', (('@', 'x'), ('@', 'y'), ('@', 'z')))), ('(', (',', (('#', '0'), ('#', '1'), ('#', '2')))))) 210 | tex1: {?h\left(r\ e\ a\ l = True, c\ o\ {h\left(commutative = True, real = True \right)}\left(x, y, z \right) u\ t\ a\ t\ i\ v\ e = True \right)}\left(x, y, z \right) \left(0, 1, 2 \right) 211 | tex2: {?h\left(r\ e\ a\ l = True, c\ o\ {h}\left(c\ o\ m\ \mu\ t\ a\ t\ i\ v\ e = True, \Re{\left(a \right)} l = True \right) \left(x, y, z \right) u\ t\ a\ t\ i\ v\ e = True \right)}\left(x, y, z \right) \left(0, 1, 2 \right) 212 | 213 | 214 | text: ?(x,y,r eal=e)(0,1) 215 | ast: ('*', (('-ufunc', '?', (('@', 'x'), ('@', 'y')), (('real', ('@', 'e')),)), ('(', (',', (('#', '0'), ('#', '1')))))) 216 | ast: ('*', (('-ufunc', '?', (('@', 'x'), ('@', 'y'), ('=', ('*', (('-func', 're', (('@', 'a'),)), ('@', 'l'))), ('@', 'e')))), ('(', (',', (('#', '0'), ('#', '1')))))) 217 | tex1: {?\left(x, y, real = e \right)}\left(0, 1 \right) 218 | tex2: {?\left(x, y, \Re{\left(a \right)} l = e \right)}\left(0, 1 \right) 219 | 220 | 221 | text: d**2/dzdydz?h(r eal=True,) 222 | ast: ('-diff', ('*', (('@', 'dz'), ('-ufunc', '?h', (), (('real', ('@', 'True')),)))), 'd', (('z', 1), ('y', 1))) 223 | ast: ('-diff', ('(', ('*', (('@', 'dz'), ('@', 'h'), ('(', ('=', ('*', (('-func', 're', (('@', 'a'),)), ('@', 'l'))), ('@', 'True')))))), 'partial', (('z', 1), ('y', 1))) 224 | tex1: \frac{\partial^2}{\partial z \partial y}\left(dz\ h\left(real = True \right) \right) 225 | tex2: \frac{\partial^2}{\partial z \partial y}\left(dz\ {h}\left(\Re{\left(a \right)} l = True \right) \right) 226 | 227 | 228 | text: \[notintt((\partialx)),not-1.31=\emptyset.lyTD0S1I.FoVz,\partial'''^^1=Vz,\partial'''^^1=oo,]! 229 | ast: ('!', ('-mat', ((('-not', ('-func', 'intt', (('(', ('@', 'partialx')),))),), (('=', ('-not', ('#', '-1.31')), ('.', ('.', ('-set', ()), 'lyTD0S1I'), 'FoVz')),), (('=', ('^^', (('-diffp', ('@', 'partial'), 3), ('#', '1'))), ('*', (('@', 'V'), ('@', 'z')))),), (('=', ('^^', (('-diffp', ('@', 'partial'), 3), ('#', '1'))), ('@', 'oo')),)))) 230 | nat1: \[not intt((partialx)), not -1.31 = \{}.lyTD0S1I.FoVz, partial''' ^^ 1 = V z, partial''' ^^ 1 = oo]! 231 | parse () 232 | 233 | 234 | Errors: 235 | ------- 236 | 237 | 238 | -------------------------------------------------------------------------------- /splot.py: -------------------------------------------------------------------------------- 1 | # Plot functions and expressions to image using matplotlib. 2 | 3 | import base64 4 | from io import BytesIO 5 | import itertools as it 6 | import math 7 | 8 | import sympy as sp 9 | 10 | _SPLOT = False 11 | 12 | try: 13 | import matplotlib 14 | import matplotlib.pyplot as plt 15 | 16 | matplotlib.style.use ('bmh') # ('seaborn') # ('classic') # ('fivethirtyeight') 17 | 18 | _SPLOT = True 19 | _FIGURE = None 20 | _TRANSPARENT = True 21 | 22 | except: 23 | pass 24 | 25 | #............................................................................................... 26 | def _cast_num (arg): 27 | try: 28 | return float (arg) 29 | except: 30 | return None 31 | 32 | def _process_head (obj, args, fs, style = None, ret_xrng = False, ret_yrng = False, kw = {}): 33 | global _FIGURE, _TRANSPARENT 34 | 35 | if style is not None: 36 | if style [:1] == '-': 37 | style, _TRANSPARENT = style [1:], True 38 | else: 39 | _TRANSPARENT = False 40 | 41 | matplotlib.style.use (style) 42 | 43 | args = list (reversed (args)) 44 | 45 | if args and args [-1] == '+': # continuing plot on previous figure? 46 | args.pop () 47 | 48 | elif _FIGURE: 49 | plt.close (_FIGURE) 50 | 51 | _FIGURE = None 52 | 53 | if not _FIGURE: 54 | _FIGURE = plt.figure () 55 | 56 | if fs is not None: # process figsize if present 57 | if isinstance (fs, (sp.Tuple, tuple)): 58 | fs = (_cast_num (fs [0]), _cast_num (fs [1])) 59 | 60 | else: 61 | fs = _cast_num (fs) 62 | 63 | if fs >= 0: 64 | fs = (fs, fs * 3 / 4) 65 | else: 66 | fs = (-fs, -fs) 67 | 68 | _FIGURE.set_figwidth (fs [0]) 69 | _FIGURE.set_figheight (fs [1]) 70 | 71 | xmax, ymin, ymax = None, None, None 72 | xmin = _cast_num (args [-1]) if args else None 73 | 74 | if xmin is not None: # process xmin / xmax, ymin, ymax if present 75 | args = args [:-1] 76 | xmax = _cast_num (args [-1]) if args else None 77 | 78 | if xmax is not None: 79 | args = args [:-1] 80 | ymin = _cast_num (args [-1]) if args else None 81 | 82 | if ymin is not None: 83 | args = args [:-1] 84 | ymax = _cast_num (args [-1]) if args else None 85 | 86 | if ymax is not None: 87 | args = args [:-1] 88 | else: 89 | xmin, xmax, ymin, ymax = -xmin, xmin, xmax, ymin 90 | else: 91 | xmin, xmax = -xmin, xmin 92 | 93 | if xmin is not None: 94 | obj.xlim (xmin, xmax) 95 | elif ret_xrng: 96 | xmin, xmax = obj.xlim () 97 | 98 | if ymin is not None: 99 | obj.ylim (ymin, ymax) 100 | elif ret_yrng: 101 | ymin, ymax = obj.ylim () 102 | 103 | kw = dict ((k, # cast certain sympy objects which don't play nice with matplotlib using numpy 104 | int (v) if isinstance (v, sp.Integer) else 105 | float (v) if isinstance (v, (sp.Float, sp.Rational)) else 106 | v) for k, v in kw.items ()) 107 | 108 | return args, xmin, xmax, ymin, ymax, kw 109 | 110 | def _process_fmt (args, kw = {}): 111 | kw = kw.copy () 112 | fargs = [] 113 | 114 | if args and isinstance (args [-1], str): 115 | fmt, lbl = (args.pop ().split ('=', 1) + [None]) [:2] 116 | fmt, clr = (fmt.split ('#', 1) + [None]) [:2] 117 | 118 | if lbl: 119 | kw ['label'] = lbl.strip () 120 | 121 | if clr: 122 | clr = clr.strip () 123 | 124 | if len (clr) == 6: 125 | try: 126 | _ = int (clr, 16) 127 | clr = f'#{clr}' 128 | except: 129 | pass 130 | 131 | kw ['color'] = clr 132 | 133 | fargs = [fmt.strip ()] 134 | 135 | if args and isinstance (args [-1], dict): 136 | kw.update (args.pop ()) 137 | 138 | return args, fargs, kw 139 | 140 | def _figure_to_image (): 141 | data = BytesIO () 142 | 143 | _FIGURE.savefig (data, format = 'png', bbox_inches = 'tight', facecolor = 'none', edgecolor = 'none', transparent = _TRANSPARENT) 144 | 145 | return base64.b64encode (data.getvalue ()).decode () 146 | 147 | #............................................................................................... 148 | def plotf (*args, fs = None, res = 12, style = None, **kw): 149 | """Plot function(s), point(s) and / or line(s). 150 | 151 | plotf ([+,] [limits,] *args, fs = None, res = 12, **kw) 152 | 153 | limits = set absolute axis bounds: (default x is (0, 1), y is automatic) 154 | x -> (-x, x, y auto) 155 | x0, x1 -> (x0, x1, y auto) 156 | x, y0, y1 -> (-x, x, y0, y1) 157 | x0, x1, y0, y1 -> (x0, x1, y0, y1) 158 | 159 | fs = set figure figsize if present: (default is (6.4, 4.8)) 160 | x -> (x, x * 3 / 4) 161 | -x -> (x, x) 162 | (x, y) -> (x, y) 163 | 164 | res = minimum target resolution points per 50 x pixels (more or less 1 figsize x unit), 165 | may be raised a little to align with grid 166 | style = optional matplotlib plot style 167 | 168 | *args = functions and their formatting: (func, ['fmt',] [{kw},] func, ['fmt',] [{kw},] ...) 169 | func -> callable function takes x and returns y 170 | (x, y) -> point at x, y 171 | (x0, y0, x1, y1, ...) -> connected lines from x0, y1 to x1, y1 to etc... 172 | ((x0, y0), (x1, y1), ...) -> same thing 173 | 174 | fmt = 'fmt[#color][=label]' 175 | """ 176 | 177 | if not _SPLOT: 178 | return None 179 | 180 | obj = plt 181 | legend = False 182 | 183 | args, xmin, xmax, ymin, ymax, kw = _process_head (obj, args, fs, style, ret_xrng = True, kw = kw) 184 | 185 | while args: 186 | arg = args.pop () 187 | 188 | if isinstance (arg, (sp.Tuple, tuple, list)): # list of x, y coords 189 | if isinstance (arg [0], (sp.Tuple, tuple, list)): 190 | arg = list (it.chain.from_iterable (arg)) 191 | 192 | pargs = [arg [0::2], arg [1::2]] 193 | 194 | else: # y = function (x) 195 | if not callable (arg): 196 | if len (arg.free_symbols) != 1: 197 | raise ValueError ('expression must have exactly one free variable') 198 | 199 | arg = sp.Lambda (arg.free_symbols.pop (), arg) 200 | 201 | win = _FIGURE.axes [-1].get_window_extent () 202 | xrs = (win.x1 - win.x0) // 50 # scale resolution to roughly 'res' points every 50 pixels 203 | rng = res * xrs 204 | dx = dx2 = xmax - xmin 205 | 206 | while dx2 < (res * xrs) / 2: # align sampling grid on integers and fractions of integers while rng stays small enough 207 | rng = int (rng + (dx2 - (rng % dx2)) % dx2) 208 | dx2 = dx2 * 2 209 | 210 | xs = [xmin + dx * i / rng for i in range (rng + 1)] 211 | ys = [None] * len (xs) 212 | 213 | for i in range (len (xs)): 214 | try: 215 | ys [i] = _cast_num (arg (xs [i])) 216 | except (ValueError, ZeroDivisionError, FloatingPointError): 217 | pass 218 | 219 | # remove lines crossing graph vertically due to poles (more or less) 220 | if ymin is not None: 221 | for i in range (1, len (xs)): 222 | if ys [i] is not None and ys [i-1] is not None: 223 | if ys [i] < ymin and ys [i-1] > ymax: 224 | ys [i] = None 225 | elif ys [i] > ymax and ys [i-1] < ymin: 226 | ys [i] = None 227 | 228 | pargs = [xs, ys] 229 | 230 | args, fargs, kwf = _process_fmt (args, kw) 231 | legend = legend or ('label' in kwf) 232 | 233 | obj.plot (*(pargs + fargs), **kwf) 234 | 235 | if legend or 'label' in kw: 236 | obj.legend () 237 | 238 | return _figure_to_image () 239 | 240 | #............................................................................................... 241 | def __fxfy2fxy (f1, f2): # u = f1 (x, y), v = f2 (x, y) -> (u, v) = f' (x, y) 242 | return lambda x, y, f1 = f1, f2 = f2: (float (f1 (x, y)), float (f2 (x, y))) 243 | 244 | def __fxy2fxy (f): # (u, v) = f (x, y) -> (u, v) = f' (x, y) 245 | return lambda x, y, f = f: tuple (float (v) for v in f (x, y)) 246 | 247 | def __fdy2fxy (f): # v/u = f (x, y) -> (u, v) = f' (x, y) 248 | return lambda x, y, f = f: tuple ((math.cos (t), math.sin (t)) for t in (math.atan2 (f (x, y), 1),)) [0] 249 | 250 | def _process_funcxy (args, testx, testy): 251 | isdy = False 252 | f = args.pop () 253 | 254 | if isinstance (f, (sp.Tuple, tuple, list)): # if (f1 (x, y), f2 (x, y)) functions or expressions present in args they are individual u and v functions 255 | c1, c2 = callable (f [0]), callable (f [1]) 256 | 257 | if c1 and c2: # two Lambdas 258 | f = __fxfy2fxy (f [0], f [1]) 259 | 260 | elif not (c1 or c2): # two expressions 261 | vars = tuple (sorted (sp.Tuple (f [0], f [1]).free_symbols, key = lambda s: s.name)) 262 | 263 | if len (vars) != 2: 264 | raise ValueError ('expression must have exactly two free variables') 265 | 266 | return args, __fxfy2fxy (sp.Lambda (vars, f [0]), sp.Lambda (vars, f [1])), False 267 | 268 | else: 269 | raise ValueError ('field must be specified by two lambdas or two expressions, not a mix') 270 | 271 | # one function or expression 272 | if not callable (f): # convert expression to function 273 | if len (f.free_symbols) != 2: 274 | raise ValueError ('expression must have exactly two free variables') 275 | 276 | f = sp.Lambda (tuple (sorted (f.free_symbols, key = lambda s: s.name)), f) 277 | 278 | for y in testy: # check if returns 1 dy or 2 u and v values 279 | for x in testx: 280 | try: 281 | v = f (x, y) 282 | except (ValueError, ZeroDivisionError, FloatingPointError): 283 | continue 284 | 285 | try: 286 | _, _ = v 287 | f = __fxy2fxy (f) 288 | 289 | break 290 | 291 | except: 292 | f = __fdy2fxy (f) 293 | isdy = True 294 | 295 | break 296 | 297 | else: 298 | continue 299 | 300 | break 301 | 302 | return args, f, isdy 303 | 304 | _plotv_clr_mag = lambda x, y, u, v: math.sqrt (u**2 + v**2) 305 | _plotv_clr_dir = lambda x, y, u, v: math.atan2 (v, u) 306 | 307 | _plotv_clr_func = {'mag': _plotv_clr_mag, 'dir': _plotv_clr_dir} 308 | 309 | #............................................................................................... 310 | def plotv (*args, fs = None, res = 13, style = None, resw = 1, kww = {}, **kw): 311 | """Plot vector field. 312 | 313 | plotv (['+',] [limits,] func(s), [color,] [fmt,] [*walks,] fs = None, res = 13, style = None, resw = 1, kww = {}, **kw) 314 | 315 | limits = set absolute axis bounds: (default x is (0, 1), y is automatic) 316 | x -> (-x, x, y auto) 317 | x0, x1 -> (x0, x1, y auto) 318 | x, y0, y1 -> (-x, x, y0, y1) 319 | x0, x1, y0, y1 -> (x0, x1, y0, y1) 320 | 321 | fs = set figure figsize if present: (default is (6.4, 4.8)) 322 | x -> (x, x / 6 * 4) 323 | -x -> (x, x) 324 | (x, y) -> (x, y) 325 | 326 | res = (w, h) number of arrows across x and y dimensions, if single digit then h will be w*3/4 327 | resw = resolution for optional plotw, see plotw for meaning 328 | kww = optional keyword arguments to be passed to plotw if that is being called 329 | style = optional matplotlib plot style 330 | 331 | func(s) = function or two functions or expressions returning either (u, v) or v/u 332 | f (x, y) -> returning (u, v) 333 | f (x, y) -> returning v/u will be interpreted without direction 334 | (f1 (x, y), f2 (x, y)) -> returning u and v respectively 335 | 336 | color = followed optionally by individual arrow color selection function (can not be expression) 337 | 'mag' -> color by magnitude of (u, v) vector 338 | 'dir' -> color by direction of (u, v) vector 339 | f (x, y, u, v) -> relative scalar, will be scaled according to whole field to select color 340 | 341 | fmt = followed optionally by color and label format string '[#color][=label]' 342 | 343 | *walks = followed optionally by arguments to plotw for individual x, y walks and formatting 344 | """ 345 | 346 | if not _SPLOT: 347 | return None 348 | 349 | obj = plt 350 | 351 | args, xmin, xmax, ymin, ymax, kw = _process_head (obj, args, fs, style, ret_xrng = True, ret_yrng = True, kw = kw) 352 | 353 | if not isinstance (res, (sp.Tuple, tuple, list)): 354 | win = _FIGURE.axes [-1].get_window_extent () 355 | res = (int (res), int ((win.y1 - win.y0) // ((win.x1 - win.x0) / (res + 1)))) 356 | else: 357 | res = (int (res [0]), int (res [1])) 358 | 359 | xs = (xmax - xmin) / (res [0] + 1) 360 | ys = (ymax - ymin) / (res [1] + 1) 361 | x0 = xmin + xs / 2 362 | y0 = ymin + ys / 2 363 | xd = (xmax - xs / 2) - x0 364 | yd = (ymax - ys / 2) - y0 365 | X = [[x0 + xd * i / (res [0] - 1)] * res [1] for i in range (res [0])] 366 | Y = [y0 + yd * i / (res [1] - 1) for i in range (res [1])] 367 | Y = [Y [:] for _ in range (res [0])] 368 | U = [[0] * res [1] for _ in range (res [0])] 369 | V = [[0] * res [1] for _ in range (res [0])] 370 | 371 | args, f, isdy = _process_funcxy (args, [x [0] for x in X], Y [0]) 372 | 373 | if isdy: 374 | d, kw = kw, {'headwidth': 0, 'headlength': 0, 'headaxislength': 0, 'pivot': 'middle'} 375 | kw.update (d) 376 | 377 | # populate U and Vs from X, Y grid 378 | for j in range (res [1]): 379 | for i in range (res [0]): 380 | try: 381 | U [i] [j], V [i] [j] = f (X [i] [j], Y [i] [j]) 382 | except (ValueError, ZeroDivisionError, FloatingPointError): 383 | U [i] [j] = V [i] [j] = 0 384 | 385 | clrf = None 386 | 387 | if args: 388 | if callable (args [-1]): # color function present? f (x, y, u, v) 389 | clrf = args.pop () 390 | 391 | elif isinstance (args [-1], str): # pre-defined color function string? 392 | clrf = _plotv_clr_func.get (args [-1]) 393 | 394 | if clrf: 395 | args.pop () 396 | 397 | args, _, kw = _process_fmt (args, kw) 398 | 399 | if clrf: 400 | C = [[float (clrf (X [i] [j], Y [i] [j], U [i] [j], V [i] [j])) for j in range (res [1])] for i in range (res [0])] 401 | 402 | obj.quiver (X, Y, U, V, C, **kw) 403 | 404 | else: 405 | obj.quiver (X, Y, U, V, **kw) 406 | 407 | if 'label' in kw: 408 | obj.legend () 409 | 410 | if args: # if arguments remain, pass them on to plotw to draw differential curves 411 | plotw (resw = resw, from_plotv = (args, xmin, xmax, ymin, ymax, f), **kww) 412 | 413 | return _figure_to_image () 414 | 415 | #............................................................................................... 416 | def plotw (*args, fs = None, resw = 1, style = None, from_plotv = False, **kw): 417 | """Plot walk(s) over vector field. 418 | 419 | plotw (['+',] [limits,] func(s), *args, fs = None, resw = 1, style = None, **kw) 420 | 421 | limits = set absolute axis bounds: (default x is (0, 1), y is automatic) 422 | x -> (-x, x, y auto) 423 | x0, x1 -> (x0, x1, y auto) 424 | x, y0, y1 -> (-x, x, y0, y1) 425 | x0, x1, y0, y1 -> (x0, x1, y0, y1) 426 | 427 | fs = set figure figsize if present: (default is (6.4, 4.8)) 428 | x -> (x, x / 6 * 4) 429 | -x -> (x, x) 430 | (x, y) -> (x, y) 431 | 432 | resw = maximum pixel steps to allow walk step to deviate before drawing, smaller = better quality 433 | style = optional matplotlib plot style 434 | 435 | func(s) = function or two functions returning either (u, v) or v/u 436 | f (x, y) -> returning (u, v) 437 | f (x, y) -> returning v/u will be interpreted without direction 438 | f (x, y), f2 (x, y) -> returning u and v respectively 439 | 440 | *args = followed by initial x, y points for walks (x, y, ['fmt',] [{kw},] x, y, ['fmt',] [{kw},] ...) 441 | fmt = 'fmt[#color][=label]' 442 | 443 | HACK: Python complex type used as 2D vector. 444 | """ 445 | 446 | def dot (p0, p1): # dot product of two 2d vectors stored as complexes 447 | return p0.real * p1.real + p0.imag * p1.imag 448 | 449 | def walk (x, y, f, o = 1): # returns [(x, y), (x, y), ...], True if looped else False 450 | def delta (p, d = None): 451 | try: 452 | t = math.atan2 (*(f (p.real, p.imag) [::-1])) 453 | 454 | return complex (math.cos (t), math.sin (t)) 455 | 456 | except (ValueError, ZeroDivisionError, FloatingPointError): 457 | if d is not None: 458 | return d 459 | 460 | raise FloatingPointError 461 | 462 | xys = [(x, y)] 463 | err = 0 464 | p0 = complex (x, y) 465 | p = p0 466 | # d = pxs 467 | d = delta (p, pxs) 468 | 469 | while 1: 470 | # d = delta (p, d) 471 | s = 0 472 | o2 = o 473 | p2 = p 474 | d2 = d 475 | 476 | while 1: 477 | st = 0.25 * pxm 478 | d3 = o2 * d2 479 | 480 | while 1: 481 | p3 = p2 + d3 * st # * pxm 482 | 483 | try: 484 | d4 = delta (p3) 485 | dc = math.acos (dot (d2, d4)) 486 | 487 | if dc > 2.748893571891069: # (7 * pi / 8), abrupt reverse of direction? 488 | o2 = -o2 489 | 490 | elif dc > 0.005: 491 | st = st * (0.004 / dc) 492 | continue 493 | 494 | err = err + dc * st # * pxm 495 | d2 = d4 496 | 497 | break 498 | 499 | except FloatingPointError: 500 | break 501 | 502 | s = s + st 503 | isloop = (dot (d3, p0 - p2) > 0) and abs (p3 - p0) < (2 * err) # (8 * pxm) 504 | p2 = p3 505 | 506 | if isloop or p2.real < xmin or p2.real > xmax or p2.imag < ymin or p2.imag > ymax: 507 | xys.extend ([(p2.real, p2.imag)] + [(x, y)] * bool (isloop)) 508 | return xys, isloop 509 | 510 | if abs (p2 - (p + o * d * s)) >= resw: # * pxm)) >= resw: 511 | xys.append ((p2.real, p2.imag)) 512 | 513 | o = o2 514 | p = p2 515 | d = d2 516 | 517 | break 518 | 519 | if not _SPLOT: 520 | return None 521 | 522 | obj = plt 523 | 524 | if from_plotv: 525 | args, xmin, xmax, ymin, ymax, f = from_plotv 526 | else: 527 | args, xmin, xmax, ymin, ymax, kw = _process_head (obj, args, fs, style, ret_xrng = True, ret_yrng = True, kw = kw) 528 | args, f, _ = _process_funcxy (args, [xmin + (xmax - xmin) * i / 4 for i in range (5)], [ymin + (ymax - ymin) * i / 4 for i in range (5)]) 529 | 530 | win = _FIGURE.axes [-1].get_window_extent () 531 | pxs = complex ((xmax - xmin) / (win.x1 - win.x0), (ymax - ymin) / (win.y1 - win.y0)) # pixel scale from xmin/max ymin/max scale 532 | pxm = abs (pxs) 533 | resw = resw * pxm 534 | 535 | leg = False 536 | 537 | while args: 538 | x, y = args.pop () 539 | xys, isloop = walk (x, y, f) 540 | 541 | if not isloop: 542 | xys = xys [::-1] [:-1] + walk (x, y, f, -1) [0] 543 | 544 | args, fargs, kwf = _process_fmt (args, kw) 545 | leg = leg or ('label' in kwf) 546 | 547 | obj.plot (*([[xy [0] for xy in xys], [xy [1] for xy in xys]] + fargs), **kwf) 548 | 549 | if leg or 'label' in kw: 550 | obj.legend () 551 | 552 | return _figure_to_image () 553 | 554 | #............................................................................................... 555 | class splot: # for single script 556 | plotf = plotf 557 | plotv = plotv 558 | plotw = plotw 559 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | // TODO: Change how left/right arrows interact with autocomplete. 2 | // TODO: Stupid scrollbars... 3 | // TODO: Change input to text field for longer expression support? 4 | // TODO: Arrow keys in Edge? 5 | // TODO: clear() function to delete old log items? 6 | 7 | URL = '/'; 8 | WaitIcon = '/wait.webp'; // 'https://i.gifer.com/origin/3f/3face8da2a6c3dcd27cb4a1aaa32c926_w200.webp'; 9 | 10 | JQInput = null; 11 | MJQueue = null; 12 | MarginTop = Infinity; 13 | PreventFocusOut = true; 14 | 15 | LogIdx = 0; 16 | UniqueID = 1; 17 | 18 | LastValidation = null; 19 | Validations = [undefined]; 20 | Evaluations = [undefined]; 21 | ErrorIdx = null; 22 | Autocomplete = []; 23 | 24 | LastClickTime = 0; 25 | NumClicks = 0; 26 | 27 | GreetingFadedOut = false; 28 | ExceptionDone = false; 29 | SymPyDevVersion = '1.7.1' 30 | 31 | // replaced in env.js 32 | History = []; 33 | HistIdx = 0; 34 | Version = 'None' 35 | SymPyVersion = 'None' 36 | DisplayStyle = 1 37 | 38 | //............................................................................................... 39 | function copyInputStyle () { 40 | let left = $('#LogEntry1').position ().left; 41 | 42 | JQInput.css ({left: left}); 43 | JQInput.width (window.innerWidth - left - 32); 44 | $('#InputCoverLeft').width (left); 45 | $('#InputCoverRight').css ({left: window.innerWidth - 30}); 46 | 47 | let style = getComputedStyle (document.getElementById ('Input')); 48 | let overlay = document.getElementById ('InputOverlay'); 49 | 50 | for (let prop of style) { 51 | overlay.style [prop] = style [prop]; 52 | } 53 | 54 | overlay.style ['z-index'] = 4; 55 | overlay.style ['pointer-events'] = 'none'; 56 | } 57 | 58 | function scrollToEnd () { 59 | window.scrollTo (0, document.body.scrollHeight); 60 | } 61 | 62 | function resize () { 63 | copyInputStyle (); 64 | scrollToEnd (); 65 | } 66 | 67 | var LastDocHeight = undefined; 68 | var LastWinHeight = undefined; 69 | 70 | function monitorStuff () { 71 | let curDocHeight = $(document).height (); 72 | let curWinHeight = $(window).height (); 73 | 74 | if (curDocHeight != LastDocHeight || curWinHeight != LastWinHeight) { 75 | copyInputStyle (); 76 | 77 | window.LastDocHeight = curDocHeight; 78 | window.LastWinHeight = curWinHeight; 79 | } 80 | 81 | if (PreventFocusOut) { 82 | JQInput.focus (); 83 | } 84 | 85 | updateOverlayPosition (); 86 | setTimeout (monitorStuff, 50); 87 | } 88 | 89 | function readyMathJax () { 90 | window.MJQueue = MathJax.Hub.queue; 91 | 92 | if (DisplayStyle) { 93 | var TEX = MathJax.InputJax.TeX; 94 | var PREFILTER = TEX.prefilterMath; 95 | 96 | TEX.Augment ({ 97 | prefilterMath: function (tex, displaymode, script) { 98 | return PREFILTER.call (TEX, '\\displaystyle{' + tex + '}', displaymode, script); 99 | } 100 | }); 101 | } 102 | } 103 | 104 | function reprioritizeMJQueue () { 105 | let p = MJQueue.queue.pop (); 106 | 107 | if (p !== undefined) { 108 | MJQueue.queue.splice (0, 0, p); 109 | } 110 | } 111 | 112 | function escapeHTML (text) { 113 | const entityMap = { 114 | '&': '&', 115 | '<': '<', 116 | '>': '>', 117 | '"': '"', 118 | "'": ''', 119 | '/': '/', 120 | '`': '`', 121 | '=': '=' 122 | }; 123 | 124 | return text.replace (/[&<>"'`=\/]/g, function (s) { 125 | return entityMap [s]; 126 | }); 127 | } 128 | 129 | function escapeHTMLtex (text) { 130 | return text.replace (/\\text{(['"]).*?\1}/g, escapeHTML); 131 | } 132 | 133 | //............................................................................................... 134 | function logResize () { 135 | let margin = Math.max (BodyMarginTop, Math.floor (window.innerHeight - $('body').height () - BodyMarginBottom + 3)); // +3 is fudge factor 136 | 137 | if (margin < MarginTop) { 138 | MarginTop = margin; 139 | $('body').css ({'margin-top': margin}); 140 | } 141 | } 142 | 143 | function addLogEntry () { 144 | LogIdx += 1; 145 | 146 | $('#Log').append (` 147 |
${LogIdx}.
148 | 149 |
`) 150 | 151 | Validations.push (undefined); 152 | Evaluations.push (undefined); 153 | } 154 | 155 | function updateNumClicks () { 156 | let t = performance.now (); 157 | 158 | if ((t - LastClickTime) > 500) { 159 | NumClicks = 1; 160 | } else { 161 | NumClicks += 1; 162 | } 163 | 164 | LastClickTime = t; 165 | } 166 | 167 | function flashElement (e) { 168 | e.style.color = 'white'; 169 | e.style.background = 'black'; 170 | 171 | setTimeout (function () { 172 | e.style.color = 'black'; 173 | e.style.background = 'transparent'; 174 | }, 100); 175 | } 176 | 177 | function writeToClipboard (text) { 178 | PreventFocusOut = false; 179 | 180 | $('#Clipboard').val (text); 181 | $('#Clipboard').focus (); 182 | $('#Clipboard').select (); 183 | document.execCommand ('copy'); 184 | 185 | PreventFocusOut = true; 186 | 187 | if (JQInput !== null) { 188 | JQInput.focus (); 189 | } 190 | } 191 | 192 | function cE2C (e) { 193 | writeToClipboard (e.textContent); 194 | flashElement (e); 195 | } 196 | 197 | function copyLogToClipboard (e, val_or_eval, idx, subidx = 0, mathidx = 0) { 198 | let resp = val_or_eval ? Evaluations [idx].data [subidx].math [mathidx] : Validations [idx]; 199 | 200 | updateNumClicks (); 201 | writeToClipboard (NumClicks == 1 ? resp.nat : NumClicks == 2 ? resp.py : resp.tex); 202 | flashElement (e); 203 | } 204 | 205 | function copyVarToClipboard (e, full = true) { 206 | updateNumClicks (); 207 | 208 | e = e.parentElement; 209 | let text = Variables.vars.get (e.name); 210 | text = NumClicks == 1 ? text.nat : NumClicks == 2 ? text.py : text.tex; 211 | 212 | if (!full && (NumClicks == 2) && text [1].startsWith ('Lambda(')) { // special case process py Lambda body 213 | if (text [1] [7] === '(') { 214 | text = [text [0], text [1].slice (text [1].indexOf (')') + 2, -1).trim ()]; 215 | } else { 216 | text = [text [0], text [1].slice (text [1].indexOf (',') + 1, -1).trim ()]; 217 | } 218 | } 219 | 220 | writeToClipboard (full ? text.join (' = ') : text [1]); 221 | flashElement (full ? e : e.childNodes [2]); 222 | } 223 | 224 | function updateOverlayPosition () { 225 | let left = -JQInput.scrollLeft (); 226 | let goodwidth = $('#OverlayGood').width (); 227 | let errorwidth = $('#OverlayError').width (); 228 | 229 | $('#OverlayGood').css ({left: left}) 230 | $('#OverlayError').css ({top: 0, left: left + goodwidth}); 231 | $('#OverlayAutocomplete').css ({top: 0, left: left + goodwidth + errorwidth}); 232 | } 233 | 234 | function updateOverlay (text, erridx, autocomplete) { 235 | ErrorIdx = erridx; 236 | Autocomplete = autocomplete; 237 | 238 | if (ErrorIdx === null) { 239 | $('#OverlayGood').text (text); 240 | $('#OverlayError').text (''); 241 | 242 | } else { 243 | $('#OverlayGood').text (text.substr (0, ErrorIdx)); 244 | $('#OverlayError').text (text.substr (ErrorIdx)); 245 | } 246 | 247 | $('#OverlayAutocomplete').text (Autocomplete.join ('')); 248 | 249 | updateOverlayPosition (); 250 | } 251 | 252 | //............................................................................................... 253 | function ajaxValidate (resp) { 254 | if (Validations [resp.idx] !== undefined && Validations [resp.idx].subidx >= resp.subidx) { 255 | return; // ignore out of order responses (which should never happen with single threaded server) 256 | } 257 | 258 | LastValidation = resp; 259 | 260 | if (resp.tex !== null) { 261 | Validations [resp.idx] = resp; 262 | 263 | let eLogInput = document.getElementById ('LogInput' + resp.idx); 264 | 265 | let queue = []; 266 | [queue, MJQueue.queue] = [MJQueue.queue, queue]; 267 | 268 | MJQueue.queue = queue.filter (function (obj, idx, arr) { // remove previous pending updates to same element 269 | return obj.data [0].parentElement !== eLogInput; 270 | }); 271 | 272 | let eLogInputWait = document.getElementById ('LogInputWait' + resp.idx); 273 | let math = resp.tex ? `$${resp.tex}$` : ''; 274 | eLogInputWait.style.visibility = ''; 275 | 276 | $(eLogInput).append (``); 277 | 278 | let eMath = eLogInput.lastElementChild; 279 | 280 | MJQueue.Push (['Typeset', MathJax.Hub, eMath, function () { 281 | if (eMath === eLogInput.children [eLogInput.children.length - 1]) { 282 | eLogInput.appendChild (eLogInputWait); 283 | 284 | for (let i = eLogInput.children.length - 3; i >= 0; i --) { 285 | eLogInput.removeChild (eLogInput.children [i]); 286 | } 287 | 288 | eLogInputWait.style.visibility = 'hidden'; 289 | eMath.style.visibility = ''; 290 | 291 | logResize (); 292 | scrollToEnd (); // ??? 293 | } 294 | }]); 295 | 296 | reprioritizeMJQueue (); 297 | } 298 | 299 | updateOverlay (JQInput.val (), resp.erridx, resp.autocomplete); 300 | } 301 | 302 | function ajaxEvaluate (resp) { 303 | Variables.update (resp.vars); 304 | 305 | Evaluations [resp.idx] = resp; 306 | let eLogEval = document.getElementById ('LogEval' + resp.idx); 307 | 308 | eLogEval.removeChild (document.getElementById ('LogEvalWait' + resp.idx)); 309 | 310 | for (let subidx in resp.data) { 311 | subresp = resp.data [subidx]; 312 | 313 | if (subresp.msg !== undefined && subresp.msg.length) { // message present? 314 | for (let msg of subresp.msg) { 315 | $(eLogEval).append (`
${escapeHTML (msg).replace (/ /g, ' ')}
`); 316 | } 317 | 318 | logResize (); 319 | scrollToEnd (); 320 | } 321 | 322 | if (subresp.math !== undefined && subresp.math.length) { // math results present? 323 | for (let mathidx in subresp.math) { 324 | $(eLogEval).append (`
`); 325 | let eLogEvalDiv = eLogEval.lastElementChild; 326 | 327 | $(eLogEvalDiv).append (``); 328 | let eLogEvalMath = eLogEvalDiv.lastElementChild; 329 | 330 | $(eLogEvalDiv).append (``); 331 | let eLogEvalWait = eLogEvalDiv.lastElementChild; 332 | 333 | MJQueue.Push (['Typeset', MathJax.Hub, eLogEvalMath, function () { 334 | eLogEvalDiv.removeChild (eLogEvalWait); 335 | 336 | eLogEvalMath.style.visibility = ''; 337 | 338 | logResize (); 339 | scrollToEnd (); 340 | }]); 341 | 342 | reprioritizeMJQueue (); 343 | } 344 | } 345 | 346 | if (subresp.err !== undefined) { // error? 347 | let eErrorHiddenBox, eLogErrorHidden; 348 | 349 | if (subresp.err.length > 1) { 350 | $(eLogEval).append ('
'); 351 | eErrorHiddenBox = eLogEval.lastElementChild; 352 | 353 | $(eErrorHiddenBox).append (`
`); 354 | eLogErrorHidden = eErrorHiddenBox.lastElementChild; 355 | 356 | for (let i = 0; i < subresp.err.length - 1; i ++) { 357 | $(eLogErrorHidden).append (`
${escapeHTML (subresp.err [i]).replace (/ /g, ' ')}
`); 358 | } 359 | } 360 | 361 | $(eLogEval).append (`
${escapeHTML (subresp.err [subresp.err.length - 1])}
`); 362 | let eLogErrorBottom = eLogEval.lastElementChild; 363 | 364 | if (subresp.err.length > 1) { 365 | let ClickHereToOpen = null; 366 | 367 | if (!ExceptionDone) { 368 | $(eLogErrorBottom).append (' <-- click to open'); 369 | 370 | ClickHereToOpen = eLogErrorBottom.lastElementChild; 371 | ExceptionDone = true; 372 | } 373 | 374 | $(eErrorHiddenBox).append (`
\u25b7
`); 375 | let eLogErrorTriangle = eErrorHiddenBox.lastElementChild; 376 | 377 | f = function () { 378 | if (eLogErrorHidden.style.display === 'none') { 379 | eLogErrorHidden.style.display = 'block'; 380 | eLogErrorTriangle.innerText = '\u25bd'; 381 | } else { 382 | eLogErrorHidden.style.display = 'none'; 383 | eLogErrorTriangle.innerText = '\u25b7'; 384 | } 385 | 386 | if (ClickHereToOpen) { 387 | ClickHereToOpen.parentNode.removeChild (ClickHereToOpen); 388 | ClickHereToOpen = null; 389 | } 390 | 391 | logResize (); 392 | }; 393 | 394 | $(eLogErrorHidden).click (f); 395 | $(eLogErrorBottom).click (f); 396 | $(eLogErrorTriangle).click (f); 397 | } 398 | 399 | logResize (); 400 | scrollToEnd (); 401 | } 402 | 403 | if (subresp.img !== undefined) { // image present? 404 | $(eLogEval).append (`
`); 405 | 406 | setTimeout (function () { // image seems to take some time to register size even though it is directly present 407 | logResize (); 408 | scrollToEnd (); 409 | }, 0); 410 | } 411 | } 412 | } 413 | 414 | function inputting (text, reset = false) { 415 | if (reset) { 416 | ErrorIdx = null; 417 | Autocomplete = []; 418 | 419 | JQInput.val (text); 420 | } 421 | 422 | updateOverlay (text, ErrorIdx, Autocomplete); 423 | 424 | $.ajax ({ 425 | url: URL, 426 | type: 'POST', 427 | cache: false, 428 | dataType: 'json', 429 | success: ajaxValidate, 430 | data: { 431 | mode: 'validate', 432 | idx: LogIdx, 433 | subidx: UniqueID ++, 434 | text: text, 435 | }, 436 | }); 437 | } 438 | 439 | function inputted (text) { 440 | $.ajax ({ 441 | url: URL, 442 | type: 'POST', 443 | cache: false, 444 | dataType: 'json', 445 | success: ajaxEvaluate, 446 | data: { 447 | mode: 'evaluate', 448 | idx: LogIdx, 449 | text: text, 450 | }, 451 | }); 452 | 453 | $('#LogEntry' + LogIdx).append (` 454 |
455 | 456 |
`); 457 | 458 | History.push (text); 459 | 460 | HistIdx = History.length; 461 | 462 | addLogEntry (); 463 | logResize (); 464 | scrollToEnd (); 465 | } 466 | 467 | //............................................................................................... 468 | function inputKeypress (e) { 469 | if (e.which == 13) { 470 | s = JQInput.val ().trim (); 471 | 472 | if ((s && ErrorIdx === null) || s === '?') { 473 | if (!GreetingFadedOut) { 474 | GreetingFadedOut = true; 475 | $('#Greeting').fadeOut (3000); 476 | } 477 | 478 | if (s === 'help' || s === '?') { 479 | window.open (`${URL}help.html`); 480 | inputting ('', true); 481 | 482 | return false; 483 | } 484 | 485 | if (Autocomplete.length > 0) { 486 | s = s + Autocomplete.join (''); 487 | inputting (s); 488 | } 489 | 490 | JQInput.val (''); 491 | updateOverlay ('', null, []); 492 | inputted (s); 493 | 494 | return false; 495 | 496 | } else if (LastValidation !== null && LastValidation.error) { // last validation had error, display 497 | let eLogInput = document.getElementById (`LogInput${LastValidation.idx}`); 498 | 499 | $('#ValidationError').remove (); 500 | $(eLogInput).append (`<-- ${escapeHTML (LastValidation.error)}`) 501 | } 502 | 503 | } else if (e.which == 32) { 504 | if (!JQInput.val ()) { 505 | return false; 506 | } 507 | } 508 | 509 | return true; 510 | } 511 | 512 | function inputKeydown (e) { 513 | if (e.code == 'Escape') { 514 | e.preventDefault (); 515 | 516 | if (JQInput.val ()) { 517 | HistIdx = History.length; 518 | inputting ('', true); 519 | 520 | return false; 521 | } 522 | 523 | } else if (e.code == 'Tab') { 524 | e.preventDefault (); 525 | $(this).focus (); 526 | 527 | return false; 528 | 529 | } else if (e.code == 'ArrowUp') { 530 | e.preventDefault (); 531 | 532 | if (HistIdx) { 533 | inputting (History [-- HistIdx], true); 534 | 535 | return false; 536 | } 537 | 538 | } else if (e.code == 'ArrowDown') { 539 | e.preventDefault (); 540 | 541 | if (HistIdx < History.length - 1) { 542 | inputting (History [++ HistIdx], true); 543 | 544 | return false; 545 | 546 | } else if (HistIdx != History.length) { 547 | HistIdx = History.length; 548 | inputting ('', true); 549 | 550 | return false; 551 | } 552 | 553 | } else if (e.code == 'ArrowRight') { 554 | if (JQInput.get (0).selectionStart === JQInput.val ().length && Autocomplete.length) { 555 | let text = JQInput.val (); 556 | 557 | text = text + Autocomplete [0]; 558 | Autocomplete = Autocomplete.slice (1); 559 | 560 | JQInput.val (text); 561 | inputting (text); 562 | } 563 | } 564 | 565 | setTimeout (updateOverlayPosition, 0); 566 | 567 | return true; 568 | } 569 | 570 | //............................................................................................... 571 | class _Variables { 572 | constructor () { 573 | this.eVarDiv = document.getElementById ('VarDiv'); 574 | this.eVarTab = document.getElementById ('VarTab'); 575 | this.eVarContent = document.getElementById ('VarContent'); 576 | this.eVarTable = document.getElementById ('VarTable'); 577 | this.queued_update = null; 578 | this.display = true; 579 | this.vars = new Map (); 580 | } 581 | 582 | _update (vars) { 583 | function spliteq (text) { 584 | let p = text.split (' = '); 585 | 586 | return [p [0], p.slice (1).join (' = ')]; 587 | } 588 | 589 | vars = new Map (vars.map (function (e) { 590 | let nat = spliteq (e.nat); 591 | 592 | return [nat [0], {tex: spliteq (e.tex), nat: nat, py: spliteq (e.py)}]; 593 | })); 594 | 595 | let same = new Set (); 596 | 597 | for (let r of Array.from (this.eVarTable.childNodes)) { 598 | let v = vars.get (r.name); 599 | 600 | if (v === undefined || r.val !== v.tex.join (' = ')) { 601 | this.eVarTable.removeChild (r); 602 | } else { 603 | same.add (r.name); 604 | } 605 | } 606 | 607 | let added = false; 608 | 609 | for (let [n, v] of vars) { 610 | if (same.has (n)) { 611 | continue; 612 | } 613 | 614 | let inserted = false; 615 | let isfunc = n.includes ('('); 616 | let e = $(`$${escapeHTMLtex (v.tex [0])}$$=$$${escapeHTMLtex (v.tex [1])}$`); 617 | e [0].name = n; 618 | e [0].val = v.tex.join (' = '); 619 | added = true; 620 | 621 | for (let r of this.eVarTable.childNodes) { 622 | let isfuncr = r.name.includes ('('); 623 | 624 | if ((isfunc && !isfuncr) || ((n < r.name) && (isfunc === isfuncr))) { 625 | e.insertBefore (r); 626 | inserted = true; 627 | 628 | break; 629 | } 630 | } 631 | 632 | if (!inserted) { 633 | e.appendTo (this.eVarTable); 634 | } 635 | } 636 | 637 | this.vars = vars; 638 | 639 | if (added) { 640 | MJQueue.Push (['Typeset', MathJax.Hub, this.eVarTable]); 641 | reprioritizeMJQueue (); 642 | } 643 | } 644 | 645 | update (vars) { 646 | if (this.display) { 647 | this._update (vars); 648 | } else { 649 | this.queued_update = vars; 650 | } 651 | 652 | this.eVarDiv.style.display = vars.length ? 'block' : 'none'; 653 | } 654 | 655 | toggle () { 656 | this.eVarContent.style.minWidth = `${this.eVarTab.clientWidth + 2}px`; 657 | 658 | if (!this.display && this.queued_update !== null) { 659 | this._update (this.queued_update); 660 | this.queued_update = null; 661 | } 662 | 663 | this.display = !this.display; 664 | this.eVarContent.style.display = this.display ? 'block' : 'none'; 665 | } 666 | } 667 | 668 | //............................................................................................... 669 | $(function () { 670 | if (window.location.pathname != '/') { 671 | return; 672 | } 673 | 674 | window.JQInput = $('#Input'); 675 | window.Variables = new _Variables (); 676 | 677 | let margin = $('body').css ('margin-top'); 678 | BodyMarginTop = Number (margin.slice (0, margin.length - 2)); 679 | margin = $('body').css ('margin-bottom'); 680 | BodyMarginBottom = Number (margin.slice (0, margin.length - 2)); 681 | 682 | $('#Clipboard').prop ('readonly', true); 683 | $('#InputCover') [0].height = $('#InputCover').height (); 684 | 685 | JQInput.keypress (inputKeypress); 686 | JQInput.keydown (inputKeydown); 687 | 688 | addLogEntry (); 689 | logResize (); 690 | resize (); 691 | monitorStuff (); 692 | 693 | function first_vars_update (resp) { 694 | if (MJQueue === null) { // wait for MathJax ready 695 | setTimeout (function () { first_vars_update (resp); }, 50); 696 | } else { 697 | Variables.update (resp.vars); 698 | } 699 | } 700 | 701 | $.ajax ({ 702 | url: URL, 703 | type: 'POST', 704 | cache: false, 705 | dataType: 'json', 706 | success: first_vars_update, 707 | data: {mode: 'vars'}, 708 | }); 709 | }); 710 | -------------------------------------------------------------------------------- /sxlat.py: -------------------------------------------------------------------------------- 1 | # AST translations for funtions to display or convert to internal AST or SymPy S() escaping. 2 | 3 | import itertools as it 4 | 5 | from sast import AST # AUTO_REMOVE_IN_SINGLE_SCRIPT 6 | 7 | _SX_XLAT_AND = True # ability to turn off And translation for testing 8 | _SX_READ_PY_ASS_EQ = False # ability to parse py Eq() functions which were marked as assignment for testing 9 | 10 | _AST_StrPlus = AST ('"', '+') 11 | 12 | #............................................................................................... 13 | def _xlat_f2a_slice (*args): 14 | if len (args) == 1: 15 | return AST ('-slice', False, False if args [0] == AST.None_ else args [0], None) 16 | if len (args) == 2: 17 | return AST ('-slice', False if args [0] == AST.None_ else args [0], False if args [1] == AST.None_ else args [1], None) 18 | else: 19 | return AST ('-slice', False if args [0] == AST.None_ else args [0], False if args [1] == AST.None_ else args [1], args [2] if args [2] != AST.None_ else None if args [1] == AST.None_ else False) 20 | 21 | _xlat_f2a_Add_invert = {'==': '==', '!=': '!=', '<': '>', '<=': '>=', '>': '<', '>=': '<='} 22 | 23 | def _xlat_f2a_And (*args, canon = False, force = False): # patch together out of order extended comparison objects potentially inverting comparisons 24 | def concat (lhs, rhs): 25 | return AST ('<>', lhs.lhs, lhs.cmp + rhs.cmp) 26 | 27 | def invert (ast): 28 | cmp = [] 29 | lhs = ast.lhs 30 | 31 | for c in ast.cmp: 32 | v = _xlat_f2a_Add_invert.get (c [0]) 33 | 34 | if v is None: 35 | return None 36 | 37 | cmp.append ((v, lhs)) 38 | 39 | lhs = c [1] 40 | 41 | return AST ('<>', lhs, tuple (cmp [::-1])) 42 | 43 | def match (ast): 44 | li, ll = None, 0 45 | ri, rl = None, 0 46 | 47 | for i in range (len (args)): 48 | if args [i].is_cmp: 49 | if ast.lhs == args [i].cmp [-1] [1] and (li is None or args [i].cmp.len > ll): 50 | li, ll = i, args [i].cmp.len 51 | 52 | if ast.cmp [-1] [1] == args [i].lhs and (ri is None or args [i].cmp.len > rl): 53 | ri, rl = i, args [i].cmp.len 54 | 55 | return li, ri, ll + rl 56 | 57 | def canonicalize (ast): 58 | return invert (ast) if (canon and ast.is_cmp and sum ((r [0] == '>') - (r [0] == '<') for r, c in ast.cmp) > 0) else ast 59 | 60 | def count_ops (ast): 61 | if ast.is_and: 62 | return ast.and_.len - 1 + sum (count_ops (a) for a in ast.and_) 63 | elif ast.is_cmp: 64 | return ast.cmp.len + count_ops (ast.lhs) + sum (count_ops (ra [1]) for ra in ast.cmp) 65 | 66 | return 0 67 | 68 | # start here 69 | if not _SX_XLAT_AND and not force: 70 | return None # AST ('-func', 'And', args) 71 | 72 | and_ = AST ('-and', tuple (args)) # simple and 73 | andc = [args [0]] # build concatenated and from comparisons 74 | 75 | for arg in args [1:]: 76 | if arg.is_cmp and andc [-1].is_cmp and arg.lhs == andc [-1].cmp [-1] [1]: 77 | andc [-1] = AST ('<>', andc [-1].lhs, andc [-1].cmp + arg.cmp) 78 | else: 79 | andc.append (arg) 80 | 81 | andc = AST ('-and', tuple (andc)) if len (andc) > 1 else andc [0] 82 | itr = iter (args) 83 | args = [] 84 | 85 | for arg in itr: # build optimized and 86 | if not args or not arg.is_cmp: 87 | args.append (arg) 88 | 89 | else: 90 | while 1: 91 | li, ri, l = match (arg) 92 | argv = invert (arg) 93 | 94 | if argv is not None: 95 | liv, riv, lv = match (argv) 96 | 97 | if lv > l: 98 | li, ri = liv, riv 99 | arg = argv 100 | 101 | if li is None or li == ri: 102 | if ri is None: 103 | args.append (arg) 104 | break 105 | 106 | else: 107 | arg = concat (arg, args [ri]) 108 | del args [ri] 109 | 110 | elif ri is None: 111 | arg = concat (args [li], arg) 112 | del args [li] 113 | 114 | else: 115 | i1, i2 = min (li, ri), max (li, ri) 116 | arg = concat (concat (args [li], arg), args [ri]) 117 | 118 | del args [i2], args [i1] 119 | 120 | if len (args) == 1: 121 | ast = canonicalize (args [0]) 122 | else: 123 | ast = AST ('-and', tuple (canonicalize (a) for a in args)) 124 | 125 | return min (andc, and_, ast, key = lambda a: count_ops (a)) 126 | 127 | def _xlat_f2a_Lambda (args, expr): 128 | args = args.strip_paren 129 | args = args.comma if args.is_comma else (args,) 130 | vars = [] 131 | 132 | for v in args: 133 | if not v.is_var_nonconst: 134 | return None 135 | 136 | vars.append (v.var) 137 | 138 | return AST ('-lamb', expr, tuple (vars)) 139 | 140 | def _xlat_f2a_Pow (ast = AST.VarNull, exp = AST.VarNull): 141 | return AST ('^', ast, exp) 142 | 143 | def _xlat_f2a_Matrix (ast = AST.VarNull): 144 | if ast.is_var_null: 145 | return AST.MatEmpty 146 | 147 | if ast.is_brack: 148 | if not ast.brack: 149 | return AST.MatEmpty 150 | 151 | elif not ast.brack [0].is_brack: # single layer or brackets, column matrix? 152 | return AST ('-mat', tuple ((c,) for c in ast.brack)) 153 | 154 | elif ast.brack [0].brack: 155 | rows = [ast.brack [0].brack] 156 | cols = len (rows [0]) 157 | 158 | for row in ast.brack [1 : -1]: 159 | if row.brack.len != cols: 160 | break 161 | 162 | rows.append (row.brack) 163 | 164 | else: 165 | l = ast.brack [-1].brack.len 166 | 167 | if l <= cols: 168 | if ast.brack.len > 1: 169 | rows.append (ast.brack [-1].brack + (AST.VarNull,) * (cols - l)) 170 | 171 | if l != cols: 172 | return AST ('-mat', tuple (rows)) 173 | elif cols > 1: 174 | return AST ('-mat', tuple (rows)) 175 | else: 176 | return AST ('-mat', tuple ((r [0],) for r in rows)) 177 | 178 | return None 179 | 180 | def _xlat_f2a_Piecewise (*args): 181 | pcs = [] 182 | 183 | if not args or args [0].is_var_null: 184 | return AST ('-piece', ((AST.VarNull, AST.VarNull),)) 185 | 186 | if len (args) > 1: 187 | for c in args [:-1]: 188 | c = c.strip 189 | 190 | if not c.is_comma or c.comma.len != 2: 191 | return None 192 | 193 | pcs.append (c.comma) 194 | 195 | ast = args [-1] 196 | 197 | if not ast.is_paren: 198 | return None 199 | 200 | ast = ast.strip 201 | pcs = tuple (pcs) 202 | 203 | if not ast.is_comma: 204 | return AST ('-piece', pcs + ((ast, AST.VarNull),)) 205 | elif ast.comma.len == 0: 206 | return AST ('-piece', pcs + ()) 207 | 208 | if not ast.comma [0].is_comma: 209 | if ast.comma.len == 1: 210 | return AST ('-piece', pcs + ((ast.comma [0], AST.VarNull),)) 211 | elif ast.comma.len == 2: 212 | return AST ('-piece', pcs + ((ast.comma [0], True if ast.comma [1] == AST.True_ else ast.comma [1]),)) 213 | 214 | return None 215 | 216 | def _xlat_f2a_Derivative_NAT (ast = AST.VarNull, *dvs, **kw): 217 | if not kw: 218 | return _xlat_f2a_Derivative (ast, *dvs) 219 | 220 | def _xlat_f2a_Derivative (ast = AST.VarNull, *dvs, **kw): 221 | ds = [] 222 | 223 | if not dvs: 224 | if ast.is_diffp: 225 | return AST ('-diffp', ast.diffp, ast.count + 1) 226 | else: 227 | return AST ('-diffp', ast, 1) 228 | 229 | else: 230 | dvs = list (dvs [::-1]) 231 | 232 | while dvs: 233 | v = dvs.pop () 234 | 235 | if not v.is_var: 236 | return None 237 | 238 | ds.append ((v.var, dvs.pop ().as_int if dvs and dvs [-1].is_num_pos_int else 1)) 239 | 240 | return AST ('-diff', ast, 'd', tuple (ds)) 241 | 242 | def _xlat_f2a_Integral_NAT (ast = None, dvab = None, *args, **kw): 243 | if not kw: 244 | return _xlat_f2a_Integral (ast, dvab, *args, **kw) 245 | 246 | def _xlat_f2a_Integral (ast = None, dvab = None, *args, **kw): 247 | if ast is None: 248 | return AST ('-intg', AST.VarNull, AST.VarNull) 249 | 250 | if dvab is None: 251 | vars = ast.free_vars 252 | 253 | if len (vars) == 1: 254 | return AST ('-intg', ast, ('@', f'd{vars.pop ().var}')) 255 | 256 | return AST ('-intg', ast, AST.VarNull) 257 | 258 | dvab = dvab.strip_paren 259 | ast2 = None 260 | 261 | if dvab.is_comma: 262 | if dvab.comma and dvab.comma [0].is_var:#_nonconst: 263 | if dvab.comma.len == 1: 264 | ast2 = AST ('-intg', ast, ('@', f'd{dvab.comma [0].var}')) 265 | elif dvab.comma.len == 2: 266 | ast2 = AST ('-intg', ast, ('@', f'd{dvab.comma [0].var}'), AST.Zero, dvab.comma [1]) 267 | elif dvab.comma.len == 3: 268 | ast2 = AST ('-intg', ast, ('@', f'd{dvab.comma [0].var}'), dvab.comma [1], dvab.comma [2]) 269 | 270 | elif dvab.is_var: 271 | ast2 = AST ('-intg', ast, ('@', f'd{dvab.var}')) 272 | 273 | if ast2 is None: 274 | return None 275 | 276 | return _xlat_f2a_Integral (ast2, *args) if args else ast2 277 | 278 | _xlat_f2a_Limit_dirs = {AST ('"', '+'): ('+',), AST ('"', '-'): ('-',), AST ('"', '+-'): ()} 279 | 280 | def _xlat_f2a_Limit (ast = AST.VarNull, var = AST.VarNull, to = AST.VarNull, dir = _AST_StrPlus): 281 | if var.is_var_nonconst: 282 | return AST ('-lim', ast, var, to, *_xlat_f2a_Limit_dirs [dir]) 283 | 284 | return None 285 | 286 | def _xlat_f2a_Sum_NAT (ast = AST.VarNull, ab = None, **kw): 287 | if not kw: 288 | return _xlat_f2a_Sum (ast, ab, **kw) 289 | 290 | return None 291 | 292 | def _xlat_f2a_Sum (ast = AST.VarNull, ab = None, **kw): 293 | if ab is None: 294 | return AST ('-sum', ast, AST.VarNull, AST.VarNull, AST.VarNull) 295 | 296 | ab = ab.strip_paren 297 | 298 | if ab.is_var: 299 | return AST ('-sum', ast, ab, AST.VarNull, AST.VarNull) 300 | elif ab.is_comma and ab.comma and ab.comma.len <= 3 and ab.comma [0].is_var: 301 | return AST ('-sum', ast, *ab.comma, *((AST.VarNull,) * (3 - ab.comma.len))) 302 | 303 | return None 304 | 305 | def _xlat_f2a_SymmetricDifference (*args): 306 | if len (args) != 2: 307 | return None 308 | 309 | if not args [0].is_sdiff: 310 | return AST ('^^', args) 311 | 312 | return AST ('^^', args [0].sdiff + (args [1],)) 313 | 314 | def _xlat_f2a_Subs (expr = None, src = None, dst = None): 315 | def parse_subs (src, dst): 316 | if src is None: 317 | return ((AST.VarNull, AST.VarNull),) 318 | 319 | src = src.strip_paren.comma if src.strip_paren.is_comma else (src,) # (src.strip_paren,) 320 | 321 | if dst is None: 322 | return tuple (it.zip_longest (src, (), fillvalue = AST.VarNull)) 323 | 324 | dst = dst.strip_paren.comma if dst.strip_paren.is_comma else (dst,) # (dst.stip_paren,) 325 | 326 | if len (dst) > len (src): 327 | return None 328 | 329 | return tuple (it.zip_longest (src, dst, fillvalue = AST.VarNull)) 330 | 331 | # start here 332 | if expr is None: 333 | return AST ('-subs', AST.VarNull, ((AST.VarNull, AST.VarNull),)) 334 | 335 | subs = parse_subs (src, dst) 336 | 337 | if subs is None: 338 | return None 339 | 340 | if expr.is_subs: 341 | return AST ('-subs', expr.expr, expr.subs + subs) 342 | else: 343 | return AST ('-subs', expr, subs) 344 | 345 | def _xlat_f2a_subs (expr, src = AST.VarNull, dst = None): 346 | def parse_subs (src, dst): 347 | if dst is not None: 348 | return ((src, dst),) 349 | 350 | src = src.strip_paren 351 | 352 | if src.is_dict: 353 | return src.dict 354 | elif src.op not in {',', '[', '-set'}: 355 | return None # ((src, AST.VarNull),) 356 | 357 | else: 358 | subs = [] 359 | 360 | for arg in src [1]: 361 | ast = arg.strip_paren 362 | 363 | if ast.op in {',', '['} and ast [1].len <= 2: 364 | subs.append ((ast [1] + (AST.VarNull, AST.VarNull)) [:2]) 365 | elif arg.is_paren and arg is src [1] [-1]: 366 | subs.append ((ast, AST.VarNull)) 367 | else: 368 | return None 369 | 370 | return tuple (subs) 371 | 372 | # start here 373 | subs = parse_subs (src, dst) 374 | 375 | if subs is None: 376 | return None 377 | 378 | if expr.is_subs: # collapse multiple subs into one 379 | return AST ('-subs', expr.expr, expr.subs + subs) 380 | 381 | return AST ('-subs', expr, subs) 382 | 383 | #............................................................................................... 384 | _XLAT_FUNC2AST_REIM = { 385 | 'Re' : lambda *args: AST ('-func', 're', tuple (args)), 386 | 'Im' : lambda *args: AST ('-func', 'im', tuple (args)), 387 | } 388 | 389 | _XLAT_FUNC2AST_ALL = { 390 | 'Subs' : _xlat_f2a_Subs, 391 | '.subs' : _xlat_f2a_subs, 392 | } 393 | 394 | _XLAT_FUNC2AST_TEXNATPY = {**_XLAT_FUNC2AST_ALL, 395 | 'slice' : _xlat_f2a_slice, 396 | 397 | 'Eq' : lambda a, b, *args: AST ('<>', a, (('==', b),)) if not args else AST ('=', a, b, ass_is_not_kw = True) if _SX_READ_PY_ASS_EQ else None, # extra *args is for marking as assignment during testing 398 | 'Ne' : lambda a, b: AST ('<>', a, (('!=', b),)), 399 | 'Lt' : lambda a, b: AST ('<>', a, (('<', b),)), 400 | 'Le' : lambda a, b: AST ('<>', a, (('<=', b),)), 401 | 'Gt' : lambda a, b: AST ('<>', a, (('>', b),)), 402 | 'Ge' : lambda a, b: AST ('<>', a, (('>=', b),)), 403 | 404 | 'Or' : lambda *args: AST ('-or', tuple (args)), 405 | 'And' : _xlat_f2a_And, 406 | 'Not' : lambda not_: AST ('-not', not_), 407 | } 408 | 409 | _XLAT_FUNC2AST_TEXNAT = {**_XLAT_FUNC2AST_TEXNATPY, 410 | 'S' : lambda ast, **kw: ast if ast.is_num and not kw else None, 411 | 412 | 'abs' : lambda *args: AST ('|', args [0] if len (args) == 1 else AST (',', args)), 413 | 'Abs' : lambda *args: AST ('|', args [0] if len (args) == 1 else AST (',', args)), 414 | 'exp' : lambda ast: AST ('^', AST.E, ast), 415 | 'factorial' : lambda ast: AST ('!', ast), 416 | 'Lambda' : _xlat_f2a_Lambda, 417 | 'Matrix' : _xlat_f2a_Matrix, 418 | 'MutableDenseMatrix' : _xlat_f2a_Matrix, 419 | 'Piecewise' : _xlat_f2a_Piecewise, 420 | 'Pow' : _xlat_f2a_Pow, 421 | 'pow' : _xlat_f2a_Pow, 422 | 'Tuple' : lambda *args: AST ('(', (',', args)), 423 | 424 | 'Limit' : _xlat_f2a_Limit, 425 | 'limit' : _xlat_f2a_Limit, 426 | 427 | 'EmptySet' : lambda *args: AST.SetEmpty, 428 | 'FiniteSet' : lambda *args: AST ('-set', tuple (args)), 429 | 'Contains' : lambda a, b: AST ('<>', a, (('in', b),)), 430 | 'Complement' : lambda *args: AST ('+', (args [0], ('-', args [1]))), 431 | 'Union' : lambda *args: AST ('||', tuple (args)), # _xlat_f2a_Union, 432 | 'SymmetricDifference' : _xlat_f2a_SymmetricDifference, 433 | 'Intersection' : lambda *args: AST ('&&', tuple (args)), 434 | } 435 | 436 | XLAT_FUNC2AST_TEX = {**_XLAT_FUNC2AST_TEXNAT, 437 | 'Add' : lambda *args, **kw: AST ('+', args), 438 | 'Mul' : lambda *args, **kw: AST ('*', args), 439 | 440 | 'Derivative' : _xlat_f2a_Derivative, 441 | 'Integral' : _xlat_f2a_Integral, 442 | 'Sum' : _xlat_f2a_Sum, 443 | 'diff' : _xlat_f2a_Derivative, 444 | 'integrate' : _xlat_f2a_Integral, 445 | 'summation' : _xlat_f2a_Sum, 446 | 447 | 'SparseMatrix' : _xlat_f2a_Matrix, 448 | 'MutableSparseMatrix' : _xlat_f2a_Matrix, 449 | 'ImmutableDenseMatrix' : _xlat_f2a_Matrix, 450 | 'ImmutableSparseMatrix': _xlat_f2a_Matrix, 451 | 452 | 'diag' : True, 453 | 'eye' : True, 454 | 'ones' : True, 455 | 'zeros' : True, 456 | } 457 | 458 | XLAT_FUNC2AST_NAT = {**_XLAT_FUNC2AST_TEXNAT, **_XLAT_FUNC2AST_REIM, 459 | 'Add' : lambda *args, **kw: None if kw else AST ('+', args), 460 | 'Mul' : lambda *args, **kw: None if kw else AST ('*', args), 461 | 462 | 'Derivative' : _xlat_f2a_Derivative_NAT, 463 | 'Integral' : _xlat_f2a_Integral_NAT, 464 | 'Sum' : _xlat_f2a_Sum_NAT, 465 | } 466 | 467 | XLAT_FUNC2AST_PY = {**_XLAT_FUNC2AST_TEXNATPY, **_XLAT_FUNC2AST_REIM, 468 | 'Gamma' : lambda *args: AST ('-func', 'gamma', tuple (args)), 469 | } 470 | 471 | XLAT_FUNC2AST_SPARSER = { 472 | 'Lambda' : _xlat_f2a_Lambda, 473 | 'Limit' : _xlat_f2a_Limit, 474 | 'Sum' : _xlat_f2a_Sum_NAT, 475 | 'Derivative' : _xlat_f2a_Derivative_NAT, 476 | 'Integral' : _xlat_f2a_Integral_NAT, 477 | 'Subs' : _xlat_f2a_Subs, 478 | '.subs' : _xlat_f2a_subs, 479 | } 480 | 481 | XLAT_FUNC2AST_SPT = XLAT_FUNC2AST_PY 482 | 483 | def xlat_funcs2asts (ast, xlat, func_call = None, recurse = True): # translate eligible functions in tree to other AST representations 484 | if not isinstance (ast, AST): 485 | return ast 486 | 487 | if ast.is_func: 488 | xact = xlat.get (ast.func) 489 | args = ast.args 490 | ret = lambda: AST ('-func', ast.func, args) 491 | 492 | elif ast.is_attr_func: 493 | xact = xlat.get (f'.{ast.attr}') 494 | args = (ast.obj,) + ast.args 495 | ret = lambda: AST ('.', args [0], ast.attr, tuple (args [1:])) 496 | 497 | else: 498 | xact = None 499 | 500 | if xact is not None: 501 | if recurse: 502 | args = AST (*(xlat_funcs2asts (a, xlat, func_call = func_call) for a in args)) 503 | 504 | try: 505 | if xact is True: # True means execute function and use return value for ast, only happens for -func 506 | return func_call (ast.func, args) # not checking func_call None because that should never happen 507 | 508 | xargs, xkw = AST.args2kwargs (args) 509 | ast2 = xact (*xargs, **xkw) 510 | 511 | if ast2 is not None: 512 | return ast2 513 | 514 | except: 515 | pass 516 | 517 | return ret () 518 | 519 | if recurse: 520 | return AST (*(xlat_funcs2asts (a, xlat, func_call = func_call) for a in ast))#, **ast._kw) 521 | 522 | return ast 523 | 524 | #............................................................................................... 525 | _XLAT_FUNC2TEX = { 526 | 'beta' : lambda ast2tex, *args: f'\\beta{{\\left({ast2tex (AST.tuple2argskw (args))} \\right)}}', 527 | 'gamma' : lambda ast2tex, *args: f'\\Gamma{{\\left({ast2tex (AST.tuple2argskw (args))} \\right)}}', 528 | 'Gamma' : lambda ast2tex, *args: f'\\Gamma{{\\left({ast2tex (AST.tuple2argskw (args))} \\right)}}', 529 | 'Lambda' : lambda ast2tex, *args: f'\\Lambda{{\\left({ast2tex (AST.tuple2argskw (args))} \\right)}}', 530 | 'zeta' : lambda ast2tex, *args: f'\\zeta{{\\left({ast2tex (AST.tuple2argskw (args))} \\right)}}', 531 | 532 | 're' : lambda ast2tex, *args: f'\\Re{{\\left({ast2tex (AST.tuple2argskw (args))} \\right)}}', 533 | 'im' : lambda ast2tex, *args: f'\\Im{{\\left({ast2tex (AST.tuple2argskw (args))} \\right)}}', 534 | 535 | 'binomial': lambda ast2tex, *args: f'\\binom{{{ast2tex (args [0])}}}{{{ast2tex (args [1])}}}' if len (args) == 2 else None, 536 | 'set' : lambda ast2tex, *args: '\\emptyset' if not args else None, 537 | } 538 | 539 | _XLAT_ATTRFUNC2TEX = { 540 | 'diff' : lambda ast2tex, ast, *dvs, **kw: ast2tex (_xlat_f2a_Derivative (ast, *dvs, **kw)), 541 | 'integrate': lambda ast2tex, ast, dvab = None, *args, **kw: ast2tex (_xlat_f2a_Integral (ast, dvab, *args, **kw)), 542 | 'limit' : lambda ast2tex, ast, var = AST.VarNull, to = AST.VarNull, dir = _AST_StrPlus: ast2tex (_xlat_f2a_Limit (ast, var, to, dir)), 543 | } 544 | 545 | def xlat_func2tex (ast, ast2tex): 546 | xact = _XLAT_FUNC2TEX.get (ast.func) 547 | 548 | if xact: 549 | args, kw = AST.args2kwargs (ast.args) 550 | 551 | try: 552 | return xact (ast2tex, *args, **kw) 553 | except: 554 | pass 555 | 556 | return None 557 | 558 | def xlat_attr2tex (ast, ast2tex): 559 | if ast.is_attr_func: 560 | xact = _XLAT_ATTRFUNC2TEX.get (ast.attr) 561 | 562 | if xact: 563 | args, kw = AST.args2kwargs (ast.args) 564 | 565 | try: 566 | return xact (ast2tex, ast.obj, *args, **kw) 567 | except: 568 | pass 569 | 570 | return None 571 | 572 | #............................................................................................... 573 | def _xlat_pyS (ast, need = False): # Python S(1)/2 escaping where necessary 574 | if not isinstance (ast, AST): 575 | return ast, False 576 | 577 | if ast.is_num: 578 | if need: 579 | return AST ('-func', 'S', (ast,)), True 580 | else: 581 | return ast, False 582 | 583 | if ast.is_comma or ast.is_brack: 584 | return AST (ast.op, tuple (_xlat_pyS (a) [0] for a in ast [1])), False 585 | 586 | if ast.is_curly or ast.is_paren or ast.is_minus: 587 | expr, has = _xlat_pyS (ast [1], need) 588 | 589 | return AST (ast.op, expr), has 590 | 591 | if ast.is_add or ast.is_mul: 592 | es = [_xlat_pyS (a) for a in ast [1] [1:]] 593 | has = any (e [1] for e in es) 594 | e0 = _xlat_pyS (ast [1] [0], need and not has) 595 | es = (e0 [0],) + tuple (e [0] for e in es) 596 | 597 | return (AST ('+', es) if ast.is_add else AST ('*', es, ast.exp)), has or e0 [1] 598 | 599 | if ast.is_div: 600 | denom, has = _xlat_pyS (ast.denom) 601 | numer = _xlat_pyS (ast.numer, not has) [0] 602 | 603 | return AST ('/', numer, denom), True 604 | 605 | if ast.is_pow: 606 | exp, has = _xlat_pyS (ast.exp) 607 | base = _xlat_pyS (ast.base, not (has or exp.is_num_pos)) [0] 608 | 609 | return AST ('^', base, exp), True 610 | 611 | es = [_xlat_pyS (a) for a in ast] 612 | 613 | return AST (*tuple (e [0] for e in es)), \ 614 | ast.op in {'=', '<>', '@', '.', '|', '!', '-log', '-sqrt', '-func', '-lim', '-sum', '-diff', '-intg', '-mat', '-piece', '-lamb', '||', '^^', '&&', '-or', '-and', '-not', '-ufunc', '-subs'} or any (e [1] for e in es) 615 | 616 | xlat_pyS = lambda ast: _xlat_pyS (ast) [0] 617 | 618 | #............................................................................................... 619 | class sxlat: # for single script 620 | XLAT_FUNC2AST_SPARSER = XLAT_FUNC2AST_SPARSER 621 | XLAT_FUNC2AST_TEX = XLAT_FUNC2AST_TEX 622 | XLAT_FUNC2AST_NAT = XLAT_FUNC2AST_NAT 623 | XLAT_FUNC2AST_PY = XLAT_FUNC2AST_PY 624 | XLAT_FUNC2AST_SPT = XLAT_FUNC2AST_SPT 625 | xlat_funcs2asts = xlat_funcs2asts 626 | xlat_func2tex = xlat_func2tex 627 | xlat_attr2tex = xlat_attr2tex 628 | xlat_pyS = xlat_pyS 629 | _xlat_f2a_And = _xlat_f2a_And 630 | 631 | # AUTO_REMOVE_IN_SINGLE_SCRIPT_BLOCK_START 632 | if __name__ == '__main__': # DEBUG! 633 | ast = AST ('.', ('(', ('*', (('@', 'x'), ('@', 'y')))), 'subs', (('@', 'x'), ('#', '2'))) 634 | res = xlat_funcs2asts (ast, XLAT_FUNC2AST_TEX) 635 | print (res) 636 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # python 3.6+ 3 | 4 | # Server for web component and state machine for expressions. 5 | 6 | import getopt 7 | import io 8 | import json 9 | import os 10 | import re 11 | import subprocess 12 | import sys 13 | import time 14 | import threading 15 | import traceback 16 | import webbrowser 17 | 18 | from collections import OrderedDict 19 | from http.server import HTTPServer, SimpleHTTPRequestHandler 20 | from socketserver import ThreadingMixIn 21 | from urllib.parse import parse_qs 22 | 23 | _RUNNING_AS_SINGLE_SCRIPT = False # AUTO_REMOVE_IN_SINGLE_SCRIPT 24 | 25 | _VERSION = '1.1.6' 26 | 27 | _ONE_FUNCS = {'N', 'O', 'S', 'beta', 'gamma', 'Gamma', 'Lambda', 'zeta'} 28 | _ENV_OPTS = {'EI', 'quick', 'pyS', 'simplify', 'matsimp', 'ufuncmap', 'prodrat', 'doit', 'strict', *_ONE_FUNCS} 29 | _ENV_OPTS_ALL = _ENV_OPTS.union (f'no{opt}' for opt in _ENV_OPTS) 30 | 31 | __OPTS, __ARGV = getopt.getopt (sys.argv [1:], 'hvnudr', ['child', 'firstrun', 'help', 'version', 'nobrowser', 'ugly', 'debug', 'restert', *_ENV_OPTS_ALL]) 32 | __IS_MAIN = __name__ == '__main__' 33 | __IS_MODULE_RUN = sys.argv [0] == '-m' 34 | 35 | _SERVER_DEBUG = __IS_MAIN and __ARGV and __ARGV [0] == 'server-debug' 36 | 37 | _SYMPAD_PATH = os.path.dirname (sys.argv [0]) 38 | _SYMPAD_NAME = os.path.basename (sys.argv [0]) 39 | _SYMPAD_RESTART = not __IS_MODULE_RUN and (('-r', '') in __OPTS or ('--restart', '') in __OPTS) 40 | _SYMPAD_CHILD = not _SYMPAD_RESTART or ('--child', '') in __OPTS 41 | _SYMPAD_FIRSTRUN = not _SYMPAD_RESTART or ('--firstrun', '') in __OPTS 42 | _SYMPAD_DEBUG = os.environ.get ('SYMPAD_DEBUG') 43 | 44 | _DEFAULT_ADDRESS = ('localhost', 9000) 45 | _FILES = {} # pylint food # AUTO_REMOVE_IN_SINGLE_SCRIPT 46 | _STATIC_FILES = {'/style.css': 'text/css', '/script.js': 'text/javascript', '/index.html': 'text/html', 47 | '/help.html': 'text/html', '/bg.png': 'image/png', '/wait.webp': 'image/webp'} 48 | 49 | _HELP = f'usage: sympad [options] [host:port | host | :port]' ''' 50 | 51 | -h, --help - Show help information 52 | -v, --version - Show version string 53 | -n, --nobrowser - Don't start system browser to SymPad page 54 | -u, --ugly - Start in draft display style (only on command line) 55 | -d, --debug - Dump debug info to server log 56 | -r, --restart - Restart server on source file changes (for development) 57 | --EI, --noEI - Start with SymPy constants 'E' and 'I' or regular 'e' and 'i' 58 | --quick, --noquick - Start in/not quick input mode 59 | --pyS, --nopyS - Start with/out Python S escaping 60 | --simplify, --nosimplify - Start with/out post-evaluation simplification 61 | --matsimp, --nomatsimp - Start with/out matrix simplification 62 | --ufuncmap, --noufuncmap - Start with/out undefined function mapping back to variables 63 | --prodrat, --noprodrat - Start with/out separate product leading rational 64 | --doit, --nodoit - Start with/out automatic expression doit() 65 | --strict, --nostrict - Start with/out strict LaTeX formatting 66 | --N, --noN - Start with/out N function 67 | --S, --noS - Start with/out S function 68 | --O, --noO - Start with/out O function 69 | --beta, --nobeta - Start with/out beta function 70 | --gamma, --nogamma - Start with/out gamma function 71 | --Gamma, --noGamma - Start with/out Gamma function 72 | --Lambda, --noLambda - Start with/out Lambda function 73 | --zeta, --nozeta - Start with/out zeta function 74 | '''.lstrip () 75 | 76 | if _SYMPAD_CHILD: # sympy slow to import so don't do it for watcher process as is unnecessary there 77 | sys.path.insert (0, '') # allow importing from current directory first (for SymPy development version) # AUTO_REMOVE_IN_SINGLE_SCRIPT 78 | 79 | import sympy as sp 80 | import lalr1 # AUTO_REMOVE_IN_SINGLE_SCRIPT 81 | from sast import AST # AUTO_REMOVE_IN_SINGLE_SCRIPT 82 | import sym # AUTO_REMOVE_IN_SINGLE_SCRIPT 83 | import sparser # AUTO_REMOVE_IN_SINGLE_SCRIPT 84 | import spatch # AUTO_REMOVE_IN_SINGLE_SCRIPT 85 | import splot # AUTO_REMOVE_IN_SINGLE_SCRIPT 86 | 87 | _SYS_STDOUT = sys.stdout 88 | _DISPLAYSTYLE = [1] # use "\displaystyle{}" formatting in MathJax 89 | _HISTORY = [] # persistent history across browser closings 90 | 91 | _UFUNC_MAPBACK = True # map undefined functions from SymPy back to variables if possible 92 | _UFUNC_MAP = {} # map of ufunc asts to ordered sequence of variable names 93 | _SYM_MAP = {} # map of sym asts to ordered sequence of variable names 94 | _SYM_VARS = set () # set of all variables mapped to symbols 95 | 96 | _PARSER = sparser.Parser () 97 | _START_ENV = OrderedDict ([ 98 | ('EI', False), ('quick', False), ('pyS', True), ('simplify', False), ('matsimp', True), ('ufuncmap', True), ('prodrat', False), ('doit', True), ('strict', False), 99 | ('N', True), ('O', True), ('S', True), ('beta', True), ('gamma', True), ('Gamma', True), ('Lambda', True), ('zeta', True)]) 100 | 101 | _ENV = _START_ENV.copy () # This is individual session STATE! Threading can corrupt this! It is GLOBAL to survive multiple Handlers. 102 | _VARS = {'_': AST.Zero} # This also! 103 | _VARS_FLAT = _VARS.copy () # Flattened vars. 104 | 105 | #............................................................................................... 106 | def _admin_vars (*args): 107 | asts = _sorted_vars () 108 | 109 | if not asts: 110 | return 'No variables defined.' 111 | 112 | return asts 113 | 114 | def _admin_del (*args): 115 | vars = OrderedDict () 116 | msgs = [] 117 | 118 | for arg in args: 119 | var = arg.as_identifier 120 | 121 | if var is None or var == '_': 122 | raise TypeError (f'invalid argument {sym.ast2nat (arg)!r}') 123 | 124 | vars [var] = _VARS.get (var) 125 | 126 | if vars [var] is None: 127 | raise AE35UnitError (f'Variable {var!r} is not defined, it can only be attributable to human error.') 128 | 129 | for var, ast in vars.items (): 130 | msgs.append (f'{"Lambda function" if ast.is_lamb else "Undefined function" if ast.is_ufunc else "Variable"} {var!r} deleted.') 131 | 132 | del _VARS [var] 133 | 134 | _vars_updated () 135 | 136 | if not msgs: 137 | msgs.append ('No variables specified!') 138 | 139 | return msgs 140 | 141 | def _admin_delall (*args): 142 | last_var = _VARS ['_'] 143 | 144 | _VARS.clear () 145 | 146 | _VARS ['_'] = last_var 147 | 148 | _vars_updated () 149 | 150 | return 'All variables deleted.' 151 | 152 | def _admin_env (*args): 153 | vars_updated = False 154 | 155 | def _envop (env, apply): 156 | nonlocal vars_updated 157 | 158 | msgs = [] 159 | 160 | for var, state in env.items (): 161 | if apply: 162 | _ENV [var] = state 163 | 164 | if var == 'EI': 165 | msgs.append (f'Uppercase E and I is {"on" if state else "off"}.') 166 | 167 | if apply: 168 | AST.EI (state) 169 | 170 | for var in (AST.E.var, AST.I.var): 171 | if var in _VARS: 172 | del _VARS [var] 173 | 174 | elif var == 'quick': 175 | msgs.append (f'Quick input mode is {"on" if state else "off"}.') 176 | 177 | if apply: 178 | sym.set_quick (state) 179 | _PARSER.set_quick (state) 180 | 181 | vars_updated = True 182 | 183 | elif var == 'pyS': 184 | msgs.append (f'Python S escaping is {"on" if state else "off"}.') 185 | 186 | if apply: 187 | sym.set_pyS (state) 188 | 189 | elif var == 'simplify': 190 | msgs.append (f'Post-evaluation simplify is {"on" if state else "off"}.') 191 | 192 | if apply: 193 | sym.set_simplify (state) 194 | 195 | elif var == 'matsimp': 196 | msgs.append (f'Matrix simplify is {"broken" if not spatch.SPATCHED else "on" if state else "off"}.') 197 | 198 | if apply: 199 | spatch.set_matmulsimp (state) 200 | 201 | elif var == 'ufuncmap': 202 | msgs.append (f'Undefined function map to variable is {"on" if state else "off"}.') 203 | 204 | if apply: 205 | global _UFUNC_MAPBACK 206 | _UFUNC_MAPBACK = state 207 | 208 | elif var == 'prodrat': 209 | msgs.append (f'Leading product rational is {"on" if state else "off"}.') 210 | 211 | if apply: 212 | sym.set_prodrat (state) 213 | 214 | elif var == 'doit': 215 | msgs.append (f'Expression doit is {"on" if state else "off"}.') 216 | 217 | if apply: 218 | sym.set_doit (state) 219 | 220 | elif var == 'strict': 221 | msgs.append (f'Strict LaTeX formatting is {"on" if state else "off"}.') 222 | 223 | if apply: 224 | sym.set_strict (state) 225 | 226 | elif var in _ONE_FUNCS: 227 | msgs.append (f'Function {var} is {"on" if state else "off"}.') 228 | 229 | if apply: 230 | vars_updated = True 231 | 232 | return msgs 233 | 234 | # start here 235 | if not args: 236 | return _envop (_ENV, False) 237 | 238 | env = OrderedDict () 239 | 240 | for arg in args: 241 | if arg.is_ass: 242 | var = arg.lhs.as_identifier 243 | 244 | if var: 245 | state = bool (sym.ast2spt (arg.rhs)) 246 | 247 | else: 248 | var = arg.as_identifier 249 | 250 | if var: 251 | if var [:2] == 'no': 252 | var, state = var [2:], False 253 | else: 254 | state = True 255 | 256 | if var is None: 257 | raise TypeError (f'invalid argument {sym.ast2nat (arg)!r}') 258 | elif var not in _ENV_OPTS: 259 | raise NameError (f'invalid environment setting {var!r}') 260 | 261 | env [var] = state 262 | 263 | ret = _envop (env, True) 264 | 265 | if vars_updated: 266 | _vars_updated () 267 | 268 | return ret 269 | 270 | def _admin_envreset (*args): 271 | return ['Environment has been reset.'] + _admin_env (*(AST ('@', var if state else f'no{var}') for var, state in _START_ENV.items ())) 272 | 273 | #............................................................................................... 274 | class RealityRedefinitionError (NameError): pass 275 | class CircularReferenceError (RecursionError): pass 276 | class AE35UnitError (Exception): pass 277 | 278 | def _mapback (ast, assvar = None, exclude = set ()): # map back ufuncs and symbols to the variables they are assigned to if possible 279 | if not isinstance (ast, AST): 280 | return ast 281 | 282 | if ast.is_var: 283 | if ast.var not in _SYM_VARS: 284 | return ast 285 | 286 | if ast.var == assvar: 287 | raise CircularReferenceError ('trying to assign unqualified symbol to variable of the same name') 288 | 289 | return AST ('-sym', ast.var) 290 | 291 | if ast.is_sym: 292 | vars = _SYM_MAP.get (ast) 293 | 294 | if not vars: 295 | return ast 296 | 297 | if ast.sym in vars: 298 | return AST ('@', ast.sym) 299 | 300 | return AST ('@', next (iter (vars))) 301 | 302 | if _UFUNC_MAPBACK: 303 | if ast.is_ass and ast.lhs.is_ufunc: 304 | return AST ('=', ast.lhs, _mapback (ast.rhs, assvar, exclude)) 305 | elif not ast.is_ufunc: 306 | return AST (*(_mapback (a, assvar, exclude) for a in ast)) 307 | 308 | vars = _UFUNC_MAP.get (ast) 309 | 310 | if vars: # prevent mapping to self on assignment 311 | if ast.ufunc in vars and ast.ufunc not in exclude: 312 | return AST ('@', ast.ufunc) 313 | 314 | for var in vars: 315 | if var not in exclude: 316 | return AST ('@', var) 317 | 318 | return AST (*(_mapback (a, assvar, exclude) for a in ast)) 319 | 320 | def _present_vars (vars): 321 | asts = [] 322 | 323 | for v, e in vars: 324 | if v != '_': 325 | if e.is_lamb: 326 | asts.append (AST ('=', ('-ufunc', v, tuple (('@', vv) for vv in e.vars)), e.lamb)) 327 | else: 328 | asts.append (AST ('=', ('@', v), e)) 329 | 330 | return asts 331 | 332 | def _sorted_vars (): 333 | return _present_vars (sorted (_VARS.items (), key = lambda kv: (kv [1].op not in {'-lamb', '-ufunc'}, kv [0]))) 334 | 335 | def _vars_updated (): 336 | global _VARS_FLAT 337 | 338 | vars = {v: a if a.is_lamb else AST.apply_vars (a, _VARS, mode = False) for v, a in _VARS.items ()} # flattened vars so sym and sparser don't need to do apply_vars() 339 | one = (f for f in filter (lambda f: _ENV.get (f), _ONE_FUNCS)) # hidden functions for stuff like Gamma 340 | lamb = (va [0] for va in filter (lambda va: va [1].is_lamb, vars.items ())) # user lambda functions 341 | assfunc = (va [0] for va in filter (lambda va: va [1].is_var and va [1].var in AST.Func.PYBASE, vars.items ())) # user variables assigned to concrete functions 342 | funcs = {*one, *lamb, *assfunc} 343 | 344 | sym.set_sym_user_vars (vars) 345 | sym.set_sym_user_funcs (funcs) 346 | sparser.set_sp_user_vars (vars) 347 | sparser.set_sp_user_funcs (funcs) 348 | 349 | _UFUNC_MAP.clear () 350 | _SYM_MAP.clear () 351 | _SYM_VARS.clear () 352 | 353 | _VARS_FLAT = vars 354 | 355 | for v, a in vars.items (): # build ufunc and sym mapback dict 356 | if v != '_': 357 | if a.is_ufunc: 358 | _UFUNC_MAP.setdefault (a, set ()).add (v) 359 | 360 | elif a.is_sym: 361 | _SYM_MAP.setdefault (a, set ()).add (v) 362 | _SYM_VARS.add (v) 363 | 364 | def _prepare_ass (ast): # check and prepare for simple or tuple assignment 365 | if not ast.ass_valid: 366 | vars = None 367 | elif ast.ass_valid.error: 368 | raise RealityRedefinitionError (ast.ass_valid.error) 369 | 370 | else: 371 | vars, ast = ast.ass_valid.lhs, ast.ass_valid.rhs 372 | vars = list (vars.comma) if vars.is_comma else [vars] 373 | 374 | return AST.apply_vars (ast, _VARS_FLAT), vars 375 | 376 | def _execute_ass (ast, vars): # execute assignment if it was detected 377 | def set_vars (vars): 378 | nvars = {} 379 | 380 | for v, a in vars.items (): 381 | v = v.var 382 | 383 | if a.is_ufunc: 384 | if v in sparser.RESERVED_FUNCS: 385 | raise NameError (f'cannot assign undefined function to concrete function name {v!r}') 386 | 387 | if a.is_ufunc_anonymous: 388 | a = AST (a.op, v, *a [2:]) 389 | 390 | elif a.is_sym_anonymous: 391 | if a.is_sym_unqualified: 392 | raise CircularReferenceError ('cannot asign unqualified anonymous symbol') 393 | 394 | a = AST (a.op, v, *a [2:]) 395 | 396 | nvars [v] = a 397 | 398 | try: # check for circular references 399 | AST.apply_vars (AST (',', tuple (('@', v) for v in nvars)), {**_VARS, **nvars}) 400 | except RecursionError: 401 | raise CircularReferenceError ("I'm sorry, Dave. I'm afraid I can't do that.") from None 402 | 403 | _VARS.update (nvars) 404 | 405 | return list (nvars.items ()) 406 | 407 | # start here 408 | if not vars: # no assignment 409 | if not ast.is_ufunc: 410 | ast = _mapback (ast) 411 | 412 | _VARS ['_'] = ast 413 | 414 | _vars_updated () 415 | 416 | return [ast] 417 | 418 | if len (vars) == 1: # simple assignment 419 | if ast.op not in {'-ufunc', '-sym'}: 420 | ast = _mapback (ast, vars [0].var, {vars [0].var}) 421 | 422 | vars = set_vars ({vars [0]: ast}) 423 | 424 | else: # tuple assignment 425 | ast = ast.strip_paren 426 | 427 | if ast.op in {',', '[', '-set'}: 428 | asts = ast [1] 429 | 430 | else: 431 | asts = [] 432 | itr = iter (sym.ast2spt (ast)) 433 | 434 | for i in range (len (vars) + 1): 435 | try: 436 | ast = sym.spt2ast (next (itr)) 437 | except StopIteration: 438 | break 439 | 440 | if vars [i].is_ufunc_named: 441 | asts.append (AST.Ass.ufunc2lamb (vars [i], ast)) 442 | 443 | vars [i] = AST ('@', vars [i].ufunc) 444 | 445 | else: 446 | asts.append (ast) 447 | 448 | if len (vars) < len (asts): 449 | raise ValueError (f'too many values to unpack (expected {len (vars)})') 450 | elif len (vars) > len (asts): 451 | raise ValueError (f'not enough values to unpack (expected {len (vars)}, got {len (asts)})') 452 | 453 | vasts = list (zip (vars, asts)) 454 | exclude = set (va [0].var for va in filter (lambda va: va [1].is_ufunc, vasts)) 455 | asts = [a if a.op in {'-ufunc', '-sym'} else _mapback (a, v.var, exclude) for v, a in vasts] 456 | vars = set_vars (dict (zip (vars, asts))) 457 | 458 | _vars_updated () 459 | 460 | return _present_vars (vars) 461 | 462 | #............................................................................................... 463 | class Handler (SimpleHTTPRequestHandler): 464 | def vars (self, request): 465 | asts = _sorted_vars () 466 | 467 | return {'vars': [{ 468 | 'tex': sym.ast2tex (ast), 469 | 'nat': sym.ast2nat (ast), 470 | 'py' : sym.ast2py (ast), 471 | } for ast in asts]} 472 | 473 | def validate (self, request): 474 | ast, erridx, autocomplete, error = _PARSER.parse (request ['text']) 475 | tex = nat = py = None 476 | 477 | if ast is not None: 478 | tex, xlattex = sym.ast2tex (ast, retxlat = True) 479 | nat, xlatnat = sym.ast2nat (ast, retxlat = True) 480 | py, xlatpy = sym.ast2py (ast, retxlat = True) 481 | 482 | if _SYMPAD_DEBUG: 483 | print ('free:', list (v.var for v in ast.free_vars), file = sys.stderr) 484 | print ('ast: ', ast, file = sys.stderr) 485 | 486 | if xlattex: 487 | print ('astt:', repr (xlattex), file = sys.stderr) 488 | 489 | if xlatnat: 490 | print ('astn:', repr (xlatnat), file = sys.stderr) 491 | 492 | if xlatpy: 493 | print ('astp:', repr (xlatpy), file = sys.stderr) 494 | 495 | print ('tex: ', tex, file = sys.stderr) 496 | print ('nat: ', nat, file = sys.stderr) 497 | print ('py: ', py, file = sys.stderr) 498 | print (file = sys.stderr) 499 | 500 | if isinstance (error, Exception): 501 | error = (f'{error.__class__.__name__}: ' if not isinstance (error, SyntaxError) else '') + error.args [0].replace ('\n', ' ').strip () 502 | 503 | return { 504 | 'tex' : tex, 505 | 'nat' : nat, 506 | 'py' : py, 507 | 'erridx' : erridx, 508 | 'autocomplete': autocomplete, 509 | 'error' : error, 510 | } 511 | 512 | def evaluate (self, request): 513 | def evalexpr (ast): 514 | sym.ast2spt.set_precision (ast) 515 | 516 | if ast.is_func and ast.func in AST.Func.PLOT: # plotting? 517 | args, kw = AST.args2kwargs (AST.apply_vars (ast.args, _VARS), sym.ast2spt) 518 | ret = getattr (splot, ast.func) (*args, **kw) 519 | 520 | return {'msg': ['Plotting not available because matplotlib is not installed.']} if ret is None else {'img': ret} 521 | 522 | elif ast.op in {'@', '-func'} and ast [1] in AST.Func.ADMIN: # special admin function? 523 | asts = globals () [f'_admin_{ast [1]}'] (*(ast.args if ast.is_func else ())) 524 | 525 | if isinstance (asts, str): 526 | return {'msg': [asts]} 527 | elif isinstance (asts, list) and isinstance (asts [0], str): 528 | return {'msg': asts} 529 | 530 | else: # not admin function, normal evaluation 531 | ast, vars = _prepare_ass (ast) 532 | 533 | if _SYMPAD_DEBUG: 534 | print ('ast: ', ast, file = sys.stderr) 535 | 536 | try: 537 | spt, xlat = sym.ast2spt (ast, retxlat = True) # , _VARS) 538 | 539 | if _SYMPAD_DEBUG and xlat: 540 | print ('xlat: ', xlat, file = sys.stderr) 541 | 542 | sptast = sym.spt2ast (spt) 543 | 544 | except: 545 | if _SYMPAD_DEBUG: 546 | print (file = sys.stderr) 547 | 548 | raise 549 | 550 | if _SYMPAD_DEBUG: 551 | try: 552 | print ('spt: ', repr (spt), file = sys.stderr) 553 | except: 554 | pass 555 | 556 | print ('spt type: ', type (spt), file = sys.stderr) 557 | 558 | try: 559 | print ('spt args: ', repr (spt.args), file = sys.stderr) 560 | except: 561 | pass 562 | 563 | print ('spt latex: ', sp.latex (spt), file = sys.stderr) 564 | print ('spt ast: ', sptast, file = sys.stderr) 565 | print ('spt tex: ', sym.ast2tex (sptast), file = sys.stderr) 566 | print ('spt nat: ', sym.ast2nat (sptast), file = sys.stderr) 567 | print ('spt py: ', sym.ast2py (sptast), file = sys.stderr) 568 | print (file = sys.stderr) 569 | 570 | asts = _execute_ass (sptast, vars) 571 | 572 | response = {} 573 | 574 | if asts and asts [0] != AST.None_: 575 | response.update ({'math': [{ 576 | 'tex': sym.ast2tex (ast), 577 | 'nat': sym.ast2nat (ast), 578 | 'py' : sym.ast2py (ast), 579 | } for ast in asts]}) 580 | 581 | return response 582 | 583 | # start here 584 | responses = [] 585 | 586 | try: 587 | _HISTORY.append (request ['text']) 588 | 589 | ast, _, _, _ = _PARSER.parse (request ['text']) 590 | 591 | if ast: 592 | for ast in (ast.scolon if ast.is_scolon else (ast,)): 593 | sys.stdout = _SYS_STDOUT if _SERVER_DEBUG else io.StringIO () 594 | response = evalexpr (ast) 595 | 596 | if sys.stdout.tell (): 597 | responses.append ({'msg': sys.stdout.getvalue ().strip ().split ('\n')}) 598 | 599 | responses.append (response) 600 | 601 | except Exception: 602 | if sys.stdout is not _SYS_STDOUT and sys.stdout.tell (): # flush any printed messages before exception 603 | responses.append ({'msg': sys.stdout.getvalue ().strip ().split ('\n')}) 604 | 605 | etype, exc, tb = sys.exc_info () 606 | 607 | if exc.args and isinstance (exc.args [0], str): 608 | exc = etype (exc.args [0].replace ('\n', ' ').strip (), *exc.args [1:]).with_traceback (tb) # reformat text to remove newlines 609 | 610 | responses.append ({'err': ''.join (traceback.format_exception (etype, exc, tb)).strip ().split ('\n')}) 611 | 612 | finally: 613 | sys.stdout = _SYS_STDOUT 614 | 615 | return {'data': responses} if responses else {} 616 | 617 | def do_GET (self): 618 | if self.path == '/': 619 | self.path = '/index.html' 620 | 621 | fnm = os.path.join (_SYMPAD_PATH, self.path.lstrip ('/')) 622 | 623 | if self.path != '/env.js' and (self.path not in _STATIC_FILES or (not _RUNNING_AS_SINGLE_SCRIPT and not os.path.isfile (fnm))): 624 | self.send_error (404, f'Invalid path {self.path!r}') 625 | 626 | else: 627 | self.send_response (200) 628 | 629 | if self.path == '/env.js': 630 | content = 'text/javascript' 631 | data = f'History = {_HISTORY}\nHistIdx = {len (_HISTORY)}\nVersion = {_VERSION!r}\nSymPyVersion = {sp.__version__!r}\nDisplayStyle = {_DISPLAYSTYLE [0]}'.encode ('utf8') 632 | 633 | self.send_header ('Cache-Control', 'no-store') 634 | 635 | else: 636 | content = _STATIC_FILES [self.path] 637 | 638 | if _RUNNING_AS_SINGLE_SCRIPT: 639 | data = _FILES [self.path [1:]] 640 | else: 641 | data = open (fnm, 'rb').read () 642 | 643 | self.send_header ('Content-type', f'{content}') 644 | self.end_headers () 645 | self.wfile.write (data) 646 | 647 | def do_POST (self): 648 | request = parse_qs (self.rfile.read (int (self.headers ['Content-Length'])).decode ('utf8'), keep_blank_values = True) 649 | 650 | for key, val in list (request.items ()): 651 | if isinstance (val, list) and len (val) == 1: 652 | request [key] = val [0] 653 | 654 | if request ['mode'] == 'vars': 655 | response = self.vars (request) 656 | 657 | else: 658 | if request ['mode'] == 'validate': 659 | response = self.validate (request) 660 | else: # if request ['mode'] == 'evaluate': 661 | response = {**self.evaluate (request), **self.vars (request)} 662 | 663 | response ['idx'] = request ['idx'] 664 | response ['text'] = request ['text'] 665 | 666 | response ['mode'] = request ['mode'] 667 | 668 | self.send_response (200) 669 | self.send_header ('Content-type', 'application/json') 670 | self.send_header ('Cache-Control', 'no-store') 671 | self.end_headers () 672 | self.wfile.write (json.dumps (response).encode ('utf8')) 673 | # self.wfile.write (json.dumps ({**request, **response}).encode ('utf8')) 674 | 675 | #............................................................................................... 676 | def start_server (logging = True): 677 | if not logging: 678 | Handler.log_message = lambda *args, **kwargs: None 679 | 680 | if ('--ugly', '') in __OPTS or ('-u', '') in __OPTS: 681 | _DISPLAYSTYLE [0] = 0 682 | 683 | for opt, _ in __OPTS: 684 | opt = opt.lstrip ('-') 685 | 686 | if opt in _ENV_OPTS_ALL: 687 | _admin_env (AST ('@', opt)) 688 | 689 | _START_ENV.update (_ENV) 690 | _vars_updated () 691 | 692 | if not __ARGV: 693 | host, port = _DEFAULT_ADDRESS 694 | else: 695 | host, port = (re.split (r'(?<=\]):' if __ARGV [0].startswith ('[') else ':', __ARGV [0]) + [_DEFAULT_ADDRESS [1]]) [:2] 696 | host, port = host.strip ('[]'), int (port) 697 | 698 | try: 699 | httpd = HTTPServer ((host, port), Handler) 700 | thread = threading.Thread (target = httpd.serve_forever, daemon = True) 701 | 702 | thread.start () 703 | 704 | return httpd 705 | 706 | except OSError as e: 707 | if e.errno != 98: 708 | raise 709 | 710 | print (f'Port {port} seems to be in use, try specifying different port as a command line parameter, e.g. localhost:9001') 711 | 712 | sys.exit (-1) 713 | 714 | _MONTH_NAME = (None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec') 715 | 716 | def child (): 717 | def log_message (msg): 718 | y, m, d, hh, mm, ss, _, _, _ = time.localtime (time.time ()) 719 | 720 | sys.stderr.write (f'{httpd.server_address [0]} - - ' \ 721 | f'[{"%02d/%3s/%04d %02d:%02d:%02d" % (d, _MONTH_NAME [m], y, hh, mm, ss)}] {msg}\n') 722 | 723 | # start here 724 | httpd = start_server () 725 | 726 | if _SYMPAD_FIRSTRUN and ('--nobrowser', '') not in __OPTS and ('-n', '') not in __OPTS: 727 | webbrowser.open (f'http://{httpd.server_address [0] if httpd.server_address [0] != "0.0.0.0" else "127.0.0.1"}:{httpd.server_address [1]}') 728 | 729 | if _SYMPAD_FIRSTRUN: 730 | print (f'SymPad v{_VERSION} server running. If a browser window does not automatically open to the address below then try navigating to that URL manually.\n') 731 | 732 | log_message (f'Serving at http://{httpd.server_address [0]}:{httpd.server_address [1]}/') 733 | 734 | if not _SYMPAD_RESTART: 735 | try: 736 | while 1: 737 | time.sleep (0.5) # thread.join () doesn't catch KeyboardInterupt on Windows 738 | 739 | except KeyboardInterrupt: 740 | sys.exit (0) 741 | 742 | else: 743 | fnms = (_SYMPAD_NAME,) if _RUNNING_AS_SINGLE_SCRIPT else (_SYMPAD_NAME, 'splot.py', 'spatch.py', 'sparser.py', 'sym.py', 'sxlat.py', 'sast.py', 'lalr1.py') 744 | watch = [os.path.join (_SYMPAD_PATH, fnm) for fnm in fnms] 745 | tstamps = [os.stat (fnm).st_mtime for fnm in watch] 746 | 747 | try: 748 | while 1: 749 | time.sleep (0.5) 750 | 751 | if [os.stat (fnm).st_mtime for fnm in watch] != tstamps: 752 | log_message ('Files changed, restarting...') 753 | sys.exit (0) 754 | 755 | except KeyboardInterrupt: 756 | sys.exit (0) 757 | 758 | sys.exit (-1) 759 | 760 | def parent (): 761 | if not _SYMPAD_RESTART or __IS_MODULE_RUN: 762 | child () # does not return 763 | 764 | # continue as parent process and wait for child process to return due to file changes and restart it 765 | base = [sys.executable] + sys.argv [:1] + ['--child'] # (['--child'] if __IS_MAIN else ['sympad', '--child']) 766 | opts = [o [0] for o in __OPTS] 767 | first_run = ['--firstrun'] 768 | 769 | try: 770 | while 1: 771 | ret = subprocess.run (base + opts + first_run + __ARGV) 772 | first_run = [] 773 | 774 | if ret.returncode != 0 and not _SYMPAD_DEBUG: 775 | sys.exit (0) 776 | 777 | except KeyboardInterrupt: 778 | sys.exit (0) 779 | 780 | #............................................................................................... 781 | # AUTO_REMOVE_IN_SINGLE_SCRIPT_BLOCK_START 782 | if _SERVER_DEBUG: # DEBUG! 783 | Handler.__init__ = lambda self: None 784 | 785 | h = Handler () 786 | 787 | # _VARS ['_'] = AST ('[', (('=', ('-ufunc', 'x', (('@', 't'),)), ('*', (('+', (('@', 'C1'), ('*', (('#', '8'), ('@', 'C2'), ('-intg', ('/', ('^', ('@', 'e'), ('/', ('*', (('#', '19'), ('^', ('@', 't'), ('#', '2')))), ('#', '2'))), ('^', ('-ufunc', 'x0', (('@', 't'),)), ('#', '2'))), ('@', 'dt')))))), ('-ufunc', 'x0', (('@', 't'),))))), ('=', ('-ufunc', 'y', (('@', 't'),)), ('+', (('*', (('@', 'C1'), ('-ufunc', 'y0', (('@', 't'),)))), ('*', (('@', 'C2'), ('+', (('/', ('^', ('@', 'e'), ('/', ('*', (('#', '19'), ('^', ('@', 't'), ('#', '2')))), ('#', '2'))), ('-ufunc', 'x0', (('@', 't'),))), ('*', (('#', '8'), ('-intg', ('/', ('^', ('@', 'e'), ('/', ('*', (('#', '19'), ('^', ('@', 't'), ('#', '2')))), ('#', '2'))), ('^', ('-ufunc', 'x0', (('@', 't'),)), ('#', '2'))), ('@', 'dt')), ('-ufunc', 'y0', (('@', 't'),))), {2})))))))))) 788 | _VARS ['_'] = AST.Zero 789 | 790 | # print (h.validate ({'text': r'f = g'})) 791 | print (h.evaluate ({'text': r'sin = ?(x)'})) 792 | print (h.evaluate ({'text': r'sin (2)'})) 793 | 794 | sys.exit (0) 795 | # AUTO_REMOVE_IN_SINGLE_SCRIPT_BLOCK_END 796 | 797 | def main (): 798 | if ('--help', '') in __OPTS or ('-h', '') in __OPTS: 799 | print (_HELP) 800 | sys.exit (0) 801 | 802 | if ('--version', '') in __OPTS or ('-v', '') in __OPTS: 803 | print (_VERSION) 804 | sys.exit (0) 805 | 806 | if ('--debug', '') in __OPTS or ('-d', '') in __OPTS: 807 | _SYMPAD_DEBUG = os.environ ['SYMPAD_DEBUG'] = '1' 808 | 809 | if _SYMPAD_CHILD: 810 | child () 811 | else: 812 | parent () 813 | 814 | if __IS_MAIN: 815 | main () 816 | -------------------------------------------------------------------------------- /test_sym.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # python 3.6+ 3 | 4 | # Randomized CONSISTENCY testing of parsing vs. writing: text -> ast -> tex/nat/py -> ast -> tex/nat/py 5 | 6 | from getopt import getopt 7 | from random import random, randint, randrange, choice 8 | import math 9 | import string 10 | import sys 11 | import time 12 | 13 | from sast import AST 14 | import sast 15 | import spatch 16 | import sxlat 17 | import sym 18 | import sparser 19 | 20 | _STATIC_TERMS = [ 21 | '0', 22 | '1', 23 | '-1', 24 | '1.0', 25 | '-1.0', 26 | '.1', 27 | '-.1', 28 | '1.', 29 | '2', 30 | '1e-100', 31 | '1e100', 32 | '1e+100', 33 | 'a', 34 | 'b', 35 | 'c', 36 | 'x' 37 | 'y' 38 | 'z' 39 | 'd', 40 | 'dx', 41 | 'dy', 42 | 'dz', 43 | 'x0', 44 | 'y1', 45 | 'z20', 46 | 'w_{1}', 47 | 'partial', 48 | 'partialx', 49 | '\\partial ', 50 | '\\partialx', 51 | '\\partial x', 52 | '\\partialy', 53 | 'oo', 54 | '\\infty ' 55 | 'zoo', 56 | '\\tilde\\infty ', 57 | "'s'", 58 | '"s"', 59 | 'None', 60 | 'True', 61 | 'False', 62 | '\\emptyset', 63 | ] 64 | 65 | # previously problematic static test expressions 66 | 67 | _EXPRESSIONS = r""" 68 | \sqrt[{{1} / {1.0}}]{({oo},{partial})} 69 | sqrt{{-1.0}**{0}} 70 | {{\frac{1.0}{dx}} \cdot {{partial} / {partialx}} \cdot {{d} >= {oo}}} 71 | \frac{{partial}**{1}}{{{partialx}*{dx}*{1.0}}} 72 | {{\frac{1.0}{partialx}} \cdot {\exp({0},{a})} \cdot {{{d}+{oo}}}} 73 | {\arcsin({-1.0},{dx},{oo})}^{{d} <= {-1}} 74 | ({{d}**{1}},{\arcsech({partial},{partial})}) 75 | Limit ({d} > {-1.0}, x, {{1.0}*{partial}*{dx}}) 76 | {{d}^{1}} / {{{dx} {oo}}} 77 | {{{d}*{1}}} / {partial^{5} / partialy^{1} partialy^{2} partialz^{2} {oo}} 78 | {{{0}!} \cdot {partial^{1} / partialx^{1} {dx}} \cdot {{d}**{d}}} 79 | {{partial^{4} / partialy^{3} partialy^{1} {a}} \cdot {{'s'}^{d}}} 80 | {\int {-1} dx} / {\int {1} dx} 81 | {\int_{dx}^{a} {-1} dx}! 82 | \int {partial^{3} / partialy^{3} {a}} dx 83 | {{\int {partial} dx} {partial^{4} / partialy^{1} partialz^{1} partialz^{2} {a}}} 84 | \int_{[{-1.0}]}^{\int {partialx} dx} {{{oo}+{-1}}} dx 85 | \int_{partial^{6} / partialy^{2} partialx^{2} partialz^{2} {partialx}}^{partial^{4} / partialz^{1} partialz^{2} partialx^{1} {0}} {{a} != {'s'}} dx 86 | {{{oo}**{'s'}}+{\int {oo} dx}+{partial^{7} / partialz^{3} partialx^{2} partialx^{2} {0}}} 87 | [{{{-1} \cdot {oo}}},{{{dx},{1.0},{oo}}},{partial^{8} / partialx^{3} partialx^{2} partialz^{3} {oo}}] 88 | {{lambda x, y, z: {1}}+{{1.0} > {1.0}}+{{oo} / {'s'}}} 89 | {{lambda: {-1}} \cdot {\frac{partialx}{oo}} \cdot {{1.0} if {1} else {a} if {0}}} 90 | {{{a} / {-1}} {\lim_{x \to partial} {-1}} * [lambda x, y, z: {partialx}]} 91 | \int_{\sqrt[{a}]{1.0}}^{[]} {lambda x: {partialx}} dx 92 | lambda x: {{dx} = {dx}} 93 | \int {{{{a} / {dx}} {partial^{2} / partialz^{2} {partialx}}}} dx 94 | \int \frac{d}{dx} x dx 95 | \int d / dx x dx 96 | \int_{{partial^{4} / partialx^{1} partialy^{3} {partial}}**{\sqrt[{oo}]{0}}}^{{{{-1} == {0}}*{({partial},{'s'},{a})}*{{1} / {1}}}} {-{partial^{6} / partialy^{3} partialx^{3} {0}}} dx 97 | \int {-{partial^{6} / partialy^{3} partialx^{3} {0}}} dx 98 | \lim_{x \to \frac{lambda x, y, z: {-{0}}}{partial^{5} / partialz^{2} partialz^{1} partialx^{2} {Limit (a, x, 1)}}} {\arctan()} 99 | -{{{{{{partialx},{partial},{oo},},{{dx},{-1.0},{a},},}}**{StrictGreaterThan({1.0})}} > {partial^{4} / partialz^{1} partialx^{2} partialy^{1} {{1.0}^{1}}}} 100 | -{{{{{\sum_{x = 0}^{-1.0} {oo}} \cdot {({0})}}},}} 101 | \int {{{{d}+{partialx}+{1}}} if {lambda x, y, z: {a}} else {{1} / {partialx}}} dx 102 | {|{\log_{partial^{1} / partialy^{1} {{{0}*{'s'}}}}{[{{-1.0} / {'s'}}]}}|} 103 | {\lim_{x \to -1.0} {dx}} > {{oo} if {-1.0} else {d} if {d} else {1}} 104 | \frac{{-1.0} > {oo}}{\ln{-1.0}} 105 | {{|{d}|}{{({1.0},{1})},{[{oo}]},},} 106 | 1/2 * {a+b} [lambda: {d}] 107 | {{{'s'} < {1.0}} \cdot {({a})} \cdot {{1} if {a}}} 108 | -{1.0 if partial else d if 1 else oo if 1.0 else 's'} 109 | {partial^{5} / partialy^{2} partialy^{2} partialy^{1} {partial}}^{{-1.0} > {d}} 110 | {lambda x: {a}} if {{{'s'}*{a}*{1}}} 111 | \int_{{-1.0} <= {1}}^{-{1}} {{-1.0} <= {1.0}} dx 112 | {{({a1.0})}+{{a}!}+{{d} if {1} else {dx}}} 113 | \int_{{{a}+{a}+{0}}}^{{'s'} / {a}} {\int {1} dx} dx 114 | lambda x: {lambda x, y: {oo}} 115 | \sqrt[3]{({oo},{a})} 116 | Limit (\sum_{x = oo}^{partial} {-1.0}, x, \sec({-1.0},{-1},{partialx})) 117 | {{a} = {partial}} if {{{oo}+{0}+{-1}}} else {\int {a} dx} 118 | \sum_{x = {{1}*{d}*{oo}}}^{\exp({a},{1})} {\log_{1.0}{a}} 119 | lambda x: {{a} = {dx}} 120 | {{{d}^{oo}}*{{a}^{d}}} 121 | {{oo} if {oo}} = {is_mersenne_prime({'s'})} 122 | \lim_{x \to 0} {sqrt(dx) + [lambda x, y: -1.0]} 123 | {{\frac{\int_{a}^{1} {dx} dx}{{{oo} \cdot {d} \cdot {dx}}}}} 124 | # \int d/dx dx 125 | (((-1)**partial)**({a_prime, oo, 's'}))**-{-{0}} 126 | Limit ({{{0}^{'s'}} {\left|{a}\right|} {({a},{a})}}, x, lambda x: {{1}!}) 127 | \left(\left(\text{'s'} \right)! \le \left(\left(x, y \right) \mapsto -1.0 \right) \right) == \int_{\left[-1.0, \partial, -1 \right]}^{\log_{-1.0}\left(-1 \right)} \begin{cases} 1 & \text{for}\: \infty \\ 1.0 & \text{for}\: 1.0 \end{cases} \ dx 128 | x^{-{{1} / {1.0}}} 129 | cofactors( 1 , {lambda x: 1 = lambda: 2} ) 130 | ({{{-{cse()}},{{{{partial} != {-1}}*{{{-1.0} {1.0}}}}},{lambda: {{-1.0} == {dx}}},},{{\lim_{x \to \log_{0}{d}} {[{-1.0}]}},{partial^{7} / partialx^{3} partialy^{1} partialx^{3} {{partialx} if {a} else {-1.0} if {a} else {d} if {1.0} else {partialx}}},{{lambda x, y, z: {oo}} = {\tanh()}},},{{partial^{3} / partialz^{3} {{oo} / {'s'}}},{({{{\left|{dx}\right|},{{a} if {d}},},{{-{oo}},{({{-1.0},{oo},{-1.0},})},},})},{partial^{5} / partialx^{1} partialy^{1} partialz^{3} {{-1}!}},},}) 131 | {\left|{a}\right|} if {\int {'s'} dx} else {({-1},{-1},{a})} if {\left|{1.0}\right|} 132 | {lambda x: {{1.0} if {oo} else {1.0} if {oo}}} = {{{{partial} \cdot {partialx}}}**{{a}!}} 133 | {Sum (\int {1} dx, (x, 0, 1))} dx 134 | {{\sum_{x = \left|{0}\right|}^{\tan({-1.0})} {\int_{partialx}^{oo} {d} dx}}+{{{\lim_{x \to 1} {d}} \cdot {{{a}+{-1}+{dx}}}}}+{{{{a} = {a}}+{({dx0d})}+{{{dx}*{dx}*{a}}}}}} 135 | log(partialx*'s'*partialx) / log(Derivative(a, z, 3, y, 2)) 136 | dpartial 137 | a, lambda: b = 1 138 | \exp({a},{-1},{1}) 139 | x, y = lambda: 1, lambda: 2 140 | doo 141 | Sum(a*Integral(x, x), (x, 0, 1)) + 1*dx 142 | (dx**p*artial)*Limit(sqrt(-1), x, 0**d)[(Matrix([[partialx]])), lcm_list()] 143 | ln((a)**b) 144 | a * \int dx + {\int dx dx} 145 | 1 if {a = x if z} else 0 if y 146 | a, lambda: b = 1 147 | a * [2] 148 | sqrt(1, 2) 149 | x*[][y] 150 | lambda: x: 151 | a*[x][y][z] 152 | a*()**2 153 | a*().t 154 | a*()[2] 155 | lambda*x:2 156 | lambda*x, y:2 157 | d**2e0/dx**2e0 x**3 158 | y**z [w] 159 | {y**z} [w] 160 | x {y**z} [w] 161 | {x y**z} [w] 162 | \sqrt[{lambda x, y, z: {ConditionSet()}}]{x} 163 | {1:2:3}[2] 164 | {1:2:3}.x 165 | None**-1.0**\[[\emptyset,],[0,],[\partial x,],] / {not \[None,\emptyset,]} 166 | \int_{\lim_{x \to 1} oo^{not 1e100}}^\{{partialx+dx},{\partialx*.1},partialx!} \log_{\left|partialx\right|}{1 \cdot False} dx 167 | {{\[[{{\emptyset} = {.1}},{\[[{\emptyset},],[{"s"},],]},],]} if {-{{\partial x}!}} else {{{{False}!} and {{{\partial x}||{oo}||{"s"}}}}}} 168 | {\int {{{{{1e-100} {1} {partialx}}}*{{True}^{\tilde\infty }}}} dx} 169 | {{{{-{"s"}} : {lambda x, y: {\partialx}}} \cdot {{not {{'s'} : {1.} : {.1}}}}}} 170 | {-{-1}}^{{1} : {\partial x} : {0}} 171 | {{{\sum_{x = {{a} : {"s"} : {True}}}^{({\partial x})} {[]}}||{{{1.0} : {False} : {\emptyset}} [{{-1} == {\partialx}}]}||{{{{oo} if {None} else {\partialx}}^^{{.1} [{oo}]}}}}} 172 | {lambda x, y, z: {lambda x, y: {{{-1.0}&&{False}&&{d}}}}} 173 | \int {{\partialx} : {d} : {1.0}} dx 174 | {\lim_{x \to {{1} : {1e+100} : {.1}}} {({\partial x},{\partialx})}} 175 | x + {-1 2} 176 | x + {-1 * 2} 177 | x - {{1 2} 3} 178 | x - {{1 * 2} * 3} 179 | {sqrt{{{{not {1.}}}+{\int_{a}^{-1.0} {s} dx}+{{{-1} \cdot {1e100} \cdot {\infty zoo}}}}}} 180 | x - a b! 181 | \int x * \frac{y}{z} \ dx 182 | 1+{{-1 * 2}+1} 183 | -1 * a 184 | x - y! () 185 | -x * a! 186 | a * {-b} * c 187 | a * {-b} c 188 | --1 * x 189 | ---1 * x 190 | a**{-1 [y]} 191 | -{\int x dx} + y * dz 192 | {z = x <= y} in [1, 2] 193 | \int_a^b {d != c} dx 194 | \int_a^b {d = c} dx 195 | {a in b} not in c 196 | a*()! 197 | \frac12.and () 198 | lambda: a or lambda: b 199 | {{a in b} * y} in z 200 | \[] 201 | \[[]] 202 | \[[], []] 203 | \{a:b} 204 | {-x} y / z 205 | d / dz {-1} a 206 | 1 / {-2} x 207 | \sum_{x=0}^b {-x} y 208 | \lim_{x\to0} {-x} y 209 | \int a / -1 dx 210 | \[[[x]]] 211 | \[[[1, 2]], [[3]]] 212 | \sqrt(a:b) 213 | \sqrt[3](a:b) 214 | {z : v,c : z,0 : u = {lambda x, y: a}} 215 | a.inverse_mellin_transform() 216 | a**b.c {x * y}! 217 | \int x / --1 dx 218 | \lim_{x \to a = {lambda: c}} b 219 | ?f (x, y, real = True) 220 | Function ('f', real = True) (x, y) 221 | a [b]' 222 | a.b ()' 223 | {x/y}' 224 | 1'['ac'] 225 | |x|' 226 | | 's'|' 227 | {x**y}' 228 | {{-1}'} 229 | {a [b]}'' 230 | 1.''' 231 | 2y - 3/2 * x 232 | 2y + -3/2 * x 233 | 2y - -3/2 * x 234 | 2y + {-3/2} * x 235 | 2y + {-3/2 * x} 236 | x - y z 237 | x + -y z 238 | x - -y z 239 | x + {-y} z 240 | x - {-y} z 241 | x + {-y z} 242 | x - {-y z} 243 | 1 / -2 x 244 | -1''' {d/dx x} 245 | x + -{1 + -1} 246 | x + -1' 247 | 1 * -1' 248 | x * [y]' 249 | x * [y].a 250 | x!' + ('s') 251 | |x|' + ('s') 252 | {x^y'}' 253 | sin{x}! 254 | sin{x}' 255 | \sqrt{-\partial x d^{5} / dx^{2} dy^{3} "s" \{0}}' 256 | \int a b - 1 dx 257 | \int {a b - 1} dx 258 | a * [b]!' 259 | {\sum_{x=y}^z x} / -{d/dx x} 260 | Sum (x, (x, y, z)) / -{a/b} 261 | {-a / z}' 262 | a * [b]' [c] 263 | a * [a]!' [b] 264 | a * [a]! [b] 265 | a * [a].a [b] 266 | a * [a].a' [b] 267 | a * [a].a!' [b] 268 | False * ()' 269 | -{1!} 270 | -{1'} 271 | -{1 [b]} 272 | -{1 [b] [c]} 273 | -{a [b]} 274 | -{a [b] [c]} 275 | {x in y} {1 : 2 : 3} 276 | x^{-{a and b}} 277 | x^{-{a or b}} 278 | x^{-{a || b}} 279 | x^{-{a && b}} 280 | x^{-{a ^^ b}} 281 | {x if 2 else z} b^c 282 | x^{a = b} 283 | {{\sqrt[{?(x, y, reals = False, commutative = False)}]{{.1} = {\emptyset}}} \cdot {{{partialx}||{oo}} {{dy}||{'s'}}} \cdot {{Derivative ({dx}, x, 1)} \cdot {{dy}^^{1.}^^{dx}} \cdot {Limit ({dy}, x, {None})}}} 284 | {\frac{\sqrt{[{.1},{\partial },{1e100}]}}{{{\partialy} / {b}} {{\partialx}+{\partialx}} {{-1}**{True}}}} 285 | {\frac{{not {1e-100}} {{a}**{False}}}{{{partial}||{True}||{1.0}}&&{{b} / {a}}&&{{\partial x}!}}} 286 | 1 / {a in b} 287 | {a * -1} {lambda: 2} 288 | \frac{d\partial x}{dx} 289 | partial / partialx \partial x 290 | -{{1 [2]} c} 291 | {{{?h(x, y, z)},{{{partialx}'''}^^{{1e100} or {1}}^^{{}}},{log{lambda x, y: {1.0}}}}} 292 | sin (x) {a b / c} 293 | {{{{-1.0}**{a}}^{{\partialy} [{c}, {partial}]}}*{{\sqrt{\tilde\infty }}*{\log_{'s'}{1.}}*{-{dz}}}} 294 | Derivative ({partial}, x, 1) 295 | Derivative ({\partial}, x, 1) 296 | Derivative ({\partial x}, x, 1) 297 | None {x = y} 298 | {d / y} * a 299 | {{-1.0} = {1.}} and {{True}+{False}} and {{\infty zoo} = {-1.0}} 300 | a * \log_{2}{a} [x] 301 | {a = b} * c^d 302 | {lambda x: 1}**{-{x in b}} 303 | {\[[{{{oo} : {\tilde\infty }} not in {Limit ({c}, x, {a})}},{\[{{\tilde\infty }||{\infty zoo}},]},],[{acoth()},{{{1} if {False} else {2} if {\partialy} else {0} if {-1.0}} \cdot {{xyzd}&&{1.0}&&{b}} \cdot {not {-1}}},],[{{{\partialx} if {"s"} else {0} if {\partialx} else {partial} if {1e100}}*{{xyzd}*{partial}}*{\int {False} dx}},{\int_{{2} [{\partialx}]}^{{"s"} and {1.} and {oo}} {[]} dx},],]} 304 | {\int_{Derivative ({\[{0},{\emptyset},]}, z, 2, z, 2)}^{not {lambda: {-1.0}}} {{{dx} or {1}}**{{2} not in {None}}} dx} 305 | {\{{{{1.} in {a}} {{{1e-100}}} {{a} = {-1.0}}},{{besselk({a},{\partialy},{1e-100})}''},{{Limit ({dx}, x, {False})} {\frac{1e-100}{.1}}}}} 306 | {\int_{{{-1.0}''}||{\int_{None}^{.1} {dz} dx}||{{\tilde\infty }+{None}}}^{{\lim_{x \to {oo}} {\partial }}**{{1.0}**{1e+100}}} {{-{-1}}^{{1.} == {\partialx} == {\emptyset} < {dx}}} dx} 307 | {{?(x, y)} = {{\[{1e-100},]}||{{\tilde\infty }^{'s'}}}} 308 | {{{{-1}^^{c}} [{{1e+100}+{1e+100}}, {{True}**{0}}]}**{-{not {1e-100}}}} 309 | {{\gcd({\sum_{x = {-1.0}}^{\partial x} {\emptyset}})}**{-{{False}+{2}}}} 310 | {{{d^{6} / dx^{3} dy^{3} {'s'}}+{{False} {dz}}}**{-{{\partial x} = {\partial }}}} 311 | {\sqrt[{-{\log_{partialx}{1e+100}}}]{{{.1} if {1e+100}}*{{b} \cdot {b}}}} 312 | sqrt[log_2{x}]2 313 | {{{?f()}**{{"s"} = {1e+100}}} = {{-1.0 : {Derivative ({1e100}, z, 1, x, 1, x, 2)},oo : {{}},1e-100 : {{1e100}^{\tilde\infty }}}}} 314 | {{LeviCivita({?h(x, y, reals = False, commutative = False)},{{{partial},{\partial }}})}**{{Limit ({\emptyset}, x, {b})}+{{1.0}!}+{{"s"}'}}} 315 | {partialx : {\partial x : \emptyset,-1 : 1e-100},\partial : (oo,False)} : \lim_{x \to partialx = \emptyset} lambda x, y, z: "s" : \{} 316 | {{-{{b} [{\tilde\infty }, {dx}]}}**{-{lambda x, y, z: {\partialy}}}} 317 | {{\min({{None}*{0}},{{True : {1e100},0 : {None},\partial : {2}}})}^{-{{b} : {.1} : {partialx}}}} 318 | a in {-{b in c}} 319 | -{{1'}!} 320 | \ln(((a))) 321 | \sqrt(((a))) 322 | \ln{({(a, b, c)})} 323 | Limit(x:1, a, b) 324 | {-\partialx} / \partialy 325 | Sum (x, (x, a, a : b)) 326 | -{Derivative (x, x) {a in b}} 327 | \int dx dx / dx 328 | b = dx [?h(x, y)]^lambda x, y, z: True! 329 | dy / dx / 2 330 | Sum ({2 \cdot {1 x} \cdot {\int_y^x {dy} dx}}, (x, 0, 1)) * 1 331 | 1 if True else 3 if True else 4 332 | 1 if True else 3 if True 333 | 1 if True else 3 334 | 1 if True 335 | # |x, y| 336 | # |lambda: 1| 337 | # |lambda x: 1| 338 | # |lambda x, y: 1| 339 | x:None 340 | 1 and {-{a * b} + 2} 341 | a in -(1) 342 | :c: 343 | x:: 344 | a {b : c : None} 345 | \sqrt[-{2}]{a} 346 | \int_0^1 {x:y:None} dx 347 | a : b : (None) 348 | log\left|None+xyzd\right| - (1e+100) 349 | Limit (1, x, 1) || a in x if True 350 | not lambda x, y, z: partialx! or -ln1.or lambda x: .1' or [Sum (1e+100, (x, 1, \infty zoo))&&\int 1e100 dx] 351 | -v1.or lambda: 1 352 | \sum_{x = a, b}^n 1 353 | 1+1. 1. [None]**2 354 | 0 1\left[x \right]**2 355 | lambda x, y, z: ln lambda x: None 356 | \int \gamma dx 357 | gamma * x 358 | x^{gamma} y 359 | {d/dx y}.a 360 | {y'}.a 361 | a.b\_c 362 | {a**b}.c 363 | {a!}.b 364 | a.b c.d 365 | {\log_2 b}.c 366 | a * \log_2 b 367 | {\lambda: x} 368 | {-\lambda: x} 369 | {a = \lambda: x} 370 | {a != \lambda: x} 371 | {a, \lambda: x} 372 | {a - \lambda: x} 373 | {a + \lambda: x} 374 | {a * \lambda: x} 375 | {a / \lambda: x} 376 | {a ^ \lambda: x} 377 | {a || \lambda: x} 378 | {a ^^ \lambda: x} 379 | {a && \lambda: x} 380 | {a or \lambda: x} 381 | {a and \lambda: x} 382 | {not \lambda: x} 383 | N lambda: x 384 | \int {2**gamma} dx 385 | \ln\partialx[.1,z20,\Omega]/"s"!||z20>=oo>2.924745719942591e-14||2.B1Cxzr().sUCb()/{None:lambdax,y,z:(10900247533345.432:dy:),\tilde\infty:False+x0&&\int"s"dx,1:\{}/\partial**b} 386 | sqrt\[Lambda[dx,0,b][:\lambda:1e-100,\alpha1,\{}],] 387 | None:1:,c:a 388 | -a.b{1:None,w:b,a:c}! 389 | \sqrt[a]\sqrt a [x] 390 | \sqrt[x]\{}**1[-1] 391 | \sqrt[a](:)[b]**c 392 | \left|a\right|**-1.00[a]**b 393 | a**\sqrt[b]-1e+1[c] 394 | |a|**[a][b].c 395 | sin(b)tan(a)**1[c].d 396 | {b,c}**2[d].a() 397 | sin(a)^h(x)*sin() 398 | \{}**'s'[b].c[d] 399 | sin(a)^2 sin(c) 400 | 1 a**f(x) 401 | a**?f(x) 402 | a**?f(x).a 403 | a**?f(x)[0] 404 | f({x})' 405 | -f({x})' 406 | a^\frac{partialx}\partialx 407 | a^\lambda*lambdax:1 408 | x**?f(x,y).a^1 409 | (LambertW(5.194664222299675e-09[1e100]=-4.904486369506518e-17*\lambda*a,lambdax,y,z:\emptyset''')) 410 | x**?g(x)**x 411 | a**?f(x)^a' 412 | a**?f(x)^b^c 413 | a**?f(x)' 414 | a / c \int dx * d/dx a 415 | d/dx a \lambda: 416 | f(d/dx 1,x) 417 | f(ln(2)) 418 | \sum_{x=0}^1 0.f()\int0dD + 1 419 | a:b^\Lambda(True,1) 420 | a**-\sqrt[b]1[c] 421 | notassoc_legendre(Pi_{44},-1.0),z20=phi,1e+100*1e100*theta*variations() 422 | a = {::b}, c 423 | \partialx / \partial \partial 424 | dx / dd 425 | partial\theta 426 | \.\.a|_{b=c}|_{d=e} 427 | a**\.b[c]|_{x=1} 428 | {d / dx (f(x))(0)} [1] 429 | a*d/dx(h(x))(0) 430 | \. {\. a |_{x = 1}} |_{c = d} 431 | FiniteSet()**1[b].c 432 | ln**2 lambda: 1 433 | sin(v)**[a][b].c 434 | a * d / dx (f(x,y))(0,1).c 435 | a * d / dx (h(x))(0)'' 436 | a.b(((c))) 437 | a[((()))] 438 | a[(:)] 439 | \[a]**b[c][d].e 440 | \.x|_{x=(:)} 441 | \.x|_{x=(sin x)} 442 | \.x|_{(x)=sin x} 443 | \.x|_{(1,2)=y} 444 | \.x|_{(((1,2),))=y} 445 | \.x|_{(((1,2)),)=y} 446 | inverse_mell in_transform(()) 447 | 1 e100a**2 448 | ?(),w_{1}=\psi*\sum_{x=1e+100}^partia lxNone/$ZDv()*oo\cdot"s"\cdot.1orTrue,c,dy\cdot{{{1e-100notin1.0,\sum_{x=1}^5530110.904839005c}}} 449 | \. \int x dx |_{x=1} 450 | a**{d/dx(f(x))(0)} 451 | \int {\lim_{x \to 1} x} dy 452 | \int {\sum_{x = 1}^2 x} dy 453 | {d/dx (u(x))(0)}.a 454 | x / {d / dx (f(x))(0)} 455 | \int {dy}+{-1} dx 456 | {{d/dx (f(real = True)(x))(0)}'} [a] 457 | {\int_{Derivative ({\[{0},{\emptyset},]}, z, 2, z, 2)}^{not {lambda: {-1.0}}} {{{dx} or {1}}**{{2} not in {None}}} dx} 458 | \int {dy**a}**c {dz} dx 459 | \frac{a\int x dx}b 460 | a**b[1]**(1/3)**c 461 | {?(x)'(0)}' 462 | \int {\frac{1}{a \lim_{x\to2} y}} dx 463 | \int {dz < 3} dx 464 | a**{-{d/dx (g(x))(0)}} 465 | partialx/\partialy(x,real=True)(0) 466 | {a \int -1 dx} / 2 467 | {a / b \int x dx} c 468 | {\sqrt{lambda: 1}}.a{\sqrt{lambda: 1}}.a 469 | 1 / {{d/dx (g(x))(0)} a} 470 | Function('f', positive = True)(x, real = True) 471 | \left. x \right|_{{f(x, commutative = True)} = 1} 472 | {a \int x dx / c}*b 473 | {( {\frac{ { { \tilde\infty } or { a } or { c } }+{ d^{5} / dz^{2} dz^{1} dy^{2} { b } } }{ -{ not { \lambda } } } } : : )} 474 | a / { -{d/dx (?f(x))(0)}} 475 | {\int x dx a + b * c + d} 476 | {\int x dx * a + b * c + d} 477 | \int^{a dx b} x dx 478 | \int {d**2 / dx dx (f(0))} dx 479 | \int {d**2 / dy dx (?f(x, y))(0, 1)} dx 480 | {\int { d^{1} / dz^{1} ({d**3 / dx dx dx g(commutative = True)(x, y)(0, 1)}) } dZ } 481 | \int {{d / dy dy dx a} [dz]} dx 482 | \frac{a}{b}*{{{{xx}'}^c}!} 483 | \int a**N dx 484 | { : {\int { { {\[{ 's' },{ dy },{ \beta },]} \cdot { { -9.712711016258549e-12 } { Gamma } { -1.0 } } \cdot { { 0 } && { 6.222789060821971e-22 } } }*{ d^{4} / dz^{1} dz^{2} dy^{1} {\sum_{x = { x0 }}^{ .1 } { 2.040706058303616e-14 } } } } dz } } 485 | sin (a b = c) 486 | x.y (a b = c) 487 | \. a, b |_{x = 1} 488 | ln(1).or lambda: 1 489 | \$()*{a**b} 490 | """.strip ().split ('\n') 491 | 492 | _LETTERS = string.ascii_letters 493 | _LETTERS_NUMBERS = _LETTERS + '_' + string.digits 494 | 495 | def _randidentifier (): 496 | while 1: 497 | s = f'{choice (_LETTERS)}{"".join (choice (_LETTERS_NUMBERS) for _ in range (randint (0, 6)))}{choice (_LETTERS)}' 498 | 499 | if not (s in sparser.RESERVED_ALL or s [:2] == 'd_' or s [:8] == 'partial_' or (s [:1] == 'd' and s [1:] in sparser.RESERVED_ALL) or (s [:7] == 'partial' and s [7:] in sparser.RESERVED_ALL)): 500 | break 501 | 502 | return s 503 | 504 | def term_num (): 505 | return f' {str (math.exp (random () * 100 - 50) * (-1 if random () >= 0.5 else 1))} ' 506 | 507 | _TERM_VARS = sast.AST_Var.GREEK + tuple ('\\' + g for g in sast.AST_Var.GREEK) + tuple (sast.AST_Var.PY2TEXMULTI.keys ()) 508 | 509 | def term_var (): 510 | return f' {choice (_TERM_VARS)}{f"_{{{randint (0, 100)}}}" if random () < 0.25 else ""} ' 511 | 512 | def expr_semicolon (): 513 | return '; '.join (expr () for _ in range (randrange (2, 5))) 514 | 515 | def expr_ass (): 516 | return f'{expr ()} = {expr ()}' 517 | 518 | def expr_in (): 519 | s = expr () 520 | 521 | for _ in range (randrange (1, 4)): 522 | s = s + f' {choice (["in", "not in"])} {expr ()}' 523 | 524 | return s 525 | 526 | def expr_cmp (): # this gets processed and possibly reordered in sxlat 527 | s = expr () 528 | 529 | for _ in range (randrange (1, 4)): 530 | s = s + f' {choice (["==", "!=", "<", "<=", ">", ">="])} {expr ()}' 531 | 532 | return s 533 | 534 | def expr_attr (): 535 | return f' {expr ()}{"".join (f".{_randidentifier ()}" + ("()" if random () >= 0.5 else "") for _ in range (randint (1, 3)))} ' 536 | 537 | def expr_comma (): 538 | return f" {','.join (f'{expr ()}' for _ in range (randint (2, 3)))} " 539 | 540 | def expr_curly (): 541 | s = ','.join (f'{expr ()}' for _ in range (randint (1, 3))) if random () < 0.8 else '' 542 | 543 | for _ in range (randint (1, 3)): 544 | s = f'{{{s}}}' 545 | 546 | return s 547 | 548 | def expr_paren (): 549 | s = ','.join (f'{expr ()}' for _ in range (randint (1, 3))) if random () < 0.8 else '' 550 | 551 | for _ in range (randint (1, 3)): 552 | s = f'({s})' 553 | 554 | return s 555 | 556 | def expr_brack (): 557 | s = ','.join (f'{expr ()}' for _ in range (randint (1, 3))) if random () < 0.8 else '' 558 | 559 | for _ in range (randint (1, 3)): 560 | s = f'[{s}]' 561 | 562 | return s 563 | 564 | def expr_abs (): 565 | return f'\\left|{expr ()}\\right|' 566 | 567 | def expr_minus (): 568 | return f' -{expr ()} ' 569 | 570 | def expr_fact (): 571 | return f' {expr ()}! ' 572 | 573 | def expr_add (): 574 | return f" {'+'.join (f'{expr ()}' for i in range (randrange (2, 4)))} " 575 | 576 | def expr_mul_imp (): 577 | return f" {' '.join (f'{expr ()}' for i in range (randrange (2, 4)))} " 578 | 579 | def expr_mul_exp (): 580 | return f" {'*'.join (f'{expr ()}' for i in range (randrange (2, 4)))} " 581 | 582 | def expr_mul_cdot (): 583 | return ' ' + ' \\cdot '.join (f'{expr ()}' for i in range (randrange (2, 4))) + ' ' 584 | 585 | def expr_div (): 586 | return f' {expr ()} / {expr ()} ' 587 | 588 | def expr_frac (): 589 | return f'\\frac{expr ()}{expr ()} ' 590 | 591 | def expr_caret (): 592 | return f' {expr ()}^{expr ()} ' 593 | 594 | def expr_dblstar (): 595 | return f' {expr ()}**{expr ()} ' 596 | 597 | def expr_log (): 598 | return \ 599 | choice ([' ', '\\']) + f'{choice (["ln", "log"])}{expr ()} ' \ 600 | if random () >= 0.5 else \ 601 | f'\\log_{expr ()}{expr ()} ' 602 | 603 | def expr_sqrt (): 604 | return \ 605 | choice ([' ', '\\']) + f'sqrt{expr ()} ' \ 606 | if random () >= 0.5 else \ 607 | f'\\sqrt[{expr ()}]{expr ()} ' 608 | 609 | _FORBIDDEN_SXLAT_FUNCS = set (sxlat.XLAT_FUNC2AST_TEX) | set (sxlat.XLAT_FUNC2AST_NAT) | set (sxlat.XLAT_FUNC2AST_PY) | set (sxlat._XLAT_FUNC2TEX) | {'Gamma', 'digamma', 'idiff'} 610 | 611 | def expr_func (): 612 | while 1: 613 | py = choice (list (AST.Func.PY)) 614 | 615 | if py not in _FORBIDDEN_SXLAT_FUNCS: 616 | break 617 | 618 | while 1: 619 | tex = choice (list (AST.Func.TEX)) 620 | 621 | if tex not in _FORBIDDEN_SXLAT_FUNCS: 622 | break 623 | 624 | return \ 625 | '\\' + f'{tex}{expr_paren ()}' \ 626 | if random () >= 0.5 else \ 627 | f' {py}{expr_paren ()}' \ 628 | 629 | def expr_lim (): 630 | return \ 631 | '\\lim_{x \\to ' + f'{expr ()}}} {expr ()} ' \ 632 | # if random () >= 0.5 else \ 633 | # f'Limit ({expr ()}, x, ({expr ()}))' 634 | 635 | def expr_sum (): 636 | return \ 637 | '\\sum_{x = ' + f'{expr ()}}}^{expr ()} {expr ()} ' \ 638 | # if random () >= 0.5 else \ 639 | # f'Sum ({expr ()}, (x, {expr ()}, {expr ()}))' 640 | 641 | def expr_diff (): 642 | d = choice (['d', 'partial']) 643 | p = 0 644 | dv = [] 645 | 646 | for _ in range (randrange (1, 4)): 647 | n = randrange (1, 4) 648 | p += n 649 | 650 | dv.append ((choice (['x', 'y', 'z']), n)) 651 | 652 | diff = expr () if random () < 0.5 else f'({expr ()})' 653 | 654 | return \ 655 | f' {d}^{{{p}}} / {" ".join (f"{d + v}^{{{dp}}}" for v, dp in dv)} {diff} ' \ 656 | # if random () >= 0.5 else \ 657 | # f'Derivative ({expr ()}, {", ".join (f"{v}, {dp}" for v, dp in dv)})' 658 | 659 | def expr_diffp (): 660 | return f"""{expr ()}{"'" * randrange (1, 4)}""" 661 | 662 | def expr_intg (): 663 | dv = f'd{_randidentifier () if random () >= 0.5 else choice (_LETTERS)}' 664 | 665 | if random () >= 0.5: 666 | return f'\\int_{expr ()}^{expr ()} {expr ()} {dv} ' 667 | else: 668 | return f'\\int {expr ()} {dv} ' 669 | 670 | def expr_vec (): 671 | return '\\[' + ','.join (f'{expr ()}' for i in range (randrange (1, 4))) + ',]' 672 | 673 | def expr_mat (): 674 | cols = randrange (1, 4) 675 | 676 | return '\\[' + ','.join ('[' + ','.join (f'{expr ()}' for j in range (cols)) + ',]' for i in range (randrange (1, 4))) + ',]' 677 | 678 | def expr_piece (): 679 | p = [f' {expr ()} if {expr ()} '] 680 | 681 | for _ in range (randrange (3)): 682 | p.append (f' else {expr ()} if {expr ()} ') 683 | 684 | if random () >= 0.5: 685 | p.append (f' else {expr ()} ') 686 | 687 | return ' '.join (p) 688 | 689 | def expr_lamb (): 690 | return f' lambda{choice (["", " x", " x, y", " x, y, z"])}: {expr ()} ' 691 | 692 | def expr_idx (): 693 | if random () >= 0.5: 694 | return f' {expr ()} [{expr ()}]' 695 | elif random () >= 0.5: 696 | return f' {expr ()} [{expr ()}, {expr ()}]' 697 | else: 698 | return f' {expr ()} [{expr ()}, {expr ()}, {expr ()}]' 699 | 700 | def expr_slice (): 701 | start, stop, step = expr ().replace ('None', 'C'), expr ().replace ('None', 'C'), expr ().replace ('None', 'C') 702 | 703 | if random () >= 0.5: 704 | ret = f' {choice ([start, ""])} : {choice ([stop, ""])} ' 705 | else: 706 | ret = f' {choice ([start, ""])} : {choice ([stop, ""])} : {choice ([step, ""])} ' 707 | 708 | return ret if random () >= 0.5 else f'{{{ret}}}' if random () >= 0.5 else f'({ret})' 709 | 710 | def expr_set (): 711 | return '\\{' + ','.join (f'{expr ()}' for i in range (randrange (4))) + '}' 712 | 713 | def expr_dict (): 714 | return f" {' {' + ','.join (f'{expr ()} : {expr ()}' for i in range (randrange (4))) + '}'} " 715 | 716 | def expr_union (): 717 | return f" {' || '.join (f'{expr ()}' for i in range (randrange (2, 4)))} " 718 | 719 | def expr_sdiff (): 720 | return f" {' ^^ '.join (f'{expr ()}' for i in range (randrange (2, 4)))} " 721 | 722 | def expr_xsect (): 723 | return f" {' && '.join (f'{expr ()}' for i in range (randrange (2, 4)))} " 724 | 725 | def expr_or (): 726 | return f" {' or '.join (f'{expr ()}' for i in range (randrange (2, 4)))} " 727 | 728 | def expr_and (): 729 | return f" {' and '.join (f'{expr ()}' for i in range (randrange (2, 4)))} " 730 | 731 | def expr_not (): 732 | return f' not {expr ()} ' 733 | 734 | def expr_ufunc (): 735 | name = choice (('', 'f', 'g', 'h', 'u')) 736 | vars = choice (((), ('x',), ('x', 'y'), ('x', 'y', 'z'))) 737 | kw = (() if random () < 0.8 else ('real = True',)) + (() if random () < 0.8 else ('commutative = True',)) 738 | 739 | if random () < 0.25: 740 | s = f"Function({name!r}, {', '.join (kw)})({', '.join (vars)})" 741 | 742 | else: 743 | q = '?' if not name or random () < 0.25 else '' 744 | 745 | if random () < 0.5: 746 | s = f"{q}{name}({', '.join (kw)})({', '.join (vars)})" 747 | else: 748 | s = f"{q}{name}({', '.join (vars + kw)})" 749 | 750 | if len (vars) == 1 and random () < 0.2: 751 | s = s + choice (("'", "''", "'''")) 752 | 753 | elif vars and random () < 0.5: 754 | p = randint (1, 3) 755 | d = f"d{f'**{p}' if p > 1 else ''} / {' '.join (f'd{choice (vars)}' for _ in range (randint (1, 3)))}" 756 | s = f'{d} {s}' if random () < 0.25 else f'{d} ({s})' 757 | 758 | if vars and random () < 0.5: 759 | s = f"{s}({', '.join (str (i) for i in range (len (vars)))})" 760 | 761 | return s 762 | 763 | def expr_subs (): 764 | t = [(expr (), expr ()) for _ in range (randint (1, 3))] 765 | r = randrange (3) 766 | 767 | if r == 0: 768 | s = ', '.join (f'{s} = {d}' for s, d in t) 769 | elif r == 1: 770 | s = f"{', '.join (s for s, d in t)} = {', '.join (d for s, d in t)}" 771 | else: 772 | s = '\\substack{' + ' \\\\ '.join (f'{s} = {d}' for s, d in t) + '}' 773 | 774 | if random () < 0.5: 775 | return f'\\. {expr ()} |_{{{s}}}' 776 | else: 777 | return f'\\left. {expr ()} \\right|_{{{s}}}' 778 | # else: Subs () 779 | 780 | def expr_sym (): 781 | name = _randidentifier () if random () < 0.95 else '' 782 | kw = (() if random () < 0.5 else ('real = True',)) + (() if random () < 0.5 else ('commutative = True',)) 783 | 784 | if random () < 0.25: 785 | return f"Symbol({name!r}, {', '.join (kw)})" 786 | else: 787 | return f"${name}({', '.join (kw)})" 788 | 789 | #............................................................................................... 790 | DEPTH = 0 # pylint snack 791 | EXPRS = [va [1] for va in filter (lambda va: va [0] [:5] == 'expr_', globals ().items ())] 792 | TERMS = [va [1] for va in filter (lambda va: va [0] [:5] == 'term_', globals ().items ())] 793 | CURLYS = True # if False then intentionally introduces grammatical ambiguity to test consistency in those cases 794 | 795 | def expr_term (): 796 | ret = choice (TERMS) () if random () < 0.2 else f' {choice (_STATIC_TERMS)} ' 797 | 798 | return f'{{{ret}}}' if CURLYS else ret 799 | 800 | def expr (): 801 | global DEPTH 802 | 803 | if DEPTH <= 0: 804 | return expr_term () 805 | 806 | else: 807 | DEPTH -= 1 808 | ret = choice (EXPRS) () 809 | DEPTH += 1 810 | 811 | return f'{{{ret}}}' if CURLYS else ret 812 | 813 | #............................................................................................... 814 | parser = sparser.Parser () 815 | 816 | sym.set_pyS (False) 817 | 818 | def parse (text, retprepost = False): 819 | t0 = time.process_time () 820 | ret = parser.parse (text) 821 | t = time.process_time () - t0 822 | 823 | if t > 2: 824 | print () 825 | print (f'Slow parse {t}s: \n{text}', file = sys.stderr) 826 | 827 | if not ret [0] or ret [1] or ret [2]: 828 | return None 829 | 830 | return (ret [0], ret [0].pre_parse_postprocess) if retprepost else ret [0] 831 | 832 | def test (argv = None): 833 | global DEPTH, CURLYS 834 | 835 | funcs = {'N', 'O', 'S', 'beta', 'gamma', 'Gamma', 'Lambda', 'zeta'} 836 | 837 | sym.set_sym_user_funcs (funcs) 838 | sparser.set_sp_user_funcs (funcs) 839 | sym.set_strict (True) 840 | 841 | # sxlat._SX_XLAT_AND = False # turn off py And translation because it mangles things 842 | 843 | depth = 3 844 | single = None 845 | quick = False 846 | topexpr = None 847 | opts, _ = getopt (sys.argv [1:] if argv is None else argv, 'tnpiqScd:e:E:', ['tex', 'nat', 'py', 'dump', 'show', 'se', 'showerr', 'inf', 'infinite', 'nc', 'nocurlys', 'ns', 'nospaces', 'rs', 'randomspaces', 'tp', 'transpose', 'quick', 'nopyS', 'cross', 'depth=', 'expr=', 'topexpr=']) 848 | 849 | if ('-q', '') in opts or ('--quick', '') in opts: 850 | parser.set_quick (True) 851 | quick = True 852 | 853 | if ('-S', '') in opts or ('--nopyS', '') in opts: 854 | sym.set_pyS (False) 855 | 856 | for opt, arg in opts: 857 | if opt in ('-d', '--depth'): 858 | depth = int (arg) 859 | elif opt in ('-e', '--expr'): 860 | single = [arg] 861 | elif opt in ('-E', '--topexpr'): 862 | topexpr = globals ().get (f'expr_{arg}') 863 | 864 | if topexpr is None: 865 | topexpr = expr 866 | else: 867 | EXPRS.remove (topexpr) 868 | 869 | if ('--dump', '') in opts: 870 | DEPTH = 0 871 | 872 | for e in EXPRS: 873 | print (e ()) 874 | 875 | sys.exit (0) 876 | 877 | dotex = ('--tex', '') in opts or ('-t', '') in opts 878 | donat = ('--nat', '') in opts or ('-n', '') in opts 879 | dopy = ('--py', '') in opts or ('-p', '') in opts 880 | showerr = ('--se', '') in opts or ('--showerr', '') in opts 881 | CURLYS = not (('--nc', '') in opts or ('--nocurlys', '') in opts) 882 | spaces = not (('--ns', '') in opts or ('--nospaces', '') in opts) 883 | rndspace = ('--rs', '') in opts or ('--randomspaces', '') in opts 884 | transpose = ('--tp', '') in opts or ('--transpose', '') in opts 885 | show = ('--show', '') in opts 886 | infinite = (('-i', '') in opts or ('--inf', '') in opts or ('--infinite', '') in opts) 887 | docross = ('--cross', '') in opts or ('-c', '') in opts 888 | 889 | if not (dotex or donat or dopy): 890 | dotex = donat = dopy = True 891 | 892 | if infinite and not single: 893 | expr_func = (lambda: topexpr ()) if spaces else (lambda: topexpr ().replace (' ', '')) 894 | else: 895 | expr_func = iter (single or filter (lambda s: s [0] != '#', _EXPRESSIONS)).__next__ 896 | 897 | try: 898 | while 1: 899 | def validate (ast): # validate ast rules have not been broken by garbling functions 900 | if not isinstance (ast, AST): 901 | return ast 902 | 903 | if ast.is_var: 904 | if ast.var in sparser.RESERVED_ALL or ast.var_name.startswith ('_'): 905 | return AST ('@', 'C') 906 | 907 | if ast.is_func: # the slice function is evil 908 | if ast.func == 'slice' and ast.args.len == 2 and ast.args [0] == AST.None_: # :x gets written as slice(x) but may come from slice(None, x) 909 | ast = AST ('-slice', AST.None_, ast.args [1], None) 910 | elif ast.func in _FORBIDDEN_SXLAT_FUNCS: # random spaces can create forbidden functions 911 | ast = AST ('-func', 'print', *ast [2:]) 912 | 913 | elif ast.is_diff: # reserved words can make it into diff via dif or partialelse 914 | if any (v [0] in sparser.RESERVED_ALL for v in ast.dvs): 915 | return AST ('@', 'C') 916 | 917 | elif ast.is_intg: # same 918 | if ast.dv.as_var.var in sparser.RESERVED_ALL: 919 | return AST ('@', 'C') 920 | 921 | elif ast.is_slice: # the slice object is evil 922 | if ast.start == AST.None_ or ast.stop == AST.None_ or ast.step == AST.None_: 923 | raise ValueError ('malformed slice') 924 | # ast = AST ('-slice', ast.start, ast.stop, None) 925 | 926 | elif ast.is_ufunc: # remove spaces inserted into ufunc name 927 | if ' ' in ast.ufunc: 928 | ast = AST ('-ufunc', ast.ufunc_full.replace (' ', ''), ast.vars, ast.kw) 929 | 930 | elif ast.is_subs: 931 | if ast.expr.is_comma: 932 | ast = AST ('-subs', ('(', ast.expr), ast.subs) 933 | 934 | elif ast.is_sym: # remove spaces inserted into ufunc name 935 | if ' ' in ast.sym: 936 | ast = AST ('-sym', ast.sym.replace (' ', ''), ast.kw) 937 | 938 | return AST (*(validate (a) for a in ast)) 939 | 940 | def check_double_curlys (ast): 941 | if not isinstance (ast, AST): 942 | return False 943 | elif ast.is_curly and ast.curly.is_curly: 944 | return True 945 | 946 | return any (check_double_curlys (a) for a in ast) 947 | 948 | # start here 949 | status = [] 950 | DEPTH = depth 951 | text = expr_func () 952 | 953 | if text and infinite and not single and rndspace: # insert a random space to break shit 954 | i = randrange (0, len (text)) 955 | text = f'{text [:i]} {text [i:]}' 956 | 957 | if transpose: # transpose random block of text to another location overwriting that location 958 | s0, s1, d0, d1 = (randrange (len (text)) for _ in range (4)) 959 | s0, s1 = sorted ((s0, s1)) 960 | d0, d1 = sorted ((d0, d1)) 961 | text = text [:d0] + text [s0 : s1] + text [d1:] 962 | 963 | status.append (f'text: {text}') 964 | ast = parse (text) # fixstuff (parse (text)) 965 | status.append (f'ast: {ast}') 966 | 967 | err = None 968 | 969 | if not ast: 970 | if single or (not infinite and not quick) or showerr: 971 | err = ValueError ("error parsing") 972 | 973 | if ast and not err: 974 | try: 975 | ast2 = validate (ast) 976 | 977 | except Exception as e: # make sure garbling functions did not create an invalid ast 978 | if single or showerr: 979 | err = e 980 | else: 981 | ast = None 982 | 983 | if ast and not err: 984 | if ast2 != ast: 985 | status.append (f'astv: {ast2}') 986 | 987 | ast = ast2 988 | 989 | if dopy and any (a.is_ass for a in (ast.scolon if ast.is_scolon else (ast,))): # reject assignments at top level if doing py because all sorts of mangling goes on there, we just want expressions in that case 990 | if single or showerr: 991 | err = ValueError ("disallowed assignment") 992 | else: 993 | ast = None 994 | 995 | if err or not ast: 996 | if err and not showerr: 997 | raise err 998 | 999 | if showerr: 1000 | print (f'\n{text} ... {err}') 1001 | 1002 | continue 1003 | 1004 | if show: 1005 | print (f'{text}\n') 1006 | 1007 | sxlat._SX_XLAT_AND = False # turn off py And translation because it mangles things 1008 | 1009 | for rep in ('tex', 'nat', 'py'): 1010 | if locals () [f'do{rep}']: 1011 | symfunc = getattr (sym, f'ast2{rep}') 1012 | 1013 | status.append (f'sym.ast2{rep} ()') 1014 | 1015 | text1 = symfunc (ast) 1016 | status [-1] = f'{rep}1: {" " if rep == "py" else ""}{text1}' 1017 | 1018 | status.append ('parse ()') 1019 | 1020 | rast, rpre = parse (text1, retprepost = True) # fixstuff (parse (text1)) 1021 | 1022 | if not rast: 1023 | raise ValueError (f"error parsing") 1024 | 1025 | if check_double_curlys (rpre): 1026 | status [-1] = f'astd: {rpre}' 1027 | 1028 | raise ValueError ("doubled curlys") 1029 | 1030 | status [-1:] = [f'ast: {rast}', f'sym.ast2{rep} ()'] 1031 | text2 = symfunc (rast) 1032 | status [-1] = f'{rep}2: {" " if rep == "py" else ""}{text2}' 1033 | 1034 | if text2 != text1: 1035 | raise ValueError ("doesn't match") 1036 | 1037 | del status [-3:] 1038 | 1039 | if docross and dotex + donat + dopy > 1: 1040 | def sanitize (ast): # prune or reformat information not encoded same across different representations and asts which are not possible from parsing 1041 | if not isinstance (ast, AST): 1042 | return ast 1043 | 1044 | # elif ast.is_ass: 1045 | # return AST ('<>', sanitize (AST ('(', ast.lhs) if ast.lhs.is_comma else ast.lhs), (('==', sanitize (AST ('(', ast.rhs) if ast.rhs.is_comma else ast.rhs)),)) 1046 | 1047 | elif ast.is_minus: 1048 | if ast.minus.is_num_pos: 1049 | return AST ('#', f'-{ast.minus.num}') 1050 | 1051 | elif ast.is_paren: 1052 | if not ast.paren.is_comma: 1053 | return sanitize (ast.paren) 1054 | 1055 | elif ast.is_mul: 1056 | return AST ('*', tuple (sanitize (a) for a in ast.mul)) 1057 | 1058 | elif ast.is_log: 1059 | return AST ('-log', sanitize (ast.log)) 1060 | 1061 | elif ast.is_sqrt: 1062 | return AST ('-sqrt', sanitize (ast.rad)) 1063 | 1064 | elif ast.is_func: 1065 | if ast.func == 'And': 1066 | args = sanitize (ast.args) 1067 | ast2 = sxlat._xlat_f2a_And (*args, force = True) 1068 | 1069 | if ast2 is not None: 1070 | ast = ast2 1071 | else: 1072 | return AST ('-and', args) 1073 | 1074 | elif ast.is_sum: 1075 | if ast.from_.is_comma: 1076 | return AST ('-sum', sanitize (ast.sum), ast.svar, sanitize (AST ('(', ast.from_) if ast.from_.is_comma else ast.from_), ast.to) 1077 | 1078 | elif ast.is_diff: 1079 | if len (set (dv [0] for dv in ast.dvs)) == 1 and ast.is_diff_partial: 1080 | return AST ('-diff', sanitize (ast.diff), 'd', ast.dvs) 1081 | 1082 | elif ast.is_intg: 1083 | if ast.intg is None: 1084 | return AST ('-intg', AST.One, *tuple (sanitize (a) for a in ast [2:])) 1085 | 1086 | elif ast.is_piece: 1087 | if ast.piece [-1] [1] == AST.True_: 1088 | ast = AST ('-piece', ast.piece [:-1] + ((ast.piece [-1] [0], True),)) 1089 | 1090 | if ast.piece.len == 1 and ast.piece [0] [1] is True: 1091 | ast = ast.piece [0] [0] 1092 | 1093 | elif ast.is_slice: 1094 | ast = AST ('-slice', False if ast.start == AST.None_ else ast.start, False if ast.stop == AST.None_ else ast.stop, AST ('@', 'C') if ast.step == AST.None_ else False if ast.step is None else ast.step) 1095 | 1096 | elif ast.is_and: 1097 | args = sanitize (ast.and_) 1098 | ast2 = sxlat._xlat_f2a_And (*args, force = True) 1099 | 1100 | if ast2 is not None: 1101 | ast = ast2 1102 | 1103 | elif ast.is_ufunc: 1104 | if ast.is_ufunc_explicit: 1105 | ast = AST ('-ufunc', ast.ufunc, *ast [2:]) 1106 | 1107 | return AST (*tuple (sanitize (a) for a in ast))#, **ast._kw) 1108 | 1109 | sxlat._SX_XLAT_AND = True # turn on py And translation because it is needed here 1110 | 1111 | # start here 1112 | ast = sanitize (ast) 1113 | status.append (f'ast: {ast}') 1114 | 1115 | if dotex: 1116 | tex1 = sym.ast2tex (ast) 1117 | status.append (f'tex1: {tex1}') 1118 | ast2 = ast = sanitize (parse (tex1)).flat 1119 | 1120 | if donat: 1121 | status.append (f'ast: {ast2}') 1122 | nat = sym.ast2nat (ast2) 1123 | status.append (f'nat: {nat}') 1124 | ast2 = parse (nat) 1125 | 1126 | if dopy: 1127 | try: 1128 | sym._SYM_MARK_PY_ASS_EQ = True # allow xlat of marked Eq functions which indicate assignment in python text 1129 | 1130 | status.append (f'ast: {ast2}') 1131 | py = sym.ast2py (ast2, ass2cmp = False) 1132 | status.append (f'py: {py}') 1133 | ast2 = parse (py) 1134 | 1135 | finally: 1136 | sym._SYM_MARK_PY_ASS_EQ = False # allow xlat of marked Eq functions which indicate assignment in python text 1137 | 1138 | try: 1139 | if dopy: 1140 | sxlat._SX_READ_PY_ASS_EQ = True # allow xlat of marked Eq functions which indicate assignment in python text 1141 | 1142 | status.append (f'ast: {ast2}') 1143 | tex2 = sym.ast2tex (ast2) 1144 | status.append (f'tex2: {tex2}') 1145 | ast2 = sanitize (parse (tex2)).flat 1146 | 1147 | finally: 1148 | sxlat._SX_READ_PY_ASS_EQ = False # allow xlat of marked Eq functions which indicate assignment in python text 1149 | 1150 | elif donat: # TODO: add more status updates for intermediate steps like above 1151 | nat1 = sym.ast2nat (ast) 1152 | status.append (f'nat1: {nat1}') 1153 | ast2 = ast = sanitize (parse (nat1)).flat 1154 | 1155 | try: 1156 | sym._SYM_MARK_PY_ASS_EQ = True # allow xlat of marked Eq functions which indicate assignment in python text 1157 | 1158 | status.append (f'ast: {ast2}') 1159 | py = sym.ast2py (ast2, ass2cmp = False) 1160 | status.append (f'py: {py}') 1161 | ast2 = parse (py) 1162 | 1163 | finally: 1164 | sym._SYM_MARK_PY_ASS_EQ = False # allow xlat of marked Eq functions which indicate assignment in python text 1165 | 1166 | try: 1167 | sxlat._SX_READ_PY_ASS_EQ = True # allow xlat of marked Eq functions which indicate assignment in python text 1168 | 1169 | status.append (f'ast: {ast2}') 1170 | nat2 = sym.ast2nat (ast2) 1171 | status.append (f'nat2: {nat2}') 1172 | ast2 = sanitize (parse (nat2)).flat 1173 | 1174 | finally: 1175 | sxlat._SX_READ_PY_ASS_EQ = False # allow xlat of marked Eq functions which indicate assignment in python text 1176 | 1177 | if ast2 != ast: 1178 | status.extend ([f'ast: {ast2}', f'org: {ast}']) 1179 | 1180 | raise ValueError ("doesn't match across representations") 1181 | 1182 | except (KeyboardInterrupt, StopIteration): 1183 | pass 1184 | 1185 | except: 1186 | print ('Exception!\n') 1187 | print ('\n'.join (status)) 1188 | print () 1189 | 1190 | raise 1191 | 1192 | finally: 1193 | sxlat._SX_XLAT_AND = True 1194 | 1195 | return True 1196 | 1197 | if __name__ == '__main__': 1198 | # test ('-d4 --nc --ns --rs --tp --quick --showerr'.split ()) 1199 | # test (['-c', '-e', r"""{{\int { { log{ oo } }+{ { 1e+100 } \cdot { \Omega } }+{ { \infty zoo }^{ \zeta } } } dS }'}"""]) 1200 | # test (['--quick', '--showerr']) 1201 | # test ([]) 1202 | test () 1203 | --------------------------------------------------------------------------------