@jhlagado/azm 0.2.0 → 0.2.1

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 (36) hide show
  1. package/README.md +95 -70
  2. package/dist/src/api-compile.js +1 -1
  3. package/dist/src/assembly/address-planning.js +2 -0
  4. package/dist/src/assembly/program-emission.js +1 -0
  5. package/dist/src/expansion/op-expansion.js +1 -0
  6. package/dist/src/model/source-item.d.ts +6 -0
  7. package/dist/src/outputs/write-asm80.js +122 -5
  8. package/dist/src/register-care/analyze.js +36 -8
  9. package/dist/src/register-care/annotate.d.ts +11 -0
  10. package/dist/src/register-care/annotate.js +76 -0
  11. package/dist/src/register-care/annotations.js +33 -146
  12. package/dist/src/register-care/fix.d.ts +2 -0
  13. package/dist/src/register-care/fix.js +52 -0
  14. package/dist/src/register-care/instruction-shape.d.ts +11 -0
  15. package/dist/src/register-care/instruction-shape.js +129 -0
  16. package/dist/src/register-care/liveness.js +15 -7
  17. package/dist/src/register-care/profiles.js +4 -0
  18. package/dist/src/register-care/programModel.js +79 -13
  19. package/dist/src/register-care/report.d.ts +2 -1
  20. package/dist/src/register-care/report.js +91 -34
  21. package/dist/src/register-care/routine-summaries.d.ts +6 -0
  22. package/dist/src/register-care/routine-summaries.js +89 -0
  23. package/dist/src/register-care/sourceText.d.ts +8 -0
  24. package/dist/src/register-care/sourceText.js +15 -0
  25. package/dist/src/register-care/summaries.d.ts +3 -3
  26. package/dist/src/register-care/summaries.js +42 -75
  27. package/dist/src/register-care/summary.d.ts +3 -0
  28. package/dist/src/register-care/summary.js +474 -0
  29. package/dist/src/register-care/types.d.ts +6 -1
  30. package/dist/src/source/strip-line-comment.d.ts +2 -0
  31. package/dist/src/source/strip-line-comment.js +26 -0
  32. package/dist/src/syntax/parse-diagnostics.d.ts +12 -0
  33. package/dist/src/syntax/parse-diagnostics.js +18 -0
  34. package/dist/src/syntax/parse-line.js +63 -10
  35. package/docs/reference/tooling-api.md +13 -6
  36. package/package.json +4 -2
