From 255f10dfae10a8db16168b2c166546102523d460 Mon Sep 17 00:00:00 2001 From: Sidi Liang <1467329765@qq.com> Date: Sun, 3 Nov 2024 16:13:00 +0800 Subject: [PATCH 01/20] Updated gitignore --- .gitignore | 76 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 60 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 7b57570..7ccf091 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,21 @@ +# Build directories +build/ +out/ +dist/ +cmake-build-*/ + +# IDE and editor files +.vscode/ +.idea/ +.vs/ +*.swp +*.swo +*~ +.DS_Store +.env +.env.local + +# C++ specific # Prerequisites *.d @@ -30,32 +48,58 @@ *.exe *.out *.app +nasal +nasal.exe -# VS C++ sln +# Visual Studio specific *.sln *.vcxproj *.vcxproj.filters *.vcxproj.user -.vs -x64 +x64/ CMakePresents.json +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb -# nasal executable -nasal -nasal.exe +# CMake +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +install_manifest.txt +CTestTestfile.cmake +_deps/ -# misc -.vscode -dump +# Node.js specific (for the web app) +node_modules/ +npm-debug.log +yarn-debug.log +yarn-error.log +package-lock.json + +# Project specific +dump/ fgfs.log .temp.* +*.ppm -# build dir -build -out +# Logs and databases +*.log +*.sqlite +*.sqlite3 +*.db -# macOS special cache directory +# OS generated files .DS_Store - -# ppm picture generated by ppmgen.nas -*.ppm \ No newline at end of file +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db \ No newline at end of file From b7176b39eda7ed5f52c15752ae1dda2834a9dc16 Mon Sep 17 00:00:00 2001 From: Sidi Liang <1467329765@qq.com> Date: Sun, 3 Nov 2024 16:14:30 +0800 Subject: [PATCH 02/20] [web] Added lib for web app --- CMakeLists.txt | 12 +++++ src/nasal_web.cpp | 112 ++++++++++++++++++++++++++++++++++++++++++++++ src/nasal_web.h | 23 ++++++++++ 3 files changed, 147 insertions(+) create mode 100644 src/nasal_web.cpp create mode 100644 src/nasal_web.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 6941632..1a58c3d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,3 +100,15 @@ target_link_libraries(mat module-used-object) add_library(nasock SHARED ${CMAKE_SOURCE_DIR}/module/nasocket.cpp) target_include_directories(nasock PRIVATE ${CMAKE_SOURCE_DIR}/src) target_link_libraries(nasock module-used-object) + +# Add web library +add_library(nasal-web SHARED + src/nasal_web.cpp + ${NASAL_OBJECT_SOURCE_FILE} +) +target_include_directories(nasal-web PRIVATE ${CMAKE_SOURCE_DIR}/src) +set_target_properties(nasal-web PROPERTIES + C_VISIBILITY_PRESET hidden + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON +) diff --git a/src/nasal_web.cpp b/src/nasal_web.cpp new file mode 100644 index 0000000..cf13c58 --- /dev/null +++ b/src/nasal_web.cpp @@ -0,0 +1,112 @@ +#include "nasal_web.h" +#include "nasal_vm.h" +#include "nasal_parse.h" +#include "nasal_codegen.h" +#include "nasal_import.h" +#include "nasal_err.h" +#include "nasal_lexer.h" +#include +#include +#include +#include +#include +#include + +struct NasalContext { + nasal::gc gc_instance; + std::unique_ptr vm_instance; + std::string last_result; + std::string last_error; + + NasalContext() { + vm_instance = std::make_unique(); + } +}; + +void* nasal_init() { + return new NasalContext(); +} + +void nasal_cleanup(void* context) { + delete static_cast(context); +} + +const char* nasal_eval(void* context, const char* code) { + auto* ctx = static_cast(context); + + try { + nasal::lexer lex; + nasal::parse parse; + nasal::linker ld; + nasal::codegen gen; + + // Create a unique temporary file + char temp_filename[] = "/tmp/nasal_eval_XXXXXX.nasal"; + int fd = mkstemp(temp_filename); + if (fd == -1) { + throw std::runtime_error("Failed to create temporary file"); + } + + // Write the code to the temporary file + std::ofstream temp_file(temp_filename); + if (!temp_file.is_open()) { + close(fd); + throw std::runtime_error("Failed to open temporary file for writing"); + } + temp_file << code; + temp_file.close(); + close(fd); // Close the file descriptor + + // Capture stdout and stderr + std::stringstream output; + std::stringstream error_output; + std::streambuf* old_cout = std::cout.rdbuf(output.rdbuf()); + std::streambuf* old_cerr = std::cerr.rdbuf(error_output.rdbuf()); + + // Process the code by scanning the temporary file + if (lex.scan(std::string(temp_filename)).geterr()) { + ctx->last_error = error_output.str(); + std::cout.rdbuf(old_cout); + std::cerr.rdbuf(old_cerr); + // Remove the temporary file + std::remove(temp_filename); + return ctx->last_error.c_str(); + } + + if (parse.compile(lex).geterr()) { + ctx->last_error = error_output.str(); + std::cout.rdbuf(old_cout); + std::cerr.rdbuf(old_cerr); + // Remove the temporary file + std::remove(temp_filename); + return ctx->last_error.c_str(); + } + + ld.link(parse, false).chkerr(); + gen.compile(parse, ld, false, false).chkerr(); + + // Run the code + ctx->vm_instance->run(gen, ld, {}); + + // Restore stdout and stderr and get the outputs + std::cout.rdbuf(old_cout); + std::cerr.rdbuf(old_cerr); + ctx->last_result = output.str() + error_output.str(); + if (ctx->last_result.empty()) { + ctx->last_result = "Execution completed successfully."; + } + + // Remove the temporary file + std::remove(temp_filename); + + return ctx->last_result.c_str(); + } catch (const std::exception& e) { + ctx->last_error = e.what(); + return ctx->last_error.c_str(); + } +} + +const char* nasal_get_error(void* context) { + auto* ctx = static_cast(context); + return ctx->last_error.c_str(); +} diff --git a/src/nasal_web.h b/src/nasal_web.h new file mode 100644 index 0000000..f3c5eb5 --- /dev/null +++ b/src/nasal_web.h @@ -0,0 +1,23 @@ +#pragma once + +#include "nasal.h" + +#ifdef _WIN32 + #define NASAL_EXPORT __declspec(dllexport) +#else + #define NASAL_EXPORT __attribute__((visibility("default"))) +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +// Main API functions +NASAL_EXPORT void* nasal_init(); +NASAL_EXPORT void nasal_cleanup(void* context); +NASAL_EXPORT const char* nasal_eval(void* context, const char* code); +NASAL_EXPORT const char* nasal_get_error(void* context); + +#ifdef __cplusplus +} +#endif From 1c48e088895e1050c9c50f56d068e74b3e7e6af2 Mon Sep 17 00:00:00 2001 From: Sidi Liang <1467329765@qq.com> Date: Sun, 3 Nov 2024 16:14:59 +0800 Subject: [PATCH 03/20] [web] Added nodejs web app demo --- nasal-web-app/package.json | 16 + nasal-web-app/public/index.html | 620 ++++++++++++++++++++++++++++++++ nasal-web-app/server.js | 50 +++ nasal-web-app/std | 1 + 4 files changed, 687 insertions(+) create mode 100644 nasal-web-app/package.json create mode 100644 nasal-web-app/public/index.html create mode 100644 nasal-web-app/server.js create mode 120000 nasal-web-app/std diff --git a/nasal-web-app/package.json b/nasal-web-app/package.json new file mode 100644 index 0000000..f582aca --- /dev/null +++ b/nasal-web-app/package.json @@ -0,0 +1,16 @@ +{ + "name": "nasal-web-app", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "express": "^4.21.1", + "ffi-napi": "^4.0.3" + } +} diff --git a/nasal-web-app/public/index.html b/nasal-web-app/public/index.html new file mode 100644 index 0000000..5b194cf --- /dev/null +++ b/nasal-web-app/public/index.html @@ -0,0 +1,620 @@ + + + + + + Nasal Interpreter Web Demo + + + + + +
+
+ +

Nasal Interpreter Web Demo

+

Write and execute Nasal code directly in your browser

+
+ +
+ Web App by + + LIANG Sidi + +
+
+
+ +
+
+

Code Editor

+ +
+
+
+

Output

+
+
+
+
+
+ +
+ + +
+ +
+

