← Code Compare

Variables & Types

Before any heap appears, you need values: an integer, a float, a boolean, and a fixed-width integer like u8 or i64. This topic declares all four and prints a one-line summary in each language, so you can read the type spellings side by side. The thing to watch is signedness and width: systems code lives and dies on whether an integer is 8 or 64 bits and whether it can go negative, and these languages differ on which defaults are explicit (Zig, Hare, Odin name the width up front) versus implementation-defined (C/C++ int is at least 16 bits; char may even be signed or unsigned).

Show: CC++HolyCZigHareOdinForth
C
#include <stdio.h>
#include <stdbool.h>   /* C99: bool, true, false */
#include <stdint.h>    /* fixed-width ints: uint8_t, int64_t, ... */
#include <inttypes.h>  /* PRIu8 / PRId64 format macros */

int main(void) {
    int     count = 7;        /* signed, >= 16 bits, width is impl-defined */
    double  ratio = 3.14;     /* 64-bit IEEE-754 float */
    bool    ready = true;     /* really an int: 0 or 1 */
    uint8_t flags = 0xFF;     /* EXACTLY 8 bits, unsigned -> 0..255 */
    int64_t big   = 9000000000;/* EXACTLY 64 bits, signed */

    printf("count=%d ratio=%.2f ready=%d flags=%" PRIu8 " big=%" PRId64 "\n",
           count, ratio, ready, flags, big);
    return 0;
}

Plain int/double have implementation-defined width, so for portable sizes C99 gives <stdint.h> exact-width types like uint8_t/int64_t and the matching PRIu8/PRId64 format macros in <inttypes.h>. bool (from <stdbool.h>) is just an integer that holds 0 or 1.

C++
#include <iostream>
#include <cstdint>   // std::uint8_t, std::int64_t
#include <cstdio>    // (not needed: iostream prints typed values directly)

int main() {
    int           count = 7;        // signed, width is implementation-defined
    double        ratio = 3.14;     // 64-bit IEEE-754
    bool          ready = true;     // a real boolean type (prints 0/1)
    std::uint8_t  flags = 0xFF;     // exactly 8 bits, unsigned (0..255)
    std::int64_t  big   = 9'000'000'000;  // exactly 64 bits; ' is a digit separator

    std::cout << "count=" << count
              << " ratio=" << ratio
              << " ready=" << ready
              // uint8_t is an alias for unsigned char, so cast or it prints as a glyph:
              << " flags=" << static_cast<int>(flags)
              << " big=" << big << '\n';
    return 0;
}

std::cout is type-aware, so each value formats itself with no printf specifiers. The one trap: std::uint8_t is an alias for unsigned char, so streaming it prints a character unless you static_cast<int> it. Use digit separators (9'000'000'000) to keep wide literals readable.

HolyC
// Top-level code runs in TempleOS -- no main() needed.
I64 count = 7;       // 64-bit signed; I64 is HolyC's everyday integer
F64 ratio = 3.14;    // 64-bit float (HolyC has no F32)
Bool ready = TRUE;   // Bool is an alias for I8; TRUE == 1, FALSE == 0
U8  flags = 0xFF;    // exactly 8 bits, unsigned -> 0..255

Print("count=%d ratio=%5.2f ready=%d flags=%u\n",
      count, ratio, ready, flags);

HolyC's integer family is sized in the name: U8/U16/U32/U64 (unsigned) and I8/I16/I32/I64 (signed), with I64 the natural register-width default and F64 the only float. Bool is just an I8; there is no separate boolean type. Everything is concrete-width, which suits ring-0 code.

Zig
const std = @import("std");

pub fn main() void {
    // Zig spells the width in the type name; signedness is the i/u prefix.
    const count: i32 = 7;      // 32-bit signed
    const ratio: f64 = 3.14;   // 64-bit float
    const ready: bool = true;  // a distinct boolean type (no int coercion)
    const flags: u8 = 0xFF;    // exactly 8 bits, unsigned -> 0..255
    const big: i64 = 9_000_000_000; // 64-bit signed; _ groups digits

    // No allocations here: all five live on the stack / in registers.
    std.debug.print("count={d} ratio={d:.2} ready={s} flags={d} big={d}\n",
        .{ count, ratio, if (ready) "true" else "false", flags, big });
}

Zig has no implementation-defined integer width: every integer names its bits and signedness (i32, u8, i64), and you can even ask for arbitrary widths like u3. bool is its own type and never silently converts to an integer. Nothing here touches an allocator, true to Zig's no-hidden-allocation rule.

Hare
use fmt;

export fn main() void = {
	// Hare's integer types name their width; i*/u* set signedness.
	const count: i32 = 7;      // 32-bit signed
	const ratio: f64 = 3.14;   // 64-bit float
	const ready: bool = true;  // distinct boolean type
	const flags: u8 = 0xFF;    // exactly 8 bits, unsigned -> 0..255
	const big: i64 = 9000000000; // 64-bit signed

	// No alloc/free: these are values, not heap objects.
	fmt::printfln("count={} ratio={} ready={} flags={} big={}",
		count, ratio, ready, flags, big)!;
};

Hare gives exact-width integers (i8..i64, u8..u64) plus the platform-sized int/uint and size; there is no implicit narrowing, so widths and signedness are always explicit. fmt::printfln infers each argument's type, and the trailing ! propagates any write error rather than ignoring it.

Odin
package main

import "core:fmt"

main :: proc() {
	// Odin integer types carry their width; i/u prefixes set signedness.
	count: i32 = 7        // 32-bit signed
	ratio: f64 = 3.14     // 64-bit float
	ready: bool = true    // distinct boolean type
	flags: u8 = 0xFF      // exactly 8 bits, unsigned -> 0..255
	big: i64 = 9_000_000_000 // 64-bit signed; _ groups digits

	// Pure values: the implicit context.allocator is never touched.
	fmt.printfln("count=%d ratio=%.2f ready=%t flags=%d big=%d",
		count, ratio, ready, flags, big)
}

Odin's numeric types state their size (i8..i64, u8..u64, plus int/uint that match the pointer width) and bool is its own type. With explicit type annotations no inference is needed; %t formats the boolean and %d/%f the numbers, all through the implicit context allocator (which stays unused for plain values).

Forth
\ Forth is typeless: the stack holds machine cells (signed by default).
\ There is no float/bool keyword -- we model each value with a constant.
7 CONSTANT count          ( a signed single-cell integer )
TRUE  CONSTANT ready      ( TRUE = all-bits-set (-1), FALSE = 0 )
255   CONSTANT flags      ( a u8 is just a cell we agree to keep 0..255 )

\ Float, if the optional Floating-point word set is present, lives on a
\ separate float stack: 3.14e0 pushes it; F. prints it.
: SUMMARY ( -- )
  ." count="  count .
  ." ratio="  3.14e0 F.
  ." ready="  ready .
  ." flags="  flags .  CR
;
SUMMARY

Standard Forth has no type system: a cell is just bits, signed when you print with ., and TRUE/FALSE are simply -1/0. Fixed widths (u8) and floats aren't built into the core language -- a u8 is a cell you promise to keep in range, and floats need the optional Floating-point word set with its own stack (3.14e0, F.). This is the closest idiom; the discipline that other languages enforce in the type checker is left to the programmer here.