diff --git a/samples/notebooks/openqasm_parsing.ipynb b/samples/notebooks/openqasm_parsing.ipynb index 364b95ddb1c..d6c4986e083 100644 --- a/samples/notebooks/openqasm_parsing.ipynb +++ b/samples/notebooks/openqasm_parsing.ipynb @@ -1,354 +1,526 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "6a8c4cee", - "metadata": {}, - "source": [ - "# Parsing and analyzing OpenQASM with the QDK\n", - "\n", - "The QDK exposes its OpenQASM front end to Python, so you can inspect a program\n", - "instead of only running it. There are two layers:\n", - "\n", - "- `qdk.openqasm.parser.parse` lexes and parses. It reports the structure of the\n", - " source as written, without resolving names or types.\n", - "- `qdk.openqasm.semantic.analyze` additionally resolves identifiers, infers\n", - " types, evaluates constants, and expands broadcast gate calls.\n", - "\n", - "Both return a result rather than raising, so a program with errors still gives\n", - "you a tree to work with. This API is in preview and may change between QDK\n", - "releases." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0f9b24a2", - "metadata": {}, - "outputs": [], - "source": [ - "from qdk.openqasm import parser, semantic\n", - "\n", - "SOURCE = \"\"\"OPENQASM 3.0;\n", - "include \"stdgates.inc\";\n", - "\n", - "const angle theta = pi / 4;\n", - "\n", - "qubit[2] q;\n", - "bit[2] c;\n", - "\n", - "h q[0];\n", - "ctrl @ x q[0], q[1];\n", - "rz(theta) q[1];\n", - "c = measure q;\n", - "\"\"\"\n", - "\n", - "result = parser.parse(SOURCE)\n", - "print(\"errors:\", result.has_errors)\n", - "print(\"version:\", result.program.version)\n", - "\n", - "for statement in result.program.statements:\n", - " print(type(statement).__name__)" - ] - }, - { - "cell_type": "markdown", - "id": "23471780", - "metadata": {}, - "source": [ - "## Navigating a tree\n", - "\n", - "Every node has named accessors for its own parts and a `children()` method for\n", - "generic traversal. There is no `kind` discriminant: dispatch with `isinstance`\n", - "or on `type(node).__name__`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b7c56a27", - "metadata": {}, - "outputs": [], - "source": [ - "gate = result.program.statements[6]\n", - "\n", - "print(type(gate).__name__)\n", - "print(\"name: \", gate.name)\n", - "print(\"modifiers: \", gate.modifiers)\n", - "print(\"qubits: \", gate.qubits)\n", - "print(\"children: \", [type(child).__name__ for child in gate.children()])" - ] - }, - { - "cell_type": "markdown", - "id": "97e0fcbf", - "metadata": {}, - "source": [ - "## Diagnostics and source positions\n", - "\n", - "Spans are half-open UTF-8 byte ranges over the whole parse, including any\n", - "resolved includes. Use the result's source map to turn a span into a\n", - "line and column. A diagnostic can also render itself with the offending\n", - "source inline." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b6c6283e", - "metadata": {}, - "outputs": [], - "source": [ - "broken = parser.parse(\"OPENQASM 3.0;\\nqubit[2] q\\nh q[0];\\n\")\n", - "source_map = broken.document.source_map\n", - "\n", - "for diagnostic in broken.diagnostics:\n", - " where = source_map.range_from_span(diagnostic.labels[0].span).start\n", - " print(f\"line {where.line}, column {where.column}: {diagnostic.message}\")\n", - "\n", - "print()\n", - "print(broken.diagnostics[0].render())" - ] - }, - { - "cell_type": "markdown", - "id": "d1e688da", - "metadata": {}, - "source": [ - "## Semantic analysis\n", - "\n", - "`analyze` returns a different tree. Includes are resolved away, `qubit[2] q`\n", - "becomes a `QubitArrayDeclaration`, and each expression carries a resolved type\n", - "and, where the value is known at compile time, a constant value.\n", - "\n", - "Resolved types are nodes too, so branch on them with `isinstance` rather than\n", - "parsing a type name." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ef8661d2", - "metadata": {}, - "outputs": [], - "source": [ - "analysis = semantic.analyze(SOURCE)\n", - "print(\"errors:\", analysis.has_errors)\n", - "\n", - "for statement in analysis.program.statements:\n", - " print(type(statement).__name__)\n", - "\n", - "declaration = analysis.program.statements[0]\n", - "print()\n", - "print(\"declared type:\", type(declaration.type).__name__)\n", - "print(\"is an angle: \", isinstance(declaration.type, semantic.AngleType))\n", - "print(\"folded value: \", declaration.init_expr.const_value.radians)" - ] - }, - { - "cell_type": "markdown", - "id": "f7110b83", - "metadata": {}, - "source": [ - "## The symbol table\n", - "\n", - "Analysis also returns the resolved symbols. The table includes everything the\n", - "program can name, so it holds the standard gates pulled in by `stdgates.inc`\n", - "alongside the program's own declarations." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c2cceb7e", - "metadata": {}, - "outputs": [], - "source": [ - "declared = {\"theta\", \"q\", \"c\"}\n", - "\n", - "for symbol in analysis.symbols:\n", - " if symbol.name in declared:\n", - " print(f\"{symbol.name}: {type(symbol.ty).__name__} ({symbol.ty.name})\")" - ] - }, - { - "cell_type": "markdown", - "id": "0bc63dc9", - "metadata": {}, - "source": [ - "## Walking a tree with a visitor\n", - "\n", - "`QASMVisitor` walks either layer. Define `visit_` for the nodes you\n", - "care about and call `generic_visit` to keep descending. Note that broadcast gate\n", - "calls are expanded by analysis: `h q` over a two-qubit register is one node in\n", - "the syntax tree and two in the semantic tree." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4f8110e7", - "metadata": {}, - "outputs": [], - "source": [ - "from qdk.openqasm import QASMVisitor\n", - "\n", - "\n", - "class GateCounter(QASMVisitor):\n", - " def __init__(self):\n", - " self.counts = {}\n", - "\n", - " def visit_QuantumGate(self, node):\n", - " # The semantic layer resolves the gate name to a string; the syntax\n", - " # layer reports the `Identifier` node as written.\n", - " name = node.name if isinstance(node.name, str) else node.name.name\n", - " self.counts[name] = self.counts.get(name, 0) + 1\n", - " self.generic_visit(node)\n", - "\n", - "\n", - "broadcast = 'OPENQASM 3.0; include \"stdgates.inc\"; qubit[2] q; h q;'\n", - "\n", - "syntactic = GateCounter()\n", - "syntactic.visit(parser.parse(broadcast).program)\n", - "print(\"syntax: \", syntactic.counts)\n", - "\n", - "analyzed = GateCounter()\n", - "analyzed.visit(semantic.analyze(broadcast).program)\n", - "print(\"semantic: \", analyzed.counts)" - ] - }, - { - "cell_type": "markdown", - "id": "7aa0ca1c", - "metadata": {}, - "source": [ - "A visitor can also thread a context object through the walk. Pass it to\n", - "`visit`, and every callback that declares a second parameter receives it." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5ad82299", - "metadata": {}, - "outputs": [], - "source": [ - "class QubitCollector(QASMVisitor):\n", - " def visit_QubitArrayDeclaration(self, node, context):\n", - " context.append((node.name, node.size.const_value))\n", - " self.generic_visit(node, context)\n", - "\n", - "\n", - "registers = []\n", - "QubitCollector().visit(analysis.program, registers)\n", - "print(registers)" - ] - }, - { - "cell_type": "markdown", - "id": "7a0d8d21", - "metadata": {}, - "source": [ - "## Telling the two layers apart\n", - "\n", - "Most class names exist in both layers, so a value named `Program` or `IntType`\n", - "does not say which tree produced it. `SyntaxNode` and `SemanticNode` answer\n", - "that at an API boundary." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b704853a", - "metadata": {}, - "outputs": [], - "source": [ - "print(\"same class in both layers: \", parser.Program is semantic.Program)\n", - "print(\"parsed is a SyntaxNode: \", isinstance(result.program, parser.SyntaxNode))\n", - "print(\"analyzed is a SemanticNode: \", isinstance(analysis.program, semantic.SemanticNode))" - ] - }, - { - "cell_type": "markdown", - "id": "3555efda", - "metadata": {}, - "source": [ - "## Resolving includes\n", - "\n", - "`stdgates.inc`, `qelib1.inc`, and the QDK extension `qdk.inc` are built in.\n", - "Any other include is resolved through the `includes` argument, which takes a\n", - "mapping or a callback over logical `/`-separated names. Nothing falls back to\n", - "the filesystem or the network, and the resolver is not retained after the call." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b6d538fa", - "metadata": {}, - "outputs": [], - "source": [ - "with_include = semantic.analyze(\n", - " 'OPENQASM 3.0;\\ninclude \"mylib.inc\";\\nqubit q;\\nmygate q;\\n',\n", - " includes={\"mylib.inc\": \"gate mygate a { U(0, 0, 0) a; }\"},\n", - ")\n", - "\n", - "print(\"errors:\", with_include.has_errors)\n", - "print([type(s).__name__ for s in with_include.program.statements])\n", - "\n", - "unresolved = parser.parse('OPENQASM 3.0;\\ninclude \"missing.inc\";\\n')\n", - "print(\"missing include:\", unresolved.diagnostics[0].message)" - ] - }, - { - "cell_type": "markdown", - "id": "a6c071df", - "metadata": {}, - "source": [ - "## Canonical source and structural equality\n", - "\n", - "`dumps` re-emits a syntactic program as canonical OpenQASM. It does not preserve\n", - "comments or original spelling, and it accepts only a syntax `Program`.\n", - "\n", - "Nodes compare and hash structurally, so two parses of the same source are equal\n", - "and usable as `dict` keys or `set` members. Source position does not participate,\n", - "which means the same construct at two different offsets also compares equal." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3cd49737", - "metadata": {}, - "outputs": [], - "source": [ - "print(parser.dumps(result.program))\n", - "\n", - "print(\"equal across parses:\", parser.parse(SOURCE).program == result.program)\n", - "\n", - "shifted = parser.parse(\"// a leading comment\\n\" + SOURCE)\n", - "print(\"equal after a shift:\", shifted.program == result.program)" - ] - }, - { - "cell_type": "markdown", - "id": "5d790a5c", - "metadata": {}, - "source": [ - "## Where to go next\n", - "\n", - "- `help(qdk.openqasm.parser)` and `help(qdk.openqasm.semantic)` document every\n", - " node class and accessor.\n", - "- The [OpenQASM interop notebook](./openqasm.ipynb) covers running, compiling,\n", - " and estimating OpenQASM programs instead of inspecting them." - ] - } - ], - "metadata": { - "language_info": { - "name": "python" - } - }, - "nbformat": 4, - "nbformat_minor": 5 + "cells": [ + { + "cell_type": "markdown", + "id": "6a8c4cee", + "metadata": { + "id": "6a8c4cee", + "language": "markdown" + }, + "source": [ + "# Build an OpenQASM program inspector\n", + "\n", + "The QDK's OpenQASM front end exposes two read-only views of a program. Parsing preserves the syntax as written, while semantic analysis resolves names and types, evaluates constants, and expands broadcast gate calls.\n", + "\n", + "In this notebook, one `SOURCE` value moves through both views to answer five practical questions:\n", + "\n", + "1. Did the program parse, and where are its syntax elements and diagnostics?\n", + "2. Which declarations, constants, and qubit registers belong to the program?\n", + "3. How does the analyzed program differ from what was written?\n", + "4. How can those facts become one reusable inspector?\n", + "5. What does the inspector report for valid and invalid programs?\n", + "\n", + "These APIs are in preview and may change between QDK releases." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f9b24a2", + "metadata": { + "id": "0f9b24a2", + "language": "python" + }, + "outputs": [], + "source": [ + "from typing import Any\n", + "from qdk.openqasm import QASMVisitor, parser, semantic\n", + "SOURCE = \"\"\"OPENQASM 3.0;\n", + "include \"stdgates.inc\";\n", + "const angle theta = pi / 4;\n", + "qubit[2] q;\n", + "bit[2] c;\n", + "h q;\n", + "ctrl @ x q[0], q[1];\n", + "rz(theta) q[1];\n", + "c = measure q;\n", + "\"\"\"\n", + "\n", + "result = parser.parse(SOURCE)\n", + "print(\"version:\", result.program.version)\n", + "print(\"parse errors:\", result.has_errors)\n", + "print(\"diagnostics:\", len(result.diagnostics))" + ] + }, + { + "cell_type": "markdown", + "id": "23471780", + "metadata": { + "id": "23471780", + "language": "markdown" + }, + "source": [ + "## 1. What did the parser read?\n", + "\n", + "The syntax tree preserves each gate call exactly as it appears in `SOURCE`. A visitor avoids assumptions about statement order, while the parse result's source map converts each node's byte span into a one-based line and column for readers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b7c56a27", + "metadata": { + "id": "b7c56a27", + "language": "python" + }, + "outputs": [], + "source": [ + "class SyntaxGateCollector(QASMVisitor):\n", + " def __init__(self):\n", + " self.gates = []\n", + "\n", + " def visit_QuantumGate(self, node, source_map):\n", + " source_range = source_map.range_from_span(node.span)\n", + " self.gates.append(\n", + " {\n", + " \"name\": node.name.name,\n", + " \"line\": source_range.start.line + 1,\n", + " \"column\": source_range.start.column + 1,\n", + " }\n", + " )\n", + " self.generic_visit(node, source_map)\n", + "\n", + "\n", + "syntax_gates = SyntaxGateCollector()\n", + "syntax_gates.visit(result.program, result.document.source_map)\n", + "syntax_gates.gates" + ] + }, + { + "cell_type": "markdown", + "id": "97e0fcbf", + "metadata": { + "id": "97e0fcbf", + "language": "markdown" + }, + "source": [ + "### See parser recovery in action\n", + "\n", + "Parser calls return diagnostics instead of raising, so tools can inspect the recovered tree and show the problem in context. This variant is derived from `SOURCE` by removing one closing bracket. `Diagnostic.render` produces a source-annotated explanation with the offending token underlined." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b6c6283e", + "metadata": { + "id": "b6c6283e", + "language": "python" + }, + "outputs": [], + "source": [ + "def diagnostics_to_plain(result: Any) -> list[dict[str, Any]]:\n", + " diagnostics = []\n", + " source_map = result.document.source_map\n", + "\n", + " for diagnostic in result.diagnostics:\n", + " location = {\"source\": None, \"line\": None, \"column\": None}\n", + " if diagnostic.labels:\n", + " source_range = source_map.range_from_span(diagnostic.labels[0].span)\n", + " location = {\n", + " \"source\": source_map.get(source_range.source_id).path,\n", + " \"line\": source_range.start.line + 1,\n", + " \"column\": source_range.start.column + 1,\n", + " }\n", + "\n", + " diagnostics.append({\"message\": diagnostic.message, **location})\n", + "\n", + " return diagnostics\n", + "\n", + "\n", + "SYNTAX_ERROR_SOURCE = SOURCE.replace(\"q[1];\", \"q[1;\", 1)\n", + "syntax_error_result = parser.parse(SYNTAX_ERROR_SOURCE)\n", + "\n", + "assert syntax_error_result.has_errors\n", + "print(syntax_error_result.diagnostics[0].render(color=True))" + ] + }, + { + "cell_type": "markdown", + "id": "d1e688da", + "metadata": { + "id": "d1e688da", + "language": "markdown" + }, + "source": [ + "## 2. What does semantic analysis add?\n", + "\n", + "Semantic analysis resolves the declarations that the program can name. The symbol table also contains declarations from `stdgates.inc`, so the inspector uses each symbol's span and the analysis source map to retain only declarations owned by the entry source.\n", + "\n", + "A second variant replaces `theta` with an undefined name. It is valid syntax, but semantic analysis pinpoints the unresolved symbol. This makes the parser-versus-analyzer boundary concrete." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef8661d2", + "metadata": { + "id": "ef8661d2", + "language": "python" + }, + "outputs": [], + "source": [ + "analysis = semantic.analyze(SOURCE)\n", + "analysis_source_map = analysis.document.source_map\n", + "entry_source_id = analysis_source_map.entry.id\n", + "\n", + "entry_symbols = [\n", + " symbol\n", + " for symbol in analysis.symbols\n", + " if symbol.span.lo != symbol.span.hi\n", + " and analysis_source_map.range_from_span(symbol.span).source_id == entry_source_id\n", + "]\n", + "\n", + "declarations = [\n", + " {\n", + " \"name\": symbol.name,\n", + " \"type\": type(symbol.ty).__name__,\n", + " \"type_name\": symbol.ty.name,\n", + " }\n", + " for symbol in entry_symbols\n", + "]\n", + "\n", + "SEMANTIC_ERROR_SOURCE = SOURCE.replace(\"rz(theta)\", \"rz(missing_angle)\")\n", + "semantic_error_parse = parser.parse(SEMANTIC_ERROR_SOURCE)\n", + "semantic_error_analysis = semantic.analyze(SEMANTIC_ERROR_SOURCE)\n", + "\n", + "assert not semantic_error_parse.has_errors\n", + "assert semantic_error_analysis.has_errors\n", + "print(semantic_error_analysis.diagnostics[0].render(color=True))\n", + "\n", + "declarations" + ] + }, + { + "cell_type": "markdown", + "id": "f7110b83", + "metadata": { + "id": "f7110b83", + "language": "markdown" + }, + "source": [ + "### Constants and qubit registers are structured values\n", + "\n", + "Resolved type classes expose properties such as a qubit array's `size`. Constants are Python values; angles additionally expose `radians`. Reading those properties directly keeps the report independent of formatted type or tree dumps." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c2cceb7e", + "metadata": { + "id": "c2cceb7e", + "language": "python" + }, + "outputs": [], + "source": [ + "def constant_to_plain(value):\n", + " if isinstance(value, semantic.Angle):\n", + " return value.radians\n", + " if isinstance(value, semantic.Duration):\n", + " return {\"value\": value.value, \"unit\": str(value.unit)}\n", + " if isinstance(value, complex):\n", + " return {\"real\": value.real, \"imag\": value.imag}\n", + " if isinstance(value, (bool, int, float, str)):\n", + " return value\n", + " return None\n", + "\n", + "\n", + "constants = {\n", + " symbol.name: constant_to_plain(symbol.const_value)\n", + " for symbol in entry_symbols\n", + " if symbol.const_value is not None\n", + "}\n", + "\n", + "qubit_registers = [\n", + " {\n", + " \"name\": symbol.name,\n", + " \"size\": symbol.ty.size\n", + " if isinstance(symbol.ty, semantic.QubitArrayType)\n", + " else 1,\n", + " }\n", + " for symbol in entry_symbols\n", + " if isinstance(symbol.ty, (semantic.QubitType, semantic.QubitArrayType))\n", + "]\n", + "\n", + "print(\"constants:\", constants)\n", + "print(\"qubit registers:\", qubit_registers)" + ] + }, + { + "cell_type": "markdown", + "id": "0bc63dc9", + "metadata": { + "id": "0bc63dc9", + "language": "markdown" + }, + "source": [ + "## 3. What changed between syntax and semantics?\n", + "\n", + "`QASMVisitor` walks either tree layer. The syntax tree contains the single `h q` call that was written. Semantic analysis resolves `q` as a two-qubit register and expands that broadcast into one `h` operation per qubit. A shared counter makes the difference visible without changing the source." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4f8110e7", + "metadata": { + "id": "4f8110e7", + "language": "python" + }, + "outputs": [], + "source": [ + "class GateCounter(QASMVisitor):\n", + " def __init__(self):\n", + " self.counts = {}\n", + "\n", + " def visit_QuantumGate(self, node: Any) -> None:\n", + " raw_name = node.name\n", + " name = raw_name if isinstance(raw_name, str) else getattr(raw_name, \"name\", None)\n", + " if name is not None:\n", + " self.counts[name] = self.counts.get(name, 0) + 1\n", + " self.generic_visit(node)\n", + "\n", + "\n", + "def gate_counts(program: Any) -> dict[str, int]:\n", + " counter = GateCounter()\n", + " counter.visit(program)\n", + " return counter.counts\n", + "\n", + "\n", + "syntax_gate_counts = gate_counts(result.program)\n", + "semantic_gate_counts = gate_counts(analysis.program)" + ] + }, + { + "cell_type": "markdown", + "id": "7aa0ca1c", + "metadata": { + "id": "7aa0ca1c", + "language": "markdown" + }, + "source": [ + "The two counts answer different questions. Syntax counts describe the source text. Semantic counts describe resolved operations after transformations such as broadcast expansion. Neither view is more correct; the inspector keeps both so callers can choose the level they need." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ad82299", + "metadata": { + "id": "5ad82299", + "language": "python" + }, + "outputs": [], + "source": [ + "print(\"syntax gates: \", syntax_gate_counts)\n", + "print(\"semantic gates:\", semantic_gate_counts)\n", + "\n", + "assert syntax_gate_counts[\"h\"] == 1\n", + "assert semantic_gate_counts[\"h\"] == 2" + ] + }, + { + "cell_type": "markdown", + "id": "7a0d8d21", + "metadata": { + "id": "7a0d8d21", + "language": "markdown" + }, + "source": [ + "## 4. Assemble the inspector\n", + "\n", + "The final callable repeats the same stages behind one small interface. Its dictionary shape stays fixed even when parsing or analysis reports errors. Recovered or unavailable facts are skipped, while diagnostics keep `None` for positions that have no source label." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b704853a", + "metadata": { + "id": "b704853a", + "language": "python" + }, + "outputs": [], + "source": [ + "def entry_symbols_for(analysis: Any) -> list[Any]:\n", + " source_map = analysis.document.source_map\n", + " entry_source_id = source_map.entry.id\n", + " symbols = []\n", + "\n", + " for symbol in analysis.symbols:\n", + " if symbol.span.lo == symbol.span.hi:\n", + " continue\n", + " try:\n", + " source_id = source_map.range_from_span(symbol.span).source_id\n", + " except ValueError:\n", + " continue\n", + " if source_id == entry_source_id:\n", + " symbols.append(symbol)\n", + "\n", + " return symbols" + ] + }, + { + "cell_type": "markdown", + "id": "3555efda", + "metadata": { + "id": "3555efda", + "language": "markdown" + }, + "source": [ + "The report keeps parser and analyzer error states separate because syntax can recover successfully while semantic checks still find unresolved names or type errors. Analyzer diagnostics can repeat parser diagnostics, so the callable deduplicates equal plain-data entries before returning them." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b6d538fa", + "metadata": { + "id": "b6d538fa", + "language": "python" + }, + "outputs": [], + "source": [ + "def inspect_openqasm(source: str) -> dict[str, Any]:\n", + " parsed = parser.parse(source)\n", + " analyzed = semantic.analyze(source)\n", + " symbols = entry_symbols_for(analyzed)\n", + "\n", + " declarations = [\n", + " {\n", + " \"name\": symbol.name,\n", + " \"type\": type(symbol.ty).__name__,\n", + " \"type_name\": symbol.ty.name,\n", + " }\n", + " for symbol in symbols\n", + " ]\n", + "\n", + " constants = {}\n", + " for symbol in symbols:\n", + " if symbol.const_value is None:\n", + " continue\n", + " value = constant_to_plain(symbol.const_value)\n", + " if value is not None:\n", + " constants[symbol.name] = value\n", + "\n", + " qubit_registers = [\n", + " {\n", + " \"name\": symbol.name,\n", + " \"size\": symbol.ty.size\n", + " if isinstance(symbol.ty, semantic.QubitArrayType)\n", + " else 1,\n", + " }\n", + " for symbol in symbols\n", + " if isinstance(symbol.ty, (semantic.QubitType, semantic.QubitArrayType))\n", + " ]\n", + "\n", + " diagnostics = diagnostics_to_plain(parsed)\n", + " for diagnostic in diagnostics_to_plain(analyzed):\n", + " if diagnostic not in diagnostics:\n", + " diagnostics.append(diagnostic)\n", + "\n", + " return {\n", + " \"has_parse_errors\": parsed.has_errors,\n", + " \"has_analysis_errors\": analyzed.has_errors,\n", + " \"diagnostics\": diagnostics,\n", + " \"declarations\": declarations,\n", + " \"constants\": constants,\n", + " \"qubit_registers\": qubit_registers,\n", + " \"syntax_gate_counts\": gate_counts(parsed.program),\n", + " \"semantic_gate_counts\": gate_counts(analyzed.program),\n", + " }" + ] + }, + { + "cell_type": "markdown", + "id": "a6c071df", + "metadata": { + "id": "a6c071df", + "language": "markdown" + }, + "source": [ + "## 5. Compare all three outcomes\n", + "\n", + "The final inspector receives the valid program and both variants derived from it. The compact error matrix shows why separate parser and analyzer status fields matter, while the earlier rendered diagnostics provide the detailed explanation a developer would see." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3cd49737", + "metadata": { + "id": "3cd49737", + "language": "python" + }, + "outputs": [], + "source": [ + "from pprint import pprint\n", + "\n", + "reports = {\n", + " \"valid\": inspect_openqasm(SOURCE),\n", + " \"syntax_error\": inspect_openqasm(SYNTAX_ERROR_SOURCE),\n", + " \"semantic_error\": inspect_openqasm(SEMANTIC_ERROR_SOURCE),\n", + "}\n", + "\n", + "assert not reports[\"valid\"][\"has_parse_errors\"]\n", + "assert not reports[\"valid\"][\"has_analysis_errors\"]\n", + "assert {item[\"name\"] for item in reports[\"valid\"][\"declarations\"]} == {\"theta\", \"q\", \"c\"}\n", + "assert reports[\"valid\"][\"syntax_gate_counts\"][\"h\"] == 1\n", + "assert reports[\"valid\"][\"semantic_gate_counts\"][\"h\"] == 2\n", + "assert reports[\"syntax_error\"][\"has_parse_errors\"]\n", + "assert not reports[\"semantic_error\"][\"has_parse_errors\"]\n", + "assert reports[\"semantic_error\"][\"has_analysis_errors\"]\n", + "\n", + "error_matrix = {\n", + " name: {\n", + " \"parse_errors\": report[\"has_parse_errors\"],\n", + " \"analysis_errors\": report[\"has_analysis_errors\"],\n", + " \"diagnostics\": len(report[\"diagnostics\"]),\n", + " }\n", + " for name, report in reports.items()\n", + "}\n", + "\n", + "pprint(\n", + " {\n", + " \"error_matrix\": error_matrix,\n", + " \"valid_program\": reports[\"valid\"],\n", + " },\n", + " indent=4,\n", + " sort_dicts=False,\n", + " width=1,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "5d790a5c", + "metadata": { + "id": "5d790a5c", + "language": "markdown" + }, + "source": [ + "## Where to go next\n", + "\n", + "The inspector is intentionally read-only. Use `help(qdk.openqasm.parser)` and `help(qdk.openqasm.semantic)` to explore custom include resolution, canonical syntax serialization, and additional node types. The [OpenQASM interop notebook](./openqasm.ipynb) covers running, compiling, and estimating programs." + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 }