Example Programs:

+ + + +
+
+ + + + + + diff --git a/nasal-web-app/server.js b/nasal-web-app/server.js new file mode 100644 index 0000000..5e69dae --- /dev/null +++ b/nasal-web-app/server.js @@ -0,0 +1,50 @@ +const express = require('express'); +const ffi = require('ffi-napi'); +const path = require('path'); + +const app = express(); + +app.use(express.json()); +app.use(express.static('public')); + +const nasalLib = ffi.Library(path.join(__dirname, '../module/libnasal-web'), { + 'nasal_init': ['pointer', []], + 'nasal_cleanup': ['void', ['pointer']], + 'nasal_eval': ['string', ['pointer', 'string']], + 'nasal_get_error': ['string', ['pointer']] +}); + +app.post('/eval', (req, res) => { + const { code } = req.body; + if (!code) { + return res.status(400).json({ error: 'No code provided' }); + } + + const ctx = nasalLib.nasal_init(); + try { + const result = nasalLib.nasal_eval(ctx, code); + const error = nasalLib.nasal_get_error(ctx); + + // Check if there's an error first + if (error && error !== 'null' && error.trim() !== '') { + console.log('Nasal error:', error); // For debugging + res.json({ error: error }); + } else if (result && result.trim() !== '') { + console.log('Nasal output:', result); // For debugging + res.json({ result: result }); + } else { + res.json({ error: 'No output or error returned' }); + } + } catch (err) { + console.error('Server error:', err); // For debugging + res.status(500).json({ error: err.message }); + } finally { + nasalLib.nasal_cleanup(ctx); + } +}); + +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); + console.log(`Visit http://localhost:${PORT} to use the Nasal interpreter`); +}); \ No newline at end of file diff --git a/nasal-web-app/std b/nasal-web-app/std new file mode 120000 index 0000000..6ef7545 --- /dev/null +++ b/nasal-web-app/std @@ -0,0 +1 @@ +../std \ No newline at end of file From 83ffcc3087e7deae7b3fbb02029212da607e968d Mon Sep 17 00:00:00 2001 From: Sidi Liang <1467329765@qq.com> Date: Sun, 3 Nov 2024 16:31:37 +0800 Subject: [PATCH 04/20] [web] enable limit_mode for safety, enabled optimizer --- src/nasal_web.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/nasal_web.cpp b/src/nasal_web.cpp index cf13c58..689e2dc 100644 --- a/src/nasal_web.cpp +++ b/src/nasal_web.cpp @@ -3,8 +3,10 @@ #include "nasal_parse.h" #include "nasal_codegen.h" #include "nasal_import.h" +#include "optimizer.h" #include "nasal_err.h" #include "nasal_lexer.h" + #include #include #include @@ -83,7 +85,10 @@ const char* nasal_eval(void* context, const char* code) { } ld.link(parse, false).chkerr(); - gen.compile(parse, ld, false, false).chkerr(); + // optimizer does simple optimization on ast + auto opt = std::make_unique(); + opt->do_optimization(parse.tree()); + gen.compile(parse, ld, false, true).chkerr(); // enable limit_mode for safety // Run the code ctx->vm_instance->run(gen, ld, {}); From 037ac9e79f8da337239424a7f4b247295312b213 Mon Sep 17 00:00:00 2001 From: Sidi Liang <1467329765@qq.com> Date: Sun, 3 Nov 2024 17:30:02 +0800 Subject: [PATCH 05/20] [web] nasal_web.cpp: remove gc in NasalContext, which is not needed --- src/nasal_web.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/nasal_web.cpp b/src/nasal_web.cpp index 689e2dc..e1c9f2a 100644 --- a/src/nasal_web.cpp +++ b/src/nasal_web.cpp @@ -15,7 +15,6 @@ #include struct NasalContext { - nasal::gc gc_instance; std::unique_ptr vm_instance; std::string last_result; std::string last_error; From 95031f508b12e533e8541827f2d9a70c74c6c8d9 Mon Sep 17 00:00:00 2001 From: Sidi Liang <1467329765@qq.com> Date: Sun, 3 Nov 2024 17:30:26 +0800 Subject: [PATCH 06/20] [web] Added an option to display execution time --- src/nasal_web.cpp | 31 +++++++++++++++++++++++++------ src/nasal_web.h | 7 +++++-- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/nasal_web.cpp b/src/nasal_web.cpp index e1c9f2a..4925f8e 100644 --- a/src/nasal_web.cpp +++ b/src/nasal_web.cpp @@ -13,6 +13,7 @@ #include #include #include +#include struct NasalContext { std::unique_ptr vm_instance; @@ -32,7 +33,10 @@ void nasal_cleanup(void* context) { delete static_cast(context); } -const char* nasal_eval(void* context, const char* code) { +const char* nasal_eval(void* context, const char* code, int show_time) { + using clk = std::chrono::high_resolution_clock; + const auto den = clk::duration::period::den; + auto* ctx = static_cast(context); try { @@ -89,16 +93,31 @@ const char* nasal_eval(void* context, const char* code) { opt->do_optimization(parse.tree()); gen.compile(parse, ld, false, true).chkerr(); // enable limit_mode for safety - // Run the code + // Run the code with optional timing + const auto start = show_time ? clk::now() : clk::time_point(); ctx->vm_instance->run(gen, ld, {}); - + const auto end = show_time ? clk::now() : clk::time_point(); + // Restore stdout and stderr and get the outputs std::cout.rdbuf(old_cout); std::cerr.rdbuf(old_cerr); - ctx->last_result = output.str() + error_output.str(); - if (ctx->last_result.empty()) { - ctx->last_result = "Execution completed successfully."; + + std::stringstream result; + result << output.str(); + if (!error_output.str().empty()) { + result << error_output.str(); } + if (result.str().empty()) { + result << "Execution completed successfully.\n"; + } + + // Add execution time if requested + if (show_time) { + double execution_time = static_cast((end-start).count())/den; + result << "\nExecution time: " << execution_time << "s"; + } + + ctx->last_result = result.str(); // Remove the temporary file std::remove(temp_filename); diff --git a/src/nasal_web.h b/src/nasal_web.h index f3c5eb5..63a734b 100644 --- a/src/nasal_web.h +++ b/src/nasal_web.h @@ -1,4 +1,5 @@ -#pragma once +#ifndef __NASAL_WEB_H__ +#define __NASAL_WEB_H__ #include "nasal.h" @@ -15,9 +16,11 @@ extern "C" { // Main API functions NASAL_EXPORT void* nasal_init(); NASAL_EXPORT void nasal_cleanup(void* context); -NASAL_EXPORT const char* nasal_eval(void* context, const char* code); +NASAL_EXPORT const char* nasal_eval(void* context, const char* code, int show_time); NASAL_EXPORT const char* nasal_get_error(void* context); #ifdef __cplusplus } #endif + +#endif From 8ecf30979198ea23ec48884fc0d5934fa50bb315 Mon Sep 17 00:00:00 2001 From: Sidi Liang <1467329765@qq.com> Date: Sun, 3 Nov 2024 17:31:20 +0800 Subject: [PATCH 07/20] [web] Added an option to show execution time on the front end, and improved server.js --- nasal-web-app/package.json | 3 +- nasal-web-app/public/index.html | 26 ++++++++++++++--- nasal-web-app/server.js | 51 +++++++++++++++++++++++++++------ 3 files changed, 66 insertions(+), 14 deletions(-) diff --git a/nasal-web-app/package.json b/nasal-web-app/package.json index f582aca..b0483e1 100644 --- a/nasal-web-app/package.json +++ b/nasal-web-app/package.json @@ -11,6 +11,7 @@ "license": "ISC", "dependencies": { "express": "^4.21.1", - "ffi-napi": "^4.0.3" + "ffi-napi": "^4.0.3", + "yargs": "^17.7.2" } } diff --git a/nasal-web-app/public/index.html b/nasal-web-app/public/index.html index 5b194cf..26dc8e4 100644 --- a/nasal-web-app/public/index.html +++ b/nasal-web-app/public/index.html @@ -30,7 +30,7 @@ } .logo { - margin-bottom: 20px; + margin-bottom: 2px; } .ascii-art { @@ -400,6 +400,17 @@ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; line-height: 1.5; } + + .timing-checkbox { + display: inline-flex; + align-items: center; + margin-left: 10px; + color: #abb2bf; + } + + .timing-checkbox input { + margin-right: 5px; + } @@ -449,8 +460,11 @@ println(x);
- - + + +
@@ -557,6 +571,7 @@ for(var i = 0; i <= 5; i += 1) { const runBtn = document.getElementById('runBtn'); const output = document.getElementById('output'); const status = document.getElementById('status'); + const showTime = document.getElementById('showTime').checked; try { runBtn.disabled = true; @@ -567,7 +582,10 @@ for(var i = 0; i <= 5; i += 1) { headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ code }) + body: JSON.stringify({ + code, + showTime + }) }); const data = await response.json(); diff --git a/nasal-web-app/server.js b/nasal-web-app/server.js index 5e69dae..18f3e17 100644 --- a/nasal-web-app/server.js +++ b/nasal-web-app/server.js @@ -1,6 +1,32 @@ const express = require('express'); const ffi = require('ffi-napi'); const path = require('path'); +const yargs = require('yargs/yargs'); +const { hideBin } = require('yargs/helpers'); + +// Parse command line arguments +const argv = yargs(hideBin(process.argv)) + .usage('Usage: $0 [options]') + .option('verbose', { + alias: 'v', + type: 'boolean', + description: 'Run with verbose logging' + }) + .option('port', { + alias: 'p', + type: 'number', + description: 'Port to run the server on', + default: 3000 + }) + .option('host', { + type: 'string', + description: 'Host to run the server on', + default: 'localhost' + }) + .help() + .alias('help', 'h') + .version() + .argv; const app = express(); @@ -10,41 +36,48 @@ app.use(express.static('public')); const nasalLib = ffi.Library(path.join(__dirname, '../module/libnasal-web'), { 'nasal_init': ['pointer', []], 'nasal_cleanup': ['void', ['pointer']], - 'nasal_eval': ['string', ['pointer', 'string']], + 'nasal_eval': ['string', ['pointer', 'string', 'int']], 'nasal_get_error': ['string', ['pointer']] }); app.post('/eval', (req, res) => { - const { code } = req.body; + const { code, showTime = false } = req.body; if (!code) { return res.status(400).json({ error: 'No code provided' }); } + if (argv.verbose) { + console.log('Received code evaluation request:', code); + console.log('Show time:', showTime); + } + const ctx = nasalLib.nasal_init(); try { - const result = nasalLib.nasal_eval(ctx, code); + const result = nasalLib.nasal_eval(ctx, code, showTime ? 1 : 0); const error = nasalLib.nasal_get_error(ctx); - // Check if there's an error first - if (error && error !== 'null' && error.trim() !== '') { - console.log('Nasal error:', error); // For debugging + if (error && error !== 'null') { + if (argv.verbose) console.log('Nasal error:', error); res.json({ error: error }); } else if (result && result.trim() !== '') { - console.log('Nasal output:', result); // For debugging + if (argv.verbose) console.log('Nasal output:', result); res.json({ result: result }); } else { + if (argv.verbose) console.log('No output or error returned'); res.json({ error: 'No output or error returned' }); } } catch (err) { - console.error('Server error:', err); // For debugging + if (argv.verbose) console.error('Server error:', err); res.status(500).json({ error: err.message }); } finally { + if (argv.verbose) console.log('Cleaning up Nasal context'); nasalLib.nasal_cleanup(ctx); } }); -const PORT = process.env.PORT || 3000; +const PORT = argv.port || 3000; app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); console.log(`Visit http://localhost:${PORT} to use the Nasal interpreter`); + if (argv.verbose) console.log('Verbose logging enabled'); }); \ No newline at end of file From 6260cc166548c2cf8b973a9a3c420ea4deb1f929 Mon Sep 17 00:00:00 2001 From: Sidi Liang <1467329765@qq.com> Date: Mon, 4 Nov 2024 12:04:14 +0800 Subject: [PATCH 08/20] [web] Added REPL support to the library --- src/nasal_web.cpp | 212 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 199 insertions(+), 13 deletions(-) diff --git a/src/nasal_web.cpp b/src/nasal_web.cpp index 4925f8e..771e325 100644 --- a/src/nasal_web.cpp +++ b/src/nasal_web.cpp @@ -6,6 +6,7 @@ #include "optimizer.h" #include "nasal_err.h" #include "nasal_lexer.h" +#include "repl/repl.h" #include #include @@ -14,6 +15,30 @@ #include #include #include +#include + +namespace { + // Helper function implementations inside anonymous namespace + std::vector split_string(const std::string& str, char delim) { + std::vector result; + std::stringstream ss(str); + std::string item; + while (std::getline(ss, item, delim)) { + result.push_back(item); + } + return result; + } + + std::string join_string(const std::vector& vec, const std::string& delim) { + if (vec.empty()) return ""; + std::stringstream ss; + ss << vec[0]; + for (size_t i = 1; i < vec.size(); ++i) { + ss << delim << vec[i]; + } + return ss.str(); + } +} struct NasalContext { std::unique_ptr vm_instance; @@ -25,6 +50,19 @@ struct NasalContext { } }; +struct WebReplContext { + std::unique_ptr repl_instance; + std::vector source; + std::string last_result; + std::string last_error; + bool allow_output; + bool initialized; + + WebReplContext() : allow_output(false), initialized(false) { + repl_instance = std::make_unique(); + } +}; + void* nasal_init() { return new NasalContext(); } @@ -60,20 +98,19 @@ const char* nasal_eval(void* context, const char* code, int show_time) { } temp_file << code; temp_file.close(); - close(fd); // Close the file descriptor + close(fd); // Capture stdout and stderr std::stringstream output; std::stringstream error_output; - std::streambuf* old_cout = std::cout.rdbuf(output.rdbuf()); - std::streambuf* old_cerr = std::cerr.rdbuf(error_output.rdbuf()); + auto old_cout = std::cout.rdbuf(output.rdbuf()); + auto old_cerr = std::cerr.rdbuf(error_output.rdbuf()); - // Process the code by scanning the temporary file + // Process the code if (lex.scan(std::string(temp_filename)).geterr()) { ctx->last_error = error_output.str(); std::cout.rdbuf(old_cout); std::cerr.rdbuf(old_cerr); - // Remove the temporary file std::remove(temp_filename); return ctx->last_error.c_str(); } @@ -82,23 +119,19 @@ const char* nasal_eval(void* context, const char* code, int show_time) { ctx->last_error = error_output.str(); std::cout.rdbuf(old_cout); std::cerr.rdbuf(old_cerr); - // Remove the temporary file std::remove(temp_filename); return ctx->last_error.c_str(); } ld.link(parse, false).chkerr(); - // optimizer does simple optimization on ast auto opt = std::make_unique(); opt->do_optimization(parse.tree()); - gen.compile(parse, ld, false, true).chkerr(); // enable limit_mode for safety + gen.compile(parse, ld, false, true).chkerr(); - // Run the code with optional timing const auto start = show_time ? clk::now() : clk::time_point(); ctx->vm_instance->run(gen, ld, {}); const auto end = show_time ? clk::now() : clk::time_point(); - // Restore stdout and stderr and get the outputs std::cout.rdbuf(old_cout); std::cerr.rdbuf(old_cerr); @@ -111,15 +144,12 @@ const char* nasal_eval(void* context, const char* code, int show_time) { result << "Execution completed successfully.\n"; } - // Add execution time if requested if (show_time) { double execution_time = static_cast((end-start).count())/den; result << "\nExecution time: " << execution_time << "s"; } ctx->last_result = result.str(); - - // Remove the temporary file std::remove(temp_filename); return ctx->last_result.c_str(); @@ -133,3 +163,159 @@ const char* nasal_get_error(void* context) { auto* ctx = static_cast(context); return ctx->last_error.c_str(); } + +void* nasal_repl_init() { + auto* ctx = new WebReplContext(); + + try { + // Initialize environment silently + nasal::repl::info::instance()->in_repl_mode = true; + ctx->repl_instance->get_runtime().set_repl_mode_flag(true); + ctx->repl_instance->get_runtime().set_detail_report_info(false); + + // Run initial setup + ctx->repl_instance->set_source({}); + if (!ctx->repl_instance->run()) { + ctx->last_error = "Failed to initialize REPL environment"; + return ctx; + } + + // Enable output after initialization + ctx->allow_output = true; + ctx->repl_instance->get_runtime().set_allow_repl_output_flag(true); + ctx->initialized = true; + } catch (const std::exception& e) { + ctx->last_error = std::string("Initialization error: ") + e.what(); + } + + return ctx; +} + +void nasal_repl_cleanup(void* context) { + delete static_cast(context); +} + +const char* nasal_repl_eval(void* context, const char* line) { + auto* ctx = static_cast(context); + + if (!ctx->initialized) { + ctx->last_error = "REPL not properly initialized"; + return ctx->last_error.c_str(); + } + + try { + std::string input_line(line); + + // Handle empty input + if (input_line.empty()) { + ctx->last_result = ""; + return ctx->last_result.c_str(); + } + + // Handle REPL commands + if (input_line[0] == '.') { + if (input_line == ".help" || input_line == ".h") { + ctx->last_result = + "Nasal REPL commands:\n" + " .help .h show this help message\n" + " .clear .c clear screen\n" + " .exit .e exit repl\n" + " .quit .q exit repl\n" + " .source .s show source\n"; + return ctx->last_result.c_str(); + } + else if (input_line == ".clear" || input_line == ".c") { + ctx->last_result = "\033c"; // Special marker for clear screen + return ctx->last_result.c_str(); + } + else if (input_line == ".exit" || input_line == ".e" || + input_line == ".quit" || input_line == ".q") { + ctx->last_result = "__EXIT__"; // Special marker for exit + return ctx->last_result.c_str(); + } + else if (input_line == ".source" || input_line == ".s") { + // Return accumulated source + ctx->last_result = ctx->source.empty() ? + "(no source)" : + join_string(ctx->source, "\n"); + return ctx->last_result.c_str(); + } + else { + ctx->last_error = "no such command \"" + input_line + "\", input \".help\" for help"; + return ctx->last_error.c_str(); + } + } + + // Add the line to source + ctx->source.push_back(input_line); + + // Capture output + std::stringstream output; + auto old_cout = std::cout.rdbuf(output.rdbuf()); + auto old_cerr = std::cerr.rdbuf(output.rdbuf()); + + // Update source in repl instance and run + ctx->repl_instance->get_runtime().set_repl_mode_flag(true); + ctx->repl_instance->get_runtime().set_allow_repl_output_flag(true); + ctx->repl_instance->set_source(ctx->source); + bool success = ctx->repl_instance->run(); + + // Restore output streams + std::cout.rdbuf(old_cout); + std::cerr.rdbuf(old_cerr); + + // Get the output + std::string result = output.str(); + + if (!success) { + ctx->last_error = result; + ctx->source.pop_back(); // Remove failed line + return ctx->last_error.c_str(); + } + + // // Process output + // auto lines = split_string(result, '\n'); + // if (!lines.empty()) { + // // Remove empty lines from the end + // while (!lines.empty() && lines.back().empty()) { + // lines.pop_back(); + // } + // result = join_string(lines, "\n"); + // } + + ctx->last_result = result; + return ctx->last_result.c_str(); + + } catch (const std::exception& e) { + ctx->last_error = std::string("Error: ") + e.what(); + ctx->source.pop_back(); // Remove failed line + return ctx->last_error.c_str(); + } +} + +int nasal_repl_is_complete(void* context, const char* line) { + auto* ctx = static_cast(context); + std::string input_line(line); + + // Handle empty input or single semicolon + if (input_line.empty() || input_line == ";") { + return 1; // Input is complete + } + + // Add the new line to source + ctx->source.push_back(input_line); + + // Use existing REPL check_need_more_input functionality + bool needs_more = ctx->repl_instance->check_need_more_input(ctx->source); + + ctx->source.pop_back(); + return needs_more; +} + +// Add this function to expose version info +const char* nasal_repl_get_version() { + static std::string version_info = + std::string("version ") + __nasver__ + + " (" + __DATE__ + " " + __TIME__ + ")"; + return version_info.c_str(); +} From 40976bf0c1fa5a838918a1b95ea6165b80a44cb5 Mon Sep 17 00:00:00 2001 From: Sidi Liang <1467329765@qq.com> Date: Mon, 4 Nov 2024 12:04:31 +0800 Subject: [PATCH 09/20] [web] Added REPL support to the library --- src/nasal_web.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/nasal_web.h b/src/nasal_web.h index 63a734b..dd492b7 100644 --- a/src/nasal_web.h +++ b/src/nasal_web.h @@ -19,6 +19,13 @@ NASAL_EXPORT void nasal_cleanup(void* context); NASAL_EXPORT const char* nasal_eval(void* context, const char* code, int show_time); NASAL_EXPORT const char* nasal_get_error(void* context); +// REPL +NASAL_EXPORT void* nasal_repl_init(); +NASAL_EXPORT void nasal_repl_cleanup(void* repl_context); +NASAL_EXPORT const char* nasal_repl_eval(void* repl_context, const char* line); +NASAL_EXPORT int nasal_repl_is_complete(void* repl_context, const char* line); +NASAL_EXPORT const char* nasal_repl_get_version(); + #ifdef __cplusplus } #endif From 8dc06c085cdc83d6f02273775156bac615885d18 Mon Sep 17 00:00:00 2001 From: Sidi Liang <1467329765@qq.com> Date: Mon, 4 Nov 2024 20:46:31 +0800 Subject: [PATCH 10/20] [web] Make the temporary file name actually unique to handle concurrent requests --- src/nasal_web.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/nasal_web.cpp b/src/nasal_web.cpp index 771e325..c1325f8 100644 --- a/src/nasal_web.cpp +++ b/src/nasal_web.cpp @@ -84,7 +84,8 @@ const char* nasal_eval(void* context, const char* code, int show_time) { nasal::codegen gen; // Create a unique temporary file - char temp_filename[] = "/tmp/nasal_eval_XXXXXX.nasal"; + char temp_filename[256]; + snprintf(temp_filename, sizeof(temp_filename), "/tmp/nasal_eval_%d.nasal", getpid()); int fd = mkstemp(temp_filename); if (fd == -1) { throw std::runtime_error("Failed to create temporary file"); From 1ecd0a6912950d9a6c9d4f3f2a53bc6d175fe38f Mon Sep 17 00:00:00 2001 From: Sidi Liang <1467329765@qq.com> Date: Tue, 5 Nov 2024 01:24:18 +0800 Subject: [PATCH 11/20] [REPL] Modified to support web repl --- src/repl/repl.cpp | 38 +++++++++++++++++++++++++++++++++++++- src/repl/repl.h | 15 ++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/repl/repl.cpp b/src/repl/repl.cpp index b499d67..7970132 100644 --- a/src/repl/repl.cpp +++ b/src/repl/repl.cpp @@ -34,6 +34,14 @@ void repl::update_temp_file() { info::instance()->repl_file_source = content + " "; } +void repl::update_temp_file(const std::vector& src) { + auto content = std::string(""); + for(const auto& i : src) { + content += i + "\n"; + } + info::instance()->repl_file_source = content + " "; +} + bool repl::check_need_more_input() { while(true) { update_temp_file(); @@ -67,6 +75,34 @@ bool repl::check_need_more_input() { return true; } +int repl::check_need_more_input(std::vector& src) { + update_temp_file(src); + auto nasal_lexer = std::make_unique(); + if (nasal_lexer->scan("").geterr()) { + return -1; + } + + i64 in_curve = 0; + i64 in_bracket = 0; + i64 in_brace = 0; + for(const auto& t : nasal_lexer->result()) { + switch(t.type) { + case tok::tk_lcurve: ++in_curve; break; + case tok::tk_rcurve: --in_curve; break; + case tok::tk_lbracket: ++in_bracket; break; + case tok::tk_rbracket: --in_bracket; break; + case tok::tk_lbrace: ++in_brace; break; + case tok::tk_rbrace: --in_brace; break; + default: break; + } + } + if (in_curve > 0 || in_bracket > 0 || in_brace > 0) { + return 1; // More input needed + } + return 0; // Input is complete +} + + void repl::help() { std::cout << ".h, .help | show help\n"; std::cout << ".e, .exit | quit the REPL\n"; @@ -150,7 +186,7 @@ void repl::execute() { std::cout << "\", input \".help\" for help\n"; continue; } - + source.push_back(line); if (!check_need_more_input()) { source.pop_back(); diff --git a/src/repl/repl.h b/src/repl/repl.h index e38ec3f..d9f844b 100644 --- a/src/repl/repl.h +++ b/src/repl/repl.h @@ -36,8 +36,8 @@ private: std::string readline(const std::string&); bool check_need_more_input(); void update_temp_file(); + void update_temp_file(const std::vector& src); void help(); - bool run(); public: repl() { @@ -48,7 +48,20 @@ public: // set empty history command_history = {""}; } + + // Make these methods public for web REPL + bool run(); void execute(); + int check_need_more_input(std::vector& src); + // Add method to access source + void set_source(const std::vector& src) { + source = src; + } + + // Add method to access runtime + vm& get_runtime() { + return runtime; + } }; } From 068da2fb41534717647d6f912271375dd1ae322f Mon Sep 17 00:00:00 2001 From: Sidi Liang <1467329765@qq.com> Date: Tue, 5 Nov 2024 01:24:47 +0800 Subject: [PATCH 12/20] [web] Support multiline mode in web REPL --- src/nasal_web.cpp | 43 ++++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/src/nasal_web.cpp b/src/nasal_web.cpp index c1325f8..a88e915 100644 --- a/src/nasal_web.cpp +++ b/src/nasal_web.cpp @@ -274,16 +274,6 @@ const char* nasal_repl_eval(void* context, const char* line) { return ctx->last_error.c_str(); } - // // Process output - // auto lines = split_string(result, '\n'); - // if (!lines.empty()) { - // // Remove empty lines from the end - // while (!lines.empty() && lines.back().empty()) { - // lines.pop_back(); - // } - // result = join_string(lines, "\n"); - // } - ctx->last_result = result; return ctx->last_result.c_str(); @@ -296,21 +286,28 @@ const char* nasal_repl_eval(void* context, const char* line) { int nasal_repl_is_complete(void* context, const char* line) { auto* ctx = static_cast(context); - std::string input_line(line); - // Handle empty input or single semicolon - if (input_line.empty() || input_line == ";") { - return 1; // Input is complete + if (!ctx->initialized) { + return -1; // Error state } - - // Add the new line to source - ctx->source.push_back(input_line); - - // Use existing REPL check_need_more_input functionality - bool needs_more = ctx->repl_instance->check_need_more_input(ctx->source); - - ctx->source.pop_back(); - return needs_more; + + // Handle empty input + if (!line || strlen(line) == 0) { + return 0; // Complete + } + + // Handle REPL commands + if (line[0] == '.') { + return 0; // Commands are always complete + } + + // Create a temporary source vector with existing source plus new line + std::vector temp_source = ctx->source; + temp_source.push_back(line); + + // Use the REPL's check_need_more_input method + int result = ctx->repl_instance->check_need_more_input(temp_source); + return result; // Ensure a return value is provided } // Add this function to expose version info From e6b88c9dec881382f457d5576011ea54e3d31a09 Mon Sep 17 00:00:00 2001 From: Sidi Liang <1467329765@qq.com> Date: Tue, 5 Nov 2024 01:25:09 +0800 Subject: [PATCH 13/20] [web] Added Web REPL server and frontend demo --- nasal-web-app/public/repl.html | 240 +++++++++++++++++++++++++++++++ nasal-web-app/public/repl.js | 249 +++++++++++++++++++++++++++++++++ nasal-web-app/server_repl.js | 176 +++++++++++++++++++++++ 3 files changed, 665 insertions(+) create mode 100644 nasal-web-app/public/repl.html create mode 100644 nasal-web-app/public/repl.js create mode 100644 nasal-web-app/server_repl.js diff --git a/nasal-web-app/public/repl.html b/nasal-web-app/public/repl.html new file mode 100644 index 0000000..9481424 --- /dev/null +++ b/nasal-web-app/public/repl.html @@ -0,0 +1,240 @@ + + + + + + Nasal Interpreter Web REPL + + + + + +
+
+

Nasal Interpreter Web REPL

+

Interactive Read-Eval-Print Loop for Nasal

+

Powered by ValKmjolnir's Nasal Interpreter, Web App by LIANG Sidi

+
+ +
+
+
Welcome to Nasal Web REPL Demo!
+
+
+ >>> + +
+
+ +
+ +
+ +
+

Example Commands:

+ + + +
+
+ + + + \ No newline at end of file diff --git a/nasal-web-app/public/repl.js b/nasal-web-app/public/repl.js new file mode 100644 index 0000000..742120e --- /dev/null +++ b/nasal-web-app/public/repl.js @@ -0,0 +1,249 @@ +let replSessionId = null; +let multilineInput = []; +let historyIndex = -1; +let commandHistory = []; +let multilineBuffer = []; +let isMultilineMode = false; + +// Initialize REPL +async function initRepl() { + try { + const response = await fetch('/repl/init', { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + const data = await response.json(); + if (data.sessionId) { + replSessionId = data.sessionId; + console.log('REPL session initialized:', replSessionId); + + // Display version info + appendOutput('Nasal REPL interpreter version ' + data.version); + appendOutput('Type your code below and press Enter to execute.'); + appendOutput('Use Shift+Enter for multiline input.\n'); + showPrompt(); + } else { + throw new Error('Failed to initialize REPL session'); + } + } catch (err) { + appendOutput(`Error: ${err.message}`, 'error-line'); + } +} + +// Format error messages to match command-line REPL +function formatError(error) { + // Split the error message into lines + const lines = error.split('\n'); + return lines.map(line => { + // Add appropriate indentation for the error pointer + if (line.includes('-->')) { + return ' ' + line; + } else if (line.includes('^')) { + return ' ' + line; + } + return line; + }).join('\n'); +} + +// Handle input +const input = document.getElementById('repl-input'); +input.addEventListener('keydown', async (e) => { + if (e.key === 'Enter') { + if (e.shiftKey) { + // Shift+Enter: add newline + const pos = input.selectionStart; + const value = input.value; + input.value = value.substring(0, pos) + '\n' + value.substring(pos); + input.selectionStart = input.selectionEnd = pos + 1; + input.style.height = 'auto'; + input.style.height = input.scrollHeight + 'px'; + e.preventDefault(); + return; + } + e.preventDefault(); + + const code = input.value.trim(); + + // Skip empty lines but still show prompt + if (!code) { + showPrompt(isMultilineMode ? '... ' : '>>> '); + return; + } + + try { + // First check if input is complete + const checkResponse = await fetch('/repl/eval', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sessionId: replSessionId, + line: code, + check: true, + buffer: multilineBuffer // Send existing buffer + }) + }); + + const checkData = await checkResponse.json(); + + if (checkData.needsMore) { + // Add to multiline buffer and show continuation prompt + multilineBuffer.push(code); + isMultilineMode = true; + + // Display the input with continuation prompt + appendOutput(code, 'input-line', multilineBuffer.length === 1 ? '>>> ' : '... '); + + input.value = ''; + showPrompt('... '); + return; + } + + // If we were in multiline mode, add the final line + if (isMultilineMode) { + multilineBuffer.push(code); + } + + // Get the complete code to evaluate + const fullCode = isMultilineMode ? + multilineBuffer.join('\n') : code; + + // Display the input + appendOutput(code, 'input-line', isMultilineMode ? '... ' : '>>> '); + + // Evaluate the code + const response = await fetch('/repl/eval', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sessionId: replSessionId, + line: fullCode + }) + }); + + const data = await response.json(); + + if (data.error) { + appendOutput(formatError(data.error.trim()), 'error-line'); + } else if (data.result) { + handleResult(data.result); + } + + // Reset multiline state + multilineBuffer = []; + isMultilineMode = false; + input.value = ''; + showPrompt('>>> '); + + } catch (err) { + appendOutput(`Error: ${err.message}`, 'error-line'); + multilineBuffer = []; + isMultilineMode = false; + input.value = ''; + showPrompt('>>> '); + } + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + if (historyIndex > 0) { + historyIndex--; + input.value = commandHistory[historyIndex]; + } + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + if (historyIndex < commandHistory.length - 1) { + historyIndex++; + input.value = commandHistory[historyIndex]; + } else { + historyIndex = commandHistory.length; + input.value = ''; + } + } +}); + +// Auto-resize input +input.addEventListener('input', () => { + input.style.height = 'auto'; + input.style.height = input.scrollHeight + 'px'; +}); + +// Show prompt and scroll to bottom +function showPrompt(prompt = '>>> ') { + const promptSpan = document.querySelector('.repl-prompt'); + if (promptSpan) { + promptSpan.textContent = prompt; + } +} + +// Append output to REPL +function appendOutput(text, className = '', prefix = '') { + const output = document.getElementById('repl-output'); + const line = document.createElement('div'); + line.className = `output-line ${className}`; + + line.innerHTML = prefix + formatErrorMessage(text); + + + output.appendChild(line); + output.scrollTop = output.scrollHeight; +} + +// Clear REPL +function clearREPL() { + const output = document.getElementById('repl-output'); + output.innerHTML = ''; + appendOutput('Screen cleared', 'system-message'); + showPrompt(); +} + +// Example snippets +const examples = { + basic: `var x = 1011 + 1013; +println("x = ", x);`, + + loops: `var sum = 0; +for(var i = 1; i <= 5; i += 1) { + sum += i; +} +println("Sum:", sum);`, + + functions: `var factorial = func(n) { + if (n <= 1) return 1; + return n * factorial(n - 1); +}; +factorial(5);` +}; + +// Insert example into input +function insertExample(type) { + input.value = examples[type]; + input.style.height = 'auto'; + input.style.height = input.scrollHeight + 'px'; + input.focus(); +} + +// Initialize REPL on page load +window.addEventListener('load', initRepl); + +// Add these utility functions +function formatErrorMessage(text) { + // Replace ANSI escape codes with CSS classes + return text + // Remove any existing formatting first + .replace(/\u001b\[\d+(?:;\d+)*m/g, '') + // Format the error line + .replace(/^parse: (.+)$/m, 'parse: $1') + // Format the file location + .replace(/^\s*--> (.+?)(\d+):(\d+)$/m, '--> $1$2:$3') + // Format the code line + .replace(/^(\d+ \|)(.*)$/m, '$1$2') + // Format the error pointer + .replace(/^(\s*\|)(\s*)(\^+)$/m, '$1$2$3'); +} + +function handleResult(result) { + const lines = result.split('\n'); + lines.forEach(line => { + if (line.trim()) { + appendOutput(line.trim(), 'result-line'); + } + }); +} \ No newline at end of file diff --git a/nasal-web-app/server_repl.js b/nasal-web-app/server_repl.js new file mode 100644 index 0000000..76153a3 --- /dev/null +++ b/nasal-web-app/server_repl.js @@ -0,0 +1,176 @@ +const express = require('express'); +const ffi = require('ffi-napi'); +const path = require('path'); +const yargs = require('yargs/yargs'); +const { hideBin } = require('yargs/helpers'); + +// Parse command line arguments +const argv = yargs(hideBin(process.argv)) + .usage('Usage: $0 [options]') + .option('verbose', { + alias: 'v', + type: 'boolean', + description: 'Run with verbose logging' + }) + .option('port', { + alias: 'p', + type: 'number', + description: 'Port to run the server on', + default: 3001 + }) + .option('host', { + type: 'string', + description: 'Host to run the server on', + default: 'localhost' + }) + .help() + .alias('help', 'h') + .version() + .argv; + +const app = express(); +app.use(express.json()); +app.use(express.static('public')); + + +// Load Nasal REPL library functions +const nasalLib = ffi.Library(path.join(__dirname, '../module/libnasal-web'), { + 'nasal_repl_init': ['pointer', []], + 'nasal_repl_cleanup': ['void', ['pointer']], + 'nasal_repl_eval': ['string', ['pointer', 'string']], + 'nasal_repl_is_complete': ['int', ['pointer', 'string']], + 'nasal_repl_get_version': ['string', []], +}); + +// Store active REPL sessions +const replSessions = new Map(); + +// Clean up inactive sessions periodically (30 minutes timeout) +const SESSION_TIMEOUT = 30 * 60 * 1000; +setInterval(() => { + const now = Date.now(); + for (const [sessionId, session] of replSessions.entries()) { + if (now - session.lastAccess > SESSION_TIMEOUT) { + if (argv.verbose) { + console.log(`Cleaning up inactive session: ${sessionId}`); + } + nasalLib.nasal_repl_cleanup(session.context); + replSessions.delete(sessionId); + } + } +}, 60000); // Check every minute + +app.post('/repl/init', (req, res) => { + try { + const ctx = nasalLib.nasal_repl_init(); + const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const version = nasalLib.nasal_repl_get_version(); + + replSessions.set(sessionId, { + context: ctx, + lastAccess: Date.now() + }); + + if (argv.verbose) { + console.log(`New REPL session initialized: ${sessionId}`); + } + + res.json({ + sessionId, + version + }); + } catch (err) { + if (argv.verbose) { + console.error('Failed to initialize REPL session:', err); + } + res.status(500).json({ error: 'Failed to initialize REPL session' }); + } +}); + +app.post('/repl/eval', (req, res) => { + const { sessionId, line, check, buffer } = req.body; + + if (!sessionId || !replSessions.has(sessionId)) { + return res.status(400).json({ error: 'Invalid or expired session' }); + } + + if (!line) { + return res.status(400).json({ error: 'No code provided' }); + } + + try { + const session = replSessions.get(sessionId); + session.lastAccess = Date.now(); + + if (check) { + const codeToCheck = buffer ? [...buffer, line].join('\n') : line; + const isComplete = nasalLib.nasal_repl_is_complete(session.context, codeToCheck); + + if (isComplete === 1) { + return res.json({ needsMore: true }); + } else if (isComplete === -1) { + return res.json({ error: 'Invalid input' }); + } + } + + const result = nasalLib.nasal_repl_eval(session.context, line); + + if (argv.verbose) { + console.log(`REPL evaluation for session ${sessionId}:`, { line, result }); + } + + res.json({ result }); + } catch (err) { + if (argv.verbose) { + console.error(`REPL evaluation error for session ${sessionId}:`, err); + } + res.status(500).json({ error: err.message }); + } +}); + +app.post('/repl/cleanup', (req, res) => { + const { sessionId } = req.body; + + if (!sessionId || !replSessions.has(sessionId)) { + return res.status(400).json({ error: 'Invalid session' }); + } + + try { + const session = replSessions.get(sessionId); + nasalLib.nasal_repl_cleanup(session.context); + replSessions.delete(sessionId); + + if (argv.verbose) { + console.log(`REPL session cleaned up: ${sessionId}`); + } + + res.json({ message: 'Session cleaned up successfully' }); + } catch (err) { + if (argv.verbose) { + console.error(`Failed to cleanup session ${sessionId}:`, err); + } + res.status(500).json({ error: err.message }); + } +}); + +// Handle cleanup on server shutdown +process.on('SIGINT', () => { + console.log('\nCleaning up REPL sessions before exit...'); + for (const [sessionId, session] of replSessions.entries()) { + try { + nasalLib.nasal_repl_cleanup(session.context); + if (argv.verbose) { + console.log(`Cleaned up session: ${sessionId}`); + } + } catch (err) { + console.error(`Error cleaning up session ${sessionId}:`, err); + } + } + process.exit(0); +}); + +const PORT = argv.port || 3001; +app.listen(PORT, () => { + console.log(`REPL Server running on http://localhost:${PORT}`); + if (argv.verbose) console.log('Verbose logging enabled'); +}); \ No newline at end of file From 44e7077f46f74ccd7f17266526d49b0eba0d4e3a Mon Sep 17 00:00:00 2001 From: Sidi Liang <1467329765@qq.com> Date: Tue, 5 Nov 2024 22:40:01 +0800 Subject: [PATCH 14/20] [web] Added execution time limit for we b app --- src/nasal_web.cpp | 25 +++++++++++++++++++++++-- src/nasal_web.h | 1 + 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/nasal_web.cpp b/src/nasal_web.cpp index a88e915..fbc6ceb 100644 --- a/src/nasal_web.cpp +++ b/src/nasal_web.cpp @@ -16,6 +16,7 @@ #include #include #include +#include namespace { // Helper function implementations inside anonymous namespace @@ -44,6 +45,7 @@ struct NasalContext { std::unique_ptr vm_instance; std::string last_result; std::string last_error; + std::chrono::seconds timeout{5}; // Default 5 second timeout NasalContext() { vm_instance = std::make_unique(); @@ -71,6 +73,12 @@ void nasal_cleanup(void* context) { delete static_cast(context); } +// Add new function to set timeout +void nasal_set_timeout(void* context, int seconds) { + auto* ctx = static_cast(context); + ctx->timeout = std::chrono::seconds(seconds); +} + const char* nasal_eval(void* context, const char* code, int show_time) { using clk = std::chrono::high_resolution_clock; const auto den = clk::duration::period::den; @@ -85,7 +93,7 @@ const char* nasal_eval(void* context, const char* code, int show_time) { // Create a unique temporary file char temp_filename[256]; - snprintf(temp_filename, sizeof(temp_filename), "/tmp/nasal_eval_%d.nasal", getpid()); + snprintf(temp_filename, sizeof(temp_filename), "/tmp/nasal_eval_%ld.nasal", std::time(nullptr)); int fd = mkstemp(temp_filename); if (fd == -1) { throw std::runtime_error("Failed to create temporary file"); @@ -130,7 +138,20 @@ const char* nasal_eval(void* context, const char* code, int show_time) { gen.compile(parse, ld, false, true).chkerr(); const auto start = show_time ? clk::now() : clk::time_point(); - ctx->vm_instance->run(gen, ld, {}); + + // Create a future for the VM execution + auto future = std::async(std::launch::async, [&]() { + ctx->vm_instance->run(gen, ld, {}); + }); + + // Wait for completion or timeout + auto status = future.wait_for(ctx->timeout); + if (status == std::future_status::timeout) { + std::remove(temp_filename); + throw std::runtime_error("Execution timed out after " + + std::to_string(ctx->timeout.count()) + " seconds"); + } + const auto end = show_time ? clk::now() : clk::time_point(); std::cout.rdbuf(old_cout); diff --git a/src/nasal_web.h b/src/nasal_web.h index dd492b7..ad4cf14 100644 --- a/src/nasal_web.h +++ b/src/nasal_web.h @@ -16,6 +16,7 @@ extern "C" { // Main API functions NASAL_EXPORT void* nasal_init(); NASAL_EXPORT void nasal_cleanup(void* context); +NASAL_EXPORT void nasal_set_timeout(void* context, int seconds); NASAL_EXPORT const char* nasal_eval(void* context, const char* code, int show_time); NASAL_EXPORT const char* nasal_get_error(void* context); From d3d204a7046f4a70422f8f86b4c338fea9e6d6c6 Mon Sep 17 00:00:00 2001 From: Sidi Liang <1467329765@qq.com> Date: Wed, 6 Nov 2024 23:44:22 +0800 Subject: [PATCH 15/20] [web] Switch from ffi-napi to koffi --- nasal-web-app/server.js | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/nasal-web-app/server.js b/nasal-web-app/server.js index 18f3e17..d0784c2 100644 --- a/nasal-web-app/server.js +++ b/nasal-web-app/server.js @@ -1,8 +1,8 @@ const express = require('express'); -const ffi = require('ffi-napi'); const path = require('path'); const yargs = require('yargs/yargs'); const { hideBin } = require('yargs/helpers'); +const koffi = require('koffi'); // Parse command line arguments const argv = yargs(hideBin(process.argv)) @@ -33,12 +33,23 @@ const app = express(); app.use(express.json()); app.use(express.static('public')); -const nasalLib = ffi.Library(path.join(__dirname, '../module/libnasal-web'), { - 'nasal_init': ['pointer', []], - 'nasal_cleanup': ['void', ['pointer']], - 'nasal_eval': ['string', ['pointer', 'string', 'int']], - 'nasal_get_error': ['string', ['pointer']] -}); +let nasalLib; +try { + // First load the library + const lib = koffi.load(path.join(__dirname, '../module/libnasal-web.dylib')); + + // Then declare the functions explicitly + nasalLib = { + nasal_init: lib.func('nasal_init', 'void*', []), + nasal_cleanup: lib.func('nasal_cleanup', 'void', ['void*']), + nasal_eval: lib.func('nasal_eval', 'const char*', ['void*', 'const char*', 'int']), + nasal_get_error: lib.func('nasal_get_error', 'const char*', ['void*']) + }; + +} catch (err) { + console.error('Failed to load nasal library:', err); + process.exit(1); +} app.post('/eval', (req, res) => { const { code, showTime = false } = req.body; From eae25eeb9a056a0eb1702d7318535837817de504 Mon Sep 17 00:00:00 2001 From: Sidi Liang <1467329765@qq.com> Date: Thu, 7 Nov 2024 11:30:54 +0800 Subject: [PATCH 16/20] [web] Fixed failures when creating tmp files on linxu --- src/nasal_web.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nasal_web.cpp b/src/nasal_web.cpp index fbc6ceb..889e5a0 100644 --- a/src/nasal_web.cpp +++ b/src/nasal_web.cpp @@ -93,7 +93,7 @@ const char* nasal_eval(void* context, const char* code, int show_time) { // Create a unique temporary file char temp_filename[256]; - snprintf(temp_filename, sizeof(temp_filename), "/tmp/nasal_eval_%ld.nasal", std::time(nullptr)); + snprintf(temp_filename, sizeof(temp_filename), "/tmp/nasal_eval_%ld_XXXXXX", std::time(nullptr)); int fd = mkstemp(temp_filename); if (fd == -1) { throw std::runtime_error("Failed to create temporary file"); From afdaac96708ee8e071706ce256aa7ae40437c033 Mon Sep 17 00:00:00 2001 From: Sidi Liang <1467329765@qq.com> Date: Fri, 8 Nov 2024 11:50:14 +0800 Subject: [PATCH 17/20] [web] Switched REPL server demo to koffi --- nasal-web-app/server_repl.js | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/nasal-web-app/server_repl.js b/nasal-web-app/server_repl.js index 76153a3..5847dfb 100644 --- a/nasal-web-app/server_repl.js +++ b/nasal-web-app/server_repl.js @@ -1,8 +1,8 @@ const express = require('express'); -const ffi = require('ffi-napi'); const path = require('path'); const yargs = require('yargs/yargs'); const { hideBin } = require('yargs/helpers'); +const koffi = require('koffi'); // Parse command line arguments const argv = yargs(hideBin(process.argv)) @@ -34,13 +34,25 @@ app.use(express.static('public')); // Load Nasal REPL library functions -const nasalLib = ffi.Library(path.join(__dirname, '../module/libnasal-web'), { - 'nasal_repl_init': ['pointer', []], - 'nasal_repl_cleanup': ['void', ['pointer']], - 'nasal_repl_eval': ['string', ['pointer', 'string']], - 'nasal_repl_is_complete': ['int', ['pointer', 'string']], - 'nasal_repl_get_version': ['string', []], -}); +let nasalLib; +try { + const lib = koffi.load(path.join(__dirname, '../module/libnasal-web.dylib')); + + nasalLib = { + nasal_repl_init: lib.func('nasal_repl_init', 'void*', []), + nasal_repl_cleanup: lib.func('nasal_repl_cleanup', 'void', ['void*']), + nasal_repl_eval: lib.func('nasal_repl_eval', 'const char*', ['void*', 'const char*']), + nasal_repl_is_complete: lib.func('nasal_repl_is_complete', 'int', ['void*', 'const char*']), + nasal_repl_get_version: lib.func('nasal_repl_get_version', 'const char*', []) + }; + + if (argv.verbose) { + console.log('REPL Library loaded successfully'); + } +} catch (err) { + console.error('Failed to load REPL library:', err); + process.exit(1); +} // Store active REPL sessions const replSessions = new Map(); From 8e7074fd2500868e60d2944f474740a140d56f0a Mon Sep 17 00:00:00 2001 From: Sidi Liang <1467329765@qq.com> Date: Sat, 9 Nov 2024 13:40:45 +0800 Subject: [PATCH 18/20] [web] Attempt to add timeouts for REPL (not working properly, it works in some cases but causes a segfault, and in other cases the timeouts were ignored) --- src/nasal_web.cpp | 41 ++++++++++++++++++++++++++++++++++------- src/nasal_web.h | 1 + 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/nasal_web.cpp b/src/nasal_web.cpp index 889e5a0..67b5c2f 100644 --- a/src/nasal_web.cpp +++ b/src/nasal_web.cpp @@ -59,6 +59,7 @@ struct WebReplContext { std::string last_error; bool allow_output; bool initialized; + std::chrono::seconds timeout{1}; // Default 1 second timeout WebReplContext() : allow_output(false), initialized(false) { repl_instance = std::make_unique(); @@ -217,6 +218,12 @@ void nasal_repl_cleanup(void* context) { delete static_cast(context); } +// Add new function to set REPL timeout +void nasal_repl_set_timeout(void* context, int seconds) { + auto* ctx = static_cast(context); + ctx->timeout = std::chrono::seconds(seconds); +} + const char* nasal_repl_eval(void* context, const char* line) { auto* ctx = static_cast(context); @@ -276,17 +283,37 @@ const char* nasal_repl_eval(void* context, const char* line) { auto old_cout = std::cout.rdbuf(output.rdbuf()); auto old_cerr = std::cerr.rdbuf(output.rdbuf()); - // Update source in repl instance and run - ctx->repl_instance->get_runtime().set_repl_mode_flag(true); - ctx->repl_instance->get_runtime().set_allow_repl_output_flag(true); - ctx->repl_instance->set_source(ctx->source); - bool success = ctx->repl_instance->run(); + // Create a copy of the source for the async task + auto source_copy = ctx->source; - // Restore output streams + // Create a future for the REPL execution using the existing instance + auto future = std::async(std::launch::async, [repl = ctx->repl_instance.get(), source_copy]() { + repl->get_runtime().set_repl_mode_flag(true); + repl->get_runtime().set_allow_repl_output_flag(true); + repl->set_source(source_copy); + return repl->run(); + }); + + // Wait for completion or timeout + auto status = future.wait_for(ctx->timeout); + + // Restore output streams first std::cout.rdbuf(old_cout); std::cerr.rdbuf(old_cerr); - // Get the output + if (status == std::future_status::timeout) { + ctx->source.pop_back(); // Remove the line that caused timeout + + // Reset the REPL instance state + ctx->repl_instance->get_runtime().set_repl_mode_flag(true); + ctx->repl_instance->get_runtime().set_allow_repl_output_flag(true); + ctx->repl_instance->set_source(ctx->source); + + throw std::runtime_error("Execution timed out after " + + std::to_string(ctx->timeout.count()) + " seconds"); + } + + bool success = future.get(); std::string result = output.str(); if (!success) { diff --git a/src/nasal_web.h b/src/nasal_web.h index ad4cf14..3bc7d98 100644 --- a/src/nasal_web.h +++ b/src/nasal_web.h @@ -23,6 +23,7 @@ NASAL_EXPORT const char* nasal_get_error(void* context); // REPL NASAL_EXPORT void* nasal_repl_init(); NASAL_EXPORT void nasal_repl_cleanup(void* repl_context); +NASAL_EXPORT void nasal_repl_set_timeout(void* repl_context, int seconds); NASAL_EXPORT const char* nasal_repl_eval(void* repl_context, const char* line); NASAL_EXPORT int nasal_repl_is_complete(void* repl_context, const char* line); NASAL_EXPORT const char* nasal_repl_get_version(); From c8869080c64addaceade9f5054112ad22e4ee95a Mon Sep 17 00:00:00 2001 From: Sidi Liang <1467329765@qq.com> Date: Sat, 9 Nov 2024 14:14:45 +0800 Subject: [PATCH 19/20] [web] Updated README.md --- README.md | 55 ++++++++++++++++++++++++++++++++++++++++---- doc/README_zh.md | 59 +++++++++++++++++++++++++++++++++++++++++++++--- package.json | 1 + 3 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 package.json diff --git a/README.md b/README.md index 78d0b99..4145a47 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ * [__Trace Back Info__](#trace-back-info) * [__Debugger__](#debugger) * [__REPL__](#repl) +* [__Web Interface__](#web-interface) __Contact us if having great ideas to share!__ @@ -121,7 +122,7 @@ runtime.windows.set_utf8_output(); ![error](./doc/gif/error.gif) -
Must use `var` to define variables +
Must use `var` to define variables This interpreter uses more strict syntax to make sure it is easier for you to program and debug. And flightgear's nasal interpreter also has the same rule. @@ -146,13 +147,13 @@ If you forget to add the keyword `var`, you will get this: ```javascript code: undefined symbol "i" --> test.nas:1:9 - | + | 1 | foreach(i; [0, 1, 2, 3]) | ^ undefined symbol "i" code: undefined symbol "i" --> test.nas:2:11 - | + | 2 | print(i) | ^ undefined symbol "i" ``` @@ -441,5 +442,51 @@ Nasal REPL interpreter version 11.1 (Nov 1 2023 23:37:30) >>> use std.json; {stringify:func(..) {..},parse:func(..) {..}} ->>> +>>> ``` + +## __Web Interface__ + +A web-based interface is now available for trying out Nasal code directly in your browser. It includes both a code editor and an interactive REPL (WIP). + +### Web Code Editor +- Syntax highlighting using CodeMirror +- Error highlighting and formatting +- Example programs +- Execution time display option +- Configurable execution time limits +- Notice: The security of the online interpreter is not well tested, please use it with sandbox mechanism or other security measures. + +### Web REPL +- ** IMPORTANT: The time limit in REPL is not correctly implemented yet. Thus this REPL web binding is not considered finished. Do not use it in production before it's fixed. ** +- Interactive command-line style interface in browser +- Multi-line input support with proper prompts (`>>>` and `...`) +- Command history navigation +- Error handling with formatted error messages +- Example snippets for quick testing + +### Running the Web Interface + +1. Build the Nasal shared library: +```bash +cmake -DBUILD_SHARED_LIBS=ON . +make nasal-web +``` + +2. Set up and run the web application: + +For the code editor: +```bash +cd nasal-web-app +npm install +node server.js +``` +Visit `http://127.0.0.1:3000/` + +For the REPL: +```bash +cd nasal-web-app +npm install +node server_repl.js +``` +Visit `http://127.0.0.1:3001/repl.html` diff --git a/doc/README_zh.md b/doc/README_zh.md index e77f655..77c47c8 100644 --- a/doc/README_zh.md +++ b/doc/README_zh.md @@ -26,9 +26,8 @@ __如果有好的意见或建议,欢迎联系我们!__ -* __lhk101lhk101@qq.com__ (ValKmjolnir) - -* __sidi.liang@gmail.com__ (Sidi) +- __lhk101lhk101@qq.com__ (ValKmjolnir) +- __sidi.liang@gmail.com__ (Sidi) ## __简介__ @@ -428,3 +427,57 @@ Nasal REPL interpreter version 11.1 (Nov 1 2023 23:37:30) >>> ``` +## __Web 界面__ + +现已提供基于 Web 的库以及示例界面,您可以直接在浏览器中编写和运行 Nasal 代码。该界面包括代码编辑器和交互式 REPL(未完成)。 + +### __Web 代码编辑器__ + +- **语法高亮:** 使用 CodeMirror 提供增强的编码体验。 +- **错误高亮和格式化:** 清晰显示语法和运行时错误。 +- **示例程序:** 预加载的示例,帮助您快速上手。 +- **执行时间显示选项:** 可选择查看代码执行所需时间。 +- **可配置的执行时间限制:** 设置时间限制以防止代码长时间运行。 +- **提示:** 在线解释器的安全性尚未得到广泛测试,建议配合沙盒机制等安全措施使用。 + +### __Web REPL__ + +- **重要提示:** REPL 中的代码执行时间限制尚未正确实现。此 REPL 库目前不稳定,请勿在生产环境中使用。 +- **交互式命令行界面:** 在浏览器中体验熟悉的 REPL 环境。 +- **多行输入支持:** 使用 `>>>` 和 `...` 提示符无缝输入多行代码。 +- **命令历史导航:** 使用箭头键轻松浏览命令历史。 +- **格式化的错误处理:** 接收清晰且格式化的错误消息,助力调试。 +- **快速测试的示例代码片段:** 访问并运行示例代码片段,快速测试功能。 + +### __运行 Web 界面__ + +1. **构建 Nasal 共享库:** + + ```bash + cmake -DBUILD_SHARED_LIBS=ON . + make nasal-web + ``` + +2. **设置并运行 Web 应用:** + + **代码编辑器:** + + ```bash + cd nasal-web-app + npm install + node server.js + ``` + + 在浏览器中访问 `http://127.0.0.1:3000/` 以使用代码编辑器。 + + **REPL:** + + ```bash + cd nasal-web-app + npm install + node server_repl.js + ``` + + 在浏览器中访问 `http://127.0.0.1:3001/repl.html` 以使用 REPL 界面。 + +--- diff --git a/package.json b/package.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/package.json @@ -0,0 +1 @@ +{} From ebca28d0aace5faf01a11c11a955f00991ed2cc7 Mon Sep 17 00:00:00 2001 From: LIANG Sidi <1467329765@qq.com> Date: Sat, 9 Nov 2024 14:27:58 +0800 Subject: [PATCH 20/20] Delete package.json --- package.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 package.json diff --git a/package.json b/package.json deleted file mode 100644 index 0967ef4..0000000 --- a/package.json +++ /dev/null @@ -1 +0,0 @@ -{}