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 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/README.md b/README.md index e5e29cc..027b5ed 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!__ @@ -122,7 +123,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. @@ -147,13 +148,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" ``` @@ -442,5 +443,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 ea6a5e8..9a64b6b 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) ## __简介__ @@ -429,3 +428,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/nasal-web-app/package.json b/nasal-web-app/package.json new file mode 100644 index 0000000..b0483e1 --- /dev/null +++ b/nasal-web-app/package.json @@ -0,0 +1,17 @@ +{ + "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", + "yargs": "^17.7.2" + } +} diff --git a/nasal-web-app/public/index.html b/nasal-web-app/public/index.html new file mode 100644 index 0000000..26dc8e4 --- /dev/null +++ b/nasal-web-app/public/index.html @@ -0,0 +1,638 @@ + + + + + + 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/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.js b/nasal-web-app/server.js new file mode 100644 index 0000000..d0784c2 --- /dev/null +++ b/nasal-web-app/server.js @@ -0,0 +1,94 @@ +const express = require('express'); +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)) + .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(); + +app.use(express.json()); +app.use(express.static('public')); + +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; + 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, showTime ? 1 : 0); + const error = nasalLib.nasal_get_error(ctx); + + if (error && error !== 'null') { + if (argv.verbose) console.log('Nasal error:', error); + res.json({ error: error }); + } else if (result && result.trim() !== '') { + 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) { + 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 = 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 diff --git a/nasal-web-app/server_repl.js b/nasal-web-app/server_repl.js new file mode 100644 index 0000000..5847dfb --- /dev/null +++ b/nasal-web-app/server_repl.js @@ -0,0 +1,188 @@ +const express = require('express'); +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)) + .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 +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(); + +// 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 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 diff --git a/src/nasal_web.cpp b/src/nasal_web.cpp new file mode 100644 index 0000000..67b5c2f --- /dev/null +++ b/src/nasal_web.cpp @@ -0,0 +1,367 @@ +#include "nasal_web.h" +#include "nasal_vm.h" +#include "nasal_parse.h" +#include "nasal_codegen.h" +#include "nasal_import.h" +#include "optimizer.h" +#include "nasal_err.h" +#include "nasal_lexer.h" +#include "repl/repl.h" + +#include +#include +#include +#include +#include +#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; + std::string last_result; + std::string last_error; + std::chrono::seconds timeout{5}; // Default 5 second timeout + + NasalContext() { + vm_instance = std::make_unique(); + } +}; + +struct WebReplContext { + std::unique_ptr repl_instance; + std::vector source; + std::string last_result; + 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(); + } +}; + +void* nasal_init() { + return new NasalContext(); +} + +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; + + 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[256]; + 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"); + } + + // 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); + + // Capture stdout and stderr + std::stringstream output; + std::stringstream error_output; + auto old_cout = std::cout.rdbuf(output.rdbuf()); + auto old_cerr = std::cerr.rdbuf(error_output.rdbuf()); + + // 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); + 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); + std::remove(temp_filename); + return ctx->last_error.c_str(); + } + + ld.link(parse, false).chkerr(); + auto opt = std::make_unique(); + opt->do_optimization(parse.tree()); + gen.compile(parse, ld, false, true).chkerr(); + + const auto start = show_time ? clk::now() : clk::time_point(); + + // 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); + std::cerr.rdbuf(old_cerr); + + std::stringstream result; + result << output.str(); + if (!error_output.str().empty()) { + result << error_output.str(); + } + if (result.str().empty()) { + result << "Execution completed successfully.\n"; + } + + if (show_time) { + double execution_time = static_cast((end-start).count())/den; + result << "\nExecution time: " << execution_time << "s"; + } + + ctx->last_result = result.str(); + 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(); +} + +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); +} + +// 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); + + 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()); + + // Create a copy of the source for the async task + auto source_copy = ctx->source; + + // 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); + + 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) { + ctx->last_error = result; + ctx->source.pop_back(); // Remove failed line + return ctx->last_error.c_str(); + } + + 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); + + if (!ctx->initialized) { + return -1; // Error state + } + + // 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 +const char* nasal_repl_get_version() { + static std::string version_info = + std::string("version ") + __nasver__ + + " (" + __DATE__ + " " + __TIME__ + ")"; + return version_info.c_str(); +} diff --git a/src/nasal_web.h b/src/nasal_web.h new file mode 100644 index 0000000..3bc7d98 --- /dev/null +++ b/src/nasal_web.h @@ -0,0 +1,35 @@ +#ifndef __NASAL_WEB_H__ +#define __NASAL_WEB_H__ + +#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 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); + +// 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(); + +#ifdef __cplusplus +} +#endif + +#endif 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; + } }; }