@tracecode/harness 0.6.2 → 0.6.5

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.
@@ -16,6 +16,10 @@ const DEFAULT_MAX_STORED_EVENTS = 50_000;
16
16
  const IDLE_TIMEOUT_MS = 90_000;
17
17
  const SCRIPT_METHOD_NAME = '__tracecodeScript';
18
18
 
19
+ if (typeof self.importScripts === 'function') {
20
+ self.importScripts('java-source-augmentations.cjs');
21
+ }
22
+
19
23
  let workerReadyPromise = null;
20
24
  let idleTimer = null;
21
25
  let queue = Promise.resolve();
@@ -30,6 +34,72 @@ function postMessageResponse(message) {
30
34
  self.postMessage(message);
31
35
  }
32
36
 
37
+ function formatWorkerErrorMessage(error) {
38
+ if (error instanceof Error && typeof error.message === 'string' && error.message.length > 0) {
39
+ return error.message;
40
+ }
41
+ if (typeof error === 'string' && error.length > 0) {
42
+ return error;
43
+ }
44
+ if (error && typeof error === 'object') {
45
+ const directKeys = ['message', 'detail', 'reason', 'cause', 'stack', 'name', 'className'];
46
+ for (const key of directKeys) {
47
+ try {
48
+ const value = error[key];
49
+ if (typeof value === 'string' && value.length > 0) {
50
+ return value;
51
+ }
52
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
53
+ return String(value);
54
+ }
55
+ } catch {}
56
+ }
57
+ try {
58
+ const propertyNames = Object.getOwnPropertyNames(error);
59
+ for (const key of propertyNames) {
60
+ const value = error[key];
61
+ if (typeof value === 'string' && value.length > 0) {
62
+ return `${key}: ${value}`;
63
+ }
64
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
65
+ return `${key}: ${String(value)}`;
66
+ }
67
+ }
68
+ } catch {}
69
+ try {
70
+ const tag = Object.prototype.toString.call(error);
71
+ if (tag && tag !== '[object Object]') {
72
+ return tag;
73
+ }
74
+ } catch {}
75
+ try {
76
+ if (typeof error.toString === 'function' && error.toString !== Object.prototype.toString) {
77
+ const value = error.toString();
78
+ if (typeof value === 'string' && value.length > 0 && value !== '[object Object]') {
79
+ return value;
80
+ }
81
+ }
82
+ } catch {}
83
+ }
84
+ try {
85
+ const stringified = String(error);
86
+ if (stringified && stringified !== '[object Object]') {
87
+ return stringified;
88
+ }
89
+ } catch {}
90
+ try {
91
+ const json = JSON.stringify(error);
92
+ if (json && json !== '{}') {
93
+ return json;
94
+ }
95
+ } catch {}
96
+ return 'Unknown Java worker error';
97
+ }
98
+
99
+ function makeWorkerStageError(stage, error) {
100
+ return new Error(`Java worker ${stage} failed: ${formatWorkerErrorMessage(error)}`);
101
+ }
102
+
33
103
  function resetIdleTimer() {
34
104
  idleGeneration += 1;
35
105
  const generation = idleGeneration;
@@ -348,6 +418,414 @@ function indentBlock(source, spaces = 2) {
348
418
  .join('\n');
349
419
  }
350
420
 
421
+ function isJavaIdentifierPart(ch) {
422
+ return /[A-Za-z0-9_$]/.test(ch);
423
+ }
424
+
425
+ function scanJavaCode(source, start, end, onNormalChar) {
426
+ let state = 'normal';
427
+ for (let index = start; index < end; index += 1) {
428
+ const ch = source[index];
429
+ const next = index + 1 < end ? source[index + 1] : '';
430
+
431
+ if (state === 'line-comment') {
432
+ if (ch === '\n') state = 'normal';
433
+ continue;
434
+ }
435
+ if (state === 'block-comment') {
436
+ if (ch === '*' && next === '/') {
437
+ state = 'normal';
438
+ index += 1;
439
+ }
440
+ continue;
441
+ }
442
+ if (state === 'string') {
443
+ if (ch === '\\') {
444
+ index += 1;
445
+ continue;
446
+ }
447
+ if (ch === '"') state = 'normal';
448
+ continue;
449
+ }
450
+ if (state === 'char') {
451
+ if (ch === '\\') {
452
+ index += 1;
453
+ continue;
454
+ }
455
+ if (ch === "'") state = 'normal';
456
+ continue;
457
+ }
458
+ if (ch === '/' && next === '/') {
459
+ state = 'line-comment';
460
+ index += 1;
461
+ continue;
462
+ }
463
+ if (ch === '/' && next === '*') {
464
+ state = 'block-comment';
465
+ index += 1;
466
+ continue;
467
+ }
468
+ if (ch === '"') {
469
+ state = 'string';
470
+ continue;
471
+ }
472
+ if (ch === "'") {
473
+ state = 'char';
474
+ continue;
475
+ }
476
+
477
+ const result = onNormalChar(index, ch);
478
+ if (result === false) return index;
479
+ if (typeof result === 'number') index = result;
480
+ }
481
+ return end;
482
+ }
483
+
484
+ function findMatchingParen(source, openIndex) {
485
+ let depth = 0;
486
+ let closeIndex = -1;
487
+ scanJavaCode(source, openIndex, source.length, (index, ch) => {
488
+ if (ch === '(') depth += 1;
489
+ if (ch === ')') {
490
+ depth -= 1;
491
+ if (depth === 0) {
492
+ closeIndex = index;
493
+ return false;
494
+ }
495
+ }
496
+ return undefined;
497
+ });
498
+ return closeIndex;
499
+ }
500
+
501
+ function findSingleStatementEnd(source, bodyStart) {
502
+ let parenDepth = 0;
503
+ let bracketDepth = 0;
504
+ let braceDepth = 0;
505
+ let statementEnd = -1;
506
+ scanJavaCode(source, bodyStart, source.length, (index, ch) => {
507
+ if (ch === '(') parenDepth += 1;
508
+ if (ch === ')') parenDepth = Math.max(0, parenDepth - 1);
509
+ if (ch === '[') bracketDepth += 1;
510
+ if (ch === ']') bracketDepth = Math.max(0, bracketDepth - 1);
511
+ if (ch === '{') braceDepth += 1;
512
+ if (ch === '}') {
513
+ if (braceDepth === 0) return false;
514
+ braceDepth -= 1;
515
+ }
516
+ if (ch === ';' && parenDepth === 0 && bracketDepth === 0 && braceDepth === 0) {
517
+ statementEnd = index;
518
+ return false;
519
+ }
520
+ return undefined;
521
+ });
522
+ return statementEnd;
523
+ }
524
+
525
+ function startsWithJavaKeyword(source, index, keyword) {
526
+ if (!source.startsWith(keyword, index)) return false;
527
+ const after = source[index + keyword.length] ?? '';
528
+ return !after || !isJavaIdentifierPart(after);
529
+ }
530
+
531
+ function wrapSingleStatementLoopBodies(source) {
532
+ const inserts = [];
533
+ scanJavaCode(source, 0, source.length, (index) => {
534
+ const keyword = source.startsWith('for', index)
535
+ ? 'for'
536
+ : source.startsWith('while', index)
537
+ ? 'while'
538
+ : null;
539
+ if (!keyword) return undefined;
540
+
541
+ const before = index > 0 ? source[index - 1] : '';
542
+ const after = source[index + keyword.length] ?? '';
543
+ if ((before && isJavaIdentifierPart(before)) || (after && isJavaIdentifierPart(after))) {
544
+ return undefined;
545
+ }
546
+
547
+ let cursor = index + keyword.length;
548
+ while (/\s/.test(source[cursor] ?? '')) cursor += 1;
549
+ if (source[cursor] !== '(') return undefined;
550
+
551
+ const closeParen = findMatchingParen(source, cursor);
552
+ if (closeParen < 0) return undefined;
553
+
554
+ let bodyStart = closeParen + 1;
555
+ while (/\s/.test(source[bodyStart] ?? '')) bodyStart += 1;
556
+ const bodyChar = source[bodyStart];
557
+ if (!bodyChar || bodyChar === '{' || bodyChar === ';') return closeParen;
558
+ if (
559
+ startsWithJavaKeyword(source, bodyStart, 'if') ||
560
+ startsWithJavaKeyword(source, bodyStart, 'switch') ||
561
+ startsWithJavaKeyword(source, bodyStart, 'synchronized') ||
562
+ startsWithJavaKeyword(source, bodyStart, 'try')
563
+ ) {
564
+ return closeParen;
565
+ }
566
+
567
+ const bodyEnd = findSingleStatementEnd(source, bodyStart);
568
+ if (bodyEnd < 0) return closeParen;
569
+
570
+ inserts.push({ index: bodyStart, text: '{ ' });
571
+ inserts.push({ index: bodyEnd + 1, text: ' }' });
572
+ return bodyEnd;
573
+ });
574
+
575
+ if (inserts.length === 0) return source;
576
+
577
+ const insertsByIndex = new Map();
578
+ for (const insert of inserts) {
579
+ insertsByIndex.set(insert.index, `${insertsByIndex.get(insert.index) ?? ''}${insert.text}`);
580
+ }
581
+
582
+ let output = '';
583
+ for (let index = 0; index <= source.length; index += 1) {
584
+ output += insertsByIndex.get(index) ?? '';
585
+ if (index < source.length) output += source[index];
586
+ }
587
+ return output;
588
+ }
589
+
590
+ function splitTopLevelJavaList(value) {
591
+ const parts = [];
592
+ let start = 0;
593
+ let parenDepth = 0;
594
+ let bracketDepth = 0;
595
+ let braceDepth = 0;
596
+ let angleDepth = 0;
597
+ let quote = null;
598
+
599
+ for (let index = 0; index < value.length; index += 1) {
600
+ const ch = value[index];
601
+ const previous = index > 0 ? value[index - 1] : '';
602
+ if (quote) {
603
+ if (ch === quote && previous !== '\\') quote = null;
604
+ continue;
605
+ }
606
+ if (ch === '"' || ch === "'") {
607
+ quote = ch;
608
+ continue;
609
+ }
610
+ if (ch === '(') parenDepth += 1;
611
+ if (ch === ')') parenDepth = Math.max(0, parenDepth - 1);
612
+ if (ch === '[') bracketDepth += 1;
613
+ if (ch === ']') bracketDepth = Math.max(0, bracketDepth - 1);
614
+ if (ch === '{') braceDepth += 1;
615
+ if (ch === '}') braceDepth = Math.max(0, braceDepth - 1);
616
+ if (ch === '<') angleDepth += 1;
617
+ if (ch === '>') angleDepth = Math.max(0, angleDepth - 1);
618
+ if (
619
+ ch === ',' &&
620
+ parenDepth === 0 &&
621
+ bracketDepth === 0 &&
622
+ braceDepth === 0 &&
623
+ angleDepth === 0
624
+ ) {
625
+ parts.push(value.slice(start, index).trim());
626
+ start = index + 1;
627
+ }
628
+ }
629
+
630
+ const tail = value.slice(start).trim();
631
+ if (tail.length > 0) {
632
+ parts.push(tail);
633
+ }
634
+ return parts;
635
+ }
636
+
637
+ function parseJavaParameters(parametersSource) {
638
+ return splitTopLevelJavaList(parametersSource)
639
+ .map((parameter) => parameter.replace(/@\w+(?:\([^)]*\))?/g, '').replace(/\bfinal\b/g, '').trim())
640
+ .map((parameter) => {
641
+ const match = parameter.match(/([A-Za-z_][A-Za-z0-9_]*)\s*(?:\.\.\.)?$/);
642
+ const name = match?.[1] ?? '';
643
+ return {
644
+ name,
645
+ isArray: parameter.includes('[]') || parameter.includes('...'),
646
+ };
647
+ })
648
+ .filter((parameter) => parameter.name.length > 0);
649
+ }
650
+
651
+ function parseJavaParameterNames(parametersSource) {
652
+ return parseJavaParameters(parametersSource).map((parameter) => parameter.name);
653
+ }
654
+
655
+ function collectJavaArrayDeclarations(line) {
656
+ const names = [];
657
+ const declarationPattern =
658
+ /\b(?:boolean|byte|char|short|int|long|float|double|String|[A-Za-z_][A-Za-z0-9_<>.?]*)\s*(?:\[\s*\])+\s+([A-Za-z_][A-Za-z0-9_]*)\b/g;
659
+ for (const match of line.matchAll(declarationPattern)) {
660
+ if (match[1]) names.push(match[1]);
661
+ }
662
+ return names;
663
+ }
664
+
665
+ function escapeRegExp(value) {
666
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
667
+ }
668
+
669
+ function augmentTraceCallArgumentSnapshots(source) {
670
+ const lines = source.split('\n');
671
+ const methodStack = [];
672
+ const methodStartPattern =
673
+ /^(\s*)(?:(?:public|private|protected|static|final|synchronized)\s+)*(?:[A-Za-z_][A-Za-z0-9_<>\[\], ?]*\s+)+([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)\s*\{\s*$/;
674
+
675
+ return lines.map((line) => {
676
+ const methodMatch = line.match(methodStartPattern);
677
+ if (methodMatch) {
678
+ methodStack.push({
679
+ name: methodMatch[2],
680
+ params: parseJavaParameterNames(methodMatch[3] ?? ''),
681
+ depth: 1,
682
+ patchedCall: false,
683
+ });
684
+ return line;
685
+ }
686
+
687
+ const currentMethod = methodStack[methodStack.length - 1];
688
+ let nextLine = line;
689
+ if (currentMethod && !currentMethod.patchedCall && currentMethod.params.length > 0) {
690
+ const callPattern = new RegExp(
691
+ `^(\\s*)TraceHooks\\.emit\\((\"line=\\d+ call ${escapeRegExp(currentMethod.name)}\").*\\);\\s*$`
692
+ );
693
+ const callMatch = line.match(callPattern);
694
+ if (callMatch) {
695
+ const serializedArgs = currentMethod.params
696
+ .map((paramName) => ` + " ${paramName}=" + TraceHooks.serializeResult(${paramName})`)
697
+ .join('');
698
+ nextLine = `${callMatch[1]}TraceHooks.emit(${callMatch[2]}${serializedArgs});`;
699
+ currentMethod.patchedCall = true;
700
+ }
701
+ }
702
+
703
+ if (currentMethod) {
704
+ currentMethod.depth += braceDelta(nextLine);
705
+ while (methodStack.length > 0 && methodStack[methodStack.length - 1].depth <= 0) {
706
+ methodStack.pop();
707
+ }
708
+ }
709
+
710
+ return nextLine;
711
+ }).join('\n');
712
+ }
713
+
714
+ function augmentArrayLengthReads(source) {
715
+ const lines = source.split('\n');
716
+ const methodStack = [];
717
+ const methodStartPattern =
718
+ /^(\s*)(?:(?:public|private|protected|static|final|synchronized)\s+)*(?:[A-Za-z_][A-Za-z0-9_<>\[\], ?]*\s+)+([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)\s*\{\s*$/;
719
+
720
+ return lines.map((line) => {
721
+ const methodMatch = line.match(methodStartPattern);
722
+ if (methodMatch) {
723
+ const parameters = parseJavaParameters(methodMatch[3] ?? '');
724
+ methodStack.push({
725
+ depth: 1,
726
+ currentTraceLine: null,
727
+ hasTraceEmit: false,
728
+ arrayNames: new Set(parameters.filter((parameter) => parameter.isArray).map((parameter) => parameter.name)),
729
+ });
730
+ return line;
731
+ }
732
+
733
+ const currentMethod = methodStack[methodStack.length - 1];
734
+ let nextLine = line;
735
+
736
+ if (currentMethod) {
737
+ for (const name of collectJavaArrayDeclarations(line)) {
738
+ currentMethod.arrayNames.add(name);
739
+ }
740
+
741
+ const traceLineMatch = line.match(/TraceHooks\.emit\("line=(\d+)(?:\s|")/);
742
+ if (traceLineMatch) {
743
+ currentMethod.currentTraceLine = Number.parseInt(traceLineMatch[1], 10);
744
+ currentMethod.hasTraceEmit = true;
745
+ }
746
+
747
+ if (
748
+ currentMethod.hasTraceEmit &&
749
+ currentMethod.currentTraceLine !== null &&
750
+ !line.includes('TraceHooks.readArrayLengthAtLine')
751
+ ) {
752
+ for (const arrayName of currentMethod.arrayNames) {
753
+ const lengthPattern = new RegExp(`\\b${escapeRegExp(arrayName)}\\.length\\b`, 'g');
754
+ nextLine = nextLine.replace(
755
+ lengthPattern,
756
+ `TraceHooks.readArrayLengthAtLine(${currentMethod.currentTraceLine}, "${arrayName}", ${arrayName})`
757
+ );
758
+ }
759
+ }
760
+
761
+ currentMethod.depth += braceDelta(nextLine);
762
+ while (methodStack.length > 0 && methodStack[methodStack.length - 1].depth <= 0) {
763
+ methodStack.pop();
764
+ }
765
+ }
766
+
767
+ return nextLine;
768
+ }).join('\n');
769
+ }
770
+
771
+ function augmentTraceReturnValueSnapshots(source) {
772
+ const lines = source.split('\n');
773
+ const output = [];
774
+ const methodStack = [];
775
+ let returnValueIndex = 0;
776
+ const methodStartPattern =
777
+ /^(\s*)(?:(?:public|private|protected|static|final|synchronized)\s+)*([A-Za-z_][A-Za-z0-9_<>\[\], ?]*(?:\[\])?)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)\s*\{\s*$/;
778
+
779
+ for (let index = 0; index < lines.length; index += 1) {
780
+ const line = lines[index];
781
+ const methodMatch = line.match(methodStartPattern);
782
+ if (methodMatch) {
783
+ methodStack.push({
784
+ name: methodMatch[3],
785
+ returnType: (methodMatch[2] ?? '').trim(),
786
+ depth: 1,
787
+ });
788
+ output.push(line);
789
+ continue;
790
+ }
791
+
792
+ const currentMethod = methodStack[methodStack.length - 1];
793
+ if (currentMethod && currentMethod.returnType !== 'void') {
794
+ const returnEmitMatch = line.match(
795
+ /^(\s*)TraceHooks\.emit\("line=(\d+) return ([A-Za-z_][A-Za-z0-9_]*)"\);\s*$/
796
+ );
797
+ const nextLine = lines[index + 1] ?? '';
798
+ const returnMatch = nextLine.match(/^(\s*)return\s+(.+);\s*$/);
799
+ if (returnEmitMatch && returnMatch && returnEmitMatch[3] === currentMethod.name) {
800
+ const tempName = `__tracecodeReturnValue${returnValueIndex++}`;
801
+ const indent = returnEmitMatch[1] ?? returnMatch[1] ?? '';
802
+ const returnExpression = returnMatch[2].trim();
803
+ output.push(`${indent}${currentMethod.returnType} ${tempName} = ${returnExpression};`);
804
+ output.push(
805
+ `${indent}TraceHooks.emit("line=${returnEmitMatch[2]} return ${currentMethod.name} value=" + TraceHooks.serializeResult(${tempName}));`
806
+ );
807
+ output.push(`${returnMatch[1] ?? indent}return ${tempName};`);
808
+ currentMethod.depth += braceDelta(line) + braceDelta(nextLine);
809
+ index += 1;
810
+ while (methodStack.length > 0 && methodStack[methodStack.length - 1].depth <= 0) {
811
+ methodStack.pop();
812
+ }
813
+ continue;
814
+ }
815
+ }
816
+
817
+ output.push(line);
818
+ if (currentMethod) {
819
+ currentMethod.depth += braceDelta(line);
820
+ while (methodStack.length > 0 && methodStack[methodStack.length - 1].depth <= 0) {
821
+ methodStack.pop();
822
+ }
823
+ }
824
+ }
825
+
826
+ return output.join('\n');
827
+ }
828
+
351
829
  function splitImportPrelude(source) {
352
830
  const lines = source.split('\n');
353
831
  const importLines = [];
@@ -367,13 +845,99 @@ function splitImportPrelude(source) {
367
845
  return { importLines, bodyLines };
368
846
  }
369
847
 
848
+ function splitImportPreludeEntries(source) {
849
+ const lines = source.split('\n');
850
+ const importEntries = [];
851
+ const bodyEntries = [];
852
+ let inImportPrelude = true;
853
+
854
+ for (let index = 0; index < lines.length; index += 1) {
855
+ const line = lines[index];
856
+ const entry = { line, sourceLine: index + 1 };
857
+ const trimmed = line.trim();
858
+ if (inImportPrelude && (trimmed === '' || trimmed.startsWith('import '))) {
859
+ importEntries.push(entry);
860
+ continue;
861
+ }
862
+ inImportPrelude = false;
863
+ bodyEntries.push(entry);
864
+ }
865
+
866
+ return { importEntries, bodyEntries };
867
+ }
868
+
869
+ function trimBlankEntries(entries) {
870
+ let start = 0;
871
+ let end = entries.length;
872
+ while (start < end && entries[start].line.trim().length === 0) start += 1;
873
+ while (end > start && entries[end - 1].line.trim().length === 0) end -= 1;
874
+ return entries.slice(start, end);
875
+ }
876
+
877
+ function isTopLevelMethodStart(line) {
878
+ const trimmed = line.trim();
879
+ return /^(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:[\w<>\[\], ?]+\s+)+[A-Za-z_][A-Za-z0-9_]*\s*\([^;]*\)\s*\{/.test(trimmed);
880
+ }
881
+
882
+ function braceDelta(line) {
883
+ let delta = 0;
884
+ let quote = null;
885
+ for (let index = 0; index < line.length; index += 1) {
886
+ const ch = line[index];
887
+ const prev = index > 0 ? line[index - 1] : '';
888
+ if (quote) {
889
+ if (ch === quote && prev !== '\\') quote = null;
890
+ continue;
891
+ }
892
+ if (ch === '"' || ch === "'") {
893
+ quote = ch;
894
+ continue;
895
+ }
896
+ if (ch === '{') delta += 1;
897
+ if (ch === '}') delta -= 1;
898
+ }
899
+ return delta;
900
+ }
901
+
902
+ function splitScriptMembersAndStatements(lines) {
903
+ const memberLines = [];
904
+ const statementLines = [];
905
+ let statementDepth = 0;
906
+ for (let index = 0; index < lines.length; index += 1) {
907
+ const entry = lines[index];
908
+ const line = typeof entry === 'string' ? entry : entry.line;
909
+ if (statementDepth !== 0 || !isTopLevelMethodStart(line)) {
910
+ statementLines.push(entry);
911
+ statementDepth += braceDelta(line);
912
+ if (statementDepth < 0) statementDepth = 0;
913
+ continue;
914
+ }
915
+
916
+ let depth = 0;
917
+ do {
918
+ const current = lines[index] ?? '';
919
+ const currentLine = typeof current === 'string' ? current : current.line;
920
+ memberLines.push(current);
921
+ depth += braceDelta(currentLine);
922
+ index += 1;
923
+ } while (index < lines.length && depth > 0);
924
+ index -= 1;
925
+ }
926
+ return {
927
+ memberLines,
928
+ statementLines,
929
+ memberEntries: memberLines,
930
+ statementEntries: statementLines,
931
+ };
932
+ }
933
+
370
934
  function normalizeFunctionSource(source) {
371
935
  if (/\bpackage\s+[A-Za-z_][A-Za-z0-9_.]*\s*;/.test(source)) {
372
936
  throw new Error('Java function style should not declare a package; the harness manages package isolation.');
373
937
  }
374
938
 
375
939
  if (/\bclass\s+Solution\b/.test(source)) {
376
- return source;
940
+ return wrapSingleStatementLoopBodies(source);
377
941
  }
378
942
 
379
943
  if (/\b(class|interface|enum|record)\b/.test(source)) {
@@ -390,23 +954,62 @@ function normalizeFunctionSource(source) {
390
954
  throw new Error('Java function style requires a method fragment.');
391
955
  }
392
956
 
393
- return `${importBlock ? `${importBlock}\n\n` : ''}class Solution {\n${indentBlock(body, 2)}\n}`;
957
+ return wrapSingleStatementLoopBodies(`${importBlock ? `${importBlock}\n\n` : ''}class Solution {\n${indentBlock(body, 2)}\n}`);
394
958
  }
395
959
 
396
960
  function normalizeScriptSource(source) {
961
+ return normalizeScriptSourceWithLineMap(source).code;
962
+ }
963
+
964
+ function normalizeScriptSourceWithLineMap(source) {
397
965
  if (/\bpackage\s+[A-Za-z_][A-Za-z0-9_.]*\s*;/.test(source)) {
398
966
  throw new Error('Java script style should not declare a package; the harness manages package isolation.');
399
967
  }
400
968
 
401
- const { importLines, bodyLines } = splitImportPrelude(source);
402
- const body = bodyLines.join('\n');
403
- if (!body.trim()) {
969
+ const { importEntries, bodyEntries } = splitImportPreludeEntries(source);
970
+ const { memberEntries, statementEntries } = splitScriptMembersAndStatements(bodyEntries);
971
+ const trimmedMemberEntries = trimBlankEntries(memberEntries);
972
+ const trimmedStatementEntries = trimBlankEntries(statementEntries);
973
+ if (trimmedStatementEntries.length === 0) {
404
974
  throw new Error('Java script style requires executable statements and a result assignment.');
405
975
  }
406
976
 
407
- const prelude = importLines.length > 0 ? `${importLines.join('\n')}\n` : '';
408
- const wrapperPrefix = `class Solution { Object ${SCRIPT_METHOD_NAME}() { Object result = null; `;
409
- return `${prelude}${wrapperPrefix}${body}\nreturn result;\n} }`;
977
+ const outputLines = [];
978
+ const lineMap = {};
979
+ const firstStatementLine = trimmedStatementEntries[0]?.sourceLine;
980
+ const lastStatementLine = trimmedStatementEntries[trimmedStatementEntries.length - 1]?.sourceLine;
981
+ const declaresResult = trimmedStatementEntries.some((entry) =>
982
+ /^(?:final\s+)?[\w<>\[\], ?]+\s+result\s*(?:=|;)/.test(entry.line.trim())
983
+ );
984
+ const pushLine = (line, sourceLine) => {
985
+ outputLines.push(line);
986
+ if (Number.isFinite(sourceLine) && sourceLine > 0) {
987
+ lineMap[outputLines.length] = sourceLine;
988
+ }
989
+ };
990
+
991
+ for (const entry of importEntries) {
992
+ pushLine(entry.line, entry.sourceLine);
993
+ }
994
+ pushLine('class Solution {');
995
+ for (const entry of trimmedMemberEntries) {
996
+ pushLine(entry.line.trim().length === 0 ? '' : ` ${entry.line}`, entry.sourceLine);
997
+ }
998
+ pushLine(` Object ${SCRIPT_METHOD_NAME}() {`, firstStatementLine);
999
+ if (!declaresResult) {
1000
+ pushLine(' Object result = null;', firstStatementLine);
1001
+ }
1002
+ for (const entry of trimmedStatementEntries) {
1003
+ pushLine(entry.line.trim().length === 0 ? '' : ` ${entry.line}`, entry.sourceLine);
1004
+ }
1005
+ pushLine(' return result;', lastStatementLine);
1006
+ pushLine(' }');
1007
+ pushLine('}');
1008
+
1009
+ return {
1010
+ code: wrapSingleStatementLoopBodies(outputLines.join('\n')),
1011
+ lineMap,
1012
+ };
410
1013
  }
411
1014
 
412
1015
  function normalizeJavaRequest(payload) {
@@ -415,12 +1018,14 @@ function normalizeJavaRequest(payload) {
415
1018
  throw new Error('Java script-mode execution only supports executionStyle="function".');
416
1019
  }
417
1020
 
1021
+ const normalizedScript = normalizeScriptSourceWithLineMap(payload.code);
418
1022
  return {
419
1023
  ...payload,
420
- code: normalizeScriptSource(payload.code),
1024
+ code: normalizedScript.code,
421
1025
  executionStyle: 'solution-method',
422
1026
  functionName: SCRIPT_METHOD_NAME,
423
1027
  sourceText: payload.code,
1028
+ sourceLineMap: normalizedScript.lineMap,
424
1029
  userCodeLineCount: payload.code.split(/\r?\n/).length,
425
1030
  scriptMode: true,
426
1031
  };
@@ -595,12 +1200,21 @@ async function rewriteSource(payload, requestId) {
595
1200
  );
596
1201
  }
597
1202
 
598
- function normalizeScriptTraceEvents(events, scriptMode, userCodeLineCount) {
1203
+ function normalizeScriptTraceEvents(events, scriptMode, userCodeLineCount, sourceLineMap) {
599
1204
  if (!scriptMode || !Array.isArray(events)) return events;
600
1205
  return events.map((event) => {
601
- const normalizedEvent = String(event)
1206
+ let normalizedEvent = String(event)
602
1207
  .replace(new RegExp(`\\bcall\\s+${SCRIPT_METHOD_NAME}\\b`, 'g'), 'call <module>')
603
1208
  .replace(new RegExp(`\\breturn\\s+${SCRIPT_METHOD_NAME}\\b`, 'g'), 'return <module>');
1209
+
1210
+ const lineMatch = normalizedEvent.match(/^line=(\d+)(.*)$/);
1211
+ if (lineMatch && sourceLineMap && Object.prototype.hasOwnProperty.call(sourceLineMap, lineMatch[1])) {
1212
+ const mappedLine = Number(sourceLineMap[lineMatch[1]]);
1213
+ if (Number.isFinite(mappedLine) && mappedLine > 0) {
1214
+ normalizedEvent = `line=${mappedLine}${lineMatch[2] ?? ''}`;
1215
+ }
1216
+ }
1217
+
604
1218
  const match = normalizedEvent.match(/^line=(\d+)\s+return\s+<module>$/);
605
1219
  if (!match || !Number.isFinite(userCodeLineCount) || userCodeLineCount <= 0) {
606
1220
  return normalizedEvent;
@@ -611,6 +1225,56 @@ function normalizeScriptTraceEvents(events, scriptMode, userCodeLineCount) {
611
1225
  });
612
1226
  }
613
1227
 
1228
+ function parseTraceLineNumber(event) {
1229
+ const match = String(event).match(/^line=(\d+)(?:\s|$)/);
1230
+ if (!match) return null;
1231
+ const line = Number.parseInt(match[1], 10);
1232
+ return Number.isFinite(line) && line > 0 ? line : null;
1233
+ }
1234
+
1235
+ function isBareTraceLineEvent(event) {
1236
+ return /^line=\d+$/.test(String(event));
1237
+ }
1238
+
1239
+ function buildLoopBodyLineMap(sourceText) {
1240
+ if (typeof sourceText !== 'string' || sourceText.length === 0) return null;
1241
+ const lines = sourceText.split(/\r?\n/);
1242
+ const loopBodyLineToHeaderLine = new Map();
1243
+
1244
+ for (let index = 0; index < lines.length; index += 1) {
1245
+ const line = lines[index];
1246
+ if (!/\b(?:for|while)\s*\(/.test(line) || !line.includes('{')) continue;
1247
+
1248
+ for (let bodyIndex = index + 1; bodyIndex < lines.length; bodyIndex += 1) {
1249
+ const trimmed = lines[bodyIndex].trim();
1250
+ if (trimmed.length === 0) continue;
1251
+ if (trimmed.startsWith('}')) break;
1252
+ loopBodyLineToHeaderLine.set(bodyIndex + 1, index + 1);
1253
+ break;
1254
+ }
1255
+ }
1256
+
1257
+ return loopBodyLineToHeaderLine.size > 0 ? loopBodyLineToHeaderLine : null;
1258
+ }
1259
+
1260
+ function expandLoopHeaderTraceEvents(events, sourceText) {
1261
+ if (!Array.isArray(events) || events.length === 0) return events;
1262
+ const loopBodyLineToHeaderLine = buildLoopBodyLineMap(sourceText);
1263
+ if (!loopBodyLineToHeaderLine) return events;
1264
+
1265
+ const expanded = [];
1266
+ for (const event of events) {
1267
+ const line = parseTraceLineNumber(event);
1268
+ const headerLine = line === null ? undefined : loopBodyLineToHeaderLine.get(line);
1269
+ const previousLine = expanded.length > 0 ? parseTraceLineNumber(expanded[expanded.length - 1]) : null;
1270
+ if (headerLine !== undefined && isBareTraceLineEvent(event) && previousLine !== headerLine) {
1271
+ expanded.push(`line=${headerLine}`);
1272
+ }
1273
+ expanded.push(event);
1274
+ }
1275
+ return expanded;
1276
+ }
1277
+
614
1278
  async function runJavaRequest(payload, requestId) {
615
1279
  assertSupportedExecutionStyle(payload.executionStyle);
616
1280
  if (typeof payload.code !== 'string') {
@@ -623,8 +1287,23 @@ async function runJavaRequest(payload, requestId) {
623
1287
 
624
1288
  const totalStart = performance.now();
625
1289
  const rewriteStart = performance.now();
626
- const normalizedPayload = normalizeJavaRequest(payload);
627
- const rewrittenSource = await rewriteSource(normalizedPayload, requestId);
1290
+ let normalizedPayload;
1291
+ try {
1292
+ normalizedPayload = normalizeJavaRequest(payload);
1293
+ } catch (error) {
1294
+ throw makeWorkerStageError('request normalization', error);
1295
+ }
1296
+
1297
+ let rewrittenSource;
1298
+ try {
1299
+ rewrittenSource = await rewriteSource(normalizedPayload, requestId);
1300
+ rewrittenSource = augmentTraceCallArgumentSnapshots(rewrittenSource);
1301
+ rewrittenSource = augmentArrayLengthReads(rewrittenSource);
1302
+ rewrittenSource = self.TraceCodeJavaSourceAugmentations.augmentJavaCollectionOperations(rewrittenSource);
1303
+ rewrittenSource = augmentTraceReturnValueSnapshots(rewrittenSource);
1304
+ } catch (error) {
1305
+ throw makeWorkerStageError('source rewrite', error);
1306
+ }
628
1307
  const rewriteEnd = performance.now();
629
1308
 
630
1309
  const exportsClassName = buildExportsClassName(requestId);
@@ -632,21 +1311,41 @@ async function runJavaRequest(payload, requestId) {
632
1311
  const sourcePath = `/str/${exportsClassName}.java`;
633
1312
  const classesDir = `/files/java-worker/${requestId}/classes`;
634
1313
 
635
- await self.cheerpOSAddStringFile(sourcePath, rewrittenSource);
1314
+ try {
1315
+ await self.cheerpOSAddStringFile(sourcePath, rewrittenSource);
1316
+ } catch (error) {
1317
+ throw makeWorkerStageError('source file write', error);
1318
+ }
1319
+
1320
+ let compileLibraryClass;
1321
+ try {
1322
+ compileLibraryClass = await getCompileLibraryClass();
1323
+ } catch (error) {
1324
+ throw makeWorkerStageError('compiler bridge load', error);
1325
+ }
636
1326
 
637
- const compileLibraryClass = await getCompileLibraryClass();
638
1327
  const libraryCallStart = performance.now();
639
- const reportText = await compileLibraryClass.compileAndTrace(
640
- sourcePath,
641
- classesDir,
642
- `${packageName}.${exportsClassName}`,
643
- HELPER_JAR_PATH,
644
- DEFAULT_COMPILER_DEBUG_PROFILE,
645
- String(resolveMaxStoredEvents(payload.options))
646
- );
1328
+ let reportText;
1329
+ try {
1330
+ reportText = await compileLibraryClass.compileAndTrace(
1331
+ sourcePath,
1332
+ classesDir,
1333
+ `${packageName}.${exportsClassName}`,
1334
+ HELPER_JAR_PATH,
1335
+ DEFAULT_COMPILER_DEBUG_PROFILE,
1336
+ String(resolveMaxStoredEvents(payload.options))
1337
+ );
1338
+ } catch (error) {
1339
+ throw makeWorkerStageError('compile and trace', error);
1340
+ }
647
1341
  const libraryCallEnd = performance.now();
648
1342
 
649
- const report = JSON.parse(reportText);
1343
+ let report;
1344
+ try {
1345
+ report = JSON.parse(reportText);
1346
+ } catch (error) {
1347
+ throw makeWorkerStageError('trace report parse', error);
1348
+ }
650
1349
  const totalEnd = performance.now();
651
1350
  const consoleOutput = [report.compilerStdout, report.compilerStderr].filter(
652
1351
  (entry) => typeof entry === 'string' && entry.trim().length > 0
@@ -655,10 +1354,14 @@ async function runJavaRequest(payload, requestId) {
655
1354
  if (report.success !== true) {
656
1355
  return {
657
1356
  success: false,
658
- events: normalizeScriptTraceEvents(
659
- Array.isArray(report.events) ? report.events : [],
660
- normalizedPayload.scriptMode,
661
- normalizedPayload.userCodeLineCount
1357
+ events: expandLoopHeaderTraceEvents(
1358
+ normalizeScriptTraceEvents(
1359
+ Array.isArray(report.events) ? report.events : [],
1360
+ normalizedPayload.scriptMode,
1361
+ normalizedPayload.userCodeLineCount,
1362
+ normalizedPayload.sourceLineMap
1363
+ ),
1364
+ normalizedPayload.sourceText
662
1365
  ),
663
1366
  ...(normalizedPayload.sourceText ? { sourceText: normalizedPayload.sourceText } : {}),
664
1367
  executionTimeMs: totalEnd - totalStart,
@@ -686,10 +1389,14 @@ async function runJavaRequest(payload, requestId) {
686
1389
  return {
687
1390
  success: true,
688
1391
  output: report.output ? JSON.parse(report.output) : undefined,
689
- events: normalizeScriptTraceEvents(
690
- Array.isArray(report.events) ? report.events : [],
691
- normalizedPayload.scriptMode,
692
- normalizedPayload.userCodeLineCount
1392
+ events: expandLoopHeaderTraceEvents(
1393
+ normalizeScriptTraceEvents(
1394
+ Array.isArray(report.events) ? report.events : [],
1395
+ normalizedPayload.scriptMode,
1396
+ normalizedPayload.userCodeLineCount,
1397
+ normalizedPayload.sourceLineMap
1398
+ ),
1399
+ normalizedPayload.sourceText
693
1400
  ),
694
1401
  ...(normalizedPayload.sourceText ? { sourceText: normalizedPayload.sourceText } : {}),
695
1402
  executionTimeMs: totalEnd - totalStart,
@@ -747,7 +1454,7 @@ self.onmessage = (event) => {
747
1454
  postMessageResponse({
748
1455
  id: message.id,
749
1456
  type: 'error',
750
- payload: { error: error instanceof Error ? error.message : String(error) },
1457
+ payload: { error: formatWorkerErrorMessage(error) },
751
1458
  });
752
1459
  }
753
1460
  });
@@ -772,7 +1479,7 @@ self.onmessage = (event) => {
772
1479
  postMessageResponse({
773
1480
  id: message.id,
774
1481
  type: 'error',
775
- payload: { error: error instanceof Error ? error.message : String(error) },
1482
+ payload: { error: formatWorkerErrorMessage(error) },
776
1483
  });
777
1484
  }
778
1485
  });