From dc327c6ca69f7ac75daf899063e86fd9293a5dba Mon Sep 17 00:00:00 2001 From: ellainng <299958882+ellainng@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:37:28 -0700 Subject: [PATCH] chore(REL-03): prepare compiler and browser APIs for 0.2.0 --- .github/workflows/ci.yaml | 2 ++ .github/workflows/release.yaml | 2 ++ CHANGELOG.md | 8 +++++ IMPLEMENTATION_PLAN.md | 10 ++++++ README.md | 40 +++++++++++++--------- RELEASING.md | 3 +- docs/compiler-async.md | 2 +- docs/compiler-browser.md | 6 ++-- docs/compiler-conformance.md | 2 +- docs/compiler-emitter.md | 6 ++-- docs/compiler-front-end.md | 4 +-- docs/compiler-language.md | 2 +- docs/compiler-measurements.md | 2 +- docs/compiler-modules.md | 2 +- docs/release-0.2.0.md | 48 ++++++++++++++++++++++++++ pyproject.toml | 6 ++-- scripts/check_distribution_files.py | 53 +++++++++++++++++++++++++++++ uv.lock | 2 +- 18 files changed, 165 insertions(+), 35 deletions(-) create mode 100644 docs/release-0.2.0.md create mode 100644 scripts/check_distribution_files.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 10d1dfb..5cfaad4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -33,6 +33,8 @@ jobs: run: python -m pip install --upgrade build twine - name: Build sdist and wheel from sdist run: python -m build + - name: Verify distribution file allowlists + run: python scripts/check_distribution_files.py - name: Check distribution metadata run: python -m twine check --strict dist/* - name: Install wheel with published dependencies diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f6509e8..256024b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -45,6 +45,8 @@ jobs: run: python -m pip install --upgrade build twine - name: Build sdist and wheel from the sdist run: python -m build + - name: Verify distribution file allowlists + run: python scripts/check_distribution_files.py - name: Check distribution metadata run: python -m twine check --strict dist/* - name: Install the wheel diff --git a/CHANGELOG.md b/CHANGELOG.md index aab8dd7..e939f1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.2.0 + +- Add the binding-aware source compiler, deterministic ES module emitter and original-source diagnostics/maps. +- Compile explicitly supplied local modules and re-exports with selected helper and notice closure. +- Add lazy coroutine/Promise awaits and named browser worker/host capabilities with stop/restart. +- Verify the approved corpus across CPython, Node and Chromium and record scoped payload/startup/memory measurements. +- Preserve the JavaScript runtime namespace and existing Fetch/JSON dependency ranges. + ## 0.1.0 - Introduce the case-sensitive `JavaScript` namespace with Object, sparse Array, diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index fcbd1ab..321130c 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -489,3 +489,13 @@ helpers, maps, notices, metadata/harness and compressed payloads, with raw warm build, fresh/warm context startup and separate dedicated-worker V8 heap samples. These synchronous compiler fixtures do not include the browser-host/PWA shell; complete application and future interpreter measurements remain separate scopes. + +## 0.2.0 publication plan + +[REL-03](https://github.com/outside-labs/Python-JavaScript/issues/38) publishes +the merged compiler, module/async/browser profiles, conformance and measurements. +See the [release contract](docs/release-0.2.0.md). Runtime companion dependency +ranges remain unchanged; compiler and browser assets are in the distributions. +Strict artifact allowlists, installed-wheel/sdist and browser checks, passing +final main CI, PyPI/workflow hash comparison and a fresh PyPI install are gates. +The independent browser source interpreter remains a separate pocket-python task. diff --git a/README.md b/README.md index 788ed1b..09feb5c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ case-sensitive `JavaScript` namespace. An alpha compatibility layer with a documented subset of ECMAScript behavior. ```sh -python -m pip install Python-JavaScript==0.1.0 +python -m pip install Python-JavaScript==0.2.0 ``` ```python @@ -78,19 +78,26 @@ indices follow JavaScript rules: `values[-1]` is a named property, and `values.at(-1)` accesses the last element. `Array(3)` creates three holes; `Array.of(3)` creates one element containing 3. -The next program adds our Python-to-JavaScript compiler, a browser target and -CPython/client parity, coordinated with Shimify source reduction and pocket-python -assembly. See [the implementation plan](IMPLEMENTATION_PLAN.md). A separately -staged independent interpreter is tracked by pocket-python. v0.1.0 -provides the runtime foundation; it does not translate source or execute -JavaScript. Prototypes, descriptors, Date, RegExp, Map/Set, typed arrays, browser -globals and other unlisted APIs remain future work. Development is tracked in -the [issues](https://github.com/outside-labs/Python-JavaScript/issues) and -[Project](https://github.com/orgs/outside-labs/projects/11). +Version 0.2.0 also includes a bounded Python-to-JavaScript compiler: a +[binding-aware front end](docs/compiler-front-end.md), [ES module emitter](docs/compiler-emitter.md), +[local-module compiler](docs/compiler-modules.md), [async profile](docs/compiler-async.md) +and [browser worker/host target](docs/compiler-browser.md). Python parses source +at build time; generated modules and selected helpers execute on the client. +The separate pocket-python interpreter will parse newly entered Python in the browser. + +```python +from JavaScript.emitter import compile_source -The development checkout now includes a [binding-aware compiler front end](docs/compiler-front-end.md) -that validates source and produces typed IR. The emitter and [local-module compiler](docs/compiler-modules.md) -produce executable ES modules; browser adapters remain a subsequent milestone. +artifact = compile_source("def count(text):\n return len(text)", filename="counter.py") +# artifact.files contains emitted JavaScript, source map and provenance notice. +``` + +This alpha supports the explicit [compiler language](docs/compiler-language.md), +not arbitrary Python dependencies or complete ECMAScript. Prototypes, descriptors, +Date, RegExp, Map/Set, typed arrays and unlisted APIs remain future work. See +[0.2.0 release notes](docs/release-0.2.0.md), the [implementation plan](IMPLEMENTATION_PLAN.md), +[issues](https://github.com/outside-labs/Python-JavaScript/issues) and +[Project](https://github.com/orgs/outside-labs/projects/11). ## Development @@ -105,9 +112,8 @@ when Node is absent locally; CI and release builds require it. Runtime code needs only Python and the two declared companions. Tests make no external network requests. -The development [compiler emitter](docs/compiler-emitter.md) produces deterministic -ES modules for the approved single-module Python profile. This API is not part -of the published 0.1.0 distribution. +The [compiler emitter](docs/compiler-emitter.md) produces deterministic +ES modules for the approved single-module Python profile. The compiler APIs are included in the 0.2.0 distribution. The explicit [async compiler profile](docs/compiler-async.md) adds lazy coroutines, awaited Promise values, callbacks and context cancellation. The [browser target](docs/compiler-browser.md) adds named DOM controls, callbacks, @@ -118,4 +124,4 @@ with `uv run python examples/browser_text_metrics.py --output build/browser-text It uses our emitted modules and helpers in a worker; source parsing remains the separate independent-interpreter milestone. -The development compiler has a versioned [CPython/Node/browser parity suite](docs/compiler-conformance.md) with typed results, source-map error checks and explicit unsupported boundaries. +The compiler has a versioned [CPython/Node/browser parity suite](docs/compiler-conformance.md) with typed results, source-map error checks and explicit unsupported boundaries. diff --git a/RELEASING.md b/RELEASING.md index baf5b52..dcbd2c5 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -31,5 +31,4 @@ token belongs in the repository or workflow. run the examples/tests, and compare published SHA256 hashes with build artifacts. Publishing a GitHub release triggers upload; a tag push alone does not. PyPI -versions are immutable. Repository visibility stays private; published artifacts -include package source. No open-source license has been selected for this release. +versions are immutable. The repository is public; published artifacts include the selected package source. No open-source license has been selected; existing provenance notices grant no additional rights. diff --git a/docs/compiler-async.md b/docs/compiler-async.md index 9911d72..f69a812 100644 --- a/docs/compiler-async.md +++ b/docs/compiler-async.md @@ -1,6 +1,6 @@ # Compiled async profile -The development APIs `parse_source`, `compile_source` and `compile_modules` +The 0.2.0 APIs `parse_source`, `compile_source` and `compile_modules` accept `async_enabled=True`. Without it, async syntax remains rejected. Published 0.1.0 artifacts predate these compiler APIs. Single-module artifacts identify `python-browser-async/1` and ABI `python-browser-async-esm/1`; local-module artifacts diff --git a/docs/compiler-browser.md b/docs/compiler-browser.md index 51ba3fd..dcce728 100644 --- a/docs/compiler-browser.md +++ b/docs/compiler-browser.md @@ -1,9 +1,9 @@ # Compiled browser target -The development checkout provides `JavaScript.browser.compile_browser`. It +Version 0.2.0 provides `JavaScript.browser.compile_browser`. It compiles verified [local source modules](compiler-modules.md), enables the [async profile](compiler-async.md), and records the complete local worker/host -asset closure. It is not included in the published 0.1.0 distribution. +asset closure. It is included in 0.2.0; the published 0.1.0 distribution predates this API. The target is `python-browser-modules-host-async/1`, compiler ABI `python-browser-host-esm/1`, using host protocol `pocket-python-host` version 1. @@ -31,7 +31,7 @@ The builder reads the approved text_metrics corpus, emits selected local modules source maps/notices, the host/worker/protocol modules, a manifest and a small HTML entry. It requires a new output directory and preserves existing outputs. The same computation can be imported under CPython in a separate server entry; -client entrypoints and host adapters are explicit. E2E-01 owns the complete +client entrypoints and host adapters are explicit. The pocket-python shared-application example provides the complete server/client packaging demonstration. ## Python namespace and entry diff --git a/docs/compiler-conformance.md b/docs/compiler-conformance.md index 10b4fd9..7fc1bb1 100644 --- a/docs/compiler-conformance.md +++ b/docs/compiler-conformance.md @@ -1,6 +1,6 @@ # Compiler compatibility matrix: compiler-parity/1 -The development compiler runs the same approved source corpus under CPython +The 0.2.0 compiler runs the same approved source corpus under CPython 3.14.7 and as identical emitted ES modules in Node 24.19.0 and a Chromium module worker. This is a bounded ahead-of-time Python profile. The independent source interpreter is a separate pocket-python implementation. Published 0.1.0 predates diff --git a/docs/compiler-emitter.md b/docs/compiler-emitter.md index 11b8f9d..46eb89d 100644 --- a/docs/compiler-emitter.md +++ b/docs/compiler-emitter.md @@ -1,6 +1,6 @@ # Experimental ES module emitter -The development checkout exposes `JavaScript.emitter.compile_source`. It +Version 0.2.0 exposes `JavaScript.emitter.compile_source`. It validates source through the python-browser/1 front end, lowers the immutable IR and returns a CompiledArtifact without executing input or writing files. The published 0.1.0 package predates this compiler implementation. @@ -16,7 +16,7 @@ print(artifact.manifest["application_exports"]) # ['count'] ``` `files` is a tuple of immutable OutputFile(path, data, role) values. The caller -can inspect or pass those bytes to the future pocket-python application builder. +can inspect or pass those bytes to the pocket-python 0.2 application builder. The manifest bytes are separate from their own file/hash closure and can be saved as PYTHON-JAVASCRIPT.json. Every recorded file has exact size, SHA-256 and role; imports, notices and selected helper dependencies are explicit. Current @@ -83,7 +83,7 @@ runtime is embedded. The repository has no declared code license; this notice does not choose or grant one. Redistribution licensing remains a release decision. Run `python -m unittest discover -s tests -p 'test_compiler*.py' -v` with the -installed development checkout and Node 24.19.0. Tests compare CPython and emitted +installed package and Node 24.19.0. Tests compare CPython and emitted Node output/errors/evaluation order, the single-module text_metrics application, precision/Unicode/tuple/default behavior and an independent Node source-map consumer. Local modules and async browser capabilities have separate adopted diff --git a/docs/compiler-front-end.md b/docs/compiler-front-end.md index 6cdeb02..a2bebdc 100644 --- a/docs/compiler-front-end.md +++ b/docs/compiler-front-end.md @@ -1,9 +1,9 @@ # Compiler front end -The development checkout provides JavaScript.compiler.parse_source(source, +Version 0.2.0 provides JavaScript.compiler.parse_source(source, filename=...) for the adopted python-browser/1 profile. It parses, binds and validates source without running target code, then returns an immutable ModuleIR. -It does not emit or execute JavaScript yet; EMIT-01 owns that next step. +Use `JavaScript.emitter.compile_source` to emit executable JavaScript from this IR. The API uses only the declared Python runtime and standard library. A ModuleIR records profile/schema identity, SHA-256 of UTF-8 input, stable lexical binding diff --git a/docs/compiler-language.md b/docs/compiler-language.md index 81ac27b..78eb31d 100644 --- a/docs/compiler-language.md +++ b/docs/compiler-language.md @@ -1,7 +1,7 @@ # Compiler source language contract PARSE-01 adopts `python-browser/1` as the initial language profile, using Python -3.14 grammar and semantics for ordinary Python constructs. The development checkout implements its front end and +3.14 grammar and semantics for ordinary Python constructs. Version 0.2.0 implements its front end and [ES module emitter](compiler-emitter.md). The released 0.1.0 package predates these APIs and provides Python-side JavaScript compatibility objects. diff --git a/docs/compiler-measurements.md b/docs/compiler-measurements.md index fe2bff6..0b657d7 100644 --- a/docs/compiler-measurements.md +++ b/docs/compiler-measurements.md @@ -1,6 +1,6 @@ # Reproducing compiler measurements -PERF-01 measures the existing helper selection in the development compiler. It +PERF-01 measures the existing helper selection in the 0.2.0 compiler. It compares required helpers with every helper available to the same language profile; it does not add an optimization or enable otherwise unsupported syntax. The complete positive compiler corpus must match CPython in both variants, in diff --git a/docs/compiler-modules.md b/docs/compiler-modules.md index f2ade7d..d15a047 100644 --- a/docs/compiler-modules.md +++ b/docs/compiler-modules.md @@ -1,6 +1,6 @@ # Compiling local source modules -The development checkout provides `JavaScript.modules.compile_modules`. The +Version 0.2.0 provides `JavaScript.modules.compile_modules`. The published 0.1.0 package predates this API. `python-browser-modules/1` extends the [single-module language](compiler-language.md) with explicit named source imports. diff --git a/docs/release-0.2.0.md b/docs/release-0.2.0.md new file mode 100644 index 0000000..fc3506d --- /dev/null +++ b/docs/release-0.2.0.md @@ -0,0 +1,48 @@ +# Python-JavaScript 0.2.0 + +An experimental minor release for Python 3.14+. The case-sensitive `JavaScript` +compatibility namespace and the existing Fetch/JSON companion dependency ranges +are retained. No new runtime dependency is introduced. + +## Compiler and browser target + +The distribution now includes `JavaScript.compiler.parse_source`, +`JavaScript.emitter.compile_source`, `JavaScript.modules.compile_modules` and +`JavaScript.browser.compile_browser`, plus the matching `.mjs` worker/host assets. +They parse/bind a documented Python subset at build time and produce deterministic +ES modules, selected compatibility helpers, original-source maps, hashes and +provenance notices. They never import or execute the target source to compile it. + +Adopted profiles cover Python large integers, code-point strings and tuples, +short-circuit/evaluation order, plain functions with immutable defaults, verified +named local modules/re-exports, and explicit async/browser host capabilities. +Named controls, text output, callbacks and exact URL-granted Fetch execute through +the versioned browser worker protocol. Stop/restart releases the worker/session. + +The [compiler language](compiler-language.md), [modules](compiler-modules.md), +[async](compiler-async.md) and [browser target](compiler-browser.md) documents +specify the exact boundaries. CPython/Node/Chromium differential reports and +component measurements cover the approved corpus; measurements are fixture and +engine specific and do not establish a general size/speed advantage. + +## Limits + +This is ahead-of-time compilation. It does not parse newly entered Python in a +browser or delegate to a Python interpreter. Arbitrary packages/stdlib, classes, +descriptors, dynamic imports, unrestricted host attributes and unlisted Python or +ECMAScript APIs remain unsupported. Unsupported syntax is rejected with original +source diagnostics. Compiled applications require the documented external argument +representations and are not instruction/memory sandboxed. + +The repository has no declared code license; this release retains that metadata. +Original helper provenance and the borrowed fixture's MIT notice are preserved; +provenance does not grant additional redistribution rights. + +## Publication gates + +Require installed-wheel and installed-sdist tests on Linux/macOS/Windows, full +approved browser conformance, strict distribution allowlists/metadata, and final +main CI. Publish immutable `v0.2.0` through the existing `pypi` Trusted Publisher, +compare PyPI hashes with workflow artifacts, then test a fresh installation and +compile/run a representative module before closing +[REL-03](https://github.com/outside-labs/Python-JavaScript/issues/38). diff --git a/pyproject.toml b/pyproject.toml index 99173ae..40860f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "Python-JavaScript" -version = "0.1.0" -description = "JavaScript-style objects, methods and functions for Python" +version = "0.2.0" +description = "JavaScript-style Python values and a bounded Python-to-JavaScript browser compiler" readme = "README.md" authors = [ { name = "ellainng", email = "299958882+ellainng@users.noreply.github.com" } @@ -39,6 +39,8 @@ source-include = [ "tests/fixtures/licenses/SHIMIFY-LICENSE.txt", "docs/compatibility.md", "docs/compiler*.md", + "docs/release-*.md", + "scripts/check_distribution_files.py", "scripts/install_distribution.py", "scripts/compiler_browser_smoke.py", "scripts/compiler_conformance.py", diff --git a/scripts/check_distribution_files.py b/scripts/check_distribution_files.py new file mode 100644 index 0000000..64985be --- /dev/null +++ b/scripts/check_distribution_files.py @@ -0,0 +1,53 @@ +"""Check exact publication contents and declared companion dependency metadata.""" + +from email.parser import BytesParser +from pathlib import Path +import sys +import tarfile +import tomllib +import zipfile + + +def main(): + root = Path(__file__).resolve().parents[1] + project_data = tomllib.loads((root / 'pyproject.toml').read_text(encoding='utf-8')) + project = project_data['project'] + version = project['version'] + directory = Path(sys.argv[1]) if sys.argv[1:] else root / 'dist' + prefix = f'python_javascript-{version}' + modules = {p.relative_to(root / 'src').as_posix(): p for p in (root / 'src/JavaScript').rglob('*') + if p.is_file() and (p.suffix in {'.py', '.mjs'} or p.name == 'py.typed')} + wheels = list(directory.glob('*.whl')) + sources = list(directory.glob('*.tar.gz')) + assert len(wheels) == len(sources) == 1, 'Expected exactly one wheel and sdist' + assert wheels[0].name == prefix + '-py3-none-any.whl' + assert sources[0].name == prefix + '.tar.gz' + info = prefix + '.dist-info/' + with zipfile.ZipFile(wheels[0]) as archive: + names = [item.filename for item in archive.infolist() if not item.is_dir()] + assert len(names) == len(set(names)), 'Duplicate wheel files' + assert set(names) == set(modules) | {info + name for name in ['METADATA', 'WHEEL', 'RECORD']}, 'Unexpected wheel files' + for name, path in modules.items(): + assert archive.read(name) == path.read_bytes(), name + metadata = BytesParser().parsebytes(archive.read(info + 'METADATA')) + assert metadata['Name'] == project['name'] and metadata['Version'] == version + assert metadata['Requires-Python'] == project['requires-python'] + expected_dependencies = {'javascript-fetch-api>=0.2.0,<0.3.0', 'javascript-json>=0.1.0,<0.2.0'} + assert set(metadata.get_all('Requires-Dist', [])) == expected_dependencies + allowed = {'PKG-INFO', 'pyproject.toml', 'pyproject.toml.orig', 'README.md'} + allowed |= {'src/' + name for name in modules} + for pattern in project_data['tool']['uv']['build-backend']['source-include']: + allowed |= {p.relative_to(root).as_posix() for p in root.glob(pattern) if p.is_file()} + with tarfile.open(sources[0]) as archive: + files = [item for item in archive.getmembers() if item.isfile()] + names = [item.name for item in files] + assert len(names) == len(set(names)), 'Duplicate sdist files' + assert all(item.isdir() or item.isfile() for item in archive.getmembers()), 'Nonregular sdist entry' + assert set(names) == {prefix + '/' + name for name in allowed}, 'Unexpected sdist files' + for name in allowed - {'PKG-INFO', 'pyproject.toml', 'pyproject.toml.orig'}: + assert archive.extractfile(prefix + '/' + name).read() == (root / name).read_bytes(), name + print(f'Verified {len(modules)} package files and {len(allowed)} source-distribution files for {version}') + + +if __name__ == '__main__': + main() diff --git a/uv.lock b/uv.lock index a1d7a1c..c3ffa6f 100644 --- a/uv.lock +++ b/uv.lock @@ -100,7 +100,7 @@ wheels = [ [[package]] name = "python-javascript" -version = "0.1.0" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "javascript-fetch-api" },