Conditional compilation

The goal of this feature, is to tell V to not compile a function, and all its calls, in the final executable, if a provided custom flag is not passed.

V will still type check the function and all its calls, even if they will not be present in the final executable, due to the passed -d flags.

In order to see it in action, run the following example with v run example.v once, and then a second time with v -d trace_logs example.v:

@[if trace_logs ?] fn elog(s string) { eprintln(s) } fn main() { elog('some expression: ${2 + 2}') // such calls will not be done *at all*, if `-d trace_logs` is not passed println('hi') elog('finish') }

Conditional compilation, based on custom flags, can also be used to produce slightly different executables, which share the majority of the same code, but where some of the logic, is needed only some of the time, for example a network server/client program can be written like so:

fn act_as_client() { ... } fn act_as_server() { ... } fn main() { $if as_client ? { act_as_client() } $if as_server ? { act_as_server() } }

To generate a client.exe executable do: v -d as_client -o client.exe . To generate a server.exe executable do: v -d as_server -o server.exe .

Compile time pseudo variables

V also gives your code access to a set of pseudo string variables, that are substituted at compile time:

  • @FN => replaced with the name of the current V function.
  • @METHOD => replaced with ReceiverType.MethodName.
  • @MOD => replaced with the name of the current V module.
  • @STRUCT => replaced with the name of the current V struct.
  • @FILE => replaced with the absolute path of the V source file.
  • @LINE => replaced with the V line number where it appears (as a string).
  • @FILE_LINE => like @FILE:@LINE, but the file part is a relative path.
  • @LOCATION => file, line and name of the current type + method; suitable for logging.
  • @COLUMN => replaced with the column where it appears (as a string).
  • @VEXE => replaced with the path to the V compiler.
  • @VEXEROOT => will be substituted with the folder, where the V executable is (as a string).
  • @VHASH => replaced with the shortened commit hash of the V compiler (as a string).
  • @VCURRENTHASH => Similar to @VHASH, but changes when the compiler is recompiled on a different commit (after local modifications, or after using git bisect etc).
  • @VMOD_FILE => replaced with the contents of the nearest v.mod file (as a string).
  • @VMODHASH => is replaced by the shortened commit hash, derived from the .git directory next to the nearest v.mod file (as a string).
  • @VMODROOT => will be substituted with the folder, where the nearest v.mod file is (as a string).
  • @BUILD_DATE => replaced with the build date, for example '2024-09-13' .
  • @BUILD_TIME => replaced with the build time, for example '12:32:07' .
  • @BUILD_TIMESTAMP => replaced with the build timestamp, for example '1726219885' . Note: @BUILD_DATE, @BUILD_TIME, @BUILD_TIMESTAMP represent times in the UTC timezone. By default, they are based on the current time of the compilation/build. They can be overriden by setting the environment variable SOURCE_DATE_EPOCH. That is also useful while making releases, since you can use the equivalent of this in your build system/script: export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) ; , and then use @BUILD_DATE etc., inside your program, when you for example print your version information to users. See also https://reproducible-builds.org/docs/source-date-epoch/ .

The compile time pseudo variables allow you to do the following example, which is useful while debugging/logging/tracing your code:

eprintln(@LOCATION)

Another example, is if you want to embed the version/name from v.mod inside your executable:

import v.vmod vm := vmod.decode( @VMOD_FILE ) or { panic(err) } eprintln('${vm.name} ${vm.version}\n ${vm.description}')

A program that prints its own source code (a quine):

print($embed_file(@FILE).to_string())

A program that prints the time when it was built:

import time println('This program, was compiled at ${time.unix(@BUILD_TIMESTAMP.i64()).format_ss_milli()} .')

[!NOTE] you can have arbitrary source code in the file, without problems, since the full file will be embedded into the executable, produced by compiling it. Also note that printing is done with print and not println, to not add another new line, missing in the source code.

Compile time reflection

$ is used as a prefix for compile time (also referred to as 'comptime') operations.

Having built-in JSON support is nice, but V also allows you to create efficient serializers for any data format. V has compile time if and for constructs:

.fields

