@jhlagado/azm 0.2.12 → 0.2.13

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 (44) hide show
  1. package/dist/src/api-compile.d.ts +7 -1
  2. package/dist/src/api-compile.js +17 -5
  3. package/dist/src/api-register-contracts.js +69 -2
  4. package/dist/src/api-tooling.d.ts +1 -1
  5. package/dist/src/cli/artifact-files.d.ts +1 -0
  6. package/dist/src/cli/artifact-files.js +5 -0
  7. package/dist/src/cli/parse-args.d.ts +6 -1
  8. package/dist/src/cli/parse-args.js +59 -0
  9. package/dist/src/cli/run.js +2 -2
  10. package/dist/src/cli/usage.js +5 -0
  11. package/dist/src/cli/write-artifacts.d.ts +1 -1
  12. package/dist/src/cli/write-artifacts.js +15 -2
  13. package/dist/src/expansion/op-expansion.js +12 -1
  14. package/dist/src/index.d.ts +1 -1
  15. package/dist/src/outputs/types.d.ts +13 -1
  16. package/dist/src/register-contracts/analyze-helpers.d.ts +6 -1
  17. package/dist/src/register-contracts/analyze-helpers.js +67 -0
  18. package/dist/src/register-contracts/analyze.d.ts +8 -1
  19. package/dist/src/register-contracts/analyze.js +353 -16
  20. package/dist/src/register-contracts/interfaceContracts.js +45 -0
  21. package/dist/src/register-contracts/liveness.js +23 -0
  22. package/dist/src/register-contracts/policy.d.ts +2 -0
  23. package/dist/src/register-contracts/policy.js +54 -0
  24. package/dist/src/register-contracts/programModel-boundaries.d.ts +5 -1
  25. package/dist/src/register-contracts/programModel-boundaries.js +20 -5
  26. package/dist/src/register-contracts/programModel-routines.js +37 -6
  27. package/dist/src/register-contracts/ratchet.d.ts +3 -0
  28. package/dist/src/register-contracts/ratchet.js +88 -0
  29. package/dist/src/register-contracts/report.d.ts +8 -1
  30. package/dist/src/register-contracts/report.js +174 -0
  31. package/dist/src/register-contracts/smartCommentParsing.js +22 -0
  32. package/dist/src/register-contracts/summary-boundary.js +9 -2
  33. package/dist/src/register-contracts/tooling.d.ts +2 -1
  34. package/dist/src/register-contracts/tooling.js +2 -0
  35. package/dist/src/register-contracts/types.d.ts +157 -0
  36. package/dist/src/syntax/parse-line.js +3 -0
  37. package/docs/codebase/02-source-loading-and-parsing.md +10 -6
  38. package/docs/codebase/04-ops-and-register-contracts.md +49 -4
  39. package/docs/codebase/05-interfaces-and-output-artifacts.md +56 -6
  40. package/docs/codebase/06-verification-and-maintenance.md +10 -2
  41. package/docs/codebase/appendices/a-directory-file-reference.md +3 -1
  42. package/docs/codebase/appendices/b-compile-flow-reference.md +7 -5
  43. package/docs/codebase/appendices/c-public-surface-reference.md +19 -5
  44. package/package.json +1 -1
@@ -2,6 +2,9 @@ import { instructionCallTarget, pushDirectBoundary } from './programModel-bounda
2
2
  function isGlobalLabel(name) {
3
3
  return !name.startsWith('.');
4
4
  }
