Merge pull request #55 from sidi762/master

Added web app demo
This commit is contained in:
ValK 2024-11-09 16:48:20 +08:00 committed by GitHub
commit 0e85bfccab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 2059 additions and 25 deletions

76
.gitignore vendored
View File

@ -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 # Prerequisites
*.d *.d
@ -30,32 +48,58 @@
*.exe *.exe
*.out *.out
*.app *.app
nasal
nasal.exe
# VS C++ sln # Visual Studio specific
*.sln *.sln
*.vcxproj *.vcxproj
*.vcxproj.filters *.vcxproj.filters
*.vcxproj.user *.vcxproj.user
.vs x64/
x64
CMakePresents.json CMakePresents.json
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# nasal executable # CMake
nasal CMakeCache.txt
nasal.exe CMakeFiles/
cmake_install.cmake
install_manifest.txt
CTestTestfile.cmake
_deps/
# misc # Node.js specific (for the web app)
.vscode node_modules/
dump npm-debug.log
yarn-debug.log
yarn-error.log
package-lock.json
# Project specific
dump/
fgfs.log fgfs.log
.temp.* .temp.*
*.ppm
# build dir # Logs and databases
build *.log
out *.sqlite
*.sqlite3
*.db
# macOS special cache directory # OS generated files
.DS_Store .DS_Store
.DS_Store?
# ppm picture generated by ppmgen.nas ._*
*.ppm .Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

View File

@ -100,3 +100,15 @@ target_link_libraries(mat module-used-object)
add_library(nasock SHARED ${CMAKE_SOURCE_DIR}/module/nasocket.cpp) add_library(nasock SHARED ${CMAKE_SOURCE_DIR}/module/nasocket.cpp)
target_include_directories(nasock PRIVATE ${CMAKE_SOURCE_DIR}/src) target_include_directories(nasock PRIVATE ${CMAKE_SOURCE_DIR}/src)
target_link_libraries(nasock module-used-object) 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
)

View File

