Skip to content

Step 5: Multi-File Projects

In this step, you'll learn to split your project across multiple files using import declarations.

Why Multiple Files?

As projects grow, it helps to separate concerns:

  • Constants in one file (shared across the project)
  • Parameters in another (easy to find and tune)
  • Main calculations in the entry point

Files Are Packages

Every .gcl file in Graphcal is a package. Without a graphcal.toml manifest, the file is a virtual package — a standalone Graphcal script. The package contains exactly one module: the file itself. You can self-reference its top-level decls from inline DAGs (e.g. import dynamics.{type T}; from inside dynamics.gcl), but you cannot import a sibling file. The first multi-file step in any Graphcal project is to add a manifest.

In other words: virtual = one file. The moment you want a second file, you add a graphcal.toml and graduate to a real package. The rest of this step walks through that promotion end to end.

Project Structure

A multi-file project always has a graphcal.toml manifest at the root and source files arranged under the package's source directory:

rocket_project/
  graphcal.toml                # [package] name = "rocket_project"
  src/
    rocket_project/
      constants.gcl
      params.gcl
      main.gcl

constants.gcl

pub const node g0: Acceleration = 9.80665 m/s^2;

params.gcl

param dry_mass: Mass = 1200.0 kg;
param fuel_mass: Mass = 2800.0 kg;
param isp: Time = 320.0 s;

main.gcl

import rocket_project.constants.{g0};
include rocket_project.params().{dry_mass, fuel_mass, isp};

node v_exhaust: Velocity = @isp * @g0;
node mass_ratio: Dimensionless = (@dry_mass + @fuel_mass) / @dry_mass;
node delta_v: Velocity = @v_exhaust * ln(@mass_ratio);

Try the Multi-File Project in Your Browser

Use the file tabs to edit graphcal.toml, both library modules, and the entry file. Imports are resolved entirely inside the browser's in-memory filesystem.

The editable playground requires JavaScript. Read the tutorial above, open the static entry source, or inspect its project manifest.

Expected initial output includes all seven projected values from g0 through delta_v.

The path before .{...} is absolute from the package root. The first segment is the package name (from graphcal.toml); subsequent segments walk the directory tree under source_dir.

Note params uses include, not import: params.gcl exposes params (runtime values), and runtime values cross file boundaries only through DAG instantiation. import brings compile-time names only.

Running a Multi-File Project

Point graphcal eval at the entry file:

$ graphcal eval rocket_project/src/rocket_project/main.gcl
g0         = 9.80665 m/s^2
dry_mass   = 1200 kg
fuel_mass  = 2800 kg
isp        = 320 s
v_exhaust  = 3138.128 m/s
mass_ratio = 3.333333
delta_v    = 3778.220768 m/s

Graphcal resolves each import against the package tree.

The import Statement

There are three forms; pick the one that matches what you want to bring into scope:

import rocket_project.constants;                  // brings module `constants`
import rocket_project.constants as c;             // brings module under alias `c`
import rocket_project.constants.{g0, g_mars};     // brings only `g0` and `g_mars`

The brace form is the most common in practice — it makes every imported name explicit.

Import Aliasing

If two files export the same name, rename one or both with as:

import rocket_project.file_a.{velocity as velocity_a};
import rocket_project.file_b.{velocity as velocity_b};

You can also alias a whole module:

import very.long.package.path as p;
node y: Length = @p.helper(...).result;

What Gets Imported

import brings only compile-time names. To use a runtime value (like a param or non-const node) from another file, include the producing DAG instead of importing the value (see Multi-File Projects).

Declaration kind How to import How to reference
const node import package.file.{name} @name
dim import package.file.{DimName} DimName
unit import package.file.{unit_name} unit_name
type import package.file.{type TypeName} TypeName
index import package.file.{IndexName} IndexName
dag import package.file.{dag_name} include-d, or called as @dag_name(...).out
assert import package.file.{assert_name} #[assumes(assert_name)]

When a Single File Suffices

If your whole calculation fits in one file, you don't need a manifest at all. A standalone rocket.gcl script behaves like a virtual package — its only externally addressable name is its own stem. References from inline DAGs back to top-level decls use that self-reference path:

// rocket.gcl  (standalone script, no graphcal.toml)
type OrbitType { OrbitType(sma: Length, ecc: Dimensionless) }

dag analyze {
    import rocket.{type OrbitType};   // file's own name
    param o: OrbitType;
    // ...
}

The moment you split into a second file, add a graphcal.toml at the project root and arrange the files under <source_dir>/<pkg>/ as shown above. Sibling-file imports are rejected with a clear error from any file that is not itself inside the package namespace — including a file sitting next to a graphcal.toml but outside its <source_dir>/<pkg>/ directory.

Circular Import Detection

Graphcal detects circular imports at compile time. In a real package with two modules <pkg>.a and <pkg>.b:

// src/<pkg>/a.gcl
import <pkg>.b.{x};

// src/<pkg>/b.gcl
import <pkg>.a.{y};   // ERROR: circular import

Assertions Are Always Checked

When you import a file, all of its assertions are automatically evaluated, even if you don't import them by name. This ensures that safety invariants in library files are never silently skipped. See Assertions for details.

What You Learned

  • Every .gcl file is a package — virtual (single-file standalone script) or real (manifest-backed multi-file project).
  • A virtual package has exactly one file. Multi-file projects always have a graphcal.toml.
  • The three import forms — bare, aliased, and brace list — bring exactly the names you write into scope.
  • import is for compile-time names; runtime values cross file boundaries via include.
  • Circular imports and assertion checks are handled automatically.

Next Step

In Step 6, you'll work with indexed collections for multi-element calculations.