Skip to content

8. Examples#

Throughout the examples in this section we will use a sample contract. We need some setup code to create a CompilationUnit with the contents of the contract:

sample-contract.mts
import { CompilationUnit } from "@nomicfoundation/slang/compilation";
import { buildCompilationUnit } from "./compilation-builder.mjs";

const CONTRACT_VFS = new Map<string, string>([
  [
    "contract.sol",
    `
contract Counter {
  uint _count;
  constructor(uint initialCount) {
    _count = initialCount;
  }
  function count() public view returns (uint) {
    return _count;
  }
  function increment(uint delta) public returns (uint) {
    require(delta > 0, "Delta must be positive");
    _count += delta;
    return _count;
  }
}
    `,
  ],
]);

export default function (): Promise<CompilationUnit> {
  return buildCompilationUnit(CONTRACT_VFS, "0.8.0", "contract.sol");
}
compilation-builder.mts
import { Cursor } from "@nomicfoundation/slang/cst";
import { CompilationBuilder, CompilationUnit } from "@nomicfoundation/slang/compilation";

function buildReadFile(virtualFs: Map<string, string>): (fileId: string) => Promise<string> {
  return async (fileId: string) => {
    const contents = virtualFs.get(fileId);
    if (!contents) {
      throw new Error(`${fileId} not found`);
    }
    return contents;
  };
}

async function resolveImport(_sourceFileId: string, importPath: Cursor) {
  const importLiteral = importPath.node.unparse();
  const importString = importLiteral.replace(/^["']/, "").replace(/["']$/, "");
  return importString;
}

export async function buildCompilationUnit(
  virtualFs: Map<string, string>,
  version: string,
  mainFileId: string,
): Promise<CompilationUnit> {
  const builder = CompilationBuilder.create({
    languageVersion: version,
    readFile: buildReadFile(virtualFs),
    resolveImport,
  });

  await builder.addFile(mainFileId);

  return builder.build();
}

For the last two examples we also need an easy way to obtain a Cursor pointing to a specific line and column in a file. This can be achieved using the Cursor Navigation API:

find-terminal-node-at.mts
import assert from "node:assert";
import { Cursor } from "@nomicfoundation/slang/cst";

export function findTerminalNodeAt(cursor: Cursor, line: number, column: number): Cursor | undefined {
  const range = cursor.textRange;
  if (
    line < range.start.line ||
    (line == range.start.line && column < range.start.column) ||
    line > range.end.line ||
    (line == range.end.line && column > range.end.column)
  ) {
    // initial cursor is outside of the range of the CST tree
    return undefined;
  }

  outer: while (cursor.node.isNonterminalNode()) {
    assert(cursor.goToFirstChild());
    do {
      const childRange = cursor.textRange;
      if (line < childRange.end.line || (line == childRange.end.line && column <= childRange.end.column)) {
        continue outer;
      }
    } while (cursor.goToNextSibling());

    // we should have found a child to recurse into (or the target terminal node)
    throw new Error("should not be reached");
  }
  assert(cursor.node.isTerminalNode());
  return cursor;
}