commit
0e85bfccab
|
@ -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
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
55
README.md
55
README.md
|
@ -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`
|
||||||
|
|
|
@ -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 界面。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -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>
|
|
@ -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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -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');
|
||||||
|
});
|
|
@ -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');
|
||||||
|
});
|
|
@ -0,0 +1 @@
|
||||||
|
../std
|
|
@ -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();
|
||||||
|
}
|
|
@ -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
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue