Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 7 additions & 1 deletion grammar.ebnf
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ KEYWORD_REF = 'ref' ;
KEYWORD_RETURN = 'return' ;
KEYWORD_SILENT = 'silent' ;
KEYWORD_STATUS = 'status' ;
KEYWORD_SUCCEEDED = 'succeeded' ;
KEYWORD_THEN = 'then' ;
KEYWORD_TRUST = 'trust' ;
KEYWORD_UNSAFE = 'unsafe' ;
Expand Down Expand Up @@ -126,7 +127,7 @@ list = empty_list | full_list ;
command_modifier = SILENT_MOD, [ TRUST_MOD ] ;
command_modifier_block = command_modifier, multiline_block ;
command_base = '$', { ANY_CHAR | interpolation }, '$' ;
command = [ SILENT_MOD ], command_base, [ failure_handler ] ;
command = [ SILENT_MOD ], command_base, [ failure_handler | success_handler ] ;
command_trust = [ SILENT_MOD ], TRUST_MOD, command_base ;

(* Operations *)
Expand All @@ -141,6 +142,10 @@ failure_propagation = '?';
failure_block = KEYWORD_FAILED, block ;
failure_handler = failure_propagation | failure_block ;

(* Success handler *)
success_block = KEYWORD_SUCCEEDED, block ;
success_handler = success_block ;

(* Variable *)
variable_index = '[', expression, ']' ;
variable_init_mut = KEYWORD_LET, identifier, '=', expression ;
Expand All @@ -151,6 +156,7 @@ variable_set = identifier, variable_index?, '=', expression ;
(* Function *)
function_call = identifier, '(', [ expression, { ',', expression } ], ')' ;
function_call_failed = [ SILENT_MOD ], function_call, failure_handler ;
function_call_succeeded = [ SILENT_MOD ], function_call, success_handler ;
function_call_trust = [ SILENT_MOD ], TRUST_MOD, function_call ;
function_def = [ VISIBILITY ], KEYWORD_FUN, identifier, '(', [ identifier, { ',', identifier } ], ')', block ;
function_def_typed = [ VISIBILITY ], KEYWORD_FUN, identifier, '(',
Expand Down
59 changes: 48 additions & 11 deletions src/modules/command/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::mem::swap;
use crate::modules::types::{Type, Typed};
use crate::modules::expression::literal::bool;
use crate::modules::condition::failed::Failed;
use crate::modules::condition::succeeded::Succeeded;
use crate::modules::expression::expr::Expr;
use crate::modules::expression::interpolated_region::{InterpolatedRegionType, parse_interpolated_region};
use super::modifier::CommandModifier;
Expand All @@ -14,7 +15,8 @@ pub struct Command {
strings: Vec<String>,
interps: Vec<Expr>,
modifier: CommandModifier,
failed: Failed
failed: Failed,
succeeded: Succeeded
}

impl Typed for Command {
Expand All @@ -31,7 +33,8 @@ impl SyntaxModule<ParserMetadata> for Command {
strings: vec![],
interps: vec![],
modifier: CommandModifier::new().parse_expr(),
failed: Failed::new()
failed: Failed::new(),
succeeded: Succeeded::new()
}
}

