@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.
@@ -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
- return new Map(names.map((serviceName, api) => [
92
+ const services = new Map(names.map((serviceName, api) => [
93
93
  api,
94
94
  overrides.get(api) ?? conservativeMon3ApiSummary(api, serviceName),
95
95
  ]));
96
+ for (const [api, summary] of overrides) {
97
+ services.set(api, summary);
98
+ }
99
+ return services;
96
100
  }
97
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
- return service ? [service.name] : [];
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(effect.stack.units.map((unit) => readToken(tokens, unit)));
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhlagado/azm",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "AZM assembler for the Z80 family (Node.js CLI)",
5
5
  "license": "GPL-3.0-only",
6
6
  "engines": {