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 | 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 | 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 | 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 | 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 | 281 | if str[i] == '~' { | |
| 69 | 30 | constraint.kind = AsmConstraintKind::CLOBBER; | |
| 70 | 30 | i++; | |
| 71 | 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 | 279 | } else if str[i] == '=' { | |
| 76 | 105 | constraint.kind = AsmConstraintKind::OUTPUT; | |
| 77 | 105 | i++; | |
| 78 | 251 | } else if str[i] == '!' { | |
| 79 | 2 | errorMessage = "Label constraint '" + entry + "' is not supported"; | |
| 80 | 2 | return false; | |
| 81 | 251 | } | |
| 82 | 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 | 285 | while i < end { | |
| 91 | 284 | const char c = str[i]; | |
| 92 | 284 | if c == '&' { | |
| 93 | 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 | 281 | } else if c == '%' { | |
| 99 | ✗ | 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 | 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 | 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 | 538 | while i < end { | |
| 119 | 272 | const char c = str[i]; | |
| 120 | 272 | if c == '{' { // Physical register | |
| 121 | 40 | const long closeIdx = str.find('}', i); | |
| 122 | 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 | 271 | } else if isAsmDigit(c) { // Tied operand | |
| 128 | 38 | long outputIdx = 0l; | |
| 129 | 76 | while i < end && isAsmDigit(str[i]) { | |
| 130 | 38 | outputIdx = outputIdx * 10l + cast<long>(getAsmDigitValue(str[i])); | |
| 131 | 38 | i++; | |
| 132 | 38 | } | |
| 133 | 38 | if constraint.kind != AsmConstraintKind::INPUT { | |
| 134 | ✗ | errorMessage = "Constraint '" + entry + "' ties an operand, which is only allowed on inputs"; | |
| 135 | ✗ | return false; | |
| 136 | ✗ | } | |
| 137 | 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 | 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 | 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 | 227 | } else if c == '|' { | |
| 153 | ✗ | errorMessage = "Constraint '" + entry + "' uses multiple alternatives, which are not supported"; | |
| 154 | ✗ | return false; | |
| 155 | 194 | } else if c == '^' { // Two-letter constraint code | |
| 156 | 1 | if i + 3l > end { | |
| 157 | ✗ | errorMessage = "Constraint '" + entry + "' contains an incomplete '^' constraint code"; | |
| 158 | ✗ | return false; | |
| 159 | ✗ | } | |
| 160 | 1 | i += 3l; | |
| 161 | 194 | } else if c == '@' { // Constraint code with the given number of letters | |
| 162 | 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 | 421 | while start < length { | |
| 191 | 283 | long end = str.find(',', start); | |
| 192 | 283 | if end == -1l { | |
| 193 | 133 | end = cast<long>(length); | |
| 194 | 133 | } | |
| 195 | 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 | 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 | 390 | foreach const AsmConstraint& constraint : constraints { | |
| 210 | 256 | if constraint.kind == AsmConstraintKind::OUTPUT { | |
| 211 | 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 | 96 | if !constraint.isIndirect { | |
| 216 | 81 | numDirectOutputs++; | |
| 217 | 81 | } | |
| 218 | 254 | } else if constraint.kind == AsmConstraintKind::INPUT { | |
| 219 | 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 | 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 | 745 | while i < length { | |
| 249 | 672 | if str[i] != '$' { | |
| 250 | 554 | i++; | |
| 251 | 554 | continue; | |
| 252 | } | ||
| 253 | 118 | i++; // Consume '$' | |
| 254 | |||
| 255 | // Escapes and dialect variants | ||
| 256 | 118 | if i < length && str[i] == '$' { | |
| 257 | 6 | i++; | |
| 258 | 6 | continue; | |
| 259 | } | ||
| 260 | 112 | if i < length && str[i] == '(' { | |
| 261 | 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 | 107 | if i < length && (str[i] == '|' || str[i] == ')') { | |
| 270 | 2 | if str[i] == ')' { | |
| 271 | 1 | inVariant = false; | |
| 272 | 1 | } | |
| 273 | 2 | i++; | |
| 274 | 2 | continue; | |
| 275 | } | ||
| 276 | |||
| 277 | // Special strings like '${:uid}' | ||
| 278 | 105 | const bool hasBraces = i < length && str[i] == '{'; | |
| 279 | 105 | if hasBraces { | |
| 280 | 23 | i++; | |
| 281 | 23 | if i < length && str[i] == ':' { | |
| 282 | 10 | const long closeIdx = str.find('}', i); | |
| 283 | 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 | 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 | 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 | 190 | while i < length && isAsmDigit(str[i]) { | |
| 304 | 96 | operandIdx = operandIdx * 10l + getAsmDigitValue(str[i]); | |
| 305 | 96 | i++; | |
| 306 | 96 | } | |
| 307 | 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 | 90 | if hasBraces { | |
| 312 | 13 | if i < length && str[i] == ':' { // Operand modifier like '${0:w}' | |
| 313 | 11 | i++; | |
| 314 | 11 | if i == length { | |
| 315 | ✗ | errorMessage = String("Missing operand modifier after ':'"); | |
| 316 | ✗ | return false; | |
| 317 | ✗ | } | |
| 318 | 11 | i++; | |
| 319 | 11 | } | |
| 320 | 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 | 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 | 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 | 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 |