Expand All @@ -40,13 +43,37 @@ impl SyntaxModule<ParserMetadata> for Command {
self.modifier.use_modifiers(meta, |_this, meta| {
let tok = meta.get_current_token();
(self.strings, self.interps) = parse_interpolated_region(meta, &InterpolatedRegionType::Command)?;
self.failed.set_position(PositionInfo::from_between_tokens(meta, tok.clone(), meta.get_current_token()));

// Set position for both failed and succeeded handlers
let position = PositionInfo::from_between_tokens(meta, tok.clone(), meta.get_current_token());
self.failed.set_position(position.clone());
self.succeeded.set_position(position);

// Try to parse succeeded block first
syntax(meta, &mut self.succeeded)?;

// If succeeded block was parsed successfully, check for conflicts with failed
if self.succeeded.is_parsed {
// Check if there's an attempt to use failed block as well
if token(meta, "failed").is_ok() {
return error!(meta, meta.get_current_token() => {
message: "Cannot use both 'succeeded' and 'failed' blocks for the same command",
comment: "Use either 'succeeded' or 'failed' block, but not both"
});
}
return Ok(());
}

// If no succeeded block, try to parse failed block
match syntax(meta, &mut self.failed) {
Ok(_) => Ok(()),
Err(Failure::Quiet(_)) => error!(meta, tok => {
message: "Every command statement must handle failed execution",
comment: "You can use '?' in the end to propagate the failure"
}),
Err(Failure::Quiet(_)) => {
// Neither succeeded nor failed block found
error!(meta, tok => {
message: "Every command statement must handle execution result",
comment: "You can use '?' to propagate failure, 'failed' block to handle failure, 'succeeded' block to handle success, or 'trust' modifier to ignore results"
})
},
Err(err) => Err(err)
}
})
Expand All @@ -60,6 +87,7 @@ impl Command {
.map(|item| item.translate(meta).with_quotes(false))
.collect::<Vec<FragmentKind>>();
let failed = self.failed.translate(meta);
let succeeded = self.succeeded.translate(meta);

let mut is_silent = self.modifier.is_silent || meta.silenced;
swap(&mut is_silent, &mut meta.silenced);
Expand All @@ -74,21 +102,30 @@ impl Command {
let translation = fragments!(translation, silent);
swap(&mut is_silent, &mut meta.silenced);

// Choose between failed, succeeded, or no handler
let handler = if self.failed.is_parsed {
failed
} else if self.succeeded.is_parsed {
succeeded
} else {
FragmentKind::Empty
};

if is_statement {
if let FragmentKind::Empty = failed {
if let FragmentKind::Empty = handler {
translation
} else {
meta.stmt_queue.push_back(translation);
failed
handler
}
} else if let FragmentKind::Empty = failed {
} else if let FragmentKind::Empty = handler {
SubprocessFragment::new(translation).to_frag()
} else {
let id = meta.gen_value_id();
let value = SubprocessFragment::new(translation).to_frag();
let var_stmt = VarStmtFragment::new("__command", Type::Text, value).with_global_id(id);
let var_expr = meta.push_ephemeral_variable(var_stmt);
meta.stmt_queue.push_back(failed);
meta.stmt_queue.push_back(handler);
var_expr.to_frag()
}
}
Expand Down
1 change: 1 addition & 0 deletions src/modules/condition/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod ifcond;
pub mod ifchain;
pub mod failed;
pub mod succeeded;
91 changes: 91 additions & 0 deletions src/modules/condition/succeeded.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use heraclitus_compiler::prelude::*;
use crate::fragments;
use crate::modules::prelude::*;
use crate::modules::block::Block;
use crate::modules::types::Type;

#[derive(Debug, Clone)]
pub struct Succeeded {
pub is_parsed: bool,
error_position: Option<PositionInfo>,
function_name: Option<String>,
block: Box<Block>
}

impl Succeeded {
pub fn set_position(&mut self, position: PositionInfo) {
self.error_position = Some(position);
}

pub fn set_function_name(&mut self, name: String) {
self.function_name = Some(name);
}
}

impl SyntaxModule<ParserMetadata> for Succeeded {
syntax_name!("Succeeded Expression");

fn new() -> Self {
Succeeded {
is_parsed: false,
function_name: None,
error_position: None,
block: Box::new(Block::new().with_needs_noop().with_condition())
}
}

fn parse(&mut self, meta: &mut ParserMetadata) -> SyntaxResult {
match token(meta, "succeeded") {
Ok(_) => {
let tok = meta.get_current_token();
syntax(meta, &mut *self.block)?;
if self.block.is_empty() {
let message = Message::new_warn_at_token(meta, tok)
.message("Empty succeeded block")
.comment("You should use 'trust' modifier to run commands without handling errors");
meta.add_message(message);
}
self.is_parsed = true;
Ok(())
},
Err(_) => {
// If we're in a trust context, mark as parsed
if meta.context.is_trust_ctx {
self.is_parsed = true;
}
// Otherwise, return quietly (no succeeded block found)
Ok(())
}
}
}
}

impl TranslateModule for Succeeded {
fn translate(&self, meta: &mut TranslateMetadata) -> FragmentKind {
if self.is_parsed {
let block = self.block.translate(meta);
// the condition of '$?' clears the status code thus we need to store it in a variable
let status_variable_stmt = VarStmtFragment::new("__status", Type::Num, fragments!("$?"));
let status_variable_expr = VarExprFragment::from_stmt(&status_variable_stmt);

match &block {
FragmentKind::Empty => {
status_variable_stmt.to_frag()
},
FragmentKind::Block(block) if block.statements.is_empty() => {
status_variable_stmt.to_frag()
},
_ => {
BlockFragment::new(vec![
status_variable_stmt.to_frag(),
fragments!("if [ ", status_variable_expr.to_frag(), " = 0 ]; then"),
block,
fragments!("fi"),
], false).to_frag()
}
}
} else {
FragmentKind::Empty
}
}
}
51 changes: 40 additions & 11 deletions src/modules/function/invocation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::modules::prelude::*;
use itertools::izip;
use crate::modules::command::modifier::CommandModifier;
use crate::modules::condition::failed::Failed;
use crate::modules::condition::succeeded::Succeeded;
use crate::modules::types::{Type, Typed};
use crate::modules::variable::variable_name_extensions;
use crate::modules::expression::expr::{Expr, ExprType};
Expand All @@ -22,6 +23,7 @@ pub struct FunctionInvocation {
line: usize,
col: usize,
failed: Failed,
succeeded: Succeeded,
modifier: CommandModifier,
is_failable: bool
}
Expand Down Expand Up @@ -53,6 +55,7 @@ impl SyntaxModule<ParserMetadata> for FunctionInvocation {
line: 0,
col: 0,
failed: Failed::new(),
succeeded: Succeeded::new(),
modifier: CommandModifier::new().parse_expr(),
is_failable: false
}
Expand All @@ -68,6 +71,7 @@ impl SyntaxModule<ParserMetadata> for FunctionInvocation {
}
self.name = variable(meta, variable_name_extensions())?;
self.failed.set_function_name(self.name.clone());
self.succeeded.set_function_name(self.name.clone());
// Get the arguments
token(meta, "(")?;
self.id = handle_function_reference(meta, tok.clone(), &self.name)?;
Expand Down Expand Up @@ -104,21 +108,40 @@ impl SyntaxModule<ParserMetadata> for FunctionInvocation {
let var_refs = self.args.iter().map(is_ref).collect::<Vec<bool>>();
self.refs.clone_from(&function_unit.arg_refs);
(self.kind, self.variant_id) = handle_function_parameters(meta, self.id, function_unit.clone(), &types, &var_refs, tok.clone())?;
self.failed.set_position(PositionInfo::from_between_tokens(meta, tok.clone(), meta.get_current_token()));

// Set position for both failed and succeeded handlers
let position = PositionInfo::from_between_tokens(meta, tok.clone(), meta.get_current_token());
self.failed.set_position(position.clone());
self.succeeded.set_position(position);

self.is_failable = function_unit.is_failable;
if self.is_failable {
match syntax(meta, &mut self.failed) {
Ok(_) => (),
Err(Failure::Quiet(_)) => return error!(meta, tok => {
message: "This function can fail. Please handle the failure",
comment: "You can use '?' in the end to propagate the failure"
}),
Err(err) => return Err(err)
// Try to parse succeeded block first
syntax(meta, &mut self.succeeded)?;

// If succeeded block was parsed successfully, check for conflicts
if self.succeeded.is_parsed {
// Check if there's an attempt to use failed block as well
if token(meta, "failed").is_ok() {
return error!(meta, meta.get_current_token() => {
message: "Cannot use both 'succeeded' and 'failed' blocks for the same function call",
comment: "Use either 'succeeded' or 'failed' block, but not both"
});
}
} else {
// Try to parse failed block
match syntax(meta, &mut self.failed) {
Ok(_) => (),
Err(Failure::Quiet(_)) => return error!(meta, tok => {
message: "This function can fail. Please handle the failure or success",
comment: "You can use '?' to propagate failure, 'failed' block to handle failure, or 'succeeded' block to handle success"
}),
Err(err) => return Err(err)
}
}
} else {
let tok = meta.get_current_token();
if let Ok(symbol) = token_by(meta, |word| ["?", "failed"].contains(&word.as_str())) {
if let Ok(symbol) = token_by(meta, |word| ["?", "failed", "succeeded"].contains(&word.as_str())) {
let message = Message::new_warn_at_token(meta, tok)
.message("This function cannot fail")
.comment(format!("You can remove the '{symbol}' in the end"));
Expand Down Expand Up @@ -147,8 +170,14 @@ impl TranslateModule for FunctionInvocation {
meta.stmt_queue.push_back(fragments!(name, " ", args, silent));
swap(&mut is_silent, &mut meta.silenced);
if self.is_failable {
let failed = self.failed.translate(meta);
meta.stmt_queue.push_back(failed);
// Choose between failed or succeeded handler
if self.failed.is_parsed {
let failed = self.failed.translate(meta);
meta.stmt_queue.push_back(failed);
} else if self.succeeded.is_parsed {
let succeeded = self.succeeded.translate(meta);
meta.stmt_queue.push_back(succeeded);
}
}
if self.kind != Type::Null {
let invocation_return = format!("__ret_{}{}_v{}", self.name, self.id, self.variant_id);
Expand Down
8 changes: 8 additions & 0 deletions src/tests/erroring/succeeded_failed_same_line.ab
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Output
// Cannot use both 'succeeded' and 'failed' blocks for the same command

$ echo "test" $ succeeded {
echo "Success!"
} failed {
echo "Failed!"
}
8 changes: 3 additions & 5 deletions src/tests/stdlib/text_contains.ab
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { text_contains } from "std/text"

main {
if text_contains("Hello World", "World") {
echo "Succeeded"
}
}
if text_contains("Hello World", "World") {
echo "Succeeded"
}
10 changes: 10 additions & 0 deletions src/tests/validity/function_succeeded.ab
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
fun test_failable(): Num? {
if 1 > 5 {
fail 1
}
return 42
}

test_failable() succeeded {
echo "Succeeded"
}
11 changes: 11 additions & 0 deletions src/tests/validity/function_succeeded_fail.ab
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
fun test_failable(): Num? {
Copy link
Member

Choose a reason for hiding this comment

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

@copilot Add to the end of this main block

echo "Succeeded"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added echo "Succeeded" to the end of the main block. This demonstrates that execution continues normally after a failed function call with a succeeded block - the succeeded block doesn't execute (as expected), but main continues and prints "Succeeded". f11ae6a

if 1 < 5 {
fail 1
}
return 42
}

test_failable() succeeded {
echo "This should not print!"
}
echo "Succeeded"
3 changes: 3 additions & 0 deletions src/tests/validity/succeeded.ab
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
$ printf "S" $ succeeded {
echo "ucceeded"
}