wgsl-play 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/wgsl-play.js CHANGED
@@ -630,6 +630,112 @@ function emptyScope(parent, kind = "scope") {
630
630
  };
631
631
  }
632
632
  //#endregion
633
+ //#region ../wesl/src/LinkerUtil.ts
634
+ /** A module's top-level declarations of a given kind, narrowed to that elem type. */
635
+ function declsOfKind(moduleElem, kind) {
636
+ return moduleElem.decls.filter((e) => e.kind === kind);
637
+ }
638
+ /**
639
+ * Child elems of any AST node: the typed structural fields (attributes plus
640
+ * body / condition / decls / ...) in source order. Returns [] for leaf elems.
641
+ */
642
+ function childElems(elem) {
643
+ return [...elem.attributes ?? [], ...structuralFields(elem)];
644
+ }
645
+ /** The child elems held in a node's typed fields, in source order (attributes
646
+ * are added separately by {@link childElems}). Empty for leaf kinds. The
647
+ * switch is exhaustive: a new elem kind without a case fails to compile in the
648
+ * default, so children can't be silently dropped. */
649
+ function structuralFields(elem) {
650
+ switch (elem.kind) {
651
+ case "module": return elem.decls;
652
+ case "var":
653
+ case "gvar": return [
654
+ ...elem.template ?? [],
655
+ elem.name,
656
+ ...elem.init ? [elem.init] : []
657
+ ];
658
+ case "let":
659
+ case "const":
660
+ case "override": return [elem.name, ...elem.init ? [elem.init] : []];
661
+ case "alias": return [elem.name, elem.typeRef];
662
+ case "assert": return [elem.expression];
663
+ case "struct": return [elem.name, ...elem.members];
664
+ case "member": return [elem.name, elem.typeRef];
665
+ case "type": return [elem.name.refIdentElem, ...elem.templateParams ?? []];
666
+ case "expression": return [elem.expression];
667
+ case "binary-expression": return [elem.left, elem.right];
668
+ case "unary-expression":
669
+ case "parenthesized-expression": return [elem.expression];
670
+ case "component-expression": return [elem.base, elem.access];
671
+ case "component-member-expression": return [elem.base, elem.access];
672
+ case "call-expression": return [
673
+ elem.function,
674
+ ...elem.templateArgs ?? [],
675
+ ...elem.arguments
676
+ ];
677
+ case "param": return [elem.name];
678
+ case "typeDecl": return elem.typeRef ? [elem.decl, elem.typeRef] : [elem.decl];
679
+ case "fn": return [
680
+ elem.name,
681
+ ...elem.params,
682
+ ...elem.returnType ? [elem.returnType] : [],
683
+ elem.body
684
+ ];
685
+ case "block": return elem.body;
686
+ case "if": return [
687
+ elem.condition,
688
+ elem.body,
689
+ ...elem.else ? [elem.else] : []
690
+ ];
691
+ case "for": return [
692
+ elem.init,
693
+ elem.condition,
694
+ elem.update,
695
+ elem.body
696
+ ].filter(isDefined);
697
+ case "while": return [elem.condition, elem.body];
698
+ case "loop":
699
+ case "continuing": return [elem.body];
700
+ case "switch": return [
701
+ elem.selector,
702
+ ...elem.bodyAttributes ?? [],
703
+ ...elem.clauses
704
+ ];
705
+ case "switch-clause": return [...exprSelectors(elem.selectors), elem.body];
706
+ case "return": return elem.value ? [elem.value] : [];
707
+ case "break": return elem.condition ? [elem.condition] : [];
708
+ case "assign": return elem.lhs.kind === "phony" ? [elem.rhs] : [elem.lhs, elem.rhs];
709
+ case "increment":
710
+ case "decrement": return [elem.target];
711
+ case "call": return [elem.call];
712
+ case "continue":
713
+ case "discard":
714
+ case "empty": return [];
715
+ case "do": return [
716
+ elem.name,
717
+ ...elem.params,
718
+ elem.body
719
+ ];
720
+ case "import":
721
+ case "directive":
722
+ case "attribute":
723
+ case "name":
724
+ case "literal":
725
+ case "ref":
726
+ case "decl":
727
+ case "synthetic": return [];
728
+ default: return [];
729
+ }
730
+ }
731
+ function isDefined(value) {
732
+ return value !== void 0;
733
+ }
734
+ /** Drop the `"default"` sentinel, keeping only real case-selector expressions. */
735
+ function exprSelectors(selectors) {
736
+ return selectors.filter((s) => s !== "default");
737
+ }
738
+ //#endregion
633
739
  //#region ../wesl/src/StandardTypes.ts
634
740
  const stdFns = `bitcast all any select arrayLength
635
741
  abs acos acosh asin asinh atan atanh atan2 ceil clamp cos cosh
@@ -660,17 +766,6 @@ const stdFns = `bitcast all any select arrayLength
660
766
  subgroupInclusiveMul subgroupMax subgroupMin subgroupMul subgroupOr
661
767
  subgroupShuffle subgroupShuffleUp subgroupShuffleXor subgroupXor
662
768
  quadBroadcast quadSwapDiagonal quadSwapX quadSwapY`.split(/\s+/);
663
- const sampledTextureTypes = `
664
- texture_1d texture_2d texture_2d_array texture_3d
665
- texture_cube texture_cube_array
666
- `;
667
- const multisampledTextureTypes = `
668
- texture_multisampled_2d texture_depth_multisampled_2d
669
- `;
670
- const textureStorageTypes = `
671
- texture_storage_1d texture_storage_2d texture_storage_2d_array
672
- texture_storage_3d
673
- `;
674
769
  const stdTypes = `array atomic bool f16 f32 i32
675
770
  mat2x2 mat2x3 mat2x4 mat3x2 mat3x3 mat3x4 mat4x2 mat4x3 mat4x4
676
771
  mat2x2f mat2x3f mat2x4f mat3x2f mat3x3f mat3x4f
