moonscratch 0.1.0-alpha.0
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.
- package/.agents/skills/moonbit-agent-guide/LICENSE +202 -0
- package/.agents/skills/moonbit-agent-guide/SKILL.mbt.md +1126 -0
- package/.agents/skills/moonbit-agent-guide/SKILL.md +1126 -0
- package/.agents/skills/moonbit-agent-guide/ide.md +116 -0
- package/.agents/skills/moonbit-agent-guide/references/advanced-moonbit-build.md +106 -0
- package/.agents/skills/moonbit-agent-guide/references/moonbit-language-fundamentals.mbt.md +422 -0
- package/.agents/skills/moonbit-agent-guide/references/moonbit-language-fundamentals.md +422 -0
- package/.agents/skills/moonbit-practice/SKILL.md +258 -0
- package/.agents/skills/moonbit-practice/assets/ci.yaml +25 -0
- package/.agents/skills/moonbit-practice/reference/agents.md +1469 -0
- package/.agents/skills/moonbit-practice/reference/configuration.md +228 -0
- package/.agents/skills/moonbit-practice/reference/ffi.md +229 -0
- package/.agents/skills/moonbit-practice/reference/ide.md +189 -0
- package/.agents/skills/moonbit-practice/reference/performance.md +217 -0
- package/.agents/skills/moonbit-practice/reference/refactor.md +154 -0
- package/.agents/skills/moonbit-practice/reference/stdlib.md +351 -0
- package/.agents/skills/moonbit-practice/reference/testing.md +228 -0
- package/.agents/skills/moonbit-refactoring/LICENSE +21 -0
- package/.agents/skills/moonbit-refactoring/SKILL.md +323 -0
- package/.githooks/README.md +23 -0
- package/.githooks/pre-commit +3 -0
- package/.github/workflows/copilot-setup-steps.yml +40 -0
- package/.turbo/turbo-typecheck.log +2 -0
- package/AGENTS.md +91 -0
- package/LICENSE +21 -0
- package/PLAN.md +64 -0
- package/README.mbt.md +77 -0
- package/README.md +84 -0
- package/TODO.md +120 -0
- package/a.png +0 -0
- package/benchmarks/calc.bench.ts +144 -0
- package/benchmarks/draw.bench.ts +215 -0
- package/benchmarks/load.bench.ts +28 -0
- package/benchmarks/render.bench.ts +53 -0
- package/benchmarks/run.bench.ts +8 -0
- package/benchmarks/types.d.ts +15 -0
- package/docs/scratch-vm-specs/eventloop.md +103 -0
- package/docs/scratch-vm-specs/moonscratch-time-separation.md +50 -0
- package/index.html +91 -0
- package/js/AGENTS.md +5 -0
- package/js/a.ts +52 -0
- package/js/assets/AGENTS.md +5 -0
- package/js/assets/base64.test.ts +14 -0
- package/js/assets/base64.ts +21 -0
- package/js/assets/build-asset.test.ts +26 -0
- package/js/assets/build-asset.ts +28 -0
- package/js/assets/create.test.ts +142 -0
- package/js/assets/create.ts +122 -0
- package/js/assets/index.test.ts +15 -0
- package/js/assets/index.ts +2 -0
- package/js/assets/types.ts +26 -0
- package/js/assets/validation.test.ts +34 -0
- package/js/assets/validation.ts +25 -0
- package/js/assets.test.ts +14 -0
- package/js/assets.ts +1 -0
- package/js/index.test.ts +26 -0
- package/js/index.ts +3 -0
- package/js/render/index.test.ts +65 -0
- package/js/render/index.ts +13 -0
- package/js/render/sharp.ts +87 -0
- package/js/render/svg.ts +68 -0
- package/js/render/types.ts +35 -0
- package/js/render/utils.ts +108 -0
- package/js/render/webgl.ts +274 -0
- package/js/sharp-optional.d.ts +16 -0
- package/js/test/helpers.ts +116 -0
- package/js/test/hikkaku-sample.test.ts +37 -0
- package/js/test/rubik-components.input-motion.test.ts +60 -0
- package/js/test/rubik-components.lists.test.ts +49 -0
- package/js/test/rubik-components.operators.test.ts +104 -0
- package/js/test/rubik-components.pen.test.ts +112 -0
- package/js/test/rubik-components.procedures-loops.test.ts +72 -0
- package/js/test/rubik-components.variables-branches.test.ts +57 -0
- package/js/test/rubik-components.visibility-entry.test.ts +31 -0
- package/js/test/test-projects.ts +598 -0
- package/js/test/variable.ts +200 -0
- package/js/test/warp.test.ts +59 -0
- package/js/vm/AGENTS.md +6 -0
- package/js/vm/README.md +183 -0
- package/js/vm/bindings.test.ts +13 -0
- package/js/vm/bindings.ts +5 -0
- package/js/vm/compare-operators.test.ts +145 -0
- package/js/vm/constants.test.ts +11 -0
- package/js/vm/constants.ts +4 -0
- package/js/vm/effect-guards.test.ts +68 -0
- package/js/vm/effect-guards.ts +44 -0
- package/js/vm/factory.test.ts +486 -0
- package/js/vm/factory.ts +615 -0
- package/js/vm/headless-vm.test.ts +131 -0
- package/js/vm/headless-vm.ts +342 -0
- package/js/vm/index.test.ts +28 -0
- package/js/vm/index.ts +5 -0
- package/js/vm/internal-types.ts +32 -0
- package/js/vm/json.test.ts +40 -0
- package/js/vm/json.ts +273 -0
- package/js/vm/normalize.test.ts +48 -0
- package/js/vm/normalize.ts +65 -0
- package/js/vm/options.test.ts +30 -0
- package/js/vm/options.ts +55 -0
- package/js/vm/pen-transparency.test.ts +115 -0
- package/js/vm/program-wasm.ts +322 -0
- package/js/vm/scheduler-render.test.ts +401 -0
- package/js/vm/scratch-assets.test.ts +136 -0
- package/js/vm/scratch-assets.ts +202 -0
- package/js/vm/types.ts +358 -0
- package/js/vm/value-guards.test.ts +25 -0
- package/js/vm/value-guards.ts +18 -0
- package/moon.mod.json +10 -0
- package/package.json +33 -0
- package/scripts/preinstall.ts +4 -0
- package/src/AGENTS.md +6 -0
- package/src/api.mbt +161 -0
- package/src/api_aot_commands.mbt +184 -0
- package/src/api_effects_json.mbt +72 -0
- package/src/api_options.mbt +60 -0
- package/src/api_program_wasm.mbt +1647 -0
- package/src/api_program_wat.mbt +2206 -0
- package/src/api_snapshot_json.mbt +44 -0
- package/src/cmd/AGENTS.md +5 -0
- package/src/cmd/main/AGENTS.md +5 -0
- package/src/cmd/main/main.mbt +29 -0
- package/src/cmd/main/moon.pkg +7 -0
- package/src/cmd/main/pkg.generated.mbti +13 -0
- package/src/json_helpers.mbt +176 -0
- package/src/moon.pkg +65 -0
- package/src/moonscratch.mbt +3 -0
- package/src/moonscratch_wbtest.mbt +40 -0
- package/src/parser_sb3.mbt +890 -0
- package/src/pkg.generated.mbti +479 -0
- package/src/runtime_eval.mbt +2844 -0
- package/src/runtime_exec.mbt +3850 -0
- package/src/runtime_render.mbt +2550 -0
- package/src/runtime_state.mbt +870 -0
- package/src/test/AGENTS.md +3 -0
- package/src/test/projects/AGENTS.md +6 -0
- package/src/test/projects/moon.pkg +4 -0
- package/src/test/projects/moonscratch_compat_test.mbt +642 -0
- package/src/test/projects/moonscratch_core_test.mbt +1332 -0
- package/src/test/projects/moonscratch_runtime_test.mbt +1087 -0
- package/src/test/projects/pkg.generated.mbti +13 -0
- package/src/test/projects/test_support.mbt +35 -0
- package/src/types_effects.mbt +20 -0
- package/src/types_error.mbt +4 -0
- package/src/types_options.mbt +31 -0
- package/src/types_runtime_structs.mbt +254 -0
- package/src/types_vm.mbt +109 -0
- package/tsconfig.json +29 -0
- package/viewer/index.ts +399 -0
- package/viewer/vite.d.ts +1 -0
- package/viewer/worker.ts +161 -0
- package/vite.config.ts +11 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MusicPlayDrumEffect,
|
|
3
|
+
MusicPlayNoteEffect,
|
|
4
|
+
TextToSpeechEffect,
|
|
5
|
+
TranslateRequestEffect,
|
|
6
|
+
VMEffect,
|
|
7
|
+
} from './types.ts'
|
|
8
|
+
import { hasNumberField, hasStringField } from './value-guards.ts'
|
|
9
|
+
|
|
10
|
+
export const isTranslateRequestEffect = (
|
|
11
|
+
effect: VMEffect,
|
|
12
|
+
): effect is TranslateRequestEffect =>
|
|
13
|
+
effect.type === 'translate_request' &&
|
|
14
|
+
hasStringField(effect, 'words') &&
|
|
15
|
+
hasStringField(effect, 'language')
|
|
16
|
+
|
|
17
|
+
export const isTextToSpeechEffect = (
|
|
18
|
+
effect: VMEffect,
|
|
19
|
+
): effect is TextToSpeechEffect =>
|
|
20
|
+
effect.type === 'text_to_speech' &&
|
|
21
|
+
hasStringField(effect, 'target') &&
|
|
22
|
+
hasStringField(effect, 'words') &&
|
|
23
|
+
hasStringField(effect, 'voice') &&
|
|
24
|
+
hasStringField(effect, 'language') &&
|
|
25
|
+
hasStringField(effect, 'waitKey')
|
|
26
|
+
|
|
27
|
+
export const isMusicPlayNoteEffect = (
|
|
28
|
+
effect: VMEffect,
|
|
29
|
+
): effect is MusicPlayNoteEffect =>
|
|
30
|
+
effect.type === 'music_play_note' &&
|
|
31
|
+
hasStringField(effect, 'target') &&
|
|
32
|
+
hasNumberField(effect, 'note') &&
|
|
33
|
+
hasNumberField(effect, 'beats') &&
|
|
34
|
+
hasNumberField(effect, 'instrument') &&
|
|
35
|
+
hasNumberField(effect, 'tempo')
|
|
36
|
+
|
|
37
|
+
export const isMusicPlayDrumEffect = (
|
|
38
|
+
effect: VMEffect,
|
|
39
|
+
): effect is MusicPlayDrumEffect =>
|
|
40
|
+
effect.type === 'music_play_drum' &&
|
|
41
|
+
hasStringField(effect, 'target') &&
|
|
42
|
+
hasNumberField(effect, 'drum') &&
|
|
43
|
+
hasNumberField(effect, 'beats') &&
|
|
44
|
+
hasNumberField(effect, 'tempo')
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from 'vite-plus/test'
|
|
2
|
+
import {
|
|
3
|
+
EXAMPLE_PROJECT,
|
|
4
|
+
getStageVariables,
|
|
5
|
+
HOST_OPCODE_FALLBACK_PROJECT,
|
|
6
|
+
stepMany,
|
|
7
|
+
TEXT_TO_SPEECH_TRANSLATE_PROJECT,
|
|
8
|
+
WASM_MATHOP_LOOP_COUNT_ID,
|
|
9
|
+
WASM_MATHOP_LOOP_PROJECT,
|
|
10
|
+
WASM_ONLY_HIKKAKU_BRANCH_ID,
|
|
11
|
+
WASM_ONLY_HIKKAKU_PROJECT,
|
|
12
|
+
WASM_ONLY_HIKKAKU_RESULT_ID,
|
|
13
|
+
} from '../test/test-projects.ts'
|
|
14
|
+
import {
|
|
15
|
+
compileProjectToWasm,
|
|
16
|
+
compileProjectToWat,
|
|
17
|
+
createHeadlessVM,
|
|
18
|
+
createHeadlessVMFromProject,
|
|
19
|
+
createHeadlessVMWithScratchAssets,
|
|
20
|
+
createProgramModule,
|
|
21
|
+
createProgramModuleFromProject,
|
|
22
|
+
createRuntime,
|
|
23
|
+
createVM,
|
|
24
|
+
createVMFromProject,
|
|
25
|
+
createVMWithScratchAssets,
|
|
26
|
+
moonscratch,
|
|
27
|
+
precompileProgramForRuntime,
|
|
28
|
+
} from './factory.ts'
|
|
29
|
+
|
|
30
|
+
const DRAW_OPCODE_PROJECT = JSON.stringify({
|
|
31
|
+
targets: [
|
|
32
|
+
{
|
|
33
|
+
isStage: true,
|
|
34
|
+
name: 'Stage',
|
|
35
|
+
variables: {},
|
|
36
|
+
lists: {},
|
|
37
|
+
blocks: {},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
isStage: false,
|
|
41
|
+
name: 'Sprite1',
|
|
42
|
+
variables: {},
|
|
43
|
+
lists: {},
|
|
44
|
+
blocks: {
|
|
45
|
+
hat: {
|
|
46
|
+
opcode: 'event_whenflagclicked',
|
|
47
|
+
next: 'pen_clear',
|
|
48
|
+
parent: null,
|
|
49
|
+
inputs: {},
|
|
50
|
+
fields: {},
|
|
51
|
+
topLevel: true,
|
|
52
|
+
},
|
|
53
|
+
pen_clear: {
|
|
54
|
+
opcode: 'pen_clear',
|
|
55
|
+
next: 'move_x',
|
|
56
|
+
parent: 'hat',
|
|
57
|
+
inputs: {},
|
|
58
|
+
fields: {},
|
|
59
|
+
topLevel: false,
|
|
60
|
+
},
|
|
61
|
+
move_x: {
|
|
62
|
+
opcode: 'motion_changexby',
|
|
63
|
+
next: 'move_y',
|
|
64
|
+
parent: 'pen_clear',
|
|
65
|
+
inputs: {
|
|
66
|
+
DX: [1, [4, 12]],
|
|
67
|
+
},
|
|
68
|
+
fields: {},
|
|
69
|
+
topLevel: false,
|
|
70
|
+
},
|
|
71
|
+
move_y: {
|
|
72
|
+
opcode: 'motion_changeyby',
|
|
73
|
+
next: null,
|
|
74
|
+
parent: 'move_x',
|
|
75
|
+
inputs: {
|
|
76
|
+
DY: [1, [4, -8]],
|
|
77
|
+
},
|
|
78
|
+
fields: {},
|
|
79
|
+
topLevel: false,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('moonscratch/js/vm/factory.ts', () => {
|
|
87
|
+
test('exports createVM aliases', () => {
|
|
88
|
+
expect(createVM).toBe(createHeadlessVM)
|
|
89
|
+
expect(createVMFromProject).toBe(createHeadlessVMFromProject)
|
|
90
|
+
expect(createVMWithScratchAssets).toBe(createHeadlessVMWithScratchAssets)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('compiles project JSON into generated WAT metadata', () => {
|
|
94
|
+
const runtime = createRuntime()
|
|
95
|
+
const compiled = compileProjectToWat({
|
|
96
|
+
projectJson: TEXT_TO_SPEECH_TRANSLATE_PROJECT,
|
|
97
|
+
})
|
|
98
|
+
expect(compiled.abiVersion).toBe(runtime.abiVersion)
|
|
99
|
+
expect(compiled.manifest.abiVersion).toBe(runtime.abiVersion)
|
|
100
|
+
expect(compiled.wat).toContain(';; moonscratch_program_v1')
|
|
101
|
+
expect(compiled.wat).toContain(
|
|
102
|
+
`;; abi_version=${String(runtime.abiVersion)}`,
|
|
103
|
+
)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('builds and loads project WASM module from generated WAT', () => {
|
|
107
|
+
const compiled = compileProjectToWasm({
|
|
108
|
+
projectJson: TEXT_TO_SPEECH_TRANSLATE_PROJECT,
|
|
109
|
+
assets: { custom_asset: { width: 1, height: 1, rgbaBase64: 'AP8A/w==' } },
|
|
110
|
+
})
|
|
111
|
+
const program = createProgramModule({
|
|
112
|
+
wasmBytes: compiled.wasmBytes,
|
|
113
|
+
manifest: compiled.manifest,
|
|
114
|
+
})
|
|
115
|
+
const payload = program.readPayload()
|
|
116
|
+
expect(payload.projectJson).toContain('"targets"')
|
|
117
|
+
expect(payload.assetsJson).toContain('custom_asset')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('builds project WASM bytes through moonbit binding by default', () => {
|
|
121
|
+
const compileWasmSpy = vi.spyOn(
|
|
122
|
+
moonscratch as unknown as {
|
|
123
|
+
vm_compile_project_to_wasm: (...args: unknown[]) => unknown
|
|
124
|
+
},
|
|
125
|
+
'vm_compile_project_to_wasm',
|
|
126
|
+
)
|
|
127
|
+
const compiled = compileProjectToWasm({
|
|
128
|
+
projectJson: TEXT_TO_SPEECH_TRANSLATE_PROJECT,
|
|
129
|
+
})
|
|
130
|
+
expect(compiled.wasmBytes.byteLength).toBeGreaterThan(8)
|
|
131
|
+
expect(compileWasmSpy).toHaveBeenCalledTimes(1)
|
|
132
|
+
compileWasmSpy.mockRestore()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('embeds AOT command payload for eligible linear green-flag scripts', () => {
|
|
136
|
+
const program = createProgramModuleFromProject({
|
|
137
|
+
projectJson: EXAMPLE_PROJECT,
|
|
138
|
+
})
|
|
139
|
+
const payload = program.readPayload()
|
|
140
|
+
expect(payload.commandsJson).toBeDefined()
|
|
141
|
+
expect(payload.commandsJson).toMatch(/"op":"set_var_(num_expr|json_const)"/)
|
|
142
|
+
expect(payload.commandsJson).toContain('"catalog"')
|
|
143
|
+
const parsed = JSON.parse(payload.commandsJson ?? '{}') as {
|
|
144
|
+
exec_mode?: string
|
|
145
|
+
full_green_flag_starts?: unknown[]
|
|
146
|
+
}
|
|
147
|
+
expect(parsed.exec_mode).toBe('linear')
|
|
148
|
+
expect(Array.isArray(parsed.full_green_flag_starts)).toBe(true)
|
|
149
|
+
expect(parsed.full_green_flag_starts?.length ?? 0).toBeGreaterThan(0)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('stores opcode catalog and host-tail commands for unsupported opcodes', () => {
|
|
153
|
+
const program = createProgramModuleFromProject({
|
|
154
|
+
projectJson: TEXT_TO_SPEECH_TRANSLATE_PROJECT,
|
|
155
|
+
})
|
|
156
|
+
const payload = program.readPayload()
|
|
157
|
+
expect(payload.commandsJson).toBeDefined()
|
|
158
|
+
expect(payload.commandsJson).toContain('"catalog"')
|
|
159
|
+
expect(payload.commandsJson).toContain('"op":"host_tail"')
|
|
160
|
+
expect(payload.commandsJson).toContain('translate_getViewerLanguage')
|
|
161
|
+
const parsed = JSON.parse(payload.commandsJson ?? '{}') as {
|
|
162
|
+
exec_mode?: string
|
|
163
|
+
full_green_flag_starts?: unknown[]
|
|
164
|
+
}
|
|
165
|
+
expect(parsed.exec_mode).toBe('linear')
|
|
166
|
+
expect(Array.isArray(parsed.full_green_flag_starts)).toBe(true)
|
|
167
|
+
expect(parsed.full_green_flag_starts?.length ?? 0).toBeGreaterThan(0)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('caches runtime precompile result per program module', () => {
|
|
171
|
+
const program = createProgramModuleFromProject({
|
|
172
|
+
projectJson: TEXT_TO_SPEECH_TRANSLATE_PROJECT,
|
|
173
|
+
})
|
|
174
|
+
const compileSpy = vi.spyOn(
|
|
175
|
+
moonscratch as { vm_compile_from_json: (...args: unknown[]) => unknown },
|
|
176
|
+
'vm_compile_from_json',
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
const first = createHeadlessVM({ program, initialNowMs: 0 })
|
|
180
|
+
const second = createHeadlessVM({ program, initialNowMs: 0 })
|
|
181
|
+
|
|
182
|
+
expect(first).toBeDefined()
|
|
183
|
+
expect(second).toBeDefined()
|
|
184
|
+
expect(compileSpy).toHaveBeenCalledTimes(1)
|
|
185
|
+
compileSpy.mockRestore()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('allows explicit runtime precompile before VM creation', () => {
|
|
189
|
+
const runtime = createRuntime()
|
|
190
|
+
const program = createProgramModuleFromProject({
|
|
191
|
+
projectJson: TEXT_TO_SPEECH_TRANSLATE_PROJECT,
|
|
192
|
+
})
|
|
193
|
+
const compileSpy = vi.spyOn(
|
|
194
|
+
moonscratch as { vm_compile_from_json: (...args: unknown[]) => unknown },
|
|
195
|
+
'vm_compile_from_json',
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
precompileProgramForRuntime({ program, runtime })
|
|
199
|
+
const vm = createHeadlessVM({ runtime, program, initialNowMs: 0 })
|
|
200
|
+
|
|
201
|
+
expect(vm).toBeDefined()
|
|
202
|
+
expect(compileSpy).toHaveBeenCalledTimes(1)
|
|
203
|
+
compileSpy.mockRestore()
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
test('passes AOT command payload to runtime when present', () => {
|
|
207
|
+
const program = createProgramModuleFromProject({
|
|
208
|
+
projectJson: EXAMPLE_PROJECT,
|
|
209
|
+
})
|
|
210
|
+
const aotSpy = vi.spyOn(
|
|
211
|
+
moonscratch as unknown as {
|
|
212
|
+
vm_set_aot_commands_json: (...args: unknown[]) => unknown
|
|
213
|
+
},
|
|
214
|
+
'vm_set_aot_commands_json',
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
const vm = createHeadlessVM({ program, initialNowMs: 0 })
|
|
218
|
+
vm.greenFlag()
|
|
219
|
+
const frame = vm.stepFrame()
|
|
220
|
+
|
|
221
|
+
expect(frame.stopReason).toBe('finished')
|
|
222
|
+
expect(getStageVariables(vm).var_score).toBe(42)
|
|
223
|
+
expect(aotSpy).toHaveBeenCalledTimes(1)
|
|
224
|
+
aotSpy.mockRestore()
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('runs linear AOT logic through program wasm on green flag', () => {
|
|
228
|
+
const program = createProgramModuleFromProject({
|
|
229
|
+
projectJson: EXAMPLE_PROJECT,
|
|
230
|
+
})
|
|
231
|
+
expect(program.hasWasmExec()).toBe(true)
|
|
232
|
+
|
|
233
|
+
const vm = createHeadlessVM({ program, initialNowMs: 0 })
|
|
234
|
+
vm.greenFlag()
|
|
235
|
+
|
|
236
|
+
expect(getStageVariables(vm).var_score).toBe(42)
|
|
237
|
+
const frame = vm.stepFrame()
|
|
238
|
+
expect(frame.stopReason).toBe('finished')
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('runs control/operator/data command graph through program wasm', () => {
|
|
242
|
+
const program = createProgramModuleFromProject({
|
|
243
|
+
projectJson: WASM_ONLY_HIKKAKU_PROJECT,
|
|
244
|
+
})
|
|
245
|
+
expect(program.hasWasmExec()).toBe(true)
|
|
246
|
+
expect(program.readPayload().commandsJson).not.toContain(
|
|
247
|
+
'"op":"host_opcode"',
|
|
248
|
+
)
|
|
249
|
+
expect(program.readPayload().commandsJson).not.toContain('"op":"host_tail"')
|
|
250
|
+
|
|
251
|
+
const execOpcodeSpy = vi.spyOn(
|
|
252
|
+
moonscratch as unknown as {
|
|
253
|
+
vm_exec_opcode_once_by_pc: (...args: unknown[]) => number
|
|
254
|
+
},
|
|
255
|
+
'vm_exec_opcode_once_by_pc',
|
|
256
|
+
)
|
|
257
|
+
const execTailSpy = vi.spyOn(
|
|
258
|
+
moonscratch as unknown as {
|
|
259
|
+
vm_exec_script_tail_by_pc: (...args: unknown[]) => number
|
|
260
|
+
},
|
|
261
|
+
'vm_exec_script_tail_by_pc',
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
const vm = createHeadlessVM({ program, initialNowMs: 0 })
|
|
265
|
+
vm.greenFlag()
|
|
266
|
+
|
|
267
|
+
const vars = getStageVariables(vm)
|
|
268
|
+
expect(vars[WASM_ONLY_HIKKAKU_RESULT_ID]).toBe(18)
|
|
269
|
+
expect(vars[WASM_ONLY_HIKKAKU_BRANCH_ID]).toBe(1)
|
|
270
|
+
expect(execOpcodeSpy).toHaveBeenCalledTimes(0)
|
|
271
|
+
expect(execTailSpy).toHaveBeenCalledTimes(0)
|
|
272
|
+
execOpcodeSpy.mockRestore()
|
|
273
|
+
execTailSpy.mockRestore()
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
test('executes repeat-until mathop expressions through wasm without host-tail fallback', () => {
|
|
277
|
+
const program = createProgramModuleFromProject({
|
|
278
|
+
projectJson: WASM_MATHOP_LOOP_PROJECT,
|
|
279
|
+
})
|
|
280
|
+
expect(program.hasWasmExec()).toBe(true)
|
|
281
|
+
expect(program.readPayload().commandsJson).not.toContain(
|
|
282
|
+
'"op":"host_opcode"',
|
|
283
|
+
)
|
|
284
|
+
expect(program.readPayload().commandsJson).not.toContain('"op":"host_tail"')
|
|
285
|
+
|
|
286
|
+
const vm = createHeadlessVM({ program, initialNowMs: 0 })
|
|
287
|
+
vm.greenFlag()
|
|
288
|
+
const frame = vm.stepFrame()
|
|
289
|
+
|
|
290
|
+
const vars = getStageVariables(vm)
|
|
291
|
+
expect(vars[WASM_MATHOP_LOOP_COUNT_ID]).toBe(3)
|
|
292
|
+
expect(frame.stopReason).toBe('finished')
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
test('executes motion/pen draw commands through draw-opcode bridge', () => {
|
|
296
|
+
const program = createProgramModuleFromProject({
|
|
297
|
+
projectJson: DRAW_OPCODE_PROJECT,
|
|
298
|
+
})
|
|
299
|
+
const commandsJson = program.readPayload().commandsJson ?? ''
|
|
300
|
+
expect(commandsJson).toContain('"op":"draw_opcode"')
|
|
301
|
+
expect(commandsJson).not.toContain('"op":"host_tail"')
|
|
302
|
+
|
|
303
|
+
const execDrawSpy = vi.spyOn(
|
|
304
|
+
moonscratch as unknown as {
|
|
305
|
+
vm_exec_draw_opcode: (...args: unknown[]) => number
|
|
306
|
+
},
|
|
307
|
+
'vm_exec_draw_opcode',
|
|
308
|
+
)
|
|
309
|
+
const execOpcodeSpy = vi.spyOn(
|
|
310
|
+
moonscratch as unknown as {
|
|
311
|
+
vm_exec_opcode_once_by_pc: (...args: unknown[]) => number
|
|
312
|
+
},
|
|
313
|
+
'vm_exec_opcode_once_by_pc',
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
const vm = createHeadlessVM({ program, initialNowMs: 0 })
|
|
317
|
+
vm.greenFlag()
|
|
318
|
+
const frame = vm.stepFrame()
|
|
319
|
+
|
|
320
|
+
const sprite = vm
|
|
321
|
+
.snapshot()
|
|
322
|
+
.targets.find((target) => target.name === 'Sprite1')
|
|
323
|
+
expect(frame.stopReason).toBe('finished')
|
|
324
|
+
expect(sprite?.x).toBe(12)
|
|
325
|
+
expect(sprite?.y).toBe(-8)
|
|
326
|
+
expect(execDrawSpy).toHaveBeenCalled()
|
|
327
|
+
expect(execOpcodeSpy).toHaveBeenCalledTimes(0)
|
|
328
|
+
|
|
329
|
+
execDrawSpy.mockRestore()
|
|
330
|
+
execOpcodeSpy.mockRestore()
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
test('delegates unsupported opcode to moonbit host during wasm exec', () => {
|
|
334
|
+
const program = createProgramModuleFromProject({
|
|
335
|
+
projectJson: HOST_OPCODE_FALLBACK_PROJECT,
|
|
336
|
+
})
|
|
337
|
+
expect(program.hasWasmExec()).toBe(true)
|
|
338
|
+
expect(program.readPayload().commandsJson).toContain('"op":"host_opcode"')
|
|
339
|
+
|
|
340
|
+
const vm = createHeadlessVM({ program, initialNowMs: 0 })
|
|
341
|
+
vm.greenFlag()
|
|
342
|
+
const frame = vm.stepFrame()
|
|
343
|
+
|
|
344
|
+
const vars = getStageVariables(vm)
|
|
345
|
+
expect(vars.var_done).toBe(1)
|
|
346
|
+
expect(frame.stopReason).toBe('finished')
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
test('creates VM from compiled program module', () => {
|
|
350
|
+
const program = createProgramModuleFromProject({
|
|
351
|
+
projectJson: TEXT_TO_SPEECH_TRANSLATE_PROJECT,
|
|
352
|
+
})
|
|
353
|
+
const vm = createHeadlessVM({
|
|
354
|
+
program,
|
|
355
|
+
initialNowMs: 0,
|
|
356
|
+
viewerLanguage: 'ja',
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
vm.greenFlag()
|
|
360
|
+
stepMany(vm, 6)
|
|
361
|
+
|
|
362
|
+
const effects = vm.takeEffects()
|
|
363
|
+
expect(effects.some((effect) => effect.type === 'text_to_speech')).toBe(
|
|
364
|
+
true,
|
|
365
|
+
)
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
test('reuses compiled program module across multiple VM instances', () => {
|
|
369
|
+
const program = createProgramModuleFromProject({
|
|
370
|
+
projectJson: TEXT_TO_SPEECH_TRANSLATE_PROJECT,
|
|
371
|
+
})
|
|
372
|
+
const first = createHeadlessVM({ program, initialNowMs: 0 })
|
|
373
|
+
const second = createHeadlessVM({ program, initialNowMs: 0 })
|
|
374
|
+
|
|
375
|
+
first.greenFlag()
|
|
376
|
+
second.greenFlag()
|
|
377
|
+
stepMany(first, 6)
|
|
378
|
+
stepMany(second, 6)
|
|
379
|
+
|
|
380
|
+
expect(getStageVariables(first)).toEqual(getStageVariables(second))
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
test('keeps VM execution state isolated even when precompiled caches are reused', () => {
|
|
384
|
+
const program = createProgramModuleFromProject({
|
|
385
|
+
projectJson: EXAMPLE_PROJECT,
|
|
386
|
+
})
|
|
387
|
+
const first = createHeadlessVM({ program, initialNowMs: 0 })
|
|
388
|
+
const second = createHeadlessVM({ program, initialNowMs: 0 })
|
|
389
|
+
|
|
390
|
+
first.greenFlag()
|
|
391
|
+
stepMany(first, 2)
|
|
392
|
+
first.renderFrame()
|
|
393
|
+
|
|
394
|
+
expect(getStageVariables(first).var_score).toBe(42)
|
|
395
|
+
expect(getStageVariables(second).var_score).toBe(0)
|
|
396
|
+
|
|
397
|
+
second.renderFrame()
|
|
398
|
+
expect(getStageVariables(second).var_score).toBe(0)
|
|
399
|
+
|
|
400
|
+
second.greenFlag()
|
|
401
|
+
stepMany(second, 2)
|
|
402
|
+
expect(getStageVariables(second).var_score).toBe(42)
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
test('normalizes viewer language and translate cache in constructor options', () => {
|
|
406
|
+
const program = createProgramModuleFromProject({
|
|
407
|
+
projectJson: TEXT_TO_SPEECH_TRANSLATE_PROJECT,
|
|
408
|
+
})
|
|
409
|
+
const vm = createHeadlessVM({
|
|
410
|
+
program,
|
|
411
|
+
viewerLanguage: ' JA ',
|
|
412
|
+
translateCache: { JA: { hello: 'こんにちは' } },
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
vm.greenFlag()
|
|
416
|
+
stepMany(vm, 6)
|
|
417
|
+
|
|
418
|
+
const effects = vm.takeEffects()
|
|
419
|
+
expect(effects).toEqual(
|
|
420
|
+
expect.arrayContaining([
|
|
421
|
+
expect.objectContaining({
|
|
422
|
+
type: 'text_to_speech',
|
|
423
|
+
waitKey: 'text2speech_done_1',
|
|
424
|
+
language: 'ja',
|
|
425
|
+
}),
|
|
426
|
+
]),
|
|
427
|
+
)
|
|
428
|
+
expect(effects.some((effect) => effect.type === 'translate_request')).toBe(
|
|
429
|
+
false,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
vm.ackTextToSpeech('text2speech_done_1')
|
|
433
|
+
stepMany(vm, 10)
|
|
434
|
+
|
|
435
|
+
const stageVars = getStageVariables(vm)
|
|
436
|
+
expect(stageVars.var_viewer).toBe('ja')
|
|
437
|
+
expect(stageVars.var_trans).toBe('こんにちは')
|
|
438
|
+
expect(stageVars.var_done).toBe(1)
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
test('loads missing costume assets from Scratch CDN', async () => {
|
|
442
|
+
const fetchAsset = vi.fn(async () => ({
|
|
443
|
+
ok: true,
|
|
444
|
+
status: 200,
|
|
445
|
+
statusText: 'OK',
|
|
446
|
+
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
|
447
|
+
}))
|
|
448
|
+
const decodeImageBytes = vi.fn(async () => ({
|
|
449
|
+
width: 1,
|
|
450
|
+
height: 1,
|
|
451
|
+
rgbaBase64: 'AP8A/w==',
|
|
452
|
+
}))
|
|
453
|
+
|
|
454
|
+
const vm = await createHeadlessVMWithScratchAssets({
|
|
455
|
+
projectJson: {
|
|
456
|
+
targets: [
|
|
457
|
+
{
|
|
458
|
+
isStage: true,
|
|
459
|
+
name: 'Stage',
|
|
460
|
+
variables: {},
|
|
461
|
+
lists: {},
|
|
462
|
+
blocks: {},
|
|
463
|
+
costumes: [
|
|
464
|
+
{
|
|
465
|
+
name: 'backdrop1',
|
|
466
|
+
assetId: 'bg_green',
|
|
467
|
+
md5ext: 'bg_green.png',
|
|
468
|
+
bitmapResolution: 1,
|
|
469
|
+
rotationCenterX: 0,
|
|
470
|
+
rotationCenterY: 0,
|
|
471
|
+
},
|
|
472
|
+
],
|
|
473
|
+
},
|
|
474
|
+
],
|
|
475
|
+
},
|
|
476
|
+
fetchAsset,
|
|
477
|
+
decodeImageBytes,
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
expect(vm).toBeDefined()
|
|
481
|
+
expect(fetchAsset).toHaveBeenCalledWith(
|
|
482
|
+
'https://cdn.scratch.mit.edu/internalapi/asset/bg_green.png/get/',
|
|
483
|
+
)
|
|
484
|
+
expect(decodeImageBytes).toHaveBeenCalledTimes(1)
|
|
485
|
+
})
|
|
486
|
+
})
|