@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.
- 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 +7 -2
- package/dist/src/register-contracts/profiles.js +33 -7
- 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 +6 -4
- package/dist/src/register-contracts/summary-boundary.d.ts +2 -2
- package/dist/src/register-contracts/summary-boundary.js +17 -6
- package/dist/src/register-contracts/summary.d.ts +2 -2
- package/dist/src/register-contracts/summary.js +98 -7
- package/dist/src/register-contracts/types.d.ts +10 -0
- package/docs/codebase/04-ops-and-register-contracts.md +23 -2
- package/docs/codebase/05-interfaces-and-output-artifacts.md +13 -0
- 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>;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
|
106
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|