wesl 0.7.27 → 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 (61) hide show
  1. package/dist/index.d.ts +272 -126
  2. package/dist/index.js +1742 -1102
  3. package/package.json +1 -1
  4. package/src/AbstractElems.ts +263 -81
  5. package/src/Linker.ts +14 -2
  6. package/src/LinkerUtil.ts +141 -7
  7. package/src/LowerAndEmit.ts +660 -304
  8. package/src/ModuleResolver.ts +15 -4
  9. package/src/ParseWESL.ts +21 -33
  10. package/src/SrcMap.ts +7 -19
  11. package/src/debug/ASTtoString.ts +92 -76
  12. package/src/index.ts +1 -1
  13. package/src/parse/AttachComments.ts +289 -0
  14. package/src/parse/ExpressionUtil.ts +3 -3
  15. package/src/parse/ParseAttribute.ts +49 -71
  16. package/src/parse/ParseCall.ts +3 -4
  17. package/src/parse/ParseControlFlow.ts +100 -56
  18. package/src/parse/ParseDirective.ts +9 -8
  19. package/src/parse/ParseDoBlock.ts +63 -0
  20. package/src/parse/ParseExpression.ts +11 -10
  21. package/src/parse/ParseFn.ts +11 -33
  22. package/src/parse/ParseGlobalVar.ts +36 -27
  23. package/src/parse/ParseIdent.ts +4 -5
  24. package/src/parse/ParseLocalVar.ts +16 -12
  25. package/src/parse/ParseLoop.ts +76 -39
  26. package/src/parse/ParseModule.ts +65 -19
  27. package/src/parse/ParseSimpleStatement.ts +112 -66
  28. package/src/parse/ParseStatement.ts +39 -66
  29. package/src/parse/ParseStruct.ts +8 -22
  30. package/src/parse/ParseType.ts +10 -13
  31. package/src/parse/ParseUtil.ts +26 -12
  32. package/src/parse/ParseValueDeclaration.ts +18 -31
  33. package/src/parse/ParseWesl.ts +11 -14
  34. package/src/parse/ParsingContext.ts +11 -9
  35. package/src/parse/WeslStream.ts +138 -121
  36. package/src/parse/stream/RegexMatchers.ts +45 -0
  37. package/src/parse/stream/WeslLexer.ts +249 -0
  38. package/src/test/BevyLink.test.ts +17 -10
  39. package/src/test/ConditionalElif.test.ts +4 -2
  40. package/src/test/DeclCommentEmit.test.ts +122 -0
  41. package/src/test/DoBlock.test.ts +164 -0
  42. package/src/test/FilterValidElements.test.ts +4 -6
  43. package/src/test/Linker.test.ts +23 -7
  44. package/src/test/Mangling.test.ts +8 -4
  45. package/src/test/ParseComments.test.ts +234 -6
  46. package/src/test/ParseConditionsV2.test.ts +47 -175
  47. package/src/test/ParseElifV2.test.ts +12 -33
  48. package/src/test/ParseErrorV2.test.ts +0 -0
  49. package/src/test/ParseWeslV2.test.ts +247 -626
  50. package/src/test/StatementEmit.test.ts +143 -0
  51. package/src/test/StripWesl.ts +24 -3
  52. package/src/test/TestLink.ts +19 -3
  53. package/src/test/TestUtil.ts +18 -8
  54. package/src/test/Tokenizer.test.ts +96 -0
  55. package/src/test/__snapshots__/ParseWeslV2.test.ts.snap +10 -42
  56. package/src/RawEmit.ts +0 -103
  57. package/src/Reflection.ts +0 -336
  58. package/src/TransformBindingStructs.ts +0 -320
  59. package/src/parse/ContentsHelpers.ts +0 -70
  60. package/src/parse/stream/CachingStream.ts +0 -48
  61. package/src/parse/stream/MatchersStream.ts +0 -85
package/dist/index.js CHANGED
@@ -499,6 +499,120 @@ function childIdent(child) {
499
499
  return !childScope(child);
500
500
  }
501
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
502
616
  //#region src/StandardTypes.ts
