Solc AST generator

I’m trying to obtain an AST from a solidity contract
I cannot find anyway to achieve that using solc

I saw on previous versions there were --ast options in command line
or other ast commands…
Now i cannot find any of that

Tried also doing an input json with the contract but i get

Cannot retry compilation with SMT because there are no SMT solvers available.
{“sources”:{“SolcTest.sol”:{“id”:0}}}
every time…

Is there a way to obtain an AST using solc ?

thanks

There are many ways to achieve such output. For example:

  • solc path/to/Contract.sol --ast-compact-json returns AST in a JSON format.
  • solc path/to/Contract.sol --ir-ast-json returns AST of Intermediate Representation (IR) of contract in a JSON format.
  • solc path/to/Contract.sol --ir-optimized-ast-json returns AST of optimized Intermediate Representation (IR) of the contract in a JSON format.
  • solc path/to/Contract.sol --combined-json ast returns a single JSON document containing the AST.

You can use solc --help and search for all the options you can use to get the AST.

thanks for the reply… but the reason i posted is because that doesn’t work
This is my solc version:
0.8.28+commit.7893614a.Emscripten.clang

This is my solc help
$ npx solc --help
Usage: solcjs [options]

Options:
-V, --version output the version number
–version Show version and exit.
–optimize Enable bytecode optimizer. (default: false)
–optimize-runs The number of runs specifies roughly how often each opcode of the deployed code will be executed across the lifetime of the contract. Lower values will optimize more for
initial deployment cost, higher values will optimize more for high-frequency usage.
–bin Binary of the contracts in hex.
–abi ABI of the contracts.
–standard-json Turn on Standard JSON Input / Output mode.
–base-path Root of the project source tree. The import callback will attempt to interpret all import paths as relative to this directory.
–include-path <path…> Extra source directories available to the import callback. When using a package manager to install libraries, use this option to specify directories where packages are
installed. Can be used multiple times to provide multiple locations.
-o, --output-dir Output directory for the contracts.
-p, --pretty-json Pretty-print all JSON output. (default: false)
-v, --verbose More detailed console output. (default: false)
-h, --help display help for command

There is no ast option
error: unknown option ‘–ast-compact-json’

Hi @eldanba, unlike the native solc binary, solc-js requires using the standard JSON input interface to specify AST outputs. Unfortunately, not all flags available in the native command-line binary are supported in solc-js via command-line.

This means you can only generate the AST by providing a JSON input that includes the desired output selection when using solc-js. For example:

npx solc --standard-json - <<EOF
{
  "language": "Solidity",
  "sources": {
    "test.sol": {
      "content": "contract C { function foo(uint x) external pure returns(uint) { return x + 2; }}"
    }
  },
  "settings": {
    "outputSelection": {
      "*": {
        "": ["ast"]
      }
    }
  }
}
EOF

or

var solc = require("solc");

var input = {
  language: "Solidity",
  sources: {
    "test.sol": {
      content: "contract C { function foo(uint x) external pure returns(uint) { return x + 2; }}"
    }
  },
  settings: {
    outputSelection: {
      "*": {
        "": ["ast"]
      }
    }
  }
};

var output = JSON.parse(solc.compile(JSON.stringify(input)));
console.log(output.sources["test.sol"]);

Please check out our documentation for more details about the Compiler Input and Output JSON Interface

thanks a lot for your input @r0qs
that did work with that small content.

i’m trying with a much bigger contract having some imports… it gives me:
“message”: “Source "@openzeppelin/contracts/math/SafeMath.sol" not found: File import callback not supported”,

I understand the error because safeMath is not there… but i just want to generate the ast from the contract… dont care about dependencies
Is there a way to bypass those kind of errors ?

I see, sadly, as far as I know, using solc-js the AST output is only file-level and not contract-level. So for your case I believe you would need to provide the missing import callback. You can see an example in javascript here: Example usage with import callback

You can also use the flag --base-path so the compiler will be able to find the sources (see: GitHub - ethereum/solc-js: Javascript bindings for the Solidity compiler), for example, assuming that you have a lib.sol at the root directory of your project, you could do:

echo "library L{}" > lib.sol

npx solc --standard-json --base-path . - <<EOF
{
  "language": "Solidity",
  "sources": {
    "test.sol": {
      "content": "import \"lib.sol\"; contract C { function foo(uint x) external pure returns(uint) { return x + 2; }}"
    }
  },
  "settings": {
    "outputSelection": {
      "*": {
        "": ["ast"]
      }
    }
  }
}
EOF

Oh, actually, just a correction, you can have the AST output by “contract-level” in solc-js as well, the following should work (note the change in the outputSelection from "*" to "test.sol"). But it will also return an empty sources entry for all other sources as well.

echo "library L{}" > lib.sol

npx solc --standard-json --base-path . - <<EOF
{
  "language": "Solidity",
  "sources": {
    "test.sol": {
      "content": "import \"lib.sol\"; contract C { function foo(uint x) external pure returns(uint) { return x + 2; }}"
    }
  },
  "settings": {
    "outputSelection": {
      "test.sol": {
        "": ["ast"]
      }
    }
  }
}
EOF

But you are required to specify the import callback or use the --base-path which has a default import callback in any case.

Thanks a lot for your answers, with examples and all… I really appreciate. I will try this, thanks again for taking the time!

Im trying to execute this in node js as astGenerator.js
This file is located in the same folder as ContractsFactory.sol which is the file I’m trying to get the AST and all import of that file are there (except the @openzeppelin ones that are in node_modules)

const solc = require("solc");
const fs = require("fs");
const shell = require("shelljs");

