Skip to content

Introduce QdkContext API for isolated interpreter sessions#3029

Draft
minestarks wants to merge 1 commit intomainfrom
minestarks/qdk-context-api
Draft

Introduce QdkContext API for isolated interpreter sessions#3029
minestarks wants to merge 1 commit intomainfrom
minestarks/qdk-context-api

Conversation

@minestarks
Copy link
Copy Markdown
Member

@minestarks minestarks commented Mar 18, 2026

Closes #2998

Summary

This PR introduces the QdkContext class, which provides isolated Q# interpreter contexts with their own configuration, compiled code, and state. This directly addresses the architectural problem described in #2998: the qsharp package previously stored a single global interpreter, making it impossible for two libraries (or a library and end-user code) to coexist without silently clobbering each other's state.

See #2998 (comment) for full context.

What changed

New public API

Function / Class Purpose
QdkContext An isolated interpreter context with .eval(), .run(), .compile(), .circuit(), .estimate(), .logical_counts(), .set_quantum_seed(), .set_classical_seed(), .dump_machine(), .dump_circuit(), .import_openqasm(), and a .config property.
qsharp.new_context(...) Creates a new isolated QdkContext.
qsharp.get_context() Returns the current global context (lazily initialized).
qsharp.context_of(callable) Returns the QdkContext that compiled a given callable.

Behavioral changes

  • init() now returns QdkContext instead of Config. The context proxies __repr__ and _repr_mimebundle_ from its config, so Jupyter notebook display is unchanged.
  • Callables are bound to their context. Each callable carries a _qdk_get_context attribute. Passing a callable to a different context's method (e.g., ctx_b.run(ctx_a.code.Foo)) raises QSharpError with a clear message.
  • Stale callable protection. After init() is called, callables from the prior context raise QSharpError ("disposed") when invoked.
  • Module-level functions are unchanged. qsharp.eval(), qsharp.run(), etc. delegate to the global default context exactly as before.

Add QdkContext class with instance methods (.eval(), .run(), .compile(),
.circuit(), .estimate(), .logical_counts(), etc.) that mirror module-level
functions. Module-level functions delegate to a global default context.

New public API:
- qsharp.new_context(...) creates an isolated context
- qsharp.get_context() returns the global context (lazy init)
- qsharp.context_of(callable) returns the context that compiled it
- init() now returns QdkContext (backward-compatible)

Cross-context safety: passing a callable from one context to another's
method raises QSharpError. Stale callables (from a prior init) raise
QSharpError when invoked.

Includes 20 test cases covering isolation, cross-context validation,
stale callable detection, backward compatibility, and config access.
Comment on lines +1211 to +1229
result = ctx.eval("1 + 2")
assert result == 3


def test_context_isolation() -> None:
ctx1 = qsharp.new_context()
ctx2 = qsharp.new_context()
ctx1.eval("function Foo() : Int { 42 }")
result1 = ctx1.eval("Foo()")
assert result1 == 42
# ctx2 should not have Foo defined
with pytest.raises(Exception):
ctx2.eval("Foo()")


def test_context_run() -> None:
ctx = qsharp.new_context()
ctx.eval('operation Foo() : Result { Message("hi"); Zero }')
results = ctx.run("Foo()", 3)

Check notice

Code scanning / devskim

If untrusted data (data from HTTP requests, user submitted files, etc.) is included in an eval statement it can allow an attacker to inject their own code. Note test

Review eval for untrusted data
Comment on lines +1235 to +1240
result = qsharp.eval("1 + 1")
assert result == 2


def test_init_returns_context() -> None:
ctx = qsharp.init()

Check notice

Code scanning / devskim

If untrusted data (data from HTTP requests, user submitted files, etc.) is included in an eval statement it can allow an attacker to inject their own code. Note test

Review eval for untrusted data
Comment on lines +1243 to +1252
result = ctx.eval("3 + 4")
assert result == 7
# Module-level eval should use the same context
result2 = qsharp.eval("3 + 4")
assert result2 == 7


