> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blobhub.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Compiler

> How a Python package becomes the flat, import-less namespace a logic.code component executes

The platform executes a `logic.code` component's code in a namespace with no `__import__` at all, so a surviving
`import` statement fails at runtime with `ImportError: __import__ not found`. Code that runs there has to be one
flat namespace with every import already resolved.

The compiler is what turns an ordinary Python package into that namespace. It runs inside
[`build`](/cli/workflow/commands/build) and [`deploy`](/cli/workflow/commands/deploy), and in memory inside
[`diff`](/cli/workflow/commands/diff). It parses with `ast` and never imports, executes or evaluates your code,
so compiling is safe on a package you have not run.

```mermaid theme={null}
flowchart LR
    EP["entry_point"] --> WALK["Walk<br/>parse and classify every import"]
    WALK --> CHECK["Hard checks<br/>cycles, collisions, shadowing"]
    CHECK --> EMIT["Emit<br/>one part per module,<br/>imports rewritten"]
    EMIT --> HASH["Hash<br/>content_hash and derived id"]
    HASH --> PORT["The code port in<br/>the definition file"]
```

Everything up to the port is a pure function of the files on disk and the import allowlist. Nothing here talks to
the platform.

## The walk

Starting from `entry_point`, each module is parsed and every `import` / `from ... import` in it is classified —
including the ones nested inside functions and classes, which are classified exactly where they sit rather than
hoisted.

| Classification    | Condition                                                                                    | Action                                                                                         |
| ----------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| Local             | Resolves to a file under `base_path`: `a/b.py`, else `a/b/__init__.py`                       | Recurse into it and inline it                                                                  |
| Sandbox pre-bound | Root name is one of the [sandbox](/cli/workflow/sandbox)'s stdlib, typing or exception names | Drop the statement                                                                             |
| Blob-allowlisted  | Root name is in `workflow_component_code_imports`, when it is readable                       | Drop the statement                                                                             |
| Unavailable       | Anything else                                                                                | `IMPORT_UNAVAILABLE`, or the `IMPORT_UNVERIFIED` advisory when the allowlist could not be read |

Resolution is by module path, not by bare file existence. `from a.b import c` looks for `a/b.py`, then
`a/b/__init__.py`; landing on `a/b/c.py` instead means `c` is a *module*, not a name, and that is
`LOCAL_MODULE_IMPORT_FORM` rather than a resolution.

Relative imports resolve against the importing module's package. `from . import x` inside that package's own
`__init__.py` resolves to the sibling `x.py`, never to `__init__.py` itself, which would report a self-cycle. A
path that resolves outside `base_path` — including an `entry_point` that is not under it at all — is
`BASE_PATH_ESCAPE`.

Where a local module shadows a pre-bound stdlib name, local wins, because the sandbox's own merge order does the
same thing. The `LOCAL_SHADOWS_SANDBOX_MODULE` advisory names the shadow.

## Emission

Modules are ordered topologically: dependencies first, the entry point last. Each becomes one `content[]` part,
headed by a frozen marker naming the file it came from:

```python theme={null}
{"type": "text", "text": "# blobhub:source lib/pricing.py\n<module body>"}
```

That marker is what makes [`eject`](/cli/workflow/commands/eject) possible, and it is part of the contract rather
than a comment.

The module body is the source with every import statement replaced in place — by exact character span, so several
statements on one physical line are each spliced separately and the surrounding indentation and text are
untouched. Nothing else is reformatted or reordered.

### The import-rewrite table

What replaces an import depends on what the runtime actually binds, which is **the module's own name, never the
names inside it**. The sandbox pre-binds `json`, `datetime`, `re` and the rest as module objects, and an
allowlisted third-party package is merged in under its literal allowlist name the same way. A local import is
different: once its target is inlined, the names it exports are already bound flatly under their own names.

| Source form                                           | Emitted in place of the import                       |
| ----------------------------------------------------- | ---------------------------------------------------- |
| local `from x import y`                               | nothing — `y` is already bound by the inlined module |
| local `from x import y as z`                          | `z = y`                                              |
| local `from x import *`                               | nothing — the module is inlined whole                |
| local `import x` or `import x.y`                      | rejected: `LOCAL_MODULE_IMPORT_FORM`                 |
| local `from x import y`, where `y` is itself a module | rejected: `LOCAL_MODULE_IMPORT_FORM`                 |
| non-local `import x`                                  | nothing — `x` is already bound                       |
| non-local `import x as y`                             | `y = x`                                              |
| non-local `import x.y as z`                           | `z = x.y`                                            |
| non-local `from x import a`                           | `a = x.a`                                            |
| non-local `from x import a as b`                      | `b = x.a`                                            |
| non-local `from x import a, b`                        | `a = x.a; b = x.b` — one statement, semicolon-joined |
| non-local `from x import *`                           | rejected: `UNSUPPORTED_IMPORT_FORM`                  |