const solcVersion = "0.8.28";
console.log("solc version:", solc.version());

const filename = "ContractsFactory.sol";

const sourceCode = fs.readFileSync(filename, "utf8");

const input = {
  language: "Solidity",
  sources: {
    // [filename]: {
    "ContractsFactory.sol": {
      content: sourceCode,
    },
  },
  settings: {
    outputSelection: {
      filename: {
        "": ["ast"],
      },
    },
  },
};

// Convert to json
const jsonInput = JSON.stringify(input);

const { code, stdout } = shell.exec(
  `npx solc --standard-json --base-path . - <<EOF
    ${input}
    EOF`
);

This is falling
I tried different ways to do that, but I cannot make it work
It will be a nice addition to solhint project to avoid depending on parsers and getting the AST directly

i can also used --include-path

but when i executed i get:

node astGenerator.js
solc version: 0.8.28+commit.7893614a.Emscripten.clang

Cannot retry compilation with SMT because there are no SMT solvers available.
{“errors”:[{“component”:“general”,“formattedMessage”:“parse error at line 2, column 5: syntax error while parsing value - invalid literal; last read: ‘"ast"]}}}}<U+000A> E’; expected end of input”,“message”:“parse error at line 2, column 5: syntax error while parsing value - invalid literal; last read: ‘"ast"]}}}}<U+000A> E’; expected end of input”,“severity”:“error”,“type”:“JSONError”}]}
stdout: >>> Cannot retry compilation with SMT because there are no SMT solvers available.
{“errors”:[{“component”:“general”,“formattedMessage”:“parse error at line 2, column 5: syntax error while parsing value - invalid literal; last read: ‘"ast"]}}}}<U+000A> E’; expected end of input”,“message”:“parse error at line 2, column 5: syntax error while parsing value - invalid literal; last read: ‘"ast"]}}}}<U+000A> E’; expected end of input”,“severity”:“error”,“type”:“JSONError”}]}

I see. Since you want to use solc-js as a library, the recommended approach is to define an import callback function that loads all contracts from disk and includes them in the JSON input. As shown in this example: GitHub - ethereum/solc-js: Javascript bindings for the Solidity compiler. Pay special attention to the findImports function and how it is used.

However, implementing this can be tricky, depending on your setup. If you’re using Foundry, it typically installs dependencies under ./lib and applies remappings, while Hardhat relies on node_modules.

I created a small example that works with both Foundry and Hardhat. I haven’t exhaustively tested it, but I believe it should serve as a helpful guide to achieving what you’re trying to do.

forge init test --no-commit
cd test
forge install OpenZeppelin/openzeppelin-contracts --no-commit
echo "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/" > remappings.txt

astGenerator.js:

const solc = require("solc");
const fs = require("fs");
const path = require("path");

console.log("solc version:", solc.version());

const projectRoot = path.resolve(__dirname);
const remappingsPath = path.join(projectRoot, "remappings.txt");
// Read remappings and store them in an object
const remappings = fs.existsSync(remappingsPath)
	? Object.fromEntries(
		fs.readFileSync(remappingsPath, "utf8")
			.split("\n")
			.map(line => line.trim())
			.filter(Boolean)
			.map(line => line.split("="))
	)
	: {};

// Remmapings lookups
function resolveWithRemappings(importPath) {
	for (const prefix in remappings) {
		if (importPath.startsWith(prefix)) {
			const resolvedPath = path.resolve(projectRoot, remappings[prefix], importPath.slice(prefix.length));
			return { contents: fs.readFileSync(resolvedPath, "utf8") };
		}
	}
	return null;
}

function findImports(importPath) {
	console.log(`Resolving import: ${importPath}`);

	// Remappings.txt
	const remappedImport = resolveWithRemappings(importPath);
	if (remappedImport) return remappedImport;

	// Node modules
	const nodeModulesPath = path.resolve(projectRoot, "node_modules", importPath);
	if (fs.existsSync(nodeModulesPath))
		return { contents: fs.readFileSync(nodeModulesPath, "utf8") };

	// Fallback to relative path
	const resolvedPath = path.resolve(projectRoot, importPath);
	if (fs.existsSync(resolvedPath))
		return { contents: fs.readFileSync(resolvedPath, "utf8") };

	return { error: `File not found: ${importPath}` };
}

const filename = "C.sol";
const sourceCode = fs.readFileSync("src/C.sol", "utf8");

// If you want to extract the AST of a single file:
var input = {
	language: "Solidity",
	sources: {
		[filename]: {
			content: sourceCode,
		},
	},
	settings: {
		outputSelection: {
			[filename]: {
				"": ["ast"],
			},
		},
	},
};

const output = JSON.parse(solc.compile(JSON.stringify(input), { import: findImports }));
if (output.sources && output.sources[filename]) {
	console.log(JSON.stringify(output.sources[filename].ast, null, 2));
} else {
	console.log("No AST found for", filename);
}

// If you want to extract the AST of all contracts:
// NOTE: remember to change the AST output selection to "*" in the settings
//Object.keys(output.sources || {}).forEach(contractName => {
//	console.log(`AST for ${contractName}:`);
//	console.log(JSON.stringify(output.sources[contractName].ast, null, 2));
//});

src/C.sol:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract C is Ownable {
    constructor(address _owner) Ownable(_owner) {}
}

Wow, thanks a lot for taking the time to this
I will look into that … again I really appreciate

I guess the only blocker will be the fact that if we have different pragmas, solc versions need to be detected and downloaded beforehand for each contract to generate ast

Thanks!