@jhlagado/azm 0.2.14 → 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>;
@@ -15,5 +15,5 @@ export interface RegisterContractsProfileSummary {
15
15
  }
16
16
  export declare function rstTargetName(vector: number): string;
17
17
  export declare function rstServiceTargetName(vector: number, service: string): string;
18
- 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[];
19
19
  export declare function getRegisterContractsProfile(name: 'mon3' | undefined): RegisterContractsProfileSummary | undefined;
@@ -98,19 +98,26 @@ function mon3ApiServices(overrides) {
98
98
  }
99
99
  return services;
100
100
  }
101
- export function rstDispatcherServiceTargetNames(vector, selectorValue) {
101
+ export function rstDispatcherServiceTargetNames(vector, selectorValue, configuredRanges = []) {
102
102
  const mon3 = getRegisterContractsProfile('mon3');
103
103
  const dispatcher = mon3?.rstDispatchers.get(vector);
104
- if (dispatcher === undefined)
105
- return [];
106
- const value = selectorValue(dispatcher.selector);
104
+ const selector = dispatcher?.selector ?? 'C';
105
+ const value = selectorValue(selector);
107
106
  if (value === undefined)
108
107
  return [];
109
- const service = dispatcher.services.get(value);
110
- if (service)
111
- return [service.name];
112
- const rangeService = dispatcher.rangeServices?.find((entry) => value >= entry.min && (entry.max === undefined || value <= entry.max));
113
- return rangeService ? [rangeService.summary.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);
114
121
  }
115
122
  export function getRegisterContractsProfile(name) {
116
123
  if (name !== 'mon3')
@@ -186,16 +193,6 @@ export function getRegisterContractsProfile(name) {
186
193
  hasUnknownStackEffect: false,
187
194
  consumesStackFrame: ['AF', 'DE', 'HL'],
188
195
  };
189
- const tecmateExpansionService = {
190
- name: 'TECMATE_EXPANSION_SERVICE',
191
- mayRead: ['C'],
192
- mayWrite: ['A', ...FLAG_UNITS],
193
- mayOutput: ['A', 'carry'],
194
- preserved: ['B', 'C', 'D', 'E', 'H', 'L'],
195
- valueRelations: [{ out: ['A', 'carry'], from: [] }],
196
- stackBalanced: true,
197
- hasUnknownStackEffect: false,
198
- };
199
196
  return {
200
197
  name: 'mon3',
201
198
  rst: new Map([
@@ -242,7 +239,6 @@ export function getRegisterContractsProfile(name) {
242
239
  [54, parseMatrixScan],
243
240
  [0x53, bankCall],
244
241
  ])),
245
- rangeServices: [{ min: 0x60, summary: tecmateExpansionService }],
246
242
  },
247
243
  ],
248
244
  ]),
