@jhlagado/azm 0.2.0 → 0.2.1
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/README.md +95 -70
- package/dist/src/api-compile.js +1 -1
- package/dist/src/assembly/address-planning.js +2 -0
- package/dist/src/assembly/program-emission.js +1 -0
- package/dist/src/expansion/op-expansion.js +1 -0
- package/dist/src/model/source-item.d.ts +6 -0
- package/dist/src/outputs/write-asm80.js +122 -5
- package/dist/src/register-care/analyze.js +36 -8
- package/dist/src/register-care/annotate.d.ts +11 -0
- package/dist/src/register-care/annotate.js +76 -0
- package/dist/src/register-care/annotations.js +33 -146
- package/dist/src/register-care/fix.d.ts +2 -0
- package/dist/src/register-care/fix.js +52 -0
- package/dist/src/register-care/instruction-shape.d.ts +11 -0
- package/dist/src/register-care/instruction-shape.js +129 -0
- package/dist/src/register-care/liveness.js +15 -7
- package/dist/src/register-care/profiles.js +4 -0
- package/dist/src/register-care/programModel.js +79 -13
- package/dist/src/register-care/report.d.ts +2 -1
- package/dist/src/register-care/report.js +91 -34
- package/dist/src/register-care/routine-summaries.d.ts +6 -0
- package/dist/src/register-care/routine-summaries.js +89 -0
- package/dist/src/register-care/sourceText.d.ts +8 -0
- package/dist/src/register-care/sourceText.js +15 -0
- package/dist/src/register-care/summaries.d.ts +3 -3
- package/dist/src/register-care/summaries.js +42 -75
- package/dist/src/register-care/summary.d.ts +3 -0
- package/dist/src/register-care/summary.js +474 -0
- package/dist/src/register-care/types.d.ts +6 -1
- package/dist/src/source/strip-line-comment.d.ts +2 -0
- package/dist/src/source/strip-line-comment.js +26 -0
- package/dist/src/syntax/parse-diagnostics.d.ts +12 -0
- package/dist/src/syntax/parse-diagnostics.js +18 -0
- package/dist/src/syntax/parse-line.js +63 -10
- package/docs/reference/tooling-api.md +13 -6
- package/package.json +4 -2
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import { getZ80InstructionEffect } from '../z80/effects.js';
|
|
2
|
+
import { instructionHead, instructionOperand, instructionOperandCount, isAccumulatorSelfOperand, isImmediateZeroOperand, isPureTokenTransferInstruction, isRegisterOperand, isUnconditionalReturnInstruction, regName, } from './instruction-shape.js';
|
|
3
|
+
import { precedingCServiceName } from './boundaryHints.js';
|
|
4
|
+
import { expandCarrierList } from './carriers.js';
|
|
5
|
+
import { rstServiceTargetName, rstTargetName } from './profiles.js';
|
|
6
|
+
const FLAG_UNIT_LIST = ['carry', 'zero', 'sign', 'parity', 'halfCarry'];
|
|
7
|
+
const TRACKED_UNITS = [
|
|
8
|
+
'A',
|
|
9
|
+
'B',
|
|
10
|
+
'C',
|
|
11
|
+
'D',
|
|
12
|
+
'E',
|
|
13
|
+
'H',
|
|
14
|
+
'L',
|
|
15
|
+
'IXH',
|
|
16
|
+
'IXL',
|
|
17
|
+
'IYH',
|
|
18
|
+
'IYL',
|
|
19
|
+
...FLAG_UNIT_LIST,
|
|
20
|
+
];
|
|
21
|
+
const GENERAL_REGISTER_UNITS = new Set(['A', 'B', 'C', 'D', 'E', 'H', 'L']);
|
|
22
|
+
const CONTRACT_FLAG_UNITS = new Set(['carry', 'zero']);
|
|
23
|
+
const STACK_POINTER_UNITS = new Set(['SPH', 'SPL']);
|
|
24
|
+
const REGISTER_PAIRS = [
|
|
25
|
+
['B', 'C'],
|
|
26
|
+
['D', 'E'],
|
|
27
|
+
['H', 'L'],
|
|
28
|
+
];
|
|
29
|
+
function unique(items) {
|
|
30
|
+
return [...new Set(items)];
|
|
31
|
+
}
|
|
32
|
+
function isTrackedUnit(unit) {
|
|
33
|
+
return TRACKED_UNITS.includes(unit);
|
|
34
|
+
}
|
|
35
|
+
function getRegisterUnits(name) {
|
|
36
|
+
return expandCarrierList([name]);
|
|
37
|
+
}
|
|
38
|
+
function readToken(tokens, unit) {
|
|
39
|
+
return tokens.get(unit) ?? { origin: 'unknown' };
|
|
40
|
+
}
|
|
41
|
+
function semanticReadOrigins(tokens, units) {
|
|
42
|
+
const origins = [];
|
|
43
|
+
for (const unit of units) {
|
|
44
|
+
if (!isTrackedUnit(unit)) {
|
|
45
|
+
origins.push(unit);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const token = readToken(tokens, unit);
|
|
49
|
+
if (token.origin !== 'unknown' && token.origin !== 'produced')
|
|
50
|
+
origins.push(token.origin);
|
|
51
|
+
}
|
|
52
|
+
return origins;
|
|
53
|
+
}
|
|
54
|
+
function markProducedReadsConsumed(tokens, consumedProduced, reads, writes, item) {
|
|
55
|
+
for (const unit of reads) {
|
|
56
|
+
if (!isTrackedUnit(unit) || writes.has(unit))
|
|
57
|
+
continue;
|
|
58
|
+
if (item !== undefined && instructionHead(item) === 'cp' && unit === 'A')
|
|
59
|
+
continue;
|
|
60
|
+
if (readToken(tokens, unit).origin === 'produced')
|
|
61
|
+
consumedProduced.add(unit);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function tokenPreservesUnit(token, unit) {
|
|
65
|
+
return token?.origin === unit;
|
|
66
|
+
}
|
|
67
|
+
function isOpaqueBoundary(item, effect) {
|
|
68
|
+
if (effect.control.kind === 'call' || effect.control.kind === 'rst')
|
|
69
|
+
return true;
|
|
70
|
+
return (effect.control.kind === 'jump' &&
|
|
71
|
+
(instructionHead(item) === 'jp' || instructionHead(item) === 'jp-cc') &&
|
|
72
|
+
!effect.control.conditional &&
|
|
73
|
+
Boolean(effect.control.target) &&
|
|
74
|
+
!effect.control.target?.startsWith('.'));
|
|
75
|
+
}
|
|
76
|
+
function boundarySummary(routine, index, summaries) {
|
|
77
|
+
const item = routine.instructions[index];
|
|
78
|
+
if (!item)
|
|
79
|
+
return undefined;
|
|
80
|
+
const effect = getZ80InstructionEffect(item.instruction);
|
|
81
|
+
if (effect.control.kind === 'call' && effect.control.target) {
|
|
82
|
+
return summaries.get(effect.control.target);
|
|
83
|
+
}
|
|
84
|
+
if (effect.control.kind === 'jump' &&
|
|
85
|
+
(instructionHead(item) === 'jp' || instructionHead(item) === 'jp-cc') &&
|
|
86
|
+
effect.control.target &&
|
|
87
|
+
!effect.control.target.startsWith('.') &&
|
|
88
|
+
!routine.labels.includes(effect.control.target)) {
|
|
89
|
+
return summaries.get(effect.control.target);
|
|
90
|
+
}
|
|
91
|
+
if (effect.control.kind === 'rst' && effect.control.vector !== undefined) {
|
|
92
|
+
const service = precedingCServiceName(routine.instructions[index - 1]);
|
|
93
|
+
if (service) {
|
|
94
|
+
const serviceSummary = summaries.get(rstServiceTargetName(effect.control.vector, service));
|
|
95
|
+
if (serviceSummary)
|
|
96
|
+
return serviceSummary;
|
|
97
|
+
}
|
|
98
|
+
return summaries.get(rstTargetName(effect.control.vector));
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
function relationKey(relation) {
|
|
103
|
+
return `${relation.out.join(',')}<- ${relation.from.join(',')}`;
|
|
104
|
+
}
|
|
105
|
+
function addRelation(out, relation) {
|
|
106
|
+
if (relation.out.length === 0 || relation.from.length === 0)
|
|
107
|
+
return;
|
|
108
|
+
const key = relationKey(relation);
|
|
109
|
+
if (!out.some((existing) => relationKey(existing) === key))
|
|
110
|
+
out.push(relation);
|
|
111
|
+
}
|
|
112
|
+
function addContractRelation(out, relation) {
|
|
113
|
+
if (relation.out.length === 0)
|
|
114
|
+
return;
|
|
115
|
+
const key = relationKey(relation);
|
|
116
|
+
if (!out.some((existing) => relationKey(existing) === key))
|
|
117
|
+
out.push(relation);
|
|
118
|
+
}
|
|
119
|
+
function pairRelation(tokens, out) {
|
|
120
|
+
const from = [];
|
|
121
|
+
for (const unit of out) {
|
|
122
|
+
const token = tokens.get(unit);
|
|
123
|
+
if (!token || token.origin === 'unknown' || token.origin === 'produced')
|
|
124
|
+
return undefined;
|
|
125
|
+
from.push(token.origin);
|
|
126
|
+
}
|
|
127
|
+
if (out.every((unit, idx) => unit === from[idx]))
|
|
128
|
+
return undefined;
|
|
129
|
+
if (out.some((unit, idx) => unit === from[idx]))
|
|
130
|
+
return undefined;
|
|
131
|
+
return { out, from };
|
|
132
|
+
}
|
|
133
|
+
function producedPairRelation(tokens, consumedProduced, out) {
|
|
134
|
+
if (out.some((unit) => tokens.get(unit)?.origin !== 'produced' || consumedProduced.has(unit))) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
return { out, from: [] };
|
|
138
|
+
}
|
|
139
|
+
function withImpliedFlagUnits(units) {
|
|
140
|
+
return unique(units);
|
|
141
|
+
}
|
|
142
|
+
function contractOutRelation(contractIn, contractOut) {
|
|
143
|
+
if (contractOut.length === 0)
|
|
144
|
+
return undefined;
|
|
145
|
+
return {
|
|
146
|
+
out: contractOut,
|
|
147
|
+
from: contractIn.length === contractOut.length ? contractIn : [],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function isUnconditionalReturn(item) {
|
|
151
|
+
return isUnconditionalReturnInstruction(item);
|
|
152
|
+
}
|
|
153
|
+
function isPureTokenTransfer(item) {
|
|
154
|
+
return isPureTokenTransferInstruction(item);
|
|
155
|
+
}
|
|
156
|
+
function isCarryClearBeforeSbcHl(item, next) {
|
|
157
|
+
const head = instructionHead(item).toLowerCase();
|
|
158
|
+
if (head !== 'or' && head !== 'and')
|
|
159
|
+
return false;
|
|
160
|
+
if (!isAccumulatorSelfOperand(item))
|
|
161
|
+
return false;
|
|
162
|
+
return (next !== undefined &&
|
|
163
|
+
instructionHead(next) === 'sbc' &&
|
|
164
|
+
isRegisterOperand(next, 0, 'HL'));
|
|
165
|
+
}
|
|
166
|
+
function intentOutputUnits(item) {
|
|
167
|
+
const head = instructionHead(item).toLowerCase();
|
|
168
|
+
if (head === 'scf' || head === 'ccf')
|
|
169
|
+
return ['carry'];
|
|
170
|
+
if (head === 'cp')
|
|
171
|
+
return isImmediateZeroOperand(item) ? ['A', 'carry', 'zero'] : ['carry', 'zero'];
|
|
172
|
+
if ((head === 'or' || head === 'and' || head === 'xor') && isAccumulatorSelfOperand(item)) {
|
|
173
|
+
return ['A', 'carry', 'zero'];
|
|
174
|
+
}
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
function isMechanicalResidueWrite(item, unit) {
|
|
178
|
+
const head = instructionHead(item).toLowerCase();
|
|
179
|
+
if (head === 'djnz')
|
|
180
|
+
return unit === 'B';
|
|
181
|
+
if (head === 'ldi' || head === 'ldir' || head === 'ldd' || head === 'lddr') {
|
|
182
|
+
return (unit === 'B' || unit === 'C' || unit === 'D' || unit === 'E' || unit === 'H' || unit === 'L');
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
function applyPureTokenTransfer(tokens, consumedProduced, item) {
|
|
187
|
+
const inst = item.instruction;
|
|
188
|
+
const head = instructionHead(item).toLowerCase();
|
|
189
|
+
if (head === 'ld' && instructionOperandCount(inst) === 2) {
|
|
190
|
+
const dst = instructionOperand(inst, 0);
|
|
191
|
+
const src = instructionOperand(inst, 1);
|
|
192
|
+
const dstName = regName(dst);
|
|
193
|
+
if (dstName === undefined)
|
|
194
|
+
return [];
|
|
195
|
+
const dstUnits = getRegisterUnits(dstName);
|
|
196
|
+
if (!dstUnits)
|
|
197
|
+
return [];
|
|
198
|
+
const srcName = regName(src);
|
|
199
|
+
const srcUnits = srcName ? getRegisterUnits(srcName) : undefined;
|
|
200
|
+
if (srcUnits && srcUnits.length === dstUnits.length) {
|
|
201
|
+
dstUnits.forEach((unit, index) => {
|
|
202
|
+
tokens.set(unit, readToken(tokens, srcUnits[index]));
|
|
203
|
+
if (readToken(tokens, srcUnits[index]).origin === 'produced') {
|
|
204
|
+
consumedProduced.add(srcUnits[index]);
|
|
205
|
+
}
|
|
206
|
+
consumedProduced.delete(unit);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
for (const unit of dstUnits) {
|
|
211
|
+
tokens.set(unit, { origin: 'produced' });
|
|
212
|
+
consumedProduced.delete(unit);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return dstUnits;
|
|
216
|
+
}
|
|
217
|
+
if (head === 'ex' && instructionOperandCount(inst) === 2) {
|
|
218
|
+
const left = instructionOperand(inst, 0);
|
|
219
|
+
const right = instructionOperand(inst, 1);
|
|
220
|
+
const leftName = regName(left);
|
|
221
|
+
const rightName = regName(right);
|
|
222
|
+
if (leftName === undefined || rightName === undefined)
|
|
223
|
+
return [];
|
|
224
|
+
const leftUnits = getRegisterUnits(leftName);
|
|
225
|
+
const rightUnits = getRegisterUnits(rightName);
|
|
226
|
+
if (!leftUnits || !rightUnits || leftUnits.length !== rightUnits.length)
|
|
227
|
+
return [];
|
|
228
|
+
const leftTokens = leftUnits.map((unit) => readToken(tokens, unit));
|
|
229
|
+
const rightTokens = rightUnits.map((unit) => readToken(tokens, unit));
|
|
230
|
+
leftUnits.forEach((unit, index) => tokens.set(unit, rightTokens[index] ?? { origin: 'unknown' }));
|
|
231
|
+
rightUnits.forEach((unit, index) => tokens.set(unit, leftTokens[index] ?? { origin: 'unknown' }));
|
|
232
|
+
return unique([...leftUnits, ...rightUnits]);
|
|
233
|
+
}
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
function applyKnownBoundarySummary(tokens, consumedProduced, intendedProduced, directMayWrite, summary) {
|
|
237
|
+
for (const relation of summary.valueRelations) {
|
|
238
|
+
const sameCarrierRelation = relation.out.length === relation.from.length &&
|
|
239
|
+
relation.out.every((unit, index) => unit === relation.from[index]);
|
|
240
|
+
relation.out.forEach((unit, index) => {
|
|
241
|
+
if (!sameCarrierRelation &&
|
|
242
|
+
relation.from.length === relation.out.length &&
|
|
243
|
+
relation.from[index]) {
|
|
244
|
+
tokens.set(unit, readToken(tokens, relation.from[index]));
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
tokens.set(unit, { origin: 'produced' });
|
|
248
|
+
consumedProduced.delete(unit);
|
|
249
|
+
if (CONTRACT_FLAG_UNITS.has(unit))
|
|
250
|
+
intendedProduced.add(unit);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
for (const unit of summary.mayWrite) {
|
|
255
|
+
if (STACK_POINTER_UNITS.has(unit))
|
|
256
|
+
continue;
|
|
257
|
+
if (isTrackedUnit(unit)) {
|
|
258
|
+
tokens.set(unit, { origin: 'unknown' });
|
|
259
|
+
consumedProduced.delete(unit);
|
|
260
|
+
intendedProduced.delete(unit);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
directMayWrite.push(unit);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
export function inferRoutineSummary(routine, boundarySummaries = new Map()) {
|
|
268
|
+
const tokens = new Map();
|
|
269
|
+
for (const unit of TRACKED_UNITS)
|
|
270
|
+
tokens.set(unit, { origin: unit });
|
|
271
|
+
const stack = [];
|
|
272
|
+
const mayRead = [];
|
|
273
|
+
const directMayWrite = [];
|
|
274
|
+
const consumedProduced = new Set();
|
|
275
|
+
const intendedProduced = new Set();
|
|
276
|
+
let stackBalanced = true;
|
|
277
|
+
let hasUnknownStackEffect = false;
|
|
278
|
+
for (let index = 0; index < routine.instructions.length; index += 1) {
|
|
279
|
+
const item = routine.instructions[index];
|
|
280
|
+
const effect = getZ80InstructionEffect(item.instruction);
|
|
281
|
+
const knownBoundary = boundarySummary(routine, index, boundarySummaries);
|
|
282
|
+
const carryClearBeforeSbcHl = isCarryClearBeforeSbcHl(item, routine.instructions[index + 1]);
|
|
283
|
+
const expectedTerminalReturn = index === routine.instructions.length - 1 && isUnconditionalReturn(item);
|
|
284
|
+
const effectWrites = new Set(effect.writes);
|
|
285
|
+
const instructionIntentOutputs = carryClearBeforeSbcHl ? [] : intentOutputUnits(item);
|
|
286
|
+
const semanticReads = carryClearBeforeSbcHl
|
|
287
|
+
? effect.reads.filter((unit) => unit !== 'A')
|
|
288
|
+
: effect.reads;
|
|
289
|
+
if (effect.stack.kind !== 'push' && !isPureTokenTransfer(item)) {
|
|
290
|
+
mayRead.push(...semanticReadOrigins(tokens, semanticReads));
|
|
291
|
+
markProducedReadsConsumed(tokens, consumedProduced, semanticReads, effectWrites, item);
|
|
292
|
+
}
|
|
293
|
+
if (instructionHead(item).toLowerCase() === 'djnz') {
|
|
294
|
+
for (const unit of TRACKED_UNITS) {
|
|
295
|
+
if (readToken(tokens, unit).origin === 'produced')
|
|
296
|
+
consumedProduced.add(unit);
|
|
297
|
+
intendedProduced.delete(unit);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (knownBoundary) {
|
|
301
|
+
mayRead.push(...semanticReadOrigins(tokens, knownBoundary.mayRead));
|
|
302
|
+
markProducedReadsConsumed(tokens, consumedProduced, knownBoundary.mayRead, new Set());
|
|
303
|
+
}
|
|
304
|
+
if (effect.stack.kind === 'push') {
|
|
305
|
+
stack.push(effect.stack.units.map((unit) => readToken(tokens, unit)));
|
|
306
|
+
}
|
|
307
|
+
else if (effect.stack.kind === 'pop') {
|
|
308
|
+
const popped = stack.pop();
|
|
309
|
+
if (!popped) {
|
|
310
|
+
stackBalanced = false;
|
|
311
|
+
for (const unit of effect.stack.units) {
|
|
312
|
+
tokens.set(unit, { origin: 'unknown' });
|
|
313
|
+
intendedProduced.delete(unit);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else if (popped.length !== effect.stack.units.length) {
|
|
317
|
+
for (const unit of effect.stack.units) {
|
|
318
|
+
tokens.set(unit, { origin: 'unknown' });
|
|
319
|
+
consumedProduced.delete(unit);
|
|
320
|
+
intendedProduced.delete(unit);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
effect.stack.units.forEach((unit, idx) => {
|
|
325
|
+
tokens.set(unit, popped[idx] ?? { origin: 'unknown' });
|
|
326
|
+
consumedProduced.delete(unit);
|
|
327
|
+
intendedProduced.delete(unit);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
else if (effect.stack.kind === 'exchangeTop') {
|
|
332
|
+
hasUnknownStackEffect = true;
|
|
333
|
+
for (const unit of effect.stack.units) {
|
|
334
|
+
tokens.set(unit, { origin: 'unknown' });
|
|
335
|
+
consumedProduced.delete(unit);
|
|
336
|
+
intendedProduced.delete(unit);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else if (effect.stack.kind === 'unknown' &&
|
|
340
|
+
!expectedTerminalReturn &&
|
|
341
|
+
(!knownBoundary || !knownBoundary.stackBalanced || knownBoundary.hasUnknownStackEffect)) {
|
|
342
|
+
hasUnknownStackEffect = true;
|
|
343
|
+
}
|
|
344
|
+
const transferWrites = new Set(isPureTokenTransfer(item) ? applyPureTokenTransfer(tokens, consumedProduced, item) : []);
|
|
345
|
+
if (knownBoundary) {
|
|
346
|
+
applyKnownBoundarySummary(tokens, consumedProduced, intendedProduced, directMayWrite, knownBoundary);
|
|
347
|
+
}
|
|
348
|
+
else if (isOpaqueBoundary(item, effect)) {
|
|
349
|
+
for (const unit of TRACKED_UNITS) {
|
|
350
|
+
tokens.set(unit, { origin: 'unknown' });
|
|
351
|
+
consumedProduced.delete(unit);
|
|
352
|
+
intendedProduced.delete(unit);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
for (const unit of effect.writes) {
|
|
356
|
+
if (STACK_POINTER_UNITS.has(unit))
|
|
357
|
+
continue;
|
|
358
|
+
if (effect.stack.kind === 'pop' && effect.stack.units.includes(unit) && isTrackedUnit(unit)) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (transferWrites.has(unit) && isTrackedUnit(unit))
|
|
362
|
+
continue;
|
|
363
|
+
if (unit === 'A' &&
|
|
364
|
+
(instructionHead(item).toLowerCase() === 'or' || instructionHead(item).toLowerCase() === 'and') &&
|
|
365
|
+
isAccumulatorSelfOperand(item)) {
|
|
366
|
+
if (!carryClearBeforeSbcHl)
|
|
367
|
+
intendedProduced.add(unit);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (isTrackedUnit(unit)) {
|
|
371
|
+
tokens.set(unit, { origin: isMechanicalResidueWrite(item, unit) ? 'unknown' : 'produced' });
|
|
372
|
+
consumedProduced.delete(unit);
|
|
373
|
+
if (instructionIntentOutputs.includes(unit))
|
|
374
|
+
intendedProduced.add(unit);
|
|
375
|
+
else
|
|
376
|
+
intendedProduced.delete(unit);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
directMayWrite.push(unit);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
for (const unit of instructionIntentOutputs) {
|
|
383
|
+
if (!isTrackedUnit(unit) || effectWrites.has(unit))
|
|
384
|
+
continue;
|
|
385
|
+
intendedProduced.add(unit);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (stack.length !== 0)
|
|
389
|
+
stackBalanced = false;
|
|
390
|
+
const mayWrite = [...directMayWrite];
|
|
391
|
+
const preserved = [];
|
|
392
|
+
const valueRelations = [];
|
|
393
|
+
const outputUnits = new Set();
|
|
394
|
+
for (const pair of REGISTER_PAIRS) {
|
|
395
|
+
const relation = producedPairRelation(tokens, consumedProduced, pair);
|
|
396
|
+
if (relation) {
|
|
397
|
+
addContractRelation(valueRelations, relation);
|
|
398
|
+
for (const unit of pair)
|
|
399
|
+
outputUnits.add(unit);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
for (const unit of TRACKED_UNITS) {
|
|
403
|
+
if (outputUnits.has(unit))
|
|
404
|
+
continue;
|
|
405
|
+
const current = tokens.get(unit);
|
|
406
|
+
const eligibleProduced = current?.origin === 'produced' &&
|
|
407
|
+
(GENERAL_REGISTER_UNITS.has(unit) ||
|
|
408
|
+
(CONTRACT_FLAG_UNITS.has(unit) && intendedProduced.has(unit)));
|
|
409
|
+
const eligiblePreservedIntent = current?.origin === unit && GENERAL_REGISTER_UNITS.has(unit) && intendedProduced.has(unit);
|
|
410
|
+
if ((eligibleProduced || eligiblePreservedIntent) && !consumedProduced.has(unit)) {
|
|
411
|
+
addContractRelation(valueRelations, {
|
|
412
|
+
out: [unit],
|
|
413
|
+
from: eligiblePreservedIntent ? [unit] : [],
|
|
414
|
+
});
|
|
415
|
+
outputUnits.add(unit);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
for (const unit of TRACKED_UNITS) {
|
|
419
|
+
const current = tokens.get(unit);
|
|
420
|
+
if (tokenPreservesUnit(current, unit)) {
|
|
421
|
+
preserved.push(unit);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (outputUnits.has(unit))
|
|
425
|
+
continue;
|
|
426
|
+
mayWrite.push(unit);
|
|
427
|
+
}
|
|
428
|
+
for (const pair of REGISTER_PAIRS) {
|
|
429
|
+
const relation = pairRelation(tokens, pair);
|
|
430
|
+
if (relation)
|
|
431
|
+
addRelation(valueRelations, relation);
|
|
432
|
+
}
|
|
433
|
+
mayRead.push(...valueRelations.flatMap((relation) => relation.from));
|
|
434
|
+
return {
|
|
435
|
+
name: routine.name,
|
|
436
|
+
mayRead: unique(mayRead),
|
|
437
|
+
mayWrite: unique(mayWrite),
|
|
438
|
+
preserved: unique(preserved),
|
|
439
|
+
valueRelations,
|
|
440
|
+
stackBalanced,
|
|
441
|
+
hasUnknownStackEffect,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
export function applyRoutineContract(summary, contract) {
|
|
445
|
+
const contractIn = withImpliedFlagUnits(contract.in);
|
|
446
|
+
const contractOut = withImpliedFlagUnits(contract.out);
|
|
447
|
+
const contractClobbers = withImpliedFlagUnits(contract.clobbers);
|
|
448
|
+
const contractPreserves = withImpliedFlagUnits(contract.preserves);
|
|
449
|
+
const outputSet = new Set(contractOut);
|
|
450
|
+
const preservedSet = new Set(contractPreserves);
|
|
451
|
+
const inferredWrites = withImpliedFlagUnits(summary.mayWrite);
|
|
452
|
+
const baseMayWrite = contract.complete
|
|
453
|
+
? inferredWrites.filter((unit) => FLAG_UNIT_LIST.includes(unit))
|
|
454
|
+
: inferredWrites;
|
|
455
|
+
const mayWrite = baseMayWrite.filter((unit) => !outputSet.has(unit) && !preservedSet.has(unit));
|
|
456
|
+
for (const unit of contractClobbers) {
|
|
457
|
+
if (!outputSet.has(unit) && !preservedSet.has(unit) && !mayWrite.includes(unit)) {
|
|
458
|
+
mayWrite.push(unit);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const mayWriteSet = new Set(withImpliedFlagUnits(mayWrite));
|
|
462
|
+
const preserved = unique([...summary.preserved, ...contractPreserves]).filter((unit) => !outputSet.has(unit) && !mayWriteSet.has(unit));
|
|
463
|
+
const valueRelations = [...summary.valueRelations];
|
|
464
|
+
const relation = contractOutRelation(contractIn, contractOut);
|
|
465
|
+
if (relation)
|
|
466
|
+
addContractRelation(valueRelations, relation);
|
|
467
|
+
return {
|
|
468
|
+
...summary,
|
|
469
|
+
mayRead: unique(contractIn),
|
|
470
|
+
mayWrite,
|
|
471
|
+
preserved,
|
|
472
|
+
valueRelations,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
@@ -64,6 +64,7 @@ export interface RegisterCareRoutine {
|
|
|
64
64
|
}
|
|
65
65
|
export interface RegisterCareDirectCall {
|
|
66
66
|
target: string;
|
|
67
|
+
subject: string;
|
|
67
68
|
file: string;
|
|
68
69
|
line: number;
|
|
69
70
|
column: number;
|
|
@@ -71,6 +72,7 @@ export interface RegisterCareDirectCall {
|
|
|
71
72
|
export interface RegisterCareProgramModel {
|
|
72
73
|
routines: RegisterCareRoutine[];
|
|
73
74
|
directCalls: RegisterCareDirectCall[];
|
|
75
|
+
directBoundaries: RegisterCareDirectCall[];
|
|
74
76
|
}
|
|
75
77
|
export type StackEffect = {
|
|
76
78
|
kind: 'none';
|
|
@@ -121,7 +123,10 @@ export interface RoutineSummary {
|
|
|
121
123
|
mayWrite: RegisterCareUnit[];
|
|
122
124
|
mayOutput?: RegisterCareUnit[];
|
|
123
125
|
preserved: RegisterCareUnit[];
|
|
124
|
-
valueRelations
|
|
126
|
+
valueRelations: ValueRelation[];
|
|
127
|
+
stackBalanced: boolean;
|
|
128
|
+
hasUnknownStackEffect?: boolean;
|
|
129
|
+
outputCandidates?: RegisterCareUnit[];
|
|
125
130
|
}
|
|
126
131
|
export interface RegisterCareOutputCandidate {
|
|
127
132
|
file: string;
|
|
@@ -2,3 +2,5 @@
|
|
|
2
2
|
* Remove an ASM80-style end-of-line comment (`;`), respecting quoted strings.
|
|
3
3
|
*/
|
|
4
4
|
export declare function stripLineComment(text: string): string;
|
|
5
|
+
/** Trailing `;` comment text, or undefined when absent or whitespace-only. */
|
|
6
|
+
export declare function extractLineComment(text: string): string | undefined;
|
|
@@ -25,3 +25,29 @@ export function stripLineComment(text) {
|
|
|
25
25
|
}
|
|
26
26
|
return text;
|
|
27
27
|
}
|
|
28
|
+
/** Trailing `;` comment text, or undefined when absent or whitespace-only. */
|
|
29
|
+
export function extractLineComment(text) {
|
|
30
|
+
let quote;
|
|
31
|
+
let escaped = false;
|
|
32
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
33
|
+
const char = text[index];
|
|
34
|
+
if (escaped) {
|
|
35
|
+
escaped = false;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (char === '\\' && quote) {
|
|
39
|
+
escaped = true;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if ((char === '"' || char === "'") &&
|
|
43
|
+
!(char === "'" && quote === undefined && /[A-Za-z0-9_]/.test(text[index - 1] ?? ''))) {
|
|
44
|
+
quote = quote === char ? undefined : (quote ?? char);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (char === ';' && !quote) {
|
|
48
|
+
const comment = text.slice(index + 1).trim();
|
|
49
|
+
return comment.length > 0 ? comment : undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Diagnostic, DiagnosticId, DiagnosticSeverity } from '../model/diagnostic.js';
|
|
2
|
+
type ParseDiagLocation = {
|
|
3
|
+
line: number;
|
|
4
|
+
column: number;
|
|
5
|
+
};
|
|
6
|
+
/** Push a parse diagnostic with Next default code/severity (`AZMN_PARSE` / error). */
|
|
7
|
+
export declare function parseDiag(diagnostics: Diagnostic[], sourceName: string, message: string, where?: ParseDiagLocation): void;
|
|
8
|
+
/** Push a parse diagnostic at an explicit 1-based line/column. */
|
|
9
|
+
export declare function parseDiagAt(diagnostics: Diagnostic[], sourceName: string, message: string, line: number, column: number): void;
|
|
10
|
+
/** Push a diagnostic with explicit code, severity, and optional location. */
|
|
11
|
+
export declare function parseDiagAtWithId(diagnostics: Diagnostic[], sourceName: string, code: DiagnosticId | string, severity: DiagnosticSeverity, message: string, where?: ParseDiagLocation): void;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Push a parse diagnostic with Next default code/severity (`AZMN_PARSE` / error). */
|
|
2
|
+
export function parseDiag(diagnostics, sourceName, message, where) {
|
|
3
|
+
parseDiagAtWithId(diagnostics, sourceName, 'AZMN_PARSE', 'error', message, where);
|
|
4
|
+
}
|
|
5
|
+
/** Push a parse diagnostic at an explicit 1-based line/column. */
|
|
6
|
+
export function parseDiagAt(diagnostics, sourceName, message, line, column) {
|
|
7
|
+
parseDiag(diagnostics, sourceName, message, { line, column });
|
|
8
|
+
}
|
|
9
|
+
/** Push a diagnostic with explicit code, severity, and optional location. */
|
|
10
|
+
export function parseDiagAtWithId(diagnostics, sourceName, code, severity, message, where) {
|
|
11
|
+
diagnostics.push({
|
|
12
|
+
code,
|
|
13
|
+
severity,
|
|
14
|
+
message,
|
|
15
|
+
sourceName,
|
|
16
|
+
...(where ? { line: where.line, column: where.column } : {}),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -1,35 +1,88 @@
|
|
|
1
|
-
import { stripLineComment } from '../source/strip-line-comment.js';
|
|
1
|
+
import { extractLineComment, stripLineComment } from '../source/strip-line-comment.js';
|
|
2
2
|
import { normalizeDirectiveAlias } from './directive-aliases.js';
|
|
3
3
|
import { parseExpression, parseTypeExpr } from './parse-expression.js';
|
|
4
4
|
import { parseZ80Instruction } from '../z80/parse-instruction.js';
|
|
5
5
|
export function parseLogicalLine(line, options = {}) {
|
|
6
6
|
const text = normalizeDirectiveAlias(stripLineComment(line.text), options.directiveAliasPolicy).trim();
|
|
7
7
|
if (text.length === 0) {
|
|
8
|
-
return
|
|
8
|
+
return commentOnlyLine(line);
|
|
9
9
|
}
|
|
10
10
|
const span = { sourceName: line.sourceName, line: line.line, column: firstColumn(line.text) };
|
|
11
11
|
const labelWithStatement = /^(@?[A-Za-z_.$?][A-Za-z0-9_.$?]*):\s*(.+)$/.exec(text);
|
|
12
12
|
if (labelWithStatement) {
|
|
13
|
-
const
|
|
13
|
+
const rawLabel = labelWithStatement[1] ?? '';
|
|
14
|
+
const labelName = normalizeEntryLabelName(rawLabel);
|
|
15
|
+
const isEntry = rawLabel.startsWith('@');
|
|
14
16
|
const statementText = labelWithStatement[2] ?? '';
|
|
15
17
|
const equStatement = parseColonLabelEqu(line, labelName, statementText, span);
|
|
16
18
|
if (equStatement) {
|
|
17
19
|
return equStatement;
|
|
18
20
|
}
|
|
19
21
|
const parsedStatement = parseCanonicalStatement(line, statementText, span);
|
|
20
|
-
return {
|
|
21
|
-
items: [{ kind: 'label', name: labelName, span }, ...parsedStatement.items],
|
|
22
|
+
return withLineComment(line, {
|
|
23
|
+
items: [{ kind: 'label', name: labelName, ...(isEntry ? { isEntry: true } : {}), span }, ...parsedStatement.items],
|
|
22
24
|
diagnostics: parsedStatement.diagnostics,
|
|
23
|
-
};
|
|
25
|
+
});
|
|
24
26
|
}
|
|
25
27
|
const labelOnly = /^(@?[A-Za-z_.$?][A-Za-z0-9_.$?]*):$/.exec(text);
|
|
26
28
|
if (labelOnly) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
const rawLabel = labelOnly[1] ?? '';
|
|
30
|
+
return withLineComment(line, {
|
|
31
|
+
items: [
|
|
32
|
+
{
|
|
33
|
+
kind: 'label',
|
|
34
|
+
name: normalizeEntryLabelName(rawLabel),
|
|
35
|
+
...(rawLabel.startsWith('@') ? { isEntry: true } : {}),
|
|
36
|
+
span,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
29
39
|
diagnostics: [],
|
|
30
|
-
};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return withLineComment(line, parseCanonicalStatement(line, text, span));
|
|
43
|
+
}
|
|
44
|
+
function commentOnlyLine(line) {
|
|
45
|
+
const comment = extractLineComment(line.text);
|
|
46
|
+
if (!comment) {
|
|
47
|
+
return { items: [], diagnostics: [] };
|
|
31
48
|
}
|
|
32
|
-
return
|
|
49
|
+
return {
|
|
50
|
+
items: [
|
|
51
|
+
{
|
|
52
|
+
kind: 'comment',
|
|
53
|
+
text: comment,
|
|
54
|
+
origin: 'user',
|
|
55
|
+
span: {
|
|
56
|
+
sourceName: line.sourceName,
|
|
57
|
+
line: line.line,
|
|
58
|
+
column: firstColumn(line.text),
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
diagnostics: [],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function withLineComment(line, result) {
|
|
66
|
+
const comment = extractLineComment(line.text);
|
|
67
|
+
if (!comment) {
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
items: [
|
|
72
|
+
...result.items,
|
|
73
|
+
{
|
|
74
|
+
kind: 'comment',
|
|
75
|
+
text: comment,
|
|
76
|
+
origin: 'user',
|
|
77
|
+
span: {
|
|
78
|
+
sourceName: line.sourceName,
|
|
79
|
+
line: line.line,
|
|
80
|
+
column: firstColumn(line.text),
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
diagnostics: result.diagnostics,
|
|
85
|
+
};
|
|
33
86
|
}
|
|
34
87
|
function parseEquItem(line, name, expressionText, span) {
|
|
35
88
|
const stringValue = parseWholeQuotedString(expressionText.trim());
|