From c01d13b1c72558fdd644f60a9af8e8558fef1fef Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sun, 1 Nov 2026 05:36:00 -0500 Subject: [PATCH 01/10] Retain native function parameter layout and original metadata operands --- .../native_compilation.py | 71 ++++++++++++++++++- .../native_declarations.py | 35 ++++++--- tests/test_native_parameter_defaults.py | 32 +++++++++ 3 files changed, 129 insertions(+), 9 deletions(-) diff --git a/nominal_refactor_advisor/native_compilation.py b/nominal_refactor_advisor/native_compilation.py index 89029405..8b5ec917 100644 --- a/nominal_refactor_advisor/native_compilation.py +++ b/nominal_refactor_advisor/native_compilation.py @@ -63,6 +63,7 @@ from .native_declarations import ( NativeDeclaration, NativeDictionaryKey, + NativeFunctionParameters, NativeScalar, NativeConstantContentsABC, NativeScalarValueABC, @@ -597,6 +598,14 @@ def require_literal_contents(self, expected: object) -> None: """A transfer alone does not prove the contents of a source literal.""" raise ValueError("Native literal construction contents remain unproved") + @property + def parameters(self) -> NativeFunctionParameters: + raise ValueError("Native function parameter storage layout remains unproved") + + def attribute_operand(self, flag: int) -> NativeProducedValue: + """Original function metadata input, not a runtime attribute observation.""" + raise ValueError("Native function attribute has no original attachment") + def resolve( self, resolver: NativeValueResolverABC[NativeResolutionT] ) -> NativeResolutionT: @@ -631,6 +640,25 @@ def resolve( return resolver._function_native_value_resolution(self) +@dataclass(frozen=True, eq=False) +class NativeRawFunctionValue(NativeFunctionValue): + """MAKE_FUNCTION operands with their actual code layout and native flag order.""" + + parameter_layout: NativeFunctionParameters + attribute_flags: tuple[int, ...] + + parameters = AliasProperty[NativeFunctionParameters]("parameter_layout") + + def attribute_operand(self, flag: int) -> NativeProducedValue: + if len(self.inputs) != len(self.attribute_flags) + 1: + raise ValueError("Native function metadata differs from its creation inputs") + try: + index = self.attribute_flags.index(flag) + except ValueError: + return super().attribute_operand(flag) + return self.inputs[index] + + @dataclass(frozen=True, eq=False) class NativeFunctionAttributeValue(NativeFunctionValue): """An attribute attachment retains its original function creation.""" @@ -642,6 +670,12 @@ def function(self) -> NativeFunctionValue: return cast(NativeFunctionValue, self.inputs[-1]) creation = AliasProperty[NativeFunctionValue]("function.creation") + parameters = AliasProperty[NativeFunctionParameters]("function.parameters") + + def attribute_operand(self, flag: int) -> NativeProducedValue: + if self.attribute_flag == flag: + return self.inputs[0] + return self.function.attribute_operand(flag) @dataclass(frozen=True, eq=False) @@ -3303,7 +3337,15 @@ def capture_operands( ): raise ValueError("Native function creation has a different code operand") stack.stack.append( - stack.emit(NativeFunctionValue(instruction.offset, inputs), instruction) + stack.emit( + NativeRawFunctionValue( + instruction.offset, + inputs, + NativeFunctionParameters.from_code(emission.code), + stack.backend.creation_attribute_flags(instruction), + ), + instruction, + ) ) @classmethod @@ -3521,6 +3563,12 @@ def generated_class_frame( ) -> NativeGeneratedClassFrame: raise ValueError("Generated class wrapper activation remains unproved") + def creation_attribute_flags(self, instruction: dis.Instruction) -> tuple[int, ...]: + raise ValueError("Native function creation metadata remains unproved") + + def function_attribute_flag(self, name: str) -> int: + raise ValueError("Native function attribute ABI remains unproved") + @property def call_operand_order(self) -> NativeCallOperandOrder: raise ValueError("Native call operand layout remains unproved") @@ -3988,6 +4036,12 @@ class ContiguousNativeCreationBackend(NativeCreationBackend): class_capture_fallback = NativeExecutionUnavailable.NO_EMITTED_CODE attribute_flags: frozenset[int] application_prelude: tuple[type[NativeCreationOperation], ...] + native_attributes: tuple[str, ...] + + def function_attribute_flag(self, name: str) -> int: + if self.native_attributes.count(name) != 1: + raise ValueError("Native function attribute requires one declared ABI field") + return 1 << self.native_attributes.index(name) def require_invocation( self, prelude: list[dis.Instruction], call: dis.Instruction @@ -4530,6 +4584,16 @@ class CPython311CreationBackend( call_operand_order = NativeCallOperandOrder.NULL_CALLEE + def __init__(self) -> None: + self.native_attributes = dis.MAKE_FUNCTION_FLAGS + + def creation_attribute_flags(self, instruction: dis.Instruction) -> tuple[int, ...]: + mask = instruction.arg + flags = tuple(1 << index for index, _ in enumerate(self.native_attributes)) + if type(mask) is not int or mask < 0 or mask & ~sum(flags): + raise ValueError("MAKE_FUNCTION has unsupported native metadata flags") + return tuple(flag for flag in flags if mask & flag) + class CPythonGeneratedClassBinding(StrEnum): """Compiler-owned class-body closure fields and their exact native types.""" @@ -4604,6 +4668,11 @@ def __init__(self) -> None: "Admitted CPython 3.14 backend lacks unique annotate metadata" ) + def creation_attribute_flags(self, instruction: dis.Instruction) -> tuple[int, ...]: + if instruction.arg is not None: + raise ValueError("MAKE_FUNCTION must use separate attribute attachments") + return () + @cached_property def binary_subscription_argument(self) -> int: # This is the admitted interpreter ABI table, not an analyzer operator map. diff --git a/nominal_refactor_advisor/native_declarations.py b/nominal_refactor_advisor/native_declarations.py index 60357823..f57b8836 100644 --- a/nominal_refactor_advisor/native_declarations.py +++ b/nominal_refactor_advisor/native_declarations.py @@ -18,6 +18,7 @@ import inspect from textwrap import dedent from types import ( + CodeType, FunctionType, ModuleType, ) @@ -169,6 +170,26 @@ def require_same_native_constant(self, other: NativeConstantContentsABC) -> None self.require_constant_contents(other._native_constant_value()) +@dataclass(frozen=True) +class NativeFunctionParameters: + """Parameter storage layout read from the original native code, not inspect.signature.""" + + positional: tuple[str, ...] + keyword_only: tuple[str, ...] + + @classmethod + def from_code(cls, code: CodeType) -> Self: + return cls( + code.co_varnames[: code.co_argcount], + code.co_varnames[ + code.co_argcount : code.co_argcount + code.co_kwonlyargcount + ], + ) + + def positional_default_names(self, count: int) -> tuple[str, ...]: + return self.positional[max(0, len(self.positional) - count) :] + + @dataclass(frozen=True, eq=False) class NativeParameterDefault(NativeConstantContentsABC): """One observed native default association, not source syntax or an effect proof. @@ -201,21 +222,19 @@ def from_function(cls, function: FunctionType) -> tuple[Self, ...]: ) if any(type(name) is not str for name, _ in keyword_items): raise ValueError("Native keyword default keys require exact string storage") - names = code.co_varnames[: code.co_argcount] - keyword_names = frozenset( - code.co_varnames[ - code.co_argcount : code.co_argcount + code.co_kwonlyargcount - ] - ) + parameters = NativeFunctionParameters.from_code(code) + names = parameters.positional_default_names(len(positional)) return tuple( cls(name, value) for name, value in zip( - names[max(0, len(names) - len(positional)) :], + names, positional[max(0, len(positional) - len(names)) :], strict=True, ) ) + tuple( - cls(name, value) for name, value in keyword_items if name in keyword_names + cls(name, value) + for name, value in keyword_items + if name in parameters.keyword_only ) diff --git a/tests/test_native_parameter_defaults.py b/tests/test_native_parameter_defaults.py index ce322d2b..0c52e978 100644 --- a/tests/test_native_parameter_defaults.py +++ b/tests/test_native_parameter_defaults.py @@ -5,11 +5,14 @@ import pytest +from nominal_refactor_advisor.ast_tools import SourceModule from nominal_refactor_advisor.call_binding import ( CompactFunctionSignature, DefaultedFunctionParameter, ) +from nominal_refactor_advisor.native_compilation import NativeCreationBackend from nominal_refactor_advisor.native_declarations import NativeParameterDefault +from nominal_refactor_advisor.source_geometry import SourceByteSpan from nominal_refactor_advisor.scan_cache import ScanCache from test_native_dataclass_factory import factory_environment @@ -207,3 +210,32 @@ def function(value=None): after.require_constant_contents(17) assert before is not after before.require_constant_contents(None) # An observation, not current storage. + + +@pytest.mark.parametrize('arguments,attached', ( + ('first, /, second=0, *items, option=None, **options', ('defaults', 'kwdefaults')), + ('value=0', ('defaults',)), + ('*, option=None', ('kwdefaults',)), + ('value', ()), +)) +def test_original_function_default_metadata_is_joined_to_native_storage( + tmp_path, arguments, attached): + """Only compile the subject: the stored inputs are not runtime default objects.""" + path = tmp_path / 'unexecuted.py' + source = f'def selected({arguments}):\n raise AssertionError("body executed")\n' + parsed = SourceModule.from_source_path(path, source).parse() + method, = (node for _, node in parsed.syntax_index.named_functions) + span = SourceByteSpan.require_node(method) + store = parsed.native_compilation.value_store_for(span, span, method.name) + function = store.value + backend = NativeCreationBackend.current() + for name, native_type in (('defaults', tuple), ('kwdefaults', dict)): + flag = backend.function_attribute_flag(name) + if name not in attached: + with pytest.raises(ValueError, match='no original attachment'): + function.attribute_operand(flag) + else: + operand = function.attribute_operand(flag) + store.require_value(operand) + assert operand.native_type is native_type + assert not path.exists() From 08d1c664c81380aa20b029dbaeb4b134079e1e0b Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sun, 1 Nov 2026 05:38:43 -0500 Subject: [PATCH 02/10] Declare unproved produced-value type queries on their family --- nominal_refactor_advisor/native_compilation.py | 5 +++++ tests/test_native_parameter_defaults.py | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/nominal_refactor_advisor/native_compilation.py b/nominal_refactor_advisor/native_compilation.py index 8b5ec917..2d626d25 100644 --- a/nominal_refactor_advisor/native_compilation.py +++ b/nominal_refactor_advisor/native_compilation.py @@ -598,6 +598,11 @@ def require_literal_contents(self, expected: object) -> None: """A transfer alone does not prove the contents of a source literal.""" raise ValueError("Native literal construction contents remain unproved") + @property + def native_type(self) -> type: + """Untyped productions do not acquire a type from their consumer's expectation.""" + raise ValueError("Native production type remains unproved") + @property def parameters(self) -> NativeFunctionParameters: raise ValueError("Native function parameter storage layout remains unproved") diff --git a/tests/test_native_parameter_defaults.py b/tests/test_native_parameter_defaults.py index 0c52e978..8c241626 100644 --- a/tests/test_native_parameter_defaults.py +++ b/tests/test_native_parameter_defaults.py @@ -2,6 +2,7 @@ import ast import dataclasses +from pathlib import Path import pytest @@ -219,7 +220,7 @@ def function(value=None): ('value', ()), )) def test_original_function_default_metadata_is_joined_to_native_storage( - tmp_path, arguments, attached): + tmp_path: Path, arguments: str, attached: tuple[str, ...]) -> None: """Only compile the subject: the stored inputs are not runtime default objects.""" path = tmp_path / 'unexecuted.py' source = f'def selected({arguments}):\n raise AssertionError("body executed")\n' @@ -232,7 +233,7 @@ def test_original_function_default_metadata_is_joined_to_native_storage( for name, native_type in (('defaults', tuple), ('kwdefaults', dict)): flag = backend.function_attribute_flag(name) if name not in attached: - with pytest.raises(ValueError, match='no original attachment'): + with pytest.raises(ValueError): function.attribute_operand(flag) else: operand = function.attribute_operand(flag) From 4d7f9c6dcb287c86382d90912f2303b93ca7641d Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sun, 1 Nov 2026 06:44:15 -0500 Subject: [PATCH 03/10] Retain original native default-slot associations without inventing productions --- .../native_compilation.py | 197 +++++++++++++++--- .../native_declarations.py | 12 +- tests/test_native_dictionary_operands.py | 30 ++- tests/test_native_parameter_defaults.py | 84 ++++++-- 4 files changed, 276 insertions(+), 47 deletions(-) diff --git a/nominal_refactor_advisor/native_compilation.py b/nominal_refactor_advisor/native_compilation.py index 2d626d25..23eb1e9a 100644 --- a/nominal_refactor_advisor/native_compilation.py +++ b/nominal_refactor_advisor/native_compilation.py @@ -11,6 +11,7 @@ Hashable, Iterable, Iterator, + Mapping, ) from dataclasses import ( dataclass, @@ -35,6 +36,7 @@ from types import ( CodeType, FunctionType, + MappingProxyType, ) from typing import ( ClassVar, @@ -64,6 +66,7 @@ NativeDeclaration, NativeDictionaryKey, NativeFunctionParameters, + NativeLiteralContentsABC, NativeScalar, NativeConstantContentsABC, NativeScalarValueABC, @@ -549,7 +552,7 @@ def from_instruction(cls, instruction: dis.Instruction) -> Self: @dataclass(frozen=True) -class NativeProducedValue(DataclassGraphNode, NativeCallSlotABC): +class NativeProducedValue(DataclassGraphNode, NativeCallSlotABC, NativeLiteralContentsABC): """One original native production and its inputs, not runtime identity.""" instruction_offset: int @@ -594,9 +597,24 @@ def require_scalar_store_value(self) -> NativeConstantValue: """A produced type or read alone does not establish scalar contents.""" raise ValueError("Native scalar production remains unproved") - def require_literal_contents(self, expected: object) -> None: - """A transfer alone does not prove the contents of a source literal.""" - raise ValueError("Native literal construction contents remain unproved") + @property + def constant_tuple_contents(self) -> tuple[object, ...]: + raise ValueError("Native original constant tuple remains unproved") + + @property + def name_tuple(self) -> tuple[str, ...]: + names = self.constant_tuple_contents + if any(type(name) is not str for name in names): + raise ValueError("Native metadata names require exact string contents") + return cast(tuple[str, ...], names) + + @property + def tuple_elements(self) -> tuple[NativeLiteralContentsABC, ...]: + raise ValueError("Native original tuple elements remain unproved") + + @property + def named_operands(self) -> Mapping[str, NativeProducedValue]: + raise ValueError("Native original named dictionary operands remain unproved") @property def native_type(self) -> type: @@ -611,6 +629,12 @@ def attribute_operand(self, flag: int) -> NativeProducedValue: """Original function metadata input, not a runtime attribute observation.""" raise ValueError("Native function attribute has no original attachment") + def positional_default_operands(self) -> tuple[NativePositionalDefaultOperand, ...]: + raise ValueError("Native positional default association remains unproved") + + def keyword_default_operands(self) -> tuple[NativeKeywordDefaultOperand, ...]: + raise ValueError("Native keyword default association remains unproved") + def resolve( self, resolver: NativeValueResolverABC[NativeResolutionT] ) -> NativeResolutionT: @@ -639,12 +663,65 @@ class NativeFunctionValue(DataclassGraphValue, NativeTypedValue): def creation(self) -> NativeFunctionValue: return self + def positional_default_operands(self) -> tuple[NativePositionalDefaultOperand, ...]: + """Require original birth storage; absent metadata is not an empty proof.""" + storage = self.attribute_operand( + NativeCreationBackend.current().function_attribute_flag("defaults") + ) + elements = storage.tuple_elements + names = self.parameters.positional_default_names(len(elements)) + if len(names) != len(elements): + raise ValueError("Original positional defaults exceed native parameters") + return tuple( + NativePositionalDefaultOperand(name, storage, index) + for index, name in enumerate(names) + ) + + def keyword_default_operands(self) -> tuple[NativeKeywordDefaultOperand, ...]: + """Join original exact-name dictionary entries, not current runtime storage.""" + storage = self.attribute_operand( + NativeCreationBackend.current().function_attribute_flag("kwdefaults") + ) + names = storage.named_operands + if any(name not in self.parameters.keyword_only for name in names): + raise ValueError("Original keyword defaults differ from native parameters") + return tuple(NativeKeywordDefaultOperand(name, storage) for name in names) + def resolve( self, resolver: NativeValueResolverABC[NativeResolutionT] ) -> NativeResolutionT: return resolver._function_native_value_resolution(self) +@dataclass(frozen=True, eq=False) +class NativeParameterDefaultOperand(ABC): + """One original parameter/storage association, never an observed target object.""" + + parameter_name: str + storage: NativeProducedValue + + @property + @abstractmethod + def value(self) -> NativeLiteralContentsABC: + raise NotImplementedError + + +@dataclass(frozen=True, eq=False) +class NativePositionalDefaultOperand(NativeParameterDefaultOperand): + index: int + + @property + def value(self) -> NativeLiteralContentsABC: + return self.storage.tuple_elements[self.index] + + +@dataclass(frozen=True, eq=False) +class NativeKeywordDefaultOperand(NativeParameterDefaultOperand): + @property + def value(self) -> NativeProducedValue: + return self.storage.named_operands[self.parameter_name] + + @dataclass(frozen=True, eq=False) class NativeRawFunctionValue(NativeFunctionValue): """MAKE_FUNCTION operands with their actual code layout and native flag order.""" @@ -735,7 +812,7 @@ def resolve( class NativeKeywordCallValue(NativeCallValue): """Original keyword metadata and values, independent of source admission.""" - keyword_names: NativeConstantValue = field(kw_only=True) + keyword_names: NativeProducedValue = field(kw_only=True) def __post_init__(self) -> None: self.names @@ -746,11 +823,7 @@ def graph_children(self) -> tuple[NativeProducedValue, ...]: @property def names(self) -> tuple[str, ...]: - if not isinstance(self.keyword_names, NativeConstantValue): - raise ValueError("Native keyword names require original constant metadata") - names = self.keyword_names.value - if type(names) is not tuple or any(type(name) is not str for name in names): - raise ValueError("Native keyword names require an exact string tuple") + names = self.keyword_names.name_tuple if len(set(names)) != len(names) or len(names) > ( len(self.arguments) - len(self.argument_slot.implicit_arguments) ): @@ -864,6 +937,10 @@ class NativeTupleValue(NativeSequenceValue): declaration: ClassVar[NativeTypeDeclaration] = NativeTypeDeclaration(tuple) + @property + def tuple_elements(self) -> tuple[NativeLiteralContentsABC, ...]: + return self.inputs + def resolve( self, resolver: NativeValueResolverABC[NativeResolutionT] ) -> NativeResolutionT: @@ -880,8 +957,18 @@ class NativeConstantValue(NativeTypedValue): def _native_constant_value(self) -> object: return self.value - def require_literal_contents(self, expected: object) -> None: - self.require_constant_contents(expected) + @property + def constant_tuple_contents(self) -> tuple[object, ...]: + if type(self.value) is not tuple: + raise ValueError("Native original constant tuple requires exact tuple storage") + return cast(tuple[object, ...], self.value) + + @property + def tuple_elements(self) -> tuple[NativeLiteralContentsABC, ...]: + return tuple( + NativeConstantTupleElement(self, index) + for index in range(len(self.constant_tuple_contents)) + ) def require_scalar_store_value(self) -> NativeConstantValue: self.require_native_scalar() @@ -895,14 +982,74 @@ def _native_scalar_value(self) -> object: return self.value +@dataclass(frozen=True, eq=False) +class NativeConstantTupleElement(NativeConstantContentsABC): + """A projection of an original constant, with no invented instruction offset.""" + + storage: NativeConstantValue + index: int + + def _native_constant_value(self) -> object: + return self.storage.constant_tuple_contents[self.index] + + @dataclass(frozen=True) -class NativeEmptyDictionaryValue(NativeTypedValue): - """Fresh exact empty-map production; source activation remains separate.""" +class NativeDictionaryValue(NativeTypedValue): + """Dict-typed production; only declared construction proves entry positions.""" declaration: ClassVar[NativeTypeDeclaration] = NativeTypeDeclaration(dict) + @property + def named_keys(self) -> tuple[str, ...]: + raise ValueError("Native dictionary key names remain unproved") + + @property + def entry_operands(self) -> tuple[NativeProducedValue, ...]: + raise ValueError("Native dictionary value positions remain unproved") + + @property + def named_operands(self) -> Mapping[str, NativeProducedValue]: + # Exact strings use native dict overwrite/order semantics, without target callbacks. + return MappingProxyType(dict(zip(self.named_keys, self.entry_operands, strict=True))) + + +class NativePairedDictionaryValue(NativeDictionaryValue): + @property + def named_keys(self) -> tuple[str, ...]: + return tuple( + key.require_scalar_store_value().require_native_text() + for key in self.inputs[::2] + ) + + @property + def entry_operands(self) -> tuple[NativeProducedValue, ...]: + return self.inputs[1::2] + + +class NativeConstantKeyDictionaryValue(NativeDictionaryValue): + @property + def named_keys(self) -> tuple[str, ...]: + return self.inputs[-1].name_tuple + + @property + def entry_operands(self) -> tuple[NativeProducedValue, ...]: + return self.inputs[:-1] + + +@dataclass(frozen=True) +class NativeEmptyDictionaryValue(NativeDictionaryValue): + """Fresh exact empty-map production; source activation remains separate.""" + inputs: ClassVar[tuple[NativeProducedValue, ...]] = () + @property + def named_keys(self) -> tuple[str, ...]: + return () + + @property + def entry_operands(self) -> tuple[NativeProducedValue, ...]: + return () + def resolve( self, resolver: NativeValueResolverABC[NativeResolutionT] ) -> NativeResolutionT: @@ -910,14 +1057,13 @@ def resolve( @dataclass(frozen=True) -class NativeAnnotationNamespaceValue(NativeTypedValue): +class NativeAnnotationNamespaceValue(NativeDictionaryValue): """SETUP_ANNOTATIONS observes existing storage or creates an exact dictionary. Freshness requires independent absence at this original frame's entry. """ namespace_name: ClassVar[str] = "__annotations__" - declaration: ClassVar[NativeTypeDeclaration] = NativeTypeDeclaration(dict) inputs: ClassVar[tuple[NativeProducedValue, ...]] = () def resolve( @@ -1543,10 +1689,6 @@ def invoke_keywords( self, operation: NativePrimitiveOperation, instruction: dis.Instruction ) -> None: (names,) = self.pop(1) - if not isinstance(names, NativeConstantValue): - raise ValueError( - "Native keyword metadata requires its original constant tuple" - ) self.call_factory = partial(NativeKeywordCallValue, keyword_names=names) self.invoke(operation, instruction) @@ -1578,7 +1720,10 @@ def dictionary_value( if type(instruction.arg) is not int or instruction.arg < 0: raise ValueError("Native dictionary has no exact nonnegative input count") self.stack.append( - self.typed(instruction, dict, 2 * instruction.arg) + self.emit( + NativePairedDictionaryValue(instruction.offset, self.pop(2 * instruction.arg)), + instruction, + ) if instruction.arg else self.emit(NativeEmptyDictionaryValue(instruction.offset), instruction) ) @@ -1590,19 +1735,13 @@ def constant_key_dictionary( raise ValueError("Native dictionary has no exact nonnegative input count") inputs = self.pop(instruction.arg + 1) keys = inputs[-1] - if ( - not isinstance(keys, NativeConstantValue) - or type(keys.value) is not tuple - or len(keys.value) != instruction.arg - ): + if len(keys.constant_tuple_contents) != instruction.arg: raise ValueError( "Native dictionary requires its original constant key tuple" ) self.stack.append( self.emit( - NativeTypedValue( - instruction.offset, inputs, NativeTypeDeclaration(dict) - ), + NativeConstantKeyDictionaryValue(instruction.offset, inputs), instruction, ) ) diff --git a/nominal_refactor_advisor/native_declarations.py b/nominal_refactor_advisor/native_declarations.py index f57b8836..cefa5463 100644 --- a/nominal_refactor_advisor/native_declarations.py +++ b/nominal_refactor_advisor/native_declarations.py @@ -116,7 +116,14 @@ def dataclass_decorator_for_name(cls, reference_name: str | None) -> Self | None NativeConstantAtom: TypeAlias = NativeScalar | types.EllipsisType -class NativeConstantContentsABC(ABC): +class NativeLiteralContentsABC(ABC): + """A literal-content question, not production, activation or object identity.""" + + def require_literal_contents(self, expected: object) -> None: + raise ValueError("Native literal construction contents remain unproved") + + +class NativeConstantContentsABC(NativeLiteralContentsABC): """Exact primitive, ellipsis and tuple contents, independent of object identity. Comparison consumes only declared exact atoms and exact tuples. It never @@ -169,6 +176,9 @@ def require_constant_contents(self, expected: object) -> None: def require_same_native_constant(self, other: NativeConstantContentsABC) -> None: self.require_constant_contents(other._native_constant_value()) + def require_literal_contents(self, expected: object) -> None: + self.require_constant_contents(expected) + @dataclass(frozen=True) class NativeFunctionParameters: diff --git a/tests/test_native_dictionary_operands.py b/tests/test_native_dictionary_operands.py index 1fc4c237..1f952096 100644 --- a/tests/test_native_dictionary_operands.py +++ b/tests/test_native_dictionary_operands.py @@ -2,6 +2,7 @@ import ast import dis +from operator import setitem import pytest @@ -11,6 +12,7 @@ NativeOperandStack, NativePrimitiveOperation, NativePythonCompilation, + NativeValueStore, ) from test_native_value_store import value_store from test_source_function_result import execution, function @@ -38,7 +40,7 @@ def test_nonempty_dictionary_retains_inputs_without_inventing_contents(expressio "BUILD_CONST_KEY_MAP" not in dis.opmap, reason="Interpreter has no constant-key-map opcode", ) -def test_constant_key_dictionary_requires_matching_original_tuple_contents(keys): +def test_constant_key_dictionary_requires_matching_original_tuple_contents(keys: object) -> None: instruction = next( event for event in dis.get_instructions( @@ -54,10 +56,34 @@ def test_constant_key_dictionary_requires_matching_original_tuple_contents(keys) NativeConstantValue(4, (), keys), ) ) - with pytest.raises(ValueError, match="original constant key tuple"): + with pytest.raises(ValueError): NativePrimitiveOperation.BUILD_CONST_KEY_MAP.capture(stack, instruction) +@pytest.mark.parametrize('expression', ( + "{'one': first, 'two': middle, 'one': last}", + "{key: value}", +)) +def test_named_dictionary_queries_preserve_operands_without_admitting_opaque_keys( + expression: str) -> None: + source = f'payload = {expression}\n' + compiled = NativePythonCompilation(source, 'named_dictionary.py') + store: NativeValueStore = value_store(compiled, ast.parse(source).body[0]) + if expression == '{key: value}': + with pytest.raises(ValueError): + _ = store.value.named_operands + else: + operands = store.value.named_operands + assert tuple(operands) == ('one', 'two') + assert operands['one'].instruction_offset > operands['two'].instruction_offset + for value in operands.values(): + store.require_value(value) + with pytest.raises(ValueError): + value.require_literal_contents(0) + with pytest.raises(TypeError): + setitem(operands, 'one', operands['two']) + + @pytest.mark.parametrize( "header", ("*, value=callback()", "*, first=callback(), second=None") ) diff --git a/tests/test_native_parameter_defaults.py b/tests/test_native_parameter_defaults.py index 8c241626..193fa6d6 100644 --- a/tests/test_native_parameter_defaults.py +++ b/tests/test_native_parameter_defaults.py @@ -3,6 +3,7 @@ import ast import dataclasses from pathlib import Path +from typing import cast import pytest @@ -11,7 +12,11 @@ CompactFunctionSignature, DefaultedFunctionParameter, ) -from nominal_refactor_advisor.native_compilation import NativeCreationBackend +from nominal_refactor_advisor.native_compilation import ( + NativeConstantTupleElement, + NativeCreationBackend, + NativeValueStore, +) from nominal_refactor_advisor.native_declarations import NativeParameterDefault from nominal_refactor_advisor.source_geometry import SourceByteSpan from nominal_refactor_advisor.scan_cache import ScanCache @@ -213,30 +218,79 @@ def function(value=None): before.require_constant_contents(None) # An observation, not current storage. -@pytest.mark.parametrize('arguments,attached', ( - ('first, /, second=0, *items, option=None, **options', ('defaults', 'kwdefaults')), - ('value=0', ('defaults',)), - ('*, option=None', ('kwdefaults',)), - ('value', ()), -)) -def test_original_function_default_metadata_is_joined_to_native_storage( - tmp_path: Path, arguments: str, attached: tuple[str, ...]) -> None: - """Only compile the subject: the stored inputs are not runtime default objects.""" +@pytest.fixture +def original_default_store(tmp_path: Path, arguments: str) -> NativeValueStore: + """Compile original metadata without executing either the function or its defaults.""" path = tmp_path / 'unexecuted.py' - source = f'def selected({arguments}):\n raise AssertionError("body executed")\n' + source = ( + 'def make():\n raise AssertionError("callback executed")\n' + f'def selected({arguments}):\n raise AssertionError("body executed")\n' + ) parsed = SourceModule.from_source_path(path, source).parse() - method, = (node for _, node in parsed.syntax_index.named_functions) + method = next(node for name, node in parsed.syntax_index.named_functions if name == 'selected') span = SourceByteSpan.require_node(method) store = parsed.native_compilation.value_store_for(span, span, method.name) + assert not path.exists() + return store + + +@pytest.mark.parametrize('arguments,positional,keyword', ( + ('first, /, second=0, *items, option=None, **options', ('second',), ('option',)), + ('value=0', ('value',), ()), + ('*, option=None', (), ('option',)), + ('value', (), ()), +)) +def test_original_function_default_metadata_is_joined_to_native_storage( + original_default_store: NativeValueStore, + positional: tuple[str, ...], keyword: tuple[str, ...]) -> None: + store = original_default_store function = store.value backend = NativeCreationBackend.current() - for name, native_type in (('defaults', tuple), ('kwdefaults', dict)): + for name, native_type, query, names, expected in ( + ('defaults', tuple, function.positional_default_operands, positional, 0), + ('kwdefaults', dict, function.keyword_default_operands, keyword, None), + ): flag = backend.function_attribute_flag(name) - if name not in attached: + if not names: with pytest.raises(ValueError): function.attribute_operand(flag) + with pytest.raises(ValueError): + query() else: operand = function.attribute_operand(flag) store.require_value(operand) assert operand.native_type is native_type - assert not path.exists() + bindings = query() + assert tuple(binding.parameter_name for binding in bindings) == names + for binding in bindings: + assert binding.storage is operand + binding.value.require_literal_contents(expected) + + +@pytest.mark.parametrize('arguments', ('left=make(), right=make(), *, option=None',)) +def test_default_callbacks_remain_original_unproved_productions( + original_default_store: NativeValueStore) -> None: + store = original_default_store + left, right = store.value.positional_default_operands() + assert (left.parameter_name, right.parameter_name) == ('left', 'right') + assert left.storage is right.storage + assert left.index != right.index + assert left.value is not right.value + for binding in (left, right): + store.require_value(binding.storage) + with pytest.raises(ValueError): + binding.value.require_literal_contents(None) + + +@pytest.mark.parametrize('arguments', ('left=0, right=0',)) +def test_folded_default_slots_project_the_original_constant_without_new_productions( + original_default_store: NativeValueStore) -> None: + store = original_default_store + bindings = store.value.positional_default_operands() + assert tuple(binding.parameter_name for binding in bindings) == ('left', 'right') + for index, binding in enumerate(bindings): + element = cast(NativeConstantTupleElement, binding.value) + assert binding.index == element.index == index + assert binding.storage is element.storage + store.require_value(element.storage) + element.require_literal_contents(0) From 60f45d65ddc631c67a4057afb25d8d82a64f5546 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sun, 1 Nov 2026 07:31:19 -0500 Subject: [PATCH 04/10] Join source function installation to its original native store --- .../native_compilation.py | 7 +++++ nominal_refactor_advisor/source_execution.py | 27 +++++++------------ tests/test_source_function_installation.py | 27 ++++++++++++++++++- 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/nominal_refactor_advisor/native_compilation.py b/nominal_refactor_advisor/native_compilation.py index 23eb1e9a..4df9e600 100644 --- a/nominal_refactor_advisor/native_compilation.py +++ b/nominal_refactor_advisor/native_compilation.py @@ -2429,6 +2429,13 @@ def source_span(self) -> SourceByteSpan: raise ValueError("Native store has no original target span") return span + def require_original_installation( + self, binding: NativeBindingTransfer, frame: NativeFrameOrigin + ) -> None: + """A matching observation cannot replace the original binding or frame.""" + if self.binding is not binding or self.frame is not frame: + raise ValueError("Native value store has a different original installation") + def require_return(self) -> NativeReturn: if self.continuation is None: raise ValueError("Native store has no proved straight-line return") diff --git a/nominal_refactor_advisor/source_execution.py b/nominal_refactor_advisor/source_execution.py index 92e032cb..a991a257 100644 --- a/nominal_refactor_advisor/source_execution.py +++ b/nominal_refactor_advisor/source_execution.py @@ -528,15 +528,9 @@ def require_descriptor_argument(self) -> None: ) _ = self.native_frame_prefix - def require_native_function_operand(self, value: NativeFunctionValue) -> None: + def require_native_function_operand(self, value: NativeProducedValue) -> None: self.require_descriptor_argument() - if ( - value.creation.instruction_offset - != self.native_execution.require_creation().instruction_offset - ): - raise ValueError( - "Native operand has a different original function creation" - ) + self.native_execution.require_creation().require_definition_operand(value) @dataclass(frozen=True, eq=False) @@ -576,9 +570,14 @@ def require_native_installation( ) -> NativeBindingTransfer: """Join the original native store to its admitted source definition.""" self.require_available_in(prefix) - return self.require_installation_name( - self.native_execution.require_installation() + native = self.native_execution + installation = native.require_installation() + store = self.native_result_store + store.require_original_installation( + installation, native.require_creation().frame ) + self.require_native_function_operand(store.value) + return self.require_installation_name(installation) @property def declaration(self) -> CompactFunctionDeclaration: @@ -961,13 +960,7 @@ def _native_application_path( native = self.creation.native_execution installed = native.require_applied_installation() store = self.production - if ( - store.binding is not installed - or store.frame is not native.require_creation().frame - ): - raise ValueError( - "Decorator store differs from its original native installation" - ) + store.require_original_installation(installed, native.require_creation().frame) raw = store.production_at(native.require_creation().instruction_offset) applications = tuple( application.operand_in(store) diff --git a/tests/test_source_function_installation.py b/tests/test_source_function_installation.py index 8397a50f..2b4bfc59 100644 --- a/tests/test_source_function_installation.py +++ b/tests/test_source_function_installation.py @@ -58,7 +58,11 @@ def test_prepared_root_final_method_joins_canonical_native_installation(): @pytest.mark.native_proof @pytest.mark.parametrize( - "header", ("def chosen():", "async def chosen():", "def __chosen():") + "header", ( + "def chosen():", "async def chosen():", "def __chosen():", + "def chosen(left=0, right=0):", "def chosen(*, option=None):", + "def chosen(first, /, second=0, *, option=None):", + ) ) def test_module_installation_uses_original_source_binding_without_name_conventions( header, @@ -71,6 +75,27 @@ def test_module_installation_uses_original_source_binding_without_name_conventio installation.name == function.node.name == function.definition.target.bound_name ) assert function.require_native_installation(prefix) is installation + store = function.native_result_store + assert store.binding is installation + assert store.frame is function.native_execution.require_creation().frame + function.require_native_function_operand(store.value) + + +@pytest.mark.native_proof +def test_warmed_function_store_cannot_replace_its_original_binding(): + environment, function = module_function() + prefix = environment.required_prefix(environment.entry.context, None) + original = function.require_native_installation(prefix) + store = function.native_result_store + copied = replace(original) + original.require_observation(copied) # An observation is not the original store. + object.__setattr__(store, "binding", copied) + try: + with pytest.raises(ValueError, match="original installation"): + function.require_native_installation(prefix) + finally: + object.__setattr__(store, "binding", original) + assert function.require_native_installation(prefix) is original @pytest.mark.native_proof From bdd0e2198aa4529a5c1f48a18b2e5b10c8d6c1ca Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sun, 1 Nov 2026 08:57:00 -0500 Subject: [PATCH 05/10] Join source defaults to original slots and dispatch operands on their owners --- .../captured_reference.py | 18 ++ nominal_refactor_advisor/lexical_bindings.py | 70 +++++- .../native_compilation.py | 209 +++++++++++------- nominal_refactor_advisor/source_execution.py | 100 +++++++-- tests/test_native_definition_operand_chain.py | 37 +++- tests/test_parameter_source_declarations.py | 43 +--- tests/test_source_default_operand_join.py | 87 ++++++++ 7 files changed, 422 insertions(+), 142 deletions(-) create mode 100644 tests/test_source_default_operand_join.py diff --git a/nominal_refactor_advisor/captured_reference.py b/nominal_refactor_advisor/captured_reference.py index 1145602e..df9dbd21 100644 --- a/nominal_refactor_advisor/captured_reference.py +++ b/nominal_refactor_advisor/captured_reference.py @@ -90,12 +90,14 @@ from .value_expression import LexicalValueReference, TargetResolutionT if TYPE_CHECKING: + from .native_compilation import NativeProducedValue, NativeTupleValue, NativeListValue from .native_call import CallAuthority, NativeDefinitionApplicationAuthorityABC from .native_reference import NativeReferenceEnvironment from .source_entry import SourceModuleEntryPremise from .source_execution import ( SourceDefinitionDecoratorApplicationABC, SourceFunctionActivationABC, + SourceNativeExpressionABC, ) @@ -124,6 +126,22 @@ def __init__(self, evidence: OpenCapturedReference, query: str) -> None: class CapturedReferenceResolution(NativeScalarValueABC): + def require_native_function_operand(self, value: NativeProducedValue) -> None: + self.require_closed() + raise ValueError("Native function requires original source creation evidence") + + def require_native_tuple_operand( + self, value: NativeTupleValue, source: SourceNativeExpressionABC + ) -> None: + self.require_closed() + raise ValueError("Native tuple requires an original source tuple construction") + + def require_native_list_operand( + self, value: NativeListValue, source: SourceNativeExpressionABC + ) -> None: + self.require_closed() + raise ValueError("Native list requires its original literal construction") + def require_dictionary_key(self) -> NativeDictionaryKey: """Scalar contents supply a key; other identities need their own evidence.""" return self.require_native_scalar() diff --git a/nominal_refactor_advisor/lexical_bindings.py b/nominal_refactor_advisor/lexical_bindings.py index 8b376da9..e48e224f 100644 --- a/nominal_refactor_advisor/lexical_bindings.py +++ b/nominal_refactor_advisor/lexical_bindings.py @@ -14,6 +14,7 @@ from enum import StrEnum from functools import cached_property from typing import ( + TYPE_CHECKING, ClassVar, Generic, Self, @@ -27,6 +28,9 @@ ) from .python_module_identity import PythonModulePathIdentity +if TYPE_CHECKING: + from .native_compilation import NativeParameterDefaultOperand, NativeProducedValue + class CompactParameterKind(StrEnum): """Python parameter kinds with their binding behavior on each member.""" @@ -130,11 +134,20 @@ def from_arguments( ) -> tuple[FunctionParameterSource, ...]: positional = (*arguments.posonlyargs, *arguments.args) undefaulted = len(positional) - len(arguments.defaults) - defaults: dict[ast.arg, ast.expr] = dict( - zip(positional[undefaulted:], arguments.defaults, strict=True) - ) + parameters = FunctionArgumentSource.from_arguments(arguments) + by_argument = {parameter.argument: parameter for parameter in parameters} + defaults: dict[ast.arg, DefaultedParameterSource] = { + argument: PositionalDefaultedParameterSource( + argument, by_argument[argument].kind, default + ) + for argument, default in zip( + positional[undefaulted:], arguments.defaults, strict=True + ) + } defaults.update( - (argument, default) + (argument, KeywordDefaultedParameterSource( + argument, by_argument[argument].kind, default + )) for argument, default in zip( arguments.kwonlyargs, arguments.kw_defaults, strict=True ) @@ -142,18 +155,19 @@ def from_arguments( if default is not None ) return tuple( - DefaultedParameterSource( - parameter.argument, parameter.kind, defaults[parameter.argument] - ) + defaults[parameter.argument] if parameter.argument in defaults else UndefaultedParameterSource(parameter.argument, parameter.kind) - for parameter in FunctionArgumentSource.from_arguments(arguments) + for parameter in parameters ) @abstractmethod def answer(self, question: ParameterDefaultQuestion[DefaultAnswerT]) -> DefaultAnswerT: raise NotImplementedError + def declares_default(self, source: DefaultedParameterSource) -> bool: + return False + @dataclass(frozen=True) class DefaultedParameterSource(FunctionParameterSource): @@ -164,6 +178,46 @@ class DefaultedParameterSource(FunctionParameterSource): def answer(self, question: ParameterDefaultQuestion[DefaultAnswerT]) -> DefaultAnswerT: return question.defaulted(self) + def declares_default(self, source: DefaultedParameterSource) -> bool: + return ( + self.argument is source.argument + and self.kind is source.kind + and self.default is source.default + ) + + def native_default_operand( + self, function: NativeProducedValue + ) -> NativeParameterDefaultOperand: + candidates = tuple( + operand + for operand in self._native_default_operands(function) + if operand.parameter_name == self.argument.arg + ) + if len(candidates) != 1: + raise ValueError("Source default has no unique original native slot") + return candidates[0] + + @abstractmethod + def _native_default_operands( + self, function: NativeProducedValue + ) -> tuple[NativeParameterDefaultOperand, ...]: + """Select this source storage shape, not a runtime default object.""" + raise NotImplementedError + + +class PositionalDefaultedParameterSource(DefaultedParameterSource): + def _native_default_operands( + self, function: NativeProducedValue + ) -> tuple[NativeParameterDefaultOperand, ...]: + return function.positional_default_operands() + + +class KeywordDefaultedParameterSource(DefaultedParameterSource): + def _native_default_operands( + self, function: NativeProducedValue + ) -> tuple[NativeParameterDefaultOperand, ...]: + return function.keyword_default_operands() + @dataclass(frozen=True) class UndefaultedParameterSource(FunctionParameterSource): diff --git a/nominal_refactor_advisor/native_compilation.py b/nominal_refactor_advisor/native_compilation.py index 4df9e600..d3018692 100644 --- a/nominal_refactor_advisor/native_compilation.py +++ b/nominal_refactor_advisor/native_compilation.py @@ -412,8 +412,28 @@ def _local_ensure_resolution( raise NotImplementedError +class NativeOperandValueABC(NativeLiteralContentsABC): + """An original operand, including projections that are not instructions.""" + + @abstractmethod + def resolve_operand( + self, resolver: NativeValueResolverABC[NativeResolutionT] + ) -> NativeResolutionT: + raise NotImplementedError + + class NativeValueResolverABC(ABC, Generic[NativeResolutionT]): + def _produced_native_operand_resolution( + self, value: NativeProducedValue + ) -> NativeResolutionT: + raise ValueError("Native original operand/source join remains unproved") + + def _constant_tuple_operand_resolution( + self, value: NativeConstantTupleElement + ) -> NativeResolutionT: + raise ValueError("Native folded constant/source join remains unproved") + def _subscription_native_value_resolution( self, value: NativeSubscriptionValue ) -> NativeResolutionT: @@ -552,7 +572,7 @@ def from_instruction(cls, instruction: dis.Instruction) -> Self: @dataclass(frozen=True) -class NativeProducedValue(DataclassGraphNode, NativeCallSlotABC, NativeLiteralContentsABC): +class NativeProducedValue(DataclassGraphNode, NativeCallSlotABC, NativeOperandValueABC): """One original native production and its inputs, not runtime identity.""" instruction_offset: int @@ -593,6 +613,30 @@ def productions(self) -> tuple[NativeProducedValue, ...]: ) return values + def require_function_creation(self, offset: int) -> NativeFunctionValue: + raise ValueError("Native operand differs from its original function creation") + + def require_definition_application( + self, site: NativeDefinitionApplication + ) -> NativeCallValue: + raise ValueError("Native operand differs from its original application") + + def append_definition_application( + self, store: NativeValueStore, applications: list[NativeCallValue] + ) -> NativeProducedValue: + raise ValueError("Native definition has no application path to storage") + + def class_constructions_for(self, span: SourceByteSpan) -> tuple[NativeCallValue, ...]: + return () + + def require_class_construction( + self, capture: ExactNativeClassCapture, store: NativeValueStore + ) -> NativeCallValue: + raise ValueError("Native class result has no original construction call") + + def require_class_builder(self, site: NativeCaptureSite) -> None: + raise ValueError("Native class call differs from its original builder load") + def require_scalar_store_value(self) -> NativeConstantValue: """A produced type or read alone does not establish scalar contents.""" raise ValueError("Native scalar production remains unproved") @@ -609,7 +653,7 @@ def name_tuple(self) -> tuple[str, ...]: return cast(tuple[str, ...], names) @property - def tuple_elements(self) -> tuple[NativeLiteralContentsABC, ...]: + def tuple_elements(self) -> tuple[NativeOperandValueABC, ...]: raise ValueError("Native original tuple elements remain unproved") @property @@ -635,6 +679,11 @@ def positional_default_operands(self) -> tuple[NativePositionalDefaultOperand, . def keyword_default_operands(self) -> tuple[NativeKeywordDefaultOperand, ...]: raise ValueError("Native keyword default association remains unproved") + def resolve_operand( + self, resolver: NativeValueResolverABC[NativeResolutionT] + ) -> NativeResolutionT: + return resolver._produced_native_operand_resolution(self) + def resolve( self, resolver: NativeValueResolverABC[NativeResolutionT] ) -> NativeResolutionT: @@ -663,6 +712,11 @@ class NativeFunctionValue(DataclassGraphValue, NativeTypedValue): def creation(self) -> NativeFunctionValue: return self + def require_function_creation(self, offset: int) -> NativeFunctionValue: + if self.creation.instruction_offset != offset: + raise ValueError("Native operand differs from its original function creation") + return self + def positional_default_operands(self) -> tuple[NativePositionalDefaultOperand, ...]: """Require original birth storage; absent metadata is not an empty proof.""" storage = self.attribute_operand( @@ -702,18 +756,25 @@ class NativeParameterDefaultOperand(ABC): @property @abstractmethod - def value(self) -> NativeLiteralContentsABC: + def value(self) -> NativeOperandValueABC: raise NotImplementedError + def require_projection(self, value: NativeConstantTupleElement) -> None: + raise ValueError("Native default has no original folded tuple projection") + @dataclass(frozen=True, eq=False) class NativePositionalDefaultOperand(NativeParameterDefaultOperand): index: int @property - def value(self) -> NativeLiteralContentsABC: + def value(self) -> NativeOperandValueABC: return self.storage.tuple_elements[self.index] + def require_projection(self, value: NativeConstantTupleElement) -> None: + if value.storage is not self.storage or value.index != self.index: + raise ValueError("Folded default differs from its original storage slot") + @dataclass(frozen=True, eq=False) class NativeKeywordDefaultOperand(NativeParameterDefaultOperand): @@ -766,6 +827,49 @@ class NativeCallValue(DataclassGraphValue, NativeProducedValue): argument_slot: NativeCallSlotABC + def require_definition_application( + self, site: NativeDefinitionApplication + ) -> NativeCallValue: + if ( + self.instruction_offset != site.instruction_offset + or self.source_span != site.source_span + ): + raise ValueError("Native operand differs from its original application") + return self + + def append_definition_application( + self, store: NativeValueStore, applications: list[NativeCallValue] + ) -> NativeProducedValue: + argument = self.require_definition_argument() + for operand in self.inputs: + store.require_value(operand) + if operand.instruction_offset >= self.instruction_offset: + raise ValueError("Native application must follow its operands") + applications.append(self) + return argument + + def class_constructions_for(self, span: SourceByteSpan) -> tuple[NativeCallValue, ...]: + return (self,) if self.source_span == span else () + + def require_class_construction( + self, capture: ExactNativeClassCapture, store: NativeValueStore + ) -> NativeCallValue: + store.require_value(self) + if ( + store.frame is not capture.builder.frame + or capture.creation.frame is not store.frame + ): + raise ValueError("Native class result belongs to a different creator frame") + if self.source_span != capture.source_span: + raise ValueError("Native class result has no original construction call") + if self.argument_slot.implicit_arguments or len(self.positional_arguments) < 2: + raise ValueError("Native class construction requires its explicit body and name") + for operand in self.productions(): + store.require_value(operand) + self.callee.require_class_builder(capture.builder) + capture.creation.require_definition_operand(self.arguments[0]) + return self + @property def callee(self) -> NativeProducedValue: return self.inputs[0] @@ -938,7 +1042,7 @@ class NativeTupleValue(NativeSequenceValue): declaration: ClassVar[NativeTypeDeclaration] = NativeTypeDeclaration(tuple) @property - def tuple_elements(self) -> tuple[NativeLiteralContentsABC, ...]: + def tuple_elements(self) -> tuple[NativeOperandValueABC, ...]: return self.inputs def resolve( @@ -964,7 +1068,7 @@ def constant_tuple_contents(self) -> tuple[object, ...]: return cast(tuple[object, ...], self.value) @property - def tuple_elements(self) -> tuple[NativeLiteralContentsABC, ...]: + def tuple_elements(self) -> tuple[NativeOperandValueABC, ...]: return tuple( NativeConstantTupleElement(self, index) for index in range(len(self.constant_tuple_contents)) @@ -983,7 +1087,7 @@ def _native_scalar_value(self) -> object: @dataclass(frozen=True, eq=False) -class NativeConstantTupleElement(NativeConstantContentsABC): +class NativeConstantTupleElement(NativeConstantContentsABC, NativeOperandValueABC): """A projection of an original constant, with no invented instruction offset.""" storage: NativeConstantValue @@ -992,6 +1096,11 @@ class NativeConstantTupleElement(NativeConstantContentsABC): def _native_constant_value(self) -> object: return self.storage.constant_tuple_contents[self.index] + def resolve_operand( + self, resolver: NativeValueResolverABC[NativeResolutionT] + ) -> NativeResolutionT: + return resolver._constant_tuple_operand_resolution(self) + @dataclass(frozen=True) class NativeDictionaryValue(NativeTypedValue): @@ -1133,6 +1242,10 @@ class NativeClassBuilderValue(NativeProducedValue): inputs: ClassVar[tuple[NativeProducedValue, ...]] = () + def require_class_builder(self, site: NativeCaptureSite) -> None: + if self.instruction_offset != site.instruction_offset: + raise ValueError("Native class call differs from its original builder load") + @dataclass(frozen=True) class NativeReadValue(NativeProducedValue): @@ -2133,14 +2246,8 @@ class NativeCaptureSite: frame: NativeFrameOrigin instruction_offset: int - def require_definition_operand(self, value: NativeProducedValue) -> None: - if ( - not isinstance(value, NativeFunctionValue) - or value.creation.instruction_offset != self.instruction_offset - ): - raise ValueError( - "Native operand differs from its original function creation" - ) + def require_definition_operand(self, value: NativeProducedValue) -> NativeProducedValue: + return value.require_function_creation(self.instruction_offset) @dataclass(frozen=True, eq=False) @@ -2156,20 +2263,14 @@ class NativeDefinitionApplication(DataclassGraphValue, NativeCaptureSite): prelude: tuple[NativeCaptureSite, ...] argument: NativeCaptureSite - def require_definition_operand(self, value: NativeProducedValue) -> None: - if ( - not isinstance(value, NativeCallValue) - or value.instruction_offset != self.instruction_offset - or value.source_span != self.source_span - ): - raise ValueError("Native operand differs from its original application") + def require_definition_operand(self, value: NativeProducedValue) -> NativeCallValue: + return value.require_definition_application(self) def operand_in(self, store: NativeValueStore) -> NativeCallValue: if store.frame is not self.frame or self.argument.frame is not self.frame: raise ValueError("Native application operand belongs to a different frame") value = store.production_at(self.instruction_offset) - self.require_definition_operand(value) - call = cast(NativeCallValue, value) + call = self.require_definition_operand(value) argument = call.require_definition_argument() for operand in call.inputs: store.require_value(operand) @@ -2207,7 +2308,7 @@ def creation(self) -> NativeCaptureSite: def construction_in(self, store: NativeValueStore) -> NativeCallValue: """Require direct raw construction as the stored result, without decorators.""" - return self._require_construction_value(store, store.value) + return store.value.require_class_construction(self, store) def definition_construction_in(self, store: NativeValueStore) -> NativeCallValue: """Join raw construction to the stored definition's original application chain. @@ -2216,48 +2317,16 @@ def definition_construction_in(self, store: NativeValueStore) -> NativeCallValue behavior or runtime class identity follows from this compiler relation. """ candidates = tuple( - value + call for value in store.values - if isinstance(value, NativeCallValue) - and value.source_span == self.source_span + for call in value.class_constructions_for(self.source_span) ) if len(candidates) != 1: raise ValueError("Native definition has no unique class construction") - value = self._require_construction_value(store, candidates[0]) + value = candidates[0].require_class_construction(self, store) store.applications_after(value) return value - def _require_construction_value( - self, store: NativeValueStore, value: NativeProducedValue - ) -> NativeCallValue: - store.require_value(value) - if ( - store.frame is not self.builder.frame - or self.creation.frame is not store.frame - ): - raise ValueError("Native class result belongs to a different creator frame") - if ( - not isinstance(value, NativeCallValue) - or value.source_span != self.source_span - ): - raise ValueError("Native class result has no original construction call") - if ( - value.argument_slot.implicit_arguments - or len(value.positional_arguments) < 2 - ): - raise ValueError( - "Native class construction requires its explicit body and name" - ) - for operand in value.productions(): - store.require_value(operand) - if ( - not isinstance(value.callee, NativeClassBuilderValue) - or value.callee.instruction_offset != self.builder.instruction_offset - ): - raise ValueError("Native class call differs from its original builder load") - self.creation.require_definition_operand(value.arguments[0]) - return value - def prologue_return_operand( self, receipt: NativeReturn, value: NativeProducedValue ) -> NativeProducedValue: @@ -2453,18 +2522,10 @@ def applications_after( """ self.require_value(creation) current = self.value - applications = [] + applications: list[NativeCallValue] = [] while current is not creation: self.require_value(current) - if not isinstance(current, NativeCallValue): - raise ValueError("Native definition has no application path to storage") - argument = current.require_definition_argument() - for operand in current.inputs: - self.require_value(operand) - if operand.instruction_offset >= current.instruction_offset: - raise ValueError("Native application must follow its operands") - applications.append(current) - current = argument + current = current.append_definition_application(self, applications) return tuple(reversed(applications)) @property @@ -3527,13 +3588,7 @@ def capture_operands( ) -> None: inputs = stack.pop(2) function = inputs[-1] - if ( - not isinstance(function, NativeFunctionValue) - or function.creation.instruction_offset != emission.creation.offset - ): - raise ValueError( - "Native function attachment has a different original function" - ) + function.require_function_creation(emission.creation.offset) stack.stack.append( stack.emit( NativeFunctionAttributeValue( diff --git a/nominal_refactor_advisor/source_execution.py b/nominal_refactor_advisor/source_execution.py index a991a257..6881220e 100644 --- a/nominal_refactor_advisor/source_execution.py +++ b/nominal_refactor_advisor/source_execution.py @@ -89,6 +89,8 @@ NativeClassCaptureResolverABC, NativeClassPrologueResolverABC, NativeConstantStore, + NativeConstantTupleElement, + NativeParameterDefaultOperand, NativeDefinitionApplication, NativeCompletionResolverABC, NativeCreationBackend, @@ -1645,6 +1647,17 @@ def elements(self) -> tuple[CapturedReferenceResolution, ...]: value.require_closed() return values + def require_native_tuple_operand( + self, value: NativeTupleValue, source: SourceNativeExpressionABC + ) -> None: + inputs = self.production.inputs + if len(inputs) != len(value.inputs): + raise ValueError("Native tuple has different original source operands") + for use, operand in zip(inputs, value.inputs, strict=True): + SourceNativeOperandJoin( + source, CompactFlowValue(source.source_read.context, use) + ).require_join(operand) + def require_closed(self) -> None: _ = self.production self.execution.required_prefix(self.read.context, self.read.use.position) @@ -1684,6 +1697,13 @@ class SourceLiteralCapture(SourceReadCaptureABC): Release follows the native exact-type contract; other protocols stay open. """ + def require_native_list_operand( + self, value: NativeListValue, source: SourceNativeExpressionABC + ) -> None: + for operand in value.productions(): + source._require_native_production(operand) + value.require_literal_contents(self.literal_value) + def _native_scalar_value(self) -> object: return self.literal_value @@ -1983,6 +2003,11 @@ class SourceNativeExpressionABC( native_bindings: ClassVar[tuple[NativeBindingTransfer, ...]] = () + def _produced_native_operand_resolution( + self, value: NativeProducedValue + ) -> CapturedReferenceResolution: + return self.require_join(value) + def _call_native_value_resolution( self, value: NativeCallValue ) -> CapturedReferenceResolution: @@ -2075,28 +2100,14 @@ def _tuple_native_value_resolution( self, value: NativeTupleValue ) -> CapturedReferenceResolution: source = self.source_value - if not isinstance(source, SourceTupleCapture): - raise ValueError( - "Native tuple requires an original source tuple construction" - ) - inputs = source.production.inputs - if len(inputs) != len(value.inputs): - raise ValueError("Native tuple has different original source operands") - for use, operand in zip(inputs, value.inputs, strict=True): - SourceNativeOperandJoin( - self, CompactFlowValue(self.source_read.context, use) - ).require_join(operand) + source.require_native_tuple_operand(value, self) return source def _list_native_value_resolution( self, value: NativeListValue ) -> CapturedReferenceResolution: source = self.source_value - if not isinstance(source, SourceLiteralCapture): - raise ValueError("Native list requires its original literal construction") - for operand in value.productions(): - self._require_native_production(operand) - value.require_literal_contents(source.literal_value) + source.require_native_list_operand(value, self) return source def _empty_dictionary_native_value_resolution( @@ -2111,10 +2122,6 @@ def _function_native_value_resolution( value: NativeFunctionValue, ) -> CapturedReferenceResolution: source = self.source_value - if not isinstance(source, SourceFunctionCreationABC): - raise ValueError( - "Native function requires original source creation evidence" - ) source.require_native_function_operand(value) return source @@ -2149,6 +2156,59 @@ def _require_native_production(self, value: NativeProducedValue) -> None: self.parent._require_native_production(value) +@dataclass(frozen=True, eq=False) +class SourceFunctionDefault(SourceNativeExpressionABC): + """An original header read and metadata slot, not runtime-object transport.""" + + function: SourceCreatedFunctionCapture + parameter: DefaultedParameterSource + prefix: AdmittedExecutionPrefixABC + + execution = AliasProperty["SourceExecutionABC"]("function.execution") + source_completion_prefix = AliasProperty[AdmittedExecutionPrefixABC]("prefix") + + @property + def source_read(self) -> CompactFlowValue: + self.function.require_original_operation() + parameters = FunctionParameterSource.from_arguments(self.function.node.args) + if not any(source.declares_default(self.parameter) for source in parameters): + raise ValueError("Default does not belong to the original function header") + read = self.execution.source.value_reads_by_node.get(self.parameter.default) + if read is None or read.context is not self.function.parent_context: + raise ValueError("Default has no original creation-frame read") + self.execution.source.value_operation(read) + return read + + @property + def store(self) -> NativeValueStore: + _ = self.source_read + self.function.require_native_installation(self.prefix) + return self.function.native_result_store + + @property + def operand(self) -> NativeParameterDefaultOperand: + store = self.store + operand = self.parameter.native_default_operand(store.value) + store.require_value(operand.storage) + return operand + + def _require_native_production(self, value: NativeProducedValue) -> None: + self.store.require_value(value) + + def _constant_tuple_operand_resolution( + self, value: NativeConstantTupleElement + ) -> CapturedReferenceResolution: + self.operand.require_projection(value) + self._require_native_production(value.storage) + source = self.source_value + source.require_closed() + source.require_same_native_constant(value) + return source + + def result(self) -> CapturedReferenceResolution: + return self.operand.value.resolve_operand(self) + + class SourceStackEffectReturnABC(SourceEventReturnABC, SourceNativeExpressionABC): """A source operation completed by an original consuming native transfer.""" diff --git a/tests/test_native_definition_operand_chain.py b/tests/test_native_definition_operand_chain.py index 41ab0ac4..abe4bb2a 100644 --- a/tests/test_native_definition_operand_chain.py +++ b/tests/test_native_definition_operand_chain.py @@ -3,13 +3,16 @@ import ast from copy import copy import pickle +from types import FunctionType import pytest from nominal_refactor_advisor.native_compilation import ( NativeCallMarker, NativePythonCompilation, + NativeTypedValue, ) +from nominal_refactor_advisor.native_declarations import NativeTypeDeclaration from nominal_refactor_advisor.source_geometry import SourceByteSpan from test_source_function_result import execution @@ -147,17 +150,13 @@ def test_application_chain_rejects_nonoriginal_or_nonimplicit_links(damage): store.applications_after(raw) -def test_operand_chain_survives_snapshot_without_recompilation(monkeypatch): +def test_operand_chain_survives_snapshot_with_original_links(): compilation, node, store = observed("@first\n@second\nclass Target: pass\n") span = SourceByteSpan.require_node(node) raw = compilation.class_capture_for(span).definition_construction_in(store) assert len(store.applications_after(raw)) == 2 payload = pickle.dumps(compilation) - def forbid(self): - raise AssertionError("A persisted original operand graph must not recompile") - - monkeypatch.setattr(NativePythonCompilation, "compile", forbid) restored = pickle.loads(payload) restored_store = restored.value_store_for(store.production_span, span, node.name) restored_raw = restored.class_capture_for(span).definition_construction_in( @@ -184,3 +183,31 @@ def test_source_decorator_proof_preserves_the_distinct_compiler_chain(): raw = compilation.class_capture_for(span).definition_construction_in(store) assert len(store.applications_after(raw)) == 1 environment.class_entry(node).result().require_closed() + + +def test_function_and_application_sites_reject_each_others_owned_operands(): + compilation, node, store = observed("@decorate\ndef Target(): pass\n") + native = compilation.execution_for(SourceByteSpan.require_node(node)) + creation = native.require_creation() + raw = store.production_at(creation.instruction_offset) + (application,) = native.require_applications() + call = application.operand_in(store) + assert creation.require_definition_operand(raw) is raw + assert application.require_definition_operand(call) is call + with pytest.raises(ValueError, match="original function creation"): + creation.require_definition_operand(call) + with pytest.raises(ValueError, match="original application"): + application.require_definition_operand(raw) + + +def test_function_type_alone_does_not_supply_original_creation_membership(): + compilation, node, store = observed("def Target(): pass\n") + creation = compilation.execution_for(SourceByteSpan.require_node(node)).require_creation() + raw = store.production_at(creation.instruction_offset) + typed_only = NativeTypedValue( + raw.instruction_offset, raw.inputs, NativeTypeDeclaration(FunctionType), + source_span=raw.source_span, + ) + assert typed_only.native_type is FunctionType + with pytest.raises(ValueError, match="original function creation"): + creation.require_definition_operand(typed_only) diff --git a/tests/test_parameter_source_declarations.py b/tests/test_parameter_source_declarations.py index e215edec..c66b0c6f 100644 --- a/tests/test_parameter_source_declarations.py +++ b/tests/test_parameter_source_declarations.py @@ -16,14 +16,20 @@ CompactParameterKind, DefaultedParameterSource, FunctionParameterSource, + ParameterDefaultQuestion, UndefaultedParameterSource, ) -def _declared_default(source: FunctionParameterSource) -> ast.expr | None: - """The default node a source declares, None for the undefaulted subtype.""" +class DeclaredDefaultNode(ParameterDefaultQuestion[ast.expr | None]): + def defaulted(self, source: DefaultedParameterSource) -> ast.expr: + return source.default - return source.default if isinstance(source, DefaultedParameterSource) else None + def undefaulted(self, source: UndefaultedParameterSource) -> None: + return None + + +DEFAULT_NODE = DeclaredDefaultNode() def _arguments(source: str) -> ast.arguments: @@ -73,7 +79,7 @@ def test_positional_default_cutoff_retains_exact_nodes( expected_default = ( None if index < default_start else arguments.defaults[index - default_start] ) - assert _declared_default(source) is expected_default + assert source.answer(DEFAULT_NODE) is expected_default def test_variadics_and_keyword_only_defaults_keep_source_identity() -> None: @@ -112,7 +118,7 @@ def test_variadics_and_keyword_only_defaults_keep_source_identity() -> None: assert source.argument is argument assert source.argument.annotation is argument.annotation assert source.kind is kind - assert _declared_default(source) is default + assert source.answer(DEFAULT_NODE) is default required, optional = sources[3:5] assert isinstance(required, UndefaultedParameterSource) assert isinstance(optional, DefaultedParameterSource) @@ -148,33 +154,6 @@ def test_empty_parameter_list_has_empty_source_and_compact_views() -> None: assert CompactFunctionSignature.from_arguments(arguments).parameters == () -def test_compact_signature_consumes_the_shared_source_projection( - monkeypatch: pytest.MonkeyPatch, -) -> None: - arguments = _arguments("def sample(first: int, second: str=None): pass") - selected = DefaultedParameterSource( - arguments.args[1], CompactParameterKind.KEYWORD_ONLY, arguments.defaults[0] - ) - requests = [] - - def project(cls, requested): - assert cls is FunctionParameterSource - requests.append(requested) - return (selected,) - - monkeypatch.setattr(FunctionParameterSource, "from_arguments", classmethod(project)) - compact = CompactFunctionSignature.from_arguments(arguments) - assert len(requests) == 1 - assert requests[0] is arguments - (parameter,) = compact.parameters - assert parameter.name == selected.argument.arg - assert parameter.kind is selected.kind - assert isinstance(parameter, DefaultedFunctionParameter) - assert compact.annotations_by_name == { - parameter.name: ast.unparse(selected.argument.annotation) - } - - def test_source_receipt_is_frozen_but_owns_original_syntax() -> None: arguments = _arguments("def sample(value=None): pass") (source,) = FunctionParameterSource.from_arguments(arguments) diff --git a/tests/test_source_default_operand_join.py b/tests/test_source_default_operand_join.py new file mode 100644 index 00000000..35a627da --- /dev/null +++ b/tests/test_source_default_operand_join.py @@ -0,0 +1,87 @@ +"""Original header/slot joins do not imply current storage or relocation.""" + +from copy import copy +from typing import cast + +import pytest + +from nominal_refactor_advisor.lexical_bindings import ( + DefaultedParameterSource, + FunctionParameterSource, +) +from nominal_refactor_advisor.native_compilation import ( + NativeConstantTupleElement, + NativeConstantValue, +) +from nominal_refactor_advisor.source_execution import SourceFunctionDefault +from test_source_call_binding import source_call +from test_source_function_installation import module_function + +pytestmark = pytest.mark.native_proof + + +def joined_default(header: str, name: str) -> SourceFunctionDefault: + environment, function = module_function(f"def chosen({header}): pass\n") + source = next( + source + for source in FunctionParameterSource.from_arguments(function.node.args) + if source.argument.arg == name + ) + assert isinstance(source, DefaultedParameterSource) + return SourceFunctionDefault( + function, source, environment.required_prefix(environment.entry.context, None) + ) + + +@pytest.mark.parametrize( + "header,name,expected", + ( + ("left=0, right=0", "left", 0), + ("left=0, right=0", "right", 0), + ("*, option=None", "option", None), + ("first, /, second=0, *, option=None", "second", 0), + ("first, /, second=0, *, option=None", "option", None), + ("value=(0, True)", "value", (0, True)), + ), +) +def test_original_default_joins_its_source_read_and_native_slot( + header: str, name: str, expected: object +) -> None: + join = joined_default(header, name) + result = join.result() + result.require_constant_contents(expected) + assert join.operand.parameter_name == name + join.store.require_value(join.operand.storage) + # A matching endpoint is not the admitted original cut. + with pytest.raises(ValueError): + SourceFunctionDefault(join.function, join.parameter, copy(join.prefix)).result() + + +def test_equal_contents_do_not_join_a_different_positional_slot() -> None: + join = joined_default("left=0, right=0", "left") + projection = NativeConstantTupleElement(cast(NativeConstantValue, join.operand.storage), 1) + with pytest.raises(ValueError, match="original storage slot"): + projection.resolve_operand(join) + + +def test_matching_foreign_syntax_does_not_supply_the_original_default() -> None: + original = joined_default("value=0", "value") + foreign = joined_default("value=0", "value") + with pytest.raises(ValueError, match="original function header"): + SourceFunctionDefault( + original.function, foreign.parameter, original.prefix + ).result() + + +def test_new_join_does_not_replace_a_broader_existing_parameter_entry() -> None: + _, _, authority = source_call("value=0.5", "chosen()") + assert authority.initial_entries["value"].native_type is float + # Existing lifetime refusal is independent of source parameter entry. + with pytest.raises(ValueError, match="Native instance lifetime remains unproved"): + authority.require_closed() + source = FunctionParameterSource.from_arguments(authority.callee.node.args)[0] + assert isinstance(source, DefaultedParameterSource) + with pytest.raises(ValueError, match="Native original tuple elements remain unproved"): + SourceFunctionDefault( + authority.callee, source, authority.caller_prefix + ).result() From 526e90b8de94e52971a6ee4c8b6d33a1b60d17cb Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sun, 1 Nov 2026 09:45:59 -0500 Subject: [PATCH 06/10] Let compact values own their tuple production role --- nominal_refactor_advisor/product_flow.py | 6 ++++++ nominal_refactor_advisor/source_execution.py | 11 ++++------- nominal_refactor_advisor/value_expression.py | 9 +++++++++ tests/test_source_tuple_capture.py | 15 ++++++++++++++- 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/nominal_refactor_advisor/product_flow.py b/nominal_refactor_advisor/product_flow.py index c61f3153..58033b3c 100644 --- a/nominal_refactor_advisor/product_flow.py +++ b/nominal_refactor_advisor/product_flow.py @@ -2307,6 +2307,9 @@ class CompactTupleValue( inputs: tuple[CompactValueUse, ...] + def require_tuple_production(self) -> CompactTupleValue: + return self + def resolve_value( self, resolver: CompactValueResolverABC[ResolutionContextT, TargetResolutionT], @@ -2403,6 +2406,9 @@ class CompactValueUse(DataclassGraphValue, CompactPositionedReference): "value.lexical_reference" ) + def require_tuple_production(self) -> CompactTupleValue: + return self.value.require_tuple_production() + def resolve_value( self, resolver: CompactValueResolverABC[ResolutionContextT, TargetResolutionT], diff --git a/nominal_refactor_advisor/source_execution.py b/nominal_refactor_advisor/source_execution.py index 6881220e..afa04a7d 100644 --- a/nominal_refactor_advisor/source_execution.py +++ b/nominal_refactor_advisor/source_execution.py @@ -1613,18 +1613,15 @@ def _native_constant_value(self) -> object: def production(self) -> CompactTupleValue: operation = self.execution.source.value_operation(self.read) use = self.read.use - if not isinstance(use, CompactValueUse) or not isinstance( - use.value, CompactTupleValue - ): - raise ValueError("Tuple capture requires its original compact production") + production = use.require_tuple_production() if use.position.branch_path or use.position.evaluation_path: raise ValueError("Conditional or repeated tuple creation remains unproved") node = operation.node NativeCreationBackend.current().require_tuple_construction(node) - if len(use.value.inputs) != len(node.elts): + if len(production.inputs) != len(node.elts): raise ValueError("Tuple production has a different input count") previous = None - for element, input_use in zip(node.elts, use.value.inputs, strict=True): + for element, input_use in zip(node.elts, production.inputs, strict=True): original = self.execution.source.value_operation( CompactFlowValue(self.read.context, input_use) ) @@ -1635,7 +1632,7 @@ def production(self) -> CompactTupleValue: ): raise ValueError("Tuple inputs have no original ordered completion") previous = input_use.position - return use.value + return production @cached_property def elements(self) -> tuple[CapturedReferenceResolution, ...]: diff --git a/nominal_refactor_advisor/value_expression.py b/nominal_refactor_advisor/value_expression.py index 71a1772e..8711076c 100644 --- a/nominal_refactor_advisor/value_expression.py +++ b/nominal_refactor_advisor/value_expression.py @@ -12,6 +12,7 @@ from dataclasses import dataclass from functools import singledispatch from typing import ( + TYPE_CHECKING, Generic, Self, TypeAlias, @@ -19,6 +20,10 @@ cast, ) +if TYPE_CHECKING: + from .product_flow import CompactTupleValue + + ValueExpressionNode: TypeAlias = ast.expr | ast.Slice WriteT = TypeVar("WriteT") @@ -102,6 +107,10 @@ def _empty_dictionary_resolution( class ValueExpressionShapeABC(ABC): """Retained lexical shape, independent of interpreter requirements.""" + def require_tuple_production(self) -> CompactTupleValue: + """Tuple role alone supplies no occurrence, ordering or runtime identity.""" + raise ValueError("Tuple capture requires its original compact production") + @property @abstractmethod def lexical_reference(self) -> LexicalValueReference | None: diff --git a/tests/test_source_tuple_capture.py b/tests/test_source_tuple_capture.py index cab9dbb2..3ab932dc 100644 --- a/tests/test_source_tuple_capture.py +++ b/tests/test_source_tuple_capture.py @@ -63,7 +63,9 @@ def test_compact_tuple_retains_actual_ordered_element_productions(): environment = execution(BASE + "held = (Alpha, Beta)\n") node = tuple_node(environment) read = environment.source.value_reads_by_node[node] - production = read.use.value + production = read.use.require_tuple_production() + assert production is read.use.value + assert production.require_tuple_production() is production assert isinstance(production, CompactTupleValue) assert len(production.inputs) == 2 for element, use in zip(node.elts, production.inputs, strict=True): @@ -75,6 +77,17 @@ def test_compact_tuple_retains_actual_ordered_element_productions(): assert production.resolve_value(ConservativeResolver(), unknown) is unknown +@pytest.mark.parametrize("expression", ("0", "object")) +def test_non_tuple_use_and_payload_do_not_supply_tuple_production(expression): + environment = execution(f"held={expression}\n") + node = environment.module.module.body[-1].value + read = environment.source.value_reads_by_node[node] + with pytest.raises(ValueError, match="original compact production"): + read.use.require_tuple_production() + with pytest.raises(ValueError): + SourceTupleCapture(environment, read, frozenset()).require_closed() + + @pytest.mark.native_proof def test_source_tuple_elements_keep_actual_original_class_creation_owners(): source = BASE + "held = (Alpha, Beta)\nalias = held\n" From 6cf5a21eef80664b9cafc37eae05783190d225d5 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sun, 1 Nov 2026 10:31:08 -0500 Subject: [PATCH 07/10] Require declared native birth storage for original defaults --- .../native_compilation.py | 43 +++++++++++++++++-- nominal_refactor_advisor/source_execution.py | 14 ++++++ tests/test_source_default_operand_join.py | 29 +++++++++++++ 3 files changed, 82 insertions(+), 4 deletions(-) diff --git a/nominal_refactor_advisor/native_compilation.py b/nominal_refactor_advisor/native_compilation.py index d3018692..ab1e4c47 100644 --- a/nominal_refactor_advisor/native_compilation.py +++ b/nominal_refactor_advisor/native_compilation.py @@ -3795,6 +3795,12 @@ def require_fresh_function_namespace( """Conditional empty custom storage at raw native function creation only.""" raise ValueError("Native fresh function namespace remains unproved") + def require_function_default_storage( + self, compilation: NativeCompilationIdentity + ) -> None: + """Conditional native retention of original birth operands, not later storage.""" + raise ValueError("Native function birth-default storage remains unproved") + def require_nonabstract_member_type(self, value_type: type) -> None: """Require inert absence of ABCMeta's marker in the native type lookup. @@ -4561,17 +4567,33 @@ class CPythonFunctionConstruction(NativeCreationBackend): This is conditional on noninterfering external instrumentation and native API compliance; source activation must supply that execution restriction. Invoked source/dependency callbacks remain separate operation obligations. + 3.11 MAKE_FUNCTION transfers the popped defaults tuple/dictionary directly + into func_defaults/func_kwdefaults. In 3.14 SET_FUNCTION_ATTRIBUTE transfers + the same operand pointer through _Py_FunctionAttributeOffsets (ceval.c). + The original compiler operand chain owns the selected attachment and slot; + this law introduces no field table or value reconstruction. The create + watcher runs before attachments, so external noninterference is essential. No later attribute state, abstractness lookup or callable behavior is proved. """ - def require_fresh_function_namespace( + def _require_actual_function_compilation( self, compilation: NativeCompilationIdentity ) -> None: if compilation.interpreter != InterpreterBuild.running() or self.compiler_identity != CompilerIdentity.running(): raise ValueError( - "Fresh function namespace requires its actual native interpreter backend" + "Native function birth requires its actual native interpreter backend" ) + def require_fresh_function_namespace( + self, compilation: NativeCompilationIdentity + ) -> None: + self._require_actual_function_compilation(compilation) + + def require_function_default_storage( + self, compilation: NativeCompilationIdentity + ) -> None: + self._require_actual_function_compilation(compilation) + class CPythonStaticTypeRequirement(IntEnum): """Native flag requirements for supported CPython static type lifetime.""" @@ -5148,6 +5170,13 @@ def scalar_store_for( production_span, target_span, name ) + def require_original_function_creation( + self, receipt: NativeFunctionExecution + ) -> None: + """Authenticate a compiler creation record, not an observed runtime birth.""" + self.execution_outcome.require_function(receipt) + receipt.require_creation() + def require_fresh_function_namespace( self, receipt: NativeFunctionExecution ) -> None: @@ -5155,10 +5184,16 @@ def require_fresh_function_namespace( # Conditional initialization is not an observed target function or continued # emptiness through decorators, later writes or callbacks. Runtime activation # and noninterference remain the source execution owner's duties. - self.execution_outcome.require_function(receipt) - receipt.require_creation() + self.require_original_function_creation(receipt) NativeCreationBackend.current().require_fresh_function_namespace(self.identity) + def require_function_default_storage( + self, receipt: NativeFunctionExecution + ) -> None: + """Retain original birth operands; source admission/noninterference are separate.""" + self.require_original_function_creation(receipt) + NativeCreationBackend.current().require_function_default_storage(self.identity) + def constant_store_for( self, source_span: SourceByteSpan, name: str ) -> NativeConstantStore: diff --git a/nominal_refactor_advisor/source_execution.py b/nominal_refactor_advisor/source_execution.py index afa04a7d..53dd502d 100644 --- a/nominal_refactor_advisor/source_execution.py +++ b/nominal_refactor_advisor/source_execution.py @@ -2189,6 +2189,20 @@ def operand(self) -> NativeParameterDefaultOperand: store.require_value(operand.storage) return operand + def require_birth_storage(self) -> NativeParameterDefaultOperand: + """Retain this original birth slot, not current storage or relocated objects. + + The weaker source/slot query remains available without upgrading its + supplied entry premise. This stronger query requires noninterference + because native create watchers can see the function before attachment. + """ + operand = self.operand + self.execution.entry.require_external_noninterference(self.prefix) + self.execution.module.native_compilation.require_function_default_storage( + self.function.native_execution + ) + return operand + def _require_native_production(self, value: NativeProducedValue) -> None: self.store.require_value(value) diff --git a/tests/test_source_default_operand_join.py b/tests/test_source_default_operand_join.py index 35a627da..0c2b4a86 100644 --- a/tests/test_source_default_operand_join.py +++ b/tests/test_source_default_operand_join.py @@ -16,6 +16,7 @@ from nominal_refactor_advisor.source_execution import SourceFunctionDefault from test_source_call_binding import source_call from test_source_function_installation import module_function +from test_source_function_storage import SOURCE, prepared_function pytestmark = pytest.mark.native_proof @@ -57,6 +58,34 @@ def test_original_default_joins_its_source_read_and_native_slot( SourceFunctionDefault(join.function, join.parameter, copy(join.prefix)).result() +@pytest.mark.parametrize("default", ("value=0", "*, value=None")) +def test_birth_storage_requires_the_admitted_original_creation(default: str) -> None: + environment, entry, function = prepared_function( + source=SOURCE.replace("value=1", default) + ) + parameter = next( + source + for source in FunctionParameterSource.from_arguments(function.node.args) + if source.argument.arg == "value" + ) + assert isinstance(parameter, DefaultedParameterSource) + join = SourceFunctionDefault(function, parameter, entry.completion_prefix) + operand = join.require_birth_storage() + assert operand.storage is join.operand.storage + assert operand.parameter_name == "value" + with pytest.raises(ValueError): + SourceFunctionDefault(function, parameter, copy(join.prefix)).require_birth_storage() + + +def test_slot_information_does_not_silently_declare_noninterference() -> None: + join = joined_default("value=0", "value") + join.result().require_constant_contents(0) + with pytest.raises(ValueError, match="External source interference remains unproved"): + join.require_birth_storage() + # Refusing the stronger question does not destroy the weaker information. + join.result().require_constant_contents(0) + + def test_equal_contents_do_not_join_a_different_positional_slot() -> None: join = joined_default("left=0, right=0", "left") projection = NativeConstantTupleElement(cast(NativeConstantValue, join.operand.storage), 1) From e988ea58a184d352c0fd6e8f5ffdbe82f327aefb Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sun, 1 Nov 2026 10:38:34 -0500 Subject: [PATCH 08/10] Share original function creation authentication across source consumers --- nominal_refactor_advisor/native_compilation.py | 4 ++-- nominal_refactor_advisor/source_execution.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/nominal_refactor_advisor/native_compilation.py b/nominal_refactor_advisor/native_compilation.py index ab1e4c47..e51357bb 100644 --- a/nominal_refactor_advisor/native_compilation.py +++ b/nominal_refactor_advisor/native_compilation.py @@ -5172,10 +5172,10 @@ def scalar_store_for( def require_original_function_creation( self, receipt: NativeFunctionExecution - ) -> None: + ) -> NativeCaptureSite: """Authenticate a compiler creation record, not an observed runtime birth.""" self.execution_outcome.require_function(receipt) - receipt.require_creation() + return receipt.require_creation() def require_fresh_function_namespace( self, receipt: NativeFunctionExecution diff --git a/nominal_refactor_advisor/source_execution.py b/nominal_refactor_advisor/source_execution.py index 53dd502d..bbe4331d 100644 --- a/nominal_refactor_advisor/source_execution.py +++ b/nominal_refactor_advisor/source_execution.py @@ -518,10 +518,9 @@ def creation_position(self) -> CompactFlowPosition: def require_descriptor_argument(self) -> None: native = self.native_execution - self.execution.module.native_compilation.execution_outcome.require_function( + creation = self.execution.module.native_compilation.require_original_function_creation( native ) - creation = native.require_creation() creation.frame.resolve(self) position = self.creation_position if position.branch_path or position.evaluation_path: From fe454b9d8dbcaf82acf82f837f9df5c70824ff1c Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sun, 1 Nov 2026 11:41:55 -0500 Subject: [PATCH 09/10] Retain observed compiler constant aliases in compact operands --- .../native_compilation.py | 149 ++++++++++++++---- .../native_declarations.py | 45 +++++- tests/test_native_constant_aliases.py | 117 ++++++++++++++ 3 files changed, 277 insertions(+), 34 deletions(-) create mode 100644 tests/test_native_constant_aliases.py diff --git a/nominal_refactor_advisor/native_compilation.py b/nominal_refactor_advisor/native_compilation.py index e51357bb..98ede84e 100644 --- a/nominal_refactor_advisor/native_compilation.py +++ b/nominal_refactor_advisor/native_compilation.py @@ -69,6 +69,9 @@ NativeLiteralContentsABC, NativeScalar, NativeConstantContentsABC, + NativeConstantIdentityABC, + NativeConstantIdentity, + UnobservedNativeConstantIdentity, NativeScalarValueABC, NativeTypeDeclaration, ) @@ -125,6 +128,77 @@ class NativeExecutionUnavailable(StrEnum): UNSUPPORTED_PROLOGUE = "unsupported_prologue" +class NativeConstantGraphABC(ABC): + """One observation's compiler-alias authority, separate from constant contents.""" + + def identity_for(self, value: object) -> NativeConstantIdentityABC: + return UnobservedNativeConstantIdentity() + + +class UnobservedNativeConstants(NativeConstantGraphABC): + """Synthetic/unsupported observations preserve contents but prove no aliases.""" + + +@dataclass(eq=False) +class NativeConstantGraph(NativeConstantGraphABC): + """Transient code owner; only nominal alias nodes reach compact receipts. + + Raw code/constants are foreign boundary data here. Their object identities + are consumed once, before serialization; retained payload identity is never + consulted. The resulting graph proves no link to an external target value. + """ + + code: CodeType + _identities: dict[int, NativeConstantIdentity] = field(default_factory=dict, init=False) + + def _observe(self, value: object) -> NativeConstantIdentity: + identity = id(value) + if identity in self._identities: + return self._identities[identity] + if type(value) is CodeType: + children = (self._observe(value.co_consts),) + elif type(value) in (tuple, frozenset): + children = tuple(self._observe(item) for item in value) + else: + children = () + result = NativeConstantIdentity(children) + self._identities[identity] = result + return result + + @cached_property + def root(self) -> NativeConstantIdentity: + return self._observe(self.code) + + def identity_for(self, value: object) -> NativeConstantIdentity: + _ = self.root + try: + return self._identities[id(value)] + except KeyError: + raise ValueError("Native constant is outside its original compiled graph") from None + + @property + def alias_relations(self) -> tuple[int, ...]: + handles: dict[NativeConstantIdentity, int] = {} + relations: list[int] = [] + pending = [self.root] + while pending: + node = pending.pop() + if node in handles: + relations.append(handles[node]) + continue + handles[node] = len(handles) + relations.append(handles[node]) + pending.extend(reversed(node.members)) + return tuple(relations) + + +@dataclass +class NativeConstantContext: + """Shared compiler-alias lifetime carried by native observation capabilities.""" + + constants: NativeConstantGraphABC = field(default_factory=UnobservedNativeConstants, kw_only=True) + + @dataclass(frozen=True) class NativeCompilationIdentity: """Exact source and interpreter provenance of one native compilation.""" @@ -1057,6 +1131,12 @@ class NativeConstantValue(NativeTypedValue): declaration: ClassVar[NativeTypeDeclaration] value: object + # Contents equality is not provenance. Strong alias queries use this + # nominal graph reference only after original operand authentication. + origin: NativeConstantIdentityABC = field(default_factory=UnobservedNativeConstantIdentity, compare=False, repr=False, kw_only=True) + + def compiler_constant_identity(self) -> NativeConstantIdentityABC: + return self.origin def _native_constant_value(self) -> object: return self.value @@ -1096,6 +1176,12 @@ class NativeConstantTupleElement(NativeConstantContentsABC, NativeOperandValueAB def _native_constant_value(self) -> object: return self.storage.constant_tuple_contents[self.index] + def compiler_constant_identity(self) -> NativeConstantIdentityABC: + # Preserve the original storage/index projection, never a scalar's + # post-serialization Python identity or an invented load instruction. + _ = self._native_constant_value() + return self.storage.compiler_constant_identity().member(self.index) + def resolve_operand( self, resolver: NativeValueResolverABC[NativeResolutionT] ) -> NativeResolutionT: @@ -1715,7 +1801,7 @@ def split( @dataclass -class NativeOperandStack: +class NativeOperandStack(NativeConstantContext): """Transient operand projection shared by native prefix and entry-store observations. Each production is retained once. Stores and dependent productions refer @@ -1791,7 +1877,8 @@ def prepare_keyword_names( ) names = self.emit( NativeConstantValue( - instruction.offset, (), self.code.co_consts[instruction.arg] + instruction.offset, (), self.code.co_consts[instruction.arg], + origin=self.constants.identity_for(self.code.co_consts[instruction.arg]), ), instruction, ) @@ -1923,9 +2010,21 @@ def constant( instruction: dis.Instruction, ) -> None: + self._constant(instruction, self.constants.identity_for(instruction.argval)) + + def immediate_constant( + self, operation: NativePrimitiveOperation, instruction: dis.Instruction + ) -> None: + # An immediate opcode is not a constant-pool object, even when dis's + # boxed argval happens to share one in this analyzer process. + self._constant(instruction, UnobservedNativeConstantIdentity()) + + def _constant( + self, instruction: dis.Instruction, origin: NativeConstantIdentityABC + ) -> None: value = instruction.argval production = ( - self.emit(NativeConstantValue(instruction.offset, (), value), instruction) + self.emit(NativeConstantValue(instruction.offset, (), value, origin=origin), instruction) if NativeConstantValue.supports_constant(value) else self.typed(instruction, type(value)) ) @@ -1995,7 +2094,7 @@ class NativePrimitiveOperation(Enum): LOAD_ATTR = (auto(), NativeAttributeValue.capture) LOAD_GLOBAL = (auto(), NativeGlobalValue.capture) LOAD_CONST = (auto(), NativeOperandStack.constant) - LOAD_SMALL_INT = (auto(), NativeOperandStack.constant) + LOAD_SMALL_INT = (auto(), NativeOperandStack.immediate_constant) LOAD_FAST_BORROW = (auto(), NativeLocalValue.capture) BUILD_TUPLE = (auto(), NativeTupleValue.capture) BUILD_LIST = (auto(), NativeListValue.capture) @@ -2149,7 +2248,7 @@ def resolve_binding( @dataclass(eq=False) -class NativeCodeObservation: +class NativeCodeObservation(NativeConstantContext): """Transient native stream with one cached initial source-body boundary.""" code: CodeType @@ -2557,14 +2656,14 @@ def require_from_entry(self) -> NativeReturn: return self -class NativeOperandObservation(ABC): +class NativeOperandObservation(NativeConstantContext, ABC): """The observation's original code supplies its transient operand interpreter.""" code: CodeType | None @cached_property def operands(self) -> NativeOperandStack: - return NativeOperandStack(code=self.code) + return NativeOperandStack(code=self.code, constants=self.constants) @dataclass(eq=False) @@ -2735,7 +2834,7 @@ def receipt( @dataclass -class NativeStoreStream: +class NativeStoreStream(NativeConstantContext): """One continuation observer consuming original stores from their owners.""" code: CodeType @@ -2747,7 +2846,7 @@ def new_continuation(self) -> NativeContinuationWindow: context = self.creation_context if context is not None and context.code is not self.code: raise ValueError("Native stream has no original creation context") - return NativeContinuationWindow(code=self.code, creation_context=context) + return NativeContinuationWindow(code=self.code, creation_context=context, constants=self.constants) def record_store(self, binding: NativeBindingTransfer) -> NativeBindingTransfer: if self.continuation is None: @@ -2835,11 +2934,11 @@ class NativeScopeObservation(NativeCodeObservation): @cached_property def entry_window(self) -> NativeEntryValueWindow: - return NativeEntryValueWindow(self.code, self.operations) + return NativeEntryValueWindow(self.code, self.operations, constants=self.constants) @cached_property def value_stream(self) -> NativeValueStoreStream: - return NativeValueStoreStream(self.code, self.operations, creation_context=self) + return NativeValueStoreStream(self.code, self.operations, creation_context=self, constants=self.constants) def observe_values( self, @@ -2893,7 +2992,7 @@ def returns(self) -> tuple[NativeReturn, ...]: @dataclass(eq=False) -class NativeCreationInventory(NativeScopeInventoryABC): +class NativeCreationInventory(NativeScopeInventoryABC, NativeConstantContext): """Transient results of the one complete emitted-code instruction walk.""" root_code: CodeType @@ -3715,25 +3814,7 @@ def code_contents(code: CodeType) -> tuple[bytes, tuple[int, ...]]: equal serialized values alone would erase observable constant sharing. This projection does not identify any object with an external reference. """ - payload = marshal.dumps(code, 2) - handles: dict[int, int] = {} - relations: list[int] = [] - - def visit(value: object) -> None: - identity = id(value) - if identity in handles: - relations.append(handles[identity]) - return - handles[identity] = len(handles) - relations.append(handles[identity]) - if type(value) is CodeType: - visit(value.co_consts) - elif type(value) in (tuple, frozenset): - for item in value: - visit(item) - - visit(code) - return payload, tuple(relations) + return marshal.dumps(code, 2), NativeConstantGraph(code).alias_relations def require_static_type_mro(self, value_type: type) -> tuple[type, ...]: """Authenticate each original native MRO owner once for lookup consumers.""" @@ -3924,6 +4005,7 @@ def inventory( self, code: CodeType, compilation: NativeCompilationIdentity ) -> NativeCreationInventory: """Observe creations, initial prefixes and entry stores in one native walk.""" + constants = NativeConstantGraph(code) pending = [(code, False)] emissions: list[NativeCodeEmission] = [] builders: list[NativeInstructionSite] = [] @@ -3934,7 +4016,7 @@ def inventory( has_children = any( isinstance(value, CodeType) for value in parent.co_consts ) - prefix = NativeScopeObservation(parent, self.primitive_operations) + prefix = NativeScopeObservation(parent, self.primitive_operations, constants=constants) prefixes[id(parent)] = prefix observations.append(prefix) parent_builders: set[SourceByteSpan] = set() @@ -3972,6 +4054,7 @@ def inventory( tuple(builders), prefixes, tuple(observations), + constants=constants, ) @abstractmethod @@ -4318,7 +4401,7 @@ def class_prologue(self, prefix: NativeCodeObservation) -> NativeClassPrologue: return OpenNativeClassPrologue( NativeExecutionUnavailable.INCOMPLETE_SOURCE_RANGES ) - stack = NativeOperandStack(code=prefix.code) + stack = NativeOperandStack(code=prefix.code, constants=prefix.constants) for instruction in initial: operation = self.primitive_operations.get(instruction.opcode) if instruction.is_jump_target: diff --git a/nominal_refactor_advisor/native_declarations.py b/nominal_refactor_advisor/native_declarations.py index cefa5463..1b994566 100644 --- a/nominal_refactor_advisor/native_declarations.py +++ b/nominal_refactor_advisor/native_declarations.py @@ -117,11 +117,54 @@ def dataclass_decorator_for_name(cls, reference_name: str | None) -> Self | None class NativeLiteralContentsABC(ABC): - """A literal-content question, not production, activation or object identity.""" + """Literal contents/observed compiler sharing, never target object identity.""" def require_literal_contents(self, expected: object) -> None: raise ValueError("Native literal construction contents remain unproved") + def compiler_constant_identity(self) -> NativeConstantIdentityABC: + return UnobservedNativeConstantIdentity() + + def require_same_compiler_constant(self, other: NativeLiteralContentsABC) -> None: + """Require shared observed compiler storage, without comparing raw payloads.""" + self.compiler_constant_identity().require_same(other.compiler_constant_identity()) + + +class NativeConstantIdentityABC(ABC): + """Compiler-graph sharing only; never identity with a target/external object.""" + + def require_same(self, other: NativeConstantIdentityABC) -> None: + raise ValueError("Native compiler constant alias remains unproved") + + def require_original(self, expected: NativeConstantIdentity) -> None: + raise ValueError("Native compiler constant alias remains unproved") + + def member(self, index: int) -> NativeConstantIdentityABC: + raise ValueError("Native compiler constant members remain unproved") + + +class UnobservedNativeConstantIdentity(NativeConstantIdentityABC): + """Contents-only or immediate values have no observed pool identity.""" + + +@dataclass(frozen=True, eq=False) +class NativeConstantIdentity(NativeConstantIdentityABC): + """One nominal alias node survives compact serialization by graph reference.""" + + members: tuple[NativeConstantIdentity, ...] + + def require_same(self, other: NativeConstantIdentityABC) -> None: + other.require_original(self) + + def require_original(self, expected: NativeConstantIdentity) -> None: + if self is not expected: + raise ValueError("Native constants have no shared original compiler identity") + + def member(self, index: int) -> NativeConstantIdentity: + if not 0 <= index < len(self.members): + raise ValueError("Native compiler constant member is outside its original graph") + return self.members[index] + class NativeConstantContentsABC(NativeLiteralContentsABC): """Exact primitive, ellipsis and tuple contents, independent of object identity. diff --git a/tests/test_native_constant_aliases.py b/tests/test_native_constant_aliases.py new file mode 100644 index 00000000..faad77cf --- /dev/null +++ b/tests/test_native_constant_aliases.py @@ -0,0 +1,117 @@ +"""Compiler alias records survive transport without becoming target identities.""" + +import ast +import dis +from dataclasses import replace +import marshal +from pathlib import Path +import pickle + +import pytest + +from nominal_refactor_advisor.ast_tools import SourceModule +from nominal_refactor_advisor.native_compilation import ( + NativeConstantValue, + NativeCreationBackend, + NativePrimitiveOperation, + NativeValueStore, +) +from nominal_refactor_advisor.source_geometry import SourceByteSpan + + +def default_stores(source: str, path: Path) -> tuple[NativeValueStore, ...]: + parsed = SourceModule.from_source_path(path, source).parse() + result = [] + for node in parsed.module.body: + assert isinstance(node, ast.FunctionDef) + span = SourceByteSpan.require_node(node) + result.append(parsed.native_compilation.value_store_for(span, span, node.name)) + assert not path.exists() + return tuple(result) + + +@pytest.fixture +def original_stores(tmp_path: Path) -> tuple[NativeValueStore, ...]: + return default_stores( + "def positional(value=5000): pass\ndef keyword(*, value=5000): pass\n", + tmp_path / "unexecuted.py", + ) + + +def test_original_alias_survives_compact_serialization( + original_stores: tuple[NativeValueStore, ...], +) -> None: + for stores in (original_stores, pickle.loads(pickle.dumps(original_stores))): + first = stores[0].value.positional_default_operands()[0] + second = stores[1].value.keyword_default_operands()[0] + stores[0].require_value(first.storage) + stores[1].require_value(second.storage) + first.value.require_same_compiler_constant(second.value) + first.value.require_literal_contents(5000) + second.value.require_literal_contents(5000) + with pytest.raises(ValueError, match="requires its original production"): + stores[0].require_value(replace(first.storage)) + + +def test_distinct_pool_origins_do_not_gain_shared_identity(tmp_path: Path) -> None: + first, second = default_stores( + "def positional(value=5000): pass\ndef keyword(*, value=5001): pass\n", + tmp_path / "distinct.py", + ) + left = first.value.positional_default_operands()[0].value + right = second.value.keyword_default_operands()[0].value + with pytest.raises(ValueError, match="shared original compiler identity"): + left.require_same_compiler_constant(right) + + +def test_equal_contents_in_independent_observations_are_not_one_origin(tmp_path: Path) -> None: + source = "def selected(value=5000): pass\n" + (first,) = default_stores(source, tmp_path / "first.py") + (second,) = default_stores(source, tmp_path / "second.py") + left = first.value.positional_default_operands()[0].value + right = second.value.positional_default_operands()[0].value + left.require_literal_contents(5000) + right.require_literal_contents(5000) + with pytest.raises(ValueError, match="shared original compiler identity"): + left.require_same_compiler_constant(right) + + +def test_contents_only_value_does_not_claim_its_own_alias_identity() -> None: + value = NativeConstantValue(0, (), 5000) + value.require_literal_contents(5000) + with pytest.raises(ValueError, match="alias remains unproved"): + value.require_same_compiler_constant(value) + + +@pytest.mark.skipif(NativePrimitiveOperation.LOAD_SMALL_INT.name not in dis.opmap, reason="Compiler has no immediate integer opcode") +def test_immediate_boxed_argval_is_not_constant_pool_identity(tmp_path: Path) -> None: + first, second = default_stores( + "def positional(value=0): pass\ndef keyword(*, value=0): pass\n", + tmp_path / "immediate.py", + ) + pooled = first.value.positional_default_operands()[0].value + immediate = second.value.keyword_default_operands()[0].value + pooled.require_literal_contents(0) + immediate.require_literal_contents(0) + with pytest.raises(ValueError, match="alias remains unproved"): + pooled.require_same_compiler_constant(immediate) + + +@pytest.mark.parametrize("index", [-1, 1]) +def test_constant_member_stays_inside_original_tuple( + original_stores: tuple[NativeValueStore, ...], index: int +) -> None: + storage = original_stores[0].value.positional_default_operands()[0].storage + with pytest.raises(ValueError, match="member is outside its original graph"): + storage.compiler_constant_identity().member(index) + + +def test_fingerprint_still_distinguishes_equal_contents_with_different_aliases() -> None: + first, second = int("5000"), int("5000") + assert first is not second + template = compile("pass", "unexecuted.py", "exec") + shared = template.replace(co_consts=((first, first),)) + distinct = template.replace(co_consts=((first, second),)) + assert marshal.dumps(shared, 2) == marshal.dumps(distinct, 2) + backend = NativeCreationBackend.current() + assert backend.code_contents(shared) != backend.code_contents(distinct) From 5ccb0d7998e28616e278e918e30de0f3ac707aed Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sun, 1 Nov 2026 13:13:52 -0500 Subject: [PATCH 10/10] Bind source default queries to supplied target code --- .../native_compilation.py | 16 +++ .../native_declarations.py | 7 +- nominal_refactor_advisor/source_entry.py | 38 +++++- nominal_refactor_advisor/source_execution.py | 20 +++ tests/test_source_target_compilation.py | 114 ++++++++++++++++++ 5 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 tests/test_source_target_compilation.py diff --git a/nominal_refactor_advisor/native_compilation.py b/nominal_refactor_advisor/native_compilation.py index 98ede84e..7df2d415 100644 --- a/nominal_refactor_advisor/native_compilation.py +++ b/nominal_refactor_advisor/native_compilation.py @@ -5304,6 +5304,22 @@ def compile(self, *, filename: str | None = None) -> CodeType: raise ValueError("Compiler filename belongs to a different source path") return compile(self.source, filename, "exec", dont_inherit=True, optimize=0) + def project_code(self, code: CodeType) -> _NativeCompilationOutcome: + """Project the supplied original module code, not another compiled specimen. + + Contents establish source correspondence only. The caller must separately + declare that its entry executes this exact held CodeType. Returning this + projection does not establish an execution, a function birth or effects. + The entry must retain the code and this original projection together; + serialized compact payloads alone cannot recover the held objects. + """ + if type(code) is not CodeType: + raise TypeError("Target compilation requires an exact native code object") + backend = NativeCreationBackend.current() + if backend.code_contents(code) != backend.code_contents(self.compile()): + raise ValueError("Target module code differs from its source compilation") + return backend.project(code, self.identity) + @property def execution_outcome(self) -> _NativeCompilationOutcome: """Expose only the native observation belonging to this source owner.""" diff --git a/nominal_refactor_advisor/native_declarations.py b/nominal_refactor_advisor/native_declarations.py index 1b994566..5a6cb3eb 100644 --- a/nominal_refactor_advisor/native_declarations.py +++ b/nominal_refactor_advisor/native_declarations.py @@ -122,6 +122,10 @@ class NativeLiteralContentsABC(ABC): def require_literal_contents(self, expected: object) -> None: raise ValueError("Native literal construction contents remain unproved") + def _native_constant_value(self) -> object: + """Observed payload only; its relation to target execution is separate.""" + raise ValueError("Native constant contents remain unproved") + def compiler_constant_identity(self) -> NativeConstantIdentityABC: return UnobservedNativeConstantIdentity() @@ -177,9 +181,6 @@ class NativeConstantContentsABC(NativeLiteralContentsABC): def supports_scalar(value: object) -> bool: return any(type(value) is declaration for declaration in get_args(NativeScalar)) - def _native_constant_value(self) -> object: - raise ValueError("Native constant contents remain unproved") - @staticmethod def _require_same_constant(left: object, right: object) -> None: pending = [(left, right)] diff --git a/nominal_refactor_advisor/source_entry.py b/nominal_refactor_advisor/source_entry.py index 0286c0c9..9eaf5eff 100644 --- a/nominal_refactor_advisor/source_entry.py +++ b/nominal_refactor_advisor/source_entry.py @@ -21,7 +21,7 @@ from functools import cached_property, cache from importlib.machinery import SourceFileLoader from importlib.util import module_from_spec, spec_from_file_location -from types import MappingProxyType +from types import CodeType, MappingProxyType from .captured_reference import ( AdmittedExecutionPrefixABC, @@ -41,6 +41,7 @@ ) from .collection_algebra import UniqueIdentityIndexAuthority from .descriptor_algebra import AliasProperty +from .native_compilation import NativeValueStore, _NativeCompilationOutcome from .native_declarations import ( NativeDeclarationFamily, NativeLanguageFeature, @@ -110,6 +111,10 @@ class SourceExecutionEntryABC(NamespaceEvidenceABC): bootstrap_binding_name = staticmethod(_native_exec_bootstrap_binding) + def target_store(self, original: NativeValueStore) -> NativeValueStore: + """A source fact table alone does not identify the code the target executes.""" + raise ValueError("Source entry target compilation remains unproved") + @property @abstractmethod def context(self) -> CompactFlowContext: @@ -269,6 +274,37 @@ def _member(self, key: NativeDictionaryKey) -> CapturedReferenceResolution | Non return self.initial_entries.get(key) +@dataclass(frozen=True, eq=False) +class CompiledSourceModuleEntryPremise(SourceModuleEntryPremise): + """Supplied entry contract: this activation executes exactly the held code. + + This is not an observed process or a claim inferred from code contents. The + caller supplies the actual target CodeType; the compiler owner only checks + its correspondence to this entry's original source and projects its held + operands. Ordinary source loaders never infer this stronger premise. + Noninterference, source effects, binding and invocation remain independent. + Native code is deliberately retained here: serializing compact operand + payloads cannot substitute for retaining these actual objects. + """ + + target_code: CodeType = field(kw_only=True, repr=False) + + @cached_property + def target_outcome(self) -> _NativeCompilationOutcome: + return self.source.module.native_compilation.project_code(self.target_code) + + def target_store(self, original: NativeValueStore) -> NativeValueStore: + compilation = self.source.module.native_compilation + canonical = compilation.value_store_for( + original.production_span, original.source_span, original.binding.name + ) + if original is not canonical: + raise ValueError("Target projection requires its original source store") + return self.target_outcome.value_store_for( + original.production_span, original.source_span, original.binding.name + ) + + class NoninterferingSourceModuleEntryPremise(SourceModuleEntryPremise): """Explicitly supplied exclusion of external interference during activation. diff --git a/nominal_refactor_advisor/source_execution.py b/nominal_refactor_advisor/source_execution.py index bbe4331d..54aa029d 100644 --- a/nominal_refactor_advisor/source_execution.py +++ b/nominal_refactor_advisor/source_execution.py @@ -2202,6 +2202,23 @@ def require_birth_storage(self) -> NativeParameterDefaultOperand: ) return operand + def require_target_birth_object(self) -> CapturedNativeObject: + """Conditional birth object from the entry's actual retained constant pool. + + Source/compiler correspondence alone never supplies this answer. The + entry must declare its held target code, and birth/noninterference and + original source-slot admission must already hold. This does not observe + a current function/default, transport between executions, or the identity + of a fresh runtime-built value. Immediate/contents-only values refuse. + """ + self.require_birth_storage() + target = self.execution.entry.target_store(self.store) + operand = self.parameter.native_default_operand(target.value) + target.require_value(operand.storage) + value = operand.value + value.require_same_compiler_constant(value) + return CapturedNativeObject(value._native_constant_value()) + def _require_native_production(self, value: NativeProducedValue) -> None: self.store.require_value(value) @@ -4151,6 +4168,9 @@ def require_external_noninterference( ) -> None: self.activation.callee.execution.entry.require_external_noninterference(prefix) + def target_store(self, original: NativeValueStore) -> NativeValueStore: + return self.activation.callee.execution.entry.target_store(original) + def require_native_behavior(self, authority: SourceActivationAuthorityABC) -> None: self.activation.callee.execution.entry.require_native_behavior(authority) diff --git a/tests/test_source_target_compilation.py b/tests/test_source_target_compilation.py new file mode 100644 index 00000000..72bd4cc3 --- /dev/null +++ b/tests/test_source_target_compilation.py @@ -0,0 +1,114 @@ +"""A supplied target-code contract is not an inferred runtime observation.""" + +import ast +from copy import copy +from dataclasses import dataclass +from pathlib import Path +from types import CodeType +from typing import cast + +import pytest + +from nominal_refactor_advisor.ast_tools import ParsedModule +from nominal_refactor_advisor.captured_reference import CapturedNativeObject +from nominal_refactor_advisor.lexical_bindings import DefaultedParameterSource, FunctionParameterSource +from nominal_refactor_advisor.source_entry import ( + CompiledSourceModuleEntryPremise, + NoninterferingSourceModuleEntryPremise, +) +from nominal_refactor_advisor.source_execution import ( + SourceCreatedFunctionCapture, + SourceFunctionDefault, + SourceModuleExecution, +) + +pytestmark = pytest.mark.native_proof + + +class NoninterferingCompiledEntry( + CompiledSourceModuleEntryPremise, NoninterferingSourceModuleEntryPremise +): + pass + + +@dataclass(frozen=True) +class TargetDefault: + join: SourceFunctionDefault + code: CodeType + ordinary: SourceModuleExecution + + @classmethod + def declared( + cls, header: str, + entry_kind: type[CompiledSourceModuleEntryPremise] = NoninterferingCompiledEntry, + ) -> "TargetDefault": + text = f"def chosen({header}): pass\n" + module = ParsedModule(Path("target-entry.py"), "target_entry", ast.parse(text), text) + ordinary = SourceModuleExecution.from_module(module) + original = ordinary.entry + code = module.native_compilation.compile() + entry = entry_kind( + source=original.source, native_island=original.initial, + bindings=dict(original.initial_entries), builtins=original.builtins, + target_code=code, + ) + execution = SourceModuleExecution(entry) + function = SourceCreatedFunctionCapture(execution, module.module.body[0]) + parameter = cast( + DefaultedParameterSource, + FunctionParameterSource.from_arguments(function.node.args)[0], + ) + return cls( + SourceFunctionDefault( + function, parameter, execution.required_prefix(entry.context, None) + ), code, ordinary, + ) + + +@pytest.mark.parametrize("header,expected", ( + ("value=5000", 5000), ("*, value=5000", 5000), + ("value=(5000, True)", (5000, True)), ("*, value=None", None), +)) +def test_declared_birth_object_belongs_to_the_held_target_pool(header: str, expected: object) -> None: + target = TargetDefault.declared(header) + captured = target.join.require_target_birth_object() + assert captured.value == expected + # The query must name an object in the exact held code graph, independently + # of the analyzer's separately compiled specimen or AST literal object. + roots = list(target.code.co_consts) + while roots: + original = roots.pop() + if type(original) is tuple: + roots.extend(original) + if captured.proves_same_object(CapturedNativeObject(original)): + break + else: + pytest.fail("birth object is outside the supplied target's constant pool") + with pytest.raises(ValueError, match="target compilation remains unproved"): + target.ordinary.entry.target_store(target.join.store) + + +def test_target_code_contract_does_not_also_declare_noninterference() -> None: + target = TargetDefault.declared("value=5000", CompiledSourceModuleEntryPremise) + target.join.result().require_constant_contents(5000) + with pytest.raises(ValueError, match="External source interference remains unproved"): + target.join.require_target_birth_object() + target.join.result().require_constant_contents(5000) + + +def test_target_projection_requires_original_source_store_and_cut() -> None: + target = TargetDefault.declared("value=5000") + join = target.join + with pytest.raises(ValueError, match="original source store"): + join.execution.entry.target_store(copy(join.store)) + with pytest.raises(ValueError, match="canonical closed source cut"): + SourceFunctionDefault(join.function, join.parameter, copy(join.prefix)).require_target_birth_object() + + +def test_foreign_code_cannot_borrow_the_source_target_association() -> None: + target = TargetDefault.declared("value=5000") + compilation = target.join.execution.module.native_compilation + with pytest.raises(ValueError, match="differs from its source"): + compilation.project_code(target.code.replace(co_filename="foreign.py")) + with pytest.raises(TypeError, match="exact native code object"): + compilation.project_code(cast(CodeType, object()))