diff --git a/examples/single-page/script/generate_test_file b/examples/single-page/script/generate_test_file index 337e569..57c9a5c 100755 --- a/examples/single-page/script/generate_test_file +++ b/examples/single-page/script/generate_test_file @@ -1,4 +1,4 @@ #!/usr/bin/env bash # generate a 10MB test file -< /dev/urandom tr -dc "[:alnum:]" | head -c10000000 > file.txt +< /dev/urandom tr -dc "[:alnum:]" | head -c10000000 > input.txt diff --git a/examples/single-page/src/Main.elm b/examples/single-page/src/Main.elm index d038c27..c495827 100644 --- a/examples/single-page/src/Main.elm +++ b/examples/single-page/src/Main.elm @@ -1,4 +1,4 @@ -module Main exposing (view, view2, view3, badReturnType) +module Main exposing (view, view2, view3, view4, view5, badReturnType, true) import Html exposing (Html, div, text) import Svg exposing (Svg, svg) @@ -36,6 +36,24 @@ view3 model = Nothing -> text "Failed to decode" +view4 : String -> Html msg +view4 model = + if model == "bar" then + div [] + [ text <| "Hello world" ++ model ] + else + div [] + [ text "Hello world" ] + +view5 : { t | x : String } -> Html msg +view5 { x } = + div [] + [ text x ] + badReturnType : String -> Int badReturnType _ = 42 + +true : Bool +true = + True diff --git a/src/elm.rs b/src/elm.rs index 693960c..0e4861d 100644 --- a/src/elm.rs +++ b/src/elm.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use std::fs::{self, canonicalize, metadata}; use std::io::{self, BufRead, Error, ErrorKind, Write}; use std::path::{Path, PathBuf}; -use std::process::{Command}; +use std::process::Command; use tracing::info_span; pub fn make(file: &Path, debug: bool, verbosity: u64) -> Result<(), Problem> { @@ -39,7 +39,9 @@ pub fn make(file: &Path, debug: bool, verbosity: u64) -> Result<(), Problem> { Ok(()) } -pub fn load_interfaces(elm_cache_dir: &Path) -> Result, Problem> { +pub fn load_interfaces( + elm_cache_dir: &Path, +) -> Result, Problem> { let mut interfaces = HashMap::new(); let entries = fs::read_dir(&elm_cache_dir) @@ -51,6 +53,27 @@ pub fn load_interfaces(elm_cache_dir: &Path) -> Result { + let data = std::fs::read(&path) + .map_err(|io_err| CompilerError::ReadInputFailed(io_err, path.clone()))?; + + let (_remaining, i) = ::get(&data) + .map_err(|_err| { + CompilerError::FailedElmiParse("todo elmi parsing".to_owned()) + })?; + + for (module_name, dep_interface) in i.into_iter() { + match dep_interface { + elmi::DependencyInterface::Public(interface) => { + interfaces.insert(module_name, interface); + } + _ => {} + } + } + } + _ => {} + } match path.extension() { Some(ext) if ext == "elmi" => { // step 4 load all the modules @@ -70,12 +93,16 @@ pub fn load_interfaces(elm_cache_dir: &Path) -> Result>() .join("."); - interfaces.insert(module_name, i); + let cannonical_module = elmi::ModuleNameCanonical { + package: elmi::PackageName::new("author", "project"), + module: elmi::Name(module), + }; + interfaces.insert(cannonical_module, i); } } _ => (), @@ -103,13 +130,14 @@ pub fn load_objects(elm_cache_dir: &Path) -> Result {}, + _ => {} } match path.extension() { Some(ext) if ext == "elmo" => { @@ -123,9 +151,10 @@ pub fn load_objects(elm_cache_dir: &Path) -> Result match interface.values.get(&elmi::Name::from(&function)) { + let (input_type, output_type) = match interfaces.get(&entrypoint.0) { + Some(interface) => match interface.values.get(&entrypoint.1) { Some(annotation) => { let elmi::CannonicalAnnotation(_free_vars, tipe) = annotation; resolve_function_type(tipe)? } - None => return Err(CompilerError::BadImport(target_module, function).into()), + None => return Err(CompilerError::BadImport(entrypoint).into()), }, - None => return Err(CompilerError::MissingModuleTypeInformation(target_module).into()), + None => return Err(CompilerError::MissingModuleTypeInformation(entrypoint.0).into()), }; drop(timing_guard); @@ -144,8 +150,8 @@ fn exec( let timing_guard = span.enter(); let (gen_module_name, source) = fixture::generate( source_checksum, - target_module, - function, + entrypoint.0.module.clone(), + entrypoint.1.clone(), input_type, output_type, ); diff --git a/src/reporting.rs b/src/reporting.rs index 1e1be96..3a62f5f 100644 --- a/src/reporting.rs +++ b/src/reporting.rs @@ -1,3 +1,4 @@ +use crate::reporting::doc::reflow; use deno_core::error::AnyError; use elm_project_utils::RedoScriptError; use elmi; @@ -89,8 +90,8 @@ pub enum CompilerError { MissingElmStuff(PathBuf), CantParseModule(String), // first line EmptyModule, - MissingModuleTypeInformation(String), - BadImport(String, String), + MissingModuleTypeInformation(elmi::ModuleNameCanonical), + BadImport(elmi::Global), FailedBuildingFixture, ReadInputFailed(io::Error, PathBuf), WriteOutputFailed(io::Error, PathBuf), @@ -147,9 +148,9 @@ impl CompilerError { CantParseModule(_first_line_prefix) => Doc::text("todo could not parse module"), EmptyModule => Doc::text("I did not expect the module file to be empty"), MissingModuleTypeInformation(_) => Doc::text("todo missing module type information"), - BadImport(module, symbol) => { + BadImport(elmi::Global(module, symbol)) => { // TODO suggest alternatives using edit distance - Doc::text(format!( + reflow(format!( "The `{}` module does not expose `{}`", module, symbol )) diff --git a/src/transpile.rs b/src/transpile.rs index 10a4b40..7531b9c 100644 --- a/src/transpile.rs +++ b/src/transpile.rs @@ -1,9 +1,11 @@ -use crate::reporting::{CompilerError, Problem, TypeError}; -use elmi::DataBinary; -use std::collections::HashMap; -use std::path::{PathBuf}; -use tracing::info_span; use crate::elm; +use crate::reporting::{CompilerError, Problem, TypeError}; +use genco::lang::rust; +use genco::tokens::quoted; +use genco::{quote, quote_in}; +use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; +use tracing::info_span; pub fn transpile( file: PathBuf, @@ -36,22 +38,32 @@ pub fn transpile( // used in the project. So I would want to reduce the tuples of file function into a set of // files to load. Then recursively load all dependencies. + let entrypoint = elmi::Global( + elmi::ModuleNameCanonical { + package: elmi::PackageName::new("author", "project"), + module: elmi::Name(target_module.clone()), + }, + elmi::Name(function.clone()), + ); + // step 3 find all the filepaths in the elm-stuff/0.19.1/* folder let interfaces = elm::load_interfaces(&elm_cache_dir)?; // Step 4, check for the desired functions have types that we can compile. let span = info_span!("resolved target function"); let timing_guard = span.enter(); - match interfaces.get(&target_module) { + let signature = match interfaces.get(&entrypoint.0) { Some(interface) => match interface.values.get(&elmi::Name::from(&function)) { Some(annotation) => { - let elmi::CannonicalAnnotation(_free_vars, tipe) = annotation; + let elmi::CannonicalAnnotation(_free_vars, ref tipe) = annotation; - validate_function_type(tipe)? + validate_function_type(tipe)?; + + annotation } - None => return Err(CompilerError::BadImport(target_module, function).into()), + None => return Err(CompilerError::BadImport(entrypoint).into()), }, - None => return Err(CompilerError::MissingModuleTypeInformation(target_module).into()), + None => return Err(CompilerError::MissingModuleTypeInformation(entrypoint.0).into()), }; drop(timing_guard); @@ -63,74 +75,98 @@ pub fn transpile( let objects = elm::load_objects(&elm_cache_dir)?; - let entrypoint = elmi::Global( - elmi::ModuleNameCanonical { - package: elmi::PackageName::new("author", "project"), - module: elmi::Name(target_module.clone()), - }, - elmi::Name(function.clone()), - ); + if let Some(node) = objects.get(&entrypoint) { + match node { + elmi::Node::Define(elmi::Expr::Function(ref parameters, ref body), deps) => { + for dep in deps { + println!("I depend on {}", dep); + } + let elmi::CannonicalAnnotation(elmi::FreeVars(free_variables), tipe) = signature; - println!("the artifacts has the symbol {}", objects.contains_key(&entrypoint)); + let (parameter_types, return_type) = + extract_function_types(&tipe, parameters.len()).unwrap(); + + let xs = parameters + .into_iter() + .zip(parameter_types.into_iter()) + .collect::>(); + + // TODO add any TLambdas in the signature to the type parameters of the functions + // as where bounds + + let mut tokens = rust::Tokens::new(); + codegen_function( + &mut tokens, + &entrypoint.1, + &free_variables, + &xs, + return_type, + body, + ); + println!("{}", tokens.to_file_string().unwrap()); + } + _ => println!("I don't know how to transpile that node"), + } + } + println!("total symbols {}", objects.len()); //let visited: HashSet = HashSet::new(); - for (_key, node) in objects.iter() { + for (key, node) in objects.iter() { //println!("key {}", key); - match node { - elmi::Node::Define(_expr, deps) => { - for dep in deps.iter() { - if !objects.contains_key(&dep) { + match node { + elmi::Node::Define(expr, deps) => { + //println!("key => {:?}", expr); + for dep in deps.iter() { + if !objects.contains_key(&dep) { println!("could not find dep {}", dep); } - } - } - elmi::Node::DefineTailFunc(_, _, deps) => { - for dep in deps.iter() { - if !objects.contains_key(&dep) { + } + break; + } + elmi::Node::DefineTailFunc(_, _, deps) => { + for dep in deps.iter() { + if !objects.contains_key(&dep) { println!("could not find dep {}", dep); } - } - } - elmi::Node::Ctor(_, _) => { - } - elmi::Node::Enum(_) => { - } - elmi::Node::Box => { - } - elmi::Node::Link(dep) => { + } + } + elmi::Node::Ctor(_, _) => {} + elmi::Node::Enum(_) => {} + elmi::Node::Box => {} + elmi::Node::Link(dep) => { if !objects.contains_key(&dep) { println!("could not find dep {}", dep); } - } - elmi::Node::Cycle(_, _, _, deps) => { - for dep in deps.iter() { + } + elmi::Node::Cycle(_, _, _, deps) => { + for dep in deps.iter() { if !objects.contains_key(&dep) { println!("could not find dep {}", dep); } - } - } - elmi::Node::Manager(_) => (), - elmi::Node::Kernel(_, deps) => { - for dep in deps.iter() { + } + } + elmi::Node::Manager(_) => (), + elmi::Node::Kernel(_, deps) => { + for dep in deps.iter() { if !objects.contains_key(&dep) { println!("could not find dep {}", dep); } - } - } - elmi::Node::PortIncoming(_, deps) => { - for dep in deps.iter() { + } + } + elmi::Node::PortIncoming(_, deps) => { + for dep in deps.iter() { if !objects.contains_key(&dep) { println!("could not find dep {}", dep); } - } - } - elmi::Node::PortOutgoing(_, deps) => { - for dep in deps.iter() { + } + } + elmi::Node::PortOutgoing(_, deps) => { + for dep in deps.iter() { if !objects.contains_key(&dep) { println!("could not find dep {}", dep); } - } - } - } + } + } + } } let _symbol_table: HashMap = HashMap::new(); @@ -217,6 +253,7 @@ fn validate_input_type(tipe: &elmi::Type) -> Result<(), TypeError> { elmi::AliasType::Filled(tipe) => validate_input_type(tipe), elmi::AliasType::Holey(_) => Err(TypeError::CantEvalHoleyAlias), }, + elmi::Type::TRecord(_, _) => Ok(()), _ => Err(TypeError::OutputTypeNotSupported(tipe.clone())), } } @@ -244,6 +281,219 @@ fn validate_output_type(tipe: &elmi::Type) -> Result<(), TypeError> { } } +fn codegen_function( + tokens: &mut rust::Tokens, + name: &elmi::Name, + type_variables: &HashSet, + parameters: &[(&elmi::Name, elmi::Type)], + return_type: elmi::Type, + body: &elmi::Expr, +) { + quote_in! { *tokens => + fn #(&name.0)#(if !type_variables.is_empty() => + <#(for elmi::Name(ref tvar) in type_variables.iter() join (, ) => + #tvar + )> + )(#(for (elmi::Name(ref parameter), tipe) in parameters.iter() join (, ) => + #parameter: #(ref out { codegen_type(out, tipe) }) + )) -> #(ref out { codegen_type(out, &return_type) }) { + #(ref out { codegen_expr(out, body) }) + } + } +} + +fn codegen_type(tokens: &mut rust::Tokens, tipe: &elmi::Type) { + quote_in! { *tokens => + #(match tipe { + elmi::Type::TLambda(a, b) => { + ( #(ref out => codegen_type(out, a) ) -> #(ref out => codegen_type(out, b) ) ) + } + elmi::Type::TVar(elmi::Name(variable)) => { + #variable + }, + elmi::Type::TType(module_name, name, args) if module_name == "elm/core/String" && name == "String" && args.is_empty() => { + String + } + elmi::Type::TType(home, name, args) if args.is_empty() => { + #(ref out => codegen_name_from_global(out, home, name)) + } + elmi::Type::TType(home, name, args) => { + #(ref out => codegen_name_from_global(out, home, name))<#(for arg in args join(, ) => + #(ref out => codegen_type(out, arg)) + )> + } + // // Might be a primitive type + // #(if module_name == "elm/core/String" && name == "String" => String) + // #(if module_name == "elm/core/Basics" && name == "Int" => i64) + // #(if module_name == "elm/core/Basics" && name == "Float" => f64) + // #(if module_name == "elm/core/Basics" && name == "Bool" => bool) + // #(if module_name == "elm/core/Maybe" && name == "Maybe" => Option) + // #(if module_name == "elm/bytes/Bytes" && name == "Bytes" => Vec) + //} + //elmi::Type::TType(_, _, _) => Err(TypeError::CantEvalCustomType), + //elmi::Type::TRecord(_, _) => Err(TypeError::CantEvalRecord), + elmi::Type::TUnit => (), + _ => { + println!("failed to solve code {:?}", tipe); + todo_tipe + }, + //elmi::Type::TTuple(_, _, _) => Err(TypeError::CantEvalTuple), + //elmi::Type::TAlias(_, _, _, ref alias) => { + // match &**alias { + // elmi::AliasType::Filled(tipe) => { + // // I think the recursion is limited to a single step. I have not tested what + // // the CannonicalAnnotation would look like for a doubly indirect alias, for + // // example for `view` below + // // ```elm + // // type alias Foo = Int + // // type alias Bar = String + // // + // // type alias Zap = Foo -> Bar + // // + // // view : Zap + // // ``` + // validate_function_type(tipe) + // } + // elmi::AliasType::Holey(_) => return Err(TypeError::CantEvalHoleyAlias), + // } + //} + }) + }; +} + +fn codegen_name_from_global( + tokens: &mut rust::Tokens, + home: &elmi::ModuleNameCanonical, + name: &elmi::Name, +) { + quote_in! { *tokens => + #(ref out => codegen_home_to_builder(out, home) )__#(&name.0) + } +} + +fn codegen_home_to_builder(tokens: &mut rust::Tokens, global: &elmi::ModuleNameCanonical) { + let elmi::ModuleNameCanonical { + package: elmi::PackageName { author, project }, + module: home, + } = global; + + quote_in! { *tokens => + _#(author.replace("-", "_"))_#(project.replace("-", "_"))__#(home.0.replace(".", "_")) + } +} + +fn codegen_expr(tokens: &mut rust::Tokens, expr: &elmi::Expr) { + match expr { + elmi::Expr::Bool(true) => quote_in! { *tokens => true }, + elmi::Expr::Bool(false) => quote_in! { *tokens => false }, + elmi::Expr::Chr(c) => quote_in! { *tokens => #("'")#c#("'") }, + elmi::Expr::Str(s) => quote_in! { *tokens => #(quoted(s)) }, + elmi::Expr::Int(x) => quote_in! { *tokens => #(x.to_string()) }, + elmi::Expr::Float(x) => quote_in! { *tokens => #(x.to_string()) }, + elmi::Expr::VarLocal(elmi::Name(ref name)) => { + quote_in! { *tokens => + #name + } + } + elmi::Expr::VarGlobal(elmi::Global(home, name)) => { + quote_in! { *tokens => + #(ref out => codegen_name_from_global(out, home, name)) + } + } + //elmi::Expr::VarEnum(Global, IndexZeroBased), + //elmi::Expr::VarBox(Global), + //elmi::Expr::VarCycle(ModuleNameCanonical, Name), + //elmi::Expr::VarDebug(Name, ModuleNameCanonical, AnnotationRegion, Option), + //elmi::Expr::VarKernel(Name, Name), + elmi::Expr::List(xs) => { + if xs.is_empty() { + quote_in! { *tokens => &[] } + } else { + quote_in! { *tokens => + &[ + #(for x in xs join (,#) => #(ref out => codegen_expr(out, x) ) ) + ] + } + } + } + elmi::Expr::Function(parameters, body) => { + quote_in! { *tokens => + "i don't know how to code gen a function expression" + //#(for elmi::Name(ref parameter) in parameters.iter() join (, ) => + //) + } + } + elmi::Expr::Call(ref fexpr, args) => { + quote_in! { *tokens => + #(match &**fexpr { + elmi::Expr::VarGlobal(elmi::Global(home, name)) => { + #(ref out => codegen_name_from_global(out, home, name)) + } + _ => { + "unknown" + } + })( + #(for arg in args join (,#) => #(ref out => + codegen_expr(out, arg) ) + ) + ) + } + } + //elmi::Expr::TailCall(Name, Vec<(Name, Expr)>), + //elmi::Expr::If(Vec<(Expr, Expr)>, Box), + //elmi::Expr::Let(Def, Box), + //elmi::Expr::Destruct(Destructor, Box), + //elmi::Expr::Case(Name, Name, Decider, Vec<(i64, Expr)>), + //elmi::Expr::Accessor(Name), + //elmi::Expr::Access(Box, Name), + //elmi::Expr::Update(Box, HashMap), + //elmi::Expr::Record(HashMap), + elmi::Expr::Unit => (), + elmi::Expr::Tuple(a, b, None) => { + quote_in! { *tokens => + ( #(ref out => codegen_expr(out, a) ), #(ref out => codegen_expr(out, b) ) ) + } + } + elmi::Expr::Tuple(a, b, Some(c)) => { + quote_in! { *tokens => + ( #(ref out => codegen_expr(out, a) ), #(ref out => codegen_expr(out, b) ), #(ref out => codegen_expr(out, c) ) ) + } + } + //elmi::Expr::Shader(ShaderSource, HashSet, HashSet), + _ => quote_in! { *tokens => #(format!("{:?}", expr)) }, + } +} + +fn extract_function_types( + mut tipe: &elmi::Type, + mut nargs: usize, +) -> Option<(Vec, elmi::Type)> { + let mut parameters = Vec::with_capacity(nargs); + loop { + if nargs == 0 { + return Some((parameters, tipe.clone())); + } + match tipe { + elmi::Type::TLambda(a, b) => { + parameters.push(reduce_alias_types(&*a).clone()); + tipe = reduce_alias_types(&*b); + nargs -= 1; + } + _ => return None, + } + } +} + +fn reduce_alias_types(a: &elmi::Type) -> &elmi::Type { + match a { + elmi::Type::TAlias(_, _, _, ref alias) => match &**alias { + elmi::AliasType::Filled(b) => &b, + elmi::AliasType::Holey(_) => a, + }, + _ => a, + } +} + // Figure out how to do structural types. If I could name mangle all the functions I could write // them out in the same namespace as lambdas which would avoid the structural typing problem if the // lambda was used by one type. Monomorphism. But if the lambda is used by multiple types then I