Commit 9dbe684854

Andrew Kelley <andrew@ziglang.org>
2021-07-08 20:21:06
C backend: cleanups to wrapping int operations
* less branching by passing parameters in the main op code switch. * properly pass the target when asking the type system for int info. * handle u8, i16, etc when it is represented using int_unsigned/int_signed tag. * compile error instead of assertion failure for unimplemented cases (greater than 64 bits integer). * control flow cleanups * zig.h: expand macros into inline functions * reduce the complexity of the test case by making it one test case that calls multiple functions. Also fix the problem of c_int max value mismatch between host and target.
1 parent fb16633
Changed files (3)
src
codegen
link
test
stage2
src/codegen/c.zig
@@ -846,15 +846,15 @@ pub fn genBody(o: *Object, body: ir.Body) error{ AnalysisFail, OutOfMemory }!voi
             // TODO use a different strategy for add that communicates to the optimizer
             // that wrapping is UB.
             .add => try genBinOp(o, inst.castTag(.add).?, " + "),
-            .addwrap => try genWrapOp(o, .add, inst.castTag(.addwrap).?),
+            .addwrap => try genWrapOp(o, inst.castTag(.addwrap).?, " + ", "addw_"),
             // TODO use a different strategy for sub that communicates to the optimizer
             // that wrapping is UB.
             .sub => try genBinOp(o, inst.castTag(.sub).?, " - "),
-            .subwrap => try genWrapOp(o, .sub, inst.castTag(.subwrap).?),
+            .subwrap => try genWrapOp(o, inst.castTag(.subwrap).?, " - ", "subw_"),
             // TODO use a different strategy for mul that communicates to the optimizer
             // that wrapping is UB.
             .mul => try genBinOp(o, inst.castTag(.sub).?, " * "),
-            .mulwrap => try genWrapOp(o, .mul, inst.castTag(.mulwrap).?),
+            .mulwrap => try genWrapOp(o, inst.castTag(.mulwrap).?, " * ", "mulw_"),
             // TODO use a different strategy for div that communicates to the optimizer
             // that wrapping is UB.
             .div => try genBinOp(o, inst.castTag(.div).?, " / "),
@@ -1039,44 +1039,44 @@ fn genStore(o: *Object, inst: *Inst.BinOp) !CValue {
     return CValue.none;
 }
 
