wesl 0.7.26 → 0.7.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.ts +283 -147
  3. package/dist/index.js +1765 -1143
  4. package/package.json +2 -2
  5. package/src/AbstractElems.ts +264 -82
  6. package/src/ClickableError.ts +8 -1
  7. package/src/Linker.ts +63 -67
  8. package/src/LinkerUtil.ts +141 -7
  9. package/src/LowerAndEmit.ts +660 -304
  10. package/src/Mangler.ts +1 -1
  11. package/src/ModuleResolver.ts +15 -4
  12. package/src/ParseWESL.ts +21 -33
  13. package/src/SrcMap.ts +7 -19
  14. package/src/StandardTypes.ts +1 -1
  15. package/src/WeslDevice.ts +1 -0
  16. package/src/debug/ASTtoString.ts +92 -76
  17. package/src/index.ts +1 -1
  18. package/src/parse/AttachComments.ts +289 -0
  19. package/src/parse/ExpressionUtil.ts +3 -3
  20. package/src/parse/Keywords.ts +1 -1
  21. package/src/parse/ParseAttribute.ts +49 -71
  22. package/src/parse/ParseCall.ts +3 -4
  23. package/src/parse/ParseControlFlow.ts +100 -56
  24. package/src/parse/ParseDirective.ts +9 -8
  25. package/src/parse/ParseDoBlock.ts +63 -0
  26. package/src/parse/ParseExpression.ts +11 -10
  27. package/src/parse/ParseFn.ts +11 -33
  28. package/src/parse/ParseGlobalVar.ts +36 -27
  29. package/src/parse/ParseIdent.ts +4 -5
  30. package/src/parse/ParseLocalVar.ts +16 -12
  31. package/src/parse/ParseLoop.ts +76 -39
  32. package/src/parse/ParseModule.ts +65 -19
  33. package/src/parse/ParseSimpleStatement.ts +112 -66
  34. package/src/parse/ParseStatement.ts +40 -67
  35. package/src/parse/ParseStruct.ts +8 -22
  36. package/src/parse/ParseType.ts +10 -13
  37. package/src/parse/ParseUtil.ts +26 -12
  38. package/src/parse/ParseValueDeclaration.ts +18 -31
  39. package/src/parse/ParseWesl.ts +11 -14
  40. package/src/parse/ParsingContext.ts +11 -9
  41. package/src/parse/WeslStream.ts +138 -121
  42. package/src/parse/stream/RegexMatchers.ts +45 -0
  43. package/src/parse/stream/WeslLexer.ts +249 -0
  44. package/src/test/BevyLink.test.ts +18 -11
  45. package/src/test/ConditionalElif.test.ts +4 -2
  46. package/src/test/DeclCommentEmit.test.ts +122 -0
  47. package/src/test/DoBlock.test.ts +164 -0
  48. package/src/test/FilterValidElements.test.ts +4 -6
  49. package/src/test/Linker.test.ts +23 -7
  50. package/src/test/Mangling.test.ts +8 -4
  51. package/src/test/ParseComments.test.ts +234 -6
  52. package/src/test/ParseConditionsV2.test.ts +47 -175
  53. package/src/test/ParseElifV2.test.ts +12 -33
  54. package/src/test/ParseErrorV2.test.ts +0 -0
  55. package/src/test/ParseWeslV2.test.ts +247 -626
  56. package/src/test/StatementEmit.test.ts +143 -0
  57. package/src/test/StripWesl.ts +24 -3
  58. package/src/test/TestLink.ts +19 -3
  59. package/src/test/TestUtil.ts +18 -8
  60. package/src/test/Tokenizer.test.ts +96 -0
  61. package/src/test/__snapshots__/ParseWeslV2.test.ts.snap +10 -42
  62. package/src/RawEmit.ts +0 -103
  63. package/src/Reflection.ts +0 -336
  64. package/src/TransformBindingStructs.ts +0 -320
  65. package/src/parse/ContentsHelpers.ts +0 -70
  66. package/src/parse/stream/CachingStream.ts +0 -48
  67. package/src/parse/stream/MatchersStream.ts +0 -85
package/dist/index.js CHANGED
@@ -283,12 +283,13 @@ const isBrowser = "document" in globalThis;
283
283
  /** Throw an error with an embedded source map so that browser users can
284
284
  * click on the error in the browser debug console and see the wesl source code. */
285
285
  function throwClickableError(params) {
286
- const { url, text, lineNumber, lineColumn, length, error } = params;
286
+ const { url, text, lineNumber, lineColumn, length, offset, error } = params;
287
287
  const weslLocation = {
288
288
  file: url,
289
289
  line: lineNumber,
290
290
  column: lineColumn,
291
- length
291
+ length,
292
+ offset
292
293
  };
293
294
  error.weslLocation = weslLocation;
294
295
  if (!isBrowser) throw error;
@@ -349,6 +350,7 @@ function failIdentElem(identElem, msg = "") {
349
350
  lineNumber,
350
351
  lineColumn,
351
352
  length: end - start,
353
+ offset: start,
352
354
  error: new Error(detailedMessage)
353
355
  });
354
356
  }
@@ -497,6 +499,120 @@ function childIdent(child) {
497
499
  return !childScope(child);
498
500
  }
499
501
  //#endregion
502
+ //#region src/LinkerUtil.ts
503
+ /** A module's top-level declarations of a given kind, narrowed to that elem type. */
504
+ function declsOfKind(moduleElem, kind) {
505
+ return moduleElem.decls.filter((e) => e.kind === kind);
506
+ }
507
+ /** Visit an elem and all its descendants, parent before children (pre-order). */
508
+ function visitAst(elem, visitor) {
509
+ visitor(elem);
510
+ for (const child of childElems(elem)) visitAst(child, visitor);
511
+ }
512
+ /**
513
+ * Child elems of any AST node: the typed structural fields (attributes plus
514
+ * body / condition / decls / ...) in source order. Returns [] for leaf elems.
515
+ */
516
+ function childElems(elem) {
517
+ return [...elem.attributes ?? [], ...structuralFields(elem)];
518
+ }
519
+ function identElemLog(identElem, ...messages) {
520
+ srcLog(identElem.srcModule.src, [identElem.start, identElem.end], ...messages);
521
+ }
522
+ /** The child elems held in a node's typed fields, in source order (attributes
523
+ * are added separately by {@link childElems}). Empty for leaf kinds. The
524
+ * switch is exhaustive: a new elem kind without a case fails to compile in the
525
+ * default, so children can't be silently dropped. */
526
+ function structuralFields(elem) {
527
+ switch (elem.kind) {
528
+ case "module": return elem.decls;
529
+ case "var":
530
+ case "gvar": return [
531
+ ...elem.template ?? [],
532
+ elem.name,
533
+ ...elem.init ? [elem.init] : []
534
+ ];
535
+ case "let":
536
+ case "const":
537
+ case "override": return [elem.name, ...elem.init ? [elem.init] : []];
538
+ case "alias": return [elem.name, elem.typeRef];
539
+ case "assert": return [elem.expression];
540
+ case "struct": return [elem.name, ...elem.members];
541
+ case "member": return [elem.name, elem.typeRef];
542
+ case "type": return [elem.name.refIdentElem, ...elem.templateParams ?? []];
543
+ case "expression": return [elem.expression];
544
+ case "binary-expression": return [elem.left, elem.right];
545
+ case "unary-expression":
546
+ case "parenthesized-expression": return [elem.expression];
547
+ case "component-expression": return [elem.base, elem.access];
548
+ case "component-member-expression": return [elem.base, elem.access];
549
+ case "call-expression": return [
550
+ elem.function,
551
+ ...elem.templateArgs ?? [],
552
+ ...elem.arguments
553
+ ];
554
+ case "param": return [elem.name];
555
+ case "typeDecl": return elem.typeRef ? [elem.decl, elem.typeRef] : [elem.decl];
556
+ case "fn": return [
557
+ elem.name,
558
+ ...elem.params,
559
+ ...elem.returnType ? [elem.returnType] : [],
560
+ elem.body
561
+ ];
562
+ case "block": return elem.body;
563
+ case "if": return [
564
+ elem.condition,
565
+ elem.body,
566
+ ...elem.else ? [elem.else] : []
567
+ ];
568
+ case "for": return [
569
+ elem.init,
570
+ elem.condition,
571
+ elem.update,
572
+ elem.body
573
+ ].filter(isDefined);
574
+ case "while": return [elem.condition, elem.body];
575
+ case "loop":
576
+ case "continuing": return [elem.body];
577
+ case "switch": return [
578
+ elem.selector,
579
+ ...elem.bodyAttributes ?? [],
580
+ ...elem.clauses
581
+ ];
582
+ case "switch-clause": return [...exprSelectors(elem.selectors), elem.body];
583
+ case "return": return elem.value ? [elem.value] : [];
584
+ case "break": return elem.condition ? [elem.condition] : [];
585
+ case "assign": return elem.lhs.kind === "phony" ? [elem.rhs] : [elem.lhs, elem.rhs];
586
+ case "increment":
587
+ case "decrement": return [elem.target];
588
+ case "call": return [elem.call];
589
+ case "continue":
590
+ case "discard":
591
+ case "empty": return [];
592
+ case "do": return [
593
+ elem.name,
594
+ ...elem.params,
595
+ elem.body
596
+ ];
597
+ case "import":
598
+ case "directive":
599
+ case "attribute":
600
+ case "name":
601
+ case "literal":
602
+ case "ref":
603
+ case "decl":
604
+ case "synthetic": return [];
605
+ default: return [];
606
+ }
607
+ }
608
+ function isDefined(value) {
609
+ return value !== void 0;
610
+ }
611
+ /** Drop the `"default"` sentinel, keeping only real case-selector expressions. */
612
+ function exprSelectors(selectors) {
613
+ return selectors.filter((s) => s !== "default");
614
+ }
615
+ //#endregion
500
616
  //#region src/StandardTypes.ts
