├── examples ├── out │ └── .gitkeep ├── __main__.py ├── helloworld.py ├── music.py ├── basics.py ├── branflakes.py ├── assets │ ├── scratchcat.svg │ └── beer.b └── all_supported_blocks.py ├── __init__.py ├── boiga ├── __init__.py ├── utils.py ├── expressions.py ├── ast.py ├── statements.py ├── codegen.py └── ast_core.py ├── LICENSE ├── .gitignore ├── README.md └── tools └── run_scratch.js /examples/out/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from .boiga import * 2 | -------------------------------------------------------------------------------- /boiga/__init__.py: -------------------------------------------------------------------------------- 1 | from .codegen import Project 2 | from .ast import * 3 | -------------------------------------------------------------------------------- /examples/__main__.py: -------------------------------------------------------------------------------- 1 | from . import helloworld 2 | from . import basics 3 | from . import branflakes 4 | from . import music 5 | from . import all_supported_blocks 6 | -------------------------------------------------------------------------------- /examples/helloworld.py: -------------------------------------------------------------------------------- 1 | from boiga import * 2 | 3 | project = Project() 4 | 5 | cat = project.new_sprite("Scratch Cat") 6 | cat.add_costume("scratchcat", "examples/assets/scratchcat.svg", center=(48, 50)) 7 | 8 | cat.on_flag([ 9 | Say("Hello, world!"), 10 | ]) 11 | 12 | project.save("examples/out/Boiga Examples: Hello World.sb3") 13 | -------------------------------------------------------------------------------- /examples/music.py: -------------------------------------------------------------------------------- 1 | from boiga import * 2 | 3 | project = Project() 4 | 5 | cat = project.new_sprite("Scratch Cat") 6 | cat.add_costume("scratchcat", "examples/assets/scratchcat.svg", center=(48, 50)) 7 | 8 | cat.on_flag([ 9 | SetTempo(50), 10 | ChangeTempoBy(10), 11 | SetInstrument(Instruments.ElectricPiano), 12 | PlayNote(60, 0.25), 13 | RestFor(0.5), 14 | PlayDrum(Drums.HandClap, 1), 15 | Say(GetTempo()), 16 | ]) 17 | 18 | project.save("examples/out/Boiga Examples: Music.sb3") 19 | -------------------------------------------------------------------------------- /boiga/utils.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | BLANK_SVG = b""" 4 | 5 | """ 6 | 7 | 8 | _SOUP = '!#%()*+,-./:;=?@[]^_`{|}~' + \ 9 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 10 | 11 | def gen_uid(seed): 12 | n = int.from_bytes(hashlib.sha256(repr(seed).encode()).digest(), "little") 13 | uid = "" 14 | for _ in range(20): 15 | uid += _SOUP[n % len(_SOUP)] 16 | n //= len(_SOUP) 17 | #return "".join(random.choices(_SOUP, k=20)) 18 | return uid 19 | 20 | 21 | # https://stackoverflow.com/a/12472564 22 | def flatten(S): 23 | if S == []: 24 | return S 25 | if isinstance(S[0], list): 26 | return flatten(S[0]) + flatten(S[1:]) 27 | return S[:1] + flatten(S[1:]) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 David Buchanan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/basics.py: -------------------------------------------------------------------------------- 1 | from boiga import * 2 | 3 | project = Project() 4 | 5 | cat = project.new_sprite("Scratch Cat") 6 | cat.add_costume("scratchcat", "examples/assets/scratchcat.svg", center=(48, 50)) 7 | 8 | my_variable = project.stage.new_var("my variable") 9 | var_foo = cat.new_var("foo", 123) 10 | stdout = project.stage.new_list("stdout", [], monitor=[0, 0, 480-2, 140]) 11 | 12 | 13 | hex_out = cat.new_list("hex_out") 14 | i = cat.new_var("i") 15 | tmp = cat.new_var("tmp") 16 | 17 | @cat.proc_def() 18 | def hex_decode(locals, hex_in): return [ 19 | hex_out.delete_all(), 20 | locals.i[:hex_in.len():2] >> [ 21 | hex_out.append( Literal("0x").join(hex_in[locals.i]).join(hex_in[locals.i+1]) + 0 ) 22 | ] 23 | ] 24 | 25 | @cat.proc_def("multiply [number a] with [number b]") 26 | def multiply_proc(locals, number_a, number_b): return [ 27 | locals.result <= number_a * number_b 28 | ] 29 | 30 | cat.on_flag([ 31 | stdout.delete_all(), 32 | stdout.append("Hello, world!"), 33 | multiply_proc(7, 9), 34 | stdout.append(multiply_proc.result), 35 | hex_decode("deadbeefcafebabe"), 36 | tmp <= "", 37 | i[:hex_out.len()] >> [ 38 | tmp <= tmp.join(hex_out[i]), 39 | If (i != hex_out.len() - 1) [ 40 | tmp <= tmp.join(", ") 41 | ] 42 | ], 43 | stdout.append(tmp), 44 | stdout.append(hex_out) 45 | ]) 46 | 47 | project.save("examples/out/Boiga Examples: Basics.sb3") 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | #lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | *.sb3 132 | DEBUG.json 133 | 134 | # npm noise 135 | node_modules 136 | package-lock.json 137 | package.json 138 | -------------------------------------------------------------------------------- /examples/branflakes.py: -------------------------------------------------------------------------------- 1 | from boiga import * 2 | 3 | ASCII_LUT = Literal("�"*0x1f + bytes(range(0x20, 0x7f)).decode()) 4 | 5 | project = Project() 6 | 7 | stdout = project.stage.new_list("stdout", [], monitor=[0, 0, 480-2, 292]) 8 | 9 | sprite = project.new_sprite("Branflakes") 10 | bfmem = sprite.new_list("bfmem") 11 | memptr = sprite.new_var("memptr") 12 | 13 | @sprite.proc_def() 14 | def putchar(locals, codepoint): return [ 15 | If (codepoint == 0x0a) [ 16 | stdout.append(locals.line_buffer), 17 | locals.line_buffer <= "" 18 | ].Else [ 19 | If (codepoint == 0x0d) [ 20 | locals.line_buffer <= "" 21 | ].Else [ 22 | locals.line_buffer <= locals.line_buffer.join(ASCII_LUT[codepoint-1]) 23 | ] 24 | ] 25 | ] 26 | 27 | def compile_bf(prog, is_subblock=False): 28 | compiled = [] 29 | i = 0 30 | ptrshift = 0 31 | while i < len(prog): 32 | if prog[i] in "+-": 33 | delta = 0 34 | while i < len(prog) and prog[i] in "+-": 35 | delta += 1 if prog[i] == "+" else -1 36 | i += 1 37 | compiled.append( 38 | bfmem[memptr+ptrshift] <= (bfmem[memptr+ptrshift] + delta) & 0xff 39 | ) 40 | elif prog[i] in "><": 41 | delta = 0 42 | while i < len(prog) and prog[i] in "><": 43 | delta += 1 if prog[i] == ">" else -1 44 | i += 1 45 | ptrshift += delta 46 | elif prog[i] == "[": 47 | depth = 0 48 | i += 1 49 | body = "" 50 | while i < len(prog): 51 | if prog[i] == "[": 52 | depth += 1 53 | elif prog[i] == "]": 54 | if depth: 55 | depth -= 1 56 | else: 57 | i += 1 58 | break 59 | body += prog[i] 60 | i += 1 61 | else: 62 | raise Exception("Unexpected EOF") 63 | 64 | compiled += [ 65 | memptr.changeby(ptrshift) if ptrshift else [], 66 | RepeatUntil (bfmem[memptr] == 0) [ 67 | compile_bf(body, is_subblock=True) 68 | ] 69 | ] 70 | ptrshift = 0 71 | 72 | elif prog[i] == "]": 73 | raise Exception("Unexpected ]") 74 | elif prog[i] == ",": 75 | raise Exception("Not implemented: ,") 76 | elif prog[i] == ".": 77 | compiled.append( 78 | putchar(bfmem[memptr+ptrshift]) 79 | ) 80 | i += 1 81 | else: 82 | i += 1 83 | 84 | if is_subblock and ptrshift: 85 | compiled.append( 86 | memptr.changeby(ptrshift) 87 | ) 88 | 89 | return compiled 90 | 91 | @sprite.proc_def(turbo=False) 92 | def bf_main(locals): 93 | return compile_bf(open("examples/assets/beer.b").read()) 94 | 95 | sprite.on_flag([ 96 | stdout.delete_all(), 97 | bfmem.delete_all(), 98 | Repeat (30000) [ 99 | bfmem.append(0) 100 | ], 101 | memptr <= 0, 102 | bf_main() 103 | ]) 104 | 105 | project.save("examples/out/Boiga Examples: Branflakes.sb3") 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # boiga 2 | 3 | Boiga is a Python library enabling ergonomic Scratch 3.0 code generation. 4 | 5 | Have you ever attempted to author advanced or accelerated algorithms in Scratch? It's not much fun - you might end up making miles of mouse movements for even the most miniscule of modifications. 6 | 7 | Boiga (ab)uses Python's operator overloading, to write Scratch expressions with intuitive(ish) Python syntax. We expose a simple AST representation, making it easy to write custom code generators and macros. 8 | 9 | See the examples directory for... examples. Further usage examples can be seen in my [Scratch Cryptography Library](https://github.com/DavidBuchanan314/scratch-cryptography-library). 10 | 11 | See also: [scratch-vscode](https://github.com/DavidBuchanan314/scratch-vscode) - A vscode extension that lets you preview Scratch projects right in your editor, with live reload. 12 | 13 | ## Project Status: 14 | 15 | It's a working prototype. Not all blocks are supported, but you can use this to write non-trivial scratch projects. 16 | If you plan to actually use this for something, you might want to make a copy of the code - I'll probably be making lots of breaking changes as I implement more features. 17 | 18 | ## Features: 19 | - Scratch .sb3 code generation. 20 | - Very basic AST optimisation (e.g. constant folding) 21 | - Optional inlining of "custom blocks". 22 | - Some bitwise operators. 23 | - 0-indexed lists. 24 | 25 | ## TODO 26 | - Documentation (sorry lol - for now, just look at the examples) 27 | - Static allocation of "sub-lists" within lists 28 | - Dynamic allocation of space within lists (i.e. malloc)? 29 | - For both of the above, we can pass indices around like pointers 30 | 31 | ## Building Examples: 32 | 33 | ```sh 34 | python3 -m examples 35 | ``` 36 | 37 | NOTE: Requires Python 3.10 or above! 38 | 39 | [`examples/helloworld.py`](https://github.com/DavidBuchanan314/boiga/blob/main/examples/helloworld.py) looks like this: 40 | 41 | ```python 42 | from boiga import * 43 | 44 | project = Project() 45 | 46 | cat = project.new_sprite("Scratch Cat") 47 | cat.add_costume("scratchcat", "examples/assets/scratchcat.svg", center=(48, 50)) 48 | 49 | cat.on_flag([ 50 | Say("Hello, world!"), 51 | ]) 52 | 53 | project.save("examples/out/Boiga Examples: Hello World.sb3") 54 | ``` 55 | 56 | Which compiles to the following scratch project: 57 | 58 | image 59 | 60 | Obviously, it's probably easier to write programs like that using the drag-and-drop interface. Conversely, [`examples/branflakes.py`](https://github.com/DavidBuchanan314/boiga/blob/main/examples/branflakes.py) implements a brainf\*ck to Scratch compiler, which compiles a "99 bottles of beer" program into the following: 61 | 62 | image 63 | 64 | This is just a preview, the whole script doesn't even come close to fitting in a single screenshot! You can see the full scratch code [here](https://scratch.mit.edu/projects/677776603/). 65 | -------------------------------------------------------------------------------- /tools/run_scratch.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Script taken from https://github.com/shinh/elvm/blob/master/tools/run_scratch.js 4 | 5 | // This script executes a Scratch 3.0 program that the Scratch backend generates. 6 | // To run this script, first install npm package 'scratch-vm' under 'tools' directory (here) 7 | // by the following command: 8 | // $ ls run_scratch.js # make sure you are in the same directory 9 | // run_scratch.js 10 | // $ npm install scratch-vm # this generates 'node_modules' on this directory. 11 | 12 | const fs = require('fs'); 13 | 14 | const readline = require('readline'); 15 | const VirtualMachine = require('scratch-vm'); 16 | const ScratchStorage = require('scratch-storage'); 17 | const storage = new ScratchStorage(); 18 | const vm = new VirtualMachine(); 19 | vm.attachStorage(storage); 20 | vm.start(); 21 | vm.clear(); 22 | vm.setCompatibilityMode(false); 23 | vm.setTurboMode(true); 24 | 25 | // Ignore warning / error messages 26 | require('minilog').disable(); 27 | 28 | // Replace special characters with '\dXXX' 29 | const special_chars = /([^ !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~])/g; 30 | //const inputStr = fs.readFileSync(0).toString(). 31 | // replace(special_chars, (_match, g, _offset, _str) => '\d' + g.charCodeAt(0).toString().padStart(3, '0')); 32 | 33 | // Input bytes from STDIN as a response to Scratch's ask block. 34 | // If the program requires more input, put EOF ('\0'). 35 | 36 | const rl = readline.createInterface({ 37 | input: process.stdin, 38 | output: process.stdout, 39 | }); 40 | 41 | vm.runtime.addListener('QUESTION', (q, r) => { 42 | if (q === null) 43 | return; 44 | rl.question("> ", ans => { 45 | //rl.close(); 46 | vm.runtime.emit('ANSWER', ans); 47 | }) 48 | }); 49 | 50 | // Get output bytes that the program wrote as contents of Scratch's list block, 51 | // then replace special characters with corresponding ones. 52 | // ('\dXXX' (decimal XXX) → a character with code point XXX) 53 | function printResult() { 54 | if (0) {// I patched scratch3_data.js to print on append to stdout 55 | const stdout_result = Object.values(vm.runtime.targets[0].variables).filter(v => v.name == "stdout")[0].value; 56 | const stdout_str = 57 | stdout_result.join('\n').replace(/\(\d{1,3})/g, 58 | (_match, g, _offset, _str) => String.fromCharCode(g)); 59 | process.stdout.write(stdout_str + "\n"); 60 | } 61 | process.nextTick(process.exit); 62 | }; 63 | 64 | // Count active threads and detect finish of execution. 65 | function whenThreadsComplete() { 66 | return new Promise((resolve, reject) => { 67 | setInterval(() => { 68 | let active = 0; 69 | const threads = vm.runtime.threads; 70 | for (let i = 0; i < threads.length; i++) { 71 | if (!threads[i].updateMonitor) { 72 | active += 1; 73 | } 74 | } 75 | if (active === 0) { 76 | resolve(); 77 | } 78 | }, 100); 79 | }) 80 | } 81 | 82 | const filename = process.argv[2]; 83 | const project = new Buffer.from(fs.readFileSync(filename)); 84 | 85 | vm.loadProject(project) 86 | .then(() => vm.greenFlag()) 87 | .then(() => whenThreadsComplete()) 88 | .then(printResult); 89 | -------------------------------------------------------------------------------- /examples/assets/scratchcat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | costume1.1 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/all_supported_blocks.py: -------------------------------------------------------------------------------- 1 | from math import floor, ceil 2 | from boiga import * 3 | 4 | project = Project() 5 | 6 | cat = project.new_sprite("Scratch Cat") 7 | cat.add_costume("scratchcat", "examples/assets/scratchcat.svg", center=(48, 50)) 8 | 9 | a = cat.new_var("a") 10 | b = cat.new_var("b") 11 | c = cat.new_var("c") 12 | 13 | mylist = cat.new_list("mylist") 14 | 15 | cat.on_receive("hello", [ 16 | Say("ok"), 17 | ]) 18 | 19 | @cat.proc_def() 20 | def my_custom_block(locals, a, b, c: bool): return [ 21 | If (c) [ 22 | locals.result <= a + b 23 | ] 24 | ] 25 | 26 | cat.on_flag([ 27 | # ================= MOTION ==================== 28 | 29 | #MoveSteps(10), 30 | #TurnDegreesCW(15), 31 | #TurnDegreesCCW(15), 32 | 33 | #GoTo(Position.Random), 34 | #GoTo(Position.Mouse), 35 | SetXYPos(0, 0), 36 | #GlideTo(), 37 | #GlideToXYPos(), 38 | 39 | #PointInDirection(), 40 | #PointTowards(), 41 | 42 | ChangeXPos(10), 43 | SetXPos(0), 44 | ChangeYPos(10), 45 | SetYPos(0), 46 | 47 | #IfOnEdgeBounce(), 48 | 49 | #SetRotationStyle(), 50 | 51 | Say(GetXPos()), 52 | Say(GetYPos()), 53 | Say(GetDirection()), 54 | 55 | 56 | # ================= LOOKS ==================== 57 | 58 | #SayForDuration("Hello", 1), 59 | Say("Hello!"), 60 | #ThinkForDuration("Hmm...", 1), 61 | #Think("Hmm..."), 62 | 63 | SetCostume("scratchcat"), 64 | #NextCostume(), 65 | #SetBackdrop(), 66 | #NextBackdrop(), 67 | 68 | #ChangeSizeBy(), 69 | SetSize(100), 70 | 71 | ChangeEffect(Effects.Color, 25), 72 | SetEffect(Effects.Color, 0), 73 | #ClearEffects(), 74 | 75 | Hide(), 76 | Show(), 77 | 78 | #GoToLayer(), 79 | #GoForwardsLayers(), 80 | #GoBackwardsLayers(), 81 | 82 | Say(CostumeNumber()), 83 | #CostumeName(), 84 | #BackdropNumber(), 85 | #BackdropName(), 86 | 87 | #GetSize(), 88 | 89 | 90 | # ================= SOUNDS ==================== 91 | 92 | #PlaySoundUntilDone(), 93 | #StartSound(), 94 | #StopAllSounds(), 95 | 96 | #ChangeSoundEffect(), 97 | #SetSoundEffect(), 98 | #ClearSoundEffects(), 99 | 100 | #ChangeVolumeBy(), 101 | #SetVolumeTo(), 102 | 103 | #GetVolume(), 104 | 105 | 106 | # ================= EVENTS ==================== 107 | 108 | BroadcastAndWait("hello"), 109 | #Broadcast("hello"), 110 | 111 | 112 | # ================= CONTROL ==================== 113 | 114 | Wait(1), 115 | 116 | Repeat (10) [ 117 | Say("foo"), # TODO: allow empty body? 118 | ], 119 | 120 | If (Literal(1) == 2) [ 121 | Forever [ 122 | Say("foo"), # TODO: allow empty body? 123 | ], 124 | ], 125 | 126 | If (Literal(1) == 1) [ 127 | Say("foo"), # TODO: allow empty body? 128 | ].Else [ 129 | Say("bar"), # TODO: allow empty body? 130 | ], 131 | 132 | #WaitUntil(Literal(1) == 1), 133 | 134 | RepeatUntil (Literal(1) == 1) [ 135 | Say("foo"), # TODO: allow empty body? 136 | ], 137 | 138 | If (Literal(1) == 2) [ 139 | StopAll(), 140 | ], 141 | If (Literal(1) == 2) [ 142 | StopThisScript(), 143 | ], 144 | StopOtherScriptsInSprite(), 145 | 146 | #CreateClone(), 147 | #DeleteThisClone(), 148 | 149 | 150 | # ================= SENSING ==================== 151 | 152 | Say(Touching("edge")), 153 | Say(TouchingColour(LiteralColour("#FF0000"))), 154 | #Say(ColourTouchingColour(LiteralColour("#0000FF"), LiteralColour("#FF0000"))) 155 | #Say(DistanceTo()), 156 | 157 | AskAndWait("What's the answer?"), 158 | Say(Answer()), 159 | 160 | #Say(KeyPressed("a")), 161 | Say(MouseDown()), 162 | Say(MouseX()), 163 | Say(MouseY()), 164 | 165 | #SetDragMode(DragMode.DRAGGABLE), 166 | 167 | #Say(Loudness()), 168 | 169 | #Say(Timer()), 170 | #ResetTimer(), 171 | 172 | #GetProperty("backdrop #", "Stage"); 173 | 174 | #GetCurrent("year"), 175 | 176 | Say(DaysSince2k()), 177 | 178 | #Say(Username()), 179 | 180 | 181 | # ================= OPERATORS ==================== 182 | 183 | Say(a + b), 184 | Say(a - b), 185 | Say(a * b), 186 | Say(a / b), 187 | 188 | # reflected operators 189 | Say(5 + b), 190 | Say(5 - b), 191 | Say(5 * b), 192 | Say(5 / b), 193 | 194 | Say(pickrandom(1, 10)), 195 | 196 | Say(a > b), 197 | Say(a < b), 198 | Say(a == b), 199 | Say((a == a).AND(b == b)), 200 | Say((a == a).OR(b == b)), 201 | Say((a == a).NOT()), 202 | 203 | Say(Literal("apple").join("banana")), 204 | Say(Literal("apple")[0]), 205 | Say(Literal("apple").item(1)), # 1-index alternative to the above 206 | Say(Literal("apple").len()), 207 | #Say(Literal("apple").contains("a")), 208 | 209 | Say(a % b), 210 | Say(round(a)), 211 | 212 | Say(abs(a)), 213 | Say(floor(a)), 214 | Say(ceil(a)), 215 | Say(a.sqrt()), 216 | Say(a.sin()), 217 | Say(a.cos()), 218 | #Say(a.tan()), 219 | #Say(a.asin()), 220 | #Say(a.acos()), 221 | Say(a.atan()), 222 | Say(a.log()), 223 | Say(a.log10()), 224 | Say(a ** b), 225 | 226 | 227 | # ================= VARIABLES ==================== 228 | 229 | a <= 1, 230 | a.changeby(1), 231 | #a.show(), 232 | #a.hide(), 233 | 234 | mylist.append("thing"), 235 | 236 | mylist.delete_at(0), 237 | mylist.delete_at1(1), # 1-index alternative to the above 238 | 239 | mylist.delete_all(), 240 | 241 | #mylist.insert_at(0, "thing"), 242 | #mylist.insert_at1(1, "thing"), # 1-index alternative to the above 243 | 244 | mylist[0] <= "thing", 245 | mylist.item(1) <= "thing", # 1-index alternative to the above 246 | 247 | Say(mylist[0]), 248 | Say(mylist.item(1)), # 1-index alternative to the above 249 | 250 | Say(mylist.index("thing")), 251 | Say(mylist.index1("thing")), # 1-index alternative to the above 252 | 253 | Say(mylist.contains("thing")), 254 | 255 | #mylist.show(), 256 | #mylist.hide(), 257 | 258 | 259 | # ================= CUSTOM BLOCKS ==================== 260 | 261 | my_custom_block(a, b, a == a), 262 | my_custom_block(a, b, a == a).inline(), 263 | 264 | 265 | # ================= MUSIC ==================== 266 | 267 | PlayDrum(Drums.SnareDrum, 0.25), 268 | RestFor(0.25), 269 | PlayNote(60, 0.25), 270 | SetInstrument(Instruments.Piano), 271 | SetTempo(60), 272 | ChangeTempoBy(10), 273 | Say(GetTempo()), 274 | 275 | 276 | # ================= PEN ==================== 277 | 278 | EraseAll(), 279 | Stamp(), 280 | PenDown(), 281 | PenUp(), 282 | SetPenColour(LiteralColour("#ff0000")), 283 | #ChangePenEffect("color", 10), 284 | SetPenParam("color", 10), 285 | #ChangePenSizeBy(1), 286 | SetPenSize(10), 287 | ]) 288 | 289 | 290 | cat.on_press("a", [ 291 | Say("a"), 292 | ]) 293 | 294 | project.save("examples/out/Boiga Examples: All Blocks.sb3") 295 | -------------------------------------------------------------------------------- /boiga/expressions.py: -------------------------------------------------------------------------------- 1 | from . import ast 2 | 3 | def serialise_expression(sprite, expression, parent, shadow=False): 4 | if not issubclass(type(expression), ast.core.Expression): 5 | raise Exception(f"Cannot serialise {expression!r} as a expression") 6 | 7 | blocks_json = sprite.blocks_json 8 | 9 | uid = sprite.gen_uid() 10 | blocks_json[uid] = { 11 | "next": None, 12 | "parent": parent, 13 | "inputs": {}, 14 | "fields": {}, 15 | "shadow": shadow, 16 | "topLevel": False, 17 | } 18 | 19 | UNARYMATHOPS = ["abs", "floor", "ceiling", "sqrt", "sin", "cos", "tan", 20 | "asin", "acos", "atan", "ln", "log", "e ^", "10 ^"] 21 | 22 | if type(expression) is ast.core.BinaryOp: 23 | opmap = { 24 | "+": ("operator_add", "NUM"), 25 | "-": ("operator_subtract", "NUM"), 26 | "*": ("operator_multiply", "NUM"), 27 | "/": ("operator_divide", "NUM"), 28 | "<": ("operator_lt", "OPERAND"), 29 | ">": ("operator_gt", "OPERAND"), 30 | "==": ("operator_equals", "OPERAND"), 31 | "&&": ("operator_and", "OPERAND"), 32 | "||": ("operator_or", "OPERAND"), 33 | "join": ("operator_join", "STRING"), 34 | "%": ("operator_mod", "NUM"), 35 | "[]": ("operator_letter_of", "LETTER"), 36 | "random": ("operator_random", "FROM") 37 | } 38 | if expression.op in opmap: 39 | opcode, argname = opmap[expression.op] 40 | serialiser = sprite.serialise_bool if expression.op in ["&&", "||"] else sprite.serialise_arg 41 | 42 | an1 = argname+"1" 43 | an2 = argname+"2" 44 | 45 | # TODO: encode this nicely in the table above... 46 | if expression.op == "[]": 47 | an1 = "LETTER" 48 | an2 = "STRING" 49 | elif expression.op == "random": 50 | an1 = "FROM" 51 | an2 = "TO" 52 | 53 | out = { 54 | "opcode": opcode, 55 | "inputs": { 56 | an1: serialiser(expression.lval, uid), 57 | an2: serialiser(expression.rval, uid), 58 | }, 59 | } 60 | else: 61 | raise Exception(f"Unable to serialise expression {expression!r}") 62 | 63 | elif type(expression) is ast.core.ListIndex: 64 | out = { 65 | "opcode": "data_itemoflist", 66 | "inputs": { 67 | "INDEX": sprite.serialise_arg(expression.index, uid) 68 | }, 69 | "fields": { 70 | "LIST": [ 71 | expression.list.name, 72 | expression.list.uid 73 | ] 74 | }, 75 | } 76 | 77 | elif type(expression) is ast.core.ListItemNum: 78 | out = { 79 | "opcode": "data_itemnumoflist", 80 | "inputs": { 81 | "ITEM": sprite.serialise_arg(expression.item, uid) 82 | }, 83 | "fields": { 84 | "LIST": [ 85 | expression.list.name, 86 | expression.list.uid 87 | ] 88 | }, 89 | } 90 | 91 | elif type(expression) is ast.core.ListContains: 92 | out = { 93 | "opcode": "data_listcontainsitem", 94 | "inputs": { 95 | "ITEM": sprite.serialise_arg(expression.thing, uid) 96 | }, 97 | "fields": { 98 | "LIST": [ 99 | expression.list.name, 100 | expression.list.uid 101 | ] 102 | }, 103 | } 104 | 105 | elif type(expression) is ast.core.UnaryOp: 106 | if expression.op == "!": 107 | out = { 108 | "opcode": "operator_not", 109 | "inputs": { 110 | "OPERAND": sprite.serialise_bool(expression.value, uid), 111 | }, 112 | } 113 | 114 | elif expression.op == "len": 115 | out = { 116 | "opcode": "operator_length", 117 | "inputs": { 118 | "STRING": sprite.serialise_arg(expression.value, uid), 119 | }, 120 | } 121 | 122 | elif expression.op == "round": 123 | out = { 124 | "opcode": "operator_round", 125 | "inputs": { 126 | "NUM": sprite.serialise_arg(expression.value, uid), 127 | }, 128 | } 129 | 130 | elif expression.op in UNARYMATHOPS: 131 | out = { 132 | "opcode": "operator_mathop", 133 | "inputs": { 134 | "NUM": sprite.serialise_arg(expression.value, uid), 135 | }, 136 | "fields": { 137 | "OPERATOR": [expression.op, None] 138 | }, 139 | } 140 | 141 | elif expression.op == "listlen": 142 | out = { 143 | "opcode": "data_lengthoflist", 144 | "fields": { 145 | "LIST": [expression.value.name, expression.value.uid], 146 | }, 147 | } 148 | 149 | else: 150 | raise Exception(f"Unable to serialise expression {expression!r}") 151 | 152 | elif type(expression) is ast.core.ProcVar: 153 | out = { 154 | "opcode": "argument_reporter_string_number", 155 | "fields": { 156 | "VALUE": [expression.name, None] 157 | }, 158 | } 159 | 160 | elif type(expression) is ast.core.ProcVarBool: 161 | out = { 162 | "opcode": "argument_reporter_boolean", 163 | "fields": { 164 | "VALUE": [expression.name, None] 165 | }, 166 | } 167 | 168 | elif type(expression) is ast.DaysSince2k: 169 | out = {"opcode": "sensing_dayssince2000"} 170 | 171 | elif type(expression) is ast.Answer: 172 | out = {"opcode": "sensing_answer"} 173 | 174 | elif type(expression) is ast.MouseDown: 175 | out = {"opcode": "sensing_mousedown"} 176 | 177 | elif type(expression) is ast.MouseX: 178 | out = {"opcode": "sensing_mousex"} 179 | 180 | elif type(expression) is ast.MouseY: 181 | out = {"opcode": "sensing_mousey"} 182 | 183 | elif type(expression) is ast.CostumeNumber: 184 | out = { 185 | "opcode": "looks_costumenumbername", 186 | "fields": { 187 | "NUMBER_NAME": ["number", None] 188 | }, 189 | } 190 | 191 | elif type(expression) is ast.Touching: 192 | out = { 193 | "opcode": "sensing_touchingobject", 194 | "inputs": { 195 | "TOUCHINGOBJECTMENU": sprite.serialise_arg(expression.thing, uid) 196 | }, 197 | } 198 | 199 | elif type(expression) is ast.TouchingColour: 200 | out = { 201 | "opcode": "sensing_touchingcolor", 202 | "inputs": { 203 | "COLOR": sprite.serialise_arg(expression.colour, uid, alternative=[9, "#FF0000"]) 204 | }, 205 | } 206 | 207 | elif type(expression) is ast.core.PenParamMenu: 208 | out = { 209 | "opcode": "pen_menu_colorParam", 210 | "fields": { 211 | "colorParam": [expression.param, None], 212 | } 213 | } 214 | 215 | elif type(expression) is ast.core.TouchingObjectMenu: 216 | out = { 217 | "opcode": "sensing_touchingobjectmenu", 218 | "fields": { 219 | "TOUCHINGOBJECTMENU": [expression.object, None], 220 | } 221 | } 222 | 223 | elif type(expression) is ast.core.Costume: 224 | out = { 225 | "opcode": "looks_costume", 226 | "fields": { 227 | "COSTUME": [expression.costumename, None], 228 | } 229 | } 230 | 231 | elif type(expression) is ast.core.Instrument: 232 | out = { 233 | "opcode": expression.op, 234 | "fields": { 235 | "INSTRUMENT": [expression.instrument, None], 236 | } 237 | } 238 | 239 | elif type(expression) is ast.core.Drum: 240 | out = { 241 | "opcode": expression.op, 242 | "fields": { 243 | "DRUM": [expression.drum, None], 244 | } 245 | } 246 | 247 | elif type(expression) is ast.GetTempo: 248 | out = { 249 | "opcode": "music_getTempo", 250 | } 251 | 252 | elif type(expression) is ast.GetXPos: 253 | out = { 254 | "opcode": "motion_xposition", 255 | } 256 | 257 | elif type(expression) is ast.GetYPos: 258 | out = { 259 | "opcode": "motion_yposition", 260 | } 261 | 262 | elif type(expression) is ast.GetDirection: 263 | out = { 264 | "opcode": "motion_direction", 265 | } 266 | 267 | else: 268 | raise Exception(f"Unable to serialise expression {expression!r}") 269 | 270 | blocks_json[uid].update(out) 271 | return uid 272 | -------------------------------------------------------------------------------- /examples/assets/beer.b: -------------------------------------------------------------------------------- 1 | ########################## 2 | ### 3 | ### Severely updated version! 4 | ### (now says "1 bottle" and 5 | ### contains no extra "0" verse) 6 | ### 7 | ########################## 8 | ### 99 Bottles of Beer ### 9 | ### coded in Brainf**k ### 10 | ### with explanations ### 11 | ########################## 12 | # 13 | # This Bottles of Beer program 14 | # was written by Andrew Paczkowski 15 | # Coder Alias: thepacz 16 | # three_halves_plus_one@yahoo.com 17 | ##### 18 | 19 | > 0 in the zeroth cell 20 | +++++++>++++++++++[<+++++>-] 57 in the first cell or "9" 21 | +++++++>++++++++++[<+++++>-] 57 in second cell or "9" 22 | ++++++++++ 10 in third cell 23 | >+++++++++ 9 in fourth cell 24 | 25 | ########################################## 26 | ### create ASCII chars in higher cells ### 27 | ########################################## 28 | 29 | >>++++++++[<++++>-] " " 30 | >++++++++++++++[<+++++++>-] b 31 | +>+++++++++++[<++++++++++>-] o 32 | ++>+++++++++++++++++++[<++++++>-] t 33 | ++>+++++++++++++++++++[<++++++>-] t 34 | >++++++++++++[<+++++++++>-] l 35 | +>++++++++++[<++++++++++>-] e 36 | +>+++++++++++++++++++[<++++++>-] s 37 | >++++++++[<++++>-] " " 38 | +>+++++++++++[<++++++++++>-] o 39 | ++>++++++++++[<++++++++++>-] f 40 | >++++++++[<++++>-] " " 41 | >++++++++++++++[<+++++++>-] b 42 | +>++++++++++[<++++++++++>-] e 43 | +>++++++++++[<++++++++++>-] e 44 | >+++++++++++++++++++[<++++++>-] r 45 | >++++++++[<++++>-] " " 46 | +>+++++++++++[<++++++++++>-] o 47 | >+++++++++++[<++++++++++>-] n 48 | >++++++++[<++++>-] " " 49 | ++>+++++++++++++++++++[<++++++>-] t 50 | ++++>++++++++++[<++++++++++>-] h 51 | +>++++++++++[<++++++++++>-] e 52 | >++++++++[<++++>-] " " 53 | ++>+++++++++++++[<+++++++++>-] w 54 | +>++++++++++++[<++++++++>-] a 55 | >++++++++++++[<+++++++++>-] l 56 | >++++++++++++[<+++++++++>-] l 57 | >+++++[<++>-] LF 58 | ++>+++++++++++++++++++[<++++++>-] t 59 | +>++++++++++++[<++++++++>-] a 60 | +++>+++++++++++++[<++++++++>-] k 61 | +>++++++++++[<++++++++++>-] e 62 | >++++++++[<++++>-] " " 63 | +>+++++++++++[<++++++++++>-] o 64 | >+++++++++++[<++++++++++>-] n 65 | +>++++++++++[<++++++++++>-] e 66 | >++++++++[<++++>-] " " 67 | >++++++++++[<++++++++++>-] d 68 | +>+++++++++++[<++++++++++>-] o 69 | ++>+++++++++++++[<+++++++++>-] w 70 | >+++++++++++[<++++++++++>-] n 71 | >++++++++[<++++>-] " " 72 | +>++++++++++++[<++++++++>-] a 73 | >+++++++++++[<++++++++++>-] n 74 | >++++++++++[<++++++++++>-] d 75 | >++++++++[<++++>-] " " 76 | ++>+++++++++++[<++++++++++>-] p 77 | +>++++++++++++[<++++++++>-] a 78 | +>+++++++++++++++++++[<++++++>-] s 79 | +>+++++++++++++++++++[<++++++>-] s 80 | >++++++++[<++++>-] " " 81 | +>+++++++++++++[<++++++++>-] i 82 | ++>+++++++++++++++++++[<++++++>-] t 83 | >++++++++[<++++>-] " " 84 | +>++++++++++++[<++++++++>-] a 85 | >+++++++++++++++++++[<++++++>-] r 86 | +>+++++++++++[<++++++++++>-] o 87 | >+++++++++++++[<+++++++++>-] u 88 | >+++++++++++[<++++++++++>-] n 89 | >++++++++++[<++++++++++>-] d 90 | >+++++[<++>-] LF 91 | +++++++++++++ CR 92 | 93 | [<]>>>> go back to fourth cell 94 | 95 | ################################# 96 | ### initiate the display loop ### 97 | ################################# 98 | 99 | [ loop 100 | < back to cell 3 101 | [ loop 102 | [>]<< go to last cell and back to LF 103 | .. output 2 newlines 104 | [<]> go to first cell 105 | 106 | ################################### 107 | #### begin display of characters### 108 | ################################### 109 | # 110 | #.>.>>>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.> 111 | #X X b o t t l e s o f b e e r 112 | #.>.>.>.>.>.>.>.>.>.>.>. 113 | #o n t h e w a l l N 114 | #[<]> go to first cell 115 | #.>.>>>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>>>>>>>>>>>>>.> 116 | #X X b o t t l e s o f b e e r N 117 | #.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.> 118 | #t a k e o n e d o w n a n d p a s s 119 | #.>.>.>.>.>.>.>.>.>. 120 | #i t a r o u n d N 121 | ##### 122 | 123 | [<]>> go to cell 2 124 | - subtract 1 from cell 2 125 | < go to cell 1 126 | 127 | ######################## 128 | ### display last line ## 129 | ######################## 130 | # 131 | #.>.>>>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.> 132 | #X X b o t t l e s o f b e e r 133 | #.>.>.>.>.>.>.>.>.>.>. 134 | #o n t h e w a l l 135 | ##### 136 | 137 | [<]>>>- go to cell 3/subtract 1 138 | ] end loop when cell 3 is 0 139 | ++++++++++ add 10 to cell 3 140 | <++++++++++ back to cell 2/add 10 141 | <- back to cell 1/subtract 1 142 | [>]<. go to last line/carriage return 143 | [<]> go to first line 144 | 145 | ######################## 146 | ### correct last line ## 147 | ######################## 148 | # 149 | #.>.>>>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.> 150 | #X X b o t t l e s o f b e e r 151 | #.>.>.>.>.>.>.>.>.>.>. 152 | #o n t h e w a l l 153 | ##### 154 | 155 | [<]>>>>- go to cell 4/subtract 1 156 | ] end loop when cell 4 is 0 157 | 158 | ############################################################## 159 | ### By this point verses 99\10 are displayed but to work ### 160 | ### with the lower numbered verses in a more readable way ### 161 | ### we initiate a new loop for verses 9{CODE} that will not ### 162 | ### use the fourth cell at all ### 163 | ############################################################## 164 | 165 | + add 1 to cell four (to keep it non\zero) 166 | <-- back to cell 3/subtract 2 167 | 168 | [ loop 169 | [>]<< go to last cell and back to LF 170 | .. output 2 newlines 171 | [<]> go to first cell 172 | 173 | ################################### 174 | #### begin display of characters### 175 | ################################### 176 | # 177 | #>.>>>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.> 178 | # X b o t t l e s o f b e e r 179 | #.>.>.>.>.>.>.>.>.>.>.>. 180 | #o n t h e w a l l N 181 | #[<]> go to first cell 182 | #>.>>>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>>>>>>>>>>>>>.> 183 | # X b o t t l e s o f b e e r N 184 | #.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.> 185 | #t a k e o n e d o w n a n d p a s s 186 | #.>.>.>.>.>.>.>.>.>. 187 | #i t a r o u n d N 188 | ##### 189 | 190 | [<]>> go to cell 2 191 | - subtract 1 from cell 2 192 | 193 | ######################## 194 | ### display last line ## 195 | ######################## 196 | # 197 | #.>>>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.> 198 | #X b o t t l e s o f b e e r 199 | #.>.>.>.>.>.>.>.>.>.>. 200 | #o n t h e w a l l 201 | ##### 202 | 203 | [<]>>>- go to cell 3/subtract 1 204 | ] end loop when cell 3 is 0 205 | + add 1 to cell 3 to keep it non\zero 206 | 207 | [>]<. go to last line/carriage return 208 | [<]> go to first line 209 | 210 | ######################## 211 | ### correct last line ## 212 | ######################## 213 | # 214 | #>.>>>.>.>.>.>.>.>.>>.>.>.>.>.>.>.>.>.> 215 | # X b o t t l e o f b e e r 216 | #.>.>.>.>.>.>.>.>.>.>.<<<<. 217 | #o n t h e w a l l 218 | ##### 219 | 220 | [>]<< go to last cell and back to LF 221 | .. output 2 newlines 222 | [<]> go to first line 223 | 224 | ######################### 225 | ### the final verse ## 226 | ######################### 227 | # 228 | #>.>>>.>.>.>.>.>.>.>>.>.>.>.>.>.>.>.>.> 229 | # X b o t t l e o f b e e r 230 | #.>.>.>.>.>.>.>.>.>.>.>. 231 | #o n t h e w a l l N 232 | #[<]> go to first cell 233 | #>.>>>.>.>.>.>.>.>.>>.>.>.>.>.>.>.>.>>>>>>>>>>>>>.> 234 | # X b o t t l e o f b e e r N 235 | #.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.> 236 | #t a k e o n e d o w n a n d p a s s 237 | #.>.>.>.>.>.>.>.>.>. 238 | #i t a r o u n d N 239 | #[>]< go to last line 240 | #<<<.<<.<<<. 241 | # n o 242 | #[<]>>>> go to fourth cell 243 | #>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.> 244 | # b o t t l e s o f b e e r 245 | #.>.>.>.>.>.>.>.>.>.>.>. 246 | #o n t h e w a l l N 247 | #####fin## 248 | -------------------------------------------------------------------------------- /boiga/ast.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from . import ast_core as core 4 | from .ast_core import ensure_expression, Literal, LiteralColour 5 | 6 | # MOTION 7 | 8 | class SetXYPos(core.Statement): 9 | def __init__(self, x, y): 10 | super().__init__("motion_gotoxy", 11 | X=ensure_expression(x), 12 | Y=ensure_expression(y)) 13 | 14 | class ChangeXPos(core.Statement): 15 | def __init__(self, x): 16 | super().__init__("motion_changexby", 17 | DX=ensure_expression(x)) 18 | 19 | class SetXPos(core.Statement): 20 | def __init__(self, x): 21 | super().__init__("motion_setx", 22 | X=ensure_expression(x)) 23 | 24 | class ChangeYPos(core.Statement): 25 | def __init__(self, y): 26 | super().__init__("motion_changeyby", 27 | DY=ensure_expression(y)) 28 | 29 | class SetYPos(core.Statement): 30 | def __init__(self, y): 31 | super().__init__("motion_sety", 32 | Y=ensure_expression(y)) 33 | 34 | class GetXPos(core.Expression): 35 | def __init__(self): 36 | pass 37 | 38 | class GetYPos(core.Expression): 39 | def __init__(self): 40 | pass 41 | 42 | class GetDirection(core.Expression): 43 | def __init__(self): 44 | pass 45 | 46 | # END MOTION 47 | 48 | # LOOKS 49 | 50 | class SetCostume(core.Statement): 51 | def __init__(self, costume): 52 | if type(costume) is str: 53 | costume = core.Costume(costume) 54 | super().__init__("looks_switchcostumeto", 55 | COSTUME=costume) 56 | 57 | class Say(core.Statement): 58 | def __init__(self, msg): 59 | super().__init__("looks_say", 60 | MESSAGE=ensure_expression(msg)) 61 | 62 | class SetEffect(core.Statement): 63 | def __init__(self, effect, value): 64 | super().__init__("looks_seteffectto", 65 | EFFECT=str(effect), # TODO: assert is str? 66 | VALUE=ensure_expression(value)) 67 | 68 | class Effects: 69 | Color = "color" 70 | Fisheye = "fisheye" 71 | Whirl = "whirl" 72 | Pixelate = "pixelate" 73 | Mosaic = "mosaic" 74 | Brightness = "brightness" 75 | Ghost = "ghost" 76 | 77 | class ChangeEffect(core.Statement): 78 | def __init__(self, effect, change): 79 | super().__init__("looks_changeeffectby", 80 | EFFECT=str(effect), # TODO: assert is str? 81 | CHANGE=ensure_expression(change)) 82 | 83 | class SetSize(core.Statement): 84 | def __init__(self, size): 85 | super().__init__("looks_setsizeto", 86 | SIZE=ensure_expression(size)) 87 | 88 | class Show(core.Statement): 89 | def __init__(self): 90 | self.op = "looks_show" 91 | 92 | class Hide(core.Statement): 93 | def __init__(self): 94 | self.op = "looks_hide" 95 | 96 | # END LOOKS 97 | 98 | # BEGIN SOUND 99 | 100 | # END SOUND 101 | 102 | # EVENTS 103 | 104 | class BroadcastAndWait(core.Statement): 105 | def __init__(self, event): 106 | super().__init__("event_broadcastandwait", BROADCAST_INPUT=str(event)) 107 | 108 | # END EVENTS 109 | 110 | # BEGIN CONTROL 111 | 112 | class Wait(core.Statement): 113 | def __init__(self, duration): 114 | super().__init__("control_wait", DURATION=ensure_expression(duration)) 115 | 116 | Repeat = core.repeatn 117 | 118 | class ForeverHack(): 119 | def __getitem__(self, do): 120 | if type(do) != tuple: 121 | do = [do] 122 | return core.Statement("control_forever", SUBSTACK=list(do)) 123 | Forever = ForeverHack() 124 | 125 | def If(condition, then=None): 126 | if then is None: 127 | return core.getitem_hack(If, condition) 128 | return core.IfStatement(condition, then) 129 | 130 | RepeatUntil = core.repeatuntil 131 | 132 | def StopAll(): 133 | return core.Statement("control_stop", STOP_OPTION="all") 134 | 135 | def StopThisScript(): 136 | return core.Statement("control_stop", STOP_OPTION="this script") 137 | 138 | def StopOtherScriptsInSprite(): 139 | return core.Statement("control_stop", STOP_OPTION="other scripts in sprite") 140 | 141 | # END CONTROL 142 | 143 | # BEGIN SENSING 144 | 145 | class AskAndWait(core.Statement): 146 | def __init__(self, prompt=""): 147 | self.op = "sensing_askandwait" 148 | self.prompt = ensure_expression(prompt) 149 | 150 | class Answer(core.Expression): 151 | def __init__(self): 152 | pass 153 | 154 | class MouseDown(core.Expression): 155 | type = "bool" 156 | def __init__(self): 157 | pass 158 | 159 | class Touching(core.Expression): 160 | type = "bool" 161 | def __init__(self, object): 162 | self.thing = core.TouchingObjectMenu(object) 163 | 164 | class TouchingColour(core.Expression): 165 | type = "bool" 166 | def __init__(self, colour): 167 | self.colour = colour 168 | 169 | class MouseX(core.Expression): 170 | def __init__(self): 171 | pass 172 | 173 | class MouseY(core.Expression): 174 | def __init__(self): 175 | pass 176 | 177 | class CostumeNumber(core.Expression): 178 | def __init__(self): 179 | pass 180 | 181 | class DaysSince2k(core.Expression): 182 | def __init__(self): 183 | pass 184 | 185 | # END SENSING 186 | 187 | # BEGIN OPERATORS 188 | 189 | def pickrandom(a, b): 190 | return core.BinaryOp("random", a, b) 191 | 192 | # END OPERATORS 193 | 194 | # BEGIN PEN 195 | 196 | class EraseAll(core.Statement): 197 | def __init__(self): 198 | self.op = "pen_clear" 199 | 200 | class Stamp(core.Statement): 201 | def __init__(self): 202 | self.op = "pen_stamp" 203 | 204 | class PenDown(core.Statement): 205 | def __init__(self): 206 | self.op = "pen_penDown" 207 | 208 | class PenUp(core.Statement): 209 | def __init__(self): 210 | self.op = "pen_penUp" 211 | 212 | # correct spellings only! 213 | class SetPenColour(core.Statement): 214 | def __init__(self, colour): 215 | if type(colour) is int and colour < 0x1000000: 216 | colour = LiteralColour(f"#{colour:06x}") 217 | elif type(colour) is not LiteralColour: 218 | colour = ensure_expression(colour)+0 # TODO: only add zero if it's not an expression to begin with 219 | super().__init__("pen_setPenColorToColor", COLOR=colour) 220 | 221 | 222 | class SetPenParam(core.Statement): 223 | def __init__(self, param, value): 224 | super().__init__("pen_setPenColorParamTo", COLOR_PARAM=core.PenParamMenu(param), VALUE=ensure_expression(value)) 225 | 226 | def RGBA(r, g, b, a): 227 | return (ensure_expression(r) << 16) + \ 228 | (ensure_expression(g) << 8) + \ 229 | (ensure_expression(b)) + \ 230 | (ensure_expression(a) << 24) 231 | 232 | def RGB(r, g, b): 233 | return RGBA(r, g, b, 0) 234 | 235 | class SetPenSize(core.Statement): 236 | def __init__(self, size): 237 | super().__init__("pen_setPenSizeTo", SIZE=ensure_expression(size)) 238 | 239 | # END PEN 240 | 241 | # BEGIN MUSIC 242 | 243 | class Instruments: 244 | Piano = core.Instrument(1) 245 | ElectricPiano = core.Instrument(2) 246 | Organ = core.Instrument(3) 247 | Guitar = core.Instrument(4) 248 | ElectricGuitar = core.Instrument(5) 249 | Bass = core.Instrument(6) 250 | Pizzicato = core.Instrument(7) 251 | Cello = core.Instrument(8) 252 | Trombone = core.Instrument(9) 253 | Clarinet = core.Instrument(10) 254 | Saxophone = core.Instrument(11) 255 | Flute = core.Instrument(12) 256 | WoodenFlute = core.Instrument(13) 257 | Bassoon = core.Instrument(14) 258 | Choir = core.Instrument(15) 259 | Vibraphone = core.Instrument(16) 260 | MusicBox = core.Instrument(17) 261 | SteelDrum = core.Instrument(18) 262 | Marimba = core.Instrument(19) 263 | SynthLead = core.Instrument(20) 264 | SynthPad = core.Instrument(21) 265 | 266 | class Drums: 267 | SnareDrum = core.Drum(1) 268 | BassDrum = core.Drum(2) 269 | SideStick = core.Drum(3) 270 | CrashCymbal = core.Drum(4) 271 | OpenHiHat = core.Drum(5) 272 | ClosedHiHat = core.Drum(6) 273 | Tambourine = core.Drum(7) 274 | HandClap = core.Drum(8) 275 | Claves = core.Drum(9) 276 | WoodBlock = core.Drum(10) 277 | Cowbell = core.Drum(11) 278 | Triangle = core.Drum(12) 279 | Bongo = core.Drum(13) 280 | Conga = core.Drum(14) 281 | Cabasa = core.Drum(15) 282 | Guiro = core.Drum(16) 283 | Vibraslap = core.Drum(17) 284 | Cuica = core.Drum(18) 285 | 286 | class SetInstrument(core.Statement): 287 | def __init__(self, instrument): 288 | if not core.is_expression(instrument): 289 | instrument = ensure_expression(instrument).join("") 290 | super().__init__("music_setInstrument", INSTRUMENT=instrument) 291 | 292 | class SetTempo(core.Statement): 293 | def __init__(self, tempo): 294 | super().__init__("music_setTempo", TEMPO=ensure_expression(tempo)) 295 | 296 | class ChangeTempoBy(core.Statement): 297 | def __init__(self, tempo): 298 | super().__init__("music_changeTempo", TEMPO=ensure_expression(tempo)) 299 | 300 | class GetTempo(core.Expression): 301 | def __init__(self): 302 | pass 303 | 304 | class PlayNote(core.Statement): 305 | def __init__(self, note, beats): 306 | super().__init__("music_playNoteForBeats", NOTE=ensure_expression(note), BEATS=ensure_expression(beats)) 307 | 308 | class PlayDrum(core.Statement): 309 | def __init__(self, drum, beats): 310 | if not core.is_expression(drum): 311 | drum = ensure_expression(drum).join("") 312 | super().__init__("music_playDrumForBeats", DRUM=drum, BEATS=ensure_expression(beats)) 313 | 314 | class RestFor(core.Statement): 315 | def __init__(self, beats): 316 | super().__init__("music_restForBeats", BEATS=ensure_expression(beats)) 317 | 318 | # END MUSIC 319 | 320 | # misc helpers 321 | 322 | def sumchain(arr): 323 | result = arr[0] 324 | for i in arr[1:]: 325 | result += i 326 | return result 327 | 328 | millis_now = DaysSince2k() * 86400000 329 | 330 | def nop(*args): 331 | return [] 332 | -------------------------------------------------------------------------------- /boiga/statements.py: -------------------------------------------------------------------------------- 1 | from email.policy import default 2 | import json 3 | 4 | from . import ast_core 5 | 6 | def serialise_statement(sprite, statement): 7 | if not issubclass(type(statement), ast_core.Statement): 8 | raise Exception(f"Cannot serialise {statement!r} as a statement") 9 | 10 | blocks_json = sprite.blocks_json 11 | 12 | uid = sprite.gen_uid() 13 | blocks_json[uid] = { 14 | "inputs": {}, 15 | "fields": {}, 16 | "shadow": False, 17 | "topLevel": False, 18 | } 19 | 20 | # ===== EVENTS ======= 21 | if statement.op == "event_whenflagclicked": 22 | out = { 23 | "opcode": "event_whenflagclicked" 24 | } 25 | 26 | elif statement.op == "event_whenbroadcastreceived": 27 | out = { 28 | "opcode": "event_whenbroadcastreceived", 29 | "fields": { 30 | "BROADCAST_OPTION": statement.args["BROADCAST_OPTION"], 31 | } 32 | } 33 | 34 | elif statement.op == "event_whenkeypressed": 35 | out = { 36 | "opcode": "event_whenkeypressed", 37 | "fields": { 38 | "KEY_OPTION": [statement.args["KEY_OPTION"], None], 39 | } 40 | } 41 | 42 | elif statement.op == "event_broadcastandwait": 43 | out = { 44 | "opcode": "event_broadcastandwait", 45 | "inputs": { 46 | "BROADCAST_INPUT": [1, [11, statement.args["BROADCAST_INPUT"], sprite.project.broadcasts[statement.args["BROADCAST_INPUT"]]]], 47 | } 48 | } 49 | 50 | # ===== CONTROL ======= 51 | elif statement.op == "control_repeat": 52 | out = { 53 | "opcode": "control_repeat", 54 | "inputs": { 55 | "TIMES": sprite.serialise_arg(statement.args["TIMES"], uid), 56 | "SUBSTACK": sprite.serialise_script(statement.args["SUBSTACK"], uid) 57 | } 58 | } 59 | elif statement.op == "control_repeat_until": 60 | out = { 61 | "opcode": "control_repeat_until", 62 | "inputs": { 63 | "CONDITION": sprite.serialise_bool(statement.args["CONDITION"], uid), 64 | "SUBSTACK": sprite.serialise_script(statement.args["SUBSTACK"], uid) 65 | } 66 | } 67 | elif statement.op == "control_forever": 68 | out = { 69 | "opcode": "control_forever", 70 | "inputs": { 71 | "SUBSTACK": sprite.serialise_script(statement.args["SUBSTACK"], uid) 72 | } 73 | } 74 | elif statement.op == "control_if": 75 | out = { 76 | "opcode": "control_if", 77 | "inputs": { 78 | "CONDITION": sprite.serialise_bool(statement.args["CONDITION"], uid), 79 | "SUBSTACK": sprite.serialise_script(statement.args["SUBSTACK"], uid) 80 | } 81 | } 82 | elif statement.op == "control_if_else": 83 | out = { 84 | "opcode": "control_if_else", 85 | "inputs": { 86 | "CONDITION": sprite.serialise_bool(statement.args["CONDITION"], uid), 87 | "SUBSTACK": sprite.serialise_script(statement.args["SUBSTACK"], uid), 88 | "SUBSTACK2": sprite.serialise_script(statement.args["SUBSTACK2"], uid) 89 | } 90 | } 91 | elif statement.op == "control_wait": 92 | out = { 93 | "opcode": "control_wait", 94 | "inputs": { 95 | "DURATION": sprite.serialise_arg(statement.args["DURATION"], uid) 96 | } 97 | } 98 | elif statement.op == "control_stop": 99 | out = { 100 | "opcode": "control_stop", 101 | "fields": { 102 | "STOP_OPTION": [statement.args["STOP_OPTION"], None] 103 | }, 104 | "mutation": { 105 | "tagName": "mutation", 106 | "children": [], 107 | "hasnext": "true" if statement.args["STOP_OPTION"] == "other scripts in sprite" else "false" 108 | }, 109 | } 110 | 111 | # ===== DATA ======= 112 | elif statement.op == "data_setvariableto": 113 | out = { 114 | "opcode": "data_setvariableto", 115 | "inputs": { 116 | "VALUE": sprite.serialise_arg(statement.args["VALUE"], uid) 117 | }, 118 | "fields": { 119 | "VARIABLE": [ 120 | statement.args["VARIABLE"].name, 121 | statement.args["VARIABLE"].uid 122 | ] 123 | } 124 | } 125 | elif statement.op == "data_changevariableby": 126 | out = { 127 | "opcode": "data_changevariableby", 128 | "inputs": { 129 | "VALUE": sprite.serialise_arg(statement.args["VALUE"], uid) 130 | }, 131 | "fields": { 132 | "VARIABLE": [ 133 | statement.args["VARIABLE"].name, 134 | statement.args["VARIABLE"].uid 135 | ] 136 | } 137 | } 138 | elif statement.op == "data_replaceitemoflist": 139 | out = { 140 | "opcode": "data_replaceitemoflist", 141 | "inputs": { 142 | "INDEX": sprite.serialise_arg(statement.args["INDEX"], uid), 143 | "ITEM": sprite.serialise_arg(statement.args["ITEM"], uid) 144 | }, 145 | "fields": { 146 | "LIST": [ 147 | statement.args["LIST"].name, 148 | statement.args["LIST"].uid 149 | ] 150 | } 151 | } 152 | elif statement.op == "data_addtolist": 153 | out = { 154 | "opcode": "data_addtolist", 155 | "inputs": { 156 | "ITEM": sprite.serialise_arg(statement.args["ITEM"], uid) 157 | }, 158 | "fields": { 159 | "LIST": [ 160 | statement.args["LIST"].name, 161 | statement.args["LIST"].uid 162 | ] 163 | } 164 | } 165 | elif statement.op == "data_deletealloflist": 166 | out = { 167 | "opcode": "data_deletealloflist", 168 | "fields": { 169 | "LIST": [ 170 | statement.args["LIST"].name, 171 | statement.args["LIST"].uid 172 | ] 173 | } 174 | } 175 | elif statement.op == "data_deleteoflist": 176 | out = { 177 | "opcode": "data_deleteoflist", 178 | "inputs": { 179 | "INDEX": sprite.serialise_arg(statement.args["INDEX"], uid) 180 | }, 181 | "fields": { 182 | "LIST": [ 183 | statement.args["LIST"].name, 184 | statement.args["LIST"].uid 185 | ] 186 | } 187 | } 188 | 189 | # ======= custom blocks ======= 190 | 191 | elif statement.op == "procedures_definition": 192 | out = { 193 | "opcode": "procedures_definition", 194 | "inputs": { 195 | "custom_block": sprite.serialise_procproto(statement.proto, uid) 196 | } 197 | } 198 | 199 | elif statement.op == "procedures_call": 200 | inputs = {} 201 | for arg, var in zip(statement.args["ARGS"], statement.proc.vars): 202 | inputs[var.uid2] = sprite.serialise_arg(arg, uid) 203 | out = { 204 | "opcode": "procedures_call", 205 | "inputs": inputs, 206 | "mutation": { 207 | "tagName": "mutation", 208 | "children": [], 209 | "proccode": statement.proc.proccode, 210 | "argumentids": json.dumps(list(inputs.keys())), 211 | "warp": "true" if statement.proc.turbo else "false" 212 | } 213 | } 214 | 215 | elif statement.op == "sensing_askandwait": 216 | out = { 217 | "opcode": "sensing_askandwait", 218 | "inputs": { 219 | "QUESTION": sprite.serialise_arg(statement.prompt, uid) 220 | } 221 | } 222 | 223 | # ======= motion ======= 224 | 225 | elif statement.op == "motion_gotoxy": 226 | out = { 227 | "opcode": statement.op, 228 | "inputs": { 229 | "X": sprite.serialise_arg(statement.args["X"], uid), 230 | "Y": sprite.serialise_arg(statement.args["Y"], uid) 231 | } 232 | } 233 | 234 | elif statement.op == "motion_changexby": 235 | out = { 236 | "opcode": statement.op, 237 | "inputs": { 238 | "DX": sprite.serialise_arg(statement.args["DX"], uid) 239 | } 240 | } 241 | 242 | elif statement.op == "motion_setx": 243 | out = { 244 | "opcode": statement.op, 245 | "inputs": { 246 | "X": sprite.serialise_arg(statement.args["X"], uid) 247 | } 248 | } 249 | 250 | elif statement.op == "motion_changeyby": 251 | out = { 252 | "opcode": statement.op, 253 | "inputs": { 254 | "DY": sprite.serialise_arg(statement.args["DY"], uid) 255 | } 256 | } 257 | 258 | elif statement.op == "motion_sety": 259 | out = { 260 | "opcode": statement.op, 261 | "inputs": { 262 | "Y": sprite.serialise_arg(statement.args["Y"], uid) 263 | } 264 | } 265 | 266 | # ======= looks ======= 267 | 268 | elif statement.op in ["looks_show", "looks_hide"]: 269 | out = {"opcode": statement.op} 270 | 271 | elif statement.op == "looks_switchcostumeto": 272 | out = { 273 | "opcode": statement.op, 274 | "inputs": { # TODO: insert correct sub-block 275 | "COSTUME": sprite.serialise_arg(statement.args["COSTUME"], uid, alternative=sprite.serialise_expression(ast_core.Costume(next(iter(sprite.costumes.keys()))), uid, shadow=True)) 276 | } 277 | } 278 | 279 | elif statement.op == "looks_say": 280 | out = { 281 | "opcode": statement.op, 282 | "inputs": { 283 | "MESSAGE": sprite.serialise_arg(statement.args["MESSAGE"], uid) 284 | } 285 | } 286 | 287 | elif statement.op == "looks_seteffectto": 288 | out = { 289 | "opcode": statement.op, 290 | "fields": { 291 | "EFFECT": [statement.args["EFFECT"], None], 292 | }, 293 | "inputs": { # TODO: insert correct sub-block 294 | "VALUE": sprite.serialise_arg(statement.args["VALUE"], uid) 295 | } 296 | } 297 | 298 | elif statement.op == "looks_changeeffectby": 299 | out = { 300 | "opcode": statement.op, 301 | "fields": { 302 | "EFFECT": [statement.args["EFFECT"], None], 303 | }, 304 | "inputs": { # TODO: insert correct sub-block 305 | "CHANGE": sprite.serialise_arg(statement.args["CHANGE"], uid) 306 | } 307 | } 308 | 309 | elif statement.op == "looks_setsizeto": 310 | out = { 311 | "opcode": statement.op, 312 | "inputs": { 313 | "SIZE": sprite.serialise_arg(statement.args["SIZE"], uid) 314 | } 315 | } 316 | 317 | # ======= pen ======= 318 | 319 | elif statement.op in ["pen_clear", "pen_stamp", "pen_penDown", "pen_penUp"]: 320 | out = {"opcode": statement.op} 321 | 322 | elif statement.op == "pen_setPenColorToColor": 323 | out = { 324 | "opcode": "pen_setPenColorToColor", 325 | "inputs": { 326 | "COLOR": sprite.serialise_arg(statement.args["COLOR"], uid, alternative=[9, "#FF0000"]) 327 | } 328 | } 329 | 330 | elif statement.op == "pen_setPenSizeTo": 331 | out = { 332 | "opcode": "pen_setPenSizeTo", 333 | "inputs": { 334 | "SIZE": sprite.serialise_arg(statement.args["SIZE"], uid) 335 | } 336 | } 337 | 338 | elif statement.op == "pen_setPenColorParamTo": 339 | out = { 340 | "opcode": "pen_setPenColorParamTo", 341 | "inputs": { 342 | "COLOR_PARAM": sprite.serialise_arg(statement.args["COLOR_PARAM"], uid), 343 | "VALUE": sprite.serialise_arg(statement.args["VALUE"], uid) 344 | } 345 | } 346 | 347 | #elif statement.op == "pen_menu_colorParam": 348 | # out = { 349 | # "opcode": "pen_menu_colorParam", 350 | # "fields": { 351 | # "colorParam": [statement.args["colorParam"], None], 352 | # } 353 | # } 354 | 355 | elif statement.op == "music_setInstrument": 356 | out = { 357 | "opcode": "music_setInstrument", 358 | "inputs": { 359 | "INSTRUMENT": sprite.serialise_arg(statement.args["INSTRUMENT"], uid, alternative=sprite.serialise_expression(ast_core.Instrument(1), uid, shadow=True)), 360 | } 361 | } 362 | 363 | elif statement.op == "music_setTempo": 364 | out = { 365 | "opcode": "music_setTempo", 366 | "inputs": { 367 | "TEMPO": sprite.serialise_arg(statement.args["TEMPO"], uid), 368 | } 369 | } 370 | 371 | elif statement.op == "music_changeTempo": 372 | out = { 373 | "opcode": "music_changeTempo", 374 | "inputs": { 375 | "TEMPO": sprite.serialise_arg(statement.args["TEMPO"], uid), 376 | } 377 | } 378 | 379 | elif statement.op == "music_restForBeats": 380 | out = { 381 | "opcode": "music_restForBeats", 382 | "inputs": { 383 | "BEATS": sprite.serialise_arg(statement.args["BEATS"], uid), 384 | } 385 | } 386 | 387 | elif statement.op == "music_playNoteForBeats": 388 | out = { 389 | "opcode": "music_playNoteForBeats", 390 | "inputs": { 391 | "NOTE": sprite.serialise_arg(statement.args["NOTE"], uid), 392 | "BEATS": sprite.serialise_arg(statement.args["BEATS"], uid), 393 | } 394 | } 395 | 396 | elif statement.op == "music_playDrumForBeats": 397 | out = { 398 | "opcode": "music_playDrumForBeats", 399 | "inputs": { 400 | "DRUM": sprite.serialise_arg(statement.args["DRUM"], uid, alternative=sprite.serialise_expression(ast_core.Instrument(1), uid, shadow=True)), 401 | "BEATS": sprite.serialise_arg(statement.args["BEATS"], uid), 402 | } 403 | } 404 | 405 | else: 406 | raise Exception(f"I don't know how to serialise this op: {statement.op!r}") 407 | 408 | blocks_json[uid].update(out) 409 | return uid 410 | -------------------------------------------------------------------------------- /boiga/codegen.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import io 3 | 4 | from .utils import flatten, gen_uid, BLANK_SVG 5 | from zipfile import ZipFile 6 | import json 7 | import sys 8 | import subprocess 9 | import os 10 | 11 | from . import ast 12 | from . import ast_core 13 | from .expressions import serialise_expression 14 | from .statements import serialise_statement 15 | 16 | class Project(): 17 | def __init__(self): 18 | self.asset_data = {} # maps file name (md5.ext) to file contents 19 | self.sprites = [] 20 | self.monitors = [] 21 | self.broadcasts = {} # maps broadcast names to uids 22 | self.stage = self.new_sprite("Stage", is_stage=True) 23 | 24 | def new_sprite(self, name, is_stage=False): 25 | sprite = Sprite(self, name, is_stage=is_stage) 26 | self.sprites.append(sprite) 27 | return sprite 28 | 29 | def save(self, filename, stealthy=False, execute=False, capture=False): 30 | self.asset_data = {} 31 | self.used_layers = set() # used during serialisation 32 | 33 | print(f"[*] Creating sb3 project file: {filename!r}") 34 | 35 | zip_buf = io.BytesIO() 36 | with ZipFile(zip_buf, "w") as zf: 37 | project = { 38 | "targets": [s.serialise(self.used_layers) for s in self.sprites], 39 | "monitors": self.monitors, 40 | "extensions": ["pen", "music"], 41 | "meta": { 42 | "semver": "3.0.0", 43 | "vm": "0.2.0-prerelease.20210706190652", 44 | # plausible useragent string (dunno what the best long-term value is...) 45 | "agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36" 46 | } if stealthy else { 47 | "semver": "3.0.0", 48 | "vm": "0.0.1-com.github.davidbuchanan314.boiga", 49 | "agent": "Python " + sys.version.replace("\n", "") 50 | } 51 | } 52 | 53 | block_count = 0 54 | for serialised, sprite in zip(project["targets"], self.sprites): 55 | block_count += len(serialised["blocks"]) + sprite.block_count 56 | print(f"[*] Serialised {block_count} blocks") 57 | 58 | # TODO: put this behind a debug flag 59 | debug_json = json.dumps(project, indent=4) 60 | #print(debug_json) 61 | open("DEBUG.json", "w").write(debug_json + "\n") 62 | 63 | with zf.open("project.json", "w") as projfile: 64 | json_blob = json.dumps(project).encode() 65 | print(f"[*] project.json size: {len(json_blob)}") 66 | projfile.write(json_blob) 67 | 68 | for asset_name, data in self.asset_data.items(): 69 | with zf.open(asset_name, "w") as f: 70 | f.write(data) 71 | 72 | # we buffer the zip in memory, and write it to disk in a single write, 73 | # to avoid tripping up programs like inotify/fswatch 74 | # TODO: do this for .sprite3 exports too 75 | zip_bytes = zip_buf.getvalue() 76 | with open(filename, "wb") as zf: 77 | zf.write(zip_bytes) 78 | 79 | print(f"[*] Done writing {filename!r} ({len(zip_bytes)} bytes)") 80 | 81 | if execute: 82 | return subprocess.run([os.path.dirname(__file__) + "/../tools/run_scratch.js", filename], check=True, capture_output=capture) 83 | 84 | 85 | class Sprite(): 86 | def __init__(self, project, name, is_stage=False): 87 | self.project = project 88 | self.name = name 89 | self.is_stage = is_stage 90 | self.variable_uids = {} # name to uid 91 | self.variable_values = {} # uid to value 92 | self.list_uids = {} # name to uid 93 | self.list_values = {} # uid to value 94 | self.scripts = [] 95 | self.costumes = {} # indexed by name 96 | 97 | self.current_costume = 0 # some way to adjust this? 98 | self.volume = 100 99 | 100 | def new_var(self, name, value="", monitor=None): 101 | if not type(name) is str: 102 | raise Exception("Variable name must be a string") 103 | 104 | if name in self.variable_uids: 105 | uid = self.variable_uids[name] 106 | else: 107 | uid = self.gen_uid(["var", name]) 108 | 109 | self.variable_uids[name] = uid 110 | self.variable_values[uid] = value 111 | 112 | if monitor: 113 | self.project.monitors.append({ 114 | "id": uid, 115 | "mode": "default", 116 | "opcode": "data_variable", 117 | "params": { 118 | "VARIABLE": name 119 | }, 120 | "spriteName": None if self.name == "Stage" else self.name, 121 | "value": [], 122 | "width": 0, 123 | "height": 0, 124 | "x": monitor[0], 125 | "y": monitor[1], 126 | "visible": True, 127 | "sliderMin": 0, 128 | "sliderMax": 100, 129 | "isDiscrete": True 130 | }) 131 | 132 | return ast_core.Var(self, name, uid) 133 | 134 | def new_list(self, name, value=[], monitor=None): 135 | if not type(name) is str: 136 | raise Exception("List name must be a string") 137 | 138 | uid = self.list_uids.get(name, self.gen_uid(["list", name])) 139 | self.list_uids[name] = uid 140 | self.list_values[uid] = value 141 | 142 | if monitor: 143 | self.project.monitors.append({ 144 | "id": uid, 145 | "mode": "list", 146 | "opcode": "data_listcontents", 147 | "params": { 148 | "LIST": name 149 | }, 150 | "spriteName": None if self.name == "Stage" else self.name, 151 | "value": [], 152 | "width": monitor[2], 153 | "height": monitor[3], 154 | "x": monitor[0], 155 | "y": monitor[1], 156 | "visible": True 157 | }) 158 | 159 | return ast_core.List(self, name, uid) 160 | 161 | def add_script(self, stack): 162 | self.scripts.append(stack) 163 | 164 | def add_costume(self, name, data_or_path, extension=None, center=(0, 0)): 165 | if type(data_or_path) is str: 166 | path = data_or_path 167 | data = open(path, "rb").read() 168 | if extension is None: 169 | extension = path.split(".")[-1] 170 | else: 171 | data = data_or_path 172 | if not extension: 173 | raise Exception("Unspecified file extension") 174 | 175 | self.costumes[name] = (data, extension, center) 176 | 177 | def on_flag(self, stack): 178 | self.add_script(ast_core.on_flag(stack)) 179 | 180 | def on_receive(self, event, stack): 181 | if event not in self.project.broadcasts: 182 | self.project.broadcasts[event] = gen_uid(["broadcast", event]) 183 | 184 | self.add_script(ast_core.on_receive(event, self.project.broadcasts[event], stack)) 185 | 186 | def on_press(self, key, stack): 187 | self.add_script(ast_core.on_press(key, stack)) 188 | 189 | def proc_def(self, fmt=None, generator=None, locals_prefix=None, inline_only=False, turbo=True): 190 | if generator is None: # function decorator hackery 191 | return lambda generator: self.proc_def(fmt, generator, locals_prefix, inline_only, turbo) 192 | 193 | if fmt is None: 194 | arg_names = generator.__code__.co_varnames[:generator.__code__.co_argcount] 195 | fmt = generator.__name__ 196 | for arg in arg_names[1:]: # skip locals 197 | if generator.__annotations__.get(arg) is bool: 198 | fmt += f" <{arg}>" 199 | else: 200 | fmt += f" [{arg}]" 201 | 202 | uid = gen_uid(["procproto", fmt]) 203 | proc_proto = ast_core.ProcProto(self, fmt, uid, locals_prefix, turbo) 204 | 205 | for varname, vartype in zip(proc_proto.argnames, proc_proto.argtypes): 206 | varinit = ast_core.ProcVarBool if vartype == "bool" else ast_core.ProcVar 207 | proc_proto.vars.append( 208 | varinit( 209 | self, proc_proto, varname, 210 | gen_uid(["procvar1", fmt, varname]), 211 | gen_uid(["procvar2", fmt, varname]) 212 | ) 213 | ) 214 | 215 | procdef = ast_core.ProcDef(proc_proto, generator) 216 | 217 | if not inline_only: 218 | self.add_script([procdef] + generator(procdef, *proc_proto.vars)) 219 | 220 | return procdef 221 | 222 | def save(self, filename): 223 | self.project.asset_data = {} # XXX these are both written to by sprite.serialize() - they should probably be returned by it instead 224 | 225 | print(f"[*] Creating sprite3 file: {filename!r}") 226 | with ZipFile(filename, "w") as zf: 227 | with zf.open("sprite.json", "w") as spritefile: 228 | serialised = self.serialise() 229 | print(f"[*] Serialised {len(serialised['blocks']) + self.block_count} blocks") 230 | json_blob = json.dumps(serialised).encode() 231 | print(f"[*] sprite.json size: {len(json_blob)}") 232 | spritefile.write(json_blob) 233 | 234 | 235 | for asset_name, data in self.project.asset_data.items(): 236 | with zf.open(asset_name, "w") as f: 237 | f.write(data) 238 | 239 | print(f"[*] Done writing {filename!r} ({os.path.getsize(filename)} bytes)") 240 | 241 | def serialise(self, used_layers=None): 242 | self.block_count = 0 243 | self.blocks_json = {} 244 | self.hat_count = 0 245 | self.uid_ctr = 0 246 | 247 | if not self.costumes: 248 | self.add_costume("costume", BLANK_SVG, "svg") 249 | 250 | sprite = { 251 | "isStage": self.is_stage, 252 | "name": self.name, 253 | "variables": { 254 | uid: [name, self.variable_values[uid]] 255 | for name, uid 256 | in self.variable_uids.items() 257 | }, 258 | "lists": { 259 | uid: [name, self.list_values[uid]] 260 | for name, uid 261 | in self.list_uids.items() 262 | }, 263 | "broadcasts": {}, 264 | "blocks": self.blocks_json, 265 | "comments": {}, 266 | "currentCostume": self.current_costume, 267 | "costumes": [], 268 | "sounds": [], 269 | "volume": self.volume 270 | } 271 | 272 | # we don't care about this if we're serialising for a .sprite3 273 | if used_layers is not None: 274 | # find the next unused layer 275 | for i in range(99999999): 276 | if i not in used_layers: 277 | sprite["layerOrder"] = i 278 | used_layers.add(i) 279 | break 280 | else: 281 | raise Exception("Too many layers?!??!") 282 | 283 | if self.is_stage: 284 | sprite.update({ 285 | "tempo": 60, 286 | "videoTransparency": 50, 287 | "videoState": "on", 288 | "textToSpeechLanguage": None, 289 | }) 290 | else: 291 | sprite.update({ 292 | "visible": True, 293 | "x": 0, 294 | "y": 0, 295 | "size": 100, 296 | "direction": 90, 297 | "draggable": False, 298 | "rotationStyle": "all around" 299 | }) 300 | 301 | for costume_name, (data, extension, center) in self.costumes.items(): 302 | md5 = hashlib.md5(data).hexdigest() 303 | md5ext = f"{md5}.{extension}" 304 | sprite["costumes"].append({ 305 | "assetId": md5, 306 | "name": costume_name, 307 | "md5ext": md5ext, 308 | "dataFormat": extension, 309 | "rotationCenterX": center[0], 310 | "rotationCenterY": center[1] 311 | }) 312 | self.project.asset_data[md5ext] = data 313 | 314 | for script in self.scripts: 315 | self.serialise_script(script) 316 | 317 | return sprite 318 | 319 | def serialise_script(self, script, parent=None): 320 | top_uid = None 321 | script = flatten(script) 322 | for statement in script: 323 | uid = self.serialise_statement(statement) 324 | top_uid = top_uid or uid 325 | self.blocks_json[uid]["next"] = None 326 | if parent: 327 | self.blocks_json[uid]["parent"] = parent 328 | self.blocks_json[parent]["next"] = uid 329 | else: 330 | self.blocks_json[uid].update({ 331 | "parent": None, 332 | "topLevel": True, 333 | "x": self.hat_count * 500, # naively arrange in columns 334 | "y": 0 335 | }) 336 | self.hat_count += 1 337 | parent = uid 338 | 339 | return [2, top_uid] 340 | 341 | def serialise_arg(self, expression, parent, alternative=[10, ""]): 342 | #expression = expression.simplified() # experimental! 343 | 344 | # primitive expressions https://github.com/LLK/scratch-vm/blob/80e25f7b2a47ec2f3d8bb05fb62c7ceb8a1c99f0/src/serialization/sb3.js#L63 345 | if type(expression) is ast_core.Literal: 346 | return [1, [10 if type(expression.value) is str else 4, str(expression.value)]] 347 | if type(expression) is ast_core.LiteralColour: 348 | return [1, [9, expression.value]] 349 | if type(expression) is ast_core.Var: 350 | self.block_count += 1 351 | return [3, [12, expression.name, expression.uid], alternative] 352 | if type(expression) is ast_core.List: 353 | self.block_count += 1 354 | return [3, [13, expression.name, expression.uid], alternative] 355 | if issubclass(type(expression), ast_core.MenuExpression): 356 | #print("MenuExpression detected!") 357 | # todo: how does this affect block count? 358 | return [1, self.serialise_expression(expression, parent, shadow=True)] 359 | 360 | # compound expressions 361 | return [3, self.serialise_expression(expression, parent), alternative] 362 | 363 | def serialise_bool(self, expression, parent): 364 | if expression.type != "bool": 365 | raise Exception("Cannot serialise non-bool expression as bool: " + repr(expression)) 366 | return [2, self.serialise_expression(expression, parent)] 367 | 368 | def serialise_procproto(self, proto, parent): 369 | inputs = {} 370 | self.block_count -= 1 371 | 372 | for var in proto.vars: 373 | self.block_count -= 1 374 | inputs[var.uid2] = [1, var.uid] 375 | self.serialise_expression(var, proto.uid, shadow=True) 376 | 377 | self.blocks_json[proto.uid] = { 378 | "opcode": "procedures_prototype", 379 | "next": None, 380 | "parent": parent, 381 | "inputs": inputs, 382 | "fields": {}, 383 | "shadow": True, 384 | "topLevel": False, 385 | "mutation": { 386 | "tagName": "mutation", 387 | "children": [], 388 | "proccode": proto.proccode, 389 | "argumentids": json.dumps(list(inputs.keys())), 390 | "argumentnames": json.dumps(proto.argnames), 391 | "argumentdefaults": json.dumps(["false" if x == "bool" else "" for x in proto.argtypes]), 392 | "warp": "true" if proto.turbo else "false" 393 | } 394 | } 395 | return [1, proto.uid] 396 | 397 | def serialise_expression(self, expression, parent, shadow=False): 398 | return serialise_expression(self, expression, parent, shadow) 399 | 400 | def serialise_statement(self, statement): 401 | return serialise_statement(self, statement) 402 | 403 | def gen_uid(self, seed=None): 404 | if seed is None: 405 | seed = [self.uid_ctr] 406 | self.uid_ctr += 1 407 | seed = [self.name] + seed 408 | return gen_uid(seed) 409 | -------------------------------------------------------------------------------- /boiga/ast_core.py: -------------------------------------------------------------------------------- 1 | import math 2 | import operator 3 | 4 | 5 | def is_expression(value): 6 | return issubclass(type(value), Expression) 7 | 8 | 9 | def ensure_expression(value): 10 | if is_expression(value): 11 | return value 12 | if type(value) in [str, int, float]: 13 | return Literal(value) 14 | if type(value) is ProcCall: 15 | raise Exception("Scratch procedure calls cannot be used as expressions!") 16 | raise Exception(f"Can't interpret {value!r} as Expression") 17 | 18 | 19 | class Expression(): 20 | type = "generic" 21 | 22 | def __init__(self): 23 | raise Exception("Expression can't be instantiated directly (Maybe you want a Literal?)") 24 | 25 | def __add__(self, other): 26 | return BinaryOp("+", self, other) 27 | 28 | def __radd__(self, other): 29 | return BinaryOp("+", other, self) 30 | 31 | def __sub__(self, other): 32 | return BinaryOp("-", self, other) 33 | 34 | def __rsub__(self, other): 35 | return BinaryOp("-", other, self) 36 | 37 | def __neg__(self): 38 | return Literal(0) - self 39 | 40 | def __mul__(self, other): 41 | return BinaryOp("*", self, other) 42 | 43 | def __rmul__(self, other): 44 | return BinaryOp("*", other, self) 45 | 46 | def __mod__(self, other): 47 | return BinaryOp("%", self, other) 48 | 49 | def __rmod__(self, other): 50 | return BinaryOp("%", other, self) 51 | 52 | def __truediv__(self, other): 53 | return BinaryOp("/", self, other) 54 | 55 | def __rtruediv__(self, other): 56 | return BinaryOp("/", other, self) 57 | 58 | def __floordiv__(self, other): 59 | return (self / other).__floor__() 60 | 61 | def __rfloordiv__(self, other): 62 | return (other / self).__floor__() 63 | 64 | def __eq__(self, other): 65 | return BinaryOp("==", self, other) 66 | 67 | def __ne__(self, other): 68 | return BinaryOp("==", self, other).NOT() 69 | 70 | def __gt__(self, other): 71 | return BinaryOp(">", self, other) 72 | 73 | def __lt__(self, other): 74 | return BinaryOp("<", self, other) 75 | 76 | def __floor__(self): 77 | return UnaryOp("floor", self) 78 | 79 | def __ceil__(self): 80 | return UnaryOp("ceiling", self) 81 | 82 | def __round__(self): 83 | return UnaryOp("round", self) 84 | 85 | def round(self): 86 | return self.__round__() 87 | 88 | def __abs__(self): 89 | return UnaryOp("abs", self) 90 | 91 | def sqrt(self): 92 | return UnaryOp("sqrt", self) 93 | 94 | def sin(self): 95 | return UnaryOp("sin", self) 96 | 97 | def cos(self): 98 | return UnaryOp("cos", self) 99 | 100 | def atan(self): 101 | return UnaryOp("atan", self) 102 | 103 | def log(self): 104 | return UnaryOp("ln", self) 105 | 106 | def log10(self): 107 | return UnaryOp("log", self) 108 | 109 | def __pow__(self, other): 110 | return UnaryOp("e ^", (self.log() * other)) 111 | 112 | #def __rpow__(self, other): 113 | # TODO 114 | # return UnaryOp("e ^", (other.log() * self)) 115 | 116 | def root(self, other): 117 | return UnaryOp("e ^", (self.log() / other)) 118 | 119 | def __rshift__(self, other): 120 | if type(other) is int: 121 | return self // (1 << other) 122 | return self // round(Literal(2) ** other) # exponentiation relies on log, so results need rounding 123 | 124 | def __lshift__(self, other): 125 | if type(other) is int: 126 | return self * (1 << other) 127 | return self * round(Literal(2) ** other) # exponentiation relies on log, so results need rounding 128 | 129 | def __and__(self, other): 130 | if not type(other) is int: 131 | raise Exception("Can only AND with constant (for now)") 132 | is_low_mask = (other & (other + 1) == 0) 133 | if not is_low_mask: 134 | raise Exception("AND can only be used to mask off low bits, for now") 135 | return self % (other + 1) 136 | 137 | def __rand__(self, other): 138 | return self.__and__(other) 139 | 140 | def __getitem__(self, other): 141 | return BinaryOp("[]", ensure_expression(other+1).simplified(), self) 142 | 143 | def item(self, other): 144 | return BinaryOp("[]", ensure_expression(other), self) 145 | 146 | def len(self): 147 | return UnaryOp("len", self) 148 | 149 | def join(self, other): 150 | return BinaryOp("join", self, other) 151 | 152 | def simplified(self): 153 | return self 154 | 155 | def OR(self, other): 156 | return BinaryOp("||", self, other) 157 | 158 | def AND(self, other): 159 | return BinaryOp("&&", self, other) 160 | 161 | def NOT(self): 162 | return UnaryOp("!", self) 163 | 164 | 165 | class Literal(Expression): 166 | def __init__(self, value): 167 | assert(type(value) in [str, int, float]) 168 | self.value = value 169 | 170 | def __repr__(self): 171 | #return f"Literal({self.value!r})" 172 | return repr(self.value) 173 | 174 | 175 | class LiteralColour(Expression): 176 | def __init__(self, value): 177 | self.value = value 178 | 179 | def __repr__(self): 180 | return f"LiteralColour({self.value!r})" 181 | 182 | 183 | class BinaryOp(Expression): 184 | def __init__(self, op, lval, rval): 185 | self.op = op 186 | self.lval = ensure_expression(lval) 187 | self.rval = ensure_expression(rval) 188 | 189 | if op in [">", "<", "==", "&&", "||"]: 190 | self.type = "bool" 191 | 192 | def __repr__(self): 193 | return f"BinaryOpExpression({self.lval!r} {self.op} {self.rval!r})" 194 | 195 | def simplified(self): 196 | # todo: don't modify in-place... 197 | simpler = BinaryOp(self.op, self.lval.simplified(), self.rval.simplified()) 198 | #print(simpler) 199 | 200 | #print("simplifying:", self) 201 | match simpler: 202 | case BinaryOp( 203 | lval=Literal(value=int()|float()), 204 | op="+"|"-"|"%"|"*"|"/", 205 | rval=Literal(value=int()|float()) 206 | ): 207 | opmap = { 208 | "+": operator.add, 209 | "-": operator.sub, 210 | "%": operator.mod, 211 | "*": operator.mul, 212 | "/": operator.truediv, 213 | } 214 | 215 | # convert the inputs to floats, to emulate scratch's arithmetic 216 | value = opmap[simpler.op](float(simpler.lval.value), float(simpler.rval.value)) 217 | if value == math.floor(value): 218 | value = int(value) # if we can, convert back to int, to avoid redundant ".0"s in the output 219 | return Literal(value) 220 | 221 | # foo * 1 => foo 222 | case BinaryOp(op=("*"|"/"), rval=Literal(value=1)): 223 | return simpler.lval 224 | 225 | # 1 * foo => foo 226 | case BinaryOp(lval=Literal(value=1), op="*"): 227 | return simpler.rval 228 | 229 | # foo + 0 => foo 230 | case BinaryOp(op=("+"|"-"), rval=Literal(value=0)): 231 | return simpler.lval 232 | 233 | # 0 + foo => foo 234 | case BinaryOp(lval=Literal(value=0), op="+"): 235 | return simpler.rval 236 | 237 | # ((foo + a) + b) => (foo + (a+b)) 238 | case BinaryOp( 239 | lval=BinaryOp( 240 | lval=subexpr, 241 | op=("+"|"-"), 242 | rval=Literal() 243 | ), 244 | op=("+"|"-"), 245 | rval=Literal() 246 | ): 247 | a = simpler.rval.value if simpler.op == "+" else -simpler.rval.value 248 | b = simpler.lval.rval.value if simpler.lval.op == "+" else -simpler.lval.rval.value 249 | val = a + b 250 | if val == 0: 251 | return subexpr 252 | return BinaryOp("+", subexpr, val) 253 | 254 | return simpler 255 | 256 | 257 | class UnaryOp(Expression): 258 | def __init__(self, op, value): 259 | self.op = op 260 | self.value = ensure_expression(value) 261 | 262 | if op == "!": 263 | self.type = "bool" 264 | 265 | #def simplified(self): 266 | # return UnaryOp(self.op, self.value.simplified()) 267 | 268 | def __repr__(self): 269 | return f"UnaryOpExpression({self.op}({self.value!r}))" 270 | 271 | 272 | class Var(Expression): 273 | def __init__(self, sprite, name, uid): 274 | self.sprite = sprite 275 | self.name = name 276 | self.uid = uid 277 | 278 | def __le__(self, other): 279 | other = ensure_expression(other) 280 | 281 | # If other is (self+x) or (x+self), optimise to "change by" 282 | if type(other) is BinaryOp and other.op == "+": 283 | #print(repr(other)) 284 | if other.lval is self: 285 | return Statement("data_changevariableby", VARIABLE=self, VALUE=other.rval) 286 | elif other.rval is self: 287 | return Statement("data_changevariableby", VARIABLE=self, VALUE=other.lval) 288 | 289 | return Statement("data_setvariableto", VARIABLE=self, VALUE=other) 290 | 291 | def __getitem__(self, _slice): 292 | if type(_slice) is not slice: 293 | #raise Exception("You can't index a non-list variable") 294 | return super().__getitem__(_slice) 295 | 296 | return VarRangeIterationHack(self, _slice.start, _slice.stop, _slice.step) 297 | 298 | def changeby(self, other): 299 | return Statement("data_changevariableby", VARIABLE=self, VALUE=ensure_expression(other)) 300 | 301 | def __repr__(self): 302 | return f"Var({self.sprite.name}: {self.name})" 303 | 304 | class VarRangeIterationHack(): 305 | def __init__(self, var, start, stop, step): 306 | self.var = var 307 | self.start = 0 if start is None else start 308 | self.stop = stop 309 | self.step = 1 if step is None else step 310 | 311 | def __rshift__(self, values): 312 | if type(self.start) is int and type(self.stop) is int and type(self.step) is int: 313 | return varloop(self.var, range(self.start, self.stop, self.step), values) 314 | else: 315 | return condvarloop(self.var, self.start, self.stop, self.step, values) 316 | 317 | class List(Expression): 318 | def __init__(self, sprite, name, uid): 319 | self.sprite = sprite 320 | self.name = name 321 | self.uid = uid 322 | 323 | def append(self, other): 324 | return Statement("data_addtolist", LIST=self, ITEM=ensure_expression(other)) 325 | 326 | def delete_all(self): 327 | return Statement("data_deletealloflist", LIST=self) 328 | 329 | def delete_at(self, other): 330 | return Statement("data_deleteoflist", LIST=self, INDEX=(ensure_expression(other)+1).simplified()) 331 | 332 | def delete_at1(self, other): 333 | return Statement("data_deleteoflist", LIST=self, INDEX=ensure_expression(other)) 334 | 335 | def len(self): 336 | return UnaryOp("listlen", self) 337 | 338 | def index(self, other): 339 | return ListItemNum(self, other) - 1 340 | 341 | def index1(self, other): 342 | return ListItemNum(self, other) 343 | 344 | def __repr__(self): 345 | return f"ListVar({self.sprite.name}: {self.name})" 346 | 347 | def __getitem__(self, index): 348 | return ListIndex(self, (ensure_expression(index)+1).simplified()) 349 | 350 | def item(self, one_index): 351 | return ListIndex(self, one_index) 352 | 353 | def contains(self, thing): 354 | return ListContains(self, thing) 355 | 356 | class ListItemNum(Expression): 357 | def __init__(self, list_, item): 358 | self.list = list_ 359 | self.item = ensure_expression(item) 360 | 361 | class ListIndex(Expression): 362 | def __init__(self, list_, index): 363 | self.list = list_ 364 | self.index = ensure_expression(index) 365 | 366 | def __le__(self, other): 367 | other = ensure_expression(other) 368 | return Statement("data_replaceitemoflist", LIST=self.list, INDEX=self.index, ITEM=other) 369 | 370 | def __repr__(self): 371 | return f"{self.list!r}[{self.index!r}]" 372 | 373 | class ListContains(Expression): 374 | type = "bool" 375 | def __init__(self, list_, thing): 376 | self.list = list_ 377 | self.thing = ensure_expression(thing) 378 | 379 | class Statement(): 380 | def __init__(self, op, **args): 381 | self.op = op 382 | self.args = args 383 | 384 | def __repr__(self): 385 | return f"Statement({self.op}, {self.args!r})" 386 | 387 | class ElseHack(): 388 | def __init__(self, condition, then): 389 | self.condition = condition 390 | self.then = then 391 | 392 | def __getitem__(self, do): 393 | if type(do) != tuple: 394 | do = [do] 395 | return IfElseStatement(self.condition, self.then, list(do)) 396 | 397 | class IfStatement(Statement): 398 | def __init__(self, condition, then): 399 | super().__init__("control_if", CONDITION=ensure_expression(condition), SUBSTACK=then) 400 | self.Else = ElseHack(condition, then) 401 | 402 | class IfElseStatement(Statement): 403 | def __init__(self, condition, then, elsedo): 404 | super().__init__("control_if_else", CONDITION=ensure_expression(condition), SUBSTACK=then, SUBSTACK2=elsedo) 405 | 406 | 407 | 408 | 409 | 410 | 411 | class ProcDef(Statement): 412 | def __init__(self, proto, generator): 413 | self.op = "procedures_definition" 414 | self.proto = proto 415 | self.generator = generator # todo: maybe store generator inside proto? 416 | 417 | def __call__(self, *args): 418 | if len(args) != len(self.proto.vars): 419 | raise Exception(f"{self!r} expects {len(self.proto.vars)} args, {len(args)} given") 420 | 421 | return ProcCall(self, args, self.generator) 422 | 423 | def __getattr__(self, attr): 424 | varname = self.proto.locals_prefix + attr 425 | return self.proto.sprite.new_var(varname) 426 | 427 | def __repr__(self): 428 | return f"ProcDef({self.proto!r})" 429 | 430 | class ProcProto(Statement): 431 | def __init__(self, sprite, fmt, uid, locals_prefix, turbo=True): 432 | self.op = "procedures_prototype" 433 | self.sprite = sprite 434 | self.uid = uid 435 | self.fmt = fmt 436 | self.locals_prefix = fmt + ":" if locals_prefix is None else locals_prefix 437 | self.turbo = turbo 438 | 439 | # quick and dirty parser state machine 440 | # [square] brackets denote numeric/string args, brackets denote bool args 441 | # (does anyone ever actually use bool args?) 442 | self.argtypes = [] 443 | self.proccode = "" 444 | self.argnames = [] 445 | mode = "label" 446 | for char in fmt: 447 | if mode == "label": 448 | if char == "[": 449 | self.argtypes.append("generic") 450 | self.argnames.append("") 451 | self.proccode += "%s" 452 | mode = "strarg" 453 | elif char == "<": 454 | self.argtypes.append("bool") 455 | self.argnames.append("") 456 | self.proccode += "%b" 457 | mode = "boolarg" 458 | else: 459 | self.proccode += char 460 | elif mode == "strarg": 461 | if char == "]": 462 | mode = "label" 463 | else: 464 | self.argnames[-1] += char 465 | elif mode == "boolarg": 466 | if char == ">": 467 | mode = "label" 468 | else: 469 | self.argnames[-1] += char 470 | else: 471 | raise Exception("Invalid parser state") 472 | 473 | #print(self.argtypes, self.proccode, self.argnames) 474 | 475 | # NOTE: codegen will initialise self.vars 476 | self.vars = [] 477 | 478 | def __repr__(self): 479 | return f"ProcProto({self.fmt!r})" 480 | 481 | 482 | class ProcCall(Statement): 483 | def __init__(self, proc, args, generator, turbo=True): 484 | self.procdef = proc # todo: fix these field names lol 485 | self.proc = proc.proto 486 | self.argv = list(map(ensure_expression, args)) 487 | self.generator = generator 488 | 489 | for arg, argtype in zip(self.argv, proc.proto.argtypes): 490 | if argtype == "bool" and arg.type != "bool": 491 | raise Exception("Cannot pass non-boolean expression to boolean proc arg") 492 | 493 | super().__init__("procedures_call", PROC=proc.proto.uid, ARGS=self.argv) 494 | 495 | def inline(self): 496 | return self.generator(self.procdef, *self.argv) # todo use better variable namespacing 497 | 498 | 499 | class ProcVar(Expression): 500 | def __init__(self, sprite, procproto, name, uid, uid2): 501 | self.sprite = sprite 502 | self.procproto = procproto 503 | self.name = name 504 | self.uid = uid 505 | self.uid2 = uid2 506 | 507 | def __repr__(self): 508 | return f"ProcVar({self.procproto.fmt!r}: {self.name})" 509 | 510 | class ProcVarBool(Expression): 511 | def __init__(self, sprite, procproto, name, uid, uid2): 512 | self.type = "bool" 513 | self.sprite = sprite 514 | self.procproto = procproto 515 | self.name = name 516 | self.uid = uid 517 | self.uid2 = uid2 518 | 519 | def __repr__(self): 520 | return f"ProcVarBool({self.procproto.fmt!r}: {self.name})" 521 | 522 | 523 | def getitem_hack(fn, *args): 524 | class GetitemHack(): 525 | def __init__(self, *args): 526 | self.args = args 527 | 528 | def __getitem__(self, then): 529 | if type(then) != tuple: 530 | then = [then] 531 | return fn(*self.args, list(then)) 532 | 533 | return GetitemHack(*args) 534 | 535 | def varloop(var, _range, body): return [ 536 | var <= _range.start, 537 | repeatn(len(_range), 538 | body + 539 | [ var <= var + _range.step ] 540 | ) 541 | ] 542 | 543 | # when number of iterations not known at compile-time 544 | def condvarloop(var, start, stop, step, body): return [ 545 | var <= start, 546 | repeatuntil((var + 1).simplified() > stop, 547 | body + 548 | [var.changeby(step)] 549 | ) 550 | ] 551 | 552 | def repeatn(times, body=None): 553 | if body is None: 554 | return getitem_hack(repeatn, times) 555 | return Statement("control_repeat", TIMES=ensure_expression(times), SUBSTACK=body) 556 | 557 | def repeatuntil(cond, body=None): 558 | if body is None: 559 | return getitem_hack(repeatuntil, cond) 560 | return Statement("control_repeat_until", CONDITION=ensure_expression(cond), SUBSTACK=body) 561 | 562 | def on_flag(substack=None): 563 | return [Statement("event_whenflagclicked")] + substack 564 | 565 | def on_receive(event, event_uid, substack=None): 566 | return [Statement("event_whenbroadcastreceived", BROADCAST_OPTION=[str(event), event_uid])] + substack 567 | 568 | def on_press(key, substack=None): 569 | # TODO: use enum for key, check the argument is actually an enum field 570 | return [Statement("event_whenkeypressed", KEY_OPTION=str(key))] + substack 571 | 572 | class MenuExpression(Expression): 573 | pass 574 | 575 | class PenParamMenu(MenuExpression): 576 | def __init__(self, param): 577 | self.op = "pen_menu_colorParam" 578 | self.param = param 579 | 580 | # todo: probably need one of these for costume selection 581 | class TouchingObjectMenu(MenuExpression): 582 | def __init__(self, object): 583 | self.op = "sensing_touchingobjectmenu" 584 | self.object = object 585 | 586 | class Costume(MenuExpression): 587 | def __init__(self, costume): 588 | self.op = "looks_costume" 589 | self.costumename = costume 590 | 591 | class Instrument(MenuExpression): 592 | def __init__(self, instrument): 593 | if type(instrument) is not int: 594 | raise TypeError() 595 | self.op = "music_menu_INSTRUMENT" 596 | self.instrument = instrument 597 | 598 | class Drum(MenuExpression): 599 | def __init__(self, drum): 600 | if type(drum) is not int: 601 | raise TypeError() 602 | self.op = "music_menu_DRUM" 603 | self.drum = drum 604 | 605 | if __name__ == "__main__": 606 | class Sprite(): 607 | name = "Sprite" 608 | s = Sprite() 609 | print(math.floor((Var(s, "foo", None) + 7 + 3) * 5) == Literal(3) / 5) 610 | print(List(s, "bar", None)[5]) 611 | print(Literal(3) + 4) 612 | print(Literal(123) >> 2) 613 | print(Literal(1234123) & 0xFF) 614 | print(Literal(5) > 7) 615 | #List("foo")[5] = 123 616 | --------------------------------------------------------------------------------