src/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
276 / 299
92.3%
Branches:
156 / 172, 50 excl
90.7%
Functions:
11 / 11, 1 excl
100.0%

typechecker/inline-asm.spice
Line Branch Exec Source
1 // Copyright (c) 2021-2026 ChilliBits. All rights reserved.
2
3 // Std imports
4 import "std/data/vector";
5 import "std/type/type-conversion";
6
7 /**
8 * Kind of an inline assembly constraint
9 */
10 public type AsmConstraintKind enum {
11 OUTPUT,
12 INPUT,
13 CLOBBER
14 }
15
16 /**
17 * One comma-separated entry of an inline assembly constraint string in the LLVM syntax
18 */
19 public type AsmConstraint struct {
20 281 public AsmConstraintKind kind = AsmConstraintKind::INPUT
21 281 public bool isIndirect = false // Operand is passed as pointer to the memory, the asm reads or writes ('*')
22 281 public long tiedOutputIdx = -1l // Index of the output constraint, this input is tied to, or -1
23 281 public bool hasTiedInput = false // Output constraint, that an input is tied to
24 }
25
26 562 public p AsmConstraint.ctor() {}
27
28 // Whether this is an output, that is the result of the asm call
29 441 public f<bool> AsmConstraint.isDirectOutput() {
30
2/2
✓ Branch 0 taken 257 times.
✓ Branch 1 taken 184 times.
441 return this.kind == AsmConstraintKind::OUTPUT && !this.isIndirect;
31 }
32
33 // Whether the operand of this constraint is an argument of the asm call (indirect outputs and inputs)
34 353 public f<bool> AsmConstraint.hasArg() {
35
2/2
✓ Branch 0 taken 37 times.
✓ Branch 1 taken 316 times.
353 return this.kind != AsmConstraintKind::CLOBBER && !this.isDirectOutput();
36 }
37
38 // Whether the given char is a decimal digit
39 511 f<bool> isAsmDigit(char c) {
40
2/2
✓ Branch 0 taken 39 times.
✓ Branch 1 taken 472 times.
511 return c >= '0' && c <= '9';
41 }
42
43 // Numeric value of the given decimal digit
44 136 f<unsigned long> getAsmDigitValue(char c) {
45 136 return cast<unsigned long>(cast<int>(c) - cast<int>('0'));
46 }
47
48 /**
49 * Parse a single entry of a constraint string, mirroring llvm::InlineAsm::ConstraintInfo::Parse
50 *
51 * @param str Constraint string
52 * @param start Start index of the entry
53 * @param end End index of the entry (exclusive)
54 * @param constraints Constraints parsed so far, receives the new constraint
55 * @param errorMessage Receives the error message, if the entry is invalid
56 * @return Valid or not
57 */
58 283 f<bool> parseAsmConstraint(const String& str, unsigned long start, unsigned long end, Vector<AsmConstraint>& constraints, String& errorMessage) {
59 283 const String entry = str.getSubstring(start, cast<long>(end - start));
60
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 281 times.
283 if start == end {
61 2 errorMessage = String("Empty constraint");
62 2 return false;
63 2 }
64
65 // Parse the prefixes
66 281 AsmConstraint constraint;
67 281 unsigned long i = start;
68
2/2
✓ Branch 0 taken 30 times.
✓ Branch 1 taken 251 times.
281 if str[i] == '~' {
69 30 constraint.kind = AsmConstraintKind::CLOBBER;
70 30 i++;
71
3/4
✗ Branch 0 not taken.
✓ Branch 1 taken 30 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 28 times.
30 if i == end || str[i] != '{' {
72 2 errorMessage = "Clobber constraint '" + entry + "' must name the clobbered register or memory in braces, like '~{memory}'";
73 2 return false;
74 2 }
75
2/2
✓ Branch 0 taken 105 times.
✓ Branch 1 taken 146 times.
279 } else if str[i] == '=' {
76 105 constraint.kind = AsmConstraintKind::OUTPUT;
77 105 i++;
78
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 144 times.
251 } else if str[i] == '!' {
79 2 errorMessage = "Label constraint '" + entry + "' is not supported";
80 2 return false;
81 251 }
82
4/4
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 276 times.
✓ Branch 2 taken 254 times.
✓ Branch 3 taken 23 times.
277 if i < end && str[i] == '*' {
83 23 constraint.isIndirect = true;
84 23 i++;
85 23 }
86
87 // Parse the modifiers
88 277 bool isEarlyClobber = false;
89 277 bool isCommutative = false;
90
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 284 times.
285 while i < end {
91 284 const char c = str[i];
92
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 273 times.
284 if c == '&' {
93
4/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 9 times.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 3 times.
11 if constraint.kind != AsmConstraintKind::OUTPUT || isEarlyClobber {
94 3 errorMessage = "Constraint '" + entry + "' uses the early clobber modifier '&', which is only allowed once on outputs";
95 3 return false;
96 3 }
97 8 isEarlyClobber = true;
98
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 273 times.
281 } else if c == '%' {
99
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ if constraint.kind == AsmConstraintKind::CLOBBER || isCommutative {
100 ✗ errorMessage = "Constraint '" + entry + "' uses the commutative modifier '%', which is only allowed once on inputs and outputs";
101 ✗ return false;
102 ✗ }
103 ✗ isCommutative = true;
104
4/4
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 272 times.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 272 times.
273 } else if c == '#' || c == '*' {
105 1 errorMessage = "Constraint '" + entry + "' uses the unsupported modifier '" + c + "'";
106 1 return false;
107 1 } else {
108 272 break;
109 ✗ }
110 8 i++;
111 8 }
112
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 272 times.
273 if i == end {
113 1 errorMessage = "Constraint '" + entry + "' does not contain a constraint code";
114 1 return false;
115 1 }
116
117 // Parse the constraint codes
118
2/2
✓ Branch 0 taken 266 times.
✓ Branch 1 taken 272 times.
538 while i < end {
119 272 const char c = str[i];
120
2/2
✓ Branch 0 taken 40 times.
✓ Branch 1 taken 232 times.
272 if c == '{' { // Physical register
121 40 const long closeIdx = str.find('}', i);
122
4/4
✓ Branch 0 taken 39 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 39 times.
✓ Branch 3 taken 1 time.
40 if closeIdx == -1l || cast<unsigned long>(closeIdx) >= end {
123 1 errorMessage = "Constraint '" + entry + "' contains an unterminated register name";
124 1 return false;
125 1 }
126 39 i = cast<unsigned long>(closeIdx) + 1l;
127
2/2
✓ Branch 0 taken 38 times.
✓ Branch 1 taken 194 times.
271 } else if isAsmDigit(c) { // Tied operand
128 38 long outputIdx = 0l;
129
4/4
✓ Branch 0 taken 38 times.
✓ Branch 1 taken 38 times.
✓ Branch 2 taken 38 times.
✓ Branch 3 taken 38 times.
76 while i < end && isAsmDigit(str[i]) {
130 38 outputIdx = outputIdx * 10l + cast<long>(getAsmDigitValue(str[i]));
131 38 i++;
132 38 }
133
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 38 times.
38 if constraint.kind != AsmConstraintKind::INPUT {
134 ✗ errorMessage = "Constraint '" + entry + "' ties an operand, which is only allowed on inputs";
135 ✗ return false;
136 ✗ }
137
4/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 36 times.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 35 times.
38 if outputIdx >= cast<long>(constraints.getSize()) || constraints.get(outputIdx).kind != AsmConstraintKind::OUTPUT {
138 3 errorMessage = "Constraint '" + entry + "' ties the input to operand " + toString(outputIdx) + ", which is no output";
139 3 return false;
140 3 }
141 35 AsmConstraint& output = constraints.get(outputIdx);
142
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 34 times.
35 if output.isIndirect {
143 1 errorMessage = "Constraint '" + entry + "' ties the input to an indirect output, which is not supported";
144 1 return false;
145 1 }
146
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 33 times.
34 if output.hasTiedInput {
147 1 errorMessage = "Constraint '" + entry + "' ties the input to output " + toString(outputIdx) + ", which already has a tied input";
148 1 return false;
149 1 }
150 33 output.hasTiedInput = true;
151 33 constraint.tiedOutputIdx = outputIdx;
152
1/2
✓ Branch 0 taken 194 times.
✗ Branch 1 not taken.
227 } else if c == '|' {
153 ✗ errorMessage = "Constraint '" + entry + "' uses multiple alternatives, which are not supported";
154 ✗ return false;
155
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 193 times.
194 } else if c == '^' { // Two-letter constraint code
156
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1 if i + 3l > end {
157 ✗ errorMessage = "Constraint '" + entry + "' contains an incomplete '^' constraint code";
158 ✗ return false;
159 ✗ }
160 1 i += 3l;
161
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 192 times.
194 } else if c == '@' { // Constraint code with the given number of letters
162
4/8
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 time.
✓ Branch 6 taken 1 time.
✗ Branch 7 not taken.
1 if i + 1l >= end || !isAsmDigit(str[i + 1l]) || str[i + 1l] == '0' || i + 2l + getAsmDigitValue(str[i + 1l]) > end {
163 ✗ errorMessage = "Constraint '" + entry + "' contains an invalid '@' constraint code";
164 ✗ return false;
165 ✗ }
166 1 i += 2l + getAsmDigitValue(str[i + 1l]);
167 1 } else { // Single-letter constraint code
168 192 i++;
169 227 }
170 266 }
171
172 266 constraints.pushBack(constraint);
173 266 return true;
174 283 }
175
176 /**
177 * Parse an inline assembly constraint string in the LLVM syntax and verify its structure, mirroring
178 * llvm::InlineAsm::ParseConstraints and llvm::InlineAsm::verify. LLVM itself does not verify the constraints, when building
179 * the inline asm, so invalid constraints would only surface as an assertion or crash in the backend.
180 *
181 * @param str Constraint string
182 * @param constraints Receives the parsed constraints
183 * @param errorMessage Receives the error message, if the constraint string is invalid
184 * @return Valid or not
185 */
186 157 public f<bool> parseAsmConstraints(const String& str, Vector<AsmConstraint>& constraints, String& errorMessage) {
187 // Split the constraint string at the commas and parse each entry
188 157 const unsigned long length = str.getLength();
189 157 unsigned long start = 0l;
190
2/2
✓ Branch 0 taken 283 times.
✓ Branch 1 taken 138 times.
421 while start < length {
191 283 long end = str.find(',', start);
192
2/2
✓ Branch 0 taken 150 times.
✓ Branch 1 taken 133 times.
283 if end == -1l {
193 133 end = cast<long>(length);
194 133 }
195
2/2
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 266 times.
283 if !parseAsmConstraint(str, start, cast<unsigned long>(end), constraints, errorMessage) {
196 17 return false;
197 }
198 266 start = cast<unsigned long>(end) + 1l;
199
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 264 times.
266 if start == length {
200 2 errorMessage = String("The constraint string must not end with a comma");
201 2 return false;
202 2 }
203 264 }
204
205 // Verify the order of outputs, inputs and clobbers
206 138 unsigned long numDirectOutputs = 0l;
207 138 unsigned long numInputs = 0l;
208 138 unsigned long numClobbers = 0l;
209
2/2
✓ Branch 0 taken 256 times.
✓ Branch 1 taken 134 times.
390 foreach const AsmConstraint& constraint : constraints {
210
2/2
✓ Branch 0 taken 158 times.
✓ Branch 1 taken 98 times.
256 if constraint.kind == AsmConstraintKind::OUTPUT {
211
4/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 96 times.
✓ Branch 2 taken 96 times.
✓ Branch 3 taken 2 times.
98 if numInputs > 0l || numClobbers > 0l {
212 2 errorMessage = String("Output constraints must come before input and clobber constraints");
213 2 return false;
214 2 }
215
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 81 times.
96 if !constraint.isIndirect {
216 81 numDirectOutputs++;
217 81 }
218
2/2
✓ Branch 0 taken 130 times.
✓ Branch 1 taken 28 times.
254 } else if constraint.kind == AsmConstraintKind::INPUT {
219
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 128 times.
130 if numClobbers > 0l {
220 2 errorMessage = String("Input constraints must come before clobber constraints");
221 2 return false;
222 2 }
223 128 numInputs++;
224 128 } else {
225 28 numClobbers++;
226 156 }
227 252 }
228
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 132 times.
134 if numDirectOutputs > 1l {
229 2 errorMessage = String("Only one direct output is supported. Use indirect outputs ('=*m') for further results");
230 2 return false;
231 2 }
232 132 return true;
233 }
234
235 /**
236 * Verify the operand references of an inline assembly template string in the LLVM syntax, mirroring the operand handling
237 * of llvm::AsmPrinter::emitInlineAsm. LLVM aborts the whole compilation with a fatal error on an invalid reference.
238 *
239 * @param str Template string
240 * @param numOperands Number of operands (all constraints except for clobbers)
241 * @param errorMessage Receives the error message, if the template string is invalid
242 * @return Valid or not
243 */
244 84 public f<bool> verifyAsmTemplate(const String& str, unsigned long numOperands, String& errorMessage) {
245 84 const unsigned long length = str.getLength();
246 84 bool inVariant = false;
247 84 unsigned long i = 0l;
248
2/2
✓ Branch 0 taken 672 times.
✓ Branch 1 taken 73 times.
745 while i < length {
249
2/2
✓ Branch 0 taken 554 times.
✓ Branch 1 taken 118 times.
672 if str[i] != '$' {
250 554 i++;
251 554 continue;
252 }
253 118 i++; // Consume '$'
254
255 // Escapes and dialect variants
256
4/4
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 117 times.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 112 times.
118 if i < length && str[i] == '$' {
257 6 i++;
258 6 continue;
259 }
260
4/4
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 111 times.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 107 times.
112 if i < length && str[i] == '(' {
261
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 time.
5 if inVariant {
262 1 errorMessage = String("Nested variants '$(' are not allowed");
263 1 return false;
264 1 }
265 4 inVariant = true;
266 4 i++;
267 4 continue;
268 }
269
6/6
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 106 times.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 105 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 105 times.
107 if i < length && (str[i] == '|' || str[i] == ')') {
270
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
2 if str[i] == ')' {
271 1 inVariant = false;
272 1 }
273 2 i++;
274 2 continue;
275 }
276
277 // Special strings like '${:uid}'
278
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 104 times.
105 const bool hasBraces = i < length && str[i] == '{';
279
2/2
✓ Branch 0 taken 82 times.
✓ Branch 1 taken 23 times.
105 if hasBraces {
280 23 i++;
281
3/4
✗ Branch 0 not taken.
✓ Branch 1 taken 23 times.
✓ Branch 2 taken 10 times.
✓ Branch 3 taken 13 times.
23 if i < length && str[i] == ':' {
282 10 const long closeIdx = str.find('}', i);
283
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if closeIdx == -1l {
284 ✗ errorMessage = String("Unterminated '${:' expression");
285 ✗ return false;
286 ✗ }
287 10 const String special = str.getSubstring(i + 1l, closeIdx - cast<long>(i) - 1l);
288
6/6
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 8 times.
10 if special != "uid" && special != "comment" && special != "private" {
289 2 errorMessage = "Unknown special string '${:" + special + "}'. Valid ones are '${:uid}', '${:comment}' and '${:private}'";
290 2 return false;
291 2 }
292 8 i = cast<unsigned long>(closeIdx) + 1l;
293 8 continue;
294 10 }
295 13 }
296
297 // Operand reference
298
4/4
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 94 times.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 94 times.
95 if i == length || !isAsmDigit(str[i]) {
299 1 errorMessage = String("'$' must be followed by an operand number. Use '$$' for a literal '$'");
300 1 return false;
301 1 }
302 94 unsigned long operandIdx = 0l;
303
4/4
✓ Branch 0 taken 44 times.
✓ Branch 1 taken 146 times.
✓ Branch 2 taken 96 times.
✓ Branch 3 taken 94 times.
190 while i < length && isAsmDigit(str[i]) {
304 96 operandIdx = operandIdx * 10l + getAsmDigitValue(str[i]);
305 96 i++;
306 96 }
307
2/2
✓ Branch 0 taken 90 times.
✓ Branch 1 taken 4 times.
94 if operandIdx >= numOperands {
308 4 errorMessage = "Operand $" + toString(cast<long>(operandIdx)) + " does not exist, the asm has " + toString(cast<long>(numOperands)) + " operand(s). Use '$$' for a literal '$'";
309 4 return false;
310 4 }
311
2/2
✓ Branch 0 taken 77 times.
✓ Branch 1 taken 13 times.
90 if hasBraces {
312
4/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 11 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 11 times.
13 if i < length && str[i] == ':' { // Operand modifier like '${0:w}'
313 11 i++;
314
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 if i == length {
315 ✗ errorMessage = String("Missing operand modifier after ':'");
316 ✗ return false;
317 ✗ }
318 11 i++;
319 11 }
320
4/4
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 10 times.
✓ Branch 2 taken 10 times.
✓ Branch 3 taken 3 times.
13 if i == length || str[i] != '}' {
321 3 errorMessage = String("Unterminated '${' expression");
322 3 return false;
323 3 }
324 10 i++;
325 10 }
326 87 }
327
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 71 times.
73 if inVariant {
328 2 errorMessage = String("Unterminated variant '$('");
329 2 return false;
330 2 }
331 71 return true;
332 }
333
334 1 #[test]
335 f<bool> testParseAsmConstraints() {
336 1 Vector<AsmConstraint> constraints;
337 1 String errorMessage;
338 1 assert parseAsmConstraints(String(""), constraints, errorMessage);
339 1 assert constraints.isEmpty();
340
341 1 assert parseAsmConstraints(String("=r,0,r,~{memory},~{cc}"), constraints, errorMessage);
342 1 assert constraints.getSize() == 5l;
343 1 const AsmConstraint& output = constraints.get(0l);
344 1 assert output.isDirectOutput();
345 1 assert output.hasTiedInput;
346 1 const AsmConstraint& tiedInput = constraints.get(1l);
347 1 assert tiedInput.tiedOutputIdx == 0l;
348 1 assert tiedInput.hasArg();
349 1 const AsmConstraint& clobber = constraints.get(3l);
350 1 assert clobber.kind == AsmConstraintKind::CLOBBER;
351 1 assert !clobber.hasArg();
352
353 1 Vector<AsmConstraint> indirect;
354 1 assert parseAsmConstraints(String("=*m,=&{rax},*m,@3Upa,^Rg"), indirect, errorMessage);
355 1 const AsmConstraint& indirectOutput = indirect.get(0l);
356 1 assert indirectOutput.isIndirect;
357 1 assert indirectOutput.hasArg();
358 1 const AsmConstraint& registerOutput = indirect.get(1l);
359 1 assert registerOutput.isDirectOutput();
360 1 const AsmConstraint& indirectInput = indirect.get(2l);
361 1 assert indirectInput.isIndirect;
362 1 return true;
363 1 }
364
365 1 #[test]
366 f<bool> testParseAsmConstraintsErrors() {
367 1 const string[14] invalid = ["r,,r", "r,", "~memory", "!i", "=", "=&&r", "&r", "#r", "=r,1", "r,0", "=*m,0", "=r,0,0", "r,=r", "~{memory},r"];
368
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 1 time.
15 for unsigned long i = 0l; i < len(invalid); i++ {
369 14 Vector<AsmConstraint> constraints;
370 14 String errorMessage;
371 14 assert !parseAsmConstraints(String(invalid[i]), constraints, errorMessage);
372 14 assert !errorMessage.isEmpty();
373 14 }
374 1 Vector<AsmConstraint> constraints;
375 1 String errorMessage;
376 1 assert !parseAsmConstraints(String("=r,=r"), constraints, errorMessage);
377 1 assert errorMessage == "Only one direct output is supported. Use indirect outputs ('=*m') for further results";
378 1 return true;
379 1 }
380
381 1 #[test]
382 f<bool> testVerifyAsmTemplate() {
383 1 String errorMessage;
384 1 assert verifyAsmTemplate(String("addl $2, $0"), 3l, errorMessage);
385 1 assert verifyAsmTemplate(String("movq $$42, ${0:q} # ${:comment} ${:uid}"), 1l, errorMessage);
386 1 assert verifyAsmTemplate(String("$(movl$|mov$) $0"), 1l, errorMessage);
387 1 assert verifyAsmTemplate(String("nop"), 0l, errorMessage);
388
389 1 const string[8] invalid = ["movq $42, $0", "mov $1", "mov $", "${0", "${0:}", "${:foo}", "$($(", "$(mov"];
390
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 1 time.
9 for unsigned long i = 0l; i < len(invalid); i++ {
391 8 String message;
392 8 assert !verifyAsmTemplate(String(invalid[i]), 1l, message);
393 8 assert !message.isEmpty();
394 8 }
395 1 return true;
396 1 }
397