← Code Compare

Input & Output

Formatted output is where each language's type system meets the terminal. The task is identical in all seven - print one line, n=7 pi=3.14, mixing an integer and a float with a fixed two-decimal precision. Watch the spectrum of safety: C's printf is variadic and type-unchecked (a wrong % specifier is undefined behavior), while Zig, Hare, and Odin route everything through comptime/type-aware formatters that can't mismatch. Note too that none of this needs the heap - the format string is static read-only data and the values live in registers, so these print calls allocate nothing and have nothing to free. (std.debug.print writes to stderr; the rest go to stdout.)

Show: CC++HolyCZigHareOdinForth
C
#include <stdio.h>

int main(void) {
    int n = 7;
    double pi = 3.14159;

    /* printf is variadic: the compiler does NOT verify that the
       %d/%.2f specifiers match the argument types -- a mismatch is
       undefined behavior. The format string is static read-only
       data; nothing here touches the heap. */
    printf("n=%d pi=%.2f\n", n, pi);   /* prints: n=7 pi=3.14 */
    return 0;
}

C's printf is the archetypal variadic, type-unchecked formatter: %d consumes an int, %.2f a (promoted) double rounded to two places, but a wrong specifier is undefined behavior the compiler may not catch. The format string lives in static read-only memory, so there is no malloc and nothing to free.

C++
#include <iostream>
#include <format>   // C++20

int main() {
    int n = 7;
    double pi = 3.14159;

    // std::format (C++20) is type-safe and checked at compile time:
    // {} infers the type, {:.2f} fixes two decimals. It returns a
    // std::string (heap-backed, freed by RAII), which cout streams.
    std::cout << std::format("n={} pi={:.2f}\n", n, pi); // n=7 pi=3.14
}

Modern C++20 std::format replaces printf with a compile-time-checked, type-safe API: {} deduces the type and {:.2f} sets the precision, with no specifier-vs-argument mismatch possible. It returns a std::string whose heap buffer is released by RAII; std::cout << ... streams the result to stdout.

HolyC
// HolyC (TempleOS): Print is the printf-style formatter.
// Top-level code runs directly -- no main() needed.
I64 n = 7;
F64 pi = 3.14159;

// %d formats the I64, %.2f the F64 to two decimals. The format
// string is compiled into the binary, not the per-task heap, so
// there is no MAlloc/Free for this.
Print("n=%d pi=%.2f\n", n, pi);   // prints: n=7 pi=3.14

HolyC's Print is its printf-style formatter, and top-level statements run with no main. %d formats the I64 and %.2f the F64 to two decimals; the format literal lives in the compiled binary rather than the per-task heap, so there is no MAlloc/Free here.

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

pub fn main() void {
    const n: i32 = 7;
    const pi: f64 = 3.14159;

    // std.debug.print parses the format string at comptime and checks
    // each {} against the args tuple's types -- a mismatch is a
    // COMPILE error, not UB. {d} is decimal, {d:.2} is 2 decimals.
    // It writes to stderr and performs NO heap allocation.
    std.debug.print("n={d} pi={d:.2}\n", .{ n, pi }); // n=7 pi=3.14
}

Zig's std.debug.print parses its format string at comptime and type-checks every {} against the args tuple, so {d} (decimal) and {d:.2} (two decimals) can never silently mismatch - an error is caught at compile time. It writes to stderr with no hidden allocation; no allocator is involved.

Hare
use fmt;

export fn main() void = {
	const n: int = 7;
	const pi: f64 = 3.14159;

	// fmt::printfln is printf-style: {} formats n by its int type and
	// {:.2} prints the f64 to two decimals, then it appends a newline.
	// The trailing ! propagates any I/O error rather than ignoring it.
	fmt::printfln("n={} pi={:.2}", n, pi)!; // prints: n=7 pi=3.14
};

Hare's fmt::printfln is type-aware: {} formats n by its int type and {:.2} prints the f64 to two decimals, appending a newline. No heap is used - the values and the static format literal need no alloc/free - and the trailing ! propagates any write error instead of discarding it.

Odin
package main

import "core:fmt"

main :: proc() {
	n := 7
	pi := 3.14159

	// fmt.printf is type-checked at runtime against each verb: %d for
	// the int, %.2f for the float (two decimals). It writes to stdout
	// and uses no heap -- the implicit context.allocator is untouched.
	fmt.printf("n=%d pi=%.2f\n", n, pi) // prints: n=7 pi=3.14
}

Odin's fmt.printf validates each verb against its argument's type at runtime (%d for the int, %.2f for the float to two decimals), unlike C's unchecked variadics. It writes to stdout and touches no heap - the implicit context.allocator is never used, so there is nothing to delete.

Forth
\ Forth has no printf: you compose output from primitive words.
\ ." prints an inline literal, . prints a signed integer (with a
\ trailing space), and SPACE/CR emit a space and newline. There is
\ no native float-with-precision verb in core Forth, so we fake the
\ two decimals with integer math: pi*100 = 314, then split it.

: .PI2 ( scaled -- )        \ print scaled/100 as d.dd, e.g. 314 -> 3.14
  100 /MOD                  ( -- rem quot ) quot=whole, rem=hundredths
  0 .R [CHAR] . EMIT        \ whole part, then a literal '.'
  S>D <# # # #> TYPE ;      \ format rem as exactly two digits

: SHOW ( n scaled -- )      ( n on stack, then scaled pi )
  >R                        \ stash scaled pi on the return stack
  ." n=" .                  \ print "n=" then the integer n ( . adds a space )
  ." pi=" R> .PI2 CR ;      \ print "pi=" then the formatted float

7 314 SHOW                  \ prints: n=7 pi=3.14

Core Forth has no printf and no precision verb for floats, so output is built from primitives: ." for literals, . for integers, EMIT/CR for single chars and the newline. The closest idiom for 3.14 is fixed-point - scale pi to the integer 314 and format it as two digits with the <# # # #> pictured-numeric-output words; everything stays on the data and return stacks, so nothing is allocated or freed.