1. Installation, v run, modules and projects
Sections
Reading settings
No voice for this language in this browser.
More voices
Code and English terms
Off: the page’s voice reads everything, with no pause between two voices.
No voice for this language in this browser.
More voices
More voices: Microsoft Edge has natural online voices, and Windows (Settings › Time & language › Speech), macOS and Android install others.
Code: examples/l01_hello.v, examples/l01_script.v and the project l01-project.
How V builds a program
Section titled “How V builds a program”| C# | Java | V | |
|---|---|---|---|
| The compiler produces | IL in a .dll |
bytecode in .class files |
C code, in a temporary file |
| Then | the CLR compiles the IL at run time (JIT) | the JVM interprets and compiles the bytecode (JIT) | a C compiler turns the C into a native executable |
| To run it you need | the .NET runtime, unless you publish self-contained or Native AOT | a JVM, or a jlink image |
nothing: the executable is native |
V ships with a C compiler, TCC (Tiny C Compiler), in thirdparty/tcc of every release archive. TCC compiles fast and optimizes little: V uses it for everyday builds, and the system’s C compiler (gcc, clang or MSVC) for optimized builds with -prod. v -showcc prints the C compiler command. The course’s CI shows which compiler builds the “Hello World” of v new, and how big the result is:
| OS | v . |
size | v -prod . |
size |
|---|---|---|---|---|
| Windows (my machine) | tcc.exe |
281,600 bytes | MSVC cl.exe |
29,184 bytes |
| Windows (CI runner) | tcc.exe |
280,576 bytes | gcc (MinGW) |
160,784 bytes |
| Linux (CI runner) | tcc.exe |
385,782 bytes | cc (gcc 13.3) |
138,296 bytes |
| macOS (CI runner) | cc (Apple clang) |
96,808 bytes | cc (Apple clang) |
126,408 bytes |
On macOS, V chose cc even without -prod. On Windows, -prod uses whatever C compiler it finds: MSVC on my machine, which has the Visual Studio Build Tools and no gcc, and MinGW gcc on the runner. Lesson 4 shows why that choice matters for memory.
Installing
Section titled “Installing”The documentation recommends building V from source with git clone and make, which follows the master branch. This course pins the 0.5.2 release instead, and downloads its archive: one per OS, with v, the standard library, the tools and TCC. I install it in a folder of its own and don’t put it on the global PATH.
Invoke-WebRequest https://github.com/vlang/v/releases/download/0.5.2/v_windows.zip -OutFile v_windows.zipExpand-Archive v_windows.zip -DestinationPath C:\tools # creates C:\tools\v (it took 53 s on my machine)C:\tools\v\v.exe versionTo call it v from any terminal, add C:\tools\v to your PATH, or run v.exe symlink in an administrator shell, as the README describes (to verify: I haven’t run it).
curl -fLO https://github.com/vlang/v/releases/download/0.5.2/v_linux.zip # v_linux_arm64.zip on ARMunzip -q v_linux.zip -d ~/tools # creates ~/tools/v~/tools/v/v versionexport PATH="$HOME/tools/v:$PATH" # add it to ~/.bashrc to keep itThe archive keeps the executable bits of v and thirdparty/tcc/tcc.exe (yes, with .exe, on Linux too). TCC needs the C headers of the system; the Ubuntu 24.04 runner has them. On a minimal distribution, install build-essential or its equivalent (to verify).
On WSL, install the Linux archive inside the distribution, and keep your projects in the Linux file system (~/src/...), not under /mnt/c.
xcode-select --install # the Apple command-line tools: V compiles with cc on macOScurl -fLO https://github.com/vlang/v/releases/download/0.5.2/v_macos_arm64.zip # v_macos_x86_64.zip on Intelunzip -q v_macos_arm64.zip -d ~/tools # creates ~/tools/v~/tools/v/v versionexport PATH="$HOME/tools/v:$PATH" # add it to ~/.zshrc to keep itcurl doesn’t set the quarantine attribute; an archive downloaded with a browser may need xattr -dr com.apple.quarantine ~/tools/v before macOS runs v (to verify).
Then, on any OS:
> v versionV 0.5.2 7647ce1v doctor prints what V found: its folder, the VMODULES folder where packages go, and every C compiler. On my machine:
|V full version |V 0.5.2 45ae01d23168b6372f734eeb38a77360bbcf184a.7647ce1|V executable |C:\Users\spare\tools\v-0.5.2\v.exe|VMODULES |OK, value: C:\Users\spare\.vmodules|cc version |N/A|gcc version |N/A|clang version |N/A|msvc version |N/A|tcc version |tcc version 0.9.27 (x86_64 Windows)v doctor says msvc version N/A, yet v -prod found MSVC through the Build Tools installation.
Hello, world
Section titled “Hello, world”fn main() { println('Hello, world!')}> v run examples/l01_hello.vHello, world!| C# | Java | V | |
|---|---|---|---|
| Compile and run one file | dotnet run app.cs (.NET 10) |
java Hello.java |
v run hello.v |
| Compile only | dotnet build |
javac Hello.java |
v hello.v, which writes hello.exe or hello |
| Optimized build | dotnet publish -c Release |
— | v -prod hello.v |
v run compiles to a temporary executable, runs it, and deletes it. Compiler flags go before run; everything after the file name goes to your program.
fndeclares a function; the return type, if any, comes after the parameters.printlnis a built-in function: no import, noConsole.orSystem.out..- Strings use single quotes;
v fmtturns double quotes into single quotes. - There are no semicolons, and V code is indented with tabs.
Scripts: no fn main
Section titled “Scripts: no fn main”A single file can skip fn main, like top-level statements in C# or a compact source file in Java 25:
fn greeting(name string) string { return 'Hello, ${name}!'}
println(greeting('C#'))println(greeting('Java'))Hello, C#!Hello, Java!${…} interpolates an expression in a string, like $"…{name}" in C#. The order is the opposite of C#, where top-level statements must come before the type declarations: in a V script, the definitions come first, and a function after the first statement is rejected.
fn greeting(name string) string { return 'Hello, ${name}!'}
println(greeting('C#'))
fn farewell(name string) string { return 'Bye, ${name}!'}e01_script_order.v:5:1: notice: script mode started here 3 | } 4 | 5 | println(greeting('C#')) | ~~~~~~~ 6 | 7 | fn farewell(name string) string {e01_script_order.v:7:4: error: all definitions must occur before code in script mode 5 | println(greeting('C#')) 6 | 7 | fn farewell(name string) string { | ~~~~~~~~ 8 | return 'Bye, ${name}!' 9 | }The compiler prints errors on standard error, with the file, the line and the column, and exits with code 1.
A project: v new and v.mod
Section titled “A project: v new and v.mod”v new hellov new asks three questions (description, version, license) and creates a folder:
hello/├── .editorconfig├── .gitattributes├── .gitignore├── main.v ← module main, fn main└── v.mod ← like .csproj or pom.xml, but only metadataModule { name: 'hello' description: 'Greets people' version: '0.0.0' license: 'MIT' dependencies: []}It also runs git init when Git is installed. v init does the same in an existing folder.
The answers are read from standard input: if it is closed, as in a script without input, v new doesn’t stop, and writes '<EOF>' in the three fields. os.input returns that string at the end of the input (os.v, lines 436-441), and vcreate.v only replaces an empty answer with the default. The CI answers with printf 'Greets people\n\n\n' | v new hello.
v run . compiles every .v file of the folder into one program, named after the folder:
> cd hello> v run .Hello World!There is no build file to maintain: V finds the files and the modules from the folders. v.mod names the project, lists its dependencies, and marks the root from which import finds the modules of the project.
Modules
Section titled “Modules”A module is a folder. Every .v file of the folder starts with module <folder name>, and they all see each other’s declarations; the files at the root of the project are module main.
l01-project/├── v.mod├── main.v└── greet/ └── greet.vmodule greet
// hello is public: other modules can call it as `greet.hello`pub fn hello(name string) string { return 'Hello, ${capitalize(name)}!'}
// capitalize is private to the module, like `internal` in C# or package-private in Javafn capitalize(s string) string { return s.capitalize()}module main
import osimport greet
fn main() { names := if os.args.len > 1 { os.args[1..] } else { ['world'] } for name in names { println(greet.hello(name)) }}> v run .Hello, World!> v run . ada graceHello, Ada!Hello, Grace!os.args holds the program’s path, then its arguments: os.args[1..] skips the path, like string[] args in C# and Java.
| C# | Java | V | |
|---|---|---|---|
| Unit | namespace, free of the folders |
package, one folder |
module, one folder |
| Import | using Greet; |
import greet.*; |
import greet |
| Call | Greeter.Hello(…) |
Greeter.hello(…) |
greet.hello(…): always with the module name |
| Public | public |
public |
pub |
| Default | internal (the assembly) for a type, private for a member |
package-private | private to the module |
Without pub, another module can’t call the function:
module main
import greet
fn main() { println(greet.capitalize('java'))}main.v:6:16: error: function `greet.capitalize` is private 4 | 5 | fn main() { 6 | println(greet.capitalize('java')) | ~~~~~~~~~~~~~~~~~~ 7 | }Two more rules from the modules documentation: imports can’t be circular, and a module in a/b/ is imported as a.b but called with its last name, b.hello().
Everyday commands
Section titled “Everyday commands”| Task | V | dotnet | Java |
|---|---|---|---|
| New app | v new app |
dotnet new console |
mvn archetype:generate |
| Run a folder | v run . |
dotnet run |
mvn exec:java |
| Build | v . |
dotnet build |
mvn compile |
| Optimized build | v -prod . |
dotnet publish -c Release |
mvn package |
| Test | v test . |
dotnet test |
mvn test |
| Format | v fmt -w . |
dotnet format |
a plugin such as Spotless |
| Lint | v vet . |
Roslyn analyzers | Error Prone |
| Check the setup | v doctor |
dotnet --info |
java -XshowSettings |
v help <command> documents each command. Tests, v fmt, v vet and v doc are the subject of lesson 8, packages of lesson 12.
Key takeaways
Section titled “Key takeaways”- V translates your program to C, and a C compiler makes the executable: TCC, shipped with V, for everyday builds;
gcc,clangor MSVC with-prod. v run file.vorv run .compiles and runs;v file.vorv .only compiles.- A single file can skip
fn main, if every definition comes before the first statement. - A module is a folder; a function is private to its module unless it is
pub, and it is always called with the module name. v.modholds the project’s metadata; there is no list of files to maintain.
Exercises
Section titled “Exercises”- Move
fn greetingofl01_script.vbelow the twoprintlncalls. What does the compiler say, and why doesn’t the same order work in a script?
Solution
error: all definitions must occur before code in script modeIn a file without fn main, V makes an implicit main from the first statement to the end of the file (script mode): a function declared after that point would be inside main. In a file with fn main, the order doesn’t matter: V hoists every declaration.
- Add a module
statsto a copy ofl01-project, with a public functionword_count(s string) int, and print the number of words of the program’s arguments.
Solution
module stats
// word_count counts the words separated by spacespub fn word_count(s string) int { return s.fields().len}module main
import osimport greetimport stats
fn main() { text := os.args[1..].join(' ') println(greet.hello('reader')) println('${stats.word_count(text)} words')}> v run . V for C# and Java developersHello, Reader!6 wordsNothing else changes: no project file to edit, import stats finds the folder next to v.mod. fields splits on whitespace.
- Build
l01-projectwithv -showcc .and withv -showcc -prod .. Which C compiler does each build use on your machine, and how large are the two executables?
Solution
The first line starting with > C compiler cmd: names the compiler. On Windows and Linux, the first build uses thirdparty/tcc/tcc.exe; the -prod build uses gcc, cc or MSVC, whichever V finds. On macOS, both use cc. Compare your sizes with the table at the top of this lesson: the size depends as much on the C compiler as on -prod, since on the macOS runner the -prod build is the larger one.