Skip to content

Commit

Permalink
Support array indexing by range (#628)
Browse files Browse the repository at this point in the history
* Use pretty assertions crate.

* Rename, rewrite and extend array tests.

* Refactor variable translation; optimise imports.

* Refactor array index by negative number.

* Support array index by range.

* Quote dollar for by ref array concatenation.

* Separate inclusive and exclusive range tests.

* Refactor array variable indexing and typing.

* Address Clippy errors.

* Add comment to integer indexing test.

* Call standard translate function if negative is not numeric.

* Add tests for incorrect array slicing.

* Reword error for incorrect array slicing.

* Reword error for incorrect array slicing.

* Reword error for incorrect array slicing.
  • Loading branch information
hdwalters authored Dec 12, 2024
1 parent 82817af commit a4bbf9e
Show file tree
Hide file tree
Showing 26 changed files with 666 additions and 68 deletions.
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"
tempfile = "3.10.1"
tiny_http = "0.12.0"

Expand Down
3 changes: 2 additions & 1 deletion src/modules/expression/binop/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,11 @@ impl TranslateModule for Add {
match self.kind {
Type::Array(_) => {
let quote = meta.gen_quote();
let dollar = meta.gen_dollar();
let id = meta.gen_value_id();
let name = format!("__AMBER_ARRAY_ADD_{id}");
meta.stmt_queue.push_back(format!("{name}=({left} {right})"));
format!("{quote}${{{name}[@]}}{quote}")
format!("{quote}{dollar}{{{name}[@]}}{quote}")
},
Type::Text => format!("{}{}", left, right),
_ => translate_computation(meta, ArithOp::Add, Some(left), Some(right))
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 {
(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() {
// 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> {
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
26 changes: 23 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;
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,20 @@ 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 {
self.translate(meta)
}
}
}

impl DocumentationModule for Neg {
fn document(&self, _meta: &ParserMetadata) -> String {
"".to_string()
Expand Down
128 changes: 94 additions & 34 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 @@ -12,21 +15,25 @@ pub struct VariableGet {
is_ref: bool
}

impl VariableGet {
pub fn get_translated_name(&self) -> String {
match self.global_id {
Some(id) => format!("__{id}_{}", self.name),
None => self.name.to_string()
}
}
}

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()
match (&self.kind, self.index.as_ref()) {
(Type::Array(kind), Some(index)) if matches!(index.value, Some(ExprType::Range(_))) => {
// Array type (indexing array by range)
Type::Array(kind.clone())
}
(Type::Array(kind), Some(_)) => {
// Item type (indexing array by number)
*kind.clone()
}
(Type::Array(kind), None) => {
// Array type (returning array)
Type::Array(kind.clone())
}
(kind, _) => {
// Variable type (returning text or number)
kind.clone()
}
}
}
}
Expand All @@ -51,7 +58,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 @@ -63,27 +70,80 @@ impl SyntaxModule<ParserMetadata> for VariableGet {
impl TranslateModule for VariableGet {
fn translate(&self, meta: &mut TranslateMetadata) -> String {
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) => {
match &self.kind {
Type::Array(_) if self.is_ref => {
if let Some(index) = self.index.as_ref() {
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
let value = Self::slice_ref_array(meta, &name, index);
let stmt = format!("eval \"local __AMBER_ARRAY_GET_{id}_{name}=\\\"\\${{{value}}}\\\"\"");
meta.stmt_queue.push_back(stmt);
format!("$__AMBER_ARRAY_GET_{id}_{name}")
} else {
format!("{quote}${{!__AMBER_ARRAY_{name}}}{quote}")
}
}
Type::Array(_) if !self.is_ref => {
if let Some(index) = self.index.as_ref() {
let value = Self::slice_copy_array(meta, &name, index);
format!("{quote}{value}{quote}")
} else {
format!("{quote}${{{name}[@]}}{quote}")
}
}
Type::Text => {
let prefix = if self.is_ref { "!" } else { "" };
format!("{quote}${{{prefix}{name}}}{quote}")
}
_ => {
let prefix = if self.is_ref { "!" } else { "" };
format!("${{{prefix}{name}}}")
}
}
}
}

impl VariableGet {
pub fn get_translated_name(&self) -> String {
match self.global_id {
Some(id) => format!("__{id}_{}", self.name),
None => self.name.to_string()
}
}

fn slice_ref_array(meta: &mut TranslateMetadata, name: &str, index: &Expr) -> String {
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}]")
}
}
}

fn slice_copy_array(meta: &mut TranslateMetadata, name: &str, index: &Expr) -> String {
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(meta);
format!("${{{name}[{index}]}}")
}
}
}
}
Expand Down
Loading

0 comments on commit a4bbf9e

Please sign in to comment.