<commit_before>use super::{
    ast::{self, Assignment, Ast, AstKind, Identifier, Int, Struct, Symbol, Text},
    cst::{self, CstDb},
    cst_to_ast::CstToAst,
    error::{CompilerError, CompilerErrorPayload},
    hir::{self, Body, Expression, HirError, Lambda},
};
use crate::{builtin_functions, input::Input};
use im::HashMap;
use std::{ops::Range, sync::Arc};

#[salsa::query_group(AstToHirStorage)]
pub trait AstToHir: CstDb + CstToAst {
    fn hir_to_ast_id(&self, id: hir::Id) -> Option<ast::Id>;
    fn hir_to_cst_id(&self, id: hir::Id) -> Option<cst::Id>;
    fn hir_id_to_span(&self, id: hir::Id) -> Option<Range<usize>>;
    fn hir_id_to_display_span(&self, id: hir::Id) -> Option<Range<usize>>;

    fn ast_to_hir_id(&self, id: ast::Id) -> Option<hir::Id>;
    fn cst_to_hir_id(&self, input: Input, id: cst::Id) -> Option<hir::Id>;

    fn hir(&self, input: Input) -> Option<(Arc<Body>, HashMap<hir::Id, ast::Id>)>;
}

fn hir_to_ast_id(db: &dyn AstToHir, id: hir::Id) -> Option<ast::Id> {
    let (_, hir_to_ast_id_mapping) = db.hir(id.input.clone()).unwrap();
    hir_to_ast_id_mapping.get(&id).cloned()
}
fn hir_to_cst_id(db: &dyn AstToHir, id: hir::Id) -> Option<cst::Id> {
    db.ast_to_cst_id(db.hir_to_ast_id(id.clone())?)
}
fn hir_id_to_span(db: &dyn AstToHir, id: hir::Id) -> Option<Range<usize>> {
    db.ast_id_to_span(db.hir_to_ast_id(id.clone())?)
}
fn hir_id_to_display_span(db: &dyn AstToHir, id: hir::Id) -> Option<Range<usize>> {
    let cst_id = db.hir_to_cst_id(id.clone())?;
    Some(db.find_cst(id.input, cst_id).display_span())
}

fn ast_to_hir_id(db: &dyn AstToHir, id: ast::Id) -> Option<hir::Id> {
    let (_, hir_to_ast_id_mapping) = db.hir(id.input.clone()).unwrap();
    hir_to_ast_id_mapping
        .iter()
        .find_map(|(key, value)| if value == &id { Some(key) } else { None })
        .cloned()
}
fn cst_to_hir_id(db: &dyn AstToHir, input: Input, id: cst::Id) -> Option<hir::Id> {
    let id = db.cst_to_ast_id(input, id)?;
    db.ast_to_hir_id(id)
}

fn hir(db: &dyn AstToHir, input: Input) -> Option<(Arc<Body>, HashMap<hir::Id, ast::Id>)> {
    let (ast, _) = db.ast(input.clone())?;

    let mut context = Context {
        db,
        input: input.clone(),
    };
    let mut compiler = Compiler::new(&mut context);
    compiler.compile(&ast);
    Some((Arc::new(compiler.body), compiler.id_mapping))
}

struct Context<'c> {
    db: &'c dyn AstToHir,
    input: Input,
}

struct Compiler<'c> {
    context: &'c Context<'c>,
    id_mapping: HashMap<hir::Id, ast::Id>,
    body: Body,
    parent_keys: Vec<String>,
    identifiers: HashMap<String, hir::Id>,
}
impl<'c> Compiler<'c> {
    fn new(context: &'c Context<'c>) -> Self {
        let builtin_identifiers = builtin_functions::VALUES
            .iter()
            .map(|builtin_function| {
                let string = format!("builtin{:?}", builtin_function);
                (
                    string.clone(),
                    hir::Id::new(context.input.clone(), vec![string]),
                )
            })
            .collect::<HashMap<_, _>>();

        let mut compiler = Compiler {
            context,
            id_mapping: HashMap::new(),
            parent_keys: vec![],
            body: Body::new(),
            identifiers: builtin_identifiers,
        };
        compiler.generate_use();
        compiler
    }

    fn generate_use(&mut self) {
        let mut assignment_inner = Compiler::<'c> {
            context: &mut self.context,
            id_mapping: self.id_mapping.clone(),
            body: Body::new(),
            parent_keys: add_keys(&self.parent_keys, "use".to_string()),
            identifiers: self.identifiers.clone(),
        };

        let lambda_keys = add_keys(&assignment_inner.parent_keys, "0".to_string());
        let lambda_parameter_id = hir::Id::new(
            assignment_inner.context.input.clone(),
            add_keys(&lambda_keys[..], "target".to_string()),
        );
        let mut lambda_inner = Compiler::<'c> {
            context: &mut assignment_inner.context,
            id_mapping: assignment_inner.id_mapping.clone(),
            body: Body::new(),
            parent_keys: lambda_keys.clone(),
            identifiers: assignment_inner.identifiers.clone(),
        };

        let panic_id = lambda_inner.identifiers["builtinPanic"].clone();
        match &lambda_inner.context.input {
            Input::File(path) => {
                let current_path_content = path
                    .iter()
                    .enumerate()
                    .map(|(index, it)| {
                        (
                            lambda_inner
                                .push_without_ast_mapping(Expression::Int(index as u64), None),
                            lambda_inner
                                .push_without_ast_mapping(Expression::Text(it.to_owned()), None),
                        )
                    })
                    .collect();
                let current_path = lambda_inner
                    .push_without_ast_mapping(Expression::Struct(current_path_content), None);
                lambda_inner.push_without_ast_mapping(
                    Expression::Call {
                        function: lambda_inner.identifiers["builtinUse"].clone(),
                        arguments: vec![current_path, lambda_parameter_id.clone()],
                    },
                    None,
                );
            }
            Input::ExternalFile(_) => {
                let message_id = lambda_inner.push_without_ast_mapping(
                    Expression::Text(
                        "File doesn't belong to the currently opened project.".to_string(),
                    ),
                    None,
                );
                lambda_inner.push_without_ast_mapping(
                    Expression::Call {
                        function: panic_id,
                        arguments: vec![message_id],
                    },
                    None,
                );
            }
            Input::Untitled(_) => {
                let message_id = lambda_inner.push_without_ast_mapping(
                    Expression::Text("Untitled files can't call `use`.".to_string()),
                    None,
                );
                lambda_inner.push_without_ast_mapping(
                    Expression::Call {
                        function: panic_id,
                        arguments: vec![message_id],
                    },
                    None,
                );
            }
        }

        assignment_inner.id_mapping = lambda_inner.id_mapping;
        assignment_inner.push_without_ast_mapping(
            Expression::Lambda(Lambda {
                parameters: vec![hir::Id::new(
                    self.context.input.clone(),
                    vec!["use".to_string(), "0".to_string(), "target".to_string()],
                )],
                body: lambda_inner.body,
            }),
            None,
        );

        self.id_mapping = assignment_inner.id_mapping;

        self.push_without_ast_mapping(
            Expression::Body(assignment_inner.body),
            Some("use".to_string()),
        );
    }

    fn compile(&mut self, asts: &[Ast]) {
        if asts.is_empty() {
            self.push_without_ast_mapping(Expression::nothing(), None);
        } else {
            for ast in asts.into_iter() {
                self.compile_single(ast);
            }
        }
    }
    fn compile_single(&mut self, ast: &Ast) -> hir::Id {
        match &ast.kind {
            AstKind::Int(Int(int)) => {
                self.push(ast.id.clone(), Expression::Int(int.to_owned()), None)
            }
            AstKind::Text(Text(string)) => self.push(
                ast.id.clone(),
                Expression::Text(string.value.to_owned()),
                None,
            ),
            AstKind::Identifier(Identifier(symbol)) => {
                let reference = match self.identifiers.get(&symbol.value) {
                    Some(reference) => reference.to_owned(),
                    None => {
                        return self.push(
                            symbol.id.clone(),
                            Expression::Error {
                                child: None,
                                errors: vec![CompilerError {
                                    input: ast.id.input.clone(),
                                    span: self.context.db.ast_id_to_span(ast.id.clone()).unwrap(),
                                    payload: CompilerErrorPayload::Hir(
                                        HirError::UnknownReference {
                                            symbol: symbol.value.clone(),
                                        },
                                    ),
                                }],
                            },
                            None,
                        );
                    }
                };
                self.push(
                    ast.id.clone(),
                    Expression::Reference(reference.to_owned()),
                    None,
                )
            }
            AstKind::Symbol(Symbol(symbol)) => self.push(
                ast.id.clone(),
                Expression::Symbol(symbol.value.to_owned()),
                None,
            ),
            AstKind::Struct(Struct { fields }) => {
                let fields = fields
                    .iter()
                    .map(|(key, value)| (self.compile_single(key), self.compile_single(value)))
                    .collect();
                self.push(ast.id.clone(), Expression::Struct(fields), None)
            }
            AstKind::Lambda(ast::Lambda {
                parameters,
                body: body_asts,
            }) => {
                let mut body = Body::new();
                let lambda_id = self.create_next_id(ast.id.clone(), None);
                let mut identifiers = self.identifiers.clone();

                for parameter in parameters.iter() {
                    let name = parameter.value.to_string();
                    let id = hir::Id::new(
                        self.context.input.clone(),
                        add_keys(&lambda_id.keys, name.clone()),
                    );
                    self.id_mapping.insert(id.clone(), parameter.id.clone());
                    body.identifiers.insert(id.clone(), name.clone());
                    identifiers.insert(name, id);
                }
                let mut inner = Compiler::<'c> {
                    context: &mut self.context,
                    id_mapping: self.id_mapping.clone(),
                    body,
                    parent_keys: lambda_id.keys.clone(),
                    identifiers,
                };

                inner.compile(&body_asts);
                self.id_mapping = inner.id_mapping;
                self.push_with_existing_id(
                    lambda_id.clone(),
                    Expression::Lambda(Lambda {
                        parameters: parameters
                            .iter()
                            .map(|parameter| {
                                hir::Id::new(
                                    self.context.input.clone(),
                                    add_keys(&lambda_id.keys[..], parameter.value.to_string()),
                                )
                            })
                            .collect(),
                        body: inner.body,
                    }),
                    None,
                )
            }
            AstKind::Call(ast::Call { name, arguments }) => {
                let arguments = arguments
                    .iter()
                    .map(|argument| self.compile_single(argument))
                    .collect();

                let function = match self.identifiers.get(&name.value) {
                    Some(function) => function.to_owned(),
                    None => {
                        return self.push(
                            name.id.clone(),
                            Expression::Error {
                                child: None,
                                errors: vec![CompilerError {
                                    input: name.id.input.clone(),
                                    span: self.context.db.ast_id_to_span(name.id.clone()).unwrap(),
                                    payload: CompilerErrorPayload::Hir(HirError::UnknownFunction {
                                        name: name.value.clone(),
                                    }),
                                }],
                            },
                            None,
                        );
                    }
                };
                self.push(
                    ast.id.clone(),
                    Expression::Call {
                        function,
                        arguments,
                    },
                    None,
                )
            }
            AstKind::Assignment(Assignment { name, body }) => {
                let name = name.value.to_owned();
                let mut inner = Compiler::<'c> {
                    context: &mut self.context,
                    id_mapping: self.id_mapping.clone(),
                    body: Body::new(),
                    parent_keys: add_keys(&self.parent_keys, name.clone()),
                    identifiers: self.identifiers.clone(),
                };
                inner.compile(&body);
                self.id_mapping = inner.id_mapping;
                self.push(ast.id.clone(), Expression::Body(inner.body), Some(name))
            }
            AstKind::Error { child, errors } => {
                let child = if let Some(child) = child {
                    Some(self.compile_single(&*child))
                } else {
                    None
                };
                self.push(
                    ast.id.clone(),
                    Expression::Error {
                        child,
                        errors: errors.clone(),
                    },
                    None,
                )
            }
        }
    }

    fn push(
        &mut self,
        ast_id: ast::Id,
        expression: Expression,
        identifier: Option<String>,
    ) -> hir::Id {
        let id = self.create_next_id(ast_id, identifier.clone());
        self.push_with_existing_id(id, expression, identifier)
    }
    fn push_without_ast_mapping(
        &mut self,
        expression: Expression,
        identifier: Option<String>,
    ) -> hir::Id {
        let id = self.create_next_id_without_ast_mapping(identifier.clone());
        self.push_with_existing_id(id, expression, identifier)
    }
    fn push_with_existing_id(
        &mut self,
        id: hir::Id,
        expression: Expression,
        identifier: Option<String>,
    ) -> hir::Id {
        self.body.push(id.to_owned(), expression, None);
        if let Some(identifier) = identifier {
            self.identifiers.insert(identifier, id.clone());
        }
        id
    }

    fn create_next_id(&mut self, ast_id: ast::Id, key: Option<String>) -> hir::Id {
        let id = self.create_next_id_without_ast_mapping(key);
        assert!(self.id_mapping.insert(id.to_owned(), ast_id).is_none());
        id
    }
    fn create_next_id_without_ast_mapping(&mut self, key: Option<String>) -> hir::Id {
        for disambiguator in 0.. {
            let last_part = if let Some(key) = &key {
                let disambiguator = if disambiguator == 0 {
                    "".to_string()
                } else {
                    format!("${}", disambiguator - 1)
                };
                format!("{}{}", key, disambiguator)
            } else {
                format!("{}", disambiguator)
            };
            let id = hir::Id::new(
                self.context.input.clone(),
                add_keys(&self.parent_keys, last_part),
            );
            if !self.id_mapping.contains_key(&id) {
                return id;
            }
        }
        unreachable!()
    }
}

