diff --git a/package.json b/package.json index d885a51ab..4db5dec06 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "test": "jest", "coverage": "cross-env COVERAGE=true jest", "release": "yarn clean && yarn build && yarn coverage && yarn release-it --npm.yarn1", + "type": "tsc --noEmit", "lint": "yarn eslint .", "lint:schema": "ajv validate -s schemas/configSchema.json -d tact.config.json", "fmt": "yarn prettier -l -w .", diff --git a/src/generator/writers/writeFunction.ts b/src/generator/writers/writeFunction.ts index 25cc63cf6..88c3fc51c 100644 --- a/src/generator/writers/writeFunction.ts +++ b/src/generator/writers/writeFunction.ts @@ -23,7 +23,7 @@ import { resolveFuncTupleType } from "./resolveFuncTupleType"; import { ops } from "./ops"; import { freshIdentifier } from "./freshIdentifier"; import { idTextErr, throwInternalCompilerError } from "../../errors"; -import { prettyPrintAsmShuffle } from "../../prettyPrinter"; +import { ppAsmShuffle } from "../../prettyPrinter"; export function writeCastedExpression( expression: AstExpression, @@ -580,7 +580,7 @@ export function writeFunction(f: FunctionDescription, ctx: WriterContext) { args: fAst.shuffle.args.map((id) => idOfText(funcIdOf(id))), }; ctx.asm( - prettyPrintAsmShuffle(asmShuffleEscaped), + ppAsmShuffle(asmShuffleEscaped), fAst.instructions.join(" "), ); }); diff --git a/src/grammar/ast.ts b/src/grammar/ast.ts index 200d190e8..536d31346 100644 --- a/src/grammar/ast.ts +++ b/src/grammar/ast.ts @@ -378,19 +378,22 @@ export type AstBouncedMessageType = { // export type AstExpression = + | AstExpressionPrimary | AstOpBinary | AstOpUnary - | AstFieldAccess - | AstNumber - | AstId - | AstBoolean + | AstConditional; + +export type AstExpressionPrimary = | AstMethodCall + | AstFieldAccess | AstStaticCall | AstStructInstance + | AstNumber + | AstBoolean + | AstId | AstNull | AstInitOf - | AstString - | AstConditional; + | AstString; export type AstBinaryOperation = | "+" diff --git a/src/grammar/grammar.ohm b/src/grammar/grammar.ohm index 3c3b4ad7f..2f17501e4 100644 --- a/src/grammar/grammar.ohm +++ b/src/grammar/grammar.ohm @@ -212,13 +212,22 @@ Tact { | "+" ExpressionUnary --plus | "!" ExpressionUnary --not | "~" ExpressionUnary --bitwiseNot - | ExpressionPrimary + | ExpressionPostfix // Order is important - ExpressionPrimary = ExpressionUnboxNotNull + ExpressionPostfix = ExpressionUnboxNotNull | ExpressionMethodCall | ExpressionFieldAccess - | ExpressionStaticCall + | ExpressionPrimary + + ExpressionUnboxNotNull = ExpressionPostfix "!!" + + ExpressionFieldAccess = ExpressionPostfix "." id ~"(" + + ExpressionMethodCall = ExpressionPostfix "." id Arguments + + // Order is important + ExpressionPrimary = ExpressionStaticCall | ExpressionParens | ExpressionStructInstance | integerLiteral @@ -230,12 +239,6 @@ Tact { ExpressionParens = "(" Expression ")" - ExpressionUnboxNotNull = ExpressionPrimary "!!" - - ExpressionFieldAccess = ExpressionPrimary "." id ~"(" - - ExpressionMethodCall = ExpressionPrimary "." id Arguments - ExpressionStructInstance = typeId "{" ListOf ","? "}" ExpressionStaticCall = id Arguments diff --git a/src/prettyPrinter.ts b/src/prettyPrinter.ts index 2e1ccf630..a825ec5f0 100644 --- a/src/prettyPrinter.ts +++ b/src/prettyPrinter.ts @@ -1,804 +1,897 @@ -import { - AstConstantDef, - AstImport, - AstNativeFunctionDecl, - AstReceiver, - AstStatementRepeat, - AstStatementUntil, - AstStatementWhile, - AstStatementForEach, - AstStatementTry, - AstStatementTryCatch, - AstCondition, - AstStatementAugmentedAssign, - AstStatementAssign, - AstStatementExpression, - AstStatementReturn, - AstStatementLet, - AstFunctionDef, - AstType, - AstStatement, - AstExpression, - AstContract, - AstTrait, - AstId, - AstModule, - AstModuleItem, - AstStructDecl, - AstMessageDecl, - AstTraitDeclaration, - AstFunctionDecl, - AstConstantDecl, - AstContractDeclaration, - AstContractInit, - AstStructFieldInitializer, - AstPrimitiveTypeDecl, - AstTypeId, - AstMapType, - AstBouncedMessageType, - AstFieldDecl, - AstOptionalType, - AstNode, - AstFuncId, - idText, - AstAsmFunctionDef, - AstFunctionAttribute, - AstTypedParameter, - AstAsmInstruction, - AstAsmShuffle, - astNumToString, - AstStatementDestruct, -} from "./grammar/ast"; -import { throwInternalCompilerError } from "./errors"; -import JSONbig from "json-bigint"; +import * as A from "./grammar/ast"; +import { groupBy, intercalate, isUndefined } from "./utils/array"; +import { makeVisitor } from "./utils/tricks"; + +// +// Types +// + +export const ppAstTypeId = A.idText; + +export const ppAstTypeIdWithStorage = ( + type: A.AstTypeId, + storageType: A.AstId | null, +): string => { + const alias = storageType ? ` as ${ppAstId(storageType)}` : ""; + return `${ppAstTypeId(type)}${alias}`; +}; + +export const ppAstMapType = ({ + keyType, + keyStorageType, + valueType, + valueStorageType, +}: A.AstMapType): string => { + const key = ppAstTypeIdWithStorage(keyType, keyStorageType); + const value = ppAstTypeIdWithStorage(valueType, valueStorageType); + return `map<${key}, ${value}>`; +}; + +export const ppAstBouncedMessageType = ({ + messageType, +}: A.AstBouncedMessageType): string => `bounced<${ppAstTypeId(messageType)}>`; + +export const ppAstOptionalType = ({ typeArg }: A.AstOptionalType): string => + `${ppAstType(typeArg)}?`; + +export const ppAstType = makeVisitor()({ + type_id: ppAstTypeId, + map_type: ppAstMapType, + bounced_message_type: ppAstBouncedMessageType, + optional_type: ppAstOptionalType, +}); + +// +// Expressions +// + +export const unaryOperatorType: Record = { + "+": "pre", + "-": "pre", + "!": "pre", + "~": "pre", + + "!!": "post", +}; + +export const checkPostfix = (operator: A.AstUnaryOperation) => + unaryOperatorType[operator] === "post"; /** - * Provides methods to format and indent Tact code. + * Description of precedence of certain type of AST node */ -export class PrettyPrinter { +export type Precedence = { /** - * @param indentLevel Initial level of indentation. - * @param indentSpaces Number of spaces per indentation level. + * Add parentheses around `code` if in this `parent` position we need brackets + * @param check Position-checking function from parent + * @param code Code to put parentheses around + * @returns */ - constructor( - private indentLevel: number = 0, - private readonly indentSpaces: number = 4, - ) {} + brace: ( + position: (childPrecedence: number) => boolean, + code: string, + ) => string; + /** + * Used in positions where grammar rule mentions itself + * + * Passed down when a position allows same unparenthesized operator + * For example, on left side of addition we can use another addition without + * parentheses: `1 + 2 + 3` means `(1 + 2) + 3`. Thus for left-associative + * operators we pass `self` to their left argument printer. + */ + self: (childPrecedence: number) => boolean; + /** + * Used in positions where grammar rule mentions other rule + * + * Passed down when a position disallows same unparenthesized operator + * For example, on the right side of subtraction we can't use another subtraction + * without parentheses: `1 - (2 - 3)` is not the same as `(1 - 2) - 3`. Thus for + * left-associative operators we pass `child` to their right argument printer. + */ + child: (childPrecedence: number) => boolean; +}; - private increaseIndent() { - this.indentLevel += 1; - } +/** + * Given numeric value of precedence, where higher values stand for higher binding power, + * create a helper object for precedence checking + */ +export const makePrecedence = (myPrecedence: number): Precedence => ({ + brace: (position, code) => (position(myPrecedence) ? `(${code})` : code), + self: (childPrecedence) => childPrecedence < myPrecedence, + child: (childPrecedence) => childPrecedence <= myPrecedence, +}); - private decreaseIndent() { - this.indentLevel -= 1; - } +// Least binding operator +export const lowestPrecedence = makePrecedence(0); - private indent(): string { - return " ".repeat(this.indentLevel * this.indentSpaces); - } +export const conditionalPrecedence = makePrecedence(20); - ppAstPrimitiveTypeDecl(primitive: AstPrimitiveTypeDecl): string { - return `${this.indent()}primitive ${this.ppAstId(primitive.name)};`; - } +export const binaryPrecedence: Readonly< + Record +> = { + "||": makePrecedence(30), - // - // Types - // - - ppAstType(typeRef: AstType): string { - switch (typeRef.kind) { - case "type_id": - return this.ppAstTypeId(typeRef); - case "map_type": - return this.ppAstMapType(typeRef); - case "bounced_message_type": - return this.ppAstBouncedMessageType(typeRef); - case "optional_type": - return this.ppAstOptionalType(typeRef); - } - } + "&&": makePrecedence(40), - ppAstTypeId(typeRef: AstTypeId): string { - return idText(typeRef); - } + "|": makePrecedence(50), - ppAstOptionalType(typeRef: AstOptionalType): string { - return `${this.ppAstType(typeRef.typeArg)}?`; - } + "^": makePrecedence(60), - ppAstMapType(typeRef: AstMapType): string { - const keyAlias = typeRef.keyStorageType - ? ` as ${this.ppAstId(typeRef.keyStorageType)}` - : ""; - const valueAlias = typeRef.valueStorageType - ? ` as ${this.ppAstId(typeRef.valueStorageType)}` - : ""; - return `map<${this.ppAstTypeId(typeRef.keyType)}${keyAlias}, ${this.ppAstTypeId(typeRef.valueType)}${valueAlias}>`; - } + "&": makePrecedence(70), - ppAstBouncedMessageType(typeRef: AstBouncedMessageType): string { - return `bounced<${this.ppAstTypeId(typeRef.messageType)}>`; - } + "==": makePrecedence(80), + "!=": makePrecedence(80), - // - // Expressions - // + "<": makePrecedence(90), + ">": makePrecedence(90), + "<=": makePrecedence(90), + ">=": makePrecedence(90), - /** - * Returns precedence used in unary/binary operations. - * Lower number means higher precedence - */ - getPrecedence(kind: string, op?: string): number { - switch (kind) { - case "op_binary": - switch (op) { - case "||": - return 1; - case "&&": - return 2; - case "|": - return 3; - case "^": - return 4; - case "&": - return 5; - case "==": - case "!=": - return 6; - case "<": - case ">": - case "<=": - case ">=": - return 7; - case "+": - case "-": - return 8; - case "*": - case "/": - case "%": - return 9; - default: - return 11; - } - case "conditional": - case "static_call": - case "method_call": - return 0; - case "op_unary": - return 10; - default: - return 11; - } - } + "<<": makePrecedence(100), + ">>": makePrecedence(100), - ppAstExpression(expr: AstExpression, parentPrecedence: number = 0): string { - let result; - let currentPrecedence = this.getPrecedence(expr.kind); - - switch (expr.kind) { - case "op_binary": - currentPrecedence = this.getPrecedence(expr.kind, expr.op); - result = `${this.ppAstExpression(expr.left, currentPrecedence)} ${expr.op} ${this.ppAstExpression(expr.right, currentPrecedence)}`; - break; - case "op_unary": - currentPrecedence = this.getPrecedence(expr.kind, expr.op); - result = `${expr.op}${this.ppAstExpression(expr.operand, currentPrecedence)}`; - break; - case "field_access": - result = `${this.ppAstExpression(expr.aggregate, currentPrecedence)}.${this.ppAstId(expr.field)}`; - break; - case "method_call": - result = `${this.ppAstExpression(expr.self, currentPrecedence)}.${this.ppAstId(expr.method)}(${expr.args.map((arg) => this.ppAstExpression(arg, currentPrecedence)).join(", ")})`; - break; - case "static_call": - result = `${this.ppAstId(expr.function)}(${expr.args.map((arg) => this.ppAstExpression(arg, currentPrecedence)).join(", ")})`; - break; - case "struct_instance": - result = `${this.ppAstId(expr.type)}{${expr.args.map((x) => this.ppAstStructFieldInit(x)).join(", ")}}`; - break; - case "init_of": - result = `initOf ${this.ppAstId(expr.contract)}(${expr.args.map((arg) => this.ppAstExpression(arg, currentPrecedence)).join(", ")})`; - break; - case "conditional": - result = `${this.ppAstExpression(expr.condition, currentPrecedence)} ? ${this.ppAstExpression(expr.thenBranch, currentPrecedence)} : ${this.ppAstExpression(expr.elseBranch, currentPrecedence)}`; - break; - case "number": - result = astNumToString(expr); - break; - case "id": - result = expr.text; - break; - case "boolean": - result = expr.value.toString(); - break; - case "string": - result = `"${expr.value}"`; - break; - case "null": - result = "null"; - break; - } + "+": makePrecedence(110), + "-": makePrecedence(110), - // Set parens when needed - if ( - parentPrecedence > 0 && - currentPrecedence > 0 && - currentPrecedence < parentPrecedence - ) { - result = `(${result})`; - } - - return result; - } + "*": makePrecedence(120), + "/": makePrecedence(120), + "%": makePrecedence(120), +}; - ppAstStructFieldInit(param: AstStructFieldInitializer): string { - return `${this.ppAstId(param.field)}: ${this.ppAstExpression(param.initializer)}`; - } +export const prefixPrecedence = makePrecedence(140); - // - // Program - // - - ppAstModule(program: AstModule): string { - const importsFormatted = - program.imports.length > 0 - ? `${program.imports - .map((entry) => this.ppAstImport(entry)) - .join("\n")}\n\n` - : ""; - const entriesFormatted = program.items - .map((entry, index, array) => { - const formattedEntry = this.ppModuleItem(entry); - const nextEntry = array[index + 1]; - if ( - entry.kind === "constant_def" && - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - nextEntry?.kind === "constant_def" - ) { - return formattedEntry; - } - return formattedEntry + "\n"; - }) - .join("\n"); - return `${importsFormatted}${entriesFormatted.trim()}`; - } - - ppModuleItem(item: AstModuleItem): string { - switch (item.kind) { - case "struct_decl": - return this.ppAstStruct(item); - case "contract": - return this.ppAstContract(item); - case "primitive_type_decl": - return this.ppAstPrimitiveTypeDecl(item); - case "function_def": - return this.ppAstFunctionDef(item); - case "asm_function_def": - return this.ppAstAsmFunctionDef(item); - case "native_function_decl": - return this.ppAstNativeFunction(item); - case "trait": - return this.ppAstTrait(item); - // case "program_import": - // return this.ppASTProgramImport(item); - case "constant_def": - return this.ppAstConstant(item); - case "message_decl": - return this.ppAstMessage(item); - } - } - - ppAstImport(importItem: AstImport): string { - return `${this.indent()}import "${importItem.path.value}";`; - } - - ppAstStruct(struct: AstStructDecl): string { - this.increaseIndent(); - const fieldsFormatted = struct.fields - .map((field) => this.ppAstFieldDecl(field)) - .join("\n"); - this.decreaseIndent(); - return `${this.indent()}struct ${this.ppAstId(struct.name)} {\n${fieldsFormatted}\n}`; - } +// Used by postfix unary !!, method calls and field accesses +export const postfixPrecedence = makePrecedence(150); - ppAstMessage(struct: AstMessageDecl): string { - const prefixFormatted = - struct.opcode !== null ? `(${astNumToString(struct.opcode)})` : ""; - this.increaseIndent(); - const fieldsFormatted = struct.fields - .map((field) => this.ppAstFieldDecl(field)) - .join("\n"); - this.decreaseIndent(); - return `${this.indent()}message${prefixFormatted} ${this.ppAstId(struct.name)} {\n${fieldsFormatted}\n}`; - } - - ppAstTrait(trait: AstTrait): string { - const traitsFormatted = trait.traits - .map((t) => this.ppAstId(t)) - .join(", "); - const attrsRaw = trait.attributes - .map((attr) => `@${attr.type}("${attr.name.value}")`) - .join(" "); - const attrsFormatted = attrsRaw ? `${attrsRaw} ` : ""; - this.increaseIndent(); - const bodyFormatted = trait.declarations - .map((dec, index, array) => { - const formattedDec = this.ppTraitBody(dec); - const nextDec = array[index + 1]; - /* eslint-disable @typescript-eslint/no-unnecessary-condition */ - if ( - ((dec.kind === "constant_def" || - dec.kind === "constant_decl") && - (nextDec?.kind === "constant_def" || - nextDec?.kind === "constant_decl")) || - (dec.kind === "field_decl" && - nextDec?.kind === "field_decl") - ) { - return formattedDec; - } - /* eslint-enable @typescript-eslint/no-unnecessary-condition */ - return formattedDec + "\n"; - }) - .join("\n"); - const header = traitsFormatted - ? `trait ${this.ppAstId(trait.name)} with ${traitsFormatted}` - : `trait ${this.ppAstId(trait.name)}`; - this.decreaseIndent(); - return `${this.indent()}${attrsFormatted}${header} {\n${bodyFormatted}${this.indent()}}`; - } - - ppTraitBody(item: AstTraitDeclaration): string { - switch (item.kind) { - case "field_decl": - return this.ppAstFieldDecl(item); - case "function_def": - return this.ppAstFunctionDef(item); - case "asm_function_def": - return this.ppAstAsmFunctionDef(item); - case "receiver": - return this.ppAstReceiver(item); - case "constant_def": - return this.ppAstConstant(item); - case "function_decl": - return this.ppAstFunctionDecl(item); - case "constant_decl": - return this.ppAstConstDecl(item); - } - } - - ppAstFieldDecl(field: AstFieldDecl): string { - const typeFormatted = this.ppAstType(field.type); - const initializer = field.initializer - ? ` = ${this.ppAstExpression(field.initializer)}` - : ""; - const asAlias = field.as ? ` as ${this.ppAstId(field.as)}` : ""; - return `${this.indent()}${this.ppAstId(field.name)}: ${typeFormatted}${asAlias}${initializer};`; - } +/** + * Expression printer takes an expression and a function from parent AST node printer that checks + * whether expressions with given precedence should be parenthesized in parent context + */ +export type ExprPrinter = ( + expr: T, +) => (check: (childPrecedence: number) => boolean) => string; - ppAstConstant(constant: AstConstantDef): string { - const valueFormatted = ` = ${this.ppAstExpression(constant.initializer)}`; - const attrsRaw = constant.attributes.map((attr) => attr.type).join(" "); - const attrsFormatted = attrsRaw ? `${attrsRaw} ` : ""; - return `${this.indent()}${attrsFormatted}const ${this.ppAstId(constant.name)}: ${this.ppAstType(constant.type)}${valueFormatted};`; - } +/** + * Wrapper for AST nodes that should never be parenthesized, and thus do not require information + * about the position they're printed in + * + * Takes a regular printer function and returns corresponding ExprPrinter that ignores all + * position and precedence information + */ +export const ppLeaf = + (printer: (t: T) => string): ExprPrinter => + (node) => + () => + printer(node); + +export const ppExprArgs = (args: A.AstExpression[]) => + args.map((arg) => ppAstExpression(arg)).join(", "); + +export const ppAstStructFieldInit = ( + param: A.AstStructFieldInitializer, +): string => `${ppAstId(param.field)}: ${ppAstExpression(param.initializer)}`; + +export const ppAstStructInstance = ({ type, args }: A.AstStructInstance) => + `${ppAstId(type)}{${args.map((x) => ppAstStructFieldInit(x)).join(", ")}}`; + +export const ppAstInitOf = ({ contract, args }: A.AstInitOf) => + `initOf ${ppAstId(contract)}(${ppExprArgs(args)})`; + +export const ppAstNumber = A.astNumToString; +export const ppAstBoolean = ({ value }: A.AstBoolean) => value.toString(); +export const ppAstId = ({ text }: A.AstId) => text; +export const ppAstNull = (_expr: A.AstNull) => "null"; +export const ppAstString = ({ value }: A.AstString) => `"${value}"`; + +export const ppAstStaticCall = ({ function: func, args }: A.AstStaticCall) => { + return `${ppAstId(func)}(${ppExprArgs(args)})`; +}; + +export const ppAstMethodCall: ExprPrinter = + ({ self: object, method, args }) => + (position) => { + const { brace, self } = postfixPrecedence; + return brace( + position, + `${ppAstExpressionNested(object)(self)}.${ppAstId(method)}(${ppExprArgs(args)})`, + ); + }; + +export const ppAstFieldAccess: ExprPrinter = + ({ aggregate, field }) => + (position) => { + const { brace, self } = postfixPrecedence; + return brace( + position, + `${ppAstExpressionNested(aggregate)(self)}.${ppAstId(field)}`, + ); + }; + +export const ppAstOpUnary: ExprPrinter = + ({ op, operand }) => + (position) => { + const isPostfix = checkPostfix(op); + const { brace, self } = isPostfix + ? postfixPrecedence + : prefixPrecedence; + const code = ppAstExpressionNested(operand)(self); + return brace(position, isPostfix ? `${code}${op}` : `${op}${code}`); + }; + +export const ppAstOpBinary: ExprPrinter = + ({ left, op, right }) => + (position) => { + const { brace, self, child } = binaryPrecedence[op]; + const leftCode = ppAstExpressionNested(left)(self); + const rightCode = ppAstExpressionNested(right)(child); + return brace(position, `${leftCode} ${op} ${rightCode}`); + }; + +export const ppAstConditional: ExprPrinter = + ({ condition, thenBranch, elseBranch }) => + (position) => { + const { brace, self, child } = conditionalPrecedence; + const conditionCode = ppAstExpressionNested(condition)(child); + const thenCode = ppAstExpressionNested(thenBranch)(child); + const elseCode = ppAstExpressionNested(elseBranch)(self); + return brace(position, `${conditionCode} ? ${thenCode} : ${elseCode}`); + }; + +export const ppAstExpressionNested = makeVisitor()({ + struct_instance: ppLeaf(ppAstStructInstance), + number: ppLeaf(ppAstNumber), + boolean: ppLeaf(ppAstBoolean), + id: ppLeaf(ppAstId), + null: ppLeaf(ppAstNull), + init_of: ppLeaf(ppAstInitOf), + string: ppLeaf(ppAstString), + static_call: ppLeaf(ppAstStaticCall), + + method_call: ppAstMethodCall, + field_access: ppAstFieldAccess, + + op_unary: ppAstOpUnary, + + op_binary: ppAstOpBinary, + + conditional: ppAstConditional, +}); + +export const ppAstExpression = (expr: A.AstExpression): string => { + return ppAstExpressionNested(expr)(lowestPrecedence.child); +}; - ppAstConstDecl(constant: AstConstantDecl): string { - const attrsRaw = constant.attributes.map((attr) => attr.type).join(" "); - const attrsFormatted = attrsRaw ? `${attrsRaw} ` : ""; - return `${this.indent()}${attrsFormatted}const ${this.ppAstId(constant.name)}: ${this.ppAstType(constant.type)};`; - } +/** + * An intermediate language that is only concerned of spacing and indentation + */ +type Context = { + /** + * Line of code with \n implied + */ + row: (s: string) => U; - ppAstContract(contract: AstContract): string { - const traitsFormatted = contract.traits - .map((trait) => trait.text) - .join(", "); - this.increaseIndent(); - const bodyFormatted = contract.declarations - .map((dec, index, array) => { - const formattedDec = this.ppContractBody(dec); - const nextDec = array[index + 1]; - if ( - (dec.kind === "constant_def" && - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - nextDec?.kind === "constant_def") || - (dec.kind === "field_decl" && - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - nextDec?.kind === "field_decl") - ) { - return formattedDec; - } - return formattedDec + "\n"; - }) - .join("\n"); - this.decreaseIndent(); - const header = traitsFormatted - ? `contract ${this.ppAstId(contract.name)} with ${traitsFormatted}` - : `contract ${this.ppAstId(contract.name)}`; - const attrsRaw = contract.attributes - .map((attr) => `@interface("${attr.name.value}")`) - .join(" "); - const attrsFormatted = attrsRaw ? `${attrsRaw} ` : ""; - return `${this.indent()}${attrsFormatted}${header} {\n${bodyFormatted}${this.indent()}}`; - } + /** + * Stacks lines after each other + */ + block: (rows: readonly U[]) => U; - ppContractBody(declaration: AstContractDeclaration): string { - switch (declaration.kind) { - case "field_decl": - return this.ppAstFieldDecl(declaration); - case "function_def": - return this.ppAstFunctionDef(declaration); - case "asm_function_def": - return this.ppAstAsmFunctionDef(declaration); - case "contract_init": - return this.ppAstInitFunction(declaration); - case "receiver": - return this.ppAstReceiver(declaration); - case "constant_def": - return this.ppAstConstant(declaration); - } - } + /** + * Similar to `block`, but adjacent lines of groups get concatenated + * [a, b] + [c, d] = [a, bc, d] + */ + concat: (rows: readonly U[]) => U; - public ppAstFunctionDef(f: AstFunctionDef): string { - const body = this.ppStatementBlock(f.statements); - return `${this.indent()}${this.ppAstFunctionSignature(f.attributes, f.name, f.return, f.params)} ${body}`; - } + /** + * Same as `indent`, but indents `rows` 1 level deeper and adds `{` and `}` + */ + braced: (rows: readonly U[]) => U; - public ppAstAsmFunctionDef(f: AstAsmFunctionDef): string { - const asmAttr = `asm${prettyPrintAsmShuffle(f.shuffle)}`; - const body = this.ppAsmInstructionsBlock(f.instructions); - return `${this.indent()}${asmAttr} ${this.ppAstFunctionSignature(f.attributes, f.name, f.return, f.params)} ${body}`; - } + /** + * Print a list of `items` with `print` + */ + list: (items: readonly T[], print: Printer) => readonly U[]; - ppAstFunctionDecl(f: AstFunctionDecl): string { - return `${this.indent()}${this.ppAstFunctionSignature(f.attributes, f.name, f.return, f.params)};`; - } + /** + * Display `items` with `print` in groups distinguished by return value of `getTag` + */ + grouped: (options: { + items: readonly T[]; + /** + * Items with the same tag are displayed without extra empty line between them + * + * Use NaN for tag whenever items should always be displayed with empty line, + * because NaN !== NaN + */ + getTag: (t: T) => V; + print: Printer; + }) => readonly U[]; +}; - ppAstFunctionSignature( - attributes: AstFunctionAttribute[], - name: AstId, - retTy: AstType | null, - params: AstTypedParameter[], - ): string { - const argsFormatted = params - .map( - (arg) => - `${this.ppAstId(arg.name)}: ${this.ppAstType(arg.type)}`, - ) - .join(", "); - const attrsRaw = attributes - .map((attr) => this.ppAstFunctionAttribute(attr)) - .join(" "); - const attrsFormatted = attrsRaw ? `${attrsRaw} ` : ""; - const returnType = retTy ? `: ${this.ppAstType(retTy)}` : ""; - return `${attrsFormatted}fun ${this.ppAstId(name)}(${argsFormatted})${returnType}`; - } +/** + * Generates line of code with indentation, given desired indentation level of outer scope + */ +type LevelFn = (level: number) => string; - ppAstFunctionAttribute(attr: AstFunctionAttribute): string { - if (attr.type === "get" && attr.methodId !== null) { - return `get(${this.ppAstExpression(attr.methodId)})`; - } else { - return attr.type; - } - } +/** + * Result of printing an expression is an array of rows, parameterized over indentation + * of outer scope + */ +type ContextModel = readonly LevelFn[]; - ppAstReceiver(receive: AstReceiver): string { - const header = this.ppAstReceiverHeader(receive); - const stmtsFormatted = this.ppStatementBlock(receive.statements); - return `${this.indent()}${header} ${stmtsFormatted}`; - } +/** + * Concatenates an array of printing results, so that last line of each expression is merged + * with first line of next expression + * + * Typically used to generate multiline indented code as part of single-line expression + * + * Roughly, `concat(["while (true)"], [" "], ["{", "...", "}"]) = ["while (true) {", "...", "}"]` + */ +const concat = ([head, ...tail]: readonly ContextModel[]): ContextModel => { + // If we're concatenating an empty array, the result is always empty + if (isUndefined(head)) { + return []; + } + // Create a copy of first printing result, where we'll accumulate other results + const init = [...head]; + // Recursively concatenate all printing results except for first + const next = concat(tail); + // Take last line of first printing result + const last = init.pop(); + // If first printing result has no lines, return concatenation result of all others + if (isUndefined(last)) { + return next; + } + // Get first line on concatenated printing results starting with second + const [nextHead, ...nextTail] = next; + // If they all concatenated into an array of 0 lines, just return first printing result + if (isUndefined(nextHead)) { + return head; + } + // Otherwise concatenate results, leaving indent only in front of the merged line + return [...init, (level) => last(level) + nextHead(0), ...nextTail]; +}; + +const createContext = (spaces: number): Context => { + const row = (s: string) => [ + // Empty lines are not indented + (level: number) => (s === "" ? s : " ".repeat(level * spaces) + s), + ]; + const block = (rows: readonly ContextModel[]) => rows.flat(); + const indent = (rows: readonly ContextModel[]) => + block(rows).map((f) => (level: number) => f(level + 1)); + const braced = (rows: readonly ContextModel[]) => + block([row(`{`), indent(rows), row(`}`)]); + const list = (items: readonly T[], print: Printer) => + items.map((node) => print(node)(ctx)); + const grouped = ({ + items, + getTag, + print, + }: { + items: readonly T[]; + getTag: (t: T) => V; + print: Printer; + }) => { + return intercalate( + groupBy(items, getTag).map((group) => list(group, print)), + row(""), + ); + }; + const ctx: Context = { + row, + concat, + block, + braced, + list, + grouped, + }; + return ctx; +}; - ppAstReceiverHeader(receive: AstReceiver): string { - switch (receive.selector.kind) { - case "internal-simple": - return `receive(${this.ppAstId(receive.selector.param.name)}: ${this.ppAstType(receive.selector.param.type)})`; - case "internal-fallback": - return `receive()`; - case "internal-comment": - return `receive("${receive.selector.comment.value}")`; - case "bounce": - return `bounced(${this.ppAstId(receive.selector.param.name)}: ${this.ppAstType(receive.selector.param.type)})`; - case "external-simple": - return `external(${this.ppAstId(receive.selector.param.name)}: ${this.ppAstType(receive.selector.param.type)})`; - case "external-fallback": - return `external()`; - case "external-comment": - return `external("${receive.selector.comment.value}")`; +/** + * Prints AST node of type `T` into an intermediate language of row of type `U` + * + * We enforce `U` to be a generic argument so that no implementation can (ab)use + * the fact it's a string and generate some indentation without resorting to + * methods of `Context`. + */ +type Printer = (item: T) => (ctx: Context) => U; + +export const ppAstModule: Printer = + ({ imports, items }) => + (c) => { + const itemsCode = c.grouped({ + items, + getTag: ({ kind }) => (kind === "constant_def" ? 1 : NaN), + print: ppModuleItem, + }); + if (imports.length === 0) { + return c.block(itemsCode); } - } - - ppAstNativeFunction(func: AstNativeFunctionDecl): string { - const argsFormatted = func.params - .map( - (arg) => - `${this.ppAstId(arg.name)}: ${this.ppAstType(arg.type)}`, - ) + return c.block([ + ...c.list(imports, ppAstImport), + c.row(""), + ...itemsCode, + ]); + }; + +export const ppAstStruct: Printer = + ({ name, fields }) => + (c) => + c.concat([ + c.row(`struct ${ppAstId(name)} `), + c.braced(c.list(fields, ppAstFieldDecl)), + ]); + +export const ppAstContract: Printer = + ({ name, traits, declarations, attributes }) => + (c) => { + const attrsCode = attributes + .map(({ name: { value } }) => `@interface("${value}") `) + .join(""); + const traitsCode = traits.map((trait) => trait.text).join(", "); + const header = traitsCode + ? `contract ${ppAstId(name)} with ${traitsCode}` + : `contract ${ppAstId(name)}`; + return c.concat([ + c.row(`${attrsCode}${header} `), + c.braced( + c.grouped({ + items: declarations, + getTag: ({ kind }) => + kind === "constant_def" + ? 1 + : kind === "field_decl" + ? 2 + : NaN, + print: ppContractBody, + }), + ), + ]); + }; + +export const ppAstPrimitiveTypeDecl: Printer = + ({ name }) => + (c) => + c.row(`primitive ${ppAstId(name)};`); + +export const ppAstFunctionDef: Printer = (node) => (c) => + c.concat([ + c.row(ppAstFunctionSignature(node)), + c.row(" "), + ppStatementBlock(node.statements)(c), + ]); + +export const ppAsmShuffle = ({ args, ret }: A.AstAsmShuffle): string => { + if (args.length === 0 && ret.length === 0) { + return ""; + } + const argsCode = args.map(({ text }) => text).join(" "); + if (ret.length === 0) { + return `(${argsCode})`; + } + const retCode = ret.map(({ value }) => value.toString()).join(" "); + return `(${argsCode} -> ${retCode})`; +}; + +export const ppAstAsmFunctionDef: Printer = + (node) => (c) => + c.concat([ + c.row( + `asm${ppAsmShuffle(node.shuffle)} ${ppAstFunctionSignature(node)} `, + ), + ppAsmInstructionsBlock(node.instructions)(c), + ]); + +export const ppAstNativeFunction: Printer = + ({ name, nativeName, params, return: retTy, attributes }) => + (c) => { + const attrs = attributes.map(({ type }) => type + " ").join(""); + const argsCode = params + .map(({ name, type }) => `${ppAstId(name)}: ${ppAstType(type)}`) .join(", "); - const returnType = func.return - ? `: ${this.ppAstType(func.return)}` + const returnType = retTy ? `: ${ppAstType(retTy)}` : ""; + return c.block([ + c.row(`@name(${ppAstFuncId(nativeName)})`), + c.row(`${attrs}native ${ppAstId(name)}(${argsCode})${returnType};`), + ]); + }; + +export const ppAstTrait: Printer = + ({ name, traits, attributes, declarations }) => + (c) => { + const attrsCode = attributes + .map((attr) => `@${attr.type}("${attr.name.value}") `) + .join(""); + const traitsCode = traits.map((t) => ppAstId(t)).join(", "); + const header = traitsCode + ? `trait ${ppAstId(name)} with ${traitsCode}` + : `trait ${ppAstId(name)}`; + return c.concat([ + c.row(`${attrsCode}${header} `), + c.braced( + c.grouped({ + items: declarations, + getTag: ({ kind }) => + kind === "constant_def" || kind === "constant_decl" + ? 1 + : kind === "field_decl" + ? 2 + : NaN, + print: ppTraitBody, + }), + ), + ]); + }; + +export const ppAstConstant: Printer = + ({ attributes, initializer, name, type }) => + (c) => { + const attrsCode = attributes.map(({ type }) => type + " ").join(""); + return c.row( + `${attrsCode}const ${ppAstId(name)}: ${ppAstType(type)} = ${ppAstExpression(initializer)};`, + ); + }; + +export const ppAstMessage: Printer = + ({ name, opcode, fields }) => + (c) => { + const prefixCode = + opcode !== null ? `(${A.astNumToString(opcode)})` : ""; + + return c.concat([ + c.row(`message${prefixCode} ${ppAstId(name)} `), + c.braced(c.list(fields, ppAstFieldDecl)), + ]); + }; + +export const ppModuleItem: Printer = + makeVisitor()({ + struct_decl: ppAstStruct, + contract: ppAstContract, + primitive_type_decl: ppAstPrimitiveTypeDecl, + function_def: ppAstFunctionDef, + asm_function_def: ppAstAsmFunctionDef, + native_function_decl: ppAstNativeFunction, + trait: ppAstTrait, + constant_def: ppAstConstant, + message_decl: ppAstMessage, + }); + +export const ppAstFieldDecl: Printer = + ({ type, initializer, as, name }) => + (c) => { + const asAlias = as ? ` as ${ppAstId(as)}` : ""; + const initializerCode = initializer + ? ` = ${ppAstExpression(initializer)}` : ""; - let attrs = func.attributes.map((attr) => attr.type).join(" "); - attrs = attrs ? attrs + " " : ""; - return `${this.indent()}@name(${this.ppAstFuncId(func.nativeName)})\n${this.indent()}${attrs}native ${this.ppAstId(func.name)}(${argsFormatted})${returnType};`; - } - - ppAstFuncId(func: AstFuncId): string { - return func.text; - } - - ppAstInitFunction(initFunc: AstContractInit): string { - const argsFormatted = initFunc.params - .map( - (arg) => - `${this.ppAstId(arg.name)}: ${this.ppAstType(arg.type)}`, - ) + return c.row( + `${ppAstId(name)}: ${ppAstType(type)}${asAlias}${initializerCode};`, + ); + }; + +export const ppAstReceiver: Printer = + ({ selector, statements }) => + (c) => + c.concat([ + c.row(`${ppAstReceiverHeader(selector)} `), + ppStatementBlock(statements)(c), + ]); + +export const ppAstFunctionDecl: Printer = (f) => (c) => + c.row(`${ppAstFunctionSignature(f)};`); + +export const ppAstConstDecl: Printer = + ({ attributes, name, type }) => + (c) => { + const attrsCode = attributes.map(({ type }) => type + " ").join(""); + return c.row(`${attrsCode}const ${ppAstId(name)}: ${ppAstType(type)};`); + }; + +export const ppTraitBody: Printer = + makeVisitor()({ + function_def: ppAstFunctionDef, + asm_function_def: ppAstAsmFunctionDef, + constant_def: ppAstConstant, + field_decl: ppAstFieldDecl, + receiver: ppAstReceiver, + function_decl: ppAstFunctionDecl, + constant_decl: ppAstConstDecl, + }); + +export const ppAstInitFunction: Printer = + ({ params, statements }) => + (c) => { + const argsCode = params + .map(({ name, type }) => `${ppAstId(name)}: ${ppAstType(type)}`) .join(", "); - - this.increaseIndent(); - const stmtsFormatted = initFunc.statements - .map((stmt) => this.ppAstStatement(stmt)) - .join("\n"); - this.decreaseIndent(); - - return `${this.indent()}init(${argsFormatted}) {${stmtsFormatted == "" ? "" : "\n"}${stmtsFormatted}${stmtsFormatted == "" ? "" : "\n" + this.indent()}}`; - } - - // - // Statements - // - - ppAstStatement(stmt: AstStatement): string { - switch (stmt.kind) { - case "statement_let": - return this.ppAstStatementLet(stmt as AstStatementLet); - case "statement_return": - return this.ppAstStatementReturn(stmt as AstStatementReturn); - case "statement_expression": - return this.ppAstStatementExpression( - stmt as AstStatementExpression, - ); - case "statement_assign": - return this.ppAstStatementAssign(stmt as AstStatementAssign); - case "statement_augmentedassign": - return this.ppAstStatementAugmentedAssign( - stmt as AstStatementAugmentedAssign, - ); - case "statement_condition": - return this.ppAstCondition(stmt as AstCondition); - case "statement_while": - return this.ppAstStatementWhile(stmt as AstStatementWhile); - case "statement_until": - return this.ppAstStatementUntil(stmt as AstStatementUntil); - case "statement_repeat": - return this.ppAstStatementRepeat(stmt as AstStatementRepeat); - case "statement_foreach": - return this.ppAstStatementForEach(stmt as AstStatementForEach); - case "statement_try": - return this.ppAstStatementTry(stmt as AstStatementTry); - case "statement_try_catch": - return this.ppAstStatementTryCatch( - stmt as AstStatementTryCatch, - ); - case "statement_destruct": - return this.ppAstStatementDestruct( - stmt as AstStatementDestruct, - ); + if (statements.length === 0) { + return c.row(`init(${argsCode}) {}`); } - } - - ppStatementBlock(stmts: AstStatement[]): string { - this.increaseIndent(); - const stmtsFormatted = stmts - .map((stmt) => this.ppAstStatement(stmt)) - .join("\n"); - this.decreaseIndent(); - const result = `{\n${stmtsFormatted}\n${this.indent()}}`; - return result; - } - - ppAsmInstructionsBlock(instructions: AstAsmInstruction[]): string { - this.increaseIndent(); - const instructionsFormatted = instructions - .map((instr) => this.ppAstAsmInstruction(instr)) - .join("\n"); - this.decreaseIndent(); - return `{\n${instructionsFormatted}\n${this.indent()}}`; - } - - ppAstAsmInstruction(instruction: AstAsmInstruction): string { - return `${this.indent()}${instruction}`; - } - - ppAstStatementLet(statement: AstStatementLet): string { - const expression = this.ppAstExpression(statement.expression); - const tyAnnotation = - statement.type === null - ? "" - : `: ${this.ppAstType(statement.type)}`; - return `${this.indent()}let ${this.ppAstId(statement.name)}${tyAnnotation} = ${expression};`; - } - - ppAstStatementReturn(statement: AstStatementReturn): string { - const expression = statement.expression - ? this.ppAstExpression(statement.expression) - : ""; - return `${this.indent()}return ${expression};`; - } - - ppAstStatementExpression(statement: AstStatementExpression): string { - return `${this.indent()}${this.ppAstExpression(statement.expression)};`; - } - - ppAstId(id: AstId) { - return id.text; - } - - ppAstStatementAssign(statement: AstStatementAssign): string { - return `${this.indent()}${this.ppAstExpression(statement.path)} = ${this.ppAstExpression(statement.expression)};`; - } - - ppAstStatementAugmentedAssign( - statement: AstStatementAugmentedAssign, - ): string { - return `${this.indent()}${this.ppAstExpression(statement.path)} ${statement.op}= ${this.ppAstExpression(statement.expression)};`; - } - - ppAstCondition(statement: AstCondition): string { - const condition = this.ppAstExpression(statement.condition); - const trueBranch = this.ppStatementBlock(statement.trueStatements); - const falseBranch = statement.falseStatements - ? ` else ${this.ppStatementBlock(statement.falseStatements)}` - : ""; - return `${this.indent()}if (${condition}) ${trueBranch}${falseBranch}`; - } - - ppAstStatementWhile(statement: AstStatementWhile): string { - const condition = this.ppAstExpression(statement.condition); - const stmts = this.ppStatementBlock(statement.statements); - return `${this.indent()}while (${condition}) ${stmts}`; - } - - ppAstStatementRepeat(statement: AstStatementRepeat): string { - const condition = this.ppAstExpression(statement.iterations); - const stmts = this.ppStatementBlock(statement.statements); - return `${this.indent()}repeat (${condition}) ${stmts}`; - } - - ppAstStatementUntil(statement: AstStatementUntil): string { - const condition = this.ppAstExpression(statement.condition); - const stmts = this.ppStatementBlock(statement.statements); - return `${this.indent()}do ${stmts} until (${condition});`; - } - - ppAstStatementForEach(statement: AstStatementForEach): string { - const header = `foreach (${this.ppAstId(statement.keyName)}, ${this.ppAstId(statement.valueName)} in ${this.ppAstExpression(statement.map)})`; - const body = this.ppStatementBlock(statement.statements); - return `${this.indent()}${header} ${body}`; - } - - ppAstStatementTry(statement: AstStatementTry): string { - const body = this.ppStatementBlock(statement.statements); - return `${this.indent()}try ${body}`; - } - - ppAstStatementTryCatch(statement: AstStatementTryCatch): string { - const tryBody = this.ppStatementBlock(statement.statements); - const catchBody = this.ppStatementBlock(statement.catchStatements); - return `${this.indent()}try ${tryBody} catch (${this.ppAstId(statement.catchName)}) ${catchBody}`; - } - - ppAstStatementDestruct(statement: AstStatementDestruct): string { - const ids = statement.identifiers - .values() - .reduce((acc: string[], [field, name]) => { - const id = - field.text === name.text - ? this.ppAstId(name) - : `${this.ppAstId(field)}: ${this.ppAstId(name)}`; - acc.push(id); - return acc; - }, []); - const restPattern = statement.ignoreUnspecifiedFields ? ", .." : ""; - return `${this.indent()}let ${this.ppAstTypeId(statement.type)} {${ids.join(", ")}${restPattern}} = ${this.ppAstExpression(statement.expression)};`; - } -} + return c.concat([ + c.row(`init(${argsCode}) `), + c.braced(c.list(statements, ppAstStatement)), + ]); + }; + +export const ppContractBody: Printer = + makeVisitor()({ + field_decl: ppAstFieldDecl, + function_def: ppAstFunctionDef, + asm_function_def: ppAstAsmFunctionDef, + contract_init: ppAstInitFunction, + receiver: ppAstReceiver, + constant_def: ppAstConstant, + }); + +export const ppAstImport: Printer = + ({ path }) => + (c) => + c.row(`import "${path.value}";`); + +export const ppAstFunctionSignature = ({ + name, + attributes, + return: retTy, + params, +}: A.AstFunctionDef | A.AstAsmFunctionDef | A.AstFunctionDecl): string => { + const argsCode = params + .map(({ name, type }) => `${ppAstId(name)}: ${ppAstType(type)}`) + .join(", "); + const attrsCode = attributes + .map((attr) => ppAstFunctionAttribute(attr) + " ") + .join(""); + const returnType = retTy ? `: ${ppAstType(retTy)}` : ""; + return `${attrsCode}fun ${ppAstId(name)}(${argsCode})${returnType}`; +}; + +export const ppAstFunctionAttribute = ( + attr: A.AstFunctionAttribute, +): string => { + if (attr.type === "get" && attr.methodId !== null) { + return `get(${ppAstExpression(attr.methodId)})`; + } else { + return attr.type; + } +}; + +export const ppAstReceiverHeader = makeVisitor()({ + bounce: ({ param: { name, type } }) => + `bounced(${ppAstId(name)}: ${ppAstType(type)})`, + "internal-simple": ({ param: { name, type } }) => + `receive(${ppAstId(name)}: ${ppAstType(type)})`, + "external-simple": ({ param: { name, type } }) => + `external(${ppAstId(name)}: ${ppAstType(type)})`, + "internal-fallback": () => `receive()`, + "external-fallback": () => `external()`, + "internal-comment": ({ comment: { value } }) => `receive("${value}")`, + "external-comment": ({ comment: { value } }) => `external("${value}")`, +}); + +export const ppAstFuncId = (func: A.AstFuncId): string => func.text; + +// +// Statements +// + +export const ppStatementBlock: Printer = (stmts) => (c) => + c.braced(stmts.length === 0 ? [c.row("")] : c.list(stmts, ppAstStatement)); + +export const ppAsmInstructionsBlock: Printer = + (instructions) => (c) => + c.braced(instructions.map(c.row)); + +export const ppAstStatementLet: Printer = + ({ type, name, expression }) => + (c) => { + const tyAnnotation = type === null ? "" : `: ${ppAstType(type)}`; + return c.row( + `let ${ppAstId(name)}${tyAnnotation} = ${ppAstExpression(expression)};`, + ); + }; + +export const ppAstStatementReturn: Printer = + ({ expression }) => + (c) => + c.row(`return ${expression ? ppAstExpression(expression) : ""};`); + +export const ppAstStatementExpression: Printer = + ({ expression }) => + (c) => + c.row(`${ppAstExpression(expression)};`); + +export const ppAstStatementAssign: Printer = + ({ path, expression }) => + (c) => + c.row(`${ppAstExpression(path)} = ${ppAstExpression(expression)};`); + +export const ppAstStatementAugmentedAssign: Printer< + A.AstStatementAugmentedAssign +> = + ({ path, op, expression }) => + (c) => + c.row( + `${ppAstExpression(path)} ${op}= ${ppAstExpression(expression)};`, + ); + +export const ppAstCondition: Printer = + ({ condition, trueStatements, falseStatements }) => + (c) => { + if (falseStatements) { + return c.concat([ + c.row(`if (${ppAstExpression(condition)}) `), + ppStatementBlock(trueStatements)(c), + c.row(" else "), + ppStatementBlock(falseStatements)(c), + ]); + } else { + return c.concat([ + c.row(`if (${ppAstExpression(condition)}) `), + ppStatementBlock(trueStatements)(c), + ]); + } + }; + +export const ppAstStatementWhile: Printer = + ({ condition, statements }) => + (c) => + c.concat([ + c.row(`while (${ppAstExpression(condition)}) `), + ppStatementBlock(statements)(c), + ]); + +export const ppAstStatementRepeat: Printer = + ({ iterations, statements }) => + (c) => + c.concat([ + c.row(`repeat (${ppAstExpression(iterations)}) `), + ppStatementBlock(statements)(c), + ]); + +export const ppAstStatementUntil: Printer = + ({ condition, statements }) => + (c) => + c.concat([ + c.row(`do `), + ppStatementBlock(statements)(c), + c.row(` until (${ppAstExpression(condition)});`), + ]); + +export const ppAstStatementForEach: Printer = + ({ keyName, valueName, map, statements }) => + (c) => + c.concat([ + c.row( + `foreach (${ppAstId(keyName)}, ${ppAstId(valueName)} in ${ppAstExpression(map)}) `, + ), + ppStatementBlock(statements)(c), + ]); + +export const ppAstStatementTry: Printer = + ({ statements }) => + (c) => + c.concat([c.row(`try `), ppStatementBlock(statements)(c)]); + +export const ppAstStatementTryCatch: Printer = + ({ statements, catchName, catchStatements }) => + (c) => + c.concat([ + c.row(`try `), + ppStatementBlock(statements)(c), + c.row(` catch (${ppAstId(catchName)}) `), + ppStatementBlock(catchStatements)(c), + ]); + +export const ppAstStatementDestruct: Printer = + ({ type, identifiers, ignoreUnspecifiedFields, expression }) => + (c) => { + const ids: string[] = []; + for (const [field, name] of identifiers.values()) { + const id = + field.text === name.text + ? ppAstId(name) + : `${ppAstId(field)}: ${ppAstId(name)}`; + ids.push(id); + } + const restPattern = ignoreUnspecifiedFields ? ", .." : ""; + return c.row( + `let ${ppAstTypeId(type)} {${ids.join(", ")}${restPattern}} = ${ppAstExpression(expression)};`, + ); + }; + +export const ppAstStatement: Printer = + makeVisitor()({ + statement_let: ppAstStatementLet, + statement_return: ppAstStatementReturn, + statement_expression: ppAstStatementExpression, + statement_assign: ppAstStatementAssign, + statement_augmentedassign: ppAstStatementAugmentedAssign, + statement_condition: ppAstCondition, + statement_while: ppAstStatementWhile, + statement_until: ppAstStatementUntil, + statement_repeat: ppAstStatementRepeat, + statement_foreach: ppAstStatementForEach, + statement_try: ppAstStatementTry, + statement_try_catch: ppAstStatementTryCatch, + statement_destruct: ppAstStatementDestruct, + }); + +export const exprNode = + (exprPrinter: (expr: T) => string): Printer => + (node) => + (c) => + c.row(exprPrinter(node)); + +export const ppAstNode: Printer = makeVisitor()({ + op_binary: exprNode(ppAstExpression), + op_unary: exprNode(ppAstExpression), + field_access: exprNode(ppAstExpression), + method_call: exprNode(ppAstExpression), + static_call: exprNode(ppAstExpression), + struct_instance: exprNode(ppAstExpression), + init_of: exprNode(ppAstExpression), + conditional: exprNode(ppAstExpression), + number: exprNode(ppAstExpression), + id: exprNode(ppAstExpression), + boolean: exprNode(ppAstExpression), + string: exprNode(ppAstExpression), + null: exprNode(ppAstExpression), + type_id: exprNode(ppAstType), + optional_type: exprNode(ppAstType), + map_type: exprNode(ppAstType), + bounced_message_type: exprNode(ppAstType), + struct_field_initializer: exprNode(ppAstStructFieldInit), + destruct_mapping: () => { + throw new Error("Not implemented"); + }, + typed_parameter: () => { + throw new Error("Not implemented"); + }, + destruct_end: () => { + throw new Error("Not implemented"); + }, + + module: ppAstModule, + struct_decl: ppAstStruct, + constant_def: ppAstConstant, + constant_decl: ppAstConstDecl, + function_def: ppAstFunctionDef, + contract: ppAstContract, + trait: ppAstTrait, + primitive_type_decl: ppAstPrimitiveTypeDecl, + message_decl: ppAstMessage, + native_function_decl: ppAstNativeFunction, + field_decl: ppAstFieldDecl, + function_decl: ppAstFunctionDecl, + receiver: ppAstReceiver, + contract_init: ppAstInitFunction, + statement_let: ppAstStatementLet, + statement_return: ppAstStatementReturn, + statement_expression: ppAstStatementExpression, + statement_assign: ppAstStatementAssign, + statement_augmentedassign: ppAstStatementAugmentedAssign, + statement_condition: ppAstCondition, + statement_while: ppAstStatementWhile, + statement_until: ppAstStatementUntil, + statement_repeat: ppAstStatementRepeat, + statement_try: ppAstStatementTry, + statement_try_catch: ppAstStatementTryCatch, + statement_foreach: ppAstStatementForEach, + import: ppAstImport, + func_id: exprNode(ppAstFuncId), + statement_destruct: ppAstStatementDestruct, + function_attribute: exprNode(ppAstFunctionAttribute), + asm_function_def: ppAstAsmFunctionDef, +}); /** * Pretty-prints an AST node into a string representation. * @param node The AST node to format. * @returns A string that represents the formatted AST node. */ -export function prettyPrint(node: AstNode): string { - const pp = new PrettyPrinter(); - switch (node.kind) { - case "module": - return pp.ppAstModule(node); - case "op_binary": - case "op_unary": - case "field_access": - case "method_call": - case "static_call": - case "struct_instance": - case "init_of": - case "conditional": - case "number": - case "id": - case "boolean": - case "string": - case "null": - return pp.ppAstExpression(node); - case "struct_decl": - return pp.ppAstStruct(node); - case "constant_def": - return pp.ppAstConstant(node); - case "constant_decl": - return pp.ppAstConstDecl(node); - case "function_def": - return pp.ppAstFunctionDef(node); - case "contract": - return pp.ppAstContract(node); - case "trait": - return pp.ppAstTrait(node); - case "type_id": - case "optional_type": - case "map_type": - case "bounced_message_type": - return pp.ppAstType(node); - case "primitive_type_decl": - return pp.ppAstPrimitiveTypeDecl(node); - case "message_decl": - return pp.ppAstMessage(node); - case "native_function_decl": - return pp.ppAstNativeFunction(node); - case "field_decl": - return pp.ppAstFieldDecl(node); - case "function_decl": - return pp.ppAstFunctionDecl(node); - case "receiver": - return pp.ppAstReceiver(node); - case "contract_init": - return pp.ppAstInitFunction(node); - case "statement_let": - return pp.ppAstStatementLet(node); - case "statement_return": - return pp.ppAstStatementReturn(node); - case "statement_expression": - return pp.ppAstStatementExpression(node); - case "statement_assign": - return pp.ppAstStatementAssign(node); - case "statement_augmentedassign": - return pp.ppAstStatementAugmentedAssign(node); - case "statement_condition": - return pp.ppAstCondition(node); - case "statement_while": - return pp.ppAstStatementWhile(node); - case "statement_until": - return pp.ppAstStatementUntil(node); - case "statement_repeat": - return pp.ppAstStatementRepeat(node); - case "statement_try": - return pp.ppAstStatementTry(node); - case "statement_try_catch": - return pp.ppAstStatementTryCatch(node); - case "statement_foreach": - return pp.ppAstStatementForEach(node); - case "struct_field_initializer": - return pp.ppAstStructFieldInit(node); - case "import": - return pp.ppAstImport(node); - default: - throwInternalCompilerError( - `Unsupported AST type: ${JSONbig.stringify(node, null, 2)}`, - ); - } -} - -export function prettyPrintAsmShuffle(shuffle: AstAsmShuffle): string { - const ppArgShuffle = shuffle.args.map((id) => idText(id)).join(" "); - const ppRetShuffle = - shuffle.ret.length === 0 - ? "" - : ` -> ${shuffle.ret.map((num) => num.value.toString()).join(" ")}`; - return shuffle.args.length === 0 && shuffle.ret.length === 0 - ? "" - : `(${ppArgShuffle}${ppRetShuffle})`; -} +export const prettyPrint = (node: A.AstNode): string => + ppAstNode(node)(createContext(4)) + // Initial level of indentation is 0 + .map((f) => f(0)) + // Lines are terminated with \n + .join("\n"); diff --git a/src/test/contracts/case-priority.tact b/src/test/contracts/case-priority.tact new file mode 100644 index 000000000..0c290c55c --- /dev/null +++ b/src/test/contracts/case-priority.tact @@ -0,0 +1,17 @@ +contract Priority { + x: Bool; + y: Int; + + init() { + self.x = true || true && true == 5 < 6 << 9 + 7 * 8; + self.x = true || true && true != 5 > 6 >> 9 - 7 / 8; + self.x = true || true && true != 5 <= 6 >> 9 - 7 % 8; + self.x = true || true && true != 5 >= 6 >> 9 - 7 % 8; + self.y = 1 | 2 ^ 3 & 6 >> 9 - 7 % 8; + self.x = (true ? true : false) ? 1 : 2; + self.x = true ? (true ? 1 : 2) : 3; + self.x = false ? 1 : false ? 2 : 3; + self.x = +self.x!!; + self.x = (+self.x)!!; + } +} diff --git a/src/test/contracts/renamer-expected/case-priority.tact b/src/test/contracts/renamer-expected/case-priority.tact new file mode 100644 index 000000000..d113b924f --- /dev/null +++ b/src/test/contracts/renamer-expected/case-priority.tact @@ -0,0 +1,17 @@ +contract contract_0 { + x: Bool; + y: Int; + + init() { + self.x = true || true && true == 5 < 6 << 9 + 7 * 8; + self.x = true || true && true != 5 > 6 >> 9 - 7 / 8; + self.x = true || true && true != 5 <= 6 >> 9 - 7 % 8; + self.x = true || true && true != 5 >= 6 >> 9 - 7 % 8; + self.y = 1 | 2 ^ 3 & 6 >> 9 - 7 % 8; + self.x = (true ? true : false) ? 1 : 2; + self.x = true ? (true ? 1 : 2) : 3; + self.x = false ? 1 : false ? 2 : 3; + self.x = +self.x!!; + self.x = (+self.x)!!; + } +} diff --git a/src/utils/array.ts b/src/utils/array.ts new file mode 100644 index 000000000..ab1c33bf0 --- /dev/null +++ b/src/utils/array.ts @@ -0,0 +1,42 @@ +export const isUndefined = (t: T | undefined): t is undefined => + typeof t === "undefined"; + +export const groupBy = ( + items: readonly T[], + f: (t: T) => U, +): readonly (readonly T[])[] => { + const result: T[][] = []; + const [head, ...tail] = items; + if (isUndefined(head)) { + return result; + } + let group: T[] = [head]; + result.push(group); + let tag: U = f(head); + for (const item of tail) { + const nextTag = f(item); + if (tag === nextTag) { + group.push(item); + } else { + group = [item]; + result.push(group); + tag = nextTag; + } + } + return result; +}; + +export const intercalate = ( + items: readonly (readonly T[])[], + value: T, +): readonly T[] => { + const [head, ...tail] = items; + if (isUndefined(head)) { + return []; + } + const result: T[] = [...head]; + for (const item of tail) { + result.push(value, ...item); + } + return result; +}; diff --git a/src/utils/tricks.ts b/src/utils/tricks.ts index f85c7e6dd..7afbfdff2 100644 --- a/src/utils/tricks.ts +++ b/src/utils/tricks.ts @@ -88,3 +88,49 @@ export const match = ( throw new Error("Not exhaustive"); }) as MV, never>; }; + +import { throwInternalCompilerError } from "../errors"; + +/** + * Convert union to intersection. See https://stackoverflow.com/q/50374908 + */ +type Intersect = (T extends unknown ? (x: T) => 0 : never) extends ( + x: infer R, +) => 0 + ? R + : never; + +/** + * Makes types more readable + * Example: Unwrap<{ a: 1 } & { b: 2 }> = { a: 1, b: 2 } + */ +type Unwrap = T extends infer R ? { [K in keyof R]: R[K] } : never; + +type Inputs = I extends { kind: infer K } + ? K extends string + ? Record unknown> + : never + : never; +type Outputs = { [K in keyof O]: (input: never) => O[K] }; +type Handlers = Unwrap>> & Outputs; + +/** + * Make visitor for disjoint union (tagged union, discriminated union) + */ +export const makeVisitor = + () => + (handlers: Handlers) => + (input: Extract): O[keyof O] => { + const handler = (handlers as Record O[keyof O]>)[ + input.kind + ]; + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (handler) { + return handler(input); + } else { + throwInternalCompilerError( + `Reached impossible case: ${input.kind}`, + ); + } + };