You can iterate over struct fields using .fields, it also works with generic types (e.g. T.fields) and generic arguments (e.g. param.fields where fn gen[T](param T) {).

struct User { name string age int } fn main() { $for field in User.fields { $if field.typ is string { println('${field.name} is of type string') } } } // Output: // name is of type string

.values

You can read Enum values and their attributes.

enum Color { red @[RED] // first attribute blue @[BLUE] // second attribute } fn main() { $for e in Color.values { println(e.name) println(e.attrs) } } // Output: // red // ['RED'] // blue // ['BLUE']

.attributes

You can read Struct attributes.

@[COLOR] struct Foo { a int } fn main() { $for e in Foo.attributes { println(e) } } // Output: // StructAttribute{ // name: 'COLOR' // has_arg: false // arg: '' // kind: plain // }

.variants

You can read variant types from Sum type.

type MySum = int | string fn main() { $for v in MySum.variants { $if v.typ is int { println('has int type') } $else $if v.typ is string { println('has string type') } } } // Output: // has int type // has string type

.methods

You can retrieve information about struct methods.

struct Foo { } fn (f Foo) test() int { return 123 } fn (f Foo) test2() string { return 'foo' } fn main() { foo := Foo{} $for m in Foo.methods { $if m.return_type is int { print('${m.name} returns int: ') println(foo.$method()) } $else $if m.return_type is string { print('${m.name} returns string: ') println(foo.$method()) } } } // Output: // test returns int: 123 // test2 returns string: foo

.params

You can retrieve information about struct method params.

struct Test { } fn (t Test) foo(arg1 int, arg2 string) { } fn main() { $for m in Test.methods { $for param in m.params { println('${typeof(param.typ).name}: ${param.name}') } } } // Output: // int: arg1 // string: arg2

See examples/compiletime/reflection.v for a more complete example.

Compile time code

$if condition

fn main() { // Support for multiple conditions in one branch $if ios || android { println('Running on a mobile device!') } $if linux && x64 { println('64-bit Linux.') } // Usage as expression os := $if windows { 'Windows' } $else { 'UNIX' } println('Using ${os}') // $else-$if branches $if tinyc { println('tinyc') } $else $if clang { println('clang') } $else $if gcc { println('gcc') } $else { println('different compiler') } $if test { println('testing') } // v -cg ... $if debug { println('debugging') } // v -prod ... $if prod { println('production build') } // v -d option ... $if option ? { println('custom option') } }

If you want an if to be evaluated at compile time it must be prefixed with a $ sign. Right now it can be used to detect an OS, compiler, platform or compilation options. $if debug is a special option like $if windows or $if x32, it's enabled if the program is compiled with v -g or v -cg. If you're using a custom ifdef, then you do need $if option ? {} and compile withv -d option. Full list of builtin options:

OS Compilers Platforms Other
windows, linux, macos gcc, tinyc amd64, arm64, aarch64 debug, prod, test
darwin, ios, bsd clang, mingw i386, arm32 js, glibc, prealloc
freebsd, openbsd, netbsd msvc rv64, rv32 no_bounds_checking, freestanding
android, mach, dragonfly cplusplus x64, x32 no_segfault_handler, no_backtrace
gnu, hpux, haiku, qnx little_endian, big_endian no_main, fast_math, apk, threads
solaris, termux js_node, js_browser, js_freestanding
serenity, vinix, plan9 interpreter, es5, profile, wasm32
wasm32_emscripten, wasm32_wasi
native, autofree

$embed_file

import os fn main() { embedded_file := $embed_file('v.png') os.write_file('exported.png', embedded_file.to_string())! }

V can embed arbitrary files into the executable with the $embed_file(<path>) compile time call. Paths can be absolute or relative to the source file.

Note that by default, using $embed_file(file), will always embed the whole content of the file, but you can modify that behaviour by passing: -d embed_only_metadata when compiling your program. In that case, the file will not be embedded. Instead, it will be loaded the first time your program calls embedded_file.data() at runtime, making it easier to change in external editor programs, without needing to recompile your program.

Embedding a file inside your executable, will increase its size, but it will make it more self contained and thus easier to distribute. When that happens (the default), embedded_file.data() will cause no IO, and it will always return the same data.

$embed_file supports compression of the embedded file when compiling with -prod. Currently only one compression type is supported: zlib.

import os fn main() { embedded_file := $embed_file('x.css', .zlib) // compressed using zlib os.write_file('exported.css', embedded_file.to_string())! }

Note: compressing binary assets like png or zip files, usually will not gain you much, and in some cases may even take more space in the final executable, since they are already compressed.

$embed_file returns EmbedFileData which could be used to obtain the file contents as string or []u8.

$tmpl for embedding and parsing V template files

V has a simple template language for text and html templates, and they can easily be embedded via $tmpl('path/to/template.txt'):

fn build() string { name := 'Peter' age := 25 numbers := [1, 2, 3] return $tmpl('1.txt') } fn main() { println(build()) }

1.txt:

name: @name

age: @age

numbers: @numbers

@for number in numbers
  @number
@end

output:

name: Peter

age: 25

numbers: [1, 2, 3]

1
2
3

See more details

$env

module main fn main() { compile_time_env := $env('ENV_VAR') println(compile_time_env) }

V can bring in values at compile time from environment variables. $env('ENV_VAR') can also be used in top-level #flag and #include statements: #flag linux -I $env('JAVA_HOME')/include.

$d

V can bring in values at compile time from -d ident=value flag defines, passed on the command line to the compiler. You can also pass -d ident, which will have the same meaning as passing -d ident=true.

To get the value in your code, use: $d('ident', default), where default can be false for booleans, 0 or 123 for i64 numbers, 0.0 or 113.0 for f64 numbers, 'a string' for strings.

When a flag is not provided via the command line, $d() will return the default value provided as the second argument.

module main const my_i64 = $d('my_i64', 1024) fn main() { compile_time_value := $d('my_string', 'V') println(compile_time_value) println(my_i64) }

Running the above with v run . will output:

V
1024

Running the above with v -d my_i64=4096 -d my_string="V rocks" run . will output:

V rocks
4096

Here is an example of how to use the default values, which have to be pure literals:

fn main() { val_str := $d('id_str', 'value') // can be changed by providing `-d id_str="my id"` val_f64 := $d('id_f64', 42.0) // can be changed by providing `-d id_f64=84.0` val_i64 := $d('id_i64', 56) // can be changed by providing `-d id_i64=123` val_bool := $d('id_bool', false) // can be changed by providing `-d id_bool=true` val_char := $d('id_char', `f`) // can be changed by providing `-d id_char=v` println(val_str) println(val_f64) println(val_i64) println(val_bool) println(rune(val_char)) }

$d('ident','value') can also be used in top-level statements like #flag and #include: #flag linux -I $d('my_include','/usr')/include. The default value for $d when used in these statements should be literal strings.

$d('ident', false) can also be used inside $if $d('ident', false) { statements, granting you the ability to selectively turn on/off certain sections of code, at compile time, without modifying your source code, or keeping different versions of it.

$compile_error and $compile_warn

These two comptime functions are very useful for displaying custom errors/warnings during compile time.

Both receive as their only argument a string literal that contains the message to display:

// x.v module main $if linux { $compile_error('Linux is not supported') } fn main() { } $ v run x.v x.v:4:5: error: Linux is not supported 2 | 3 | $if linux { 4 | $compile_error('Linux is not supported') | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | } 6 |

Compile time types

Compile time types group multiple types into a general higher-level type. This is useful in functions with generic parameters, where the input type must have a specific property, for example the .len attribute in arrays.

V supports the following compile time types:

Environment specific files

If a file has an environment-specific suffix, it will only be compiled for that environment.

  • .js.v => will be used only by the JS backend. These files can contain JS. code.
  • .c.v => will be used only by the C backend. These files can contain C. code.
  • .native.v => will be used only by V's native backend.
  • _nix.c.v => will be used only on Unix systems (non Windows).
  • _${os}.c.v => will be used only on the specific os system. For example, _windows.c.v will be used only when compiling on Windows, or with -os windows.
  • _default.c.v => will be used only if there is NOT a more specific platform file. For example, if you have both file_linux.c.v and file_default.c.v, and you are compiling for linux, then only file_linux.c.v will be used, and file_default.c.v will be ignored.

Here is a more complete example:

main.v:

module main fn main() { println(message) }

main_default.c.v:

module main const message = 'Hello world'

main_linux.c.v:

module main const message = 'Hello linux'

main_windows.c.v:

module main const message = 'Hello windows'

With the example above:

  • when you compile for Windows, you will get Hello windows

  • when you compile for Linux, you will get Hello linux

  • when you compile for any other platform, you will get the non specific Hello world message.

  • _d_customflag.v => will be used only if you pass -d customflag to V. That corresponds to $if customflag ? {}, but for a whole file, not just a single block. customflag should be a snake_case identifier, it can not contain arbitrary characters (only lower case latin letters + numbers + _).

    Note

    A combinatorial _d_customflag_linux.c.v postfix will not work. If you do need a custom flag file, that has platform dependent code, use the postfix _d_customflag.v, and then use platform dependent compile time conditional blocks inside it, i.e. $if linux {} etc.

  • _notd_customflag.v => similar to _d_customflag.v, but will be used only if you do NOT pass -d customflag to V.

See also Cross Compilation.