Module Resolution

Grove organizes code into modules based on the filesystem directory structure. Every directory containing .grove files constitutes a module. Files within a module are merged into a single logical unit, and modules reference each other through import declarations with path-based resolution.

Prerequisites: Declaration Grammar for import syntax, Lexical Grammar for identifier rules. What you'll learn: How Grove discovers modules, merges files, resolves imports, handles test files, and determines type visibility.


Directory-Based Modules

A module is a directory that contains one or more .grove files. The module name is the directory name.

my-project/
  order/
    types.grove         # \
    events.grove        #  |-- these form the "order" module
    actions.grove       #  |
    order_test.grove    # /   (excluded from production)
  customer/
    types.grove         # \
    actions.grove       #  |-- these form the "customer" module
    customer_test.grove # /

File Merging

All .grove files within a module directory (excluding test files) are merged into a single logical unit before analysis. This means:

  1. Declaration order is irrelevant. A type defined in types.grove can be referenced in actions.grove without an import.
  2. No file-level scoping. All declarations in all files of a module share the same namespace.
  3. Name uniqueness. Two declarations with the same name in different files of the same module are an error.
order/
  types.grove    -->  contains: record OrderItem { ... }
  events.grove   -->  contains: event OrderCreated:1 { items: List<OrderItem> }
  actions.grove  -->  contains: action create_order(...) { emit OrderCreated { ... } }

All three files are merged. OrderItem is visible in events.grove and actions.grove without any import.

Test File Exclusion

Files matching the pattern *_test.grove are excluded from production builds:

order/
  actions.grove       # included in production
  order_test.grove    # excluded from production, included in test
  helpers_test.grove  # also excluded from production

The naming convention is <anything>_test.grove. The prefix does not need to match the module name.

Import Resolution

Standard Library Imports

Imports with the grove/ prefix resolve to the Grove standard library:

import "grove/time"
import "grove/http"
import "grove/json"
import "grove/crypto"
import "grove/uuid"

Standard library modules are bundled with the Grove runtime and are always available.

Local Module Imports

All other import paths are resolved relative to the importing file's directory:

// In order/actions.grove:
import "../shared/types"       // resolves to <project>/shared/types/
import "../customer"           // resolves to <project>/customer/

Import Aliasing

The as clause provides an alias for the imported module:

import "grove/time" as t
import "../shared/types" as shared

let now = t.DateTime.now()
let addr = shared.Address { street: "123 Main", city: "Springfield" }

Without an alias, the module is referenced by its directory name:

import "../shared/types"

let addr = types.Address { street: "123 Main", city: "Springfield" }

Import Cycles

Circular imports are not permitted. If module A imports module B, then B (or any module transitively imported by B) cannot import A.

Checker rule: A circular import produces: import cycle detected: <A> -> <B> -> ... -> <A>.

Type Visibility

Grove uses a Go-inspired visibility model based on qualified names:

Within a Module

All declarations within a module are visible to all files in that module without qualification. No imports are needed.

// In order/types.grove
record OrderItem {
  product_id: Id
  quantity: Int
  price: Decimal
}

// In order/actions.grove -- no import needed
action create_order(items: List<OrderItem>) {
  // OrderItem is directly visible
}

Across Modules

Declarations from other modules are accessed through qualified names using the module name (or alias) as a prefix:

import "../customer"

action create_order(customer_id: Id) {
  let cust = customer.get_customer(customer_id)
  // ...
}

All top-level declarations in a module are public. Grove does not have a private or internal visibility modifier. If a declaration exists in a module, it can be imported and used by other modules.

Name Conflicts

If two imported modules export the same name, you must use aliasing to disambiguate:

import "../billing/types" as billing_types
import "../shipping/types" as shipping_types

action process(
  invoice: billing_types.Invoice,
  label: shipping_types.ShippingLabel,
) {
  // ...
}

Without aliases, two modules named types would conflict.

Module Initialization

Modules do not have initialization logic. There are no init blocks or module-level statements. All computation happens within declaration bodies (actions, functions, workflows, etc.).

Project Structure Conventions

While not enforced by the compiler, the conventional project layout is:

my-project/
  grove.toml              # project configuration
  shared/
    types/                # shared type definitions
      types.grove
  order/                  # order domain module
    types.grove
    events.grove
    actions.grove
    queries.grove
    workflows.grove
    order_test.grove
  customer/               # customer domain module
    types.grove
    events.grove
    actions.grove
    customer_test.grove

Each domain concept gets its own module directory. Shared types used across modules live in a shared/ area.

See Also