Skip to content

Commit f2c3df8

Browse files
committed
json extension compiling and raw js object return types for codegen
1 parent fb110a3 commit f2c3df8

10 files changed

Lines changed: 632 additions & 45 deletions

File tree

src/compiler/compat-blocks.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,6 @@ const inputs = [
4444
'assets_infolder',
4545
'motion_xscroll',
4646
'motion_yscroll',
47-
// ponytail: string helpers use the compatibility layer; add compiler IR only if profiling shows it matters.
48-
'operator_change_case',
49-
'operator_index_of',
50-
'operator_letters_of',
51-
'operator_repeat',
52-
'operator_replace',
53-
'operator_trim',
5447
'sensing_loud',
5548
'sensing_loudness',
5649
'sensing_userid',

src/compiler/enums.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,18 @@ const TYPES = {
3838
OBJECT: 0x2000,
3939
ARRAY: 0x4000,
4040

41-
ANY: 0x7FFF,
41+
ANY: 0x47FFF,
4242
COLOR: 0x8000,
4343

4444
NUMBER_INT: 0x03A,
4545
NUMBER_NAN: 0x1FF,
46-
UNKNOWN: 0x7FFF,
46+
UNKNOWN: 0x47FFF,
4747
LOWER_STRING: 0x10000,
48-
PROCEDURE_ARG: 0x20000
48+
PROCEDURE_ARG: 0x20000,
49+
// Native object or array used by compiled extension blocks.
50+
JSON: 0x40000,
51+
// A JSON item: native JSON or a Scratch-compatible primitive.
52+
JSON_VALUE: 0x41FFF
4953
};
5054

5155
let INPUT_I = 1;
@@ -181,16 +185,22 @@ const BLOCKS = {
181185
NOT: id(),
182186
OR: id(),
183187
AND: id(),
188+
CHANGECASE: id(),
184189
EQUALS: id(),
185190
GREATER: id(),
191+
INDEXOF: id(),
186192
LESS: id(),
187193
LETTEROF: id(),
194+
LETTERSOF: id(),
188195
LENGTH: id(),
189196
CONTAINS: id(),
190197
MOD: id(),
191198
EXP: id(),
192199
JOIN: id(),
200+
REPEAT: id(),
201+
REPLACE: id(),
193202
TENEXP: id(),
203+
TRIM: id(),
194204
PI: id(),
195205
NEWLINE: id()
196206
},
@@ -206,6 +216,8 @@ const BLOCKS = {
206216

207217
COMPAT: id(),
208218

219+
EXTENSION: id(),
220+
209221
ADDONS: {
210222
CALL: id()
211223
},

src/compiler/extensions/index.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const extensions = [
2+
require('./skyhigh173JSON')
3+
];
4+
5+
const blocks = Object.create(null);
6+
for (const extension of extensions) {
7+
for (const [opcode, info] of Object.entries(extension.blocks)) {
8+
blocks[`${extension.id}_${opcode}`] = {
9+
extension: extension.id,
10+
generate: extension.generate,
11+
native: extension.isNative(opcode),
12+
...info
13+
};
14+
}
15+
}
16+
17+
const get = opcode => blocks[opcode] || null;
18+
const canCompile = (opcode, runtime) => {
19+
const block = get(opcode);
20+
return Boolean(block && (block.native || runtime[`ext_${block.extension}`]));
21+
};
22+
23+
module.exports = {get, canCompile};
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
const {TYPES} = require('../enums');
2+
const {sanitize} = require('../shared');
3+
4+
const id = 'skyhigh173JSON';
5+
const block = (type, target = false) => ({type, target});
6+
7+
const blocks = {
8+
json_is_valid: block(TYPES.BOOLEAN),
9+
json_is: block(TYPES.BOOLEAN),
10+
json_get_all: block(TYPES.JSON),
11+
json_new: block(TYPES.JSON),
12+
json_has_key: block(TYPES.BOOLEAN),
13+
json_has_value: block(TYPES.BOOLEAN),
14+
json_equal: block(TYPES.BOOLEAN),
15+
json_jlength: block(TYPES.UNKNOWN),
16+
json_get: block(TYPES.JSON_VALUE),
17+
json_set: block(TYPES.JSON),
18+
json_delete: block(TYPES.JSON),
19+
json_length: block(TYPES.UNKNOWN),
20+
json_array_get: block(TYPES.JSON_VALUE),
21+
json_array_push: block(TYPES.JSON),
22+
json_array_set: block(TYPES.JSON),
23+
json_array_insert: block(TYPES.JSON),
24+
json_array_delete: block(TYPES.JSON),
25+
json_array_remove_all: block(TYPES.JSON),
26+
json_array_itemH: block(TYPES.STRING),
27+
json_array_from: block(TYPES.JSON),
28+
json_array_fromto: block(TYPES.JSON),
29+
json_array_reverse: block(TYPES.JSON),
30+
json_array_flat: block(TYPES.JSON),
31+
json_array_concat: block(TYPES.JSON),
32+
json_array_filter: block(TYPES.JSON),
33+
json_array_setlen: block(TYPES.JSON),
34+
json_array_create: block(TYPES.JSON),
35+
json_array_join: block(TYPES.STRING),
36+
json_array_sort: block(TYPES.JSON),
37+
json_array_analysis: block(TYPES.NUMBER_NAN),
38+
json_vm_getlist: block(TYPES.JSON, true),
39+
json_vm_setlist: block(null, true)
40+
};
41+
42+
const helperSources = {
43+
extJSONValue: `const extJSONValue = value => {
44+
if (typeof value === "string" && (
45+
(value[0] === "[" && value[value.length - 1] === "]") ||
46+
(value[0] === "{" && value[value.length - 1] === "}")
47+
)) {
48+
try {
49+
value = JSON.parse(value) ?? "";
50+
} catch {
51+
// Keep the original string.
52+
}
53+
}
54+
if (Number.isNaN(value)) return "NaN";
55+
if (value === Infinity) return "Infinity";
56+
if (value === -Infinity) return "-Infinity";
57+
return value ?? "";
58+
}`,
59+
extJSONIsValid: `const extJSONIsValid = value => {
60+
if (typeof value !== "string") return false;
61+
value = value.trim();
62+
if (!(
63+
(value[0] === "[" && value[value.length - 1] === "]") ||
64+
(value[0] === "{" && value[value.length - 1] === "}")
65+
)) return false;
66+
try {
67+
JSON.parse(value);
68+
return true;
69+
} catch {
70+
return false;
71+
}
72+
}`,
73+
extJSONIs: `const extJSONIs = (json, type) => {
74+
if (!extJSONIsValid(json)) return false;
75+
const value = JSON.parse(json);
76+
return type === "Array" ? Array.isArray(value) : type === "Object" && !Array.isArray(value);
77+
}`,
78+
extJSONGetAll: `const extJSONGetAll = (json, type) => {
79+
try {
80+
const keys = Object.keys(json);
81+
if (type === "keys") return keys;
82+
if (type === "values") return keys.map(key => json[key] ?? "");
83+
if (type === "datas") return keys.map(key => [key, json[key] ?? ""]);
84+
} catch {
85+
// Return an empty JSON value.
86+
}
87+
return [];
88+
}`,
89+
extJSONNew: `const extJSONNew = type => type === "Array" ? [] : {}`,
90+
extJSONHasKey: `const extJSONHasKey = (json, key) => {
91+
try {
92+
return extJSONValue(key) in json;
93+
} catch {
94+
return false;
95+
}
96+
}`,
97+
extJSONGet: `const extJSONGet = (json, key) => {
98+
try {
99+
if (Object.prototype.hasOwnProperty.call(json, key)) {
100+
return json[key] ?? "";
101+
}
102+
} catch {
103+
// Return the extension's empty fallback.
104+
}
105+
return "";
106+
}`,
107+
extJSONSet: `const extJSONSet = (json, key, value) => {
108+
if (json === null || typeof json !== "object") return {};
109+
const result = Array.isArray(json) ? json.slice() : {...json};
110+
result[key] = extJSONValue(value);
111+
return result;
112+
}`,
113+
extJSONDelete: `const extJSONDelete = (json, key) => {
114+
if (json === null || typeof json !== "object") return {};
115+
const result = Array.isArray(json) ? json.slice() : {...json};
116+
delete result[key];
117+
return result;
118+
}`,
119+
extJSONLength: `const extJSONLength = json => {
120+
try {
121+
return Object.keys(json).length;
122+
} catch {
123+
return " ";
124+
}
125+
}`,
126+
extJSONArrayGet: `const extJSONArrayGet = (json, index) => {
127+
try {
128+
index = +index;
129+
if (Number.isNaN(index) || index === 0) return "";
130+
if (index > 0) index--;
131+
return (index >= 0 ? json[index] : json[json.length + index]) ?? "";
132+
} catch {
133+
return "";
134+
}
135+
}`,
136+
extJSONArrayPush: `const extJSONArrayPush = (json, value) => {
137+
if (!Array.isArray(json)) return [];
138+
const result = json.slice();
139+
result.push(extJSONValue(value));
140+
return result;
141+
}`,
142+
extJSONArraySet: `const extJSONArraySet = (json, index, value) => {
143+
if (!Array.isArray(json)) return [];
144+
const result = json.slice();
145+
result[index - 1] = extJSONValue(value);
146+
return result;
147+
}`,
148+
extJSONArrayInsert: `const extJSONArrayInsert = (json, index, value) => {
149+
if (!Array.isArray(json)) return [];
150+
const result = json.slice();
151+
result.splice(index - 1, 0, extJSONValue(value));
152+
return result;
153+
}`,
154+
extJSONArrayDelete: `const extJSONArrayDelete = (json, index) => {
155+
if (!Array.isArray(json)) return [];
156+
const result = json.slice();
157+
result.splice(index - 1, 1);
158+
return result;
159+
}`
160+
};
161+
162+
const helperDependencies = {
163+
extJSONIs: ['extJSONIsValid'],
164+
extJSONHasKey: ['extJSONValue'],
165+
extJSONSet: ['extJSONValue'],
166+
extJSONArrayPush: ['extJSONValue'],
167+
extJSONArraySet: ['extJSONValue'],
168+
extJSONArrayInsert: ['extJSONValue']
169+
};
170+
171+
const useHelper = (compiler, name) => {
172+
for (const dependency of helperDependencies[name] || []) {
173+
useHelper(compiler, dependency);
174+
}
175+
compiler.prependFunctions.set(name, helperSources[name]);
176+
return name;
177+
};
178+
179+
const findArgument = (values, name) => Object.keys(values || {})
180+
.find(candidate => candidate.toLowerCase() === name.toLowerCase());
181+
182+
const compileArgument = (node, compiler, name) => {
183+
const inputName = findArgument(node.inputs, name);
184+
if (inputName && node.inputs[inputName]) return compiler.descendInput(node.inputs[inputName]);
185+
186+
const fieldName = findArgument(node.fields, name);
187+
if (fieldName) {
188+
const field = node.fields[fieldName];
189+
return compiler.safeConstantInput(field && typeof field === 'object' ? field.value : field);
190+
}
191+
192+
// Old projects and extension patches can omit menu shadows. Match Scratch's empty-input fallback.
193+
return compiler.safeConstantInput('');
194+
};
195+
196+
const nativeCall = (node, compiler, helper, inputs, jsonInputs = [], valueInputs = []) =>
197+
`${useHelper(compiler, helper)}(${inputs
198+
.map(name => {
199+
const input = compileArgument(node, compiler, name);
200+
if (jsonInputs.includes(name)) return input.asJSON();
201+
if (valueInputs.includes(name) && !input.isAlwaysConstant()) return input.source;
202+
return input.asSafe();
203+
})
204+
.join(', ')})`;
205+
206+
const nativeGenerators = {
207+
json_is_valid: (node, compiler) => nativeCall(node, compiler, 'extJSONIsValid', ['json']),
208+
json_is: (node, compiler) => nativeCall(node, compiler, 'extJSONIs', ['json', 'types']),
209+
json_get_all: (node, compiler) => nativeCall(node, compiler, 'extJSONGetAll', ['json', 'Stype'], ['json']),
210+
json_new: (node, compiler) => nativeCall(node, compiler, 'extJSONNew', ['json']),
211+
json_has_key: (node, compiler) => nativeCall(node, compiler, 'extJSONHasKey', ['json', 'key'], ['json']),
212+
json_jlength: (node, compiler) => nativeCall(node, compiler, 'extJSONLength', ['json'], ['json']),
213+
json_get: (node, compiler) => nativeCall(node, compiler, 'extJSONGet', ['json', 'item'], ['json']),
214+
json_set: (node, compiler) => nativeCall(node, compiler, 'extJSONSet',
215+
['json', 'item', 'value'], ['json'], ['value']),
216+
json_delete: (node, compiler) => nativeCall(node, compiler, 'extJSONDelete', ['json', 'item'], ['json']),
217+
json_length: (node, compiler) => nativeCall(node, compiler, 'extJSONLength', ['json'], ['json']),
218+
json_array_get: (node, compiler) => nativeCall(node, compiler, 'extJSONArrayGet', ['json', 'item'], ['json']),
219+
json_array_push: (node, compiler) => nativeCall(node, compiler, 'extJSONArrayPush',
220+
['json', 'item'], ['json'], ['item']),
221+
json_array_set: (node, compiler) => nativeCall(node, compiler, 'extJSONArraySet',
222+
['json', 'pos', 'item'], ['json'], ['item']),
223+
json_array_insert: (node, compiler) => nativeCall(node, compiler, 'extJSONArrayInsert',
224+
['json', 'pos', 'item'], ['json'], ['item']),
225+
json_array_delete: (node, compiler) => nativeCall(node, compiler, 'extJSONArrayDelete',
226+
['json', 'item'], ['json'])
227+
};
228+
229+
const generateExtensionCall = (node, compiler, info) => {
230+
const args = [];
231+
for (const [name, input] of Object.entries(node.inputs)) {
232+
args.push(`"${sanitize(name)}":${compiler.descendInput(input).asSafe()}`);
233+
}
234+
for (const [name, value] of Object.entries(node.fields)) {
235+
args.push(`"${sanitize(name)}":"${sanitize(value)}"`);
236+
}
237+
const method = node.opcode.substring(id.length + 1);
238+
return `runtime.ext_${id}.${method}({${args.join(',')}}${info.target ? ', {target}' : ''})`;
239+
};
240+
241+
const generate = (node, compiler, info) => {
242+
const opcode = node.opcode.substring(id.length + 1);
243+
const native = nativeGenerators[opcode];
244+
if (native) return native(node, compiler);
245+
const source = generateExtensionCall(node, compiler, info);
246+
return info.type === TYPES.JSON ? `(parseJSON(${source}) ?? {})` : source;
247+
};
248+
249+
const isNative = opcode => Boolean(nativeGenerators[opcode]);
250+
251+
module.exports = {id, blocks, generate, isNative};

0 commit comments

Comments
 (0)