fn add_keys(parents: &[String], id: String) -> Vec<String> {
    parents
        .iter()
        .map(|it| it.to_string())
        .chain(vec![id])
        .collect()
}
<commit_msg>Make HIR IDs use named keys<commit_after>use super::{
    ast::{self, Assignment, Ast, AstKind, Identifier, Int, Struct, Symbol, Text},
    cst::{self, CstDb},
    cst_to_ast::CstToAst,
    error::{CompilerError, CompilerErrorPayload},
    hir::{self, Body, Expression, HirError, Lambda},
};
use crate::{builtin_functions, input::Input};
use im::HashMap;
use std::{ops::Range, sync::Arc};

#[salsa::query_group(AstToHirStorage)]
pub trait AstToHir: CstDb + CstToAst {
    fn hir_to_ast_id(&self, id: hir::Id) -> Option<ast::Id>;
    fn hir_to_cst_id(&self, id: hir::Id) -> Option<cst::Id>;
    fn hir_id_to_span(&self, id: hir::Id) -> Option<Range<usize>>;
    fn hir_id_to_display_span(&self, id: hir::Id) -> Option<Range<usize>>;

    fn ast_to_hir_id(&self, id: ast::Id) -> Option<hir::Id>;
    fn cst_to_hir_id(&self, input: Input, id: cst::Id) -> Option<hir::Id>;

    fn hir(&self, input: Input) -> Option<(Arc<Body>, HashMap<hir::Id, ast::Id>)>;
}

fn hir_to_ast_id(db: &dyn AstToHir, id: hir::Id) -> Option<ast::Id> {
    let (_, hir_to_ast_id_mapping) = db.hir(id.input.clone()).unwrap();
    hir_to_ast_id_mapping.get(&id).cloned()
}
fn hir_to_cst_id(db: &dyn AstToHir, id: hir::Id) -> Option<cst::Id> {
    db.ast_to_cst_id(db.hir_to_ast_id(id.clone())?)
}
fn hir_id_to_span(db: &dyn AstToHir, id: hir::Id) -> Option<Range<usize>> {
    db.ast_id_to_span(db.hir_to_ast_id(id.clone())?)
}
fn hir_id_to_display_span(db: &dyn AstToHir, id: hir::Id) -> Option<Range<usize>> {
    let cst_id = db.hir_to_cst_id(id.clone())?;
    Some(db.find_cst(id.input, cst_id).display_span())
}

fn ast_to_hir_id(db: &dyn AstToHir, id: ast::Id) -> Option<hir::Id> {
    let (_, hir_to_ast_id_mapping) = db.hir(id.input.clone()).unwrap();
    hir_to_ast_id_mapping
        .iter()
        .find_map(|(key, value)| if value == &id { Some(key) } else { None })
        .cloned()
}
fn cst_to_hir_id(db: &dyn AstToHir, input: Input, id: cst::Id) -> Option<hir::Id> {
    let id = db.cst_to_ast_id(input, id)?;
    db.ast_to_hir_id(id)
}

fn hir(db: &dyn AstToHir, input: Input) -> Option<(Arc<Body>, HashMap<hir::Id, ast::Id>)> {
    let (ast, _) = db.ast(input.clone())?;

    let mut context = Context {
        db,
        input: input.clone(),
    };
    let mut compiler = Compiler::new(&mut context);
    compiler.compile(&ast);
    Some((Arc::new(compiler.body), compiler.id_mapping))
}

struct Context<'c> {
    db: &'c dyn AstToHir,
    input: Input,
}

struct Compiler<'c> {
    context: &'c Context<'c>,
    id_mapping: HashMap<hir::Id, ast::Id>,
    body: Body,
    parent_keys: Vec<String>,
    identifiers: HashMap<String, hir::Id>,
}
impl<'c> Compiler<'c> {
    fn new(context: &'c Context<'c>) -> Self {
        let builtin_identifiers = builtin_functions::VALUES
            .iter()
            .map(|builtin_function| {
                let string = format!("builtin{:?}", builtin_function);
                (
                    string.clone(),
                    hir::Id::new(context.input.clone(), vec![string]),
                )
            })
            .collect::<HashMap<_, _>>();

        let mut compiler = Compiler {
            context,
            id_mapping: HashMap::new(),
            parent_keys: vec![],
            body: Body::new(),
            identifiers: builtin_identifiers,
        };
        compiler.generate_use();
        compiler
    }

    fn generate_use(&mut self) {
        // HirId(project-file:test.candy:11) = body {
        //   HirId(project-file:test.candy:11:0) = lambda { target ->
        //     HirId(project-file:test.candy:11:0:1) = int 0
        //     HirId(project-file:test.candy:11:0:2) = text "test.candy"
        //     HirId(project-file:test.candy:11:0:3) = struct [
        //       HirId(project-file:test.candy:11:0:1): HirId(project-file:test.candy:11:0:2),
        //     ]
        //     HirId(project-file:test.candy:11:0:4) = call HirId(project-file:test.candy:10) with these arguments:
        //       HirId(project-file:test.candy:11:0:3)
        //       HirId(project-file:test.candy:11:0:0)
        //   }
        // }
        let mut assignment_inner = Compiler::<'c> {
            context: &mut self.context,
            id_mapping: self.id_mapping.clone(),
            body: Body::new(),
            parent_keys: add_keys(&self.parent_keys, "use".to_string()),
            identifiers: self.identifiers.clone(),
        };