@@ -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[];
@@ -57,11 +57,9 @@ export function buildProfileSummaryLookup(profileName) {
57
57
  export function routineNames(routines) {
58
58
  return routines.flatMap((routine) => boundaryLabels(routine));
59
59
  }
60
- export function buildSummaries(routines, contractMap, profileSummaries = []) {
60
+ export function buildSummaries(routines, contractMap, profileSummaries = [], serviceRanges = []) {
61
61
  const names = routineNameSet(routines);
62
- const routineSummaries = inferRoutineSummariesToFixedPoint([...routines], contractMap, names, [
63
- ...profileSummaries,
64
- ]);
62
+ const routineSummaries = inferRoutineSummariesToFixedPoint([...routines], contractMap, names, [...profileSummaries], serviceRanges);
65
63
  const summaries = routineSummaries.map((item) => item.summary);
66
64
  return summariesWithExternalContracts(summaries, contractMap, names);
67
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;
@@ -2,14 +2,14 @@ import { getZ80InstructionEffect } from '../z80/effects.js';
2
2
  import { precedingCServiceName, precedingRegisterImmediateValue } from './boundaryHints.js';
3
3
  import { instructionHead } from './instruction-head.js';
4
4
  import { rstDispatcherServiceTargetNames, rstServiceTargetName, rstTargetName } from './profiles.js';
5
- export function boundarySummary(routine, index, summaries) {
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,22 +28,22 @@ 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
- const profileTarget = firstSummary(rstDispatcherServiceTargetNames(vector, (register) => register === 'C' ? numericService : undefined), summaries);
42
- if (profileTarget !== undefined)
43
- return profileTarget;
44
41
  const numericSummary = summaries.get(rstServiceTargetName(vector, String(numericService)));
45
42
  if (numericSummary !== undefined)
46
43
  return numericSummary;
44
+ const profileTarget = firstSummary(rstDispatcherServiceTargetNames(vector, (register) => (register === 'C' ? numericService : undefined), serviceRanges), summaries);
45
+ if (profileTarget !== undefined)
46
+ return profileTarget;
47
47
  }
48
48
  const service = precedingCServiceName(previous);
49
49
  return service ? summaries.get(rstServiceTargetName(vector, service)) : undefined;
@@ -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';
@@ -163,6 +164,64 @@ function applyStackEffect(tokens, consumedProduced, intendedProduced, stack, sta
163
164
  state.hasUnknownStackEffect = true;
164
165
  }
165
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
+ }
166
225
  function applyEffectWrites(tokens, consumedProduced, intendedProduced, directMayWrite, item, effect, transferWrites, instructionIntentOutputs, carryClearBeforeSbcHl) {
167
226
  for (const unit of effect.writes) {
168
227
  if (shouldIgnoreEffectWrite(unit, effect, transferWrites))
@@ -244,14 +303,14 @@ function createInferenceState() {
244
303
  },
245
304
  };
246
305
  }
247
- function instructionInferenceContext(routine, index, boundarySummaries) {
306
+ function instructionInferenceContext(routine, index, boundarySummaries, serviceRanges) {
248
307
  const item = routine.instructions[index];
249
308
  const effect = getZ80InstructionEffect(item.instruction);
250
309
  const carryClearBeforeSbcHl = isCarryClearBeforeSbcHl(item, routine.instructions[index + 1]);
251
310
  return {
252
311
  item,
253
312
  effect,
254
- knownBoundary: boundarySummary(routine, index, boundarySummaries),
313
+ knownBoundary: boundarySummary(routine, index, boundarySummaries, serviceRanges),
255
314
  carryClearBeforeSbcHl,
256
315
  expectedTerminalReturn: isRoutineReturn(effect),
257
316
  effectWrites: new Set(effect.writes),
@@ -283,12 +342,16 @@ function inferInstructionSummaryStep(state, context) {
283
342
  applyEffectWrites(state.tokens, state.consumedProduced, state.intendedProduced, state.directMayWrite, context.item, context.effect, transferWrites, context.instructionIntentOutputs, context.carryClearBeforeSbcHl);
284
343
  addInstructionIntentOutputs(state.intendedProduced, context.effectWrites, context.instructionIntentOutputs);
285
344
  }
286
- export function inferRoutineSummary(routine, boundarySummaries = new Map()) {
345
+ export function inferRoutineSummary(routine, boundarySummaries = new Map(), serviceRanges = []) {
287
346
  const state = createInferenceState();
288
347
  for (let index = 0; index < routine.instructions.length; index += 1) {
289
- inferInstructionSummaryStep(state, instructionInferenceContext(routine, index, boundarySummaries));
348
+ inferInstructionSummaryStep(state, instructionInferenceContext(routine, index, boundarySummaries, serviceRanges));
290
349
  }
291
350
  if (state.stack.length !== 0)
292
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;
293
356
  return buildRoutineSummary(routine, state.tokens, state.consumedProduced, state.intendedProduced, state.directMayWrite, state.mayRead, state.stackState);
294
357
  }
@@ -55,6 +55,13 @@ export interface RoutineContract {
55
55
  preserves: RegisterContractsUnit[];
56
56
  complete?: boolean;
57
57
  }
58
+ export interface RegisterContractsServiceRangeContract {
59
+ vector: number;
60
+ selector: RegisterContractsUnit;
61
+ min: number;
62
+ max?: number;
63
+ target: string;
64
+ }
58
65
  export interface RegisterContractsInstruction {
59
66
  instruction: Z80Instruction;
60
67
  file: string;
@@ -335,6 +342,7 @@ export interface AnalyzeRegisterContractsOptions {
335
342
  fixRegisterContracts?: boolean;
336
343
  registerContractsProfile?: 'mon3';
337
344
  interfaceContracts?: RoutineContract[];
345
+ interfaceServiceRanges?: RegisterContractsServiceRangeContract[];
338
346
  acceptedOutputCandidates?: ReadonlyMap<string, RegisterContractsUnit[]>;
339
347
  baselineReport?: RegisterContractsJsonReportModel;
340
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
 
@@ -225,9 +227,19 @@ boundary. The built-in MON3/TecMate profile models `RST $10` with `C=$53`
225
227
  (`MON_BANK_CALL`) as a service-specific boundary that consumes the top stack
226
228
  entries `AF`, `DE` and `HL`, returns `A` and carry from the banked target, and
227
229
  leaves the caller stack balanced. This is not a blanket relaxation for `RST`;
228
- the behavior is selected by the proven `C` service value. The same profile also
229
- provides a `C >= $60` TecMate expansion-service range fallback that returns
230
- `A` and carry for installed expansion services.
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.
231
243
 
232
244
  The analysis now emits typed findings rather than only free-form conflict text.
233
245
  The current finding set covers:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhlagado/azm",
3
- "version": "0.2.14",
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": {