Preprocessor

We developed Psol, the first ever Solidity lexical preprocessor, in order to enhance smart contract development in ways that were never possible before.

Psol (Preprocessed Solidity)

Psol is a lexical preprocessor for Solidity. Among many use cases of Psol is context variables for conditional compilation, macro substitution, inline code-describing unit tests, remote dependency auto-fetching and many others.

Features

  • Fully Solidity-compatible

  • Remote Contract Inclusion

  • Macro Substitution

  • Conditional Compilation

  • Context Variables

  • Inline Mocha Unit Testing

  • Smart Contract Lifecycle Hooks

Usage

Solidity preprocessing works each before the Solidity compiler processes a .psol file from the contracts/ directory. This includes running the development environment by running parasol or deploying contract by running parasol deploy [network]. The usage of the preprocessor only requires knowledge of Javascript syntax.

Because Parasol is about freedom, the usage of the preprocessor is completely optional. Only file names that end with the .psol extension in the contracts/ directory will be preprocessed. All.sol files are skipped by the preprocessor and are compiled directly.

Javascript Execution & Conditional Rendering

By default, any Javascript code surrounded by Mustache templating syntax {{ }} will be executed during preprocessing.

If Condition

Surrounding a Javascript if condition header and closing tag with the {{ }} tags will render the contained content conditionally. This is particularly useful if pieces of your code should only be compiled before deployment on particular networks but not others, or if they should be compiled differently for different networks.

Syntax:

{{ if (condition) { }}
// statement
{{ } }}

or using else:

{{ if (condition) { }}
// content
{{ } else { }}
// content
{{ } }}

Example:

Source:

