typescratch 1.3.0__tar.gz → 1.6.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {typescratch-1.3.0 → typescratch-1.6.2}/PKG-INFO +2 -1
- {typescratch-1.3.0 → typescratch-1.6.2}/pyproject.toml +7 -2
- {typescratch-1.3.0/typescratch_pypi_release/typescratch_src → typescratch-1.6.2/typescratch}/__init__.py +42 -1
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch/ast_nodes.py +36 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch/blocks.py +46 -5
- typescratch-1.6.2/typescratch/cli.py +171 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch/codegen.py +129 -3
- typescratch-1.6.2/typescratch/decompiler.py +871 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch/ide.py +14 -6
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch/lexer.py +3 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch/logo.svg +0 -0
- typescratch-1.6.2/typescratch/logo_ascii.txt +5 -0
- typescratch-1.6.2/typescratch/meow.txt +48 -0
- typescratch-1.6.2/typescratch/package_manager.py +127 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch/parser.py +129 -8
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch.egg-info/PKG-INFO +2 -1
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch.egg-info/SOURCES.txt +8 -0
- typescratch-1.6.2/typescratch.egg-info/requires.txt +3 -0
- {typescratch-1.3.0/typescratch → typescratch-1.6.2/typescratch_pypi_release/typescratch_src}/__init__.py +42 -1
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/ast_nodes.py +36 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/blocks.py +13 -5
- typescratch-1.6.2/typescratch_pypi_release/typescratch_src/cli.py +171 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/codegen.py +156 -24
- typescratch-1.6.2/typescratch_pypi_release/typescratch_src/decompiler.py +868 -0
- typescratch-1.6.2/typescratch_pypi_release/typescratch_src/ide.py +515 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/lexer.py +3 -0
- typescratch-1.6.2/typescratch_pypi_release/typescratch_src/package_manager.py +127 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/parser.py +129 -8
- typescratch-1.3.0/typescratch/cli.py +0 -76
- typescratch-1.3.0/typescratch_pypi_release/typescratch_src/cli.py +0 -67
- {typescratch-1.3.0 → typescratch-1.6.2}/LICENSE +0 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/README.md +0 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/setup.cfg +0 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/setup.py +0 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/tests/test_basic.py +0 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch/__main__.py +0 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch/assets.py +0 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch/errors.py +0 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch/sb3.py +0 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch/scratchcat_official.svg +0 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch.egg-info/dependency_links.txt +0 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch.egg-info/entry_points.txt +0 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch.egg-info/top_level.txt +0 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch_pypi_release/setup.py +0 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/__main__.py +0 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/assets.py +0 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/errors.py +0 -0
- {typescratch-1.3.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/sb3.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: typescratch
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.2
|
|
4
4
|
Summary: A programming language that compiles to Scratch 3 (.sb3) files
|
|
5
5
|
Author: TypeScratch
|
|
6
6
|
License: Apache-2.0
|
|
@@ -23,6 +23,7 @@ Classifier: Topic :: Software Development :: Compilers
|
|
|
23
23
|
Requires-Python: >=3.8
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
License-File: LICENSE
|
|
26
|
+
Requires-Dist: windows-curses>=2.0; sys_platform == "win32"
|
|
26
27
|
Dynamic: license-file
|
|
27
28
|
|
|
28
29
|
# TypeScratch
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "typescratch"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.6.2"
|
|
8
8
|
description = "A programming language that compiles to Scratch 3 (.sb3) files"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -25,6 +25,11 @@ classifiers = [
|
|
|
25
25
|
"Topic :: Education",
|
|
26
26
|
"Topic :: Software Development :: Compilers",
|
|
27
27
|
]
|
|
28
|
+
# On Windows, curses is not built-in, so we need windows-curses.
|
|
29
|
+
# The platform marker ensures pip only installs it on Windows.
|
|
30
|
+
dependencies = [
|
|
31
|
+
"windows-curses>=2.0; sys_platform == 'win32'",
|
|
32
|
+
]
|
|
28
33
|
|
|
29
34
|
[project.scripts]
|
|
30
35
|
typescratch = "typescratch.cli:main"
|
|
@@ -39,4 +44,4 @@ include = ["typescratch*"]
|
|
|
39
44
|
exclude = ["tests*"]
|
|
40
45
|
|
|
41
46
|
[tool.setuptools.package-data]
|
|
42
|
-
typescratch = ["*.svg", "*.wav"]
|
|
47
|
+
typescratch = ["*.svg", "*.wav", "*.txt"]
|
|
@@ -46,7 +46,7 @@ from .codegen import Codegen
|
|
|
46
46
|
from .sb3 import write_sb3, write_sb3_bytes
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
__version__ = "1.2
|
|
49
|
+
__version__ = "1.6.2"
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
def compile_source(src: str, filename: str = "<tysh>", debug: bool = False,
|
|
@@ -58,6 +58,10 @@ def compile_source(src: str, filename: str = "<tysh>", debug: bool = False,
|
|
|
58
58
|
If None, defaults to the directory of `filename` (or cwd).
|
|
59
59
|
"""
|
|
60
60
|
dbg = Debug(enabled=debug)
|
|
61
|
+
|
|
62
|
+
# Preprocess: handle `fuse "file.tysh"` directives
|
|
63
|
+
src = _preprocess_fuse(src, filename, dbg)
|
|
64
|
+
|
|
61
65
|
dbg.section("Tokens")
|
|
62
66
|
toks = tokenize(src, filename)
|
|
63
67
|
for t in toks:
|
|
@@ -83,6 +87,43 @@ def compile_source(src: str, filename: str = "<tysh>", debug: bool = False,
|
|
|
83
87
|
return project, assets
|
|
84
88
|
|
|
85
89
|
|
|
90
|
+
def _preprocess_fuse(src: str, filename: str, dbg: Debug) -> str:
|
|
91
|
+
"""Handle `fuse "file.tysh"` directives by inlining file contents.
|
|
92
|
+
|
|
93
|
+
The fuse directive must be on its own line. The file path is relative
|
|
94
|
+
to the directory of the source file.
|
|
95
|
+
"""
|
|
96
|
+
import re
|
|
97
|
+
base_dir = os.path.dirname(os.path.abspath(filename)) if filename != "<tysh>" else os.getcwd()
|
|
98
|
+
|
|
99
|
+
lines = src.split("\n")
|
|
100
|
+
result = []
|
|
101
|
+
for line in lines:
|
|
102
|
+
stripped = line.strip()
|
|
103
|
+
# Match: fuse "file.tysh" or fuse 'file.tysh'
|
|
104
|
+
m = re.match(r'^fuse\s+["\']([^"\']+)["\']\s*$', stripped)
|
|
105
|
+
if m:
|
|
106
|
+
fuse_path = m.group(1)
|
|
107
|
+
if not os.path.isabs(fuse_path):
|
|
108
|
+
fuse_path = os.path.join(base_dir, fuse_path)
|
|
109
|
+
if os.path.isfile(fuse_path):
|
|
110
|
+
dbg.line(f"Fusing: {fuse_path}")
|
|
111
|
+
with open(fuse_path, "r", encoding="utf-8") as f:
|
|
112
|
+
fused_content = f.read()
|
|
113
|
+
# Recursively process fuse directives in the fused file
|
|
114
|
+
fused_content = _preprocess_fuse(fused_content, fuse_path, dbg)
|
|
115
|
+
result.append(f"// === fused from {m.group(1)} ===")
|
|
116
|
+
result.extend(fused_content.split("\n"))
|
|
117
|
+
result.append(f"// === end fuse ===")
|
|
118
|
+
else:
|
|
119
|
+
from .errors import ForgotOops
|
|
120
|
+
raise ForgotOops(f'Cannot fuse file "{m.group(1)}" - file not found')
|
|
121
|
+
else:
|
|
122
|
+
result.append(line)
|
|
123
|
+
|
|
124
|
+
return "\n".join(result)
|
|
125
|
+
|
|
126
|
+
|
|
86
127
|
def source(src: str, out: Optional[str] = None, debug: bool = False,
|
|
87
128
|
filename: str = "<tysh>") -> bytes:
|
|
88
129
|
"""Compile a TypeScratch source string and return the .sb3 bytes.
|
|
@@ -361,3 +361,39 @@ class ListReporterRef:
|
|
|
361
361
|
method: str # 'item' | 'length' | 'contains' | 'itemNum' | 'contents'
|
|
362
362
|
args: List[Any]
|
|
363
363
|
line: int = 0
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@dataclass
|
|
367
|
+
class TernaryExpr:
|
|
368
|
+
"""Ternary expression: [if (cond) true_val else false_val]
|
|
369
|
+
|
|
370
|
+
Desugared at codegen time into if/else that sets a temp variable.
|
|
371
|
+
"""
|
|
372
|
+
cond: Any
|
|
373
|
+
true_expr: Any
|
|
374
|
+
false_expr: Any
|
|
375
|
+
line: int = 0
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@dataclass
|
|
379
|
+
class SoundImport:
|
|
380
|
+
"""sound name from "path.wav" - imports a sound file."""
|
|
381
|
+
name: str
|
|
382
|
+
path: str
|
|
383
|
+
line: int = 0
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@dataclass
|
|
387
|
+
class CostumeImport:
|
|
388
|
+
"""costume name from "path.png" - imports a costume file."""
|
|
389
|
+
name: str
|
|
390
|
+
path: str
|
|
391
|
+
line: int = 0
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@dataclass
|
|
395
|
+
class LocalVarDecl:
|
|
396
|
+
"""private x = value - declares a local variable inside a custom block."""
|
|
397
|
+
name: str
|
|
398
|
+
value: Any
|
|
399
|
+
line: int = 0
|
|
@@ -153,25 +153,51 @@ STACK_BLOCKS = {
|
|
|
153
153
|
"args": [],
|
|
154
154
|
},
|
|
155
155
|
"goToFront": {
|
|
156
|
+
"opcode": "looks_gotofrontback",
|
|
157
|
+
"fields": [("FRONT_BACK", "front")],
|
|
158
|
+
},
|
|
159
|
+
"goToBack": {
|
|
160
|
+
"opcode": "looks_gotofrontback",
|
|
161
|
+
"fields": [("FRONT_BACK", "back")],
|
|
162
|
+
},
|
|
163
|
+
"goFront": {
|
|
164
|
+
"opcode": "looks_gotofrontback",
|
|
165
|
+
"fields": [("FRONT_BACK", "front")],
|
|
166
|
+
},
|
|
167
|
+
"goBack": {
|
|
168
|
+
"opcode": "looks_gotofrontback",
|
|
169
|
+
"fields": [("FRONT_BACK", "back")],
|
|
170
|
+
},
|
|
171
|
+
"goForwardLayers": {
|
|
156
172
|
"opcode": "looks_goforwardbackwardlayers",
|
|
157
173
|
"fields": [("FRONTBACK", "front")],
|
|
158
|
-
"args": [("NUM", "number", "args"
|
|
174
|
+
"args": [("NUM", "number", "args")],
|
|
159
175
|
},
|
|
160
|
-
"
|
|
176
|
+
"goBackLayers": {
|
|
161
177
|
"opcode": "looks_goforwardbackwardlayers",
|
|
162
178
|
"fields": [("FRONTBACK", "back")],
|
|
163
|
-
"args": [("NUM", "number", "args"
|
|
179
|
+
"args": [("NUM", "number", "args")],
|
|
164
180
|
},
|
|
165
|
-
"
|
|
181
|
+
"goFwdLyrs": {
|
|
182
|
+
"opcode": "looks_goforwardbackwardlayers",
|
|
183
|
+
"fields": [("FRONTBACK", "front")],
|
|
184
|
+
"args": [("NUM", "number", "args")],
|
|
185
|
+
},
|
|
186
|
+
"goBwdLyrs": {
|
|
166
187
|
"opcode": "looks_goforwardbackwardlayers",
|
|
167
188
|
"fields": [("FRONTBACK", "back")],
|
|
168
189
|
"args": [("NUM", "number", "args")],
|
|
169
190
|
},
|
|
170
|
-
"
|
|
191
|
+
"goFwdLayers": {
|
|
171
192
|
"opcode": "looks_goforwardbackwardlayers",
|
|
172
193
|
"fields": [("FRONTBACK", "front")],
|
|
173
194
|
"args": [("NUM", "number", "args")],
|
|
174
195
|
},
|
|
196
|
+
"goBwdLayers": {
|
|
197
|
+
"opcode": "looks_goforwardbackwardlayers",
|
|
198
|
+
"fields": [("FRONTBACK", "back")],
|
|
199
|
+
"args": [("NUM", "number", "args")],
|
|
200
|
+
},
|
|
175
201
|
|
|
176
202
|
# ---- Sound ----
|
|
177
203
|
"playSound": {
|
|
@@ -552,8 +578,23 @@ REPORTERS = {
|
|
|
552
578
|
# Looks
|
|
553
579
|
"costumeName": {"opcode": "looks_costumename", "returns": "string"},
|
|
554
580
|
"costumeNumber": {"opcode": "looks_costumenumber", "returns": "number"},
|
|
581
|
+
"costumeNumberName": {
|
|
582
|
+
"opcode": "looks_costumenumbername",
|
|
583
|
+
"fields": [("NUMBER_NAME", None)],
|
|
584
|
+
"returns": "any",
|
|
585
|
+
},
|
|
586
|
+
"cstmNumName": {
|
|
587
|
+
"opcode": "looks_costumenumbername",
|
|
588
|
+
"fields": [("NUMBER_NAME", None)],
|
|
589
|
+
"returns": "any",
|
|
590
|
+
},
|
|
555
591
|
"backdropName": {"opcode": "looks_backdropname", "returns": "string"},
|
|
556
592
|
"backdropNumber": {"opcode": "looks_backdropnumber", "returns": "number"},
|
|
593
|
+
"backdropNumberName": {
|
|
594
|
+
"opcode": "looks_backdropnumbername",
|
|
595
|
+
"fields": [("NUMBER_NAME", None)],
|
|
596
|
+
"returns": "any",
|
|
597
|
+
},
|
|
557
598
|
"size": {"opcode": "looks_size", "returns": "number"},
|
|
558
599
|
|
|
559
600
|
# Sound
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""CLI entry point for TypeScratch.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
typescratch build <file.tysh> [--out <out.sb3>] [--debug]
|
|
5
|
+
typescratch run <file.tysh> [--debug]
|
|
6
|
+
typescratch ide [file.tysh]
|
|
7
|
+
typescratch decompile <file.sb3> [--out <file.tysh>]
|
|
8
|
+
typescratch fmt <file.tysh>
|
|
9
|
+
typescratch meow
|
|
10
|
+
typescratch version
|
|
11
|
+
typescratch help
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import sys
|
|
18
|
+
from typing import List, Optional
|
|
19
|
+
|
|
20
|
+
from . import __version__, file as _file
|
|
21
|
+
from .errors import CompileError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
25
|
+
parser = argparse.ArgumentParser(
|
|
26
|
+
prog="typescratch",
|
|
27
|
+
description="Compile .tysh source files to Scratch 3 (.sb3) files.",
|
|
28
|
+
)
|
|
29
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
30
|
+
|
|
31
|
+
p_build = sub.add_parser("build", help="Compile a .tysh file to .sb3")
|
|
32
|
+
p_build.add_argument("source", help="path to .tysh source file")
|
|
33
|
+
p_build.add_argument("--out", "-o", default=None, help="output .sb3 path")
|
|
34
|
+
p_build.add_argument("--debug", action="store_true", help="verbose debug output")
|
|
35
|
+
|
|
36
|
+
p_run = sub.add_parser("run", help="Alias for build")
|
|
37
|
+
p_run.add_argument("source")
|
|
38
|
+
p_run.add_argument("--out", "-o", default=None)
|
|
39
|
+
p_run.add_argument("--debug", action="store_true")
|
|
40
|
+
|
|
41
|
+
p_ide = sub.add_parser("ide", help="Launch the TypeScratch IDE")
|
|
42
|
+
p_ide.add_argument("source", nargs="?", default=None, help="optional .tysh file to open")
|
|
43
|
+
|
|
44
|
+
p_decompile = sub.add_parser("decompile", help="Decompile a .sb3 file to .tysh")
|
|
45
|
+
p_decompile.add_argument("source", help="path to .sb3 file")
|
|
46
|
+
p_decompile.add_argument("--out", "-o", default=None, help="output .tysh path")
|
|
47
|
+
|
|
48
|
+
p_fmt = sub.add_parser("fmt", help="Format a .tysh file (auto-indent)")
|
|
49
|
+
p_fmt.add_argument("source", help="path to .tysh file")
|
|
50
|
+
p_fmt.add_argument("--out", "-o", default=None, help="output path (default: in-place)")
|
|
51
|
+
|
|
52
|
+
sub.add_parser("meow", help="Print the Scratch Cat in ASCII art")
|
|
53
|
+
|
|
54
|
+
p_install = sub.add_parser("install", help="Install a package from GitHub")
|
|
55
|
+
p_install.add_argument("repo", help="github user/repo or user/repo@tag")
|
|
56
|
+
|
|
57
|
+
p_uninstall = sub.add_parser("uninstall", help="Remove an installed package")
|
|
58
|
+
p_uninstall.add_argument("name", help="package name")
|
|
59
|
+
|
|
60
|
+
sub.add_parser("list", help="List installed packages")
|
|
61
|
+
|
|
62
|
+
sub.add_parser("version", help="Print version")
|
|
63
|
+
|
|
64
|
+
args = parser.parse_args(argv)
|
|
65
|
+
|
|
66
|
+
if args.cmd == "version":
|
|
67
|
+
print(f"TypeScratch v{__version__}")
|
|
68
|
+
return 0
|
|
69
|
+
|
|
70
|
+
if args.cmd == "ide":
|
|
71
|
+
from .ide import run_ide
|
|
72
|
+
run_ide(args.source)
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
if args.cmd == "decompile":
|
|
76
|
+
from .decompiler import decompile_to_file
|
|
77
|
+
try:
|
|
78
|
+
out = decompile_to_file(args.source, args.out)
|
|
79
|
+
print(f"OK: decompiled to {out}", file=sys.stderr)
|
|
80
|
+
return 0
|
|
81
|
+
except Exception as e:
|
|
82
|
+
print(f"error: {e}", file=sys.stderr)
|
|
83
|
+
return 1
|
|
84
|
+
|
|
85
|
+
if args.cmd == "fmt":
|
|
86
|
+
return _format_file(args.source, args.out)
|
|
87
|
+
|
|
88
|
+
if args.cmd == "meow":
|
|
89
|
+
return _meow()
|
|
90
|
+
|
|
91
|
+
if args.cmd == "install":
|
|
92
|
+
from .package_manager import install
|
|
93
|
+
return install(args.repo)
|
|
94
|
+
|
|
95
|
+
if args.cmd == "uninstall":
|
|
96
|
+
from .package_manager import uninstall
|
|
97
|
+
return uninstall(args.name)
|
|
98
|
+
|
|
99
|
+
if args.cmd == "list":
|
|
100
|
+
from .package_manager import list_packages
|
|
101
|
+
return list_packages()
|
|
102
|
+
|
|
103
|
+
if args.cmd in ("build", "run"):
|
|
104
|
+
try:
|
|
105
|
+
out_path = _file(args.source, out=args.out, debug=args.debug)
|
|
106
|
+
print(f"OK: wrote {out_path}", file=sys.stderr)
|
|
107
|
+
return 0
|
|
108
|
+
except CompileError as e:
|
|
109
|
+
print(f"error: {e}", file=sys.stderr)
|
|
110
|
+
return 1
|
|
111
|
+
except FileNotFoundError as e:
|
|
112
|
+
print(f"error: {e}", file=sys.stderr)
|
|
113
|
+
return 2
|
|
114
|
+
except Exception as e:
|
|
115
|
+
print(f"internal error: {e}", file=sys.stderr)
|
|
116
|
+
import traceback
|
|
117
|
+
traceback.print_exc(file=sys.stderr)
|
|
118
|
+
return 3
|
|
119
|
+
|
|
120
|
+
parser.print_help()
|
|
121
|
+
return 1
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _format_file(path: str, out: Optional[str] = None) -> int:
|
|
125
|
+
"""Format a .tysh file with proper indentation."""
|
|
126
|
+
try:
|
|
127
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
128
|
+
lines = f.readlines()
|
|
129
|
+
except Exception as e:
|
|
130
|
+
print(f"error: {e}", file=sys.stderr)
|
|
131
|
+
return 1
|
|
132
|
+
|
|
133
|
+
formatted = []
|
|
134
|
+
indent = 0
|
|
135
|
+
for line in lines:
|
|
136
|
+
stripped = line.strip()
|
|
137
|
+
# Decrease indent for closing braces
|
|
138
|
+
if stripped.startswith("}"):
|
|
139
|
+
indent = max(0, indent - 1)
|
|
140
|
+
# Add the line with proper indent
|
|
141
|
+
if stripped:
|
|
142
|
+
formatted.append(" " * indent + stripped + "\n")
|
|
143
|
+
else:
|
|
144
|
+
formatted.append("\n")
|
|
145
|
+
# Increase indent after opening braces
|
|
146
|
+
# Count opening and closing braces on this line
|
|
147
|
+
opens = stripped.count("{") - stripped.count("}")
|
|
148
|
+
if opens > 0 and not stripped.startswith("}"):
|
|
149
|
+
indent += opens
|
|
150
|
+
|
|
151
|
+
out_path = out or path
|
|
152
|
+
with open(out_path, "w", encoding="utf-8") as f:
|
|
153
|
+
f.writelines(formatted)
|
|
154
|
+
print(f"OK: formatted {path}" + (f" -> {out_path}" if out else " (in-place)"), file=sys.stderr)
|
|
155
|
+
return 0
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _meow() -> int:
|
|
159
|
+
"""Print the Scratch Cat in ASCII art."""
|
|
160
|
+
import os
|
|
161
|
+
meow_path = os.path.join(os.path.dirname(__file__), "meow.txt")
|
|
162
|
+
try:
|
|
163
|
+
with open(meow_path, "r") as f:
|
|
164
|
+
print(f.read())
|
|
165
|
+
except FileNotFoundError:
|
|
166
|
+
print("Meow! 🐱")
|
|
167
|
+
return 0
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
sys.exit(main())
|
|
@@ -476,7 +476,7 @@ class Codegen:
|
|
|
476
476
|
"rotationCenterY": rcy,
|
|
477
477
|
})
|
|
478
478
|
else:
|
|
479
|
-
# Sprite costume
|
|
479
|
+
# Sprite costume - use img= if specified
|
|
480
480
|
path = target.img
|
|
481
481
|
data, _ignored_md5ext, fmt, rcx, rcy = self._load_asset(path, is_stage=False)
|
|
482
482
|
asset_id = _md5(data)
|
|
@@ -491,11 +491,73 @@ class Codegen:
|
|
|
491
491
|
"rotationCenterX": rcx,
|
|
492
492
|
"rotationCenterY": rcy,
|
|
493
493
|
})
|
|
494
|
+
# Additional costumes from `costume name from "path.png"` imports
|
|
495
|
+
for cinfo in getattr(target, "_costume_imports", []):
|
|
496
|
+
cname = cinfo["name"]
|
|
497
|
+
cpath = cinfo["path"]
|
|
498
|
+
cdata, _cmd5, cfmt, crcx, crcy = self._load_asset(cpath, is_stage=False)
|
|
499
|
+
casset_id = _md5(cdata)
|
|
500
|
+
cmd5ext = f"{casset_id}.{cfmt}"
|
|
501
|
+
self.assets[cmd5ext] = cdata
|
|
502
|
+
costumes.append({
|
|
503
|
+
"assetId": casset_id,
|
|
504
|
+
"name": cname,
|
|
505
|
+
"bitmapResolution": 1 if cfmt == "svg" else 2,
|
|
506
|
+
"md5ext": cmd5ext,
|
|
507
|
+
"dataFormat": cfmt,
|
|
508
|
+
"rotationCenterX": crcx,
|
|
509
|
+
"rotationCenterY": crcy,
|
|
510
|
+
})
|
|
494
511
|
return costumes
|
|
495
512
|
|
|
496
513
|
def gen_sounds(self, target: A.Target) -> List[dict]:
|
|
497
|
-
|
|
498
|
-
|
|
514
|
+
"""Generate sound assets from `sound name from "path.wav"` imports."""
|
|
515
|
+
sounds = []
|
|
516
|
+
for sinfo in getattr(target, "_sound_imports", []):
|
|
517
|
+
name = sinfo["name"]
|
|
518
|
+
path = sinfo["path"]
|
|
519
|
+
full = path
|
|
520
|
+
if self.asset_root and not os.path.isabs(path):
|
|
521
|
+
full = os.path.join(self.asset_root, path)
|
|
522
|
+
if os.path.isfile(full):
|
|
523
|
+
with open(full, "rb") as f:
|
|
524
|
+
data = f.read()
|
|
525
|
+
# Determine format
|
|
526
|
+
if full.lower().endswith(".wav"):
|
|
527
|
+
fmt = "wav"
|
|
528
|
+
elif full.lower().endswith(".mp3"):
|
|
529
|
+
fmt = "mp3"
|
|
530
|
+
else:
|
|
531
|
+
fmt = "wav"
|
|
532
|
+
asset_id = _md5(data)
|
|
533
|
+
md5ext = f"{asset_id}.{fmt}"
|
|
534
|
+
self.assets[md5ext] = data
|
|
535
|
+
sounds.append({
|
|
536
|
+
"assetId": asset_id,
|
|
537
|
+
"name": name,
|
|
538
|
+
"dataFormat": fmt,
|
|
539
|
+
"format": "",
|
|
540
|
+
"rate": 48000,
|
|
541
|
+
"sampleCount": 0,
|
|
542
|
+
"md5ext": md5ext,
|
|
543
|
+
})
|
|
544
|
+
else:
|
|
545
|
+
# File not found - use silent fallback
|
|
546
|
+
from . import assets as ASSETS2
|
|
547
|
+
data = ASSETS2.SILENT_WAV
|
|
548
|
+
asset_id = _md5(data)
|
|
549
|
+
md5ext = f"{asset_id}.wav"
|
|
550
|
+
self.assets[md5ext] = data
|
|
551
|
+
sounds.append({
|
|
552
|
+
"assetId": asset_id,
|
|
553
|
+
"name": name,
|
|
554
|
+
"dataFormat": "wav",
|
|
555
|
+
"format": "",
|
|
556
|
+
"rate": 48000,
|
|
557
|
+
"sampleCount": 0,
|
|
558
|
+
"md5ext": md5ext,
|
|
559
|
+
})
|
|
560
|
+
return sounds
|
|
499
561
|
|
|
500
562
|
def _load_asset(self, path: Optional[str], is_stage: bool) -> Tuple[bytes, str, str, int, int]:
|
|
501
563
|
"""Returns (data, md5ext, format, rotationCenterX, rotationCenterY)."""
|
|
@@ -676,6 +738,11 @@ class Codegen:
|
|
|
676
738
|
parent_id, stmt.line)
|
|
677
739
|
if isinstance(stmt, A.BlockCall):
|
|
678
740
|
return self.gen_block_call(stmt, parent_id)
|
|
741
|
+
if isinstance(stmt, A.LocalVarDecl):
|
|
742
|
+
return self.gen_var_assign(
|
|
743
|
+
A.VarAssign(name=stmt.name, value=stmt.value, line=stmt.line),
|
|
744
|
+
parent_id
|
|
745
|
+
)
|
|
679
746
|
raise CompileError(f"unknown statement type: {type(stmt).__name__}", stmt.line)
|
|
680
747
|
|
|
681
748
|
def gen_simple_stack(self, opcode: str, inputs_spec, parent_id, line) -> Tuple[str, str]:
|
|
@@ -1485,6 +1552,8 @@ class Codegen:
|
|
|
1485
1552
|
return None, block_id
|
|
1486
1553
|
if isinstance(expr, A.ListReporterRef):
|
|
1487
1554
|
return self._gen_list_reporter(expr)
|
|
1555
|
+
if isinstance(expr, A.TernaryExpr):
|
|
1556
|
+
return self._gen_ternary(expr)
|
|
1488
1557
|
if isinstance(expr, A.BuiltinRef):
|
|
1489
1558
|
# Builtin reporter (e.g. answer, mouseX)
|
|
1490
1559
|
spec = B.REPORTERS.get(expr.name)
|
|
@@ -1544,6 +1613,63 @@ class Codegen:
|
|
|
1544
1613
|
# default: string literal
|
|
1545
1614
|
return A.StringLit(value=name, line=ref.line)
|
|
1546
1615
|
|
|
1616
|
+
def _gen_ternary(self, expr: A.TernaryExpr) -> Tuple[Optional[list], Optional[str]]:
|
|
1617
|
+
"""Generate a ternary expression [if (cond) x else y].
|
|
1618
|
+
|
|
1619
|
+
This is desugared into: a temp variable, set by an if/else.
|
|
1620
|
+
We create the if/else blocks inline and return a VarRef to the temp.
|
|
1621
|
+
"""
|
|
1622
|
+
# Create a temp variable
|
|
1623
|
+
tmp_name = f"_ternary_{gen_id()[:8]}"
|
|
1624
|
+
self._declare_var(tmp_name, 0)
|
|
1625
|
+
vid = self.current_vars[tmp_name]
|
|
1626
|
+
|
|
1627
|
+
# Coerce the condition
|
|
1628
|
+
cond = self._coerce_condition(expr.cond)
|
|
1629
|
+
shadow_cond, sub_cond = self.gen_input_value(cond, "boolean")
|
|
1630
|
+
cond_input = self._pack_input("boolean", shadow_cond, sub_cond)
|
|
1631
|
+
|
|
1632
|
+
# Generate the true branch: set tmp = true_expr
|
|
1633
|
+
true_shadow, true_sub = self.gen_input_value(expr.true_expr, "any")
|
|
1634
|
+
true_block_id = gen_id()
|
|
1635
|
+
self.current_blocks[true_block_id] = {
|
|
1636
|
+
"opcode": "data_setvariableto",
|
|
1637
|
+
"next": None, "parent": None,
|
|
1638
|
+
"inputs": {"VALUE": self._pack_input("any", true_shadow, true_sub)},
|
|
1639
|
+
"fields": {"VARIABLE": [tmp_name, vid]},
|
|
1640
|
+
"shadow": False, "topLevel": False,
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
# Generate the false branch: set tmp = false_expr
|
|
1644
|
+
false_shadow, false_sub = self.gen_input_value(expr.false_expr, "any")
|
|
1645
|
+
false_block_id = gen_id()
|
|
1646
|
+
self.current_blocks[false_block_id] = {
|
|
1647
|
+
"opcode": "data_setvariableto",
|
|
1648
|
+
"next": None, "parent": None,
|
|
1649
|
+
"inputs": {"VALUE": self._pack_input("any", false_shadow, false_sub)},
|
|
1650
|
+
"fields": {"VARIABLE": [tmp_name, vid]},
|
|
1651
|
+
"shadow": False, "topLevel": False,
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
# Generate the if/else block
|
|
1655
|
+
if_block_id = gen_id()
|
|
1656
|
+
self.current_blocks[if_block_id] = {
|
|
1657
|
+
"opcode": "control_if_else",
|
|
1658
|
+
"next": None, "parent": None,
|
|
1659
|
+
"inputs": {
|
|
1660
|
+
"CONDITION": cond_input,
|
|
1661
|
+
"SUBSTACK": [2, true_block_id],
|
|
1662
|
+
"SUBSTACK2": [2, false_block_id],
|
|
1663
|
+
},
|
|
1664
|
+
"fields": {},
|
|
1665
|
+
"shadow": False, "topLevel": False,
|
|
1666
|
+
}
|
|
1667
|
+
self.current_blocks[true_block_id]["parent"] = if_block_id
|
|
1668
|
+
self.current_blocks[false_block_id]["parent"] = if_block_id
|
|
1669
|
+
|
|
1670
|
+
# Return a variable reference to the temp
|
|
1671
|
+
return [SHADOW_VAR, tmp_name, vid], None
|
|
1672
|
+
|
|
1547
1673
|
def _gen_list_reporter(self, expr: A.ListReporterRef) -> Tuple[Optional[list], Optional[str]]:
|
|
1548
1674
|
"""Generate a list reporter block (data_itemoflist, data_lengthoflist,
|
|
1549
1675
|
data_listcontainsitem, data_itemnumoflist, data_listcontents).
|