A Dodo project is an ordinary folder with a main.dodo entry file. Start with one
file, then move reusable code into imported files or subfolders as the program
grows. Every source file starts with a package declaration.
Three names have different jobs:
| Name | Example | Meaning |
|---|---|---|
| Entry filename | main.dodo |
The file selected by dodo run in a project folder. |
| Package declaration | package math |
The package named by a source unit. |
| Import path and local qualifier | import "math", then math.answer() |
Find a dependency and access its public declarations. |
An optional dodo.toml can select entries and save build settings. No manifest is required. There is no lockfile, package manager, dependency cache, or separate library project type. Follow the complete shared-code example below to make a two-package project.
Project entry and subfolders
Without a manifest, run dodo run, dodo check, or dodo build from the project folder to use
main.dodo. dodo compile is an alias for dodo build. Passing a project
folder explicitly selects the same entry: dodo run app uses app/main.dodo.
Passing a file, such as dodo run app/main.dodo, uses that file directly.
Only the entry file and its imports are loaded. Other files beside main.dodo
and unimported subfolders are not included automatically. Missing main.dodo
is an error; the compiler does not search parent folders or infer another entry.
Put shared code in subfolders and import their paths. An imported folder’s
immediate .dodo files form one package, in sorted order. They must all declare
the same package and share declarations and import aliases. Nested folders are
included only through imports. No filename is special inside an imported
folder: it needs neither main.dodo nor lib.dodo, and a folder named lib
behaves like any other local folder.
Local imports
import "math" resolves relative to the importing package directory to either
math.dodo or a math/ directory containing .dodo files. Nested import paths
are supported; the imported package declaration must match the final path
component. An ambiguous file-and-directory match, cycle, duplicate import alias,
or conflicting package mapping is diagnosed. Dependencies are local files;
there is no registry, network resolver, or re-export mechanism.
Only directly imported package names are available in a package.
Paths use / even on Windows. They must be relative, with nonempty components;
absolute paths, . and .. components, and backslashes are rejected. A local
import is resolved from the importing package’s directory, so organize shared
dependencies within that directory tree. The compiler does not fetch remote
dependencies.
For a single-file dependency:
app/
main.dodo // imports "math"
math.dodo // begins with package math
For a directory dependency, use math/ instead of math.dodo, as in the complete
example below. Having both at the same import path is ambiguous and is rejected.
Visibility and methods
Declarations and fields are private unless pub; public functions cannot expose
private types. Public enum variants are available with the enum. Struct methods
are statically dispatched; associated functions use Type.name(...).
Public methods of a private struct may mention their own struct type, allowing
the struct to implement generic protocols without publishing its name. Other
private types remain prohibited in public signatures.
package counter
pub struct Counter {
value: u32
pub fn new(start: u32) -> Self {
Counter { value: start }
}
pub fn current(&self) -> u32 {
self.value
}
}
A caller importing counter can construct counter.Counter.new(0) and call
current(), but cannot read or write its private value field directly. A field
needs its own pub if callers should access it. Names beginning with capital
letters are not automatically public.
Object exports and reachability
An import makes public declarations available to the source checker; it does
not export every imported function from the compiled object. The compiler keeps
root-package public functions and methods, root main, and all extern "C"
definitions as exports. Other functions, including generic specializations, are
removed when unreachable, at every optimization level. Reachability includes
implicit destructors and callback addresses. ELF and COFF output place functions
in separate code sections so a linker can discard unused exported code as well.
Compiler-provided packages
core/mmio, core/ptr, and core/mem are compiler-provided imports.
core.drop(value) destroys an owned value early. Intrinsics are ordinary checked
calls with compiler lowering, not user-definable macros. See memory and foreign calls
for the supported intrinsic signatures.
core.wrapping_add, core.wrapping_sub, and core.wrapping_mul provide
explicit wrapping integer arithmetic
without an import.
The compiler also embeds the Dodo source packages listed in
the standard library. Imports beginning with core/,
alloc/, or std/ always resolve from this bundled library, independent of the current
directory; local files cannot shadow them. Unknown standard imports are errors.
Only the imported packages and their dependencies are loaded.
Package names core, mem, ptr, and mmio are reserved. Other package identities follow their resolved paths.
Names default to the final path component; import "core/bytes" as raw assigns
a package-local alias. This allows importing std/bytes and core/bytes
together, and allows transitive dependencies to use their own aliases without
conflicts. Importing two distinct paths under the same local name is an error;
assign an explicit alias to disambiguate. An alias does not rename the imported
package declaration and does not grant access to private declarations. Aliases
are shared across files in one directory package; inconsistent aliases for the
same path are rejected. Each package must directly import the
intrinsics it uses; a dependency’s import does not grant access to its callers.
Resolve a name collision with aliases
package main
import "core/bytes" as raw
import "std/bytes" as binary
fn main() {
core.assert_eq(raw.compare(b"a", b"a"), 0i32)
}
Both imports normally use the name bytes; explicit aliases give them distinct
local qualifiers. You can use raw and binary only in this package. The
dependencies keep their original declarations and their own import scopes.
A project with shared code
Create this directory layout:
app/
main.dodo
math/
answer.dodo
base.dodo
Put the entry point in app/main.dodo:
package main
import "math"
fn main() -> i32 {
math.answer() - 42
}
Put the public function in app/math/answer.dodo:
package math
pub fn answer() -> i32 {
base() + 2
}
Put its private helper in app/math/base.dodo:
package math
fn base() -> i32 {
40
}
Both files in math/ are loaded by import "math". The entry file can call
math.answer() because it is public. The helper is available within math/.
Check, run, and compile from the project folder:
cd app
dodo check
dodo run
dodo compile
./build/app
The executable is named after the project folder: build/app.
The program exits successfully with no output. You can add more ordinary
subfolders in the same way; there is no library registration step.
The editor setup guide explains how to check the entry file and combine files while editing an imported folder.
On Windows, the compiled executable has an .exe suffix; run it as
./build/app.exe from PowerShell. dodo run selects and launches the executable
for your host automatically.
Initialization and global state
Imports load declarations, not executable initialization code. A function named
init runs only if your program calls it. Package constants and statics require
compile-time initializers, so the order of imports does not define a runtime
initialization sequence. Constants can refer forward to other constants;
dependency cycles are diagnosed.
Prefer constructing state in main and passing it to functions. When static
storage is needed, see static storage
for initialization, unsafe mutable access, and program-exit behavior.
Troubleshoot package errors
| Diagnostic or symptom | Check |
|---|---|
Missing main.dodo |
Run from the project folder, or pass the desired file explicitly. |
| Helper declaration is missing | Entry-file siblings are not loaded automatically; move helpers to an imported package. |
| Import cannot resolve | Resolve the path from the importing package’s directory, and check its final component and package declaration. |
| Both file and directory match | Keep either name.dodo or name/ for that import. |
| Conflicting local package name | Add as alias to distinguish imports with the same last component. |
| Declaration or field is private | Mark the intended API pub; import aliases do not bypass visibility. |
| A dependency’s dependency is unavailable | Import the package directly where you use it; imports are not re-exports. |
| Import cycle | Move the shared declarations into a lower-level package both callers can import. |
Use dodo check main.dodo to verify the complete reachable project before
compiling. The compiler loads only imported packages and their dependencies,
including bundled packages; an unused folder does not participate in checking.