Skip to content

Commit

Permalink
Alan to Rust transpiler code (#656)
Browse files Browse the repository at this point in the history
* First step in making a more real transpiler: establish a subdirectory for all of the soon-to-be source files

* Run cargo fmt, add support for exported conts, types, and functions, and switch to 'export fn main' as the entrypoint instead of 'on start'

* Add logic to properly load the standard library from embedded strings in the compiler (and prove it by parsing the root scope on evey compilation, slowing the test suite down a bit)

* Move the function generation to a separate file

* Move the old root scope to an unused filename and create a new root scope with a single entry for now, then implement very basic type and function resolution and use it to generate the hello world app

* Fix formatting

* Update TODOs in the resolve functions to make it clearer which way they're going to evolve

* Add super simple 'to-rs' sub-command to get the generated Rust code as a file. Good for debugging, at least.

* Modify the scope to use a vector of function implementations per function name, to allow for multiple function implementations with different arguments, and then select the function by the arguments. Also update the root scope to specify the arguments.

* Fix fmt

* Update the license copyright

* Rename 'exit' to 'return' because that's what it actually is

* Microstatements are back, baby!

* Remove no longer relevant TODO comment

* Fix fmt
  • Loading branch information
dfellis authored Feb 22, 2024
1 parent 9944334 commit 1e6a843
Show file tree
Hide file tree
Showing 11 changed files with 3,913 additions and 3,563 deletions.
1 change: 1 addition & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Copyright (c) 2020-2023 Alan Technologies, Inc
Copyright (c) 2024 Alan Language Contributors

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
Expand Down
27 changes: 21 additions & 6 deletions src/compile.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// TODO: Figure out how to integrate `rustc` into the `alan` binary.
use std::fs::{remove_file, write};
use std::path::PathBuf;
use std::process::{Command, Stdio};
Expand Down Expand Up @@ -31,6 +30,22 @@ pub fn compile(source_file: String) -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}

/// The `tors` function is an even thinner wrapper on top of `lntors` that shoves the output into a
/// `.rs` file.
pub fn to_rs(source_file: String) -> Result<(), Box<dyn std::error::Error>> {
// Generate the rust code to compile
let rs_str = lntors(source_file.clone())?;
// Shove it into a temp file for rustc
let out_file = match PathBuf::from(source_file).file_stem() {
Some(pb) => format!("{}.rs", pb.to_string_lossy().to_string()),
None => {
return Err("Invalid path".into());
}
};
write(&out_file, rs_str)?;
Ok(())
}

/// The majority of this file is dedicated to a comprehensive test suite, converted from the prior
/// test suite using macros to make it a bit more dense than it would have been otherwise.
/// The macro here is composed of three parts: The test program source, the expected exit code
Expand Down Expand Up @@ -68,26 +83,26 @@ macro_rules! stdout {
( $test_val:expr, $real_val:expr ) => {
let std_out = String::from_utf8($real_val.stdout.clone())?;
assert_eq!($test_val, &std_out);
}
};
}
#[cfg(test)]
macro_rules! stderr {
( $test_val:expr, $real_val:expr ) => {
let std_err = String::from_utf8($real_val.stderr.clone())?;
assert_eq!($test_val, &std_err);
}
};
}
#[cfg(test)]
macro_rules! status {
( $test_val:expr, $real_val:expr ) => {
let status = $real_val.status.code().unwrap();
assert_eq!($test_val, status);
}
};
}

