wesl 0.7.14 → 0.7.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -27,6 +27,85 @@ declare class ParseError extends Error {
27
27
  constructor(msg: string, span: Span);
28
28
  }
29
29
  //#endregion
30
+ //#region src/Stream.d.ts
31
+ /**
32
+ * Interface for a tokenizer. Returns a "next token", and can be reset to
33
+ * previously saved positions (checkpoints).
34
+ */
35
+ interface Stream<T extends Token> {
36
+ /** Returns the current position */
37
+ checkpoint(): number;
38
+ /** Restores a position */
39
+ reset(position: number): void;
40
+ /**
41
+ * Returns the next token, or `null` if the end of the stream has been reached.
42
+ * Always leaves `checkpoint` right after the token.
43
+ */
44
+ nextToken(): T | null;
45
+ /** src text */
46
+ src: string;
47
+ }
48
+ /** A text token */
49
+ interface Token {
50
+ kind: string;
51
+ text: string;
52
+ span: Span;
53
+ }
54
+ interface TypedToken<Kind extends string> extends Token {
55
+ kind: Kind;
56
+ }
57
+ //#endregion
58
+ //#region src/parse/WeslStream.d.ts
59
+ type WeslTokenKind = "word" | "keyword" | "number" | "symbol";
60
+ type WeslToken<Kind extends WeslTokenKind = WeslTokenKind> = TypedToken<Kind>;
61
+ /** A stream that produces WESL tokens, skipping over comments and white space */
62
+ declare class WeslStream implements Stream<WeslToken> {
63
+ private stream;
64
+ /** New line */
65
+ private eolPattern;
66
+ private blockCommentPattern;
67
+ src: string;
68
+ constructor(src: string);
69
+ checkpoint(): number;
70
+ reset(position: number): void;
71
+ nextToken(): WeslToken | null;
72
+ /** Peek at the next token without consuming it */
73
+ peek(): WeslToken | null;
74
+ /** Consume token if text matches, otherwise leave position unchanged */
75
+ matchText(text: string): WeslToken | null;
76
+ /** Consume token if kind matches (and optionally text), otherwise leave position unchanged */
77
+ matchKind<K extends WeslTokenKind>(kind: K, text?: string): WeslToken<K> | null;
78
+ /** Consume token if predicate matches, otherwise leave position unchanged */
79
+ nextIf(predicate: (token: WeslToken) => boolean): WeslToken | null;
80
+ /** Match a sequence of tokens by text. Resets and returns null if any fails. */
81
+ matchSequence(...texts: string[]): WeslToken[] | null;
82
+ private skipToEol;
83
+ private skipBlockComment;
84
+ /**
85
+ * Only matches the `<` token if it is a template
86
+ * Precondition: An ident was parsed right before this.
87
+ * Runs the [template list discovery algorithm](https://www.w3.org/TR/WGSL/#template-list-discovery).
88
+ */
89
+ nextTemplateStartToken(): (WeslToken & {
90
+ kind: "symbol";
91
+ }) | null;
92
+ nextTemplateEndToken(): (WeslToken & {
93
+ kind: "symbol";
94
+ }) | null;
95
+ private isTemplateStart;
96
+ /**
97
+ * Call this after consuming an opening bracket.
98
+ * Skips until a closing bracket. This also consumes the closing bracket.
99
+ */
100
+ private skipBracketsTo;
101
+ }
102
+ //#endregion
103
+ //#region src/parse/ParsingContext.d.ts
104
+ interface ParseOptions {
105
+ /** Store expression AST nodes in statement contents (for tooling/validation). */
106
+ preserveExpressions?: boolean;
107
+ }
108
+ //#endregion
30
109
  //#region src/ParseWESL.d.ts
31
110
  /** Partial element being constructed during parsing. */
32
111
  type OpenElem<T extends ContainerElem = ContainerElem> = Pick<T, "kind" | "contents">;
@@ -77,7 +156,7 @@ declare class WeslParseError extends Error {
77
156
  });
78
157
  }
79
158
  /** Parse a WESL file. */
80
- declare function parseSrcModule(srcModule: SrcModule): WeslAST;
159
+ declare function parseSrcModule(srcModule: SrcModule, options?: ParseOptions): WeslAST;
81
160
  /** @return flattened form of import tree for binding idents. */
82
161
  declare function flatImports(ast: BindingAST, conditions?: Conditions): FlatImport[];
83
162
  //#endregion
@@ -860,8 +939,19 @@ interface BindResults {
860
939
  decls: DeclIdent[];
861
940
  /** Additional global statements to emit (e.g., const_assert). */
862
941
  newStatements: EmittableElem[];
863
- /** Unbound module paths (only if accumulateUnbound is true). */
864
- unbound?: string[][];
942
+ /** Unbound identifiers with position info (only if accumulateUnbound is true). */
943
+ unbound?: UnboundRef[];
944
+ }
945
+ /** An unresolved reference with position info for error reporting. */
946
+ interface UnboundRef {
947
+ /** Module path that couldn't be resolved (e.g., ["package", "foo", "bar"]). */
948
+ path: string[];
949
+ /** Source module containing this reference. */
950
+ srcModule: SrcModule;
951
+ /** Start offset in the source. */
952
+ start: number;
953
+ /** End offset in the source. */
954
+ end: number;
865
955
  }
866
956
  /** An element that can be directly emitted into the linked result. */
