diff --git a/as-packages/as-contract-runtime/assembly/seal0.ts b/as-packages/as-contract-runtime/assembly/seal0.ts index abc1b7b..5f58f54 100644 --- a/as-packages/as-contract-runtime/assembly/seal0.ts +++ b/as-packages/as-contract-runtime/assembly/seal0.ts @@ -1,6 +1,6 @@ import { Ptr, ReturnCode, Size } from "."; -export * from "./unstable"; +// export * from "./unstable"; // Set the value at the given key in the contract storage. diff --git a/as-packages/lang/assembly/env/onchain.ts b/as-packages/lang/assembly/env/onchain.ts index d4297d9..fedfe73 100644 --- a/as-packages/lang/assembly/env/onchain.ts +++ b/as-packages/lang/assembly/env/onchain.ts @@ -44,6 +44,12 @@ export class EnvInstance implements TypedEnvBackend { return ScaleDeserializer.deserialize(BytesBuffer.wrap(storageBuffer.buffer)); } + @inline + debugMessage(message: string): void { + const buffer = String.UTF8.encode(message); + seal0.seal_debug_message(changetype(buffer), buffer.byteLength); + } + setContractStorage( key: K, value: V diff --git a/as-packages/lang/assembly/interfaces/backend.ts b/as-packages/lang/assembly/interfaces/backend.ts index f7dbc2d..e838fc9 100644 --- a/as-packages/lang/assembly/interfaces/backend.ts +++ b/as-packages/lang/assembly/interfaces/backend.ts @@ -49,6 +49,8 @@ export interface EnvBackend { */ returnValue(flags: u32, value: V): void; + debugMessage(message: string): void; + // TODO: add more methods } diff --git a/ts-packages/contract-metadata/src/specs.ts b/ts-packages/contract-metadata/src/specs.ts index d35e5e1..3d568f0 100644 --- a/ts-packages/contract-metadata/src/specs.ts +++ b/ts-packages/contract-metadata/src/specs.ts @@ -86,9 +86,10 @@ export type IPrimitiveDef = Def<{ }>; export type ITupleDef = Def<{ - readonly tuple: { - readonly fields: Array; - }; + readonly tuple: Array; + // readonly tuple: { + // readonly fields: Array; + // }; }>; export type IArrayDef = Def<{ @@ -112,20 +113,20 @@ export type ICompositeDef = Def<{ }>; export type IVariantDef = Def<{ - readonly variants: Array; + readonly variant: { readonly variants: Array }; // readonly path: Array; }>; export interface IVariant { readonly name: string; - readonly fields: Array; - readonly discriminant: number | null; + readonly fields: Array | null; + readonly index: number; } export interface IField { readonly name: string | null; readonly type: number; - readonly typeName: string; + readonly typeName: string | null; } export interface IContract { diff --git a/ts-packages/contract-metadata/src/types.ts b/ts-packages/contract-metadata/src/types.ts index 37cb79f..156ae45 100644 --- a/ts-packages/contract-metadata/src/types.ts +++ b/ts-packages/contract-metadata/src/types.ts @@ -77,9 +77,7 @@ export class TupleDef implements Type { toMetadata(): ITupleDef { return { def: { - tuple: { - fields: this.fields, - }, + tuple: this.fields, }, path: null, }; @@ -165,7 +163,7 @@ export class VariantDef implements Type { toMetadata(): IVariantDef { return { def: { - variants: this.variants.map((v) => v.toMetadata()), + variant: { variants: this.variants.map((v) => v.toMetadata()) }, }, path: this.path.length > 0 ? this.path : null, }; @@ -195,15 +193,15 @@ export class VariantDef implements Type { export class Variant implements ToMetadata { constructor( public readonly name: string, - public readonly fields: Array, - public readonly discriminant: number | null, + public readonly fields: Array | null, + public readonly index: number, ) {} toMetadata(): IVariant { return { name: this.name, - fields: this.fields.map((f) => f.toMetadata()), - discriminant: this.discriminant, + fields: this.fields ? this.fields.map((f) => f.toMetadata()) : null, + index: this.index, }; } } diff --git a/ts-packages/transform/src/ast.ts b/ts-packages/transform/src/ast.ts index 7a2b65c..677df05 100644 --- a/ts-packages/transform/src/ast.ts +++ b/ts-packages/transform/src/ast.ts @@ -7,13 +7,17 @@ import { } from "visitor-as/as"; import { DecoratorConfig, extractConfigFromDecorator, extractDecorator } from "./util"; import { utils } from "visitor-as/dist"; -import { DecoratorNode } from "assemblyscript"; +import { DecoratorNode, FieldDeclaration } from "assemblyscript"; import blake from "blakejs"; /** * Ask supported decorators. */ export enum ContractDecoratorKind { + // tuple + Tuple = "tuple", + Enumeration = "enumeration", + Variant = "variant", SpreadLayout = "spreadLayout", PackedLayout = "packedLayout", Contract = "contract", @@ -319,6 +323,52 @@ export class EventDeclaration implements AskNode { } } +/** + * Varinat Declaration represents a `@variant` info + */ +export class VariantDeclaration implements AskNode { + public readonly contractKind: ContractDecoratorKind = ContractDecoratorKind.Variant; + constructor( + /** + * Event Id + */ + public readonly index: number | null, + public readonly name: string | null, + public readonly variant: FieldDeclaration, + ) {} + + /** + * Collect variant infos from config. + * @param node + * @param cfg + * @returns + */ + static extractFrom(emitter: DiagnosticEmitter, node: FieldDeclaration): VariantDeclaration { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const decorator = extractDecorator(emitter, node, ContractDecoratorKind.Variant)!; + const cfg = extractConfigFromDecorator(emitter, decorator); + const variantIndex = cfg.get("index"); + const variantName = cfg.get("name"); + + const name = variantName ? variantName.substring(1, variantName.length - 1) : null; + let index = null; + if (variantIndex) { + if (!isNaN(+variantIndex)) { + index = +variantIndex; + } else { + emitter.errorRelated( + DiagnosticCode.User_defined_0, + node.range, + decorator.range, + `Ask-lang: '@variant' index config is illegal`, + ); + } + } + + return new VariantDeclaration(index, name, utils.cloneNode(node)); + } +} + function getParamsTypeName(fn: FunctionTypeNode): string[] { return fn.parameters.map((p) => p.type.range.toString()); } diff --git a/ts-packages/transform/src/metadata/generator.ts b/ts-packages/transform/src/metadata/generator.ts index a85db3f..0b433a1 100644 --- a/ts-packages/transform/src/metadata/generator.ts +++ b/ts-packages/transform/src/metadata/generator.ts @@ -15,6 +15,7 @@ import { Class, ClassDeclaration, DiagnosticCode, + Type, } from "assemblyscript"; import { version } from "visitor-as/as"; import { @@ -34,6 +35,9 @@ import { SequenceDef, ArrayDef, VersionedContractMetadata, + TupleDef, + VariantDef, + Variant, } from "ask-contract-metadata"; import * as metadata from "ask-contract-metadata"; import { ASK_VERSION } from "../consts"; @@ -53,7 +57,7 @@ import { } from "."; import debug from "debug"; import { uniqBy } from "lodash"; -import { ArrayTypeInfo } from "./typeInfo"; +import { ArrayTypeInfo, TupleTypeInfo, VariantTypeInfo } from "./typeInfo"; export const LANGUAGE = `Ask! ${ASK_VERSION}`; export const COMPILER = `asc ${version}`; @@ -183,11 +187,24 @@ export class MetadataGenerator { } case metadata.TypeKind.Composite: { + // log("class name", info.type?.getClass()?.name); const def = this.genCompositeType(resolver, info as CompositeTypeInfo); typeSpecs[info.index] = new metadata.TypeWithId(info.index, def); break; } + case metadata.TypeKind.Tuple: { + const def = this.genTupleType(resolver, info as TupleTypeInfo); + typeSpecs[info.index] = new metadata.TypeWithId(info.index, def); + break; + } + + case metadata.TypeKind.Variant: { + const def = this.genVariantType(resolver, info as VariantTypeInfo); + typeSpecs[info.index] = new metadata.TypeWithId(info.index, def); + break; + } + default: { assert(false, `Ask-lang: unspported type kind in metadata: ${info.kind}`); } @@ -198,48 +215,99 @@ export class MetadataGenerator { return typeSpecs; } - private genCompositeType(resolver: TypeResolver, info: CompositeTypeInfo): CompositeDef { + private genTupleType(resolver: TypeResolver, info: TupleTypeInfo): TupleDef { + const types = resolver.resolvedTypes(); + return new TupleDef( + info.types.map((type) => { + const fieldTypeInfo = types.get(type); + assert(fieldTypeInfo != null, `Ask-lang: '${type.getClass()?.name} not found`); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return fieldTypeInfo!.index; + }), + ); + } + + private genVariantType(resolver: TypeResolver, info: VariantTypeInfo): VariantDef { assert( info.fields.length > 0, `Ask-lang: Composite def ${info.type?.toString()} fields are empty`, ); - const types = resolver.resolvedTypes(); - const fields: metadata.Field[] = info.fields.map((field) => { - // field is named - if (field instanceof Field) { + const variants: metadata.Variant[] = info.fields.map( + ({ field, isEmpty, innerFields, variantDecl }, idx) => { const fieldDecl = field.prototype.declaration as FieldDeclaration; - const fieldTypeInfo = types.get(field.type); - assert( - fieldTypeInfo != null, - `Ask-lang: '${field.name}: ${field.type.toString()}' not found`, - ); - const ret = new metadata.Field( - fieldDecl.name.range.toString(), - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - fieldTypeInfo!.index, - // we make sure all fields gived a type explicitly - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - fieldDecl.type!.range.toString(), - ); - return ret; - } else { - // type is unnamed - const fieldTypeInfo = types.get(field); - assert(fieldTypeInfo != null, `Ask-lang: type '${field.toString()}' not found`); - const ret = new metadata.Field( - null, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - fieldTypeInfo!.index, + const index = info.fields.length - idx - 1; + const name = variantDecl.name ?? fieldDecl.name.range.toString(); + + if (isEmpty) { + // empty variant + return new Variant( + name, + null, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + index, + ); + } else { + // either composite or tuple + const types = resolver.resolvedTypes(); + assert(innerFields !== null); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - field.toString(), - ); - return ret; - } - }); + const innerVariantFields: metadata.Field[] = innerFields!.map((variantField) => + this.convertToMetadataField(types, variantField), + ); + return new Variant(name, innerVariantFields, index); + } + }, + ); + let path = info.type ? info.type.toString() : "unknown"; + return new VariantDef(variants).setPath([path]); + } + + private genCompositeType(resolver: TypeResolver, info: CompositeTypeInfo): CompositeDef { + assert( + info.fields.length > 0, + `Ask-lang: Composite def ${info.type?.toString()} fields are empty`, + ); + const types = resolver.resolvedTypes(); + const fields: metadata.Field[] = info.fields.map((field) => + this.convertToMetadataField(types, field), + ); let path = info.type ? info.type.toString() : "unknown"; return new CompositeDef(fields).setPath([path]); } + convertToMetadataField(types: TypeInfoMap, field: Field | Type): metadata.Field { + // field is named + if (field instanceof Field) { + const fieldDecl = field.prototype.declaration as FieldDeclaration; + const fieldTypeInfo = types.get(field.type); + assert( + fieldTypeInfo != null, + `Ask-lang: '${field.name}: ${field.type.toString()}' not found`, + ); + const ret = new metadata.Field( + fieldDecl.name.range.toString(), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + fieldTypeInfo!.index, + // we make sure all fields gived a type explicitly + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + fieldDecl.type!.range.toString(), + ); + return ret; + } else { + // type is unnamed + const fieldTypeInfo = types.get(field); + assert(fieldTypeInfo != null, `Ask-lang: type '${field.toString()}' not found`); + const ret = new metadata.Field( + null, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + fieldTypeInfo!.index, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + field.toString(), + ); + return ret; + } + } + private genMessages(resolver: TypeResolver): MessageSpec[] { log(this.genMessages.name); diff --git a/ts-packages/transform/src/metadata/typeInfo.ts b/ts-packages/transform/src/metadata/typeInfo.ts index 5dd0060..a0b7368 100644 --- a/ts-packages/transform/src/metadata/typeInfo.ts +++ b/ts-packages/transform/src/metadata/typeInfo.ts @@ -1,5 +1,6 @@ import * as metadata from "ask-contract-metadata"; import { Field, Type } from "visitor-as/as"; +import { VariantField } from "./typeResolver"; /** * TypeInfo contains some type infos needed for metadata types @@ -12,12 +13,24 @@ export abstract class TypeInfo { ) {} } +export class TupleTypeInfo extends TypeInfo { + constructor(type: Type, index: number, public readonly types: Type[]) { + super(type, index, metadata.TypeKind.Tuple); + } +} + export class CompositeTypeInfo extends TypeInfo { constructor(type: Type | null, index: number, public readonly fields: Field[] | Type[]) { super(type, index, metadata.TypeKind.Composite); } } +export class VariantTypeInfo extends TypeInfo { + constructor(type: Type | null, index: number, public readonly fields: VariantField[]) { + super(type, index, metadata.TypeKind.Variant); + } +} + /** * AssemblyScript has no native fixed array. So we define these types by hand. */ diff --git a/ts-packages/transform/src/metadata/typeResolver.ts b/ts-packages/transform/src/metadata/typeResolver.ts index 5cc446c..ee757c0 100644 --- a/ts-packages/transform/src/metadata/typeResolver.ts +++ b/ts-packages/transform/src/metadata/typeResolver.ts @@ -22,12 +22,22 @@ import { TypeInfo, ArrayTypeInfo, SequenceTypeInfo, + TupleTypeInfo, + VariantTypeInfo, } from "./typeInfo"; +import { hasDecorator } from "../util"; +import { ContractDecoratorKind, VariantDeclaration } from "../ast"; +import { FieldDeclaration } from "assemblyscript"; const log = debug("TypeResolver"); export type TypeInfoMap = Map; - +export interface VariantField { + variantDecl: VariantDeclaration; + field: Field; + innerFields: Array | null; + isEmpty: bool; +} /** * TypeResolver resolve an entrypoint contract type infos, and collect some useful type infos. */ @@ -251,6 +261,14 @@ export class TypeResolver { this.resolveScaleType(typeArgs[0]); this.types.set(type, new SequenceTypeInfo(type, this.currentIndex++, typeArgs[0])); } + } else if (isTuple(clz)) { + // get all fields + const types = this.resovleCompositeField(type).map((field) => field.type); + this.types.set(type, new TupleTypeInfo(type, this.currentIndex++, types)); + } else if (isEnumeration(clz)) { + // it's an enumeration + const fields = this.resolveVariantField(type); + this.types.set(type, new VariantTypeInfo(type, this.currentIndex++, fields)); } else { // It's composite type const fields = this.resovleCompositeField(type); @@ -258,6 +276,57 @@ export class TypeResolver { } } + private resolveVariantField(type: Type): VariantField[] { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const clz: Class = type.getClass()!; + assert(clz.declaration.kind === NodeKind.CLASSDECLARATION); + const fields: VariantField[] = []; + + // TODO: forbid class inherihance + + const decl = clz.declaration as ClassDeclaration; + for (let i = decl.members.length - 1; i >= 0; i--) { + let member = decl.members[i]; + // we only need to know non-static field type + if (member.isAny(CommonFlags.STATIC)) { + continue; + } + if (member.kind != NodeKind.FIELDDECLARATION) { + continue; + } + // if not decorated with @variant we skip + if (!isVariantField(member as FieldDeclaration)) { + continue; + } + + const variantDecl = VariantDeclaration.extractFrom( + this.program, + member as FieldDeclaration, + ); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const elem = clz!.members!.get(member.name.text); + assert(elem?.kind === ElementKind.FIELD); + const field = elem as Field; + + const fieldClz = field.type.getClass(); + const isTupleVaraint = fieldClz && isTuple(fieldClz); + const isEmpty = isEmptyVariantField(field); + + let innerFields: Array | null = null; + + if (!isEmpty) { + // tuple variant + innerFields = isTupleVaraint + ? this.resovleCompositeField(field.type).map((f) => f.type) + : this.resovleCompositeField(field.type); + } + fields.push({ field, isEmpty, innerFields, variantDecl }); + } + + return fields; + } + private resovleCompositeField(type: Type): Field[] { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const clz: Class = type.getClass()!; @@ -387,3 +456,20 @@ function getFirstValue(map: Map): V { } return v; } + +function isTuple(clz: Class): bool { + return hasDecorator(clz.decoratorNodes, ContractDecoratorKind.Tuple); +} + +function isEnumeration(clz: Class): bool { + return hasDecorator(clz.decoratorNodes, ContractDecoratorKind.Enumeration); +} + +function isVariantField(member: FieldDeclaration): bool { + return hasDecorator(member.decorators, ContractDecoratorKind.Variant); +} + +function isEmptyVariantField(field: Field): bool { + // TODO: better way to find empty variants + return isVariantField(field.declaration as FieldDeclaration) && field.type.isNumericValue; +}