Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A94 Amber Docs generator #62

Merged
merged 14 commits into from
Jul 19, 2024
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
# Test files
test*.ab
test*.sh
test*.md
/docs/

# Flamegraph files
flamegraph.svg
Expand All @@ -12,4 +14,4 @@ flamegraph.svg

# Nixos
.direnv
result
/result
18 changes: 13 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Along the way, you may need help with your code. The best way to ask is in [our
Amber consists of the following layers:

1. [CLI Interface](#1-cli-interface)
2. [Compiler](#2-compiler)
2. [Compiler](#2-compiler)
1. [Parser & tokenizer](#21-parser--tokenizer)
2. [Translator](#22-translator)
2. [Built-in](#23-built-in-creation)
Expand Down Expand Up @@ -97,7 +97,7 @@ fn translate() -> String {

Basically, the `translate()` method should return a `String` for the compiler to construct a compiled file from all of them. If it translates to nothing, you should output an empty string, like `String::new()`

#### 2.3. Built-in creation
#### 2.3. Built-in creation

In this guide we will see how to create a basic built-in function that in Amber syntax presents like:
```sh
Expand Down Expand Up @@ -125,6 +125,8 @@ use crate::translate::module::TranslateModule;
// - `ParserMetadata` - it carries the necessary information about the current parsing context such as variables and functions that were declared up to this point, warning messages aggregated up to this point, information whether this syntax is declared in a loop, function, main block, unsafe scope etc.
// `TranslateMetadata` - it carries the necessary information for translation such as wether we are in a silent scope, in an eval context or what indentation should be used.
use crate::utils::{ParserMetadata, TranslateMetadata};
// Documentation module tells compiler what markdown content should it generate for this syntax module. This is irrelevent to our simple module so we will just return empty string.
use crate::docs::module::DocumentationModule;

// This is a declaration of your built-in. Set the name accordingly.
#[derive(Debug, Clone)]
Expand Down Expand Up @@ -169,6 +171,13 @@ impl TranslateModule for Builtin {
format!("echo {}", value)
}
}

// Here we implement what should documentation generation render (in markdown format) when encounters this syntax module. Since this is just a simple built-in that does not need to be documented, we simply return an empty String.
impl DocumentationModule for Expr {
fn document(&self, _meta: &ParserMetadata) -> String {
String::new()
}
}
```

Now let's import it in the main module for built-ins `src/modules/builtin/mod.rs`
Expand Down Expand Up @@ -199,7 +208,7 @@ impl Statement {
Builtin,
// ...
}

// ...
}
```
Expand All @@ -213,7 +222,7 @@ impl Statement {
### 4. Tests
Amber uses `cargo test` for tests. `stdlib` and `validity` tests usually work by executing amber code and checking its output.

We have [`validity tests`](src/tests/validity.rs) to check if the compiler outputs a valid bash code, [`stdlib tests`](src/tests/stdlib.rs) and [`CLI tests`](src/tests/cli.rs).
We have [`validity tests`](src/tests/validity.rs) to check if the compiler outputs a valid bash code, [`stdlib tests`](src/tests/stdlib.rs) and [`CLI tests`](src/tests/cli.rs).

The majority of `stdlib` tests are Written in pure Amber in the folder [`tests/stdlib`](src/tests/stdlib). For every test there is a `*.output.txt` file that contains the expected output.
Tests will be executed without recompilation. Amber will load the scripts and verify the output in the designated file to determine if the test passes.
Expand All @@ -240,4 +249,3 @@ To run ALL tests, run `cargo test`.
If you want to run only tests from a specific file, let's say from [`stdlib.rs`](src/tests/stdlib.rs), you add the file name to the command: `cargo test stdlib`

And if there is a specific function, like `test_function()` in `stdlib.rs`, you should add the full path to it: `cargo test stdlib::test_function`

86 changes: 76 additions & 10 deletions src/compiler.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
extern crate chrono;
use chrono::prelude::*;
use crate::docs::module::DocumentationModule;
use itertools::Itertools;
use crate::modules::block::Block;
use crate::rules;
use crate::translate::check_all_blocks;
use crate::translate::module::TranslateModule;
use crate::utils::{ParserMetadata, TranslateMetadata};
use std::fs::File;
use std::io::Write;
use colored::Colorize;
use heraclitus_compiler::prelude::*;
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::{Command, ExitStatus};
use std::time::Instant;

Expand Down Expand Up @@ -81,9 +86,10 @@ impl AmberCompiler {
}
}

pub fn parse(&self, tokens: Vec<Token>) -> Result<(Block, ParserMetadata), Message> {
pub fn parse(&self, tokens: Vec<Token>, is_docs_gen: bool) -> Result<(Block, ParserMetadata), Message> {
let code = self.cc.code.as_ref().expect(NO_CODE_PROVIDED).clone();
let mut meta = ParserMetadata::new(tokens, self.path.clone(), Some(code));
meta.is_docs_gen = is_docs_gen;
if let Err(Failure::Loud(err)) = check_all_blocks(&meta) {
return Err(err);
}
Expand All @@ -110,22 +116,32 @@ impl AmberCompiler {
}
}

pub fn translate(&self, block: Block, meta: ParserMetadata) -> String {
pub fn get_sorted_ast_forest(&self, block: Block, meta: &ParserMetadata) -> Vec<(String, Block)> {
let imports_sorted = meta.import_cache.topological_sort();
let imports_blocks = meta
.import_cache
.files
.iter()
.map(|file| file.metadata.as_ref().map(|meta| meta.block.clone()))
.collect::<Vec<Option<Block>>>();
let mut meta = TranslateMetadata::new(meta);
.map(|file| file.metadata.as_ref().map(|meta| (file.path.clone(), meta.block.clone())))
.collect::<Vec<Option<(String, Block)>>>();
let mut result = vec![];
let time = Instant::now();
for index in imports_sorted.iter() {
if let Some(block) = imports_blocks[*index].clone() {
result.push(block.translate(&mut meta));
if let Some((path, block)) = imports_blocks[*index].clone() {
result.push((path, block));
}
}
result.push((self.path.clone().unwrap_or(String::from("unknown")), block));
result
}

pub fn translate(&self, block: Block, meta: ParserMetadata) -> String {
let ast_forest = self.get_sorted_ast_forest(block, &meta);
let mut meta_translate = TranslateMetadata::new(meta);
let time = Instant::now();
let mut result = vec![];
for (_path, block) in ast_forest {
result.push(block.translate(&mut meta_translate));
}
if Self::env_flag_set(AMBER_DEBUG_TIME) {
let pathname = self.path.clone().unwrap_or(String::from("unknown"));
println!(
Expand All @@ -134,7 +150,6 @@ impl AmberCompiler {
time.elapsed().as_millis()
);
}
result.push(block.translate(&mut meta));
let res = result.join("\n");
let header = [
include_str!("header.sh"),
Expand All @@ -144,9 +159,55 @@ impl AmberCompiler {
format!("{}\n{}", header, res)
}

pub fn document(&self, block: Block, meta: ParserMetadata, output: String) {
let base_path = PathBuf::from(meta.get_path().expect("Input file must exist in docs generation"));
let base_dir = fs::canonicalize(base_path)
.map(|val| val.parent().expect("Parent dir must exist in docs generation").to_owned().clone());
if let Err(err) = base_dir {
Message::new_err_msg("Couldn't get the absolute path to the provided input file")
.comment(err.to_string())
.show();
std::process::exit(1);
}
let base_dir = base_dir.unwrap();
let ast_forest = self.get_sorted_ast_forest(block, &meta);
for (path, block) in ast_forest {
let dep_path = {
let dep_path = fs::canonicalize(PathBuf::from(path.clone()));
if dep_path.is_err() {
continue
}
let dep_path = dep_path.unwrap();

if !dep_path.starts_with(&base_dir) {
continue
}

dep_path
};
let document = block.document(&meta);
// Save to file
let dir_path = {
let file_dir = dep_path.strip_prefix(&base_dir).unwrap();
let parent = file_dir.parent().unwrap().display();
format!("{}/{output}/{}", base_dir.to_string_lossy(), parent)
};
if let Err(err) = fs::create_dir_all(dir_path.clone()) {
Message::new_err_msg(format!("Couldn't create directory `{dir_path}`. Do you have sufficient permissions?"))
.comment(err.to_string())
.show();
std::process::exit(1);
}
let filename = dep_path.file_stem().unwrap().to_string_lossy();
let path = format!("{dir_path}/{filename}.md");
let mut file = File::create(path).unwrap();
file.write_all(document.as_bytes()).unwrap();
}
}

pub fn compile(&self) -> Result<(Vec<Message>, String), Message> {
self.tokenize()
.and_then(|tokens| self.parse(tokens))
.and_then(|tokens| self.parse(tokens, false))
.map(|(block, meta)| (meta.messages.clone(), self.translate(block, meta)))
}

Expand All @@ -160,6 +221,11 @@ impl AmberCompiler {
.wait()?)
}

pub fn generate_docs(&self, output: String) -> Result<(), Message> {
self.tokenize().and_then(|tokens| self.parse(tokens, true))
.map(|(block, meta)| self.document(block, meta, output))
}

#[allow(dead_code)]
pub fn test_eval(&mut self) -> Result<String, Message> {
self.compile().map_or_else(Err, |(_, code)| {
Expand Down
1 change: 1 addition & 0 deletions src/docs/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod module;
5 changes: 5 additions & 0 deletions src/docs/module.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use crate::utils::ParserMetadata;

pub trait DocumentationModule {
fn document(&self, meta: &ParserMetadata) -> String;
}
77 changes: 60 additions & 17 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod compiler;
mod modules;
mod rules;
mod translate;
mod docs;
mod utils;
mod stdlib;

Expand All @@ -27,28 +28,27 @@ struct Cli {
/// Code to evaluate
#[arg(short, long)]
eval: Option<String>,

/// Generate docs
#[arg(long)]
docs: bool
}

fn main() -> Result<(), Box<dyn Error>> {
let cli = Cli::parse();
if cli.docs {
handle_docs(cli)?;
} else if let Some(code) = cli.eval {
handle_eval(code)?;
} else {
handle_compile(cli)?;
}
Ok(())
}

if let Some(code) = cli.eval {
let code = format!("import * from \"std\"\n{code}");
match AmberCompiler::new(code, None).compile() {
Ok((messages, code)) => {
messages.iter().for_each(|m| m.show());
(!messages.is_empty()).then(|| render_dash());
let exit_status = AmberCompiler::execute(code, &vec![])?;
std::process::exit(exit_status.code().unwrap_or(1));
}
Err(err) => {
err.show();
std::process::exit(1);
}
}
} else if let Some(input) = cli.input {
fn handle_compile(cli: Cli) -> Result<(), Box<dyn Error>> {
if let Some(input) = cli.input {
let input = String::from(input.to_string_lossy());

match fs::read_to_string(&input) {
Ok(code) => {
match AmberCompiler::new(code, Some(input)).compile() {
Expand Down Expand Up @@ -88,11 +88,54 @@ fn main() -> Result<(), Box<dyn Error>> {
std::process::exit(1);
}
}
} else {
}
Ok(())
}

fn handle_eval(code: String) -> Result<(), Box<dyn Error>> {
let code = format!("import * from \"std\"\n{code}");
match AmberCompiler::new(code, None).compile() {
Ok((messages, code)) => {
messages.iter().for_each(|m| m.show());
(!messages.is_empty()).then(|| render_dash());
let exit_status = AmberCompiler::execute(code, &vec![])?;
std::process::exit(exit_status.code().unwrap_or(1));
}
Err(err) => {
err.show();
std::process::exit(1);
}
}
}

fn handle_docs(cli: Cli) -> Result<(), Box<dyn Error>> {
if let Some(input) = cli.input {
let input = String::from(input.to_string_lossy());
let output = {
let out = cli.output.unwrap_or_else(|| PathBuf::from("docs"));
String::from(out.to_string_lossy())
};
match fs::read_to_string(&input) {
Ok(code) => {
match AmberCompiler::new(code, Some(input)).generate_docs(output) {
Ok(_) => Ok(()),
Err(err) => {
err.show();
std::process::exit(1);
}
}
},
Err(err) => {
Message::new_err_msg(err.to_string()).show();
std::process::exit(1);
}
}
} else {
Message::new_err_msg("You need to provide a path to an entry file to generate the documentation").show();
std::process::exit(1);
}
}

#[cfg(target_os = "windows")]
fn set_file_permission(_file: &fs::File, _output: String) {
// We don't need to set permission on Windows
Expand Down
13 changes: 11 additions & 2 deletions src/modules/block.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use std::collections::VecDeque;

use heraclitus_compiler::prelude::*;
use crate::{utils::{metadata::ParserMetadata, TranslateMetadata}};
use crate::docs::module::DocumentationModule;
use crate::utils::{metadata::ParserMetadata, TranslateMetadata};
use crate::translate::module::TranslateModule;
use super::statement::stmt::Statement;

Expand Down Expand Up @@ -40,7 +41,7 @@ impl SyntaxModule<ParserMetadata> for Block {
continue;
}
// Handle comments
if token.word.starts_with("//") {
if token.word.starts_with("//") && !token.word.starts_with("///") {
meta.increment_index();
continue
}
Expand Down Expand Up @@ -83,3 +84,11 @@ impl TranslateModule for Block {
result
}
}

impl DocumentationModule for Block {
fn document(&self, meta: &ParserMetadata) -> String {
self.statements.iter()
.map(|statement| statement.document(meta))
.collect::<Vec<_>>().join("")
}
}
7 changes: 7 additions & 0 deletions src/modules/builtin/echo.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use heraclitus_compiler::prelude::*;
use crate::docs::module::DocumentationModule;
Ph0enixKM marked this conversation as resolved.
Show resolved Hide resolved
use crate::modules::expression::expr::Expr;
use crate::translate::module::TranslateModule;
use crate::utils::{ParserMetadata, TranslateMetadata};
Expand Down Expand Up @@ -30,3 +31,9 @@ impl TranslateModule for Echo {
format!("echo {}", value)
}
}

impl DocumentationModule for Echo {
fn document(&self, _meta: &ParserMetadata) -> String {
"".to_string()
}
}
Loading
Loading