        let lambda_keys = add_keys(&assignment_inner.parent_keys, "0".to_string());
        let lambda_parameter_id = hir::Id::new(
            assignment_inner.context.input.clone(),
            add_keys(&lambda_keys[..], "target".to_string()),
        );
        let mut lambda_inner = Compiler::<'c> {
            context: &mut assignment_inner.context,
            id_mapping: assignment_inner.id_mapping.clone(),
            body: Body::new(),
            parent_keys: lambda_keys.clone(),
            identifiers: assignment_inner.identifiers.clone(),
        };

        let panic_id = lambda_inner.identifiers["builtinPanic"].clone();
        match &lambda_inner.context.input {
            Input::File(path) => {
                let current_path_content = path
                    .iter()
                    .enumerate()
                    .map(|(index, it)| {
                        (
                            lambda_inner.push_without_ast_mapping(
                                Expression::Int(index as u64),
                                Some("key".to_string()),
                            ),
                            lambda_inner.push_without_ast_mapping(
                                Expression::Text(it.to_owned()),
                                Some("value".to_string()),
                            ),
                        )
                    })
                    .collect();
                let current_path = lambda_inner.push_without_ast_mapping(
                    Expression::Struct(current_path_content),
                    Some("currentPath".to_string()),
                );
                lambda_inner.push_without_ast_mapping(
                    Expression::Call {
                        function: lambda_inner.identifiers["builtinUse"].clone(),
                        arguments: vec![current_path, lambda_parameter_id.clone()],
                    },
                    Some("module".to_string()),
                );
            }
            Input::ExternalFile(_) => {
                let message_id = lambda_inner.push_without_ast_mapping(
                    Expression::Text(
                        "File doesn't belong to the currently opened project.".to_string(),
                    ),
                    Some("message".to_string()),
                );
                lambda_inner.push_without_ast_mapping(
                    Expression::Call {
                        function: panic_id,
                        arguments: vec![message_id],
                    },
                    Some("panicked".to_string()),
                );
            }
            Input::Untitled(_) => {
                let message_id = lambda_inner.push_without_ast_mapping(
                    Expression::Text("Untitled files can't call `use`.".to_string()),
                    Some("message".to_string()),
                );
                lambda_inner.push_without_ast_mapping(
                    Expression::Call {
                        function: panic_id,
                        arguments: vec![message_id],
                    },
                    Some("panicked".to_string()),
                );
            }
        }

        assignment_inner.id_mapping = lambda_inner.id_mapping;
        assignment_inner.push_without_ast_mapping(
            Expression::Lambda(Lambda {
                parameters: vec![lambda_parameter_id],
                body: lambda_inner.body,
            }),
            None,
        );

        self.id_mapping = assignment_inner.id_mapping;

        self.push_without_ast_mapping(
            Expression::Body(assignment_inner.body),
            Some("use".to_string()),
        );
    }

    fn compile(&mut self, asts: &[Ast]) {
        if asts.is_empty() {
            self.push_without_ast_mapping(Expression::nothing(), None);
        } else {
            for ast in asts.into_iter() {
                self.compile_single(ast);
            }
        }
    }
    fn compile_single(&mut self, ast: &Ast) -> hir::Id {
        match &ast.kind {
            AstKind::Int(Int(int)) => {
                self.push(ast.id.clone(), Expression::Int(int.to_owned()), None)
            }
            AstKind::Text(Text(string)) => self.push(
                ast.id.clone(),
                Expression::Text(string.value.to_owned()),
                None,
            ),
            AstKind::Identifier(Identifier(symbol)) => {
                let reference = match self.identifiers.get(&symbol.value) {
                    Some(reference) => reference.to_owned(),
                    None => {
                        return self.push(
                            symbol.id.clone(),
                            Expression::Error {
                                child: None,
                                errors: vec![CompilerError {
                                    input: ast.id.input.clone(),
                                    span: self.context.db.ast_id_to_span(ast.id.clone()).unwrap(),
                                    payload: CompilerErrorPayload::Hir(
                                        HirError::UnknownReference {
                                            symbol: symbol.value.clone(),
                                        },
                                    ),
                                }],
                            },
                            None,
                        );
                    }
                };
                self.push(
                    ast.id.clone(),
                    Expression::Reference(reference.to_owned()),
                    None,
                )
            }
            AstKind::Symbol(Symbol(symbol)) => self.push(
                ast.id.clone(),
                Expression::Symbol(symbol.value.to_owned()),
                None,
            ),
            AstKind::Struct(Struct { fields }) => {
                let fields = fields
                    .iter()
                    .map(|(key, value)| (self.compile_single(key), self.compile_single(value)))
                    .collect();
                self.push(ast.id.clone(), Expression::Struct(fields), None)
            }
            AstKind::Lambda(ast::Lambda {
                parameters,
                body: body_asts,
            }) => {
                let mut body = Body::new();
                let lambda_id = self.create_next_id(ast.id.clone(), None);
                let mut identifiers = self.identifiers.clone();

                for parameter in parameters.iter() {
                    let name = parameter.value.to_string();
                    let id = hir::Id::new(
                        self.context.input.clone(),
                        add_keys(&lambda_id.keys, name.clone()),
                    );
                    self.id_mapping.insert(id.clone(), parameter.id.clone());
                    body.identifiers.insert(id.clone(), name.clone());
                    identifiers.insert(name, id);
                }
                let mut inner = Compiler::<'c> {
                    context: &mut self.context,
                    id_mapping: self.id_mapping.clone(),
                    body,
                    parent_keys: lambda_id.keys.clone(),
                    identifiers,
                };

                inner.compile(&body_asts);
                self.id_mapping = inner.id_mapping;
                self.push_with_existing_id(
                    lambda_id.clone(),
                    Expression::Lambda(Lambda {
                        parameters: parameters
                            .iter()
                            .map(|parameter| {
                                hir::Id::new(
                                    self.context.input.clone(),
                                    add_keys(&lambda_id.keys[..], parameter.value.to_string()),
                                )
                            })
                            .collect(),
                        body: inner.body,
                    }),
                    None,
                )
            }
            AstKind::Call(ast::Call { name, arguments }) => {
                let arguments = arguments
                    .iter()
                    .map(|argument| self.compile_single(argument))
                    .collect();

                let function = match self.identifiers.get(&name.value) {
                    Some(function) => function.to_owned(),
                    None => {
                        return self.push(
                            name.id.clone(),
                            Expression::Error {
                                child: None,
                                errors: vec![CompilerError {
                                    input: name.id.input.clone(),
                                    span: self.context.db.ast_id_to_span(name.id.clone()).unwrap(),
                                    payload: CompilerErrorPayload::Hir(HirError::UnknownFunction {
                                        name: name.value.clone(),
                                    }),
                                }],
                            },
                            None,
                        );
                    }
                };
                self.push(
                    ast.id.clone(),
                    Expression::Call {
                        function,
                        arguments,
                    },
                    None,
                )
            }
            AstKind::Assignment(Assignment { name, body }) => {
                let name = name.value.to_owned();
                let mut inner = Compiler::<'c> {
                    context: &mut self.context,
                    id_mapping: self.id_mapping.clone(),
                    body: Body::new(),
                    parent_keys: add_keys(&self.parent_keys, name.clone()),
                    identifiers: self.identifiers.clone(),
                };
                inner.compile(&body);
                self.id_mapping = inner.id_mapping;
                self.push(ast.id.clone(), Expression::Body(inner.body), Some(name))
            }
            AstKind::Error { child, errors } => {
                let child = if let Some(child) = child {
                    Some(self.compile_single(&*child))
                } else {
                    None
                };
                self.push(
                    ast.id.clone(),
                    Expression::Error {
                        child,
                        errors: errors.clone(),
                    },
                    None,
                )
            }
        }
    }

    fn push(
        &mut self,
        ast_id: ast::Id,
        expression: Expression,
        identifier: Option<String>,
    ) -> hir::Id {
        let id = self.create_next_id(ast_id, identifier.clone());
        self.push_with_existing_id(id, expression, identifier)
    }
    fn push_without_ast_mapping(
        &mut self,
        expression: Expression,
        identifier: Option<String>,
    ) -> hir::Id {
        let id = self.create_next_id_without_ast_mapping(identifier.clone());
        self.push_with_existing_id(id, expression, identifier)
    }
    fn push_with_existing_id(
        &mut self,
        id: hir::Id,
        expression: Expression,
        identifier: Option<String>,
    ) -> hir::Id {
        self.body
            .push(id.to_owned(), expression, identifier.clone());
        if let Some(identifier) = identifier {
            self.identifiers.insert(identifier, id.clone());
        }
        id
    }

    fn create_next_id(&mut self, ast_id: ast::Id, key: Option<String>) -> hir::Id {
        let id = self.create_next_id_without_ast_mapping(key);
        assert!(self.id_mapping.insert(id.to_owned(), ast_id).is_none());
        id
    }
    fn create_next_id_without_ast_mapping(&mut self, key: Option<String>) -> hir::Id {
        for disambiguator in 0.. {
            let last_part = if let Some(key) = &key {
                let disambiguator = if disambiguator == 0 {
                    "".to_string()
                } else {
                    format!("${}", disambiguator - 1)
                };
                format!("{}{}", key, disambiguator)
            } else {
                format!("{}", disambiguator)
            };
            let id = hir::Id::new(
                self.context.input.clone(),
                add_keys(&self.parent_keys, last_part),
            );
            if !self.id_mapping.contains_key(&id) {
                return id;
            }
        }
        unreachable!()
    }
}

fn add_keys(parents: &[String], id: String) -> Vec<String> {
    parents
        .iter()
        .map(|it| it.to_string())
        .chain(vec![id])
        .collect()
}
<|endoftext|>


<commit_before>use std::ffi::CString;
use std::mem;
use std::io::Read;
use std::io::Error;
use std::io::Cursor;

extern crate byteorder;
use self::byteorder::{LittleEndian, WriteBytesExt, ReadBytesExt};

use super::packet_type::PacketType;
use super::packet_type::PacketDirection;

#[derive(Debug)]
pub struct Packet {
    pub id: i32,
    pub net_type: PacketType,
    pub body: CString
}

impl Packet {
    fn size(&self) -> i32 {
        // packet size is not used in size
        // packet id + net_type + teminated body + empty terminated string
        (mem::size_of::<i32>() + mem::size_of::<i32>() + self.body.as_bytes_with_nul().len() + 1) as i32
    }

    pub fn serialize(&self) -> Vec<u8> {
        let mut buff = vec![];

        buff.write_i32::<LittleEndian>(self.size()).unwrap();
        buff.write_i32::<LittleEndian>(self.id).unwrap();
        buff.write_i32::<LittleEndian>(self.net_type.value()).unwrap();
        buff.extend(self.body.as_bytes());
        buff.push(0);
        buff.push(0);

        buff
    }

