Connect with us

Tech

V Language Review

Published

on

V is a programming language promising to be “Simple, fast, safe, compiled. For developing maintainable software.”
V has a controversial past but what is the state of V in 2022?

Is V worth checking out?

In this post, we’ll take a look at V as it exists in May 2022.

TLDR Read the summary

Rules of engagement

I’ll be using the current version of V built from git which is 50ab2cfd1ae02d4f4280f38c60b8dbd17f7599f6.
I’ll also stick to the compiler defaults as much as possible to keep to the “happy path” so that I get a typical V experience.

To evaluate the language, I’ll validate the major claims made on their homepage and related sub-pages/documentation.
Features indicated to be incomplete/work in progress/unimplemented will be mentioned as such.
Where possible, I’ll try to include the code used to evaluate a claim.

I’m also scoping this to focus on V the language and not any of the standard libraries or other related projects as V seems to be the most developed.

Evaluation

Simple language for building maintainable programs

I don’t see any objective way to evaluate the claims in this section, so we’ll give them a pass.

Evaluation: N/a (subjective claim)

Safety

  • No null

The V docs indicate V has references and “in general, V’s references are similar to Go pointers and C++ references”.

Let’s see if we can pass a null reference to a function.
The docs contain an example of a tree structure, so let’s adapt that and see if we can create a null reference.
Null usually corresponds to 0 so we’ll try that:

struct Node {
    val   int
    left  &Node
    right &Node
}

fn main() {
    n := Node { 123, 0, 0 }
    println(n.left)
}
$ ./v run test.v
signal 11: segmentation fault
/tmp/v_1000/test.2938032035736349851.tmp.c: 6750: at print_backtrace: Backtrace
/tmp/v_1000/test.2938032035736349851.tmp.c: 6821: by v_segmentation_fault_handler
7f6ff9bfe210 : by ???
/tmp/v_1000/test.2938032035736349851.tmp.c: 11990: by main

Oh, no. 😰
Let’s check the generated C and see what’s up:

...
struct main__Node {
    int val;
    main__Node* left;
    main__Node* right;
};
...
VV_LOCAL_SYMBOL void main__main(void) {
    main__Node n = ((main__Node){.val = 123,.left = 0,.right = 0,});
    println(str_intp(1, _MOV((StrIntpData[])})));
}
...

So yeah, we’re able to create a null pointer (V reference) with no compiler errors or warnings.

Evaluation: 🛑 Doesn’t seem to hold up

  • No undefined values

It’s not obvious to me what this means.
Javascript has a undefined value but that doesn’t really seem relevant.
C allows you to use an uninitialized variable which can result in Undefined Behavior.
I’ll assume that’s what this means.

Typically, uninitialized values come from a memory allocation that hasn’t been written to.
The V docs say we can create an empty array by passing the allocation length in the initialization expression.
Let’s see if we can get the V compiler to allocate memory for us without writing to it:

fn main() {
    a := []&int { len: 1 }
    println(a)
}
$ ./v run test.v
signal 11: segmentation fault
/tmp/v_1000/test.926306498470184027.tmp.c: 6770: at print_backtrace: Backtrace
/tmp/v_1000/test.926306498470184027.tmp.c: 6841: by v_segmentation_fault_handler
7fc58f7fd210 : by ???
/tmp/v_1000/test.926306498470184027.tmp.c: 1973: by Array_main__Node_ptr_str
/tmp/v_1000/test.926306498470184027.tmp.c: 11644: by main__main
/tmp/v_1000/test.926306498470184027.tmp.c: 12010: by main

Sigh.
The generated C code is 12,000 lines long and nothing jumps out to me with a quick glance, so I won’t try to dig in further.

Evaluation: 🛑 Doesn’t seem to hold up

  • No undefined behavior

Wikipedia has a list of Undefined Behaviors (UB) in C and C++.
Let’s see if we can get V to generate C code which contains some of these behaviors!

  • Integer Overflow

In C and C++, signed integer overflow results in UB.
Let’s try to add 1 to the max value of an integer and see what happens:

fn main() {
    x := i32(2147483647) + 1
    println(x)
}
$ ./v -o test.c test.v
$ cc -fsanitize=undefined test.c
test.c: In function ‘main__main’:
test.c: 11605: 30: warning: integer overflow in expression of type ‘int’ results in ‘-2147483648’ [-Woverflow]
11605 |  int x = ((i32)(2147483647)) + 1;
      |                              ^
$ ./a.out
test.c: 7248: 33: runtime error: signed integer overflow: -2147483648 - 2147483600 cannot be represented in type 'int'
-2147483648

That’s no good.

Let’s try some of the other classic cases of UB.

  • Divide by 0
fn main() {
    x := 42
    y := 0
    z := x / y
    println(z)
}
$ ./v -o test.c test.v
$ cc -fsanitize=undefined test.c
$ ./a.out
test.c: 11607: 12: runtime error: division by zero
Floating point exception
  • Temporal memory safety violations (dangling pointer)
struct Something {
    val   int
}

fn main() {
    x := voidptr(123)
    y := &Something(x)
    println(y)
}
$ ./v run test.v
signal 11: segmentation fault
/tmp/v_1000/test.5460990319614516137.tmp.c: 6746: at print_backtrace: Backtrace
/tmp/v_1000/test.5460990319614516137.tmp.c: 6817: by v_segmentation_fault_handler
7f062f88d210 : by ???
/tmp/v_1000/test.5460990319614516137.tmp.c: 11987: by main

There’s more UB we could test for but I’m just going to call it there.

Evaluation: 🛑 Doesn’t seem to hold up

  • No variable shadowing

I don’t see how variable shadowing is a safety issue and shadowing variables after they should no longer be used in a common technique in functional programming languages but let’s test it out:

