Skip to content

Commit

Permalink
optimization: merge string appends
Browse files Browse the repository at this point in the history
  • Loading branch information
jfschwarz committed Feb 8, 2022
1 parent df3f2c7 commit 8a47243
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 87 deletions.
File renamed without changes.
80 changes: 63 additions & 17 deletions src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ export const compile = (template: string, options: Options = {}): string => {

const lines = processProgram(ast, {})
if (Object.keys(inputsType.members).length === 0) {
throw new Error("The template does not use any interpolation.")
throw new Error(
"The template file does not contain any template expressions."
)
}
const structDefs = solDefineStruct(inputsType, INPUT_STRUCT_NAME)

Expand All @@ -61,7 +63,7 @@ ${options.contract ? "contract" : "library"} ${options.name || "Template"} {
pure
returns (string memory ${RESULT_VAR_NAME})
{
${lines.join("\n")}
${lines.map((l) => l.line).join("\n")}
}
}
`,
Expand All @@ -74,18 +76,56 @@ ${options.contract ? "contract" : "library"} ${options.name || "Template"} {

/** AST processing function sharing access to inputsType variable via function scope */

interface CodeLine {
line: string
}
interface AppendStrings {
append: string[]
}
type Output = CodeLine | AppendStrings

function processProgram(
program: AST.Program,
pathAliases: Record<string, string>
) {
const lines = program.body.map((s) => process(s, pathAliases)).flat()
return lines
): CodeLine[] {
// generate complete output
const output = program.body.map((s) => process(s, pathAliases)).flat()

// merge appends into single statement until we hit the EVM's stack size limit
const LIMIT = 16
const merged: Output[] = []

output.forEach((out, i) => {
if ("line" in out) {
merged.push(out)
} else {
const last = merged[merged.length - 1]
if (
last &&
"append" in last &&
last.append.length + out.append.length <= LIMIT
) {
last.append = last.append.concat(out.append)
} else {
merged.push(out)
}
}
}, [] as Output[])

// convert appends to code lines
return merged.map((out) => {
if ("append" in out) {
return { line: solStrAppend(out.append) }
} else {
return out
}
})
}

function process(
statement: AST.Statement,
pathAliases: Record<string, string>
): string[] {
): Output[] {
switch (statement.type) {
case "ContentStatement":
return processContentStatement(statement as AST.ContentStatement)
Expand All @@ -106,21 +146,21 @@ ${options.contract ? "contract" : "library"} ${options.name || "Template"} {
throw new Error(`Unexpected statement type: ${statement.type}`)
}

function processContentStatement(statement: AST.ContentStatement) {
return [solStrAppend(`"${solEscape(statement.value)}"`)]
function processContentStatement(statement: AST.ContentStatement): Output[] {
return [{ append: [`"${solEscape(statement.value)}"`] }]
}

function processMustacheStatement(
statement: AST.MustacheStatement,
pathAliases: Record<string, string>
) {
): Output[] {
const path = statement.path
if (path.type === "PathExpression") {
const pathExpr = path as AST.PathExpression
const fullPath = resolvePath(pathExpr, pathAliases)

narrowInput(inputsType, fullPath, "string")
return [solStrAppend(fullPath)]
return [{ append: [fullPath] }]
}

throw new Error(`Unsupported path type: ${statement.path.type}`)
Expand All @@ -129,7 +169,7 @@ ${options.contract ? "contract" : "library"} ${options.name || "Template"} {
function processBlockStatement(
statement: AST.BlockStatement,
pathAliases: Record<string, string>
) {
): Output[] {
const { head } = statement.path

if (head === "each") {
Expand All @@ -142,7 +182,7 @@ ${options.contract ? "contract" : "library"} ${options.name || "Template"} {
function processEachBlock(
statement: AST.BlockStatement,
pathAliases: Record<string, string>
) {
): Output[] {
const path = statement.params[0]
if (path.type !== "PathExpression") throw new Error("Unsupported")
const pathExpr = path as AST.PathExpression
Expand All @@ -155,16 +195,20 @@ ${options.contract ? "contract" : "library"} ${options.name || "Template"} {
while (pathAliases[indexVarName]) indexVarName = incrementName(indexVarName)

const [itemVarName = "this", indexVarAlias = "@index"] =
statement.program.blockParams
statement.program.blockParams || []

return [
`for(uint256 ${indexVarName}; ${indexVarName} < ${iterateeResolvedPath}.length; ${indexVarName}++) {`,
{
line: `for(uint256 ${indexVarName}; ${indexVarName} < ${iterateeResolvedPath}.length; ${indexVarName}++) {`,
},
...processProgram(statement.program, {
...pathAliases,
[indexVarAlias]: indexVarName,
[itemVarName]: `${iterateeResolvedPath}[${indexVarName}]`,
}),
`}`,
{
line: `}`,
},
]
}
}
Expand Down Expand Up @@ -273,8 +317,10 @@ const resolvePath = (
return `${origin}${joined}`
}

const solStrAppend = (str: string) =>
`${RESULT_VAR_NAME} = string(abi.encodePacked(${RESULT_VAR_NAME}, ${str}));`
const solStrAppend = (strings: string[]) => {
const args = [RESULT_VAR_NAME, ...strings].join(", ")
return `${RESULT_VAR_NAME} = string(abi.encodePacked(${args}));`
}

const solEscape = (str: string) =>
str
Expand Down
14 changes: 7 additions & 7 deletions test/cases/birthchart/0.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"planets": [
{ "x": "0222", "y": "0387" },
{ "x": "0176", "y": "0486" },
{ "x": "0000", "y": "0000" },
{ "x": "0277", "y": "0252" },
{ "x": "0278", "y": "0166" },
{ "x": "0326", "y": "0142" },
{ "x": "0100", "y": "0510" },
{ "x": "0224", "y": "0393" },
{ "x": "0250", "y": "0356" },
{ "x": "0259", "y": "0271" },
{ "x": "0311", "y": "0247" },
{ "x": "0000", "y": "0000" },
{ "x": "0000", "y": "0000" },
{ "x": "0297", "y": "0275" }
{ "x": "0000", "y": "0000" },
{ "x": "0000", "y": "0000" },
{ "x": "0270", "y": "0379" }
]
}
48 changes: 32 additions & 16 deletions test/cases/birthchart/0.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
70 changes: 31 additions & 39 deletions test/cases/birthchart/Template.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,38 @@
pragma solidity ^0.8.6;

contract Template {
struct Planet {
string x;
string y;
}
struct Planet {
string x;
string y;
}

struct __Input {
Planet[] planets;
}
struct __Input {
Planet[] planets;
}

function render(__Input memory __input)
public
pure
returns (string memory __result)
{
__result = string(
abi.encodePacked(
__result,
'<svg\n version="1.1"\n xmlns="http://www.w3.org/2000/svg"\n viewBox="0 0 2000 3000"\n style="background: black;"\n>\n <style type="text/css">\n .bc{fill:none;stroke:#8BA0A5;}\n </style>\n <g>\n <circle class="bc" cx="1000" cy="1060" r="260"></circle>\n <circle class="bc" cx="1000" cy="1060" r="360"></circle>\n <circle class="bc" cx="1000" cy="1060" r="440"></circle>\n <circle class="bc" cx="1000" cy="1060" r="520"></circle>\n <line class="bc" x1="740" y1="610" x2="1260" y2="1510"></line>\n <line class="bc" x1="1260" y1="610" x2="740" y2="1510"></line>\n <line class="bc" x1="1450" y1="800" x2="550" y2="1320"></line>\n <line class="bc" x1="1450" y1="1320" x2="550" y2="800"></line>\n <g transform="translate(1000 1060)">\n'
)
);
for (uint256 __i; __i < __input.planets.length; __i++) {
__result = string(
abi.encodePacked(__result, ' <circle\n cx="')
);
__result = string(
abi.encodePacked(__result, __input.planets[__i].x)
);
__result = string(abi.encodePacked(__result, '"\n cy="'));
__result = string(
abi.encodePacked(__result, __input.planets[__i].y)
);
__result = string(
abi.encodePacked(
__result,
'"\n r="6.5"\n fill="white"\n ></circle>\n'
)
);
}
__result = string(
abi.encodePacked(__result, " </g>\n </g>\n</svg>")
);
function render(__Input memory __input)
public
pure
returns (string memory __result)
{
__result = string(
abi.encodePacked(
__result,
'<svg\n xmlns="http://www.w3.org/2000/svg"\n version="1.1"\n viewBox="0 0 2000 3000"\n style="background: black;"\n>\n <style type="text/css">\n .bc{fill:none;stroke:#8BA0A5;}\n </style>\n <g><clipPath id="clip">\n <!--\n Everything outside the circle will be\n clipped and therefore invisible.\n -->\n <circle cx="1000" cy="1060" r="520"></circle>\n </clipPath>\n <filter id="filter">\n <feTurbulence baseFrequency="0.1" seed="5"></feTurbulence>\n <feColorMatrix\n values="0 0 0 7 -4 0 0 0 7 -4 0 0 0 7 -4 0 0 0 0 1"\n ></feColorMatrix>\n </filter>\n <g clip-path="url(#clip)"><g filter="url(#filter)" transform="scale(2)">\n <rect width="100%" height="100%"></rect>\n </g>\n </g>\n <circle class="bc" cx="1000" cy="1060" r="260"></circle>\n <circle class="bc" cx="1000" cy="1060" r="360"></circle>\n <circle class="bc" cx="1000" cy="1060" r="440"></circle>\n <circle class="bc" cx="1000" cy="1060" r="520"></circle>\n <line class="bc" x1="740" y1="610" x2="1260" y2="1510"></line>\n <line class="bc" x1="1260" y1="610" x2="740" y2="1510"></line>\n <line class="bc" x1="1450" y1="800" x2="550" y2="1320"></line>\n <line class="bc" x1="1450" y1="1320" x2="550" y2="800"></line>\n <g transform="translate(1000 1060)">\n'
)
);
for (uint256 __i; __i < __input.planets.length; __i++) {
__result = string(
abi.encodePacked(
__result,
' <circle\n cx="',
__input.planets[__i].x,
'"\n cy="',
__input.planets[__i].y,
'"\n r="6.5"\n fill="white"\n ></circle>\n'
)
);
}
__result = string(abi.encodePacked(__result, " </g>\n </g>\n</svg>"));
}
}
20 changes: 18 additions & 2 deletions test/cases/birthchart/template.svg.hbs
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
viewBox="0 0 2000 3000"
style="background: black;"
>
<style type="text/css">
.bc{fill:none;stroke:#8BA0A5;}
</style>
<g>
<g><clipPath id="clip">
<!--
Everything outside the circle will be
clipped and therefore invisible.
-->
<circle cx="1000" cy="1060" r="520"></circle>
</clipPath>
<filter id="filter">
<feTurbulence baseFrequency="0.1" seed="5"></feTurbulence>
<feColorMatrix
values="0 0 0 7 -4 0 0 0 7 -4 0 0 0 7 -4 0 0 0 0 1"
></feColorMatrix>
</filter>
<g clip-path="url(#clip)"><g filter="url(#filter)" transform="scale(2)">
<rect width="100%" height="100%"></rect>
</g>
</g>
<circle class="bc" cx="1000" cy="1060" r="260"></circle>
<circle class="bc" cx="1000" cy="1060" r="360"></circle>
<circle class="bc" cx="1000" cy="1060" r="440"></circle>
Expand Down
Loading

0 comments on commit 8a47243

Please sign in to comment.