Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Implement basic std functions to manipulate arrays #626

Closed
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ wildmatch = "2.4.0"
[dev-dependencies]
assert_cmd = "2.0.14"
predicates = "3.1.0"
pretty_assertions = "1.4.1"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creates more readable output for assert_eq! macro. See https://crates.io/crates/pretty_assertions for more information.

tempfile = "3.10.1"
tiny_http = "0.12.0"

Expand Down
71 changes: 57 additions & 14 deletions src/modules/expression/binop/range.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use std::ops::Sub;
use heraclitus_compiler::prelude::*;
use crate::docs::module::DocumentationModule;
use crate::modules::expression::expr::{Expr, ExprType};
use crate::modules::expression::binop::BinOp;
use crate::modules::expression::expr::Expr;
use crate::modules::types::{Type, Typed};
use crate::utils::metadata::ParserMetadata;
use crate::translate::compute::{translate_computation, ArithOp};
use crate::translate::module::TranslateModule;
use crate::utils::metadata::ParserMetadata;
use crate::utils::TranslateMetadata;
use crate::{handle_binop, error_type_match};
use super::BinOp;
use crate::{error_type_match, handle_binop};
use heraclitus_compiler::prelude::*;
use std::cmp::max;

#[derive(Debug, Clone)]
pub struct Range {
Expand Down Expand Up @@ -59,17 +59,60 @@ impl SyntaxModule<ParserMetadata> for Range {
impl TranslateModule for Range {
fn translate(&self, meta: &mut TranslateMetadata) -> String {
let from = self.from.translate(meta);
let to = self.to.translate(meta);
if self.neq {
let to_neq = if let Some(ExprType::Number(_)) = &self.to.value {
to.parse::<isize>().unwrap_or_default().sub(1).to_string()
let to = if let Some(to) = self.to.get_integer_value() {
if self.neq {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use compile-time arithmetic to convert Amber [1..11] to Bash $(seq 1 10); fall back to calling bc if not supplied a number.

Copy link
Contributor Author

@hdwalters hdwalters Dec 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's ok to assume we're given an integer not a decimal here, but we can always come back to this when we have a separate integer type.

(to - 1).to_string()
} else {
translate_computation(meta, ArithOp::Sub, Some(to), Some("1".to_string()))
};
meta.gen_subprocess(&format!("seq {} {}", from, to_neq))
to.to_string()
}
} else {
meta.gen_subprocess(&format!("seq {} {}", from, to))
let to = self.to.translate(meta);
if self.neq {
translate_computation(meta, ArithOp::Sub, Some(to), Some("1".to_string()))
} else {
to
}
};
let stmt = format!("seq {} {}", from, to);
meta.gen_subprocess(&stmt)
}
}

impl Range {
pub fn get_array_index(&self, meta: &mut TranslateMetadata) -> (String, String) {
if let Some(from) = self.from.get_integer_value() {
if let Some(mut to) = self.to.get_integer_value() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use compile-time arithmetic to convert Amber array[10..=12] to Bash ${array[@]:10:3}; fall back to calling bc if not supplied a number.

// Make the upper bound exclusive.
if !self.neq {
to += 1;
}
// Cap the lower bound at zero.
let offset = max(from, 0);
// Cap the slice length at zero.
let length = max(to - offset, 0);
return (offset.to_string(), length.to_string());
}
}
let local = if meta.fun_meta.is_some() { "local " } else { "" };
// Make the upper bound exclusive.
let upper_name = format!("__SLICE_UPPER_{}", meta.gen_value_id());
let mut upper_val = self.to.translate(meta);
if !self.neq {
upper_val = translate_computation(meta, ArithOp::Add, Some(upper_val), Some("1".to_string()));
}
meta.stmt_queue.push_back(format!("{local}{upper_name}={upper_val}"));
// Cap the lower bound at zero.
let offset_name = format!("__SLICE_OFFSET_{}", meta.gen_value_id());
let offset_val = self.from.translate(meta);
meta.stmt_queue.push_back(format!("{local}{offset_name}={offset_val}"));
meta.stmt_queue.push_back(format!("{offset_name}=$(({offset_name} > 0 ? {offset_name} : 0))"));
let offset_val = format!("${offset_name}");
// Cap the slice length at zero.
let length_name = format!("__SLICE_LENGTH_{}", meta.gen_value_id());
let length_val = translate_computation(meta, ArithOp::Sub, Some(upper_val), Some(offset_val));
meta.stmt_queue.push_back(format!("{local}{length_name}={length_val}"));
meta.stmt_queue.push_back(format!("{length_name}=$(({length_name} > 0 ? {length_name} : 0))"));
(format!("${offset_name}"), format!("${length_name}"))
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/modules/expression/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ impl Typed for Expr {
}

impl Expr {
pub fn get_integer_value(&self) -> Option<isize> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is intended to return an integer for compile-time arithmetic, but only when given integer literals like 42 or -42. We're not worrying about float literals like 123.45 because this will only be called in contexts where an integer is required.

Copy link
Contributor Author

@hdwalters hdwalters Dec 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was tempted to put this functionality into the TranslateModule trait, but held off because it's fairly niche at the moment. However, I think doing compile time arithmetic where possible cannot be a bad idea; and we may decide to refactor if it becomes more widely used.

match &self.value {
Some(ExprType::Number(value)) => value.get_integer_value(),
Some(ExprType::Neg(value)) => value.get_integer_value(),
_ => None,
}
}

pub fn get_position(&self, meta: &mut ParserMetadata) -> PositionInfo {
let begin = meta.get_token_at(self.pos.0);
let end = meta.get_token_at(self.pos.1);
Expand Down
7 changes: 7 additions & 0 deletions src/modules/expression/literal/number.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ impl TranslateModule for Number {
}
}

impl Number {
pub fn get_integer_value(&self) -> Option<isize> {
let value = self.value.parse().unwrap_or_default();
Some(value)
}
}

impl DocumentationModule for Number {
fn document(&self, _meta: &ParserMetadata) -> String {
"".to_string()
Expand Down
27 changes: 24 additions & 3 deletions src/modules/expression/unop/neg.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
use heraclitus_compiler::prelude::*;
use crate::{utils::{metadata::ParserMetadata, TranslateMetadata}, modules::types::{Type, Typed}, translate::{module::TranslateModule, compute::{translate_computation, ArithOp}}};
use super::{super::expr::Expr, UnOp};
use crate::docs::module::DocumentationModule;
use crate::error_type_match;
use crate::modules::expression::expr::Expr;
use crate::modules::expression::unop::UnOp;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should avoid nested use statements like the one I'm replacing here, as they're hard to read and maintain. RustRover has handy tools to "flatten use statements" and "optimize imports".

use crate::modules::types::{Type, Typed};
use crate::translate::compute::{translate_computation, ArithOp};
use crate::translate::module::TranslateModule;
use crate::utils::metadata::ParserMetadata;
use crate::utils::TranslateMetadata;
use heraclitus_compiler::prelude::*;
use std::ops::Neg as _;

#[derive(Debug, Clone)]
pub struct Neg {
Expand Down Expand Up @@ -51,6 +57,21 @@ impl TranslateModule for Neg {
}
}

impl Neg {
pub fn get_integer_value(&self) -> Option<isize> {
self.expr.get_integer_value().map(isize::neg)
}

pub fn get_array_index(&self, meta: &mut TranslateMetadata) -> String {
if let Some(expr) = self.get_integer_value() {
expr.to_string()
} else {
let expr = self.expr.translate(meta);
translate_computation(meta, ArithOp::Neg, None, Some(expr))
}
}
}

impl DocumentationModule for Neg {
fn document(&self, _meta: &ParserMetadata) -> String {
"".to_string()
Expand Down
2 changes: 1 addition & 1 deletion src/modules/function/invocation_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ fn run_function_with_args(meta: &mut ParserMetadata, mut fun: FunctionDecl, args
})?;
// Set the new return type or null if nothing was returned
if let Type::Generic = fun.returns {
fun.returns = context.fun_ret_type.clone().unwrap_or_else(|| Type::Null);
fun.returns = context.fun_ret_type.clone().unwrap_or(Type::Null);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Clippy-suggested change has already been made on the master branch by @Ph0enixKM; but my change is identical, and should not cause a merge conflict.

};
// Set the new argument types
fun.arg_types = args.to_vec();
Expand Down
104 changes: 78 additions & 26 deletions src/modules/variable/get.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use heraclitus_compiler::prelude::*;
use crate::{docs::module::DocumentationModule, modules::{expression::expr::Expr, types::{Type, Typed}}, utils::{ParserMetadata, TranslateMetadata}};
use crate::docs::module::DocumentationModule;
use crate::modules::expression::expr::{Expr, ExprType};
use crate::modules::types::{Type, Typed};
use crate::modules::variable::{handle_index_accessor, handle_variable_reference, variable_name_extensions};
use crate::translate::module::TranslateModule;
use super::{variable_name_extensions, handle_variable_reference, handle_index_accessor};
use crate::utils::{ParserMetadata, TranslateMetadata};
use heraclitus_compiler::prelude::*;

#[derive(Debug, Clone)]
pub struct VariableGet {
Expand All @@ -23,10 +26,22 @@ impl VariableGet {

impl Typed for VariableGet {
fn get_type(&self) -> Type {
match (&*self.index, self.kind.clone()) {
// Return the type of the array element if indexed
(Some(_), Type::Array(kind)) => *kind,
_ => self.kind.clone()
if let Type::Array(item_kind) = &self.kind {
if let Some(index) = self.index.as_ref() {
if let Some(ExprType::Range(_)) = &index.value {
// Array type (indexing array by range)
self.kind.clone()
} else {
// Item type (indexing array by number)
*item_kind.clone()
}
} else {
// Array type (returning array)
self.kind.clone()
}
} else {
// Variable type (returning text or number)
self.kind.clone()
}
}
}
Expand All @@ -51,7 +66,7 @@ impl SyntaxModule<ParserMetadata> for VariableGet {
self.global_id = variable.global_id;
self.is_ref = variable.is_ref;
self.kind = variable.kind.clone();
self.index = Box::new(handle_index_accessor(meta)?);
self.index = Box::new(handle_index_accessor(meta, true)?);
// Check if the variable can be indexed
if self.index.is_some() && !matches!(variable.kind, Type::Array(_)) {
return error!(meta, tok, format!("Cannot index a non-array variable of type '{}'", self.kind));
Expand All @@ -61,29 +76,66 @@ impl SyntaxModule<ParserMetadata> for VariableGet {
}

impl TranslateModule for VariableGet {
#[allow(clippy::collapsible_else_if)]
fn translate(&self, meta: &mut TranslateMetadata) -> String {
Copy link
Contributor Author

@hdwalters hdwalters Dec 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not want to collapse the else if chain here, because the two main branches under Type::Array(_) deal with the "pass by reference" and "pass by value" cases, and should therefore be at the same indentation level.

let name = self.get_translated_name();
let ref_prefix = if self.is_ref { "!" } else { "" };
let res = format!("${{{ref_prefix}{name}}}");
// Text variables need to be encapsulated in string literals
// Otherwise, they will be "spread" into tokens
let quote = meta.gen_quote();
match (self.is_ref, &self.kind) {
(false, Type::Array(_)) => match *self.index {
Some(ref expr) => format!("{quote}${{{name}[{}]}}{quote}", expr.translate(meta)),
None => format!("{quote}${{{name}[@]}}{quote}")
},
(true, Type::Array(_)) => match *self.index {
Some(ref expr) => {
let id = meta.gen_value_id();
let expr = expr.translate_eval(meta, true);
meta.stmt_queue.push_back(format!("eval \"local __AMBER_ARRAY_GET_{id}_{name}=\\\"\\${{${name}[{expr}]}}\\\"\""));
format!("$__AMBER_ARRAY_GET_{id}_{name}") // echo $__ARRAY_GET
},
None => format!("{quote}${{!__AMBER_ARRAY_{name}}}{quote}")
},
(_, Type::Text) => format!("{quote}{res}{quote}"),
_ => res
match &self.kind {
Type::Array(_) => {
if self.is_ref {
if let Some(index) = self.index.as_ref() {
let value = match &index.value {
Some(ExprType::Range(range)) => {
let (offset, length) = range.get_array_index(meta);
format!("\\\"\\${{${name}[@]:{offset}:{length}}}\\\"")
}
Some(ExprType::Neg(neg)) => {
let index = neg.get_array_index(meta);
format!("\\\"\\${{${name}[{index}]}}\\\"")
}
_ => {
let index = index.translate_eval(meta, true);
format!("\\\"\\${{${name}[{index}]}}\\\"")
}
};
let id = meta.gen_value_id();
let stmt = format!("eval \"local __AMBER_ARRAY_GET_{id}_{name}={value}\"");
meta.stmt_queue.push_back(stmt);
format!("$__AMBER_ARRAY_GET_{id}_{name}") // echo $__ARRAY_GET
} else {
format!("{quote}${{!__AMBER_ARRAY_{name}}}{quote}")
}
} else {
if let Some(index) = self.index.as_ref() {
match &index.value {
Some(ExprType::Range(range)) => {
let (offset, length) = range.get_array_index(meta);
format!("{quote}${{{name}[@]:{offset}:{length}}}{quote}")
}
Some(ExprType::Neg(neg)) => {
let index = neg.get_array_index(meta);
format!("{quote}${{{name}[{index}]}}{quote}")
}
_ => {
let index = index.translate(meta);
format!("{quote}${{{name}[{index}]}}{quote}")
}
}
} else {
format!("{quote}${{{name}[@]}}{quote}")
}
}
}
Type::Text => {
let ref_prefix = if self.is_ref { "!" } else { "" };
format!("{quote}${{{ref_prefix}{name}}}{quote}")
}
_ => {
let ref_prefix = if self.is_ref { "!" } else { "" };
format!("${{{ref_prefix}{name}}}")
}
}
}
}
Expand Down
Loading
Loading