Module System
Prism provides a modern module system inspired by ES modules, allowing you to organize code into reusable components. The module system supports imports, exports, and proper isolation between modules.
Basic Usage
Exporting from Modules
You can export values, functions, and variables from a module:
// math.prism
export const PI = 3.14159
export const E = 2.71828
export const square = x => x * x
export const cube = x => x * x * x
export function circleArea(radius) {
return PI * square(radius)
}
// Default export
export default {
PI,
E,
square,
cube,
circleArea
}
Importing into Modules
Import functionality from other modules using various patterns:
// Named imports
import {PI, square} from "./math.prism"
// Default import
import math from "./math.prism"
// Namespace import
import * as mathUtils from "./math.prism"
// Renamed imports
import {square as sq, cube as cb} from "./math.prism"
// Mixed imports
import defaultMath, {PI, E} from "./math.prism"
Export Patterns
Named Exports
Export individual values with names:
// Direct export
export const VERSION = "1.0.0"
export const apiKey = "secret-key" ~> 0.9
// Export functions
export const greet = name => "Hello, " + name
export function calculate(x, y) {
return x * y
}
// Export after declaration
let userId = 12345
let userName = "Alice"
export {userId, userName}
// Export with renaming
let internalFunction = () => "internal"
export {internalFunction as publicAPI}
Default Exports
Each module can have one default export:
// Export default object
export default {
name: "MyModule",
version: "1.0.0",
initialize: () => console.log("Initialized")
}
// Export default function
export default function main() {
return "Main module function"
}
// Export default value
export default 42
Re-exports
Re-export from other modules:
// Re-export specific items
export {helper1, helper2} from "./helpers.prism"
// Re-export with renaming
export {oldName as newName} from "./legacy.prism"
// Re-export all named exports
export * from "./utilities.prism"
Module Resolution
Relative Paths
Modules are resolved relative to the importing file:
// Same directory
import {util} from "./utils.prism"
// Parent directory
import {shared} from "../shared.prism"
// Nested directories
import {component} from "./components/button.prism"
File Extensions
The .prism extension is optional in imports:
// Both work the same
import {data} from "./data.prism"
import {data} from "./data"
Absolute Paths
When an import path starts with /, Prism resolves it from the filesystem root (or the virtual root used by your host environment):
import settings from "/config/app-settings"
This is helpful for tooling that presents a virtual filesystem or for server-side projects with a known root directory.
Package-style Imports
Bare specifiers (no ./, ../, or /) are resolved like Node.js: Prism looks for a node_modules/<package> directory starting from the importing file's folder and walking up parent directories. You can omit .prism; the runtime tries both <package>.prism and <package>/index.prism.
import logger from "shared-logger"
import { formatNumber } from "@acme/prism-utils"
This allows sharing utilities between Prism projects without manual relative paths.
Module Execution
Import Hoisting
Imports are hoisted and processed before any other code:
// This works even though import appears after usage
console.log("PI is", PI)
import {PI} from "./math.prism"
Module Isolation
Each module has its own scope:
// module-a.prism
let secret = "module A secret"
export const public = "module A public"
// module-b.prism
import {public} from "./module-a.prism"
console.log(public) // "module A public"
console.log(secret) // Error: undefined variable
Single Evaluation
Modules are evaluated only once, even if imported multiple times:
// counter.prism
console.log("Counter module loaded")
let count = 0
export const increment = () => ++count
export const getCount = () => count
// main.prism
import {increment, getCount} from "./counter.prism"
import * as counter from "./counter.prism"
// "Counter module loaded" printed only once
increment()
console.log(getCount()) // 1
console.log(counter.getCount()) // 1 (same instance)
Hot Reloading and Cache Invalidation
During development you may want to reload a module after editing it. The runtime exposes helpers for this:
import { createRuntime } from '@prism-lang/core';
const runtime = createRuntime();
// execute your entry module once
await runtime.getModuleSystem().loadModule('/app/main.prism', runtime);
// Later, when /shared/state.prism changes:
await runtime.reloadModule('/shared/state.prism');
runtime.reloadModule(path) internally invalidates the cached module (and all dependents by default) before re-running it with the existing interpreter, so globals/LLM providers remain intact. If you only want to evict a single file without touching dependents, call runtime.invalidateModule(path, { invalidateDependents: false }) and then re-import it manually.
Tip:
prism run --watch app.prismuses the same APIs under the hood—when a file changes, the CLI invalidates the module, reloads it, and re-runs your entry module without tearing down the interpreter or LLM providers.
Confidence in Modules
Confidence values are preserved through imports and exports:
// sensor.prism
export const temperature = 23.5 ~> 0.92
export const humidity = 65 ~> 0.88
export async function readSensor() {
// Simulate sensor reading
let value = await getSensorValue()
return value ~> 0.9
}
// main.prism
import {temperature, readSensor} from "./sensor.prism"
// Confidence is preserved
console.log(temperature) // 23.5 ~> 0.92
let reading = await readSensor()
// Confidence flows through function calls
Async Modules
Modules can use async/await at the top level:
// data-loader.prism
const data = await fetchData("/api/config")
export const config = data.config
export const settings = data.settings
// main.prism
import {config, settings} from "./data-loader.prism"
// Module waits for async operations before exporting
Best Practices
1. Single Responsibility
Keep modules focused on a single concern:
// Good: math-utils.prism
export const add = (a, b) => a + b
export const multiply = (a, b) => a * b
export const average = nums => nums.reduce(add, 0) / nums.length
// Avoid: utilities.prism with mixed concerns
2. Clear Exports
Be explicit about what you export:
// Good: Clear public API
let internal = "private implementation"
let helper = x => x * 2
export function publicFunction() {
return helper(internal)
}
// Only publicFunction is accessible
3. Consistent Naming
Use consistent naming for modules and exports:
// user-service.prism
export function getUser(id) { /* ... */ }
export function updateUser(id, data) { /* ... */ }
export function deleteUser(id) { /* ... */ }
// Import with clear context
import * as userService from "./user-service.prism"
const user = userService.getUser(123)
4. Avoid Circular Dependencies
Structure modules to avoid circular imports:
// Bad: Circular dependency
// a.prism: import {b} from "./b.prism"
// b.prism: import {a} from "./a.prism"
// Good: Extract shared code
// shared.prism: export common = "shared"
// a.prism: import {common} from "./shared.prism"
// b.prism: import {common} from "./shared.prism"
5. Index Modules
Create index modules for cleaner imports:
// utils/index.prism
export * from "./math.prism"
export * from "./string.prism"
export * from "./array.prism"
// Clean import
import {add, capitalize, unique} from "./utils"
Common Patterns
Factory Modules
Export factory functions:
// logger.prism
export function createLogger(name) {
return {
log: msg => console.log(`[${name}] ${msg}`),
error: msg => console.error(`[${name}] ERROR: ${msg}`),
warn: msg => console.warn(`[${name}] WARN: ${msg}`)
}
}
// usage.prism
import {createLogger} from "./logger.prism"
const logger = createLogger("MyApp")
logger.log("Application started")
Configuration Modules
Centralize configuration:
// config.prism
export default {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3,
features: {
analytics: true,
debug: false
}
}
// app.prism
import config from "./config.prism"
if (config.features.debug) {
console.log("Debug mode enabled")
}
Service Modules
Organize related functionality:
// api-service.prism
let baseUrl = "https://api.example.com"
export async function get(endpoint) {
let response = await fetch(baseUrl + endpoint)
return response.json()
}
export async function post(endpoint, data) {
response = await fetch(baseUrl + endpoint, {
method: "POST",
body: JSON.stringify(data)
})
return response.json()
}
// usage.prism
import * as api from "./api-service.prism"
const users = await api.get("/users")
const newUser = await api.post("/users", {name: "Bob"})
Limitations
Currently, the Prism module system has some limitations:
- No Dynamic Imports: All imports must be static
- Limited Circular Dependencies: Circular dependencies are partially supported:
- ✅ Functions can reference each other across circular imports
- ❌ Values cannot be used during module initialization in circular dependencies
- Example that works:
// a.prism
import {funcB} from "./b.prism"
export funcA = () => "A"
export callB = () => funcB()
// b.prism
import {funcA} from "./a.prism"
export funcB = () => "B"
export callA = () => funcA() - Example that doesn't work:
// a.prism
import {valueB} from "./b.prism"
export valueA = "A"
// b.prism
import {valueA} from "./a.prism"
export valueB = "B uses " + valueA // valueA is not initialized here
- File System Based: Modules must exist as files (no virtual modules)
- No Import Maps: No support for import maps or module aliasing
These limitations may be addressed in future versions of Prism.