@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.
- package/dist/src/api-register-contracts.js +13 -5
- package/dist/src/register-contracts/analyze.js +3 -2
- package/dist/src/register-contracts/controlFlow.js +4 -2
- package/dist/src/register-contracts/interfaceContracts.d.ts +6 -1
- package/dist/src/register-contracts/interfaceContracts.js +44 -1
- package/dist/src/register-contracts/liveness.d.ts +2 -2
- package/dist/src/register-contracts/liveness.js +10 -10
- package/dist/src/register-contracts/profiles.d.ts +2 -2
- package/dist/src/register-contracts/profiles.js +16 -20
- package/dist/src/register-contracts/routine-summaries.d.ts +2 -2
- package/dist/src/register-contracts/routine-summaries.js +5 -5
- package/dist/src/register-contracts/summaries.d.ts +2 -2
- package/dist/src/register-contracts/summaries.js +2 -4
- package/dist/src/register-contracts/summary-boundary.d.ts +2 -2
- package/dist/src/register-contracts/summary-boundary.js +8 -8
- package/dist/src/register-contracts/summary.d.ts +2 -2
- package/dist/src/register-contracts/summary.js +67 -4
- package/dist/src/register-contracts/types.d.ts +8 -0
- package/docs/codebase/04-ops-and-register-contracts.md +17 -5
- package/package.json +1 -1
|
@@ -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 {
|
|
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
|
|
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
|
-
...(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
105
|
-
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
193
|
-
|
|
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.
|
|
229
|
-
|
|
230
|
-
|
|
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:
|