@@ -1,154 +1,41 @@
1
- function formatCarrierLine(tag, units) {
2
- return `;! ${tag.padEnd(10)}${units.join(',')}`;
3
- }
4
- function formatCandidateUnits(units) {
5
- return units.length === 1 ? units[0] : `{${units.join(',')}}`;
6
- }
7
- function formatCarrierLineWithExpectOut(indentation, units) {
8
- return `${indentation}; expects out ${formatCandidateUnits(units)}`;
9
- }
10
- function formatCarrierLineWithMaybeOut(indentation, units) {
11
- return `${indentation};! ${'maybe-out'.padEnd(10)}${formatCandidateUnits(units)}`;
12
- }
13
- function isGeneratedRegisterContractLine(line) {
14
- return /^\s*;!\s*(in|out|clobbers|preserves|maybe-out)\b/i.test(line);
15
- }
16
- function isOutputCandidateHintLine(line) {
17
- return /^\s*;\s*expects\s+out\b/i.test(line) || /^\s*;\s*!\s*maybe-out\b/i.test(line);
18
- }
19
- function normalizeLineEnding(text) {
20
- return text.replace(/\r\n/g, '\n');
21
- }
22
- function splitSourceLines(text) {
23
- return normalizeLineEnding(text).split('\n');
24
- }
25
- function lineDeltaForCandidate(line, deltas) {
26
- let shift = 0;
27
- for (const delta of deltas) {
28
- if (delta.anchorLine < line) {
29
- shift += delta.delta;
30
- }
31
- }
32
- return shift;
33
- }
34
- function applyOutputCandidateHints(sourceText, outputCandidates, candidateFixability, deltas, outputCandidateKey) {
35
- const lines = splitSourceLines(sourceText);
36
- const grouped = new Map();
37
- for (const candidate of outputCandidates) {
38
- const adjustedLine = candidate.line + lineDeltaForCandidate(candidate.line, deltas);
39
- const existing = grouped.get(adjustedLine);
40
- const autoFixable = candidateFixability.get(outputCandidateKey(candidate.file, candidate.line, candidate.column)) ?? false;
41
- if (existing === undefined) {
42
- grouped.set(adjustedLine, { carriers: [...candidate.carriers], autoFixable });
43
- continue;
44
- }
45
- const carriers = existing.carriers;
46
- for (const carrier of candidate.carriers) {
47
- if (!carriers.includes(carrier)) {
48
- carriers.push(carrier);
49
- }
50
- }
51
- existing.autoFixable = existing.autoFixable && autoFixable;
52
- }
53
- const candidates = [...grouped.entries()]
54
- .map(([line, entry]) => ({ line, ...entry }))
55
- .sort((left, right) => right.line - left.line);
56
- for (const candidate of candidates) {
57
- const index = candidate.line - 1;
58
- if (index < 0 || index > lines.length)
59
- continue;
60
- if (index > 0 && isOutputCandidateHintLine(lines[index - 1] ?? ''))
61
- continue;
62
- const indentation = lines[index]?.match(/^\s*/)?.[0] ?? '';
63
- const hint = candidate.autoFixable
64
- ? formatCarrierLineWithExpectOut(indentation, candidate.carriers)
65
- : formatCarrierLineWithMaybeOut(indentation, candidate.carriers);
66
- lines.splice(index, 0, hint);
67
- }
68
- return lines.join('\n');
69
- }
70
- function annotateSourceFile(sourceText, routines, summariesByName) {
71
- const routineLines = Array.from(routines)
1
+ import { annotateRegisterCareContracts } from './annotate.js';
2
+ import { applyExpectOutFixesToSource, findExpectOutFixesForCandidates, } from './fix.js';
3
+ export function buildAnnotations(loaded, programRoutines, summariesByName, outputCandidates, options) {
4
+ const routines = programRoutines
72
5
  .filter((routine) => summariesByName.has(routine.name))
73
- .sort((left, right) => right.span.start.line - left.span.start.line);
74
- if (routineLines.length === 0)
75
- return undefined;
76
- const lines = splitSourceLines(sourceText);
77
- let changed = false;
78
- const deltas = [];
79
- for (const routine of routineLines) {
80
- const summary = summariesByName.get(routine.name);
81
- if (!summary)
82
- continue;
83
- const insertLine = routine.span.start.line - 1;
84
- if (insertLine < 0 || insertLine > lines.length)
85
- continue;
86
- const generatedLines = [
87
- ...(summary.mayRead.length > 0 ? [formatCarrierLine('in', summary.mayRead)] : []),
88
- ...(summary.mayWrite.length > 0 ? [formatCarrierLine('out', summary.mayWrite)] : []),
89
- ...(summary.preserved.length > 0 ? [formatCarrierLine('preserves', summary.preserved)] : []),
90
- ];
91
- if (generatedLines.length === 0)
92
- continue;
93
- let start = insertLine;
94
- for (let index = insertLine - 1; index >= 0 && isGeneratedRegisterContractLine(lines[index] ?? ''); index -= 1) {
95
- start = index;
96
- }
97
- if (start === insertLine ||
98
- lines.slice(start, insertLine).some((line) => line.trim().length === 0)) {
99
- start = insertLine;
100
- }
101
- const existing = lines.slice(start, insertLine);
102
- if (existing.length !== generatedLines.length ||
103
- existing.some((line, index) => line !== generatedLines[index])) {
104
- changed = true;
105
- deltas.push({
106
- anchorLine: routine.span.start.line,
107
- delta: generatedLines.length - (insertLine - start),
108
- });
109
- lines.splice(start, insertLine - start, ...generatedLines);
110
- }
6
+ .map((routine) => ({
7
+ routine,
8
+ summary: summariesByName.get(routine.name),
9
+ }));
10
+ const annotated = annotateRegisterCareContracts(loaded.sourceTexts, routines);
11
+ if (!options.fixOutputCandidates) {
12
+ return annotated.map((file) => ({ path: file.path, text: file.text }));
111
13
  }
112
- if (!changed)
113
- return undefined;
114
- return {
115
- text: lines.join('\n'),
116
- deltas,
117
- };
118
- }
119
- export function buildAnnotations(loaded, programRoutines, summariesByName, outputCandidates, options) {
120
- const byFile = new Map();
121
- for (const routine of programRoutines) {
122
- if (!summariesByName.has(routine.name))
123
- continue;
124
- const file = byFile.get(routine.span.file);
125
- if (file === undefined) {
126
- byFile.set(routine.span.file, [routine]);
127
- }
128
- else {
129
- file.push(routine);
130
- }
14
+ const autoFixableCandidates = outputCandidates.filter((candidate) => options.outputCandidateFixability.get(options.outputCandidateKey(candidate.file, candidate.line, candidate.column)) === true);
15
+ const fixes = findExpectOutFixesForCandidates([...programRoutines], autoFixableCandidates);
16
+ if (fixes.length === 0) {
17
+ return annotated.map((file) => ({ path: file.path, text: file.text }));
18
+ }
19
+ const workingTexts = new Map(loaded.sourceTexts);
20
+ for (const file of annotated) {
21
+ workingTexts.set(file.path, file.text);
131
22
  }
132
23
  const out = [];
133
- for (const [path, routines] of byFile) {
134
- const sourceText = loaded.sourceTexts.get(path);
135
- if (sourceText === undefined)
24
+ const fixesByFile = new Map();
25
+ for (const fix of fixes) {
26
+ const items = fixesByFile.get(fix.file) ?? [];
27
+ items.push(fix);
28
+ fixesByFile.set(fix.file, items);
29
+ }
30
+ for (const [path, text] of workingTexts) {
31
+ const reference = loaded.sourceTexts.get(path);
32
+ if (reference === undefined)
136
33
  continue;
137
- let text = sourceText;
138
- let deltas = [];
139
- const annotation = annotateSourceFile(sourceText, routines, summariesByName);
140
- if (annotation !== undefined) {
141
- text = annotation.text;
142
- deltas = annotation.deltas;
143
- }
144
- if (options.fixOutputCandidates) {
145
- const candidatesForFile = outputCandidates.filter((candidate) => candidate.file === path);
146
- if (candidatesForFile.length > 0) {
147
- text = applyOutputCandidateHints(text, candidatesForFile, options.outputCandidateFixability, deltas, options.outputCandidateKey);
148
- }
34
+ const fileFixes = fixesByFile.get(path) ?? [];
35
+ const nextText = fileFixes.length > 0 ? applyExpectOutFixesToSource(text, fileFixes, reference) : text;
36
+ if (nextText !== reference) {
37
+ out.push({ path, text: nextText });
149
38
  }
150
- if (text !== sourceText)
151
- out.push({ path, text });
152
39
  }
153
- return out;
40
+ return out.sort((left, right) => left.path.localeCompare(right.path));
154
41
  }
@@ -6,4 +6,6 @@ export interface RegisterCareExpectOutFix {
6
6
  routine: string;
7
7
  carriers: RegisterCareUnit[];
8
8
  }
9
+ export declare function findExpectOutFixesForCandidates(routines: RegisterCareRoutine[], candidates: RegisterCareOutputCandidate[]): RegisterCareExpectOutFix[];
9
10
  export declare function autoFixableCandidateKeys(routines: RegisterCareRoutine[], candidates: RegisterCareOutputCandidate[]): Set<string>;
11
+ export declare function applyExpectOutFixesToSource(source: string, fixes: RegisterCareExpectOutFix[], referenceSource?: string): string;
@@ -1,5 +1,7 @@
1
1
  import { getZ80InstructionEffect } from '../z80/effects.js';
2
2
  import { instructionSuccessors, labelIndex } from './controlFlow.js';
3
+ import { contractCarrierList } from './report.js';
4
+ import { joinSourceLines, splitSourceLines } from './sourceText.js';
3
5
  function sameLocation(a, b) {
4
6
  return a.file === b.file && a.line === b.line && a.column === b.column;
5
7
  }
@@ -68,6 +70,9 @@ function findExpectOutFixes(routines, candidates) {
68
70
  }
69
71
  return out;
70
72
  }
73
+ export function findExpectOutFixesForCandidates(routines, candidates) {
74
+ return findExpectOutFixes(routines, candidates);
75
+ }
71
76
  export function autoFixableCandidateKeys(routines, candidates) {
72
77
  const fixes = findExpectOutFixes(routines, candidates);
73
78
  const out = new Set();
@@ -76,3 +81,50 @@ export function autoFixableCandidateKeys(routines, candidates) {
76
81
  }
77
82
  return out;
78
83
  }
84
+ function isExpectOutLine(line) {
85
+ return /^\s*;\s*expects\s+out\b/i.test(line);
86
+ }
87
+ function expectedCallLine(originalLines, fix) {
88
+ return originalLines[fix.line - 1]?.trim();
89
+ }
90
+ function findCallLineIndex(lines, originalLines, fix) {
91
+ const expected = expectedCallLine(originalLines, fix);
92
+ if (!expected)
93
+ return undefined;
94
+ const preferred = fix.line - 1;
95
+ if (lines[preferred]?.trim() === expected)
96
+ return preferred;
97
+ let best;
98
+ let bestDistance = Number.POSITIVE_INFINITY;
99
+ for (let index = 0; index < lines.length; index += 1) {
100
+ if (lines[index]?.trim() !== expected)
101
+ continue;
102
+ const distance = Math.abs(index - preferred);
103
+ if (distance < bestDistance) {
104
+ best = index;
105
+ bestDistance = distance;
106
+ }
107
+ }
108
+ return best;
109
+ }
110
+ function indentation(line) {
111
+ return line.match(/^\s*/)?.[0] ?? '';
112
+ }
113
+ export function applyExpectOutFixesToSource(source, fixes, referenceSource = source) {
114
+ if (fixes.length === 0)
115
+ return source;
116
+ const originalLines = referenceSource.split(/\r?\n/);
117
+ const sourceLines = splitSourceLines(source);
118
+ const { lines } = sourceLines;
119
+ const sorted = [...fixes].sort((a, b) => b.line - a.line || b.column - a.column);
120
+ for (const fix of sorted) {
121
+ const index = findCallLineIndex(lines, originalLines, fix);
122
+ if (index === undefined)
123
+ continue;
124
+ if (index > 0 && isExpectOutLine(lines[index - 1] ?? ''))
125
+ continue;
126
+ const prefix = indentation(lines[index] ?? '');
127
+ lines.splice(index, 0, `${prefix}; expects out ${contractCarrierList(fix.carriers)}`);
128
+ }
129
+ return joinSourceLines(sourceLines);
130
+ }
@@ -0,0 +1,11 @@
1
+ import type { Z80Instruction, Z80Operand } from '../z80/instruction.js';
2
+ import type { RegisterCareInstruction } from './types.js';
3
+ export declare function instructionHead(item: RegisterCareInstruction): string;
4
+ export declare function regName(operand: Z80Operand | undefined): string | undefined;
5
+ export declare function instructionOperandCount(instruction: Z80Instruction): number;
6
+ export declare function instructionOperand(instruction: Z80Instruction, index: number): Z80Operand | undefined;
7
+ export declare function isUnconditionalReturnInstruction(item: RegisterCareInstruction): boolean;
8
+ export declare function isPureTokenTransferInstruction(item: RegisterCareInstruction): boolean;
9
+ export declare function isAccumulatorSelfOperand(item: RegisterCareInstruction): boolean;
10
+ export declare function isImmediateZeroOperand(item: RegisterCareInstruction): boolean;
11
+ export declare function isRegisterOperand(item: RegisterCareInstruction | undefined, index: number, name: string): boolean;
@@ -0,0 +1,129 @@
1
+ export function instructionHead(item) {
2
+ return item.instruction.mnemonic.toLowerCase();
3
+ }
4
+ export function regName(operand) {
5
+ if (operand === undefined)
6
+ return undefined;
7
+ switch (operand.kind) {
8
+ case 'reg8':
9
+ return operand.register.toUpperCase();
10
+ case 'reg16':
11
+ return operand.register.toUpperCase();
12
+ case 'reg-index16':
13
+ return operand.register.toUpperCase();
14
+ case 'reg-half-index':
15
+ return operand.register.toUpperCase();
16
+ default:
17
+ return undefined;
18
+ }
19
+ }
20
+ function immValue(operand) {
21
+ if (operand?.kind !== 'imm')
22
+ return undefined;
23
+ const expression = operand.expression;
24
+ return expression.kind === 'number' ? expression.value : undefined;
25
+ }
26
+ export function instructionOperandCount(instruction) {
27
+ switch (instruction.mnemonic) {
28
+ case 'ret':
29
+ case 'ret-cc':
30
+ return instruction.mnemonic === 'ret' ? 0 : 1;
31
+ case 'ld':
32
+ return 2;
33
+ case 'ex':
34
+ return 2;
35
+ case 'jp':
36
+ case 'jp-cc':
37
+ case 'jr':
38
+ case 'jr-cc':
39
+ case 'djnz':
40
+ case 'call':
41
+ case 'call-cc':
42
+ return 1;
43
+ case 'add':
44
+ case 'adc':
45
+ case 'sbc':
46
+ return 'target' in instruction ? 2 : 1;
47
+ case 'sub':
48
+ case 'and':
49
+ case 'or':
50
+ case 'xor':
51
+ case 'cp':
52
+ return 1;
53
+ default:
54
+ return 0;
55
+ }
56
+ }
57
+ export function instructionOperand(instruction, index) {
58
+ switch (instruction.mnemonic) {
59
+ case 'ld':
60
+ return index === 0 ? instruction.target : index === 1 ? instruction.source : undefined;
61
+ case 'ex': {
62
+ if (index === 0) {
63
+ return instruction.form === 'de-hl'
64
+ ? { kind: 'reg16', register: 'de' }
65
+ : instruction.form === 'af-af'
66
+ ? { kind: 'reg16', register: 'af' }
67
+ : undefined;
68
+ }
69
+ if (index === 1) {
70
+ return instruction.form === 'de-hl'
71
+ ? { kind: 'reg16', register: 'hl' }
72
+ : undefined;
73
+ }
74
+ return undefined;
75
+ }
76
+ case 'add':
77
+ case 'adc':
78
+ case 'sbc':
79
+ if ('target' in instruction) {
80
+ return index === 0 ? instruction.target : index === 1 ? instruction.source : undefined;
81
+ }
82
+ return index === 0 ? instruction.source : undefined;
83
+ case 'sub':
84
+ case 'and':
85
+ case 'or':
86
+ case 'xor':
87
+ case 'cp':
88
+ return index === 0 ? instruction.source : undefined;
89
+ default:
90
+ return undefined;
91
+ }
92
+ }
93
+ export function isUnconditionalReturnInstruction(item) {
94
+ const head = instructionHead(item);
95
+ if (head === 'ret')
96
+ return item.instruction.mnemonic === 'ret';
97
+ return head === 'retn' || head === 'reti';
98
+ }
99
+ export function isPureTokenTransferInstruction(item) {
100
+ const head = instructionHead(item);
101
+ if (head === 'ex')
102
+ return true;
103
+ if (head !== 'ld' || instructionOperandCount(item.instruction) !== 2)
104
+ return false;
105
+ const dst = instructionOperand(item.instruction, 0);
106
+ const src = instructionOperand(item.instruction, 1);
107
+ if (regName(dst) === undefined)
108
+ return false;
109
+ return regName(src) !== undefined || src?.kind === 'imm';
110
+ }
111
+ export function isAccumulatorSelfOperand(item) {
112
+ const inst = item.instruction;
113
+ if (inst.mnemonic === 'or' || inst.mnemonic === 'and' || inst.mnemonic === 'xor') {
114
+ return inst.source.kind === 'reg8' && inst.source.register === 'a';
115
+ }
116
+ return false;
117
+ }
118
+ export function isImmediateZeroOperand(item) {
119
+ const inst = item.instruction;
120
+ if (inst.mnemonic !== 'cp')
121
+ return false;
122
+ return immValue(inst.source) === 0;
123
+ }
124
+ export function isRegisterOperand(item, index, name) {
125
+ if (item === undefined)
126
+ return false;
127
+ const operand = instructionOperand(item.instruction, index);
128
+ return regName(operand) === name.toUpperCase();
129
+ }
@@ -48,10 +48,7 @@ function hintUnitsForLine(hints, file, callLine) {
48
48
  return prior?.comment.kind === 'expectOut' ? unique(prior.comment.carriers) : [];
49
49
  }
50
50
  function outputUnits(summary) {
51
- return unique([
52
- ...(summary.valueRelations?.flatMap((relation) => relation.out) ?? []),
53
- ...(summary.mayOutput ?? []),
54
- ]);
51
+ return unique(summary.valueRelations.flatMap((relation) => relation.out));
55
52
  }
56
53
  function setEqual(left, right) {
57
54
  if (left.size !== right.size)
@@ -148,7 +145,7 @@ export function findRegisterCareConflicts(routine, summaries, hints) {
148
145
  for (const unit of outputUnits(summary))
149
146
  accepted.add(unit);
150
147
  const carriers = unique(summary.mayWrite.filter((unit) => liveOut[index].has(unit) && !accepted.has(unit)));
151
- if (carriers.length > 0 && !boundary.conditional) {
148
+ if (carriers.length > 0) {
152
149
  conflicts.push({
153
150
  file: item.file,
154
151
  line: item.line,
@@ -171,8 +168,19 @@ export function findCallerOutputCandidateObservations(routines, summaries) {
171
168
  for (const routine of routines) {
172
169
  const { liveOut } = liveSetsForRoutine(routine, summaries);
173
170
  for (const { item, index, boundary, target, summary } of resolvedBoundariesForRoutine(routine, summaries)) {
174
- const alreadyOutput = new Set(outputUnits(summary));
175
- const carriers = unique(summary.mayWrite.filter((unit) => liveOut[index].has(unit) && !alreadyOutput.has(unit)));
171
+ const intentionalOutputs = new Set(outputUnits(summary));
172
+ const carrierSources = [];
173
+ for (const unit of summary.mayWrite) {
174
+ if (liveOut[index].has(unit) && !intentionalOutputs.has(unit)) {
175
+ carrierSources.push(unit);
176
+ }
177
+ }
178
+ for (const unit of intentionalOutputs) {
179
+ if (liveOut[index].has(unit)) {
180
+ carrierSources.push(unit);
181
+ }
182
+ }
183
+ const carriers = unique(carrierSources);
176
184
  if (carriers.length > 0) {
177
185
  out.push({
178
186
  file: item.file,
@@ -23,6 +23,8 @@ export function getRegisterCareProfile(name) {
23
23
  mayOutput: [],
24
24
  preserved: ['B', 'C', 'D', 'E', 'H', 'L'],
25
25
  valueRelations: [],
26
+ stackBalanced: true,
27
+ hasUnknownStackEffect: false,
26
28
  },
27
29
  ],
28
30
  ]),
@@ -36,6 +38,8 @@ export function getRegisterCareProfile(name) {
36
38
  mayOutput: ['A', 'carry', 'zero'],
37
39
  preserved: ['B', 'C', 'D', 'E', 'H', 'L'],
38
40
  valueRelations: [{ out: ['A', 'carry', 'zero'], from: [] }],
41
+ stackBalanced: true,
42
+ hasUnknownStackEffect: false,
39
43
  },
40
44
  ],
41
45
  ]),
@@ -13,6 +13,21 @@ function instructionCallTarget(item) {
13
13
  }
14
14
  return undefined;
15
15
  }
16
+ function instructionTailJumpTarget(item, entryNames) {
17
+ if (item.kind !== 'instruction')
18
+ return undefined;
19
+ const mnemonic = item.instruction.mnemonic;
20
+ if (mnemonic === 'jp-cc' && entryNames === undefined)
21
+ return undefined;
22
+ if (mnemonic !== 'jp' && mnemonic !== 'jp-cc')
23
+ return undefined;
24
+ const target = routineNameFromExpression(item.instruction.expression);
25
+ if (target === undefined || target.startsWith('.'))
26
+ return undefined;
27
+ if (entryNames !== undefined && !entryNames.has(target))
28
+ return undefined;
29
+ return target;
30
+ }
16
31
  function toInstruction(item, labels) {
17
32
  return {
18
33
  instruction: item.instruction,
@@ -22,9 +37,16 @@ function toInstruction(item, labels) {
22
37
  labels: [...labels],
23
38
  };
24
39
  }
40
+ function pushDirectBoundary(boundaries, target, subject, file, line, column) {
41
+ boundaries.push({ target, subject, file, line, column });
42
+ }
25
43
  export function buildRegisterCareProgramModel(items) {
26
44
  const routines = [];
27
45
  const directCalls = [];
46
+ const filesWithEntryLabels = new Set(items
47
+ .filter((item) => item.kind === 'label')
48
+ .filter((item) => item.isEntry === true)
49
+ .map((item) => item.span.sourceName));
28
50
  let routineName;
29
51
  let entryLabels = [];
30
52
  let labels = [];
@@ -35,14 +57,28 @@ export function buildRegisterCareProgramModel(items) {
35
57
  const startRoutine = (item) => {
36
58
  sourceName = item.span.sourceName;
37
59
  routineName = item.name;
38
- entryLabels = [item.name];
60
+ entryLabels = item.isEntry === true ? [item.name] : [];
39
61
  labels = [item.name];
40
62
  routineStartLine = item.span.line;
41
63
  routineStartColumn = item.span.column;
42
64
  instructions = [];
43
65
  };
44
66
  const flushRoutine = () => {
45
- if (routineName === undefined || instructions.length === 0 || routineStartLine === undefined) {
67
+ if (routineName === undefined || routineStartLine === undefined) {
68
+ return;
69
+ }
70
+ if (instructions.length === 0) {
71
+ routines.push({
72
+ name: routineName,
73
+ labels: [...labels],
74
+ entryLabels: [...entryLabels],
75
+ instructions: [],
76
+ span: {
77
+ file: sourceName ?? '',
78
+ start: { line: routineStartLine, column: routineStartColumn ?? 1 },
79
+ end: { line: routineStartLine, column: routineStartColumn ?? 1 },
80
+ },
81
+ });
46
82
  return;
47
83
  }
48
84
  const end = instructions[instructions.length - 1];
@@ -88,12 +124,7 @@ export function buildRegisterCareProgramModel(items) {
88
124
  instructions.push(toInstruction(item, labels));
89
125
  const directTarget = instructionCallTarget(item);
90
126
  if (directTarget !== undefined) {
91
- directCalls.push({
92
- target: directTarget,
93
- file: item.span.sourceName,
94
- line: item.span.line,
95
- column: item.span.column,
96
- });
127
+ pushDirectBoundary(directCalls, directTarget, `CALL ${directTarget}`, item.span.sourceName, item.span.line, item.span.column);
97
128
  }
98
129
  continue;
99
130
  }
@@ -104,19 +135,54 @@ export function buildRegisterCareProgramModel(items) {
104
135
  continue;
105
136
  }
106
137
  if (routineName === undefined) {
138
+ if (filesWithEntryLabels.has(item.span.sourceName) && item.isEntry !== true) {
139
+ continue;
140
+ }
107
141
  startRoutine(item);
108
142
  continue;
109
143
  }
110
- if (instructions.length > 0 ||
111
- sourceName === undefined ||
112
- sourceName !== item.span.sourceName) {
144
+ if (sourceName === undefined || sourceName !== item.span.sourceName) {
145
+ finalizeAndRestart(item);
146
+ continue;
147
+ }
148
+ if (instructions.length > 0) {
149
+ if (filesWithEntryLabels.has(item.span.sourceName) && item.isEntry !== true) {
150
+ labels.push(item.name);
151
+ continue;
152
+ }
113
153
  finalizeAndRestart(item);
114
154
  continue;
115
155
  }
116
156
  // Multiple global labels before body on same routine are coalesced as entry labels.
117
157
  labels.push(item.name);
118
- entryLabels.push(item.name);
158
+ if (item.isEntry === true) {
159
+ entryLabels.push(item.name);
160
+ }
119
161
  }
120
162
  flushRoutine();
121
- return { routines, directCalls };
163
+ const entryNamesByFile = new Map();
164
+ for (const item of items) {
165
+ if (item.kind !== 'label' || item.isEntry !== true)
166
+ continue;
167
+ const names = entryNamesByFile.get(item.span.sourceName) ?? new Set();
168
+ names.add(item.name);
169
+ entryNamesByFile.set(item.span.sourceName, names);
170
+ }
171
+ const directTailJumps = [];
172
+ for (const item of items) {
173
+ if (item.kind !== 'instruction')
174
+ continue;
175
+ const entryNames = filesWithEntryLabels.has(item.span.sourceName)
176
+ ? entryNamesByFile.get(item.span.sourceName)
177
+ : undefined;
178
+ const target = instructionTailJumpTarget(item, entryNames);
179
+ if (target === undefined)
180
+ continue;
181
+ pushDirectBoundary(directTailJumps, target, `JP ${target}`, item.span.sourceName, item.span.line, item.span.column);
182
+ }
183
+ return {
184
+ routines,
185
+ directCalls,
186
+ directBoundaries: [...directCalls, ...directTailJumps],
187
+ };
122
188
  }
@@ -1,4 +1,5 @@
1
1
  import type { RegisterCareReportModel, RegisterCareUnit, RoutineSummary } from './types.js';
2
- export declare function renderRegisterCareReport(model: RegisterCareReportModel): string;
3
2
  export declare function contractCarrierList(units: RegisterCareUnit[]): string;
3
+ export declare function renderRegisterCareReport(model: RegisterCareReportModel): string;
4
4
  export declare function renderRegisterCareInterface(summaries: RoutineSummary[]): string;
5
+ export declare function renderRegisterCareSourceBlock(summary: RoutineSummary): string[];