@jhlagado/azm 0.2.13 → 0.2.14
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/register-contracts/profiles.d.ts +5 -0
- package/dist/src/register-contracts/profiles.js +32 -2
- package/dist/src/register-contracts/summaries.js +4 -0
- package/dist/src/register-contracts/summary-boundary.js +12 -1
- package/dist/src/register-contracts/summary.js +31 -3
- package/dist/src/register-contracts/types.d.ts +2 -0
- package/docs/codebase/04-ops-and-register-contracts.md +9 -0
- package/docs/codebase/05-interfaces-and-output-artifacts.md +13 -0
- package/package.json +1 -1
|
@@ -6,6 +6,11 @@ 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;
|
|
@@ -89,10 +89,14 @@ 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
101
|
export function rstDispatcherServiceTargetNames(vector, selectorValue) {
|
|
98
102
|
const mon3 = getRegisterContractsProfile('mon3');
|
|
@@ -103,7 +107,10 @@ export function rstDispatcherServiceTargetNames(vector, selectorValue) {
|
|
|
103
107
|
if (value === undefined)
|
|
104
108
|
return [];
|
|
105
109
|
const service = dispatcher.services.get(value);
|
|
106
|
-
|
|
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] : [];
|
|
107
114
|
}
|
|
108
115
|
export function getRegisterContractsProfile(name) {
|
|
109
116
|
if (name !== 'mon3')
|
|
@@ -168,6 +175,27 @@ export function getRegisterContractsProfile(name) {
|
|
|
168
175
|
stackBalanced: true,
|
|
169
176
|
hasUnknownStackEffect: false,
|
|
170
177
|
};
|
|
178
|
+
const bankCall = {
|
|
179
|
+
name: mon3ApiTargetName(0x53, 'BANK_CALL'),
|
|
180
|
+
mayRead: ['B', 'C', 'H', 'L'],
|
|
181
|
+
mayWrite: ['A', 'B', 'C', 'D', 'E', 'H', 'L', ...FLAG_UNITS],
|
|
182
|
+
mayOutput: ['A', 'carry'],
|
|
183
|
+
preserved: [],
|
|
184
|
+
valueRelations: [{ out: ['A', 'carry'], from: [] }],
|
|
185
|
+
stackBalanced: true,
|
|
186
|
+
hasUnknownStackEffect: false,
|
|
187
|
+
consumesStackFrame: ['AF', 'DE', 'HL'],
|
|
188
|
+
};
|
|
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
|
+
};
|
|
171
199
|
return {
|
|
172
200
|
name: 'mon3',
|
|
173
201
|
rst: new Map([
|
|
@@ -212,7 +240,9 @@ export function getRegisterContractsProfile(name) {
|
|
|
212
240
|
[16, scanKeys],
|
|
213
241
|
[18, matrixScan],
|
|
214
242
|
[54, parseMatrixScan],
|
|
243
|
+
[0x53, bankCall],
|
|
215
244
|
])),
|
|
245
|
+
rangeServices: [{ min: 0x60, summary: tecmateExpansionService }],
|
|
216
246
|
},
|
|
217
247
|
],
|
|
218
248
|
]),
|
|
@@ -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,6 +48,9 @@ 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
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
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';
|
|
4
|
+
import { rstDispatcherServiceTargetNames, rstServiceTargetName, rstTargetName } from './profiles.js';
|
|
5
5
|
export function boundarySummary(routine, index, summaries) {
|
|
6
6
|
const item = routine.instructions[index];
|
|
7
7
|
if (!item)
|
|
@@ -38,6 +38,9 @@ function rstServiceBoundarySummary(routine, index, vector, summaries) {
|
|
|
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;
|
|
41
44
|
const numericSummary = summaries.get(rstServiceTargetName(vector, String(numericService)));
|
|
42
45
|
if (numericSummary !== undefined)
|
|
43
46
|
return numericSummary;
|
|
@@ -45,3 +48,11 @@ function rstServiceBoundarySummary(routine, index, vector, summaries) {
|
|
|
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
|
+
}
|
|
@@ -91,7 +91,7 @@ function applyStackPop(tokens, consumedProduced, intendedProduced, stack, state,
|
|
|
91
91
|
}
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
|
-
if (popped.length !== units.length) {
|
|
94
|
+
if (popped.tokens.length !== units.length) {
|
|
95
95
|
for (const unit of units) {
|
|
96
96
|
tokens.set(unit, { origin: 'unknown' });
|
|
97
97
|
consumedProduced.delete(unit);
|
|
@@ -100,11 +100,35 @@ function applyStackPop(tokens, consumedProduced, intendedProduced, stack, state,
|
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
102
102
|
units.forEach((unit, idx) => {
|
|
103
|
-
tokens.set(unit, popped[idx] ?? { origin: 'unknown' });
|
|
103
|
+
tokens.set(unit, popped.tokens[idx] ?? { origin: 'unknown' });
|
|
104
104
|
consumedProduced.delete(unit);
|
|
105
105
|
intendedProduced.delete(unit);
|
|
106
106
|
});
|
|
107
107
|
}
|
|
108
|
+
function stackFrameUnits(frameUnit) {
|
|
109
|
+
if (frameUnit === 'AF')
|
|
110
|
+
return ['A', 'sign', 'zero', 'halfCarry', 'parity', 'carry'];
|
|
111
|
+
if (frameUnit === 'BC')
|
|
112
|
+
return ['B', 'C'];
|
|
113
|
+
if (frameUnit === 'DE')
|
|
114
|
+
return ['D', 'E'];
|
|
115
|
+
if (frameUnit === 'HL')
|
|
116
|
+
return ['H', 'L'];
|
|
117
|
+
if (frameUnit === 'IX')
|
|
118
|
+
return ['IXH', 'IXL'];
|
|
119
|
+
return ['IYH', 'IYL'];
|
|
120
|
+
}
|
|
121
|
+
function consumeKnownBoundaryStackFrame(stack, state, knownBoundary) {
|
|
122
|
+
for (const frameUnit of knownBoundary?.consumesStackFrame ?? []) {
|
|
123
|
+
const expected = stackFrameUnits(frameUnit);
|
|
124
|
+
const popped = stack.pop();
|
|
125
|
+
if (popped === undefined ||
|
|
126
|
+
popped.units.length !== expected.length ||
|
|
127
|
+
!popped.units.every((unit, index) => unit === expected[index])) {
|
|
128
|
+
state.stackBalanced = false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
108
132
|
function applyUnknownStackUnits(tokens, consumedProduced, intendedProduced, units) {
|
|
109
133
|
for (const unit of units) {
|
|
110
134
|
tokens.set(unit, { origin: 'unknown' });
|
|
@@ -114,7 +138,10 @@ function applyUnknownStackUnits(tokens, consumedProduced, intendedProduced, unit
|
|
|
114
138
|
}
|
|
115
139
|
function applyStackEffect(tokens, consumedProduced, intendedProduced, stack, state, effect, expectedTerminalReturn, knownBoundary) {
|
|
116
140
|
if (effect.stack.kind === 'push') {
|
|
117
|
-
stack.push(
|
|
141
|
+
stack.push({
|
|
142
|
+
units: effect.stack.units,
|
|
143
|
+
tokens: effect.stack.units.map((unit) => readToken(tokens, unit)),
|
|
144
|
+
});
|
|
118
145
|
return;
|
|
119
146
|
}
|
|
120
147
|
if (effect.stack.kind === 'pop') {
|
|
@@ -126,6 +153,7 @@ function applyStackEffect(tokens, consumedProduced, intendedProduced, stack, sta
|
|
|
126
153
|
applyUnknownStackUnits(tokens, consumedProduced, intendedProduced, effect.stack.units);
|
|
127
154
|
return;
|
|
128
155
|
}
|
|
156
|
+
consumeKnownBoundaryStackFrame(stack, state, knownBoundary);
|
|
129
157
|
if (expectedTerminalReturn && stack.length !== 0) {
|
|
130
158
|
state.stackBalanced = false;
|
|
131
159
|
}
|
|
@@ -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 = {
|
|
@@ -161,6 +162,7 @@ export interface RoutineSummary {
|
|
|
161
162
|
valueRelations: ValueRelation[];
|
|
162
163
|
stackBalanced: boolean;
|
|
163
164
|
hasUnknownStackEffect?: boolean;
|
|
165
|
+
consumesStackFrame?: RegisterContractsStackFrameUnit[];
|
|
164
166
|
outputCandidates?: RegisterContractsUnit[];
|
|
165
167
|
}
|
|
166
168
|
export interface RegisterContractsOutputCandidate {
|
|
@@ -220,6 +220,15 @@ summaries before the final pass. Strict mode uses `stackBalanced` and
|
|
|
220
220
|
`hasUnknownStackEffect` to distinguish balanced stack use from a routine whose
|
|
221
221
|
boundary may leave the stack in an unknown state.
|
|
222
222
|
|
|
223
|
+
Some monitor ABIs deliberately consume a caller-prepared stack frame at a known
|
|
224
|
+
boundary. The built-in MON3/TecMate profile models `RST $10` with `C=$53`
|
|
225
|
+
(`MON_BANK_CALL`) as a service-specific boundary that consumes the top stack
|
|
226
|
+
entries `AF`, `DE` and `HL`, returns `A` and carry from the banked target, and
|
|
227
|
+
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.
|
|
231
|
+
|
|
223
232
|
The analysis now emits typed findings rather than only free-form conflict text.
|
|
224
233
|
The current finding set covers:
|
|
225
234
|
|
|
@@ -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
|