diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 4dc22640..3a8b0674 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -20,10 +20,10 @@ jobs: run: cargo build --verbose - if: ${{ github.ref_name == 'main' }} name: Run tests - run: cargo test --verbose -- --test-threads=1 --include-ignored + run: cargo test --verbose -- --include-ignored - if: ${{ github.ref_name != 'main' }} name: Run tests - run: cargo test --verbose -- --test-threads=1 + run: cargo test --verbose test-windows: runs-on: [self-hosted, windows] @@ -33,10 +33,10 @@ jobs: run: cargo build --verbose - if: ${{ github.ref_name == 'main' }} name: Run tests - run: cargo test --verbose -- --test-threads=1 --include-ignored + run: cargo test --verbose -- --include-ignored - if: ${{ github.ref_name != 'main' }} name: Run tests - run: cargo test --verbose -- --test-threads=1 + run: cargo test --verbose test-macos: runs-on: [self-hosted, macOS] @@ -46,10 +46,10 @@ jobs: run: cargo build --verbose - if: ${{ github.ref_name == 'main' }} name: Run tests - run: cargo test --verbose -- --test-threads=1 --include-ignored + run: cargo test --verbose -- --include-ignored - if: ${{ github.ref_name != 'main' }} name: Run tests - run: cargo test --verbose -- --test-threads=1 + run: cargo test --verbose test-arm-linux: runs-on: [self-hosted, linux, ARM64] @@ -59,7 +59,7 @@ jobs: run: cargo build --verbose - if: ${{ github.ref_name == 'main' }} name: Run tests - run: cargo test --verbose -- --test-threads=1 --include-ignored + run: cargo test --verbose -- --include-ignored - if: ${{ github.ref_name != 'main' }} name: Run tests - run: cargo test --verbose -- --test-threads=1 + run: cargo test --verbose diff --git a/README.md b/README.md index 18aed705..42c30405 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,8 @@ This will create a file with the name `` that you can run (or error if i If you wish to contribute to Alan, you'll need a development environment to build Alan locally: * git (any recent version should work) -* Rust >=1.76.0 +* Rust >=1.80.0 +* Node.js >=22.0.0 * A complete C toolchain (gcc, clang, msvc) Once those are installed, simply follow the install instructions above, replacing `cargo install --path .` with a simple `cargo build` to compile and `cargo test` to run the test suite. diff --git a/alan/src/compile/integration_tests.rs b/alan/src/compile/integration_tests.rs index 5118298f..bf5d7ce8 100644 --- a/alan/src/compile/integration_tests.rs +++ b/alan/src/compile/integration_tests.rs @@ -17,6 +17,7 @@ macro_rules! test { mod $rule { #[test] fn $rule() -> Result<(), Box> { + crate::program::Program::set_target_lang_rs(); let filename = format!("{}.ln", stringify!($rule)); match std::fs::write(&filename, $code) { Ok(_) => { /* Do nothing */ } @@ -24,8 +25,10 @@ macro_rules! test { return Err(format!("Unable to write {} to disk. {:?}", filename, e).into()); } }; - std::env::set_var("ALAN_TARGET", "test"); - std::env::set_var("ALAN_OUTPUT_LANG", "rs"); + { + let mut program = crate::program::Program::get_program().lock().unwrap(); + program.env.insert("ALAN_TARGET".to_string(), "test".to_string()); + } match crate::compile::build(filename.to_string()) { Ok(_) => { /* Do nothing */ } Err(e) => { @@ -53,6 +56,7 @@ macro_rules! test_full { mod $rule { #[test] fn $rule() -> Result<(), Box> { + crate::program::Program::set_target_lang_rs(); let filename = format!("{}.ln", stringify!($rule)); match std::fs::write(&filename, $code) { Ok(_) => { /* Do nothing */ } @@ -60,8 +64,10 @@ macro_rules! test_full { return Err(format!("Unable to write {} to disk. {:?}", filename, e).into()); } }; - std::env::set_var("ALAN_TARGET", "test"); - std::env::set_var("ALAN_OUTPUT_LANG", "rs"); + { + let mut program = crate::program::Program::get_program().lock().unwrap(); + program.env.insert("ALAN_TARGET".to_string(), "test".to_string()); + } match crate::compile::build(filename.to_string()) { Ok(_) => { /* Do nothing */ } Err(e) => { @@ -83,7 +89,11 @@ macro_rules! test_full { Ok(a) => Ok(a), Err(e) => Err(format!("Could not remove the test binary {:?}", e)), }?; - std::env::set_var("ALAN_OUTPUT_LANG", "js"); + crate::program::Program::set_target_lang_js(); + { + let mut program = crate::program::Program::get_program().lock().unwrap(); + program.env.insert("ALAN_TARGET".to_string(), "test".to_string()); + } match crate::compile::web(filename.to_string()) { Ok(_) => { /* Do nothing */ } Err(e) => { @@ -117,6 +127,7 @@ macro_rules! test_gpgpu { mod $rule { #[test] fn $rule() -> Result<(), Box> { + crate::program::Program::set_target_lang_rs(); let filename = format!("{}.ln", stringify!($rule)); match std::fs::write(&filename, $code) { Ok(_) => { /* Do nothing */ } @@ -124,8 +135,10 @@ macro_rules! test_gpgpu { return Err(format!("Unable to write {} to disk. {:?}", filename, e).into()); } }; - std::env::set_var("ALAN_TARGET", "test"); - std::env::set_var("ALAN_OUTPUT_LANG", "rs"); + { + let mut program = crate::program::Program::get_program().lock().unwrap(); + program.env.insert("ALAN_TARGET".to_string(), "test".to_string()); + } match crate::compile::build(filename.to_string()) { Ok(_) => { /* Do nothing */ } Err(e) => { @@ -153,8 +166,16 @@ macro_rules! test_gpgpu { // My playwright scripts only work on Linux and MacOS, though, so that reduces it // to just MacOS to test this on. // if cfg!(windows) || cfg!(macos) { - if cfg!(macos) { - std::env::set_var("ALAN_OUTPUT_LANG", "js"); + // TODO: This apparently wasn't working at all because the `macos` cfg keyword was + // deprecated at some point and the new version of Rust finally told me? In any + // case, fixing this will be in a follow-up PR + /* + if cfg!(target_os = "macos") { + crate::program::Program::set_target_lang_js(); + { + let mut program = crate::program::Program::get_program().lock().unwrap(); + program.env.insert("ALAN_TARGET".to_string(), "test".to_string()); + } match crate::compile::web(filename.to_string()) { Ok(_) => { /* Do nothing */ } Err(e) => { @@ -220,6 +241,7 @@ macro_rules! test_gpgpu { Err(e) => Err(format!("Could not remove the generated HTML file {:?}", e)), }?; } + */ std::fs::remove_file(&filename)?; Ok(()) } @@ -241,8 +263,10 @@ macro_rules! test_ignore { return Err(format!("Unable to write {} to disk. {:?}", filename, e).into()); } }; - std::env::set_var("ALAN_TARGET", "test"); - std::env::set_var("ALAN_OUTPUT_LANG", "rs"); + { + let mut program = crate::program::Program::get_program().lock().unwrap(); + program.env.insert("ALAN_TARGET".to_string(), "test".to_string()); + } match crate::compile::build(filename.to_string()) { Ok(_) => { /* Do nothing */ } Err(e) => { diff --git a/alan/src/compile/mod.rs b/alan/src/compile/mod.rs index 8e52220f..ba8b51c9 100644 --- a/alan/src/compile/mod.rs +++ b/alan/src/compile/mod.rs @@ -1,4 +1,4 @@ -use std::env::{current_dir, set_var, var}; +use std::env::current_dir; use std::fs::{create_dir_all, remove_file, write, File}; use std::io::Read; use std::path::PathBuf; @@ -10,6 +10,7 @@ use fs2::FileExt; use crate::lntojs::lntojs; use crate::lntors::lntors; +use crate::program::Program; mod integration_tests; @@ -377,10 +378,13 @@ edition = "2021" /// mode and exits, printing the time it took to run on success. pub fn compile(source_file: String) -> Result<(), Box> { let start_time = Instant::now(); - if var("ALAN_TARGET").is_err() { - set_var("ALAN_TARGET", "release"); + Program::set_target_lang_rs(); + { + let mut program = Program::get_program().lock().unwrap(); + program + .env + .insert("ALAN_TARGET".to_string(), "release".to_string()); } - set_var("ALAN_OUTPUT_LANG", "rs"); build(source_file)?; println!("Done! Took {:.2}sec", start_time.elapsed().as_secs_f32()); Ok(()) @@ -389,8 +393,13 @@ pub fn compile(source_file: String) -> Result<(), Box> { /// The `test` function is a thin wrapper on top of `compile` that compiles the specified file in /// test mode, then immediately invokes it, and deletes the binary when done. pub fn test(source_file: String) -> Result<(), Box> { - set_var("ALAN_TARGET", "test"); - set_var("ALAN_OUTPUT_LANG", "rs"); + Program::set_target_lang_rs(); + { + let mut program = Program::get_program().lock().unwrap(); + program + .env + .insert("ALAN_TARGET".to_string(), "test".to_string()); + } let binary = build(source_file)?; let mut run = Command::new(format!("./{}", binary)) .current_dir(current_dir()?) @@ -685,10 +694,13 @@ pub fn web(source_file: String) -> Result> { /// mode and exits, printing the time it took to run on success. pub fn bundle(source_file: String) -> Result<(), Box> { let start_time = Instant::now(); - if var("ALAN_TARGET").is_err() { - set_var("ALAN_TARGET", "release"); + Program::set_target_lang_js(); + { + let mut program = Program::get_program().lock().unwrap(); + program + .env + .insert("ALAN_TARGET".to_string(), "release".to_string()); } - set_var("ALAN_OUTPUT_LANG", "js"); web(source_file)?; println!("Done! Took {:.2}sec", start_time.elapsed().as_secs_f32()); Ok(()) @@ -697,10 +709,13 @@ pub fn bundle(source_file: String) -> Result<(), Box> { /// The `to_rs` function is an thin wrapper on top of `lntors` that shoves the output into a `.rs` /// file. pub fn to_rs(source_file: String) -> Result<(), Box> { - if var("ALAN_TARGET").is_err() { - set_var("ALAN_TARGET", "release"); + Program::set_target_lang_rs(); + { + let mut program = Program::get_program().lock().unwrap(); + program + .env + .insert("ALAN_TARGET".to_string(), "release".to_string()); } - set_var("ALAN_OUTPUT_LANG", "rs"); // Generate the rust code to compile let (rs_str, deps) = lntors(source_file.clone())?; // Shove it into a temp file for rustc @@ -737,10 +752,13 @@ pub fn to_rs(source_file: String) -> Result<(), Box> { /// The `to_js` function is an thin wrapper on top of `lntojs` that shoves the output into a `.js` /// file. pub fn to_js(source_file: String) -> Result<(), Box> { - if var("ALAN_TARGET").is_err() { - set_var("ALAN_TARGET", "release"); + Program::set_target_lang_js(); + { + let mut program = Program::get_program().lock().unwrap(); + program + .env + .insert("ALAN_TARGET".to_string(), "release".to_string()); } - set_var("ALAN_OUTPUT_LANG", "js"); // Generate the rust code to compile let (js_str, deps) = lntojs(source_file.clone())?; // Shove it into a temp file for rustc diff --git a/alan/src/lntojs/mod.rs b/alan/src/lntojs/mod.rs index 2043b4db..fa65e9a8 100644 --- a/alan/src/lntojs/mod.rs +++ b/alan/src/lntojs/mod.rs @@ -9,14 +9,10 @@ mod typen; pub fn lntojs( entry_file: String, ) -> Result<(String, OrderedHashMap), Box> { - let program = Program::new(entry_file)?; - // Getting the entry scope, where the `main` function is expected - 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()); - } - }; + Program::set_target_lang_js(); + Program::load(entry_file.clone())?; + let program = Program::get_program().lock().unwrap(); + let scope = program.scope_by_file(&entry_file)?; // 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") { diff --git a/alan/src/lntors/mod.rs b/alan/src/lntors/mod.rs index 66631425..caebca30 100644 --- a/alan/src/lntors/mod.rs +++ b/alan/src/lntors/mod.rs @@ -9,14 +9,10 @@ mod typen; pub fn lntors( entry_file: String, ) -> Result<(String, OrderedHashMap), Box> { - let program = Program::new(entry_file)?; - // Getting the entry scope, where the `main` function is expected - 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()); - } - }; + Program::set_target_lang_rs(); + Program::load(entry_file.clone())?; + let program = Program::get_program().lock().unwrap(); + let scope = program.scope_by_file(&entry_file)?; // 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") { diff --git a/alan/src/main.rs b/alan/src/main.rs index cdc84ae4..f817be48 100644 --- a/alan/src/main.rs +++ b/alan/src/main.rs @@ -1,5 +1,4 @@ use crate::compile::{bundle, compile, test, to_js, to_rs}; -use crate::program::Program; use clap::{Parser, Subcommand}; mod compile; @@ -78,9 +77,8 @@ enum Commands { fn main() -> Result<(), Box> { let args = Cli::parse(); - if let Some(file) = args.file { - let program = Program::new(file)?; - println!("{:?}", program); + if args.file.is_some() { + println!("TODO: Interpreter mode someday"); Ok(()) } else { match &args.commands { diff --git a/alan/src/program/ctype.rs b/alan/src/program/ctype.rs index 711584ed..a0d137be 100644 --- a/alan/src/program/ctype.rs +++ b/alan/src/program/ctype.rs @@ -6,6 +6,7 @@ use super::Export; use super::FnKind; use super::Function; use super::Microstatement; +use super::Program; use super::Scope; use super::TypeOperatorMapping; use crate::parse; @@ -80,7 +81,8 @@ pub enum CType { impl CType { // TODO: Find a better way to handle these primitive types pub fn i64() -> CType { - match std::env::var("ALAN_OUTPUT_LANG").unwrap().as_str() { + let program = Program::get_program().lock().unwrap(); + match program.env.get("ALAN_OUTPUT_LANG").unwrap().as_str() { "rs" => CType::Type( "i64".to_string(), Box::new(CType::Binds( @@ -110,7 +112,8 @@ impl CType { } } pub fn f64() -> CType { - match std::env::var("ALAN_OUTPUT_LANG").unwrap().as_str() { + let program = Program::get_program().lock().unwrap(); + match program.env.get("ALAN_OUTPUT_LANG").unwrap().as_str() { "rs" => CType::Type( "f64".to_string(), Box::new(CType::Binds( @@ -140,7 +143,8 @@ impl CType { } } pub fn bool() -> CType { - match std::env::var("ALAN_OUTPUT_LANG").unwrap().as_str() { + let program = Program::get_program().lock().unwrap(); + match program.env.get("ALAN_OUTPUT_LANG").unwrap().as_str() { "rs" => CType::Type( "bool".to_string(), Box::new(CType::Binds( @@ -170,7 +174,8 @@ impl CType { } } pub fn string() -> CType { - match std::env::var("ALAN_OUTPUT_LANG").unwrap().as_str() { + let program = Program::get_program().lock().unwrap(); + match program.env.get("ALAN_OUTPUT_LANG").unwrap().as_str() { "rs" => CType::Type( "string".to_string(), Box::new(CType::Binds( @@ -3241,29 +3246,20 @@ impl CType { } } pub fn env(k: &CType) -> CType { + let program = Program::get_program().lock().unwrap(); match k { - CType::TString(s) => match std::env::var(s) { - Err(e) => CType::fail(&format!( - "Failed to load environment variable {}: {:?}\nAll current envvars:\n{}", - s, - e, - std::env::vars() - .map(|(k, v)| format!("{}: {}", k, v)) - .collect::>() - .join("\n") - )), - Ok(s) => CType::TString(s.clone()), + CType::TString(s) => match program.env.get(s) { + None => CType::fail(&format!("Failed to load environment variable {}", s,)), + Some(s) => CType::TString(s.clone()), }, CType::Infer(..) => CType::Env(vec![k.clone()]), _ => CType::fail("Env{K} must be given a key as a string to load"), } } pub fn envexists(k: &CType) -> CType { + let program = Program::get_program().lock().unwrap(); match k { - CType::TString(s) => match std::env::var(s) { - Err(_) => CType::Bool(false), - Ok(_) => CType::Bool(true), - }, + CType::TString(s) => CType::Bool(program.env.contains_key(s)), CType::Infer(..) => CType::EnvExists(Box::new(k.clone())), _ => CType::fail("EnvExists{K} must be given a key as a string to check"), } @@ -3427,10 +3423,11 @@ impl CType { } } pub fn envdefault(k: &CType, d: &CType) -> CType { + let program = Program::get_program().lock().unwrap(); match (k, d) { - (CType::TString(s), CType::TString(def)) => match std::env::var(s) { - Err(_) => CType::TString(def.clone()), - Ok(v) => CType::TString(v), + (CType::TString(s), CType::TString(def)) => match program.env.get(s) { + None => CType::TString(def.clone()), + Some(v) => CType::TString(v.clone()), }, (CType::Infer(..), CType::TString(_)) | (CType::TString(_), CType::Infer(..)) diff --git a/alan/src/program/program.rs b/alan/src/program/program.rs index 35c049cd..61f1b9d4 100644 --- a/alan/src/program/program.rs +++ b/alan/src/program/program.rs @@ -1,5 +1,7 @@ +use std::cell::Cell; use std::fs::read_to_string; use std::pin::Pin; +use std::sync::{LazyLock, Mutex}; use super::Scope; use crate::parse; @@ -11,30 +13,42 @@ use ordered_hash_map::OrderedHashMap; // this might just be "good enough" assuming non-insane source file sizes. #[derive(Debug)] pub struct Program<'a> { - pub entry_file: String, #[allow(clippy::box_collection)] pub scopes_by_file: OrderedHashMap>, parse::Ln, Scope<'a>)>, + pub env: OrderedHashMap, } -impl<'a> Program<'a> { - pub fn new(entry_file: String) -> Result, Box> { - let mut p = Program { - entry_file: entry_file.clone(), - scopes_by_file: OrderedHashMap::new(), - }; - // Load the entry file - match p.load(entry_file) { - Ok(p) => p, - Err(e) => { - // Somehow, trying to print this error can crash Rust!? Really not good. - // Will need to figure out how to make these errors clearer to users. - return Err(format!("{}", e).into()); +pub static PROGRAM_RS: LazyLock>> = LazyLock::new(|| { + Mutex::new(Program { + scopes_by_file: OrderedHashMap::new(), + env: { + let mut env = OrderedHashMap::new(); + for (k, v) in std::env::vars() { + env.insert(k.to_string(), v.to_string()); } - }; - Ok(p) - } + env.insert("ALAN_OUTPUT_LANG".to_string(), "rs".to_string()); + env + }, + }) +}); +pub static PROGRAM_JS: LazyLock>> = LazyLock::new(|| { + Mutex::new(Program { + scopes_by_file: OrderedHashMap::new(), + env: { + let mut env = OrderedHashMap::new(); + for (k, v) in std::env::vars() { + env.insert(k.to_string(), v.to_string()); + } + env.insert("ALAN_OUTPUT_LANG".to_string(), "js".to_string()); + env + }, + }) +}); - pub fn load(&mut self, path: String) -> Result<(), Box> { +thread_local!(static TARGET_LANG_RS: Cell = const { Cell::new(true) }); + +impl<'a> Program<'a> { + pub fn load(path: String) -> Result<(), Box> { let ln_src = if path.starts_with('@') { match path.as_str() { //"@std/app" => include_str!("../std/app.ln").to_string(), @@ -45,6 +59,33 @@ impl<'a> Program<'a> { } else { read_to_string(&path)? }; - Scope::from_src(self, &path, ln_src) + Scope::from_src(&path, ln_src) + } + + pub fn scope_by_file(&self, path: &str) -> Result<&Scope<'a>, Box> { + match self.scopes_by_file.get(path) { + Some((_, _, s)) => Ok(s), + None => Err(format!("Could not find a scope for file {}", path).into()), + } + } + + pub fn get_program<'b>() -> &'b LazyLock>> { + if TARGET_LANG_RS.get() { + &PROGRAM_RS + } else { + &PROGRAM_JS + } + } + + pub fn set_target_lang_js() { + TARGET_LANG_RS.set(false); + } + + pub fn set_target_lang_rs() { + TARGET_LANG_RS.set(true); + } + + pub fn is_target_lang_rs() -> bool { + TARGET_LANG_RS.get() } } diff --git a/alan/src/program/scope.rs b/alan/src/program/scope.rs index e85c85ca..7e467eef 100644 --- a/alan/src/program/scope.rs +++ b/alan/src/program/scope.rs @@ -129,17 +129,13 @@ impl<'a> Scope<'a> { }; Scope::load_scope(s, ast, true).expect("Invalid root scope definition") }; - if matches!(std::env::var("ALAN_OUTPUT_LANG"), Ok(v) if v == "rs") { + if Program::is_target_lang_rs() { ROOT_SCOPE_RS.get_or_init(resolver) } else { ROOT_SCOPE_JS.get_or_init(resolver) } } - pub fn from_src( - program: &'a mut Program, - path: &str, - src: String, - ) -> Result<(), Box> { + pub fn from_src(path: &str, src: String) -> Result<(), Box> { let txt = Box::pin(src); let txt_ptr: *const str = &**txt; // *How* would this move, anyways? But TODO: See if there's a way to handle this safely @@ -155,6 +151,7 @@ impl<'a> Scope<'a> { exports: OrderedHashMap::new(), }; s = Scope::load_scope(s, &ast, false)?; + let mut program = Program::get_program().lock().unwrap(); program .scopes_by_file .insert(path.to_string(), (txt, ast, s));