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 Locally

The standalone playground supports only one .gcl file. Use the CLI for this multi-file lesson; it is not flattened into a different browser example.

Read the entry source, constants, parameters, and package manifest. Arrange them as shown above and run the command below.

Expected 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.221 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. Selective dimensions, units, types, and indexes use the explicit dim, unit, type, and index markers; bare items select terms such as constants, DAGs, assertions, and constructors. 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::{dim DimName} DimName
unit import package.file::{unit unit_name} unit_name
type import package.file::{type TypeName} TypeName
index import package.file::{index 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 Belong to Explicit Instances

An import loads compile-time names without evaluating the module, so it does not run assertions. Every explicit include instance runs its own assertions with that instance's bindings. Select an assertion in the include braces when you need its name in #[assumes(...)]. 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 are detected automatically; assertions run for explicit include instances.

Next Step

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