@futpib/parser 1.0.4 → 1.0.6

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 (250) hide show
  1. package/.claude/settings.local.json +24 -0
  2. package/.github/workflows/main.yml +1 -0
  3. package/build/androidPackageParser.js +30 -32
  4. package/build/arbitraryDalvikBytecode.d.ts +3 -3
  5. package/build/arbitraryDalvikBytecode.js +33 -27
  6. package/build/arbitraryDalvikExecutable.js +55 -17
  7. package/build/arbitraryJava.d.ts +31 -0
  8. package/build/arbitraryJava.js +532 -0
  9. package/build/arbitraryJavaScript.d.ts +3 -0
  10. package/build/arbitraryJavaScript.js +263 -0
  11. package/build/arbitraryJavascript.d.ts +3 -0
  12. package/build/arbitraryJavascript.js +263 -0
  13. package/build/arbitraryZig.d.ts +3 -0
  14. package/build/arbitraryZig.js +240 -0
  15. package/build/arbitraryZipStream.d.ts +1 -1
  16. package/build/arrayParser.js +72 -13
  17. package/build/backsmali.d.ts +4 -3
  18. package/build/backsmali.js +26 -6
  19. package/build/bash.d.ts +6 -1
  20. package/build/bashParser.js +131 -90
  21. package/build/bashParser.test.js +162 -0
  22. package/build/bashParserEdgeCases.test.d.ts +1 -0
  23. package/build/bashParserEdgeCases.test.js +117 -0
  24. package/build/dalvikBytecodeParser/addressConversion.d.ts +110 -0
  25. package/build/dalvikBytecodeParser/addressConversion.js +334 -0
  26. package/build/dalvikBytecodeParser/formatParsers.d.ts +7 -6
  27. package/build/dalvikBytecodeParser/formatParsers.js +13 -14
  28. package/build/dalvikBytecodeParser.d.ts +60 -31
  29. package/build/dalvikBytecodeParser.js +92 -35
  30. package/build/dalvikBytecodeParser.test-d.d.ts +1 -0
  31. package/build/dalvikBytecodeParser.test-d.js +268 -0
  32. package/build/dalvikBytecodeUnparser/formatUnparsers.d.ts +9 -8
  33. package/build/dalvikBytecodeUnparser/formatUnparsers.js +13 -12
  34. package/build/dalvikBytecodeUnparser.d.ts +2 -2
  35. package/build/dalvikBytecodeUnparser.js +23 -23
  36. package/build/dalvikBytecodeUnparser.test.js +7 -7
  37. package/build/dalvikExecutable.d.ts +3 -3
  38. package/build/dalvikExecutable.test-d.d.ts +1 -0
  39. package/build/dalvikExecutable.test-d.js +59 -0
  40. package/build/dalvikExecutableParser/typedNumbers.d.ts +18 -0
  41. package/build/dalvikExecutableParser/typedNumbers.js +3 -0
  42. package/build/dalvikExecutableParser.d.ts +2 -1
  43. package/build/dalvikExecutableParser.js +96 -77
  44. package/build/dalvikExecutableParser.test.js +24 -3
  45. package/build/dalvikExecutableParserAgainstSmaliParser.test.js +3 -0
  46. package/build/dalvikExecutableUnparser/poolScanners.d.ts +2 -2
  47. package/build/dalvikExecutableUnparser/sectionUnparsers.d.ts +3 -3
  48. package/build/dalvikExecutableUnparser/sectionUnparsers.js +26 -11
  49. package/build/dalvikExecutableUnparser.d.ts +2 -2
  50. package/build/dalvikExecutableUnparser.test.js +2 -1
  51. package/build/disjunctionParser.d.ts +5 -3
  52. package/build/disjunctionParser.js +79 -17
  53. package/build/disjunctionParser.test-d.d.ts +1 -0
  54. package/build/disjunctionParser.test-d.js +72 -0
  55. package/build/elementSwitchParser.d.ts +4 -0
  56. package/build/{exactElementSwitchParser.js → elementSwitchParser.js} +3 -4
  57. package/build/elementSwitchParser.test-d.d.ts +1 -0
  58. package/build/elementSwitchParser.test-d.js +44 -0
  59. package/build/exactSequenceParser.d.ts +4 -2
  60. package/build/exactSequenceParser.test-d.d.ts +1 -0
  61. package/build/exactSequenceParser.test-d.js +36 -0
  62. package/build/fetchCid.js +2 -66
  63. package/build/index.d.ts +3 -2
  64. package/build/index.js +2 -1
  65. package/build/index.test.js +16 -1
  66. package/build/inputReader.d.ts +10 -0
  67. package/build/inputReader.js +36 -0
  68. package/build/java.d.ts +502 -0
  69. package/build/java.js +2 -0
  70. package/build/javaKeyStoreParser.js +14 -17
  71. package/build/javaParser.d.ts +51 -0
  72. package/build/javaParser.js +1538 -0
  73. package/build/javaParser.test.d.ts +1 -0
  74. package/build/javaParser.test.js +1287 -0
  75. package/build/javaScript.d.ts +35 -0
  76. package/build/javaScript.js +1 -0
  77. package/build/javaScriptParser.d.ts +9 -0
  78. package/build/javaScriptParser.js +34 -0
  79. package/build/javaScriptUnparser.d.ts +3 -0
  80. package/build/javaScriptUnparser.js +4 -0
  81. package/build/javaScriptUnparser.test.d.ts +1 -0
  82. package/build/javaScriptUnparser.test.js +24 -0
  83. package/build/javaUnparser.d.ts +2 -0
  84. package/build/javaUnparser.js +519 -0
  85. package/build/javaUnparser.test.d.ts +1 -0
  86. package/build/javaUnparser.test.js +24 -0
  87. package/build/javascript.d.ts +35 -0
  88. package/build/javascript.js +1 -0
  89. package/build/javascriptParser.d.ts +9 -0
  90. package/build/javascriptParser.js +34 -0
  91. package/build/javascriptUnparser.d.ts +3 -0
  92. package/build/javascriptUnparser.js +4 -0
  93. package/build/javascriptUnparser.test.d.ts +1 -0
  94. package/build/javascriptUnparser.test.js +24 -0
  95. package/build/jsonParser.js +2 -12
  96. package/build/lazyMessageError.d.ts +3 -0
  97. package/build/lookaheadParser.js +60 -3
  98. package/build/negativeLookaheadParser.js +70 -11
  99. package/build/nonEmptyArrayParser.js +72 -13
  100. package/build/objectParser.d.ts +12 -0
  101. package/build/objectParser.js +31 -0
  102. package/build/objectParser.test-d.d.ts +1 -0
  103. package/build/objectParser.test-d.js +112 -0
  104. package/build/objectParser.test.d.ts +1 -0
  105. package/build/objectParser.test.js +55 -0
  106. package/build/optionalParser.js +69 -10
  107. package/build/parser.d.ts +4 -0
  108. package/build/parser.js +3 -1
  109. package/build/parser.test.js +114 -1
  110. package/build/parserConsumedSequenceParser.js +66 -7
  111. package/build/parserContext.d.ts +6 -0
  112. package/build/parserContext.js +20 -11
  113. package/build/parserError.d.ts +119 -27
  114. package/build/parserError.js +16 -8
  115. package/build/regexpParser.js +33 -3
  116. package/build/regexpParser.test.js +31 -0
  117. package/build/regularExpressionParser.js +35 -15
  118. package/build/separatedArrayParser.js +73 -14
  119. package/build/separatedNonEmptyArrayParser.js +73 -14
  120. package/build/sliceBoundedParser.js +62 -5
  121. package/build/smaliParser.d.ts +7 -7
  122. package/build/smaliParser.js +185 -268
  123. package/build/smaliParser.test.js +58 -0
  124. package/build/stringEscapes.d.ts +5 -0
  125. package/build/stringEscapes.js +244 -0
  126. package/build/symbolicExpression.d.ts +29 -0
  127. package/build/symbolicExpression.js +1 -0
  128. package/build/symbolicExpressionParser.d.ts +4 -0
  129. package/build/symbolicExpressionParser.js +123 -0
  130. package/build/symbolicExpressionParser.test.d.ts +1 -0
  131. package/build/symbolicExpressionParser.test.js +289 -0
  132. package/build/terminatedArrayParser.js +113 -38
  133. package/build/terminatedArrayParser.test.js +4 -2
  134. package/build/tupleParser.d.ts +7 -15
  135. package/build/tupleParser.js +1 -0
  136. package/build/unionParser.d.ts +5 -3
  137. package/build/unionParser.js +7 -2
  138. package/build/unionParser.test-d.d.ts +1 -0
  139. package/build/unionParser.test-d.js +72 -0
  140. package/build/unionParser.test.js +10 -11
  141. package/build/zig.d.ts +280 -0
  142. package/build/zig.js +2 -0
  143. package/build/zigParser.d.ts +3 -0
  144. package/build/zigParser.js +1119 -0
  145. package/build/zigParser.test.d.ts +1 -0
  146. package/build/zigParser.test.js +1590 -0
  147. package/build/zigUnparser.d.ts +2 -0
  148. package/build/zigUnparser.js +460 -0
  149. package/build/zigUnparser.test.d.ts +1 -0
  150. package/build/zigUnparser.test.js +24 -0
  151. package/build/zipParser.js +19 -32
  152. package/build/zipUnparser.js +19 -7
  153. package/build/zipUnparser.test.js +1 -1
  154. package/node_modules-@types/s-expression/index.d.ts +5 -0
  155. package/package.json +24 -6
  156. package/src/androidPackageParser.ts +33 -60
  157. package/src/arbitraryDalvikBytecode.ts +39 -31
  158. package/src/arbitraryDalvikExecutable.ts +65 -20
  159. package/src/arbitraryJava.ts +804 -0
  160. package/src/arbitraryJavaScript.ts +410 -0
  161. package/src/arbitraryZig.ts +380 -0
  162. package/src/arrayParser.ts +1 -3
  163. package/src/backsmali.ts +35 -4
  164. package/src/bash.ts +8 -1
  165. package/src/bashParser.test.ts +258 -0
  166. package/src/bashParser.ts +180 -143
  167. package/src/dalvikBytecodeParser/addressConversion.ts +496 -0
  168. package/src/dalvikBytecodeParser/formatParsers.ts +19 -29
  169. package/src/dalvikBytecodeParser.test-d.ts +310 -0
  170. package/src/dalvikBytecodeParser.ts +194 -69
  171. package/src/dalvikBytecodeUnparser/formatUnparsers.ts +27 -26
  172. package/src/dalvikBytecodeUnparser.test.ts +7 -7
  173. package/src/dalvikBytecodeUnparser.ts +31 -30
  174. package/src/dalvikExecutable.test-d.ts +132 -0
  175. package/src/dalvikExecutable.ts +3 -3
  176. package/src/dalvikExecutableParser/typedNumbers.ts +11 -0
  177. package/src/dalvikExecutableParser.test.ts +37 -3
  178. package/src/dalvikExecutableParser.test.ts.md +163 -2
  179. package/src/dalvikExecutableParser.test.ts.snap +0 -0
  180. package/src/dalvikExecutableParser.ts +121 -139
  181. package/src/dalvikExecutableParserAgainstSmaliParser.test.ts +4 -0
  182. package/src/dalvikExecutableUnparser/poolScanners.ts +6 -6
  183. package/src/dalvikExecutableUnparser/sectionUnparsers.ts +38 -14
  184. package/src/dalvikExecutableUnparser.test.ts +3 -2
  185. package/src/dalvikExecutableUnparser.ts +4 -4
  186. package/src/disjunctionParser.test-d.ts +105 -0
  187. package/src/disjunctionParser.ts +18 -15
  188. package/src/elementSwitchParser.test-d.ts +74 -0
  189. package/src/elementSwitchParser.ts +51 -0
  190. package/src/exactSequenceParser.test-d.ts +43 -0
  191. package/src/exactSequenceParser.ts +13 -8
  192. package/src/fetchCid.ts +2 -76
  193. package/src/index.test.ts +22 -1
  194. package/src/index.ts +7 -1
  195. package/src/inputReader.ts +53 -0
  196. package/src/java.ts +708 -0
  197. package/src/javaKeyStoreParser.ts +18 -32
  198. package/src/javaParser.test.ts +1592 -0
  199. package/src/javaParser.ts +2640 -0
  200. package/src/javaScript.ts +36 -0
  201. package/src/javaScriptParser.ts +57 -0
  202. package/src/javaScriptUnparser.test.ts +37 -0
  203. package/src/javaScriptUnparser.ts +7 -0
  204. package/src/javaUnparser.test.ts +37 -0
  205. package/src/javaUnparser.ts +640 -0
  206. package/src/jsonParser.ts +6 -27
  207. package/src/lookaheadParser.ts +2 -6
  208. package/src/negativeLookaheadParser.ts +1 -3
  209. package/src/nonEmptyArrayParser.ts +1 -3
  210. package/src/objectParser.test-d.ts +152 -0
  211. package/src/objectParser.test.ts +71 -0
  212. package/src/objectParser.ts +69 -0
  213. package/src/optionalParser.ts +1 -3
  214. package/src/parser.test.ts +151 -4
  215. package/src/parser.ts +11 -1
  216. package/src/parserConsumedSequenceParser.ts +2 -4
  217. package/src/parserContext.ts +26 -11
  218. package/src/parserError.ts +17 -3
  219. package/src/regexpParser.test.ts +78 -0
  220. package/src/regexpParser.ts +35 -3
  221. package/src/regularExpressionParser.ts +36 -37
  222. package/src/separatedArrayParser.ts +1 -3
  223. package/src/separatedNonEmptyArrayParser.ts +1 -3
  224. package/src/sliceBoundedParser.test.ts +2 -2
  225. package/src/sliceBoundedParser.ts +15 -19
  226. package/src/smaliParser.test.ts +64 -0
  227. package/src/smaliParser.test.ts.md +12 -12
  228. package/src/smaliParser.test.ts.snap +0 -0
  229. package/src/smaliParser.ts +246 -534
  230. package/src/stringEscapes.ts +253 -0
  231. package/src/symbolicExpression.ts +17 -0
  232. package/src/symbolicExpressionParser.test.ts +466 -0
  233. package/src/symbolicExpressionParser.ts +190 -0
  234. package/src/terminatedArrayParser.test.ts +9 -6
  235. package/src/terminatedArrayParser.ts +25 -29
  236. package/src/tupleParser.ts +21 -18
  237. package/src/unionParser.test-d.ts +105 -0
  238. package/src/unionParser.test.ts +18 -17
  239. package/src/unionParser.ts +28 -16
  240. package/src/zig.ts +411 -0
  241. package/src/zigParser.test.ts +1693 -0
  242. package/src/zigParser.ts +1745 -0
  243. package/src/zigUnparser.test.ts +37 -0
  244. package/src/zigUnparser.ts +615 -0
  245. package/src/zipParser.ts +20 -56
  246. package/src/zipUnparser.test.ts +1 -1
  247. package/src/zipUnparser.ts +22 -7
  248. package/tsconfig.json +2 -2
  249. package/build/exactElementSwitchParser.d.ts +0 -3
  250. package/src/exactElementSwitchParser.ts +0 -41
