Skip to content

Commit ac477dd

Browse files
committed
Reduce code duplication across Lexer, VM, Evaluator, and BuiltinRegistry
1 parent dd19397 commit ac477dd

5 files changed

Lines changed: 89 additions & 159 deletions

File tree

jjq-core/src/main/java/io/hyperfoil/tools/jjq/builtin/BuiltinRegistry.java

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ public final class BuiltinRegistry {
2020

2121
private final Map<String, BuiltinFunction> builtins = new HashMap<>();
2222

23+
/** Require the input to be a JqArray, throwing a descriptive error if not. */
24+
private static JqArray requireArray(JqValue input, String operation) {
25+
if (input instanceof JqArray arr) return arr;
26+
throw new JqException(operation + " requires array input");
27+
}
28+
2329
public BuiltinRegistry() {
2430
registerDefaults();
2531
}
@@ -177,7 +183,7 @@ private void registerDefaults() {
177183

178184
// Collection builtins
179185
register("map", 1, (input, args, env, eval, out) -> {
180-
if (!(input instanceof JqArray arr)) throw new JqException("map requires array input");
186+
JqArray arr = requireArray(input, "map");
181187
var results = new ArrayList<JqValue>();
182188
for (JqValue elem : arr.arrayValue()) {
183189
eval.eval(args.getFirst(), elem, env, results::add);
@@ -215,7 +221,7 @@ private void registerDefaults() {
215221
});
216222

217223
register("add", 0, (input, args, env, eval, out) -> {
218-
if (!(input instanceof JqArray arr)) throw new JqException("add requires array input");
224+
JqArray arr = requireArray(input, "add");
219225
if (arr.arrayValue().isEmpty()) {
220226
out.accept(JqNull.NULL);
221227
return;
@@ -247,12 +253,12 @@ private void registerDefaults() {
247253
});
248254

249255
register("any", 0, (input, args, env, eval, out) -> {
250-
if (!(input instanceof JqArray arr)) throw new JqException("any requires array input");
256+
JqArray arr = requireArray(input, "any");
251257
out.accept(JqBoolean.of(arr.arrayValue().stream().anyMatch(JqValue::isTruthy)));
252258
});
253259

254260
register("any", 1, (input, args, env, eval, out) -> {
255-
if (!(input instanceof JqArray arr)) throw new JqException("any requires array input");
261+
JqArray arr = requireArray(input, "any");
256262
for (JqValue elem : arr.arrayValue()) {
257263
var results = eval.eval(args.getFirst(), elem, env);
258264
if (results.stream().anyMatch(JqValue::isTruthy)) {
@@ -264,12 +270,12 @@ private void registerDefaults() {
264270
});
265271

266272
register("all", 0, (input, args, env, eval, out) -> {
267-
if (!(input instanceof JqArray arr)) throw new JqException("all requires array input");
273+
JqArray arr = requireArray(input, "all");
268274
out.accept(JqBoolean.of(arr.arrayValue().stream().allMatch(JqValue::isTruthy)));
269275
});
270276

271277
register("all", 1, (input, args, env, eval, out) -> {
272-
if (!(input instanceof JqArray arr)) throw new JqException("all requires array input");
278+
JqArray arr = requireArray(input, "all");
273279
for (JqValue elem : arr.arrayValue()) {
274280
var results = eval.eval(args.getFirst(), elem, env);
275281
if (results.stream().noneMatch(JqValue::isTruthy)) {
@@ -281,14 +287,14 @@ private void registerDefaults() {
281287
});
282288

283289
register("flatten", 0, (input, args, env, eval, out) -> {
284-
if (!(input instanceof JqArray arr)) throw new JqException("flatten requires array input");
290+
JqArray arr = requireArray(input, "flatten");
285291
var result = new ArrayList<JqValue>();
286292
flattenArray(arr, result, Integer.MAX_VALUE);
287293
out.accept(JqArray.of(result));
288294
});
289295

290296
register("flatten", 1, (input, args, env, eval, out) -> {
291-
if (!(input instanceof JqArray arr)) throw new JqException("flatten requires array input");
297+
JqArray arr = requireArray(input, "flatten");
292298
eval.eval(args.getFirst(), input, env, depthVal -> {
293299
int depth = (int) depthVal.longValue();
294300
if (depth < 0) throw new JqException("flatten depth must not be negative");
@@ -299,14 +305,14 @@ private void registerDefaults() {
299305
});
300306

301307
register("sort", 0, (input, args, env, eval, out) -> {
302-
if (!(input instanceof JqArray arr)) throw new JqException("sort requires array input");
308+
JqArray arr = requireArray(input, "sort");
303309
var list = new ArrayList<>(arr.arrayValue());
304310
list.sort(JqValue::compareTo);
305311
out.accept(JqArray.of(list));
306312
});
307313

308314
register("sort_by", 1, (input, args, env, eval, out) -> {
309-
if (!(input instanceof JqArray arr)) throw new JqException("sort_by requires array input");
315+
JqArray arr = requireArray(input, "sort_by");
310316
var list = new ArrayList<>(arr.arrayValue());
311317
list.sort((a, b) -> {
312318
var keysA = eval.eval(args.getFirst(), a, env);
@@ -321,7 +327,7 @@ private void registerDefaults() {
321327
});
322328

323329
register("group_by", 1, (input, args, env, eval, out) -> {
324-
if (!(input instanceof JqArray arr)) throw new JqException("group_by requires array input");
330+
JqArray arr = requireArray(input, "group_by");
325331
var groups = new LinkedHashMap<String, List<JqValue>>();
326332
var sortedList = new ArrayList<>(arr.arrayValue());
327333
sortedList.sort((a, b) -> {
@@ -340,7 +346,7 @@ private void registerDefaults() {
340346
});
341347

342348
register("unique", 0, (input, args, env, eval, out) -> {
343-
if (!(input instanceof JqArray arr)) throw new JqException("unique requires array input");
349+
JqArray arr = requireArray(input, "unique");
344350
var seen = new LinkedHashSet<String>();
345351
var result = new ArrayList<JqValue>();
346352
var sorted = new ArrayList<>(arr.arrayValue());
@@ -354,7 +360,7 @@ private void registerDefaults() {
354360
});
355361

356362
register("unique_by", 1, (input, args, env, eval, out) -> {
357-
if (!(input instanceof JqArray arr)) throw new JqException("unique_by requires array input");
363+
JqArray arr = requireArray(input, "unique_by");
358364
var seen = new LinkedHashSet<String>();
359365
var result = new ArrayList<JqValue>();
360366
for (JqValue elem : arr.arrayValue()) {
@@ -425,7 +431,7 @@ private void registerDefaults() {
425431
});
426432

427433
register("transpose", 0, (input, args, env, eval, out) -> {
428-
if (!(input instanceof JqArray arr)) throw new JqException("transpose requires array input");
434+
JqArray arr = requireArray(input, "transpose");
429435
int maxLen = 0;
430436
for (JqValue v : arr.arrayValue()) {
431437
if (v instanceof JqArray a) maxLen = Math.max(maxLen, a.arrayValue().size());
@@ -653,7 +659,7 @@ private void registerDefaults() {
653659
});
654660

655661
register("join", 1, (input, args, env, eval, out) -> {
656-
if (!(input instanceof JqArray arr)) throw new JqException("join requires array input");
662+
JqArray arr = requireArray(input, "join");
657663
eval.eval(args.getFirst(), input, env, sep -> {
658664
String sepStr = sep instanceof JqString s ? s.stringValue() : sep.toJsonString();
659665
var sb = new StringBuilder();
@@ -808,7 +814,7 @@ private void registerDefaults() {
808814
});
809815

810816
register("implode", 0, (input, args, env, eval, out) -> {
811-
if (!(input instanceof JqArray arr)) throw new JqException("implode input must be an array");
817+
JqArray arr = requireArray(input, "implode");
812818
var sb = new StringBuilder();
813819
for (JqValue v : arr.arrayValue()) {
814820
if (!(v instanceof JqNumber n)) {
@@ -1119,7 +1125,7 @@ private void registerDefaults() {
11191125
});
11201126

11211127
register("from_entries", 0, (input, args, env, eval, out) -> {
1122-
if (!(input instanceof JqArray arr)) throw new JqException("from_entries requires array");
1128+
JqArray arr = requireArray(input, "from_entries");
11231129
var map = new LinkedHashMap<String, JqValue>();
11241130
for (JqValue v : arr.arrayValue()) {
11251131
if (v instanceof JqObject obj) {
@@ -1694,7 +1700,7 @@ private void registerDefaults() {
16941700

16951701
// combinations
16961702
register("combinations", 0, (input, args, env, eval, out) -> {
1697-
if (!(input instanceof JqArray arr)) throw new JqException("combinations requires array of arrays");
1703+
JqArray arr = requireArray(input, "combinations");
16981704
var arrays = new ArrayList<List<JqValue>>();
16991705
for (JqValue v : arr.arrayValue()) {
17001706
if (v instanceof JqArray a) {
@@ -1708,7 +1714,7 @@ private void registerDefaults() {
17081714

17091715
register("combinations", 1, (input, args, env, eval, out) -> {
17101716
int n = (int) eval.eval(args.getFirst(), input, env).getFirst().longValue();
1711-
if (!(input instanceof JqArray arr)) throw new JqException("combinations requires array");
1717+
JqArray arr = requireArray(input, "combinations");
17121718
var arrays = new ArrayList<List<JqValue>>();
17131719
for (int i = 0; i < n; i++) {
17141720
arrays.add(arr.arrayValue());
@@ -1730,9 +1736,7 @@ private void registerDefaults() {
17301736

17311737
// bsearch(x) - binary search in sorted array
17321738
register("bsearch", 1, (input, args, env, eval, out) -> {
1733-
if (!(input instanceof JqArray arr)) {
1734-
throw new JqException(input.type().jqName() + " (" + input.toJsonString() + ") cannot be searched from");
1735-
}
1739+
JqArray arr = requireArray(input, "bsearch");
17361740
eval.eval(args.getFirst(), input, env, target -> {
17371741
List<JqValue> list = arr.arrayValue();
17381742
int lo = 0, hi = list.size() - 1;
@@ -1983,9 +1987,7 @@ private void registerDefaults() {
19831987
// JOIN(idx; idx_expr) — for each element $x in input array, output [$x, idx[$x | idx_expr]]
19841988
register("JOIN", 2, (input, args, env, eval, out) -> {
19851989
JqValue idx = eval.eval(args.get(0), input, env).getFirst();
1986-
if (!(input instanceof JqArray arr)) {
1987-
throw new JqTypeError("Cannot iterate over " + input.type().jqName());
1988-
}
1990+
JqArray arr = requireArray(input, "JOIN");
19891991
var results = new java.util.ArrayList<JqValue>();
19901992
for (JqValue elem : arr.arrayValue()) {
19911993
JqValue key = eval.eval(args.get(1), elem, env).getFirst();

jjq-core/src/main/java/io/hyperfoil/tools/jjq/evaluator/Evaluator.java

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -497,15 +497,7 @@ private Integer sliceIndexOrNull(JqValue val, boolean isStart) {
497497
}
498498

499499
private JqValue indexValue(JqValue base, JqValue index) {
500-
return switch (base) {
501-
case JqArray arr when index instanceof JqNumber n -> {
502-
if (n.isNaN()) yield JqNull.NULL;
503-
yield arr.get((int) n.longValue());
504-
}
505-
case JqObject obj when index instanceof JqString s -> obj.get(s.stringValue());
506-
case JqNull ignored -> JqNull.NULL;
507-
default -> throw new JqException("Cannot index " + base.type().jqName() + " with " + index.type().jqName() + " (" + index.toJsonString() + ")");
508-
};
500+
return JqValues.indexValue(base, index);
509501
}
510502

511503
private void buildObject(List<ObjectConstructExpr.ObjectEntry> entries, int idx,

jjq-core/src/main/java/io/hyperfoil/tools/jjq/lexer/Lexer.java

Lines changed: 33 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -245,12 +245,23 @@ private Token lexNumber(int startPos, int startLine, int startCol) {
245245

246246
private Token lexString(int startPos, int startLine, int startCol) {
247247
advance(); // skip opening quote
248+
return lexStringBody(TokenType.STRING, TokenType.STRING_INTERP_START,
249+
"Unterminated string", startPos, startLine, startCol);
250+
}
251+
252+
private Token lexStringAfterInterp(int startPos, int startLine, int startCol) {
253+
return lexStringBody(TokenType.STRING_INTERP_END, TokenType.STRING_INTERP_START,
254+
"Unterminated string interpolation", startPos, startLine, startCol);
255+
}
256+
257+
private Token lexStringBody(TokenType endType, TokenType interpType,
258+
String unterminatedMsg, int startPos, int startLine, int startCol) {
248259
var sb = new StringBuilder();
249260
while (pos < input.length()) {
250261
char c = input.charAt(pos);
251262
if (c == '"') {
252263
advance();
253-
return new Token(TokenType.STRING, sb.toString(), startPos, startLine, startCol);
264+
return new Token(endType, sb.toString(), startPos, startLine, startCol);
254265
}
255266
if (c == '\\') {
256267
advance();
@@ -259,89 +270,42 @@ private Token lexString(int startPos, int startLine, int startCol) {
259270
}
260271
char esc = input.charAt(pos);
261272
if (esc == '(') {
262-
// String interpolation: \( ... )
263273
advance();
264274
interpDepth++;
265275
parenDepth = 0;
266-
return new Token(TokenType.STRING_INTERP_START, sb.toString(), startPos, startLine, startCol);
276+
return new Token(interpType, sb.toString(), startPos, startLine, startCol);
267277
}
268-
sb.append(switch (esc) {
269-
case '"' -> '"';
270-
case '\\' -> '\\';
271-
case '/' -> '/';
272-
case 'b' -> '\b';
273-
case 'f' -> '\f';
274-
case 'n' -> '\n';
275-
case 'r' -> '\r';
276-
case 't' -> '\t';
277-
case 'u' -> {
278-
advance();
279-
if (pos + 3 >= input.length()) {
280-
throw new LexerException("Invalid unicode escape", line, column);
281-
}
282-
String hex = input.substring(pos, pos + 4);
283-
pos += 3; column += 3;
284-
yield (char) Integer.parseInt(hex, 16);
285-
}
286-
default -> throw new LexerException("Invalid escape: \\" + esc, line, column);
287-
});
278+
sb.append(processEscapeChar(esc));
288279
advance();
289280
} else {
290281
sb.append(c);
291282
advance();
292283
}
293284
}
294-
throw new LexerException("Unterminated string", startLine, startCol);
285+
throw new LexerException(unterminatedMsg, startLine, startCol);
295286
}
296287

297-
private Token lexStringAfterInterp(int startPos, int startLine, int startCol) {
298-
// Continue reading string after \(expr)
299-
var sb = new StringBuilder();
300-
while (pos < input.length()) {
301-
char c = input.charAt(pos);
302-
if (c == '"') {
288+
private char processEscapeChar(char esc) {
289+
return switch (esc) {
290+
case '"' -> '"';
291+
case '\\' -> '\\';
292+
case '/' -> '/';
293+
case 'b' -> '\b';
294+
case 'f' -> '\f';
295+
case 'n' -> '\n';
296+
case 'r' -> '\r';
297+
case 't' -> '\t';
298+
case 'u' -> {
303299
advance();
304-
return new Token(TokenType.STRING_INTERP_END, sb.toString(), startPos, startLine, startCol);
305-
}
306-
if (c == '\\') {
307-
advance();
308-
if (pos >= input.length()) {
309-
throw new LexerException("Unterminated string escape", line, column);
310-
}
311-
char esc = input.charAt(pos);
312-
if (esc == '(') {
313-
advance();
314-
interpDepth++;
315-
parenDepth = 0;
316-
return new Token(TokenType.STRING_INTERP_START, sb.toString(), startPos, startLine, startCol);
300+
if (pos + 3 >= input.length()) {
301+
throw new LexerException("Invalid unicode escape", line, column);
317302
}
318-
sb.append(switch (esc) {
319-
case '"' -> '"';
320-
case '\\' -> '\\';
321-
case '/' -> '/';
322-
case 'b' -> '\b';
323-
case 'f' -> '\f';
324-
case 'n' -> '\n';
325-
case 'r' -> '\r';
326-
case 't' -> '\t';
327-
case 'u' -> {
328-
advance();
329-
if (pos + 3 >= input.length()) {
330-
throw new LexerException("Invalid unicode escape", line, column);
331-
}
332-
String hex = input.substring(pos, pos + 4);
333-
pos += 3; column += 3;
334-
yield (char) Integer.parseInt(hex, 16);
335-
}
336-
default -> throw new LexerException("Invalid escape: \\" + esc, line, column);
337-
});
338-
advance();
339-
} else {
340-
sb.append(c);
341-
advance();
303+
String hex = input.substring(pos, pos + 4);
304+
pos += 3; column += 3;
305+
yield (char) Integer.parseInt(hex, 16);
342306
}
343-
}
344-
throw new LexerException("Unterminated string interpolation", startLine, startCol);
307+
default -> throw new LexerException("Invalid escape: \\" + esc, line, column);
308+
};
345309
}
346310

347311
private Token lexVariable(int startPos, int startLine, int startCol) {

jjq-core/src/main/java/io/hyperfoil/tools/jjq/value/JqValues.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,4 +216,21 @@ private static JqNumber parseNumber(String json, int[] pos) {
216216
private static void skipWs(String json, int[] pos) {
217217
while (pos[0] < json.length() && Character.isWhitespace(json.charAt(pos[0]))) pos[0]++;
218218
}
219+
220+
/**
221+
* Index into a JqValue: array[number], object["key"], null[anything] = null.
222+
* Shared by both the bytecode VM and the tree-walk evaluator.
223+
*/
224+
public static JqValue indexValue(JqValue base, JqValue index) {
225+
return switch (base) {
226+
case JqArray arr when index instanceof JqNumber n -> {
227+
if (n.isNaN()) yield JqNull.NULL;
228+
yield arr.get((int) n.longValue());
229+
}
230+
case JqObject obj when index instanceof JqString s -> obj.get(s.stringValue());
231+
case JqNull _ -> JqNull.NULL;
232+
default -> throw new JqTypeError("Cannot index " + base.type().jqName()
233+
+ " with " + index.type().jqName() + " (" + index.toJsonString() + ")");
234+
};
235+
}
219236
}

0 commit comments

Comments
 (0)