-const WrappingOp = enum {
-    add,
-    sub,
-    mul,
-};
-
-fn genWrapOp(o: *Object, op: WrappingOp, inst: *Inst.BinOp) !CValue {
+fn genWrapOp(o: *Object, inst: *Inst.BinOp, str_op: [*:0]const u8, fn_op: [*:0]const u8) !CValue {
     if (inst.base.isUnused())
         return CValue.none;
 
-    const is_signed = inst.base.ty.isSignedInt();
+    const int_info = inst.base.ty.intInfo(o.dg.module.getTarget());
+    const bits = int_info.bits;
 
     // if it's an unsigned int with non-arbitrary bit size then we can just add
-    if (!is_signed and inst.base.ty.tag() != .int_unsigned) {
-        return try genBinOp(o, inst, switch (op) {
-            .add => " + ",
-            .sub => " - ",
-            .mul => " * ",
-        });
+    if (int_info.signedness == .unsigned) {
+        const ok_bits = switch (bits) {
+            8, 16, 32, 64, 128 => true,
+            else => false,
+        };
+        if (ok_bits or inst.base.ty.tag() != .int_unsigned) {
+            return try genBinOp(o, inst, str_op);
+        }
+    }
+
+    if (bits > 64) {
+        return o.dg.fail(.{ .node_offset = 0 }, "TODO: C backend: genWrapOp for large integers", .{});
     }
 
     var min_buf: [80]u8 = undefined;
-    const min = if (!is_signed)
-        "0"
-    else switch (inst.base.ty.tag()) {
-        .c_short => "SHRT_MIN",
-        .c_int => "INT_MIN",
-        .c_long => "LONG_MIN",
-        .c_longlong => "LLONG_MIN",
-        .isize => "INTPTR_MIN",
-        else => blk: {
-            // should be able to use undefined here since all the target specifics are handled
-            const bits = inst.base.ty.intInfo(@as(std.Target, undefined)).bits;
-            assert(bits <= 64); // TODO: large integers
-            const val = -1 * std.math.pow(i64, 2, @intCast(i64, bits - 1));
-            break :blk std.fmt.bufPrint(&min_buf, "{}", .{val}) catch |e|
-            // doesn't fit in some upwards error set, but should never happen
-                return if (e == error.NoSpaceLeft) unreachable else e;
+    const min = switch (int_info.signedness) {
+        .unsigned => "0",
+        else => switch (inst.base.ty.tag()) {
+            .c_short => "SHRT_MIN",
+            .c_int => "INT_MIN",
+            .c_long => "LONG_MIN",
+            .c_longlong => "LLONG_MIN",
+            .isize => "INTPTR_MIN",
+            else => blk: {
+                const val = -1 * std.math.pow(i64, 2, @intCast(i64, bits - 1));
+                break :blk std.fmt.bufPrint(&min_buf, "{d}", .{val}) catch |err| switch (err) {
+                    error.NoSpaceLeft => unreachable,
+                    else => |e| return e,
+                };
+            },
         },
     };
 
@@ -1093,13 +1093,15 @@ fn genWrapOp(o: *Object, op: WrappingOp, inst: *Inst.BinOp) !CValue {
         .isize => "INTPTR_MAX",
         .usize => "UINTPTR_MAX",
         else => blk: {
-            // should be able to use undefined here since all the target specifics are handled
-            const bits = inst.base.ty.intInfo(@as(std.Target, undefined)).bits;
-            assert(bits <= 64); // TODO: large integers
-            const val = std.math.pow(u64, 2, if (is_signed) (bits - 1) else bits) - 1;
-            break :blk std.fmt.bufPrint(&max_buf, "{}", .{val}) catch |e|
-            // doesn't fit in some upwards error set, but should never happen
-                return if (e == error.NoSpaceLeft) unreachable else e;
+            const pow_bits = switch (int_info.signedness) {
+                .signed => bits - 1,
+                .unsigned => bits,
+            };
+            const val = std.math.pow(u64, 2, pow_bits) - 1;
+            break :blk std.fmt.bufPrint(&max_buf, "{}", .{val}) catch |err| switch (err) {
+                error.NoSpaceLeft => unreachable,
+                else => |e| return e,
+            };
         },
     };
 
@@ -1108,45 +1110,28 @@ fn genWrapOp(o: *Object, op: WrappingOp, inst: *Inst.BinOp) !CValue {
     const w = o.writer();
 
     const ret = try o.allocLocal(inst.base.ty, .Mut);
-    try w.writeAll(" = zig_");
-    try w.writeAll(switch (op) {
-        .add => "addw_",
-        .sub => "subw_",
-        .mul => return o.dg.fail(.{ .node_offset = 0 }, "TODO: C backend: implement wrapping multiplication operator", .{}),
-    });
+    try w.print(" = zig_{s}", .{fn_op});
 
     switch (inst.base.ty.tag()) {
-        .u8 => try w.writeAll("u8"),
-        .i8 => try w.writeAll("i8"),
-        .u16 => try w.writeAll("u16"),
-        .i16 => try w.writeAll("i16"),
-        .u32 => try w.writeAll("u32"),
-        .i32 => try w.writeAll("i32"),
-        .u64 => try w.writeAll("u64"),
-        .i64 => try w.writeAll("i64"),
         .isize => try w.writeAll("isize"),
         .c_short => try w.writeAll("short"),
         .c_int => try w.writeAll("int"),
         .c_long => try w.writeAll("long"),
         .c_longlong => try w.writeAll("longlong"),
-        .int_signed, .int_unsigned => {
-            if (is_signed) {
-                try w.writeByte('i');
-            } else {
-                try w.writeByte('u');
-            }
-
-            const info_bits = inst.base.ty.intInfo(@as(std.Target, undefined)).bits;
-            inline for (.{ 8, 16, 32, 64 }) |nbits| {
-                if (info_bits <= nbits) {
-                    try w.print("{d}", .{nbits});
+        else => {
+            const prefix_byte: u8 = switch (int_info.signedness) {
+                .signed => 'i',
+                .unsigned => 'u',
+            };
+            for ([_]u8{ 8, 16, 32, 64 }) |nbits| {
+                if (bits <= nbits) {
+                    try w.print("{c}{d}", .{ prefix_byte, nbits });
                     break;
                 }
             } else {
-                return o.dg.fail(.{ .node_offset = 0 }, "TODO: C backend: implement integer types larger than 64 bits", .{});
+                unreachable;
             }
         },
-        else => unreachable,
     }
 
     try w.writeByte('(');
@@ -1154,7 +1139,7 @@ fn genWrapOp(o: *Object, op: WrappingOp, inst: *Inst.BinOp) !CValue {
     try w.writeAll(", ");
     try o.writeCValue(w, rhs);
 
-    if (is_signed) {
+    if (int_info.signedness == .signed) {
         try w.print(", {s}", .{min});
     }
 
@@ -1164,7 +1149,7 @@ fn genWrapOp(o: *Object, op: WrappingOp, inst: *Inst.BinOp) !CValue {
     return ret;
 }
 
-fn genBinOp(o: *Object, inst: *Inst.BinOp, operator: []const u8) !CValue {
+fn genBinOp(o: *Object, inst: *Inst.BinOp, operator: [*:0]const u8) !CValue {
     if (inst.base.isUnused())
         return CValue.none;
 
@@ -1176,7 +1161,7 @@ fn genBinOp(o: *Object, inst: *Inst.BinOp, operator: []const u8) !CValue {
 
     try writer.writeAll(" = ");
     try o.writeCValue(writer, lhs);
-    try writer.writeAll(operator);
+    try writer.print("{s}", .{operator});
     try o.writeCValue(writer, rhs);
     try writer.writeAll(";\n");
 
src/link/C/zig.h
@@ -60,51 +60,6 @@
 #define zig_breakpoint() raise(SIGTRAP)
 #endif
 
-
-#define ZIG_UADDW(Type, lhs, rhs, max)                      \
-    Type thresh = max - rhs;                                \
-    if (lhs > thresh) {                                     \
-        return lhs - thresh - 1;                            \
-    } else {                                                \
-        return lhs + rhs;                                   \
-    }
-
-#define ZIG_SADDW(Type, lhs, rhs, min, max)                 \
-    if ((lhs > 0) && (rhs > 0)) {                           \
-        Type thresh = max - rhs;                            \
-        if (lhs > thresh) {                                 \
-            return min + lhs - thresh - 1;                  \
-        }                                                   \
-    } else if ((lhs < 0) && (rhs < 0)) {                    \
-        Type thresh = min - rhs;                            \
-        if (lhs < thresh) {                                 \
-            return max + lhs - thresh + 1;                  \
-        }                                                   \
-    }                                                       \
-                                                            \
-    return lhs + rhs;
-
-#define ZIG_USUBW(lhs, rhs, max)                            \
-    if (lhs < rhs) {                                        \
-        return max - rhs - lhs + 1;                         \
-    } else {                                                \
-        return lhs - rhs;                                   \
-    }
-
-#define ZIG_SSUBW(Type, lhs, rhs, min, max)                 \
-    if ((lhs > 0) && (rhs < 0)) {                           \
-        Type thresh = lhs - max;                            \
-        if (rhs < thresh) {                                 \
-            return min + (thresh - rhs - 1);                \
-        }                                                   \
-    } else if ((lhs < 0) && (rhs > 0)) {                    \
-        Type thresh = lhs - min;                            \
-        if (rhs > thresh) {                                 \
-            return max - (rhs - thresh - 1);                \
-        }                                                   \
-    }                                                       \
-    return lhs - rhs;
-
 #include <stdint.h>
 #include <stddef.h>
 #include <limits.h>
@@ -112,37 +67,100 @@
 #define uint128_t unsigned __int128
 ZIG_EXTERN_C void *memcpy (void *ZIG_RESTRICT, const void *ZIG_RESTRICT, size_t);
 
-/* Wrapping addition operators */
 static inline uint8_t zig_addw_u8(uint8_t lhs, uint8_t rhs, uint8_t max) {
-    ZIG_UADDW(uint8_t, lhs, rhs, max);
+    uint8_t thresh = max - rhs;
+    if (lhs > thresh) {
+        return lhs - thresh - 1;
+    } else {
+        return lhs + rhs;
+    }
 }
 
 static inline int8_t zig_addw_i8(int8_t lhs, int8_t rhs, int8_t min, int8_t max) {
-    ZIG_SADDW(int8_t, lhs, rhs, min, max);
+    if ((lhs > 0) && (rhs > 0)) {
+        int8_t thresh = max - rhs;
+        if (lhs > thresh) {
+            return min + lhs - thresh - 1;
+        }
+    } else if ((lhs < 0) && (rhs < 0)) {
+        int8_t thresh = min - rhs;
+        if (lhs < thresh) {
+            return max + lhs - thresh + 1;
+        }
+    }
+    return lhs + rhs;
 }
 
 static inline uint16_t zig_addw_u16(uint16_t lhs, uint16_t rhs, uint16_t max) {
-    ZIG_UADDW(uint16_t, lhs, rhs, max);
+    uint16_t thresh = max - rhs;
+    if (lhs > thresh) {
+        return lhs - thresh - 1;
+    } else {
+        return lhs + rhs;
+    }
 }
 
 static inline int16_t zig_addw_i16(int16_t lhs, int16_t rhs, int16_t min, int16_t max) {
-    ZIG_SADDW(int16_t, lhs, rhs, min, max);
+    if ((lhs > 0) && (rhs > 0)) {
+        int16_t thresh = max - rhs;
+        if (lhs > thresh) {
+            return min + lhs - thresh - 1;
+        }
+    } else if ((lhs < 0) && (rhs < 0)) {
+        int16_t thresh = min - rhs;
+        if (lhs < thresh) {
+            return max + lhs - thresh + 1;
+        }
+    }
+    return lhs + rhs;
 }
 
 static inline uint32_t zig_addw_u32(uint32_t lhs, uint32_t rhs, uint32_t max) {
-    ZIG_UADDW(uint32_t, lhs, rhs, max);
+    uint32_t thresh = max - rhs;
+    if (lhs > thresh) {
+        return lhs - thresh - 1;
+    } else {
+        return lhs + rhs;
+    }
 }
 
 static inline int32_t zig_addw_i32(int32_t lhs, int32_t rhs, int32_t min, int32_t max) {
-    ZIG_SADDW(int32_t, lhs, rhs, min, max);
+    if ((lhs > 0) && (rhs > 0)) {
+        int32_t thresh = max - rhs;
+        if (lhs > thresh) {
+            return min + lhs - thresh - 1;
+        }
+    } else if ((lhs < 0) && (rhs < 0)) {
+        int32_t thresh = min - rhs;
+        if (lhs < thresh) {
+            return max + lhs - thresh + 1;
+        }
+    }
+    return lhs + rhs;
 }
 
 static inline uint64_t zig_addw_u64(uint64_t lhs, uint64_t rhs, uint64_t max) {
-    ZIG_UADDW(uint64_t, lhs, rhs, max);
+    uint64_t thresh = max - rhs;
+    if (lhs > thresh) {
+        return lhs - thresh - 1;
+    } else {
+        return lhs + rhs;
+    }
 }
 
 static inline int64_t zig_addw_i64(int64_t lhs, int64_t rhs, int64_t min, int64_t max) {
-    ZIG_SADDW(int64_t, lhs, rhs, min, max);
+    if ((lhs > 0) && (rhs > 0)) {
+        int64_t thresh = max - rhs;
+        if (lhs > thresh) {
+            return min + lhs - thresh - 1;
+        }
+    } else if ((lhs < 0) && (rhs < 0)) {
+        int64_t thresh = min - rhs;
+        if (lhs < thresh) {
+            return max + lhs - thresh + 1;
+        }
+    }
+    return lhs + rhs;
 }
 
 static inline intptr_t zig_addw_isize(intptr_t lhs, intptr_t rhs, intptr_t min, intptr_t max) {
@@ -165,37 +183,96 @@ static inline long long zig_addw_longlong(long long lhs, long long rhs, long lon
     return (long long)(((unsigned long long)lhs) + ((unsigned long long)rhs));
 }
 
-/* Wrapping subtraction operators */
 static inline uint8_t zig_subw_u8(uint8_t lhs, uint8_t rhs, uint8_t max) {
-    ZIG_USUBW(lhs, rhs, max);
+    if (lhs < rhs) {
+        return max - rhs - lhs + 1;
+    } else {
+        return lhs - rhs;
+    }
 }
 
 static inline int8_t zig_subw_i8(int8_t lhs, int8_t rhs, int8_t min, int8_t max) {
-    ZIG_SSUBW(int8_t, lhs, rhs, min, max);
+    if ((lhs > 0) && (rhs < 0)) {
+        int8_t thresh = lhs - max;
+        if (rhs < thresh) {
+            return min + (thresh - rhs - 1);
+        }
+    } else if ((lhs < 0) && (rhs > 0)) {
+        int8_t thresh = lhs - min;
+        if (rhs > thresh) {
+            return max - (rhs - thresh - 1);
+        }
+    }
+    return lhs - rhs;
 }
 
 static inline uint16_t zig_subw_u16(uint16_t lhs, uint16_t rhs, uint16_t max) {
-    ZIG_USUBW(lhs, rhs, max);
+    if (lhs < rhs) {
+        return max - rhs - lhs + 1;
+    } else {
+        return lhs - rhs;
+    }
 }
 
 static inline int16_t zig_subw_i16(int16_t lhs, int16_t rhs, int16_t min, int16_t max) {
-    ZIG_SSUBW(int16_t, lhs, rhs, min, max);
+    if ((lhs > 0) && (rhs < 0)) {
+        int16_t thresh = lhs - max;
+        if (rhs < thresh) {
+            return min + (thresh - rhs - 1);
+        }
+    } else if ((lhs < 0) && (rhs > 0)) {
+        int16_t thresh = lhs - min;
+        if (rhs > thresh) {
+            return max - (rhs - thresh - 1);
+        }
+    }
+    return lhs - rhs;
 }
 
 static inline uint32_t zig_subw_u32(uint32_t lhs, uint32_t rhs, uint32_t max) {
-    ZIG_USUBW(lhs, rhs, max);
+    if (lhs < rhs) {
+        return max - rhs - lhs + 1;
+    } else {
+        return lhs - rhs;
+    }
 }
 
 static inline int32_t zig_subw_i32(int32_t lhs, int32_t rhs, int32_t min, int32_t max) {
-    ZIG_SSUBW(int32_t, lhs, rhs, min, max);
+    if ((lhs > 0) && (rhs < 0)) {
+        int32_t thresh = lhs - max;
+        if (rhs < thresh) {
+            return min + (thresh - rhs - 1);
+        }
+    } else if ((lhs < 0) && (rhs > 0)) {
+        int32_t thresh = lhs - min;
+        if (rhs > thresh) {
+            return max - (rhs - thresh - 1);
+        }
+    }
+    return lhs - rhs;
 }
 
 static inline uint64_t zig_subw_u64(uint64_t lhs, uint64_t rhs, uint64_t max) {
-    ZIG_USUBW(lhs, rhs, max);
+    if (lhs < rhs) {
+        return max - rhs - lhs + 1;
+    } else {
+        return lhs - rhs;
+    }
 }
 
 static inline int64_t zig_subw_i64(int64_t lhs, int64_t rhs, int64_t min, int64_t max) {
-    ZIG_SSUBW(int64_t, lhs, rhs, min, max);
+    if ((lhs > 0) && (rhs < 0)) {
+        int64_t thresh = lhs - max;
+        if (rhs < thresh) {
+            return min + (thresh - rhs - 1);
+        }
+    } else if ((lhs < 0) && (rhs > 0)) {
+        int64_t thresh = lhs - min;
+        if (rhs > thresh) {
+            return max - (rhs - thresh - 1);
+        }
+    }
+    return lhs - rhs;
 }
 
 static inline intptr_t zig_subw_isize(intptr_t lhs, intptr_t rhs, intptr_t min, intptr_t max) {
test/stage2/cbe.zig
@@ -825,52 +825,53 @@ pub fn addCases(ctx: *TestContext) !void {
     }
 
     {
-        // TODO: move these cases into the programs themselves once stage 2 has array literals
         // TODO: add u64 tests, ran into issues with the literal generated for std.math.maxInt(u64)
-        var case = ctx.exeFromCompiledC("Wrapping operations", .{});
-        const programs = comptime blk: {
-            const cases = .{
-                // Addition
-                .{ u3, "+%", 1, 1, 2 },
-                .{ u3, "+%", 7, 1, 0 },
-                .{ i3, "+%", 1, 1, 2 },
-                .{ i3, "+%", 3, 2, -3 },
-                .{ i3, "+%", -3, -2, 3 },
-                .{ c_int, "+%", 1, 1, 2 },
-                .{ c_int, "+%", std.math.maxInt(c_int), 2, std.math.minInt(c_int) + 1 },
-                .{ c_int, "+%", std.math.minInt(c_int) + 1, -2, std.math.maxInt(c_int) },
-
-                // Subtraction
-                .{ u3, "-%", 2, 1, 1 },
-                .{ u3, "-%", 0, 1, 7 },
-                .{ i3, "-%", 2, 1, 1 },
-                .{ i3, "-%", 3, -2, -3 },
-                .{ i3, "-%", -3, 2, 3 },
-                .{ c_int, "-%", 2, 1, 1 },
-                .{ c_int, "-%", std.math.maxInt(c_int), -2, std.math.minInt(c_int) + 1 },
-                .{ c_int, "-%", std.math.minInt(c_int) + 1, 2, std.math.maxInt(c_int) },
-            };
-
-            var ret: [cases.len][:0]const u8 = undefined;
-            for (cases) |c, i| ret[i] = std.fmt.comptimePrint(
-                \\export fn main() i32 {{
-                \\    var lhs: {0} = {2};
-                \\    var rhs: {0} = {3};
-                \\    var expected: {0} = {4};
-                \\
-                \\    if (expected != lhs {1s} rhs) {{
-                \\        return 1;
-                \\    }} else {{
-                \\        return 0;
-                \\    }}
-                \\}}
-                \\
-            , c);
-
-            break :blk ret;
-        };
-
-        inline for (programs) |prog| case.addCompareOutput(prog, "");
+        var case = ctx.exeFromCompiledC("add/sub wrapping operations", .{});
+        case.addCompareOutput(
+            \\pub export fn main() c_int {
+            \\    // Addition
+            \\    if (!add_u3(1, 1, 2)) return 1;
+            \\    if (!add_u3(7, 1, 0)) return 1;
+            \\    if (!add_i3(1, 1, 2)) return 1;
+            \\    if (!add_i3(3, 2, -3)) return 1;
+            \\    if (!add_i3(-3, -2, 3)) return 1;
+            \\    if (!add_c_int(1, 1, 2)) return 1;
+            \\    // TODO enable these when stage2 supports std.math.maxInt
+            \\    //if (!add_c_int(maxInt(c_int), 2, minInt(c_int) + 1)) return 1;
+            \\    //if (!add_c_int(maxInt(c_int) + 1, -2, maxInt(c_int))) return 1;
+            \\
+            \\    // Subtraction
+            \\    if (!sub_u3(2, 1, 1)) return 1;
+            \\    if (!sub_u3(0, 1, 7)) return 1;
+            \\    if (!sub_i3(2, 1, 1)) return 1;
+            \\    if (!sub_i3(3, -2, -3)) return 1;
+            \\    if (!sub_i3(-3, 2, 3)) return 1;
+            \\    if (!sub_c_int(2, 1, 1)) return 1;
+            \\    // TODO enable these when stage2 supports std.math.maxInt
+            \\    //if (!sub_c_int(maxInt(c_int), -2, minInt(c_int) + 1)) return 1;
+            \\    //if (!sub_c_int(minInt(c_int) + 1, 2, maxInt(c_int))) return 1;
+            \\
+            \\    return 0;
+            \\}
+            \\fn add_u3(lhs: u3, rhs: u3, expected: u3) bool {
+            \\    return expected == lhs +% rhs;
+            \\}
+            \\fn add_i3(lhs: i3, rhs: i3, expected: i3) bool {
+            \\    return expected == lhs +% rhs;
+            \\}
+            \\fn add_c_int(lhs: c_int, rhs: c_int, expected: c_int) bool {
+            \\    return expected == lhs +% rhs;
+            \\}
+            \\fn sub_u3(lhs: u3, rhs: u3, expected: u3) bool {
+            \\    return expected == lhs -% rhs;
+            \\}
+            \\fn sub_i3(lhs: i3, rhs: i3, expected: i3) bool {
+            \\    return expected == lhs -% rhs;
+            \\}
+            \\fn sub_c_int(lhs: c_int, rhs: c_int, expected: c_int) bool {
+            \\    return expected == lhs -% rhs;
+            \\}
+        , "");
     }
 
     ctx.h("simple header", linux_x64,