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

Conversation

hdwalters
Copy link
Contributor

Implements some standard library functions that allow to manipulate arrays:

  1. first(array) to get the first element (merely a shorthand for array[0])
  2. last(array) to get the last element (merely a shorthand for array[len(array) - 1])
  3. remove_at(array, index) to remove an element at the index, translates to unset array[index]
  4. extract_at(array, index) — same as remove_at, but also returns the element
  5. pop(array) — remove and return the last element
  6. shift(array) — remove and return the first element

@Mte90
Copy link
Member

Mte90 commented Dec 2, 2024

Test fails right now

@hdwalters
Copy link
Contributor Author

Yes I know, still working on it.

@@ -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.

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() {
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.


impl Range {
pub fn get_array_index(&self, meta: &mut TranslateMetadata) -> (String, String) {
if let Some(from) = self.from.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.

@@ -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.

use crate::docs::module::DocumentationModule;
use crate::error_type_match;
use crate::modules::expression::expr::Expr;
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".

fn allow_index_accessor(index: &Expr, range: bool) -> bool {
match (&index.kind, &index.value) {
(Type::Num, _) => true,
(Type::Array(_), Some(ExprType::Range(_))) => range,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Allow range indexing for echo array[2..5] but not array[2..5] = [100, 101].

@@ -1,7 +1,7 @@
/// Returns index of the first value found in the specified array.
///
/// If the value is not found, the function returns -1.
pub fun array_first_index(array, value): Num {
pub fun array_index(array, value): Num {
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 renamed this standard library function to more closely align with array_search. I think it's ok to do this while we're still in alpha, but further down the line we may want to have the compiler automatically use deprecated function names (with a warning) via a new annotation:

#[replaces_deprecated(array_first_index)]
pub fun array_index(array, value): Num {

return result >= 0
}

/// Returns the first element in the array
pub fun first(array) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should we also call these array_first() and array_last() while we're at it?

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.

The major argument for more verbose standard library function names (e.g. containing at least one underscore) is that we're unlikely to want to reuse them for builtins (a case in which a #[replaces_deprecated] annotation would not help). Note that I have already renamed lines as split_lines to make way for the lines builtin.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With this in mind, we may want a separate discussion on renaming the wider set of standard library functions, before we release 0.4.0. I will create a discussion channel for this on Discord.

@@ -0,0 +1,25 @@
import { remove_at } from "std/array"

// Output
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Together with array_extract(), this unit test is currently failing. I have boiled down the code under test to the simplest possible version, which I will post on Discord, along with a plea for help.

// Array after 4: (4) [apple banana cherry date]

fun test_remove(inner: [Text], index: Num): Null {
let value = remove_at(inner, index)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy/paste error. Note to self: remove let value = as value is unused here.

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}")
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 fixes the failing unit tests. We were passing the contents of the temporary array, rather than the name, in an eval context; and the contents were all getting smooshed together into a single array item.

@mks-h
Copy link
Member

mks-h commented Dec 4, 2024

Please don't combine multiple issues into a single PR. This makes it nearly impossible to review, as there's too much context. The standard library changes should be in their own PR (as they were), while your range changes should be in another PR (which one should be merged first is up to you). Adding new dependencies which are not required for the work at hand should be in their own PR. And even more, stylistic changes of unrelated code ("optimize imports") should be their own PR.

I'm sorry, but I'll have to close this PR. Please decouple your changes as much as possible, and open separate PRs.

@mks-h mks-h closed this Dec 4, 2024
@hdwalters
Copy link
Contributor Author

I'm sorry, but I'll have to close this PR. Please decouple your changes as much as possible, and open separate PRs.

Fair point. Since I needed the compiler changes to implement the stdlib functions, I put them in the same branch. But I don't mind separating them; it's just cherry picking.

@hdwalters
Copy link
Contributor Author

hdwalters commented Dec 4, 2024

The core changes should be merged first, but I'm not sure I agree that import optimisation needs its own PR, unless we touch the entire codebase (which I'm not suggesting btw).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants