diff --git a/src/cli.ts b/src/cli.ts index b3175fb..d63f4ee 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,6 +3,8 @@ import { ILexingError, IRecognitionException } from "chevrotain"; import { Command } from "commander"; import { readFileSync, writeFileSync } from "fs"; +import { formatGoFile } from "./generator/common/go/index.js"; +import { Extension } from "./generator/common/types.js"; import { generateSia, getExtension } from "./generator/index.js"; import { compile } from "./index.js"; import { logError } from "./utils/log.js"; @@ -48,6 +50,11 @@ program const newFileName = file.replace(".sia", `.${extension}`); const generatedSia = await generateSia(sir, extension); writeFileSync(newFileName, generatedSia); + + if (extension === Extension.GO) { + formatGoFile(newFileName); + } + console.info(`Sia file written to ${newFileName}`); } catch (error) { logError(src, file, error as ILexingError | IRecognitionException); diff --git a/src/generator/common/go/index.ts b/src/generator/common/go/index.ts new file mode 100644 index 0000000..cd335fe --- /dev/null +++ b/src/generator/common/go/index.ts @@ -0,0 +1,207 @@ +import { exec } from "child_process"; +import { promisify } from "util"; +import { FieldDefinition, SchemaDefinition } from "../../../visitor.js"; +import { getStringTypeFromLength, isAnyString, isByteArray } from "../index.js"; +import { SiaType } from "../types.js"; +import { + siaTypeArraySizeFunctionMap, + siaTypeFunctionMap, + siaTypeSerializerArrayItemMap, +} from "./maps.js"; +import { + capitalizeFirstLetter, + createAttributeString, + createCustomSerializerFunctionCallString, + createIfConditionString, + createSiaAddTypeFunctionCallString, + generateTypeFieldString, +} from "./strings.js"; + +const makeArrayType = (type: string) => { + return `[]${type}`; +}; + +export const hasBigInt = (schemas: SchemaDefinition[]): boolean => { + return schemas.some((schema) => + schema.fields.some((field) => field.type.startsWith("bigint")), + ); +}; + +const getGoType = (fieldType: string, isArray: boolean): string => { + if (isAnyString(fieldType as SiaType)) { + return isArray ? makeArrayType("string") : "string"; + } + if (fieldType.startsWith("byte")) { + return isArray ? makeArrayType("byte") : "Buffer"; + } + if (fieldType.startsWith("bigint")) { + return isArray ? makeArrayType("big.Int") : "big.Int"; + } + return isArray ? makeArrayType(fieldType) : fieldType; +}; + +export const generateInterfaceField = (field: FieldDefinition) => { + const type = getGoType(field.type, Boolean(field.isArray)); + return generateTypeFieldString(field.name, type, field.optional); +}; + +export const generateTypeFields = (fields: FieldDefinition[]) => { + return fields.map((field) => { + return generateInterfaceField(field); + }); +}; + +export const getDefaultValueForType = ( + field: SchemaDefinition["fields"][0], +): string => { + if (field.optional) { + return "nil"; + } + if (field.isArray) { + if (isAnyString(field.type as SiaType)) { + return `make([]string, 0)`; + } + if (isByteArray(field.type as SiaType)) { + return `make([]byte, 0)`; + } + if (field.type.startsWith("bigint")) { + return `make([]big.Int, 0)`; + } + return `make([]${field.type}, 0)`; + } + if (field.defaultValue) { + return `"${field.defaultValue}"`; + } + if (field.type.startsWith("int") || field.type.startsWith("uint")) { + return "0"; + } + if (field.type === "bool") { + return "false"; + } + return '""'; +}; + +export const generateAttribute = ( + field: FieldDefinition, + schemas: SchemaDefinition[], +): string => { + if (Object.values(SiaType).includes(field.type as SiaType)) { + return createAttributeString(field.name, getDefaultValueForType(field)); + } else { + return generateNestedObjectAttribute(field, schemas); + } +}; + +export const generateNestedObjectAttribute = ( + field: SchemaDefinition["fields"][0], + schemas: SchemaDefinition[], +): string => { + const referencedSchema = schemas.find((s) => s.name === field.type); + if (!referencedSchema) { + throw new Error(`Referenced schema ${field.type} not found`); + } + + const nestedFields = referencedSchema.fields + .map((nestedField) => generateAttribute(nestedField, schemas)) + .join("\n"); + + return createAttributeString( + field.type, + `${field.optional ? "&" : ""}${field.type}{\n${nestedFields}\n}`, + ); +}; + +const execAsync = promisify(exec); + +const isGoInstalled = async (): Promise => { + try { + await execAsync("go version"); + return true; + } catch { + return false; + } +}; + +export const formatGoFile = async (filePath: string) => { + if (await isGoInstalled()) { + await execAsync(`go fmt ${filePath}`); + console.info("Go file formatted"); + } else { + console.warn("Go is not installed. Skipping formatting..."); + } +}; + +export const generateArraySerializer = ( + fieldType: SiaType, + fieldName: string, + arraySize?: number, +) => { + if (isByteArray(fieldType)) { + return createSiaAddTypeFunctionCallString( + siaTypeFunctionMap[fieldType], + fieldName, + ); + } else { + const serializer = + siaTypeSerializerArrayItemMap[ + fieldType as keyof typeof siaTypeSerializerArrayItemMap + ]; + return createSiaAddTypeFunctionCallString( + arraySize + ? siaTypeArraySizeFunctionMap[arraySize] + : siaTypeArraySizeFunctionMap[8], + fieldName, + serializer, + ); + } +}; + +export const generateSchemaFunctionBody = (fields: FieldDefinition[]) => { + let fnBody = ""; + + fields.forEach((field) => { + let fieldType = field.type as SiaType; + const fieldName = `obj.${capitalizeFirstLetter(field.name)}`; + + if (fieldType === SiaType.String) { + fieldType = getStringTypeFromLength(field.max); + } + + let serializer = ""; + + if (field.isArray) { + serializer = generateArraySerializer( + fieldType, + `${field.optional ? "*" : ""}${fieldName}`, + field.arraySize, + ); + } else if (isAnyString(fieldType) && field.encoding === "ascii") { + serializer = createSiaAddTypeFunctionCallString( + "AddAscii", + `${field.optional ? "*" : ""}${fieldName}`, + ); + } else if (!Object.values(SiaType).includes(fieldType)) { + serializer = createCustomSerializerFunctionCallString( + fieldType, + "sia", + `${field.optional ? "" : "&"}${fieldName}`, + ); + } else { + const fn = siaTypeFunctionMap[fieldType]; + if (fn) { + serializer = createSiaAddTypeFunctionCallString( + fn, + `${field.optional ? "*" : ""}${fieldName}`, + ); + } + } + + if (field.optional) { + fnBody += createIfConditionString(`${fieldName} != nil`, serializer); + } else { + fnBody += serializer; + } + }); + + return fnBody; +}; diff --git a/src/generator/common/go/maps.ts b/src/generator/common/go/maps.ts new file mode 100644 index 0000000..b68a39e --- /dev/null +++ b/src/generator/common/go/maps.ts @@ -0,0 +1,52 @@ +import { SiaType } from "../types.js"; + +export const siaTypeFunctionMap: Record = { + [SiaType.Int8]: "AddInt8", + [SiaType.Int16]: "AddInt16", + [SiaType.Int32]: "AddInt32", + [SiaType.Int64]: "AddInt64", + [SiaType.UInt8]: "AddUInt8", + [SiaType.UInt16]: "AddUInt16", + [SiaType.UInt32]: "AddUInt32", + [SiaType.UInt64]: "AddUInt64", + [SiaType.String]: "AddStringN", + [SiaType.String8]: "AddString8", + [SiaType.String16]: "AddString16", + [SiaType.String32]: "AddString32", + [SiaType.String64]: "AddString64", + [SiaType.ByteArray8]: "AddByteArray8", + [SiaType.ByteArray16]: "AddByteArray16", + [SiaType.ByteArray32]: "AddByteArray32", + [SiaType.ByteArray64]: "AddByteArray64", + [SiaType.Bool]: "AddBool", + [SiaType.BigInt]: "AddBigInt", +}; + +export const siaTypeSerializerArrayItemMap: Record = { + [SiaType.Int8]: "SerializeInt8ArrayItem", + [SiaType.Int16]: "SerializeInt16ArrayItem", + [SiaType.Int32]: "SerializeInt32ArrayItem", + [SiaType.Int64]: "SerializeInt64ArrayItem", + [SiaType.UInt8]: "SerializeUInt8ArrayItem", + [SiaType.UInt16]: "SerializeUInt16ArrayItem", + [SiaType.UInt32]: "SerializeUInt32ArrayItem", + [SiaType.UInt64]: "SerializeUInt64ArrayItem", + [SiaType.String]: "SerializeStringArrayItem", + [SiaType.String8]: "SerializeString8ArrayItem", + [SiaType.String16]: "SerializeString16ArrayItem", + [SiaType.String32]: "SerializeString32ArrayItem", + [SiaType.String64]: "SerializeString64ArrayItem", + [SiaType.ByteArray8]: "SerializeByteArray8ArrayItem", + [SiaType.ByteArray16]: "SerializeByteArray16ArrayItem", + [SiaType.ByteArray32]: "SerializeByteArray32ArrayItem", + [SiaType.ByteArray64]: "SerializeByteArray64ArrayItem", + [SiaType.Bool]: "SerializeBoolArrayItem", + [SiaType.BigInt]: "SerializeBigIntArrayItem", +}; + +export const siaTypeArraySizeFunctionMap: Record = { + [8]: "AddArray8", + [16]: "AddArray16", + [32]: "AddArray32", + [64]: "AddArray64", +}; diff --git a/src/generator/common/go/strings.ts b/src/generator/common/go/strings.ts new file mode 100644 index 0000000..9d9aa53 --- /dev/null +++ b/src/generator/common/go/strings.ts @@ -0,0 +1,83 @@ +export const capitalizeFirstLetter = (str: string) => { + return str.charAt(0).toUpperCase() + str.slice(1); +}; + +export const createSiaImportString = (hasBigInt: boolean) => { + let importString = "import (\n"; + if (hasBigInt) { + importString += '"math/big"\n'; + } + importString += 'sializer "github.com/pouya-eghbali/go-sia/v2/pkg"\n'; + + return importString + ")"; +}; + +export const generateTypeString = (typeName: string, body: string) => { + return `type ${typeName} struct {\n${body}\n}`; +}; + +export const generateTypeFieldString = ( + name: string, + type: string, + optional: boolean = false, +) => { + return `${capitalizeFirstLetter(name)} ${optional ? "*" : ""}${type}`; +}; + +export const createAttributeString = (name: string, value: string) => { + return `${capitalizeFirstLetter(name)}: ${value},`; +}; + +export const createNamedObjectString = ( + name: string, + body: string, + type: string, +) => { + return `var ${name} = ${type}{\n${body}\n};\n`; +}; + +export const createSiaInstanceString = (schemaName: string) => { + return `var ${schemaName.toLowerCase()}Sia = sializer.New()\n`; +}; + +export const createSiaAddTypeFunctionCallString = ( + fn: string, + fieldName: string, + serializer?: string, +) => { + const serializerArg = serializer ? `, sializer.${serializer}` : ""; + return `sia.${fn}(${fieldName}${serializerArg})\n`; +}; + +export const createIfConditionString = (condition: string, body: string) => { + return `if ${condition} {\n${body}}\n`; +}; + +export const createCustomSerializerFunctionCallString = ( + serializer: string, + siaInstance: string, + fieldName: string, +) => { + return `serialize${capitalizeFirstLetter(serializer)}(${siaInstance}, ${fieldName})\n`; +}; + +export const createCustomSerializerFunctionDeclarationString = ( + fnName: string, + signature: string, + body: string, +) => { + return `func ${fnName}(${signature}) *sializer.Sia {\n${body}return sia\n}\n`; +}; + +export const createSiaResultString = ( + schemaName: string, + instanceName: string, +) => { + return ` + func main() { + serialize${schemaName}(${instanceName}, &${schemaName.toLowerCase()}) + result := ${instanceName}.Content() + _ = result + } + `; +}; diff --git a/src/generator/common/types.ts b/src/generator/common/types.ts index 0c95783..2c62e62 100644 --- a/src/generator/common/types.ts +++ b/src/generator/common/types.ts @@ -25,6 +25,7 @@ export enum SiaType { export enum Extension { JS = "js", TS = "ts", + GO = "go", } export interface Generator { diff --git a/src/generator/go.ts b/src/generator/go.ts new file mode 100644 index 0000000..b3f89f6 --- /dev/null +++ b/src/generator/go.ts @@ -0,0 +1,107 @@ +import { existsSync } from "fs"; +import path from "path"; +import { SchemaDefinition } from "../visitor.js"; +import { + generateAttribute, + generateSchemaFunctionBody, + generateTypeFields, + hasBigInt, +} from "./common/go/index.js"; +import { + createCustomSerializerFunctionDeclarationString, + createNamedObjectString, + createSiaImportString, + createSiaInstanceString, + createSiaResultString, + generateTypeString, +} from "./common/go/strings.js"; +import { Generator } from "./common/types.js"; +import { createLineBreakString } from "./index.js"; + +export const isGoProject = () => { + return existsSync(path.join(process.cwd(), "go.mod")); +}; + +export class GoGenerator implements Generator { + sir: SchemaDefinition[]; + + constructor(schemas: SchemaDefinition[]) { + this.sir = schemas; + } + + imports(): string { + return createSiaImportString(hasBigInt(this.sir)); + } + + types(): string { + return this.sir + .map((schema) => { + const fields = generateTypeFields(schema.fields); + return generateTypeString(schema.name, fields.join("\n")); + }) + .join(createLineBreakString(2)); + } + + sampleObject(): string { + const mainSchema = this.sir[0]; + const fields = mainSchema.fields + .map((field) => generateAttribute(field, this.sir)) + .join(createLineBreakString()); + const objectType = mainSchema.name; + + return createNamedObjectString( + mainSchema.name.toLowerCase(), + fields, + objectType, + ); + } + + siaInstance(): string { + const mainSchema = this.sir[0]; + const instanceName = `${mainSchema.name.toLowerCase()}Sia`; + let output = createSiaInstanceString(mainSchema.name); + output += createLineBreakString(); + + this.sir.forEach((schema) => { + output += this.generateSchemaFunction(schema); + output += createLineBreakString(2); + }); + + output += createLineBreakString(2); + output += createSiaResultString(mainSchema.name, instanceName); + + return output; + } + + private generateSchemaFunction(schema: SchemaDefinition): string { + const fnBody = generateSchemaFunctionBody(schema.fields); + const fnName = `serialize${schema.name}`; + const signature = `sia *sializer.Sia, obj *${schema.name}`; + return createCustomSerializerFunctionDeclarationString( + fnName, + signature, + fnBody, + ); + } + + toString(): string { + const parts = [ + "package main", + "", + this.imports(), + "", + this.types(), + "", + this.sampleObject(), + "", + this.siaInstance(), + ]; + + return parts.join("\n"); + } +} + +export const generateGo = async (schemas: SchemaDefinition[]) => { + const generator = new GoGenerator(schemas); + return generator.toString(); +}; diff --git a/src/generator/index.ts b/src/generator/index.ts index 08621f2..f964c86 100644 --- a/src/generator/index.ts +++ b/src/generator/index.ts @@ -1,5 +1,6 @@ import { SchemaDefinition } from "../visitor.js"; import { Extension } from "./common/types.js"; +import { generateGo, isGoProject } from "./go.js"; import { generateJs, isJsProject } from "./javascript.js"; import { generateTs, isTsProject } from "./typescript.js"; @@ -14,6 +15,8 @@ export const getExtension = (extension?: string) => { return Extension.TS; } else if (isJsProject()) { return Extension.JS; + } else if (isGoProject()) { + return Extension.GO; } } @@ -37,6 +40,9 @@ export const generateSia = (sir: SchemaDefinition[], extension: Extension) => { case Extension.TS: processor = generateTs; break; + case Extension.GO: + processor = generateGo; + break; } if (!processor) {