@ -23,6 +23,7 @@
* [__Trace Back Info__](#trace-back-info) * [__Trace Back Info__](#trace-back-info)
* [__Debugger__](#debugger) * [__Debugger__](#debugger)
* [__REPL__](#repl) * [__REPL__](#repl)
* [__Web Interface__](#web-interface)
__Contact us if having great ideas to share!__ __Contact us if having great ideas to share!__
@ -121,7 +122,7 @@ runtime.windows.set_utf8_output();
![error](./doc/gif/error.gif) ![error](./doc/gif/error.gif)
<details><summary>Must use `var` to define variables</summary> <details><summary>Must use `var` to define variables</summary>
This interpreter uses more strict syntax to make sure it is easier for you to program and debug. 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. 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 ```javascript
code: undefined symbol "i" code: undefined symbol "i"
--> test.nas:1:9 --> test.nas:1:9
| |
1 | foreach(i; [0, 1, 2, 3]) 1 | foreach(i; [0, 1, 2, 3])
| ^ undefined symbol "i" | ^ undefined symbol "i"
code: undefined symbol "i" code: undefined symbol "i"
--> test.nas:2:11 --> test.nas:2:11
| |
2 | print(i) 2 | print(i)
| ^ undefined symbol "i" | ^ undefined symbol "i"
``` ```
@ -441,5 +442,51 @@ Nasal REPL interpreter version 11.1 (Nov 1 2023 23:37:30)
>>> use std.json; >>> use std.json;
{stringify:func(..) {..},parse:func(..) {..}} {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`

View File

@ -26,9 +26,8 @@
__如果有好的意见或建议欢迎联系我们!__ __如果有好的意见或建议欢迎联系我们!__
* __lhk101lhk101@qq.com__ (ValKmjolnir) - __lhk101lhk101@qq.com__ (ValKmjolnir)
- __sidi.liang@gmail.com__ (Sidi)
* __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 界面。
---

View File

@ -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"
}
}

View File

@ -0,0 +1,638 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nasal Interpreter Web Demo</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/theme/monokai.min.css" rel="stylesheet">
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 20px;
padding: 30px;
background: linear-gradient(145deg, #2c3e50, #3498db);
border-radius: 12px;
color: white;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.logo {
margin-bottom: 2px;
}
.ascii-art {
font-family: 'Monaco', monospace;
color: #ecf0f1;
text-shadow: 0 0 10px rgba(255,255,255,0.3);
margin: 0;
line-height: 1.2;
user-select: none;
}
.header h1 {
font-size: 2.5em;
margin: 0 0 10px 0;
font-weight: 700;
background: linear-gradient(to right, #fff, #ecf0f1);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.subtitle {
font-size: 1.2em;
color: #ecf0f1;
margin: 0 0 25px 0;
opacity: 0.9;
}
.credits {
display: flex;
justify-content: center;
gap: 30px;
flex-wrap: wrap;
}
.credit-item {
display: flex;
align-items: center;
gap: 8px;
}
.credit-label {
color: #bdc3c7;
font-size: 0.9em;
}
.credit-link {
text-decoration: none;
padding: 5px 10px;
border-radius: 20px;
background: rgba(255,255,255,0.1);
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 5px;
}
.credit-link:hover {
background: rgba(255,255,255,0.2);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.highlight {
color: #fff;
font-weight: 500;
}
.author {
color: #bdc3c7;
font-size: 0.9em;
}
@media (max-width: 600px) {
.header {
padding: 20px;
}
.header h1 {
font-size: 2em;
}
.credits {
flex-direction: column;
align-items: center;
gap: 15px;
}
}
.editor-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.code-section, .output-section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.CodeMirror {
height: 400px;
border: 1px solid #ddd;
border-radius: 4px;
}
.output-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.status-indicator {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 5px;
}
.status-success {
background-color: #2ecc71;
}
.status-error {
background-color: #e74c3c;
}
#output {
height: 400px;
background: #282c34;
color: #abb2bf;
padding: 10px;
border-radius: 4px;
border: none;
overflow-y: auto;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
line-height: 1.5;
}
.message {
margin: 4px 0;
padding: 8px;
border-radius: 4px;
background: #2c313a;
}
.error-message {
border-left: 3px solid #e06c75;
}
.success-message {
border-left: 3px solid #98c379;
}
.timestamp {
color: #5c6370;
font-size: 0.9em;
margin-right: 8px;
}
.terminal-output {
margin: 8px 0 0 0;
padding: 8px;
background: #21252b;
border-radius: 3px;
white-space: pre-wrap;
overflow-x: auto;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
line-height: 1.5;
}
.terminal-output:empty {
display: none;
}
.message + .message {
margin-top: 8px;
}
.status-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
.status-success {
background-color: #98c379;
}
.status-error {
background-color: #e06c75;
}
.output-header {
display: flex;
align-items: center;
margin-bottom: 10px;
padding: 0 10px;
}
.output-header h2 {
margin: 0;
color: #abb2bf;
}
.controls {
text-align: center;
margin: 20px 0;
}
button {
background-color: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
margin-right: 10px;
}
button:hover {
background-color: #2980b9;
}
button:disabled {
background-color: #bdc3c7;
cursor: not-allowed;
}
.examples {
margin-top: 20px;
}
.example-btn {
background-color: #27ae60;
margin: 0 5px;
}
.example-btn:hover {
background-color: #219a52;
}
.terminal-output {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
margin: 0;
padding: 8px;
background: #2c3e50;
color: #ecf0f1;
border-radius: 4px;
}
.timestamp {
font-size: 0.9em;
color: #7f8c8d;
margin-bottom: 4px;
}
.terminal-red-bold {
color: #ff5555;
font-weight: bold;
}
.terminal-cyan-bold {
color: #8be9fd;
font-weight: bold;
}
.terminal-bold {
font-weight: bold;
}
.terminal-caret {
color: #ff5555;
font-weight: bold;
}
.error-message {
background: #2c3e50;
border-left: 4px solid #e74c3c;
padding: 10px;
margin: 5px 0;
color: #ecf0f1;
}
.success-message {
background: #2c3e50;
border-left: 4px solid #2ecc71;
padding: 10px;
margin: 5px 0;
color: #ecf0f1;
}
#output {
background: #34495e;
color: #ecf0f1;
padding: 15px;
border-radius: 4px;
border: none;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
}
.error-message pre, .success-message pre {
margin: 0;
}
/* Error message styling */
.error-type {
color: #e06c75;
font-weight: bold;
}
.error-desc {
color: #abb2bf;
font-weight: bold;
}
.error-arrow {
color: #56b6c2;
font-weight: bold;
}
.error-file {
color: #e06c75;
font-weight: bold;
}
.error-loc {
color: #abb2bf;
}
.error-line-number {
color: #56b6c2;
user-select: none;
}
.error-code {
color: #abb2bf;
margin-left: 8px;
}
.error-pointer-space {
color: #abb2bf;
white-space: pre;
}
.error-pointer {
color: #e06c75;
font-weight: bold;
}
.terminal-output {
margin: 8px 0 0 0;
padding: 8px;
background: #21252b;
border-radius: 3px;
white-space: pre-wrap;
overflow-x: auto;
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;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">
<pre class="ascii-art">
__ _
/\ \ \__ _ ___ __ _| |
/ \/ / _` / __|/ _` | |
/ /\ / (_| \__ \ (_| | |
\_\ \/ \__,_|___/\__,_|_|
</pre>
</div>
<h1>Nasal Interpreter Web Demo</h1>
<p class="subtitle">Write and execute Nasal code directly in your browser</p>
<div class="credits">
<div class="credit-item">
<span class="credit-label">Powered by</span>
<a href="https://www.fgprc.org.cn/nasal_interpreter.html" class="credit-link">
<span class="highlight">Nasal Interpreter</span>
<span class="author">by ValKmjolnir</span>
</a>
</div>
<div class="credit-item">
<span class="credit-label">Web App by</span>
<a href="https://sidi762.github.io" class="credit-link">
<span class="highlight">LIANG Sidi</span>
</a>
</div>
</div>
</div>
<div class="editor-container">
<div class="code-section">
<h2>Code Editor</h2>
<textarea id="code">var x = 1 + 2;
println(x);</textarea>
</div>
<div class="output-section">
<div class="output-header">
<h2>Output</h2>
<div id="status"></div>
</div>
<div id="output"></div>
</div>
</div>
<div class="controls">
<button id="runBtn" onclick="runCode()">Run</button>
<button onclick="clearOutput()">Clear Output</button>
<label class="timing-checkbox">
<input type="checkbox" id="showTime"> Show execution time
</label>
</div>
<div class="examples">
<h3>Example Programs:</h3>
<button class="example-btn" onclick="loadExample('basic')">Basic Math</button>
<button class="example-btn" onclick="loadExample('loops')">Loops</button>
<button class="example-btn" onclick="loadExample('functions')">Functions</button>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/javascript/javascript.min.js"></script>
<script>
// Initialize CodeMirror
var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
lineNumbers: true,
mode: "javascript", // Using JavaScript mode as it's close enough to Nasal
theme: "monokai",
autoCloseBrackets: true,
matchBrackets: true,
indentUnit: 4,
tabSize: 4,
lineWrapping: true
});
// Example programs
const examples = {
basic: `# Basic math operations
var a = 10;
var b = 5;
println("Addition: ", a + b);
println("Subtraction: ", a - b);
println("Multiplication: ", a * b);
println("Division: ", a / b);`,
loops: `# Loop example
var sum = 0;
for(var i = 1; i <= 5; i += 1) {
sum += i;
println("Current sum: ", sum);
}
println("Final sum: ", sum);`,
functions: `# Function example
var factorial = func(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
for(var i = 0; i <= 5; i += 1) {
println("Factorial of ", i, " is ", factorial(i));
}`
};
function loadExample(type) {
editor.setValue(examples[type]);
}
function formatTerminalOutput(text) {
// Remove the output/error message header if present
text = text.replace(/^\[(.*?)\] (Output|Error):\s*\n/, '');
// Convert ANSI color codes to CSS classes
const colorMap = {
'\u001b[91;1m': '<span class="terminal-red-bold">', // bright red bold
'\u001b[36;1m': '<span class="terminal-cyan-bold">', // bright cyan bold
'\u001b[0m': '</span>', // reset
'\u001b[1m': '<span class="terminal-bold">' // bold
};
// Replace ANSI codes with HTML spans
Object.entries(colorMap).forEach(([code, html]) => {
text = text.replace(new RegExp(escapeRegExp(code), 'g'), html);
});
// Preserve whitespace and arrow formatting
text = text.replace(/\^/g, '<span class="terminal-caret">^</span>');
return text;
}
// Utility function to escape special characters in strings for RegExp
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
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, '<span class="error-type">parse:</span> <span class="error-desc">$1</span>')
// Format the file location
.replace(/^\s*--> (.+?)(\d+):(\d+)$/m, '<span class="error-arrow">--></span> <span class="error-file">$1</span><span class="error-loc">$2:$3</span>')
// Format the code line
.replace(/^(\d+ \|)(.*)$/m, '<span class="error-line-number">$1</span><span class="error-code">$2</span>')
// Format the error pointer
.replace(/^(\s*\|)(\s*)(\^+)$/m, '<span class="error-line-number">$1</span><span class="error-pointer-space">$2</span><span class="error-pointer">$3</span>');
}
async function runCode() {
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;
const code = editor.getValue();
const response = await fetch('/eval', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
code,
showTime
})
});
const data = await response.json();
const timestamp = new Date().toLocaleTimeString();
if (data.error) {
status.innerHTML = '<span class="status-indicator status-error"></span>';
output.innerHTML += `<div class="message error-message">
<span class="timestamp">[${timestamp}]</span>
<pre class="terminal-output">${formatErrorMessage(data.error)}</pre>
</div>`;
} else {
status.innerHTML = '<span class="status-indicator status-success"></span>';
output.innerHTML += `<div class="message success-message">
<span class="timestamp">[${timestamp}]</span>
<pre class="terminal-output">${data.result.trim()}</pre>
</div>`;
}
output.scrollTop = output.scrollHeight;
} catch (err) {
status.innerHTML = '<span class="status-indicator status-error"></span>';
output.innerHTML += `<div class="message error-message">
<span class="timestamp">[${new Date().toLocaleTimeString()}]</span>
<pre class="terminal-output">Server Error: ${err.message}</pre>
</div>`;
} finally {
runBtn.disabled = false;
}
}
function clearOutput() {
document.getElementById('output').innerHTML = '';
document.getElementById('status').innerHTML = '';
}
// Utility function to escape HTML to prevent XSS
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}
</script>
</body>
</html>

