il: Print numbers without arena allocations

e0c7ad6ca2ca3c55bf1d73401de7ad9d8c944d48e19725b65cb87d9ee146daa9
Numeric output is consumed synchronously. Format into local buffers
to avoid temporary arena allocations and copies, and remove the
printer arena parameters from callers. Keep each register token in
one output write.

Assisted-by: Codex:gpt-6-astra
Alexis Sellier committed ago 1 parent d3a86b21
compiler/radiance.rad +1 -1
1041 1041
    let mut out = sexpr::Output::Stdout;
1042 1042
1043 1043
    if ctx.dump == Dump::Il {
1044 1044
        // Lower all packages into a single unified IL program for dumping.
1045 1045
        let program = try lowerAllPackages(ctx, res);
1046 -
        il::printer::printProgram(&mut out, &mut res.arena, &program);
1046 +
        il::printer::printProgram(&mut out, &program);
1047 1047
        io::print("\n");
1048 1048
        return;
1049 1049
    }
1050 1050
    if ctx.dump == Dump::Asm {
1051 1051
        let result = try lowerAndGenerateAllPackages(ctx, res, fnArena, CodegenOptions {
lib/std/lang/il/printer.rad +89 -113
1 1
//! IL pretty printer.
2 2
3 3
use std::fmt;
4 -
use std::mem;
5 4
use std::lang::sexpr;
6 -
use std::lang::alloc;
7 5
8 6
///////////////////////
9 7
// String formatting //
10 8
///////////////////////
11 9
12 -
/// Copy a byte slice into arena-allocated storage.
13 -
fn copyToArena(a: *mut alloc::Arena, text: *[u8]) -> *[u8] {
14 -
    let slice = try! alloc::allocSlice(a, 1, 1, text.len) as *mut [u8];
15 -
    try! mem::copy(slice, text);
16 -
    return slice;
17 -
}
18 -
19 -
/// Format a `u32` into a string in the arena.
20 -
fn formatU32(a: *mut alloc::Arena, val: u32) -> *[u8] {
10 +
/// Write a `u32` before its stack buffer leaves scope.
11 +
fn writeU32(out: *mut sexpr::Output, val: u32) {
21 12
    let mut digits: [u8; 10] = undefined;
22 -
    return copyToArena(a, fmt::formatU32(val, &mut digits[..]));
13 +
    write(out, fmt::formatU32(val, &mut digits[..]));
23 14
}
24 15
25 -
/// Format an `i32` into a string in the arena.
26 -
fn formatI32(a: *mut alloc::Arena, val: i32) -> *[u8] {
16 +
/// Write an `i32` before its stack buffer leaves scope.
17 +
fn writeI32(out: *mut sexpr::Output, val: i32) {
27 18
    let mut digits: [u8; 12] = undefined;
28 -
    return copyToArena(a, fmt::formatI32(val, &mut digits[..]));
19 +
    write(out, fmt::formatI32(val, &mut digits[..]));
29 20
}
30 21
31 -
/// Format an `i64` into a string in the arena.
32 -
fn formatI64(a: *mut alloc::Arena, val: i64) -> *[u8] {
22 +
/// Write an `i64` before its stack buffer leaves scope.
23 +
fn writeI64(out: *mut sexpr::Output, val: i64) {
33 24
    let mut digits: [u8; 20] = undefined;
34 -
    return copyToArena(a, fmt::formatI64(val, &mut digits[..]));
35 -
}
36 -
37 -
/// Concatenate prefix and string in the arena.
38 -
fn prefixStr(a: *mut alloc::Arena, prefix: u8, str: *[u8]) -> *[u8] {
39 -
    let len = str.len + 1;
40 -
    let ptr = try! alloc::allocSlice(a, 1, 1, len);
41 -
    let slice = ptr as *mut [u8];
42 -
    set slice[0] = prefix;
43 -
    try! mem::copy(&mut slice[1..], str);
44 -
45 -
    return slice;
46 -
}
47 -
48 -
/// Format a register reference (`%n`).
49 -
fn regStr(a: *mut alloc::Arena, reg: super::Reg) -> *[u8] {
50 -
    return prefixStr(a, '%', formatU32(a, reg.n));
25 +
    write(out, fmt::formatI64(val, &mut digits[..]));
51 26
}
52 27
53 28
/////////////////////
54 29
// Output helpers  //
55 30
/////////////////////
147 122
////////////////////
148 123
// Value printing //
149 124
////////////////////
150 125
151 126
/// Write a value (register, immediate, symbol, or undefined).
152 -
fn writeVal(out: *mut sexpr::Output, a: *mut alloc::Arena, val: super::Val) {
127 +
fn writeVal(out: *mut sexpr::Output, val: super::Val) {
153 128
    match val {
154 -
        case super::Val::Reg(reg) => write(out, regStr(a, reg)),
155 -
        case super::Val::Imm(v) => write(out, formatI64(a, v)),
129 +
        case super::Val::Reg(reg) => writeReg(out, reg),
130 +
        case super::Val::Imm(v) => writeI64(out, v),
156 131
        case super::Val::DataSym(name) => writeSymbol(out, name),
157 132
        case super::Val::FnAddr(name) => writeSymbol(out, name),
158 133
        case super::Val::Undef => write(out, "undefined"),
159 134
    }
160 135
}
161 136
162 -
/// Write a register.
163 -
fn writeReg(out: *mut sexpr::Output, a: *mut alloc::Arena, reg: super::Reg) {
164 -
    write(out, regStr(a, reg));
137 +
/// Write a register in one operation before its stack buffer leaves scope.
138 +
fn writeReg(out: *mut sexpr::Output, reg: super::Reg) {
139 +
    let mut buffer: [u8; 11] = undefined;
140 +
    let digits = fmt::formatU32(reg.n, &mut buffer[1..]);
141 +
    let start = buffer.len - digits.len - 1;
142 +
    set buffer[start] = '%';
143 +
    write(out, &buffer[start..]);
165 144
}
166 145
167 146
/// Write a comma-separated argument list in parentheses.
168 -
fn writeArgs(out: *mut sexpr::Output, a: *mut alloc::Arena, args: *[super::Val]) {
147 +
fn writeArgs(out: *mut sexpr::Output, args: *[super::Val]) {
169 148
    write(out, "(");
170 149
    for arg, i in args {
171 150
        if i > 0 {
172 151
            write(out, ", ");
173 152
        }
174 -
        writeVal(out, a, arg);
153 +
        writeVal(out, arg);
175 154
    }
176 155
    write(out, ")");
177 156
}
178 157
179 158
/// Write a typed parameter (e.g., `w32 %0`).
180 -
fn writeParam(out: *mut sexpr::Output, a: *mut alloc::Arena, param: super::Param) {
159 +
fn writeParam(out: *mut sexpr::Output, param: super::Param) {
181 160
    writeType(out, param.type);
182 161
    write(out, " ");
183 -
    writeReg(out, a, param.value);
162 +
    writeReg(out, param.value);
184 163
}
185 164
186 165
/// Write a comma-separated parameter list.
187 -
fn writeParams(out: *mut sexpr::Output, a: *mut alloc::Arena, params: *[super::Param]) {
166 +
fn writeParams(out: *mut sexpr::Output, params: *[super::Param]) {
188 167
    for param, i in params {
189 168
        if i > 0 {
190 169
            write(out, ", ");
191 170
        }
192 -
        writeParam(out, a, param);
171 +
        writeParam(out, param);
193 172
    }
194 173
}
195 174
196 175
//////////////////////////
197 176
// Instruction printing //
198 177
//////////////////////////
199 178
200 179
/// Write an instruction.
201 -
fn writeInstr(out: *mut sexpr::Output, a: *mut alloc::Arena, blocks: *[super::Block], inst: super::Instr) {
180 +
fn writeInstr(out: *mut sexpr::Output, blocks: *[super::Block], inst: super::Instr) {
202 181
    match inst {
203 182
        // Memory operations.
204 183
        case super::Instr::Reserve { dst, size, alignment } => {
205 184
            write(out, "reserve ");
206 -
            writeReg(out, a, dst);
185 +
            writeReg(out, dst);
207 186
            write(out, " ");
208 -
            writeVal(out, a, size);
187 +
            writeVal(out, size);
209 188
            write(out, " ");
210 -
            write(out, formatU32(a, alignment));
189 +
            writeU32(out, alignment);
211 190
        }
212 191
        case super::Instr::Load { dst, typ, src, offset } => {
213 192
            write(out, "load ");
214 193
            writeType(out, typ);
215 194
            write(out, " ");
216 -
            writeReg(out, a, dst);
195 +
            writeReg(out, dst);
217 196
            write(out, " ");
218 -
            writeReg(out, a, src);
197 +
            writeReg(out, src);
219 198
            write(out, " ");
220 -
            write(out, formatI32(a, offset));
199 +
            writeI32(out, offset);
221 200
        }
222 201
        case super::Instr::Sload { dst, typ, src, offset } => {
223 202
            write(out, "sload ");
224 203
            writeType(out, typ);
225 204
            write(out, " ");
226 -
            writeReg(out, a, dst);
205 +
            writeReg(out, dst);
227 206
            write(out, " ");
228 -
            writeReg(out, a, src);
207 +
            writeReg(out, src);
229 208
            write(out, " ");
230 -
            write(out, formatI32(a, offset));
209 +
            writeI32(out, offset);
231 210
        }
232 211
        case super::Instr::Store { typ, src, dst, offset } => {
233 212
            write(out, "store ");
234 213
            writeType(out, typ);
235 214
            write(out, " ");
236 -
            writeVal(out, a, src);
215 +
            writeVal(out, src);
237 216
            write(out, " ");
238 -
            writeReg(out, a, dst);
217 +
            writeReg(out, dst);
239 218
            write(out, " ");
240 -
            write(out, formatI32(a, offset));
219 +
            writeI32(out, offset);
241 220
        }
242 221
        case super::Instr::Blit { dst, src, size } => {
243 222
            write(out, "blit ");
244 -
            writeReg(out, a, dst);
223 +
            writeReg(out, dst);
245 224
            write(out, " ");
246 -
            writeReg(out, a, src);
225 +
            writeReg(out, src);
247 226
            write(out, " ");
248 -
            writeVal(out, a, size);
227 +
            writeVal(out, size);
249 228
        }
250 229
        case super::Instr::Copy { dst, val } => {
251 230
            write(out, "copy ");
252 -
            writeReg(out, a, dst);
231 +
            writeReg(out, dst);
253 232
            write(out, " ");
254 -
            writeVal(out, a, val);
233 +
            writeVal(out, val);
255 234
        }
256 235
257 236
        // ALU operations.
258 237
        case super::Instr::BinOp { op, dst, typ, a: va, b } =>
259 -
            writeTypedBinOp(out, a, binOpStr(op), typ, dst, va, b),
238 +
            writeTypedBinOp(out, binOpStr(op), typ, dst, va, b),
260 239
        case super::Instr::UnOp { op, dst, typ, a: va } =>
261 -
            writeTypedUnaryOp(out, a, unOpStr(op), typ, dst, va),
240 +
            writeTypedUnaryOp(out, unOpStr(op), typ, dst, va),
262 241
263 242
        // Conversion operations.
264 243
        case super::Instr::Zext { dst, typ, val } =>
265 -
            writeTypedUnaryOp(out, a, "zext", typ, dst, val),
244 +
            writeTypedUnaryOp(out, "zext", typ, dst, val),
266 245
        case super::Instr::Sext { dst, typ, val } =>
267 -
            writeTypedUnaryOp(out, a, "sext", typ, dst, val),
246 +
            writeTypedUnaryOp(out, "sext", typ, dst, val),
268 247
269 248
        // Call.
270 249
        case super::Instr::Call { dst, retTy, func, args } => {
271 250
            write(out, "call ");
272 251
            writeType(out, retTy);
273 252
            write(out, " ");
274 253
            if let d = dst {
275 -
                writeReg(out, a, d);
254 +
                writeReg(out, d);
276 255
                write(out, " ");
277 256
            }
278 -
            writeVal(out, a, func);
279 -
            writeArgs(out, a, args);
257 +
            writeVal(out, func);
258 +
            writeArgs(out, args);
280 259
        }
281 260
282 261
        // Terminators.
283 262
        case super::Instr::Ret { val } => {
284 263
            write(out, "ret");
285 264
            if let v = val {
286 265
                write(out, " ");
287 -
                writeVal(out, a, v);
266 +
                writeVal(out, v);
288 267
            }
289 268
        }
290 269
        case super::Instr::Jmp { target, args } => {
291 270
            write(out, "jmp @");
292 271
            write(out, blocks[target].label);
293 272
            if args.len > 0 {
294 -
                writeArgs(out, a, args);
273 +
                writeArgs(out, args);
295 274
            }
296 275
        }
297 276
        case super::Instr::Br { op, typ, a: va, b: vb, thenTarget, thenArgs, elseTarget, elseArgs } => {
298 277
            write(out, "br.");
299 278
            match op {
303 282
                case super::CmpOp::Ult => write(out, "ult"),
304 283
            }
305 284
            write(out, " ");
306 285
            writeType(out, typ);
307 286
            write(out, " ");
308 -
            writeVal(out, a, va);
287 +
            writeVal(out, va);
309 288
            write(out, " ");
310 -
            writeVal(out, a, vb);
289 +
            writeVal(out, vb);
311 290
            write(out, " @");
312 291
            write(out, blocks[thenTarget].label);
313 292
            if thenArgs.len > 0 {
314 -
                writeArgs(out, a, thenArgs);
293 +
                writeArgs(out, thenArgs);
315 294
            }
316 295
            write(out, " @");
317 296
            write(out, blocks[elseTarget].label);
318 297
            if elseArgs.len > 0 {
319 -
                writeArgs(out, a, elseArgs);
298 +
                writeArgs(out, elseArgs);
320 299
            }
321 300
        }
322 301
        case super::Instr::Switch { val, defaultTarget, defaultArgs, cases } => {
323 302
            write(out, "switch ");
324 -
            writeVal(out, a, val);
303 +
            writeVal(out, val);
325 304
            for c in cases {
326 305
                write(out, " (");
327 -
                write(out, formatI64(a, c.value));
306 +
                writeI64(out, c.value);
328 307
                write(out, " @");
329 308
                write(out, blocks[c.target].label);
330 309
                if c.args.len > 0 {
331 -
                    writeArgs(out, a, c.args);
310 +
                    writeArgs(out, c.args);
332 311
                }
333 312
                write(out, ")");
334 313
            }
335 314
            write(out, " @");
336 315
            write(out, blocks[defaultTarget].label);
337 316
            if defaultArgs.len > 0 {
338 -
                writeArgs(out, a, defaultArgs);
317 +
                writeArgs(out, defaultArgs);
339 318
            }
340 319
        }
341 320
        case super::Instr::Unreachable => {
342 321
            write(out, "unreachable");
343 322
        }
344 323
345 324
        // Intrinsics.
346 325
        case super::Instr::Ecall { dst, num, a0, a1, a2, a3 } => {
347 326
            write(out, "ecall ");
348 -
            writeReg(out, a, dst);
327 +
            writeReg(out, dst);
349 328
            write(out, " ");
350 -
            writeVal(out, a, num);
329 +
            writeVal(out, num);
351 330
            write(out, " ");
352 -
            writeVal(out, a, a0);
331 +
            writeVal(out, a0);
353 332
            write(out, " ");
354 -
            writeVal(out, a, a1);
333 +
            writeVal(out, a1);
355 334
            write(out, " ");
356 -
            writeVal(out, a, a2);
335 +
            writeVal(out, a2);
357 336
            write(out, " ");
358 -
            writeVal(out, a, a3);
337 +
            writeVal(out, a3);
359 338
        }
360 339
        case super::Instr::Ebreak => {
361 340
            write(out, "ebreak");
362 341
        }
363 342
        case super::Instr::MemoryFence => {
367 346
}
368 347
369 348
/// Write a typed binary operation: `op type %dst %a %b`.
370 349
fn writeTypedBinOp(
371 350
    out: *mut sexpr::Output,
372 -
    a: *mut alloc::Arena,
373 351
    name: *[u8],
374 352
    typ: super::Type,
375 353
    dst: super::Reg,
376 354
    va: super::Val,
377 355
    vb: super::Val
378 356
) {
379 357
    write(out, name);
380 358
    write(out, " ");
381 359
    writeType(out, typ);
382 360
    write(out, " ");
383 -
    writeReg(out, a, dst);
361 +
    writeReg(out, dst);
384 362
    write(out, " ");
385 -
    writeVal(out, a, va);
363 +
    writeVal(out, va);
386 364
    write(out, " ");
387 -
    writeVal(out, a, vb);
365 +
    writeVal(out, vb);
388 366
}
389 367
390 368
/// Write a typed unary operation: `op type %dst %val`.
391 369
fn writeTypedUnaryOp(
392 370
    out: *mut sexpr::Output,
393 -
    a: *mut alloc::Arena,
394 371
    name: *[u8],
395 372
    typ: super::Type,
396 373
    dst: super::Reg,
397 374
    val: super::Val
398 375
) {
399 376
    write(out, name);
400 377
    write(out, " ");
401 378
    writeType(out, typ);
402 379
    write(out, " ");
403 -
    writeReg(out, a, dst);
380 +
    writeReg(out, dst);
404 381
    write(out, " ");
405 -
    writeVal(out, a, val);
382 +
    writeVal(out, val);
406 383
}
407 384
408 385
////////////////////
409 386
// Block printing //
410 387
////////////////////
411 388
412 389
/// Write a basic block.
413 -
fn writeBlock(out: *mut sexpr::Output, a: *mut alloc::Arena, blocks: *[super::Block], block: *super::Block) {
390 +
fn writeBlock(out: *mut sexpr::Output, blocks: *[super::Block], block: *super::Block) {
414 391
    // Block label.
415 392
    write(out, "  @");
416 393
    write(out, block.label);
417 394
418 395
    // Block parameters.
419 396
    if block.params.len > 0 {
420 397
        write(out, "(");
421 -
        writeParams(out, a, block.params);
398 +
        writeParams(out, block.params);
422 399
        write(out, ")");
423 400
    }
424 401
    write(out, "\n");
425 402
426 403
    // Instructions.
427 404
    for instr in block.instrs {
428 405
        indent(out, 1);
429 -
        writeInstr(out, a, blocks, instr);
406 +
        writeInstr(out, blocks, instr);
430 407
        write(out, ";\n");
431 408
    }
432 409
}
433 410
434 411
///////////////////////
435 412
// Function printing //
436 413
///////////////////////
437 414
438 415
/// Write a function.
439 -
fn writeFn(out: *mut sexpr::Output, a: *mut alloc::Arena, f: *super::Fn) {
416 +
fn writeFn(out: *mut sexpr::Output, f: *super::Fn) {
440 417
    // Function signature.
441 418
    if f.isExtern {
442 419
        write(out, "extern ");
443 420
    }
444 421
    write(out, "fn ");
445 422
    writeType(out, f.returnType);
446 423
    write(out, " ");
447 424
    writeSymbol(out, f.name);
448 425
    write(out, "(");
449 -
    writeParams(out, a, f.params);
426 +
    writeParams(out, f.params);
450 427
    write(out, ")");
451 428
452 429
    // Extern functions have no body.
453 430
    if f.isExtern {
454 431
        write(out, ";\n");
457 434
    write(out, " {\n");
458 435
459 436
    // Blocks.
460 437
    for i in 0..f.blocks.len {
461 438
        if f.blocks[i].instrs.len > 0 {
462 -
            writeBlock(out, a, f.blocks, &f.blocks[i]);
439 +
            writeBlock(out, f.blocks, &f.blocks[i]);
463 440
        }
464 441
    }
465 442
    write(out, "}\n");
466 443
}
467 444
468 445
///////////////////
469 446
// Data printing //
470 447
///////////////////
471 448
472 449
/// Write a data item.
473 -
fn writeDataItem(out: *mut sexpr::Output, a: *mut alloc::Arena, item: super::DataItem) {
450 +
fn writeDataItem(out: *mut sexpr::Output, item: super::DataItem) {
474 451
    match item {
475 452
        case super::DataItem::Val { typ, val } => {
476 453
            writeType(out, typ);
477 454
            write(out, " ");
478 -
            write(out, formatI64(a, val));
455 +
            writeI64(out, val);
479 456
        }
480 457
        case super::DataItem::Sym(name) => {
481 458
            write(out, "sym ");
482 459
            writeSymbol(out, name);
483 460
        }
494 471
        }
495 472
    }
496 473
}
497 474
498 475
/// Write a data value (item with optional repeat count).
499 -
fn writeDataValue(out: *mut sexpr::Output, a: *mut alloc::Arena, value: super::DataValue) {
500 -
    writeDataItem(out, a, value.item);
476 +
fn writeDataValue(out: *mut sexpr::Output, value: super::DataValue) {
477 +
    writeDataItem(out, value.item);
501 478
    if value.count > 1 {
502 479
        write(out, " * ");
503 -
        write(out, formatU32(a, value.count));
480 +
        writeU32(out, value.count);
504 481
    }
505 482
}
506 483
507 484
/// Write global data.
508 -
fn writeData(out: *mut sexpr::Output, a: *mut alloc::Arena, d: super::Data) {
485 +
fn writeData(out: *mut sexpr::Output, d: super::Data) {
509 486
    write(out, "data ");
510 487
    if not d.readOnly {
511 488
        write(out, "mut ");
512 489
    }
513 490
    writeSymbol(out, d.name);
514 491
    write(out, " align ");
515 -
    write(out, formatU32(a, d.alignment));
492 +
    writeU32(out, d.alignment);
516 493
    write(out, " {\n");
517 494
518 495
    for v in d.values {
519 496
        indent(out, 1);
520 -
        writeDataValue(out, a, v);
497 +
        writeDataValue(out, v);
521 498
        write(out, ";\n");
522 499
    }
523 500
    write(out, "}\n");
524 501
}
525 502
526 503
//////////////////////
527 504
// Program printing //
528 505
//////////////////////
529 506
530 507
/// Print a program.
531 -
export fn printProgram(out: *mut sexpr::Output, a: *mut alloc::Arena, program: *super::Program) {
508 +
export fn printProgram(out: *mut sexpr::Output, program: *super::Program) {
532 509
    // Data declarations.
533 510
    for data, i in program.data {
534 -
        writeData(out, a, data);
511 +
        writeData(out, data);
535 512
        if i < program.data.len - 1 or program.fns.len > 0 {
536 513
            write(out, "\n");
537 514
        }
538 515
    }
539 516
    // Functions.
540 517
    for func, i in program.fns {
541 -
        writeFn(out, a, func);
518 +
        writeFn(out, func);
542 519
        if i < program.fns.len - 1 {
543 520
            write(out, "\n");
544 521
        }
545 522
    }
546 523
}
547 524
548 525
/// Print a program to a buffer, returning the written slice.
549 526
export fn printProgramToBuffer(
550 527
    program: *super::Program,
551 -
    arena: *mut alloc::Arena,
552 528
    buf: *mut [u8]
553 529
) -> *[u8] {
554 530
    let mut pos: u32 = 0;
555 531
    let mut out = sexpr::Output::Buffer { buf, pos: &mut pos };
556 -
    printProgram(&mut out, arena, program);
532 +
    printProgram(&mut out, program);
557 533
558 534
    return &buf[..pos];
559 535
}
test/runner.rad +1 -3
51 51
static SOURCE_BUF: [u8; SOURCE_BUF_SIZE] = undefined;
52 52
static EXPECTED_BUF: [u8; EXPECTED_BUF_SIZE] = undefined;
53 53
static OUTPUT_BUF: [u8; OUTPUT_BUF_SIZE] = undefined;
54 54
static AST_ARENA_STORAGE: [u8; ARENA_SIZE] = undefined;
55 55
static IL_ARENA_STORAGE: [u8; ARENA_SIZE] = undefined;
56 -
static PRINT_ARENA_STORAGE: [u8; ARENA_SIZE] = undefined;
57 56
static RESOLVER_ARENA_STORAGE: [u8; ARENA_SIZE] = undefined;
58 57
static NODE_DATA_STORAGE: [resolver::NodeData; MAX_NODE_DATA] = undefined;
59 58
static ERROR_STORAGE: [resolver::Error; MAX_ERRORS] = undefined;
60 59
static ASM_TEXT_STORAGE: [u32; ASM_TEXT_CAPACITY] = undefined;
61 60
static ASM_DATA_STORAGE: [u8; ASM_DATA_CAPACITY] = undefined;
274 273
        io::printLn("");
275 274
        return false;
276 275
    };
277 276
278 277
    // Print IL to buffer.
279 -
    let mut printArena = alloc::new(&mut PRINT_ARENA_STORAGE[..]);
280 -
    let actual = printer::printProgramToBuffer(&program, &mut printArena, &mut OUTPUT_BUF[..]);
278 +
    let actual = printer::printProgramToBuffer(&program, &mut OUTPUT_BUF[..]);
281 279
282 280
    // Compare ignoring comments.
283 281
    if not stringsEqual(actual, expected) {
284 282
        io::printLn("FAILED");
285 283
        printDiff(expected, actual);