867
957
  interface EmittableElem {
@@ -905,7 +995,7 @@ interface BindContext {
905
995
  mangler: ManglerFn;
906
996
  virtuals?: VirtualLibrarySet;
907
997
  /** Unbound identifiers if accumulateUnbound is true. */
908
- unbound?: string[][];
998
+ unbound?: UnboundRef[];
909
999
  /** Don't follow references from declarations (for library dependency detection). */
910
1000
  dontFollowDecls?: boolean;
911
1001
  }
@@ -941,6 +1031,8 @@ declare function identToString(ident?: Ident): string;
941
1031
  * (e.g., [['foo', 'bar', 'baz'], ['other', 'pkg']])
942
1032
  */
943
1033
  declare function findUnboundIdents(resolver: BatchModuleResolver): string[][];
1034
+ /** Find unbound references with full position info. */
1035
+ declare function findUnboundRefs(resolver: BatchModuleResolver): UnboundRef[];
944
1036
  //#endregion
945
1037
  //#region src/discovery/PackageNameUtils.d.ts
946
1038
  /** Package name sanitization for WESL.
@@ -1029,78 +1121,20 @@ declare function normalize(path: string): string;
1029
1121
  * e.g. /foo/bar.wgsl => /foo/bar */
1030
1122
  declare function noSuffix(path: string): string;
1031
1123
  //#endregion
1032
- //#region src/Stream.d.ts
1033
- /**
1034
- * Interface for a tokenizer. Returns a "next token", and can be reset to
1035
- * previously saved positions (checkpoints).
1036
- */
1037
- interface Stream<T extends Token> {
1038
- /** Returns the current position */
1039
- checkpoint(): number;
1040
- /** Restores a position */
1041
- reset(position: number): void;
1042
- /**
1043
- * Returns the next token, or `null` if the end of the stream has been reached.
1044
- * Always leaves `checkpoint` right after the token.
1045
- */
1046
- nextToken(): T | null;
1047
- /** src text */
1048
- src: string;
1049
- }
1050
- /** A text token */
1051
- interface Token {
1052
- kind: string;
1053
- text: string;
1054
- span: Span;
1055
- }
1056
- interface TypedToken<Kind extends string> extends Token {
1057
- kind: Kind;
1058
- }
1059
- //#endregion
1060
- //#region src/parse/WeslStream.d.ts
1061
- type WeslTokenKind = "word" | "keyword" | "number" | "symbol";
1062
- type WeslToken<Kind extends WeslTokenKind = WeslTokenKind> = TypedToken<Kind>;
1063
- /** A stream that produces WESL tokens, skipping over comments and white space */
1064
- declare class WeslStream implements Stream<WeslToken> {
1065
- private stream;
1066
- /** New line */
1067
- private eolPattern;
1068
- private blockCommentPattern;
1069
- src: string;
1070
- constructor(src: string);
1071
- checkpoint(): number;
1072
- reset(position: number): void;
1073
- nextToken(): WeslToken | null;
1074
- /** Peek at the next token without consuming it */
1075
- peek(): WeslToken | null;
1076
- /** Consume token if text matches, otherwise leave position unchanged */
1077
- matchText(text: string): WeslToken | null;
1078
- /** Consume token if kind matches (and optionally text), otherwise leave position unchanged */
1079
- matchKind<K extends WeslTokenKind>(kind: K, text?: string): WeslToken<K> | null;
1080
- /** Consume token if predicate matches, otherwise leave position unchanged */
1081
- nextIf(predicate: (token: WeslToken) => boolean): WeslToken | null;
1082
- /** Match a sequence of tokens by text. Resets and returns null if any fails. */
1083
- matchSequence(...texts: string[]): WeslToken[] | null;
1084
- private skipToEol;
1085
- private skipBlockComment;
1086
- /**
1087
- * Only matches the `<` token if it is a template
1088
- * Precondition: An ident was parsed right before this.
1089
- * Runs the [template list discovery algorithm](https://www.w3.org/TR/WGSL/#template-list-discovery).
1090
- */
1091
- nextTemplateStartToken(): (WeslToken & {
1092
- kind: "symbol";
1093
- }) | null;
1094
- nextTemplateEndToken(): (WeslToken & {
1095
- kind: "symbol";
1096
- }) | null;
1097
- private isTemplateStart;
1098
- /**
1099
- * Call this after consuming an opening bracket.
1100
- * Skips until a closing bracket. This also consumes the closing bracket.
1101
- */
1102
- private skipBracketsTo;
1103
- }
1124
+ //#region src/StandardTypes.d.ts
1125
+ declare const stdFns: string[];
1126
+ declare const sampledTextureTypes = "\n texture_1d texture_2d texture_2d_array texture_3d \n texture_cube texture_cube_array\n";
1127
+ declare const multisampledTextureTypes = "\n texture_multisampled_2d texture_depth_multisampled_2d\n";
1128
+ declare const textureStorageTypes = "\n texture_storage_1d texture_storage_2d texture_storage_2d_array \n texture_storage_3d\n";
1129
+ declare const stdTypes: string[];
1130
+ /** https://www.w3.org/TR/WGSL/#predeclared-enumerants */
1131
+ declare const stdEnumerants: string[];
1132
+ /** return true if the name is for a built in type (not a user struct) */
1133
+ declare function stdType(name: string): boolean;
1134
+ /** return true if the name is for a built in fn (not a user function) */
1135
+ declare function stdFn(name: string): boolean;
1136
+ /** return true if the name is for a built in enumerant */
1137
+ declare function stdEnumerant(name: string): boolean;
1104
1138
  //#endregion
1105
1139
  //#region src/TransformBindingStructs.d.ts
1106
1140
  declare function bindingStructsPlugin(): WeslJsPlugin;
@@ -1197,4 +1231,4 @@ declare function offsetToLineNumber(offset: number, text: string): [lineNum: num
1197
1231
  */
1198
1232
  declare function errorHighlight(source: string, span: Span): [string, string];
1199
1233
  //#endregion
1200
- export { AbstractElem, AbstractElemBase, AliasElem, Attribute, AttributeElem, BatchModuleResolver, BinaryExpression, BinaryOperator, BindIdentsParams, BindResults, BindingAST, BindingStructElem, BlockStatement, BoundAndTransformed, BuiltinAttribute, BundleResolver, ComponentExpression, ComponentMemberExpression, CompositeResolver, ConditionalAttribute, Conditions, ConstAssertElem, ConstElem, ContainerElem, ContinuingElem, DeclIdent, DeclIdentElem, DeclarationElem, DiagnosticAttribute, DiagnosticDirective, DiagnosticRule, DirectiveElem, DirectiveVariant, ElemKindMap, ElemWithAttributes, ElemWithContentsBase, ElifAttribute, ElseAttribute, EmittableElem, EnableDirective, ExpressionElem, ExtendedGPUValidationError, FnElem, FnParamElem, FunctionCallExpression, GlobalDeclarationElem, GlobalVarElem, GrammarElem, HasAttributes, Ident, IfAttribute, ImportCollection, ImportElem, ImportItem, ImportSegment, ImportStatement, InterpolateAttribute, LetElem, LexicalScope, LinkConfig, LinkParams, LinkRegistryParams, LinkedWesl, LinkerTransform, Literal, LiveDecls, ManglerFn, ModuleElem, ModuleResolver, NameElem, OpenElem, OverrideElem, ParenthesizedExpression, ParseError, PartialScope, RecordResolver, RecordResolverOptions, RefIdent, RefIdentElem, RequiresDirective, Scope, SimpleMemberRef, Span, SrcMap, SrcMapBuilder, SrcMapEntry, SrcModule, SrcPosition, SrcWithPath, StableState, StandardAttribute, StatementElem, StructElem, StructMemberElem, StuffElem, SwitchClauseElem, SyntheticElem, TerminalElem, TextElem, TransformedAST, TranslateTimeExpressionElem, TypeRefElem, TypeTemplateParameter, TypedDeclElem, UnaryExpression, UnaryOperator, UnknownExpressionElem, VarElem, VirtualLibrary, VirtualLibraryFn, VirtualLibrarySet, WeslAST, WeslBundle, WeslDevice, WeslGPUCompilationInfo, WeslGPUCompilationMessage, WeslJsPlugin, WeslParseContext, WeslParseError, WeslParseState, WeslStream, _linkSync, astToString, attributeToString, bindAndTransform, bindIdents, bindIdentsRecursive, bindingStructsPlugin, childIdent, childScope, containsScope, debug, debugContentsToString, emptyScope, errorHighlight, fileToModulePath, filterMap, findMap, findRefsToBindingStructs, findUnboundIdents, findValidRootDecls, flatImports, groupBy, grouped, identToString, last, lengthPrefixMangle, link, linkRegistry, liveDeclsToString, log, lowerBindingStructs, makeLiveDecls, makeWeslDevice, mapForward, mapValues, markBindingStructs, markEntryTypes, mergeScope, minimalMangle, minimallyMangledName, modulePartsToRelativePath, moduleToRelativePath, multiKeySet, nextIdentId, noSuffix, normalize, normalizeDebugRoot, normalizeModuleName, npmNameVariations, offsetToLineNumber, overlapTail, parseSrcModule, partition, publicDecl, replaceWords, requestWeslDevice, resetScopeIds, resolveModulePath, sanitizePackageName, scan, scopeToString, scopeToStringLong, srcLog, transformBindingReference, transformBindingStruct, underscoreMangle, validation, withLoggerAsync };
1234
+ export { AbstractElem, AbstractElemBase, AliasElem, Attribute, AttributeElem, BatchModuleResolver, BinaryExpression, BinaryOperator, BindIdentsParams, BindResults, BindingAST, BindingStructElem, BlockStatement, BoundAndTransformed, BuiltinAttribute, BundleResolver, ComponentExpression, ComponentMemberExpression, CompositeResolver, ConditionalAttribute, Conditions, ConstAssertElem, ConstElem, ContainerElem, ContinuingElem, DeclIdent, DeclIdentElem, DeclarationElem, DiagnosticAttribute, DiagnosticDirective, DiagnosticRule, DirectiveElem, DirectiveVariant, ElemKindMap, ElemWithAttributes, ElemWithContentsBase, ElifAttribute, ElseAttribute, EmittableElem, EnableDirective, ExpressionElem, ExtendedGPUValidationError, FnElem, FnParamElem, FunctionCallExpression, GlobalDeclarationElem, GlobalVarElem, GrammarElem, HasAttributes, Ident, IfAttribute, ImportCollection, ImportElem, ImportItem, ImportSegment, ImportStatement, InterpolateAttribute, LetElem, LexicalScope, LinkConfig, LinkParams, LinkRegistryParams, LinkedWesl, LinkerTransform, Literal, LiveDecls, ManglerFn, ModuleElem, ModuleResolver, NameElem, OpenElem, OverrideElem, ParenthesizedExpression, ParseError, type ParseOptions, PartialScope, RecordResolver, RecordResolverOptions, RefIdent, RefIdentElem, RequiresDirective, Scope, SimpleMemberRef, Span, SrcMap, SrcMapBuilder, SrcMapEntry, SrcModule, SrcPosition, SrcWithPath, StableState, StandardAttribute, StatementElem, StructElem, StructMemberElem, StuffElem, SwitchClauseElem, SyntheticElem, TerminalElem, TextElem, TransformedAST, TranslateTimeExpressionElem, TypeRefElem, TypeTemplateParameter, TypedDeclElem, UnaryExpression, UnaryOperator, UnboundRef, UnknownExpressionElem, VarElem, VirtualLibrary, VirtualLibraryFn, VirtualLibrarySet, WeslAST, WeslBundle, WeslDevice, WeslGPUCompilationInfo, WeslGPUCompilationMessage, WeslJsPlugin, WeslParseContext, WeslParseError, WeslParseState, WeslStream, _linkSync, astToString, attributeToString, bindAndTransform, bindIdents, bindIdentsRecursive, bindingStructsPlugin, childIdent, childScope, containsScope, debug, debugContentsToString, emptyScope, errorHighlight, fileToModulePath, filterMap, findMap, findRefsToBindingStructs, findUnboundIdents, findUnboundRefs, findValidRootDecls, flatImports, 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, withLoggerAsync };
package/dist/index.js CHANGED
@@ -46,9 +46,9 @@ function logInternalSrc(logFn, src, pos, ...msgs) {
46
46
  logFn(carets(linePos, linePos2));
47
47
  }
48
48
  function carets(linePos, linePos2) {
49
- const indent = " ".repeat(linePos);
49
+ const indent = " ".repeat(Math.max(0, linePos));
50
50
  const numCarets = linePos2 ? linePos2 - linePos : 1;
51
- return indent + "^".repeat(numCarets);
51
+ return indent + "^".repeat(Math.max(1, numCarets));
52
52
  }
53
53
  const startCache = /* @__PURE__ */ new Map();
54
54
  /** return the line in the src containing a given character position */
@@ -235,12 +235,12 @@ function offsetToLineNumber(offset, text) {
235
235
  */
236
236
  function errorHighlight(source, span) {
237
237
  let lineStartOffset = source.lastIndexOf("\n", span[0]);
238
- if (lineStartOffset === -1) lineStartOffset = 0;
238
+ lineStartOffset = lineStartOffset === -1 ? 0 : lineStartOffset + 1;
239
239
  let lineEndOffset = source.indexOf("\n", span[0]);
240
240
  if (lineEndOffset === -1) lineEndOffset = source.length;
241
241
  const errorLength = span[1] - span[0];
242
242
  const caretCount = Math.max(1, errorLength);
243
- const linePos = span[0] - lineStartOffset;
243
+ const linePos = Math.max(0, span[0] - lineStartOffset);
244
244
  return [source.slice(lineStartOffset, lineEndOffset), " ".repeat(linePos) + "^".repeat(caretCount)];
245
245
  }
246
246
 
@@ -1443,6 +1443,7 @@ function expectWord(stream, errorMsg) {
1443
1443
  function expectExpression(ctx, errorMsg = "Expected expression") {
1444
1444
  const expr = parseExpression(ctx);
1445
1445
  if (!expr) throwParseError(ctx.stream, errorMsg);
1446
+ if (ctx.options.preserveExpressions) ctx.addElem(expr);
1446
1447
  return expr;
1447
1448
  }
1448
1449
  /** Throw a ParseError at the current/next token position. */
@@ -2481,7 +2482,8 @@ function parseReturnStmt(ctx, startPos, attributes) {
2481
2482
  const { stream } = ctx;
2482
2483
  if (!stream.matchText("return")) return null;
2483
2484
  beginElem(ctx, "statement", attributes);
2484
- parseExpression(ctx);
2485
+ const expr = parseExpression(ctx);
2486
+ if (expr && ctx.options.preserveExpressions) ctx.addElem(expr);
2485
2487
  expect(stream, ";", "return statement");
2486
2488
  return finishBlockStatement(startPos, ctx, attributes);
2487
2489
  }
@@ -2532,11 +2534,13 @@ function parsePhonyAssignment(ctx, startPos, attributes) {
2532
2534
  function parseExpressionStmt(ctx, startPos, attributes) {
2533
2535
  const { stream } = ctx;
2534
2536
  beginElem(ctx, "statement", attributes);
2535
- if (!parseExpression(ctx)) {
2537
+ const expr = parseExpression(ctx);
2538
+ if (!expr) {
2536
2539
  finishContents(ctx, startPos, startPos);
2537
2540
  stream.reset(startPos);
2538
2541
  return null;
2539
2542
  }
2543
+ if (ctx.options.preserveExpressions) ctx.addElem(expr);
2540
2544
  if (!parseIncDecOperator(stream)) parseAssignmentRhs(ctx);
2541
2545
  expect(stream, ";", "expression");
2542
2546
  return finishBlockStatement(startPos, ctx, attributes);
@@ -2569,7 +2573,8 @@ function parseForStatement(ctx, attributes) {
2569
2573
  ctx.pushScope();
2570
2574
  expect(stream, "(", "'for'");
2571
2575
  parseForInit(ctx);
2572
- parseExpression(ctx);
2576
+ const cond = parseExpression(ctx);
2577
+ if (cond && ctx.options.preserveExpressions) ctx.addElem(cond);
2573
2578
  expect(stream, ";", "for loop condition");
2574
2579
  parseForUpdate(ctx);
2575
2580
  expect(stream, ")", "for loop header");
@@ -2611,14 +2616,16 @@ function parseForInit(ctx) {
2611
2616
  const varDecl = parseLocalVarDecl(ctx);
2612
2617
  if (varDecl) ctx.addElem(varDecl);
2613
2618
  else {
2614
- parseExpression(ctx);
2619
+ const expr = parseExpression(ctx);
2620
+ if (expr && ctx.options.preserveExpressions) ctx.addElem(expr);
2615
2621
  expect(stream, ";", "for loop init");
2616
2622
  }
2617
2623
  }
2618
2624
  /** Grammar: for_update : variable_updating_statement | func_call_statement
2619
2625
  * variable_updating_statement : assignment_statement | increment_statement | decrement_statement */
2620
2626
  function parseForUpdate(ctx) {
2621
- parseExpression(ctx);
2627
+ const expr = parseExpression(ctx);
2628
+ if (expr && ctx.options.preserveExpressions) ctx.addElem(expr);
2622
2629
  parseIncDecOperator(ctx.stream) || parseAssignmentRhs(ctx);
2623
2630
  }
2624
2631
 
@@ -3096,11 +3103,13 @@ var ParsingContext = class {
3096
3103
  srcModule;
3097
3104
  stream;
3098
3105
  state;
3099
- constructor(stream, state) {
3106
+ options;
3107
+ constructor(stream, state, options) {
3100
3108
  this.stream = stream;
3101
3109
  this.state = state;
3102
3110
  this.srcModule = state.stable.srcModule;
3103
3111
  this.src = this.srcModule.src;
3112
+ this.options = options ?? {};
3104
3113
  }
3105
3114
  position() {
3106
3115
  return this.stream.checkpoint();
@@ -3509,8 +3518,8 @@ var WeslStream = class {
3509
3518
  //#endregion
3510
3519
  //#region src/parse/ParseWesl.ts
3511
3520
  /** Parse a WESL source module into an AST. */
3512
- function parseWesl(srcModule) {
3513
- const { ctx, state } = createParseState(srcModule);
3521
+ function parseWesl(srcModule, options) {
3522
+ const { ctx, state } = createParseState(srcModule, options);
3514
3523
  try {
3515
3524
  beginElem(ctx, "module");
3516
3525
  parseModule(ctx);
@@ -3529,7 +3538,7 @@ function parseWesl(srcModule) {
3529
3538
  }
3530
3539
  }
3531
3540
  /** Initialize parse state: token stream, root scope, and module element. */
3532
- function createParseState(srcModule) {
3541
+ function createParseState(srcModule, options) {
3533
3542
  const stream = new WeslStream(srcModule.src);
3534
3543
  const rootScope = emptyScope(null);
3535
3544
  const moduleElem = {
@@ -3551,7 +3560,7 @@ function createParseState(srcModule) {
3551
3560
  }
3552
3561
  };
3553
3562
  return {
3554
- ctx: new ParsingContext(stream, state),
3563
+ ctx: new ParsingContext(stream, state, options),
3555
3564
  state
3556
3565
  };
3557
3566
  }
@@ -3574,8 +3583,8 @@ var WeslParseError = class extends Error {
3574
3583
  }
3575
3584
  };
3576
3585
  /** Parse a WESL file. */
3577
- function parseSrcModule(srcModule) {
3578
- return parseWesl(srcModule);
3586
+ function parseSrcModule(srcModule, options) {
3587
+ return parseWesl(srcModule, options);
3579
3588
  }
3580
3589
  /** @return flattened form of import tree for binding idents. */
3581
3590
  function flatImports(ast, conditions) {
@@ -3849,14 +3858,24 @@ function findQualifiedImport(refIdent, ctx) {
3849
3858
  const identParts = refIdent.originalName.split("::");
3850
3859
  const pathParts = matchingImport(identParts, flatImps) ?? qualifiedIdent(identParts);
3851
3860
  if (!pathParts) {
3852
- if (unbound && !stdWgsl(refIdent.originalName)) unbound.push(identParts);
3861
+ if (unbound && !stdWgsl(refIdent.originalName)) pushUnbound(unbound, identParts, refIdent);
3853
3862
  return;
3854
3863
  }
3855
3864
  const result = findExport(pathParts, refIdent.ast.srcModule, ctx);
3856
- if (!result) if (unbound) unbound.push(pathParts);
3865
+ if (!result) if (unbound) pushUnbound(unbound, pathParts, refIdent);
3857
3866
  else failIdent(refIdent, `module not found for '${pathParts.join("::")}'`);
3858
3867
  return result;
3859
3868
  }
3869
+ /** Add an unbound reference with position info. */
3870
+ function pushUnbound(unbound, path, refIdent) {
3871
+ const { srcModule, start, end } = refIdent.refIdentElem;
3872
+ unbound.push({
3873
+ path,
3874
+ srcModule,
3875
+ start,
3876
+ end
3877
+ });
3878
+ }
3860
3879
  /** Find an import statement that matches a provided identifier. */
3861
3880
  function matchingImport(identParts, imports) {
3862
3881
  const flat = imports.find((f) => f.importPath.at(-1) === identParts[0]);
@@ -3940,6 +3959,10 @@ function collectDecls(scope, found) {
3940
3959
  * (e.g., [['foo', 'bar', 'baz'], ['other', 'pkg']])
3941
3960
  */
3942
3961
  function findUnboundIdents(resolver) {
3962
+ return findUnboundRefs(resolver).map((ref) => ref.path);
3963
+ }
3964
+ /** Find unbound references with full position info. */
3965
+ function findUnboundRefs(resolver) {
3943
3966
  const bindContext = {
3944
3967
  resolver,
3945
3968
  conditions: {},
@@ -4308,7 +4331,7 @@ var SrcMap = class SrcMap {
4308
4331
  const newEntries = [prev];
4309
4332
  for (let i = 1; i < this.entries.length; i++) {
4310
4333
  const e = this.entries[i];
4311
- if (e.src.path === prev.src.path && e.src.text === prev.src.text && prev.destEnd === e.destStart && prev.srcEnd === e.srcStart) {
4334
+ if (e.src.path === prev.src.path && e.src.text === prev.src.text && prev.destEnd === e.destStart && prev.srcEnd === e.srcStart && prev.srcEnd - prev.srcStart === prev.destEnd - prev.destStart) {
4312
4335
  prev.destEnd = e.destEnd;
4313
4336
  prev.srcEnd = e.srcEnd;
4314
4337
  } else {
@@ -4937,4 +4960,4 @@ function makeWeslDevice(device) {
4937
4960
  }
4938
4961
 
4939
4962
  //#endregion
4940
- export { BundleResolver, CompositeResolver, LinkedWesl, ParseError, RecordResolver, SrcMap, SrcMapBuilder, WeslParseError, WeslStream, _linkSync, astToString, attributeToString, bindAndTransform, bindIdents, bindIdentsRecursive, bindingStructsPlugin, childIdent, childScope, containsScope, debug, debugContentsToString, emptyScope, errorHighlight, fileToModulePath, filterMap, findMap, findRefsToBindingStructs, findUnboundIdents, findValidRootDecls, flatImports, groupBy, grouped, identToString, last, lengthPrefixMangle, link, linkRegistry, liveDeclsToString, log, lowerBindingStructs, makeLiveDecls, makeWeslDevice, mapForward, mapValues, markBindingStructs, markEntryTypes, mergeScope, minimalMangle, minimallyMangledName, modulePartsToRelativePath, moduleToRelativePath, multiKeySet, nextIdentId, noSuffix, normalize, normalizeDebugRoot, normalizeModuleName, npmNameVariations, offsetToLineNumber, overlapTail, parseSrcModule, partition, publicDecl, replaceWords, requestWeslDevice, resetScopeIds, resolveModulePath, sanitizePackageName, scan, scopeToString, scopeToStringLong, srcLog, transformBindingReference, transformBindingStruct, underscoreMangle, validation, withLoggerAsync };
4963
+ export { BundleResolver, CompositeResolver, LinkedWesl, ParseError, RecordResolver, SrcMap, SrcMapBuilder, WeslParseError, WeslStream, _linkSync, astToString, attributeToString, bindAndTransform, bindIdents, bindIdentsRecursive, bindingStructsPlugin, childIdent, childScope, containsScope, debug, debugContentsToString, emptyScope, errorHighlight, fileToModulePath, filterMap, findMap, findRefsToBindingStructs, findUnboundIdents, findUnboundRefs, findValidRootDecls, flatImports, 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, withLoggerAsync };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wesl",
3
- "version": "0.7.14",
3
+ "version": "0.7.15",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -27,7 +27,7 @@
27
27
  "wrangler": "^4.22.0"
28
28
  },
29
29
  "peerDependencies": {
30
- "random_wgsl": "^0.6.63"
30
+ "random_wgsl": "^0.6.64"
31
31
  },
32
32
  "peerDependenciesMeta": {
33
33
  "random_wgsl": {
package/src/BindIdents.ts CHANGED
@@ -70,8 +70,20 @@ export interface BindResults {
70
70
  /** Additional global statements to emit (e.g., const_assert). */
71
71
  newStatements: EmittableElem[];
72
72
 
73
- /** Unbound module paths (only if accumulateUnbound is true). */
74
- unbound?: string[][];
73
+ /** Unbound identifiers with position info (only if accumulateUnbound is true). */
74
+ unbound?: UnboundRef[];
75
+ }
76
+
77
+ /** An unresolved reference with position info for error reporting. */
78
+ export interface UnboundRef {
79
+ /** Module path that couldn't be resolved (e.g., ["package", "foo", "bar"]). */
80
+ path: string[];
81
+ /** Source module containing this reference. */
82
+ srcModule: SrcModule;
83
+ /** Start offset in the source. */
84
+ start: number;
85
+ /** End offset in the source. */
86
+ end: number;
75
87
  }
76
88
 
77
89
  /** An element that can be directly emitted into the linked result. */
@@ -223,7 +235,7 @@ interface BindContext {
223
235
  virtuals?: VirtualLibrarySet;
224
236
 
225
237
  /** Unbound identifiers if accumulateUnbound is true. */
226
- unbound?: string[][];
238
+ unbound?: UnboundRef[];
227
239
 
228
240
  /** Don't follow references from declarations (for library dependency detection). */
229
241
  dontFollowDecls?: boolean;
@@ -370,18 +382,30 @@ function findQualifiedImport(
370
382
  matchingImport(identParts, flatImps) ?? qualifiedIdent(identParts);
371
383
 
372
384
  if (!pathParts) {
373
- if (unbound && !stdWgsl(refIdent.originalName)) unbound.push(identParts);
385
+ if (unbound && !stdWgsl(refIdent.originalName)) {
386
+ pushUnbound(unbound, identParts, refIdent);
387
+ }
374
388
  return undefined;
375
389
  }
376
390
 
377
391
  const result = findExport(pathParts, refIdent.ast.srcModule, ctx);
378
392
  if (!result) {
379
- if (unbound) unbound.push(pathParts);
393
+ if (unbound) pushUnbound(unbound, pathParts, refIdent);
380
394
  else failIdent(refIdent, `module not found for '${pathParts.join("::")}'`);
381
395
  }
382
396
  return result;
383
397
  }
384
398
 
399
+ /** Add an unbound reference with position info. */
400
+ function pushUnbound(
401
+ unbound: UnboundRef[],
402
+ path: string[],
403
+ refIdent: RefIdent,
404
+ ): void {
405
+ const { srcModule, start, end } = refIdent.refIdentElem;
406
+ unbound.push({ path, srcModule, start, end });
407
+ }
408
+
385
409
  /** Find an import statement that matches a provided identifier. */
386
410
  function matchingImport(
387
411
  identParts: string[],
package/src/Logging.ts CHANGED
@@ -88,9 +88,9 @@ function logInternalSrc(
88
88
  }
89
89
 
90
90
  function carets(linePos: number, linePos2?: number): string {
91
- const indent = " ".repeat(linePos);
91
+ const indent = " ".repeat(Math.max(0, linePos));
92
92
  const numCarets = linePos2 ? linePos2 - linePos : 1;
93
- const caretStr = "^".repeat(numCarets);
93
+ const caretStr = "^".repeat(Math.max(1, numCarets));
94
94
  return indent + caretStr;
95
95
  }
96
96
 
package/src/ParseWESL.ts CHANGED
@@ -9,10 +9,13 @@ import { filterValidElements } from "./Conditions.ts";
9
9
  import { type FlatImport, flattenTreeImport } from "./FlattenTreeImport.ts";
10
10
  import type { ParseError } from "./ParseError.ts";
11
11
  import { parseWesl } from "./parse/ParseWesl.ts";
12
+ import type { ParseOptions } from "./parse/ParsingContext.ts";
12
13
  import type { Conditions, Scope, SrcModule } from "./Scope.ts";
13
14
  import type { Span } from "./Span.ts";
14
15
  import { errorHighlight, offsetToLineNumber } from "./Util.ts";
15
16
 
17
+ export type { ParseOptions };
18
+
16
19
  /** Partial element being constructed during parsing. */
17
20
  export type OpenElem<T extends ContainerElem = ContainerElem> = Pick<
18
21
  T,
@@ -78,8 +81,11 @@ export class WeslParseError extends Error {
78
81
  }
79
82
 
80
83
  /** Parse a WESL file. */
81
- export function parseSrcModule(srcModule: SrcModule): WeslAST {
82
- return parseWesl(srcModule);
84
+ export function parseSrcModule(
85
+ srcModule: SrcModule,
86
+ options?: ParseOptions,
87
+ ): WeslAST {
88
+ return parseWesl(srcModule, options);
83
89
  }
84
90
 
85
91
  /** @return flattened form of import tree for binding idents. */
package/src/SrcMap.ts CHANGED
@@ -50,7 +50,8 @@ export class SrcMap {
50
50
  e.src.path === prev.src.path &&
51
51
  e.src.text === prev.src.text &&
52
52
  prev.destEnd === e.destStart &&
53
- prev.srcEnd === e.srcStart
53
+ prev.srcEnd === e.srcStart &&
54
+ prev.srcEnd - prev.srcStart === prev.destEnd - prev.destStart
54
55
  ) {
55
56
  // combine adjacent range entries into one
56
57
  prev.destEnd = e.destEnd;
package/src/Util.ts CHANGED
@@ -175,9 +175,7 @@ export function offsetToLineNumber(
175
175
  */
176
176
  export function errorHighlight(source: string, span: Span): [string, string] {
177
177
  let lineStartOffset = source.lastIndexOf("\n", span[0]);
178
- if (lineStartOffset === -1) {
179
- lineStartOffset = 0;
180
- }
178
+ lineStartOffset = lineStartOffset === -1 ? 0 : lineStartOffset + 1;
181
179
  let lineEndOffset = source.indexOf("\n", span[0]);
182
180
  if (lineEndOffset === -1) {
183
181
  lineEndOffset = source.length;
@@ -186,7 +184,7 @@ export function errorHighlight(source: string, span: Span): [string, string] {
186
184
  // LATER Handle multiline spans
187
185
  const errorLength = span[1] - span[0];
188
186
  const caretCount = Math.max(1, errorLength);
189
- const linePos = span[0] - lineStartOffset;
187
+ const linePos = Math.max(0, span[0] - lineStartOffset);
190
188
  return [
191
189
  source.slice(lineStartOffset, lineEndOffset),
192
190
  " ".repeat(linePos) + "^".repeat(caretCount),
@@ -3,6 +3,7 @@ import {
3
3
  bindIdentsRecursive,
4
4
  type EmittableElem,
5
5
  findValidRootDecls,
6
+ type UnboundRef,
6
7
  } from "../BindIdents.ts";
7
8
  import { type LiveDecls, makeLiveDecls } from "../LiveDeclarations.ts";
8
9
  import { minimalMangle } from "../Mangler.ts";
@@ -21,6 +22,11 @@ import { filterMap } from "../Util.ts";
21
22
  * (e.g., [['foo', 'bar', 'baz'], ['other', 'pkg']])
22
23
  */
23
24
  export function findUnboundIdents(resolver: BatchModuleResolver): string[][] {
25
+ return findUnboundRefs(resolver).map(ref => ref.path);
26
+ }
27
+
28
+ /** Find unbound references with full position info. */
29
+ export function findUnboundRefs(resolver: BatchModuleResolver): UnboundRef[] {
24
30
  const bindContext = {
25
31
  resolver,
26
32
  conditions: {},
@@ -29,7 +35,7 @@ export function findUnboundIdents(resolver: BatchModuleResolver): string[][] {
29
35
  globalNames: new Set<string>(),
30
36
  globalStatements: new Map<AbstractElem, EmittableElem>(),
31
37
  mangler: minimalMangle,
32
- unbound: [] as string[][],
38
+ unbound: [] as UnboundRef[],
33
39
  dontFollowDecls: true,
34
40
  };
35
41
 
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ export { WeslStream } from "./parse/WeslStream.ts";
18
18
  export * from "./Scope.ts";
19
19
  export * from "./Span.ts";
20
20
  export * from "./SrcMap.ts";
21
+ export * from "./StandardTypes.ts";
21
22
  export * from "./TransformBindingStructs.ts";
22
23
  export * from "./Util.ts";
23
24
  export * from "./WeslBundle.ts";
@@ -33,7 +33,8 @@ export function parseForStatement(
33
33
  expect(stream, "(", "'for'");
34
34
 
35
35
  parseForInit(ctx);
36
- parseExpression(ctx); // returns null if empty condition
36
+ const cond = parseExpression(ctx); // returns null if empty condition
37
+ if (cond && ctx.options.preserveExpressions) ctx.addElem(cond);
37
38
  expect(stream, ";", "for loop condition");
38
39
  parseForUpdate(ctx);
39
40
  expect(stream, ")", "for loop header");
@@ -99,7 +100,8 @@ function parseForInit(ctx: ParsingContext): void {
99
100
  ctx.addElem(varDecl);
100
101
  // parseLocalVarDecl already consumed the ';'
101
102
  } else {
102
- parseExpression(ctx); // returns null for empty case
103
+ const expr = parseExpression(ctx); // returns null for empty case
104
+ if (expr && ctx.options.preserveExpressions) ctx.addElem(expr);
103
105
  expect(stream, ";", "for loop init");
104
106
  }
105
107
  }
@@ -107,6 +109,7 @@ function parseForInit(ctx: ParsingContext): void {
107
109
  /** Grammar: for_update : variable_updating_statement | func_call_statement
108
110
  * variable_updating_statement : assignment_statement | increment_statement | decrement_statement */
109
111
  function parseForUpdate(ctx: ParsingContext): void {
110
- parseExpression(ctx);
112
+ const expr = parseExpression(ctx);
113
+ if (expr && ctx.options.preserveExpressions) ctx.addElem(expr);
111
114
  parseIncDecOperator(ctx.stream) || parseAssignmentRhs(ctx);
112
115
  }
@@ -57,7 +57,8 @@ function parseReturnStmt(
57
57
  const { stream } = ctx;
58
58
  if (!stream.matchText("return")) return null;
59
59
  beginElem(ctx, "statement", attributes);
60
- parseExpression(ctx);
60
+ const expr = parseExpression(ctx);
61
+ if (expr && ctx.options.preserveExpressions) ctx.addElem(expr);
61
62
  expect(stream, ";", "return statement");
62
63
  return finishBlockStatement(startPos, ctx, attributes);
63
64
  }
@@ -138,6 +139,7 @@ function parseExpressionStmt(
138
139
  stream.reset(startPos);
139
140
  return null;
140
141
  }
142
+ if (ctx.options.preserveExpressions) ctx.addElem(expr);
141
143
 
142
144
  if (!parseIncDecOperator(stream)) parseAssignmentRhs(ctx);
143
145
  expect(stream, ";", "expression");
@@ -54,6 +54,7 @@ export function expectExpression(
54
54
  ): ExpressionElem {
55
55
  const expr = parseExpression(ctx);
56
56
  if (!expr) throwParseError(ctx.stream, errorMsg);
57
+ if (ctx.options.preserveExpressions) ctx.addElem(expr);
57
58
  return expr;
58
59
  }
59
60
 
@@ -6,12 +6,15 @@ import type { SrcModule } from "../Scope.ts";
6
6
  import { emptyScope } from "../Scope.ts";
7
7
  import { beginElem, finishContents } from "./ContentsHelpers.ts";
8
8
  import { parseModule } from "./ParseModule.ts";
9
- import { ParsingContext } from "./ParsingContext.ts";
9
+ import { type ParseOptions, ParsingContext } from "./ParsingContext.ts";
10
10
  import { WeslStream } from "./WeslStream.ts";
11
11
 
12
12
  /** Parse a WESL source module into an AST. */
13
- export function parseWesl(srcModule: SrcModule): WeslAST {
14
- const { ctx, state } = createParseState(srcModule);
13
+ export function parseWesl(
14
+ srcModule: SrcModule,
15
+ options?: ParseOptions,
16
+ ): WeslAST {
17
+ const { ctx, state } = createParseState(srcModule, options);
15
18
  try {
16
19
  beginElem(ctx, "module");
17
20
  parseModule(ctx);
@@ -30,7 +33,10 @@ export function parseWesl(srcModule: SrcModule): WeslAST {
30
33
  }
31
34
 
32
35
  /** Initialize parse state: token stream, root scope, and module element. */
33
- function createParseState(srcModule: SrcModule): {
36
+ function createParseState(
37
+ srcModule: SrcModule,
38
+ options?: ParseOptions,
39
+ ): {
34
40
  ctx: ParsingContext;
35
41
  state: WeslParseState;
36
42
  } {
@@ -46,6 +52,6 @@ function createParseState(srcModule: SrcModule): {
46
52
  context: { scope: rootScope, openElems: [] },
47
53
  stable: { srcModule, moduleElem, rootScope, imports: [] },
48
54
  };
49
- const ctx = new ParsingContext(stream, state);
55
+ const ctx = new ParsingContext(stream, state, options);
50
56
  return { ctx, state };
51
57
  }
@@ -11,18 +11,30 @@ import {
11
11
  } from "../Scope.ts";
12
12
  import type { WeslStream } from "./WeslStream.ts";
13
13
 
14
+ export interface ParseOptions {
15
+ /** Store expression AST nodes in statement contents (for tooling/validation). */
16
+ // LATER we'll always store expressions in the AST, (but this partial support is for wgsl-edit validation)
17
+ preserveExpressions?: boolean;
18
+ }
19
+
14
20
  /** Context for parsers to build AST and manage scopes. */
15
21
  export class ParsingContext {
16
22
  src: string;
17
23
  srcModule: SrcModule;
18
24
  stream: WeslStream;
19
25
  state: WeslParseState;
26
+ options: ParseOptions;
20
27
 
21
- constructor(stream: WeslStream, state: WeslParseState) {
28
+ constructor(
29
+ stream: WeslStream,
30
+ state: WeslParseState,
31
+ options?: ParseOptions,
32
+ ) {
22
33
  this.stream = stream;
23
34
  this.state = state;
24
35
  this.srcModule = state.stable.srcModule;
25
36
  this.src = this.srcModule.src;
37
+ this.options = options ?? {};
26
38
  }
27
39
 
28
40
  position(): number {
@@ -79,8 +79,9 @@ test("collect unbound references", async () => {
79
79
  const bindResult = bindIdents({ resolver, rootAst, accumulateUnbound: true });
80
80
 
81
81
  const expected = ["pkg1::bar::baz", "pkg2::foo"];
82
- const expectedArrays = expected.map(s => s.split("::")).sort();
83
- expect(bindResult.unbound?.sort()).deep.equal(expectedArrays);
82
+ const expectedPaths = expected.map(s => s.split("::")).sort();
83
+ const unboundPaths = bindResult.unbound?.map(ref => ref.path).sort();
84
+ expect(unboundPaths).deep.equal(expectedPaths);
84
85
  });
85
86
 
86
87
  test("publicDecl finds valid conditional declaration", () => {
@@ -18,9 +18,8 @@ test("parse invalid if", () => {
18
18
  }`;
19
19
  expect(() => parseTest(src)).toThrowErrorMatchingInlineSnapshot(`
20
20
  [Error: ./test.wesl:3:13 error: Invalid token 🐈
21
-
22
21
  if(1<1) { 🐈‍⬛ } else { }
23
- ^^]
22
+ ^^]
24
23
  `);
25
24
  });
26
25
 
@@ -0,0 +1,69 @@
1
+ import { expect, test } from "vitest";
2
+ import { SrcMap, type SrcMapEntry, type SrcWithPath } from "../SrcMap.ts";
3
+
4
+ const src: SrcWithPath = {
5
+ text: "let x = lygia::math::consts::PI;\n let x = 7;",
6
+ path: "test.wesl",
7
+ };
8
+
9
+ function makeMap(entries: SrcMapEntry[]): SrcMap {
10
+ const dest: SrcWithPath = { text: "PI;\n let x = 7;" };
11
+ return new SrcMap(dest, entries);
12
+ }
13
+
14
+ test("compact does not merge entries with different src/dest lengths", () => {
15
+ // Entry A: src "lygia::math::consts::PI" (25 chars) -> dest "PI" (2 chars)
16
+ // Entry B: src ";\n let x = 7;" (14 chars) -> dest ";\n let x = 7;" (14 chars)
17
+ const entries: SrcMapEntry[] = [
18
+ { src, srcStart: 8, srcEnd: 33, destStart: 0, destEnd: 2 },
19
+ { src, srcStart: 33, srcEnd: 47, destStart: 2, destEnd: 16 },
20
+ ];
21
+ const map = makeMap(entries);
22
+ map.compact();
23
+ expect(map.entries).toHaveLength(2);
24
+ });
25
+
26
+ test("compact merges adjacent entries with equal src/dest lengths", () => {
27
+ const entries: SrcMapEntry[] = [
28
+ { src, srcStart: 0, srcEnd: 5, destStart: 0, destEnd: 5 },
29
+ { src, srcStart: 5, srcEnd: 10, destStart: 5, destEnd: 10 },
30
+ ];
31
+ const map = makeMap(entries);
32
+ map.compact();
33
+ expect(map.entries).toHaveLength(1);
34
+ expect(map.entries[0]).toMatchObject({
35
+ srcStart: 0,
36
+ srcEnd: 10,
37
+ destStart: 0,
38
+ destEnd: 10,
39
+ });
40
+ });
41
+
42
+ test("destToSrc returns correct position after compact with mixed-length entries", () => {
43
+ // dest: "PI;\n let x = 7;"
44
+ // ^^ entry A (2 chars dest, 25 chars src)
45
+ // ^^^^^^^^^^^^^^ entry B (14 chars dest, 14 chars src)
46
+ const entries: SrcMapEntry[] = [
47
+ { src, srcStart: 8, srcEnd: 33, destStart: 0, destEnd: 2 },
48
+ { src, srcStart: 33, srcEnd: 47, destStart: 2, destEnd: 16 },
49
+ ];
50
+ const map = makeMap(entries);
51
+ map.compact();
52
+
53
+ // "x" in "let x = 7" is at dest offset 10 (inside entry B)
54
+ const result = map.destToSrc(10);
55
+ expect(result.position).toBe(41); // srcStart 33 + (10 - 2) = 41
56
+ });
57
+
58
+ test("destToSrc for unmapped position falls back to dest identity", () => {
59
+ const entries: SrcMapEntry[] = [
60
+ { src, srcStart: 0, srcEnd: 3, destStart: 0, destEnd: 3 },
61
+ { src, srcStart: 10, srcEnd: 13, destStart: 10, destEnd: 13 },
62
+ ];
63
+ const map = makeMap(entries);
64
+
65
+ // position 5 is in the gap between entries
66
+ const result = map.destToSrc(5);
67
+ expect(result.src).toBe(map.dest);
68
+ expect(result.position).toBe(5);
69
+ });