├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── question.md │ ├── feature-request.md │ ├── bug-report.md │ └── task.md └── workflows │ ├── ci.yml │ └── release.yml ├── crates ├── common │ ├── src │ │ ├── lib.rs │ │ ├── span.rs │ │ └── source.rs │ └── Cargo.toml ├── scanner │ ├── src │ │ ├── lib.rs │ │ ├── scanner_error.rs │ │ ├── source_iter.rs │ │ ├── token.rs │ │ └── scanner.rs │ └── Cargo.toml ├── parser │ ├── src │ │ ├── lib.rs │ │ ├── scope_tracker.rs │ │ ├── parser_error.rs │ │ ├── token_iter.rs │ │ └── ast.rs │ └── Cargo.toml ├── prelude │ └── Cargo.toml ├── reporting │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── os-platform │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── tenda-playground-platform │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── tenda-playground │ ├── Cargo.toml │ └── src │ │ ├── protocol_message.rs │ │ └── main.rs ├── runtime │ ├── src │ │ ├── lib.rs │ │ ├── associative_array.rs │ │ ├── frame.rs │ │ ├── platform.rs │ │ ├── environment.rs │ │ ├── function.rs │ │ ├── stack.rs │ │ ├── date.rs │ │ ├── value.rs │ │ └── runtime_error.rs │ └── Cargo.toml ├── tenda │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── core │ ├── src │ │ └── lib.rs │ └── Cargo.toml └── reporting-derive │ └── Cargo.toml ├── tests ├── Cargo.toml └── src │ ├── lib.rs │ ├── syntax.rs │ ├── ops.rs │ └── core.rs ├── .gitignore ├── Cargo.toml ├── WORKFLOW.md ├── README.md ├── README.en.md └── scripts ├── install.ps1 └── install.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [gabrielbrunop] 2 | -------------------------------------------------------------------------------- /crates/common/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod source; 2 | pub mod span; 3 | -------------------------------------------------------------------------------- /crates/scanner/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod scanner; 2 | mod scanner_error; 3 | mod source_iter; 4 | mod token; 5 | 6 | pub use scanner::*; 7 | pub use scanner_error::*; 8 | pub use token::*; 9 | -------------------------------------------------------------------------------- /crates/parser/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod ast; 2 | mod closures; 3 | mod parser; 4 | mod parser_error; 5 | mod scope_tracker; 6 | mod token_iter; 7 | 8 | pub use parser::*; 9 | pub use parser_error::*; 10 | -------------------------------------------------------------------------------- /tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tests" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition = "2021" 6 | publish = false 7 | 8 | [dependencies] 9 | tenda-core = { path = "../crates/core" } 10 | rstest = "0.25.0" 11 | -------------------------------------------------------------------------------- /crates/prelude/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tenda-prelude" 3 | version.workspace = true 4 | authors.workspace = true 5 | license.workspace = true 6 | repository.workspace = true 7 | description = "Prelude for the Tenda programming language" 8 | edition = "2021" 9 | 10 | [dependencies] 11 | tenda-runtime = { workspace = true } 12 | indexmap = { workspace = true } 13 | -------------------------------------------------------------------------------- /crates/reporting/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tenda-reporting" 3 | version.workspace = true 4 | authors.workspace = true 5 | license.workspace = true 6 | repository.workspace = true 7 | description = "Reporting utilities for the Tenda programming language" 8 | edition = "2021" 9 | 10 | [dependencies] 11 | thiserror = { workspace = true } 12 | aegean = "0.6.0" 13 | -------------------------------------------------------------------------------- /crates/common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tenda-common" 3 | version.workspace = true 4 | authors.workspace = true 5 | license.workspace = true 6 | repository.workspace = true 7 | description = "Common utilities for the Tenda programming language" 8 | edition = "2021" 9 | 10 | [dependencies] 11 | tenda-reporting = { workspace = true } 12 | thiserror = { workspace = true } 13 | -------------------------------------------------------------------------------- /crates/os-platform/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tenda-os-platform" 3 | version.workspace = true 4 | authors.workspace = true 5 | license.workspace = true 6 | repository.workspace = true 7 | description = "Operating system platform support for the Tenda programming language" 8 | edition = "2021" 9 | 10 | [dependencies] 11 | tenda-runtime = { workspace = true } 12 | chrono = { workspace = true } 13 | rand = "0.9.0" 14 | -------------------------------------------------------------------------------- /crates/tenda-playground-platform/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tenda-playground-platform" 3 | version = "0.0.0" 4 | authors.workspace = true 5 | license.workspace = true 6 | repository.workspace = true 7 | publish = false 8 | edition = "2021" 9 | 10 | [dependencies] 11 | tenda-runtime = { version = "0.1.0", path = "../runtime" } 12 | tenda-os-platform = { version = "0.1.0", path = "../os-platform" } 13 | delegate = "0.13.3" 14 | -------------------------------------------------------------------------------- /crates/scanner/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tenda-scanner" 3 | version.workspace = true 4 | authors.workspace = true 5 | license.workspace = true 6 | repository.workspace = true 7 | description = "Scanner for the Tenda programming language" 8 | edition = "2021" 9 | 10 | [dependencies] 11 | tenda-common = { workspace = true } 12 | tenda-reporting-derive = { workspace = true } 13 | tenda-reporting = { workspace = true } 14 | thiserror = { workspace = true } 15 | -------------------------------------------------------------------------------- /crates/tenda-playground/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tenda-playground" 3 | version = "0.0.0" 4 | authors.workspace = true 5 | license.workspace = true 6 | repository.workspace = true 7 | publish = false 8 | edition = "2021" 9 | 10 | [dependencies] 11 | tenda-core = { version = "0.1.0", path = "../core" } 12 | tenda-playground-platform = { path = "../tenda-playground-platform" } 13 | serde_json = "1.0" 14 | serde = { version = "1.0", features = ["derive"] } 15 | -------------------------------------------------------------------------------- /crates/runtime/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod associative_array; 2 | mod date; 3 | mod environment; 4 | mod frame; 5 | mod function; 6 | mod platform; 7 | mod runtime; 8 | mod runtime_error; 9 | mod stack; 10 | mod value; 11 | 12 | pub use associative_array::*; 13 | pub use date::*; 14 | pub use environment::*; 15 | pub use frame::*; 16 | pub use function::*; 17 | pub use platform::*; 18 | pub use runtime::*; 19 | pub use runtime_error::*; 20 | pub use stack::*; 21 | pub use value::*; 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pergunta / Ajuda 3 | about: Tirar dúvidas ou pedir ajuda com o projeto 4 | title: "[Pergunta] " 5 | labels: ["dúvida"] 6 | assignees: [] 7 | --- 8 | 9 | ## Pergunta 10 | 11 | 12 | 13 | ## Contexto 14 | 15 | 16 | 17 | ## Ambiente (opcional) 18 | 19 | - SO/Versão: 20 | - Versão da Tenda (`tenda --version`): 21 | -------------------------------------------------------------------------------- /crates/tenda/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tenda" 3 | version.workspace = true 4 | authors.workspace = true 5 | description.workspace = true 6 | keywords.workspace = true 7 | categories.workspace = true 8 | readme.workspace = true 9 | license.workspace = true 10 | repository.workspace = true 11 | edition = "2021" 12 | default-run = "tenda" 13 | 14 | [dependencies] 15 | tenda-core = { workspace = true } 16 | reedline = "0.39.0" 17 | yansi = "1.0" 18 | clap = { version = "4.5", features = ["derive"] } 19 | -------------------------------------------------------------------------------- /crates/core/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod runtime { 2 | pub use ::tenda_runtime::*; 3 | } 4 | 5 | pub mod parser { 6 | pub use ::tenda_parser::*; 7 | } 8 | 9 | pub mod scanner { 10 | pub use ::tenda_scanner::*; 11 | } 12 | 13 | pub mod common { 14 | pub use tenda_common::*; 15 | } 16 | 17 | pub mod reporting { 18 | pub use ::tenda_reporting::*; 19 | } 20 | 21 | pub mod prelude { 22 | pub use ::tenda_prelude::*; 23 | } 24 | 25 | pub mod platform { 26 | pub use ::tenda_os_platform::Platform as OSPlatform; 27 | } 28 | -------------------------------------------------------------------------------- /crates/parser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tenda-parser" 3 | version.workspace = true 4 | authors.workspace = true 5 | license.workspace = true 6 | repository.workspace = true 7 | description = "Parser for the Tenda programming language" 8 | edition = "2021" 9 | 10 | [dependencies] 11 | tenda-scanner = { workspace = true } 12 | tenda-common = { workspace = true } 13 | tenda-reporting = { workspace = true } 14 | tenda-reporting-derive = { workspace = true } 15 | thiserror = { workspace = true } 16 | peekmore = { workspace = true } 17 | -------------------------------------------------------------------------------- /crates/core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tenda-core" 3 | version.workspace = true 4 | authors.workspace = true 5 | license.workspace = true 6 | repository.workspace = true 7 | description = "Core functionalities for the Tenda programming language" 8 | edition = "2021" 9 | 10 | [dependencies] 11 | tenda-runtime = { workspace = true } 12 | tenda-parser = { workspace = true } 13 | tenda-scanner = { workspace = true } 14 | tenda-common = { workspace = true } 15 | tenda-reporting = { workspace = true } 16 | tenda-prelude = { workspace = true } 17 | tenda-os-platform = { workspace = true } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | # Idea files 17 | .idea/ 18 | 19 | # Visual Studio Code files 20 | .vscode/ 21 | 22 | # Temporary, quick tests 23 | test.tnd -------------------------------------------------------------------------------- /crates/runtime/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tenda-runtime" 3 | version.workspace = true 4 | authors.workspace = true 5 | license.workspace = true 6 | repository.workspace = true 7 | description = "Runtime support for the Tenda programming language" 8 | edition = "2021" 9 | 10 | [dependencies] 11 | tenda-parser = { workspace = true } 12 | tenda-scanner = { workspace = true } 13 | tenda-common = { workspace = true } 14 | tenda-reporting = { workspace = true } 15 | tenda-reporting-derive = { workspace = true } 16 | thiserror = { workspace = true } 17 | chrono = { workspace = true } 18 | indexmap = { workspace = true } 19 | chrono-tz = "0.10.1" 20 | -------------------------------------------------------------------------------- /crates/runtime/src/associative_array.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::fmt::Display; 3 | 4 | use crate::value::Value; 5 | 6 | pub type AssociativeArray = indexmap::IndexMap; 7 | 8 | #[derive(Debug, Clone, Hash, Eq, PartialEq)] 9 | pub enum AssociativeArrayKey { 10 | String(String), 11 | Number(i64), 12 | } 13 | 14 | impl Display for AssociativeArrayKey { 15 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 16 | match self { 17 | AssociativeArrayKey::String(key) => write!(f, "{}", key), 18 | AssociativeArrayKey::Number(key) => write!(f, "{}", key), 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /crates/reporting-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tenda-reporting-derive" 3 | version.workspace = true 4 | authors.workspace = true 5 | license.workspace = true 6 | repository.workspace = true 7 | description = "Procedural macros for Tenda reporting utilities" 8 | edition = "2021" 9 | 10 | [dependencies] 11 | tenda-common = { workspace = true } 12 | tenda-reporting = { workspace = true } 13 | thiserror = { workspace = true } 14 | 15 | [dependencies.syn] 16 | version = "2.0" 17 | features = ["derive", "parsing"] 18 | 19 | [dependencies.quote] 20 | version = "1.0" 21 | 22 | [dependencies.proc-macro2] 23 | version = "1.0" 24 | 25 | [lib] 26 | proc-macro = true 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Proposta de Funcionalidade 3 | about: Sugerir uma nova funcionalidade ou melhoria 4 | title: "[Melhoria] " 5 | labels: ["proposta"] 6 | assignees: [] 7 | --- 8 | 9 | ## Problema ou motivação 10 | 11 | 12 | 13 | ## Proposta de solução 14 | 15 | 16 | 17 | ## Alternativas consideradas 18 | 19 | 20 | 21 | ## Impacto esperado 22 | 23 | 24 | 25 | ## Contexto adicional 26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Relatório de Bug 3 | about: Relatar um comportamento incorreto ou inesperado 4 | title: "[Bug] " 5 | labels: ["bug"] 6 | assignees: [] 7 | --- 8 | 9 | ## Descrição 10 | 11 | 12 | 13 | ## Passos para reproduzir 14 | 15 | 1. … 16 | 2. … 17 | 3. … 18 | 19 | ## Comportamento atual 20 | 21 | 22 | 23 | ## Comportamento esperado 24 | 25 | 26 | 27 | ## Ambiente 28 | 29 | - SO/Versão: 30 | - Versão da Tenda (`tenda --version`): 31 | - Outras dependências relevantes: 32 | 33 | ## Observações adicionais 34 | 35 | 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/task.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Tarefa 3 | about: Uma tarefa específica a ser realizada 4 | title: "[Tarefa] " 5 | labels: ["tarefa"] 6 | --- 7 | 8 | ### Descrição 9 | 10 | 11 | 12 | ### Critérios de Aceitação 13 | 14 | 15 | 16 | - [ ] Critério 1 17 | - [ ] Critério 2 18 | - [ ] Critério 3 19 | 20 | ### Subtarefas 21 | 22 | 23 | 24 | - [ ] Subtarefa 1 25 | - [ ] Subtarefa 2 26 | - [ ] Subtarefa 3 27 | 28 | ### Issues Relacionadas 29 | 30 | 31 | 32 | - Issue #1 33 | - Issue #2 34 | 35 | ### Contexto Adicional 36 | 37 | 38 | -------------------------------------------------------------------------------- /crates/runtime/src/frame.rs: -------------------------------------------------------------------------------- 1 | use crate::environment::{Environment, ValueCell}; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct Frame { 5 | env: Environment, 6 | return_value: Option, 7 | } 8 | 9 | impl Frame { 10 | pub fn new() -> Self { 11 | Frame { 12 | env: Environment::new(), 13 | return_value: None, 14 | } 15 | } 16 | 17 | pub fn from_env(env: Environment) -> Self { 18 | Frame { 19 | env, 20 | return_value: None, 21 | } 22 | } 23 | 24 | pub fn get_env(&self) -> &Environment { 25 | &self.env 26 | } 27 | 28 | pub fn get_env_mut(&mut self) -> &mut Environment { 29 | &mut self.env 30 | } 31 | 32 | pub fn set_return_value(&mut self, value: ValueCell) { 33 | self.return_value = Some(value); 34 | } 35 | 36 | pub fn get_return_value(&self) -> Option<&ValueCell> { 37 | self.return_value.as_ref() 38 | } 39 | 40 | pub fn clear_return_value(&mut self) { 41 | self.return_value = None; 42 | } 43 | } 44 | 45 | impl Default for Frame { 46 | fn default() -> Self { 47 | Self::new() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["crates/*", "tests"] 3 | resolver = "2" 4 | 5 | [workspace.dependencies] 6 | tenda-reporting = { path = "crates/reporting", version = "0.1.0" } 7 | tenda-reporting-derive = { path = "crates/reporting-derive", version = "0.1.0" } 8 | tenda-core = { path = "crates/core", version = "0.1.0" } 9 | tenda-common = { path = "crates/common", version = "0.1.0" } 10 | tenda-parser = { path = "crates/parser", version = "0.1.0" } 11 | tenda-scanner = { path = "crates/scanner", version = "0.1.0" } 12 | tenda-runtime = { path = "crates/runtime", version = "0.1.0" } 13 | tenda-prelude = { path = "crates/prelude", version = "0.1.0" } 14 | tenda-os-platform = { path = "crates/os-platform", version = "0.1.0" } 15 | 16 | peekmore = "1.3.0" 17 | thiserror = "1.0" 18 | chrono = "0.4.40" 19 | indexmap = "1.7.0" 20 | 21 | [workspace.package] 22 | version = "0.1.0" 23 | authors = ["Gabriel Bruno Oliveira Pereira "] 24 | license = "GPL-3.0-or-later" 25 | description = "Tenda - a programming language for Portuguese speakers" 26 | repository = "https://github.com/gabrielbrunop/tenda" 27 | readme = "README.en.md" 28 | keywords = ["language", "interpreter", "cli"] 29 | categories = ["command-line-utilities", "parsing", "compilers"] 30 | -------------------------------------------------------------------------------- /crates/runtime/src/platform.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | #[non_exhaustive] 4 | pub enum FileErrorKind { 5 | NotFound, 6 | PermissionDenied, 7 | AlreadyExists, 8 | Other, 9 | } 10 | 11 | pub trait Platform: Debug { 12 | fn println(&self, message: &str); 13 | fn print(&self, message: &str); 14 | fn write(&self, message: &str); 15 | fn read_line(&self) -> String; 16 | fn rand(&self) -> f64; 17 | fn read_file(&self, path: &str) -> Result; 18 | fn write_file(&self, path: &str, content: &str) -> Result<(), FileErrorKind>; 19 | fn remove_file(&self, path: &str) -> Result<(), FileErrorKind>; 20 | fn list_files(&self, path: &str) -> Result, FileErrorKind>; 21 | fn create_dir(&self, path: &str) -> Result<(), FileErrorKind>; 22 | fn remove_dir(&self, path: &str) -> Result<(), FileErrorKind>; 23 | fn list_dirs(&self, path: &str) -> Result, FileErrorKind>; 24 | fn current_dir(&self) -> Result; 25 | fn file_append(&self, path: &str, content: &str) -> Result<(), FileErrorKind>; 26 | fn args(&self) -> Vec; 27 | fn exit(&self, code: i32); 28 | fn sleep(&self, seconds: f64); 29 | fn date_now(&self) -> i64; 30 | fn timezone_offset(&self) -> i32; 31 | } 32 | -------------------------------------------------------------------------------- /crates/parser/src/scope_tracker.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cell::{RefCell, RefMut}, 3 | rc::Rc, 4 | }; 5 | 6 | #[derive(Debug, Clone, PartialEq)] 7 | pub enum BlockScope { 8 | If, 9 | Else, 10 | Loop, 11 | Function, 12 | Global, 13 | } 14 | 15 | #[derive(Clone)] 16 | pub struct ScopeTracker(Rc>>); 17 | 18 | impl ScopeTracker { 19 | pub fn new() -> ScopeTracker { 20 | ScopeTracker(Rc::new(RefCell::new(vec![BlockScope::Global]))) 21 | } 22 | 23 | pub fn get(&self) -> RefMut> { 24 | self.0.as_ref().borrow_mut() 25 | } 26 | 27 | pub fn guard(&self, scope: BlockScope) -> ScopeGuard { 28 | ScopeGuard::new(self.clone(), scope) 29 | } 30 | 31 | pub fn has_scope(&self, scope: BlockScope) -> bool { 32 | self.get().contains(&scope) 33 | } 34 | } 35 | 36 | impl Default for ScopeTracker { 37 | fn default() -> Self { 38 | Self::new() 39 | } 40 | } 41 | 42 | pub struct ScopeGuard { 43 | stack: ScopeTracker, 44 | } 45 | 46 | impl ScopeGuard { 47 | pub fn new(stack: ScopeTracker, scope: BlockScope) -> ScopeGuard { 48 | stack.get().push(scope); 49 | ScopeGuard { stack } 50 | } 51 | } 52 | 53 | impl Drop for ScopeGuard { 54 | fn drop(&mut self) { 55 | self.stack.get().pop(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /crates/scanner/src/scanner_error.rs: -------------------------------------------------------------------------------- 1 | use tenda_common::span::SourceSpan; 2 | use tenda_reporting_derive::Diagnostic; 3 | use thiserror::Error; 4 | 5 | #[derive(Error, Debug, PartialEq, Clone, Diagnostic)] 6 | #[report("erro léxico")] 7 | pub enum LexicalError { 8 | #[error("zeros à esquerda em literais numéricos não são permitidos")] 9 | LeadingZeroNumberLiterals { 10 | #[span] 11 | span: SourceSpan, 12 | }, 13 | 14 | #[error("fim de linha inesperado em texto")] 15 | UnexpectedStringEol { 16 | #[span] 17 | span: SourceSpan, 18 | }, 19 | 20 | #[error("caractere inesperado: {}", .character)] 21 | UnexpectedChar { 22 | character: char, 23 | #[span] 24 | span: SourceSpan, 25 | }, 26 | 27 | #[error("fim inesperado de entrada")] 28 | UnexpectedEoi { 29 | #[span] 30 | span: SourceSpan, 31 | }, 32 | 33 | #[error("escape hexadecimal inválido")] 34 | InvalidHexEscape { 35 | #[span] 36 | span: SourceSpan, 37 | }, 38 | 39 | #[error("escape octal inválido")] 40 | InvalidOctalEscape { 41 | #[span] 42 | span: SourceSpan, 43 | }, 44 | 45 | #[error("escape unicode inválido")] 46 | InvalidUnicodeEscape { 47 | #[span] 48 | span: SourceSpan, 49 | }, 50 | 51 | #[error("escape não reconhecido: {}", .found)] 52 | UnknownEscape { 53 | #[span] 54 | span: SourceSpan, 55 | found: char, 56 | }, 57 | } 58 | -------------------------------------------------------------------------------- /crates/tenda-playground/src/protocol_message.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use tenda_playground_platform::ProtocolMessage; 3 | 4 | #[derive(Serialize, Deserialize)] 5 | #[serde(tag = "type")] 6 | pub enum JsonProtocolMessage { 7 | #[serde(rename = "ready")] 8 | Ready, 9 | #[serde(rename = "unlock")] 10 | Unlock, 11 | #[serde(rename = "output")] 12 | Output { payload: String }, 13 | #[serde(rename = "result")] 14 | Result { value_type: String, value: String }, 15 | #[serde(rename = "error")] 16 | Error { payload: Vec }, 17 | } 18 | 19 | impl From for JsonProtocolMessage { 20 | fn from(message: ProtocolMessage) -> Self { 21 | use ProtocolMessage::*; 22 | 23 | match message { 24 | Ready => JsonProtocolMessage::Ready, 25 | Unlock => JsonProtocolMessage::Unlock, 26 | Output(output) => JsonProtocolMessage::Output { payload: output }, 27 | Result(value_type, value) => JsonProtocolMessage::Result { 28 | value_type: value_type.to_string(), 29 | value, 30 | }, 31 | Error(message) => JsonProtocolMessage::Error { payload: message }, 32 | } 33 | } 34 | } 35 | 36 | impl std::fmt::Display for JsonProtocolMessage { 37 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 38 | let json_string = serde_json::to_string(self).unwrap(); 39 | write!(f, "{}", json_string) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /crates/common/src/span.rs: -------------------------------------------------------------------------------- 1 | use crate::source::IdentifiedSource; 2 | 3 | pub trait Span: Clone + std::fmt::Debug + PartialEq + tenda_reporting::Span { 4 | fn start(&self) -> usize; 5 | fn end(&self) -> usize; 6 | fn source(&self) -> IdentifiedSource; 7 | fn extract(&self, source: &str) -> String; 8 | } 9 | 10 | #[derive(Debug, Clone, PartialEq)] 11 | pub struct SourceSpan { 12 | start: usize, 13 | end: usize, 14 | source: IdentifiedSource, 15 | label: Option, 16 | } 17 | 18 | impl SourceSpan { 19 | pub fn new(start: usize, end: usize, source: IdentifiedSource) -> Self { 20 | SourceSpan { 21 | start, 22 | end, 23 | source, 24 | label: None, 25 | } 26 | } 27 | 28 | pub fn with_label(mut self, label: String) -> Self { 29 | self.label = Some(label); 30 | self 31 | } 32 | 33 | pub fn label(&self) -> Option<&String> { 34 | self.label.as_ref() 35 | } 36 | } 37 | 38 | impl tenda_reporting::Span for SourceSpan { 39 | type SourceId = IdentifiedSource; 40 | 41 | fn source(&self) -> &Self::SourceId { 42 | &self.source 43 | } 44 | 45 | fn start(&self) -> usize { 46 | self.start 47 | } 48 | 49 | fn end(&self) -> usize { 50 | self.end 51 | } 52 | } 53 | 54 | impl Span for SourceSpan { 55 | fn start(&self) -> usize { 56 | self.start 57 | } 58 | 59 | fn end(&self) -> usize { 60 | self.end 61 | } 62 | 63 | fn source(&self) -> IdentifiedSource { 64 | self.source 65 | } 66 | 67 | fn extract(&self, source: &str) -> String { 68 | source[self.start..self.end].to_string() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /WORKFLOW.md: -------------------------------------------------------------------------------- 1 | # Workflow 2 | 3 | Este documento descreve o fluxo de trabalho Git e CI/CD usado neste projeto. 4 | 5 | ## Visão geral 6 | 7 | ``` 8 | feat/* → dev → main → tag vX.Y.Z → Publicação 9 | ``` 10 | 11 | - `feat/*`: Desenvolvimento individual de funcionalidades e correções. 12 | - `dev`: Branch para integração contínua dos recursos. 13 | - `main`: Branch contendo a última versão estável. 14 | - `tag vX.Y.Z`: Tag SemVer que dispara a publicação via CI/CD. 15 | 16 | ## Branches 17 | 18 | | Branch | Descrição | 19 | | -------- | ------------------------------- | 20 | | `feat/*` | Funcionalidades e correções | 21 | | `dev` | Integração contínua | 22 | | `main` | Última versão estável publicada | 23 | 24 | ## Desenvolvimento 25 | 26 | ### Nova funcionalidade 27 | 28 | ```bash 29 | git switch dev 30 | git pull 31 | git switch -c feat/nova-funcionalidade 32 | # Desenvolva e faça commits... 33 | git push origin feat/nova-funcionalidade 34 | ``` 35 | 36 | Abra pull request para `dev`. 37 | 38 | ### Preparação de release 39 | 40 | No branch `dev`: 41 | 42 | ```bash 43 | cargo set-version minor --workspace 44 | git commit -am "chore: vX.Y.Z" 45 | git push 46 | ``` 47 | 48 | Abra pull request para `main`. Após revisão e CI aprovados, faça merge. 49 | 50 | ### Gerando a versão oficial 51 | 52 | No branch `main`: 53 | 54 | ```bash 55 | git tag -a vX.Y.Z -m "Versão vX.Y.Z" 56 | git push origin vX.Y.Z 57 | ``` 58 | 59 | ## Contribuições externas 60 | 61 | Colaboradores externos normalmente fariam um fork e criariam um branch `feat/` a partir de `dev`, para depois abrir um pull request; **no entanto, atualmente este projeto está fechado a contribuições externas**. Se você tiver sugestões ou encontrar bugs, por favor abra uma **issue** no repositório do GitHub. 62 | 63 | ## Fluxo resumido 64 | 65 | ``` 66 | feat/* (CI/Testes) → dev (CI/Testes) → main (CI/Testes) → tag vX.Y.Z → CI/CD Publicação 67 | ``` 68 | -------------------------------------------------------------------------------- /crates/scanner/src/source_iter.rs: -------------------------------------------------------------------------------- 1 | use std::iter::Peekable; 2 | use std::str::Chars; 3 | use tenda_common::{source::IdentifiedSource, span::SourceSpan}; 4 | 5 | use crate::token::{Literal, Token, TokenKind}; 6 | 7 | pub struct SourceIter<'a> { 8 | iter: Peekable>, 9 | source_id: IdentifiedSource, 10 | start_position: usize, 11 | end_position: usize, 12 | } 13 | 14 | impl<'a> SourceIter<'a> { 15 | pub fn new(input: &'a str, source_id: IdentifiedSource) -> Self { 16 | Self { 17 | iter: input.chars().peekable(), 18 | source_id, 19 | start_position: 0, 20 | end_position: 0, 21 | } 22 | } 23 | 24 | pub fn consume_token(&mut self, kind: TokenKind, lexeme: &str) -> Token { 25 | Token::new(kind, lexeme.to_string(), None, self.consume_span()) 26 | } 27 | 28 | pub fn consume_token_with_literal( 29 | &mut self, 30 | kind: TokenKind, 31 | lexeme: String, 32 | literal: Literal, 33 | ) -> Token { 34 | Token::new(kind, lexeme, Some(literal), self.consume_span()) 35 | } 36 | 37 | pub fn consume_eof(&mut self) -> Token { 38 | Token::eoi(self.consume_span()) 39 | } 40 | 41 | pub fn consume_span(&mut self) -> SourceSpan { 42 | let span = SourceSpan::new(self.start_position, self.end_position, self.source_id); 43 | 44 | self.start_position = self.end_position; 45 | 46 | span 47 | } 48 | 49 | pub fn ignore_char(&mut self) { 50 | self.start_position = self.end_position; 51 | } 52 | 53 | pub fn peek(&mut self) -> Option<&char> { 54 | self.iter.peek() 55 | } 56 | } 57 | 58 | impl Iterator for SourceIter<'_> { 59 | type Item = char; 60 | 61 | fn next(&mut self) -> Option { 62 | if let Some(c) = self.iter.next() { 63 | self.end_position += c.len_utf8(); 64 | 65 | Some(c) 66 | } else { 67 | None 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Português](https://img.shields.io/badge/idioma-pt--BR-green)](README.md) 2 | [![English](https://img.shields.io/badge/lang-en-blue)](README.en.md) 3 | 4 | # Tenda 5 | 6 | [![Status](https://img.shields.io/badge/status-em%20desenvolvimento-yellow)](https://tenda.dev/) 7 | [![License: GPL-3.0](https://img.shields.io/badge/licença-GPLv3-blue)](LICENSE) 8 | 9 | ## Recursos 10 | 11 | - 🌐 **Site Oficial:** [tenda.dev](https://tenda.dev/) 12 | - 📚 **Documentação:** [tenda.dev/docs](https://tenda.dev/docs) 13 | - 🎯 **Playground:** [tenda.dev/playground](https://tenda.dev/playground) 14 | 15 | ## Descrição 16 | 17 | **Tenda** é uma linguagem de programação moderna em português que tem como objetivo tornar a programação mais acessível. Para isso, utiliza palavras-chave em português e uma sintaxe próxima da linguagem natural, permitindo uma _leitura narrativa_ do código. Essa abordagem busca reduzir a barreira de entrada para iniciantes e educadores nos países lusófonos. 18 | 19 | ## Instalação 20 | 21 | ### macOS / Linux 22 | 23 | ```bash 24 | curl -fsSL https://tenda.dev/instalar | bash 25 | ``` 26 | 27 | ### Windows (PowerShell) 28 | 29 | ```powershell 30 | iwr https://tenda.dev/instalar.ps1 -UseBasicParsing | iex 31 | ``` 32 | 33 | _Por padrão, a Tenda é instalada em `$HOME/.tenda/bin` (ou `%USERPROFILE%\.tenda\bin` no Windows) e seu perfil de usuário é atualizado automaticamente com:_ 34 | 35 | ```bash 36 | export TENDA_INSTALL="$HOME/.tenda" 37 | export PATH="$TENDA_INSTALL/bin:$PATH" 38 | ``` 39 | 40 | ## Mantenedores 41 | 42 | - [@gabrielbrunop](https://github.com/gabrielbrunop) 43 | 44 | ## Contribuindo 45 | 46 | No momento, as contribuições externas estão fechadas. Isto significa que o recebimento de pull requests não está aberto ao público. Se você tiver sugestões ou encontrar bugs, sinta-se à vontade para abrir uma issue no nosso repositório do GitHub. 47 | 48 | ## Licença 49 | 50 | Tenda é distribuída sob a licença GNU General Public License v3.0. Para mais detalhes, consulte o arquivo LICENSE no repositório. 51 | -------------------------------------------------------------------------------- /crates/common/src/source.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::Display, 3 | hash::Hash, 4 | sync::atomic::{AtomicUsize, Ordering}, 5 | }; 6 | 7 | static NEXT_ID: AtomicUsize = AtomicUsize::new(1); 8 | static DUMMY_ID: UniqueId = UniqueId(0); 9 | 10 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 11 | pub struct UniqueId(usize); 12 | 13 | impl UniqueId { 14 | pub fn new() -> Self { 15 | UniqueId(NEXT_ID.fetch_add(1, Ordering::Relaxed)) 16 | } 17 | } 18 | 19 | impl Default for UniqueId { 20 | fn default() -> Self { 21 | Self::new() 22 | } 23 | } 24 | 25 | #[derive(Debug, Clone, Copy, Eq)] 26 | pub struct IdentifiedSource { 27 | id: UniqueId, 28 | name: Option<&'static str>, 29 | } 30 | 31 | impl IdentifiedSource { 32 | pub fn new() -> Self { 33 | IdentifiedSource { 34 | id: UniqueId::new(), 35 | name: None, 36 | } 37 | } 38 | 39 | pub fn dummy() -> Self { 40 | IdentifiedSource { 41 | id: DUMMY_ID, 42 | name: None, 43 | } 44 | } 45 | 46 | pub fn set_name(&mut self, name: &'static str) { 47 | self.name = Some(name); 48 | } 49 | 50 | pub fn id(&self) -> UniqueId { 51 | self.id 52 | } 53 | 54 | pub fn name(&self) -> Option<&'static str> { 55 | self.name 56 | } 57 | } 58 | 59 | impl Default for IdentifiedSource { 60 | fn default() -> Self { 61 | Self::new() 62 | } 63 | } 64 | 65 | impl PartialEq for IdentifiedSource { 66 | fn eq(&self, other: &Self) -> bool { 67 | self.id == other.id 68 | } 69 | } 70 | 71 | impl Hash for IdentifiedSource { 72 | fn hash(&self, state: &mut H) { 73 | self.id.hash(state); 74 | } 75 | } 76 | 77 | impl Display for IdentifiedSource { 78 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 79 | if let Some(name) = self.name { 80 | write!(f, "{}", name) 81 | } else { 82 | write!(f, "", self.id.0) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /crates/runtime/src/environment.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, collections::HashMap, rc::Rc}; 2 | 3 | use super::value::Value; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct Environment { 7 | state: HashMap, 8 | } 9 | 10 | impl Environment { 11 | pub fn new() -> Self { 12 | Environment { 13 | state: HashMap::new(), 14 | } 15 | } 16 | 17 | pub fn get(&self, name: &str) -> Option<&ValueCell> { 18 | self.state.get(name) 19 | } 20 | 21 | pub fn has(&self, name: &String) -> bool { 22 | self.state.contains_key(name) 23 | } 24 | 25 | pub fn set(&mut self, name: String, value: ValueCell) { 26 | match self.state.get_mut(&name) { 27 | Some(val) => match val { 28 | ValueCell::Shared(val) => { 29 | *val.borrow_mut() = value.extract(); 30 | } 31 | ValueCell::Owned(_) => { 32 | self.state.insert(name, value); 33 | } 34 | }, 35 | None => { 36 | self.state.insert(name, value); 37 | } 38 | } 39 | } 40 | } 41 | 42 | impl<'a> IntoIterator for &'a Environment { 43 | type Item = (&'a String, &'a ValueCell); 44 | type IntoIter = std::collections::hash_map::Iter<'a, String, ValueCell>; 45 | 46 | fn into_iter(self) -> Self::IntoIter { 47 | self.state.iter() 48 | } 49 | } 50 | 51 | impl Default for Environment { 52 | fn default() -> Self { 53 | Self::new() 54 | } 55 | } 56 | 57 | #[derive(Debug, Clone)] 58 | pub enum ValueCell { 59 | Owned(Value), 60 | Shared(Rc>), 61 | } 62 | 63 | impl ValueCell { 64 | pub fn new(value: Value) -> Self { 65 | ValueCell::Owned(value) 66 | } 67 | 68 | pub fn new_shared(value: Value) -> Self { 69 | ValueCell::Shared(Rc::new(RefCell::new(value))) 70 | } 71 | 72 | pub fn extract(&self) -> Value { 73 | match self { 74 | ValueCell::Owned(val) => val.clone(), 75 | ValueCell::Shared(val) => val.borrow().clone(), 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | [![Português](https://img.shields.io/badge/idioma-pt--BR-green)](README.md) 2 | [![English](https://img.shields.io/badge/lang-en-blue)](README.en.md) 3 | 4 | # Tenda 5 | 6 | [![Status](https://img.shields.io/badge/status-in%20development-yellow)](https://tenda.dev/) 7 | [![License: GPL-3.0](https://img.shields.io/badge/license-GPLv3-blue)](LICENSE) 8 | 9 | ## Resources 10 | 11 | - 🌐 **Official Website:** [tenda.dev](https://tenda.dev/) 12 | - 📚 **Documentation:** [tenda.dev/docs](https://tenda.dev/docs) 13 | - 🎯 **Playground:** [tenda.dev/playground](https://tenda.dev/playground) 14 | 15 | ## Overview 16 | 17 | **Tenda** is a programming language designed for native Portuguese speakers. Its primary goal is to make programming more accessible by using Portuguese keywords and a syntax that reads like natural language. This narrative-style approach aims to lower the barrier to entry for beginners and educators in Portuguese-speaking communities. 18 | 19 | While Tenda is tailored for Portuguese syntax and semantics, it draws inspiration from modern programming paradigms and emphasizes readability and expressiveness. It's particularly suited for educational purposes, allowing learners to grasp programming concepts without the added complexity of a foreign language. 20 | 21 | ## Installation 22 | 23 | ### macOS / Linux 24 | 25 | ```bash 26 | curl -fsSL https://tenda.dev/install | bash 27 | ``` 28 | 29 | ### Windows (PowerShell) 30 | 31 | ```powershell 32 | iwr https://tenda.dev/install.ps1 -UseBasicParsing | iex 33 | ``` 34 | 35 | _By default, Tenda is installed to `$HOME/.tenda/bin` (or `%USERPROFILE%\.tenda\bin` on Windows) and your profile is automatically updated with:_ 36 | 37 | ```bash 38 | export TENDA_INSTALL="$HOME/.tenda" 39 | export PATH="$TENDA_INSTALL/bin:$PATH" 40 | ``` 41 | 42 | ## Maintainers 43 | 44 | - [@gabrielbrunop](https://github.com/gabrielbrunop) 45 | 46 | ## Contributing 47 | 48 | Currently, external contributions are not being accepted. This means that pull requests are closed to the public. However, if you have suggestions or encounter any issues, feel free to open an issue on our GitHub repository. 49 | 50 | ## License 51 | 52 | Tenda is distributed under the GNU General Public License v3.0. For more details, please refer to the [LICENSE](LICENSE) file in the repository. 53 | -------------------------------------------------------------------------------- /crates/reporting/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use aegean::*; 2 | 3 | pub trait Diagnostic { 4 | fn to_report(&self) -> crate::Report; 5 | fn set_span(&mut self, new_span: &S); 6 | fn get_span(&self) -> Option; 7 | fn get_message(&self) -> Option; 8 | fn get_labels(&self) -> Vec; 9 | fn get_helps(&self) -> Vec; 10 | fn get_notes(&self) -> Vec; 11 | fn build_report_config(&self) -> DiagnosticConfig; 12 | } 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct DiagnosticConfig { 16 | pub span: Option, 17 | pub labels: Vec, 18 | pub helps: Vec, 19 | pub notes: Vec, 20 | pub message: String, 21 | pub stacktrace: Vec>, 22 | } 23 | 24 | impl DiagnosticConfig { 25 | pub fn new( 26 | span: Option, 27 | labels: Vec, 28 | helps: Vec, 29 | notes: Vec, 30 | message: String, 31 | stacktrace: Vec>, 32 | ) -> Self { 33 | Self { 34 | span, 35 | labels, 36 | helps, 37 | notes, 38 | message, 39 | stacktrace, 40 | } 41 | } 42 | 43 | pub fn span(mut self, new_span: Option) -> Self { 44 | self.span = new_span; 45 | self 46 | } 47 | 48 | pub fn labels(mut self, new_labels: Vec) -> Self { 49 | self.labels.extend(new_labels); 50 | self 51 | } 52 | 53 | pub fn helps(mut self, new_helps: Vec) -> Self { 54 | self.helps.extend(new_helps); 55 | self 56 | } 57 | 58 | pub fn notes(mut self, new_notes: Vec) -> Self { 59 | self.notes.extend(new_notes); 60 | self 61 | } 62 | 63 | pub fn message(mut self, new_message: String) -> Self { 64 | self.message = new_message; 65 | self 66 | } 67 | 68 | pub fn stacktrace(mut self, new_stacktrace: Vec>) -> Self { 69 | self.stacktrace.extend(new_stacktrace); 70 | self 71 | } 72 | } 73 | 74 | type DiagnosticConfigFn = fn(&T, DiagnosticConfig) -> DiagnosticConfig; 75 | 76 | pub trait HasDiagnosticHooks { 77 | fn hooks() -> &'static [DiagnosticConfigFn] { 78 | &[] 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /crates/scanner/src/token.rs: -------------------------------------------------------------------------------- 1 | use tenda_common::span::SourceSpan; 2 | 3 | #[derive(Debug, Clone, PartialEq)] 4 | pub struct Token { 5 | pub kind: TokenKind, 6 | pub lexeme: String, 7 | pub literal: Option, 8 | pub span: SourceSpan, 9 | } 10 | 11 | impl Token { 12 | pub fn new( 13 | kind: TokenKind, 14 | lexeme: String, 15 | literal: Option, 16 | span: SourceSpan, 17 | ) -> Token { 18 | Token { 19 | kind, 20 | lexeme, 21 | literal, 22 | span, 23 | } 24 | } 25 | 26 | pub fn eoi(span: SourceSpan) -> Token { 27 | Token::new(TokenKind::Eof, "EOF".to_string(), None, span) 28 | } 29 | 30 | pub fn clone_ref(&self) -> Token { 31 | (*self).clone() 32 | } 33 | } 34 | 35 | impl From for Result, T> { 36 | fn from(val: Token) -> Self { 37 | Ok(Some(val)) 38 | } 39 | } 40 | 41 | #[derive(Debug, Clone, Copy, PartialEq)] 42 | pub enum TokenKind { 43 | Number, 44 | String, 45 | True, 46 | False, 47 | Nil, 48 | Equals, 49 | Not, 50 | Or, 51 | And, 52 | Greater, 53 | GreaterOrEqual, 54 | Less, 55 | LessOrEqual, 56 | Let, 57 | If, 58 | Function, 59 | Then, 60 | Else, 61 | Return, 62 | BlockEnd, 63 | While, 64 | Do, 65 | Continue, 66 | Identifier, 67 | EqualSign, 68 | Until, 69 | ForOrBreak, 70 | Each, 71 | In, 72 | Has, 73 | Colon, 74 | Plus, 75 | Minus, 76 | Star, 77 | Slash, 78 | Percent, 79 | Caret, 80 | LeftParen, 81 | RightParen, 82 | LeftBracket, 83 | RightBracket, 84 | LeftBrace, 85 | RightBrace, 86 | Comma, 87 | Dot, 88 | Arrow, 89 | Newline, 90 | Eof, 91 | } 92 | 93 | #[derive(Debug, Clone, PartialEq)] 94 | pub enum Literal { 95 | Number(f64), 96 | String(String), 97 | Boolean(bool), 98 | Nil, 99 | } 100 | 101 | impl Literal { 102 | pub const TRUE_LITERAL: &'static str = "verdadeiro"; 103 | pub const FALSE_LITERAL: &'static str = "falso"; 104 | pub const NIL_LITERAL: &'static str = "Nada"; 105 | pub const POSITIVE_INFINITY_LITERAL: &'static str = "infinito"; 106 | pub const NEGATIVE_INFINITY_LITERAL: &'static str = "-infinito"; 107 | pub const NAN_LITERAL: &'static str = "NaN"; 108 | } 109 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: read 13 | pull-requests: read 14 | 15 | env: 16 | RUSTFLAGS: "-Dwarnings" 17 | 18 | jobs: 19 | cache: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | 25 | - name: Cache Cargo registry 26 | uses: actions/cache@v3 27 | with: 28 | path: ~/.cargo/registry 29 | key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} 30 | restore-keys: | 31 | ${{ runner.os }}-cargo-registry- 32 | 33 | - name: Cache Cargo index 34 | uses: actions/cache@v3 35 | with: 36 | path: ~/.cargo/index 37 | key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} 38 | restore-keys: | 39 | ${{ runner.os }}-cargo-index- 40 | 41 | build: 42 | runs-on: ubuntu-latest 43 | needs: cache 44 | steps: 45 | - name: Checkout 46 | uses: actions/checkout@v4 47 | 48 | - name: Setup Rust 49 | uses: actions-rs/toolchain@v1 50 | with: 51 | toolchain: stable 52 | override: true 53 | 54 | - name: Build 55 | run: cargo build --verbose 56 | 57 | test: 58 | runs-on: ubuntu-latest 59 | needs: build 60 | steps: 61 | - name: Checkout 62 | uses: actions/checkout@v4 63 | 64 | - name: Setup Rust 65 | uses: actions-rs/toolchain@v1 66 | with: 67 | toolchain: stable 68 | override: true 69 | 70 | - name: Test 71 | run: cargo test --verbose 72 | 73 | - name: Check formatting 74 | run: cargo fmt -- --check 75 | 76 | clippy: 77 | runs-on: ubuntu-latest 78 | needs: build 79 | steps: 80 | - name: Checkout 81 | uses: actions/checkout@v4 82 | 83 | - name: Setup Rust 84 | uses: actions-rs/toolchain@v1 85 | with: 86 | toolchain: stable 87 | override: true 88 | 89 | - name: Check Clippy 90 | run: cargo clippy -- -D warnings 91 | 92 | commitlint: 93 | runs-on: ubuntu-latest 94 | steps: 95 | - name: Checkout 96 | uses: actions/checkout@v4 97 | 98 | - name: Commitlint 99 | uses: wagoid/commitlint-github-action@v6 100 | -------------------------------------------------------------------------------- /tests/src/lib.rs: -------------------------------------------------------------------------------- 1 | use tenda_core::common::source::IdentifiedSource; 2 | use tenda_core::parser::ast::Ast; 3 | use tenda_core::parser::Parser; 4 | use tenda_core::prelude::setup_runtime_prelude; 5 | use tenda_core::runtime::{Platform, Runtime, Value}; 6 | use tenda_core::scanner::Scanner; 7 | 8 | #[cfg(test)] 9 | mod ops; 10 | 11 | #[cfg(test)] 12 | mod core; 13 | 14 | #[cfg(test)] 15 | mod syntax; 16 | 17 | pub fn src_to_ast(source: &str) -> Ast { 18 | let source_id = IdentifiedSource::dummy(); 19 | let tokens = Scanner::new(source, source_id).scan().unwrap(); 20 | 21 | Parser::new(&tokens, source_id).parse().unwrap() 22 | } 23 | 24 | pub fn interpret_expr(platform: P, source: &str) -> Value { 25 | let ast = src_to_ast(source); 26 | 27 | Runtime::new(platform).eval(&ast).unwrap() 28 | } 29 | 30 | pub fn interpret_expr_with_prelude(platform: P, source: &str) -> Value { 31 | let ast = src_to_ast(source); 32 | let mut runtime = Runtime::new(platform); 33 | 34 | setup_runtime_prelude(runtime.get_global_env_mut()); 35 | 36 | runtime.eval(&ast).unwrap() 37 | } 38 | 39 | pub fn interpret_stmt(platform: P, source: &str) -> Runtime { 40 | let ast = src_to_ast(source); 41 | let mut runtime = Runtime::new(platform); 42 | 43 | runtime.eval(&ast).unwrap(); 44 | runtime 45 | } 46 | 47 | pub fn interpret_stmt_and_get( 48 | platform: P, 49 | source: &str, 50 | name: &str, 51 | ) -> Value { 52 | let runtime = interpret_stmt(platform, source); 53 | 54 | runtime.get_global_env().get(name).unwrap().extract() 55 | } 56 | 57 | #[macro_export] 58 | macro_rules! expr_tests { 59 | ($($name:ident: $expr:expr => $variant:ident $parens:tt),+ $(,)?) => { 60 | $( 61 | #[rstest::rstest] 62 | #[case(OSPlatform)] 63 | fn $name(#[case] platform: impl Platform + 'static) { 64 | use $crate::Value::*; 65 | 66 | assert_eq!( 67 | $crate::interpret_expr_with_prelude(platform, $expr), 68 | $variant $parens 69 | ); 70 | } 71 | )* 72 | }; 73 | } 74 | 75 | #[macro_export] 76 | macro_rules! expr_tests_should_panic { 77 | ($($name:ident: $expr:expr),+ $(,)?) => { 78 | $( 79 | #[rstest::rstest] 80 | #[case(OSPlatform)] 81 | #[should_panic] 82 | fn $name(#[case] platform: impl Platform + 'static) { 83 | $crate::interpret_expr_with_prelude(platform, $expr); 84 | } 85 | )* 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /scripts/install.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [string]$Tag 3 | ) 4 | 5 | function ErrorExit ($msg) { 6 | Write-Host "erro: $msg" -ForegroundColor Red 7 | exit 1 8 | } 9 | 10 | # Choose release 11 | 12 | $repo = 'gabrielbrunop/tenda' 13 | if (-not $Tag) { 14 | try { $Tag = (Invoke-RestMethod "https://api.github.com/repos/$repo/releases/latest").tag_name } 15 | catch { ErrorExit 'Falha ao buscar a tag da última versão no GitHub' } 16 | } 17 | Write-Host "Instalando Tenda $Tag" 18 | 19 | # Asset and URLs 20 | 21 | $asset = 'tenda-x86_64-pc-windows-msvc.zip' 22 | $download = "https://github.com/$repo/releases/download/$Tag/$asset" 23 | 24 | # Prepare installation directories 25 | 26 | $installRoot = if ($Env:TENDA_INSTALL) { $Env:TENDA_INSTALL } 27 | else { Join-Path ([Environment]::GetFolderPath('UserProfile')) '.tenda' } 28 | $binDir = Join-Path $installRoot 'bin' 29 | New-Item $binDir -ItemType Directory -Force | Out-Null 30 | 31 | $tmp = New-Item -ItemType Directory -Path ([IO.Path]::GetTempPath()) ` 32 | -Name ([Guid]::NewGuid()) 33 | 34 | # Download & unzip 35 | 36 | Write-Host "Baixando $download" 37 | Invoke-WebRequest -Uri $download -OutFile "$tmp\$asset" -UseBasicParsing -ErrorAction Stop 38 | 39 | Write-Host 'Extraindo arquivos' 40 | Expand-Archive -LiteralPath "$tmp\$asset" -DestinationPath $tmp -Force 41 | 42 | Move-Item -Force "$tmp\tenda.exe" (Join-Path $binDir 'tenda.exe') -ErrorAction Stop 43 | Remove-Item -Recurse -Force $tmp 44 | 45 | # Make PATH changes idempotent 46 | 47 | $pathLine = $binDir 48 | 49 | $current = [Environment]::GetEnvironmentVariable('Path', 'User') 50 | if ($null -eq $current) { $current = '' } 51 | 52 | if (-not ($current.Split(';') -contains $pathLine)) { 53 | $newPath = ($current.TrimEnd(';') + ';' + $pathLine).TrimStart(';') 54 | [Environment]::SetEnvironmentVariable('Path', $newPath, 'User') 55 | Write-Host "Adicionado $binDir ao PATH do usuário" 56 | } 57 | 58 | if (-not $Env:TENDA_INSTALL) { 59 | [Environment]::SetEnvironmentVariable('TENDA_INSTALL', $installRoot, 'User') 60 | Write-Host "Set TENDA_INSTALL=$installRoot" 61 | } 62 | 63 | # Update current session and finish 64 | 65 | $Env:PATH = "$binDir;$Env:PATH" 66 | $Env:TENDA_INSTALL = $installRoot 67 | 68 | Write-Host "" 69 | Write-Host "Tenda instalada com sucesso -> $(Join-Path $binDir 'tenda.exe')" -ForegroundColor Green 70 | 71 | # Show usage hint 72 | 73 | $hint = "`$env:Path = `"$binDir;`$env:Path`"" 74 | Write-Host "`nPara usar a Tenda imediatamente, execute:" -ForegroundColor Yellow 75 | Write-Host " $hint" -ForegroundColor Yellow 76 | Write-Host "`nExecute 'tenda --ajuda' para saber mais!" -ForegroundColor Yellow 77 | -------------------------------------------------------------------------------- /crates/tenda-playground-platform/src/lib.rs: -------------------------------------------------------------------------------- 1 | use delegate::delegate; 2 | use tenda_os_platform::Platform as OSPlatform; 3 | use tenda_runtime::ValueType; 4 | 5 | #[derive(Debug)] 6 | pub struct Platform { 7 | pub inner: OSPlatform, 8 | pub send: fn(ProtocolMessage), 9 | pub read_line: fn() -> String, 10 | } 11 | 12 | pub enum ProtocolMessage { 13 | Ready, 14 | Unlock, 15 | Output(String), 16 | Result(ValueType, String), 17 | Error(Vec), 18 | } 19 | 20 | impl Platform { 21 | pub fn new(send: fn(ProtocolMessage), read_line: fn() -> String) -> Self { 22 | Platform { 23 | inner: OSPlatform, 24 | send, 25 | read_line, 26 | } 27 | } 28 | } 29 | 30 | impl tenda_runtime::Platform for Platform { 31 | delegate! { 32 | to self.inner { 33 | fn rand(&self) -> f64; 34 | fn read_file(&self, path: &str) -> Result; 35 | fn write_file(&self, path: &str, content: &str) -> Result<(), tenda_runtime::FileErrorKind>; 36 | fn remove_file(&self, path: &str) -> Result<(), tenda_runtime::FileErrorKind>; 37 | fn list_files(&self, path: &str) -> Result, tenda_runtime::FileErrorKind>; 38 | fn create_dir(&self, path: &str) -> Result<(), tenda_runtime::FileErrorKind>; 39 | fn remove_dir(&self, path: &str) -> Result<(), tenda_runtime::FileErrorKind>; 40 | fn list_dirs(&self, path: &str) -> Result, tenda_runtime::FileErrorKind>; 41 | fn current_dir(&self) -> Result; 42 | fn file_append(&self, path: &str, content: &str) -> Result<(), tenda_runtime::FileErrorKind>; 43 | fn args(&self) -> Vec; 44 | fn exit(&self, code: i32); 45 | fn sleep(&self, seconds: f64); 46 | fn date_now(&self) -> i64; 47 | fn timezone_offset(&self) -> i32; 48 | } 49 | } 50 | 51 | fn println(&self, message: &str) { 52 | let message = format!("{}\n", message); 53 | 54 | (self.send)(ProtocolMessage::Output(message.to_string())) 55 | } 56 | 57 | fn print(&self, message: &str) { 58 | (self.send)(ProtocolMessage::Output(message.to_string())); 59 | } 60 | 61 | fn write(&self, message: &str) { 62 | (self.send)(ProtocolMessage::Output(message.to_string())); 63 | } 64 | 65 | fn read_line(&self) -> String { 66 | (self.send)(ProtocolMessage::Ready); 67 | 68 | let input = (self.read_line)(); 69 | 70 | (self.send)(ProtocolMessage::Unlock); 71 | 72 | input.trim_end_matches('\n').to_string() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /crates/parser/src/parser_error.rs: -------------------------------------------------------------------------------- 1 | use tenda_common::span::SourceSpan; 2 | use tenda_reporting_derive::Diagnostic; 3 | use tenda_scanner::Token; 4 | use thiserror::Error; 5 | 6 | pub type Result = std::result::Result>; 7 | 8 | #[derive(Error, Debug, PartialEq, Clone, Diagnostic)] 9 | #[report("erro sintático")] 10 | pub enum ParserError { 11 | #[error("fim inesperado de entrada")] 12 | UnexpectedEoi { 13 | #[span] 14 | span: SourceSpan, 15 | }, 16 | 17 | #[error("esperado ')'")] 18 | MissingParentheses { 19 | #[span] 20 | span: SourceSpan, 21 | }, 22 | 23 | #[error("esperado ']'")] 24 | MissingBrackets { 25 | #[span] 26 | span: SourceSpan, 27 | }, 28 | 29 | #[error("esperado '}}'")] 30 | MissingBraces { 31 | #[span] 32 | span: SourceSpan, 33 | }, 34 | 35 | #[error("esperado ':'")] 36 | MissingColon { 37 | #[span] 38 | span: SourceSpan, 39 | }, 40 | 41 | #[error("token inesperado: {}", .token.lexeme.escape_debug())] 42 | UnexpectedToken { 43 | token: Token, 44 | 45 | #[span] 46 | span: SourceSpan, 47 | }, 48 | 49 | #[error("o valor à direita do '=' não é um valor válido para receber atribuições")] 50 | InvalidAssignmentTarget { 51 | token: Token, 52 | 53 | #[span] 54 | span: SourceSpan, 55 | }, 56 | 57 | #[error("retorno fora de uma função")] 58 | IllegalReturn { 59 | #[span] 60 | span: SourceSpan, 61 | }, 62 | 63 | #[error("'pare' fora de uma estrutura de repetição")] 64 | IllegalBreak { 65 | #[span] 66 | span: SourceSpan, 67 | }, 68 | 69 | #[error("'continue' fora de uma estrutura de repetição")] 70 | IllegalContinue { 71 | #[span] 72 | span: SourceSpan, 73 | }, 74 | 75 | #[error("parâmetro '{}' duplicado na função", .name)] 76 | DuplicateParameter { 77 | name: String, 78 | 79 | #[span] 80 | span: SourceSpan, 81 | }, 82 | 83 | #[error("o operador '{}' não pode ser encadeado", .op.lexeme)] 84 | InvalidChaining { 85 | op: Token, 86 | 87 | #[span] 88 | span: SourceSpan, 89 | 90 | #[message] 91 | message: Option, 92 | 93 | #[help] 94 | help: Option, 95 | }, 96 | } 97 | 98 | macro_rules! unexpected_token { 99 | ($token:expr) => {{ 100 | let token = $token; 101 | 102 | match token.kind { 103 | TokenKind::Eof => ParserError::UnexpectedEoi { 104 | span: token.span.clone(), 105 | }, 106 | _ => ParserError::UnexpectedToken { 107 | token: token.clone_ref(), 108 | span: token.span.clone(), 109 | }, 110 | } 111 | }}; 112 | } 113 | 114 | pub(crate) use unexpected_token; 115 | -------------------------------------------------------------------------------- /tests/src/syntax.rs: -------------------------------------------------------------------------------- 1 | use tenda_core::{platform::OSPlatform, runtime::Platform}; 2 | 3 | use crate::{expr_tests, expr_tests_should_panic}; 4 | 5 | expr_tests_should_panic!( 6 | parse_error_unclosed_paren: "(1 + 2", 7 | parse_error_unclosed_bracket: "[1, 2", 8 | parse_error_unclosed_brace: "{ 1 : 2", 9 | parse_error_missing_colon_in_assoc: "{ 1 2 }", 10 | parse_error_invalid_operator: "1 $ 2", 11 | ); 12 | 13 | expr_tests!( 14 | escape_null_literal: "\"\\0\"" => String("\0".to_string()), 15 | escape_bell_literal: "\"Sino:\\a\"" => String("Sino:\x07".to_string()), 16 | escape_backspace_literal: "\"BS:\\b\"" => String("BS:\x08".to_string()), 17 | escape_formfeed_literal: "\"FF:\\f\"" => String("FF:\x0C".to_string()), 18 | escape_vertical_tab_literal: "\"VT:\\v\"" => String("VT:\x0B".to_string()), 19 | escape_escape_literal: "\"ESC:\\e\"" => String("ESC:\x1B".to_string()), 20 | escape_hex_uppercase_a: "\"hex:\\x41\"" => String("hex:A".to_string()), 21 | escape_unicode_16bit_a: "\"uni16:\\u0041\"" => String("uni16:A".to_string()), 22 | escape_unicode_32bit_a: "\"uni32:\\U00000041\"" => String("uni32:A".to_string()), 23 | escape_octal_literal_a: "\"oct:\\101\"" => String("oct:A".to_string()), 24 | escape_all_combined: "\"\\0\\a\\b\\e\\f\\n\\r\\t\\v\\\\\\\"\"" => 25 | String("\0\x07\x08\x1B\x0C\n\r\t\x0B\\\"".to_string()) 26 | ); 27 | 28 | expr_tests_should_panic!( 29 | escape_invalid_hex_digit: "\"\\xG1\"", 30 | escape_incomplete_hex_escape: "\"\\x1\"", 31 | escape_invalid_unicode_16: "\"\\uZZZZ\"", 32 | escape_unicode32_out_of_range: "\"\\U0FFFFFFF\"", 33 | escape_octal_value_too_large: "\"\\400\"", 34 | escape_octal_too_short: "\"\\12\"", 35 | escape_unknown_escape: "\"\\q\"" 36 | ); 37 | 38 | expr_tests!( 39 | number_zero: "0" => Number(0.0), 40 | number_plain: "123" => Number(123.0), 41 | number_underscores: "1_000_000" => Number(1_000_000.0), 42 | number_leading_zero: "0123" => Number(123.0), 43 | number_bin: "0b1010" => Number(10.0), 44 | number_bin_underscores: "0b1010_0101" => Number(0b1010_0101 as f64), 45 | number_bin_uppercase: "0B1101" => Number(13.0), 46 | number_oct: "0o755" => Number(0o755 as f64), 47 | number_oct_uppercase: "0O644" => Number(0o644 as f64), 48 | number_hex: "0xdead_beef" => Number(0xDEAD_BEEFu32 as f64), 49 | number_hex_uppercase: "0XCAFE" => Number(0xCAFE as f64), 50 | number_float: "0.123" => Number(0.123), 51 | number_float_trailing: "1." => Number(1.0), 52 | number_exp: "1e3" => Number(1e3), 53 | number_uppercase_exp: "1E3" => Number(1e3), 54 | number_exp_signed: "2.5e-2" => Number(2.5e-2), 55 | number_exp_plus: "2.5E+2" => Number(2.5e+2), 56 | number_exp_underscores: "1_2e3_0" => Number(12e30), 57 | ); 58 | 59 | expr_tests_should_panic!( 60 | number_bad_bin_digit: "0b102", 61 | number_bad_oct_digit: "0o9", 62 | number_bad_hex_digit: "0xG1", 63 | number_missing_bin_digits: "0b", 64 | number_missing_oct_digits: "0o", 65 | number_missing_hex_digits: "0x", 66 | number_multi_dot: "1.2.3", 67 | number_multi_exp: "1e2e3", 68 | number_missing_exp_digits: "1e", 69 | number_invalid_suffix: "123abc", 70 | ); 71 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish & Release 2 | 3 | on: 4 | push: 5 | tags: ["v*"] 6 | 7 | permissions: 8 | contents: write 9 | id-token: write 10 | packages: write 11 | 12 | jobs: 13 | ensure-on-main: 14 | if: startsWith(github.ref, 'refs/tags/v') 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: { fetch-depth: 0 } 19 | - run: | 20 | git fetch origin main 21 | git merge-base --is-ancestor $GITHUB_SHA origin/main || { 22 | echo "::error::Tag must point at main"; exit 1; } 23 | 24 | build-binaries: 25 | needs: ensure-on-main 26 | if: startsWith(github.ref, 'refs/tags/v') 27 | strategy: 28 | matrix: 29 | include: 30 | - os: ubuntu-latest 31 | target: x86_64-unknown-linux-gnu 32 | ext: "" 33 | pack: tar 34 | - os: macos-latest 35 | target: aarch64-apple-darwin 36 | ext: "" 37 | pack: tar 38 | - os: macos-latest 39 | target: x86_64-apple-darwin 40 | ext: "" 41 | pack: tar 42 | - os: windows-latest 43 | target: x86_64-pc-windows-msvc 44 | ext: .exe 45 | pack: zip 46 | runs-on: ${{ matrix.os }} 47 | steps: 48 | - uses: actions/checkout@v4 49 | 50 | - name: Install Rust toolchain 51 | uses: actions-rs/toolchain@v1 52 | with: 53 | toolchain: stable 54 | target: ${{ matrix.target }} 55 | override: true 56 | 57 | - name: Build release binary 58 | run: cargo build --release --package tenda --target ${{ matrix.target }} 59 | 60 | - name: Pack artifact 61 | run: | 62 | mkdir dist 63 | BIN=target/${{ matrix.target }}/release/tenda${{ matrix.ext }} 64 | cp "$BIN" dist/ 65 | cd dist 66 | ARCHIVE="tenda-${{ matrix.target }}.${{ matrix.pack == 'zip' && 'zip' || 'tar.gz' }}" 67 | if [[ "${{ matrix.pack }}" == "zip" ]]; then 68 | 7z a "$ARCHIVE" "tenda${{ matrix.ext }}" 69 | else 70 | tar -czf "$ARCHIVE" "tenda${{ matrix.ext }}" 71 | fi 72 | echo "ASSET=$ARCHIVE" >>"$GITHUB_ENV" 73 | shell: bash 74 | 75 | - name: Upload artifact 76 | uses: actions/upload-artifact@v4 77 | with: 78 | name: bin-${{ matrix.target }} 79 | path: dist/tenda-* 80 | 81 | publish-crates: 82 | if: startsWith(github.ref, 'refs/tags/v') 83 | needs: 84 | - ensure-on-main 85 | - build-binaries 86 | runs-on: ubuntu-latest 87 | 88 | steps: 89 | - uses: actions/checkout@v4 90 | 91 | - name: Install cargo-workspaces 92 | run: cargo install cargo-workspaces --locked 93 | 94 | - uses: Swatinem/rust-cache@v2 95 | 96 | - name: Dry-run publish 97 | env: 98 | CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} 99 | run: | 100 | cargo workspaces publish \ 101 | --from-git \ 102 | --skip-published \ 103 | --publish-interval 10 \ 104 | --yes \ 105 | --dry-run \ 106 | --token "$CARGO_REGISTRY_TOKEN" 107 | 108 | - name: Publish to crates.io 109 | env: 110 | CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} 111 | run: | 112 | cargo workspaces publish \ 113 | --from-git \ 114 | --skip-published \ 115 | --publish-interval 10 \ 116 | --yes \ 117 | --token "$CARGO_REGISTRY_TOKEN" 118 | 119 | github-release: 120 | if: startsWith(github.ref, 'refs/tags/v') 121 | needs: 122 | - ensure-on-main 123 | - build-binaries 124 | - publish-crates 125 | runs-on: ubuntu-latest 126 | steps: 127 | - uses: actions/checkout@v4 128 | 129 | - name: Download all binary artifacts 130 | uses: actions/download-artifact@v4 131 | with: 132 | path: dist 133 | 134 | - name: Create / update GitHub Release 135 | id: release 136 | uses: softprops/action-gh-release@v2 137 | with: 138 | files: dist/**/* 139 | draft: false 140 | prerelease: ${{ contains(github.ref_name, '-') }} 141 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # Helpers 5 | 6 | error() { 7 | printf "\033[0;31merro:\033[0m %s\n" "$*" >&2 8 | exit 1 9 | } 10 | info() { printf "\033[0;2m%s\033[0m\n" "$*"; } 11 | success() { printf "\033[0;32m%s\033[0m\n" "$*"; } 12 | need() { command -v "$1" >/dev/null || error "$1 é necessário"; } 13 | 14 | detect_shell() { 15 | local sh=${SHELL##*/} 16 | [[ $sh =~ (bash|zsh|fish) ]] && { 17 | printf %s "$sh" 18 | return 19 | } 20 | [[ -r /proc/$$/cmdline ]] && head -c 128 /proc/$$/cmdline | 21 | tr '\0' ' ' | awk '{print $1}' | xargs basename 22 | } 23 | 24 | add_line() { grep -Fqx "$2" "$1" 2>/dev/null || printf '%s\n' "$2" >>"$1"; } 25 | 26 | # Delegate to PowerShell on native Windows 27 | 28 | if [[ ${OS:-} == Windows_NT && ! $(uname -s) =~ MINGW64 ]]; then 29 | powershell -NoLogo -Command \ 30 | "iwr https://raw.githubusercontent.com/gabrielbrunop/tenda/main/scripts/install.ps1 -UseBasicParsing | iex" 31 | exit $? 32 | fi 33 | 34 | # Prerequisites 35 | 36 | need curl 37 | need tar 38 | need uname 39 | 40 | # Choose release 41 | 42 | REPO="gabrielbrunop/tenda" 43 | TAG="${1:-}" 44 | if [[ -z $TAG ]]; then 45 | TAG=$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" | 46 | grep -oP '"tag_name":\s*"\K[^"]+') || 47 | error "Não foi possível obter a última versão da Tenda" 48 | fi 49 | info "Instalando Tenda $TAG" 50 | 51 | # Detect platform 52 | 53 | kernel=$(uname -s) 54 | arch=$(uname -m) 55 | case "$kernel,$arch" in 56 | Darwin,x86_64) asset="tenda-x86_64-apple-darwin.tar.gz" ;; 57 | Darwin,arm64) asset="tenda-aarch64-apple-darwin.tar.gz" ;; 58 | Linux,x86_64) asset="tenda-x86_64-unknown-linux-gnu.tar.gz" ;; 59 | MINGW* | MSYS* | CYGWIN*) 60 | need unzip 61 | asset="tenda-x86_64-pc-windows-msvc.zip" 62 | ;; 63 | *) error "Plataforma não suportada: $kernel $arch" ;; 64 | esac 65 | info "Plataforma detectada → $asset" 66 | 67 | # Download asset 68 | 69 | INSTALL_DIR="${TENDA_INSTALL:-$HOME/.tenda}" 70 | BIN_DIR="$INSTALL_DIR/bin" 71 | mkdir -p "$BIN_DIR" 72 | tmp=$(mktemp -d) 73 | trap 'rm -rf "$tmp"' EXIT 74 | 75 | url="https://github.com/$REPO/releases/download/$TAG/$asset" 76 | info "Baixando $url" 77 | curl -#fL "$url" -o "$tmp/$asset" || error "Falha ao baixar" 78 | 79 | # Unpack & install 80 | 81 | case $asset in 82 | *.tar.gz) tar -xzf "$tmp/$asset" -C "$tmp" ;; 83 | *.zip) unzip -q "$tmp/$asset" -d "$tmp" ;; 84 | esac 85 | rm "$tmp/$asset" 86 | 87 | BIN_NAME=$([[ $asset == *.zip ]] && echo "tenda.exe" || echo "tenda") 88 | mv -f "$tmp/$BIN_NAME" "$BIN_DIR/$BIN_NAME" || 89 | error "Falha ao mover o binário para $BIN_DIR" 90 | chmod +x "$BIN_DIR/$BIN_NAME" 91 | success "Tenda instalada em $BIN_DIR/$BIN_NAME" 92 | 93 | # Update shell profile(s) 94 | 95 | PROFILE_MODIFIED="" 96 | add_to_profile() { 97 | local export_root="export TENDA_INSTALL=\"$INSTALL_DIR\"" 98 | local export_path="export PATH=\"$BIN_DIR:\$PATH\"" 99 | case $(detect_shell) in 100 | bash) 101 | for rc in "$HOME/.bashrc" "$HOME/.bash_profile" \ 102 | "${XDG_CONFIG_HOME:-$HOME/.config}/bashrc"; do 103 | [[ -w $rc || ! -e $rc ]] || continue 104 | touch "$rc" 105 | add_line "$rc" "$export_root" 106 | add_line "$rc" "$export_path" 107 | PROFILE_MODIFIED=$rc 108 | break 109 | done 110 | ;; 111 | zsh) 112 | local rc="$HOME/.zshrc" 113 | [[ -w $rc || ! -e $rc ]] || rc="$HOME/.zprofile" 114 | touch "$rc" 115 | add_line "$rc" "$export_root" 116 | add_line "$rc" "$export_path" 117 | PROFILE_MODIFIED=$rc 118 | ;; 119 | fish) 120 | need fish_add_path # fish >= 3.2 121 | fish_add_path "$BIN_DIR" >/dev/null 2>&1 122 | set -Ux TENDA_INSTALL "$INSTALL_DIR" 123 | PROFILE_MODIFIED="fish_user_paths" 124 | ;; 125 | *) 126 | info "Shell desconhecido - adicione manualmente:" 127 | printf ' %s\n %s\n' "$export_root" "$export_path" 128 | ;; 129 | esac 130 | } 131 | add_to_profile 132 | 133 | # Finale 134 | 135 | if command -v tenda >/dev/null 2>&1; then 136 | success "Execute 'tenda --ajuda' para começar!" 137 | else 138 | echo 139 | info "Reincie seu terminal ou execute:" 140 | case $(detect_shell) in 141 | fish) info " source ~/.config/fish/config.fish" ;; 142 | zsh) info " exec \$SHELL" ;; 143 | *) info " source ${PROFILE_MODIFIED:-}" ;; 144 | esac 145 | fi 146 | -------------------------------------------------------------------------------- /crates/tenda-playground/src/main.rs: -------------------------------------------------------------------------------- 1 | use protocol_message::JsonProtocolMessage; 2 | use std::io::{self, BufRead, Write}; 3 | use std::rc::Rc; 4 | use tenda_core::common::span::SourceSpan; 5 | use tenda_core::runtime::escape_value; 6 | use tenda_core::{ 7 | common::source::IdentifiedSource, parser::Parser, prelude::setup_runtime_prelude, 8 | runtime::Runtime, scanner::Scanner, 9 | }; 10 | use tenda_playground_platform::Platform; 11 | use tenda_playground_platform::ProtocolMessage; 12 | 13 | const PROMPT_TERMINATOR: u8 = b'\x04'; 14 | 15 | mod protocol_message; 16 | 17 | fn send(message: ProtocolMessage) { 18 | let json_message = JsonProtocolMessage::from(message); 19 | let json_string = json_message.to_string(); 20 | 21 | let mut stdout = io::stdout(); 22 | stdout.write_all(json_string.as_bytes()).unwrap(); 23 | stdout.write_all(b"\n").unwrap(); 24 | stdout.flush().unwrap(); 25 | } 26 | 27 | fn send_diagnostic( 28 | errs: Vec<( 29 | impl tenda_core::reporting::Diagnostic, 30 | impl tenda_core::reporting::Cache, 31 | )>, 32 | ) { 33 | let errs_str: Vec<_> = errs 34 | .into_iter() 35 | .map(|(err, cache)| { 36 | let mut buf = Vec::::new(); 37 | 38 | err.to_report().write(cache, &mut buf).unwrap(); 39 | 40 | let message = String::from_utf8_lossy(&buf).into_owned(); 41 | 42 | message 43 | }) 44 | .collect(); 45 | 46 | send(ProtocolMessage::Error(errs_str)); 47 | } 48 | 49 | fn read_line() -> String { 50 | let mut input = String::new(); 51 | let stdin = io::stdin(); 52 | let mut reader = stdin.lock(); 53 | 54 | reader.read_line(&mut input).unwrap(); 55 | 56 | input 57 | } 58 | 59 | fn read_prompt(buffer: &mut Vec) -> Result, io::Error> { 60 | let bytes_read = { 61 | let stdin = io::stdin(); 62 | let mut reader = stdin.lock(); 63 | 64 | buffer.clear(); 65 | 66 | reader.read_until(PROMPT_TERMINATOR, buffer)? 67 | }; 68 | 69 | if bytes_read == 0 { 70 | return Ok(None); 71 | } 72 | 73 | if let Some(&last) = buffer.last() { 74 | if last == PROMPT_TERMINATOR { 75 | buffer.pop(); 76 | } 77 | } 78 | 79 | let source = String::from_utf8_lossy(buffer).into_owned(); 80 | 81 | Ok(Some(source)) 82 | } 83 | 84 | fn main() -> io::Result<()> { 85 | let platform = Platform::new(send, read_line); 86 | 87 | let mut runtime = Runtime::new(platform); 88 | setup_runtime_prelude(runtime.get_global_env_mut()); 89 | 90 | let mut buffer = Vec::new(); 91 | let mut source_history: Vec<(IdentifiedSource, Rc)> = Vec::new(); 92 | 93 | loop { 94 | let source = match read_prompt(&mut buffer)? { 95 | Some(source) => source, 96 | None => continue, 97 | }; 98 | 99 | let source_id = IdentifiedSource::new(); 100 | let source_rc = Rc::from(source.clone()); 101 | source_history.push((source_id, source_rc)); 102 | 103 | if source.trim().is_empty() { 104 | continue; 105 | } 106 | 107 | let tokens = match Scanner::new(&source, source_id).scan() { 108 | Ok(tokens) => tokens, 109 | Err(errs) => { 110 | let diagnostic_pairs = errs 111 | .into_iter() 112 | .map(|err| (err, tenda_core::reporting::sources(source_history.clone()))) 113 | .collect(); 114 | 115 | send_diagnostic(diagnostic_pairs); 116 | continue; 117 | } 118 | }; 119 | 120 | let ast = match Parser::new(&tokens, source_id).parse() { 121 | Ok(ast) => ast, 122 | Err(errors) => { 123 | let diagnostic_pairs = errors 124 | .into_iter() 125 | .map(|err| (err, tenda_core::reporting::sources(source_history.clone()))) 126 | .collect(); 127 | 128 | send_diagnostic(diagnostic_pairs); 129 | continue; 130 | } 131 | }; 132 | 133 | match runtime.eval(&ast) { 134 | Ok(result) => { 135 | send(ProtocolMessage::Result( 136 | result.kind(), 137 | escape_value(&result), 138 | )); 139 | } 140 | Err(err) => { 141 | send_diagnostic(vec![( 142 | *err, 143 | tenda_core::reporting::sources(source_history.clone()), 144 | )]); 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /crates/os-platform/src/lib.rs: -------------------------------------------------------------------------------- 1 | use chrono::Local; 2 | use std::io::Write; 3 | 4 | #[derive(Debug)] 5 | pub struct Platform; 6 | 7 | fn map_file_error_kind(kind: std::io::ErrorKind) -> tenda_runtime::FileErrorKind { 8 | use std::io; 9 | use tenda_runtime::FileErrorKind; 10 | 11 | match kind { 12 | io::ErrorKind::NotFound => FileErrorKind::NotFound, 13 | io::ErrorKind::PermissionDenied => FileErrorKind::PermissionDenied, 14 | io::ErrorKind::AlreadyExists => FileErrorKind::AlreadyExists, 15 | io::ErrorKind::Other => FileErrorKind::Other, 16 | _ => FileErrorKind::Other, 17 | } 18 | } 19 | 20 | impl tenda_runtime::Platform for Platform { 21 | fn println(&self, message: &str) { 22 | println!("{}", message); 23 | } 24 | 25 | fn print(&self, message: &str) { 26 | print!("{}", message); 27 | 28 | std::io::stdout().flush().unwrap(); 29 | } 30 | 31 | fn write(&self, message: &str) { 32 | print!("{}", message); 33 | } 34 | 35 | fn rand(&self) -> f64 { 36 | rand::random() 37 | } 38 | 39 | fn read_file(&self, path: &str) -> Result { 40 | match std::fs::read_to_string(path) { 41 | Ok(content) => Ok(content), 42 | Err(error) => Err(map_file_error_kind(error.kind())), 43 | } 44 | } 45 | 46 | fn write_file(&self, path: &str, content: &str) -> Result<(), tenda_runtime::FileErrorKind> { 47 | match std::fs::write(path, content) { 48 | Ok(_) => Ok(()), 49 | Err(error) => Err(map_file_error_kind(error.kind())), 50 | } 51 | } 52 | 53 | fn remove_file(&self, path: &str) -> Result<(), tenda_runtime::FileErrorKind> { 54 | match std::fs::remove_file(path) { 55 | Ok(_) => Ok(()), 56 | Err(error) => Err(map_file_error_kind(error.kind())), 57 | } 58 | } 59 | 60 | fn list_files(&self, path: &str) -> Result, tenda_runtime::FileErrorKind> { 61 | match std::fs::read_dir(path) { 62 | Ok(entries) => Ok(entries 63 | .filter_map(|entry| entry.ok().map(|entry| entry.file_name())) 64 | .map(|name| name.to_string_lossy().to_string()) 65 | .collect()), 66 | Err(error) => Err(map_file_error_kind(error.kind())), 67 | } 68 | } 69 | 70 | fn create_dir(&self, path: &str) -> Result<(), tenda_runtime::FileErrorKind> { 71 | match std::fs::create_dir(path) { 72 | Ok(_) => Ok(()), 73 | Err(error) => Err(map_file_error_kind(error.kind())), 74 | } 75 | } 76 | 77 | fn remove_dir(&self, path: &str) -> Result<(), tenda_runtime::FileErrorKind> { 78 | match std::fs::remove_dir(path) { 79 | Ok(_) => Ok(()), 80 | Err(error) => Err(map_file_error_kind(error.kind())), 81 | } 82 | } 83 | 84 | fn list_dirs(&self, path: &str) -> Result, tenda_runtime::FileErrorKind> { 85 | match std::fs::read_dir(path) { 86 | Ok(entries) => Ok(entries 87 | .filter_map(|entry| entry.ok().map(|entry| entry.file_name())) 88 | .map(|name| name.to_string_lossy().to_string()) 89 | .collect()), 90 | Err(error) => Err(map_file_error_kind(error.kind())), 91 | } 92 | } 93 | 94 | fn current_dir(&self) -> Result { 95 | match std::env::current_dir() { 96 | Ok(path) => Ok(path.to_string_lossy().to_string()), 97 | Err(error) => Err(map_file_error_kind(error.kind())), 98 | } 99 | } 100 | 101 | fn file_append(&self, path: &str, content: &str) -> Result<(), tenda_runtime::FileErrorKind> { 102 | match std::fs::OpenOptions::new().append(true).open(path) { 103 | Ok(mut file) => match file.write_all(content.as_bytes()) { 104 | Ok(_) => Ok(()), 105 | Err(error) => Err(map_file_error_kind(error.kind())), 106 | }, 107 | Err(error) => Err(map_file_error_kind(error.kind())), 108 | } 109 | } 110 | 111 | fn args(&self) -> Vec { 112 | std::env::args().collect() 113 | } 114 | 115 | fn exit(&self, code: i32) { 116 | std::process::exit(code); 117 | } 118 | 119 | fn sleep(&self, seconds: f64) { 120 | std::thread::sleep(std::time::Duration::from_secs_f64(seconds)); 121 | } 122 | 123 | fn date_now(&self) -> i64 { 124 | Local::now().timestamp_millis() 125 | } 126 | 127 | fn timezone_offset(&self) -> i32 { 128 | Local::now().offset().local_minus_utc() 129 | } 130 | 131 | fn read_line(&self) -> String { 132 | let mut input = String::new(); 133 | std::io::stdin().read_line(&mut input).unwrap(); 134 | input.trim_end().to_string() 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /crates/runtime/src/function.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicUsize, Ordering}; 2 | use tenda_common::span::SourceSpan; 3 | use tenda_parser::ast; 4 | 5 | use crate::environment::Environment; 6 | use crate::runtime::Runtime; 7 | 8 | use super::runtime_error::Result; 9 | use super::value::Value; 10 | 11 | static FUNCTION_ID_COUNTER: AtomicUsize = AtomicUsize::new(1); 12 | 13 | #[derive(Debug, Clone)] 14 | pub struct Function { 15 | pub id: usize, 16 | pub object: FunctionObject, 17 | pub metadata: Option, 18 | } 19 | 20 | impl Function { 21 | pub fn new( 22 | params: Vec, 23 | captured_env: Environment, 24 | body: Box, 25 | ) -> Self { 26 | let unique_id = FUNCTION_ID_COUNTER.fetch_add(1, Ordering::SeqCst); 27 | 28 | Function { 29 | id: unique_id, 30 | object: FunctionObject::new(params, Box::new(captured_env), body), 31 | metadata: None, 32 | } 33 | } 34 | 35 | pub fn new_builtin(params: Vec, func_ptr: BuiltinFunctionPointer) -> Self { 36 | let unique_id = FUNCTION_ID_COUNTER.fetch_add(1, Ordering::SeqCst); 37 | 38 | Function { 39 | id: unique_id, 40 | object: FunctionObject::new_builtin(params, Box::default(), func_ptr), 41 | metadata: None, 42 | } 43 | } 44 | 45 | pub fn get_params(&self) -> Vec { 46 | match &self.object { 47 | FunctionObject::UserDefined { params, .. } => params.clone(), 48 | FunctionObject::Builtin { params, .. } => params.clone(), 49 | } 50 | } 51 | 52 | pub fn get_env(&self) -> &Environment { 53 | match &self.object { 54 | FunctionObject::UserDefined { env, .. } => env, 55 | FunctionObject::Builtin { env, .. } => env, 56 | } 57 | } 58 | 59 | pub fn get_env_mut(&mut self) -> &mut Box { 60 | match &mut self.object { 61 | FunctionObject::UserDefined { env, .. } => env, 62 | FunctionObject::Builtin { env, .. } => env, 63 | } 64 | } 65 | 66 | pub fn set_metadata(&mut self, metadata: FunctionRuntimeMetadata) { 67 | self.metadata = Some(metadata); 68 | } 69 | } 70 | 71 | impl PartialEq for Function { 72 | fn eq(&self, other: &Self) -> bool { 73 | self.id == other.id 74 | } 75 | } 76 | 77 | #[derive(Debug, Clone)] 78 | pub struct FunctionParam { 79 | pub name: String, 80 | pub is_captured: bool, 81 | } 82 | 83 | impl From for FunctionParam { 84 | fn from(param: ast::FunctionParam) -> Self { 85 | FunctionParam { 86 | name: param.name, 87 | is_captured: param.captured, 88 | } 89 | } 90 | } 91 | 92 | type BuiltinFunctionPointer = fn( 93 | params: Vec<(FunctionParam, Value)>, 94 | runtime: &mut Runtime, 95 | context: Box, 96 | ) -> Result; 97 | 98 | #[derive(Debug, Clone)] 99 | pub enum FunctionObject { 100 | UserDefined { 101 | params: Vec, 102 | env: Box, 103 | body: Box, 104 | }, 105 | Builtin { 106 | params: Vec, 107 | env: Box, 108 | func_ptr: BuiltinFunctionPointer, 109 | }, 110 | } 111 | 112 | impl FunctionObject { 113 | pub fn new( 114 | params: Vec, 115 | context: Box, 116 | body: Box, 117 | ) -> Self { 118 | FunctionObject::UserDefined { 119 | params, 120 | body, 121 | env: context, 122 | } 123 | } 124 | 125 | pub fn new_builtin( 126 | params: Vec, 127 | env: Box, 128 | func_ptr: BuiltinFunctionPointer, 129 | ) -> Self { 130 | FunctionObject::Builtin { 131 | params, 132 | env, 133 | func_ptr, 134 | } 135 | } 136 | } 137 | 138 | #[derive(Debug, Clone)] 139 | pub struct FunctionRuntimeMetadata { 140 | span: Option>, 141 | name: Option, 142 | } 143 | 144 | impl FunctionRuntimeMetadata { 145 | pub fn new(span: Option, name: Option) -> Self { 146 | FunctionRuntimeMetadata { 147 | span: span.map(Box::new), 148 | name, 149 | } 150 | } 151 | } 152 | 153 | impl FunctionRuntimeMetadata { 154 | pub fn get_span(&self) -> Option> { 155 | self.span.clone() 156 | } 157 | 158 | pub fn get_name(&self) -> Option { 159 | self.name.clone() 160 | } 161 | } 162 | 163 | #[macro_export] 164 | macro_rules! params { 165 | ($($kind:expr),*) => { 166 | { 167 | use $crate::FunctionParam; 168 | vec![$($kind.to_string()),*].into_iter().map(|name| FunctionParam { 169 | name, 170 | is_captured: false, 171 | }).collect() 172 | } 173 | }; 174 | } 175 | -------------------------------------------------------------------------------- /crates/runtime/src/stack.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use crate::{environment::ValueCell, frame::Frame}; 4 | 5 | type Result = std::result::Result; 6 | 7 | #[derive(Debug)] 8 | pub struct Stack { 9 | global: Frame, 10 | frame: Vec, 11 | has_break: bool, 12 | has_continue: bool, 13 | } 14 | 15 | impl Stack { 16 | pub fn new() -> Self { 17 | Stack { 18 | global: Frame::new(), 19 | frame: vec![], 20 | has_break: false, 21 | has_continue: false, 22 | } 23 | } 24 | 25 | pub fn is_name_in_local_scope(&self, name: &String) -> bool { 26 | self.get_innermost_frame().get_env().has(name) 27 | } 28 | 29 | pub fn define(&mut self, name: String, value: ValueCell) -> Result<()> { 30 | let scope = self.get_innermost_scope_mut(); 31 | 32 | if scope.get_env().has(&name) { 33 | return Err(StackError::AlreadyDeclared); 34 | } 35 | 36 | scope.get_env_mut().set(name, value); 37 | 38 | Ok(()) 39 | } 40 | 41 | pub fn assign(&mut self, name: String, value: ValueCell) -> Result<()> { 42 | let frame = self 43 | .frame 44 | .iter_mut() 45 | .rev() 46 | .find(|frame| frame.get_env().has(&name)) 47 | .unwrap_or(&mut self.global); 48 | 49 | if frame.get_env().has(&name) { 50 | frame.get_env_mut().set(name, value); 51 | Ok(()) 52 | } else { 53 | Err(StackError::AssignToUndefined(name)) 54 | } 55 | } 56 | 57 | pub fn lookup(&mut self, name: &str) -> Option<&ValueCell> { 58 | for frame in self.frame.iter().rev() { 59 | if let Some(var) = frame.get_env().get(name) { 60 | return Some(var); 61 | } 62 | } 63 | 64 | self.global.get_env().get(name) 65 | } 66 | 67 | pub fn push(&mut self, frame: Frame) { 68 | self.frame.push(frame); 69 | } 70 | 71 | pub fn pop(&mut self) { 72 | if self.get_innermost_frame().get_return_value().is_some() { 73 | self.shift_return_to_upper_frame(); 74 | } 75 | 76 | self.frame.pop(); 77 | } 78 | 79 | pub fn set_return_value(&mut self, value: ValueCell) { 80 | self.get_innermost_scope_mut().set_return_value(value); 81 | } 82 | 83 | pub fn has_return_value(&self) -> bool { 84 | self.get_innermost_frame().get_return_value().is_some() 85 | } 86 | 87 | pub fn consume_return_value(&mut self) -> Option { 88 | let value = self.get_innermost_frame().get_return_value().cloned(); 89 | 90 | self.get_innermost_scope_mut().clear_return_value(); 91 | 92 | value 93 | } 94 | 95 | pub fn set_loop_break_flag(&mut self, value: bool) { 96 | self.has_break = value; 97 | } 98 | 99 | pub fn has_loop_break_flag(&self) -> bool { 100 | self.has_break 101 | } 102 | 103 | pub fn set_loop_continue_flag(&mut self, value: bool) { 104 | self.has_continue = value; 105 | } 106 | 107 | pub fn has_loop_continue_flag(&self) -> bool { 108 | self.has_continue 109 | } 110 | 111 | pub fn global(&self) -> &Frame { 112 | &self.global 113 | } 114 | 115 | pub fn global_mut(&mut self) -> &mut Frame { 116 | &mut self.global 117 | } 118 | } 119 | 120 | impl Stack { 121 | fn get_innermost_frame(&self) -> &Frame { 122 | self.frame.last().unwrap_or(&self.global) 123 | } 124 | 125 | fn get_innermost_scope_mut(&mut self) -> &mut Frame { 126 | self.frame.last_mut().unwrap_or(&mut self.global) 127 | } 128 | 129 | fn shift_return_to_upper_frame(&mut self) { 130 | let len = self.frame.len(); 131 | 132 | let return_value = match self.get_innermost_frame().get_return_value().cloned() { 133 | Some(value) => value, 134 | None => return, 135 | }; 136 | 137 | let last_index = len - 1; 138 | let decremented_index = last_index - 1; 139 | 140 | let scope_above = match self.frame.get_mut(decremented_index) { 141 | Some(scope) => scope, 142 | None => return, 143 | }; 144 | 145 | scope_above.set_return_value(return_value.clone()); 146 | } 147 | } 148 | 149 | impl<'a> IntoIterator for &'a Stack { 150 | type Item = &'a Frame; 151 | type IntoIter = std::vec::IntoIter<&'a Frame>; 152 | 153 | fn into_iter(self) -> Self::IntoIter { 154 | let mut frames: Vec<&Frame> = Vec::with_capacity(self.frame.len() + 1); 155 | frames.push(&self.global); 156 | frames.extend(self.frame.iter()); 157 | frames.into_iter() 158 | } 159 | } 160 | 161 | impl Default for Stack { 162 | fn default() -> Self { 163 | Self::new() 164 | } 165 | } 166 | 167 | #[derive(Error, Debug, PartialEq, Clone)] 168 | pub enum StackError { 169 | #[error("variable already declared")] 170 | AlreadyDeclared, 171 | 172 | #[error("assignment to undefined variable")] 173 | AssignToUndefined(String), 174 | } 175 | -------------------------------------------------------------------------------- /crates/runtime/src/date.rs: -------------------------------------------------------------------------------- 1 | use chrono::{ 2 | DateTime, Datelike, FixedOffset, LocalResult, Offset, TimeZone, Timelike, Utc, Weekday, 3 | }; 4 | use chrono_tz::Tz; 5 | use std::ops::{Add, Sub}; 6 | 7 | use crate::runtime_error::RuntimeError; 8 | 9 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 10 | pub struct Date { 11 | ts: i64, 12 | tz: FixedOffset, 13 | } 14 | 15 | impl Date { 16 | pub fn from_timestamp_millis(ts: i64, tz: Option) -> Result> { 17 | let tz = tz.map(|offset| FixedOffset::east_opt(offset).unwrap()); 18 | 19 | match Utc.timestamp_millis_opt(ts) { 20 | LocalResult::Single(_) => Ok(Self { 21 | ts, 22 | tz: tz.unwrap_or(FixedOffset::east_opt(0).unwrap()), 23 | }), 24 | _ => Err(Box::new(RuntimeError::InvalidTimestamp { 25 | timestamp: ts, 26 | span: None, 27 | stacktrace: vec![], 28 | })), 29 | } 30 | } 31 | 32 | pub fn from_iso_string(s: &str) -> Result> { 33 | let fixed_dt = match chrono::DateTime::parse_from_rfc3339(s) { 34 | Ok(dt) => dt, 35 | Err(e) => { 36 | return Err(Box::new(RuntimeError::DateIsoParseError { 37 | source: e, 38 | span: None, 39 | stacktrace: vec![], 40 | })) 41 | } 42 | }; 43 | 44 | let utc_ts = fixed_dt.with_timezone(&Utc).timestamp_millis(); 45 | let offset = fixed_dt.offset().fix(); 46 | 47 | Ok(Self { 48 | ts: utc_ts, 49 | tz: offset, 50 | }) 51 | } 52 | 53 | pub fn with_named_timezone(&self, tz_str: &str) -> Result> { 54 | let named_zone = match tz_str.parse::() { 55 | Ok(z) => z, 56 | Err(_) => { 57 | return Err(Box::new(RuntimeError::InvalidTimeZoneString { 58 | tz_str: tz_str.into(), 59 | span: None, 60 | stacktrace: vec![], 61 | })); 62 | } 63 | }; 64 | 65 | let utc_dt = Utc.timestamp_millis_opt(self.ts).single().unwrap(); 66 | let dt_in_zone = utc_dt.with_timezone(&named_zone); 67 | let new_offset = dt_in_zone.offset().fix(); 68 | 69 | Ok(Self { 70 | ts: self.ts, 71 | tz: new_offset, 72 | }) 73 | } 74 | 75 | pub fn to_offset_string(&self) -> String { 76 | let total_seconds = self.tz.local_minus_utc(); 77 | 78 | let sign = if total_seconds >= 0 { '+' } else { '-' }; 79 | let secs = total_seconds.abs(); 80 | 81 | let hours = secs / 3600; 82 | let minutes = (secs % 3600) / 60; 83 | 84 | format!("{}{:02}:{:02}", sign, hours, minutes) 85 | } 86 | 87 | pub fn to_iso_string(&self) -> String { 88 | let dt_tz = self.as_datetime_tz(); 89 | dt_tz.to_rfc3339() 90 | } 91 | 92 | pub fn to_timestamp_millis(&self) -> i64 { 93 | self.ts 94 | } 95 | 96 | pub fn year(&self) -> i32 { 97 | self.as_datetime_tz().year() 98 | } 99 | 100 | pub fn month(&self) -> u32 { 101 | self.as_datetime_tz().month() 102 | } 103 | 104 | pub fn day(&self) -> u32 { 105 | self.as_datetime_tz().day() 106 | } 107 | 108 | pub fn hour(&self) -> u32 { 109 | self.as_datetime_tz().hour() 110 | } 111 | 112 | pub fn minute(&self) -> u32 { 113 | self.as_datetime_tz().minute() 114 | } 115 | 116 | pub fn second(&self) -> u32 { 117 | self.as_datetime_tz().second() 118 | } 119 | 120 | pub fn weekday(&self) -> u8 { 121 | match self.as_datetime_tz().weekday() { 122 | Weekday::Sun => 0, 123 | Weekday::Mon => 1, 124 | Weekday::Tue => 2, 125 | Weekday::Wed => 3, 126 | Weekday::Thu => 4, 127 | Weekday::Fri => 5, 128 | Weekday::Sat => 6, 129 | } 130 | } 131 | 132 | pub fn ordinal(&self) -> u32 { 133 | self.as_datetime_tz().ordinal() 134 | } 135 | 136 | pub fn iso_week(&self) -> u32 { 137 | self.as_datetime_tz().iso_week().week() 138 | } 139 | 140 | pub fn is_leap_year(&self) -> bool { 141 | let year = self.year(); 142 | (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) 143 | } 144 | 145 | fn as_datetime_tz(&self) -> DateTime { 146 | let utc_dt = Utc.timestamp_millis_opt(self.ts).single().unwrap(); 147 | 148 | utc_dt.with_timezone(&self.tz) 149 | } 150 | } 151 | 152 | impl Add for Date { 153 | type Output = Self; 154 | 155 | fn add(self, rhs: i64) -> Self::Output { 156 | Date { 157 | ts: self.ts + rhs, 158 | tz: self.tz, 159 | } 160 | } 161 | } 162 | 163 | impl Sub for Date { 164 | type Output = Self; 165 | 166 | fn sub(self, rhs: i64) -> Self::Output { 167 | Date { 168 | ts: self.ts - rhs, 169 | tz: self.tz, 170 | } 171 | } 172 | } 173 | 174 | impl Ord for Date { 175 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 176 | self.ts.cmp(&other.ts) 177 | } 178 | } 179 | 180 | impl PartialOrd for Date { 181 | fn partial_cmp(&self, other: &Self) -> Option { 182 | Some(self.ts.cmp(&other.ts)) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /crates/runtime/src/value.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::fmt; 3 | use std::fmt::Display; 4 | use std::rc::Rc; 5 | use tenda_scanner::Literal; 6 | 7 | use crate::associative_array::{AssociativeArray, AssociativeArrayKey}; 8 | use crate::date::Date; 9 | use crate::function::Function; 10 | 11 | #[derive(Debug, Clone, PartialEq)] 12 | pub enum Value { 13 | Number(f64), 14 | Boolean(bool), 15 | String(String), 16 | Function(Function), 17 | List(Rc>>), 18 | Range(usize, usize), 19 | AssociativeArray(Rc>), 20 | Date(Date), 21 | Nil, 22 | } 23 | 24 | impl Value { 25 | pub fn kind(&self) -> ValueType { 26 | use Value::*; 27 | 28 | match self { 29 | Number(_) => ValueType::Number, 30 | Boolean(_) => ValueType::Boolean, 31 | String(_) => ValueType::String, 32 | Function(_) => ValueType::Function, 33 | List(_) => ValueType::List, 34 | Range(_, _) => ValueType::Range, 35 | Nil => ValueType::Nil, 36 | AssociativeArray(_) => ValueType::AssociativeArray, 37 | Date(_) => ValueType::Date, 38 | } 39 | } 40 | 41 | pub fn to_bool(&self) -> bool { 42 | match self { 43 | Value::Number(value) => *value != 0.0, 44 | Value::Boolean(value) => *value, 45 | Value::String(_) => true, 46 | Value::Function(_) => true, 47 | Value::List(_) => true, 48 | Value::Range(_, _) => true, 49 | Value::Nil => false, 50 | Value::AssociativeArray(_) => true, 51 | Value::Date(_) => true, 52 | } 53 | } 54 | 55 | pub fn is_iterable(&self) -> bool { 56 | matches!(self, Value::List(_) | Value::Range(_, _)) 57 | } 58 | } 59 | 60 | impl Display for Value { 61 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 62 | use Value::*; 63 | 64 | write!( 65 | f, 66 | "{}", 67 | match self { 68 | Number(value) => match value { 69 | v if v.is_infinite() => { 70 | if v.is_sign_positive() { 71 | Literal::POSITIVE_INFINITY_LITERAL.to_string() 72 | } else { 73 | Literal::NEGATIVE_INFINITY_LITERAL.to_string() 74 | } 75 | } 76 | v if v.is_nan() => Literal::NAN_LITERAL.to_string(), 77 | _ => value.to_string(), 78 | }, 79 | Boolean(value) => match *value { 80 | true => Literal::TRUE_LITERAL.to_string(), 81 | false => Literal::FALSE_LITERAL.to_string(), 82 | }, 83 | String(value) => format!("\"{}\"", value), 84 | Function(value) => format!("", value.id), 85 | List(value) => format!( 86 | "[{}]", 87 | value 88 | .borrow() 89 | .iter() 90 | .map(|v| match v { 91 | Value::String(s) => format!("\"{}\"", escape_special_chars(s)), 92 | _ => v.to_string(), 93 | }) 94 | .collect::>() 95 | .join(", ") 96 | ), 97 | Range(start, end) => format!("{} até {}", start, end), 98 | Nil => Literal::NIL_LITERAL.to_string(), 99 | AssociativeArray(value) => format!( 100 | "{{ {} }}", 101 | value 102 | .borrow() 103 | .iter() 104 | .map(|(k, v)| match v { 105 | Value::String(s) => (k, format!("\"{}\"", escape_special_chars(s))), 106 | _ => (k, v.to_string()), 107 | }) 108 | .map(|(k, v)| match k { 109 | AssociativeArrayKey::String(key) => format!("\"{}\": {}", key, v), 110 | AssociativeArrayKey::Number(key) => format!("{}: {}", key, v), 111 | }) 112 | .collect::>() 113 | .join(", ") 114 | ), 115 | Date(value) => value.to_iso_string(), 116 | } 117 | ) 118 | } 119 | } 120 | 121 | impl From for Value { 122 | fn from(literal: Literal) -> Self { 123 | use Literal::*; 124 | 125 | match literal { 126 | Number(value) => Value::Number(value), 127 | String(value) => Value::String(value), 128 | Boolean(value) => Value::Boolean(value), 129 | Nil => Value::Nil, 130 | } 131 | } 132 | } 133 | 134 | impl IntoIterator for Value { 135 | type Item = Value; 136 | type IntoIter = std::vec::IntoIter; 137 | 138 | fn into_iter(self) -> Self::IntoIter { 139 | if !self.is_iterable() { 140 | panic!("value is not iterable"); 141 | } 142 | 143 | match self { 144 | Value::List(list) => list.borrow_mut().clone().into_iter(), 145 | Value::Range(start, end) => (start..=end) 146 | .map(|i| Value::Number(i as f64)) 147 | .collect::>() 148 | .into_iter(), 149 | _ => unreachable!(), 150 | } 151 | } 152 | } 153 | 154 | #[derive(Debug, PartialEq, Clone)] 155 | pub enum ValueType { 156 | Number, 157 | Boolean, 158 | String, 159 | Function, 160 | List, 161 | Range, 162 | Nil, 163 | AssociativeArray, 164 | Date, 165 | } 166 | 167 | impl From for ValueType { 168 | fn from(value: Value) -> Self { 169 | value.kind() 170 | } 171 | } 172 | 173 | impl Display for ValueType { 174 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 175 | use ValueType::*; 176 | 177 | let str = match self { 178 | Number => "número".to_string(), 179 | Boolean => "lógico".to_string(), 180 | String => "texto".to_string(), 181 | Function => "função".to_string(), 182 | List => "lista".to_string(), 183 | Range => "intervalo".to_string(), 184 | AssociativeArray => "dicionário".to_string(), 185 | Date => "data".to_string(), 186 | Nil => "Nada".to_string(), 187 | }; 188 | 189 | write!(f, "{}", str) 190 | } 191 | } 192 | 193 | pub fn escape_special_chars(s: &str) -> String { 194 | let mut result = String::with_capacity(s.len()); 195 | 196 | for c in s.chars() { 197 | match c { 198 | '\0' => result.push_str("\\0"), 199 | '\x07' => result.push_str("\\a"), 200 | '\x08' => result.push_str("\\b"), 201 | '\x0C' => result.push_str("\\f"), 202 | '\x0B' => result.push_str("\\v"), 203 | '\x1B' => result.push_str("\\e"), 204 | '\r' => result.push_str("\\r"), 205 | '\n' => result.push_str("\\n"), 206 | '\t' => result.push_str("\\t"), 207 | '\\' => result.push_str("\\\\"), 208 | '"' => result.push_str("\\\""), 209 | '\'' => result.push_str("\\\'"), 210 | 211 | c if c.is_control() => { 212 | let byte = c as u32; 213 | result.push_str(&format!("\\x{byte:02X}")); 214 | } 215 | 216 | _ => result.push(c), 217 | } 218 | } 219 | 220 | result 221 | } 222 | 223 | pub fn escape_value(value: &Value) -> String { 224 | match value { 225 | Value::String(s) => format!("\"{}\"", escape_special_chars(s)), 226 | _ => value.to_string(), 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /crates/parser/src/token_iter.rs: -------------------------------------------------------------------------------- 1 | use peekmore::{PeekMore, PeekMoreIterator}; 2 | use std::{cell::RefCell, ops::Neg, rc::Rc, slice::Iter}; 3 | use tenda_scanner::{Token, TokenKind}; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct TokenIterator<'a> { 7 | tokens: PeekMoreIterator>, 8 | ignoring_newline_counter: Rc>, 9 | last_token: &'a Token, 10 | } 11 | 12 | impl TokenIterator<'_> { 13 | pub fn peek(&mut self) -> Option<&&Token> { 14 | self.skip_ignored_newlines(); 15 | self.tokens.peek() 16 | } 17 | 18 | pub fn set_ignoring_newline(&mut self) -> NewlineGuard { 19 | NewlineGuard::new(self.ignoring_newline_counter.clone(), None) 20 | } 21 | 22 | pub fn halt_ignoring_newline(&mut self) -> NewlineGuard { 23 | let size = self.ignoring_newline_counter.borrow().neg(); 24 | 25 | NewlineGuard::new(self.ignoring_newline_counter.clone(), Some(size)) 26 | } 27 | 28 | pub fn consume_one_of(&mut self, token_types: &[TokenKind]) -> Option { 29 | self.tokens.reset_cursor(); 30 | 31 | while let Some(token) = self.tokens.peek() { 32 | if token.kind == TokenKind::Newline && *self.ignoring_newline_counter.borrow() > 0 { 33 | self.tokens.advance_cursor(); 34 | } else { 35 | break; 36 | } 37 | } 38 | 39 | let skipped = self.tokens.cursor(); 40 | 41 | if let Some(token) = self.tokens.peek() { 42 | if token_types.contains(&token.kind) { 43 | for _ in 0..skipped { 44 | self.tokens.next(); 45 | } 46 | 47 | return self.tokens.next().cloned(); 48 | } 49 | } 50 | 51 | self.tokens.reset_cursor(); 52 | 53 | None 54 | } 55 | 56 | pub fn consume_sequence(&mut self, token_types: &[TokenKind]) -> Option> { 57 | self.tokens.reset_cursor(); 58 | 59 | for token_type in token_types { 60 | while let Some(token) = self.tokens.peek() { 61 | if token.kind == TokenKind::Newline && *self.ignoring_newline_counter.borrow() > 0 { 62 | self.tokens.advance_cursor(); 63 | } else { 64 | break; 65 | } 66 | } 67 | 68 | match self.tokens.peek() { 69 | Some(token) if token.kind == *token_type => { 70 | self.tokens.advance_cursor(); 71 | } 72 | _ => { 73 | self.tokens.reset_cursor(); 74 | 75 | return None; 76 | } 77 | } 78 | } 79 | 80 | let count = self.tokens.cursor(); 81 | let mut tokens = Vec::with_capacity(count); 82 | 83 | for _ in 0..count { 84 | tokens.push(self.tokens.next().cloned().unwrap()); 85 | } 86 | 87 | Some(tokens) 88 | } 89 | 90 | pub fn check_sequence(&mut self, token_types: &[TokenKind]) -> bool { 91 | self.tokens.reset_cursor(); 92 | 93 | for token_type in token_types { 94 | while let Some(token) = self.tokens.peek() { 95 | if token.kind == TokenKind::Newline && *self.ignoring_newline_counter.borrow() > 0 { 96 | self.tokens.advance_cursor(); 97 | } else { 98 | break; 99 | } 100 | } 101 | 102 | match self.tokens.peek() { 103 | Some(token) if token.kind == *token_type => { 104 | self.tokens.advance_cursor(); 105 | } 106 | _ => { 107 | self.tokens.reset_cursor(); 108 | 109 | return false; 110 | } 111 | } 112 | } 113 | 114 | self.tokens.reset_cursor(); 115 | 116 | true 117 | } 118 | 119 | pub fn is_next_token(&mut self, token_type: TokenKind) -> bool { 120 | self.tokens.reset_cursor(); 121 | 122 | while let Some(token) = self.tokens.peek() { 123 | if token.kind == TokenKind::Newline && *self.ignoring_newline_counter.borrow() > 0 { 124 | self.tokens.advance_cursor(); 125 | } else { 126 | break; 127 | } 128 | } 129 | 130 | if let Some(token) = self.tokens.peek() { 131 | if token.kind == token_type { 132 | return true; 133 | } 134 | } 135 | 136 | self.tokens.reset_cursor(); 137 | 138 | false 139 | } 140 | 141 | pub fn is_next_eof(&mut self) -> bool { 142 | self.tokens.reset_cursor(); 143 | 144 | while let Some(token) = self.tokens.peek() { 145 | if token.kind == TokenKind::Newline { 146 | self.tokens.advance_cursor(); 147 | } else { 148 | break; 149 | } 150 | } 151 | 152 | let is_eof = matches!( 153 | self.tokens.peek(), 154 | None | Some(Token { 155 | kind: TokenKind::Eof, 156 | .. 157 | }) 158 | ); 159 | 160 | self.tokens.reset_cursor(); 161 | 162 | is_eof 163 | } 164 | 165 | pub fn is_next_valid(&mut self) -> bool { 166 | self.tokens.reset_cursor(); 167 | 168 | while let Some(token) = self.tokens.peek() { 169 | if token.kind == TokenKind::Newline && *self.ignoring_newline_counter.borrow() > 0 { 170 | self.tokens.advance_cursor(); 171 | } else { 172 | break; 173 | } 174 | } 175 | 176 | let is_next_valid = !matches!( 177 | self.tokens.peek(), 178 | None | Some(Token { 179 | kind: TokenKind::Eof, 180 | .. 181 | }) 182 | ); 183 | 184 | self.tokens.reset_cursor(); 185 | 186 | is_next_valid 187 | } 188 | 189 | pub fn advance_while(&mut self, expected_types: &[TokenKind]) { 190 | while let Some(token) = self.tokens.peek() { 191 | if expected_types.contains(&token.kind) { 192 | self.tokens.next(); 193 | } else { 194 | break; 195 | } 196 | } 197 | } 198 | 199 | pub fn last_token(&self) -> &Token { 200 | self.last_token 201 | } 202 | 203 | fn skip_ignored_newlines(&mut self) { 204 | while let Some(token) = self.tokens.peek() { 205 | if token.kind == TokenKind::Newline && *self.ignoring_newline_counter.borrow() > 0 { 206 | self.tokens.next(); 207 | } else { 208 | break; 209 | } 210 | } 211 | } 212 | } 213 | 214 | impl<'a> Iterator for TokenIterator<'a> { 215 | type Item = &'a Token; 216 | 217 | fn next(&mut self) -> Option { 218 | self.skip_ignored_newlines(); 219 | self.tokens.next() 220 | } 221 | } 222 | 223 | impl<'a> From<&'a [Token]> for TokenIterator<'a> { 224 | fn from(value: &'a [Token]) -> Self { 225 | TokenIterator { 226 | tokens: value.iter().peekmore(), 227 | ignoring_newline_counter: Rc::new(RefCell::new(0)), 228 | last_token: value.last().expect("token list should not be empty"), 229 | } 230 | } 231 | } 232 | 233 | pub struct NewlineGuard { 234 | counter: Rc>, 235 | size: isize, 236 | } 237 | 238 | impl NewlineGuard { 239 | pub fn new(counter: Rc>, custom_size: Option) -> Self { 240 | let size = custom_size.unwrap_or(1); 241 | 242 | *counter.borrow_mut() += size; 243 | 244 | NewlineGuard { counter, size } 245 | } 246 | } 247 | 248 | impl Drop for NewlineGuard { 249 | fn drop(&mut self) { 250 | *self.counter.borrow_mut() -= self.size; 251 | } 252 | } 253 | 254 | #[macro_export] 255 | macro_rules! token_slice { 256 | ($($kind:expr),*) => { 257 | { 258 | use tenda_scanner::TokenKind::*; 259 | &[$($kind),*] 260 | } 261 | }; 262 | } 263 | -------------------------------------------------------------------------------- /crates/tenda/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::{CommandFactory, Parser as CommandParser}; 2 | use reedline::{ 3 | default_emacs_keybindings, DefaultPrompt, Reedline, Signal, ValidationResult, Validator, 4 | }; 5 | use std::io::{IsTerminal, Read}; 6 | use std::rc::Rc; 7 | use std::{io, process}; 8 | use tenda_core::runtime::escape_value; 9 | use tenda_core::{ 10 | common::source::IdentifiedSource, parser::Parser, parser::ParserError, platform::OSPlatform, 11 | prelude::setup_runtime_prelude, reporting::Diagnostic, runtime::Runtime, scanner::LexicalError, 12 | scanner::Scanner, 13 | }; 14 | use yansi::Paint; 15 | 16 | #[derive(CommandParser)] 17 | #[command( 18 | author, 19 | version, 20 | about = "Tenda - interpretador e REPL", 21 | long_about = "Execute arquivos .tnd ou inicie o REPL da linguagem Tenda.", 22 | disable_help_flag = true, 23 | disable_version_flag = true, 24 | help_template = "\ 25 | {name} {version}\n\n\ 26 | {about-with-newline}\n\ 27 | \x1b[1;4mUso:\x1b[0m {usage}\n\n\ 28 | \x1b[1;4mArgumentos:\x1b[0m\n{positionals}\n\n\ 29 | \x1b[1;4mOpções:\x1b[0m\n{options}\n" 30 | )] 31 | struct Cli { 32 | #[arg(value_name = "ARQUIVO")] 33 | file: Option, 34 | 35 | #[arg( 36 | short = 'h', 37 | long = "help", 38 | alias = "ajuda", 39 | help = "Exibe ajuda (use '-h' para ver resumo)" 40 | )] 41 | help: bool, 42 | 43 | #[arg(short = 'V', long = "version", alias = "versão", help = "Exibe versão")] 44 | version: bool, 45 | } 46 | 47 | struct BlockValidator; 48 | 49 | impl Validator for BlockValidator { 50 | fn validate(&self, input: &str) -> ValidationResult { 51 | let dummy_source_id = IdentifiedSource::dummy(); 52 | 53 | let tokens = match Scanner::new(input, dummy_source_id).scan() { 54 | Ok(tokens) => tokens, 55 | Err(errors) => { 56 | if errors 57 | .iter() 58 | .any(|e| matches!(e, LexicalError::UnexpectedEoi { .. })) 59 | { 60 | return ValidationResult::Incomplete; 61 | } else { 62 | return ValidationResult::Complete; 63 | } 64 | } 65 | }; 66 | 67 | let mut parser = Parser::new(&tokens, dummy_source_id); 68 | 69 | match parser.parse() { 70 | Ok(_) => ValidationResult::Complete, 71 | Err(errors) => { 72 | let has_eoi = errors 73 | .iter() 74 | .any(|e| matches!(e, ParserError::UnexpectedEoi { .. })); 75 | 76 | if has_eoi { 77 | ValidationResult::Incomplete 78 | } else { 79 | ValidationResult::Complete 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | fn main() -> io::Result<()> { 87 | let cli = Cli::parse(); 88 | 89 | if cli.help { 90 | Cli::command().print_long_help().unwrap(); 91 | println!(); 92 | process::exit(0); 93 | } 94 | 95 | if cli.version { 96 | println!("tenda {}", env!("CARGO_PKG_VERSION")); 97 | process::exit(0); 98 | } 99 | 100 | if let Some(path) = cli.file { 101 | let file_content = std::fs::read_to_string(&path); 102 | 103 | match file_content { 104 | Ok(source) => run_source(&source, Box::leak(path.into_boxed_str())), 105 | Err(err) => match err.kind() { 106 | io::ErrorKind::NotFound => eprintln!("Arquivo não encontrado: {}", path), 107 | _ => eprintln!("Erro ao ler arquivo: {}", err), 108 | }, 109 | } 110 | 111 | return Ok(()); 112 | } 113 | 114 | let mut stdin = io::stdin(); 115 | 116 | if !stdin.is_terminal() { 117 | let mut buffer = String::new(); 118 | stdin.read_to_string(&mut buffer)?; 119 | 120 | run_source(&buffer, "stdin"); 121 | 122 | return Ok(()); 123 | } 124 | 125 | start_repl(); 126 | 127 | Ok(()) 128 | } 129 | 130 | fn start_repl() { 131 | let keybindings = default_emacs_keybindings(); 132 | let edit_mode = Box::new(reedline::Emacs::new(keybindings)); 133 | let validator = Box::new(BlockValidator); 134 | 135 | let mut rl = Reedline::create() 136 | .with_edit_mode(edit_mode) 137 | .with_validator(validator); 138 | 139 | let prompt = DefaultPrompt::new( 140 | reedline::DefaultPromptSegment::Empty, 141 | reedline::DefaultPromptSegment::Empty, 142 | ); 143 | 144 | let platform = OSPlatform; 145 | let mut runtime = Runtime::new(platform); 146 | let mut exiting = false; 147 | let mut source_history: Vec<(IdentifiedSource, Rc)> = Vec::new(); 148 | 149 | setup_runtime_prelude(runtime.get_global_env_mut()); 150 | 151 | loop { 152 | let sig = rl.read_line(&prompt); 153 | 154 | match sig { 155 | Ok(Signal::Success(line)) if line.trim() == ".sair" => break, 156 | Ok(Signal::Success(line)) => { 157 | exiting = false; 158 | 159 | let source_id = IdentifiedSource::new(); 160 | let source_rc = Rc::from(line.clone()); 161 | source_history.push((source_id, source_rc)); 162 | 163 | let tokens = match Scanner::new(&line, source_id).scan() { 164 | Ok(tokens) => tokens, 165 | Err(errs) => { 166 | for err in errs { 167 | let caches = tenda_core::reporting::sources(source_history.clone()); 168 | err.to_report().eprint(caches).unwrap(); 169 | } 170 | 171 | continue; 172 | } 173 | }; 174 | 175 | let ast = match Parser::new(&tokens, source_id).parse() { 176 | Ok(ast) => ast, 177 | Err(errs) => { 178 | for err in errs { 179 | let caches = tenda_core::reporting::sources(source_history.clone()); 180 | err.to_report().eprint(caches).unwrap(); 181 | } 182 | 183 | continue; 184 | } 185 | }; 186 | 187 | match runtime.eval(&ast) { 188 | Ok(result) => println!("{}", escape_value(&result)), 189 | Err(err) => { 190 | let caches = tenda_core::reporting::sources(source_history.clone()); 191 | err.to_report().eprint(caches).unwrap(); 192 | } 193 | } 194 | } 195 | Ok(Signal::CtrlC) if exiting => break, 196 | Ok(Signal::CtrlC) => { 197 | exiting = true; 198 | println!( 199 | "Para sair, pressione CTRL+C novamente, ou digite .sair, ou pressione CTRL+D" 200 | ); 201 | } 202 | Ok(Signal::CtrlD) => { 203 | println!("CTRL-D"); 204 | break; 205 | } 206 | Err(err) => { 207 | eprintln!("{}", err); 208 | break; 209 | } 210 | } 211 | } 212 | } 213 | 214 | fn run_source(source: &str, name: &'static str) { 215 | let platform = OSPlatform; 216 | 217 | let mut source_id = IdentifiedSource::new(); 218 | source_id.set_name(name); 219 | 220 | let cache = (source_id, tenda_core::reporting::Source::from(source)); 221 | 222 | let tokens = match Scanner::new(source, source_id).scan() { 223 | Ok(tokens) => tokens, 224 | Err(errs) => { 225 | let len = errs.len(); 226 | 227 | for err in errs { 228 | err.to_report().eprint(cache.clone()).unwrap(); 229 | } 230 | 231 | println!( 232 | "\n{} programa não pôde ser executado devido a {} erro(s) léxico(s) encontrado(s)", 233 | Paint::red("erro:").bold(), 234 | len, 235 | ); 236 | 237 | return; 238 | } 239 | }; 240 | 241 | let ast = match Parser::new(&tokens, source_id).parse() { 242 | Ok(ast) => ast, 243 | Err(errs) => { 244 | let len = errs.len(); 245 | 246 | for err in errs { 247 | err.to_report().eprint(cache.clone()).unwrap(); 248 | } 249 | 250 | println!( 251 | "\n{} programa não pôde ser executado devido a {} erro(s) de sintaxe encontrado(s)", 252 | Paint::red("erro:").bold(), 253 | len, 254 | ); 255 | 256 | return; 257 | } 258 | }; 259 | 260 | let mut runtime = Runtime::new(platform); 261 | 262 | setup_runtime_prelude(runtime.get_global_env_mut()); 263 | 264 | if let Err(err) = runtime.eval(&ast) { 265 | err.to_report().eprint(cache.clone()).unwrap(); 266 | 267 | println!( 268 | "\n{} programa encerrado devido a um erro durante a execução", 269 | Paint::red("erro:").bold(), 270 | ); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /crates/runtime/src/runtime_error.rs: -------------------------------------------------------------------------------- 1 | use tenda_common::span::SourceSpan; 2 | use tenda_reporting::{Diagnostic, DiagnosticConfig, HasDiagnosticHooks}; 3 | use tenda_reporting_derive::Diagnostic; 4 | use thiserror::Error; 5 | 6 | use crate::{ 7 | associative_array::AssociativeArrayKey, 8 | value::{Value, ValueType}, 9 | }; 10 | 11 | pub type Result = std::result::Result>; 12 | 13 | #[derive(Debug, Clone, PartialEq)] 14 | pub enum FunctionName { 15 | Anonymous, 16 | TopLevel, 17 | Named(String), 18 | } 19 | 20 | #[derive(Debug, Clone, PartialEq)] 21 | pub struct StackFrame { 22 | pub function_name: FunctionName, 23 | pub location: Option, 24 | } 25 | 26 | impl StackFrame { 27 | pub fn new(function_name: FunctionName, location: Option) -> Self { 28 | Self { 29 | function_name, 30 | location, 31 | } 32 | } 33 | } 34 | 35 | impl From for tenda_reporting::StackFrame { 36 | fn from(val: StackFrame) -> Self { 37 | let function_name = match val.function_name { 38 | FunctionName::Anonymous => "".to_string(), 39 | FunctionName::TopLevel => "".to_string(), 40 | FunctionName::Named(name) => name, 41 | }; 42 | 43 | tenda_reporting::StackFrame::new(function_name, val.location) 44 | } 45 | } 46 | 47 | #[derive(Error, Debug, PartialEq, Clone, Diagnostic)] 48 | #[accept_hooks] 49 | #[report("erro de execução")] 50 | pub enum RuntimeError { 51 | #[error("divisão por zero não é permitida")] 52 | DivisionByZero { 53 | #[span] 54 | span: Option, 55 | 56 | #[metadata] 57 | stacktrace: Vec, 58 | }, 59 | 60 | #[error("operação inválida para os tipos '{}' e '{}'", .first.to_string(), .second.to_string())] 61 | TypeMismatch { 62 | first: ValueType, 63 | second: ValueType, 64 | 65 | #[span] 66 | span: Option, 67 | 68 | #[message] 69 | message: Option, 70 | 71 | #[metadata] 72 | stacktrace: Vec, 73 | }, 74 | 75 | #[error("esperado valor de tipo '{}', encontrado '{}'", .expected.to_string(), .found.to_string())] 76 | UnexpectedTypeError { 77 | expected: ValueType, 78 | found: ValueType, 79 | 80 | #[message] 81 | message: Option, 82 | 83 | #[span] 84 | span: Option, 85 | 86 | #[metadata] 87 | stacktrace: Vec, 88 | }, 89 | 90 | #[error("a variável identificada por '{}' não está definida neste escopo", .var_name)] 91 | UndefinedReference { 92 | var_name: String, 93 | 94 | #[span] 95 | span: Option, 96 | 97 | #[help] 98 | help: Option, 99 | 100 | #[metadata] 101 | stacktrace: Vec, 102 | }, 103 | 104 | #[error("variável identificada por {0} já está declarada neste escopo", .var_name)] 105 | AlreadyDeclared { 106 | var_name: String, 107 | 108 | #[span] 109 | span: Option, 110 | 111 | #[help] 112 | help: Option, 113 | 114 | #[metadata] 115 | stacktrace: Vec, 116 | }, 117 | 118 | #[error("número de argumentos incorreto: esperado {}, encontrado {}", .expected, .found)] 119 | WrongNumberOfArguments { 120 | expected: usize, 121 | found: usize, 122 | 123 | #[span] 124 | span: Option, 125 | 126 | #[metadata] 127 | stacktrace: Vec, 128 | }, 129 | 130 | #[error("índice fora dos limites: índice {}, tamanho {}", .index, .len)] 131 | IndexOutOfBounds { 132 | index: usize, 133 | len: usize, 134 | 135 | #[span] 136 | span: Option, 137 | 138 | #[help] 139 | help: Vec, 140 | 141 | #[metadata] 142 | stacktrace: Vec, 143 | }, 144 | 145 | #[error("não é possível acessar um valor do tipo '{}'", .value.to_string())] 146 | WrongIndexType { 147 | value: ValueType, 148 | 149 | #[span] 150 | span: Option, 151 | 152 | #[metadata] 153 | stacktrace: Vec, 154 | }, 155 | 156 | #[error("limites de intervalo precisam ser números inteiros finitos: encontrado '{}'", .bound)] 157 | InvalidRangeBounds { 158 | bound: f64, 159 | 160 | #[span] 161 | span: Option, 162 | 163 | #[metadata] 164 | stacktrace: Vec, 165 | }, 166 | 167 | #[error("índice de lista precisa ser um número inteiro positivo e finito: encontrado '{}'", .index)] 168 | InvalidIndex { 169 | index: f64, 170 | 171 | #[span] 172 | span: Option, 173 | 174 | #[metadata] 175 | stacktrace: Vec, 176 | }, 177 | 178 | #[error("chave de dicionário precisa ser número inteiro ou texto: encontrado '{}'", .key)] 179 | InvalidNumberAssociativeArrayKey { 180 | key: f64, 181 | 182 | #[span] 183 | span: Option, 184 | 185 | #[metadata] 186 | stacktrace: Vec, 187 | }, 188 | 189 | #[error("chave de dicionário precisa ser número inteiro ou texto: encontrado '{}'", .key)] 190 | InvalidTypeAssociativeArrayKey { 191 | key: ValueType, 192 | 193 | #[span] 194 | span: Option, 195 | 196 | #[metadata] 197 | stacktrace: Vec, 198 | }, 199 | 200 | #[error("chave de dicionário não encontrada: '{}'", .key.to_string())] 201 | AssociativeArrayKeyNotFound { 202 | key: AssociativeArrayKey, 203 | 204 | #[span] 205 | span: Option, 206 | 207 | #[metadata] 208 | stacktrace: Vec, 209 | }, 210 | 211 | #[error("não é possível iterar sobre um valor do tipo '{}'", .value.to_string())] 212 | NotIterable { 213 | value: ValueType, 214 | 215 | #[span] 216 | span: Option, 217 | 218 | #[metadata] 219 | stacktrace: Vec, 220 | }, 221 | 222 | #[error("o valor do tipo '{}' não é um argumento válido para a função", .value.to_string())] 223 | InvalidArgument { 224 | value: Value, 225 | 226 | #[span] 227 | span: Option, 228 | 229 | #[metadata] 230 | stacktrace: Vec, 231 | }, 232 | 233 | #[error("textos são imutáveis e não podem ser modificados")] 234 | ImmutableString { 235 | #[span] 236 | span: Option, 237 | 238 | #[help] 239 | help: Option, 240 | 241 | #[metadata] 242 | stacktrace: Vec, 243 | }, 244 | 245 | #[error("timestamp inválido: {}", .timestamp.to_string())] 246 | InvalidTimestamp { 247 | timestamp: i64, 248 | 249 | #[span] 250 | span: Option, 251 | 252 | #[metadata] 253 | stacktrace: Vec, 254 | }, 255 | 256 | #[error("falha ao analisar data ISO: {}", .source)] 257 | DateIsoParseError { 258 | source: chrono::ParseError, 259 | 260 | #[span] 261 | span: Option, 262 | 263 | #[metadata] 264 | stacktrace: Vec, 265 | }, 266 | 267 | #[error("fuso horário inválido: '{tz_str}'")] 268 | InvalidTimeZoneString { 269 | tz_str: String, 270 | 271 | #[span] 272 | span: Option, 273 | 274 | #[metadata] 275 | stacktrace: Vec, 276 | }, 277 | 278 | #[error("valor inválido para conversão para tipo '{}'", .value.to_string())] 279 | InvalidValueForConversion { 280 | value: Value, 281 | 282 | #[span] 283 | span: Option, 284 | 285 | #[metadata] 286 | stacktrace: Vec, 287 | }, 288 | } 289 | 290 | impl HasDiagnosticHooks for RuntimeError { 291 | fn hooks() -> &'static [fn(&Self, DiagnosticConfig) -> DiagnosticConfig] 292 | { 293 | &[add_stacktrace] 294 | } 295 | } 296 | 297 | /// Builds a Vec from a RuntimeError’s stacktrace: 298 | /// - Returns `None` if there’s no stacktrace or it’s empty. 299 | /// - Otherwise, returns `Some(frames)` where `frames.len() == original.len() + 1`: 300 | /// 1. **First frame**: the first entry’s function name paired with the error span. 301 | /// 2. **Middle frames**: for each adjacent pair in the original stacktrace, 302 | /// the later function name is paired with the previous call-site location. 303 | /// 3. **Last frame**: a “top-level” placeholder (`FunctionName::TopLevel`) 304 | /// paired with the last call-site location. 305 | /// 306 | /// Note: at the moment in the runtime we’re still associating function names with 307 | /// their call sites rather than their actual in-function error sites. 308 | /// This is a temporary workaround until we can implement a better solution. 309 | fn build_stack_frames(runtime_error: &RuntimeError) -> Option> { 310 | let st = runtime_error.get_stacktrace()?; 311 | 312 | if st.is_empty() { 313 | return None; 314 | } 315 | 316 | let mut frames = Vec::with_capacity(st.len() + 1); 317 | 318 | frames.push(StackFrame::new( 319 | st[0].function_name.clone(), 320 | runtime_error.get_span().clone(), 321 | )); 322 | 323 | for window in st.windows(2) { 324 | let (prev, curr) = (&window[0], &window[1]); 325 | 326 | frames.push(StackFrame::new( 327 | curr.function_name.clone(), 328 | prev.location.clone(), 329 | )); 330 | } 331 | 332 | frames.push(StackFrame::new( 333 | FunctionName::TopLevel, 334 | st.last().unwrap().location.clone(), 335 | )); 336 | 337 | Some(frames) 338 | } 339 | 340 | fn add_stacktrace( 341 | runtime_error: &RuntimeError, 342 | config: DiagnosticConfig, 343 | ) -> DiagnosticConfig { 344 | match build_stack_frames(runtime_error) { 345 | Some(frames) => config.stacktrace(frames.into_iter().map(Into::into).collect()), 346 | None => config, 347 | } 348 | } 349 | 350 | macro_rules! attach_span_if_missing { 351 | ($err:expr, $span:expr) => {{ 352 | if $err.get_span().is_none() { 353 | $err.set_span($span); 354 | } 355 | 356 | $err 357 | }}; 358 | } 359 | 360 | pub(crate) use attach_span_if_missing; 361 | -------------------------------------------------------------------------------- /crates/parser/src/ast.rs: -------------------------------------------------------------------------------- 1 | use tenda_common::span::SourceSpan; 2 | use tenda_scanner::{Token, TokenKind}; 3 | 4 | #[derive(Debug, PartialEq, Clone)] 5 | pub struct Ast { 6 | pub inner: Vec, 7 | pub span: SourceSpan, 8 | } 9 | 10 | impl Ast { 11 | pub fn new(span: SourceSpan) -> Self { 12 | Ast { 13 | inner: vec![], 14 | span, 15 | } 16 | } 17 | 18 | pub fn from(inner: Vec, span: SourceSpan) -> Self { 19 | Ast { inner, span } 20 | } 21 | } 22 | 23 | impl IntoIterator for Ast { 24 | type Item = Stmt; 25 | type IntoIter = std::vec::IntoIter; 26 | 27 | fn into_iter(self) -> Self::IntoIter { 28 | self.inner.into_iter() 29 | } 30 | } 31 | 32 | #[derive(Debug, PartialEq, Clone)] 33 | pub enum Stmt { 34 | Expr(Expr), 35 | Decl(Decl), 36 | Cond(Cond), 37 | While(While), 38 | ForEach(ForEach), 39 | Block(Block), 40 | Return(Return), 41 | Break(Break), 42 | Continue(Continue), 43 | } 44 | 45 | impl Stmt { 46 | pub fn get_span(&self) -> &SourceSpan { 47 | match self { 48 | Stmt::Expr(expr) => expr.get_span(), 49 | Stmt::Decl(decl) => match decl { 50 | Decl::Local(local) => &local.span, 51 | Decl::Function(function) => &function.span, 52 | }, 53 | Stmt::Cond(cond) => &cond.span, 54 | Stmt::While(while_stmt) => &while_stmt.span, 55 | Stmt::ForEach(for_each) => &for_each.span, 56 | Stmt::Block(block) => &block.span, 57 | Stmt::Return(return_stmt) => &return_stmt.span, 58 | Stmt::Break(break_stmt) => &break_stmt.span, 59 | Stmt::Continue(continue_stmt) => &continue_stmt.span, 60 | } 61 | } 62 | } 63 | 64 | #[derive(Debug, PartialEq, Clone)] 65 | pub struct Return { 66 | pub value: Option, 67 | pub span: SourceSpan, 68 | } 69 | 70 | impl Return { 71 | pub fn new(value: Option, span: SourceSpan) -> Self { 72 | Return { value, span } 73 | } 74 | } 75 | 76 | #[derive(Debug, PartialEq, Clone)] 77 | pub struct Break { 78 | pub span: SourceSpan, 79 | } 80 | 81 | impl Break { 82 | pub fn new(span: SourceSpan) -> Self { 83 | Break { span } 84 | } 85 | } 86 | 87 | #[derive(Debug, PartialEq, Clone)] 88 | pub struct Continue { 89 | pub span: SourceSpan, 90 | } 91 | 92 | impl Continue { 93 | pub fn new(span: SourceSpan) -> Self { 94 | Continue { span } 95 | } 96 | } 97 | 98 | #[derive(Debug, PartialEq, Clone)] 99 | pub struct Block { 100 | pub inner: Ast, 101 | pub span: SourceSpan, 102 | } 103 | 104 | impl Block { 105 | pub fn new(inner: Ast, span: SourceSpan) -> Self { 106 | Block { inner, span } 107 | } 108 | } 109 | 110 | #[derive(Debug, PartialEq, Clone)] 111 | pub enum Decl { 112 | Local(LocalDecl), 113 | Function(FunctionDecl), 114 | } 115 | 116 | impl Decl { 117 | pub fn get_name(&self) -> &str { 118 | match self { 119 | Decl::Local(local) => &local.name, 120 | Decl::Function(function) => &function.name, 121 | } 122 | } 123 | 124 | pub fn get_uid(&self) -> usize { 125 | match self { 126 | Decl::Local(local) => local.uid, 127 | Decl::Function(function) => function.uid, 128 | } 129 | } 130 | } 131 | 132 | #[derive(Debug, PartialEq, Clone)] 133 | pub struct LocalDecl { 134 | pub name: String, 135 | pub value: Expr, 136 | pub captured: bool, 137 | pub uid: usize, 138 | pub span: SourceSpan, 139 | } 140 | 141 | impl LocalDecl { 142 | pub fn new(name: String, value: Expr, uid: usize, span: SourceSpan) -> Self { 143 | LocalDecl { 144 | name, 145 | value, 146 | captured: false, 147 | uid, 148 | span, 149 | } 150 | } 151 | } 152 | 153 | #[derive(Debug, PartialEq, Clone)] 154 | pub struct FunctionParam { 155 | pub name: String, 156 | pub uid: usize, 157 | pub captured: bool, 158 | pub span: SourceSpan, 159 | } 160 | 161 | impl FunctionParam { 162 | pub fn new(name: String, uid: usize, span: SourceSpan) -> Self { 163 | FunctionParam { 164 | name, 165 | uid, 166 | captured: false, 167 | span, 168 | } 169 | } 170 | } 171 | 172 | #[derive(Debug, PartialEq, Clone)] 173 | pub struct FunctionDecl { 174 | pub name: String, 175 | pub params: Vec, 176 | pub body: Box, 177 | pub free_vars: Vec, 178 | pub captured: bool, 179 | pub uid: usize, 180 | pub span: SourceSpan, 181 | } 182 | 183 | impl FunctionDecl { 184 | pub fn new( 185 | name: String, 186 | params: Vec, 187 | body: Stmt, 188 | uid: usize, 189 | span: SourceSpan, 190 | ) -> Self { 191 | FunctionDecl { 192 | name, 193 | params, 194 | body: Box::new(body), 195 | free_vars: vec![], 196 | captured: false, 197 | uid, 198 | span, 199 | } 200 | } 201 | } 202 | 203 | #[derive(Debug, PartialEq, Clone)] 204 | pub struct Cond { 205 | pub cond: Expr, 206 | pub then: Box, 207 | pub or_else: Option>, 208 | pub span: SourceSpan, 209 | } 210 | 211 | impl Cond { 212 | pub fn new(cond: Expr, then: Stmt, or_else: Option, span: SourceSpan) -> Self { 213 | Cond { 214 | cond, 215 | then: Box::new(then), 216 | or_else: or_else.map(Box::new), 217 | span, 218 | } 219 | } 220 | } 221 | 222 | #[derive(Debug, PartialEq, Clone)] 223 | pub struct While { 224 | pub cond: Expr, 225 | pub body: Box, 226 | pub span: SourceSpan, 227 | } 228 | 229 | impl While { 230 | pub fn new(cond: Expr, body: Stmt, span: SourceSpan) -> Self { 231 | While { 232 | cond, 233 | body: Box::new(body), 234 | span, 235 | } 236 | } 237 | } 238 | 239 | #[derive(Debug, PartialEq, Clone)] 240 | pub struct ForEachItem { 241 | pub name: String, 242 | pub uid: usize, 243 | pub captured: bool, 244 | pub span: SourceSpan, 245 | } 246 | 247 | impl ForEachItem { 248 | pub fn new(name: String, uid: usize, span: SourceSpan) -> Self { 249 | ForEachItem { 250 | name, 251 | uid, 252 | captured: false, 253 | span, 254 | } 255 | } 256 | } 257 | 258 | #[derive(Debug, PartialEq, Clone)] 259 | pub struct ForEach { 260 | pub item: ForEachItem, 261 | pub iterable: Expr, 262 | pub body: Box, 263 | pub span: SourceSpan, 264 | } 265 | 266 | impl ForEach { 267 | pub fn new(item: ForEachItem, iterable: Expr, body: Stmt, span: SourceSpan) -> Self { 268 | ForEach { 269 | item, 270 | iterable, 271 | body: Box::new(body), 272 | span, 273 | } 274 | } 275 | } 276 | 277 | #[derive(Debug, PartialEq, Clone)] 278 | pub enum Expr { 279 | Binary(BinaryOp), 280 | Unary(UnaryOp), 281 | Ternary(TernaryOp), 282 | Call(Call), 283 | Assign(Assign), 284 | Access(Access), 285 | List(List), 286 | Grouping(Grouping), 287 | Literal(Literal), 288 | Variable(Variable), 289 | AssociativeArray(AssociativeArray), 290 | AnonymousFunction(AnonymousFunction), 291 | } 292 | 293 | impl Expr { 294 | pub fn get_span(&self) -> &SourceSpan { 295 | match self { 296 | Expr::Binary(binary_op) => &binary_op.span, 297 | Expr::Unary(unary_op) => &unary_op.span, 298 | Expr::Ternary(ternary_op) => &ternary_op.span, 299 | Expr::Call(call) => &call.span, 300 | Expr::Assign(assign) => &assign.span, 301 | Expr::Access(access) => &access.span, 302 | Expr::List(list) => &list.span, 303 | Expr::Grouping(grouping) => &grouping.span, 304 | Expr::Literal(literal) => &literal.span, 305 | Expr::Variable(variable) => &variable.span, 306 | Expr::AssociativeArray(associative_array) => &associative_array.span, 307 | Expr::AnonymousFunction(anonymous_function) => &anonymous_function.span, 308 | } 309 | } 310 | } 311 | 312 | #[derive(Debug, PartialEq, Clone)] 313 | pub struct BinaryOp { 314 | pub lhs: Box, 315 | pub op: BinaryOperator, 316 | pub rhs: Box, 317 | pub span: SourceSpan, 318 | } 319 | 320 | impl BinaryOp { 321 | pub fn new(lhs: Expr, op: BinaryOperator, rhs: Expr, span: SourceSpan) -> Self { 322 | BinaryOp { 323 | lhs: Box::new(lhs), 324 | op, 325 | rhs: Box::new(rhs), 326 | span, 327 | } 328 | } 329 | } 330 | 331 | #[derive(Debug, PartialEq, Clone)] 332 | pub struct UnaryOp { 333 | pub op: UnaryOperator, 334 | pub rhs: Box, 335 | pub span: SourceSpan, 336 | } 337 | 338 | impl UnaryOp { 339 | pub fn new(op: UnaryOperator, rhs: Expr, span: SourceSpan) -> Self { 340 | UnaryOp { 341 | op, 342 | rhs: Box::new(rhs), 343 | span, 344 | } 345 | } 346 | } 347 | 348 | #[derive(Debug, PartialEq, Clone)] 349 | pub struct TernaryOp { 350 | pub cond: Box, 351 | pub then: Box, 352 | pub or_else: Box, 353 | pub span: SourceSpan, 354 | } 355 | 356 | impl TernaryOp { 357 | pub fn new(cond: Expr, then: Expr, or_else: Expr, span: SourceSpan) -> Self { 358 | TernaryOp { 359 | cond: Box::new(cond), 360 | then: Box::new(then), 361 | or_else: Box::new(or_else), 362 | span, 363 | } 364 | } 365 | } 366 | 367 | #[derive(Debug, PartialEq, Clone)] 368 | pub struct Call { 369 | pub callee: Box, 370 | pub args: Vec, 371 | pub span: SourceSpan, 372 | } 373 | 374 | impl Call { 375 | pub fn new(callee: Expr, args: Vec, span: SourceSpan) -> Self { 376 | Call { 377 | callee: Box::new(callee), 378 | args, 379 | span, 380 | } 381 | } 382 | } 383 | 384 | #[derive(Debug, PartialEq, Clone)] 385 | pub struct Assign { 386 | pub name: Box, 387 | pub value: Box, 388 | pub span: SourceSpan, 389 | } 390 | 391 | impl Assign { 392 | pub fn new(name: Expr, value: Expr, span: SourceSpan) -> Self { 393 | Assign { 394 | name: Box::new(name), 395 | value: Box::new(value), 396 | span, 397 | } 398 | } 399 | } 400 | 401 | #[derive(Debug, PartialEq, Clone)] 402 | pub struct Access { 403 | pub subscripted: Box, 404 | pub index: Box, 405 | pub span: SourceSpan, 406 | } 407 | 408 | impl Access { 409 | pub fn new(subscripted: Expr, index: Expr, span: SourceSpan) -> Self { 410 | Access { 411 | subscripted: Box::new(subscripted), 412 | index: Box::new(index), 413 | span, 414 | } 415 | } 416 | } 417 | 418 | #[derive(Debug, PartialEq, Clone)] 419 | pub struct List { 420 | pub elements: Vec, 421 | pub span: SourceSpan, 422 | } 423 | 424 | impl List { 425 | pub fn new(elements: Vec, span: SourceSpan) -> Self { 426 | List { elements, span } 427 | } 428 | } 429 | 430 | #[derive(Debug, PartialEq, Clone)] 431 | pub struct AssociativeArray { 432 | pub elements: Vec<(Literal, Expr)>, 433 | pub span: SourceSpan, 434 | } 435 | 436 | impl AssociativeArray { 437 | pub fn new(elements: Vec<(Literal, Expr)>, span: SourceSpan) -> Self { 438 | AssociativeArray { elements, span } 439 | } 440 | } 441 | 442 | #[derive(Debug, PartialEq, Clone)] 443 | pub struct Grouping { 444 | pub expr: Box, 445 | pub span: SourceSpan, 446 | } 447 | 448 | impl Grouping { 449 | pub fn new(expr: Expr, span: SourceSpan) -> Self { 450 | Grouping { 451 | expr: Box::new(expr), 452 | span, 453 | } 454 | } 455 | } 456 | 457 | #[derive(Debug, PartialEq, Clone)] 458 | pub struct Literal { 459 | pub value: tenda_scanner::Literal, 460 | pub span: SourceSpan, 461 | } 462 | 463 | impl Literal { 464 | pub fn new(value: tenda_scanner::Literal, span: SourceSpan) -> Self { 465 | Literal { value, span } 466 | } 467 | } 468 | 469 | #[derive(Debug, PartialEq, Clone)] 470 | pub struct Variable { 471 | pub name: String, 472 | pub uid: usize, 473 | pub captured: bool, 474 | pub span: SourceSpan, 475 | } 476 | 477 | impl Variable { 478 | pub fn new(name: String, id: usize, span: SourceSpan) -> Self { 479 | Variable { 480 | name, 481 | uid: id, 482 | captured: false, 483 | span, 484 | } 485 | } 486 | } 487 | 488 | #[derive(Debug, PartialEq, Clone)] 489 | pub struct AnonymousFunction { 490 | pub params: Vec, 491 | pub body: Box, 492 | pub uid: usize, 493 | pub free_vars: Vec, 494 | pub span: SourceSpan, 495 | } 496 | 497 | impl AnonymousFunction { 498 | pub fn new(params: Vec, body: Stmt, uid: usize, span: SourceSpan) -> Self { 499 | AnonymousFunction { 500 | params, 501 | body: Box::new(body), 502 | uid, 503 | free_vars: vec![], 504 | span, 505 | } 506 | } 507 | } 508 | 509 | #[derive(Debug, Copy, Clone, PartialEq)] 510 | pub enum BinaryOperator { 511 | Add, 512 | Subtract, 513 | Multiply, 514 | Divide, 515 | Exponentiation, 516 | Modulo, 517 | Equality, 518 | Inequality, 519 | Greater, 520 | GreaterOrEqual, 521 | Less, 522 | LessOrEqual, 523 | LogicalAnd, 524 | LogicalOr, 525 | Range, 526 | Has, 527 | Lacks, 528 | } 529 | 530 | impl From for BinaryOperator { 531 | fn from(value: Token) -> Self { 532 | use BinaryOperator::*; 533 | 534 | match value.kind { 535 | TokenKind::Plus => Add, 536 | TokenKind::Minus => Subtract, 537 | TokenKind::Star => Multiply, 538 | TokenKind::Slash => Divide, 539 | TokenKind::Percent => Modulo, 540 | TokenKind::Caret => Exponentiation, 541 | TokenKind::Equals => Equality, 542 | TokenKind::Greater => Greater, 543 | TokenKind::GreaterOrEqual => GreaterOrEqual, 544 | TokenKind::Less => Less, 545 | TokenKind::LessOrEqual => LessOrEqual, 546 | TokenKind::Or => LogicalOr, 547 | TokenKind::And => LogicalAnd, 548 | TokenKind::Until => Range, 549 | _ => panic!("invalid token for binary operation"), 550 | } 551 | } 552 | } 553 | 554 | #[derive(Debug, Copy, Clone, PartialEq)] 555 | pub enum UnaryOperator { 556 | Negative, 557 | LogicalNot, 558 | } 559 | 560 | impl From for UnaryOperator { 561 | fn from(value: Token) -> Self { 562 | use UnaryOperator::*; 563 | 564 | match value.kind { 565 | TokenKind::Minus => Negative, 566 | TokenKind::Not => LogicalNot, 567 | _ => panic!("invalid token for unary operation"), 568 | } 569 | } 570 | } 571 | -------------------------------------------------------------------------------- /crates/scanner/src/scanner.rs: -------------------------------------------------------------------------------- 1 | use tenda_common::source::IdentifiedSource; 2 | 3 | use crate::scanner_error::LexicalError; 4 | use crate::source_iter::SourceIter; 5 | use crate::token::{Literal, Token, TokenKind}; 6 | use std::char; 7 | 8 | pub struct Scanner<'a> { 9 | source: SourceIter<'a>, 10 | } 11 | 12 | impl<'a> Scanner<'a> { 13 | pub fn new(source: &'a str, source_id: IdentifiedSource) -> Scanner<'a> { 14 | Scanner { 15 | source: SourceIter::new(source, source_id), 16 | } 17 | } 18 | 19 | pub fn scan(&mut self) -> Result, Vec> { 20 | let mut tokens: Vec = Vec::new(); 21 | let mut errors = Vec::new(); 22 | let mut had_error = false; 23 | 24 | while let Some(c) = self.source.next() { 25 | let token = self.consume_token(c, tokens.last()); 26 | 27 | match token { 28 | Ok(Some(value)) => { 29 | had_error = false; 30 | tokens.push(value) 31 | } 32 | Err(err) if !had_error => { 33 | had_error = true; 34 | errors.push(err); 35 | } 36 | _ => (), 37 | }; 38 | } 39 | 40 | tokens.push(self.source.consume_eof()); 41 | 42 | if errors.is_empty() { 43 | Ok(tokens) 44 | } else { 45 | Err(errors) 46 | } 47 | } 48 | 49 | fn consume_token( 50 | &mut self, 51 | char: char, 52 | previous_token: Option<&Token>, 53 | ) -> Result, LexicalError> { 54 | match char { 55 | '\n' => match previous_token { 56 | Some(token) if token.kind != TokenKind::Newline => { 57 | self.source.consume_token(TokenKind::Newline, "\n").into() 58 | } 59 | _ => { 60 | self.source.ignore_char(); 61 | Ok(None) 62 | } 63 | }, 64 | c if c.is_whitespace() => { 65 | self.source.ignore_char(); 66 | Ok(None) 67 | } 68 | '(' => self.source.consume_token(TokenKind::LeftParen, "(").into(), 69 | ')' => self.source.consume_token(TokenKind::RightParen, ")").into(), 70 | '[' => self 71 | .source 72 | .consume_token(TokenKind::LeftBracket, "[") 73 | .into(), 74 | ']' => self 75 | .source 76 | .consume_token(TokenKind::RightBracket, "]") 77 | .into(), 78 | '{' => self.source.consume_token(TokenKind::LeftBrace, "{").into(), 79 | '}' => self.source.consume_token(TokenKind::RightBrace, "}").into(), 80 | ':' => self.source.consume_token(TokenKind::Colon, ":").into(), 81 | '+' => self.source.consume_token(TokenKind::Plus, "+").into(), 82 | '-' => { 83 | if let Some('>') = self.source.peek() { 84 | self.source.next(); 85 | self.source.consume_token(TokenKind::Arrow, "->").into() 86 | } else { 87 | self.source.consume_token(TokenKind::Minus, "-").into() 88 | } 89 | } 90 | '*' => self.source.consume_token(TokenKind::Star, "*").into(), 91 | '^' => self.source.consume_token(TokenKind::Caret, "^").into(), 92 | '%' => self.source.consume_token(TokenKind::Percent, "%").into(), 93 | '=' => self.source.consume_token(TokenKind::EqualSign, "=").into(), 94 | '"' => self.consume_string(char).map(Some), 95 | ',' => self.source.consume_token(TokenKind::Comma, ",").into(), 96 | '.' => self.source.consume_token(TokenKind::Dot, ".").into(), 97 | '>' => match self.source.peek() { 98 | Some('=') => { 99 | self.source.next(); 100 | self.source 101 | .consume_token(TokenKind::GreaterOrEqual, ">") 102 | .into() 103 | } 104 | _ => self.source.consume_token(TokenKind::Greater, ">").into(), 105 | }, 106 | '<' => match self.source.peek() { 107 | Some('=') => { 108 | self.source.next(); 109 | self.source 110 | .consume_token(TokenKind::LessOrEqual, "<") 111 | .into() 112 | } 113 | _ => self.source.consume_token(TokenKind::Less, "<").into(), 114 | }, 115 | c if c.is_ascii_digit() => self.consume_number(c).map(Some), 116 | c if c.is_alphabetic() || c == '_' => self.consume_identifier(c).map(Some), 117 | '/' => match self.source.peek() { 118 | Some('/') => { 119 | self.consume_comment(); 120 | Ok(None) 121 | } 122 | Some('*') => { 123 | self.consume_multiline_comment(); 124 | Ok(None) 125 | } 126 | _ => self.source.consume_token(TokenKind::Slash, "/").into(), 127 | }, 128 | _ => Err(LexicalError::UnexpectedChar { 129 | character: char, 130 | span: self.source.consume_span(), 131 | }), 132 | } 133 | } 134 | 135 | fn consume_string(&mut self, first_quote: char) -> Result { 136 | let mut buf = String::new(); 137 | let mut closed = false; 138 | 139 | buf.push(first_quote); 140 | 141 | while let Some(&ch) = self.source.peek() { 142 | match ch { 143 | '"' => { 144 | self.source.next(); 145 | closed = true; 146 | break; 147 | } 148 | '\n' => { 149 | return Err(LexicalError::UnexpectedStringEol { 150 | span: self.source.consume_span(), 151 | }); 152 | } 153 | '\\' => { 154 | self.source.next(); 155 | 156 | let esc = self 157 | .source 158 | .next() 159 | .ok_or(LexicalError::UnexpectedStringEol { 160 | span: self.source.consume_span(), 161 | })?; 162 | 163 | let resolved = match esc { 164 | '0' => Some('\0'), 165 | 'a' => Some('\x07'), 166 | 'b' => Some('\x08'), 167 | 'e' => Some('\x1B'), 168 | 'f' => Some('\x0C'), 169 | 'n' => Some('\n'), 170 | 'r' => Some('\r'), 171 | 't' => Some('\t'), 172 | 'v' => Some('\x0B'), 173 | '\\' => Some('\\'), 174 | '\'' => Some('\''), 175 | '"' => Some('"'), 176 | 'x' => { 177 | let hi = self.read_hex_digit()?; 178 | let lo = self.read_hex_digit()?; 179 | Some(char::from( 180 | u8::from_str_radix(&format!("{hi}{lo}"), 16).unwrap(), 181 | )) 182 | } 183 | 'u' => { 184 | let code = self.read_n_hex(4)?; 185 | char::from_u32(code) 186 | } 187 | 'U' => { 188 | let code = self.read_n_hex(8)?; 189 | char::from_u32(code) 190 | } 191 | d @ '1'..='7' => { 192 | let d2 = self.read_octal_digit()?; 193 | let d3 = self.read_octal_digit()?; 194 | let val = u8::from_str_radix(&format!("{d}{d2}{d3}"), 8).unwrap(); 195 | Some(char::from(val)) 196 | } 197 | _ => { 198 | return Err(LexicalError::UnknownEscape { 199 | span: self.source.consume_span(), 200 | found: esc, 201 | }) 202 | } 203 | }; 204 | 205 | if let Some(c) = resolved { 206 | buf.push(c); 207 | } else { 208 | return Err(LexicalError::InvalidUnicodeEscape { 209 | span: self.source.consume_span(), 210 | }); 211 | } 212 | } 213 | _ => { 214 | buf.push(ch); 215 | self.source.next(); 216 | } 217 | } 218 | } 219 | 220 | if !closed { 221 | return Err(LexicalError::UnexpectedStringEol { 222 | span: self.source.consume_span(), 223 | }); 224 | } 225 | 226 | let literal = buf[1..].to_owned(); 227 | 228 | Ok(self.source.consume_token_with_literal( 229 | TokenKind::String, 230 | literal.clone(), 231 | Literal::String(literal), 232 | )) 233 | } 234 | 235 | fn consume_number(&mut self, first: char) -> Result { 236 | let mut raw = String::new(); 237 | raw.push(first); 238 | 239 | if first == '0' { 240 | if let Some(&next) = self.source.peek() { 241 | match next { 242 | 'b' | 'B' | 'o' | 'O' | 'x' | 'X' => { 243 | self.source.next(); 244 | raw.push(next); 245 | 246 | let (radix, valid_digit): (u32, fn(char) -> bool) = match next { 247 | 'b' | 'B' => (2, |c: char| c == '0' || c == '1'), 248 | 'o' | 'O' => (8, |c: char| ('0'..='7').contains(&c)), 249 | 'x' | 'X' => (16, |c: char| c.is_ascii_hexdigit()), 250 | _ => unreachable!(), 251 | }; 252 | 253 | let mut digits = String::new(); 254 | 255 | while let Some(&ch) = self.source.peek() { 256 | if ch == '_' { 257 | self.source.next(); 258 | continue; 259 | } 260 | if valid_digit(ch) { 261 | digits.push(ch); 262 | raw.push(ch); 263 | self.source.next(); 264 | } else { 265 | break; 266 | } 267 | } 268 | 269 | if digits.is_empty() { 270 | return Err(LexicalError::UnexpectedChar { 271 | character: next, 272 | span: self.source.consume_span(), 273 | }); 274 | } 275 | 276 | let value = u64::from_str_radix(&digits, radix).unwrap() as f64; 277 | 278 | return Ok(self.source.consume_token_with_literal( 279 | TokenKind::Number, 280 | raw, 281 | Literal::Number(value), 282 | )); 283 | } 284 | _ => (), 285 | } 286 | } 287 | } 288 | 289 | let mut matched_dot = first == '.'; 290 | let mut matched_exp = false; 291 | 292 | while let Some(&ch) = self.source.peek() { 293 | match ch { 294 | '_' => { 295 | raw.push(ch); 296 | self.source.next(); 297 | } 298 | d if d.is_ascii_digit() => { 299 | raw.push(d); 300 | self.source.next(); 301 | } 302 | '.' if !matched_dot && !matched_exp => { 303 | matched_dot = true; 304 | raw.push('.'); 305 | self.source.next(); 306 | } 307 | 'e' | 'E' if !matched_exp => { 308 | matched_exp = true; 309 | raw.push(ch); 310 | self.source.next(); 311 | 312 | if let Some(&sign @ ('+' | '-')) = self.source.peek() { 313 | raw.push(sign); 314 | self.source.next(); 315 | } 316 | } 317 | c if c.is_alphabetic() => { 318 | return Err(LexicalError::UnexpectedChar { 319 | character: c, 320 | span: self.source.consume_span(), 321 | }); 322 | } 323 | 324 | _ => break, 325 | } 326 | } 327 | 328 | let cleaned: String = raw.chars().filter(|c| *c != '_').collect(); 329 | let value: f64 = cleaned.parse().unwrap(); 330 | 331 | Ok(self 332 | .source 333 | .consume_token_with_literal(TokenKind::Number, raw, Literal::Number(value))) 334 | } 335 | 336 | fn consume_identifier(&mut self, char: char) -> Result { 337 | let mut identifier = String::new(); 338 | 339 | identifier.push(char); 340 | 341 | while let Some(&peeked) = self.source.peek() { 342 | if peeked.is_alphanumeric() || peeked == '_' { 343 | identifier.push(peeked); 344 | self.source.next(); 345 | } else { 346 | break; 347 | } 348 | } 349 | 350 | let token = match identifier.as_str() { 351 | Literal::TRUE_LITERAL => self.source.consume_token_with_literal( 352 | TokenKind::True, 353 | Literal::TRUE_LITERAL.to_string(), 354 | Literal::Boolean(true), 355 | ), 356 | Literal::FALSE_LITERAL => self.source.consume_token_with_literal( 357 | TokenKind::False, 358 | Literal::FALSE_LITERAL.to_string(), 359 | Literal::Boolean(false), 360 | ), 361 | Literal::NIL_LITERAL => self.source.consume_token_with_literal( 362 | TokenKind::Nil, 363 | Literal::NIL_LITERAL.to_string(), 364 | Literal::Nil, 365 | ), 366 | "função" => self.source.consume_token(TokenKind::Function, "função"), 367 | "não" => self.source.consume_token(TokenKind::Not, "não"), 368 | "é" => self.source.consume_token(TokenKind::Equals, "é"), 369 | "seja" => self.source.consume_token(TokenKind::Let, "seja"), 370 | "se" => self.source.consume_token(TokenKind::If, "se"), 371 | "então" => self.source.consume_token(TokenKind::Then, "então"), 372 | "retorna" => self.source.consume_token(TokenKind::Return, "retorna"), 373 | "senão" => self.source.consume_token(TokenKind::Else, "senão"), 374 | "fim" => self.source.consume_token(TokenKind::BlockEnd, "fim"), 375 | "ou" => self.source.consume_token(TokenKind::Or, "ou"), 376 | "e" => self.source.consume_token(TokenKind::And, "e"), 377 | "até" => self.source.consume_token(TokenKind::Until, "até"), 378 | "para" => self.source.consume_token(TokenKind::ForOrBreak, "para"), 379 | "cada" => self.source.consume_token(TokenKind::Each, "cada"), 380 | "em" => self.source.consume_token(TokenKind::In, "em"), 381 | "tem" => self.source.consume_token(TokenKind::Has, "tem"), 382 | "enquanto" => self.source.consume_token(TokenKind::While, "enquanto"), 383 | "faça" => self.source.consume_token(TokenKind::Do, "faça"), 384 | "continua" => self.source.consume_token(TokenKind::Continue, "continua"), 385 | identifier => self.source.consume_token_with_literal( 386 | TokenKind::Identifier, 387 | identifier.to_string(), 388 | Literal::String(identifier.to_string()), 389 | ), 390 | }; 391 | 392 | Ok(token) 393 | } 394 | 395 | fn consume_comment(&mut self) { 396 | while let Some(&peeked) = self.source.peek() { 397 | if peeked == '\n' { 398 | break; 399 | } 400 | 401 | self.source.next(); 402 | } 403 | 404 | self.source.ignore_char(); 405 | } 406 | 407 | fn consume_multiline_comment(&mut self) { 408 | while let Some(_) = self.source.next() { 409 | if self.peek_match("*/") { 410 | break; 411 | } 412 | } 413 | 414 | self.source.ignore_char(); 415 | } 416 | } 417 | 418 | impl Scanner<'_> { 419 | fn peek_match(&mut self, expected: &str) -> bool { 420 | for c in expected.chars() { 421 | if let Some(&peeked) = self.source.peek() { 422 | if peeked != c { 423 | return false; 424 | } 425 | 426 | self.source.next(); 427 | } 428 | } 429 | 430 | true 431 | } 432 | 433 | fn read_hex_digit(&mut self) -> Result { 434 | self.source 435 | .next() 436 | .filter(|c| c.is_ascii_hexdigit()) 437 | .ok_or(LexicalError::InvalidHexEscape { 438 | span: self.source.consume_span(), 439 | }) 440 | } 441 | 442 | fn read_n_hex(&mut self, n: usize) -> Result { 443 | let mut s = String::new(); 444 | for _ in 0..n { 445 | s.push(self.read_hex_digit()?); 446 | } 447 | u32::from_str_radix(&s, 16).map_err(|_| LexicalError::InvalidHexEscape { 448 | span: self.source.consume_span(), 449 | }) 450 | } 451 | 452 | fn read_octal_digit(&mut self) -> Result { 453 | self.source 454 | .next() 455 | .filter(|c| ('0'..='7').contains(c)) 456 | .ok_or(LexicalError::InvalidOctalEscape { 457 | span: self.source.consume_span(), 458 | }) 459 | } 460 | } 461 | -------------------------------------------------------------------------------- /tests/src/ops.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc}; 2 | use tenda_core::{ 3 | platform::OSPlatform, 4 | runtime::{self, AssociativeArrayKey, Platform}, 5 | }; 6 | 7 | use crate::{expr_tests, expr_tests_should_panic}; 8 | 9 | expr_tests_should_panic!( 10 | chained_comparison_expr: "1 < 2 < 3", 11 | chained_comparison2_expr: "1 > 2 > 3", 12 | chained_comparison3_expr: "1 <= 2 <= 3", 13 | chained_comparison4_expr: "1 >= 2 >= 3", 14 | chained_comparison_mixed_expr: "1 < 2 > 3", 15 | chained_comparison_mixed2_expr: "1 > 2 < 3", 16 | chained_comparison_mixed3_expr: "1 <= 2 >= 3", 17 | chained_comparison_mixed4_expr: "1 >= 2 <= 3", 18 | ); 19 | 20 | expr_tests_should_panic!( 21 | func_call_invalid_type_expr: "verdadeiro()", 22 | func_call_invalid_type2_expr: "[1, 2, 3]()", 23 | func_call_invalid_type3_expr: "{ 1: 2 }()", 24 | func_call_invalid_type4_expr: "\"olá\"()", 25 | ); 26 | 27 | expr_tests!( 28 | num_expr: "1" => Number(1.0), 29 | num_sum_expr: "(1 + 2) + (3 + 4) + 5" => Number(15.0), 30 | num_mult_expr: "(1 * 2) * (3 * 4) * 5" => Number(120.0), 31 | num_sub_expr: "(1 - 2) - (3 - 4) - 5" => Number(-5.0), 32 | num_div_expr: "10 / 2" => Number(5.0), 33 | num_exp_expr: "2 ^ 3" => Number(8.0), 34 | num_mod_expr: "10 % 3" => Number(1.0), 35 | num_greater_expr: "5 > 3" => Boolean(true), 36 | num_greater_equality_expr: "5 >= 5" => Boolean(true), 37 | num_less_expr: "3 < 5" => Boolean(true), 38 | num_less_equality_expr: "3 <= 3" => Boolean(true), 39 | num_equality_num_expr: "3 é 3" => Boolean(true), 40 | num_nequality_expr: "3 não é 4" => Boolean(true), 41 | num_neg_exp_expr: "-2 ^ 3" => Number(-8.0), 42 | num_nequality_str_expr: "3 não é \"3\"" => Boolean(true), 43 | num_mod_neg_expr: "-10 % 3" => Number(-1.0), 44 | num_neg_zero_literal: "(-0)" => Number(0.0), 45 | num_sum_with_neg_operand: "1 + -2" => Number(-1.0), 46 | num_decimal_sum_then_mult: "(1.5 + 2.5) * 2" => Number(8.0), 47 | num_div_by_fraction: "2 / 0.5" => Number(4.0), 48 | num_div_result_fraction: "10 / 4" => Number(2.5), 49 | num_neg_exponentiation_square: "(-5) ^ 2" => Number(25.0), 50 | num_mod_operation_positive: "5 % 2" => Number(1.0), 51 | num_neg_exponentiation_reciprocal: "(-2) ^ -2" => Number(0.25), 52 | num_mixed_precedence_exponentiation: "(2 * 2) ^ 3" => Number(64.0), 53 | num_operator_precedence_linear: "1 + 2 * 3 / 6" => Number(2.0), 54 | num_small_number_scaling: "0.0001 * 10000" => Number(1.0), 55 | num_equality_ge_test_equal: "1 >= 1" => Boolean(true), 56 | num_inequality_test_equal_fail: "1 > 1" => Boolean(false), 57 | num_large_precision_sum: "1000000 + 0.000001" => Number(1000000.000001), 58 | num_neg_mult: "(-2) * (-3)" => Number(6.0), 59 | num_mixed_add_and_mult: "2 * 3 + 4 * 5" => Number(26.0), 60 | num_decimal_imprecision_sum: "0.1 + 0.2" => Number(0.30000000000000004), 61 | num_large_integers_sum: "123456789 + 987654321" => Number(1111111110.0), 62 | num_sub_groupings: "(1 - 2) + (3 - 4)" => Number(-2.0), 63 | num_neg_mod_with_negs: "(-10) % (-3)" => Number(-1.0), 64 | ); 65 | 66 | expr_tests_should_panic!( 67 | num_div_by_zero: "0 / 0", 68 | ); 69 | 70 | expr_tests!( 71 | bool_expr: "verdadeiro" => Boolean(true), 72 | bool_expr2: "falso" => Boolean(false), 73 | bool_equality_expr: "verdadeiro é falso" => Boolean(false), 74 | bool_and_expr: "verdadeiro e falso" => Boolean(false), 75 | bool_or_expr: "verdadeiro ou falso" => Boolean(true), 76 | bool_all_true_and_chain: "verdadeiro e verdadeiro e verdadeiro" => Boolean(true), 77 | bool_and_chain_includes_false: "verdadeiro e verdadeiro e falso" => Boolean(false), 78 | bool_or_chain_ends_true: "falso ou falso ou verdadeiro" => Boolean(true), 79 | bool_chained_equality_false: "falso é falso é falso" => Boolean(false), 80 | bool_chained_equality_all_true: "verdadeiro é verdadeiro é verdadeiro" => Boolean(true), 81 | bool_and_with_comparison_true: "verdadeiro e (1 < 2)" => Boolean(true), 82 | bool_or_with_comparison_false: "falso ou (2 > 3)" => Boolean(false), 83 | bool_equality_comparison_in_and: "(1 é 1) e (2 é 3)" => Boolean(false), 84 | bool_multiple_less_than_comparisons_true: "(1 < 2) e (3 < 4) e (5 < 6)" => Boolean(true), 85 | bool_or_with_greater_equal_true: "(10 >= 10) ou (1 > 2)" => Boolean(true), 86 | bool_equality_of_false_values: "(5 não é 5) ou (6 não é 6)" => Boolean(false), 87 | bool_or_of_false_and_true_combo: "verdadeiro ou falso e verdadeiro" => Boolean(true), 88 | bool_nested_triple_equality_true: "((1 < 2) é (3 < 4)) é verdadeiro" => Boolean(true), 89 | bool_complex_and_with_false: "((1 > 2) e verdadeiro) ou falso" => Boolean(false), 90 | bool_not_true_expr: "não verdadeiro" => Boolean(false), 91 | bool_not_false_expr: "não falso" => Boolean(true), 92 | bool_or_with_and_precedence_expr: "verdadeiro ou verdadeiro e falso" => Boolean(true), 93 | bool_equality_comparison_with_range: "(1 < 2) é (1 < 2)" => Boolean(true), 94 | bool_multiple_chained_and_comparisons: "(1 >= 1) e (1 <= 1)" => Boolean(true), 95 | ); 96 | 97 | expr_tests!( 98 | str_expr: "\"abc\"" => String("abc".to_string()), 99 | str_concat_expr: "\"Olá, \" + \"mundo!\"" => String("Olá, mundo!".to_string()), 100 | str_equality_expr: "\"abc\" é \"abc\"" => Boolean(true), 101 | str_nequality_expr: "\"abc\" não é \"def\"" => Boolean(true), 102 | str_num_concat_expr: "\"abc\" + 123" => String("abc123".to_string()), 103 | str_bool_concat_expr: "\"abc\" + verdadeiro" => String("abcverdadeiro".to_string()), 104 | str_list_concat_expr: "\"abc\" + [1, 2]" => String("abc[1, 2]".to_string()), 105 | str_assoc_array_concat_expr: "\"abc\" + { 1: 2 }" => String("abc{ 1: 2 }".to_string()), 106 | str_range_concat_expr: "\"abc\" + (1 até 5)" => String("abc1 até 5".to_string()), 107 | str_date_concat_expr: "\"abc\" + Data.de_iso(\"2025-04-10T00:45:26.580-03:00\")" => 108 | String("abc2025-04-10T00:45:26.580-03:00".to_string()), 109 | str_nil_concat_expr: "\"abc\" + Nada" => String("abcNada".to_string()), 110 | str_nequality_num_expr: "\"123\" não é 123" => Boolean(true), 111 | str_unicode_chinese: "\"unicode: 你好\"" => String("unicode: 你好".to_string()), 112 | str_unicode_emoji_snake: "\"unicode: 🐍\"" => String("unicode: 🐍".to_string()), 113 | str_multiple_spaces: "\"Espaços múltiplos\"" => String("Espaços múltiplos".to_string()), 114 | str_with_newline_escape: "\"Com\\nQuebra\"" => String("Com\nQuebra".to_string()), 115 | str_concat_with_numeric_computation: "\"2 + 2 = \" + (2 + 2)" => String("2 + 2 = 4".to_string()), 116 | str_concat_empty_strings: "\"\" + \"\"" => String("".to_string()), 117 | str_hello_space_world: "\"Olá\" + \" \" + \"Mundo\"" => String("Olá Mundo".to_string()), 118 | str_concat_with_negative_number: "\"foo\" + (-5)" => String("foo-5".to_string()), 119 | str_concatenate_two_literals: "\"AB\" + \"CD\"" => String("ABCD".to_string()), 120 | str_emoji_concatenation: "\"😃\" + \"😡\"" => String("😃😡".to_string()), 121 | str_with_tab_escape: "\"Tab:\\t\"" => String("Tab:\t".to_string()), 122 | str_with_linefeed_escape: "\"Linha:\\n\"" => String("Linha:\n".to_string()), 123 | str_with_backslash_escape: "\"Backslash: \\\\\"" => String("Backslash: \\".to_string()), 124 | str_concat_with_trailing_spaces: "\"Margem \" + \" \"" => String("Margem ".to_string()), 125 | str_trim_test_unchanged: "\" trim \"" => String(" trim ".to_string()), 126 | str_mixed_case_literal: "\"MixedCase\"" => String("MixedCase".to_string()), 127 | str_multiple_concat_sequence: "\"algo \" + \"coisas\" + \" aqui\"" => String("algo coisas aqui".to_string()), 128 | str_concat_boolean_comparison_false: "\"abc\" + (verdadeiro é falso)" => String("abcfalso".to_string()), 129 | str_concat_comparison_numeric_true: "\"abc\" + (10 > 9)" => String("abcverdadeiro".to_string()), 130 | str_concat_with_nil: "\"xyz\" + Nada" => String("xyzNada".to_string()), 131 | str_concat_with_list_representation: "\"Combinar\" + [1, 2]" => String("Combinar[1, 2]".to_string()), 132 | str_nested_concatenation_with_space: "\"Olá\" + (\" \" + \"amigo\")" => String("Olá amigo".to_string()) 133 | ); 134 | 135 | expr_tests!( 136 | date_expr: "Data.de_iso(\"2025-04-10T00:45:26.580-03:00\")" => 137 | Date(runtime::Date::from_iso_string("2025-04-10T00:45:26.580-03:00").unwrap()), 138 | date_equality_expr: "Data.de_iso(\"2025-04-10T00:45:26.580-03:00\") é Data.de_iso(\"2025-04-10T00:45:26.580-03:00\")" => 139 | Boolean(true), 140 | date_nequality_expr: "Data.de_iso(\"2025-04-10T00:45:26.580-03:00\") não é Data.de_iso(\"2025-04-10T00:45:26.580-03:00\")" => 141 | Boolean(false), 142 | date_sum_expr: "Data.de_iso(\"2025-04-10T00:45:26.580-03:00\") + 1" => 143 | Date(runtime::Date::from_iso_string("2025-04-10T00:45:26.581-03:00").unwrap()), 144 | date_sub: "Data.de_iso(\"2025-04-10T00:45:26.580-03:00\") - 1" => 145 | Date(runtime::Date::from_iso_string("2025-04-10T00:45:26.579-03:00").unwrap()), 146 | date_greater_expr: "Data.de_iso(\"2025-04-10T00:45:26.580-03:00\") > Data.de_iso(\"2025-04-09T00:45:26.580-03:00\")" => 147 | Boolean(true), 148 | date_greater_equality_expr: "Data.de_iso(\"2025-04-10T00:45:26.580-03:00\") >= Data.de_iso(\"2025-04-10T00:45:26.580-03:00\")" => 149 | Boolean(true), 150 | date_less_expr: "Data.de_iso(\"2025-04-09T00:45:26.580-03:00\") < Data.de_iso(\"2025-04-10T00:45:26.580-03:00\")" => 151 | Boolean(true), 152 | date_less_equality_expr: "Data.de_iso(\"2025-04-10T00:45:26.580-03:00\") <= Data.de_iso(\"2025-04-10T00:45:26.580-03:00\")" => 153 | Boolean(true), 154 | date_iso_jan_first_2023: "Data.de_iso(\"2023-01-01T00:00:00Z\")" => 155 | Date(runtime::Date::from_iso_string("2023-01-01T00:00:00Z").unwrap()), 156 | date_iso_leap_feb29_2024: "Data.de_iso(\"2024-02-29T12:34:56Z\")" => 157 | Date(runtime::Date::from_iso_string("2024-02-29T12:34:56Z").unwrap()), 158 | date_end_of_year_increment_msec: "Data.de_iso(\"2030-12-31T23:59:59Z\") + 1" => 159 | Date(runtime::Date::from_iso_string("2030-12-31T23:59:59.001Z").unwrap()), 160 | date_end_of_year_decrement_sec: "Data.de_iso(\"2030-12-31T23:59:59Z\") - 1000" => 161 | Date(runtime::Date::from_iso_string("2030-12-31T23:59:58Z").unwrap()), 162 | date_comparison_later_time_true: "Data.de_iso(\"2023-05-10T10:00:00Z\") > Data.de_iso(\"2023-05-10T09:59:59Z\")" => 163 | Boolean(true), 164 | date_comparison_earlier_time_true: "Data.de_iso(\"2025-12-31T23:59:59Z\") < Data.de_iso(\"2026-01-01T00:00:00Z\")" => 165 | Boolean(true), 166 | date_exact_equality_expr: "Data.de_iso(\"2023-01-01T00:00:00Z\") é Data.de_iso(\"2023-01-01T00:00:00Z\")" => 167 | Boolean(true), 168 | date_exact_inequality_test_seconds_diff: "Data.de_iso(\"2023-01-01T00:00:00Z\") não é Data.de_iso(\"2023-01-01T00:00:01Z\")" => 169 | Boolean(true), 170 | date_with_positive_timezone_offset: "Data.de_iso(\"2022-01-01T00:00:00+02:00\")" => 171 | Date(runtime::Date::from_iso_string("2022-01-01T00:00:00+02:00").unwrap()), 172 | date_leap_year_exact_equality: "Data.de_iso(\"2000-02-29T12:00:00Z\") é Data.de_iso(\"2000-02-29T12:00:00Z\")" => 173 | Boolean(true), 174 | date_different_offset_inequality: "Data.de_iso(\"2000-02-29T12:00:00Z\") não é Data.de_iso(\"2000-02-29T12:00:00-01:00\")" => 175 | Boolean(true), 176 | date_2038_boundary_plus_msec: "Data.de_iso(\"2038-01-19T03:14:07Z\") + 1" => 177 | Date(runtime::Date::from_iso_string("2038-01-19T03:14:07.001Z").unwrap()), 178 | date_epoch_minus_one_msec: "Data.de_iso(\"1970-01-01T00:00:00Z\") - 1" => 179 | Date(runtime::Date::from_iso_string("1969-12-31T23:59:59.999Z").unwrap()), 180 | date_2038_comparison_true: "Data.de_iso(\"2038-01-19T03:14:08Z\") > Data.de_iso(\"2038-01-19T03:14:07Z\")" => 181 | Boolean(true), 182 | date_chronological_false_expr: "Data.de_iso(\"2000-01-01T00:00:00Z\") < Data.de_iso(\"1999-12-31T23:59:59Z\")" => 183 | Boolean(false), 184 | date_equality_or_greater_expr: "Data.de_iso(\"2000-01-01T00:00:00Z\") >= Data.de_iso(\"2000-01-01T00:00:00Z\")" => 185 | Boolean(true), 186 | date_comparison_false_for_less: "Data.de_iso(\"2038-01-19T03:14:07Z\") <= Data.de_iso(\"2038-01-19T03:14:06Z\")" => 187 | Boolean(false), 188 | date_new_year_from_ending_msec: "Data.de_iso(\"2021-12-31T23:59:59.999Z\") + 1" => 189 | Date(runtime::Date::from_iso_string("2022-01-01T00:00:00Z").unwrap()), 190 | date_equal_with_z_suffix: "Data.de_iso(\"2022-01-01T00:00:00Z\") é Data.de_iso(\"2022-01-01T00:00:00+00:00\")" => 191 | Boolean(true), 192 | date_different_timezone_false: "Data.de_iso(\"2022-01-01T00:00:00-03:00\") não é Data.de_iso(\"2022-01-01T00:00:00Z\")" => 193 | Boolean(true), 194 | date_post_leap_comparison_true: "Data.de_iso(\"2000-03-01T00:00:00Z\") > Data.de_iso(\"2000-02-29T23:59:59Z\")" => 195 | Boolean(true), 196 | date_epoch_transition_comparison_true: "Data.de_iso(\"1999-12-31T23:59:59Z\") < Data.de_iso(\"2000-01-01T00:00:00Z\")" => 197 | Boolean(true) 198 | ); 199 | 200 | expr_tests_should_panic!( 201 | date_mult_error: "Data.de_iso(\"2025-04-10T00:00:00Z\") * 2", 202 | ); 203 | 204 | expr_tests!( 205 | list_expr: "[1, 2, 3]" => 206 | List(Rc::new(RefCell::new(vec![Number(1.0), Number(2.0), Number(3.0)]))), 207 | list_concat_expr: "[1, 2] + [3, 4]" => 208 | List(Rc::new(RefCell::new(vec![Number(1.0), Number(2.0), Number(3.0), Number(4.0)]))), 209 | list_equality_expr: "[1, 2] é [1, 2]" => Boolean(true), 210 | list_equality2_expr: "[1, 2] é [1, 2, 3]" => Boolean(false), 211 | list_has_expr: "[1, 2, 3] tem 2" => Boolean(true), 212 | list_lacks_expr: "[1, 2, 3] não tem 4" => Boolean(true), 213 | empty_list_expr: "[]" => 214 | List(Rc::new(RefCell::new(vec![]))), 215 | nested_list_of_numbers_expr: "[[1, 2], [3, 4]]" => 216 | List(Rc::new(RefCell::new(vec![ 217 | List(Rc::new(RefCell::new(vec![Number(1.0), Number(2.0)]))), 218 | List(Rc::new(RefCell::new(vec![Number(3.0), Number(4.0)]))), 219 | ]))), 220 | list_with_nested_list_mixed_expr: "[1, [2, 3], 4]" => 221 | List(Rc::new(RefCell::new(vec![ 222 | Number(1.0), 223 | List(Rc::new(RefCell::new(vec![Number(2.0), Number(3.0)]))), 224 | Number(4.0), 225 | ]))), 226 | list_of_mixed_types_expr: "[\"olá\", verdadeiro, 123, Nada]" => 227 | List(Rc::new(RefCell::new(vec![ 228 | String("olá".to_string()), 229 | Boolean(true), 230 | Number(123.0), 231 | Nil, 232 | ]))), 233 | list_including_range_expr: "[(1 até 3), \"coisas\", 42]" => 234 | List(Rc::new(RefCell::new(vec![ 235 | Range(1, 3), 236 | String("coisas".to_string()), 237 | Number(42.0), 238 | ]))), 239 | list_arithmetic_expressions_expr: "[1 + 2, 3 * 4]" => 240 | List(Rc::new(RefCell::new(vec![Number(3.0), Number(12.0)]))), 241 | list_chained_concatenation_expr: "([1, 2] + [3, 4]) + [5]" => 242 | List(Rc::new(RefCell::new(vec![ 243 | Number(1.0), Number(2.0), Number(3.0), Number(4.0), Number(5.0) 244 | ]))), 245 | list_deeply_nested_empty_expr: "[[[]]]" => 246 | List(Rc::new(RefCell::new(vec![ 247 | List(Rc::new(RefCell::new(vec![ 248 | List(Rc::new(RefCell::new(vec![]))), 249 | ]))), 250 | ]))), 251 | list_nested_equality_true_expr: "[[1], [2]] é [[1], [2]]" => Boolean(true), 252 | list_string_elements_equality_false_expr: "[\"abc\", \"def\"] é [\"abc\", \"xyz\"]" => Boolean(false), 253 | list_inequality_due_to_order_expr: "[1, 2] não é [2, 1]" => Boolean(true), 254 | list_multiple_concatenation_expr: "[1, 2] + [3] + [4, 5]" => 255 | List(Rc::new(RefCell::new(vec![ 256 | Number(1.0), Number(2.0), Number(3.0), Number(4.0), Number(5.0) 257 | ]))), 258 | list_empty_concatenation_expr: "[] + []" => 259 | List(Rc::new(RefCell::new(vec![]))), 260 | list_membership_found_numeric_expr: "[1, 2, 3] tem 1" => Boolean(true), 261 | list_membership_not_found_wrong_type_expr: "[1, 2, 3] tem \"1\"" => Boolean(false), 262 | list_membership_boolean_nil_expr: "[Nada, verdadeiro, falso] tem verdadeiro" => Boolean(true), 263 | list_membership_arithmetic_expr: "[1, 2, 3] tem (1 + 1)" => Boolean(true), 264 | list_membership_negative_expr: "[1, 2, 3] não tem 2" => Boolean(false), 265 | list_equality_chained_comparison_true_expr: "([1, 2] é [1, 2]) é ([1, 2] é [1, 2])" => Boolean(true), 266 | list_inequality_different_length_expr: "[1, 2, 3] não é [1, 2, 3, 4]" => Boolean(true), 267 | list_mixed_elements_structure_expr: "[Nada, Nada] é [Nada, Nada]" => Boolean(true) 268 | ); 269 | 270 | expr_tests!( 271 | assoc_array_expr: "{ 1: 2, 3: 4 }" => AssociativeArray(Rc::new(RefCell::new( 272 | runtime::AssociativeArray::from([ 273 | (AssociativeArrayKey::Number(1), Number(2.0)), 274 | (AssociativeArrayKey::Number(3), Number(4.0)), 275 | ]) 276 | ))), 277 | assoc_array_has: "{ 1: 2, 3: 4 } tem 1" => Boolean(true), 278 | assoc_array_lack: "{ 1: 2, 3: 4 } não tem 5" => Boolean(true), 279 | assoc_array_equality_expr: "{ 1: 2, 3: 4 } é { 1: 2, 3: 4 }" => Boolean(true), 280 | assoc_array_equality2_expr: "{ 1: 2, 3: 4 } é { 1: 2, 3: 5 }" => Boolean(false), 281 | assoc_array_empty_expr: "{ }" => 282 | AssociativeArray(Rc::new(RefCell::new( 283 | runtime::AssociativeArray::from([]) 284 | ))), 285 | assoc_array_single_string_key_expr: "{ \"foo\": \"bar\" }" => 286 | AssociativeArray(Rc::new(RefCell::new( 287 | runtime::AssociativeArray::from([ 288 | (AssociativeArrayKey::String("foo".to_string()), 289 | String("bar".to_string())) 290 | ]) 291 | ))), 292 | assoc_array_nested_list_and_map_expr: "{ \"aninhado\": [1, 2], \"dicionário\": { \"interior\": 42 } }" => 293 | AssociativeArray(Rc::new(RefCell::new( 294 | runtime::AssociativeArray::from([ 295 | (AssociativeArrayKey::String("aninhado".to_string()), 296 | List(Rc::new(RefCell::new(vec![Number(1.0), Number(2.0)])))), 297 | (AssociativeArrayKey::String("dicionário".to_string()), 298 | AssociativeArray(Rc::new(RefCell::new( 299 | runtime::AssociativeArray::from([ 300 | (AssociativeArrayKey::String("interior".to_string()), 301 | Number(42.0)) 302 | ]) 303 | )))) 304 | ]) 305 | ))), 306 | assoc_array_numeric_keys_with_list_expr: "{ 1: [2, 3], 2: \"algo\" }" => 307 | AssociativeArray(Rc::new(RefCell::new( 308 | runtime::AssociativeArray::from([ 309 | (AssociativeArrayKey::Number(1), 310 | List(Rc::new(RefCell::new(vec![Number(2.0), Number(3.0)])))), 311 | (AssociativeArrayKey::Number(2), 312 | String("algo".to_string())) 313 | ]) 314 | ))), 315 | assoc_array_mixed_numeric_values_expr: "{ \"a\": 1.5, \"b\": -2 }" => 316 | AssociativeArray(Rc::new(RefCell::new( 317 | runtime::AssociativeArray::from([ 318 | (AssociativeArrayKey::String("a".to_string()), Number(1.5)), 319 | (AssociativeArrayKey::String("b".to_string()), Number(-2.0)) 320 | ]) 321 | ))), 322 | assoc_array_equality_true_expr: "{ \"a\": \"b\" } é { \"a\": \"b\" }" => Boolean(true), 323 | assoc_array_equality_false_expr: "{ \"a\": 1 } é { \"a\": 2 }" => Boolean(false), 324 | assoc_array_membership_found_numeric_key_in_list_expr: "{ 1: [\"x\"] } tem 1" => Boolean(true), 325 | assoc_array_membership_wrong_type_expr: "{ 1: [\"x\"] } tem \"1\"" => Boolean(false), 326 | assoc_array_membership_found_string_key_expr: "{ \"abc\": 123, \"def\": 456 } tem \"abc\"" => Boolean(true), 327 | assoc_array_membership_key_not_found_expr: "{ \"abc\": 123 } não tem \"xyz\"" => Boolean(true), 328 | assoc_array_nested_assoc_in_value_expr: "{ \"x\": [1,2], \"y\": {\"aninhado\": verdadeiro} } tem \"y\"" => Boolean(true), 329 | assoc_array_equality_nested_maps_true_expr: "{ \"x\": {1: 2}, \"y\": {1: 2} } é { \"x\": {1: 2}, \"y\": {1: 2} }" => Boolean(true), 330 | assoc_array_inequality_nested_maps_diff_expr: "{ \"x\": {1: 2}, \"y\": {1: 2} } é { \"x\": {1: 2}, \"y\": {1: 3} }" => Boolean(false), 331 | assoc_array_empty_inner_assoc_expr: "{ \"vazio\": { } }" => 332 | AssociativeArray(Rc::new(RefCell::new( 333 | runtime::AssociativeArray::from([ 334 | (AssociativeArrayKey::String("vazio".to_string()), 335 | AssociativeArray(Rc::new(RefCell::new( 336 | runtime::AssociativeArray::from([]) 337 | )))) 338 | ]) 339 | ))), 340 | assoc_array_membership_numeric_key_true_expr: "{ 10: 20, 11: 21 } tem 10" => Boolean(true), 341 | assoc_array_membership_numeric_key_false_expr: "{ 10: 20, 11: 21 } tem 12" => Boolean(false), 342 | assoc_array_deeply_nested_assoc_expr: "{ 1: {2: {3: 4}} }" => 343 | AssociativeArray(Rc::new(RefCell::new( 344 | runtime::AssociativeArray::from([ 345 | (AssociativeArrayKey::Number(1), 346 | AssociativeArray(Rc::new(RefCell::new( 347 | runtime::AssociativeArray::from([ 348 | (AssociativeArrayKey::Number(2), 349 | AssociativeArray(Rc::new(RefCell::new( 350 | runtime::AssociativeArray::from([ 351 | (AssociativeArrayKey::Number(3), 352 | Number(4.0)) 353 | ]) 354 | )))) 355 | ]) 356 | )))) 357 | ]) 358 | ))), 359 | assoc_array_numeric_keys_equality_expr: "{ 1: {2: 3} } é { 1: {2: 3} }" => Boolean(true), 360 | assoc_array_complex_nested_inequality_expr: 361 | "{ \"abc\": 1, \"def\": [1,2], \"ghi\": { \"x\": 9} } não é { \"abc\": 1, \"def\": [1,2], \"ghi\": { \"x\": 10} }" 362 | => Boolean(true), 363 | assoc_array_unordered_keys_equality_expr: 364 | "{ \"k1\": \"v1\", \"k2\": \"v2\" } é { \"k2\": \"v2\", \"k1\": \"v1\" }" => Boolean(true), 365 | assoc_array_mixed_elements_membership_expr: "{ \"arr\": [1,2,3], \"flag\": verdadeiro }" => 366 | AssociativeArray(Rc::new(RefCell::new( 367 | runtime::AssociativeArray::from([ 368 | (tenda_core::runtime::AssociativeArrayKey::String("arr".to_string()), 369 | List(Rc::new(RefCell::new(vec![ 370 | Number(1.0), Number(2.0), Number(3.0) 371 | ])))), 372 | (tenda_core::runtime::AssociativeArrayKey::String("flag".to_string()), 373 | Boolean(true)) 374 | ]) 375 | ))) 376 | ); 377 | 378 | expr_tests!( 379 | range_expr: "1 até 5" => Range(1, 5), 380 | range_equality_expr: "1 até 5 é 1 até 5" => Boolean(true), 381 | range_descending_expr: "5 até 1" => Range(5, 1), 382 | range_zero_expr: "0 até 0" => Range(0, 0), 383 | range_single_value_expr: "5 até 5" => Range(5, 5), 384 | range_normal_increasing_expr: "1 até 10" => Range(1, 10), 385 | range_small_descending_expr: "2 até 1" => Range(2, 1), 386 | range_large_descending_expr: "10 até 2" => Range(10, 2), 387 | range_with_arithmetic_expression_expr: "(1 + 2) até (3 + 4)" => Range(3, 7), 388 | range_minimal_increasing_expr: "0 até 1" => Range(0, 1), 389 | range_identical_single_expr: "3 até 3" => Range(3, 3), 390 | range_equality_true_expr: "1 até 2 é 1 até 2" => Boolean(true), 391 | range_inequality_due_to_shift_expr: "1 até 2 não é 2 até 3" => Boolean(true), 392 | range_equality_single_value_expr: "1 até 1 é 1 até 1" => Boolean(true), 393 | range_inequality_false_expr: "5 até 4 não é 5 até 4" => Boolean(false), 394 | range_length_difference_expr: "1 até 5 não é 1 até 4" => Boolean(true), 395 | range_expression_calculation_expr: "(2 + 3) até (2 * 3)" => Range(5, 6), 396 | range_chained_equality_with_boolean_true_expr: "((1 até 2) é (1 até 2)) é verdadeiro" => Boolean(true), 397 | range_chained_equality_failure_expr: "((1 até 1) é (1 até 2))" => Boolean(false), 398 | range_comparison_with_nil_false_expr: "((1 até 2) é Nada)" => Boolean(false), 399 | range_comparison_with_nil_true_expr: "((1 até 2) não é Nada)" => Boolean(true), 400 | range_nested_equality_comparison_expr: "((1 até 2) é (1 até 2)) é ((1 até 3) é (1 até 3))" => Boolean(true), 401 | range_singleton_expr: "1 até 1" => Range(1, 1), 402 | range_equality_of_same_values_expr: "(2 até 2) é (2 até 2)" => Boolean(true) 403 | ); 404 | 405 | expr_tests_should_panic!( 406 | range_plus_num_error: "(1 até 5) + 1", 407 | ); 408 | 409 | expr_tests!( 410 | dot_access_on_assoc_array: "{ \"a\": 1 }.a" => Number(1.0), 411 | dot_access_chained: "{ \"a\": { \"b\": 2 } }.a.b" => Number(2.0), 412 | ); 413 | -------------------------------------------------------------------------------- /tests/src/core.rs: -------------------------------------------------------------------------------- 1 | use rstest::rstest; 2 | use tenda_core::{ 3 | platform::OSPlatform, 4 | runtime::{Platform, Value}, 5 | }; 6 | 7 | use crate::{interpret_expr, interpret_stmt, interpret_stmt_and_get}; 8 | 9 | #[rstest] 10 | #[case(OSPlatform)] 11 | fn if_statement(#[case] platform: impl Platform + 'static) { 12 | let source = r#" 13 | seja resultado = 0 14 | 15 | se verdadeiro então faça 16 | resultado = 1 17 | senão faça 18 | resultado = 2 19 | fim 20 | "#; 21 | 22 | assert_eq!( 23 | interpret_stmt_and_get(platform, source, "resultado"), 24 | Value::Number(1.0) 25 | ); 26 | } 27 | 28 | #[rstest] 29 | #[case(OSPlatform)] 30 | fn for_loop_with_list(#[case] platform: impl Platform + 'static) { 31 | let source = r#" 32 | seja resultado = 0 33 | seja lista = [1, 2, 3, 4, 5] 34 | 35 | para cada i em lista faça 36 | resultado = resultado + i 37 | fim 38 | "#; 39 | 40 | assert_eq!( 41 | interpret_stmt_and_get(platform, source, "resultado"), 42 | Value::Number(15.0) 43 | ); 44 | } 45 | 46 | #[rstest] 47 | #[case(OSPlatform)] 48 | fn for_loop_with_range(#[case] platform: impl Platform + 'static) { 49 | let source = r#" 50 | seja resultado = 0 51 | 52 | para cada i em 1 até 5 faça 53 | resultado = resultado + i 54 | fim 55 | "#; 56 | 57 | assert_eq!( 58 | interpret_stmt_and_get(platform, source, "resultado"), 59 | Value::Number(15.0) 60 | ); 61 | } 62 | 63 | #[rstest] 64 | #[case(OSPlatform)] 65 | fn while_loop(#[case] platform: impl Platform + 'static) { 66 | let source = r#" 67 | seja resultado = 0 68 | seja i = 1 69 | 70 | enquanto i <= 5 faça 71 | resultado = resultado + i 72 | i = i + 1 73 | fim 74 | "#; 75 | 76 | assert_eq!( 77 | interpret_stmt_and_get(platform, source, "resultado"), 78 | Value::Number(15.0) 79 | ); 80 | } 81 | 82 | #[rstest] 83 | #[case(OSPlatform)] 84 | fn function_definition_and_call(#[case] platform: impl Platform + 'static) { 85 | let source = r#" 86 | seja soma(a, b) = faça 87 | retorna a + b 88 | fim 89 | 90 | seja resultado = soma(2, 3) 91 | "#; 92 | 93 | assert_eq!( 94 | interpret_stmt_and_get(platform, source, "resultado"), 95 | Value::Number(5.0) 96 | ); 97 | } 98 | 99 | #[rstest] 100 | #[case(OSPlatform)] 101 | fn closures(#[case] platform: impl Platform + 'static) { 102 | let source = r#" 103 | seja cria_somador(x) = faça 104 | seja somador(y) = faça 105 | retorna x + y 106 | fim 107 | 108 | retorna somador 109 | fim 110 | 111 | seja somador = cria_somador(2) 112 | seja resultado = somador(3) 113 | "#; 114 | 115 | assert_eq!( 116 | interpret_stmt_and_get(platform, source, "resultado"), 117 | Value::Number(5.0) 118 | ); 119 | } 120 | 121 | #[rstest] 122 | #[case(OSPlatform)] 123 | fn nested_closures(#[case] platform: impl Platform + 'static) { 124 | let source = r#" 125 | seja cria_multiplicador(x) = faça 126 | seja multiplicador(y) = faça 127 | seja multiplicador_interno(z) = faça 128 | retorna x * y * z 129 | fim 130 | 131 | retorna multiplicador_interno 132 | fim 133 | 134 | retorna multiplicador 135 | fim 136 | 137 | seja multiplicador = cria_multiplicador(2) 138 | seja resultado = multiplicador(3)(4) 139 | "#; 140 | 141 | assert_eq!( 142 | interpret_stmt_and_get(platform, source, "resultado"), 143 | Value::Number(24.0) 144 | ); 145 | } 146 | 147 | #[rstest] 148 | #[case(OSPlatform)] 149 | fn anonymous_function(#[case] platform: impl Platform + 'static) { 150 | let source = r#" 151 | seja resultado = (função(x, y) -> x + y)(2, 3) 152 | "#; 153 | 154 | assert_eq!( 155 | interpret_stmt_and_get(platform, source, "resultado"), 156 | Value::Number(5.0) 157 | ); 158 | } 159 | 160 | #[rstest] 161 | #[case(OSPlatform)] 162 | fn list_access(#[case] platform: impl Platform + 'static) { 163 | let source = r#" 164 | seja lista = [1, 2, 3] 165 | seja resultado = lista[1] 166 | "#; 167 | 168 | assert_eq!( 169 | interpret_stmt_and_get(platform, source, "resultado"), 170 | Value::Number(2.0) 171 | ); 172 | } 173 | 174 | #[rstest] 175 | #[case(OSPlatform)] 176 | fn list_mutation(#[case] platform: impl Platform + 'static) { 177 | let source = r#" 178 | seja lista = [1, 2, 3] 179 | 180 | lista[0] = 10 181 | 182 | seja resultado = lista[0] 183 | "#; 184 | 185 | assert_eq!( 186 | interpret_stmt_and_get(platform, source, "resultado"), 187 | Value::Number(10.0) 188 | ); 189 | } 190 | 191 | #[rstest] 192 | #[case(OSPlatform)] 193 | fn associative_array_access(#[case] platform: impl Platform + 'static) { 194 | let source = r#" 195 | seja dicionário = { "chave1": 1, "chave2": 2 } 196 | seja resultado = dicionário["chave1"] + dicionário.chave2 197 | "#; 198 | 199 | assert_eq!( 200 | interpret_stmt_and_get(platform, source, "resultado"), 201 | Value::Number(3.0) 202 | ); 203 | } 204 | 205 | #[rstest] 206 | #[case(OSPlatform)] 207 | fn associative_array_mutation(#[case] platform: impl Platform + 'static) { 208 | let source = r#" 209 | seja dicionário = { "chave1": 1, "chave2": 2 } 210 | 211 | dicionário["chave1"] = 10 212 | dicionário.chave2 = 20 213 | 214 | seja resultado = dicionário["chave1"] + dicionário.chave2 215 | "#; 216 | 217 | assert_eq!( 218 | interpret_stmt_and_get(platform, source, "resultado"), 219 | Value::Number(30.0) 220 | ); 221 | } 222 | 223 | #[rstest] 224 | #[case(OSPlatform)] 225 | fn for_loop_break(#[case] platform: impl Platform + 'static) { 226 | let source = r#" 227 | seja resultado = 0 228 | 229 | para cada i em 1 até 10 faça 230 | se i é 5 então faça 231 | para 232 | fim 233 | 234 | resultado = resultado + i 235 | fim 236 | "#; 237 | 238 | assert_eq!( 239 | interpret_stmt_and_get(platform, source, "resultado"), 240 | Value::Number(10.0) 241 | ); 242 | } 243 | 244 | #[rstest] 245 | #[case(OSPlatform)] 246 | fn for_loop_continue(#[case] platform: impl Platform + 'static) { 247 | let source = r#" 248 | seja resultado = 0 249 | 250 | para cada i em 1 até 10 faça 251 | se i é 5 então faça 252 | continua 253 | fim 254 | 255 | resultado = resultado + i 256 | fim 257 | "#; 258 | 259 | assert_eq!( 260 | interpret_stmt_and_get(platform, source, "resultado"), 261 | Value::Number(50.0) 262 | ); 263 | } 264 | 265 | #[rstest] 266 | #[case(OSPlatform)] 267 | #[should_panic] 268 | fn undefined_reference(#[case] platform: impl Platform + 'static) { 269 | interpret_expr(platform, "resultado"); 270 | } 271 | 272 | #[rstest] 273 | #[case(OSPlatform)] 274 | #[should_panic] 275 | fn already_declared(#[case] platform: impl Platform + 'static) { 276 | let source = r#" 277 | seja resultado = 0 278 | seja resultado = 1 279 | "#; 280 | 281 | interpret_stmt(platform, source); 282 | } 283 | 284 | #[rstest] 285 | #[case(OSPlatform)] 286 | #[should_panic] 287 | fn wrong_number_of_arguments(#[case] platform: impl Platform + 'static) { 288 | let source = r#" 289 | função soma(a, b) 290 | retorna a + b 291 | fim 292 | 293 | seja resultado = soma(1) 294 | "#; 295 | 296 | interpret_stmt(platform, source); 297 | } 298 | 299 | #[rstest] 300 | #[case(OSPlatform)] 301 | #[should_panic] 302 | fn list_out_of_bounds(#[case] platform: impl Platform + 'static) { 303 | let source = r#" 304 | seja lista = [1, 2, 3] 305 | seja resultado = lista[3] 306 | "#; 307 | 308 | interpret_stmt(platform, source); 309 | } 310 | 311 | #[rstest] 312 | #[case(OSPlatform)] 313 | #[should_panic] 314 | fn invalid_index_type(#[case] platform: impl Platform + 'static) { 315 | let source = r#" 316 | seja lista = [1, 2, 3] 317 | seja resultado = lista["chave"] 318 | "#; 319 | 320 | interpret_stmt(platform, source); 321 | } 322 | 323 | #[rstest] 324 | #[case(OSPlatform)] 325 | #[should_panic] 326 | fn invalid_range_bounds(#[case] platform: impl Platform + 'static) { 327 | interpret_stmt(platform, "1.1 até 2"); 328 | } 329 | 330 | #[rstest] 331 | #[case(OSPlatform)] 332 | #[should_panic] 333 | fn invalid_associative_array_key_value(#[case] platform: impl Platform + 'static) { 334 | interpret_stmt(platform, "{ 1.5: 2 }"); 335 | } 336 | 337 | #[rstest] 338 | #[case(OSPlatform)] 339 | #[should_panic] 340 | fn associative_array_key_not_found(#[case] platform: impl Platform + 'static) { 341 | let source = r#" 342 | seja dicionário = { "chave1": 1, "chave2": 2 } 343 | seja resultado = dicionário["chave3"] 344 | "#; 345 | 346 | interpret_stmt(platform, source); 347 | } 348 | 349 | #[rstest] 350 | #[case(OSPlatform)] 351 | #[should_panic] 352 | fn not_iterable(#[case] platform: impl Platform + 'static) { 353 | let source = r#" 354 | seja resultado = 0 355 | 356 | para cada i em 1.5 faça 357 | resultado = resultado + i 358 | fim 359 | "#; 360 | 361 | interpret_stmt(platform, source); 362 | } 363 | 364 | #[rstest] 365 | #[case(OSPlatform)] 366 | #[should_panic] 367 | fn immutable_strings(#[case] platform: impl Platform + 'static) { 368 | let source = r#" 369 | seja texto = "Olá, mundo!" 370 | texto[0] = "o" 371 | "#; 372 | 373 | interpret_stmt(platform, source); 374 | } 375 | 376 | #[rstest] 377 | #[case(OSPlatform)] 378 | #[should_panic] 379 | fn negative_list_index(#[case] platform: impl Platform + 'static) { 380 | let source = r#" 381 | seja lista = [10, 20, 30] 382 | seja x = lista[-1] 383 | "#; 384 | 385 | interpret_stmt(platform, source); 386 | } 387 | 388 | #[rstest] 389 | #[case(OSPlatform)] 390 | fn short_circuit_and(#[case] platform: impl Platform + 'static) { 391 | let source = r#" 392 | seja x = 0 393 | 394 | seja incrementa() = faça 395 | x = x + 1 396 | retorna verdadeiro 397 | fim 398 | 399 | falso e incrementa() 400 | "#; 401 | 402 | let runtime = interpret_stmt(platform, source); 403 | let value = runtime.get_global_env().get("x").unwrap().extract(); 404 | 405 | assert_eq!(value, Value::Number(0.0)); 406 | } 407 | 408 | #[rstest] 409 | #[case(OSPlatform)] 410 | fn short_circuit_or(#[case] platform: impl Platform + 'static) { 411 | let source = r#" 412 | seja x = 0 413 | 414 | seja incrementa() = faça 415 | x = x + 1 416 | retorna verdadeiro 417 | fim 418 | 419 | verdadeiro ou incrementa() 420 | "#; 421 | 422 | let runtime = interpret_stmt(platform, source); 423 | let value = runtime.get_global_env().get("x").unwrap().extract(); 424 | 425 | assert_eq!(value, Value::Number(0.0)); 426 | } 427 | 428 | #[rstest] 429 | #[case(OSPlatform)] 430 | fn nested_loops_break(#[case] platform: impl Platform + 'static) { 431 | let source = r#" 432 | seja soma_exterior = 0 433 | 434 | para cada i em [1,2] faça 435 | seja soma_interior = 0 436 | 437 | enquanto verdadeiro faça 438 | soma_interior = soma_interior + 1 439 | para 440 | fim 441 | 442 | soma_exterior = soma_exterior + soma_interior 443 | fim 444 | "#; 445 | 446 | let runtime = interpret_stmt(platform, source); 447 | let value = runtime 448 | .get_global_env() 449 | .get("soma_exterior") 450 | .unwrap() 451 | .extract(); 452 | 453 | assert_eq!(value, Value::Number(2.0)); 454 | } 455 | 456 | #[rstest] 457 | #[case(OSPlatform)] 458 | fn nested_loops_break_outer(#[case] platform: impl Platform + 'static) { 459 | let source = r#" 460 | seja soma_exterior = 0 461 | 462 | para cada i em [1, 2, 3, 4, 5] faça 463 | seja j = 0 464 | 465 | enquanto j < 3 faça 466 | j = j + 1 467 | fim 468 | 469 | soma_exterior = soma_exterior + i 470 | 471 | se i é 3 então faça 472 | para 473 | fim 474 | fim 475 | "#; 476 | 477 | let runtime = interpret_stmt(platform, source); 478 | let value = runtime 479 | .get_global_env() 480 | .get("soma_exterior") 481 | .unwrap() 482 | .extract(); 483 | 484 | assert_eq!(value, Value::Number(6.0)); 485 | } 486 | 487 | #[rstest] 488 | #[case(OSPlatform)] 489 | fn overshadow_local_in_nested_block(#[case] platform: impl Platform + 'static) { 490 | let source = r#" 491 | seja x = 1 492 | 493 | seja testa() = faça 494 | seja x = 2 495 | fim 496 | 497 | testa() 498 | "#; 499 | 500 | let runtime = interpret_stmt(platform, source); 501 | let value = runtime.get_global_env().get("x").unwrap().extract(); 502 | 503 | assert_eq!(value, Value::Number(1.0)); 504 | } 505 | 506 | #[rstest] 507 | #[case(OSPlatform)] 508 | fn overshadow_function_param(#[case] platform: impl Platform + 'static) { 509 | let source = r#" 510 | seja duplica(x) = faça 511 | seja x = x * 2 512 | retorna x 513 | fim 514 | 515 | seja resultado = duplica(3) 516 | "#; 517 | 518 | let runtime = interpret_stmt(platform, source); 519 | let value = runtime.get_global_env().get("resultado").unwrap().extract(); 520 | 521 | assert_eq!(value, Value::Number(6.0)); 522 | } 523 | 524 | #[rstest] 525 | #[case(OSPlatform)] 526 | fn empty_block_in_if(#[case] platform: impl Platform + 'static) { 527 | let source = r#" 528 | seja x = 0 529 | 530 | se verdadeiro então faça 531 | fim 532 | "#; 533 | 534 | let runtime = interpret_stmt(platform, source); 535 | let value = runtime.get_global_env().get("x").unwrap().extract(); 536 | 537 | assert_eq!(value, Value::Number(0.0)); 538 | } 539 | 540 | #[rstest] 541 | #[case(OSPlatform)] 542 | fn if_return(#[case] platform: impl Platform + 'static) { 543 | let source = r#" 544 | seja testa(a) = faça 545 | se a > 5 então faça 546 | retorna 100 547 | fim 548 | 549 | retorna 0 550 | fim 551 | 552 | seja resultado1 = testa(10) 553 | seja resultado2 = testa(2) 554 | "#; 555 | 556 | let runtime = interpret_stmt(platform, source); 557 | let r1 = runtime 558 | .get_global_env() 559 | .get("resultado1") 560 | .unwrap() 561 | .extract(); 562 | 563 | let r2 = runtime 564 | .get_global_env() 565 | .get("resultado2") 566 | .unwrap() 567 | .extract(); 568 | 569 | assert_eq!(r1, Value::Number(100.0)); 570 | assert_eq!(r2, Value::Number(0.0)); 571 | } 572 | 573 | #[rstest] 574 | #[case(OSPlatform)] 575 | fn else_return(#[case] platform: impl Platform + 'static) { 576 | let source = r#" 577 | seja testa(b) = faça 578 | se b é 0 então faça 579 | retorna 999 580 | senão faça 581 | retorna -1 582 | fim 583 | fim 584 | 585 | seja a = testa(0) 586 | seja c = testa(42) 587 | "#; 588 | 589 | let runtime = interpret_stmt(platform, source); 590 | let a_val = runtime.get_global_env().get("a").unwrap().extract(); 591 | let c_val = runtime.get_global_env().get("c").unwrap().extract(); 592 | 593 | assert_eq!(a_val, Value::Number(999.0)); 594 | assert_eq!(c_val, Value::Number(-1.0)); 595 | } 596 | 597 | #[rstest] 598 | #[case(OSPlatform)] 599 | fn closure_sees_updated_var(#[case] platform: impl Platform + 'static) { 600 | let source = r#" 601 | seja x = 1 602 | 603 | seja cria() = faça 604 | seja closure() = faça 605 | retorna x 606 | fim 607 | 608 | retorna closure 609 | fim 610 | 611 | seja c = cria() 612 | 613 | x = 999 614 | 615 | seja resultado = c() 616 | "#; 617 | 618 | let runtime = interpret_stmt(platform, source); 619 | let value = runtime.get_global_env().get("resultado").unwrap().extract(); 620 | 621 | assert_eq!(value, Value::Number(999.0)); 622 | } 623 | 624 | #[rstest] 625 | #[case(OSPlatform)] 626 | fn function_reference_equality(#[case] platform: impl Platform + 'static) { 627 | let source = r#" 628 | seja f() = 0 629 | seja g() = 1 630 | seja cmp = f é g 631 | "#; 632 | 633 | let runtime = interpret_stmt(platform, source); 634 | let value = runtime.get_global_env().get("cmp").unwrap().extract(); 635 | 636 | assert_eq!(value, Value::Boolean(false)); 637 | } 638 | 639 | #[rstest] 640 | #[case(OSPlatform)] 641 | fn same_function_reference_equality(#[case] platform: impl Platform + 'static) { 642 | let source = r#" 643 | seja f() = faça 644 | retorna 0 645 | fim 646 | 647 | seja cmp = f é f 648 | "#; 649 | 650 | let runtime = interpret_stmt(platform, source); 651 | let value = runtime.get_global_env().get("cmp").unwrap().extract(); 652 | 653 | assert_eq!(value, Value::Boolean(true)); 654 | } 655 | 656 | #[rstest] 657 | #[case(OSPlatform)] 658 | fn while_break_in_nested_if(#[case] platform: impl Platform + 'static) { 659 | let source = r#" 660 | seja i = 0 661 | seja total = 0 662 | 663 | enquanto i < 10 faça 664 | i = i + 1 665 | 666 | se i é 5 então faça 667 | para 668 | fim 669 | 670 | total = total + i 671 | fim 672 | "#; 673 | 674 | let runtime = interpret_stmt(platform, source); 675 | let val = runtime.get_global_env().get("total").unwrap().extract(); 676 | 677 | assert_eq!(val, Value::Number(10.0)); 678 | } 679 | 680 | #[rstest] 681 | #[case(OSPlatform)] 682 | #[should_panic] 683 | fn break_outside_loop(#[case] platform: impl Platform + 'static) { 684 | interpret_stmt(platform, "para"); 685 | } 686 | 687 | #[rstest] 688 | #[case(OSPlatform)] 689 | #[should_panic] 690 | fn continue_outside_loop(#[case] platform: impl Platform + 'static) { 691 | interpret_stmt(platform, "continua"); 692 | } 693 | 694 | #[rstest] 695 | #[case(OSPlatform)] 696 | #[should_panic] 697 | fn repeated_function_param(#[case] platform: impl Platform + 'static) { 698 | let source = r#" 699 | seja f(a, a) = faça 700 | retorna a 701 | fim 702 | "#; 703 | 704 | interpret_stmt(platform, source); 705 | } 706 | 707 | #[rstest] 708 | #[case(OSPlatform)] 709 | #[should_panic] 710 | fn block_scope_after_while(#[case] platform: impl Platform + 'static) { 711 | let source = r#" 712 | enquanto falso faça 713 | seja x = 1 714 | fim 715 | 716 | seja y = x 717 | "#; 718 | 719 | interpret_stmt(platform, source); 720 | } 721 | 722 | #[rstest] 723 | #[case(OSPlatform)] 724 | fn function_returns_no_explicit_value(#[case] platform: impl Platform + 'static) { 725 | let source = r#" 726 | seja sem_retorno() = faça 727 | seja x = 123 728 | fim 729 | 730 | seja resultado = sem_retorno() 731 | "#; 732 | 733 | let runtime = interpret_stmt(platform, source); 734 | let val = runtime.get_global_env().get("resultado").unwrap().extract(); 735 | 736 | assert_eq!(val, Value::Nil); 737 | } 738 | 739 | #[rstest] 740 | #[case(OSPlatform)] 741 | #[should_panic] 742 | fn negative_list_index_assignment(#[case] platform: impl Platform + 'static) { 743 | let source = r#" 744 | seja lista = [100, 200] 745 | lista[-1] = 999 746 | "#; 747 | 748 | interpret_stmt(platform, source); 749 | } 750 | 751 | #[rstest] 752 | #[case(OSPlatform)] 753 | #[should_panic] 754 | fn function_in_lhs_of_assignment(#[case] platform: impl Platform + 'static) { 755 | let source = r#" 756 | seja f() = 0 757 | f() = 999 758 | "#; 759 | 760 | interpret_stmt(platform, source); 761 | } 762 | 763 | #[rstest] 764 | #[case(OSPlatform)] 765 | fn overshadow_variable_in_function_block(#[case] platform: impl Platform + 'static) { 766 | let source = r#" 767 | seja x = 10 768 | 769 | seja testa() = faça 770 | seja x = 999 771 | 772 | retorna x 773 | fim 774 | 775 | seja resultado = testa() 776 | "#; 777 | 778 | let runtime = interpret_stmt(platform, source); 779 | let outer_x = runtime.get_global_env().get("x").unwrap().extract(); 780 | let result = runtime.get_global_env().get("resultado").unwrap().extract(); 781 | 782 | assert_eq!(outer_x, Value::Number(10.0)); 783 | assert_eq!(result, Value::Number(999.0)); 784 | } 785 | 786 | #[rstest] 787 | #[case(OSPlatform)] 788 | fn overshadow_parameter_in_function_block(#[case] platform: impl Platform + 'static) { 789 | let source = r#" 790 | seja soma_duas_vezes(a) = faça 791 | seja a = a + a 792 | retorna a 793 | fim 794 | 795 | seja resultado = soma_duas_vezes(5) 796 | "#; 797 | 798 | let runtime = interpret_stmt(platform, source); 799 | let val = runtime.get_global_env().get("resultado").unwrap().extract(); 800 | 801 | assert_eq!(val, Value::Number(10.0)); 802 | } 803 | 804 | #[rstest] 805 | #[case(OSPlatform)] 806 | fn partial_return_in_if(#[case] platform: impl Platform + 'static) { 807 | let source = r#" 808 | seja checa_flag(flag) = faça 809 | se flag então faça 810 | retorna 111 811 | fim 812 | 813 | retorna 222 814 | fim 815 | 816 | seja r1 = checa_flag(verdadeiro) 817 | seja r2 = checa_flag(falso) 818 | "#; 819 | 820 | let runtime = interpret_stmt(platform, source); 821 | let r1 = runtime.get_global_env().get("r1").unwrap().extract(); 822 | let r2 = runtime.get_global_env().get("r2").unwrap().extract(); 823 | 824 | assert_eq!(r1, Value::Number(111.0)); 825 | assert_eq!(r2, Value::Number(222.0)); 826 | } 827 | 828 | #[rstest] 829 | #[case(OSPlatform)] 830 | #[should_panic] 831 | fn parse_error_incomplete_function_declaration(#[case] platform: impl Platform + 'static) { 832 | interpret_stmt(platform, r#"seja soma(a, b)"#); 833 | } 834 | 835 | #[rstest] 836 | #[case(OSPlatform)] 837 | #[should_panic] 838 | fn parse_error_incomplete_function_declaration_2(#[case] platform: impl Platform + 'static) { 839 | interpret_stmt(platform, r#"seja soma(a, b) ="#); 840 | } 841 | 842 | #[rstest] 843 | #[case(OSPlatform)] 844 | #[should_panic] 845 | fn parse_error_incomplete_if(#[case] platform: impl Platform + 'static) { 846 | let input = r#" 847 | se verdadeiro então faça 848 | seja x = 1 849 | "#; 850 | 851 | interpret_stmt(platform, input); 852 | } 853 | 854 | #[rstest] 855 | #[case(OSPlatform)] 856 | #[should_panic] 857 | fn parse_error_incomplete_while(#[case] platform: impl Platform + 'static) { 858 | let input = r#" 859 | enquanto verdadeiro faça 860 | seja x = 1 861 | "#; 862 | 863 | interpret_stmt(platform, input); 864 | } 865 | 866 | #[rstest] 867 | #[case(OSPlatform)] 868 | #[should_panic] 869 | fn parse_error_incomplete_for(#[case] platform: impl Platform + 'static) { 870 | let input = r#" 871 | para cada i em [1, 2] 872 | seja x = i 873 | "#; 874 | 875 | interpret_stmt(platform, input); 876 | } 877 | 878 | #[rstest] 879 | #[case(OSPlatform)] 880 | #[should_panic] 881 | fn parse_error_return_outside_function(#[case] platform: impl Platform + 'static) { 882 | interpret_stmt(platform, "retorna 5"); 883 | } 884 | 885 | #[rstest] 886 | #[case(OSPlatform)] 887 | #[should_panic] 888 | fn parse_error_break_outside_loop(#[case] platform: impl Platform + 'static) { 889 | interpret_stmt(platform, "para"); 890 | } 891 | 892 | #[rstest] 893 | #[case(OSPlatform)] 894 | #[should_panic] 895 | fn parse_error_continue_outside_loop(#[case] platform: impl Platform + 'static) { 896 | interpret_stmt(platform, "continua"); 897 | } 898 | 899 | #[rstest] 900 | #[case(OSPlatform)] 901 | fn zero_or_negative_range_iteration(#[case] platform: impl Platform + 'static) { 902 | let source = r#" 903 | seja soma = 0 904 | 905 | para cada i em 5 até 1 faça 906 | soma = soma + i 907 | fim 908 | "#; 909 | 910 | let runtime = interpret_stmt(platform, source); 911 | let val = runtime.get_global_env().get("soma").unwrap().extract(); 912 | 913 | assert_eq!(val, Value::Number(0.0)); 914 | } 915 | 916 | #[rstest] 917 | #[case(OSPlatform)] 918 | #[should_panic] 919 | fn use_function_argument_outside_function(#[case] platform: impl Platform + 'static) { 920 | let source = r#" 921 | seja f(x) = x 922 | f(1) 923 | x 924 | "#; 925 | 926 | interpret_stmt(platform, source); 927 | } 928 | --------------------------------------------------------------------------------