Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ env:
# Toolchain release used to run the test suite. Pinned rather than tracking
# `latest` so a failed release (which publishes an asset-less tag) cannot
# turn every PR red. Bump after a release publishes its artifacts.
FUSE_TOOLCHAIN_VERSION: v0.3.4
FUSE_TOOLCHAIN_VERSION: v0.4.1

jobs:
format:
Expand Down
3 changes: 0 additions & 3 deletions examples/list.fuse
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ impl List[A]:
Cons(h, t) => List::fold_left(t, f(acc, h), f)
Nil => acc

fun append[A](l1: List[A], l2: List[A]) -> List[A]
List::fold_right(l1, l2, (h, t) => Cons(h, t))

fun sum(l: List[i32]) -> i32
List::fold_right(l, 0, (acc, b) => acc + b)

Expand Down
8 changes: 6 additions & 2 deletions grin/runtime.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ extern int64_t _heap_ptr_;
int g_argc = 0;
char** g_argv = NULL;

/* Compiled Fuse programs guarantee `grinMain` yields the process exit code as
a T_Int64: `main() -> i32` returns its own value, every other `main` return
type is wrapped by the code generator so the result is discarded and 0 is
returned in its place. */
int64_t grinMain();

void __runtime_error(int64_t c){
Expand All @@ -28,11 +32,11 @@ int main(int argc, char** argv) {
_heap_ptr_ = (int64_t)heap;
#endif

grinMain();
int64_t exit_code = grinMain();

#ifndef USE_BOEHM_GC
free(heap);
#endif

return 0;
return (int)exit_code;
}
91 changes: 61 additions & 30 deletions src/main/scala/Fuse.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.monovore.decline.effect.CommandIOApp
import core.Context.Error

import java.io.{
ByteArrayOutputStream,
File,
FileInputStream,
FileOutputStream,
Expand Down Expand Up @@ -42,10 +43,11 @@ object Fuse
version = "0.4.0" // x-release-please-version
) {

// File extensions (public for test access)
// File extensions (public for test access). A built program is an
// extensionless executable named after its source, as `cc -o`/`rustc`
// produce; only the intermediate GRIN carries an extension of its own.
val FuseFileExtension = "fuse"
val FuseGrinExtension = "grin"
val FuseOutputExtension = "out"

/** Paths for build artifacts derived from source file. */
case class BuildPaths(
Expand All @@ -56,11 +58,11 @@ object Fuse

object BuildPaths {
def fromSource(sourcePath: String): BuildPaths = {
val base = sourcePath.stripSuffix(FuseFileExtension)
val base = sourcePath.stripSuffix(s".$FuseFileExtension")
BuildPaths(
source = Paths.get(sourcePath),
grin = Paths.get(base + FuseGrinExtension),
output = Paths.get(base + FuseOutputExtension)
grin = Paths.get(s"$base.$FuseGrinExtension"),
output = Paths.get(base)
)
}
}
Expand Down Expand Up @@ -127,8 +129,13 @@ object Fuse
}

/** Shared compile-then-execute pipeline. Builds the Fuse source to a native
* binary, runs the binary via the supplied executor, and removes the `.grin`
* and `.out` intermediates regardless of executor outcome.
* binary, runs the binary via the supplied executor, and removes both the
* `.grin` intermediate and the binary regardless of executor outcome.
*
* The executor receives an absolute path: a source given without a directory
* component builds to a bare program name, which a spawned process resolves
* against `PATH` rather than the working directory, so the binary that was
* just built would not be found.
*/
def runFile[A](
file: String,
Expand All @@ -144,7 +151,7 @@ object Fuse
cleanupIntermediateFiles(List(paths.grin))
)
result <- EitherT.right[BuildError](
executor(paths.output, args)
executor(paths.output.toAbsolutePath, args)
.guarantee(cleanupIntermediateFiles(List(paths.output)))
)
} yield result
Expand Down Expand Up @@ -180,7 +187,11 @@ object Fuse
paths: BuildPaths
): EitherT[IO, BuildError, Unit] =
EitherT(
compileFile(command, paths.source.toFile, paths.grin.toFile).map {
compileFile(
command,
paths.source.toFile,
IO(new FileOutputStream(paths.grin.toFile))
).map {
case Some(error) => Left(FuseCompileError(error))
case None => Right(())
}
Expand Down Expand Up @@ -216,32 +227,36 @@ object Fuse
IO.blocking(Files.deleteIfExists(path)).void
}

/** Type check command. */
def check(command: CheckFile): IO[ExitCode] = {
val program = new File(command.file)
val output = new File(
command.file.stripSuffix(FuseFileExtension) + FuseOutputExtension
)
compileFile(command, program, output).flatMap {
case Some(error) => IO.println(error).as(ExitCode.Error)
case None => IO.pure(ExitCode.Success)
/** Type check command. The type representation goes to stdout so it can be
* read or piped; checking a file leaves no artifact behind.
*/
def check(command: CheckFile): IO[ExitCode] =
IO(new ByteArrayOutputStream()).flatMap { representation =>
compileFile(command, new File(command.file), IO.pure(representation))
.flatMap {
case Some(error) => IO.println(error).as(ExitCode.Error)
case None =>
new String(representation.toByteArray).trim match {
case "" => IO.pure(ExitCode.Success)
case types => IO.println(types).as(ExitCode.Success)
}
}
}
}

/** Compile a Fuse file using bracket for resource safety. */
/** Compile a Fuse file using bracket for resource safety. The destination is
* acquired rather than passed open, so a source that fails validation leaves
* no half-created output behind.
*/
def compileFile(
command: Command,
origin: File,
destination: File
destination: IO[OutputStream]
): IO[Option[Error]] =
validateSourceFile(origin).flatMap {
case Some(error) => IO.pure(Some(error))
case None =>
val acquireStreams: IO[(InputStream, OutputStream)] =
(
IO(new FileInputStream(origin)),
IO(new FileOutputStream(destination))
).tupled
(IO(new FileInputStream(origin)), destination).tupled
val releaseStreams: ((InputStream, OutputStream)) => IO[Unit] = {
case (in, out) =>
(IO(in.close()), IO(out.close())).tupled
Expand All @@ -264,30 +279,46 @@ object Fuse
}
}

/** Validate that a source file exists, is a regular file, and is readable. */
/** Validate that a source file exists, is a regular file, is readable, and
* carries the `.fuse` extension — the built executable is the source name
* with that extension removed, so a source without it would be overwritten
* by its own build output.
*/
def validateSourceFile(file: File): IO[Option[Error]] = IO {
(file.exists, file.isFile, file.canRead) match {
case (false, _, _) =>
(
file.exists,
file.isFile,
file.canRead,
file.getName.endsWith(s".$FuseFileExtension")
) match {
case (false, _, _, _) =>
Some(
Utils.consoleError(
s"file not found: ${file.getPath}",
UnknownInfo
)
)
case (_, false, _) =>
case (_, false, _, _) =>
Some(
Utils.consoleError(
s"not a file: ${file.getPath}",
UnknownInfo
)
)
case (_, _, false) =>
case (_, _, false, _) =>
Some(
Utils.consoleError(
s"file not readable: ${file.getPath}",
UnknownInfo
)
)
case (_, _, _, false) =>
Some(
Utils.consoleError(
s"not a .$FuseFileExtension source file: ${file.getPath}",
UnknownInfo
)
)
case _ => None
}
}
Expand Down
65 changes: 64 additions & 1 deletion src/main/scala/code/Grin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import fuse.SpecializedMethodUtils

object Grin {
val MainFunction = "grinMain"
// Holds a `main` whose result is not an exit code; see `withExitCodeEntry`.
val WrappedMainFunction = "_fuse_main"

/** Checks if a De Bruijn-indexed variable is referenced in a term. Used to
* detect dead let-bindings for unused closures.
Expand Down Expand Up @@ -108,7 +110,8 @@ object Grin {
(lambdaBindings, partialFunctions) = values.flatten.unzip
applyFunction <- buildApply(partialFunctions.flatten)
} yield {
val raw = (lambdaBindings.flatten.map(_.show) :+ applyFunction)
val entry = withExitCodeEntry(lambdaBindings.flatten, bindings)
val raw = (entry.map(_.show) :+ applyFunction)
.mkString("\n\n")
.replaceAll(
"([a-zA-Z0-9])#",
Expand All @@ -124,6 +127,66 @@ object Grin {
s.runEmptyA.value
}

/** The C runtime uses `grinMain`'s result as the process exit status, so the
* entry point has to yield a `T_Int64`. A `main() -> i32` already does and
* is emitted untouched — its value becomes the exit code.
*
* Every other `main` return type would hand the runtime whatever
* representation it happens to have — a heap pointer, a float, or, for unit,
* no value at all. Those are compiled under `WrappedMainFunction` and called
* from a generated `grinMain` that binds the result — keeping the call, and
* with it the program's effects, alive through GRIN's dead-code passes — and
* returns 0 in its place.
*/
def withExitCodeEntry(
lambdaBindings: List[LambdaBinding],
bindings: List[Bind]
): List[LambdaBinding] =
(
mainReturnsExitCode(bindings),
lambdaBindings.exists(_.name == MainFunction)
) match {
case (false, true) =>
lambdaBindings.map {
case LambdaBinding(MainFunction, e) =>
LambdaBinding(WrappedMainFunction, e)
case b => b
} :+ exitCodeEntry
case _ => lambdaBindings
}

/** True when `main` returns `i32`, the one Fuse type whose GRIN
* representation is the bare `T_Int64` the runtime can exit with.
*/
def mainReturnsExitCode(bindings: List[Bind]): Boolean =
bindings.find(_.i == TypeChecker.MainFunction).map(_.b) match {
case Some(TermAbbBind(_, Some(TypeArrow(_, _, _: TypeInt)))) => true
case Some(TermAbbBind(TermAbs(_, _, _, _, Some(_: TypeInt)), _)) => true
case _ => false
}

/** `grinMain` for a `main` that does not itself produce an exit code. The
* unit argument mirrors the one the code generator gives every nullary Fuse
* function.
*/
def exitCodeEntry: LambdaBinding =
LambdaBinding(
MainFunction,
Abs(
s"${WrappedMainFunction}_arg",
MultiLineExpr(
List(
BindExpr(
s"${WrappedMainFunction}_res <- $WrappedMainFunction 0",
s"${WrappedMainFunction}_res",
Nil
)
),
Value("0")
)
)
)

// Strips `[` and `]` from TypeApp identifier names without touching the
// bytes inside GRIN string literals of the form `#"..."`. The unguarded
// global `replaceAll("[\\[\\]]", "")` previously deleted brackets from
Expand Down
Loading
Loading