Skip to content

Order-independent imports (#181) on top of #203 - #205

Merged
patham9 merged 2 commits into
trueagi-io:import-overhaulfrom
MesTTo:import-order-independence
Jul 23, 2026
Merged

patham9 merged 2 commits into
trueagi-io:import-overhaulfrom
MesTTo:import-order-independence

Conversation

@MesTTo

@MesTTo MesTTo commented Jul 22, 2026

Copy link
Copy Markdown

Stacks on #203 (import-overhaul). It adds the one import requirement #203 does not cover: import order independence (#181). #203 detects the bad-order case and warns; this makes the order actually not matter, so the four semantics from the tracking discussion (duplicate is a no-op, cycles are fine, order does not matter, relative paths resolve) are all covered once this lands on top of it.

Why order matters today

PeTTa freezes the function-vs-data decision at compile time from the fun/1 registry. Within a single file that is fine, because the first pass registers every head before any body compiles. Across files it is not: import! runs during the second pass one file at a time in textual order, so a definition in an earlier-loaded file that calls a function defined in a later-loaded file compiles that call as inert data. This is the #181 repro: importing c (which has (= (bar) (foo))) before b (which has (= (foo) ...)) leaves (bar) returning an unreduced (foo).

Approach

A name-collection pre-pass in process_metta_string/3 runs once per outer load, walks the static import closure, and parses each file so every function name is registered in fun/1 before any body compiles. It:

  • runs only at the outermost load, guarded by import_signature_scan_active, so nested real imports never re-scan their subtree,
  • only parses, and never compiles a clause, runs a runnable, or evaluates code to resolve a path,
  • resolves only literal file paths and (library ...) forms, reusing Overhaul import loading semantics #203's resolve_existing_import_path/3, current_working_dir/1, and ensure_metta_ext/2 so relative resolution stays identical,
  • is cycle-safe by canonical path and best-effort, so a missing or malformed file during the scan is ignored and still surfaces its real error at the runtime import!.

The prescan in #201 was pulled as "more trouble than it's worth". This avoids the three things that made that costly: it scans once instead of per file, it parses only instead of compiling, and it never evaluates an import expression to compute a path. After the change, #203's late-registration warning correctly stays quiet for the fixed cases and fires only for genuinely dynamic import paths, so the warning test now uses a computed path.

Verification

  • Order of imports changes the code behavior #181 is fixed in both directions. An index that imports a caller before its callee reduces the cross-module call. New regression example under examples/imports/import_order/ using the test builtin.
  • Duplicate, cyclic, relative, and missing-file behavior are byte-identical to Overhaul import loading semantics #203 on a shared fixture set.
  • Differential over the whole existing example corpus against Overhaul import loading semantics #203: no semantic differences. The only textual diffs are internal Prolog variable-counter display names, a wall-clock timestamp, and interactive examples that read stdin and do not terminate.
  • test.sh passes across the exercised examples and the Python tests pass.

The change is one commit and touches src/filereader.pl plus the example and test fixtures.

…dent imports

Fix issue #181 on top of PR #203. A definition referencing a not-yet-registered
function compiles the reference as inert data, so cross-file meaning depended on
import order. Scanning imports upfront cannot work: files fetched by git-import!
or generated at runtime are invisible to a scan on a fresh machine but visible
once a previous run created them, making program behavior depend on leftover
on-disk state. Instead, when a function name registers after stored definitions
already compiled it as plain data, those definitions are recompiled from their
source terms. Definitions then mean the same thing on every machine regardless
of import order, including for libraries that only become loadable at runtime.
Expressions that already executed cannot be recompiled retroactively and keep
the warning.
@MesTTo
MesTTo force-pushed the import-order-independence branch from 5c21b45 to 8c143dc Compare July 22, 2026 12:21
A (git-dependency url rev [build [basedir]]) form declares a repository the
file needs at an exact commit. Declarations are collected after parsing and
satisfied before any form runs, the way cargo fetches its locked dependency
graph before compiling: checkouts are staged, verified against the pinned
commit, retargeted when the pin changes, and refused when locally modified,
so a fresh clone and a machine with a leftover checkout behave identically.
A checkout may declare its own dependencies in deps.metta at its repository
root, which are acquired transitively; the same url pinned to two commits in
one run is an error. The machinery lives in src/gitimport.pl; the runnable
git-import! in lib_import.pl remains for dynamic acquisition.
@MesTTo

MesTTo commented Jul 22, 2026 •

Copy link
Copy Markdown
Author

The prescan approach this PR originally used had the machine-dependence problem @rTreutlein raised: a signature scan can only see files that already exist, while git-import! creates its files mid-run. It reproduced directly. With a definition using libfn above the import and the import pointing into a git-import! checkout, the same program run twice in the same directory gave two results: the first run (nothing cloned yet) left (libfn 1) unreduced with the warning, and the second run (checkout persisted from the first) resolved it to (libok 1). The prescan also crashed on a call placed above its import ("Unknown procedure: f/2"), because it registered the name while the clauses only arrive when the import runs.

The PR now takes the inverse approach, and the prescan is gone. #203 already records which names were compiled as plain data (note_symbol_head), and the runtime already maps every compiled clause to its source expression (translated_from). The first commit combines them: when register_fun/1 registers a name that stored definitions had compiled as data, those definitions are recompiled from their source terms, in clause order, with specializations invalidated the same way add-atom does. Nothing is scanned upfront and nothing depends on what a previous run left behind. This covers git-import! and cycles through it by construction: whenever the library actually loads, the stale definitions heal. The scenario above now produces the same resolved output on a fresh directory and on one with a leftover checkout. The case that intentionally still depends on order is calling a function before importing it, which already executed with the data meaning; that matches hyperon-experimental, where reduction resolves against the definitions present at call time, and the warning from #203 is kept for exactly that case.

The second commit picks up the other half of the discussion, including moving git-import to the core. Cargo's handling of git dependencies is the model: declarations are data, every git dependency is locked to a concrete commit, the graph is fetched before anything compiles, and checkouts are verified rather than trusted. PeTTa has no versions or registry, so with exact pins none of the resolver machinery is needed. A file can now declare (git-dependency "url" "<40-hex sha>") as a plain form; declarations are satisfied after parsing and before any form runs, so the checkout exists at the pinned commit by the time (library ...) resolves, identically on every machine. The engine is the pinned machinery from #195 moved into src/gitimport.pl: staged clone, detached checkout, HEAD verification, retargeting on pin change, refusal of dirty checkouts. A checkout can declare its own dependencies in a deps.metta at its root, acquired transitively. git-import! stays as the dynamic escape hatch, and the recompilation covers the names it loads late.

tests/regression/test_git_dependency.sh exercises this against local bare repositories, including a definition above its import resolving across the git boundary and a fresh-versus-reused-checkout diff. The full example corpus is semantically unchanged and the specializer regression suite passes.

@patham9 patham9 left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thank you!

@patham9
patham9 merged commit 4b7dc4c into trueagi-io:import-overhaul Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants