@woosh/meep-engine 3.29.0 → 3.30.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.
Files changed (226) hide show
  1. package/build/bundle-worker-image-decoder.js +1 -1
  2. package/build/bundle-worker-terrain.js +1 -1
  3. package/package.json +1 -1
  4. package/src/core/binary/BinaryBuffer.d.ts.map +1 -1
  5. package/src/core/binary/BinaryBuffer.js +14 -2
  6. package/src/core/bvh2/bvh3/BVH.d.ts.map +1 -1
  7. package/src/core/bvh2/bvh3/BVH.js +14 -1
  8. package/src/core/cache/FrequencySketch.d.ts +3 -1
  9. package/src/core/cache/FrequencySketch.d.ts.map +1 -1
  10. package/src/core/cache/FrequencySketch.js +10 -4
  11. package/src/core/cache/wtinylfu/CacheWTinylfu.d.ts.map +1 -1
  12. package/src/core/cache/wtinylfu/CacheWTinylfu.js +24 -2
  13. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.d.ts +20 -0
  14. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.d.ts.map +1 -1
  15. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.js +69 -4
  16. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.d.ts +48 -18
  17. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.d.ts.map +1 -1
  18. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.js +262 -70
  19. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_fold_limit.d.ts +25 -0
  20. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_fold_limit.d.ts.map +1 -0
  21. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_fold_limit.js +228 -0
  22. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_would_fold.d.ts +13 -0
  23. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_would_fold.d.ts.map +1 -1
  24. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_would_fold.js +10 -128
  25. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_face_bvh.d.ts.map +1 -1
  26. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_face_bvh.js +11 -1
  27. package/src/core/graph/mn_graph_collapse_weighted_edge.js +6 -6
  28. package/src/core/math/lookup/ParameterLookupTable.d.ts.map +1 -1
  29. package/src/core/math/lookup/ParameterLookupTable.js +10 -5
  30. package/src/core/math/noise/sdnoise.d.ts.map +1 -1
  31. package/src/core/math/noise/sdnoise.js +16 -10
  32. package/src/core/model/object/ImmutableObjectPool.d.ts.map +1 -1
  33. package/src/core/model/object/ImmutableObjectPool.js +17 -7
  34. package/src/core/process/executor/ConcurrentExecutor.d.ts +1 -5
  35. package/src/core/process/executor/ConcurrentExecutor.d.ts.map +1 -1
  36. package/src/core/process/executor/ConcurrentExecutor.js +95 -1
  37. package/src/core/process/task/task_find_dependency_cycle.d.ts +17 -0
  38. package/src/core/process/task/task_find_dependency_cycle.d.ts.map +1 -0
  39. package/src/core/process/task/task_find_dependency_cycle.js +81 -0
  40. package/src/core/wfc/WFCCellChoice.d.ts +17 -0
  41. package/src/core/wfc/WFCCellChoice.d.ts.map +1 -0
  42. package/src/core/wfc/WFCCellChoice.js +36 -0
  43. package/src/core/wfc/WFCLattice.d.ts +105 -0
  44. package/src/core/wfc/WFCLattice.d.ts.map +1 -0
  45. package/src/core/wfc/WFCLattice.js +235 -0
  46. package/src/core/wfc/WFCRuleTable.d.ts +172 -0
  47. package/src/core/wfc/WFCRuleTable.d.ts.map +1 -0
  48. package/src/core/wfc/WFCRuleTable.js +334 -0
  49. package/src/core/wfc/WFCState.d.ts +273 -0
  50. package/src/core/wfc/WFCState.d.ts.map +1 -0
  51. package/src/core/wfc/WFCState.js +552 -0
  52. package/src/core/wfc/WFCStatus.d.ts +11 -0
  53. package/src/core/wfc/WFCStatus.d.ts.map +1 -0
  54. package/src/core/wfc/WFCStatus.js +28 -0
  55. package/src/core/wfc/WFCTileWeights.d.ts +48 -0
  56. package/src/core/wfc/WFCTileWeights.d.ts.map +1 -0
  57. package/src/core/wfc/WFCTileWeights.js +86 -0
  58. package/src/core/wfc/WFC_UNRESOLVED.d.ts +12 -0
  59. package/src/core/wfc/WFC_UNRESOLVED.d.ts.map +1 -0
  60. package/src/core/wfc/WFC_UNRESOLVED.js +11 -0
  61. package/src/core/wfc/wfc_estimate_bytes.d.ts +24 -0
  62. package/src/core/wfc/wfc_estimate_bytes.d.ts.map +1 -0
  63. package/src/core/wfc/wfc_estimate_bytes.js +47 -0
  64. package/src/core/wfc/wfc_rule_table_tile_components.d.ts +27 -0
  65. package/src/core/wfc/wfc_rule_table_tile_components.d.ts.map +1 -0
  66. package/src/core/wfc/wfc_rule_table_tile_components.js +80 -0
  67. package/src/core/wfc/wfc_solve.d.ts +21 -0
  68. package/src/core/wfc/wfc_solve.d.ts.map +1 -0
  69. package/src/core/wfc/wfc_solve.js +154 -0
  70. package/src/core/wfc/wfc_state_apply_boundary.d.ts +18 -0
  71. package/src/core/wfc/wfc_state_apply_boundary.d.ts.map +1 -0
  72. package/src/core/wfc/wfc_state_apply_boundary.js +61 -0
  73. package/src/core/wfc/wfc_state_assign.d.ts +11 -0
  74. package/src/core/wfc/wfc_state_assign.d.ts.map +1 -0
  75. package/src/core/wfc/wfc_state_assign.js +36 -0
  76. package/src/core/wfc/wfc_state_ban_bits.d.ts +17 -0
  77. package/src/core/wfc/wfc_state_ban_bits.d.ts.map +1 -0
  78. package/src/core/wfc/wfc_state_ban_bits.js +81 -0
  79. package/src/core/wfc/wfc_state_collapse.d.ts +12 -0
  80. package/src/core/wfc/wfc_state_collapse.d.ts.map +1 -0
  81. package/src/core/wfc/wfc_state_collapse.js +73 -0
  82. package/src/core/wfc/wfc_state_flush_dirty.d.ts +12 -0
  83. package/src/core/wfc/wfc_state_flush_dirty.d.ts.map +1 -0
  84. package/src/core/wfc/wfc_state_flush_dirty.js +39 -0
  85. package/src/core/wfc/wfc_state_propagate.d.ts +14 -0
  86. package/src/core/wfc/wfc_state_propagate.d.ts.map +1 -0
  87. package/src/core/wfc/wfc_state_propagate.js +155 -0
  88. package/src/core/wfc/wfc_state_read.d.ts +11 -0
  89. package/src/core/wfc/wfc_state_read.d.ts.map +1 -0
  90. package/src/core/wfc/wfc_state_read.js +38 -0
  91. package/src/core/wfc/wfc_state_restrict.d.ts +18 -0
  92. package/src/core/wfc/wfc_state_restrict.d.ts.map +1 -0
  93. package/src/core/wfc/wfc_state_restrict.js +39 -0
  94. package/src/core/wfc/wfc_state_set_eligible.d.ts +24 -0
  95. package/src/core/wfc/wfc_state_set_eligible.d.ts.map +1 -0
  96. package/src/core/wfc/wfc_state_set_eligible.js +49 -0
  97. package/src/core/wfc/wfc_state_undo_to.d.ts +13 -0
  98. package/src/core/wfc/wfc_state_undo_to.d.ts.map +1 -0
  99. package/src/core/wfc/wfc_state_undo_to.js +84 -0
  100. package/src/core/wfc/wfc_words_per_cell.d.ts +11 -0
  101. package/src/core/wfc/wfc_words_per_cell.d.ts.map +1 -0
  102. package/src/core/wfc/wfc_words_per_cell.js +16 -0
  103. package/src/engine/ecs/EntityComponentDataset.d.ts +8 -0
  104. package/src/engine/ecs/EntityComponentDataset.d.ts.map +1 -1
  105. package/src/engine/ecs/EntityComponentDataset.js +37 -14
  106. package/src/engine/ecs/fow/FogOfWarRevealerSystem.d.ts +8 -1
  107. package/src/engine/ecs/fow/FogOfWarRevealerSystem.d.ts.map +1 -1
  108. package/src/engine/ecs/fow/FogOfWarRevealerSystem.js +37 -2
  109. package/src/engine/ecs/guid/UUID.d.ts.map +1 -1
  110. package/src/engine/ecs/guid/UUID.js +45 -17
  111. package/src/engine/ecs/ik/OneBoneSurfaceAlignmentSolver.d.ts.map +1 -1
  112. package/src/engine/ecs/ik/OneBoneSurfaceAlignmentSolver.js +3 -1
  113. package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.d.ts.map +1 -1
  114. package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.js +366 -360
  115. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_linear.d.ts.map +1 -1
  116. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_linear.js +24 -10
  117. package/src/engine/grid/grid2transform/GridPosition2TransformSystem.d.ts.map +1 -1
  118. package/src/engine/grid/grid2transform/GridPosition2TransformSystem.js +24 -5
  119. package/src/engine/interpolation/Interpoland.d.ts +7 -1
  120. package/src/engine/interpolation/Interpoland.d.ts.map +1 -1
  121. package/src/engine/interpolation/Interpoland.js +8 -1
  122. package/src/engine/interpolation/InterpolationSystem.d.ts.map +1 -1
  123. package/src/engine/interpolation/InterpolationSystem.js +3 -0
  124. package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts +5 -3
  125. package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts.map +1 -1
  126. package/src/engine/interpolation/TransformPoseSerializationAdapter.js +14 -3
  127. package/src/engine/interpolation/pose_interpoland.d.ts.map +1 -1
  128. package/src/engine/interpolation/pose_interpoland.js +2 -0
  129. package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
  130. package/src/engine/network/orchestrator/NetworkPeer.js +15 -10
  131. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts.map +1 -1
  132. package/src/engine/physics/cloth/ecs/ClothWorld.js +17 -0
  133. package/src/engine/physics/mls-mpm/MLS_MPM.d.ts.map +1 -1
  134. package/src/engine/physics/mls-mpm/MLS_MPM.js +14 -0
  135. package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.d.ts +2 -1
  136. package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.d.ts.map +1 -1
  137. package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.js +1 -1
  138. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.d.ts +1 -0
  139. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.d.ts.map +1 -1
  140. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.d.ts +1 -0
  141. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.d.ts.map +1 -1
  142. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.d.ts +2 -1
  143. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.d.ts.map +1 -1
  144. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.js +12 -3
  145. package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClip.d.ts +1 -0
  146. package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClip.d.ts.map +1 -1
  147. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts +1 -0
  148. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts.map +1 -1
  149. package/src/engine/sound/sopra/runtime/EventInstance.d.ts.map +1 -1
  150. package/src/engine/sound/sopra/runtime/EventInstance.js +4 -1
  151. package/src/format/image/avif/av1/decode/decode_coefficients.d.ts.map +1 -1
  152. package/src/format/image/avif/av1/decode/decode_coefficients.js +248 -259
  153. package/src/format/image/avif/av1/decode/read_golomb.d.ts +21 -0
  154. package/src/format/image/avif/av1/decode/read_golomb.d.ts.map +1 -0
  155. package/src/format/image/avif/av1/decode/read_golomb.js +49 -0
  156. package/src/format/image/avif/av1/filter/loop_restoration.js +461 -451
  157. package/src/format/image/avif/av1/util/round2.d.ts +19 -2
  158. package/src/format/image/avif/av1/util/round2.d.ts.map +1 -1
  159. package/src/format/image/avif/av1/util/round2.js +51 -27
  160. package/src/format/image/avif/heif/parse_avif_file.js +24 -0
  161. package/src/shade/renderer/buffer/table/GPUTypedTable.d.ts.map +1 -1
  162. package/src/shade/renderer/buffer/table/GPUTypedTable.js +14 -0
  163. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +46 -3
  164. package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +91 -1
  165. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts +0 -13
  166. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts.map +1 -1
  167. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.js +0 -15
  168. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_group_step.js +5 -5
  169. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_DEFAULT_ATTRIBUTE_WEIGHT_RATIOS.d.ts +11 -1
  170. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_DEFAULT_ATTRIBUTE_WEIGHT_RATIOS.d.ts.map +1 -1
  171. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_DEFAULT_ATTRIBUTE_WEIGHT_RATIOS.js +11 -1
  172. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.d.ts +18 -9
  173. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.d.ts.map +1 -1
  174. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.js +20 -26
  175. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.d.ts +1 -3
  176. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.d.ts.map +1 -1
  177. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.js +2 -5
  178. package/src/shade/renderer/geometry/virtual/build/vgeo_build.js +1 -1
  179. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.d.ts.map +1 -1
  180. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.js +14 -1
  181. package/src/shade/renderer/global_illumination/probe/octahedral/GPUProbeAtlas.d.ts.map +1 -1
  182. package/src/shade/renderer/global_illumination/probe/octahedral/GPUProbeAtlas.js +12 -3
  183. package/src/shade/renderer/light/environment/rgbe/RGBE_ReadPixels_RLE.d.ts.map +1 -1
  184. package/src/shade/renderer/light/environment/rgbe/RGBE_ReadPixels_RLE.js +7 -1
  185. package/src/shade/renderer/lightmap/GPULightMap.d.ts.map +1 -1
  186. package/src/shade/renderer/lightmap/GPULightMap.js +349 -336
  187. package/src/shade/renderer/loader/gltf/load_gltf.d.ts.map +1 -1
  188. package/src/shade/renderer/loader/gltf/load_gltf.js +1137 -1128
  189. package/src/shade/renderer/particles/DESIGN.md +14 -5
  190. package/src/shade/renderer/particles/GPUParticleSystem.d.ts +25 -10
  191. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  192. package/src/shade/renderer/particles/GPUParticleSystem.js +182 -38
  193. package/src/shade/renderer/particles/graph/groups/simplex_reference.d.ts.map +1 -1
  194. package/src/shade/renderer/particles/graph/groups/simplex_reference.js +16 -10
  195. package/src/shade/renderer/particles/graph_particles.d.ts +4 -2
  196. package/src/shade/renderer/particles/graph_particles.d.ts.map +1 -1
  197. package/src/shade/renderer/particles/graph_particles.js +8 -2
  198. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.d.ts.map +1 -1
  199. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.js +8 -2
  200. package/src/shade/renderer/particles/optimizer/pass_fold_constants.d.ts.map +1 -1
  201. package/src/shade/renderer/particles/optimizer/pass_fold_constants.js +10 -4
  202. package/src/shade/renderer/particles/shaders/shader_particle_init_free_list.d.ts +31 -0
  203. package/src/shade/renderer/particles/shaders/shader_particle_init_free_list.d.ts.map +1 -0
  204. package/src/shade/renderer/particles/shaders/shader_particle_init_free_list.js +62 -0
  205. package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts +13 -0
  206. package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts.map +1 -1
  207. package/src/shade/renderer/particles/vm/ParticleVMReference.js +95 -17
  208. package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.d.ts +21 -0
  209. package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.d.ts.map +1 -0
  210. package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.js +28 -0
  211. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.d.ts.map +1 -1
  212. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.js +19 -2
  213. package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.d.ts.map +1 -1
  214. package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.js +19 -2
  215. package/src/shade/renderer/shader/type/WebGPUStruct.d.ts.map +1 -1
  216. package/src/shade/renderer/shader/type/WebGPUStruct.js +12 -0
  217. package/src/shade/renderer/texture/mipmaps/WebGPUMipmapGenerator.d.ts +8 -2
  218. package/src/shade/renderer/texture/mipmaps/WebGPUMipmapGenerator.d.ts.map +1 -1
  219. package/src/shade/renderer/texture/mipmaps/WebGPUMipmapGenerator.js +41 -6
  220. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +2383 -2356
  221. package/src/view/tooltip/DomTooltipManager.d.ts +7 -0
  222. package/src/view/tooltip/DomTooltipManager.d.ts.map +1 -1
  223. package/src/view/tooltip/DomTooltipManager.js +22 -0
  224. package/src/view/tooltip/DomTooltipObserver.d.ts.map +1 -1
  225. package/src/view/tooltip/DomTooltipObserver.js +13 -3
  226. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_CREASE_ANGLE.js +0 -45
