├── tests ├── __init__.py ├── native_hebi_tests │ ├── __init__.py │ └── test_native.hebi ├── compile_native_tests.py └── test_parser.py ├── src └── hebi │ ├── __init__.py │ ├── basic │ ├── __init__.py │ └── _macro_.py │ ├── __main__.py │ ├── kernel.py │ ├── parser.py │ └── bootstrap.py ├── requirements.txt ├── .github └── workflows │ └── test-package.yml ├── setup.py ├── README.md └── LICENSE /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/hebi/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | --editable . 2 | hypothesis 3 | -------------------------------------------------------------------------------- /src/hebi/basic/__init__.py: -------------------------------------------------------------------------------- 1 | from . import _macro_ 2 | -------------------------------------------------------------------------------- /tests/native_hebi_tests/__init__.py: -------------------------------------------------------------------------------- 1 | print(__package__) 2 | 3 | from hebi.parser import transpile 4 | 5 | def recompile(): 6 | transpile(__package__, "test_native") 7 | -------------------------------------------------------------------------------- /tests/compile_native_tests.py: -------------------------------------------------------------------------------- 1 | # import unittest 2 | # import sys 3 | # from pprint import pprint 4 | # 5 | # pprint(sys.path) 6 | 7 | 8 | from tests.native_hebi_tests import recompile 9 | recompile() 10 | 11 | # from tests.native_hebi_tests import native_tests 12 | # 13 | # unittest.main(native_tests) 14 | -------------------------------------------------------------------------------- /src/hebi/basic/_macro_.py: -------------------------------------------------------------------------------- 1 | from ..bootstrap import ( 2 | def_, 3 | class_, 4 | import_, 5 | from_, 6 | if_, 7 | raise_, 8 | mask, 9 | begin, 10 | begin0, 11 | and_, 12 | or_, 13 | not_, 14 | with_, 15 | assert_, 16 | let, 17 | loop, 18 | try_, 19 | for_, 20 | break_, 21 | continue_, 22 | runtime, 23 | of, 24 | attach, 25 | del_, 26 | ) 27 | -------------------------------------------------------------------------------- /src/hebi/__main__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import subprocess 6 | import sys 7 | import time 8 | 9 | from jupyter_console import app 10 | 11 | 12 | def main(): 13 | print("Attempting to start Hebigo kernel without installing kernelspec.") 14 | kernel = subprocess.Popen([sys.executable, "-m", "hebi.kernel"]) 15 | print("Waiting for kernel to start.") 16 | time.sleep(3) # TODO: this seems brittle. 17 | print("Starting Jupyter Console with the most recent active kernel...") 18 | app.launch_new_instance(argv=["jupyter", "console", "--existing"]) 19 | print("Console exit. Terminating Hebigo kernel.") 20 | kernel.kill() 21 | 22 | 23 | if __name__ == "__main__": 24 | main() 25 | -------------------------------------------------------------------------------- /.github/workflows/test-package.yml: -------------------------------------------------------------------------------- 1 | name: Test package 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Set up Python 3.8 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: 3.8 16 | - uses: actions/cache@v1 17 | with: 18 | path: ~/.cache/pip 19 | key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} 20 | restore-keys: | 21 | ${{ runner.os }}-pip- 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install -r requirements.txt 26 | - name: Lint with flake8 27 | run: | 28 | pip install flake8 29 | # stop the build if there are Python syntax errors or undefined names 30 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 31 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 32 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 33 | - name: Create Hebigo package 34 | run: | 35 | pip install setuptools 36 | python setup.py sdist 37 | - name: Install Hebigo package from dist/ 38 | run: | 39 | pip install dist/* 40 | - name: Run unit tests 41 | run: | 42 | pwd 43 | python -c 'import tests.compile_native_tests' 44 | python -m unittest discover --verbose 45 | 46 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import setuptools 3 | 4 | 5 | with open("README.md", encoding="utf8") as f: 6 | long_description = f.read() 7 | 8 | 9 | setuptools.setup( 10 | name="hebigo", 11 | version="0.1.0", 12 | description="An indentation-based skin for Hissp.", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | author="Matthew Egan Odendahl", 16 | author_email="hissp.gilch@xoxy.net", 17 | license="Apache-2.0", 18 | url="https://github.com/gilch/hebigo", 19 | classifiers=[ 20 | "Development Status :: 2 - Pre-Alpha", 21 | "Intended Audience :: Developers", 22 | "License :: OSI Approved :: Apache Software License", 23 | "Natural Language :: English", 24 | "Operating System :: MacOS :: MacOS X", 25 | "Operating System :: Microsoft :: Windows", 26 | "Operating System :: POSIX", 27 | "Operating System :: Unix", 28 | "Programming Language :: Python :: 3", 29 | "Programming Language :: Python :: 3.8", 30 | "Topic :: Software Development", 31 | "Topic :: Software Development :: Code Generators", 32 | "Topic :: Software Development :: Compilers", 33 | "Topic :: Software Development :: Libraries", 34 | ], 35 | keywords="macro metaprogramming compiler DSL AST transpiler", 36 | packages=setuptools.find_packages("src"), 37 | package_dir={"": "src"}, 38 | install_requires=["hissp", "jupyter-console"], 39 | python_requires=">=3.8", 40 | entry_points={"console_scripts": ["hebi=hebi.__main__:main"]}, 41 | ) 42 | -------------------------------------------------------------------------------- /src/hebi/kernel.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import traceback 5 | from typing import Optional 6 | 7 | from hissp.compiler import Compiler 8 | from ipykernel.kernelbase import Kernel 9 | 10 | from hebi import parser 11 | 12 | 13 | class HebigoKernel(Kernel): 14 | implementation = "hebigo" 15 | implementation_version = "0.1.0" 16 | language = "hebigo" 17 | language_version = "0.1.0" 18 | banner = "Hebigo kernel" 19 | 20 | language_info = {"mimetype": "text/hebigo", "file_extension": "hebi"} 21 | 22 | def __init__(self, **kwargs): 23 | super().__init__(**kwargs) 24 | self.compiler = Compiler(evaluate=False) 25 | 26 | def do_execute( 27 | self, 28 | code: str, 29 | silent: bool, 30 | store_history: bool = True, 31 | user_expressions: Optional[dict] = None, 32 | allow_stdin: bool = False, 33 | ): 34 | """ 35 | code (str) – The code to be executed. 36 | silent (bool) – Whether to display output. 37 | store_history (bool) – Whether to record this code in history 38 | and increase the execution count. If silent is True, this 39 | is implicitly False. Currently ignored. 40 | user_expressions (dict) – Mapping of names to expressions to 41 | evaluate after the code has run. Currently ignored. 42 | allow_stdin (bool) – Whether the frontend can provide input on 43 | request (e.g. for Python’s raw_input()). Currently ignored. 44 | 45 | returns a dict containing the fields described in Execution 46 | results. 47 | """ 48 | # To display output, it can send messages using 49 | # send_response(). See Messaging in IPython for details of the 50 | # different message types. 51 | try: 52 | exec( 53 | compile(self.compiler.compile(parser.reads(code)), "", "single"), 54 | self.compiler.ns, 55 | ) 56 | except SystemExit: 57 | raise # TODO: how do we shut down properly? 58 | except: 59 | if not silent: 60 | self.send_response( 61 | self.iopub_socket, 62 | "stream", 63 | {"name": "stderr", "text": traceback.format_exc()}, 64 | ) 65 | 66 | return { 67 | "status": "ok", 68 | "execution_count": self.execution_count, 69 | "payload": [], # Deprecated? 70 | "user_expressions": {}, # Unused? 71 | } 72 | 73 | def do_is_complete(self, code: str): 74 | status = "incomplete" 75 | if code.endswith("\n"): # Empty line; user declined more input. 76 | status = "complete" 77 | try: 78 | if not 'multiary' in dict(parser.lex(code)): 79 | status = "complete" # Nothing to take a block. 80 | list(parser.reads(code)) 81 | except parser.SoftSyntaxError: 82 | status = "incomplete" # Bracketed expression. 83 | except Exception: 84 | status = "invalid" 85 | assert status in {"complete", "incomplete", "invalid", "unknown"} 86 | return {"status": status} 87 | 88 | 89 | if __name__ == "__main__": 90 | from ipykernel.kernelapp import IPKernelApp 91 | 92 | IPKernelApp.launch_instance(kernel_class=HebigoKernel) 93 | -------------------------------------------------------------------------------- /tests/test_parser.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from unittest import TestCase 6 | 7 | from hebi.parser import lex, parse 8 | 9 | 10 | EXPECTED = { 11 | '':[], 12 | '1':[1], 13 | ':foo':[":foo"], 14 | ':!@$%':[":!@$%"], 15 | '1+1j':[1+1j], 16 | '-1.3e5':[-1.3e5], 17 | 18 | 'a':['a'], 19 | '\na':['a'], 20 | 'a\n':['a'], 21 | '\na\n':['a'], 22 | 'a b c':['a','b','c'], 23 | 24 | '''\ 25 | a 26 | b 27 | c 28 | ''':['a','b','c'], 29 | 30 | '''\ 31 | a b 32 | c 33 | ''':['a','b','c'], 34 | 35 | 'a:b': [('a','b')], 36 | 'a:': [('a',)], 37 | 'a: b': [('a','b')], 38 | 'a: b c': [('a','b','c')], 39 | 40 | '''\ 41 | a: 42 | b 43 | c 44 | ''': [('a','b','c')], 45 | 46 | '''\ 47 | a: b 48 | c 49 | ''': [('a','b','c')], 50 | 51 | '''\ 52 | quote:a: b: c: d 53 | e 54 | : 55 | f g 56 | ''':[ 57 | ('quote', 58 | ('a', ('b', ('c','d'),), 59 | 'e', 60 | ':', 61 | 'f','g'),) 62 | ], 63 | 64 | """\ 65 | for: x :in range:3 66 | q 67 | print: x ( 68 | print(x) 69 | ) 70 | """: [('hebi.basic.._macro_.for_', 'x', ':in', ('range',3,),'q',('print','x','''(\ 71 | ( 72 | print(x) 73 | )\ 74 | )''',),)], 75 | 76 | """\ 77 | a: 78 | b: 79 | c: 80 | q 81 | """: [('a',('b',('c',),),),'q'], 82 | 83 | """\ 84 | a: x: y: 85 | b: z: 86 | c: 87 | """: [('a',('x',('y',),),('b',('z',),('c',),),)], 88 | 89 | """\ 90 | pass: 91 | pass: 92 | z:a: b: c: d : e f 93 | e 94 | """: [ 95 | (), 96 | (('z', 97 | ('a',('b', ('c', 'd', ':', 'e', 'f'),), 98 | 'e',),),), 99 | ], 100 | 101 | 'pass:a': [('a',)], 102 | 'pass:a pass:b pass:c': [('a',),('b',),('c',)], 103 | 'pass: a pass:b pass:c': [('a',('b',),('c',),)], 104 | 105 | 'foo: quux: spam': [('foo',('quux','spam'))], 106 | 'foo: pass: quux spam': [('foo',('quux','spam'))], 107 | 'foo: pass:quux spam': [('foo',('quux',),'spam')], 108 | 109 | 'print: "Hello, World!"': [('print', '("Hello, World!")',)], 110 | 111 | ''' 112 | lambda: name: 113 | print: f"Hi, {name}!" 114 | ''': 115 | [ 116 | ('lambda',('name',), 117 | ('print','(f"Hi, {name}!")',),) 118 | ], 119 | 120 | ''' 121 | lambda: pass: name 122 | print: f"Hi, {name}!" 123 | ''': 124 | [ 125 | ('lambda',('name',), 126 | ('print','(f"Hi, {name}!")',),) 127 | ], 128 | 129 | 'lambda: pass:name print: f"Hi, {name}!"': 130 | [ 131 | ('lambda',('name',), 132 | ('print','(f"Hi, {name}!")',),) 133 | ], 134 | 135 | 'foo.bar..spam.eggs':['foo.bar..spam.eggs'], 136 | 'foo.bar..spam.eggs: toast':[('foo.bar..spam.eggs','toast')], 137 | 'foo.bar..spam.eggs:toast':[('foo.bar..spam.eggs','toast')], 138 | 139 | 'pass:pass:foo':[(('foo',),)], 140 | 'pass:pass:pass:foo':[((('foo',),),)], 141 | 'pass: pass: pass:foo':[((('foo',),),)], 142 | 143 | 'operator..getitem:pass:globals':[('operator..getitem',('globals',),)], 144 | 145 | ''' 146 | nest: "foobar" 147 | .replace: "oo" "8" 148 | .upper: 149 | ''': [('nest','("foobar")',('.replace','("oo")','("8")',),('.upper',),)], 150 | ''' 151 | nest: "foobar" pass:.upper .replace: "oo" "8" 152 | ''': [('nest','("foobar")',('.upper',),('.replace','("oo")','("8")',),)], 153 | 154 | ''' 155 | def: a 1 156 | ''': [('hebi.basic.._macro_.def_', 'a', 1,)], 157 | 158 | ''' 159 | def: name: arg1 arg2 : kw1 default1 160 | "docstring" 161 | ... 162 | ''': [('hebi.basic.._macro_.def_', 163 | ('name', 'arg1', 'arg2', ':', 'kw1', 'default1',), 164 | '("docstring")', 165 | '...',)], 166 | 167 | 'quote:def': [('quote', 'hebi.basic.._macro_.def_',)], 168 | '"def"': ['("def")'], 169 | 'quote:"def"': [('quote', '("def")',)], 170 | 'b"def"':['(b"def")'], 171 | 172 | '''"""barfoo""" 173 | ''':['("""barfoo""")'], 174 | ''' 175 | """barfoo"""''':['("""barfoo""")'], 176 | '''""" 177 | foo 178 | baz 179 | """''':['("""\nfoo\nbaz\n""")'], 180 | ''' 181 | """ 182 | foo 183 | bar 184 | """ 185 | ''':['("""\nfoo\nbar\n""")'], 186 | ''' 187 | hot: 188 | block 189 | () 190 | ''':[ 191 | ('hot', 192 | 'block'), 193 | '()'], 194 | ''' 195 | hot: 196 | block 197 | "spam" 198 | ''':[ 199 | ('hot', 200 | 'block'), 201 | '("spam")'], 202 | ''' 203 | """ 204 | docstring 205 | """ 206 | # comment 207 | 208 | def: greet: name 209 | """Says Hi.""" 210 | print: "Hello," name 211 | 212 | # more commentary 213 | (greet('World') if __name__ == '__main__' else None) 214 | ''':['("""\ndocstring\n""")', 215 | ('hebi.basic.._macro_.def_',('greet', 'name'), 216 | '("""Says Hi.""")', 217 | ('print', '("Hello,")', 'name')), 218 | "((greet('World') if __name__ == '__main__' else None))"], 219 | 220 | ''' 221 | lambda: pass: x : :* a b 222 | print: x a 223 | ''': [('lambda',('x',':',':*','a','b'), 224 | ('print','x','a',),)], 225 | '''\ 226 | !foo:bar 227 | !spam: eggs 228 | !quux 229 | if 230 | !if_ 231 | ''':[ 232 | ('hebi.basic.._macro_.foo', 'bar'), 233 | ('hebi.basic.._macro_.spam', 'eggs', 'hebi.basic.._macro_.quux'), 234 | 'hebi.basic.._macro_.if_', 235 | 'hebi.basic.._macro_.if_', 236 | ], 237 | 238 | ''' 239 | !mask:pass:lambda: :: :,:thing_sym :,:thing 240 | print: "Hi!" # Builtin symbol. 241 | greet: "World!" # Local symbol. 242 | :,@:map: # Splice 243 | lambda: pass:call 244 | !mask:pass: # Nested !mask. 245 | :,:operator..getitem: call 0 246 | :,:thing_sym 247 | :,@:operator..getitem: call slice: 1 None 248 | calls 249 | :,:thing_sym 250 | ''':[ 251 | ('hebi.basic.._macro_.mask', 252 | (('lambda', (':', (':,', 'thing_sym'), (':,', 'thing')), 253 | ('print', '("Hi!")'), 254 | ('greet', '("World!")'), 255 | (':,@', ('map', 256 | ('lambda', ('call',), 257 | ('hebi.basic.._macro_.mask', 258 | ((':,', ('operator..getitem', 'call', 0)), 259 | (':,', 'thing_sym'), 260 | (':,@', ('operator..getitem', 'call', ('slice', 1, 'None')))))), 261 | 'calls')), 262 | (':,', 'thing_sym')),)) 263 | ], 264 | 265 | ''' 266 | :=: :strs: a b c 267 | :default: a ('a'+'b') 268 | ''': [ 269 | (':=', 270 | (':strs', 'a', 'b', 'c'), 271 | (':default', 'a', "(('a'+'b'))"), 272 | ) 273 | ], 274 | 275 | ''' 276 | foo: 277 | [1, 2, 278 | 3, 4] 279 | x 280 | bar: 1 281 | ''': [ 282 | ('foo', 283 | '([1, 2,\n 3, 4])', 284 | 'x'), 285 | ('bar', 1), 286 | ], 287 | 288 | ''' 289 | !mask:!mask::,::,:a 290 | :foo:bar 291 | :foo :bar 292 | ''': [ 293 | ('hebi.basic.._macro_.mask', 294 | ('hebi.basic.._macro_.mask', 295 | (':,', 296 | (':,','a')))), 297 | (':foo','bar'), 298 | ':foo', ':bar', 299 | ] 300 | } 301 | 302 | BAD_INDENTS = [''' 303 | # Missing colons. 304 | test_default_strs lambda self: 305 | self.assertEqual: 306 | ['ab', 22, 33] 307 | !let 308 | :=: :strs: a b c 309 | :default: a ('a'+'b') 310 | {'b':22,'c':33} 311 | [a, b, c] 312 | ''', 313 | ''' 314 | norlf: 315 | foo: 316 | [1, 2, 317 | 3, 4] 318 | x # Extra indent. 319 | y bar: 320 | 1 321 | ''', 322 | ] 323 | class TestParser(TestCase): 324 | def test_transpile(self): 325 | import tests.native_hebi_tests 326 | 327 | def test_examples(self): 328 | for k, v in EXPECTED.items(): 329 | with self.subTest(code=k, parsed=v): 330 | print(k) 331 | lex_k = [*lex(k)] 332 | print(lex_k) 333 | parsed = [*parse(lex_k)] 334 | print(parsed) 335 | self.assertEqual(parsed, v) 336 | print('OK') 337 | 338 | def test_bad_indent(self): 339 | for e in BAD_INDENTS: 340 | with self.subTest(example=e): 341 | with self.assertRaises(IndentationError): 342 | print([*lex(e)]) 343 | -------------------------------------------------------------------------------- /src/hebi/parser.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import ast 6 | import os 7 | import re 8 | from codeop import compile_command 9 | from contextlib import contextmanager 10 | from contextvars import ContextVar 11 | from importlib import resources 12 | from pathlib import Path, PurePath 13 | from types import ModuleType 14 | from typing import Union 15 | 16 | from hissp import compiler 17 | 18 | TOKEN = re.compile( 19 | r"""(?x) 20 | (?P[)\]}]) 21 | |(?P\n?[ ]*[#].*) 22 | |(?P(?<=\n)[ ]*(?=[^\r\n])) 23 | |(?P\([ \r\n]*\)) 24 | |(?P[(\[{]) 25 | |(?P 26 | (?:[rR][bfBF]?|[bfBF][rR]?|[uU])? 27 | (?:'''(?:[^'\\]|\\.|\\\n|''?[^'])*''' 28 | |["]""(?:[^"\\]|\\.|\\\n|""?[^"])*["]"" 29 | |'(?:[^'\\]|\\.|\\\n)*' 30 | |"(?:[^"\\]|\\.|\\\n)*")) 31 | |(?P\r?\n) 32 | |(?P[ ]) 33 | |(?P(?<=\n)) 34 | 35 | # Hotwords 36 | |(?P(?: 37 | !? # basic macro? 38 | [.\w]+ # unary symbol 39 | |:[^: \r\n"')\]}]* # unary control word 40 | ):(?=[^ \r\n])) # lack of space after ending colon makes it unary 41 | |(?P(?: 42 | !? # basic macro? 43 | [.\w]+ # multiary symbol 44 | |:[^: \r\n"')\]}]* # multiary control word 45 | ):(?=[ \r\n])) # space after ending colon makes in multiary 46 | 47 | |(?P:[^: \r\n"')\]}]*) 48 | |(?P[^ \r\n"')\]}]+) 49 | |(?P.|\n) 50 | """ 51 | ) 52 | 53 | 54 | def end(s): 55 | for q in ['"""', "'''", "'", '"']: 56 | if q in s: 57 | return q 58 | return {"(": ")", "[": "]", "{": "}"}[s] 59 | 60 | 61 | IGNORE = frozenset( 62 | { 63 | "comment", 64 | "sp", 65 | "blank", 66 | } 67 | ) 68 | 69 | class SoftSyntaxError(SyntaxError): 70 | pass 71 | 72 | 73 | def lex(code): 74 | """ 75 | Because Hebigo is context-sensitive, the lexer has to do extra work. 76 | It keeps an indentation stack and a count of open hot word forms, 77 | so it can infer when to close them. 78 | 79 | The language rules also change when inside a Python expression. 80 | Rather than re-implementing Python's complex grammar, the lexer 81 | just defers to Python's parser to determine if an expression 82 | that might have been completed has been. 83 | 84 | """ 85 | opens = 0 86 | indents = [0] 87 | tokens = iter(TOKEN.finditer(code + "\n")) 88 | for token in tokens: 89 | case = token.lastgroup 90 | group = token.group() 91 | assert case != "error" 92 | if case == "string": 93 | yield "python", group 94 | elif case == "bracket": 95 | python_list = [group] 96 | while True: 97 | try: 98 | t = next(tokens) 99 | except StopIteration: 100 | raise SoftSyntaxError("Incomplete bracketed expression.") from None 101 | python_list.append(t.group()) 102 | if t.lastgroup == "end" and t.group() == end(group): 103 | python = "".join(python_list) 104 | if compile_command(python + "\n\n", symbol="eval") is None: 105 | continue 106 | else: 107 | yield "python", python 108 | break 109 | elif case in IGNORE: 110 | pass 111 | elif case == "indent": 112 | width = len(group) 113 | if width > indents[-1]: 114 | if len(indents) > opens: 115 | ie = IndentationError( 116 | "New indent in same block. (Did you miss a colon?)" 117 | ) 118 | ie.text = code[: code.find("\n", token.span()[1])] 119 | ie.lineno = ie.text.count("\n") 120 | raise ie 121 | indents.append(width) 122 | elif width < indents[-1]: 123 | while width < indents[-1]: 124 | indents.pop() 125 | opens -= 1 126 | yield "close", "DEDENT" 127 | while opens >= len(indents): 128 | opens -= 1 129 | yield "close", "EQDENT" 130 | elif case == "multiary": 131 | opens += 1 132 | yield "open", ":" 133 | if group != "pass:": 134 | yield case, group[:-1] 135 | elif case == "unary": 136 | yield case, group[:-1] 137 | elif case == "eol": 138 | while opens - 1 > len(indents): 139 | opens -= 1 140 | yield "close", "EOL" 141 | else: 142 | yield case, group 143 | 144 | while opens: 145 | opens -= 1 146 | yield "close", "EOF" 147 | 148 | 149 | RESERVED_WORDS = frozenset( 150 | { 151 | "and", 152 | "as", 153 | "assert", 154 | "async", 155 | "await", 156 | "break", 157 | "class", 158 | "continue", 159 | "def", 160 | "del", 161 | "elif", 162 | "else", 163 | "except", 164 | "finally", 165 | "for", 166 | "from", 167 | "global", 168 | "if", 169 | "import", 170 | "in", 171 | "is", 172 | "nonlocal", 173 | "not", 174 | "or", 175 | "raise", 176 | "return", 177 | "try", 178 | "while", 179 | "with", 180 | "yield", 181 | } 182 | ) 183 | 184 | 185 | def parse(tokens): 186 | tokens = iter(tokens) 187 | for case, group in tokens: 188 | if group in RESERVED_WORDS: 189 | group = f"hebi.basic.._macro_.{group}_" 190 | elif group.startswith("!"): 191 | group = f"hebi.basic.._macro_.{group[1:]}" 192 | if case == "open": 193 | yield (*parse(tokens),) 194 | elif case == "close": 195 | return 196 | elif case == "unary": 197 | if group == "pass": 198 | yield next(parse(tokens)), 199 | else: 200 | yield group, next(parse(tokens)), 201 | elif case == "symbol": 202 | if all(s.isidentifier() for s in group.split(".") if s): 203 | yield group 204 | else: 205 | yield ast.literal_eval(group) 206 | elif case == "python": 207 | # Parentheses let the compiler know it's Python expression code. 208 | yield f"({group})" 209 | else: 210 | yield group 211 | 212 | 213 | def reads(hebigo): 214 | res = parse(lex(hebigo)) 215 | return res 216 | 217 | 218 | def transpile(package: resources.Package, *modules: Union[str, PurePath]): 219 | for module in modules: 220 | transpile_module(package, module + ".hebi") 221 | 222 | 223 | QUALSYMBOL = ContextVar("QUALSYMBOL", default=None) 224 | 225 | 226 | @contextmanager 227 | def qualify_context(qualname): 228 | token = QUALSYMBOL.set(qualname) 229 | try: 230 | yield qualname 231 | finally: 232 | QUALSYMBOL.reset(token) 233 | 234 | 235 | def transpile_module( 236 | package: resources.Package, 237 | resource: Union[str, PurePath], 238 | out: Union[None, str, bytes, Path] = None, 239 | ): 240 | code = resources.read_text(package, resource) 241 | path: Path 242 | with resources.path(package, resource) as path: 243 | out = out or path.with_suffix(".py") 244 | if isinstance(package, ModuleType): 245 | package = package.__package__ 246 | if isinstance(package, os.PathLike): 247 | resource = resource.stem 248 | with open(out, "w") as f, qualify_context( 249 | f"{package}.{resource.split('.')[0]}" 250 | ) as qualsymbol: 251 | print("writing to", out) 252 | hissp = parse(lex(code)) 253 | f.write(compiler.Compiler(qualsymbol, evaluate=True).compile(hissp)) 254 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gitter](https://badges.gitter.im/hissp-lang/community.svg)](https://gitter.im/hissp-lang/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 2 | # Hebigo 3 | 蛇語(HEH-bee-go): Snake-speak. 4 | Hebigo is an indentation-based [Hissp](https://github.com/gilch/hissp) skin designed to resemble Python. 5 | 6 | It includes the Hebigo Hissp reader, the Jupyter-console based Hebigo REPL, 7 | and the Hebigo basic macros—a collection of Python-like compiler macros, 8 | which also function independently of the Hebigo reader 9 | (i.e. they work in Lissp or Hissp readerless mode as well). 10 | 11 | Hebigo is still in the prototyping phase, so it's not on PyPI yet. 12 | Install it directly from GitHub with 13 | ``` 14 | pip install git+https://github.com/gilch/hebigo 15 | ``` 16 | See the native tests for example Hebigo code. 17 | 18 | Hebigo keeps Python's expressions as *bracketed expressions*, 19 | but completely replaces Python's *statements* with *hotword expressions*, 20 | which have Hissp's literals, semantics, and macros. 21 | 22 | ## Bracketed Expressions 23 | Bracketed expression are called that because they must be "bracketed" 24 | somehow in order to be distinguishable from the hotword expressions. 25 | Parentheses will always work, but `[]` or `{}` are sufficient. 26 | Quotation marks also work, even with prefixes, like `b''` or `f""""""`, etc. 27 | 28 | Bracketed expressions are mainly used for infix operators, simple literals, and f-strings 29 | (things that might be awkward as hotword expressions), 30 | but any Python expression will work, 31 | even more complex ones like nested comprehensions or chained method calls. 32 | It's best to keep these simple though. 33 | You can't use macros in them. 34 | 35 | ## Hotword Expressions 36 | Hotword expressions are called expressions because they evaluate to a value, 37 | but they resemble Python's statements in form: 38 | ``` 39 | word: 40 | block1 41 | subword: 42 | subblock 43 | block2 44 | ``` 45 | etc. 46 | 47 | The rules are as follows. 48 | 49 | 1. A word ending in a `:` is a "hotword", that is, a function or macro invocation that can take arguments. 50 | ``` 51 | hotword: not_hotword 52 | ``` 53 | 54 | 2. A hotword with no whitespace after its colon is *unary*. Otherwise it's *multiary*. 55 | ``` 56 | unary:arg 57 | multiary0: 58 | multiary1: arg 59 | multiary2: a b 60 | multiary3: a b c 61 | ``` 62 | 63 | 3. Multiary hotwords take all remaning arguments in a line. 64 | ``` 65 | hotword: arg1 arg2 arg3: a:0 b: 1 2 66 | ``` 67 | Parsed like 68 | ``` 69 | hotword(arg1, arg2, arg3(a(0), b(1, 2))) 70 | ``` 71 | 72 | 4. The first (multiary) hotword of the line gets the arguments in the indented block for that line (if any). 73 | ``` 74 | multiary: a b c 75 | d e f 76 | foo unary:gets_block: gets_line: 1 2 77 | a b 78 | c d 79 | ``` 80 | Parsed like 81 | ``` 82 | multiary(a, b, c, d, e, f) 83 | foo 84 | unary(gets_block(gets_line(1, 2), a, b, c, d)) 85 | ``` 86 | Another way to think of it is that a unary applied to another hotword creates a *compound hotword*, which is a composition of the functions. 87 | In the example above, `foo` is not a hotword (no colon), 88 | and the compound hotword `unary:gets_block:` is the first hotword of the line, 89 | so it gets the indented block below the line. 90 | 91 | 5. The special hotword `pass:` invokes its first argument, passing it the remainder. 92 | This allows you to invoke things that are not words, like lambda expressions: 93 | ``` 94 | pass: foo a b 95 | pass: (lambda *a: a) 1 2 3 96 | ``` 97 | Parsed like 98 | ``` 99 | foo(a, b) 100 | (lambda *a: a)(1, 2, 3) 101 | ``` 102 | ### Style 103 | These indentation rules were designed to resemble Python and make editing easier with a basic editor than for S-expressions. 104 | As a matter of style, arguments should be passed in one of three forms, which should not be mixed for function calls. 105 | ``` 106 | linear: a b c d 107 | linear_block: 108 | a b c d 109 | block: 110 | a 111 | b 112 | c 113 | d 114 | # What NOT to do, although it compiles fine. 115 | bad_mix: a 116 | b c 117 | d 118 | ``` 119 | compare that to the same layout for Python invocations. 120 | ``` 121 | linear(a, b, c, d) 122 | linear_block( 123 | a, b, c, d 124 | ) 125 | block( 126 | a, 127 | b, 128 | c, 129 | d, 130 | ) 131 | # What NOT to do. 132 | bad_mix(a, 133 | b, c 134 | d 135 | ) # PEP 8 that. Please. 136 | ``` 137 | The above is for function calls only. 138 | Macro invocations are not exactly bound by these three layout styles, 139 | and may instead have other documented preferred layouts. 140 | 141 | You should group arguments using whitespace when it makes sense to do so. 142 | Anywhere you'd use a comma (or newline) in Clojure, you add an extra space or newline. 143 | This usually after the `:` in function invocations or parameter tuples, 144 | where the arguments are implicitly paired. 145 | ``` 146 | linear: x : a b c d # Note extra space between b and c. 147 | linear_block: 148 | x : a b c d 149 | block: 150 | x 151 | : a b # Newline also works. 152 | c d 153 | ``` 154 | ### Literals 155 | Literals are mostly the same as Lissp for hotword expressions 156 | (and exactly like Python in bracketed expressions). 157 | 158 | Hebigo does not munge symbols like Lissp does. 159 | Qualified symbols (like ``builtins..print``) are allowed, 160 | but not in bracketed expressions, which must be pure Python. 161 | 162 | *Control words* are words that start with a `:`. 163 | These are not allowed in bracketed expressions either 164 | (although they're just compiled to strings, which are). 165 | You'll need these for paired arguments, same as Lissp. 166 | These two expressions are normally equivalent in Hebigo. 167 | ``` 168 | print: 1 2 3 : :* 'abc' sep "/" # Hotword expression. 169 | (print(1, 2, 3, *'abc', sep="/")) # Bracketed expression. 170 | ``` 171 | However, if a macro named `print` were defined, 172 | then the hotword version would invoke the macro, 173 | but the bracketed version would still invoke the builtin, 174 | because macros can only be invoked in hotword expressions. 175 | 176 | Control words may also be used as hotwords, 177 | in which case they both begin and end with a colon. 178 | This makes no sense at the top level (because strings are not callable), 179 | but macros do use them to group arguments. 180 | 181 | Unlike Lissp, normal string literals cannot contain literal newlines. 182 | Use `\n` or triple quotes like Python instead. 183 | (Recall that strings count as bracketed expressions.) 184 | 185 | Hotword expressions may contain bracketed expressions, 186 | but not the reverse, since bracketed expressions must be valid Python, 187 | just like how the statements that the hotword expressions replace may contain expressions, 188 | but Python expressions may not contain Python statements. 189 | 190 | And finally, the `!` is an abbreviation for `hebi.basic.._macro_.`, 191 | the qualifier for Hebigo's included macros. 192 | (This can't work in bracketed expressions either.) 193 | Hebigo has no other "reader macros". 194 | 195 | ## Examples 196 | 197 | (Also see Hebigo's native tests.) 198 | 199 | ### Obligatory factorial example. 200 | 201 | In basic Lissp. (Prelude assumed.) 202 | ```racket 203 | (define factorial 204 | (lambda n 205 | (if-else (eq n 0) 206 | 1 207 | (mul n (factorial (sub n 1)))))) 208 | ``` 209 | Literal translation of the above to Hebigo. 210 | ```python 211 | define: factorial 212 | lambda: n 213 | ifQz_else: eq: n 0 214 | 1 215 | mul: n factorial: sub: n 1 216 | ``` 217 | Note the munged name. 218 | 219 | In more idiomatic Hebigo with statement macros and bracketed expressions. 220 | ```python 221 | def: factorial: n 222 | if: (n == 0) 223 | :then: 1 224 | :else: (n * factorial(n - 1)) 225 | ``` 226 | Literal translation of the above to Lissp. (Statement macros required.) 227 | ```racket 228 | (def_ (factorial n) 229 | (if_ .#"n == 0" 230 | (:then 1) 231 | (:else .#"n * factorial(n - 1)"))) 232 | ``` 233 | Note the injections. 234 | 235 | Finally, in idiomatic Lissp with Hebigo's macros. 236 | ```racket 237 | (def_ (factorial n) 238 | (if-else (eq n 0) 239 | 1 240 | (mul n (factorial (sub n 1))))) 241 | ``` 242 | 243 | ### Fibonacci 244 | 245 | In Python. 246 | ```python 247 | from functools import lru_cache 248 | 249 | @lru_cache(None) 250 | def fibonacci(n): 251 | if n <= 1: 252 | return n 253 | return fibonacci(n - 1) + fibonacci(n - 2) 254 | ``` 255 | In Hebigo. 256 | ```python 257 | def: fibonacci: n 258 | :@ functools..lru_cache: None # Qualified identifier in decorator. 259 | if: (n <= 1) 260 | :then: n 261 | :else: (fibonacci(n - 1) + fibonacci(n - 2)) 262 | ``` 263 | Literal translation to Lissp. 264 | ```racket 265 | (def_ (fibonacci n) 266 | :@ (functools..lru_cache None) ; Qualified identifier in decorator. 267 | (if_ .#"n <= 1" 268 | (:then n) 269 | (:else .#"fibonacci(n - 1) + fibonacci(n - 2)"))) 270 | ``` 271 | In basic Lissp. 272 | ```racket 273 | (define fibonacci 274 | ((functools..lru_cache None) ; Callable expression. 275 | (lambda n 276 | (if-else (le n 1) 277 | n 278 | (add (fibonacci (sub n 1)) 279 | (fibonacci (sub n 2))))))) 280 | ``` 281 | Literal translation to Hebigo 282 | ```python 283 | define: fibonacci 284 | pass: functools..lru_cache: None # Callable expression. 285 | lambda: n 286 | ifQz_else: le: n 1 287 | n 288 | add: 289 | fibonacci: sub: n 1 290 | fibonacci: sub: n 2 291 | ``` 292 | -------------------------------------------------------------------------------- /tests/native_hebi_tests/test_native.hebi: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | """ 6 | Native tests in the Hebi-go. 7 | """ 8 | 9 | from: unittest :import TestCase 10 | 11 | from: hypothesis :import given 12 | import: hypothesis.strategies :as st 13 | 14 | from: hebi.bootstrap :import function 15 | 16 | print: st 17 | 18 | print: TestCase 19 | 20 | def: greet: name 21 | print: "Hello," name 22 | 23 | class: TestAnd: TestCase 24 | def: .test_null: self 25 | self.assertIs: True and: 26 | def: .test_one: self x 27 | :@ given: st.from_type: type 28 | self.assertIs: x and: x 29 | def: .test_two: self x y 30 | :@ given: 31 | st.from_type: type 32 | st.from_type: type 33 | self.assertIs: (x and y) and: x y 34 | def: .test_shortcut: self 35 | and: 0 (0/0) 36 | and: 1 0 (0/0) 37 | and: 0 (0/0) (0/0) 38 | def: .my_given given 39 | def: .test_three: self x y z 40 | :@ .my_given: # Try to read a decorator from _ns_. 41 | st.from_type: type 42 | st.from_type: type 43 | st.from_type: type 44 | self.assertIs: (x and y and z) and: x y z 45 | 46 | class: TestOr: TestCase 47 | def: .test_null: self 48 | self.assertEqual: () or: 49 | def: .test_one: self x 50 | :@ given: st.from_type: type 51 | self.assertIs: x or: x 52 | def: .test_two: self x y 53 | :@ given: 54 | st.from_type: type 55 | st.from_type: type 56 | self.assertIs: (x or y) or: x y 57 | def: .test_shortcut: self 58 | or: 1 (0/0) 59 | or: 0 1 (0/0) 60 | or: 1 (0/0) (0/0) 61 | def: .test_three: self x y z 62 | :@ given: 63 | st.from_type: type 64 | st.from_type: type 65 | st.from_type: type 66 | self.assertIs: (x or y or z) or: x y z 67 | 68 | class: TestLet: TestCase 69 | def: .test_single: self 70 | self.assertEqual: 71 | 42 72 | !let: a :be (40 + 2) 73 | a 74 | def: .test_2: self 75 | self.assertEqual: 76 | 24 77 | !let: 78 | :,: a b 79 | :be [20, 4] 80 | (a + b) 81 | def: .test_nested: self 82 | self.assertEqual: 83 | [1,2,3,4,5,6,7,8] 84 | !let: 85 | :,: 86 | :,: 87 | :,: a b 88 | :,: c d 89 | :,: 90 | :,: e f 91 | :,: g h 92 | :be [[[1, 2], 93 | [3, 4]], 94 | [[5, 6], 95 | [7, 8]]] 96 | [a,b,c,d,e,f,g,h] 97 | def: .test_ignored: self 98 | self.assertEqual: 99 | [1, 4, 5, 42] 100 | !let: _ :be 42 101 | !let: 102 | :,: a _ _ d e 103 | :be [1, 2, 3, 4, 5] 104 | [a, d, e, _] 105 | def: .test_list: self 106 | self.assertEqual: 107 | [1, 2, [3, 4, 5]] 108 | !let: 109 | :,: a b :list c 110 | :be [1, 2, 3, 4, 5] 111 | [a, b, c] 112 | def: .test_iter: self 113 | self.assertEqual: 114 | ['a', 'c', 'b', 'd', 'e'] 115 | !let: 116 | :,: a b :iter c 117 | :be 'abcde' 118 | [a, next(c), b, *c] 119 | def: .test_mapping: self 120 | self.assertEqual: 121 | ['one', 'bar'] 122 | !let: 123 | :=: a 1 b 'foo' 124 | :be {1: 'one', 'foo': 'bar'} 125 | [a, b] 126 | def: .test_nested_mapping: self 127 | self.assertEqual: 128 | ['one', 'spam', 'eggs'] 129 | !let: 130 | :=: 131 | a 132 | 1 133 | # 134 | :=: b 'foo' 135 | 2 136 | # 137 | :=: c 'bar' 138 | 3 139 | :be {1: 'one', 140 | 2: {'foo': 'spam'}, 141 | 3: {'bar': 'eggs'}} 142 | [a, b, c] 143 | def: .test_too_many: self 144 | self.assertEqual: 145 | [1, 2, 3] 146 | !let: 147 | :,: a b c 148 | :be [1, 2, 3, 4, 5] 149 | [a, b, c] 150 | def: .test_as: self 151 | self.assertEqual: 152 | [[3,7], 3, 7, 'xy', 'x', 'y'] 153 | !let: 154 | :,: 155 | :,: x1 y1 :as point1 156 | :,: x2 y2 :as point2 157 | :be [[3, 7], 'xy'] 158 | [point1, x1, y1, point2, x2, y2] 159 | def: .test_mapping_as: self 160 | self.assertEqual: 161 | ['spam', {1:'spam'}] 162 | !let: 163 | :=: s 1 :as d 164 | :be {1:'spam'} 165 | [s, d] 166 | def: .test_nested_mixed: self 167 | self.assertEqual: 168 | ['a', 'b', 'ab'] 169 | !let: 170 | :=: 171 | :,: a b :as s 172 | 1 173 | :be {1:'ab'} 174 | [a, b, s] 175 | self.assertEqual: 176 | [[{1:'ab'}], 'ab'] 177 | !let: 178 | :,: 179 | :=: s 1 180 | :as d 181 | :be [{1:'ab'}] 182 | [d, s] 183 | self.assertEqual: 184 | ['a', 'b', [{1:'ab'}], 'ab'] 185 | !let: 186 | :,: 187 | :=: 188 | :,: a b :as s 189 | 1 190 | :as d 191 | :be [{1:'ab'}] 192 | [a, b, d, s] 193 | self.assertEqual: 194 | 'spam' 195 | !let: 196 | :=: 197 | :,: :=: s 2 198 | 1 199 | :be {1:[{2:'spam'}]} 200 | s 201 | def: .test_strs: self 202 | self.assertEqual: 203 | [1, 2, 3] 204 | !let: 205 | :=: :strs: a b c 206 | :be {'a':1, 'b':2, 'c':3} 207 | [a, b, c] 208 | def: .test_default: self 209 | self.assertEqual: 210 | ['A','b','C','d'] 211 | !let: 212 | :=: a 1 b 2 c 3 d 4 :default: a 'A' b 'X' c 'C' 213 | :be {2:'b', 4:'d'} 214 | [a,b,c,d] 215 | def: .test_default_strs: self 216 | self.assertEqual: 217 | ['ab', 22, 33] 218 | !let: 219 | :=: 220 | :strs: a b c 221 | :default: a ('a'+'b') 222 | :be {'b':22,'c':33} 223 | [a, b, c] 224 | 225 | class: TestLoop: TestCase 226 | def: .test_loop: self 227 | self.assertEqual: 228 | 'cba' 229 | !loop: recur: xs 'abc' ys '' 230 | if: xs 231 | :then: recur: (xs[:-1]) (ys + xs[-1]) 232 | :else: ys 233 | def: .test_try_iter: self 234 | !let: xs :be [] 235 | !loop: recur: it iter:'abc' 236 | try: .append: xs .upper:next:it 237 | :except: StopIteration 238 | :else: recur: it 239 | self.assertEqual: 240 | ['A', 'B', 'C'] 241 | xs 242 | def: .test_for: self 243 | !let: xs :be [] 244 | for: c :in 'abc' 245 | .append: xs .upper: c 246 | self.assertEqual: 247 | ['A', 'B', 'C'] 248 | xs 249 | def: .test_for_bind: self 250 | !let: xs :be [] 251 | for: 252 | :,: c i 253 | :in zip: 'abc' [1,2,3] 254 | xs.append:(c*i) 255 | self.assertEqual: 256 | ['a','bb','ccc'] 257 | xs 258 | def: .test_break: self 259 | !let: cs :be iter:'abcdefg' 260 | self.assertEqual: 261 | 'c' 262 | for: c :in cs 263 | if: (c=='c') :then: break: c 264 | self.assertEqual: 265 | ['d', 'e', 'f', 'g'] 266 | list:cs 267 | self.assertEqual: 268 | None 269 | for: c :in 'abc' 270 | if: (c=='c') :then: break: 271 | def: .test_labeled_break: self 272 | !let: ijs :be [] 273 | self.assertEqual: 274 | 1 275 | for: :top i :in [0,1,2,3] 276 | for: j :in [2,1] 277 | ijs.append: [i, j] 278 | if: (i==j) :then: break: :top i 279 | self.assertEqual: 280 | [[0, 2], 281 | [0, 1], 282 | [1, 2], 283 | [1, 1]] 284 | ijs 285 | def: .test_continue: self 286 | !let: ijs :be [] 287 | self.assertEqual: 288 | () 289 | for: i :in [1,2,3] 290 | for: j :in [1,2,3] 291 | if: (i==j) :then: continue: 292 | ijs.append: [i, j] 293 | self.assertEqual: 294 | [[1, 2], 295 | [1, 3], 296 | [2, 1], 297 | [2, 3], 298 | [3, 1], 299 | [3, 2]] 300 | ijs 301 | def: .test_labeled_continue: self 302 | !let: ijs :be [] 303 | self.assertEqual: 304 | () 305 | for: :top i :in [1,2,3] 306 | for: j :in [1,2,3] 307 | if: (i==j) :then: continue: :top 308 | ijs.append: [i, j] 309 | self.assertEqual: 310 | [[2, 1], 311 | [3, 1], 312 | [3, 2]] 313 | ijs 314 | def: .test_for_else: self 315 | self.assertEqual: 316 | 'c' 317 | for: c :in 'abcdefg' 318 | if: (c=='c') :then: break: c 319 | :else: 'z' 320 | self.assertEqual: 321 | 'z' 322 | for: c :in 'abcdefg' 323 | if: (c=='C') :then: break: c 324 | :else: 'z' 325 | 326 | class: TestDef: TestCase 327 | def: .test_def_ns: self 328 | """How to emulate local reassignment.""" 329 | !let: o :be types..SimpleNamespace: 330 | def: o.foo 2 331 | def: o.foo (o.foo * 3) 332 | self.assertEqual: 333 | 6 334 | o.foo 335 | def: .test_def_anaphoric_ns: self 336 | !let: _ns_ :be types..SimpleNamespace: 337 | def: .foo 2 338 | def: .foo (_ns_.foo * 3) 339 | self.assertEqual: 340 | 6 341 | _ns_.foo 342 | 343 | class: TestBegin: TestCase 344 | "Test Begin Docstring." 345 | def: .test_begin: self 346 | !let: xs :be [] 347 | self.assertEqual: 348 | 3 349 | !begin: 350 | xs.append: 1 351 | xs.append: 2 352 | 3 353 | self.assertEqual: xs [1, 2] 354 | def: .test_begin_empty: self 355 | self.assertEqual: 356 | () 357 | !begin: 358 | def: .test_begin0: self 359 | !let: xs :be [] 360 | self.assertEqual: 361 | 0 362 | !begin0: 363 | 0 364 | xs.append: 1 365 | xs.append: 2 366 | self.assertEqual: xs [1, 2] 367 | 368 | class: TestClass: TestCase 369 | """Test doc""" 370 | def: .test_module: self 371 | self.assertEqual: 372 | 'tests.native_hebi_tests.test_native' 373 | self.__module__ 374 | def: .test_qualname: self 375 | self.assertEqual: 376 | 'TestClass' 377 | self.__class__.__qualname__ 378 | def: .test_method_qualname: self 379 | self.assertEqual: 380 | 'TestClass.test_method_qualname' 381 | self.test_method_qualname.__qualname__ 382 | def: ._y_watch: cls 383 | def: cls.watch (cls.watch + "y") 384 | cls 385 | class: .Spam: 386 | :@ ._y_watch 387 | :@ lambda: cls: 388 | def: cls.watch "x" 389 | cls 390 | def: .eggs: self 391 | !let: o :be types..SimpleNamespace: 392 | class: o.Bacon: 393 | o.Bacon 394 | def: .test_nested_qualname: self 395 | self.assertEqual: 396 | 'TestClass.Spam' 397 | self.Spam.__qualname__ 398 | def: .test_nested_method_qualname: self 399 | self.assertEqual: 400 | 'TestClass.Spam.eggs' 401 | self.Spam.eggs.__qualname__ 402 | def: .test_class_in_method_qualname: self 403 | self.assertEqual: 404 | 'o.Bacon' 405 | # (self.Spam().eggs().__qualname__) 406 | !of: .__qualname__ .eggs: .Spam: self 407 | def: .test_doc: self 408 | self.assertEqual: 409 | """Test doc""" 410 | self.__doc__ 411 | def: .test_decorator_order: self 412 | self.assertEqual: 413 | "xy" 414 | self.Spam.watch 415 | 416 | # test anaphoric _ns_ decorator factory 417 | # test metaclass with kwargs 418 | # test __doc__ not set in metaclass if not present 419 | def: .test_super: self 420 | !let: o :be types..SimpleNamespace: 421 | class: o.Spam: 422 | def: .__init__: self 423 | super: 424 | None 425 | o.Spam: 426 | 427 | class: TestOf: TestCase 428 | def: .test_symbol: self 429 | !let: o :be types..SimpleNamespace: : a 1 b 2 430 | self.assertEqual: 1 !of: .a o 431 | self.assertEqual: 2 !of: .b o 432 | def: .test_key: self 433 | !let: d :be {'.a':1, 10:2} 434 | self.assertEqual: 1 !of: '.a' d 435 | self.assertEqual: 2 !of: 10 d 436 | def: .test_chained: self 437 | self.assertEqual: 438 | 42 439 | # types.SimpleNamespace(attr=[0, {'key': -42}]).attr[1]['key'].__neg__() 440 | .__neg__:!of: 'key' 1 .attr types..SimpleNamespace: : attr [0, {'key': -42}] 441 | def: .test_identifier: self 442 | self.assertEqual: 443 | 3.0 444 | !let: i :be 2 445 | !of: .imag i [1j,2j,3j] 446 | 447 | class: TestAttach: TestCase 448 | def: .test_attach: self 449 | !let: :,: foo bar baz spam eggs o 450 | :be hebi.bootstrap..entuple: 1 2 3 4 5 types..SimpleNamespace: 451 | self.assertIs: 452 | o 453 | !attach: o foo bar baz : quux spam norlf eggs 454 | self.assertEqual: 455 | types..SimpleNamespace: : foo 1 bar 2 baz 3 quux 4 norlf 5 456 | o 457 | 458 | class: TestDel: TestCase 459 | def: .setUp: self 460 | !let: _ns_ :be self 461 | def: .delete_me 'foo delete' 462 | def: DELETE_ME 'bar delete' 463 | def: self.o types..SimpleNamespace: 464 | def: self.o.foo 2 465 | def: .test_del_ns: self 466 | self.assertIn: 'delete_me' vars:self 467 | !let: _ns_ :be self 468 | del: .delete_me 469 | self.assertNotIn: 'delete_me' vars:self 470 | def: .test_del_global: self 471 | self.assertIn: 'DELETE_ME' globals: 472 | del: DELETE_ME 473 | self.assertNotIn: 'DELETE_ME' globals: 474 | def: .test_del_attr: self 475 | !let: o :be types..SimpleNamespace: : a 1 476 | self.assertIn: 'a' vars:o 477 | del: o.a 478 | self.assertNotIn: 'a' vars:o 479 | def: .test_del_empty: self 480 | del: 481 | def: .test_del_multiple: self 482 | !let: :,: _ns_ o 483 | :be !mask:pass: :,:self :,:types..SimpleNamespace: : a 1 b 2 c 3 484 | del: DELETE_ME .delete_me o.a 485 | self.assertNotIn: 'a' vars:o 486 | self.assertEqual: 487 | types..SimpleNamespace: : b 2 c 3 488 | o 489 | self.assertNotIn: 'delete_me' vars:self 490 | self.assertNotIn: 'DELETE_ME' globals: 491 | def: .test_del_nested: self 492 | self.assertEqual: 493 | types..SimpleNamespace: : foo 2 494 | self.o 495 | del: self.o.foo 496 | self.assertEqual: 497 | types..SimpleNamespace: 498 | self.o 499 | 500 | class: TestNative: TestCase 501 | def: .test_mask: self 502 | self.assertEqual: 503 | quote:pass:lambda: pass: : foo spam 504 | builtins..print: "Hi!" 505 | tests.native_hebi_tests.test_native..greet: "World!" 506 | frobnicate: foo 7 24 507 | reticulate: foo spline 508 | foo 509 | pass: # Example based on cascade macro. 510 | lambda: pass: thing thing_sym : :* calls 511 | !mask:pass:lambda: pass: : :,:thing_sym :,:thing 512 | print: "Hi!" # Builtin symbol. 513 | greet: "World!" # Local symbol. 514 | :,@:map: # Splice 515 | lambda: pass:call 516 | !mask:pass: # Nested !mask. 517 | :,:operator..getitem: call 0 518 | :,:thing_sym 519 | :,@:operator..getitem: call slice: 1 None 520 | calls 521 | :,:thing_sym 522 | : :* 523 | quote:pass: spam foo 524 | frobnicate: 7 24 525 | reticulate: spline 526 | def: .test_simple_double_unquote: self 527 | self.assertEqual: 528 | 1 529 | !let: a :be 1 530 | !mask:!mask::,::,:a 531 | def: .test_double_unquote: self 532 | self.assertEqual: 533 | quote:pass: 534 | hebi.bootstrap..entuple: : :? 1 :? 2 535 | 3 536 | !let: a :be 1 537 | !mask:pass: 538 | !mask:pass: :,::,:a 2 539 | 3 540 | def: .test_simple_alternate_double_unquote: self 541 | self.assertEqual: 542 | 2 543 | !let: a :be 2 544 | !mask::,:!mask::,:a 545 | def: .test_alternate_double_unquote: self 546 | self.assertEqual: 547 | quote:pass: 0 pass: 1 2 548 | !let: a :be 2 549 | !mask:pass: 550 | 0 551 | :,:!mask:pass: 1 :,:a 552 | def: .test_simple_double_mask_unquote: self 553 | self.assertEqual: 554 | quote:builtins..print 555 | !let: print :be 3 556 | !mask:!mask::,:print 557 | def: .test_double_mask_unquote: self 558 | self.assertEqual: 559 | quote:pass: 560 | 0 561 | hebi.bootstrap..entuple: 562 | : :? 1 563 | :? 2 564 | :? builtins..print 565 | !let: print :be 3 566 | !mask:pass: 567 | 0 568 | !mask:pass: 569 | 1 570 | 2 571 | :,:print 572 | def: .test_elif: self 573 | self.assertEqual: 574 | quote:hebi.bootstrap.._if_: 575 | ('a'<'b') 576 | lambda: pass: 577 | print: "less" 578 | lambda: pass: 579 | ('a'>'b') 580 | lambda: pass: 581 | print: "more" 582 | lambda: pass: 583 | ('a'=='b') 584 | lambda: pass: 585 | print: "equal" 586 | : else_ 587 | lambda: pass: 588 | print: "nan" 589 | hebi.bootstrap..if_: 590 | : :* quote:pass: 591 | ('a'<'b') 592 | :then: 593 | print: "less" 594 | :elif: ('a'>'b') 595 | print: "more" 596 | :elif: ('a'=='b') 597 | print: "equal" 598 | :else: 599 | print: "nan" 600 | 601 | 602 | # TODO: test try 603 | 604 | #def: __package__ "tests.native_hebi_tests" 605 | 606 | import: unittest 607 | 608 | if: (__name__ == "__main__") 609 | :then: 610 | # print: __package__ 611 | # from: . :import recompile 612 | .main: unittest 613 | # recompile: 614 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /src/hebi/bootstrap.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import ast 6 | import builtins 7 | import re 8 | from functools import wraps 9 | from itertools import islice, zip_longest, chain, takewhile 10 | from types import new_class 11 | 12 | from hissp.compiler import NS 13 | 14 | from hebi.parser import QUALSYMBOL 15 | 16 | BOOTSTRAP = 'hebi.bootstrap..' 17 | 18 | 19 | def _thunk(*args): 20 | return ('lambda', (), *args) 21 | 22 | 23 | def _and_(expr, *thunks): 24 | result = expr 25 | for thunk in thunks: 26 | if not result: 27 | break 28 | result = thunk() 29 | return result 30 | 31 | 32 | def and_(*args): 33 | if args: 34 | if len(args) == 1: 35 | return args[0] 36 | return (BOOTSTRAP + '_and_', args[0], *( 37 | _thunk(arg) for arg in args[1:] 38 | )) 39 | return True 40 | 41 | 42 | def _or_(expr, *thunks): 43 | result = expr 44 | for thunk in thunks: 45 | if result: 46 | break 47 | result = thunk() 48 | return result 49 | 50 | 51 | def or_(*args): 52 | if args: 53 | if len(args) == 1: 54 | return args[0] 55 | return (BOOTSTRAP + '_or_', args[0], *( 56 | _thunk(arg) for arg in args[1:] 57 | )) 58 | return () 59 | 60 | 61 | def _not_(b): 62 | return True if b else () 63 | 64 | 65 | def not_(expr): 66 | return BOOTSTRAP + '_not_', expr 67 | 68 | 69 | def _qualname(ns, name): 70 | if hasattr(ns, '__qualname__'): 71 | name = name.partition('.')[-1] 72 | name = f"{ns.__qualname__}.{name}" 73 | return name 74 | 75 | 76 | def def_(name, *body): 77 | """ 78 | Assigns a global value or function in the current module. 79 | """ 80 | if type(name) is tuple: 81 | args, decorators, doc, ibody, name = destructure_decorators(name, body) 82 | qualname = ('quote', name,) 83 | if name.startswith('_ns_.'): 84 | qualname = (BOOTSTRAP + '_qualname', '_ns_', ('quote', name),) 85 | return ( 86 | 'hebi.basic.._macro_.def_', 87 | name, 88 | _decorate( 89 | decorators, 90 | (BOOTSTRAP + 'function', 91 | qualname, 92 | ('lambda', tuple(args), *ibody), 93 | doc,), 94 | ), 95 | ) 96 | if len(body) == 1: 97 | name = _expand_ns(name) 98 | if '.' in name: 99 | ns, attr = name.rsplit('.', 1) 100 | return ( 101 | 'builtins..setattr', 102 | ns, 103 | ('quote', attr), 104 | body[0], 105 | ) 106 | return ( 107 | '.__setitem__', 108 | ('builtins..globals',), 109 | ('quote', name,), 110 | body[0] 111 | ) 112 | raise SyntaxError 113 | 114 | 115 | def class_(name, *body): 116 | args, decorators, doc, ibody, name = destructure_decorators(name, body) 117 | ns = [('lambda', (), '_ns_')] if name.startswith('_ns_.') else () 118 | return ( 119 | 'hebi.basic.._macro_.def_', 120 | name, 121 | _decorate( 122 | decorators, 123 | (BOOTSTRAP + '_class_', 124 | ('quote', name), 125 | (BOOTSTRAP + 'akword', *args), 126 | doc, 127 | ('globals',), 128 | # new_class() isn't setting the __class__ cell for super() for some reason. 129 | # So we need to make one here. 130 | ('lambda',('__class__',), 131 | ('lambda',('_ns_',), 132 | # Make sure __class__ appears in __closure__, but don't actually use it. 133 | '(None and __class__)', 134 | *ibody),), 135 | *ns,), 136 | ) 137 | ) 138 | 139 | 140 | def destructure_decorators(name, body): 141 | name, *args = name 142 | doc = None 143 | decorators = [] 144 | ibody = iter(body) 145 | for expr in ibody: 146 | if expr == ":@": 147 | decorators.append(next(ibody)) 148 | continue 149 | if _is_str(expr): 150 | doc = expr 151 | else: 152 | ibody = expr, *ibody 153 | break 154 | name = _expand_ns(name) 155 | return args, decorators, doc, ibody, name 156 | 157 | 158 | def akword(*args, **kwargs): 159 | return args, kwargs 160 | 161 | 162 | def _class_(name, args, doc, module, wrapped_callback, ns=None): 163 | class_cell, callback = _class_cell(wrapped_callback) 164 | __qualname__ = name 165 | if ns and hasattr(ns(), '__qualname__'): 166 | name = name.partition('.')[-1] 167 | __qualname__ = f"{ns().__qualname__}.{name}" 168 | name = name.split('.')[-1] 169 | 170 | def exec_callback(ns): 171 | ns['__module__'] = module['__name__'] 172 | ns['__qualname__'] = __qualname__ 173 | if doc is not None: 174 | ns['__doc__'] = doc 175 | callback(attrs(ns)) 176 | return ns 177 | 178 | bases, kwds = args 179 | cls = new_class(name, bases, kwds, exec_callback) 180 | # super() needs this. 181 | class_cell.cell_contents = cls 182 | return cls 183 | 184 | 185 | def _class_cell(wrapped_callback): 186 | sentinel = object() 187 | callback = wrapped_callback(sentinel) 188 | # new_class() isn't setting the __class__ cell for super() for some reason. 189 | class_cell = dict(zip(callback.__code__.co_freevars, callback.__closure__))['__class__'] 190 | # Not sure if it's always this easy to find the right cell. 191 | assert class_cell.cell_contents is sentinel 192 | # Empty the cell so we get the proper error in case it's used too soon. 193 | del class_cell.cell_contents 194 | return class_cell, callback 195 | 196 | 197 | def _decorate(decorators, callable): 198 | for decorator in reversed(decorators): 199 | callable = _expand_ns(decorator), callable 200 | return callable 201 | 202 | 203 | def _expand_ns(name): 204 | if type(name) is tuple: 205 | return (_expand_ns(name[0]), *name[1:]) 206 | if type(name) is str and name.startswith('.'): 207 | name = '_ns_' + name 208 | return name 209 | 210 | 211 | def _is_str(s): 212 | if type(s) is str: 213 | try: 214 | return type(ast.literal_eval(s)) is str 215 | except: 216 | pass 217 | 218 | 219 | def function(qualname, fn, doc=None, annotations=None, dict_=()): 220 | """Enhances a Hissp lambda with function metadata. 221 | 222 | Replaces __code__ with co_name set to name. 223 | Assigns __doc__, __name__, __qualname__, and __annotations__. 224 | Then updates __dict__. 225 | """ 226 | name = qualname.split('.')[-1] 227 | fn.__code__ = fn.__code__.replace(co_name=name) 228 | fn.__doc__ = doc 229 | fn.__name__ = name 230 | fn.__qualname__ = qualname 231 | fn.__annotations__ = annotations or {} 232 | fn.__dict__.update(dict_) 233 | return fn 234 | 235 | 236 | def import_(*specs): 237 | pairs = [] 238 | specs = iter(specs) 239 | for spec in specs: 240 | if spec != ":as": 241 | pairs.append([spec.split('.')[0], spec]) 242 | else: 243 | pairs[-1][0] = next(specs) 244 | return (_thunk( 245 | *(('.__setitem__', 246 | ('builtins..globals',), 247 | ('quote',k), 248 | ('__import__', 249 | ('quote',v), 250 | ('builtins..globals',), 251 | ':','fromlist',':',),) 252 | for k, v in pairs),),) 253 | 254 | 255 | def from_(name, import_, *specs): 256 | if import_ != ':import': 257 | raise SyntaxError 258 | fromlist = [] 259 | pairs = [] 260 | specs = iter(specs) 261 | for spec in specs: 262 | if spec != ":as": 263 | pairs.extend([spec, 'module.' + spec]) 264 | fromlist.append(spec) 265 | else: 266 | pairs[-2] = next(specs) 267 | sname = name.lstrip('.') 268 | return (('lambda',('module',), 269 | ('.update', 270 | ('builtins..globals',), 271 | ('dict',':',*pairs),),), 272 | ('__import__', 273 | ('quote',sname), 274 | ':', 275 | 'globals',('builtins..globals',), 276 | 'fromlist',fromlist, 277 | 'level',len(name)-len(sname),),) 278 | 279 | 280 | def _if_(b, thunk, *elifs, else_=lambda:()): 281 | if b: 282 | return thunk() 283 | elifs = iter(elifs) 284 | for elif_ in elifs: 285 | thunk = next(elifs) 286 | if elif_(): 287 | return thunk() 288 | return else_() 289 | 290 | 291 | def if_(condition, then, *pairs): 292 | """ 293 | if: (ab) 297 | print: "more" 298 | :elif: (a==b) 299 | print: "equal" 300 | :else: 301 | print: "nan" 302 | """ 303 | 304 | else_ = () 305 | if pairs and pairs[-1][0] == ':else': 306 | *pairs, else_ = pairs 307 | else_ = [ 308 | ':','else_',_thunk(*else_[1:]) 309 | ] 310 | 311 | elifs = [] 312 | for pair in pairs: 313 | if pair[0] != ':elif': 314 | raise SyntaxError(pair[0]) 315 | elifs.extend([ 316 | _thunk(pair[1],), 317 | _thunk(*pair[2:],) 318 | ]) 319 | 320 | if then[0] != ':then': 321 | raise SyntaxError(then) 322 | 323 | return ( 324 | BOOTSTRAP + '_if_', 325 | condition, 326 | _thunk(*then[1:]), 327 | *elifs, 328 | *else_, 329 | ) 330 | 331 | 332 | _sentinel = object() 333 | 334 | 335 | def _raise_(): 336 | raise 337 | 338 | 339 | def _raise_ex(ex): 340 | raise ex 341 | 342 | 343 | def _raise_ex_from(ex, from_): 344 | raise ex from from_ 345 | 346 | 347 | def raise_(ex=None, key=_sentinel, from_=_sentinel): 348 | if ex: 349 | if key is not _sentinel: 350 | if key == ':from': 351 | return BOOTSTRAP + '_raise_ex_from', ex, from_, 352 | else: 353 | raise SyntaxError(key) 354 | return BOOTSTRAP + '_raise_ex', ex 355 | return BOOTSTRAP + '_raise' 356 | 357 | 358 | def partition(iterable, n=2, step=None, fillvalue=_sentinel): 359 | """ 360 | Chunks iterable into tuples of length n. (default pairs) 361 | >>> list(partition(range(10))) 362 | [(0, 1), (2, 3), (4, 5), (6, 7), (8, 9)] 363 | 364 | The remainder, if any, is not included. 365 | >>> list(partition(range(10), 3)) 366 | [(0, 1, 2), (3, 4, 5), (6, 7, 8)] 367 | 368 | Keep the remainder by using a fillvalue. 369 | >>> list(partition(range(10), 3, fillvalue=None)) 370 | [(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, None, None)] 371 | >>> list(partition(range(10), 3, fillvalue='x')) 372 | [(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, 'x', 'x')] 373 | 374 | The step defaults to n, but can be more to skip elements. 375 | >>> list(partition(range(10), 2, 3)) 376 | [(0, 1), (3, 4), (6, 7)] 377 | 378 | Or less for a sliding window with overlap. 379 | >>> list(partition(range(5), 2, 1)) 380 | [(0, 1), (1, 2), (2, 3), (3, 4)] 381 | """ 382 | step = step or n 383 | slices = (islice(iterable, start, None, step) for start in range(n)) 384 | if fillvalue is _sentinel: 385 | return zip(*slices) 386 | else: 387 | return zip_longest(*slices, fillvalue=fillvalue) 388 | 389 | 390 | def _try_(thunk, *except_, else_=None, finally_=lambda:()): 391 | if not all(isinstance(x, tuple) 392 | or issubclass(x, BaseException) 393 | for x, c in partition(except_)): 394 | raise TypeError 395 | try: 396 | res = thunk() 397 | except BaseException as ex: 398 | for ex_type, ex_handler in partition(except_): 399 | if isinstance(ex, ex_type): 400 | return ex_handler(ex) 401 | else: 402 | raise 403 | else: 404 | if else_: 405 | res = else_() 406 | finally: 407 | finally_() 408 | return res 409 | 410 | 411 | def try_(expr, *handlers): 412 | """ 413 | try: 414 | !begin: 415 | print: "It's dangerous!" 416 | something_risky: thing 417 | :except: LikelyProblemError 418 | print: "Oops!" 419 | fix_it: 420 | :except: Exception :as ex 421 | do_something: ex 422 | :else: 423 | print: "Hooray!" 424 | thing 425 | :finally: 426 | .close: thing 427 | """ 428 | else_ = () 429 | finally_ = () 430 | except_ = [] 431 | for handler in handlers: 432 | if handler[0] == ':except': 433 | if len(handler) > 3 and handler[2] == ':as': 434 | arg = handler[3] 435 | block_start = 4 436 | else: 437 | arg = 'xAUTO0_' 438 | block_start = 2 439 | except_.extend([handler[1], ('lambda',(arg,),*handler[block_start:],)]) 440 | elif handler[0] == ':else': 441 | if else_: 442 | raise SyntaxError(handler) 443 | else_ = 'else_', _thunk(*handler[1:]), 444 | elif handler[0] == ':finally': 445 | if finally_: 446 | raise SyntaxError(handler) 447 | finally_ = 'finally', _thunk(*handler[1:]), 448 | else: 449 | raise SyntaxError(handler) 450 | return (BOOTSTRAP + '_try_', _thunk(expr), *except_, ':', *else_, *finally_,) 451 | 452 | 453 | def mask(form): 454 | case = type(form) 455 | if case is tuple and form: 456 | if _is_str(form): 457 | return 'quote', form 458 | if form[0] == ':,': 459 | return form[1] 460 | if form[0] == 'hebi.basic.._macro_.mask': 461 | return mask(mask(form[1])) 462 | return ( 463 | BOOTSTRAP + 'entuple', ':', *chain.from_iterable(_mask(form)), 464 | ) 465 | if case is str and not form.startswith(':'): 466 | return 'quote', _qualify(form) 467 | return form 468 | 469 | 470 | def _mask(forms): 471 | for form in forms: 472 | case = type(form) 473 | if case is str and not form.startswith(':'): 474 | yield ':?', ('quote', _qualify(form)) 475 | elif case is tuple and form: 476 | if form[0] == ':,': 477 | yield ':?', form[1] 478 | elif form[0] == ':,@': 479 | yield ':*', form[1] 480 | elif form[0] == 'hebi.basic.._macro_.mask': 481 | yield ':?', mask(mask(form[1])) 482 | else: 483 | yield ':?', mask(form) 484 | else: 485 | yield ':?', form 486 | 487 | 488 | def _qualify(symbol): 489 | if symbol.startswith('('): 490 | return symbol 491 | if symbol in {e for e in dir(builtins) if not e.startswith('_')}: 492 | return f'builtins..{symbol}' 493 | if re.search(r"\.\.|^\.|^quote$|^lambda$|xAUTO\d+_$", symbol): 494 | return symbol 495 | qualname = QUALSYMBOL.get() 496 | if qualname: 497 | if symbol in vars(NS.get().get("_macro_", lambda: ())): 498 | return f"{qualname}.._macro_.{symbol}" 499 | return f"{qualname}..{symbol}" 500 | return symbol 501 | 502 | 503 | def _begin(*body): 504 | return body[-1] 505 | 506 | 507 | def begin(*body): 508 | case = len(body) 509 | if case == 0: 510 | return () 511 | if case == 1: 512 | return body[0] 513 | return (BOOTSTRAP + '_begin', *body) 514 | 515 | 516 | def _begin0(zero, *body): 517 | return zero 518 | 519 | 520 | def begin0(*body): 521 | if len(body) == 1: 522 | return body[0] 523 | return (BOOTSTRAP + '_begin0', *body) 524 | 525 | 526 | def _with_(guard, body): 527 | with guard() as g: 528 | return body(g) 529 | 530 | 531 | def with_(guard, *body): 532 | """ 533 | with: foo:bar :as baz 534 | frobnicate: baz 535 | """ 536 | if len(body) > 2 and body[1] == ':as': 537 | return BOOTSTRAP + '_with_', _thunk(guard), ('lambda',(body[2],),*body[3:]), 538 | return BOOTSTRAP + '_with_', _thunk(guard), ('lambda',('xAUTO0_',),*body), 539 | 540 | 541 | def _assert_(b): 542 | assert b 543 | 544 | 545 | def _assert_message(b, thunk): 546 | assert b, thunk() 547 | 548 | 549 | def assert_(b, *message): 550 | if message: 551 | return BOOTSTRAP + '_assert_message', b, _thunk(*message) 552 | return BOOTSTRAP + '_assert_', b 553 | 554 | 555 | def _flatten_tuples(expr): 556 | if expr[0] == ':=': 557 | yield from _flatten_mapping(iter(expr)) 558 | else: 559 | for e in expr: 560 | if type(e) is tuple: 561 | yield from _flatten_tuples(e) 562 | elif ( 563 | type(e) is str 564 | and e != '_' 565 | and not e.startswith('(') 566 | and not e.startswith(':') 567 | ): 568 | yield e 569 | 570 | 571 | def _flatten_mapping(expr): 572 | head = next(expr) 573 | assert head == ':=' 574 | for e in expr: 575 | if type(e) is tuple: 576 | if e[0] == ':default': 577 | continue 578 | if e[0] == ':strs': 579 | yield from _flatten_tuples(e) 580 | continue 581 | if e == ':as': 582 | yield next(expr) 583 | continue 584 | yield from _flatten_tuples(e) 585 | next(expr) 586 | 587 | 588 | def _unpack(target, value): 589 | if type(target) is tuple and target: 590 | if target[0] == ':,': 591 | yield from _unpack_iterable(target, value) 592 | if target[0] == ':=': 593 | yield from _unpack_mapping(target, value) 594 | elif target == '_': 595 | pass 596 | else: 597 | yield value 598 | 599 | 600 | def _unpack_iterable(target, value): 601 | ivalue = iter(value) 602 | itarget = iter(target) 603 | head = next(itarget) 604 | assert head == ':,' 605 | for t in itarget: 606 | if t == ':list': 607 | yield from _unpack(next(itarget), list(ivalue)) 608 | elif t == ':iter': 609 | yield from _unpack(next(itarget), ivalue) 610 | elif t == ':as': 611 | yield from _unpack(next(itarget), value) 612 | else: 613 | yield from _unpack(t, next(ivalue)) 614 | 615 | 616 | def _unpack_mapping(target, value): 617 | itarget = iter(target) 618 | head = next(itarget) 619 | assert head == ':=' 620 | for t in target: 621 | if type(t) is tuple and t[0] == ':default': 622 | default = dict(partition(t[1:])) 623 | break 624 | else: 625 | default = {} 626 | for t in itarget: 627 | if t == ':as': 628 | next(itarget) 629 | yield value 630 | elif type(t) is tuple and t[0] == ':strs': 631 | for s in t[1:]: 632 | try: 633 | yield value[s] 634 | except LookupError: 635 | yield default[s] 636 | elif type(t) is tuple and t[0] == ':default': 637 | continue 638 | else: 639 | try: 640 | yield from _unpack(t, value[next(itarget)]) 641 | except LookupError: 642 | yield default[t] 643 | 644 | 645 | def _quote_tuple(target): 646 | head = next(target) 647 | yield 'quote', head 648 | for t in target: 649 | if type(t) is tuple: 650 | if head == ':=' and t[0] == ':strs': 651 | yield 'quote', t 652 | elif head == ':=' and t[0] == ':default': 653 | yield _quote_target(t) 654 | else: 655 | yield _quote_target(t) 656 | if head == ':=': 657 | yield next(target) 658 | elif head in ':=' and type(t) is str and t.startswith(':'): 659 | yield t 660 | t = next(target) 661 | if type(t) is tuple: 662 | yield _quote_target(t) 663 | else: 664 | yield 'quote', t 665 | else: 666 | yield 'quote', t 667 | if head in {':=', ':default'}: 668 | yield next(target) 669 | 670 | 671 | def _quote_target(target): 672 | return (BOOTSTRAP + 'entuple', *_quote_tuple(iter(target))) 673 | 674 | 675 | def let(target, be, value, *body): 676 | if be != ':be': 677 | raise SyntaxError('Missing :be in !let.') 678 | if type(target) is tuple: 679 | parameters = tuple(_flatten_tuples(target)) 680 | return ( 681 | ('lambda', parameters, *body), 682 | ':', ':*', (BOOTSTRAP + '_unpack', _quote_target(target), value,), 683 | ) 684 | return ('lambda',(target,),*body,), value, 685 | 686 | 687 | def entuple(*xs): 688 | return xs 689 | 690 | 691 | def _loop(f): 692 | again = False 693 | 694 | def recur(*args, **kwargs): 695 | nonlocal again 696 | again = True 697 | # The recursion thunk. 698 | return lambda: f(recur, *args, **kwargs) 699 | 700 | @wraps(f) 701 | def wrapper(*args, **kwargs): 702 | nonlocal again 703 | res = f(recur, *args, **kwargs) 704 | while again: 705 | again = False 706 | res = res() # when recur is called it must be returned! 707 | return res 708 | 709 | return wrapper 710 | 711 | 712 | def loop(start, *body): 713 | """ 714 | !loop: recur: xs 'abc' ys '' 715 | if: xs :then: recur(xs[:-1], ys+xs[-1]) 716 | :else: ys 717 | """ 718 | return ( 719 | BOOTSTRAP + '_loop', 720 | ('lambda',(start[0],':',*start[1:],), 721 | *body), 722 | ), 723 | 724 | 725 | class LabeledBreak(BaseException): 726 | def handle(self, label=None): 727 | """Re-raise self if label doesn't match.""" 728 | if self.label is None or self.label == label: 729 | return 730 | else: 731 | raise self 732 | 733 | def __init__(self, label=None): 734 | self.label = label 735 | raise self 736 | 737 | 738 | class LabeledResultBreak(LabeledBreak): 739 | def __init__(self, result=None, *results, label=None): 740 | if results: 741 | self.result = (result,) + results 742 | else: 743 | self.result = result 744 | LabeledBreak.__init__(self, label) 745 | 746 | 747 | class Break(LabeledResultBreak): 748 | pass 749 | 750 | 751 | def break_(*args): 752 | if args and args[0] and args[0].startswith(':'): 753 | return (BOOTSTRAP + 'Break', *args[1:], ':', 'label', args[0]) 754 | return (BOOTSTRAP + 'Break', *args) 755 | 756 | 757 | class Continue(LabeledBreak): 758 | pass 759 | 760 | 761 | def continue_(label=None): 762 | return BOOTSTRAP + 'Continue', label 763 | 764 | 765 | def _for_(iterable, body, else_=lambda:(), label=None): 766 | try: 767 | for e in iterable: 768 | try: 769 | body(e) 770 | except Continue as c: 771 | c.handle(label) 772 | except Break as b: 773 | b.handle(label) 774 | # skip else_() on Break 775 | return b.result 776 | return else_() 777 | 778 | 779 | def for_(*exprs): 780 | label = 'label', None, 781 | else_ = () 782 | if type(exprs[-1]) is tuple and exprs[-1] and exprs[-1][0] == ':else': 783 | else_ = 'else_', _thunk(*exprs[-1][1:]) 784 | exprs = exprs[:-1] 785 | iexprs = iter(exprs) 786 | if type(exprs[0]) is str and exprs[0].startswith(':'): 787 | label = 'label', exprs[0], 788 | next(iexprs) 789 | *bindings, = takewhile(lambda a: a != ':in', iexprs) 790 | iterable = next(iexprs) 791 | *body, = iexprs 792 | if body and type(body[-1]) is tuple and body[-1] and body[-1][0] == ':else': 793 | else_ = 'else_', body.pop()[1:] 794 | if type(bindings[0]) is str: 795 | body = (tuple(bindings), *body) 796 | else: 797 | body = ('xAUTO0_',), ('hebi.basic.._macro_.let', *bindings, ':be', 'xAUTO0_', *body), 798 | return ( 799 | BOOTSTRAP + '_for_', 800 | iterable, 801 | ('lambda', *body), 802 | ':', 803 | *else_, 804 | *label, 805 | ) 806 | 807 | 808 | def runtime(*forms): 809 | return ('hebi.basic.._macro_.if_', "(__name__!='')", 810 | (':then', *forms)) 811 | 812 | 813 | class attrs(object): 814 | """ 815 | Attribute view proxy of a mapping. 816 | 817 | Provides Lua-like syntactic sugar when the mapping has string 818 | keys that are also valid Python identifiers, which is a common 819 | occurrence in Python, for example, calling vars() on an object 820 | returns such a dict. 821 | 822 | Unlike a SimpleNamespace, an attrs proxy doesn't show the extra 823 | magic attrs from the class, and it can write through to any type of 824 | mapping. 825 | 826 | >>> spam = {} 827 | >>> atspam = attrs(spam) 828 | 829 | get and set string keys as attrs 830 | >>> atspam.one = 1 831 | >>> atspam.one 832 | 1 833 | >>> atspam 834 | attrs({'one': 1}) 835 | 836 | changes write through to underlying dict 837 | >>> spam 838 | {'one': 1} 839 | 840 | calling the object returns the underlying dict for direct access 841 | to all dict methods and syntax 842 | >>> list( 843 | ... atspam().items() 844 | ... ) 845 | [('one', 1)] 846 | >>> atspam()['one'] = 42 847 | >>> atspam() 848 | {'one': 42} 849 | 850 | del removes the key 851 | >>> del atspam.one 852 | >>> atspam 853 | attrs({}) 854 | """ 855 | 856 | __slots__ = "mapping" 857 | 858 | def __init__(self, mapping): 859 | object.__setattr__(self, "mapping", mapping) 860 | 861 | def __call__(self): 862 | return object.__getattribute__(self, "mapping") 863 | 864 | def __getattribute__(self, attr): 865 | try: 866 | return self()[attr] 867 | except KeyError as ke: 868 | raise AttributeError(*ke.args) 869 | 870 | def __setattr__(self, attr, val): 871 | self()[attr] = val 872 | 873 | def __delattr__(self, attr): 874 | try: 875 | del self()[attr] 876 | except KeyError as ke: 877 | raise AttributeError(*ke.args) 878 | 879 | def __repr__(self): 880 | return "attrs(" + repr(self()) + ")" 881 | 882 | 883 | def of(*exprs): 884 | *keys, collection = exprs 885 | for key in reversed(keys): 886 | if type(key) is str and key.startswith('.'): 887 | collection = ('builtins..getattr', collection, ('quote', key[1:]),) 888 | else: 889 | collection = ('operator..getitem', collection, key) 890 | return collection 891 | 892 | 893 | def _attach(target, **kwargs): 894 | for k, v in kwargs.items(): 895 | setattr(target, k, v) 896 | return target 897 | 898 | 899 | def attach(target, *args): 900 | iargs = iter(args) 901 | args = takewhile(lambda a: a!=':', iargs) 902 | return ( 903 | BOOTSTRAP + '_attach', 904 | target, 905 | ':', 906 | *chain.from_iterable((a, a) for a in args), 907 | *iargs, 908 | ) 909 | 910 | 911 | def del_(*args): 912 | return ( 913 | BOOTSTRAP + 'begin', 914 | *_del_(args), 915 | (), 916 | ) 917 | 918 | 919 | def _del_(args): 920 | for arg in args: 921 | if '.' in arg: 922 | if arg.startswith('.'): 923 | arg = '_ns_'+arg 924 | o, attr = arg.rsplit('.', 1) 925 | yield ('builtins..delattr', o, ('quote', attr,),) 926 | else: 927 | yield ('operator..delitem', ('globals',), ('quote', arg,),) 928 | --------------------------------------------------------------------------------