Getting this wrong would be worse than a `NameError`. Stripping `from datetime import datetime` to nothing leaves
the name `datetime` bound to the *module* rather than the class, so `datetime.now()` fails with a confusing
attribute error — or silently resolves against the wrong object. The rebinding line is what keeps the flat
namespace behaving the way the source reads. It has one sharp edge of its own, in
[Limitations](#limitations) below: that same rebinding takes over the module's name.

A block left empty by the rewrite gets a `pass`, so a function whose whole body was an import stays parseable —
`def configure():` followed only by `import json` compiles to `def configure():` followed by `pass`.

## Hard checks

Each of these stops the command before anything is written, and each prevents a failure that would otherwise
surface only at execution time, with no plausible cause.

| Code                                        | What it catches                                                                                                                                                                                                                                                 |
| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `IMPORT_CYCLE`                              | Concatenation cannot express a cycle. The message names the full cycle.                                                                                                                                                                                         |
| `SYMBOL_COLLISION`                          | Two inlined modules define the same top-level name. Names are never mangled or renamed — mangled code is unreadable in the visual editor and in execution traces, which is where it gets debugged.                                                              |
| `SANDBOX_NAME_SHADOWED`                     | An inlined module defines a name the sandbox pre-binds (`result`, `set_result`, `data_inputs`, …). The sandbox binds these *after* your code, so the redefinition would be silently discarded at runtime.                                                       |
| `LOCAL_MODULE_IMPORT_FORM`                  | An import naming a local *module* where the flat namespace can only bind names inside it: `import pkg.mod`, `from pkg import mod`, `from . import mod`. The hint shows the working form for the statement as written.                                           |
| `IMPORT_NAME_NOT_FOUND`                     | A confirmed-local module does not define the name asked for — a typo, or a name since renamed. **Always fatal**, whatever the allowlist's state: it is decided offline by parsing a file already on disk, so an unreadable allowlist is no reason to soften it. |
| `UNSUPPORTED_IMPORT_FORM`                   | `from x import *` against a non-local module, whose exports cannot be enumerated to rebind.                                                                                                                                                                     |
| `LANGUAGE_NOT_PYTHON`                       | The bound component's `language` port is not exactly `python`. Checked before compiling at all, since writing code into a component that cannot execute it is precisely the failure this CLI moves to deploy time.                                              |
| `BASE_PATH_ESCAPE`, `ENTRY_POINT_NOT_FOUND` | A module resolves outside `base_path`, or the entry point does not exist under it.                                                                                                                                                                              |

Dotted imports of *non-local* modules are fine — only local ones are rejected by
`LOCAL_MODULE_IMPORT_FORM`.

## Determinism

Compilation is a pure function of the sources, and the writer touches no key it does not own:

* **`content_hash`** — SHA-256 over the canonical compiled `content[]`: the JSON array serialized with sorted
  keys, `ensure_ascii=False` and no insignificant whitespace.
* **`attributes.id`** — `uuid5` of that hash under a fixed namespace, lower-cased. Identical content always
  yields the identical id.
* **`attributes.created_at`** — carried forward from whatever is already in the definition file, and stamped
  with the current time only when there is nothing to carry.
* **`attributes.syntax_mode`** — always written as `python`.
* Any other attribute key already present is preserved.

So a rebuild over unchanged sources leaves the definition file byte-identical and `git status` clean. A code port
this CLI has never built changes once, on its first build, and is stable from the second onward.

**"Byte-for-byte" is anchored after newline translation.** Module sources are read in universal-newlines mode, so
`\r\n` and `\r` become `\n` before the compiler sees them. A CRLF-authored file therefore deploys as LF and
hashes identically to its LF-authored twin — which is the point, since the same package must compile the same
way whatever checked it out — but the deployed text is not literally the file's bytes, and an `eject` round trip
will not restore CRLF endings.

## The remote-edit gate

`content_hash` is what lets the CLI refuse to overwrite code someone edited in the visual editor. Before writing
a code port that already has content:

| Recorded `content_hash`               | State                              | Behaviour                                                      |
| ------------------------------------- | ---------------------------------- | -------------------------------------------------------------- |
| Present, matches the embedded content | Built by this CLI, untouched since | Overwritten silently                                           |
| Present, does not match               | Edited since the last build        | `REMOTE_EDIT` — the command stops                              |
| Absent                                | Never built by this CLI            | `UNMANAGED_CODE` — the command stops, unless the port is empty |

`--force` skips the classification. The other way out is [`eject`](/cli/workflow/commands/eject), which brings the
edited code onto disk so you can reconcile it by hand first. [`diff`](/cli/workflow/commands/diff) reports the
same three states without writing anything.

The scheme holds whichever way the visual editor treats an attribute it does not recognize: preserved, the hash
goes stale and mismatches; regenerated, the hash disappears. Both stop and tell you, so the CLI never depends on
the editor cooperating.

Note which document is classified. `build` and `deploy` read the code port in the **local** definition file,
because that is the only document they have; `diff` is the command that classifies the **remote** port.

## Limitations

Stated plainly rather than omitted, because each one is a surprise the first time you meet it in deployed code.

**A rebinding can shadow the module it came from.** `from datetime import datetime, timezone` emits
`datetime = datetime.datetime; timezone = datetime.timezone`, and the first assignment rebinds `datetime` to the
class before the second reads `.timezone` off it — an `AttributeError` at execution time. The same happens across
modules: one inlined module doing `from datetime import datetime` breaks a later module's `from datetime import
timezone`. It bites whenever the imported name equals its module's name, `datetime` being the common case. Either
reach through the module, which needs no rebinding at all, or alias to a different name:

```python theme={null}
# Reach through the module: nothing is emitted, so `datetime` stays the module.
import datetime
datetime.datetime.now()

# Or alias, so the rebinding lands on a name the module does not need.
from datetime import datetime as dt, timezone   # dt = datetime.datetime; timezone = datetime.timezone
```

**A submodule import does not inline the parent package's `__init__.py`.** Real Python executes `a/__init__.py`
before `a/b.py`; this compiler resolves `a.b` straight to `a/b.py` and never visits `a/__init__.py` unless some
other statement reaches it directly. Inlining every ancestor on every submodule import would drag unrelated
package-level code into the flat namespace and manufacture spurious `SYMBOL_COLLISION`s. The consequence: side
effects or definitions living only in a parent `__init__.py` are silently absent at runtime.

**A conditional import pair reports as `SYMBOL_COLLISION`.** `try: from a import helper` /
`except ImportError: from b import helper` classifies both branches statically — the compiler never evaluates
which would win — so both modules are inlined and their two `helper`s collide, and the message names two modules
without mentioning the `try`. Rewrite to a single branch; the sandbox has no `__import__` at all, so an
`ImportError` fallback could never fire there anyway.

**A module with a top-level `from x import *` opts out of `IMPORT_NAME_NOT_FOUND` entirely.** Its exports cannot
be enumerated without evaluating `__all__` or executing code, so every name requested from it is let through. The
wildcard's own source module is still inlined, so a genuinely re-exported name really is bound; this only widens
what the compiler accepts, never invents a binding.

**A dotted non-local import is classified by its root, but rebinds through an attribute.** `import numpy.linalg
as la` is accepted when `numpy` is allowlisted and emits `la = numpy.linalg` — which resolves at runtime only if
the sandbox actually imported the submodule, that is, only if `numpy.linalg` is itself an allowlist entry. See
[Sandbox](/cli/workflow/sandbox) for why a dotted entry is worth an advisory of its own.

## The allowlist

`workflow_component_code_imports` is the one compilation input that lives on the server, and reading it needs an
`admin`-scoped key — which the `read` and `write` keys the platform recommends for automation are not.

So the compiler classifies against a local cache at `~/.blobhub/cache/allowlist/`, keyed on the manifest's own
`blob:` reference. That is what keeps `build` networkless. With no cache entry, `ALLOWLIST_UNREADABLE` is raised
as an advisory and an unresolved import degrades from `IMPORT_UNAVAILABLE` to the advisory `IMPORT_UNVERIFIED`;
`--strict-imports` restores the hard failure, which is what a CI job should set. A cache entry older than seven
days additionally raises `ALLOWLIST_STALE`.

Everything that does not depend on the allowlist is enforced regardless: local resolution, cycles, collisions,
name lookups and sandbox shadowing.

[`blobhub blob limits`](/cli/blob/limits) covers the allowlist itself, the key that can read it, and how the
cache is written.

## Errors

Every code named on this page, with its remediation, is in [Error codes](/cli/error-codes). The compiler's own
family is `IMPORT_CYCLE`, `SYMBOL_COLLISION`, `SANDBOX_NAME_SHADOWED`, `LOCAL_MODULE_IMPORT_FORM`,
`IMPORT_NAME_NOT_FOUND`, `IMPORT_UNAVAILABLE`, `UNSUPPORTED_IMPORT_FORM`, `BASE_PATH_ESCAPE` and
`ENTRY_POINT_NOT_FOUND`, each naming the module and the statement that caused it.

## See also

* [`blobhub workflow build`](/cli/workflow/commands/build) — the command that runs this, offline.
* [`blobhub workflow eject`](/cli/workflow/commands/eject) — the closest thing to an inverse, and explicitly not
  one.
* [Manifest](/cli/workflow/manifest) — where `base_path` and `entry_point` are declared.
* [Sandbox](/cli/workflow/sandbox) — the names the compiler may drop, and the ones your code must not redefine.
* [Component code](/blob-types/workflow/workflows/component-code) — what the platform executes once this has run.