@@ -1,2356 +1,2383 @@
1
- import { assert } from "../../../core/assert.js";
2
- import LineBuilder from "../../../core/codegen/LineBuilder.js";
3
-
4
- // =========================================================================
5
- // Type representation
6
- // =========================================================================
7
-
8
- /**
9
- * @typedef {object} WgslType
10
- * @property {'primitive' | 'vec' | 'mat' | 'array' | 'struct' | 'texture_2d' | 'sampler' | 'atomic' | 'unknown'} kind
11
- * @property {string} [name]
12
- * @property {number} [size]
13
- * @property {number} [cols]
14
- * @property {number} [rows]
15
- * @property {string} [scalar]
16
- * @property {WgslType} [element]
17
- * @property {number} [count]
18
- */
19
-
20
- const T_UNKNOWN = Object.freeze({ kind: 'unknown' });
21
- const T_I32 = Object.freeze({ kind: 'primitive', name: 'i32' });
22
-
23
- const VECTOR_ALIASES = {
24
- vec2i: { size: 2, scalar: 'i32' },
25
- vec3i: { size: 3, scalar: 'i32' },
26
- vec4i: { size: 4, scalar: 'i32' },
27
- vec2u: { size: 2, scalar: 'u32' },
28
- vec3u: { size: 3, scalar: 'u32' },
29
- vec4u: { size: 4, scalar: 'u32' },
30
- vec2f: { size: 2, scalar: 'f32' },
31
- vec3f: { size: 3, scalar: 'f32' },
32
- vec4f: { size: 4, scalar: 'f32' },
33
- vec2h: { size: 2, scalar: 'f16' },
34
- vec3h: { size: 3, scalar: 'f16' },
35
- vec4h: { size: 4, scalar: 'f16' },
36
- };
37
-
38
- const PRIMITIVE_NAMES = new Set(['i32', 'u32', 'f32', 'f16', 'bool']);
39
-
40
- const SWIZZLE_LETTERS = new Set(['x', 'y', 'z', 'w', 'r', 'g', 'b', 'a']);
41
-
42
- const SWIZZLE_INDEX = { x: 0, y: 1, z: 2, w: 3, r: 0, g: 1, b: 2, a: 3 };
43
-
44
- /**
45
- * Build a vec type alias name (e.g., {size:3, scalar:'f32'} → 'vec3f').
46
- */
47
- function vecAliasName(size, scalar) {
48
- const suffix = { i32: 'i', u32: 'u', f32: 'f', f16: 'h' }[scalar] || 'f';
49
- return `vec${size}${suffix}`;
50
- }
51
-
52
- // =========================================================================
53
- // Scope (symbol table) — tracks variable types for accurate codegen
54
- // =========================================================================
55
-
56
- class Scope {
57
- constructor(parent = null) {
58
- this.parent = parent;
59
- /** @type {Map<string, WgslType>} */
60
- this.symbols = new Map();
61
- }
62
-
63
- declare(name, type) {
64
- this.symbols.set(name, type || T_UNKNOWN);
65
- }
66
-
67
- /** @returns {WgslType} */
68
- lookup(name) {
69
- if (this.symbols.has(name)) return this.symbols.get(name);
70
- if (this.parent) return this.parent.lookup(name);
71
- return T_UNKNOWN;
72
- }
73
-
74
- push() {
75
- return new Scope(this);
76
- }
77
- }
78
-
79
- // =========================================================================
80
- // Tree-sitter helpers
81
- // =========================================================================
82
-
83
- /**
84
- * Return only the named children of a node (filters out punctuation tokens).
85
- */
86
- function namedChildren(node) {
87
- return node.namedChildren;
88
- }
89
-
90
- /**
91
- * First named child by type, or null.
92
- */
93
- function namedChildByType(node, type) {
94
- for (const c of node.namedChildren) {
95
- if (c.type === type) return c;
96
- }
97
- return null;
98
- }
99
-
100
- /**
101
- * All named children with the given type.
102
- */
103
- function namedChildrenByType(node, type) {
104
- return node.namedChildren.filter(c => c.type === type);
105
- }
106
-
107
- /**
108
- * Extract the source-text slice of `parent` that lies between the end of `left` and the start
109
- * of `right`. tree-sitter-web's `node.children` does not include unnamed tokens, so this is the
110
- * reliable way to read operator characters that sit between two named children.
111
- *
112
- * @param {Node} parent
113
- * @param {Node} left
114
- * @param {Node} right
115
- * @returns {string}
116
- */
117
- function textBetween(parent, left, right) {
118
- const left_offset = (left.endIndex - parent.startIndex);
119
- const right_offset = (right.startIndex - parent.startIndex);
120
- return parent.text.slice(left_offset, right_offset);
121
- }
122
-
123
- /**
124
- * Extract any leading source text in `parent` that precedes its first named child. Used for
125
- * unary operators (e.g. `-x`, `!cond`) where the operator is an unnamed token.
126
- *
127
- * @param {Node} parent
128
- * @returns {string}
129
- */
130
- function textBeforeFirstNamed(parent) {
131
- if (parent.namedChildCount === 0) return '';
132
- const first = parent.namedChildren[0];
133
- const offset = first.startIndex - parent.startIndex;
134
- return parent.text.slice(0, offset);
135
- }
136
-
137
- /**
138
- * Strip wrapper nodes that have a single named child of an expression-passthrough type.
139
- * The WGSL grammar has a long precedence chain: expression > relational > shift > additive > ...
140
- * When an expression is "simple" each level just wraps the next.
141
- */
142
- function unwrapExpression(node) {
143
- const PASSTHROUGH = new Set([
144
- 'expression',
145
- 'relational_expression',
146
- 'shift_expression',
147
- 'additive_expression',
148
- 'multiplicative_expression',
149
- 'unary_expression',
150
- 'singular_expression',
151
- 'primary_expression',
152
- ]);
153
- while (PASSTHROUGH.has(node.type) && node.namedChildren.length === 1) {
154
- // `unary_expression` keeps its operator as an unnamed token before the operand. If there
155
- // is one, don't unwrap — the caller needs to see the unary node so it can apply the op.
156
- if (node.type === 'unary_expression' && textBeforeFirstNamed(node).trim() !== '') {
157
- break;
158
- }
159
- node = node.namedChildren[0];
160
- }
161
- return node;
162
- }
163
-
164
- // =========================================================================
165
- // Type parsing (from `type_specifier` nodes or raw text)
166
- // =========================================================================
167
-
168
- /**
169
- * Parse a `type_specifier` tree-sitter node.
170
- * @param {Node} node
171
- * @returns {WgslType}
172
- */
173
- function parseTypeSpecifier(node) {
174
- if (node === null || node === undefined) return T_UNKNOWN;
175
- if (node.type === 'type_specifier') {
176
- return parseTypeSpecifier(node.namedChildren[0]);
177
- }
178
- if (node.type === 'template_elaborated_ident') {
179
- const ident = namedChildByType(node, 'ident');
180
- const ident_text = ident ? ident.text : node.text;
181
- const template = namedChildByType(node, 'template_list');
182
- const args = template ? extractTemplateArgs(template) : [];
183
- return classifyType(ident_text, args, template);
184
- }
185
- return T_UNKNOWN;
186
- }
187
-
188
- /**
189
- * Pull the textual template arguments from a `template_list` node, e.g. `<f32, 256>` → ['f32', '256'].
190
- */
191
- function extractTemplateArgs(template_list_node) {
192
- const out = [];
193
- const comma_list = namedChildByType(template_list_node, 'template_arg_comma_list');
194
- if (comma_list === null) return out;
195
- for (const c of comma_list.namedChildren) {
196
- if (c.type === 'template_arg_expression') {
197
- out.push(c.text.trim());
198
- }
199
- }
200
- return out;
201
- }
202
-
203
- /**
204
- * @param {string} ident
205
- * @param {string[]} args — textual template args (may be types or numbers)
206
- * @param {Node | null} template_list_node — the original node, for parsing nested array element types
207
- * @returns {WgslType}
208
- */
209
- function classifyType(ident, args, template_list_node) {
210
- if (PRIMITIVE_NAMES.has(ident)) {
211
- return { kind: 'primitive', name: ident };
212
- }
213
- if (VECTOR_ALIASES.hasOwnProperty(ident)) {
214
- const a = VECTOR_ALIASES[ident];
215
- return { kind: 'vec', size: a.size, scalar: a.scalar };
216
- }
217
- if (ident === 'vec2' || ident === 'vec3' || ident === 'vec4') {
218
- const size = parseInt(ident.slice(3));
219
- const scalar = args.length > 0 ? args[0] : 'f32';
220
- return { kind: 'vec', size, scalar };
221
- }
222
- {
223
- const m = ident.match(/^mat(\d)x(\d)([ifh]?)$/);
224
- if (m !== null) {
225
- const cols = parseInt(m[1]);
226
- const rows = parseInt(m[2]);
227
- const suffix = m[3];
228
- let scalar;
229
- if (suffix === 'f') scalar = 'f32';
230
- else if (suffix === 'i') scalar = 'i32';
231
- else if (suffix === 'h') scalar = 'f16';
232
- else scalar = (args.length > 0 ? args[0] : 'f32');
233
- return { kind: 'mat', cols, rows, scalar };
234
- }
235
- }
236
- if (ident === 'array') {
237
- // template arg 0 is the element type (sub-template), arg 1 is the count
238
- let element = T_UNKNOWN;
239
- let count = -1;
240
- if (template_list_node !== null) {
241
- const comma_list = namedChildByType(template_list_node, 'template_arg_comma_list');
242
- if (comma_list !== null) {
243
- const arg_nodes = namedChildrenByType(comma_list, 'template_arg_expression');
244
- if (arg_nodes.length > 0) {
245
- // Walk down to find the inner type ident
246
- const inner_expr = unwrapExpression(arg_nodes[0].namedChildren[0]);
247
- element = parseTypeSpecifier(inner_expr);
248
- }
249
- if (arg_nodes.length > 1) {
250
- const c = parseInt(arg_nodes[1].text.trim());
251
- if (!isNaN(c)) count = c;
252
- }
253
- }
254
- }
255
- return { kind: 'array', element, count };
256
- }
257
- if (ident === 'atomic') {
258
- const inner = args.length > 0 ? args[0] : 'i32';
259
- return { kind: 'atomic', scalar: inner };
260
- }
261
- // Texture types - 1D / 2D / 3D, sampled and storage variants. We
262
- // collapse all of them to a single kind because the intrinsics in
263
- // WGSLLanguageCore dispatch on the bound CPUBitmapData's
264
- // `dimensionality`, not on the static WGSL type. (The 'texture_2d'
265
- // kind label is preserved for backward compatibility with any
266
- // downstream consumer that may grep for it.)
267
- if (
268
- ident === 'texture_2d' || ident === 'texture_storage_2d'
269
- || ident === 'texture_1d' || ident === 'texture_storage_1d'
270
- || ident === 'texture_3d' || ident === 'texture_storage_3d'
271
- || ident === 'texture_2d_array'
272
- ) {
273
- const scalar = args.length > 0 ? args[0] : 'f32';
274
- return { kind: 'texture_2d', scalar };
275
- }
276
- if (ident === 'sampler') {
277
- return { kind: 'sampler' };
278
- }
279
- if (ident === 'ptr') {
280
- // Pointer — peel off the address space and read the inner type.
281
- if (template_list_node !== null) {
282
- const comma_list = namedChildByType(template_list_node, 'template_arg_comma_list');
283
- if (comma_list !== null) {
284
- const arg_nodes = namedChildrenByType(comma_list, 'template_arg_expression');
285
- if (arg_nodes.length >= 2) {
286
- const inner_expr = unwrapExpression(arg_nodes[1].namedChildren[0]);
287
- return parseTypeSpecifier(inner_expr);
288
- }
289
- }
290
- }
291
- return T_UNKNOWN;
292
- }
293
- // Otherwise: assume it's a struct name.
294
- return { kind: 'struct', name: ident };
295
- }
296
-
297
- // =========================================================================
298
- // Literal handling — strip WGSL type suffixes for JS
299
- // =========================================================================
300
-
301
- /**
302
- * Convert a WGSL literal token (e.g. `7u`, `1.0f`, `true`) to its JS equivalent.
303
- *
304
- * Stripping the type suffix is shape-sensitive: integer literals only carry `u`/`i`, and float
305
- * literals only carry `f`/`h`. Crucially, a hex int like `0x0300f00f` ends in a hex digit `f`
306
- * that must NOT be treated as a suffix — only an `f` after a decimal-format float counts.
307
- */
308
- function literalToJs(text) {
309
- const t = text.trim();
310
- if (t === 'true' || t === 'false') return t;
311
-
312
- // Hex int literals: `0xNN[ui]?`. Only `u`/`i` are real suffixes here; trailing `f`/`h` are
313
- // hex digits.
314
- if (/^0[xX]/.test(t)) {
315
- return t.replace(/[ui]$/, '');
316
- }
317
-
318
- // Decimal int / float. Float forms have a `.`, `e`/`E`, or end in `f`/`h`. Int forms don't.
319
- if (/[.eE]/.test(t)) {
320
- return t.replace(/[fh]$/, '');
321
- }
322
-
323
- // Plain integer (no `.`, no exponent): strip `u`/`i`/`f`/`h` (the latter two appear only on
324
- // forms like `1f` which WGSL allows as a float written without a decimal point).
325
- return t.replace(/[uifh]$/, '');
326
- }
327
-
328
- /**
329
- * The tracked type of a literal token, as far as its own spelling settles it.
330
- *
331
- * Only an explicit suffix is honoured. A bare `2043` is WGSL's AbstractInt, which takes its
332
- * concrete type from context this compiler does not track, so it stays unknown rather than being
333
- * guessed at.
334
- *
335
- * The suffix matters because operator selection reads these types. `index / 2043u` is truncating
336
- * integer division in WGSL and plain division in JavaScript, and the two differ for every pair
337
- * where the divisor does not divide the dividend — which is what a paged table's
338
- * `index / elements_per_page` is. Left untyped, the generated code returned a fractional page
339
- * index, indexed the page-address table with it, and read `undefined` back as zero: every record
340
- * past the first page slot came out of the emulator zero-filled, with nothing to say it had.
341
- *
342
- * @param {string} text
343
- * @returns {WgslType}
344
- */
345
- function literalType(text) {
346
- const t = text.trim();
347
-
348
- if (t === 'true' || t === 'false') {
349
- return { kind: 'primitive', name: 'bool' };
350
- }
351
-
352
- if (/[ui]$/.test(t)) {
353
- return { kind: 'primitive', name: t.endsWith('u') ? 'u32' : 'i32' };
354
- }
355
-
356
- // `f`/`h` are only suffixes on a decimal-format literal; on a hex int they are digits.
357
- if (!/^0[xX]/.test(t) && /[fh]$/.test(t)) {
358
- return { kind: 'primitive', name: t.endsWith('f') ? 'f32' : 'f16' };
359
- }
360
-
361
- if (/[.eE]/.test(t) && !/^0[xX]/.test(t)) {
362
- return { kind: 'primitive', name: 'f32' };
363
- }
364
-
365
- return T_UNKNOWN;
366
- }
367
-
368
- // =========================================================================
369
- // Operator handling
370
- // =========================================================================
371
-
372
- /**
373
- * Map a WGSL binary operator to a `wgsl.*` runtime helper or to a raw JS operator.
374
- * Arithmetic must go through wgsl.* so vectors are handled element-wise.
375
- */
376
- const ARITHMETIC_OP = {
377
- '+': 'add',
378
- '-': 'sub',
379
- '*': 'mul',
380
- '/': 'div',
381
- '%': 'rem',
382
- };
383
-
384
- const RAW_BINARY_OPS = new Set(['<', '>', '<=', '>=', '==', '!=', '&&', '||', '&', '|', '^', '<<', '>>']);
385
-
386
- /**
387
- * Arithmetic operators that wrap at 32 bits when WGSL types the result as an integer. Mapped to the
388
- * helper-name prefix; the scalar type (`u32` / `i32`) is appended at the call site.
389
- */
390
- const INTEGER_WRAPPING_OP = {
391
- '+': 'add',
392
- '-': 'sub',
393
- '*': 'mul',
394
- };
395
-
396
- // =========================================================================
397
- // Compiler context — central state during compilation
398
- // =========================================================================
399
-
400
- class Compiler {
401
- constructor() {
402
- /** @type {LineBuilder} */
403
- this.out = new LineBuilder();
404
-
405
- /** @type {Scope} */
406
- this.scope = new Scope();
407
-
408
- /** @type {Map<string, { name: string, fields: Array<{ name: string, type: WgslType }> }>} */
409
- this.structs = new Map();
410
-
411
- /** @type {Map<string, { name: string, returnType: WgslType, params: Array<{ name: string, type: WgslType }> }>} */
412
- this.functions = new Map();
413
-
414
- /** @type {Array<{ name: string, type: WgslType, group: number, binding: number, addressSpace: string | null, accessMode: string | null }>} */
415
- this.globalBindings = [];
416
-
417
- /** @type {Array<{ name: string, type: WgslType }>} */
418
- this.workgroupVars = [];
419
-
420
- /**
421
- * Module-scope `var<private>` and `const` declarations, in declaration order.
422
- * Each entry is `{ name, kind: 'let' | 'const', code, init }` — `code` is the JS line
423
- * emitted verbatim; `name` and `kind` are also tracked so the returned module object can
424
- * expose them as getter/setter (or read-only) properties for testing, and `init` is the
425
- * initializer expression on its own, which the private-state reset re-emits for the `let`
426
- * ones (a `const` cannot be reassigned, and no invocation can disturb it).
427
- * @type {Array<{ name: string, kind: 'let' | 'const', code: string, init: string }>}
428
- */
429
- this.privateGlobals = [];
430
-
431
- /** @type {{ name: string, params: Array<{ name: string, type: WgslType, builtin: string | null }>, workgroup_size: number[] } | null} */
432
- this.entry = null;
433
-
434
- /**
435
- * Label of the enclosing `loop` body block when that loop has a `continuing`
436
- * block: WGSL `continue` must transfer control INTO the continuing block, so it
437
- * is emitted as `break <label>` out of the labeled body block. `null` when the
438
- * nearest enclosing loop has no continuing block (plain JS `continue` suffices).
439
- * Loop emitters save/clear/restore this around nested loop bodies.
440
- * @type {string | null}
441
- */
442
- this.continue_label = null;
443
-
444
- /**
445
- * Monotonic counter used to generate unique loop-body labels.
446
- * @type {number}
447
- */
448
- this.loop_label_counter = 0;
449
- }
450
-
451
- error(node, message) {
452
- throw new Error(`WGSL compiler: ${message} (at ${node ? node.type : '<unknown>'} : ${node ? JSON.stringify(node.text.slice(0, 60)) : ''})`);
453
- }
454
- }
455
-
456
- // =========================================================================
457
- // Top-level compile entry point
458
- // =========================================================================
459
-
460
- export class WGSLJavaScriptCompiler {
461
-
462
- /**
463
- * Compile a tree-sitter WGSL parse tree to a JS source string.
464
- *
465
- * The output is a JavaScript expression: a function `factory(wgsl)` that returns
466
- * a shader module object. The shape depends on what the WGSL declared:
467
- *
468
- * - **Always present:** every top-level user function (e.g. `module.random()`),
469
- * every struct class (e.g. `module.Foo`), a getter/setter property for each
470
- * `var<private>` and getter for each module-scope `const`, and `reset_private()`,
471
- * which returns every `var<private>` to its start-of-invocation value.
472
- * - **Compute-shader fields (only when a `@compute` entry is present):**
473
- * ```
474
- * {
475
- * entry_name: string,
476
- * workgroup_size: [x, y, z],
477
- * param_names: string[],
478
- * entry: (bindings, workgroup, ...args) => void,
479
- * create_workgroup_state: () => object,
480
- * }
481
- * ```
482
- *
483
- * Without a `@compute` entry the result is a "library" module — useful for testing
484
- * pure WGSL helper code chunks (hashes, packing, math) one function at a time.
485
- *
486
- * @param {Tree} ast — tree-sitter WGSL AST (returned by `WGSLParser.parse`)
487
- * @returns {string}
488
- */
489
- compile(ast) {
490
- const c = new Compiler();
491
- compileTranslationUnit(c, ast.rootNode);
492
- return c.out.build();
493
- }
494
- }
495
-
496
- // =========================================================================
497
- // Translation unit (root)
498
- // =========================================================================
499
-
500
- /**
501
- * @param {Compiler} c
502
- * @param {Node} root
503
- */
504
- function compileTranslationUnit(c, root) {
505
- assert.equal(root.type, 'translation_unit', 'root must be translation_unit');
506
-
507
- // First pass — collect top-level decls so forward references and global scope resolve.
508
- for (const node of root.namedChildren) {
509
- switch (node.type) {
510
- case 'struct_decl':
511
- collectStruct(c, node);
512
- break;
513
- case 'function_decl':
514
- collectFunction(c, node);
515
- break;
516
- case 'global_variable_decl':
517
- collectGlobalVariable(c, node);
518
- break;
519
- case 'global_value_decl':
520
- collectGlobalValue(c, node);
521
- break;
522
- }
523
- }
524
-
525
- // Emit the wrapper: `(wgsl) => { ... }`
526
- c.out.add('(wgsl) => {');
527
- c.out.indent();
528
-
529
- // 1) Struct classes — must be defined before any user fn that references them or before
530
- // the workgroup state factory (which may need to construct struct instances).
531
- for (const node of root.namedChildren) {
532
- if (node.type === 'struct_decl') emitStruct(c, node);
533
- }
534
-
535
- // 2) Module-scope `let` slots for bindings and workgroup vars. The entry wrapper assigns
536
- // these on each dispatch so that user functions see them via their enclosing closure
537
- // — WGSL functions can read globals freely.
538
- for (const b of c.globalBindings) {
539
- c.out.add(`let ${b.name} = undefined;`);
540
- }
541
- for (const wv of c.workgroupVars) {
542
- c.out.add(`let ${wv.name} = undefined;`);
543
- }
544
-
545
- // 3) Module-scope `var<private>` and `const` declarations.
546
- for (const g of c.privateGlobals) {
547
- c.out.add(g.code);
548
- }
549
-
550
- // 4) User-defined functions (including the @compute entry, if any).
551
- for (const node of root.namedChildren) {
552
- if (node.type === 'function_decl') emitFunction(c, node);
553
- }
554
-
555
- // 5) The private-state reset. Emitted for every module, library ones included: a
556
- // `var<private>` is per-invocation state on the GPU, and the JS `let` above outlives the
557
- // dispatch that wrote it, so something has to put the initial values back.
558
- emitPrivateStateReset(c);
559
-
560
- // 6) Entry-stage support code: the dispatch wrapper for any entry, plus a workgroup-state
561
- // factory for @compute entries (fragment shaders have no var<workgroup>). Skipped entirely
562
- // in library mode (no entry attribute) so the resulting module is just a bag of pure
563
- // functions and globals.
564
- if (c.entry !== null) {
565
- if (c.entry.stage === 'compute') {
566
- emitWorkgroupStateFactory(c);
567
- }
568
- emitEntryWrapper(c);
569
- }
570
-
571
- // 7) Build and return the module object.
572
- emitModuleReturn(c);
573
-
574
- c.out.dedent();
575
- c.out.add('}');
576
- }
577
-
578
- /**
579
- * Emit the `return { ... }` block of the factory. Every top-level user function and struct
580
- * class is exposed by name; module-scope `var<private>` slots become getter/setter properties
581
- * (so tests can inspect or seed RNG state etc.), and module-scope `const`s become read-only
582
- * getters. `reset_private` is always included. When the WGSL had a `@compute` entry, the dispatch
583
- * trio (`entry_name`, `workgroup_size`, `param_names`, `entry`, `create_workgroup_state`) is
584
- * included too.
585
- */
586
- function emitModuleReturn(c) {
587
- c.out.add('const __module__ = {};');
588
-
589
- for (const fn_name of c.functions.keys()) {
590
- c.out.add(`__module__.${fn_name} = ${fn_name};`);
591
- }
592
- for (const struct_name of c.structs.keys()) {
593
- c.out.add(`__module__.${struct_name} = ${struct_name};`);
594
- }
595
-
596
- for (const g of c.privateGlobals) {
597
- if (g.kind === 'let') {
598
- c.out.add(`Object.defineProperty(__module__, ${JSON.stringify(g.name)}, { get: () => ${g.name}, set: (v) => { ${g.name} = v; } });`);
599
- } else {
600
- c.out.add(`Object.defineProperty(__module__, ${JSON.stringify(g.name)}, { get: () => ${g.name} });`);
601
- }
602
- }
603
-
604
- c.out.add('__module__.reset_private = __reset_private__;');
605
-
606
- if (c.entry !== null) {
607
- c.out.add(`__module__.entry_name = ${JSON.stringify(c.entry.name)};`);
608
- c.out.add(`__module__.stage = ${JSON.stringify(c.entry.stage)};`);
609
- c.out.add(`__module__.param_names = [${c.entry.params.map(p => JSON.stringify(p.name)).join(', ')}];`);
610
- c.out.add(`__module__.entry = __entry__;`);
611
- if (c.entry.stage === 'compute') {
612
- c.out.add(`__module__.workgroup_size = [${c.entry.workgroup_size.join(', ')}];`);
613
- c.out.add(`__module__.create_workgroup_state = __create_workgroup_state__;`);
614
- }
615
- }
616
-
617
- c.out.add('return __module__;');
618
- }
619
-
620
- // =========================================================================
621
- // Struct collection + emission
622
- // =========================================================================
623
-
624
- /**
625
- * @param {Compiler} c
626
- * @param {Node} node
627
- */
628
- function collectStruct(c, node) {
629
- const ident = namedChildByType(node, 'ident');
630
- const name = ident.text;
631
-
632
- const body = namedChildByType(node, 'struct_body_decl');
633
- const members = namedChildrenByType(body, 'struct_member');
634
-
635
- /** @type {Array<{ name: string, type: WgslType }>} */
636
- const fields = [];
637
-
638
- for (const m of members) {
639
- const member_ident = namedChildByType(m, 'member_ident');
640
- const type_spec = namedChildByType(m, 'type_specifier');
641
- fields.push({
642
- name: member_ident.text,
643
- type: parseTypeSpecifier(type_spec),
644
- });
645
- }
646
-
647
- c.structs.set(name, { name, fields });
648
- }
649
-
650
- /**
651
- * @param {Compiler} c
652
- * @param {Node} node
653
- */
654
- function emitStruct(c, node) {
655
- const ident = namedChildByType(node, 'ident');
656
- const name = ident.text;
657
- const info = c.structs.get(name);
658
-
659
- c.out.add(`class ${name} {`);
660
- c.out.indent();
661
- c.out.add('constructor() {');
662
- c.out.indent();
663
- for (const f of info.fields) {
664
- c.out.add(`this.${f.name} = ${defaultValueForType(f.type)};`);
665
- }
666
- c.out.dedent();
667
- c.out.add('}');
668
- c.out.dedent();
669
- c.out.add('}');
670
- }
671
-
672
- /**
673
- * Initialiser expression for a fresh value of the given type.
674
- * @param {WgslType} type
675
- * @returns {string}
676
- */
677
- function defaultValueForType(type) {
678
- if (type === undefined || type === null) return 'undefined';
679
- switch (type.kind) {
680
- case 'primitive':
681
- return type.name === 'bool' ? 'false' : '0';
682
- case 'vec':
683
- return `wgsl.${vecAliasName(type.size, type.scalar)}()`;
684
- case 'mat':
685
- return `new Float32Array(${type.cols * type.rows})`;
686
- case 'array': {
687
- if (type.count > 0) {
688
- if (type.element.kind === 'primitive') {
689
- const ctor = arrayCtorForScalar(type.element.name);
690
- return `new ${ctor}(${type.count})`;
691
- }
692
- // For non-primitive elements (vec / mat / struct / nested array) build the
693
- // array eagerly so `out[i]` is a fresh, mutable element rather than `undefined`.
694
- return `Array.from({ length: ${type.count} }, () => ${defaultValueForType(type.element)})`;
695
- }
696
- return '[]';
697
- }
698
- case 'struct':
699
- return `new ${type.name}()`;
700
- case 'atomic':
701
- return '{ value: 0 }';
702
- default:
703
- return 'undefined';
704
- }
705
- }
706
-
707
- function arrayCtorForScalar(name) {
708
- switch (name) {
709
- case 'f32':
710
- return 'Float32Array';
711
- case 'f16':
712
- return 'Float16Array';
713
- case 'i32':
714
- return 'Int32Array';
715
- case 'u32':
716
- return 'Uint32Array';
717
- default:
718
- return 'Array';
719
- }
720
- }
721
-
722
- // =========================================================================
723
- // Function collection + emission
724
- // =========================================================================
725
-
726
- /**
727
- * @param {Compiler} c
728
- * @param {Node} node
729
- */
730
- function collectFunction(c, node) {
731
- const header = namedChildByType(node, 'function_header');
732
- const ident = namedChildByType(header, 'ident');
733
- const name = ident.text;
734
-
735
- const param_list_node = namedChildByType(header, 'param_list');
736
- /** @type {Array<{ name: string, type: WgslType, builtin: string | null }>} */
737
- const params = [];
738
- if (param_list_node !== null) {
739
- const param_nodes = namedChildrenByType(param_list_node, 'param');
740
- for (const p of param_nodes) {
741
- const p_ident = namedChildByType(p, 'ident');
742
- const p_type = namedChildByType(p, 'type_specifier');
743
- const builtin = extractBuiltin(p);
744
- params.push({
745
- name: p_ident.text,
746
- type: parseTypeSpecifier(p_type),
747
- builtin,
748
- });
749
- }
750
- }
751
-
752
- // Optional return type — `template_elaborated_ident` directly under the header.
753
- let returnType = T_UNKNOWN;
754
- for (const child of header.namedChildren) {
755
- if (child.type === 'template_elaborated_ident') {
756
- returnType = parseTypeSpecifier(child);
757
- }
758
- }
759
-
760
- c.functions.set(name, { name, returnType, params: params.map(p => ({ name: p.name, type: p.type })) });
761
-
762
- // Detect the entry attribute (@compute, @fragment, or @vertex) and capture stage-specific
763
- // metadata. The emulator supports compute and fragment entries; vertex is rejected because
764
- // ImageShader testing in the emulator skips the vertex phase intentionally.
765
- const attrs = namedChildrenByType(node, 'attribute');
766
- let stage = null;
767
- let workgroup_size = [1, 1, 1];
768
- for (const a of attrs) {
769
- const inner = a.namedChildren[0];
770
- if (inner.type === 'compute_attr') {
771
- stage = 'compute';
772
- } else if (inner.type === 'fragment_attr') {
773
- stage = 'fragment';
774
- } else if (inner.type === 'vertex_attr') {
775
- throw new Error(`WGSL compiler: @vertex stage is not supported by the emulator (function '${name}'). For ImageShader testing the vertex phase is skipped — invoke the @fragment entry directly.`);
776
- } else if (inner.type === 'workgroup_size_attr') {
777
- const sizes = namedChildrenByType(inner, 'expression').map(e => parseInt(e.text.trim()) | 0);
778
- for (let i = 0; i < sizes.length && i < 3; i++) {
779
- workgroup_size[i] = sizes[i];
780
- }
781
- }
782
- }
783
-
784
- if (stage !== null) {
785
- if (c.entry !== null) {
786
- throw new Error(`WGSL compiler: multiple @${stage} entry functions found ('${c.entry.name}' and '${name}')`);
787
- }
788
- c.entry = { name, params, stage, workgroup_size };
789
- }
790
- }
791
-
792
- /**
793
- * Pull the `@builtin(name)` attribute name from a function param node, if present.
794
- */
795
- function extractBuiltin(param_node) {
796
- for (const a of namedChildrenByType(param_node, 'attribute')) {
797
- const inner = a.namedChildren[0];
798
- if (inner.type === 'builtin_attr') {
799
- const name_node = namedChildByType(inner, 'builtin_value_name');
800
- if (name_node !== null) return name_node.text;
801
- }
802
- }
803
- return null;
804
- }
805
-
806
- /**
807
- * @param {Compiler} c
808
- * @param {Node} node
809
- */
810
- function emitFunction(c, node) {
811
- const header = namedChildByType(node, 'function_header');
812
- const ident = namedChildByType(header, 'ident');
813
- const name = ident.text;
814
- const info = c.functions.get(name);
815
-
816
- const param_list_node = namedChildByType(header, 'param_list');
817
- const param_nodes = param_list_node !== null ? namedChildrenByType(param_list_node, 'param') : [];
818
-
819
- const param_names = param_nodes.map(p => namedChildByType(p, 'ident').text);
820
-
821
- c.out.add(`function ${name}(${param_names.join(', ')}) {`);
822
- c.out.indent();
823
-
824
- c.scope = c.scope.push();
825
- for (let i = 0; i < param_nodes.length; i++) {
826
- c.scope.declare(param_names[i], info.params[i].type);
827
- }
828
-
829
- const body = namedChildByType(node, 'compound_statement');
830
- emitCompoundStatementBody(c, body);
831
-
832
- c.scope = c.scope.parent;
833
-
834
- c.out.dedent();
835
- c.out.add('}');
836
- }
837
-
838
- // =========================================================================
839
- // Globals — bindings, workgroup vars, private/const
840
- // =========================================================================
841
-
842
- /**
843
- * Inspect a `global_variable_decl` and record its metadata.
844
- * @param {Compiler} c
845
- * @param {Node} node
846
- */
847
- function collectGlobalVariable(c, node) {
848
- const var_decl = namedChildByType(node, 'variable_decl');
849
- if (var_decl === null) {
850
- collectGlobalValue(c, node);
851
- return;
852
- }
853
-
854
- const template = namedChildByType(var_decl, 'template_list');
855
- const opt_ident = namedChildByType(var_decl, 'optionally_typed_ident');
856
- const var_ident = namedChildByType(opt_ident, 'ident');
857
- const type_spec = namedChildByType(opt_ident, 'type_specifier');
858
- const name = var_ident.text;
859
- const type = parseTypeSpecifier(type_spec);
860
-
861
- let address_space = null;
862
- let access_mode = null;
863
- if (template !== null) {
864
- const args = extractTemplateArgs(template);
865
- address_space = args[0] || null;
866
- access_mode = args[1] || null;
867
- }
868
-
869
- let group = null;
870
- let binding = null;
871
- for (const a of namedChildrenByType(node, 'attribute')) {
872
- const inner = a.namedChildren[0];
873
- if (inner.type === 'group_attr') {
874
- const expr = namedChildByType(inner, 'expression');
875
- group = parseInt(expr.text.trim());
876
- } else if (inner.type === 'binding_attr') {
877
- const expr = namedChildByType(inner, 'expression');
878
- binding = parseInt(expr.text.trim());
879
- }
880
- }
881
-
882
- c.scope.declare(name, type);
883
-
884
- // `var<immediate>` carries neither attribute — immediate data is part of the shader interface
885
- // but is not a bound resource, so WGSL gives it no @group/@binding. It still arrives from
886
- // outside the shader, so it is a binding as far as the emulator is concerned, and binding here
887
- // is what keeps it out of the `privateGlobals` branch below. Falling through to that branch
888
- // would default-initialise it and every spec reading it would quietly see zeroes.
889
- const is_immediate = address_space === 'immediate';
890
-
891
- if (is_immediate || (group !== null && binding !== null)) {
892
- c.globalBindings.push({
893
- name, type, group, binding, addressSpace: address_space, accessMode: access_mode,
894
- });
895
- return;
896
- }
897
-
898
- if (address_space === 'workgroup') {
899
- c.workgroupVars.push({ name, type });
900
- return;
901
- }
902
-
903
- // Private / module-scope `var<private>`. If the WGSL has an explicit initializer
904
- // (e.g. `var<private> rnd_state: u32 = 2891336453u`), use it; otherwise fall back
905
- // to the type's default value.
906
- const initializer_node = namedChildByType(node, 'expression');
907
- const init_code = initializer_node !== null
908
- ? compileExpression(c, initializer_node).code
909
- : defaultValueForType(type);
910
- c.privateGlobals.push({
911
- name,
912
- kind: 'let',
913
- code: `let ${name} = ${init_code};`,
914
- init: init_code,
915
- });
916
- }
917
-
918
- /**
919
- * Inspect a `global_value_decl` (`const x = …;` / `let x = …;` at module scope) and record it.
920
- * @param {Compiler} c
921
- * @param {Node} node
922
- */
923
- function collectGlobalValue(c, node) {
924
- let ident = null;
925
- let value_node = null;
926
- let type_node = null;
927
- for (const child of node.namedChildren) {
928
- if (child.type === 'optionally_typed_ident') {
929
- ident = namedChildByType(child, 'ident');
930
- type_node = namedChildByType(child, 'type_specifier');
931
- } else if (child.type === 'expression') {
932
- value_node = child;
933
- }
934
- }
935
- if (ident === null || value_node === null) return;
936
- const name = ident.text;
937
- let type = type_node !== null ? parseTypeSpecifier(type_node) : T_UNKNOWN;
938
-
939
- const expr = compileExpression(c, value_node);
940
-
941
- // Infer the type from the RHS when there's no explicit annotation (mirrors the local
942
- // `let`/`var` path). Without this a `const M = mat3x3<f32>(...)` is left T_UNKNOWN, so a
943
- // later `M * v` can't tell M is a matrix and emits a vector multiply → length mismatch.
944
- if (type === T_UNKNOWN || type.kind === 'unknown') {
945
- type = expr.type;
946
- }
947
-
948
- c.scope.declare(name, type);
949
- c.privateGlobals.push({
950
- name,
951
- kind: 'const',
952
- code: `const ${name} = ${expr.code};`,
953
- init: expr.code,
954
- });
955
- }
956
-
957
- // =========================================================================
958
- // Workgroup state factory + entry wrapper
959
- // =========================================================================
960
-
961
- /**
962
- * Emit `__reset_private__()`, which restores every module-scope `var<private>` to the value it
963
- * would have at the start of a WGSL invocation — its declared initializer, or the type's zero
964
- * value when it was declared without one.
965
- *
966
- * This is the private-address-space counterpart of {@link emitWorkgroupStateFactory}. The
967
- * generated module keeps its private slots in `let` bindings that live as long as the module, so
968
- * without this a second dispatch would start from whatever the first one left behind — state the
969
- * GPU never carries.
970
- *
971
- * Module-scope `const`s are skipped: they are immutable in the generated JS and no invocation can
972
- * disturb them.
973
- *
974
- * @param {Compiler} c
975
- */
976
- function emitPrivateStateReset(c) {
977
- c.out.add('function __reset_private__() {');
978
- c.out.indent();
979
- for (const g of c.privateGlobals) {
980
- if (g.kind !== 'let') continue;
981
- c.out.add(`${g.name} = ${g.init};`);
982
- }
983
- c.out.dedent();
984
- c.out.add('}');
985
- }
986
-
987
- function emitWorkgroupStateFactory(c) {
988
- c.out.add('function __create_workgroup_state__() {');
989
- c.out.indent();
990
- c.out.add('return {');
991
- c.out.indent();
992
- for (const wv of c.workgroupVars) {
993
- c.out.add(`${wv.name}: ${defaultValueForType(wv.type)},`);
994
- }
995
- c.out.dedent();
996
- c.out.add('};');
997
- c.out.dedent();
998
- c.out.add('}');
999
- }
1000
-
1001
- function emitEntryWrapper(c) {
1002
- const param_names = c.entry.params.map(p => p.name);
1003
- const is_compute = c.entry.stage === 'compute';
1004
- // Compute entries take a per-dispatch workgroup-state object; fragment entries don't have
1005
- // var<workgroup> at all, so the wrapper signature drops it.
1006
- const fixed_args = is_compute ? '__bindings__, __workgroup__' : '__bindings__';
1007
- const all_args = param_names.length > 0 ? `${fixed_args}, ${param_names.join(', ')}` : fixed_args;
1008
-
1009
- c.out.add(`function __entry__(${all_args}) {`);
1010
- c.out.indent();
1011
-
1012
- // Refresh each module-scope binding/workgroup slot before invoking the user's main.
1013
- // User functions read these via their enclosing closure, so this assignment makes the
1014
- // current dispatch's bindings visible to every helper they may call.
1015
- for (const b of c.globalBindings) {
1016
- c.out.add(`${b.name} = __bindings__.${b.name};`);
1017
- }
1018
- if (is_compute) {
1019
- for (const wv of c.workgroupVars) {
1020
- c.out.add(`${wv.name} = __workgroup__.${wv.name};`);
1021
- }
1022
- }
1023
-
1024
- if (is_compute && c.workgroupVars.length > 0) {
1025
- // The lanes of one workgroup are dispatched one call at a time, and a scalar
1026
- // `var<workgroup>` a lane assigns lives in the module-scope slot above — a copy of the
1027
- // state object's field, not the field. Writing it back is what lets the next lane see it,
1028
- // which is the whole point of workgroup memory (an array slot already persists, since the
1029
- // slot holds a reference and the lane mutates it in place).
1030
- c.out.add(`const __result__ = ${c.entry.name}(${param_names.join(', ')});`);
1031
- for (const wv of c.workgroupVars) {
1032
- c.out.add(`__workgroup__.${wv.name} = ${wv.name};`);
1033
- }
1034
- c.out.add('return __result__;');
1035
- } else {
1036
- c.out.add(`return ${c.entry.name}(${param_names.join(', ')});`);
1037
- }
1038
-
1039
- c.out.dedent();
1040
- c.out.add('}');
1041
- }
1042
-
1043
- // =========================================================================
1044
- // Statements
1045
- // =========================================================================
1046
-
1047
- /**
1048
- * Emit only the inside of a compound_statement (no `{` / `}`).
1049
- * @param {Compiler} c
1050
- * @param {Node} node — `compound_statement`
1051
- */
1052
- function emitCompoundStatementBody(c, node) {
1053
- for (const child of node.namedChildren) {
1054
- if (child.type === 'statement') {
1055
- emitStatement(c, child);
1056
- }
1057
- }
1058
- }
1059
-
1060
- function emitCompoundStatement(c, node) {
1061
- c.out.add('{');
1062
- c.out.indent();
1063
- c.scope = c.scope.push();
1064
- emitCompoundStatementBody(c, node);
1065
- c.scope = c.scope.parent;
1066
- c.out.dedent();
1067
- c.out.add('}');
1068
- }
1069
-
1070
- /**
1071
- * Emit a single `statement` node (its child is the actual statement kind).
1072
- * @param {Compiler} c
1073
- * @param {Node} node
1074
- */
1075
- function emitStatement(c, node) {
1076
- const inner = node.namedChildren[0];
1077
- switch (inner.type) {
1078
- case 'variable_or_value_statement':
1079
- emitVariableOrValueStatement(c, inner);
1080
- break;
1081
- case 'variable_updating_statement':
1082
- emitVariableUpdatingStatement(c, inner);
1083
- break;
1084
- case 'return_statement':
1085
- emitReturnStatement(c, inner);
1086
- break;
1087
- case 'if_statement':
1088
- emitIfStatement(c, inner);
1089
- break;
1090
- case 'for_statement':
1091
- emitForStatement(c, inner);
1092
- break;
1093
- case 'while_statement':
1094
- emitWhileStatement(c, inner);
1095
- break;
1096
- case 'loop_statement':
1097
- emitLoopStatement(c, inner);
1098
- break;
1099
- case 'switch_statement':
1100
- emitSwitchStatement(c, inner);
1101
- break;
1102
- case 'break_statement':
1103
- c.out.add('break;');
1104
- break;
1105
- case 'continue_statement':
1106
- // Inside a `loop` with a `continuing` block, `continue` must run the
1107
- // continuing block: it is emitted as a break out of the labeled body block
1108
- // (the continuing code follows that block inside the same `while`).
1109
- if (c.continue_label !== null) {
1110
- c.out.add(`break ${c.continue_label};`);
1111
- } else {
1112
- c.out.add('continue;');
1113
- }
1114
- break;
1115
- case 'discard_statement':
1116
- c.out.add('return;');
1117
- break;
1118
- case 'compound_statement':
1119
- emitCompoundStatement(c, inner);
1120
- break;
1121
- case 'func_call_statement':
1122
- emitFunctionCallStatement(c, inner);
1123
- break;
1124
- case 'const_assert_statement':
1125
- // Skipped at runtime.
1126
- break;
1127
- default:
1128
- c.error(inner, `unsupported statement: ${inner.type}`);
1129
- }
1130
- }
1131
-
1132
- /**
1133
- * `let x = expr;` or `var x: T = expr;` or `var x: T;`
1134
- */
1135
- function emitVariableOrValueStatement(c, node) {
1136
- const var_decl = namedChildByType(node, 'variable_decl');
1137
- const opt_ident_direct = namedChildByType(node, 'optionally_typed_ident');
1138
- const expr_node = namedChildByType(node, 'expression');
1139
-
1140
- /** @type {'let' | 'const'} */
1141
- let kind = 'let';
1142
- let opt_ident = null;
1143
- let template = null;
1144
-
1145
- if (var_decl !== null) {
1146
- // WGSL `var` is the mutable form → JS `let`.
1147
- kind = 'let';
1148
- opt_ident = namedChildByType(var_decl, 'optionally_typed_ident');
1149
- template = namedChildByType(var_decl, 'template_list');
1150
- } else {
1151
- // WGSL `let` and `const` are both immutable bindings → JS `const`.
1152
- // Tree-sitter exposes the keyword as an unnamed leading token; read it from the source.
1153
- kind = 'const';
1154
- opt_ident = opt_ident_direct;
1155
- }
1156
-
1157
- const ident = namedChildByType(opt_ident, 'ident');
1158
- const type_spec = namedChildByType(opt_ident, 'type_specifier');
1159
- const name = ident.text;
1160
- let type = type_spec !== null ? parseTypeSpecifier(type_spec) : T_UNKNOWN;
1161
-
1162
- if (expr_node !== null) {
1163
- const expr = compileExpression(c, expr_node);
1164
- // Infer the type from the RHS when no explicit annotation is present. A bare integer
1165
- // literal concretises to i32 here, as WGSL does for `let x = 3;`.
1166
- if (type === T_UNKNOWN || type.kind === 'unknown') {
1167
- type = expr.abstract_int === true ? T_I32 : expr.type;
1168
- }
1169
- c.scope.declare(name, type);
1170
- c.out.add(`${kind} ${name} = ${expr.code};`);
1171
- } else {
1172
- c.scope.declare(name, type);
1173
- c.out.add(`${kind} ${name} = ${defaultValueForType(type)};`);
1174
- }
1175
- }
1176
-
1177
- /**
1178
- * Assignment / compound assignment / increment / decrement.
1179
- */
1180
- function emitVariableUpdatingStatement(c, node) {
1181
- const inner = node.namedChildren[0];
1182
- if (inner.type === 'assignment_statement') {
1183
- const lhs = namedChildByType(inner, 'lhs_expression');
1184
- const expr = namedChildByType(inner, 'expression');
1185
-
1186
- // The compound assignment operator sits between the LHS and the RHS as an unnamed token.
1187
- const op_text = textBetween(inner, lhs, expr).trim();
1188
-
1189
- const lhs_compiled = compileLhs(c, lhs);
1190
- const rhs_compiled = compileExpression(c, expr);
1191
-
1192
- if (op_text === '=' || op_text === '') {
1193
- c.out.add(emitWriteStatement(lhs_compiled, rhs_compiled.code));
1194
- return;
1195
- }
1196
-
1197
- const arith = {
1198
- '+=': 'add', '-=': 'sub', '*=': 'mul', '/=': 'div', '%=': 'rem',
1199
- };
1200
- if (arith.hasOwnProperty(op_text)) {
1201
- c.out.add(emitWriteStatement(lhs_compiled, `wgsl.${arith[op_text]}(${lhs_compiled.code}, ${rhs_compiled.code})`));
1202
- return;
1203
- }
1204
-
1205
- // Bitwise / shift compound assignment, routed like the binary operators: through the
1206
- // element-wise helpers for a vec/mat or bool LHS (raw JS coerces a typed array to NaN and
1207
- // a bool to 0/1), and for `>>=` on u32 (JS `>>` sign-extends).
1208
- const bitwise_helper = BITWISE_HELPER[op_text.slice(0, -1)];
1209
- if (bitwise_helper !== undefined && (
1210
- isContainerType(lhs_compiled.type)
1211
- || isScalarBool(lhs_compiled.type)
1212
- || (op_text === '>>=' && (isScalarUnsigned(lhs_compiled.type) || isScalarUnsigned(rhs_compiled.type)))
1213
- )) {
1214
- c.out.add(emitWriteStatement(lhs_compiled, `wgsl.${bitwise_helper}(${lhs_compiled.code}, ${rhs_compiled.code})`));
1215
- return;
1216
- }
1217
- c.out.add(`${lhs_compiled.code} ${op_text} ${rhs_compiled.code};`);
1218
- return;
1219
- }
1220
- if (inner.type === 'increment_statement') {
1221
- const lhs = namedChildByType(inner, 'lhs_expression');
1222
- const lhs_compiled = compileLhs(c, lhs);
1223
- c.out.add(emitWriteStatement(lhs_compiled, `wgsl.add(${lhs_compiled.code}, 1)`));
1224
- return;
1225
- }
1226
- if (inner.type === 'decrement_statement') {
1227
- const lhs = namedChildByType(inner, 'lhs_expression');
1228
- const lhs_compiled = compileLhs(c, lhs);
1229
- c.out.add(emitWriteStatement(lhs_compiled, `wgsl.sub(${lhs_compiled.code}, 1)`));
1230
- return;
1231
- }
1232
- c.error(inner, `unsupported variable_updating_statement: ${inner.type}`);
1233
- }
1234
-
1235
- /**
1236
- * Emit a write to an LHS. Most LHS expressions are plain JS LValues (`x`, `arr[i]`, `obj.f`) and
1237
- * are written with `=`. Mat-column LHS — `m[i]` for a mat — is special: the compiler emits it as
1238
- * `wgsl.mat_col(m, i, rows)`, which is a typed-array view (a function call result, not assignable).
1239
- * To write through the view we use `Float32Array.prototype.set(rhs)` instead, which copies into
1240
- * the underlying matrix storage in place.
1241
- *
1242
- * @param {{ code: string }} lhs_compiled
1243
- * @param {string} rhs_code
1244
- * @returns {string}
1245
- */
1246
- function emitWriteStatement(lhs_compiled, rhs_code) {
1247
- // Only the bare `wgsl.mat_col(...)` form (no chained `[i]` / `.x`) needs `.set()` — that's a
1248
- // typed-array view, an unassignable function-call result. Anything chained off it (e.g.
1249
- // `wgsl.mat_col(...)[3]` or `wgsl.mat_col(...).x` becoming `[i]`) is a scalar slot and must
1250
- // use plain `=`.
1251
- if (lhs_compiled.code.startsWith('wgsl.mat_col(') && lhs_compiled.code.endsWith(')')) {
1252
- return `${lhs_compiled.code}.set(${rhs_code});`;
1253
- }
1254
- // `*p = v` has to reach the caller's object; assigning would only rebind the parameter.
1255
- if (lhs_compiled.through_pointer === true) {
1256
- return `${lhs_compiled.code} = wgsl.store_ptr(${lhs_compiled.code}, ${rhs_code});`;
1257
- }
1258
- return `${lhs_compiled.code} = ${rhs_code};`;
1259
- }
1260
-
1261
- function emitReturnStatement(c, node) {
1262
- const expr = namedChildByType(node, 'expression');
1263
- if (expr === null) {
1264
- c.out.add('return;');
1265
- } else {
1266
- const e = compileExpression(c, expr);
1267
- c.out.add(`return ${e.code};`);
1268
- }
1269
- }
1270
-
1271
- function emitIfStatement(c, node) {
1272
- const if_clause = namedChildByType(node, 'if_clause');
1273
- const cond = namedChildByType(if_clause, 'expression');
1274
- const if_body = namedChildByType(if_clause, 'compound_statement');
1275
-
1276
- const cond_compiled = compileExpression(c, cond);
1277
- c.out.add(`if (${cond_compiled.code}) {`);
1278
- c.out.indent();
1279
- c.scope = c.scope.push();
1280
- emitCompoundStatementBody(c, if_body);
1281
- c.scope = c.scope.parent;
1282
- c.out.dedent();
1283
- c.out.add('}');
1284
-
1285
- // else-if and else clauses.
1286
- for (const child of node.namedChildren) {
1287
- if (child.type === 'else_if_clause') {
1288
- const eif_cond = namedChildByType(child, 'expression');
1289
- const eif_body = namedChildByType(child, 'compound_statement');
1290
- const eif_compiled = compileExpression(c, eif_cond);
1291
- c.out.add(`else if (${eif_compiled.code}) {`);
1292
- c.out.indent();
1293
- c.scope = c.scope.push();
1294
- emitCompoundStatementBody(c, eif_body);
1295
- c.scope = c.scope.parent;
1296
- c.out.dedent();
1297
- c.out.add('}');
1298
- } else if (child.type === 'else_clause') {
1299
- const else_body = namedChildByType(child, 'compound_statement');
1300
- // else_body may instead be an if_statement (else-if shorthand).
1301
- const nested_if = namedChildByType(child, 'if_statement');
1302
- if (else_body !== null) {
1303
- c.out.add('else {');
1304
- c.out.indent();
1305
- c.scope = c.scope.push();
1306
- emitCompoundStatementBody(c, else_body);
1307
- c.scope = c.scope.parent;
1308
- c.out.dedent();
1309
- c.out.add('}');
1310
- } else if (nested_if !== null) {
1311
- c.out.add('else');
1312
- emitIfStatement(c, nested_if);
1313
- }
1314
- }
1315
- }
1316
- }
1317
-
1318
- function emitForStatement(c, node) {
1319
- const for_header = namedChildByType(node, 'for_header');
1320
- const for_init = namedChildByType(for_header, 'for_init');
1321
- const cond_expr = namedChildByType(for_header, 'expression');
1322
- const for_update = namedChildByType(for_header, 'for_update');
1323
- const body = namedChildByType(node, 'compound_statement');
1324
-
1325
- c.scope = c.scope.push();
1326
-
1327
- // Init / cond / update parts.
1328
- let init_str = '';
1329
- if (for_init !== null) {
1330
- // Tree-sitter wraps it in a variable_or_value_statement; capture the emission as text.
1331
- const sub = new Compiler();
1332
- sub.scope = c.scope;
1333
- sub.structs = c.structs;
1334
- sub.functions = c.functions;
1335
- const inner = for_init.namedChildren[0];
1336
- emitForInitInline(sub, inner);
1337
- init_str = sub.out.build().trimEnd();
1338
- if (init_str.endsWith(';')) init_str = init_str.slice(0, -1);
1339
- }
1340
-
1341
- const cond_str = cond_expr !== null ? compileExpression(c, cond_expr).code : '';
1342
-
1343
- let update_str = '';
1344
- if (for_update !== null) {
1345
- const inner = for_update.namedChildren[0];
1346
- update_str = emitForUpdateInline(c, inner);
1347
- }
1348
-
1349
- c.out.add(`for (${init_str}; ${cond_str}; ${update_str}) {`);
1350
- c.out.indent();
1351
- // `continue` inside this loop targets this loop, not an outer `loop`'s continuing block
1352
- const saved_continue_label = c.continue_label;
1353
- c.continue_label = null;
1354
- emitCompoundStatementBody(c, body);
1355
- c.continue_label = saved_continue_label;
1356
- c.out.dedent();
1357
- c.out.add('}');
1358
-
1359
- c.scope = c.scope.parent;
1360
- }
1361
-
1362
- /**
1363
- * Emit a `variable_or_value_statement` inline (as a single line with no trailing newline structure).
1364
- * Used by `for(init; ...)`.
1365
- */
1366
- function emitForInitInline(c, node) {
1367
- if (node.type === 'variable_or_value_statement') {
1368
- const var_decl = node.namedChildren[0] && node.namedChildren[0].type === 'variable_decl'
1369
- ? node.namedChildren[0]
1370
- : null;
1371
- const opt_ident = namedChildByType(var_decl !== null ? var_decl : node, 'optionally_typed_ident');
1372
- const expr_node = namedChildByType(node, 'expression');
1373
- const ident = namedChildByType(opt_ident, 'ident');
1374
- const type_spec = namedChildByType(opt_ident, 'type_specifier');
1375
- const name = ident.text;
1376
- const init = expr_node !== null ? compileExpression(c, expr_node) : null;
1377
- let type = type_spec !== null ? parseTypeSpecifier(type_spec) : T_UNKNOWN;
1378
- if (type.kind === 'unknown' && init !== null) {
1379
- // `for (var i = 0; ...)`: a bare integer literal concretises to i32, as in a `let`
1380
- type = init.abstract_int === true ? T_I32 : init.type;
1381
- }
1382
- c.scope.declare(name, type);
1383
- const value = init !== null ? init.code : defaultValueForType(type);
1384
- // `var i = …` (mutable, common in `for (var i = 0; ...; i = i + 1)`) → JS `let`.
1385
- // `let i = …` / `const i = …` (immutable) → JS `const`.
1386
- const kind = var_decl !== null ? 'let' : 'const';
1387
- c.out.add(`${kind} ${name} = ${value};`);
1388
- return;
1389
- }
1390
- if (node.type === 'variable_updating_statement') {
1391
- emitVariableUpdatingStatement(c, node);
1392
- return;
1393
- }
1394
- if (node.type === 'func_call_statement') {
1395
- emitFunctionCallStatement(c, node);
1396
- return;
1397
- }
1398
- }
1399
-
1400
- /**
1401
- * @returns {string} JS update expression (no semicolon).
1402
- */
1403
- function emitForUpdateInline(c, node) {
1404
- if (node.type === 'variable_updating_statement') {
1405
- const inner = node.namedChildren[0];
1406
- if (inner.type === 'assignment_statement') {
1407
- const lhs = namedChildByType(inner, 'lhs_expression');
1408
- const expr = namedChildByType(inner, 'expression');
1409
- const op_text = textBetween(inner, lhs, expr).trim();
1410
- const lhs_compiled = compileLhs(c, lhs);
1411
- const rhs_compiled = compileExpression(c, expr);
1412
- if (op_text === '=' || op_text === '') {
1413
- if (lhs_compiled.through_pointer === true) {
1414
- return `${lhs_compiled.code} = wgsl.store_ptr(${lhs_compiled.code}, ${rhs_compiled.code})`;
1415
- }
1416
- return `${lhs_compiled.code} = ${rhs_compiled.code}`;
1417
- }
1418
- const arith = { '+=': 'add', '-=': 'sub', '*=': 'mul', '/=': 'div', '%=': 'rem' };
1419
- if (arith.hasOwnProperty(op_text)) {
1420
- if (lhs_compiled.through_pointer === true) {
1421
- return `${lhs_compiled.code} = wgsl.store_ptr(${lhs_compiled.code}, wgsl.${arith[op_text]}(${lhs_compiled.code}, ${rhs_compiled.code}))`;
1422
- }
1423
- return `${lhs_compiled.code} = wgsl.${arith[op_text]}(${lhs_compiled.code}, ${rhs_compiled.code})`;
1424
- }
1425
- return `${lhs_compiled.code} ${op_text} ${rhs_compiled.code}`;
1426
- }
1427
- if (inner.type === 'increment_statement') {
1428
- const lhs = namedChildByType(inner, 'lhs_expression');
1429
- const lhs_compiled = compileLhs(c, lhs);
1430
- return `${lhs_compiled.code} = wgsl.add(${lhs_compiled.code}, 1)`;
1431
- }
1432
- if (inner.type === 'decrement_statement') {
1433
- const lhs = namedChildByType(inner, 'lhs_expression');
1434
- const lhs_compiled = compileLhs(c, lhs);
1435
- return `${lhs_compiled.code} = wgsl.sub(${lhs_compiled.code}, 1)`;
1436
- }
1437
- }
1438
- return '';
1439
- }
1440
-
1441
- function emitWhileStatement(c, node) {
1442
- const cond = namedChildByType(node, 'expression');
1443
- const body = namedChildByType(node, 'compound_statement');
1444
- const cc = compileExpression(c, cond);
1445
- c.out.add(`while (${cc.code}) {`);
1446
- c.out.indent();
1447
- c.scope = c.scope.push();
1448
- // `continue` inside this loop targets this loop, not an outer `loop`'s continuing block
1449
- const saved_continue_label = c.continue_label;
1450
- c.continue_label = null;
1451
- emitCompoundStatementBody(c, body);
1452
- c.continue_label = saved_continue_label;
1453
- c.scope = c.scope.parent;
1454
- c.out.dedent();
1455
- c.out.add('}');
1456
- }
1457
-
1458
- function emitLoopStatement(c, node) {
1459
- // NOTE: the grammar puts the loop's `statement` children and the optional
1460
- // `continuing_statement` directly on the `loop_statement` node (no
1461
- // `compound_statement` wrapper); emitCompoundStatementBody visits only the
1462
- // `statement` children, which is exactly the loop body.
1463
- const body = node;
1464
- const continuing = namedChildByType(node, 'continuing_statement');
1465
-
1466
- const saved_continue_label = c.continue_label;
1467
-
1468
- c.out.add('while (true) {');
1469
- c.out.indent();
1470
- c.scope = c.scope.push();
1471
-
1472
- if (continuing === null) {
1473
-
1474
- // no continuing block: JS `continue` already has the right semantics
1475
- c.continue_label = null;
1476
- emitCompoundStatementBody(c, body);
1477
-
1478
- } else {
1479
-
1480
- // WGSL `continue` transfers control INTO the `continuing` block. Emit the loop
1481
- // body inside a labeled block: `continue` becomes `break <label>`, which lands on
1482
- // the continuing code that follows the block (still inside the `while`).
1483
- const label = `loop_body_${c.loop_label_counter++}`;
1484
- c.continue_label = label;
1485
-
1486
- c.out.add(`${label}: {`);
1487
- c.out.indent();
1488
- emitCompoundStatementBody(c, body);
1489
- c.out.dedent();
1490
- c.out.add('}');
1491
-
1492
- c.continue_label = null;
1493
-
1494
- const cont_body = namedChildByType(continuing, 'continuing_compound_statement');
1495
- if (cont_body !== null) {
1496
- emitCompoundStatementBody(c, cont_body);
1497
-
1498
- // `break if expr;` — only legal as the trailing statement of a continuing block.
1499
- // It is a direct named child of the continuing compound (not wrapped in `statement`),
1500
- // so emitCompoundStatementBody does not visit it.
1501
- const break_if = namedChildByType(cont_body, 'break_if_statement');
1502
- if (break_if !== null) {
1503
- const break_expr = namedChildByType(break_if, 'expression');
1504
- const bc = compileExpression(c, break_expr);
1505
- c.out.add(`if (${bc.code}) { break; }`);
1506
- }
1507
- }
1508
-
1509
- }
1510
-
1511
- c.continue_label = saved_continue_label;
1512
- c.scope = c.scope.parent;
1513
- c.out.dedent();
1514
- c.out.add('}');
1515
- }
1516
-
1517
- /**
1518
- * Emit a WGSL `switch`.
1519
- *
1520
- * Grammar (tree-sitter-wgsl): `switch_statement { expression, switch_body { switch_clause* } }`,
1521
- * where each `switch_clause` wraps either a `case_clause { case_selectors { case_selector+ },
1522
- * compound_statement }` or a `default_alone_clause { compound_statement }`. A `case_selector` holds
1523
- * either an `expression` (a case value) or the `default` keyword.
1524
- *
1525
- * WGSL switch has no fall-through — each selected clause runs its body and stops — so we terminate
1526
- * every clause body with `break;`. Multiple selectors on one clause become stacked JS `case` labels
1527
- * over a shared body.
1528
- */
1529
- function emitSwitchStatement(c, node) {
1530
- const expr = namedChildByType(node, 'expression');
1531
- const ec = compileExpression(c, expr);
1532
- c.out.add(`switch (${ec.code}) {`);
1533
- c.out.indent();
1534
-
1535
- const body = namedChildByType(node, 'switch_body');
1536
- if (body) {
1537
- for (const clause of namedChildrenByType(body, 'switch_clause')) {
1538
- const case_clause = namedChildByType(clause, 'case_clause');
1539
- const default_clause = namedChildByType(clause, 'default_alone_clause');
1540
- const inner = case_clause ?? default_clause;
1541
- if (!inner) {
1542
- continue;
1543
- }
1544
-
1545
- if (case_clause) {
1546
- const case_selectors = namedChildByType(case_clause, 'case_selectors');
1547
- if (case_selectors) {
1548
- for (const sel of namedChildrenByType(case_selectors, 'case_selector')) {
1549
- const sel_expr = namedChildByType(sel, 'expression');
1550
- if (sel_expr) {
1551
- c.out.add(`case ${compileExpression(c, sel_expr).code}:`);
1552
- } else {
1553
- // a bare `default` used within a case_selectors list
1554
- c.out.add('default:');
1555
- }
1556
- }
1557
- }
1558
- } else {
1559
- c.out.add('default:');
1560
- }
1561
-
1562
- // Wrap each clause body in its own JS block. WGSL case clauses are independent scopes,
1563
- // but JS switch clauses share the switch's block scope — without the braces, `let`
1564
- // temporaries declared in two clauses collide ("Identifier already declared").
1565
- const case_body = namedChildByType(inner, 'compound_statement');
1566
- c.out.add('{');
1567
- c.out.indent();
1568
- c.scope = c.scope.push();
1569
- if (case_body) {
1570
- emitCompoundStatementBody(c, case_body);
1571
- }
1572
- c.out.add('break;');
1573
- c.scope = c.scope.parent;
1574
- c.out.dedent();
1575
- c.out.add('}');
1576
- }
1577
- }
1578
-
1579
- c.out.dedent();
1580
- c.out.add('}');
1581
- }
1582
-
1583
- function emitFunctionCallStatement(c, node) {
1584
- const call_phrase = namedChildByType(node, 'call_phrase');
1585
- const r = compileCallPhraseTyped(c, call_phrase);
1586
- c.out.add(`${r.code};`);
1587
- }
1588
-
1589
- // =========================================================================
1590
- // Expressions
1591
- // =========================================================================
1592
-
1593
- /**
1594
- * Compile an expression node (any wrapper or specific kind).
1595
- * @param {Compiler} c
1596
- * @param {Node} node
1597
- * @returns {{ code: string, type: WgslType }}
1598
- */
1599
- function compileExpression(c, node) {
1600
- const u = unwrapExpression(node);
1601
-
1602
- // Binary operations
1603
- switch (u.type) {
1604
- case 'expression':
1605
- // tree-sitter-wgsl can leave `&&` / `||` directly under an `expression` node — i.e.
1606
- // the operator wraps two operands without a more specific short_circuit_*_expression
1607
- // wrapper around it. With 2 named children the unnamed operator sits between them.
1608
- return compileBinaryExpression(c, u, '__shortcircuit_unused__', 'raw');
1609
- case 'relational_expression':
1610
- return compileBinaryExpression(c, u, 'relational_operator', 'raw');
1611
- case 'shift_expression':
1612
- return compileBinaryExpression(c, u, 'shift_operator', 'raw');
1613
- case 'additive_expression':
1614
- return compileBinaryExpression(c, u, 'additive_operator', 'arith');
1615
- case 'multiplicative_expression':
1616
- return compileBinaryExpression(c, u, 'multiplicative_operator', 'arith');
1617
- case 'short_circuit_and_expression':
1618
- case 'short_circuit_or_expression':
1619
- return compileBinaryExpression(c, u, '__shortcircuit_unused__', 'raw');
1620
- case 'bitwise_expression':
1621
- // Top-level WGSL `^` / `&` / `|` operator. The operator is an unnamed token between
1622
- // the two named operands; compileBinaryExpression's 2-named-children path picks it up.
1623
- return compileBinaryExpression(c, u, '__bitwise_unused__', 'raw');
1624
- case 'binary_and_expression':
1625
- case 'binary_or_expression':
1626
- case 'binary_xor_expression':
1627
- // Inner chain nodes from the WGSL grammar (e.g. `a ^ b ^ c` nests as
1628
- // `binary_xor_expression(binary_xor_expression(a, b), c)`). When 2 named children
1629
- // appear, the unnamed operator sits between them; the 1-child case is a passthrough.
1630
- return compileBinaryExpression(c, u, '__bitwise_unused__', 'raw');
1631
- }
1632
-
1633
- // Unary
1634
- if (u.type === 'unary_expression') {
1635
- return compileUnaryExpression(c, u);
1636
- }
1637
-
1638
- // Singular (primary + chained component access)
1639
- if (u.type === 'singular_expression') {
1640
- return compileSingularExpression(c, u);
1641
- }
1642
-
1643
- // Primary subkinds
1644
- if (u.type === 'literal' || u.type === 'int_literal' || u.type === 'float_literal' || u.type === 'bool_literal') {
1645
- // A bare integer literal is WGSL's AbstractInt. Its concrete type comes from context this
1646
- // compiler does not track, so the type stays unknown, but operator selection still needs
1647
- // to know it is an integer: see integerDivision().
1648
- return { code: literalToJs(u.text), type: literalType(u.text), abstract_int: isBareIntLiteral(u.text) };
1649
- }
1650
- if (u.type === 'paren_expression') {
1651
- const inner = namedChildByType(u, 'expression');
1652
- const e = compileExpression(c, inner);
1653
- return { code: `(${e.code})`, type: e.type, abstract_int: e.abstract_int };
1654
- }
1655
- if (u.type === 'call_expression') {
1656
- const call_phrase = namedChildByType(u, 'call_phrase');
1657
- return compileCallPhraseTyped(c, call_phrase);
1658
- }
1659
- if (u.type === 'template_elaborated_ident') {
1660
- const ident = namedChildByType(u, 'ident');
1661
- const name = ident !== null ? ident.text : u.text;
1662
- return { code: name, type: c.scope.lookup(name) };
1663
- }
1664
- if (u.type === 'ident') {
1665
- return { code: u.text, type: c.scope.lookup(u.text) };
1666
- }
1667
-
1668
- c.error(u, `unsupported expression: ${u.type}`);
1669
- }
1670
-
1671
- /**
1672
- * Compile a binary expression node. Two grammar shapes are supported:
1673
- * - 3 named children where the middle child has type `op_type` — used for additive / multiplicative
1674
- * where the operator is an explicit named node (`additive_operator`, `multiplicative_operator`).
1675
- * - 2 named children with an unnamed operator token between them — used for relational / shift,
1676
- * where the operator (e.g. `<`, `>=`, `<<`) is a raw token.
1677
- *
1678
- * Mode:
1679
- * - `'arith'` → use `wgsl.add/sub/mul/div/rem` (handles vectors)
1680
- * - `'raw'` → emit raw JS operator
1681
- */
1682
- function compileBinaryExpression(c, node, op_type, mode) {
1683
- const named = node.namedChildren;
1684
- if (named.length === 1) {
1685
- return compileExpression(c, named[0]);
1686
- }
1687
-
1688
- let op_text = null;
1689
- let left_node;
1690
- let right_node;
1691
-
1692
- if (named.length === 3 && named[1].type === op_type) {
1693
- op_text = named[1].text;
1694
- left_node = named[0];
1695
- right_node = named[2];
1696
- } else if (named.length === 2) {
1697
- left_node = named[0];
1698
- right_node = named[1];
1699
- // tree-sitter-web doesn't surface unnamed children in `node.children`. Extract the
1700
- // operator by slicing the original text using the operands' source positions.
1701
- op_text = textBetween(node, left_node, right_node).trim();
1702
- } else {
1703
- return { code: node.text, type: T_UNKNOWN };
1704
- }
1705
-
1706
- const left = compileExpression(c, left_node);
1707
- const right = compileExpression(c, right_node);
1708
-
1709
- if (mode === 'arith') {
1710
- const helper = ARITHMETIC_OP[op_text];
1711
- if (helper !== undefined) {
1712
- // `*` on matrix/vector operands has WGSL-specific algebra (dot-products and
1713
- // accumulation) that the element-wise `wgsl.mul` doesn't capture. Route to a
1714
- // dedicated `mNxR_multiply_*` helper when at least one side is a matrix.
1715
- if (op_text === '*') {
1716
- const dispatched = dispatchMatrixMul(left, right);
1717
- if (dispatched !== null) return dispatched;
1718
- }
1719
- // WGSL `/` on integer types is truncated integer division. Route through the
1720
- // integer-truncating helper when WGSL would type the division as integer;
1721
- // otherwise use float div.
1722
- let actual_helper = helper;
1723
- if (op_text === '/' && integerDivision(left, right)) {
1724
- actual_helper = 'idiv';
1725
- }
1726
- // WGSL `+` / `-` / `*` on integer types wrap at 32 bits. JS numbers don't, and the bits
1727
- // an overflowing f64 drops are the low ones — which silently ruins hashes and PRNGs
1728
- // (see the wrapping helpers in WGSLLanguageCore.js). Route to the wrapping variant only
1729
- // when the result type is *known* to be an integer, so anything untyped or float keeps
1730
- // the plain helper.
1731
- const arith_type = inferBinaryArithResultType(left.type, right.type);
1732
- const wrapping = INTEGER_WRAPPING_OP[op_text];
1733
- if (wrapping !== undefined && isIntegerType(arith_type)) {
1734
- actual_helper = `${wrapping}_${integerScalarName(arith_type)}`;
1735
- }
1736
- return {
1737
- code: `wgsl.${actual_helper}(${left.code}, ${right.code})`,
1738
- type: arith_type,
1739
- // arithmetic on two AbstractInts is still AbstractInt: `(1 + 2) / 2` is 1
1740
- abstract_int: left.abstract_int === true && right.abstract_int === true,
1741
- };
1742
- }
1743
- }
1744
-
1745
- // Bitwise (`&` / `|` / `^`) and shift (`<<` / `>>`) operators. Routed through element-wise
1746
- // helpers for vec/mat operands (JS raw operators coerce typed arrays to NaN), for bool
1747
- // operands (raw JS yields 0/1 where WGSL yields a bool), AND for `>>` on u32 scalars — JS's
1748
- // signed `>>` sign-extends, but WGSL u32 shift is logical.
1749
- const bitwise_helper = BITWISE_HELPER[op_text];
1750
- if (bitwise_helper !== undefined) {
1751
- if (isContainerType(left.type) || isContainerType(right.type)
1752
- || isScalarBool(left.type) || isScalarBool(right.type)) {
1753
- return {
1754
- code: `wgsl.${bitwise_helper}(${left.code}, ${right.code})`,
1755
- type: inferRawBinaryResultType(left.type, right.type, op_text),
1756
- };
1757
- }
1758
- // For `>>` specifically: any u32-typed scalar operand must use the unsigned shift helper.
1759
- if (op_text === '>>' && (isScalarUnsigned(left.type) || isScalarUnsigned(right.type))) {
1760
- return {
1761
- code: `wgsl.shr(${left.code}, ${right.code})`,
1762
- type: inferRawBinaryResultType(left.type, right.type, op_text),
1763
- };
1764
- }
1765
- }
1766
-
1767
- // Comparison operators. JS's raw `==` etc. compare TypedArrays by reference; for WGSL
1768
- // `vec<T>` operands we need element-wise comparison returning a `vec<bool>`.
1769
- const cmp_helper = COMPARISON_HELPER[op_text];
1770
- if (cmp_helper !== undefined && (isContainerType(left.type) || isContainerType(right.type))) {
1771
- return {
1772
- code: `wgsl.${cmp_helper}(${left.code}, ${right.code})`,
1773
- type: inferRawBinaryResultType(left.type, right.type, op_text),
1774
- };
1775
- }
1776
-
1777
- return {
1778
- code: `${left.code} ${op_text} ${right.code}`,
1779
- type: inferRawBinaryResultType(left.type, right.type, op_text),
1780
- };
1781
- }
1782
-
1783
- /** Map WGSL bitwise/shift operators to language-core element-wise helper names. */
1784
- const BITWISE_HELPER = {
1785
- '&': 'bit_and',
1786
- '|': 'bit_or',
1787
- '^': 'bit_xor',
1788
- '<<': 'shl',
1789
- '>>': 'shr',
1790
- };
1791
-
1792
- /** Map WGSL comparison operators to language-core element-wise helper names. */
1793
- const COMPARISON_HELPER = {
1794
- '==': 'eq',
1795
- '!=': 'ne',
1796
- '<': 'lt',
1797
- '>': 'gt',
1798
- '<=': 'le',
1799
- '>=': 'ge',
1800
- };
1801
-
1802
- /** True if `t` is a vector or matrix — operand types that need element-wise dispatch. */
1803
- function isContainerType(t) {
1804
- return t && (t.kind === 'vec' || t.kind === 'mat');
1805
- }
1806
-
1807
- /** True if `t` is a u32 scalar — needed to route `>>` through the unsigned-shift helper. */
1808
- function isScalarUnsigned(t) {
1809
- return t && t.kind === 'primitive' && t.name === 'u32';
1810
- }
1811
-
1812
- /** True if `t` is a bool scalar — WGSL `&` / `|` / `^` on bools are logical and stay bool. */
1813
- function isScalarBool(t) {
1814
- return t && t.kind === 'primitive' && t.name === 'bool';
1815
- }
1816
-
1817
- /** The scalar name (`u32` / `i32`) of an integer type, for picking a wrapping arithmetic helper. */
1818
- function integerScalarName(t) {
1819
- if (t.kind === 'primitive') return t.name;
1820
- return t.scalar;
1821
- }
1822
-
1823
- /** True if `t` is an integer type (scalar or vec/mat of i32/u32). */
1824
- function isIntegerType(t) {
1825
- if (!t) return false;
1826
- if (t.kind === 'primitive') return t.name === 'i32' || t.name === 'u32';
1827
- if (t.kind === 'vec' || t.kind === 'mat') return t.scalar === 'i32' || t.scalar === 'u32';
1828
- return false;
1829
- }
1830
-
1831
- /** True if the literal's spelling is a bare integer: no suffix, no fraction, no exponent. */
1832
- function isBareIntLiteral(text) {
1833
- return /^(0[xX][0-9a-fA-F]+|[0-9]+)$/.test(text.trim());
1834
- }
1835
-
1836
- /**
1837
- * True if WGSL evaluates `left / right` as integer division. Both operands integer-typed is the
1838
- * obvious case. A bare literal is AbstractInt and takes the other operand's integer type, so
1839
- * `index / 3` truncates; two bare literals divide as AbstractInt, so `7 / 2` is 3 even where an
1840
- * f32 is expected. Negation and parentheses keep a literal abstract: `index / -3` and
1841
- * `index / (3)` truncate too.
1842
- */
1843
- function integerDivision(left, right) {
1844
- const isInt = (operand) => isIntegerType(operand.type) || operand.abstract_int === true;
1845
- return isInt(left) && isInt(right);
1846
- }
1847
-
1848
- /**
1849
- * Dispatch a WGSL `*` expression to the appropriate matrix/vector helper in the language
1850
- * core when at least one operand is a matrix. Returns `null` to indicate "fall through to the
1851
- * element-wise multiply" — used for scalar×scalar, vec×scalar, vec×vec (Hadamard product),
1852
- * matrix×scalar, etc., which `wgsl.mul` already handles correctly.
1853
- *
1854
- * Naming includes the scalar type suffix (`f` / `i` / `u` / `h`), e.g.
1855
- * `m4x4f_multiply_v4f`, `v3f_multiply_m3x3f`, `m4x4f_multiply_m4x4f`. Unsupported
1856
- * shapes (e.g. mat3x4 × vec3) produce a runtime `TypeError: wgsl.<name> is not a function`
1857
- * — adding support is a single new function in WGSLLanguageCore.js.
1858
- *
1859
- * @param {{ code: string, type: WgslType }} left
1860
- * @param {{ code: string, type: WgslType }} right
1861
- * @returns {{ code: string, type: WgslType } | null}
1862
- */
1863
- function dispatchMatrixMul(left, right) {
1864
- const lt = left.type;
1865
- const rt = right.type;
1866
- const isVec = (t) => t && t.kind === 'vec';
1867
- const isMat = (t) => t && t.kind === 'mat';
1868
-
1869
- // mat × mat → mat(b.cols, a.rows). Multiplication is defined only when a.cols == b.rows.
1870
- if (isMat(lt) && isMat(rt)) {
1871
- const lh = `m${lt.cols}x${lt.rows}${scalarTypeSuffix(lt.scalar)}`;
1872
- const rh = `m${rt.cols}x${rt.rows}${scalarTypeSuffix(rt.scalar)}`;
1873
- return {
1874
- code: `wgsl.${lh}_multiply_${rh}(${left.code}, ${right.code})`,
1875
- type: { kind: 'mat', cols: rt.cols, rows: lt.rows, scalar: lt.scalar },
1876
- };
1877
- }
1878
-
1879
- // mat × vec → vec(rows). Vec must have length == cols.
1880
- if (isMat(lt) && isVec(rt)) {
1881
- const lh = `m${lt.cols}x${lt.rows}${scalarTypeSuffix(lt.scalar)}`;
1882
- const rh = `v${rt.size}${scalarTypeSuffix(rt.scalar)}`;
1883
- return {
1884
- code: `wgsl.${lh}_multiply_${rh}(${left.code}, ${right.code})`,
1885
- type: { kind: 'vec', size: lt.rows, scalar: lt.scalar },
1886
- };
1887
- }
1888
-
1889
- // vec × mat → vec(cols). Vec acts as a row vector; vec.length must equal rows.
1890
- if (isVec(lt) && isMat(rt)) {
1891
- const lh = `v${lt.size}${scalarTypeSuffix(lt.scalar)}`;
1892
- const rh = `m${rt.cols}x${rt.rows}${scalarTypeSuffix(rt.scalar)}`;
1893
- return {
1894
- code: `wgsl.${lh}_multiply_${rh}(${left.code}, ${right.code})`,
1895
- type: { kind: 'vec', size: rt.cols, scalar: rt.scalar },
1896
- };
1897
- }
1898
-
1899
- return null;
1900
- }
1901
-
1902
- /** Scalar type → suffix character used in matrix/vector helper names: f / i / u / h. */
1903
- function scalarTypeSuffix(scalar) {
1904
- return { f32: 'f', i32: 'i', u32: 'u', f16: 'h' }[scalar] || 'f';
1905
- }
1906
-
1907
- /**
1908
- * Pick the result type of an element-wise arithmetic op given the operand types.
1909
- * Vec wins over scalar; matrix wins over scalar; scalar otherwise.
1910
- */
1911
- function inferBinaryArithResultType(a, b) {
1912
- const isVec = (t) => t && t.kind === 'vec';
1913
- const isMat = (t) => t && t.kind === 'mat';
1914
- const isPrim = (t) => t && t.kind === 'primitive';
1915
-
1916
- if (isVec(a)) return a;
1917
- if (isVec(b)) return b;
1918
- if (isMat(a)) return a;
1919
- if (isMat(b)) return b;
1920
- if (isPrim(a)) return a;
1921
- if (isPrim(b)) return b;
1922
- return T_UNKNOWN;
1923
- }
1924
-
1925
- /**
1926
- * Pick the result type of a non-arithmetic binary op (comparison / logical / bitwise / shift).
1927
- * Comparisons return bool; bitwise and shift preserve the leftmost known integer operand type.
1928
- */
1929
- function inferRawBinaryResultType(a, b, op) {
1930
- const COMPARISON = new Set(['==', '!=', '<', '>', '<=', '>=']);
1931
- const LOGICAL = new Set(['&&', '||']);
1932
- if (COMPARISON.has(op) || LOGICAL.has(op)) {
1933
- return { kind: 'primitive', name: 'bool' };
1934
- }
1935
- // Bitwise / shift: propagate the first known integer-ish operand type. This lets `bitcast`
1936
- // and other downstream consumers see u32/i32 even when the literal on one side is untyped.
1937
- if (a && a.kind === 'primitive') return a;
1938
- if (b && b.kind === 'primitive') return b;
1939
- if (a && a.kind === 'vec') return a;
1940
- if (b && b.kind === 'vec') return b;
1941
- return T_UNKNOWN;
1942
- }
1943
-
1944
- function compileUnaryExpression(c, node) {
1945
- const named = node.namedChildren;
1946
- const prefix = textBeforeFirstNamed(node).trim();
1947
-
1948
- if (prefix === '') {
1949
- return compileExpression(c, named[0]);
1950
- }
1951
-
1952
- const operand = compileExpression(c, named[named.length - 1]);
1953
-
1954
- if (prefix === '-') return { code: `wgsl.neg(${operand.code})`, type: operand.type, abstract_int: operand.abstract_int };
1955
- if (prefix === '!') return { code: `!${operand.code}`, type: operand.type };
1956
- if (prefix === '~') return { code: `wgsl.not(${operand.code})`, type: operand.type };
1957
- if (prefix === '*' || prefix === '&') return { code: operand.code, type: operand.type };
1958
-
1959
- return { code: `${prefix}${operand.code}`, type: operand.type };
1960
- }
1961
-
1962
- function compileSingularExpression(c, node) {
1963
- const named = node.namedChildren;
1964
- const head = named[0];
1965
- const head_result = head.type === 'primary_expression'
1966
- ? compileExpression(c, unwrapExpression(head))
1967
- : compileExpression(c, head);
1968
-
1969
- let code = head_result.code;
1970
- let type = head_result.type;
1971
-
1972
- for (let i = 1; i < named.length; i++) {
1973
- const spec = named[i];
1974
- if (spec.type !== 'component_or_swizzle_specifier') break;
1975
- const next = applyAccessSpecifier(c, spec, code, type);
1976
- code = next.code;
1977
- type = next.type;
1978
- }
1979
-
1980
- // a bare literal reached through a singular expression is still AbstractInt; anything with an
1981
- // access specifier on it is not a literal
1982
- return { code, type, abstract_int: named.length === 1 ? head_result.abstract_int : undefined };
1983
- }
1984
-
1985
- /**
1986
- * Apply a `.member`, `.swizzle`, or `[index]` access to an expression that has already been
1987
- * compiled to `lhs_code` of type `lhs_type`.
1988
- */
1989
- function applyAccessSpecifier(c, node, lhs_code, lhs_type) {
1990
- // tree-sitter-wgsl groups chained access like `[i].x` (or `.foo[i]`) as a SINGLE outer
1991
- // `component_or_swizzle_specifier` whose first named child is the immediate access (an
1992
- // `expression` for `[]` or a `member_ident` for `.field`) and whose second named child
1993
- // is a NESTED `component_or_swizzle_specifier` for the rest of the chain. We apply the
1994
- // immediate access first, then recurse into any nested specifier.
1995
- let code = lhs_code;
1996
- let type = lhs_type;
1997
-
1998
- const member_ident = namedChildByType(node, 'member_ident');
1999
- if (member_ident !== null) {
2000
- const r = applyMemberAccess(c, member_ident.text, code, type);
2001
- code = r.code;
2002
- type = r.type;
2003
- } else {
2004
- const expr = namedChildByType(node, 'expression');
2005
- if (expr !== null) {
2006
- const e = compileExpression(c, expr);
2007
- // Matrices are stored as flat column-major typed arrays, so the bare `m[i]`
2008
- // would index a scalar — route through `wgsl.mat_col` to get the column as a vec.
2009
- if (type && type.kind === 'mat') {
2010
- code = `wgsl.mat_col(${code}, ${e.code}, ${type.rows})`;
2011
- type = { kind: 'vec', size: type.rows, scalar: type.scalar };
2012
- } else {
2013
- code = `${code}[${e.code}]`;
2014
- if (type && type.kind === 'array') type = type.element;
2015
- else if (type && type.kind === 'vec') type = { kind: 'primitive', name: type.scalar };
2016
- else type = T_UNKNOWN;
2017
- }
2018
- }
2019
- }
2020
-
2021
- const nested = namedChildByType(node, 'component_or_swizzle_specifier');
2022
- if (nested !== null) {
2023
- const r = applyAccessSpecifier(c, nested, code, type);
2024
- code = r.code;
2025
- type = r.type;
2026
- }
2027
-
2028
- return { code, type };
2029
- }
2030
-
2031
- /** Apply a `.field` / `.swizzle` access to an already-compiled expression of `lhs_type`. */
2032
- function applyMemberAccess(c, name, lhs_code, lhs_type) {
2033
- if (lhs_type !== null && lhs_type !== undefined && lhs_type.kind === 'vec' && isSwizzlePattern(name)) {
2034
- if (name.length === 1) {
2035
- return {
2036
- code: `${lhs_code}[${SWIZZLE_INDEX[name]}]`,
2037
- type: { kind: 'primitive', name: lhs_type.scalar },
2038
- };
2039
- }
2040
- const new_size = name.length;
2041
- const ctor = vecAliasName(new_size, lhs_type.scalar);
2042
- const elements = Array.from(name).map(ch => `${lhs_code}[${SWIZZLE_INDEX[ch]}]`);
2043
- return {
2044
- code: `wgsl.${ctor}(${elements.join(', ')})`,
2045
- type: { kind: 'vec', size: new_size, scalar: lhs_type.scalar },
2046
- };
2047
- }
2048
- if (lhs_type !== null && lhs_type !== undefined && lhs_type.kind === 'struct') {
2049
- const struct_info = c.structs.get(lhs_type.name);
2050
- if (struct_info !== undefined) {
2051
- const field = struct_info.fields.find(f => f.name === name);
2052
- if (field !== undefined) {
2053
- return { code: `${lhs_code}.${name}`, type: field.type };
2054
- }
2055
- }
2056
- }
2057
- return { code: `${lhs_code}.${name}`, type: T_UNKNOWN };
2058
- }
2059
-
2060
- function isSwizzlePattern(name) {
2061
- if (name.length === 0 || name.length > 4) return false;
2062
- for (const ch of name) {
2063
- if (!SWIZZLE_LETTERS.has(ch)) return false;
2064
- }
2065
- return true;
2066
- }
2067
-
2068
- /**
2069
- * Compile a `call_phrase` and return both the JS code and the inferred result type.
2070
- *
2071
- * Special cases:
2072
- * - `vec3<f32>(a, b, c)` → `wgsl.vec3f(a, b, c)` (vec result)
2073
- * - `f32(x)` → `wgsl.f32(x)` (scalar result)
2074
- * - WGSL builtin (mix, clamp, dot, length, ...) → `wgsl.<name>(...)` (best-effort type)
2075
- * - User function → `name(...)` (uses declared return type)
2076
- *
2077
- * @returns {{ code: string, type: WgslType }}
2078
- */
2079
- function compileCallPhraseTyped(c, node) {
2080
- const ident_node = namedChildByType(node, 'template_elaborated_ident');
2081
- const arg_list = namedChildByType(node, 'argument_expression_list');
2082
-
2083
- const ident_inner = namedChildByType(ident_node, 'ident');
2084
- const callee_name = ident_inner.text;
2085
- const template = namedChildByType(ident_node, 'template_list');
2086
-
2087
- /** @type {Array<{ code: string, type: WgslType }>} */
2088
- const arg_results = [];
2089
- if (arg_list !== null) {
2090
- const comma_list = namedChildByType(arg_list, 'expression_comma_list');
2091
- if (comma_list !== null) {
2092
- for (const arg of namedChildrenByType(comma_list, 'expression')) {
2093
- arg_results.push(compileExpression(c, arg));
2094
- }
2095
- }
2096
- }
2097
- const args = arg_results.map(a => a.code);
2098
-
2099
- // 1) Vec aliases: vec3f, vec4i, vec2h, etc.
2100
- if (VECTOR_ALIASES.hasOwnProperty(callee_name)) {
2101
- const a = VECTOR_ALIASES[callee_name];
2102
- return {
2103
- code: `wgsl.${callee_name}(${args.join(', ')})`,
2104
- type: { kind: 'vec', size: a.size, scalar: a.scalar },
2105
- };
2106
- }
2107
- // 2) Templated vec: vec3<f32>(...).
2108
- if (callee_name === 'vec2' || callee_name === 'vec3' || callee_name === 'vec4') {
2109
- const size = parseInt(callee_name.slice(3));
2110
- let scalar;
2111
- if (template !== null) {
2112
- const targs = extractTemplateArgs(template);
2113
- if (targs.length > 0) scalar = targs[0];
2114
- }
2115
- if (scalar === undefined) {
2116
- // No explicit template — infer the component type from the arguments, mirroring
2117
- // WGSL's overload resolution. e.g. `vec4(some_u32)` is `vec4<u32>`, not `vec4<f32>`.
2118
- for (const a of arg_results) {
2119
- const t = a.type;
2120
- if (t && t.kind === 'primitive' && t.name !== 'bool') { scalar = t.name; break; }
2121
- if (t && (t.kind === 'vec' || t.kind === 'mat')) { scalar = t.scalar; break; }
2122
- }
2123
- if (scalar === undefined) scalar = 'f32';
2124
- }
2125
- return {
2126
- code: `wgsl.${vecAliasName(size, scalar)}(${args.join(', ')})`,
2127
- type: { kind: 'vec', size, scalar },
2128
- };
2129
- }
2130
- // 3) Matrix constructors.
2131
- {
2132
- const m = callee_name.match(/^mat(\d)x(\d)([ifh]?)$/);
2133
- if (m !== null) {
2134
- const cols = parseInt(m[1]);
2135
- const rows = parseInt(m[2]);
2136
- const suffix = m[3];
2137
- let scalar;
2138
- if (suffix === 'f') scalar = 'f32';
2139
- else if (suffix === 'i') scalar = 'i32';
2140
- else if (suffix === 'h') scalar = 'f16';
2141
- else if (template !== null) {
2142
- const targs = extractTemplateArgs(template);
2143
- scalar = targs[0] || 'f32';
2144
- } else {
2145
- scalar = 'f32';
2146
- }
2147
- const fn_suffix = suffix !== '' ? '' : ({ f32: 'f', i32: 'i', f16: 'h' }[scalar] || 'f');
2148
- return {
2149
- code: `wgsl.${callee_name}${fn_suffix}(${args.join(', ')})`,
2150
- type: { kind: 'mat', cols, rows, scalar },
2151
- };
2152
- }
2153
- }
2154
-
2155
- // 4) Scalar casts: f32(x), i32(x), u32(x), f16(x), bool(x).
2156
- if (PRIMITIVE_NAMES.has(callee_name)) {
2157
- return {
2158
- code: `wgsl.${callee_name}(${args.join(', ')})`,
2159
- type: { kind: 'primitive', name: callee_name },
2160
- };
2161
- }
2162
-
2163
- // 5) `bitcast<T>(x)` — JS can't tell a u32 from an f32 by value alone, so we emit the
2164
- // source type alongside the target. The compiler infers source from the argument's tracked
2165
- // type; if that's unknown the runtime falls back to a heuristic.
2166
- if (callee_name === 'bitcast') {
2167
- let target = 'f32';
2168
- if (template !== null) {
2169
- const targs = extractTemplateArgs(template);
2170
- if (targs.length > 0) target = targs[0];
2171
- }
2172
- const arg_type = arg_results[0] !== undefined ? arg_results[0].type : null;
2173
- let source = null;
2174
- if (arg_type) {
2175
- if (arg_type.kind === 'primitive') source = arg_type.name;
2176
- else if (arg_type.kind === 'vec') source = arg_type.scalar;
2177
- }
2178
- const source_arg = source !== null ? `, ${JSON.stringify(source)}` : '';
2179
- // Parse the target string: `vec3<u32>` etc. become a vec type so that downstream
2180
- // swizzle/component access compiles correctly. Bare `f32` / `u32` / `i32` stay primitive.
2181
- let resultType;
2182
- const vec_match = target.match(/^vec(\d)<([^>]+)>$/);
2183
- if (vec_match !== null) {
2184
- resultType = { kind: 'vec', size: parseInt(vec_match[1]), scalar: vec_match[2] };
2185
- } else {
2186
- resultType = { kind: 'primitive', name: target };
2187
- }
2188
- return {
2189
- code: `wgsl.bitcast(${args[0]}, ${JSON.stringify(target)}${source_arg})`,
2190
- type: resultType,
2191
- };
2192
- }
2193
-
2194
- // 6) array / array<T, N> constructor.
2195
- if (callee_name === 'array') {
2196
- return { code: `[${args.join(', ')}]`, type: T_UNKNOWN };
2197
- }
2198
-
2199
- // 7) Struct constructor.
2200
- if (c.structs.has(callee_name)) {
2201
- const info = c.structs.get(callee_name);
2202
- const parts = [`(() => { const __s__ = new ${callee_name}();`];
2203
- for (let i = 0; i < info.fields.length && i < args.length; i++) {
2204
- parts.push(`__s__.${info.fields[i].name} = ${args[i]};`);
2205
- }
2206
- parts.push(`return __s__; })()`);
2207
- return { code: parts.join(' '), type: { kind: 'struct', name: callee_name } };
2208
- }
2209
-
2210
- // 8) User-defined function.
2211
- if (c.functions.has(callee_name)) {
2212
- return {
2213
- code: `${callee_name}(${args.join(', ')})`,
2214
- type: c.functions.get(callee_name).returnType,
2215
- };
2216
- }
2217
-
2218
- // 9) WGSL builtins — best-effort type inference for common helpers.
2219
- return {
2220
- code: `wgsl.${callee_name}(${args.join(', ')})`,
2221
- type: inferBuiltinReturnType(callee_name, arg_results),
2222
- };
2223
- }
2224
-
2225
- /**
2226
- * Best-effort return type for WGSL builtins commonly used in compute shaders.
2227
- * Handles the cases needed for accurate swizzle / member-access codegen downstream.
2228
- */
2229
- function inferBuiltinReturnType(name, args) {
2230
- // Element-wise vector-preserving math: result follows the first vector argument.
2231
- const ELEMENT_WISE = new Set([
2232
- 'abs', 'sign', 'floor', 'ceil', 'round', 'trunc', 'fract', 'sqrt', 'inverseSqrt',
2233
- 'exp', 'exp2', 'log', 'log2', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan',
2234
- 'pow', 'atan2', 'min', 'max', 'step', 'clamp', 'saturate', 'mix', 'smoothstep',
2235
- 'normalize', 'reflect', 'fma',
2236
- // not math, but the same shape: the result has the type of its one argument
2237
- 'subgroupBroadcastFirst',
2238
- ]);
2239
- if (ELEMENT_WISE.has(name)) {
2240
- for (const a of args) {
2241
- if (a.type && (a.type.kind === 'vec' || a.type.kind === 'mat')) return a.type;
2242
- }
2243
- if (args[0] && args[0].type) return args[0].type;
2244
- return T_UNKNOWN;
2245
- }
2246
- if (name === 'length' || name === 'distance' || name === 'dot' || name === 'determinant') {
2247
- return { kind: 'primitive', name: 'f32' };
2248
- }
2249
- if (name === 'cross') {
2250
- return args[0] && args[0].type && args[0].type.kind === 'vec'
2251
- ? args[0].type
2252
- : { kind: 'vec', size: 3, scalar: 'f32' };
2253
- }
2254
- if (name === 'all' || name === 'any') return { kind: 'primitive', name: 'bool' };
2255
- if (name === 'select') {
2256
- if (args[0] && args[0].type) return args[0].type;
2257
- if (args[1] && args[1].type) return args[1].type;
2258
- return T_UNKNOWN;
2259
- }
2260
- if (name === 'textureLoad' || name === 'textureSampleLevel' || name === 'textureSample' || name === 'textureSampleGrad') {
2261
- return { kind: 'vec', size: 4, scalar: 'f32' };
2262
- }
2263
- // Both gathers return the four texels of a 2x2 footprint. Saying so here matters more than
2264
- // for most builtins: the shadow chunks immediately swizzle the result (`samples.wzxy`), and
2265
- // an unknown type degrades swizzle codegen downstream.
2266
- if (name === 'textureGather' || name === 'textureGatherCompare') {
2267
- return { kind: 'vec', size: 4, scalar: 'f32' };
2268
- }
2269
- if (name === 'textureDimensions') {
2270
- return { kind: 'vec', size: 2, scalar: 'u32' };
2271
- }
2272
- if (name === 'atomicLoad') return { kind: 'primitive', name: 'i32' };
2273
- if (name === 'unpack2x16unorm' || name === 'unpack2x16snorm' || name === 'unpack2x16float') {
2274
- return { kind: 'vec', size: 2, scalar: 'f32' };
2275
- }
2276
- if (name === 'unpack4x8unorm' || name === 'unpack4x8snorm') {
2277
- return { kind: 'vec', size: 4, scalar: 'f32' };
2278
- }
2279
- if (name === 'pack2x16unorm' || name === 'pack2x16snorm' || name === 'pack2x16float'
2280
- || name === 'pack4x8unorm' || name === 'pack4x8snorm') {
2281
- return { kind: 'primitive', name: 'u32' };
2282
- }
2283
- // extractBits/insertBits return the integer type (scalar or vector) of their first operand.
2284
- if (name === 'extractBits' || name === 'insertBits') {
2285
- return args[0] && args[0].type ? args[0].type : T_UNKNOWN;
2286
- }
2287
- return T_UNKNOWN;
2288
- }
2289
-
2290
- // =========================================================================
2291
- // LHS expressions (for assignments)
2292
- // =========================================================================
2293
-
2294
- function compileLhs(c, node) {
2295
- if (node.type !== 'lhs_expression') {
2296
- return compileExpression(c, node);
2297
- }
2298
-
2299
- // Pointer deref / address-of: `*lhs` / `&lhs`. The emulator models pointers as values, so
2300
- // both collapse to the operand's value. The grammar puts the `*` / `&` as an unnamed leading
2301
- // token with a nested `lhs_expression` named child.
2302
- //
2303
- // A whole-value store through a deref (`*p = v`, with nothing selected out of it) is flagged
2304
- // so {@link emitWriteStatement} writes through the reference rather than rebinding the local
2305
- // — see `wgsl.store_ptr`. A store into part of the pointee (`(*p).x`, `(*p)[i]`) already
2306
- // lands on the shared object and needs no such treatment.
2307
- const prefix = textBeforeFirstNamed(node).trim();
2308
- if (prefix === '*' || prefix === '&') {
2309
- const nested_lhs = namedChildByType(node, 'lhs_expression');
2310
- if (nested_lhs !== null) {
2311
- const inner = compileLhs(c, nested_lhs);
2312
-
2313
- const selects_part = node.namedChildren.some(
2314
- child => child.type === 'component_or_swizzle_specifier'
2315
- );
2316
-
2317
- return {
2318
- ...inner,
2319
- through_pointer: prefix === '*' && !selects_part,
2320
- };
2321
- }
2322
- }
2323
-
2324
- const core = namedChildByType(node, 'core_lhs_expression');
2325
- let code;
2326
- let type;
2327
- if (core !== null) {
2328
- const ident = namedChildByType(core, 'ident');
2329
- if (ident !== null) {
2330
- code = ident.text;
2331
- type = c.scope.lookup(ident.text);
2332
- } else {
2333
- const nested = namedChildByType(core, 'lhs_expression');
2334
- if (nested !== null) {
2335
- const r = compileLhs(c, nested);
2336
- code = `(${r.code})`;
2337
- type = r.type;
2338
- } else {
2339
- code = core.text;
2340
- type = T_UNKNOWN;
2341
- }
2342
- }
2343
- } else {
2344
- code = node.text;
2345
- type = T_UNKNOWN;
2346
- }
2347
-
2348
- for (const child of node.namedChildren) {
2349
- if (child.type === 'component_or_swizzle_specifier') {
2350
- const next = applyAccessSpecifier(c, child, code, type);
2351
- code = next.code;
2352
- type = next.type;
2353
- }
2354
- }
2355
- return { code, type };
2356
- }
1
+ import { assert } from "../../../core/assert.js";
2
+ import LineBuilder from "../../../core/codegen/LineBuilder.js";
3
+
4
+ // =========================================================================
5
+ // Type representation
6
+ // =========================================================================
7
+
8
+ /**
9
+ * @typedef {object} WgslType
10
+ * @property {'primitive' | 'vec' | 'mat' | 'array' | 'struct' | 'texture_2d' | 'sampler' | 'atomic' | 'unknown'} kind
11
+ * @property {string} [name]
12
+ * @property {number} [size]
13
+ * @property {number} [cols]
14
+ * @property {number} [rows]
15
+ * @property {string} [scalar]
16
+ * @property {WgslType} [element]
17
+ * @property {number} [count]
18
+ */
19
+
20
+ const T_UNKNOWN = Object.freeze({ kind: 'unknown' });
21
+ const T_I32 = Object.freeze({ kind: 'primitive', name: 'i32' });
22
+
23
+ const VECTOR_ALIASES = {
24
+ vec2i: { size: 2, scalar: 'i32' },
25
+ vec3i: { size: 3, scalar: 'i32' },
26
+ vec4i: { size: 4, scalar: 'i32' },
27
+ vec2u: { size: 2, scalar: 'u32' },
28
+ vec3u: { size: 3, scalar: 'u32' },
29
+ vec4u: { size: 4, scalar: 'u32' },
30
+ vec2f: { size: 2, scalar: 'f32' },
31
+ vec3f: { size: 3, scalar: 'f32' },
32
+ vec4f: { size: 4, scalar: 'f32' },
33
+ vec2h: { size: 2, scalar: 'f16' },
34
+ vec3h: { size: 3, scalar: 'f16' },
35
+ vec4h: { size: 4, scalar: 'f16' },
36
+ };
37
+
38
+ const PRIMITIVE_NAMES = new Set(['i32', 'u32', 'f32', 'f16', 'bool']);
39
+
40
+ const SWIZZLE_LETTERS = new Set(['x', 'y', 'z', 'w', 'r', 'g', 'b', 'a']);
41
+
42
+ const SWIZZLE_INDEX = { x: 0, y: 1, z: 2, w: 3, r: 0, g: 1, b: 2, a: 3 };
43
+
44
+ /**
45
+ * Build a vec type alias name (e.g., {size:3, scalar:'f32'} → 'vec3f').
46
+ */
47
+ function vecAliasName(size, scalar) {
48
+ const suffix = { i32: 'i', u32: 'u', f32: 'f', f16: 'h' }[scalar] || 'f';
49
+ return `vec${size}${suffix}`;
50
+ }
51
+
52
+ // =========================================================================
53
+ // Scope (symbol table) — tracks variable types for accurate codegen
54
+ // =========================================================================
55
+
56
+ class Scope {
57
+ constructor(parent = null) {
58
+ this.parent = parent;
59
+ /** @type {Map<string, WgslType>} */
60
+ this.symbols = new Map();
61
+ }
62
+
63
+ declare(name, type) {
64
+ this.symbols.set(name, type || T_UNKNOWN);
65
+ }
66
+
67
+ /** @returns {WgslType} */
68
+ lookup(name) {
69
+ if (this.symbols.has(name)) return this.symbols.get(name);
70
+ if (this.parent) return this.parent.lookup(name);
71
+ return T_UNKNOWN;
72
+ }
73
+
74
+ push() {
75
+ return new Scope(this);
76
+ }
77
+ }
78
+
79
+ // =========================================================================
80
+ // Tree-sitter helpers
81
+ // =========================================================================
82
+
83
+ /**
84
+ * Return only the named children of a node (filters out punctuation tokens).
85
+ */
86
+ function namedChildren(node) {
87
+ return node.namedChildren;
88
+ }
89
+
90
+ /**
91
+ * First named child by type, or null.
92
+ */
93
+ function namedChildByType(node, type) {
94
+ for (const c of node.namedChildren) {
95
+ if (c.type === type) return c;
96
+ }
97
+ return null;
98
+ }
99
+
100
+ /**
101
+ * All named children with the given type.
102
+ */
103
+ function namedChildrenByType(node, type) {
104
+ return node.namedChildren.filter(c => c.type === type);
105
+ }
106
+
107
+ /**
108
+ * Extract the source-text slice of `parent` that lies between the end of `left` and the start
109
+ * of `right`. tree-sitter-web's `node.children` does not include unnamed tokens, so this is the
110
+ * reliable way to read operator characters that sit between two named children.
111
+ *
112
+ * @param {Node} parent
113
+ * @param {Node} left
114
+ * @param {Node} right
115
+ * @returns {string}
116
+ */
117
+ function textBetween(parent, left, right) {
118
+ const left_offset = (left.endIndex - parent.startIndex);
119
+ const right_offset = (right.startIndex - parent.startIndex);
120
+ return parent.text.slice(left_offset, right_offset);
121
+ }
122
+
123
+ /**
124
+ * Extract any leading source text in `parent` that precedes its first named child. Used for
125
+ * unary operators (e.g. `-x`, `!cond`) where the operator is an unnamed token.
126
+ *
127
+ * @param {Node} parent
128
+ * @returns {string}
129
+ */
130
+ function textBeforeFirstNamed(parent) {
131
+ if (parent.namedChildCount === 0) return '';
132
+ const first = parent.namedChildren[0];
133
+ const offset = first.startIndex - parent.startIndex;
134
+ return parent.text.slice(0, offset);
135
+ }
136
+
137
+ /**
138
+ * Strip wrapper nodes that have a single named child of an expression-passthrough type.
139
+ * The WGSL grammar has a long precedence chain: expression > relational > shift > additive > ...
140
+ * When an expression is "simple" each level just wraps the next.
141
+ */
142
+ function unwrapExpression(node) {
143
+ const PASSTHROUGH = new Set([
144
+ 'expression',
145
+ 'relational_expression',
146
+ 'shift_expression',
147
+ 'additive_expression',
148
+ 'multiplicative_expression',
149
+ 'unary_expression',
150
+ 'singular_expression',
151
+ 'primary_expression',
152
+ ]);
153
+ while (PASSTHROUGH.has(node.type) && node.namedChildren.length === 1) {
154
+ // `unary_expression` keeps its operator as an unnamed token before the operand. If there
155
+ // is one, don't unwrap — the caller needs to see the unary node so it can apply the op.
156
+ if (node.type === 'unary_expression' && textBeforeFirstNamed(node).trim() !== '') {
157
+ break;
158
+ }
159
+ node = node.namedChildren[0];
160
+ }
161
+ return node;
162
+ }
163
+
164
+ // =========================================================================
165
+ // Type parsing (from `type_specifier` nodes or raw text)
166
+ // =========================================================================
167
+
168
+ /**
169
+ * Parse a `type_specifier` tree-sitter node.
170
+ * @param {Node} node
171
+ * @returns {WgslType}
172
+ */
173
+ function parseTypeSpecifier(node) {
174
+ if (node === null || node === undefined) return T_UNKNOWN;
175
+ if (node.type === 'type_specifier') {
176
+ return parseTypeSpecifier(node.namedChildren[0]);
177
+ }
178
+ if (node.type === 'template_elaborated_ident') {
179
+ const ident = namedChildByType(node, 'ident');
180
+ const ident_text = ident ? ident.text : node.text;
181
+ const template = namedChildByType(node, 'template_list');
182
+ const args = template ? extractTemplateArgs(template) : [];
183
+ return classifyType(ident_text, args, template);
184
+ }
185
+ return T_UNKNOWN;
186
+ }
187
+
188
+ /**
189
+ * Pull the textual template arguments from a `template_list` node, e.g. `<f32, 256>` → ['f32', '256'].
190
+ */
191
+ function extractTemplateArgs(template_list_node) {
192
+ const out = [];
193
+ const comma_list = namedChildByType(template_list_node, 'template_arg_comma_list');
194
+ if (comma_list === null) return out;
195
+ for (const c of comma_list.namedChildren) {
196
+ if (c.type === 'template_arg_expression') {
197
+ out.push(c.text.trim());
198
+ }
199
+ }
200
+ return out;
201
+ }
202
+
203
+ /**
204
+ * @param {string} ident
205
+ * @param {string[]} args — textual template args (may be types or numbers)
206
+ * @param {Node | null} template_list_node — the original node, for parsing nested array element types
207
+ * @returns {WgslType}
208
+ */
209
+ function classifyType(ident, args, template_list_node) {
210
+ if (PRIMITIVE_NAMES.has(ident)) {
211
+ return { kind: 'primitive', name: ident };
212
+ }
213
+ if (VECTOR_ALIASES.hasOwnProperty(ident)) {
214
+ const a = VECTOR_ALIASES[ident];
215
+ return { kind: 'vec', size: a.size, scalar: a.scalar };
216
+ }
217
+ if (ident === 'vec2' || ident === 'vec3' || ident === 'vec4') {
218
+ const size = parseInt(ident.slice(3));
219
+ const scalar = args.length > 0 ? args[0] : 'f32';
220
+ return { kind: 'vec', size, scalar };
221
+ }
222
+ {
223
+ const m = ident.match(/^mat(\d)x(\d)([ifh]?)$/);
224
+ if (m !== null) {
225
+ const cols = parseInt(m[1]);
226
+ const rows = parseInt(m[2]);
227
+ const suffix = m[3];
228
+ let scalar;
229
+ if (suffix === 'f') scalar = 'f32';
230
+ else if (suffix === 'i') scalar = 'i32';
231
+ else if (suffix === 'h') scalar = 'f16';
232
+ else scalar = (args.length > 0 ? args[0] : 'f32');
233
+ return { kind: 'mat', cols, rows, scalar };
234
+ }
235
+ }
236
+ if (ident === 'array') {
237
+ // template arg 0 is the element type (sub-template), arg 1 is the count
238
+ let element = T_UNKNOWN;
239
+ let count = -1;
240
+ if (template_list_node !== null) {
241
+ const comma_list = namedChildByType(template_list_node, 'template_arg_comma_list');
242
+ if (comma_list !== null) {
243
+ const arg_nodes = namedChildrenByType(comma_list, 'template_arg_expression');
244
+ if (arg_nodes.length > 0) {
245
+ // Walk down to find the inner type ident
246
+ const inner_expr = unwrapExpression(arg_nodes[0].namedChildren[0]);
247
+ element = parseTypeSpecifier(inner_expr);
248
+ }
249
+ if (arg_nodes.length > 1) {
250
+ const c = parseInt(arg_nodes[1].text.trim());
251
+ if (!isNaN(c)) count = c;
252
+ }
253
+ }
254
+ }
255
+ return { kind: 'array', element, count };
256
+ }
257
+ if (ident === 'atomic') {
258
+ const inner = args.length > 0 ? args[0] : 'i32';
259
+ return { kind: 'atomic', scalar: inner };
260
+ }
261
+ // Texture types - 1D / 2D / 3D, sampled and storage variants. We
262
+ // collapse all of them to a single kind because the intrinsics in
263
+ // WGSLLanguageCore dispatch on the bound CPUBitmapData's
264
+ // `dimensionality`, not on the static WGSL type. (The 'texture_2d'
265
+ // kind label is preserved for backward compatibility with any
266
+ // downstream consumer that may grep for it.)
267
+ if (
268
+ ident === 'texture_2d' || ident === 'texture_storage_2d'
269
+ || ident === 'texture_1d' || ident === 'texture_storage_1d'
270
+ || ident === 'texture_3d' || ident === 'texture_storage_3d'
271
+ || ident === 'texture_2d_array'
272
+ ) {
273
+ const scalar = args.length > 0 ? args[0] : 'f32';
274
+ return { kind: 'texture_2d', scalar };
275
+ }
276
+ if (ident === 'sampler') {
277
+ return { kind: 'sampler' };
278
+ }
279
+ if (ident === 'ptr') {
280
+ // Pointer — peel off the address space and read the inner type.
281
+ if (template_list_node !== null) {
282
+ const comma_list = namedChildByType(template_list_node, 'template_arg_comma_list');
283
+ if (comma_list !== null) {
284
+ const arg_nodes = namedChildrenByType(comma_list, 'template_arg_expression');
285
+ if (arg_nodes.length >= 2) {
286
+ const inner_expr = unwrapExpression(arg_nodes[1].namedChildren[0]);
287
+ return parseTypeSpecifier(inner_expr);
288
+ }
289
+ }
290
+ }
291
+ return T_UNKNOWN;
292
+ }
293
+ // Otherwise: assume it's a struct name.
294
+ return { kind: 'struct', name: ident };
295
+ }
296
+
297
+ // =========================================================================
298
+ // Literal handling — strip WGSL type suffixes for JS
299
+ // =========================================================================
300
+
301
+ /**
302
+ * Convert a WGSL literal token (e.g. `7u`, `1.0f`, `true`) to its JS equivalent.
303
+ *
304
+ * Stripping the type suffix is shape-sensitive: integer literals only carry `u`/`i`, and float
305
+ * literals only carry `f`/`h`. Crucially, a hex int like `0x0300f00f` ends in a hex digit `f`
306
+ * that must NOT be treated as a suffix — only an `f` after a decimal-format float counts.
307
+ */
308
+ function literalToJs(text) {
309
+ const t = text.trim();
310
+ if (t === 'true' || t === 'false') return t;
311
+
312
+ // Hex int literals: `0xNN[ui]?`. Only `u`/`i` are real suffixes here; trailing `f`/`h` are
313
+ // hex digits.
314
+ if (/^0[xX]/.test(t)) {
315
+ return t.replace(/[ui]$/, '');
316
+ }
317
+
318
+ // Decimal int / float. Float forms have a `.`, `e`/`E`, or end in `f`/`h`. Int forms don't.
319
+ if (/[.eE]/.test(t)) {
320
+ return t.replace(/[fh]$/, '');
321
+ }
322
+
323
+ // Plain integer (no `.`, no exponent): strip `u`/`i`/`f`/`h` (the latter two appear only on
324
+ // forms like `1f` which WGSL allows as a float written without a decimal point).
325
+ return t.replace(/[uifh]$/, '');
326
+ }
327
+
328
+ /**
329
+ * The tracked type of a literal token, as far as its own spelling settles it.
330
+ *
331
+ * Only an explicit suffix is honoured. A bare `2043` is WGSL's AbstractInt, which takes its
332
+ * concrete type from context this compiler does not track, so it stays unknown rather than being
333
+ * guessed at.
334
+ *
335
+ * The suffix matters because operator selection reads these types. `index / 2043u` is truncating
336
+ * integer division in WGSL and plain division in JavaScript, and the two differ for every pair
337
+ * where the divisor does not divide the dividend — which is what a paged table's
338
+ * `index / elements_per_page` is. Left untyped, the generated code returned a fractional page
339
+ * index, indexed the page-address table with it, and read `undefined` back as zero: every record
340
+ * past the first page slot came out of the emulator zero-filled, with nothing to say it had.
341
+ *
342
+ * @param {string} text
343
+ * @returns {WgslType}
344
+ */
345
+ function literalType(text) {
346
+ const t = text.trim();
347
+
348
+ if (t === 'true' || t === 'false') {
349
+ return { kind: 'primitive', name: 'bool' };
350
+ }
351
+
352
+ if (/[ui]$/.test(t)) {
353
+ return { kind: 'primitive', name: t.endsWith('u') ? 'u32' : 'i32' };
354
+ }
355
+
356
+ // `f`/`h` are only suffixes on a decimal-format literal; on a hex int they are digits.
357
+ if (!/^0[xX]/.test(t) && /[fh]$/.test(t)) {
358
+ return { kind: 'primitive', name: t.endsWith('f') ? 'f32' : 'f16' };
359
+ }
360
+
361
+ if (/[.eE]/.test(t) && !/^0[xX]/.test(t)) {
362
+ return { kind: 'primitive', name: 'f32' };
363
+ }
364
+
365
+ return T_UNKNOWN;
366
+ }
367
+
368
+ // =========================================================================
369
+ // Operator handling
370
+ // =========================================================================
371
+
372
+ /**
373
+ * Map a WGSL binary operator to a `wgsl.*` runtime helper or to a raw JS operator.
374
+ * Arithmetic must go through wgsl.* so vectors are handled element-wise.
375
+ */
376
+ const ARITHMETIC_OP = {
377
+ '+': 'add',
378
+ '-': 'sub',
379
+ '*': 'mul',
380
+ '/': 'div',
381
+ '%': 'rem',
382
+ };
383
+
384
+ const RAW_BINARY_OPS = new Set(['<', '>', '<=', '>=', '==', '!=', '&&', '||', '&', '|', '^', '<<', '>>']);
385
+
386
+ /**
387
+ * Arithmetic operators that wrap at 32 bits when WGSL types the result as an integer. Mapped to the
388
+ * helper-name prefix; the scalar type (`u32` / `i32`) is appended at the call site.
389
+ */
390
+ const INTEGER_WRAPPING_OP = {
391
+ '+': 'add',
392
+ '-': 'sub',
393
+ '*': 'mul',
394
+ };
395
+
396
+ // =========================================================================
397
+ // Compiler context — central state during compilation
398
+ // =========================================================================
399
+
400
+ class Compiler {
401
+ constructor() {
402
+ /** @type {LineBuilder} */
403
+ this.out = new LineBuilder();
404
+
405
+ /** @type {Scope} */
406
+ this.scope = new Scope();
407
+
408
+ /** @type {Map<string, { name: string, fields: Array<{ name: string, type: WgslType }> }>} */
409
+ this.structs = new Map();
410
+
411
+ /** @type {Map<string, { name: string, returnType: WgslType, params: Array<{ name: string, type: WgslType }> }>} */
412
+ this.functions = new Map();
413
+
414
+ /** @type {Array<{ name: string, type: WgslType, group: number, binding: number, addressSpace: string | null, accessMode: string | null }>} */
415
+ this.globalBindings = [];
416
+
417
+ /** @type {Array<{ name: string, type: WgslType }>} */
418
+ this.workgroupVars = [];
419
+
420
+ /**
421
+ * Module-scope `var<private>` and `const` declarations, in declaration order.
422
+ * Each entry is `{ name, kind: 'let' | 'const', code, init }` — `code` is the JS line
423
+ * emitted verbatim; `name` and `kind` are also tracked so the returned module object can
424
+ * expose them as getter/setter (or read-only) properties for testing, and `init` is the
425
+ * initializer expression on its own, which the private-state reset re-emits for the `let`
426
+ * ones (a `const` cannot be reassigned, and no invocation can disturb it).
427
+ * @type {Array<{ name: string, kind: 'let' | 'const', code: string, init: string }>}
428
+ */
429
+ this.privateGlobals = [];
430
+
431
+ /** @type {{ name: string, params: Array<{ name: string, type: WgslType, builtin: string | null }>, workgroup_size: number[] } | null} */
432
+ this.entry = null;
433
+
434
+ /**
435
+ * Label of the enclosing `loop` body block when that loop has a `continuing`
436
+ * block: WGSL `continue` must transfer control INTO the continuing block, so it
437
+ * is emitted as `break <label>` out of the labeled body block. `null` when the
438
+ * nearest enclosing loop has no continuing block (plain JS `continue` suffices).
439
+ * Loop emitters save/clear/restore this around nested loop bodies.
440
+ * @type {string | null}
441
+ */
442
+ this.continue_label = null;
443
+
444
+ /**
445
+ * Monotonic counter used to generate unique loop-body labels.
446
+ * @type {number}
447
+ */
448
+ this.loop_label_counter = 0;
449
+ }
450
+
451
+ error(node, message) {
452
+ throw new Error(`WGSL compiler: ${message} (at ${node ? node.type : '<unknown>'} : ${node ? JSON.stringify(node.text.slice(0, 60)) : ''})`);
453
+ }
454
+ }
455
+
456
+ // =========================================================================
457
+ // Top-level compile entry point
458
+ // =========================================================================
459
+
460
+ export class WGSLJavaScriptCompiler {
461
+
462
+ /**
463
+ * Compile a tree-sitter WGSL parse tree to a JS source string.
464
+ *
465
+ * The output is a JavaScript expression: a function `factory(wgsl)` that returns
466
+ * a shader module object. The shape depends on what the WGSL declared:
467
+ *
468
+ * - **Always present:** every top-level user function (e.g. `module.random()`),
469
+ * every struct class (e.g. `module.Foo`), a getter/setter property for each
470
+ * `var<private>` and getter for each module-scope `const`, and `reset_private()`,
471
+ * which returns every `var<private>` to its start-of-invocation value.
472
+ * - **Compute-shader fields (only when a `@compute` entry is present):**
473
+ * ```
474
+ * {
475
+ * entry_name: string,
476
+ * workgroup_size: [x, y, z],
477
+ * param_names: string[],
478
+ * entry: (bindings, workgroup, ...args) => void,
479
+ * create_workgroup_state: () => object,
480
+ * }
481
+ * ```
482
+ *
483
+ * Without a `@compute` entry the result is a "library" module — useful for testing
484
+ * pure WGSL helper code chunks (hashes, packing, math) one function at a time.
485
+ *
486
+ * @param {Tree} ast — tree-sitter WGSL AST (returned by `WGSLParser.parse`)
487
+ * @returns {string}
488
+ */
489
+ compile(ast) {
490
+ const c = new Compiler();
491
+ compileTranslationUnit(c, ast.rootNode);
492
+ return c.out.build();
493
+ }
494
+ }
495
+
496
+ // =========================================================================
497
+ // Translation unit (root)
498
+ // =========================================================================
499
+
500
+ /**
501
+ * @param {Compiler} c
502
+ * @param {Node} root
503
+ */
504
+ function compileTranslationUnit(c, root) {
505
+ assert.equal(root.type, 'translation_unit', 'root must be translation_unit');
506
+
507
+ // First pass — collect top-level decls so forward references and global scope resolve.
508
+ for (const node of root.namedChildren) {
509
+ switch (node.type) {
510
+ case 'struct_decl':
511
+ collectStruct(c, node);
512
+ break;
513
+ case 'function_decl':
514
+ collectFunction(c, node);
515
+ break;
516
+ case 'global_variable_decl':
517
+ collectGlobalVariable(c, node);
518
+ break;
519
+ case 'global_value_decl':
520
+ collectGlobalValue(c, node);
521
+ break;
522
+ }
523
+ }
524
+
525
+ // Emit the wrapper: `(wgsl) => { ... }`
526
+ c.out.add('(wgsl) => {');
527
+ c.out.indent();
528
+
529
+ // 1) Struct classes — must be defined before any user fn that references them or before
530
+ // the workgroup state factory (which may need to construct struct instances).
531
+ for (const node of root.namedChildren) {
532
+ if (node.type === 'struct_decl') emitStruct(c, node);
533
+ }
534
+
535
+ // 2) Module-scope `let` slots for bindings and workgroup vars. The entry wrapper assigns
536
+ // these on each dispatch so that user functions see them via their enclosing closure
537
+ // — WGSL functions can read globals freely.
538
+ for (const b of c.globalBindings) {
539
+ c.out.add(`let ${b.name} = undefined;`);
540
+ }
541
+ for (const wv of c.workgroupVars) {
542
+ c.out.add(`let ${wv.name} = undefined;`);
543
+ }
544
+
545
+ // 3) Module-scope `var<private>` and `const` declarations.
546
+ for (const g of c.privateGlobals) {
547
+ c.out.add(g.code);
548
+ }
549
+
550
+ // 4) User-defined functions (including the @compute entry, if any).
551
+ for (const node of root.namedChildren) {
552
+ if (node.type === 'function_decl') emitFunction(c, node);
553
+ }
554
+
555
+ // 5) The private-state reset. Emitted for every module, library ones included: a
556
+ // `var<private>` is per-invocation state on the GPU, and the JS `let` above outlives the
557
+ // dispatch that wrote it, so something has to put the initial values back.
558
+ emitPrivateStateReset(c);
559
+
560
+ // 6) Entry-stage support code: the dispatch wrapper for any entry, plus a workgroup-state
561
+ // factory for @compute entries (fragment shaders have no var<workgroup>). Skipped entirely
562
+ // in library mode (no entry attribute) so the resulting module is just a bag of pure
563
+ // functions and globals.
564
+ if (c.entry !== null) {
565
+ if (c.entry.stage === 'compute') {
566
+ emitWorkgroupStateFactory(c);
567
+ }
568
+ emitEntryWrapper(c);
569
+ }
570
+
571
+ // 7) Build and return the module object.
572
+ emitModuleReturn(c);
573
+
574
+ c.out.dedent();
575
+ c.out.add('}');
576
+ }
577
+
578
+ /**
579
+ * Emit the `return { ... }` block of the factory. Every top-level user function and struct
580
+ * class is exposed by name; module-scope `var<private>` slots become getter/setter properties
581
+ * (so tests can inspect or seed RNG state etc.), and module-scope `const`s become read-only
582
+ * getters. `reset_private` is always included. When the WGSL had a `@compute` entry, the dispatch
583
+ * trio (`entry_name`, `workgroup_size`, `param_names`, `entry`, `create_workgroup_state`) is
584
+ * included too.
585
+ */
586
+ function emitModuleReturn(c) {
587
+ c.out.add('const __module__ = {};');
588
+
589
+ for (const fn_name of c.functions.keys()) {
590
+ c.out.add(`__module__.${fn_name} = ${fn_name};`);
591
+ }
592
+ for (const struct_name of c.structs.keys()) {
593
+ c.out.add(`__module__.${struct_name} = ${struct_name};`);
594
+ }
595
+
596
+ for (const g of c.privateGlobals) {
597
+ if (g.kind === 'let') {
598
+ c.out.add(`Object.defineProperty(__module__, ${JSON.stringify(g.name)}, { get: () => ${g.name}, set: (v) => { ${g.name} = v; } });`);
599
+ } else {
600
+ c.out.add(`Object.defineProperty(__module__, ${JSON.stringify(g.name)}, { get: () => ${g.name} });`);
601
+ }
602
+ }
603
+
604
+ c.out.add('__module__.reset_private = __reset_private__;');
605
+
606
+ if (c.entry !== null) {
607
+ c.out.add(`__module__.entry_name = ${JSON.stringify(c.entry.name)};`);
608
+ c.out.add(`__module__.stage = ${JSON.stringify(c.entry.stage)};`);
609
+ c.out.add(`__module__.param_names = [${c.entry.params.map(p => JSON.stringify(p.name)).join(', ')}];`);
610
+ c.out.add(`__module__.entry = __entry__;`);
611
+ if (c.entry.stage === 'compute') {
612
+ c.out.add(`__module__.workgroup_size = [${c.entry.workgroup_size.join(', ')}];`);
613
+ c.out.add(`__module__.create_workgroup_state = __create_workgroup_state__;`);
614
+ }
615
+ }
616
+
617
+ c.out.add('return __module__;');
618
+ }
619
+
620
+ // =========================================================================
621
+ // Struct collection + emission
622
+ // =========================================================================
623
+
624
+ /**
625
+ * @param {Compiler} c
626
+ * @param {Node} node
627
+ */
628
+ function collectStruct(c, node) {
629
+ const ident = namedChildByType(node, 'ident');
630
+ const name = ident.text;
631
+
632
+ const body = namedChildByType(node, 'struct_body_decl');
633
+ const members = namedChildrenByType(body, 'struct_member');
634
+
635
+ /** @type {Array<{ name: string, type: WgslType }>} */
636
+ const fields = [];
637
+
638
+ for (const m of members) {
639
+ const member_ident = namedChildByType(m, 'member_ident');
640
+ const type_spec = namedChildByType(m, 'type_specifier');
641
+ fields.push({
642
+ name: member_ident.text,
643
+ type: parseTypeSpecifier(type_spec),
644
+ });
645
+ }
646
+
647
+ c.structs.set(name, { name, fields });
648
+ }
649
+
650
+ /**
651
+ * @param {Compiler} c
652
+ * @param {Node} node
653
+ */
654
+ function emitStruct(c, node) {
655
+ const ident = namedChildByType(node, 'ident');
656
+ const name = ident.text;
657
+ const info = c.structs.get(name);
658
+
659
+ c.out.add(`class ${name} {`);
660
+ c.out.indent();
661
+ c.out.add('constructor() {');
662
+ c.out.indent();
663
+ for (const f of info.fields) {
664
+ c.out.add(`this.${f.name} = ${defaultValueForType(f.type)};`);
665
+ }
666
+ c.out.dedent();
667
+ c.out.add('}');
668
+ c.out.dedent();
669
+ c.out.add('}');
670
+ }
671
+
672
+ /**
673
+ * Initialiser expression for a fresh value of the given type.
674
+ * @param {WgslType} type
675
+ * @returns {string}
676
+ */
677
+ function defaultValueForType(type) {
678
+ if (type === undefined || type === null) return 'undefined';
679
+ switch (type.kind) {
680
+ case 'primitive':
681
+ return type.name === 'bool' ? 'false' : '0';
682
+ case 'vec':
683
+ return `wgsl.${vecAliasName(type.size, type.scalar)}()`;
684
+ case 'mat':
685
+ return `new Float32Array(${type.cols * type.rows})`;
686
+ case 'array': {
687
+ if (type.count > 0) {
688
+ if (type.element.kind === 'primitive') {
689
+ const ctor = arrayCtorForScalar(type.element.name);
690
+ return `new ${ctor}(${type.count})`;
691
+ }
692
+ // For non-primitive elements (vec / mat / struct / nested array) build the
693
+ // array eagerly so `out[i]` is a fresh, mutable element rather than `undefined`.
694
+ return `Array.from({ length: ${type.count} }, () => ${defaultValueForType(type.element)})`;
695
+ }
696
+ return '[]';
697
+ }
698
+ case 'struct':
699
+ return `new ${type.name}()`;
700
+ case 'atomic':
701
+ return '{ value: 0 }';
702
+ default:
703
+ return 'undefined';
704
+ }
705
+ }
706
+
707
+ function arrayCtorForScalar(name) {
708
+ switch (name) {
709
+ case 'f32':
710
+ return 'Float32Array';
711
+ case 'f16':
712
+ return 'Float16Array';
713
+ case 'i32':
714
+ return 'Int32Array';
715
+ case 'u32':
716
+ return 'Uint32Array';
717
+ default:
718
+ return 'Array';
719
+ }
720
+ }
721
+
722
+ // =========================================================================
723
+ // Function collection + emission
724
+ // =========================================================================
725
+
726
+ /**
727
+ * @param {Compiler} c
728
+ * @param {Node} node
729
+ */
730
+ function collectFunction(c, node) {
731
+ const header = namedChildByType(node, 'function_header');
732
+ const ident = namedChildByType(header, 'ident');
733
+ const name = ident.text;
734
+
735
+ const param_list_node = namedChildByType(header, 'param_list');
736
+ /** @type {Array<{ name: string, type: WgslType, builtin: string | null }>} */
737
+ const params = [];
738
+ if (param_list_node !== null) {
739
+ const param_nodes = namedChildrenByType(param_list_node, 'param');
740
+ for (const p of param_nodes) {
741
+ const p_ident = namedChildByType(p, 'ident');
742
+ const p_type = namedChildByType(p, 'type_specifier');
743
+ const builtin = extractBuiltin(p);
744
+ params.push({
745
+ name: p_ident.text,
746
+ type: parseTypeSpecifier(p_type),
747
+ builtin,
748
+ });
749
+ }
750
+ }
751
+
752
+ // Optional return type — `template_elaborated_ident` directly under the header.
753
+ let returnType = T_UNKNOWN;
754
+ for (const child of header.namedChildren) {
755
+ if (child.type === 'template_elaborated_ident') {
756
+ returnType = parseTypeSpecifier(child);
757
+ }
758
+ }
759
+
760
+ c.functions.set(name, { name, returnType, params: params.map(p => ({ name: p.name, type: p.type })) });
761
+
762
+ // Detect the entry attribute (@compute, @fragment, or @vertex) and capture stage-specific
763
+ // metadata. The emulator supports compute and fragment entries; vertex is rejected because
764
+ // ImageShader testing in the emulator skips the vertex phase intentionally.
765
+ const attrs = namedChildrenByType(node, 'attribute');
766
+ let stage = null;
767
+ let workgroup_size = [1, 1, 1];
768
+ for (const a of attrs) {
769
+ const inner = a.namedChildren[0];
770
+ if (inner.type === 'compute_attr') {
771
+ stage = 'compute';
772
+ } else if (inner.type === 'fragment_attr') {
773
+ stage = 'fragment';
774
+ } else if (inner.type === 'vertex_attr') {
775
+ throw new Error(`WGSL compiler: @vertex stage is not supported by the emulator (function '${name}'). For ImageShader testing the vertex phase is skipped — invoke the @fragment entry directly.`);
776
+ } else if (inner.type === 'workgroup_size_attr') {
777
+ const sizes = namedChildrenByType(inner, 'expression').map(e => parseInt(e.text.trim()) | 0);
778
+ for (let i = 0; i < sizes.length && i < 3; i++) {
779
+ workgroup_size[i] = sizes[i];
780
+ }
781
+ }
782
+ }
783
+
784
+ if (stage !== null) {
785
+ if (c.entry !== null) {
786
+ throw new Error(`WGSL compiler: multiple @${stage} entry functions found ('${c.entry.name}' and '${name}')`);
787
+ }
788
+ c.entry = { name, params, stage, workgroup_size };
789
+ }
790
+ }
791
+
792
+ /**
793
+ * Pull the `@builtin(name)` attribute name from a function param node, if present.
794
+ */
795
+ function extractBuiltin(param_node) {
796
+ for (const a of namedChildrenByType(param_node, 'attribute')) {
797
+ const inner = a.namedChildren[0];
798
+ if (inner.type === 'builtin_attr') {
799
+ const name_node = namedChildByType(inner, 'builtin_value_name');
800
+ if (name_node !== null) return name_node.text;
801
+ }
802
+ }
803
+ return null;
804
+ }
805
+
806
+ /**
807
+ * @param {Compiler} c
808
+ * @param {Node} node
809
+ */
810
+ function emitFunction(c, node) {
811
+ const header = namedChildByType(node, 'function_header');
812
+ const ident = namedChildByType(header, 'ident');
813
+ const name = ident.text;
814
+ const info = c.functions.get(name);
815
+
816
+ const param_list_node = namedChildByType(header, 'param_list');
817
+ const param_nodes = param_list_node !== null ? namedChildrenByType(param_list_node, 'param') : [];
818
+
819
+ const param_names = param_nodes.map(p => namedChildByType(p, 'ident').text);
820
+
821
+ c.out.add(`function ${name}(${param_names.join(', ')}) {`);
822
+ c.out.indent();
823
+
824
+ c.scope = c.scope.push();
825
+ for (let i = 0; i < param_nodes.length; i++) {
826
+ c.scope.declare(param_names[i], info.params[i].type);
827
+ }
828
+
829
+ const body = namedChildByType(node, 'compound_statement');
830
+ emitCompoundStatementBody(c, body);
831
+
832
+ c.scope = c.scope.parent;
833
+
834
+ c.out.dedent();
835
+ c.out.add('}');
836
+ }
837
+
838
+ // =========================================================================
839
+ // Globals — bindings, workgroup vars, private/const
840
+ // =========================================================================
841
+
842
+ /**
843
+ * Inspect a `global_variable_decl` and record its metadata.
844
+ * @param {Compiler} c
845
+ * @param {Node} node
846
+ */
847
+ function collectGlobalVariable(c, node) {
848
+ const var_decl = namedChildByType(node, 'variable_decl');
849
+ if (var_decl === null) {
850
+ collectGlobalValue(c, node);
851
+ return;
852
+ }
853
+
854
+ const template = namedChildByType(var_decl, 'template_list');
855
+ const opt_ident = namedChildByType(var_decl, 'optionally_typed_ident');
856
+ const var_ident = namedChildByType(opt_ident, 'ident');
857
+ const type_spec = namedChildByType(opt_ident, 'type_specifier');
858
+ const name = var_ident.text;
859
+ const type = parseTypeSpecifier(type_spec);
860
+
861
+ let address_space = null;
862
+ let access_mode = null;
863
+ if (template !== null) {
864
+ const args = extractTemplateArgs(template);
865
+ address_space = args[0] || null;
866
+ access_mode = args[1] || null;
867
+ }
868
+
869
+ let group = null;
870
+ let binding = null;
871
+ for (const a of namedChildrenByType(node, 'attribute')) {
872
+ const inner = a.namedChildren[0];
873
+ if (inner.type === 'group_attr') {
874
+ const expr = namedChildByType(inner, 'expression');
875
+ group = parseInt(expr.text.trim());
876
+ } else if (inner.type === 'binding_attr') {
877
+ const expr = namedChildByType(inner, 'expression');
878
+ binding = parseInt(expr.text.trim());
879
+ }
880
+ }
881
+
882
+ c.scope.declare(name, type);
883
+
884
+ // `var<immediate>` carries neither attribute — immediate data is part of the shader interface
885
+ // but is not a bound resource, so WGSL gives it no @group/@binding. It still arrives from
886
+ // outside the shader, so it is a binding as far as the emulator is concerned, and binding here
887
+ // is what keeps it out of the `privateGlobals` branch below. Falling through to that branch
888
+ // would default-initialise it and every spec reading it would quietly see zeroes.
889
+ const is_immediate = address_space === 'immediate';
890
+
891
+ if (is_immediate || (group !== null && binding !== null)) {
892
+ c.globalBindings.push({
893
+ name, type, group, binding, addressSpace: address_space, accessMode: access_mode,
894
+ });
895
+ return;
896
+ }
897
+
898
+ if (address_space === 'workgroup') {
899
+ c.workgroupVars.push({ name, type });
900
+ return;
901
+ }
902
+
903
+ // Private / module-scope `var<private>`. If the WGSL has an explicit initializer
904
+ // (e.g. `var<private> rnd_state: u32 = 2891336453u`), use it; otherwise fall back
905
+ // to the type's default value.
906
+ const initializer_node = namedChildByType(node, 'expression');
907
+ const init_code = initializer_node !== null
908
+ ? compileExpression(c, initializer_node).code
909
+ : defaultValueForType(type);
910
+ c.privateGlobals.push({
911
+ name,
912
+ kind: 'let',
913
+ code: `let ${name} = ${init_code};`,
914
+ init: init_code,
915
+ });
916
+ }
917
+
918
+ /**
919
+ * Inspect a `global_value_decl` (`const x = …;` / `let x = …;` at module scope) and record it.
920
+ * @param {Compiler} c
921
+ * @param {Node} node
922
+ */
923
+ function collectGlobalValue(c, node) {
924
+ let ident = null;
925
+ let value_node = null;
926
+ let type_node = null;
927
+ for (const child of node.namedChildren) {
928
+ if (child.type === 'optionally_typed_ident') {
929
+ ident = namedChildByType(child, 'ident');
930
+ type_node = namedChildByType(child, 'type_specifier');
931
+ } else if (child.type === 'expression') {
932
+ value_node = child;
933
+ }
934
+ }
935
+ if (ident === null || value_node === null) return;
936
+ const name = ident.text;
937
+ let type = type_node !== null ? parseTypeSpecifier(type_node) : T_UNKNOWN;
938
+
939
+ const expr = compileExpression(c, value_node);
940
+
941
+ // Infer the type from the RHS when there's no explicit annotation (mirrors the local
942
+ // `let`/`var` path). Without this a `const M = mat3x3<f32>(...)` is left T_UNKNOWN, so a
943
+ // later `M * v` can't tell M is a matrix and emits a vector multiply → length mismatch.
944
+ if (type === T_UNKNOWN || type.kind === 'unknown') {
945
+ type = expr.type;
946
+ }
947
+
948
+ c.scope.declare(name, type);
949
+ c.privateGlobals.push({
950
+ name,
951
+ kind: 'const',
952
+ code: `const ${name} = ${expr.code};`,
953
+ init: expr.code,
954
+ });
955
+ }
956
+
957
+ // =========================================================================
958
+ // Workgroup state factory + entry wrapper
959
+ // =========================================================================
960
+
961
+ /**
962
+ * Emit `__reset_private__()`, which restores every module-scope `var<private>` to the value it
963
+ * would have at the start of a WGSL invocation — its declared initializer, or the type's zero
964
+ * value when it was declared without one.
965
+ *
966
+ * This is the private-address-space counterpart of {@link emitWorkgroupStateFactory}. The
967
+ * generated module keeps its private slots in `let` bindings that live as long as the module, so
968
+ * without this a second dispatch would start from whatever the first one left behind — state the
969
+ * GPU never carries.
970
+ *
971
+ * Module-scope `const`s are skipped: they are immutable in the generated JS and no invocation can
972
+ * disturb them.
973
+ *
974
+ * @param {Compiler} c
975
+ */
976
+ function emitPrivateStateReset(c) {
977
+ c.out.add('function __reset_private__() {');
978
+ c.out.indent();
979
+ for (const g of c.privateGlobals) {
980
+ if (g.kind !== 'let') continue;
981
+ c.out.add(`${g.name} = ${g.init};`);
982
+ }
983
+ c.out.dedent();
984
+ c.out.add('}');
985
+ }
986
+
987
+ function emitWorkgroupStateFactory(c) {
988
+ c.out.add('function __create_workgroup_state__() {');
989
+ c.out.indent();
990
+ c.out.add('return {');
991
+ c.out.indent();
992
+ for (const wv of c.workgroupVars) {
993
+ c.out.add(`${wv.name}: ${defaultValueForType(wv.type)},`);
994
+ }
995
+ c.out.dedent();
996
+ c.out.add('};');
997
+ c.out.dedent();
998
+ c.out.add('}');
999
+ }
1000
+
1001
+ function emitEntryWrapper(c) {
1002
+ const param_names = c.entry.params.map(p => p.name);
1003
+ const is_compute = c.entry.stage === 'compute';
1004
+ // Compute entries take a per-dispatch workgroup-state object; fragment entries don't have
1005
+ // var<workgroup> at all, so the wrapper signature drops it.
1006
+ const fixed_args = is_compute ? '__bindings__, __workgroup__' : '__bindings__';
1007
+ const all_args = param_names.length > 0 ? `${fixed_args}, ${param_names.join(', ')}` : fixed_args;
1008
+
1009
+ c.out.add(`function __entry__(${all_args}) {`);
1010
+ c.out.indent();
1011
+
1012
+ // Refresh each module-scope binding/workgroup slot before invoking the user's main.
1013
+ // User functions read these via their enclosing closure, so this assignment makes the
1014
+ // current dispatch's bindings visible to every helper they may call.
1015
+ for (const b of c.globalBindings) {
1016
+ c.out.add(`${b.name} = __bindings__.${b.name};`);
1017
+ }
1018
+ if (is_compute) {
1019
+ for (const wv of c.workgroupVars) {
1020
+ c.out.add(`${wv.name} = __workgroup__.${wv.name};`);
1021
+ }
1022
+ }
1023
+
1024
+ if (is_compute && c.workgroupVars.length > 0) {
1025
+ // The lanes of one workgroup are dispatched one call at a time, and a scalar
1026
+ // `var<workgroup>` a lane assigns lives in the module-scope slot above — a copy of the
1027
+ // state object's field, not the field. Writing it back is what lets the next lane see it,
1028
+ // which is the whole point of workgroup memory (an array slot already persists, since the
1029
+ // slot holds a reference and the lane mutates it in place).
1030
+ c.out.add(`const __result__ = ${c.entry.name}(${param_names.join(', ')});`);
1031
+ for (const wv of c.workgroupVars) {
1032
+ c.out.add(`__workgroup__.${wv.name} = ${wv.name};`);
1033
+ }
1034
+ c.out.add('return __result__;');
1035
+ } else {
1036
+ c.out.add(`return ${c.entry.name}(${param_names.join(', ')});`);
1037
+ }
1038
+
1039
+ c.out.dedent();
1040
+ c.out.add('}');
1041
+ }
1042
+
1043
+ // =========================================================================
1044
+ // Statements
1045
+ // =========================================================================
1046
+
1047
+ /**
1048
+ * Emit only the inside of a compound_statement (no `{` / `}`).
1049
+ * @param {Compiler} c
1050
+ * @param {Node} node — `compound_statement`
1051
+ */
1052
+ function emitCompoundStatementBody(c, node) {
1053
+ for (const child of node.namedChildren) {
1054
+ if (child.type === 'statement') {
1055
+ emitStatement(c, child);
1056
+ }
1057
+ }
1058
+ }
1059
+
1060
+ function emitCompoundStatement(c, node) {
1061
+ c.out.add('{');
1062
+ c.out.indent();
1063
+ c.scope = c.scope.push();
1064
+ emitCompoundStatementBody(c, node);
1065
+ c.scope = c.scope.parent;
1066
+ c.out.dedent();
1067
+ c.out.add('}');
1068
+ }
1069
+
1070
+ /**
1071
+ * Emit a single `statement` node (its child is the actual statement kind).
1072
+ * @param {Compiler} c
1073
+ * @param {Node} node
1074
+ */
1075
+ function emitStatement(c, node) {
1076
+ const inner = node.namedChildren[0];
1077
+ switch (inner.type) {
1078
+ case 'variable_or_value_statement':
1079
+ emitVariableOrValueStatement(c, inner);
1080
+ break;
1081
+ case 'variable_updating_statement':
1082
+ emitVariableUpdatingStatement(c, inner);
1083
+ break;
1084
+ case 'return_statement':
1085
+ emitReturnStatement(c, inner);
1086
+ break;
1087
+ case 'if_statement':
1088
+ emitIfStatement(c, inner);
1089
+ break;
1090
+ case 'for_statement':
1091
+ emitForStatement(c, inner);
1092
+ break;
1093
+ case 'while_statement':
1094
+ emitWhileStatement(c, inner);
1095
+ break;
1096
+ case 'loop_statement':
1097
+ emitLoopStatement(c, inner);
1098
+ break;
1099
+ case 'switch_statement':
1100
+ emitSwitchStatement(c, inner);
1101
+ break;
1102
+ case 'break_statement':
1103
+ c.out.add('break;');
1104
+ break;
1105
+ case 'continue_statement':
1106
+ // Inside a `loop` with a `continuing` block, `continue` must run the
1107
+ // continuing block: it is emitted as a break out of the labeled body block
1108
+ // (the continuing code follows that block inside the same `while`).
1109
+ if (c.continue_label !== null) {
1110
+ c.out.add(`break ${c.continue_label};`);
1111
+ } else {
1112
+ c.out.add('continue;');
1113
+ }
1114
+ break;
1115
+ case 'discard_statement':
1116
+ c.out.add('return;');
1117
+ break;
1118
+ case 'compound_statement':
1119
+ emitCompoundStatement(c, inner);
1120
+ break;
1121
+ case 'func_call_statement':
1122
+ emitFunctionCallStatement(c, inner);
1123
+ break;
1124
+ case 'const_assert_statement':
1125
+ // Skipped at runtime.
1126
+ break;
1127
+ default:
1128
+ c.error(inner, `unsupported statement: ${inner.type}`);
1129
+ }
1130
+ }
1131
+
1132
+ /**
1133
+ * `let x = expr;` or `var x: T = expr;` or `var x: T;`
1134
+ */
1135
+ function emitVariableOrValueStatement(c, node) {
1136
+ const var_decl = namedChildByType(node, 'variable_decl');
1137
+ const opt_ident_direct = namedChildByType(node, 'optionally_typed_ident');
1138
+ const expr_node = namedChildByType(node, 'expression');
1139
+
1140
+ /** @type {'let' | 'const'} */
1141
+ let kind = 'let';
1142
+ let opt_ident = null;
1143
+ let template = null;
1144
+
1145
+ if (var_decl !== null) {
1146
+ // WGSL `var` is the mutable form → JS `let`.
1147
+ kind = 'let';
1148
+ opt_ident = namedChildByType(var_decl, 'optionally_typed_ident');
1149
+ template = namedChildByType(var_decl, 'template_list');
1150
+ } else {
1151
+ // WGSL `let` and `const` are both immutable bindings → JS `const`.
1152
+ // Tree-sitter exposes the keyword as an unnamed leading token; read it from the source.
1153
+ kind = 'const';
1154
+ opt_ident = opt_ident_direct;
1155
+ }
1156
+
1157
+ const ident = namedChildByType(opt_ident, 'ident');
1158
+ const type_spec = namedChildByType(opt_ident, 'type_specifier');
1159
+ const name = ident.text;
1160
+ let type = type_spec !== null ? parseTypeSpecifier(type_spec) : T_UNKNOWN;
1161
+
1162
+ if (expr_node !== null) {
1163
+ const expr = compileExpression(c, expr_node);
1164
+ // Infer the type from the RHS when no explicit annotation is present. A bare integer
1165
+ // literal concretises to i32 here, as WGSL does for `let x = 3;`.
1166
+ if (type === T_UNKNOWN || type.kind === 'unknown') {
1167
+ type = expr.abstract_int === true ? T_I32 : expr.type;
1168
+ }
1169
+ c.scope.declare(name, type);
1170
+ c.out.add(`${kind} ${name} = ${expr.code};`);
1171
+ } else {
1172
+ c.scope.declare(name, type);
1173
+ c.out.add(`${kind} ${name} = ${defaultValueForType(type)};`);
1174
+ }
1175
+ }
1176
+
1177
+ /**
1178
+ * Assignment / compound assignment / increment / decrement.
1179
+ */
1180
+ function emitVariableUpdatingStatement(c, node) {
1181
+ const inner = node.namedChildren[0];
1182
+ if (inner.type === 'assignment_statement') {
1183
+ const lhs = namedChildByType(inner, 'lhs_expression');
1184
+ const expr = namedChildByType(inner, 'expression');
1185
+
1186
+ // The compound assignment operator sits between the LHS and the RHS as an unnamed token.
1187
+ const op_text = textBetween(inner, lhs, expr).trim();
1188
+
1189
+ const lhs_compiled = compileLhs(c, lhs);
1190
+ const rhs_compiled = compileExpression(c, expr);
1191
+
1192
+ if (op_text === '=' || op_text === '') {
1193
+ c.out.add(emitWriteStatement(lhs_compiled, rhs_compiled.code));
1194
+ return;
1195
+ }
1196
+
1197
+ const arith = {
1198
+ '+=': 'add', '-=': 'sub', '*=': 'mul', '/=': 'div', '%=': 'rem',
1199
+ };
1200
+ if (arith.hasOwnProperty(op_text)) {
1201
+ let helper = arith[op_text];
1202
+
1203
+ /**
1204
+ * The same integer dispatch the binary path does, which this used not to have at all.
1205
+ * WGSL `+` / `-` / `*` on integers wrap at 32 bits; JS numbers grow instead, and the bits
1206
+ * an overflowing f64 drops are the low ones — exactly the ones an output permutation
1207
+ * exists to mix. And compound assignment is the WGSL idiom for a hash, so every hash
1208
+ * chunk in the tree took the unwrapped path while its `x = x * K` spelling took the
1209
+ * correct one: `v *= 0x21f0aaadu` at 123456789 gave 70298821705959730 where the device
1210
+ * gives 3890841905, and the shipped `chunk_hash_lowbias32` disagreed with the CPU
1211
+ * `lowbias32` on 4,076 of its first 4,096 inputs, half of them not u32 values at all.
1212
+ *
1213
+ * That is the second tier of the GPU testing strategy: `chunk/random` and `chunk/noise`
1214
+ * can only be tested here, because a hash cannot be eyeballed.
1215
+ */
1216
+ const arith_type = inferBinaryArithResultType(lhs_compiled.type, rhs_compiled.type);
1217
+ const wrapping = INTEGER_WRAPPING_OP[op_text.slice(0, -1)];
1218
+
1219
+ if (wrapping !== undefined && isIntegerType(arith_type)) {
1220
+ helper = `${wrapping}_${integerScalarName(arith_type)}`;
1221
+ } else if (op_text === '/=' && integerDivision(lhs_compiled, rhs_compiled)) {
1222
+ //WGSL `/` on integers is truncated integer division, as at the binary site
1223
+ helper = 'idiv';
1224
+ }
1225
+
1226
+ c.out.add(emitWriteStatement(lhs_compiled, `wgsl.${helper}(${lhs_compiled.code}, ${rhs_compiled.code})`));
1227
+ return;
1228
+ }
1229
+
1230
+ // Bitwise / shift compound assignment, routed exactly like the binary operators above:
1231
+ // through the element-wise helpers for a vec/mat or bool LHS, and for any u32 operand. The
1232
+ // WGSL idiom for a hash is `x ^= x >> 16u`, so this is the spelling that matters most.
1233
+ const bitwise_helper = BITWISE_HELPER[op_text.slice(0, -1)];
1234
+ if (bitwise_helper !== undefined && (
1235
+ isContainerType(lhs_compiled.type)
1236
+ || isScalarBool(lhs_compiled.type)
1237
+ || isScalarUnsigned(lhs_compiled.type)
1238
+ || isScalarUnsigned(rhs_compiled.type)
1239
+ )) {
1240
+ c.out.add(emitWriteStatement(lhs_compiled, `wgsl.${bitwise_helper}(${lhs_compiled.code}, ${rhs_compiled.code})`));
1241
+ return;
1242
+ }
1243
+ c.out.add(`${lhs_compiled.code} ${op_text} ${rhs_compiled.code};`);
1244
+ return;
1245
+ }
1246
+ if (inner.type === 'increment_statement') {
1247
+ const lhs = namedChildByType(inner, 'lhs_expression');
1248
+ const lhs_compiled = compileLhs(c, lhs);
1249
+ c.out.add(emitWriteStatement(lhs_compiled, `wgsl.add(${lhs_compiled.code}, 1)`));
1250
+ return;
1251
+ }
1252
+ if (inner.type === 'decrement_statement') {
1253
+ const lhs = namedChildByType(inner, 'lhs_expression');
1254
+ const lhs_compiled = compileLhs(c, lhs);
1255
+ c.out.add(emitWriteStatement(lhs_compiled, `wgsl.sub(${lhs_compiled.code}, 1)`));
1256
+ return;
1257
+ }
1258
+ c.error(inner, `unsupported variable_updating_statement: ${inner.type}`);
1259
+ }
1260
+
1261
+ /**
1262
+ * Emit a write to an LHS. Most LHS expressions are plain JS LValues (`x`, `arr[i]`, `obj.f`) and
1263
+ * are written with `=`. Mat-column LHS — `m[i]` for a mat — is special: the compiler emits it as
1264
+ * `wgsl.mat_col(m, i, rows)`, which is a typed-array view (a function call result, not assignable).
1265
+ * To write through the view we use `Float32Array.prototype.set(rhs)` instead, which copies into
1266
+ * the underlying matrix storage in place.
1267
+ *
1268
+ * @param {{ code: string }} lhs_compiled
1269
+ * @param {string} rhs_code
1270
+ * @returns {string}
1271
+ */
1272
+ function emitWriteStatement(lhs_compiled, rhs_code) {
1273
+ // Only the bare `wgsl.mat_col(...)` form (no chained `[i]` / `.x`) needs `.set()` — that's a
1274
+ // typed-array view, an unassignable function-call result. Anything chained off it (e.g.
1275
+ // `wgsl.mat_col(...)[3]` or `wgsl.mat_col(...).x` becoming `[i]`) is a scalar slot and must
1276
+ // use plain `=`.
1277
+ if (lhs_compiled.code.startsWith('wgsl.mat_col(') && lhs_compiled.code.endsWith(')')) {
1278
+ return `${lhs_compiled.code}.set(${rhs_code});`;
1279
+ }
1280
+ // `*p = v` has to reach the caller's object; assigning would only rebind the parameter.
1281
+ if (lhs_compiled.through_pointer === true) {
1282
+ return `${lhs_compiled.code} = wgsl.store_ptr(${lhs_compiled.code}, ${rhs_code});`;
1283
+ }
1284
+ return `${lhs_compiled.code} = ${rhs_code};`;
1285
+ }
1286
+
1287
+ function emitReturnStatement(c, node) {
1288
+ const expr = namedChildByType(node, 'expression');
1289
+ if (expr === null) {
1290
+ c.out.add('return;');
1291
+ } else {
1292
+ const e = compileExpression(c, expr);
1293
+ c.out.add(`return ${e.code};`);
1294
+ }
1295
+ }
1296
+
1297
+ function emitIfStatement(c, node) {
1298
+ const if_clause = namedChildByType(node, 'if_clause');
1299
+ const cond = namedChildByType(if_clause, 'expression');
1300
+ const if_body = namedChildByType(if_clause, 'compound_statement');
1301
+
1302
+ const cond_compiled = compileExpression(c, cond);
1303
+ c.out.add(`if (${cond_compiled.code}) {`);
1304
+ c.out.indent();
1305
+ c.scope = c.scope.push();
1306
+ emitCompoundStatementBody(c, if_body);
1307
+ c.scope = c.scope.parent;
1308
+ c.out.dedent();
1309
+ c.out.add('}');
1310
+
1311
+ // else-if and else clauses.
1312
+ for (const child of node.namedChildren) {
1313
+ if (child.type === 'else_if_clause') {
1314
+ const eif_cond = namedChildByType(child, 'expression');
1315
+ const eif_body = namedChildByType(child, 'compound_statement');
1316
+ const eif_compiled = compileExpression(c, eif_cond);
1317
+ c.out.add(`else if (${eif_compiled.code}) {`);
1318
+ c.out.indent();
1319
+ c.scope = c.scope.push();
1320
+ emitCompoundStatementBody(c, eif_body);
1321
+ c.scope = c.scope.parent;
1322
+ c.out.dedent();
1323
+ c.out.add('}');
1324
+ } else if (child.type === 'else_clause') {
1325
+ const else_body = namedChildByType(child, 'compound_statement');
1326
+ // else_body may instead be an if_statement (else-if shorthand).
1327
+ const nested_if = namedChildByType(child, 'if_statement');
1328
+ if (else_body !== null) {
1329
+ c.out.add('else {');
1330
+ c.out.indent();
1331
+ c.scope = c.scope.push();
1332
+ emitCompoundStatementBody(c, else_body);
1333
+ c.scope = c.scope.parent;
1334
+ c.out.dedent();
1335
+ c.out.add('}');
1336
+ } else if (nested_if !== null) {
1337
+ c.out.add('else');
1338
+ emitIfStatement(c, nested_if);
1339
+ }
1340
+ }
1341
+ }
1342
+ }
1343
+
1344
+ function emitForStatement(c, node) {
1345
+ const for_header = namedChildByType(node, 'for_header');
1346
+ const for_init = namedChildByType(for_header, 'for_init');
1347
+ const cond_expr = namedChildByType(for_header, 'expression');
1348
+ const for_update = namedChildByType(for_header, 'for_update');
1349
+ const body = namedChildByType(node, 'compound_statement');
1350
+
1351
+ c.scope = c.scope.push();
1352
+
1353
+ // Init / cond / update parts.
1354
+ let init_str = '';
1355
+ if (for_init !== null) {
1356
+ // Tree-sitter wraps it in a variable_or_value_statement; capture the emission as text.
1357
+ const sub = new Compiler();
1358
+ sub.scope = c.scope;
1359
+ sub.structs = c.structs;
1360
+ sub.functions = c.functions;
1361
+ const inner = for_init.namedChildren[0];
1362
+ emitForInitInline(sub, inner);
1363
+ init_str = sub.out.build().trimEnd();
1364
+ if (init_str.endsWith(';')) init_str = init_str.slice(0, -1);
1365
+ }
1366
+
1367
+ const cond_str = cond_expr !== null ? compileExpression(c, cond_expr).code : '';
1368
+
1369
+ let update_str = '';
1370
+ if (for_update !== null) {
1371
+ const inner = for_update.namedChildren[0];
1372
+ update_str = emitForUpdateInline(c, inner);
1373
+ }
1374
+
1375
+ c.out.add(`for (${init_str}; ${cond_str}; ${update_str}) {`);
1376
+ c.out.indent();
1377
+ // `continue` inside this loop targets this loop, not an outer `loop`'s continuing block
1378
+ const saved_continue_label = c.continue_label;
1379
+ c.continue_label = null;
1380
+ emitCompoundStatementBody(c, body);
1381
+ c.continue_label = saved_continue_label;
1382
+ c.out.dedent();
1383
+ c.out.add('}');
1384
+
1385
+ c.scope = c.scope.parent;
1386
+ }
1387
+
1388
+ /**
1389
+ * Emit a `variable_or_value_statement` inline (as a single line with no trailing newline structure).
1390
+ * Used by `for(init; ...)`.
1391
+ */
1392
+ function emitForInitInline(c, node) {
1393
+ if (node.type === 'variable_or_value_statement') {
1394
+ const var_decl = node.namedChildren[0] && node.namedChildren[0].type === 'variable_decl'
1395
+ ? node.namedChildren[0]
1396
+ : null;
1397
+ const opt_ident = namedChildByType(var_decl !== null ? var_decl : node, 'optionally_typed_ident');
1398
+ const expr_node = namedChildByType(node, 'expression');
1399
+ const ident = namedChildByType(opt_ident, 'ident');
1400
+ const type_spec = namedChildByType(opt_ident, 'type_specifier');
1401
+ const name = ident.text;
1402
+ const init = expr_node !== null ? compileExpression(c, expr_node) : null;
1403
+ let type = type_spec !== null ? parseTypeSpecifier(type_spec) : T_UNKNOWN;
1404
+ if (type.kind === 'unknown' && init !== null) {
1405
+ // `for (var i = 0; ...)`: a bare integer literal concretises to i32, as in a `let`
1406
+ type = init.abstract_int === true ? T_I32 : init.type;
1407
+ }
1408
+ c.scope.declare(name, type);
1409
+ const value = init !== null ? init.code : defaultValueForType(type);
1410
+ // `var i = …` (mutable, common in `for (var i = 0; ...; i = i + 1)`) → JS `let`.
1411
+ // `let i = …` / `const i = …` (immutable) → JS `const`.
1412
+ const kind = var_decl !== null ? 'let' : 'const';
1413
+ c.out.add(`${kind} ${name} = ${value};`);
1414
+ return;
1415
+ }
1416
+ if (node.type === 'variable_updating_statement') {
1417
+ emitVariableUpdatingStatement(c, node);
1418
+ return;
1419
+ }
1420
+ if (node.type === 'func_call_statement') {
1421
+ emitFunctionCallStatement(c, node);
1422
+ return;
1423
+ }
1424
+ }
1425
+
1426
+ /**
1427
+ * @returns {string} JS update expression (no semicolon).
1428
+ */
1429
+ function emitForUpdateInline(c, node) {
1430
+ if (node.type === 'variable_updating_statement') {
1431
+ const inner = node.namedChildren[0];
1432
+ if (inner.type === 'assignment_statement') {
1433
+ const lhs = namedChildByType(inner, 'lhs_expression');
1434
+ const expr = namedChildByType(inner, 'expression');
1435
+ const op_text = textBetween(inner, lhs, expr).trim();
1436
+ const lhs_compiled = compileLhs(c, lhs);
1437
+ const rhs_compiled = compileExpression(c, expr);
1438
+ if (op_text === '=' || op_text === '') {
1439
+ if (lhs_compiled.through_pointer === true) {
1440
+ return `${lhs_compiled.code} = wgsl.store_ptr(${lhs_compiled.code}, ${rhs_compiled.code})`;
1441
+ }
1442
+ return `${lhs_compiled.code} = ${rhs_compiled.code}`;
1443
+ }
1444
+ const arith = { '+=': 'add', '-=': 'sub', '*=': 'mul', '/=': 'div', '%=': 'rem' };
1445
+ if (arith.hasOwnProperty(op_text)) {
1446
+ if (lhs_compiled.through_pointer === true) {
1447
+ return `${lhs_compiled.code} = wgsl.store_ptr(${lhs_compiled.code}, wgsl.${arith[op_text]}(${lhs_compiled.code}, ${rhs_compiled.code}))`;
1448
+ }
1449
+ return `${lhs_compiled.code} = wgsl.${arith[op_text]}(${lhs_compiled.code}, ${rhs_compiled.code})`;
1450
+ }
1451
+ return `${lhs_compiled.code} ${op_text} ${rhs_compiled.code}`;
1452
+ }
1453
+ if (inner.type === 'increment_statement') {
1454
+ const lhs = namedChildByType(inner, 'lhs_expression');
1455
+ const lhs_compiled = compileLhs(c, lhs);
1456
+ return `${lhs_compiled.code} = wgsl.add(${lhs_compiled.code}, 1)`;
1457
+ }
1458
+ if (inner.type === 'decrement_statement') {
1459
+ const lhs = namedChildByType(inner, 'lhs_expression');
1460
+ const lhs_compiled = compileLhs(c, lhs);
1461
+ return `${lhs_compiled.code} = wgsl.sub(${lhs_compiled.code}, 1)`;
1462
+ }
1463
+ }
1464
+ return '';
1465
+ }
1466
+
1467
+ function emitWhileStatement(c, node) {
1468
+ const cond = namedChildByType(node, 'expression');
1469
+ const body = namedChildByType(node, 'compound_statement');
1470
+ const cc = compileExpression(c, cond);
1471
+ c.out.add(`while (${cc.code}) {`);
1472
+ c.out.indent();
1473
+ c.scope = c.scope.push();
1474
+ // `continue` inside this loop targets this loop, not an outer `loop`'s continuing block
1475
+ const saved_continue_label = c.continue_label;
1476
+ c.continue_label = null;
1477
+ emitCompoundStatementBody(c, body);
1478
+ c.continue_label = saved_continue_label;
1479
+ c.scope = c.scope.parent;
1480
+ c.out.dedent();
1481
+ c.out.add('}');
1482
+ }
1483
+
1484
+ function emitLoopStatement(c, node) {
1485
+ // NOTE: the grammar puts the loop's `statement` children and the optional
1486
+ // `continuing_statement` directly on the `loop_statement` node (no
1487
+ // `compound_statement` wrapper); emitCompoundStatementBody visits only the
1488
+ // `statement` children, which is exactly the loop body.
1489
+ const body = node;
1490
+ const continuing = namedChildByType(node, 'continuing_statement');
1491
+
1492
+ const saved_continue_label = c.continue_label;
1493
+
1494
+ c.out.add('while (true) {');
1495
+ c.out.indent();
1496
+ c.scope = c.scope.push();
1497
+
1498
+ if (continuing === null) {
1499
+
1500
+ // no continuing block: JS `continue` already has the right semantics
1501
+ c.continue_label = null;
1502
+ emitCompoundStatementBody(c, body);
1503
+
1504
+ } else {
1505
+
1506
+ // WGSL `continue` transfers control INTO the `continuing` block. Emit the loop
1507
+ // body inside a labeled block: `continue` becomes `break <label>`, which lands on
1508
+ // the continuing code that follows the block (still inside the `while`).
1509
+ const label = `loop_body_${c.loop_label_counter++}`;
1510
+ c.continue_label = label;
1511
+
1512
+ c.out.add(`${label}: {`);
1513
+ c.out.indent();
1514
+ emitCompoundStatementBody(c, body);
1515
+ c.out.dedent();
1516
+ c.out.add('}');
1517
+
1518
+ c.continue_label = null;
1519
+
1520
+ const cont_body = namedChildByType(continuing, 'continuing_compound_statement');
1521
+ if (cont_body !== null) {
1522
+ emitCompoundStatementBody(c, cont_body);
1523
+
1524
+ // `break if expr;` — only legal as the trailing statement of a continuing block.
1525
+ // It is a direct named child of the continuing compound (not wrapped in `statement`),
1526
+ // so emitCompoundStatementBody does not visit it.
1527
+ const break_if = namedChildByType(cont_body, 'break_if_statement');
1528
+ if (break_if !== null) {
1529
+ const break_expr = namedChildByType(break_if, 'expression');
1530
+ const bc = compileExpression(c, break_expr);
1531
+ c.out.add(`if (${bc.code}) { break; }`);
1532
+ }
1533
+ }
1534
+
1535
+ }
1536
+
1537
+ c.continue_label = saved_continue_label;
1538
+ c.scope = c.scope.parent;
1539
+ c.out.dedent();
1540
+ c.out.add('}');
1541
+ }
1542
+
1543
+ /**
1544
+ * Emit a WGSL `switch`.
1545
+ *
1546
+ * Grammar (tree-sitter-wgsl): `switch_statement { expression, switch_body { switch_clause* } }`,
1547
+ * where each `switch_clause` wraps either a `case_clause { case_selectors { case_selector+ },
1548
+ * compound_statement }` or a `default_alone_clause { compound_statement }`. A `case_selector` holds
1549
+ * either an `expression` (a case value) or the `default` keyword.
1550
+ *
1551
+ * WGSL switch has no fall-through — each selected clause runs its body and stops — so we terminate
1552
+ * every clause body with `break;`. Multiple selectors on one clause become stacked JS `case` labels
1553
+ * over a shared body.
1554
+ */
1555
+ function emitSwitchStatement(c, node) {
1556
+ const expr = namedChildByType(node, 'expression');
1557
+ const ec = compileExpression(c, expr);
1558
+ c.out.add(`switch (${ec.code}) {`);
1559
+ c.out.indent();
1560
+
1561
+ const body = namedChildByType(node, 'switch_body');
1562
+ if (body) {
1563
+ for (const clause of namedChildrenByType(body, 'switch_clause')) {
1564
+ const case_clause = namedChildByType(clause, 'case_clause');
1565
+ const default_clause = namedChildByType(clause, 'default_alone_clause');
1566
+ const inner = case_clause ?? default_clause;
1567
+ if (!inner) {
1568
+ continue;
1569
+ }
1570
+
1571
+ if (case_clause) {
1572
+ const case_selectors = namedChildByType(case_clause, 'case_selectors');
1573
+ if (case_selectors) {
1574
+ for (const sel of namedChildrenByType(case_selectors, 'case_selector')) {
1575
+ const sel_expr = namedChildByType(sel, 'expression');
1576
+ if (sel_expr) {
1577
+ c.out.add(`case ${compileExpression(c, sel_expr).code}:`);
1578
+ } else {
1579
+ // a bare `default` used within a case_selectors list
1580
+ c.out.add('default:');
1581
+ }
1582
+ }
1583
+ }
1584
+ } else {
1585
+ c.out.add('default:');
1586
+ }
1587
+
1588
+ // Wrap each clause body in its own JS block. WGSL case clauses are independent scopes,
1589
+ // but JS switch clauses share the switch's block scope — without the braces, `let`
1590
+ // temporaries declared in two clauses collide ("Identifier already declared").
1591
+ const case_body = namedChildByType(inner, 'compound_statement');
1592
+ c.out.add('{');
1593
+ c.out.indent();
1594
+ c.scope = c.scope.push();
1595
+ if (case_body) {
1596
+ emitCompoundStatementBody(c, case_body);
1597
+ }
1598
+ c.out.add('break;');
1599
+ c.scope = c.scope.parent;
1600
+ c.out.dedent();
1601
+ c.out.add('}');
1602
+ }
1603
+ }
1604
+
1605
+ c.out.dedent();
1606
+ c.out.add('}');
1607
+ }
1608
+
1609
+ function emitFunctionCallStatement(c, node) {
1610
+ const call_phrase = namedChildByType(node, 'call_phrase');
1611
+ const r = compileCallPhraseTyped(c, call_phrase);
1612
+ c.out.add(`${r.code};`);
1613
+ }
1614
+
1615
+ // =========================================================================
1616
+ // Expressions
1617
+ // =========================================================================
1618
+
1619
+ /**
1620
+ * Compile an expression node (any wrapper or specific kind).
1621
+ * @param {Compiler} c
1622
+ * @param {Node} node
1623
+ * @returns {{ code: string, type: WgslType }}
1624
+ */
1625
+ function compileExpression(c, node) {
1626
+ const u = unwrapExpression(node);
1627
+
1628
+ // Binary operations
1629
+ switch (u.type) {
1630
+ case 'expression':
1631
+ // tree-sitter-wgsl can leave `&&` / `||` directly under an `expression` node — i.e.
1632
+ // the operator wraps two operands without a more specific short_circuit_*_expression
1633
+ // wrapper around it. With 2 named children the unnamed operator sits between them.
1634
+ return compileBinaryExpression(c, u, '__shortcircuit_unused__', 'raw');
1635
+ case 'relational_expression':
1636
+ return compileBinaryExpression(c, u, 'relational_operator', 'raw');
1637
+ case 'shift_expression':
1638
+ return compileBinaryExpression(c, u, 'shift_operator', 'raw');
1639
+ case 'additive_expression':
1640
+ return compileBinaryExpression(c, u, 'additive_operator', 'arith');
1641
+ case 'multiplicative_expression':
1642
+ return compileBinaryExpression(c, u, 'multiplicative_operator', 'arith');
1643
+ case 'short_circuit_and_expression':
1644
+ case 'short_circuit_or_expression':
1645
+ return compileBinaryExpression(c, u, '__shortcircuit_unused__', 'raw');
1646
+ case 'bitwise_expression':
1647
+ // Top-level WGSL `^` / `&` / `|` operator. The operator is an unnamed token between
1648
+ // the two named operands; compileBinaryExpression's 2-named-children path picks it up.
1649
+ return compileBinaryExpression(c, u, '__bitwise_unused__', 'raw');
1650
+ case 'binary_and_expression':
1651
+ case 'binary_or_expression':
1652
+ case 'binary_xor_expression':
1653
+ // Inner chain nodes from the WGSL grammar (e.g. `a ^ b ^ c` nests as
1654
+ // `binary_xor_expression(binary_xor_expression(a, b), c)`). When 2 named children
1655
+ // appear, the unnamed operator sits between them; the 1-child case is a passthrough.
1656
+ return compileBinaryExpression(c, u, '__bitwise_unused__', 'raw');
1657
+ }
1658
+
1659
+ // Unary
1660
+ if (u.type === 'unary_expression') {
1661
+ return compileUnaryExpression(c, u);
1662
+ }
1663
+
1664
+ // Singular (primary + chained component access)
1665
+ if (u.type === 'singular_expression') {
1666
+ return compileSingularExpression(c, u);
1667
+ }
1668
+
1669
+ // Primary subkinds
1670
+ if (u.type === 'literal' || u.type === 'int_literal' || u.type === 'float_literal' || u.type === 'bool_literal') {
1671
+ // A bare integer literal is WGSL's AbstractInt. Its concrete type comes from context this
1672
+ // compiler does not track, so the type stays unknown, but operator selection still needs
1673
+ // to know it is an integer: see integerDivision().
1674
+ return { code: literalToJs(u.text), type: literalType(u.text), abstract_int: isBareIntLiteral(u.text) };
1675
+ }
1676
+ if (u.type === 'paren_expression') {
1677
+ const inner = namedChildByType(u, 'expression');
1678
+ const e = compileExpression(c, inner);
1679
+ return { code: `(${e.code})`, type: e.type, abstract_int: e.abstract_int };
1680
+ }
1681
+ if (u.type === 'call_expression') {
1682
+ const call_phrase = namedChildByType(u, 'call_phrase');
1683
+ return compileCallPhraseTyped(c, call_phrase);
1684
+ }
1685
+ if (u.type === 'template_elaborated_ident') {
1686
+ const ident = namedChildByType(u, 'ident');
1687
+ const name = ident !== null ? ident.text : u.text;
1688
+ return { code: name, type: c.scope.lookup(name) };
1689
+ }
1690
+ if (u.type === 'ident') {
1691
+ return { code: u.text, type: c.scope.lookup(u.text) };
1692
+ }
1693
+
1694
+ c.error(u, `unsupported expression: ${u.type}`);
1695
+ }
1696
+
1697
+ /**
1698
+ * Compile a binary expression node. Two grammar shapes are supported:
1699
+ * - 3 named children where the middle child has type `op_type` — used for additive / multiplicative
1700
+ * where the operator is an explicit named node (`additive_operator`, `multiplicative_operator`).
1701
+ * - 2 named children with an unnamed operator token between them — used for relational / shift,
1702
+ * where the operator (e.g. `<`, `>=`, `<<`) is a raw token.
1703
+ *
1704
+ * Mode:
1705
+ * - `'arith'` → use `wgsl.add/sub/mul/div/rem` (handles vectors)
1706
+ * - `'raw'` → emit raw JS operator
1707
+ */
1708
+ function compileBinaryExpression(c, node, op_type, mode) {
1709
+ const named = node.namedChildren;
1710
+ if (named.length === 1) {
1711
+ return compileExpression(c, named[0]);
1712
+ }
1713
+
1714
+ let op_text = null;
1715
+ let left_node;
1716
+ let right_node;
1717
+
1718
+ if (named.length === 3 && named[1].type === op_type) {
1719
+ op_text = named[1].text;
1720
+ left_node = named[0];
1721
+ right_node = named[2];
1722
+ } else if (named.length === 2) {
1723
+ left_node = named[0];
1724
+ right_node = named[1];
1725
+ // tree-sitter-web doesn't surface unnamed children in `node.children`. Extract the
1726
+ // operator by slicing the original text using the operands' source positions.
1727
+ op_text = textBetween(node, left_node, right_node).trim();
1728
+ } else {
1729
+ return { code: node.text, type: T_UNKNOWN };
1730
+ }
1731
+
1732
+ const left = compileExpression(c, left_node);
1733
+ const right = compileExpression(c, right_node);
1734
+
1735
+ if (mode === 'arith') {
1736
+ const helper = ARITHMETIC_OP[op_text];
1737
+ if (helper !== undefined) {
1738
+ // `*` on matrix/vector operands has WGSL-specific algebra (dot-products and
1739
+ // accumulation) that the element-wise `wgsl.mul` doesn't capture. Route to a
1740
+ // dedicated `mNxR_multiply_*` helper when at least one side is a matrix.
1741
+ if (op_text === '*') {
1742
+ const dispatched = dispatchMatrixMul(left, right);
1743
+ if (dispatched !== null) return dispatched;
1744
+ }
1745
+ // WGSL `/` on integer types is truncated integer division. Route through the
1746
+ // integer-truncating helper when WGSL would type the division as integer;
1747
+ // otherwise use float div.
1748
+ let actual_helper = helper;
1749
+ if (op_text === '/' && integerDivision(left, right)) {
1750
+ actual_helper = 'idiv';
1751
+ }
1752
+ // WGSL `+` / `-` / `*` on integer types wrap at 32 bits. JS numbers don't, and the bits
1753
+ // an overflowing f64 drops are the low ones — which silently ruins hashes and PRNGs
1754
+ // (see the wrapping helpers in WGSLLanguageCore.js). Route to the wrapping variant only
1755
+ // when the result type is *known* to be an integer, so anything untyped or float keeps
1756
+ // the plain helper.
1757
+ const arith_type = inferBinaryArithResultType(left.type, right.type);
1758
+ const wrapping = INTEGER_WRAPPING_OP[op_text];
1759
+ if (wrapping !== undefined && isIntegerType(arith_type)) {
1760
+ actual_helper = `${wrapping}_${integerScalarName(arith_type)}`;
1761
+ }
1762
+ return {
1763
+ code: `wgsl.${actual_helper}(${left.code}, ${right.code})`,
1764
+ type: arith_type,
1765
+ // arithmetic on two AbstractInts is still AbstractInt: `(1 + 2) / 2` is 1
1766
+ abstract_int: left.abstract_int === true && right.abstract_int === true,
1767
+ };
1768
+ }
1769
+ }
1770
+
1771
+ // Bitwise (`&` / `|` / `^`) and shift (`<<` / `>>`) operators. Routed through the element-wise
1772
+ // helpers for vec/mat operands (JS raw operators coerce typed arrays to NaN), for bool operands
1773
+ // (raw JS yields 0/1 where WGSL yields a bool), and for *any* u32-typed scalar operand.
1774
+ //
1775
+ // That last one used to be `>>` alone. Every one of these raw JS operators is ToInt32-based and
1776
+ // hands back a signed int32, so on a u32 they go negative the moment bit 31 is involved:
1777
+ // `x << 31u` at x=1 was -2147483648 against the device's 2147483648, and `x ^ y` with the high
1778
+ // bit set was negative too. The helpers normalise with `>>> 0`, which is what WGSL u32 means.
1779
+ //
1780
+ // Deliberately not extended to i32: `bit_and` and friends return an unsigned result, and a
1781
+ // signed operand is exactly the case raw JS already gets right.
1782
+ const bitwise_helper = BITWISE_HELPER[op_text];
1783
+ if (bitwise_helper !== undefined) {
1784
+ if (isContainerType(left.type) || isContainerType(right.type)
1785
+ || isScalarBool(left.type) || isScalarBool(right.type)
1786
+ || isScalarUnsigned(left.type) || isScalarUnsigned(right.type)) {
1787
+ return {
1788
+ code: `wgsl.${bitwise_helper}(${left.code}, ${right.code})`,
1789
+ type: inferRawBinaryResultType(left.type, right.type, op_text),
1790
+ };
1791
+ }
1792
+ }
1793
+
1794
+ // Comparison operators. JS's raw `==` etc. compare TypedArrays by reference; for WGSL
1795
+ // `vec<T>` operands we need element-wise comparison returning a `vec<bool>`.
1796
+ const cmp_helper = COMPARISON_HELPER[op_text];
1797
+ if (cmp_helper !== undefined && (isContainerType(left.type) || isContainerType(right.type))) {
1798
+ return {
1799
+ code: `wgsl.${cmp_helper}(${left.code}, ${right.code})`,
1800
+ type: inferRawBinaryResultType(left.type, right.type, op_text),
1801
+ };
1802
+ }
1803
+
1804
+ return {
1805
+ code: `${left.code} ${op_text} ${right.code}`,
1806
+ type: inferRawBinaryResultType(left.type, right.type, op_text),
1807
+ };
1808
+ }
1809
+
1810
+ /** Map WGSL bitwise/shift operators to language-core element-wise helper names. */
1811
+ const BITWISE_HELPER = {
1812
+ '&': 'bit_and',
1813
+ '|': 'bit_or',
1814
+ '^': 'bit_xor',
1815
+ '<<': 'shl',
1816
+ '>>': 'shr',
1817
+ };
1818
+
1819
+ /** Map WGSL comparison operators to language-core element-wise helper names. */
1820
+ const COMPARISON_HELPER = {
1821
+ '==': 'eq',
1822
+ '!=': 'ne',
1823
+ '<': 'lt',
1824
+ '>': 'gt',
1825
+ '<=': 'le',
1826
+ '>=': 'ge',
1827
+ };
1828
+
1829
+ /** True if `t` is a vector or matrix — operand types that need element-wise dispatch. */
1830
+ function isContainerType(t) {
1831
+ return t && (t.kind === 'vec' || t.kind === 'mat');
1832
+ }
1833
+
1834
+ /** True if `t` is a u32 scalar — needed to route `>>` through the unsigned-shift helper. */
1835
+ function isScalarUnsigned(t) {
1836
+ return t && t.kind === 'primitive' && t.name === 'u32';
1837
+ }
1838
+
1839
+ /** True if `t` is a bool scalar — WGSL `&` / `|` / `^` on bools are logical and stay bool. */
1840
+ function isScalarBool(t) {
1841
+ return t && t.kind === 'primitive' && t.name === 'bool';
1842
+ }
1843
+
1844
+ /** The scalar name (`u32` / `i32`) of an integer type, for picking a wrapping arithmetic helper. */
1845
+ function integerScalarName(t) {
1846
+ if (t.kind === 'primitive') return t.name;
1847
+ return t.scalar;
1848
+ }
1849
+
1850
+ /** True if `t` is an integer type (scalar or vec/mat of i32/u32). */
1851
+ function isIntegerType(t) {
1852
+ if (!t) return false;
1853
+ if (t.kind === 'primitive') return t.name === 'i32' || t.name === 'u32';
1854
+ if (t.kind === 'vec' || t.kind === 'mat') return t.scalar === 'i32' || t.scalar === 'u32';
1855
+ return false;
1856
+ }
1857
+
1858
+ /** True if the literal's spelling is a bare integer: no suffix, no fraction, no exponent. */
1859
+ function isBareIntLiteral(text) {
1860
+ return /^(0[xX][0-9a-fA-F]+|[0-9]+)$/.test(text.trim());
1861
+ }
1862
+
1863
+ /**
1864
+ * True if WGSL evaluates `left / right` as integer division. Both operands integer-typed is the
1865
+ * obvious case. A bare literal is AbstractInt and takes the other operand's integer type, so
1866
+ * `index / 3` truncates; two bare literals divide as AbstractInt, so `7 / 2` is 3 even where an
1867
+ * f32 is expected. Negation and parentheses keep a literal abstract: `index / -3` and
1868
+ * `index / (3)` truncate too.
1869
+ */
1870
+ function integerDivision(left, right) {
1871
+ const isInt = (operand) => isIntegerType(operand.type) || operand.abstract_int === true;
1872
+ return isInt(left) && isInt(right);
1873
+ }
1874
+
1875
+ /**
1876
+ * Dispatch a WGSL `*` expression to the appropriate matrix/vector helper in the language
1877
+ * core when at least one operand is a matrix. Returns `null` to indicate "fall through to the
1878
+ * element-wise multiply" — used for scalar×scalar, vec×scalar, vec×vec (Hadamard product),
1879
+ * matrix×scalar, etc., which `wgsl.mul` already handles correctly.
1880
+ *
1881
+ * Naming includes the scalar type suffix (`f` / `i` / `u` / `h`), e.g.
1882
+ * `m4x4f_multiply_v4f`, `v3f_multiply_m3x3f`, `m4x4f_multiply_m4x4f`. Unsupported
1883
+ * shapes (e.g. mat3x4 × vec3) produce a runtime `TypeError: wgsl.<name> is not a function`
1884
+ * — adding support is a single new function in WGSLLanguageCore.js.
1885
+ *
1886
+ * @param {{ code: string, type: WgslType }} left
1887
+ * @param {{ code: string, type: WgslType }} right
1888
+ * @returns {{ code: string, type: WgslType } | null}
1889
+ */
1890
+ function dispatchMatrixMul(left, right) {
1891
+ const lt = left.type;
1892
+ const rt = right.type;
1893
+ const isVec = (t) => t && t.kind === 'vec';
1894
+ const isMat = (t) => t && t.kind === 'mat';
1895
+
1896
+ // mat × mat → mat(b.cols, a.rows). Multiplication is defined only when a.cols == b.rows.
1897
+ if (isMat(lt) && isMat(rt)) {
1898
+ const lh = `m${lt.cols}x${lt.rows}${scalarTypeSuffix(lt.scalar)}`;
1899
+ const rh = `m${rt.cols}x${rt.rows}${scalarTypeSuffix(rt.scalar)}`;
1900
+ return {
1901
+ code: `wgsl.${lh}_multiply_${rh}(${left.code}, ${right.code})`,
1902
+ type: { kind: 'mat', cols: rt.cols, rows: lt.rows, scalar: lt.scalar },
1903
+ };
1904
+ }
1905
+
1906
+ // mat × vec → vec(rows). Vec must have length == cols.
1907
+ if (isMat(lt) && isVec(rt)) {
1908
+ const lh = `m${lt.cols}x${lt.rows}${scalarTypeSuffix(lt.scalar)}`;
1909
+ const rh = `v${rt.size}${scalarTypeSuffix(rt.scalar)}`;
1910
+ return {
1911
+ code: `wgsl.${lh}_multiply_${rh}(${left.code}, ${right.code})`,
1912
+ type: { kind: 'vec', size: lt.rows, scalar: lt.scalar },
1913
+ };
1914
+ }
1915
+
1916
+ // vec × mat → vec(cols). Vec acts as a row vector; vec.length must equal rows.
1917
+ if (isVec(lt) && isMat(rt)) {
1918
+ const lh = `v${lt.size}${scalarTypeSuffix(lt.scalar)}`;
1919
+ const rh = `m${rt.cols}x${rt.rows}${scalarTypeSuffix(rt.scalar)}`;
1920
+ return {
1921
+ code: `wgsl.${lh}_multiply_${rh}(${left.code}, ${right.code})`,
1922
+ type: { kind: 'vec', size: rt.cols, scalar: rt.scalar },
1923
+ };
1924
+ }
1925
+
1926
+ return null;
1927
+ }
1928
+
1929
+ /** Scalar type → suffix character used in matrix/vector helper names: f / i / u / h. */
1930
+ function scalarTypeSuffix(scalar) {
1931
+ return { f32: 'f', i32: 'i', u32: 'u', f16: 'h' }[scalar] || 'f';
1932
+ }
1933
+
1934
+ /**
1935
+ * Pick the result type of an element-wise arithmetic op given the operand types.
1936
+ * Vec wins over scalar; matrix wins over scalar; scalar otherwise.
1937
+ */
1938
+ function inferBinaryArithResultType(a, b) {
1939
+ const isVec = (t) => t && t.kind === 'vec';
1940
+ const isMat = (t) => t && t.kind === 'mat';
1941
+ const isPrim = (t) => t && t.kind === 'primitive';
1942
+
1943
+ if (isVec(a)) return a;
1944
+ if (isVec(b)) return b;
1945
+ if (isMat(a)) return a;
1946
+ if (isMat(b)) return b;
1947
+ if (isPrim(a)) return a;
1948
+ if (isPrim(b)) return b;
1949
+ return T_UNKNOWN;
1950
+ }
1951
+
1952
+ /**
1953
+ * Pick the result type of a non-arithmetic binary op (comparison / logical / bitwise / shift).
1954
+ * Comparisons return bool; bitwise and shift preserve the leftmost known integer operand type.
1955
+ */
1956
+ function inferRawBinaryResultType(a, b, op) {
1957
+ const COMPARISON = new Set(['==', '!=', '<', '>', '<=', '>=']);
1958
+ const LOGICAL = new Set(['&&', '||']);
1959
+ if (COMPARISON.has(op) || LOGICAL.has(op)) {
1960
+ return { kind: 'primitive', name: 'bool' };
1961
+ }
1962
+ // Bitwise / shift: propagate the first known integer-ish operand type. This lets `bitcast`
1963
+ // and other downstream consumers see u32/i32 even when the literal on one side is untyped.
1964
+ if (a && a.kind === 'primitive') return a;
1965
+ if (b && b.kind === 'primitive') return b;
1966
+ if (a && a.kind === 'vec') return a;
1967
+ if (b && b.kind === 'vec') return b;
1968
+ return T_UNKNOWN;
1969
+ }
1970
+
1971
+ function compileUnaryExpression(c, node) {
1972
+ const named = node.namedChildren;
1973
+ const prefix = textBeforeFirstNamed(node).trim();
1974
+
1975
+ if (prefix === '') {
1976
+ return compileExpression(c, named[0]);
1977
+ }
1978
+
1979
+ const operand = compileExpression(c, named[named.length - 1]);
1980
+
1981
+ if (prefix === '-') return { code: `wgsl.neg(${operand.code})`, type: operand.type, abstract_int: operand.abstract_int };
1982
+ if (prefix === '!') return { code: `!${operand.code}`, type: operand.type };
1983
+ if (prefix === '~') return { code: `wgsl.not(${operand.code})`, type: operand.type };
1984
+ if (prefix === '*' || prefix === '&') return { code: operand.code, type: operand.type };
1985
+
1986
+ return { code: `${prefix}${operand.code}`, type: operand.type };
1987
+ }
1988
+
1989
+ function compileSingularExpression(c, node) {
1990
+ const named = node.namedChildren;
1991
+ const head = named[0];
1992
+ const head_result = head.type === 'primary_expression'
1993
+ ? compileExpression(c, unwrapExpression(head))
1994
+ : compileExpression(c, head);
1995
+
1996
+ let code = head_result.code;
1997
+ let type = head_result.type;
1998
+
1999
+ for (let i = 1; i < named.length; i++) {
2000
+ const spec = named[i];
2001
+ if (spec.type !== 'component_or_swizzle_specifier') break;
2002
+ const next = applyAccessSpecifier(c, spec, code, type);
2003
+ code = next.code;
2004
+ type = next.type;
2005
+ }
2006
+
2007
+ // a bare literal reached through a singular expression is still AbstractInt; anything with an
2008
+ // access specifier on it is not a literal
2009
+ return { code, type, abstract_int: named.length === 1 ? head_result.abstract_int : undefined };
2010
+ }
2011
+
2012
+ /**
2013
+ * Apply a `.member`, `.swizzle`, or `[index]` access to an expression that has already been
2014
+ * compiled to `lhs_code` of type `lhs_type`.
2015
+ */
2016
+ function applyAccessSpecifier(c, node, lhs_code, lhs_type) {
2017
+ // tree-sitter-wgsl groups chained access like `[i].x` (or `.foo[i]`) as a SINGLE outer
2018
+ // `component_or_swizzle_specifier` whose first named child is the immediate access (an
2019
+ // `expression` for `[]` or a `member_ident` for `.field`) and whose second named child
2020
+ // is a NESTED `component_or_swizzle_specifier` for the rest of the chain. We apply the
2021
+ // immediate access first, then recurse into any nested specifier.
2022
+ let code = lhs_code;
2023
+ let type = lhs_type;
2024
+
2025
+ const member_ident = namedChildByType(node, 'member_ident');
2026
+ if (member_ident !== null) {
2027
+ const r = applyMemberAccess(c, member_ident.text, code, type);
2028
+ code = r.code;
2029
+ type = r.type;
2030
+ } else {
2031
+ const expr = namedChildByType(node, 'expression');
2032
+ if (expr !== null) {
2033
+ const e = compileExpression(c, expr);
2034
+ // Matrices are stored as flat column-major typed arrays, so the bare `m[i]`
2035
+ // would index a scalar — route through `wgsl.mat_col` to get the column as a vec.
2036
+ if (type && type.kind === 'mat') {
2037
+ code = `wgsl.mat_col(${code}, ${e.code}, ${type.rows})`;
2038
+ type = { kind: 'vec', size: type.rows, scalar: type.scalar };
2039
+ } else {
2040
+ code = `${code}[${e.code}]`;
2041
+ if (type && type.kind === 'array') type = type.element;
2042
+ else if (type && type.kind === 'vec') type = { kind: 'primitive', name: type.scalar };
2043
+ else type = T_UNKNOWN;
2044
+ }
2045
+ }
2046
+ }
2047
+
2048
+ const nested = namedChildByType(node, 'component_or_swizzle_specifier');
2049
+ if (nested !== null) {
2050
+ const r = applyAccessSpecifier(c, nested, code, type);
2051
+ code = r.code;
2052
+ type = r.type;
2053
+ }
2054
+
2055
+ return { code, type };
2056
+ }
2057
+
2058
+ /** Apply a `.field` / `.swizzle` access to an already-compiled expression of `lhs_type`. */
2059
+ function applyMemberAccess(c, name, lhs_code, lhs_type) {
2060
+ if (lhs_type !== null && lhs_type !== undefined && lhs_type.kind === 'vec' && isSwizzlePattern(name)) {
2061
+ if (name.length === 1) {
2062
+ return {
2063
+ code: `${lhs_code}[${SWIZZLE_INDEX[name]}]`,
2064
+ type: { kind: 'primitive', name: lhs_type.scalar },
2065
+ };
2066
+ }
2067
+ const new_size = name.length;
2068
+ const ctor = vecAliasName(new_size, lhs_type.scalar);
2069
+ const elements = Array.from(name).map(ch => `${lhs_code}[${SWIZZLE_INDEX[ch]}]`);
2070
+ return {
2071
+ code: `wgsl.${ctor}(${elements.join(', ')})`,
2072
+ type: { kind: 'vec', size: new_size, scalar: lhs_type.scalar },
2073
+ };
2074
+ }
2075
+ if (lhs_type !== null && lhs_type !== undefined && lhs_type.kind === 'struct') {
2076
+ const struct_info = c.structs.get(lhs_type.name);
2077
+ if (struct_info !== undefined) {
2078
+ const field = struct_info.fields.find(f => f.name === name);
2079
+ if (field !== undefined) {
2080
+ return { code: `${lhs_code}.${name}`, type: field.type };
2081
+ }
2082
+ }
2083
+ }
2084
+ return { code: `${lhs_code}.${name}`, type: T_UNKNOWN };
2085
+ }
2086
+
2087
+ function isSwizzlePattern(name) {
2088
+ if (name.length === 0 || name.length > 4) return false;
2089
+ for (const ch of name) {
2090
+ if (!SWIZZLE_LETTERS.has(ch)) return false;
2091
+ }
2092
+ return true;
2093
+ }
2094
+
2095
+ /**
2096
+ * Compile a `call_phrase` and return both the JS code and the inferred result type.
2097
+ *
2098
+ * Special cases:
2099
+ * - `vec3<f32>(a, b, c)` → `wgsl.vec3f(a, b, c)` (vec result)
2100
+ * - `f32(x)` → `wgsl.f32(x)` (scalar result)
2101
+ * - WGSL builtin (mix, clamp, dot, length, ...) → `wgsl.<name>(...)` (best-effort type)
2102
+ * - User function → `name(...)` (uses declared return type)
2103
+ *
2104
+ * @returns {{ code: string, type: WgslType }}
2105
+ */
2106
+ function compileCallPhraseTyped(c, node) {
2107
+ const ident_node = namedChildByType(node, 'template_elaborated_ident');
2108
+ const arg_list = namedChildByType(node, 'argument_expression_list');
2109
+
2110
+ const ident_inner = namedChildByType(ident_node, 'ident');
2111
+ const callee_name = ident_inner.text;
2112
+ const template = namedChildByType(ident_node, 'template_list');
2113
+
2114
+ /** @type {Array<{ code: string, type: WgslType }>} */
2115
+ const arg_results = [];
2116
+ if (arg_list !== null) {
2117
+ const comma_list = namedChildByType(arg_list, 'expression_comma_list');
2118
+ if (comma_list !== null) {
2119
+ for (const arg of namedChildrenByType(comma_list, 'expression')) {
2120
+ arg_results.push(compileExpression(c, arg));
2121
+ }
2122
+ }
2123
+ }
2124
+ const args = arg_results.map(a => a.code);
2125
+
2126
+ // 1) Vec aliases: vec3f, vec4i, vec2h, etc.
2127
+ if (VECTOR_ALIASES.hasOwnProperty(callee_name)) {
2128
+ const a = VECTOR_ALIASES[callee_name];
2129
+ return {
2130
+ code: `wgsl.${callee_name}(${args.join(', ')})`,
2131
+ type: { kind: 'vec', size: a.size, scalar: a.scalar },
2132
+ };
2133
+ }
2134
+ // 2) Templated vec: vec3<f32>(...).
2135
+ if (callee_name === 'vec2' || callee_name === 'vec3' || callee_name === 'vec4') {
2136
+ const size = parseInt(callee_name.slice(3));
2137
+ let scalar;
2138
+ if (template !== null) {
2139
+ const targs = extractTemplateArgs(template);
2140
+ if (targs.length > 0) scalar = targs[0];
2141
+ }
2142
+ if (scalar === undefined) {
2143
+ // No explicit template — infer the component type from the arguments, mirroring
2144
+ // WGSL's overload resolution. e.g. `vec4(some_u32)` is `vec4<u32>`, not `vec4<f32>`.
2145
+ for (const a of arg_results) {
2146
+ const t = a.type;
2147
+ if (t && t.kind === 'primitive' && t.name !== 'bool') { scalar = t.name; break; }
2148
+ if (t && (t.kind === 'vec' || t.kind === 'mat')) { scalar = t.scalar; break; }
2149
+ }
2150
+ if (scalar === undefined) scalar = 'f32';
2151
+ }
2152
+ return {
2153
+ code: `wgsl.${vecAliasName(size, scalar)}(${args.join(', ')})`,
2154
+ type: { kind: 'vec', size, scalar },
2155
+ };
2156
+ }
2157
+ // 3) Matrix constructors.
2158
+ {
2159
+ const m = callee_name.match(/^mat(\d)x(\d)([ifh]?)$/);
2160
+ if (m !== null) {
2161
+ const cols = parseInt(m[1]);
2162
+ const rows = parseInt(m[2]);
2163
+ const suffix = m[3];
2164
+ let scalar;
2165
+ if (suffix === 'f') scalar = 'f32';
2166
+ else if (suffix === 'i') scalar = 'i32';
2167
+ else if (suffix === 'h') scalar = 'f16';
2168
+ else if (template !== null) {
2169
+ const targs = extractTemplateArgs(template);
2170
+ scalar = targs[0] || 'f32';
2171
+ } else {
2172
+ scalar = 'f32';
2173
+ }
2174
+ const fn_suffix = suffix !== '' ? '' : ({ f32: 'f', i32: 'i', f16: 'h' }[scalar] || 'f');
2175
+ return {
2176
+ code: `wgsl.${callee_name}${fn_suffix}(${args.join(', ')})`,
2177
+ type: { kind: 'mat', cols, rows, scalar },
2178
+ };
2179
+ }
2180
+ }
2181
+
2182
+ // 4) Scalar casts: f32(x), i32(x), u32(x), f16(x), bool(x).
2183
+ if (PRIMITIVE_NAMES.has(callee_name)) {
2184
+ return {
2185
+ code: `wgsl.${callee_name}(${args.join(', ')})`,
2186
+ type: { kind: 'primitive', name: callee_name },
2187
+ };
2188
+ }
2189
+
2190
+ // 5) `bitcast<T>(x)` — JS can't tell a u32 from an f32 by value alone, so we emit the
2191
+ // source type alongside the target. The compiler infers source from the argument's tracked
2192
+ // type; if that's unknown the runtime falls back to a heuristic.
2193
+ if (callee_name === 'bitcast') {
2194
+ let target = 'f32';
2195
+ if (template !== null) {
2196
+ const targs = extractTemplateArgs(template);
2197
+ if (targs.length > 0) target = targs[0];
2198
+ }
2199
+ const arg_type = arg_results[0] !== undefined ? arg_results[0].type : null;
2200
+ let source = null;
2201
+ if (arg_type) {
2202
+ if (arg_type.kind === 'primitive') source = arg_type.name;
2203
+ else if (arg_type.kind === 'vec') source = arg_type.scalar;
2204
+ }
2205
+ const source_arg = source !== null ? `, ${JSON.stringify(source)}` : '';
2206
+ // Parse the target string: `vec3<u32>` etc. become a vec type so that downstream
2207
+ // swizzle/component access compiles correctly. Bare `f32` / `u32` / `i32` stay primitive.
2208
+ let resultType;
2209
+ const vec_match = target.match(/^vec(\d)<([^>]+)>$/);
2210
+ if (vec_match !== null) {
2211
+ resultType = { kind: 'vec', size: parseInt(vec_match[1]), scalar: vec_match[2] };
2212
+ } else {
2213
+ resultType = { kind: 'primitive', name: target };
2214
+ }
2215
+ return {
2216
+ code: `wgsl.bitcast(${args[0]}, ${JSON.stringify(target)}${source_arg})`,
2217
+ type: resultType,
2218
+ };
2219
+ }
2220
+
2221
+ // 6) array / array<T, N> constructor.
2222
+ if (callee_name === 'array') {
2223
+ return { code: `[${args.join(', ')}]`, type: T_UNKNOWN };
2224
+ }
2225
+
2226
+ // 7) Struct constructor.
2227
+ if (c.structs.has(callee_name)) {
2228
+ const info = c.structs.get(callee_name);
2229
+ const parts = [`(() => { const __s__ = new ${callee_name}();`];
2230
+ for (let i = 0; i < info.fields.length && i < args.length; i++) {
2231
+ parts.push(`__s__.${info.fields[i].name} = ${args[i]};`);
2232
+ }
2233
+ parts.push(`return __s__; })()`);
2234
+ return { code: parts.join(' '), type: { kind: 'struct', name: callee_name } };
2235
+ }
2236
+
2237
+ // 8) User-defined function.
2238
+ if (c.functions.has(callee_name)) {
2239
+ return {
2240
+ code: `${callee_name}(${args.join(', ')})`,
2241
+ type: c.functions.get(callee_name).returnType,
2242
+ };
2243
+ }
2244
+
2245
+ // 9) WGSL builtins — best-effort type inference for common helpers.
2246
+ return {
2247
+ code: `wgsl.${callee_name}(${args.join(', ')})`,
2248
+ type: inferBuiltinReturnType(callee_name, arg_results),
2249
+ };
2250
+ }
2251
+
2252
+ /**
2253
+ * Best-effort return type for WGSL builtins commonly used in compute shaders.
2254
+ * Handles the cases needed for accurate swizzle / member-access codegen downstream.
2255
+ */
2256
+ function inferBuiltinReturnType(name, args) {
2257
+ // Element-wise vector-preserving math: result follows the first vector argument.
2258
+ const ELEMENT_WISE = new Set([
2259
+ 'abs', 'sign', 'floor', 'ceil', 'round', 'trunc', 'fract', 'sqrt', 'inverseSqrt',
2260
+ 'exp', 'exp2', 'log', 'log2', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan',
2261
+ 'pow', 'atan2', 'min', 'max', 'step', 'clamp', 'saturate', 'mix', 'smoothstep',
2262
+ 'normalize', 'reflect', 'fma',
2263
+ // not math, but the same shape: the result has the type of its one argument
2264
+ 'subgroupBroadcastFirst',
2265
+ ]);
2266
+ if (ELEMENT_WISE.has(name)) {
2267
+ for (const a of args) {
2268
+ if (a.type && (a.type.kind === 'vec' || a.type.kind === 'mat')) return a.type;
2269
+ }
2270
+ if (args[0] && args[0].type) return args[0].type;
2271
+ return T_UNKNOWN;
2272
+ }
2273
+ if (name === 'length' || name === 'distance' || name === 'dot' || name === 'determinant') {
2274
+ return { kind: 'primitive', name: 'f32' };
2275
+ }
2276
+ if (name === 'cross') {
2277
+ return args[0] && args[0].type && args[0].type.kind === 'vec'
2278
+ ? args[0].type
2279
+ : { kind: 'vec', size: 3, scalar: 'f32' };
2280
+ }
2281
+ if (name === 'all' || name === 'any') return { kind: 'primitive', name: 'bool' };
2282
+ if (name === 'select') {
2283
+ if (args[0] && args[0].type) return args[0].type;
2284
+ if (args[1] && args[1].type) return args[1].type;
2285
+ return T_UNKNOWN;
2286
+ }
2287
+ if (name === 'textureLoad' || name === 'textureSampleLevel' || name === 'textureSample' || name === 'textureSampleGrad') {
2288
+ return { kind: 'vec', size: 4, scalar: 'f32' };
2289
+ }
2290
+ // Both gathers return the four texels of a 2x2 footprint. Saying so here matters more than
2291
+ // for most builtins: the shadow chunks immediately swizzle the result (`samples.wzxy`), and
2292
+ // an unknown type degrades swizzle codegen downstream.
2293
+ if (name === 'textureGather' || name === 'textureGatherCompare') {
2294
+ return { kind: 'vec', size: 4, scalar: 'f32' };
2295
+ }
2296
+ if (name === 'textureDimensions') {
2297
+ return { kind: 'vec', size: 2, scalar: 'u32' };
2298
+ }
2299
+ if (name === 'atomicLoad') return { kind: 'primitive', name: 'i32' };
2300
+ if (name === 'unpack2x16unorm' || name === 'unpack2x16snorm' || name === 'unpack2x16float') {
2301
+ return { kind: 'vec', size: 2, scalar: 'f32' };
2302
+ }
2303
+ if (name === 'unpack4x8unorm' || name === 'unpack4x8snorm') {
2304
+ return { kind: 'vec', size: 4, scalar: 'f32' };
2305
+ }
2306
+ if (name === 'pack2x16unorm' || name === 'pack2x16snorm' || name === 'pack2x16float'
2307
+ || name === 'pack4x8unorm' || name === 'pack4x8snorm') {
2308
+ return { kind: 'primitive', name: 'u32' };
2309
+ }
2310
+ // extractBits/insertBits return the integer type (scalar or vector) of their first operand.
2311
+ if (name === 'extractBits' || name === 'insertBits') {
2312
+ return args[0] && args[0].type ? args[0].type : T_UNKNOWN;
2313
+ }
2314
+ return T_UNKNOWN;
2315
+ }
2316
+
2317
+ // =========================================================================
2318
+ // LHS expressions (for assignments)
2319
+ // =========================================================================
2320
+
2321
+ function compileLhs(c, node) {
2322
+ if (node.type !== 'lhs_expression') {
2323
+ return compileExpression(c, node);
2324
+ }
2325
+
2326
+ // Pointer deref / address-of: `*lhs` / `&lhs`. The emulator models pointers as values, so
2327
+ // both collapse to the operand's value. The grammar puts the `*` / `&` as an unnamed leading
2328
+ // token with a nested `lhs_expression` named child.
2329
+ //
2330
+ // A whole-value store through a deref (`*p = v`, with nothing selected out of it) is flagged
2331
+ // so {@link emitWriteStatement} writes through the reference rather than rebinding the local
2332
+ // — see `wgsl.store_ptr`. A store into part of the pointee (`(*p).x`, `(*p)[i]`) already
2333
+ // lands on the shared object and needs no such treatment.
2334
+ const prefix = textBeforeFirstNamed(node).trim();
2335
+ if (prefix === '*' || prefix === '&') {
2336
+ const nested_lhs = namedChildByType(node, 'lhs_expression');
2337
+ if (nested_lhs !== null) {
2338
+ const inner = compileLhs(c, nested_lhs);
2339
+
2340
+ const selects_part = node.namedChildren.some(
2341
+ child => child.type === 'component_or_swizzle_specifier'
2342
+ );
2343
+
2344
+ return {
2345
+ ...inner,
2346
+ through_pointer: prefix === '*' && !selects_part,
2347
+ };
2348
+ }
2349
+ }
2350
+
2351
+ const core = namedChildByType(node, 'core_lhs_expression');
2352
+ let code;
2353
+ let type;
2354
+ if (core !== null) {
2355
+ const ident = namedChildByType(core, 'ident');
2356
+ if (ident !== null) {
2357
+ code = ident.text;
2358
+ type = c.scope.lookup(ident.text);
2359
+ } else {
2360
+ const nested = namedChildByType(core, 'lhs_expression');
2361
+ if (nested !== null) {
2362
+ const r = compileLhs(c, nested);
2363
+ code = `(${r.code})`;
2364
+ type = r.type;
2365
+ } else {
2366
+ code = core.text;
2367
+ type = T_UNKNOWN;
2368
+ }
2369
+ }
2370
+ } else {
2371
+ code = node.text;
2372
+ type = T_UNKNOWN;
2373
+ }
2374
+
2375
+ for (const child of node.namedChildren) {
2376
+ if (child.type === 'component_or_swizzle_specifier') {
2377
+ const next = applyAccessSpecifier(c, child, code, type);
2378
+ code = next.code;
2379
+ type = next.type;
2380
+ }
2381
+ }
2382
+ return { code, type };
2383
+ }