501
617
  const stdFns = `bitcast all any select arrayLength
502
618
  abs acos acosh asin asinh atan atanh atan2 ceil clamp cos cosh
@@ -598,6 +714,22 @@ function stdEnumerant(name) {
598
714
  }
599
715
  //#endregion
600
716
  //#region src/LowerAndEmit.ts
717
+ /** Statement kinds that emitStatement must not follow with a ';': compound
718
+ * statements take none, locals already carry their own, empty needs none. */
719
+ const noSemicolon = new Set([
720
+ "block",
721
+ "if",
722
+ "for",
723
+ "while",
724
+ "loop",
725
+ "continuing",
726
+ "switch",
727
+ "empty",
728
+ "var",
729
+ "let",
730
+ "const",
731
+ "assert"
732
+ ]);
601
733
  /** Traverse the AST, starting from root elements, emitting WGSL for each. */
602
734
  function lowerAndEmit(params) {
603
735
  const { srcBuilder, rootElems, conditions } = params;
@@ -605,17 +737,53 @@ function lowerAndEmit(params) {
605
737
  const emitContext = {
606
738
  conditions,
607
739
  srcBuilder,
608
- extracting
740
+ extracting,
741
+ indent: 0
609
742
  };
610
743
  const validElements = skipConditionalFiltering ? rootElems : filterValidElements(rootElems, conditions);
611
744
  for (const e of validElements) lowerAndEmitElem(e, emitContext);
612
745
  }
746
+ /** Format a diagnostic control as "(severity, rule)" for @diagnostic text. */
747
+ function diagnosticControlToString(severity, rule) {
748
+ const ruleStr = rule[0].name + (rule[1] !== null ? "." + rule[1].name : "");
749
+ return `(${severity.name}, ${ruleStr})`;
750
+ }
751
+ /** Render an expression back to WGSL source text (template args elided as <...>). */
752
+ function expressionToString(elem) {
753
+ const { kind } = elem;
754
+ switch (kind) {
755
+ case "binary-expression": {
756
+ const left = expressionToString(elem.left);
757
+ const right = expressionToString(elem.right);
758
+ return `${left} ${elem.operator.value} ${right}`;
759
+ }
760
+ case "unary-expression": return `${elem.operator.value}${expressionToString(elem.expression)}`;
761
+ case "ref": return elem.ident.originalName;
762
+ case "literal": return elem.value;
763
+ case "parenthesized-expression": return `(${expressionToString(elem.expression)})`;
764
+ case "component-expression": return `${expressionToString(elem.base)}[${elem.access}]`;
765
+ case "component-member-expression": return `${expressionToString(elem.base)}.${elem.access}`;
766
+ case "call-expression": {
767
+ const fn = elem.function;
768
+ return `${fn.kind === "ref" ? fn.ident.originalName : fn.name.originalName}${elem.templateArgs ? `<...>` : ""}(${elem.arguments.map(expressionToString).join(", ")})`;
769
+ }
770
+ case "type": return elem.name.originalName;
771
+ default: assertUnreachable(kind);
772
+ }
773
+ }
774
+ /** Trace through refersTo links until we find the declaration. */
775
+ function findDecl(ident) {
776
+ let i = ident;
777
+ do {
778
+ if (i.kind === "decl") return i;
779
+ i = i.refersTo;
780
+ } while (i);
781
+ throw new Error(`unresolved identifer: ${ident.originalName}`);
782
+ }
613
783
  function lowerAndEmitElem(e, ctx) {
614
784
  switch (e.kind) {
615
785
  case "import": return;
616
- case "text":
617
- emitText(e, ctx);
618
- return;
786
+ case "do": return;
619
787
  case "name":
620
788
  emitName(e, ctx);
621
789
  return;
@@ -638,25 +806,46 @@ function lowerAndEmitElem(e, ctx) {
638
806
  emitExpression(e, ctx);
639
807
  return;
640
808
  case "param":
809
+ emitAttributes(e.attributes, ctx);
810
+ emitTypedDecl(e.name, ctx);
811
+ return;
641
812
  case "typeDecl":
813
+ emitTypedDecl(e, ctx);
814
+ return;
642
815
  case "member":
643
- case "memberRef":
816
+ emitMember(e, ctx);
817
+ return;
644
818
  case "expression":
645
- case "type":
819
+ emitExpression(e.expression, ctx);
820
+ return;
646
821
  case "switch-clause":
647
- emitContents(e, ctx);
822
+ emitSwitchClause(e, ctx);
648
823
  return;
649
- case "stuff":
650
- emitStuff(e, ctx);
824
+ case "type":
825
+ emitTypeRef(e, ctx);
651
826
  return;
652
827
  case "module":
653
828
  emitModule(e, ctx);
654
829
  return;
655
830
  case "var":
656
831
  case "let":
657
- case "statement":
832
+ case "block":
833
+ case "if":
834
+ case "for":
835
+ case "while":
836
+ case "loop":
658
837
  case "continuing":
659
- emitStatement$1(e, ctx);
838
+ case "switch":
839
+ case "return":
840
+ case "break":
841
+ case "continue":
842
+ case "discard":
843
+ case "assign":
844
+ case "increment":
845
+ case "decrement":
846
+ case "call":
847
+ case "empty":
848
+ emitStatement(e, ctx);
660
849
  return;
661
850
  case "override":
662
851
  case "const":
@@ -667,52 +856,131 @@ function lowerAndEmitElem(e, ctx) {
667
856
  return;
668
857
  case "fn":
669
858
  emitRootElemNl(ctx);
859
+ emitRootLeading(e, ctx);
670
860
  emitFn(e, ctx);
861
+ emitTrailingComments(e, ctx);
671
862
  return;
672
863
  case "struct":
673
864
  emitRootElemNl(ctx);
865
+ emitRootLeading(e, ctx);
674
866
  emitStruct(e, ctx);
867
+ emitTrailingComments(e, ctx);
675
868
  return;
676
869
  case "attribute":
677
870
  emitAttribute(e, ctx);
678
871
  return;
679
872
  case "directive":
873
+ ctx.srcBuilder.addNl();
874
+ emitRootLeading(e, ctx);
680
875
  emitDirective(e, ctx);
876
+ emitTrailingComments(e, ctx);
681
877
  return;
682
878
  default: assertUnreachable(e);
683
879
  }
684
880
  }
685
- function emitStuff(e, ctx) {
686
- emitContentsWithTrimming(e, ctx);
881
+ function emitName(e, ctx) {
882
+ ctx.srcBuilder.add(e.name, e.start, e.end);
687
883
  }
688
- function emitModule(e, ctx) {
689
- const validElements = filterValidElements(e.contents, ctx.conditions);
690
- for (const child of validElements) {
691
- if (child.kind === "text") {
692
- if (child.srcModule.src.slice(child.start, child.end).trim() === "") continue;
884
+ function emitSynthetic(e, ctx) {
885
+ const { text } = e;
886
+ ctx.srcBuilder.addSynthetic(text, text, 0, text.length);
887
+ }
888
+ function emitRefIdent(e, ctx) {
889
+ if (e.ident.std) ctx.srcBuilder.add(e.ident.originalName, e.start, e.end);
890
+ else ctx.srcBuilder.add(displayName(findDecl(e.ident)), e.start, e.end);
891
+ }
892
+ function emitDeclIdent(e, ctx) {
893
+ ctx.srcBuilder.add(displayName(e.ident), e.start, e.end);
894
+ }
895
+ /** Emit an expression with any comments attached to it. Comments inside an
896
+ * expression (e.g. `foo(1, /* x *\/ 2)`) ride on the relevant sub-node and are
897
+ * emitted inline around it here. */
898
+ function emitExpression(e, ctx) {
899
+ emitInlineLeading(e, ctx);
900
+ emitExpressionCore(e, ctx);
901
+ emitInlineTrailing(e, ctx);
902
+ }
903
+ function emitAttributes(attributes, ctx) {
904
+ attributes?.forEach((a) => {
905
+ emitInlineLeading(a, ctx);
906
+ if (emitAttribute(a, ctx)) {
907
+ emitInlineTrailing(a, ctx);
908
+ ctx.srcBuilder.add(" ", a.start, a.end);
693
909
  }
694
- lowerAndEmitElem(child, ctx);
910
+ });
911
+ }
912
+ /** Emit a declared identifier with its optional `: type` annotation. */
913
+ function emitTypedDecl(name, ctx) {
914
+ emitInlineLeading(name, ctx);
915
+ emitDeclIdent(name.decl, ctx);
916
+ if (name.typeRef) {
917
+ ctx.srcBuilder.appendNext(": ");
918
+ emitTypeRef(name.typeRef, ctx);
919
+ }
920
+ emitInlineTrailing(name, ctx);
921
+ }
922
+ /** Emit a struct member from its typed fields: `[attrs] name: type`. */
923
+ function emitMember(member, ctx) {
924
+ emitAttributes(member.attributes, ctx);
925
+ emitName(member.name, ctx);
926
+ ctx.srcBuilder.appendNext(": ");
927
+ emitTypeRef(member.typeRef, ctx);
928
+ }
929
+ /** A `case sel, ...:` or `default:` clause with its `{ ... }` body. The selector
930
+ * colon is optional in WGSL but kept here as the canonical form. */
931
+ function emitSwitchClause(e, ctx) {
932
+ const builder = ctx.srcBuilder;
933
+ emitLeadingComments(e, ctx);
934
+ newLine(ctx);
935
+ emitAttributes(e.attributes, ctx);
936
+ if (e.selectors.length === 1 && e.selectors[0] === "default") builder.appendNext("default");
937
+ else {
938
+ builder.appendNext("case ");
939
+ e.selectors.forEach((sel, i) => {
940
+ if (i > 0) builder.appendNext(", ");
941
+ if (sel === "default") builder.appendNext("default");
942
+ else emitExpression(sel, ctx);
943
+ });
695
944
  }
945
+ builder.appendNext(": ");
946
+ emitBlock(e.body, ctx);
947
+ emitTrailingComments(e, ctx);
948
+ }
949
+ /** Emit a type reference structurally: name plus an optional <...> arg list. */
950
+ function emitTypeRef(e, ctx) {
951
+ emitRefIdent(e.name.refIdentElem, ctx);
952
+ if (e.templateParams) emitTemplateArgs(e.templateParams, ctx);
696
953
  }
697
- function emitStatement$1(e, ctx) {
698
- if (!(e.contents.length > 0 && e.contents[0].kind === "attribute")) emitAttributes(e.attributes, ctx);
699
- emitContents(e, ctx);
954
+ function emitModule(e, ctx) {
955
+ const validElements = filterValidElements(e.decls, ctx.conditions);
956
+ for (const child of validElements) lowerAndEmitElem(child, ctx);
957
+ }
958
+ /** Emit one statement on its own line, with attached leading/trailing comments. */
959
+ function emitStatement(stmt, ctx) {
960
+ emitLeadingComments(stmt, ctx);
961
+ newLine(ctx);
962
+ emitCoreSemi(stmt, ctx);
963
+ emitTrailingComments(stmt, ctx);
700
964
  }
701
965
  function emitRootDecl(e, ctx) {
702
966
  emitRootElemNl(ctx);
703
- if (!(e.contents.length > 0 && e.contents[0].kind === "attribute")) emitAttributes(e.attributes, ctx);
704
- emitContentsWithTrimming(e, ctx);
967
+ emitRootLeading(e, ctx);
968
+ emitValueDecl(e, ctx);
969
+ emitTrailingComments(e, ctx);
705
970
  }
706
971
  /** Emit newlines between root elements. */
707
972
  function emitRootElemNl(ctx) {
708
973
  ctx.srcBuilder.addNl();
709
974
  ctx.srcBuilder.addNl();
710
975
  }
711
- function emitText(e, ctx) {
712
- ctx.srcBuilder.addCopy(e.start, e.end);
713
- }
714
- function emitName(e, ctx) {
715
- ctx.srcBuilder.add(e.name, e.start, e.end);
976
+ /** Leading comments for a root declaration, each on its own line. The inter-decl
977
+ * spacing already left us at a fresh line, so a newline follows each comment
978
+ * (rather than preceding it as in emitLeadingComments). */
979
+ function emitRootLeading(e, ctx) {
980
+ for (const c of e.commentsBefore ?? []) {
981
+ emitComment(c, ctx);
982
+ newLine(ctx);
983
+ }
716
984
  }
717
985
  /** Emit function explicitly to control commas between conditional parameters. */
718
986
  function emitFn(e, ctx) {
@@ -720,27 +988,35 @@ function emitFn(e, ctx) {
720
988
  const { conditions, srcBuilder: builder } = ctx;
721
989
  emitAttributes(attributes, ctx);
722
990
  builder.add("fn ", name.start - 3, name.start);
991
+ emitInlineLeading(name, ctx);
723
992
  emitDeclIdent(name, ctx);
993
+ emitInlineTrailing(name, ctx);
724
994
  builder.appendNext("(");
725
995
  const validParams = filterValidElements(params, conditions);
726
996
  validParams.forEach((p, i) => {
727
- if (!(p.contents.length > 0 && p.contents[0].kind === "attribute")) emitAttributes(p.attributes, ctx);
728
- emitContentsNoWs(p, ctx);
997
+ emitInlineLeading(p, ctx);
998
+ emitAttributes(p.attributes, ctx);
999
+ emitTypedDecl(p.name, ctx);
1000
+ emitInlineTrailing(p, ctx);
729
1001
  if (i < validParams.length - 1) builder.appendNext(", ");
730
1002
  });
731
1003
  builder.appendNext(") ");
732
1004
  if (returnType) {
733
1005
  builder.appendNext("-> ");
734
1006
  emitAttributes(returnAttributes, ctx);
735
- emitContentsNoWs(returnType, ctx);
1007
+ emitInlineLeading(returnType, ctx);
1008
+ emitTypeRef(returnType, ctx);
736
1009
  builder.appendNext(" ");
737
1010
  }
738
- emitContents(body, ctx);
1011
+ emitBlock(body, ctx);
739
1012
  }
740
- function emitAttributes(attributes, ctx) {
741
- attributes?.forEach((a) => {
742
- if (emitAttribute(a, ctx)) ctx.srcBuilder.add(" ", a.start, a.end);
743
- });
1013
+ /** Trailing comments: kept on the element's line, after its text. */
1014
+ function emitTrailingComments(e, ctx) {
1015
+ if (!e.commentsAfter) return;
1016
+ for (const c of e.commentsAfter) {
1017
+ ctx.srcBuilder.appendNext(" ");
1018
+ emitComment(c, ctx);
1019
+ }
744
1020
  }
745
1021
  /** Emit structs explicitly to control commas between conditional members. */
746
1022
  function emitStruct(e, ctx) {
@@ -754,125 +1030,22 @@ function emitStruct(e, ctx) {
754
1030
  }
755
1031
  emitAttributes(attributes, ctx);
756
1032
  srcBuilder.add("struct ", start, name.start);
1033
+ emitInlineLeading(name, ctx);
757
1034
  emitDeclIdent(name, ctx);
758
- if (validLength === 1) {
1035
+ emitInlineTrailing(name, ctx);
1036
+ if (validLength === 1 && !hasComments(validMembers[0])) {
759
1037
  srcBuilder.appendNext(" { ");
760
- emitContentsWithTrimming(validMembers[0], ctx);
1038
+ emitMember(validMembers[0], ctx);
761
1039
  srcBuilder.appendNext(" }");
762
1040
  srcBuilder.addNl();
763
1041
  } else {
764
1042
  srcBuilder.appendNext(" {");
765
1043
  srcBuilder.addNl();
766
- validMembers.forEach((m) => {
767
- srcBuilder.appendNext(" ");
768
- emitContentsNoWs(m, ctx);
769
- srcBuilder.appendNext(",");
770
- srcBuilder.addNl();
771
- });
1044
+ for (const m of validMembers) emitMemberLine(m, ctx);
772
1045
  srcBuilder.appendNext("}");
773
1046
  srcBuilder.addNl();
774
1047
  }
775
1048
  }
776
- function warnEmptyStruct(e) {
777
- const { name, members } = e;
778
- const condStr = members.length ? "(with current conditions)" : "";
779
- failIdentElem(name, `struct '${name.ident.originalName}' has no members ${condStr}`);
780
- }
781
- function emitSynthetic(e, ctx) {
782
- const { text } = e;
783
- ctx.srcBuilder.addSynthetic(text, text, 0, text.length);
784
- }
785
- function emitContents(elem, ctx) {
786
- const validElements = filterValidElements(elem.contents, ctx.conditions);
787
- for (const e of validElements) lowerAndEmitElem(e, ctx);
788
- }
789
- /** Emit contents with leading/trailing whitespace trimming (V2 parser). */
790
- function emitContentsWithTrimming(elem, ctx) {
791
- const validElements = filterValidElements(elem.contents, ctx.conditions);
792
- const firstEmit = validElements.findIndex((e) => !isConditionalAttr(e));
793
- const lastEmit = validElements.findLastIndex((e) => !isConditionalAttr(e));
794
- validElements.forEach((elem, i) => {
795
- if (elem.kind === "text") {
796
- let text = elem.srcModule.src.slice(elem.start, elem.end);
797
- if (i === firstEmit) text = text.trimStart();
798
- if (i === lastEmit) text = text.trimEnd();
799
- if (text) ctx.srcBuilder.add(text, elem.start, elem.end);
800
- } else lowerAndEmitElem(elem, ctx);
801
- });
802
- }
803
- function isConditionalAttr(e) {
804
- if (e.kind !== "attribute") return false;
805
- const { kind } = e.attribute;
806
- return kind === "@if" || kind === "@elif" || kind === "@else";
807
- }
808
- /** Emit contents without whitespace. */
809
- function emitContentsNoWs(elem, ctx) {
810
- filterValidElements(elem.contents, ctx.conditions).forEach((e) => {
811
- if (e.kind === "text") {
812
- const { srcModule, start, end } = e;
813
- if (srcModule.src.slice(start, end).trim() === "") return;
814
- }
815
- lowerAndEmitElem(e, ctx);
816
- });
817
- }
818
- function emitRefIdent(e, ctx) {
819
- if (e.ident.std) ctx.srcBuilder.add(e.ident.originalName, e.start, e.end);
820
- else {
821
- const mangledName = displayName(findDecl(e.ident));
822
- ctx.srcBuilder.add(mangledName, e.start, e.end);
823
- }
824
- }
825
- function emitDeclIdent(e, ctx) {
826
- const mangledName = displayName(e.ident);
827
- ctx.srcBuilder.add(mangledName, e.start, e.end);
828
- }
829
- function emitExpression(e, ctx) {
830
- const { kind } = e;
831
- if (kind === "literal") {
832
- ctx.srcBuilder.add(e.value, e.start, e.end);
833
- return;
834
- }
835
- if (kind === "ref") {
836
- emitRefIdent(e, ctx);
837
- return;
838
- }
839
- if (kind === "type") {
840
- emitContents(e, ctx);
841
- return;
842
- }
843
- if (kind === "binary-expression") {
844
- emitExpression(e.left, ctx);
845
- ctx.srcBuilder.add(` ${e.operator.value} `, e.operator.span[0], e.operator.span[1]);
846
- emitExpression(e.right, ctx);
847
- return;
848
- }
849
- if (kind === "unary-expression") {
850
- ctx.srcBuilder.add(e.operator.value, e.operator.span[0], e.operator.span[1]);
851
- emitExpression(e.expression, ctx);
852
- return;
853
- }
854
- if (kind === "parenthesized-expression") {
855
- emitExpression(e.expression, ctx);
856
- return;
857
- }
858
- if (kind === "call-expression") {
859
- emitExpression(e.function, ctx);
860
- if (e.templateArgs) for (const targ of e.templateArgs) lowerAndEmitElem(targ, ctx);
861
- for (const arg of e.arguments) emitExpression(arg, ctx);
862
- return;
863
- }
864
- if (kind === "component-expression") {
865
- emitExpression(e.base, ctx);
866
- emitExpression(e.access, ctx);
867
- return;
868
- }
869
- if (kind === "component-member-expression") {
870
- emitExpression(e.base, ctx);
871
- if (e.access.kind === "name") ctx.srcBuilder.add(e.access.name, e.access.start, e.access.end);
872
- return;
873
- }
874
- assertUnreachable(kind);
875
- }
876
1049
  function emitAttribute(e, ctx) {
877
1050
  const { kind } = e.attribute;
878
1051
  if (kind === "@if" || kind === "@elif" || kind === "@else") return false;
@@ -882,11 +1055,13 @@ function emitAttribute(e, ctx) {
882
1055
  return true;
883
1056
  }
884
1057
  if (kind === "@builtin") {
885
- ctx.srcBuilder.add("@builtin(" + e.attribute.param.name + ")", e.start, e.end);
1058
+ const builtinStr = `@builtin(${e.attribute.param.name})`;
1059
+ ctx.srcBuilder.add(builtinStr, e.start, e.end);
886
1060
  return true;
887
1061
  }
888
1062
  if (kind === "@diagnostic") {
889
- const diagStr = "@diagnostic" + diagnosticControlToString(e.attribute.severity, e.attribute.rule);
1063
+ const { severity, rule } = e.attribute;
1064
+ const diagStr = `@diagnostic${diagnosticControlToString(severity, rule)}`;
890
1065
  ctx.srcBuilder.add(diagStr, e.start, e.end);
891
1066
  return true;
892
1067
  }
@@ -897,46 +1072,6 @@ function emitAttribute(e, ctx) {
897
1072
  }
898
1073
  assertUnreachable(kind);
899
1074
  }
900
- function emitStandardAttribute(e, ctx) {
901
- if (e.attribute.kind !== "@attribute") return;
902
- const { params } = e.attribute;
903
- if (!params || params.length === 0) {
904
- ctx.srcBuilder.add("@" + e.attribute.name, e.start, e.end);
905
- return;
906
- }
907
- ctx.srcBuilder.add("@" + e.attribute.name + "(", e.start, params[0].start);
908
- for (let i = 0; i < params.length; i++) {
909
- emitContents(params[i], ctx);
910
- if (i < params.length - 1) ctx.srcBuilder.add(",", params[i].end, params[i + 1].start);
911
- }
912
- ctx.srcBuilder.add(")", params[params.length - 1].end, e.end);
913
- }
914
- function diagnosticControlToString(severity, rule) {
915
- const ruleStr = rule[0].name + (rule[1] !== null ? "." + rule[1].name : "");
916
- return `(${severity.name}, ${ruleStr})`;
917
- }
918
- function expressionToString(elem) {
919
- const { kind } = elem;
920
- switch (kind) {
921
- case "binary-expression": {
922
- const left = expressionToString(elem.left);
923
- const right = expressionToString(elem.right);
924
- return `${left} ${elem.operator.value} ${right}`;
925
- }
926
- case "unary-expression": return `${elem.operator.value}${expressionToString(elem.expression)}`;
927
- case "ref": return elem.ident.originalName;
928
- case "literal": return elem.value;
929
- case "parenthesized-expression": return `(${expressionToString(elem.expression)})`;
930
- case "component-expression": return `${expressionToString(elem.base)}[${elem.access}]`;
931
- case "component-member-expression": return `${expressionToString(elem.base)}.${elem.access}`;
932
- case "call-expression": {
933
- const fn = elem.function;
934
- return `${fn.kind === "ref" ? fn.ident.originalName : fn.name.originalName}${elem.templateArgs ? `<...>` : ""}(${elem.arguments.map(expressionToString).join(", ")})`;
935
- }
936
- case "type": return elem.name.originalName;
937
- default: assertUnreachable(kind);
938
- }
939
- }
940
1075
  function emitDirective(e, ctx) {
941
1076
  const { directive } = e;
942
1077
  const { kind } = directive;
@@ -958,14 +1093,351 @@ function displayName(declIdent) {
958
1093
  }
959
1094
  return declIdent.mangledName || declIdent.originalName;
960
1095
  }
961
- /** Trace through refersTo links until we find the declaration. */
962
- function findDecl(ident) {
963
- let i = ident;
964
- do {
965
- if (i.kind === "decl") return i;
966
- i = i.refersTo;
967
- } while (i);
968
- throw new Error(`unresolved identifer: ${ident.originalName}`);
1096
+ /** Leading comments on an inline node: block comments get a trailing space, line
1097
+ * comments force a newline so the following code is not swallowed. */
1098
+ function emitInlineLeading(e, ctx) {
1099
+ for (const c of e.commentsBefore ?? []) {
1100
+ emitComment(c, ctx);
1101
+ if (c.style === "line") newLine(ctx);
1102
+ else ctx.srcBuilder.appendNext(" ");
1103
+ }
1104
+ }
1105
+ function emitExpressionCore(e, ctx) {
1106
+ const builder = ctx.srcBuilder;
1107
+ switch (e.kind) {
1108
+ case "literal":
1109
+ builder.add(e.value, e.start, e.end);
1110
+ return;
1111
+ case "ref":
1112
+ emitRefIdent(e, ctx);
1113
+ return;
1114
+ case "type":
1115
+ emitTypeRef(e, ctx);
1116
+ return;
1117
+ case "binary-expression": {
1118
+ const [start, end] = e.operator.span;
1119
+ emitExpression(e.left, ctx);
1120
+ builder.add(` ${e.operator.value} `, start, end);
1121
+ emitExpression(e.right, ctx);
1122
+ return;
1123
+ }
1124
+ case "unary-expression": {
1125
+ const { value, start, end } = e.operator;
1126
+ builder.add(value, start, end);
1127
+ emitExpression(e.expression, ctx);
1128
+ return;
1129
+ }
1130
+ case "parenthesized-expression":
1131
+ builder.appendNext("(");
1132
+ emitExpression(e.expression, ctx);
1133
+ builder.appendNext(")");
1134
+ return;
1135
+ case "call-expression":
1136
+ emitExpression(e.function, ctx);
1137
+ if (e.templateArgs) emitTemplateArgs(e.templateArgs, ctx);
1138
+ builder.appendNext("(");
1139
+ e.arguments.forEach((arg, i) => {
1140
+ if (i > 0) builder.appendNext(", ");
1141
+ emitExpression(arg, ctx);
1142
+ });
1143
+ builder.appendNext(")");
1144
+ return;
1145
+ case "component-expression":
1146
+ emitExpression(e.base, ctx);
1147
+ builder.appendNext("[");
1148
+ emitExpression(e.access, ctx);
1149
+ builder.appendNext("]");
1150
+ return;
1151
+ case "component-member-expression":
1152
+ emitExpression(e.base, ctx);
1153
+ builder.add("." + e.access.name, e.access.start, e.access.end);
1154
+ return;
1155
+ default: assertUnreachable(e);
1156
+ }
1157
+ }
1158
+ /** Trailing comments on an inline node: a leading space, plus a newline after a
1159
+ * line comment so following code stays off its line. */
1160
+ function emitInlineTrailing(e, ctx) {
1161
+ for (const c of e.commentsAfter ?? []) {
1162
+ ctx.srcBuilder.appendNext(" ");
1163
+ emitComment(c, ctx);
1164
+ if (c.style === "line") newLine(ctx);
1165
+ }
1166
+ }
1167
+ /** Leading comments: each on its own indented line above the element. */
1168
+ function emitLeadingComments(e, ctx) {
1169
+ if (!e.commentsBefore) return;
1170
+ for (const c of e.commentsBefore) {
1171
+ if (c.blankBefore) ctx.srcBuilder.addNl();
1172
+ newLine(ctx);
1173
+ emitComment(c, ctx);
1174
+ }
1175
+ }
1176
+ /** Start a fresh line at the current indent. */
1177
+ function newLine(ctx) {
1178
+ ctx.srcBuilder.addNl();
1179
+ if (ctx.indent > 0) ctx.srcBuilder.appendNext(" ".repeat(ctx.indent));
1180
+ }
1181
+ /** Emit a `{ ... }` block, one statement per indented line. A block with no
1182
+ * statements collapses to `{ }` unless it holds dangling inner comments. */
1183
+ function emitBlock(e, ctx) {
1184
+ emitAttributes(e.attributes, ctx);
1185
+ const stmts = filterValidElements(e.body, ctx.conditions);
1186
+ if (stmts.length === 0 && !e.innerComments?.length) {
1187
+ ctx.srcBuilder.appendNext("{ }");
1188
+ return;
1189
+ }
1190
+ ctx.srcBuilder.appendNext("{");
1191
+ const inner = childIndent(ctx);
1192
+ for (const comment of e.innerComments ?? []) {
1193
+ newLine(inner);
1194
+ emitComment(comment, inner);
1195
+ }
1196
+ for (const stmt of stmts) emitStatement(stmt, inner);
1197
+ newLine(ctx);
1198
+ ctx.srcBuilder.appendNext("}");
1199
+ }
1200
+ /** Emit a comma-separated template argument list: <a, b, c>. */
1201
+ function emitTemplateArgs(args, ctx) {
1202
+ ctx.srcBuilder.appendNext("<");
1203
+ args.forEach((a, i) => {
1204
+ if (i > 0) ctx.srcBuilder.appendNext(", ");
1205
+ emitExpression(a, ctx);
1206
+ });
1207
+ ctx.srcBuilder.appendNext(">");
1208
+ }
1209
+ /** Emit a statement's syntax followed by ';' unless its kind takes none. */
1210
+ function emitCoreSemi(stmt, ctx) {
1211
+ emitStatementCore(stmt, ctx);
1212
+ if (!noSemicolon.has(stmt.kind)) ctx.srcBuilder.appendNext(";");
1213
+ }
1214
+ /** Emit a declaration from its typed fields, including its trailing ';':
1215
+ * `[attrs] var<...> name: T = init;`, `const name = init;`, `override n;`,
1216
+ * `alias name = T;`, `const_assert expr;`. */
1217
+ function emitValueDecl(e, ctx) {
1218
+ emitAttributes(e.attributes, ctx);
1219
+ const builder = ctx.srcBuilder;
1220
+ switch (e.kind) {
1221
+ case "var":
1222
+ case "gvar":
1223
+ builder.appendNext("var");
1224
+ if (e.template) emitVarTemplate(e.template, ctx);
1225
+ builder.appendNext(" ");
1226
+ emitTypedDecl(e.name, ctx);
1227
+ emitInit(e.init, ctx);
1228
+ break;
1229
+ case "let":
1230
+ case "const":
1231
+ case "override":
1232
+ builder.appendNext(`${e.kind} `);
1233
+ emitTypedDecl(e.name, ctx);
1234
+ emitInit(e.init, ctx);
1235
+ break;
1236
+ case "alias":
1237
+ builder.appendNext("alias ");
1238
+ emitInlineLeading(e.name, ctx);
1239
+ emitDeclIdent(e.name, ctx);
1240
+ emitInlineTrailing(e.name, ctx);
1241
+ builder.appendNext(" = ");
1242
+ emitTypeRef(e.typeRef, ctx);
1243
+ break;
1244
+ case "assert":
1245
+ builder.appendNext("const_assert ");
1246
+ emitExpression(e.expression, ctx);
1247
+ break;
1248
+ default: assertUnreachable(e);
1249
+ }
1250
+ builder.appendNext(";");
1251
+ }
1252
+ function emitComment(c, ctx) {
1253
+ ctx.srcBuilder.add(c.srcModule.src.slice(c.start, c.end), c.start, c.end);
1254
+ }
1255
+ function warnEmptyStruct(e) {
1256
+ const { name, members } = e;
1257
+ const condStr = members.length ? "(with current conditions)" : "";
1258
+ failIdentElem(name, `struct '${name.ident.originalName}' has no members ${condStr}`);
1259
+ }
1260
+ /** Whether an element carries any attached comments. */
1261
+ function hasComments(e) {
1262
+ return !!(e.commentsBefore?.length || e.commentsAfter?.length);
1263
+ }
1264
+ /** Emit one struct member on its own line: leading comments above, `name: type,`,
1265
+ * then trailing comments inline. */
1266
+ function emitMemberLine(m, ctx) {
1267
+ const builder = ctx.srcBuilder;
1268
+ for (const c of m.commentsBefore ?? []) {
1269
+ builder.appendNext(" ");
1270
+ emitComment(c, ctx);
1271
+ builder.addNl();
1272
+ }
1273
+ builder.appendNext(" ");
1274
+ emitMember(m, ctx);
1275
+ builder.appendNext(",");
1276
+ for (const c of m.commentsAfter ?? []) {
1277
+ builder.appendNext(" ");
1278
+ emitComment(c, ctx);
1279
+ }
1280
+ builder.addNl();
1281
+ }
1282
+ function emitStandardAttribute(e, ctx) {
1283
+ if (e.attribute.kind !== "@attribute") return;
1284
+ const { params } = e.attribute;
1285
+ if (!params || params.length === 0) {
1286
+ ctx.srcBuilder.add("@" + e.attribute.name, e.start, e.end);
1287
+ return;
1288
+ }
1289
+ ctx.srcBuilder.add("@" + e.attribute.name + "(", e.start, params[0].start);
1290
+ params.forEach((param, i) => {
1291
+ if (i > 0) ctx.srcBuilder.appendNext(", ");
1292
+ emitExpression(param.expression, ctx);
1293
+ });
1294
+ ctx.srcBuilder.add(")", params[params.length - 1].end, e.end);
1295
+ }
1296
+ /** A child context indented one level deeper. */
1297
+ function childIndent(ctx) {
1298
+ return {
1299
+ ...ctx,
1300
+ indent: ctx.indent + 1
1301
+ };
1302
+ }
1303
+ /** Emit a statement's syntax, without surrounding line breaks, ';', or comments. */
1304
+ function emitStatementCore(stmt, ctx) {
1305
+ if (stmt.kind === "var" || stmt.kind === "let" || stmt.kind === "const" || stmt.kind === "assert") {
1306
+ emitValueDecl(stmt, ctx);
1307
+ return;
1308
+ }
1309
+ if (stmt.kind !== "block") emitAttributes(stmt.attributes, ctx);
1310
+ const builder = ctx.srcBuilder;
1311
+ switch (stmt.kind) {
1312
+ case "block":
1313
+ emitBlock(stmt, ctx);
1314
+ return;
1315
+ case "if":
1316
+ emitIf(stmt, ctx);
1317
+ return;
1318
+ case "for":
1319
+ emitFor(stmt, ctx);
1320
+ return;
1321
+ case "while":
1322
+ emitWhile(stmt, ctx);
1323
+ return;
1324
+ case "loop":
1325
+ builder.appendNext("loop ");
1326
+ emitBlock(stmt.body, ctx);
1327
+ return;
1328
+ case "continuing":
1329
+ builder.appendNext("continuing ");
1330
+ emitBlock(stmt.body, ctx);
1331
+ return;
1332
+ case "switch":
1333
+ emitSwitch(stmt, ctx);
1334
+ return;
1335
+ case "return":
1336
+ builder.appendNext("return");
1337
+ if (stmt.value) {
1338
+ builder.appendNext(" ");
1339
+ emitExpression(stmt.value, ctx);
1340
+ }
1341
+ return;
1342
+ case "break":
1343
+ builder.appendNext("break");
1344
+ if (stmt.condition) {
1345
+ builder.appendNext(" if ");
1346
+ emitExpression(stmt.condition, ctx);
1347
+ }
1348
+ return;
1349
+ case "continue":
1350
+ builder.appendNext("continue");
1351
+ return;
1352
+ case "discard":
1353
+ builder.appendNext("discard");
1354
+ return;
1355
+ case "assign":
1356
+ emitAssign(stmt, ctx);
1357
+ return;
1358
+ case "increment":
1359
+ emitExpression(stmt.target, ctx);
1360
+ builder.appendNext("++");
1361
+ return;
1362
+ case "decrement":
1363
+ emitExpression(stmt.target, ctx);
1364
+ builder.appendNext("--");
1365
+ return;
1366
+ case "call":
1367
+ emitExpression(stmt.call, ctx);
1368
+ return;
1369
+ case "empty": return;
1370
+ default: assertUnreachable(stmt);
1371
+ }
1372
+ }
1373
+ /** Emit a var's `<address_space[, access_mode]>` enumerant template. */
1374
+ function emitVarTemplate(template, ctx) {
1375
+ const builder = ctx.srcBuilder;
1376
+ builder.appendNext("<");
1377
+ template.forEach((name, i) => {
1378
+ if (i > 0) builder.appendNext(", ");
1379
+ emitName(name, ctx);
1380
+ });
1381
+ builder.appendNext(">");
1382
+ }
1383
+ /** Emit a ` = init` clause, if present. */
1384
+ function emitInit(init, ctx) {
1385
+ if (!init) return;
1386
+ ctx.srcBuilder.appendNext(" = ");
1387
+ emitExpression(init, ctx);
1388
+ }
1389
+ /** if / else-if / else: a nested IfElem prints as `else if`, a BlockElem as `else`. */
1390
+ function emitIf(e, ctx) {
1391
+ const builder = ctx.srcBuilder;
1392
+ builder.appendNext("if ");
1393
+ emitExpression(e.condition, ctx);
1394
+ builder.appendNext(" ");
1395
+ emitBlock(e.body, ctx);
1396
+ if (e.else) {
1397
+ builder.appendNext(" else ");
1398
+ if (e.else.kind === "if") emitIf(e.else, ctx);
1399
+ else emitBlock(e.else, ctx);
1400
+ }
1401
+ }
1402
+ /** for (init; condition; update) { ... }. The init takes a ';' like any
1403
+ * statement; the update is the last clause before ')', so it gets none. */
1404
+ function emitFor(e, ctx) {
1405
+ const builder = ctx.srcBuilder;
1406
+ builder.appendNext("for (");
1407
+ if (e.init) emitCoreSemi(e.init, ctx);
1408
+ else builder.appendNext(";");
1409
+ builder.appendNext(" ");
1410
+ if (e.condition) emitExpression(e.condition, ctx);
1411
+ builder.appendNext("; ");
1412
+ if (e.update) emitStatementCore(e.update, ctx);
1413
+ builder.appendNext(") ");
1414
+ emitBlock(e.body, ctx);
1415
+ }
1416
+ function emitWhile(e, ctx) {
1417
+ ctx.srcBuilder.appendNext("while ");
1418
+ emitExpression(e.condition, ctx);
1419
+ ctx.srcBuilder.appendNext(" ");
1420
+ emitBlock(e.body, ctx);
1421
+ }
1422
+ function emitSwitch(e, ctx) {
1423
+ const builder = ctx.srcBuilder;
1424
+ builder.appendNext("switch ");
1425
+ emitExpression(e.selector, ctx);
1426
+ builder.appendNext(" ");
1427
+ emitAttributes(e.bodyAttributes, ctx);
1428
+ builder.appendNext("{");
1429
+ const inner = childIndent(ctx);
1430
+ for (const clause of filterValidElements(e.clauses, ctx.conditions)) emitSwitchClause(clause, inner);
1431
+ newLine(ctx);
1432
+ builder.appendNext("}");
1433
+ }
1434
+ /** lhs op rhs, with the phony target printed as `_`. */
1435
+ function emitAssign(e, ctx) {
1436
+ if (e.lhs.kind === "phony") ctx.srcBuilder.add("_", e.lhs.span[0], e.lhs.span[1]);
1437
+ else emitExpression(e.lhs, ctx);
1438
+ const [start, end] = e.op.span;
1439
+ ctx.srcBuilder.add(` ${e.op.value} `, start, end);
1440
+ emitExpression(e.rhs, ctx);
969
1441
  }
970
1442
  //#endregion
971
1443
  //#region src/debug/ImportToString.ts
@@ -1050,19 +1522,25 @@ function astToString(elem, indent = 0) {
1050
1522
  const str = new LineWrapper(indent, maxLineLength);
1051
1523
  str.add(kind);
1052
1524
  addElemFields(elem, str);
1053
- let childStrings = [];
1054
- if ("contents" in elem) childStrings = elem.contents.map((e) => astToString(e, indent + 2));
1055
- if (childStrings.length) {
1525
+ addCommentFields(elem, str);
1526
+ const children = childElems(elem);
1527
+ if (children.length) {
1056
1528
  str.nl();
1529
+ const childStrings = children.map((e) => astToString(e, indent + 2));
1057
1530
  str.addBlock(childStrings.join("\n"), false);
1058
1531
  }
1059
1532
  return str.result;
1060
1533
  }
1534
+ /** @return string representation of an attribute (for test/debug) */
1535
+ function attributeToString(attr) {
1536
+ const str = new LineWrapper(0, maxLineLength);
1537
+ addAttributeFields(attr, str);
1538
+ return str.result;
1539
+ }
1061
1540
  function addElemFields(elem, str) {
1062
1541
  const { kind } = elem;
1063
- if (kind === "assert" || kind === "module" || kind === "param" || kind === "stuff" || kind === "switch-clause") return;
1064
- if (kind === "text") str.add(` '${elem.srcModule.src.slice(elem.start, elem.end)}'`);
1065
- else if (kind === "var" || kind === "let" || kind === "gvar" || kind === "const" || kind === "override") {
1542
+ if (kind === "assert" || kind === "expression" || kind === "module" || kind === "param" || kind === "switch-clause") return;
1543
+ if (kind === "var" || kind === "let" || kind === "gvar" || kind === "const" || kind === "override") {
1066
1544
  addTypedDeclIdent(elem.name, str);
1067
1545
  listAttributeElems(elem.attributes, str);
1068
1546
  } else if (kind === "struct") str.add(" " + elem.name.ident.originalName);
@@ -1070,22 +1548,14 @@ function addElemFields(elem, str) {
1070
1548
  listAttributeElems(elem.attributes, str);
1071
1549
  str.add(` ${elem.name.name}: ${typeRefElemToString(elem.typeRef)}`);
1072
1550
  } else if (kind === "name") str.add(" " + elem.name);
1073
- else if (kind === "memberRef") {
1074
- const extra = elem.extraComponents ? debugContentsToString(elem.extraComponents) : "";
1075
- str.add(` ${elem.name.ident.originalName}.${elem.member.name}${extra}`);
1076
- } else if (kind === "fn") addFnFields(elem, str);
1551
+ else if (kind === "fn") addFnFields(elem, str);
1552
+ else if (kind === "do") addDoFields(elem, str);
1077
1553
  else if (kind === "alias") {
1078
1554
  const prefix = elem.name.ident.kind === "decl" ? "%" : "";
1079
1555
  str.add(` ${prefix}${elem.name.ident.originalName}=${typeRefElemToString(elem.typeRef)}`);
1080
1556
  } else if (kind === "attribute") addAttributeFields(elem.attribute, str);
1081
- else if (kind === "expression") {
1082
- const contents = elem.contents.map((e) => e.kind === "text" ? `'${e.srcModule.src.slice(e.start, e.end)}'` : astToString(e)).join(" ");
1083
- str.add(" " + contents);
1084
- } else if (kind === "type") {
1085
- const nameStr = typeof elem.name === "string" ? elem.name : elem.name.originalName;
1086
- const params = elem.templateParams?.map(templateParamToString).join(", ");
1087
- str.add(params ? ` ${nameStr}<${params}>` : ` ${nameStr}`);
1088
- } else if (kind === "synthetic") str.add(` '${elem.text}'`);
1557
+ else if (kind === "type") str.add(" " + typeRefElemToString(elem));
1558
+ else if (kind === "synthetic") str.add(` '${elem.text}'`);
1089
1559
  else if (kind === "import") {
1090
1560
  str.add(" " + importToString(elem.imports));
1091
1561
  listAttributeElems(elem.attributes, str);
@@ -1093,7 +1563,7 @@ function addElemFields(elem, str) {
1093
1563
  else if (kind === "typeDecl") addTypedDeclIdent(elem, str);
1094
1564
  else if (kind === "decl") str.add(" %" + elem.ident.originalName);
1095
1565
  else if (kind === "directive") addDirective(elem, str);
1096
- else if (kind === "statement" || kind === "continuing") listAttributeElems(elem.attributes, str);
1566
+ else if (kind === "block" || kind === "if" || kind === "for" || kind === "while" || kind === "loop" || kind === "continuing" || kind === "switch" || kind === "return" || kind === "break" || kind === "continue" || kind === "discard" || kind === "assign" || kind === "increment" || kind === "decrement" || kind === "call" || kind === "empty") listAttributeElems(elem.attributes, str);
1097
1567
  else if (kind === "literal") str.add(` literal(${elem.value})`);
1098
1568
  else if (kind === "binary-expression") str.add(` binop(${elem.operator.value})`);
1099
1569
  else if (kind === "unary-expression") str.add(` unop(${elem.operator.value})`);
@@ -1103,6 +1573,14 @@ function addElemFields(elem, str) {
1103
1573
  else if (kind === "component-member-expression") str.add(" .");
1104
1574
  else assertUnreachable(kind);
1105
1575
  }
1576
+ /** Show attached leading/trailing comments to verify comment attachment. */
1577
+ function addCommentFields(elem, str) {
1578
+ if (!("start" in elem)) return;
1579
+ const { commentsBefore, commentsAfter } = elem;
1580
+ if (commentsBefore?.length) str.add(commentsToString("before", commentsBefore));
1581
+ if (commentsAfter?.length) str.add(commentsToString("after", commentsAfter));
1582
+ if ("innerComments" in elem && elem.innerComments?.length) str.add(commentsToString("inner", elem.innerComments));
1583
+ }
1106
1584
  function addAttributeFields(attr, str) {
1107
1585
  const { kind } = attr;
1108
1586
  if (kind === "@attribute") {
@@ -1116,35 +1594,33 @@ function addAttributeFields(attr, str) {
1116
1594
  else if (kind === "@interpolate") str.add(` @interpolate(${attr.params.map((v) => v.name).join(", ")})`);
1117
1595
  else assertUnreachable(kind);
1118
1596
  }
1119
- /** @return string representation of an attribute (for test/debug) */
1120
- function attributeToString(attr) {
1121
- const str = new LineWrapper(0, maxLineLength);
1122
- addAttributeFields(attr, str);
1123
- return str.result;
1124
- }
1125
1597
  function addTypedDeclIdent(elem, str) {
1126
1598
  const { decl, typeRef } = elem;
1127
1599
  str.add(" %" + decl.ident.originalName);
1128
1600
  if (typeRef) str.add(" : " + typeRefElemToString(typeRef));
1129
1601
  }
1130
- function addFnFields(elem, str) {
1131
- const { name, params, returnType, attributes } = elem;
1132
- const paramStrs = params.map((p) => {
1133
- const { originalName } = p.name.decl.ident;
1134
- return `${originalName}: ${typeRefElemToString(p.name.typeRef)}`;
1135
- });
1136
- str.add(` ${name.ident.originalName}(${paramStrs.join(", ")})`);
1137
- listAttributeElems(attributes, str);
1138
- if (returnType) str.add(" -> " + typeRefElemToString(returnType));
1139
- }
1140
1602
  /** show attribute names in short form to verify collection */
1141
1603
  function listAttributeElems(attrs, str) {
1142
1604
  attrs?.forEach((a) => {
1143
1605
  str.add(" " + attributeName(a.attribute));
1144
1606
  });
1145
1607
  }
1146
- function attributeName(attr) {
1147
- return attr.kind === "@attribute" ? "@" + attr.name : attr.kind;
1608
+ function typeRefElemToString(elem) {
1609
+ if (!elem) return "?type?";
1610
+ const nameStr = typeof elem.name === "string" ? elem.name : elem.name.originalName;
1611
+ const params = elem.templateParams?.map(templateParamToString).join(", ");
1612
+ return params ? `${nameStr}<${params}>` : nameStr;
1613
+ }
1614
+ function addFnFields(elem, str) {
1615
+ const { name, params, returnType, attributes } = elem;
1616
+ str.add(` ${name.ident.originalName}(${paramListToString(params)})`);
1617
+ listAttributeElems(attributes, str);
1618
+ if (returnType) str.add(" -> " + typeRefElemToString(returnType));
1619
+ }
1620
+ function addDoFields(elem, str) {
1621
+ const { name, params, attributes } = elem;
1622
+ str.add(` ${name.name}(${paramListToString(params)})`);
1623
+ listAttributeElems(attributes, str);
1148
1624
  }
1149
1625
  function addDirective(elem, str) {
1150
1626
  const { directive, attributes } = elem;
@@ -1157,30 +1633,29 @@ function addDirective(elem, str) {
1157
1633
  else assertUnreachable(kind);
1158
1634
  listAttributeElems(attributes, str);
1159
1635
  }
1636
+ function commentsToString(label, comments) {
1637
+ return ` ${label}[${comments.map((c) => {
1638
+ const text = c.srcModule.src.slice(c.start, c.end);
1639
+ return c.blankBefore ? `'${text}'(blank)` : `'${text}'`;
1640
+ }).join(", ")}]`;
1641
+ }
1160
1642
  function unknownExpressionToString(elem) {
1161
- if (!("contents" in elem)) return astToString(elem);
1162
- return elem.contents.map((e) => {
1163
- if (e.kind === "text") return `'${e.srcModule.src.slice(e.start, e.end)}'`;
1164
- return astToString(e);
1165
- }).join(" ");
1643
+ return astToString(elem.expression);
1644
+ }
1645
+ function attributeName(attr) {
1646
+ return attr.kind === "@attribute" ? "@" + attr.name : attr.kind;
1166
1647
  }
1167
1648
  function templateParamToString(p) {
1168
1649
  if (typeof p === "string") return p;
1169
1650
  if (p.kind === "type") return typeRefElemToString(p);
1170
- return astToString(p);
1171
- }
1172
- function typeRefElemToString(elem) {
1173
- if (!elem) return "?type?";
1174
- const nameStr = typeof elem.name === "string" ? elem.name : elem.name.originalName;
1175
- const params = elem.templateParams?.map(templateParamToString).join(", ");
1176
- return params ? `${nameStr}<${params}>` : nameStr;
1651
+ return expressionToString(p);
1177
1652
  }
1178
- function debugContentsToString(elem) {
1179
- return elem.contents.map((c) => {
1180
- if (c.kind === "text") return c.srcModule.src.slice(c.start, c.end);
1181
- if (c.kind === "ref") return c.ident.originalName;
1182
- return `?${c.kind}?`;
1183
- }).join(" ");
1653
+ /** @return "name: type, ..." for a fn/do parameter list. */
1654
+ function paramListToString(params) {
1655
+ return params.map((p) => {
1656
+ const { originalName } = p.name.decl.ident;
1657
+ return `${originalName}: ${typeRefElemToString(p.name.typeRef)}`;
1658
+ }).join(", ");
1184
1659
  }
1185
1660
  //#endregion
1186
1661
  //#region src/debug/ScopeToString.ts
@@ -1257,7 +1732,7 @@ function liveDeclsToString(liveDecls) {
1257
1732
  /**
1258
1733
  * Construct a globally unique name based on the declaration
1259
1734
  * module path separated by underscores.
1260
- * Corresponds to "Underscore-count mangling" from [NameMangling.md](https://github.com/wgsl-tooling-wg/wesl-spec/blob/main/NameMangling.md)
1735
+ * Corresponds to "Underscore-count mangling" from [NameMangling.md](https://github.com/webgpu-tools/wesl-spec/blob/main/NameMangling.md)
1261
1736
  */
1262
1737
  function underscoreMangle(decl, srcModule) {
1263
1738
  const { modulePath } = srcModule;
@@ -1363,53 +1838,184 @@ var ParseError = class extends Error {
1363
1838
  }
1364
1839
  };
1365
1840
  //#endregion
1366
- //#region src/parse/ContentsHelpers.ts
1367
- /** Push partial element onto stack for content collection. */
1368
- function beginElem(ctx, kind, contents = []) {
1369
- ctx.state.context.openElems.push({
1370
- kind,
1371
- contents: [...contents]
1372
- });
1373
- }
1374
- /** Pop element from stack, fill gaps with TextElems, return contents. */
1375
- function finishContents(ctx, start, end) {
1376
- const open = ctx.state.context.openElems.pop();
1377
- if (!open) throw new Error("No open element to close");
1378
- return coverWithText(ctx, open.contents, start, end);
1379
- }
1380
- /** Finish element: get end position, close contents, return complete element. */
1381
- function finishElem(kind, start, ctx, params) {
1382
- const end = ctx.stream.checkpoint();
1383
- return {
1384
- kind,
1385
- start,
1386
- end,
1387
- contents: finishContents(ctx, start, end),
1388
- ...params
1389
- };
1390
- }
1391
- /** Create a TextElem */
1392
- function makeText(srcModule, start, end) {
1393
- return {
1394
- kind: "text",
1841
+ //#region src/parse/AttachComments.ts
1842
+ const tab = 9;
1843
+ const lineFeed = 10;
1844
+ const verticalTab = 11;
1845
+ const formFeed = 12;
1846
+ const carriageReturn = 13;
1847
+ const space = 32;
1848
+ const nextLine = 133;
1849
+ const leftToRightMark = 8206;
1850
+ const rightToLeftMark = 8207;
1851
+ const lineSeparator = 8232;
1852
+ const paragraphSeparator = 8233;
1853
+ /**
1854
+ * Attach every recorded comment to a node in the parsed tree.
1855
+ *
1856
+ * Each comment run (a contiguous group of comments between two real tokens) is
1857
+ * anchored to the deepest node that contains it in a gap between children, then
1858
+ * distributed to the surrounding children:
1859
+ * - a comment that begins its own line leads the next child (`commentsBefore`);
1860
+ * - an inline comment trails the previous child (`commentsAfter`), unless it
1861
+ * hugs the next child -- only blank space and comments, no separator token,
1862
+ * between them -- in which case it leads the next child instead;
1863
+ * - comments before a closing token with no following child trail the last child,
1864
+ * or land in an empty block's `innerComments`.
1865
+ *
1866
+ * Descending into expressions (via {@link childElems}) means interior comments
1867
+ * like the one in `foo(1, /* x *\/ 2)` are preserved on the `2`, not dropped.
1868
+ */
1869
+ function attachComments(ctx, moduleElem) {
1870
+ const { srcModule } = ctx.state.stable;
1871
+ const cache = /* @__PURE__ */ new Map();
1872
+ for (const run of ctx.stream.commentRuns()) distribute(deepestContaining(moduleElem, run[0].start, runEnd(run), cache), run, srcModule, cache);
1873
+ }
1874
+ /** The deepest node whose span contains the whole [start, end) range, found by
1875
+ * descending into the child that brackets it. Comments live in gaps between
1876
+ * tokens, so the result is the node holding the gap, with the run between two
1877
+ * of its children. */
1878
+ function deepestContaining(root, start, end, cache) {
1879
+ let node = root;
1880
+ while (true) {
1881
+ const child = positionedChildren(node, cache).find((c) => c.start <= start && end <= c.end);
1882
+ if (!child) return node;
1883
+ node = child;
1884
+ }
1885
+ }
1886
+ function runEnd(run) {
1887
+ return run[run.length - 1].end;
1888
+ }
1889
+ /** Split a comment run between the previous child (trailing) and the next child
1890
+ * (leading) of its anchor, or onto the last child / inner comments when it
1891
+ * dangles before a closing token. */
1892
+ function distribute(anchor, run, srcModule, cache) {
1893
+ const children = positionedChildren(anchor, cache);
1894
+ const start = run[0].start;
1895
+ const end = runEnd(run);
1896
+ const prev = children.findLast((c) => c.end <= start);
1897
+ const next = children.find((c) => c.start >= end);
1898
+ if (!next) {
1899
+ if (prev) addComments(prev, "commentsAfter", run, srcModule);
1900
+ else if (anchor.kind === "block") anchor.innerComments = run.map((t) => makeComment(t, srcModule));
1901
+ else addComments(anchor, "commentsBefore", run, srcModule);
1902
+ return;
1903
+ }
1904
+ const split = splitPoint(prev, next, run, srcModule.src);
1905
+ if (prev && split > 0) addComments(prev, "commentsAfter", run.slice(0, split), srcModule);
1906
+ if (split < run.length) addComments(next, "commentsBefore", run.slice(split), srcModule);
1907
+ }
1908
+ function positionedChildren(node, cache) {
1909
+ let children = cache.get(node);
1910
+ if (children === void 0) {
1911
+ children = positioned(childElems(node));
1912
+ cache.set(node, children);
1913
+ }
1914
+ return children;
1915
+ }
1916
+ /** Append converted comments to an element's leading or trailing list. */
1917
+ function addComments(elem, field, trivia, srcModule) {
1918
+ const comments = trivia.map((t) => makeComment(t, srcModule));
1919
+ const existing = elem[field];
1920
+ elem[field] = existing ? [...existing, ...comments] : comments;
1921
+ }
1922
+ function makeComment(trivia, srcModule) {
1923
+ const { style, start, end } = trivia;
1924
+ const comment = {
1925
+ kind: "comment",
1926
+ style,
1395
1927
  start,
1396
1928
  end,
1397
1929
  srcModule
1398
1930
  };
1931
+ if (lineBreaksBefore(srcModule.src, start) >= 2) comment.blankBefore = true;
1932
+ return comment;
1399
1933
  }
1400
- /** Fill gaps between child elements with TextElems. */
1401
- function coverWithText(ctx, contents, start, end) {
1402
- const { srcModule } = ctx.state.stable;
1403
- const sorted = contents.slice().sort((a, b) => a.start - b.start);
1404
- const elems = [];
1405
- let pos = start;
1406
- for (const elem of sorted) {
1407
- if (pos < elem.start) elems.push(makeText(srcModule, pos, elem.start));
1408
- elems.push(elem);
1409
- pos = elem.end;
1410
- }
1411
- if (pos < end) elems.push(makeText(srcModule, pos, end));
1412
- return elems;
1934
+ /**
1935
+ * Index into `run` where it flips from trailing `prev` to leading `next`:
1936
+ * caller attaches run[0, split) to `prev` and run[split, end) to `next`.
1937
+ *
1938
+ * The aim is to keep each comment with the code it describes, so it stays
1939
+ * meaningful after the tree is reordered or reformatted. That follows how people
1940
+ * write comments: a comment on its own line documents what comes after it, while
1941
+ * a comment sharing a line with code documents that code. Hence:
1942
+ * - with no previous child the whole run leads (split 0);
1943
+ * - otherwise the split is the first comment that begins its own line;
1944
+ * - failing that (an all-inline run), it is the start of the suffix that hugs
1945
+ * `next` on the same line, so the comment in `foo(1, /* x *\/ 2)` documents,
1946
+ * and lands on, the `2`.
1947
+ */
1948
+ function splitPoint(prev, next, run, src) {
1949
+ if (!prev) return 0;
1950
+ const ownLine = firstOwnLine(run, src);
1951
+ return ownLine >= 0 ? ownLine : hugsNextStart(run, next.start, src);
1952
+ }
1953
+ /** Source-positioned children, sorted in source order. Synthetic elems (no
1954
+ * source position) cannot anchor comments and are dropped. Children usually
1955
+ * arrive already in source order, so the sort is skipped when possible.
1956
+ * Filtering and order-checking share one pass: this runs per node, so it's
1957
+ * kept deliberately lean. */
1958
+ function positioned(elems) {
1959
+ const result = [];
1960
+ let sorted = true;
1961
+ let lastStart = -1;
1962
+ for (const e of elems) {
1963
+ if (e.kind === "synthetic") continue;
1964
+ if (e.start < lastStart) sorted = false;
1965
+ lastStart = e.start;
1966
+ result.push(e);
1967
+ }
1968
+ if (!sorted) result.sort((a, b) => a.start - b.start);
1969
+ return result;
1970
+ }
1971
+ /** Count line breaks in the whitespace run immediately before `pos`, capped at 2:
1972
+ * callers only need none / one line break / a blank line. Scans backward over
1973
+ * blankspace, stopping at the first non-blankspace char, so cost is the gap
1974
+ * length, not the source length. `\r\n` counts as one break. */
1975
+ function lineBreaksBefore(src, pos) {
1976
+ let count = 0;
1977
+ for (let i = pos - 1; i >= 0; i--) {
1978
+ const c = src.charCodeAt(i);
1979
+ if (isLineBreak(c)) {
1980
+ if (c === lineFeed && src.charCodeAt(i - 1) === carriageReturn) i--;
1981
+ } else if (isInlineSpace(c)) continue;
1982
+ else break;
1983
+ if (++count >= 2) return 2;
1984
+ }
1985
+ return count;
1986
+ }
1987
+ /** Index of the first comment that begins its own line (after a line break),
1988
+ * or -1 if none do. */
1989
+ function firstOwnLine(run, src) {
1990
+ return run.findIndex((t) => lineBreaksBefore(src, t.start) >= 1);
1991
+ }
1992
+ /** Start index of the trailing suffix of `run` that is joined to `next` by
1993
+ * same-line whitespace only (no line break, no separator token between them).
1994
+ * Returns run.length when nothing hugs `next`. */
1995
+ function hugsNextStart(run, nextStart, src) {
1996
+ let suffixStart = run.length;
1997
+ let rightStart = nextStart;
1998
+ for (let i = run.length - 1; i >= 0; i--) {
1999
+ const comment = run[i];
2000
+ if (!sameLineGap(src, comment.end, rightStart)) break;
2001
+ suffixStart = i;
2002
+ rightStart = comment.start;
2003
+ }
2004
+ return suffixStart;
2005
+ }
2006
+ /** A WGSL line break code point. `\r\n` is two of these; callers coalesce it. */
2007
+ function isLineBreak(c) {
2008
+ return c === lineFeed || c === carriageReturn || c === verticalTab || c === formFeed || c === nextLine || c === lineSeparator || c === paragraphSeparator;
2009
+ }
2010
+ /** WGSL blankspace that stays on the same line (not a line break). */
2011
+ function isInlineSpace(c) {
2012
+ return c === space || c === tab || c === leftToRightMark || c === rightToLeftMark;
2013
+ }
2014
+ /** True when [from, to) is inline blankspace only (no line break): the two ends
2015
+ * sit on the same line with nothing but same-line spaces between. */
2016
+ function sameLineGap(src, from, to) {
2017
+ for (let i = from; i < to; i++) if (!isInlineSpace(src.charCodeAt(i))) return false;
2018
+ return true;
1413
2019
  }
1414
2020
  //#endregion
1415
2021
  //#region src/parse/ExpressionUtil.ts
@@ -1423,9 +2029,11 @@ function makeLiteral(token) {
1423
2029
  };
1424
2030
  }
1425
2031
  function makeUnaryOperator(token) {
2032
+ const [start, end] = token.span;
1426
2033
  return {
1427
2034
  value: token.text,
1428
- span: token.span
2035
+ start,
2036
+ end
1429
2037
  };
1430
2038
  }
1431
2039
  function makeBinaryOperator(token) {
@@ -1435,12 +2043,11 @@ function makeBinaryOperator(token) {
1435
2043
  };
1436
2044
  }
1437
2045
  function makeUnaryExpression(operator, expr) {
1438
- const [start] = operator.span;
1439
2046
  return {
1440
2047
  kind: "unary-expression",
1441
2048
  operator,
1442
2049
  expression: expr,
1443
- start,
2050
+ start: operator.start,
1444
2051
  end: expr.end
1445
2052
  };
1446
2053
  }
@@ -1501,13 +2108,16 @@ function expectWord(stream, errorMsg) {
1501
2108
  stream.nextToken();
1502
2109
  return token;
1503
2110
  }
1504
- /** Parse expression and throw ParseError if not found. */
2111
+ /** Parse a required expression, throwing if absent. */
1505
2112
  function expectExpression(ctx, errorMsg = "Expected expression") {
1506
2113
  const expr = parseExpression(ctx);
1507
2114
  if (!expr) throwParseError(ctx.stream, errorMsg);
1508
- if (ctx.options.preserveExpressions) ctx.addElem(expr);
1509
2115
  return expr;
1510
2116
  }
2117
+ /** Parse an optional expression. */
2118
+ function parseContentExpression(ctx, opts) {
2119
+ return parseExpression(ctx, opts);
2120
+ }
1511
2121
  /** Throw a ParseError at the current/next token position. */
1512
2122
  function throwParseError(stream, message) {
1513
2123
  const weslStream = stream;
@@ -1563,18 +2173,27 @@ function makeRefIdentElem(ctx, refIdent, start, end) {
1563
2173
  refIdent.refIdentElem = elem;
1564
2174
  return elem;
1565
2175
  }
1566
- function isConditionalAttribute$1(attr) {
2176
+ /** @return true if the attribute is a conditional (@if, @elif, @else) */
2177
+ function isConditionalAttribute(attr) {
1567
2178
  const { kind } = attr;
1568
2179
  return kind === "@if" || kind === "@elif" || kind === "@else";
1569
2180
  }
1570
2181
  /** @return true if any attribute is a conditional (@if, @elif, @else) */
1571
2182
  function hasConditionalAttribute(attributes) {
1572
- return attributes.some((attr) => isConditionalAttribute$1(attr.attribute));
2183
+ return attributes.some((attr) => isConditionalAttribute(attr.attribute));
2184
+ }
2185
+ /** The first conditional (@if, @elif, @else) attribute in the list, if any. */
2186
+ function conditionalAttribute(attributes) {
2187
+ return attributes.find((a) => isConditionalAttribute(a.attribute))?.attribute;
1573
2188
  }
1574
2189
  /** Attach non-empty attributes array to element. */
1575
2190
  function attachAttributes(elem, attributes) {
1576
2191
  if (attributes?.length) elem.attributes = attributes;
1577
2192
  }
2193
+ /** Normalize an empty attribute list to undefined (the elem's "no attributes"). */
2194
+ function attrsOrUndef(attrs) {
2195
+ return attrs.length ? attrs : void 0;
2196
+ }
1578
2197
  /** Link a DeclIdentElem's ident to its parent declaration. */
1579
2198
  function linkDeclIdentElem(declIdentElem, declElem) {
1580
2199
  declIdentElem.ident.declElem = declElem;
@@ -1717,10 +2336,7 @@ function parseIdent(ctx, conditionRef) {
1717
2336
  const ident = ctx.createRefIdent(parts.join("::"));
1718
2337
  if (conditionRef) ident.conditionRef = true;
1719
2338
  const refIdentElem = makeRefIdentElem(ctx, ident, start, end);
1720
- if (!conditionRef) {
1721
- ctx.saveIdent(ident);
1722
- ctx.addElem(refIdentElem);
1723
- }
2339
+ if (!conditionRef) ctx.saveIdent(ident);
1724
2340
  return refIdentElem;
1725
2341
  }
1726
2342
  /** Check if token is valid as a path segment (word or allowed keyword). */
@@ -1742,19 +2358,20 @@ function parseSimpleTypeRef(ctx) {
1742
2358
  if (!path) return null;
1743
2359
  const { parts, start, end: nameEnd } = path;
1744
2360
  const refIdent = ctx.createRefIdent(parts.join("::"));
1745
- beginElem(ctx, "type");
1746
- const refIdentElem = makeRefIdentElem(ctx, refIdent, start, nameEnd);
2361
+ makeRefIdentElem(ctx, refIdent, start, nameEnd);
1747
2362
  ctx.saveIdent(refIdent);
1748
- ctx.addElem(refIdentElem);
1749
- return finishElem("type", start, ctx, {
2363
+ return {
2364
+ kind: "type",
1750
2365
  name: refIdent,
1751
- templateParams: ctx.stream.nextTemplateStartToken() ? parseTemplateParams(ctx) : void 0
1752
- });
2366
+ templateParams: ctx.stream.nextTemplateStartToken() ? parseTemplateParams(ctx) : void 0,
2367
+ start,
2368
+ end: ctx.stream.checkpoint()
2369
+ };
1753
2370
  }
1754
2371
  /** Parse comma-separated template parameters until closing '>'. */
1755
2372
  function parseTemplateParams(ctx) {
1756
2373
  const { stream } = ctx;
1757
- if (consumeTemplateEnd(stream)) return [];
2374
+ if (consumeTemplateEnd(stream)) throwParseError(stream, "Empty template parameter list '<>'");
1758
2375
  const params = [parseTemplateParam(ctx)];
1759
2376
  while (stream.matchText(",")) params.push(parseTemplateParam(ctx));
1760
2377
  if (!consumeTemplateEnd(stream)) throwParseError(stream, "Expected '>' or ',' after template parameter");
@@ -1769,8 +2386,8 @@ function consumeTemplateEnd(stream) {
1769
2386
  /** Grammar: template_arg_expression : expression */
1770
2387
  function parseTemplateParam(ctx) {
1771
2388
  const expr = parseExpression(ctx, { inTemplate: true });
1772
- if (expr) return expr;
1773
- throwParseError(ctx.stream, "Expected expression in template parameters");
2389
+ if (!expr) throwParseError(ctx.stream, "Expected expression in template parameters");
2390
+ return expr;
1774
2391
  }
1775
2392
  //#endregion
1776
2393
  //#region src/parse/ParseCall.ts
@@ -1781,7 +2398,7 @@ function parseTemplateParam(ctx) {
1781
2398
  function parseCallSuffix(ctx, current, parseExpr) {
1782
2399
  if (current.kind !== "ref" && current.kind !== "type") return null;
1783
2400
  const { stream } = ctx;
1784
- const templateArgs = current.kind === "type" ? current.templateParams ?? null : parseCallTemplateArgs(ctx);
2401
+ const templateArgs = current.kind === "ref" ? parseCallTemplateArgs(ctx) : null;
1785
2402
  if (!stream.matchText("(")) return null;
1786
2403
  const args = [];
1787
2404
  while (true) {
@@ -1946,8 +2563,7 @@ function parseTemplateElaboratedIdent(ctx, conditionRef) {
1946
2563
  name: refIdent.ident,
1947
2564
  templateParams,
1948
2565
  start: refIdent.start,
1949
- end: ctx.stream.checkpoint(),
1950
- contents: []
2566
+ end: ctx.stream.checkpoint()
1951
2567
  };
1952
2568
  }
1953
2569
  /** Parse postfix operators: member access, indexing, function calls. */
@@ -1974,7 +2590,9 @@ function parseMemberAccess(ctx, base) {
1974
2590
  function parseIndexAccess(ctx, base) {
1975
2591
  const { stream } = ctx;
1976
2592
  if (!stream.matchText("[")) return null;
1977
- return makeComponentExpression(base, expectExpression(ctx, "Expected expression in array index"), expect(stream, "]", "array index").span[1]);
2593
+ const indexExpr = parseExpression(ctx);
2594
+ if (!indexExpr) throwParseError(stream, "Expected expression in array index");
2595
+ return makeComponentExpression(base, indexExpr, expect(stream, "]", "array index").span[1]);
1978
2596
  }
1979
2597
  //#endregion
1980
2598
  //#region src/parse/ParseAttribute.ts
@@ -1995,6 +2613,20 @@ function parseElseAttribute(ctx) {
1995
2613
  function parseElifAttribute(ctx) {
1996
2614
  return parseConditionalAttribute(ctx, "elif", makeElifAttribute);
1997
2615
  }
2616
+ /** Parse WESL conditional attributes (@if, @elif, @else) */
2617
+ function parseWeslConditional(ctx) {
2618
+ const { stream } = ctx;
2619
+ const peeked = stream.peek();
2620
+ if (peeked?.text !== "@") return null;
2621
+ const startPos = peeked.span[0];
2622
+ const ifAttr = parseIfAttribute(ctx);
2623
+ if (ifAttr) return attributeElem(ifAttr, startPos, stream.checkpoint());
2624
+ const elifAttr = parseElifAttribute(ctx);
2625
+ if (elifAttr) return attributeElem(elifAttr, startPos, stream.checkpoint());
2626
+ const elseAttr = parseElseAttribute(ctx);
2627
+ if (elseAttr) return attributeElem(elseAttr, startPos, stream.checkpoint());
2628
+ return null;
2629
+ }
1998
2630
  /**
1999
2631
  * Grammar: attribute :
2000
2632
  * '@' ident_pattern_token argument_expression_list ?
@@ -2005,15 +2637,8 @@ function parseElifAttribute(ctx) {
2005
2637
  * WESL extensions: @if, @elif, @else
2006
2638
  */
2007
2639
  function parseAttribute(ctx) {
2008
- const { stream } = ctx;
2009
- const startPos = stream.checkpoint();
2010
- if (!stream.matchText("@")) return null;
2011
- stream.reset(startPos);
2012
- const weslAttr = parseWeslConditional(ctx);
2013
- if (weslAttr) return weslAttr;
2014
- const stdAttr = parseStandardAttribute(ctx);
2015
- if (stdAttr) return stdAttr;
2016
- return null;
2640
+ if (ctx.stream.peek()?.text !== "@") return null;
2641
+ return parseWeslConditional(ctx) ?? parseStandardAttribute(ctx);
2017
2642
  }
2018
2643
  /** Parse `@if(expr)` or `@elif(expr)` conditional attributes. */
2019
2644
  function parseConditionalAttribute(ctx, keyword, makeAttr) {
@@ -2022,13 +2647,15 @@ function parseConditionalAttribute(ctx, keyword, makeAttr) {
2022
2647
  if (!stream.matchSequence("@", keyword)) return null;
2023
2648
  expect(stream, "(", `@${keyword}`);
2024
2649
  const expr = parseExpression(ctx, true);
2025
- if (!expr) return null;
2650
+ if (!expr) throwParseError(stream, `Expected expression after @${keyword}(`);
2026
2651
  stream.matchText(",");
2027
2652
  expect(stream, ")", `@${keyword} expression`);
2028
- return makeAttr(makeTranslateTimeExpressionElem({
2029
- value: expr,
2030
- span: [startPos, stream.checkpoint()]
2031
- }));
2653
+ return makeAttr({
2654
+ kind: "translate-time-expression",
2655
+ expression: expr,
2656
+ start: startPos,
2657
+ end: stream.checkpoint()
2658
+ });
2032
2659
  }
2033
2660
  function makeIfAttribute(param) {
2034
2661
  return {
@@ -2045,19 +2672,13 @@ function makeElifAttribute(param) {
2045
2672
  param
2046
2673
  };
2047
2674
  }
2048
- /** Parse WESL conditional attributes (@if, @elif, @else) */
2049
- function parseWeslConditional(ctx) {
2050
- const { stream } = ctx;
2051
- const peeked = stream.peek();
2052
- if (peeked?.text !== "@") return null;
2053
- const startPos = peeked.span[0];
2054
- const ifAttr = parseIfAttribute(ctx);
2055
- if (ifAttr) return attributeElem(ifAttr, startPos, stream.checkpoint());
2056
- const elifAttr = parseElifAttribute(ctx);
2057
- if (elifAttr) return attributeElem(elifAttr, startPos, stream.checkpoint());
2058
- const elseAttr = parseElseAttribute(ctx);
2059
- if (elseAttr) return attributeElem(elseAttr, startPos, stream.checkpoint());
2060
- return null;
2675
+ function attributeElem(attribute, start, end) {
2676
+ return {
2677
+ kind: "attribute",
2678
+ attribute,
2679
+ start,
2680
+ end
2681
+ };
2061
2682
  }
2062
2683
  /** Parse a standard attribute (not @if/@elif/@else) */
2063
2684
  function parseStandardAttribute(ctx) {
@@ -2067,7 +2688,7 @@ function parseStandardAttribute(ctx) {
2067
2688
  if (!atToken) return null;
2068
2689
  const startPos = atToken.span[0];
2069
2690
  const nameToken = stream.peek();
2070
- if (!nameToken || nameToken.kind !== "word" && nameToken.kind !== "keyword") {
2691
+ if (nameToken?.kind !== "word" && nameToken?.kind !== "keyword") {
2071
2692
  stream.reset(resetPos);
2072
2693
  return null;
2073
2694
  }
@@ -2090,22 +2711,6 @@ function parseStandardAttribute(ctx) {
2090
2711
  params
2091
2712
  }, startPos, stream.checkpoint());
2092
2713
  }
2093
- function makeTranslateTimeExpressionElem(args) {
2094
- return {
2095
- kind: "translate-time-expression",
2096
- expression: args.value,
2097
- span: args.span
2098
- };
2099
- }
2100
- function attributeElem(attribute, start, end) {
2101
- return {
2102
- kind: "attribute",
2103
- attribute,
2104
- start,
2105
- end,
2106
- contents: []
2107
- };
2108
- }
2109
2714
  function parseBuiltinAttribute(ctx, startPos) {
2110
2715
  const { stream } = ctx;
2111
2716
  expect(stream, "(", "@builtin");
@@ -2126,9 +2731,6 @@ function parseInterpolateAttribute(ctx, startPos) {
2126
2731
  params
2127
2732
  }, startPos, stream.checkpoint());
2128
2733
  }
2129
- function parseNameElem(ctx) {
2130
- return makeNameElem(expectWord(ctx.stream, "Expected identifier"));
2131
- }
2132
2734
  /** @diagnostic(severity, rule) or @diagnostic(severity, namespace.rule) */
2133
2735
  function parseDiagnosticAttribute(ctx, startPos) {
2134
2736
  const { stream } = ctx;
@@ -2150,78 +2752,20 @@ function parseDiagnosticAttribute(ctx, startPos) {
2150
2752
  function parseAttributeParams(ctx) {
2151
2753
  return parseCommaList(ctx, parseAttrParam);
2152
2754
  }
2755
+ function parseNameElem(ctx) {
2756
+ return makeNameElem(expectWord(ctx.stream, "Expected identifier"));
2757
+ }
2153
2758
  function parseAttrParam(ctx) {
2154
2759
  const { stream } = ctx;
2155
2760
  const start = stream.checkpoint();
2156
- beginElem(ctx, "expression");
2157
- parseExpression(ctx);
2158
- const end = stream.checkpoint();
2761
+ const expression = parseExpression(ctx);
2762
+ if (!expression) throwParseError(stream, "Expected attribute parameter");
2159
2763
  return {
2160
2764
  kind: "expression",
2765
+ expression,
2161
2766
  start,
2162
- end,
2163
- contents: finishContents(ctx, start, end)
2164
- };
2165
- }
2166
- //#endregion
2167
- //#region src/parse/ParseDirective.ts
2168
- /** Grammar: global_directive : diagnostic_directive | enable_directive | requires_directive */
2169
- function parseDirective(ctx) {
2170
- const { stream } = ctx;
2171
- const startPos = stream.checkpoint();
2172
- const attributes = parseAttributeList(ctx);
2173
- const attrs = attributes.length > 0 ? attributes : void 0;
2174
- const result = parseExtensionDirective(ctx, "enable", attrs) || parseExtensionDirective(ctx, "requires", attrs) || parseDiagnosticDirective(ctx, attrs);
2175
- if (!result) stream.reset(startPos);
2176
- return result;
2177
- }
2178
- /** Grammar: enable_directive | requires_directive : keyword extension_list ';' */
2179
- function parseExtensionDirective(ctx, keyword, attributes) {
2180
- const { stream } = ctx;
2181
- const token = stream.matchText(keyword);
2182
- if (!token) return null;
2183
- const extensions = parseCommaList(ctx, parseDirectiveName);
2184
- expect(stream, ";", `${keyword} directive`);
2185
- return makeDirectiveElem({
2186
- kind: keyword,
2187
- extensions
2188
- }, token, stream, attributes);
2189
- }
2190
- function parseDirectiveName(ctx) {
2191
- return makeNameElem(expectWord(ctx.stream, "Expected identifier in name list"));
2192
- }
2193
- /**
2194
- * Grammar: diagnostic_directive : 'diagnostic' diagnostic_control ';'
2195
- * Grammar: diagnostic_control : '(' severity_control_name ',' diagnostic_rule_name ',' ? ')'
2196
- */
2197
- function parseDiagnosticDirective(ctx, attributes) {
2198
- const { stream } = ctx;
2199
- const token = stream.matchText("diagnostic");
2200
- if (!token) return null;
2201
- expect(stream, "(", "diagnostic");
2202
- const severity = makeNameElem(expectWord(stream, "Expected severity in diagnostic"));
2203
- expect(stream, ",", "diagnostic severity");
2204
- const ruleName = makeNameElem(expectWord(stream, "Expected rule name in diagnostic"));
2205
- let subrule = null;
2206
- if (stream.matchText(".")) subrule = makeNameElem(expectWord(stream, "Expected subrule name after '.'"));
2207
- stream.matchText(",");
2208
- expect(stream, ")", "diagnostic rule");
2209
- expect(stream, ";", "diagnostic directive");
2210
- return makeDirectiveElem({
2211
- kind: "diagnostic",
2212
- severity,
2213
- rule: [ruleName, subrule]
2214
- }, token, stream, attributes);
2215
- }
2216
- function makeDirectiveElem(directive, token, stream, attributes) {
2217
- const elem = {
2218
- kind: "directive",
2219
- directive,
2220
- start: attributes?.[0]?.start ?? token.span[0],
2221
2767
  end: stream.checkpoint()
2222
2768
  };
2223
- attachAttributes(elem, attributes);
2224
- return elem;
2225
2769
  }
2226
2770
  //#endregion
2227
2771
  //#region src/parse/ParseControlFlow.ts
@@ -2232,37 +2776,50 @@ function makeDirectiveElem(directive, token, stream, attributes) {
2232
2776
  function parseIfStatement(ctx, attributes) {
2233
2777
  const startPos = beginStatement(ctx, "if", attributes);
2234
2778
  if (startPos === null) return null;
2235
- expectExpression(ctx, "Expected condition expression after 'if'");
2236
- const body = expectCompound(ctx, "Expected '{' after if condition");
2237
- ctx.addElem(body);
2238
- parseElseChain(ctx);
2239
- return finishBlockStatement(startPos, ctx, attributes);
2779
+ return finishStatement("if", startPos, ctx, {
2780
+ condition: expectExpression(ctx, "Expected condition after 'if'"),
2781
+ body: expectCompound(ctx, "Expected '{' after if condition"),
2782
+ else: parseElseChain(ctx)
2783
+ }, attributes);
2240
2784
  }
2241
2785
  /** Grammar: switch_statement : attribute* 'switch' expression switch_body */
2242
2786
  function parseSwitchStatement(ctx, attributes) {
2243
2787
  const startPos = beginStatement(ctx, "switch", attributes);
2244
2788
  if (startPos === null) return null;
2245
- expectExpression(ctx, "Expected expression after 'switch'");
2246
- expectSwitchClauses(ctx);
2247
- return finishBlockStatement(startPos, ctx, attributes);
2789
+ const selector = expectExpression(ctx, "Expected expression after 'switch'");
2790
+ const { bodyAttributes, clauses } = expectSwitchClauses(ctx);
2791
+ return finishStatement("switch", startPos, ctx, {
2792
+ selector,
2793
+ clauses,
2794
+ bodyAttributes
2795
+ }, attributes);
2248
2796
  }
2249
2797
  /**
2250
2798
  * Grammar: else_if_clause : 'else' 'if' expression compound_statement
2251
2799
  * Grammar: else_clause : 'else' compound_statement
2800
+ *
2801
+ * An else-if nests as an IfElem in the outer if's `else` field; a plain else is
2802
+ * a BlockElem. Emit and the AST dump read these typed fields.
2252
2803
  */
2253
2804
  function parseElseChain(ctx) {
2254
2805
  const { stream } = ctx;
2255
- while (stream.matchText("else")) {
2256
- if (stream.matchText("if")) {
2257
- expectExpression(ctx, "Expected expression after 'else if'");
2258
- const body = expectCompound(ctx, "Expected '{' after else if");
2259
- ctx.addElem(body);
2260
- continue;
2261
- }
2262
- const body = expectCompound(ctx, "Expected '{' after else");
2263
- ctx.addElem(body);
2264
- break;
2806
+ const elseToken = stream.matchText("else");
2807
+ if (!elseToken) return void 0;
2808
+ if (stream.matchText("if")) {
2809
+ const condition = expectExpression(ctx, "Expected expression after 'else if'");
2810
+ const body = expectCompound(ctx, "Expected '{' after else if");
2811
+ const elseBranch = parseElseChain(ctx);
2812
+ const end = stream.checkpoint();
2813
+ return {
2814
+ kind: "if",
2815
+ condition,
2816
+ body,
2817
+ else: elseBranch,
2818
+ start: elseToken.span[0],
2819
+ end
2820
+ };
2265
2821
  }
2822
+ return expectCompound(ctx, "Expected '{' after else");
2266
2823
  }
2267
2824
  /**
2268
2825
  * Grammar: switch_body : attribute* '{' switch_clause+ '}'
@@ -2272,27 +2829,43 @@ function parseElseChain(ctx) {
2272
2829
  */
2273
2830
  function expectSwitchClauses(ctx) {
2274
2831
  const { stream } = ctx;
2275
- parseAttributeList(ctx);
2832
+ const bodyAttrs = parseAttributeList(ctx);
2276
2833
  expect(stream, "{", "switch expression");
2277
- while (!stream.matchText("}")) {
2278
- const clauseStart = stream.checkpoint();
2279
- const clauseAttrs = parseAttributeList(ctx);
2280
- beginElem(ctx, "switch-clause", clauseAttrs.length ? clauseAttrs : void 0);
2281
- if (stream.matchText("case")) {
2282
- parseCaseSelectors(ctx);
2283
- parseCaseBody(ctx, "Expected '{' after case value");
2284
- } else if (stream.matchText("default")) parseCaseBody(ctx, "Expected '{' after 'default'");
2285
- else throwParseError(stream, "Expected 'case', 'default', or '}' in switch");
2286
- const clauseElem = finishElem("switch-clause", clauseStart, ctx, {});
2287
- attachAttributes(clauseElem, clauseAttrs.length ? clauseAttrs : void 0);
2288
- ctx.addElem(clauseElem);
2834
+ const clauses = [];
2835
+ while (!stream.matchText("}")) clauses.push(parseSwitchClause(ctx));
2836
+ return {
2837
+ bodyAttributes: attrsOrUndef(bodyAttrs),
2838
+ clauses
2839
+ };
2840
+ }
2841
+ /** Parse one 'case'/'default' clause (the keyword has not yet been consumed). */
2842
+ function parseSwitchClause(ctx) {
2843
+ const { stream } = ctx;
2844
+ const attrs = attrsOrUndef(parseAttributeList(ctx));
2845
+ const caseTok = stream.matchText("case");
2846
+ const keyword = caseTok ?? stream.matchText("default");
2847
+ if (!keyword) throwParseError(stream, "Expected 'case', 'default', or '}' in switch");
2848
+ const clauseStart = getStartWithAttributes(attrs, keyword.span[0]);
2849
+ let selectors;
2850
+ let body;
2851
+ if (caseTok) {
2852
+ selectors = parseCaseSelectors(ctx);
2853
+ body = parseCaseBody(ctx, "Expected '{' after case value");
2854
+ } else {
2855
+ selectors = ["default"];
2856
+ body = parseCaseBody(ctx, "Expected '{' after 'default'");
2289
2857
  }
2858
+ return finishStatement("switch-clause", clauseStart, ctx, {
2859
+ selectors,
2860
+ body
2861
+ }, attrs);
2290
2862
  }
2291
2863
  /** Grammar: case_selectors : case_selector (',' case_selector)* ','? */
2292
2864
  function parseCaseSelectors(ctx) {
2293
2865
  const { stream } = ctx;
2294
- expectExpression(ctx, "Expected expression after 'case'");
2295
- while (stream.matchText(",")) expectExpression(ctx, "Expected expression after ',' in case values");
2866
+ const selectors = [expectExpression(ctx, "Expected expression after 'case'")];
2867
+ while (stream.matchText(",")) selectors.push(expectExpression(ctx, "Expected expression after ',' in case values"));
2868
+ return selectors;
2296
2869
  }
2297
2870
  /**
2298
2871
  * Grammar: case_clause : 'case' case_selectors ':'? compound_statement
@@ -2300,16 +2873,15 @@ function parseCaseSelectors(ctx) {
2300
2873
  */
2301
2874
  function parseCaseBody(ctx, errorMsg) {
2302
2875
  ctx.stream.matchText(":");
2303
- const bodyAttrs = parseAttributeList(ctx);
2304
- const body = parseCompoundStatement(ctx, bodyAttrs.length > 0 ? bodyAttrs : void 0);
2876
+ const body = parseCompoundStatement(ctx, attrsOrUndef(parseAttributeList(ctx)));
2305
2877
  if (!body) throwParseError(ctx.stream, errorMsg);
2306
- ctx.addElem(body);
2878
+ return body;
2307
2879
  }
2308
2880
  //#endregion
2309
2881
  //#region src/parse/ParseValueDeclaration.ts
2310
2882
  /** Grammar: 'const' optionally_typed_ident '=' expression (global or local) */
2311
2883
  function parseConstDecl(ctx, attributes) {
2312
- return parseValueDecl(ctx, "const", true, isModuleScope(ctx), attributes);
2884
+ return parseValueDecl(ctx, "const", true, ctx.isModuleScope(), attributes);
2313
2885
  }
2314
2886
  /** Grammar: 'override' optionally_typed_ident ( '=' expression )? */
2315
2887
  function parseOverrideDecl(ctx, attributes) {
@@ -2320,20 +2892,16 @@ function parseTypedDecl(ctx, isGlobal = true) {
2320
2892
  const nameToken = ctx.stream.matchKind("word");
2321
2893
  if (!nameToken) return null;
2322
2894
  const start = nameToken.span[0];
2323
- beginElem(ctx, "typeDecl");
2324
2895
  const decl = createDeclIdentElem(ctx, nameToken, isGlobal);
2325
- ctx.addElem(decl);
2326
2896
  ctx.saveIdent(decl.ident);
2327
2897
  const { typeRef, typeScope } = parseOptionalType(ctx);
2328
- const end = ctx.stream.checkpoint();
2329
2898
  return {
2330
2899
  kind: "typeDecl",
2331
2900
  decl,
2332
2901
  typeRef,
2333
2902
  typeScope,
2334
2903
  start,
2335
- end,
2336
- contents: finishContents(ctx, start, end)
2904
+ end: ctx.stream.checkpoint()
2337
2905
  };
2338
2906
  }
2339
2907
  /** Shared parser for const/override declarations. */
@@ -2343,43 +2911,29 @@ function parseValueDecl(ctx, keyword, requiresInit, isGlobal, attributes) {
2343
2911
  if (!token) return null;
2344
2912
  const startPos = getStartWithAttributes(attributes, token.span[0]);
2345
2913
  ctx.pushScope("partial");
2346
- beginElem(ctx, keyword, attributes);
2347
2914
  const typedDecl = parseTypedDecl(ctx, isGlobal);
2348
2915
  if (!typedDecl) throwParseError(stream, `Expected identifier after '${keyword}'`);
2349
- ctx.addElem(typedDecl);
2916
+ let init;
2350
2917
  if (requiresInit) {
2351
2918
  expect(stream, "=", `${keyword} identifier`);
2352
- expectExpression(ctx);
2353
- } else if (stream.matchText("=")) expectExpression(ctx);
2919
+ init = expectExpression(ctx);
2920
+ } else if (stream.matchText("=")) init = expectExpression(ctx);
2354
2921
  expect(stream, ";", `${keyword} declaration`);
2355
- const endPos = stream.checkpoint();
2356
- const contents = finishContents(ctx, startPos, endPos);
2357
2922
  typedDecl.decl.ident.dependentScope = ctx.currentScope();
2358
2923
  ctx.popScope();
2359
- const elem = {
2360
- kind: keyword,
2924
+ const elem = finishStatement(keyword, startPos, ctx, {
2361
2925
  name: typedDecl,
2362
- start: startPos,
2363
- end: endPos,
2364
- contents
2365
- };
2366
- attachAttributes(elem, attributes);
2926
+ init
2927
+ }, attributes);
2367
2928
  linkDeclIdent(typedDecl, elem);
2368
2929
  return elem;
2369
2930
  }
2370
- /** @return true if ctx is at module level (not inside fn/block) */
2371
- function isModuleScope(ctx) {
2372
- let scope = ctx.currentScope();
2373
- while (scope.kind === "partial" && scope.parent) scope = scope.parent;
2374
- return scope.parent === null;
2375
- }
2376
2931
  /** Parse optional ': type' annotation, managing scope for type references. */
2377
2932
  function parseOptionalType(ctx) {
2378
2933
  if (!ctx.stream.matchText(":")) return {};
2379
2934
  ctx.pushScope();
2380
2935
  const typeRef = parseSimpleTypeRef(ctx);
2381
2936
  if (!typeRef) throwParseError(ctx.stream, "Expected type after ':'");
2382
- ctx.addElem(typeRef);
2383
2937
  const typeScope = ctx.currentScope();
2384
2938
  ctx.popScope();
2385
2939
  return {
@@ -2399,17 +2953,19 @@ function parseGlobalVarDecl(ctx, attributes) {
2399
2953
  if (!varToken) return null;
2400
2954
  const startPos = getStartWithAttributes(attributes, varToken.span[0]);
2401
2955
  ctx.pushScope("partial");
2402
- beginElem(ctx, "gvar", attributes);
2403
- skipTemplateList(ctx);
2956
+ const template = parseTemplateList(ctx);
2404
2957
  const typedDecl = parseTypedDecl(ctx);
2405
2958
  if (!typedDecl) throwParseError(stream, "Expected identifier after 'var'");
2406
- ctx.addElem(typedDecl);
2407
- if (stream.matchText("=")) expectExpression(ctx);
2959
+ let init;
2960
+ if (stream.matchText("=")) init = expectExpression(ctx);
2408
2961
  expect(stream, ";", "var declaration");
2409
2962
  typedDecl.decl.ident.dependentScope = ctx.currentScope();
2410
2963
  ctx.popScope();
2411
- const varElem = finishElem("gvar", startPos, ctx, { name: typedDecl });
2412
- attachAttributes(varElem, attributes);
2964
+ const varElem = finishStatement("gvar", startPos, ctx, {
2965
+ name: typedDecl,
2966
+ template,
2967
+ init
2968
+ }, attributes);
2413
2969
  linkDeclIdent(typedDecl, varElem);
2414
2970
  return varElem;
2415
2971
  }
@@ -2419,23 +2975,19 @@ function parseAliasDecl(ctx, attributes) {
2419
2975
  const aliasToken = stream.matchText("alias");
2420
2976
  if (!aliasToken) return null;
2421
2977
  const startPos = getStartWithAttributes(attributes, aliasToken.span[0]);
2422
- beginElem(ctx, "alias", attributes);
2423
2978
  const declIdentElem = createDeclIdentElem(ctx, expectWord(stream, "Expected identifier after 'alias'"), true);
2424
- ctx.addElem(declIdentElem);
2425
2979
  ctx.saveIdent(declIdentElem.ident);
2426
2980
  expect(stream, "=", "alias name");
2427
2981
  ctx.pushScope();
2428
2982
  const typeRef = parseSimpleTypeRef(ctx);
2429
2983
  if (!typeRef) throwParseError(stream, "Expected type after '=' in alias declaration");
2430
- ctx.addElem(typeRef);
2431
2984
  declIdentElem.ident.dependentScope = ctx.currentScope();
2432
2985
  ctx.popScope();
2433
2986
  expect(stream, ";", "alias declaration");
2434
- const aliasElem = finishElem("alias", startPos, ctx, {
2987
+ const aliasElem = finishStatement("alias", startPos, ctx, {
2435
2988
  name: declIdentElem,
2436
2989
  typeRef
2437
- });
2438
- attachAttributes(aliasElem, attributes);
2990
+ }, attributes);
2439
2991
  linkDeclIdentElem(declIdentElem, aliasElem);
2440
2992
  return aliasElem;
2441
2993
  }
@@ -2444,25 +2996,29 @@ function parseConstAssert(ctx, attributes) {
2444
2996
  const assertToken = ctx.stream.matchText("const_assert");
2445
2997
  if (!assertToken) return null;
2446
2998
  const startPos = getStartWithAttributes(attributes, assertToken.span[0]);
2447
- beginElem(ctx, "assert", attributes);
2448
- expectExpression(ctx);
2999
+ const expression = expectExpression(ctx);
2449
3000
  expect(ctx.stream, ";", "const_assert expression");
2450
- const elem = finishElem("assert", startPos, ctx, {});
2451
- attachAttributes(elem, attributes);
2452
- return elem;
3001
+ return finishStatement("assert", startPos, ctx, { expression }, attributes);
2453
3002
  }
2454
- /** Skip optional template list (e.g., <storage, read_write>). */
2455
- function skipTemplateList(ctx) {
3003
+ /**
3004
+ * Parse an optional var template list `<storage, read_write>`. The entries are
3005
+ * predeclared enumerants (address space / access mode), not user idents, so they
3006
+ * are captured as unbound NameElems.
3007
+ */
3008
+ function parseTemplateList(ctx) {
2456
3009
  const { stream } = ctx;
2457
- if (!stream.nextTemplateStartToken()) return;
3010
+ if (!stream.nextTemplateStartToken()) return void 0;
3011
+ const names = [];
2458
3012
  while (true) {
2459
3013
  const next = stream.peek();
2460
3014
  if (!next) throwParseError(stream, "Unclosed template in var declaration");
2461
3015
  if (next.text.startsWith(">")) {
2462
3016
  stream.nextTemplateEndToken();
2463
- return;
3017
+ return names;
2464
3018
  }
2465
- stream.nextToken();
3019
+ const word = stream.matchKind("word");
3020
+ if (word) names.push(makeNameElem(word));
3021
+ else if (!stream.matchText(",")) throwParseError(stream, "Expected ',' or '>' in var template list");
2466
3022
  }
2467
3023
  }
2468
3024
  //#endregion
@@ -2484,18 +3040,20 @@ function parseVarOrLet(ctx, keyword, hasTemplate, requiresInit, attributes) {
2484
3040
  const token = stream.matchText(keyword);
2485
3041
  if (!token) return null;
2486
3042
  const startPos = getStartWithAttributes(attributes, token.span[0]);
2487
- beginElem(ctx, keyword, attributes);
2488
- if (hasTemplate) skipTemplateList(ctx);
3043
+ const template = hasTemplate ? parseTemplateList(ctx) : void 0;
2489
3044
  const typedDecl = parseTypedDecl(ctx, false);
2490
3045
  if (!typedDecl) throwParseError(stream, `Expected identifier after '${keyword}'`);
2491
- ctx.addElem(typedDecl);
3046
+ let init;
2492
3047
  if (requiresInit) {
2493
3048
  expect(stream, "=", `${keyword} identifier (${keyword} requires initialization)`);
2494
- expectExpression(ctx);
2495
- } else if (stream.matchText("=")) expectExpression(ctx);
3049
+ init = expectExpression(ctx);
3050
+ } else if (stream.matchText("=")) init = expectExpression(ctx);
2496
3051
  expect(stream, ";", `${keyword} declaration`);
2497
- const elem = finishElem(keyword, startPos, ctx, { name: typedDecl });
2498
- attachAttributes(elem, attributes);
3052
+ const elem = finishStatement(keyword, startPos, ctx, {
3053
+ name: typedDecl,
3054
+ init
3055
+ }, attributes);
3056
+ if (template && elem.kind === "var") elem.template = template;
2499
3057
  linkDeclIdent(typedDecl, elem);
2500
3058
  return elem;
2501
3059
  }
@@ -2524,17 +3082,42 @@ const assignmentOps = new Set([
2524
3082
  function parseSimpleStatement(ctx, attributes) {
2525
3083
  const { stream } = ctx;
2526
3084
  const startPos = getStartWithAttributes(attributes, stream.checkpoint());
2527
- 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);
3085
+ 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);
3086
+ }
3087
+ /** Match an assignment operator, capturing its value and span. */
3088
+ function parseAssignmentOp(stream) {
3089
+ const token = stream.nextIf(({ text }) => assignmentOps.has(text));
3090
+ if (!token) return null;
3091
+ return {
3092
+ value: token.text,
3093
+ span: token.span
3094
+ };
3095
+ }
3096
+ /** Match '++' or '--', returning its text and span (or null if absent). */
3097
+ function parseIncDecOp(stream) {
3098
+ const token = stream.nextIf(({ text }) => text === "++" || text === "--");
3099
+ if (!token) return null;
3100
+ return {
3101
+ op: token.text,
3102
+ span: token.span
3103
+ };
3104
+ }
3105
+ /** Grammar: ( '=' | compound_assignment_operator ) expression. Returns op + rhs. */
3106
+ function parseAssignmentRhs(ctx) {
3107
+ const op = parseAssignmentOp(ctx.stream);
3108
+ if (!op) return null;
3109
+ return {
3110
+ op,
3111
+ rhs: expectExpression(ctx, "Expected expression after assignment operator")
3112
+ };
2528
3113
  }
2529
3114
  /** Grammar: return_statement : 'return' expression? ';' */
2530
3115
  function parseReturnStmt(ctx, startPos, attributes) {
2531
3116
  const { stream } = ctx;
2532
3117
  if (!stream.matchText("return")) return null;
2533
- beginElem(ctx, "statement", attributes);
2534
- const expr = parseExpression(ctx);
2535
- if (expr && ctx.options.preserveExpressions) ctx.addElem(expr);
3118
+ const value = parseContentExpression(ctx) ?? void 0;
2536
3119
  expect(stream, ";", "return statement");
2537
- return finishBlockStatement(startPos, ctx, attributes);
3120
+ return finishStatement("return", startPos, ctx, { value }, attributes);
2538
3121
  }
2539
3122
  /**
2540
3123
  * Grammar: break_statement : 'break' ';'
@@ -2543,70 +3126,79 @@ function parseReturnStmt(ctx, startPos, attributes) {
2543
3126
  function parseBreakStmt(ctx, startPos, attributes) {
2544
3127
  const { stream } = ctx;
2545
3128
  if (!stream.matchText("break")) return null;
2546
- beginElem(ctx, "statement", attributes);
2547
- if (stream.matchText("if")) expectExpression(ctx, "Expected condition after 'break if'");
3129
+ let condition;
3130
+ if (stream.matchText("if")) condition = expectExpression(ctx, "Expected condition after 'break if'");
2548
3131
  expect(stream, ";", "break statement");
2549
- return finishBlockStatement(startPos, ctx, attributes);
3132
+ return finishStatement("break", startPos, ctx, { condition }, attributes);
3133
+ }
3134
+ /** Grammar: continue_statement : 'continue' ';' */
3135
+ function parseContinueStmt(ctx, startPos, attributes) {
3136
+ const { stream } = ctx;
3137
+ if (!stream.matchText("continue")) return null;
3138
+ expect(stream, ";", "continue statement");
3139
+ return finishStatement("continue", startPos, ctx, {}, attributes);
2550
3140
  }
2551
- /** Grammar: continue_statement : 'continue' ';' also handles 'discard' */
2552
- function parseKeywordStmt(ctx, startPos, attributes, keyword) {
3141
+ /** Grammar: 'discard' ';' */
3142
+ function parseDiscardStmt(ctx, startPos, attributes) {
2553
3143
  const { stream } = ctx;
2554
- if (!stream.matchText(keyword)) return null;
2555
- beginElem(ctx, "statement", attributes);
2556
- expect(stream, ";", `${keyword} statement`);
2557
- return finishBlockStatement(startPos, ctx, attributes);
3144
+ if (!stream.matchText("discard")) return null;
3145
+ expect(stream, ";", "discard statement");
3146
+ return finishStatement("discard", startPos, ctx, {}, attributes);
2558
3147
  }
2559
- /** Parse empty statement (just ';'). */
3148
+ /** Parse empty statement (just ';'). Spans the ';' so it emits no extra text. */
2560
3149
  function parseEmptyStmt(stream, start) {
2561
3150
  if (!stream.matchText(";")) return null;
2562
3151
  return {
2563
- kind: "statement",
3152
+ kind: "empty",
2564
3153
  start,
2565
- end: stream.checkpoint(),
2566
- contents: []
3154
+ end: stream.checkpoint()
2567
3155
  };
2568
3156
  }
2569
3157
  /** Grammar: assignment_statement : '_' '=' expression ';' (phony assignment) */
2570
3158
  function parsePhonyAssignment(ctx, startPos, attributes) {
2571
3159
  const { stream } = ctx;
2572
- if (!stream.matchText("_")) return null;
2573
- if (!parseAssignmentOperator(stream)) throwParseError(stream, "Expected assignment operator after '_'");
2574
- beginElem(ctx, "statement", attributes);
2575
- expectExpression(ctx, "Expected expression after assignment operator");
3160
+ const underscore = stream.matchText("_");
3161
+ if (!underscore) return null;
3162
+ const lhs = {
3163
+ kind: "phony",
3164
+ span: underscore.span
3165
+ };
3166
+ const eq = stream.matchText("=");
3167
+ if (!eq) throwParseError(stream, "Expected '=' after '_'");
3168
+ const op = {
3169
+ value: "=",
3170
+ span: eq.span
3171
+ };
3172
+ const rhs = expectExpression(ctx, "Expected expression after '_ ='");
2576
3173
  expect(stream, ";", "assignment");
2577
- return finishBlockStatement(startPos, ctx, attributes);
3174
+ return finishStatement("assign", startPos, ctx, {
3175
+ lhs,
3176
+ op,
3177
+ rhs
3178
+ }, attributes);
2578
3179
  }
2579
- /**
2580
- * Parses expression statements: assignments, increments/decrements, or function calls.
2581
- * Grammar: ( assignment_statement | increment_statement | decrement_statement | call_phrase ) ';'
2582
- */
3180
+ /** Grammar: ( assignment_statement | increment_statement | decrement_statement | call_phrase ) ';' */
2583
3181
  function parseExpressionStmt(ctx, startPos, attributes) {
2584
3182
  const { stream } = ctx;
2585
- beginElem(ctx, "statement", attributes);
2586
3183
  const expr = parseExpression(ctx);
2587
3184
  if (!expr) {
2588
- finishContents(ctx, startPos, startPos);
2589
3185
  stream.reset(startPos);
2590
3186
  return null;
2591
3187
  }
2592
- if (ctx.options.preserveExpressions) ctx.addElem(expr);
2593
- if (!parseIncDecOperator(stream)) parseAssignmentRhs(ctx);
3188
+ const incDec = parseIncDecOp(stream);
3189
+ if (incDec) {
3190
+ expect(stream, ";", "expression");
3191
+ return finishStatement(incDec.op === "++" ? "increment" : "decrement", startPos, ctx, { target: expr }, attributes);
3192
+ }
3193
+ const assign = parseAssignmentRhs(ctx);
2594
3194
  expect(stream, ";", "expression");
2595
- return finishBlockStatement(startPos, ctx, attributes);
2596
- }
2597
- /** Grammar: assignment_statement : lhs_expression ( '=' | compound_assignment_operator ) expression */
2598
- function parseAssignmentOperator(stream) {
2599
- return !!stream.nextIf(({ text }) => assignmentOps.has(text));
2600
- }
2601
- /** Grammar: ( '=' | compound_assignment_operator ) expression (rhs of assignment_statement) */
2602
- function parseAssignmentRhs(ctx) {
2603
- if (!parseAssignmentOperator(ctx.stream)) return false;
2604
- expectExpression(ctx, "Expected expression after assignment operator");
2605
- return true;
2606
- }
2607
- /** Grammar: increment_statement : lhs_expression '++' ; decrement_statement : lhs_expression '--' */
2608
- function parseIncDecOperator(stream) {
2609
- return !!stream.nextIf(({ text }) => text === "++" || text === "--");
3195
+ if (assign) return finishStatement("assign", startPos, ctx, {
3196
+ lhs: expr,
3197
+ op: assign.op,
3198
+ rhs: assign.rhs
3199
+ }, attributes);
3200
+ if (expr.kind !== "call-expression") throwParseError(stream, "Expected call, assignment, or increment");
3201
+ return finishStatement("call", startPos, ctx, { call: expr }, attributes);
2610
3202
  }
2611
3203
  //#endregion
2612
3204
  //#region src/parse/ParseLoop.ts
@@ -2620,41 +3212,48 @@ function parseForStatement(ctx, attributes) {
2620
3212
  if (startPos === null) return null;
2621
3213
  ctx.pushScope();
2622
3214
  expect(stream, "(", "'for'");
2623
- parseForInit(ctx);
2624
- const cond = parseExpression(ctx);
2625
- if (cond && ctx.options.preserveExpressions) ctx.addElem(cond);
3215
+ const init = parseForInit(ctx);
3216
+ const condition = parseContentExpression(ctx) ?? void 0;
2626
3217
  expect(stream, ";", "for loop condition");
2627
- parseForUpdate(ctx);
3218
+ const update = parseForUpdate(ctx);
2628
3219
  expect(stream, ")", "for loop header");
2629
3220
  const body = expectCompound(ctx, "Expected '{' after for loop header");
2630
- ctx.addElem(body);
2631
3221
  ctx.popScope();
2632
- return finishBlockStatement(startPos, ctx, attributes);
3222
+ return finishStatement("for", startPos, ctx, {
3223
+ init,
3224
+ condition,
3225
+ update,
3226
+ body
3227
+ }, attributes);
2633
3228
  }
2634
3229
  /** Grammar: while_statement : attribute* 'while' expression compound_statement */
2635
3230
  function parseWhileStatement(ctx, attributes) {
2636
3231
  const startPos = beginStatement(ctx, "while", attributes);
2637
3232
  if (startPos === null) return null;
2638
- expectExpression(ctx, "Expected condition expression after 'while'");
2639
- const body = expectCompound(ctx, "Expected '{' after while condition");
2640
- ctx.addElem(body);
2641
- return finishBlockStatement(startPos, ctx, attributes);
3233
+ return finishStatement("while", startPos, ctx, {
3234
+ condition: expectExpression(ctx, "Expected condition after 'while'"),
3235
+ body: expectCompound(ctx, "Expected '{' after while condition")
3236
+ }, attributes);
2642
3237
  }
2643
3238
  /** Grammar: loop_statement : attribute* 'loop' attribute* '{' statement* continuing_statement? '}' */
2644
3239
  function parseLoopStatement(ctx, attributes) {
2645
3240
  const startPos = beginStatement(ctx, "loop", attributes);
2646
3241
  if (startPos === null) return null;
2647
3242
  const body = expectCompound(ctx, "Expected '{' after 'loop'", true);
2648
- ctx.addElem(body);
2649
- return finishBlockStatement(startPos, ctx, attributes);
3243
+ return finishStatement("loop", startPos, ctx, {
3244
+ body,
3245
+ continuing: body.body.find((s) => s.kind === "continuing")
3246
+ }, attributes);
2650
3247
  }
2651
3248
  /** Grammar: continuing_statement : 'continuing' continuing_compound_statement */
2652
3249
  function parseContinuingStatement(ctx, attributes) {
2653
- const startPos = beginStatement(ctx, "continuing", attributes, "continuing");
3250
+ const startPos = beginStatement(ctx, "continuing", attributes);
2654
3251
  if (startPos === null) return null;
2655
3252
  const body = expectCompound(ctx, "Expected '{' after 'continuing'");
2656
- ctx.addElem(body);
2657
- return finishBlockStatement(startPos, ctx, attributes, "continuing");
3253
+ return finishStatement("continuing", startPos, ctx, {
3254
+ body,
3255
+ breakIf: body.body.find((s) => s.kind === "break")?.condition
3256
+ }, attributes);
2658
3257
  }
2659
3258
  /** Grammar: for_init? ';'
2660
3259
  * for_init : variable_or_value_statement | variable_updating_statement | func_call_statement
@@ -2662,19 +3261,60 @@ function parseContinuingStatement(ctx, attributes) {
2662
3261
  function parseForInit(ctx) {
2663
3262
  const { stream } = ctx;
2664
3263
  const varDecl = parseLocalVarDecl(ctx);
2665
- if (varDecl) ctx.addElem(varDecl);
2666
- else {
2667
- const expr = parseExpression(ctx);
2668
- if (expr && ctx.options.preserveExpressions) ctx.addElem(expr);
2669
- expect(stream, ";", "for loop init");
2670
- }
3264
+ if (varDecl) return varDecl;
3265
+ const expr = parseContentExpression(ctx);
3266
+ const update = expr ? finishForUpdate(ctx, expr) : void 0;
3267
+ expect(stream, ";", "for loop init");
3268
+ return update;
2671
3269
  }
2672
- /** Grammar: for_update : variable_updating_statement | func_call_statement
2673
- * variable_updating_statement : assignment_statement | increment_statement | decrement_statement */
3270
+ /** Grammar: for_update : variable_updating_statement | func_call_statement */
2674
3271
  function parseForUpdate(ctx) {
2675
- const expr = parseExpression(ctx);
2676
- if (expr && ctx.options.preserveExpressions) ctx.addElem(expr);
2677
- parseIncDecOperator(ctx.stream) || parseAssignmentRhs(ctx);
3272
+ const expr = parseContentExpression(ctx);
3273
+ if (!expr) return void 0;
3274
+ return finishForUpdate(ctx, expr);
3275
+ }
3276
+ /**
3277
+ * Build a typed for-init/for-update node from an already-parsed lhs expression,
3278
+ * consuming its trailing operator (++/-- or an assignment + rhs).
3279
+ */
3280
+ function finishForUpdate(ctx, expr) {
3281
+ const { stream } = ctx;
3282
+ const start = expr.start;
3283
+ const incDec = parseIncDecOp(stream);
3284
+ if (incDec) {
3285
+ const end = stream.checkpoint();
3286
+ if (incDec.op === "++") return {
3287
+ kind: "increment",
3288
+ target: expr,
3289
+ start,
3290
+ end
3291
+ };
3292
+ return {
3293
+ kind: "decrement",
3294
+ target: expr,
3295
+ start,
3296
+ end
3297
+ };
3298
+ }
3299
+ const assign = parseAssignmentRhs(ctx);
3300
+ if (assign) {
3301
+ const { op, rhs } = assign;
3302
+ return {
3303
+ kind: "assign",
3304
+ lhs: expr,
3305
+ op,
3306
+ rhs,
3307
+ start,
3308
+ end: stream.checkpoint()
3309
+ };
3310
+ }
3311
+ if (expr.kind === "call-expression") return {
3312
+ kind: "call",
3313
+ call: expr,
3314
+ start,
3315
+ end: stream.checkpoint()
3316
+ };
3317
+ throwParseError(stream, "Expected an assignment, increment, decrement, or call in for-clause");
2678
3318
  }
2679
3319
  //#endregion
2680
3320
  //#region src/parse/ParseStatement.ts
@@ -2690,17 +3330,17 @@ function parseCompoundStatement(ctx, attributes, options) {
2690
3330
  const brace = ctx.stream.matchText("{");
2691
3331
  if (!brace) return null;
2692
3332
  const startPos = getStartWithAttributes(attributes, brace.span[0]);
2693
- beginElem(ctx, "statement", attributes);
2694
3333
  const skipScope = options?.noScope || hasConditionalAttr(attributes);
2695
3334
  if (!skipScope) ctx.pushScope();
2696
- parseBlockStatements(ctx, options?.loopBody);
3335
+ const body = parseBlockStatements(ctx, options?.loopBody);
2697
3336
  if (!skipScope) ctx.popScope();
2698
- return finishBlockStatement(startPos, ctx, attributes);
3337
+ return finishStatement("block", startPos, ctx, { body }, attributes);
2699
3338
  }
2700
3339
  /** Grammar: attribute* compound_statement (for control flow bodies) */
2701
3340
  function expectCompound(ctx, errorMsg, loopBody) {
2702
3341
  const attrs = parseAttributeList(ctx);
2703
- const block = parseCompoundStatement(ctx, attrs.length > 0 ? attrs : void 0, loopBody ? { loopBody } : void 0);
3342
+ const options = loopBody ? { loopBody } : void 0;
3343
+ const block = parseCompoundStatement(ctx, attrsOrUndef(attrs), options);
2704
3344
  if (!block) throwParseError(ctx.stream, errorMsg);
2705
3345
  return block;
2706
3346
  }
@@ -2708,16 +3348,20 @@ function expectCompound(ctx, errorMsg, loopBody) {
2708
3348
  function getStartWithAttributes(attributes, keywordPos) {
2709
3349
  return attributes?.[0]?.start ?? keywordPos;
2710
3350
  }
2711
- /** Match keyword and begin statement element. Returns start position or null. */
2712
- function beginStatement(ctx, keyword, attributes, kind = "statement") {
2713
- const keywordPos = ctx.stream.checkpoint();
2714
- if (!ctx.stream.matchText(keyword)) return null;
2715
- const startPos = getStartWithAttributes(attributes, keywordPos);
2716
- beginElem(ctx, kind, attributes);
2717
- return startPos;
3351
+ /** Match keyword and return the statement's start position (or null if no match). */
3352
+ function beginStatement(ctx, keyword, attributes) {
3353
+ const token = ctx.stream.matchText(keyword);
3354
+ if (!token) return null;
3355
+ return getStartWithAttributes(attributes, token.span[0]);
2718
3356
  }
2719
- function finishBlockStatement(start, ctx, attributes, kind = "statement") {
2720
- const elem = finishElem(kind, start, ctx, {});
3357
+ /** Build a statement element from its typed fields and attach its attributes. */
3358
+ function finishStatement(kind, start, ctx, params, attributes) {
3359
+ const elem = {
3360
+ kind,
3361
+ start,
3362
+ end: ctx.stream.checkpoint(),
3363
+ ...params
3364
+ };
2721
3365
  attachAttributes(elem, attributes);
2722
3366
  return elem;
2723
3367
  }
@@ -2727,16 +3371,18 @@ function hasConditionalAttr(attributes) {
2727
3371
  /** Grammar: statement* '}' (after '{' consumed). Loop bodies may end with continuing. */
2728
3372
  function parseBlockStatements(ctx, loopBody) {
2729
3373
  const { stream } = ctx;
3374
+ const body = [];
2730
3375
  while (true) {
2731
3376
  if (stream.matchText("}")) break;
2732
3377
  const stmt = parseStatement(ctx);
2733
3378
  if (!stmt) throwParseError(stream, "Expected statement or '}'");
2734
- ctx.addElem(stmt);
3379
+ body.push(stmt);
2735
3380
  if (loopBody && stmt.kind === "continuing") {
2736
3381
  expect(stream, "}", "continuing block");
2737
3382
  break;
2738
3383
  }
2739
3384
  }
3385
+ return body;
2740
3386
  }
2741
3387
  /**
2742
3388
  * Grammar: statement :
@@ -2757,7 +3403,6 @@ function parseStatement(ctx) {
2757
3403
  }
2758
3404
  const hasConditional = attributes.length > 0 && hasConditionalAttribute(attributes);
2759
3405
  if (hasConditional) ctx.pushScope("partial");
2760
- const attrsOrUndef = attributes.length > 0 ? attributes : void 0;
2761
3406
  const stmt = findMap([
2762
3407
  parseLocalVarDecl,
2763
3408
  parseLetDecl,
@@ -2771,19 +3416,71 @@ function parseStatement(ctx) {
2771
3416
  parseLoopStatement,
2772
3417
  parseContinuingStatement,
2773
3418
  parseSimpleStatement
2774
- ], (p) => p(ctx, attrsOrUndef));
2775
- if (!stmt) return null;
2776
- finalizeConditional$1(ctx, hasConditional, attributes);
2777
- return stmt;
2778
- }
2779
- function finalizeConditional$1(ctx, hasConditional, attributes) {
3419
+ ], (p) => p(ctx, attrsOrUndef(attributes)));
2780
3420
  if (hasConditional) {
2781
3421
  const partialScope = ctx.popScope();
2782
- partialScope.condAttribute = getConditionalAttribute(attributes);
3422
+ if (stmt) partialScope.condAttribute = conditionalAttribute(attributes);
2783
3423
  }
3424
+ return stmt ? stmt : null;
3425
+ }
3426
+ //#endregion
3427
+ //#region src/parse/ParseDirective.ts
3428
+ /** Grammar: global_directive : diagnostic_directive | enable_directive | requires_directive */
3429
+ function parseDirective(ctx) {
3430
+ const { stream } = ctx;
3431
+ const startPos = stream.checkpoint();
3432
+ const attrs = attrsOrUndef(parseAttributeList(ctx));
3433
+ const result = parseExtensionDirective(ctx, "enable", attrs) || parseExtensionDirective(ctx, "requires", attrs) || parseDiagnosticDirective(ctx, attrs);
3434
+ if (!result) stream.reset(startPos);
3435
+ return result;
3436
+ }
3437
+ /** Grammar: enable_directive | requires_directive : keyword extension_list ';' */
3438
+ function parseExtensionDirective(ctx, keyword, attributes) {
3439
+ const { stream } = ctx;
3440
+ const token = stream.matchText(keyword);
3441
+ if (!token) return null;
3442
+ const extensions = parseCommaList(ctx, parseDirectiveName);
3443
+ expect(stream, ";", `${keyword} directive`);
3444
+ return makeDirectiveElem({
3445
+ kind: keyword,
3446
+ extensions
3447
+ }, token, stream, attributes);
3448
+ }
3449
+ /**
3450
+ * Grammar: diagnostic_directive : 'diagnostic' diagnostic_control ';'
3451
+ * Grammar: diagnostic_control : '(' severity_control_name ',' diagnostic_rule_name ',' ? ')'
3452
+ */
3453
+ function parseDiagnosticDirective(ctx, attributes) {
3454
+ const { stream } = ctx;
3455
+ const token = stream.matchText("diagnostic");
3456
+ if (!token) return null;
3457
+ expect(stream, "(", "diagnostic");
3458
+ const severity = makeNameElem(expectWord(stream, "Expected severity in diagnostic"));
3459
+ expect(stream, ",", "diagnostic severity");
3460
+ const ruleName = makeNameElem(expectWord(stream, "Expected rule name in diagnostic"));
3461
+ let subrule = null;
3462
+ if (stream.matchText(".")) subrule = makeNameElem(expectWord(stream, "Expected subrule name after '.'"));
3463
+ stream.matchText(",");
3464
+ expect(stream, ")", "diagnostic rule");
3465
+ expect(stream, ";", "diagnostic directive");
3466
+ return makeDirectiveElem({
3467
+ kind: "diagnostic",
3468
+ severity,
3469
+ rule: [ruleName, subrule]
3470
+ }, token, stream, attributes);
3471
+ }
3472
+ function parseDirectiveName(ctx) {
3473
+ return makeNameElem(expectWord(ctx.stream, "Expected identifier in name list"));
2784
3474
  }
2785
- function getConditionalAttribute(attributes) {
2786
- return attributes.find((a) => isConditionalAttribute$1(a.attribute))?.attribute;
3475
+ function makeDirectiveElem(directive, token, stream, attributes) {
3476
+ const elem = {
3477
+ kind: "directive",
3478
+ directive,
3479
+ start: getStartWithAttributes(attributes, token.span[0]),
3480
+ end: stream.checkpoint()
3481
+ };
3482
+ attachAttributes(elem, attributes);
3483
+ return elem;
2787
3484
  }
2788
3485
  //#endregion
2789
3486
  //#region src/parse/ParseFn.ts
@@ -2820,8 +3517,7 @@ function parseFnDecl(ctx, attributes) {
2820
3517
  returnType,
2821
3518
  returnAttributes,
2822
3519
  start: startPos,
2823
- end: stream.checkpoint(),
2824
- contents: buildFnContents(attributes, declIdentElem, params, returnType, body)
3520
+ end: stream.checkpoint()
2825
3521
  };
2826
3522
  attachAttributes(fnElem, attributes);
2827
3523
  linkDeclIdentElem(declIdentElem, fnElem);
@@ -2851,37 +3547,63 @@ function parseFnReturn(ctx) {
2851
3547
  if (!returnType) throwParseError(stream, "Expected type after '->'");
2852
3548
  return {
2853
3549
  returnType,
2854
- returnAttributes: attrs.length > 0 ? attrs : void 0
3550
+ returnAttributes: attrsOrUndef(attrs)
2855
3551
  };
2856
3552
  }
2857
- /** Build contents array for function element */
2858
- function buildFnContents(attributes, decl, params, returnType, body) {
2859
- const base = returnType ? [
2860
- decl,
2861
- ...params,
2862
- returnType,
2863
- body
2864
- ] : [
2865
- decl,
2866
- ...params,
2867
- body
2868
- ];
2869
- return attributes?.length ? [...attributes, ...base] : base;
2870
- }
2871
3553
  /** Grammar: param : attribute* optionally_typed_ident */
2872
3554
  function parseFnParam(ctx) {
2873
- const attributes = parseAttributeList(ctx);
3555
+ const attrs = parseAttributeList(ctx);
2874
3556
  if (ctx.stream.peek()?.kind !== "word") return null;
2875
- beginElem(ctx, "param", attributes.length ? attributes : void 0);
3557
+ const attributes = attrsOrUndef(attrs);
2876
3558
  const name = parseTypedDecl(ctx, false);
2877
3559
  if (!name) throw new Error("Unexpected: peek succeeded but parseTypedDecl failed");
2878
- ctx.addElem(name);
2879
- const elem = finishElem("param", getStartWithAttributes(attributes, name.start), ctx, { name });
3560
+ const elem = finishStatement("param", getStartWithAttributes(attributes, name.start), ctx, { name }, attributes);
2880
3561
  linkDeclIdent(name, elem);
2881
- attachAttributes(elem, attributes.length > 0 ? attributes : void 0);
2882
3562
  return elem;
2883
3563
  }
2884
3564
  //#endregion
3565
+ //#region src/parse/ParseDoBlock.ts
3566
+ /**
3567
+ * Grammar: do_block_decl : attribute* 'do' ident '(' param_list? ')' compound_statement
3568
+ *
3569
+ * `do` is a module-level declaration when the `doBlocks` extension is on;
3570
+ * otherwise it stays a reserved word. The body is parsed structurally but
3571
+ * inside a scope that is detached from the module scope graph, so its idents
3572
+ * never reach bindIdents. do blocks are module-local, resolved later by AST
3573
+ * name match.
3574
+ */
3575
+ function parseDoBlock(ctx, attributes) {
3576
+ if (!ctx.options.weslExtensions?.doBlocks) return null;
3577
+ const { stream } = ctx;
3578
+ const doToken = stream.matchText("do");
3579
+ if (!doToken) return null;
3580
+ const startPos = getStartWithAttributes(attributes, doToken.span[0]);
3581
+ const name = makeNameElem(expectWord(stream, "Expected identifier after 'do'"));
3582
+ const parentScope = ctx.currentScope();
3583
+ ctx.pushScope();
3584
+ const doScope = ctx.currentScope();
3585
+ const params = parseFnParams(ctx);
3586
+ const body = parseFunctionBody(ctx);
3587
+ if (!body) throwParseError(stream, "Expected do block body");
3588
+ ctx.popScope();
3589
+ detachScope(parentScope, doScope);
3590
+ const doBlock = {
3591
+ kind: "do",
3592
+ name,
3593
+ params,
3594
+ body,
3595
+ start: startPos,
3596
+ end: stream.checkpoint()
3597
+ };
3598
+ attachAttributes(doBlock, attributes);
3599
+ return doBlock;
3600
+ }
3601
+ /** Remove a child scope from its parent so bindIdents never traverses it. */
3602
+ function detachScope(parent, child) {
3603
+ const i = parent.contents.indexOf(child);
3604
+ if (i >= 0) parent.contents.splice(i, 1);
3605
+ }
3606
+ //#endregion
2885
3607
  //#region src/parse/ParseImport.ts
2886
3608
  /** WESL Grammar: translation_unit : import_statement* global_directive* global_decl* */
2887
3609
  function parseWeslImports(ctx) {
@@ -3019,52 +3741,39 @@ function parseStructDecl(ctx, attributes) {
3019
3741
  const start = getStartWithAttributes(attributes, structToken.span[0]);
3020
3742
  const identElem = createDeclIdentElem(ctx, expectWord(stream, "Expected identifier after 'struct'"), true);
3021
3743
  ctx.saveIdent(identElem.ident);
3022
- beginElem(ctx, "struct", attributes);
3023
- ctx.addElem(identElem);
3024
3744
  expect(stream, "{", "struct name");
3025
3745
  ctx.pushScope();
3026
- const members = parseStructMembers(ctx);
3746
+ const members = parseCommaList(ctx, parseStructMember);
3027
3747
  identElem.ident.dependentScope = ctx.currentScope();
3028
3748
  ctx.popScope();
3029
3749
  expect(stream, "}", "struct member");
3030
- const elem = finishElem("struct", start, ctx, {
3750
+ const elem = finishStatement("struct", start, ctx, {
3031
3751
  name: identElem,
3032
3752
  members
3033
- });
3034
- attachAttributes(elem, attributes);
3753
+ }, attributes);
3035
3754
  linkDeclIdentElem(identElem, elem);
3036
3755
  return elem;
3037
3756
  }
3038
- /** Grammar: struct_body_decl : '{' struct_member (',' struct_member)* ','? '}' */
3039
- function parseStructMembers(ctx) {
3040
- const members = parseCommaList(ctx, parseStructMember);
3041
- for (const member of members) ctx.addElem(member);
3042
- return members;
3043
- }
3044
3757
  /** Grammar: struct_member : attribute* member_ident ':' type_specifier */
3045
3758
  function parseStructMember(ctx) {
3046
3759
  const { stream } = ctx;
3047
3760
  const checkpoint = stream.checkpoint();
3048
- const attributes = parseAttributeList(ctx);
3761
+ const attrs = parseAttributeList(ctx);
3049
3762
  const nameToken = stream.matchKind("word");
3050
3763
  if (!nameToken) {
3051
3764
  stream.reset(checkpoint);
3052
3765
  return null;
3053
3766
  }
3767
+ const attributes = attrsOrUndef(attrs);
3054
3768
  const start = getStartWithAttributes(attributes, nameToken.span[0]);
3055
- beginElem(ctx, "member", attributes.length ? attributes : void 0);
3056
3769
  const name = makeNameElem(nameToken);
3057
- ctx.addElem(name);
3058
3770
  expect(stream, ":", "struct member name");
3059
3771
  const typeRef = parseSimpleTypeRef(ctx);
3060
3772
  if (!typeRef) throwParseError(stream, "Expected type after ':'");
3061
- ctx.addElem(typeRef);
3062
- const elem = finishElem("member", start, ctx, {
3773
+ return finishStatement("member", start, ctx, {
3063
3774
  name,
3064
3775
  typeRef
3065
- });
3066
- attachAttributes(elem, attributes.length ? attributes : void 0);
3067
- return elem;
3776
+ }, attributes);
3068
3777
  }
3069
3778
  //#endregion
3070
3779
  //#region src/parse/ParseModule.ts
@@ -3075,6 +3784,7 @@ const declParsers = [
3075
3784
  parseAliasDecl,
3076
3785
  parseStructDecl,
3077
3786
  parseFnDecl,
3787
+ parseDoBlock,
3078
3788
  parseConstAssert
3079
3789
  ];
3080
3790
  /** Grammar: translation_unit : global_directive* ( global_decl | global_assert | ';' )* */
@@ -3082,19 +3792,39 @@ function parseModule(ctx) {
3082
3792
  parseImports(ctx);
3083
3793
  parseDirectives(ctx);
3084
3794
  while (parseNextDeclaration(ctx));
3795
+ if (ctx.stream.peek() !== null) throwParseError(ctx.stream, "Expected a declaration or directive");
3796
+ }
3797
+ /**
3798
+ * Reject a module that gives a `do` block a name that clashes with a fn/global
3799
+ * or with another `do` block (`do` blocks share the module's declaration
3800
+ * namespace, and runners key blocks by name so a duplicate would silently
3801
+ * shadow the earlier one). This is a small module-local pass, deliberately not
3802
+ * part of bindIdents.
3803
+ */
3804
+ function checkDoBlockNames(moduleElem) {
3805
+ const doBlocks = declsOfKind(moduleElem, "do");
3806
+ if (doBlocks.length === 0) return;
3807
+ const declNames = new Set(moduleElem.decls.map(globalDeclName));
3808
+ const seen = /* @__PURE__ */ new Set();
3809
+ for (const block of doBlocks) {
3810
+ const { name, start, end } = block.name;
3811
+ if (declNames.has(name)) throw new ParseError(`'${name}' declared as both fn and do`, [start, end]);
3812
+ if (seen.has(name)) throw new ParseError(`'${name}' declared as do more than once`, [start, end]);
3813
+ seen.add(name);
3814
+ }
3085
3815
  }
3086
3816
  /** Parse WESL import statements at the start of the module. */
3087
3817
  function parseImports(ctx) {
3088
3818
  const importElems = parseWeslImports(ctx);
3089
3819
  for (const importElem of importElems) {
3090
- ctx.addElem(importElem);
3820
+ ctx.addModuleDecl(importElem);
3091
3821
  ctx.state.stable.imports.push(importElem.imports);
3092
3822
  }
3093
3823
  }
3094
3824
  /** Grammar: global_directive : diagnostic_directive | enable_directive | requires_directive */
3095
3825
  function parseDirectives(ctx) {
3096
3826
  const directives = parseMany(ctx, parseDirective);
3097
- for (const elem of directives) ctx.addElem(elem);
3827
+ for (const elem of directives) ctx.addModuleDecl(elem);
3098
3828
  }
3099
3829
  /** Parse one declaration, return true if more may exist. */
3100
3830
  function parseNextDeclaration(ctx) {
@@ -3109,34 +3839,40 @@ function parseNextDeclaration(ctx) {
3109
3839
  if (attrs.length) throwParseError(stream, "Expected declaration after attributes");
3110
3840
  return false;
3111
3841
  }
3842
+ /** @return the declared name of a module-level declaration, if it has one. */
3843
+ function globalDeclName(elem) {
3844
+ switch (elem.kind) {
3845
+ case "fn":
3846
+ case "struct":
3847
+ case "alias": return elem.name.ident.originalName;
3848
+ case "gvar":
3849
+ case "const":
3850
+ case "override": return elem.name.decl.ident.originalName;
3851
+ default: return;
3852
+ }
3853
+ }
3112
3854
  /** Try each declaration parser until one succeeds. */
3113
3855
  function parseDecl(ctx, attrs) {
3114
- const attrsOrUndef = attrs.length ? attrs : void 0;
3115
- const elem = findMap(declParsers, (p) => p(ctx, attrsOrUndef));
3116
- if (elem) {
3117
- recordDecl(ctx, elem, attrs);
3118
- return true;
3119
- }
3120
- return false;
3856
+ const elem = findMap(declParsers, (p) => p(ctx, attrsOrUndef(attrs)));
3857
+ if (!elem) return false;
3858
+ recordDecl(ctx, elem, attrs);
3859
+ return true;
3121
3860
  }
3122
3861
  /** Pop conditional scope and attach the conditional attribute. */
3123
3862
  function finalizeConditional(ctx, attrs) {
3124
3863
  const partialScope = ctx.popScope();
3125
- partialScope.condAttribute = findMap(attrs, ({ attribute }) => isConditionalAttribute(attribute) ? attribute : void 0);
3864
+ partialScope.condAttribute = conditionalAttribute(attrs);
3126
3865
  }
3127
3866
  /** Record a parsed declaration, extending start to include attributes. */
3128
3867
  function recordDecl(ctx, elem, attrs) {
3129
3868
  if (attrs.length && elem.start > attrs[0].start) elem.start = attrs[0].start;
3130
- ctx.addElem(elem);
3869
+ ctx.addModuleDecl(elem);
3131
3870
  if (elem.kind === "assert") {
3132
3871
  const { stable } = ctx.state;
3133
3872
  stable.moduleAsserts ??= [];
3134
3873
  stable.moduleAsserts.push(elem);
3135
3874
  }
3136
3875
  }
3137
- function isConditionalAttribute(a) {
3138
- return a.kind === "@if" || a.kind === "@elif" || a.kind === "@else";
3139
- }
3140
3876
  //#endregion
3141
3877
  //#region src/parse/ParsingContext.ts
3142
3878
  /** Context for parsers to build AST and manage scopes. */
@@ -3159,9 +3895,9 @@ var ParsingContext = class {
3159
3895
  currentScope() {
3160
3896
  return this.state.context.scope;
3161
3897
  }
3162
- addElem(elem) {
3163
- const { openElems } = this.state.context;
3164
- if (openElems.length > 0) openElems[openElems.length - 1].contents.push(elem);
3898
+ /** Append a top-level declaration to the module, in source order. */
3899
+ addModuleDecl(elem) {
3900
+ this.state.stable.moduleElem.decls.push(elem);
3165
3901
  }
3166
3902
  pushScope(kind = "scope") {
3167
3903
  const { scope } = this.state.context;
@@ -3229,55 +3965,6 @@ const reservedWords = `NULL Self abstract active alignas alignof as asm asm_frag
3229
3965
  target template this thread_local throw trait try type typedef typeid typename typeof
3230
3966
  union unless unorm unsafe unsized use using varying virtual volatile wgsl where with writeonly yield`.split(/\s+/);
3231
3967
  //#endregion
3232
- //#region src/parse/stream/CachingStream.ts
3233
- var CachingStream = class {
3234
- cache = new Cache(5);
3235
- inner;
3236
- constructor(inner) {
3237
- this.inner = inner;
3238
- }
3239
- checkpoint() {
3240
- return this.inner.checkpoint();
3241
- }
3242
- reset(position) {
3243
- this.inner.reset(position);
3244
- }
3245
- nextToken() {
3246
- const startPos = this.checkpoint();
3247
- const cachedValue = this.cache.get(startPos);
3248
- if (cachedValue !== void 0) {
3249
- this.reset(cachedValue.checkpoint);
3250
- return cachedValue.token;
3251
- } else {
3252
- const token = this.inner.nextToken();
3253
- const checkpoint = this.checkpoint();
3254
- this.cache.set(startPos, {
3255
- token,
3256
- checkpoint
3257
- });
3258
- return token;
3259
- }
3260
- }
3261
- get src() {
3262
- return this.inner.src;
3263
- }
3264
- };
3265
- /** size limited key value cache */
3266
- var Cache = class extends Map {
3267
- max;
3268
- constructor(max) {
3269
- super();
3270
- this.max = max;
3271
- }
3272
- set(k, v) {
3273
- if (this.size > this.max) {
3274
- const first = this.keys().next().value;
3275
- if (first) this.delete(first);
3276
- }
3277
- return super.set(k, v);
3278
- }
3279
- };
3280
- //#endregion
3281
3968
  //#region src/parse/stream/RegexHelpers.ts
3282
3969
  function toRegexSource(nameExp) {
3283
3970
  const [name, e] = nameExp;
@@ -3307,33 +3994,10 @@ function matchOneOf(syms) {
3307
3994
  return new RegExp(escaped.join("|"));
3308
3995
  }
3309
3996
  //#endregion
3310
- //#region src/parse/stream/MatchersStream.ts
3311
- /** Runs a `RegexMatchers` on an input string */
3312
- var MatchersStream = class {
3313
- position = 0;
3314
- text;
3315
- matchers;
3316
- constructor(text, matchers) {
3317
- this.text = text;
3318
- this.matchers = matchers;
3319
- }
3320
- checkpoint() {
3321
- return this.position;
3322
- }
3323
- reset(position) {
3324
- this.position = position;
3325
- }
3326
- nextToken() {
3327
- const result = this.matchers.execAt(this.text, this.position);
3328
- if (result === null) return null;
3329
- this.position = result.span[1];
3330
- return result;
3331
- }
3332
- get src() {
3333
- return this.text;
3334
- }
3335
- };
3997
+ //#region src/parse/stream/RegexMatchers.ts
3336
3998
  /**
3999
+ * Matches tokens by kind with one combined regex.
4000
+ *
3337
4001
  * The matchers passed to this object must follow certain rules:
3338
4002
  * - They must use non-capturing groups: `(?:...)`
3339
4003
  * - They must NOT use `^` or `$`
@@ -3344,55 +4008,230 @@ var RegexMatchers = class {
3344
4008
  constructor(matchers) {
3345
4009
  this.groups = Object.keys(matchers);
3346
4010
  const expParts = Object.entries(matchers).map(toRegexSource).join("|");
3347
- this.exp = new RegExp(expParts, "dyu");
4011
+ this.exp = new RegExp(expParts, "yu");
3348
4012
  }
3349
4013
  execAt(text, position) {
3350
4014
  this.exp.lastIndex = position;
3351
- const matchedIndex = findGroupDex(this.exp.exec(text)?.indices);
3352
- if (matchedIndex) {
3353
- const { span, groupDex } = matchedIndex;
3354
- return {
3355
- kind: this.groups[groupDex],
3356
- span,
3357
- text: text.slice(span[0], span[1])
3358
- };
3359
- } else return null;
3360
- }
3361
- };
3362
- function findGroupDex(indices) {
3363
- if (indices !== void 0) for (let i = 1; i < indices.length; i++) {
3364
- const span = indices[i];
3365
- if (span !== void 0) return {
4015
+ const matches = this.exp.exec(text);
4016
+ if (matches === null) return null;
4017
+ const matched = matches[0];
4018
+ const span = [position, position + matched.length];
4019
+ return {
4020
+ kind: this.groups[matchedGroupIndex(matches)],
3366
4021
  span,
3367
- groupDex: i - 1
4022
+ text: matched
3368
4023
  };
3369
4024
  }
4025
+ };
4026
+ /** @return index of the alternation group that matched */
4027
+ function matchedGroupIndex(matches) {
4028
+ for (let i = 1; i < matches.length; i++) if (matches[i] !== void 0) return i - 1;
4029
+ throw new Error("no matching group");
3370
4030
  }
3371
4031
  //#endregion
3372
- //#region src/parse/WeslStream.ts
4032
+ //#region src/parse/stream/WeslLexer.ts
4033
+ const ident = /(?:(?:[_\p{XID_Start}][\p{XID_Continue}]+)|(?:[\p{XID_Start}]))/u;
3373
4034
  /** Whitespaces including new lines */
3374
4035
  const blankspaces = /[ \t\n\v\f\r\u{0085}\u{200E}\u{200F}\u{2028}\u{2029}]+/u;
3375
- const symbolSet = "& && -> @ / ! [ ] { } :: : , == = != >>= >> >= > <<= << <= < % - -- . + ++ | || ( ) ; * ~ ^ // /* */ += -= *= /= %= &= |= ^= _";
3376
- const ident = /(?:(?:[_\p{XID_Start}][\p{XID_Continue}]+)|(?:[\p{XID_Start}]))/u;
3377
- const keywordOrReserved = new Set(keywords.concat(reservedWords));
3378
- const weslMatcher = new RegexMatchers({
4036
+ 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);
4037
+ /** Unicode-aware fallback for characters the ASCII fast path can't handle.
4038
+ * lastIndex is set on every use, so sharing one instance is safe. */
4039
+ const unicodeMatcher = new RegexMatchers({
3379
4040
  word: ident,
3380
- 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),
4041
+ number: digits,
3381
4042
  blankspaces,
3382
4043
  commentStart: /\/\/|\/\*/,
3383
- symbol: matchOneOf(symbolSet),
4044
+ symbol: matchOneOf("& && -> @ / ! [ ] { } :: : , == = != >>= >> >= > <<= << <= < % - -- . + ++ | || ( ) ; * ~ ^ // /* */ += -= *= /= %= &= |= ^= _"),
3384
4045
  invalid: /[^]/
3385
4046
  });
4047
+ /**
4048
+ * Hand-written scanner for WESL/WGSL raw tokens.
4049
+ *
4050
+ * The common case is dispatched on ASCII char codes: whitespace is skipped
4051
+ * without producing tokens, and idents, numbers, symbols, and comment openers
4052
+ * are matched directly. Any character >= 0x80 (unicode idents, unicode
4053
+ * blankspace) falls back to the unicode-aware regex, so there is no up-front
4054
+ * scan and no unicode restriction.
4055
+ */
4056
+ var WeslLexer = class {
4057
+ position = 0;
4058
+ /** sticky number matcher (stateful via lastIndex, so kept per-instance) */
4059
+ numberExp = new RegExp(digits.source, "y");
4060
+ src;
4061
+ constructor(src) {
4062
+ this.src = src;
4063
+ }
4064
+ checkpoint() {
4065
+ return this.position;
4066
+ }
4067
+ reset(position) {
4068
+ this.position = position;
4069
+ }
4070
+ nextToken() {
4071
+ const { src } = this;
4072
+ const len = src.length;
4073
+ let pos = this.position;
4074
+ while (pos < len) {
4075
+ const c = src.charCodeAt(pos);
4076
+ if (c === 32 || c >= 9 && c <= 13) pos++;
4077
+ else break;
4078
+ }
4079
+ if (pos >= len) {
4080
+ this.position = pos;
4081
+ return null;
4082
+ }
4083
+ const c = src.charCodeAt(pos);
4084
+ if (c >= 97 && c <= 122 || c >= 65 && c <= 90) return this.word(pos);
4085
+ if (c >= 48 && c <= 57) return this.number(pos);
4086
+ const n = src.charCodeAt(pos + 1);
4087
+ switch (c) {
4088
+ case 95:
4089
+ if (isWordChar(n)) return this.word(pos);
4090
+ if (n >= 128) return this.regexToken(pos);
4091
+ return this.symbol(pos, "_");
4092
+ case 47:
4093
+ if (n === 47) return this.token("commentStart", pos, "//");
4094
+ if (n === 42) return this.token("commentStart", pos, "/*");
4095
+ if (n === 61) return this.symbol(pos, "/=");
4096
+ return this.symbol(pos, "/");
4097
+ case 46:
4098
+ if (n >= 48 && n <= 57) return this.number(pos);
4099
+ return this.symbol(pos, ".");
4100
+ case 62:
4101
+ if (n === 62) {
4102
+ if (src.charCodeAt(pos + 2) === 61) return this.symbol(pos, ">>=");
4103
+ return this.symbol(pos, ">>");
4104
+ }
4105
+ if (n === 61) return this.symbol(pos, ">=");
4106
+ return this.symbol(pos, ">");
4107
+ case 60:
4108
+ if (n === 60) {
4109
+ if (src.charCodeAt(pos + 2) === 61) return this.symbol(pos, "<<=");
4110
+ return this.symbol(pos, "<<");
4111
+ }
4112
+ if (n === 61) return this.symbol(pos, "<=");
4113
+ return this.symbol(pos, "<");
4114
+ case 38:
4115
+ if (n === 38) return this.symbol(pos, "&&");
4116
+ if (n === 61) return this.symbol(pos, "&=");
4117
+ return this.symbol(pos, "&");
4118
+ case 124:
4119
+ if (n === 124) return this.symbol(pos, "||");
4120
+ if (n === 61) return this.symbol(pos, "|=");
4121
+ return this.symbol(pos, "|");
4122
+ case 45:
4123
+ if (n === 62) return this.symbol(pos, "->");
4124
+ if (n === 45) return this.symbol(pos, "--");
4125
+ if (n === 61) return this.symbol(pos, "-=");
4126
+ return this.symbol(pos, "-");
4127
+ case 43:
4128
+ if (n === 43) return this.symbol(pos, "++");
4129
+ if (n === 61) return this.symbol(pos, "+=");
4130
+ return this.symbol(pos, "+");
4131
+ case 58:
4132
+ if (n === 58) return this.symbol(pos, "::");
4133
+ return this.symbol(pos, ":");
4134
+ case 61:
4135
+ if (n === 61) return this.symbol(pos, "==");
4136
+ return this.symbol(pos, "=");
4137
+ case 33:
4138
+ if (n === 61) return this.symbol(pos, "!=");
4139
+ return this.symbol(pos, "!");
4140
+ case 42:
4141
+ if (n === 47) return this.symbol(pos, "*/");
4142
+ if (n === 61) return this.symbol(pos, "*=");
4143
+ return this.symbol(pos, "*");
4144
+ case 37:
4145
+ if (n === 61) return this.symbol(pos, "%=");
4146
+ return this.symbol(pos, "%");
4147
+ case 94:
4148
+ if (n === 61) return this.symbol(pos, "^=");
4149
+ return this.symbol(pos, "^");
4150
+ case 64: return this.symbol(pos, "@");
4151
+ case 91: return this.symbol(pos, "[");
4152
+ case 93: return this.symbol(pos, "]");
4153
+ case 123: return this.symbol(pos, "{");
4154
+ case 125: return this.symbol(pos, "}");
4155
+ case 40: return this.symbol(pos, "(");
4156
+ case 41: return this.symbol(pos, ")");
4157
+ case 44: return this.symbol(pos, ",");
4158
+ case 59: return this.symbol(pos, ";");
4159
+ case 126: return this.symbol(pos, "~");
4160
+ }
4161
+ if (c >= 128) return this.regexToken(pos);
4162
+ this.position = pos + 1;
4163
+ return {
4164
+ kind: "invalid",
4165
+ span: [pos, pos + 1],
4166
+ text: src[pos]
4167
+ };
4168
+ }
4169
+ token(kind, start, text) {
4170
+ const end = start + text.length;
4171
+ this.position = end;
4172
+ return {
4173
+ kind,
4174
+ span: [start, end],
4175
+ text
4176
+ };
4177
+ }
4178
+ symbol(start, text) {
4179
+ return this.token("symbol", start, text);
4180
+ }
4181
+ word(start) {
4182
+ const { src } = this;
4183
+ const len = src.length;
4184
+ let pos = start + 1;
4185
+ while (pos < len) {
4186
+ const c = src.charCodeAt(pos);
4187
+ if (isWordChar(c)) pos++;
4188
+ else if (c >= 128) return this.regexToken(start);
4189
+ else break;
4190
+ }
4191
+ this.position = pos;
4192
+ return {
4193
+ kind: "word",
4194
+ span: [start, pos],
4195
+ text: src.slice(start, pos)
4196
+ };
4197
+ }
4198
+ number(start) {
4199
+ this.numberExp.lastIndex = start;
4200
+ const text = this.numberExp.exec(this.src)[0];
4201
+ return this.token("number", start, text);
4202
+ }
4203
+ /** match one token (or blankspace run) with the unicode-aware regex */
4204
+ regexToken(start) {
4205
+ const token = unicodeMatcher.execAt(this.src, start);
4206
+ if (token === null) return null;
4207
+ this.position = token.span[1];
4208
+ return token;
4209
+ }
4210
+ };
4211
+ function isWordChar(c) {
4212
+ return c >= 97 && c <= 122 || c >= 65 && c <= 90 || c >= 48 && c <= 57 || c === 95;
4213
+ }
4214
+ //#endregion
4215
+ //#region src/parse/WeslStream.ts
4216
+ /** One line break, treating \r\n as a single break.
4217
+ * Same code points as `isLineBreak` in AttachComments.ts (charCode form). */
4218
+ const lineBreak = String.raw`\r\n?|[\n\v\f\u{0085}\u{2028}\u{2029}]`;
4219
+ const keywordOrReserved = new Set(keywords.concat(reservedWords));
3386
4220
  /** A stream that produces WESL tokens, skipping over comments and white space */
3387
4221
  var WeslStream = class {
3388
4222
  stream;
3389
- /** New line */
3390
- eolPattern = /[\n\v\f\u{0085}\u{2028}\u{2029}]|\r\n?/gu;
4223
+ /** New line (stateful: scanned via lastIndex, so kept per-instance). */
4224
+ eolPattern = new RegExp(lineBreak, "gu");
3391
4225
  blockCommentPattern = /\/\*|\*\//g;
4226
+ /** Comments skipped before a real token, keyed by that token's start position. */
4227
+ triviaByPos = /* @__PURE__ */ new Map();
4228
+ /** Last peeked token, so the following nextToken() skips the rescan.
4229
+ * Never invalidated: tokenization is deterministic per position. */
4230
+ peeked = null;
3392
4231
  src;
3393
4232
  constructor(src) {
3394
4233
  this.src = src;
3395
- this.stream = new CachingStream(new MatchersStream(src, weslMatcher));
4234
+ this.stream = new WeslLexer(src);
3396
4235
  }
3397
4236
  checkpoint() {
3398
4237
  return this.stream.checkpoint();
@@ -3400,27 +4239,79 @@ var WeslStream = class {
3400
4239
  reset(position) {
3401
4240
  this.stream.reset(position);
3402
4241
  }
4242
+ /** All recorded comment runs (each a contiguous group of comments between two
4243
+ * real tokens), in source order. Consumed by the post-parse comment pass. */
4244
+ commentRuns() {
4245
+ return [...this.triviaByPos.entries()].sort((a, b) => a[0] - b[0]).map(([, run]) => run);
4246
+ }
4247
+ recordTrivia(pos, pending) {
4248
+ if (pending) this.triviaByPos.set(pos, pending);
4249
+ }
4250
+ /** Next real token (comments/blankspace skipped and recorded as trivia); null at EOF. */
3403
4251
  nextToken() {
4252
+ const peeked = this.usePeeked();
4253
+ if (peeked !== null) return peeked.token;
4254
+ let pending;
3404
4255
  while (true) {
3405
4256
  const token = this.stream.nextToken();
3406
- if (token === null) return null;
4257
+ if (token === null) {
4258
+ this.recordTrivia(this.src.length, pending);
4259
+ return null;
4260
+ }
3407
4261
  const kind = token.kind;
3408
4262
  if (kind === "blankspaces") continue;
3409
- else if (kind === "commentStart") if (token.text === "//") this.stream.reset(this.skipToEol(token.span[1]));
3410
- else this.stream.reset(this.skipBlockComment(token.span[1]));
3411
- else if (kind === "word") {
3412
- const returnToken = token;
3413
- if (keywordOrReserved.has(token.text)) returnToken.kind = "keyword";
3414
- return returnToken;
4263
+ else if (kind === "commentStart") {
4264
+ pending ??= [];
4265
+ pending.push(this.consumeComment(token));
3415
4266
  } else if (kind === "invalid") throw new ParseError("Invalid token " + token.text, token.span);
3416
- else return token;
4267
+ else {
4268
+ this.recordTrivia(token.span[0], pending);
4269
+ const result = token;
4270
+ if (kind === "word" && keywordOrReserved.has(token.text)) result.kind = "keyword";
4271
+ return result;
4272
+ }
4273
+ }
4274
+ }
4275
+ /** If the last peek() was at the current position, consume and return it. */
4276
+ usePeeked() {
4277
+ const peeked = this.peeked;
4278
+ if (peeked !== null && peeked.pos === this.checkpoint()) {
4279
+ this.reset(peeked.end);
4280
+ return peeked;
4281
+ }
4282
+ return null;
4283
+ }
4284
+ /** Skip a comment (rejecting embedded \0), advance the stream past it,
4285
+ * and return it as trivia. */
4286
+ consumeComment(token) {
4287
+ const style = token.text === "//" ? "line" : "block";
4288
+ const start = token.span[0];
4289
+ const end = this.commentEnd(token);
4290
+ const bodyNull = this.src.slice(start, end).indexOf("\0");
4291
+ if (bodyNull >= 0) {
4292
+ const at = start + bodyNull;
4293
+ throw new ParseError("Invalid token \\0", [at, at + 1]);
3417
4294
  }
4295
+ this.stream.reset(end);
4296
+ return {
4297
+ style,
4298
+ start,
4299
+ end
4300
+ };
3418
4301
  }
3419
4302
  /** Peek at the next token without consuming it */
3420
4303
  peek() {
3421
4304
  const pos = this.checkpoint();
4305
+ const peeked = this.peeked;
4306
+ if (peeked !== null && peeked.pos === pos) return peeked.token;
3422
4307
  const token = this.nextToken();
4308
+ const end = this.checkpoint();
3423
4309
  this.reset(pos);
4310
+ this.peeked = {
4311
+ pos,
4312
+ token,
4313
+ end
4314
+ };
3424
4315
  return token;
3425
4316
  }
3426
4317
  /** Consume token if text matches, otherwise leave position unchanged */
@@ -3464,10 +4355,15 @@ var WeslStream = class {
3464
4355
  }
3465
4356
  return tokens;
3466
4357
  }
3467
- skipToEol(position) {
4358
+ /** End position of a comment opened by a commentStart token. */
4359
+ commentEnd(token) {
4360
+ return token.text === "//" ? this.lineCommentEnd(token.span[1]) : this.skipBlockComment(token.span[1]);
4361
+ }
4362
+ /** End of a line comment: the start of the next line break (or end of file). */
4363
+ lineCommentEnd(position) {
3468
4364
  this.eolPattern.lastIndex = position;
3469
- if (this.eolPattern.exec(this.src) === null) return this.src.length;
3470
- else return this.eolPattern.lastIndex;
4365
+ const result = this.eolPattern.exec(this.src);
4366
+ return result === null ? this.src.length : result.index;
3471
4367
  }
3472
4368
  skipBlockComment(start) {
3473
4369
  let position = start;
@@ -3489,39 +4385,46 @@ var WeslStream = class {
3489
4385
  const startPosition = this.stream.checkpoint();
3490
4386
  const token = this.nextToken();
3491
4387
  this.stream.reset(startPosition);
3492
- if (token === null) return null;
3493
- if (token.kind !== "symbol") return null;
3494
- if (token.text === "<") if (this.isTemplateStart(token.span[1])) {
3495
- this.stream.reset(token.span[1]);
3496
- return token;
3497
- } else {
4388
+ if (token === null || token.kind !== "symbol" || token.text !== "<") return null;
4389
+ if (!this.isTemplateStart(token.span[1])) {
3498
4390
  this.stream.reset(startPosition);
3499
4391
  return null;
3500
4392
  }
3501
- else return null;
4393
+ this.stream.reset(token.span[1]);
4394
+ return token;
3502
4395
  }
4396
+ /** Match a template-closing `>`, splitting it off a `>>`/`>=`/`>>=` token when needed. */
3503
4397
  nextTemplateEndToken() {
3504
4398
  const startPosition = this.stream.checkpoint();
3505
4399
  const token = this.nextToken();
3506
4400
  this.stream.reset(startPosition);
3507
4401
  if (token === null) return null;
3508
- if (token.kind === "symbol" && token.text[0] === ">") {
3509
- const tokenPosition = token.span[0];
3510
- this.stream.reset(tokenPosition + 1);
3511
- return {
3512
- kind: "symbol",
3513
- span: [tokenPosition, tokenPosition + 1],
3514
- text: ">"
3515
- };
3516
- } else return null;
4402
+ if (token.kind !== "symbol" || token.text[0] !== ">") return null;
4403
+ const tokenPosition = token.span[0];
4404
+ this.stream.reset(tokenPosition + 1);
4405
+ return {
4406
+ kind: "symbol",
4407
+ span: [tokenPosition, tokenPosition + 1],
4408
+ text: ">"
4409
+ };
3517
4410
  }
4411
+ /** Next symbol from the raw stream, skipping comment bodies (so symbols
4412
+ * inside comments don't count) and non-symbol tokens; null at EOF. */
4413
+ nextRawSymbol() {
4414
+ while (true) {
4415
+ const token = this.stream.nextToken();
4416
+ if (token === null) return null;
4417
+ if (token.kind === "commentStart") this.stream.reset(this.commentEnd(token));
4418
+ else if (token.kind === "symbol") return token;
4419
+ }
4420
+ }
4421
+ /** Template-list discovery: scan forward from `<` for a balanced closing `>`. */
3518
4422
  isTemplateStart(afterToken) {
3519
4423
  this.stream.reset(afterToken);
3520
4424
  let pendingCounter = 1;
3521
4425
  while (true) {
3522
- const nextToken = this.stream.nextToken();
4426
+ const nextToken = this.nextRawSymbol();
3523
4427
  if (nextToken === null) return false;
3524
- if (nextToken.kind !== "symbol") continue;
3525
4428
  if (nextToken.text === "<") pendingCounter += 1;
3526
4429
  else if (nextToken.text[0] === ">") {
3527
4430
  if (nextToken.text === ">" || nextToken.text === ">=") pendingCounter -= 1;
@@ -3539,12 +4442,11 @@ var WeslStream = class {
3539
4442
  */
3540
4443
  skipBracketsTo(closingBracket) {
3541
4444
  while (true) {
3542
- const nextToken = this.stream.nextToken();
4445
+ const nextToken = this.nextRawSymbol();
3543
4446
  if (nextToken === null) {
3544
4447
  const after = this.stream.checkpoint();
3545
4448
  throw new ParseError("Unclosed bracket!", [after, after]);
3546
4449
  }
3547
- if (nextToken.kind !== "symbol") continue;
3548
4450
  if (nextToken.text === "(") this.skipBracketsTo(")");
3549
4451
  else if (nextToken.text === "[") this.skipBracketsTo("]");
3550
4452
  else if (nextToken.text === closingBracket) return;
@@ -3557,10 +4459,10 @@ var WeslStream = class {
3557
4459
  function parseWesl(srcModule, options) {
3558
4460
  const { ctx, state } = createParseState(srcModule, options);
3559
4461
  try {
3560
- beginElem(ctx, "module");
3561
4462
  parseModule(ctx);
3562
- const moduleElem = state.stable.moduleElem;
3563
- moduleElem.contents = finishContents(ctx, 0, moduleElem.end);
4463
+ const { moduleElem } = state.stable;
4464
+ attachComments(ctx, moduleElem);
4465
+ checkDoBlockNames(moduleElem);
3564
4466
  return state.stable;
3565
4467
  } catch (e) {
3566
4468
  if (e instanceof ParseError) throw new WeslParseError({
@@ -3574,29 +4476,27 @@ function parseWesl(srcModule, options) {
3574
4476
  }
3575
4477
  }
3576
4478
  /** Initialize parse state: token stream, root scope, and module element. */
3577
- function createParseState(srcModule, options) {
4479
+ function createParseState(srcModule, parseOptions) {
3578
4480
  const stream = new WeslStream(srcModule.src);
3579
4481
  const rootScope = emptyScope(null);
3580
4482
  const moduleElem = {
3581
4483
  kind: "module",
3582
- contents: [],
4484
+ decls: [],
3583
4485
  start: 0,
3584
4486
  end: srcModule.src.length
3585
4487
  };
3586
4488
  const state = {
3587
- context: {
3588
- scope: rootScope,
3589
- openElems: []
3590
- },
4489
+ context: { scope: rootScope },
3591
4490
  stable: {
3592
4491
  srcModule,
3593
4492
  moduleElem,
3594
4493
  rootScope,
3595
- imports: []
4494
+ imports: [],
4495
+ parseOptions
3596
4496
  }
3597
4497
  };
3598
4498
  return {
3599
- ctx: new ParsingContext(stream, state, options),
4499
+ ctx: new ParsingContext(stream, state, parseOptions),
3600
4500
  state
3601
4501
  };
3602
4502
  }
@@ -3607,14 +4507,14 @@ var WeslParseError = class extends Error {
3607
4507
  span;
3608
4508
  src;
3609
4509
  constructor(opts) {
3610
- const source = opts.src.src;
3611
- const [lineNum, linePos] = offsetToLineNumber(opts.cause.span[0], source);
3612
- let message = `${opts.src.debugFilePath}:${lineNum}:${linePos}`;
3613
- message += ` error: ${opts.cause.message}\n`;
3614
- message += errorHighlight(source, opts.cause.span).join("\n");
3615
- super(message, { cause: opts.cause });
3616
- this.span = opts.cause.span;
3617
- this.src = opts.src;
4510
+ const { cause, src } = opts;
4511
+ const source = src.src;
4512
+ const [lineNum, linePos] = offsetToLineNumber(cause.span[0], source);
4513
+ const highlight = errorHighlight(source, cause.span).join("\n");
4514
+ const message = `${src.debugFilePath}:${lineNum}:${linePos} error: ${cause.message}\n${highlight}`;
4515
+ super(message, { cause });
4516
+ this.span = cause.span;
4517
+ this.src = src;
3618
4518
  }
3619
4519
  };
3620
4520
  /** Parse a WESL file. */
@@ -3624,8 +4524,8 @@ function parseSrcModule(srcModule, options) {
3624
4524
  /** @return flattened form of import tree for binding idents. */
3625
4525
  function flatImports(ast, conditions) {
3626
4526
  if (ast._flatImports && !conditions) return ast._flatImports;
3627
- const importElems = ast.moduleElem.contents.filter((elem) => elem.kind === "import");
3628
- const flat = (conditions ? filterValidElements(importElems, conditions) : importElems).map((elem) => elem.imports).flatMap(flattenTreeImport);
4527
+ const importElems = declsOfKind(ast.moduleElem, "import");
4528
+ const flat = (conditions ? filterValidElements(importElems, conditions) : importElems).flatMap((elem) => flattenTreeImport(elem.imports));
3629
4529
  if (!conditions) ast._flatImports = flat;
3630
4530
  return flat;
3631
4531
  }
@@ -3922,11 +4822,13 @@ var RecordResolver = class {
3922
4822
  sources;
3923
4823
  packageName;
3924
4824
  debugWeslRoot;
4825
+ weslExtensions;
3925
4826
  constructor(sources, options = {}) {
3926
- const { packageName = "package", debugWeslRoot } = options;
4827
+ const { packageName = "package", debugWeslRoot, weslExtensions } = options;
3927
4828
  this.sources = sources;
3928
4829
  this.packageName = packageName;
3929
4830
  this.debugWeslRoot = normalizeDebugRoot(debugWeslRoot);
4831
+ this.weslExtensions = weslExtensions;
3930
4832
  }
3931
4833
  resolveModule(modulePath) {
3932
4834
  const cached = this.astCache.get(modulePath);
@@ -3937,7 +4839,7 @@ var RecordResolver = class {
3937
4839
  modulePath,
3938
4840
  debugFilePath: this.modulePathToDebugPath(modulePath),
3939
4841
  src: source
3940
- });
4842
+ }, { weslExtensions: this.weslExtensions });
3941
4843
  this.astCache.set(modulePath, ast);
3942
4844
  return ast;
3943
4845
  }
@@ -4033,7 +4935,7 @@ function freshResolver(inner) {
4033
4935
  if (cached) return cached;
4034
4936
  const ast = inner.resolveModule(modulePath);
4035
4937
  if (!ast) return void 0;
4036
- const fresh = parseSrcModule(ast.srcModule);
4938
+ const fresh = parseSrcModule(ast.srcModule, ast.parseOptions);
4037
4939
  cache.set(modulePath, fresh);
4038
4940
  return fresh;
4039
4941
  } };
@@ -4357,12 +5259,13 @@ var SrcMap = class SrcMap {
4357
5259
  const { src, position: srcStart } = this.destToSrc(e.srcStart);
4358
5260
  const { src: endSrc, position: srcEnd } = this.destToSrc(e.srcEnd);
4359
5261
  if (endSrc !== src) throw new Error("NYI, need to split");
5262
+ const { destStart, destEnd } = e;
4360
5263
  return {
4361
5264
  src,
4362
5265
  srcStart,
4363
5266
  srcEnd,
4364
- destStart: e.destStart,
4365
- destEnd: e.destEnd
5267
+ destStart,
5268
+ destEnd
4366
5269
  };
4367
5270
  });
4368
5271
  const otherSources = other.entries.filter((e) => e.src.path !== this.dest.path || e.src.text !== this.dest.text);
@@ -4383,10 +5286,6 @@ var SrcMap = class SrcMap {
4383
5286
  };
4384
5287
  }
4385
5288
  };
4386
- /** sort entries in place by src start position */
4387
- function sortSrc(entries) {
4388
- entries.sort((a, b) => a.srcStart - b.srcStart);
4389
- }
4390
5289
  /** Incrementally append to a string, tracking source references */
4391
5290
  var SrcMapBuilder = class {
4392
5291
  #fragments = [];
@@ -4440,11 +5339,6 @@ var SrcMapBuilder = class {
4440
5339
  };
4441
5340
  this.add("\n", srcStart, srcEnd);
4442
5341
  }
4443
- /** copy a string fragment from the src to the destination string */
4444
- addCopy(srcStart, srcEnd) {
4445
- const fragment = this.source.text.slice(srcStart, srcEnd);
4446
- this.add(fragment, srcStart, srcEnd);
4447
- }
4448
5342
  /** return a SrcMap */
4449
5343
  static build(builders) {
4450
5344
  const map = new SrcMap({ text: builders.map((b) => b.#fragments.join("")).join("") }, builders.flatMap((b) => b.#entries));
@@ -4452,6 +5346,10 @@ var SrcMapBuilder = class {
4452
5346
  return map;
4453
5347
  }
4454
5348
  };
5349
+ /** sort entries in place by src start position */
5350
+ function sortSrc(entries) {
5351
+ entries.sort((a, b) => a.srcStart - b.srcStart);
5352
+ }
4455
5353
  //#endregion
4456
5354
  //#region src/Linker.ts
4457
5355
  /**
@@ -4468,20 +5366,15 @@ async function link(params) {
4468
5366
  }
4469
5367
  /** linker api for benchmarking */
4470
5368
  function _linkSync(params) {
4471
- const { weslSrc, libs = [], packageName, debugWeslRoot } = params;
4472
- const { resolver } = params;
4473
- const resolvers = [];
4474
- if (resolver) resolvers.push(resolver);
4475
- else if (weslSrc) resolvers.push(new RecordResolver(weslSrc, {
5369
+ const { weslSrc, libs = [], packageName, debugWeslRoot, resolver } = params;
5370
+ const { weslExtensions } = params;
5371
+ if (!resolver && !weslSrc) throw new Error("Either resolver or weslSrc must be provided");
5372
+ const allResolvers = [resolver ?? new RecordResolver(weslSrc, {
4476
5373
  packageName,
4477
- debugWeslRoot
4478
- }));
4479
- else throw new Error("Either resolver or weslSrc must be provided");
4480
- if (libs.length > 0) {
4481
- const libResolvers = createLibraryResolvers(libs, debugWeslRoot);
4482
- resolvers.push(...libResolvers);
4483
- }
4484
- const finalResolver = resolvers.length === 1 ? resolvers[0] : new CompositeResolver(resolvers);
5374
+ debugWeslRoot,
5375
+ weslExtensions
5376
+ }), ...createLibraryResolvers(libs, debugWeslRoot)];
5377
+ const finalResolver = allResolvers.length === 1 ? allResolvers[0] : new CompositeResolver(allResolvers);
4485
5378
  return linkRegistry({
4486
5379
  ...params,
4487
5380
  resolver: finalResolver
@@ -4565,6 +5458,7 @@ function setupVirtualLibs(virtualLibs, constants) {
4565
5458
  }
4566
5459
  return libs && mapValues(libs, (fn) => ({ fn }));
4567
5460
  }
5461
+ /** Run registered transform plugins over the bound AST. */
4568
5462
  function applyTransformPlugins(rootModule, globalNames, config) {
4569
5463
  const { moduleElem, srcModule } = rootModule;
4570
5464
  const startAst = {
@@ -4577,311 +5471,38 @@ function applyTransformPlugins(rootModule, globalNames, config) {
4577
5471
  }
4578
5472
  /** Assemble WGSL output from prologue statements, root module, and imported declarations. */
4579
5473
  function emitWgsl(rootModuleElem, srcModule, newDecls, newStatements, conditions = {}) {
4580
- const prologueBuilders = newStatements.map((s) => emitStatement(s, conditions));
4581
- const rootBuilder = new SrcMapBuilder({
4582
- text: srcModule.src,
4583
- path: srcModule.debugFilePath
4584
- });
5474
+ const prologueBuilders = newStatements.map((s) => emitElem(s.srcModule, s.elem, conditions, { addNl: true }));
5475
+ const rootBuilder = builderFromModule(srcModule);
4585
5476
  lowerAndEmit({
4586
5477
  srcBuilder: rootBuilder,
4587
5478
  rootElems: [rootModuleElem],
4588
5479
  conditions,
4589
5480
  extracting: false
4590
5481
  });
4591
- const declBuilders = newDecls.map((decl) => emitDecl(decl, conditions));
5482
+ const declBuilders = newDecls.map((decl) => emitElem(decl.srcModule, decl.declElem, conditions, { skipConditionalFiltering: true }));
4592
5483
  return [
4593
5484
  ...prologueBuilders,
4594
5485
  rootBuilder,
4595
5486
  ...declBuilders
4596
5487
  ];
4597
5488
  }
4598
- function emitStatement(s, conditions) {
4599
- const { elem, srcModule } = s;
4600
- const { src: text, debugFilePath: path } = srcModule;
4601
- const builder = new SrcMapBuilder({
4602
- text,
4603
- path
4604
- });
5489
+ /** Emit a single element (prologue statement or imported declaration) into a SrcMapBuilder. */
5490
+ function emitElem(srcModule, elem, conditions, opts = {}) {
5491
+ const builder = builderFromModule(srcModule);
4605
5492
  lowerAndEmit({
4606
5493
  srcBuilder: builder,
4607
5494
  rootElems: [elem],
4608
- conditions
4609
- });
4610
- builder.addNl();
4611
- return builder;
4612
- }
4613
- /** Skip conditional filtering because findValidRootDecls already validated these declarations */
4614
- function emitDecl(decl, conditions) {
4615
- const { src: text, debugFilePath: path } = decl.srcModule;
4616
- const builder = new SrcMapBuilder({
4617
- text,
4618
- path
4619
- });
4620
- lowerAndEmit({
4621
- srcBuilder: builder,
4622
- rootElems: [decl.declElem],
4623
5495
  conditions,
4624
- skipConditionalFiltering: true
5496
+ skipConditionalFiltering: opts.skipConditionalFiltering
4625
5497
  });
5498
+ if (opts.addNl) builder.addNl();
4626
5499
  return builder;
4627
5500
  }
4628
- //#endregion
4629
- //#region src/LinkerUtil.ts
4630
- function visitAst(elem, visitor) {
4631
- visitor(elem);
4632
- if (elem.contents) elem.contents.forEach((child) => {
4633
- visitAst(child, visitor);
4634
- });
4635
- }
4636
- //#endregion
4637
- //#region src/RawEmit.ts
4638
- function attributeToString$1(e) {
4639
- const { kind } = e.attribute;
4640
- if (kind === "@attribute") {
4641
- const { params } = e.attribute;
4642
- if (params === void 0 || params.length === 0) return "@" + e.attribute.name;
4643
- else return `@${e.attribute.name}(${params.map((param) => contentsToString(param)).join(", ")})`;
4644
- } else if (kind === "@builtin") return "@builtin(" + e.attribute.param.name + ")";
4645
- else if (kind === "@diagnostic") return "@diagnostic" + diagnosticControlToString(e.attribute.severity, e.attribute.rule);
4646
- else if (kind === "@if") return `@if(${expressionToString(e.attribute.param.expression)})`;
4647
- else if (kind === "@elif") return `@elif(${expressionToString(e.attribute.param.expression)})`;
4648
- else if (kind === "@else") return "@else";
4649
- else if (kind === "@interpolate") return `@interpolate(${e.attribute.params.map((v) => v.name).join(", ")})`;
4650
- else assertUnreachable(kind);
4651
- }
4652
- function typeListToString(params) {
4653
- return `<${params.map(typeParamToString).join(", ")}>`;
4654
- }
4655
- function typeParamToString(param) {
4656
- if (param === void 0) return "?";
4657
- if (param.kind === "type") return typeRefToString(param);
4658
- return expressionToString(param);
4659
- }
4660
- function typeRefToString(t) {
4661
- if (!t) return "?";
4662
- const { name, templateParams } = t;
4663
- const params = templateParams ? typeListToString(templateParams) : "";
4664
- return `${refToString(name)}${params}`;
4665
- }
4666
- function refToString(ref) {
4667
- if (typeof ref === "string") return ref;
4668
- if (ref.std) return ref.originalName;
4669
- const decl = findDecl(ref);
4670
- return decl.mangledName || decl.originalName;
4671
- }
4672
- function contentsToString(elem) {
4673
- if (elem.kind === "translate-time-expression") throw new Error("Not supported");
4674
- else if (elem.kind === "expression" || elem.kind === "stuff") return elem.contents.map((c) => {
4675
- const { kind } = c;
4676
- if (kind === "text") return c.srcModule.src.slice(c.start, c.end);
4677
- else if (kind === "ref") return refToString(c.ident);
4678
- else return `?${c.kind}?`;
4679
- }).join(" ");
4680
- else if (elem.kind === "name") return elem.name;
4681
- else assertUnreachable(elem);
4682
- }
4683
- //#endregion
4684
- //#region src/Reflection.ts
4685
- const textureStorage = matchOneOf(textureStorageTypes);
4686
- matchOneOf(sampledTextureTypes);
4687
- matchOneOf(multisampledTextureTypes);
4688
- //#endregion
4689
- //#region src/TransformBindingStructs.ts
4690
- function bindingStructsPlugin() {
4691
- return { transform: lowerBindingStructs };
4692
- }
4693
- /**
4694
- * Transform binding structures into binding variables by mutating the AST.
4695
- *
4696
- * First we find all the binding structs:
4697
- * . find all the structs in the module by filtering the moduleElem.contents
4698
- * . for each struct:
4699
- * . mark any structs with that contain @group or @binding annotations as 'binding structs' and save them in a list
4700
- * . (later) create reverse links from structs to struct members
4701
- * . (later) visit all the binding structs and traverse to referencing structs, marking the referencing structs as binding structs too
4702
- * Generate synethic AST nodes for binding variables
4703
- *
4704
- * Find all references to binding struct members
4705
- * . find the componound idents by traversing moduleElem.contents
4706
- * . filter to find the compound idents that refer to 'binding structs'
4707
- * . go from each ident to its declaration,
4708
- * . declaration to typeRef reference
4709
- * . typeRef to type declaration
4710
- * . check type declaration to see if it's a binding struct
4711
- * . record the intermediate declaration (e.g. a fn param b:Bindings from 'fn(b:Bindings)' )
4712
- * rewrite references to binding struct members as synthetic elements
4713
- *
4714
- * Remove the binding structs from the AST
4715
- * Remove the intermediate fn param declarations from the AST
4716
- * Add the new binding variables to the AST
4717
- *
4718
- * @return the binding structs and the mutated AST
4719
- */
4720
- function lowerBindingStructs(ast) {
4721
- const clonedAst = structuredClone(ast);
4722
- const { moduleElem, globalNames, notableElems } = clonedAst;
4723
- const bindingStructs = markBindingStructs(moduleElem);
4724
- markEntryTypes(moduleElem, bindingStructs);
4725
- const newVars = bindingStructs.flatMap((s) => transformBindingStruct(s, globalNames));
4726
- const bindingRefs = findRefsToBindingStructs(moduleElem);
4727
- bindingRefs.forEach(({ memberRef, struct }) => {
4728
- transformBindingReference(memberRef, struct);
4729
- });
4730
- bindingRefs.forEach(({ intermediates }) => {
4731
- intermediates.forEach((e) => {
4732
- e.contents = [];
4733
- });
4734
- });
4735
- const contents = removeBindingStructs(moduleElem);
4736
- moduleElem.contents = [...newVars, ...contents];
4737
- notableElems.bindingStructs = bindingStructs;
4738
- return {
4739
- ...clonedAst,
4740
- moduleElem
4741
- };
4742
- }
4743
- function markEntryTypes(moduleElem, bindingStructs) {
4744
- const fnFound = fnReferencesBindingStruct(moduleElem.contents.filter((e) => e.kind === "fn"), bindingStructs);
4745
- if (fnFound) {
4746
- const { fn, struct } = fnFound;
4747
- struct.entryFn = fn;
4748
- }
4749
- }
4750
- function fnReferencesBindingStruct(fns, bindingStructs) {
4751
- for (const fn of fns) {
4752
- const { params } = fn;
4753
- for (const p of params) {
4754
- const referencedElem = ((p.name?.typeRef?.name)?.refersTo)?.declElem;
4755
- const struct = bindingStructs.find((s) => s === referencedElem);
4756
- if (struct) return {
4757
- fn,
4758
- struct
4759
- };
4760
- }
4761
- }
4762
- }
4763
- function removeBindingStructs(moduleElem) {
4764
- return moduleElem.contents.filter((elem) => elem.kind !== "struct" || !elem.bindingStruct);
4765
- }
4766
- /** mutate the AST, marking StructElems as bindingStructs
4767
- * (if they contain ptrs with @group @binding annotations)
4768
- * @return the binding structs
4769
- */
4770
- function markBindingStructs(moduleElem) {
4771
- const bindingStructs = moduleElem.contents.filter((elem) => elem.kind === "struct").filter(containsBinding);
4772
- bindingStructs.forEach((struct) => {
4773
- struct.bindingStruct = true;
4774
- });
4775
- return bindingStructs;
4776
- }
4777
- /** @return true if this struct contains a member with marked with @binding or @group */
4778
- function containsBinding(struct) {
4779
- return struct.members.some(({ attributes }) => bindingAttribute(attributes));
4780
- }
4781
- function bindingAttribute(attributes) {
4782
- if (!attributes) return false;
4783
- return attributes.some(({ attribute }) => attribute.kind === "@attribute" && (attribute.name === "binding" || attribute.name === "group"));
4784
- }
4785
- /** convert each member of the binding struct into a synthetic global variable */
4786
- function transformBindingStruct(s, globalNames) {
4787
- return s.members.map((member) => {
4788
- const { typeRef, name: memberName } = member;
4789
- const { name: typeName } = typeRef;
4790
- const typeParameters = typeRef?.templateParams;
4791
- const varName = minimallyMangledName(memberName.name, globalNames);
4792
- member.mangledVarName = varName;
4793
- globalNames.add(varName);
4794
- const attributes = member.attributes?.map(attributeToString$1).join(" ") ?? "";
4795
- const varTypes = lowerPtrMember(typeName, typeParameters) ?? lowerStdTypeMember(typeName, typeParameters) ?? lowerStorageTextureMember(typeName, typeParameters);
4796
- if (!varTypes) {
4797
- console.log("unhandled case transforming member", typeName);
4798
- return syntheticVar(attributes, varName, "", "??");
4799
- }
4800
- const { storage: storageType, varType } = varTypes;
4801
- return syntheticVar(attributes, varName, storageType, varType);
4802
- });
4803
- }
4804
- function lowerPtrMember(typeName, typeParameters) {
4805
- if (typeName.originalName === "ptr") {
4806
- const origParams = typeParameters ?? [];
4807
- const newParams = [origParams[0]];
4808
- if (origParams[2]) newParams.push(origParams[2]);
4809
- return {
4810
- storage: typeListToString(newParams),
4811
- varType: typeParamToString(origParams?.[1])
4812
- };
4813
- }
4814
- }
4815
- function lowerStdTypeMember(typeName, typeParameters) {
4816
- if (typeof typeName !== "string") return {
4817
- varType: (typeName.std ? typeName.originalName : "??") + (typeParameters ? typeListToString(typeParameters) : ""),
4818
- storage: ""
4819
- };
4820
- }
4821
- function lowerStorageTextureMember(typeName, typeParameters) {
4822
- if (textureStorage.test(typeName.originalName)) return {
4823
- varType: typeName + (typeParameters ? typeListToString(typeParameters) : ""),
4824
- storage: ""
4825
- };
4826
- }
4827
- function syntheticVar(attributes, varName, storageTemplate, varType) {
4828
- return {
4829
- kind: "synthetic",
4830
- text: `${attributes} var${storageTemplate} ${varName} : ${varType};\n`
4831
- };
4832
- }
4833
- /** find all simple member references in the module that refer to binding structs */
4834
- function findRefsToBindingStructs(moduleElem) {
4835
- const members = [];
4836
- visitAst(moduleElem, (elem) => {
4837
- if (elem.kind === "memberRef") members.push(elem);
5501
+ function builderFromModule(srcModule) {
5502
+ return new SrcMapBuilder({
5503
+ text: srcModule.src,
5504
+ path: srcModule.debugFilePath
4838
5505
  });
4839
- return filterMap(members, refersToBindingStruct);
4840
- }
4841
- /** @return true if this memberRef refers to a binding struct */
4842
- function refersToBindingStruct(memberRef) {
4843
- const found = traceToStruct(memberRef.name.ident);
4844
- if (found?.struct.bindingStruct) return {
4845
- memberRef,
4846
- ...found
4847
- };
4848
- }
4849
- /** If this identifier ultimately refers to a struct type, return the struct declaration */
4850
- function traceToStruct(ident) {
4851
- const declElem = findDecl(ident).declElem;
4852
- if (declElem && declElem.kind === "param") {
4853
- const name = declElem.name.typeRef?.name;
4854
- if (typeof name !== "string") {
4855
- if (name?.std) return;
4856
- const structElem = findDecl(name).declElem;
4857
- if (structElem?.kind === "struct") return {
4858
- struct: structElem,
4859
- intermediates: [declElem]
4860
- };
4861
- return;
4862
- }
4863
- }
4864
- }
4865
- /** Mutate the member reference elem to instead contain synthetic elem text.
4866
- * The new text is the mangled var name of the struct member that the memberRef refers to. */
4867
- function transformBindingReference(memberRef, struct) {
4868
- const refName = memberRef.member.name;
4869
- const structMember = struct.members.find((m) => m.name.name === refName);
4870
- if (!structMember || !structMember.mangledVarName) {
4871
- console.log(`missing mangledVarName for ${refName}`);
4872
- return {
4873
- kind: "synthetic",
4874
- text: refName
4875
- };
4876
- }
4877
- const { extraComponents } = memberRef;
4878
- const extraText = extraComponents ? contentsToString(extraComponents) : "";
4879
- const synthElem = {
4880
- kind: "synthetic",
4881
- text: structMember.mangledVarName + extraText
4882
- };
4883
- memberRef.contents = [synthElem];
4884
- return synthElem;
4885
5506
  }
4886
5507
  //#endregion
4887
5508
  //#region src/WeslDevice.ts
@@ -4924,6 +5545,7 @@ function makeWeslDevice(device) {
4924
5545
  lineNumber: message.lineNum,
4925
5546
  lineColumn: message.linePos,
4926
5547
  length: message.length,
5548
+ offset: message.offset,
4927
5549
  error: /* @__PURE__ */ new Error(message.type + ": " + message.message)
4928
5550
  });
4929
5551
  else console.error(ev.error.message);
@@ -4951,4 +5573,4 @@ function makeWeslDevice(device) {
4951
5573
  return device;
4952
5574
  }
4953
5575
  //#endregion
4954
- export { BundleResolver, CompositeResolver, LinkedWesl, ParseError, RecordResolver, SrcMap, SrcMapBuilder, TrackingResolver, WeslParseError, WeslStream, _linkSync, astToString, attributeToString, bindAndTransform, bindIdents, bindIdentsRecursive, bindingStructsPlugin, childIdent, childScope, containsScope, debug, debugContentsToString, discoverModules, emptyScope, errorHighlight, fileToModulePath, filterMap, filterValidElements, findAllRootDecls, findMap, findRefsToBindingStructs, findUnboundIdents, findUnboundRefs, findValidRootDecls, flatImports, freshResolver, groupBy, grouped, identToString, last, lengthPrefixMangle, link, linkRegistry, liveDeclsToString, log, lowerBindingStructs, makeLiveDecls, makeWeslDevice, mapForward, mapValues, markBindingStructs, markEntryTypes, mergeScope, minimalMangle, minimallyMangledName, modulePartsToRelativePath, moduleToRelativePath, multiKeySet, multisampledTextureTypes, nextIdentId, noSuffix, normalize, normalizeDebugRoot, normalizeModuleName, npmNameVariations, offsetToLineNumber, overlapTail, parseSrcModule, partition, publicDecl, replaceWords, requestWeslDevice, resetScopeIds, resolveModulePath, sampledTextureTypes, sanitizePackageName, scan, scopeToString, scopeToStringLong, srcLog, stdEnumerant, stdEnumerants, stdFn, stdFns, stdType, stdTypes, textureStorageTypes, transformBindingReference, transformBindingStruct, underscoreMangle, validation, wgslStandardAttributes, withLoggerAsync };
5576
+ export { BundleResolver, CompositeResolver, LinkedWesl, ParseError, RecordResolver, SrcMap, SrcMapBuilder, TrackingResolver, WeslParseError, WeslStream, _linkSync, astToString, attributeToString, bindAndTransform, bindIdents, bindIdentsRecursive, childElems, childIdent, childScope, containsScope, debug, declsOfKind, discoverModules, emptyScope, errorHighlight, fileToModulePath, filterMap, filterValidElements, findAllRootDecls, findMap, findUnboundIdents, findUnboundRefs, findValidRootDecls, flatImports, freshResolver, groupBy, grouped, identElemLog, identToString, last, lengthPrefixMangle, link, linkRegistry, liveDeclsToString, log, makeLiveDecls, makeWeslDevice, mapForward, mapValues, mergeScope, minimalMangle, minimallyMangledName, modulePartsToRelativePath, moduleToRelativePath, multiKeySet, multisampledTextureTypes, nextIdentId, noSuffix, normalize, normalizeDebugRoot, normalizeModuleName, npmNameVariations, offsetToLineNumber, overlapTail, parseSrcModule, partition, publicDecl, replaceWords, requestWeslDevice, resetScopeIds, resolveModulePath, sampledTextureTypes, sanitizePackageName, scan, scopeToString, scopeToStringLong, srcLog, stdEnumerant, stdEnumerants, stdFn, stdFns, stdType, stdTypes, textureStorageTypes, underscoreMangle, validation, visitAst, wgslStandardAttributes, withLoggerAsync };