05-11-2026 07:57 AM
I’ve been wiring in LabVIEW since October 2000, starting with LabVIEW 6.0i (honestly, what the hell was that…).
And over all these years, one small thing has been persistently annoying me...
This one:
What could possibly be wrong with such a trivial operation, you might ask?
Here’s the small nuance: the input array is empty, but the Index Array node returns a "default" value — 0.0.
That’s it.
The same happens at the other end: if there are two elements in the array and I read the third one, I get 0.0 again:
Just to be clear: LabVIEW was not my first programming language. I started my learning with Fortran and Pascal, then C and Delphi, a bit of C++, Modula-2, Oberon, and Assembly (both DEC PDP-11 and Intel). In almost all of these languages, attempting something similar would result in kind of hmm... “side effects.” For example:
#include <ansi_c.h>
int main (int argc, char *argv[])
{
double arr[2] = {1.0, 2.0};
int index = 2;
double element = arr[index];
printf("%f\n", element);
return 0;
}
This program in C invokes well known Undefined Behavior because it accesses an array element outside its bounds.
It might print garbage, or zero; it might crash in debug mode, or appear to “work” in release mode...
But not in LabVIEW — there is no UB at all in this case. Instead, a default value is silently returned. Some "traditional" compilers can detect this via static analysis and warn you, others not.
Now the real problem is that I cannot distinguish between two cases:
0.0 as a valid return (this is obviously perfectly valid floating-point value)
0.0 as a signal and indication of an out-of-bounds access
Well, for floating-point types, we could theoretically use NaN as a sentinel. But for integers, this is impossible — every bit pattern represents a valid value.
Of course, I could always implement my own manual bounds checking, but that adds unnecessary overhead and boilerplate. So most of the time I rely on the built-in LabVIEW behavior — while always keeping this "issue" in mind whenever I drop an Index Array primitive on the block diagram.
It gets even more interesting and funny with Replace Array Subset:
In a “normal” "traditional" programming language, this would likely result in a “Bang” — some kind of runtime error or exception caused by writing to memory that does not belong to the allocated array. But not in LabVIEW.
That said, as a sidenote, this “luxury” behavior isn’t free and comes at a cost: bounds checking is performed implicitly inside these primitives. Every operation effectively includes checks like index >= 0 && index < array_length.
Why is my 25‑year anxiety finally — and slowly — starting to fade away?
Because I’ve begun learning another programming language, one where this problem is solved so elegantly with Option<T>.
I know, it sounds like advertising — so sorry about that..., but just take a look at this:
fn main() {
let v = vec![1.0, 2.0];
let index = 2;
match v.get(index) {
Some(&value) => println!("Value = {}", value),
None => println!("Index {} is out of bounds", index),
}
}
What happens here?
v.get(index) does not return a naked raw value. Instead, it returns an Option<&T> (in this case Option<&f64>), and two cases
If the index is valid → Some(&value)
If the index is invalid → None
No undefined behavior. No garbage values. No silent defaults. No NaN hacks. No guessing. Just explicit, predictable behavior — simple, elegant, and, from my humble point of view, genuinely beautiful.
By the way, another nice touch: there are first() and last() methods, so no need for v.get(0) or omit index input like in LabVIEW. These methods behave exactly like get() — they never panic, never fabricate defaults, and never hide errors.
A Note on Null.
In general using a “null” value to represent absence (not only for numbers, but also for references and handles as well) isn’t new. It was introduced by Sir Tony Hoare, who later called it his famous “billion-dollar mistake” at a QCon software conference in 2009 in London and apologized for inventing the null reference introduced more than sixty years ago:
Let’s continue and revisit a simple following "old school" C example with File I/O and NULL Check:
#include <ansi_c.h>
int main (int argc, char *argv[])
{
double arr[2] = {1.0, 2.0};
size_t count = sizeof(arr) / sizeof(arr[0]);
FILE *fp = fopen("arr.bin", "wb");
if (fp == NULL) return EXIT_FAILURE; // NULL check
size_t written = fwrite(arr, sizeof(arr[0]), count, fp);
if (written != count) {
fclose(fp);
return EXIT_FAILURE;
}
fclose(fp);
return 0;
}
In opposite to C, here I was a very happy with LabVIEW, loved the error cluster and dataflow chaining:
There is no silver bullet, but the new toy provides a similarly elegant mechanism using Result<T> and the ? operator, which is "key point":
use std::fs::File;
use std::io::{Write, Result};
fn main() -> Result<()> {
let vec = [1.0_f64, 2.0_f64];
File::create("vec.bin")?.write_all(
&vec.iter()
.flat_map(|x| x.to_ne_bytes())
.collect::<Vec<_>>()
)?;
Ok(())
}
Error handling with `?` is straightforward and works as follows:
This closely mirrors LabVIEW’s error-chain semantics — just expressed textually, without the clutter of repetitive NULL checks.
You might ask: where is file.close()? It isn’t needed. This programming language relies on RAII and hidden drop() destructor, so resources are released automatically when they go out of scope. And the scope can also be explicitly controlled with `{}` if needed.
No manual cleanup is required, no leaks, no forgotten closes — unlike in C (or even LabVIEW, where you still explicitly call Close File/Close Reference).
I’m truly enjoying every page of this language as I explore it step by step and discovering more and more... It reminds me of the excitement I felt reading Kernighan & Ritchie for the first time more than 30 years ago...
Andrey.
05-11-2026 08:56 AM
Interesting. I have to say, if I was to start learning a new language, Rust would be up there.
I am interested, though, as to where the checking for bounds is..... This is obviously not a "free" operation, there needs to be boundary checks, there needs to be some kind of bookkeeping in the background (similar to LabVIEW). The differentiation between value and None has to happen somewhere...The way I currently read it is the addition of None simply formalises on the data level a failure of any given boundary test.
Semantically (not really) like an "overflow bit" we can use on FXP? On the wire but handled "in the background"? So it's essentially a visibility boon? Is that correct?
05-11-2026 11:32 AM
@Intaris wrote:
Interesting. I have to say, if I was to start learning a new language, Rust would be up there.
I am interested, though, as to where the checking for bounds is..... This is obviously not a "free" operation, there needs to be boundary checks, there needs to be some kind of bookkeeping in the background (similar to LabVIEW). The differentiation between value and None has to happen somewhere...The way I currently read it is the addition of None simply formalises on the data level a failure of any given boundary test.
Semantically (not really) like an "overflow bit" we can use on FXP? On the wire but handled "in the background"? So it's essentially a visibility boon? Is that correct?
Great question — thank you. Semantically, it’s similar to an “overflow bit” — but not exactly. It’s not something hidden on the wire or handled silently in the background. And it’s more than just a visibility improvement.
What Rust does with Option<T> is make that condition explicit and part of the value itself, rather than attaching it as hidden metadata. Internally, v.get(index) does something roughly like this, similar to LabVIEW:
if index < v.len() {
Some(&v[index])
} else {
None
}
So yes, there is a runtime bounds check, and — just like in LabVIEW — it’s not “free”. The only key difference is how correctness is enforced. LabVIEW performs a bounds check and then silently returns a default value, so the error becomes implicit and hidden in the value — essentially: “Here’s a value — good luck figuring out if it’s valid.” Rust also performs a bounds check, but returns Some(value) or None, making the result explicit in the type system: “Here’s a result — you must explicitly handle whether it exists or valid”
But `Option<T>` is not just a status flag or hidden bit, the Option<T> is a real value with two explicit states, internally implemented as enum:
enum Option<T> {
Some(T),
None,
}
and because is part of the value and compiler forces me to handle it, that means if I have variable x obtained as let x = v.get(2), then I cannot use x as a plain value until I handle it with match or case.
So, conceptually here two overheads - first is a bounds check, and second, strictly required branching (Some/ None). So, the statement "None simply formalizes on the data level a failure of any given boundary test”, is true, but with an important addition - …and makes it impossible to ignore accidentally.
But Rust is very flexible, for example, most common replacement of the match is like this with "if let", this is "idiomatic" code:
fn main() {
let v = vec![1.0, 2.0];
let index = 2;
if let Some(&value) = v.get(index) {
println!("Value = {}", value);
} else {
println!("Index {} is out of bounds", index);
}
}
By the way, if we really want LabVIEW-like behavior (default `0.0`), we can do it explicitly, it is allowed:
fn main() {
let v = vec![1.0, 2.0];
let index = 2;
let value = v.get(index).copied().unwrap_or(0.0);
println!("Value = {}", value); // 0.0
}
But this reintroduces ambiguity — we can no longer distinguish between a real `0.0` and an out-of-bounds access. And out-of-bound is not ignored here, just handled differently.
Rust’s safety model goes far beyond array bounds. It also prevents common concurrency bugs. For example, this classical race condition
Similar to what can happen in LabVIEW is easy to write in C++:
#include <thread>
int counter = 0;
void increment() {
for (int i = 0; i < 1000; i++) {
counter++;
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Counter: " << counter << std::endl;
return 0;
}
This compiles — but the result is undefined due to a data race. But In Rust, the equivalent code does not compile at all:
use std::thread;
static mut counter:i32 = 0;
fn increment() {
for _ in 0..1000 {
counter += 1;
}
}
fn main() {
let t1 = thread::spawn(|| increment());
let t2 = thread::spawn(|| increment());
t1.join().unwrap();
t2.join().unwrap();
println!("Counter: {}", counter );
}
With error:
error[E0133]: use of mutable static is unsafe and requires unsafe block
--> src\main.rs:7:9
|
7 | counter += 1;
| ^^^^^^^ use of mutable static
|
= note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior
Rust requires unsafe here (or proper synchronization like a mutex), making the danger explicit.
From an overall performance point of view (in term of machine code efficiency), Rust is generally much faster than LabVIEW and only slightly slower than C (while still very close). In addition, Rust also allows embedding low-level intrinsics or even assembly when needed — but doing so will make the code unsafe.
05-11-2026 11:41 AM
Great explanation, thanks.
Every time I read something about Rust, it seems like just my cup of tea.
Maybe I WILL start learning it....
05-12-2026 03:33 AM
Obligatory post of this video. I saw this presentation and thought immediately that a lot of the syntax choices in Rust would fix a lot of the "invisible" errors people make with G.
https://labviewwiki.org/wiki/GDevCon-2/Rebar:_What_Rust_Can_Teach_G
I'm assuming anyone looking into Rust from a LabVIEW background is familiar?
05-25-2026 04:11 AM
Rust came in pretty late and for sure did a lot of things right. I'm not a particular fan of the syntax but have worked with enough different programming languages to not get hung up about it.
The people designing Rust for sure did think about it before getting started. And they had a lot of other programming languages to look at and analyze what tradeoffs they did, where and how.
The funny part in your post was the:
@Andrey_Dmitriev wrote:
This program in C invokes well known Undefined Behavior because it accesses an array element outside its bounds.
I personally always preferred LabVIEW's well known Defined Behavior for some reason, unless when I was trying to do (sometimes premature) optimization. 😁.
For some reason, knowing that may program won't generate some bogus value, or cause a GPE dialog, or cause my computer to burst out in flames, or play suddenly Swan Lake, felt much more comforting than the discomfort of getting a 'default default' value in these cases without a big flashy red warn sign.
Of course what many people also frequently forget is the fact that many of these new programming languages would be very hard to implement and get to run reliably on a computer system with a 30MHz CPU, 4MB of RAM and 100MB harddisk. LabVIEW fully did back in those days! It sure helped to have a 66MHz CPU and 8MB of RAM, but you could write LabVIEW programs and also run them on the first system too.
Unfortunately many of those fundamental choices made by necessity back then can't be easily changed after the fact.