typescratch 1.0.0__py3-none-any.whl
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/__init__.py +146 -0
- typescratch/__main__.py +10 -0
- typescratch/assets.py +69 -0
- typescratch/ast_nodes.py +363 -0
- typescratch/blocks.py +961 -0
- typescratch/cli.py +67 -0
- typescratch/codegen.py +1631 -0
- typescratch/errors.py +94 -0
- typescratch/lexer.py +302 -0
- typescratch/parser.py +1022 -0
- typescratch/sb3.py +60 -0
- typescratch/scratchcat_official.svg +42 -0
- typescratch-1.0.0.dist-info/METADATA +654 -0
- typescratch-1.0.0.dist-info/RECORD +18 -0
- typescratch-1.0.0.dist-info/WHEEL +5 -0
- typescratch-1.0.0.dist-info/entry_points.txt +2 -0
- typescratch-1.0.0.dist-info/licenses/LICENSE +201 -0
- typescratch-1.0.0.dist-info/top_level.txt +1 -0
typescratch/codegen.py
ADDED
|
@@ -0,0 +1,1631 @@
|
|
|
1
|
+
"""Code generator: AST -> Scratch 3.0 project.json.
|
|
2
|
+
|
|
3
|
+
Strategy
|
|
4
|
+
--------
|
|
5
|
+
Each TypeScratch sprite becomes a Scratch target. Each script becomes
|
|
6
|
+
a hat block + a chain of stack blocks. Substacks (if/repeat/forever)
|
|
7
|
+
become SUBSTACK inputs pointing at the first block of the inner chain.
|
|
8
|
+
|
|
9
|
+
Variables are resolved in two passes:
|
|
10
|
+
1. Pre-scan the target for every `Name = ...` and `list Name` to
|
|
11
|
+
collect declared variables/lists.
|
|
12
|
+
2. During codegen, a BareRef node resolves to a VarRef if its name
|
|
13
|
+
is in the declared set, otherwise to a StringLit (since "Scratch
|
|
14
|
+
has no strings, only text").
|
|
15
|
+
|
|
16
|
+
Inputs use the canonical Scratch 3.0 format:
|
|
17
|
+
* literal shadow : [1, [primitive_type, value]]
|
|
18
|
+
* block + shadow : [2, block_id, [primitive_type, ""]] (or null shadow)
|
|
19
|
+
* variable input : [3, [12, name, id], [12, name, id]]
|
|
20
|
+
* substack : [2, block_id]
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import hashlib
|
|
26
|
+
import os
|
|
27
|
+
import uuid
|
|
28
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
29
|
+
|
|
30
|
+
from . import ast_nodes as A
|
|
31
|
+
from . import blocks as B
|
|
32
|
+
from .errors import CompileError, Debug, did_you_mean
|
|
33
|
+
from . import assets as ASSETS
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def gen_id() -> str:
|
|
37
|
+
return uuid.uuid4().hex[:20]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Primitive shadow type codes
|
|
41
|
+
SHADOW_NUMBER = 4
|
|
42
|
+
SHADOW_COLOR = 9
|
|
43
|
+
SHADOW_STRING = 10
|
|
44
|
+
SHADOW_BROADCAST = 11
|
|
45
|
+
SHADOW_VAR = 12
|
|
46
|
+
SHADOW_LIST = 13
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Codegen:
|
|
50
|
+
def __init__(self, program: A.Program, debug: Optional[Debug] = None,
|
|
51
|
+
asset_root: Optional[str] = None):
|
|
52
|
+
self.program = program
|
|
53
|
+
self.debug = debug or Debug(enabled=False)
|
|
54
|
+
self.asset_root = asset_root # base dir for resolving relative img paths
|
|
55
|
+
# md5ext -> bytes (collected for SB3 packaging)
|
|
56
|
+
self.assets: Dict[str, bytes] = {}
|
|
57
|
+
# per-target state
|
|
58
|
+
self.current_target: Optional[A.Target] = None
|
|
59
|
+
self.current_blocks: Dict[str, dict] = {}
|
|
60
|
+
self.current_vars: Dict[str, str] = {} # name -> var_id
|
|
61
|
+
self.current_var_values: Dict[str, Any] = {} # name -> initial value
|
|
62
|
+
self.current_lists: Dict[str, str] = {} # name -> list_id
|
|
63
|
+
self.current_list_values: Dict[str, list] = {} # name -> initial list
|
|
64
|
+
self.current_broadcasts: Dict[str, str] = {} # msg -> msg (id = name)
|
|
65
|
+
self.current_custom_blocks: Dict[str, dict] = {} # name -> {proccode, argument_ids, warp}
|
|
66
|
+
# Per-custom-block state (set when generating a def body)
|
|
67
|
+
self.current_proc_params: Dict[str, str] = {} # param name -> argument_id
|
|
68
|
+
|
|
69
|
+
# =====================================================================
|
|
70
|
+
# Top-level
|
|
71
|
+
# =====================================================================
|
|
72
|
+
def generate(self) -> Tuple[dict, Dict[str, bytes]]:
|
|
73
|
+
self.debug.section("Codegen")
|
|
74
|
+
targets_json: List[dict] = []
|
|
75
|
+
|
|
76
|
+
stage = next((t for t in self.program.targets if t.kind == "stage"), None)
|
|
77
|
+
if stage is None:
|
|
78
|
+
stage = A.Target(kind="stage", name="Stage")
|
|
79
|
+
self.program.targets.insert(0, stage)
|
|
80
|
+
|
|
81
|
+
# Stage first
|
|
82
|
+
stage_json = self.gen_target(stage, is_stage=True, layer_order=0)
|
|
83
|
+
targets_json.append(stage_json)
|
|
84
|
+
|
|
85
|
+
# Then sprites
|
|
86
|
+
layer = 1
|
|
87
|
+
for tgt in self.program.targets:
|
|
88
|
+
if tgt.kind == "sprite":
|
|
89
|
+
targets_json.append(self.gen_target(tgt, is_stage=False, layer_order=layer))
|
|
90
|
+
layer += 1
|
|
91
|
+
|
|
92
|
+
# Collect ALL broadcasts from ALL sprites and merge them into the stage.
|
|
93
|
+
# In Scratch 3, broadcasts are stored on the stage target.
|
|
94
|
+
all_broadcasts = {}
|
|
95
|
+
for tgt_json in targets_json:
|
|
96
|
+
for bid, bname in tgt_json.get("broadcasts", {}).items():
|
|
97
|
+
all_broadcasts[bid] = bname
|
|
98
|
+
stage_json["broadcasts"] = all_broadcasts
|
|
99
|
+
# Clear broadcasts from sprites (they belong on the stage)
|
|
100
|
+
for tgt_json in targets_json:
|
|
101
|
+
if not tgt_json["isStage"]:
|
|
102
|
+
tgt_json["broadcasts"] = {}
|
|
103
|
+
|
|
104
|
+
extensions = self._convert_extensions()
|
|
105
|
+
|
|
106
|
+
project = {
|
|
107
|
+
"targets": targets_json,
|
|
108
|
+
"monitors": [],
|
|
109
|
+
"extensions": extensions,
|
|
110
|
+
"meta": {
|
|
111
|
+
"semver": "3.0.0",
|
|
112
|
+
"vm": "0.2.0",
|
|
113
|
+
"agent": "TypeScratch/1.0"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
self.debug.dump("project.json", project)
|
|
117
|
+
return project, self.assets
|
|
118
|
+
|
|
119
|
+
def _convert_extensions(self) -> List[str]:
|
|
120
|
+
"""Map TypeScratch extension names to Scratch extension IDs."""
|
|
121
|
+
ext_map = {
|
|
122
|
+
"pen": "pen",
|
|
123
|
+
"music": "music",
|
|
124
|
+
"video sensing": "video_sensing",
|
|
125
|
+
"videosensing": "video_sensing",
|
|
126
|
+
"text to speech": "text2speech",
|
|
127
|
+
"text2speech": "text2speech",
|
|
128
|
+
"tts": "text2speech",
|
|
129
|
+
"speech to text": "speech2text",
|
|
130
|
+
"speech2text": "speech2text",
|
|
131
|
+
"listen": "speech2text",
|
|
132
|
+
"translate": "translate",
|
|
133
|
+
"makey makey": "makeymakey",
|
|
134
|
+
"makeymakey": "makeymakey",
|
|
135
|
+
"microbit": "microbit",
|
|
136
|
+
"micro bit": "microbit",
|
|
137
|
+
"micro:bit": "microbit",
|
|
138
|
+
"micro :bit": "microbit",
|
|
139
|
+
"micro : bit": "microbit",
|
|
140
|
+
"mesh": "mesh",
|
|
141
|
+
"ev3": "ev3",
|
|
142
|
+
"lego ev3": "ev3",
|
|
143
|
+
"lego mindstorms": "ev3",
|
|
144
|
+
"boost": "boost",
|
|
145
|
+
"lego boost": "boost",
|
|
146
|
+
"wedo": "wedov2",
|
|
147
|
+
"wedo2": "wedov2",
|
|
148
|
+
"wedov2": "wedov2",
|
|
149
|
+
"lego wedo": "wedov2",
|
|
150
|
+
"lego wedo 2": "wedov2",
|
|
151
|
+
"lego wedo2": "wedov2",
|
|
152
|
+
"gdxfor": "gdxfor",
|
|
153
|
+
"go direct force": "gdxfor",
|
|
154
|
+
"go direct": "gdxfor",
|
|
155
|
+
"force and acceleration": "gdxfor",
|
|
156
|
+
"nfc": "nfc",
|
|
157
|
+
}
|
|
158
|
+
out = []
|
|
159
|
+
for ext in self.program.extensions:
|
|
160
|
+
key = ext.lower().strip()
|
|
161
|
+
mapped = ext_map.get(key, key.replace(" ", "_"))
|
|
162
|
+
if mapped not in out:
|
|
163
|
+
out.append(mapped)
|
|
164
|
+
return out
|
|
165
|
+
|
|
166
|
+
# =====================================================================
|
|
167
|
+
# Target
|
|
168
|
+
# =====================================================================
|
|
169
|
+
def gen_target(self, target: A.Target, is_stage: bool, layer_order: int) -> dict:
|
|
170
|
+
self.debug.line(f"target: {target.name} (stage={is_stage})")
|
|
171
|
+
self.current_target = target
|
|
172
|
+
self.current_blocks = {}
|
|
173
|
+
self.current_vars = {}
|
|
174
|
+
self.current_var_values = {}
|
|
175
|
+
self.current_lists = {}
|
|
176
|
+
self.current_list_values = {}
|
|
177
|
+
self.current_broadcasts = {}
|
|
178
|
+
self.current_custom_blocks = {}
|
|
179
|
+
self.current_proc_params = {}
|
|
180
|
+
|
|
181
|
+
# Pre-scan for variables and lists
|
|
182
|
+
self._scan_variables(target)
|
|
183
|
+
self._scan_lists(target)
|
|
184
|
+
|
|
185
|
+
# Generate custom block definitions (procedures_definition)
|
|
186
|
+
for cb in target.custom_blocks:
|
|
187
|
+
self.gen_custom_block_def(cb)
|
|
188
|
+
|
|
189
|
+
# Generate scripts
|
|
190
|
+
for script in target.scripts:
|
|
191
|
+
self.gen_script(script)
|
|
192
|
+
|
|
193
|
+
# Costumes
|
|
194
|
+
costumes_json = self.gen_costumes(target, is_stage)
|
|
195
|
+
|
|
196
|
+
# Sounds
|
|
197
|
+
sounds_json = self.gen_sounds(target)
|
|
198
|
+
|
|
199
|
+
# Variables JSON - Scratch 3 format: {id: [name, value]}
|
|
200
|
+
variables_json = {}
|
|
201
|
+
for vname, vid in self.current_vars.items():
|
|
202
|
+
val = self.current_var_values.get(vname, 0)
|
|
203
|
+
variables_json[vid] = [vname, val]
|
|
204
|
+
|
|
205
|
+
# Lists JSON - Scratch 3 format: {id: [name, [items...]]}
|
|
206
|
+
lists_json = {}
|
|
207
|
+
for lname, lid in self.current_lists.items():
|
|
208
|
+
val = self.current_list_values.get(lname, [])
|
|
209
|
+
lists_json[lid] = [lname, val]
|
|
210
|
+
|
|
211
|
+
target_json: dict = {
|
|
212
|
+
"isStage": is_stage,
|
|
213
|
+
"name": target.name,
|
|
214
|
+
"variables": variables_json,
|
|
215
|
+
"lists": lists_json,
|
|
216
|
+
"broadcasts": self.current_broadcasts,
|
|
217
|
+
"blocks": self.current_blocks,
|
|
218
|
+
"comments": {},
|
|
219
|
+
"currentCostume": 0,
|
|
220
|
+
"costumes": costumes_json,
|
|
221
|
+
"sounds": sounds_json,
|
|
222
|
+
"volume": 100,
|
|
223
|
+
"layerOrder": layer_order,
|
|
224
|
+
}
|
|
225
|
+
if is_stage:
|
|
226
|
+
target_json.update({
|
|
227
|
+
"tempo": 60,
|
|
228
|
+
"videoState": "on",
|
|
229
|
+
"videoTransparency": 50,
|
|
230
|
+
"textToSpeechLanguage": None,
|
|
231
|
+
})
|
|
232
|
+
else:
|
|
233
|
+
target_json.update({
|
|
234
|
+
"visible": target.visible,
|
|
235
|
+
"x": target.x,
|
|
236
|
+
"y": target.y,
|
|
237
|
+
"size": target.size,
|
|
238
|
+
"direction": target.direction,
|
|
239
|
+
"draggable": False,
|
|
240
|
+
"rotationStyle": B.ROTATION_STYLES.get(target.rotation_style, "all around"),
|
|
241
|
+
})
|
|
242
|
+
return target_json
|
|
243
|
+
|
|
244
|
+
# ---- variable / list scanning -----------------------------------------
|
|
245
|
+
def _scan_variables(self, target: A.Target) -> None:
|
|
246
|
+
"""Walk target.scripts and target.custom_blocks looking for
|
|
247
|
+
VarAssign / VarModify nodes and declare them."""
|
|
248
|
+
def walk(stmts):
|
|
249
|
+
for s in stmts:
|
|
250
|
+
if isinstance(s, A.VarAssign):
|
|
251
|
+
self._declare_var(s.name, s.value)
|
|
252
|
+
elif isinstance(s, A.VarModify):
|
|
253
|
+
self._declare_var(s.name, 0)
|
|
254
|
+
elif isinstance(s, A.IfStatement):
|
|
255
|
+
walk(s.then_body)
|
|
256
|
+
if s.else_body:
|
|
257
|
+
walk(s.else_body)
|
|
258
|
+
elif isinstance(s, (A.RepeatStatement, A.RepeatUntilStatement,
|
|
259
|
+
A.ForeverStatement, A.WhileStatement,
|
|
260
|
+
A.ForEachStatement, A.AllAtOnceStatement)):
|
|
261
|
+
walk(s.body)
|
|
262
|
+
|
|
263
|
+
# If the user wrote `Points = 0` somewhere, declare Points as a var
|
|
264
|
+
# with initial value 0 (only the FIRST assignment counts as init).
|
|
265
|
+
for script in target.scripts:
|
|
266
|
+
walk(script.body)
|
|
267
|
+
for cb in target.custom_blocks:
|
|
268
|
+
walk(cb.body)
|
|
269
|
+
|
|
270
|
+
def _declare_var(self, name: str, value: Any) -> None:
|
|
271
|
+
if name in self.current_vars:
|
|
272
|
+
return
|
|
273
|
+
vid = gen_id()
|
|
274
|
+
self.current_vars[name] = vid
|
|
275
|
+
# Only use the value if it's a literal. BareRef values get
|
|
276
|
+
# resolved: if it's a known variable or builtin reporter, the
|
|
277
|
+
# initial value is 0 (the actual value is set at runtime by the
|
|
278
|
+
# `set var to ...` block); otherwise it's a string literal.
|
|
279
|
+
if isinstance(value, A.NumberLit):
|
|
280
|
+
self.current_var_values[name] = value.value
|
|
281
|
+
elif isinstance(value, A.StringLit):
|
|
282
|
+
self.current_var_values[name] = value.value
|
|
283
|
+
elif isinstance(value, A.BareRef):
|
|
284
|
+
lower = value.name.lower()
|
|
285
|
+
if (value.name in self.current_vars
|
|
286
|
+
or lower in B.BUILTIN_ALIASES
|
|
287
|
+
or value.name in B.REPORTERS):
|
|
288
|
+
# will be set at runtime - initialize to 0
|
|
289
|
+
self.current_var_values[name] = 0
|
|
290
|
+
else:
|
|
291
|
+
# genuine string literal
|
|
292
|
+
self.current_var_values[name] = value.name
|
|
293
|
+
else:
|
|
294
|
+
# complex expression - will be set at runtime
|
|
295
|
+
self.current_var_values[name] = 0
|
|
296
|
+
|
|
297
|
+
def _scan_lists(self, target: A.Target) -> None:
|
|
298
|
+
decls = getattr(target, "_list_decls", [])
|
|
299
|
+
for decl in decls:
|
|
300
|
+
name = decl["name"]
|
|
301
|
+
if name in self.current_lists:
|
|
302
|
+
continue
|
|
303
|
+
lid = gen_id()
|
|
304
|
+
self.current_lists[name] = lid
|
|
305
|
+
# Resolve initial values
|
|
306
|
+
initial = []
|
|
307
|
+
for item in decl["initial"]:
|
|
308
|
+
if isinstance(item, A.NumberLit):
|
|
309
|
+
initial.append(item.value)
|
|
310
|
+
elif isinstance(item, A.StringLit):
|
|
311
|
+
initial.append(item.value)
|
|
312
|
+
elif isinstance(item, A.BareRef):
|
|
313
|
+
initial.append(item.name)
|
|
314
|
+
else:
|
|
315
|
+
initial.append(0)
|
|
316
|
+
self.current_list_values[name] = initial
|
|
317
|
+
|
|
318
|
+
# =====================================================================
|
|
319
|
+
# Costumes and sounds
|
|
320
|
+
# =====================================================================
|
|
321
|
+
def gen_costumes(self, target: A.Target, is_stage: bool) -> List[dict]:
|
|
322
|
+
costumes: List[dict] = []
|
|
323
|
+
if is_stage:
|
|
324
|
+
# Stage costumes = backdrops declared via b "name" ...
|
|
325
|
+
if not target.costumes:
|
|
326
|
+
# Default blank backdrop
|
|
327
|
+
name, data, _ignored_md5ext, rcx, rcy = ASSETS.default_backdrop()
|
|
328
|
+
asset_id = _md5(data)
|
|
329
|
+
md5ext = f"{asset_id}.svg"
|
|
330
|
+
self.assets[md5ext] = data
|
|
331
|
+
costumes.append({
|
|
332
|
+
"assetId": asset_id,
|
|
333
|
+
"name": name,
|
|
334
|
+
"bitmapResolution": 1,
|
|
335
|
+
"md5ext": md5ext,
|
|
336
|
+
"dataFormat": "svg",
|
|
337
|
+
"rotationCenterX": rcx,
|
|
338
|
+
"rotationCenterY": rcy,
|
|
339
|
+
})
|
|
340
|
+
for c in target.costumes:
|
|
341
|
+
name = c["name"]
|
|
342
|
+
path = c.get("path")
|
|
343
|
+
data, _ignored_md5ext, fmt, rcx, rcy = self._load_asset(path, is_stage=True)
|
|
344
|
+
asset_id = _md5(data)
|
|
345
|
+
md5ext = f"{asset_id}.{fmt}"
|
|
346
|
+
self.assets[md5ext] = data
|
|
347
|
+
costumes.append({
|
|
348
|
+
"assetId": asset_id,
|
|
349
|
+
"name": name,
|
|
350
|
+
"bitmapResolution": 1 if fmt == "svg" else 2,
|
|
351
|
+
"md5ext": md5ext,
|
|
352
|
+
"dataFormat": fmt,
|
|
353
|
+
"rotationCenterX": rcx,
|
|
354
|
+
"rotationCenterY": rcy,
|
|
355
|
+
})
|
|
356
|
+
else:
|
|
357
|
+
# Sprite costume
|
|
358
|
+
path = target.img
|
|
359
|
+
data, _ignored_md5ext, fmt, rcx, rcy = self._load_asset(path, is_stage=False)
|
|
360
|
+
asset_id = _md5(data)
|
|
361
|
+
md5ext = f"{asset_id}.{fmt}"
|
|
362
|
+
self.assets[md5ext] = data
|
|
363
|
+
costumes.append({
|
|
364
|
+
"assetId": asset_id,
|
|
365
|
+
"name": "costume1",
|
|
366
|
+
"bitmapResolution": 1 if fmt == "svg" else 2,
|
|
367
|
+
"md5ext": md5ext,
|
|
368
|
+
"dataFormat": fmt,
|
|
369
|
+
"rotationCenterX": rcx,
|
|
370
|
+
"rotationCenterY": rcy,
|
|
371
|
+
})
|
|
372
|
+
return costumes
|
|
373
|
+
|
|
374
|
+
def gen_sounds(self, target: A.Target) -> List[dict]:
|
|
375
|
+
# No sound declaration syntax yet - emit an empty list
|
|
376
|
+
return []
|
|
377
|
+
|
|
378
|
+
def _load_asset(self, path: Optional[str], is_stage: bool) -> Tuple[bytes, str, str, int, int]:
|
|
379
|
+
"""Returns (data, md5ext, format, rotationCenterX, rotationCenterY)."""
|
|
380
|
+
if path:
|
|
381
|
+
# Resolve relative to asset_root if set
|
|
382
|
+
full = path
|
|
383
|
+
if self.asset_root and not os.path.isabs(path):
|
|
384
|
+
full = os.path.join(self.asset_root, path)
|
|
385
|
+
if os.path.isfile(full):
|
|
386
|
+
with open(full, "rb") as f:
|
|
387
|
+
data = f.read()
|
|
388
|
+
fmt = "svg" if full.lower().endswith(".svg") else (
|
|
389
|
+
"png" if full.lower().endswith(".png") else "svg"
|
|
390
|
+
)
|
|
391
|
+
# rotation center: half of image size (rough)
|
|
392
|
+
rcx, rcy = self._image_rotation_center(data, fmt, is_stage)
|
|
393
|
+
md5 = _md5(data)
|
|
394
|
+
return data, f"{md5}.{fmt}", fmt, rcx, rcy
|
|
395
|
+
# File not found - fall through to default
|
|
396
|
+
if is_stage:
|
|
397
|
+
name, data, md5ext, rcx, rcy = ASSETS.default_backdrop()
|
|
398
|
+
return data, md5ext, "svg", rcx, rcy
|
|
399
|
+
else:
|
|
400
|
+
name, data, md5ext, rcx, rcy = ASSETS.default_sprite_costume()
|
|
401
|
+
return data, md5ext, "svg", rcx, rcy
|
|
402
|
+
|
|
403
|
+
def _image_rotation_center(self, data: bytes, fmt: str, is_stage: bool) -> Tuple[int, int]:
|
|
404
|
+
"""Best-effort rotation center based on image dimensions."""
|
|
405
|
+
if fmt == "svg":
|
|
406
|
+
# Try to parse viewBox
|
|
407
|
+
try:
|
|
408
|
+
txt = data.decode("utf-8", errors="replace")
|
|
409
|
+
import re
|
|
410
|
+
m = re.search(r'viewBox=["\']\s*[\d.\-]+\s+[\d.\-]+\s+([\d.]+)\s+([\d.]+)', txt)
|
|
411
|
+
if m:
|
|
412
|
+
w, h = float(m.group(1)), float(m.group(2))
|
|
413
|
+
return (int(w / 2), int(h / 2))
|
|
414
|
+
m = re.search(r'<svg[^>]*width=["\']([\d.]+)', txt)
|
|
415
|
+
m2 = re.search(r'<svg[^>]*height=["\']([\d.]+)', txt)
|
|
416
|
+
if m and m2:
|
|
417
|
+
w, h = float(m.group(1)), float(m2.group(1))
|
|
418
|
+
return (int(w / 2), int(h / 2))
|
|
419
|
+
except Exception:
|
|
420
|
+
pass
|
|
421
|
+
return (240, 180) if is_stage else (48, 50)
|
|
422
|
+
# PNG: read header for dimensions
|
|
423
|
+
try:
|
|
424
|
+
if data[:8] == b"\x89PNG\r\n\x1a\n":
|
|
425
|
+
w = int.from_bytes(data[16:20], "big")
|
|
426
|
+
h = int.from_bytes(data[20:24], "big")
|
|
427
|
+
return (w // 2, h // 2)
|
|
428
|
+
except Exception:
|
|
429
|
+
pass
|
|
430
|
+
return (240, 180) if is_stage else (48, 50)
|
|
431
|
+
|
|
432
|
+
# =====================================================================
|
|
433
|
+
# Script (hat + body)
|
|
434
|
+
# =====================================================================
|
|
435
|
+
def gen_script(self, script: A.Script) -> None:
|
|
436
|
+
hat = script.hat
|
|
437
|
+
spec = B.HAT_BLOCKS.get(hat.kind)
|
|
438
|
+
if spec is None:
|
|
439
|
+
raise CompileError(f"unknown hat kind: {hat.kind!r}", hat.line)
|
|
440
|
+
|
|
441
|
+
# Emit warning for hidden/experimental blocks
|
|
442
|
+
warn = spec.get("_warn")
|
|
443
|
+
if warn:
|
|
444
|
+
import sys
|
|
445
|
+
print(f"WARNING (line {hat.line}): {warn}", file=sys.stderr)
|
|
446
|
+
|
|
447
|
+
hat_id = gen_id()
|
|
448
|
+
hat_block: dict = {
|
|
449
|
+
"opcode": spec["opcode"],
|
|
450
|
+
"next": None,
|
|
451
|
+
"parent": None,
|
|
452
|
+
"inputs": {},
|
|
453
|
+
"fields": {},
|
|
454
|
+
"shadow": False,
|
|
455
|
+
"topLevel": True,
|
|
456
|
+
"x": 0,
|
|
457
|
+
"y": 0,
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
# Hat fields
|
|
461
|
+
for fname, arg_idx in spec.get("fields", []):
|
|
462
|
+
val = hat.args[arg_idx] if arg_idx < len(hat.args) else ""
|
|
463
|
+
# For broadcast receive hat, the field name is BROADCAST_OPTION
|
|
464
|
+
# and the value is [name, broadcast_id] where broadcast_id is a
|
|
465
|
+
# unique ID that must match the broadcasts dict.
|
|
466
|
+
if hat.kind == "receive":
|
|
467
|
+
# Generate a unique broadcast ID
|
|
468
|
+
bcast_id = gen_id()
|
|
469
|
+
hat_block["fields"]["BROADCAST_OPTION"] = [val, bcast_id]
|
|
470
|
+
# Track the broadcast with its ID
|
|
471
|
+
self.current_broadcasts[bcast_id] = val
|
|
472
|
+
else:
|
|
473
|
+
hat_block["fields"][fname] = [val, None]
|
|
474
|
+
|
|
475
|
+
# Hat inputs (for greater_than)
|
|
476
|
+
arg_consumed = 0
|
|
477
|
+
for input_name, input_kind, source in spec.get("args", []):
|
|
478
|
+
# for greater_than, the VALUE comes from args[1]
|
|
479
|
+
if hat.kind == "greater_than":
|
|
480
|
+
val_node = hat.args[1] if len(hat.args) > 1 else A.NumberLit(0)
|
|
481
|
+
else:
|
|
482
|
+
val_node = hat.args[arg_consumed] if arg_consumed < len(hat.args) else None
|
|
483
|
+
arg_consumed += 1
|
|
484
|
+
shadow, block_id = self.gen_input_value(val_node, input_kind)
|
|
485
|
+
hat_block["inputs"][input_name] = self._pack_input(input_kind, shadow, block_id)
|
|
486
|
+
|
|
487
|
+
self.current_blocks[hat_id] = hat_block
|
|
488
|
+
|
|
489
|
+
# Body
|
|
490
|
+
if script.body:
|
|
491
|
+
first_id, last_id = self.gen_chain(script.body, parent_id=hat_id)
|
|
492
|
+
hat_block["next"] = first_id
|
|
493
|
+
|
|
494
|
+
# =====================================================================
|
|
495
|
+
# Statement chain
|
|
496
|
+
# =====================================================================
|
|
497
|
+
def gen_chain(self, stmts: List[A.Statement], parent_id: Optional[str]) -> Tuple[Optional[str], Optional[str]]:
|
|
498
|
+
"""Generate a chain of statements linked via parent/next.
|
|
499
|
+
Returns (first_id, last_id)."""
|
|
500
|
+
if not stmts:
|
|
501
|
+
return None, None
|
|
502
|
+
first_id: Optional[str] = None
|
|
503
|
+
prev_id: Optional[str] = parent_id
|
|
504
|
+
for stmt in stmts:
|
|
505
|
+
top_id, last_id = self.gen_statement(stmt, parent_id=prev_id)
|
|
506
|
+
if top_id is None:
|
|
507
|
+
continue
|
|
508
|
+
if first_id is None:
|
|
509
|
+
first_id = top_id
|
|
510
|
+
if prev_id is not None:
|
|
511
|
+
self.current_blocks[prev_id]["next"] = top_id
|
|
512
|
+
prev_id = last_id
|
|
513
|
+
return first_id, prev_id
|
|
514
|
+
|
|
515
|
+
# =====================================================================
|
|
516
|
+
# Statement dispatch
|
|
517
|
+
# =====================================================================
|
|
518
|
+
def gen_statement(self, stmt, parent_id: Optional[str]) -> Tuple[Optional[str], Optional[str]]:
|
|
519
|
+
if isinstance(stmt, A.VarAssign):
|
|
520
|
+
return self.gen_var_assign(stmt, parent_id)
|
|
521
|
+
if isinstance(stmt, A.VarModify):
|
|
522
|
+
return self.gen_var_modify(stmt, parent_id)
|
|
523
|
+
if isinstance(stmt, A.ListOp):
|
|
524
|
+
return self.gen_list_op(stmt, parent_id)
|
|
525
|
+
if isinstance(stmt, A.IfStatement):
|
|
526
|
+
return self.gen_if(stmt, parent_id)
|
|
527
|
+
if isinstance(stmt, A.RepeatStatement):
|
|
528
|
+
return self.gen_repeat(stmt, parent_id)
|
|
529
|
+
if isinstance(stmt, A.RepeatUntilStatement):
|
|
530
|
+
return self.gen_repeat_until(stmt, parent_id)
|
|
531
|
+
if isinstance(stmt, A.ForeverStatement):
|
|
532
|
+
return self.gen_forever(stmt, parent_id)
|
|
533
|
+
if isinstance(stmt, A.WhileStatement):
|
|
534
|
+
return self.gen_while(stmt, parent_id)
|
|
535
|
+
if isinstance(stmt, A.ForEachStatement):
|
|
536
|
+
return self.gen_for_each(stmt, parent_id)
|
|
537
|
+
if isinstance(stmt, A.AllAtOnceStatement):
|
|
538
|
+
return self.gen_all_at_once(stmt, parent_id)
|
|
539
|
+
if isinstance(stmt, A.WaitStatement):
|
|
540
|
+
return self.gen_simple_stack("control_wait",
|
|
541
|
+
[("DURATION", "number", stmt.secs)],
|
|
542
|
+
parent_id, stmt.line)
|
|
543
|
+
if isinstance(stmt, A.WaitUntilStatement):
|
|
544
|
+
return self.gen_wait_until(stmt, parent_id)
|
|
545
|
+
if isinstance(stmt, A.StopStatement):
|
|
546
|
+
return self.gen_stop(stmt, parent_id)
|
|
547
|
+
if isinstance(stmt, A.BroadcastStatement):
|
|
548
|
+
opcode = "event_broadcastandwait" if stmt.wait else "event_broadcast"
|
|
549
|
+
# message must be a broadcast input - if it's a literal string,
|
|
550
|
+
# we need to create a broadcast msg
|
|
551
|
+
msg = self._resolve_broadcast_msg(stmt.msg)
|
|
552
|
+
return self.gen_simple_stack(opcode,
|
|
553
|
+
[("BROADCAST_INPUT", "broadcast", msg)],
|
|
554
|
+
parent_id, stmt.line)
|
|
555
|
+
if isinstance(stmt, A.BlockCall):
|
|
556
|
+
return self.gen_block_call(stmt, parent_id)
|
|
557
|
+
raise CompileError(f"unknown statement type: {type(stmt).__name__}", stmt.line)
|
|
558
|
+
|
|
559
|
+
def gen_simple_stack(self, opcode: str, inputs_spec, parent_id, line) -> Tuple[str, str]:
|
|
560
|
+
"""Generate a simple stack block with the given inputs.
|
|
561
|
+
|
|
562
|
+
inputs_spec: list of (input_name, input_kind, value_node_or_string)
|
|
563
|
+
"""
|
|
564
|
+
block_id = gen_id()
|
|
565
|
+
inputs: Dict[str, list] = {}
|
|
566
|
+
for input_name, input_kind, val in inputs_spec:
|
|
567
|
+
# If val is already a string (e.g. broadcast msg), use as literal
|
|
568
|
+
if isinstance(val, str):
|
|
569
|
+
if input_kind == "broadcast":
|
|
570
|
+
shadow = [SHADOW_BROADCAST, val]
|
|
571
|
+
inputs[input_name] = [1, shadow]
|
|
572
|
+
else:
|
|
573
|
+
shadow = [SHADOW_STRING, val]
|
|
574
|
+
inputs[input_name] = [1, shadow]
|
|
575
|
+
else:
|
|
576
|
+
shadow, sub = self.gen_input_value(val, input_kind)
|
|
577
|
+
inputs[input_name] = self._pack_input(input_kind, shadow, sub)
|
|
578
|
+
self.current_blocks[block_id] = {
|
|
579
|
+
"opcode": opcode,
|
|
580
|
+
"next": None, "parent": parent_id,
|
|
581
|
+
"inputs": inputs,
|
|
582
|
+
"fields": {},
|
|
583
|
+
"shadow": False, "topLevel": False,
|
|
584
|
+
}
|
|
585
|
+
return block_id, block_id
|
|
586
|
+
|
|
587
|
+
def gen_wait_until(self, stmt: A.WaitUntilStatement, parent_id: Optional[str]):
|
|
588
|
+
"""control_wait_until - has a CONDITION input (boolean)."""
|
|
589
|
+
shadow_c, sub_c = self.gen_input_value(stmt.cond, "boolean")
|
|
590
|
+
cond_input = self._pack_input("boolean", shadow_c, sub_c)
|
|
591
|
+
block_id = gen_id()
|
|
592
|
+
self.current_blocks[block_id] = {
|
|
593
|
+
"opcode": "control_wait_until",
|
|
594
|
+
"next": None, "parent": parent_id,
|
|
595
|
+
"inputs": {"CONDITION": cond_input},
|
|
596
|
+
"fields": {},
|
|
597
|
+
"shadow": False, "topLevel": False,
|
|
598
|
+
}
|
|
599
|
+
return block_id, block_id
|
|
600
|
+
|
|
601
|
+
# ---- variable assignment ---------------------------------------------
|
|
602
|
+
def gen_var_assign(self, stmt: A.VarAssign, parent_id: Optional[str]):
|
|
603
|
+
# Ensure var is declared
|
|
604
|
+
if stmt.name not in self.current_vars:
|
|
605
|
+
self._declare_var(stmt.name, stmt.value)
|
|
606
|
+
vid = self.current_vars[stmt.name]
|
|
607
|
+
block_id = gen_id()
|
|
608
|
+
shadow, sub_block = self.gen_input_value(stmt.value, "any")
|
|
609
|
+
inp = self._pack_input("any", shadow, sub_block)
|
|
610
|
+
self.current_blocks[block_id] = {
|
|
611
|
+
"opcode": "data_setvariableto",
|
|
612
|
+
"next": None,
|
|
613
|
+
"parent": parent_id,
|
|
614
|
+
"inputs": {"VALUE": inp},
|
|
615
|
+
"fields": {"VARIABLE": [stmt.name, vid]},
|
|
616
|
+
"shadow": False,
|
|
617
|
+
"topLevel": False,
|
|
618
|
+
}
|
|
619
|
+
return block_id, block_id
|
|
620
|
+
|
|
621
|
+
def gen_var_modify(self, stmt: A.VarModify, parent_id: Optional[str]):
|
|
622
|
+
if stmt.name not in self.current_vars:
|
|
623
|
+
self._declare_var(stmt.name, 0)
|
|
624
|
+
vid = self.current_vars[stmt.name]
|
|
625
|
+
# Compile += as: set var to (var + value), -= as set var to (var - value), etc.
|
|
626
|
+
# Actually Scratch has data_changevariableby only for +=.
|
|
627
|
+
# For -=, we need: change var by (0 - value)
|
|
628
|
+
# For *=, /= : set var to (var * value)
|
|
629
|
+
if stmt.op == "+=":
|
|
630
|
+
block_id = gen_id()
|
|
631
|
+
shadow, sub_block = self.gen_input_value(stmt.value, "number")
|
|
632
|
+
inp = self._pack_input("number", shadow, sub_block)
|
|
633
|
+
self.current_blocks[block_id] = {
|
|
634
|
+
"opcode": "data_changevariableby",
|
|
635
|
+
"next": None,
|
|
636
|
+
"parent": parent_id,
|
|
637
|
+
"inputs": {"VALUE": inp},
|
|
638
|
+
"fields": {"VARIABLE": [stmt.name, vid]},
|
|
639
|
+
"shadow": False,
|
|
640
|
+
"topLevel": False,
|
|
641
|
+
}
|
|
642
|
+
return block_id, block_id
|
|
643
|
+
else:
|
|
644
|
+
# General case: set var to (var OP value)
|
|
645
|
+
op_map = {"+=": "+", "-=": "-", "*=": "*", "/=": "/"}
|
|
646
|
+
op = op_map[stmt.op]
|
|
647
|
+
var_ref = A.VarRef(name=stmt.name, line=stmt.line)
|
|
648
|
+
new_value = A.BinOp(op=op, left=var_ref, right=stmt.value, line=stmt.line)
|
|
649
|
+
return self.gen_var_assign(A.VarAssign(name=stmt.name, value=new_value, line=stmt.line), parent_id)
|
|
650
|
+
|
|
651
|
+
# ---- list operations --------------------------------------------------
|
|
652
|
+
def gen_list_op(self, stmt: A.ListOp, parent_id: Optional[str]):
|
|
653
|
+
if stmt.list_name not in self.current_lists:
|
|
654
|
+
# auto-declare
|
|
655
|
+
lid = gen_id()
|
|
656
|
+
self.current_lists[stmt.list_name] = lid
|
|
657
|
+
self.current_list_values[stmt.list_name] = []
|
|
658
|
+
lid = self.current_lists[stmt.list_name]
|
|
659
|
+
|
|
660
|
+
if stmt.op == "add":
|
|
661
|
+
# data_addtolist: LIST, ITEM (ITEM is required)
|
|
662
|
+
if not stmt.args:
|
|
663
|
+
raise CompileError(
|
|
664
|
+
f'{stmt.list_name}.add() requires an argument, '
|
|
665
|
+
f'e.g. {stmt.list_name}.add(item)',
|
|
666
|
+
stmt.line
|
|
667
|
+
)
|
|
668
|
+
shadow, sub = self.gen_input_value(stmt.args[0], "string")
|
|
669
|
+
inp = self._pack_input("string", shadow, sub)
|
|
670
|
+
block_id = gen_id()
|
|
671
|
+
self.current_blocks[block_id] = {
|
|
672
|
+
"opcode": "data_addtolist",
|
|
673
|
+
"next": None, "parent": parent_id,
|
|
674
|
+
"inputs": {"ITEM": inp},
|
|
675
|
+
"fields": {"LIST": [stmt.list_name, lid]},
|
|
676
|
+
"shadow": False, "topLevel": False,
|
|
677
|
+
}
|
|
678
|
+
return block_id, block_id
|
|
679
|
+
if stmt.op == "delete":
|
|
680
|
+
# data_deleteoflist: LIST, INDEX (INDEX is required)
|
|
681
|
+
# special: index "all" -> data_deletealloflist
|
|
682
|
+
if stmt.args and isinstance(stmt.args[0], A.BareRef) and stmt.args[0].name.lower() == "all":
|
|
683
|
+
block_id = gen_id()
|
|
684
|
+
self.current_blocks[block_id] = {
|
|
685
|
+
"opcode": "data_deletealloflist",
|
|
686
|
+
"next": None, "parent": parent_id,
|
|
687
|
+
"inputs": {},
|
|
688
|
+
"fields": {"LIST": [stmt.list_name, lid]},
|
|
689
|
+
"shadow": False, "topLevel": False,
|
|
690
|
+
}
|
|
691
|
+
return block_id, block_id
|
|
692
|
+
if not stmt.args:
|
|
693
|
+
raise CompileError(
|
|
694
|
+
f'{stmt.list_name}.delete() requires an index argument, '
|
|
695
|
+
f'e.g. {stmt.list_name}.delete(1) or {stmt.list_name}.delete(all)',
|
|
696
|
+
stmt.line
|
|
697
|
+
)
|
|
698
|
+
shadow, sub = self.gen_input_value(stmt.args[0], "number")
|
|
699
|
+
inp = self._pack_input("number", shadow, sub)
|
|
700
|
+
block_id = gen_id()
|
|
701
|
+
self.current_blocks[block_id] = {
|
|
702
|
+
"opcode": "data_deleteoflist",
|
|
703
|
+
"next": None, "parent": parent_id,
|
|
704
|
+
"inputs": {"INDEX": inp},
|
|
705
|
+
"fields": {"LIST": [stmt.list_name, lid]},
|
|
706
|
+
"shadow": False, "topLevel": False,
|
|
707
|
+
}
|
|
708
|
+
return block_id, block_id
|
|
709
|
+
if stmt.op == "insert":
|
|
710
|
+
# data_insertatlist: LIST, INDEX, ITEM (both required)
|
|
711
|
+
# Syntax: list.insert(item, index)
|
|
712
|
+
if len(stmt.args) < 2:
|
|
713
|
+
raise CompileError(
|
|
714
|
+
f'{stmt.list_name}.insert() requires 2 arguments: '
|
|
715
|
+
f'item and index, e.g. {stmt.list_name}.insert(item, 1)',
|
|
716
|
+
stmt.line
|
|
717
|
+
)
|
|
718
|
+
shadow_idx, sub_idx = self.gen_input_value(stmt.args[1], "number")
|
|
719
|
+
shadow_item, sub_item = self.gen_input_value(stmt.args[0], "string")
|
|
720
|
+
block_id = gen_id()
|
|
721
|
+
self.current_blocks[block_id] = {
|
|
722
|
+
"opcode": "data_insertatlist",
|
|
723
|
+
"next": None, "parent": parent_id,
|
|
724
|
+
"inputs": {
|
|
725
|
+
"INDEX": self._pack_input("number", shadow_idx, sub_idx),
|
|
726
|
+
"ITEM": self._pack_input("string", shadow_item, sub_item),
|
|
727
|
+
},
|
|
728
|
+
"fields": {"LIST": [stmt.list_name, lid]},
|
|
729
|
+
"shadow": False, "topLevel": False,
|
|
730
|
+
}
|
|
731
|
+
return block_id, block_id
|
|
732
|
+
if stmt.op == "replace":
|
|
733
|
+
# data_replaceitemoflist: LIST, INDEX, ITEM (both required)
|
|
734
|
+
# Syntax: list.replace(index, item)
|
|
735
|
+
if len(stmt.args) < 2:
|
|
736
|
+
raise CompileError(
|
|
737
|
+
f'{stmt.list_name}.replace() requires 2 arguments: '
|
|
738
|
+
f'index and item, e.g. {stmt.list_name}.replace(1, item)',
|
|
739
|
+
stmt.line
|
|
740
|
+
)
|
|
741
|
+
shadow_idx, sub_idx = self.gen_input_value(stmt.args[0], "number")
|
|
742
|
+
shadow_item, sub_item = self.gen_input_value(stmt.args[1], "string")
|
|
743
|
+
block_id = gen_id()
|
|
744
|
+
self.current_blocks[block_id] = {
|
|
745
|
+
"opcode": "data_replaceitemoflist",
|
|
746
|
+
"next": None, "parent": parent_id,
|
|
747
|
+
"inputs": {
|
|
748
|
+
"INDEX": self._pack_input("number", shadow_idx, sub_idx),
|
|
749
|
+
"ITEM": self._pack_input("string", shadow_item, sub_item),
|
|
750
|
+
},
|
|
751
|
+
"fields": {"LIST": [stmt.list_name, lid]},
|
|
752
|
+
"shadow": False, "topLevel": False,
|
|
753
|
+
}
|
|
754
|
+
return block_id, block_id
|
|
755
|
+
if stmt.op == "show":
|
|
756
|
+
block_id = gen_id()
|
|
757
|
+
self.current_blocks[block_id] = {
|
|
758
|
+
"opcode": "data_showlist",
|
|
759
|
+
"next": None, "parent": parent_id,
|
|
760
|
+
"inputs": {},
|
|
761
|
+
"fields": {"LIST": [stmt.list_name, lid]},
|
|
762
|
+
"shadow": False, "topLevel": False,
|
|
763
|
+
}
|
|
764
|
+
return block_id, block_id
|
|
765
|
+
if stmt.op == "hide":
|
|
766
|
+
block_id = gen_id()
|
|
767
|
+
self.current_blocks[block_id] = {
|
|
768
|
+
"opcode": "data_hidelist",
|
|
769
|
+
"next": None, "parent": parent_id,
|
|
770
|
+
"inputs": {},
|
|
771
|
+
"fields": {"LIST": [stmt.list_name, lid]},
|
|
772
|
+
"shadow": False, "topLevel": False,
|
|
773
|
+
}
|
|
774
|
+
return block_id, block_id
|
|
775
|
+
raise CompileError(f"unknown list op: {stmt.op}", stmt.line)
|
|
776
|
+
|
|
777
|
+
# ---- control flow -----------------------------------------------------
|
|
778
|
+
def gen_if(self, stmt: A.IfStatement, parent_id: Optional[str]):
|
|
779
|
+
shadow_cond, sub_cond = self.gen_input_value(stmt.cond, "boolean")
|
|
780
|
+
cond_input = self._pack_input("boolean", shadow_cond, sub_cond)
|
|
781
|
+
then_first, then_last = self.gen_chain(stmt.then_body, parent_id=None)
|
|
782
|
+
block_id = gen_id()
|
|
783
|
+
inputs = {
|
|
784
|
+
"CONDITION": cond_input,
|
|
785
|
+
"SUBSTACK": self._pack_substack(then_first),
|
|
786
|
+
}
|
|
787
|
+
if stmt.else_body:
|
|
788
|
+
else_first, else_last = self.gen_chain(stmt.else_body, parent_id=None)
|
|
789
|
+
inputs["SUBSTACK2"] = self._pack_substack(else_first)
|
|
790
|
+
self.current_blocks[block_id] = {
|
|
791
|
+
"opcode": "control_if_else" if stmt.else_body else "control_if",
|
|
792
|
+
"next": None, "parent": parent_id,
|
|
793
|
+
"inputs": inputs,
|
|
794
|
+
"fields": {},
|
|
795
|
+
"shadow": False, "topLevel": False,
|
|
796
|
+
}
|
|
797
|
+
# Fix parents of substack chain
|
|
798
|
+
if then_first is not None:
|
|
799
|
+
self.current_blocks[then_first]["parent"] = block_id
|
|
800
|
+
if stmt.else_body and else_first is not None:
|
|
801
|
+
self.current_blocks[else_first]["parent"] = block_id
|
|
802
|
+
return block_id, block_id
|
|
803
|
+
|
|
804
|
+
def gen_repeat(self, stmt: A.RepeatStatement, parent_id: Optional[str]):
|
|
805
|
+
shadow_t, sub_t = self.gen_input_value(stmt.times, "number")
|
|
806
|
+
body_first, body_last = self.gen_chain(stmt.body, parent_id=None)
|
|
807
|
+
block_id = gen_id()
|
|
808
|
+
self.current_blocks[block_id] = {
|
|
809
|
+
"opcode": "control_repeat",
|
|
810
|
+
"next": None, "parent": parent_id,
|
|
811
|
+
"inputs": {
|
|
812
|
+
"TIMES": self._pack_input("number", shadow_t, sub_t),
|
|
813
|
+
"SUBSTACK": self._pack_substack(body_first),
|
|
814
|
+
},
|
|
815
|
+
"fields": {},
|
|
816
|
+
"shadow": False, "topLevel": False,
|
|
817
|
+
}
|
|
818
|
+
if body_first is not None:
|
|
819
|
+
self.current_blocks[body_first]["parent"] = block_id
|
|
820
|
+
return block_id, block_id
|
|
821
|
+
|
|
822
|
+
def gen_repeat_until(self, stmt: A.RepeatUntilStatement, parent_id: Optional[str]):
|
|
823
|
+
shadow_c, sub_c = self.gen_input_value(stmt.cond, "boolean")
|
|
824
|
+
body_first, body_last = self.gen_chain(stmt.body, parent_id=None)
|
|
825
|
+
block_id = gen_id()
|
|
826
|
+
self.current_blocks[block_id] = {
|
|
827
|
+
"opcode": "control_repeat_until",
|
|
828
|
+
"next": None, "parent": parent_id,
|
|
829
|
+
"inputs": {
|
|
830
|
+
"CONDITION": self._pack_input("boolean", shadow_c, sub_c),
|
|
831
|
+
"SUBSTACK": self._pack_substack(body_first),
|
|
832
|
+
},
|
|
833
|
+
"fields": {},
|
|
834
|
+
"shadow": False, "topLevel": False,
|
|
835
|
+
}
|
|
836
|
+
if body_first is not None:
|
|
837
|
+
self.current_blocks[body_first]["parent"] = block_id
|
|
838
|
+
return block_id, block_id
|
|
839
|
+
|
|
840
|
+
def gen_forever(self, stmt: A.ForeverStatement, parent_id: Optional[str]):
|
|
841
|
+
body_first, body_last = self.gen_chain(stmt.body, parent_id=None)
|
|
842
|
+
block_id = gen_id()
|
|
843
|
+
self.current_blocks[block_id] = {
|
|
844
|
+
"opcode": "control_forever",
|
|
845
|
+
"next": None, "parent": parent_id,
|
|
846
|
+
"inputs": {"SUBSTACK": self._pack_substack(body_first)},
|
|
847
|
+
"fields": {},
|
|
848
|
+
"shadow": False, "topLevel": False,
|
|
849
|
+
}
|
|
850
|
+
if body_first is not None:
|
|
851
|
+
self.current_blocks[body_first]["parent"] = block_id
|
|
852
|
+
# forever has no "next" - it loops forever
|
|
853
|
+
return block_id, block_id
|
|
854
|
+
|
|
855
|
+
def gen_while(self, stmt: A.WhileStatement, parent_id: Optional[str]):
|
|
856
|
+
# while (c) { body } => repeatUntil (not c) { body }
|
|
857
|
+
new_cond = A.UnaryOp(op="not", operand=stmt.cond, line=stmt.line)
|
|
858
|
+
ru = A.RepeatUntilStatement(cond=new_cond, body=stmt.body, line=stmt.line)
|
|
859
|
+
return self.gen_repeat_until(ru, parent_id)
|
|
860
|
+
|
|
861
|
+
def gen_for_each(self, stmt: A.ForEachStatement, parent_id: Optional[str]):
|
|
862
|
+
"""control_for_each: VALUE input (count), VARIABLE field, SUBSTACK input.
|
|
863
|
+
|
|
864
|
+
The VARIABLE field uses the [name, id] format like a variable
|
|
865
|
+
reference, so the loop counter variable must be declared.
|
|
866
|
+
"""
|
|
867
|
+
if stmt.var_name not in self.current_vars:
|
|
868
|
+
self._declare_var(stmt.var_name, 0)
|
|
869
|
+
vid = self.current_vars[stmt.var_name]
|
|
870
|
+
var_field = [stmt.var_name, vid]
|
|
871
|
+
|
|
872
|
+
shadow_c, sub_c = self.gen_input_value(stmt.count, "number")
|
|
873
|
+
body_first, body_last = self.gen_chain(stmt.body, parent_id=None)
|
|
874
|
+
block_id = gen_id()
|
|
875
|
+
self.current_blocks[block_id] = {
|
|
876
|
+
"opcode": "control_for_each",
|
|
877
|
+
"next": None, "parent": parent_id,
|
|
878
|
+
"inputs": {
|
|
879
|
+
"VALUE": self._pack_input("number", shadow_c, sub_c),
|
|
880
|
+
"SUBSTACK": self._pack_substack(body_first),
|
|
881
|
+
},
|
|
882
|
+
"fields": {"VARIABLE": var_field},
|
|
883
|
+
"shadow": False, "topLevel": False,
|
|
884
|
+
}
|
|
885
|
+
if body_first is not None:
|
|
886
|
+
self.current_blocks[body_first]["parent"] = block_id
|
|
887
|
+
return block_id, block_id
|
|
888
|
+
|
|
889
|
+
def gen_all_at_once(self, stmt: A.AllAtOnceStatement, parent_id: Optional[str]):
|
|
890
|
+
"""control_all_at_once: SUBSTACK input only."""
|
|
891
|
+
body_first, body_last = self.gen_chain(stmt.body, parent_id=None)
|
|
892
|
+
block_id = gen_id()
|
|
893
|
+
self.current_blocks[block_id] = {
|
|
894
|
+
"opcode": "control_all_at_once",
|
|
895
|
+
"next": None, "parent": parent_id,
|
|
896
|
+
"inputs": {"SUBSTACK": self._pack_substack(body_first)},
|
|
897
|
+
"fields": {},
|
|
898
|
+
"shadow": False, "topLevel": False,
|
|
899
|
+
}
|
|
900
|
+
if body_first is not None:
|
|
901
|
+
self.current_blocks[body_first]["parent"] = block_id
|
|
902
|
+
return block_id, block_id
|
|
903
|
+
|
|
904
|
+
def gen_stop(self, stmt: A.StopStatement, parent_id: Optional[str]):
|
|
905
|
+
kind = B.STOP_OPTIONS.get(stmt.kind.lower(), stmt.kind)
|
|
906
|
+
block_id = gen_id()
|
|
907
|
+
self.current_blocks[block_id] = {
|
|
908
|
+
"opcode": "control_stop",
|
|
909
|
+
"next": None, "parent": parent_id,
|
|
910
|
+
"inputs": {},
|
|
911
|
+
"fields": {"STOP_OPTION": [kind, None]},
|
|
912
|
+
"shadow": False, "topLevel": False,
|
|
913
|
+
}
|
|
914
|
+
return block_id, block_id
|
|
915
|
+
|
|
916
|
+
def gen_broadcast(self, stmt: A.BroadcastStatement, parent_id: Optional[str]):
|
|
917
|
+
opcode = "event_broadcastandwait" if stmt.wait else "event_broadcast"
|
|
918
|
+
msg = self._resolve_broadcast_msg(stmt.msg)
|
|
919
|
+
# Generate a unique broadcast ID for this message
|
|
920
|
+
bcast_id = gen_id()
|
|
921
|
+
self.current_broadcasts[bcast_id] = msg
|
|
922
|
+
block_id = gen_id()
|
|
923
|
+
# Broadcast input: [shadow_type, [shadow_type, name, id]]
|
|
924
|
+
# The format is [1, [11, name, id]] where 11 = SHADOW_BROADCAST
|
|
925
|
+
shadow = [SHADOW_BROADCAST, msg, bcast_id]
|
|
926
|
+
self.current_blocks[block_id] = {
|
|
927
|
+
"opcode": opcode,
|
|
928
|
+
"next": None, "parent": parent_id,
|
|
929
|
+
"inputs": {"BROADCAST_INPUT": [1, shadow]},
|
|
930
|
+
"fields": {},
|
|
931
|
+
"shadow": False, "topLevel": False,
|
|
932
|
+
}
|
|
933
|
+
return block_id, block_id
|
|
934
|
+
|
|
935
|
+
def _resolve_broadcast_msg(self, msg_node) -> str:
|
|
936
|
+
if isinstance(msg_node, A.StringLit):
|
|
937
|
+
msg = msg_node.value
|
|
938
|
+
elif isinstance(msg_node, A.BareRef):
|
|
939
|
+
msg = msg_node.name
|
|
940
|
+
elif isinstance(msg_node, A.NumberLit):
|
|
941
|
+
msg = str(msg_node.value)
|
|
942
|
+
else:
|
|
943
|
+
# complex expr - use a placeholder
|
|
944
|
+
msg = "message"
|
|
945
|
+
return msg
|
|
946
|
+
|
|
947
|
+
# ---- generic stack block from table ----------------------------------
|
|
948
|
+
def gen_block_call(self, stmt: A.BlockCall, parent_id: Optional[str]):
|
|
949
|
+
# Resolve the key: "namespace.method" or "method"
|
|
950
|
+
if stmt.namespace:
|
|
951
|
+
key = f"{stmt.namespace}.{stmt.method}"
|
|
952
|
+
else:
|
|
953
|
+
key = stmt.method
|
|
954
|
+
|
|
955
|
+
# Special: say/think without post parens
|
|
956
|
+
effective_key = key
|
|
957
|
+
if key in ("say", "think") and not stmt.post:
|
|
958
|
+
effective_key = key + "_"
|
|
959
|
+
|
|
960
|
+
# Custom block call?
|
|
961
|
+
if not stmt.namespace and stmt.method in self.current_custom_blocks:
|
|
962
|
+
return self.gen_custom_block_call(stmt, parent_id)
|
|
963
|
+
|
|
964
|
+
spec = B.STACK_BLOCKS.get(effective_key)
|
|
965
|
+
if spec is None:
|
|
966
|
+
all_keys = list(B.STACK_BLOCKS.keys()) + list(self.current_custom_blocks.keys())
|
|
967
|
+
suggestion = did_you_mean(key, all_keys)
|
|
968
|
+
raise CompileError(
|
|
969
|
+
f'"{key}" is not valid TypeScratch syntax{suggestion}'
|
|
970
|
+
+ (f" (did you mean to use it as a custom block?)" if not stmt.namespace and not suggestion else ""),
|
|
971
|
+
stmt.line
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
# Emit warning for hidden/experimental blocks
|
|
975
|
+
warn = spec.get("_warn")
|
|
976
|
+
if warn:
|
|
977
|
+
import sys
|
|
978
|
+
print(f"WARNING (line {stmt.line}): {warn}", file=sys.stderr)
|
|
979
|
+
|
|
980
|
+
block_id = gen_id()
|
|
981
|
+
inputs: Dict[str, list] = {}
|
|
982
|
+
fields: Dict[str, list] = {}
|
|
983
|
+
|
|
984
|
+
# Consume args from stmt.args and stmt.post in order
|
|
985
|
+
args_iter = list(stmt.args)
|
|
986
|
+
post_iter = list(stmt.post or [])
|
|
987
|
+
arg_idx = 0
|
|
988
|
+
post_idx = 0
|
|
989
|
+
|
|
990
|
+
# First, fields that come from args (None value)
|
|
991
|
+
for fname, fval in spec.get("fields", []):
|
|
992
|
+
if fval is None:
|
|
993
|
+
# value comes from next arg
|
|
994
|
+
if arg_idx < len(args_iter):
|
|
995
|
+
val_node = args_iter[arg_idx]
|
|
996
|
+
arg_idx += 1
|
|
997
|
+
fields[fname] = [self._resolve_field_value(val_node, fname), None]
|
|
998
|
+
else:
|
|
999
|
+
fields[fname] = ["", None]
|
|
1000
|
+
else:
|
|
1001
|
+
fields[fname] = [fval, None]
|
|
1002
|
+
|
|
1003
|
+
# Then, args (inputs)
|
|
1004
|
+
for spec_entry in spec.get("args", []):
|
|
1005
|
+
input_name = spec_entry[0]
|
|
1006
|
+
input_kind = spec_entry[1]
|
|
1007
|
+
source = spec_entry[2] if len(spec_entry) > 2 else "args"
|
|
1008
|
+
optional = spec_entry[3] if len(spec_entry) > 3 else False
|
|
1009
|
+
|
|
1010
|
+
if source == "args":
|
|
1011
|
+
if arg_idx < len(args_iter):
|
|
1012
|
+
val_node = args_iter[arg_idx]
|
|
1013
|
+
arg_idx += 1
|
|
1014
|
+
shadow, sub = self.gen_input_value(val_node, input_kind)
|
|
1015
|
+
inputs[input_name] = self._pack_input(input_kind, shadow, sub)
|
|
1016
|
+
elif post_idx < len(post_iter):
|
|
1017
|
+
# Allow falling through to post parens if args are exhausted.
|
|
1018
|
+
# This lets users write `changeEffect(color)(25)` as well as
|
|
1019
|
+
# `changeEffect(color, 25)`.
|
|
1020
|
+
val_node = post_iter[post_idx]
|
|
1021
|
+
post_idx += 1
|
|
1022
|
+
shadow, sub = self.gen_input_value(val_node, input_kind)
|
|
1023
|
+
inputs[input_name] = self._pack_input(input_kind, shadow, sub)
|
|
1024
|
+
elif optional:
|
|
1025
|
+
# provide default
|
|
1026
|
+
shadow, sub = self.gen_input_value(A.NumberLit(1), input_kind)
|
|
1027
|
+
inputs[input_name] = self._pack_input(input_kind, shadow, sub)
|
|
1028
|
+
else:
|
|
1029
|
+
raise CompileError(
|
|
1030
|
+
f"block {key!r} expects more arguments (input {input_name!r})",
|
|
1031
|
+
stmt.line
|
|
1032
|
+
)
|
|
1033
|
+
elif source == "post":
|
|
1034
|
+
if post_idx < len(post_iter):
|
|
1035
|
+
val_node = post_iter[post_idx]
|
|
1036
|
+
post_idx += 1
|
|
1037
|
+
shadow, sub = self.gen_input_value(val_node, input_kind)
|
|
1038
|
+
inputs[input_name] = self._pack_input(input_kind, shadow, sub)
|
|
1039
|
+
elif arg_idx < len(args_iter):
|
|
1040
|
+
# Allow falling back to args
|
|
1041
|
+
val_node = args_iter[arg_idx]
|
|
1042
|
+
arg_idx += 1
|
|
1043
|
+
shadow, sub = self.gen_input_value(val_node, input_kind)
|
|
1044
|
+
inputs[input_name] = self._pack_input(input_kind, shadow, sub)
|
|
1045
|
+
else:
|
|
1046
|
+
raise CompileError(
|
|
1047
|
+
f"block {key!r} expects a post-paren argument for {input_name!r} "
|
|
1048
|
+
f"(e.g. {key}(...)(value))",
|
|
1049
|
+
stmt.line
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
# Special handling: if we have extra args for a string input, join them
|
|
1053
|
+
# This handles `say(Hello, World!)` where MESSAGE should be "Hello, World!"
|
|
1054
|
+
if arg_idx < len(args_iter):
|
|
1055
|
+
# find a string input we already filled and re-join
|
|
1056
|
+
for spec_entry in spec.get("args", []):
|
|
1057
|
+
input_name = spec_entry[0]
|
|
1058
|
+
input_kind = spec_entry[1]
|
|
1059
|
+
source = spec_entry[2] if len(spec_entry) > 2 else "args"
|
|
1060
|
+
if input_kind in ("string", "any") and source == "args" and arg_idx < len(args_iter):
|
|
1061
|
+
# Re-join: get the existing value and append remaining args
|
|
1062
|
+
existing = inputs.get(input_name)
|
|
1063
|
+
# existing is packed; we'll just create a new joined string
|
|
1064
|
+
existing_val = ""
|
|
1065
|
+
if existing and len(existing) > 1:
|
|
1066
|
+
prim = existing[1]
|
|
1067
|
+
if isinstance(prim, list) and len(prim) > 1:
|
|
1068
|
+
existing_val = prim[1]
|
|
1069
|
+
parts = [str(existing_val)]
|
|
1070
|
+
while arg_idx < len(args_iter):
|
|
1071
|
+
v = args_iter[arg_idx]
|
|
1072
|
+
arg_idx += 1
|
|
1073
|
+
parts.append(self._stringify(v))
|
|
1074
|
+
joined = ", ".join(parts) if parts[0] else ", ".join(parts[1:])
|
|
1075
|
+
shadow = [SHADOW_STRING, joined]
|
|
1076
|
+
inputs[input_name] = [1, shadow]
|
|
1077
|
+
|
|
1078
|
+
self.current_blocks[block_id] = {
|
|
1079
|
+
"opcode": spec["opcode"],
|
|
1080
|
+
"next": None, "parent": parent_id,
|
|
1081
|
+
"inputs": inputs,
|
|
1082
|
+
"fields": fields,
|
|
1083
|
+
"shadow": False, "topLevel": False,
|
|
1084
|
+
}
|
|
1085
|
+
return block_id, block_id
|
|
1086
|
+
|
|
1087
|
+
def _resolve_field_value(self, val_node, fname: str) -> str:
|
|
1088
|
+
"""Resolve an arg to a field value (string)."""
|
|
1089
|
+
if isinstance(val_node, A.StringLit):
|
|
1090
|
+
return val_node.value
|
|
1091
|
+
if isinstance(val_node, A.NumberLit):
|
|
1092
|
+
return str(val_node.value)
|
|
1093
|
+
if isinstance(val_node, A.BareRef):
|
|
1094
|
+
# Check if it matches a menu option
|
|
1095
|
+
return val_node.name
|
|
1096
|
+
if isinstance(val_node, A.ColorLit):
|
|
1097
|
+
return val_node.value
|
|
1098
|
+
raise CompileError(f"cannot use complex expression as field value for {fname!r}", val_node.line if hasattr(val_node, 'line') else 0)
|
|
1099
|
+
|
|
1100
|
+
def _stringify(self, val_node) -> str:
|
|
1101
|
+
if isinstance(val_node, A.StringLit):
|
|
1102
|
+
return val_node.value
|
|
1103
|
+
if isinstance(val_node, A.NumberLit):
|
|
1104
|
+
return str(val_node.value)
|
|
1105
|
+
if isinstance(val_node, A.BareRef):
|
|
1106
|
+
return val_node.name
|
|
1107
|
+
if isinstance(val_node, A.ColorLit):
|
|
1108
|
+
return val_node.value
|
|
1109
|
+
return ""
|
|
1110
|
+
|
|
1111
|
+
# ---- custom blocks ----------------------------------------------------
|
|
1112
|
+
def gen_custom_block_def(self, cb: A.CustomBlockDef) -> None:
|
|
1113
|
+
# Build proccode: "name %s %n %b" based on param types
|
|
1114
|
+
type_codes = {"string": "%s", "number": "%n", "boolean": "%b"}
|
|
1115
|
+
proccode = cb.name + "".join(" " + type_codes.get(p["type"], "%s") for p in cb.params)
|
|
1116
|
+
argument_ids = [gen_id() for _ in cb.params]
|
|
1117
|
+
argument_names = [p["name"] for p in cb.params]
|
|
1118
|
+
argument_defaults = []
|
|
1119
|
+
for p in cb.params:
|
|
1120
|
+
if p["type"] == "number":
|
|
1121
|
+
argument_defaults.append(0)
|
|
1122
|
+
elif p["type"] == "boolean":
|
|
1123
|
+
argument_defaults.append(False)
|
|
1124
|
+
else:
|
|
1125
|
+
argument_defaults.append("")
|
|
1126
|
+
|
|
1127
|
+
self.current_custom_blocks[cb.name] = {
|
|
1128
|
+
"proccode": proccode,
|
|
1129
|
+
"argument_ids": argument_ids,
|
|
1130
|
+
"argument_names": argument_names,
|
|
1131
|
+
"argument_defaults": argument_defaults,
|
|
1132
|
+
"warp": cb.warp,
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
# Generate the prototype block (procedures_prototype)
|
|
1136
|
+
# In real Scratch, the prototype has EMPTY inputs and shadow=false.
|
|
1137
|
+
# The argument info lives entirely in the mutation.
|
|
1138
|
+
proto_id = gen_id()
|
|
1139
|
+
proto_block = {
|
|
1140
|
+
"opcode": "procedures_prototype",
|
|
1141
|
+
"next": None,
|
|
1142
|
+
"parent": None,
|
|
1143
|
+
"inputs": {},
|
|
1144
|
+
"fields": {},
|
|
1145
|
+
"shadow": False,
|
|
1146
|
+
"topLevel": False,
|
|
1147
|
+
}
|
|
1148
|
+
# mutation - this is where the argument info lives
|
|
1149
|
+
proto_block["mutation"] = {
|
|
1150
|
+
"tagName": "mutation",
|
|
1151
|
+
"children": [],
|
|
1152
|
+
"proccode": proccode,
|
|
1153
|
+
"argumentids": "[]",
|
|
1154
|
+
"warp": "true" if cb.warp else "false",
|
|
1155
|
+
"argumentdefaults": "[]",
|
|
1156
|
+
"argumentnames": "[]",
|
|
1157
|
+
}
|
|
1158
|
+
# JSON-encode the lists for mutation
|
|
1159
|
+
import json as _json
|
|
1160
|
+
proto_block["mutation"]["argumentids"] = _json.dumps(argument_ids)
|
|
1161
|
+
proto_block["mutation"]["argumentdefaults"] = _json.dumps(argument_defaults)
|
|
1162
|
+
proto_block["mutation"]["argumentnames"] = _json.dumps(argument_names)
|
|
1163
|
+
|
|
1164
|
+
self.current_blocks[proto_id] = proto_block
|
|
1165
|
+
|
|
1166
|
+
# Generate the definition block (procedures_definition)
|
|
1167
|
+
def_id = gen_id()
|
|
1168
|
+
# Set the current proc params so body expressions can resolve
|
|
1169
|
+
# parameter names to argument_reporter blocks.
|
|
1170
|
+
old_params = self.current_proc_params
|
|
1171
|
+
self.current_proc_params = {
|
|
1172
|
+
p["name"]: {"arg_id": arg_id, "type": p["type"]}
|
|
1173
|
+
for p, arg_id in zip(cb.params, argument_ids)
|
|
1174
|
+
}
|
|
1175
|
+
# The custom block body is the chain inside the def
|
|
1176
|
+
body_first, body_last = self.gen_chain(cb.body, parent_id=None)
|
|
1177
|
+
self.current_proc_params = old_params
|
|
1178
|
+
|
|
1179
|
+
self.current_blocks[def_id] = {
|
|
1180
|
+
"opcode": "procedures_definition",
|
|
1181
|
+
"next": body_first,
|
|
1182
|
+
"parent": None,
|
|
1183
|
+
"inputs": {"custom_block": [2, proto_id]},
|
|
1184
|
+
"fields": {},
|
|
1185
|
+
"shadow": False,
|
|
1186
|
+
"topLevel": True,
|
|
1187
|
+
"x": 0,
|
|
1188
|
+
"y": 0,
|
|
1189
|
+
}
|
|
1190
|
+
if body_first is not None:
|
|
1191
|
+
self.current_blocks[body_first]["parent"] = def_id
|
|
1192
|
+
|
|
1193
|
+
# Store the body_first for reference (for calls that need to know)
|
|
1194
|
+
self.current_custom_blocks[cb.name]["def_block_id"] = def_id
|
|
1195
|
+
self.current_custom_blocks[cb.name]["proto_block_id"] = proto_id
|
|
1196
|
+
|
|
1197
|
+
def gen_custom_block_call(self, stmt: A.BlockCall, parent_id: Optional[str]):
|
|
1198
|
+
info = self.current_custom_blocks[stmt.method]
|
|
1199
|
+
block_id = gen_id()
|
|
1200
|
+
# arguments: each input is one of the argument_ids
|
|
1201
|
+
inputs = {}
|
|
1202
|
+
for arg_id, arg_node in zip(info["argument_ids"], stmt.args):
|
|
1203
|
+
# determine kind from param type
|
|
1204
|
+
param_types = info.get("argument_types") or ["string"] * len(info["argument_ids"])
|
|
1205
|
+
# actually we need the param types - re-derive from proccode? store them
|
|
1206
|
+
# We'll use 'any' to be flexible
|
|
1207
|
+
shadow, sub = self.gen_input_value(arg_node, "any")
|
|
1208
|
+
inputs[arg_id] = self._pack_input("any", shadow, sub)
|
|
1209
|
+
self.current_blocks[block_id] = {
|
|
1210
|
+
"opcode": "procedures_call",
|
|
1211
|
+
"next": None, "parent": parent_id,
|
|
1212
|
+
"inputs": inputs,
|
|
1213
|
+
"fields": {},
|
|
1214
|
+
"shadow": False, "topLevel": False,
|
|
1215
|
+
"mutation": {
|
|
1216
|
+
"tagName": "mutation",
|
|
1217
|
+
"children": [],
|
|
1218
|
+
"proccode": info["proccode"],
|
|
1219
|
+
"argumentids": _json_dumps(info["argument_ids"]),
|
|
1220
|
+
"warp": "true" if info["warp"] else "false",
|
|
1221
|
+
},
|
|
1222
|
+
}
|
|
1223
|
+
return block_id, block_id
|
|
1224
|
+
|
|
1225
|
+
# =====================================================================
|
|
1226
|
+
# Expressions
|
|
1227
|
+
# =====================================================================
|
|
1228
|
+
def gen_input_value(self, expr, kind: str) -> Tuple[Optional[list], Optional[str]]:
|
|
1229
|
+
"""Generate an input value.
|
|
1230
|
+
Returns (shadow_primitive, block_id_or_None).
|
|
1231
|
+
One of them is None:
|
|
1232
|
+
- For literal shadow: ([prim_type, value], None)
|
|
1233
|
+
- For block input: (None, block_id)
|
|
1234
|
+
- For variable: ([12, name, id], None) - and we'll pack as [3, ...]
|
|
1235
|
+
"""
|
|
1236
|
+
if expr is None:
|
|
1237
|
+
return self._default_shadow(kind), None
|
|
1238
|
+
|
|
1239
|
+
# Plain Python values (str, int, float) - happen when broadcast
|
|
1240
|
+
# messages come back as strings, etc.
|
|
1241
|
+
if isinstance(expr, str):
|
|
1242
|
+
if kind == "broadcast":
|
|
1243
|
+
self.current_broadcasts[expr] = expr
|
|
1244
|
+
return [SHADOW_BROADCAST, expr], None
|
|
1245
|
+
return [SHADOW_STRING, expr], None
|
|
1246
|
+
if isinstance(expr, (int, float)):
|
|
1247
|
+
return [SHADOW_NUMBER, str(expr)], None
|
|
1248
|
+
|
|
1249
|
+
# Resolve BareRef first
|
|
1250
|
+
if isinstance(expr, A.BareRef):
|
|
1251
|
+
expr = self._resolve_bare_ref(expr)
|
|
1252
|
+
|
|
1253
|
+
if isinstance(expr, A.NumberLit):
|
|
1254
|
+
val = expr.value
|
|
1255
|
+
return [SHADOW_NUMBER, str(val)], None
|
|
1256
|
+
if isinstance(expr, A.StringLit):
|
|
1257
|
+
return [SHADOW_STRING, expr.value], None
|
|
1258
|
+
if isinstance(expr, A.ColorLit):
|
|
1259
|
+
return [SHADOW_COLOR, expr.value], None
|
|
1260
|
+
if isinstance(expr, A.VarRef):
|
|
1261
|
+
vid = self.current_vars.get(expr.name)
|
|
1262
|
+
if vid is None:
|
|
1263
|
+
# auto-declare (shouldn't normally happen)
|
|
1264
|
+
self._declare_var(expr.name, 0)
|
|
1265
|
+
vid = self.current_vars[expr.name]
|
|
1266
|
+
return [SHADOW_VAR, expr.name, vid], None
|
|
1267
|
+
if isinstance(expr, A.ArgumentRef):
|
|
1268
|
+
# Generate an argument_reporter block.
|
|
1269
|
+
# In real Scratch, argument_reporter blocks inside a custom block
|
|
1270
|
+
# body have shadow=false (they're real blocks, not shadows).
|
|
1271
|
+
arg_type = getattr(expr, "_arg_type", "string")
|
|
1272
|
+
opcode = "argument_reporter_boolean" if arg_type == "boolean" else "argument_reporter_string_number"
|
|
1273
|
+
block_id = gen_id()
|
|
1274
|
+
self.current_blocks[block_id] = {
|
|
1275
|
+
"opcode": opcode,
|
|
1276
|
+
"next": None, "parent": None,
|
|
1277
|
+
"inputs": {},
|
|
1278
|
+
"fields": {"VALUE": [expr.name, None]},
|
|
1279
|
+
"shadow": False, "topLevel": False,
|
|
1280
|
+
}
|
|
1281
|
+
return None, block_id
|
|
1282
|
+
if isinstance(expr, A.ListReporterRef):
|
|
1283
|
+
return self._gen_list_reporter(expr)
|
|
1284
|
+
if isinstance(expr, A.BuiltinRef):
|
|
1285
|
+
# Builtin reporter (e.g. answer, mouseX)
|
|
1286
|
+
spec = B.REPORTERS.get(expr.name)
|
|
1287
|
+
if spec is None:
|
|
1288
|
+
# try alias
|
|
1289
|
+
alias = B.BUILTIN_ALIASES.get(expr.name.lower())
|
|
1290
|
+
if alias:
|
|
1291
|
+
spec = B.REPORTERS.get(alias)
|
|
1292
|
+
if spec is None:
|
|
1293
|
+
suggestion = did_you_mean(expr.name, B.REPORTERS.keys())
|
|
1294
|
+
raise CompileError(
|
|
1295
|
+
f'"{expr.name}" is not valid TypeScratch syntax{suggestion}',
|
|
1296
|
+
expr.line
|
|
1297
|
+
)
|
|
1298
|
+
return self._gen_reporter_block(spec, [], expr.line)
|
|
1299
|
+
if isinstance(expr, A.FuncCall):
|
|
1300
|
+
# Could be: reporter (pickRandom, join, abs, etc.)
|
|
1301
|
+
# or math op like Math.abs
|
|
1302
|
+
name = expr.name
|
|
1303
|
+
spec = B.REPORTERS.get(name)
|
|
1304
|
+
if spec is None:
|
|
1305
|
+
# try alias
|
|
1306
|
+
alias = B.BUILTIN_ALIASES.get(name.lower())
|
|
1307
|
+
if alias:
|
|
1308
|
+
spec = B.REPORTERS.get(alias)
|
|
1309
|
+
if spec is None:
|
|
1310
|
+
suggestion = did_you_mean(name, B.REPORTERS.keys())
|
|
1311
|
+
raise CompileError(
|
|
1312
|
+
f'"{name}" is not valid TypeScratch syntax{suggestion}',
|
|
1313
|
+
expr.line
|
|
1314
|
+
)
|
|
1315
|
+
return self._gen_reporter_block(spec, expr.args, expr.line)
|
|
1316
|
+
if isinstance(expr, A.BinOp):
|
|
1317
|
+
return self._gen_binop(expr)
|
|
1318
|
+
if isinstance(expr, A.UnaryOp):
|
|
1319
|
+
return self._gen_unaryop(expr)
|
|
1320
|
+
raise CompileError(f"cannot generate expression: {type(expr).__name__}", getattr(expr, 'line', 0))
|
|
1321
|
+
|
|
1322
|
+
def _resolve_bare_ref(self, ref: A.BareRef):
|
|
1323
|
+
"""Resolve a BareRef into VarRef / ArgumentRef / BuiltinRef / StringLit."""
|
|
1324
|
+
name = ref.name
|
|
1325
|
+
if name in self.current_vars:
|
|
1326
|
+
return A.VarRef(name=name, line=ref.line)
|
|
1327
|
+
# custom block parameter?
|
|
1328
|
+
if name in self.current_proc_params:
|
|
1329
|
+
param_info = self.current_proc_params[name]
|
|
1330
|
+
ref = A.ArgumentRef(name=name, arg_id=param_info["arg_id"], line=ref.line)
|
|
1331
|
+
ref._arg_type = param_info["type"]
|
|
1332
|
+
return ref
|
|
1333
|
+
# check builtin aliases (case-insensitive)
|
|
1334
|
+
lower = name.lower()
|
|
1335
|
+
if lower in B.BUILTIN_ALIASES:
|
|
1336
|
+
return A.BuiltinRef(name=B.BUILTIN_ALIASES[lower], line=ref.line)
|
|
1337
|
+
# check direct reporter name
|
|
1338
|
+
if name in B.REPORTERS:
|
|
1339
|
+
return A.BuiltinRef(name=name, line=ref.line)
|
|
1340
|
+
# default: string literal
|
|
1341
|
+
return A.StringLit(value=name, line=ref.line)
|
|
1342
|
+
|
|
1343
|
+
def _gen_list_reporter(self, expr: A.ListReporterRef) -> Tuple[Optional[list], Optional[str]]:
|
|
1344
|
+
"""Generate a list reporter block (data_itemoflist, data_lengthoflist,
|
|
1345
|
+
data_listcontainsitem, data_itemnumoflist, data_listcontents).
|
|
1346
|
+
|
|
1347
|
+
The list reference is a FIELD with [name, id] format."""
|
|
1348
|
+
# Make sure the list is declared
|
|
1349
|
+
if expr.list_name not in self.current_lists:
|
|
1350
|
+
lid = gen_id()
|
|
1351
|
+
self.current_lists[expr.list_name] = lid
|
|
1352
|
+
self.current_list_values[expr.list_name] = []
|
|
1353
|
+
lid = self.current_lists[expr.list_name]
|
|
1354
|
+
list_field = [expr.list_name, lid]
|
|
1355
|
+
|
|
1356
|
+
method = expr.method
|
|
1357
|
+
# Normalize aliases
|
|
1358
|
+
if method == "itemNumber":
|
|
1359
|
+
method = "itemNum"
|
|
1360
|
+
if method == "asString":
|
|
1361
|
+
method = "contents"
|
|
1362
|
+
|
|
1363
|
+
block_id = gen_id()
|
|
1364
|
+
|
|
1365
|
+
if method == "item":
|
|
1366
|
+
# data_itemoflist: INDEX input, LIST field (INDEX is required)
|
|
1367
|
+
if not expr.args:
|
|
1368
|
+
raise CompileError(
|
|
1369
|
+
f'{expr.list_name}.item() requires an index argument, '
|
|
1370
|
+
f'e.g. {expr.list_name}.item(1)',
|
|
1371
|
+
expr.line
|
|
1372
|
+
)
|
|
1373
|
+
idx_node = expr.args[0]
|
|
1374
|
+
shadow, sub = self.gen_input_value(idx_node, "number")
|
|
1375
|
+
self.current_blocks[block_id] = {
|
|
1376
|
+
"opcode": "data_itemoflist",
|
|
1377
|
+
"next": None, "parent": None,
|
|
1378
|
+
"inputs": {"INDEX": self._pack_input("number", shadow, sub)},
|
|
1379
|
+
"fields": {"LIST": list_field},
|
|
1380
|
+
"shadow": False, "topLevel": False,
|
|
1381
|
+
}
|
|
1382
|
+
return None, block_id
|
|
1383
|
+
|
|
1384
|
+
if method == "length":
|
|
1385
|
+
# data_lengthoflist: just LIST field
|
|
1386
|
+
self.current_blocks[block_id] = {
|
|
1387
|
+
"opcode": "data_lengthoflist",
|
|
1388
|
+
"next": None, "parent": None,
|
|
1389
|
+
"inputs": {},
|
|
1390
|
+
"fields": {"LIST": list_field},
|
|
1391
|
+
"shadow": False, "topLevel": False,
|
|
1392
|
+
}
|
|
1393
|
+
return None, block_id
|
|
1394
|
+
|
|
1395
|
+
if method == "contains":
|
|
1396
|
+
# data_listcontainsitem: ITEM input, LIST field (ITEM is required)
|
|
1397
|
+
if not expr.args:
|
|
1398
|
+
raise CompileError(
|
|
1399
|
+
f'{expr.list_name}.contains() requires an argument, '
|
|
1400
|
+
f'e.g. {expr.list_name}.contains(item)',
|
|
1401
|
+
expr.line
|
|
1402
|
+
)
|
|
1403
|
+
item_node = expr.args[0]
|
|
1404
|
+
shadow, sub = self.gen_input_value(item_node, "string")
|
|
1405
|
+
self.current_blocks[block_id] = {
|
|
1406
|
+
"opcode": "data_listcontainsitem",
|
|
1407
|
+
"next": None, "parent": None,
|
|
1408
|
+
"inputs": {"ITEM": self._pack_input("string", shadow, sub)},
|
|
1409
|
+
"fields": {"LIST": list_field},
|
|
1410
|
+
"shadow": False, "topLevel": False,
|
|
1411
|
+
}
|
|
1412
|
+
return None, block_id
|
|
1413
|
+
|
|
1414
|
+
if method == "itemNum":
|
|
1415
|
+
# data_itemnumoflist: ITEM input, LIST field (ITEM is required)
|
|
1416
|
+
if not expr.args:
|
|
1417
|
+
raise CompileError(
|
|
1418
|
+
f'{expr.list_name}.itemNum() requires an argument, '
|
|
1419
|
+
f'e.g. {expr.list_name}.itemNum(item)',
|
|
1420
|
+
expr.line
|
|
1421
|
+
)
|
|
1422
|
+
item_node = expr.args[0]
|
|
1423
|
+
shadow, sub = self.gen_input_value(item_node, "string")
|
|
1424
|
+
self.current_blocks[block_id] = {
|
|
1425
|
+
"opcode": "data_itemnumoflist",
|
|
1426
|
+
"next": None, "parent": None,
|
|
1427
|
+
"inputs": {"ITEM": self._pack_input("string", shadow, sub)},
|
|
1428
|
+
"fields": {"LIST": list_field},
|
|
1429
|
+
"shadow": False, "topLevel": False,
|
|
1430
|
+
}
|
|
1431
|
+
return None, block_id
|
|
1432
|
+
|
|
1433
|
+
if method == "contents":
|
|
1434
|
+
# data_listcontents: just LIST field
|
|
1435
|
+
self.current_blocks[block_id] = {
|
|
1436
|
+
"opcode": "data_listcontents",
|
|
1437
|
+
"next": None, "parent": None,
|
|
1438
|
+
"inputs": {},
|
|
1439
|
+
"fields": {"LIST": list_field},
|
|
1440
|
+
"shadow": False, "topLevel": False,
|
|
1441
|
+
}
|
|
1442
|
+
return None, block_id
|
|
1443
|
+
|
|
1444
|
+
raise CompileError(
|
|
1445
|
+
f"unknown list reporter method: {method!r} "
|
|
1446
|
+
f"(supported: item, length, contains, itemNum, contents)",
|
|
1447
|
+
expr.line
|
|
1448
|
+
)
|
|
1449
|
+
|
|
1450
|
+
def _gen_reporter_block(self, spec, args, line) -> Tuple[Optional[list], Optional[str]]:
|
|
1451
|
+
# Emit warning for hidden/experimental reporters
|
|
1452
|
+
warn = spec.get("_warn")
|
|
1453
|
+
if warn:
|
|
1454
|
+
import sys
|
|
1455
|
+
print(f"WARNING (line {line}): {warn}", file=sys.stderr)
|
|
1456
|
+
|
|
1457
|
+
block_id = gen_id()
|
|
1458
|
+
inputs = {}
|
|
1459
|
+
fields = {}
|
|
1460
|
+
arg_idx = 0
|
|
1461
|
+
for fname, fval in spec.get("fields", []):
|
|
1462
|
+
if fval is None:
|
|
1463
|
+
if arg_idx < len(args):
|
|
1464
|
+
val_node = args[arg_idx]
|
|
1465
|
+
arg_idx += 1
|
|
1466
|
+
fields[fname] = [self._resolve_field_value(val_node, fname), None]
|
|
1467
|
+
else:
|
|
1468
|
+
fields[fname] = ["", None]
|
|
1469
|
+
else:
|
|
1470
|
+
fields[fname] = [fval, None]
|
|
1471
|
+
for spec_entry in spec.get("args", []):
|
|
1472
|
+
input_name = spec_entry[0]
|
|
1473
|
+
input_kind = spec_entry[1]
|
|
1474
|
+
if arg_idx < len(args):
|
|
1475
|
+
val_node = args[arg_idx]
|
|
1476
|
+
arg_idx += 1
|
|
1477
|
+
shadow, sub = self.gen_input_value(val_node, input_kind)
|
|
1478
|
+
inputs[input_name] = self._pack_input(input_kind, shadow, sub)
|
|
1479
|
+
else:
|
|
1480
|
+
shadow, sub = self.gen_input_value(A.NumberLit(0), input_kind)
|
|
1481
|
+
inputs[input_name] = self._pack_input(input_kind, shadow, sub)
|
|
1482
|
+
self.current_blocks[block_id] = {
|
|
1483
|
+
"opcode": spec["opcode"],
|
|
1484
|
+
"next": None, "parent": None,
|
|
1485
|
+
"inputs": inputs,
|
|
1486
|
+
"fields": fields,
|
|
1487
|
+
"shadow": False, "topLevel": False,
|
|
1488
|
+
}
|
|
1489
|
+
return None, block_id
|
|
1490
|
+
|
|
1491
|
+
def _gen_binop(self, expr: A.BinOp) -> Tuple[Optional[list], Optional[str]]:
|
|
1492
|
+
op = expr.op
|
|
1493
|
+
op_to_opcode = {
|
|
1494
|
+
"+": "operator_add",
|
|
1495
|
+
"-": "operator_subtract",
|
|
1496
|
+
"*": "operator_multiply",
|
|
1497
|
+
"/": "operator_divide",
|
|
1498
|
+
"mod": "operator_mod",
|
|
1499
|
+
">": "operator_gt",
|
|
1500
|
+
"<": "operator_lt",
|
|
1501
|
+
"=": "operator_equals",
|
|
1502
|
+
"and": "operator_and",
|
|
1503
|
+
"or": "operator_or",
|
|
1504
|
+
}
|
|
1505
|
+
opcode = op_to_opcode.get(op)
|
|
1506
|
+
if opcode is None:
|
|
1507
|
+
raise CompileError(f"unknown binary operator: {op!r}", expr.line)
|
|
1508
|
+
|
|
1509
|
+
block_id = gen_id()
|
|
1510
|
+
# Determine input kind and input names based on operator type
|
|
1511
|
+
# Math operators (+,-,*,/,mod) use NUM1/NUM2
|
|
1512
|
+
# Comparison operators (>,<,=) use OPERAND1/OPERAND2
|
|
1513
|
+
# Logic operators (and,or) use OPERAND1/OPERAND2
|
|
1514
|
+
if op in ("+", "-", "*", "/", "mod"):
|
|
1515
|
+
input1_name = "NUM1"
|
|
1516
|
+
input2_name = "NUM2"
|
|
1517
|
+
kind = "number"
|
|
1518
|
+
elif op in (">", "<", "="):
|
|
1519
|
+
input1_name = "OPERAND1"
|
|
1520
|
+
input2_name = "OPERAND2"
|
|
1521
|
+
kind = "any"
|
|
1522
|
+
else: # and, or
|
|
1523
|
+
input1_name = "OPERAND1"
|
|
1524
|
+
input2_name = "OPERAND2"
|
|
1525
|
+
kind = "boolean"
|
|
1526
|
+
left_shadow, left_sub = self.gen_input_value(expr.left, kind)
|
|
1527
|
+
right_shadow, right_sub = self.gen_input_value(expr.right, kind)
|
|
1528
|
+
inputs = {
|
|
1529
|
+
input1_name: self._pack_input(kind, left_shadow, left_sub),
|
|
1530
|
+
input2_name: self._pack_input(kind, right_shadow, right_sub),
|
|
1531
|
+
}
|
|
1532
|
+
self.current_blocks[block_id] = {
|
|
1533
|
+
"opcode": opcode,
|
|
1534
|
+
"next": None, "parent": None,
|
|
1535
|
+
"inputs": inputs,
|
|
1536
|
+
"fields": {},
|
|
1537
|
+
"shadow": False, "topLevel": False,
|
|
1538
|
+
}
|
|
1539
|
+
return None, block_id
|
|
1540
|
+
|
|
1541
|
+
def _gen_unaryop(self, expr: A.UnaryOp) -> Tuple[Optional[list], Optional[str]]:
|
|
1542
|
+
op = expr.op
|
|
1543
|
+
if op == "not":
|
|
1544
|
+
block_id = gen_id()
|
|
1545
|
+
shadow, sub = self.gen_input_value(expr.operand, "boolean")
|
|
1546
|
+
self.current_blocks[block_id] = {
|
|
1547
|
+
"opcode": "operator_not",
|
|
1548
|
+
"next": None, "parent": None,
|
|
1549
|
+
"inputs": {"OPERAND": self._pack_input("boolean", shadow, sub)},
|
|
1550
|
+
"fields": {},
|
|
1551
|
+
"shadow": False, "topLevel": False,
|
|
1552
|
+
}
|
|
1553
|
+
return None, block_id
|
|
1554
|
+
if op == "-":
|
|
1555
|
+
# negate: 0 - operand
|
|
1556
|
+
zero = A.NumberLit(0)
|
|
1557
|
+
new_expr = A.BinOp(op="-", left=zero, right=expr.operand, line=expr.line)
|
|
1558
|
+
return self._gen_binop(new_expr)
|
|
1559
|
+
raise CompileError(f"unknown unary operator: {op!r}", expr.line)
|
|
1560
|
+
|
|
1561
|
+
# =====================================================================
|
|
1562
|
+
# Input packing
|
|
1563
|
+
# =====================================================================
|
|
1564
|
+
def _pack_input(self, kind: str, shadow: Optional[list], block_id: Optional[str]) -> list:
|
|
1565
|
+
"""Pack a shadow/block into the Scratch input format.
|
|
1566
|
+
|
|
1567
|
+
Scratch 3 input formats:
|
|
1568
|
+
[1, [prim, val]] - pure shadow (literal value)
|
|
1569
|
+
[2, block_id] - block only, no shadow (substacks, booleans)
|
|
1570
|
+
[2, block_id, [prim, val]] - block with shadow (reporter covering default)
|
|
1571
|
+
[3, [12, name, id]] - variable reference
|
|
1572
|
+
[3, block_id, [prim, val]] - block with shadow (alt form, used by reporters)
|
|
1573
|
+
|
|
1574
|
+
When a reporter covers an input, we MUST include the shadow so that
|
|
1575
|
+
removing the reporter in the editor shows a default value instead of
|
|
1576
|
+
a "hole". This is the key fix for the hole bug.
|
|
1577
|
+
"""
|
|
1578
|
+
# Variable input - use format [3, [12, name, id]]
|
|
1579
|
+
if shadow is not None and len(shadow) > 0 and shadow[0] == SHADOW_VAR:
|
|
1580
|
+
return [3, shadow]
|
|
1581
|
+
# List input (similar)
|
|
1582
|
+
if shadow is not None and len(shadow) > 0 and shadow[0] == SHADOW_LIST:
|
|
1583
|
+
return [3, shadow]
|
|
1584
|
+
# Broadcast input
|
|
1585
|
+
if shadow is not None and len(shadow) > 0 and shadow[0] == SHADOW_BROADCAST:
|
|
1586
|
+
if block_id is None:
|
|
1587
|
+
return [1, shadow]
|
|
1588
|
+
return [2, block_id, shadow]
|
|
1589
|
+
# Block covering a shadow - MUST include the shadow so the editor
|
|
1590
|
+
# shows a default value when the reporter is removed.
|
|
1591
|
+
# Use format [3, block_id, shadow] (shadow type 3) like real Scratch.
|
|
1592
|
+
if block_id is not None and shadow is not None:
|
|
1593
|
+
return [3, block_id, shadow]
|
|
1594
|
+
# Block only (no shadow provided) - generate a default shadow based
|
|
1595
|
+
# on the input kind so the editor doesn't show a "hole" when the
|
|
1596
|
+
# reporter is removed.
|
|
1597
|
+
if block_id is not None and shadow is None:
|
|
1598
|
+
default = self._default_shadow(kind)
|
|
1599
|
+
if default is not None:
|
|
1600
|
+
return [3, block_id, default]
|
|
1601
|
+
return [2, block_id]
|
|
1602
|
+
# Pure shadow
|
|
1603
|
+
if shadow is not None:
|
|
1604
|
+
return [1, shadow]
|
|
1605
|
+
# Empty - shouldn't happen
|
|
1606
|
+
return [1, self._default_shadow(kind)]
|
|
1607
|
+
|
|
1608
|
+
def _pack_substack(self, first_block_id: Optional[str]) -> list:
|
|
1609
|
+
if first_block_id is None:
|
|
1610
|
+
return [2, None] # empty substack
|
|
1611
|
+
return [2, first_block_id]
|
|
1612
|
+
|
|
1613
|
+
def _default_shadow(self, kind: str) -> list:
|
|
1614
|
+
if kind == "number":
|
|
1615
|
+
return [SHADOW_NUMBER, ""]
|
|
1616
|
+
if kind == "color":
|
|
1617
|
+
return [SHADOW_COLOR, "#000000"]
|
|
1618
|
+
if kind == "boolean":
|
|
1619
|
+
return None # no shadow for booleans
|
|
1620
|
+
if kind == "broadcast":
|
|
1621
|
+
return [SHADOW_BROADCAST, ""]
|
|
1622
|
+
return [SHADOW_STRING, ""]
|
|
1623
|
+
|
|
1624
|
+
|
|
1625
|
+
def _md5(data: bytes) -> str:
|
|
1626
|
+
return hashlib.md5(data).hexdigest()
|
|
1627
|
+
|
|
1628
|
+
|
|
1629
|
+
def _json_dumps(obj) -> str:
|
|
1630
|
+
import json
|
|
1631
|
+
return json.dumps(obj)
|