Skip to content

Compiler & artifacts

This page explains what happens between your .py file and the compiled program file, .dbc, that d/OS runs. After it you will know which parts of the compiler are Python-specific, why two builds of the same source are always identical, what each dpython command does, and how far the compiler's type inference goes today.

.py source → RustPython parser → Python semantic analysis → DBC lowering → shared emitter + optimizer → validate → .dbc + hash-bound .dbl

What is Python's and what is shared

The parser is RustPython's, at release 0.4.0, used under its MIT notice. Semantic analysis is d/Python's own: Python's scoping, name binding, type inference and the translation to d/OS bytecode. It is kept apart from the d/BASIC compiler's analysis (d/BASIC is d/OS's other language and shares the same runtime) so that sharing the later stages cannot let BASIC assumptions leak into Python. The code generator, optimizer, relocation and validation are shared. They were separated out of the d/BASIC compiler when a second language needed them; the d/BASIC compiler kept working unchanged and its whole test suite kept passing. The container format is the authority on opcodes, typed operands, headers, versions, program kinds and permissions, and the intent definitions, generated from the official list of intents, supply exact signatures.

The compiler today emits ordinary bytecode: objects, fields, iterator steps and typed procedure calls. It adds no new opcode, no private channel for Python, and no native Python object. New Python behavior arrives as shared support libraries or as versioned additions to the container format that leave every existing d/BASIC program working.

Determinism

The same source, the same library bytes, the same target and the same options produce the same bytes every time, with no host paths or timestamps inside. Every measured corpus program has identical debug and release output, and the browser build's fully optimized program and both of its libraries match the native command line byte for byte for the playground's test program. The header of a compiled program records exactly what its code and libraries need, and the d/OS runtime, the program that runs your compiled code, declines a program that needs something it cannot supply before executing it.

The commands

dpython check   program.py                  # validate the emitted artifact; not an execution claim
dpython build   program.py -o out/app.dbc   # write the image and lib/*.dbl beside it
dpython run     program.py                  # compile and execute; GUI source opens the native host
dpython run     out/app.dbc                 # execute a built artifact
dpython run     --native program.py         # select the window face of a dual-face image
dpython inspect program.py                  # JSON: code, pool, static, frames, capabilities, libraries
dpython trace   program.py                  # execute against the deterministic scripted host
dpython bindings                            # print the callable registry the compiler reads
dpython package program.py --metadata app.json -o app.dapp

inspect reads the compiled program with an independent reader of the container format. For fibonacci.py it reports a program of 1,561 bytes: 734 bytes of code, 763 of constant pool and 56 of static data, 15 value-stack slots, 24 call frames, a console program with no window, no permissions required, and one library, dpython.integers, of 2,791 bytes.

Optimization

The native command line always uses the ALL optimization profile; the browser API also offers NONE. For the playground's test program, NONE produced 2,615 bytes and ALL 2,082, with the same two libraries. Sharing method bodies as cached typed procedures took the tic-tac-toe game from 67,275 bytes to 17,968 without changing a rule of the game or a reviewed pixel. d/Python's promise for every optimization is simple: correct behavior, isolation and a responsive interface are requirements, and a smaller program that quietly changes what Python code does is not an improvement.

Type inference, today

The compiler specializes the code it emits on the types it can infer: one element type per list, dictionary or set; a fixed shape per tuple; one concrete result type per function, found from a base-case return before any recursive call; the same type on both sides of a conditional expression or an and/or. Where inference cannot decide, the compiler stops and tells you rather than guessing, and its message names the missing piece the compatibility table is waiting on: tagged values for mixed types, whole-scope inference for empty containers. These are limits of today's implementation, not of the language.

Scheduling and budgets

Every loop, arithmetic operation and copy is charged against the runtime's instruction and memory budgets. The playground runs programs in slices of 5,000 instructions; the test runner uses explicit finite budgets and reports when one runs out. A group of fused instructions can never run past the end of its slice: that was a defect found by the tic-tac-toe game, fixed in the shared runtime, and proven for every budget from 1 to 32 with fusion on and off. Event handlers run within the same budgets, and the user interface never waits on a program.

Provenance

The RustPython parser is at release 0.4.0 under its MIT notice. The reference CPython build (3.9.6) that every test is checked against is a test dependency only; no CPython runtime is embedded, and no native Python code takes part in running a compiled program. See Conformance.