View File

@ -0,0 +1,240 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nasal Interpreter Web REPL</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/theme/monokai.min.css" rel="stylesheet">
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #2c3e50;
color: #ecf0f1;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 20px;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.repl-container {
background: #34495e;
padding: 20px;
border-radius: 8px;
height: 600px;
display: flex;
flex-direction: column;
}
.repl-output {
flex-grow: 1;
background: #21252b;
color: #abb2bf;
padding: 10px;
border-radius: 4px;
margin-bottom: 10px;
overflow-y: auto;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
line-height: 1.5;
white-space: pre-wrap;
}
.repl-input-container {
display: flex;
align-items: flex-start;
}
.repl-prompt {
color: #3498db;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
padding: 5px;
user-select: none;
}
.repl-input {
flex-grow: 1;
background: #21252b;
border: none;
color: #abb2bf;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 14px;
padding: 5px;
outline: none;
resize: none;
min-height: 24px;
overflow-y: hidden;
}
.controls {
text-align: center;
margin-top: 20px;
}
button {
background-color: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
margin-right: 10px;
}
button:hover {
background-color: #2980b9;
}
button:disabled {
background-color: #bdc3c7;
cursor: not-allowed;
}
.examples {
margin-top: 20px;
text-align: center;
}
.example-btn {
background-color: #27ae60;
margin: 0 5px;
}
.example-btn:hover {
background-color: #219a52;
}
.output-line {
margin: 2px 0;
min-height: 1.2em;
}
.input-line {
color: #3498db;
white-space: pre;
}
.error-line {
color: #e74c3c;
white-space: pre;
}
.result-line {
color: #2ecc71;
white-space: pre;
margin-left: 4px; /* Slight indent for results */
}
.system-message {
color: #7f8c8d;
font-style: italic;
}
.help-text {
color: #95a5a6;
white-space: pre;
font-family: monospace;
}
.error-type {
color: #ff5f5f;
font-weight: bold;
}
.error-desc {
color: #abb2bf;
font-weight: bold;
}
.error-arrow, .error-line-number {
color: #56b6c2;
font-weight: bold;
}
.error-file {
color: #ff5f5f;
font-weight: bold;
}
.error-code {
color: #abb2bf;
}
.error-pointer-space {
white-space: pre;
}
.error-pointer {
color: #ff5f5f;
font-weight: bold;
}
.error-red-bold {
color: #e06c75;
font-weight: bold;
}
.error-cyan-bold {
color: #56b6c2;
font-weight: bold;
}
.error-bold {
font-weight: bold;
}
@media (max-width: 768px) {
.repl-container {
height: 400px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Nasal Interpreter Web REPL</h1>
<p>Interactive Read-Eval-Print Loop for Nasal</p>
<p>Powered by ValKmjolnir's <a href="https://www.fgprc.org.cn/nasal_interpreter.html">Nasal Interpreter</a>, Web App by <a href="https://sidi762.github.io">LIANG Sidi</a></p>
</div>
<div class="repl-container">
<div id="repl-output" class="repl-output">
<div class="output-line">Welcome to Nasal Web REPL Demo!</div>
</div>
<div class="repl-input-container">
<span class="repl-prompt">>>></span>
<textarea id="repl-input" class="repl-input" rows="1"
placeholder="Enter Nasal code here..."></textarea>
</div>
</div>
<div class="controls">
<button onclick="clearREPL()">Clear REPL</button>
</div>
<div class="examples">
<h3>Example Commands:</h3>
<button class="example-btn" onclick="insertExample('basic')">Basic Math</button>
<button class="example-btn" onclick="insertExample('loops')">Loops</button>
<button class="example-btn" onclick="insertExample('functions')">Functions</button>
</div>
</div>
<script src="repl.js"></script>
</body>
</html>

View File

@ -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, '<span class="error-type">parse:</span> <span class="error-desc">$1</span>')
// Format the file location
.replace(/^\s*--> (.+?)(\d+):(\d+)$/m, '<span class="error-arrow">--></span> <span class="error-file">$1</span><span class="error-loc">$2:$3</span>')
// Format the code line
.replace(/^(\d+ \|)(.*)$/m, '<span class="error-line-number">$1</span><span class="error-code">$2</span>')
// Format the error pointer
.replace(/^(\s*\|)(\s*)(\^+)$/m, '<span class="error-line-number">$1</span><span class="error-pointer-space">$2</span><span class="error-pointer">$3</span>');
}
function handleResult(result) {
const lines = result.split('\n');
lines.forEach(line => {
if (line.trim()) {
appendOutput(line.trim(), 'result-line');
}
});
}