5
+ function isGeneratedOpLabel(name) {
6
+ return name.startsWith('__azm_op_');
7
+ }
5
8
  function emptyState() {
6
9
  return {
7
10
  entryLabels: [],
@@ -10,17 +13,40 @@ function emptyState() {
10
13
  };
11
14
  }
12
15
  function toInstruction(item, labels, constants) {
16
+ const span = effectiveInstructionSpan(item);
13
17
  return {
14
18
  instruction: item.instruction,
15
- file: item.span.sourceName,
16
- line: item.span.line,
17
- column: item.span.column,
19
+ file: span.sourceName,
20
+ line: span.line,
21
+ column: span.column,
22
+ ...(span.sourceUnit !== undefined ? { sourceUnit: span.sourceUnit } : {}),
23
+ ...(span.sourceRelation !== undefined ? { sourceRelation: span.sourceRelation } : {}),
24
+ ...(span.sourceUnitRelation !== undefined
25
+ ? { sourceUnitRelation: span.sourceUnitRelation }
26
+ : {}),
18
27
  labels: [...labels],
19
28
  constants,
20
29
  };
21
30
  }
31
+ function effectiveInstructionSpan(item) {
32
+ return item.emittedSource?.span ?? item.span;
33
+ }
22
34
  function startRoutine(state, item) {
23
35
  state.sourceName = item.span.sourceName;
36
+ if (item.span.sourceUnit !== undefined)
37
+ state.sourceUnit = item.span.sourceUnit;
38
+ else
39
+ delete state.sourceUnit;
40
+ if (item.span.sourceRelation !== undefined)
41
+ state.sourceRelation = item.span.sourceRelation;
42
+ else
43
+ delete state.sourceRelation;
44
+ if (item.span.sourceUnitRelation !== undefined) {
45
+ state.sourceUnitRelation = item.span.sourceUnitRelation;
46
+ }
47
+ else {
48
+ delete state.sourceUnitRelation;
49
+ }
24
50
  state.routineName = item.name;
25
51
  state.entryLabels = item.isEntry === true ? [item.name] : [];
26
52
  state.labels = [item.name];
@@ -32,6 +58,11 @@ function routineSpan(state, end) {
32
58
  const line = state.routineStartLine ?? 1;
33
59
  return {
34
60
  file: state.sourceName ?? '',
61
+ ...(state.sourceUnit !== undefined ? { sourceUnit: state.sourceUnit } : {}),
62
+ ...(state.sourceRelation !== undefined ? { sourceRelation: state.sourceRelation } : {}),
63
+ ...(state.sourceUnitRelation !== undefined
64
+ ? { sourceUnitRelation: state.sourceUnitRelation }
65
+ : {}),
35
66
  start: { line, column: state.routineStartColumn ?? 1 },
36
67
  end: { line: end?.line ?? line, column: end?.column ?? state.routineStartColumn ?? 1 },
37
68
  };
@@ -58,12 +89,12 @@ function appendDirectCall(directCalls, item) {
58
89
  const directTarget = instructionCallTarget(item);
59
90
  if (directTarget === undefined)
60
91
  return;
61
- pushDirectBoundary(directCalls, directTarget, `CALL ${directTarget}`, item.span.sourceName, item.span.line, item.span.column);
92
+ pushDirectBoundary(directCalls, directTarget, `CALL ${directTarget}`, effectiveInstructionSpan(item));
62
93
  }
63
94
  function handleInstruction(state, directCalls, item, context) {
64
95
  if (state.routineName === undefined || state.sourceName === undefined)
65
96
  return;
66
- if (item.span.sourceName !== state.sourceName)
97
+ if (effectiveInstructionSpan(item).sourceName !== state.sourceName)
67
98
  return;
68
99
  state.instructions.push(toInstruction(item, state.labels, context.constants));
69
100
  appendDirectCall(directCalls, item);
@@ -106,7 +137,7 @@ function appendRoutineLabel(state, item) {
106
137
  state.entryLabels.push(item.name);
107
138
  }
108
139
  function handleLabel(routines, state, item, context) {
109
- if (!isGlobalLabel(item.name)) {
140
+ if (!isGlobalLabel(item.name) || isGeneratedOpLabel(item.name)) {
110
141
  if (state.routineName !== undefined)
111
142
  state.labels.push(item.name);
112
143
  return;
@@ -0,0 +1,3 @@
1
+ import type { RegisterContractsJsonFinding, RegisterContractsJsonReportModel, RegisterContractsRatchetResult } from './types.js';
2
+ export declare function registerContractsFindingIdentity(finding: RegisterContractsJsonFinding): string;
3
+ export declare function compareRegisterContractsBaseline(current: RegisterContractsJsonReportModel, baseline: RegisterContractsJsonReportModel, baselineFile: string | undefined): RegisterContractsRatchetResult;
@@ -0,0 +1,88 @@
1
+ function carriersKey(finding) {
2
+ return [...(finding.carriers ?? [])].sort().join(',');
3
+ }
4
+ export function registerContractsFindingIdentity(finding) {
5
+ const target = finding.callTarget ?? finding.routine ?? finding.subject ?? '';
6
+ return [
7
+ finding.kind,
8
+ target,
9
+ carriersKey(finding),
10
+ ].join('|');
11
+ }
12
+ function displayIdentity(finding) {
13
+ return [
14
+ registerContractsFindingIdentity(finding),
15
+ finding.location.file,
16
+ finding.location.line,
17
+ finding.location.column,
18
+ ].join('|');
19
+ }
20
+ function ratchetEntries(findings) {
21
+ const out = new Map();
22
+ for (const finding of findings) {
23
+ const identity = registerContractsFindingIdentity(finding);
24
+ const entries = out.get(identity) ?? [];
25
+ entries.push({ identity: displayIdentity(finding), finding });
26
+ out.set(identity, entries);
27
+ }
28
+ return out;
29
+ }
30
+ function findingFingerprint(finding) {
31
+ return JSON.stringify({
32
+ location: finding.location,
33
+ message: finding.message,
34
+ remediationCategory: finding.remediation.category,
35
+ remediationHint: finding.remediation.hint,
36
+ });
37
+ }
38
+ export function compareRegisterContractsBaseline(current, baseline, baselineFile) {
39
+ const currentEntries = ratchetEntries(current.findings);
40
+ const baselineEntries = ratchetEntries(baseline.findings);
41
+ const identities = new Set([...currentEntries.keys(), ...baselineEntries.keys()]);
42
+ const newFindings = [];
43
+ const removedFindings = [];
44
+ const changedFindings = [];
45
+ for (const identity of identities) {
46
+ const currentGroup = [...(currentEntries.get(identity) ?? [])];
47
+ const baselineGroup = [...(baselineEntries.get(identity) ?? [])];
48
+ const matchedBaseline = new Set();
49
+ const matchedCurrent = new Set();
50
+ for (const [currentIndex, currentEntry] of currentGroup.entries()) {
51
+ const exactIndex = baselineGroup.findIndex((baselineEntry, baselineIndex) => !matchedBaseline.has(baselineIndex) &&
52
+ findingFingerprint(baselineEntry.finding) === findingFingerprint(currentEntry.finding));
53
+ if (exactIndex === -1)
54
+ continue;
55
+ matchedCurrent.add(currentIndex);
56
+ matchedBaseline.add(exactIndex);
57
+ }
58
+ for (const [currentIndex, currentEntry] of currentGroup.entries()) {
59
+ if (matchedCurrent.has(currentIndex))
60
+ continue;
61
+ const changedIndex = baselineGroup.findIndex((_baselineEntry, baselineIndex) => !matchedBaseline.has(baselineIndex));
62
+ if (changedIndex === -1)
63
+ continue;
64
+ const baselineEntry = baselineGroup[changedIndex];
65
+ matchedCurrent.add(currentIndex);
66
+ matchedBaseline.add(changedIndex);
67
+ changedFindings.push({
68
+ identity,
69
+ baseline: baselineEntry.finding,
70
+ current: currentEntry.finding,
71
+ });
72
+ }
73
+ for (const [currentIndex, currentEntry] of currentGroup.entries()) {
74
+ if (!matchedCurrent.has(currentIndex))
75
+ newFindings.push(currentEntry);
76
+ }
77
+ for (const [baselineIndex, baselineEntry] of baselineGroup.entries()) {
78
+ if (!matchedBaseline.has(baselineIndex))
79
+ removedFindings.push(baselineEntry);
80
+ }
81
+ }
82
+ return {
83
+ ...(baselineFile !== undefined ? { baselineFile } : {}),
84
+ newFindings,
85
+ removedFindings,
86
+ changedFindings,
87
+ };
88
+ }
@@ -1,5 +1,12 @@
1
- import type { RegisterContractsReportModel, RegisterContractsUnit, RoutineSummary } from './types.js';
1
+ import type { RegisterContractsJsonReportModel, RegisterContractsInferenceModel, RegisterContractsReportModel, RegisterContractsUnit, RoutineSummary } from './types.js';
2
2
  export declare function contractCarrierList(units: RegisterContractsUnit[]): string;
3
3
  export declare function renderRegisterContractsReport(model: RegisterContractsReportModel): string;
4
+ export declare function buildRegisterContractsJsonReport(model: RegisterContractsReportModel): RegisterContractsJsonReportModel;
5
+ export declare function renderRegisterContractsJsonReport(model: RegisterContractsReportModel): {
6
+ json: RegisterContractsJsonReportModel;
7
+ text: string;
8
+ };
4
9
  export declare function renderRegisterContractsInterface(summaries: RoutineSummary[]): string;
10
+ export declare function buildRegisterContractsInference(summaries: readonly RoutineSummary[]): RegisterContractsInferenceModel;
11
+ export declare function renderRegisterContractsInferenceMarkdown(model: RegisterContractsInferenceModel): string;
5
12
  export declare function renderRegisterContractsSourceBlock(summary: RoutineSummary): string[];
@@ -90,11 +90,123 @@ export function renderRegisterContractsReport(model) {
90
90
  lines.push(`Profile: ${model.profile}`);
91
91
  lines.push('');
92
92
  appendRoutineSummaries(lines, model);
93
+ appendFindings(lines, model);
93
94
  appendConflicts(lines, model);
94
95
  appendOutputCandidates(lines, model);
96
+ appendRatchet(lines, model);
95
97
  appendUnknownCalls(lines, model);
96
98
  return `${lines.join('\n')}\n`;
97
99
  }
100
+ export function buildRegisterContractsJsonReport(model) {
101
+ return {
102
+ format: 'azm-register-contracts-report',
103
+ version: 1,
104
+ entryFile: model.entryFile,
105
+ mode: model.mode,
106
+ ...(model.profile !== undefined ? { profile: model.profile } : {}),
107
+ summaries: model.summaries,
108
+ findings: (model.findings ?? []).map(jsonFinding),
109
+ ...(model.suppressedFindings !== undefined && model.suppressedFindings.length > 0
110
+ ? {
111
+ suppressedFindings: model.suppressedFindings.map((item) => ({
112
+ finding: jsonFinding(item.finding),
113
+ suppression: item.suppression,
114
+ })),
115
+ }
116
+ : {}),
117
+ unknownCalls: model.unknownCalls,
118
+ ...(model.ratchet !== undefined ? { ratchet: model.ratchet } : {}),
119
+ };
120
+ }
121
+ export function renderRegisterContractsJsonReport(model) {
122
+ const json = buildRegisterContractsJsonReport(model);
123
+ return { json, text: `${JSON.stringify(json, null, 2)}\n` };
124
+ }
125
+ function jsonFinding(finding) {
126
+ return {
127
+ kind: finding.kind,
128
+ location: jsonLocation(finding),
129
+ message: finding.message,
130
+ ...('routine' in finding && finding.routine !== undefined ? { routine: finding.routine } : {}),
131
+ ...('callTarget' in finding ? { callTarget: finding.callTarget } : {}),
132
+ ...('subject' in finding ? { subject: finding.subject } : {}),
133
+ ...(finding.carriers !== undefined ? { carriers: finding.carriers } : {}),
134
+ ...('stackBalanced' in finding ? { stackBalanced: finding.stackBalanced } : {}),
135
+ ...('hasUnknownStackEffect' in finding && finding.hasUnknownStackEffect !== undefined
136
+ ? { hasUnknownStackEffect: finding.hasUnknownStackEffect }
137
+ : {}),
138
+ ...('autoFixable' in finding && finding.autoFixable !== undefined
139
+ ? { autoFixable: finding.autoFixable }
140
+ : {}),
141
+ remediation: remediationForFinding(finding),
142
+ };
143
+ }
144
+ function jsonLocation(finding) {
145
+ return {
146
+ file: finding.file,
147
+ line: finding.line,
148
+ column: finding.column,
149
+ ...(finding.sourceUnit !== undefined ? { sourceUnit: finding.sourceUnit } : {}),
150
+ ...(finding.sourceRelation !== undefined ? { sourceRelation: finding.sourceRelation } : {}),
151
+ ...(finding.sourceUnitRelation !== undefined
152
+ ? { sourceUnitRelation: finding.sourceUnitRelation }
153
+ : {}),
154
+ };
155
+ }
156
+ function remediationForFinding(finding) {
157
+ switch (finding.kind) {
158
+ case 'missing_callee_contract':
159
+ case 'external_interface_unknown':
160
+ return {
161
+ category: 'add_contract',
162
+ hint: 'Add a routine body or .asmi extern contract for the boundary target.',
163
+ };
164
+ case 'unknown_control_flow':
165
+ return {
166
+ category: 'review_control_flow',
167
+ hint: 'Keep stack-changing paths inside one @ routine boundary or split the flow into explicit routines.',
168
+ };
169
+ case 'output_candidate':
170
+ return {
171
+ category: 'review_output_contract',
172
+ hint: finding.autoFixable === true
173
+ ? 'Generated contracts can promote this candidate to an output.'
174
+ : 'Review the caller and callee before marking this carrier as an output.',
175
+ };
176
+ case 'definite_contract_violation':
177
+ case 'flag_lifetime_risk':
178
+ return {
179
+ category: 'fix_call_or_contract',
180
+ hint: 'Fix the caller liveness issue or update the callee contract if the value is an intentional output.',
181
+ };
182
+ }
183
+ }
184
+ function appendFindings(lines, model) {
185
+ lines.push('Findings:');
186
+ if (!model.findings || model.findings.length === 0) {
187
+ lines.push(' none');
188
+ }
189
+ else {
190
+ for (const finding of model.findings) {
191
+ const carriers = finding.carriers ? `: ${list(finding.carriers)}` : '';
192
+ const target = 'callTarget' in finding ? `: ${finding.callTarget}` : '';
193
+ lines.push(` ${finding.file}:${finding.line}:${finding.column}: ${finding.kind}${target}${carriers}: ${finding.message}`);
194
+ }
195
+ }
196
+ lines.push('');
197
+ lines.push('Suppressed findings:');
198
+ if (!model.suppressedFindings || model.suppressedFindings.length === 0) {
199
+ lines.push(' none');
200
+ }
201
+ else {
202
+ for (const item of model.suppressedFindings) {
203
+ const finding = item.finding;
204
+ const target = 'callTarget' in finding ? `: ${finding.callTarget}` : '';
205
+ lines.push(` ${finding.file}:${finding.line}:${finding.column}: ${finding.kind}${target}: ${item.suppression.reason}`);
206
+ }
207
+ }
208
+ lines.push('');
209
+ }
98
210
  function appendRoutineSummaries(lines, model) {
99
211
  if (model.summaries.length === 0) {
100
212
  lines.push('Routines: none', '');
@@ -149,6 +261,27 @@ function appendUnknownCalls(lines, model) {
149
261
  }
150
262
  lines.push('');
151
263
  }
264
+ function appendRatchet(lines, model) {
265
+ if (model.ratchet === undefined)
266
+ return;
267
+ lines.push('Ratchet:');
268
+ if (model.ratchet.baselineFile !== undefined) {
269
+ lines.push(` baseline: ${model.ratchet.baselineFile}`);
270
+ }
271
+ lines.push(` new findings: ${model.ratchet.newFindings.length}`);
272
+ for (const entry of model.ratchet.newFindings) {
273
+ lines.push(` ${entry.finding.location.file}:${entry.finding.location.line}:${entry.finding.location.column}: ${entry.finding.kind}: ${entry.finding.message}`);
274
+ }
275
+ lines.push(` removed findings: ${model.ratchet.removedFindings.length}`);
276
+ for (const entry of model.ratchet.removedFindings) {
277
+ lines.push(` ${entry.finding.location.file}:${entry.finding.location.line}:${entry.finding.location.column}: ${entry.finding.kind}: ${entry.finding.message}`);
278
+ }
279
+ lines.push(` changed findings: ${model.ratchet.changedFindings.length}`);
280
+ for (const entry of model.ratchet.changedFindings) {
281
+ lines.push(` ${entry.current.location.file}:${entry.current.location.line}:${entry.current.location.column}: ${entry.current.kind}: ${entry.current.message}`);
282
+ }
283
+ lines.push('');
284
+ }
152
285
  export function renderRegisterContractsInterface(summaries) {
153
286
  const lines = [];
154
287
  for (const summary of summaries) {
@@ -160,6 +293,47 @@ export function renderRegisterContractsInterface(summaries) {
160
293
  }
161
294
  return `${lines.join('\n')}\n`;
162
295
  }
296
+ export function buildRegisterContractsInference(summaries) {
297
+ return {
298
+ format: 'azm-register-contracts-inference',
299
+ version: 1,
300
+ routines: summaries.map((summary) => {
301
+ const out = relationOutputUnits(summary.valueRelations);
302
+ const outputCandidateCarriers = summary.outputCandidates ?? [];
303
+ return {
304
+ name: summary.name,
305
+ in: summary.mayRead,
306
+ out,
307
+ clobbers: summary.mayWrite.filter((unit) => !out.includes(unit)),
308
+ preserves: summary.preserved,
309
+ confidence: out.length > 0 || summary.mayRead.length > 0 || summary.preserved.length > 0
310
+ ? 'inferred'
311
+ : 'draft',
312
+ callerImpact: {
313
+ outputCandidateCount: outputCandidateCarriers.length,
314
+ outputCandidateCarriers,
315
+ },
316
+ };
317
+ }),
318
+ };
319
+ }
320
+ export function renderRegisterContractsInferenceMarkdown(model) {
321
+ const lines = ['# AZM Register Contracts Inference', ''];
322
+ for (const routine of model.routines) {
323
+ lines.push(`## ${routine.name}`);
324
+ lines.push(`- confidence: ${routine.confidence}`);
325
+ lines.push(`- in: ${contractCarrierList(routine.in)}`);
326
+ lines.push(`- out: ${contractCarrierList(routine.out)}`);
327
+ lines.push(`- clobbers: ${contractCarrierList(routine.clobbers)}`);
328
+ lines.push(`- preserves: ${contractCarrierList(routine.preserves)}`);
329
+ lines.push(`- caller impact: ${routine.callerImpact.outputCandidateCount} output candidate carrier(s)`);
330
+ if (routine.callerImpact.outputCandidateCarriers.length > 0) {
331
+ lines.push(`- output candidates: ${contractCarrierList(routine.callerImpact.outputCandidateCarriers)}`);
332
+ }
333
+ lines.push('');
334
+ }
335
+ return `${lines.join('\n')}\n`;
336
+ }
163
337
  export function renderRegisterContractsSourceBlock(summary) {
164
338
  const entries = sourceContractEntries(summary);
165
339
  if (entries.length === 0)
@@ -4,6 +4,14 @@ const COMPACT_SOURCE_CLAUSE_RE = /^(in|out|clobbers|preserves)(?:\s+(.+))?$/i;
4
4
  const COMPACT_SOURCE_LINE_RE = /^\s*;\s*!\s*(?:in|out|maybe-out|clobbers|preserves)(?:\s|$)/i;
5
5
  const CARRIER_RE = /^\{([^}]+)\}(?:\s+(.+))?$/;
6
6
  const CONTRACT_COMMENT_KINDS = new Set(['in', 'out', 'clobbers', 'preserves']);
7
+ const FINDING_KINDS = new Set([
8
+ 'definite_contract_violation',
9
+ 'flag_lifetime_risk',
10
+ 'missing_callee_contract',
11
+ 'unknown_control_flow',
12
+ 'external_interface_unknown',
13
+ 'output_candidate',
14
+ ]);
7
15
  function parseCarrierPayload(rest) {
8
16
  if (!rest)
9
17
  return undefined;
@@ -40,6 +48,9 @@ export function parseSmartCommentLine(line) {
40
48
  }
41
49
  export function parseSmartCommentLines(line) {
42
50
  const trimmed = line.trim();
51
+ const rcIgnoreNext = parseRcIgnoreNextComment(trimmed);
52
+ if (rcIgnoreNext !== undefined)
53
+ return [rcIgnoreNext];
43
54
  const expectOut = parseExpectOutComment(trimmed);
44
55
  if (expectOut !== undefined)
45
56
  return [expectOut];
@@ -56,6 +67,17 @@ export function parseSmartCommentLines(line) {
56
67
  }
57
68
  return [];
58
69
  }
70
+ function parseRcIgnoreNextComment(trimmed) {
71
+ const match = /^;?\s*!\s*rc-ignore-next\s+([A-Za-z0-9_]+)\s*:\s*(.+)$/i.exec(trimmed);
72
+ if (match === null)
73
+ return undefined;
74
+ const findingKind = match[1];
75
+ const reason = match[2]?.trim();
76
+ if (!FINDING_KINDS.has(findingKind) || reason === undefined || reason.length === 0) {
77
+ return undefined;
78
+ }
79
+ return { kind: 'rcIgnoreNext', findingKind, reason };
80
+ }
59
81
  function parseSemicolonSeparatedSourceComments(trimmed) {
60
82
  const sourcePrefix = /^;?\s*!\s*/.exec(trimmed);
61
83
  if (sourcePrefix === null)
@@ -1,5 +1,5 @@
1
1
  import { getZ80InstructionEffect } from '../z80/effects.js';
2
- import { precedingCServiceName } from './boundaryHints.js';
2
+ import { precedingCServiceName, precedingRegisterImmediateValue } from './boundaryHints.js';
3
3
  import { instructionHead } from './instruction-head.js';
4
4
  import { rstServiceTargetName, rstTargetName } from './profiles.js';
5
5
  export function boundarySummary(routine, index, summaries) {
@@ -35,6 +35,13 @@ function rstBoundarySummary(routine, index, effect, summaries) {
35
35
  summaries.get(rstTargetName(effect.control.vector)));
36
36
  }
37
37
  function rstServiceBoundarySummary(routine, index, vector, summaries) {
38
- const service = precedingCServiceName(routine.instructions[index - 1]);
38
+ const previous = routine.instructions[index - 1];
39
+ const numericService = precedingRegisterImmediateValue(previous, 'C');
40
+ if (numericService !== undefined) {
41
+ const numericSummary = summaries.get(rstServiceTargetName(vector, String(numericService)));
42
+ if (numericSummary !== undefined)
43
+ return numericSummary;
44
+ }
45
+ const service = precedingCServiceName(previous);
39
46
  return service ? summaries.get(rstServiceTargetName(vector, service)) : undefined;
40
47
  }
@@ -1,6 +1,6 @@
1
1
  import type { Diagnostic } from '../model/diagnostic.js';
2
2
  import type { LoadedProgram } from '../tooling/api.js';
3
- import type { AnalyzeRegisterContractsOptions, RegisterContractsOutputCandidate, RegisterContractsUnit } from './types.js';
3
+ import type { AnalyzeRegisterContractsOptions, RegisterContractsFinding, RegisterContractsOutputCandidate, RegisterContractsUnit } from './types.js';
4
4
  export interface RegisterContractsTextEdit {
5
5
  file: string;
6
6
  line: number;
@@ -42,6 +42,7 @@ export interface AnalyzeRegisterContractsForToolsOptions extends Omit<AnalyzeReg
42
42
  export type AnalyzeRegisterCareForToolsOptions = AnalyzeRegisterContractsForToolsOptions;
43
43
  export interface AnalyzeRegisterContractsForToolsResult {
44
44
  diagnostics: Diagnostic[];
45
+ findings: RegisterContractsFinding[];
45
46
  outputCandidates: RegisterContractsOutputCandidate[];
46
47
  candidateDiagnostics: RegisterContractsCandidateDiagnostic[];
47
48
  codeActions: RegisterContractsCodeAction[];
@@ -46,12 +46,14 @@ export function analyzeRegisterContractsForTools(loaded, options) {
46
46
  ? baseResultOptions
47
47
  : { ...baseResultOptions, registerContractsProfile: profile });
48
48
  const outputCandidates = result.outputCandidates ?? [];
49
+ const findings = result.findings ?? [];
49
50
  const candidateDiagnostics = outputCandidates.map(diagnosticForOutputCandidate);
50
51
  const codeActions = outputCandidates
51
52
  .filter((candidate) => candidate.autoFixable === true)
52
53
  .map(codeActionForOutputCandidate);
53
54
  return {
54
55
  diagnostics: result.diagnostics,
56
+ findings,
55
57
  outputCandidates,
56
58
  candidateDiagnostics,
57
59
  codeActions,