Skip to content

Commit ff1128e

Browse files
authored
test(PARSE-01): adopt the compiler language contract (#28)
* test(PARSE-01): adopt the compiler language contract * build(PARSE-01): include the compiler reference corpus * test(PARSE-01): make isolated oracle output UTF-8
1 parent 8a4977c commit ff1128e

5 files changed

Lines changed: 224 additions & 0 deletions

File tree

‎IMPLEMENTATION_PLAN.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,10 @@ BROWSER-01 uses the pocket-python host contract. QA-03 proves CPython/Node/brows
366366
parity; PERF-01 measures output/helpers. This sequence does not require completing
367367
Date, RegExp, Map/Set or prototype/descriptor expansion first.
368368

369+
[The adopted compiler language contract](docs/compiler-language.md) specifies the
370+
first syntax/semantic decisions and reference corpus. It does not claim that
371+
translation is implemented yet.
372+
369373
## Compiler contracts and artifacts
370374

371375
Preserve ordinary supported Python truthiness, integers, Unicode, evaluation

‎docs/compiler-language.md‎

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Compiler source language contract
2+
3+
PARSE-01 adopts `python-browser/1` as the initial language profile, using Python
4+
3.14 grammar and semantics for ordinary Python constructs. This contract is
5+
future compiler work; the released package still provides Python-side
6+
JavaScript compatibility objects. The reference corpus checks Python behavior,
7+
not successful translation.
8+
9+
## First application and accepted source
10+
11+
Start with a single-module equivalent of Shimify's text_metrics application:
12+
Unicode character count, explicitly selected ASCII word separators, LF-based
13+
line count and a tuple of Python integer results. MOD-01 later adds its local
14+
helper import and package re-export. The selected input domain is builtin str;
15+
non-string input raises TypeError("text must be str"). String subclass overrides
16+
and arbitrary user objects are outside the first profile.
17+
18+
| Syntax/API | First adopted contract |
19+
| --- | --- |
20+
| Scalars | None, bool, arbitrary-precision int and Unicode str literals; tuples of supported values. Floats/bytes/complex wait for separate numeric/binary contracts. |
21+
| Bindings | Simple names, immutable module constants, ordinary local assignments and top-level plain functions. Preserve lexical binding identity and evaluation order. |
22+
| Functions | Positional arguments and immutable literal defaults; return, direct calls and recursion. Keyword/variadic calls, nested closures, decorators, annotations and async functions are deferred. |
23+
| Control flow | if/elif/else, for over approved strings/tuples, break/continue, ordinary return and expression statements. while/comprehensions/generators are deferred initially. |
24+
| Operations | Supported scalar arithmetic/comparisons, Python and/or/not, identity on supported values, string membership and supported indexing. Mixed or unknown operand forms need helpers or a source-located rejection. |
25+
| Builtins/errors | len on adopted strings/tuples, print of adopted values, approved isinstance checks and explicit builtin TypeError raising needed by the fixture. No eval/exec, reflection or undeclared I/O. |
26+
| JavaScript API | Resolve explicit imports/aliases through a versioned symbol/method registry. Start with Array.of and length as a binding fixture; other exports/methods need adopted signatures and parity cases before lowering. |
27+
| Imports | Only approved JavaScript registry imports in the single-module profile. Local modules follow MOD-01; Python stdlib/third-party/native imports require explicit future adapters. |
28+
29+
Every AST form and operation is supported, rejected or deferred explicitly. A
30+
Python-side JavaScript API existing today does not automatically make it
31+
translatable. No Date/RegExp/Map/Set expansion is a prerequisite for this compiler.
32+
33+
The first operators are integer +, -, *, // and %, unary +/-, boolean and/or/not,
34+
adopted scalar comparisons and string membership. True division, exponentiation
35+
and bitwise operations are deferred. Indexing supports builtin strings/tuples
36+
with integer indices and Python negative-index/error behavior; slicing is
37+
deferred. Identity tests initially cover None/bool singletons and retained
38+
object aliases, not implementation-dependent integer/string interning.
39+
40+
## Python and JavaScript semantics
41+
42+
Preserve Python truthiness and short-circuit operand results, rather than native
43+
JavaScript truthiness for containers. Strings count and iterate Unicode code
44+
points, preserving astral characters and combining marks without normalization.
45+
Python integers retain precision beyond 2**53; use deliberate representations
46+
and conversions, not silent Number rounding. Floor division/modulo must match
47+
negative operands and Python errors. Compare values and side effects in the
48+
original order with each operand evaluated once.
49+
50+
Tuple results remain distinguishable from JavaScript arrays in internal and
51+
transport contracts; implement required Python operations or reject them. Do not
52+
claim general tuple hashing, mixed comparisons or arbitrary object equality.
53+
Explicit JavaScript Array/String/Number/Object values retain their own approved
54+
API semantics. Crossing the two models needs an explicit conversion/signature.
55+
56+
Defaults are definition-time values, with immutable literal restrictions in the
57+
first profile. A direct call resolves to the actual lexical binding; aliases,
58+
local shadowing and recursion must not be rewritten by spelling alone. Preserve
59+
source-visible guards and errors even when static types guide lowering.
60+
61+
Runtime boundary values must satisfy the export/input contract. Incompatible
62+
operations raise the adopted Python error or fail validation clearly; the
63+
compiler must not emit superficially equivalent JavaScript. Unsupported async,
64+
classes, reflection, mutable module initialization and dynamic execution remain
65+
source-located diagnostics, not an interpreter/server fallback.
66+
67+
## Front end and diagnostics
68+
69+
Use ast -> bind -> validate -> typed IR -> lower -> emit. Never import or execute
70+
application/dependency modules to compile them. Carry original file/line/column
71+
spans and stable binding identities through every pass. Validate imports,
72+
exports, operation signatures and unsupported forms before publishing output.
73+
74+
Diagnostic families: PJS100 unsupported syntax/feature, PJS110 unresolved or
75+
unsupported binding/import, PJS120 incompatible operation/value contract.
76+
Include code, phase, original path, one-based line and column, and a concrete
77+
message. Reject an entire unsupported compilation without partially publishing
78+
an executable artifact. Do not catch an application exception and retry a
79+
callback or silently alter evaluation order.
80+
81+
## Output and conformance
82+
83+
EMIT-01 produces deterministic ES modules, source maps and an explicit helper
84+
closure. Its manifest identifies compiler/language/target versions, source and
85+
output hashes, entrypoint/exports, static module imports, required helpers and
86+
notices. Application profile schema 2 is defined by pocket-python; it must not
87+
be confused with its current MicroPython runtime asset lock.
88+
89+
Use CPython as the ordinary Python semantics oracle and native Node/browser
90+
observations for explicit JavaScript APIs. tests/fixtures/compiler_contract.json
91+
provides initial positive and rejected/deferred cases; parsing known sources
92+
for the contract does not prove a compiler already exists. PARSE-02 implements
93+
the front end and EMIT-01 must execute emitted output against that corpus.
94+
95+
MOD-01 adds explicit local-module identity and initialization; ASYNC-01 adds
96+
supported await/Promise/cancellation behavior. BROWSER-01 integrates main-thread
97+
DOM/events and native Fetch through the versioned host protocol; QA-03 verifies
98+
CPython/Node/browser parity and source-map errors. Origin/CORS and worker message
99+
constraints remain explicit.
100+
101+
This build-time application compiler does not execute newly entered source in
102+
the browser. That is a separate independent-interpreter milestone. Shared
103+
fixtures/host protocols do not make the two engines' IR, bytecode or value models
104+
interchangeable.

‎pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ module-name = "JavaScript"
3333
source-include = [
3434
"tests/test_*.py",
3535
"tests/fixtures/conformance.mjs",
36+
"tests/fixtures/compiler_contract.json",
3637
"docs/compatibility.md",
3738
"scripts/install_distribution.py",
3839
"CHANGELOG.md",
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
{
2+
"schema_version": 1,
3+
"profile": "python-browser/1",
4+
"reference": "CPython 3.14",
5+
"cases": [
6+
{
7+
"id": "large-integer",
8+
"source": "print(9007199254740993 + 1)",
9+
"stdout": "9007199254740994\n"
10+
},
11+
{
12+
"id": "unicode",
13+
"source": "print(len('A😀é'))",
14+
"stdout": "4\n"
15+
},
16+
{
17+
"id": "negative-division",
18+
"source": "print(-7 // 3, -7 % 3)",
19+
"stdout": "-3 2\n"
20+
},
21+
{
22+
"id": "short-circuit",
23+
"source": "print('' or 'fallback', 'left' and 'right')",
24+
"stdout": "fallback right\n"
25+
},
26+
{
27+
"id": "loop-and-tuple",
28+
"source": "def selected(text):\n count = 0\n for character in text:\n if character == 'a':\n count = count + 1\n return len(text), count\nprint(selected('a😀a'))",
29+
"stdout": "(3, 2)\n"
30+
},
31+
{
32+
"id": "default",
33+
"source": "def add(value, amount=2):\n return value + amount\nprint(add(3))",
34+
"stdout": "5\n"
35+
},
36+
{
37+
"id": "alias",
38+
"source": "from JavaScript import Array as A\nprint(A.of(1, 2).length)",
39+
"stdout": "2\n"
40+
}
41+
],
42+
"boundaries": [
43+
{
44+
"id": "class",
45+
"classification": "deferred",
46+
"source": "class Example:\n pass",
47+
"diagnostic": "PJS100"
48+
},
49+
{
50+
"id": "async",
51+
"classification": "deferred",
52+
"source": "async def example():\n return 1",
53+
"diagnostic": "PJS100"
54+
},
55+
{
56+
"id": "unapproved-import",
57+
"classification": "rejected",
58+
"source": "import os",
59+
"diagnostic": "PJS110"
60+
},
61+
{
62+
"id": "dynamic-execution",
63+
"classification": "rejected",
64+
"source": "eval('2 + 2')",
65+
"diagnostic": "PJS110"
66+
},
67+
{
68+
"id": "mutable-global",
69+
"classification": "deferred",
70+
"source": "values = []",
71+
"diagnostic": "PJS100"
72+
},
73+
{
74+
"id": "shadowed-call",
75+
"classification": "rejected",
76+
"source": "def selected(len):\n return len(1)",
77+
"diagnostic": "PJS120"
78+
},
79+
{
80+
"id": "keyword-call",
81+
"classification": "deferred",
82+
"source": "def selected(value):\n return value\nselected(value=1)",
83+
"diagnostic": "PJS100"
84+
}
85+
]
86+
}

‎tests/test_compiler_contract.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import ast
2+
import json
3+
from pathlib import Path
4+
import subprocess
5+
import sys
6+
import unittest
7+
8+
9+
CORPUS = Path(__file__).parent / "fixtures" / "compiler_contract.json"
10+
11+
12+
class CompilerContractTests(unittest.TestCase):
13+
def test_reference_behavior_and_feature_boundaries(self):
14+
corpus = json.loads(CORPUS.read_text(encoding="utf-8"))
15+
entries = corpus["cases"] + corpus["boundaries"]
16+
self.assertEqual(len({entry["id"] for entry in entries}), len(entries))
17+
for case in corpus["cases"]:
18+
with self.subTest(case=case["id"]):
19+
result = subprocess.run(
20+
[sys.executable, "-I", "-X", "utf8", "-c", case["source"]],
21+
capture_output=True, text=True, encoding="utf-8", timeout=5,
22+
)
23+
self.assertEqual(result.returncode, 0, result.stderr)
24+
self.assertEqual(result.stdout.replace("\r\n", "\n"), case["stdout"])
25+
for case in corpus["boundaries"]:
26+
with self.subTest(boundary=case["id"]):
27+
self.assertIn(case["classification"], {"rejected", "deferred"})
28+
self.assertIn(case["diagnostic"], {"PJS100", "PJS110", "PJS120"})
29+
ast.parse(case["source"])

0 commit comments

Comments
 (0)