94
nasal-web-app/server.js Normal file
View File

@ -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');
});

View File

@ -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');
});

1
nasal-web-app/std Symbolic link
View File

@ -0,0 +1 @@
../std

367
src/nasal_web.cpp Normal file
View File

@ -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 <string>
#include <sstream>
#include <fstream>
#include <cstdlib>
#include <cstdio>
#include <stdexcept>
#include <chrono>
#include <vector>
#include <future>
namespace {
// Helper function implementations inside anonymous namespace
std::vector<std::string> split_string(const std::string& str, char delim) {
std::vector<std::string> 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<std::string>& 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<nasal::vm> 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<nasal::vm>();
}
};
struct WebReplContext {
std::unique_ptr<nasal::repl::repl> repl_instance;
std::vector<std::string> 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<nasal::repl::repl>();
}
};
void* nasal_init() {
return new NasalContext();
}
void nasal_cleanup(void* context) {
delete static_cast<NasalContext*>(context);
}
// Add new function to set timeout
void nasal_set_timeout(void* context, int seconds) {
auto* ctx = static_cast<NasalContext*>(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<NasalContext*>(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<nasal::optimizer>();
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<double>((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<NasalContext*>(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<WebReplContext*>(context);
}
// Add new function to set REPL timeout
void nasal_repl_set_timeout(void* context, int seconds) {
auto* ctx = static_cast<WebReplContext*>(context);
ctx->timeout = std::chrono::seconds(seconds);
}
const char* nasal_repl_eval(void* context, const char* line) {
auto* ctx = static_cast<WebReplContext*>(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<WebReplContext*>(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<std::string> 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();
}

35
src/nasal_web.h Normal file
View File

@ -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

View File

@ -34,6 +34,14 @@ void repl::update_temp_file() {
info::instance()->repl_file_source = content + " "; info::instance()->repl_file_source = content + " ";
} }
void repl::update_temp_file(const std::vector<std::string>& 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() { bool repl::check_need_more_input() {
while(true) { while(true) {
update_temp_file(); update_temp_file();
@ -67,6 +75,34 @@ bool repl::check_need_more_input() {
return true; return true;
} }
int repl::check_need_more_input(std::vector<std::string>& src) {
update_temp_file(src);
auto nasal_lexer = std::make_unique<lexer>();
if (nasal_lexer->scan("<nasal-repl>").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() { void repl::help() {
std::cout << ".h, .help | show help\n"; std::cout << ".h, .help | show help\n";
std::cout << ".e, .exit | quit the REPL\n"; std::cout << ".e, .exit | quit the REPL\n";
@ -150,7 +186,7 @@ void repl::execute() {
std::cout << "\", input \".help\" for help\n"; std::cout << "\", input \".help\" for help\n";
continue; continue;
} }
source.push_back(line); source.push_back(line);
if (!check_need_more_input()) { if (!check_need_more_input()) {
source.pop_back(); source.pop_back();

View File

@ -36,8 +36,8 @@ private:
std::string readline(const std::string&); std::string readline(const std::string&);
bool check_need_more_input(); bool check_need_more_input();
void update_temp_file(); void update_temp_file();
void update_temp_file(const std::vector<std::string>& src);
void help(); void help();
bool run();
public: public:
repl() { repl() {
@ -48,7 +48,20 @@ public:
// set empty history // set empty history
command_history = {""}; command_history = {""};
} }
// Make these methods public for web REPL
bool run();
void execute(); void execute();
int check_need_more_input(std::vector<std::string>& src);
// Add method to access source
void set_source(const std::vector<std::string>& src) {
source = src;
}
// Add method to access runtime
vm& get_runtime() {
return runtime;
}
}; };
} }