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

621 lines (531 loc) 16.9 kB
import { makeErrorCollector, makeTypeError } from "@helios-lang/compiler-utils" import { $ } from "@helios-lang/ir" import { expectDefined } from "@helios-lang/type-utils" import { ToIRContext } from "../codegen/index.js" import { Scope } from "../scopes/index.js" import { AllType, AnyType, DataEntity, FuncType, IntType, ParametricFunc, RealType, getTupleItemTypes } from "../typecheck/index.js" import { CallArgExpr } from "./CallArgExpr.js" import { Expr } from "./Expr.js" import { MemberExpr } from "./MemberExpr.js" import { ParametricExpr } from "./ParametricExpr.js" import { PathExpr } from "./PathExpr.js" /** * @import { Site } from "@helios-lang/compiler-utils" * @import { SourceMappedStringI } from "@helios-lang/ir" * @import { TypeCheckContext } from "../index.js" * @typedef {import("../typecheck/index.js").EvalEntity} EvalEntity * @typedef {import("../typecheck/index.js").Func} Func * @typedef {import("../typecheck/index.js").Type} Type * @typedef {import("../typecheck/index.js").Typed} Typed */ /** * ...(...) expression */ export class CallExpr extends Expr { /** * @private * @readonly * @type {Expr} */ _fnExpr /** * @private * @readonly * @type {CallArgExpr[]} */ _argExprs /** * @private * @type {Type[]} */ _paramTypes /** * @private * @type {Func | undefined} */ _appliedFnVal /** * @private * @type {Typed[]} */ posArgVals /** * @private * @type {Record<string, Typed>} */ namedArgVals /** * @private * @type {Typed[]} */ castedPosArgVals /** * @private * @type {Record<string, Typed>} */ castedNamedArgVals /** * @param {Site} site * @param {Expr} fnExpr * @param {CallArgExpr[]} argExprs */ constructor(site, fnExpr, argExprs) { super(site) this._fnExpr = fnExpr this._argExprs = argExprs this._paramTypes = [] this._appliedFnVal = undefined // only for infered parametric funcions this.posArgVals = [] this.namedArgVals = {} } get fnExpr() { return this._fnExpr } toString() { return `${this._fnExpr.toString()}(${this._argExprs.map((a) => a.toString()).join(", ")})` } /** * @returns {boolean} */ isLiteral() { if ( this._fnExpr instanceof PathExpr && this.cache?.asTyped && this._fnExpr.baseExpr.cache?.asType?.isBaseOf( this.cache.asTyped.type ) ) { return true } else { return false } } /** * @param {TypeCheckContext} ctx * @param {Scope} scope * @returns {EvalEntity} */ evalInternal(ctx, scope) { const fnVal = this._fnExpr.eval(ctx, scope) const argVals = this._argExprs.map((ae, i) => { const av_ = ae.eval(ctx, scope) const av = av_.asTyped if (!av) { ctx.errors.type(ae.site, `arg ${i + 1} not an instance`) return new DataEntity(new AnyType()) } return av }) this.posArgVals = [] this._argExprs.forEach((argExpr, i) => { if (!argExpr.isNamed()) { this.posArgVals.push(argVals[i]) } }) this.namedArgVals = {} this._argExprs.forEach((argExpr, i) => { if (argExpr.isNamed()) { const val = argVals[i] if (val.asTyped) { this.namedArgVals[argExpr.name] = val.asTyped } else { throw new Error("unexpected") } } }) if (this.posArgVals.some((pav) => pav == undefined)) { throw new Error("unexpected") } // might be mutated for implicit casting, so take a copy this.castedPosArgVals = this.posArgVals.slice() this.castedNamedArgVals = { ...this.namedArgVals } if (fnVal.asParametric) { this._paramTypes = [] this._appliedFnVal = fnVal.asParametric.inferCall( ctx, this.site, this.castedPosArgVals, this.castedNamedArgVals, this._paramTypes ) if (!this._appliedFnVal) { return new DataEntity(new AllType()) } return this._appliedFnVal.call( ctx, this.site, this.castedPosArgVals, this.castedNamedArgVals, viableCasts ) } else if (fnVal.asFunc) { return fnVal.asFunc.call( ctx, this.site, this.castedPosArgVals, this.castedNamedArgVals, viableCasts ) } else { if ( !(fnVal.asType instanceof AllType) && !(fnVal instanceof AllType) ) { ctx.errors.type( this._fnExpr.site, `unable to call ${fnVal.toString()} (returned by ${this._fnExpr.toString()})` ) } return new DataEntity(new AllType()) } } /** * Don't call this inside eval() because param types won't yet be complete. * @type {FuncType} */ get fn() { const ft = !!this._fnExpr.cache?.asParametric ? this._appliedFnVal?.type?.asType : this._fnExpr.cache?.asTyped?.type.asType if (ft instanceof FuncType) { return ft } else { throw new Error("unexpected") } } /** * @private * @param {Type} argType * @param {Type} targetType * @param {SourceMappedStringI} argIR * @param {ToIRContext} ctx * @returns {SourceMappedStringI} */ injectCastIR(argType, targetType, argIR, ctx) { if (IntType.isBaseOf(argType) && RealType.isBaseOf(targetType)) { return $`__helios__int__to_real(${argIR})()` } else { throw new Error("unhandled cast") } } /** * @private * @param {number | Expr} e * @param {ToIRContext} ctx * @returns {SourceMappedStringI} */ argExprToIR(e, ctx) { const i = typeof e == "number" ? e : this._argExprs.findIndex((ae) => ae.valueExpr == e) const ae = this._argExprs[i] const expr = ae.valueExpr let ir = expr.toIR(ctx) if (ae.isNamed()) { if ( this.namedArgVals[ae.name] != this.castedNamedArgVals[ae.name] ) { ir = this.injectCastIR( this.namedArgVals[ae.name].type, this.castedNamedArgVals[ae.name].type, ir, ctx ) } else { } } else { if (this.posArgVals[i] != this.castedPosArgVals[i]) { ir = this.injectCastIR( this.posArgVals[i].type, this.castedPosArgVals[i].type, ir, ctx ) } } return ir } /** * @param {ToIRContext} ctx * @returns {[Expr[], SourceMappedStringI[]]} - first list are positional args, second list named args and remaining opt args */ expandArgs(ctx) { const fn = this.fn const nNonOptArgs = fn.nNonOptArgs /** * @type {Expr[]} */ const positional = [] this._argExprs.forEach((ae) => { if (!ae.isNamed()) { positional.push(ae.valueExpr) } }) /** * @type {SourceMappedStringI[]} */ const namedOptional = [] this._argExprs.forEach((ae, i) => { if (ae.isNamed()) { // i is the index in this call, j is the index in function being called (named args can be in a completely different order) const errors = makeErrorCollector() const j = fn.getNamedIndex({ errors }, ae.site, ae.name) errors.throw() if (j < nNonOptArgs) { positional[j] = ae.valueExpr } else { namedOptional[j - nNonOptArgs] = $([ $("true"), $(", "), this.argExprToIR(i, ctx) ]) } } }) for (let i = nNonOptArgs; i < fn.nArgs; i++) { if (namedOptional[i - nNonOptArgs] == undefined) { namedOptional[i - nNonOptArgs] = $([ $("false"), $(", "), $("()") ]) } } return [positional.filter((p) => p != undefined), namedOptional] } /** * @param {ToIRContext} ctx * @returns {SourceMappedStringI} */ toFnExprIR(ctx) { if (this._fnExpr.cache?.asParametric instanceof ParametricFunc) { if (this._paramTypes.length == 0) { throw new Error("unexpected") } const params = ParametricExpr.toApplicationIR(this._paramTypes) if (this._fnExpr instanceof MemberExpr) { return this._fnExpr.toIR(ctx, params) } else { return $( `${this._fnExpr.toIR(ctx).toString()}${params}`, this._fnExpr.site ) } } else { return this._fnExpr.toIR(ctx) } } /** * @private * @param {Expr[]} posExprs * @returns {Map<Expr, number>} */ detectExpandedTuples(posExprs) { /** * @type {Map<Expr, number>} */ const result = new Map() let somePosArgsNull = false /** * @type {Typed[]} */ const posArgs = [] posExprs.forEach((e) => { const pa = e.cache?.asTyped if (!pa) { somePosArgsNull = true } else { posArgs.push(pa) } }) if (somePosArgsNull) { posExprs.forEach((e) => { result.set(e, 0) }) return result } const expandedPosArgs = this.fn.expandTuplesInPosArgs(posArgs) let j = 0 for (let i = 0; i < posArgs.length; i++) { if (j >= expandedPosArgs.length) { throw new Error("unexpected") } if (posArgs[i] == expandedPosArgs[j]) { result.set(posExprs[i], 0) j++ } else { const tupleItemTypes = getTupleItemTypes(posArgs[i].type) if (!tupleItemTypes) { throw new Error("unexpected") } result.set(posExprs[i], tupleItemTypes.length) j += tupleItemTypes.length } } return result } /** * @param {ToIRContext} ctx * @returns {SourceMappedStringI} */ toIR(ctx) { let fnIR = this.toFnExprIR(ctx) /** * We need the func type for things like multivalued args and optional args * @type {FuncType} */ const fn = this.fn /** * First step is to eliminate the named args * @type {[Expr[], SourceMappedStringI[]]} */ const [posExprs, namedOptExprs] = this.expandArgs(ctx) // some multiValued args (always positional) const isExpandedTuple = this.detectExpandedTuples(posExprs) if (posExprs.some((e) => (isExpandedTuple.get(e) ?? 0) > 0)) { // count the number of final args let n = 0 posExprs.forEach((e, i) => { if ((isExpandedTuple.get(e) ?? 0) > 0) { n += expectDefined(isExpandedTuple.get(e)) } else { n += 1 } }) n += namedOptExprs.length if (n > fn.nArgs) { namedOptExprs.splice(0, n - fn.nArgs) } let names = [] for (let i = 0; i < fn.nArgs; i++) { if (i >= fn.nNonOptArgs) { names.push(`__useopt__x${i}`) } names.push(`x${i}`) } let ir = $([ fnIR, $("("), $(names.map((n) => $(n))).join(", "), $(")", this.site) ]) for (let namedIR of namedOptExprs.slice().reverse()) { const n2 = expectDefined(names.pop()) const n1 = expectDefined(names.pop()) if (!n1.startsWith("__useopt__")) { throw new Error("unexpected") } ir = $([ $("("), $(n1), $(", "), $(n2), $(") -> {"), ir, $("}("), expectDefined(namedIR), // bool - val pair $(")") ]) } for (let i = posExprs.length - 1; i >= 0; i--) { const e = posExprs[i] if ((isExpandedTuple.get(e) ?? 0) > 0) { const nMulti = expectDefined(isExpandedTuple.get(e)) const multiNames = [] const multiOpt = [] while (multiNames.length < nMulti) { multiNames.unshift(expectDefined(names.pop())) if ( names.length > 0 && names[names.length - 1] == `__useopt__${multiNames[0]}` ) { multiOpt.unshift(expectDefined(names.pop())) } } if (multiOpt.length > 0) { ir = $([ $("("), $(multiOpt.map((n) => $(n))).join(", "), $(") -> {"), ir, $("}("), $(multiOpt.map((n) => $("true"))).join(", "), $(")") ]) } ir = $([ this.argExprToIR(e, ctx), $("(("), $(multiNames.map((n) => $(n))).join(", "), $(") -> {"), ir, $("})") ]) } else { const name = expectDefined(names.pop()) if ( names.length > 0 && names[names.length - 1] == `__useopt__${name}` ) { ir = $([ $("("), $(expectDefined(names.pop())), $(") -> {"), $("}(true)") ]) } ir = $([ $("("), $(name), $(") -> {"), ir, $("}("), this.argExprToIR(e, ctx), $(")") ]) } } return ir } /* no multivalued args */ else { if (posExprs.length + namedOptExprs.length > fn.nArgs) { namedOptExprs.splice( 0, posExprs.length + namedOptExprs.length - fn.nArgs ) } let args = posExprs .map((a, i) => { let ir = this.argExprToIR(a, ctx) if (i >= fn.nNonOptArgs) { ir = $([$("true, "), ir]) } return ir }) .concat(namedOptExprs) return $([fnIR, $("(", this.site), $(args).join(", "), $(")")]) } } } /** * @param {Type} argType * @param {Type} targetType * @returns {Type | undefined} */ function viableCasts(argType, targetType) { if (IntType.isBaseOf(argType) && RealType.isBaseOf(targetType)) { return targetType } else { return undefined } }