qsharp-compiler: Default values for qubits and callables in uninitialized arrays cause runtime errors

Describe the bug

Q# array initializers allow creating an array with default values for the given type. For callable and qubit types, the default value is an invalid callable and an invalid qubit.

To Reproduce

let f = (new (Int -> Int)[1])[0];
let _ = f(7);

Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object. —> Example.Main on C:\Users\samarsha\Desktop\QSharpProject\Program.qs:line 8

let q = (new Qubit[1])[0];
X(q);

Unhandled exception. System.ArgumentNullException: Trying to perform a primitive operation on a null Qubit (Parameter ‘q1’) —> Microsoft.Quantum.Intrinsic.X on :line 0 at Example.Main on C:\Users\samarsha\Desktop\QSharpProject\Program.qs:line 8

Expected behavior

There are no reasonable default values for qubits and callables, so Q# should make it harder or impossible to create them. In general, not every type will have a reasonable default, so there should be some way of requiring that a default exists before creating an array. One way to do this is make array constructors explicitly take in a value with which to initialize each cell.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 26 (26 by maintainers)

Most upvoted comments

How about EmptyArray<'T> for a zero-length array and ConstantArray<'T>(size : Int, element : 'T) for bigger arrays. I think we still need ConstantArray to become intrinsic – perhaps “guaranteed” is better than “intrinsic” – as you said above, Chris.

ConstantArray already exists, so the change to make it intrinsic can be done without affecting the Q# API. EmptyArray should complement it nicely. We couldn’t write EmptyArray in pure Q# using ConstantArray, though, without Default<'T>, such that it would need to be intrinsic as well.

Yup.

We also need to figure out what to do with Default<'T>.

Sounds like deprecate-and-drop, so that we can close the loophole where Qubit can be effectively null? If we want nullability, explicit DUs and type-parameterized UDTs are the way to go, I think:

newtype Maybe<'T> = Some(Value : 'T) | None();

Dropping Default would be a lovely thing to do. I think everything is simple if we drop it and new at the same time.

// tired:
mutable emptyArray = new 'T[0];
// wired:
mutable emptyArray = EmptyArray<'T>();

How about EmptyArray<'T> for a zero-length array and ConstantArray<'T>(size : Int, element : 'T) for bigger arrays.

I think we still need ConstantArray to become intrinsic – perhaps “guaranteed” is better than “intrinsic” – as you said above, Chris.

ConstantArray already exists, so the change to make it intrinsic can be done without affecting the Q# API. EmptyArray should complement it nicely. We couldn’t write EmptyArray in pure Q# using ConstantArray, though, without Default<'T>, such that it would need to be intrinsic as well.

We also need to figure out what to do with Default<'T>.

Sounds like deprecate-and-drop, so that we can close the loophole where Qubit can be effectively null? If we want nullability, explicit DUs and type-parameterized UDTs are the way to go, I think:

newtype Maybe<'T> = Some(Value : 'T) | None();

I agree, down with new! I really like @cgranade’s EmptyArray suggestion.

I think modifying the “new” operator to take an initial value would be a good thing, and actually useful – it makes it easier to create, say, an array of PailiZs instead of an array of PauliIs. That way, we don’t have to require a default value for every type, should we ever want to have a type with no values… This would be a breaking change, though.

Odd idea: remove the new keyword, and make Microsoft.Quantum.Arrays.ConstantArray intrinsic.

Actually, I kind of like that…

I think modifying the “new” operator to take an initial value would be a good thing, and actually useful – it makes it easier to create, say, an array of PailiZs instead of an array of PauliIs. That way, we don’t have to require a default value for every type, should we ever want to have a type with no values… This would be a breaking change, though.

Odd idea: remove the new keyword, and make Microsoft.Quantum.Arrays.ConstantArray intrinsic.

I think modifying the “new” operator to take an initial value would be a good thing, and actually useful – it makes it easier to create, say, an array of PailiZs instead of an array of PauliIs. That way, we don’t have to require a default value for every type, should we ever want to have a type with no values… This would be a breaking change, though.

Items (1) and (2) are actually identical, of course:

Yes - the implementation of Default is how I discovered this bug. 😃

I think the notion of default values is incredibly useful in some contexts, but I agree it doesn’t make any sense for some types (e.g.: Qubit). That could probably be rectified by having a concept indicating that a type supports default:

In some contexts, yes, but I’m not sure that a universal default can be applied to a type without knowing that context. Array initializers think that the default Int is 0, but IndexOf would say that the default Int is -1. In other cases, there is no reasonable default and you should use None from Option<'T> instead. I feel like default-ness depends too much on context to be a universal function.

Thanks for the great examples @swernli! It’s good to show that null is so dangerous because it’s useful for control flow, people will be tempted to use it, and it can spread throughout a code base and libraries, encouraging more people to use null or defensively check for null.

We should fix this and provide the type-safe alternative: an option type (#406).

It turns out, null Result variables allow for some very odd uses. They work in comparisons, but are neither One nor Zero, effectively making Result a ternary value instead of binary:

let r = Default<Result>();
if (r == Zero) {
    Message("Zero");
}
elif (r == One) {
    Message("One");
}
else {
    Message("Two?!?");
}

That code will output “Two?!?” which is both hilarious and ripe for abuse.

Number 2 is especially dangerous, as Default allows you to create null qubits or null results pretty trivially:

    let q = Default<Qubit>();
    let r = Default<Result>();
    Message($"Values: [{q}] , [{r}]");

This code will output Values: [] , [], showing that both the variables are null.