Skip to content

Commit

Permalink
Create symbols for gates in standard library (#185)
Browse files Browse the repository at this point in the history
This commit implements the following.  Upon encountering `include "stdgates.qasm"` symbols
are created for gates in the standard library according to the spec. No corresponding file
need exist. None is actually read or included. If there is such a file in the include
path, it is ignored.

Symbols carry a name and a type. The type `Gate` is parameterized by number of qubits and
number of classical parameters. So the symbols created contain all the information that a
declaration (but not definition) of the gate would carry. At the level of this
parser/analyzer, this allows the gates to be used (called) after the include statement and
allows type-checking to ensure that they are called with valid parameters (The latter is
only partly implemented at the time of this commit.)

If a name in the standard library is already bound, then a semantic error is recorded.
Since no real file is actually included, the location associated with the error is that of
the `include` statement.
  • Loading branch information
jlapeyre authored Mar 22, 2024
1 parent 4694478 commit 5200b3b
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 22 deletions.
23 changes: 22 additions & 1 deletion crates/oq3_semantics/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,26 @@ impl Context {
&self.symbol_table
}

// `SymbolTable::standard_library_gates()` returns a vector of
// all names that were already bound. We record a redeclaration error
// for each of these. The caller of the present method should pass
// the node corresponding to `include "stdgates.qasm"`. This is the
// best we can do since no real file has been included.
/// Define gates in the standard library.
pub fn standard_library_gates<T>(&mut self, node: &T)
where
T: AstNode,
{
self.symbol_table
.standard_library_gates()
.into_iter()
.map(|name| {
self.semantic_errors
.insert(RedeclarationError(name.to_string()), node);
})
.for_each(drop);
}

pub fn as_tuple(self) -> (asg::Program, SemanticErrorList, SymbolTable) {
(self.program, self.semantic_errors, self.symbol_table)
}
Expand Down Expand Up @@ -108,7 +128,8 @@ impl Context {
// let symbol_id_result = self.symbol_table.new_binding(name, typ, node.syntax());
let symbol_id_result = self.symbol_table.new_binding(name, typ);
if symbol_id_result.is_err() {
self.semantic_errors.insert(RedeclarationError, node);
self.semantic_errors
.insert(RedeclarationError(name.to_string()), node);
}
symbol_id_result
}
Expand Down
2 changes: 1 addition & 1 deletion crates/oq3_semantics/src/semantic_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::TextRange;
pub enum SemanticErrorKind {
UndefVarError,
UndefGateError,
RedeclarationError,
RedeclarationError(String),
ConstIntegerError, // need a better way to organize this kind of type error
IncompatibleTypesError,
MutateConstError,
Expand Down
42 changes: 41 additions & 1 deletion crates/oq3_semantics/src/symbols.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,46 @@ pub struct SymbolTable {
symbol_id_counter: SymbolId,
}

impl SymbolTable {
// This will be called if `include "stdgates.qasm"` is encountered. At present we don't have any include guard.
// FIXME: This function allocates a vector. The caller iterates over the vector.
// Would be nice to return the `FlatMap` instead. I tried doing this, but it was super compilcated.
// The compiler helps with which trait to use as the return type. But then tons of bugs occur within
// the body.
/// Define gates in standard library "as if" a file of definitions (or declarations) had been read.
pub(crate) fn standard_library_gates(&mut self) -> Vec<&str> {
let g1q0p = (
vec![
"x", "y", "z", "h", "s", "sdg", "t", "tdg", "sx", /* 2.0 */ "id",
],
[0, 1],
);
let g1q1p = (vec!["p", "rx", "ry", "rz", /* 2.0 */ "phase", "u1"], [1, 1]);
let g1q2p = (vec![/* 2.0 */ "u2"], [2, 1]);
let g1q3p = (vec![/* 2.0 */ "u3"], [3, 1]);
let g2q0p = (vec!["cx", "cy", "cz", "ch", "swap", /* 2.0 */ "CX"], [0, 2]);
let g2q1p = (vec!["cp", "crx", "cry", "crz", /* 2.0 */ "cphase"], [1, 2]);
let g2q4p = (vec!["cu"], [4, 2]);
let g3q0p = (vec!["ccx", "cswap"], [0, 3]);
let all_gates = vec![g1q0p, g1q1p, g1q2p, g1q3p, g2q0p, g2q1p, g2q4p, g3q0p];
// If `new_binding` returns `Err`, we push `name` onto a vector which will be
// used by the caller to record errors. Here flat_map and filter are used to
// select filter the names.
all_gates
.into_iter()
.flat_map(|(names, [n_cl, n_qu])| {
names
.into_iter()
.filter(|name| {
// The side effect of the test is important!
self.new_binding(name, &Type::Gate(n_cl, n_qu)).is_err()
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
}
}

#[allow(dead_code)]
impl SymbolTable {
/// Create a new `SymbolTable` and initialize with the global scope.
Expand All @@ -231,7 +271,7 @@ impl SymbolTable {
all_symbols: Vec::<Symbol>::new(),
};
symbol_table.enter_scope(ScopeType::Global);
// Define global, built-in constants
// Define global, built-in constants, and the single built-in gate
for const_name in ["pi", "π", "euler", "ℇ", "tau", "τ"] {
let _ =
symbol_table.new_binding(const_name, &Type::Float(Some(64), types::IsConst::True));
Expand Down
43 changes: 25 additions & 18 deletions crates/oq3_semantics/src/syntax_to_semantics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,25 +152,32 @@ pub fn syntax_to_semantic<T: SourceTrait>(
// It is probably possible to encapsulate the manipulations of (context, errors).
// But I have not made much of an attempt to do so.
synast::Stmt::Include(include) => {
// Get SourceFile object with syntax AST for the next included file.
let included_parsed_source = included_iter.next().unwrap();
// Empty list for possible semantic errors in the included file.
let mut errors_in_included =
SemanticErrorList::new(included_parsed_source.file_path().clone());
// The following path is likely never taken
if context.symbol_table().current_scope_type() != ScopeType::Global {
context.insert_error(IncludeNotInGlobalScopeError, &include);
let file: synast::FilePath = include.file().unwrap();
let file_path = file.to_string().unwrap();
if file_path == "stdgates.qasm" {
// We do not use a file for standard library, but rather create the symbols.
context.standard_library_gates(&include);
} else {
// Get SourceFile object with syntax AST for the next included file.
let included_parsed_source = included_iter.next().unwrap();
// Empty list for possible semantic errors in the included file.
let mut errors_in_included =
SemanticErrorList::new(included_parsed_source.file_path().clone());
// The following path is likely never taken
if context.symbol_table().current_scope_type() != ScopeType::Global {
context.insert_error(IncludeNotInGlobalScopeError, &include);
}
// Call this function recursively passing the new, empty, storage for errors.
// Note that `errors_in_included` will be swapped into `context` upon entering `syntax_to_semantic`.
(context, errors_in_included) =
syntax_to_semantic(included_parsed_source, context, errors_in_included);
// Just before exiting the previous call, `errors_in_included` and `errors` are swapped again in `context`.
// Push the newly-populated list of errors onto the list of included errors in `context`, which now
// holds `errors`, the list passed in the current call to this `syntax_to_semantic`. And `errors`
// corresponds to the source in which `include` was encountered.
context.push_included(errors_in_included);
// Return `None` because have evaluated (and removed)the `include` statement.
}
// Call this function recursively passing the new, empty, storage for errors.
// Note that `errors_in_included` will be swapped into `context` upon entering `syntax_to_semantic`.
(context, errors_in_included) =
syntax_to_semantic(included_parsed_source, context, errors_in_included);
// Just before exiting the previous call, `errors_in_included` and `errors` are swapped again in `context`.
// Push the newly-populated list of errors onto the list of included errors in `context`, which now
// holds `errors`, the list passed in the current call to this `syntax_to_semantic`. And `errors`
// corresponds to the source in which `include` was encountered.
context.push_included(errors_in_included);
// Return `None` because have evaluated (and removed)the `include` statement.
None
}

Expand Down
23 changes: 23 additions & 0 deletions crates/oq3_semantics/tests/from_string_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -664,3 +664,26 @@ def xcheck(qubit[4] d, qubit a) -> bit {
assert_eq!(errors.len(), 0);
assert_eq!(program.len(), 2);
}

#[test]
fn test_from_string_stdgates() {
let code = r##"
include "stdgates.qasm";
qubit q;
h q;
"##;
let (program, errors, _symbol_table) = parse_string(code);
assert_eq!(errors.len(), 0);
assert_eq!(program.len(), 2);
}

#[test]
fn test_from_string_stdgates_2() {
let code = r##"
gate h q {}
include "stdgates.qasm";
"##;
let (program, errors, _symbol_table) = parse_string(code);
assert_eq!(errors.len(), 1);
assert_eq!(program.len(), 1);
}
7 changes: 6 additions & 1 deletion crates/oq3_source_file/src/source_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,12 @@ pub(crate) fn parse_included_files<P: AsRef<Path>>(
synast::Stmt::Include(include) => {
let file: synast::FilePath = include.file().unwrap();
let file_path = file.to_string().unwrap();
Some(parse_source_file(file_path, search_path_list))
// stdgates.qasm will be handled "as if" it really existed.
if file_path == "stdgates.qasm" {
None
} else {
Some(parse_source_file(file_path, search_path_list))
}
}
_ => None,
})
Expand Down

0 comments on commit 5200b3b

Please sign in to comment.