diff options
author | Robin Haberkorn <robin.haberkorn@googlemail.com> | 2025-08-31 02:24:11 +0300 |
---|---|---|
committer | Robin Haberkorn <robin.haberkorn@googlemail.com> | 2025-08-31 02:24:11 +0300 |
commit | 9425ad37ec95a40dc039169031259161c92cc217 (patch) | |
tree | 11525a5d372eba9feac20b6ff47ef91e5cbc3b47 | |
parent | e82dc6639e829490cb11267fa4a49ef97c6459ae (diff) | |
download | sciteco-9425ad37ec95a40dc039169031259161c92cc217.tar.gz |
support <:O>: if a label is not found, continue execution after the go-to statement
* this is a SciTECO extension - it's not in TECO-11
* Allows for select-case-like constructs with default-clauses as in
:Os.^EQa$
!* default *!
!s.foo!
!* ... *!
!s.bar!
!* ... *!
* Consistent with nOlabel0,label1,...$ if <n> is out of range.
Unfortunately this form of computed goto is not applicable when
"selecting" by strings or non-consecutive integers.
* In order to continue after the <:O> statement, we must keep the
program counter along with the label we were looking for.
At the end of the macro, the PC is restored instead of throwing
an error.
* Since that would be very inefficient in loops - where potentially
all iterations would result in rescanning till the end of the
macro - we now store a completed-flag in the goto table.
If it is set while trying to :O to an unknown label, we can
just continue execution.
-rw-r--r-- | TODO | 3 | ||||
-rw-r--r-- | src/core-commands.c | 2 | ||||
-rw-r--r-- | src/error.h | 9 | ||||
-rw-r--r-- | src/goto-commands.c | 34 | ||||
-rw-r--r-- | src/goto-commands.h | 3 | ||||
-rw-r--r-- | src/goto.h | 9 | ||||
-rw-r--r-- | src/parser.c | 75 | ||||
-rw-r--r-- | src/undo.c | 1 | ||||
-rw-r--r-- | src/undo.h | 3 | ||||
-rw-r--r-- | tests/testsuite.at | 1 |
10 files changed, 101 insertions, 39 deletions
@@ -729,9 +729,6 @@ Features: This would ease subscripting, e.g. to remove the last character: 0,:Qq-1Qq^Uq$ There should also be m,nA for consistency. - * :Olabel$ to avoid throwing an error if label is not defined. - This would be very useful when using computed Gotos as select-case - like constructs as the code after the Goto would act as a default-clause. * Sort order in Q-register autocompletions should be numeric, so ^A11 comes after ^A1. * FFI interface: There could be a command nFXlib$func$str$ diff --git a/src/core-commands.c b/src/core-commands.c index 5fe960b..8ce7a24 100644 --- a/src/core-commands.c +++ b/src/core-commands.c @@ -633,7 +633,7 @@ teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error) ['$'] = {&teco_state_escape}, ['!'] = {&teco_state_label}, ['O'] = {&teco_state_goto, - .modifier_at = TRUE}, + .modifier_at = TRUE, .modifier_colon = 1}, ['^'] = {&teco_state_control, .modifier_at = TRUE, .modifier_colon = 2}, ['F'] = {&teco_state_fcommand, diff --git a/src/error.h b/src/error.h index b4f92ad..2a12733 100644 --- a/src/error.h +++ b/src/error.h @@ -42,6 +42,7 @@ typedef enum { TECO_ERROR_SYNTAX, TECO_ERROR_MODIFIER, TECO_ERROR_ARGEXPECTED, + TECO_ERROR_LABEL, TECO_ERROR_CODEPOINT, TECO_ERROR_MOVE, TECO_ERROR_WORDS, @@ -92,6 +93,14 @@ teco_error_argexpected_set(GError **error, const gchar *cmd) } static inline void +teco_error_label_set(GError **error, const gchar *name, gsize len) +{ + g_autofree gchar *label_printable = teco_string_echo(name, len); + g_set_error(error, TECO_ERROR, TECO_ERROR_LABEL, + "Label \"%s\" not found", label_printable); +} + +static inline void teco_error_codepoint_set(GError **error, const gchar *cmd) { g_set_error(error, TECO_ERROR, TECO_ERROR_CODEPOINT, diff --git a/src/goto-commands.c b/src/goto-commands.c index ac98b70..73575ca 100644 --- a/src/goto-commands.c +++ b/src/goto-commands.c @@ -38,7 +38,17 @@ TECO_DECLARE_STATE(teco_state_blockcomment); TECO_DECLARE_STATE(teco_state_eolcomment); +/** + * In TECO_MODE_PARSE_ONLY_GOTO mode, we remain in parse-only mode + * until the given label is encountered. + */ teco_string_t teco_goto_skip_label = {NULL, 0}; +/** + * The program counter to restore if the teco_goto_skip_label + * is \b not found (after :Olabel$). + * If smaller than 0 an error is thrown instead. + */ +gssize teco_goto_backup_pc = -1; /* * NOTE: The comma is theoretically not allowed in a label @@ -68,6 +78,7 @@ teco_state_label_input(teco_machine_main_t *ctx, gunichar chr, GError **error) !teco_string_cmp(&ctx->goto_label, teco_goto_skip_label.data, teco_goto_skip_label.len)) { teco_undo_string_own(teco_goto_skip_label); memset(&teco_goto_skip_label, 0, sizeof(teco_goto_skip_label)); + teco_undo_gssize(teco_goto_backup_pc) = -1; if (ctx->parent.must_undo) teco_undo_flags(ctx->flags); @@ -122,6 +133,8 @@ teco_state_goto_done(teco_machine_main_t *ctx, const teco_string_t *str, GError if (!teco_expressions_pop_num_calc(&value, 0, error)) return NULL; + gboolean colon_modified = teco_machine_main_eval_colon(ctx) > 0; + /* * Find the comma-separated substring in str indexed by `value`. */ @@ -142,14 +155,19 @@ teco_state_goto_done(teco_machine_main_t *ctx, const teco_string_t *str, GError if (pc >= 0) { ctx->macro_pc = pc; - } else { + } else if (!ctx->goto_table.complete) { /* skip till label is defined */ g_assert(teco_goto_skip_label.len == 0); undo__teco_string_truncate(&teco_goto_skip_label, 0); teco_string_init(&teco_goto_skip_label, label.data, label.len); + teco_undo_gssize(teco_goto_backup_pc) = colon_modified ? ctx->macro_pc : -1; if (ctx->parent.must_undo) teco_undo_flags(ctx->flags); ctx->flags.mode = TECO_MODE_PARSE_ONLY_GOTO; + } else if (!colon_modified) { + /* can happen if we previously executed a colon-modified go-to */ + teco_error_label_set(error, teco_goto_skip_label.data, teco_goto_skip_label.len); + return NULL; } } @@ -164,6 +182,7 @@ gboolean teco_state_goto_insert_completion(teco_machine_main_t *ctx, const teco_ /*$ "O" goto * Olabel$ -- Go to label + * :Olabel$ * [n]Olabel0[,label1,...]$ * * Go to <label>. @@ -186,14 +205,19 @@ gboolean teco_state_goto_insert_completion(teco_machine_main_t *ctx, const teco_ * Otherwise, parsing continues until the <label> * is defined. * The command will yield an error if a label has - * not been defined when the macro or command-line - * is terminated. - * In the latter case, the user will not be able to - * terminate the command-line. + * not been defined when the macro is terminated. + * When jumping to a non-existent <label> in the + * command-line macro, you cannot practically terminate + * the command-line until defining the <label>. * * String building constructs are enabled in \fBO\fP * which allows for a second kind of computed go-to, * where the label name contains the value to select. + * When colon-modifying the \fBO\fP command, execution + * will continue after the command if the given <label> + * isn't found. + * This is useful to handle the \(lqdefault\(rq case + * when using computed go-tos of the second kind. */ TECO_DEFINE_STATE_EXPECTSTRING(teco_state_goto, .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_goto_process_edit_cmd, diff --git a/src/goto-commands.h b/src/goto-commands.h index f4f52d5..45c68e9 100644 --- a/src/goto-commands.h +++ b/src/goto-commands.h @@ -16,10 +16,13 @@ */ #pragma once +#include <glib.h> + #include "parser.h" #include "string-utils.h" extern teco_string_t teco_goto_skip_label; +extern gssize teco_goto_backup_pc; TECO_DECLARE_STATE(teco_state_label); TECO_DECLARE_STATE(teco_state_goto); @@ -26,10 +26,14 @@ typedef struct { teco_rb3str_tree_t tree; + /** Whether to generate undo tokens (unnecessary in macro invocations) */ + guint must_undo : 1; + /** - * Whether to generate undo tokens (unnecessary in macro invocations) + * Whether the table is guaranteed to be complete because the entire + * macro has already been parsed. */ - gboolean must_undo; + guint complete : 1; } teco_goto_table_t; /** @memberof teco_goto_table_t */ @@ -38,6 +42,7 @@ teco_goto_table_init(teco_goto_table_t *ctx, gboolean must_undo) { rb3_reset_tree(&ctx->tree); ctx->must_undo = must_undo; + ctx->complete = FALSE; } gboolean teco_goto_table_remove(teco_goto_table_t *ctx, const gchar *name, gsize len); diff --git a/src/parser.c b/src/parser.c index 6d4cd60..a5e6e4f 100644 --- a/src/parser.c +++ b/src/parser.c @@ -186,41 +186,60 @@ teco_execute_macro(const gchar *macro, gsize macro_len, GError *tmp_error = NULL; - if (!teco_machine_main_step(¯o_machine, macro, macro_len, &tmp_error)) { - if (!g_error_matches(tmp_error, TECO_ERROR, TECO_ERROR_RETURN)) { - /* passes ownership of tmp_error */ - g_propagate_error(error, tmp_error); - goto error_cleanup; + for (;;) { + if (!teco_machine_main_step(¯o_machine, macro, macro_len, &tmp_error)) { + if (!g_error_matches(tmp_error, TECO_ERROR, TECO_ERROR_RETURN)) { + /* passes ownership of tmp_error */ + g_propagate_error(error, tmp_error); + goto error_cleanup; + } + g_error_free(tmp_error); + + /* + * Macro returned - handle like regular + * end of macro, even though some checks + * are unnecessary here. + * macro_pc will still point to the return PC. + */ + g_assert(macro_machine.parent.current == &teco_state_start); + + /* + * Discard all braces, except the current one. + */ + if (!teco_expressions_brace_return(parent_brace_level, teco_error_return_args, error)) + goto error_cleanup; + + /* + * Clean up the loop stack. + * We are allowed to return in loops. + * NOTE: This does not have to be undone. + */ + g_array_remove_range(teco_loop_stack, macro_machine.loop_stack_fp, + teco_loop_stack->len - macro_machine.loop_stack_fp); } - g_error_free(tmp_error); - /* - * Macro returned - handle like regular - * end of macro, even though some checks - * are unnecessary here. - * macro_pc will still point to the return PC. - */ - g_assert(macro_machine.parent.current == &teco_state_start); + if (G_LIKELY(teco_goto_backup_pc < 0)) + break; - /* - * Discard all braces, except the current one. - */ - if (!teco_expressions_brace_return(parent_brace_level, teco_error_return_args, error)) - goto error_cleanup; + /* continue after :Olabel$ */ + macro_machine.macro_pc = teco_goto_backup_pc; + /* macro could have ended in a "lookahead" state */ + macro_machine.parent.current = &teco_state_start; - /* - * Clean up the loop stack. - * We are allowed to return in loops. - * NOTE: This does not have to be undone. - */ - g_array_remove_range(teco_loop_stack, macro_machine.loop_stack_fp, - teco_loop_stack->len - macro_machine.loop_stack_fp); + teco_undo_string_own(teco_goto_skip_label); + memset(&teco_goto_skip_label, 0, sizeof(teco_goto_skip_label)); + teco_undo_gssize(teco_goto_backup_pc) = -1; + + if (macro_machine.parent.must_undo) + teco_undo_flags(macro_machine.flags); + macro_machine.flags.mode = TECO_MODE_NORMAL; + + /* no need to reparse everything in the future */ + macro_machine.goto_table.complete = TRUE; } if (G_UNLIKELY(teco_goto_skip_label.len > 0)) { - g_autofree gchar *label_printable = teco_string_echo(teco_goto_skip_label.data, teco_goto_skip_label.len); - g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, - "Label \"%s\" not found", label_printable); + teco_error_label_set(error, teco_goto_skip_label.data, teco_goto_skip_label.len); goto error_attach; } @@ -34,6 +34,7 @@ TECO_DEFINE_UNDO_SCALAR(gunichar); TECO_DEFINE_UNDO_SCALAR(gint); TECO_DEFINE_UNDO_SCALAR(guint); TECO_DEFINE_UNDO_SCALAR(gsize); +TECO_DEFINE_UNDO_SCALAR(gssize); TECO_DEFINE_UNDO_SCALAR(teco_int_t); TECO_DEFINE_UNDO_SCALAR(gboolean); TECO_DEFINE_UNDO_SCALAR(gconstpointer); @@ -182,6 +182,9 @@ TECO_DECLARE_UNDO_SCALAR(guint); TECO_DECLARE_UNDO_SCALAR(gsize); #define teco_undo_gsize(VAR) (*teco_undo_object_gsize_push(&(VAR))) +TECO_DECLARE_UNDO_SCALAR(gssize); +#define teco_undo_gssize(VAR) (*teco_undo_object_gssize_push(&(VAR))) + TECO_DECLARE_UNDO_SCALAR(teco_int_t); #define teco_undo_int(VAR) (*teco_undo_object_teco_int_t_push(&(VAR))) diff --git a/tests/testsuite.at b/tests/testsuite.at index 3cca113..0079c38 100644 --- a/tests/testsuite.at +++ b/tests/testsuite.at @@ -117,6 +117,7 @@ AT_CLEANUP AT_SETUP([Gotos and labels]) TE_CHECK([[@O//]], 1, ignore, ignore) TE_CHECK([[^^XUq @O/s.^EUq/ (0/0) !s.X!]], 0, ignore, ignore) +TE_CHECK([[:@O/foo/]], 0, ignore, ignore) TE_CHECK([[1@O/foo,bar/ (0/0) !bar!]], 0, ignore, ignore) # No-op gotos TE_CHECK([[-1@O/foo/ 1@O/foo/ @O/,foo/]], 0, ignore, ignore) |