function transfer(address _to, uint256 _value) public returns (bool success) {
require(_balances[msg.sender] >= _value);
_balances[msg.sender] -= _value;
_balances[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
// Preprocessor directive. Only compiles if in 'dev' network
{{ if (network === "dev") { }}
// This function in insecure but may be useful for testing during development
function forceTransfer(address _from, address _to, uint256 _value) public returns (bool success) {
require(_balances[_from] >= _value);
_balances[_from] -= _value;
_balances[_to] += _value;
emit Transfer(_from, _to, _value);
return true;
}
{{ } }}

When running parasol in development mode, the output will be:

function transfer(address _to, uint256 _value) public returns (bool success) {
require(_balances[msg.sender] >= _value);
_balances[msg.sender] -= _value;
_balances[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
// Preprocessor directive. Only compiles if in 'dev' network
function forceTransfer(address _from, address _to, uint256 _value) public returns (bool success) {
require(_balances[_from] >= _value);
_balances[_from] -= _value;
_balances[_to] += _value;
emit Transfer(_from, _to, _value);
return true;
}

When running parasol deploy [network] on any network, the output becomes:

function transfer(address _to, uint256 _value) public returns (bool success) {
require(_balances[msg.sender] >= _value);
_balances[msg.sender] -= _value;
_balances[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}

Loops

Identical to if conditions, surrounding any Javascript loop header and closing tag with the {{ }} tags will repeat the rendered content

Syntax:

{{ for ([initialExpression]; [condition]; [incrementExpression]) { }}
// statement
{{ } }}

Example:

Source:

{{ for (var i = 0; i < 2; i++) { }}
string private variable{{= i}};
{{ } }}

Output:

string private variable0;
string private variable1;

Variable Interpolation

By default, surrounding a Javascript variable with {{= }} tags, will interpolate (inject) its value at its location.

Example:

Source:

{{ tokenName = "myToken" }}
string private name = "{{= tokenName}}";

Output:

string private name = "myToken";

Default Context Variables

web3 Object

The active web3 1.0 instance. If the already attached to the Ganache provider and 10 generated accounts funded with 100 ETH each. Read the web3 1.0 docs for full API

network String

The name of the current network. When using parasol, the network is dev. Otherwise, the network name is the value of [network] argument of the parasol deploy [network] command.

accounts Array

List of addresses available to the web instance. If used in the dev network, the list is generated on runtime for development blockchain network. When used with parasol deploy [network], the list if generated from the private keys at the secrets.json file.

strict Boolean

Strict mode as configured at the parasol.js file. If changed, it will trigger strict mode where both compilation warnings and errors will abort deployment.

ignore Function

When called, it will instruct the compiler to ignore this file completely at compile time. It takes one argument, which is the context of the .psol file.

abort Function

When called, it will abort all contract deployments after compilation. It takes one optional argument which is a string indicating the reason of abortion.

parasol Object

This object contains all internal hook functions which can be used as a low-level access to Parasol's life cycle from within .psol files.

test Function

Allows for in-line mocha unit tests in .psol files. Takes context as a first argument, test description as a second argument and test function as a third argument.

Custom Context Variables

Adding custom variables to the context of .psol files can be done through the parasol.js configuration file in the project root directory.

Example:

parasol.js:

parasol.js
module.exports = {
preprocessor: {
context: {
title: "A minimal token contract",
tokenSupply: "1*10**28",
tokenName: "Token",
tokenDecimals:18,
tokenSymbol: "TOK"
},
strict: false // If true, strict mode will abort deployment on warnings as well as errors
}
}

contracts/Token.psol:

pragma solidity ^0.4.24;
/// @title {{= title }}
contract Token {
string public name = "{{= tokenName}}";
string public symbol = "{{= tokenSymbol}}";
uint8 public decimals = {{= tokenDecimals}};
uint256 public totalSupply = {{= tokenSupply}};
mapping (address => uint256) public _balances;
event Transfer(address indexed _from, address indexed _to, uint256 _value);
constructor() public {
_balances[msg.sender] = totalSupply;
}
function transfer(address _to, uint256 _value) public returns (bool success) {
require(_balances[msg.sender] >= _value);
_balances[msg.sender] -= _value;
_balances[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
function balanceOf(address _owner) constant public returns (uint256 balance) {
return _balances[_owner];
}
}

Inline Mocha Unit Tests

Instead of writing separate unit test files, Psol allows you to write inline code-describing Mocha unit tests using javascript:

Syntax:

{{
test(this, [testDescription], [testFunction])
}}

Example:

{{
test(this, 'Balance should change balance after transfer()', async function() {
var preBalance = await contracts["Token.psol:Token"].methods.balanceOf(accounts[0]).call()
await contracts["Token.psol:Token"].methods.transfer(accounts[1], 1).send()
var postBalance = await contracts["Token.psol:Token"].methods.balanceOf(accounts[0]).call()
assert.notEqual(preBalance, postBalance)
})
}}

Lifecycle Hooks

You can also use the contract lifecycle hooks to control the contract compilation and deployment. For example, to abort compilation if a specific test fails.

onCompiled

Executes a callback function after the compilation of this file.

{{
parasol.onCompiled(this, function(fileName){
console.log("File: " + fileName + " compiled")
})
}}

onDeployed

Executes a callback function after the deployment of each contract in this file

{{
parasol.onDeployed(this, function(contract, contractName){
console.log(contractName + " deployed")
})
}}

onError

Executes a callback function for each error in this file

{{
parasol.onError(this, function(e){
console.log("Error: " + e)
})
}}

Template Syntax Customization

Psol has the default Underscore template ERB-style Syntax. But, It provides you the settings object at the parasol.js configuration file that allows you to manipulate Underscore template settings and use different symbols to set off interpolated code.

For example, the default user settings object switches the syntax to Mustache.js-style symbols {{ }}. This makes it easier for beginners to kick-off instead of using the ERB-style syntax. Simply removing the settings objects will revert back to the default ERB-style syntax:

parasol.js
// Mustache.js style syntax
var config = {
settings: {
evaluate: /{{([\s\S]+?)}}/g,
interpolate: /{{=([\s\S]+?)}}/g,
escape: /{{-([\s\S]+?)}}/g
}
}

Hence, creating your own templating style with ease.