def test_context_callable_has_interpreter_ref() -> None:
"""Callables created via eval carry a _qdk_get_interpreter attribute."""
ctx = qsharp.new_context()

Check notice

Code scanning / devskim

If untrusted data (data from HTTP requests, user submitted files, etc.) is included in an eval statement it can allow an attacker to inject their own code. Note test

Review eval for untrusted data
Comment on lines +1298 to +1312
ctx.eval("function Hello() : Int { 1 }")
fn = ctx.code.Hello
assert qsharp.context_of(fn) is ctx


def test_context_of_global_callable() -> None:
"""context_of() works for callables in the global context."""
ctx = qsharp.init()
qsharp.eval("function Hi() : Int { 2 }")
fn = qsharp.code.Hi
assert qsharp.context_of(fn) is ctx


def test_context_of_rejects_non_callable() -> None:
"""context_of() raises TypeError for non-QDK objects."""

Check notice

Code scanning / devskim

If untrusted data (data from HTTP requests, user submitted files, etc.) is included in an eval statement it can allow an attacker to inject their own code. Note test

Review eval for untrusted data
"""Passing a callable from one context to another's run() raises."""
ctx_a = qsharp.new_context()
ctx_b = qsharp.new_context()
ctx_a.eval("operation Foo() : Result { use q = Qubit(); M(q) }")

Check notice

Code scanning / devskim

If untrusted data (data from HTTP requests, user submitted files, etc.) is included in an eval statement it can allow an attacker to inject their own code. Note test

Review eval for untrusted data
"""Passing a callable from one context to another's compile() raises."""
ctx_a = qsharp.new_context(target_profile=qsharp.TargetProfile.Base)
ctx_b = qsharp.new_context(target_profile=qsharp.TargetProfile.Base)
ctx_a.eval("operation Bar() : Result { use q = Qubit(); M(q) }")

Check notice

Code scanning / devskim

If untrusted data (data from HTTP requests, user submitted files, etc.) is included in an eval statement it can allow an attacker to inject their own code. Note test

Review eval for untrusted data
"""Passing a callable from one context to another's circuit() raises."""
ctx_a = qsharp.new_context()
ctx_b = qsharp.new_context()
ctx_a.eval("operation Baz() : Unit { use q = Qubit(); H(q); }")

Check notice

Code scanning / devskim

If untrusted data (data from HTTP requests, user submitted files, etc.) is included in an eval statement it can allow an attacker to inject their own code. Note test

Review eval for untrusted data
"""Passing a callable from one context to another's estimate() raises."""
ctx_a = qsharp.new_context()
ctx_b = qsharp.new_context()
ctx_a.eval("operation Qux() : Unit { use q = Qubit(); H(q); }")

Check notice

Code scanning / devskim

If untrusted data (data from HTTP requests, user submitted files, etc.) is included in an eval statement it can allow an attacker to inject their own code. Note test

Review eval for untrusted data
"""Passing a callable from one context to another's logical_counts() raises."""
ctx_a = qsharp.new_context()
ctx_b = qsharp.new_context()
ctx_a.eval("operation Corge() : Unit { use q = Qubit(); H(q); }")

Check notice

Code scanning / devskim

If untrusted data (data from HTTP requests, user submitted files, etc.) is included in an eval statement it can allow an attacker to inject their own code. Note test

Review eval for untrusted data
Comment on lines +1370 to +1373
qsharp.eval("function Stale() : Int { 99 }")
old_fn = qsharp.code.Stale
# Reinitialize — old callable should now be stale
qsharp.init()

Check notice

Code scanning / devskim

If untrusted data (data from HTTP requests, user submitted files, etc.) is included in an eval statement it can allow an attacker to inject their own code. Note test

Review eval for untrusted data
@minestarks minestarks changed the title Introduce QdkContext API for isolated interpreter sessions Introduce QdkContext API for isolated interpreter sessions Mar 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The QDK Python layer should have knowledge of its initialization state

2 participants