503
617
  const stdFns = `bitcast all any select arrayLength
504
618
  abs acos acosh asin asinh atan atanh atan2 ceil clamp cos cosh
@@ -600,6 +714,22 @@ function stdEnumerant(name) {
600
714
  }
601
715
  //#endregion
602
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
+ ]);
603
733
  /** Traverse the AST, starting from root elements, emitting WGSL for each. */
604
734
  function lowerAndEmit(params) {
605
735
  const { srcBuilder, rootElems, conditions } = params;
@@ -607,17 +737,53 @@ function lowerAndEmit(params) {
607
737
  const emitContext = {
608
738
  conditions,
609
739
  srcBuilder,
610
- extracting
740
+ extracting,
741
+ indent: 0
611
742
  };
612
743
  const validElements = skipConditionalFiltering ? rootElems : filterValidElements(rootElems, conditions);
613
744
  for (const e of validElements) lowerAndEmitElem(e, emitContext);
614
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
+ }
615
783
  function lowerAndEmitElem(e, ctx) {
616
784
  switch (e.kind) {
617
785
  case "import": return;
618
- case "text":
619
- emitText(e, ctx);
620
- return;
786
+ case "do": return;
621
787
  case "name":
622
788
  emitName(e, ctx);
623
789
  return;
@@ -640,24 +806,45 @@ function lowerAndEmitElem(e, ctx) {
640
806
  emitExpression(e, ctx);
641
807
  return;
642
808
  case "param":
809
+ emitAttributes(e.attributes, ctx);
810
+ emitTypedDecl(e.name, ctx);
811
+ return;
643
812
  case "typeDecl":
813
+ emitTypedDecl(e, ctx);
814
+ return;
644
815
  case "member":
645
- case "memberRef":
816
+ emitMember(e, ctx);
817
+ return;
646
818
  case "expression":
647
- case "type":
819
+ emitExpression(e.expression, ctx);
820
+ return;
648
821
  case "switch-clause":
649
- emitContents(e, ctx);
822
+ emitSwitchClause(e, ctx);
650
823
  return;
651
- case "stuff":
652
- emitStuff(e, ctx);
824
+ case "type":
825
+ emitTypeRef(e, ctx);
653
826
  return;
654
827
  case "module":
655
828
  emitModule(e, ctx);
656
829
  return;
657
830
  case "var":
658
831
  case "let":
659
- case "statement":
832
+ case "block":
833
+ case "if":
834
+ case "for":
835
+ case "while":
836
+ case "loop":
660
837
  case "continuing":
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":
661
848
  emitStatement(e, ctx);
662
849
  return;
663
850
  case "override":
@@ -669,52 +856,131 @@ function lowerAndEmitElem(e, ctx) {
669
856
  return;
670
857
  case "fn":
671
858
  emitRootElemNl(ctx);
859
+ emitRootLeading(e, ctx);
672
860
  emitFn(e, ctx);
861
+ emitTrailingComments(e, ctx);
673
862
  return;
674
863
  case "struct":
675
864
  emitRootElemNl(ctx);
865
+ emitRootLeading(e, ctx);
676
866
  emitStruct(e, ctx);
867
+ emitTrailingComments(e, ctx);
677
868
  return;
678
869
  case "attribute":
679
870
  emitAttribute(e, ctx);
680
871
  return;
681
872
  case "directive":
873
+ ctx.srcBuilder.addNl();
874
+ emitRootLeading(e, ctx);
682
875
  emitDirective(e, ctx);
876
+ emitTrailingComments(e, ctx);
683
877
  return;
684
878
  default: assertUnreachable(e);
685
879
  }
686
880
  }
687
- function emitStuff(e, ctx) {
688
- emitContentsWithTrimming(e, ctx);
881
+ function emitName(e, ctx) {
882
+ ctx.srcBuilder.add(e.name, e.start, e.end);
689
883
  }
690
- function emitModule(e, ctx) {
691
- const validElements = filterValidElements(e.contents, ctx.conditions);
692
- for (const child of validElements) {
693
- if (child.kind === "text") {
694
- 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);
695
909
  }
696
- 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
+ });
697
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);
953
+ }
954
+ function emitModule(e, ctx) {
955
+ const validElements = filterValidElements(e.decls, ctx.conditions);
956
+ for (const child of validElements) lowerAndEmitElem(child, ctx);
698
957
  }
699
- function emitStatement(e, ctx) {
700
- if (!(e.contents.length > 0 && e.contents[0].kind === "attribute")) emitAttributes(e.attributes, ctx);
701
- emitContents(e, ctx);
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);
702
964
  }
703
965
  function emitRootDecl(e, ctx) {
704
966
  emitRootElemNl(ctx);
705
- if (!(e.contents.length > 0 && e.contents[0].kind === "attribute")) emitAttributes(e.attributes, ctx);
706
- emitContentsWithTrimming(e, ctx);
967
+ emitRootLeading(e, ctx);
968
+ emitValueDecl(e, ctx);
969
+ emitTrailingComments(e, ctx);
707
970
  }
708
971
  /** Emit newlines between root elements. */
709
972
  function emitRootElemNl(ctx) {
710
973
  ctx.srcBuilder.addNl();
711
974
  ctx.srcBuilder.addNl();
712
975
  }
713
- function emitText(e, ctx) {
714
- ctx.srcBuilder.addCopy(e.start, e.end);
715
- }
716
- function emitName(e, ctx) {
717
- 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
+ }
718
984
  }
719
985
  /** Emit function explicitly to control commas between conditional parameters. */
720
986
  function emitFn(e, ctx) {
@@ -722,27 +988,35 @@ function emitFn(e, ctx) {
722
988
  const { conditions, srcBuilder: builder } = ctx;
723
989
  emitAttributes(attributes, ctx);
724
990
  builder.add("fn ", name.start - 3, name.start);
991
+ emitInlineLeading(name, ctx);
725
992
  emitDeclIdent(name, ctx);
993
+ emitInlineTrailing(name, ctx);
726
994
  builder.appendNext("(");
727
995
  const validParams = filterValidElements(params, conditions);
728
996
  validParams.forEach((p, i) => {
729
- if (!(p.contents.length > 0 && p.contents[0].kind === "attribute")) emitAttributes(p.attributes, ctx);
730
- emitContentsNoWs(p, ctx);
997
+ emitInlineLeading(p, ctx);
998
+ emitAttributes(p.attributes, ctx);
999
+ emitTypedDecl(p.name, ctx);
1000
+ emitInlineTrailing(p, ctx);
731
1001
  if (i < validParams.length - 1) builder.appendNext(", ");
732
1002
  });
733
1003
  builder.appendNext(") ");
734
1004
  if (returnType) {
735
1005
  builder.appendNext("-> ");
736
1006
  emitAttributes(returnAttributes, ctx);
737
- emitContentsNoWs(returnType, ctx);
1007
+ emitInlineLeading(returnType, ctx);
1008
+ emitTypeRef(returnType, ctx);
738
1009
  builder.appendNext(" ");
739
1010
  }
740
- emitContents(body, ctx);
1011
+ emitBlock(body, ctx);
741
1012
  }
742
- function emitAttributes(attributes, ctx) {
743
- attributes?.forEach((a) => {
744
- if (emitAttribute(a, ctx)) ctx.srcBuilder.add(" ", a.start, a.end);
745
- });
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
+ }
746
1020
  }
747
1021
  /** Emit structs explicitly to control commas between conditional members. */
748
1022
  function emitStruct(e, ctx) {
@@ -756,125 +1030,22 @@ function emitStruct(e, ctx) {
756
1030
  }
757
1031
  emitAttributes(attributes, ctx);
758
1032
  srcBuilder.add("struct ", start, name.start);
1033
+ emitInlineLeading(name, ctx);
759
1034
  emitDeclIdent(name, ctx);
760
- if (validLength === 1) {
1035
+ emitInlineTrailing(name, ctx);
1036
+ if (validLength === 1 && !hasComments(validMembers[0])) {
761
1037
  srcBuilder.appendNext(" { ");
762
- emitContentsWithTrimming(validMembers[0], ctx);
1038
+ emitMember(validMembers[0], ctx);
763
1039
  srcBuilder.appendNext(" }");
764
1040
  srcBuilder.addNl();
765
1041
  } else {
766
1042
  srcBuilder.appendNext(" {");
767
1043
  srcBuilder.addNl();
768
- validMembers.forEach((m) => {
769
- srcBuilder.appendNext(" ");
770
- emitContentsNoWs(m, ctx);
771
- srcBuilder.appendNext(",");
772
- srcBuilder.addNl();
773
- });
1044
+ for (const m of validMembers) emitMemberLine(m, ctx);
774
1045
  srcBuilder.appendNext("}");
775
1046
  srcBuilder.addNl();
776
1047
  }
777
1048
  }
778
- function warnEmptyStruct(e) {
779
- const { name, members } = e;
780
- const condStr = members.length ? "(with current conditions)" : "";
781
- failIdentElem(name, `struct '${name.ident.originalName}' has no members ${condStr}`);
782
- }
783
- function emitSynthetic(e, ctx) {
784
- const { text } = e;
785
- ctx.srcBuilder.addSynthetic(text, text, 0, text.length);
786
- }
787
- function emitContents(elem, ctx) {
788
- const validElements = filterValidElements(elem.contents, ctx.conditions);
789
- for (const e of validElements) lowerAndEmitElem(e, ctx);
790
- }
791
- /** Emit contents with leading/trailing whitespace trimming (V2 parser). */
792
- function emitContentsWithTrimming(elem, ctx) {
793
- const validElements = filterValidElements(elem.contents, ctx.conditions);
794
- const firstEmit = validElements.findIndex((e) => !isConditionalAttr(e));
795
- const lastEmit = validElements.findLastIndex((e) => !isConditionalAttr(e));
796
- validElements.forEach((elem, i) => {
797
- if (elem.kind === "text") {
798
- let text = elem.srcModule.src.slice(elem.start, elem.end);
799
- if (i === firstEmit) text = text.trimStart();
800
- if (i === lastEmit) text = text.trimEnd();
801
- if (text) ctx.srcBuilder.add(text, elem.start, elem.end);
802
- } else lowerAndEmitElem(elem, ctx);
803
- });
804
- }
805
- function isConditionalAttr(e) {
806
- if (e.kind !== "attribute") return false;
807
- const { kind } = e.attribute;
808
- return kind === "@if" || kind === "@elif" || kind === "@else";
809
- }
810
- /** Emit contents without whitespace. */
811
- function emitContentsNoWs(elem, ctx) {
812
- filterValidElements(elem.contents, ctx.conditions).forEach((e) => {
813
- if (e.kind === "text") {
814
- const { srcModule, start, end } = e;
815
- if (srcModule.src.slice(start, end).trim() === "") return;
816
- }
817
- lowerAndEmitElem(e, ctx);
818
- });
819
- }
820
- function emitRefIdent(e, ctx) {
821
- if (e.ident.std) ctx.srcBuilder.add(e.ident.originalName, e.start, e.end);
822
- else {
823
- const mangledName = displayName(findDecl(e.ident));
824
- ctx.srcBuilder.add(mangledName, e.start, e.end);
825
- }
826
- }
827
- function emitDeclIdent(e, ctx) {
828
- const mangledName = displayName(e.ident);
829
- ctx.srcBuilder.add(mangledName, e.start, e.end);
830
- }
831
- function emitExpression(e, ctx) {
832
- const { kind } = e;
833
- if (kind === "literal") {
834
- ctx.srcBuilder.add(e.value, e.start, e.end);
835
- return;
836
- }
837
- if (kind === "ref") {
838
- emitRefIdent(e, ctx);
839
- return;
840
- }
841
- if (kind === "type") {
842
- emitContents(e, ctx);
843
- return;
844
- }
845
- if (kind === "binary-expression") {
846
- emitExpression(e.left, ctx);
847
- ctx.srcBuilder.add(` ${e.operator.value} `, e.operator.span[0], e.operator.span[1]);
848
- emitExpression(e.right, ctx);
849
- return;
850
- }
851
- if (kind === "unary-expression") {
852
- ctx.srcBuilder.add(e.operator.value, e.operator.span[0], e.operator.span[1]);
853
- emitExpression(e.expression, ctx);
854
- return;
855
- }
856
- if (kind === "parenthesized-expression") {
857
- emitExpression(e.expression, ctx);
858
- return;
859
- }
860
- if (kind === "call-expression") {
861
- emitExpression(e.function, ctx);
862
- if (e.templateArgs) for (const targ of e.templateArgs) lowerAndEmitElem(targ, ctx);
863
- for (const arg of e.arguments) emitExpression(arg, ctx);
864
- return;
865
- }
866
- if (kind === "component-expression") {
867
- emitExpression(e.base, ctx);
868
- emitExpression(e.access, ctx);
869
- return;
870
- }
871
- if (kind === "component-member-expression") {
872
- emitExpression(e.base, ctx);
873
- if (e.access.kind === "name") ctx.srcBuilder.add(e.access.name, e.access.start, e.access.end);
874
- return;
875
- }
876
- assertUnreachable(kind);
877
- }
878
1049
  function emitAttribute(e, ctx) {
879
1050
  const { kind } = e.attribute;
880
1051
  if (kind === "@if" || kind === "@elif" || kind === "@else") return false;
@@ -884,11 +1055,13 @@ function emitAttribute(e, ctx) {
884
1055
  return true;
885
1056
  }
886
1057
  if (kind === "@builtin") {
887
- 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);
888
1060
  return true;
889
1061
  }
890
1062
  if (kind === "@diagnostic") {
891
- const diagStr = "@diagnostic" + diagnosticControlToString(e.attribute.severity, e.attribute.rule);
1063
+ const { severity, rule } = e.attribute;
1064
+ const diagStr = `@diagnostic${diagnosticControlToString(severity, rule)}`;
892
1065
  ctx.srcBuilder.add(diagStr, e.start, e.end);
893
1066
  return true;
894
1067
  }
@@ -899,46 +1072,6 @@ function emitAttribute(e, ctx) {
899
1072
  }
900
1073
  assertUnreachable(kind);
901
1074
  }
902
- function emitStandardAttribute(e, ctx) {
903
- if (e.attribute.kind !== "@attribute") return;
904
- const { params } = e.attribute;
905
- if (!params || params.length === 0) {
906
- ctx.srcBuilder.add("@" + e.attribute.name, e.start, e.end);
907
- return;
908
- }
909
- ctx.srcBuilder.add("@" + e.attribute.name + "(", e.start, params[0].start);
910
- for (let i = 0; i < params.length; i++) {
911
- emitContents(params[i], ctx);
912
- if (i < params.length - 1) ctx.srcBuilder.add(",", params[i].end, params[i + 1].start);
913
- }
914
- ctx.srcBuilder.add(")", params[params.length - 1].end, e.end);
915
- }
916
- function diagnosticControlToString(severity, rule) {
917
- const ruleStr = rule[0].name + (rule[1] !== null ? "." + rule[1].name : "");
918
- return `(${severity.name}, ${ruleStr})`;
919
- }
920
- function expressionToString(elem) {
921
- const { kind } = elem;
922
- switch (kind) {
923
- case "binary-expression": {
924
- const left = expressionToString(elem.left);
925
- const right = expressionToString(elem.right);
926
- return `${left} ${elem.operator.value} ${right}`;
927
- }
928
- case "unary-expression": return `${elem.operator.value}${expressionToString(elem.expression)}`;
929
- case "ref": return elem.ident.originalName;
930
- case "literal": return elem.value;
931
- case "parenthesized-expression": return `(${expressionToString(elem.expression)})`;
932
- case "component-expression": return `${expressionToString(elem.base)}[${elem.access}]`;
933
- case "component-member-expression": return `${expressionToString(elem.base)}.${elem.access}`;
934
- case "call-expression": {
935
- const fn = elem.function;
936
- return `${fn.kind === "ref" ? fn.ident.originalName : fn.name.originalName}${elem.templateArgs ? `<...>` : ""}(${elem.arguments.map(expressionToString).join(", ")})`;
937
- }
938
- case "type": return elem.name.originalName;
939
- default: assertUnreachable(kind);
940
- }
941
- }
942
1075
  function emitDirective(e, ctx) {
943
1076
  const { directive } = e;
944
1077
  const { kind } = directive;
@@ -960,14 +1093,351 @@ function displayName(declIdent) {
960
1093
  }
961
1094
  return declIdent.mangledName || declIdent.originalName;
962
1095
  }
963
- /** Trace through refersTo links until we find the declaration. */
964
- function findDecl(ident) {
965
- let i = ident;
966
- do {
967
- if (i.kind === "decl") return i;
968
- i = i.refersTo;
969
- } while (i);
970
- 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);
971
1441
  }
972
1442
  //#endregion
973
1443
  //#region src/debug/ImportToString.ts
@@ -1052,19 +1522,25 @@ function astToString(elem, indent = 0) {
1052
1522
  const str = new LineWrapper(indent, maxLineLength);
1053
1523
  str.add(kind);
1054
1524
  addElemFields(elem, str);
1055
- let childStrings = [];
1056
- if ("contents" in elem) childStrings = elem.contents.map((e) => astToString(e, indent + 2));
1057
- if (childStrings.length) {
1525
+ addCommentFields(elem, str);
1526
+ const children = childElems(elem);
1527
+ if (children.length) {
1058
1528
  str.nl();
1529
+ const childStrings = children.map((e) => astToString(e, indent + 2));
1059
1530
  str.addBlock(childStrings.join("\n"), false);
1060
1531
  }
1061
1532
  return str.result;
1062
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
+ }
1063
1540
  function addElemFields(elem, str) {
1064
1541
  const { kind } = elem;
1065
- if (kind === "assert" || kind === "module" || kind === "param" || kind === "stuff" || kind === "switch-clause") return;
1066
- if (kind === "text") str.add(` '${elem.srcModule.src.slice(elem.start, elem.end)}'`);
1067
- 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") {
1068
1544
  addTypedDeclIdent(elem.name, str);
1069
1545
  listAttributeElems(elem.attributes, str);
1070
1546
  } else if (kind === "struct") str.add(" " + elem.name.ident.originalName);
@@ -1072,22 +1548,14 @@ function addElemFields(elem, str) {
1072
1548
  listAttributeElems(elem.attributes, str);
1073
1549
  str.add(` ${elem.name.name}: ${typeRefElemToString(elem.typeRef)}`);
1074
1550
  } else if (kind === "name") str.add(" " + elem.name);
1075
- else if (kind === "memberRef") {
1076
- const extra = elem.extraComponents ? debugContentsToString(elem.extraComponents) : "";
1077
- str.add(` ${elem.name.ident.originalName}.${elem.member.name}${extra}`);
1078
- } else if (kind === "fn") addFnFields(elem, str);
1551
+ else if (kind === "fn") addFnFields(elem, str);
1552
+ else if (kind === "do") addDoFields(elem, str);
1079
1553
  else if (kind === "alias") {
1080
1554
  const prefix = elem.name.ident.kind === "decl" ? "%" : "";
1081
1555
  str.add(` ${prefix}${elem.name.ident.originalName}=${typeRefElemToString(elem.typeRef)}`);
1082
1556
  } else if (kind === "attribute") addAttributeFields(elem.attribute, str);
1083
- else if (kind === "expression") {
1084
- const contents = elem.contents.map((e) => e.kind === "text" ? `'${e.srcModule.src.slice(e.start, e.end)}'` : astToString(e)).join(" ");
1085
- str.add(" " + contents);
1086
- } else if (kind === "type") {
1087
- const nameStr = typeof elem.name === "string" ? elem.name : elem.name.originalName;
1088
- const params = elem.templateParams?.map(templateParamToString).join(", ");
1089
- str.add(params ? ` ${nameStr}<${params}>` : ` ${nameStr}`);
1090
- } 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}'`);
1091
1559
  else if (kind === "import") {
1092
1560
  str.add(" " + importToString(elem.imports));
1093
1561
  listAttributeElems(elem.attributes, str);
@@ -1095,7 +1563,7 @@ function addElemFields(elem, str) {
1095
1563
  else if (kind === "typeDecl") addTypedDeclIdent(elem, str);
1096
1564
  else if (kind === "decl") str.add(" %" + elem.ident.originalName);
1097
1565
  else if (kind === "directive") addDirective(elem, str);
1098
- 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);
1099
1567
  else if (kind === "literal") str.add(` literal(${elem.value})`);
1100
1568
  else if (kind === "binary-expression") str.add(` binop(${elem.operator.value})`);
1101
1569
  else if (kind === "unary-expression") str.add(` unop(${elem.operator.value})`);
@@ -1105,6 +1573,14 @@ function addElemFields(elem, str) {
1105
1573
  else if (kind === "component-member-expression") str.add(" .");
1106
1574
  else assertUnreachable(kind);
1107
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
+ }
1108
1584
  function addAttributeFields(attr, str) {
1109
1585
  const { kind } = attr;
1110
1586
  if (kind === "@attribute") {
@@ -1118,35 +1594,33 @@ function addAttributeFields(attr, str) {
1118
1594
  else if (kind === "@interpolate") str.add(` @interpolate(${attr.params.map((v) => v.name).join(", ")})`);
1119
1595
  else assertUnreachable(kind);
1120
1596
  }
1121
- /** @return string representation of an attribute (for test/debug) */
1122
- function attributeToString(attr) {
1123
- const str = new LineWrapper(0, maxLineLength);
1124
- addAttributeFields(attr, str);
1125
- return str.result;
1126
- }
1127
1597
  function addTypedDeclIdent(elem, str) {
1128
1598
  const { decl, typeRef } = elem;
1129
1599
  str.add(" %" + decl.ident.originalName);
1130
1600
  if (typeRef) str.add(" : " + typeRefElemToString(typeRef));
1131
1601
  }
1132
- function addFnFields(elem, str) {
1133
- const { name, params, returnType, attributes } = elem;
1134
- const paramStrs = params.map((p) => {
1135
- const { originalName } = p.name.decl.ident;
1136
- return `${originalName}: ${typeRefElemToString(p.name.typeRef)}`;
1137
- });
1138
- str.add(` ${name.ident.originalName}(${paramStrs.join(", ")})`);
1139
- listAttributeElems(attributes, str);
1140
- if (returnType) str.add(" -> " + typeRefElemToString(returnType));
1141
- }
1142
1602
  /** show attribute names in short form to verify collection */
1143
1603
  function listAttributeElems(attrs, str) {
1144
1604
  attrs?.forEach((a) => {
1145
1605
  str.add(" " + attributeName(a.attribute));
1146
1606
  });
1147
1607
  }
1148
- function attributeName(attr) {
1149
- 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);
1150
1624
  }
1151
1625
  function addDirective(elem, str) {
1152
1626
  const { directive, attributes } = elem;
@@ -1159,30 +1633,29 @@ function addDirective(elem, str) {
1159
1633
  else assertUnreachable(kind);
1160
1634
  listAttributeElems(attributes, str);
1161
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
+ }
1162
1642
  function unknownExpressionToString(elem) {
1163
- if (!("contents" in elem)) return astToString(elem);
1164
- return elem.contents.map((e) => {
1165
- if (e.kind === "text") return `'${e.srcModule.src.slice(e.start, e.end)}'`;
1166
- return astToString(e);
1167
- }).join(" ");
1643
+ return astToString(elem.expression);
1644
+ }
1645
+ function attributeName(attr) {
1646
+ return attr.kind === "@attribute" ? "@" + attr.name : attr.kind;
1168
1647
  }
1169
1648
  function templateParamToString(p) {
1170
1649
  if (typeof p === "string") return p;
1171
1650
  if (p.kind === "type") return typeRefElemToString(p);
1172
- return astToString(p);
1651
+ return expressionToString(p);
1173
1652
  }
1174
- function typeRefElemToString(elem) {
1175
- if (!elem) return "?type?";
1176
- const nameStr = typeof elem.name === "string" ? elem.name : elem.name.originalName;
1177
- const params = elem.templateParams?.map(templateParamToString).join(", ");
1178
- return params ? `${nameStr}<${params}>` : nameStr;
1179
- }
1180
- function debugContentsToString(elem) {
1181
- return elem.contents.map((c) => {
1182
- if (c.kind === "text") return c.srcModule.src.slice(c.start, c.end);
1183
- if (c.kind === "ref") return c.ident.originalName;
1184
- return `?${c.kind}?`;
1185
- }).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(", ");
1186
1659
  }
1187
1660
  //#endregion
1188
1661
  //#region src/debug/ScopeToString.ts
@@ -1365,53 +1838,184 @@ var ParseError = class extends Error {
1365
1838
  }
1366
1839
  };
1367
1840
  //#endregion
1368
- //#region src/parse/ContentsHelpers.ts
1369
- /** Push partial element onto stack for content collection. */
1370
- function beginElem(ctx, kind, contents = []) {
1371
- ctx.state.context.openElems.push({
1372
- kind,
1373
- contents: [...contents]
1374
- });
1375
- }
1376
- /** Pop element from stack, fill gaps with TextElems, return contents. */
1377
- function finishContents(ctx, start, end) {
1378
- const open = ctx.state.context.openElems.pop();
1379
- if (!open) throw new Error("No open element to close");
1380
- return coverWithText(ctx, open.contents, start, end);
1381
- }
1382
- /** Finish element: get end position, close contents, return complete element. */
1383
- function finishElem(kind, start, ctx, params) {
1384
- const end = ctx.stream.checkpoint();
1385
- return {
1386
- kind,
1387
- start,
1388
- end,
1389
- contents: finishContents(ctx, start, end),
1390
- ...params
1391
- };
1392
- }
1393
- /** Create a TextElem */
1394
- function makeText(srcModule, start, end) {
1395
- return {
1396
- 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,
1397
1927
  start,
1398
1928
  end,
1399
1929
  srcModule
1400
1930
  };
1931
+ if (lineBreaksBefore(srcModule.src, start) >= 2) comment.blankBefore = true;
1932
+ return comment;
1401
1933
  }
1402
- /** Fill gaps between child elements with TextElems. */
1403
- function coverWithText(ctx, contents, start, end) {
1404
- const { srcModule } = ctx.state.stable;
1405
- const sorted = contents.slice().sort((a, b) => a.start - b.start);
1406
- const elems = [];
1407
- let pos = start;
1408
- for (const elem of sorted) {
1409
- if (pos < elem.start) elems.push(makeText(srcModule, pos, elem.start));
1410
- elems.push(elem);
1411
- pos = elem.end;
1412
- }
1413
- if (pos < end) elems.push(makeText(srcModule, pos, end));
1414
- 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;
1415
2019
  }
1416
2020
  //#endregion
1417
2021
  //#region src/parse/ExpressionUtil.ts
@@ -1425,9 +2029,11 @@ function makeLiteral(token) {
1425
2029
  };
1426
2030
  }
1427
2031
  function makeUnaryOperator(token) {
2032
+ const [start, end] = token.span;
1428
2033
  return {
1429
2034
  value: token.text,
1430
- span: token.span
2035
+ start,
2036
+ end
1431
2037
  };
1432
2038
  }
1433
2039
  function makeBinaryOperator(token) {
@@ -1437,12 +2043,11 @@ function makeBinaryOperator(token) {
1437
2043
  };
1438
2044
  }
1439
2045
  function makeUnaryExpression(operator, expr) {
1440
- const [start] = operator.span;
1441
2046
  return {
1442
2047
  kind: "unary-expression",
1443
2048
  operator,
1444
2049
  expression: expr,
1445
- start,
2050
+ start: operator.start,
1446
2051
  end: expr.end
1447
2052
  };
1448
2053
  }
@@ -1503,13 +2108,16 @@ function expectWord(stream, errorMsg) {
1503
2108
  stream.nextToken();
1504
2109
  return token;
1505
2110
  }
1506
- /** Parse expression and throw ParseError if not found. */
2111
+ /** Parse a required expression, throwing if absent. */
1507
2112
  function expectExpression(ctx, errorMsg = "Expected expression") {
1508
2113
  const expr = parseExpression(ctx);
1509
2114
  if (!expr) throwParseError(ctx.stream, errorMsg);
1510
- if (ctx.options.preserveExpressions) ctx.addElem(expr);
1511
2115
  return expr;
1512
2116
  }
2117
+ /** Parse an optional expression. */
2118
+ function parseContentExpression(ctx, opts) {
2119
+ return parseExpression(ctx, opts);
2120
+ }
1513
2121
  /** Throw a ParseError at the current/next token position. */
1514
2122
  function throwParseError(stream, message) {
1515
2123
  const weslStream = stream;
@@ -1565,18 +2173,27 @@ function makeRefIdentElem(ctx, refIdent, start, end) {
1565
2173
  refIdent.refIdentElem = elem;
1566
2174
  return elem;
1567
2175
  }
1568
- function isConditionalAttribute$1(attr) {
2176
+ /** @return true if the attribute is a conditional (@if, @elif, @else) */
2177
+ function isConditionalAttribute(attr) {
1569
2178
  const { kind } = attr;
1570
2179
  return kind === "@if" || kind === "@elif" || kind === "@else";
1571
2180
  }
1572
2181
  /** @return true if any attribute is a conditional (@if, @elif, @else) */
1573
2182
  function hasConditionalAttribute(attributes) {
1574
- 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;
1575
2188
  }
1576
2189
  /** Attach non-empty attributes array to element. */
1577
2190
  function attachAttributes(elem, attributes) {
1578
2191
  if (attributes?.length) elem.attributes = attributes;
1579
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
+ }
1580
2197
  /** Link a DeclIdentElem's ident to its parent declaration. */
1581
2198
  function linkDeclIdentElem(declIdentElem, declElem) {
1582
2199
  declIdentElem.ident.declElem = declElem;
@@ -1719,10 +2336,7 @@ function parseIdent(ctx, conditionRef) {
1719
2336
  const ident = ctx.createRefIdent(parts.join("::"));
1720
2337
  if (conditionRef) ident.conditionRef = true;
1721
2338
  const refIdentElem = makeRefIdentElem(ctx, ident, start, end);
1722
- if (!conditionRef) {
1723
- ctx.saveIdent(ident);
1724
- ctx.addElem(refIdentElem);
1725
- }
2339
+ if (!conditionRef) ctx.saveIdent(ident);
1726
2340
  return refIdentElem;
1727
2341
  }
1728
2342
  /** Check if token is valid as a path segment (word or allowed keyword). */
@@ -1744,19 +2358,20 @@ function parseSimpleTypeRef(ctx) {
1744
2358
  if (!path) return null;
1745
2359
  const { parts, start, end: nameEnd } = path;
1746
2360
  const refIdent = ctx.createRefIdent(parts.join("::"));
1747
- beginElem(ctx, "type");
1748
- const refIdentElem = makeRefIdentElem(ctx, refIdent, start, nameEnd);
2361
+ makeRefIdentElem(ctx, refIdent, start, nameEnd);
1749
2362
  ctx.saveIdent(refIdent);
1750
- ctx.addElem(refIdentElem);
1751
- return finishElem("type", start, ctx, {
2363
+ return {
2364
+ kind: "type",
1752
2365
  name: refIdent,
1753
- templateParams: ctx.stream.nextTemplateStartToken() ? parseTemplateParams(ctx) : void 0
1754
- });
2366
+ templateParams: ctx.stream.nextTemplateStartToken() ? parseTemplateParams(ctx) : void 0,
2367
+ start,
2368
+ end: ctx.stream.checkpoint()
2369
+ };
1755
2370
  }
1756
2371
  /** Parse comma-separated template parameters until closing '>'. */
1757
2372
  function parseTemplateParams(ctx) {
1758
2373
  const { stream } = ctx;
1759
- if (consumeTemplateEnd(stream)) return [];
2374
+ if (consumeTemplateEnd(stream)) throwParseError(stream, "Empty template parameter list '<>'");
1760
2375
  const params = [parseTemplateParam(ctx)];
1761
2376
  while (stream.matchText(",")) params.push(parseTemplateParam(ctx));
1762
2377
  if (!consumeTemplateEnd(stream)) throwParseError(stream, "Expected '>' or ',' after template parameter");
@@ -1771,8 +2386,8 @@ function consumeTemplateEnd(stream) {
1771
2386
  /** Grammar: template_arg_expression : expression */
1772
2387
  function parseTemplateParam(ctx) {
1773
2388
  const expr = parseExpression(ctx, { inTemplate: true });
1774
- if (expr) return expr;
1775
- throwParseError(ctx.stream, "Expected expression in template parameters");
2389
+ if (!expr) throwParseError(ctx.stream, "Expected expression in template parameters");
2390
+ return expr;
1776
2391
  }
1777
2392
  //#endregion
1778
2393
  //#region src/parse/ParseCall.ts
@@ -1783,7 +2398,7 @@ function parseTemplateParam(ctx) {
1783
2398
  function parseCallSuffix(ctx, current, parseExpr) {
1784
2399
  if (current.kind !== "ref" && current.kind !== "type") return null;
1785
2400
  const { stream } = ctx;
1786
- const templateArgs = current.kind === "type" ? current.templateParams ?? null : parseCallTemplateArgs(ctx);
2401
+ const templateArgs = current.kind === "ref" ? parseCallTemplateArgs(ctx) : null;
1787
2402
  if (!stream.matchText("(")) return null;
1788
2403
  const args = [];
1789
2404
  while (true) {
@@ -1948,8 +2563,7 @@ function parseTemplateElaboratedIdent(ctx, conditionRef) {
1948
2563
  name: refIdent.ident,
1949
2564
  templateParams,
1950
2565
  start: refIdent.start,
1951
- end: ctx.stream.checkpoint(),
1952
- contents: []
2566
+ end: ctx.stream.checkpoint()
1953
2567
  };
1954
2568
  }
1955
2569
  /** Parse postfix operators: member access, indexing, function calls. */
@@ -1976,7 +2590,9 @@ function parseMemberAccess(ctx, base) {
1976
2590
  function parseIndexAccess(ctx, base) {
1977
2591
  const { stream } = ctx;
1978
2592
  if (!stream.matchText("[")) return null;
1979
- 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]);
1980
2596
  }
1981
2597
  //#endregion
1982
2598
  //#region src/parse/ParseAttribute.ts
@@ -1997,6 +2613,20 @@ function parseElseAttribute(ctx) {
1997
2613
  function parseElifAttribute(ctx) {
1998
2614
  return parseConditionalAttribute(ctx, "elif", makeElifAttribute);
1999
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
+ }
2000
2630
  /**
2001
2631
  * Grammar: attribute :
2002
2632
  * '@' ident_pattern_token argument_expression_list ?
@@ -2007,15 +2637,8 @@ function parseElifAttribute(ctx) {
2007
2637
  * WESL extensions: @if, @elif, @else
2008
2638
  */
2009
2639
  function parseAttribute(ctx) {
2010
- const { stream } = ctx;
2011
- const startPos = stream.checkpoint();
2012
- if (!stream.matchText("@")) return null;
2013
- stream.reset(startPos);
2014
- const weslAttr = parseWeslConditional(ctx);
2015
- if (weslAttr) return weslAttr;
2016
- const stdAttr = parseStandardAttribute(ctx);
2017
- if (stdAttr) return stdAttr;
2018
- return null;
2640
+ if (ctx.stream.peek()?.text !== "@") return null;
2641
+ return parseWeslConditional(ctx) ?? parseStandardAttribute(ctx);
2019
2642
  }
2020
2643
  /** Parse `@if(expr)` or `@elif(expr)` conditional attributes. */
2021
2644
  function parseConditionalAttribute(ctx, keyword, makeAttr) {
@@ -2024,13 +2647,15 @@ function parseConditionalAttribute(ctx, keyword, makeAttr) {
2024
2647
  if (!stream.matchSequence("@", keyword)) return null;
2025
2648
  expect(stream, "(", `@${keyword}`);
2026
2649
  const expr = parseExpression(ctx, true);
2027
- if (!expr) return null;
2650
+ if (!expr) throwParseError(stream, `Expected expression after @${keyword}(`);
2028
2651
  stream.matchText(",");
2029
2652
  expect(stream, ")", `@${keyword} expression`);
2030
- return makeAttr(makeTranslateTimeExpressionElem({
2031
- value: expr,
2032
- span: [startPos, stream.checkpoint()]
2033
- }));
2653
+ return makeAttr({
2654
+ kind: "translate-time-expression",
2655
+ expression: expr,
2656
+ start: startPos,
2657
+ end: stream.checkpoint()
2658
+ });
2034
2659
  }
2035
2660
  function makeIfAttribute(param) {
2036
2661
  return {
@@ -2047,19 +2672,13 @@ function makeElifAttribute(param) {
2047
2672
  param
2048
2673
  };
2049
2674
  }
2050
- /** Parse WESL conditional attributes (@if, @elif, @else) */
2051
- function parseWeslConditional(ctx) {
2052
- const { stream } = ctx;
2053
- const peeked = stream.peek();
2054
- if (peeked?.text !== "@") return null;
2055
- const startPos = peeked.span[0];
2056
- const ifAttr = parseIfAttribute(ctx);
2057
- if (ifAttr) return attributeElem(ifAttr, startPos, stream.checkpoint());
2058
- const elifAttr = parseElifAttribute(ctx);
2059
- if (elifAttr) return attributeElem(elifAttr, startPos, stream.checkpoint());
2060
- const elseAttr = parseElseAttribute(ctx);
2061
- if (elseAttr) return attributeElem(elseAttr, startPos, stream.checkpoint());
2062
- return null;
2675
+ function attributeElem(attribute, start, end) {
2676
+ return {
2677
+ kind: "attribute",
2678
+ attribute,
2679
+ start,
2680
+ end
2681
+ };
2063
2682
  }
2064
2683
  /** Parse a standard attribute (not @if/@elif/@else) */
2065
2684
  function parseStandardAttribute(ctx) {
@@ -2069,7 +2688,7 @@ function parseStandardAttribute(ctx) {
2069
2688
  if (!atToken) return null;
2070
2689
  const startPos = atToken.span[0];
2071
2690
  const nameToken = stream.peek();
2072
- if (!nameToken || nameToken.kind !== "word" && nameToken.kind !== "keyword") {
2691
+ if (nameToken?.kind !== "word" && nameToken?.kind !== "keyword") {
2073
2692
  stream.reset(resetPos);
2074
2693
  return null;
2075
2694
  }
@@ -2092,22 +2711,6 @@ function parseStandardAttribute(ctx) {
2092
2711
  params
2093
2712
  }, startPos, stream.checkpoint());
2094
2713
  }
2095
- function makeTranslateTimeExpressionElem(args) {
2096
- return {
2097
- kind: "translate-time-expression",
2098
- expression: args.value,
2099
- span: args.span
2100
- };
2101
- }
2102
- function attributeElem(attribute, start, end) {
2103
- return {
2104
- kind: "attribute",
2105
- attribute,
2106
- start,
2107
- end,
2108
- contents: []
2109
- };
2110
- }
2111
2714
  function parseBuiltinAttribute(ctx, startPos) {
2112
2715
  const { stream } = ctx;
2113
2716
  expect(stream, "(", "@builtin");
@@ -2128,9 +2731,6 @@ function parseInterpolateAttribute(ctx, startPos) {
2128
2731
  params
2129
2732
  }, startPos, stream.checkpoint());
2130
2733
  }
2131
- function parseNameElem(ctx) {
2132
- return makeNameElem(expectWord(ctx.stream, "Expected identifier"));
2133
- }
2134
2734
  /** @diagnostic(severity, rule) or @diagnostic(severity, namespace.rule) */
2135
2735
  function parseDiagnosticAttribute(ctx, startPos) {
2136
2736
  const { stream } = ctx;
@@ -2152,78 +2752,20 @@ function parseDiagnosticAttribute(ctx, startPos) {
2152
2752
  function parseAttributeParams(ctx) {
2153
2753
  return parseCommaList(ctx, parseAttrParam);
2154
2754
  }
2755
+ function parseNameElem(ctx) {
2756
+ return makeNameElem(expectWord(ctx.stream, "Expected identifier"));
2757
+ }
2155
2758
  function parseAttrParam(ctx) {
2156
2759
  const { stream } = ctx;
2157
2760
  const start = stream.checkpoint();
2158
- beginElem(ctx, "expression");
2159
- parseExpression(ctx);
2160
- const end = stream.checkpoint();
2761
+ const expression = parseExpression(ctx);
2762
+ if (!expression) throwParseError(stream, "Expected attribute parameter");
2161
2763
  return {
2162
2764
  kind: "expression",
2765
+ expression,
2163
2766
  start,
2164
- end,
2165
- contents: finishContents(ctx, start, end)
2166
- };
2167
- }
2168
- //#endregion
2169
- //#region src/parse/ParseDirective.ts
2170
- /** Grammar: global_directive : diagnostic_directive | enable_directive | requires_directive */
2171
- function parseDirective(ctx) {
2172
- const { stream } = ctx;
2173
- const startPos = stream.checkpoint();
2174
- const attributes = parseAttributeList(ctx);
2175
- const attrs = attributes.length > 0 ? attributes : void 0;
2176
- const result = parseExtensionDirective(ctx, "enable", attrs) || parseExtensionDirective(ctx, "requires", attrs) || parseDiagnosticDirective(ctx, attrs);
2177
- if (!result) stream.reset(startPos);
2178
- return result;
2179
- }
2180
- /** Grammar: enable_directive | requires_directive : keyword extension_list ';' */
2181
- function parseExtensionDirective(ctx, keyword, attributes) {
2182
- const { stream } = ctx;
2183
- const token = stream.matchText(keyword);
2184
- if (!token) return null;
2185
- const extensions = parseCommaList(ctx, parseDirectiveName);
2186
- expect(stream, ";", `${keyword} directive`);
2187
- return makeDirectiveElem({
2188
- kind: keyword,
2189
- extensions
2190
- }, token, stream, attributes);
2191
- }
2192
- function parseDirectiveName(ctx) {
2193
- return makeNameElem(expectWord(ctx.stream, "Expected identifier in name list"));
2194
- }
2195
- /**
2196
- * Grammar: diagnostic_directive : 'diagnostic' diagnostic_control ';'
2197
- * Grammar: diagnostic_control : '(' severity_control_name ',' diagnostic_rule_name ',' ? ')'
2198
- */
2199
- function parseDiagnosticDirective(ctx, attributes) {
2200
- const { stream } = ctx;
2201
- const token = stream.matchText("diagnostic");
2202
- if (!token) return null;
2203
- expect(stream, "(", "diagnostic");
2204
- const severity = makeNameElem(expectWord(stream, "Expected severity in diagnostic"));
2205
- expect(stream, ",", "diagnostic severity");
2206
- const ruleName = makeNameElem(expectWord(stream, "Expected rule name in diagnostic"));
2207
- let subrule = null;
2208
- if (stream.matchText(".")) subrule = makeNameElem(expectWord(stream, "Expected subrule name after '.'"));
2209
- stream.matchText(",");
2210
- expect(stream, ")", "diagnostic rule");
2211
- expect(stream, ";", "diagnostic directive");
2212
- return makeDirectiveElem({
2213
- kind: "diagnostic",
2214
- severity,
2215
- rule: [ruleName, subrule]
2216
- }, token, stream, attributes);
2217
- }
2218
- function makeDirectiveElem(directive, token, stream, attributes) {
2219
- const elem = {
2220
- kind: "directive",
2221
- directive,
2222
- start: attributes?.[0]?.start ?? token.span[0],
2223
2767
  end: stream.checkpoint()
2224
2768
  };
2225
- attachAttributes(elem, attributes);
2226
- return elem;
2227
2769
  }
2228
2770
  //#endregion
2229
2771
  //#region src/parse/ParseControlFlow.ts
@@ -2234,37 +2776,50 @@ function makeDirectiveElem(directive, token, stream, attributes) {
2234
2776
  function parseIfStatement(ctx, attributes) {
2235
2777
  const startPos = beginStatement(ctx, "if", attributes);
2236
2778
  if (startPos === null) return null;
2237
- expectExpression(ctx, "Expected condition expression after 'if'");
2238
- const body = expectCompound(ctx, "Expected '{' after if condition");
2239
- ctx.addElem(body);
2240
- parseElseChain(ctx);
2241
- 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);
2242
2784
  }
2243
2785
  /** Grammar: switch_statement : attribute* 'switch' expression switch_body */
2244
2786
  function parseSwitchStatement(ctx, attributes) {
2245
2787
  const startPos = beginStatement(ctx, "switch", attributes);
2246
2788
  if (startPos === null) return null;
2247
- expectExpression(ctx, "Expected expression after 'switch'");
2248
- expectSwitchClauses(ctx);
2249
- 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);
2250
2796
  }
2251
2797
  /**
2252
2798
  * Grammar: else_if_clause : 'else' 'if' expression compound_statement
2253
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.
2254
2803
  */
2255
2804
  function parseElseChain(ctx) {
2256
2805
  const { stream } = ctx;
2257
- while (stream.matchText("else")) {
2258
- if (stream.matchText("if")) {
2259
- expectExpression(ctx, "Expected expression after 'else if'");
2260
- const body = expectCompound(ctx, "Expected '{' after else if");
2261
- ctx.addElem(body);
2262
- continue;
2263
- }
2264
- const body = expectCompound(ctx, "Expected '{' after else");
2265
- ctx.addElem(body);
2266
- 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
+ };
2267
2821
  }
2822
+ return expectCompound(ctx, "Expected '{' after else");
2268
2823
  }
2269
2824
  /**
2270
2825
  * Grammar: switch_body : attribute* '{' switch_clause+ '}'
@@ -2274,27 +2829,43 @@ function parseElseChain(ctx) {
2274
2829
  */
2275
2830
  function expectSwitchClauses(ctx) {
2276
2831
  const { stream } = ctx;
2277
- parseAttributeList(ctx);
2832
+ const bodyAttrs = parseAttributeList(ctx);
2278
2833
  expect(stream, "{", "switch expression");
2279
- while (!stream.matchText("}")) {
2280
- const clauseStart = stream.checkpoint();
2281
- const clauseAttrs = parseAttributeList(ctx);
2282
- beginElem(ctx, "switch-clause", clauseAttrs.length ? clauseAttrs : void 0);
2283
- if (stream.matchText("case")) {
2284
- parseCaseSelectors(ctx);
2285
- parseCaseBody(ctx, "Expected '{' after case value");
2286
- } else if (stream.matchText("default")) parseCaseBody(ctx, "Expected '{' after 'default'");
2287
- else throwParseError(stream, "Expected 'case', 'default', or '}' in switch");
2288
- const clauseElem = finishElem("switch-clause", clauseStart, ctx, {});
2289
- attachAttributes(clauseElem, clauseAttrs.length ? clauseAttrs : void 0);
2290
- 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'");
2291
2857
  }
2858
+ return finishStatement("switch-clause", clauseStart, ctx, {
2859
+ selectors,
2860
+ body
2861
+ }, attrs);
2292
2862
  }
2293
2863
  /** Grammar: case_selectors : case_selector (',' case_selector)* ','? */
2294
2864
  function parseCaseSelectors(ctx) {
2295
2865
  const { stream } = ctx;
2296
- expectExpression(ctx, "Expected expression after 'case'");
2297
- 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;
2298
2869
  }
2299
2870
  /**
2300
2871
  * Grammar: case_clause : 'case' case_selectors ':'? compound_statement
@@ -2302,16 +2873,15 @@ function parseCaseSelectors(ctx) {
2302
2873
  */
2303
2874
  function parseCaseBody(ctx, errorMsg) {
2304
2875
  ctx.stream.matchText(":");
2305
- const bodyAttrs = parseAttributeList(ctx);
2306
- const body = parseCompoundStatement(ctx, bodyAttrs.length > 0 ? bodyAttrs : void 0);
2876
+ const body = parseCompoundStatement(ctx, attrsOrUndef(parseAttributeList(ctx)));
2307
2877
  if (!body) throwParseError(ctx.stream, errorMsg);
2308
- ctx.addElem(body);
2878
+ return body;
2309
2879
  }
2310
2880
  //#endregion
2311
2881
  //#region src/parse/ParseValueDeclaration.ts
2312
2882
  /** Grammar: 'const' optionally_typed_ident '=' expression (global or local) */
2313
2883
  function parseConstDecl(ctx, attributes) {
2314
- return parseValueDecl(ctx, "const", true, isModuleScope(ctx), attributes);
2884
+ return parseValueDecl(ctx, "const", true, ctx.isModuleScope(), attributes);
2315
2885
  }
2316
2886
  /** Grammar: 'override' optionally_typed_ident ( '=' expression )? */
2317
2887
  function parseOverrideDecl(ctx, attributes) {
@@ -2322,20 +2892,16 @@ function parseTypedDecl(ctx, isGlobal = true) {
2322
2892
  const nameToken = ctx.stream.matchKind("word");
2323
2893
  if (!nameToken) return null;
2324
2894
  const start = nameToken.span[0];
2325
- beginElem(ctx, "typeDecl");
2326
2895
  const decl = createDeclIdentElem(ctx, nameToken, isGlobal);
2327
- ctx.addElem(decl);
2328
2896
  ctx.saveIdent(decl.ident);
2329
2897
  const { typeRef, typeScope } = parseOptionalType(ctx);
2330
- const end = ctx.stream.checkpoint();
2331
2898
  return {
2332
2899
  kind: "typeDecl",
2333
2900
  decl,
2334
2901
  typeRef,
2335
2902
  typeScope,
2336
2903
  start,
2337
- end,
2338
- contents: finishContents(ctx, start, end)
2904
+ end: ctx.stream.checkpoint()
2339
2905
  };
2340
2906
  }
2341
2907
  /** Shared parser for const/override declarations. */
@@ -2345,43 +2911,29 @@ function parseValueDecl(ctx, keyword, requiresInit, isGlobal, attributes) {
2345
2911
  if (!token) return null;
2346
2912
  const startPos = getStartWithAttributes(attributes, token.span[0]);
2347
2913
  ctx.pushScope("partial");
2348
- beginElem(ctx, keyword, attributes);
2349
2914
  const typedDecl = parseTypedDecl(ctx, isGlobal);
2350
2915
  if (!typedDecl) throwParseError(stream, `Expected identifier after '${keyword}'`);
2351
- ctx.addElem(typedDecl);
2916
+ let init;
2352
2917
  if (requiresInit) {
2353
2918
  expect(stream, "=", `${keyword} identifier`);
2354
- expectExpression(ctx);
2355
- } else if (stream.matchText("=")) expectExpression(ctx);
2919
+ init = expectExpression(ctx);
2920
+ } else if (stream.matchText("=")) init = expectExpression(ctx);
2356
2921
  expect(stream, ";", `${keyword} declaration`);
2357
- const endPos = stream.checkpoint();
2358
- const contents = finishContents(ctx, startPos, endPos);
2359
2922
  typedDecl.decl.ident.dependentScope = ctx.currentScope();
2360
2923
  ctx.popScope();
2361
- const elem = {
2362
- kind: keyword,
2924
+ const elem = finishStatement(keyword, startPos, ctx, {
2363
2925
  name: typedDecl,
2364
- start: startPos,
2365
- end: endPos,
2366
- contents
2367
- };
2368
- attachAttributes(elem, attributes);
2926
+ init
2927
+ }, attributes);
2369
2928
  linkDeclIdent(typedDecl, elem);
2370
2929
  return elem;
2371
2930
  }
2372
- /** @return true if ctx is at module level (not inside fn/block) */
2373
- function isModuleScope(ctx) {
2374
- let scope = ctx.currentScope();
2375
- while (scope.kind === "partial" && scope.parent) scope = scope.parent;
2376
- return scope.parent === null;
2377
- }
2378
2931
  /** Parse optional ': type' annotation, managing scope for type references. */
2379
2932
  function parseOptionalType(ctx) {
2380
2933
  if (!ctx.stream.matchText(":")) return {};
2381
2934
  ctx.pushScope();
2382
2935
  const typeRef = parseSimpleTypeRef(ctx);
2383
2936
  if (!typeRef) throwParseError(ctx.stream, "Expected type after ':'");
2384
- ctx.addElem(typeRef);
2385
2937
  const typeScope = ctx.currentScope();
2386
2938
  ctx.popScope();
2387
2939
  return {
@@ -2401,17 +2953,19 @@ function parseGlobalVarDecl(ctx, attributes) {
2401
2953
  if (!varToken) return null;
2402
2954
  const startPos = getStartWithAttributes(attributes, varToken.span[0]);
2403
2955
  ctx.pushScope("partial");
2404
- beginElem(ctx, "gvar", attributes);
2405
- skipTemplateList(ctx);
2956
+ const template = parseTemplateList(ctx);
2406
2957
  const typedDecl = parseTypedDecl(ctx);
2407
2958
  if (!typedDecl) throwParseError(stream, "Expected identifier after 'var'");
2408
- ctx.addElem(typedDecl);
2409
- if (stream.matchText("=")) expectExpression(ctx);
2959
+ let init;
2960
+ if (stream.matchText("=")) init = expectExpression(ctx);
2410
2961
  expect(stream, ";", "var declaration");
2411
2962
  typedDecl.decl.ident.dependentScope = ctx.currentScope();
2412
2963
  ctx.popScope();
2413
- const varElem = finishElem("gvar", startPos, ctx, { name: typedDecl });
2414
- attachAttributes(varElem, attributes);
2964
+ const varElem = finishStatement("gvar", startPos, ctx, {
2965
+ name: typedDecl,
2966
+ template,
2967
+ init
2968
+ }, attributes);
2415
2969
  linkDeclIdent(typedDecl, varElem);
2416
2970
  return varElem;
2417
2971
  }
@@ -2421,23 +2975,19 @@ function parseAliasDecl(ctx, attributes) {
2421
2975
  const aliasToken = stream.matchText("alias");
2422
2976
  if (!aliasToken) return null;
2423
2977
  const startPos = getStartWithAttributes(attributes, aliasToken.span[0]);
2424
- beginElem(ctx, "alias", attributes);
2425
2978
  const declIdentElem = createDeclIdentElem(ctx, expectWord(stream, "Expected identifier after 'alias'"), true);
2426
- ctx.addElem(declIdentElem);
2427
2979
  ctx.saveIdent(declIdentElem.ident);
2428
2980
  expect(stream, "=", "alias name");
2429
2981
  ctx.pushScope();
2430
2982
  const typeRef = parseSimpleTypeRef(ctx);
2431
2983
  if (!typeRef) throwParseError(stream, "Expected type after '=' in alias declaration");
2432
- ctx.addElem(typeRef);
2433
2984
  declIdentElem.ident.dependentScope = ctx.currentScope();
2434
2985
  ctx.popScope();
2435
2986
  expect(stream, ";", "alias declaration");
2436
- const aliasElem = finishElem("alias", startPos, ctx, {
2987
+ const aliasElem = finishStatement("alias", startPos, ctx, {
2437
2988
  name: declIdentElem,
2438
2989
  typeRef
2439
- });
2440
- attachAttributes(aliasElem, attributes);
2990
+ }, attributes);
2441
2991
  linkDeclIdentElem(declIdentElem, aliasElem);
2442
2992
  return aliasElem;
2443
2993
  }
@@ -2446,25 +2996,29 @@ function parseConstAssert(ctx, attributes) {
2446
2996
  const assertToken = ctx.stream.matchText("const_assert");
2447
2997
  if (!assertToken) return null;
2448
2998
  const startPos = getStartWithAttributes(attributes, assertToken.span[0]);
2449
- beginElem(ctx, "assert", attributes);
2450
- expectExpression(ctx);
2999
+ const expression = expectExpression(ctx);
2451
3000
  expect(ctx.stream, ";", "const_assert expression");
2452
- const elem = finishElem("assert", startPos, ctx, {});
2453
- attachAttributes(elem, attributes);
2454
- return elem;
3001
+ return finishStatement("assert", startPos, ctx, { expression }, attributes);
2455
3002
  }
2456
- /** Skip optional template list (e.g., <storage, read_write>). */
2457
- 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) {
2458
3009
  const { stream } = ctx;
2459
- if (!stream.nextTemplateStartToken()) return;
3010
+ if (!stream.nextTemplateStartToken()) return void 0;
3011
+ const names = [];
2460
3012
  while (true) {
2461
3013
  const next = stream.peek();
2462
3014
  if (!next) throwParseError(stream, "Unclosed template in var declaration");
2463
3015
  if (next.text.startsWith(">")) {
2464
3016
  stream.nextTemplateEndToken();
2465
- return;
3017
+ return names;
2466
3018
  }
2467
- 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");
2468
3022
  }
2469
3023
  }
2470
3024
  //#endregion
@@ -2486,18 +3040,20 @@ function parseVarOrLet(ctx, keyword, hasTemplate, requiresInit, attributes) {
2486
3040
  const token = stream.matchText(keyword);
2487
3041
  if (!token) return null;
2488
3042
  const startPos = getStartWithAttributes(attributes, token.span[0]);
2489
- beginElem(ctx, keyword, attributes);
2490
- if (hasTemplate) skipTemplateList(ctx);
3043
+ const template = hasTemplate ? parseTemplateList(ctx) : void 0;
2491
3044
  const typedDecl = parseTypedDecl(ctx, false);
2492
3045
  if (!typedDecl) throwParseError(stream, `Expected identifier after '${keyword}'`);
2493
- ctx.addElem(typedDecl);
3046
+ let init;
2494
3047
  if (requiresInit) {
2495
3048
  expect(stream, "=", `${keyword} identifier (${keyword} requires initialization)`);
2496
- expectExpression(ctx);
2497
- } else if (stream.matchText("=")) expectExpression(ctx);
3049
+ init = expectExpression(ctx);
3050
+ } else if (stream.matchText("=")) init = expectExpression(ctx);
2498
3051
  expect(stream, ";", `${keyword} declaration`);
2499
- const elem = finishElem(keyword, startPos, ctx, { name: typedDecl });
2500
- 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;
2501
3057
  linkDeclIdent(typedDecl, elem);
2502
3058
  return elem;
2503
3059
  }
@@ -2526,17 +3082,42 @@ const assignmentOps = new Set([
2526
3082
  function parseSimpleStatement(ctx, attributes) {
2527
3083
  const { stream } = ctx;
2528
3084
  const startPos = getStartWithAttributes(attributes, stream.checkpoint());
2529
- 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
+ };
2530
3113
  }
2531
3114
  /** Grammar: return_statement : 'return' expression? ';' */
2532
3115
  function parseReturnStmt(ctx, startPos, attributes) {
2533
3116
  const { stream } = ctx;
2534
3117
  if (!stream.matchText("return")) return null;
2535
- beginElem(ctx, "statement", attributes);
2536
- const expr = parseExpression(ctx);
2537
- if (expr && ctx.options.preserveExpressions) ctx.addElem(expr);
3118
+ const value = parseContentExpression(ctx) ?? void 0;
2538
3119
  expect(stream, ";", "return statement");
2539
- return finishBlockStatement(startPos, ctx, attributes);
3120
+ return finishStatement("return", startPos, ctx, { value }, attributes);
2540
3121
  }
2541
3122
  /**
2542
3123
  * Grammar: break_statement : 'break' ';'
@@ -2545,70 +3126,79 @@ function parseReturnStmt(ctx, startPos, attributes) {
2545
3126
  function parseBreakStmt(ctx, startPos, attributes) {
2546
3127
  const { stream } = ctx;
2547
3128
  if (!stream.matchText("break")) return null;
2548
- beginElem(ctx, "statement", attributes);
2549
- 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'");
2550
3131
  expect(stream, ";", "break statement");
2551
- 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);
2552
3140
  }
2553
- /** Grammar: continue_statement : 'continue' ';' also handles 'discard' */
2554
- function parseKeywordStmt(ctx, startPos, attributes, keyword) {
3141
+ /** Grammar: 'discard' ';' */
3142
+ function parseDiscardStmt(ctx, startPos, attributes) {
2555
3143
  const { stream } = ctx;
2556
- if (!stream.matchText(keyword)) return null;
2557
- beginElem(ctx, "statement", attributes);
2558
- expect(stream, ";", `${keyword} statement`);
2559
- return finishBlockStatement(startPos, ctx, attributes);
3144
+ if (!stream.matchText("discard")) return null;
3145
+ expect(stream, ";", "discard statement");
3146
+ return finishStatement("discard", startPos, ctx, {}, attributes);
2560
3147
  }
2561
- /** Parse empty statement (just ';'). */
3148
+ /** Parse empty statement (just ';'). Spans the ';' so it emits no extra text. */
2562
3149
  function parseEmptyStmt(stream, start) {
2563
3150
  if (!stream.matchText(";")) return null;
2564
3151
  return {
2565
- kind: "statement",
3152
+ kind: "empty",
2566
3153
  start,
2567
- end: stream.checkpoint(),
2568
- contents: []
3154
+ end: stream.checkpoint()
2569
3155
  };
2570
3156
  }
2571
3157
  /** Grammar: assignment_statement : '_' '=' expression ';' (phony assignment) */
2572
3158
  function parsePhonyAssignment(ctx, startPos, attributes) {
2573
3159
  const { stream } = ctx;
2574
- if (!stream.matchText("_")) return null;
2575
- if (!parseAssignmentOperator(stream)) throwParseError(stream, "Expected assignment operator after '_'");
2576
- beginElem(ctx, "statement", attributes);
2577
- 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 '_ ='");
2578
3173
  expect(stream, ";", "assignment");
2579
- return finishBlockStatement(startPos, ctx, attributes);
3174
+ return finishStatement("assign", startPos, ctx, {
3175
+ lhs,
3176
+ op,
3177
+ rhs
3178
+ }, attributes);
2580
3179
  }
2581
- /**
2582
- * Parses expression statements: assignments, increments/decrements, or function calls.
2583
- * Grammar: ( assignment_statement | increment_statement | decrement_statement | call_phrase ) ';'
2584
- */
3180
+ /** Grammar: ( assignment_statement | increment_statement | decrement_statement | call_phrase ) ';' */
2585
3181
  function parseExpressionStmt(ctx, startPos, attributes) {
2586
3182
  const { stream } = ctx;
2587
- beginElem(ctx, "statement", attributes);
2588
3183
  const expr = parseExpression(ctx);
2589
3184
  if (!expr) {
2590
- finishContents(ctx, startPos, startPos);
2591
3185
  stream.reset(startPos);
2592
3186
  return null;
2593
3187
  }
2594
- if (ctx.options.preserveExpressions) ctx.addElem(expr);
2595
- 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);
2596
3194
  expect(stream, ";", "expression");
2597
- return finishBlockStatement(startPos, ctx, attributes);
2598
- }
2599
- /** Grammar: assignment_statement : lhs_expression ( '=' | compound_assignment_operator ) expression */
2600
- function parseAssignmentOperator(stream) {
2601
- return !!stream.nextIf(({ text }) => assignmentOps.has(text));
2602
- }
2603
- /** Grammar: ( '=' | compound_assignment_operator ) expression (rhs of assignment_statement) */
2604
- function parseAssignmentRhs(ctx) {
2605
- if (!parseAssignmentOperator(ctx.stream)) return false;
2606
- expectExpression(ctx, "Expected expression after assignment operator");
2607
- return true;
2608
- }
2609
- /** Grammar: increment_statement : lhs_expression '++' ; decrement_statement : lhs_expression '--' */
2610
- function parseIncDecOperator(stream) {
2611
- 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);
2612
3202
  }
2613
3203
  //#endregion
2614
3204
  //#region src/parse/ParseLoop.ts
@@ -2622,41 +3212,48 @@ function parseForStatement(ctx, attributes) {
2622
3212
  if (startPos === null) return null;
2623
3213
  ctx.pushScope();
2624
3214
  expect(stream, "(", "'for'");
2625
- parseForInit(ctx);
2626
- const cond = parseExpression(ctx);
2627
- if (cond && ctx.options.preserveExpressions) ctx.addElem(cond);
3215
+ const init = parseForInit(ctx);
3216
+ const condition = parseContentExpression(ctx) ?? void 0;
2628
3217
  expect(stream, ";", "for loop condition");
2629
- parseForUpdate(ctx);
3218
+ const update = parseForUpdate(ctx);
2630
3219
  expect(stream, ")", "for loop header");
2631
3220
  const body = expectCompound(ctx, "Expected '{' after for loop header");
2632
- ctx.addElem(body);
2633
3221
  ctx.popScope();
2634
- return finishBlockStatement(startPos, ctx, attributes);
3222
+ return finishStatement("for", startPos, ctx, {
3223
+ init,
3224
+ condition,
3225
+ update,
3226
+ body
3227
+ }, attributes);
2635
3228
  }
2636
3229
  /** Grammar: while_statement : attribute* 'while' expression compound_statement */
2637
3230
  function parseWhileStatement(ctx, attributes) {
2638
3231
  const startPos = beginStatement(ctx, "while", attributes);
2639
3232
  if (startPos === null) return null;
2640
- expectExpression(ctx, "Expected condition expression after 'while'");
2641
- const body = expectCompound(ctx, "Expected '{' after while condition");
2642
- ctx.addElem(body);
2643
- 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);
2644
3237
  }
2645
3238
  /** Grammar: loop_statement : attribute* 'loop' attribute* '{' statement* continuing_statement? '}' */
2646
3239
  function parseLoopStatement(ctx, attributes) {
2647
3240
  const startPos = beginStatement(ctx, "loop", attributes);
2648
3241
  if (startPos === null) return null;
2649
3242
  const body = expectCompound(ctx, "Expected '{' after 'loop'", true);
2650
- ctx.addElem(body);
2651
- return finishBlockStatement(startPos, ctx, attributes);
3243
+ return finishStatement("loop", startPos, ctx, {
3244
+ body,
3245
+ continuing: body.body.find((s) => s.kind === "continuing")
3246
+ }, attributes);
2652
3247
  }
2653
3248
  /** Grammar: continuing_statement : 'continuing' continuing_compound_statement */
2654
3249
  function parseContinuingStatement(ctx, attributes) {
2655
- const startPos = beginStatement(ctx, "continuing", attributes, "continuing");
3250
+ const startPos = beginStatement(ctx, "continuing", attributes);
2656
3251
  if (startPos === null) return null;
2657
3252
  const body = expectCompound(ctx, "Expected '{' after 'continuing'");
2658
- ctx.addElem(body);
2659
- 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);
2660
3257
  }
2661
3258
  /** Grammar: for_init? ';'
2662
3259
  * for_init : variable_or_value_statement | variable_updating_statement | func_call_statement
@@ -2664,19 +3261,60 @@ function parseContinuingStatement(ctx, attributes) {
2664
3261
  function parseForInit(ctx) {
2665
3262
  const { stream } = ctx;
2666
3263
  const varDecl = parseLocalVarDecl(ctx);
2667
- if (varDecl) ctx.addElem(varDecl);
2668
- else {
2669
- const expr = parseExpression(ctx);
2670
- if (expr && ctx.options.preserveExpressions) ctx.addElem(expr);
2671
- expect(stream, ";", "for loop init");
2672
- }
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;
2673
3269
  }
2674
- /** Grammar: for_update : variable_updating_statement | func_call_statement
2675
- * variable_updating_statement : assignment_statement | increment_statement | decrement_statement */
3270
+ /** Grammar: for_update : variable_updating_statement | func_call_statement */
2676
3271
  function parseForUpdate(ctx) {
2677
- const expr = parseExpression(ctx);
2678
- if (expr && ctx.options.preserveExpressions) ctx.addElem(expr);
2679
- 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");
2680
3318
  }
2681
3319
  //#endregion
2682
3320
  //#region src/parse/ParseStatement.ts
@@ -2692,17 +3330,17 @@ function parseCompoundStatement(ctx, attributes, options) {
2692
3330
  const brace = ctx.stream.matchText("{");
2693
3331
  if (!brace) return null;
2694
3332
  const startPos = getStartWithAttributes(attributes, brace.span[0]);
2695
- beginElem(ctx, "statement", attributes);
2696
3333
  const skipScope = options?.noScope || hasConditionalAttr(attributes);
2697
3334
  if (!skipScope) ctx.pushScope();
2698
- parseBlockStatements(ctx, options?.loopBody);
3335
+ const body = parseBlockStatements(ctx, options?.loopBody);
2699
3336
  if (!skipScope) ctx.popScope();
2700
- return finishBlockStatement(startPos, ctx, attributes);
3337
+ return finishStatement("block", startPos, ctx, { body }, attributes);
2701
3338
  }
2702
3339
  /** Grammar: attribute* compound_statement (for control flow bodies) */
2703
3340
  function expectCompound(ctx, errorMsg, loopBody) {
2704
3341
  const attrs = parseAttributeList(ctx);
2705
- 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);
2706
3344
  if (!block) throwParseError(ctx.stream, errorMsg);
2707
3345
  return block;
2708
3346
  }
@@ -2710,16 +3348,20 @@ function expectCompound(ctx, errorMsg, loopBody) {
2710
3348
  function getStartWithAttributes(attributes, keywordPos) {
2711
3349
  return attributes?.[0]?.start ?? keywordPos;
2712
3350
  }
2713
- /** Match keyword and begin statement element. Returns start position or null. */
2714
- function beginStatement(ctx, keyword, attributes, kind = "statement") {
2715
- const keywordPos = ctx.stream.checkpoint();
2716
- if (!ctx.stream.matchText(keyword)) return null;
2717
- const startPos = getStartWithAttributes(attributes, keywordPos);
2718
- beginElem(ctx, kind, attributes);
2719
- 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]);
2720
3356
  }
2721
- function finishBlockStatement(start, ctx, attributes, kind = "statement") {
2722
- 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
+ };
2723
3365
  attachAttributes(elem, attributes);
2724
3366
  return elem;
2725
3367
  }
@@ -2729,16 +3371,18 @@ function hasConditionalAttr(attributes) {
2729
3371
  /** Grammar: statement* '}' (after '{' consumed). Loop bodies may end with continuing. */
2730
3372
  function parseBlockStatements(ctx, loopBody) {
2731
3373
  const { stream } = ctx;
3374
+ const body = [];
2732
3375
  while (true) {
2733
3376
  if (stream.matchText("}")) break;
2734
3377
  const stmt = parseStatement(ctx);
2735
3378
  if (!stmt) throwParseError(stream, "Expected statement or '}'");
2736
- ctx.addElem(stmt);
3379
+ body.push(stmt);
2737
3380
  if (loopBody && stmt.kind === "continuing") {
2738
3381
  expect(stream, "}", "continuing block");
2739
3382
  break;
2740
3383
  }
2741
3384
  }
3385
+ return body;
2742
3386
  }
2743
3387
  /**
2744
3388
  * Grammar: statement :
@@ -2759,7 +3403,6 @@ function parseStatement(ctx) {
2759
3403
  }
2760
3404
  const hasConditional = attributes.length > 0 && hasConditionalAttribute(attributes);
2761
3405
  if (hasConditional) ctx.pushScope("partial");
2762
- const attrsOrUndef = attributes.length > 0 ? attributes : void 0;
2763
3406
  const stmt = findMap([
2764
3407
  parseLocalVarDecl,
2765
3408
  parseLetDecl,
@@ -2773,19 +3416,71 @@ function parseStatement(ctx) {
2773
3416
  parseLoopStatement,
2774
3417
  parseContinuingStatement,
2775
3418
  parseSimpleStatement
2776
- ], (p) => p(ctx, attrsOrUndef));
2777
- if (!stmt) return null;
2778
- finalizeConditional$1(ctx, hasConditional, attributes);
2779
- return stmt;
3419
+ ], (p) => p(ctx, attrsOrUndef(attributes)));
3420
+ if (hasConditional) {
3421
+ const partialScope = ctx.popScope();
3422
+ if (stmt) partialScope.condAttribute = conditionalAttribute(attributes);
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);
2780
3471
  }
2781
- function finalizeConditional$1(ctx, hasConditional, attributes) {
2782
- if (hasConditional) {
2783
- const partialScope = ctx.popScope();
2784
- partialScope.condAttribute = getConditionalAttribute(attributes);
2785
- }
3472
+ function parseDirectiveName(ctx) {
3473
+ return makeNameElem(expectWord(ctx.stream, "Expected identifier in name list"));
2786
3474
  }
2787
- function getConditionalAttribute(attributes) {
2788
- 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;
2789
3484
  }
2790
3485
  //#endregion
2791
3486
  //#region src/parse/ParseFn.ts
@@ -2822,8 +3517,7 @@ function parseFnDecl(ctx, attributes) {
2822
3517
  returnType,
2823
3518
  returnAttributes,
2824
3519
  start: startPos,
2825
- end: stream.checkpoint(),
2826
- contents: buildFnContents(attributes, declIdentElem, params, returnType, body)
3520
+ end: stream.checkpoint()
2827
3521
  };
2828
3522
  attachAttributes(fnElem, attributes);
2829
3523
  linkDeclIdentElem(declIdentElem, fnElem);
@@ -2853,37 +3547,63 @@ function parseFnReturn(ctx) {
2853
3547
  if (!returnType) throwParseError(stream, "Expected type after '->'");
2854
3548
  return {
2855
3549
  returnType,
2856
- returnAttributes: attrs.length > 0 ? attrs : void 0
3550
+ returnAttributes: attrsOrUndef(attrs)
2857
3551
  };
2858
3552
  }
2859
- /** Build contents array for function element */
2860
- function buildFnContents(attributes, decl, params, returnType, body) {
2861
- const base = returnType ? [
2862
- decl,
2863
- ...params,
2864
- returnType,
2865
- body
2866
- ] : [
2867
- decl,
2868
- ...params,
2869
- body
2870
- ];
2871
- return attributes?.length ? [...attributes, ...base] : base;
2872
- }
2873
3553
  /** Grammar: param : attribute* optionally_typed_ident */
2874
3554
  function parseFnParam(ctx) {
2875
- const attributes = parseAttributeList(ctx);
3555
+ const attrs = parseAttributeList(ctx);
2876
3556
  if (ctx.stream.peek()?.kind !== "word") return null;
2877
- beginElem(ctx, "param", attributes.length ? attributes : void 0);
3557
+ const attributes = attrsOrUndef(attrs);
2878
3558
  const name = parseTypedDecl(ctx, false);
2879
3559
  if (!name) throw new Error("Unexpected: peek succeeded but parseTypedDecl failed");
2880
- ctx.addElem(name);
2881
- const elem = finishElem("param", getStartWithAttributes(attributes, name.start), ctx, { name });
3560
+ const elem = finishStatement("param", getStartWithAttributes(attributes, name.start), ctx, { name }, attributes);
2882
3561
  linkDeclIdent(name, elem);
2883
- attachAttributes(elem, attributes.length > 0 ? attributes : void 0);
2884
3562
  return elem;
2885
3563
  }
2886
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
2887
3607
  //#region src/parse/ParseImport.ts
2888
3608
  /** WESL Grammar: translation_unit : import_statement* global_directive* global_decl* */
2889
3609
  function parseWeslImports(ctx) {
@@ -3021,52 +3741,39 @@ function parseStructDecl(ctx, attributes) {
3021
3741
  const start = getStartWithAttributes(attributes, structToken.span[0]);
3022
3742
  const identElem = createDeclIdentElem(ctx, expectWord(stream, "Expected identifier after 'struct'"), true);
3023
3743
  ctx.saveIdent(identElem.ident);
3024
- beginElem(ctx, "struct", attributes);
3025
- ctx.addElem(identElem);
3026
3744
  expect(stream, "{", "struct name");
3027
3745
  ctx.pushScope();
3028
- const members = parseStructMembers(ctx);
3746
+ const members = parseCommaList(ctx, parseStructMember);
3029
3747
  identElem.ident.dependentScope = ctx.currentScope();
3030
3748
  ctx.popScope();
3031
3749
  expect(stream, "}", "struct member");
3032
- const elem = finishElem("struct", start, ctx, {
3750
+ const elem = finishStatement("struct", start, ctx, {
3033
3751
  name: identElem,
3034
3752
  members
3035
- });
3036
- attachAttributes(elem, attributes);
3753
+ }, attributes);
3037
3754
  linkDeclIdentElem(identElem, elem);
3038
3755
  return elem;
3039
3756
  }
3040
- /** Grammar: struct_body_decl : '{' struct_member (',' struct_member)* ','? '}' */
3041
- function parseStructMembers(ctx) {
3042
- const members = parseCommaList(ctx, parseStructMember);
3043
- for (const member of members) ctx.addElem(member);
3044
- return members;
3045
- }
3046
3757
  /** Grammar: struct_member : attribute* member_ident ':' type_specifier */
3047
3758
  function parseStructMember(ctx) {
3048
3759
  const { stream } = ctx;
3049
3760
  const checkpoint = stream.checkpoint();
3050
- const attributes = parseAttributeList(ctx);
3761
+ const attrs = parseAttributeList(ctx);
3051
3762
  const nameToken = stream.matchKind("word");
3052
3763
  if (!nameToken) {
3053
3764
  stream.reset(checkpoint);
3054
3765
  return null;
3055
3766
  }
3767
+ const attributes = attrsOrUndef(attrs);
3056
3768
  const start = getStartWithAttributes(attributes, nameToken.span[0]);
3057
- beginElem(ctx, "member", attributes.length ? attributes : void 0);
3058
3769
  const name = makeNameElem(nameToken);
3059
- ctx.addElem(name);
3060
3770
  expect(stream, ":", "struct member name");
3061
3771
  const typeRef = parseSimpleTypeRef(ctx);
3062
3772
  if (!typeRef) throwParseError(stream, "Expected type after ':'");
3063
- ctx.addElem(typeRef);
3064
- const elem = finishElem("member", start, ctx, {
3773
+ return finishStatement("member", start, ctx, {
3065
3774
  name,
3066
3775
  typeRef
3067
- });
3068
- attachAttributes(elem, attributes.length ? attributes : void 0);
3069
- return elem;
3776
+ }, attributes);
3070
3777
  }
3071
3778
  //#endregion
3072
3779
  //#region src/parse/ParseModule.ts
@@ -3077,6 +3784,7 @@ const declParsers = [
3077
3784
  parseAliasDecl,
3078
3785
  parseStructDecl,
3079
3786
  parseFnDecl,
3787
+ parseDoBlock,
3080
3788
  parseConstAssert
3081
3789
  ];
3082
3790
  /** Grammar: translation_unit : global_directive* ( global_decl | global_assert | ';' )* */
@@ -3084,19 +3792,39 @@ function parseModule(ctx) {
3084
3792
  parseImports(ctx);
3085
3793
  parseDirectives(ctx);
3086
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
+ }
3087
3815
  }
3088
3816
  /** Parse WESL import statements at the start of the module. */
3089
3817
  function parseImports(ctx) {
3090
3818
  const importElems = parseWeslImports(ctx);
3091
3819
  for (const importElem of importElems) {
3092
- ctx.addElem(importElem);
3820
+ ctx.addModuleDecl(importElem);
3093
3821
  ctx.state.stable.imports.push(importElem.imports);
3094
3822
  }
3095
3823
  }
3096
3824
  /** Grammar: global_directive : diagnostic_directive | enable_directive | requires_directive */
3097
3825
  function parseDirectives(ctx) {
3098
3826
  const directives = parseMany(ctx, parseDirective);
3099
- for (const elem of directives) ctx.addElem(elem);
3827
+ for (const elem of directives) ctx.addModuleDecl(elem);
3100
3828
  }
3101
3829
  /** Parse one declaration, return true if more may exist. */
3102
3830
  function parseNextDeclaration(ctx) {
@@ -3111,34 +3839,40 @@ function parseNextDeclaration(ctx) {
3111
3839
  if (attrs.length) throwParseError(stream, "Expected declaration after attributes");
3112
3840
  return false;
3113
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
+ }
3114
3854
  /** Try each declaration parser until one succeeds. */
3115
3855
  function parseDecl(ctx, attrs) {
3116
- const attrsOrUndef = attrs.length ? attrs : void 0;
3117
- const elem = findMap(declParsers, (p) => p(ctx, attrsOrUndef));
3118
- if (elem) {
3119
- recordDecl(ctx, elem, attrs);
3120
- return true;
3121
- }
3122
- 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;
3123
3860
  }
3124
3861
  /** Pop conditional scope and attach the conditional attribute. */
3125
3862
  function finalizeConditional(ctx, attrs) {
3126
3863
  const partialScope = ctx.popScope();
3127
- partialScope.condAttribute = findMap(attrs, ({ attribute }) => isConditionalAttribute(attribute) ? attribute : void 0);
3864
+ partialScope.condAttribute = conditionalAttribute(attrs);
3128
3865
  }
3129
3866
  /** Record a parsed declaration, extending start to include attributes. */
3130
3867
  function recordDecl(ctx, elem, attrs) {
3131
3868
  if (attrs.length && elem.start > attrs[0].start) elem.start = attrs[0].start;
3132
- ctx.addElem(elem);
3869
+ ctx.addModuleDecl(elem);
3133
3870
  if (elem.kind === "assert") {
3134
3871
  const { stable } = ctx.state;
3135
3872
  stable.moduleAsserts ??= [];
3136
3873
  stable.moduleAsserts.push(elem);
3137
3874
  }
3138
3875
  }
3139
- function isConditionalAttribute(a) {
3140
- return a.kind === "@if" || a.kind === "@elif" || a.kind === "@else";
3141
- }
3142
3876
  //#endregion
3143
3877
  //#region src/parse/ParsingContext.ts
3144
3878
  /** Context for parsers to build AST and manage scopes. */
@@ -3161,9 +3895,9 @@ var ParsingContext = class {
3161
3895
  currentScope() {
3162
3896
  return this.state.context.scope;
3163
3897
  }
3164
- addElem(elem) {
3165
- const { openElems } = this.state.context;
3166
- 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);
3167
3901
  }
3168
3902
  pushScope(kind = "scope") {
3169
3903
  const { scope } = this.state.context;
@@ -3231,55 +3965,6 @@ const reservedWords = `NULL Self abstract active alignas alignof as asm asm_frag
3231
3965
  target template this thread_local throw trait try type typedef typeid typename typeof
3232
3966
  union unless unorm unsafe unsized use using varying virtual volatile wgsl where with writeonly yield`.split(/\s+/);
3233
3967
  //#endregion
3234
- //#region src/parse/stream/CachingStream.ts
3235
- var CachingStream = class {
3236
- cache = new Cache(5);
3237
- inner;
3238
- constructor(inner) {
3239
- this.inner = inner;
3240
- }
3241
- checkpoint() {
3242
- return this.inner.checkpoint();
3243
- }
3244
- reset(position) {
3245
- this.inner.reset(position);
3246
- }
3247
- nextToken() {
3248
- const startPos = this.checkpoint();
3249
- const cachedValue = this.cache.get(startPos);
3250
- if (cachedValue !== void 0) {
3251
- this.reset(cachedValue.checkpoint);
3252
- return cachedValue.token;
3253
- } else {
3254
- const token = this.inner.nextToken();
3255
- const checkpoint = this.checkpoint();
3256
- this.cache.set(startPos, {
3257
- token,
3258
- checkpoint
3259
- });
3260
- return token;
3261
- }
3262
- }
3263
- get src() {
3264
- return this.inner.src;
3265
- }
3266
- };
3267
- /** size limited key value cache */
3268
- var Cache = class extends Map {
3269
- max;
3270
- constructor(max) {
3271
- super();
3272
- this.max = max;
3273
- }
3274
- set(k, v) {
3275
- if (this.size > this.max) {
3276
- const first = this.keys().next().value;
3277
- if (first) this.delete(first);
3278
- }
3279
- return super.set(k, v);
3280
- }
3281
- };
3282
- //#endregion
3283
3968
  //#region src/parse/stream/RegexHelpers.ts
3284
3969
  function toRegexSource(nameExp) {
3285
3970
  const [name, e] = nameExp;
@@ -3309,33 +3994,10 @@ function matchOneOf(syms) {
3309
3994
  return new RegExp(escaped.join("|"));
3310
3995
  }
3311
3996
  //#endregion
3312
- //#region src/parse/stream/MatchersStream.ts
3313
- /** Runs a `RegexMatchers` on an input string */
3314
- var MatchersStream = class {
3315
- position = 0;
3316
- text;
3317
- matchers;
3318
- constructor(text, matchers) {
3319
- this.text = text;
3320
- this.matchers = matchers;
3321
- }
3322
- checkpoint() {
3323
- return this.position;
3324
- }
3325
- reset(position) {
3326
- this.position = position;
3327
- }
3328
- nextToken() {
3329
- const result = this.matchers.execAt(this.text, this.position);
3330
- if (result === null) return null;
3331
- this.position = result.span[1];
3332
- return result;
3333
- }
3334
- get src() {
3335
- return this.text;
3336
- }
3337
- };
3997
+ //#region src/parse/stream/RegexMatchers.ts
3338
3998
  /**
3999
+ * Matches tokens by kind with one combined regex.
4000
+ *
3339
4001
  * The matchers passed to this object must follow certain rules:
3340
4002
  * - They must use non-capturing groups: `(?:...)`
3341
4003
  * - They must NOT use `^` or `$`
@@ -3346,55 +4008,230 @@ var RegexMatchers = class {
3346
4008
  constructor(matchers) {
3347
4009
  this.groups = Object.keys(matchers);
3348
4010
  const expParts = Object.entries(matchers).map(toRegexSource).join("|");
3349
- this.exp = new RegExp(expParts, "dyu");
4011
+ this.exp = new RegExp(expParts, "yu");
3350
4012
  }
3351
4013
  execAt(text, position) {
3352
4014
  this.exp.lastIndex = position;
3353
- const matchedIndex = findGroupDex(this.exp.exec(text)?.indices);
3354
- if (matchedIndex) {
3355
- const { span, groupDex } = matchedIndex;
3356
- return {
3357
- kind: this.groups[groupDex],
3358
- span,
3359
- text: text.slice(span[0], span[1])
3360
- };
3361
- } else return null;
3362
- }
3363
- };
3364
- function findGroupDex(indices) {
3365
- if (indices !== void 0) for (let i = 1; i < indices.length; i++) {
3366
- const span = indices[i];
3367
- 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)],
3368
4021
  span,
3369
- groupDex: i - 1
4022
+ text: matched
3370
4023
  };
3371
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");
3372
4030
  }
3373
4031
  //#endregion
3374
- //#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;
3375
4034
  /** Whitespaces including new lines */
3376
4035
  const blankspaces = /[ \t\n\v\f\r\u{0085}\u{200E}\u{200F}\u{2028}\u{2029}]+/u;
3377
- const symbolSet = "& && -> @ / ! [ ] { } :: : , == = != >>= >> >= > <<= << <= < % - -- . + ++ | || ( ) ; * ~ ^ // /* */ += -= *= /= %= &= |= ^= _";
3378
- const ident = /(?:(?:[_\p{XID_Start}][\p{XID_Continue}]+)|(?:[\p{XID_Start}]))/u;
3379
- const keywordOrReserved = new Set(keywords.concat(reservedWords));
3380
- 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({
3381
4040
  word: ident,
3382
- 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,
3383
4042
  blankspaces,
3384
4043
  commentStart: /\/\/|\/\*/,
3385
- symbol: matchOneOf(symbolSet),
4044
+ symbol: matchOneOf("& && -> @ / ! [ ] { } :: : , == = != >>= >> >= > <<= << <= < % - -- . + ++ | || ( ) ; * ~ ^ // /* */ += -= *= /= %= &= |= ^= _"),
3386
4045
  invalid: /[^]/
3387
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));
3388
4220
  /** A stream that produces WESL tokens, skipping over comments and white space */
3389
4221
  var WeslStream = class {
3390
4222
  stream;
3391
- /** New line */
3392
- 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");
3393
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;
3394
4231
  src;
3395
4232
  constructor(src) {
3396
4233
  this.src = src;
3397
- this.stream = new CachingStream(new MatchersStream(src, weslMatcher));
4234
+ this.stream = new WeslLexer(src);
3398
4235
  }
3399
4236
  checkpoint() {
3400
4237
  return this.stream.checkpoint();
@@ -3402,27 +4239,79 @@ var WeslStream = class {
3402
4239
  reset(position) {
3403
4240
  this.stream.reset(position);
3404
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. */
3405
4251
  nextToken() {
4252
+ const peeked = this.usePeeked();
4253
+ if (peeked !== null) return peeked.token;
4254
+ let pending;
3406
4255
  while (true) {
3407
4256
  const token = this.stream.nextToken();
3408
- if (token === null) return null;
4257
+ if (token === null) {
4258
+ this.recordTrivia(this.src.length, pending);
4259
+ return null;
4260
+ }
3409
4261
  const kind = token.kind;
3410
4262
  if (kind === "blankspaces") continue;
3411
- else if (kind === "commentStart") if (token.text === "//") this.stream.reset(this.skipToEol(token.span[1]));
3412
- else this.stream.reset(this.skipBlockComment(token.span[1]));
3413
- else if (kind === "word") {
3414
- const returnToken = token;
3415
- if (keywordOrReserved.has(token.text)) returnToken.kind = "keyword";
3416
- return returnToken;
4263
+ else if (kind === "commentStart") {
4264
+ pending ??= [];
4265
+ pending.push(this.consumeComment(token));
3417
4266
  } else if (kind === "invalid") throw new ParseError("Invalid token " + token.text, token.span);
3418
- 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;
3419
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]);
4294
+ }
4295
+ this.stream.reset(end);
4296
+ return {
4297
+ style,
4298
+ start,
4299
+ end
4300
+ };
3420
4301
  }
3421
4302
  /** Peek at the next token without consuming it */
3422
4303
  peek() {
3423
4304
  const pos = this.checkpoint();
4305
+ const peeked = this.peeked;
4306
+ if (peeked !== null && peeked.pos === pos) return peeked.token;
3424
4307
  const token = this.nextToken();
4308
+ const end = this.checkpoint();
3425
4309
  this.reset(pos);
4310
+ this.peeked = {
4311
+ pos,
4312
+ token,
4313
+ end
4314
+ };
3426
4315
  return token;
3427
4316
  }
3428
4317
  /** Consume token if text matches, otherwise leave position unchanged */
@@ -3466,10 +4355,15 @@ var WeslStream = class {
3466
4355
  }
3467
4356
  return tokens;
3468
4357
  }
3469
- 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) {
3470
4364
  this.eolPattern.lastIndex = position;
3471
- if (this.eolPattern.exec(this.src) === null) return this.src.length;
3472
- else return this.eolPattern.lastIndex;
4365
+ const result = this.eolPattern.exec(this.src);
4366
+ return result === null ? this.src.length : result.index;
3473
4367
  }
3474
4368
  skipBlockComment(start) {
3475
4369
  let position = start;
@@ -3491,39 +4385,46 @@ var WeslStream = class {
3491
4385
  const startPosition = this.stream.checkpoint();
3492
4386
  const token = this.nextToken();
3493
4387
  this.stream.reset(startPosition);
3494
- if (token === null) return null;
3495
- if (token.kind !== "symbol") return null;
3496
- if (token.text === "<") if (this.isTemplateStart(token.span[1])) {
3497
- this.stream.reset(token.span[1]);
3498
- return token;
3499
- } else {
4388
+ if (token === null || token.kind !== "symbol" || token.text !== "<") return null;
4389
+ if (!this.isTemplateStart(token.span[1])) {
3500
4390
  this.stream.reset(startPosition);
3501
4391
  return null;
3502
4392
  }
3503
- else return null;
4393
+ this.stream.reset(token.span[1]);
4394
+ return token;
3504
4395
  }
4396
+ /** Match a template-closing `>`, splitting it off a `>>`/`>=`/`>>=` token when needed. */
3505
4397
  nextTemplateEndToken() {
3506
4398
  const startPosition = this.stream.checkpoint();
3507
4399
  const token = this.nextToken();
3508
4400
  this.stream.reset(startPosition);
3509
4401
  if (token === null) return null;
3510
- if (token.kind === "symbol" && token.text[0] === ">") {
3511
- const tokenPosition = token.span[0];
3512
- this.stream.reset(tokenPosition + 1);
3513
- return {
3514
- kind: "symbol",
3515
- span: [tokenPosition, tokenPosition + 1],
3516
- text: ">"
3517
- };
3518
- } 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
+ };
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
+ }
3519
4420
  }
4421
+ /** Template-list discovery: scan forward from `<` for a balanced closing `>`. */
3520
4422
  isTemplateStart(afterToken) {
3521
4423
  this.stream.reset(afterToken);
3522
4424
  let pendingCounter = 1;
3523
4425
  while (true) {
3524
- const nextToken = this.stream.nextToken();
4426
+ const nextToken = this.nextRawSymbol();
3525
4427
  if (nextToken === null) return false;
3526
- if (nextToken.kind !== "symbol") continue;
3527
4428
  if (nextToken.text === "<") pendingCounter += 1;
3528
4429
  else if (nextToken.text[0] === ">") {
3529
4430
  if (nextToken.text === ">" || nextToken.text === ">=") pendingCounter -= 1;
@@ -3541,12 +4442,11 @@ var WeslStream = class {
3541
4442
  */
3542
4443
  skipBracketsTo(closingBracket) {
3543
4444
  while (true) {
3544
- const nextToken = this.stream.nextToken();
4445
+ const nextToken = this.nextRawSymbol();
3545
4446
  if (nextToken === null) {
3546
4447
  const after = this.stream.checkpoint();
3547
4448
  throw new ParseError("Unclosed bracket!", [after, after]);
3548
4449
  }
3549
- if (nextToken.kind !== "symbol") continue;
3550
4450
  if (nextToken.text === "(") this.skipBracketsTo(")");
3551
4451
  else if (nextToken.text === "[") this.skipBracketsTo("]");
3552
4452
  else if (nextToken.text === closingBracket) return;
@@ -3559,10 +4459,10 @@ var WeslStream = class {
3559
4459
  function parseWesl(srcModule, options) {
3560
4460
  const { ctx, state } = createParseState(srcModule, options);
3561
4461
  try {
3562
- beginElem(ctx, "module");
3563
4462
  parseModule(ctx);
3564
- const moduleElem = state.stable.moduleElem;
3565
- moduleElem.contents = finishContents(ctx, 0, moduleElem.end);
4463
+ const { moduleElem } = state.stable;
4464
+ attachComments(ctx, moduleElem);
4465
+ checkDoBlockNames(moduleElem);
3566
4466
  return state.stable;
3567
4467
  } catch (e) {
3568
4468
  if (e instanceof ParseError) throw new WeslParseError({
@@ -3576,29 +4476,27 @@ function parseWesl(srcModule, options) {
3576
4476
  }
3577
4477
  }
3578
4478
  /** Initialize parse state: token stream, root scope, and module element. */
3579
- function createParseState(srcModule, options) {
4479
+ function createParseState(srcModule, parseOptions) {
3580
4480
  const stream = new WeslStream(srcModule.src);
3581
4481
  const rootScope = emptyScope(null);
3582
4482
  const moduleElem = {
3583
4483
  kind: "module",
3584
- contents: [],
4484
+ decls: [],
3585
4485
  start: 0,
3586
4486
  end: srcModule.src.length
3587
4487
  };
3588
4488
  const state = {
3589
- context: {
3590
- scope: rootScope,
3591
- openElems: []
3592
- },
4489
+ context: { scope: rootScope },
3593
4490
  stable: {
3594
4491
  srcModule,
3595
4492
  moduleElem,
3596
4493
  rootScope,
3597
- imports: []
4494
+ imports: [],
4495
+ parseOptions
3598
4496
  }
3599
4497
  };
3600
4498
  return {
3601
- ctx: new ParsingContext(stream, state, options),
4499
+ ctx: new ParsingContext(stream, state, parseOptions),
3602
4500
  state
3603
4501
  };
3604
4502
  }
@@ -3609,14 +4507,14 @@ var WeslParseError = class extends Error {
3609
4507
  span;
3610
4508
  src;
3611
4509
  constructor(opts) {
3612
- const source = opts.src.src;
3613
- const [lineNum, linePos] = offsetToLineNumber(opts.cause.span[0], source);
3614
- let message = `${opts.src.debugFilePath}:${lineNum}:${linePos}`;
3615
- message += ` error: ${opts.cause.message}\n`;
3616
- message += errorHighlight(source, opts.cause.span).join("\n");
3617
- super(message, { cause: opts.cause });
3618
- this.span = opts.cause.span;
3619
- 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;
3620
4518
  }
3621
4519
  };
3622
4520
  /** Parse a WESL file. */
@@ -3626,8 +4524,8 @@ function parseSrcModule(srcModule, options) {
3626
4524
  /** @return flattened form of import tree for binding idents. */
3627
4525
  function flatImports(ast, conditions) {
3628
4526
  if (ast._flatImports && !conditions) return ast._flatImports;
3629
- const importElems = ast.moduleElem.contents.filter((elem) => elem.kind === "import");
3630
- 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));
3631
4529
  if (!conditions) ast._flatImports = flat;
3632
4530
  return flat;
3633
4531
  }
@@ -3924,11 +4822,13 @@ var RecordResolver = class {
3924
4822
  sources;
3925
4823
  packageName;
3926
4824
  debugWeslRoot;
4825
+ weslExtensions;
3927
4826
  constructor(sources, options = {}) {
3928
- const { packageName = "package", debugWeslRoot } = options;
4827
+ const { packageName = "package", debugWeslRoot, weslExtensions } = options;
3929
4828
  this.sources = sources;
3930
4829
  this.packageName = packageName;
3931
4830
  this.debugWeslRoot = normalizeDebugRoot(debugWeslRoot);
4831
+ this.weslExtensions = weslExtensions;
3932
4832
  }
3933
4833
  resolveModule(modulePath) {
3934
4834
  const cached = this.astCache.get(modulePath);
@@ -3939,7 +4839,7 @@ var RecordResolver = class {
3939
4839
  modulePath,
3940
4840
  debugFilePath: this.modulePathToDebugPath(modulePath),
3941
4841
  src: source
3942
- });
4842
+ }, { weslExtensions: this.weslExtensions });
3943
4843
  this.astCache.set(modulePath, ast);
3944
4844
  return ast;
3945
4845
  }
@@ -4035,7 +4935,7 @@ function freshResolver(inner) {
4035
4935
  if (cached) return cached;
4036
4936
  const ast = inner.resolveModule(modulePath);
4037
4937
  if (!ast) return void 0;
4038
- const fresh = parseSrcModule(ast.srcModule);
4938
+ const fresh = parseSrcModule(ast.srcModule, ast.parseOptions);
4039
4939
  cache.set(modulePath, fresh);
4040
4940
  return fresh;
4041
4941
  } };
@@ -4359,12 +5259,13 @@ var SrcMap = class SrcMap {
4359
5259
  const { src, position: srcStart } = this.destToSrc(e.srcStart);
4360
5260
  const { src: endSrc, position: srcEnd } = this.destToSrc(e.srcEnd);
4361
5261
  if (endSrc !== src) throw new Error("NYI, need to split");
5262
+ const { destStart, destEnd } = e;
4362
5263
  return {
4363
5264
  src,
4364
5265
  srcStart,
4365
5266
  srcEnd,
4366
- destStart: e.destStart,
4367
- destEnd: e.destEnd
5267
+ destStart,
5268
+ destEnd
4368
5269
  };
4369
5270
  });
4370
5271
  const otherSources = other.entries.filter((e) => e.src.path !== this.dest.path || e.src.text !== this.dest.text);
@@ -4385,10 +5286,6 @@ var SrcMap = class SrcMap {
4385
5286
  };
4386
5287
  }
4387
5288
  };
4388
- /** sort entries in place by src start position */
4389
- function sortSrc(entries) {
4390
- entries.sort((a, b) => a.srcStart - b.srcStart);
4391
- }
4392
5289
  /** Incrementally append to a string, tracking source references */
4393
5290
  var SrcMapBuilder = class {
4394
5291
  #fragments = [];
@@ -4442,11 +5339,6 @@ var SrcMapBuilder = class {
4442
5339
  };
4443
5340
  this.add("\n", srcStart, srcEnd);
4444
5341
  }
4445
- /** copy a string fragment from the src to the destination string */
4446
- addCopy(srcStart, srcEnd) {
4447
- const fragment = this.source.text.slice(srcStart, srcEnd);
4448
- this.add(fragment, srcStart, srcEnd);
4449
- }
4450
5342
  /** return a SrcMap */
4451
5343
  static build(builders) {
4452
5344
  const map = new SrcMap({ text: builders.map((b) => b.#fragments.join("")).join("") }, builders.flatMap((b) => b.#entries));
@@ -4454,6 +5346,10 @@ var SrcMapBuilder = class {
4454
5346
  return map;
4455
5347
  }
4456
5348
  };
5349
+ /** sort entries in place by src start position */
5350
+ function sortSrc(entries) {
5351
+ entries.sort((a, b) => a.srcStart - b.srcStart);
5352
+ }
4457
5353
  //#endregion
4458
5354
  //#region src/Linker.ts
4459
5355
  /**
@@ -4471,10 +5367,12 @@ async function link(params) {
4471
5367
  /** linker api for benchmarking */
4472
5368
  function _linkSync(params) {
4473
5369
  const { weslSrc, libs = [], packageName, debugWeslRoot, resolver } = params;
5370
+ const { weslExtensions } = params;
4474
5371
  if (!resolver && !weslSrc) throw new Error("Either resolver or weslSrc must be provided");
4475
5372
  const allResolvers = [resolver ?? new RecordResolver(weslSrc, {
4476
5373
  packageName,
4477
- debugWeslRoot
5374
+ debugWeslRoot,
5375
+ weslExtensions
4478
5376
  }), ...createLibraryResolvers(libs, debugWeslRoot)];
4479
5377
  const finalResolver = allResolvers.length === 1 ? allResolvers[0] : new CompositeResolver(allResolvers);
4480
5378
  return linkRegistry({
@@ -4607,264 +5505,6 @@ function builderFromModule(srcModule) {
4607
5505
  });
4608
5506
  }
4609
5507
  //#endregion
4610
- //#region src/LinkerUtil.ts
4611
- function visitAst(elem, visitor) {
4612
- visitor(elem);
4613
- if (elem.contents) elem.contents.forEach((child) => {
4614
- visitAst(child, visitor);
4615
- });
4616
- }
4617
- //#endregion
4618
- //#region src/RawEmit.ts
4619
- function attributeToString$1(e) {
4620
- const { kind } = e.attribute;
4621
- if (kind === "@attribute") {
4622
- const { params } = e.attribute;
4623
- if (params === void 0 || params.length === 0) return "@" + e.attribute.name;
4624
- else return `@${e.attribute.name}(${params.map((param) => contentsToString(param)).join(", ")})`;
4625
- } else if (kind === "@builtin") return "@builtin(" + e.attribute.param.name + ")";
4626
- else if (kind === "@diagnostic") return "@diagnostic" + diagnosticControlToString(e.attribute.severity, e.attribute.rule);
4627
- else if (kind === "@if") return `@if(${expressionToString(e.attribute.param.expression)})`;
4628
- else if (kind === "@elif") return `@elif(${expressionToString(e.attribute.param.expression)})`;
4629
- else if (kind === "@else") return "@else";
4630
- else if (kind === "@interpolate") return `@interpolate(${e.attribute.params.map((v) => v.name).join(", ")})`;
4631
- else assertUnreachable(kind);
4632
- }
4633
- function typeListToString(params) {
4634
- return `<${params.map(typeParamToString).join(", ")}>`;
4635
- }
4636
- function typeParamToString(param) {
4637
- if (param === void 0) return "?";
4638
- if (param.kind === "type") return typeRefToString(param);
4639
- return expressionToString(param);
4640
- }
4641
- function typeRefToString(t) {
4642
- if (!t) return "?";
4643
- const { name, templateParams } = t;
4644
- const params = templateParams ? typeListToString(templateParams) : "";
4645
- return `${refToString(name)}${params}`;
4646
- }
4647
- function refToString(ref) {
4648
- if (typeof ref === "string") return ref;
4649
- if (ref.std) return ref.originalName;
4650
- const decl = findDecl(ref);
4651
- return decl.mangledName || decl.originalName;
4652
- }
4653
- function contentsToString(elem) {
4654
- if (elem.kind === "translate-time-expression") throw new Error("Not supported");
4655
- else if (elem.kind === "expression" || elem.kind === "stuff") return elem.contents.map((c) => {
4656
- const { kind } = c;
4657
- if (kind === "text") return c.srcModule.src.slice(c.start, c.end);
4658
- else if (kind === "ref") return refToString(c.ident);
4659
- else return `?${c.kind}?`;
4660
- }).join(" ");
4661
- else if (elem.kind === "name") return elem.name;
4662
- else assertUnreachable(elem);
4663
- }
4664
- //#endregion
4665
- //#region src/Reflection.ts
4666
- const textureStorage = matchOneOf(textureStorageTypes);
4667
- matchOneOf(sampledTextureTypes);
4668
- matchOneOf(multisampledTextureTypes);
4669
- //#endregion
4670
- //#region src/TransformBindingStructs.ts
4671
- function bindingStructsPlugin() {
4672
- return { transform: lowerBindingStructs };
4673
- }
4674
- /**
4675
- * Transform binding structures into binding variables by mutating the AST.
4676
- *
4677
- * First we find all the binding structs:
4678
- * . find all the structs in the module by filtering the moduleElem.contents
4679
- * . for each struct:
4680
- * . mark any structs with that contain @group or @binding annotations as 'binding structs' and save them in a list
4681
- * . (later) create reverse links from structs to struct members
4682
- * . (later) visit all the binding structs and traverse to referencing structs, marking the referencing structs as binding structs too
4683
- * Generate synethic AST nodes for binding variables
4684
- *
4685
- * Find all references to binding struct members
4686
- * . find the componound idents by traversing moduleElem.contents
4687
- * . filter to find the compound idents that refer to 'binding structs'
4688
- * . go from each ident to its declaration,
4689
- * . declaration to typeRef reference
4690
- * . typeRef to type declaration
4691
- * . check type declaration to see if it's a binding struct
4692
- * . record the intermediate declaration (e.g. a fn param b:Bindings from 'fn(b:Bindings)' )
4693
- * rewrite references to binding struct members as synthetic elements
4694
- *
4695
- * Remove the binding structs from the AST
4696
- * Remove the intermediate fn param declarations from the AST
4697
- * Add the new binding variables to the AST
4698
- *
4699
- * @return the binding structs and the mutated AST
4700
- */
4701
- function lowerBindingStructs(ast) {
4702
- const clonedAst = structuredClone(ast);
4703
- const { moduleElem, globalNames, notableElems } = clonedAst;
4704
- const bindingStructs = markBindingStructs(moduleElem);
4705
- markEntryTypes(moduleElem, bindingStructs);
4706
- const newVars = bindingStructs.flatMap((s) => transformBindingStruct(s, globalNames));
4707
- const bindingRefs = findRefsToBindingStructs(moduleElem);
4708
- bindingRefs.forEach(({ memberRef, struct }) => {
4709
- transformBindingReference(memberRef, struct);
4710
- });
4711
- bindingRefs.forEach(({ intermediates }) => {
4712
- intermediates.forEach((e) => {
4713
- e.contents = [];
4714
- });
4715
- });
4716
- const contents = removeBindingStructs(moduleElem);
4717
- moduleElem.contents = [...newVars, ...contents];
4718
- notableElems.bindingStructs = bindingStructs;
4719
- return {
4720
- ...clonedAst,
4721
- moduleElem
4722
- };
4723
- }
4724
- function markEntryTypes(moduleElem, bindingStructs) {
4725
- const fnFound = fnReferencesBindingStruct(moduleElem.contents.filter((e) => e.kind === "fn"), bindingStructs);
4726
- if (fnFound) {
4727
- const { fn, struct } = fnFound;
4728
- struct.entryFn = fn;
4729
- }
4730
- }
4731
- function fnReferencesBindingStruct(fns, bindingStructs) {
4732
- for (const fn of fns) {
4733
- const { params } = fn;
4734
- for (const p of params) {
4735
- const referencedElem = ((p.name?.typeRef?.name)?.refersTo)?.declElem;
4736
- const struct = bindingStructs.find((s) => s === referencedElem);
4737
- if (struct) return {
4738
- fn,
4739
- struct
4740
- };
4741
- }
4742
- }
4743
- }
4744
- function removeBindingStructs(moduleElem) {
4745
- return moduleElem.contents.filter((elem) => elem.kind !== "struct" || !elem.bindingStruct);
4746
- }
4747
- /** mutate the AST, marking StructElems as bindingStructs
4748
- * (if they contain ptrs with @group @binding annotations)
4749
- * @return the binding structs
4750
- */
4751
- function markBindingStructs(moduleElem) {
4752
- const bindingStructs = moduleElem.contents.filter((elem) => elem.kind === "struct").filter(containsBinding);
4753
- bindingStructs.forEach((struct) => {
4754
- struct.bindingStruct = true;
4755
- });
4756
- return bindingStructs;
4757
- }
4758
- /** @return true if this struct contains a member with marked with @binding or @group */
4759
- function containsBinding(struct) {
4760
- return struct.members.some(({ attributes }) => bindingAttribute(attributes));
4761
- }
4762
- function bindingAttribute(attributes) {
4763
- if (!attributes) return false;
4764
- return attributes.some(({ attribute }) => attribute.kind === "@attribute" && (attribute.name === "binding" || attribute.name === "group"));
4765
- }
4766
- /** convert each member of the binding struct into a synthetic global variable */
4767
- function transformBindingStruct(s, globalNames) {
4768
- return s.members.map((member) => {
4769
- const { typeRef, name: memberName } = member;
4770
- const { name: typeName } = typeRef;
4771
- const typeParameters = typeRef?.templateParams;
4772
- const varName = minimallyMangledName(memberName.name, globalNames);
4773
- member.mangledVarName = varName;
4774
- globalNames.add(varName);
4775
- const attributes = member.attributes?.map(attributeToString$1).join(" ") ?? "";
4776
- const varTypes = lowerPtrMember(typeName, typeParameters) ?? lowerStdTypeMember(typeName, typeParameters) ?? lowerStorageTextureMember(typeName, typeParameters);
4777
- if (!varTypes) {
4778
- console.log("unhandled case transforming member", typeName);
4779
- return syntheticVar(attributes, varName, "", "??");
4780
- }
4781
- const { storage: storageType, varType } = varTypes;
4782
- return syntheticVar(attributes, varName, storageType, varType);
4783
- });
4784
- }
4785
- function lowerPtrMember(typeName, typeParameters) {
4786
- if (typeName.originalName === "ptr") {
4787
- const origParams = typeParameters ?? [];
4788
- const newParams = [origParams[0]];
4789
- if (origParams[2]) newParams.push(origParams[2]);
4790
- return {
4791
- storage: typeListToString(newParams),
4792
- varType: typeParamToString(origParams?.[1])
4793
- };
4794
- }
4795
- }
4796
- function lowerStdTypeMember(typeName, typeParameters) {
4797
- if (typeof typeName !== "string") return {
4798
- varType: (typeName.std ? typeName.originalName : "??") + (typeParameters ? typeListToString(typeParameters) : ""),
4799
- storage: ""
4800
- };
4801
- }
4802
- function lowerStorageTextureMember(typeName, typeParameters) {
4803
- if (textureStorage.test(typeName.originalName)) return {
4804
- varType: typeName + (typeParameters ? typeListToString(typeParameters) : ""),
4805
- storage: ""
4806
- };
4807
- }
4808
- function syntheticVar(attributes, varName, storageTemplate, varType) {
4809
- return {
4810
- kind: "synthetic",
4811
- text: `${attributes} var${storageTemplate} ${varName} : ${varType};\n`
4812
- };
4813
- }
4814
- /** find all simple member references in the module that refer to binding structs */
4815
- function findRefsToBindingStructs(moduleElem) {
4816
- const members = [];
4817
- visitAst(moduleElem, (elem) => {
4818
- if (elem.kind === "memberRef") members.push(elem);
4819
- });
4820
- return filterMap(members, refersToBindingStruct);
4821
- }
4822
- /** @return true if this memberRef refers to a binding struct */
4823
- function refersToBindingStruct(memberRef) {
4824
- const found = traceToStruct(memberRef.name.ident);
4825
- if (found?.struct.bindingStruct) return {
4826
- memberRef,
4827
- ...found
4828
- };
4829
- }
4830
- /** If this identifier ultimately refers to a struct type, return the struct declaration */
4831
- function traceToStruct(ident) {
4832
- const declElem = findDecl(ident).declElem;
4833
- if (declElem && declElem.kind === "param") {
4834
- const name = declElem.name.typeRef?.name;
4835
- if (typeof name !== "string") {
4836
- if (name?.std) return;
4837
- const structElem = findDecl(name).declElem;
4838
- if (structElem?.kind === "struct") return {
4839
- struct: structElem,
4840
- intermediates: [declElem]
4841
- };
4842
- return;
4843
- }
4844
- }
4845
- }
4846
- /** Mutate the member reference elem to instead contain synthetic elem text.
4847
- * The new text is the mangled var name of the struct member that the memberRef refers to. */
4848
- function transformBindingReference(memberRef, struct) {
4849
- const refName = memberRef.member.name;
4850
- const structMember = struct.members.find((m) => m.name.name === refName);
4851
- if (!structMember || !structMember.mangledVarName) {
4852
- console.log(`missing mangledVarName for ${refName}`);
4853
- return {
4854
- kind: "synthetic",
4855
- text: refName
4856
- };
4857
- }
4858
- const { extraComponents } = memberRef;
4859
- const extraText = extraComponents ? contentsToString(extraComponents) : "";
4860
- const synthElem = {
4861
- kind: "synthetic",
4862
- text: structMember.mangledVarName + extraText
4863
- };
4864
- memberRef.contents = [synthElem];
4865
- return synthElem;
4866
- }
4867
- //#endregion
4868
5508
  //#region src/WeslDevice.ts
4869
5509
  /**
4870
5510
  * Creates {@link WeslDevice} for usage with WESL.
@@ -4933,4 +5573,4 @@ function makeWeslDevice(device) {
4933
5573
  return device;
4934
5574
  }
4935
5575
  //#endregion
4936
- 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 };