Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce the modern Stdlib API; Fix re-exports (microsoft#1727)
This PR fixes all the rough edges around re-exports and re-exports with aliases, and specifically re-exporting namespaces. ## Summary of Changes * The package store has to be available to the HIR and FIR, because a re-export can refer to an item from another package. To resolve any `ItemId` where `package` is not `None`, we need to query the `PackageStore`. So that got plumbed all the way through. * An export can be its own `ItemKind`. It serves as a "reference" to another item in another package if it is present. This is used to ensure that items defined in package A will show up in package B's items, if it is re-exported from package B. * In both the resolver and the type checker, we follow the "chain of re-exports" until we find the original item that is being exported. There are multiple places that this happens. [Here is an example](https://github.com/microsoft/qsharp/pull/1727/files#diff-d98d007bbe0a3fce75f8d49278e0b566c9f8d91731f3e451296db14bebff001fR403). * Lots of tests. * The most nuanced thing is the re-creation of the namespace tree when adding an external package in the resolver. This was actually the bug that took me the longest to sort out. I'll break out of this list for the sake of formatting, as this one requires more explanation: ### The Nuanced Bit Namespace trees are not acyclic, and the same underlying node can show up in different places of the tree. For example, in the new standard library, `Microsoft.Quantum.Arrays` is accessible via both `Microsoft.Quantum.Arrays` _and_ `Std.Arrays`. When we add an external package to the local package, we need to recreate this structure. To accomplish this, I wrote an iterator over the namespace tree which returns all names for which each namespace can be referred to. For example, the above namespace would show up in the iterator as `[Microsoft.Quantum.Arrays, Std.Arrays]`. This allows the resolver to reconstruct the exact same tree structure and preserve the aliased/equality property of the two namespaces. And, this is all done without having to know the underlying `NamespaceId`s. This is important, because `NamespaceId`s start from 0 for each package compilation -- it'd be an issue if we had to rely on the IDs of one package to resolve namespaces in another package. ## Other Notes With re-exports working as designed, the entire stdlib diff for re-exporting all items under the new name `Std` is: ```qsharp namespace Std { operation TestFunc() : Unit { Message ("test func"); } export TestFunc, Microsoft.Quantum.Arrays, Microsoft.Quantum.Canon, Microsoft.Quantum.Convert, Microsoft.Quantum.Core, Microsoft.Quantum.Diagnostics, Microsoft.Quantum.Logical, Microsoft.Quantum.Intrinsic, Microsoft.Quantum.Math, Microsoft.Quantum.Measurement, Microsoft.Quantum.Random, Microsoft.Quantum.ResourceEstimation, Microsoft.Quantum.Unstable; } ``` When we support glob exports, in addition to glob imports, this will be even shorter: ``` // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. // This file re-exports the standard library under the name `Std`, which will be the preferred standard library API going forward. namespace Std { export Microsoft.Quantum.*; } ``` ---- ## Example Feel free to test the Bernstein Vaszirani example with the new API: ```qsharp import Std.Arrays.IndexRange; import Std.Convert.ResultArrayAsInt; import Std.Diagnostics.Fact; import Std.Math.BitSizeI; @entrypoint() operation Main() : Int[] { let nQubits = 10; let integers = [127, 238, 512]; mutable decodedIntegers = []; for integer in integers { let parityOperation = EncodeIntegerAsParityOperation(integer); let decodedBitString = BernsteinVazirani(parityOperation, nQubits); let decodedInteger = ResultArrayAsInt(decodedBitString); Fact( decodedInteger == integer, $"Decoded integer {decodedInteger}, but expected {integer}." ); Message($"Successfully decoded bit string as int: {decodedInteger}"); set decodedIntegers += [decodedInteger]; } return decodedIntegers; } operation BernsteinVazirani(Uf : ((Qubit[], Qubit) => Unit), n : Int) : Result[] { use queryRegister = Qubit[n]; use target = Qubit(); X(target); within { ApplyToEachA(H, queryRegister); } apply { H(target); Uf(queryRegister, target); } let resultArray = MResetEachZ(queryRegister); Reset(target); return resultArray; } operation ApplyParityOperation( bitStringAsInt : Int, xRegister : Qubit[], yQubit : Qubit ) : Unit { let requiredBits = BitSizeI(bitStringAsInt); let availableQubits = Length(xRegister); Fact( availableQubits >= requiredBits, $"Integer value {bitStringAsInt} requires {requiredBits} bits to be represented but the quantum register only has {availableQubits} qubits" ); for index in IndexRange(xRegister) { if ((bitStringAsInt &&& 2^index) != 0) { CNOT(xRegister[index], yQubit); } } } function EncodeIntegerAsParityOperation(bitStringAsInt : Int) : (Qubit[], Qubit) => Unit { return ApplyParityOperation(bitStringAsInt, _, _); } ``` ---- ## Description from microsoft#1736 Yes, that title is a mouthful. This is _quite_ the edge case identified by @ScottCarda-MS -- thank you for bug bashing! Re-exports are nuanced. They're basically pointers to `ItemId`s which point to another package. As a refresher, if an `ItemId` has a package id (`item_id.package.is_some()`), then it refers to an item outside of the current package, so it is thusly a reexport. If `item_id.package.is_none()`, then the item is local to the current package. Based on this preexisting notion, re-exports were supported by inserting `ItemKind::ExportedItem` for re-exports which point to another package. These are identified in `add_external_package` and inserted into the names of the user code for resolution. And if an item was local, then no `ExportedItem` is needed -- we just mark the item's `Visibility` as public. But what, what if that local item is exported with an alias? Then, we need some way to mark it as public _and_ tell the HIR that it has a different name. To handle this, we insert an `ExportedItem` if an item is local and the export has an alias. So, to recap, an `ItemKind::ExportedItem` is inserted if the item being exported is either from another package (a re-export) or from the local package, but aliased. Everything mentioned up until now is handled in microsoft#1727. This PR fixes a bug where a re-export of a local item which is introduced to the scope via an import alias loses its alias upon exporting. Wow, what a mouthful. Basically, in order to insert an `ItemKind::ExportedItem` for an export with an alias, we just checked if `export.alias.is_some()`. But, actually, you can also basically perform an aliased export like this: `import Foo as Bar; export Bar;`. In this case, there is indeed no export on the alias. But we do still need an `ItemKind::ExportedItem`, because we need to track that this same `ItemId` is accessible via both names -- the original declared name and the re-exported aliased name. --------- Co-authored-by: Stefan J. Wernli <[email protected]>
- Loading branch information