fn main() {
    x := 1
    x := 2

    if true {
        x := 3
    }
}
$ ./v run test.v
test.v:3:2: error: redefinition of `x`
    1 | fn main() {
    2 |     x := 1
    3 |     x := 2
      |     ^
    4 |
    5 |     if true {

and removing that line:

$ ./v run test.v
test.v:5:3: error: redefinition of `x`
    3 |
    4 |     if true {
    5 |         x := 3
      |         ^
    6 |     }
    7 | }

Works as advertised! ✔️

I see though that V supports closures.
What are the rules for shadowing variables in closures?

fn main() {
    x := 1

    y := fn (x int) {
        println(x)
    }

    y(x)
    y(2)
}

It looks like shadowing is allowed for closure arguments?
There are also explicit closure captures, what happens if I also capture x?

$ cat test.v
fn main() {
    x := 1

    y := fn [x] (x int) {
        println(x)
    }

    y(x)
    y(2)
}

Well, that seems like it should be disallowed.
It makes sense that x can be captured but to then shadow the argument with the same name without error or warning doesn’t seem inline with the rest of V’s behavior.

Even with this surprising behavior, I’m going to give it to V.

Evaluation: ✔️ Seems to work

  • Bounds checking

Bounds checking is important to prevent out-of-bounds access to arrays.

Let’s check a simple example:

fn main() {
    x := [1, 2, 3]
    println(x[4])
}
$ ./v run test.v
V panic: array.get: index out of range (i == 4, a.len == 3)
v hash: 50ab2cf
/tmp/v_1000/test.17032083146791375427.tmp.c: 6344: at _v_panic: Backtrace
/tmp/v_1000/test.17032083146791375427.tmp.c: 5860: by array_get
/tmp/v_1000/test.17032083146791375427.tmp.c: 11597: by main__main
/tmp/v_1000/test.17032083146791375427.tmp.c: 11963: by main

Seeing a.len == 3 reminds me that V allows you to mess with some of the array (vector) properties at creation:

fn main() {
    x := []&int { len: 10, cap: 0 }
    println(x[4])
}
$ ./v run test.v
signal 11: segmentation fault
/tmp/v_1000/test.8104236387804386445.tmp.c: 6724: at print_backtrace: Backtrace
/tmp/v_1000/test.8104236387804386445.tmp.c: 6795: by v_segmentation_fault_handler
7f8aa2432210 : by ???
/tmp/v_1000/test.8104236387804386445.tmp.c: 11964: by main

Allowing the user to control the len property is a really bad idea.

Evaluation: ⚠️ Some basic checking exists but can be trivially bypassed

  • Immutable variables by default

Immutable values by default is a good default in a modern programming language.

fn main() {
    x := 4
    x = 2
}
$ ./v run test.v
test.v:2:2: warning: unused variable: `x`
    1 | fn main() {
    2 |     x := 4
      |     ^
    3 |     x = 2
    4 | }
test.v:3:2: error: `x` is immutable, declare it with `mut` to make it mutable
    1 | fn main() {
    2 |     x := 4
    3 |     x = 2
      |     ^
    4 | }

So far so good!
V seems to be pretty loose with typecasts though so can we trick the compiler into letting us mutate an immutable value?
Yes, we can:

[heap]
struct Foo {
    mut: value int
}

fn y(x &Foo) {
    mut m := x
    m.value = 42
}

fn main() {
    x := Foo { 123 }
    y(x)
    println(x)
}
$ ./v run test.v
Foo{
    value: 42
}

You really shouldn’t be able to convert the immutable reference to a mutable one (mut m := x).
This results in us being able to mutate the “immutable” value x in main with no indication at the call site, there’s not even the clue that we’re passing the value by reference.

Evaluation: 🛑 Variables aren’t immutable in any significant way because you can trivially turn an immutable reference into a mutable one.

  • Pure functions by default

The docs say:

V functions are pure by default, meaning that their return values are a function of their arguments only, and their evaluation has no side effects (besides I/O).

besides I/O

wat

I/O is literally the definition of an impure operation because it allows a function to trivially break referential transparency which V claims to uphold (“their {V functions} return values are a function of their arguments only”).

This is achieved by a lack of global variables and all function arguments being immutable by default, even when references are passed.

As we saw in the previous section, immutability in V is broken so this guarantee doesn’t hold.

To demonstrate how completely ludicrous this definition is, here is a “pure” V module that re-implements global variables using file I/O:

import os

fn read_global(name string) int {
    content := os.read_file(name) or { return 0 }
    return content.int()
}

fn write_global(name string, value int) {
    s := value.str()
    os.write_file(name, s) or { }
}

and we’ll go ahead and use it:

import globals

fn x() {
    globals.write_global("my_global", 7)
}

fn y() {
    my_global := globals.read_global("my_global")
    globals.write_global("my_global", my_global 3)
}

fn z() int {
    my_global := globals.read_global("my_global")
    globals.write_global("my_global", my_global 2)
    return globals.read_global("my_global")
}

fn main() {
    x()
    y()
    my_global := z()
    println(my_global)
}

Evaluation: 🛑 Claim is meaningless as it redefines “pure” to mean “impure”

Immutable structs by default

We’ve already broke immutable values in a previous section using structs so we know this doesn’t hold.

Evaluation: 🛑 Doesn’t seem to hold

  • Option/Result and mandatory error checks

Mandatory error checks seem to work correctly in most contexts.
Is it possible to confuse the compiler and have it ignore the return value?

fn ignore(x any) {
}

fn create_error() ?int {
    return error("an error")
}

fn main() {
    ignore(create_error())
}
$ ./v run test.v
test.v:9:9: error: create_error() returns an option, so it should have either an `or {}` block, or `?` at the end
    7 |
    8 | fn main() {
    9 |     ignore(create_error())
      |            ~~~~~~~~~~~~~~
   10 | }

Looks good!
I tried a few different variations on this idea but wasn’t able to confuse the compiler.

Evaluation: ✔️ Works as advertised

  • Sum types

Sum types are a very nice feature!
Let’s see how they work:

type MyType = f64 | i64

fn test(x MyType) {
    println(x)
}

fn main() {
    test(i64(0))
}
$ ./v run test.v
MyType(0)

Good!
The implementation seems pretty fragile though.
Many operations that are allowed for normal types, seem to crash the compiler when used with sumtypes.
For instance, casting:

type MyType = f64 | i64

fn test(x MyType) {
    println(i64(x))
}

fn main() {
    test(i64(0))
}
$ ./v run test.v
==================
/tmp/v_1000/test.11297848884384464859.tmp.c: 11633: error: cannot convert 'struct main__MyType' to 'long'
...
==================
(Use `v -cg` to print the entire error message)

builder error:
==================
C error. This should never happen.

This is a compiler bug, please report it using `v bug file.v`.

https://github.com/vlang/v/issues/new/choose

You can also use #help on Discord: https://discord.gg/vlang

Sum types have some restrictions:

type MyType = &i64 | &f64

fn main() {
}
$ ./v run test.v
test.v:1: 15: error: sum type cannot hold a reference type
    1 | type MyType = &i64 | &f64
      |               ~~~~
    2 |
    3 | fn main() {

but they don’t seem to be well checked 😢:

type MyType = Foo | Bar
type Foo = &i64
type Bar = &f64

fn main() {
}
$ ./v run test.v
$ echo $?
0

Evaluation: ⚠️ Sum types generally seem to work but there are implementation issues.

  • Generics

V generics are a little strange.
They’re much more like C++ templates than they are generics as known from Java, C#, ML, Rust, etc.
V doesn’t have a way to restrict the set of types allowed to be substituted (where T: IInterface in C#) which means the compiler can’t check generic code at declaration time:

fn foo(x T) {
        this_func_does_not_exist(x)
        what.the.f = "wut"
        x.not_a_real_method()
}

fn main() {
}
$ ./v run test.v
$ echo $?
0

Even though V ~generics~ templates are conceptually similar to C++ templates, the lack of SFINAE seems to lead to compiler crashes:

struct Wrapped {
    value T
}

fn (w Wrapped) double() Wrapped {
    return Wrapped { w.value 2 }
}

fn make_wrapped(value T) Wrapped {
    return Wrapped { value }
}

fn main() {
    println(make_wrapped("str"))
}
$ ./v run test.v
==================
/tmp/v_1000/test.6663405339574001690.tmp.c: 11622: error: invalid operand types for binary operation
/tmp/v_1000/test.6663405339574001690.tmp.c: 11622: error: invalid aggregate type for register load
...
==================
(Use `v -cg` to print the entire error message)

builder error:
==================
C error. This should never happen.

This is a compiler bug, please report it using `v bug file.v`.

https://github.com/vlang/v/issues/new/choose

You can also use #help on Discord: https://discord.gg/vlang

V’s decision here requires stamping out (monomorphisizing) functions for all concrete instances of templated types used in the program.
In C++, this generally leads to “template bloat” as often many monomorphic instances of a templated function can share code.
While the effects on code size can often be resolved via link-time optimizations, this can significantly hurt compile times.
Given V’s desire for extremely fast compile times, this is probably not a good strategy for the long term as it requires both V’s compiler to type-check each copy of the function separately as well as the C compiler to compile each instantiation as well.

The V compiler doesn’t seem to prevent the “mangled” templated type names from colliding with user defined type names either:

struct MyStruct {
    val T
}

struct Foo {
    x int
}

struct MyStruct_T_main__Foo {
    bla string
}

fn main() {
    x := MyStruct { Foo { 0 } }
    y := MyStruct_T_main__Foo { "" }
    println(x)
    println(y)
}
$ ./v run test.v
==================
/tmp/v_1000/test.5104305706961604858.tmp.c: 1112: error: struct/union/enum already defined
...
==================
(Use `v -cg` to print the entire error message)

builder error:
==================
C error. This should never happen.

This is a compiler bug, please report it using `v bug file.v`.

https://github.com/vlang/v/issues/new/choose

You can also use #help on Discord: https://discord.gg/vlang

I’m very torn on evaluating this feature.
On the one hand, very basic usage seems to work.
On the other hand, the implementation seems to be little more than textual substitution and bears no real relation to the generics of other programming languages (Go included!).

Evaluation: 🛑 A basic implementation exists but is very buggy and seems completely out of place in a language with an self described emphasis on safety and compiler performance.

  • No global variables (can be enabled for low level applications like kernels via a command line flag)

By default, V doesn’t allow global variables.

__global(
    x = i64(42)
)

fn main() {
}
$ ./v run test.v
test.v:1:1: error: use `v -enable-globals ...` to enable globals
    1 | __global(
      | ~~~~~~~~
    2 |     x = i64(42)
    3 | )

However, we’ve already re-implemented global variables using “pure” functions only.
Can we do better than that?
Oh, yes!

Presenting global variables in V:

const foo = make_global('hello world')

[heap]
struct Global {
    mut: value &T
}

fn make_global(value T) Global {
    return Global { &value }
}

fn assign_global(g &Global, value T) {
    mut g_m := g
    g_m.value = &value
}

fn main() {
    println(foo)
    assign_global(foo, 'changed')
    println(foo)
}
$ ./v run test.v
Global{
    value: &'hello world'
}
Global{
    value: &'changed'
}

We’re able to use V constants to re-implement global variables.
To do this, we use a function call to implicitly cast away the “const-ness” (to borrow a C++ term) of the foo constant and then we can confuse the compiler by aliasing an immutable reference as a mutable one.
Once there, we can directly assign to the value field of the Global.
Since V constants are just global variables to begin with, there’s no runtime protection for this either.

Evaluation: 🛑 V does not prevent you from creating and mutating globally shared state in any meaningful way.

Performance

  • As fast as C (V’s main backend compiles to human readable C)

To start, I’ll preface this by saying that comparing programming languages on the basis of performance is extremely difficult.
There are many factors and tradeoffs which have to be weighed for a complete comparison.
No individual benchmark or measurement will be perfect and results can vary widely from one machine to another.

I think it’s also important to point out that compiling to C does not automatically make your language fast and specifically it does not automatically give you C level performance.
To see this statement being used to justify the performance claim is frankly shocking.

As a novice V programmer, I can’t claim to be able to write fast V code.
I will therefore lean on community benchmarks where interested parties can battle it out themselves.

First up, let’s look at kostya/benchmarks.
Let’s check out V vs C.
For each benchmark that has both a C entry and a V entry, we’ll compare the fastest versions of each language:

Benchmark Fastest V time Fastest C time V delta
helloworld 1.5ms 1.5ms Equal
nbody 988ms 321ms 3.1x slower 🔻
nsieve 509ms 795ms 1.5% faster ⬆️
spectral-norm 4117ms 1406ms 2.9x slower 🔻

It seems early to draw conclusions from so few data points, but this doesn’t look good for V.
C is missing from quite a few of these benchmarks which makes it difficult to draw a conclusion.
Rust has many more of these benchmarks implemented and is considered to be on par with C for performance, so let’s look at those:

Benchmark Fastest V time Fastest Rust time V delta
binarytrees 1118ms 1792ms 1.6x faster ⬆️
coro-prime-sieve 2078ms 74ms 28.1x slower 🔻
edigits timeout 147ms Significantly slower 🔻
fannkuch-redux 3311ms 726ms 4.6x slower 🔻
fasta 769ms 180ms 4.3x slower 🔻
helloworld 1.5ms 2.1ms 1.4x faster ⬆️
lru 399ms 53ms 7.5x slower 🔻
nbody 988ms 253ms 3.9x slower 🔻
nsieve 509ms 465ms 1.1x slower 🔻
pidigits 4672ms 1829ms 2.6x slower 🔻
spectral-norm 4117ms 1065ms 3.9x slower 🔻

So V doesn’t seem to even be close to either C or Rust in these benchmarks.
Perhaps we should try looking at higher-level language that doesn’t offer as much control as C and Rust aim to: C#.

Benchmark Fastest V time Fastest C# time V delta
binarytrees 1118ms 1400ms 1.3x faster ⬆️
coro-prime-sieve 2078ms 410ms 5.1x slower 🔻
edigits timeout 1112ms Significantly slower 🔻
fasta 769ms 386ms 2.0x slower 🔻
helloworld 1.5ms 19ms 12.7x faster ⬆️
lru 399ms 187ms 2.1x slower 🔻
nbody 988ms 438ms 2.3x slower 🔻
nsieve 509ms 910ms 1.8x faster ⬆️
pidigits 4672ms timeout Significantly faster ⬆️
spectral-norm 4117ms 2999ms 1.4x slower 🔻

This seems like a much more mixed result and so I think it would be fair to say V is more competitive with a language like C# or Java than C.

If V was closer with either of C or Rust, I’d be interested to investigate more deeply but as it stands, I think it’s safe to say:

Evaluation: 🛑 V’s performance claims don’t seem to be valid

  • C interop without any costs

V does not use greenthreads or segmented stacks for async programming which can lead to C interp being expensive.
V strings are essentially just wrappers over C strings and there are convenient and cheap functions to convert between them.
v functions are just regular functions so you can freely pass pointers to them into C and vice-versa.

Evaluation: ✔️ Works as advertised

Minimal amount of allocations

This seems like an inherently subjective claim to me that can’t be objectively evaluated (who decides which allocations are necessary and which aren’t?).

Coming from a C background, I’m very surprised to see that merely taking the address of a value causes it to be heap allocated:

fn main() {
    x := 1
    y := &x
}
...
voidptr memdup(voidptr src, int sz) {
    if (sz == 0) {
        return vcalloc(1);
    }
    { // Unsafe block
        u8* mem = _v_malloc(sz);
        return memcpy(mem, src, sz);
    }
    return 0;
}
...
#define HEAP(type, expr) ((type*)memdup((void*)&((type[]){expr}[0]), sizeof(type)))
...
VV_LOCAL_SYMBOL void main__main(void) {
    int *x = HEAP(int, (1));
    int* y = &(*(x));
}

I would even argue this is clearly an unnecessary allocation which should not occur in a language that has a “minimal number of allocations”.
As I said earlier though, this claim is inherently subjective.

Evaluation: N/a (subjective claim)

  • Built-in serialization without runtime reflection

According to the docs, “V generates code for JSON encoding and decoding. No runtime reflection is used.”

Let’s see what code is generated:

import json

struct SerializeMe {
    field_one int
    field_two string
    field_three SomethingElse
}

struct SomethingElse {
    another_field bool
}

fn main() {
    x := SerializeMe { }
    json.encode(x)
}
...
cJSON* json__encode_main__SerializeMe(main__SerializeMe val) {
    cJSON *o;
    o = cJSON_CreateObject();
    cJSON_AddItemToObject(o, "field_one", json__encode_int(val.field_one));

    cJSON_AddItemToObject(o, "field_two", json__encode_string(val.field_two));

    cJSON_AddItemToObject(o, "field_three", json__encode_main__SomethingElse(val.field_three));

    return o;
}

cJSON* json__encode_main__SomethingElse(main__SomethingElse val) {
    cJSON *o;
    o = cJSON_CreateObject();
    cJSON_AddItemToObject(o, "another_field", json__encode_bool(val.another_field));

    return o;
}
...

Straightforward enough!
What’s cJSON though?

...
#if defined(__has_include)

#if __has_include("cJSON.h")
#include "cJSON.h"
#else
#error VERROR_MESSAGE Header file "cJSON.h", needed for module `json` was not found. Please install the corresponding development headers.
#endif

#else
#include "cJSON.h"
#endif
...

I don’t recall installing this library on my dev machine …
We’ll return to this in the next section.

As for this claim:

Evaluation: ✔️ Works as advertised

  • Compiles to native binaries without any dependencies: a simple web server is only 65 KB

There’s really two claims here and we’ll look at them both.

Compiles to native binaries without any dependencies

Let’s look at the program v run made for us:

$ file ./run
./run: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, not stripped
$ ldd ./run
        linux-vdso.so.1 (0x00007ffd17dfa000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f58a2988000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f58a2b84000)

So our program is a native binary that links to the system’s libc and loader dynamically but has no other dependencies.
Given that the claim was “without any dependencies”, I was expecting to see a statically linked binary but dynamically linking to libc is a reasonable choice and I can’t really fault V for that.

But wait, what was that cJSON business from the previous section?
Oh …

$ tree -L 2 thirdparty/
thirdparty/
├── cJSON
│   ├── cJSON.c
│   ├── cJSON.h
│   └── readme.txt
├── fontstash
│   ├── fontstash.h
│   └── stb_truetype.h
├── ios
│   └── ios.m
├── libbacktrace
│   ├── amalgamation.txt
│   ├── backtrace.c
│   ├── backtrace.h
│   ├── base.c
│   ├── darwin.c
│   ├── linux.c
│   └── windows.c
├── libgc
│   ├── amalgamation.txt
│   ├── gc.c
│   └── include
├── mssql
│   └── include
├── picoev
│   ├── picoev.c
│   └── src
├── picohttpparser
│   ├── picohttpparser.c
│   ├── picohttpparser.h
│   └── src
├── sokol
│   ├── sokol_app.h
│   ├── sokol_app2.h
│   ├── sokol_audio.h
│   ├── sokol_gfx.h
│   ├── sokol_v.post.h
│   ├── sokol_v.pre.h
│   └── util
├── stb_image
│   ├── stb_image.h
│   ├── stb_image_write.h
│   ├── stb_v_header.h
│   └── stbi.c
├── stdatomic
│   ├── nix
│   └── win
├── tcc
│   ├── README.md
│   ├── include
│   ├── lib
│   ├── share
│   └── tcc.exe
├── vschannel
│   ├── vschannel.c
│   └── vschannel.h
├── walloc
│   └── walloc.c
└── zip
    ├── miniz.h
    ├── zip.c
    └── zip.h
33 directories, 85 files

So when V says “without any dependencies”, it seems like what they’re actually saying is “we’ve bundled the dependencies for you”.
This isn’t the worst thing (although, I’m sure Linux distros will love having to undo this if they ever decide they want to package V for some reason) but it hardly seems like V programs exist “without any dependencies”.

I would suggest that the V developers consider changing this claim to something more precise such as “Compiles to native binaries without additional runtime dependencies.”

Now, onto the other claim:

a simple web server is only 65 KB

V comes with a builtin web server library called vweb which is tightly integrated into the compiler and standard library.
There’s also a “hello world” example using vweb in examples/vweb.
Let’s try compiling it and see what we get:

$ cd examples/vweb
$ ../../v ./vweb_example.v
builder error: 'openssl/rand.h' not found

I thought we were done talking about dependencies!
Sigh

$ sudo apt install libssl-dev
...
$ ../../v ./vweb_example.v
$ du -sh vweb_example
2.0M    vweb_example

Uh…
Maybe let’s try in prod mode which enables optimizations?

$ ../../v -prod ./vweb_example.v
$ du -sh vweb_example
280K    vweb_example

Well, that’s quite a bit smaller but still over 4x larger than what was claimed.
Maybe we should try stripping it?

$ strip vweb_example
$ du -sh vweb_example
264K    vweb_example

Not much difference.

Perhaps this example isn’t as minimal as it could be, but the code for the example fits on one screen.
It’s also possible the claim is referring to the most minimal web server possible: one which parses HTTP requests and does nothing with them.
Staking this claim on something so useless seems unreasonable though and I don’t feel particularly compelled to investigate that possibility further.

Evaluation: ⚠️ V doesn’t seem to achieve the exact claims made at this time but there is some truth to the general ideas that V programs are relatively self-contained and small.

Fast compilation

V compiles ≈110k (Clang backend) and ≈1 million (x64 and tcc backends) lines of code per second per CPU core.

According to Is V still fast, V’s compiler benchmarking site, V currently compiles 207,972 “V lines/s” using tcc.
Even if we accept the note on the page that “typical desktop hardware is 2-3 times faster”, which seems dubious in a single-threaded benchmark, that still puts the compiler around 500,000 – 600,000 lines of V per second: half the claimed amount.

On a side note, why is the V benchmark being run on a burstable AWS instance?
When measuring performance, especially wall clock time like is done here, it is critical you keep you keep your environment as stable as possible, especially CPU performance.
Serious performance investigations often go so far as to disable turboboosting, hyper threading, lock process affinity and other various tweaks in an attempt to produce consistent results.
Given the incredibly noisy environment these tests are being run in, I’m not that surprised to see that the site seems to only trigger the green/red (faster/slower) indicators at changes of at least 10%.

I’ll also try generating some 1,000,000 V programs locally and then time how long compiling them takes.
Since I’m using tcc, it should only take about 1 second to compile each program.

First up is “hello world – 1m” a basic program that contains 999,998 println('hello world') statements (so that the total line count including the main function declaration is 1,000,000 lines):

#!/usr/bin/bash
echo 'fn main() {'
for i in {3..1000000}
do
    echo '    println("hello world")'
done
echo '}'
$ ./t.sh > 1m_helloworld.v
$ wc -l 1m_helloworld.v
1000000 1m_helloworld.v
$ time ./v 1m_helloworld.v
parsed 100000 statements so far from fn main.main ...
parsed 200000 statements so far from fn main.main ...
parsed 300000 statements so far from fn main.main ...
parsed 400000 statements so far from fn main.main ...
parsed 500000 statements so far from fn main.main ...
parsed 600000 statements so far from fn main.main ...
parsed 700000 statements so far from fn main.main ...
parsed 800000 statements so far from fn main.main ...
parsed 900000 statements so far from fn main.main ...

real    0m12.451s
user    0m11.158s
sys     0m1.236s

It was at this point I suspected that when I built the compiler, it wasn’t built with optimizations enabled by default.
A quick look at the Makefile confirmed this and a rebuild later:

$ VFLAGS="-prod" make
cd ./vc && git clean -xf && git pull --quiet
cd ./thirdparty/tcc && git clean -xf && git pull --quiet
cc  -std=gnu99 -w -o v1.exe ./vc/v.c -lm -lpthread
./v1.exe -no-parallel -o v2.exe -prod cmd/v
./v2.exe -o ./v -prod cmd/v
rm -rf v1.exe v2.exe
Note: building an optimized binary takes much longer. It shouldn't be used with `v run`.
Use `v run` without optimization, or build an optimized binary with -prod first, then run it separately.

Note: `tcc` was not used, so unless you install it yourself, your backend
C compiler will be `cc`, which is usually either `clang`, `gcc` or `msvc`.

These C compilers, are several times slower at compiling C source code,
compared to `tcc`. They do produce more optimised executables, but that
is done at the cost of compilation speed.

V has been successfully built
V 0.2.4 50ab2cf

The “Note” in the above is wrong in this case.
I confirmed via strace that my optimized version of v still invokes tcc and not cc.

With that out of the way:

$ time ./v 1m_helloworld.v
parsed 100000 statements so far from fn main.main ...
parsed 200000 statements so far from fn main.main ...
parsed 300000 statements so far from fn main.main ...
parsed 400000 statements so far from fn main.main ...
parsed 500000 statements so far from fn main.main ...
parsed 600000 statements so far from fn main.main ...
parsed 700000 statements so far from fn main.main ...
parsed 800000 statements so far from fn main.main ...
parsed 900000 statements so far from fn main.main ...

real    0m7.158s
user    0m5.910s
sys     0m1.275s

So quite a bit faster (now at 169,205 lines/second), but not close to the 1 second time that was advertised.

For the record, my (quite old 😢) hardware is a i7-4770 with an SSD while the claim uses a i5-7500 with SSD.
A quick search shows these are essentially equivalent to each other or, at least, there isn’t a 7x performance difference between them.

Let’s try another test, this time we’ll generate 499,999 empty functions and an empty main function for a total of 1,000,000 lines:

#!/usr/bin/bash
echo 'fn main() {'
echo '}'
for i in {0..499998}
do
        echo "fn my_func${i}() {"
        echo '}'
done
$ ./t.sh > 1m_funcs.v
$ wc -l 1m_funcs.v
1000000 1m_funcs.v
$ time ./v 1m_funcs.v

real    0m3.686s
user    0m2.587s
sys     0m1.105s

Quite a bit faster (386,548 lines/second) but still 2.5x slower than claimed.

Let’s try one more test in which we’ll create a sum type and a function to print an argument of that type:

#!/usr/bin/bash
for i in {3..65000}
do
        echo "type Foo${i} = i64 | byte | string"
        echo "fn my_func${i}(x Foo${i}) {"
        echo "    println(x)"
        echo "}"
done
echo 'fn main() {'
echo '}'

Wait, why are we only looping to 65000 and not 1000000?
The V compiler ICEs if you have more than 2^16 - 1 types in it:

$ time ./v 1m_types_and_funcs.v
V panic: new_type: idx must be between 1 & 65535
v hash: 50ab2cf
    | 0x55896206ca8e | ./v(+0x7ea8e)
    | 0x55896207a449 | ./v(+0x8c449)
    | 0x5589620805d0 | ./v(+0x925d0)
    | 0x558962083f64 | ./v(+0x95f64)
    | 0x558962075bbb | ./v(+0x87bbb)
    | 0x558962079533 | ./v(+0x8b533)
    | 0x558962029e15 | ./v(+0x3be15)
    | 0x55896202e1b4 | ./v(+0x401b4)
    | 0x558961ff059e | ./v(+0x259e)
    | 0x7fc805ff40b3 | /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf3)
    | 0x558961ff05de | ./v(+0x25de)

Therefore, after adjusting the test case to account for that, we get this:

$ ./t.sh > 1m_types_and_funcs.v
$ wc -l 1m_types_and_funcs.v
259994 1m_types_and_funcs.v
$ time ./v 1m_types_and_funcs.v

real    0m46.592s
user    0m45.574s
sys     0m1.030s

Aka 5,705 lines/second.

Measuring compiler throughput in “lines per second” is completely useless because it matters so much what those lines are.
As we’ve seen, by changing the programs we fed to V, we got anywhere from just under 400,000 lines per second to less than 6,000 lines per second.

Evaluation: 🛑 The V compiler does not come close to the claimed level of performance.

V is written in V and compiles itself in under a second.

V is most certainly written in V, so that is accurate!

Let’s change their Makefile to include timings:

diff --git a/GNUmakefile b/GNUmakefile
index aab552aa1..e81520924 100644
--- a/GNUmakefile
+++ b/GNUmakefile
@@ -91,8 +91,8 @@ ifdef WIN32
        del v2.exe
 else
        $(CC) $(CFLAGS) -std=gnu99 -w -o v1.exe $(VC)/$(VCFILE) -lm -lpthread $(LDFLAGS)
-       ./v1.exe -no-parallel -o v2.exe $(VFLAGS) cmd/v
-       ./v2.exe -o $(V) $(VFLAGS) cmd/v
+       bash -c 'time ./v1.exe -no-parallel -o v2.exe $(VFLAGS) cmd/v'
+       bash -c 'time ./v2.exe -o $(V) $(VFLAGS) cmd/v'
        rm -rf v1.exe v2.exe
 endif
        @$(V) run cmd/tools/detect_tcc.v
$ make
cd ./vc && git clean -xf && git pull --quiet
cd ./thirdparty/tcc && git clean -xf && git pull --quiet
cc  -std=gnu99 -w -o v1.exe ./vc/v.c -lm -lpthread
bash -c 'time ./v1.exe -no-parallel -o v2.exe  cmd/v'

real    0m0.955s
user    0m0.885s
sys     0m0.070s
bash -c 'time ./v2.exe -o ./v  cmd/v'

real    0m1.029s
user    0m1.338s
sys     0m0.067s
rm -rf v1.exe v2.exe
Your `tcc` is working. Good - it is much faster at compiling C source code.
V has been successfully built
V 0.2.4 50ab2cf

So a stage 2 compiler builds in ~1 second and so does a stage 3 compiler.
Excellent!

Evaluation: ✔️ Works as advertised

Innovative memory management

I’m not even sure how to approach this topic.
There are a ton of claims made in this section and I can’t really figure out what the basis for these claims is.

Let’s start with the example given in the docs:

import strings

fn draw_text(s string, x int, y int) {
	// ...
}

fn draw_scene() {
	// ...
	name1 := 'abc'
	name2 := 'def ghi'
	draw_text('hello $name1', 10, 10)
	draw_text('hello $name2', 100, 10)
	draw_text(strings.repeat(`X`, 10000), 10, 50)
	// ...
}

The strings don’t escape draw_text, so they are cleaned up when the function exits.

While this is true, it’s not clear how the compiler knows this.
The types don’t include “does escape”/”does not escape” information so we must deduce that the compiler performs an analysis of the body of draw_text to determine if the values escape or not.
We’ll modify draw_text in a minute to see if we can determine how this analysis works but right now, let’s compile this with autofree enabled and see if any memory is leaked:

(Note, the docs say “Autofree is still WIP” but this really should be mentioned on the feature list)

$ ./v -autofree autofree_example.v
$ valgrind --leak-check=full ./autofree_example
==2183== Memcheck, a memory error detector
==2183== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==2183== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==2183== Command: ./autofree_example
==2183==
==2183==
==2183== HEAP SUMMARY:
==2183==     in use at exit: 0 bytes in 0 blocks
==2183==   total heap usage: 18 allocs, 18 frees, 17,240 bytes allocated
==2183==
==2183== All heap blocks were freed -- no leaks are possible
==2183==
==2183== For lists of detected and suppressed errors, rerun with: -s
==2183== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Ok, so far so good!
However, I’m not sure that these strings are actually being heap allocated.
The docs also say “V tries to put objects on the stack if possible” so let’s see if we can force a heap allocation by creating a [heap] struct instead:

[heap]
struct MyHeapValue {
    value i64
}

fn draw_text(value MyHeapValue) {
}

fn main() {
    // ...
    arg1 := MyHeapValue { 42 }
    arg2 := MyHeapValue { 100 }
    draw_text(arg1)
    draw_text(arg2)
    // ...
}
$ ./v -autofree autofree_example.v
$ valgrind --leak-check=full ./autofree_example
==2312== Memcheck, a memory error detector
==2312== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==2312== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==2312== Command: ./autofree_example
==2312==
==2312==
==2312== HEAP SUMMARY:
==2312==     in use at exit: 32 bytes in 4 blocks
==2312==   total heap usage: 22 allocs, 18 frees, 17,272 bytes allocated
==2312==
==2312== 8 bytes in 1 blocks are definitely lost in loss record 1 of 4
==2312==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==2312==    by 0x41B3AE: _v_malloc (in /tmp/v/autofree_example)
==2312==    by 0x41C095: memdup (in /tmp/v/autofree_example)
==2312==    by 0x435070: main__main (in /tmp/v/autofree_example)
==2312==    by 0x4472C4: main (in /tmp/v/autofree_example)
==2312==
==2312== 8 bytes in 1 blocks are definitely lost in loss record 2 of 4
==2312==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==2312==    by 0x41B3AE: _v_malloc (in /tmp/v/autofree_example)
==2312==    by 0x41C095: memdup (in /tmp/v/autofree_example)
==2312==    by 0x4350B9: main__main (in /tmp/v/autofree_example)
==2312==    by 0x4472C4: main (in /tmp/v/autofree_example)
==2312==
==2312== 8 bytes in 1 blocks are definitely lost in loss record 3 of 4
==2312==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==2312==    by 0x41B3AE: _v_malloc (in /tmp/v/autofree_example)
==2312==    by 0x41C095: memdup (in /tmp/v/autofree_example)
==2312==    by 0x43501A: main__draw_text (in /tmp/v/autofree_example)
==2312==    by 0x4350D4: main__main (in /tmp/v/autofree_example)
==2312==    by 0x4472C4: main (in /tmp/v/autofree_example)
==2312==
==2312== 8 bytes in 1 blocks are definitely lost in loss record 4 of 4
==2312==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==2312==    by 0x41B3AE: _v_malloc (in /tmp/v/autofree_example)
==2312==    by 0x41C095: memdup (in /tmp/v/autofree_example)
==2312==    by 0x43501A: main__draw_text (in /tmp/v/autofree_example)
==2312==    by 0x4350EB: main__main (in /tmp/v/autofree_example)
==2312==    by 0x4472C4: main (in /tmp/v/autofree_example)
==2312==
==2312== LEAK SUMMARY:
==2312==    definitely lost: 32 bytes in 4 blocks
==2312==    indirectly lost: 0 bytes in 0 blocks
==2312==      possibly lost: 0 bytes in 0 blocks
==2312==    still reachable: 0 bytes in 0 blocks
==2312==         suppressed: 0 bytes in 0 blocks
==2312==
==2312== For lists of detected and suppressed errors, rerun with: -s
==2312== ERROR SUMMARY: 4 errors from 4 contexts (suppressed: 0 from 0)

So forcing the value to be allocated on the heap reveals that autofree leaks the values.
That doesn’t really inspire confidence in the “most objects (~90-100%) are freed by V’s autofree engine” claim.

It’s interesting though that we see 4 leaks for only 2 values.
What is main__draw_text doing that causes the additional leaks?

...
#define HEAP(type, expr) ((type*)memdup((void*)&((type[]){expr}[0]), sizeof(type)))
...
VV_LOCAL_SYMBOL void main__draw_text(main__MyHeapValue _v_toheap_value) {
main__MyHeapValue* value = HEAP(main__MyHeapValue, _v_toheap_value);
}

So even though the parameter is completely unused, V still heap allocates a copy of it for no reason?
What happened to “no unnecessary allocations”?

I’m starting to suspect that there isn’t some complex compiler pass that works for strings but fails on a heap allocated integer and the reason we’re not seeing leaks when using strings has something to do with the string implementation.

Let’s take a look at the string implementation:

pub struct string {
pub:
	str &u8 = 0 // points to a C style 0 terminated string of bytes.
	len int // the length of the .str field, excluding the ending 0 byte. It is always equal to strlen(.str).
	// NB string.is_lit is an enumeration of the following:
	// .is_lit == 0 => a fresh string, should be freed by autofree
	// .is_lit == 1 => a literal string from .rodata, should NOT be freed
	// .is_lit == -98761234 => already freed string, protects against double frees.
	// ---------> ^^^^^^^^^ calling free on these is a bug.
	// Any other value means that the string has been corrupted.
mut:
	is_lit int
}

So, it looks like string literals are special cased to never have free called on them.
What happens if we take the original program but clone the strings?

import strings

fn draw_text(s string, x int, y int) {
}

fn main() {
        name1 := 'abc'.str()
        name2 := 'def ghi'.str()
        draw_text('hello $name1'.str(), 10, 10)
        draw_text('hello $name2'.str(), 100, 10)
        draw_text(strings.repeat(`X`, 10000).str(), 10, 50)
}
$ ./v -autofree autofree_example.v
$ valgrind --leak-check=full ./autofree_example
==2723== Memcheck, a memory error detector
==2723== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==2723== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==2723== Command: ./autofree_example
==2723==
==2723==
==2723== HEAP SUMMARY:
==2723==     in use at exit: 10,025 bytes in 3 blocks
==2723==   total heap usage: 30 allocs, 27 frees, 37,826 bytes allocated
==2723==
==2723== 10 bytes in 1 blocks are definitely lost in loss record 1 of 3
==2723==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==2723==    by 0x41B732: malloc_noscan (in /tmp/v/autofree_example)
==2723==    by 0x41C113: memdup_noscan (in /tmp/v/autofree_example)
==2723==    by 0x402D7C: strings__Builder_str (in /tmp/v/autofree_example)
==2723==    by 0x432D31: str_intp (in /tmp/v/autofree_example)
==2723==    by 0x435224: main__main (in /tmp/v/autofree_example)
==2723==    by 0x44776E: main (in /tmp/v/autofree_example)
==2723==
==2723== 14 bytes in 1 blocks are definitely lost in loss record 2 of 3
==2723==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==2723==    by 0x41B732: malloc_noscan (in /tmp/v/autofree_example)
==2723==    by 0x41C113: memdup_noscan (in /tmp/v/autofree_example)
==2723==    by 0x402D7C: strings__Builder_str (in /tmp/v/autofree_example)
==2723==    by 0x432D31: str_intp (in /tmp/v/autofree_example)
==2723==    by 0x435424: main__main (in /tmp/v/autofree_example)
==2723==    by 0x44776E: main (in /tmp/v/autofree_example)
==2723==
==2723== 10,001 bytes in 1 blocks are definitely lost in loss record 3 of 3
==2723==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==2723==    by 0x41B732: malloc_noscan (in /tmp/v/autofree_example)
==2723==    by 0x403D19: strings__repeat (in /tmp/v/autofree_example)
==2723==    by 0x4354C6: main__main (in /tmp/v/autofree_example)
==2723==    by 0x44776E: main (in /tmp/v/autofree_example)
==2723==
==2723== LEAK SUMMARY:
==2723==    definitely lost: 10,025 bytes in 3 blocks
==2723==    indirectly lost: 0 bytes in 0 blocks
==2723==      possibly lost: 0 bytes in 0 blocks
==2723==    still reachable: 0 bytes in 0 blocks
==2723==         suppressed: 0 bytes in 0 blocks
==2723==
==2723== For lists of detected and suppressed errors, rerun with: -s
==2723== ERROR SUMMARY: 3 errors from 3 contexts (suppressed: 0 from 0)

The strings don’t escape draw_text, so they are cleaned up when the function exits.

So the docs are completely wrong, there’s no analysis being performed to determine when the function arguments escape and when they don’t and the compiler isn’t inserting cleanups (calls to free).
There’s no leaks because there are no heap allocations and introducing heap allocations immediately causes memory to be leaked.

Let’s see if we can figure out what the compiler actually does.

Is there an escape analysis?

The compiler source code is located in vlib/v.

Grepping for “escape” shows 166 results, all of which are related to parsing or printing special (escape) characters in strings or paths.

I don’t think there is any escape analysis in the V compiler.

Is there a lifetime analysis?

Some of the documentation implies there might be some kind of lifetime analysis that decides whether to stack or heap allocate variables.

Grepping for “lifetime” returns no results.
“live” leads us to an attribute called [keep_args_alive] which only has an effect in GC’d binaries.
No other instances of “live” are related to a lifetime analysis.

I can’t find any lifetime analysis in the V compiler.

So what does -autofree actually do?

It’s very unclear to me.
“Reference counting, mut parameters and memory safety” seems to be the most detailed discussion I can find in GitHub and even it is sparse on details.

Looking through the various references to “autofree” in the gen phase of the compiler, it seems like autofree is an RAII-like scheme.


Unlike Rust and C++, V doesn’t have move semantics which requires copying/cloning data in additional situations.
V also lacks any kind of lifetime or borrowing system like Rust so to maintain memory safety, copying/cloning data is required for this reason as well.
This is a problem because unnecessary copies/clones means V will do more work than C/C++/Rust when it comes to basic memory management and it’s difficult to reconcile with their performance claims.

The current implementation lacks any kind of detailed analysis that would make it safe to elide these unnecessary copies/clones.
There does not appear to by any fallback code which would insert reference counting or garbage collection either.
At least as it is presently, autofree does not seem to be particularly innovative in any way.

Evaluation: 🛑 V’s claims are not well supported and the existing implementation cannot be described as “innovative”.

Summary

To consolidate the above into one table:

Area Rating Notes
Simple language for building maintainable programs N/a Subjective claim.
Safety    
> No null 🛑 We’re able to create a null pointer (V reference) with no compiler errors or warnings.
> No undefined values 🛑 We could read uninitialized memory without compiler error or warnings.
> No undefined behavior 🛑 The V compiler didn’t stop us from creating three different forms of UB.
> No variable shadowing ✔️ We weren’t able to shadow local variables.
> Bounds checking ⚠️ Some basic checking exists but can be trivially bypassed.
> Immutable variables by default 🛑 Variables aren’t immutable in any significant way because you can trivially turn an immutable reference into a mutable one.
> Pure functions by default 🛑 Claim is meaningless as it redefines “pure” to mean “impure”.
> Immutable structs by default 🛑 Immutability can be by-passed trivially.
> Option/Result and mandatory error checks ✔️ Works as advertised.
> Sum types ⚠️ Sum types generally seem to work but there are implementation issues.
> Generics 🛑 A basic implementation exists but is very buggy and seems completely out of place in a language with an self described emphasis on safety and compiler performance.
> No global variables 🛑 V does not prevent you from creating and mutating globally shared state in any meaningful way.
Performance    
> As fast as C (V’s main backend compiles to human readable C) 🛑 V’s performance claims don’t seem to be valid.
> C interop without any costs ✔️ Works as advertised.
> Minimal amount of allocations N/a Subjective claim.
> Built-in serialization without runtime reflection ✔️ Works as advertised.
> Compiles to native binaries without any dependencies ⚠️ V doesn’t seem to achieve the exact claims made at this time but there is some truth to the general ideas that V programs are relatively self-contained and small.
Fast Compilation    
> V compiles ≈1 million lines of code per second per CPU core 🛑 The V compiler does not come close to the claimed level of performance.
> V is written in V and compiles itself in under a second ✔️ Works as advertised.
Innovative memory management 🛑 V’s claims are not well supported and the existing implementation cannot be described as “innovative”.

At this time, I would not recommend spending time on V.
I would also be very cautious when taking claims made by the authors at face value.

Read More

Continue Reading
Click to comment

Leave a Reply

Your email address will not be published.

Tech

Google Pixel 7 and 7 Pro are getting a built-in VPN at no extra cost

Published

on

By

Google Pixel 7 and 7 Pro are getting a built-in VPN at no extra cost
Google Pixel 7 Pro hands on front Snow



(Image credit: Future / Lance Ulanoff)

Users of the Google Pixel 7 and 7 Pro devices will be able to secure their data without the need to pay for an additional Android VPN after the company said it would be including its Google One VPN service at no extra cost. 

The move will make the Pixel 7 and 7 Pro the first smartphones to include a free VPN connection. 

The offer is restricted to just some countries, though – and what’s more, some data won’t be secured inside the VPN tunnel.  

Peace of mind when you connect online ✨Later this year, #Pixel7 and 7 Pro will be the only phones with a VPN by Google One—at no extra cost.¹#MadeByGoogle¹See image for more info pic.twitter.com/P7lzyoMdekOctober 6, 2022

See more

Google Pixel 7 VPN

Despite the aforementioned limits, the big tech giant assures that the VPN software won’t associate users’ app and browsing data with users’ accounts. 

Google One VPN typically costs around $10 per month as part of the company’s Premium One plan, which also comes with a 2TB of cloud storage on top. 

This decision is the latest move to bring Google’s mobile data security to the next level. Not too long ago, the company made Google One VPN available also for iOS devices, and also introduced the option of having an always-on VPN across its latest smartphones. 

Google promises that its secure VPN software will shield your phone against hackers on unsecure networks, like public Wi-Fi. It will also hide your IP address so that third parties won’t be able to track your location.

Shorter for virtual private network, a VPN is exactly the tool you want to shield your sensitive data as it masks your real location and encrypts all your data in transit. Beside privacy, it can allow you to bypass geo-restrictions and other online blocks. 

Chiara is a multimedia journalist, with a special eye for latest trends and issues in cybersecurity. She is a Staff Writer at Future with a focus on VPNs. She mainly writes news and features about data privacy, online censorship and digital rights for TechRadar, Tom’s Guide and T3. With a passion for digital storytelling in all its forms, she also loves photography, video making and podcasting. Originally from Milan in Italy, she is now based in Bristol, UK, since 2018.

Read More

Continue Reading

Tech

The Steam Deck dock is finally here and will ship faster than you think

Published

on

By

The Steam Deck dock is finally here and will ship faster than you think
a steam deck placed in a steam deck dock



(Image credit: Valve)

After months of waiting and delays, Valve has finally announced that the Steam Deck dock is available for purchase on its official site.

Not only that but, according to Valve, the dock will ship out in an incredibly fast one to two weeks, which pairs with the fact that the Steam Deck itself is now shipping with no wait time (not to mention that it’s incredibly easy to set up). The port selection is pretty solid as well, with the dock featuring three USB-A 3.1 gen 1 ports, one Ethernet port, a DisplayPort 1.4, and an HDMI 2.0 port. And for its power supply, it uses a USB-C passthrough delivery.

A Steam Deck dock will run you $90 (around £81 / AU$140), which is a bit steeper than most third-party options on the market right now. But for those waiting it out for an official product until now, price most likely will not be an issue.

Is it worth buying? 

Considering that even Steam Decks themselves are shipping without a queue and that the dock has such a quick turnaround to delivery, it seems that the supply chain issues that had been gripping Valve are loosening considerably.

However, the deck itself is far from perfect. Because of the fact that it uses USB-C for the display port, a third-party USB-C dock that uses its own power supply and video out will output the display of the official dock. 

And as mentioned before, the price of the official Steam Deck dock is steeper than many third-party options on the market, meaning that those who are on a budget might pass this product up in favor of a lower-priced one.

There are also some bugs that Valve is working on fixing at this time, including one involving compatibility with LG displays. According to the FAQ, if the “Docking Station is connected via HDMI, sleep/wake can result in visual noise.”

It might be worth waiting for Valve to work out the kinks of its dock before investing in one. And while you’re waiting, research other options that might better suit your needs.

Allisa has been freelancing at TechRadar for nine months before joining as a Computing Staff Writer. She mainly covers breaking news and rumors in the computing industry, and does reviews and featured articles for the site. In her spare time you can find her chatting it up on her two podcasts, Megaten Marathon and Combo Chain, as well as playing any JRPGs she can get her hands on.

Read More

Continue Reading

Tech

Why doesn’t Bash’s `set -e` do what I expected?

Published

on

By

Why doesn’t set -e (or set -o errexit, or trap ERR) do what I expected?

set -e was an attempt to add “automatic error detection” to the shell. Its goal was to cause the shell to abort any time an error occurred, so you don’t have to put || exit 1 after each important command. This does not work well in practice.

The goal of automatic error detection is a noble one, but it requires the ability to tell when an error actually occurred. In modern high-level languages, most tasks are performed by using the language’s builtin commands or features. The language knows whether (for example) you tried to divide by zero, or open a file that you can’t open, and so on. It can take action based on this knowledge.

But in the shell, most of the tasks you actually care about are done by external programs. The shell can’t tell whether an external program encountered something that it considers an error — and even if it could, it wouldn’t know whether the error is an important one, worthy of aborting the entire program, or whether it should carry on.

The only information conveyed to the shell by the external program is an exit status — by convention, 0 for success, and non-zero for “some kind of error”. The developers of the original Bourne shell decided that they would create a feature that would allow the shell to check the exit status of every command that it runs, and abort if one of them returns non-zero. Thus, set -e was born.

But many commands return non-zero even when there wasn’t an error. For example,

if [ -d /foo ]; then ...; else ...; fi

If the directory doesn’t exist, the [ command returns non-zero. Clearly we don’t want to abort when that happens — our script wants to handle that in the else part. So the shell implementors made a bunch of special rules, like “commands that are part of an if test are immune”, and “commands in a pipeline, other than the last one, are immune”.

These rules are extremely convoluted, and they still fail to catch even some remarkably simple cases. Even worse, the rules change from one Bash version to another, as Bash attempts to track the extremely slippery POSIX definition of this “feature”. When a SubShell is involved, it gets worse still — the behavior changes depending on whether Bash is invoked in POSIX mode. Another wiki has a page that covers this in more detail. Be sure to check the caveats.

A reference comparing behavior across various historical shells also exists.

Story time

Consider this allegory, originally posted to bug-bash:

Once upon a time, a man with a dirty lab coat and long, uncombed hair
showed up at the town police station, demanding to see the chief of
police.  "I've done it!" he exclaimed.  "I've built the perfect
criminal-catching robot!"

The police chief was skeptical, but decided that it might be worth
the time to see what the man had invented.  Also, he secretly thought,
it might be a somewhat unwise move to completely alienate the mad
scientist and his army of hunter robots.

So, the man explained to the police chief how his invention could tell
the difference between a criminal and law-abiding citizen using a
series of heuristics.  "It's especially good at spotting recently
escaped prisoners!" he said.  "Guaranteed non-lethal restraints!"

Frowning and increasingly skeptical, the police chief nevertheless
allowed the man to demonstrate one robot for a week.  They decided that
the robot should patrol around the jail.  Sure enough, there was a
jailbreak a few days later, and an inmate digging up through the
ground outside of the prison facility was grabbed by the robot and
carried back inside the prison.

The surprised police chief allowed the robot to patrol a wider area.
The next day, the chief received an angry call from the zookeeper.
It seems the robot had cut through the bars of one of the animal cages,
grabbed the animal, and delivered it to the prison.

The chief confronted the robot's inventor, who asked what animal it
was.  "A zebra," replied the police chief.  The man slapped his head and
exclaimed, "Curses!  It was fooled by the black and white stripes!
I shall have to recalibrate!"  And so the man set about rewriting the
robot's code.  Black and white stripes would indicate an escaped
inmate UNLESS the inmate had more than two legs.  Then it should be
left alone.

The robot was redeployed with the updated code, and seemed to be
operating well enough for a few days.  Then on Saturday, a mob of
children in soccer clothing, followed by their parents, descended
on the police station.  After the chaos subsided, the chief was told
that the robot had absconded with the referee right in the middle of
a soccer game.

Scowling, the chief reported this to the scientist, who performed a
second calibration.  Black and white stripes would indicate an escaped
inmate UNLESS the inmate had more than two legs OR had a whistle on
a necklace.

Despite the second calibration, the police chief declared that the robot
would no longer be allowed to operate in his town.  However, the news
of the robot had spread, and requests from many larger cities were
pouring in.  The inventor made dozens more robots, and shipped them off
to eager police stations around the nation.  Every time a robot grabbed
something that wasn't an escaped inmate, the scientist was consulted,
and the robot was recalibrated.

Unfortunately, the inventor was just one man, and he didn't have the
time or the resources to recalibrate EVERY robot whenever one of them
went awry.  The robot in Shangri-La was recalibrated not to grab a
grave-digger working on a cold winter night while wearing a ski mask,
and the robot in Xanadu was recalibrated not to capture a black and
white television set that showed a movie about a prison break, and so
on.  But the robot in Xanadu would still grab grave-diggers with ski
masks (which it turns out was not common due to Xanadu's warmer climate),
and the robot in Shangri-La was still a menace to old televisions (of
which there were very few, the people of Shangri-La being on the average
more wealthy than those of Xanadu).

So, after a few years, there were different revisions of the
criminal-catching robot in most of the major cities.  In some places,
a clever criminal could avoid capture by wearing a whistle on a string
around the neck.  In others, one would be well-advised not to wear orange
clothing in certain rural areas, no matter how close to the Harvest
Festival it was, unless one also wore the traditional black triangular
eye-paint of the Pumpkin King.

Many people thought, "This is lunacy!"  But others thought the robots
did more good than harm, all things considered, and so in some places
the robots are used, while in other places they are shunned.

The end.

Exercises

Or, “so you think set -e is OK, huh?”

Exercise 1: why doesn’t this example print anything?

   1 
   2 set -e
   3 i=0
   4 let i++
   5 echo "i is $i"

Exercise 2: why does this one sometimes appear to work? In which versions of bash does it work, and in which versions does it fail?

   1 
   2 set -e
   3 i=0
   4 ((i++))
   5 echo "i is $i"

Exercise 3: why aren’t these two scripts identical?

   1 
   2 set -e
   3 test -d nosuchdir && echo no dir
   4 echo survived
   1 
   2 set -e
   3 f() { test -d nosuchdir && echo no dir; }
   4 f
   5 echo survived

Exercise 4: why aren’t these two scripts identical?

   1 set -e
   2 f() { test -d nosuchdir && echo no dir; }
   3 f
   4 echo survived
   1 set -e
   2 f() { if test -d nosuchdir; then echo no dir; fi; }
   3 f
   4 echo survived

Exercise 5: under what conditions will this fail?

   1 set -e
   2 read -r foo < configfile

(Answers)

But wait, there’s more!

Even if you use expr(1) (which we do not recommend — use arithmetic expressions instead), you still run into the same problem:

   1 set -e
   2 foo=$(expr 1 - 1)
   3 
   4 echo survived

Subshells from command substitution unset set -e, however (unless inherit_errexit is set with Bash 4.4):

   1 set -e
   2 foo=$(expr 1 - 1; true)
   3 
   4 echo survived

Note that set -e is not unset for commands that are run asynchronously, for example with process substitution:

   1 set -e
   2 mapfile foo < <(true; echo foo)
   3 echo ${foo[-1]} 
   4 mapfile foo < <(false; echo foo)
   5 echo ${foo[-1]} 

Another pitfall associated with set -e occurs when you use commands that look like assignments but aren’t, such as export, declare, typeset or local.

   1 set -e
   2 f() { local var=$(somecommand that fails); }
   3 f    
   4 
   5 g() { local var; var=$(somecommand that fails); }
   6 g    

In function f, the exit status of somecommand is discarded. It won’t trigger the set -e because the exit status of local masks it (the assignment to the variable succeeds, so local returns status 0). In function g, the set -e is triggered because it uses a real assignment which returns the exit status of somecommand.

A particularly dangerous pitfall with set -e is combining functions with conditionals. The following snippets will not behave the same way:

   1 set -e
   2 f() { false; echo "This won't run, right?"; }
   3 f
   4 echo survived
   1 set -e
   2 f() { false; echo "This won't run, right?"; }
   3 if f; then  
   4     echo survived
   5 fi

As soon as a function is used as a conditional (in a list or with a conditional test or loop) set -e stops being applied within the function. This may not only cause code to unexpectedly start executing in the function but also change its return status!

Using Process substitution, the exit code is also discarded as it is not visible from the main script:

   1 set -e
   2 cat <(somecommand that fails)
   3 echo survived

Using a pipe makes no difference, as only the rightmost process is considered:

   1 set -e
   2 somecommand that fails | cat -
   3 echo survived

set -o pipefail is a workaround by returning the exit code of the first failed process:

   1 set -e -o pipefail
   2 failcmd1 | failcmd2 | cat -
   3 
   4 echo survived

though with pipefail in effect, code like this will sometimes cause an error, depending on whether the output of somecmd exceeds the size of the pipe buffer or not:

   1 set -e -o pipefail
   2 somecmd | head -n1
   3 
   4 echo survived

So-called strict mode

In the mid 2010s, some people decided that the combination of set -e, set -u and set -o pipefail should be used by default in all new shell scripts. They call this unofficial bash strict mode, and they claim that it “makes many classes of subtle bugs impossible” and that if you follow this policy, you will “spend much less time debugging, and also avoid having unexpected complications in production”.

As we’ve already seen in the exercises above, these claims are dubious at best. The behavior of set -e is quite unpredictable. If you choose to use it, you will have to be hyper-aware of all the false positives that can cause it to trigger, and work around them by “marking” every line that’s allowed to fail with something like ||true.

Conclusions

GreyCat‘s personal recommendation is simple: don’t use set -e. Add your own error checking instead.

rking’s personal recommendation is to go ahead and use set -e, but beware of possible gotchas. It has useful semantics, so to exclude it from the toolbox is to give into FUD.

geirha’s personal recommendation is to handle errors properly and not rely on the unreliable set -e.

Read More

Continue Reading

Trending

Copyright © 2022 Xanatan