@jhlagado/azm 0.2.13 → 0.2.15

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.
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';
2
2
  import { normalize } from 'node:path';
3
3
  import { analyzeRegisterContracts } from './register-contracts/analyze.js';
4
4
  import { parseAcceptedOutputCandidates } from './register-contracts/accept-output.js';
5
- import { parseInterfaceContracts } from './register-contracts/interfaceContracts.js';
5
+ import { parseInterfaceContractsDetailed } from './register-contracts/interfaceContracts.js';
6
6
  export function shouldAnalyzeRegisterContracts(options) {
7
7
  const registerContractsMode = options.registerContracts ?? options.registerCare ?? 'off';
8
8
  return (registerContractsMode !== 'off' ||
@@ -19,7 +19,7 @@ export function shouldAnalyzeRegisterContracts(options) {
19
19
  export async function runRegisterContracts(loadedProgram, options) {
20
20
  const diagnostics = [];
21
21
  const artifacts = [];
22
- const interfaceContracts = await loadInterfaceContracts(options.registerContractsInterfaces ?? options.registerCareInterfaces ?? [], diagnostics);
22
+ const parsedInterfaces = await loadInterfaceContracts(options.registerContractsInterfaces ?? options.registerCareInterfaces ?? [], diagnostics);
23
23
  if (hasErrors(diagnostics)) {
24
24
  return { diagnostics, artifacts };
25
25
  }
@@ -49,7 +49,12 @@ export async function runRegisterContracts(loadedProgram, options) {
49
49
  registerContractsProfile: options.registerContractsProfile ?? options.registerCareProfile,
50
50
  }
51
51
  : {}),
52
- ...(interfaceContracts.length > 0 ? { interfaceContracts } : {}),
52
+ ...(parsedInterfaces.contracts.length > 0
53
+ ? { interfaceContracts: parsedInterfaces.contracts }
54
+ : {}),
55
+ ...(parsedInterfaces.serviceRanges.length > 0
56
+ ? { interfaceServiceRanges: parsedInterfaces.serviceRanges }
57
+ : {}),
53
58
  ...(baselineReport !== undefined ? { baselineReport } : {}),
54
59
  ...(options.registerContractsBaseline !== undefined
55
60
  ? { baselineFile: normalize(options.registerContractsBaseline) }
@@ -121,6 +126,7 @@ async function loadBaselineReport(rawPath, diagnostics) {
121
126
  }
122
127
  async function loadInterfaceContracts(interfaces, diagnostics) {
123
128
  const interfaceContracts = [];
129
+ const serviceRanges = [];
124
130
  for (const rawInterface of interfaces) {
125
131
  const contractPath = normalize(rawInterface);
126
132
  if (contractPath.slice(-5).toLowerCase() !== '.asmi') {
@@ -133,11 +139,13 @@ async function loadInterfaceContracts(interfaces, diagnostics) {
133
139
  continue;
134
140
  }
135
141
  const interfaceText = await readFile(contractPath, 'utf8');
136
- for (const contract of parseInterfaceContracts(interfaceText, contractPath).values()) {
142
+ const parsed = parseInterfaceContractsDetailed(interfaceText, contractPath);
143
+ for (const contract of parsed.contracts.values()) {
137
144
  interfaceContracts.push(contract);
138
145
  }
146
+ serviceRanges.push(...parsed.serviceRanges);
139
147
  }
140
- return interfaceContracts;
148
+ return { contracts: interfaceContracts, serviceRanges };
141
149
  }
142
150
  function hasErrors(diagnostics) {
143
151
  return diagnostics.some((diagnostic) => diagnostic.severity === 'error');
@@ -21,7 +21,8 @@ export function analyzeRegisterContracts(loaded, options) {
21
21
  }
22
22
  }
23
23
  const profileSummaries = buildProfileSummaries(options.registerContractsProfile);
24
- let summaries = buildSummaries(program.routines, contractMap, profileSummaries);
24
+ const interfaceServiceRanges = options.interfaceServiceRanges ?? [];
25
+ let summaries = buildSummaries(program.routines, contractMap, profileSummaries, interfaceServiceRanges);
25
26
  summaries = withAcceptedOutputs(summaries, options.acceptedOutputCandidates);
26
27
  let summariesByName = buildSummaryByName(program.routines, summaries, profileSummaries);
27
28
  const knownRoutines = knownRoutineNames(program.routines, contractMap.keys(), options.registerContractsProfile);
@@ -53,7 +54,7 @@ export function analyzeRegisterContracts(loaded, options) {
53
54
  summariesByName = buildSummaryByName(program.routines, summaries, profileSummaries);
54
55
  }
55
56
  const conflicts = shouldBuildOutputCandidates
56
- ? program.routines.flatMap((routine) => findRegisterContractsConflicts(routine, summariesByName, smartComments))
57
+ ? program.routines.flatMap((routine) => findRegisterContractsConflicts(routine, summariesByName, smartComments, interfaceServiceRanges))
57
58
  : [];
58
59
  const { outputCandidates: outputCandidatesWithAutoFixability, outputCandidateFixability } = outputCandidatesWithFixability(program.routines, outputCandidatesForPromotion);
59
60
  const { outputCandidates: allOutputCandidatesWithAutoFixability } = outputCandidatesWithFixability(program.routines, outputCandidates);
@@ -4,8 +4,10 @@ function unique(items) {
4
4
  export function labelIndex(routine) {
5
5
  const out = new Map();
6
6
  routine.instructions.forEach((item, index) => {
7
- for (const label of item.labels)
8
- out.set(label, index);
7
+ for (const label of item.labels) {
8
+ if (!out.has(label))
9
+ out.set(label, index);
10
+ }
9
11
  });
10
12
  return out;
11
13
  }
@@ -1,2 +1,7 @@
1
- import type { RoutineContract } from './types.js';
1
+ import type { RegisterContractsServiceRangeContract, RoutineContract } from './types.js';
2
+ export interface ParsedInterfaceContracts {
3
+ contracts: Map<string, RoutineContract>;
4
+ serviceRanges: RegisterContractsServiceRangeContract[];
5
+ }
2
6
  export declare function parseInterfaceContracts(text: string, file?: string): Map<string, RoutineContract>;
7
+ export declare function parseInterfaceContractsDetailed(text: string, file?: string): ParsedInterfaceContracts;
@@ -9,8 +9,12 @@ const INTERFACE_CONTRACT_BUILDERS = {
9
9
  preserves: (carriers) => ({ kind: 'preserves', carriers }),
10
10
  };
11
11
  export function parseInterfaceContracts(text, file = '<register-contracts-interface>') {
12
+ return parseInterfaceContractsDetailed(text, file).contracts;
13
+ }
14
+ export function parseInterfaceContractsDetailed(text, file = '<register-contracts-interface>') {
12
15
  const comments = [];
13
16
  const serviceAliases = new Map();
17
+ const serviceRanges = new Map();
14
18
  const lines = text.split(/\r?\n/u);
15
19
  for (const [index, line] of lines.entries()) {
16
20
  const trimmed = line.trim();
@@ -28,6 +32,10 @@ export function parseInterfaceContracts(text, file = '<register-contracts-interf
28
32
  if (serviceAliasesForComment !== undefined) {
29
33
  serviceAliases.set(comment.name, serviceAliasesForComment);
30
34
  }
35
+ const serviceRangeForComment = parseInterfaceServiceRange(trimmed);
36
+ if (serviceRangeForComment !== undefined) {
37
+ serviceRanges.set(comment.name, serviceRangeForComment.range);
38
+ }
31
39
  }
32
40
  comments.push({ file, line: index + 1, comment });
33
41
  }
@@ -45,7 +53,10 @@ export function parseInterfaceContracts(text, file = '<register-contracts-interf
45
53
  if (hasContractContent(contract))
46
54
  out.set(name, contract);
47
55
  }
48
- return out;
56
+ return {
57
+ contracts: out,
58
+ serviceRanges: [...serviceRanges.values()].filter((range) => out.has(range.target)),
59
+ };
49
60
  }
50
61
  function parseInterfaceContractLine(line) {
51
62
  const trimmed = line.trim();
@@ -74,6 +85,9 @@ function parseInterfaceServiceAliases(trimmed) {
74
85
  return parseInterfaceService(trimmed)?.aliases;
75
86
  }
76
87
  function parseInterfaceService(trimmed) {
88
+ const range = parseInterfaceServiceRange(trimmed);
89
+ if (range !== undefined)
90
+ return { primary: range.primary, aliases: range.aliases };
77
91
  const match = /^service\s+rst\s+(\S+)\s+(\S+)\s+(\S+)(?:\s+(\S+))?\s*$/i.exec(trimmed);
78
92
  if (match === null)
79
93
  return undefined;
@@ -87,6 +101,35 @@ function parseInterfaceService(trimmed) {
87
101
  const aliases = name === undefined ? [] : [rstServiceTargetName(vector, name)];
88
102
  return { primary, aliases };
89
103
  }
104
+ function parseInterfaceServiceRange(trimmed) {
105
+ const match = /^service\s+rst\s+(\S+)\s+(\S+)\s+>=\s*(\S+)(?:\s+(\S+))?\s*$/i.exec(trimmed);
106
+ if (match === null)
107
+ return undefined;
108
+ const vector = parseInterfaceNumber(match[1]);
109
+ const selector = match[2].toUpperCase();
110
+ const min = parseInterfaceNumber(match[3]);
111
+ if (vector === undefined || min === undefined || selector !== 'C')
112
+ return undefined;
113
+ const primary = rstServiceRangeTargetName(vector, min);
114
+ const explicitName = match[4];
115
+ const aliases = explicitName === undefined ? [] : [explicitName];
116
+ return {
117
+ primary,
118
+ aliases,
119
+ range: {
120
+ vector,
121
+ selector: 'C',
122
+ min,
123
+ target: explicitName ?? primary,
124
+ },
125
+ };
126
+ }
127
+ function rstServiceRangeTargetName(vector, min) {
128
+ return `${rstTargetPrefix(vector)}:C>=$${min.toString(16).toUpperCase().padStart(2, '0')}`;
129
+ }
130
+ function rstTargetPrefix(vector) {
131
+ return `RST_$${vector.toString(16).toUpperCase().padStart(2, '0')}`;
132
+ }
90
133
  function parseInterfaceNumber(raw) {
91
134
  const trimmed = raw.trim();
92
135
  const value = trimmed.startsWith('$')
@@ -1,3 +1,3 @@
1
- import type { LocatedSmartComment, RegisterContractsConflict, RegisterContractsOutputCandidate, RegisterContractsRoutine, RoutineSummary } from './types.js';
2
- export declare function findRegisterContractsConflicts(routine: RegisterContractsRoutine, summaries: Map<string, RoutineSummary>, hints: LocatedSmartComment[]): RegisterContractsConflict[];
1
+ import type { LocatedSmartComment, RegisterContractsConflict, RegisterContractsOutputCandidate, RegisterContractsRoutine, RegisterContractsServiceRangeContract, RoutineSummary } from './types.js';
2
+ export declare function findRegisterContractsConflicts(routine: RegisterContractsRoutine, summaries: Map<string, RoutineSummary>, hints: LocatedSmartComment[], serviceRanges?: readonly RegisterContractsServiceRangeContract[]): RegisterContractsConflict[];
3
3
  export declare function findCallerOutputCandidateObservations(routines: RegisterContractsRoutine[], summaries: Map<string, RoutineSummary>): RegisterContractsOutputCandidate[];
@@ -5,11 +5,11 @@ import { rstDispatcherServiceTargetNames, rstServiceTargetName, rstTargetName, }
5
5
  function unique(units) {
6
6
  return [...new Set(units)];
7
7
  }
8
- function boundaryTarget(routine, index, effect) {
8
+ function boundaryTarget(routine, index, effect, serviceRanges = []) {
9
9
  const item = routine.instructions[index];
10
10
  return (callBoundaryTarget(effect) ??
11
11
  tailJumpBoundaryTarget(item, effect) ??
12
- rstBoundaryTarget(routine, index, effect));
12
+ rstBoundaryTarget(routine, index, effect, serviceRanges));
13
13
  }
14
14
  function callBoundaryTarget(effect) {
15
15
  return effect.control.kind === 'call' && effect.control.target
@@ -36,23 +36,23 @@ function isTailJumpBoundary(item, effect) {
36
36
  effect.control.target !== undefined &&
37
37
  !effect.control.target.startsWith('.'));
38
38
  }
39
- function rstBoundaryTarget(routine, index, effect) {
39
+ function rstBoundaryTarget(routine, index, effect, serviceRanges) {
40
40
  if (effect.control.kind !== 'rst' || effect.control.vector === undefined)
41
41
  return undefined;
42
42
  const target = rstTargetName(effect.control.vector);
43
43
  return {
44
- targets: rstBoundaryTargets(routine, index, effect.control.vector, target),
44
+ targets: rstBoundaryTargets(routine, index, effect.control.vector, target, serviceRanges),
45
45
  conditional: false,
46
46
  subject: target,
47
47
  };
48
48
  }
49
- function rstBoundaryTargets(routine, index, vector, fallbackTarget) {
49
+ function rstBoundaryTargets(routine, index, vector, fallbackTarget, serviceRanges) {
50
50
  const previous = routine.instructions[index - 1];
51
51
  const service = precedingCServiceName(previous);
52
52
  const numericService = precedingRegisterImmediateValue(previous, 'C');
53
53
  return [
54
54
  ...(numericService !== undefined ? [rstServiceTargetName(vector, String(numericService))] : []),
55
- ...rstDispatcherServiceTargetNames(vector, (register) => precedingRegisterImmediateValue(previous, register)),
55
+ ...rstDispatcherServiceTargetNames(vector, (register) => precedingRegisterImmediateValue(previous, register), serviceRanges),
56
56
  ...(service ? [rstServiceTargetName(vector, service)] : []),
57
57
  fallbackTarget,
58
58
  ];
@@ -170,12 +170,12 @@ function liveSetsForRoutine(routine, summaries, hints = []) {
170
170
  }
171
171
  return { liveIn, liveOut };
172
172
  }
173
- function resolvedBoundariesForRoutine(routine, summaries) {
173
+ function resolvedBoundariesForRoutine(routine, summaries, serviceRanges = []) {
174
174
  const out = [];
175
175
  for (let index = 0; index < routine.instructions.length; index += 1) {
176
176
  const item = routine.instructions[index];
177
177
  const effect = getZ80InstructionEffect(item.instruction);
178
- const boundary = boundaryTarget(routine, index, effect);
178
+ const boundary = boundaryTarget(routine, index, effect, serviceRanges);
179
179
  if (!boundary)
180
180
  continue;
181
181
  const resolved = summaryForBoundary(boundary, summaries);
@@ -185,10 +185,10 @@ function resolvedBoundariesForRoutine(routine, summaries) {
185
185
  }
186
186
  return out;
187
187
  }
188
- export function findRegisterContractsConflicts(routine, summaries, hints) {
188
+ export function findRegisterContractsConflicts(routine, summaries, hints, serviceRanges = []) {
189
189
  const conflicts = [];
190
190
  const { liveOut } = liveSetsForRoutine(routine, summaries, hints);
191
- for (const { item, index, boundary, target, summary } of resolvedBoundariesForRoutine(routine, summaries)) {
191
+ for (const { item, index, boundary, target, summary } of resolvedBoundariesForRoutine(routine, summaries, serviceRanges)) {
192
192
  const accepted = new Set();
193
193
  for (const unit of hintUnitsForLine(hints, item.file, item.line))
194
194
  accepted.add(unit);
@@ -1,4 +1,4 @@
1
- import type { RegisterContractsUnit, RoutineSummary } from './types.js';
1
+ import type { RegisterContractsServiceRangeContract, RegisterContractsUnit, RoutineSummary } from './types.js';
2
2
  export interface RegisterContractsProfileSummary {
3
3
  name: 'mon3';
4
4
  rst: Map<number, RoutineSummary>;
@@ -6,9 +6,14 @@ export interface RegisterContractsProfileSummary {
6
6
  rstDispatchers: Map<number, {
7
7
  selector: RegisterContractsUnit;
8
8
  services: Map<number, RoutineSummary>;
9
+ rangeServices?: {
10
+ min: number;
11
+ max?: number;
12
+ summary: RoutineSummary;
13
+ }[];
9
14
  }>;
10
15
  }
11
16
  export declare function rstTargetName(vector: number): string;
12
17
  export declare function rstServiceTargetName(vector: number, service: string): string;
13
- export declare function rstDispatcherServiceTargetNames(vector: number, selectorValue: (register: RegisterContractsUnit) => number | undefined): string[];
18
+ export declare function rstDispatcherServiceTargetNames(vector: number, selectorValue: (register: RegisterContractsUnit) => number | undefined, configuredRanges?: readonly RegisterContractsServiceRangeContract[]): string[];
14
19
  export declare function getRegisterContractsProfile(name: 'mon3' | undefined): RegisterContractsProfileSummary | undefined;
@@ -89,21 +89,35 @@ function mon3ApiServices(overrides) {
89
89
  'WRITE_SECTOR',
90
90
  'RGB_SCAN',
91
91
  ];
92
- return new Map(names.map((serviceName, api) => [
92
+ const services = new Map(names.map((serviceName, api) => [
93
93
  api,
94
94
  overrides.get(api) ?? conservativeMon3ApiSummary(api, serviceName),
95
95
  ]));
96
+ for (const [api, summary] of overrides) {
97
+ services.set(api, summary);
98
+ }
99
+ return services;
96
100
  }
97
- export function rstDispatcherServiceTargetNames(vector, selectorValue) {
101
+ export function rstDispatcherServiceTargetNames(vector, selectorValue, configuredRanges = []) {
98
102
  const mon3 = getRegisterContractsProfile('mon3');
99
103
  const dispatcher = mon3?.rstDispatchers.get(vector);
100
- if (dispatcher === undefined)
101
- return [];
102
- const value = selectorValue(dispatcher.selector);
104
+ const selector = dispatcher?.selector ?? 'C';
105
+ const value = selectorValue(selector);
103
106
  if (value === undefined)
104
107
  return [];
105
- const service = dispatcher.services.get(value);
106
- return service ? [service.name] : [];
108
+ const service = dispatcher?.services.get(value);
109
+ const profileRangeService = dispatcher?.rangeServices?.find((entry) => rangeMatches(value, entry));
110
+ const configuredRangeServices = configuredRanges
111
+ .filter((entry) => entry.vector === vector && entry.selector === selector && rangeMatches(value, entry))
112
+ .map((entry) => entry.target);
113
+ return [
114
+ ...(service ? [service.name] : []),
115
+ ...(profileRangeService ? [profileRangeService.summary.name] : []),
116
+ ...configuredRangeServices,
117
+ ];
118
+ }
119
+ function rangeMatches(value, range) {
120
+ return value >= range.min && (range.max === undefined || value <= range.max);
107
121
  }
108
122
  export function getRegisterContractsProfile(name) {
109
123
  if (name !== 'mon3')
@@ -168,6 +182,17 @@ export function getRegisterContractsProfile(name) {
168
182
  stackBalanced: true,
169
183
  hasUnknownStackEffect: false,
170
184
  };
185
+ const bankCall = {
186
+ name: mon3ApiTargetName(0x53, 'BANK_CALL'),
187
+ mayRead: ['B', 'C', 'H', 'L'],
188
+ mayWrite: ['A', 'B', 'C', 'D', 'E', 'H', 'L', ...FLAG_UNITS],
189
+ mayOutput: ['A', 'carry'],
190
+ preserved: [],
191
+ valueRelations: [{ out: ['A', 'carry'], from: [] }],
192
+ stackBalanced: true,
193
+ hasUnknownStackEffect: false,
194
+ consumesStackFrame: ['AF', 'DE', 'HL'],
195
+ };
171
196
  return {
172
197
  name: 'mon3',
173
198
  rst: new Map([
@@ -212,6 +237,7 @@ export function getRegisterContractsProfile(name) {
212
237
  [16, scanKeys],
213
238
  [18, matrixScan],
214
239
  [54, parseMatrixScan],
240
+ [0x53, bankCall],
215
241
  ])),
216
242
  },
217
243
  ],
@@ -1,6 +1,6 @@
1
- import type { RegisterContractsRoutine, RoutineContract, RoutineSummary } from './types.js';
1
+ import type { RegisterContractsRoutine, RegisterContractsServiceRangeContract, RoutineContract, RoutineSummary } from './types.js';
2
2
  export declare function summariesWithExternalContracts(summaries: RoutineSummary[], contracts: Map<string, RoutineContract>, routineNameSet: Set<string>): RoutineSummary[];
3
- export declare function inferRoutineSummariesToFixedPoint(routines: RegisterContractsRoutine[], contracts: Map<string, RoutineContract>, routineNameSet: Set<string>, profileSummaries: RoutineSummary[]): Array<{
3
+ export declare function inferRoutineSummariesToFixedPoint(routines: RegisterContractsRoutine[], contracts: Map<string, RoutineContract>, routineNameSet: Set<string>, profileSummaries: RoutineSummary[], serviceRanges?: readonly RegisterContractsServiceRangeContract[]): Array<{
4
4
  routine: RegisterContractsRoutine;
5
5
  summary: RoutineSummary;
6
6
  }>;
@@ -55,9 +55,9 @@ function buildOptimisticInternalBoundarySummaryMap(routines) {
55
55
  }
56
56
  return out;
57
57
  }
58
- function summarizeRoutines(routines, contracts, boundarySummaryMap = new Map()) {
58
+ function summarizeRoutines(routines, contracts, boundarySummaryMap = new Map(), serviceRanges = []) {
59
59
  return routines.map((routine) => {
60
- const inferred = inferRoutineSummary(routine, boundarySummaryMap);
60
+ const inferred = inferRoutineSummary(routine, boundarySummaryMap, serviceRanges);
61
61
  const contract = contractForRoutine(routine, contracts);
62
62
  return { routine, summary: contract ? applyRoutineContract(inferred, contract) : inferred };
63
63
  });
@@ -82,13 +82,13 @@ function summaryFingerprint(summary) {
82
82
  function routineSummariesFingerprint(routineSummaries) {
83
83
  return routineSummaries.map((item) => summaryFingerprint(item.summary)).join('\n');
84
84
  }
85
- export function inferRoutineSummariesToFixedPoint(routines, contracts, routineNameSet, profileSummaries) {
86
- let routineSummaries = summarizeRoutines(routines, contracts, buildOptimisticInternalBoundarySummaryMap(routines));
85
+ export function inferRoutineSummariesToFixedPoint(routines, contracts, routineNameSet, profileSummaries, serviceRanges = []) {
86
+ let routineSummaries = summarizeRoutines(routines, contracts, buildOptimisticInternalBoundarySummaryMap(routines), serviceRanges);
87
87
  const maxPasses = Math.max(2, routines.length + 2);
88
88
  for (let pass = 0; pass < maxPasses; pass += 1) {
89
89
  const summaries = summariesWithExternalContracts(routineSummaries.map((item) => item.summary), contracts, routineNameSet);
90
90
  const boundarySummaryMap = buildBoundarySummaryMap(summaries, routineSummaries, profileSummaries);
91
- const nextRoutineSummaries = summarizeRoutines(routines, contracts, boundarySummaryMap);
91
+ const nextRoutineSummaries = summarizeRoutines(routines, contracts, boundarySummaryMap, serviceRanges);
92
92
  if (routineSummariesFingerprint(nextRoutineSummaries) ===
93
93
  routineSummariesFingerprint(routineSummaries)) {
94
94
  return nextRoutineSummaries;
@@ -1,9 +1,9 @@
1
1
  import type { Diagnostic } from '../model/diagnostic.js';
2
- import type { AnalyzeRegisterContractsOptions, RegisterContractsDirectCall, RegisterContractsRoutine, RoutineContract, RoutineSummary, RegisterContractsUnit, RegisterContractsOutputCandidate } from './types.js';
2
+ import type { AnalyzeRegisterContractsOptions, RegisterContractsDirectCall, RegisterContractsRoutine, RegisterContractsServiceRangeContract, RoutineContract, RoutineSummary, RegisterContractsUnit, RegisterContractsOutputCandidate } from './types.js';
3
3
  export declare function buildProfileSummaries(profileName: AnalyzeRegisterContractsOptions['registerContractsProfile']): RoutineSummary[];
4
4
  export declare function buildProfileSummaryLookup(profileName: AnalyzeRegisterContractsOptions['registerContractsProfile']): Map<string, RoutineSummary>;
5
5
  export declare function routineNames(routines: readonly RegisterContractsRoutine[]): string[];
6
- export declare function buildSummaries(routines: readonly RegisterContractsRoutine[], contractMap: Map<string, RoutineContract>, profileSummaries?: readonly RoutineSummary[]): RoutineSummary[];
6
+ export declare function buildSummaries(routines: readonly RegisterContractsRoutine[], contractMap: Map<string, RoutineContract>, profileSummaries?: readonly RoutineSummary[], serviceRanges?: readonly RegisterContractsServiceRangeContract[]): RoutineSummary[];
7
7
  export declare function buildSummaryByName(routines: readonly RegisterContractsRoutine[], summaries: readonly RoutineSummary[], profileSummaries?: readonly RoutineSummary[]): Map<string, RoutineSummary>;
8
8
  export declare function withAcceptedOutputs(summaries: readonly RoutineSummary[], acceptedOutputCandidates: ReadonlyMap<string, RegisterContractsUnit[]> | undefined): RoutineSummary[];
9
9
  export declare function unknownBoundaryDiagnostics(directBoundaries: readonly RegisterContractsDirectCall[], knownRoutines: ReadonlySet<string>, severity?: Diagnostic['severity']): Diagnostic[];
@@ -29,6 +29,7 @@ export function buildProfileSummaries(profileName) {
29
29
  ...profile.rstServices.values(),
30
30
  ...[...profile.rstDispatchers.values()].flatMap((dispatcher) => [
31
31
  ...dispatcher.services.values(),
32
+ ...(dispatcher.rangeServices?.map((rangeService) => rangeService.summary) ?? []),
32
33
  ]),
33
34
  ];
34
35
  }
@@ -47,17 +48,18 @@ export function buildProfileSummaryLookup(profileName) {
47
48
  for (const summary of dispatcher.services.values()) {
48
49
  out.set(summary.name, summary);
49
50
  }
51
+ for (const rangeService of dispatcher.rangeServices ?? []) {
52
+ out.set(rangeService.summary.name, rangeService.summary);
53
+ }
50
54
  }
51
55
  return out;
52
56
  }
53
57
  export function routineNames(routines) {
54
58
  return routines.flatMap((routine) => boundaryLabels(routine));
55
59
  }
56
- export function buildSummaries(routines, contractMap, profileSummaries = []) {
60
+ export function buildSummaries(routines, contractMap, profileSummaries = [], serviceRanges = []) {
57
61
  const names = routineNameSet(routines);
58
- const routineSummaries = inferRoutineSummariesToFixedPoint([...routines], contractMap, names, [
59
- ...profileSummaries,
60
- ]);
62
+ const routineSummaries = inferRoutineSummariesToFixedPoint([...routines], contractMap, names, [...profileSummaries], serviceRanges);
61
63
  const summaries = routineSummaries.map((item) => item.summary);
62
64
  return summariesWithExternalContracts(summaries, contractMap, names);
63
65
  }
@@ -1,2 +1,2 @@
1
- import type { RegisterContractsRoutine, RoutineSummary } from './types.js';
2
- export declare function boundarySummary(routine: RegisterContractsRoutine, index: number, summaries: ReadonlyMap<string, RoutineSummary>): RoutineSummary | undefined;
1
+ import type { RegisterContractsRoutine, RegisterContractsServiceRangeContract, RoutineSummary } from './types.js';
2
+ export declare function boundarySummary(routine: RegisterContractsRoutine, index: number, summaries: ReadonlyMap<string, RoutineSummary>, serviceRanges?: readonly RegisterContractsServiceRangeContract[]): RoutineSummary | undefined;
@@ -1,15 +1,15 @@
1
1
  import { getZ80InstructionEffect } from '../z80/effects.js';
2
2
  import { precedingCServiceName, precedingRegisterImmediateValue } from './boundaryHints.js';
3
3
  import { instructionHead } from './instruction-head.js';
4
- import { rstServiceTargetName, rstTargetName } from './profiles.js';
5
- export function boundarySummary(routine, index, summaries) {
4
+ import { rstDispatcherServiceTargetNames, rstServiceTargetName, rstTargetName } from './profiles.js';
5
+ export function boundarySummary(routine, index, summaries, serviceRanges = []) {
6
6
  const item = routine.instructions[index];
7
7
  if (!item)
8
8
  return undefined;
9
9
  const effect = getZ80InstructionEffect(item.instruction);
10
10
  return (callBoundarySummary(effect, summaries) ??
11
11
  jumpBoundarySummary(routine, item, effect, summaries) ??
12
- rstBoundarySummary(routine, index, effect, summaries));
12
+ rstBoundarySummary(routine, index, effect, summaries, serviceRanges));
13
13
  }
14
14
  function callBoundarySummary(effect, summaries) {
15
15
  return effect.control.kind === 'call' && effect.control.target
@@ -28,20 +28,31 @@ function isExternalTailJump(routine, item, effect) {
28
28
  !effect.control.target.startsWith('.') &&
29
29
  !routine.labels.includes(effect.control.target));
30
30
  }
31
- function rstBoundarySummary(routine, index, effect, summaries) {
31
+ function rstBoundarySummary(routine, index, effect, summaries, serviceRanges) {
32
32
  if (effect.control.kind !== 'rst' || effect.control.vector === undefined)
33
33
  return undefined;
34
- return (rstServiceBoundarySummary(routine, index, effect.control.vector, summaries) ??
34
+ return (rstServiceBoundarySummary(routine, index, effect.control.vector, summaries, serviceRanges) ??
35
35
  summaries.get(rstTargetName(effect.control.vector)));
36
36
  }
37
- function rstServiceBoundarySummary(routine, index, vector, summaries) {
37
+ function rstServiceBoundarySummary(routine, index, vector, summaries, serviceRanges) {
38
38
  const previous = routine.instructions[index - 1];
39
39
  const numericService = precedingRegisterImmediateValue(previous, 'C');
40
40
  if (numericService !== undefined) {
41
41
  const numericSummary = summaries.get(rstServiceTargetName(vector, String(numericService)));
42
42
  if (numericSummary !== undefined)
43
43
  return numericSummary;
44
+ const profileTarget = firstSummary(rstDispatcherServiceTargetNames(vector, (register) => (register === 'C' ? numericService : undefined), serviceRanges), summaries);
45
+ if (profileTarget !== undefined)
46
+ return profileTarget;
44
47
  }
45
48
  const service = precedingCServiceName(previous);
46
49
  return service ? summaries.get(rstServiceTargetName(vector, service)) : undefined;
47
50
  }
51
+ function firstSummary(names, summaries) {
52
+ for (const name of names) {
53
+ const summary = summaries.get(name);
54
+ if (summary !== undefined)
55
+ return summary;
56
+ }
57
+ return undefined;
58
+ }
@@ -1,3 +1,3 @@
1
1
  export { applyRoutineContract } from './summary-contract.js';
2
- import type { RegisterContractsRoutine, RoutineSummary } from './types.js';
3
- export declare function inferRoutineSummary(routine: RegisterContractsRoutine, boundarySummaries?: ReadonlyMap<string, RoutineSummary>): RoutineSummary;
2
+ import type { RegisterContractsRoutine, RegisterContractsServiceRangeContract, RoutineSummary } from './types.js';
3
+ export declare function inferRoutineSummary(routine: RegisterContractsRoutine, boundarySummaries?: ReadonlyMap<string, RoutineSummary>, serviceRanges?: readonly RegisterContractsServiceRangeContract[]): RoutineSummary;
@@ -1,4 +1,5 @@
1
1
  import { getZ80InstructionEffect } from '../z80/effects.js';
2
+ import { instructionSuccessors, labelIndex } from './controlFlow.js';
2
3
  import { instructionHead } from './instruction-head.js';
3
4
  import { isAccumulatorSelfOperand, isImmediateZeroOperand, isPureTokenTransferInstruction, isRegisterOperand, } from './instruction-predicates.js';
4
5
  import { boundarySummary } from './summary-boundary.js';
@@ -91,7 +92,7 @@ function applyStackPop(tokens, consumedProduced, intendedProduced, stack, state,
91
92
  }
92
93
  return;
93
94
  }
94
- if (popped.length !== units.length) {
95
+ if (popped.tokens.length !== units.length) {
95
96
  for (const unit of units) {
96
97
  tokens.set(unit, { origin: 'unknown' });
97
98
  consumedProduced.delete(unit);
@@ -100,11 +101,35 @@ function applyStackPop(tokens, consumedProduced, intendedProduced, stack, state,
100
101
  return;
101
102
  }
102
103
  units.forEach((unit, idx) => {
103
- tokens.set(unit, popped[idx] ?? { origin: 'unknown' });
104
+ tokens.set(unit, popped.tokens[idx] ?? { origin: 'unknown' });
104
105
  consumedProduced.delete(unit);
105
106
  intendedProduced.delete(unit);
106
107
  });
107
108
  }
109
+ function stackFrameUnits(frameUnit) {
110
+ if (frameUnit === 'AF')
111
+ return ['A', 'sign', 'zero', 'halfCarry', 'parity', 'carry'];
112
+ if (frameUnit === 'BC')
113
+ return ['B', 'C'];
114
+ if (frameUnit === 'DE')
115
+ return ['D', 'E'];
116
+ if (frameUnit === 'HL')
117
+ return ['H', 'L'];
118
+ if (frameUnit === 'IX')
119
+ return ['IXH', 'IXL'];
120
+ return ['IYH', 'IYL'];
121
+ }
122
+ function consumeKnownBoundaryStackFrame(stack, state, knownBoundary) {
123
+ for (const frameUnit of knownBoundary?.consumesStackFrame ?? []) {
124
+ const expected = stackFrameUnits(frameUnit);
125
+ const popped = stack.pop();
126
+ if (popped === undefined ||
127
+ popped.units.length !== expected.length ||
128
+ !popped.units.every((unit, index) => unit === expected[index])) {
129
+ state.stackBalanced = false;
130
+ }
131
+ }
132
+ }
108
133
  function applyUnknownStackUnits(tokens, consumedProduced, intendedProduced, units) {
109
134
  for (const unit of units) {
110
135
  tokens.set(unit, { origin: 'unknown' });
@@ -114,7 +139,10 @@ function applyUnknownStackUnits(tokens, consumedProduced, intendedProduced, unit
114
139
  }
115
140
  function applyStackEffect(tokens, consumedProduced, intendedProduced, stack, state, effect, expectedTerminalReturn, knownBoundary) {
116
141
  if (effect.stack.kind === 'push') {
117
- stack.push(effect.stack.units.map((unit) => readToken(tokens, unit)));
142
+ stack.push({
143
+ units: effect.stack.units,
144
+ tokens: effect.stack.units.map((unit) => readToken(tokens, unit)),
145
+ });
118
146
  return;
119
147
  }
120
148
  if (effect.stack.kind === 'pop') {
@@ -126,6 +154,7 @@ function applyStackEffect(tokens, consumedProduced, intendedProduced, stack, sta
126
154
  applyUnknownStackUnits(tokens, consumedProduced, intendedProduced, effect.stack.units);
127
155
  return;
128
156
  }
157
+ consumeKnownBoundaryStackFrame(stack, state, knownBoundary);
129
158
  if (expectedTerminalReturn && stack.length !== 0) {
130
159
  state.stackBalanced = false;
131
160
  }
@@ -135,6 +164,64 @@ function applyStackEffect(tokens, consumedProduced, intendedProduced, stack, sta
135
164
  state.hasUnknownStackEffect = true;
136
165
  }
137
166
  }
167
+ function cloneStack(stack) {
168
+ return stack.map((entry) => ({
169
+ units: [...entry.units],
170
+ tokens: [...entry.tokens],
171
+ }));
172
+ }
173
+ function stackSignature(stack) {
174
+ return stack.map((entry) => entry.units.join('+')).join('/');
175
+ }
176
+ function emptyStackProofState() {
177
+ return { stackBalanced: true, hasUnknownStackEffect: false };
178
+ }
179
+ function boundaryFallsThrough(effect) {
180
+ return effect.control.kind === 'call' || effect.control.kind === 'rst';
181
+ }
182
+ function isTerminalExit(item, effect, successors) {
183
+ if (successors.length > 0)
184
+ return false;
185
+ if (effect.control.kind === 'return' && !effect.control.conditional)
186
+ return true;
187
+ return isOpaqueBoundary(item, effect) || effect.control.kind === 'fallthrough';
188
+ }
189
+ function proveStackDiscipline(routine, boundarySummaries, serviceRanges) {
190
+ const labels = labelIndex(routine);
191
+ const state = emptyStackProofState();
192
+ const seen = new Set();
193
+ const work = routine.instructions.length > 0 ? [{ index: 0, stack: [] }] : [];
194
+ while (work.length > 0) {
195
+ const current = work.pop();
196
+ const seenKey = `${current.index}|${stackSignature(current.stack)}`;
197
+ if (seen.has(seenKey))
198
+ continue;
199
+ seen.add(seenKey);
200
+ if (seen.size > 5000) {
201
+ state.hasUnknownStackEffect = true;
202
+ return state;
203
+ }
204
+ const item = routine.instructions[current.index];
205
+ if (item === undefined) {
206
+ if (current.stack.length !== 0)
207
+ state.stackBalanced = false;
208
+ continue;
209
+ }
210
+ const effect = getZ80InstructionEffect(item.instruction);
211
+ const stack = cloneStack(current.stack);
212
+ applyStackEffect(new Map(), new Set(), new Set(), stack, state, effect, isRoutineReturn(effect), boundarySummary(routine, current.index, boundarySummaries, serviceRanges));
213
+ const successors = instructionSuccessors(routine, current.index, effect, labels, {
214
+ boundaryFallthrough: boundaryFallsThrough(effect),
215
+ });
216
+ if (isTerminalExit(item, effect, successors) && stack.length !== 0) {
217
+ state.stackBalanced = false;
218
+ }
219
+ for (const successor of successors) {
220
+ work.push({ index: successor, stack: cloneStack(stack) });
221
+ }
222
+ }
223
+ return state;
224
+ }
138
225
  function applyEffectWrites(tokens, consumedProduced, intendedProduced, directMayWrite, item, effect, transferWrites, instructionIntentOutputs, carryClearBeforeSbcHl) {
139
226
  for (const unit of effect.writes) {
140
227
  if (shouldIgnoreEffectWrite(unit, effect, transferWrites))
@@ -216,14 +303,14 @@ function createInferenceState() {
216
303
  },
217
304
  };
218
305
  }
219
- function instructionInferenceContext(routine, index, boundarySummaries) {
306
+ function instructionInferenceContext(routine, index, boundarySummaries, serviceRanges) {
220
307
  const item = routine.instructions[index];
221
308
  const effect = getZ80InstructionEffect(item.instruction);
222
309
  const carryClearBeforeSbcHl = isCarryClearBeforeSbcHl(item, routine.instructions[index + 1]);
223
310
  return {
224
311
  item,
225
312
  effect,
226
- knownBoundary: boundarySummary(routine, index, boundarySummaries),
313
+ knownBoundary: boundarySummary(routine, index, boundarySummaries, serviceRanges),
227
314
  carryClearBeforeSbcHl,
228
315
  expectedTerminalReturn: isRoutineReturn(effect),
229
316
  effectWrites: new Set(effect.writes),
@@ -255,12 +342,16 @@ function inferInstructionSummaryStep(state, context) {
255
342
  applyEffectWrites(state.tokens, state.consumedProduced, state.intendedProduced, state.directMayWrite, context.item, context.effect, transferWrites, context.instructionIntentOutputs, context.carryClearBeforeSbcHl);
256
343
  addInstructionIntentOutputs(state.intendedProduced, context.effectWrites, context.instructionIntentOutputs);
257
344
  }
258
- export function inferRoutineSummary(routine, boundarySummaries = new Map()) {
345
+ export function inferRoutineSummary(routine, boundarySummaries = new Map(), serviceRanges = []) {
259
346
  const state = createInferenceState();
260
347
  for (let index = 0; index < routine.instructions.length; index += 1) {
261
- inferInstructionSummaryStep(state, instructionInferenceContext(routine, index, boundarySummaries));
348
+ inferInstructionSummaryStep(state, instructionInferenceContext(routine, index, boundarySummaries, serviceRanges));
262
349
  }
263
350
  if (state.stack.length !== 0)
264
351
  state.stackState.stackBalanced = false;
352
+ const stackProof = proveStackDiscipline(routine, boundarySummaries, serviceRanges);
353
+ state.stackState.stackBalanced = stackProof.stackBalanced;
354
+ if (stackProof.hasUnknownStackEffect)
355
+ state.stackState.hasUnknownStackEffect = true;
265
356
  return buildRoutineSummary(routine, state.tokens, state.consumedProduced, state.intendedProduced, state.directMayWrite, state.mayRead, state.stackState);
266
357
  }
@@ -11,6 +11,7 @@ export interface RegisterContractsPolicy {
11
11
  /** @deprecated Use RegisterContractsMode. */
12
12
  export type RegisterCareMode = RegisterContractsMode;
13
13
  export type RegisterContractsUnit = 'A' | 'B' | 'C' | 'D' | 'E' | 'H' | 'L' | 'IXH' | 'IXL' | 'IYH' | 'IYL' | 'SPH' | 'SPL' | 'carry' | 'zero' | 'sign' | 'parity' | 'halfCarry';
14
+ export type RegisterContractsStackFrameUnit = 'AF' | 'BC' | 'DE' | 'HL' | 'IX' | 'IY';
14
15
  /** @deprecated Use RegisterContractsUnit. */
15
16
  export type RegisterCareUnit = RegisterContractsUnit;
16
17
  export type SmartComment = {
@@ -54,6 +55,13 @@ export interface RoutineContract {
54
55
  preserves: RegisterContractsUnit[];
55
56
  complete?: boolean;
56
57
  }
58
+ export interface RegisterContractsServiceRangeContract {
59
+ vector: number;
60
+ selector: RegisterContractsUnit;
61
+ min: number;
62
+ max?: number;
63
+ target: string;
64
+ }
57
65
  export interface RegisterContractsInstruction {
58
66
  instruction: Z80Instruction;
59
67
  file: string;
@@ -161,6 +169,7 @@ export interface RoutineSummary {
161
169
  valueRelations: ValueRelation[];
162
170
  stackBalanced: boolean;
163
171
  hasUnknownStackEffect?: boolean;
172
+ consumesStackFrame?: RegisterContractsStackFrameUnit[];
164
173
  outputCandidates?: RegisterContractsUnit[];
165
174
  }
166
175
  export interface RegisterContractsOutputCandidate {
@@ -333,6 +342,7 @@ export interface AnalyzeRegisterContractsOptions {
333
342
  fixRegisterContracts?: boolean;
334
343
  registerContractsProfile?: 'mon3';
335
344
  interfaceContracts?: RoutineContract[];
345
+ interfaceServiceRanges?: RegisterContractsServiceRangeContract[];
336
346
  acceptedOutputCandidates?: ReadonlyMap<string, RegisterContractsUnit[]>;
337
347
  baselineReport?: RegisterContractsJsonReportModel;
338
348
  baselineFile?: string;
@@ -189,8 +189,10 @@ example `;! in A; out A; clobbers F`.
189
189
 
190
190
  `.asmi` parsing stays strict. Files may contain `extern` or `service rst`
191
191
  boundaries, `in`, `out`, `clobbers` and `preserves` clauses, then `end`.
192
- Comment lines are rejected so interface files stay machine-generated and
193
- deterministic.
192
+ RST service boundaries may name an exact selector value, such as
193
+ `service rst $10 C $53 MON_BANK_CALL`, or a configured lower-bound range, such
194
+ as `service rst $10 C >= $60 TECMATE_EXPANSION_SERVICE`. Comment lines are
195
+ rejected so interface files stay machine-generated and deterministic.
194
196
 
195
197
  ## Effects, Summaries and Liveness
196
198
 
@@ -220,6 +222,25 @@ summaries before the final pass. Strict mode uses `stackBalanced` and
220
222
  `hasUnknownStackEffect` to distinguish balanced stack use from a routine whose
221
223
  boundary may leave the stack in an unknown state.
222
224
 
225
+ Some monitor ABIs deliberately consume a caller-prepared stack frame at a known
226
+ boundary. The built-in MON3/TecMate profile models `RST $10` with `C=$53`
227
+ (`MON_BANK_CALL`) as a service-specific boundary that consumes the top stack
228
+ entries `AF`, `DE` and `HL`, returns `A` and carry from the banked target, and
229
+ leaves the caller stack balanced. This is not a blanket relaxation for `RST`;
230
+ the behavior is selected by the proven `C` service value.
231
+
232
+ Project-specific service ranges are not hardwired into AZM profiles. A project
233
+ that owns `C >= $60` expansion services should declare that range in a local
234
+ `.asmi` interface file. For TECM8-style expansion services, the conservative
235
+ fallback should declare `C` as input, `A` and carry as outputs, and broad
236
+ clobbers for `B,C,D,E,H,L,zero,sign,parity,halfCarry` unless a specific service
237
+ has a tighter exact contract.
238
+
239
+ Stack behaviour has a second path-sensitive proof pass for discipline only. It
240
+ follows local branches inside one routine boundary, consumes known boundary
241
+ stack frames, and accepts dispatcher arms that pop a shared entry frame before
242
+ returning or tail-jumping. Register value inference remains summary-based.
243
+
223
244
  The analysis now emits typed findings rather than only free-form conflict text.
224
245
  The current finding set covers:
225
246
 
@@ -133,6 +133,19 @@ Important options include:
133
133
  | `registerContractsInterfaces` | External `.asmi` contract files. |
134
134
  | `skipAssembly` | Run loading and analysis only. |
135
135
 
136
+ `registerContractsPolicy` matches the physical source file recorded on each
137
+ register-contract routine, direct call and finding. In a single assembled
138
+ translation unit, included files remain distinct physical files for diagnostics
139
+ and policy matching. For example, if `monitor.asm` includes `rtc.asm` and
140
+ `disassembler.asm`, a finding owned by `rtc.asm` is matched against `rtc.asm`,
141
+ not only against the root `monitor.asm` path.
142
+
143
+ This is independent of source ownership units: `.include` keeps the surrounding
144
+ source ownership unit for import/visibility semantics, but policy matching uses
145
+ the physical `sourceName`/finding file. This allows projects to audit retained
146
+ legacy source one included file at a time while keeping the whole program in one
147
+ assembled unit.
148
+
136
149
  `compile()` returns:
137
150
 
138
151
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhlagado/azm",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "description": "AZM assembler for the Z80 family (Node.js CLI)",
5
5
  "license": "GPL-3.0-only",
6
6
  "engines": {