/*
* Copyright (C) 2012-2024 Robin Haberkorn
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include
#include
#include
#include
#include "sciteco.h"
#include "memory.h"
#include "string-utils.h"
#include "interface.h"
#include "undo.h"
#include "expressions.h"
#include "qreg.h"
#include "ring.h"
#include "glob.h"
#include "error.h"
#include "core-commands.h"
#include "goto-commands.h"
#include "parser.h"
//#define DEBUG
GArray *teco_loop_stack;
static void __attribute__((constructor))
teco_loop_stack_init(void)
{
teco_loop_stack = g_array_sized_new(FALSE, FALSE, sizeof(teco_loop_context_t), 1024);
}
TECO_DEFINE_ARRAY_UNDO_INSERT_VAL(teco_loop_stack, teco_loop_context_t);
TECO_DEFINE_ARRAY_UNDO_REMOVE_INDEX(teco_loop_stack);
static void TECO_DEBUG_CLEANUP
teco_loop_stack_cleanup(void)
{
g_array_free(teco_loop_stack, TRUE);
}
gboolean
teco_machine_input(teco_machine_t *ctx, gunichar chr, GError **error)
{
teco_state_t *next = ctx->current->input_cb(ctx, chr, error);
if (!next)
return FALSE;
if (next != ctx->current) {
if (ctx->must_undo)
teco_undo_ptr(ctx->current);
ctx->current = next;
if (ctx->current->initial_cb && !ctx->current->initial_cb(ctx, error))
return FALSE;
}
return TRUE;
}
gboolean
teco_state_end_of_macro(teco_machine_t *ctx, GError **error)
{
g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
"Unterminated command");
return FALSE;
}
/**
* Execute macro from current PC to stop position.
*
* Handles all expected exceptions and preparing them for stack frame insertion.
*
* @param ctx State machine.
* @param macro The macro to execute.
* It does not have to be complete.
* It must consist only of validated UTF-8 sequences, though.
* @param stop_pos Where to stop execution in bytes.
* @param error Location to store error.
* @return FALSE if an error occurred.
*/
gboolean
teco_machine_main_step(teco_machine_main_t *ctx, const gchar *macro, gsize stop_pos, GError **error)
{
while (ctx->macro_pc < stop_pos) {
if (G_UNLIKELY(teco_interface_is_interrupted())) {
teco_error_interrupted_set(error);
goto error_attach;
}
/*
* Most allocations are small or of limited size,
* so it is (almost) sufficient to check the memory limit regularily.
*/
if (!teco_memory_check(0, error))
goto error_attach;
/* UTF-8 sequences are already validated */
gunichar chr = g_utf8_get_char(macro+ctx->macro_pc);
#ifdef DEBUG
g_printf("EXEC(%d): input='%C' (U+%04" G_GINT32_MODIFIER "X), state=%p, mode=%d\n",
ctx->macro_pc, chr, chr, ctx->parent.current, ctx->mode);
#endif
if (!teco_machine_input(&ctx->parent, chr, error))
goto error_attach;
ctx->macro_pc = g_utf8_next_char(macro+ctx->macro_pc) - macro;
}
/*
* Provide interactive feedback when the
* PC is at the end of the command line.
* This will actually be called in other situations,
* like at the end of macros but that does not hurt.
* It should perhaps be in teco_cmdline_insert(),
* but doing it here ensures that exceptions get
* normalized.
*/
if (ctx->parent.current->refresh_cb &&
!ctx->parent.current->refresh_cb(&ctx->parent, error))
goto error_attach;
return TRUE;
error_attach:
g_assert(!error || *error != NULL);
/*
* FIXME: Maybe this can be avoided altogether by passing in ctx->macro_pc
* from the callees?
*/
teco_error_set_coord(macro, ctx->macro_pc);
return FALSE;
}
gboolean
teco_execute_macro(const gchar *macro, gsize macro_len,
teco_qreg_table_t *qreg_table_locals, GError **error)
{
const teco_string_t str = {(gchar *)macro, macro_len};
if (!teco_string_validate_utf8(&str)) {
g_set_error_literal(error, TECO_ERROR, TECO_ERROR_CODEPOINT,
"Invalid UTF-8 byte sequence in macro");
return FALSE;
}
/*
* This is not auto-cleaned up, so it can be initialized
* on demand.
*/
teco_qreg_table_t macro_locals;
if (!qreg_table_locals)
teco_qreg_table_init(¯o_locals, FALSE);
guint parent_brace_level = teco_brace_level;
g_auto(teco_machine_main_t) macro_machine;
teco_machine_main_init(¯o_machine, qreg_table_locals ? : ¯o_locals, FALSE);
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;
}
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);
}
if (G_UNLIKELY(teco_loop_stack->len > macro_machine.loop_stack_fp)) {
const teco_loop_context_t *ctx = &g_array_index(teco_loop_stack, teco_loop_context_t, teco_loop_stack->len-1);
teco_error_set_coord(macro, ctx->pc);
g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
"Unterminated loop");
goto error_cleanup;
}
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);
goto error_attach;
}
/*
* Some states (esp. commands involving a
* "lookahead") are valid at the end of a macro.
*/
if (macro_machine.parent.current->end_of_macro_cb &&
!macro_machine.parent.current->end_of_macro_cb(¯o_machine.parent, error))
goto error_attach;
/*
* This handles the problem of Q-Registers
* local to the macro invocation being edited
* when the macro terminates without additional
* complexity.
* teco_qreg_table_empty() might leave the table
* half-empty, but it will eventually be completely
* cleared by teco_qreg_table_clear().
* This does not hurt since an error will rub out the
* macro invocation itself and macro_locals don't have
* to be preserved.
*/
if (!qreg_table_locals && !teco_qreg_table_empty(¯o_locals, error))
goto error_attach;
return TRUE;
error_attach:
teco_error_set_coord(macro, macro_machine.macro_pc);
/* fall through */
error_cleanup:
if (!qreg_table_locals)
teco_qreg_table_clear(¯o_locals);
/* make sure teco_goto_skip_label will be NULL even in batch mode */
teco_string_truncate(&teco_goto_skip_label, 0);
return FALSE;
}
gboolean
teco_execute_file(const gchar *filename, teco_qreg_table_t *qreg_table_locals, GError **error)
{
g_auto(teco_string_t) macro = {NULL, 0};
if (!g_file_get_contents(filename, ¯o.data, ¯o.len, error))
return FALSE;
gchar *p;
/* only when executing files, ignore Hash-Bang line */
if (*macro.data == '#') {
/*
* NOTE: We assume that a file starting with Hash does not contain
* a null-byte in its first line.
*/
p = strpbrk(macro.data, "\r\n");
if (G_UNLIKELY(!p))
/* empty script */
return TRUE;
p++;
} else {
p = macro.data;
}
if (!teco_execute_macro(p, macro.len - (p - macro.data),
qreg_table_locals, error)) {
/* correct error position for Hash-Bang line */
teco_error_pos += p - macro.data;
if (*macro.data == '#')
teco_error_line++;
teco_error_add_frame_file(filename);
return FALSE;
}
return TRUE;
}
void
teco_machine_main_init(teco_machine_main_t *ctx, teco_qreg_table_t *qreg_table_locals,
gboolean must_undo)
{
memset(ctx, 0, sizeof(*ctx));
teco_machine_init(&ctx->parent, &teco_state_start, must_undo);
ctx->loop_stack_fp = teco_loop_stack->len;
teco_goto_table_init(&ctx->goto_table, must_undo);
ctx->qreg_table_locals = qreg_table_locals;
ctx->expectstring.nesting = 1;
teco_machine_stringbuilding_init(&ctx->expectstring.machine, '\e', qreg_table_locals, must_undo);
}
gboolean
teco_machine_main_eval_colon(teco_machine_main_t *ctx)
{
if (!ctx->modifier_colon)
return FALSE;
if (ctx->parent.must_undo)
teco_undo_guint(ctx->__flags);
ctx->modifier_colon = FALSE;
return TRUE;
}
teco_state_t *
teco_machine_main_transition_input(teco_machine_main_t *ctx,
teco_machine_main_transition_t *transitions,
guint len, gunichar chr, GError **error)
{
if (chr >= len || !transitions[chr].next) {
teco_error_syntax_set(error, chr);
return NULL;
}
if (ctx->mode == TECO_MODE_NORMAL && transitions[chr].transition_cb) {
/*
* NOTE: We could also just let transition_cb return a boolean...
*/
GError *tmp_error = NULL;
transitions[chr].transition_cb(ctx, &tmp_error);
if (tmp_error) {
g_propagate_error(error, tmp_error);
return NULL;
}
}
return transitions[chr].next;
}
void
teco_machine_main_clear(teco_machine_main_t *ctx)
{
teco_goto_table_clear(&ctx->goto_table);
teco_machine_stringbuilding_clear(&ctx->expectstring.machine);
}
/*
* FIXME: All teco_state_stringbuilding_* states could be static?
*/
static teco_state_t *teco_state_stringbuilding_ctl_input(teco_machine_stringbuilding_t *ctx,
gunichar chr, GError **error);
TECO_DECLARE_STATE(teco_state_stringbuilding_ctl);
static teco_state_t *teco_state_stringbuilding_escaped_input(teco_machine_stringbuilding_t *ctx,
gunichar chr, GError **error);
TECO_DECLARE_STATE(teco_state_stringbuilding_escaped);
TECO_DECLARE_STATE(teco_state_stringbuilding_lower);
TECO_DECLARE_STATE(teco_state_stringbuilding_upper);
TECO_DECLARE_STATE(teco_state_stringbuilding_ctle);
TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_num);
TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_u);
TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_q);
TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_quote);
TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_n);
static teco_state_t *
teco_state_stringbuilding_start_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error)
{
if (chr == '^')
return &teco_state_stringbuilding_ctl;
if (TECO_IS_CTL(chr))
return teco_state_stringbuilding_ctl_input(ctx, TECO_CTL_ECHO(chr), error);
return teco_state_stringbuilding_escaped_input(ctx, chr, error);
}
/* in cmdline.c */
gboolean teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t *ctx, teco_machine_t *parent_ctx,
gunichar key, GError **error);
TECO_DEFINE_STATE(teco_state_stringbuilding_start,
.is_start = TRUE,
.process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)
teco_state_stringbuilding_start_process_edit_cmd
);
static teco_state_t *
teco_state_stringbuilding_ctl_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error)
{
chr = teco_ascii_toupper(chr);
switch (chr) {
case '^': break;
case 'Q':
case 'R': return &teco_state_stringbuilding_escaped;
case 'V': return &teco_state_stringbuilding_lower;
case 'W': return &teco_state_stringbuilding_upper;
case 'E': return &teco_state_stringbuilding_ctle;
default:
chr = TECO_CTL_KEY(chr);
}
/*
* Source code is always in UTF-8, so it does not
* make sense to handle ctx->codepage != SC_CP_UTF8
* separately.
*/
if (ctx->result)
teco_string_append_wc(ctx->result, chr);
return &teco_state_stringbuilding_start;
}
TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_ctl);
static teco_state_t *
teco_state_stringbuilding_escaped_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error)
{
if (!ctx->result)
/* parse-only mode */
return &teco_state_stringbuilding_start;
/*
* The subtle difference between UTF-8 and single-byte targets
* is that we don't try to casefold non-ANSI characters in single-byte mode.
*/
switch (ctx->mode) {
case TECO_STRINGBUILDING_MODE_UPPER:
chr = ctx->codepage == SC_CP_UTF8 || chr < 0x80
? g_unichar_toupper(chr) : chr;
break;
case TECO_STRINGBUILDING_MODE_LOWER:
chr = ctx->codepage == SC_CP_UTF8 || chr < 0x80
? g_unichar_tolower(chr) : chr;
break;
default:
break;
}
teco_string_append_wc(ctx->result, chr);
return &teco_state_stringbuilding_start;
}
TECO_DEFINE_STATE(teco_state_stringbuilding_escaped);
static teco_state_t *
teco_state_stringbuilding_lower_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error)
{
if (!ctx->result)
/* parse-only mode */
return &teco_state_stringbuilding_start;
/*
* FIXME: This does not handle ^V^V typed with up-carets.
*/
if (chr == TECO_CTL_KEY('V')) {
if (ctx->parent.must_undo)
teco_undo_guint(ctx->mode);
ctx->mode = TECO_STRINGBUILDING_MODE_LOWER;
} else {
chr = ctx->codepage == SC_CP_UTF8 || chr < 0x80
? g_unichar_tolower(chr) : chr;
teco_string_append_wc(ctx->result, chr);
}
return &teco_state_stringbuilding_start;
}
TECO_DEFINE_STATE(teco_state_stringbuilding_lower);
static teco_state_t *
teco_state_stringbuilding_upper_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error)
{
if (!ctx->result)
/* parse-only mode */
return &teco_state_stringbuilding_start;
/*
* FIXME: This does not handle ^W^W typed with up-carets.
*/
if (chr == TECO_CTL_KEY('W')) {
if (ctx->parent.must_undo)
teco_undo_guint(ctx->mode);
ctx->mode = TECO_STRINGBUILDING_MODE_UPPER;
} else {
chr = ctx->codepage == SC_CP_UTF8 || chr < 0x80
? g_unichar_toupper(chr) : chr;
teco_string_append_wc(ctx->result, chr);
}
return &teco_state_stringbuilding_start;
}
TECO_DEFINE_STATE(teco_state_stringbuilding_upper);
static teco_state_t *
teco_state_stringbuilding_ctle_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error)
{
teco_state_t *next;
switch (teco_ascii_toupper(chr)) {
case '\\': next = &teco_state_stringbuilding_ctle_num; break;
case 'U': next = &teco_state_stringbuilding_ctle_u; break;
case 'Q': next = &teco_state_stringbuilding_ctle_q; break;
case '@': next = &teco_state_stringbuilding_ctle_quote; break;
case 'N': next = &teco_state_stringbuilding_ctle_n; break;
default:
if (ctx->result) {
gchar buf[1+6] = {TECO_CTL_KEY('E')};
gsize len = g_unichar_to_utf8(chr, buf+1);
teco_string_append(ctx->result, buf, 1+len);
}
return &teco_state_stringbuilding_start;
}
if (ctx->machine_qregspec)
teco_machine_qregspec_reset(ctx->machine_qregspec);
else
ctx->machine_qregspec = teco_machine_qregspec_new(TECO_QREG_REQUIRED,
ctx->qreg_table_locals,
ctx->parent.must_undo);
return next;
}
TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_ctle);
/* in cmdline.c */
gboolean teco_state_stringbuilding_qreg_process_edit_cmd(teco_machine_stringbuilding_t *ctx, teco_machine_t *parent_ctx,
gunichar chr, GError **error);
/**
* @interface TECO_DEFINE_STATE_STRINGBUILDING_QREG
* @implements TECO_DEFINE_STATE
* @ingroup states
*/
#define TECO_DEFINE_STATE_STRINGBUILDING_QREG(NAME, ...) \
TECO_DEFINE_STATE(NAME, \
.process_edit_cmd_cb = (teco_statHTTP/1.1 200 OK
Connection: keep-alive
Connection: keep-alive
Content-Disposition: inline; filename="parser.c"
Content-Disposition: inline; filename="parser.c"
Content-Length: 30202
Content-Length: 30202
Content-Security-Policy: default-src 'none'
Content-Security-Policy: default-src 'none'
Content-Type: text/plain; charset=UTF-8
Content-Type: text/plain; charset=UTF-8
Date: Wed, 22 Oct 2025 21:46:36 UTC
ETag: "45e31cf796f81f84c1e3342ed22a164d64327cc7"
ETag: "45e31cf796f81f84c1e3342ed22a164d64327cc7"
Expires: Sat, 20 Oct 2035 21:46:36 GMT
Expires: Sat, 20 Oct 2035 21:46:36 GMT
Last-Modified: Wed, 22 Oct 2025 21:46:36 GMT
Last-Modified: Wed, 22 Oct 2025 21:46:36 GMT
Server: OpenBSD httpd
Server: OpenBSD httpd
X-Content-Type-Options: nosniff
X-Content-Type-Options: nosniff
/*
* Copyright (C) 2012-2024 Robin Haberkorn
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include
#include
#include
#include
#include "sciteco.h"
#include "memory.h"
#include "string-utils.h"
#include "interface.h"
#include "undo.h"
#include "expressions.h"
#include "qreg.h"
#include "ring.h"
#include "glob.h"
#include "error.h"
#include "core-commands.h"
#include "goto-commands.h"
#include "parser.h"
//#define DEBUG
GArray *teco_loop_stack;
static void __attribute__((constructor))
teco_loop_stack_init(void)
{
teco_loop_stack = g_array_sized_new(FALSE, FALSE, sizeof(teco_loop_context_t), 1024);
}
TECO_DEFINE_ARRAY_UNDO_INSERT_VAL(teco_loop_stack, teco_loop_context_t);
TECO_DEFINE_ARRAY_UNDO_REMOVE_INDEX(teco_loop_stack);
static void TECO_DEBUG_CLEANUP
teco_loop_stack_cleanup(void)
{
g_array_free(teco_loop_stack, TRUE);
}
gboolean
teco_machine_input(teco_machine_t *ctx, gunichar chr, GError **error)
{
teco_state_t *next = ctx->current->input_cb(ctx, chr, error);
if (!next)
return FALSE;
if (next != ctx->current) {
if (ctx->must_undo)
teco_undo_ptr(ctx->current);
ctx->current = next;
if (ctx->current->initial_cb && !ctx->current->initial_cb(ctx, error))
return FALSE;
}
return TRUE;
}
gboolean
teco_state_end_of_macro(teco_machine_t *ctx, GError **error)
{
g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
"Unterminated command");
return FALSE;
}
/**
* Execute macro from current PC to stop position.
*
* Handles all expected exceptions and preparing them for stack frame insertion.
*
* @param ctx State machine.
* @param macro The macro to execute.
* It does not have to be complete.
* It must consist only of validated UTF-8 sequences, though.
* @param stop_pos Where to stop execution in bytes.
* @param error Location to store error.
* @return FALSE if an error occurred.
*/
gboolean
teco_machine_main_step(teco_machine_main_t *ctx, const gchar *macro, gsize stop_pos, GError **error)
{
while (ctx->macro_pc < stop_pos) {
if (G_UNLIKELY(teco_interface_is_interrupted())) {
teco_error_interrupted_set(error);
goto error_attach;
}
/*
* Most allocations are small or of limited size,
* so it is (almost) sufficient to check the memory limit regularily.
*/
if (!teco_memory_check(0, error))
goto error_attach;
/* UTF-8 sequences are already validated */
gunichar chr = g_utf8_get_char(macro+ctx->macro_pc);
#ifdef DEBUG
g_printf("EXEC(%d): input='%C' (U+%04" G_GINT32_MODIFIER "X), state=%p, mode=%d\n",
ctx->macro_pc, chr, chr, ctx->parent.current, ctx->mode);
#endif
if (!teco_machine_input(&ctx->parent, chr, error))
goto error_attach;
ctx->macro_pc = g_utf8_next_char(macro+ctx->macro_pc) - macro;
}
/*
* Provide interactive feedback when the
* PC is at the end of the command line.
* This will actually be called in other situations,
* like at the end of macros but that does not hurt.
* It should perhaps be in teco_cmdline_insert(),
* but doing it here ensures that exceptions get
* normalized.
*/
if (ctx->parent.current->refresh_cb &&
!ctx->parent.current->refresh_cb(&ctx->parent, error))
goto error_attach;
return TRUE;
error_attach:
g_assert(!error || *error != NULL);
/*
* FIXME: Maybe this can be avoided altogether by passing in ctx->macro_pc
* from the callees?
*/
teco_error_set_coord(macro, ctx->macro_pc);
return FALSE;
}
gboolean
teco_execute_macro(const gchar *macro, gsize macro_len,
teco_qreg_table_t *qreg_table_locals, GError **error)
{
const teco_string_t str = {(gchar *)macro, macro_len};
if (!teco_string_validate_utf8(&str)) {
g_set_error_literal(error, TECO_ERROR, TECO_ERROR_CODEPOINT,
"Invalid UTF-8 byte sequence in macro");
return FALSE;
}
/*
* This is not auto-cleaned up, so it can be initialized
* on demand.
*/
teco_qreg_table_t macro_locals;
if (!qreg_table_locals)
teco_qreg_table_init(¯o_locals, FALSE);
guint parent_brace_level = teco_brace_level;
g_auto(teco_machine_main_t) macro_machine;
teco_machine_main_init(¯o_machine, qreg_table_locals ? : ¯o_locals, FALSE);
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;
}
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);
}
if (G_UNLIKELY(teco_loop_stack->len > macro_machine.loop_stack_fp)) {
const teco_loop_context_t *ctx = &g_array_index(teco_loop_stack, teco_loop_context_t, teco_loop_stack->len-1);
teco_error_set_coord(macro, ctx->pc);
g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
"Unterminated loop");
goto error_cleanup;
}
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);
goto error_attach;
}
/*
* Some states (esp. commands involving a
* "lookahead") are valid at the end of a macro.
*/
if (macro_machine.parent.current->end_of_macro_cb &&
!macro_machine.parent.current->end_of_macro_cb(¯o_machine.parent, error))
goto error_attach;
/*
* This handles the problem of Q-Registers
* local to the macro invocation being edited
* when the macro terminates without additional
* complexity.
* teco_qreg_table_empty() might leave the table
* half-empty, but it will eventually be completely
* cleared by teco_qreg_table_clear().
* This does not hurt since an error will rub out the
* macro invocation itself and macro_locals don't have
* to be preserved.
*/
if (!qreg_table_locals && !teco_qreg_table_empty(¯o_locals, error))
goto error_attach;
return TRUE;
error_attach:
teco_error_set_coord(macro, macro_machine.macro_pc);
/* fall through */
error_cleanup:
if (!qreg_table_locals)
teco_qreg_table_clear(¯o_locals);
/* make sure teco_goto_skip_label will be NULL even in batch mode */
teco_string_truncate(&teco_goto_skip_label, 0);
return FALSE;
}
gboolean
teco_execute_file(const gchar *filename, teco_qreg_table_t *qreg_table_locals, GError **error)
{
g_auto(teco_string_t) macro = {NULL, 0};
if (!g_file_get_contents(filename, ¯o.data, ¯o.len, error))
return FALSE;
gchar *p;
/* only when executing files, ignore Hash-Bang line */
if (*macro.data == '#') {
/*
* NOTE: We assume that a file starting with Hash does not contain
* a null-byte in its first line.
*/
p = strpbrk(macro.data, "\r\n");
if (G_UNLIKELY(!p))
/* empty script */
return TRUE;
p++;
} else {
p = macro.data;
}
if (!teco_execute_macro(p, macro.len - (p - macro.data),
qreg_table_locals, error)) {
/* correct error position for Hash-Bang line */
teco_error_pos += p - macro.data;
if (*macro.data == '#')
teco_error_line++;
teco_error_add_frame_file(filename);
return FALSE;
}
return TRUE;
}
void
teco_machine_main_init(teco_machine_main_t *ctx, teco_qreg_table_t *qreg_table_locals,
gboolean must_undo)
{
memset(ctx, 0, sizeof(*ctx));
teco_machine_init(&ctx->parent, &teco_state_start, must_undo);
ctx->loop_stack_fp = teco_loop_stack->len;
teco_goto_table_init(&ctx->goto_table, must_undo);
ctx->qreg_table_locals = qreg_table_locals;
ctx->expectstring.nesting = 1;
teco_machine_stringbuilding_init(&ctx->expectstring.machine, '\e', qreg_table_locals, must_undo);
}
gboolean
teco_machine_main_eval_colon(teco_machine_main_t *ctx)
{
if (!ctx->modifier_colon)
return FALSE;
if (ctx->parent.must_undo)
teco_undo_guint(ctx->__flags);
ctx->modifier_colon = FALSE;
return TRUE;
}
teco_state_t *
teco_machine_main_transition_input(teco_machine_main_t *ctx,
teco_machine_main_transition_t *transitions,
guint len, gunichar chr, GError **error)
{
if (chr >= len || !transitions[chr].next) {
teco_error_syntax_set(error, chr);
return NULL;
}
if (ctx->mode == TECO_MODE_NORMAL && transitions[chr].transition_cb) {
/*
* NOTE: We could also just let transition_cb return a boolean...
*/
GError *tmp_error = NULL;
transitions[chr].transition_cb(ctx, &tmp_error);
if (tmp_error) {
g_propagate_error(error, tmp_error);
return NULL;
}
}
return transitions[chr].next;
}
void
teco_machine_main_clear(teco_machine_main_t *ctx)
{
teco_goto_table_clear(&ctx->goto_table);
teco_machine_stringbuilding_clear(&ctx->expectstring.machine);
}
/*
* FIXME: All teco_state_stringbuilding_* states could be static?
*/
static teco_state_t *teco_state_stringbuilding_ctl_input(teco_machine_stringbuilding_t *ctx,
gunichar chr, GError **error);
TECO_DECLARE_STATE(teco_state_stringbuilding_ctl);
static teco_state_t *teco_state_stringbuilding_escaped_input(teco_machine_stringbuilding_t *ctx,
gunichar chr, GError **error);
TECO_DECLARE_STATE(teco_state_stringbuilding_escaped);
TECO_DECLARE_STATE(teco_state_stringbuilding_lower);
TECO_DECLARE_STATE(teco_state_stringbuilding_upper);
TECO_DECLARE_STATE(teco_state_stringbuilding_ctle);
TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_num);
TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_u);
TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_q);
TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_quote);
TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_n);
static teco_state_t *
teco_state_stringbuilding_start_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error)
{
if (chr == '^')
return &teco_state_stringbuilding_ctl;
if (TECO_IS_CTL(chr))
return teco_state_stringbuilding_ctl_input(ctx, TECO_CTL_ECHO(chr), error);
return teco_state_stringbuilding_escaped_input(ctx, chr, error);
}
/* in cmdline.c */
gboolean teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t *ctx, teco_machine_t *parent_ctx,
gunichar key, GError **error);
TECO_DEFINE_STATE(teco_state_stringbuilding_start,
.is_start = TRUE,
.process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)
teco_state_stringbuilding_start_process_edit_cmd
);
static teco_state_t *
teco_state_stringbuilding_ctl_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error)
{
chr = teco_ascii_toupper(chr);
switch (chr) {
case '^': break;
case 'Q':
case 'R': return &teco_state_stringbuilding_escaped;
case 'V': return &teco_state_stringbuilding_lower;
case 'W': return &teco_state_stringbuilding_upper;
case 'E': return &teco_state_stringbuilding_ctle;
default:
chr = TECO_CTL_KEY(chr);
}
/*
* Source code is always in UTF-8, so it does not
* make sense to handle ctx->codepage != SC_CP_UTF8
* separately.
*/
if (ctx->result)
teco_string_append_wc(ctx->result, chr);
return &teco_state_stringbuilding_start;
}
TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_ctl);
static teco_state_t *
teco_state_stringbuilding_escaped_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error)
{
if (!ctx->result)
/* parse-only mode */
return &teco_state_stringbuilding_start;
/*
* The subtle difference between UTF-8 and single-byte targets
* is that we don't try to casefold non-ANSI characters in single