@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
252 lines (216 loc) • 7.61 kB
JavaScript
import { $, compile as compileIR } from "@helios-lang/ir"
import { expectDefined } from "@helios-lang/type-utils"
import { ToIRContext, genExtraDefs } from "../codegen/index.js"
import { FuncArg } from "../expressions/index.js"
import { IR_PARSE_OPTIONS } from "../parse/index.js"
import { ConstStatement, FuncStatement } from "../statements/index.js"
import { VoidType } from "../typecheck/index.js"
import { ModuleCollection } from "./ModuleCollection.js"
/**
* @typedef {import("@helios-lang/uplc").UplcProgramV2} UplcProgramV2
* @typedef {import("@helios-lang/ir").SourceMappedStringI} SourceMappedStringI
* @typedef {import("../typecheck/index.js").ScriptTypes} ScriptTypes
* @typedef {import("./EntryPoint.js").EntryPoint} EntryPoint
*/
export class UserFunc {
/**
* @readonly
* @type {ModuleCollection}
*/
modules
/**
* @readonly
* @type {string}
*/
name
/**
* @param {ModuleCollection} modules
* @param {string} name - member functions have a `Type::` prefix
*/
constructor(modules, name) {
this.modules = modules
this.name = name
}
/**
* @type {ConstStatement}
*/
get mainConst() {
const cn = this.main
if (!(cn instanceof ConstStatement)) {
throw new Error("expected entry point const, got function")
}
return cn
}
/**
* @type {FuncStatement}
*/
get mainFunc() {
const fn = this.main
if (!(fn instanceof FuncStatement)) {
throw new Error("expected entry point function, got const")
}
return fn
}
/**
* @type {FuncStatement | ConstStatement}
*/
get main() {
const lastModule = this.modules.lastModule
const nameParts = this.name.split("::")
for (let s of lastModule.statements) {
if (
(s instanceof FuncStatement || s instanceof ConstStatement) &&
s.name.value == this.name
) {
return s
} else if (s.name.value == nameParts[0]) {
for (let ss of s.statements) {
if (
(ss instanceof FuncStatement ||
ss instanceof ConstStatement) &&
ss.name.value == nameParts[1]
) {
return ss
}
}
throw new Error(`${this.name} undefined`)
}
}
throw new Error(`${this.name} undefined`)
}
/**
* @param {{
* optimize: boolean
* validatorTypes: ScriptTypes
* validatorIndices?: Record<string, number>
* hashDependencies: Record<string, string>
* currentScriptValue?: string
* }} props
* @returns {UplcProgramV2}
*/
compile(props) {
const { ir } = this.toIR({
validatorTypes: props.validatorTypes,
optimize: props.optimize,
hashDependencies: props.hashDependencies,
validatorIndices: props.validatorIndices,
currentScriptValue: props.currentScriptValue
})
const uplc = compileIR(ir, {
optimize: props.optimize,
parseOptions: IR_PARSE_OPTIONS
})
return uplc
}
/**
* @param {{
* validatorTypes: ScriptTypes
* validatorIndices?: Record<string, number>
* optimize?: boolean
* hashDependencies?: Record<string, string>
* currentScriptValue?: string
* }} props
* @returns {{
* ir: SourceMappedStringI
* requiresScriptContext: boolean
* requiresCurrentScript: boolean
* }}
*/
toIR(props) {
const ctx = new ToIRContext({
optimize: props.optimize ?? false,
isTestnet: false,
makeParamsSubstitutable: false
})
const extra = genExtraDefs({
dependsOnOwnHash: false,
makeParamsSubstitutable: false,
hashDependencies:
props.hashDependencies ??
Object.fromEntries(
Array.from(Object.keys(props.validatorTypes)).map(
(name) => [name, "#"]
)
),
name: this.name,
validatorTypes: props.validatorTypes,
validatorIndices: props.validatorIndices,
currentScriptValue: props.currentScriptValue ?? "__CURRENT_SCRIPT"
})
/**
* @param {FuncArg[]} args
* @returns {string}
*/
const argsToString = (args) => {
return args
.map((arg) => {
const name = arg.name.value
const typePath = expectDefined(arg.type.asDataType).path
if (arg.isIgnored()) {
return "()"
} else if (arg.isOptional) {
// assume outer arg is wrapped in data-option
const cond = `__helios__option__is_some(${name})`
const value = `__core__ifThenElse(
${cond},
() -> {
__helios__option[${typePath}]__some__some(${name})
},
() -> {()}
)()`
return `${cond}, ${value}`
} else {
return `${typePath}__from_data(${name})`
}
})
.join(", ")
}
const fn = this.main
const args = fn instanceof FuncStatement ? fn.args : []
/**
* @type {SourceMappedStringI}
*/
let ir
if (fn instanceof ConstStatement) {
const retTypePath = expectDefined(fn.type).path
ir = $`${retTypePath}____to_data(${fn.path})`
} else {
const isMethod = fn.funcExpr.isMethod()
ir = isMethod
? $`${fn.path}(${argsToString(args.slice(0, 1))})(${argsToString(args.slice(1))})`
: $`${fn.path}(${argsToString(args)})`
if (!new VoidType().isBaseOf(fn.retType)) {
const retTypePath = expectDefined(fn.retType.asDataType).path
ir = $`${retTypePath}____to_data(${ir})`
}
}
const defs = this.modules.fetchDefinitions(
ctx,
ir,
(s, isImport) =>
!isImport && s.name.value == this.name.split("::")[0],
extra
)
ir = this.modules.wrap(ctx, ir, defs)
// if a non-dummy currentScriptValue was specified, then the IR won't depend on the currentScript
const requiresCurrentScript =
ir.includes("__helios__scriptcontext__current_script") &&
!(props.currentScriptValue && props.currentScriptValue.length > 1)
const requiresScriptContext = ir.includes(
"__helios__scriptcontext__data"
)
const outerArgNames = args
.filter((arg) => !arg.isIgnored())
.map((arg) => arg.name.value)
.concat(requiresScriptContext ? ["__CONTEXT"] : [])
.concat(requiresCurrentScript ? ["__CURRENT_SCRIPT"] : [])
ir = $`(${outerArgNames.join(", ")}) -> {
${ir}
}`
return {
ir: ir,
requiresCurrentScript: requiresCurrentScript,
requiresScriptContext: requiresScriptContext
}
}
}