Skip to content

7.1. Compilation Units#

In order for Slang to perform semantic analysis on a program, it first needs to compile a list of all source files into a single CompilationUnit.

We offer a CompilationBuilder API that allows you easily do that. It will parse the provided source files, analyze the import statements within, and then transitively resolve all dependencies, until all source files have been loaded.

It is important to note that Slang is designed to be modular, and work in any environment (CLI, browser, etc..). This means it cannot access the file system directly, or make any assumptions about where your contracts are, and where their dependencies are installed.

Instead, it expects the user to provide it with a couple of callbacks to handle these tasks:

Reading Files#

The first callback is a function that will read the contents of a source file. That typically means using an API like NodeJS's fs.readFile() for local files, or browser's fetch() for remote ones.

Note that this API is also error-tolerant. If the file is not found, or cannot be read, your callback can simply return undefined to indicate that the file is not available.

For simplicity, let's assume that we have the source files defined in code:

read-file.mts
const VIRTUAL_FS = new Map<string, string>([
  [
    "contract.sol",
    `
      import { Log } from "events.sol";

      contract MyContract {
        function test() public {
          emit Log(msg.sender, "Hello World!");
        }
      }
    `,
  ],
  [
    "events.sol",
    `
      event Log(address indexed sender, string message);
    `,
  ],
]);

export async function readFile(fileId: string) {
  return VIRTUAL_FS.get(fileId);
}

The exact semantics of the fileId used throughout the Compilation API will depend on your implementation of the readFile callback. They could be paths, URLs, or opaque IDs.

Resolving Imports#

The second callback is a function that will resolve an import statement to the imported source file. In a real-world scenario, dependencies can be imported from relative paths on disk, a remote provider like IPFS, or even NPM packages.

For example, a package manager like npm would install the dependencies into sub-directory of node_modules, and users can then resolve their locations via NodeJS path.resolve() or browsers import.meta.resolve() APIs.

Note that likewise, this API is also error-tolerant. If the import cannot be resolved, your callback can also return undefined to indicate that the import is not available, and the builder will skip it.

For simplicity, let's just assume that dependencies will always be imported by their bare file name:

resolve-import.mts
import { Cursor } from "@nomicfoundation/slang/cst";

export async function resolveImport(_sourceFileId: string, importPath: Cursor) {
  // cursor points to the import string literal:
  const importLiteral = importPath.node.unparse();

  // remove surrounding quotes:
  const importString = importLiteral.replace(/^["']/, "").replace(/["']$/, "");

  return importString;
}

The import resolution callback should return a fileId, and should be meaningful to the readFile callback. We use filenames here, but URLs or opaque unique IDs would work as well.

Running the compilation builder#

With these callbacks defined, we can now create a CompilationBuilder and add our source files to it. Note that in the example below, we don't need to add dependencies, as they will be resolved and loaded automatically.

compilation-builder.mts
import { CompilationBuilder, CompilationUnit } from "@nomicfoundation/slang/compilation";
import { readFile } from "./read-file.mjs";
import { resolveImport } from "./resolve-import.mjs";

export async function buildCompilationUnit(): Promise<CompilationUnit> {
  const builder = CompilationBuilder.create({
    languageVersion: "0.8.28",
    readFile,
    resolveImport,
  });

  await builder.addFile("contract.sol");

  return builder.build();
}

Inspecting the compilation unit#

The built CompilationUnit will then contain all the source files, along with their syntax trees.

compilation-unit.mts
import assert from "node:assert";
import { assertNonterminalNode, NonterminalKind } from "@nomicfoundation/slang/cst";
import { buildCompilationUnit } from "../../common/compilation-builder.mjs";

test("compilation unit", async () => {
  const unit = await buildCompilationUnit();

  const files = unit.files();
  assert.equal(files.length, 2);

  assert.equal(files[0].id, "contract.sol");
  assertNonterminalNode(files[0].tree, NonterminalKind.SourceUnit);

  assert.equal(files[1].id, "events.sol");
  assertNonterminalNode(files[1].tree, NonterminalKind.SourceUnit);
});