@@ -680,10 +775,18 @@ const stdTypes = `array atomic bool f16 f32 i32
680
775
  u32 vec2 vec3 vec4 ptr
681
776
  vec2i vec3i vec4i vec2u vec3u vec4u
682
777
  vec2f vec3f vec4f vec2h vec3h vec4h
683
- ${sampledTextureTypes}
684
- ${multisampledTextureTypes}
778
+
779
+ texture_1d texture_2d texture_2d_array texture_3d
780
+ texture_cube texture_cube_array
781
+
782
+
783
+ texture_multisampled_2d texture_depth_multisampled_2d
784
+
685
785
  texture_external
686
- ${textureStorageTypes}
786
+
787
+ texture_storage_1d texture_storage_2d texture_storage_2d_array
788
+ texture_storage_3d
789
+
687
790
  texture_depth_2d texture_depth_2d_array texture_depth_cube
688
791
  texture_depth_cube_array
689
792
  sampler sampler_comparison
@@ -731,6 +834,22 @@ function stdEnumerant(name) {
731
834
  }
732
835
  //#endregion
733
836
  //#region ../wesl/src/LowerAndEmit.ts
837
+ /** Statement kinds that emitStatement must not follow with a ';': compound
838
+ * statements take none, locals already carry their own, empty needs none. */
839
+ const noSemicolon = new Set([
840
+ "block",
841
+ "if",
842
+ "for",
843
+ "while",
844
+ "loop",
845
+ "continuing",
846
+ "switch",
847
+ "empty",
848
+ "var",
849
+ "let",
850
+ "const",
851
+ "assert"
852
+ ]);
734
853
  /** Traverse the AST, starting from root elements, emitting WGSL for each. */
735
854
  function lowerAndEmit(params) {
736
855
  const { srcBuilder, rootElems, conditions } = params;
@@ -738,17 +857,30 @@ function lowerAndEmit(params) {
738
857
  const emitContext = {
739
858
  conditions,
740
859
  srcBuilder,
741
- extracting
860
+ extracting,
861
+ indent: 0
742
862
  };
743
863
  const validElements = skipConditionalFiltering ? rootElems : filterValidElements(rootElems, conditions);
744
864
  for (const e of validElements) lowerAndEmitElem(e, emitContext);
745
865
  }
866
+ /** Format a diagnostic control as "(severity, rule)" for @diagnostic text. */
867
+ function diagnosticControlToString(severity, rule) {
868
+ const ruleStr = rule[0].name + (rule[1] !== null ? "." + rule[1].name : "");
869
+ return `(${severity.name}, ${ruleStr})`;
870
+ }
871
+ /** Trace through refersTo links until we find the declaration. */
872
+ function findDecl(ident) {
873
+ let i = ident;
874
+ do {
875
+ if (i.kind === "decl") return i;
876
+ i = i.refersTo;
877
+ } while (i);
878
+ throw new Error(`unresolved identifer: ${ident.originalName}`);
879
+ }
746
880
  function lowerAndEmitElem(e, ctx) {
747
881
  switch (e.kind) {
748
882
  case "import": return;
749
- case "text":
750
- emitText(e, ctx);
751
- return;
883
+ case "do": return;
752
884
  case "name":
753
885
  emitName(e, ctx);
754
886
  return;
@@ -771,24 +903,45 @@ function lowerAndEmitElem(e, ctx) {
771
903
  emitExpression(e, ctx);
772
904
  return;
773
905
  case "param":
906
+ emitAttributes(e.attributes, ctx);
907
+ emitTypedDecl(e.name, ctx);
908
+ return;
774
909
  case "typeDecl":
910
+ emitTypedDecl(e, ctx);
911
+ return;
775
912
  case "member":
776
- case "memberRef":
913
+ emitMember(e, ctx);
914
+ return;
777
915
  case "expression":
778
- case "type":
916
+ emitExpression(e.expression, ctx);
917
+ return;
779
918
  case "switch-clause":
780
- emitContents(e, ctx);
919
+ emitSwitchClause(e, ctx);
781
920
  return;
782
- case "stuff":
783
- emitStuff(e, ctx);
921
+ case "type":
922
+ emitTypeRef(e, ctx);
784
923
  return;
785
924
  case "module":
786
925
  emitModule(e, ctx);
787
926
  return;
788
927
  case "var":
789
928
  case "let":
790
- case "statement":
929
+ case "block":
930
+ case "if":
931
+ case "for":
932
+ case "while":
933
+ case "loop":
791
934
  case "continuing":
935
+ case "switch":
936
+ case "return":
937
+ case "break":
938
+ case "continue":
939
+ case "discard":
940
+ case "assign":
941
+ case "increment":
942
+ case "decrement":
943
+ case "call":
944
+ case "empty":
792
945
  emitStatement(e, ctx);
793
946
  return;
794
947
  case "override":
@@ -800,52 +953,131 @@ function lowerAndEmitElem(e, ctx) {
800
953
  return;
801
954
  case "fn":
802
955
  emitRootElemNl(ctx);
956
+ emitRootLeading(e, ctx);
803
957
  emitFn(e, ctx);
958
+ emitTrailingComments(e, ctx);
804
959
  return;
805
960
  case "struct":
806
961
  emitRootElemNl(ctx);
962
+ emitRootLeading(e, ctx);
807
963
  emitStruct(e, ctx);
964
+ emitTrailingComments(e, ctx);
808
965
  return;
809
966
  case "attribute":
810
967
  emitAttribute(e, ctx);
811
968
  return;
812
969
  case "directive":
970
+ ctx.srcBuilder.addNl();
971
+ emitRootLeading(e, ctx);
813
972
  emitDirective(e, ctx);
973
+ emitTrailingComments(e, ctx);
814
974
  return;
815
975
  default: assertUnreachable(e);
816
976
  }
817
977
  }
818
- function emitStuff(e, ctx) {
819
- emitContentsWithTrimming(e, ctx);
978
+ function emitName(e, ctx) {
979
+ ctx.srcBuilder.add(e.name, e.start, e.end);
820
980
  }
821
- function emitModule(e, ctx) {
822
- const validElements = filterValidElements(e.contents, ctx.conditions);
823
- for (const child of validElements) {
824
- if (child.kind === "text") {
825
- if (child.srcModule.src.slice(child.start, child.end).trim() === "") continue;
981
+ function emitSynthetic(e, ctx) {
982
+ const { text } = e;
983
+ ctx.srcBuilder.addSynthetic(text, text, 0, text.length);
984
+ }
985
+ function emitRefIdent(e, ctx) {
986
+ if (e.ident.std) ctx.srcBuilder.add(e.ident.originalName, e.start, e.end);
987
+ else ctx.srcBuilder.add(displayName(findDecl(e.ident)), e.start, e.end);
988
+ }
989
+ function emitDeclIdent(e, ctx) {
990
+ ctx.srcBuilder.add(displayName(e.ident), e.start, e.end);
991
+ }
992
+ /** Emit an expression with any comments attached to it. Comments inside an
993
+ * expression (e.g. `foo(1, /* x *\/ 2)`) ride on the relevant sub-node and are
994
+ * emitted inline around it here. */
995
+ function emitExpression(e, ctx) {
996
+ emitInlineLeading(e, ctx);
997
+ emitExpressionCore(e, ctx);
998
+ emitInlineTrailing(e, ctx);
999
+ }
1000
+ function emitAttributes(attributes, ctx) {
1001
+ attributes?.forEach((a) => {
1002
+ emitInlineLeading(a, ctx);
1003
+ if (emitAttribute(a, ctx)) {
1004
+ emitInlineTrailing(a, ctx);
1005
+ ctx.srcBuilder.add(" ", a.start, a.end);
826
1006
  }
827
- lowerAndEmitElem(child, ctx);
1007
+ });
1008
+ }
1009
+ /** Emit a declared identifier with its optional `: type` annotation. */
1010
+ function emitTypedDecl(name, ctx) {
1011
+ emitInlineLeading(name, ctx);
1012
+ emitDeclIdent(name.decl, ctx);
1013
+ if (name.typeRef) {
1014
+ ctx.srcBuilder.appendNext(": ");
1015
+ emitTypeRef(name.typeRef, ctx);
1016
+ }
1017
+ emitInlineTrailing(name, ctx);
1018
+ }
1019
+ /** Emit a struct member from its typed fields: `[attrs] name: type`. */
1020
+ function emitMember(member, ctx) {
1021
+ emitAttributes(member.attributes, ctx);
1022
+ emitName(member.name, ctx);
1023
+ ctx.srcBuilder.appendNext(": ");
1024
+ emitTypeRef(member.typeRef, ctx);
1025
+ }
1026
+ /** A `case sel, ...:` or `default:` clause with its `{ ... }` body. The selector
1027
+ * colon is optional in WGSL but kept here as the canonical form. */
1028
+ function emitSwitchClause(e, ctx) {
1029
+ const builder = ctx.srcBuilder;
1030
+ emitLeadingComments(e, ctx);
1031
+ newLine(ctx);
1032
+ emitAttributes(e.attributes, ctx);
1033
+ if (e.selectors.length === 1 && e.selectors[0] === "default") builder.appendNext("default");
1034
+ else {
1035
+ builder.appendNext("case ");
1036
+ e.selectors.forEach((sel, i) => {
1037
+ if (i > 0) builder.appendNext(", ");
1038
+ if (sel === "default") builder.appendNext("default");
1039
+ else emitExpression(sel, ctx);
1040
+ });
828
1041
  }
1042
+ builder.appendNext(": ");
1043
+ emitBlock(e.body, ctx);
1044
+ emitTrailingComments(e, ctx);
829
1045
  }
830
- function emitStatement(e, ctx) {
831
- if (!(e.contents.length > 0 && e.contents[0].kind === "attribute")) emitAttributes(e.attributes, ctx);
832
- emitContents(e, ctx);
1046
+ /** Emit a type reference structurally: name plus an optional <...> arg list. */
1047
+ function emitTypeRef(e, ctx) {
1048
+ emitRefIdent(e.name.refIdentElem, ctx);
1049
+ if (e.templateParams) emitTemplateArgs(e.templateParams, ctx);
1050
+ }
1051
+ function emitModule(e, ctx) {
1052
+ const validElements = filterValidElements(e.decls, ctx.conditions);
1053
+ for (const child of validElements) lowerAndEmitElem(child, ctx);
1054
+ }
1055
+ /** Emit one statement on its own line, with attached leading/trailing comments. */
1056
+ function emitStatement(stmt, ctx) {
1057
+ emitLeadingComments(stmt, ctx);
1058
+ newLine(ctx);
1059
+ emitCoreSemi(stmt, ctx);
1060
+ emitTrailingComments(stmt, ctx);
833
1061
  }
834
1062
  function emitRootDecl(e, ctx) {
835
1063
  emitRootElemNl(ctx);
836
- if (!(e.contents.length > 0 && e.contents[0].kind === "attribute")) emitAttributes(e.attributes, ctx);
837
- emitContentsWithTrimming(e, ctx);
1064
+ emitRootLeading(e, ctx);
1065
+ emitValueDecl(e, ctx);
1066
+ emitTrailingComments(e, ctx);
838
1067
  }
839
1068
  /** Emit newlines between root elements. */
840
1069
  function emitRootElemNl(ctx) {
841
1070
  ctx.srcBuilder.addNl();
842
1071
  ctx.srcBuilder.addNl();
843
1072
  }
844
- function emitText(e, ctx) {
845
- ctx.srcBuilder.addCopy(e.start, e.end);
846
- }
847
- function emitName(e, ctx) {
848
- ctx.srcBuilder.add(e.name, e.start, e.end);
1073
+ /** Leading comments for a root declaration, each on its own line. The inter-decl
1074
+ * spacing already left us at a fresh line, so a newline follows each comment
1075
+ * (rather than preceding it as in emitLeadingComments). */
1076
+ function emitRootLeading(e, ctx) {
1077
+ for (const c of e.commentsBefore ?? []) {
1078
+ emitComment(c, ctx);
1079
+ newLine(ctx);
1080
+ }
849
1081
  }
850
1082
  /** Emit function explicitly to control commas between conditional parameters. */
851
1083
  function emitFn(e, ctx) {
@@ -853,27 +1085,35 @@ function emitFn(e, ctx) {
853
1085
  const { conditions, srcBuilder: builder } = ctx;
854
1086
  emitAttributes(attributes, ctx);
855
1087
  builder.add("fn ", name.start - 3, name.start);
1088
+ emitInlineLeading(name, ctx);
856
1089
  emitDeclIdent(name, ctx);
1090
+ emitInlineTrailing(name, ctx);
857
1091
  builder.appendNext("(");
858
1092
  const validParams = filterValidElements(params, conditions);
859
1093
  validParams.forEach((p, i) => {
860
- if (!(p.contents.length > 0 && p.contents[0].kind === "attribute")) emitAttributes(p.attributes, ctx);
861
- emitContentsNoWs(p, ctx);
1094
+ emitInlineLeading(p, ctx);
1095
+ emitAttributes(p.attributes, ctx);
1096
+ emitTypedDecl(p.name, ctx);
1097
+ emitInlineTrailing(p, ctx);
862
1098
  if (i < validParams.length - 1) builder.appendNext(", ");
863
1099
  });
864
1100
  builder.appendNext(") ");
865
1101
  if (returnType) {
866
1102
  builder.appendNext("-> ");
867
1103
  emitAttributes(returnAttributes, ctx);
868
- emitContentsNoWs(returnType, ctx);
1104
+ emitInlineLeading(returnType, ctx);
1105
+ emitTypeRef(returnType, ctx);
869
1106
  builder.appendNext(" ");
870
1107
  }
871
- emitContents(body, ctx);
1108
+ emitBlock(body, ctx);
872
1109
  }
873
- function emitAttributes(attributes, ctx) {
874
- attributes?.forEach((a) => {
875
- if (emitAttribute(a, ctx)) ctx.srcBuilder.add(" ", a.start, a.end);
876
- });
1110
+ /** Trailing comments: kept on the element's line, after its text. */
1111
+ function emitTrailingComments(e, ctx) {
1112
+ if (!e.commentsAfter) return;
1113
+ for (const c of e.commentsAfter) {
1114
+ ctx.srcBuilder.appendNext(" ");
1115
+ emitComment(c, ctx);
1116
+ }
877
1117
  }
878
1118
  /** Emit structs explicitly to control commas between conditional members. */
879
1119
  function emitStruct(e, ctx) {
@@ -887,125 +1127,22 @@ function emitStruct(e, ctx) {
887
1127
  }
888
1128
  emitAttributes(attributes, ctx);
889
1129
  srcBuilder.add("struct ", start, name.start);
1130
+ emitInlineLeading(name, ctx);
890
1131
  emitDeclIdent(name, ctx);
891
- if (validLength === 1) {
1132
+ emitInlineTrailing(name, ctx);
1133
+ if (validLength === 1 && !hasComments(validMembers[0])) {
892
1134
  srcBuilder.appendNext(" { ");
893
- emitContentsWithTrimming(validMembers[0], ctx);
1135
+ emitMember(validMembers[0], ctx);
894
1136
  srcBuilder.appendNext(" }");
895
1137
  srcBuilder.addNl();
896
1138
  } else {
897
1139
  srcBuilder.appendNext(" {");
898
1140
  srcBuilder.addNl();
899
- validMembers.forEach((m) => {
900
- srcBuilder.appendNext(" ");
901
- emitContentsNoWs(m, ctx);
902
- srcBuilder.appendNext(",");
903
- srcBuilder.addNl();
904
- });
1141
+ for (const m of validMembers) emitMemberLine(m, ctx);
905
1142
  srcBuilder.appendNext("}");
906
1143
  srcBuilder.addNl();
907
1144
  }
908
1145
  }
909
- function warnEmptyStruct(e) {
910
- const { name, members } = e;
911
- const condStr = members.length ? "(with current conditions)" : "";
912
- failIdentElem(name, `struct '${name.ident.originalName}' has no members ${condStr}`);
913
- }
914
- function emitSynthetic(e, ctx) {
915
- const { text } = e;
916
- ctx.srcBuilder.addSynthetic(text, text, 0, text.length);
917
- }
918
- function emitContents(elem, ctx) {
919
- const validElements = filterValidElements(elem.contents, ctx.conditions);
920
- for (const e of validElements) lowerAndEmitElem(e, ctx);
921
- }
922
- /** Emit contents with leading/trailing whitespace trimming (V2 parser). */
923
- function emitContentsWithTrimming(elem, ctx) {
924
- const validElements = filterValidElements(elem.contents, ctx.conditions);
925
- const firstEmit = validElements.findIndex((e) => !isConditionalAttr(e));
926
- const lastEmit = validElements.findLastIndex((e) => !isConditionalAttr(e));
927
- validElements.forEach((elem, i) => {
928
- if (elem.kind === "text") {
929
- let text = elem.srcModule.src.slice(elem.start, elem.end);
930
- if (i === firstEmit) text = text.trimStart();
931
- if (i === lastEmit) text = text.trimEnd();
932
- if (text) ctx.srcBuilder.add(text, elem.start, elem.end);
933
- } else lowerAndEmitElem(elem, ctx);
934
- });
935
- }
936
- function isConditionalAttr(e) {
937
- if (e.kind !== "attribute") return false;
938
- const { kind } = e.attribute;
939
- return kind === "@if" || kind === "@elif" || kind === "@else";
940
- }
941
- /** Emit contents without whitespace. */
942
- function emitContentsNoWs(elem, ctx) {
943
- filterValidElements(elem.contents, ctx.conditions).forEach((e) => {
944
- if (e.kind === "text") {
945
- const { srcModule, start, end } = e;
946
- if (srcModule.src.slice(start, end).trim() === "") return;
947
- }
948
- lowerAndEmitElem(e, ctx);
949
- });
950
- }
951
- function emitRefIdent(e, ctx) {
952
- if (e.ident.std) ctx.srcBuilder.add(e.ident.originalName, e.start, e.end);
953
- else {
954
- const mangledName = displayName(findDecl(e.ident));
955
- ctx.srcBuilder.add(mangledName, e.start, e.end);
956
- }
957
- }
958
- function emitDeclIdent(e, ctx) {
959
- const mangledName = displayName(e.ident);
960
- ctx.srcBuilder.add(mangledName, e.start, e.end);
961
- }
962
- function emitExpression(e, ctx) {
963
- const { kind } = e;
964
- if (kind === "literal") {
965
- ctx.srcBuilder.add(e.value, e.start, e.end);
966
- return;
967
- }
968
- if (kind === "ref") {
969
- emitRefIdent(e, ctx);
970
- return;
971
- }
972
- if (kind === "type") {
973
- emitContents(e, ctx);
974
- return;
975
- }
976
- if (kind === "binary-expression") {
977
- emitExpression(e.left, ctx);
978
- ctx.srcBuilder.add(` ${e.operator.value} `, e.operator.span[0], e.operator.span[1]);
979
- emitExpression(e.right, ctx);
980
- return;
981
- }
982
- if (kind === "unary-expression") {
983
- ctx.srcBuilder.add(e.operator.value, e.operator.span[0], e.operator.span[1]);
984
- emitExpression(e.expression, ctx);
985
- return;
986
- }
987
- if (kind === "parenthesized-expression") {
988
- emitExpression(e.expression, ctx);
989
- return;
990
- }
991
- if (kind === "call-expression") {
992
- emitExpression(e.function, ctx);
993
- if (e.templateArgs) for (const targ of e.templateArgs) lowerAndEmitElem(targ, ctx);
994
- for (const arg of e.arguments) emitExpression(arg, ctx);
995
- return;
996
- }
997
- if (kind === "component-expression") {
998
- emitExpression(e.base, ctx);
999
- emitExpression(e.access, ctx);
1000
- return;
1001
- }
1002
- if (kind === "component-member-expression") {
1003
- emitExpression(e.base, ctx);
1004
- if (e.access.kind === "name") ctx.srcBuilder.add(e.access.name, e.access.start, e.access.end);
1005
- return;
1006
- }
1007
- assertUnreachable(kind);
1008
- }
1009
1146
  function emitAttribute(e, ctx) {
1010
1147
  const { kind } = e.attribute;
1011
1148
  if (kind === "@if" || kind === "@elif" || kind === "@else") return false;
@@ -1015,11 +1152,13 @@ function emitAttribute(e, ctx) {
1015
1152
  return true;
1016
1153
  }
1017
1154
  if (kind === "@builtin") {
1018
- ctx.srcBuilder.add("@builtin(" + e.attribute.param.name + ")", e.start, e.end);
1155
+ const builtinStr = `@builtin(${e.attribute.param.name})`;
1156
+ ctx.srcBuilder.add(builtinStr, e.start, e.end);
1019
1157
  return true;
1020
1158
  }
1021
1159
  if (kind === "@diagnostic") {
1022
- const diagStr = "@diagnostic" + diagnosticControlToString(e.attribute.severity, e.attribute.rule);
1160
+ const { severity, rule } = e.attribute;
1161
+ const diagStr = `@diagnostic${diagnosticControlToString(severity, rule)}`;
1023
1162
  ctx.srcBuilder.add(diagStr, e.start, e.end);
1024
1163
  return true;
1025
1164
  }
@@ -1030,24 +1169,6 @@ function emitAttribute(e, ctx) {
1030
1169
  }
1031
1170
  assertUnreachable(kind);
1032
1171
  }
1033
- function emitStandardAttribute(e, ctx) {
1034
- if (e.attribute.kind !== "@attribute") return;
1035
- const { params } = e.attribute;
1036
- if (!params || params.length === 0) {
1037
- ctx.srcBuilder.add("@" + e.attribute.name, e.start, e.end);
1038
- return;
1039
- }
1040
- ctx.srcBuilder.add("@" + e.attribute.name + "(", e.start, params[0].start);
1041
- for (let i = 0; i < params.length; i++) {
1042
- emitContents(params[i], ctx);
1043
- if (i < params.length - 1) ctx.srcBuilder.add(",", params[i].end, params[i + 1].start);
1044
- }
1045
- ctx.srcBuilder.add(")", params[params.length - 1].end, e.end);
1046
- }
1047
- function diagnosticControlToString(severity, rule) {
1048
- const ruleStr = rule[0].name + (rule[1] !== null ? "." + rule[1].name : "");
1049
- return `(${severity.name}, ${ruleStr})`;
1050
- }
1051
1172
  function emitDirective(e, ctx) {
1052
1173
  const { directive } = e;
1053
1174
  const { kind } = directive;
@@ -1069,14 +1190,351 @@ function displayName(declIdent) {
1069
1190
  }
1070
1191
  return declIdent.mangledName || declIdent.originalName;
1071
1192
  }
1072
- /** Trace through refersTo links until we find the declaration. */
1073
- function findDecl(ident) {
1074
- let i = ident;
1075
- do {
1076
- if (i.kind === "decl") return i;
1077
- i = i.refersTo;
1078
- } while (i);
1079
- throw new Error(`unresolved identifer: ${ident.originalName}`);
1193
+ /** Leading comments on an inline node: block comments get a trailing space, line
1194
+ * comments force a newline so the following code is not swallowed. */
1195
+ function emitInlineLeading(e, ctx) {
1196
+ for (const c of e.commentsBefore ?? []) {
1197
+ emitComment(c, ctx);
1198
+ if (c.style === "line") newLine(ctx);
1199
+ else ctx.srcBuilder.appendNext(" ");
1200
+ }
1201
+ }
1202
+ function emitExpressionCore(e, ctx) {
1203
+ const builder = ctx.srcBuilder;
1204
+ switch (e.kind) {
1205
+ case "literal":
1206
+ builder.add(e.value, e.start, e.end);
1207
+ return;
1208
+ case "ref":
1209
+ emitRefIdent(e, ctx);
1210
+ return;
1211
+ case "type":
1212
+ emitTypeRef(e, ctx);
1213
+ return;
1214
+ case "binary-expression": {
1215
+ const [start, end] = e.operator.span;
1216
+ emitExpression(e.left, ctx);
1217
+ builder.add(` ${e.operator.value} `, start, end);
1218
+ emitExpression(e.right, ctx);
1219
+ return;
1220
+ }
1221
+ case "unary-expression": {
1222
+ const { value, start, end } = e.operator;
1223
+ builder.add(value, start, end);
1224
+ emitExpression(e.expression, ctx);
1225
+ return;
1226
+ }
1227
+ case "parenthesized-expression":
1228
+ builder.appendNext("(");
1229
+ emitExpression(e.expression, ctx);
1230
+ builder.appendNext(")");
1231
+ return;
1232
+ case "call-expression":
1233
+ emitExpression(e.function, ctx);
1234
+ if (e.templateArgs) emitTemplateArgs(e.templateArgs, ctx);
1235
+ builder.appendNext("(");
1236
+ e.arguments.forEach((arg, i) => {
1237
+ if (i > 0) builder.appendNext(", ");
1238
+ emitExpression(arg, ctx);
1239
+ });
1240
+ builder.appendNext(")");
1241
+ return;
1242
+ case "component-expression":
1243
+ emitExpression(e.base, ctx);
1244
+ builder.appendNext("[");
1245
+ emitExpression(e.access, ctx);
1246
+ builder.appendNext("]");
1247
+ return;
1248
+ case "component-member-expression":
1249
+ emitExpression(e.base, ctx);
1250
+ builder.add("." + e.access.name, e.access.start, e.access.end);
1251
+ return;
1252
+ default: assertUnreachable(e);
1253
+ }
1254
+ }
1255
+ /** Trailing comments on an inline node: a leading space, plus a newline after a
1256
+ * line comment so following code stays off its line. */
1257
+ function emitInlineTrailing(e, ctx) {
1258
+ for (const c of e.commentsAfter ?? []) {
1259
+ ctx.srcBuilder.appendNext(" ");
1260
+ emitComment(c, ctx);
1261
+ if (c.style === "line") newLine(ctx);
1262
+ }
1263
+ }
1264
+ /** Leading comments: each on its own indented line above the element. */
1265
+ function emitLeadingComments(e, ctx) {
1266
+ if (!e.commentsBefore) return;
1267
+ for (const c of e.commentsBefore) {
1268
+ if (c.blankBefore) ctx.srcBuilder.addNl();
1269
+ newLine(ctx);
1270
+ emitComment(c, ctx);
1271
+ }
1272
+ }
1273
+ /** Start a fresh line at the current indent. */
1274
+ function newLine(ctx) {
1275
+ ctx.srcBuilder.addNl();
1276
+ if (ctx.indent > 0) ctx.srcBuilder.appendNext(" ".repeat(ctx.indent));
1277
+ }
1278
+ /** Emit a `{ ... }` block, one statement per indented line. A block with no
1279
+ * statements collapses to `{ }` unless it holds dangling inner comments. */
1280
+ function emitBlock(e, ctx) {
1281
+ emitAttributes(e.attributes, ctx);
1282
+ const stmts = filterValidElements(e.body, ctx.conditions);
1283
+ if (stmts.length === 0 && !e.innerComments?.length) {
1284
+ ctx.srcBuilder.appendNext("{ }");
1285
+ return;
1286
+ }
1287
+ ctx.srcBuilder.appendNext("{");
1288
+ const inner = childIndent(ctx);
1289
+ for (const comment of e.innerComments ?? []) {
1290
+ newLine(inner);
1291
+ emitComment(comment, inner);
1292
+ }
1293
+ for (const stmt of stmts) emitStatement(stmt, inner);
1294
+ newLine(ctx);
1295
+ ctx.srcBuilder.appendNext("}");
1296
+ }
1297
+ /** Emit a comma-separated template argument list: <a, b, c>. */
1298
+ function emitTemplateArgs(args, ctx) {
1299
+ ctx.srcBuilder.appendNext("<");
1300
+ args.forEach((a, i) => {
1301
+ if (i > 0) ctx.srcBuilder.appendNext(", ");
1302
+ emitExpression(a, ctx);
1303
+ });
1304
+ ctx.srcBuilder.appendNext(">");
1305
+ }
1306
+ /** Emit a statement's syntax followed by ';' unless its kind takes none. */
1307
+ function emitCoreSemi(stmt, ctx) {
1308
+ emitStatementCore(stmt, ctx);
1309
+ if (!noSemicolon.has(stmt.kind)) ctx.srcBuilder.appendNext(";");
1310
+ }
1311
+ /** Emit a declaration from its typed fields, including its trailing ';':
1312
+ * `[attrs] var<...> name: T = init;`, `const name = init;`, `override n;`,
1313
+ * `alias name = T;`, `const_assert expr;`. */
1314
+ function emitValueDecl(e, ctx) {
1315
+ emitAttributes(e.attributes, ctx);
1316
+ const builder = ctx.srcBuilder;
1317
+ switch (e.kind) {
1318
+ case "var":
1319
+ case "gvar":
1320
+ builder.appendNext("var");
1321
+ if (e.template) emitVarTemplate(e.template, ctx);
1322
+ builder.appendNext(" ");
1323
+ emitTypedDecl(e.name, ctx);
1324
+ emitInit(e.init, ctx);
1325
+ break;
1326
+ case "let":
1327
+ case "const":
1328
+ case "override":
1329
+ builder.appendNext(`${e.kind} `);
1330
+ emitTypedDecl(e.name, ctx);
1331
+ emitInit(e.init, ctx);
1332
+ break;
1333
+ case "alias":
1334
+ builder.appendNext("alias ");
1335
+ emitInlineLeading(e.name, ctx);
1336
+ emitDeclIdent(e.name, ctx);
1337
+ emitInlineTrailing(e.name, ctx);
1338
+ builder.appendNext(" = ");
1339
+ emitTypeRef(e.typeRef, ctx);
1340
+ break;
1341
+ case "assert":
1342
+ builder.appendNext("const_assert ");
1343
+ emitExpression(e.expression, ctx);
1344
+ break;
1345
+ default: assertUnreachable(e);
1346
+ }
1347
+ builder.appendNext(";");
1348
+ }
1349
+ function emitComment(c, ctx) {
1350
+ ctx.srcBuilder.add(c.srcModule.src.slice(c.start, c.end), c.start, c.end);
1351
+ }
1352
+ function warnEmptyStruct(e) {
1353
+ const { name, members } = e;
1354
+ const condStr = members.length ? "(with current conditions)" : "";
1355
+ failIdentElem(name, `struct '${name.ident.originalName}' has no members ${condStr}`);
1356
+ }
1357
+ /** Whether an element carries any attached comments. */
1358
+ function hasComments(e) {
1359
+ return !!(e.commentsBefore?.length || e.commentsAfter?.length);
1360
+ }
1361
+ /** Emit one struct member on its own line: leading comments above, `name: type,`,
1362
+ * then trailing comments inline. */
1363
+ function emitMemberLine(m, ctx) {
1364
+ const builder = ctx.srcBuilder;
1365
+ for (const c of m.commentsBefore ?? []) {
1366
+ builder.appendNext(" ");
1367
+ emitComment(c, ctx);
1368
+ builder.addNl();
1369
+ }
1370
+ builder.appendNext(" ");
1371
+ emitMember(m, ctx);
1372
+ builder.appendNext(",");
1373
+ for (const c of m.commentsAfter ?? []) {
1374
+ builder.appendNext(" ");
1375
+ emitComment(c, ctx);
1376
+ }
1377
+ builder.addNl();
1378
+ }
1379
+ function emitStandardAttribute(e, ctx) {
1380
+ if (e.attribute.kind !== "@attribute") return;
1381
+ const { params } = e.attribute;
1382
+ if (!params || params.length === 0) {
1383
+ ctx.srcBuilder.add("@" + e.attribute.name, e.start, e.end);
1384
+ return;
1385
+ }
1386
+ ctx.srcBuilder.add("@" + e.attribute.name + "(", e.start, params[0].start);
1387
+ params.forEach((param, i) => {
1388
+ if (i > 0) ctx.srcBuilder.appendNext(", ");
1389
+ emitExpression(param.expression, ctx);
1390
+ });
1391
+ ctx.srcBuilder.add(")", params[params.length - 1].end, e.end);
1392
+ }
1393
+ /** A child context indented one level deeper. */
1394
+ function childIndent(ctx) {
1395
+ return {
1396
+ ...ctx,
1397
+ indent: ctx.indent + 1
1398
+ };
1399
+ }
1400
+ /** Emit a statement's syntax, without surrounding line breaks, ';', or comments. */
1401
+ function emitStatementCore(stmt, ctx) {
1402
+ if (stmt.kind === "var" || stmt.kind === "let" || stmt.kind === "const" || stmt.kind === "assert") {
1403
+ emitValueDecl(stmt, ctx);
1404
+ return;
1405
+ }
1406
+ if (stmt.kind !== "block") emitAttributes(stmt.attributes, ctx);
1407
+ const builder = ctx.srcBuilder;
1408
+ switch (stmt.kind) {
1409
+ case "block":
1410
+ emitBlock(stmt, ctx);
1411
+ return;
1412
+ case "if":
1413
+ emitIf(stmt, ctx);
1414
+ return;
1415
+ case "for":
1416
+ emitFor(stmt, ctx);
1417
+ return;
1418
+ case "while":
1419
+ emitWhile(stmt, ctx);
1420
+ return;
1421
+ case "loop":
1422
+ builder.appendNext("loop ");
1423
+ emitBlock(stmt.body, ctx);
1424
+ return;
1425
+ case "continuing":
1426
+ builder.appendNext("continuing ");
1427
+ emitBlock(stmt.body, ctx);
1428
+ return;
1429
+ case "switch":
1430
+ emitSwitch(stmt, ctx);
1431
+ return;
1432
+ case "return":
1433
+ builder.appendNext("return");
1434
+ if (stmt.value) {
1435
+ builder.appendNext(" ");
1436
+ emitExpression(stmt.value, ctx);
1437
+ }
1438
+ return;
1439
+ case "break":
1440
+ builder.appendNext("break");
1441
+ if (stmt.condition) {
1442
+ builder.appendNext(" if ");
1443
+ emitExpression(stmt.condition, ctx);
1444
+ }
1445
+ return;
1446
+ case "continue":
1447
+ builder.appendNext("continue");
1448
+ return;
1449
+ case "discard":
1450
+ builder.appendNext("discard");
1451
+ return;
1452
+ case "assign":
1453
+ emitAssign(stmt, ctx);
1454
+ return;
1455
+ case "increment":
1456
+ emitExpression(stmt.target, ctx);
1457
+ builder.appendNext("++");
1458
+ return;
1459
+ case "decrement":
1460
+ emitExpression(stmt.target, ctx);
1461
+ builder.appendNext("--");
1462
+ return;
1463
+ case "call":
1464
+ emitExpression(stmt.call, ctx);
1465
+ return;
1466
+ case "empty": return;
1467
+ default: assertUnreachable(stmt);
1468
+ }
1469
+ }
1470
+ /** Emit a var's `<address_space[, access_mode]>` enumerant template. */
1471
+ function emitVarTemplate(template, ctx) {
1472
+ const builder = ctx.srcBuilder;
1473
+ builder.appendNext("<");
1474
+ template.forEach((name, i) => {
1475
+ if (i > 0) builder.appendNext(", ");
1476
+ emitName(name, ctx);
1477
+ });
1478
+ builder.appendNext(">");
1479
+ }
1480
+ /** Emit a ` = init` clause, if present. */
1481
+ function emitInit(init, ctx) {
1482
+ if (!init) return;
1483
+ ctx.srcBuilder.appendNext(" = ");
1484
+ emitExpression(init, ctx);
1485
+ }
1486
+ /** if / else-if / else: a nested IfElem prints as `else if`, a BlockElem as `else`. */
1487
+ function emitIf(e, ctx) {
1488
+ const builder = ctx.srcBuilder;
1489
+ builder.appendNext("if ");
1490
+ emitExpression(e.condition, ctx);
1491
+ builder.appendNext(" ");
1492
+ emitBlock(e.body, ctx);
1493
+ if (e.else) {
1494
+ builder.appendNext(" else ");
1495
+ if (e.else.kind === "if") emitIf(e.else, ctx);
1496
+ else emitBlock(e.else, ctx);
1497
+ }
1498
+ }
1499
+ /** for (init; condition; update) { ... }. The init takes a ';' like any
1500
+ * statement; the update is the last clause before ')', so it gets none. */
1501
+ function emitFor(e, ctx) {
1502
+ const builder = ctx.srcBuilder;
1503
+ builder.appendNext("for (");
1504
+ if (e.init) emitCoreSemi(e.init, ctx);
1505
+ else builder.appendNext(";");
1506
+ builder.appendNext(" ");
1507
+ if (e.condition) emitExpression(e.condition, ctx);
1508
+ builder.appendNext("; ");
1509
+ if (e.update) emitStatementCore(e.update, ctx);
1510
+ builder.appendNext(") ");
1511
+ emitBlock(e.body, ctx);
1512
+ }
1513
+ function emitWhile(e, ctx) {
1514
+ ctx.srcBuilder.appendNext("while ");
1515
+ emitExpression(e.condition, ctx);
1516
+ ctx.srcBuilder.appendNext(" ");
1517
+ emitBlock(e.body, ctx);
1518
+ }
1519
+ function emitSwitch(e, ctx) {
1520
+ const builder = ctx.srcBuilder;
1521
+ builder.appendNext("switch ");
1522
+ emitExpression(e.selector, ctx);
1523
+ builder.appendNext(" ");
1524
+ emitAttributes(e.bodyAttributes, ctx);
1525
+ builder.appendNext("{");
1526
+ const inner = childIndent(ctx);
1527
+ for (const clause of filterValidElements(e.clauses, ctx.conditions)) emitSwitchClause(clause, inner);
1528
+ newLine(ctx);
1529
+ builder.appendNext("}");
1530
+ }
1531
+ /** lhs op rhs, with the phony target printed as `_`. */
1532
+ function emitAssign(e, ctx) {
1533
+ if (e.lhs.kind === "phony") ctx.srcBuilder.add("_", e.lhs.span[0], e.lhs.span[1]);
1534
+ else emitExpression(e.lhs, ctx);
1535
+ const [start, end] = e.op.span;
1536
+ ctx.srcBuilder.add(` ${e.op.value} `, start, end);
1537
+ emitExpression(e.rhs, ctx);
1080
1538
  }
1081
1539
  //#endregion
1082
1540
  //#region ../wesl/src/debug/ScopeToString.ts
@@ -1188,53 +1646,184 @@ var ParseError = class extends Error {
1188
1646
  }
1189
1647
  };
1190
1648
  //#endregion
1191
- //#region ../wesl/src/parse/ContentsHelpers.ts
1192
- /** Push partial element onto stack for content collection. */
1193
- function beginElem(ctx, kind, contents = []) {
1194
- ctx.state.context.openElems.push({
1195
- kind,
1196
- contents: [...contents]
1197
- });
1198
- }
1199
- /** Pop element from stack, fill gaps with TextElems, return contents. */
1200
- function finishContents(ctx, start, end) {
1201
- const open = ctx.state.context.openElems.pop();
1202
- if (!open) throw new Error("No open element to close");
1203
- return coverWithText(ctx, open.contents, start, end);
1204
- }
1205
- /** Finish element: get end position, close contents, return complete element. */
1206
- function finishElem(kind, start, ctx, params) {
1207
- const end = ctx.stream.checkpoint();
1208
- return {
1209
- kind,
1210
- start,
1211
- end,
1212
- contents: finishContents(ctx, start, end),
1213
- ...params
1214
- };
1215
- }
1216
- /** Create a TextElem */
1217
- function makeText(srcModule, start, end) {
1218
- return {
1219
- kind: "text",
1649
+ //#region ../wesl/src/parse/AttachComments.ts
1650
+ const tab = 9;
1651
+ const lineFeed = 10;
1652
+ const verticalTab = 11;
1653
+ const formFeed = 12;
1654
+ const carriageReturn = 13;
1655
+ const space = 32;
1656
+ const nextLine = 133;
1657
+ const leftToRightMark = 8206;
1658
+ const rightToLeftMark = 8207;
1659
+ const lineSeparator = 8232;
1660
+ const paragraphSeparator = 8233;
1661
+ /**
1662
+ * Attach every recorded comment to a node in the parsed tree.
1663
+ *
1664
+ * Each comment run (a contiguous group of comments between two real tokens) is
1665
+ * anchored to the deepest node that contains it in a gap between children, then
1666
+ * distributed to the surrounding children:
1667
+ * - a comment that begins its own line leads the next child (`commentsBefore`);
1668
+ * - an inline comment trails the previous child (`commentsAfter`), unless it
1669
+ * hugs the next child -- only blank space and comments, no separator token,
1670
+ * between them -- in which case it leads the next child instead;
1671
+ * - comments before a closing token with no following child trail the last child,
1672
+ * or land in an empty block's `innerComments`.
1673
+ *
1674
+ * Descending into expressions (via {@link childElems}) means interior comments
1675
+ * like the one in `foo(1, /* x *\/ 2)` are preserved on the `2`, not dropped.
1676
+ */
1677
+ function attachComments(ctx, moduleElem) {
1678
+ const { srcModule } = ctx.state.stable;
1679
+ const cache = /* @__PURE__ */ new Map();
1680
+ for (const run of ctx.stream.commentRuns()) distribute(deepestContaining(moduleElem, run[0].start, runEnd(run), cache), run, srcModule, cache);
1681
+ }
1682
+ /** The deepest node whose span contains the whole [start, end) range, found by
1683
+ * descending into the child that brackets it. Comments live in gaps between
1684
+ * tokens, so the result is the node holding the gap, with the run between two
1685
+ * of its children. */
1686
+ function deepestContaining(root, start, end, cache) {
1687
+ let node = root;
1688
+ while (true) {
1689
+ const child = positionedChildren(node, cache).find((c) => c.start <= start && end <= c.end);
1690
+ if (!child) return node;
1691
+ node = child;
1692
+ }
1693
+ }
1694
+ function runEnd(run) {
1695
+ return run[run.length - 1].end;
1696
+ }
1697
+ /** Split a comment run between the previous child (trailing) and the next child
1698
+ * (leading) of its anchor, or onto the last child / inner comments when it
1699
+ * dangles before a closing token. */
1700
+ function distribute(anchor, run, srcModule, cache) {
1701
+ const children = positionedChildren(anchor, cache);
1702
+ const start = run[0].start;
1703
+ const end = runEnd(run);
1704
+ const prev = children.findLast((c) => c.end <= start);
1705
+ const next = children.find((c) => c.start >= end);
1706
+ if (!next) {
1707
+ if (prev) addComments(prev, "commentsAfter", run, srcModule);
1708
+ else if (anchor.kind === "block") anchor.innerComments = run.map((t) => makeComment(t, srcModule));
1709
+ else addComments(anchor, "commentsBefore", run, srcModule);
1710
+ return;
1711
+ }
1712
+ const split = splitPoint(prev, next, run, srcModule.src);
1713
+ if (prev && split > 0) addComments(prev, "commentsAfter", run.slice(0, split), srcModule);
1714
+ if (split < run.length) addComments(next, "commentsBefore", run.slice(split), srcModule);
1715
+ }
1716
+ function positionedChildren(node, cache) {
1717
+ let children = cache.get(node);
1718
+ if (children === void 0) {
1719
+ children = positioned(childElems(node));
1720
+ cache.set(node, children);
1721
+ }
1722
+ return children;
1723
+ }
1724
+ /** Append converted comments to an element's leading or trailing list. */
1725
+ function addComments(elem, field, trivia, srcModule) {
1726
+ const comments = trivia.map((t) => makeComment(t, srcModule));
1727
+ const existing = elem[field];
1728
+ elem[field] = existing ? [...existing, ...comments] : comments;
1729
+ }
1730
+ function makeComment(trivia, srcModule) {
1731
+ const { style, start, end } = trivia;
1732
+ const comment = {
1733
+ kind: "comment",
1734
+ style,
1220
1735
  start,
1221
1736
  end,
1222
1737
  srcModule
1223
1738
  };
1739
+ if (lineBreaksBefore(srcModule.src, start) >= 2) comment.blankBefore = true;
1740
+ return comment;
1224
1741
  }
1225
- /** Fill gaps between child elements with TextElems. */
1226
- function coverWithText(ctx, contents, start, end) {
1227
- const { srcModule } = ctx.state.stable;
1228
- const sorted = contents.slice().sort((a, b) => a.start - b.start);
1229
- const elems = [];
1230
- let pos = start;
1231
- for (const elem of sorted) {
1232
- if (pos < elem.start) elems.push(makeText(srcModule, pos, elem.start));
1233
- elems.push(elem);
1234
- pos = elem.end;
1235
- }
1236
- if (pos < end) elems.push(makeText(srcModule, pos, end));
1237
- return elems;
1742
+ /**
1743
+ * Index into `run` where it flips from trailing `prev` to leading `next`:
1744
+ * caller attaches run[0, split) to `prev` and run[split, end) to `next`.
1745
+ *
1746
+ * The aim is to keep each comment with the code it describes, so it stays
1747
+ * meaningful after the tree is reordered or reformatted. That follows how people
1748
+ * write comments: a comment on its own line documents what comes after it, while
1749
+ * a comment sharing a line with code documents that code. Hence:
1750
+ * - with no previous child the whole run leads (split 0);
1751
+ * - otherwise the split is the first comment that begins its own line;
1752
+ * - failing that (an all-inline run), it is the start of the suffix that hugs
1753
+ * `next` on the same line, so the comment in `foo(1, /* x *\/ 2)` documents,
1754
+ * and lands on, the `2`.
1755
+ */
1756
+ function splitPoint(prev, next, run, src) {
1757
+ if (!prev) return 0;
1758
+ const ownLine = firstOwnLine(run, src);
1759
+ return ownLine >= 0 ? ownLine : hugsNextStart(run, next.start, src);
1760
+ }
1761
+ /** Source-positioned children, sorted in source order. Synthetic elems (no
1762
+ * source position) cannot anchor comments and are dropped. Children usually
1763
+ * arrive already in source order, so the sort is skipped when possible.
1764
+ * Filtering and order-checking share one pass: this runs per node, so it's
1765
+ * kept deliberately lean. */
1766
+ function positioned(elems) {
1767
+ const result = [];
1768
+ let sorted = true;
1769
+ let lastStart = -1;
1770
+ for (const e of elems) {
1771
+ if (e.kind === "synthetic") continue;
1772
+ if (e.start < lastStart) sorted = false;
1773
+ lastStart = e.start;
1774
+ result.push(e);
1775
+ }
1776
+ if (!sorted) result.sort((a, b) => a.start - b.start);
1777
+ return result;
1778
+ }
1779
+ /** Count line breaks in the whitespace run immediately before `pos`, capped at 2:
1780
+ * callers only need none / one line break / a blank line. Scans backward over
1781
+ * blankspace, stopping at the first non-blankspace char, so cost is the gap
1782
+ * length, not the source length. `\r\n` counts as one break. */
1783
+ function lineBreaksBefore(src, pos) {
1784
+ let count = 0;
1785
+ for (let i = pos - 1; i >= 0; i--) {
1786
+ const c = src.charCodeAt(i);
1787
+ if (isLineBreak(c)) {
1788
+ if (c === lineFeed && src.charCodeAt(i - 1) === carriageReturn) i--;
1789
+ } else if (isInlineSpace(c)) continue;
1790
+ else break;
1791
+ if (++count >= 2) return 2;
1792
+ }
1793
+ return count;
1794
+ }
1795
+ /** Index of the first comment that begins its own line (after a line break),
1796
+ * or -1 if none do. */
1797
+ function firstOwnLine(run, src) {
1798
+ return run.findIndex((t) => lineBreaksBefore(src, t.start) >= 1);
1799
+ }
1800
+ /** Start index of the trailing suffix of `run` that is joined to `next` by
1801
+ * same-line whitespace only (no line break, no separator token between them).
1802
+ * Returns run.length when nothing hugs `next`. */
1803
+ function hugsNextStart(run, nextStart, src) {
1804
+ let suffixStart = run.length;
1805
+ let rightStart = nextStart;
1806
+ for (let i = run.length - 1; i >= 0; i--) {
1807
+ const comment = run[i];
1808
+ if (!sameLineGap(src, comment.end, rightStart)) break;
1809
+ suffixStart = i;
1810
+ rightStart = comment.start;
1811
+ }
1812
+ return suffixStart;
1813
+ }
1814
+ /** A WGSL line break code point. `\r\n` is two of these; callers coalesce it. */
1815
+ function isLineBreak(c) {
1816
+ return c === lineFeed || c === carriageReturn || c === verticalTab || c === formFeed || c === nextLine || c === lineSeparator || c === paragraphSeparator;
1817
+ }
1818
+ /** WGSL blankspace that stays on the same line (not a line break). */
1819
+ function isInlineSpace(c) {
1820
+ return c === space || c === tab || c === leftToRightMark || c === rightToLeftMark;
1821
+ }
1822
+ /** True when [from, to) is inline blankspace only (no line break): the two ends
1823
+ * sit on the same line with nothing but same-line spaces between. */
1824
+ function sameLineGap(src, from, to) {
1825
+ for (let i = from; i < to; i++) if (!isInlineSpace(src.charCodeAt(i))) return false;
1826
+ return true;
1238
1827
  }
1239
1828
  //#endregion
1240
1829
  //#region ../wesl/src/parse/ExpressionUtil.ts
@@ -1248,9 +1837,11 @@ function makeLiteral(token) {
1248
1837
  };
1249
1838
  }
1250
1839
  function makeUnaryOperator(token) {
1840
+ const [start, end] = token.span;
1251
1841
  return {
1252
1842
  value: token.text,
1253
- span: token.span
1843
+ start,
1844
+ end
1254
1845
  };
1255
1846
  }
1256
1847
  function makeBinaryOperator(token) {
@@ -1260,12 +1851,11 @@ function makeBinaryOperator(token) {
1260
1851
  };
1261
1852
  }
1262
1853
  function makeUnaryExpression(operator, expr) {
1263
- const [start] = operator.span;
1264
1854
  return {
1265
1855
  kind: "unary-expression",
1266
1856
  operator,
1267
1857
  expression: expr,
1268
- start,
1858
+ start: operator.start,
1269
1859
  end: expr.end
1270
1860
  };
1271
1861
  }
@@ -1326,13 +1916,16 @@ function expectWord(stream, errorMsg) {
1326
1916
  stream.nextToken();
1327
1917
  return token;
1328
1918
  }
1329
- /** Parse expression and throw ParseError if not found. */
1919
+ /** Parse a required expression, throwing if absent. */
1330
1920
  function expectExpression(ctx, errorMsg = "Expected expression") {
1331
1921
  const expr = parseExpression(ctx);
1332
1922
  if (!expr) throwParseError(ctx.stream, errorMsg);
1333
- if (ctx.options.preserveExpressions) ctx.addElem(expr);
1334
1923
  return expr;
1335
1924
  }
1925
+ /** Parse an optional expression. */
1926
+ function parseContentExpression(ctx, opts) {
1927
+ return parseExpression(ctx, opts);
1928
+ }
1336
1929
  /** Throw a ParseError at the current/next token position. */
1337
1930
  function throwParseError(stream, message) {
1338
1931
  const weslStream = stream;
@@ -1388,18 +1981,27 @@ function makeRefIdentElem(ctx, refIdent, start, end) {
1388
1981
  refIdent.refIdentElem = elem;
1389
1982
  return elem;
1390
1983
  }
1391
- function isConditionalAttribute$1(attr) {
1984
+ /** @return true if the attribute is a conditional (@if, @elif, @else) */
1985
+ function isConditionalAttribute(attr) {
1392
1986
  const { kind } = attr;
1393
1987
  return kind === "@if" || kind === "@elif" || kind === "@else";
1394
1988
  }
1395
1989
  /** @return true if any attribute is a conditional (@if, @elif, @else) */
1396
1990
  function hasConditionalAttribute(attributes) {
1397
- return attributes.some((attr) => isConditionalAttribute$1(attr.attribute));
1991
+ return attributes.some((attr) => isConditionalAttribute(attr.attribute));
1992
+ }
1993
+ /** The first conditional (@if, @elif, @else) attribute in the list, if any. */
1994
+ function conditionalAttribute(attributes) {
1995
+ return attributes.find((a) => isConditionalAttribute(a.attribute))?.attribute;
1398
1996
  }
1399
1997
  /** Attach non-empty attributes array to element. */
1400
1998
  function attachAttributes(elem, attributes) {
1401
1999
  if (attributes?.length) elem.attributes = attributes;
1402
2000
  }
2001
+ /** Normalize an empty attribute list to undefined (the elem's "no attributes"). */
2002
+ function attrsOrUndef(attrs) {
2003
+ return attrs.length ? attrs : void 0;
2004
+ }
1403
2005
  /** Link a DeclIdentElem's ident to its parent declaration. */
1404
2006
  function linkDeclIdentElem(declIdentElem, declElem) {
1405
2007
  declIdentElem.ident.declElem = declElem;
@@ -1542,10 +2144,7 @@ function parseIdent(ctx, conditionRef) {
1542
2144
  const ident = ctx.createRefIdent(parts.join("::"));
1543
2145
  if (conditionRef) ident.conditionRef = true;
1544
2146
  const refIdentElem = makeRefIdentElem(ctx, ident, start, end);
1545
- if (!conditionRef) {
1546
- ctx.saveIdent(ident);
1547
- ctx.addElem(refIdentElem);
1548
- }
2147
+ if (!conditionRef) ctx.saveIdent(ident);
1549
2148
  return refIdentElem;
1550
2149
  }
1551
2150
  /** Check if token is valid as a path segment (word or allowed keyword). */
@@ -1567,19 +2166,20 @@ function parseSimpleTypeRef(ctx) {
1567
2166
  if (!path) return null;
1568
2167
  const { parts, start, end: nameEnd } = path;
1569
2168
  const refIdent = ctx.createRefIdent(parts.join("::"));
1570
- beginElem(ctx, "type");
1571
- const refIdentElem = makeRefIdentElem(ctx, refIdent, start, nameEnd);
2169
+ makeRefIdentElem(ctx, refIdent, start, nameEnd);
1572
2170
  ctx.saveIdent(refIdent);
1573
- ctx.addElem(refIdentElem);
1574
- return finishElem("type", start, ctx, {
2171
+ return {
2172
+ kind: "type",
1575
2173
  name: refIdent,
1576
- templateParams: ctx.stream.nextTemplateStartToken() ? parseTemplateParams(ctx) : void 0
1577
- });
2174
+ templateParams: ctx.stream.nextTemplateStartToken() ? parseTemplateParams(ctx) : void 0,
2175
+ start,
2176
+ end: ctx.stream.checkpoint()
2177
+ };
1578
2178
  }
1579
2179
  /** Parse comma-separated template parameters until closing '>'. */
1580
2180
  function parseTemplateParams(ctx) {
1581
2181
  const { stream } = ctx;
1582
- if (consumeTemplateEnd(stream)) return [];
2182
+ if (consumeTemplateEnd(stream)) throwParseError(stream, "Empty template parameter list '<>'");
1583
2183
  const params = [parseTemplateParam(ctx)];
1584
2184
  while (stream.matchText(",")) params.push(parseTemplateParam(ctx));
1585
2185
  if (!consumeTemplateEnd(stream)) throwParseError(stream, "Expected '>' or ',' after template parameter");
@@ -1594,8 +2194,8 @@ function consumeTemplateEnd(stream) {
1594
2194
  /** Grammar: template_arg_expression : expression */
1595
2195
  function parseTemplateParam(ctx) {
1596
2196
  const expr = parseExpression(ctx, { inTemplate: true });
1597
- if (expr) return expr;
1598
- throwParseError(ctx.stream, "Expected expression in template parameters");
2197
+ if (!expr) throwParseError(ctx.stream, "Expected expression in template parameters");
2198
+ return expr;
1599
2199
  }
1600
2200
  //#endregion
1601
2201
  //#region ../wesl/src/parse/ParseCall.ts
@@ -1606,7 +2206,7 @@ function parseTemplateParam(ctx) {
1606
2206
  function parseCallSuffix(ctx, current, parseExpr) {
1607
2207
  if (current.kind !== "ref" && current.kind !== "type") return null;
1608
2208
  const { stream } = ctx;
1609
- const templateArgs = current.kind === "type" ? current.templateParams ?? null : parseCallTemplateArgs(ctx);
2209
+ const templateArgs = current.kind === "ref" ? parseCallTemplateArgs(ctx) : null;
1610
2210
  if (!stream.matchText("(")) return null;
1611
2211
  const args = [];
1612
2212
  while (true) {
@@ -1771,8 +2371,7 @@ function parseTemplateElaboratedIdent(ctx, conditionRef) {
1771
2371
  name: refIdent.ident,
1772
2372
  templateParams,
1773
2373
  start: refIdent.start,
1774
- end: ctx.stream.checkpoint(),
1775
- contents: []
2374
+ end: ctx.stream.checkpoint()
1776
2375
  };
1777
2376
  }
1778
2377
  /** Parse postfix operators: member access, indexing, function calls. */
@@ -1799,7 +2398,9 @@ function parseMemberAccess(ctx, base) {
1799
2398
  function parseIndexAccess(ctx, base) {
1800
2399
  const { stream } = ctx;
1801
2400
  if (!stream.matchText("[")) return null;
1802
- return makeComponentExpression(base, expectExpression(ctx, "Expected expression in array index"), expect(stream, "]", "array index").span[1]);
2401
+ const indexExpr = parseExpression(ctx);
2402
+ if (!indexExpr) throwParseError(stream, "Expected expression in array index");
2403
+ return makeComponentExpression(base, indexExpr, expect(stream, "]", "array index").span[1]);
1803
2404
  }
1804
2405
  //#endregion
1805
2406
  //#region ../wesl/src/parse/ParseAttribute.ts
@@ -1820,6 +2421,20 @@ function parseElseAttribute(ctx) {
1820
2421
  function parseElifAttribute(ctx) {
1821
2422
  return parseConditionalAttribute(ctx, "elif", makeElifAttribute);
1822
2423
  }
2424
+ /** Parse WESL conditional attributes (@if, @elif, @else) */
2425
+ function parseWeslConditional(ctx) {
2426
+ const { stream } = ctx;
2427
+ const peeked = stream.peek();
2428
+ if (peeked?.text !== "@") return null;
2429
+ const startPos = peeked.span[0];
2430
+ const ifAttr = parseIfAttribute(ctx);
2431
+ if (ifAttr) return attributeElem(ifAttr, startPos, stream.checkpoint());
2432
+ const elifAttr = parseElifAttribute(ctx);
2433
+ if (elifAttr) return attributeElem(elifAttr, startPos, stream.checkpoint());
2434
+ const elseAttr = parseElseAttribute(ctx);
2435
+ if (elseAttr) return attributeElem(elseAttr, startPos, stream.checkpoint());
2436
+ return null;
2437
+ }
1823
2438
  /**
1824
2439
  * Grammar: attribute :
1825
2440
  * '@' ident_pattern_token argument_expression_list ?
@@ -1830,15 +2445,8 @@ function parseElifAttribute(ctx) {
1830
2445
  * WESL extensions: @if, @elif, @else
1831
2446
  */
1832
2447
  function parseAttribute(ctx) {
1833
- const { stream } = ctx;
1834
- const startPos = stream.checkpoint();
1835
- if (!stream.matchText("@")) return null;
1836
- stream.reset(startPos);
1837
- const weslAttr = parseWeslConditional(ctx);
1838
- if (weslAttr) return weslAttr;
1839
- const stdAttr = parseStandardAttribute(ctx);
1840
- if (stdAttr) return stdAttr;
1841
- return null;
2448
+ if (ctx.stream.peek()?.text !== "@") return null;
2449
+ return parseWeslConditional(ctx) ?? parseStandardAttribute(ctx);
1842
2450
  }
1843
2451
  /** Parse `@if(expr)` or `@elif(expr)` conditional attributes. */
1844
2452
  function parseConditionalAttribute(ctx, keyword, makeAttr) {
@@ -1847,13 +2455,15 @@ function parseConditionalAttribute(ctx, keyword, makeAttr) {
1847
2455
  if (!stream.matchSequence("@", keyword)) return null;
1848
2456
  expect(stream, "(", `@${keyword}`);
1849
2457
  const expr = parseExpression(ctx, true);
1850
- if (!expr) return null;
2458
+ if (!expr) throwParseError(stream, `Expected expression after @${keyword}(`);
1851
2459
  stream.matchText(",");
1852
2460
  expect(stream, ")", `@${keyword} expression`);
1853
- return makeAttr(makeTranslateTimeExpressionElem({
1854
- value: expr,
1855
- span: [startPos, stream.checkpoint()]
1856
- }));
2461
+ return makeAttr({
2462
+ kind: "translate-time-expression",
2463
+ expression: expr,
2464
+ start: startPos,
2465
+ end: stream.checkpoint()
2466
+ });
1857
2467
  }
1858
2468
  function makeIfAttribute(param) {
1859
2469
  return {
@@ -1870,19 +2480,13 @@ function makeElifAttribute(param) {
1870
2480
  param
1871
2481
  };
1872
2482
  }
1873
- /** Parse WESL conditional attributes (@if, @elif, @else) */
1874
- function parseWeslConditional(ctx) {
1875
- const { stream } = ctx;
1876
- const peeked = stream.peek();
1877
- if (peeked?.text !== "@") return null;
1878
- const startPos = peeked.span[0];
1879
- const ifAttr = parseIfAttribute(ctx);
1880
- if (ifAttr) return attributeElem(ifAttr, startPos, stream.checkpoint());
1881
- const elifAttr = parseElifAttribute(ctx);
1882
- if (elifAttr) return attributeElem(elifAttr, startPos, stream.checkpoint());
1883
- const elseAttr = parseElseAttribute(ctx);
1884
- if (elseAttr) return attributeElem(elseAttr, startPos, stream.checkpoint());
1885
- return null;
2483
+ function attributeElem(attribute, start, end) {
2484
+ return {
2485
+ kind: "attribute",
2486
+ attribute,
2487
+ start,
2488
+ end
2489
+ };
1886
2490
  }
1887
2491
  /** Parse a standard attribute (not @if/@elif/@else) */
1888
2492
  function parseStandardAttribute(ctx) {
@@ -1892,7 +2496,7 @@ function parseStandardAttribute(ctx) {
1892
2496
  if (!atToken) return null;
1893
2497
  const startPos = atToken.span[0];
1894
2498
  const nameToken = stream.peek();
1895
- if (!nameToken || nameToken.kind !== "word" && nameToken.kind !== "keyword") {
2499
+ if (nameToken?.kind !== "word" && nameToken?.kind !== "keyword") {
1896
2500
  stream.reset(resetPos);
1897
2501
  return null;
1898
2502
  }
@@ -1915,22 +2519,6 @@ function parseStandardAttribute(ctx) {
1915
2519
  params
1916
2520
  }, startPos, stream.checkpoint());
1917
2521
  }
1918
- function makeTranslateTimeExpressionElem(args) {
1919
- return {
1920
- kind: "translate-time-expression",
1921
- expression: args.value,
1922
- span: args.span
1923
- };
1924
- }
1925
- function attributeElem(attribute, start, end) {
1926
- return {
1927
- kind: "attribute",
1928
- attribute,
1929
- start,
1930
- end,
1931
- contents: []
1932
- };
1933
- }
1934
2522
  function parseBuiltinAttribute(ctx, startPos) {
1935
2523
  const { stream } = ctx;
1936
2524
  expect(stream, "(", "@builtin");
@@ -1951,9 +2539,6 @@ function parseInterpolateAttribute(ctx, startPos) {
1951
2539
  params
1952
2540
  }, startPos, stream.checkpoint());
1953
2541
  }
1954
- function parseNameElem(ctx) {
1955
- return makeNameElem(expectWord(ctx.stream, "Expected identifier"));
1956
- }
1957
2542
  /** @diagnostic(severity, rule) or @diagnostic(severity, namespace.rule) */
1958
2543
  function parseDiagnosticAttribute(ctx, startPos) {
1959
2544
  const { stream } = ctx;
@@ -1975,78 +2560,20 @@ function parseDiagnosticAttribute(ctx, startPos) {
1975
2560
  function parseAttributeParams(ctx) {
1976
2561
  return parseCommaList(ctx, parseAttrParam);
1977
2562
  }
2563
+ function parseNameElem(ctx) {
2564
+ return makeNameElem(expectWord(ctx.stream, "Expected identifier"));
2565
+ }
1978
2566
  function parseAttrParam(ctx) {
1979
2567
  const { stream } = ctx;
1980
2568
  const start = stream.checkpoint();
1981
- beginElem(ctx, "expression");
1982
- parseExpression(ctx);
1983
- const end = stream.checkpoint();
2569
+ const expression = parseExpression(ctx);
2570
+ if (!expression) throwParseError(stream, "Expected attribute parameter");
1984
2571
  return {
1985
2572
  kind: "expression",
2573
+ expression,
1986
2574
  start,
1987
- end,
1988
- contents: finishContents(ctx, start, end)
1989
- };
1990
- }
1991
- //#endregion
1992
- //#region ../wesl/src/parse/ParseDirective.ts
1993
- /** Grammar: global_directive : diagnostic_directive | enable_directive | requires_directive */
1994
- function parseDirective(ctx) {
1995
- const { stream } = ctx;
1996
- const startPos = stream.checkpoint();
1997
- const attributes = parseAttributeList(ctx);
1998
- const attrs = attributes.length > 0 ? attributes : void 0;
1999
- const result = parseExtensionDirective(ctx, "enable", attrs) || parseExtensionDirective(ctx, "requires", attrs) || parseDiagnosticDirective(ctx, attrs);
2000
- if (!result) stream.reset(startPos);
2001
- return result;
2002
- }
2003
- /** Grammar: enable_directive | requires_directive : keyword extension_list ';' */
2004
- function parseExtensionDirective(ctx, keyword, attributes) {
2005
- const { stream } = ctx;
2006
- const token = stream.matchText(keyword);
2007
- if (!token) return null;
2008
- const extensions = parseCommaList(ctx, parseDirectiveName);
2009
- expect(stream, ";", `${keyword} directive`);
2010
- return makeDirectiveElem({
2011
- kind: keyword,
2012
- extensions
2013
- }, token, stream, attributes);
2014
- }
2015
- function parseDirectiveName(ctx) {
2016
- return makeNameElem(expectWord(ctx.stream, "Expected identifier in name list"));
2017
- }
2018
- /**
2019
- * Grammar: diagnostic_directive : 'diagnostic' diagnostic_control ';'
2020
- * Grammar: diagnostic_control : '(' severity_control_name ',' diagnostic_rule_name ',' ? ')'
2021
- */
2022
- function parseDiagnosticDirective(ctx, attributes) {
2023
- const { stream } = ctx;
2024
- const token = stream.matchText("diagnostic");
2025
- if (!token) return null;
2026
- expect(stream, "(", "diagnostic");
2027
- const severity = makeNameElem(expectWord(stream, "Expected severity in diagnostic"));
2028
- expect(stream, ",", "diagnostic severity");
2029
- const ruleName = makeNameElem(expectWord(stream, "Expected rule name in diagnostic"));
2030
- let subrule = null;
2031
- if (stream.matchText(".")) subrule = makeNameElem(expectWord(stream, "Expected subrule name after '.'"));
2032
- stream.matchText(",");
2033
- expect(stream, ")", "diagnostic rule");
2034
- expect(stream, ";", "diagnostic directive");
2035
- return makeDirectiveElem({
2036
- kind: "diagnostic",
2037
- severity,
2038
- rule: [ruleName, subrule]
2039
- }, token, stream, attributes);
2040
- }
2041
- function makeDirectiveElem(directive, token, stream, attributes) {
2042
- const elem = {
2043
- kind: "directive",
2044
- directive,
2045
- start: attributes?.[0]?.start ?? token.span[0],
2046
2575
  end: stream.checkpoint()
2047
2576
  };
2048
- attachAttributes(elem, attributes);
2049
- return elem;
2050
2577
  }
2051
2578
  //#endregion
2052
2579
  //#region ../wesl/src/parse/ParseControlFlow.ts
@@ -2057,37 +2584,50 @@ function makeDirectiveElem(directive, token, stream, attributes) {
2057
2584
  function parseIfStatement(ctx, attributes) {
2058
2585
  const startPos = beginStatement(ctx, "if", attributes);
2059
2586
  if (startPos === null) return null;
2060
- expectExpression(ctx, "Expected condition expression after 'if'");
2061
- const body = expectCompound(ctx, "Expected '{' after if condition");
2062
- ctx.addElem(body);
2063
- parseElseChain(ctx);
2064
- return finishBlockStatement(startPos, ctx, attributes);
2587
+ return finishStatement("if", startPos, ctx, {
2588
+ condition: expectExpression(ctx, "Expected condition after 'if'"),
2589
+ body: expectCompound(ctx, "Expected '{' after if condition"),
2590
+ else: parseElseChain(ctx)
2591
+ }, attributes);
2065
2592
  }
2066
2593
  /** Grammar: switch_statement : attribute* 'switch' expression switch_body */
2067
2594
  function parseSwitchStatement(ctx, attributes) {
2068
2595
  const startPos = beginStatement(ctx, "switch", attributes);
2069
2596
  if (startPos === null) return null;
2070
- expectExpression(ctx, "Expected expression after 'switch'");
2071
- expectSwitchClauses(ctx);
2072
- return finishBlockStatement(startPos, ctx, attributes);
2597
+ const selector = expectExpression(ctx, "Expected expression after 'switch'");
2598
+ const { bodyAttributes, clauses } = expectSwitchClauses(ctx);
2599
+ return finishStatement("switch", startPos, ctx, {
2600
+ selector,
2601
+ clauses,
2602
+ bodyAttributes
2603
+ }, attributes);
2073
2604
  }
2074
2605
  /**
2075
2606
  * Grammar: else_if_clause : 'else' 'if' expression compound_statement
2076
2607
  * Grammar: else_clause : 'else' compound_statement
2608
+ *
2609
+ * An else-if nests as an IfElem in the outer if's `else` field; a plain else is
2610
+ * a BlockElem. Emit and the AST dump read these typed fields.
2077
2611
  */
2078
2612
  function parseElseChain(ctx) {
2079
2613
  const { stream } = ctx;
2080
- while (stream.matchText("else")) {
2081
- if (stream.matchText("if")) {
2082
- expectExpression(ctx, "Expected expression after 'else if'");
2083
- const body = expectCompound(ctx, "Expected '{' after else if");
2084
- ctx.addElem(body);
2085
- continue;
2086
- }
2087
- const body = expectCompound(ctx, "Expected '{' after else");
2088
- ctx.addElem(body);
2089
- break;
2614
+ const elseToken = stream.matchText("else");
2615
+ if (!elseToken) return void 0;
2616
+ if (stream.matchText("if")) {
2617
+ const condition = expectExpression(ctx, "Expected expression after 'else if'");
2618
+ const body = expectCompound(ctx, "Expected '{' after else if");
2619
+ const elseBranch = parseElseChain(ctx);
2620
+ const end = stream.checkpoint();
2621
+ return {
2622
+ kind: "if",
2623
+ condition,
2624
+ body,
2625
+ else: elseBranch,
2626
+ start: elseToken.span[0],
2627
+ end
2628
+ };
2090
2629
  }
2630
+ return expectCompound(ctx, "Expected '{' after else");
2091
2631
  }
2092
2632
  /**
2093
2633
  * Grammar: switch_body : attribute* '{' switch_clause+ '}'
@@ -2097,27 +2637,43 @@ function parseElseChain(ctx) {
2097
2637
  */
2098
2638
  function expectSwitchClauses(ctx) {
2099
2639
  const { stream } = ctx;
2100
- parseAttributeList(ctx);
2640
+ const bodyAttrs = parseAttributeList(ctx);
2101
2641
  expect(stream, "{", "switch expression");
2102
- while (!stream.matchText("}")) {
2103
- const clauseStart = stream.checkpoint();
2104
- const clauseAttrs = parseAttributeList(ctx);
2105
- beginElem(ctx, "switch-clause", clauseAttrs.length ? clauseAttrs : void 0);
2106
- if (stream.matchText("case")) {
2107
- parseCaseSelectors(ctx);
2108
- parseCaseBody(ctx, "Expected '{' after case value");
2109
- } else if (stream.matchText("default")) parseCaseBody(ctx, "Expected '{' after 'default'");
2110
- else throwParseError(stream, "Expected 'case', 'default', or '}' in switch");
2111
- const clauseElem = finishElem("switch-clause", clauseStart, ctx, {});
2112
- attachAttributes(clauseElem, clauseAttrs.length ? clauseAttrs : void 0);
2113
- ctx.addElem(clauseElem);
2642
+ const clauses = [];
2643
+ while (!stream.matchText("}")) clauses.push(parseSwitchClause(ctx));
2644
+ return {
2645
+ bodyAttributes: attrsOrUndef(bodyAttrs),
2646
+ clauses
2647
+ };
2648
+ }
2649
+ /** Parse one 'case'/'default' clause (the keyword has not yet been consumed). */
2650
+ function parseSwitchClause(ctx) {
2651
+ const { stream } = ctx;
2652
+ const attrs = attrsOrUndef(parseAttributeList(ctx));
2653
+ const caseTok = stream.matchText("case");
2654
+ const keyword = caseTok ?? stream.matchText("default");
2655
+ if (!keyword) throwParseError(stream, "Expected 'case', 'default', or '}' in switch");
2656
+ const clauseStart = getStartWithAttributes(attrs, keyword.span[0]);
2657
+ let selectors;
2658
+ let body;
2659
+ if (caseTok) {
2660
+ selectors = parseCaseSelectors(ctx);
2661
+ body = parseCaseBody(ctx, "Expected '{' after case value");
2662
+ } else {
2663
+ selectors = ["default"];
2664
+ body = parseCaseBody(ctx, "Expected '{' after 'default'");
2114
2665
  }
2666
+ return finishStatement("switch-clause", clauseStart, ctx, {
2667
+ selectors,
2668
+ body
2669
+ }, attrs);
2115
2670
  }
2116
2671
  /** Grammar: case_selectors : case_selector (',' case_selector)* ','? */
2117
2672
  function parseCaseSelectors(ctx) {
2118
2673
  const { stream } = ctx;
2119
- expectExpression(ctx, "Expected expression after 'case'");
2120
- while (stream.matchText(",")) expectExpression(ctx, "Expected expression after ',' in case values");
2674
+ const selectors = [expectExpression(ctx, "Expected expression after 'case'")];
2675
+ while (stream.matchText(",")) selectors.push(expectExpression(ctx, "Expected expression after ',' in case values"));
2676
+ return selectors;
2121
2677
  }
2122
2678
  /**
2123
2679
  * Grammar: case_clause : 'case' case_selectors ':'? compound_statement
@@ -2125,16 +2681,15 @@ function parseCaseSelectors(ctx) {
2125
2681
  */
2126
2682
  function parseCaseBody(ctx, errorMsg) {
2127
2683
  ctx.stream.matchText(":");
2128
- const bodyAttrs = parseAttributeList(ctx);
2129
- const body = parseCompoundStatement(ctx, bodyAttrs.length > 0 ? bodyAttrs : void 0);
2684
+ const body = parseCompoundStatement(ctx, attrsOrUndef(parseAttributeList(ctx)));
2130
2685
  if (!body) throwParseError(ctx.stream, errorMsg);
2131
- ctx.addElem(body);
2686
+ return body;
2132
2687
  }
2133
2688
  //#endregion
2134
2689
  //#region ../wesl/src/parse/ParseValueDeclaration.ts
2135
2690
  /** Grammar: 'const' optionally_typed_ident '=' expression (global or local) */
2136
2691
  function parseConstDecl(ctx, attributes) {
2137
- return parseValueDecl(ctx, "const", true, isModuleScope(ctx), attributes);
2692
+ return parseValueDecl(ctx, "const", true, ctx.isModuleScope(), attributes);
2138
2693
  }
2139
2694
  /** Grammar: 'override' optionally_typed_ident ( '=' expression )? */
2140
2695
  function parseOverrideDecl(ctx, attributes) {
@@ -2145,20 +2700,16 @@ function parseTypedDecl(ctx, isGlobal = true) {
2145
2700
  const nameToken = ctx.stream.matchKind("word");
2146
2701
  if (!nameToken) return null;
2147
2702
  const start = nameToken.span[0];
2148
- beginElem(ctx, "typeDecl");
2149
2703
  const decl = createDeclIdentElem(ctx, nameToken, isGlobal);
2150
- ctx.addElem(decl);
2151
2704
  ctx.saveIdent(decl.ident);
2152
2705
  const { typeRef, typeScope } = parseOptionalType(ctx);
2153
- const end = ctx.stream.checkpoint();
2154
2706
  return {
2155
2707
  kind: "typeDecl",
2156
2708
  decl,
2157
2709
  typeRef,
2158
2710
  typeScope,
2159
2711
  start,
2160
- end,
2161
- contents: finishContents(ctx, start, end)
2712
+ end: ctx.stream.checkpoint()
2162
2713
  };
2163
2714
  }
2164
2715
  /** Shared parser for const/override declarations. */
@@ -2168,43 +2719,29 @@ function parseValueDecl(ctx, keyword, requiresInit, isGlobal, attributes) {
2168
2719
  if (!token) return null;
2169
2720
  const startPos = getStartWithAttributes(attributes, token.span[0]);
2170
2721
  ctx.pushScope("partial");
2171
- beginElem(ctx, keyword, attributes);
2172
2722
  const typedDecl = parseTypedDecl(ctx, isGlobal);
2173
2723
  if (!typedDecl) throwParseError(stream, `Expected identifier after '${keyword}'`);
2174
- ctx.addElem(typedDecl);
2724
+ let init;
2175
2725
  if (requiresInit) {
2176
2726
  expect(stream, "=", `${keyword} identifier`);
2177
- expectExpression(ctx);
2178
- } else if (stream.matchText("=")) expectExpression(ctx);
2727
+ init = expectExpression(ctx);
2728
+ } else if (stream.matchText("=")) init = expectExpression(ctx);
2179
2729
  expect(stream, ";", `${keyword} declaration`);
2180
- const endPos = stream.checkpoint();
2181
- const contents = finishContents(ctx, startPos, endPos);
2182
2730
  typedDecl.decl.ident.dependentScope = ctx.currentScope();
2183
2731
  ctx.popScope();
2184
- const elem = {
2185
- kind: keyword,
2732
+ const elem = finishStatement(keyword, startPos, ctx, {
2186
2733
  name: typedDecl,
2187
- start: startPos,
2188
- end: endPos,
2189
- contents
2190
- };
2191
- attachAttributes(elem, attributes);
2734
+ init
2735
+ }, attributes);
2192
2736
  linkDeclIdent(typedDecl, elem);
2193
2737
  return elem;
2194
2738
  }
2195
- /** @return true if ctx is at module level (not inside fn/block) */
2196
- function isModuleScope(ctx) {
2197
- let scope = ctx.currentScope();
2198
- while (scope.kind === "partial" && scope.parent) scope = scope.parent;
2199
- return scope.parent === null;
2200
- }
2201
2739
  /** Parse optional ': type' annotation, managing scope for type references. */
2202
2740
  function parseOptionalType(ctx) {
2203
2741
  if (!ctx.stream.matchText(":")) return {};
2204
2742
  ctx.pushScope();
2205
2743
  const typeRef = parseSimpleTypeRef(ctx);
2206
2744
  if (!typeRef) throwParseError(ctx.stream, "Expected type after ':'");
2207
- ctx.addElem(typeRef);
2208
2745
  const typeScope = ctx.currentScope();
2209
2746
  ctx.popScope();
2210
2747
  return {
@@ -2224,17 +2761,19 @@ function parseGlobalVarDecl(ctx, attributes) {
2224
2761
  if (!varToken) return null;
2225
2762
  const startPos = getStartWithAttributes(attributes, varToken.span[0]);
2226
2763
  ctx.pushScope("partial");
2227
- beginElem(ctx, "gvar", attributes);
2228
- skipTemplateList(ctx);
2764
+ const template = parseTemplateList(ctx);
2229
2765
  const typedDecl = parseTypedDecl(ctx);
2230
2766
  if (!typedDecl) throwParseError(stream, "Expected identifier after 'var'");
2231
- ctx.addElem(typedDecl);
2232
- if (stream.matchText("=")) expectExpression(ctx);
2767
+ let init;
2768
+ if (stream.matchText("=")) init = expectExpression(ctx);
2233
2769
  expect(stream, ";", "var declaration");
2234
2770
  typedDecl.decl.ident.dependentScope = ctx.currentScope();
2235
2771
  ctx.popScope();
2236
- const varElem = finishElem("gvar", startPos, ctx, { name: typedDecl });
2237
- attachAttributes(varElem, attributes);
2772
+ const varElem = finishStatement("gvar", startPos, ctx, {
2773
+ name: typedDecl,
2774
+ template,
2775
+ init
2776
+ }, attributes);
2238
2777
  linkDeclIdent(typedDecl, varElem);
2239
2778
  return varElem;
2240
2779
  }
@@ -2244,23 +2783,19 @@ function parseAliasDecl(ctx, attributes) {
2244
2783
  const aliasToken = stream.matchText("alias");
2245
2784
  if (!aliasToken) return null;
2246
2785
  const startPos = getStartWithAttributes(attributes, aliasToken.span[0]);
2247
- beginElem(ctx, "alias", attributes);
2248
2786
  const declIdentElem = createDeclIdentElem(ctx, expectWord(stream, "Expected identifier after 'alias'"), true);
2249
- ctx.addElem(declIdentElem);
2250
2787
  ctx.saveIdent(declIdentElem.ident);
2251
2788
  expect(stream, "=", "alias name");
2252
2789
  ctx.pushScope();
2253
2790
  const typeRef = parseSimpleTypeRef(ctx);
2254
2791
  if (!typeRef) throwParseError(stream, "Expected type after '=' in alias declaration");
2255
- ctx.addElem(typeRef);
2256
2792
  declIdentElem.ident.dependentScope = ctx.currentScope();
2257
2793
  ctx.popScope();
2258
2794
  expect(stream, ";", "alias declaration");
2259
- const aliasElem = finishElem("alias", startPos, ctx, {
2795
+ const aliasElem = finishStatement("alias", startPos, ctx, {
2260
2796
  name: declIdentElem,
2261
2797
  typeRef
2262
- });
2263
- attachAttributes(aliasElem, attributes);
2798
+ }, attributes);
2264
2799
  linkDeclIdentElem(declIdentElem, aliasElem);
2265
2800
  return aliasElem;
2266
2801
  }
@@ -2269,25 +2804,29 @@ function parseConstAssert(ctx, attributes) {
2269
2804
  const assertToken = ctx.stream.matchText("const_assert");
2270
2805
  if (!assertToken) return null;
2271
2806
  const startPos = getStartWithAttributes(attributes, assertToken.span[0]);
2272
- beginElem(ctx, "assert", attributes);
2273
- expectExpression(ctx);
2807
+ const expression = expectExpression(ctx);
2274
2808
  expect(ctx.stream, ";", "const_assert expression");
2275
- const elem = finishElem("assert", startPos, ctx, {});
2276
- attachAttributes(elem, attributes);
2277
- return elem;
2809
+ return finishStatement("assert", startPos, ctx, { expression }, attributes);
2278
2810
  }
2279
- /** Skip optional template list (e.g., <storage, read_write>). */
2280
- function skipTemplateList(ctx) {
2811
+ /**
2812
+ * Parse an optional var template list `<storage, read_write>`. The entries are
2813
+ * predeclared enumerants (address space / access mode), not user idents, so they
2814
+ * are captured as unbound NameElems.
2815
+ */
2816
+ function parseTemplateList(ctx) {
2281
2817
  const { stream } = ctx;
2282
- if (!stream.nextTemplateStartToken()) return;
2818
+ if (!stream.nextTemplateStartToken()) return void 0;
2819
+ const names = [];
2283
2820
  while (true) {
2284
2821
  const next = stream.peek();
2285
2822
  if (!next) throwParseError(stream, "Unclosed template in var declaration");
2286
2823
  if (next.text.startsWith(">")) {
2287
2824
  stream.nextTemplateEndToken();
2288
- return;
2825
+ return names;
2289
2826
  }
2290
- stream.nextToken();
2827
+ const word = stream.matchKind("word");
2828
+ if (word) names.push(makeNameElem(word));
2829
+ else if (!stream.matchText(",")) throwParseError(stream, "Expected ',' or '>' in var template list");
2291
2830
  }
2292
2831
  }
2293
2832
  //#endregion
@@ -2309,18 +2848,20 @@ function parseVarOrLet(ctx, keyword, hasTemplate, requiresInit, attributes) {
2309
2848
  const token = stream.matchText(keyword);
2310
2849
  if (!token) return null;
2311
2850
  const startPos = getStartWithAttributes(attributes, token.span[0]);
2312
- beginElem(ctx, keyword, attributes);
2313
- if (hasTemplate) skipTemplateList(ctx);
2851
+ const template = hasTemplate ? parseTemplateList(ctx) : void 0;
2314
2852
  const typedDecl = parseTypedDecl(ctx, false);
2315
2853
  if (!typedDecl) throwParseError(stream, `Expected identifier after '${keyword}'`);
2316
- ctx.addElem(typedDecl);
2854
+ let init;
2317
2855
  if (requiresInit) {
2318
2856
  expect(stream, "=", `${keyword} identifier (${keyword} requires initialization)`);
2319
- expectExpression(ctx);
2320
- } else if (stream.matchText("=")) expectExpression(ctx);
2857
+ init = expectExpression(ctx);
2858
+ } else if (stream.matchText("=")) init = expectExpression(ctx);
2321
2859
  expect(stream, ";", `${keyword} declaration`);
2322
- const elem = finishElem(keyword, startPos, ctx, { name: typedDecl });
2323
- attachAttributes(elem, attributes);
2860
+ const elem = finishStatement(keyword, startPos, ctx, {
2861
+ name: typedDecl,
2862
+ init
2863
+ }, attributes);
2864
+ if (template && elem.kind === "var") elem.template = template;
2324
2865
  linkDeclIdent(typedDecl, elem);
2325
2866
  return elem;
2326
2867
  }
@@ -2349,17 +2890,42 @@ const assignmentOps = new Set([
2349
2890
  function parseSimpleStatement(ctx, attributes) {
2350
2891
  const { stream } = ctx;
2351
2892
  const startPos = getStartWithAttributes(attributes, stream.checkpoint());
2352
- return parseReturnStmt(ctx, startPos, attributes) || parseBreakStmt(ctx, startPos, attributes) || parseKeywordStmt(ctx, startPos, attributes, "continue") || parseKeywordStmt(ctx, startPos, attributes, "discard") || parseEmptyStmt(stream, startPos) || parsePhonyAssignment(ctx, startPos, attributes) || parseExpressionStmt(ctx, startPos, attributes);
2893
+ return parseReturnStmt(ctx, startPos, attributes) || parseBreakStmt(ctx, startPos, attributes) || parseContinueStmt(ctx, startPos, attributes) || parseDiscardStmt(ctx, startPos, attributes) || parseEmptyStmt(stream, startPos) || parsePhonyAssignment(ctx, startPos, attributes) || parseExpressionStmt(ctx, startPos, attributes);
2894
+ }
2895
+ /** Match an assignment operator, capturing its value and span. */
2896
+ function parseAssignmentOp(stream) {
2897
+ const token = stream.nextIf(({ text }) => assignmentOps.has(text));
2898
+ if (!token) return null;
2899
+ return {
2900
+ value: token.text,
2901
+ span: token.span
2902
+ };
2903
+ }
2904
+ /** Match '++' or '--', returning its text and span (or null if absent). */
2905
+ function parseIncDecOp(stream) {
2906
+ const token = stream.nextIf(({ text }) => text === "++" || text === "--");
2907
+ if (!token) return null;
2908
+ return {
2909
+ op: token.text,
2910
+ span: token.span
2911
+ };
2912
+ }
2913
+ /** Grammar: ( '=' | compound_assignment_operator ) expression. Returns op + rhs. */
2914
+ function parseAssignmentRhs(ctx) {
2915
+ const op = parseAssignmentOp(ctx.stream);
2916
+ if (!op) return null;
2917
+ return {
2918
+ op,
2919
+ rhs: expectExpression(ctx, "Expected expression after assignment operator")
2920
+ };
2353
2921
  }
2354
2922
  /** Grammar: return_statement : 'return' expression? ';' */
2355
2923
  function parseReturnStmt(ctx, startPos, attributes) {
2356
2924
  const { stream } = ctx;
2357
2925
  if (!stream.matchText("return")) return null;
2358
- beginElem(ctx, "statement", attributes);
2359
- const expr = parseExpression(ctx);
2360
- if (expr && ctx.options.preserveExpressions) ctx.addElem(expr);
2926
+ const value = parseContentExpression(ctx) ?? void 0;
2361
2927
  expect(stream, ";", "return statement");
2362
- return finishBlockStatement(startPos, ctx, attributes);
2928
+ return finishStatement("return", startPos, ctx, { value }, attributes);
2363
2929
  }
2364
2930
  /**
2365
2931
  * Grammar: break_statement : 'break' ';'
@@ -2368,70 +2934,79 @@ function parseReturnStmt(ctx, startPos, attributes) {
2368
2934
  function parseBreakStmt(ctx, startPos, attributes) {
2369
2935
  const { stream } = ctx;
2370
2936
  if (!stream.matchText("break")) return null;
2371
- beginElem(ctx, "statement", attributes);
2372
- if (stream.matchText("if")) expectExpression(ctx, "Expected condition after 'break if'");
2937
+ let condition;
2938
+ if (stream.matchText("if")) condition = expectExpression(ctx, "Expected condition after 'break if'");
2373
2939
  expect(stream, ";", "break statement");
2374
- return finishBlockStatement(startPos, ctx, attributes);
2940
+ return finishStatement("break", startPos, ctx, { condition }, attributes);
2941
+ }
2942
+ /** Grammar: continue_statement : 'continue' ';' */
2943
+ function parseContinueStmt(ctx, startPos, attributes) {
2944
+ const { stream } = ctx;
2945
+ if (!stream.matchText("continue")) return null;
2946
+ expect(stream, ";", "continue statement");
2947
+ return finishStatement("continue", startPos, ctx, {}, attributes);
2375
2948
  }
2376
- /** Grammar: continue_statement : 'continue' ';' also handles 'discard' */
2377
- function parseKeywordStmt(ctx, startPos, attributes, keyword) {
2949
+ /** Grammar: 'discard' ';' */
2950
+ function parseDiscardStmt(ctx, startPos, attributes) {
2378
2951
  const { stream } = ctx;
2379
- if (!stream.matchText(keyword)) return null;
2380
- beginElem(ctx, "statement", attributes);
2381
- expect(stream, ";", `${keyword} statement`);
2382
- return finishBlockStatement(startPos, ctx, attributes);
2952
+ if (!stream.matchText("discard")) return null;
2953
+ expect(stream, ";", "discard statement");
2954
+ return finishStatement("discard", startPos, ctx, {}, attributes);
2383
2955
  }
2384
- /** Parse empty statement (just ';'). */
2956
+ /** Parse empty statement (just ';'). Spans the ';' so it emits no extra text. */
2385
2957
  function parseEmptyStmt(stream, start) {
2386
2958
  if (!stream.matchText(";")) return null;
2387
2959
  return {
2388
- kind: "statement",
2960
+ kind: "empty",
2389
2961
  start,
2390
- end: stream.checkpoint(),
2391
- contents: []
2962
+ end: stream.checkpoint()
2392
2963
  };
2393
2964
  }
2394
2965
  /** Grammar: assignment_statement : '_' '=' expression ';' (phony assignment) */
2395
2966
  function parsePhonyAssignment(ctx, startPos, attributes) {
2396
2967
  const { stream } = ctx;
2397
- if (!stream.matchText("_")) return null;
2398
- if (!parseAssignmentOperator(stream)) throwParseError(stream, "Expected assignment operator after '_'");
2399
- beginElem(ctx, "statement", attributes);
2400
- expectExpression(ctx, "Expected expression after assignment operator");
2968
+ const underscore = stream.matchText("_");
2969
+ if (!underscore) return null;
2970
+ const lhs = {
2971
+ kind: "phony",
2972
+ span: underscore.span
2973
+ };
2974
+ const eq = stream.matchText("=");
2975
+ if (!eq) throwParseError(stream, "Expected '=' after '_'");
2976
+ const op = {
2977
+ value: "=",
2978
+ span: eq.span
2979
+ };
2980
+ const rhs = expectExpression(ctx, "Expected expression after '_ ='");
2401
2981
  expect(stream, ";", "assignment");
2402
- return finishBlockStatement(startPos, ctx, attributes);
2982
+ return finishStatement("assign", startPos, ctx, {
2983
+ lhs,
2984
+ op,
2985
+ rhs
2986
+ }, attributes);
2403
2987
  }
2404
- /**
2405
- * Parses expression statements: assignments, increments/decrements, or function calls.
2406
- * Grammar: ( assignment_statement | increment_statement | decrement_statement | call_phrase ) ';'
2407
- */
2988
+ /** Grammar: ( assignment_statement | increment_statement | decrement_statement | call_phrase ) ';' */
2408
2989
  function parseExpressionStmt(ctx, startPos, attributes) {
2409
2990
  const { stream } = ctx;
2410
- beginElem(ctx, "statement", attributes);
2411
2991
  const expr = parseExpression(ctx);
2412
2992
  if (!expr) {
2413
- finishContents(ctx, startPos, startPos);
2414
2993
  stream.reset(startPos);
2415
2994
  return null;
2416
2995
  }
2417
- if (ctx.options.preserveExpressions) ctx.addElem(expr);
2418
- if (!parseIncDecOperator(stream)) parseAssignmentRhs(ctx);
2996
+ const incDec = parseIncDecOp(stream);
2997
+ if (incDec) {
2998
+ expect(stream, ";", "expression");
2999
+ return finishStatement(incDec.op === "++" ? "increment" : "decrement", startPos, ctx, { target: expr }, attributes);
3000
+ }
3001
+ const assign = parseAssignmentRhs(ctx);
2419
3002
  expect(stream, ";", "expression");
2420
- return finishBlockStatement(startPos, ctx, attributes);
2421
- }
2422
- /** Grammar: assignment_statement : lhs_expression ( '=' | compound_assignment_operator ) expression */
2423
- function parseAssignmentOperator(stream) {
2424
- return !!stream.nextIf(({ text }) => assignmentOps.has(text));
2425
- }
2426
- /** Grammar: ( '=' | compound_assignment_operator ) expression (rhs of assignment_statement) */
2427
- function parseAssignmentRhs(ctx) {
2428
- if (!parseAssignmentOperator(ctx.stream)) return false;
2429
- expectExpression(ctx, "Expected expression after assignment operator");
2430
- return true;
2431
- }
2432
- /** Grammar: increment_statement : lhs_expression '++' ; decrement_statement : lhs_expression '--' */
2433
- function parseIncDecOperator(stream) {
2434
- return !!stream.nextIf(({ text }) => text === "++" || text === "--");
3003
+ if (assign) return finishStatement("assign", startPos, ctx, {
3004
+ lhs: expr,
3005
+ op: assign.op,
3006
+ rhs: assign.rhs
3007
+ }, attributes);
3008
+ if (expr.kind !== "call-expression") throwParseError(stream, "Expected call, assignment, or increment");
3009
+ return finishStatement("call", startPos, ctx, { call: expr }, attributes);
2435
3010
  }
2436
3011
  //#endregion
2437
3012
  //#region ../wesl/src/parse/ParseLoop.ts
@@ -2445,41 +3020,48 @@ function parseForStatement(ctx, attributes) {
2445
3020
  if (startPos === null) return null;
2446
3021
  ctx.pushScope();
2447
3022
  expect(stream, "(", "'for'");
2448
- parseForInit(ctx);
2449
- const cond = parseExpression(ctx);
2450
- if (cond && ctx.options.preserveExpressions) ctx.addElem(cond);
3023
+ const init = parseForInit(ctx);
3024
+ const condition = parseContentExpression(ctx) ?? void 0;
2451
3025
  expect(stream, ";", "for loop condition");
2452
- parseForUpdate(ctx);
3026
+ const update = parseForUpdate(ctx);
2453
3027
  expect(stream, ")", "for loop header");
2454
3028
  const body = expectCompound(ctx, "Expected '{' after for loop header");
2455
- ctx.addElem(body);
2456
3029
  ctx.popScope();
2457
- return finishBlockStatement(startPos, ctx, attributes);
3030
+ return finishStatement("for", startPos, ctx, {
3031
+ init,
3032
+ condition,
3033
+ update,
3034
+ body
3035
+ }, attributes);
2458
3036
  }
2459
3037
  /** Grammar: while_statement : attribute* 'while' expression compound_statement */
2460
3038
  function parseWhileStatement(ctx, attributes) {
2461
3039
  const startPos = beginStatement(ctx, "while", attributes);
2462
3040
  if (startPos === null) return null;
2463
- expectExpression(ctx, "Expected condition expression after 'while'");
2464
- const body = expectCompound(ctx, "Expected '{' after while condition");
2465
- ctx.addElem(body);
2466
- return finishBlockStatement(startPos, ctx, attributes);
3041
+ return finishStatement("while", startPos, ctx, {
3042
+ condition: expectExpression(ctx, "Expected condition after 'while'"),
3043
+ body: expectCompound(ctx, "Expected '{' after while condition")
3044
+ }, attributes);
2467
3045
  }
2468
3046
  /** Grammar: loop_statement : attribute* 'loop' attribute* '{' statement* continuing_statement? '}' */
2469
3047
  function parseLoopStatement(ctx, attributes) {
2470
3048
  const startPos = beginStatement(ctx, "loop", attributes);
2471
3049
  if (startPos === null) return null;
2472
3050
  const body = expectCompound(ctx, "Expected '{' after 'loop'", true);
2473
- ctx.addElem(body);
2474
- return finishBlockStatement(startPos, ctx, attributes);
3051
+ return finishStatement("loop", startPos, ctx, {
3052
+ body,
3053
+ continuing: body.body.find((s) => s.kind === "continuing")
3054
+ }, attributes);
2475
3055
  }
2476
3056
  /** Grammar: continuing_statement : 'continuing' continuing_compound_statement */
2477
3057
  function parseContinuingStatement(ctx, attributes) {
2478
- const startPos = beginStatement(ctx, "continuing", attributes, "continuing");
3058
+ const startPos = beginStatement(ctx, "continuing", attributes);
2479
3059
  if (startPos === null) return null;
2480
3060
  const body = expectCompound(ctx, "Expected '{' after 'continuing'");
2481
- ctx.addElem(body);
2482
- return finishBlockStatement(startPos, ctx, attributes, "continuing");
3061
+ return finishStatement("continuing", startPos, ctx, {
3062
+ body,
3063
+ breakIf: body.body.find((s) => s.kind === "break")?.condition
3064
+ }, attributes);
2483
3065
  }
2484
3066
  /** Grammar: for_init? ';'
2485
3067
  * for_init : variable_or_value_statement | variable_updating_statement | func_call_statement
@@ -2487,19 +3069,60 @@ function parseContinuingStatement(ctx, attributes) {
2487
3069
  function parseForInit(ctx) {
2488
3070
  const { stream } = ctx;
2489
3071
  const varDecl = parseLocalVarDecl(ctx);
2490
- if (varDecl) ctx.addElem(varDecl);
2491
- else {
2492
- const expr = parseExpression(ctx);
2493
- if (expr && ctx.options.preserveExpressions) ctx.addElem(expr);
2494
- expect(stream, ";", "for loop init");
2495
- }
3072
+ if (varDecl) return varDecl;
3073
+ const expr = parseContentExpression(ctx);
3074
+ const update = expr ? finishForUpdate(ctx, expr) : void 0;
3075
+ expect(stream, ";", "for loop init");
3076
+ return update;
2496
3077
  }
2497
- /** Grammar: for_update : variable_updating_statement | func_call_statement
2498
- * variable_updating_statement : assignment_statement | increment_statement | decrement_statement */
3078
+ /** Grammar: for_update : variable_updating_statement | func_call_statement */
2499
3079
  function parseForUpdate(ctx) {
2500
- const expr = parseExpression(ctx);
2501
- if (expr && ctx.options.preserveExpressions) ctx.addElem(expr);
2502
- parseIncDecOperator(ctx.stream) || parseAssignmentRhs(ctx);
3080
+ const expr = parseContentExpression(ctx);
3081
+ if (!expr) return void 0;
3082
+ return finishForUpdate(ctx, expr);
3083
+ }
3084
+ /**
3085
+ * Build a typed for-init/for-update node from an already-parsed lhs expression,
3086
+ * consuming its trailing operator (++/-- or an assignment + rhs).
3087
+ */
3088
+ function finishForUpdate(ctx, expr) {
3089
+ const { stream } = ctx;
3090
+ const start = expr.start;
3091
+ const incDec = parseIncDecOp(stream);
3092
+ if (incDec) {
3093
+ const end = stream.checkpoint();
3094
+ if (incDec.op === "++") return {
3095
+ kind: "increment",
3096
+ target: expr,
3097
+ start,
3098
+ end
3099
+ };
3100
+ return {
3101
+ kind: "decrement",
3102
+ target: expr,
3103
+ start,
3104
+ end
3105
+ };
3106
+ }
3107
+ const assign = parseAssignmentRhs(ctx);
3108
+ if (assign) {
3109
+ const { op, rhs } = assign;
3110
+ return {
3111
+ kind: "assign",
3112
+ lhs: expr,
3113
+ op,
3114
+ rhs,
3115
+ start,
3116
+ end: stream.checkpoint()
3117
+ };
3118
+ }
3119
+ if (expr.kind === "call-expression") return {
3120
+ kind: "call",
3121
+ call: expr,
3122
+ start,
3123
+ end: stream.checkpoint()
3124
+ };
3125
+ throwParseError(stream, "Expected an assignment, increment, decrement, or call in for-clause");
2503
3126
  }
2504
3127
  //#endregion
2505
3128
  //#region ../wesl/src/parse/ParseStatement.ts
@@ -2515,17 +3138,17 @@ function parseCompoundStatement(ctx, attributes, options) {
2515
3138
  const brace = ctx.stream.matchText("{");
2516
3139
  if (!brace) return null;
2517
3140
  const startPos = getStartWithAttributes(attributes, brace.span[0]);
2518
- beginElem(ctx, "statement", attributes);
2519
3141
  const skipScope = options?.noScope || hasConditionalAttr(attributes);
2520
3142
  if (!skipScope) ctx.pushScope();
2521
- parseBlockStatements(ctx, options?.loopBody);
3143
+ const body = parseBlockStatements(ctx, options?.loopBody);
2522
3144
  if (!skipScope) ctx.popScope();
2523
- return finishBlockStatement(startPos, ctx, attributes);
3145
+ return finishStatement("block", startPos, ctx, { body }, attributes);
2524
3146
  }
2525
3147
  /** Grammar: attribute* compound_statement (for control flow bodies) */
2526
3148
  function expectCompound(ctx, errorMsg, loopBody) {
2527
3149
  const attrs = parseAttributeList(ctx);
2528
- const block = parseCompoundStatement(ctx, attrs.length > 0 ? attrs : void 0, loopBody ? { loopBody } : void 0);
3150
+ const options = loopBody ? { loopBody } : void 0;
3151
+ const block = parseCompoundStatement(ctx, attrsOrUndef(attrs), options);
2529
3152
  if (!block) throwParseError(ctx.stream, errorMsg);
2530
3153
  return block;
2531
3154
  }
@@ -2533,16 +3156,20 @@ function expectCompound(ctx, errorMsg, loopBody) {
2533
3156
  function getStartWithAttributes(attributes, keywordPos) {
2534
3157
  return attributes?.[0]?.start ?? keywordPos;
2535
3158
  }
2536
- /** Match keyword and begin statement element. Returns start position or null. */
2537
- function beginStatement(ctx, keyword, attributes, kind = "statement") {
2538
- const keywordPos = ctx.stream.checkpoint();
2539
- if (!ctx.stream.matchText(keyword)) return null;
2540
- const startPos = getStartWithAttributes(attributes, keywordPos);
2541
- beginElem(ctx, kind, attributes);
2542
- return startPos;
3159
+ /** Match keyword and return the statement's start position (or null if no match). */
3160
+ function beginStatement(ctx, keyword, attributes) {
3161
+ const token = ctx.stream.matchText(keyword);
3162
+ if (!token) return null;
3163
+ return getStartWithAttributes(attributes, token.span[0]);
2543
3164
  }
2544
- function finishBlockStatement(start, ctx, attributes, kind = "statement") {
2545
- const elem = finishElem(kind, start, ctx, {});
3165
+ /** Build a statement element from its typed fields and attach its attributes. */
3166
+ function finishStatement(kind, start, ctx, params, attributes) {
3167
+ const elem = {
3168
+ kind,
3169
+ start,
3170
+ end: ctx.stream.checkpoint(),
3171
+ ...params
3172
+ };
2546
3173
  attachAttributes(elem, attributes);
2547
3174
  return elem;
2548
3175
  }
@@ -2552,16 +3179,18 @@ function hasConditionalAttr(attributes) {
2552
3179
  /** Grammar: statement* '}' (after '{' consumed). Loop bodies may end with continuing. */
2553
3180
  function parseBlockStatements(ctx, loopBody) {
2554
3181
  const { stream } = ctx;
3182
+ const body = [];
2555
3183
  while (true) {
2556
3184
  if (stream.matchText("}")) break;
2557
3185
  const stmt = parseStatement(ctx);
2558
3186
  if (!stmt) throwParseError(stream, "Expected statement or '}'");
2559
- ctx.addElem(stmt);
3187
+ body.push(stmt);
2560
3188
  if (loopBody && stmt.kind === "continuing") {
2561
3189
  expect(stream, "}", "continuing block");
2562
3190
  break;
2563
3191
  }
2564
3192
  }
3193
+ return body;
2565
3194
  }
2566
3195
  /**
2567
3196
  * Grammar: statement :
@@ -2582,7 +3211,6 @@ function parseStatement(ctx) {
2582
3211
  }
2583
3212
  const hasConditional = attributes.length > 0 && hasConditionalAttribute(attributes);
2584
3213
  if (hasConditional) ctx.pushScope("partial");
2585
- const attrsOrUndef = attributes.length > 0 ? attributes : void 0;
2586
3214
  const stmt = findMap([
2587
3215
  parseLocalVarDecl,
2588
3216
  parseLetDecl,
@@ -2596,19 +3224,71 @@ function parseStatement(ctx) {
2596
3224
  parseLoopStatement,
2597
3225
  parseContinuingStatement,
2598
3226
  parseSimpleStatement
2599
- ], (p) => p(ctx, attrsOrUndef));
2600
- if (!stmt) return null;
2601
- finalizeConditional$1(ctx, hasConditional, attributes);
2602
- return stmt;
3227
+ ], (p) => p(ctx, attrsOrUndef(attributes)));
3228
+ if (hasConditional) {
3229
+ const partialScope = ctx.popScope();
3230
+ if (stmt) partialScope.condAttribute = conditionalAttribute(attributes);
3231
+ }
3232
+ return stmt ? stmt : null;
3233
+ }
3234
+ //#endregion
3235
+ //#region ../wesl/src/parse/ParseDirective.ts
3236
+ /** Grammar: global_directive : diagnostic_directive | enable_directive | requires_directive */
3237
+ function parseDirective(ctx) {
3238
+ const { stream } = ctx;
3239
+ const startPos = stream.checkpoint();
3240
+ const attrs = attrsOrUndef(parseAttributeList(ctx));
3241
+ const result = parseExtensionDirective(ctx, "enable", attrs) || parseExtensionDirective(ctx, "requires", attrs) || parseDiagnosticDirective(ctx, attrs);
3242
+ if (!result) stream.reset(startPos);
3243
+ return result;
2603
3244
  }
2604
- function finalizeConditional$1(ctx, hasConditional, attributes) {
2605
- if (hasConditional) {
2606
- const partialScope = ctx.popScope();
2607
- partialScope.condAttribute = getConditionalAttribute(attributes);
2608
- }
3245
+ /** Grammar: enable_directive | requires_directive : keyword extension_list ';' */
3246
+ function parseExtensionDirective(ctx, keyword, attributes) {
3247
+ const { stream } = ctx;
3248
+ const token = stream.matchText(keyword);
3249
+ if (!token) return null;
3250
+ const extensions = parseCommaList(ctx, parseDirectiveName);
3251
+ expect(stream, ";", `${keyword} directive`);
3252
+ return makeDirectiveElem({
3253
+ kind: keyword,
3254
+ extensions
3255
+ }, token, stream, attributes);
3256
+ }
3257
+ /**
3258
+ * Grammar: diagnostic_directive : 'diagnostic' diagnostic_control ';'
3259
+ * Grammar: diagnostic_control : '(' severity_control_name ',' diagnostic_rule_name ',' ? ')'
3260
+ */
3261
+ function parseDiagnosticDirective(ctx, attributes) {
3262
+ const { stream } = ctx;
3263
+ const token = stream.matchText("diagnostic");
3264
+ if (!token) return null;
3265
+ expect(stream, "(", "diagnostic");
3266
+ const severity = makeNameElem(expectWord(stream, "Expected severity in diagnostic"));
3267
+ expect(stream, ",", "diagnostic severity");
3268
+ const ruleName = makeNameElem(expectWord(stream, "Expected rule name in diagnostic"));
3269
+ let subrule = null;
3270
+ if (stream.matchText(".")) subrule = makeNameElem(expectWord(stream, "Expected subrule name after '.'"));
3271
+ stream.matchText(",");
3272
+ expect(stream, ")", "diagnostic rule");
3273
+ expect(stream, ";", "diagnostic directive");
3274
+ return makeDirectiveElem({
3275
+ kind: "diagnostic",
3276
+ severity,
3277
+ rule: [ruleName, subrule]
3278
+ }, token, stream, attributes);
3279
+ }
3280
+ function parseDirectiveName(ctx) {
3281
+ return makeNameElem(expectWord(ctx.stream, "Expected identifier in name list"));
2609
3282
  }
2610
- function getConditionalAttribute(attributes) {
2611
- return attributes.find((a) => isConditionalAttribute$1(a.attribute))?.attribute;
3283
+ function makeDirectiveElem(directive, token, stream, attributes) {
3284
+ const elem = {
3285
+ kind: "directive",
3286
+ directive,
3287
+ start: getStartWithAttributes(attributes, token.span[0]),
3288
+ end: stream.checkpoint()
3289
+ };
3290
+ attachAttributes(elem, attributes);
3291
+ return elem;
2612
3292
  }
2613
3293
  //#endregion
2614
3294
  //#region ../wesl/src/parse/ParseFn.ts
@@ -2645,8 +3325,7 @@ function parseFnDecl(ctx, attributes) {
2645
3325
  returnType,
2646
3326
  returnAttributes,
2647
3327
  start: startPos,
2648
- end: stream.checkpoint(),
2649
- contents: buildFnContents(attributes, declIdentElem, params, returnType, body)
3328
+ end: stream.checkpoint()
2650
3329
  };
2651
3330
  attachAttributes(fnElem, attributes);
2652
3331
  linkDeclIdentElem(declIdentElem, fnElem);
@@ -2676,37 +3355,63 @@ function parseFnReturn(ctx) {
2676
3355
  if (!returnType) throwParseError(stream, "Expected type after '->'");
2677
3356
  return {
2678
3357
  returnType,
2679
- returnAttributes: attrs.length > 0 ? attrs : void 0
3358
+ returnAttributes: attrsOrUndef(attrs)
2680
3359
  };
2681
3360
  }
2682
- /** Build contents array for function element */
2683
- function buildFnContents(attributes, decl, params, returnType, body) {
2684
- const base = returnType ? [
2685
- decl,
2686
- ...params,
2687
- returnType,
2688
- body
2689
- ] : [
2690
- decl,
2691
- ...params,
2692
- body
2693
- ];
2694
- return attributes?.length ? [...attributes, ...base] : base;
2695
- }
2696
3361
  /** Grammar: param : attribute* optionally_typed_ident */
2697
3362
  function parseFnParam(ctx) {
2698
- const attributes = parseAttributeList(ctx);
3363
+ const attrs = parseAttributeList(ctx);
2699
3364
  if (ctx.stream.peek()?.kind !== "word") return null;
2700
- beginElem(ctx, "param", attributes.length ? attributes : void 0);
3365
+ const attributes = attrsOrUndef(attrs);
2701
3366
  const name = parseTypedDecl(ctx, false);
2702
3367
  if (!name) throw new Error("Unexpected: peek succeeded but parseTypedDecl failed");
2703
- ctx.addElem(name);
2704
- const elem = finishElem("param", getStartWithAttributes(attributes, name.start), ctx, { name });
3368
+ const elem = finishStatement("param", getStartWithAttributes(attributes, name.start), ctx, { name }, attributes);
2705
3369
  linkDeclIdent(name, elem);
2706
- attachAttributes(elem, attributes.length > 0 ? attributes : void 0);
2707
3370
  return elem;
2708
3371
  }
2709
3372
  //#endregion
3373
+ //#region ../wesl/src/parse/ParseDoBlock.ts
3374
+ /**
3375
+ * Grammar: do_block_decl : attribute* 'do' ident '(' param_list? ')' compound_statement
3376
+ *
3377
+ * `do` is a module-level declaration when the `doBlocks` extension is on;
3378
+ * otherwise it stays a reserved word. The body is parsed structurally but
3379
+ * inside a scope that is detached from the module scope graph, so its idents
3380
+ * never reach bindIdents. do blocks are module-local, resolved later by AST
3381
+ * name match.
3382
+ */
3383
+ function parseDoBlock(ctx, attributes) {
3384
+ if (!ctx.options.weslExtensions?.doBlocks) return null;
3385
+ const { stream } = ctx;
3386
+ const doToken = stream.matchText("do");
3387
+ if (!doToken) return null;
3388
+ const startPos = getStartWithAttributes(attributes, doToken.span[0]);
3389
+ const name = makeNameElem(expectWord(stream, "Expected identifier after 'do'"));
3390
+ const parentScope = ctx.currentScope();
3391
+ ctx.pushScope();
3392
+ const doScope = ctx.currentScope();
3393
+ const params = parseFnParams(ctx);
3394
+ const body = parseFunctionBody(ctx);
3395
+ if (!body) throwParseError(stream, "Expected do block body");
3396
+ ctx.popScope();
3397
+ detachScope(parentScope, doScope);
3398
+ const doBlock = {
3399
+ kind: "do",
3400
+ name,
3401
+ params,
3402
+ body,
3403
+ start: startPos,
3404
+ end: stream.checkpoint()
3405
+ };
3406
+ attachAttributes(doBlock, attributes);
3407
+ return doBlock;
3408
+ }
3409
+ /** Remove a child scope from its parent so bindIdents never traverses it. */
3410
+ function detachScope(parent, child) {
3411
+ const i = parent.contents.indexOf(child);
3412
+ if (i >= 0) parent.contents.splice(i, 1);
3413
+ }
3414
+ //#endregion
2710
3415
  //#region ../wesl/src/parse/ParseImport.ts
2711
3416
  /** WESL Grammar: translation_unit : import_statement* global_directive* global_decl* */
2712
3417
  function parseWeslImports(ctx) {
@@ -2844,52 +3549,39 @@ function parseStructDecl(ctx, attributes) {
2844
3549
  const start = getStartWithAttributes(attributes, structToken.span[0]);
2845
3550
  const identElem = createDeclIdentElem(ctx, expectWord(stream, "Expected identifier after 'struct'"), true);
2846
3551
  ctx.saveIdent(identElem.ident);
2847
- beginElem(ctx, "struct", attributes);
2848
- ctx.addElem(identElem);
2849
3552
  expect(stream, "{", "struct name");
2850
3553
  ctx.pushScope();
2851
- const members = parseStructMembers(ctx);
3554
+ const members = parseCommaList(ctx, parseStructMember);
2852
3555
  identElem.ident.dependentScope = ctx.currentScope();
2853
3556
  ctx.popScope();
2854
3557
  expect(stream, "}", "struct member");
2855
- const elem = finishElem("struct", start, ctx, {
3558
+ const elem = finishStatement("struct", start, ctx, {
2856
3559
  name: identElem,
2857
3560
  members
2858
- });
2859
- attachAttributes(elem, attributes);
3561
+ }, attributes);
2860
3562
  linkDeclIdentElem(identElem, elem);
2861
3563
  return elem;
2862
3564
  }
2863
- /** Grammar: struct_body_decl : '{' struct_member (',' struct_member)* ','? '}' */
2864
- function parseStructMembers(ctx) {
2865
- const members = parseCommaList(ctx, parseStructMember);
2866
- for (const member of members) ctx.addElem(member);
2867
- return members;
2868
- }
2869
3565
  /** Grammar: struct_member : attribute* member_ident ':' type_specifier */
2870
3566
  function parseStructMember(ctx) {
2871
3567
  const { stream } = ctx;
2872
3568
  const checkpoint = stream.checkpoint();
2873
- const attributes = parseAttributeList(ctx);
3569
+ const attrs = parseAttributeList(ctx);
2874
3570
  const nameToken = stream.matchKind("word");
2875
3571
  if (!nameToken) {
2876
3572
  stream.reset(checkpoint);
2877
3573
  return null;
2878
3574
  }
3575
+ const attributes = attrsOrUndef(attrs);
2879
3576
  const start = getStartWithAttributes(attributes, nameToken.span[0]);
2880
- beginElem(ctx, "member", attributes.length ? attributes : void 0);
2881
3577
  const name = makeNameElem(nameToken);
2882
- ctx.addElem(name);
2883
3578
  expect(stream, ":", "struct member name");
2884
3579
  const typeRef = parseSimpleTypeRef(ctx);
2885
3580
  if (!typeRef) throwParseError(stream, "Expected type after ':'");
2886
- ctx.addElem(typeRef);
2887
- const elem = finishElem("member", start, ctx, {
3581
+ return finishStatement("member", start, ctx, {
2888
3582
  name,
2889
3583
  typeRef
2890
- });
2891
- attachAttributes(elem, attributes.length ? attributes : void 0);
2892
- return elem;
3584
+ }, attributes);
2893
3585
  }
2894
3586
  //#endregion
2895
3587
  //#region ../wesl/src/parse/ParseModule.ts
@@ -2900,6 +3592,7 @@ const declParsers = [
2900
3592
  parseAliasDecl,
2901
3593
  parseStructDecl,
2902
3594
  parseFnDecl,
3595
+ parseDoBlock,
2903
3596
  parseConstAssert
2904
3597
  ];
2905
3598
  /** Grammar: translation_unit : global_directive* ( global_decl | global_assert | ';' )* */
@@ -2907,19 +3600,39 @@ function parseModule(ctx) {
2907
3600
  parseImports(ctx);
2908
3601
  parseDirectives(ctx);
2909
3602
  while (parseNextDeclaration(ctx));
3603
+ if (ctx.stream.peek() !== null) throwParseError(ctx.stream, "Expected a declaration or directive");
3604
+ }
3605
+ /**
3606
+ * Reject a module that gives a `do` block a name that clashes with a fn/global
3607
+ * or with another `do` block (`do` blocks share the module's declaration
3608
+ * namespace, and runners key blocks by name so a duplicate would silently
3609
+ * shadow the earlier one). This is a small module-local pass, deliberately not
3610
+ * part of bindIdents.
3611
+ */
3612
+ function checkDoBlockNames(moduleElem) {
3613
+ const doBlocks = declsOfKind(moduleElem, "do");
3614
+ if (doBlocks.length === 0) return;
3615
+ const declNames = new Set(moduleElem.decls.map(globalDeclName));
3616
+ const seen = /* @__PURE__ */ new Set();
3617
+ for (const block of doBlocks) {
3618
+ const { name, start, end } = block.name;
3619
+ if (declNames.has(name)) throw new ParseError(`'${name}' declared as both fn and do`, [start, end]);
3620
+ if (seen.has(name)) throw new ParseError(`'${name}' declared as do more than once`, [start, end]);
3621
+ seen.add(name);
3622
+ }
2910
3623
  }
2911
3624
  /** Parse WESL import statements at the start of the module. */
2912
3625
  function parseImports(ctx) {
2913
3626
  const importElems = parseWeslImports(ctx);
2914
3627
  for (const importElem of importElems) {
2915
- ctx.addElem(importElem);
3628
+ ctx.addModuleDecl(importElem);
2916
3629
  ctx.state.stable.imports.push(importElem.imports);
2917
3630
  }
2918
3631
  }
2919
3632
  /** Grammar: global_directive : diagnostic_directive | enable_directive | requires_directive */
2920
3633
  function parseDirectives(ctx) {
2921
3634
  const directives = parseMany(ctx, parseDirective);
2922
- for (const elem of directives) ctx.addElem(elem);
3635
+ for (const elem of directives) ctx.addModuleDecl(elem);
2923
3636
  }
2924
3637
  /** Parse one declaration, return true if more may exist. */
2925
3638
  function parseNextDeclaration(ctx) {
@@ -2934,34 +3647,40 @@ function parseNextDeclaration(ctx) {
2934
3647
  if (attrs.length) throwParseError(stream, "Expected declaration after attributes");
2935
3648
  return false;
2936
3649
  }
3650
+ /** @return the declared name of a module-level declaration, if it has one. */
3651
+ function globalDeclName(elem) {
3652
+ switch (elem.kind) {
3653
+ case "fn":
3654
+ case "struct":
3655
+ case "alias": return elem.name.ident.originalName;
3656
+ case "gvar":
3657
+ case "const":
3658
+ case "override": return elem.name.decl.ident.originalName;
3659
+ default: return;
3660
+ }
3661
+ }
2937
3662
  /** Try each declaration parser until one succeeds. */
2938
3663
  function parseDecl(ctx, attrs) {
2939
- const attrsOrUndef = attrs.length ? attrs : void 0;
2940
- const elem = findMap(declParsers, (p) => p(ctx, attrsOrUndef));
2941
- if (elem) {
2942
- recordDecl(ctx, elem, attrs);
2943
- return true;
2944
- }
2945
- return false;
3664
+ const elem = findMap(declParsers, (p) => p(ctx, attrsOrUndef(attrs)));
3665
+ if (!elem) return false;
3666
+ recordDecl(ctx, elem, attrs);
3667
+ return true;
2946
3668
  }
2947
3669
  /** Pop conditional scope and attach the conditional attribute. */
2948
3670
  function finalizeConditional(ctx, attrs) {
2949
3671
  const partialScope = ctx.popScope();
2950
- partialScope.condAttribute = findMap(attrs, ({ attribute }) => isConditionalAttribute(attribute) ? attribute : void 0);
3672
+ partialScope.condAttribute = conditionalAttribute(attrs);
2951
3673
  }
2952
3674
  /** Record a parsed declaration, extending start to include attributes. */
2953
3675
  function recordDecl(ctx, elem, attrs) {
2954
3676
  if (attrs.length && elem.start > attrs[0].start) elem.start = attrs[0].start;
2955
- ctx.addElem(elem);
3677
+ ctx.addModuleDecl(elem);
2956
3678
  if (elem.kind === "assert") {
2957
3679
  const { stable } = ctx.state;
2958
3680
  stable.moduleAsserts ??= [];
2959
3681
  stable.moduleAsserts.push(elem);
2960
3682
  }
2961
3683
  }
2962
- function isConditionalAttribute(a) {
2963
- return a.kind === "@if" || a.kind === "@elif" || a.kind === "@else";
2964
- }
2965
3684
  //#endregion
2966
3685
  //#region ../wesl/src/parse/ParsingContext.ts
2967
3686
  /** Context for parsers to build AST and manage scopes. */
@@ -2984,9 +3703,9 @@ var ParsingContext = class {
2984
3703
  currentScope() {
2985
3704
  return this.state.context.scope;
2986
3705
  }
2987
- addElem(elem) {
2988
- const { openElems } = this.state.context;
2989
- if (openElems.length > 0) openElems[openElems.length - 1].contents.push(elem);
3706
+ /** Append a top-level declaration to the module, in source order. */
3707
+ addModuleDecl(elem) {
3708
+ this.state.stable.moduleElem.decls.push(elem);
2990
3709
  }
2991
3710
  pushScope(kind = "scope") {
2992
3711
  const { scope } = this.state.context;
@@ -3054,55 +3773,6 @@ const reservedWords = `NULL Self abstract active alignas alignof as asm asm_frag
3054
3773
  target template this thread_local throw trait try type typedef typeid typename typeof
3055
3774
  union unless unorm unsafe unsized use using varying virtual volatile wgsl where with writeonly yield`.split(/\s+/);
3056
3775
  //#endregion
3057
- //#region ../wesl/src/parse/stream/CachingStream.ts
3058
- var CachingStream = class {
3059
- cache = new Cache(5);
3060
- inner;
3061
- constructor(inner) {
3062
- this.inner = inner;
3063
- }
3064
- checkpoint() {
3065
- return this.inner.checkpoint();
3066
- }
3067
- reset(position) {
3068
- this.inner.reset(position);
3069
- }
3070
- nextToken() {
3071
- const startPos = this.checkpoint();
3072
- const cachedValue = this.cache.get(startPos);
3073
- if (cachedValue !== void 0) {
3074
- this.reset(cachedValue.checkpoint);
3075
- return cachedValue.token;
3076
- } else {
3077
- const token = this.inner.nextToken();
3078
- const checkpoint = this.checkpoint();
3079
- this.cache.set(startPos, {
3080
- token,
3081
- checkpoint
3082
- });
3083
- return token;
3084
- }
3085
- }
3086
- get src() {
3087
- return this.inner.src;
3088
- }
3089
- };
3090
- /** size limited key value cache */
3091
- var Cache = class extends Map {
3092
- max;
3093
- constructor(max) {
3094
- super();
3095
- this.max = max;
3096
- }
3097
- set(k, v) {
3098
- if (this.size > this.max) {
3099
- const first = this.keys().next().value;
3100
- if (first) this.delete(first);
3101
- }
3102
- return super.set(k, v);
3103
- }
3104
- };
3105
- //#endregion
3106
3776
  //#region ../wesl/src/parse/stream/RegexHelpers.ts
3107
3777
  function toRegexSource(nameExp) {
3108
3778
  const [name, e] = nameExp;
@@ -3132,33 +3802,10 @@ function matchOneOf(syms) {
3132
3802
  return new RegExp(escaped.join("|"));
3133
3803
  }
3134
3804
  //#endregion
3135
- //#region ../wesl/src/parse/stream/MatchersStream.ts
3136
- /** Runs a `RegexMatchers` on an input string */
3137
- var MatchersStream = class {
3138
- position = 0;
3139
- text;
3140
- matchers;
3141
- constructor(text, matchers) {
3142
- this.text = text;
3143
- this.matchers = matchers;
3144
- }
3145
- checkpoint() {
3146
- return this.position;
3147
- }
3148
- reset(position) {
3149
- this.position = position;
3150
- }
3151
- nextToken() {
3152
- const result = this.matchers.execAt(this.text, this.position);
3153
- if (result === null) return null;
3154
- this.position = result.span[1];
3155
- return result;
3156
- }
3157
- get src() {
3158
- return this.text;
3159
- }
3160
- };
3805
+ //#region ../wesl/src/parse/stream/RegexMatchers.ts
3161
3806
  /**
3807
+ * Matches tokens by kind with one combined regex.
3808
+ *
3162
3809
  * The matchers passed to this object must follow certain rules:
3163
3810
  * - They must use non-capturing groups: `(?:...)`
3164
3811
  * - They must NOT use `^` or `$`
@@ -3169,55 +3816,230 @@ var RegexMatchers = class {
3169
3816
  constructor(matchers) {
3170
3817
  this.groups = Object.keys(matchers);
3171
3818
  const expParts = Object.entries(matchers).map(toRegexSource).join("|");
3172
- this.exp = new RegExp(expParts, "dyu");
3819
+ this.exp = new RegExp(expParts, "yu");
3173
3820
  }
3174
3821
  execAt(text, position) {
3175
3822
  this.exp.lastIndex = position;
3176
- const matchedIndex = findGroupDex(this.exp.exec(text)?.indices);
3177
- if (matchedIndex) {
3178
- const { span, groupDex } = matchedIndex;
3179
- return {
3180
- kind: this.groups[groupDex],
3181
- span,
3182
- text: text.slice(span[0], span[1])
3183
- };
3184
- } else return null;
3185
- }
3186
- };
3187
- function findGroupDex(indices) {
3188
- if (indices !== void 0) for (let i = 1; i < indices.length; i++) {
3189
- const span = indices[i];
3190
- if (span !== void 0) return {
3823
+ const matches = this.exp.exec(text);
3824
+ if (matches === null) return null;
3825
+ const matched = matches[0];
3826
+ const span = [position, position + matched.length];
3827
+ return {
3828
+ kind: this.groups[matchedGroupIndex(matches)],
3191
3829
  span,
3192
- groupDex: i - 1
3830
+ text: matched
3193
3831
  };
3194
3832
  }
3833
+ };
3834
+ /** @return index of the alternation group that matched */
3835
+ function matchedGroupIndex(matches) {
3836
+ for (let i = 1; i < matches.length; i++) if (matches[i] !== void 0) return i - 1;
3837
+ throw new Error("no matching group");
3195
3838
  }
3196
3839
  //#endregion
3197
- //#region ../wesl/src/parse/WeslStream.ts
3840
+ //#region ../wesl/src/parse/stream/WeslLexer.ts
3841
+ const ident = /(?:(?:[_\p{XID_Start}][\p{XID_Continue}]+)|(?:[\p{XID_Start}]))/u;
3198
3842
  /** Whitespaces including new lines */
3199
3843
  const blankspaces = /[ \t\n\v\f\r\u{0085}\u{200E}\u{200F}\u{2028}\u{2029}]+/u;
3200
- const symbolSet = "& && -> @ / ! [ ] { } :: : , == = != >>= >> >= > <<= << <= < % - -- . + ++ | || ( ) ; * ~ ^ // /* */ += -= *= /= %= &= |= ^= _";
3201
- const ident = /(?:(?:[_\p{XID_Start}][\p{XID_Continue}]+)|(?:[\p{XID_Start}]))/u;
3202
- const keywordOrReserved = new Set(keywords.concat(reservedWords));
3203
- const weslMatcher = new RegexMatchers({
3844
+ const digits = new RegExp(/(?:0[fh])|(?:[1-9][0-9]*[fh])/.source + /|(?:[0-9]*\.[0-9]+(?:[eE][+-]?[0-9]+)?[fh]?)/.source + /|(?:[0-9]+\.[0-9]*(?:[eE][+-]?[0-9]+)?[fh]?)/.source + /|(?:[0-9]+[eE][+-]?[0-9]+[fh]?)/.source + /|(?:0[xX][0-9a-fA-F]*\.[0-9a-fA-F]+(?:[pP][+-]?[0-9]+[fh]?)?)/.source + /|(?:0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*(?:[pP][+-]?[0-9]+[fh]?)?)/.source + /|(?:0[xX][0-9a-fA-F]+[pP][+-]?[0-9]+[fh]?)/.source + /|(?:0[xX][0-9a-fA-F]+[iu]?)/.source + /|(?:0[iu]?)|(?:[1-9][0-9]*[iu]?)/.source);
3845
+ /** Unicode-aware fallback for characters the ASCII fast path can't handle.
3846
+ * lastIndex is set on every use, so sharing one instance is safe. */
3847
+ const unicodeMatcher = new RegexMatchers({
3204
3848
  word: ident,
3205
- number: new RegExp(/(?:0[fh])|(?:[1-9][0-9]*[fh])/.source + /|(?:[0-9]*\.[0-9]+(?:[eE][+-]?[0-9]+)?[fh]?)/.source + /|(?:[0-9]+\.[0-9]*(?:[eE][+-]?[0-9]+)?[fh]?)/.source + /|(?:[0-9]+[eE][+-]?[0-9]+[fh]?)/.source + /|(?:0[xX][0-9a-fA-F]*\.[0-9a-fA-F]+(?:[pP][+-]?[0-9]+[fh]?)?)/.source + /|(?:0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*(?:[pP][+-]?[0-9]+[fh]?)?)/.source + /|(?:0[xX][0-9a-fA-F]+[pP][+-]?[0-9]+[fh]?)/.source + /|(?:0[xX][0-9a-fA-F]+[iu]?)/.source + /|(?:0[iu]?)|(?:[1-9][0-9]*[iu]?)/.source),
3849
+ number: digits,
3206
3850
  blankspaces,
3207
3851
  commentStart: /\/\/|\/\*/,
3208
- symbol: matchOneOf(symbolSet),
3852
+ symbol: matchOneOf("& && -> @ / ! [ ] { } :: : , == = != >>= >> >= > <<= << <= < % - -- . + ++ | || ( ) ; * ~ ^ // /* */ += -= *= /= %= &= |= ^= _"),
3209
3853
  invalid: /[^]/
3210
3854
  });
3855
+ /**
3856
+ * Hand-written scanner for WESL/WGSL raw tokens.
3857
+ *
3858
+ * The common case is dispatched on ASCII char codes: whitespace is skipped
3859
+ * without producing tokens, and idents, numbers, symbols, and comment openers
3860
+ * are matched directly. Any character >= 0x80 (unicode idents, unicode
3861
+ * blankspace) falls back to the unicode-aware regex, so there is no up-front
3862
+ * scan and no unicode restriction.
3863
+ */
3864
+ var WeslLexer = class {
3865
+ position = 0;
3866
+ /** sticky number matcher (stateful via lastIndex, so kept per-instance) */
3867
+ numberExp = new RegExp(digits.source, "y");
3868
+ src;
3869
+ constructor(src) {
3870
+ this.src = src;
3871
+ }
3872
+ checkpoint() {
3873
+ return this.position;
3874
+ }
3875
+ reset(position) {
3876
+ this.position = position;
3877
+ }
3878
+ nextToken() {
3879
+ const { src } = this;
3880
+ const len = src.length;
3881
+ let pos = this.position;
3882
+ while (pos < len) {
3883
+ const c = src.charCodeAt(pos);
3884
+ if (c === 32 || c >= 9 && c <= 13) pos++;
3885
+ else break;
3886
+ }
3887
+ if (pos >= len) {
3888
+ this.position = pos;
3889
+ return null;
3890
+ }
3891
+ const c = src.charCodeAt(pos);
3892
+ if (c >= 97 && c <= 122 || c >= 65 && c <= 90) return this.word(pos);
3893
+ if (c >= 48 && c <= 57) return this.number(pos);
3894
+ const n = src.charCodeAt(pos + 1);
3895
+ switch (c) {
3896
+ case 95:
3897
+ if (isWordChar(n)) return this.word(pos);
3898
+ if (n >= 128) return this.regexToken(pos);
3899
+ return this.symbol(pos, "_");
3900
+ case 47:
3901
+ if (n === 47) return this.token("commentStart", pos, "//");
3902
+ if (n === 42) return this.token("commentStart", pos, "/*");
3903
+ if (n === 61) return this.symbol(pos, "/=");
3904
+ return this.symbol(pos, "/");
3905
+ case 46:
3906
+ if (n >= 48 && n <= 57) return this.number(pos);
3907
+ return this.symbol(pos, ".");
3908
+ case 62:
3909
+ if (n === 62) {
3910
+ if (src.charCodeAt(pos + 2) === 61) return this.symbol(pos, ">>=");
3911
+ return this.symbol(pos, ">>");
3912
+ }
3913
+ if (n === 61) return this.symbol(pos, ">=");
3914
+ return this.symbol(pos, ">");
3915
+ case 60:
3916
+ if (n === 60) {
3917
+ if (src.charCodeAt(pos + 2) === 61) return this.symbol(pos, "<<=");
3918
+ return this.symbol(pos, "<<");
3919
+ }
3920
+ if (n === 61) return this.symbol(pos, "<=");
3921
+ return this.symbol(pos, "<");
3922
+ case 38:
3923
+ if (n === 38) return this.symbol(pos, "&&");
3924
+ if (n === 61) return this.symbol(pos, "&=");
3925
+ return this.symbol(pos, "&");
3926
+ case 124:
3927
+ if (n === 124) return this.symbol(pos, "||");
3928
+ if (n === 61) return this.symbol(pos, "|=");
3929
+ return this.symbol(pos, "|");
3930
+ case 45:
3931
+ if (n === 62) return this.symbol(pos, "->");
3932
+ if (n === 45) return this.symbol(pos, "--");
3933
+ if (n === 61) return this.symbol(pos, "-=");
3934
+ return this.symbol(pos, "-");
3935
+ case 43:
3936
+ if (n === 43) return this.symbol(pos, "++");
3937
+ if (n === 61) return this.symbol(pos, "+=");
3938
+ return this.symbol(pos, "+");
3939
+ case 58:
3940
+ if (n === 58) return this.symbol(pos, "::");
3941
+ return this.symbol(pos, ":");
3942
+ case 61:
3943
+ if (n === 61) return this.symbol(pos, "==");
3944
+ return this.symbol(pos, "=");
3945
+ case 33:
3946
+ if (n === 61) return this.symbol(pos, "!=");
3947
+ return this.symbol(pos, "!");
3948
+ case 42:
3949
+ if (n === 47) return this.symbol(pos, "*/");
3950
+ if (n === 61) return this.symbol(pos, "*=");
3951
+ return this.symbol(pos, "*");
3952
+ case 37:
3953
+ if (n === 61) return this.symbol(pos, "%=");
3954
+ return this.symbol(pos, "%");
3955
+ case 94:
3956
+ if (n === 61) return this.symbol(pos, "^=");
3957
+ return this.symbol(pos, "^");
3958
+ case 64: return this.symbol(pos, "@");
3959
+ case 91: return this.symbol(pos, "[");
3960
+ case 93: return this.symbol(pos, "]");
3961
+ case 123: return this.symbol(pos, "{");
3962
+ case 125: return this.symbol(pos, "}");
3963
+ case 40: return this.symbol(pos, "(");
3964
+ case 41: return this.symbol(pos, ")");
3965
+ case 44: return this.symbol(pos, ",");
3966
+ case 59: return this.symbol(pos, ";");
3967
+ case 126: return this.symbol(pos, "~");
3968
+ }
3969
+ if (c >= 128) return this.regexToken(pos);
3970
+ this.position = pos + 1;
3971
+ return {
3972
+ kind: "invalid",
3973
+ span: [pos, pos + 1],
3974
+ text: src[pos]
3975
+ };
3976
+ }
3977
+ token(kind, start, text) {
3978
+ const end = start + text.length;
3979
+ this.position = end;
3980
+ return {
3981
+ kind,
3982
+ span: [start, end],
3983
+ text
3984
+ };
3985
+ }
3986
+ symbol(start, text) {
3987
+ return this.token("symbol", start, text);
3988
+ }
3989
+ word(start) {
3990
+ const { src } = this;
3991
+ const len = src.length;
3992
+ let pos = start + 1;
3993
+ while (pos < len) {
3994
+ const c = src.charCodeAt(pos);
3995
+ if (isWordChar(c)) pos++;
3996
+ else if (c >= 128) return this.regexToken(start);
3997
+ else break;
3998
+ }
3999
+ this.position = pos;
4000
+ return {
4001
+ kind: "word",
4002
+ span: [start, pos],
4003
+ text: src.slice(start, pos)
4004
+ };
4005
+ }
4006
+ number(start) {
4007
+ this.numberExp.lastIndex = start;
4008
+ const text = this.numberExp.exec(this.src)[0];
4009
+ return this.token("number", start, text);
4010
+ }
4011
+ /** match one token (or blankspace run) with the unicode-aware regex */
4012
+ regexToken(start) {
4013
+ const token = unicodeMatcher.execAt(this.src, start);
4014
+ if (token === null) return null;
4015
+ this.position = token.span[1];
4016
+ return token;
4017
+ }
4018
+ };
4019
+ function isWordChar(c) {
4020
+ return c >= 97 && c <= 122 || c >= 65 && c <= 90 || c >= 48 && c <= 57 || c === 95;
4021
+ }
4022
+ //#endregion
4023
+ //#region ../wesl/src/parse/WeslStream.ts
4024
+ /** One line break, treating \r\n as a single break.
4025
+ * Same code points as `isLineBreak` in AttachComments.ts (charCode form). */
4026
+ const lineBreak = String.raw`\r\n?|[\n\v\f\u{0085}\u{2028}\u{2029}]`;
4027
+ const keywordOrReserved = new Set(keywords.concat(reservedWords));
3211
4028
  /** A stream that produces WESL tokens, skipping over comments and white space */
3212
4029
  var WeslStream = class {
3213
4030
  stream;
3214
- /** New line */
3215
- eolPattern = /[\n\v\f\u{0085}\u{2028}\u{2029}]|\r\n?/gu;
4031
+ /** New line (stateful: scanned via lastIndex, so kept per-instance). */
4032
+ eolPattern = new RegExp(lineBreak, "gu");
3216
4033
  blockCommentPattern = /\/\*|\*\//g;
4034
+ /** Comments skipped before a real token, keyed by that token's start position. */
4035
+ triviaByPos = /* @__PURE__ */ new Map();
4036
+ /** Last peeked token, so the following nextToken() skips the rescan.
4037
+ * Never invalidated: tokenization is deterministic per position. */
4038
+ peeked = null;
3217
4039
  src;
3218
4040
  constructor(src) {
3219
4041
  this.src = src;
3220
- this.stream = new CachingStream(new MatchersStream(src, weslMatcher));
4042
+ this.stream = new WeslLexer(src);
3221
4043
  }
3222
4044
  checkpoint() {
3223
4045
  return this.stream.checkpoint();
@@ -3225,27 +4047,79 @@ var WeslStream = class {
3225
4047
  reset(position) {
3226
4048
  this.stream.reset(position);
3227
4049
  }
4050
+ /** All recorded comment runs (each a contiguous group of comments between two
4051
+ * real tokens), in source order. Consumed by the post-parse comment pass. */
4052
+ commentRuns() {
4053
+ return [...this.triviaByPos.entries()].sort((a, b) => a[0] - b[0]).map(([, run]) => run);
4054
+ }
4055
+ recordTrivia(pos, pending) {
4056
+ if (pending) this.triviaByPos.set(pos, pending);
4057
+ }
4058
+ /** Next real token (comments/blankspace skipped and recorded as trivia); null at EOF. */
3228
4059
  nextToken() {
4060
+ const peeked = this.usePeeked();
4061
+ if (peeked !== null) return peeked.token;
4062
+ let pending;
3229
4063
  while (true) {
3230
4064
  const token = this.stream.nextToken();
3231
- if (token === null) return null;
4065
+ if (token === null) {
4066
+ this.recordTrivia(this.src.length, pending);
4067
+ return null;
4068
+ }
3232
4069
  const kind = token.kind;
3233
4070
  if (kind === "blankspaces") continue;
3234
- else if (kind === "commentStart") if (token.text === "//") this.stream.reset(this.skipToEol(token.span[1]));
3235
- else this.stream.reset(this.skipBlockComment(token.span[1]));
3236
- else if (kind === "word") {
3237
- const returnToken = token;
3238
- if (keywordOrReserved.has(token.text)) returnToken.kind = "keyword";
3239
- return returnToken;
4071
+ else if (kind === "commentStart") {
4072
+ pending ??= [];
4073
+ pending.push(this.consumeComment(token));
3240
4074
  } else if (kind === "invalid") throw new ParseError("Invalid token " + token.text, token.span);
3241
- else return token;
4075
+ else {
4076
+ this.recordTrivia(token.span[0], pending);
4077
+ const result = token;
4078
+ if (kind === "word" && keywordOrReserved.has(token.text)) result.kind = "keyword";
4079
+ return result;
4080
+ }
4081
+ }
4082
+ }
4083
+ /** If the last peek() was at the current position, consume and return it. */
4084
+ usePeeked() {
4085
+ const peeked = this.peeked;
4086
+ if (peeked !== null && peeked.pos === this.checkpoint()) {
4087
+ this.reset(peeked.end);
4088
+ return peeked;
4089
+ }
4090
+ return null;
4091
+ }
4092
+ /** Skip a comment (rejecting embedded \0), advance the stream past it,
4093
+ * and return it as trivia. */
4094
+ consumeComment(token) {
4095
+ const style = token.text === "//" ? "line" : "block";
4096
+ const start = token.span[0];
4097
+ const end = this.commentEnd(token);
4098
+ const bodyNull = this.src.slice(start, end).indexOf("\0");
4099
+ if (bodyNull >= 0) {
4100
+ const at = start + bodyNull;
4101
+ throw new ParseError("Invalid token \\0", [at, at + 1]);
3242
4102
  }
4103
+ this.stream.reset(end);
4104
+ return {
4105
+ style,
4106
+ start,
4107
+ end
4108
+ };
3243
4109
  }
3244
4110
  /** Peek at the next token without consuming it */
3245
4111
  peek() {
3246
4112
  const pos = this.checkpoint();
4113
+ const peeked = this.peeked;
4114
+ if (peeked !== null && peeked.pos === pos) return peeked.token;
3247
4115
  const token = this.nextToken();
4116
+ const end = this.checkpoint();
3248
4117
  this.reset(pos);
4118
+ this.peeked = {
4119
+ pos,
4120
+ token,
4121
+ end
4122
+ };
3249
4123
  return token;
3250
4124
  }
3251
4125
  /** Consume token if text matches, otherwise leave position unchanged */
@@ -3289,10 +4163,15 @@ var WeslStream = class {
3289
4163
  }
3290
4164
  return tokens;
3291
4165
  }
3292
- skipToEol(position) {
4166
+ /** End position of a comment opened by a commentStart token. */
4167
+ commentEnd(token) {
4168
+ return token.text === "//" ? this.lineCommentEnd(token.span[1]) : this.skipBlockComment(token.span[1]);
4169
+ }
4170
+ /** End of a line comment: the start of the next line break (or end of file). */
4171
+ lineCommentEnd(position) {
3293
4172
  this.eolPattern.lastIndex = position;
3294
- if (this.eolPattern.exec(this.src) === null) return this.src.length;
3295
- else return this.eolPattern.lastIndex;
4173
+ const result = this.eolPattern.exec(this.src);
4174
+ return result === null ? this.src.length : result.index;
3296
4175
  }
3297
4176
  skipBlockComment(start) {
3298
4177
  let position = start;
@@ -3314,39 +4193,46 @@ var WeslStream = class {
3314
4193
  const startPosition = this.stream.checkpoint();
3315
4194
  const token = this.nextToken();
3316
4195
  this.stream.reset(startPosition);
3317
- if (token === null) return null;
3318
- if (token.kind !== "symbol") return null;
3319
- if (token.text === "<") if (this.isTemplateStart(token.span[1])) {
3320
- this.stream.reset(token.span[1]);
3321
- return token;
3322
- } else {
4196
+ if (token === null || token.kind !== "symbol" || token.text !== "<") return null;
4197
+ if (!this.isTemplateStart(token.span[1])) {
3323
4198
  this.stream.reset(startPosition);
3324
4199
  return null;
3325
4200
  }
3326
- else return null;
4201
+ this.stream.reset(token.span[1]);
4202
+ return token;
3327
4203
  }
4204
+ /** Match a template-closing `>`, splitting it off a `>>`/`>=`/`>>=` token when needed. */
3328
4205
  nextTemplateEndToken() {
3329
4206
  const startPosition = this.stream.checkpoint();
3330
4207
  const token = this.nextToken();
3331
4208
  this.stream.reset(startPosition);
3332
4209
  if (token === null) return null;
3333
- if (token.kind === "symbol" && token.text[0] === ">") {
3334
- const tokenPosition = token.span[0];
3335
- this.stream.reset(tokenPosition + 1);
3336
- return {
3337
- kind: "symbol",
3338
- span: [tokenPosition, tokenPosition + 1],
3339
- text: ">"
3340
- };
3341
- } else return null;
4210
+ if (token.kind !== "symbol" || token.text[0] !== ">") return null;
4211
+ const tokenPosition = token.span[0];
4212
+ this.stream.reset(tokenPosition + 1);
4213
+ return {
4214
+ kind: "symbol",
4215
+ span: [tokenPosition, tokenPosition + 1],
4216
+ text: ">"
4217
+ };
4218
+ }
4219
+ /** Next symbol from the raw stream, skipping comment bodies (so symbols
4220
+ * inside comments don't count) and non-symbol tokens; null at EOF. */
4221
+ nextRawSymbol() {
4222
+ while (true) {
4223
+ const token = this.stream.nextToken();
4224
+ if (token === null) return null;
4225
+ if (token.kind === "commentStart") this.stream.reset(this.commentEnd(token));
4226
+ else if (token.kind === "symbol") return token;
4227
+ }
3342
4228
  }
4229
+ /** Template-list discovery: scan forward from `<` for a balanced closing `>`. */
3343
4230
  isTemplateStart(afterToken) {
3344
4231
  this.stream.reset(afterToken);
3345
4232
  let pendingCounter = 1;
3346
4233
  while (true) {
3347
- const nextToken = this.stream.nextToken();
4234
+ const nextToken = this.nextRawSymbol();
3348
4235
  if (nextToken === null) return false;
3349
- if (nextToken.kind !== "symbol") continue;
3350
4236
  if (nextToken.text === "<") pendingCounter += 1;
3351
4237
  else if (nextToken.text[0] === ">") {
3352
4238
  if (nextToken.text === ">" || nextToken.text === ">=") pendingCounter -= 1;
@@ -3364,12 +4250,11 @@ var WeslStream = class {
3364
4250
  */
3365
4251
  skipBracketsTo(closingBracket) {
3366
4252
  while (true) {
3367
- const nextToken = this.stream.nextToken();
4253
+ const nextToken = this.nextRawSymbol();
3368
4254
  if (nextToken === null) {
3369
4255
  const after = this.stream.checkpoint();
3370
4256
  throw new ParseError("Unclosed bracket!", [after, after]);
3371
4257
  }
3372
- if (nextToken.kind !== "symbol") continue;
3373
4258
  if (nextToken.text === "(") this.skipBracketsTo(")");
3374
4259
  else if (nextToken.text === "[") this.skipBracketsTo("]");
3375
4260
  else if (nextToken.text === closingBracket) return;
@@ -3382,10 +4267,10 @@ var WeslStream = class {
3382
4267
  function parseWesl(srcModule, options) {
3383
4268
  const { ctx, state } = createParseState(srcModule, options);
3384
4269
  try {
3385
- beginElem(ctx, "module");
3386
4270
  parseModule(ctx);
3387
- const moduleElem = state.stable.moduleElem;
3388
- moduleElem.contents = finishContents(ctx, 0, moduleElem.end);
4271
+ const { moduleElem } = state.stable;
4272
+ attachComments(ctx, moduleElem);
4273
+ checkDoBlockNames(moduleElem);
3389
4274
  return state.stable;
3390
4275
  } catch (e) {
3391
4276
  if (e instanceof ParseError) throw new WeslParseError({
@@ -3399,29 +4284,27 @@ function parseWesl(srcModule, options) {
3399
4284
  }
3400
4285
  }
3401
4286
  /** Initialize parse state: token stream, root scope, and module element. */
3402
- function createParseState(srcModule, options) {
4287
+ function createParseState(srcModule, parseOptions) {
3403
4288
  const stream = new WeslStream(srcModule.src);
3404
4289
  const rootScope = emptyScope(null);
3405
4290
  const moduleElem = {
3406
4291
  kind: "module",
3407
- contents: [],
4292
+ decls: [],
3408
4293
  start: 0,
3409
4294
  end: srcModule.src.length
3410
4295
  };
3411
4296
  const state = {
3412
- context: {
3413
- scope: rootScope,
3414
- openElems: []
3415
- },
4297
+ context: { scope: rootScope },
3416
4298
  stable: {
3417
4299
  srcModule,
3418
4300
  moduleElem,
3419
4301
  rootScope,
3420
- imports: []
4302
+ imports: [],
4303
+ parseOptions
3421
4304
  }
3422
4305
  };
3423
4306
  return {
3424
- ctx: new ParsingContext(stream, state, options),
4307
+ ctx: new ParsingContext(stream, state, parseOptions),
3425
4308
  state
3426
4309
  };
3427
4310
  }
@@ -3432,14 +4315,14 @@ var WeslParseError = class extends Error {
3432
4315
  span;
3433
4316
  src;
3434
4317
  constructor(opts) {
3435
- const source = opts.src.src;
3436
- const [lineNum, linePos] = offsetToLineNumber(opts.cause.span[0], source);
3437
- let message = `${opts.src.debugFilePath}:${lineNum}:${linePos}`;
3438
- message += ` error: ${opts.cause.message}\n`;
3439
- message += errorHighlight(source, opts.cause.span).join("\n");
3440
- super(message, { cause: opts.cause });
3441
- this.span = opts.cause.span;
3442
- this.src = opts.src;
4318
+ const { cause, src } = opts;
4319
+ const source = src.src;
4320
+ const [lineNum, linePos] = offsetToLineNumber(cause.span[0], source);
4321
+ const highlight = errorHighlight(source, cause.span).join("\n");
4322
+ const message = `${src.debugFilePath}:${lineNum}:${linePos} error: ${cause.message}\n${highlight}`;
4323
+ super(message, { cause });
4324
+ this.span = cause.span;
4325
+ this.src = src;
3443
4326
  }
3444
4327
  };
3445
4328
  /** Parse a WESL file. */
@@ -3449,8 +4332,8 @@ function parseSrcModule(srcModule, options) {
3449
4332
  /** @return flattened form of import tree for binding idents. */
3450
4333
  function flatImports(ast, conditions) {
3451
4334
  if (ast._flatImports && !conditions) return ast._flatImports;
3452
- const importElems = ast.moduleElem.contents.filter((elem) => elem.kind === "import");
3453
- const flat = (conditions ? filterValidElements(importElems, conditions) : importElems).map((elem) => elem.imports).flatMap(flattenTreeImport);
4335
+ const importElems = declsOfKind(ast.moduleElem, "import");
4336
+ const flat = (conditions ? filterValidElements(importElems, conditions) : importElems).flatMap((elem) => flattenTreeImport(elem.imports));
3454
4337
  if (!conditions) ast._flatImports = flat;
3455
4338
  return flat;
3456
4339
  }
@@ -3747,11 +4630,13 @@ var RecordResolver = class {
3747
4630
  sources;
3748
4631
  packageName;
3749
4632
  debugWeslRoot;
4633
+ weslExtensions;
3750
4634
  constructor(sources, options = {}) {
3751
- const { packageName = "package", debugWeslRoot } = options;
4635
+ const { packageName = "package", debugWeslRoot, weslExtensions } = options;
3752
4636
  this.sources = sources;
3753
4637
  this.packageName = packageName;
3754
4638
  this.debugWeslRoot = normalizeDebugRoot(debugWeslRoot);
4639
+ this.weslExtensions = weslExtensions;
3755
4640
  }
3756
4641
  resolveModule(modulePath) {
3757
4642
  const cached = this.astCache.get(modulePath);
@@ -3762,7 +4647,7 @@ var RecordResolver = class {
3762
4647
  modulePath,
3763
4648
  debugFilePath: this.modulePathToDebugPath(modulePath),
3764
4649
  src: source
3765
- });
4650
+ }, { weslExtensions: this.weslExtensions });
3766
4651
  this.astCache.set(modulePath, ast);
3767
4652
  return ast;
3768
4653
  }
@@ -4095,12 +4980,13 @@ var SrcMap = class SrcMap {
4095
4980
  const { src, position: srcStart } = this.destToSrc(e.srcStart);
4096
4981
  const { src: endSrc, position: srcEnd } = this.destToSrc(e.srcEnd);
4097
4982
  if (endSrc !== src) throw new Error("NYI, need to split");
4983
+ const { destStart, destEnd } = e;
4098
4984
  return {
4099
4985
  src,
4100
4986
  srcStart,
4101
4987
  srcEnd,
4102
- destStart: e.destStart,
4103
- destEnd: e.destEnd
4988
+ destStart,
4989
+ destEnd
4104
4990
  };
4105
4991
  });
4106
4992
  const otherSources = other.entries.filter((e) => e.src.path !== this.dest.path || e.src.text !== this.dest.text);
@@ -4121,10 +5007,6 @@ var SrcMap = class SrcMap {
4121
5007
  };
4122
5008
  }
4123
5009
  };
4124
- /** sort entries in place by src start position */
4125
- function sortSrc(entries) {
4126
- entries.sort((a, b) => a.srcStart - b.srcStart);
4127
- }
4128
5010
  /** Incrementally append to a string, tracking source references */
4129
5011
  var SrcMapBuilder = class {
4130
5012
  #fragments = [];
@@ -4178,11 +5060,6 @@ var SrcMapBuilder = class {
4178
5060
  };
4179
5061
  this.add("\n", srcStart, srcEnd);
4180
5062
  }
4181
- /** copy a string fragment from the src to the destination string */
4182
- addCopy(srcStart, srcEnd) {
4183
- const fragment = this.source.text.slice(srcStart, srcEnd);
4184
- this.add(fragment, srcStart, srcEnd);
4185
- }
4186
5063
  /** return a SrcMap */
4187
5064
  static build(builders) {
4188
5065
  const map = new SrcMap({ text: builders.map((b) => b.#fragments.join("")).join("") }, builders.flatMap((b) => b.#entries));
@@ -4190,6 +5067,10 @@ var SrcMapBuilder = class {
4190
5067
  return map;
4191
5068
  }
4192
5069
  };
5070
+ /** sort entries in place by src start position */
5071
+ function sortSrc(entries) {
5072
+ entries.sort((a, b) => a.srcStart - b.srcStart);
5073
+ }
4193
5074
  //#endregion
4194
5075
  //#region ../wesl/src/Linker.ts
4195
5076
  /**
@@ -4207,10 +5088,12 @@ async function link(params) {
4207
5088
  /** linker api for benchmarking */
4208
5089
  function _linkSync(params) {
4209
5090
  const { weslSrc, libs = [], packageName, debugWeslRoot, resolver } = params;
5091
+ const { weslExtensions } = params;
4210
5092
  if (!resolver && !weslSrc) throw new Error("Either resolver or weslSrc must be provided");
4211
5093
  const allResolvers = [resolver ?? new RecordResolver(weslSrc, {
4212
5094
  packageName,
4213
- debugWeslRoot
5095
+ debugWeslRoot,
5096
+ weslExtensions
4214
5097
  }), ...createLibraryResolvers(libs, debugWeslRoot)];
4215
5098
  const finalResolver = allResolvers.length === 1 ? allResolvers[0] : new CompositeResolver(allResolvers);
4216
5099
  return linkRegistry({
@@ -4342,9 +5225,6 @@ function builderFromModule(srcModule) {
4342
5225
  path: srcModule.debugFilePath
4343
5226
  });
4344
5227
  }
4345
- matchOneOf(textureStorageTypes);
4346
- matchOneOf(sampledTextureTypes);
4347
- matchOneOf(multisampledTextureTypes);
4348
5228
  //#endregion
4349
5229
  //#region ../wesl/src/WeslDevice.ts
4350
5230
  /**
@@ -4696,7 +5576,7 @@ async function fetchDependencies(rootModuleSource, options) {
4696
5576
  const existingSources = options?.existingSources;
4697
5577
  const fetchLibs = options?.fetchLibs ?? true;
4698
5578
  const fetchSources = options?.fetchSources ?? true;
4699
- const rootModuleName = currentPath ? urlToModulePath(currentPath, shaderRoot) : "package::main";
5579
+ const rootModuleName = options?.rootModuleName ?? (currentPath ? urlToModulePath(currentPath, shaderRoot) : "package::main");
4700
5580
  const resolver = new FetchingResolver({
4701
5581
  ...existingSources,
4702
5582
  [rootModuleName]: rootModuleSource
@@ -4850,24 +5730,33 @@ function findAnnotation(elem, name) {
4850
5730
  if (attr.kind === "@attribute" && attr.name === name) return attr;
4851
5731
  }
4852
5732
  }
4853
- /** Extract string params from an annotation's UnknownExpressionElem params. */
5733
+ /** The string value of each of an annotation's params. */
4854
5734
  function annotationParams(attr) {
4855
- if (!attr.params) return [];
4856
- return attr.params.map((expr) => exprToString(expr.contents));
5735
+ return attr.params?.map((param) => exprToString(param.expression)) ?? [];
4857
5736
  }
4858
- /** Extract the string value from an expression's contents. */
4859
- function exprToString(contents) {
4860
- for (const child of contents) {
4861
- if (child.kind === "literal") return child.value;
4862
- if (child.kind === "name") return child.name;
4863
- if (child.kind === "ref") return child.ident.originalName;
4864
- if (child.kind === "text") return child.srcModule.src.slice(child.start, child.end);
4865
- }
5737
+ /** Extract numeric params from an annotation, parsing WGSL numeric literals
5738
+ * (suffixes like `256u`/`1.5f`, digit separators, and hex). */
5739
+ function numericParams(attr) {
5740
+ return annotationParams(attr).map(wgslNumber);
5741
+ }
5742
+ /** The originalName of an attribute parameter that is a bare identifier ref. */
5743
+ function firstRefName(param) {
5744
+ const expr = param?.expression;
5745
+ return expr?.kind === "ref" ? expr.ident.originalName : void 0;
5746
+ }
5747
+ /** Extract the string value of an attribute-parameter expression. */
5748
+ function exprToString(expr) {
5749
+ if (expr.kind === "literal") return expr.value;
5750
+ if (expr.kind === "ref") return expr.ident.originalName;
4866
5751
  return "";
4867
5752
  }
4868
- /** Extract numeric params from an annotation. */
4869
- function numericParams(attr) {
4870
- return annotationParams(attr).map(Number);
5753
+ /** Convert a WGSL numeric literal's text to a number. Strips the `u`/`i`/`f`/`h`
5754
+ * suffix (only `u`/`i` for hex, where `f`/`h` are digits) and `_` separators.
5755
+ * Non-numeric text (e.g. an unresolved ref name) yields NaN. */
5756
+ function wgslNumber(text) {
5757
+ const t = text.replace(/_/g, "");
5758
+ if (/^[+-]?0[xX]/.test(t)) return Number(t.replace(/[uiUI]$/, ""));
5759
+ return Number(t.replace(/[uifhUIFH]$/, ""));
4871
5760
  }
4872
5761
  //#endregion
4873
5762
  //#region ../wesl-reflect/src/WeslStructs.ts
@@ -4923,7 +5812,7 @@ function structLayout(struct, conditions, structs) {
4923
5812
  /** Build a StructRegistry from all top-level struct declarations in `ast`. */
4924
5813
  function buildStructRegistry(ast) {
4925
5814
  const reg = /* @__PURE__ */ new Map();
4926
- for (const e of ast.moduleElem.contents) if (e.kind === "struct") reg.set(e.name.ident.originalName, e);
5815
+ for (const e of declsOfKind(ast.moduleElem, "struct")) reg.set(e.name.ident.originalName, e);
4927
5816
  return reg;
4928
5817
  }
4929
5818
  /** Resolve alignment and size for any host-shareable typeRef (primitive, array, or nested struct). */
@@ -5341,7 +6230,7 @@ function scalarSize(s) {
5341
6230
  return s === "f16" ? 2 : 4;
5342
6231
  }
5343
6232
  function findGlobalVar(ast, varName) {
5344
- return ast.moduleElem.contents.find((e) => e.kind === "gvar" && e.name.decl.ident.originalName === varName);
6233
+ return declsOfKind(ast.moduleElem, "gvar").find((g) => g.name.decl.ident.originalName === varName);
5345
6234
  }
5346
6235
  function parseAddressSpace(declText) {
5347
6236
  const m = declText.match(/var\s*<\s*([a-z_]+)/);
@@ -5497,7 +6386,7 @@ function namedTypeShape(name, structs, varName) {
5497
6386
  function findAnnotatedResources(ast) {
5498
6387
  const src = ast.srcModule.src;
5499
6388
  const structs = buildStructRegistry(ast);
5500
- return ast.moduleElem.contents.filter((e) => e.kind === "gvar").flatMap((gvar) => discoverResource(gvar, src, structs));
6389
+ return declsOfKind(ast.moduleElem, "gvar").flatMap((gvar) => discoverResource(gvar, src, structs));
5501
6390
  }
5502
6391
  /** Linker plugin that decorates @buffer/@test_texture/@sampler globals with
5503
6392
  * @group(0) @binding(N) so they emit as bindable WGSL vars.
@@ -5506,8 +6395,7 @@ function findAnnotatedResources(ast) {
5506
6395
  function annotatedResourcesPlugin(resources, startBinding = 1) {
5507
6396
  const bindingByName = new Map(resources.map((r, i) => [r.varName, startBinding + i]));
5508
6397
  return { transform: (ast) => {
5509
- for (const elem of ast.moduleElem.contents) {
5510
- if (elem.kind !== "gvar") continue;
6398
+ for (const elem of declsOfKind(ast.moduleElem, "gvar")) {
5511
6399
  const varName = elem.name.decl.ident.originalName;
5512
6400
  const binding = bindingByName.get(varName);
5513
6401
  if (binding === void 0) continue;
@@ -5520,11 +6408,6 @@ function annotatedResourcesPlugin(resources, startBinding = 1) {
5520
6408
  groupAttr,
5521
6409
  bindingAttr
5522
6410
  ];
5523
- elem.contents = [
5524
- groupAttr,
5525
- bindingAttr,
5526
- ...elem.contents
5527
- ];
5528
6411
  }
5529
6412
  return ast;
5530
6413
  } };
@@ -5533,7 +6416,7 @@ function annotatedResourcesPlugin(resources, startBinding = 1) {
5533
6416
  function discoverResource(gvar, src, structs) {
5534
6417
  if (findAnnotation(gvar, "buffer")) return [discoverBuffer(gvar, src, structs)];
5535
6418
  const textureAttr = findAnnotation(gvar, "test_texture");
5536
- if (textureAttr) return [discoverTexture(gvar, textureAttr, src)];
6419
+ if (textureAttr) return [discoverTexture(gvar, textureAttr)];
5537
6420
  const userTextureAttr = findAnnotation(gvar, "texture");
5538
6421
  if (userTextureAttr) return [discoverUserTexture(gvar, userTextureAttr)];
5539
6422
  const samplerAttr = findAnnotation(gvar, "sampler");
@@ -5571,7 +6454,6 @@ function makeStandardAttr(name, value, anchor) {
5571
6454
  kind: "attribute",
5572
6455
  start,
5573
6456
  end,
5574
- contents: [],
5575
6457
  attribute: {
5576
6458
  kind: "@attribute",
5577
6459
  name,
@@ -5579,12 +6461,12 @@ function makeStandardAttr(name, value, anchor) {
5579
6461
  kind: "expression",
5580
6462
  start,
5581
6463
  end,
5582
- contents: [{
6464
+ expression: {
5583
6465
  kind: "literal",
5584
6466
  value: String(value),
5585
6467
  start,
5586
6468
  end
5587
- }]
6469
+ }
5588
6470
  }]
5589
6471
  }
5590
6472
  };
@@ -5600,14 +6482,12 @@ function discoverBuffer(gvar, src, structs) {
5600
6482
  byteSize: typeRef ? typeShape(typeRef, structs, varName).size : 0
5601
6483
  };
5602
6484
  }
5603
- function discoverTexture(gvar, attr, src) {
5604
- const varName = gvar.name.decl.ident.originalName;
5605
- const params = attr.params ?? [];
6485
+ function discoverTexture(gvar, attr) {
5606
6486
  return {
5607
6487
  kind: "test_texture",
5608
- varName,
5609
- source: firstRefName(params[0]) ?? "",
5610
- params: params.slice(1).map((p) => Number.parseInt(src.slice(p.start, p.end).trim(), 10) || 0),
6488
+ varName: gvar.name.decl.ident.originalName,
6489
+ source: firstRefName(attr.params?.[0]) ?? "",
6490
+ params: numericParams(attr).slice(1).map((n) => n || 0),
5611
6491
  typeName: gvar.name.typeRef ? originalTypeName(gvar.name.typeRef) : ""
5612
6492
  };
5613
6493
  }
@@ -5626,11 +6506,6 @@ function discoverSampler(gvar, attr) {
5626
6506
  filter: (firstRefName(attr.params?.[0]) ?? "linear") === "nearest" ? "nearest" : "linear"
5627
6507
  };
5628
6508
  }
5629
- /** Extract the originalName of the first `ref` expression in an attribute parameter. */
5630
- function firstRefName(param) {
5631
- const ref = param?.contents.find((c) => c.kind === "ref");
5632
- return ref?.kind === "ref" ? ref.ident.originalName : void 0;
5633
- }
5634
6509
  //#endregion
5635
6510
  //#region ../wesl-reflect/src/EntryPoints.ts
5636
6511
  const stageNames = [
@@ -5640,7 +6515,7 @@ const stageNames = [
5640
6515
  ];
5641
6516
  /** Classify all functions in a parsed WESL module by entry-point stage. */
5642
6517
  function classifyEntryPoints(ast) {
5643
- return ast.moduleElem.contents.filter((e) => e.kind === "fn").flatMap((fn) => entryPointFor(fn));
6518
+ return declsOfKind(ast.moduleElem, "fn").flatMap((fn) => entryPointFor(fn));
5644
6519
  }
5645
6520
  function entryPointFor(fn) {
5646
6521
  const stage = stageNames.find((s) => findAnnotation(fn, s));
@@ -5704,11 +6579,11 @@ function scanUniforms(entrySource, rootModulePath = "package::main") {
5704
6579
  }
5705
6580
  /** Parse source and find a struct annotated with @uniforms. */
5706
6581
  function findUniformsStruct(src, modulePath) {
5707
- return parseSrcModule({
6582
+ return declsOfKind(parseSrcModule({
5708
6583
  modulePath,
5709
6584
  debugFilePath: "entry",
5710
6585
  src
5711
- }).moduleElem.contents.filter((e) => e.kind === "struct").find(isUniformsStruct);
6586
+ }).moduleElem, "struct").find(isUniformsStruct);
5712
6587
  }
5713
6588
  //#endregion
5714
6589
  //#region ../wesl-gpu/src/LinkWeslModule.ts
@@ -5717,8 +6592,11 @@ function findUniformsStruct(src, modulePath) {
5717
6592
  async function linkWeslModule(params) {
5718
6593
  const { rootSource, scanSource, conditions, constants } = params;
5719
6594
  const { device, resolver, libs = [], rootModuleName = "main" } = params;
5720
- const { packageName, config, weslSrc, virtualLibs } = params;
5721
- const resolvers = [new RecordResolver({ [rootModuleName]: rootSource }, { packageName })];
6595
+ const { packageName, config, weslSrc, virtualLibs, weslExtensions } = params;
6596
+ const resolvers = [new RecordResolver({ [rootModuleName]: rootSource }, {
6597
+ packageName,
6598
+ weslExtensions
6599
+ })];
5722
6600
  if (weslSrc) resolvers.push(new RecordResolver(weslSrc, { packageName }));
5723
6601
  let finalResolver = resolvers.length === 1 ? resolvers[0] : new CompositeResolver(resolvers);
5724
6602
  if (resolver) finalResolver = new CompositeResolver([finalResolver, resolver]);
@@ -5758,6 +6636,17 @@ async function linkComputeShader(params) {
5758
6636
  async function runCompute(p) {
5759
6637
  return withErrorScopes(p.device, () => dispatchAndReadback(p));
5760
6638
  }
6639
+ /** Record one compute pass into the caller's encoder; the caller submits.
6640
+ * Used by interpreters that need many passes in a single command buffer. */
6641
+ function recordComputePass(p) {
6642
+ const dispatch = p.dispatchWorkgroups ?? 1;
6643
+ const pass = p.encoder.beginComputePass();
6644
+ pass.setPipeline(p.pipeline);
6645
+ pass.setBindGroup(0, p.bindGroup);
6646
+ if (typeof dispatch === "number") pass.dispatchWorkgroups(dispatch);
6647
+ else pass.dispatchWorkgroups(...dispatch);
6648
+ pass.end();
6649
+ }
5761
6650
  /** Clear each buffer to zeros so re-runs / re-tests see deterministic initial state. */
5762
6651
  function clearBuffers(device, buffers) {
5763
6652
  const list = [...buffers];
@@ -5770,7 +6659,6 @@ function clearBuffers(device, buffers) {
5770
6659
  * buffer, and return the mapped contents. */
5771
6660
  async function dispatchAndReadback(p) {
5772
6661
  const { device, module, entryPoint, bindGroup, pipelineLayout } = p;
5773
- const dispatch = p.dispatchWorkgroups ?? 1;
5774
6662
  const pipeline = device.createComputePipeline({
5775
6663
  layout: pipelineLayout,
5776
6664
  compute: {
@@ -5781,12 +6669,12 @@ async function dispatchAndReadback(p) {
5781
6669
  const stagingBuffers = /* @__PURE__ */ new Map();
5782
6670
  for (const [name, src] of p.readBuffers) stagingBuffers.set(name, createStagingBuffer(device, src.size, name));
5783
6671
  const encoder = device.createCommandEncoder();
5784
- const pass = encoder.beginComputePass();
5785
- pass.setPipeline(pipeline);
5786
- pass.setBindGroup(0, bindGroup);
5787
- if (typeof dispatch === "number") pass.dispatchWorkgroups(dispatch);
5788
- else pass.dispatchWorkgroups(...dispatch);
5789
- pass.end();
6672
+ recordComputePass({
6673
+ encoder,
6674
+ pipeline,
6675
+ bindGroup,
6676
+ dispatchWorkgroups: p.dispatchWorkgroups
6677
+ });
5790
6678
  for (const [name, src] of p.readBuffers) {
5791
6679
  const dst = stagingBuffers.get(name);
5792
6680
  encoder.copyBufferToBuffer(src, 0, dst, 0, src.size);
@@ -6744,7 +7632,7 @@ async function createPipeline(state, shaderSource, resolveTexture, options) {
6744
7632
  modulePath: `${pkg}::${root}`,
6745
7633
  debugFilePath: `./${root}.wesl`,
6746
7634
  src: shaderSource
6747
- });
7635
+ }, { weslExtensions: options?.weslExtensions });
6748
7636
  const resources = findAnnotatedResources(ast);
6749
7637
  const entryPoints = classifyEntryPoints(ast);
6750
7638
  const mode = detectMode(entryPoints);
@@ -7115,7 +8003,7 @@ var WgslPlay = class extends HTMLElement {
7115
8003
  _weslSrc = {};
7116
8004
  _rootModuleName = "package::main";
7117
8005
  _libs;
7118
- _linkOptions = {};
8006
+ _linkOptions = { weslExtensions: { doBlocks: true } };
7119
8007
  _fetchSources = true;
7120
8008
  _initPromise;
7121
8009
  _sourceEl = null;
@@ -7261,10 +8149,11 @@ var WgslPlay = class extends HTMLElement {
7261
8149
  /** Set project configuration (mirrors wesl link() API). */
7262
8150
  set project(value) {
7263
8151
  const { weslSrc, rootModuleName, libs } = value;
7264
- const { packageName, conditions, constants } = value;
8152
+ const { packageName, conditions, constants, weslExtensions } = value;
7265
8153
  if (packageName !== void 0) this._linkOptions.packageName = packageName;
7266
8154
  if (conditions !== void 0) this._linkOptions.conditions = conditions;
7267
8155
  if (constants !== void 0) this._linkOptions.constants = constants;
8156
+ if (weslExtensions !== void 0) this._linkOptions.weslExtensions = weslExtensions;
7268
8157
  if (libs) this._libs = libs;
7269
8158
  if (weslSrc) {
7270
8159
  const pkg = this._linkOptions.packageName || "package";
@@ -7612,6 +8501,7 @@ var WgslPlay = class extends HTMLElement {
7612
8501
  if (this._fetchSources || this._fetchLibs) {
7613
8502
  const { weslSrc, libs } = await fetchDependencies(mainSource, {
7614
8503
  shaderRoot: this.getConfigOverrides()?.shaderRoot,
8504
+ rootModuleName: this._rootModuleName,
7615
8505
  existingSources: this._weslSrc,
7616
8506
  fetchLibs: this._fetchLibs,
7617
8507
  fetchSources: this._fetchSources