UNPKG

@helios-lang/compiler

Version:

Helios is a Domain Specific Language that compiles to Plutus-Core (i.e. Cardano on-chain validator scripts). Helios is a non-Haskell alternative to Plutus. With this library you can compile Helios scripts and build Cardano transactions, all you need to bu

809 lines (707 loc) 20.6 kB
import { bytesToHex, encodeUtf8 } from "@helios-lang/codec-utils" import { makeWord } from "@helios-lang/compiler-utils" import { $ } from "@helios-lang/ir" import { expectDefined } from "@helios-lang/type-utils" import { ToIRContext } from "../codegen/index.js" import { FuncArg, StructLiteralExpr } from "../expressions/index.js" import { Scope } from "../scopes/index.js" import { ArgType, FuncType, genCommonInstanceMembers, genCommonTypeMembers } from "../typecheck/index.js" import { DataField } from "./DataField.js" /** * @import { Site, Word } from "@helios-lang/compiler-utils" * @import { SourceMappedStringI } from "@helios-lang/ir" * @import { FieldTypeSchema } from "@helios-lang/type-utils" * @import { Definitions, TypeCheckContext } from "../index.js" * @typedef {import("../typecheck/index.js").DataType} DataType * @typedef {import("../typecheck/index.js").InstanceMembers} InstanceMembers * @typedef {import("../typecheck/index.js").Type} Type * @typedef {import("../typecheck/index.js").TypeMembers} TypeMembers */ /** * Base class for struct and enum member * @internal */ export class DataDefinition { /** * @private * @readonly * @type {Site} */ _site /** * @private * @readonly * @type {Word} */ _name /** * @private * @readonly * @type {DataField[]} */ _fields /** * @param {Site} site * @param {Word} name * @param {DataField[]} fields */ constructor(site, name, fields) { this._site = site this._name = name this._fields = fields } /** * @type {Site} */ get site() { return this._site } /** * @type {Word} */ get name() { return this._name } /** * @type {DataField[]} */ get fields() { return this._fields.slice() } /** * @type {string[]} */ get fieldNames() { return this._fields.map((f) => f.name.value) } isMappedStruct() { return this._fields.some((f) => f.hasEncodingKey()) } /** * Returns index of a field. * Returns -1 if not found. * @param {Word} name * @returns {number} */ findField(name) { let found = -1 let i = 0 for (let f of this._fields) { if (f.name.toString() == name.toString()) { found = i break } i++ } return found } /** * @param {Word} name * @returns {boolean} */ hasField(name) { return this.findField(name) != -1 } /** * @param {Word} name * @returns {boolean} */ hasMember(name) { return this.hasField(name) || name.value == "copy" } /** * @returns {string} */ toStringFields() { return `{${this._fields.map((f) => f.toString()).join(", ")}}` } /** * @returns {string} */ toString() { return `${this.name.toString()} ${this.toStringFields()}` } /** * @param {TypeCheckContext} ctx * @param {Scope} scope * @returns {InstanceMembers} */ evalFieldTypes(ctx, scope) { /** * @type {InstanceMembers} */ const fields = {} for (let f of this._fields) { const f_ = f.eval(ctx, scope) if (f_) { fields[f.name.value] = f_ } } return fields } /** * @param {Type} self * @returns {Type} */ genCopyType(self) { return new FuncType( this._fields.map((f) => new ArgType(f.name, f.type, true)), self ) } /** * @type {number} */ get nFields() { return this._fields.length } /** * @param {number} i * @returns {DataType} */ getFieldType(i) { return this._fields[i].type } /** * @param {string} name * @returns {number} */ getFieldIndex(name) { const i = this.findField(makeWord({ value: name })) if (i == -1) { throw new Error(`field ${name} not find in ${this.toString()}`) } else { return i } } /** * @param {number} i * @returns {string} */ getFieldName(i) { return this._fields[i].name.toString() } /** * Gets insance member value. * @param {Type} self * @returns {InstanceMembers} */ genInstanceMembers(self) { const members = { ...genCommonInstanceMembers(self), copy: new FuncType( this._fields.map((f) => new ArgType(f.name, f.type, true)), self ) } for (let f of this.fields) { members[f.name.value] = f.type } return members } /** * @param {Type} self * @returns {TypeMembers} */ genTypeMembers(self) { return { ...genCommonTypeMembers(self) } } /** * @param {Set<string>} parents * @returns {(FieldTypeSchema)[]} */ fieldsToSchema(parents) { /** * @type {FieldTypeSchema[]} */ const fieldSchemas = [] this._fields.forEach((f) => { // change to encodingKey const externalName = f.name.value const encodingKey = (f.hasEncodingKey() && f.encodedFieldName) || null const ts = expectDefined(f.type.toSchema(parents)) fieldSchemas.push({ name: externalName, type: ts, ...(encodingKey ? { // schemas use shorter key name to a) be concise in that context // ... and b) add distinction for that separate context key: encodingKey } : {}) // ... alt location for the comment? }) }) return fieldSchemas } /** * @param {ToIRContext} ctx * @param {string} path * @param {Definitions} map * @param {number} constrIndex */ toIR_new(ctx, path, map, constrIndex) { const isConstr = constrIndex != -1 /** * @type {SourceMappedStringI} */ let ir if (this.isMappedStruct()) { ir = $`__core__mkNilPairData(())` for (let i = this.nFields - 1; i >= 0; i--) { const f = this._fields[i] ir = $`__core__mkCons( __core__mkPairData( __core__bData(#${bytesToHex(encodeUtf8(f.encodedFieldName))}), ${f.type.path}____to_data(${f.name.value}) ), ${ir} )` } ir = $`(${$(this._fields.map((f) => $(f.name.value))).join(", ")}) -> { __core__mapData(${ir}) }` } else if (this.nFields == 1) { if (isConstr) { ir = $( `(self) -> { __core__constrData(${constrIndex}, __helios__common__list_1(${this.getFieldType(0).path}____to_data(self))) }`, this.site ) } else { ir = $`__helios__common__identity` } } else { ir = $`__core__mkNilData(())` for (let i = this.nFields - 1; i >= 0; i--) { const f = this._fields[i] ir = $`__core__mkCons(${f.type.path}____to_data(${f.name.value}), ${ir})` } if (isConstr) { ir = $`__core__constrData(${constrIndex}, ${ir})` } ir = $`(${$(this._fields.map((f) => $(f.name.value))).join(", ")}) -> {${ir}}` } const key = `${path}____new` map.set(key, { content: ir }) } /** * @param {ToIRContext} ctx * @param {string} path * @param {Definitions} map * @param {string[]} getterNames * @param {number} constrIndex */ toIR_copy(ctx, path, map, getterNames, constrIndex = -1) { const key = `${path}__copy` let ir = StructLiteralExpr.toIRInternal( ctx, this.site, path, this._fields.map((df) => $(df.name.value)) ) // wrap with defaults for (let i = getterNames.length - 1; i >= 0; i--) { const fieldName = this._fields[i].name.toString() ir = FuncArg.wrapWithDefaultInternal( ir, fieldName, $`${getterNames[i]}(self)` ) } const args = $( this._fields.map((f) => $([ $(`__useopt__${f.name.toString()}`), $(", "), $(`${f.name.toString()}`) ]) ) ).join(", ") ir = $`(self) -> { (${args}) -> { ${ir} } }` map.set(key, { content: ir }) } /** * @param {string} baseName * @param {boolean} isEnumMember * @returns {SourceMappedStringI} */ toIR_show(baseName, isEnumMember = false) { if (this.isMappedStruct()) { if (isEnumMember) { throw new Error("unexpected") } let ir = $`""` for (let i = 0; i < this.nFields; i++) { const f = this._fields[i] const p = f.type.path ir = $`__core__appendString( ${ir}, __core__appendString( "${i > 0 ? "," : ""}${f.name.value}:", option = __helios__common__mStruct_field_safe(self, #${bytesToHex(encodeUtf8(f.encodedFieldName))}); option_pair = __core__unConstrData__safe(option); option_tag = __core__fstPair(option_pair); __core__ifThenElse( __core__equalsInteger(option_tag, 1), () -> { "<n/a>" }, () -> { option_value = __core__sndPair(option_pair); inner_option = ${p}__from_data_safe(option_value); inner_option( (valid, value) -> { __core__ifThenElse( valid, () -> { ${p}__show(value)() }, () -> { "<n/a>" } )() } ) } )() ) )` } return $`(self) -> { () -> { __core__appendString( "{", __core__appendString( ${ir}, "}" ) ) } }` } else if (this.nFields == 1 && !isEnumMember) { return $`${this._fields[0].type.path}__show` } else { let ir = $`(fields) -> {""}` for (let i = this.nFields - 1; i >= 0; i--) { const f = this._fields[i] const p = f.type.path ir = $`(fields) -> { __core__chooseList( fields, () -> {""}, () -> { __core__appendString( ${i > 0 ? `", ${f.name}: "` : `"${f.name}: "`}, __core__appendString( (opt) -> { opt( (valid, value) -> { __core__ifThenElse( valid, () -> { ${p}__show(value)() }, () -> { "<n/a>" } )() } ) }(${p}__from_data_safe(__core__headList__safe(fields))), ${ir}(__core__tailList__safe(fields)) ) ) } )() }` } return $`(self) -> { () -> { __core__appendString( "${baseName}{", __core__appendString( ${ir}(self), "}" ) ) } }` } } /** * @param {boolean} argIsConstrFields - true in case of sndPair(unConstrData(...)) call for enum variant * @returns {SourceMappedStringI} */ toIR_is_valid_data(argIsConstrFields = false) { if (this.isMappedStruct()) { const fields = this._fields const mStructName = this._name.value let ir = $`true` fields.forEach((f, i) => { ir = $`__core__ifThenElse( __helios__common__test_mStruct_field( data, __core__bData(#${bytesToHex(encodeUtf8(f.encodedFieldName))}), ${f.type.path}__is_valid_data ), () -> { ${ir} }, () -> { __core__trace("Warning: invalid data in ${mStructName}.${f.encodedFieldName}", () -> { false } )() } )()` }) return $`(data) -> { ${ir} }` } else if (this.nFields == 1 && !argIsConstrFields) { return $`${this._fields[0].type.path}__is_valid_data` } else { const reversedFields = this._fields.slice().reverse() let ir = $`(fields) -> { __core__chooseList( fields, true, false ) }` reversedFields.forEach((f) => { ir = $`(fields) -> { __core__chooseList( fields, () -> { false }, () -> { (head) -> { __core__ifThenElse( ${f.type.path}__is_valid_data(head), () -> {${ir}(__core__tailList__safe(fields))}, () -> {false} )() }(__core__headList__safe(fields)) } )() }` }) return $`(data) -> { __core__chooseData( data, () -> {false}, () -> {false}, () -> { ${ir}(__core__unListData__safe(data)) }, () -> {false}, () -> {false} )() }` } } /** * @param {Site} site * @returns {SourceMappedStringI} */ toIR_mStructEq(site) { // the expected fields must exist in both let irInner = $`true` // same as reversing the fields: const n = this._fields.length - 1 for (let i = 0; i < this._fields.length; i++) { const f = this._fields[n - i] // builds the innermost field comparison from the LAST field, // so the outermost is likely to be the cheapest to find, given // the fields are stored in the same order they're defined in the struct. irInner = $`__core__ifThenElse( __core__equalsData( __helios__common__mStruct_field_internal(aFields, #${bytesToHex(encodeUtf8(f.encodedFieldName))}), __helios__common__mStruct_field_internal(bFields, #${bytesToHex(encodeUtf8(f.encodedFieldName))}) ), () -> {${irInner}}, () -> {false} )()` } let irOuter = $( `(a, b) -> { aFields = __core__unMapData(a); bFields = __core__unMapData(b); ${irInner} }`, site ) return irOuter } /** * @param {string} path * @returns {SourceMappedStringI} */ toIR_from_data_fields(path) { if (this.isMappedStruct()) { //let ir = IR.new`(data) -> {__core__mkNilPairData(())}`; let ir = $`(data) -> { (ignore) -> { data }( __core__ifThenElse( ${path}__is_valid_data(data), () -> { () }, () -> { __core__trace("Warning: invalid ${this.name.toString()} data", ()) } )() ) }` /*for (let i = this.nFields - 1; i >= 0; i--) { const f = this._fields[i] const ftPath = f.type.path; ir = IR.new`(data) -> { __core__mkCons( __core__mkPairData( __core__bData(#${bytesToHex(textToBytes(f.tag))}), ${ftPath}____to_data( ${ftPath}__from_data( __helios__common__mStruct_field( data, #${bytesToHex(textToBytes(f.tag))} ) ) ) ), ${ir}(data) ) }`; } ir = IR.new`(data) -> { __core__constrData( 0, __core__mkCons( __core__mapData(${ir}(data)), __core__mkCons( __core__iData(1), __core__mkNilData(()) ) ) ) }`;*/ return ir } else { let ir = $`(fields) -> { (ignore) -> { fields }( __core__ifThenElse( ${path}__is_valid_data(__core__listData(fields)), () -> { () }, () -> { __core__trace("Warning: invalid ${this.name.toString()} data", ()) } )() ) }` return ir /*let ir = IR.new`(fields) -> {__core__mkNilData(())}`; for (let i = this.nFields - 1; i >= 0; i--) { const ftPath = this.getFieldType(i).path; ir = IR.new`(fields) -> { __core__mkCons( ${ftPath}____to_data( ${ftPath}__from_data( __core__headList(fields) ) ), ${ir}(__core__tailList(fields)) ) }`; } return ir;*/ } } /** * Doesn't return anything, but sets its IRdef in the map * @param {ToIRContext} ctx * @param {string} path * @param {Definitions} map * @param {number} constrIndex */ toIR(ctx, path, map, constrIndex) { /** * @type {string[]} */ const getterNames = [] if (this.isMappedStruct()) { for (let i = 0; i < this._fields.length; i++) { const f = this._fields[i] const key = `${path}__${f.name.value}` // equalsData is much more efficient than first converting to byteArray const getter = $`(self) -> {${f.type.path}__from_data(__helios__common__mStruct_field(self, #${bytesToHex(encodeUtf8(f.encodedFieldName))}))}` map.set(key, { content: getter }) getterNames.push(key) } } else { const isConstr = constrIndex != -1 const getterBaseName = isConstr ? "__helios__common__enum_field" : "__helios__common__struct_field" if (this.fields.length == 1 && !isConstr) { const f = this.fields[0] const key = `${path}__${f.name.value}` const getter = $(`__helios__common__identity`, f.site) map.set(key, { content: getter }) getterNames.push(key) } else { // add a getter for each field for (let i = 0; i < this._fields.length; i++) { let f = this._fields[i] let key = `${path}__${f.name.value}` getterNames.push(key) /** * @type {SourceMappedStringI} */ let getter if (i < 20) { getter = $`(self) ${$("->", f.site)} { ${f.type.path}__from_data(${getterBaseName}_${i}(self)) }` } else { let inner = $("self") if (isConstr) { inner = $`__core__sndPair(__core__unConstrData(${inner}))` } for (let j = 0; j < i; j++) { inner = $`__core__tailList(${inner})` } inner = $`${f.type.path}__from_data(__core__headList(${inner}))` getter = $`(self) ${$("->", f.site)} {${inner}}` } map.set(key, { content: getter }) } } } this.toIR_new(ctx, path, map, constrIndex) this.toIR_copy(ctx, path, map, getterNames) } }