@@ -5,7 +5,7 @@ export const createNegativeLookaheadParser = <Sequence>(
5
5
  childParser: Parser<unknown, Sequence>,
6
6
  ): Parser<void, Sequence> => {
7
7
  const negativeLookaheadParser: Parser<void, Sequence> = async parserContext => {
8
- const childParserContext = parserContext.lookahead();
8
+ using childParserContext = parserContext.lookahead();
9
9
 
10
10
  let childParserSuccess: boolean;
11
11
 
@@ -19,8 +19,6 @@ export const createNegativeLookaheadParser = <Sequence>(
19
19
  }
20
20
 
21
21
  childParserSuccess = false;
22
- } finally {
23
- childParserContext.dispose();
24
22
  }
25
23
 
26
24
  parserContext.invariant(
@@ -8,7 +8,7 @@ export const createNonEmptyArrayParser = <ElementOutput, Sequence>(
8
8
  const elements: ElementOutput[] = [];
9
9
 
10
10
  while (true) {
11
- const elementParserContext = parserContext.lookahead();
11
+ using elementParserContext = parserContext.lookahead();
12
12
  const initialPosition = elementParserContext.position;
13
13
  try {
14
14
  const element = await elementParser(elementParserContext);
@@ -24,8 +24,6 @@ export const createNonEmptyArrayParser = <ElementOutput, Sequence>(
24
24
  }
25
25
 
26
26
  throw error;
27
- } finally {
28
- elementParserContext.dispose();
29
27
  }
30
28
  }
31
29
 
@@ -0,0 +1,152 @@
1
+ import { expectAssignable, expectNotAssignable, expectType } from 'tsd';
2
+ import { createObjectParser } from './objectParser.js';
3
+ import { type Parser, type ParserOutput } from './parser.js';
4
+ import { createExactSequenceParser } from './exactSequenceParser.js';
5
+ import { createFixedLengthSequenceParser } from './fixedLengthSequenceParser.js';
6
+
7
+ // Test: parsers only - extracts output types
8
+ {
9
+ const parser = createObjectParser({
10
+ first: createFixedLengthSequenceParser<string>(3),
11
+ second: createFixedLengthSequenceParser<string>(3),
12
+ });
13
+
14
+ type Output = ParserOutput<typeof parser>;
15
+
16
+ // Output should be assignable to and from expected type
17
+ expectAssignable<{ first: string; second: string }>(null! as Output);
18
+ expectAssignable<Output>(null! as { first: string; second: string });
19
+ }
20
+
21
+ // Test: literal types are preserved
22
+ {
23
+ const parser = createObjectParser({
24
+ type: 'block' as const,
25
+ name: createFixedLengthSequenceParser<string>(3),
26
+ });
27
+
28
+ type Output = ParserOutput<typeof parser>;
29
+
30
+ // 'block' literal should be preserved
31
+ expectAssignable<{ type: 'block'; name: string }>(null! as Output);
32
+ expectType<'block'>(null! as Output['type']);
33
+ }
34
+
35
+ // Test: underscore keys excluded from result type
36
+ {
37
+ const parser = createObjectParser({
38
+ first: createFixedLengthSequenceParser<string>(3),
39
+ _sep: createExactSequenceParser(':'),
40
+ second: createFixedLengthSequenceParser<string>(3),
41
+ });
42
+
43
+ type Output = ParserOutput<typeof parser>;
44
+
45
+ // Should have first and second but not _sep
46
+ expectAssignable<{ first: string; second: string }>(null! as Output);
47
+
48
+ // _sep should NOT be a key in Output
49
+ expectNotAssignable<{ _sep: string }>(null! as Output);
50
+ }
51
+
52
+ // Test: mixed literals and parsers with underscore keys
53
+ {
54
+ const parser = createObjectParser({
55
+ type: 'assignment' as const,
56
+ _open: createExactSequenceParser('('),
57
+ name: createFixedLengthSequenceParser<string>(1),
58
+ _eq: createExactSequenceParser('='),
59
+ value: createFixedLengthSequenceParser<string>(1),
60
+ _close: createExactSequenceParser(')'),
61
+ });
62
+
63
+ type Output = ParserOutput<typeof parser>;
64
+
65
+ expectAssignable<{ type: 'assignment'; name: string; value: string }>(null! as Output);
66
+ expectType<'assignment'>(null! as Output['type']);
67
+
68
+ // Underscore keys should not exist
69
+ expectNotAssignable<{ _open: string }>(null! as Output);
70
+ expectNotAssignable<{ _eq: string }>(null! as Output);
71
+ expectNotAssignable<{ _close: string }>(null! as Output);
72
+ }
73
+
74
+ // Test: number literal
75
+ {
76
+ const parser = createObjectParser({
77
+ code: 42 as const,
78
+ data: createFixedLengthSequenceParser<string>(2),
79
+ });
80
+
81
+ type Output = ParserOutput<typeof parser>;
82
+
83
+ expectAssignable<{ code: 42; data: string }>(null! as Output);
84
+ expectType<42>(null! as Output['code']);
85
+ }
86
+
87
+ // Test: all underscore keys results in empty object
88
+ {
89
+ const parser = createObjectParser({
90
+ _a: createExactSequenceParser('a'),
91
+ _b: createExactSequenceParser('b'),
92
+ });
93
+
94
+ type Output = ParserOutput<typeof parser>;
95
+
96
+ // Should be assignable to empty object
97
+ // eslint-disable-next-line @typescript-eslint/ban-types
98
+ expectAssignable<{}>(null! as Output);
99
+
100
+ // Should NOT have _a or _b
101
+ expectNotAssignable<{ _a: string }>(null! as Output);
102
+ expectNotAssignable<{ _b: string }>(null! as Output);
103
+ }
104
+
105
+ // Test: boolean literal
106
+ {
107
+ const parser = createObjectParser({
108
+ enabled: true as const,
109
+ name: createFixedLengthSequenceParser<string>(3),
110
+ });
111
+
112
+ type Output = ParserOutput<typeof parser>;
113
+
114
+ expectAssignable<{ enabled: true; name: string }>(null! as Output);
115
+ expectType<true>(null! as Output['enabled']);
116
+ }
117
+
118
+ // Test: null literal
119
+ {
120
+ const parser = createObjectParser({
121
+ value: null,
122
+ name: createFixedLengthSequenceParser<string>(3),
123
+ });
124
+
125
+ type Output = ParserOutput<typeof parser>;
126
+
127
+ expectAssignable<{ value: null; name: string }>(null! as Output);
128
+ expectType<null>(null! as Output['value']);
129
+ }
130
+
131
+ // Test: underscore-prefixed parsers still contribute to sequence type inference
132
+ {
133
+ const parser = createObjectParser({
134
+ _prefix: createExactSequenceParser<Uint8Array>(Buffer.from([0x00])),
135
+ operation: 'nop' as const,
136
+ });
137
+
138
+ // Parser should be assignable to Parser<{ operation: 'nop' }, Uint8Array>
139
+ expectAssignable<Parser<{ operation: 'nop' }, Uint8Array>>(parser);
140
+ }
141
+
142
+ // Test: sequence type inferred from underscore parser when no other parsers present
143
+ {
144
+ const parser = createObjectParser({
145
+ _marker: createExactSequenceParser<string>('test'),
146
+ type: 'marker' as const,
147
+ value: 42 as const,
148
+ });
149
+
150
+ // Should infer string sequence from _marker parser
151
+ expectAssignable<Parser<{ type: 'marker'; value: 42 }, string>>(parser);
152
+ }
@@ -0,0 +1,71 @@
1
+ import test from 'ava';
2
+ import { createObjectParser } from './objectParser.js';
3
+ import { runParser } from './parser.js';
4
+ import { stringParserInputCompanion } from './parserInputCompanion.js';
5
+ import { createExactSequenceParser } from './exactSequenceParser.js';
6
+ import { createFixedLengthSequenceParser } from './fixedLengthSequenceParser.js';
7
+
8
+ // Type tests are in objectParser.test-d.ts (using tsd)
9
+
10
+ test('parses object with parsers only', async t => {
11
+ const parser = createObjectParser({
12
+ first: createFixedLengthSequenceParser<string>(3),
13
+ second: createFixedLengthSequenceParser<string>(3),
14
+ });
15
+
16
+ const output = await runParser(parser, 'foobar', stringParserInputCompanion);
17
+
18
+ t.deepEqual(output, { first: 'foo', second: 'bar' });
19
+ });
20
+
21
+ test('parses object with literals', async t => {
22
+ const parser = createObjectParser({
23
+ type: 'block' as const,
24
+ value: createFixedLengthSequenceParser<string>(3),
25
+ });
26
+
27
+ const output = await runParser(parser, 'foo', stringParserInputCompanion);
28
+
29
+ t.deepEqual(output, { type: 'block', value: 'foo' });
30
+ });
31
+
32
+ test('excludes underscore-prefixed keys from result but runs parsers', async t => {
33
+ const parser = createObjectParser({
34
+ first: createFixedLengthSequenceParser<string>(3),
35
+ _separator: createExactSequenceParser(':'),
36
+ second: createFixedLengthSequenceParser<string>(3),
37
+ });
38
+
39
+ const output = await runParser(parser, 'foo:bar', stringParserInputCompanion);
40
+
41
+ t.deepEqual(output, { first: 'foo', second: 'bar' });
42
+ t.false('_separator' in output);
43
+ });
44
+
45
+ test('preserves property order', async t => {
46
+ const parser = createObjectParser({
47
+ c: createFixedLengthSequenceParser<string>(1),
48
+ a: createFixedLengthSequenceParser<string>(1),
49
+ b: createFixedLengthSequenceParser<string>(1),
50
+ });
51
+
52
+ const output = await runParser(parser, 'xyz', stringParserInputCompanion);
53
+
54
+ t.deepEqual(Object.keys(output), ['c', 'a', 'b']);
55
+ t.deepEqual(output, { c: 'x', a: 'y', b: 'z' });
56
+ });
57
+
58
+ test('mixed parsers and literals with underscore keys', async t => {
59
+ const parser = createObjectParser({
60
+ type: 'assignment' as const,
61
+ _open: createExactSequenceParser('('),
62
+ name: createFixedLengthSequenceParser<string>(1),
63
+ _eq: createExactSequenceParser('='),
64
+ value: createFixedLengthSequenceParser<string>(1),
65
+ _close: createExactSequenceParser(')'),
66
+ });
67
+
68
+ const output = await runParser(parser, '(x=5)', stringParserInputCompanion);
69
+
70
+ t.deepEqual(output, { type: 'assignment', name: 'x', value: '5' });
71
+ });
@@ -0,0 +1,69 @@
1
+ import { getParserName, setParserName, type Parser, type ParserOutput, type ParserSequence } from './parser.js';
2
+
3
+ // Extract only parser values from an object (filter out literals)
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ type ExtractParsers<T> = T[keyof T] & Parser<any, any, any>;
6
+
7
+ // Extract Sequence from an object of parsers (finds parser's sequence type, ignoring literals)
8
+ type InferSequenceFromParsers<T> = ParserSequence<ExtractParsers<T>>;
9
+
10
+ // Extract output type: Parser<O, S> → O, literal L → L
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ type ExtractOutputOrLiteral<P> = P extends Parser<any, any, any> ? ParserOutput<P> : P;
13
+
14
+ // Filter out underscore-prefixed keys
15
+ type OmitUnderscoreKeys<T> = {
16
+ [K in keyof T as K extends `_${string}` ? never : K]: T[K]
17
+ };
18
+
19
+ // Result type for object parser
20
+ type ObjectParserOutput<Parsers extends Record<string, unknown>> = OmitUnderscoreKeys<{
21
+ [K in keyof Parsers]: ExtractOutputOrLiteral<Parsers[K]>
22
+ }>;
23
+
24
+ export function createObjectParser<
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
+ const Parsers extends Record<string, Parser<any, any, any> | unknown>,
27
+ >(
28
+ parsers: Parsers,
29
+ ): Parser<ObjectParserOutput<Parsers>, InferSequenceFromParsers<Parsers>> {
30
+ type Sequence = InferSequenceFromParsers<Parsers>;
31
+
32
+ const objectParser: Parser<ObjectParserOutput<Parsers>, Sequence> = async parserContext => {
33
+ const result: Record<string, unknown> = {};
34
+
35
+ for (const [key, parserOrLiteral] of Object.entries(parsers)) {
36
+ let value: unknown;
37
+
38
+ if (typeof parserOrLiteral === 'function') {
39
+ // It's a parser - execute it
40
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
+ value = await (parserOrLiteral as Parser<unknown, any>)(parserContext);
42
+ } else {
43
+ // It's a literal - use directly
44
+ value = parserOrLiteral;
45
+ }
46
+
47
+ if (!key.startsWith('_')) {
48
+ result[key] = value;
49
+ }
50
+ }
51
+
52
+ return result as ObjectParserOutput<Parsers>;
53
+ };
54
+
55
+ setParserName(
56
+ objectParser,
57
+ '{' + Object.keys(parsers).map(k => {
58
+ const v = parsers[k];
59
+ if (typeof v === 'function') {
60
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
+ return k.startsWith('_') ? k : getParserName(v as Parser<unknown, any>, k);
62
+ }
63
+
64
+ return `${k}: ${JSON.stringify(v)}`;
65
+ }).join(', ') + '}',
66
+ );
67
+
68
+ return objectParser;
69
+ }
@@ -5,7 +5,7 @@ export const createOptionalParser = <Output, Sequence>(
5
5
  childParser: Parser<Output, Sequence>,
6
6
  ): Parser<undefined | Output, Sequence> => {
7
7
  const optionalParser: Parser<undefined | Output, Sequence> = async parserContext => {
8
- const childParserContext = parserContext.lookahead();
8
+ using childParserContext = parserContext.lookahead();
9
9
 
10
10
  try {
11
11
  const value = await childParser(childParserContext);
@@ -17,8 +17,6 @@ export const createOptionalParser = <Output, Sequence>(
17
17
  }
18
18
 
19
19
  throw error;
20
- } finally {
21
- childParserContext.dispose();
22
20
  }
23
21
  };
24
22
 
@@ -7,6 +7,7 @@ import {
7
7
  isParserParsingJoinError,
8
8
  normalParserErrorModule,
9
9
  ParserError,
10
+ ParserParsingFailedError,
10
11
  ParserParsingJoinError,
11
12
  } from './parserError.js';
12
13
  import { createTupleParser } from './tupleParser.js';
@@ -17,21 +18,23 @@ import { createArrayParser } from './arrayParser.js';
17
18
  import { createElementParser } from './elementParser.js';
18
19
  import { toAsyncIterable } from './toAsyncIterable.js';
19
20
  import { stringFromAsyncIterable } from './stringFromAsyncIterable.js';
21
+ import { createLookaheadParser } from './lookaheadParser.js';
22
+ import { createNegativeLookaheadParser } from './negativeLookaheadParser.js';
20
23
 
21
- const aUnionParser = createUnionParser<string, string>([
24
+ const aUnionParser = createUnionParser([
22
25
  createExactSequenceNaiveParser('1'),
23
26
  createExactSequenceNaiveParser('aa'),
24
27
  createExactSequenceNaiveParser('AAA'),
25
28
  ]);
26
29
 
27
- const abDisjunctionParser = createDisjunctionParser<string, string>([
30
+ const abDisjunctionParser = createDisjunctionParser([
28
31
  aUnionParser,
29
32
  createExactSequenceNaiveParser('2'),
30
33
  createExactSequenceNaiveParser('bb'),
31
34
  createExactSequenceNaiveParser('BBB'),
32
35
  ]);
33
36
 
34
- const abcUnionParser = createUnionParser<string, string>([
37
+ const abcUnionParser = createUnionParser([
35
38
  abDisjunctionParser,
36
39
  createExactSequenceNaiveParser('final_3'),
37
40
  createExactSequenceNaiveParser('final_cc'),
@@ -39,7 +42,7 @@ const abcUnionParser = createUnionParser<string, string>([
39
42
  ]);
40
43
 
41
44
  const sampleParser = promiseCompose(
42
- createTupleParser<string, string, string, string>([
45
+ createTupleParser([
43
46
  aUnionParser,
44
47
  abDisjunctionParser,
45
48
  abcUnionParser,
@@ -332,6 +335,8 @@ test('runParserWithRemainingInput with remaining input', async t => {
332
335
  output,
333
336
  remainingInput,
334
337
  position,
338
+ furthestReadPosition,
339
+ furthestPeekedPosition,
335
340
  ...resultRest
336
341
  } = await runParserWithRemainingInput(parser, 'foobar', stringParserInputCompanion);
337
342
 
@@ -339,6 +344,8 @@ test('runParserWithRemainingInput with remaining input', async t => {
339
344
  t.is(output, 'foo');
340
345
  t.is(await stringFromAsyncIterable(remainingInput!), 'bar');
341
346
  t.is(position, 3);
347
+ t.is(furthestReadPosition, 3);
348
+ t.is(furthestPeekedPosition, 3);
342
349
  });
343
350
 
344
351
  test('runParserWithRemainingInput without remaining input', async t => {
@@ -348,3 +355,143 @@ test('runParserWithRemainingInput without remaining input', async t => {
348
355
  t.is(output, 'foo');
349
356
  t.is(remainingInput, undefined);
350
357
  });
358
+
359
+ test('furthestReadPosition equals position when no backtracking', async t => {
360
+ const parser: Parser<string, string> = createExactSequenceNaiveParser('foo');
361
+ const result = await runParserWithRemainingInput(parser, 'foobar', stringParserInputCompanion);
362
+
363
+ t.is(result.position, 3);
364
+ t.is(result.furthestReadPosition, 3);
365
+ t.is(result.furthestPeekedPosition, 3);
366
+ });
367
+
368
+ test('furthestReadPosition tracks lookahead that succeeded', async t => {
369
+ // Parser: lookahead('foo') followed by 'foobar'
370
+ // The lookahead parses 'foo' (position 3) but doesn't consume it
371
+ // Then 'foobar' is parsed (position 6)
372
+ const parser = createTupleParser([
373
+ createLookaheadParser(createExactSequenceNaiveParser('foo')),
374
+ createExactSequenceNaiveParser('foobar'),
375
+ ]);
376
+
377
+ const result = await runParserWithRemainingInput(parser, 'foobar', stringParserInputCompanion);
378
+
379
+ t.is(result.position, 6);
380
+ t.is(result.furthestReadPosition, 6);
381
+ t.is(result.furthestPeekedPosition, 6);
382
+ });
383
+
384
+ test('furthestReadPosition tracks failed lookahead with backtracking', async t => {
385
+ // Parser: try 'foobar' OR 'foo'
386
+ // First tries 'foobar', reads 'f','o','o','q' (position 4) then fails because 'q' != 'b'
387
+ // Backtracks and tries 'foo', succeeds at position 3
388
+ // furthestReadPosition should be 4 (from the failed 'foobar' attempt that read up to 'q')
389
+ const parser = createDisjunctionParser([
390
+ createExactSequenceNaiveParser('foobar'),
391
+ createExactSequenceNaiveParser('foo'),
392
+ ]);
393
+
394
+ const result = await runParserWithRemainingInput(parser, 'fooqux', stringParserInputCompanion);
395
+
396
+ t.is(result.output, 'foo');
397
+ t.is(result.position, 3);
398
+ t.is(result.furthestReadPosition, 4);
399
+ t.is(result.furthestPeekedPosition, 3);
400
+ });
401
+
402
+ test('furthestReadPosition exceeds position after backtracking from longer match', async t => {
403
+ // Parser: try 'foobarqux' OR 'foo'
404
+ // First tries 'foobarqux', reads up to position 6 ('foobar'), then fails
405
+ // Backtracks and tries 'foo', succeeds at position 3
406
+ // furthestReadPosition should be 6 (from the failed 'foobarqux' attempt)
407
+ const parser = createDisjunctionParser([
408
+ createExactSequenceNaiveParser('foobarqux'),
409
+ createExactSequenceNaiveParser('foo'),
410
+ ]);
411
+
412
+ const result = await runParserWithRemainingInput(parser, 'foobar', stringParserInputCompanion);
413
+
414
+ t.is(result.output, 'foo');
415
+ t.is(result.position, 3);
416
+ t.is(result.furthestReadPosition, 6);
417
+ t.is(result.furthestPeekedPosition, 6);
418
+ });
419
+
420
+ test('furthestPeekedPosition differs from furthestReadPosition with lookahead', async t => {
421
+ // Parser: lookahead('foobar') then 'foo'
422
+ // The lookahead parses 'foobar':
423
+ // - Each read() calls peek(0) at positions 0,1,2,3,4,5 then skip(1) advancing to 1,2,3,4,5,6
424
+ // - This updates furthestReadPosition to 6 and furthestPeekedPosition to 5
425
+ // Then 'foo' is parsed from position 0, position becomes 3
426
+ // furthestReadPosition=6 (from lookahead's skip calls)
427
+ // furthestPeekedPosition=5 (from lookahead's peek calls at positions 0-5)
428
+ const parser = createTupleParser([
429
+ createLookaheadParser(createExactSequenceNaiveParser('foobar')),
430
+ createExactSequenceNaiveParser('foo'),
431
+ ]);
432
+
433
+ const result = await runParserWithRemainingInput(parser, 'foobar', stringParserInputCompanion);
434
+
435
+ t.is(result.output[1], 'foo');
436
+ t.is(result.position, 3);
437
+ t.is(result.furthestReadPosition, 6);
438
+ t.is(result.furthestPeekedPosition, 5);
439
+ });
440
+
441
+ test('error has furthestReadPosition after backtracking', async t => {
442
+ // Parser: try 'foobarqux' OR 'foobaz'
443
+ // Input: 'foobar'
444
+ // First tries 'foobarqux', reads up to position 6 ('foobar'), then fails (no 'qux')
445
+ // Then tries 'foobaz', reads up to position 4 ('foob'), then fails ('b' != 'z')
446
+ // Both fail, error at position 0, but furthestReadPosition should be 6
447
+ const parser = createDisjunctionParser([
448
+ createExactSequenceNaiveParser('foobarqux'),
449
+ createExactSequenceNaiveParser('foobaz'),
450
+ ]);
451
+
452
+ const error = await t.throwsAsync(
453
+ runParser(parser, 'foobar', stringParserInputCompanion, { errorStack: true }),
454
+ ) as ParserParsingFailedError;
455
+
456
+ t.is(error.position, 0);
457
+ t.is(error.furthestReadPosition, 6);
458
+ t.is(error.furthestPeekedPosition, 6);
459
+ });
460
+
461
+ test('error from negative lookahead has furthestReadPosition from lookahead content', async t => {
462
+ // Parser: negativeLookahead('foobar') then 'foo'
463
+ // Input: 'foobar'
464
+ // Negative lookahead tries 'foobar', succeeds (reads to position 6)
465
+ // Since lookahead succeeded, negative lookahead fails
466
+ // Error position is 0 (where negative lookahead started), but furthestReadPosition is 6
467
+ const parser = createTupleParser([
468
+ createNegativeLookaheadParser(createExactSequenceNaiveParser('foobar')),
469
+ createExactSequenceNaiveParser('foo'),
470
+ ]);
471
+
472
+ const error = await t.throwsAsync(
473
+ runParser(parser, 'foobar', stringParserInputCompanion, { errorStack: true }),
474
+ {
475
+ name: 'ParserParsingInvariantError',
476
+ },
477
+ ) as ParserParsingFailedError;
478
+
479
+ t.is(error.position, 0);
480
+ t.is(error.furthestReadPosition, 6);
481
+ t.is(error.furthestPeekedPosition, 5);
482
+ });
483
+
484
+ test('furthestReadPosition on ParserUnexpectedRemainingInputError', async t => {
485
+ const parser: Parser<string, string> = createExactSequenceNaiveParser('foo');
486
+
487
+ const error = await t.throwsAsync(
488
+ runParser(parser, 'foobar', stringParserInputCompanion),
489
+ {
490
+ name: 'ParserUnexpectedRemainingInputError',
491
+ },
492
+ ) as ParserParsingFailedError;
493
+
494
+ t.is(error.position, 3);
495
+ t.is(error.furthestReadPosition, 3);
496
+ t.is(error.furthestPeekedPosition, 3);
497
+ });
package/src/parser.ts CHANGED
@@ -16,6 +16,12 @@ export type Parser<
16
16
  Element = DeriveSequenceElement<Sequence>,
17
17
  > = (parserContext: ParserContext<Sequence, Element>) => Output | Promise<Output>;
18
18
 
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ export type ParserOutput<P> = P extends Parser<infer O, any, any> ? O : never;
21
+
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ export type ParserSequence<P> = P extends Parser<any, infer S, any> ? S : never;
24
+
19
25
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
26
  export function getParserName(parser: Parser<any, any, any>, default_ = 'anonymous'): string {
21
27
  return parser.name || default_;
@@ -69,6 +75,8 @@ export type RunParserWithRemainingInputResult<
69
75
  > = {
70
76
  output: Output;
71
77
  position: number;
78
+ furthestReadPosition: number;
79
+ furthestPeekedPosition: number;
72
80
  remainingInput: undefined | AsyncIterable<Sequence>;
73
81
  };
74
82
 
@@ -195,6 +203,8 @@ export async function runParserWithRemainingInput<
195
203
  return {
196
204
  output,
197
205
  position: parserContext.position,
206
+ furthestReadPosition: inputReader.furthestReadPosition,
207
+ furthestPeekedPosition: inputReader.furthestPeekedPosition,
198
208
  remainingInput,
199
209
  };
200
210
  });
@@ -222,7 +232,7 @@ export async function runParser<
222
232
  const inputReaderState = inputReader.toInputReaderState();
223
233
 
224
234
  if (!inputReaderStateCompanion.isDone(inputReaderState)) {
225
- throw new normalParserErrorModule.ParserUnexpectedRemainingInputError('Unexpected remaining input', 0, parserContext.position);
235
+ throw new normalParserErrorModule.ParserUnexpectedRemainingInputError('Unexpected remaining input', 0, parserContext.position, inputReader.furthestReadPosition, inputReader.furthestPeekedPosition);
226
236
  }
227
237
 
228
238
  return output;
@@ -6,15 +6,13 @@ export const createParserConsumedSequenceParser = <Output, Sequence>(
6
6
  ): Parser<[Output, Sequence], Sequence> => {
7
7
  const parserConsumedSequenceParser: Parser<[Output, Sequence], Sequence> = async parserContext => {
8
8
  const initialPosition = parserContext.position;
9
- const childParserContext = parserContext.lookahead();
10
9
 
11
10
  let value: Output;
12
11
  let consumedLength: number;
13
- try {
12
+ {
13
+ using childParserContext = parserContext.lookahead();
14
14
  value = await childParser(childParserContext);
15
15
  consumedLength = childParserContext.position - initialPosition;
16
- } finally {
17
- childParserContext.dispose();
18
16
  }
19
17
 
20
18
  const consumedSequenceParser = createFixedLengthSequenceParser<Sequence>(consumedLength);