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
*.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
.DS_Store?
._*
.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)
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
)

View File

@ -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)
<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.
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`

View File

@ -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 界面。
---

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 + " ";
}
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() {
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<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() {
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();

View File

@ -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<std::string>& 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<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;
}
};
}