// The only test that works for now
test!(hello_world => r#"
on start {
export fn main {
print("Hello, World!");
}"#;
stdout "Hello, World!\n";
Expand Down Expand Up @@ -3688,7 +3703,7 @@ Describe "Module imports"
sourceToAll "
from @std/app import start, print, exit
// Intentionally put an extra space after the import
from ./piece import Piece
from ./piece import Piece
on start {
const piece = new Piece {
Expand Down
128 changes: 0 additions & 128 deletions src/lntors.rs

This file was deleted.

95 changes: 95 additions & 0 deletions src/lntors/function.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Builds a function and everything it needs, recursively. Given a read-only handle on it's own
// scope and the program in case it needs to generate required text from somewhere else.

use crate::lntors::typen;
use crate::program::{Function, Microstatement, Program, Scope};

pub fn from_microstatement(
microstatement: &Microstatement,
scope: &Scope,
program: &Program,
) -> Result<String, Box<dyn std::error::Error>> {
match microstatement {
Microstatement::Assignment { name, value } => Ok(format!(
"let {} = {}",
name,
from_microstatement(value, scope, program)?
)
.to_string()),
Microstatement::Value { representation, .. } => Ok(representation.clone()),
Microstatement::FnCall { function, args } => {
// TODO: Add logic to get the type from the args array of microstatements. For the sake
// of keeping the hello world test working for now, adding some magic knowledge that
// should not be hardcoded until the microstatement generation adds the type
// information needed.
match program.resolve_function(scope, function, &vec!["String".to_string()]) {
None => Err(format!("Function {} not found", function).into()),
Some((f, _s)) => match &f.bind {
None => Err("Inlining user-defined functions not yet supported".into()),
Some(rustname) => {
let mut argstrs = Vec::new();
for arg in args {
let a = from_microstatement(arg, scope, program)?;
argstrs.push(a);
}
Ok(format!("{}({})", rustname, argstrs.join(", ")).to_string())
}
},
}
}
}
}

pub fn generate(
function: &Function,
scope: &Scope,
program: &Program,
) -> Result<String, Box<dyn std::error::Error>> {
let mut out = "".to_string();
// First make sure all of the function argument types are defined
for arg in &function.args {
match program.resolve_type(scope, &arg.1) {
None => continue,
Some((t, s)) => {
out = format!("{}{}", out, typen::generate(t, s, program)?);
}
}
}
match &function.rettype {
Some(rettype) => match program.resolve_type(scope, rettype) {
None => {}
Some((t, s)) => {
out = format!("{}{}", out, typen::generate(t, s, program)?);
}
},
None => {}
}
// Start generating the function output. We can do this eagerly like this because, at least for
// now, we inline all other function calls within an "entry" function (the main function, or
// any function that's attached to an event, or any function that's part of an exported set in
// a shared library). LLVM *probably* doesn't deduplicate this redundancy, so this will need to
// be revisited, but it eliminates a whole host of generation problems that I can come back to
// later.
out = format!(
"{}fn {}({}){} {{\n",
out,
function.name.clone(),
function
.args
.iter()
.map(|(argname, argtype)| format!("{}: {}", argname, argtype).to_string()) // TODO: Don't assume Rust and Alan types exactly match syntax
.collect::<Vec<String>>()
.join(", "),
match &function.rettype {
Some(rettype) => format!(" -> {}", rettype).to_string(),
None => "".to_string(),
},
)
.to_string();
for microstatement in &function.microstatements {
let stmt = from_microstatement(microstatement, scope, program)?;
out = format!("{} {};\n", out, stmt);
}
out = format!("{}}}", out);
Ok(out)
}
43 changes: 43 additions & 0 deletions src/lntors/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use crate::lntors::function::generate;
use crate::program::Program;

mod function;
mod typen;

pub fn lntors(entry_file: String) -> Result<String, Box<dyn std::error::Error>> {
let program = Program::new(entry_file)?;
// Assuming a single scope for now
let scope = match program.scopes_by_file.get(&program.entry_file.clone()) {
Some((_, _, s)) => s,
None => {
return Err("Somehow didn't find a scope for the entry file!?".into());
}
};
// Without support for building shared libs yet, assume there is an `export fn main` in the
// entry file or fail otherwise
match scope.exports.get("main") {
Some(_) => {}
None => {
return Err(
"Entry file has no `main` function exported. This is not yet supported.".into(),
);
}
};
// Getting here *should* guarantee that the `main` function exists, so let's grab it.
let func = match scope.functions.get("main") {
Some(f) => f,
None => {
return Err(
"An export has been found without a definition. This should be impossible.".into(),
);
}
};
// The `main` function takes no arguments, for now. It could have a return type, but we don't
// support that, yet. Also assert that there is only a single `main` function, since *usually*
// you're allowed to have multiple functions with the same name as long as they have different
// arguments.
assert_eq!(func.len(), 1);
assert_eq!(func[0].args.len(), 0);
// Assertion proven, start emitting the Rust `main` function
Ok(generate(&func[0], &scope, &program)?)
}
11 changes: 11 additions & 0 deletions src/lntors/typen.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// TODO: Everything in here

use crate::program::{Program, Scope, Type};

pub fn generate(
typen: &Type,
scope: &Scope,
program: &Program,
) -> Result<String, Box<dyn std::error::Error>> {
Ok("".to_string())
}
12 changes: 11 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use clap::{Parser, Subcommand};
use compile::{compile, to_rs};
use program::Program;
use compile::compile;

mod compile;
mod lntors;
Expand Down Expand Up @@ -28,6 +28,15 @@ enum Commands {
)]
file: String,
},
#[command(about = "Compile .ln file(s) to Rust")]
ToRs {
#[arg(
value_name = "LN_FILE",
help = ".ln source file to transpile to Rust.",
default_value = "./index.ln"
)]
file: String,
},
#[command(about = "Install dependencies for your Alan project")]
Install {
#[arg(
Expand All @@ -48,6 +57,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} else {
match &args.commands {
Some(Commands::Compile { file }) => Ok(compile(file.to_string())?),
Some(Commands::ToRs { file }) => Ok(to_rs(file.to_string())?),
_ => Err("Command not yet supported".into()),
}
}
Expand Down
Loading

0 comments on commit 1e6a843

Please sign in to comment.