@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
224 lines (194 loc) • 6.51 kB
JavaScript
import { isCompilerError, makeSyntaxError } from "@helios-lang/compiler-utils"
import { createSource, parseScript } from "../parse/index.js"
import { MintingEntryPoint } from "./MintingEntryPoint.js"
import { SpendingEntryPoint } from "./SpendingEntryPoint.js"
import { StakingEntryPoint } from "./StakingEntryPoint.js"
import { TestingEntryPoint } from "./TestingEntryPoint.js"
import { Module } from "./Module.js"
import { MainModule } from "./MainModule.js"
import { GenericEntryPoint } from "./GenericEntryPoint.js"
import { MixedEntryPoint } from "./MixedEntryPoint.js"
import { ModuleCollection } from "./ModuleCollection.js"
import { ModuleEntryPoint } from "./ModuleEntryPoint.js"
/**
* @import { ErrorCollector, Site, Source } from "@helios-lang/compiler-utils"
* @typedef {import("../typecheck/index.js").ScriptTypes} ScriptTypes
* @typedef {import("../typecheck/index.js").Type} Type
* @typedef {import("./EntryPoint.js").EntryPoint} EntryPoint
*/
/**
* Creates a new entry point
* This function can't be placed inside EntryPoint.js because that would create a circular import dependency
* @param {string | Source} mainSrc
* @param {object} options
* @param {(string | Source)[]} options.moduleSrcs - optional sources of modules, which can be used for imports
* @param {ScriptTypes} options.validatorTypes
* @param {ErrorCollector} options.errorCollector
* @param {boolean} [options.allowModuleEntryPoint]
* @returns {EntryPoint}
*/
export function newEntryPoint(
mainSrc,
{ moduleSrcs, validatorTypes, errorCollector, ...options }
) {
const [purpose, modules, mainHeaderSite] = parseMain(
mainSrc,
moduleSrcs,
errorCollector
)
if (!(options.allowModuleEntryPoint ?? false) && purpose == "module") {
errorCollector.syntax(mainHeaderSite, "can't use module for main")
}
const moduleCol = new ModuleCollection(modules)
/**
* @type {EntryPoint}
*/
let entryPoint
switch (purpose) {
case "testing":
entryPoint = new TestingEntryPoint(moduleCol)
break
case "spending":
entryPoint = new SpendingEntryPoint(moduleCol)
break
case "minting":
entryPoint = new MintingEntryPoint(moduleCol)
break
case "mixed":
entryPoint = new MixedEntryPoint(moduleCol)
break
case "staking":
entryPoint = new StakingEntryPoint(moduleCol)
break
case "module":
// not a real entryPoint, but used by the debugger
entryPoint = new ModuleEntryPoint(moduleCol)
break
default:
entryPoint = new GenericEntryPoint(purpose ?? "unknown", moduleCol)
}
// TODO: add type errors directly to ErrorCollector
try {
entryPoint.evalTypes({ errors: errorCollector }, validatorTypes)
} catch (e) {
if (isCompilerError(e)) {
errorCollector.errors.push(e)
} else {
throw e
}
}
return entryPoint
}
/**
* @param {string | Source} mainSrc
* @param {(string | Source)[]} moduleSrcs
* @param {ErrorCollector} errorCollector
* @returns {[string, Module[], Site]}
*/
function parseMain(mainSrc, moduleSrcs, errorCollector) {
let [purpose, modules] = parseMainInternal(mainSrc, errorCollector)
const site = modules[0].name.site
const imports = parseImports(
modules[0].name.value,
moduleSrcs,
errorCollector
)
const mainImports = modules[0].filterDependencies(
{ errors: errorCollector },
imports
)
/**
* @type {Module[]}
*/
let postImports = []
if (modules.length > 1) {
postImports = modules[modules.length - 1]
.filterDependencies({ errors: errorCollector }, imports)
.filter(
(m) => !mainImports.some((d) => d.name.value == m.name.value)
)
}
// create the final order of all the modules (this is the order in which statements will be added to the IR)
modules = mainImports
.concat([modules[0]])
.concat(postImports)
.concat(modules.slice(1))
return [purpose.value, modules, site]
}
/**
* @param {string | Source} rawSrc
* @param {ErrorCollector} errorCollector
* @returns {[purpose, Module[]]}
*/
function parseMainInternal(rawSrc, errorCollector) {
const src = createSource(rawSrc)
const { purpose, name, statements, entryPointIndex } = parseScript(
src,
errorCollector
)
if (purpose && name) {
if (purpose.value == "module") {
return [purpose, [new Module(name, statements, src)]]
} else {
/**
* @type {Module[]}
*/
const modules = [
new MainModule(
name,
statements.slice(0, entryPointIndex + 1),
src
)
]
if (entryPointIndex < statements.length - 1) {
modules.push(
new Module(name, statements.slice(entryPointIndex + 1), src)
)
}
return [purpose, modules]
}
} else {
throw new Error("unexpected") // should've been caught by calling src.throwErrors() above
}
}
/**
* @param {string | Source} rawSrc
* @param {ErrorCollector} errorCollector
* @returns {Module}
*/
function parseModule(rawSrc, errorCollector) {
const src = createSource(rawSrc)
const { name, statements } = parseScript(src, errorCollector)
if (name) {
return new Module(name, statements, src)
} else {
throw new Error("unexpected") // should've been caught by calling src.throwErrors() above
}
}
/**
* @param {string} mainName
* @param {(string | Source)[]} moduleSrcs
* @param {ErrorCollector} errorCollector
* @returns {Module[]}
*/
function parseImports(mainName, moduleSrcs, errorCollector) {
let imports = moduleSrcs.map((src) => {
return parseModule(src, errorCollector)
})
/**
* @type {Set<string>}
*/
let names = new Set()
names.add(mainName)
for (let m of imports) {
if (names.has(m.name.value)) {
errorCollector.syntax(
m.name.site,
`non-unique module name '${m.name.value}'`
)
} else {
names.add(m.name.value)
}
}
return imports
}