assembly-yasm-helper 1.4.0
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/.claude/settings.local.json +7 -0
- package/.vscodeignore +10 -0
- package/.vsixmanifest +44 -0
- package/LICENSE +21 -0
- package/Themes/tasmDark.tmTheme.json +134 -0
- package/Themes/tasmHC.tmTheme.json +136 -0
- package/Themes/tasmLight.tmTheme.json +137 -0
- package/bin/assembly-yasm-lsp.js +2 -0
- package/changelog.md +130 -0
- package/configs/language-configuration.json +49 -0
- package/icon.png +0 -0
- package/lsp/server.js +423 -0
- package/out/core/compilerEngine.js +77 -0
- package/out/core/completionEngine.js +259 -0
- package/out/core/diagnosticEngine.js +305 -0
- package/out/core/foldingEngine.js +55 -0
- package/out/core/hoverEngine.js +62 -0
- package/out/core/referencesEngine.js +39 -0
- package/out/core/semanticEngine.js +64 -0
- package/out/data/enums.js +35 -0
- package/out/data/instructionSignatures.js +422 -0
- package/out/data/keywords.js +359 -0
- package/out/data/operandTypes.js +10 -0
- package/out/data/structs.js +132 -0
- package/out/engine/memoryAddressParser.js +45 -0
- package/out/engine/memoryState.js +38 -0
- package/out/engine/memoryTokenizer.js +28 -0
- package/out/extension.js +146 -0
- package/out/instructionRegistry.js +27 -0
- package/out/providers/compilerProvider.js +77 -0
- package/out/providers/completionProvider.js +73 -0
- package/out/providers/definitionProvider.js +41 -0
- package/out/providers/diagnosticProvider.js +32 -0
- package/out/providers/foldingProvider.js +18 -0
- package/out/providers/hoverProvider.js +25 -0
- package/out/providers/referencesProvider.js +28 -0
- package/out/providers/renameProvider.js +70 -0
- package/out/providers/semanticTokensProvider.js +29 -0
- package/out/providers/signatureHelpProvider.js +125 -0
- package/out/providers/symbolProvider.js +66 -0
- package/out/registry.js +167 -0
- package/out/scanner.js +174 -0
- package/out/tokenizer.js +67 -0
- package/out/utils.js +89 -0
- package/out/vsc/fileData.js +21 -0
- package/package.json +241 -0
- package/readme.md +80 -0
- package/snippets.json +141 -0
- package/syntaxes/assembly.tmLanguage.json +315 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { Utils } = require("../utils");
|
|
4
|
+
const { KeywordType } = require("../data/enums");
|
|
5
|
+
const { KEYWORD_MAP, REGISTERS, AVX_REGISTERS, PREPROCESSOR } = require("../data/keywords");
|
|
6
|
+
const { OperandType } = require("../data/operandTypes");
|
|
7
|
+
const { INSTRUCTION_SIGNATURES } = require("../data/instructionSignatures");
|
|
8
|
+
const { MemoryAddressParser } = require("../engine/memoryAddressParser");
|
|
9
|
+
|
|
10
|
+
const MEM_DIRECTIVES = ['db','dw','dd','dq','dt','resb','resw','resd','resq','equ'];
|
|
11
|
+
const INSTRUCTION_TYPES = new Set([KeywordType.instruction, KeywordType.memoryAllocation, KeywordType.precompiled]);
|
|
12
|
+
const SIZE_KEYWORDS = ["byte","word","dword","qword","tword","oword","yword","zword"];
|
|
13
|
+
const KEYWORD_LIST = Array.from(KEYWORD_MAP.values());
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get completion items for a given context.
|
|
17
|
+
* @param {{ line: string, lineIdx: number, lines: string[] }} ctx
|
|
18
|
+
* @param {object} registry
|
|
19
|
+
* @returns {{ label: string, kindType: number, detail: string, doc: string, sortText: string, insertText?: string }[]}
|
|
20
|
+
*/
|
|
21
|
+
function getCompletionItems(ctx, registry) {
|
|
22
|
+
const { line, lineIdx, lines } = ctx;
|
|
23
|
+
const items = [];
|
|
24
|
+
|
|
25
|
+
if (_isInComment(line, line.length)) return items;
|
|
26
|
+
if (_isInString(line)) return items;
|
|
27
|
+
|
|
28
|
+
const trimmed = line.trimStart();
|
|
29
|
+
const isRootLevel = line.length === trimmed.length;
|
|
30
|
+
const words = trimmed.length > 0 ? trimmed.split(/\s+/).map(w => w.toLowerCase()) : [];
|
|
31
|
+
const isTypingOperand = words.length > 1 || (words.length === 1 && /\s$/.test(line));
|
|
32
|
+
|
|
33
|
+
if (trimmed.startsWith('%'))
|
|
34
|
+
return _completePreprocessor(lines, lineIdx, registry);
|
|
35
|
+
|
|
36
|
+
if (!isTypingOperand)
|
|
37
|
+
_addRootLevelItems(isRootLevel, items);
|
|
38
|
+
|
|
39
|
+
if (words[0] === "section" && isTypingOperand)
|
|
40
|
+
return _completeSection();
|
|
41
|
+
|
|
42
|
+
if (_isInsideBracket(line))
|
|
43
|
+
return _completeBracketMemory(line, registry);
|
|
44
|
+
|
|
45
|
+
if (isTypingOperand)
|
|
46
|
+
return _completeOperands(words, line, registry, items);
|
|
47
|
+
|
|
48
|
+
if (trimmed.length > 0)
|
|
49
|
+
_completeInstructions(words, registry, items);
|
|
50
|
+
|
|
51
|
+
return _deduplicate(items);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// -------------------------------------------------------
|
|
55
|
+
// Guards
|
|
56
|
+
// -------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
function _isInComment(line, character) {
|
|
59
|
+
const idx = line.indexOf(';');
|
|
60
|
+
return idx !== -1 && idx < character;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function _isInString(line) {
|
|
64
|
+
const quotes = line.match(/['"]/g);
|
|
65
|
+
return !!(quotes && quotes.length % 2 !== 0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function _isInsideBracket(line) {
|
|
69
|
+
return line.lastIndexOf("[") > line.lastIndexOf("]");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// -------------------------------------------------------
|
|
73
|
+
// Completion sections
|
|
74
|
+
// -------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
function _completePreprocessor(lines, lineIdx, registry) {
|
|
77
|
+
const items = [];
|
|
78
|
+
for (const p of PREPROCESSOR)
|
|
79
|
+
items.push(_makeItem(p.name, KeywordType.precompiled, p.detail, p.doc, null, "01"));
|
|
80
|
+
for (const m of registry.macros)
|
|
81
|
+
items.push(_makeItem(m.name, KeywordType.macro, "(Macro)", '', null, "05"));
|
|
82
|
+
|
|
83
|
+
const argCount = _getMacroArgCount(lines, lineIdx);
|
|
84
|
+
if (argCount > 0) {
|
|
85
|
+
for (let i = 1; i <= argCount; i++)
|
|
86
|
+
items.push(_makeItem(`%${i}`, KeywordType.constant, `(Macro arg ${i})`, '', null, "01"));
|
|
87
|
+
}
|
|
88
|
+
return items;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function _getMacroArgCount(lines, lineIdx) {
|
|
92
|
+
for (let i = lineIdx - 1; i >= 0; i--) {
|
|
93
|
+
const raw = lines[i].trim().toLowerCase();
|
|
94
|
+
if (raw.startsWith('%endmacro')) return 0;
|
|
95
|
+
if (raw.startsWith('%macro')) {
|
|
96
|
+
const parts = raw.split(/\s+/);
|
|
97
|
+
const n = parseInt(parts[2], 10);
|
|
98
|
+
return isNaN(n) ? 0 : n;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function _addRootLevelItems(isRootLevel, items) {
|
|
105
|
+
if (!isRootLevel) return;
|
|
106
|
+
for (const sec of ["section .data", "section .text", "section .bss"])
|
|
107
|
+
items.push(_makeItem(sec, KeywordType.savedWord));
|
|
108
|
+
items.push(_makeItem("global", KeywordType.savedWord));
|
|
109
|
+
items.push(_makeItem("extern", KeywordType.savedWord));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function _completeSection() {
|
|
113
|
+
const items = [];
|
|
114
|
+
for (const sec of ["data", "text", "bss"])
|
|
115
|
+
items.push(_makeItem("." + sec, KeywordType.savedWord));
|
|
116
|
+
return items;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function _completeBracketMemory(line, registry) {
|
|
120
|
+
const items = [];
|
|
121
|
+
const expr = line.slice(line.lastIndexOf("[") + 1);
|
|
122
|
+
const state = MemoryAddressParser.parse(expr);
|
|
123
|
+
|
|
124
|
+
switch (state) {
|
|
125
|
+
case "BASE":
|
|
126
|
+
for (const v of registry.vars)
|
|
127
|
+
items.push(_makeItem(v.name, KeywordType.variable, '', '', null, "01"));
|
|
128
|
+
for (const r of REGISTERS)
|
|
129
|
+
items.push(_makeItem(r, KeywordType.register, '', '', null, "02"));
|
|
130
|
+
break;
|
|
131
|
+
case "BASE_DONE":
|
|
132
|
+
items.push(_makeItem("+", KeywordType.operator, "(Offset)"));
|
|
133
|
+
break;
|
|
134
|
+
case "INDEX":
|
|
135
|
+
for (const r of REGISTERS)
|
|
136
|
+
items.push(_makeItem(r, KeywordType.register));
|
|
137
|
+
break;
|
|
138
|
+
case "INDEX_DONE":
|
|
139
|
+
items.push(_makeItem("*", KeywordType.operator, "(Scale)"));
|
|
140
|
+
break;
|
|
141
|
+
case "SCALE_INPUT":
|
|
142
|
+
for (const s of ["1","2","4","8"])
|
|
143
|
+
items.push(_makeItem(s, KeywordType.constant, "(Scale)"));
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
return items;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function _completeOperands(words, line, registry, items) {
|
|
150
|
+
const firstWord = words[0].toLowerCase();
|
|
151
|
+
|
|
152
|
+
if (!INSTRUCTION_SIGNATURES[firstWord]) {
|
|
153
|
+
if (registry.findMacro(firstWord))
|
|
154
|
+
return _completeMacroOperands(registry);
|
|
155
|
+
return _completeMemoryDirectives(words);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const allowed = getAllowedOperandTypes(firstWord, getOperandIndex(line));
|
|
159
|
+
if (allowed === undefined) return items;
|
|
160
|
+
|
|
161
|
+
if (allowed & OperandType.MEM) {
|
|
162
|
+
for (const s of SIZE_KEYWORDS)
|
|
163
|
+
items.push(_makeItem(s, KeywordType.size, "(Size)", '', null, "01"));
|
|
164
|
+
for (const v of registry.vars)
|
|
165
|
+
items.push(_makeItem(v.name, KeywordType.variable, '', '', null, "02"));
|
|
166
|
+
}
|
|
167
|
+
if (allowed & OperandType.REG) {
|
|
168
|
+
REGISTERS.forEach(r => items.push(_makeItem(r, KeywordType.register, '', '', null, _regPrefix(r))));
|
|
169
|
+
AVX_REGISTERS.forEach(r => items.push(_makeItem(r, KeywordType.register, '', '', null, "05")));
|
|
170
|
+
}
|
|
171
|
+
if (allowed & OperandType.IMM) {
|
|
172
|
+
items.push(_makeItem("0", KeywordType.constant, "(Immediate)", '', null, "04"));
|
|
173
|
+
for (const d of registry.defines || [])
|
|
174
|
+
items.push(_makeItem(d, KeywordType.constant, "(%define)", '', null, "04"));
|
|
175
|
+
}
|
|
176
|
+
if (allowed & OperandType.LABEL) {
|
|
177
|
+
const labelPrefix = (allowed === OperandType.LABEL) ? "01" : "02";
|
|
178
|
+
registry.labels.forEach(l => items.push(_makeItem(l.name, KeywordType.label, '', '', null, labelPrefix)));
|
|
179
|
+
registry.procs.forEach(p => items.push(_makeItem(p.name, KeywordType.method, '', '', null, labelPrefix)));
|
|
180
|
+
}
|
|
181
|
+
return items;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function _completeMacroOperands(registry) {
|
|
185
|
+
const items = [];
|
|
186
|
+
for (const s of SIZE_KEYWORDS)
|
|
187
|
+
items.push(_makeItem(s, KeywordType.size, "(Size)", '', null, "01"));
|
|
188
|
+
for (const v of registry.vars)
|
|
189
|
+
items.push(_makeItem(v.name, KeywordType.variable, '', '', null, "02"));
|
|
190
|
+
REGISTERS.forEach(r => items.push(_makeItem(r, KeywordType.register, '', '', null, _regPrefix(r))));
|
|
191
|
+
AVX_REGISTERS.forEach(r => items.push(_makeItem(r, KeywordType.register, '', '', null, "05")));
|
|
192
|
+
items.push(_makeItem("0", KeywordType.constant, "(Immediate)", '', null, "04"));
|
|
193
|
+
return items;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function _completeMemoryDirectives(words) {
|
|
197
|
+
const items = [];
|
|
198
|
+
const secondWord = words.filter(w => w.length > 0)[1] || '';
|
|
199
|
+
if (!MEM_DIRECTIVES.includes(secondWord)) {
|
|
200
|
+
for (const d of MEM_DIRECTIVES) {
|
|
201
|
+
const kw = KEYWORD_MAP.get(d);
|
|
202
|
+
items.push(_makeItem(d, KeywordType.memoryAllocation, kw ? kw.def : '(Memory)'));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return items;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function _completeInstructions(words, registry, items) {
|
|
209
|
+
const partial = (words[0] || '').toLowerCase();
|
|
210
|
+
for (const k of KEYWORD_LIST) {
|
|
211
|
+
if (INSTRUCTION_TYPES.has(k.type) && k.name.toLowerCase().startsWith(partial))
|
|
212
|
+
items.push(_makeItem(k.name, k.type, Utils.getType(k.type), k.def));
|
|
213
|
+
}
|
|
214
|
+
for (const m of registry.macros) {
|
|
215
|
+
if (m.name.toLowerCase().startsWith(partial))
|
|
216
|
+
items.push(_makeItem(m.name, KeywordType.macro, "(Macro)", m.doc || '', null, "01"));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function _deduplicate(items) {
|
|
221
|
+
const seen = new Set();
|
|
222
|
+
return items.filter(item => {
|
|
223
|
+
if (seen.has(item.label)) return false;
|
|
224
|
+
seen.add(item.label);
|
|
225
|
+
return true;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// -------------------------------------------------------
|
|
230
|
+
// Helpers
|
|
231
|
+
// -------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
function getOperandIndex(line) {
|
|
234
|
+
return line.split(";")[0].split(",").length - 1;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function getAllowedOperandTypes(instruction, operandIndex) {
|
|
238
|
+
const sigs = INSTRUCTION_SIGNATURES[instruction];
|
|
239
|
+
if (!sigs) return null;
|
|
240
|
+
let types = 0, found = false;
|
|
241
|
+
for (const sig of sigs) {
|
|
242
|
+
if (sig.length > operandIndex) { types |= sig[operandIndex]; found = true; }
|
|
243
|
+
}
|
|
244
|
+
return found ? types : undefined;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function _regPrefix(reg) {
|
|
248
|
+
const r = reg.toLowerCase();
|
|
249
|
+
if (/^(xmm|ymm|zmm)\d+$/.test(r) || /^k\d+$/.test(r)) return "05";
|
|
250
|
+
if (/^(r(ax|bx|cx|dx|si|di|bp|sp)|r\d+)$/.test(r)) return "03";
|
|
251
|
+
if (/^(e(ax|bx|cx|dx|si|di|bp|sp)|r\d+d)$/.test(r)) return "03";
|
|
252
|
+
return "04";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function _makeItem(label, kindType, detail = '', doc = "", insertText = null, sortPrefix = "02") {
|
|
256
|
+
return { label, kindType, detail, doc, sortText: sortPrefix + "_" + label, insertText };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = { getCompletionItems, getAllowedOperandTypes, getOperandIndex };
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { KEYWORD_DICTIONARY } = require("../data/keywords");
|
|
4
|
+
const { KeywordType, AllowKinds } = require("../data/enums");
|
|
5
|
+
const { INSTRUCTION_SIGNATURES } = require("../data/instructionSignatures");
|
|
6
|
+
const { OperandType } = require("../data/operandTypes");
|
|
7
|
+
|
|
8
|
+
const REG_SIZE = {
|
|
9
|
+
8: ["al","bl","cl","dl","ah","bh","ch","dh","sil","dil","bpl","spl",
|
|
10
|
+
"r8b","r9b","r10b","r11b","r12b","r13b","r14b","r15b"],
|
|
11
|
+
16: ["ax","bx","cx","dx","si","di","bp","sp",
|
|
12
|
+
"r8w","r9w","r10w","r11w","r12w","r13w","r14w","r15w"],
|
|
13
|
+
32: ["eax","ebx","ecx","edx","esi","edi","ebp","esp",
|
|
14
|
+
"r8d","r9d","r10d","r11d","r12d","r13d","r14d","r15d"],
|
|
15
|
+
64: ["rax","rbx","rcx","rdx","rsi","rdi","rbp","rsp",
|
|
16
|
+
"r8","r9","r10","r11","r12","r13","r14","r15","rip"],
|
|
17
|
+
128: [],
|
|
18
|
+
256: [],
|
|
19
|
+
512: [],
|
|
20
|
+
};
|
|
21
|
+
for (let i = 0; i < 16; i++) REG_SIZE[128].push(`xmm${i}`);
|
|
22
|
+
for (let i = 0; i < 16; i++) REG_SIZE[256].push(`ymm${i}`);
|
|
23
|
+
for (let i = 0; i < 32; i++) REG_SIZE[512].push(`zmm${i}`);
|
|
24
|
+
|
|
25
|
+
const REG_BITS = {};
|
|
26
|
+
for (const [bits, regs] of Object.entries(REG_SIZE)) {
|
|
27
|
+
for (const r of regs) REG_BITS[r] = Number(bits);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const KEYWORD_MAP = new Map(KEYWORD_DICTIONARY.map(k => [k.name.toLowerCase(), k]));
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Analyze lines and return plain diagnostic objects.
|
|
34
|
+
* @param {string[]} lines
|
|
35
|
+
* @param {object} registry
|
|
36
|
+
* @returns {{ line: number, startCol: number, endCol: number, message: string, severity: 'error'|'warning' }[]}
|
|
37
|
+
*/
|
|
38
|
+
function analyzeDiagnostics(lines, registry) {
|
|
39
|
+
const diags = [];
|
|
40
|
+
_checkLevel1(lines, diags);
|
|
41
|
+
_checkLevel2(lines, diags, registry);
|
|
42
|
+
_checkLevel3(lines, diags);
|
|
43
|
+
return diags;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ==========================================
|
|
47
|
+
// Level 1: Syntax
|
|
48
|
+
// ==========================================
|
|
49
|
+
function _checkLevel1(lines, diags) {
|
|
50
|
+
_checkDuplicateLabels(lines, diags);
|
|
51
|
+
_checkUnclosedBlocks(lines, diags);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function _checkDuplicateLabels(lines, diags) {
|
|
55
|
+
const seen = new Map();
|
|
56
|
+
for (let i = 0; i < lines.length; i++) {
|
|
57
|
+
const lineNoComment = lines[i].split(';')[0].trimEnd();
|
|
58
|
+
if (!lineNoComment.trimStart().endsWith(':')) continue;
|
|
59
|
+
const name = lineNoComment.trim().slice(0, -1).trim().toLowerCase();
|
|
60
|
+
if (!name) continue;
|
|
61
|
+
if (name.startsWith('%%')) continue;
|
|
62
|
+
if (seen.has(name)) {
|
|
63
|
+
diags.push(_makeDiag(i, lines[i].indexOf(name.charAt(0)), name.length,
|
|
64
|
+
`Duplicate label '${name}' (first defined at line ${seen.get(name) + 1})`,
|
|
65
|
+
'error'));
|
|
66
|
+
} else {
|
|
67
|
+
seen.set(name, i);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function _checkUnclosedBlocks(lines, diags) {
|
|
73
|
+
const procStack = [], macroStack = [], ifStack = [], tasmMacStack = [];
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < lines.length; i++) {
|
|
76
|
+
const clean = lines[i].split(';')[0].trim().toLowerCase();
|
|
77
|
+
if (!clean) continue;
|
|
78
|
+
const words = clean.split(/\s+/);
|
|
79
|
+
const first = words[0];
|
|
80
|
+
const second = words[1] || '';
|
|
81
|
+
|
|
82
|
+
if (second === "proc") procStack.push(i);
|
|
83
|
+
if (first === "endp") {
|
|
84
|
+
if (procStack.length === 0) diags.push(_makeDiag(i, 0, 4, "'endp' without matching 'proc'", 'error'));
|
|
85
|
+
else procStack.pop();
|
|
86
|
+
}
|
|
87
|
+
if (first === "%macro") macroStack.push(i);
|
|
88
|
+
if (first === "%endmacro") {
|
|
89
|
+
if (macroStack.length === 0) diags.push(_makeDiag(i, 0, 8, "'%endmacro' without matching '%macro'", 'error'));
|
|
90
|
+
else macroStack.pop();
|
|
91
|
+
}
|
|
92
|
+
if (["%if","%ifdef","%ifndef"].includes(first)) ifStack.push(i);
|
|
93
|
+
if (first === "%endif") {
|
|
94
|
+
if (ifStack.length === 0) diags.push(_makeDiag(i, 0, 6, "'%endif' without matching '%if'", 'error'));
|
|
95
|
+
else ifStack.pop();
|
|
96
|
+
}
|
|
97
|
+
if (second === "macro") tasmMacStack.push(i);
|
|
98
|
+
if (first === "endm") {
|
|
99
|
+
if (tasmMacStack.length === 0) diags.push(_makeDiag(i, 0, 4, "'endm' without matching 'macro'", 'error'));
|
|
100
|
+
else tasmMacStack.pop();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const li of procStack) diags.push(_makeDiag(li, 0, 4, `'proc' is never closed with 'endp'`, 'error'));
|
|
105
|
+
for (const li of macroStack) diags.push(_makeDiag(li, 0, 6, `'%macro' is never closed with '%endmacro'`, 'error'));
|
|
106
|
+
for (const li of ifStack) diags.push(_makeDiag(li, 0, 3, `'%if' block is never closed with '%endif'`, 'error'));
|
|
107
|
+
for (const li of tasmMacStack) diags.push(_makeDiag(li, 0, 5, `'macro' is never closed with 'endm'`, 'error'));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ==========================================
|
|
111
|
+
// Level 2: Reference
|
|
112
|
+
// ==========================================
|
|
113
|
+
function _checkLevel2(lines, diags, registry) {
|
|
114
|
+
const jumpOps = ["jmp","je","jne","jz","jnz","jg","jl","jge","jle",
|
|
115
|
+
"ja","jb","jae","jbe","jc","jnc","js","jns","jo","jno",
|
|
116
|
+
"jcxz","jecxz","jrcxz","loop","loope","loopne"];
|
|
117
|
+
|
|
118
|
+
const knownLabels = new Set([
|
|
119
|
+
...registry.labels.map(l => l.name.toLowerCase()),
|
|
120
|
+
...registry.procs.map(p => p.name.toLowerCase())
|
|
121
|
+
]);
|
|
122
|
+
const knownVars = new Set(registry.vars.map(v => v.name.toLowerCase()));
|
|
123
|
+
|
|
124
|
+
for (let i = 0; i < lines.length; i++) {
|
|
125
|
+
const lineNoComment = lines[i].split(';')[0];
|
|
126
|
+
const clean = lineNoComment.trim().toLowerCase();
|
|
127
|
+
if (!clean) continue;
|
|
128
|
+
const words = clean.split(/[\s,]+/).filter(w => w.length > 0);
|
|
129
|
+
if (words.length < 2) continue;
|
|
130
|
+
const first = words[0];
|
|
131
|
+
|
|
132
|
+
if (jumpOps.includes(first) || first === "call") {
|
|
133
|
+
const target = words[1];
|
|
134
|
+
if (!_isNumber(target) && !target.startsWith('[') && !target.startsWith('0x')) {
|
|
135
|
+
if (!knownLabels.has(target)) {
|
|
136
|
+
const col = lineNoComment.toLowerCase().indexOf(target, first.length);
|
|
137
|
+
diags.push(_makeDiag(i, col, target.length,
|
|
138
|
+
`Undefined label or procedure '${target}'`, 'warning'));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const dataOps = ["mov","add","sub","cmp","and","or","xor","test","lea","movzx","movsx"];
|
|
144
|
+
if (dataOps.includes(first)) {
|
|
145
|
+
for (let w = 1; w < words.length; w++) {
|
|
146
|
+
const word = words[w].replace(/[\[\]]/g, '');
|
|
147
|
+
if (_isRegister(word) || _isNumber(word) || _isSizeKeyword(word)) continue;
|
|
148
|
+
if (/^[a-z_][a-z0-9_]*$/.test(word) && !knownVars.has(word) && !knownLabels.has(word)) {
|
|
149
|
+
const col = lineNoComment.toLowerCase().indexOf(word, first.length);
|
|
150
|
+
if (col !== -1) {
|
|
151
|
+
diags.push(_makeDiag(i, col, word.length,
|
|
152
|
+
`'${word}' is not defined as a variable or label`, 'warning'));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ==========================================
|
|
161
|
+
// Level 3: Instruction Validation
|
|
162
|
+
// ==========================================
|
|
163
|
+
function _checkLevel3(lines, diags) {
|
|
164
|
+
for (let i = 0; i < lines.length; i++) {
|
|
165
|
+
const lineNoComment = lines[i].split(';')[0];
|
|
166
|
+
const clean = lineNoComment.trim().toLowerCase();
|
|
167
|
+
if (!clean || clean.endsWith(':')) continue;
|
|
168
|
+
|
|
169
|
+
const spaceIdx = clean.search(/[\s\t]/);
|
|
170
|
+
if (spaceIdx === -1) continue;
|
|
171
|
+
|
|
172
|
+
const opcode = clean.substring(0, spaceIdx).trim();
|
|
173
|
+
const kw = KEYWORD_MAP.get(opcode);
|
|
174
|
+
if (!kw) continue;
|
|
175
|
+
if (kw.type !== KeywordType.instruction) continue;
|
|
176
|
+
|
|
177
|
+
const operandStr = clean.substring(spaceIdx).trim();
|
|
178
|
+
const operands = operandStr.split(',').map(o => o.trim()).filter(o => o.length > 0);
|
|
179
|
+
|
|
180
|
+
const sigForms = INSTRUCTION_SIGNATURES[opcode];
|
|
181
|
+
const validBySig = sigForms?.some(sig => sig.length === operands.length);
|
|
182
|
+
if (kw.opCount >= 0 && operands.length !== kw.opCount && !validBySig) {
|
|
183
|
+
const col = lineNoComment.toLowerCase().indexOf(opcode);
|
|
184
|
+
diags.push(_makeDiag(i, col, opcode.length,
|
|
185
|
+
`'${opcode}' requires ${kw.opCount} operand(s), got ${operands.length}`, 'error'));
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (kw.opCount === 1 && operands.length === 1) {
|
|
190
|
+
const op = operands[0].replace(/[\[\]]/g, '').trim();
|
|
191
|
+
_checkOperandType(i, lineNoComment, op, opcode, kw.allowType, diags);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (kw.opCount === 2 && operands.length === 2) {
|
|
195
|
+
_checkSizeMismatch(i, lineNoComment, opcode, operands, diags);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (sigForms && operands.length >= 2) {
|
|
199
|
+
const types = operands.map(o => _classifyOperand(o));
|
|
200
|
+
const matched = sigForms.some(sig =>
|
|
201
|
+
sig.length === operands.length &&
|
|
202
|
+
sig.every((expected, j) => types[j] & expected)
|
|
203
|
+
);
|
|
204
|
+
if (!matched) {
|
|
205
|
+
const col = lineNoComment.toLowerCase().indexOf(opcode);
|
|
206
|
+
diags.push(_makeDiag(i, col, opcode.length,
|
|
207
|
+
`'${opcode}' has no valid form for (${types.map(t => _typeName(t)).join(', ')})`, 'warning'));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function _checkOperandType(lineIdx, rawLine, operand, opcode, allowType, diags) {
|
|
214
|
+
if (allowType === AllowKinds.label) {
|
|
215
|
+
if (_isRegister(operand) || _isNumber(operand)) {
|
|
216
|
+
const col = rawLine.toLowerCase().indexOf(operand, opcode.length);
|
|
217
|
+
diags.push(_makeDiag(lineIdx, col, operand.length,
|
|
218
|
+
`'${opcode}' expects a label, not '${operand}'`, 'error'));
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (allowType === AllowKinds.constants) {
|
|
223
|
+
if (!_isNumber(operand)) {
|
|
224
|
+
const col = rawLine.toLowerCase().indexOf(operand, opcode.length);
|
|
225
|
+
diags.push(_makeDiag(lineIdx, col, operand.length,
|
|
226
|
+
`'${opcode}' expects a constant, not '${operand}'`, 'error'));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function _checkSizeMismatch(lineIdx, rawLine, opcode, operands, diags) {
|
|
232
|
+
const sizeConvOps = ["movzx", "movsx", "movsxd"];
|
|
233
|
+
if (sizeConvOps.includes(opcode)) return;
|
|
234
|
+
|
|
235
|
+
const SIZE_KEYWORD_BITS = {
|
|
236
|
+
"byte": 8, "word": 16, "dword": 32, "qword": 64,
|
|
237
|
+
"tbyte": 80, "oword": 128, "yword": 256, "zword": 512
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const getInfo = (op) => {
|
|
241
|
+
let s = op.replace(/\[/g, ' ').replace(/\]/g, ' ').trim();
|
|
242
|
+
const tokens = s.split(/\s+/).filter(t => t.length > 0);
|
|
243
|
+
let sizeKeyword = null, reg = null;
|
|
244
|
+
for (const t of tokens) {
|
|
245
|
+
if (SIZE_KEYWORD_BITS[t] !== undefined) sizeKeyword = t;
|
|
246
|
+
else if (REG_BITS[t] !== undefined) reg = t;
|
|
247
|
+
}
|
|
248
|
+
if (sizeKeyword) return { bits: SIZE_KEYWORD_BITS[sizeKeyword], label: sizeKeyword, token: sizeKeyword };
|
|
249
|
+
if (reg) return { bits: REG_BITS[reg], label: reg, token: reg };
|
|
250
|
+
return null;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const info1 = getInfo(operands[0]);
|
|
254
|
+
const info2 = getInfo(operands[1]);
|
|
255
|
+
if (!info1 || !info2 || info1.bits === info2.bits) return;
|
|
256
|
+
|
|
257
|
+
const commaIdx = rawLine.indexOf(',');
|
|
258
|
+
const searchFrom = commaIdx !== -1 ? commaIdx : 0;
|
|
259
|
+
const col = rawLine.toLowerCase().indexOf(info2.token, searchFrom);
|
|
260
|
+
|
|
261
|
+
diags.push(_makeDiag(lineIdx, col, info2.token.length,
|
|
262
|
+
`Size mismatch: '${info1.label}' is ${info1.bits}-bit but '${info2.label}' is ${info2.bits}-bit`, 'error'));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ==========================================
|
|
266
|
+
// Helpers
|
|
267
|
+
// ==========================================
|
|
268
|
+
function _makeDiag(line, col, length, message, severity) {
|
|
269
|
+
const startCol = Math.max(0, col);
|
|
270
|
+
return { line, startCol, endCol: startCol + length, message, severity };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function _classifyOperand(op) {
|
|
274
|
+
if (op.includes('[')) return OperandType.MEM;
|
|
275
|
+
const tokens = op.split(/\s+/).filter(t => t.length > 0);
|
|
276
|
+
if (tokens.some(t => _isRegister(t))) return OperandType.REG;
|
|
277
|
+
if (tokens.length === 1 && _isNumber(tokens[0])) return OperandType.IMM;
|
|
278
|
+
return OperandType.LABEL | OperandType.IMM;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function _typeName(bits) {
|
|
282
|
+
if ((bits & 3) === 3) return 'r/m';
|
|
283
|
+
if (bits & OperandType.REG) return 'reg';
|
|
284
|
+
if (bits & OperandType.MEM) return 'mem';
|
|
285
|
+
if (bits & OperandType.IMM) return 'imm';
|
|
286
|
+
if (bits & OperandType.LABEL) return 'label';
|
|
287
|
+
return '?';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function _isRegister(word) {
|
|
291
|
+
return word in REG_BITS ||
|
|
292
|
+
/^(xmm|ymm|zmm)\d+$/.test(word) ||
|
|
293
|
+
/^r\d+(b|w|d)?$/.test(word);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function _isNumber(word) {
|
|
297
|
+
return /^0x[0-9a-f]+$/i.test(word) ||
|
|
298
|
+
/^[0-9][0-9a-f]*(h|b|d)?$/i.test(word);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function _isSizeKeyword(word) {
|
|
302
|
+
return ["byte","word","dword","qword","tbyte","oword","yword","zword","ptr","rel","near","far","short"].includes(word);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
module.exports = { analyzeDiagnostics };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const PROC_START = /^\s*[A-Za-z_.$?][\w.$?]*\s+proc\b/i;
|
|
4
|
+
const PROC_END = /^\s*endp\s*(?:;.*)?$/i;
|
|
5
|
+
const MACRO_START = /^\s*%macro\b/i;
|
|
6
|
+
const MACRO_END = /^\s*%endmacro\s*(?:;.*)?$/i;
|
|
7
|
+
const STRUC_START = /^\s*[A-Za-z_.$?][\w.$?]*\s+struc\b/i;
|
|
8
|
+
const STRUC_END = /^\s*ends\s*(?:;.*)?$/i;
|
|
9
|
+
const SECTION_RE = /^\s*(section|segment)\b/i;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Compute folding ranges for document lines.
|
|
13
|
+
* @param {string[]} lines
|
|
14
|
+
* @returns {{ start: number, end: number, kind: 'region'|null }[]}
|
|
15
|
+
*/
|
|
16
|
+
function getFoldingRanges(lines) {
|
|
17
|
+
const ranges = [];
|
|
18
|
+
const count = lines.length;
|
|
19
|
+
|
|
20
|
+
const procStack = [];
|
|
21
|
+
const macroStack = [];
|
|
22
|
+
const strucStack = [];
|
|
23
|
+
let sectionStart = -1;
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < count; i++) {
|
|
26
|
+
const line = lines[i];
|
|
27
|
+
|
|
28
|
+
if (SECTION_RE.test(line)) {
|
|
29
|
+
if (sectionStart >= 0) {
|
|
30
|
+
let end = i - 1;
|
|
31
|
+
while (end > sectionStart && lines[end].trim() === '') end--;
|
|
32
|
+
if (end > sectionStart) ranges.push({ start: sectionStart, end, kind: 'region' });
|
|
33
|
+
}
|
|
34
|
+
sectionStart = i;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (PROC_START.test(line)) { procStack.push(i); continue; }
|
|
39
|
+
if (PROC_END.test(line) && procStack.length) { const s = procStack.pop(); if (i > s) ranges.push({ start: s, end: i, kind: null }); continue; }
|
|
40
|
+
if (MACRO_START.test(line)) { macroStack.push(i); continue; }
|
|
41
|
+
if (MACRO_END.test(line) && macroStack.length) { const s = macroStack.pop(); if (i > s) ranges.push({ start: s, end: i, kind: null }); continue; }
|
|
42
|
+
if (STRUC_START.test(line)) { strucStack.push(i); continue; }
|
|
43
|
+
if (STRUC_END.test(line) && strucStack.length) { const s = strucStack.pop(); if (i > s) ranges.push({ start: s, end: i, kind: null }); continue; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (sectionStart >= 0) {
|
|
47
|
+
let end = count - 1;
|
|
48
|
+
while (end > sectionStart && lines[end].trim() === '') end--;
|
|
49
|
+
if (end > sectionStart) ranges.push({ start: sectionStart, end, kind: 'region' });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return ranges;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { getFoldingRanges };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { Utils } = require("../utils");
|
|
4
|
+
const { INSTRUCTION_SIGNATURES } = require("../data/instructionSignatures");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get hover content for a word.
|
|
8
|
+
* @param {string} word
|
|
9
|
+
* @param {object} registry
|
|
10
|
+
* @returns {{ language: string, value: string }[] | null}
|
|
11
|
+
*/
|
|
12
|
+
function getHoverContent(word, registry) {
|
|
13
|
+
const output = [];
|
|
14
|
+
const proc = registry.findProcedure(word);
|
|
15
|
+
const variable = registry.findVariable(word);
|
|
16
|
+
const keyword = registry.getKeyword(word);
|
|
17
|
+
const macro = registry.findMacro(word);
|
|
18
|
+
const label = registry.findLabel(word);
|
|
19
|
+
|
|
20
|
+
if (Utils.isNumberStr(word)) {
|
|
21
|
+
output.push({ language: "assembly", value: Utils.getNumMsg(word) });
|
|
22
|
+
} else if (variable) {
|
|
23
|
+
output.push({
|
|
24
|
+
language: "assembly",
|
|
25
|
+
value: `(Variable) ${variable.name}: ${variable.type}${variable.section ? " [section " + variable.section + "]" : ""}`
|
|
26
|
+
});
|
|
27
|
+
} else if (proc) {
|
|
28
|
+
output.push(
|
|
29
|
+
{ language: "assembly", value: "(Procedure) " + proc.name },
|
|
30
|
+
{ language: "plainText", value: proc.description.des },
|
|
31
|
+
{ language: "assembly", value: proc.description.paramsString() },
|
|
32
|
+
{ language: "assembly", value: proc.description.outputs() }
|
|
33
|
+
);
|
|
34
|
+
} else if (macro) {
|
|
35
|
+
const argStr = macro.argCount !== undefined ? ` — ${macro.argCount} arg${macro.argCount !== 1 ? 's' : ''}` : '';
|
|
36
|
+
const lineStr = macro.line !== undefined ? ` [line ${macro.line + 1}]` : '';
|
|
37
|
+
output.push({ language: "assembly", value: "(Macro) " + macro.name + argStr + lineStr });
|
|
38
|
+
} else if (keyword) {
|
|
39
|
+
output.push(
|
|
40
|
+
{ language: "assembly", value: Utils.getType(keyword.type) + " " + keyword.name },
|
|
41
|
+
{ language: "plainText", value: keyword.def },
|
|
42
|
+
{ language: "assembly", value: "Syntax: " + keyword.data }
|
|
43
|
+
);
|
|
44
|
+
const sigs = INSTRUCTION_SIGNATURES[word.toLowerCase()];
|
|
45
|
+
if (sigs?.length) {
|
|
46
|
+
const ts = b => ((b&3)===3)?'r/m':(b&1)?'reg':(b&2)?'mem':(b&4)?'imm':(b&8)?'label':'?';
|
|
47
|
+
const forms = sigs.map(f =>
|
|
48
|
+
f.length ? `${word.toLowerCase()} ${f.map(ts).join(', ')}` : word.toLowerCase()
|
|
49
|
+
).join('\n');
|
|
50
|
+
output.push({ language: 'assembly', value: forms });
|
|
51
|
+
}
|
|
52
|
+
} else if (label) {
|
|
53
|
+
const lineInfo = label.line !== undefined
|
|
54
|
+
? ` [line ${label.line + 1}]`
|
|
55
|
+
: (label.value ? ` => ${label.value}` : '');
|
|
56
|
+
output.push({ language: 'assembly', value: '(Label) ' + label.name + lineInfo });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return output.length > 0 ? output : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { getHoverContent };
|