    pub fn read_from<S>(stream: &mut S, direction: PacketDirection) -> Result<Packet, Error> where S: Read {
        let packet_size = stream.read_i32::<LittleEndian>().unwrap();
        let mut packet: Vec<u8> = vec![0; packet_size as usize];
        let _ = stream.read_exact(&mut packet);
        let mut stream = Cursor::new(packet);

        let id = stream.read_i32::<LittleEndian>().unwrap();
        let net_type = stream.read_i32::<LittleEndian>().unwrap();
        let net_type = PacketType::from_value(net_type, direction);
        let mut body = vec![0; (packet_size as usize) - 4 - 4 - 2];
        let _ = stream.read_exact(&mut body);
        let body = CString::new(body).unwrap();

        Ok(Packet {
            id: id,
            net_type: net_type,
            body: body
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use super::super::packet_type::PacketDirection::OUTCOMING;

    const OUTGOING_AUTH_PACKET: [u8; 21] = [
        0x11, 0x00, 0x00, 0x00, // Size
        0x00, 0x00, 0x00, 0x00, // id
        0x03, 0x00, 0x00, 0x00, // type
        0x70, 0x61, 0x73, 0x73, 0x77, 0x72, 0x64, 0x00, // command string (passwd in this case)
        0x00 // packet temination string
    ];

    #[test]
    fn test_size() {
        let mut packet = Packet {
            id: 0,
            net_type: PacketType::SERVERDATA_AUTH,
            body: CString::new("").unwrap()
        };

        assert_eq!(packet.size(), 10);

        packet.body = CString::new("body").unwrap();
        assert_eq!(packet.size(), 14);
        assert_eq!(packet.size(), (packet.serialize().len() - 4) as i32);
    }

    #[test]
    fn test_serialize() {
        let packet = Packet {
            id: 0,
            net_type: PacketType::SERVERDATA_AUTH,
            body: CString::new("passwrd").unwrap()
        };

        assert_eq!(packet.serialize()[..], OUTGOING_AUTH_PACKET);
    }

    #[test]
    fn test_read_from() {
        let mut stream = Cursor::new(OUTGOING_AUTH_PACKET);
        let packet = Packet::read_from(&mut stream, OUTCOMING).unwrap();

        assert_eq!(packet.id, 0);
        assert_eq!(packet.net_type, PacketType::SERVERDATA_AUTH);
        assert_eq!(packet.body, CString::new("passwrd").unwrap());
    }
}
<commit_msg>[srcds:rcon:packet] Better error forwarding<commit_after>use std::ffi::CString;
use std::mem;
use std::io::Read;
use std::io::Error;
use std::io::Cursor;

extern crate byteorder;
use self::byteorder::{LittleEndian, WriteBytesExt, ReadBytesExt};

use super::packet_type::PacketType;
use super::packet_type::PacketDirection;

#[derive(Debug)]
pub struct Packet {
    pub id: i32,
    pub net_type: PacketType,
    pub body: CString
}

impl Packet {
    fn size(&self) -> i32 {
        // packet size is not used in size
        // packet id + net_type + teminated body + empty terminated string
        (mem::size_of::<i32>() + mem::size_of::<i32>() + self.body.as_bytes_with_nul().len() + 1) as i32
    }

    pub fn serialize(&self) -> Vec<u8> {
        let mut buff = vec![];

        buff.write_i32::<LittleEndian>(self.size()).unwrap();
        buff.write_i32::<LittleEndian>(self.id).unwrap();
        buff.write_i32::<LittleEndian>(self.net_type.value()).unwrap();
        buff.extend(self.body.as_bytes());
        buff.push(0);
        buff.push(0);

        buff
    }

    pub fn read_from<S: Read>(stream: &mut S, direction: PacketDirection) -> Result<Packet, Error>{
        // packet id (4) - packet size (4) - two zero bytes (2)
        const SERVICE_FIELDS_SIZE: usize = 10;

        let packet_size = stream.read_i32::<LittleEndian>()? as usize;
        let mut packet_contents: Vec<u8> = vec![0; packet_size];
        stream.read_exact(&mut packet_contents)?;

        let mut stream = Cursor::new(packet_contents);
        let id = stream.read_i32::<LittleEndian>()?;
        let net_type = stream.read_i32::<LittleEndian>()?;
        let net_type = PacketType::from_value(net_type, direction);
        let mut body = vec![0; packet_size - SERVICE_FIELDS_SIZE];
        stream.read_exact(&mut body)?;
        let body = CString::new(body)?;

        Ok(Packet {
            id: id,
            net_type: net_type,
            body: body
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use super::super::packet_type::PacketDirection::OUTCOMING;

    const OUTGOING_AUTH_PACKET: [u8; 21] = [
        0x11, 0x00, 0x00, 0x00, // Size
        0x00, 0x00, 0x00, 0x00, // id
        0x03, 0x00, 0x00, 0x00, // type
        0x70, 0x61, 0x73, 0x73, 0x77, 0x72, 0x64, 0x00, // command string (passwd in this case)
        0x00 // packet temination string
    ];

    #[test]
    fn test_size() {
        let mut packet = Packet {
            id: 0,
            net_type: PacketType::SERVERDATA_AUTH,
            body: CString::new("").unwrap()
        };

        assert_eq!(packet.size(), 10);

        packet.body = CString::new("body").unwrap();
        assert_eq!(packet.size(), 14);
        assert_eq!(packet.size(), (packet.serialize().len() - 4) as i32);
    }

    #[test]
    fn test_serialize() {
        let packet = Packet {
            id: 0,
            net_type: PacketType::SERVERDATA_AUTH,
            body: CString::new("passwrd").unwrap()
        };

        assert_eq!(packet.serialize()[..], OUTGOING_AUTH_PACKET);
    }

    #[test]
    fn test_read_from() {
        let mut stream = Cursor::new(OUTGOING_AUTH_PACKET);
        let packet = Packet::read_from(&mut stream, OUTCOMING).unwrap();

        assert_eq!(packet.id, 0);
        assert_eq!(packet.net_type, PacketType::SERVERDATA_AUTH);
        assert_eq!(packet.body, CString::new("passwrd").unwrap());
    }
}
<|endoftext|>


<commit_before>//! Searches, processes and uploads release files.
use std::collections::HashMap;
use std::fmt;
use std::io::BufWriter;
use std::path::PathBuf;
use std::str;
use std::sync::Arc;
use std::time::{Duration, Instant};

use console::style;
use failure::{bail, Error};
use parking_lot::RwLock;
use rayon::prelude::*;
use rayon::ThreadPoolBuilder;
use symbolic::common::ByteView;
use symbolic::debuginfo::sourcebundle::{SourceBundleWriter, SourceFileInfo, SourceFileType};
use url::Url;

use crate::api::{Api, ChunkUploadCapability, ChunkUploadOptions, FileContents, ProgressBarMode};
use crate::constants::DEFAULT_MAX_WAIT;
use crate::utils::chunks::{upload_chunks, Chunk, ASSEMBLE_POLL_INTERVAL};
use crate::utils::fs::{get_sha1_checksums, TempFile};
use crate::utils::progress::{ProgressBar, ProgressStyle};

/// Fallback concurrency for release file uploads.
static DEFAULT_CONCURRENCY: usize = 4;

pub struct UploadContext<'a> {
    pub org: &'a str,
    pub project: Option<&'a str>,
    pub release: &'a str,
    pub dist: Option<&'a str>,
    pub wait: bool,
}

#[derive(PartialEq, Debug, Copy, Clone)]
pub enum LogLevel {
    Warning,
    Error,
}

impl fmt::Display for LogLevel {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match *self {
            LogLevel::Warning => write!(f, "warning"),
            LogLevel::Error => write!(f, "error"),
        }
    }
}

#[derive(Clone, Debug)]
pub struct ReleaseFile {
    pub url: String,
    pub path: PathBuf,
    pub contents: Vec<u8>,
    pub ty: SourceFileType,
    pub headers: Vec<(String, String)>,
    pub messages: Vec<(LogLevel, String)>,
}

impl ReleaseFile {
    pub fn log(&mut self, level: LogLevel, msg: String) {
        self.messages.push((level, msg));
    }

    pub fn warn(&mut self, msg: String) {
        self.log(LogLevel::Warning, msg);
    }

    pub fn error(&mut self, msg: String) {
        self.log(LogLevel::Error, msg);
    }
}

pub type ReleaseFiles = HashMap<String, ReleaseFile>;

pub struct ReleaseFileUpload<'a> {
    context: &'a UploadContext<'a>,
    files: ReleaseFiles,
}

impl<'a> ReleaseFileUpload<'a> {
    pub fn new(context: &'a UploadContext) -> Self {
        ReleaseFileUpload {
            context,
            files: HashMap::new(),
        }
    }

    pub fn files(&mut self, files: &ReleaseFiles) -> &mut Self {
        for (k, v) in files {
            self.files.insert(k.to_owned(), v.to_owned());
        }
        self
    }

    pub fn upload(&self) -> Result<(), Error> {
        let api = Api::current();

        let chunk_options = api.get_chunk_upload_options(self.context.org)?;
        if let Some(ref chunk_options) = chunk_options {
            if chunk_options.supports(ChunkUploadCapability::ReleaseFiles) {
                return upload_files_chunked(self.context, &self.files, chunk_options);
            }
        }

        // Do not permit uploads of more than 20k files if the server does not
        // support artifact bundles.  This is a termporary downside protection to
        // protect users from uploading more sources than we support.
        if self.files.len() > 20_000 {
            bail!(
                "Too many sources: {} exceeds maximum allowed files per release",
                &self.files.len()
            );
        }

        let concurrency = chunk_options.map_or(DEFAULT_CONCURRENCY, |o| usize::from(o.concurrency));
        upload_files_parallel(self.context, &self.files, concurrency)
    }
}

fn upload_files_parallel(
    context: &UploadContext,
    files: &ReleaseFiles,
    num_threads: usize,
) -> Result<(), Error> {
    let api = Api::current();

    // get a list of release files first so we know the file IDs of
    // files that already exist.
    let release_files: HashMap<_, _> = api
        .list_release_files(context.org, context.project, context.release)?
        .into_iter()
        .map(|artifact| ((artifact.dist, artifact.name), artifact.id))
        .collect();

    println!(
        "{} Uploading source maps for release {}",
        style(">").dim(),
        style(context.release).cyan()
    );

    let progress_style = ProgressStyle::default_bar().template(&format!(
        "{} Uploading {} source map{}...\
     \n{{wide_bar}}  {{bytes}}/{{total_bytes}} ({{eta}})",
        style(">").dim(),
        style(files.len().to_string()).yellow(),
        if files.len() == 1 { "" } else { "s" }
    ));

    let total_bytes = files.values().map(|file| file.contents.len() as u64).sum();
    let files = files.iter().collect::<Vec<_>>();

    let pb = Arc::new(ProgressBar::new(total_bytes));
    pb.set_style(progress_style);

    let pool = ThreadPoolBuilder::new().num_threads(num_threads).build()?;
    let bytes = Arc::new(RwLock::new(vec![0u64; files.len()]));

    pool.install(|| {
        files
            .into_par_iter()
            .enumerate()
            .map(|(index, (_, file))| -> Result<(), Error> {
                let api = Api::current();
                let mode = ProgressBarMode::Shared((
                    pb.clone(),
                    file.contents.len() as u64,
                    index,
                    bytes.clone(),
                ));

                if let Some(old_id) =
                    release_files.get(&(context.dist.map(|x| x.into()), file.url.clone()))
                {
                    api.delete_release_file(
                        context.org,
                        context.project,
                        &context.release,
                        &old_id,
                    )
                    .ok();
                }

                api.upload_release_file(
                    context.org,
                    context.project,
                    context.release,
                    &FileContents::FromBytes(&file.contents),
                    &file.url,
                    context.dist,
                    Some(file.headers.as_slice()),
                    mode,
                )?;

                Ok(())
            })
            .collect::<Result<(), _>>()
    })?;

    pb.finish_and_clear();

    Ok(())
}

fn upload_files_chunked(
    context: &UploadContext,
    files: &ReleaseFiles,
    options: &ChunkUploadOptions,
) -> Result<(), Error> {
    let archive = build_artifact_bundle(context, files)?;

    let progress_style =
        ProgressStyle::default_spinner().template("{spinner} Optimizing bundle for upload...");

    let progress = ProgressBar::new_spinner();
    progress.enable_steady_tick(100);
    progress.set_style(progress_style);

    let view = ByteView::open(archive.path())?;
    let (checksum, checksums) = get_sha1_checksums(&view, options.chunk_size)?;
    let chunks = view
        .chunks(options.chunk_size as usize)
        .zip(checksums.iter())
        .map(|(data, checksum)| Chunk((*checksum, data)))
        .collect::<Vec<_>>();

    progress.finish_with_duration("Optimizing");

    let progress_style = ProgressStyle::default_bar().template(&format!(
        "{} Uploading release files...\
       \n{{wide_bar}}  {{bytes}}/{{total_bytes}} ({{eta}})",
        style(">").dim(),
    ));

    upload_chunks(&chunks, options, progress_style)?;
    println!("{} Uploaded release files to Sentry", style(">").dim(),);

    let progress_style = ProgressStyle::default_spinner().template("{spinner} Processing files...");

    let progress = ProgressBar::new_spinner();
    progress.enable_steady_tick(100);
    progress.set_style(progress_style);

    let assemble_start = Instant::now();
    let max_wait = match options.max_wait {
        0 => DEFAULT_MAX_WAIT,
        secs => Duration::from_secs(secs),
    };

    let api = Api::current();
    let response = loop {
        let response =
            api.assemble_artifacts(context.org, context.release, checksum, &checksums)?;

        // Poll until there is a response, unless the user has specified to skip polling. In
        // that case, we return the potentially partial response from the server. This might
        // still contain a cached error.
        if !context.wait || response.state.is_finished() {
            break response;
        }

        if assemble_start.elapsed() > max_wait {
            break response;
        }

        std::thread::sleep(ASSEMBLE_POLL_INTERVAL);
    };

    if response.state.is_err() {
        let message = response.detail.as_deref().unwrap_or("unknown error");
        bail!("Failed to process uploaded files: {}", message);
    }

    progress.finish_with_duration("Processing");

    if response.state.is_pending() {
        if context.wait {
            bail!("Failed to process files in {}s", max_wait.as_secs());
        } else {
            println!(
                "{} File upload complete (processing pending on server)",
                style(">").dim()
            );
        }
    } else {
        println!("{} File processing complete", style(">").dim());
    }

    Ok(())
}

fn build_artifact_bundle(context: &UploadContext, files: &ReleaseFiles) -> Result<TempFile, Error> {
    let progress_style = ProgressStyle::default_bar().template(
        "{prefix:.dim} Bundling files for upload... {msg:.dim}\
       \n{wide_bar}  {pos}/{len}",
    );

    let progress = ProgressBar::new(files.len() as u64);
    progress.set_style(progress_style);
    progress.set_prefix(">");

    let archive = TempFile::create()?;
    let mut bundle = SourceBundleWriter::start(BufWriter::new(archive.open()?))?;

    bundle.set_attribute("org".to_owned(), context.org.to_owned());
    if let Some(project) = context.project {
        bundle.set_attribute("project".to_owned(), project.to_owned());
    }
    bundle.set_attribute("release".to_owned(), context.release.to_owned());
    if let Some(dist) = context.dist {
        bundle.set_attribute("dist".to_owned(), dist.to_owned());
    }

    for file in files.values() {
        progress.inc(1);
        progress.set_message(file.url.to_owned());

        let mut info = SourceFileInfo::new();
        info.set_ty(file.ty);
        info.set_url(file.url.clone());
        for (k, v) in &file.headers {
            info.add_header(k.clone(), v.clone());
        }

        let bundle_path = url_to_bundle_path(&file.url)?;
        bundle.add_file(bundle_path, file.contents.as_slice(), info)?;
    }

    bundle.finish()?;

    println!(
        "{} Bundled {} {} for upload",
        style(">").dim(),
        style(files.len()).yellow(),
        match files.len() {
            1 => "file",
            _ => "files",
        }
    );

    progress.finish_with_duration("Bundling");

    Ok(archive)
}

fn url_to_bundle_path(url: &str) -> Result<String, Error> {
    let base = Url::parse("http://~").unwrap();
    let url = if let Some(rest) = url.strip_prefix("~/") {
        base.join(rest)?
    } else {
        base.join(url)?
    };

    let mut path = url.path();
    if path.starts_with('/') {
        path = &path[1..];
    }

    Ok(match url.host_str() {
        Some("~") => format!("_/_/{}", path),
        Some(host) => format!("{}/{}/{}", url.scheme(), host, path),
        None => format!("{}/_/{}", url.scheme(), path),
    })
}
<commit_msg>fix: Correctly resolve paths with hashes in url_to_bundle_path (#1000)<commit_after>//! Searches, processes and uploads release files.
use std::collections::HashMap;
use std::fmt;
use std::io::BufWriter;
use std::path::PathBuf;
use std::str;
use std::sync::Arc;
use std::time::{Duration, Instant};

use console::style;
use failure::{bail, Error};
use parking_lot::RwLock;
use rayon::prelude::*;
use rayon::ThreadPoolBuilder;
use symbolic::common::ByteView;
use symbolic::debuginfo::sourcebundle::{SourceBundleWriter, SourceFileInfo, SourceFileType};
use url::Url;

use crate::api::{Api, ChunkUploadCapability, ChunkUploadOptions, FileContents, ProgressBarMode};
use crate::constants::DEFAULT_MAX_WAIT;
use crate::utils::chunks::{upload_chunks, Chunk, ASSEMBLE_POLL_INTERVAL};
use crate::utils::fs::{get_sha1_checksums, TempFile};
use crate::utils::progress::{ProgressBar, ProgressStyle};

/// Fallback concurrency for release file uploads.
static DEFAULT_CONCURRENCY: usize = 4;

pub struct UploadContext<'a> {
    pub org: &'a str,
    pub project: Option<&'a str>,
    pub release: &'a str,
    pub dist: Option<&'a str>,
    pub wait: bool,
}

#[derive(PartialEq, Debug, Copy, Clone)]
pub enum LogLevel {
    Warning,
    Error,
}

impl fmt::Display for LogLevel {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match *self {
            LogLevel::Warning => write!(f, "warning"),
            LogLevel::Error => write!(f, "error"),
        }
    }
}

#[derive(Clone, Debug)]
pub struct ReleaseFile {
    pub url: String,
    pub path: PathBuf,
    pub contents: Vec<u8>,
    pub ty: SourceFileType,
    pub headers: Vec<(String, String)>,
    pub messages: Vec<(LogLevel, String)>,
}

impl ReleaseFile {
    pub fn log(&mut self, level: LogLevel, msg: String) {
        self.messages.push((level, msg));
    }

    pub fn warn(&mut self, msg: String) {
        self.log(LogLevel::Warning, msg);
    }

    pub fn error(&mut self, msg: String) {
        self.log(LogLevel::Error, msg);
    }
}

pub type ReleaseFiles = HashMap<String, ReleaseFile>;

pub struct ReleaseFileUpload<'a> {
    context: &'a UploadContext<'a>,
    files: ReleaseFiles,
}

impl<'a> ReleaseFileUpload<'a> {
    pub fn new(context: &'a UploadContext) -> Self {
        ReleaseFileUpload {
            context,
            files: HashMap::new(),
        }
    }

