Skip to content

Commit

Permalink
Script REPL (#569)
Browse files Browse the repository at this point in the history
* repl: Add dummy reply app

* cmake: Cleanup naming of run targets

* core: Fix typos

* core: Support no-signal tty mode

* repl: Wip prompt logic

* core: Add tty_write_set_cursor_hor_sequence util

* core: Add 'EndOfText' unicode name

* core: Add basic tty input lexer

* repl: Add edit buffer

* repl: Minor line edit polish

* core: Add tty_write_line_wrap_sequence util

* repl: Disable line wrap

* repl: Add script execution

* repl: Add syntax highlighting

* repl: Add prev text support

* repl: Tweak syntax highlighting colors

* repl: MSVC compat

* core: Add carriage return as accept input
  • Loading branch information
BastianBlokland authored Aug 29, 2023
1 parent 6c1de1b commit 43cadea
Show file tree
Hide file tree
Showing 9 changed files with 433 additions and 9 deletions.
10 changes: 9 additions & 1 deletion apps/utilities/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ add_executable(app_schemasetup schemasetup.c)
target_link_libraries(app_schemasetup PRIVATE lib_app_cli lib_asset lib_log)
configure_debuggable(app_schemasetup)

add_custom_target(schemasetup COMMAND app_schemasetup "--out"
add_custom_target(run.schemasetup COMMAND app_schemasetup "--out"
"$<TARGET_PROPERTY:volo_assets,path>/schemas/atl.schema.json"
"$<TARGET_PROPERTY:volo_assets,path>/schemas/atx.schema.json"
"$<TARGET_PROPERTY:volo_assets,path>/schemas/bt.btschema"
Expand All @@ -30,3 +30,11 @@ message(STATUS "> application: dbgsetup")

add_executable(app_dbgsetup dbgsetup.c)
target_link_libraries(app_dbgsetup PRIVATE lib_app_cli lib_json lib_log)

message(STATUS "> application: repl")

add_executable(app_repl repl.c)
target_link_libraries(app_repl PRIVATE lib_app_cli lib_script)
configure_debuggable(app_repl)

add_custom_target(run.repl COMMAND app_repl VERBATIM USES_TERMINAL)
252 changes: 252 additions & 0 deletions apps/utilities/repl.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
#include "app_cli.h"
#include "core_alloc.h"
#include "core_file.h"
#include "core_format.h"
#include "core_tty.h"
#include "core_utf8.h"
#include "script_eval.h"
#include "script_lex.h"
#include "script_mem.h"
#include "script_read.h"

/**
* ReadEvalPrintLoop - Utility to play around with script execution.
*/

static void repl_output(const String text) { file_write_sync(g_file_stdout, text); }

static void repl_output_error(const String message) {
const String text = fmt_write_scratch(
"{}ERROR: {}{}\n",
fmt_ttystyle(.bgColor = TtyBgColor_Red, .flags = TtyStyleFlags_Bold),
fmt_text(message),
fmt_ttystyle());
repl_output(text);
}

static TtyFgColor repl_token_color(const ScriptTokenType tokenType) {
switch (tokenType) {
case ScriptTokenType_Error:
return TtyFgColor_BrightRed;
case ScriptTokenType_Number:
return TtyFgColor_Yellow;
case ScriptTokenType_Identifier:
return TtyFgColor_Magenta;
case ScriptTokenType_Key:
return TtyFgColor_Blue;
case ScriptTokenType_Eq:
case ScriptTokenType_EqEq:
case ScriptTokenType_Bang:
case ScriptTokenType_BangEq:
case ScriptTokenType_Le:
case ScriptTokenType_LeEq:
case ScriptTokenType_Gt:
case ScriptTokenType_GtEq:
case ScriptTokenType_Plus:
case ScriptTokenType_Minus:
case ScriptTokenType_Star:
case ScriptTokenType_Slash:
case ScriptTokenType_Colon:
case ScriptTokenType_SemiColon:
case ScriptTokenType_AmpAmp:
case ScriptTokenType_PipePipe:
case ScriptTokenType_QMark:
case ScriptTokenType_QMarkQMark:
return TtyFgColor_Green;
case ScriptTokenType_ParenOpen:
case ScriptTokenType_ParenClose:
case ScriptTokenType_Comma:
case ScriptTokenType_End:
break;
}
return TtyFgColor_Default;
}

typedef struct {
String editPrevText;
DynString* editBuffer;
ScriptMem* scriptMem;
} ReplState;

static bool repl_edit_empty(const ReplState* state) {
return string_is_empty(dynstring_view(state->editBuffer));
}

static void repl_edit_prev(const ReplState* state) {
if (!string_is_empty(state->editPrevText)) {
dynstring_clear(state->editBuffer);
dynstring_append(state->editBuffer, state->editPrevText);
}
}

static void repl_edit_clear(const ReplState* state) { dynstring_clear(state->editBuffer); }

static void repl_edit_insert(const ReplState* state, const Unicode cp) {
utf8_cp_write(state->editBuffer, cp);
}

static void repl_edit_delete(const ReplState* state) {
// Delete the last utf8 code-point.
String str = dynstring_view(state->editBuffer);
for (usize i = str.size; i-- > 0;) {
if (!utf8_contchar(*string_at(str, i))) {
dynstring_erase_chars(state->editBuffer, i, str.size - i);
return;
}
}
}

static void repl_edit_submit(ReplState* state) {
repl_output(string_lit("\n")); // Preserve the input line.

string_maybe_free(g_alloc_heap, state->editPrevText);
state->editPrevText = string_maybe_dup(g_alloc_heap, dynstring_view(state->editBuffer));

ScriptDoc* script = script_create(g_alloc_heap);
ScriptReadResult res;
script_read_all(script, dynstring_view(state->editBuffer), &res);

if (res.type == ScriptResult_Success) {
const ScriptVal value = script_eval(script, state->scriptMem, res.expr);
const String text = fmt_write_scratch("{}\n", fmt_text(script_val_str_scratch(value)));
repl_output(text);
} else {
repl_output_error(script_error_str(res.error));
}

script_destroy(script);
dynstring_clear(state->editBuffer);
}

static void repl_edit_render(const ReplState* state) {
Mem bufferMem = alloc_alloc(g_alloc_scratch, usize_kibibyte, 1);
DynString buffer = dynstring_create_over(bufferMem);

tty_write_clear_line_sequence(&buffer, TtyClearMode_All); // Clear line.
tty_write_set_cursor_hor_sequence(&buffer, 0); // Move cursor to beginning of line.
tty_write_line_wrap_sequence(&buffer, false); // Disable line wrap.

// Render header.
tty_write_style_sequence(&buffer, ttystyle(.flags = TtyStyleFlags_Faint));
dynstring_append(&buffer, string_lit("> "));
tty_write_style_sequence(&buffer, ttystyle());

// Render edit text.
String editText = dynstring_view(state->editBuffer);
ScriptToken token;
for (;;) {
const String remText = script_lex(editText, null, &token);
const usize tokenSize = editText.size - remText.size;
const String tokenText = string_slice(editText, 0, tokenSize);
tty_write_style_sequence(&buffer, ttystyle(.fgColor = repl_token_color(token.type)));
dynstring_append(&buffer, tokenText);
if (token.type == ScriptTokenType_End) {
break;
}
editText = remText;
}

tty_write_style_sequence(&buffer, ttystyle());
repl_output(dynstring_view(&buffer));
dynstring_destroy(&buffer);
}

static void repl_edit_render_cleanup() {
Mem bufferMem = alloc_alloc(g_alloc_scratch, usize_kibibyte, 1);
DynString buffer = dynstring_create_over(bufferMem);

tty_write_clear_line_sequence(&buffer, TtyClearMode_All);
tty_write_set_cursor_hor_sequence(&buffer, 0);
tty_write_line_wrap_sequence(&buffer, true); // TODO: Only do this if it was originally enabled?

repl_output(dynstring_view(&buffer));
dynstring_destroy(&buffer);
}

static bool repl_update(ReplState* state, TtyInputToken* input) {
switch (input->type) {
case TtyInputType_Interrupt:
return false; // Stop.
case TtyInputType_KeyEscape:
repl_edit_clear(state);
break;
case TtyInputType_Text:
repl_edit_insert(state, input->val_text);
break;
case TtyInputType_KeyBackspace:
repl_edit_delete(state);
break;
case TtyInputType_KeyUp:
repl_edit_prev(state);
break;
case TtyInputType_Accept:
if (!repl_edit_empty(state)) {
repl_edit_submit(state);
}
break;
default:
break;
}
repl_edit_render(state);
return true; // Keep running.
}

static i32 repl_run_interactive() {
if (!tty_isatty(g_file_stdin) || !tty_isatty(g_file_stdout)) {
file_write_sync(g_file_stderr, string_lit("ERROR: REPL has to be ran interactively\n"));
return 1;
}

DynString readBuffer = dynstring_create(g_alloc_heap, 32);
DynString editBuffer = dynstring_create(g_alloc_heap, 128);

ReplState state = {
.editBuffer = &editBuffer,
.scriptMem = script_mem_create(g_alloc_heap),
};

tty_opts_set(g_file_stdin, TtyOpts_NoEcho | TtyOpts_NoBuffer | TtyOpts_NoSignals);
repl_edit_render(&state);

while (tty_read(g_file_stdin, &readBuffer, TtyReadFlags_None)) {
String readStr = dynstring_view(&readBuffer);
TtyInputToken input;
for (;;) {
readStr = tty_input_lex(readStr, &input);
if (input.type == TtyInputType_End) {
break;
}
if (!repl_update(&state, &input)) {
goto Stop;
}
}
dynstring_clear(&readBuffer);
}

Stop:
repl_edit_render_cleanup();
tty_opts_set(g_file_stdin, TtyOpts_None);

dynstring_destroy(&readBuffer);
dynstring_destroy(&editBuffer);
string_maybe_free(g_alloc_heap, state.editPrevText);
script_mem_destroy(state.scriptMem);
return 0;
}

static CliId g_helpFlag;

void app_cli_configure(CliApp* app) {
cli_app_register_desc(app, string_lit("Script ReadEvalPrintLoop utility."));

g_helpFlag = cli_register_flag(app, 'h', string_lit("help"), CliOptionFlags_None);
cli_register_desc(app, g_helpFlag, string_lit("Display this help page."));
}

i32 app_cli_run(const CliApp* app, const CliInvocation* invoc) {
if (cli_parse_provided(invoc, g_helpFlag)) {
cli_help_write_file(app, g_file_stdout);
return 0;
}
return repl_run_interactive();
}
2 changes: 1 addition & 1 deletion cmake/debug.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function(configure_dbgsetup_target)
message(FATAL_ERROR "Unknown compiler")
endif()

add_custom_target(dbgsetup COMMAND app_dbgsetup
add_custom_target(run.dbgsetup COMMAND app_dbgsetup
"--debugger" "${debugger}"
"--workspace" "${PROJECT_SOURCE_DIR}"
"--targets" "$<GENEX_EVAL:${targets}>" VERBATIM USES_TERMINAL
Expand Down
48 changes: 43 additions & 5 deletions libs/core/include/core_tty.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once
#include "core_dynstring.h"
#include "core_types.h"
#include "core_unicode.h"

// Forward declare from 'core_file.h'.
typedef struct sFile File;
Expand Down Expand Up @@ -115,9 +116,10 @@ u16 tty_width(File*);
u16 tty_height(File*);

typedef enum {
TtyOpts_None = 0,
TtyOpts_NoEcho = 1 << 0,
TtyOpts_NoBuffer = 1 << 1,
TtyOpts_None = 0,
TtyOpts_NoEcho = 1 << 0,
TtyOpts_NoBuffer = 1 << 1,
TtyOpts_NoSignals = 1 << 2, // Disable signal sending, eg. enables reading ctrl-c as input.
} TtyOpts;

/**
Expand All @@ -142,6 +144,37 @@ typedef enum {
*/
bool tty_read(File*, DynString*, TtyReadFlags);

typedef enum {
TtyInputType_Accept,
TtyInputType_Interrupt,
TtyInputType_KeyEscape,
TtyInputType_KeyUp,
TtyInputType_KeyDown,
TtyInputType_KeyRight,
TtyInputType_KeyLeft,
TtyInputType_KeyEnd,
TtyInputType_KeyHome,
TtyInputType_KeyDelete,
TtyInputType_KeyBackspace,
TtyInputType_Text,
TtyInputType_Unsupported,
TtyInputType_End,
} TtyInputType;

typedef struct {
TtyInputType type;
union {
Unicode val_text;
};
} TtyInputToken;

/**
* Read a single Tty input token.
* Returns the remaining input.
* The token is written to the output pointer.
*/
String tty_input_lex(String, TtyInputToken*);

/**
* Write a ANSI escape sequence to the provided dynamic-string for setting the terminal style.
*/
Expand All @@ -158,6 +191,7 @@ void tty_write_window_title_sequence(DynString*, String title);
* NOTE: The values are 1-based.
*/
void tty_write_set_cursor_sequence(DynString*, u32 row, u32 col);
void tty_write_set_cursor_hor_sequence(DynString*, u32 col);

/**
* Write a ANSI escape sequence to the provided dynamic-string for enabling / disabling the cursor.
Expand All @@ -181,7 +215,11 @@ void tty_write_clear_sequence(DynString*, TtyClearMode);
void tty_write_clear_line_sequence(DynString*, TtyClearMode);

/**
* Write a ANSI escape sequence to the provided dynamic-string for enabling alternative screen
* buffer.
* Write a ANSI escape sequence to the provided dynamic-string for enabling alt screen buffer.
*/
void tty_write_alt_screen_sequence(DynString*, bool enable);

/**
* Write a ANSI escape sequence to the provided dynamic-string for enabling / disabling line wrap.
*/
void tty_write_line_wrap_sequence(DynString*, bool enable);
1 change: 1 addition & 0 deletions libs/core/include/core_unicode.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/
typedef enum eUnicode {
Unicode_Invalid = 0x0,
Unicode_EndOfText = 0x3,
Unicode_Bell = 0x7,
Unicode_Backspace = 0x8,
Unicode_HorizontalTab = 0x9,
Expand Down
4 changes: 2 additions & 2 deletions libs/core/src/file_pal_linux.c
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ FileResult file_write_sync(File* file, const String data) {
switch (errno) {
case EAGAIN:
case EINTR:
continue; // Retry on interupt.
continue; // Retry on interrupt.
}
return fileresult_from_errno();
}
Expand Down Expand Up @@ -181,7 +181,7 @@ FileResult file_read_sync(File* file, DynString* dynstr) {
}
switch (errno) {
case EINTR:
continue; // Retry on interupt.
continue; // Retry on interrupt.
}
return fileresult_from_errno();
}
Expand Down
Loading

0 comments on commit 43cadea

Please sign in to comment.