diff --git a/docs/SPEC.md b/docs/SPEC.md index d54621613..19603ed2d 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -70,6 +70,7 @@ fail, then implement. - **L-7** iOS: the framework carries no stdlib, so `PYTHONHOME` must point at one; the test build extracts it. `Status: partial` — exercised by the shared suite on the simulator; no iOS-specific lifecycle test beyond `PM/iosSimulatorArm64Test/.../AsyncioAvailabilityProbeTest.kt`. +- **L-8** Initialisation sets `builtins.compiled` to an identity decorator (an existing one is kept), so `@compiled` needs no import on any platform (N-7, issue #42). `Status: implemented` — `PM/commonTest/.../ffi/BuiltinCompiledTest.kt`. ## 2. Low-level C API (downcall surface) diff --git a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/Python3.kt b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/Python3.kt index 8891d5c7a..fbd3095e1 100644 --- a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/Python3.kt +++ b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/Python3.kt @@ -49,6 +49,7 @@ object Python3 { // get here and the consumer's payload has still not been put on sys.path. The hook // does its work at most once per process, so arriving here repeatedly is free. PythonPayload.installStagedRootsOnStartup() + installBuiltinCompiledOnce() return } // Runs once per process, before the call whose own failure mode is either an uncatchable @@ -78,6 +79,9 @@ object Python3 { // commonMain and commonTest is now inside withPython{} or withGIL{}, so this is safe. // See ROADMAP §1 and §4 for the history and the previous revert. if (mainThreadState == null) mainThreadState = PyEval_SaveThread() + // `compiled` becomes a builtin name here (docs/SPEC.md N-7). Needs the GIL, so it comes + // after the parking above and goes through exec()'s withPython/withGIL. + installBuiltinCompiledOnce() // The consumer's own Python code goes on sys.path here: after Py_Initialize() built the // list (nothing can add to it before that point) and before this function returns, so // before any import a caller can reach. `PYTHONHOME` above is the *standard library* and @@ -86,6 +90,38 @@ object Python3 { PythonPayload.installStagedRootsOnStartup() } + private var builtinCompiledInstalled = false + + private fun installBuiltinCompiledOnce() { + if (builtinCompiledInstalled) return + installBuiltinCompiled() + builtinCompiledInstalled = true + } + + /** + * Sets `builtins.compiled` to an identity decorator so `@compiled` needs no import. + * The TypedPython compiler picks decorated functions at build time; at run time it does + * nothing. An existing `builtins.compiled` (e.g. the typedpython dev shim) is left alone. + * + * Runs through [exec], which holds the GIL for every C API call it makes; no raw pointer is + * held here, so there is no reference to state. The snippet runs inside a function so it + * leaves no names behind in `__main__`. + */ + internal fun installBuiltinCompiled() { + exec( + """ + def _pm_install_compiled(): + import builtins + if not hasattr(builtins, "compiled"): + def compiled(f): + return f + builtins.compiled = compiled + _pm_install_compiled() + del _pm_install_compiled + """.trimIndent() + ) + } + /** * Finalize Python (Will not be able to re-initialize after this | Let this function be internal) */ @@ -110,6 +146,7 @@ object Python3 { // initialize() rebuilds it instead of calling through a dangling one. Same for the // runMain/runApp helpers, which are cached the same way and would dangle the same way. checkpointCallable = null + builtinCompiledInstalled = false runModuleCallable = null runTopLevelCallable = null runFileCallable = null diff --git a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/BuiltinCompiledTest.kt b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/BuiltinCompiledTest.kt new file mode 100644 index 000000000..5d3ef407f --- /dev/null +++ b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/BuiltinCompiledTest.kt @@ -0,0 +1,38 @@ +package python.multiplatform.ffi + +import kotlin.test.Test +import kotlin.test.assertEquals + +/** Issue #42 / SPEC N-7: `compiled` is a builtin installed at interpreter initialisation. */ +class BuiltinCompiledTest { + + private fun evalStr(expr: String): String = PythonTestFixture.eval(expr).toString() + + @Test + fun compiledResolvesWithoutImportAndIsIdentity() = PythonTestFixture.withInterpreter { + assertEquals("True", evalStr("compiled(len) is len")) + Python3.exec("def _bc_f(): return 1\n_bc_r = compiled(_bc_f) is _bc_f") + assertEquals("True", evalStr("_bc_r")) + // It must live in builtins, not leak into __main__. + assertEquals("False", evalStr("'compiled' in globals()")) + } + + @Test + fun existingBuiltinCompiledIsNotOverwritten() = PythonTestFixture.withInterpreter { + Python3.exec( + """ + import builtins as _b + _bc_orig = _b.compiled + _bc_sentinel = lambda f: f + _b.compiled = _bc_sentinel + """.trimIndent() + ) + try { + Python3.installBuiltinCompiled() + assertEquals("True", evalStr("_b.compiled is _bc_sentinel")) + } finally { + Python3.exec("_b.compiled = _bc_orig") + } + assertEquals("True", evalStr("_b.compiled is _bc_orig")) + } +}