    pub fn files(&mut self, files: &ReleaseFiles) -> &mut Self {
        for (k, v) in files {
            self.files.insert(k.to_owned(), v.to_owned());
        }
        self
    }

    pub fn upload(&self) -> Result<(), Error> {
        let api = Api::current();

        let chunk_options = api.get_chunk_upload_options(self.context.org)?;
        if let Some(ref chunk_options) = chunk_options {
            if chunk_options.supports(ChunkUploadCapability::ReleaseFiles) {
                return upload_files_chunked(self.context, &self.files, chunk_options);
            }
        }

        // Do not permit uploads of more than 20k files if the server does not
        // support artifact bundles.  This is a termporary downside protection to
        // protect users from uploading more sources than we support.
        if self.files.len() > 20_000 {
            bail!(
                "Too many sources: {} exceeds maximum allowed files per release",
                &self.files.len()
            );
        }

        let concurrency = chunk_options.map_or(DEFAULT_CONCURRENCY, |o| usize::from(o.concurrency));
        upload_files_parallel(self.context, &self.files, concurrency)
    }
}

fn upload_files_parallel(
    context: &UploadContext,
    files: &ReleaseFiles,
    num_threads: usize,
) -> Result<(), Error> {
    let api = Api::current();

    // get a list of release files first so we know the file IDs of
    // files that already exist.
    let release_files: HashMap<_, _> = api
        .list_release_files(context.org, context.project, context.release)?
        .into_iter()
        .map(|artifact| ((artifact.dist, artifact.name), artifact.id))
        .collect();

    println!(
        "{} Uploading source maps for release {}",
        style(">").dim(),
        style(context.release).cyan()
    );

    let progress_style = ProgressStyle::default_bar().template(&format!(
        "{} Uploading {} source map{}...\
     \n{{wide_bar}}  {{bytes}}/{{total_bytes}} ({{eta}})",
        style(">").dim(),
        style(files.len().to_string()).yellow(),
        if files.len() == 1 { "" } else { "s" }
    ));

    let total_bytes = files.values().map(|file| file.contents.len() as u64).sum();
    let files = files.iter().collect::<Vec<_>>();

    let pb = Arc::new(ProgressBar::new(total_bytes));
    pb.set_style(progress_style);

    let pool = ThreadPoolBuilder::new().num_threads(num_threads).build()?;
    let bytes = Arc::new(RwLock::new(vec![0u64; files.len()]));

    pool.install(|| {
        files
            .into_par_iter()
            .enumerate()
            .map(|(index, (_, file))| -> Result<(), Error> {
                let api = Api::current();
                let mode = ProgressBarMode::Shared((
                    pb.clone(),
                    file.contents.len() as u64,
                    index,
                    bytes.clone(),
                ));

                if let Some(old_id) =
                    release_files.get(&(context.dist.map(|x| x.into()), file.url.clone()))
                {
                    api.delete_release_file(
                        context.org,
                        context.project,
                        &context.release,
                        &old_id,
                    )
                    .ok();
                }

                api.upload_release_file(
                    context.org,
                    context.project,
                    context.release,
                    &FileContents::FromBytes(&file.contents),
                    &file.url,
                    context.dist,
                    Some(file.headers.as_slice()),
                    mode,
                )?;

                Ok(())
            })
            .collect::<Result<(), _>>()
    })?;

    pb.finish_and_clear();

    Ok(())
}

fn upload_files_chunked(
    context: &UploadContext,
    files: &ReleaseFiles,
    options: &ChunkUploadOptions,
) -> Result<(), Error> {
    let archive = build_artifact_bundle(context, files)?;

    let progress_style =
        ProgressStyle::default_spinner().template("{spinner} Optimizing bundle for upload...");

    let progress = ProgressBar::new_spinner();
    progress.enable_steady_tick(100);
    progress.set_style(progress_style);

    let view = ByteView::open(archive.path())?;
    let (checksum, checksums) = get_sha1_checksums(&view, options.chunk_size)?;
    let chunks = view
        .chunks(options.chunk_size as usize)
        .zip(checksums.iter())
        .map(|(data, checksum)| Chunk((*checksum, data)))
        .collect::<Vec<_>>();

    progress.finish_with_duration("Optimizing");

    let progress_style = ProgressStyle::default_bar().template(&format!(
        "{} Uploading release files...\
       \n{{wide_bar}}  {{bytes}}/{{total_bytes}} ({{eta}})",
        style(">").dim(),
    ));

    upload_chunks(&chunks, options, progress_style)?;
    println!("{} Uploaded release files to Sentry", style(">").dim(),);

    let progress_style = ProgressStyle::default_spinner().template("{spinner} Processing files...");

    let progress = ProgressBar::new_spinner();
    progress.enable_steady_tick(100);
    progress.set_style(progress_style);

    let assemble_start = Instant::now();
    let max_wait = match options.max_wait {
        0 => DEFAULT_MAX_WAIT,
        secs => Duration::from_secs(secs),
    };

    let api = Api::current();
    let response = loop {
        let response =
            api.assemble_artifacts(context.org, context.release, checksum, &checksums)?;

        // Poll until there is a response, unless the user has specified to skip polling. In
        // that case, we return the potentially partial response from the server. This might
        // still contain a cached error.
        if !context.wait || response.state.is_finished() {
            break response;
        }

        if assemble_start.elapsed() > max_wait {
            break response;
        }

        std::thread::sleep(ASSEMBLE_POLL_INTERVAL);
    };

    if response.state.is_err() {
        let message = response.detail.as_deref().unwrap_or("unknown error");
        bail!("Failed to process uploaded files: {}", message);
    }

    progress.finish_with_duration("Processing");

    if response.state.is_pending() {
        if context.wait {
            bail!("Failed to process files in {}s", max_wait.as_secs());
        } else {
            println!(
                "{} File upload complete (processing pending on server)",
                style(">").dim()
            );
        }
    } else {
        println!("{} File processing complete", style(">").dim());
    }

    Ok(())
}

fn build_artifact_bundle(context: &UploadContext, files: &ReleaseFiles) -> Result<TempFile, Error> {
    let progress_style = ProgressStyle::default_bar().template(
        "{prefix:.dim} Bundling files for upload... {msg:.dim}\
       \n{wide_bar}  {pos}/{len}",
    );

    let progress = ProgressBar::new(files.len() as u64);
    progress.set_style(progress_style);
    progress.set_prefix(">");

    let archive = TempFile::create()?;
    let mut bundle = SourceBundleWriter::start(BufWriter::new(archive.open()?))?;

    bundle.set_attribute("org".to_owned(), context.org.to_owned());
    if let Some(project) = context.project {
        bundle.set_attribute("project".to_owned(), project.to_owned());
    }
    bundle.set_attribute("release".to_owned(), context.release.to_owned());
    if let Some(dist) = context.dist {
        bundle.set_attribute("dist".to_owned(), dist.to_owned());
    }

    for file in files.values() {
        progress.inc(1);
        progress.set_message(file.url.to_owned());

        let mut info = SourceFileInfo::new();
        info.set_ty(file.ty);
        info.set_url(file.url.clone());
        for (k, v) in &file.headers {
            info.add_header(k.clone(), v.clone());
        }

        let bundle_path = url_to_bundle_path(&file.url)?;
        bundle.add_file(bundle_path, file.contents.as_slice(), info)?;
    }

    bundle.finish()?;

    println!(
        "{} Bundled {} {} for upload",
        style(">").dim(),
        style(files.len()).yellow(),
        match files.len() {
            1 => "file",
            _ => "files",
        }
    );

    progress.finish_with_duration("Bundling");

    Ok(archive)
}

fn url_to_bundle_path(url: &str) -> Result<String, Error> {
    let base = Url::parse("http://~").unwrap();
    let url = if let Some(rest) = url.strip_prefix("~/") {
        base.join(rest)?
    } else {
        base.join(url)?
    };

    let mut path = url.path().to_string();
    if let Some(fragment) = url.fragment() {
        path = format!("{}#{}", path, fragment);
    }
    if path.starts_with('/') {
        path.remove(0);
    }

    Ok(match url.host_str() {
        Some("~") => format!("_/_/{}", path),
        Some(host) => format!("{}/{}/{}", url.scheme(), host, path),
        None => format!("{}/_/{}", url.scheme(), path),
    })
}

#[test]
fn test_url_to_bundle_path() {
    assert_eq!(url_to_bundle_path("~/bar").unwrap(), "_/_/bar");
    assert_eq!(url_to_bundle_path("~/foo/bar").unwrap(), "_/_/foo/bar");
    assert_eq!(
        url_to_bundle_path("~/dist/js/bundle.js.map").unwrap(),
        "_/_/dist/js/bundle.js.map"
    );
    assert_eq!(
        url_to_bundle_path("~/babel.config.js").unwrap(),
        "_/_/babel.config.js"
    );

    assert_eq!(url_to_bundle_path("~/#/bar").unwrap(), "_/_/#/bar");
    assert_eq!(url_to_bundle_path("~/foo/#/bar").unwrap(), "_/_/foo/#/bar");
    assert_eq!(
        url_to_bundle_path("~/dist/#js/bundle.js.map").unwrap(),
        "_/_/dist/#js/bundle.js.map"
    );
    assert_eq!(
        url_to_bundle_path("~/#foo/babel.config.js").unwrap(),
        "_/_/#foo/babel.config.js"
    );
}
<|endoftext|>


<commit_before>//! Configuration for the zwreec compiler.
//!
//! Uses the `getopts` library to append a `getopts::Options` with compiler
//! specific flags and then creates a `Config` struct from `getopts::Matches`.
//!
//! # Usage
//!
//! This module depends on the [getopts](http://doc.rust-lang.org/getopts/getopts/index.html)
//! crate. To use it, you need to add `getopts` to your project. Read getopts'
//! [Usage](http://doc.rust-lang.org/getopts/getopts/index.html#usage) for more information.
//!
//! # Example
//!
//! The following example shows how to generate a `Config` struct from an empty
//! `getopts::Options`.
//!
//! ```
//! extern crate zwreec;
//! extern crate getopts;
//!
//! use zwreec::config;
//! use zwreec::config::Config;
//!
//!
//! fn main() {
//!     let args: Vec<String> = std::env::args().collect();
//!     let opts = config::zwreec_options(getopts::Options::new());
//!
//!     let matches = match opts.parse(&args[1..]) {
//!         Ok(m) => m,
//!         Err(f) => { panic!(f.to_string()) }
//!     };
//!
//!     let cfg = Config::from_matches(&matches);
//! }
//! ```
//!
//! # Appending this module (Development)
//!
//! To add new compiler flags for the compiler, you need to change three parts
//! in this module.
//!
//! 1. Appending the Struct
//!
//!     To store your new setting, you need to add it as a field to the `Config` struct. Here we
//!     add a new boolean `pub italics: bool` to control *italics*. Always add a field-comment.
//!
//!     ```ignore
//!     pub struct Config {
//!         /// Add easter egg to compiler
//!         pub easter_egg: bool,
//!         /// Instruct compiler to run these test-cases
//!         pub test_cases: Vec<TestCase>,
//!         /// Enables or disalbes Italics
//!         pub italics: bool,
//!     }
//!     ```
//!
//!     Now you also need to change `Config::default_config()` to provide the
//!     default value for your new field:
//!
//!     ```ignore
//!     pub fn default_config() -> Config {
//!         Config{
//!             easter_egg: true,
//!             test_cases: Vec::new(),
//!             italics: true,
//!         }
//!     }
//!     ```
//!
//! 2. Parse your flag in `from_matches()`
//!
//!     `from_matches` uses the Options `-F` and `-N` to enable or disable boolean flags.
//!     Therefore, if you added a new boolean flag, you can now simply add it to the match
//!     statement.
//!
//!     ```ignore
//!     for s in matches.opt_strs("F") {
//!         match s.as_ref() {
//!             "easter-egg" => {
//!                  cfg.easter_egg = true;
//!                  debug!("enabled easter-egg");
//!             },
//!             "italics" => {
//!                 cfg.italics = true;
//!                 debug!("enabbled italics");
//!             },
//!             _ => {
//!                 error!("Cannot enable feature {} - feature not known.", s);
//!             }
//!         }
//!     }
//!
//!     for s in matches.opt_strs("N") {
//!         match s.as_ref() {
//!             "easter-egg" => {
//!                 cfg.easter_egg = false;
//!                 debug!("disabled easter-egg");
//!             },
//!             "italics" => {
//!                 cfg.italics = false;
//!                 debug:("disalbed italics");
//!             }
//!             _ => {
//!                 error!("Cannot disable feature {} - feature not known.", s);
//!             }
//!         }
//!     }
//!     ```
//!
//! 3. Adding something that is not a boolean
//!
//!     If you need to add an option that is *not* a boolean, you will need to append the struct
//!     and `default_config()` accordingly (e.g. `pub notaflag: String,`). Then you need to add a new
//!     `getopts::Option` to `zwreec_options()`:
//!
//!     ```ignore
//! pub fn zwreec_options(mut opts: getopts::Options) -> getopts::Options {
//!     opts.optmulti("F", "feature", "", "FEAT");
//!     opts.optmulti("N", "no-feature", "enable or disable a feature (can occur multiple times).
//!                         List of supported features (default):
//!                             easter-egg (enabled)", "FEAT");
//!     opts.optflag("e", "generate-sample-zcode", "writes out a sample zcode file, input file is not used and can be omitted");
//!
//!     opts.optopt("n", "notaflag", "notaflag", "SOMETHING");
//!
//!     opts
//! }
//!     ```
//!
//!     Now you can append `Config::from_matches()` to analyse the provided matches for your new
//!     option and set the `Config` accordingly:
//!
//!     ```ignore
//!     if let s = matches.opt_str("n") {
//!         cfg.notaflag = s;
//!     }
//!     ```
//!
//! 4. Now you can use the new flag inside the compiler.
use getopts;

use std::vec::Vec;

/// Represents the configuration for the compiler.
///
/// This struct is created using either `config::default_config()` or
/// `config::parse_matches(matches: &getopts::Matches)`. It contains a series
/// of fields that can be used to control the behaviour of the compiler.
///
/// # Example Usage inside the compiler
///
/// ```
/// # use zwreec::config;
/// let cfg = config::Config::default_config();
///
/// if cfg.easter_egg {
///     println!("Egg!");
/// }
/// ```
pub struct Config {
    /// Force compilation despite errors
    pub force: bool,
    /// Add easter egg to compiler
    pub easter_egg: bool,
    pub force_unicode: bool,
    /// Instruct compiler to run these test-cases
    pub test_cases: Vec<TestCase>,
}

impl Config {
    /// Returns a `Config` struct with preset defaults
    ///
    /// # Example
    ///
    /// ```
    /// use zwreec::config::Config;
    /// let cfg = Config::default_config();
    /// ```
    pub fn default_config() -> Config {
        Config{
            force: false,
            easter_egg: true,
            force_unicode: false,
            test_cases: Vec::new(),
        }
    }

