website is under construction
Language

Modules

Ghost has one import statement, and two things can appear on the right of it: a file on disk, or a module behind a scheme — a standard library module under ghost:, or a module an embedding host registered under a scheme of its own.

import "helpers"          // a .gs file sitting next to this one
import "ghost:math"       // the standard library's math module
import "lumen:canvas"     // a module Lumen registered under its own scheme

Ghost tells the two apart by the string alone: anything matching scheme: — two or more letters followed by a colon — is a scheme import, and everything else is a file path. (Two letters, so a Windows drive letter is never mistaken for a scheme.)

Importing files

Every Ghost file is its own module with its own scope. Paths are written without the .gs extension and are resolved relative to the file doing the importing — there is no project root, no search path, and no package registry.

Importing a file into another does not merge its scope. Two modules can define the same top-level variable without colliding.

Importing a whole file

A bare import runs the module's top-level code once and binds the module itself, under the last segment of its path, as a map of its top-level names:

import "beverages"
import "modules/helpers"

beverages.brew()
helpers.greet("world")

as chooses the name instead:

import "modules/helpers" as h

h.greet("world")

Nothing stops you from ignoring that binding and importing purely for the side effect of running the file.

Binding names

Every top-level variable in a module is exportable. Name the ones you want:

import Request, Response from "network"

This imports and binds Request and Response from the network module, making them available in your file.

Asking for a name a module does not export is an error, and Ghost suggests the nearest name it does export.

Aliases

Import a name under a different one with as:

import str as isString from "helpers"

Importing everything

* binds every top-level name the module has:

import * from "helpers"

Importing the standard library

The standard library lives behind the ghost: scheme, and — console and type aside — a script has to import what it uses.

import "ghost:math"                    // the whole module, bound to `math`
import "ghost:math" as m               // aliased
import { pi, sqrt } from "ghost:math"  // named imports
import pi, sqrt from "ghost:math"      // the same thing without the braces
import * from "ghost:math"             // every method, property, and class

The bare form binds the module itself, so dot access works as it always has: math.pi, math.sqrt(2).

The from forms pull individual methods, properties, and classes out by name. A property is evaluated once, at the import — import { pi } from "ghost:math" binds a plain number, the same value math.pi would read.

import { sqrt, pi } from "ghost:math"

console.log(sqrt(16)) // >> 4
console.log(pi)       // >> 3.141592653589793

The modules under ghost: are math, date, random, os, file, path, json, http, and ghost.

Using a module without importing it is a name error that tells you the exact import to add, rather than a generic "did you mean". Misspelling the module name itself — "ghost:mathh" — gets the same nearest-match suggestion every other unresolved name in Ghost gets.

Importing from a host

ghost: is not special-cased. It is simply the one scheme Ghost itself pre-registers, and a Go program embedding Ghost can claim a scheme of its own. Lumen, the game engine, registers its modules under lumen::

import "lumen:keyboard"
import { Image } from "lumen:image"

Named imports, aliases, and * all work identically whatever the scheme. A class a module exports binds the class value itself, and new works on it exactly as it does on a class declared in Ghost.

Combining the two forms

The module and a name out of it can be imported in one statement: the module name first, the braced list after it.

import image, { Spritesheet } from "lumen:image"

image.something()                 // the whole module, as `import "lumen:image" as image`
new Spritesheet("sheet.png")      // the named export

The braces matter here. import a, b from "path" — no brace after the comma — keeps its own, unrelated meaning of two named imports. The combined form works for file imports too, and import name, { * } from "path" combines the module with every export.

Shared imports

Ghost keeps track of every module it imports. A module is read, parsed, and evaluated once, no matter how many files import it; every later import of the same path binds what the first one produced.

Cyclic imports

Because Ghost memoizes imports, a cycle short-circuits rather than recursing forever:

// main.gs
import "a"

// a.gs
console.log("start a")
import "b"
console.log("end a")

// b.gs
console.log("start b")
import "c"
console.log("end b")

// c.gs
console.log("start c")
import "a"
console.log("end c")
start a
start b
start c
end c
end b
end a

A cycle that cannot be resolved this way is reported as an import error naming the cycle, rather than looping. Either way, cyclic imports are worth treating as a smell.

A worked example

main.gs
modules/
    functions.gs
    variables.gs

main.gs

import * from "modules/functions"
import * from "modules/variables"

console.log("Program loaded.")

add(one, two)
greet(hello)

functions.gs

function add(a, b) {
  console.log(a + b)
}

function greet(message) {
  console.log(message)
}

variables.gs

one = 1
two = 2
hello = "Hello, world!"

Modules are nothing more than other Ghost files, and can live anywhere in your program. Putting them in a modules directory is a convention, not a requirement.