Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions docs/interpreter-generators.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ See the [yield expression and generator method reference](https://docs.python.or

## Acceptance

The corpus contains 28 adopted class comparisons and three explicit refusals;
the shared core selection has 25 comparisons and three refusals. CI runs all
The corpus contains 33 adopted class comparisons and three explicit refusals;
the shared core selection has 30 comparisons and three refusals. CI runs all
eight target/profile/driver combinations and requires zero logical teardown.
Dedicated tests collect between every instruction, retain suspended aliases and
cycles, and interrupt execution in generator bodies and comprehension callbacks.
Expand All @@ -49,3 +49,7 @@ Run `python -m examples.interpreter_generators` to build a composed console;
new source after reload, method/body errors, hard Stop/Restart and waiting
class-to-core updates. Assignment expressions in comprehensions, multi-argument
throw, generator frame introspection and async retain their future boundaries.

Injected generator exceptions retain the fresh definition or suspended yield
frame, delegated yield-from frames and active exception context. The corpus
checks these frame names and source lines against the pinned executable oracle.
10 changes: 10 additions & 0 deletions runtime/check_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ def references(corpus_path=CORPUS):
passed = passed and actual['stdout'] == case.get('stdout', '')
if 'error_type' in case:
passed = passed and actual.get('error', {}).get('type') == case['error_type']
if 'error_frames' in case:
passed = passed and actual.get('error', {}).get('frames') == case['error_frames']
if 'error_context_type' in case:
passed = passed and (actual.get('error', {}).get('context') or {}).get('type') == case['error_context_type']
records.append({'id': case['id'], 'stage': case['stage'],
'reference_status': 'pass' if passed else 'fail', 'reference': actual})
return {'schema_version': 1, 'suite': corpus.get('suite', 'interpreter-classes/1'), 'profile': corpus['profile'],
Expand Down Expand Up @@ -89,6 +93,12 @@ def check(target, channel=None, corpus_path=CORPUS, stages=None, stepped=False,
passed = actual['ok'] == expected['ok'] and actual['stdout'] == expected['stdout']
if not expected['ok']:
passed = passed and actual.get('error', {}).get('type') == expected['error']['type']
if 'error_frames' in case:
frames = [{'function': frame['function'], 'line': frame['location']['span'][0] if frame.get('location') else None}
for frame in actual.get('error', {}).get('traceback', [])]
passed = passed and frames == case['error_frames']
if 'error_context_type' in case:
passed = passed and (actual.get('error', {}).get('context') or {}).get('type') == case['error_context_type']
classification = 'reference'
else:
passed = not actual['ok'] and actual.get('error', {}).get('code') == 'UNSUPPORTED'
Expand Down
40 changes: 32 additions & 8 deletions runtime/functions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -190,21 +190,23 @@ function activation(program, {maxSteps = 10_000, maxWork = 1_000_000, maxDepth =
const {dispatch, call, deliver, notifyNext, recover, argumentOwner, appendArgument, appendKeyword, expandArguments, keywordArgument, expandMapping, finishArguments, expandDisplay, expandUnpack} = operationRuntime({state, memory, values, classes,
moduleContext, maxDepth, allocate, mutate, python, hard, charge, enter, bind, block, store, onOutput,
resumeGenerator: (...args) => resumeGenerator(...args), yieldGenerator: value => yieldGenerator(value),
delegateResult: (...args) => delegateResult(...args)});
delegateResult: (...args) => delegateResult(...args), injectGenerator: value => injectGenerator(value)});
const leaveGenerator = (frame, status) => {
const generator = frame.generator, completion = frame.completion;
state.temporaries.push(generator, ...completionRoots(completion));
frame.completion = null; frame.callsite = null;
if (!frame.ownsException) frame.activeException = null;
for (const task of frame.tasks) if (task.type === "try" && !task.previousOwned) task.previous = null;
generator.status = status; generator.frame = frame;
frame.leave(); state.frames.pop();
frame.leave(); frame.leave = null; state.frames.pop();
return completion;
};
const yieldGenerator = value => {
const frame = state.frames.at(-1);
if (!frame.generator) hard("BYTECODE", "BytecodeError", "Yield requires a generator frame.");
state.temporaries.push(value);
const task = frame.tasks.at(-1);
frame.suspensionSite = task.type === "delegate" ? task.site : {function: frame.fn.name, location: state.current.location};
const mode = frame.generatorMode, site = frame.callsite;
const completion = leaveGenerator(frame, "suspended");
if (mode === "close") python("RuntimeError", "generator ignored GeneratorExit");
Expand All @@ -215,6 +217,27 @@ function activation(program, {maxSteps = 10_000, maxWork = 1_000_000, maxDepth =
if (iterator instanceof InstanceValue) return classes.read(iterator, name);
python("AttributeError", "Delegate has no attribute '" + name + "'");
};
const injectGenerator = value => {
const frame = state.frames.at(-1);
addTrace(value, frame.suspensionSite);
if (frame.ownsException && frame.activeException && frame.activeException !== value) mutate(value, () => {
const before = value.context; value.context = frame.activeException;
return () => { value.context = before; };
});
handle(new Raised(value));
};
const definitionSite = callable => {
for (const fn of callable.pool.code.functions) for (const block of fn.blocks) {
for (let index = 0; index < block.instructions.length; index++) {
charge(1);
const [op, target] = block.instructions[index];
if (["function", "function-defaults"].includes(op) && target === callable.index)
return {function: callable.pool.code.functions[callable.index].name,
location: block.source ? {filename: block.source.filename, span: block.source.spans[index]} : null};
}
}
return {function: callable.pool.code.functions[callable.index].name, location: null};
};
const delegateAdvance = (mode, value) => {
const frame = state.frames.at(-1), task = frame.tasks.at(-1), site = task.site;
if (mode === "throw" && value.name === "GeneratorExit") mode = "close";
Expand All @@ -225,8 +248,8 @@ function activation(program, {maxSteps = 10_000, maxWork = 1_000_000, maxDepth =
try { plan = delegateMethod(task.iterator, mode); }
catch (error) {
if (!(error instanceof Raised) || error.value.name !== "AttributeError" || mode === "send") throw error;
if (mode === "close") { handle(new Raised(task.closing)); return; }
handle(new Raised(value)); return;
if (mode === "close") { injectGenerator(task.closing); return; }
injectGenerator(value); return;
}
const args = mode === "close" ? [] : [value];
if (plan.kind === "value") call(plan.value, args, [], [], site, finish);
Expand All @@ -238,7 +261,7 @@ function activation(program, {maxSteps = 10_000, maxWork = 1_000_000, maxDepth =
const frame = state.frames.at(-1), task = frame.tasks.at(-1);
if (kind === "delegate-iter") { task.iterator = value; delegateAdvance("send", null); }
else if (kind === "delegate-next") yieldGenerator(value);
else if (kind === "delegate-close") handle(new Raised(task.closing));
else if (kind === "delegate-close") injectGenerator(task.closing);
else { frame.tasks.pop(); frame.tasks.at(-1).stack.push(value); }
return null;
};
Expand All @@ -251,9 +274,10 @@ function activation(program, {maxSteps = 10_000, maxWork = 1_000_000, maxDepth =
const fresh = generator.status === "created";
if (fresh && mode === "send" && value !== null) python("TypeError", "can't send non-None value to a just-started generator");
if (generator.status === "closed" || fresh && mode !== "send") {
if (fresh && mode === "throw") addTrace(value, definitionSite(generator.callable));
generator.status = "closed"; generator.callable = null; generator.bindings.clear();
if (mode === "close") { dispatch(deliver(null, completion, site)); return; }
if (mode === "throw") throw new Raised(value);
if (mode === "throw") { addTrace(value, site); throw new Raised(value); }
python("StopIteration", "");
}
state.temporaries.push(generator, value, ...completionRoots(completion));
Expand All @@ -275,8 +299,8 @@ function activation(program, {maxSteps = 10_000, maxWork = 1_000_000, maxDepth =
generator.status = "running";
const frame = generator.frame; frame.generatorMode = mode;
if (frame.tasks.at(-1).type === "delegate") delegateAdvance(mode, value);
else if (mode === "close") handle(new Raised(allocate(ExceptionValue, "GeneratorExit", [], "")));
else if (mode === "throw") handle(new Raised(value));
else if (mode === "close") injectGenerator(allocate(ExceptionValue, "GeneratorExit", [], ""));
else if (mode === "throw") injectGenerator(value);
else if (!fresh) frame.tasks.at(-1).stack.push(value);
};
const diagnostic = value => {
Expand Down
4 changes: 2 additions & 2 deletions runtime/operation-runtime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export class Raised { constructor(value) { this.value = value; } }
// Fixed phases of the existing machine. Python callbacks enter its ordinary
// frames; destinations use its block stack and single unwinder.
export function operationRuntime({state, memory, values, classes, moduleContext, maxDepth,
allocate, mutate, python, hard, charge, enter, bind, block, store, onOutput, resumeGenerator, yieldGenerator, delegateResult}) {
allocate, mutate, python, hard, charge, enter, bind, block, store, onOutput, resumeGenerator, yieldGenerator, delegateResult, injectGenerator}) {
const count = tag => { state.transitions[tag] = (state.transitions[tag] || 0) + 1; };
const display = value => {
if (value instanceof NotImplementedValue) return "NotImplemented";
Expand Down Expand Up @@ -668,7 +668,7 @@ export function operationRuntime({state, memory, values, classes, moduleContext,
count("dispatch:" + task.kind);
let {callable, args, keywords, keywordValues, site, completion, depth} = task;
try {
if (task.kind === "unary" && task.operation === "delegate-raise") throw new Raised(task.value);
if (task.kind === "unary" && task.operation === "delegate-raise") { injectGenerator(task.value); return; }
if (task.kind === "unary" && task.operation === "unpack-short") {
state.temporaries = operationRoots(task); python("ValueError", "Not enough values to unpack");
}
Expand Down
6 changes: 3 additions & 3 deletions src/pocket_python/_interpreter_lock.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"""Known source-console assets, canonical LF bytes; refresh with scripts/lock_interpreter.py."""

ENGINE_FINGERPRINT = '838cf574d88d26ca28e945fd3fa077e7c98bf4e19a4bb202429d54beecbef697'
ENGINE_FINGERPRINT = '0482efe29e46d5f1228ee6d7ce214f1f4168729a55241e4995502e9587cb3ec2'
ENGINE_FILES = (
('browser-protocol.mjs', 10589, '920e8d07c7070995636dff40fdf4333afc9bd4143483db5d8dff82ec1f28ff76'),
('bytecode.mjs', 6836, 'e76fabeea879bc1da8016283f7e74df5974479666b100199c2d3c31a53b8ecf4'),
('class-policy.mjs', 1968, 'e3275e080c2d26584bb318e88d9a0278191815ac072c48e3054fd4c908cb771b'),
('class-values.mjs', 15372, '67fc606a6c91f7aeb1d4309cc4a0be33778181cdccb51190e5f76d4969d9b43d'),
('function-bytecode.mjs', 20438, 'ca6b3abd73d2be35422eea244b876a851bf98e38836e31a33fa6d18d4252ae68'),
('function-objects.mjs', 4277, '06af0ca936de1dd336cb7a2ac13f24efae960ea3fe448c93fb95a2e92213c4e2'),
('functions.mjs', 56333, '4450009703952a1dab3c62a94dd84aadeee9c0f2fee0a31db44495eddcc20e96'),
('functions.mjs', 57726, '7d6e4322c31187b12387695dd094a0fd4dab2220cf28c1df2bb4856ccf7ae179'),
('interpreter-host.mjs', 7019, '0bb0a64faffcf77e0064258e4cfcaa7a937c4372129849f76901bee91b5a5ae2'),
('interpreter-worker.mjs', 8282, '937a97cc49d987492de0bc16021a4a804ef51e4ea5b398f79c92e5d30ac82558'),
('library-catalog.mjs', 907, '0adcaae0f6f07b4e1acb22e46493ec99f7247a5828b426c703530d8b2736032c'),
Expand All @@ -20,7 +20,7 @@
('module-registry.mjs', 10085, 'f23f8313c0804a052cba155e193df920b0221ef978a06f16c1a22f208dfbef91'),
('module-session.mjs', 1470, '08e4ccf16361299c15b99621fdfcef0d9ef93de50d28d738158c7bc85a7c23a3'),
('numeric.mjs', 13954, '2431f099a7d40aa38d6b3c68b556207752e4191211e01005acde94325bd91bad'),
('operation-runtime.mjs', 93126, '18aff2a37a30c3fe0deebb51dae85f8200f9674588308941f5147ae10c809c9f'),
('operation-runtime.mjs', 93154, '488791c8e00d8e510feaf9bc654f06a6e0bcaed7f8ed6a2024a1a23140713315'),
('operations.mjs', 23452, 'bd4c064811e6400576cae60bfcdbd35a76c829a25085794f95520d6307fad78e'),
('source-compiler.mjs', 35239, '4201a25a0c45b8fba4777c8e98b75e37f3277888708093a77e2827dec5ff7063'),
('source-identifiers.mjs', 25949, '1e4e6a90728a2d9393eb4b2055d4bd759542f4768d1870b508d629c6888e8914'),
Expand Down
106 changes: 106 additions & 0 deletions tests/fixtures/interpreter/generators.json
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,112 @@
"source": "class D:\n def __iter__(self): return self\n def __next__(self): return 1\n @property\n def throw(self): raise AttributeError(\"missing\")\ndef g(): yield from D()\na=g()\nprint(next(a))\ntry: a.throw(ValueError(\"original\"))\nexcept ValueError as e: print(str(e))\n",
"stdout": "1\noriginal\n",
"stage": "G1"
},
{
"id": "injected-yield-trace",
"stage": "G1",
"source": "def g():\n yield 1\na=g()\nnext(a)\na.throw(ValueError(\"injected\"))\n",
"error_type": "ValueError",
"error_frames": [
{
"function": "<module>",
"line": 5
},
{
"function": "g",
"line": 2
}
],
"profiles": [
"classes-source/1",
"core-source/1"
]
},
{
"id": "injected-fresh-trace",
"stage": "G1",
"source": "def g():\n yield 1\ng().throw(ValueError(\"fresh\"))\n",
"error_type": "ValueError",
"error_frames": [
{
"function": "<module>",
"line": 3
},
{
"function": "g",
"line": 1
}
],
"profiles": [
"classes-source/1",
"core-source/1"
]
},
{
"id": "injected-delegated-trace",
"stage": "G1",
"source": "def inner():\n yield 1\ndef outer():\n yield from inner()\na=outer()\nnext(a)\na.throw(ValueError(\"delegated\"))\n",
"error_type": "ValueError",
"error_frames": [
{
"function": "<module>",
"line": 7
},
{
"function": "outer",
"line": 4
},
{
"function": "inner",
"line": 2
}
],
"profiles": [
"classes-source/1",
"core-source/1"
]
},
{
"id": "injected-active-context",
"stage": "G1",
"source": "def g():\n try:\n raise KeyError(\"saved\")\n except KeyError:\n yield 1\na=g()\nnext(a)\na.throw(ValueError(\"injected\"))\n",
"error_type": "ValueError",
"error_frames": [
{
"function": "<module>",
"line": 8
},
{
"function": "g",
"line": 5
}
],
"error_context_type": "KeyError",
"profiles": [
"classes-source/1",
"core-source/1"
]
},
{
"id": "injected-caller-context-isolation",
"stage": "G1",
"source": "def g():\n yield 1\na=g()\nnext(a)\ntry:\n raise KeyError(\"caller\")\nexcept KeyError:\n a.throw(ValueError(\"injected\"))\n",
"error_type": "ValueError",
"error_context_type": null,
"error_frames": [
{
"function": "<module>",
"line": 8
},
{
"function": "g",
"line": 2
}
],
"profiles": [
"classes-source/1",
"core-source/1"
]
}
]
}
Loading