    /// Returns a `Config` struct by using `getopts::Matches` to set the fields.
    ///
    /// This method analyses a `getopts::Matches` for fields provided by
    /// `zwreec_options()`.
    ///
    /// # Example
    ///
    /// ```
    /// # extern crate getopts;
    /// # extern crate zwreec;
    /// let args: Vec<String> = std::env::args().collect();
    /// let opts = zwreec::config::zwreec_options(getopts::Options::new());
    ///
    /// let matches = match opts.parse(&args[1..]) {
    ///     Ok(m) => m,
    ///     Err(f) => { panic!(f.to_string()) }
    /// };
    ///
    /// let cfg = zwreec::config::Config::from_matches(&matches);
    /// ```
    pub fn from_matches(matches: &getopts::Matches) -> Config {
        // load defaults
        let mut cfg = Config::default_config();

        if matches.opt_present("generate-sample-zcode") {
            cfg.test_cases.push(TestCase::ZcodeBackend);
        }

        if matches.opt_present("f") {
            cfg.force = true;
        }

        // TODO: Find a way to make these two loops somewhat less.. repetitive
        for s in matches.opt_strs("F") {
            match s.as_ref() {
                "easter-egg" => {
                     cfg.easter_egg = true;
                     debug!("enabled easter-egg");
                },
                "force-unicode" => {
                     cfg.force_unicode = true;
                     debug!("enabled force-unicode");
                },
                _ => {
                    error!("Cannot enable feature {} - feature not known.", s);
                }
            }
        }

        for s in matches.opt_strs("N") {
            match s.as_ref() {
                "easter-egg" => {
                    cfg.easter_egg = false;
                    debug!("disabled easter-egg");
                },
                "force-unicode" => {
                     cfg.force_unicode = false;
                     debug!("enabled force-unicode");
                },
                _ => {
                    error!("Cannot disable feature {} - feature not known.", s);
                }
            }
        }

        cfg
    }
}

// TODO: If this stays only one Test Case, enum should be removed
/// The Type used to define backend tests for the compiler
#[derive(PartialEq)]
pub enum TestCase {
    /// Skips the normal compiler chain and builds an example zcode file by
    /// using every opcode.
    ZcodeBackend,
}

/// Appends a `getopts::Options` with compiler specific flags
///
/// The method `Config::from_matches()` looks for very specific `getopts::Matches`.
/// This function takes an `getopts::Options` to append it with Options required
/// by `from_matches`. It currently adds three fields:
///
/// ```ignore
/// opts.optmulti("F", "feature", "", "FEAT");
/// opts.optmulti("N", "no-feature", "enable or disable a feature (can occur multiple times).
///                     List of supported features (default):
///                         easter-egg (enabled)", "FEAT");
/// opts.optflag("e", "generate-sample-zcode", "writes out a sample zcode file, input file is not used and can be omitted");
/// ```
///
/// # Example
///
/// You can use this function to append your `getopts::Options`.
///
/// ```
/// # extern crate getopts;
/// # extern crate zwreec;
///
/// let mut opts = getopts::Options::new();
/// opts.optflag("h", "help", "print this message");
///
/// let opts = zwreec::config::zwreec_options(opts);
/// ```
///
/// Another useful example is to use it to gernerate a more compact usage by
/// having a function that only returns your options.
///
/// ```
/// # extern crate getopts;
/// # extern crate zwreec;
///
/// fn options() -> getopts::Options {
///     let mut opts = getopts::Options::new();
///     opts.optflag("h", "help", "display this help and exit");
///     opts.optflag("V", "version", "display version");
///
///     opts
/// }
///
/// fn print_usage(program: &str, verbose: bool) {
///     let brief = format!("Usage: {} [options]", program);
///
///     let opts = if verbose {
///         zwreec::config::zwreec_options(options())
///     } else {
///         options()
///     };
///
///     print!("{}", opts.usage(&brief));
/// }
/// ```
/// As you can see, `options()` returns your own command line options, which are then conditionally
/// expanded by using `zwreec_options()`.
pub fn zwreec_options(mut opts: getopts::Options) -> getopts::Options {
    opts.optflag("f", "force", "Force opening of output file");
    opts.optmulti("F", "feature", "", "FEAT");
    opts.optmulti("N", "no-feature", "Enable or disable a feature (can occur multiple times).
        For more information about the supported features run --help with -v and see the feature
        list at the end of the output", "FEAT");
    opts.optflag("e", "generate-sample-zcode", "Write out a sample zcode file, input file is not used and can be omitted");

    opts
}

pub fn zwreec_usage(verbose: bool, opts: getopts::Options, brief: &str) -> String {
    use std::fmt::format;
    let options = zwreec_options(opts);

    let options_usage = options.usage(brief);

    let features_usage = if verbose {
        "List of supported features (default value in parenthesis)
    easter-egg (enabled)
        Enables the generation of easter egg code. Enter the secret combination
        in your Z-Machine interpreter to activate the easter egg
    force-unicode (disabled)
        Force the generation of unicode print opcodes every time a unicode
        character is encountered. This disables the generation of the unicode
        translation table"
    } else {
        "Additional help:
    --help -v           Print the full set of options zwreec accepts"
    };

    format(format_args!("{}\n{}\n", options_usage, features_usage.to_string()))
}

#[cfg(test)]
mod tests {
    use super::*;
    use getopts;

    fn config_from_args(args: Vec<String>) -> Config {
        let opts = zwreec_options(getopts::Options::new());

        match opts.parse(&args) {
            Ok(m) => Config::from_matches(&m),
            Err(f) => { panic!(f.to_string()) }
        }
    }

    #[test]
    fn test_feature_easter_egg_true() {
        let cfg = config_from_args(vec!["-F".to_string(), "easter-egg".to_string()]);

        assert_eq!(cfg.easter_egg, true);
    }

    #[test]
    fn test_feature_easter_egg_false() {
        let cfg = config_from_args(vec!["-N".to_string(), "easter-egg".to_string()]);

        assert_eq!(cfg.easter_egg, false);
    }

    #[test]
    fn test_feature_easter_egg_both() {
        let cfg = config_from_args(vec![
                                   "-N".to_string(),
                                   "easter-egg".to_string(),
                                   "-F".to_string(),
                                   "easter-egg".to_string()]);

        assert_eq!(cfg.easter_egg, false);
    }

    #[test]
    fn test_generate_sample_zcode() {
        let cfg = config_from_args(vec!["-e".to_string()]);

        assert_eq!(cfg.test_cases.is_empty(), false);

        let mut contains = false;
        for tc in cfg.test_cases {
            if tc == TestCase::ZcodeBackend {
                contains = true;
            }
        }

        assert!(contains);
    }
}
<commit_msg>Update --force flag description<commit_after>//! Configuration for the zwreec compiler.
//!
//! Uses the `getopts` library to append a `getopts::Options` with compiler
//! specific flags and then creates a `Config` struct from `getopts::Matches`.
//!
//! # Usage
//!
//! This module depends on the [getopts](http://doc.rust-lang.org/getopts/getopts/index.html)
//! crate. To use it, you need to add `getopts` to your project. Read getopts'
//! [Usage](http://doc.rust-lang.org/getopts/getopts/index.html#usage) for more information.
//!
//! # Example
//!
//! The following example shows how to generate a `Config` struct from an empty
//! `getopts::Options`.
//!
//! ```
//! extern crate zwreec;
//! extern crate getopts;
//!
//! use zwreec::config;
//! use zwreec::config::Config;
//!
//!
//! fn main() {
//!     let args: Vec<String> = std::env::args().collect();
//!     let opts = config::zwreec_options(getopts::Options::new());
//!
//!     let matches = match opts.parse(&args[1..]) {
//!         Ok(m) => m,
//!         Err(f) => { panic!(f.to_string()) }
//!     };
//!
//!     let cfg = Config::from_matches(&matches);
//! }
//! ```
//!
//! # Appending this module (Development)
//!
//! To add new compiler flags for the compiler, you need to change three parts
//! in this module.
//!
//! 1. Appending the Struct
//!
//!     To store your new setting, you need to add it as a field to the `Config` struct. Here we
//!     add a new boolean `pub italics: bool` to control *italics*. Always add a field-comment.
//!
//!     ```ignore
//!     pub struct Config {
//!         /// Add easter egg to compiler
//!         pub easter_egg: bool,
//!         /// Instruct compiler to run these test-cases
//!         pub test_cases: Vec<TestCase>,
//!         /// Enables or disalbes Italics
//!         pub italics: bool,
//!     }
//!     ```
//!
//!     Now you also need to change `Config::default_config()` to provide the
//!     default value for your new field:
//!
//!     ```ignore
//!     pub fn default_config() -> Config {
//!         Config{
//!             easter_egg: true,
//!             test_cases: Vec::new(),
//!             italics: true,
//!         }
//!     }
//!     ```
//!
//! 2. Parse your flag in `from_matches()`
//!
//!     `from_matches` uses the Options `-F` and `-N` to enable or disable boolean flags.
//!     Therefore, if you added a new boolean flag, you can now simply add it to the match
//!     statement.
//!
//!     ```ignore
//!     for s in matches.opt_strs("F") {
//!         match s.as_ref() {
//!             "easter-egg" => {
//!                  cfg.easter_egg = true;
//!                  debug!("enabled easter-egg");
//!             },
//!             "italics" => {
//!                 cfg.italics = true;
//!                 debug!("enabbled italics");
//!             },
//!             _ => {
//!                 error!("Cannot enable feature {} - feature not known.", s);
//!             }
//!         }
//!     }
//!
//!     for s in matches.opt_strs("N") {
//!         match s.as_ref() {
//!             "easter-egg" => {
//!                 cfg.easter_egg = false;
//!                 debug!("disabled easter-egg");
//!             },
//!             "italics" => {
//!                 cfg.italics = false;
//!                 debug:("disalbed italics");
//!             }
//!             _ => {
//!                 error!("Cannot disable feature {} - feature not known.", s);
//!             }
//!         }
//!     }
//!     ```
//!
//! 3. Adding something that is not a boolean
//!
//!     If you need to add an option that is *not* a boolean, you will need to append the struct
//!     and `default_config()` accordingly (e.g. `pub notaflag: String,`). Then you need to add a new
//!     `getopts::Option` to `zwreec_options()`:
//!
//!     ```ignore
//! pub fn zwreec_options(mut opts: getopts::Options) -> getopts::Options {
//!     opts.optmulti("F", "feature", "", "FEAT");
//!     opts.optmulti("N", "no-feature", "enable or disable a feature (can occur multiple times).
//!                         List of supported features (default):
//!                             easter-egg (enabled)", "FEAT");
//!     opts.optflag("e", "generate-sample-zcode", "writes out a sample zcode file, input file is not used and can be omitted");
//!
//!     opts.optopt("n", "notaflag", "notaflag", "SOMETHING");
//!
//!     opts
//! }
//!     ```
//!
//!     Now you can append `Config::from_matches()` to analyse the provided matches for your new
//!     option and set the `Config` accordingly:
//!
//!     ```ignore
//!     if let s = matches.opt_str("n") {
//!         cfg.notaflag = s;
//!     }
//!     ```
//!
//! 4. Now you can use the new flag inside the compiler.
use getopts;

use std::vec::Vec;

/// Represents the configuration for the compiler.
///
/// This struct is created using either `config::default_config()` or
/// `config::parse_matches(matches: &getopts::Matches)`. It contains a series
/// of fields that can be used to control the behaviour of the compiler.
///
/// # Example Usage inside the compiler
///
/// ```
/// # use zwreec::config;
/// let cfg = config::Config::default_config();
///
/// if cfg.easter_egg {
///     println!("Egg!");
/// }
/// ```
pub struct Config {
    /// Force compilation despite errors
    pub force: bool,
    /// Add easter egg to compiler
    pub easter_egg: bool,
    pub force_unicode: bool,
    /// Instruct compiler to run these test-cases
    pub test_cases: Vec<TestCase>,
}

impl Config {
    /// Returns a `Config` struct with preset defaults
    ///
    /// # Example
    ///
    /// ```
    /// use zwreec::config::Config;
    /// let cfg = Config::default_config();
    /// ```
    pub fn default_config() -> Config {
        Config{
            force: false,
            easter_egg: true,
            force_unicode: false,
            test_cases: Vec::new(),
        }
    }

    /// Returns a `Config` struct by using `getopts::Matches` to set the fields.
    ///
    /// This method analyses a `getopts::Matches` for fields provided by
    /// `zwreec_options()`.
    ///
    /// # Example
    ///
    /// ```
    /// # extern crate getopts;
    /// # extern crate zwreec;
    /// let args: Vec<String> = std::env::args().collect();
    /// let opts = zwreec::config::zwreec_options(getopts::Options::new());
    ///
    /// let matches = match opts.parse(&args[1..]) {
    ///     Ok(m) => m,
    ///     Err(f) => { panic!(f.to_string()) }
    /// };
    ///
    /// let cfg = zwreec::config::Config::from_matches(&matches);
    /// ```
    pub fn from_matches(matches: &getopts::Matches) -> Config {
        // load defaults
        let mut cfg = Config::default_config();

        if matches.opt_present("generate-sample-zcode") {
            cfg.test_cases.push(TestCase::ZcodeBackend);
        }

        if matches.opt_present("f") {
            cfg.force = true;
        }

        // TODO: Find a way to make these two loops somewhat less.. repetitive
        for s in matches.opt_strs("F") {
            match s.as_ref() {
                "easter-egg" => {
                     cfg.easter_egg = true;
                     debug!("enabled easter-egg");
                },
                "force-unicode" => {
                     cfg.force_unicode = true;
                     debug!("enabled force-unicode");
                },
                _ => {
                    error!("Cannot enable feature {} - feature not known.", s);
                }
            }
        }

        for s in matches.opt_strs("N") {
            match s.as_ref() {
                "easter-egg" => {
                    cfg.easter_egg = false;
                    debug!("disabled easter-egg");
                },
                "force-unicode" => {
                     cfg.force_unicode = false;
                     debug!("enabled force-unicode");
                },
                _ => {
                    error!("Cannot disable feature {} - feature not known.", s);
                }
            }
        }

        cfg
    }
}

// TODO: If this stays only one Test Case, enum should be removed
/// The Type used to define backend tests for the compiler
#[derive(PartialEq)]
pub enum TestCase {
    /// Skips the normal compiler chain and builds an example zcode file by
    /// using every opcode.
    ZcodeBackend,
}

/// Appends a `getopts::Options` with compiler specific flags
///
/// The method `Config::from_matches()` looks for very specific `getopts::Matches`.
/// This function takes an `getopts::Options` to append it with Options required
/// by `from_matches`. It currently adds three fields:
///
/// ```ignore
/// opts.optmulti("F", "feature", "", "FEAT");
/// opts.optmulti("N", "no-feature", "enable or disable a feature (can occur multiple times).
///                     List of supported features (default):
///                         easter-egg (enabled)", "FEAT");
/// opts.optflag("e", "generate-sample-zcode", "writes out a sample zcode file, input file is not used and can be omitted");
/// ```
///
/// # Example
///
/// You can use this function to append your `getopts::Options`.
///
/// ```
/// # extern crate getopts;
/// # extern crate zwreec;
///
/// let mut opts = getopts::Options::new();
/// opts.optflag("h", "help", "print this message");
///
/// let opts = zwreec::config::zwreec_options(opts);
/// ```
///
/// Another useful example is to use it to gernerate a more compact usage by
/// having a function that only returns your options.
///
/// ```
/// # extern crate getopts;
/// # extern crate zwreec;
///
/// fn options() -> getopts::Options {
///     let mut opts = getopts::Options::new();
///     opts.optflag("h", "help", "display this help and exit");
///     opts.optflag("V", "version", "display version");
///
///     opts
/// }
///
/// fn print_usage(program: &str, verbose: bool) {
///     let brief = format!("Usage: {} [options]", program);
///
///     let opts = if verbose {
///         zwreec::config::zwreec_options(options())
///     } else {
///         options()
///     };
///
///     print!("{}", opts.usage(&brief));
/// }
/// ```
/// As you can see, `options()` returns your own command line options, which are then conditionally
/// expanded by using `zwreec_options()`.
pub fn zwreec_options(mut opts: getopts::Options) -> getopts::Options {
    opts.optflag("f", "force", "Try ignoring any errors that may occur and generate Z-Code anyways.
        This feature is highly unstable and may lead to corrupt output files.");
    opts.optmulti("F", "feature", "", "FEAT");
    opts.optmulti("N", "no-feature", "Enable or disable a feature (can occur multiple times).
        For more information about the supported features run --help with -v and see the feature
        list at the end of the output", "FEAT");
    opts.optflag("e", "generate-sample-zcode", "Write out a sample zcode file, input file is not used and can be omitted");

    opts
}

pub fn zwreec_usage(verbose: bool, opts: getopts::Options, brief: &str) -> String {
    use std::fmt::format;
    let options = zwreec_options(opts);

    let options_usage = options.usage(brief);

    let features_usage = if verbose {
        "List of supported features (default value in parenthesis)
    easter-egg (enabled)
        Enables the generation of easter egg code. Enter the secret combination
        in your Z-Machine interpreter to activate the easter egg
    force-unicode (disabled)
        Force the generation of unicode print opcodes every time a unicode
        character is encountered. This disables the generation of the unicode
        translation table"
    } else {
        "Additional help:
    --help -v           Print the full set of options zwreec accepts"
    };

    format(format_args!("{}\n{}\n", options_usage, features_usage.to_string()))
}

#[cfg(test)]
mod tests {
    use super::*;
    use getopts;

    fn config_from_args(args: Vec<String>) -> Config {
        let opts = zwreec_options(getopts::Options::new());

        match opts.parse(&args) {
            Ok(m) => Config::from_matches(&m),
            Err(f) => { panic!(f.to_string()) }
        }
    }

    #[test]
    fn test_feature_easter_egg_true() {
        let cfg = config_from_args(vec!["-F".to_string(), "easter-egg".to_string()]);

        assert_eq!(cfg.easter_egg, true);
    }

    #[test]
    fn test_feature_easter_egg_false() {
        let cfg = config_from_args(vec!["-N".to_string(), "easter-egg".to_string()]);

        assert_eq!(cfg.easter_egg, false);
    }

    #[test]
    fn test_feature_easter_egg_both() {
        let cfg = config_from_args(vec![
                                   "-N".to_string(),
                                   "easter-egg".to_string(),
                                   "-F".to_string(),
                                   "easter-egg".to_string()]);

        assert_eq!(cfg.easter_egg, false);
    }

    #[test]
    fn test_generate_sample_zcode() {
        let cfg = config_from_args(vec!["-e".to_string()]);

        assert_eq!(cfg.test_cases.is_empty(), false);

        let mut contains = false;
        for tc in cfg.test_cases {
            if tc == TestCase::ZcodeBackend {
                contains = true;
            }
        }

        assert!(contains);
    }
}
<|endoftext|>