ASMATI・TATSUMIEXPERIMENT NOTE 01
Resolving Nim int and float to Concrete C Types
Asmati Tatsumi — “What High-Level Languages Do at the Low Level,” Part 1
This series explains how the Nim compiler translates Nim programs into C by reading the generated C code. Part 1 compiles three functions (proc declarations) and explains the concrete C types for int and float, along with the generated addition and division code.
1. Conditions
The build mode is debug (opt: none). The remaining conditions are listed below.
| Item | Condition |
|---|---|
| OS | macOS 26.5.2 |
| CPU | Apple Silicon |
| Nim | 2.2.10 |
| C compiler | Apple clang 21.0.0 |
| Memory manager | ORC |
| Build mode | debug (opt: none) |
2. Three functions with different arguments and return types
The experiment uses three small procs with different arguments, return types, and calculations so that their generated C functions can be compared.
proc noArgs(): int =
7
proc addOne(x: int): int =
x + 1
proc half(x: float): float =
x / 2.0
when isMainModule:
echo "noArgs=", noArgs()
echo "addOne(41)=", addOne(41)
echo "half(3.5)=", half(3.5)The function noArgs takes no arguments and returns an int. The function addOne accepts an int, adds 1, and returns an int. The function half accepts a float, divides it by 2.0, and returns a float. Because the arguments and return values use only integers or floating-point numbers, the three generated C functions can be compared by type and calculation.
when isMainModule specifies that the code in its block is included only when this file is compiled as the main program. isMainModule is a boolean constant defined in the Nim 2.2.10 system module, and it is true only when accessed in the main module. when is a compile-time conditional, and this block is resolved at compile time.
The block calls the three procs with concrete values and writes the results to standard output. The same source file therefore produces the generated C and the binary output used in the experiment.
The Modules section of Nim Tutorial Part I uses when isMainModule in an example that includes test code only when the module is compiled as the main file.
The following command compiles src/scalar_proc.nim. The Nim compiler writes the generated C code to observed/nimcache.
nim c --nimcache:observed/nimcache \
-o:observed/bin/scalar_proc \
src/scalar_proc.nimThe generated C declarations and function bodies are shown below.
#define NIM_INTBITS 64
N_LIB_PRIVATE N_NIMCALL(NI, noArgs__scalar95proc_u1)(void);
N_LIB_PRIVATE N_NIMCALL(NI, addOne__scalar95proc_u3)(NI x_p0);
N_LIB_PRIVATE N_NIMCALL(NF, half__scalar95proc_u6)(NF x_p0);
N_LIB_PRIVATE N_NIMCALL(NI, noArgs__scalar95proc_u1)(void) {
NI result;
result = ((NI)7);
return result;
}
N_LIB_PRIVATE N_NIMCALL(NI, addOne__scalar95proc_u3)(NI x_p0) {
NI result;
NI tmp;
result = (NI)0;
if (nimAddInt(x_p0, ((NI)1), &tmp)) {
raiseOverflow();
goto BeforeRet_;
}
result = (NI)(tmp);
BeforeRet_:
return result;
}
N_LIB_PRIVATE N_NIMCALL(NF, half__scalar95proc_u6)(NF x_p0) {
NF result;
result = ((NF)(x_p0) / (NF)(2.0));
return result;
}This excerpt includes the function signatures and the code that computes and returns each value. It omits the debug bookkeeping calls nimfr_, nimlf_, and popFrame().
The opening #define NIM_INTBITS 64 tells the generated C to use a 64-bit width for Nim int. The following paragraphs explain the declaration of the function noArgs in detail.
N_LIB_PRIVATE N_NIMCALL(NI, noArgs__scalar95proc_u1)(void);This line declares the C function noArgs__scalar95proc_u1, which was generated from the Nim function noArgs. N_NIMCALL(NI, noArgs__scalar95proc_u1)(void) uses a macro form that produces a function declaration. The declaration shows the return type name NI, and (void) states that the function takes no arguments. The line ends with a semicolon and contains only the function declaration.
This declaration alone does not determine the C code represented by N_LIB_PRIVATE, N_NIMCALL, and NI. Their meanings come from the nimbase.h definitions shown later.
3. Concrete NI and NF types from nimbase.h
The three generated declarations use the Nim C backend names NI and NF instead of int and float. Their concrete C types are selected by NIM_INTBITS in the generated C and the definitions of NI, NF, N_LIB_PRIVATE, and N_NIMCALL in Nim 2.2.10 lib/nimbase.h. The selected definitions are shown below.
#define N_LIB_PRIVATE __attribute__((visibility("hidden")))
#define N_NIMCALL(rettype, name) rettype name /* no modifier */
/* ... */
typedef int64_t NI64;
/* ... */
#ifdef NIM_INTBITS
# if NIM_INTBITS == 64
typedef NI64 NI;
typedef NU64 NU;
# elif NIM_INTBITS == 32
typedef NI32 NI;
typedef NU32 NU;
# elif NIM_INTBITS == 16
typedef NI16 NI;
typedef NU16 NU;
# elif NIM_INTBITS == 8
typedef NI8 NI;
typedef NU8 NU;
# else
# error "invalid bit width for int"
# endif
#endif
typedef double NF;Apple clang selects the C99 stdint.h branch, which defines NI64 as int64_t. The generated NIM_INTBITS 64 selects NI64, so the concrete type of NI is int64_t. NF is defined as double. On macOS, N_LIB_PRIVATE gives the function symbol hidden visibility, and N_NIMCALL(rettype, name) expands to rettype name.
Applying these definitions to the declaration of noArgs produces the following C declaration.
__attribute__((visibility("hidden")))
int64_t noArgs__scalar95proc_u1(void);N_LIB_PRIVATE becomes __attribute__((visibility("hidden"))), and N_NIMCALL(NI, noArgs__scalar95proc_u1) becomes NI noArgs__scalar95proc_u1. NI then resolves to int64_t.
Applying the same definitions to the other two functions gives the function addOne an int64_t argument and return type, while the function half has a double argument and return type.
4. Function bodies with concrete types
NI is int64_t and NF is double, and the following function bodies use those concrete types. The definition of the function noArgs is shown below.
N_LIB_PRIVATE N_NIMCALL(NI, noArgs__scalar95proc_u1)(void) {
NI result;
result = ((NI)7);
return result;
}The opening signature is the same as the declaration, and the braces contain the function body. NI result; declares the local variable result, which stores the return value. This NI resolves to int64_t. result = ((NI)7); converts the integer literal 7 to int64_t before assigning it to result. return result; returns that value to the caller. In this generated C, the Nim source proc noArgs(): int = 7 becomes a C function that stores 7 in an int64_t local variable and returns it.
The next example shows the body of the function addOne, including integer addition and the overflow branch.
N_LIB_PRIVATE N_NIMCALL(NI, addOne__scalar95proc_u3)(NI x_p0) {
NI result;
NI tmp;
result = (NI)0;
if (nimAddInt(x_p0, ((NI)1), &tmp)) {
raiseOverflow();
goto BeforeRet_;
}
result = (NI)(tmp);
BeforeRet_:
return result;
}The C function generated from addOne accepts the argument x_p0 with the type name NI, which resolves to int64_t. The nimAddInt macro adds x_p0 and 1 and writes the result to the local variable tmp. When it detects overflow, the function calls raiseOverflow(). After a successful addition, it assigns tmp to result and returns the value.
The function half shows the C expression generated for floating-point division.
N_LIB_PRIVATE N_NIMCALL(NF, half__scalar95proc_u6)(NF x_p0) {
NF result;
result = ((NF)(x_p0) / (NF)(2.0));
return result;
}The C function generated from half accepts the argument x_p0 with the type name NF, which resolves to double. It converts x_p0 and 2.0 to NF, evaluates them with the C division operator, assigns the value to result, and returns it.
5. Overflow checks in int addition
The nimAddInt macro called by the body of addOne is defined in Nim 2.2.10 lib/nimbase.h. The definition selected under the recorded conditions is shown below.
#if (!defined(_MSC_VER) || defined(__clang__)) && \
!defined(NIM_EmulateOverflowChecks)
/* ... */
#if NIM_INTBITS == 32
/* ... */
#else
#define nimAddInt(a, b, res) \
__builtin_saddll_overflow(a, b, (long long int*)res)
#endif
#endifThe function raiseOverflow is marked as a compilerproc in Nim 2.2.10 lib/system/integerops.nim. The pragma identifies a function that compiler-generated code may call. raiseOverflow calls sysFatal with OverflowDefect when overflow occurs.
proc raiseOverflow {.compilerproc, noinline.} =
sysFatal(OverflowDefect, "over- or underflow")The __builtin_saddll_overflow builtin selected by the nimAddInt macro writes the addition result through its third argument and returns true when overflow occurs. The C function generated from addOne calls raiseOverflow() when that return value is true.
In this debug build, the Nim runtime functions nimAddInt and raiseOverflow() check int addition for overflow. The generated code for the float division in half calls no runtime function and executes the C division operator.
6. Program output
The generated binary produced these values.
noArgs=7
addOne(41)=42
half(3.5)=1.75The program output shows the values returned by the three functions. The generated C presented in the preceding sections establishes the function signatures, integer width, and overflow checks.
The video compiles the Nim source and runs the generated binary directly. Readers can repeat the two visible commands in the same order to reproduce the three return values.
7. C types for Nim int and float
On the Mac (Apple Silicon) used here, Nim int became C int64_t, and Nim float became double. Macros select the concrete width of int for the target environment, and a debug build automatically inserts overflow checks into integer addition.
The source, generated C excerpt, program output, and verification script are available in asmati-lab experiment 004.